Permalink
Browse files

Final implementation of A-Star Pathfinding

Appears to work correctly, new example file provided with assets
demonstrating it working. jaws-min.js not updated here.
  • Loading branch information...
1 parent 286ee2f commit 484c3d9a7a982b64934d62c0b2da5db08c281219 @MalphasWats MalphasWats committed Dec 28, 2012
Showing with 414 additions and 62 deletions.
  1. +177 −0 examples/example14.html
  2. BIN examples/example14/basic-32.png
  3. BIN examples/example14/player.png
  4. +95 −62 jaws.js
  5. +142 −0 src/tile_map.js
View
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script src="../jaws.js"></script>
+ <title>Rouge</title>
+ </head>
+<body style="background:black; color:white;">
+
+ <canvas width="320" height="320" style="background:white;"></canvas>
+ <p>FPS: <span id="fps"></span>. Click a dirt tile to move.</p>
+
+ <div id="info">
+ <h1>Example 14: Pathfinding in a TileMap</h1>
+ </div>
+
+ <script>
+ function Rouge() {
+ var player
+ var walls
+ var fps
+
+ var floor
+
+ var sprite_sheet
+
+ /* Called once when a game state is activated. Use it for one-time setup code. */
+ this.setup = function() {
+ fps = document.getElementById("fps")
+
+ sprite_sheet = new jaws.SpriteSheet({image: "example14/basic-32.png", frame_size: [32,32]})
+
+ walls = new jaws.SpriteList()
+
+ floor = new jaws.SpriteList()
+
+ /* We create some 32x32 blocks and save them in array blocks */
+ for (var i=0 ; i<10; i++)
+ {
+ for (var j=0 ; j<10; j++)
+ {
+ floor.push( new Sprite({image: sprite_sheet.frames[5], x: i*32, y: j*32}) )
+
+ if (i==0)
+ {
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: i, y: j*32}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 9*32, y: j*32}) )
+ }
+ if (j == 0)
+ {
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: i*32, y: j}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: i*32, y: 9*32}) )
+ }
+ }
+ }
+
+ // custom placed walls
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 96, y: 64}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 96, y: 96}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 64, y: 96}) )
+
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 128, y: 160}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 128, y: 192}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 128, y: 224}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 160, y: 160}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 192, y: 160}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 160, y: 224}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 192, y: 224}) )
+
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 32, y: 7*32}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 64, y: 7*32}) )
+ walls.push( new Sprite({image: sprite_sheet.frames[0], x: 64, y: 8*32}) )
+
+ // A tilemap, each cell is 32x32 pixels. There's 10 such cells across and 10 downwards.
+ var wall_map = new jaws.TileMap({size: [10,10], cell_size: [32,32]})
+
+ var floor_map = new jaws.TileMap({size: [10,10], cell_size: [32,32]})
+ floor_map.push(floor)
+
+ // Fit all items in array blocks into correct cells in the tilemap
+ // Later on we can look them up really fast (see player.move)
+ wall_map.push(walls)
+
+ player = new jaws.Sprite({image: "example14/player.png", x:64, y:64, anchor: "top_left"})
+ player.destination = false
+ player.path = []
+ player.move = function(x, y)
+ {
+ // Have our tile map return the items that occupy the cells which are touched by player.rect
+ // If there's any items inside player.rect, reverse the movement (-> stand still)
+ // We don't really need this anymore, because our path will take care of it, but I've
+ // left it in for completeness.
+ this.x += x
+ if(wall_map.atRect(player.rect()).length > 0) { this.x -= x; }
+
+ // Same as above but for vertical movement
+ this.y += y
+ if(wall_map.atRect(player.rect()).length > 0) { this.y -= y; }
+ }
+
+ player.moveTo = function(x, y)
+ {
+ /**
+ * Here's the magic - find a path through the walls
+ */
+ this.path = wall_map.findPath([this.x, this.y], [x, y])
+ this.setDestination()
+ }
+
+ player.setDestination = function()
+ {
+ if (this.path.length > 0)
+ {
+ var next_node = this.path.shift()
+ this.destination = floor_map.cell(next_node[0], next_node[1])[0]
+ }
+ else { this.destination = false }
+ }
+
+ jaws.context.mozImageSmoothingEnabled = false; // non-blurry, blocky retro scaling
+ jaws.preventDefaultKeys(["up", "down", "left", "right", "space"])
+ }
+
+ /* update() will get called each game tick with your specified FPS. Put game logic here. */
+ this.update = function()
+ {
+ if (jaws.pressed("left_mouse_button") && !jaws.isOutsideCanvas({x: jaws.mouse_x, y: jaws.mouse_y, width: 1, height: 1}) && !player.destination) { player.moveTo(jaws.mouse_x, jaws.mouse_y) }
+
+ //move player
+ if (player.x == player.destination.x && player.y == player.destination.y)
+ {
+ player.setDestination()
+ }
+
+ if (player.destination)
+ {
+ if(player.x > player.destination.x)
+ {
+ player.move(-4, 0)
+ }
+ else if (player.x < player.destination.x)
+ {
+ player.move(4, 0)
+ }
+ if(player.y > player.destination.y)
+ {
+ player.move(0, -4)
+ }
+ else if (player.y < player.destination.y)
+ {
+ player.move(0, 4)
+ }
+ }
+ jaws.forceInsideCanvas(player)
+ fps.innerHTML = jaws.game_loop.fps
+ }
+
+ /* Directly after each update draw() will be called. Put all your on-screen operations here. */
+ this.draw = function()
+ {
+ jaws.clear()
+ floor.draw()
+ walls.draw()
+ player.draw()
+ }
+ }
+
+ jaws.onload = function()
+ {
+ jaws.unpack()
+ jaws.assets.add(["example14/basic-32.png", "example14/player.png"])
+ jaws.start(Rouge) // Our convenience function jaws.start() will load assets, call setup and loop update/draw in 60 FPS
+ }
+ </script>
+
+</body>
+</html>
+
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
157 jaws.js
@@ -2504,11 +2504,22 @@ jaws.TileMap.prototype.cell = function(col, row) {
/**
* A-Star pathfinding
+ *
+ * Takes starting and ending x,y co-ordinates (from a mouse-click for example),
+ * which are then translated onto the TileMap grid.
+ *
+ * Does not allow for Diagonal movements
+ *
+ * Uses a very simple Heuristic [see crowFlies()] for calculating node scores.
+ *
+ * Very lightly optimised for speed over memory usage.
+ *
+ * Returns a list of [col, row] pairs that define a valid path. Due to the simple Heuristic
+ * the path is not guaranteed to be the best path.
*/
jaws.TileMap.prototype.findPath = function(start_position, end_position) {
- //console.log("finding Path: "+start_position
if (start_position[0] === end_position[0] && start_position[1] === end_position[1]) {
- return [this.at(start_position[0], start_position[1])]
+ return []
}
var start_col = parseInt(start_position[0] / this.cell_size[0])
@@ -2519,95 +2530,117 @@ jaws.TileMap.prototype.findPath = function(start_position, end_position) {
var col = start_col
var row = start_row
+ var step = 0
+ var score = 0
+ //travel corner-to-corner, through every square, plus one, just to make sure
+ var max_distance = (this.size[0]*this.size[1] * 2)+1
- var max_distance = this.size[0]+this.size[1] * 2 //if there were a lot of walls, you might have to go a long way
-
- //var node = []
- //var parent = this.cell(start_col, start_row)
-
- var open_nodes = []
- var closed_nodes = []
- closed_nodes.push( {node: [col, row], parent: []} )
-
- var crowFlies = function(node) {
- return Math.abs(end_col-node[0]) + Math.abs(end_row-node[1]);
+ var open_nodes = new Array(this.size[0])
+ for(var i=0; i < this.size[0]; i++) {
+ open_nodes[i] = new Array(this.size[1])
+ for(var j=0; j < this.size[1]; j++) {
+ open_nodes[i][j] = false
+ }
}
+ open_nodes[col][row] = {parent: [], G: 0, score: max_distance}
- var findInClosed = function(col, row) {
- for (var i=0 ; i < closed_nodes.length ; i++) {
- if (closed_nodes[i].node[0] === col && closed_nodes[i].node[1] === row) {
- return true
- }
+ var closed_nodes = new Array(this.size[0])
+ for(var i=0; i < this.size[0]; i++) {
+ closed_nodes[i] = new Array(this.size[1])
+ for(var j=0; j < this.size[1]; j++) {
+ closed_nodes[i][j] = false
}
- return false
+ }
+
+ var crowFlies = function(from_node, to_node) {
+ return Math.abs(to_node[0]-from_node[0]) + Math.abs(to_node[1]-from_node[1]);
}
- /*
-var findInOpen = function(col, row) {
- for (var i=0 ; i < open_nodes.length ; i++) {
- if (open_nodes[i].col === col && open_nodes[i].row === row) {
- return true
- }
+ var findInClosed = function(col, row) {
+ if (closed_nodes[col][row])
+ {
+ return true
}
- return false
+ else {return false}
}
-*/
- //console.log("starting at "+crowFlies([col, row]) +":"+col+","+row)
- //console.log("heading to :"+end_col+","+end_row)
-
- //While something! not sure what yet.
- while (! (col === end_col && row === end_row) ) {
+ while ( !(col === end_col && row === end_row) ) {
+ /**
+ * add the nodes above, below, to the left and right of the current node
+ * if it doesn't have a sprite in it, and it hasn't already been added
+ * to the closed list, recalculate its score from the current node and
+ * update it if it's already in the open list.
+ */
if (this.cell(col-1, row).length === 0 && !findInClosed(col-1, row)) {
- open_nodes.unshift( {node: [col-1, row], parent: [col, row]} )
+ score = step+1+crowFlies([col-1,row] , [end_col, end_row])
+ if (!open_nodes[col-1][row] || (open_nodes[col-1][row] && open_nodes[col-1][row].score > score)) {
+ open_nodes[col-1][row] = {parent: [col, row], G: step+1, score: score}
+ }
}
if (this.cell(col+1, row).length === 0 && !findInClosed(col+1, row)) {
- open_nodes.unshift( {node: [col+1, row], parent: [col, row]} )
+ score = step+1+crowFlies([col+1,row] , [end_col, end_row])
+ if (!open_nodes[col+1][row] || (open_nodes[col+1][row] && open_nodes[col+1][row].score > score)) {
+ open_nodes[col+1][row] = {parent: [col, row], G: step+1, score: score}
+ }
}
if (this.cell(col, row-1).length === 0 && !findInClosed(col, row-1)) {
- open_nodes.unshift( {node: [col, row-1], parent: [col, row]} )
+ score = step+1+crowFlies([col,row-1] , [end_col, end_row])
+ if (!open_nodes[col][row-1] || (open_nodes[col][row-1] && open_nodes[col][row-1].score > score)) {
+ open_nodes[col][row-1] = {parent: [col, row], G: step+1, score: score}
+ }
}
if (this.cell(col, row+1).length === 0 && !findInClosed(col, row+1)) {
- open_nodes.unshift( {node: [col, row+1], parent: [col, row]} )
+ score = step+1+crowFlies([col,row+1] , [end_col, end_row])
+ if (!open_nodes[col][row+1] || (open_nodes[col][row+1] && open_nodes[col][row+1].score > score)) {
+ open_nodes[col][row+1] = {parent: [col, row], G: step+1, score: score}
+ }
}
- var best_node = {score: max_distance, node: [], index: -1, parent: []}
- var node_score = 0
- for (var i=0 ; i<open_nodes.length ; i++) {
- node_score = crowFlies(open_nodes[i].node)
- //console.log(node_score+":"+open_nodes[i].node[0]+","+open_nodes[i].node[1])
- if (node_score < best_node.score) {
- best_node.node = open_nodes[i].node
- best_node.parent = open_nodes[i].parent
- best_node.score = node_score
- best_node.index = i
+ /**
+ * find the lowest scoring open node
+ */
+ var best_node = {node: [], parent: [], score: max_distance, G: 0}
+ for (var i=0 ; i<this.size[0] ; i++) {
+ for(var j=0 ; j<this.size[1] ; j++) {
+ if (open_nodes[i][j] && open_nodes[i][j].score < best_node.score) {
+ best_node.node = [i, j]
+ best_node.parent = open_nodes[i][j].parent
+ best_node.score = open_nodes[i][j].score
+ best_node.G = open_nodes[i][j].G
+ }
}
}
- if (best_node.node.length === 0) { /* YIKES */ }
+ if (best_node.node.length === 0) { //open_nodes is empty, no route found to end node
+ return []
+ }
+
+ //This doesn't stop the node being added again, but it doesn't seem to matter
+ open_nodes[best_node.node[0]][best_node.node[1]] = false
- open_nodes.splice(best_node.index, 1)
col = best_node.node[0]
row = best_node.node[1]
- closed_nodes.push( {node: best_node.node, parent: best_node.parent} )
- //console.log("choosing "+best_node.score+":"+col+","+row)
- }
- //this is not complete, need to work backwards through the closed_nodes
- /*
-var path = []
- var node = closed_nodes.pop()
- path.unshift({col: node.node[0], row: node.node[1], parent: node})
- path.unshift(closed_nodes.pop())
-
- while(!(path[0].parent[0] === start_col && path[0].parent[1] === start_row)) {
+ step = best_node.G
+ closed_nodes[col][row] = {parent: best_node.parent}
}
-*/
- closed_nodes.shift() // top node is the start node.
- return closed_nodes
+ /**
+ * a path has been found, construct it by working backwards from the
+ * end node, using the closed list
+ */
+ var path = []
+ var current_node = closed_nodes[col][row]
+ path.unshift([col, row])
+ while(! (col === start_col && row === start_row) ) {
+ col = current_node.parent[0]
+ row = current_node.parent[1]
+ path.unshift([col, row])
+ current_node = closed_nodes[col][row]
+ }
+ return path
}
Oops, something went wrong.

0 comments on commit 484c3d9

Please sign in to comment.