/js/lib/circlecollision/CircleManager.js

http://github.com/onedayitwillmake/RealtimeMultiplayerNodeJs · JavaScript · 348 lines · 198 code · 51 blank · 99 comment · 58 complexity · 224fb576c7943bb12d543bdebb6392f8 MD5 · raw file

  1. /**
  2. #### ##### ##### #### ### # # ###### ###### ## ## ##### # # ######## ## # # #####
  3. # # # # ### # # ##### ### ## ## ## # ## # # # # ## # ##### ### ###
  4. ### # # ##### #### # # # ###### ## ######### ##### ##### ##### # ## # # # # # #####
  5. -
  6. File:
  7. PackedCircle.js
  8. Created By:
  9. Mario Gonzalez
  10. Project :
  11. None
  12. Abstract:
  13. Manages a set of packed circles.
  14. Basic Usage:
  15. http://onedayitwillmake.com/CirclePackJS/
  16. */
  17. (function()
  18. {
  19. // Retrieve the namespace
  20. RealtimeMultiplayerGame.namespace("RealtimeMultiplayerGame.modules.circlecollision");
  21. /**
  22. * @constructor
  23. */
  24. RealtimeMultiplayerGame.modules.circlecollision.CircleManager = function()
  25. {
  26. this.allCircles = [];
  27. return this;
  28. };
  29. RealtimeMultiplayerGame.modules.circlecollision.CircleManager.prototype = {
  30. allCircles: [], // An array containing all the circles in this CircleManager
  31. numberOfCollisionPasses: 1, // Number of times to run the collision check, higher is more accurate with less overlapping but slower
  32. numberOfTargetingPasses: 0, // Number of times to move a circle towards its target
  33. bounds: {}, // Object containing x,y,width,height
  34. collisionCallback: null, // An object containing a scope and a function block
  35. // These can be passed to the handleBoundaryForCircle function
  36. BOUNDARY_WRAP_X : 1 << 0,
  37. BOUNDARY_WRAP_Y : 1 << 1,
  38. BOUNDARY_CONSTRAIN_X : 1 << 2,
  39. BOUNDARY_CONSTRAIN_Y : 1 << 3,
  40. /**
  41. * Adds a circle to the simulation
  42. * @param aCircle
  43. */
  44. addCircle: function(aCircle) {
  45. aCircle.id = this.allCircles.length;
  46. this.allCircles.push(aCircle);
  47. return this;
  48. },
  49. /**
  50. * Removes a circle from the simulations
  51. * @param aCircle Circle to remove
  52. */
  53. removeCircle: function(aCircle) {
  54. var index = 0,
  55. found = false,
  56. len = this.allCircles.length;
  57. if(len === 0) {
  58. throw "Error: (CircleManager) attempting to remove circle, and allCircles.length === 0!!"
  59. }
  60. while (len--) {
  61. if(this.allCircles[len] === aCircle) {
  62. found = true;
  63. index = len;
  64. break;
  65. }
  66. }
  67. if(!found) {
  68. throw "Could not locate circle in allCircles array!"
  69. }
  70. // Remove
  71. this.allCircles[index].dealloc();
  72. this.allCircles[index] = null;
  73. return this;
  74. },
  75. /**
  76. * Forces all circles to move to where their delegate position is
  77. * Assumes all targets have a 'position' property!
  78. */
  79. forceCirclesToMatchDelegatePositions: function()
  80. {
  81. var len = this.allCircles.length;
  82. // push toward target position
  83. for(var n = 0; n < len; n++)
  84. {
  85. var aCircle = this.allCircles[n];
  86. if(!aCircle || !aCircle.delegate) {
  87. continue;
  88. }
  89. aCircle.position.set(aCircle.delegate.x + aCircle.offset.x, aCircle.delegate.y + aCircle.offset.y);
  90. }
  91. },
  92. pushAllCirclesTowardTarget: function(aTarget) {
  93. var v = new RealtimeMultiplayerGame.model.Point().set(0,0),
  94. circleList = this.allCircles,
  95. len = circleList.length;
  96. // push toward target position
  97. for(var n = 0; n < this.numberOfTargetingPasses; n++)
  98. {
  99. for(var i = 0; i < len; i++)
  100. {
  101. var c = circleList[i];
  102. if(c.isFixed) continue;
  103. v.x = c.position.x - (c.targetPosition.x+c.offset.x);
  104. v.y = c.position.y - (c.targetPosition.y+c.offset.y);
  105. v.multiply(c.targetChaseSpeed);
  106. c.position.x -= v.x;
  107. c.position.y -= v.y;
  108. }
  109. }
  110. },
  111. /**
  112. * Packs the circles towards the center of the bounds.
  113. * Each circle will have it's own 'targetPosition' later on
  114. */
  115. handleCollisions: function()
  116. {
  117. this.removeExpiredElements();
  118. var v = new RealtimeMultiplayerGame.model.Point().set(0, 0),
  119. circleList = this.allCircles,
  120. len = circleList.length;
  121. // Collide circles
  122. for(var n = 0; n < this.numberOfCollisionPasses; n++)
  123. {
  124. for(var i = 0; i < len; i++)
  125. {
  126. var ci = circleList[i];
  127. for (var j = i + 1; j< len; j++)
  128. {
  129. var cj = circleList[j];
  130. // Circle collision should be ignored (ci == cj, or collisionGroups are incorrect
  131. if( !this.circlesCanCollide(ci, cj) ) {
  132. continue; // It's us!
  133. }
  134. var dx = cj.position.x - ci.position.x,
  135. dy = cj.position.y - ci.position.y;
  136. // The distance between the two circles radii, but we're also gonna pad it a tiny bit
  137. var r = (ci.radius + cj.radius),
  138. d = ci.position.getDistanceSquared(cj.position);
  139. /**
  140. * Collision detected!
  141. */
  142. if (d < (r * r) - 0.02 )
  143. {
  144. v.x = dx;
  145. v.y = dy;
  146. v.normalize();
  147. var inverseForce = (r - Math.sqrt(d)) * 0.5;
  148. v.multiply(inverseForce);
  149. // Move cj opposite of the collision as long as its not fixed
  150. if(!cj.isFixed)
  151. {
  152. if(ci.isFixed)
  153. v.multiply(2.0); // Double inverse force to make up for the fact that the other object is fixed
  154. // ADD the velocity
  155. cj.position.translatePoint(v);
  156. }
  157. // Move ci opposite of the collision as long as its not fixed
  158. if(!ci.isFixed)
  159. {
  160. if(cj.isFixed)
  161. v.multiply(2.0); // Double inverse force to make up for the fact that the other object is fixed
  162. // SUBTRACT the velocity
  163. ci.position.subtract(v);
  164. }
  165. // Emit the collision event from each circle, with itself as the first parameter
  166. if(this.collisionCallback && n === 0)
  167. {
  168. this.collisionCallback.block.call( this.collisionCallback.scope, ci, cj, v );
  169. }
  170. }
  171. }
  172. }
  173. }
  174. },
  175. /**
  176. * Performs boundary check against a circle.
  177. * Valid options are:
  178. * RealtimeMultiplayerGame.modules.circlecollision.CircleManager.prototype.BOUNDARY_WRAP_X
  179. * RealtimeMultiplayerGame.modules.circlecollision.CircleManager.prototype.BOUNDARY_WRAP_Y
  180. * RealtimeMultiplayerGame.modules.circlecollision.CircleManager.prototype.BOUNDARY_CONSTRAIN_X
  181. * RealtimeMultiplayerGame.modules.circlecollision.CircleManager.prototype.BOUNDARY_CONSTRAIN_Y
  182. *
  183. * These can be combined in the form of:
  184. * RealtimeMultiplayerGame.modules.circlecollision.CircleManager.prototype.BOUNDARY_WRAP_X | RealtimeMultiplayerGame.modules.circlecollision.CircleManager.prototype.BOUNDARY_CONSTRAIN_Y
  185. * @param {RealtimeMultiplayerGame.modules.circlecollision.PackedCircle} aCircle Circle to perform boundary check against
  186. * @param {Number} boundsRule A bitmask representing the boundary rules
  187. */
  188. handleBoundaryForCircle: function(aCircle, boundsRule) {
  189. if(boundsRule === undefined) {
  190. throw "No Boundary rule defined!";
  191. }
  192. var xpos = aCircle.position.x;
  193. var ypos = aCircle.position.y;
  194. var radius = aCircle.radius;
  195. var diameter = radius*2;
  196. // Toggle these on and off,
  197. // Wrap and bounce, are opposite behaviors so pick one or the other for each axis, or bad things will happen.
  198. var wrapXMask = RealtimeMultiplayerGame.modules.circlecollision.CircleManager.prototype.BOUNDARY_WRAP_X;
  199. var wrapYMask = RealtimeMultiplayerGame.modules.circlecollision.CircleManager.prototype.BOUNDARY_WRAP_Y;
  200. var constrainXMask = RealtimeMultiplayerGame.modules.circlecollision.CircleManager.prototype.BOUNDARY_CONSTRAIN_X;
  201. var constrainYMask = RealtimeMultiplayerGame.modules.circlecollision.CircleManager.prototype.BOUNDARY_CONSTRAIN_Y;
  202. // Convert to bitmask - Uncomment the one you want, or concact your own :)
  203. // boundsRule = wrapXMask | wrapYMask; // Wrap Y axis, but constrain horizontally
  204. // Wrap X
  205. if(boundsRule & wrapXMask && xpos-diameter > this.bounds.width) {
  206. aCircle.position.x = this.bounds.x - radius;
  207. } else if(boundsRule & wrapXMask && xpos+diameter < this.bounds.x) {
  208. aCircle.position.x = this.bounds.width - radius;
  209. }
  210. // Wrap Y
  211. if(boundsRule & wrapYMask && ypos-diameter > this.bounds.height) {
  212. aCircle.position.y = this.bounds.y - radius;
  213. } else if(boundsRule & wrapYMask && ypos+diameter < this.bounds.y) {
  214. aCircle.position.y = this.bounds.height + radius;
  215. }
  216. // Constrain X
  217. if(boundsRule & constrainXMask && xpos+radius >= this.bounds.width) {
  218. aCircle.position.x = aCircle.position.x = this.bounds.width-radius;
  219. } else if(boundsRule & constrainXMask && xpos-radius < this.bounds.x) {
  220. aCircle.position.x = this.bounds.x + radius;
  221. }
  222. // Constrain Y
  223. if(boundsRule & constrainYMask && ypos+radius > this.bounds.height) {
  224. aCircle.position.y = this.bounds.height - radius;
  225. } else if(boundsRule & constrainYMask && ypos-radius < this.bounds.y) {
  226. aCircle.position.y = this.bounds.y + radius;
  227. }
  228. },
  229. /**
  230. * Performs handleBoundaryForCircle on all circles
  231. * @param {Number} boundsRule A bitmask representing the boundary rules
  232. */
  233. handleBoundaryForAllCircles: function( boundsRule ) {
  234. if( boundsRule === undefined ) {
  235. throw "handleBoundaryForAllCircles - No Bounds Rule defined!";
  236. }
  237. var len = this.allCircles.length;
  238. for(var i = 0; i < len; i++)
  239. this.handleBoundaryForCircle( this.allCircles[i], boundsRule)
  240. },
  241. /**
  242. * Checks if two Circles can collide with one another.
  243. * For example, given the following three objects
  244. *
  245. * someCircleA.collisionMask = 1;
  246. * someCircleA.collisionGroup = 2;
  247. *
  248. * someCircleB.collisionMask = 2;
  249. * someCircleB.collisionGroup = 1;
  250. *
  251. * someCircleC.collisionMask = 2;
  252. * someCircleC.collisionGroup = 1;
  253. *
  254. * A and B will collide, B and C will not collide because B and C only want to collide with group 2
  255. *
  256. * @param {RealtimeMultiplayerGame.modules.circlecollision.PackedCircle} circleA
  257. * @param {RealtimeMultiplayerGame.modules.circlecollision.PackedCircle} circleB
  258. */
  259. circlesCanCollide: function(circleA, circleB) {
  260. if(!circleA || !circleB || circleA === circleB) return false; // one is null (will be deleted next loop), or both point to same obj.
  261. if(circleA.delegate == null || circleB.delegate == null) return false; // This circle will be removed next loop, it's entity is already removed
  262. // both are fixed
  263. if(circleA.isFixed & circleB.isFixed) return false;
  264. // They dont want to collide
  265. if((circleA.collisionGroup & circleB.collisionMask) == 0) return false;
  266. if((circleB.collisionGroup & circleA.collisionMask) == 0) return false;
  267. return true;
  268. },
  269. ///// ACCESSORS
  270. getAllCircles: function() { return this.allCircles },
  271. setBounds: function(x, y, w, h) {
  272. this.bounds.x = x;
  273. this.bounds.y = y;
  274. this.bounds.width = w;
  275. this.bounds.height = h;
  276. },
  277. setNumberOfCollisionPasses: function(value) {
  278. this.numberOfCollisionPasses = value;
  279. return this;
  280. },
  281. setNumberOfTargetingPasses: function(value) {
  282. this.numberOfTargetingPasses = value;
  283. return this;
  284. },
  285. setCallback: function(block, scope) {
  286. this.collisionCallback = {'block': block, 'scope': scope};
  287. },
  288. ///// MEMORY MANAGEMENT
  289. removeExpiredElements: function() {
  290. // remove null elements
  291. for (var k = this.allCircles.length; k >= 0; k--) {
  292. if (this.allCircles[k] === null)
  293. this.allCircles.splice(k, 1);
  294. }
  295. }
  296. };
  297. })();