Game Development Community

dev|Pro Game Development Curriculum

Flexible A* Pathfinding System

by Dan Keller · 04/08/2008 (2:15 pm) · 203 comments

1.8.1 version here!

Download

Note: The TGEA version in the download is newer than the TGE version. Most people are using TGEA now, anyways. If you want the newer version in TGE, you can port it yourself, it shouldn't be too bad. If there is some interest in it and someone does a port, just send it to me and I'll put it up here.

Installation - TGEA

Add aStar.h, aStar.cpp, aStarIO.cpp, and aStarMesh.cpp to your project (in T3D). Replace or merge aiPlayer.cpp and .h with the files in in the zip.

Add #include "T3D/aStar.h" on top of sceneState.cpp

Add the commented part in SceneState::renderCurrentImages():
PROFILE_START(RenderInstManagerRender);
   gRenderInstManager.render();
   PROFILE_END();
//aStar
#ifdef AI_RENDER
   AStar::Get()->render(this);
#endif
//aStar
   gRenderInstManager.clear();

   mInteriorList.clear();


Add this in game/tools/missionEditor/scripts/editors/creator.ed.cs line 146:
%Mission_Item[6] = "NavMesh";

Add the following function at the end of game/tools/missionEditor/gui/objectBuilderGui.ed.gui:
function ObjectBuilderGui::buildNavMesh(%this)
{
	%this.className = "NavMesh";
	%this.process();
}

In [game]/scriptsAndAssets/server/scripts/game.cs, at the end of startGame(), add
readPaths();

At the end of endGame(), add
deletePaths();

before resetMission();

For an example, add aiDemo.mis.


Installation - TGE

Add the aStar.cc and aStar.h files to your project. Replace or merge aiPlayer.cc and .h with the files in in the zip.

In sceneState.cc, after the includes, add
#include "game/aStar.h"

in renderCurrentImages(), around line 481, add the commented code
for (i = 0; i < mTranslucentEndImages.size(); i++)
      renderImage(this, mTranslucentEndImages[i]);
//aStar
#ifdef AI_RENDER
   gAStar.render(this);
#endif
//aStar
   glMatrixMode(GL_MODELVIEW);
   glPopMatrix();

Save and compile.

In creator/editor/EditorGui.cs, at line 1306, change
%Mission_Item[3] = "Trigger";
   %Mission_Item[4] = "PhysicalZone";
   %Mission_Item[5] = "Camera";

to

%Mission_Item[3] = "NavMesh";
   %Mission_Item[4] = "Trigger";
   %Mission_Item[5] = "PhysicalZone";
   %Mission_Item[6] = "Camera";

In creator/editor/objectBuilderGui.gui, at line 643, add
function ObjectBuilderGui::buildNavMesh(%this)
{
	%this.className = "NavMesh";
	%this.process();
}

In [game]/server/scripts/game.cs, at the end of startGame(), add
readPaths();

At the end of endGame(), add
deletePaths();

before resetMission();

For an example, add aiDemo.mis.

Use

To add navigation points, open the mission editor creator. NavMesh objects are under Mission Objects / Mission. The properties are:
Interval - distance between points
XSize - number of points in x direction
YSize - number of points in y direction
Height - how far up or down the object will look to place points

There are two global preferences you can change. One is $Pref::AStar::Render. It controls whether or not the navigation mesh is rendered. Turn it on when editing the mesh, turn it off otherwise. The other is $Pref::AStar::Clearence. This controls the minimum horizontal clearance between two nodes when building paths. If you change this after creating points, you need to call buildPaths for it to effect those points.

There are three functions for path caching. Do not call any of these while the mission editor is running, it may get all messed up. They are
- deletePaths Deletes all NavMesh objects and all nodes.
- writePaths Writes all nodes to a file (called [mission name].as) that is loaded automatically on mission startup. This is the only function you need to call manually; call it once you're done editing the nav mesh.
- readPaths Reads nodes from file. Returns true on success, false on faliure.

Also there is
- buildPaths Manually rebuilds navigation information, for example if an object has been moved in the way of some nodes.

There is also a compiler switch, AI_RENDER, at the top of aStar.h. Comment this out when you distribute the game to remove all the nav mesh rendering code.

There are five functions that can be called on the AIPlayer object. They will return 0 on success, -1 on failure, and 1 if the bot is already where you want it to go. They are
- findPathTo This takes either a point or an object as an argument, and simply finds a way to the point.
- findCoverFrom This also takes a point or object, and finds a place hidden from it.
- searchCover This takes no arguments, and makes the bot go to the nearest place it can't see.
- sneakUpOn This takes either a point and a vector or an object as an argument. It finds a path to the point or the object that can't be seen by it.
- wander This takes a distance as an argument and wanders around for this distance.
#181
06/05/2010 (12:51 pm)
hi Dan
I encountered a new problem,
I can now use the A * console command, when i build a two-story building, use buildpaths instructions only produce mesh in 2F, Part of the first floor cant build mesh, so i use two navigation mesh to build the 1F and 2F of the mesh, i call bot use findPathTo command can found 1F objects, but 2F objects use findPathTo command did not respond.....

Do you know how I find the objects in 2F ???
#182
06/05/2010 (2:04 pm)
I get an error when I click on your link, so I'm not sure if this is exactly the problem you described, but for a bot to be able to get between floors there needs to be nodes between the two floors, like on a staircase or ramp.
#183
06/05/2010 (2:24 pm)
thx dan your reply
but still have problem
i have change a link for picture;
i can see in house's mesh, i guess there are normal's mesh
#184
06/05/2010 (2:28 pm)
Hmm, looks fine to me. How are you calling findPathTo?
#185
06/05/2010 (2:35 pm)
i call bot1.findPathTo(goal2);

==>bot1.findPathTo(goal1);
Mapping string: 2477|PathSimple to index: 14

==>bot1.findPathTo(goal2);
* no any response

i try goal1 in a house(only 1F) but it is success

my problem occur for only more than in 2F's house
if only 1F it is ok.
#186
06/05/2010 (3:41 pm)
Maybe, try calling it with a location on the second floor rather than an object.
#187
06/06/2010 (5:22 pm)
hmm i am trying...but,

==>bot1.findPathTo(414.624 791.626 150.042);
parse error

am i use wrong console command??

%bot.findPathTo(location)
location not equal position?
#188
06/06/2010 (5:57 pm)
Quote:bot1.findPathTo(414.624 791.626 150.042);

Should be:

bot1.findPathTo("414.624 791.626 150.042");

The quotation marks are needed.
#189
07/27/2010 (10:47 pm)
Wow, good to see this resource is still drawing interest, and you're still in the loop with it Dan! My online car combat game has been using it to nice effect for the last 2 years. The main efficiency change I made was to add a binning system so start/end nodes could be found quickly rather than searching the whole set of nodes (and so that I could efficiently decide if the position of a player car is already known in the nodes before adding it).

What I'd *really* like to do now is add some kind of cost-surface calculation to the route decision, rather than accept the 'shortest' route - which could actually include hills, valleys and other stuff that really messes with a car's momentum. Perhaps I'd do this using a pheromone-trail approach, i.e. whichever routes the human players use most will be strengthened by reducing the 'cost' of travelling between their nodes.

However... I find the code after your "ok, we've got start and end NMPoints, time for some a*" to be very opaque. Clearly you wrote it for maximal efficiency, but could you suggest what I might need to do in order to make use of a 'cost' function at each link (rather than the current implicit equal costing for each link right now)?

edit: what are the 'f' and 'g' members of AStarPoint used for, and the 'interval' member of NMPoint?

edit2: seems fairly obvious actually.. 'f' is distance of a node from the destination, 'g' is distance of a node from the start location? - so I should be able to code up 'interval' on each node so it represents a 'cost' (e.g. distance + steepness) rather than simply a fixed distance value?
#190
07/31/2010 (5:15 am)
f is the "cost" of going thru that node, it's equal to g (the cost of getting there) + h (a heuristic or guess of how far is left after it) The h is what makes A* different from other path search algorithms like Dijkstra.

Anyway, what you are looking for is on line 258 of aStarMesh.cpp (assuming you're using TGEA).
newPt->interval = mesh->interval;
Each point has an interval attribute, giving an idea of how far it is from its neighbors, which is used in working out the g cost. In the code as it is, it's set for each point as the interval of the mesh it came from. You would want to replace this line with something that calculates the "move cost" of using the point.

Also, just FYI, in the line
curpt[olSize].g = curpt->g + curpt->pt->interval*((i&1)?1.4f:1);
that last bit with the i&1 multiplies the interval by 1.4 (the square root of 2) if the link between the points is going on a diagonal. You may or may not want to change this for your game. (by removing everything including and after the *.
#191
09/28/2010 (8:15 pm)
What's the status of the TGE version of this? I tried dropping in the (I suppose outdated?) TGE code in the download and got these errors:

5>aStar.obj : error LNK2001: unresolved external symbol "private: void __thiscall AStar::sortPoints(class Point3F)" (?sortPoints@AStar@@AAEXVPoint3F@@@Z)
5>aStar.obj : error LNK2001: unresolved external symbol "private: struct NMPoint * __thiscall AStar::findPoint(class Point3F)" (?findPoint@AStar@@AAEPAUNMPoint@@VPoint3F@@@Z)

I see sortPoints() and findPoints() are declared in aStar.h but they're never declared in aStar.cpp.
#192
09/29/2010 (1:03 am)
I suppose it would be "deprecated and unsupported."

Although when I looked at it both functions are declared in both aStar.h and aStar.cc. The linker error makes it look like you forgot to add a .cc file to your project, though.
#193
09/29/2010 (3:37 am)
Turns out I had aStar.h from the TGE code and aStar.cc from the TGEA code added. Yeah, it's been one of those days.

Although if the TGE version is indeed deprecated I'm not sure it matters.
#194
09/29/2010 (6:41 pm)
Little update on this. I've ported the (newer?) TGEA version back to TGE. The only snag I've hit is a crash when writePaths() is called. writePaths() also crashes in the old TGE version as well so it's a bit of a stumbling block.

There's also an issue where defining AI_RENDER causes console spam. In AIPlayer::getAIMove() if AI_RENDER is defined the path state of the AIPlayer is appended to its name. This is nifty for debug purposes but it does this every time getAIMove() is called, regardless of whether the path state has changed. The result is the console being spammed with something like:
Mapping string: 1443|PathNone to index: 12
Mapping string: 1435|PathNone to index: 13
Mapping string: 1434|PathNone to index: 14
Mapping string: 1463|PathNone to index: 15
Mapping string: 1442|PathNone to index: 16
Mapping string: 1437|PathNone to index: 17
Mapping string: 1436|PathNone to index: 18
Mapping string: 1454|PathNone to index: 19
Mapping string: 1455|PathNone to index: 20
Mapping string: 1441|PathNone to index: 21
Mapping string: 1438|PathNone to index: 22
Mapping string: 1462|PathNone to index: 23
Mapping string: 1457|PathNone to index: 24
Mapping string: 1433|PathNone to index: 25
Mapping string: 1456|PathNone to index: 26
Mapping string: 1404|PathNone to index: 27
Mapping string: 1439|PathNone to index: 28
Mapping string: 1461|PathNone to index: 29
Mapping string: 1493|PathNone to index: 30
Mapping string: 1458|PathNone to index: 31

Not terribly important to fix this but it's worth noting.
#195
10/01/2010 (8:39 am)
writePaths() only causes a crash when run while the editor is open. Despite the crash, the .as file is still saved properly. Calling writePaths() outside of the editor works fine. Still debugging this one.

Everything else is working nicely. I've made some minor modifications, mainly stripping out the AI_RENDER/ENABLE_DEBUGDRAW defines and instead rendering navmeshes automatically when using the editor. I've even added a handy console function to enable/disable in-editor rendering - this is hooked into the editor menu now for easy access.

Awesome stuff, Dan!

Edit: More regarding the writePaths() in-editor crash:

After some debugging I traced the issue to the AStar::deleteAll() method. writePaths() calls deleteAll() as part of its cleanup routine. deleteAll() simply iterates through the 'meshes' vector, calls deleteObject() on the actual NavMesh object and then removes the reference from the 'meshes' vector via pop_front().

When you're outside of the editor this works fine. Inside the editor, when deleteObject() is called, NavMesh::onRemove() is also called. NavMesh::onRemove() looks like this:

void NavMesh::onRemove()
{
	removeFromScene();
	Parent::onRemove();
    if (isServerObject() && gEditingMission)
        AStar::Get()->removeMesh(this);
}

The issue here is the if statement. Outside of the editor this will eval to false, inside the editor it will eval to true. If true, it will cause AStar to remove the NavMesh object from the meshes vector. That's a problem.

In short, the order of events in the editor is something like: deleteAll() -> meshes[0].deleteObject() -> NavMesh::onRemove() -> AStar::Get()->removeMesh(this) -> meshes.pop_front() <-- crash here.

Possible fix might be simply modifying deleteAll() to first remove the NavMesh reference from the 'meshes' vector and then call deleteObject(). Something like:

void AStar::deleteAll()
{
	if (!list)
		return;

	while(meshes.size())
	{
		meshes.pop_front();
		meshes[0]->deleteObject();
		//meshes.pop_front();
	}

	delete list;
	list = 0;
	points = 0;
}

But it's late and I'm not sure if doing things in that order leaves a NavMesh dangling.
#196
10/05/2010 (2:16 am)
Ive been playing around with this and need some explanation for a few things. First, I notice in your sample mission you have multiple NavMeshes, what is the purpose of this?

Also, I am doing an RTS and the NavMesh(s) would need to change all the time. After doing some experimenting, it looks like I would have to call buildpaths quite often, so would having lots of NavMeshes or 1 big one freeze the game until its done generating? I dont have much use for the caching since like I said, the mesh(es) would need to be regenerated quite often. Any advice as to how to use this resource properly?
#197
10/05/2010 (5:22 am)
Navmesh is basically a macro object to define a grid of navigation points. So you will typically need several navmeshes as one grid won't cover everything you want.

Buildpaths is a very processor-expensive method and should only be called at level creation time. Its run time doesn't depend on number of meshes, only number of points.

Why would you need to change the navmeshes though? It's only really necessary if there's a significant change in the level's geometry.

If you really want to do this I would suggest implementing a way to rebuild only an area of the map, or to do it bit by bit, or in another thread, or some combination of those.
#198
10/05/2010 (6:51 am)
Really the only reason any change to the mesh would need to take place was if a wall or a building was placed. Any suggestion for what to do in this instance?
#199
10/06/2010 (5:01 am)
I would make a list of points within some distance of the wall/building, iterate thru those points, doing raycasts between each point and the elements in its adjacents array to see if the building blocks it, and if it does disconnect the points. I would still use the caching because it would save you rebuilding the entire mesh at the start of the game.

EDIT: essentially you want to re-write linkMeshes to run on a limited subset of points
#200
10/06/2010 (6:22 pm)
Hmm. So I've stumbled into another crash being caused by the way NavMeshes are removed. It's reproducible (for me, anyway) by going into the world editor, adding a NavMesh, buildPaths(), writePaths(), File -> Quit. This will cause an Unhandled Exception and point to the following line in NavMesh::removeMesh():

else //also works for last b/c mesh->last->next == 0
        mesh->first->prev->next = mesh->last->next; //lol

Some quick debugging showed mesh->first->prev no longer exists (meaning mesh->first->prev->next can't be referenced). Commenting out those lines replaces the Unhandled Exception with an Assert in platformMemory.cc - something about array alloc mismatch.

On a hunch I commented out the deletePaths() line in endGame() just to see what would happen. That solved the crash but NavMeshes weren't being properly cleaned up once a mission was over (obviously). I then uncommented deletePaths() and commented out the following line in NavMesh::onRemove():

if (isServerObject() || gEditingMission)
        AStar::Get()->removeMesh(this);

I'm not the best at reading other people's code but my thinking here was: NavMesh::onRemove() is deleting all NavMeshes, endGame() is then calling deletePaths() which then attempts to delete all NavMeshes again, causing a crash. Am I totally off base on that one?

The one thing that concerns me with all of this is why I seem to be the only the one running into these problems. I've tried both the TGE and TGEA versions included in the .zip on fresh installs of TGE 1.5.2 and ran into the same issues so I don't believe it's any changes I've made in my own build.

Lastly, two questions:

1). Does the 'fix' to AStar::deleteAll() look right?
void AStar::deleteAll()
{
	if (!list)
		return;

	while(meshes.size())
	{
		meshes.pop_front();
		meshes[0]->deleteObject();
		//meshes.pop_front();
	}

	delete list;
	list = 0;
	points = 0;
}

2). Does the 'fix' involving NavMesh::onRemove() I mentioned above look right?