Behind Lucky Shot Part 2 of 2 - Sharp Wit?

November 6, 2011

Last time, I showed you the simple but effective physics system that powered Lucky Shot. By the end of the article, we had a smooth, adjustable, and flexible movement system that we could apply to each of the AI enemies.  I also mentioned that the physics system took care of most of the natural-feeling movement of the game’s enemies, and that the underlying AI was comparatively simple. This is what I’ll be showing you in this article — how the physics and AI worked in tandem.

By the end of the last article, we had a physics system in which we could change the following aspects:

  • The base acceleration, which, when increased, caused the enemy to move and accelerate faster,

  • The angle at which the enemy is accelerating, and

  • The coefficient of friction which determines how long it takes for the enemy to come to a stop, and also factors in to the enemy’s top speed.

In order to keep all the enemies moving with a similar “feel”, I kept the coefficient of friction identical for every enemy, as well as the player. If I wanted to, for example, have some enemies look like they’re gliding on ice, while others start and stop instantly, I could change this.

The base acceleration differs from enemy to enemy, but is kept constant for all enemies of that type, at all times. This also means the enemies never come to a stop.

Thus, the only thing that is changed dynamically by the AI is the angle at which the enemies accelerate. This is the parameter that all the AI logic goes into — it only changes where the enemy is supposed to go. This is decided using a couple of very simple rules, but the way this is decided is different from enemy to enemy, and results in very different behaviour.

Let’s take a look at the different types of enemies we have. Lucky Shot only has three types of enemies. They’re never given official names in-game, but I like to call them drifters, dodgers, and shooters.

  • Drifters are the simplest and easiest enemy. All they do is chase you. That’s it.

  • Dodgers chase you as well, but the moment you start shooting at them they scurry away from your bullets.

  • Shooters are different in that they can shoot back. To take advantage of this, they keep their distance from you, circling you at a safe distance — not too far, not too close.

Let’s look at each of these in turn.


Drifter

Click above to play! Use the WASD or arrow keys to move, and click to fire.
Both you and the enemies are invincible.

The drifter is the simplest of the enemies. Their acceleration is always kept in the direction of the player. That’s the extent of their behaviour.

Well, almost. There’s a slight problem with that: if all of the enemies simply kept their acceleration pointed exactly towards the player at all times, as the player dodged and weaved around them, their paths would end up converging, and you’d get all the drifters overlapping in one big clump. To prevent this problem, I add a bit of noise to their motion — instead of accelerating directly at the player, they accelerate in the player’s direction, plus or minus up to 90°. That may seem like a lot, but remember that we’re dealing with acceleration rather than velocity. On average, they’re accelerating towards the player, and that means that their velocity is always going to be roughly towards the player as well. The overall effect of this is that the drifter takes a slightly wobbly path towards the player, and when you have several of them on screen, each of them heads in a slightly different path, keeping them from overlapping too much. This strategy is employed to varying degrees in all of the later enemies.


Dodger

Click above to play! Use the WASD or arrow keys to move, and click to fire.
Both you and the enemies are invincible.

If you’re not firing at it, the dodger acts identically to the drifter, with a few minor differences. It has a slightly higher base acceleration than the drifter, and a slightly smaller range of movement noise — it points itself in the player’s direction, plus or minus up to 65°. This means it’s both faster and travels in more of a straight line than the drifter.

The big difference, of course, comes when you fire at it — it flees from bullets, hence the name. This is probably the most complex of the AI behaviours, and it’s a bit rough around the edges — you may notice that they get confused if there’s a lot of bullets around them on all sides, and they sometimes travel into the stream of bullets rather than away from it. But for the purposes of the game, it works well enough.

First off, they’re not actually dodging the entire stream of bullets. They’re dodging only one bullet, the one that’s closest to them. Each frame, they look for the closest bullet to them, check the distance between themselves and the closest bullet, and depending on how close it is, they move themselves away. If the nearest bullet is all the way across the screen from them, they head towards the player as usual. As the bullet gets closer and closer, they turn further and further away from the player, until, if the bullet is right on them, they head 90° to the player, away from the bullet. By comparing the angle between them and the player, and the angle between them and the bullet, they can decide whether to dodge in a clockwise or counter-clockwise direction.

How the dodger decides which way to run from bullets

How far they turn away from the player is determined by the following formula:

Where \(\theta_{o}\) is the angle the dodger will turn away from the player  (ex. 0° is directly towards the player), \(D\) is the distance from the dodger to the closest bullet, and \(S\) is the width of the stage. If you plug in some values, you can see that when the dodger is a stage-width away from the player, it heads straight towards the player, and when the bullet is very close, the dodger will move at close to 90° away from the player.

This works out decently well in practise, even though there are some cases where the behaviour is a little wonky.


Shooter

Click above to play! Use the WASD or arrow keys to move, and click to fire.
Both you and the enemies are invincible.

These guys are the biggest threat in the game. With the ability to shoot at you, they can cause a lot more damage than any of the other enemies. In doing so, they like to keep their distance, making it harder for you to shoot back, especially if you have a short-range gun. They have a slightly higher base acceleration than the dodger, and only have a 60° range of noise added to their acceleration direction (±30° as compared to the dodgers’ ±65° and the drifters’ ±90°).

The key difference in their AI, of course, is that they keep their distance from the player at all times. To do this, I chose to use a simpler system than the dodgers’ AI, similar in nature but less complex. Depending on the distance between them and the player, they change the direction in which they’re accelerating. If they’re far from the player, they accelerate directly towards the player. A bit closer, and they accelerate at 45° to the player’s direction. At a neutral distance, they orbit the player, accelerating at 90° to the player’s direction. Any closer than that, and they move away, first at a 45° angle, and at a very close distance, directly away. Each of these is hard-coded into the shooters’ behaviour.

The shooters’ behaviour is all hard-coded, based on the distance between itself and the player.

This works quite well, and has the advantage of being a lot simpler than the dodgers’ movement system. The simplicity also means that it’s easier to tweak — if I wanted them to orbit the player at a closer distance, or have a wider range of distances at which it would be happy to orbit, it’s as simple as changing a few constants. And because we only change the acceleration rather than velocity, you don’t see an abrupt change in direction when they hit the boundaries.

There’s one thing left in their movement AI, and that’s deciding which direction they’ll be orbiting the player. To do this, I had three options — have them orbit clockwise, have them orbit counterclockwise, or have them choose between the two randomly when they’re spawned. I chose all three options. I had those three options coded in, and chose between them based on what fit the level best. Even in the above flash demo, if you refresh a couple of times, you’ll notice that one of them is always orbiting clockwise, one always counterclockwise, and the last chooses one or the other randomly.

The very last thing handled by the AI is the direction in which they’ll be shooting. This is simple — their guns are pointed directly at the player, but have a spread of ±15°. Because enemies with perfect aim are cheap and boring.


The Final Word

What I wanted to show here is that you don’t have to have a fancy, strategizing, A* pathfinding, neural network creating AI to create fun behaviours for your game. Especially in a time-limited competition like GMD, simple AI is often best. But if you fake it well enough, your enemies can seem appropriately intelligent while still keeping that simplicity of implementation.

As always, I encourage you to email me or post a comment below if you have questions or comments. Happy coding!