colyseus
Version:
Multiplayer Game Server for Node.js.
208 lines (156 loc) • 6.07 kB
text/typescript
import * as msgpack from "msgpack-lite";
import * as fossilDelta from "fossil-delta";
import * as shortid from "shortid";
import ClockTimer from "clock-timer.js";
import { EventEmitter } from "events";
import { createTimeline, Timeline } from "timeframe";
import { Client } from "./index";
import { Protocol } from "./Protocol";
import { logError, spliceOne, toJSON } from "./Utils";
export abstract class Room<T> extends EventEmitter {
public clock: ClockTimer = new ClockTimer();
public timeline?: Timeline;
public roomId: number;
public roomName: string;
public clients: Client[] = [];
public options: any;
public state: T;
// when a new user connects, it receives the '_previousState', which holds
// the last binary snapshot other users already have, therefore the patches
// that follow will be the same for all clients.
protected _previousState: any;
protected _previousStateEncoded: any;
private _simulationInterval: NodeJS.Timer;
private _patchInterval: number;
constructor ( options: any = {} ) {
super()
this.roomId = options.roomId;
this.roomName = options.roomName;
this.options = options;
// Default patch rate is 20fps (50ms)
this.setPatchRate( 1000 / 20 );
}
abstract onMessage (client: Client, data: any): void;
abstract onJoin (client: Client, options?: any): void;
abstract onLeave (client: Client): void;
abstract onDispose (): void;
public requestJoin (options: any): boolean {
return true;
}
public setSimulationInterval ( callback: Function, delay: number = 1000 / 60 ): void {
// clear previous interval in case called setSimulationInterval more than once
if ( this._simulationInterval ) clearInterval( this._simulationInterval );
this._simulationInterval = setInterval( () => {
this.clock.tick();
callback();
}, delay );
}
public setPatchRate ( milliseconds: number ): void {
// clear previous interval in case called setPatchRate more than once
if ( this._patchInterval ) clearInterval(this._patchInterval);
this._patchInterval = setInterval( this.broadcastPatch.bind(this), milliseconds );
}
public useTimeline ( maxSnapshots: number = 10 ): void {
this.timeline = createTimeline( maxSnapshots );
}
public setState (newState) {
this.clock.start();
// ensure state is populated for `sendState()` method.
this._previousState = toJSON( newState );
this._previousStateEncoded = msgpack.encode( this._previousState );
this.state = newState;
if ( this.timeline ) {
this.timeline.takeSnapshot( this.state );
}
}
public lock (): void {
this.emit('lock');
}
public unlock (): void {
this.emit('unlock');
}
public send (client: Client, data: any): void {
client.send( msgpack.encode( [Protocol.ROOM_DATA, this.roomId, data] ), { binary: true }, logError.bind(this) );
}
public broadcast (data: any): boolean {
// no data given, try to broadcast patched state
if (!data) {
throw new Error("Room#broadcast: 'data' is required to broadcast.");
}
// encode all messages with msgpack
if (!(data instanceof Buffer)) {
data = msgpack.encode([Protocol.ROOM_DATA, this.roomId, data]);
}
var numClients = this.clients.length;
while (numClients--) {
this.clients[ numClients ].send(data, { binary: true }, logError.bind(this) );
}
return true;
}
public disconnect (): void {
var i = this.clients.length;
while (i--) {
this._onLeave(this.clients[i]);
}
}
protected sendState (client: Client): void {
client.send( msgpack.encode( [
Protocol.ROOM_STATE,
this.roomId,
this._previousState,
this.clock.currentTime,
this.clock.elapsedTime,
] ), {
binary: true
}, logError.bind(this) );
}
private broadcastPatch (): boolean {
if ( !this._previousState ) {
throw new Error( 'trying to broadcast null state. you should call #setState on constructor or during user connection.' );
}
let currentState = toJSON( this.state );
let currentStateEncoded = msgpack.encode( currentState );
// skip if state has not changed.
if ( currentStateEncoded.equals( this._previousStateEncoded ) ) {
return false;
}
let patches = fossilDelta.create( this._previousStateEncoded, currentStateEncoded );
// take a snapshot of the current state
if (this.timeline) {
this.timeline.takeSnapshot( this.state, this.clock.elapsedTime );
}
this._previousState = currentState;
this._previousStateEncoded = currentStateEncoded;
// broadcast patches (diff state) to all clients,
// even if nothing has changed in order to calculate PING on client-side
return this.broadcast( msgpack.encode([ Protocol.ROOM_STATE_PATCH, this.roomId, patches ]) );
}
private _onJoin (client: Client, options?: any): void {
this.clients.push( client );
// confirm room id that matches the room name requested to join
client.send( msgpack.encode( [Protocol.JOIN_ROOM, this.roomId, this.roomName] ), { binary: true }, logError.bind(this) );
// send current state when new client joins the room
if (this.state) {
this.sendState(client);
}
if (this.onJoin) {
this.onJoin(client, options);
}
}
private _onLeave (client: Client, isDisconnect: boolean = false): void {
// remove client from client list
spliceOne(this.clients, this.clients.indexOf(client));
if (this.onLeave) this.onLeave(client);
this.emit('leave', client, isDisconnect);
if (!isDisconnect) {
client.send( msgpack.encode( [Protocol.LEAVE_ROOM, this.roomId] ), { binary: true }, logError.bind(this) );
}
// custom cleanup method & clear intervals
if ( this.clients.length == 0 ) {
if ( this.onDispose ) this.onDispose();
if ( this._patchInterval ) clearInterval( this._patchInterval );
if ( this._simulationInterval ) clearInterval( this._simulationInterval );
this.emit('dispose');
}
}
}