On and off for the last 5 years I’ve worked to improve grouped unit movement in an RTS-style game called The Maestros. As Dave Pottinger pointed out almost 20 years ago after his work on Age of Empires, the “pathfinding” part of movement gets all the attention, but making dozens of units follow a path intelligently is at least as important and quite difficult. I’d love to tell you about my... journey in this space.
What follows are by no means state of the art solutions. The industry’s had excellent minds on this problem for over two decades, and you and I have little hope of catching up over an afternoon coffee. So let’s focus on the nitty-gritty details of making basic pathfinding look and feel good for players under practical game constraints. A practical knowledge of 3D math is assumed, but a phD in AI is not recommended. I’d probably just upset you, honestly ;)
In RTS movement, some players want realistic, slow-rotating tanks and squads of infantry hustling together like Company of Heroes. Our game is about executing big plays in quick brawls so our priorities were “responsive over realistic,” and “direct-control over coordinated formations.” Think more Starcraft than Age of Empires.
Where We Started
UDK (Unreal Engine 3’s SDK) supports A* pathfinding in a navmesh-based space, and has pretty effective (if finicky) navmesh generation. Unfortunately, pathfinding was implemented almost entirely in unreachable engine code which we could not modify in UDK. All in all, if I selected a single unit and right clicked a pathable location, I could expect it to get there eventually. Both Unity and UE4 have about the same level of support today though tuned a bit better (though UE4 offers better low-level access). I thought we were pretty well covered with that. Boy, was I wrong.
Problem #1 - Stopping the Group
The next step is moving a group. If I select a few of our Doughboy units and ask them to move to the exact same location, only one of them is going to actually make it there. At best, the others will be adjacent to that one who made it. So how do they know when they’re done moving? Two clicks and we’ve already hit our first issue!
What we came up with was a sort of message-passing system. The first guy who got there was set to have reached the destination, and anybody who touched him and was also trying to get to the same place would consider himself at his destination. Then those guys could pass that message on to anybody who bumped them. We called this “transitive bumping.” This felt pretty clever, and works well for clustered groups, but still has some silly degenerate cases (e.g. if units are in a line).
Problem #2 - Moving Through the Crowd
Another issue we ran into early on was one unit being blocked by another. While UDK’s pathfinding supported creating new obstacles in the navmesh, doing it for a couple hundred units who were constantly changing their location resulted in unplayable performance. Because of this, units were always trying to move through one another instead of around.
Our solve was to allow units to apply a force to one another under certain conditions. This also needed to propagate throughout the group like our stopping messages.
A more natural looking solution might be to tell the unit to move themself out of the way (a la Starcraft 2), and then to move themselves back. In either case, determining the exact states/conditions to “push” another unit was incredibly complex and error-prone. “You can push Allies but not enemies, idle units but not attacking units.” In our case, it took ~10 unique clauses with various levels of nesting to achieve. Yikes! I’d love to find a more generic solve here.
Problem #3 - Staying in Your Lane
After our first public demo of The Maestros at GDC in 2014, I received some feedback from a mentor of mine that the game felt “messy.” Plenty of things contributed to this at the time, but the problem that was most at fault was that even simple, straight-line movements had units jockeying for position along the same path. Nobody would expect a real-life crowd to do that, and certainly not a group of military-trained robots. All of our units were still acting completely independently. When they received a single, common destination from a player’s click and tried to get there on their own fastest route, they’d often choose the same route as the guy next to them. The result was about as graceful as all 8 lanes of the 405 freeway collapsing into one lane instantaneously.
The general solution to this isn’t terribly hard. Calculate a center point for the current group, take the difference of each unit’s position from that center point, and issue a bespoke move command for each unit with their offset from the destination.
For units A, B, & C, and a clicked location (red reticle), offset each destination
That worked great for the basic case of moving a unit cluster from one open area to another, but as you’ll begin to learn in this article - most of the “general” feeling solutions have conditions where they break down. The most obvious is if you try to move next to an obstacle. As you can see below, the center point is fine, but unit C would be inside a boulder (gray box).
Another issue was that if your units were spread out and you clicked near the center, you’d expect them to collapse inwards. Using a naive offset, however, they’d generally stay put. Offsetting the destination also fails to meet expectations if your units are too spread out. For example, you’ve all your units in one cluster, but your commander (unit A) was off solo farming 2 screens away. When you issue a move to a point near the center of the cluster, you’d expect all your units, including your commander, to end up generally underneath your cursor (red reticle). In fact, none of them end up under your cursor if you apply offsets naively.
Summarizing many issues in one sentence, “There are situations where some or all units should collapse together, not maintain their offset from the group’s center.” The idea of determining who is in a group or not can sound a bit daunting, and certainly there are some complex clustering algorithms that could be applied here. My solution to this problem ended up being much simpler and has been unexpectedly effective across a huge number of scenarios. Here’s the rundown:
Borrowing language from our code, I calculate a “SmartCenter” for the group
- Calculate the average position of all units in the group
- Remove any units that aren’t within 1 standard deviation of that average
- Recalculate the average position from that subset of the group
If the point we are trying to reach is within a standard deviation of the center point, I use naive independent movement. This guarantees that units will gather shoulder-to-shoulder in a tight cluster, and gives players the kind of direct control of the group shape we’re looking for in The Maestros.
If I don’t have a meaningful “Primary Cluster,” then my units are probably spread out all over the map. In this situation, I just want them to regroup as best they can. Another win for naive independent pathfinding. I detect this situation when the standard deviation for the group is larger than a particular maximum. Ideally, that maximum is relative to the area occupied by the group so I used the sum of all unit’s radii. That’s been reasonably effective.
If I have a “Primary Cluster,” but 1 or more units are more than 1 standard deviation from the group’s center, I collapse them in by giving them a destination in the direction (i.e. normal) of their offset, but only a standard deviation’s length (i.e. magnitude) away from the group’s central destination. This has the effect of “collapsing back in” and feels much more natural.
Problem #4 - Sticking Together
Overall applying relative offsets to each unit’s destination was a huge win for the “cleanliness” of movement within our game when moving in a straight line. Pathing around obstacles was still abysmal though. First, units will take their own shortest path around an obstacle, and don’t always stick together with their group. Second, our 8-lane to 1-lane traffic jam happens all over again at each intermediate point before we reach our destination (see second image).
Not pathing together
Traffic jam on intermediate points
I sat on this problem for an embarrassingly long time without a good answer. On day one, I thought to pick 1 unit’s path, and apply the offsets to each intermediate point. This breaks down quickly when you consider that often the reason you’re pathfinding in the first place is that your going tightly around an obstacle. Applying the offsets will leave 50% of your units trying to path into a rock, and naive independent pathfinding will cause a permanent gridlock before you even get near your final destination.
My conceptual answer to this setback wasn’t terribly clever either (depicted below). I’d move the path away from the corner, about one radius width. Determining this mathematically on the other hand proved incredibly elusive to me. How do I determine whether my path is cornering close to an obstacle or far away? If I am close, is the obstacle on my left or my right? On what axis is my left or my right for a given point in my path?
At some point I was going to have to do a raycast to locate obstacle volumes. Perhaps I could try raycasting radially around each point (pictured below)? Unfortunately it was prone to missing the obstacle entirely. The accuracy of this solution scaled directly with the number of raycasts I did per point on the path, and that felt terribly inefficient.
What I really needed was the left-right axis for a given turn. The hypothesis is that the angle of the turn is telling you about where you obstacle likely is. Most of my obstacles where going to be directly inside the “elbow” of my vectors, and occasionally outside it. I hit a breakthrough when I found the axis through the following operations:
- Generate the vectors for relative movement between points - For each pathfinding point B, subtract its predecessor, A, to get the vector from A -> B
- For each pair of subsequent vectors, A & B, add them to get a vector C that goes from the beginning of A to the end of B
- Cross C with an up/down vector to get a vector P, that bisects the area between A & B.
The vector P is the right/left axis for my turn! I check for obstacles on either side and shift my pathfinding point away from the obstacle by a little more than 1 standard deviation. The result goes from a path (green) directly on top of my obstacle, to one comfortably offset from it.
Now, I can apply my offsets at the updated points along my path so my group can stick together as they path, and they won’t traffic jam. It doesn’t cover every situation, but in ~90% of cases we can get by without traffic-jamming. The improvement is enormous. Here’s a before & after of going around just one corner.
My biggest learning from doing this is that “generalized” pathfinding algorithms like A* are unlikely to be the whole movement story for your game, especially if you’re trying to coordinate a group’s movement. The second thing I learned is that complexity is truly the enemy here. Pathfinding isn’t hard because the pathfinding algorithms are complex. A tight A* implementation is easily less than a hundred lines of pretty readable code, and is perfectly serviceable for most games. Pathfinding is hard because moving multiple units in real-time and space with one another produces an incredibly large volume of scenarios, and humans have pretty specific expectations of what should happen in many of those scenarios.