Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #61 from MalphasWats/master

Implemented A* pathfinding on TileMap, example included
  • Loading branch information...
commit 96aecc64532b4d6029c1bb98fdfad2fa2f4e57e6 2 parents 1388291 + 3e2f017
@ippa authored
View
177 examples/example14.html
@@ -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>
+
View
BIN  examples/example14/basic-32.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  examples/example14/player.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
142 jaws.js
@@ -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]" }
View
142 src/tile_map.js
@@ -185,6 +185,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]" }
Please sign in to comment.
Something went wrong with that request. Please try again.