Putting It All Together

Besides making a bot "explode" when it gets hit, you need to do a few other things to create a working demo using the AI bots:

In this chapter, you're using the same simple pyramid-shape bots from the previous chapter, keeping the object animations to a minimum. You'll also create a simple overlay framework to draw a health bar. First, though, let's sum up all the different AI bot attributes you'll use.

Brains!

You'll store all the movement patterns and other attributes of the bots in a simple Brain class, shown in . Every AIBot will have an instance of the Brain class.

Listing 13.18 Brain.java

public class Brain {
 public PathFinder attackPathFinder;
 public PathFinder dodgePathFinder;
 public PathFinder aimPathFinder;
 public PathFinder idlePathFinder;
 public PathFinder chasePathFinder;
 public PathFinder runAwayPathFinder;
 // probability of each battle state
 // (the sum should be 1)
 public float attackProbability;
 public float dodgeProbability;
 public float runAwayProbability;
 public long decisionTime;
 public long aimTime;
 public float hearDistance;
 public void fixProbabilites() {
 // make the sums of the odds == 1.
 float sum = attackProbability + dodgeProbability +
 runAwayProbability;
 if (sum > 0) {
 attackProbability /= sum;
 dodgeProbability /= sum;
 runAwayProbability /= sum;
 }
 }
}

This is a bare class that just includes fields for each brain attribute. The only function here is one to fix the probabilities so their sum is 1. You can add a lot to the brain class. We could extend the class to include other AI attributes, such as speed and maximum amount of health. You could also make the system more intricate, allowing bots with certain brains to be resistant to specific weapons.

Health and Dying

Of course, now that you've got bots that can move around an attack, you need to make it so bots can be destroyed. In previous chapter demos, we had the "one strike and you're out" rule, where a projectile hitting a bot destroyed it, making it immediately disappear. In a game, you'll want to make it a bit more realistic than that. Here are some examples:

In this chapter, you'll create fairly simple health and dying routines. Each bot will start with 100 health points, and the player's weapon will cause a random amount of damage between 40 and 60 health points. Remember, the player fires projectiles that are game objects, so you can use our collision code to check whether a projectile hits anything. First, you'll add a couple more states to the bot, to signify when the bot has been hit and when it is dead:

public static final int WOUNDED_STATE_HURT = 6;
public static final int WOUNDED_STATE_DEAD = 7;

Keep a bot in the hurt state for a few seconds after it gets hit. Likewise, if the bot's AI state is dead, keep it in that state for a few seconds and then remove it from the game object manager. First, add some health functions to the AIBot class, shown here in .

Listing 13.19 Health Methods in AIBot

private static final float DEFAULT_MAX_HEALTH = 100;
private static final float CRITICAL_HEALTH_PERCENT = 5;
private float maxHealth;
private float health;
...
protected void setHealth(float health) {
 this.health = health;
}
/**
 Adds the specified amount to this bot's health. If the
 amount is less than zero, the bot's state is set to
 WOUNDED_STATE_HURT.
*/
public void addHealth(float amount) {
 if (amount < 0) {
 if (health <= 0 || aiState == WOUNDED_STATE_HURT) {
 return;
 }
 setAiState(WOUNDED_STATE_HURT, null);
 }
 setHealth(health + amount);
}
/**
 Returns true if the health is critically low (less than
 CRITICAL_HEALTH_PERCENT).
*/
public boolean isCriticalHealth() {
 return (health / maxHealth < CRITICAL_HEALTH_PERCENT / 100);
}

The addHealth() method is where all the action happens. Note that this method decreases the health and puts the bot in the hurt state only if it's not currently hurt. The isCriticalHealth() method returns true if the bot's health is critically low; if so, you can make it run away. Next you must make some decisions based on the bot's health and wounded state. You need to make sure a bot stays in the hurt state for a short period of time, and you need to destroy a bot if it has been dead for a while. The code to do all this is here in .

Listing 13.20 Wounded and Dying Methods in AIBot

public void update(GameObject player, long elapsedTime) {
 elapsedTimeInState+=elapsedTime;
 ...
 if (aiState == WOUNDED_STATE_DEAD) {
 // destroy bot after five seconds
 if (elapsedTimeInState >= 5000) {
 setState(STATE_DESTROYED);
 }
 return;
 }
 else if (aiState == WOUNDED_STATE_HURT) {
 // after 500ms switch to either the idle or dead state.
 if (elapsedTimeInState >= 500) {
 if (health <= 0) {
 setAiState(WOUNDED_STATE_DEAD, player);
 return;
 }
 else {
 aiState = NORMAL_STATE_IDLE;
 }
 }
 else {
 return;
 }
 }
 // run away if health critical
 if (isCriticalHealth() && brain.runAwayPathFinder != null) {
 setAiState(BATTLE_STATE_RUN_AWAY, player);
 return;
 }
 ...
}

The code shows some decision-making routines in the update() method of AIBot. After a bot is hurt, 500ms later it either moves to the idle state or, if the bot's health is 0, moves to the dead state. When in the dead state, it destroys itself after five seconds. Finally, if the health of the bot is critically low, the bot runs away from the player. Note that here you just make the bot disappear after five seconds, no matter what. It might seem kind of clunky to the user to watch a bot suddenly vanish. Another idea is to make the bot disappear only when it's offscreen so the player doesn't notice it vanish. Alternatively, many games show some sort of explosion effect when a bot disappears. Earlier, we talked about how you need to set the appropriate PathFinder whenever you set the AI state. You also need to change the object's animation at this time. shows the extended setAIState() method in AIBot. In this code, when a bot is hurt, it stops moving and spins around a little bit, as if getting hit made it dizzy.

Listing 13.21 Extended setAIState() Method of AIBot

protected void setAiState(int aiState, GameObject player) {
 if (this.aiState == aiState) {
 return;
 }
 this.aiState = aiState;
 elapsedTimeInState = 0;
 Vector3D playerLocation = null;
 if (player != null) {
 playerLocation = player.getLocation();
 }
 // update path
 switch (aiState) {
 case BATTLE_STATE_ATTACK:
 setPathFinder(brain.attackPathFinder);
 setFacing(playerLocation);
 break;
 case BATTLE_STATE_DODGE:
 setPathFinder(brain.dodgePathFinder);
 setFacing(null);
 break;
 ...
 case WOUNDED_STATE_HURT:
 setPathFinder(null);
 setFacing(null);
 getTransform().stop();
 getTransform().setAngleVelocityY(
 MoreMath.random(0.001f, 0.05f),
 MoreMath.random(100, 500));
 break;
 }
}

Okay! Now you've got everything you need to demo the AI bots. Now you need to show the health of the player because the AI bots can attack, thus depleting the player's health. You'll accomplish this by adding a heads-up display.

Adding a Heads-Up Display

As far as I can tell, the term heads-up display, or HUD, originated from certain car instrument panels. On common cars, the driver has to look down to see the speed, fuel meter, odometer, flux capacitor, and whatever other gauges are on the instrument panel. But with cars that have a heads-up display, the instrument panel is projected onto the windshield or is displayed in a racing driver's helmet so drivers can keep their heads up while driving. The primary purpose of heads-up displays is to provide information on the state of the game that can't be displayed in the game itself. Some types of information that are common in heads-up displays are listed here:

Heads-up displays in games are normally drawn as an overlay on top of the view window. Also, they are often resolution independent, meaning they stay the same size onscreen no matter what the resolution of the screen is. Conversely, a resolution-dependent HUD, such as one designed for a 640x480 screen, would look smaller on a higher-resolution screen such as 1,024x768. This implementation will be resolution independent. Start your implementation by creating a simple Overlay interface in .

Listing 13.22 Overlay.java

public interface Overlay {
 /**
 Updates this overlay with the specified amount of
 elapsed time since the last update.
 */
 public void update(long elapsedTime);
 /**
 Draws an overlay onto a frame. The ViewWindow specifies
 the bounds of the view window (usually, the entire
 screen). The screen bounds can be retrieved by calling
 g.getDeviceConfiguration().getBounds();
 */
 public void draw(Graphics2D g, ViewWindow viewWindow);
 /**
 Returns true if this overlay is enabled (should be drawn).
 */
 public boolean isEnabled();
}

Overlays are updated just like game objects and are drawn after each frame is drawn so they appear on top. As an example, you'll create a simple heads-up display that shows the health of the player, both as a number and as a bar. The HeadsUpDisplay class, in , is a resolution-independent display of the player's health. Also, it's animated: The displayed health slightly lags behind the actual health of the player.

Listing 13.23 HeadsUpDisplay.java

public class HeadsUpDisplay implements Overlay {
 // increase health display by 20 points per second
 private static final float DISPLAY_INC_RATE = 0.04f;
 private Player player;
 private float displayedHealth;
 private Font font;
 public HeadsUpDisplay(Player player) {
 this.player = player;
 displayedHealth = 0;
 }
 public void update(long elapsedTime) {
 // increase or decrease displayedHealth a small amount
 // at a time, instead of just setting it to the player's
 // health.
 float actualHealth = player.getHealth();
 if (actualHealth > displayedHealth) {
 displayedHealth = Math.min(actualHealth,
 displayedHealth + elapsedTime * DISPLAY_INC_RATE);
 }
 else if (actualHealth < displayedHealth) {
 displayedHealth = Math.max(actualHealth,
 displayedHealth - elapsedTime * DISPLAY_INC_RATE);
 }
 }
 public void draw(Graphics2D g, ViewWindow window) {
 // set the font (scaled for this view window)
 int fontHeight = Math.max(9, window.getHeight() / 20);
 int spacing = fontHeight / 5;
 if (font == null || fontHeight != font.getSize()) {
 font = new Font("Dialog", Font.PLAIN, fontHeight);
 }
 g.setFont(font);
 g.translate(window.getLeftOffset(), window.getTopOffset());
 // draw health value (number)
 String str = Integer.toString(Math.round(displayedHealth));
 Rectangle2D strBounds = font.getStringBounds(str,
 g.getFontRenderContext());
 g.setColor(Color.WHITE);
 g.drawString(str, spacing, (int)strBounds.getHeight());
 // draw health bar
 Rectangle bar = new Rectangle(
 (int)strBounds.getWidth() + spacing * 2,
 (int)strBounds.getHeight() / 2,
 window.getWidth() / 4,
 window.getHeight() / 60);
 g.setColor(Color.GRAY);
 g.fill(bar);
 // draw highlighted part of health bar
 bar.width = Math.round(bar.width *
 displayedHealth / player.getMaxHealth());
 g.setColor(Color.WHITE);
 g.fill(bar);
 }
 public boolean isEnabled() {
 return (player != null &&
 (player.isAlive() || displayedHealth > 0));
 }
}

The font size and the health bar size are determined by the size of the view window. Other than that, there's really nothing special about this heads-up display. A text string and a couple of rectangles are drawn, and that's it. The health bar makes a great animated effect, though. This chapter's demo (called AIBotTest) includes one more overlay that acts as a message queue. In this case, the message queue displays the various changes in the AI bots' state in the upper-right corner of the screen. This helps debug the AI bots' behavior and gives developers a clue to what the bots are thinking, including whether a bot really can see or hear the player. Check out for a screenshot.

An example with a heads-up display: a health meter (upper left) and a message queue (upper right). The message queue shows debug messages for the AI bots.
An example with a heads-up display: a health meter (upper left) and a message queue (upper right). The message queue shows debug messages for the AI bots.

Note that you should include a heads-up display only when it's necessary. If you can show the health of the player in the game itself, do so. For example, in the original Mario games, Mario's health was directly related to his size: A big Mario could get hit twice before dying, but a small Mario could get hit only once. The game didn't need the words big or small in the heads-up display because it was already visually apparent. And you don't have to limit the health display to a bar, either. Some games use pie charts or heart icons. Feel free to be creative with how you display the health because a bar can be considered passé. For example, the game Doom shows a graphic of your face-the more damage you take, the bloodier your face becomes. Finally, keep in mind that the heads-up display doesn't have to be onscreen at all times. Parts of it could appear only when a significant change occurs, such as when the player gets more points or acquires a new power-up. In this case, the heads-up display could scroll on and off the screen as needed.