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:
- You might want to show different object animations for each state. For example, the bot could have different animations for walking, running, strafing, aiming, or firing.
- The bot should give some visual feedback when it gets wounded so the player knows it is damaging the bot.
- Now that the bots can attack the player, you also need some visual feedback on the player's state. For instance, you could use a health bar and a damage indicator.
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 Listing 13.18. 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:
- Make some bots take more hits to destroy them than others.
- Make some bots more vulnerable to certain weapons. For example, a bot might be able to shield laser blasts but might be completely defenseless against missiles.
- When hit, make a bot move to a wounded state that causes the bot to stop for a moment. Also, make the bot invulnerable to any more damage for the short period of time it's in the wounded state.
- Make different decisions depending on the health of a bot. A bot with full health might be brave, while a bot with critically low health might just run away from the player.
- After a bot is destroyed, don't make it immediately disappear. Have it lie there for a short amount of time, and remove it a few seconds later. Some games just let dead bots lie there throughout the entire game. Another idea is to make the robot explode into oblivion (making blast marks on the floors) or disappear in a puff of smoke.
- A destroyed bot could drop certain items, such as ammo or energy, that the player can pick up.
- Be sure to find the right balance between the player and bad guys in your game, and make enemies progressively more difficult as the game moves forward.
- As always, be creative!
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.
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.
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. Listing 13.21 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:
- Amount of health, ammo, remaining lives, and so on
- In-game messages (which could include displaying icons when you pick up an item)
- Any currently active power-ups (such as super health or quad damage)
- Time left to play
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.
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 Listing 13.23, 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 Screenshot for a screenshot.
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.