Take Away Those Godlike Powers!

In the previous chapter on path finding, you created a demo in which bots had godlike powers: They always knew the player's location and could find a path to the player every time. This is basically the same problem as the Terminator-like Pac-Man ghosts. You rarely want to give bots godlike powers like this. Instead, you can force certain situations in which the bot doesn't know where the player is. One realistic idea that works wonders in a 3D game is to allow a bot to know where a player is only if the player is somewhere in the bot's sight. That is, the bot has artificial vision.

Seeing

Generally, the goal with artificial vision is to perform a check to find out whether a bot can see the player. Obviously, the bot shouldn't see anything that's behind an obstacle, such as something on the other side of a wall or above a ceiling. What you can do is fire a "vision ray" from the bot to the player to see if that ray hits any obstacles, as in Screenshot.

Screenshot A bot (white circle) tries to locate the player (black circle). The bot uses a "vision ray" to determine whether it can "see" the player. In this case, the bot can't see the player.

Java graphics 13fig01.gif


If the vision ray doesn't hit an obstacle, you can assume the bot "sees" the player and, therefore, knows the player's location. When the location is known, the bot can attack or whatnot. But how do you implement a vision ray? In a tile-based world, this is a simple problem to solve. Just draw a line using a standard line equation, and find all tiles that intersect that line. Then check each tile to determine whether it is "see-through." For a BSP tree, you've already implemented something like a vision ray. In , "Collision Detection" your collision-detection code had a method called getFirstWallIntersection() that found the first wall intersection between two points. You can use this function to check whether a line segment intersects any walls in the BSP tree. If no wall intersection is found, this method returns null and you can assume the player is visible. Let's try it. In this chapter, most of the AI-related code goes in the AIBot class, which is a subclass of the PathBot you created in the previous chapter. The first addition to AIBot is the canSee() method in Listing 13.1.

Listing 13.1 AIBot.canSee()
/**
 Checks if this object can see the specified object,
 (assuming this object has eyes in the back of its head).
*/
public boolean canSee(GameObject object) {
 // check if a line from this bot to the object
 // hits any walls
 boolean visible =
 (collisionDetection.getFirstWallIntersection(
 getX(), getZ(), object.getX(), object.getZ(),
 getY(), getY() + 1) == null);
 if (visible) {
 timeSincePlayerLastSeen = 0;
 }
 return visible;
}


Notice that you set the field timeSincePlayerLastSeen to 0. This variable is incremented in the update() method, so you always know the time since the player was last seen. This way, you can perform certain functions if the player is not visible but was visible recently, such as using the A* search if the player was visible just a few seconds ago. One problem with using getFirstWallIntersection() is basically a problem with using a 2D BSP tree: The ray is horizontal, which means the vision ray can't go upstairs. Ergo, bots can't see upstairs. Hopefully, with the combination of other AI capabilities, this won't be an issue. Another issue is that the bot has eyes in the back of its head. The vision ray is cast out in any direction, no matter which way the bot is facing, so the player can't sneak up behind the bot. To fix this, you could implement a "blind spot" behind the AI creature, like in Screenshot.

Screenshot You can implement a vision "blind spot" so the player can still sneak up on the bot.

Java graphics 13fig02.gif


This is fairly easy to do. Just check to make sure the angle between the vision ray and the direction the bot is facing isn't too large. (This is one of those "exercise for the reader" tricks. Enjoy!) But what if you snuck up behind a bot and just started screaming? Well, the bot couldn't hear, so it wouldn't matter. But what if the bot could hear you, such as when the player jumps, falls, or fires a weapon?

Hearing

In the real world, when you make a sound, the sound waves bounce off walls, run into each other, and travel down the hallway and around corners, and a sound gets quieter with distance until you're so far away from the sound source that you can't hear it at all. So in a game, you can give bots a "hearing radar" that can hear any noises within a certain distance, whether or not there are walls in the way, as in Screenshot.

Screenshot A bot (gray circle) tries to locate the player (black circle). The can "hear" any noises within a certain distance.

Java graphics 13fig03.gif


So, if the player is making a noise, the code to check whether it's within range merely involves checking the distance from the player, as in Listing 13.2.

Listing 13.2 AIBot.canHear()
float hearDistance;
...
/**
 Checks if this object can hear the specified object. The
 specified object must be making a noise and be within
 hearing range of this object.
*/
public boolean canHear(GameObject object) {
 // check if object is making noise and this bot is not deaf
 if (!object.isMakingNoise() || hearDistance == 0) {
 return false;
 }
 // check if this bot is close enough to hear the noise
 float distSq = getLocation().
 getDistanceSq(object.getLocation());
 float hearDistSq = hearDistance * hearDistance;
 return (distSq <= hearDistSq);
}


Here, you're actually using the square of the distances, so you don't have to perform the Math.sqrt() operation. That was easy enough, but how can you tell whether an object makes a noise? When does the isMakingNoise() method return true? Usually, a noise isn't just an "instant" event, but it actually lasts for a few seconds or more. So, you can keep track of the remaining time of the last noise made by each object. It's all summed up in a couple extra methods in the base GameObject class, in Listing 13.3.

Listing 13.3 Noise Methods of GameObject
private long noiseDuration;
...
 /**
 Returns true if this object is making a "noise."
 */
 public boolean isMakingNoise() {
 return (noiseDuration > 0);
 }
 /**
 Signifies that this object is making a "noise" of the
 specified duration. This is useful to determine if
 one object can "hear" another.
 */
 public void makeNoise(long duration) {
 if (noiseDuration < duration) {
 noiseDuration = duration;
 }
 }
public void update(GameObject player, long elapsedTime) {
 if (isMakingNoise()) {
 noiseDuration-=elapsedTime;
 }
 ...
}


In the player code, whenever the player fires a shot, you'll call makeNoise(500) so the shot is audible to the bots for 500ms. Note that this is completely separate from the SoundManager code—a virtual noise is just imaginary and makes no audible sound through the speakers. In the code, we assume all noises have the same volume. In real life, however, loud noises travel farther than faint noises. You could extend this code to give each noise a volume or distance level that would specify either the volume of the sound or the maximum distance the sound can carry. Again, this is another exercise for the reader! Before I forget, to help out with some common AI path routines, I added a couple of methods to the Vector3D class to get the distance between two locations, as in Listing 13.4.

Listing 13.4 Distance Methods of Vector3D
/**
 Gets the distance squared between this vector and the
 specified vector.
*/
public float getDistanceSq(Vector3D v) {
 float dx = v.x - x;
 float dy = v.y - y;
 float dz = v.z - z;
 return dx*dx + dy*dy + dz*dz;
}
/**
 Gets the distance between this vector and the
 specified vector.
*/
public float getDistance(Vector3D v) {
 return (float)Math.sqrt(getDistanceSq(v));
}


Okay, now the bots can see and hear the player, but they still don't act any differently. Next, you'll work on getting the bots to react to these different situations.



   
Comments