Implemented A* pathfinding on TileMap, example included #61

Merged
merged 3 commits into from Dec 31, 2012
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script src="../jaws.js"></script>
+ <title>Jaws Example #14 - TileMap pathfinding</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
@@ -2502,6 +2502,148 @@ jaws.TileMap.prototype.cell = function(col, row) {
return this.cells[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) {
+ if (start_position[0] === end_position[0] && start_position[1] === end_position[1]) {
+ return []
+ }
+
+ var start_col = parseInt(start_position[0] / this.cell_size[0])
+ var start_row = parseInt(start_position[1] / this.cell_size[1])
+
+ var end_col = parseInt(end_position[0] / this.cell_size[0])
+ var end_row = parseInt(end_position[1] / this.cell_size[1])
+
+ 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 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 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
+ }
+ }
+
+ 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 findInClosed = function(col, row) {
+ if (closed_nodes[col][row])
+ {
+ return true
+ }
+ else {return false}
+ }
+
+ 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)) {
+ 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)) {
+ 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)) {
+ 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)) {
+ 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}
+ }
+ }
+
+ /**
+ * 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) { //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
+
+ col = best_node.node[0]
+ row = best_node.node[1]
+ step = best_node.G
+
+ closed_nodes[col][row] = {parent: best_node.parent}
+ }
+
+ /**
+ * 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
+
+}
+
/** Debugstring for TileMap() */
jaws.TileMap.prototype.toString = function() { return "[TileMap " + this.size[0] + " cols, " + this.size[1] + " rows]" }
Oops, something went wrong.