-
-
- (0)
-
premium membership
-
Posts
91
|
|
The game I'm making isn't an RTS, but there are lots of people needing to find their way to a place, while avoiding any obstacles.
I have a grid of 128*128 nodes, perhaps more if I can optimise enough. I'm using the A* algorithm to move my people around, and they are constantly moving. When a person decides they want to go somewhere, they set their destination variable, and add their index to a list. A seperate thread is looping in the background. It checks the list and removes the first index. It uses the index to read the position and destination of said person. It calculates the path and puts it into the person's waypoint list and then continues to the next iteration. The person then has its path and removes each waypoint when reached, ending up at the destination.
The problem is, if my people don't calculate their paths quickly, the game lags a bit, looks bad, and the people actually die, as they really need to get to their destination. Basically I want to know how to fix this. I have seen games such as Stronghold send over a hundred units on their way on a huge grid without stopping to think. How do these games manage it? And how should I deal with it?
Thanks for any help,
Frozenwounds.
|
|
-
|
|
Re: RTS-Scale Pathfinding
|
First optimization is ofcourse to check your algorithm for easy speed gains:
-Any
looping of nodes inside the main loop is costly, and should really be
avoided. Dont store closed/open lists as actual lists.
-If path
doesnt always have to be optimal, you can cheat a little by finishing
the search when destination is found - not when its marked closed.
-Limit
search for example to 500-1000 nodes, after which tell the character go
to node with smallest estimated distance to goal found and calculate
another path when he gets there.
|
|
-
-
- (0)
-
premium membership
-
Posts
91
|
Re: RTS-Scale Pathfinding
|
u551:First optimization is ofcourse to check your algorithm for easy speed gains:
-Any looping of nodes inside the main loop is costly, and should really be avoided. Dont store closed/open lists as actual lists.
What else would I do? I've implemented a binary heap, but using Lists underneath it all for the open list, but I don't see how this would make a difference with the closed list?
And thanks, I might implement the 'cheating' method. I don't know about limiting the search though, there are situations when that would make the search unsolvable.
|
|
-
|
|
Re: RTS-Scale Pathfinding
|
Ive usually set closedness as a boolean attribute of node rather than have list of closed nodes and then just check if (node.closed).
|
|
-
-
- (0)
-
premium membership
-
Posts
91
|
Re: RTS-Scale Pathfinding
|
And you think that would be faster than just querying the list?
I suppose the Contains method would have to loop through the list itself?
|
|
-
-
- (8273)
-
premium membership
MVP
-
Posts
6,126
|
Re: RTS-Scale Pathfinding
|
You also want to optimize at a higher level. 16,000 nodes is *a lot*, even on a desktop CPU (and on the Xbox, it will absolutely crawl).
You probably want to split your playing field into sectors, where each sector might be 10x10 cells, and then build a separate connectedness graph of sectors to sectors. Within each sector, build a graph of paths from inputs to outputs. Both of those are easy to pre-calculate -- in fact, you can use Dijkstra to pre-calculate the entire sector/sector and cell input/output path finding solutions.
Then, when path finding, you want to run four find passes:
1) Pathfind from the sector you're in, to the sector the target is in. If both are in the same sector, just pathfind from source to target, and you're done.
2) Pathfind from source to the exit of the sector it's in leading to the next sector in the higher-level path.
3) When entering a new sector, set the local path to the (pre-calculated) path that takes you from entry to the next cell you want to go to.
4) When entering the sector that contains the target, pathfind from the entry node to the target node.
This has several benefits:
a) You pre-calculate the vast majority of paths, spending no time path finding while on the "cell to cell" network.
b) The local path-finding that you actually do is much shorter, as it's only within a single sector, rather than potentially spreading to the entire game board.
c) The state stored per entity is actually less, as you only need to store the high-level path (sector-to-sector), plus a short local path within the current sector.
Jon Watte, Direct3D MVP Tweets, occasionallykW X-port 3ds Max .X exporter kW Animation source code
|
|
-
|
|
Re: RTS-Scale Pathfinding
|
Frozenwounds:And you think that would be faster than just querying the list?
I suppose the Contains method would have to loop through the list itself?
I suppose so too. MSDN states its O(n) operation where n is the length of the list, and it tends to get somewhat large towards the end of the calculation. Single boolean comparison on the other hand takes virtually no time.
|
|
-
-
- (0)
-
premium membership
-
Posts
91
|
Re: RTS-Scale Pathfinding
|
@u551
Thanks, I'll do that.
@JWatte
Ouch! This will take me a while to implement but I see your logic. Also, surely the instance would have to calculate two paths itself, the one to the next 'big' node, but no the one in its own sector as this could result in doubling back. Then it should calculate from the second last big node to the finishing point. This would make sure there was very little going back on itself. I'll post back if/when I eventually implement this method.
|
|
-
-
- (8273)
-
premium membership
MVP
-
Posts
6,126
|
Re: RTS-Scale Pathfinding
|
Because each sector (big node) has a finite set of entry points and exit points, each sector could store the result of pre-calculating the path finding from each entry to each exit. That way, each unit don't have to calculate that, once entering a sector. The only two paths needed are from the current position to the next "big node," and from entering the final "big node" to the actual position within that node. The rest is taken care of by the navigation system pre-calculation. Also, note the special case: when source and destination is in the same big node, you just calculate a single path, from src to dst, but within a single sector.
Also, you don't want to calculate the final path segment up front, if it's possible that the unit can die before getting there. That's why you should keep only a local path for the current sector in each unit.
Jon Watte, Direct3D MVP Tweets, occasionallykW X-port 3ds Max .X exporter kW Animation source code
|
|
-
-
- (425)
-
premium membership
-
Posts
286
|
Re: RTS-Scale Pathfinding
|
Take a look at BRAINS which has part of what JWatte describes
You can split a "World" into a cluster of grids. Each grid can be searched independantly or you can search across grids and it will find the path across multiple grids.
It's not quite complete in that it's not inteligent enough to find the fastest path across grids and could get stuck if it comes across a grid with no path. It's not far off though, so you could implement that while learning the code, or wait for me to get around to it :)
|
|
-
-
- (0)
-
premium membership
-
Posts
91
|
Re: RTS-Scale Pathfinding
|
jwatte:Because each sector (big node) has a finite set of entry points and exit points, each sector could store the result of pre-calculating the path finding from each entry to each exit. That way, each unit don't have to calculate that, once entering a sector. The only two paths needed are from the current position to the next "big node," and from entering the final "big node" to the actual position within that node. The rest is taken care of by the navigation system pre-calculation. Also, note the special case: when source and destination is in the same big node, you just calculate a single path, from src to dst, but within a single sector.
Also, you don't want to calculate the final path segment up front, if it's possible that the unit can die before getting there. That's why you should keep only a local path for the current sector in each unit.
What I've done is calculate paths from/to the center of each sector, the method using every entry point won't work for me, as the loading time is already atrocious. I might double the distance between nodes (half the grid width and height) to ease off a bit on the pathfinding, depending on whether this works with my graphics. I still don't seem to be achieving as much as other games however, such as Stronghold which I mentioned before. Has anybody played that game? Most of the routes my people have to figure out are short anyway, so it's actually quite rare for the sectors to be used.
|
|
-
-
- (8273)
-
premium membership
MVP
-
Posts
6,126
|
Re: RTS-Scale Pathfinding
|
the loading time is already atrocious
If level are designed (rather than random), you can pathfind at compile time rather than load time.
Jon Watte, Direct3D MVP Tweets, occasionallykW X-port 3ds Max .X exporter kW Animation source code
|
|
-
-
- (0)
-
premium membership
-
Posts
91
|
Re: RTS-Scale Pathfinding
|
jwatte:the loading time is already atrocious
If level are designed (rather than random), you can pathfind at compile time rather than load time.
Unfortunately they're wonderfully generated. Anyway this still doesn't seem to be working. I did consider the option of saving every path calculated so that one path wouldn't be calculated more than once, however some paths would have to be deleted as the map changed, and would this end up a major memory drain? It would obviously take a while for the saved paths to encompass much. But the up side is that they do encompass paths from anywhere in their list to anywhere else on their list, so they would contain more than one path. I would probably have to create some imaginitive class to store all that information however.
Wow I can really rant sometimes.
Edit:
A float is 4 byes, *2 for a Vector2, so a path with 128 steps would take up a kilobyte. With 512mb RAM in an Xbox, I think a couple of megabytes of paths would be fine? Also, I could create my own class with a number smaller than a float, this would cut down on the memory needed. I would realistically only need one byte to store a coordinate number of the grids I'll be using.
|
|
-
|
|
Re: RTS-Scale Pathfinding
|
Frozenwounds:the loading time is already atrocious
Hmm... In case every 10x10 node sector had all the sides open, roughly 40*40 pathfindings would have to be done per sector, you would end up with like bit over 200k calculations, which should be pretty much superfast considering the size of the areas. So it shouldnt be more than a couple of seconds +loading time, maximum? Could we see some of your pathfinding code please?
Frozenwounds:
A float is 4 byes, *2 for a Vector2, so a path with 128 steps would take up a kilobyte. With 512mb RAM in an Xbox, I think a couple of megabytes of paths would be fine? Also, I could create my own class with a number smaller than a float, this would cut down on the memory needed. I would realistically only need one byte to store a coordinate number of the grids I'll be using.
For path from every node to every node youd need (128*128)^2 paths (again, if all areas were open). And supposing one path is on average a kilobyte, the storage would take up almost 270Gb of memory... (woah, did i really calculate this right?)
|
|
-
-
- (0)
-
premium membership
-
Posts
91
|
Re: RTS-Scale Pathfinding
|
u551:
For path from every node to every node youd need (128*128)^2 paths (again, if all areas were open). And supposing one path is on average a kilobyte, the storage would take up almost 270Gb of memory... (woah, did i really calculate this right?)
Not quite. I wouldn't be saving every possible path, just the ones that an instance had asked for. One path could actually encompass several too, for instance I save a path from point A to B to C to D. When an instance wants a path from D to B it would search through the archive of paths for a path that contained both D and B, and come up with the aforementioned path. It would then clip the ends and reverse the path if necessary. Also, you have a valid point, I would have to make sure that I set a reasonable limit on the memory that can be taken up. There would be other mechanisms with this too, such as when a new, longer path is calculated, if it contains one of the paths from the archive, it would overwrite that one rather than add a new one.
Also this idea of saving every entrance to every sector, just doesn't seem right to me, I'm not sure how I would program anything to make use of this. The idea of using the centre of each one to try to compute a mostly pre-calculated path works for me, but I'm struggling with the concept of why I would want to figure out all the edges.
I would post some code, but there's so much abstraction that it would probably make no sense without a huge page. Basically if the destination isn't in the same sector, it figures out a path through the sectors, then adds all of the saved paths between the sectors together, and adds a path from the start position to the second sector node, and from the second last sector node to the destination. Second first and last to prevent the instance from travelling to the centre of the sector it's in before heading out to the second because this could cause it to retrace its steps.
|
|
-
|
|
Re: RTS-Scale Pathfinding
|
You don't have to precalculate every exit->exit on sectors if that doesnt seem right to you, but the multilayered pathfinding can be used in some way regardless, like i see you've done already.
So how fast is it working now?
Speaking of huge code citations here's a crude pathfinder class i wrote a while ago for a zombie game, it doesnt use any very fancy optimizations but still works without noticeable fps hit even with hundreds of enemies. Probably is of no help now but anyways...
| namespace Zombie |
| { |
| static class PathFinder |
| { |
| |
| private static int depthLimit = 2000; // how many nodes to search before giving up |
| |
| /// <summary> |
| /// Method to try get path from start to goal |
| /// </summary> |
| public static Stack<Vector2> GetPath(Map kartta, Vector2 start, Vector2 goal) |
| { |
| |
| // compute tile coordinates rather than world... |
| int startX = Routines.pixelsToTiles(start.X); |
| int startY = Routines.pixelsToTiles(start.Y); |
| int goalX = Routines.pixelsToTiles(goal.X); |
| int goalY = Routines.pixelsToTiles(goal.Y); |
| |
| // reserve some memory for the node array |
| PNode[,] nodes = new PNode[(int)kartta.getDimensions().X, (int)kartta.getDimensions().Y]; |
| |
| // this is our "open list" |
| MinHeap open = new MinHeap(); |
| |
| // make node for the start point, push it to open list and array |
| PNode beginning = new PNode(startX, startY, Math.Abs(startX - goalX) + Math.Abs(startY - goalY), null); |
| open.Push(beginning); |
| nodes[startX, startY] = beginning; |
| |
| int etsitty = 0; // just a counter so we know when to give up |
| PNode curNode; // the current node |
| |
| while (etsitty < depthLimit && !open.isEmpty()) |
| { |
| // lets take the most promising one from open list |
| curNode = open.Pop(); |
| |
| // mark it as closed |
| nodes[curNode.x, curNode.y].closed = true; |
| |
| // if it so happens that we hit the goal, lets postprocess path some and return it |
| if (curNode.x == goalX && curNode.y == goalY) |
| { |
| return postProcessPath(nodes, curNode, beginning); |
| } |
| |
| // checking the adjacent, not very elegant but.... |
| int aX, aY; |
| for (int i = 0; i < 4; i++) |
| { |
| aX = curNode.x; |
| aY = curNode.y; |
| if (i == 0) aY--; //north |
| else if (i == 1) aX++; //east |
| else if (i == 2) aY++; //south |
| else aX--; //west |
| |
| // if theyre walkable |
| if (kartta.getPenetrability(aX, aY)) |
| { |
| // if not visited, lets add to open list and array |
| if (nodes[aX, aY] == null) |
| { |
| PNode uus = new PNode(aX, aY, Math.Abs(aX - goalX) + Math.Abs(aY - goalY), curNode); |
| nodes[aX, aY] = uus; |
| open.Push(uus); |
| } |
| // if visited, but path from where we just came is shorter than previous, update values |
| else if (curNode.g + 1 < nodes[aX, aY].g) |
| { |
| nodes[aX, aY].setParent(curNode); |
| } |
| |
| } |
| |
| |
| |
| } |
| |
| etsitty++; |
| } |
| |
|
| // fail... lets just return something |
| Stack<Vector2> paluu = new Stack<Vector2>(); |
| |
| // if there's something on the open list, return the best from there |
| if (!open.isEmpty()) |
| paluu = postProcessPath(nodes, open.Pop(), beginning); |
| |
| else // open list is empty -> path is not even possible |
| paluu.Push(goal); // stupid zombie will now try to walk there directly |
| |
| |
| return paluu; |
| |
| } |
| |
| |
| /// <summary> |
| /// Just a method to remove any non-necessary nodes from the path, making it more natural |
| /// </summary> |
| private static Stack<Vector2> postProcessPath(PNode[,] taulukko, PNode end, PNode start) |
| { |
| |
| Vector2 prev; |
| Stack<Vector2> paluu = new Stack<Vector2>(); |
| |
| PNode curNode = end; |
| paluu.Push(curNode.asVector()); |
| |
| Vector2 toC = new Vector2(-(Runtime.tileSize / 2), -(Runtime.tileSize / 2)) * 0.98f; |
| Vector2 toC2 = new Vector2(Runtime.tileSize / 2, -(Runtime.tileSize / 2)) * 0.98f; |
| |
| while (curNode != start && curNode.getParent() != start) |
| { |
| prev = curNode.asVector(); |
| |
| curNode = curNode.getParent(); |
| |
| // lets unwrap until we find a node that is not visible from the last one |
| //... from every corner to every appropriate corner |
| // (edit: or not exactly corners because that would go to next tile in some cases |
| // resulting in path next to a building not getting cleaned) |
| bool canSkip = true; |
| while (canSkip) |
| { |
| canSkip = false; |
| if (curNode.getParent() != start) |
| if (Routines.clearSight(prev + toC, curNode.getParent().asVector() + toC)) |
| if (Routines.clearSight(prev + toC2, curNode.getParent().asVector() + toC2)) |
| if (Routines.clearSight(prev - toC, curNode.getParent().asVector() - toC)) |
| if (Routines.clearSight(prev - toC2, curNode.getParent().asVector() - toC2)) |
| { |
| curNode = curNode.getParent(); |
| canSkip = true; |
| } |
| } |
| |
| paluu.Push(curNode.asVector()); |
| } |
| |
| return paluu; |
| |
| |
| } |
| |
| } |
| |
| /// <summary> |
| /// Class for single node |
| /// </summary> |
| class PNode |
| { |
| // koordinaatit |
| public int x { get; set; } |
| public int y { get; set; } |
| |
| public int g { get; set; } //distance start->this |
| public int h { get; set; } //distance this->goal (estimate) |
| public int f { get; set; } //weight, function of the two previous ones |
| |
| public bool closed { get; set; } // joko tämä on tutkittu loppuun |
| |
| private PNode parent; // parent, so we can backtrack the path when goal is found |
| |
| public PNode getParent() { return parent; } |
| |
| public Vector2 asVector() { return new Vector2(x * Runtime.tileSize + (int)(Runtime.tileSize / 2), y * Runtime.tileSize + (int)(Runtime.tileSize / 2)); } |
| |
| public void setParent(PNode newParent) |
| { |
| // change parent, recalc f and g |
| parent = newParent; |
| g = parent.g + 1; |
| calculateF(); |
| } |
| |
| public PNode(int px, int py, int ph, PNode pparent) |
| { |
| x = px; |
| y = py; |
| |
| h = ph; |
| if (pparent != null) |
| setParent(pparent); |
| else |
| { |
| // no parent, this starting point probably |
| g = 0; |
| calculateF(); |
| } |
| } |
| |
| private void calculateF() |
| { |
| // this pattern can be tuned.. |
| f = g + h; |
| } |
| |
| } |
| |
| /// <summary> |
| /// Storage structure for open list nodes |
| /// </summary> |
| class MinHeap |
| { |
| PNode[] heap; |
| int last; |
| |
| public MinHeap() |
| { |
| heap = new PNode[100]; |
| last = -1; |
| } |
| |
| public void Push(PNode node) |
| { |
| last++; |
| |
| if (last > heap.GetUpperBound(0)) |
| { |
| PNode[] h2 = new PNode[last * 2]; |
| System.Array.Copy(heap, h2, last); |
| heap = h2; |
| } |
| |
| heap[last] = node; |
| bubbleUp(last); |
| } |
| |
| public PNode Pop() |
| { |
| PNode paluu = heap[0]; |
| |
| heap[0] = heap[last]; |
| last--; |
| |
| if (last > 0) |
| bubbleDown(0); |
| |
| return paluu; |
| } |
| |
| private void bubbleUp(int index) |
| { |
| while (heap[index / 2].f > heap[index].f) |
| { |
| swap(index, index / 2); |
| index = index / 2; |
| } |
| } |
| |
| private void bubbleDown(int index) |
| { |
| if (!hasLeftChild(index)) return; |
| |
| int lowerChild = getLowerChild(index); |
| |
| while (hasLeftChild(index) && heap[index].f > heap[lowerChild].f) |
| { |
| swap(index, lowerChild); |
| index = lowerChild; |
| lowerChild = getLowerChild(index); |
| } |
| |
| } |
| |
| private bool hasLeftChild(int index) { return last >= index * 2; } |
| private bool hasRightChild(int index) { return last >= index * 2 + 1; } |
| |
| private int getLowerChild(int index) |
| { |
| if (!hasRightChild(index) || heap[index * 2].f < heap[index * 2 + 1].f) return index * 2; |
| else return index * 2 + 1; |
| } |
| |
| private void swap(int a, int b) |
| { |
| PNode apu = heap[a]; |
| heap[a] = heap[b]; |
| heap[b] = apu; |
| } |
| |
| public bool isEmpty() |
| { |
| return (last < 0); |
| } |
| |
| public String toString() |
| { |
| String paluu = "HEAP["; |
| foreach (PNode n in heap) |
| { |
| if (n != null) |
| paluu = paluu + "" + n.f + ","; |
| } |
| return paluu + "]"; |
| } |
| |
| } |
| } |
|
|
-
-
- (0)
-
premium membership
-
Posts
91
|
Re: RTS-Scale Pathfinding
|
Hey thanks for the help. I think I've got the A* about as optimised as it's gonna get. The thing with saving previously calculated paths works really well, I made a little counter to see how many times something used a previously calculated path, and you'd be surprised, it racks up a few hundred in a couple of minutes, and that's with only 12-16 people wandering about. I think my next hurdle will be the graphics, as each person has their own model to draw. The speed of the pathfinding actually takes a noticable hit as you have more people, and not just because more paths are calculated. Thanks again, I'll mark the topic as solved. :)
|
|
-
-
- (8273)
-
premium membership
MVP
-
Posts
6,126
|
Re: RTS-Scale Pathfinding
|
You don't need to save a Vector2 for each step of a pathfind. Just save the direction you need to move in from each step. This can easily be stored in a byte. (If you're really gung-ho, you can store 4 steps in a single byte, if you only allow N/E/W/S movement, even :-)
Jon Watte, Direct3D MVP Tweets, occasionallykW X-port 3ds Max .X exporter kW Animation source code
|
|
-
-
- (0)
-
premium membership
-
Posts
91
|
Re: RTS-Scale Pathfinding
|
|
|
-
-
- (8273)
-
premium membership
MVP
-
Posts
6,126
|
Re: RTS-Scale Pathfinding
|
That wouldn't work with the saved paths though. :\
As long as you know where you start for each path, it will work. You can calculate the next position by adding/subtracting the tile size from the previous step.
Jon Watte, Direct3D MVP Tweets, occasionallykW X-port 3ds Max .X exporter kW Animation source code
|
|
|