Combining enemy types and enemy behaviors

Past entries

After running out of inspiration for new games, I decided that I would stop for some time to worry about mobile and originality, and create something that I would enjoy making instead, no matter if it would be successful or not.

I came up with S.U.F.I.: Shut Up And Fight. The game represents everything I love about games: simple graphics, simple gameplay, but lots of explosions and enemy types.

Since I didn't have to worry about making the game succesful or anything, I was also able to explore more aspects. Indeed, I decided to use libraries, something I usually avoid. I used:

  • Pixi.js for the graphics
  • Howler.js for the sound
  • screenfull.js for the fullscreen mode
  • store.js for the local storage
  • marmottajax for the AJAX stuff
  • alertify.js for the prompt dialogs
  • socket.io for the multiplayer mode

I also decided to use a pretty clean architecture for the game, and more specifically for the gameplay part, and this is what I am going to talk about today.

Goals

In this tutorial, I will focus on how I managed to be able to add many types of enemies, while giving them even more different patterns.

By pattern, I mean the behavior the enemies are going to adopt.

I would recommend trying the game for a minute to see how enemies behave. If you look closely, they have a coordinated behavior. Sometimes it is obvious, sometimes it is a little subtle.

For instance, you may notice enemies protected by several ships rotating around them. You might also notice enemies that will move around you, trying to trap you in a mine field.

A less obvious but still important example is the small squad behavior. A small squad is simply a group of ships that will move randomly, but as a group. I was inspired by this scene from Ender's game: it seems that the ships are all moving randomly, but somehow they stick together in squads. Somehow, I needed to add coordination instead of simply giving them random patterns.

Here are a few screenshots to illustrate what they look like in the game:

Left: an enemy protected by other ships rotating around it
Center: enemies in a circle formation converging towards the player
Right: a small squad of enemies moving together

I will first describe an intuitive approach, that most people would probably adopt. I will then detail some of the issues, and finally, I will present my solution.

I decided not to give any actual code, because it would be too specific to my game. I chose to focus on the principles instead, so you can apply them on any project.

The "intuitive" approach

The first thing you usually do when creating a game with many types of enemies is to create an abstract class that will be specialized in as many subclasses as enemy types.

Here is a class diagramm that matches this model.

UML representation of the intuitive approach
UML representation of the intuitive approach

As you can see, there is a generic, abstract class for all enemies: AbstractEnemy. Below this class, there are three subclasses that inherit from it and correspond to different enemy types.

With that kind of approach, you have an easy way of creating enemies. Your classes contain properties such as speed, health, damage, weapon, name, view... They also contain functions like hit(), shoot(), collide(), destroy()...

You might also think it makes sense to include the behavior of your enemies in these classes, since you associate one enemy type with one behavior. So you would probably include it in your loop() function, within the subclasses: each enemy type would implement its own behavior.

Issues of the intuitive approach

This approach is very intuitive and is probably what most people use. It works for simple projects with few enemy types and simple behaviors.

Unfortunately, in my case, I found that it was too simple. Indeed, I used it for another game, Polygon Battle. The goal of the game was similar: adding tons of different enemies. I quickly realized that adding more complex behavior was a real pain, which is why I didn't insist. There are only two boss types, and I am not very proud of how I made them.

The main problem is that with the intuitive approach, you are mixing the behavior of the enemy, along with its fundamental properties and functions. What if you want to use the same enemy type for two different behaviors? Are you going to write the same enemy class with a different name, and put the new behavior in it?

Of course, this works. For some time. You have different behaviors for the same enemy type. Now let's turn the problem the other way around: what if you need to use the same behavior for different ship types? Are you going to write all different behaviors in all ship types?

This clearly doesn't work anymore. You have a combinatorial problem. You would clearly need to refactor your code in order to be able to maintain it.

Now, this is not the only issue. What if you want to have enemies that work together? For instance, let's say you want all ships from a group to follow a certain path, while a second group should just be moving together, and a third group should be moving around a certain point. Adding the behavior within the ships class doesn't work anymore, because there would be too many different cases to explore.

The more structured approach

The solution I found for that problem was simple: I needed to separate the behavior from the ship. In a sense, take the brain out of the body.

Let's start with the new class diagramm:

UML representation of the structured approach
UML representation of the structured approach

I have two main classes:

  • AbstractAI: an abstract AI that would need to be reimplemented in subclasses. An AI would have a loop() function, and would be able to direct several ships, which would be useful for squads and coordinated groups.
  • AbstractEnemy: the same abstract enemy class as before, without the behavior.

Each of these classes can have many subclasses. One subclass for each enemy type, which will easily define specific properties, and one subclass for each enemy behaviour.

The main thing you should see is that I removed the loop() function from the enemies, and placed it in a separate class instead.

With this approach, I am able to spawn any enemy type and to give it the behavior I want, independently from its properties. I am also able to spawn several enemies, and give them the same behavior, which will be able to coordinate them.

For instance, if I want to create an enemy that will be protected by other enemies rotating around it, I would write something similar to this:

// Creating the enemy at the center
var centerEnemy = new SlowEnemy(),
	centerAI = new RandomMovingAI(centerEnemy);

game.addEnemy(centerEnemy);
game.addAI(centerAI);

// Creating a bunch of enemies that will protect it
var protectorsArray = [],
	protector;
for(var i = 0 ; i < 5 ; i++){
	protector = new FastEnemy();
	protectorsArray.push(protector);
	game.addEnemy(protector)
}

// Giving the same AI to all the protectors: they will rotate around centerEnemy
var protectorsAI = new RotatingAI(protectorsArray,{
	center: centerEnemy
});

game.addAI(protectorsAI);

Doesn't that look way easier than writing hundreds of lines of code in each class? All you have to do afterwards is to loop though your AIs at each frame, and each AI will make the enemies move.

In the end, the idea is very simple: separate the behavior/movement of an enemy from its fundamental properties and core functions (shoot, explode...).

You may still need to add a loop() function to your enemy class, for animations, or non-behaviour related actions. The only thing to remember is not to include the AI within that function.

If you found this tutorial useful, please let me know (on Twitter for example). I am always curious about ways to improve them.

< Optimizing WarSim
Analyzing action movies' plots since 1950 >