XNA Creators Club Online
Page 1 of 1 (12 items)
Sort Posts: Previous Next

A* / Manhattan pathfinding optimisation problems

Last post 12/9/2009 1:32 PM by Xerx3s. 11 replies.
  • 11/19/2009 4:50 PM

    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.

  • 11/19/2009 6:53 PM In reply to

    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
  • 11/19/2009 7:59 PM In reply to

    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.
  • 11/29/2009 12:21 PM In reply to

    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!
  • 11/29/2009 5:36 PM In reply to

    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.
  • 12/1/2009 4:35 PM In reply to

    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.
  • 12/3/2009 10:32 AM In reply to

    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)
  • 12/3/2009 12:42 PM In reply to

    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

    In playtest Flite
  • 12/3/2009 10:41 PM In reply to

    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  

  • 12/4/2009 12:39 PM In reply to

    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
  • 12/5/2009 5:59 PM In reply to

    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.


  • 12/9/2009 1:32 PM In reply to

    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?
Page 1 of 1 (12 items) Previous Next