On Github ashblue / pathfinding-presentation
By Ash Blue / Clever Crow Games / HTML5 in Action / @ashbluewd
Process of discovering a path through obstacles between two points
No! Pathfinding is important for building complex HTML5 apps
Get our hero to the treasure with the shortest path
(click to move)function euclidean (xC, yC, xT, yT) { var dx = xT - xC, dy = yT - yC; return Math.sqrt((dx * dx) + (dy * dy)); } function manhattan (xC, yC, xT, yT) { var dx = Math.abs(xT - xC), dy = Math.abs(yT - yC); return dx + dy; }
window.Map = window.Class.extend({ ... outOfBounds: function (x, y) { return x < 0 || x >= this.data[0].length || y < 0 || y >= this.data.length; }, ... });
window.Map = window.Class.extend({ ... getNeighbors: function (x, y) { var neighbors = []; // Check top, right, bottom, left (clockwise) if (!this.blocked(x, y - 1)) neighbors.push(new window.Tile(x, y - 1)); if (!this.blocked(x + 1, y)) neighbors.push(new window.Tile(x + 1, y)); if (!this.blocked(x, y + 1)) neighbors.push(new window.Tile(x, y + 1)); if (!this.blocked(x - 1, y)) neighbors.push(new window.Tile(x - 1, y)); return neighbors; }, ... });
window.Map = window.Class.extend({ data: null, // Current map init: function (data) { this.data = data; }, outOfBounds: function (x, y) { return x < 0 || x >= this.data[0].length || y < 0 || y >= this.data.length; }, getWidthInTiles: function () { return this.data[0].length; }, getHeightInTiles: function () { return this.data.length; }, blocked: function (x, y) { if (this.outOfBounds(x, y)) { return true; } if (this.data[y][x] === 0) { return true; } return false; }, getNeighbors: function (x, y) { var neighbors = []; if (!this.blocked(x, y - 1)) neighbors.push(new window.Tile(x, y - 1)); if (!this.blocked(x + 1, y)) neighbors.push(new window.Tile(x + 1, y)); if (!this.blocked(x, y + 1)) neighbors.push(new window.Tile(x, y + 1)); if (!this.blocked(x - 1, y)) neighbors.push(new window.Tile(x - 1, y)); return neighbors; }, getCost: function (xC, yC, xT, yT) { return this.data[yT][xT]; } });
Simple interface to manage closed tiles
window.PathFinder = window.Class.extend({ ... // New closed tile addClosed: function (step) { this.addHistory(step, 'closed'); this.closed.push(step); return this; }, // Check for existing closed tile inClosed: function (step) { for (var i = 0; i < this.closed.length; i++) { if (this.closed[i].x === step.x && this.closed[i].y === step.y) return this.closed[i]; } return false; }, ... });
Used to store and analyze group data
window.PathFinder = window.Class.extend({ ... // Adds a new tile addOpen: function (step) { this.open.push(step); return this; }, // Removes an existing tile removeOpen: function (step) { for (var i = 0; i < this.open.length; i++) { if (this.open[i] === step) this.open.splice(i, 1); } return this; }, // Check if the step is already in the open set inOpen: function (step) { for (var i = 0; i < this.open.length; i++) { if (this.open[i].x === step.x && this.open[i].y === step.y) return this.open[i]; } return false; }, // Get the best possible tile from the open set getBestOpen: function () { var bestI = 0; for (var i = 0; i < this.open.length; i++) { if (this.open[i].f < this.open[bestI].f) bestI = i; } return this.open[bestI]; }, ... });
This walks through the map API data finds a walkable path
window.PathFinder = window.Class.extend({ ... findPath: function (xC, yC, xT, yT) { var current, // Current best open tile neighbors, // Dump of all nearby neighbor tiles neighborRecord, // Any pre-existing records of a neighbor stepCost, // Dump of a total step score for a neighbor i; // You must add the starting step this.reset() .addOpen(new window.Step(xC, yC, xT, yT, this.step, false)); while (this.open.length !== 0) { this.step++; current = this.getBestOpen(); // Check if goal has been discovered to build a path if (current.x === xT && current.y === yT) { return this.buildPath(current, []); } // Move current into closed set this.removeOpen(current) .addClosed(current); // Get neighbors from the map and check them neighbors = this.map.getNeighbors(current.x, current.y); for (i = 0; i < neighbors.length; i++) { this.step++; // Get current step and distance from current to neighbor stepCost = current.g + this.map.getCost(current.x, current.y, neighbors[i].x, neighbors[i].y); // Check for the neighbor in the closed set // then see if its cost is >= the stepCost, if so skip current neighbor neighborRecord = this.inClosed(neighbors[i]); if (neighborRecord && stepCost >= neighborRecord.g) continue; // Verify neighbor doesn't exist or new score for it is better neighborRecord = this.inOpen(neighbors[i]); if (!neighborRecord || stepCost < neighborRecord.g) { if (!neighborRecord) { this.addOpen(new window.Step(neighbors[i].x, neighbors[i].y, xT, yT, stepCost, current)); } else { neighborRecord.parent = current; neighborRecord.g = stepCost; neighborRecord.f = stepCost + neighborRecord.h; } } } } return false; }, ... });
Assemble walkable path from closed group
window.PathFinder = window.Class.extend({ ... // Recursive path building method buildPath: function (tile, stack) { stack.push(tile); // Look at every parent and step back until no parents are left if (tile.parent) { return this.buildPath(tile.parent, stack); } else { return stack; } }, ... });
window.PathFinder = window.Class.extend({ map: null, // Map class reference closed: null, // Taken steps open: null, // Available steps that can be taken history: null, step: 0, // Step count maxSearchDistance: 100, // Maximum number of steps that can be taken before shutting down a closed path init: function (map) { this.closed = []; this.open = []; this.history = []; this.map = map; }, addOpen: function (step) { this.addHistory(step, 'open'); this.open.push(step); return this; }, // Remove a step that already exists by object memory address (not actual x and y values) removeOpen: function (step) { for (var i = 0; i < this.open.length; i++) { if (this.open[i] === step) this.open.splice(i, 1); } return this; }, // Check if the step is already in the open set inOpen: function (step) { for (var i = 0; i < this.open.length; i++) { if (this.open[i].x === step.x && this.open[i].y === step.y) return this.open[i]; } return false; }, // Get the lowest costing tile in the open set getBestOpen: function () { var bestI = 0; for (var i = 0; i < this.open.length; i++) { if (this.open[i].f < this.open[bestI].f) bestI = i; } return this.open[bestI]; }, addClosed: function (step) { this.addHistory(step, 'closed'); this.closed.push(step); return this; }, // Check if the step is already in the closed set inClosed: function (step) { for (var i = 0; i < this.closed.length; i++) { if (this.closed[i].x === step.x && this.closed[i].y === step.y) return this.closed[i]; } return false; }, addHistory: function (step, group) { this.history.push({ step: step, group: group }); }, findPath: function (xC, yC, xT, yT) { var current, // Current best open tile neighbors, // Dump of all nearby neighbor tiles neighborRecord, // Any pre-existing records of a neighbor stepCost, // Dump of a total step score for a neighbor i; // You must add the starting step this.reset() .addOpen(new window.Step(xC, yC, xT, yT, this.step, false)); while (this.open.length !== 0) { this.step++; current = this.getBestOpen(); // Check if goal has been discovered to build a path if (current.x === xT && current.y === yT) { return this.buildPath(current, []); } // Move current into closed set this.removeOpen(current) .addClosed(current); // Get neighbors from the map and check them neighbors = this.map.getNeighbors(current.x, current.y); for (i = 0; i < neighbors.length; i++) { this.step++; // Get current step and distance from current to neighbor stepCost = current.g + this.map.getCost(current.x, current.y, neighbors[i].x, neighbors[i].y); // Check for the neighbor in the closed set // then see if its cost is >= the stepCost, if so skip current neighbor neighborRecord = this.inClosed(neighbors[i]); if (neighborRecord && stepCost >= neighborRecord.g) continue; // Verify neighbor doesn't exist or new score for it is better neighborRecord = this.inOpen(neighbors[i]); if (!neighborRecord || stepCost < neighborRecord.g) { if (!neighborRecord) { this.addOpen(new window.Step(neighbors[i].x, neighbors[i].y, xT, yT, stepCost, current)); } else { neighborRecord.parent = current; neighborRecord.g = stepCost; neighborRecord.f = stepCost + neighborRecord.h; } } } } return false; }, // Recursive path buliding method buildPath: function (tile, stack) { stack.push(tile); if (tile.parent) { return this.buildPath(tile.parent, stack); } else { return stack; } }, reset: function () { this.closed = []; this.open = []; return this; } });
For use with platformers and other games requiring gravity
For when moving characters takes up more than one tile
Not many solutions out there yet. Most are weekend hack projects (not maintained). These are two of the best.