UNPKG

wsmini

Version:

Minimalist WebSocket client and server for real-time applications with RPC, PubSub, Rooms and Game state synchronization.

150 lines (124 loc) 3.88 kB
import WSServerRoom from "./WSServerRoom.mjs"; export default class WSServerGameRoom extends WSServerRoom { constructor(name, wsServer) { super(name, wsServer); // Manage a set to handle all the callbacks with the same loop // thus avoiding to create a setTimeout for each callback this.registredUpdate = new Set(); this.unregisterPatch = () => {}; this.setSimulationPerSec(60); this.setPatchPerSec(20); this.lastTickTime = 0; this.isRunning = false; this.loop = null; this.frameDelta = 0; this.elapsedTime = 0; } onTick(deltaTime, elapsedTime) { // to be overriden } onPatch(deltaTime, elapsedTime) { // to be overriden return {}; } setSimulationPerSec(updatePerSec) { this.updatePerSec = updatePerSec; this.timestep = 1000 / this.updatePerSec; } setSimulationStep(timestep) { this.setSimulationPerSec(1000 / timestep); } setPatchPerSec(patchPerSec) { this.patchPerSec = patchPerSec; this.patchTimestep = 1000 / this.patchPerSec; this.unregisterPatch(); this.unregisterPatch = this.registerThrottle((dt, t) => this.patch(dt, t), this.patchTimestep); } setPatchTimestep(patchTimestep) { this.setPatchPerSec(1000 / patchTimestep); } hrtimeMs() { const time = process.hrtime(); return time[0] * 1000 + time[1] / 1000000; } startMainLoop() { if (this.isRunning) return; this.isRunning = true; this.lastTickTime = this.hrtimeMs(); this.loop = setTimeout(() => this._tick(), 0); } stopMainLoop() { if (!this.isRunning) return; clearTimeout(this.loop); this.isRunning = false; } patch(deltaTime, elapsedTime) { const worldState = this.onPatch(deltaTime, elapsedTime); this.broadcast(worldState); } register(callback) { this.registredUpdate.add(callback); return () => this.unregister(callback); } registerThrottle(callback, delay) { let elapsed = 0; const callbackThrottle = (dt, t) => { elapsed += dt; if (elapsed < delay) return; callback(elapsed, t); elapsed -= delay; }; return this.register(callbackThrottle); } registerTimeout(callback, delay) { let elapsed = 0; let unregister = null; const callbackTimeout = (dt, t) => { elapsed += dt; if (elapsed < delay) return; callback(elapsed, t); unregister(); }; unregister = this.register(callbackTimeout); return unregister; } unregister(callback) { return this.registredUpdate.delete(callback); } resetFrameDelta() { this.frameDelta = 0; } panic(frameDelta) { // to be overriden if you want to handle the panic case // by default we just reset the frameDelta this.resetFrameDelta(); } _tick() { const now = this.hrtimeMs(); const deltaTime = now - this.lastTickTime; this.timeout = Math.max(0, this.timestep - deltaTime); this.loop = setTimeout(() => this._tick(), this.timeout); this.frameDelta += deltaTime; this.lastTickTime = now; // Fixed simulation steps let numUpdate = 0; while (this.frameDelta >= this.timestep && numUpdate <= this.updatePerSec) { this.elapsedTime += this.timestep; for (const callback of this.registredUpdate) { callback(this.timestep, this.elapsedTime); } this.onTick(this.timestep, this.elapsedTime); this.frameDelta -= this.timestep; numUpdate++; } // We run the update more than 1 second ! if (numUpdate > this.updatePerSec) { this.wsServer.log(`Game '${this.name}' is running slow`); this.panic(this.frameDelta); } } dispose() { this.stopMainLoop(); this.registredUpdate.clear(); } }