server/src/world/physics.service.ts

114 lines
3.4 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { Humanoid, PhysicsObject, World } from 'src/game';
import RAPIER from '@dimforge/rapier3d-compat';
import { Packet } from 'src/net/packet';
import { PacketType } from 'src/types/packet-type.enum';
import { Object3D, Quaternion, SkinnedMesh, Vector3 } from 'three';
import { GameObject } from 'src/game/game-object';
import { PhysicsTicking } from 'src/physics';
@Injectable()
export class PhysicsService {
private world!: World;
private physicsWorld!: RAPIER.World;
private characterPhysics!: RAPIER.KinematicCharacterController;
private running = false;
private sceneInitialized = false;
private physicsTicker: ReturnType<typeof setTimeout>;
private trackedObjects: PhysicsTicking[] = [];
loop() {
if (!this.running) return;
this.physicsTicker = setTimeout(() => this.loop(), 20);
this.physicsWorld.step();
for (const object of this.trackedObjects) object.tick(0.02); // TODO: DT
}
async start(world: World) {
this.world = world;
await RAPIER.init();
this.physicsWorld = new RAPIER.World(new Vector3(0, this.world.gravity, 0));
this.running = true;
this.initializePhysicsScene();
this.loop();
}
stop() {
this.running = false;
}
getObjectPackets() {
const data: Buffer[] = [];
for (const object of this.trackedObjects) {
const body = (object as PhysicsObject)?.rigidBody;
if (!body) continue;
data.push(
new Packet(PacketType.STREAM_TRANSFORM)
.write(object.uuid, String)
.write(body.translation(), 'vec3')
.write(body.rotation(), 'quat')
.write(body.linvel(), 'vec3')
.write(body.angvel(), 'vec3')
.toBuffer(),
);
}
return data;
}
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(RAPIER, 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;
// Initialize object physics
object.initialize(RAPIER, this.physicsWorld);
this.trackedObjects.push(object);
});
}
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;
}
}