151 lines
5.0 KiB
TypeScript
151 lines
5.0 KiB
TypeScript
import { Object3D, SkinnedMesh, Vector3 } from 'three';
|
|
import { EngineComponent } from '../types/engine-component';
|
|
import { World } from '../gameobjects/world.object';
|
|
import { getRapier, PhysicsTicking } from '../physics';
|
|
import { Humanoid } from '../gameobjects/humanoid.object';
|
|
import { PhysicsObject } from '../gameobjects/physics.object';
|
|
import { GameObject } from '../types/game-object';
|
|
import type Rapier from '@dimforge/rapier3d';
|
|
import { ServerTransformEvent } from '..';
|
|
|
|
export class PhysicsWorldComponent extends EngineComponent {
|
|
public name = PhysicsWorldComponent.name;
|
|
private world!: World;
|
|
private physicsWorld!: Rapier.World;
|
|
private physicsEngine!: typeof Rapier;
|
|
private characterPhysics!: Rapier.KinematicCharacterController;
|
|
private cleanUpEvents?: Function;
|
|
private sceneInitialized = false;
|
|
private trackedObjects: PhysicsTicking[] = [];
|
|
|
|
initialize(): void {
|
|
this.world = this.renderer.scene.getObjectByName('World') as World;
|
|
this.cleanUpEvents = this.bindEvents();
|
|
}
|
|
|
|
update(delta: number): void {
|
|
// FIXME: physics is tied to the FPS
|
|
this.physicsWorld?.step();
|
|
for (const object of this.trackedObjects) object.tick(delta);
|
|
}
|
|
|
|
dispose(): void {
|
|
this.cleanUpEvents?.call(this);
|
|
this.physicsWorld.removeCharacterController(this.characterPhysics);
|
|
for (const object of this.trackedObjects) object.dispose();
|
|
this.physicsWorld.free();
|
|
}
|
|
|
|
private createRapier() {
|
|
getRapier().then((physicsEngine) => {
|
|
const gravity = new Vector3(0, this.world.gravity, 0);
|
|
const world = new physicsEngine.World(gravity);
|
|
this.physicsWorld = world;
|
|
this.physicsEngine = physicsEngine;
|
|
this.initializePhysicsScene();
|
|
});
|
|
}
|
|
|
|
private bindEvents() {
|
|
const worldLoadEvent = () => {
|
|
this.createRapier();
|
|
};
|
|
|
|
const sceneJoinEvent = (object: Object3D) => {
|
|
this.applyPhysics(object);
|
|
};
|
|
|
|
const sceneLeaveEvent = (object: Object3D) => {
|
|
this.removePhysics(object);
|
|
};
|
|
|
|
const serverTransformEvent = (event: ServerTransformEvent) => {
|
|
const object = this.trackedObjects.find(
|
|
(obj) => event.object === obj.uuid
|
|
) as PhysicsObject;
|
|
if (!object) return;
|
|
object.rigidBody?.setTranslation(event.position, false);
|
|
object.rigidBody?.setRotation(event.quaternion, false);
|
|
event.velocity && object.rigidBody?.setLinvel(event.velocity, false);
|
|
event.angularVelocity &&
|
|
object.rigidBody?.setAngvel(event.angularVelocity, false);
|
|
};
|
|
|
|
this.events.addListener('loadComplete', worldLoadEvent);
|
|
this.events.addListener('sceneJoin', sceneJoinEvent);
|
|
this.events.addListener('sceneLeave', sceneLeaveEvent);
|
|
this.events.addListener('serverTransform', serverTransformEvent);
|
|
|
|
return () => {
|
|
this.events.removeEventListener('loadComplete', worldLoadEvent);
|
|
this.events.removeEventListener('sceneJoin', sceneJoinEvent);
|
|
this.events.removeEventListener('sceneLeave', sceneLeaveEvent);
|
|
this.events.removeEventListener('serverTransform', serverTransformEvent);
|
|
};
|
|
}
|
|
|
|
private applyPhysics(root: Object3D) {
|
|
if (!this.physicsEngine) return;
|
|
root.traverse((object) => {
|
|
// Prevent double-init
|
|
if (this.trackedObjects.some((entry) => entry.uuid === object.uuid))
|
|
return;
|
|
|
|
// Initialize humanoids
|
|
if (object instanceof Humanoid) {
|
|
object.initialize(
|
|
this.physicsEngine,
|
|
this.physicsWorld,
|
|
this.characterPhysics
|
|
);
|
|
this.trackedObjects.push(object);
|
|
return;
|
|
}
|
|
|
|
// Only track physics object instances
|
|
if (!(object instanceof PhysicsObject)) return;
|
|
|
|
// Do not add physics to virtual objects
|
|
if ((object as GameObject).virtual) return;
|
|
|
|
// Do not apply physics to animated objects
|
|
if ((object.getMesh() as SkinnedMesh).skeleton) return;
|
|
|
|
// Initialize object physics
|
|
object.initialize(this.physicsEngine, this.physicsWorld);
|
|
this.trackedObjects.push(object);
|
|
});
|
|
}
|
|
|
|
private removePhysics(root: Object3D) {
|
|
const trackedObjects = [...this.trackedObjects];
|
|
const physicsLeaveUUIDs: string[] = [];
|
|
|
|
root.traverse((object) => {
|
|
if (trackedObjects.some((item) => item.uuid === object.uuid)) {
|
|
physicsLeaveUUIDs.push(object.uuid);
|
|
}
|
|
});
|
|
|
|
this.trackedObjects = this.trackedObjects.filter((object) =>
|
|
physicsLeaveUUIDs.includes(object.uuid)
|
|
);
|
|
}
|
|
|
|
private async initializePhysicsScene() {
|
|
if (this.sceneInitialized) return;
|
|
|
|
this.characterPhysics = this.physicsWorld.createCharacterController(0.01);
|
|
this.characterPhysics.setApplyImpulsesToDynamicBodies(true);
|
|
this.characterPhysics.setMaxSlopeClimbAngle((75 * Math.PI) / 180);
|
|
this.characterPhysics.enableAutostep(1, 0.5, true);
|
|
// Automatically slide down on slopes smaller than 30 degrees.
|
|
// this.characterPhysics.setMinSlopeSlideAngle((30 * Math.PI) / 180);
|
|
|
|
this.applyPhysics(this.world);
|
|
|
|
this.sceneInitialized = true;
|
|
this.events.emit('physicsComplete');
|
|
}
|
|
}
|