-
-
- (70)
-
premium membership
-
Posts
59
|
A* / Manhattan pathfinding optimisation problems
|
Hi,
I'm just doing some research for a project that I'm working on. To do that, I've build a little TD game that continuesly spawns enemies and allows the player to keep on placing turrets (so everything is realtime). I've done quite a bit of reading on the basics of A* and such both on this forum and places such as this. I get the basics and have written a class (based on said article) that returns the next step to the enemy. The class works pretty well but the problem is not one enemy. The problem is when I have 5,10 or more enemies. Every extra enemy severely slows down the game.
Currently I only call the next target from the A* class when the enemy enters a new grid coordinate but I've tried several other approaches as well such as only recalculating the path when a new obstacle is placed but that slows down placing obstacles.
I currently have a couple of linq elements in the class but I doubt that rewriting that will give me a fluid game with 50 - 100+ enemies. Currently the grid is roughly 50*30 points, which is not small but I don't want to make that smaller either.
I've been thinking about minimising the call the the A* class by using the same path for enemies that are on the same grid coordinate but theoretically that still wouldn't solve the problem (i.e. when all enemies are on a different position).
I've also been thinking about using arrays instead of lists but that could give problems. What if the number of steps is larger than the array? The only way that I could account for that is to make the array really big but that kind of nullifies the switch from list to array (?). Besides, that is more of a memory solution and this seems more of a processing problem (?).
Sooooo..... Any suggestions how a lot of enemies can use an independent A* routine without any serious slowdown?
My A* method:
| 1 |
public static Vector2 FindPathAstarBasic(Vector2 _coordinateCurrent, Vector2 _coordinateExit) |
| 2 |
{ |
| 3 |
Vector2 _return = Vector2.Zero; |
| 4 |
|
| 5 |
#region AI |
| 6 |
#region Step 0 |
| 7 |
List<Instance_Path> List_OpenList = new List<Instance_Path>(); |
| 8 |
List<Instance_Path> List_ClosedList = new List<Instance_Path>(); |
| 9 |
|
| 10 |
Vector2 PositionCurrent = _coordinateCurrent; |
| 11 |
Vector2 PositionExit = _coordinateExit; |
| 12 |
|
| 13 |
Instance_Path TempPathContainer; |
| 14 |
|
| 15 |
Vector2 TempPathCoordinateCurrent; |
| 16 |
Vector2 TempPathCoordinateAdjacent; |
| 17 |
int TempPathMovementCostG; |
| 18 |
int TempPathMovementCostH; |
| 19 |
|
| 20 |
bool ExitNotReached = true; |
| 21 |
bool NextStepNotFound = true; |
| 22 |
bool FinalStepFirstMoveNotSet = true; |
| 23 |
bool StepDoesntExist = true; |
| 24 |
|
| 25 |
Vector2 ReturnStepParent = Vector2.Zero; |
| 26 |
Vector2 ReturnStepCurrent = Vector2.Zero; |
| 27 |
#endregion |
| 28 |
#region Step 1 |
| 29 |
List_OpenList.Add(new Instance_Path(PositionCurrent, PositionCurrent, 0, 0)); |
| 30 |
#endregion |
| 31 |
#region Step 2 |
| 32 |
while (ExitNotReached) |
| 33 |
{ |
| 34 |
#region Step 2a |
| 35 |
TempPathContainer = new Instance_Path(Vector2.Zero, Vector2.Zero, 999999, 999999); |
| 36 |
foreach (Instance_Path _path in List_OpenList) |
| 37 |
{ |
| 38 |
if (_path.MovementCostG + _path.MovementCostH < TempPathContainer.MovementCostG + TempPathContainer.MovementCostH) |
| 39 |
{ |
| 40 |
TempPathContainer = _path; |
| 41 |
} |
| 42 |
} |
| 43 |
#endregion |
| 44 |
#region Step 2b |
| 45 |
List_OpenList.Remove(TempPathContainer); |
| 46 |
List_ClosedList.Add(TempPathContainer); |
| 47 |
#endregion |
| 48 |
#region Step 2c |
| 49 |
#region Up |
| 50 |
TempPathCoordinateCurrent = TempPathContainer.ElementGridCoordinate; |
| 51 |
TempPathCoordinateAdjacent = TempPathCoordinateCurrent - new Vector2(0, 1); |
| 52 |
TempPathMovementCostG = TempPathContainer.MovementCostG + 10; |
| 53 |
TempPathMovementCostH = CalculateHeuristicManhattan(TempPathCoordinateAdjacent, PositionExit); |
| 54 |
|
| 55 |
if (TempPathCoordinateAdjacent.X >= Game.Manage_Players.List_Players[0].GridBoundaries.X && |
| 56 |
TempPathCoordinateAdjacent.X <= Game.Manage_Players.List_Players[0].GridBoundaries.Z - 1 && |
| 57 |
TempPathCoordinateAdjacent.Y >= Game.Manage_Players.List_Players[0].GridBoundaries.Y && |
| 58 |
TempPathCoordinateAdjacent.Y <= Game.Manage_Players.List_Players[0].GridBoundaries.W - 1) |
| 59 |
{ |
| 60 |
if (Game.Manage_Obstructions.CheckObstruction(TempPathCoordinateAdjacent)) |
| 61 |
{ |
| 62 |
if (!List_ClosedList.Exists(delegate(Instance_Path I) { return I.ElementGridCoordinate == TempPathCoordinateAdjacent; })) |
| 63 |
{ |
| 64 |
StepDoesntExist = true; |
| 65 |
foreach (Instance_Path _path in List_OpenList) |
| 66 |
{ |
| 67 |
if (_path.ElementGridCoordinate == TempPathCoordinateAdjacent) |
| 68 |
{ |
| 69 |
if (TempPathMovementCostG < _path.MovementCostG) |
| 70 |
{ |
| 71 |
_path.MovementCostG = TempPathMovementCostG; |
| 72 |
_path.MovementCostH = TempPathMovementCostH; |
| 73 |
_path.ParentGridCoordinate = TempPathCoordinateCurrent; |
| 74 |
} |
| 75 |
|
| 76 |
StepDoesntExist = false; |
| 77 |
} |
| 78 |
} |
| 79 |
|
| 80 |
if (StepDoesntExist) |
| 81 |
{ |
| 82 |
List_OpenList.Add(new Instance_Path(TempPathCoordinateAdjacent, TempPathCoordinateCurrent, TempPathMovementCostG, TempPathMovementCostH)); |
| 83 |
} |
| 84 |
} |
| 85 |
} |
| 86 |
} |
| 87 |
#endregion |
| 88 |
[SNIP FOR SPACE, SAME AS UP BLOCK ABOVE] |
| 89 |
#endregion |
| 90 |
#region Step 2d |
| 91 |
foreach (Instance_Path _path in List_ClosedList) |
| 92 |
{ |
| 93 |
if (_path.ElementGridCoordinate == PositionExit)//Vector2.Distance(_path.ElementGridCoordinate, PositionExit) < 1) |
| 94 |
{ |
| 95 |
ExitNotReached = !ExitNotReached; |
| 96 |
break; |
| 97 |
} |
| 98 |
} |
| 99 |
|
| 100 |
if (List_OpenList.Count() <= 0) |
| 101 |
ExitNotReached = !ExitNotReached; |
| 102 |
#endregion |
| 103 |
} |
| 104 |
#endregion |
| 105 |
#region Step 3 |
| 106 |
while (NextStepNotFound) |
| 107 |
{ |
| 108 |
foreach (Instance_Path _path in List_ClosedList) |
| 109 |
{ |
| 110 |
if (FinalStepFirstMoveNotSet) |
| 111 |
{ |
| 112 |
if (_path.ElementGridCoordinate == PositionExit) |
| 113 |
{ |
| 114 |
//_return.Add(_path.ElementGridCoordinate); |
| 115 |
|
| 116 |
ReturnStepCurrent = _path.ElementGridCoordinate; |
| 117 |
ReturnStepParent = _path.ParentGridCoordinate; |
| 118 |
FinalStepFirstMoveNotSet = false; |
| 119 |
} |
| 120 |
} |
| 121 |
else if (_path.ElementGridCoordinate == _path.ParentGridCoordinate && _path.ElementGridCoordinate == ReturnStepParent) |
| 122 |
{ |
| 123 |
_return = ReturnStepCurrent; |
| 124 |
NextStepNotFound = false; |
| 125 |
break; |
| 126 |
} |
| 127 |
else if (_path.ElementGridCoordinate == ReturnStepParent) |
| 128 |
{ |
| 129 |
//_return.Add(_path.ElementGridCoordinate); |
| 130 |
|
| 131 |
ReturnStepCurrent = _path.ElementGridCoordinate; |
| 132 |
ReturnStepParent = _path.ParentGridCoordinate; |
| 133 |
} |
| 134 |
} |
| 135 |
} |
| 136 |
#endregion |
| 137 |
#endregion |
| 138 |
|
| 139 |
return _return; |
| 140 |
} |
EDIT: Needless to say that I've tried several of the suggestions of the article but none really got me anywhere near the numbers where I want to be.
|
|
-
-
- (1591)
-
premium membership
-
Posts
469
|
Re: A* / Manhattan pathfinding optimisation problems
|
I haven't thoroughly looked at your code since you say it works, just slowly. Off the top of my head, it seems like you are running the algorithm each time an enemy changes grid spots and only to get the next direction to move. Do you really need to do it for each grid transition, or can you instead change the algorithm to return a full path (e.g. a list of waypoints to the goal)? This would reduce the executions to once per enemy when they are spawned and once per enemy when a new object is placed. Since you have multiple enemies, you might want to consider running the algorithm in parallel (multithreading). Enemies generally don't need to know where each other is going, so there shouldn't be any issues there.
Alternately, you could calculate the pathfinding for each grid coordinate instead of for each enemy. Each grid coordinate would only need to know which grid enemies need to go to next, not the entire path. However, you would still need to update each grid whenever a new object is added.
I used a modified A* algorithm during the AI based DBP contest awhile back. While I never got to the point of needing to multithread, I had good performance in my game. I used a "hive mind" mentality of each bug having no knowledge of the grid to start with and looking for an exit in a sweeping pattern. Once an exit is found, the bugs find the best way to the exit using the accumulated knowledge of the map so far. Additionally, the traps in the game have to be overcome somehow, and this was added to the algorithm with some "hinting" mechanisms. Basically, if no direct path could be found, the algorithm would look for indirect paths, such as making a bug go back to an acid bath so it could walk through walls to get to the goal. The algorithm I used was only called when a new bug was generated (if no goal found, the bug would pathfind to the next grid in the search pattern, else it would pathfind to the goal), when a bug got to its search goal (if no goal found, pathfind to next search grid, else pathfind to goal) or if a bug encountered an obstacle in its path (if no goal found, pathfind to the next search grid, else pathfind to the goal). If you want to see it, you can download it here (Requires 2.0 framework though): http://squeebles.pouncingkitten.com
|
|
-
|
|
Re: A* / Manhattan pathfinding optimisation problems
|
I don't mean to be harsh, but I think using A* for your problem is a very poor decision.
A* is used for path FINDING. Do your entities need to FIND paths? Not really. Its a TD, they go on predefined paths and shouldn't really ever deviate significantly from them.
Set up way points and use flocking algorithms, it will be significantly faster and will also look way smoother. Use the flocking sample provided on this website, and add a small attraction field at pre-determined distances (waypoints). Once an entity reaches a way point, switch its waypoint to the next.
This is what I do for my AI units that just travel in a straight line / path. It works marvelously.
Edit: If you do feel the need to use A*, understand that it is not an inherently robust algorithm and it is rarely if ever used to a void collisions between two dynamic entities. One optimization you could do is use A* only when the entity is spawned and save the nodes that it wants to travel on. Then use some flocking algorithm to keep the horde of units away from each other. But then again, this approach is exactly like the one I mentioned above, except you replace pre-programmed waypoints with A* generated waypoints.
Madness? This is SUNSHINE.
|
|
-
-
- (70)
-
premium membership
-
Posts
59
|
Re: A* / Manhattan pathfinding optimisation problems
|
Sorry I didn't reply any sooner, I run two companies and as you can imagine, there sometimes is little time for pet projects.
Chris Covington: I don't see another way to do it, obstructions can be added on the fly. So even if I where to give them a path, I would still need to recalculate every path element for every enemy when placing the said obstruction (alternately, I could recalculate when the enemy concludes that his path is outdated but that could give some weird results). That would still slow down the game.
Because I allow the player to add obstructions, it is very well possible that there are branches in the path or even multiple paths (depending on where the enemies are when the object is placed).
I've narrowed the entire problem down to the options list, there are just too many in there when obstructions are added that it needs to consider. If I could narrow down the list, it would massively speed up things.
andorov: Nothing better than a harsh teacher. :) Quite frankly, I'm a complete n00b on many things, especially AI. I've only started last month or so. But I do like to learn about it. Al the reading material pointed towards A* being a good starting point.
Thx for the pointer on flocking and such, I'm checking it out now!
|
|
-
-
- (0)
-
premium membership
-
Posts
17
|
Re: A* / Manhattan pathfinding optimisation problems
|
Have you run this on the 360, and how does the speed compare on the two platforms?
If the slowdown is significantly more on the 360, it could be garbage collection, which is notoriously bad on the 360. Is your Instance_Path type a class or a struct? If it's a class, one easy thing to try is to convert it to a struct, since you're creating a lot of those every time you run the function, and structs are a lot easier on the garbage collector than classes.
If it's bad on the PC, garbage collection is probably less of the issue, though. I assume you've verified that it is the pathfinding causing the slowdown and not some other part of your enemy code that scales poorly?
It would probably be a good exercise to run your game through NProf, and see if that gives you any direction on where the time is being spent.
|
|
-
|
|
Re: A* / Manhattan pathfinding optimisation problems
|
I only glanced at the code; but I can tell where the problem is. Each creep having it's own path is *fine*, infact probably best (it prevents subtle problems; e.g. adding a turret in the path that a creep has already passed through). However, each one must cache it's current path; doing NxA* per frame is gonna be slow. The problem with that is the creep will walk through a block that has become occupied since it's path was built.
You need to recaculate all affected paths (and ONLY those paths) whenever the player places a turret. I will demonstrate with what you have to do with some odd pseudo-code. The biggest bit is the INotifyPropertyChanged; Google it if you need to understand what it does (it's pretty self-documenting).
class Cell : INotifyPropertyChanged
{
notify bool IsOpen;
Cell Top;
Cell Left;
Cell Right;
Cell Bottom;
// Whatever else-not.
}
The critical part is the IsOpen property than notifies subscribers. So now let's look at the path class.
class Path
{
PathNode NextNode;
handler OneOfMyPathNodes_PathNodeInvalidated()
{
PathManager.Recalculate(this);
}
}
The next logical step is the PathNode class.
class PathNode
{
PathNode NextNode;
event PathNodeInvalidated;
Cell TargetCell;
handler TargetCell_PropertyChanged()
{
if(PropertyName == "IsOpen" && !TargetCell.IsOpen)
{
PathNodeInvalidated();
}
}
}
So, the basic idea that only if the path becomes obstructed (a tower is placed there) do we updated the given path.
Hopefully this gets you going in the right direction (and hopefully my wierd pseudocode makes sense).
meh.
|
|
-
-
- (373)
-
premium membership
-
Posts
75
|
Re: A* / Manhattan pathfinding optimisation problems
|
I've recently finished doing some optimisation on my A* pathfinding in my current project. Whether this will be of any use to you, I don't know.
Just for some background, my game is 2D tile based with map sizes that can be in excess of 500*500 tiles. I have managed to optimise the game to the point where I can now have 50 enemies on the map at any one time, all trying to find the player using A* pathfinding where each map tile is a node that can be passable or not. That's a lot of nodes.
I gave my enemy class a few AI states: Idle, Following and Navigating. Following is when the enemy has direct line of sight on the player and is moving in a direct path towards the player without using A*. Navigating is of course where the enemy can't see the player and is following its A* path. Each enemy has their own Breadcrumb of nodes that they're following.
To get the pathfinding running smoothly, I needed to run the actual A* on a separate thread. Roy's library is fast, but it isn't able to run on multiple threads, which means that I could only have one pathfinding thread separate from the game thread. To that end, I used a global flag to indicate that the AI thread was in use (this is where better developers than I are probably shaking their heads). Here's roughly how my enemy update method ran:
--
If AI state is Idle Does enemy have line of sight to the player? Yes: Set AI state to Follow No: Set AI state to Navigating
If AI state is Navigating Does enemy already have a path to follow? Yes: Move towards next node in path No: We need to find a path Is the AI thread in use? (check global flag) Yes: Do nothing No: Set in use flag and asynchronously launch the pathfinding Does enemy have a line of sight to the player? Yes: Switch AI state to following No: Carry on
Asynch pathfinding method: Find a path using A* library Copy returned path to this enemy's path Unset AI thread in use flag
If AI state is Following Move toward player location
Has enemy collided with a wall? Set AI state to Idle
--
The last part, checking for a wall collision, is important. If the A* node map changes (in your case, if you place a tower/other obstacle down) then the enemy won't know about it as it's already following a stored path. By checking for a collision and resetting the AI state, it forces the enemy to re-calc a path.
I should also note that Roy's A* library is garbage heavy, so if you're worried about GC collections causing glitches then you'll need to jump through hoops to make an allocation-free algorithm.
I really want to write a tutorial combining Nick's tile engine and Roy's pathfinding and plan to do so once I'm done working on my current project. I'm probably not the best person to do it, but I'll give it a shot.
Gareth Williams - MCTS .Net 2 Web Applications - Team Mango - My Blog - Gravsheep - Dysnomia (In Peer Review)
|
|
-
-
- (650)
-
premium membership
-
Posts
355
|
Re: A* / Manhattan pathfinding optimisation problems
|
One thing you could try, is to queue the requests as well.
When a needs to update its route (as others have said, this should be less often than you are currently doing it). It could add a request to the queue. The A* engine then pulls these off the front.
The unit could continue (attempting) to follow its current route, until the new one is done. (I think I had some code that stiched routes together, but I am not sure if I went down that path).
Each update you could evaluate 1 route, so every unit would be guaranteed to be visited in under a second (if you had 60 units).
Going on from there.... Like others have said, I think I ended up splitting up the algorithm so it could just do part of a route finding in a single update, and then I went the whole hog, and got the route finding to occur on a dedicated thread.
If you can get your algorithm to the point where it can happily do 1 route in an update, you might be able to avoid going multithreaded.
J
|
|
-
|
|
Re: A* / Manhattan pathfinding optimisation problems
|
You cannot just use List and hope to achieve the correct running speed. A* requires a priority queue, which is a datastructure with these three operations: - Insert(x) - insert this element
- DeleteMin() - delete the minimum element and return it
- DecreaseKey(x,key) - change the key associated with this element
The last operation may be omitted for simplicity but best performance is achieved when it is available. A binary heap is a traditional effective implementation in which all three operations run in logarithmic time (in English, this means they are fast). The region called Step 2a in your code is your version of DeleteMin: You are finding the minimum element and deleting it. You are doing this by searching the entire list of open cells. This technique is called "linear scan" and is too slow for this purpose. A binary heap will instantly find the minimum and then spend a small amount of time rearranging itself to delete the item.
You are repeating the same mistake in several other places such as when looking in the closed list and whatever you are doing in the final step. I guarantee that these linear scans are causing a huge slowdown in your algorithm.
The boldface words above are topics you should be able to research with Google and Wikipedia. The .NET library does not have a binary heap that I know of, but I think SortedDictionary might provide a reasonable substitute if you cannot write a better one yourself.
Instead of using the closed list you should use a grid that you only allocate once to access that in constant time. To avoid clearing that grid, you can use a query counter to see if a cell has been visited during the current query. Here is some pseudo-C# that I hope explains how to do it with a priority queue and a grid: | 1 | struct Cell | | 2 | { | | 3 | public int distance, heuristic; | | 4 | public Vector2i predecessor; | | 5 | public int queryId; | | 6 | public bool closed; | | 7 | } | | 8 | | | 9 | Cell[,] grid; // allocate only once, not at every query | | 10 | int queryId; | | 11 | | | 12 | public List<Vector2i> findPath(Vector2i from, Vector2i to) | | 13 | { | | 14 | queryId++; | | 15 | grid[from].distance and heuristic = 0 | | 16 | | | 17 | PriorityQueue<int,Vector2i> queue = new ..; | | 18 | queue.insert(0, from); | | 19 | bool found = false; | | 20 | while (queue.notEmpty()) | | 21 | { | | 22 | Vector2i current = queue.deleteMin(); | | 23 | if (current == to) | | 24 | { | | 25 | found = true; | | 26 | break; | | 27 | } | | 28 | if (grid[current].closed) | | 29 | continue; | | 30 | grid[current].closed = true; | | 31 | int dist = grid[current].distance; | | 32 | int heu = grid[current].heuristic; | | 33 | for dx=-1 to 1, and dy=-1 to 1 | | 34 | { | | 35 | if (dx == 0 && dy == 0) | | 36 | continue; | | 37 | | | 38 | Vector2i target = current + Vector2i(dx,dy); | | 39 | if (target not inside map bounds) | | 40 | continue; | | 41 | | | 42 | int mov = dist + <movement cost from current to target>; | | 43 | if (grid[target].queryId < queryId) | | 44 | { | | 45 | grid[target].distance = mov; | | 46 | grid[target].heuristic = <heuristic function at target>; | | 47 | grid[target].queryId = queryId; | | 48 | grid[target].closed = false; | | 49 | grid[target].predecessor = current; | | 50 | queue.insert(grid[target].distance, target); | | 51 | } | | 52 | else if (grid[target].distance > mov) | | 53 | { | | 54 | // TODO using decreaseKey here can speed things up | | 55 | grid[target].distance = mov; | | 56 | grid[target].predecessor = current; | | 57 | queue.insert(grid[target].distance, target); | | 58 | } | | 59 | } | | 60 | } | | 61 | if (!found) | | 62 | return null; // no path | | 63 | List<Vector2i> path = new ..; | | 64 | Vector2i p = to; | | 65 | while (p != from) | | 66 | { | | 67 | path.add(p); | | 68 | p = grid[p].predecessor; | | 69 | } | | 70 | path.reverse(); | | 71 | return path; | | 72 | } | | 73 | |
|
|
-
-
- (782)
-
premium membership
-
Posts
85
|
Re: A* / Manhattan pathfinding optimisation problems
|
I have to say, I'm with andorov. Waypoint navigation is a much more practical solution in the real world. Points and paths can be calculated at design time, freeing up resources at run-time.
I tend to use dynamic checks on top of the path table to deal with with path to choose, to adapt to the minor changes in the environment (ie where enemies/objects are).
In review - Brethren of the Coast - chris@polarbluegames.co.uk
|
|
-
|
|
Re: A* / Manhattan pathfinding optimisation problems
|
If you have little guys walking around the grid and they ned to move in a certain general direction but avoid obstacles, you could use a simple random based path approach.
Step 1: Identify the current grid
Step 2: Identify the acceptable grids for the next movement
Step 3: Pick on at random
Step 4: Check if there is an obstacle, if no, move if yes, pick another grid at random.
Step 5: If all acceptable grids are blocked, have a special case to get it unstuck, like haveing it follow the left edge for a few updates or something.
Might not be exactly what you're looking for, but it would run lightning fast. :)
Check out www.xortron.net for the latest Xortron updates and tutorials or join our facebook group! Did you appreciate my comment? If so, help me by playtesting my game, Xortron 5000 Ultimax Superbeast :) Was my comment belligerent, self-rightous, or otherwise nefarious? Get revenge by playtesting my game, Xortron 5000 Ultimax Superbeast and criticizing it ruthlessly.
|
|
-
-
- (70)
-
premium membership
-
Posts
59
|
Re: A* / Manhattan pathfinding optimisation problems
|
Cheers for the pointers peeps, some really useful stuff in there, I'll dig through it once I have some more time. As it stands now, I've drastically optimised the code through a combination of some of the approach theories posted here and the use of heaps.
Had thought out an even better script (roughly 7 times faster than the one I'm using now) but that seemed to give me a very odd bug. If the creeper was on Y coordinate 0, I could place obstructions up and down the Y coordinate but for some reason the 5th obstruction down specifically refused to register (so the creeper went through the obstruction). Needles to say that I've been over that code a million times but the bug just made no sense. At the very least you would have expected it to happen to all directions or the first or last of a list somewhere.
Not really an issue though. I've tested the code as it is up to 256 creepers (can probably go way beyond that now) but the real game should only need 20 creepers (because that's not a TD game :) ).
PS: How do you reply to a thread without replying to a specific post?
|
|
|