PageRenderTime 282ms CodeModel.GetById 131ms app.highlight 19ms RepoModel.GetById 63ms app.codeStats 1ms

/js/network/ClientNetChannel.js

http://github.com/onedayitwillmake/RealtimeMultiplayerNodeJs
JavaScript | 392 lines | 215 code | 61 blank | 116 comment | 29 complexity | b26b7d28ab9941c3a0ec28f81a8a1f38 MD5 | raw file
  1/**
  2 File:
  3 ClientNetChannel.js
  4 Created By:
  5 Mario Gonzalez
  6 Project:
  7 RealtimeMultiplayerNodeJS
  8 Abstract:
  9 Communicates with the server and stores rolling world-entity-descriptions
 10
 11 -> GameController talks to this object
 12 <--> ClientNetChannel waits to be ready, when it is
 13 <-- ClientNetChannel talks to the ServerNetChannel
 14 <--> ServerNetChannel does some stuff
 15 --> ServerNetChannel talks to ClientNetChannel
 16 --> ClientNetChannel talks to the GameController  --^
 17
 18 Basic Usage:
 19 Create an object that conforms to the following protocol
 20 netChannelDidConnect();
 21 netChannelDidReceiveMessage();
 22 netChannelDidDisconnect();
 23 */
 24(function () {
 25    var BUFFER_MASK = RealtimeMultiplayerGame.Constants.CLIENT_SETTING.MAX_BUFFER;
 26
 27    // Retrieve the namespace
 28    RealtimeMultiplayerGame.namespace("RealtimeMultiplayerGame.network");
 29
 30    RealtimeMultiplayerGame.ClientNetChannel = function (aDelegate) {
 31        this.setDelegate(aDelegate);
 32        this.setupSocketIO();
 33//		this.setupWSClient();
 34        this.setupCmdMap();
 35        return this;
 36    };
 37
 38    RealtimeMultiplayerGame.ClientNetChannel.prototype = {
 39        delegate: null,				// Object informed when ClientNetChannel does interesting stuff
 40        socketio: null,				// Reference to singluar Socket.IO instance
 41        clientid: null,				// A client id is set by the server on first connect
 42
 43        // Settings
 44        cl_updateRate: RealtimeMultiplayerGame.Constants.CLIENT_SETTING.CMD_RATE,		// How often we can receive messages per sec
 45
 46        // connection info
 47        latency: 1000,				// Current latency time from server
 48        lastSentTime: -1,				// Time of last sent message
 49        lastRecievedTime: -1,				// Time of last recieved message
 50
 51        // Data
 52        messageBuffer: [],				// Store last N messages to be sent
 53        outgoingSequenceNumber: 0,
 54        incomingWorldUpdateBuffer: [],				// Store last N received WorldDescriptions
 55        reliableBuffer: null,				// We sent a 'reliable' message and are waiting for acknowledgement that it was sent
 56
 57        cmdMap: {},				// Map the CMD constants to functions
 58
 59
 60        setupSocketIO: function () {
 61            debugger;
 62            this.socketio = new io.connect(RealtimeMultiplayerGame.Constants.SERVER_SETTING.GET_URI(), {transports: ['websocket', 'xhr-polling', 'jsonp-polling'], reconnect: false, rememberTransport: false});
 63
 64            var that = this;
 65            this.socketio.on('connect', function () {
 66                that.onSocketConnect()
 67            });
 68            this.socketio.on('message', function (obj) {
 69                that.onSocketDidAcceptConnection(obj)
 70            });
 71            this.socketio.on('disconnect', function () {
 72                that.onSocketDisconnect()
 73            });
 74        },
 75
 76        setupWSClient: function () {
 77            var that = this;
 78            this.connection = new WebSocket("ws://localhost:" + RealtimeMultiplayerGame.Constants.SERVER_SETTING.SOCKET_PORT + "/");
 79            this.socketio = this.connection;
 80            this.connection.onopen = function () {
 81                DemoHelloWorld.DemoClientGame.prototype.log("Connection.onopen");
 82            };
 83
 84            this.connection.onmessage = function (event) {
 85                //DemoHelloWorld.DemoClientGame.prototype.log("Connection.onmessage");
 86                var message = BISON.decode(event.data);
 87                that.onSocketDidAcceptConnection(message);
 88            };
 89            this.connection.onclose = function (event) {
 90                DemoHelloWorld.DemoClientGame.prototype.log("Connection.onclose");
 91                that.onSocketDisconnect();
 92            };
 93        },
 94
 95        /**
 96         * Map RealtimeMultiplayerGame.Constants.CMDS to functions
 97         */
 98        setupCmdMap: function () {
 99            this.cmdMap = {};
100            this.cmdMap[RealtimeMultiplayerGame.Constants.CMDS.SERVER_FULL_UPDATE] = this.onServerWorldUpdate;
101        },
102
103        ///// SocketIO Callbacks
104        onSocketConnect: function () {
105            console.log("(ClientNetChannel):onSocketConnect", arguments, this.socketio);
106        },
107
108        /**
109         * Called when ServerNetChannel has accepted your connection and given you a client id
110         * This is only called once, use the info to set some properties
111         */
112        onSocketDidAcceptConnection: function (aNetChannelMessage) {
113
114            console.log("(ClientNetChannel)::onSocketDidAcceptConnection", aNetChannelMessage);
115
116            // Should not have received this msg
117            if (aNetChannelMessage.cmd != RealtimeMultiplayerGame.Constants.CMDS.SERVER_CONNECT) {
118                throw "(ClientNetChannel):onSocketDidAcceptConnection recieved but cmd != SERVER_CONNECT ";
119            }
120
121            this.clientid = aNetChannelMessage.id;
122            this.delegate.log("(ClientNetChannel)::ClientID - ")
123            this.delegate.netChannelDidConnect(aNetChannelMessage);
124
125            // Set onMessage function back to normal - removing event listener didn't work, so for now changing the mapping
126            // TODO: Do via removeEvent
127            //this.socketio.removeEvent("message", function( obj ){ that.onSocketDidAcceptConnection( obj ) });
128            //this.socketio.on('message', function( obj ){ that.onSocketMessage( obj ) });
129            this.onSocketDidAcceptConnection = this.onSocketMessage;
130        },
131
132        /**
133         * Called when Socket.io has received a new message
134         * @param aNetChannelMessage
135         */
136        onSocketMessage: function (aNetChannelMessage) {
137            this.lastReceivedTime = this.delegate.getGameClock();
138            this.adjustRate(aNetChannelMessage);
139
140            if (aNetChannelMessage.id == this.clientid) // We sent this, clear our reliable buffer que
141            {
142                if (aNetChannelMessage.cmd == RealtimeMultiplayerGame.Constants.CMDS.SERVER_FULL_UPDATE) {
143//					debugger; //  IF CALLED THIS IS A BUG
144                }
145
146                var messageIndex = aNetChannelMessage.seq & BUFFER_MASK;
147                var message = this.messageBuffer[messageIndex];
148
149                // Free up reliable buffer to allow for new message to be sent
150                if (this.reliableBuffer === message) {
151                    this.reliableBuffer = null;
152                }
153
154                // Remove from memory
155                this.messageBuffer[messageIndex] = null;
156                delete message;
157
158                return;
159            }
160
161            // Call the mapped function
162            if (this.cmdMap[aNetChannelMessage.cmd])
163                this.cmdMap[aNetChannelMessage.cmd].call(this, aNetChannelMessage);
164            else
165                console.log("(NetChannel)::onSocketMessage could not map '" + aNetChannelMessage.cmd + "' to function!");
166        },
167
168        onSocketDisconnect: function () {
169            this.delegate.netChannelDidDisconnect();
170            this.connection = null;
171            this.socketio = null;
172            console.log("(ClientNetChannel)::onSocketDisconnect", arguments);
173        },
174
175
176        /**
177         * Send queued messages
178         */
179        tick: function () {
180            // Can't send new message, still waiting for last imporant message to be returned
181            if (this.reliableBuffer !== null) return;
182
183            var hasReliableMessages = false;
184            var firstUnreliableMessageFound = null;
185
186            var len = this.messageBuffer.length;
187            for (var i = 0; i < len; i++) {
188                var message = this.messageBuffer[i];
189                if (!message) continue;	// Slot is empty
190
191                // We have more important things to tend to sir.
192                if (message.isReliable) {
193                    hasReliableMessages = true;
194                    this.sendMessage(message);
195                    return;
196                }
197            }
198
199            // No reliable messages waiting, enough time has passed to send an update
200            if (!hasReliableMessages && this.canSendMessage() && this.nextUnreliable != null) {
201                this.sendMessage(this.nextUnreliable);
202                this.nextUnreliable = null;
203            }
204        },
205
206        /**
207         *
208         * @param aNetChannelMessage
209         */
210        onServerWorldUpdate: function (aNetChannelMessage) {
211            var len = aNetChannelMessage.data.length;
212            var i = -1;
213
214            // Store all world updates contained in the message.
215            while (++i < len) // Want to parse through them in correct order, so no fancy --len
216            {
217                var singleWorldUpdate = aNetChannelMessage.data[i];
218                var worldEntityDescription = this.createWorldEntityDescriptionFromString(singleWorldUpdate);
219
220                // Add it to the incommingCmdBuffer and drop oldest element
221                this.incomingWorldUpdateBuffer.push(worldEntityDescription);
222                if (this.incomingWorldUpdateBuffer.length > BUFFER_MASK)
223                    this.incomingWorldUpdateBuffer.shift();
224            }
225        },
226
227        /**
228         * Takes a WorldUpdateMessage that contains the information about all the elements inside of a string
229         * and creates SortedLookupTable out of it with the entityid's as the keys
230         * @param {String} aWorldUpdateMessage
231         */
232        createWorldEntityDescriptionFromString: function (aWorldUpdateMessage) {
233            // Create a new WorldEntityDescription and store the clock and gametick in it
234            var worldDescription = new SortedLookupTable();
235            worldDescription.gameTick = aWorldUpdateMessage.gameTick;
236            worldDescription.gameClock = aWorldUpdateMessage.gameClock;
237
238
239            var allEntities = aWorldUpdateMessage.entities.split('|'),
240                allEntitiesLen = allEntities.length; //
241
242            // Loop through each entity
243            while (--allEntitiesLen)   // allEntities[0] is garbage, so by using prefix we avoid it
244            {
245                // Loop through the string representing the entities properties
246                var entityDescAsArray = allEntities[allEntitiesLen].split(',');
247                var entityDescription = this.delegate.parseEntityDescriptionArray(entityDescAsArray);
248
249                // Store the final result using the entityid
250                worldDescription.setObjectForKey(entityDescription, entityDescription.entityid);
251            }
252
253
254            return worldDescription;
255        },
256
257        /**
258         * Sends a message via socket.io
259         * @param aMessageInstance
260         */
261        sendMessage: function (aMessageInstance) {
262            if (this.socketio == undefined) {
263                console.log("(ClientNetChannel)::sendMessage - socketio is undefined!");
264                return;
265            }
266
267            if (!this.socketio.socket.connected) { // Socket.IO is not connectd, probably not ready yet
268                // console.log("(ClientNetChannel)::sendMessage - socketio is undefined!");
269                return;      //some error here
270            }
271
272            aMessageInstance.messageTime = this.delegate.getGameClock(); // Store to determine latency
273
274            this.lastSentTime = this.delegate.getGameClock();
275
276            if (aMessageInstance.isReliable) {
277                this.reliableBuffer = aMessageInstance; // Block new connections
278            }
279
280            this.socketio.json.send(aMessageInstance);
281
282            if (RealtimeMultiplayerGame.Constants.CLIENT_NETCHANNEL_DEBUG) console.log('(NetChannel) Sending Message, isReliable', aMessageInstance.isReliable, aMessageInstance);
283        },
284
285        /**
286         * Prepare a message for sending at next available time
287         * @param isReliable
288         * @param anUnencodedMessage
289         */
290        addMessageToQueue: function (isReliable, aCommandConstant, payload) {
291            // Create a NetChannelMessage
292            var message = new RealtimeMultiplayerGame.model.NetChannelMessage(this.outgoingSequenceNumber, this.clientid, isReliable, aCommandConstant, payload);
293
294            // Add to array the queue using bitmask to wrap values
295            this.messageBuffer[ this.outgoingSequenceNumber & BUFFER_MASK ] = message;
296
297            if (!isReliable) {
298                this.nextUnreliable = message;
299            }
300
301            ++this.outgoingSequenceNumber;
302            if (RealtimeMultiplayerGame.Constants.DEBUG_SETTING.CLIENT_NETCHANNEL_DEBUG) console.log('(NetChannel) Adding Message to queue', this.messageBuffer[this.outgoingSequenceNumber & BUFFER_MASK], " ReliableBuffer currently contains: ", this.reliableBuffer);
303        },
304
305        /**
306         * Adjust the message chokerate based on latency
307         * @param serverMessage
308         */
309        adjustRate: function (serverMessage) {
310            var deltaTime = serverMessage.gameClock - this.delegate.getGameClock();
311            this.latency = deltaTime;
312
313            // TODO: Adjust cl_updateRate based on message thruput
314            //		time -= 100; // Subtract 100ms
315            //		if(this.)
316            //		console.log('Time:', time)
317            // time -= 0.1; // subtract 100ms
318            //
319            // if(time <= 0)
320            // {
321            // 	this.rate = 0.12; /* 60/1000*2 */
322            // }
323            // else
324            // {
325            // }
326        },
327
328        ///// Memory
329        /**
330         * Clear memory
331         */
332        dealloc: function () {
333            this.connection.close();
334            delete this.connection;
335            delete this.messageBuffer;
336            delete this.incomingWorldUpdateBuffer;
337        },
338
339        ///// Accessors
340        /**
341         * Set the NetChannelDelegate after validation
342         * @param aDelegate
343         */
344        setDelegate: function (aDelegate) {
345            var theInterface = RealtimeMultiplayerGame.ClientNetChannelDelegateProtocol;
346            for (var member in theInterface) {
347                if ((typeof aDelegate[member] != typeof theInterface[member])) {
348                    console.error("object failed to implement interface member " + member);
349                    return false;
350                }
351            }
352
353            // Checks passed
354            this.delegate = aDelegate;
355        },
356
357        /**
358         * Determines if it's ok for the client to send a unreliable new message yet
359         */
360        canSendMessage: function () {
361            var isReady = (this.delegate.getGameClock() > this.lastSentTime + this.cl_updateRate);
362            return isReady;
363        },
364        getClientid: function () {
365            return this.clientid
366        },
367        getIncomingWorldUpdateBuffer: function () {
368            return this.incomingWorldUpdateBuffer
369        },
370        getLatency: function () {
371            return this.latency
372        }
373    };
374
375    /**
376     * Required methods for the ClientNetChannel delegate
377     */
378    RealtimeMultiplayerGame.ClientNetChannelDelegateProtocol = {
379        netChannelDidConnect: function () {
380        },
381        netChannelDidReceiveMessage: function (aMessage) {
382        },
383        netChannelDidDisconnect: function () {
384        },
385        parseEntityDescriptionArray: function () {
386        },
387        log: function () {
388        },
389        getGameClock: function () {
390        }
391    }
392})()