A while back, I wrote about pathfinding and how the townsfolk in Banished figure out how to get from one place to another. I’ll quote myself here. Pathfinding is the one system in the game that I am constantly fixing and making better. I’ll think I’ve gotten it right, and two months later…
While the problem of getting of the AI from point A to point B solved, I’ve lately been hearing feedback about where the townsfolk decide to go. This is even more true since I adding the pathing tool and you can easily see where people are relative to their workplace.
The problem is that when townsfolk decide where to go, they pick the closest accessible location and use it. While using closest linear distance works, the closest location isn’t always the best. Sometimes walking distance far exceeds linear distance. While this isn’t technically a bug, it certainly causes issues in large sprawling towns and requires a little attention.
Here’s an simple example of a bad decision. The workers in the house below used linear distance to determine where to work.
When they actually go to work, they find a long path there – all the way down to the bridge and back. This is fine and the simulation works – but it could be more optimal. This isn’t just for workplace either. It’s for deciding where to pickup food and firewood, which school to use, what market to use, where to find resource to make tools, etc
I could just let players deal with the issue or build more bridges, but I decided to take a day (or two) and see if I could come up with a fast way of quickly determining walking distance without resorting to doing full path finding.
Finding walking distance using the current path finding system is really slow. To use the current path finding system to make AI decisions would be so slow that the game would run at single digit frame rates. The AI considers a potentially large set of locations when deciding where to go. It’s just too many paths to examine.
I first thought about caching paths from critical interact points, but this soon became unwieldy. There’s really not a good way to know when a cached path becomes invalid or gets worse or better due to players adding and removing objects. And once the cache is filled with ten thousands, or a hundred thousand paths, storage and memory becomes a big issue. So forget caching these.
After a lot of thought, and talking the problem over with some other programmers, I came up with a solution to try out, and it ended up working very well.
The reason path finding is slow is simply the size of the data set – A large map has about 250,000 nodes and searching it takes time. The data is fine grained and looks like this.
What I wanted to do is reduce this data set into something simpler that still mimics the basic paths people will take.
So the map is now broken into larger chunks defined by where characters are allowed to walk. The problem arises where a chunk has disconnected parts. For example one chunk could have river running through it that cuts it in half – that chunk has to be broken into multiple pieces so that there is a distinction between one side of the river and the other.
After breaking into blocks and resolving any disconnected regions, I arrive at the simplified pathing data set. The colors just represent distinct areas.
One nice feature of this data set is that it can be updated quickly. If a new building is added, or a building is removed, only the chunks that intersect the new building need to be recomputed – and this process is really fast.
The next step is to connect each distinct region to the others and maintain connectivity information. Again updating this is fast as when a region changes, only it and its neighbors need to recompute connections. Once the connections are built I end up with a graph that represents a general but simplified map of how and where people in the game can move.
Using A* on this graph computes paths and distances at anywhere from 60 to 100 times faster than the more complex and fine grained path finder. This is used in place of all previous checks for linear distance. While the distances are coarse estimates, what results is that people find a decent idea of walking distance rather than looking at pure linear distance. The people then decide to move into the more logical homes.
While the distance estimates are fast to compute, they aren’t instantaneous. But because the data set is simple and fairly small, caching the distance between to areas actually works for them. The paths are cached for only a short time. The hit rate on the cache is very high, and makes the slight delay of the cache worth the time savings.
Once in a while people do check to see if there is a better way to get to a workplace, and they’ll move. Here I’ve built an additional bridge, and as the new estimate distance is shorter, the workers move to the closest home after a few months.
Somewhat related, because this path finding method is so much faster, I’ve got a note to make regular path finding use it. The basic idea would be do perform the coarse path find, and then when computing a path on the fine grid, only areas touched by the coarse grid would be available for A* to consider – instead of considering the entire map. From a code design and performance standpoint, this is a fairly elegant solution and I can make it generic and reusable for any set of nodes – not just a grid.
But that’s a task for another day, another game.
This was a great two days of problem solving and it fixed a major AI issue, but this wasn’t a change I really want to make this late in development. I’m starting to get a little nervous about making big changes to existing systems – and really examining what changes I make and how they are made. I considered reverting my changes several times, because I really don’t want to add new bugs at this point.
But it makes the AI so much smarter looking and optimizes worker time so well that in the end it’s worth it and makes sprawling towns work better.
The game is definitely shaping up. I currently don’t have any crash bugs to track down, and most of the current issues I have open are simple AI and user interface issues. Getting there!