114 lines
3.4 KiB
TypeScript
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;
|
|
}
|
|
}
|