Probably the thing I’m most proud of in Baseborn is the AI. In the original design document, the enemies you faced were simply going to be static targets that the player had to eliminate in a set amount of time, as a “combat training exercise”. When Chris and I teamed up, we decided to greatly increase the scope of the project, and proper enemy behavior was one of the first things I started on.

Behavior

One of my early goals was that the core of the AI system be versatile enough that creating a new type of enemy was as simple as changing a few variables. To facilitate this, I specified a number of behavior parameters that an enemy type could combine, such as “stand ground”, “afraid”, “no melee”, and “no ranged”. By designing the base AI procedure to work differently depending on these parameters, we were able to quickly design new enemy types with almost no class-specific code. Additionally, any bugs we found only needed to be fixed in one place, which was a lifesaver as the system started to grow in complexity.

Thinking

The base AI procedure (called the “think” method) is called each frame. Here’s the structure of the method:

For the sake of my own sanity, I designed each task to only modify a certain set of data. For example, the enemy’s heading is only ever set in the adjustHeading() method, and his position is only ever set in the move() method.

Tasks

When an enemy is killed, there is a chance that he’ll drop a pickup of health, mana or ammo for the player to collect. This method simply checks for collision with the player and applies the effect accordingly.

Some enemies have no default behavior. Whether this is simply because they aren’t supposed to do anything (the debris on the beach near the beginning of the game is actually an enemy type), or because their behavior is designed specifically (such as the Doppelganger enemies that appear towards the end of the game), this allows the think procedure to exit early.

Here we check if the enemy’s health has been reduced to zero, and stop the procedure if it has. From here on, all the tasks are related to moving and attacking, and we can’t have our defeated enemies doing that.

Line-of-sight is calculated as a simple rectangle check. The enemy looks to see if the player is within a rectangle offset from his position to see if the player is within its bounds. This step moves the rectangle to correspond to his location.

This is the most involved task by far, and deserves a more in-depth explanation.

There are a number of cases in which the enemy should reverse his heading. The most obvious is that he should turn to face the player, if the player is in his field of view…but not if a wall is between them. Additionally, he should reverse his movement direction if he reaches the edge of a platform (but not if the player is across a gap and within ranged weapon distance) or bumps into an obstacle (but not if the obstacle he’s bumped into is the player backed into a corner).

The first step is to find out whether the player is in view. This involves a rectangle check and a raycast, as shown here:

In this case, even though the player is obviously within the view rectangle, a wall is occluding the enemy’s view.

Not so in this case. Since the player is in view, the enemy should turn and walk towards him.

Each time an enemy moves, he remembers where he was last. If at any time his position hasn’t changed since last time, he knows he’s hit a wall and that the physics engine is preventing him from moving forward. Unless the player is in view, he should turn around and walk the other way.

Next, the enemy checks to see if his next move would cause him to fall off the platform he’s standing on. Most of the time this would cause him to turn so as not to fall, but if the player is in view he will wait on the edge and try to attack him or knock him off.

If the enemy can see the player, he goes into attack mode. Depending on his behavior parameters, he may try to shoot, get in closer for a melee attack, or run away.

If the enemy’s health is below 33%, he goes into flee mode. During this phase he’ll rush off of platform edges, and stop trying to chase the player. After a certain amount of time he’ll start to regain health, and eventually drop out of flee mode and resume his normal behavior.

Finally, the enemy’s position is updated based on his current speed.

 

This was my first time working with any kind of AI, and I’m very pleased with the way it came out. I look forward to taking what I’ve learned on this project and applying it to future implementations. If you’re interested in seeing the code behind the tasks, you can find it in the Enemy class on Baseborn’s BitBucket repository.