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