Tim-Oliver bothered me today with a question about A* pathfinding. It has been some time since I wrote our A* implementation in DVW, so I had to dig into this topic again. I used this tutorial and wrote some code. Note that this is just a straightforward implementation of the pseudo code presented in this article. I wouldn’t write productive code that way. Anyway, this might come handy for someone, so I’ll present it here. Next time, I will replace the open list with a binary heap.
Click here to see the source:
#include <queue>
#include <limits>
#include <algorithm>
#include <iostream>
const int MAP_HEIGHT = 13;
const int MAP_WIDTH = 27;
static char MAP[] =
"XXXXXXXXXXXXXXXXXXXXXXXXXXX"
"X.........................X"
"X........XXXXXXXX.........X"
"X........X......X.........X"
"X...............X.........X"
"XXXXXX.XXXXXXXXXX.........X"
"X....X.X..................X"
"X....X.XXXXXXXXXXXXXXXXXXXX"
"X....X.........X..........X"
"X....XXXXXXXX..X..XXXXXX..X"
"X....X......X..X..X.......X"
"X....X......X.....X.......X"
"XXXXXXXXXXXXXXXXXXXXXXXXXXX";
struct Point
{
int x, y;
bool operator== ( const Point& p )
{
return ( x == p.x && y == p.y );
}
};
typedef std::deque< Point > QueuePoint;
Point NEIGHBOURS[ 8 ] = { { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 }, { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 } };
int COST[ 8 ] = { 10, 14, 10, 14, 10, 14, 10, 14 };
class Pathfinder
{
public:
Pathfinder( char* theMap, int width, int height )
: theMap( theMap ),
width( width ),
height( height )
{
predecessors = new Point[ width * height ];
cost = new int[ width * height ];
memset( cost, 0, width * height * sizeof( int ) );
}
~Pathfinder()
{
cleanUp();
}
void cleanUp()
{
open.clear();
closed.clear();
delete[] predecessors;
delete[] cost;
}
QueuePoint findPath( Point start, Point end )
{
QueuePoint path;
Point current;
// Add the starting square (or node) to the open list.
open.push_back( start );
// Repeat the following:
// Stop when you: Fail to find the target square, and the open list is empty.
while( open.size() )
{
// Look for the lowest F cost square on the open list. We refer to this as the current square.
int minCost = std::numeric_limits<int>::max();
QueuePoint::iterator min = open.end();
for( QueuePoint::iterator it = open.begin(); it != open.end(); ++it )
{
Point p = *it;
int localCost = cost[ p.y * width + p.x ];
if( localCost < minCost )
{
min = it;
minCost = localCost;
}
}
current = *min;
// Stop when you Add the target square to the closed list, in which case the path has been found
if( current == end )
{
break;
}
// Switch it to the closed list.
closed.push_back( current );
open.erase( min );
int lastCost = cost[ current.y * width + current.x ];
// For each of the 8 squares adjacent to this current square …
for( int i = 0; i < 8; ++i )
{
Point newPoint;
newPoint.x = current.x + NEIGHBOURS[ i ].x;
newPoint.y = current.y + NEIGHBOURS[ i ].y;
int idx = newPoint.y * width + newPoint.x;
bool positionOk = newPoint.x >= 0 && newPoint.x < width && newPoint.y >= 0 && newPoint.y < height;
bool mapFree = positionOk && theMap[ idx ] != 'X';
bool notInClosedList = std::find( closed.begin(), closed.end(), newPoint ) == closed.end();
// If it is not walkable or if it is on the closed list, ignore it. Otherwise do the following.
if( positionOk // out of bounds: ignore
&& mapFree // blocked: ignore
&& notInClosedList // on closed list: ignore
)
{
// If it isn't on the open list, add it to the open list.
// Make the current square the parent of this square.
// Record the F, G, and H costs of the square.
if( std::find( open.begin(), open.end(), newPoint ) == open.end() )
{
// not yet in open list
open.push_back( newPoint );
predecessors[ idx ] = current;
cost[ idx ] = lastCost + COST[ i ];
}
else
{
/*
If it is on the open list already, check to see if this path to that square is better,
using G cost as the measure. A lower G cost means that this is a better path. If so,
change the parent of the square to the current square, and recalculate the G and F scores
of the square. If you are keeping your open list sorted by F score, you may need to resort
the list to account for the change.
*/
if( lastCost + COST[ i ] < cost[ idx ] )
{
predecessors[ idx ] = current;
cost[ idx ] = lastCost + COST[ i ];
}
}
}
}
}
if( open.size() )
{
// Save the path.
// Working backwards from the target square, go from each square to its parent square until you reach the starting square.
// That is your path.
while( current.x != start.x || current.y != start.y )
{
path.push_front( current );
current = predecessors[ current.y * width + current.x ];
}
}
return path;
}
private:
char* theMap;
int width, height;
Point* predecessors;
int* cost;
QueuePoint open; // open nodes
QueuePoint closed; // closed nodes
};
int main( int argc, char* argv[] )
{
Pathfinder pf( MAP, MAP_WIDTH, MAP_HEIGHT );
Point start = { 8, 6 };
Point end = { 25, 11 };
QueuePoint path = pf.findPath( start, end );
for( QueuePoint::iterator it = path.begin(); it != path.end(); ++it )
{
const Point& p = *it;
MAP[ p.y * MAP_WIDTH + p.x ] = '*';
}
MAP[ start.y * MAP_WIDTH + start.x ] = 'S';
MAP[ end.y * MAP_WIDTH + end.x ] = 'E';
for( int y = 0; y < MAP_HEIGHT; ++y )
{
std::string row( &MAP[ MAP_WIDTH * y ], MAP_WIDTH );
std::cout << row.c_str() << std::endl;
}
return 0;
}