Letting Bots Learn to Move Like Players

8 min read Original article ↗

A few years ago, during the Christmas break, I started working on a simple idea that would become a two-year project, Earth’s Greatest Defender.

Frustrated by an FPS prototype that was going nowhere, I wanted to go back to something more focused: a 2D platformer shooter where your only goal is to shoot hundreds of aliens in the coolest possible fashion while zooming through levels using your grappling hook.

And that’s what the game ended up becoming:

I released the game on Steam in November 2023, and shortly after on Poki, where it got over 4 million total gameplays (as of writing this article).

To this day, it’s still the project I’m most proud of.

While on vacation (where inspiration always hits), I had a terrible idea: what if I added multiplayer to the game?

Every game developer knows that adding multiplayer doubles, maybe triples the scope of a game.

Not just because of game design, but also because of the technical complexity of synchronizing two clients. Turning a game into real-time multiplayer isn’t just about replicating state. Different clients have authority over different entities. They often disagree with each other. It has to be fast.

Multiplayer is a huge can of worms.

My goal wasn’t to make the most incredible multiplayer game ever. The focus would still be the single-player campaign, but adding multiplayer sounded like a fun challenge for my hobby project, especially while building my own game engine.

I never intended to make money from the game. I only wanted to have fun and learn things along the way.

It would be similar to Quake, a game series I’ve spent far too many hours playing. Just pure deathmatch chaos.

So I built it, and after months of work, here’s what it looked like:

Spoiler alert: only 2 players in the video are controlled by humans.

Note: I will intentionally not use the term “AI” in this article, simply because nowadays it’s been taken over by all the generative AI, but clearly, this is about game AI.

One of the problems with building multiplayer games is that it requires multiple people to play (duh).

What I didn’t want was for players to click the multiplayer button, run around in an empty level, realize that no one else is playing, and then just leave.

That would suck for the player, and that would suck for me. After all, I build games because I enjoy seeing people have fun playing them.

Although I still think it’s a really fun game, I couldn’t realistically expect enough players to be in multiplayer at the same time to populate the lobbies.

So I gave myself another challenge: add CPU-controlled bots to populate the lobbies and allow anyone to enjoy the multiplayer at any time.

Some obvious rules that I had to set myself:

  1. Bots cannot cheat: they cannot teleport themselves, they cannot jump higher, they cannot avoid damage…

  2. Bots share the same controls as players: if a player jumps by holding the jump button, a bot should have to hold the same virtual button.

  3. Bots have access to the same tools as players: the player has a grappling hook. It’s a core mechanic of the game. Bots should be able to use it as if they were a real human.

  4. Little to no manual tweaking: adding multiplayer was already a huge task, I didn’t want to add too much to my plate. I didn’t want to have to create navigation meshes myself, I didn’t want to manually give instructions to the bots for each map.

  5. Movement doesn’t have to be perfect: if a bot takes a slightly sub-optimal path, that’s fine. They’re meant to mimic real players, and real players don’t always take the shortest path, they miss jumps…

Making bots shoot the player is fairly easy. Point gun, pull trigger.

Where it gets tricky is when it comes to movement.

Bots have to know what they can access and how. Can they walk to this spot? Can they reach this platform? Can they jump down this pit?

If the movement simply consists in walking around, it’s relatively easy. Is the target to your right? Walk right. Is the target to your left? Walk left.

Once the game includes physics, like jumping, it gets a lot trickier.

One approach is to manually calculate some paths that can be achieved. I’ve tried it before for SuperBrawl:

It’s doable, but it’s fairly manual and limited by what I decided was valid movement for the bots. More on that later.

It also doesn’t necessarily represent the true abilities of the player.

Here’s a simple example on EGD: a short jump with a lava pit:

Can the player make this jump? Probably.

You could calculate that the player can jump to a point above the lava pit, and from there land to the platform on the right.

Now what if we make it slightly longer, and add a ceiling:

Can they make this jump? Harder to tell

Suddenly it’s trickier. What’s the player’s horizontal speed? What happens when the player hits their head on that ceiling? Does their vertical velocity get canceled?

You could take all that into account, but suddenly the navigation code becomes extremely complicated and brittle.

What if you add a new mechanic? A grappling hook? A slippery floor? Slightly different physics?

And this is just for a very simple level layout. A real level will feature a lot of complex cases.

For all these reasons, I chose a different, simpler approach.

First, I determine a few starting points for the AI. I can just detect the floor. Easy.

Each white dot is a starting point

Then, for each starting point, I schedule a series of simple inputs:

  • Run left/right

  • Jump left/right

  • Grapple up left/right

Each of these inputs is really simple. They can be achieved by holding down a couple of keys.

For each simulation, I can then create a player entity, and add a tiny bit of behavior to automatically connect nodes together:

And this is what it looks like when it runs:

The level is extremely simple, and the grappling hook isn’t useful, but something interesting is happening: the bot is finding shortcuts in the navigation graph.

One of the actions I defined was jumping left or right. This allowed the bot to realize that walking on the platform isn’t the only way to reach a certain point.

And that’s actually more realistic. Players tend not to just walk in the game, they also jump around.

Let’s try it with a slightly more complicated level:

Not only does the grappling hook become useful, but the bot is also able to find paths that would be somewhat difficult to manually compute, like jumping down to the lower platform.

Now that we’ve created a navigation mesh, we can check that it’s actually usable by a bot.

I created a very simple obstacle course and a small utility that lets me define a spawn point and a destination.

Bots can only use the navigation mesh to determine a path to their destination:

The bot is able to naturally navigate the course. It can jump from platform to platform to climb, as well as climb back down. It even uses the grappling hook.

Once I have all this working, when a multiplayer game starts, I can just run the simulation, and once it’s ready, the bots can freely run around.

The cool part is that the simulation doesn’t actually have to run at the same rate as the player’s frame rate.

Everything I’ve shown so far has been for the sake of debugging, but in reality the simulation does not need to be rendered, and it can run much faster.

This is what it looks like in practice, on an actual multiplayer map:

Within a few seconds, a fairly complex map is fully crawled and ready to be populated by CPU-controlled enemies.

It only needs to run once per map. All bots can share the same graph, and it only needs to run on the server (i.e. no need to burn the player’s CPU).

You can try it yourself. Just open the game, join a multiplayer lobby, and within a few seconds a couple of bots will join and start chasing you.

While I think this approach worked fairly well with EGD, it’s not a silver bullet that can magically work for every game.

As levels grow larger and more complex, computing the mesh takes longer. If that ever became an issue, storing the mesh in a file is always an option.

It also falls apart once levels become more dynamic. What if I add a moving platform? A door? A temporary hazard?

It can still work, but needs refinement.

Overall I’d probably use it again for any game with similar mechanics that needs to mimic real players.

Real players are chaotic.

My robots shall embrace the chaos.

Discussion about this post

Ready for more?