• Register

Software Inc. combines tycoon gameplay with multi-story building mechanics, employee management and a simulated market. It is inspired by games like Game Dev Tycoon, Prison Architect, The Sims and Theme Hospital.

Report RSS Walking around realistically

In this article, I will go through some changes I made to the pathfinding algorithm in Software Inc., for anyone curious.

Posted by on

In this article, I will go through some improvements and optimizations I made for the pathfinding algorithm I use in Software Inc. You should note that most of these things has obvious superior solutions, so this article should not serve as a “how to”, but more as inspiration. I don’t know exactly who this article is aimed at, maybe aspiring programmers, maybe newbies looking for some tips, maybe just
someone with an interest in what kind of problems you deal with when you make games.

Last night I spontaneously decided to improve my pathfinding algorithm, even though I have way more important things on my todo list for Software Inc. In any case, it won’t hurt to fix up some periphery stuff.

I’ll begin with how the previous algorithm works. I don’t know how the previous algorithm works, because I downloaded off the internet… It’s a grid based jump-point search algorithm, which is an improvement to the A* algorithm. If I remember my terminology correct, it is a greedy algorithm, which means it makes a greedy/naïve choice, to avoid considering all possible solutions to a problem. It does this by going directly for the goal when trying to find a path, where A* would consider all possibilities, and then pick the seemingly best one.

I replaced it with the un-optimized A* algorithm, which means it’s slower, but I understand it better and I will have a better chance to optimize it later. This sounds stupid, and it is, but the reason was that the algorithm had a problem, and for the life of me, I didn’t know how to get the algorithm to do the
correct thing. The problem is illustrated below. The ability to move diagonally.


I want people to move diagonally, but I don’t want them to move inside walls or slither in between small cracks. For some reason I couldn’t get it to work the way I wanted with the fast algorithm I had downloaded. My solution was to rewrite the algorithm using the standard A* template.

For every point in the grid, to determine where we can move from that point, I choose every point above, below, and to the right and left, where nothing is blocking, and also diagonally, but only if there is nothing next to or above/below the diagonal point with respect to the current point. So, if we wanted to move to the north east, both the north and east point should be free. The result is illustrated below.


Okay, so far so good. Now the problem is how jagged the route is; only robots move like that. To solve this I use spline interpolation, I think it’s called. I participated in a course once, in which we were taught how to implement Bezier curves, which is what you’d get if you draw a line over four points, and then draw a curve that fits inside that line “cage”. I did what you should do when you’re not immediately sure how to do something and you don’t want to spend your day researching it, pick from the freely available solutions. Don’t reinvent the wheel, if not for educational purposes. I ended up going with something called cardinal splines, which uses a “tension” variable to decide how curvy a path should be. It basically split the path up in to more points, by placing new points in between the current points in such a way, that it looks smooth. It was still not good though, as illustrated below. It looks worse than before.


The solution here was simple enough. Every time I make a turn, I take the points at either side of the corner, find a point right between them, and move the corner point closer to that point. This makes all turns a little rounder and tighter, the spline interpolation now works much better.


And I’m done! Was what I thought, but my game now took on average 3 seconds to find a path and that is, of course, completely unacceptable. Now, it is very important to note, for the aspiring programmer, the best optimizations I could’ve done here would have been high level, like using jump point search or something similar. But, I actually got the average time down to about 3 milliseconds by using sub-par low level solutions, like switching out data structures and making some assumptions about the system I am looking for a path in, e.g. it’s a grid…

I started out putting in small performance probes, to find out where the code was slow. I note the time, do something, and output the how much time has passed.

The A* algorithm finds the best candidate point when looking for a path, by choosing the point which has the lowest guesstimate of how far it is from the goal. The guesstimate is based on how far we have traveled so far and approximately, how far away the goal is… Far. Finding the best candidate, i.e. the point with the lowest distance, took more than half of the 3 seconds. I was basically going through ALL points previously visited, looking up their individual distance, and taking the smallest one. To solve this problem you can create a data structure, which sorts values as you put them in. So now, when I put a point in my “To Visit” list, I note their distance and put them in the correct order right away, so when I have to find the best candidate, it will be on top of the pile. Think of taking random cards from a deck and putting them in an ordered deck by placing them between cards that are higher and cards that are lower, instead of looking through all of them each time.

The best way to do this is probably a balanced binary tree, which is out of scope of this article. I chose the not-so-best way to do it, for reasons of laziness (this is where you should find a free solution online). My solution was a linked list; you hold on to one point, and that point holds on to the next. When I add a new point, I go through each point and when I find a point that has a higher distance, i.e. is a worse candidate point, I place my point in that position. So in the worst case scenario, I will have to go through the entire list when I add a point, and when I want to find the best candidate, I just take the first point, since it is guaranteed to have the lowest score.

That got me halfway to a good solution, the rest of it was just doing what should you never do. I repeat myself, write code that is hard to manage and doesn’t work in the general case, e.g. “What if the system is not a grid, but a bunch of points in 3D space?” I just throw that notion out the window and think, “What works best for me in my current situation?” This meant making everything more static, writing out loops, reusing memory with global variables, hardcoding logic, etc. For example, instead of having a separate function, “Which places can I go to from here?” I just go through the points currently around me one by one directly in the algorithm. Instead of saying “Does this point exist somewhere in this list?” I just look for the number 32768*x + y, because I know my data points are 2D points in a grid, and 32768 * x + y is a way of expressing that with one unique number (all coordinates are between 0 and 256).


That is it. The code is extremely ugly, but it got me from 3 seconds to 3 milliseconds, and that is all the player is going to notice.

I will finish off by stressing that, if you put some thought into it, you could make this beautiful and still have it be optimal. There exists way better choices than the ones I have made and rethinking the design of an algorithm can often get you farther than just fixing low-level stuff in your code.

Post comment Comments
SIGILL
SIGILL - - 1,157 comments

While it's great that your solution works for you, I must say that it always make me a bit sad if a programmer says he/she decided to go with the short and easy, but code quality decreasing solution. What if you need to make changes to this code, isn't that going to take way longer than simple writing some nice and decent code? :)

Reply Good karma Bad karma+3 votes
Coredumping Author
Coredumping - - 23 comments

Thanks for your feedback!

I completely agree with you, and I have not comprised with any other system in the game. I even have a separate pathfinding implementation that has an interchangeable definition of "point" and "distance", which I use to find paths between rooms.
But pathfinding in a grid is a pretty discrete problem that I'm am comfortable in prioritizing performance over readability and modularity. It is a static well-defined subsystem that I won't be reusing or changing(at least not the design of it). It's basically a black box.

This is something you'll see a lot with commercial software as well. Most database engines have horrible hacks to keep queries quick. And it doesn't really matter, because the system has and will only ever have one main task, and that task has to work optimally, not be modular.

Reply Good karma+2 votes
SIGILL
SIGILL - - 1,157 comments

Yes I do know a lot of software have 'horrible hacks', either to finish something in time or to increase performance. It's just that I personally prefer to keep my code as 'clean' as possible in order to make the software easy to extend/maintain, either by myself or by someone else! I'm a bit of an idealist I guess ;)

Oh and thanks for writing your pathfinding article, I forgot to say that in my previous comment. It's always fun/interesting to read such articles!

Reply Good karma Bad karma+2 votes
Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: