const MESSAGE_TYPE = { UPDATE_OBJECT: 'update.object', KILL_OBJECT: 'kill.object', ACTION_START: 'action.start', ACTION_STOP: 'action.stop' } export default class PhaserWebsocketMultiplayerPlugin extends Phaser.Plugins.BasePlugin { constructor(pluginManager) { super('PhaserWebsocketMultiplayerPlugin', pluginManager); this.game = pluginManager.game; this.event = new Phaser.Events.EventEmitter(); this.socket = null; this.id = ((1<<24)*Math.random() | 0).toString(16); this.name = null; this.localObject = null; this.featureExtractor = null; this.broadcastInterval = null; this.checkTimeoutsInterval = null; this.objectRegistry = {}; this.objectLastseen = {}; this.config = { url: null, // the url of the websocket broadcastInterval: 200, // the interval in milliseconds in which the state of the tracked object is broadcasted pauseTimeout: 5000, // the time (milliseconds) after which a remote object becomes inactive deadTimeout: 15000, // the time after which a remote object is removed checkTimeoutsInterval: 100, // the interval in milliseconds how oft remote objects are checked autoConnect: false, // if the connection should be established automatically debug: false // if the debug mode is on }; } init(config = {}) { this.config = Object.assign(this.config, config); if(this.config.autoConnect) this.connect(); } connect(url = '') { if(url == '') url = this.config.url; this.log('trying to connect'); this.socket = new WebSocket(url); this.socket.addEventListener('open', this.onSocketOpen.bind(this)); this.socket.addEventListener('message', this.onSocketMessage.bind(this)); this.socket.addEventListener('close', this.onSocketClose.bind(this)); this.socket.addEventListener('error', this.onSocketError.bind(this)); } onSocketOpen(event) { this.log('socket open') this.event.emit('socket.open', event); this.checkTimeoutsInterval = setInterval(this.checkTimeouts.bind(this), this.config.checkTimeoutsInterval); } onSocketMessage(event) { let data = JSON.parse(event.data); if(data.id == this.id) return; switch(data.type) { case MESSAGE_TYPE.UPDATE_OBJECT: this.updateObject(data); break; case MESSAGE_TYPE.KILL_OBJECT: this.killObject(data.id); break; case MESSAGE_TYPE.ACTION_START: let objects = []; for(let i = 0; i < data.objects.length; i++) { if(this.objectRegistry[data.objects[i]]) objects.push(this.objectRegistry[data.objects[i]]); } this.event.emit('action.start.' + data.actionType, data.id, objects); break; case MESSAGE_TYPE.ACTION_STOP: this.event.emit('action.stop.' + data.actionType, data.id); break; } } onSocketError(event) { this.event.emit('socket.error', event); } onSocketClose(event) { clearInterval(this.checkTimeoutsInterval); this.stopBroadcast(); this.event.emit('socket.close', event); } checkTimeouts() { let currentTime = (new Date()).getTime(); Object.entries(this.objectLastseen).forEach(([key, value]) => { if(currentTime - value > this.config.pauseTimeout) this.pauseObject(key); if(currentTime - value > this.config.deadTimeout) this.killObject(key); }); } setName(name) { this.name = name; } registerObject(id, object) { this.objectRegistry[id] = object; object.setData('id', id); } pauseObject(id) { this.event.emit('object.pause', this.objectRegistry[id], id); } killObject(id) { this.event.emit('object.kill', this.objectRegistry[id], id); delete this.objectRegistry[id]; delete this.objectLastseen[id]; } updateObject(data) { if(!this.objectRegistry[data.id]) { this.objectRegistry[data.id] = true; this.event.emit('object.create', data.data, data.id); this.log('create', data.data); } if(this.objectRegistry[data.id] && this.objectRegistry[data.id] !== true) this.event.emit('object.update', this.objectRegistry[data.id], data.data, data.id); this.objectLastseen[data.id] = (new Date()).getTime(); } track(object, featureExtractor) { this.localObject = object; object.setData('id', this.id); this.featureExtractor = featureExtractor; this.registerObject(this.id, object); } startBroadcast() { this.broadcastInterval = setInterval( () => { this.broadcast(); }, this.config.broadcastInterval ); } broadcast() { this.socket.send(JSON.stringify({ type: MESSAGE_TYPE.UPDATE_OBJECT, id: this.id, data: this.featureExtractor(this.localObject) })); } stopBroadcast() { clearInterval(this.broadcastInterval); } startAction(actionType = 'generic', objects = []) { this.socket.send(JSON.stringify({ id: this.id, type: MESSAGE_TYPE.ACTION_START, actionType: actionType, objects: objects })); } stopAction(actionType = 'generic') { this.socket.send(JSON.stringify({ id: this.id, type: MESSAGE_TYPE.ACTION_STOP, actionType: actionType })); } log(msg, data = ' ') { if(this.config.debug) console.log('WEBSOCKET MULTIPLAYER: ' + msg, data); } }