freeblox/packages/engine/src/components/physicsworld.ts

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;
}
}