AStar.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. /**
  2. * The MIT License (MIT)
  3. * Copyright (c) 2014 Raphaël Roux
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20. * THE SOFTWARE.
  21. *
  22. *
  23. *
  24. */
  25. /**
  26. * @author Raphaël Roux
  27. * @copyright 2014 Raphaël Roux
  28. * @license {@link http://opensource.org/licenses/MIT}
  29. */
  30. /**
  31. * AStar is a phaser pathfinding plugin based on an A* kind of algorythm
  32. * It works with the Phaser.Tilemap
  33. *
  34. * @class Phaser.Plugin.AStar
  35. * @constructor
  36. * @param {Any} parent - The object that owns this plugin, usually Phaser.PluginManager.
  37. */
  38. Phaser.Plugin.AStar = function (parent)
  39. {
  40. /**
  41. * @property {Any} parent - The parent of this plugin. If added to the PluginManager the parent will be set to that, otherwise it will be null.
  42. */
  43. this.parent = parent;
  44. /**
  45. * @property {Phaser.Tilemap} _tilemap - A reference to the tilemap used to store astar nodes according to the Phaser.Tilemap structure.
  46. */
  47. this._tilemap;
  48. /**
  49. * @property {number} _layerIndex - The layer index of the tilemap that is used to store astar nodes.
  50. */
  51. this._layerIndex;
  52. /**
  53. * @property {number} _tilesetIndex - The tileset index of the tileset that handle tiles properties.
  54. */
  55. this._tilesetIndex;
  56. /**
  57. * @property {array} _open - An array that references nodes to be considered by the search path algorythm.
  58. */
  59. this._open;
  60. /**
  61. * @property {array} _closed - An array that references nodes not to consider anymore.
  62. */
  63. this._closed;
  64. /**
  65. * @property {array} _visited - Internal array of visited tiles, use for debug pupose.
  66. */
  67. this._visited;
  68. /**
  69. * @property {boolean} _useDiagonal - Does the astar algorythm can use tile diagonal?
  70. * @default true
  71. */
  72. this._useDiagonal = true;
  73. /**
  74. * @property {boolean} _findClosest - Does the findPath algorythm must calc the closest result if destination is unreachable. If not findPath will return an empty array
  75. * @default true
  76. */
  77. this._findClosest = true;
  78. /**
  79. * @property {string} _walkablePropName - Wich name have the walkable propertiy in your tileset.
  80. * @default 'walkable'
  81. */
  82. this._walkablePropName = 'walkable';
  83. /**
  84. * @property {function} _distanceFunction - The function used to calc distance.
  85. */
  86. this._distanceFunction = Phaser.Plugin.AStar.DISTANCE_EUCLIDIAN;
  87. /**
  88. * @property {Phaser.Plugin.AStar.AStarPath} _lastPath - The last path calculated by astar.
  89. */
  90. this._lastPath = null;
  91. /**
  92. * @property {boolean} _debug - Boolean to debug mode, stores visited nodes, and have a cost. Disable in production.
  93. * @default false
  94. */
  95. this._debug = true;
  96. };
  97. Phaser.Plugin.AStar.prototype = Object.create(Phaser.Plugin.prototype);
  98. Phaser.Plugin.AStar.prototype.constructor = Phaser.Plugin.AStar;
  99. Phaser.Plugin.AStar.VERSION = '0.0.101';
  100. Phaser.Plugin.AStar.COST_ORTHOGONAL = 1;
  101. Phaser.Plugin.AStar.COST_DIAGONAL = Phaser.Plugin.AStar.COST_ORTHOGONAL*Math.sqrt(2);
  102. Phaser.Plugin.AStar.DISTANCE_MANHATTAN = 'distManhattan';
  103. Phaser.Plugin.AStar.DISTANCE_EUCLIDIAN = 'distEuclidian';
  104. /**
  105. * Sets the Phaser.Tilemap used to searchPath into.
  106. * @method Phaser.Plugin.AStar#setAStarMap
  107. * @public
  108. * @param {Phaser.Tilemap} map - the Phaser.Tilemap used to searchPath into. It must have a tileset with tile porperties to know if tiles are walkable or not.
  109. * @param {string} layerName - The name of the layer that handle tiles.
  110. * @param {string} tilesetName - The name of the tileset that have walkable properties.
  111. * @return {Phaser.Plugin.AStar} The Phaser.Plugin.AStar itself.
  112. */
  113. Phaser.Plugin.AStar.prototype.setAStarMap = function(map, layerName, tilesetName)
  114. {
  115. this._tilemap = map;
  116. this._layerIndex = this._tilemap.getLayerIndex(layerName);;
  117. this._tilesetIndex = this._tilemap.getTilesetIndex(tilesetName);
  118. this.updateMap();
  119. return this;
  120. };
  121. /**
  122. * Sets the Phaser.Tilemap used to searchPath into.
  123. * @method Phaser.Plugin.AStar-setAStarMap
  124. * @private
  125. * @return {void} The Phaser.Plugin.AStar itself.
  126. */
  127. Phaser.Plugin.AStar.prototype.updateMap = function()
  128. {
  129. var tile;
  130. var walkable;
  131. //for each tile, add a default AStarNode with x, y and walkable properties according to the tilemap/tileset datas
  132. for(var y=0; y < this._tilemap.height; y++)
  133. {
  134. for(var x=0; x < this._tilemap.width; x++)
  135. {
  136. tile = this._tilemap.layers[this._layerIndex].data[y][x];
  137. walkable = this._tilemap.tilesets[this._tilesetIndex].tileProperties[tile.index - 1][this._walkablePropName] !== "false" ? true : false;
  138. tile.properties.astarNode = new Phaser.Plugin.AStar.AStarNode(x, y, walkable);
  139. }
  140. }
  141. };
  142. /**
  143. * Find a path between to tiles coordinates
  144. * @method Phaser.Plugin.AStar#findPath
  145. * @public
  146. * @param {Phaser.Point} startPoint - The start point x, y in tiles coordinates to search a path.
  147. * @param {Phaser.Point} goalPoint - The goal point x, y in tiles coordinates that you trying to reach.
  148. * @return {Phaser.Plugin.AStar.AStarPath} The Phaser.Plugin.AStar.AStarPath that results
  149. */
  150. Phaser.Plugin.AStar.prototype.findPath = function(startPoint, goalPoint)
  151. {
  152. var path = new Phaser.Plugin.AStar.AStarPath();
  153. var start = this._tilemap.layers[this._layerIndex].data[startPoint.y][startPoint.x].properties.astarNode; //:AStarNode;
  154. var goal = this._tilemap.layers[this._layerIndex].data[goalPoint.y][goalPoint.x].properties.astarNode
  155. path.start = start;
  156. path.goal = goal;
  157. this._open = [];
  158. this._closed = [];
  159. this._visited = [];
  160. this._open.push(start);
  161. start.g = 0;
  162. start.h = this[this._distanceFunction](start, goal);
  163. start.f = start.h;
  164. start.parent = null;
  165. //Loop until there are no more nodes to search
  166. while(this._open.length > 0)
  167. {
  168. //Find lowest f in this._open
  169. var f = Infinity;
  170. var x;
  171. for (var i=0; i<this._open.length; i++)
  172. {
  173. if (this._open[i].f < f)
  174. {
  175. x = this._open[i];
  176. f = x.f;
  177. }
  178. }
  179. //Solution found, return solution
  180. if (x == goal)
  181. {
  182. path.nodes = this.reconstructPath(goal);
  183. this._lastPath = path;
  184. if(this._debug === true) path.visited = this._visited;
  185. return path;
  186. }
  187. //Close current node
  188. this._open.splice(this._open.indexOf(x), 1);
  189. this._closed.push(x);
  190. //Then get its neighbors
  191. var n = this.neighbors(x);
  192. for(var yIndex=0; yIndex < n.length; yIndex++)
  193. {
  194. var y = n[yIndex];
  195. if (-1 != this._closed.indexOf(y))
  196. continue;
  197. var g = x.g + y.travelCost;
  198. var better = false;
  199. //Add the node for being considered next loop.
  200. if (-1 == this._open.indexOf(y))
  201. {
  202. this._open.push(y);
  203. better = true;
  204. if(this._debug === true) this.visit(y);
  205. }
  206. else if (g < y.g)
  207. {
  208. better = true;
  209. }
  210. if (better) {
  211. y.parent = x;
  212. y.g = g;
  213. y.h = this[this._distanceFunction](y, goal);
  214. y.f = y.g + y.h;
  215. }
  216. }
  217. }
  218. //If no solution found, does A* try to return the closest result?
  219. if(this._findClosest === true)
  220. {
  221. var min = Infinity;
  222. var closestGoal, node, dist;
  223. for(var i=0, ii=this._closed.length; i<ii; i++)
  224. {
  225. node = this._closed[i];
  226. var dist = this[this._distanceFunction](goal, node);
  227. if (dist < min)
  228. {
  229. min = dist;
  230. closestGoal = node;
  231. }
  232. }
  233. //Reconstruct a path a path from the closestGoal
  234. path.nodes = this.reconstructPath(closestGoal);
  235. if(this._debug === true) path.visited = this._visited;
  236. }
  237. this._lastPath = path;
  238. return path;
  239. };
  240. /**
  241. * Reconstruct the result path backwards from the goal point, crawling its parents. Internal method.
  242. * @method Phaser.Plugin.AStar-reconstructPath
  243. * @private
  244. * @param {Phaser.Plugin.AStar.AStarNode} n - The astar node from wich you want to rebuild the path.
  245. * @return {array} An array of Phaser.Plugin.AStar.AStarNode
  246. */
  247. Phaser.Plugin.AStar.prototype.reconstructPath = function(n)
  248. {
  249. var solution = [];
  250. var nn = n;
  251. while(nn.parent) {
  252. solution.push({x: nn.x, y: nn.y});
  253. nn = nn.parent;
  254. }
  255. return solution;
  256. };
  257. /**
  258. * Add a node into visited if it is not already in. Debug only.
  259. * @method Phaser.Plugin.AStar-visit
  260. * @private
  261. * @param {Phaser.Plugin.AStar.AStarNode} node - The astar node you want to register as visited
  262. * @return {void}
  263. */
  264. Phaser.Plugin.AStar.prototype.visit = function(node)
  265. {
  266. for(var i in this._visited)
  267. {
  268. if (this._visited[i] == node) return;
  269. }
  270. this._visited.push(node);
  271. };
  272. /**
  273. * Add a node into visited if it is not already in. Debug only.
  274. * @method Phaser.Plugin.AStar-neighbors
  275. * @private
  276. * @param {Phaser.Plugin.AStar.AStarNode} n - The astar node you want to register as visited
  277. * @return {void}
  278. */
  279. Phaser.Plugin.AStar.prototype.neighbors = function(node)
  280. {
  281. var x = node.x;
  282. var y = node.y;
  283. var n = null;
  284. var neighbors = [];
  285. var map = this._tilemap.layers[this._layerIndex].data;
  286. //West
  287. if (x > 0) {
  288. n = map[y][x-1].properties.astarNode;
  289. if (n.walkable) {
  290. n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL;
  291. neighbors.push(n);
  292. }
  293. }
  294. //East
  295. if (x < this._tilemap.width-1) {
  296. n = map[y][x+1].properties.astarNode;
  297. if (n.walkable) {
  298. n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL;
  299. neighbors.push(n);
  300. }
  301. }
  302. //North
  303. if (y > 0) {
  304. n = map[y-1][x].properties.astarNode;
  305. if (n.walkable) {
  306. n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL;
  307. neighbors.push(n);
  308. }
  309. }
  310. //South
  311. if (y < this._tilemap.height-1) {
  312. n = map[y+1][x].properties.astarNode;
  313. if (n.walkable) {
  314. n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL;
  315. neighbors.push(n);
  316. }
  317. }
  318. //If diagonals aren't used do not search for other neighbors and return orthogonal search result
  319. if(this._useDiagonal === false)
  320. return neighbors;
  321. //NorthWest
  322. if (x > 0 && y > 0) {
  323. n = map[y-1][x-1].properties.astarNode;
  324. if (n.walkable
  325. && map[y][x-1].properties.astarNode.walkable
  326. && map[y-1][x].properties.astarNode.walkable
  327. ) {
  328. n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL;
  329. neighbors.push(n);
  330. }
  331. }
  332. //NorthEast
  333. if (x < this._tilemap.width-1 && y > 0) {
  334. n = map[y-1][x+1].properties.astarNode;
  335. if (n.walkable
  336. && map[y][x+1].properties.astarNode.walkable
  337. && map[y-1][x].properties.astarNode.walkable
  338. ) {
  339. n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL;
  340. neighbors.push(n);
  341. }
  342. }
  343. //SouthWest
  344. if (x > 0 && y < this._tilemap.height-1) {
  345. n = map[y+1][x-1].properties.astarNode;
  346. if (n.walkable
  347. && map[y][x-1].properties.astarNode.walkable
  348. && map[y+1][x].properties.astarNode.walkable
  349. ) {
  350. n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL;
  351. neighbors.push(n);
  352. }
  353. }
  354. //SouthEast
  355. if (x < this._tilemap.width-1 && y < this._tilemap.height-1) {
  356. n = map[y+1][x+1].properties.astarNode;
  357. if (n.walkable
  358. && map[y][x+1].properties.astarNode.walkable
  359. && map[y+1][x].properties.astarNode.walkable
  360. ) {
  361. n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL;
  362. neighbors.push(n);
  363. }
  364. }
  365. return neighbors;
  366. };
  367. /**
  368. * Calculate a distance between tow astar nodes coordinates according to the Manhattan method
  369. * @method Phaser.Plugin.AStar-distManhattan
  370. * @private
  371. * @param {Phaser.Plugin.AStar.AStarNode} nodeA - The A node.
  372. * @param {Phaser.Plugin.AStar.AStarNode} nodeB - The B node.
  373. * @return {number} The distance between nodeA and nodeB
  374. */
  375. Phaser.Plugin.AStar.prototype.distManhattan = function (nodeA, nodeB)
  376. {
  377. return Math.abs(nodeA.x - nodeB.x) + Math.abs(nodeA.y - nodeB.y);
  378. };
  379. /**
  380. * Calculate a distance between tow astar nodes coordinates according to the Euclidian method. More accurate
  381. * @method Phaser.Plugin.AStar-distEuclidian
  382. * @private
  383. * @param {Phaser.Plugin.AStar.AStarNode} nodeA - The A node.
  384. * @param {Phaser.Plugin.AStar.AStarNode} nodeB - The B node.
  385. * @return {number} The distance between nodeA and nodeB
  386. */
  387. Phaser.Plugin.AStar.prototype.distEuclidian = function(nodeA, nodeB)
  388. {
  389. return Math.sqrt(Math.pow((nodeA.x - nodeB.x), 2) + Math.pow((nodeA.y -nodeB.y), 2));
  390. };
  391. /**
  392. * Tells if a tile is walkable from its tilemap coordinates
  393. * @method Phaser.Plugin.AStar-isWalkable
  394. * @public
  395. * @param {number} x - The x coordiante of the tile in tilemap's coordinate.
  396. * @param {number} y - The y coordinate of the tile in tilemap's coordinate.
  397. * @return {boolean} The distance between nodeA and nodeB
  398. */
  399. Phaser.Plugin.AStar.prototype.isWalkable = function(x, y)
  400. {
  401. return this._tilemap.layers[this._layerIndex].data[y][x].properties.astarNode.walkable;
  402. };
  403. /**
  404. * @properties {string} version - The version number of Phaser.Plugin.AStar read only
  405. */
  406. Object.defineProperty(Phaser.Plugin.AStar.prototype, "version", {
  407. get: function () {
  408. return Phaser.Plugin.AStar.VERSION;
  409. }
  410. });
  411. /**
  412. * AStarNode is an object that stores AStar value. Each tile have an AStarNode in their properties
  413. * @class Phaser.Plugin.AStar.AStarNode
  414. * @constructor
  415. * @param {number} x - The x coordinate of the tile.
  416. * @param {number} y - The y coordinate of the tile.
  417. * @param {boolean} isWalkable - Is this tile is walkable?
  418. */
  419. Phaser.Plugin.AStar.AStarNode = function(x, y, isWalkable)
  420. {
  421. /**
  422. * @property {number} x - The x coordinate of the tile.
  423. */
  424. this.x = x;
  425. /**
  426. * @property {number} y - The y coordinate of the tile.
  427. */
  428. this.y = y;
  429. /**
  430. * @property {number} g - The total travel cost from the start point. Sum of COST_ORTHOGONAL and COST_DIAGONAL
  431. */
  432. this.g = 0;
  433. /**
  434. * @property {number} h - The remaing distance as the crow flies between this node and the goal.
  435. */
  436. this.h = 0;
  437. /**
  438. * @property {number} f - The weight. Sum of g + h.
  439. */
  440. this.f = 0;
  441. /**
  442. * @property {Phaser.Plugin.AStar.AStarNode} parent - Where do we come from? It's an AStarNode reference needed to reconstruct a path backwards (from goal to start point)
  443. */
  444. this.parent;
  445. /**
  446. * @property {boolean} walkable - Is this node is walkable?
  447. */
  448. this.walkable = isWalkable;
  449. /**
  450. * @property {number} travelCost - The cost to travel to this node, COST_ORTHOGONAL or COST_DIAGONAL
  451. */
  452. this.travelCost;
  453. };
  454. /**
  455. * AStarPath is an object that stores a searchPath result.
  456. * @class Phaser.Plugin.AStar.AStarPath
  457. * @constructor
  458. * @param {array} nodes - An array of nodes coordinates sorted backward from goal to start point.
  459. * @param {Phaser.Plugin.AStarNode} start - The start AStarNode used for the searchPath.
  460. * @param {Phaser.Plugin.AStarNode} goal - The goal AStarNode used for the searchPath.
  461. */
  462. Phaser.Plugin.AStar.AStarPath = function(nodes, start, goal)
  463. {
  464. /**
  465. * @property {array} nodes - Array of AstarNodes x, y coordiantes that are the path solution from goal to start point.
  466. */
  467. this.nodes = nodes || [];
  468. /**
  469. * @property {Phaser.Plugin.Astar.AStarNode} start - Reference to the start point used by findPath.
  470. */
  471. this.start = start || null;
  472. /**
  473. * @property {Phaser.Plugin.Astar.AStarNode} goal - Reference to the goal point used by findPath.
  474. */
  475. this.goal = goal || null;
  476. /**
  477. * @property {array} visited - Array of AStarNodes that the findPath algorythm has visited. Used for debug only.
  478. */
  479. this.visited = [];
  480. };
  481. /**
  482. * Debug method to draw the last calculated path by AStar
  483. * @method Phaser.Utils.Debug.AStar
  484. * @param {Phaser.Plugin.AStar} astar- The AStar plugin that you want to debug.
  485. * @param {number} x - X position on camera for debug display.
  486. * @param {number} y - Y position on camera for debug display.
  487. * @param {string} color - Color to stroke the path line.
  488. * @return {void}
  489. */
  490. Phaser.Utils.Debug.prototype.AStar = function(astar, x, y, color, showVisited)
  491. {
  492. if (this.context == null)
  493. {
  494. return;
  495. }
  496. var pathLength = 0;
  497. if(astar._lastPath !== null)
  498. {
  499. pathLength = astar._lastPath.nodes.length;
  500. }
  501. color = color || 'rgb(255,255,255)';
  502. game.debug.start(x, y, color);
  503. if(pathLength > 0)
  504. {
  505. var node = astar._lastPath.nodes[0];
  506. this.context.strokeStyle = color;
  507. this.context.beginPath();
  508. this.context.moveTo((node.x * astar._tilemap.tileWidth) + (astar._tilemap.tileWidth/2) - game.camera.view.x, (node.y * astar._tilemap.tileHeight) + (astar._tilemap.tileHeight/2) - game.camera.view.y);
  509. for(var i=0; i<pathLength; i++)
  510. {
  511. node = astar._lastPath.nodes[i];
  512. this.context.lineTo((node.x * astar._tilemap.tileWidth) + (astar._tilemap.tileWidth/2) - game.camera.view.x, (node.y * astar._tilemap.tileHeight) + (astar._tilemap.tileHeight/2) - game.camera.view.y);
  513. }
  514. this.context.lineTo((astar._lastPath.start.x * astar._tilemap.tileWidth) + (astar._tilemap.tileWidth/2) - game.camera.view.x, (astar._lastPath.start.y * astar._tilemap.tileHeight) + (astar._tilemap.tileHeight/2) - game.camera.view.y);
  515. this.context.stroke();
  516. //Draw circles on visited nodes
  517. if(showVisited !== false)
  518. {
  519. var visitedNode;
  520. for(var j=0; j < astar._lastPath.visited.length; j++)
  521. {
  522. visitedNode = astar._lastPath.visited[j];
  523. this.context.beginPath();
  524. this.context.arc((visitedNode.x * astar._tilemap.tileWidth) + (astar._tilemap.tileWidth/2) - game.camera.view.x, (visitedNode.y * astar._tilemap.tileHeight) + (astar._tilemap.tileHeight/2) - game.camera.view.y, 2, 0, Math.PI*2, true);
  525. this.context.stroke();
  526. }
  527. }
  528. }
  529. this.line('Path length: ' + pathLength);
  530. this.line('Distance func: ' + astar._distanceFunction);
  531. this.line('Use diagonal: ' + astar._useDiagonal);
  532. this.line('Find Closest: ' + astar._findClosest);
  533. game.debug.stop();
  534. };