143 lines
4.5 KiB
TypeScript
143 lines
4.5 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 type Rapier from '@dimforge/rapier3d';
|
|
import { GameObject } 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 initEventFiredBeforeEngineLoad = false;
|
|
private cleanUpEvents?: Function;
|
|
private sceneInitialized = false;
|
|
private trackedObjects: PhysicsTicking[] = [];
|
|
|
|
initialize(): void {
|
|
this.world = this.renderer.scene.getObjectByName('World') as World;
|
|
this.cleanUpEvents = this.bindEvents();
|
|
this.createRapier();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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;
|
|
|
|
if (this.initEventFiredBeforeEngineLoad) {
|
|
this.initEventFiredBeforeEngineLoad = false;
|
|
this.initializePhysicsScene();
|
|
}
|
|
});
|
|
}
|
|
|
|
private bindEvents() {
|
|
const initializeEvent = () => {
|
|
if (!this.physicsWorld) {
|
|
this.initEventFiredBeforeEngineLoad = true;
|
|
return;
|
|
}
|
|
this.initializePhysicsScene();
|
|
};
|
|
|
|
const sceneJoinEvent = (object: Object3D) => {
|
|
this.applyPhysics(object);
|
|
};
|
|
|
|
const sceneLeaveEvent = (object: Object3D) => {
|
|
this.removePhysics(object);
|
|
};
|
|
|
|
this.events.addListener('initialized', initializeEvent);
|
|
this.events.addListener('sceneJoin', sceneJoinEvent);
|
|
this.events.addListener('sceneLeave', sceneLeaveEvent);
|
|
|
|
return () => {
|
|
this.events.removeEventListener('initialized', initializeEvent);
|
|
this.events.removeEventListener('sceneJoin', sceneJoinEvent);
|
|
this.events.removeEventListener('sceneLeave', sceneLeaveEvent);
|
|
};
|
|
}
|
|
|
|
private applyPhysics(root: Object3D) {
|
|
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;
|
|
}
|
|
}
|