I made a hero ship with beam weapons. I even built drifting asteroids that handle smashing into things. What gave me trouble was finding a way to teach enemy ships how to avoid smashing into things. You know, not perfectly, but just well enough to seem vaguely cunning and worth pretending to outsmart in a video game.
In developing Parsec Patrol, I’ve never had plans or specifications so much as doodles & daydreams. One day I imagined this scene where I’m piloting the hero ship through a shifting maze of asteroids, a dozen enemy scout ships in fast pursuit. I dodge & weave through the rocks, just barely negotiating the gaps. One by one, the baddies vanish in bursts of light & debris: Some succumb to my beam weapons, while others smash into rocks or bungle into each other. Classic space dogfight material, here.
Where to start?
From years of playing & tinkering with shooters like Unreal & Quake, I had a vague notion of bots needing “pathfinding” algorithms to find their way around levels. So, I started hitting Google, looking to see what I could find and understand just well enough to eliminate from consideration or flag for a deeper dive.
Some of the first pathfinding tools I started reading about were waypoint graphs and navigation meshes. It turns out that some bots rely on cheat sheets: map-specific and manually placed points or polygons, traversed to get from point A to point B. Generally it’s a cheap way to make bots seem like they know where they’re going, because a human being does most of the work up front.
Unfortunately, an asteroid field is not like a Quake map. In Parsec Patrol, the clusters of rocks are randomly generated and only get more chaotic as the rocks drift and collide. There’s just no way to handcraft a map for robots in this scenario.
This algorithm seems best suited for navigating graphs with few connections between nodes – a 2D grid, for example, with 8 exits per cell. If I ever wrote a roguelike dungeon crawler, this would be my jam.
Unfortunately, the world of Parsec Patrol uses a continuous coordinate system, rather than a map of discrete grid cells. And, it seems like a waste to try to find a complete path from point A to point B for this game – because that path is likely to be made obsolete from moment to moment. So, how about trying to work out the best move for just the next moment?
I took a short break from research and decided to see what I could come up with on my own. What does it mean to avoid an obstacle?
One way to avoid a collision is to make a full right angle turn, into a perpendicular course. That works on the Light Cycle Grid, but it’s a bit extreme. So, I need an angle between 0 and 90 degrees with respect to the ship’s vector and the obstacle.
So, I decided to give myself a refresher course in trigonometry:
These scribblings are not meant to impress. Rather, this shows how much I forgot about high school trigonometry. Nonetheless, I worked out how to calculate an appropriate target angle that would miss the obstacle by a defined distance. Having that angle meant that I could turn less drastically while steering for avoidance.
It was a small victory – and now I remember SOHCAHTOA again.
Spaceships with Whiskers
Now that I had a way to calculate a steering angle that avoided smashing into something, I needed a way to know when I was headed for a collision.
If I were trying to build a physical robot, I might add some whiskers to detect the nearest obstacle and trigger a turn. But, how to do that in the virtual game world? A little bit of research brought me to the notion of using raycasting combined with collision detection, something like this:
The algorithm I came up with constructs a vector on each side of the ship. I project circles along the vectors, with a radius based on the margin by which I hoped to avoid obstacles. In order from nearest to farthest, I run collision detection for each of the circles. The search stops with the first hit, using the nearest obstacle found.
So, when my ship finds an obstacle with the whiskers, calculate the avoidance angle, and set that as the steering target for the current game tick. This ends up much more lightweight than finding a complete optimal path, and it can react from moment to moment to the changing game environment.
Unfortunately, this scheme looks funny in action: Since I revert back to target seeking after resolving obstacle avoidance, the enemy ships wiggle as they oscillate between the two strategies. Tracking straight toward the target often brings the ship into collision course – so once an avoidance course is achieved, target tracking turns right back into the obstacle.
Functions with Potential
What I really wanted was some way for these ships to take many nearby obstacles into account along with seeking toward the target, and come up with a smoothly changing steering angle that seemed not entirely stupid or goofy looking.
After some further research, I started reading AI for Game Developers by David M. Bourg and Glenn Seemann. In Chapter 5, I found code using the Lennard-Jones potential function to drive avoidance of multiple obstacles and target seeking all in one algorithm.
A thumbnail sketch of this function with respect to the game goes something like this: Entities can repel & attract each other. Repulsion & attraction change over distance. For example, swarming entities can attract at long distance and repel when too close.
Here’s what the simplified AB form of this function looks like in my code:
U = (-A/Math.pow(d,n)) + (B/Math.pow(d,m))
There are 4 constants in this function:
A– magnitude of attraction
n– attenuation of attraction over distance
B– magnitude of repulsion
m– attenuation of repulsion over distance
Pick values for these constants, and you’ve got a function that yields a positive (repulsion) or negative (attraction) value for any given distance (
d). So far, I’ve just used trial & error to find values for these constants.
To apply this function, I search for obstacles within a certain radius of the ship. That limits the number of calculations, because far-flung obstacles have no significant influence. For each nearby obstacle, I calculate distance and apply the potential function. I then calculate a unit vector from each obstacle to the ship and multiply by the result of the potential function.
Repeat all the above for targets, regardless of range, and with function constants that yield attraction rather than repulsion.
That leaves me with a collection of vectors, each with a direction and a magnitude representing the weighted urgency of heading in that direction right now. I sum all these vectors, leaving a single vector with an angle useful as a steering goal. I ignore the magnitude, because it was only useful during addition for influencing the angle.
Now, perhaps I’ve come off sounding smart after having written the above. But, I’ve already established that I’m not good at math. It’s quite possible I’ve abused & misused this function entirely. That said, I think it’s produced a satisfying result.
The demo sketch has a debug mode full of confusing circles and lines, but it might help illustrate how the function works on they fly. In a nutshell, this means that closer obstacles have a greater influence on causing the ship to steer away. Meanwhile, there’s a constant influence pulling the ship back toward the target, whenever the mass of obstacles nearby do not dominate steering.
I want to tinker some more, maybe see if I can make the ships swarm with each other while also avoiding obstacles and heading toward the target. I’d also like to find a way to stop guessing and calculate the function constants based on the speed & steering characteristics of a given ship. That is, fast & nimble ships should be able to navigate tighter spaces, while big & clumsy ships should start working to avoid collisions from farther away.
I’ve been thinking I need to look into vector fields and flocking behaviors next. I’m still looking for more options to make this work, too. If you’ve made it this far reading this post, feel free to toss some suggestions & critique my way. I have basically no idea what I’m doing, nor even what terms to use in searching for this stuff.
Still, these are some pretty fun results stumbling along from daydreams to code.