PageRenderTime 121ms CodeModel.GetById 38ms app.highlight 14ms RepoModel.GetById 36ms app.codeStats 0ms

/js/core/AbstractClientGame.js

http://github.com/onedayitwillmake/RealtimeMultiplayerNodeJs
JavaScript | 316 lines | 129 code | 48 blank | 139 comment | 20 complexity | beafeb91f255023f1e92adb968072835 MD5 | raw file
  1/**
  2 File:
  3 AbstractClientGame.js
  4 Created By:
  5 Mario Gonzalez
  6 Project:
  7 RealtimeMultiplayerNodeJS
  8 Abstract:
  9 This class is the client side base Game controller
 10 Basic Usage:
 11 [This class is not instantiated! - below is an example of using this class by extending it]
 12
 13 (function(){
 14		MyGameClass = function() {
 15			return this;
 16 		}
 17
 18		RealtimeMultiplayerGame.extend(MyGameClass, RealtimeMultiplayerGame.AbstractGame, null);
 19	};
 20 */
 21(function () {
 22    RealtimeMultiplayerGame.AbstractClientGame = function () {
 23        RealtimeMultiplayerGame.AbstractClientGame.superclass.constructor.call(this);
 24        this.setupView();
 25        return this;
 26    };
 27
 28    RealtimeMultiplayerGame.AbstractClientGame.prototype = {
 29        view: null,							// View
 30        clientCharacter: null,							// Reference to this users character
 31        nickname: '',							// User 'nickname'
 32        locateUpdateFailedCount: 0,
 33
 34        // Methods
 35        /**
 36         * Setup the view
 37         * RealtimeMultiplayerNodeJS is agnostic any rendering method
 38         */
 39        setupView: function () {
 40            if (this.view === null) {  // If this is called, then user has not overwritten this function
 41                throw new Error("RealtimeMultiplayerGame.AbstractClientGame.setupView - Override this method, then call MyClientGame.superclass.setupView()");
 42            }
 43            this.fieldController.setView(this.view);
 44        },
 45
 46        /**
 47         * @inheritDoc
 48         */
 49        setupNetChannel: function () {
 50            console.log("RealtimeMultiplayerGame.AbstractClientGame.superclass", RealtimeMultiplayerGame.AbstractClientGame.superclass)
 51            RealtimeMultiplayerGame.AbstractClientGame.superclass.setupNetChannel.call(this);
 52            this.netChannel = new RealtimeMultiplayerGame.ClientNetChannel(this);
 53        },
 54
 55        /**
 56         * @inheritDoc
 57         */
 58        setupCmdMap: function () {
 59            RealtimeMultiplayerGame.AbstractClientGame.superclass.setupCmdMap.call(this);
 60        },
 61
 62        /**
 63         * @inheritDoc
 64         */
 65        tick: function () {
 66            RealtimeMultiplayerGame.AbstractClientGame.superclass.tick.call(this);
 67
 68            // Allow all entities to update their position
 69            this.fieldController.getEntities().forEach(function (key, entity) {
 70                entity.updateView();
 71            }, this);
 72
 73            // Continuously queue information about our input - which will be sent to the server by netchannel
 74            if (this.clientCharacter != null) {
 75                var input = this.clientCharacter.constructEntityDescription();
 76                this.netChannel.addMessageToQueue(false, RealtimeMultiplayerGame.Constants.CMDS.PLAYER_UPDATE, input);
 77            }
 78
 79            // Draw the gameworld
 80            this.renderAtTime(this.gameClock - RealtimeMultiplayerGame.Constants.CLIENT_SETTING.INTERP - RealtimeMultiplayerGame.Constants.CLIENT_SETTING.FAKE_LAG);
 81            this.netChannel.tick();
 82        },
 83
 84        /**
 85         * Renders back in time between two previously received messages allowing for packet-loss, and a smooth simulation
 86         * @param renderTime
 87         */
 88        renderAtTime: function (renderTime) {
 89            var cmdBuffer = this.netChannel.getIncomingWorldUpdateBuffer(),
 90                len = cmdBuffer.length;
 91
 92            // Need atleast 2 updates to render between
 93            if (len < 2) return;
 94            var newPosition = new RealtimeMultiplayerGame.model.Point(0, 0),
 95                newRotation = 0.0;
 96
 97            // if the distance between prev and next is too great - don't interpolate
 98            var maxInterpolationDistance = 150,
 99                maxInterpSQ = maxInterpolationDistance * maxInterpolationDistance;
100
101            // Store the next world-entity-description before and after the desired render time
102            var nextWED = null,
103                previousWED = null;
104
105            // Loop through the points, until we find the first one that has a timeValue which is greater than our renderTime
106            // Knowing that then we know that the combined with the one before it - that passed our just check - we know we want to render ourselves somehwere between these two points
107            var i = 0;
108            var forceUpdate = false;
109            while (++i < len) {
110                var currentWED = cmdBuffer[i];
111
112                // We fall between this "currentWorldEntityDescription", and the last one we just checked
113                if (currentWED.gameClock >= renderTime) {
114                    previousWED = cmdBuffer[i - 1];
115                    nextWED = currentWED;
116                    this.locateUpdateFailedCount = 0;
117                    break;
118                }
119
120                // Have no found a matching update for a while - the client is way behind the server, set our time to the time of the last udpate we received
121//				if(i === len -1) {
122//					if(++this.locateUpdateFailedCount === RealtimeMultiplayerGame.Constants.CLIENT_SETTING.MAX_UPDATE_FAILURE_COUNT) {
123//						this.gameClock = currentWED.gameClock;
124//						this.gameTick = currentWED.gameTick;
125//						previousWED = cmdBuffer[i-1];
126//						nextWED = currentWED;
127//					}
128//				}
129            }
130
131            // Could not find two points to render between
132            if (nextWED == null || previousWED == null) {
133                console.log("GIVE UP")
134                return false;
135            }
136
137            /**
138             * More info: http://www.learningiphone.com/2010/09/consicely-animate-an-object-along-a-path-sensitive-to-time/
139             * Find T in the time value between the points:
140             *
141             * durationBetweenPoints: Amount of time between the timestamp in both points
142             * offset: Figure out what our time would be if we pretended the previousBeforeTime.time was 0.00 by subtracting it from us
143             * t: Now that we have a zero based offsetTime, and a maximum time that is also zero based (durationBetweenPoints)
144             * we can easily figure out what offsetTime / duration.
145             *
146             * Example values: timeValue = 5.0f, nextPointTime = 10.0f, lastPointTime = 4.0f
147             * result:
148             * duration = 6.0f
149             * offsetTime = 1.0f
150             * t = 0.16
151             */
152            var durationBetweenPoints = (nextWED.gameClock - previousWED.gameClock);
153            var offsetTime = renderTime - previousWED.gameClock;
154            var activeEntities = {};
155
156            // T is where we fall between, as a function of these two points
157            var t = offsetTime / (nextWED.gameClock - previousWED.gameClock);
158            if (t > 1.0)  t = 1.0;
159            else if (t < 0) t = 0.0;
160
161            // Note: We want to render at time "B", so grab the position at time "A" (previous), and time "C"(next)
162            var entityPositionPast = new RealtimeMultiplayerGame.model.Point(0, 0),
163                entityRotationPast = 0;
164
165            var entityPositionFuture = new RealtimeMultiplayerGame.model.Point(0, 0),
166                entityRotationFuture = 0;
167
168            // Update players
169            nextWED.forEach(function (key, entityDesc) {
170                // Catch garbage values
171                var entityid = entityDesc.entityid;
172                var entity = this.fieldController.getEntityWithid(entityid);
173
174                // We don't have this entity - create it!
175                if (!entity) {
176                    this.createEntityFromDesc(entityDesc);
177                }
178                else {
179                    // We already have this entity - update it
180                    var previousEntityDescription = previousWED.objectForKey(entityid);
181
182                    // Could not find info for this entity in previous description
183                    // This can happen if this is this entities first frame in the game
184                    if (!previousEntityDescription) return;
185
186                    // Store past and future positions to compare
187                    entityPositionPast.set(previousEntityDescription.x, previousEntityDescription.y);
188                    entityRotationPast = previousEntityDescription.rotation;
189
190                    entityPositionFuture.set(entityDesc.x, entityDesc.y);
191                    entityRotationFuture = entityDesc.rotation;
192
193                    // if the distance between prev and next is too great - don't interpolate
194                    if (entityPositionPast.getDistanceSquared(entityPositionFuture) > maxInterpSQ) {
195                        t = 1;
196                    }
197
198                    // Interpolate the objects position by multiplying the Delta times T, and adding the previous position
199                    newPosition.x = ( (entityPositionFuture.x - entityPositionPast.x) * t ) + entityPositionPast.x;
200                    newPosition.y = ( (entityPositionFuture.y - entityPositionPast.y) * t ) + entityPositionPast.y;
201                    newRotation = ( (entityRotationFuture - entityRotationPast) * t ) + entityRotationPast;
202                }
203
204                // Update the entity with the new information, and insert it into the activeEntities array
205                this.fieldController.updateEntity(entityid, newPosition, newRotation, entityDesc);
206                activeEntities[entityid] = true;
207
208            }, this);
209
210
211            // Destroy removed entities, every N frames
212            if (this.gameTick % RealtimeMultiplayerGame.Constants.CLIENT_SETTING.EXPIRED_ENTITY_CHECK_RATE === 0)
213                this.fieldController.removeExpiredEntities(activeEntities);
214        },
215
216
217        /**
218         * Create an enitity using the information provided
219         * @param {Object} entityDesc An object containing information such as 'entityid', 'clientid' and usually position information atleast
220         */
221        createEntityFromDesc: function (entityDesc) {
222            // OVERRIDE
223        },
224
225        /**
226         * Called by the ClientNetChannel, it sends us an array containing tightly packed values and expects us to return a meaningful object
227         * It is left up to each game to implement this function because only the game knows what it needs to send.
228         * However the 4 example projects in RealtimeMultiplayerNodeJS offer should be used ans examples
229         *
230         * @param {Array} entityDescAsArray An array of tightly packed values
231         * @return {Object} An object which will be returned to you later on tied to a specific entity
232         */
233//		parseEntityDescriptionArray: function(entityDescAsArray)
234//		{
235//			// This is left in as an example, copy paste this into your AbstractClientGame subclass and modify from there
236//			var entityDescription = {};
237//
238//			// It is left upto each game to implement this function because only the game knows what it needs to send.
239//			// However the 4 example projects in RealtimeMultiplayerNodeJS offer this an example
240////			entityDescription.entityid = +entityDescAsArray[0];
241////			entityDescription.clientid = +entityDescAsArray[1];
242////			entityDescription.entityType = +entityDescAsArray[2];
243////			entityDescription.x = +entityDescAsArray[3];
244////			entityDescription.y = +entityDescAsArray[4];
245////			entityDescription.radius = +entityDescAsArray[5];
246////			entityDescription.rotation = +entityDescAsArray[6];
247//
248//			return entityDescription;
249//		},
250
251//////	ClientNetChannelDelegate
252        /**
253         * ClientNetChannel has connected via socket.io to server for first time
254         * Join the game
255         * @param messageData
256         */
257        netChannelDidConnect: function (messageData) {
258            // Sync time with server
259            this.gameClock = messageData.payload.gameClock;
260        },
261
262
263        /**
264         * Called when the user has entered a name, and wants to join the match
265         * @param aNickname
266         */
267        joinGame: function (aNickname) {
268            this.nickname = aNickname;
269            // Create a 'join' message and queue it in ClientNetChannel
270            this.netChannel.addMessageToQueue(true, RealtimeMultiplayerGame.Constants.CMDS.PLAYER_JOINED, { nickname: this.nickname });
271        },
272
273
274        /**
275         * Start/Restart the game tick
276         */
277        startGameClock: function () {
278            var that = this;
279            (function animationLoop() {
280                that.tick();
281
282                if (that.isRunning)
283                    requestAnimationFrame(animationLoop);
284            })()
285        },
286
287        /**
288         * Called by NetChannel when it receives a command if it decides not to intercept it.
289         * (For example CMDS.FULL_UPDATE is always intercepted, so it never calls this function, but CMDS.SERVER_MATCH_START is not intercepted so this function triggered)
290         * @param messageData
291         */
292        netChannelDidReceiveMessage: function (messageData) {
293            // OVERRIDE
294        },
295
296        netChannelDidDisconnect: function () {
297            this.isRunning = false;
298            this.stopGameClock();
299        },
300
301
302        ///// Memory
303        dealloc: function () {
304            if (this.view) this.view.dealloc();
305            this.view = null;
306
307            RealtimeMultiplayerGame.AbstractClientGame.superclass.dealloc.call(this);
308        }
309
310        ///// Accessors
311    };
312
313
314    // Extend RealtimeMultiplayerGame.AbstractGame
315    RealtimeMultiplayerGame.extend(RealtimeMultiplayerGame.AbstractClientGame, RealtimeMultiplayerGame.AbstractGame, null);
316})()