Skip to content

Commit

Permalink
Merge pull request #61 from MalphasWats/master
Browse files Browse the repository at this point in the history
Implemented A* pathfinding on TileMap, example included
  • Loading branch information
ippa committed Dec 31, 2012
2 parents 1388291 + 3e2f017 commit 96aecc6
Show file tree
Hide file tree
Showing 5 changed files with 461 additions and 0 deletions.
177 changes: 177 additions & 0 deletions 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>

Binary file added 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.
Binary file added 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.
142 changes: 142 additions & 0 deletions jaws.js
Expand Up @@ -2502,6 +2502,148 @@ jaws.TileMap.prototype.cell = function(col, row) {
return this.cells[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() */ /** Debugstring for TileMap() */
jaws.TileMap.prototype.toString = function() { return "[TileMap " + this.size[0] + " cols, " + this.size[1] + " rows]" } jaws.TileMap.prototype.toString = function() { return "[TileMap " + this.size[0] + " cols, " + this.size[1] + " rows]" }


Expand Down

0 comments on commit 96aecc6

Please sign in to comment.