Download Path Planner Application Manual

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts

Catastrophic interference wikipedia , lookup

Existential risk from artificial general intelligence wikipedia , lookup

Minimax wikipedia , lookup

Rete algorithm wikipedia , lookup

Hierarchical temporal memory wikipedia , lookup

Transcript
Path Planner Application Manual
User Manual ........................................................................................................................ 2
The Toolbar ..................................................................................................................... 2
Developer Manual ............................................................................................................... 3
Introduction ..................................................................................................................... 3
Representing the Map ..................................................................................................... 3
Standard Template Library ............................................................................................. 4
The CPathPlannerApp Class ........................................................................................... 4
The PathPlannerBase Interface ....................................................................................... 5
The PlannerNode Class ................................................................................................... 7
The Algorithms ................................................................................................................... 8
Breadth-First ................................................................................................................. 10
Best-First ....................................................................................................................... 12
Dijkstra.......................................................................................................................... 14
A*.................................................................................................................................. 17
A* Additions ................................................................................................................. 22
A* On Node Mesh ........................................................................................................ 22
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
User Manual
The Toolbar
The
(Open CollisionMap) button allows the user to open a collision map. The
collision map is the map on which the planning occurs. The application open
Data\CollisionMap\CollisionMapBop.bmp as default relative to the application
executable.
The
(Application Settings) button brings up the application settings dialog. The dialog
allows the user to select a planning algorithm, specify the start and goal coordinates, and
specify/view other application settings.
The
(Create Planner) button initializes an instance of the planner selected in the
application settings dialog. If the selected planner has not been instantiated yet, it creates
an instance of it.
The
(Destroy Planner) button destructs the instance of the planner specified in the
application settings dialog.
The
(Start Planner) button signals the application that the planer should start the
planning process. If an instance of the planner does not exist, one is created.
The
(Pause Planner) button pauses the planning process.
The
(Stop Planner) button stops the planner.
The
(Planner settings) button allows the user to bring up planner specific settings.
Note that some planners may not require additional settings. These settings are local to
the planner and if the planner is destructed, the default settings will be restored.
The
(Planner Information) button exposes the statistics and information about the
selected planner.
The
(Draw Progress) button toggles the drawings performed by the planner. When the
button is not pressed, the planner will only be allowed to draw after it has completed the
planning process. This feature is useful when timing the performance of a planner.
The
(About Help) button brings up the about help dialog.
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
Developer Manual
Introduction
The path planner application is designed to allow for convenient creation, demonstration,
and analysis of different path planning algorithms. In this manual the term path planner
refers to the application component that performs the planning, and the term path planner
application refers to the application itself. Developers can create a new planning
algorithm and study its characteristics.
The application settings dialog allows the user to specify settings for the application. For
example, it allows you to select a planning algorithm and specify some global settings
such as the start and the goal coordinate. Once an algorithm is selected, the run controls
can be used to run, pause, and stop the planner.
This tutorial covers planning algorithms that are derivatives of the Breadth-First
algorithms. Even the A* algorithm is a modified version of the Breadth-First algorithm.
The ultimate goal of this tutorial is to help you understand the A* algorithm. Instead of
jumping right into A* we will first implement Breadth-First, Dijkstra, and Best-First.
These algorithms are similar in many ways and by implementing them first, you will be
able to better understand the A* algorithm. Implementing A* is not a hard task but it is
more important to understand why it was developed and how it is different from other
algorithms.
As you go through the tutorial be sure to compare your algorithms to the completed
version to make sure you are on a right track. Keep in mind that writing code that
performs many operations and loops many times can be harder to debug. Expect to run
into bugs but use caution to avoid them.
Representing the Map
The collision map is a bitmap that represents the map the path planning occurs on. The
black regions represent obstacles. The white and gray regions represent the passable
regions. They represent regions with cost 1, 2, 3 and 4 respectively. The lighter the color
the less it costs. These regions represent costs such as resources or the risk involved. Not
all planning algorithms understand weighted regions and assume that all passable regions
are weighted equally.
To represent the small-scale version of the world, many games use a graph/mesh of
points (waypoints), polygons, or volumes. The nodes of such graph contain information
such as the location of the node and a list of connections to other nodes. For example,
two connected waypoints can represent a simple hallway. A simple hall way can also be
represented as a convex polygon. Either way, you can use the data in these graphs to
resolve paths between two points. The nodes of the graph can contain additional data to
indicate that the entity should be climbing or crawl through a region. Tile based Games
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
such as War Craft III and Age of Empires simply use a 2D grid. Here we assume that the
world is represented as a 2D grid. Please note that a 2D grid is a specific case of a node
graph where all the nodes are aligned and are connected to no more than eight immediate
neighbors. A mesh of triangles (navigation mesh) is a specific case of a node graph that
can have up to three connections. The algorithms covered here can be easily modified to
work on an arbitrary graph of nodes.
Standard Template Library
If you are not familiar with the STL, I recommend that you go to read up on it. You can
even look through Microsoft’s newer STL documentations (7.0 and up) for sample code.
Here are the list of classes and functions you should be familiar with:
list (doubly linked list)
push_front, push_back
pop_front, pop_back
front, back
begin, end
empty, clear
size
vector (dynamically growable array)
operator []
inser, erase
empty, clear
size
// This data structure is not as critical to know
map (internally represented as a binary search tree, it can be used for fast searches
for an object paired with or mapped to a unique key)
operator []
// for convenient insertion (and retrieval) of objects using a key
find
// checking if a value has been mapped to a key
Once you implemented the algorithms you can go back and optimized your code by using
your own data structures. This will payoff significantly for the list data structure, but not
as much for the vectors. Since STL linked lists cannot require you to only put data in it
that have a next and a previous pointer, every time you insert a node into an STL’s list,
additional memory is allocated that contains the next and previous pointers in addition
the data.
The CPathPlannerApp Class
This class is the application’s main class. It extends the CWinApp class, which store a
pointer to the window. Once the application has been initialized, it acts as a state machine
that provides the planner with some processing time. Here are some of the more
important members of this class
static
ApplicationState
PathPlannerBase
int
CPathPlannerApp *instance;
applicationState;
*planner;
**collisionMapData;
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
This class is a singleton, which means that the member variable instance points to the
only instance of the class. The member applicationState stores the current state of
the application and is used to decide what actions are valid in different stages of the
planning, as well as which function of the planner should be called. The member
planner stores a pointer to an instance of the planner that is specified in the
application settings dialog. collisionMapData is a dynamically allocated 2D array
that stores the data of the collision map. This array is populated by 1, 2, 3, 4 and –1 to
indicate the weight (or cost) at different locations of the terrain. Regions that represent
obstacles are set to –1. The line bellow demonstrated how to retrieve the terrain cost at
the coordinate (x, y).
int mapData = CPathPlannerApp::instance->collisionMapData[y][x];
Please note that the first index of the array is y and the second index is x (as typically
expected).
The PathPlannerBase Interface
The application communicates with a planner through the PathPlannerBase
Interface. Every planner should extend PathPlannerBase. The Interface defines the
following functions:
virtual
virtual
virtual
virtual
virtual
virtual
virtual
void
void
bool
void
void
void
void
PlanPath(int ix, int iy, int gx, int gy);
Run();
IsDone();
Draw(CDC *dc);
Settings();
ShowInfo();
CleanUp();
First thing you should notice is that all the functions are virtual and meant to be
overridden. When the
button is pressed the application initializes an instance of the
currently selected planner as indicated in the applications settings dialog. If an instance of
the planner does not exist, a new one is constructed. Otherwise, the existing instance is
reused (CleanUp is called). Here is what happens when the application is in the run
state:
void CPathPlannerApp::StateRun(){
runCount++;
//Allow the planner to run. The planner should try to spend
//approximately as much time as indicated by the timeSlice
planner->Run();
if (drawProgress){
// cause a repaint
pFrame->GetChildView()->Invalidate(FALSE);
}
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
if (planner->IsDone()){
applicationState = STATE_RUN_END;
}
}
The sections bellow explain what the overwritten functions should do
PlanPath (…)
The PlanPath(…) function is used to tell the planner that the planning process is about
to begin. It is called when the
button is pressed. In this function, you can perform
some initialization and store the start and goal points.
Run ()
Immediately after PlanPath(…) is called, the Run functions is called. The application
repeatedly calls the Run function until the planer indicates that it no longer needs
processing time. The Run function is where the planning occurs. It is extremely
important to note that the Run cannot hold on to the thread for a long time. You are
responsible for making sure that it returns after spending approximately
CPathPlannerApp::instance->timeSlice. This is critical because if the run
function takes too long, the application will not get the chance to respond to events such
as resizing, moving, and most importantly painting. You can think of the application
having a game loop. Any single function called by the application cannot take too long
because the game will not be responsive. In other words, the planning process must be
split across frames. It may sound like this will significantly complicate the planner code,
but that is not the case. Algorithms such as Breadth-First, Dijkstra, Best-First, and A* can
easily be split across frames. If you store the state of the planning when the Run function
returns, you can continue the process the next time the function is invoked. Since this
function is called repeatedly by the application, it is important not to perform any
initialization in it. Initializations can be done in PlanPath or and some in the
constructor.
IsDone ()
This function is called by the application to find out whether or not the planner has
completed the process. This function should return false if you wish the run function to
get called again, and true to indicate that the planer is done.
Draw (…)
When drawProgress is true and the run function returns, the application invalidates
the content of the window, which in turn causes the Draw function of the planner to get
called. The function can draw any debugging/visualization data on the map to allow the
user to see what the planner is doing. Please note that you should never call Draw
directly. Following this guideline allows the user to enable/disable the drawings of the
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
planner in order to speed up or time the process. The draw function can, for example, you
can call set pixel to set a pixel (or grid cell) to a specific color.
dc->SetPixel(x, y, RGB(0, 0, 255));
Settings ()
When the planner settings button
is pressed on the toolbar, the application calls the
settings function of the planner. This allows the planner to popup a dialog box in order to
collect planner specific settings. Note that these settings are local to the planner and not
saved when the planner is destroyed.
ShowInfo ()
When the planner Information button
is pressed on the toolbar, the application calls
the showInfo function of the planner. This allows the planner to display
statistics/summery of the planning process.
CleanUp ()
This function is called when the application decides to reuse the instance of the planner.
A planner is reused when
is pressed and an instance of the planner has already been
created (or
is grayed out). You should also call this function from the destructor of
your planner as well. It is your responsibility to ensure that the cleanup function does not
fail if it is called when the planner has not done any planning.
The PlannerNode Class
A path can be described as an ordered list of sub-destinations. Instances of the
PlannerNode class are used to represent a path between the start point and an arbitrary
point on the map, which may be the goal. The node class needs to store a position as well
as a parent pointer. By chaining nodes together using the parent (or previous) pointer, we
can represent a path (or a candidate solution). Note that this node class is not exactly a
waypoint node. A waypoint node is used to represent the connectivity of a map, whereas
the PlannerNode represents a specific path through a map and is used strictly by a
planning algorithm. A clear distinction between them is that a waypoint node only needs
sibling pointers, but a PlannerNode only needs a parent pointer. The PlannerNodes are
used to keep track of how we got to a spot on the map as opposed to where we can
possibly go from a spot on the map.
Start
(1,1)
(2,2)
(2,3)
Goal
A linked list of nodes that represent a path from goal to start
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
The Algorithms
During the planning process we will come across many different paths that may lead us
to the goal, and therefore need to keep track of them. By keeping track of them, we will
be able to come back to them in order to create additional paths that extend them. By
creating additional paths that extend existing paths, we will be able to work our way
through the map in search of the goal. The open list (green) will keep track of paths that
are open for consideration (or being extended), and another list, the closed list (blue), will
keep track of paths that we have already considered.
Figure 1
Figure 1 is a snapshot of a planner in process. Nodes in the open list are green and nodes
in the closed list are blue. The open list contains paths that are yet to be extended. The
closed list contains paths that used to be in the open list. They have already been
extended and check to see if they had ran into the goal. It is important to note that each
node in the open list indicates the head of a path that ends at the start point (upper left
corner in the image above).
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
Figure 2
Figure 2 shows two different paths (red and pink) that partially overlap. Each path is a
linked list of nodes that start at a green spot (a node in open) and end in the start point.
The paths in the image are only two of the many different paths.
The different algorithms such as Breadth-First, Dijkstra, Best-First, and A* have a lot in
common. They all share the pseudo code below, which is absolutely fundamental to
understand.
1. Create a node for the start point and push it into the open list
2. While the open list is not empty
a. Pop a node from the open list and call it currentNode
b. Check to see if it is at the goal. If so, we can exit the loop
c. Create the successors of currentNode and push them into the open
list.
d. Put the currentNode in to the closed list
Pseudo-code 1
The idea is simple. We want to examine the map in order to find a path between the start
and the goal. We start traversing the map and for every spot we reach we will create a
node that points to its parent. We will repeat this until we reach the goal. Once we find
the goal, we will follow the parent pointer of the node that reached the goal in order to
find out how we got to the goal. The root node will be the only node without a parent.
The algorithms first create a root node and push it into the open list. They then loop until
they have examined every node in the open list. Note that the first iteration of the loop the
only node in the open list, the root node, is popped from the list. This means that if we
fail to push additional nodes into the list, the loop condition will evaluate to false. In the
body of the loop, the algorithms attempt to create the successors of the currentNode.
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
These successors are in essence extensions to the path that currentNode indicate.
Once the successors are generated and pushed into the open list, the currentNode is
pushed into the closed list.
Before creating a successor node, the algorithms make sure that there is no more than one
node for any given spot of the map. This is to prevent unnecessary re-exploration of
different parts of the map. For example, there is no need to store more than one path
between the start and a specific point on the map. To prevent re-exploration of the map,
before creating a successor node, we have to see if we have already made a node for that
spot of the map. In order to do so, they have to check the open and the close list. This
step can be very expensive (unless you use a data structure that offers fast lookups such
as hashTable or hashMap), but is still a sound thing to do. Not catching redundant nodes
can result in substantial consumption of memory as well as substantially longer time to
find the goal.
The main difference between the different algorithms is in which node (or path) they
decide to bring out of the open list. Breadth-First brings out the one that has been waiting
the longest; Dijkstra brings out the one that is the cheapest and Best-First chooses the one
that is closest to the goal. Instead of using time, cost, or distance, A* uses a combination
of cost and distance.
Breadth-First
Breadth-First tries to find a path from start to goal by examining the search space ply-byply. This behavior is because of the fact that for every iteration of the while loop, it pops
the node that has been waiting the longest. In order to do so efficiently, you can simply
emulate a queue. Every time a successor is generated, push it to the back of the list and
always pop from the front of the list. The pseudo-code bellow shows the significant
details. The terms defined here will be using in other pseudo-codes.
Note that Breadth-First is not the perfect algorithm for path planning. As you can see it
generates many nodes before it finds the goal. This means it uses a lot of memory and
CPU. In addition, Breadth-First has no concept of cost or even distance. Even though it
finds a path to the goal that has fewest possible nodes in between, it does not find the
optimal solution in terms of distance. If you run Breadth-First for different coordinates,
you will see that the path it finds goes diagonal for no reason. The path that Breadth-First
finds depends heavily on the order in which the successor nodes are created.
list
list
list
Node
Node
Node
Point
Point
Point
open
closed
solution
currentNode
successorNode
rootNode
startPoint
goalPoint
nearbyPoint
//
//
//
//
//
//
//
//
//
//
sorted list of nodes open for consideration
list of nodes that we have already considered
the nodes along the path from start to goal
the node we are currently working on
one of the children of currentNode
the start node
a pair of x and y that contain the start coordinates
a pair of x and y that contain the goal coordinates
a pair of x and y that contain one of the 8 possible
coordinates around the currentNode
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
Breadth-First Pseudo-Code
1)create the rootNode:
- set its x and y to the startPoint
- set its parent to NULL
2)push rootNode in to open
3)while open is not empty
1)pop the node that has been waiting the longest from open and assign it to
currentNode
2)if currentNode's x and y are equal to the goalPoint, then
// Note: we can traverse through the parents of currentNode
- push the nodes that are part of the path in to solution
- break from step 3
// Note: a node has 8 points around it which can be used to create min of 0 and
//
max of 8 successor
3)for every nearbyPoint around the currentNode do the following
1)if this nearbyPoint is in a spot that is illegal such as a wall, then
- skip to the next nearbyPoint
2)if a node for this nearbyPoint has been created before, then
- skip to the next nearbyPoint
3)create successorNode:
- set its x and y to the nearbyPoint
- set its parent to be currentNode
4)push successorNode in to open
4)push the currentNode in to closed
4)if the while loop completes without finding the goal, the goalPoint is unreachable
Programming Hints

Given a coordinate, how can you compute the coordinates of its neighbors? Since
the world is represented as a grid, you can use a lookup table or a nested for loop.
A point on the grid can have up to 8 points around it. If using a for loop, be
careful to note that a 3 by 3 loop results in 9 pairs of values, and that you need to
skip one of them.

I strongly suggest that you compute the coordinates of a nearByPoint and store it
in variables such as nearbyPointX and nearbyPointY, or successorX and
successorY. Not doing so will make your code harder to read and a good
candidate for very hard to find bugs.

If you find a node that represents the goal position, you can stop the search
process. You can now store the solution to the problem, which is an ordered list of
nodes from start to goal. You can use a vector or a list.
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com

For any spot of the map there should be at most 1 node, which will be in either the
open or the closed list. You should NOT generate nodes for spots on the map that
already have a node generated for them. Every time you go to make a node you
have to check all the nodes you have ever made before! This is an expensive
check. Where are all the nodes you have ever made before? All in open? All in
closed? Important to note that some are in open and some in closed. Later on you
can replace a open/closed list with a hash table to significantly speed up this step.

Your algorithm should be split across frames. That is, it should not spend more
than CPathPlannerApp::instance->timeSlice milliseconds in the Run() function.
When Run is called, do a GetTickCount() to store when you entered the function.
Every pass of the loop check to see if you have spent more than the allowed time
in which case the function should return. The application keeps calling the run
function until isDone() returns true. You should insert the check somewhere in
your while loop so if time slice is –1, it does the loop exactly once. This will
allow the while loop to return every iteration and cause a repaint.

When your algorithm is totally done, you should set the isDone flag to true so that
the app does not keep calling your run function anymore. Note that the planner is
done when it either finds the goal or open becomes empty.

Be sure to clean up ALL nodes and other memory that you allocated. Please note
that your clean up functions should be robust. As mentioned earlier, the
application calls the cleanup before PlanPath to ensure the planer is ready to go.

Write the draw function. Walk the open and closed list and draw a pixel for every
node.
Best-First
Best-First uses problem specific knowledge about the problem to speed up the search
process. Best-First is a heuristic search (as opposed to exhaustive) and tries to head right
for the goal. Instead of keeping the open list sorted by time or cost, it uses the distance of
each node to the goal. The distance-to-goal serves as a heuristic that promotes the regions
of the map that are more likely to lead to the goal. This causes the algorithm to head
directly to the goal. It is important to note that heading towards the goal is not always the
right thing to do. Just because you are getting closer to the goal does not mean that you
will not go down a dead end track. Yet generally speaking, it is a good rule of thumb (or
estimate) to head towards the goal first.
Best-First is extremely faster than Breadth-First and Dijkstra. It typically creates very few
nodes and finds a rather straight path to the goal. Best-First suffers from a few
disadvantages. First, it does not understand terrain cost. It also heads to the goal and can
find a not so optimal path because it is only concerned with getting closer to the goal, and
not how far a path might have traveled before getting close to the goal.
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
Best-First Pseudo-Code
1)create the rootNode:
- set its x and y to the startPoint
- set its parent to NULL
- set its heuristicCost = distance of startPoint from the goalPoint
2)push rootNode in to open
3)while open is not empty
1)pop the node with the best heuristicCost from open and assign it to currentNode
2)if currentNode's x and y are equal to the goalPoint, then
// Note: we can traverse through the parents of currentNode
- push the nodes that are part of the path in to solution
- break from step 3
// Note: a node has 8 points around it which can be used to create min of 0 and
//
max of 8 successor
3)for every nearbyPoint around the currentNode do the following
1)if this nearbyPoint is in a spot that is illegal such as a wall, then
- skip to the next nearbyPoint
2)if a node for this nearbyPoint has been created before, then
- skip to the next nearbyPoint
3)create successorNode:
- set its x and y to the nearbyPoint
- set its parent to be currentNode
- set its heuristicCost = distance of nearbyPoint from the goalPoint
4)push successorNode in to open
4)push the currentNode in to closed
4)if the while loop completes without finding the goal, the goalPoint is unreachable
Programming Hints

Do we need to use a queue or a priority queue? How can you sort the open list (or
keep it sorted)? First, use a vector instead of a list for you open. Check out the
SmartInsert function. SmartInsert is basically a binary search/insert function that
will call the compare() function of the Node class as many times as it needs to in
order to find the right place to insert the node in the open vector. You should
write the Node::BestFirstCompare().

When using a vector, you should pop from the back instead of the front. Vectors
have substantial penalty for removing from the front (all the data in the vector
have to get moved)

Use a vector for open and a list for close
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com

Write the computeHeuristic() function

Write the BestFirstCompare () function in the Node class. What should it compare
for Best-First? Also make sure SmartInsert calls this compare function.

Run it and see if it likes to get closer and closer to the goal. Does it do what you
expect it to do? Compare it to the completed version.
Dijkstra
This algorithm is similar to Breadth-First but find the most optimal solution. The problem
with Breadth-First was that as it went along, it only cared about plies. Dijkstra goes a step
further and keeps track of how much a path costs and every iteration of the loop it pops
the cheapest path. This means that every node needs to store a cost that indicates how
much we have paid to get to it from the start node. When Dijkstra goes to generate a
successor node, it adds the cost of reaching the parent of the successor node to the cost of
getting from the parent to the successor node. Note that the parent of the successor node
is the current node. Also, note that when going diagonal on a grid map, we have to add
some additional cost since a little longer distance is traveled.
In general
Given cost = parent’s cost + cost of going from the parent to this successor
When moving left, right, up, or down
Given cost = parent’s cost + mapData[y][x]
When moving diagonal
Given cost = parent’s cost + mapData[y][x] * √2
a = b = 1 unit
c2 = a2 + b2
c = √ (a2 + b2)
c = √2 = 1.4142…
Dijkstra finds a better solution than Breadth-First. It can also take into account regions
with different weights. If you run Dijkstra with diagonal penalty enabled (through the
planner settings dialog), it will find the most optimal path. In fact, no other algorithms
can find a more optimal solution than Dijkstra does. At the very best they can hope to be
as good.
A special complication occurs in A* that needs some extra code. When we did BreadthFirst, since we went ply by ply, it would never find a better path to a spot on the map for
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
which it had made a node already. Unlike Best-First and Breadth-First, you cannot
simply say: “If a node for a spot of the map has been created already, forget about it”.
Instead, you have to find out if the new path (i.e. successorNode) to that spot is better
than the one that has been created before (i.e. oldNode). How do you know if a node is in
fact better than another node? Check their given costs.
The proper pseudo-code for Dijkstra is:
3. Create a node for the start point and push it into the open list
4. While the open list is not empty
a. Pop a node from the open list and call it currentNode
b. Check to see if it is at the goal. If so, we can exit the loop
c. ONLY create and push a successor node into the open list if:
i. We have not already made a node for that spot of the map.
ii. Or, a successor node is better than a node that has already been
created.
d. Put the currentNode in to the closed list
Pseudo-code 2
Note: please be aware that it is not easy to see if the additional check (check labeled ii) is
working or not. This is because it only affects path planning when diagonal penalty is
ON. Even if it is turned on, the difference is not easy to detect. To test it, make sure you
are using diagonal penalty of 1.4142 (do not use sqrt(2.0) or anything else), run on BOP,
set goal coordinate to (10, 6), the given cost of the solution must be exactly 12.071 and
not anything else.
Dijkstra suffers from a substantial problem. It does still take up a lot of memory and CPU
time to find a path. Both Breadth-First and Dijkstra are Exhaustive searches. In other
words, they do not take advantage of the location of the goal to spend more time in
regions that are more likely to lead to the goal.
Dijkstra Pseudo-Code (this version is simple, but not perfect)
(Note: It will work if the distance to the neighbors are exactly the same. In other words, it will work
if you do not use diagonal penalty. The problematic lines have been highlighted. You must implement
the second version of Dikstra that is correct)
1)create the rootNode:
- set its x and y to the startPoint
- set its parent to NULL
- set its givenCost = 0
2)push rootNode in to open
3)while open is not empty
1)pop the node with the best givenCost from open and assign it to currentNode
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
2)if currentNode's x and y are equal to the goalPoint, then
// Note: we can traverse through the parents of currentNode
- push the nodes that are part of the path in to solution
- break from step 3
// Note: a node has 8 points around it which can be used to create min of 0 and
//
max of 8 successor
3)for every nearbyPoint around the currentNode do the following
1)if this nearbyPoint is in a spot that is illegal such as a wall, then
- skip to the next nearbyPoint
2)if a node for this nearbyPoint has been created before, then
- skip to the next nearbyPoint
3)create successorNode:
- set its x and y to the nearbyPoint
- set its parent to be currentNode
- set its givenCost = currentNode’s givenCoset + cost of spot
nearbyPoint is on
4)push successorNode in to open
4)push the currentNode in to closed
4)if the while loop completes without finding the goal, the goalPoint is unreachable
Dijkstra Pseudo-Code (correct version)
1)create the rootNode:
- set its x and y to the startPoint
- set its parent to NULL
- set its givenCost = 0
2)push rootNode in to open
3)while open is not empty
1)pop the node with the best givenCost from open and assign it to currentNode
2)if currentNode's x and y are equal to the goalPoint, then
// Note: we can traverse through the parents of currentNode
- push the nodes that are part of the path in to solution
- break from step 3
// Note: a node has 8 points around it which can be used to create min of 0 and
//
max of 8 successor
3)for every nearbyPoint around the currentNode do the following
1)if this nearbyPoint is in a spot that is illegal such as a wall, then
- skip to the next nearbyPoint
2)create successorNode:
- set its x and y to the nearbyPoint
- set its parent to be currentNode
- set its givenCost = currentNode’s givenCoset + cost of spot
nearbyPoint is on
3)if a node for this nearbyPoint has been created before, then
// Note: to see whether or not a node is in fact better than the other,
//
we have to compare their givenCost
- if successorNode is better than oldNode, then
- pop the oldNode and delete it
- else
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
- skip to the next nearbyPoint
4)push successorNode in to open
4)push the currentNode in to closed
4)if the while loop completes without finding the goal, the goalPoint is unreachable
Programming Hints

What should the given cost of a node be? Keep in mind that given cost is not the
weight of the terrain for the spot of the map the node is on. It is rather the
accumulated cost paid to get from the start node to a node. (It is based on the cost
of the parent and the mapData at that spot)

Make sure that the open list
Node::DikstraCompare() is being used.

First, get Dijkstra to work without diagonal penalty. The algorithm should look
similar to Breadth-First on a map with uniform weights. On a map with different
terrain weights, Dijkstra prefers less costly paths. Then add the code so that when
diagonal penalty is on, and the planner is going diagonal, addition cost is added.
In order to enable/disable the diagonal penalty option through the PlannerSettings
dialog, you have to make sure your Settings() function is implemented properly.
Compare your planner with the completed version. Note that when diagonal
penalty is true, the paths do not have unnecessary zigzags.
is
sorted
appropriately
and
that
A*
A* resolves most issues of Breadth-First, Best-First, and Dijkstra. It does not use a lot of
memory, it is fast, and it can find optimal paths. A* pretty much combines Best-First and
Dijkstra in the sense that it considers both the give cost (how much it cost to a node from
the start) and heuristic cost (how close a node is to the goal). A* keeps the open list
sorted by final cost.
final cost = given cost + heuristic cost
Note that the extra check we added to Dijkstra is necessary for A*. When doing A*, since
the heuristic is only an estimate of how close the node is to the goal, it can come across
many scenarios where it find a better path to a spot that has been visited already. When
A* goes to create a successor node and realize that it has created a node with the same x
and y already, it goes with the better one. Better means the cheaper or the one with the
lower given cost not the final cost.
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
A* guarantees to find an optimal path if the heuristic value is not an overestimate. This
means that if it costs 10 to get from start to goal, the heuristic does not say 15. Yet it
would be okay for it to underestimate and say we will only have to pay 5 to get to the
goal.
A* Pseudo-Code
(Note: This pseudo code is easier to understand, but you should implement the next pseudo-code
labeled “A* Pseudo-Code, Recycle Redundant Nodes”)
1)create the rootNode:
- set its x and y to the startPoint
- set its parent to NULL
- set its finalCost = givenCost + heuristicCost
2)push rootNode in to open
3)while open is not empty
1)pop the node with the best finalCost from open and assign it to currentNode
2)if currentNode's x and y are equal to the goalPoint, then
// Note: we can traverse through the parents of currentNode
- push the nodes that are part of the path in to solution
- break from step 3
// Note: a node has 8 points around it which can be used to create min of 0 and
//
max of 8 successor
3)for every nearbyPoint around the currentNode do the following
1)if this nearbyPoint is in a spot that is illegal such as a wall, then
- skip to the next nearbyPoint
2)create successorNode:
- set its x and y to the nearbyPoint
- set its parent to be currentNode
- set its finalCost = givenCost + heuristicCost
3)if a node for this nearbyPoint has been created before, then
// Note: to see whether or not a node is in fact better than the other,
//
we have to compare their givenCost
- if successorNode is better than oldNode, then
- pop the oldNode and delete it
- else
- skip to the next nearbyPoint
4)push successorNode in to open
4)push the currentNode in to closed
4)if the while loop completes without finding the goal, the goalPoint is unreachable
Note that in the algorithm above, there are scenarios where a node is created but skipped
because it is more expensive than a node we have previously made. There are also
scenarios where we delete a node and create another node instead. This happens when we
find a batter way to a spot on the map that we have visited (or created a node) before. The
pseudo code bellow does not create nodes unless they are needed, and reuses some nodes
when appropriate. It overrides the data of the node so that it is like a brand new node. It
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
then inserts the updated node into the open list. By doing so, much fewer nodes are
created, which reduces time and memory fragmentation.
A* Pseudo-Code, Recycle Redundant Nodes
1)create the rootNode:
- set its x and y to the startPoint
- set its parent to NULL
- set its finalCost = givenCost + heuristicCost
2)push rootNode in to open
3)while open is not empty
1)pop the node with the best finalCost from open and assign it to currentNode
2)if currentNode's x and y are equal to the goalPoint, then
// Note: we can traverse through the parents of currentNode
- push the nodes that are part of the path in to solution
- break from step 3
// Note: a node has 8 points around it which can be used to create min of 0 and
//
max of 8 successor
3)for every nearbyPoint around the currentNode do the following
1)if this nearbyPoint is in a spot that is illegal such as a wall, then
- skip to the next nearbyPoint
2)if a node for this nearbyPoint has been created before, then
// Note: to see whether or not a node is in fact better than the other,
//
we have to compare their givenCost not their finalCost
- if this nearbyPoint would make a node better than oldNode, then
// Note: by updating the oldNode, we avoid creating a new node for the
//
nearbyPoint
- update the oldNode:
- set its parent to currentNode
- set its givenCost = currentNode’s givenCoset + cost of spot
nearbyPoint is on
- set its finalCost = givenCost + heuristicCost
- if the oldNode is in closed, then
- pop it from closed and push in open
// Note: if the successor is in open already, any changes to its
//
finalCost should affects its priority
- else
- skip to the next nearbyPoint
3)create successorNode:
- set its x and y to the nearbyPoint
- set its parent to be currentNode
- set its finalCost = givenCost + heuristicCost
4)push successorNode in to open
4)push the currentNode in to closed
4)if the while loop completes without finding the goal, the goalPoint is unreachable
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
Programming Hints

Even though both A* pseudo-codes are similar in behavior, A* Pseudo-Code
Recycle Redundant Nodes creates less nodes by reusing redundant ones. You
should do the more optimal one.

Technically A* is the combination of Best-First and Dijkstra. Instead of using
only the heuristic cost (for Best-First) or given cost (for Dijkstra) it uses the final
cost.

Write the AStarCompare() function.

This step is the most tricky and important part. You must get this right! Instead of
simply saying “don’t generate nodes for spots of the map that we have generated
nodes for already”, we have to say “if we have generated a node for a spot of the
map already, compare the paths so that we can go with the better (cheaper) one”

Do we need to look in both the open and the closed list as we have been doing?
(yes)

How would you know if you have in FACT found a cheaper way? Could we
check the final cost or it should be the given cost? Even though you will get the
same result, you must base your decisions only on facts and not guesses
(heuristics)

What do you think we have to do if we find a cheaper path to a spot on the map?
We can update the old node by replacing its data as if it is a different node. The
new/updated node should be inserted in to the open list for reconsideration. If you
are reusing a node, be sure to remove it from any list it was in already. Even if
you are updating a node already in open, you have to remove and re-smartInsert it
so that it is moved to the proper location in the list. What do you think we have to
do if we find a more expensive path to a spot on the map? We should forget about
the worse path. In order to make sure this A* specific check works properly, make
sure when you run your planner on the BOP map, it re-explores the region of the
map indicated in the following image.
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
The indicated region is re-explored by A* because better paths than existing paths are
being found. This snap shot is taken from the Bop map, heuristic weight of one, and no
diagonal penalty.
Make sure however you layout your logic, the five scenarios are handled properly.
When you try to extend a path (create a node) and:
1. It would be a better path to a spot on the map, and the worse path is in open,
2. It would be a better path to a spot on the map, and the worse path is in close.
3. It would be a worse path to a spot on the map, and the better path is in open.
4. It would be a worse path to a spot on the map, and the better path is in close.
5. It would be a path to a spot on the map that has never been visited before.

Does your A* work? Before moving on make sure it does! If it almost works, it’s
probably entirely wrong. If you run it on the BOP map with heuristic weight of
one and no diagonal penalty (starting at 1,1 searching for 98, 98), you must get
exactly the same number of constructed nodes and runCount as the completed
version. (It is ok for the numbers to be different for other maps or different
settings because of the order in which successor nodes are created. Same holds for
other algorithms. The numbers for, say, breadth fist doe not have to match the
completed version, BUT A* with exact settings mentioned above must match the
numbers)

Once you have a working A*, add the heuristic weight. It will control the CPU
and memory usage vs. optimality of the solution. f =g + (w * h). The lager the
value of w, the more Best-First the A* algorithm will behave. The smaller the
value of w, the more Dijkstra the A* algorithm will be.

Make sure your diagonal penalty also works.

Next, you can start optimizing your A* algorithm. Before starting to optimize the
algorithm, make sure it is working perfectly.
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com

Best place to start is to use a better data structure for your open and specially
closed list. Since the closed list search is the most expensive operation in of the
planner, speeding it up can result in substantial improvement. You can implement
a hash map or a hash table and use the coordinates of a node in order to compute
as the key (or hash code). For the problem here you can compute a hash code by
saying: (x<<16|y). This hash code will work for integers that are no larger than 16
bit, which is plenty for our scenario.

There are numerous optimizations possible for the A* algorithm. You can refer to
articles in AI Wisdom or Game Programming Gems series. The table of content
of these books are available at www.aiwisdom.com
A* Additions

Once you had a working A*, add a heuristic weight. It will control the speed and
memory usage vs. optimality of the solution. f =g + (w * h)

Make sure your diagonal penalty also works.

Next, you need to write a hash table for your A* algorithm to speed it up.
(Implement it in A* Optimized not A*. That way if your hash tabled does not
work, I can give you some points for having a working A*. ) Since the closed list
search is the most expensive operation in the app, speeding it up can result in
substantial improvement. Instead of using a linked list for closed, you can use a
hash table. (Note: before trying to write the hash table, make sure your code is
working perfectly with the closed list)
o The hashTable should be an array of PlannerNode pointers. If multiple
nodes fall in the same bucket, use the hashTableNext field of PlannerNode
to emulate a singly linked list.
o Write the COMPUTE_HASH_CODE (x, y) macro. The macro should use
the pair of integers to generate a unique integer.
o Write the hash table functions. First implement HashTableInsert, then
HashTableFind, then HashTableRemove. When you write the functions,
make sure you test them for special cases. For example, what if the node
being removed is in the beginning, the middle, or the end?
o Anywhere you used the closed list use the hash table instead.
o Once you hash table is working totally remove the closed list from your
code.
A* On Node Mesh
This section explains how to implement A* so that performs path planning on a node
mesh (mesh of nodes, also known as waypoint graph) as opposed to a grid. Most games
use a form of node mesh to represent the world. A node can represent a point, area (such
as triangle or polygon), or volume (such as Axis Aligned Boxes or polygonal cells) that
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com
has a list of connections to other nodes. The connections can simply be a pointer to
another node. Alternatively, you can have edge nodes that represent connection between
two nodes, in which case each node would have a pointer to an edge object that has a
pointer to another node. Node meshes whose nodes represent points are sometimes
referred to as a waypoint mesh. Node meshes whose nodes represent triangles or
polygons are typically referred to as a navigation mesh. This section focuses on nodes
that represent a point and have a list of pointers to other nodes. The nodes of a node mesh
can be placed and connected manually by a level designer or generated algorithmically.
Here we want to build a node mesh equivalent to the grid and then perform A* on the
mesh. Each node will be connected to at most eight immediate neighbors using pointers.
The node class will store the position of the node, up to eight node pointers that represent
the connection to other nodes, as well as the cost of the map at the node’s position. Note
that storing the weight of the terrain in each mesh node works fine if the mesh represents
a perfect grid. You could instead store the weight in an edge node that connects two mesh
nodes. The following image shows how the mesh will look like. Note than in a 3D game,
you can place these nodes however you wish. You could even place one above another
one to represent a connection to the second floor of a building.
A node mesh that is equivalent to the underlying grid
Programming Hints

We can perform a Breadth-First traversal to create the MeshNodes and connect
them appropriately.

The PlannerNodes should no longer use a (x, y) pair and should instead have a
pointer to a MeshNode.

Instead of using a nested loop or a lookup table to compute the coordinate of the
neighbors, we should use the connection list in the MeshNode class.
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com

When storing the PlannerNodes in open/closed lists, we can use the memory of
their MeshNode pointer as the hash code. This would be more efficient than
retrieving the MeshNode of a PlannerNode, then getting the x and y, and then
performing (x << 16 | y).
Syrus Mesdaghi
Artificial Intelligence Course Director
Game Design & Development, Full Sail Real World Education
[email protected], 407.679.0100, www.fullsail.com