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

176 lines
5.9 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 { PlayerEvent, ServerTransformEvent } from '../types/events';
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[] = [];
private physicsTick?: ReturnType<typeof setInterval>;
initialize(): void {
this.world = this.renderer.scene.getObjectByName('World') as World;
this.cleanUpEvents = this.bindEvents();
}
update(delta: number): void {
if (!this.physicsWorld) return;
// FIXME: physics is tied to the FPS
this.physicsWorld.timestep = delta;
this.physicsWorld?.step();
for (const object of this.trackedObjects) object.tickPhysics(delta);
}
dispose(): void {
clearInterval(this.physicsTick);
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.physicsWorld.timestep = 1 / 60;
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 || !object.rigidBody) return;
if (object instanceof Humanoid) {
object.synchronize(event);
} else {
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);
}
};
const characterMoveEvent = (event: PlayerEvent) => {
const object = this.trackedObjects.find(
(obj) => event.playerId === obj.uuid
) as Humanoid;
if (!object) return;
object.setVelocity(event.velocity);
object.setLook(event.lookAt);
if (event.jump) object.jump();
};
this.events.addListener('loadComplete', worldLoadEvent);
this.events.addListener('sceneJoin', sceneJoinEvent);
this.events.addListener('sceneLeave', sceneLeaveEvent);
this.events.addListener('serverTransform', serverTransformEvent);
this.events.addListener('moveCharacter', characterMoveEvent);
return () => {
this.events.removeEventListener('loadComplete', worldLoadEvent);
this.events.removeEventListener('sceneJoin', sceneJoinEvent);
this.events.removeEventListener('sceneLeave', sceneLeaveEvent);
this.events.removeEventListener('serverTransform', serverTransformEvent);
this.events.removeEventListener('moveCharacter', characterMoveEvent);
};
}
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.initializePhysics(
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.initializePhysics(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);
if ((object as PhysicsObject).dispose) {
(object as PhysicsObject).dispose();
}
}
});
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');
}
}