Putting It All Together

You've created a lot of stuff in this chapter. Now you are near the end of the chapter, so let's create a demo that uses it all. The demo is a walkthrough with three different types of game objects: static boxes, projectiles, and bots. Each of these objects has its own OBJ file to define its 3D models. The static boxes are just GameObjects and don't do anything special; they are just there to look at. The Bot object is shown in . It looks just like the example from : a turret and a base. The turret moves independently of the base and turns to face the player at all times.

Listing 9.12 Bot.java

import com.brackeen.javagamebook.math3D.*;
import com.brackeen.javagamebook.game.GameObject;
/**
 The Bot game object is a small static bot with a turret
 that turns to face the player.
*/
public class Bot extends GameObject {
 private static final float TURN_SPEED = .0005f;
 private static final long DECISION_TIME = 2000;
 protected MovingTransform3D mainTransform;
 protected MovingTransform3D turretTransform;
 protected long timeUntilDecision;
 protected Vector3D lastPlayerLocation;
 public Bot(PolygonGroup polygonGroup) {
 super(polygonGroup);
 mainTransform = polygonGroup.getTransform();
 PolygonGroup turret = polygonGroup.getGroup("turret");
 if (turret != null) {
 turretTransform = turret.getTransform();
 }
 else {
 System.out.println("No turret defined!");
 }
 lastPlayerLocation = new Vector3D();
 }
 public void notifyVisible(boolean visible) {
 if (!isDestroyed()) {
 if (visible) {
 setState(STATE_ACTIVE);
 }
 else {
 setState(STATE_IDLE);
 }
 }
 }
 public void update(GameObject player, long elapsedTime) {
 if (turretTransform == null || isIdle()) {
 return;
 }
 Vector3D playerLocation = player.getLocation();
 if (playerLocation.equals(lastPlayerLocation)) {
 timeUntilDecision = DECISION_TIME;
 }
 else {
 timeUntilDecision-=elapsedTime;
 if (timeUntilDecision <= 0 ||
 !turretTransform.isTurningY())
 {
 float x = player.getX() - getX();
 float z = player.getZ() - getZ();
 turretTransform.turnYTo(x, z,
 -mainTransform.getAngleY(), TURN_SPEED);
 lastPlayerLocation.setTo(playerLocation);
 timeUntilDecision = DECISION_TIME;
 }
 }
 super.update(player, elapsedTime);
 }
}

In the Bot object, you don't check the player's location every frame; instead, you check only every two seconds (the amount of time is defined in DECISION_TIME). This way, the bot calculates what to do only every four seconds, and we've "dumbed down" the bot a little bit by making it sometimes lag behind the player. Next up is the Blast game object, shown in . The Blast object is a projectile that the player can fire. It simply travels in a straight line (while spinning) and destroys itself after five seconds. The SPEED and ROT_SPEED fields define how fast it moves and spins.

Listing 9.13 Blast.java

import com.brackeen.javagamebook.math3D.*;
import com.brackeen.javagamebook.game.GameObject;
/**
 The Blast GameObject is a projectile, designed to travel
 in a straight line for five seconds, then die. Blasts
 destroy Bots instantly.
*/
public class Blast extends GameObject {
 private static final long DIE_TIME = 5000;
 private static final float SPEED = 1.5f;
 private static final float ROT_SPEED = .008f;
 private long aliveTime;
 /**
 Create a new Blast with the specified PolygonGroup
 and normalized vector direction.
 */
 public Blast(PolygonGroup polygonGroup, Vector3D direction) {
 super(polygonGroup);
 MovingTransform3D transform = polygonGroup.getTransform();
 Vector3D velocity = transform.getVelocity();
 velocity.setTo(direction);
 velocity.multiply(SPEED);
 transform.setVelocity(velocity);
 //transform.setAngleVelocityX(ROT_SPEED);
 transform.setAngleVelocityY(ROT_SPEED);
 transform.setAngleVelocityZ(ROT_SPEED);
 setState(STATE_ACTIVE);
 }
 public void update(GameObject player, long elapsedTime) {
 aliveTime+=elapsedTime;
 if (aliveTime >= DIE_TIME) {
 setState(STATE_DESTROYED);
 }
 else {
 super.update(player, elapsedTime);
 }
 }
}

Now that you've got all the game objects, you can create the GameObjectTest. As usual, this class extends the GameCore3D class and just overrides a couple methods. First, the createPolygon() method loads the OBJ files and sets up the game objects:

public void createPolygons() {
 // create floor
 Texture floorTexture = Texture.createTexture(
 "../images/roof1.png", true);
 ((ShadedTexture)floorTexture).setDefaultShadeLevel(
 ShadedTexture.MAX_LEVEL*3/4);
 Rectangle3D floorTextureBounds = new Rectangle3D(
 new Vector3D(0,0,0),
 new Vector3D(1,0,0),
 new Vector3D(0,0,1),
 floorTexture.getWidth(),
 floorTexture.getHeight());
 float s = GAME_AREA_SIZE;
 floor = new TexturedPolygon3D(new Vector3D[] {
 new Vector3D(-s, 0, s),
 new Vector3D(s, 0, s),
 new Vector3D(s, 0, -s),
 new Vector3D(-s, 0, -s)});
 floor.setTexture(floorTexture, floorTextureBounds);
 // set up the local lights for the model.
 float ambientLightIntensity = .5f;
 List lights = new LinkedList();
 lights.add(new PointLight3D(-100,100,100, .5f, -1));
 lights.add(new PointLight3D(100,100,0, .5f, -1));
 // load the object models
 ObjectLoader loader = new ObjectLoader();
 loader.setLights(lights, ambientLightIntensity);
 try {
 robotModel = loader.loadObject("../images/robot.obj");
 powerUpModel = loader.loadObject("../images/cube.obj");
 blastModel = loader.loadObject("../images/blast.obj");
 }
 catch (IOException ex) {
 ex.printStackTrace();
 }
 // create game objects
 gameObjectManager = new SimpleGameObjectManager();
 gameObjectManager.addPlayer(new GameObject(
 new PolygonGroup("Playerv)));
 gameObjectManager.getPlayer().getLocation().y = 5;
 for (int i=0; i<NUM_BOTS; i++) {
 Bot object = new Bot((PolygonGroup)robotModel.clone());
 placeObject(object);
 }
 for (int i=0; i<NUM_POWER_UPS; i++) {
 GameObject object =
 new GameObject((PolygonGroup)powerUpModel.clone());
 placeObject(object);
 }
}
// randomly place objects in game area public void placeObject(GameObject object) {
 float size = GAME_AREA_SIZE;
 object.getLocation().setTo(
 (float)(Math.random()*size-size/2),
 0,
 (float)(Math.random()*size-size/2));
 gameObjectManager.add(object);
}

Another method of interest in GameObjectTest is the updateWorld() method, which was changed from GameCore3D to use transforms for player movement. It adds the capability to fire a projectile:

public void updateWorld(long elapsedTime) {
 float angleVelocity;
 // cap elapsedTime
 elapsedTime = Math.min(elapsedTime, 100);
 GameObject player = gameObjectManager.getPlayer();
 MovingTransform3D playerTransform = player.getTransform();
 Vector3D velocity = playerTransform.getVelocity();
 playerTransform.stop();
 float x = -playerTransform.getSinAngleY();
 float z = -playerTransform.getCosAngleY();
 if (goForward.isPressed()) {
 velocity.add(x, 0, z);
 }
 if (goBackward.isPressed()) {
 velocity.add(-x, 0, -z);
 }
 if (goLeft.isPressed()) {
 velocity.add(z, 0, -x);
 }
 if (goRight.isPressed()) {
 velocity.add(-z, 0, x);
 }
 if (fire.isPressed()) {
 float cosX = playerTransform.getCosAngleX();
 float sinX = playerTransform.getSinAngleX();
 Blast blast = new Blast(
 (PolygonGroup)blastModel.clone(),
 new Vector3D(cosX*x, sinX, cosX*z));
 // blast starting location needs work. Looks like
 // the blast is coming out of your forehead when
 // you're shooting down.
 blast.getLocation().setTo(
 player.getX(),
 player.getY() + BULLET_HEIGHT,
 player.getZ());
 gameObjectManager.add(blast);
 }
 velocity.multiply(PLAYER_SPEED);
 playerTransform.setVelocity(velocity);
 // look up/down (rotate around x)
 angleVelocity = Math.min(tiltUp.getAmount(), 200);
 angleVelocity += Math.max(-tiltDown.getAmount(), -200);
 playerTransform.setAngleVelocityX(angleVelocity *
 PLAYER_TURN_SPEED / 200);
 // turn (rotate around y)
 angleVelocity = Math.min(turnLeft.getAmount(), 200);
 angleVelocity += Math.max(-turnRight.getAmount(), -200);
 playerTransform.setAngleVelocityY(angleVelocity *
 PLAYER_TURN_SPEED / 200);
 // for now, mark the entire world as visible in this frame.
 gameObjectManager.markAllVisible();
 // update objects
 gameObjectManager.update(elapsedTime);
 // limit look up/down
 float angleX = playerTransform.getAngleX();
 float limit = (float)Math.PI / 2;
 if (angleX < -limit) {
 playerTransform.setAngleX(-limit);
 }
 else if (angleX > limit) {
 playerTransform.setAngleX(limit);
 }
 // set the camera to be 100 units above the player
 Transform3D camera = polygonRenderer.getCamera();
 camera.setTo(playerTransform);
 camera.getLocation().add(0,100,0);
}

Also, I snuck a cool effect into the GameCore3D class to make the instructions at the top of the screen fade out after a few seconds. On previous demos, the text, "Use the mouse/keys to move. Press Esc to exit." always appeared at the top of the screen, but in this demo, the text fades away after a few seconds. The fading is accomplished by using a translucent color:

// fade out the text over 500 ms long fade = INSTRUCTIONS_TIME - drawInstructionsTime;
if (fade < 500) {
 fade = fade * 255 / 500;
 g.setColor(new Color(0xffffff | ((int)fade << 24), true));
}
else {
 g.setColor(Color.WHITE);
}

That's it for GameObjectTest. Check out the screenshot in .

Screenshot This screenshot from GameObjectTest shows polygons drawn in correct z-order.

Java graphics 09fig09.jpg

One thing about this demo is that's it's easy to forget that you actually used z-buffering. If you're curious what it would look like without z-buffering, modify ZBuffer's checkDepth() method to always return true. This draws polygons out of order and gives some strange-looking results. In the GameObjectTest demo, when you look straight down so that the green floor covers the entire screen, you might notice that the frame rate is much slower than in demos from previous chapters. This is because the z-buffer is being checked and set for every pixel on the screen, which is a bit of an overhead. As mentioned before, z-buffering is fine for game objects because they appear in only smaller portions of the screen at a time. In the next chapter, on BSP trees, you'll learn about a better algorithm to draw the static polygons of the world much faster and merge 3D objects into the scene.