phaser/plugins/AStar.js

640 lines
20 KiB
JavaScript
Raw Normal View History

2014-03-20 10:44:17 +00:00
/**
* The MIT License (MIT)
* Copyright (c) 2014 Raphaël Roux
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*
*
*/
/**
* @author Raphaël Roux
* @copyright 2014 Raphaël Roux
* @license {@link http://opensource.org/licenses/MIT}
*/
/**
* AStar is a phaser pathfinding plugin based on an A* kind of algorythm
* It works with the Phaser.Tilemap
*
* @class Phaser.Plugin.AStar
* @constructor
* @param {Any} parent - The object that owns this plugin, usually Phaser.PluginManager.
*/
Phaser.Plugin.AStar = function (parent)
{
/**
* @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.
*/
this.parent = parent;
/**
* @property {Phaser.Tilemap} _tilemap - A reference to the tilemap used to store astar nodes according to the Phaser.Tilemap structure.
*/
this._tilemap;
/**
* @property {number} _layerIndex - The layer index of the tilemap that is used to store astar nodes.
*/
this._layerIndex;
/**
* @property {number} _tilesetIndex - The tileset index of the tileset that handle tiles properties.
*/
this._tilesetIndex;
/**
* @property {array} _open - An array that references nodes to be considered by the search path algorythm.
*/
this._open;
/**
* @property {array} _closed - An array that references nodes not to consider anymore.
*/
this._closed;
/**
* @property {array} _visited - Internal array of visited tiles, use for debug pupose.
*/
this._visited;
/**
* @property {boolean} _useDiagonal - Does the astar algorythm can use tile diagonal?
* @default true
*/
this._useDiagonal = true;
/**
* @property {boolean} _findClosest - Does the findPath algorythm must calculate the closest result if destination is unreachable. If not findPath will return an empty array
* @default true
*/
this._findClosest = true;
/**
* @property {string} _walkablePropName - Wich name have the walkable propertiy in your tileset.
* @default 'walkable'
*/
this._walkablePropName = 'walkable';
/**
* @property {function} _distanceFunction - The function used to calculate distance.
*/
this._distanceFunction = Phaser.Plugin.AStar.DISTANCE_EUCLIDIAN;
/**
* @property {Phaser.Plugin.AStar.AStarPath} _lastPath - The last path calculated by astar.
*/
this._lastPath = null;
/**
* @property {boolean} _debug - Boolean to debug mode, stores visited nodes, and have a cost. Disable in production.
* @default false
*/
this._debug = true;
};
Phaser.Plugin.AStar.prototype = Object.create(Phaser.Plugin.prototype);
Phaser.Plugin.AStar.prototype.constructor = Phaser.Plugin.AStar;
Phaser.Plugin.AStar.VERSION = '0.0.101';
Phaser.Plugin.AStar.COST_ORTHOGONAL = 1;
Phaser.Plugin.AStar.COST_DIAGONAL = Phaser.Plugin.AStar.COST_ORTHOGONAL*Math.sqrt(2);
Phaser.Plugin.AStar.DISTANCE_MANHATTAN = 'distManhattan';
Phaser.Plugin.AStar.DISTANCE_EUCLIDIAN = 'distEuclidian';
/**
* Sets the Phaser.Tilemap used to searchPath into.
* @method Phaser.Plugin.AStar#setAStarMap
* @public
* @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.
* @param {string} layerName - The name of the layer that handle tiles.
* @param {string} tilesetName - The name of the tileset that have walkable properties.
* @return {Phaser.Plugin.AStar} The Phaser.Plugin.AStar itself.
*/
Phaser.Plugin.AStar.prototype.setAStarMap = function(map, layerName, tilesetName)
{
this._tilemap = map;
this._layerIndex = this._tilemap.getLayerIndex(layerName);;
this._tilesetIndex = this._tilemap.getTilesetIndex(tilesetName);
this.updateMap();
return this;
};
/**
* Sets the Phaser.Tilemap used to searchPath into.
* @method Phaser.Plugin.AStar-setAStarMap
* @private
* @return {void} The Phaser.Plugin.AStar itself.
*/
Phaser.Plugin.AStar.prototype.updateMap = function()
{
var tile;
var walkable;
//for each tile, add a default AStarNode with x, y and walkable properties according to the tilemap/tileset datas
for(var y=0; y < this._tilemap.height; y++)
{
for(var x=0; x < this._tilemap.width; x++)
{
tile = this._tilemap.layers[this._layerIndex].data[y][x];
walkable = this._tilemap.tilesets[this._tilesetIndex].tileProperties[tile.index - 1][this._walkablePropName] !== "false" ? true : false;
tile.properties.astarNode = new Phaser.Plugin.AStar.AStarNode(x, y, walkable);
}
}
};
/**
* Find a path between to tiles coordinates
* @method Phaser.Plugin.AStar#findPath
* @public
* @param {Phaser.Point} startPoint - The start point x, y in tiles coordinates to search a path.
* @param {Phaser.Point} goalPoint - The goal point x, y in tiles coordinates that you trying to reach.
* @return {Phaser.Plugin.AStar.AStarPath} The Phaser.Plugin.AStar.AStarPath that results
*/
Phaser.Plugin.AStar.prototype.findPath = function(startPoint, goalPoint)
{
var path = new Phaser.Plugin.AStar.AStarPath();
var start = this._tilemap.layers[this._layerIndex].data[startPoint.y][startPoint.x].properties.astarNode; //:AStarNode;
var goal = this._tilemap.layers[this._layerIndex].data[goalPoint.y][goalPoint.x].properties.astarNode
path.start = start;
path.goal = goal;
this._open = [];
this._closed = [];
this._visited = [];
this._open.push(start);
start.g = 0;
start.h = this[this._distanceFunction](start, goal);
start.f = start.h;
start.parent = null;
//Loop until there are no more nodes to search
while(this._open.length > 0)
{
//Find lowest f in this._open
var f = Infinity;
var x;
for (var i=0; i<this._open.length; i++)
{
if (this._open[i].f < f)
{
x = this._open[i];
f = x.f;
}
}
//Solution found, return solution
if (x == goal)
{
path.nodes = this.reconstructPath(goal);
this._lastPath = path;
if(this._debug === true) path.visited = this._visited;
return path;
}
//Close current node
this._open.splice(this._open.indexOf(x), 1);
this._closed.push(x);
//Then get its neighbors
var n = this.neighbors(x);
for(var yIndex=0; yIndex < n.length; yIndex++)
{
var y = n[yIndex];
if (-1 != this._closed.indexOf(y))
continue;
var g = x.g + y.travelCost;
var better = false;
//Add the node for being considered next loop.
if (-1 == this._open.indexOf(y))
{
this._open.push(y);
better = true;
if(this._debug === true) this.visit(y);
}
else if (g < y.g)
{
better = true;
}
if (better) {
y.parent = x;
y.g = g;
y.h = this[this._distanceFunction](y, goal);
y.f = y.g + y.h;
}
}
}
//If no solution found, does A* try to return the closest result?
if(this._findClosest === true)
{
var min = Infinity;
var closestGoal, node, dist;
for(var i=0, ii=this._closed.length; i<ii; i++)
{
node = this._closed[i];
var dist = this[this._distanceFunction](goal, node);
if (dist < min)
{
min = dist;
closestGoal = node;
}
}
//Reconstruct a path a path from the closestGoal
path.nodes = this.reconstructPath(closestGoal);
if(this._debug === true) path.visited = this._visited;
}
this._lastPath = path;
return path;
};
/**
* Reconstruct the result path backwards from the goal point, crawling its parents. Internal method.
* @method Phaser.Plugin.AStar-reconstructPath
* @private
* @param {Phaser.Plugin.AStar.AStarNode} n - The astar node from wich you want to rebuild the path.
* @return {array} An array of Phaser.Plugin.AStar.AStarNode
*/
Phaser.Plugin.AStar.prototype.reconstructPath = function(n)
{
var solution = [];
var nn = n;
while(nn.parent) {
solution.push({x: nn.x, y: nn.y});
nn = nn.parent;
}
return solution;
};
/**
* Add a node into visited if it is not already in. Debug only.
* @method Phaser.Plugin.AStar-visit
* @private
* @param {Phaser.Plugin.AStar.AStarNode} node - The astar node you want to register as visited
* @return {void}
*/
Phaser.Plugin.AStar.prototype.visit = function(node)
{
for(var i in this._visited)
{
if (this._visited[i] == node) return;
}
this._visited.push(node);
};
/**
* Add a node into visited if it is not already in. Debug only.
* @method Phaser.Plugin.AStar-neighbors
* @private
* @param {Phaser.Plugin.AStar.AStarNode} n - The astar node you want to register as visited
* @return {void}
*/
Phaser.Plugin.AStar.prototype.neighbors = function(node)
{
var x = node.x;
var y = node.y;
var n = null;
var neighbors = [];
var map = this._tilemap.layers[this._layerIndex].data;
//West
if (x > 0) {
n = map[y][x-1].properties.astarNode;
if (n.walkable) {
n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL;
neighbors.push(n);
}
}
//East
if (x < this._tilemap.width-1) {
n = map[y][x+1].properties.astarNode;
if (n.walkable) {
n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL;
neighbors.push(n);
}
}
//North
if (y > 0) {
n = map[y-1][x].properties.astarNode;
if (n.walkable) {
n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL;
neighbors.push(n);
}
}
//South
if (y < this._tilemap.height-1) {
n = map[y+1][x].properties.astarNode;
if (n.walkable) {
n.travelCost = Phaser.Plugin.AStar.COST_ORTHOGONAL;
neighbors.push(n);
}
}
//If diagonals aren't used do not search for other neighbors and return orthogonal search result
if(this._useDiagonal === false)
return neighbors;
//NorthWest
if (x > 0 && y > 0) {
n = map[y-1][x-1].properties.astarNode;
if (n.walkable
&& map[y][x-1].properties.astarNode.walkable
&& map[y-1][x].properties.astarNode.walkable
) {
n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL;
neighbors.push(n);
}
}
//NorthEast
if (x < this._tilemap.width-1 && y > 0) {
n = map[y-1][x+1].properties.astarNode;
if (n.walkable
&& map[y][x+1].properties.astarNode.walkable
&& map[y-1][x].properties.astarNode.walkable
) {
n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL;
neighbors.push(n);
}
}
//SouthWest
if (x > 0 && y < this._tilemap.height-1) {
n = map[y+1][x-1].properties.astarNode;
if (n.walkable
&& map[y][x-1].properties.astarNode.walkable
&& map[y+1][x].properties.astarNode.walkable
) {
n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL;
neighbors.push(n);
}
}
//SouthEast
if (x < this._tilemap.width-1 && y < this._tilemap.height-1) {
n = map[y+1][x+1].properties.astarNode;
if (n.walkable
&& map[y][x+1].properties.astarNode.walkable
&& map[y+1][x].properties.astarNode.walkable
) {
n.travelCost = Phaser.Plugin.AStar.COST_DIAGONAL;
neighbors.push(n);
}
}
return neighbors;
};
/**
* Calculate a distance between tow astar nodes coordinates according to the Manhattan method
* @method Phaser.Plugin.AStar-distManhattan
* @private
* @param {Phaser.Plugin.AStar.AStarNode} nodeA - The A node.
* @param {Phaser.Plugin.AStar.AStarNode} nodeB - The B node.
* @return {number} The distance between nodeA and nodeB
*/
Phaser.Plugin.AStar.prototype.distManhattan = function (nodeA, nodeB)
{
return Math.abs(nodeA.x - nodeB.x) + Math.abs(nodeA.y - nodeB.y);
};
/**
* Calculate a distance between tow astar nodes coordinates according to the Euclidian method. More accurate
* @method Phaser.Plugin.AStar-distEuclidian
* @private
* @param {Phaser.Plugin.AStar.AStarNode} nodeA - The A node.
* @param {Phaser.Plugin.AStar.AStarNode} nodeB - The B node.
* @return {number} The distance between nodeA and nodeB
*/
Phaser.Plugin.AStar.prototype.distEuclidian = function(nodeA, nodeB)
{
return Math.sqrt(Math.pow((nodeA.x - nodeB.x), 2) + Math.pow((nodeA.y -nodeB.y), 2));
};
/**
* Tells if a tile is walkable from its tilemap coordinates
* @method Phaser.Plugin.AStar-isWalkable
* @public
* @param {number} x - The x coordiante of the tile in tilemap's coordinate.
* @param {number} y - The y coordinate of the tile in tilemap's coordinate.
* @return {boolean} The distance between nodeA and nodeB
*/
Phaser.Plugin.AStar.prototype.isWalkable = function(x, y)
{
return this._tilemap.layers[this._layerIndex].data[y][x].properties.astarNode.walkable;
};
/**
* @properties {string} version - The version number of Phaser.Plugin.AStar read only
*/
Object.defineProperty(Phaser.Plugin.AStar.prototype, "version", {
get: function () {
return Phaser.Plugin.AStar.VERSION;
}
});
/**
* AStarNode is an object that stores AStar value. Each tile have an AStarNode in their properties
* @class Phaser.Plugin.AStar.AStarNode
* @constructor
* @param {number} x - The x coordinate of the tile.
* @param {number} y - The y coordinate of the tile.
* @param {boolean} isWalkable - Is this tile is walkable?
*/
Phaser.Plugin.AStar.AStarNode = function(x, y, isWalkable)
{
/**
* @property {number} x - The x coordinate of the tile.
*/
this.x = x;
/**
* @property {number} y - The y coordinate of the tile.
*/
this.y = y;
/**
* @property {number} g - The total travel cost from the start point. Sum of COST_ORTHOGONAL and COST_DIAGONAL
*/
this.g = 0;
/**
* @property {number} h - The remaing distance as the crow flies between this node and the goal.
*/
this.h = 0;
/**
* @property {number} f - The weight. Sum of g + h.
*/
this.f = 0;
/**
* @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)
*/
this.parent;
/**
* @property {boolean} walkable - Is this node is walkable?
*/
this.walkable = isWalkable;
/**
* @property {number} travelCost - The cost to travel to this node, COST_ORTHOGONAL or COST_DIAGONAL
*/
this.travelCost;
};
/**
* AStarPath is an object that stores a searchPath result.
* @class Phaser.Plugin.AStar.AStarPath
* @constructor
* @param {array} nodes - An array of nodes coordinates sorted backward from goal to start point.
* @param {Phaser.Plugin.AStarNode} start - The start AStarNode used for the searchPath.
* @param {Phaser.Plugin.AStarNode} goal - The goal AStarNode used for the searchPath.
*/
Phaser.Plugin.AStar.AStarPath = function(nodes, start, goal)
{
/**
* @property {array} nodes - Array of AstarNodes x, y coordiantes that are the path solution from goal to start point.
*/
this.nodes = nodes || [];
/**
* @property {Phaser.Plugin.Astar.AStarNode} start - Reference to the start point used by findPath.
*/
this.start = start || null;
/**
* @property {Phaser.Plugin.Astar.AStarNode} goal - Reference to the goal point used by findPath.
*/
this.goal = goal || null;
/**
* @property {array} visited - Array of AStarNodes that the findPath algorythm has visited. Used for debug only.
*/
this.visited = [];
};
/**
* Debug method to draw the last calculated path by AStar
* @method Phaser.Utils.Debug.AStar
* @param {Phaser.Plugin.AStar} astar- The AStar plugin that you want to debug.
* @param {number} x - X position on camera for debug display.
* @param {number} y - Y position on camera for debug display.
* @param {string} color - Color to stroke the path line.
* @return {void}
*/
Phaser.Utils.Debug.prototype.AStar = function(astar, x, y, color, showVisited)
{
if (this.context == null)
{
return;
}
var pathLength = 0;
if(astar._lastPath !== null)
{
pathLength = astar._lastPath.nodes.length;
}
color = color || 'rgb(255,255,255)';
game.debug.start(x, y, color);
if(pathLength > 0)
{
var node = astar._lastPath.nodes[0];
this.context.strokeStyle = color;
this.context.beginPath();
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);
for(var i=0; i<pathLength; i++)
{
node = astar._lastPath.nodes[i];
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);
}
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);
this.context.stroke();
//Draw circles on visited nodes
if(showVisited !== false)
{
var visitedNode;
for(var j=0; j < astar._lastPath.visited.length; j++)
{
visitedNode = astar._lastPath.visited[j];
this.context.beginPath();
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);
this.context.stroke();
}
}
}
this.line('Path length: ' + pathLength);
this.line('Distance func: ' + astar._distanceFunction);
this.line('Use diagonal: ' + astar._useDiagonal);
this.line('Find Closest: ' + astar._findClosest);
game.debug.stop();
};