I think it’s interesting that you mentioned this:
I think the goal is to use actual pathfinding as little as possible, since that’s the most resource-intensive part of the pathfinding system.
But Roblox’s pathfinder, and, consequently, SimplePath (since it’s just a wrapper for Roblox’s pathfinder) all run in a worker thread, which should have little to no impact on the main game thread.
The Problem:
The biggest issue that I have with Roblox’s pathfinder is that it’s honestly just very slow. It frequently takes 150-300ms to actually come up with a path and in that time, at standard WalkSpeed
a humanoid character can move anywhere from 2.4 and 3.2 studs which means that, most likely, the first two waypoints or so are useless, or you end up waiting 150 to 300ms for the path and you’re behind 2.4 to 3.2 studs from where you should be. I do think you can work around these issues by just moving towards the target between path generations, which also has its own set of issues.
Obviously there are some other issues, like with :MoveToFinished
firing at different distances from the goal for various reasons. That inconsistency leads to miss-timing jumps and other issues along the path. There’s also issues of navigation mesh accuracy, I’ve seen a navigation mesh dip 60 studs into terrain, door ways are inaccessible because the door way is just not quite aligned enough with the grid used to generate the nav mesh, etc. Then there’s issues where you just can’t get very possible path, which is something the dev has no control over.
All that said, to your point, the issue is compounded at scale, since there’s only one worker thread for all Path
objects you end up causing a really bad problem where the pathfinder get further and further behind since there’s no way to cancel a call to ComputeAsync
you’ll end up calculating old, unnecessary paths before you do the most recent, higher priority ones.
Just thought all of that was worth noting to help drive the conversation a bit.
My Experience:
This problem is interesting because I’ve been thinking about it a lot lately. I’ve been working on a zombie survival game for over a year now and with the scale of the game, as well as the number of zombies I’m dealing with the built in pathfinding solution simply doesn’t cut it.
I will say, I have not solved this problem yet, but I have some interesting ideas, that I think are worth some discussion.
I’m dealing with a map that’s about (16000, 800, 16000)
stud map consisting of rough terrain, buildings, and trees and I operate about 6,000 zombies across the map currently.
My system is designed around overwhelming numbers versus “smart” zombies. So at the moment I do no pathfinding at all, but I’ll give some more details on future plans a bit further down in this book that I’m writing.
I’ll briefly outline some of the basic systems I’m using, which eventually I’d like to incorporate with some kind of pathfinding.
Breadcrumbs:
I employed a “breadcrumb” system where the zombies would keep incremental “waypoints” of their target’s position, upon losing line of sight they begin to rely on these waypoints and continue to track breadcrumbs for a few seconds. My implementation of this is fairly buggy but sometimes the results are shockingly cool to see. Depending on your situation this may not be a good option, also there’s no guarantee that the zombie can actual reach the breadcrumb. But, when it works it gives a really neat sort of pathfinding to a last known position.
Vaulting:
Since there are a lot of obstacles that the zombies would get stuck on I gave the zombies the ability to vault over objects that are around 8 studs tall. In my game this works pretty well and allows the zombies to maintain that “relentlessness” you mentioned.
Hordes:
Zombies work off of a sort of “hive mind” concept where they move together. This allows for an aggregate approach to pathfinding, where it’s feasible to create a single path for a whole horde of zombies, as well as a lot of other aggregation optimizations. There are a few complexities with pathfinding in this manner, which I’ll discuss later in the pathfinding section.
Pathfinding:
My first attempt at pathfinding with the horde technique was intentionally naive to expose what I knew would be an issue with this simple solution
Using Roblox’s Pathfinder with Hordes:
With the simple, naive solution of a single path for a horde there’s an issue that pops up pretty quickly that I like to call “The Train” or “The Conga Line”.
The Conga Line:
Naive single pathfinding for a horde results in a zombie train, which is not only unappealing to look at, it also means that the zombies aren’t going to go to what is technically the nearest waypoint. Consider the following example:
With a horde arranged in a pattern similar to this one, which is a frequent problem for me, you can quickly see the problem. With the “gimp” with the green lines shown in the picture the naive approach is to send it to the waypoint in the middle of the horde, but that means it needs to walk away from its goal before it moves towards the goal, and it also means you’ll end up with a ball of zombies that might be getting stuck on each other when they should really just continue in the path they’re already taking.
In this same picture, I was experimenting with a possible solution to the problem of picking the best waypoint for the zombie to start with.
Here’s a picture of what the naive approach looked like:
Line of Sight Waypoint Selection:
I attempted to come up with a good solution to the best first waypoint problem by checking line of sight to each way point, but in many scenarios I would be faced with maybe 50 zombies and 200 waypoints and if you needed to check every waypoint you’d need 50 x 200 raycasts (10,000) per path. Obviously this is a non-starter. I figured another less intense approach might be to use a sort of “binary sort” approach where I look at the last waypoint first, if I didn’t have line of sight then I’d go to the middle waypoint, and I’d continue to split the waypoint list in half and pick the middle waypoint closer to the start until I did have line of sight, then I would split the list of waypoints in half and pick the middle waypoint nearer to my last failed line of sight waypoint. This could work, but also suffers from a similar problem of not being very scalable. On average I’d find a solution in O(n * log(m)) (I think haha), about 115 raycasts which is good, but not good enough for my needs.
I moved away from this relatively quickly, so there may be more to it.
Back to the Drawing Board on Pathfinding:
I started to look around at other philosophies around pathfinding. I saw a video on Ultimate Epic Battle Simulator 2 showing a battle with 10,000,000 units, all of which are pathfinding, have nearest target selection, and have collision avoidance with each other. I was pretty blown away by this and I really couldn’t understand how something like this is possible.
To be totally honest, I’m still not sure how they pulled some of this off. But, for the pathfinding, I believe they’re using some form of Goal-Based Vector Field Pathfinding. This is a technique that has been used in RTS games for many years now. The idea is that instead of pathfinding from each unit to the goal, you create a vector flow field on a grid, where each cell in the grid points in the direction the unit needs to move in to reach a goal. This, when done correctly, gives a far more efficient pathfinder for large sets of units. This is the solution I’m currently exploring for my zombies, there are some unique challenges involved and a lot of things I still need to do to pull this off.
The Solution!
*I think
Goal-Based Vector Field Pathfinding with Hordes:
*(In case you missed it, here’s a nice basic explanation of the Goal-Based Vector Field Pathfinding algorithm.)
The immediate issue that comes to mind with this solution is the requirement of a grid, as I mentioned previously my map is (16000, 800, 16000)
with the addition of buildings and potential for caves and things like that, and if I want my zombies to be able to go anywhere then this grid also needs to be everywhere as well. Since my zombies don’t fly, I can ignore grid cells that are empty. This is all theoretical for my map, and whether I can keep a grid with a decent level of fidelity at this size in memory remains to be seen.
Since Roblox provides no tooling, that I’m aware of, to find surfaces (walkable or otherwise) then it’s necessary to create a tool to do this.
Spatial Mapping:
One of the biggest challenges with this technique, and really any pathfinding is spatial mapping.
One technique that comes to mind for spatial mapping is to employ the use of an Octree where with each section of the octree we scan for parts and terrain, if we hit a part or terrain then we subdivide the octree and repeat the test until we reach the smallest child octree size (1 stud perhaps) or the space is empty. We can ignore any empty sections since those should be air, and our zombies only really need to know about surfaces. This will leave us with a cubical representation of the space occupied by parts and terrain. See:
This is a nice first step, but we don’t really care about area inside of parts and terrain, we only care about the surfaces. So the next step is to remove any cells that lie within some Y threshold beneath another cell. See:
*This scene is a 512x512x512 volume with cells 2x2x2 and takes < 2 seconds to generate (parts included).
The next step would be to find connections between these cells, I haven’t given this part a ton of thought yet, if I’m being honest. But my initial thought here is that looking at adjacent cells and comparing their height differences is a good place to start. For example, if one cell is on top of a wall and an adjacent cell is on the ground then we might want a connection going in one direction.
After that the next step would be to find connections between adjacent grids in the top level octree, this will be useful later for optimizing the pathfinder. Additionally we should record a link between the adjacent cells in the connected grids.
Additionally, we can make this spatial map dynamic thanks, again, to the octree data structure. It’s really easy to determine what parts of the spatial map need to be reevaluated given the position and volume of any change that might occur, although this isn’t something we want to do in constant time, it should be fairly quick to update.
Pathfinding:
The Goal-Based Vector Field Pathfinding algorithm requires that we evaluate a whole grid each time we want to find a path to a point, and that there exists a version of the flow field grid for each goal. Obviously we don’t want to evaluate every grid cell on the surface of a 16,000x16,000 stud map, so we need a solution to cut down on the number of grids we need to evaluate.
So the next step is to do some lower fidelity pathfinding. One of the really great things about the octree spatial map is that now we have a really big grid, perhaps 512x512x512 studs per cell, that we can do some pathfinding on. We can employ the classic A* technique on these top-level octrees to find the sections we actually want to evaluate the grids for. Consider worst case on a 16k x 16k map with 512x512 grid the number of possible grids that need to be updated would be ~50. I’m never going to pathfind that far, maybe 1000 studs at most so that would narrow the evaluation grid down to ~2 (more in some cases where there exists no connection between “adjacent” grids).
This is a huge optimization taking us from needing to update ~980 grids to ~2 grids.
Finally, the last step that comes to mind is to actually perform the Goal-Based Vector Field Pathfinding algorithm, the only difference is that your implementation would need to traverse multiple grids, thanks to the step we performed earlier where we recorded links between the adjacent cells between grids this should be somewhat trivial.