The Simulation Classes

As usual, create a single class for each object in the world. You have the following:

  • A ship
  • Invaders
  • Shots
  • Shield Blocks

The orchestration is performed by an all-knowing World class. As you saw in the last chapter, there's really not such a huge difference between 2D and 3D when it comes to object representation. Instead of GameObject and DynamicObject, you can now use GameObject3D and DynamicObject3D. The only differences are that you use Vector3 instances instead of Vector2 instances to store positions, velocities, and accelerations, and you use bounding spheres instead of bounding rectangles to represent the shapes of the objects. All that's left to do is implement the behavior of the different objects in the world.

The Shield Class

From the game mechanics definition, you know the size and behavior of your shield blocks. They just sit there in the world at some location, waiting to be annihilated by a shot, either from the ship or an invader. There's not a lot of logic in them, so the code is rather concise. Listing 12–6 shows the internals of a shield block.

Listing 12–6. Shield.java, the Shield Block Class

package com.badlogic.androidgames.droidinvaders;

import com.badlogic.androidgames.framework.GameObject3D;

public class Shield extends GameObject3D {
    static float SHIELD_RADIUS = 0.5f;

    public Shield(float x, float y, float z) {
        super(x, y, z, SHIELD_RADIUS);
    }
}

Define the shield's radius and initialize its position and bounding sphere according to the constructor parameters. That's all there is to it!

The Shot Class

The shot class is equally simple. It derives from DynamicGameObject3D, as it is actually moving. Listing 12–7 shows you the code.

Listing 12–7. Shot.java, the Shot Class

package com.badlogic.androidgames.droidinvaders;

import com.badlogic.androidgames.framework.DynamicGameObject3D;

public class Shot extends DynamicGameObject3D {
    static float SHOT_VELOCITY = 10f;
    static float SHOT_RADIUS = 0.1f;

    public Shot(float x, float y, float z, float velocityZ) {
        super(x, y, z, SHOT_RADIUS);
        velocity.z = velocityZ;
    }

    public void update(float deltaTime) {
        position.z += velocity.z * deltaTime;
        bounds.center.set(position);
    }
}

You have to define some constants, namely the shot velocity and its radius. The constructor takes a shot's initial position, as well as its velocity on the z-axis. Wait, didn't you just define the velocity as a constant? Yes, but that would let your shot travel only in the direction of the positive z-axis. That's fine for shots fired by the invaders, but the shots from the ship must travel in the opposite direction. When you create a shot (outside of this class), you know the direction the shot should travel. So the shot has its velocity set by its creator.

The update() method just does the usual point-mass physics. There is no acceleration involved, and thus you only need to add the constant velocity, multiplied by the delta time, to the shot's position. The crucial part is that you also update the position of the bounding sphere's center in accordance with the shot's position. Otherwise, the bounding sphere would not move with the shot.

The Ship Class

The Ship class is responsible for updating the ship's position, keeping it within the bounds of the game field, and keeping track of the state it is in. It can either be alive or exploding. In both cases, keep track of the amount of time the ship has been in this state. The state time can then be used to do animations, for example, just as you did in Super Jumper and its WorldRenderer class. The ship will get its current velocity from the outside, based on the user input, either with accelerometer readings, as you did for Bob, or based on a constant, depending on what on-screen buttons are being pressed. Additionally, the ship will keep track of the number of lives it has, and offer us a way to tell it that it has been killed. Listing 12–8 shows you the code.

Listing 12–8. Ship.java, the Ship Class

package com.badlogic.androidgames.droidinvaders;

import com.badlogic.androidgames.framework.DynamicGameObject3D;

public class Ship extends DynamicGameObject3D {
    static float SHIP_VELOCITY = 20f;
    static int SHIP_ALIVE = 0;
    static int SHIP_EXPLODING = 1;
    static float SHIP_EXPLOSION_TIME = 1.6f;
    static float SHIP_RADIUS = 0.5f;

Start off with a couple of constants to define the maximum ship velocity, two states (alive and exploding), the amount of time it takes the ship to explode fully, and the ship's bounding sphere radius. Also, let the class derive from DynamicGameObject3D, since it has a position and bounding sphere, as well as a velocity. The acceleration vector stored in a DynamicGameObject3D will again be unused.

    int lives;
    int state;
    float stateTime = 0;

Next, you have the members, consisting of two integers, to keep track of the number of lives the ship has, as well as its state (either SHIP_ALIVE or SHIP_EXPLODING). The last member keeps track of how many seconds the ship has been in its current state.

    public Ship(float x, float y, float z) {
        super(x, y, z, SHIP_RADIUS);
        lives = 3;
        state = SHIP_ALIVE;
    }

The constructor performs the usual super class constructor call and initializes some of the members. The ship will have a total of three lives.

    public void update(float deltaTime, float accelY) {
        if (state == SHIP_ALIVE) {
            velocity.set(accelY / 10 * SHIP_VELOCITY, 0, 0);
            position.add(velocity.x * deltaTime, 0, 0);
            if (position.x < World.WORLD_MIN_X)
                position.x = World.WORLD_MIN_X;
            if (position.x > World.WORLD_MAX_X)
                position.x = World.WORLD_MAX_X;
            bounds.center.set(position);
        } else {
            if (stateTime >= SHIP_EXPLOSION_TIME) {
                lives--;
                stateTime = 0;
                state = SHIP_ALIVE;
            }
        }
        stateTime += deltaTime;
    }

The update() method is pretty simple. It takes the delta time, as well as the current accelerometer reading on the y-axis of the device (remember, you are in landscape mode, so the accelerometer y-axis is your screen's x-axis). If the ship is alive, set its velocity based on the accelerometer value (which will be in the range −10 to 10), just as you did for Bob in Super Jumper. Additionally, update its position based on the current velocity. Next, check whether the ship has left the boundaries of the playing field, using two constants that you'll define later on in your World class. When the position is fixed, update the center of the bounding sphere for the ship.

If the ship is exploding, check how long that's been the case. After 1.6 seconds in the exploding state, the ship is finished exploding, loses one life, and goes back to the alive state.

Finally, update the stateTime member based on the given delta time.

    public void kill() {
        state = SHIP_EXPLODING;
        stateTime = 0;
        velocity.x = 0;
    }
}

The last kill() method will be called by the World class if it determines a collision has occurred between the ship and either a shot or an invader. It will set the state to exploding, reset the state time, and make sure that the ship's velocity is zero on all axes (never set the y- and z-component of the velocity vector, since you only move on the x-axis).

The Invader Class

Invaders are simply floating in space according to a predefined pattern. Figure 12–11 shows you this pattern.

images

Figure 12–11. Movement of the invaders. Left, down, right, down, left, down, right, down…

An invader follows an extremely simplistic movement pattern. From its initial position, it first moves to the right for some distance. Next, it moves downward (which means in the direction of the positive z-axis on the playing field), again for a specified distance. Once it is done with that, it will start moving to the right, basically backtracking to the same x-coordinate where it was before it started moving left.

The left and right movement distances are always the same, except in the beginning. Figure 12–11 illustrates the movement of the top-left invader. Its first left movement is shorter than all subsequent movements to the left or right. The horizontal movement distance is half the playing field width, 14 units in this case. For the first horizontal movement, the distance an invader has to travel is half this, or 7 units.

What you have to do is keep track of the direction in which an invader is moving, and how far it has already moved in that direction. If it reaches the movement distance for the given movement state (14 units for horizontal movement, 1 unit for vertical movement), it switches to the next movement state. All invaders will initially have their movement distance set to half the playing field's width. Look again at Figure 12–11 to see why that works! This will make the invaders bounce off the edges of the playing field to the left and right.

Invaders also have a constant velocity. Well, the velocity will actually increase each time you generate a new wave of invaders if all the invaders from the current wave are dead. You can achieve this simply by multiplying this default velocity by some constant that is set from outside, namely the World class responsible for updating all invaders.

Finally, you have to keep track of the state of the invader, which can be alive or exploding. Use the same mechanism as in the case of the ship, with a state and a state time. Listing 12–9 shows you the code.

Listing 12–9. Invader.java, the Invader Class

package com.badlogic.androidgames.droidinvaders;

import com.badlogic.androidgames.framework.DynamicGameObject3D;

public class Invader extends DynamicGameObject3D {
    static final int INVADER_ALIVE = 0;
    static final int INVADER_DEAD = 1;
    static final float INVADER_EXPLOSION_TIME = 1.6f;
    static final float INVADER_RADIUS = 0.75f;
    static final float INVADER_VELOCITY = 1;
    static final int MOVE_LEFT = 0;
    static final int MOVE_DOWN = 1;
    static final int MOVE_RIGHT = 2;

Start with some constants, defining the state of an invader, the duration of its explosion, its radius and default velocity, as well as three constants that allow you to keep track of the direction the invader is currently moving.

    int state = INVADER_ALIVE;
    float stateTime = 0;
    int move = MOVE_LEFT;
    boolean wasLastStateLeft = true;
    float movedDistance = World.WORLD_MAX_X / 2;

Keep track of an invader's state, state time, movement direction, and movement distance, which should initially be set to half the playing field width. Keep track of whether the last horizontal movement was to the left or not. This allows you to decide in which direction the invader should go once it has finished its vertical movement on the z-axis.

    public Invader(float x, float y, float z) {
        super(x, y, z, INVADER_RADIUS);
    }

The constructor performs the usual set up of the invader's position and bounding ship, via the super class constructor.

    public void update(float deltaTime, float speedMultiplier) {
        if (state == INVADER_ALIVE) {
            movedDistance += deltaTime * INVADER_VELOCITY * speedMultiplier;
            if (move == MOVE_LEFT) {
                position.x -= deltaTime * INVADER_VELOCITY * speedMultiplier;
                if (movedDistance > World.WORLD_MAX_X) {
                    move = MOVE_DOWN;
                    movedDistance = 0;
                    wasLastStateLeft = true;
                }
            }
            if (move == MOVE_RIGHT) {
                position.x += deltaTime * INVADER_VELOCITY * speedMultiplier;
                if (movedDistance > World.WORLD_MAX_X) {
                    move = MOVE_DOWN;
                    movedDistance = 0;
                    wasLastStateLeft = false;
                }
            }
            if (move == MOVE_DOWN) {
                position.z += deltaTime * INVADER_VELOCITY * speedMultiplier;
                if (movedDistance > 1) {
                    if (wasLastStateLeft)
                        move = MOVE_RIGHT;
                    else
                        move = MOVE_LEFT;
                    movedDistance = 0;
                }
            }

            bounds.center.set(position);
        }

        stateTime += deltaTime;
    }

The update() method takes the current delta time and speed multiplier to make the new waves of invaders move faster. Only perform the movement if the invader is alive, of course.

Start off by calculating how many units the invader will travel in this update and increase the movedDistance member accordingly. If it moves to the left, update the position directly by subtracting the movement velocity to the x-coordinate of the position multiplied by the delta time and speed multiplier. If it has moved far enough, tell it to start moving vertically by setting the move member to MOVE_DOWN. Also, set the wasLastStateLeft to true, so that you know that, after the down movement is finished, you have to move to the right.

Do exactly the same for handling movement to the right. The only difference is that you subtract the movement velocity from the position's x-coordinate and set the wasLastStateLeft to false once the movement distance has been reached.

If you move downward, manipulate the z-coordinate of the invader's position and again check how far You've been moving in that direction. If you reached the movement distance for downward movement, switch the movement state either to MOVE_LEFT or MOVE_RIGHT, depending on the last horizontal movement direction encoded in the wasLastStateLeft member. Once you are done updating the invaders position, set the position of the bounding sphere, as you did for the ship. Finally, update the current state time and consider the update done.

    public void kill() {
        state = INVADER_DEAD;
        stateTime = 0;
    }
}

The kill() method here serves the same purpose as the kill() method for the Ship class. It allows you to tell the invader that it should start dying. Set its state to INVADER_DEAD and reset its state time. The invader will then stop moving, and only update its state time based on the current delta time.

The World Class

The World class is the mastermind in all of this. It stores the ship, the invaders, and the shots, and it is responsible for updating them and checking on collisions. It's much the same as in Super Jumper, with a few minor differences. The initial placement of the shield blocks, as well as the invaders, is also a responsibility of the World class. Create a WorldListener interface to inform any outside parties of events within the world, such as an explosion or a shot that's been fired. This will allow you to play sound effects, just like in Super Jumper. It helps to go through the code one method at a time. Listing 12–10 shows you the code.

Listing 12–10. World.java, the World Class, Tying Everything Together

package com.badlogic.androidgames.droidinvaders;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import com.badlogic.androidgames.framework.math.OverlapTester;

public class World {
    public interface WorldListener {
        public void explosion();

        public void shot();
    }

You want outside parties to know when an explosion takes place or when a shot is fired. For this, define a listener interface, which you can implement and register with a World instance that will be called when one of these events happen. This is much like Super Jumper, just with different events.

    final static float WORLD_MIN_X = -14;
    final static float WORLD_MAX_X = 14;
    final static float WORLD_MIN_Z = -15;

You should also have a couple of constants that define the extents of the world, as discussed in the “Defining the Game World” section.

    WorldListener listener;
    int waves = 1;
    int score = 0;
    float speedMultiplier = 1;
    final List<Shot> shots = new ArrayList<Shot>();
    final List<Invader> invaders = new ArrayList<Invader>();
    final List<Shield> shields = new ArrayList<Shield>();
    final Ship ship;
    long lastShotTime;
    Random random;

The world keeps track of a couple of things. You have a listener that you invoke when an explosion happens or a shot is fired. Also, keep track of how many waves of invaders the player has already destroyed. The score member keeps track of the current score, and the speedMultiplier allows you to speed up the movement of the invaders (remember the Invaders.update() method). Also, store lists of the shots, invaders, and shield blocks currently alive in the world. Finally, have an instance of a Ship, and store the last time a shot was fired by the ship. Store this time in nanoseconds, as returned by System.nanoTime()—hence the long data type. The Random instance will come in handy when you want to decide whether an invader should fire a shot or not.

    public World() {
        ship = new Ship(0, 0, 0);
        generateInvaders();
        generateShields();
        lastShotTime = System.nanoTime();
        random = new Random();
    }

In the constructor, create the Ship at its initial position, generate the invaders and shields, and initialize the rest of the members.

    private void generateInvaders() {
        for (int row = 0; row < 4; row++) {
            for (int column = 0; column < 8; column++) {
                Invader invader = new Invader(-WORLD_MAX_X / 2 + column * 2f,
                        0, WORLD_MIN_Z + row * 2f);
                invaders.add(invader);
            }
        }
    }

The generateInvaders() method simply creates a grid of invaders, eight by four, arranged as in Figure12–11.

    private void generateShields() {
        for (int shield = 0; shield < 3; shield++) {
            shields.add(new Shield(-10 + shield * 10 - 1, 0, -3));
            shields.add(new Shield(-10 + shield * 10 + 0, 0, -3));
            shields.add(new Shield(-10 + shield * 10 + 1, 0, -3));
            shields.add(new Shield(-10 + shield * 10 - 1, 0, -2));
            shields.add(new Shield(-10 + shield * 10 + 1, 0, -2));
        }
    }

The generateShields() class does pretty much the same: instantiating three shields composed of five shield blocks each, as laid out in Figure 12–2.

    public void setWorldListener(WorldListener worldListener) {
        this.listener = worldListener;
    }

A setter method can set the listener of the World.

    public void update(float deltaTime, float accelX) {
        ship.update(deltaTime, accelX);
        updateInvaders(deltaTime);
        updateShots(deltaTime);

        checkShotCollisions();
        checkInvaderCollisions();

        if (invaders.size() == 0) {
            generateInvaders();
            waves++;
            speedMultiplier += 0.5f;
        }
    }

The update() method is surprisingly simple. It uses the current delta time, as well as the reading on the accelerometer's y-axis, which you can pass to Ship.update(). Once the ship has updated, call updateInvaders() and updateShots(), which are responsible for updating these two types of objects. After all the objects in the world have been updated, start checking for a collision. The checkShotCollision() method will check collisions between any shots and the ship and/or invaders.

Finally, check whether the invaders are dead, and if they are you can generate a new wave of invaders. For love of the garbage collector, you could reuse the old Invader instances, for example via a Pool. However, to keep things simple, you can simply create new instances. The same is true for shots, by the way. Given the small number of objects you create in one game session, the GC is unlikely to fire. If you want to make the GC really happy, just use a Pool to reuse dead invaders and shots. Also, note that you increase the speed multiplier here!

    private void updateInvaders(float deltaTime) {
        int len = invaders.size();
        for (int i = 0; i < len; i++) {
            Invader invader = invaders.get(i);
            invader.update(deltaTime, speedMultiplier);

            if (invader.state == Invader.INVADER_ALIVE) {
                if (random.nextFloat() < 0.001f) {
                    Shot shot = new Shot(invader.position.x,
                                 invader.position.y,
                                                             invader.position.z,
                                 Shot.SHOT_VELOCITY);
                    shots.add(shot);
                    listener.shot();
                }
            }

            if (invader.state == Invader.INVADER_DEAD &&
                            invader.stateTime > Invader.INVADER_EXPLOSION_TIME) {
                invaders.remove(i);
                i--;
                len--;
            }
        }
    }

The updateInvaders() method has a couple of responsibilities. It loops through all invaders and calls their update() method. Once an Invader instance is updated, check whether it is alive. In that case, give it a chance to fire a shot by generating a random number. If that number is below 0.001, a shot is fired. This means that each invader has a 0.1% chance of firing a shot each frame. If that happens, instantiate a new shot, set its velocity so that it moves in the direction of the positive z-axis, and inform the listener of that event. If the Invader is dead and has finished exploding, simply remove it from the current list of invaders.

    private void updateShots(float deltaTime) {
        int len = shots.size();
        for (int i = 0; i < len; i++) {
            Shot shot = shots.get(i);
            shot.update(deltaTime);
            if (shot.position.z < WORLD_MIN_Z ||
                shot.position.z > 0) {
                shots.remove(i);
                i--;
                len--;
            }
        }
    }

The updateShots() method is simple, as well. Loop through all shots, update them, and check whether each one has left the playing field, in which case remove it from the shots list.

    private void checkInvaderCollisions() {
        if (ship.state == Ship.SHIP_EXPLODING)
            return;

        int len = invaders.size();
        for (int i = 0; i < len; i++) {
            Invader invader = invaders.get(i);
            if (OverlapTester.overlapSpheres(ship.bounds, invader.bounds)) {
                ship.lives = 1;
                ship.kill();
                return;
            }
        }
    }

In the checkInvaderCollisions() method, check whether any of the invaders has collided with the ship. That's a pretty simple affair, since all you need to do is loop through all invaders and check for overlap between each invader's bounding sphere and the ship's bounding sphere. According to the game mechanics definition, the game ends if the ship collides with an invader. This is why you set the ship's lives to 1 before you call the Ship.kill() method. After that call, the ship's live member is set to 0, which you'll use in another method to check for the game-over state.

    private void checkShotCollisions() {
        int len = shots.size();
        for (int i = 0; i < len; i++) {
            Shot shot = shots.get(i);
            boolean shotRemoved = false;

            int len2 = shields.size();
            for (int j = 0; j < len2; j++) {
                Shield shield = shields.get(j);
                if (OverlapTester.overlapSpheres(shield.bounds, shot.bounds)) {
                    shields.remove(j);
                    shots.remove(i);
                    i--;
                    len--;
                    shotRemoved = true;
                    break;
                }
            }
            if (shotRemoved)
                continue;

            if (shot.velocity.z < 0) {
                len2 = invaders.size();
                for (int j = 0; j < len2; j++) {
                    Invader invader = invaders.get(j);
                    if (OverlapTester.overlapSpheres(invader.bounds,
                            shot.bounds)
                            && invader.state == Invader.INVADER_ALIVE) {
                        invader.kill();
                        listener.explosion();
                        score += 10;
                        shots.remove(i);
                        i--;
                        len--;
                        break;
                    }
                }
            } else {
                if (OverlapTester.overlapSpheres(shot.bounds, ship.bounds)
                        && ship.state == Ship.SHIP_ALIVE) {
                    ship.kill();
                    listener.explosion();
                    shots.remove(i);
                    i--;
                    len--;
                }
            }
        }
    }

The checkShotCollisions() method is a little bit more complex. It loops through each Shot instance and checks for overlap between it and a shield block, an invader, or the ship. Shield blocks can be hit by shots fired by the ship or by an invader. An invader can only be hit by a shot fired by the ship. And the ship can only be hit by a shot fired by an invader. To distinguish whether a shot was fired by a ship or an invader, all you need to do is look at its z-velocity. If it is positive, it moves toward the ship, and was therefore fired by an invader. If it is negative, it was fired by the ship.

    public boolean isGameOver() {
        return ship.lives == 0;
    }

The isGameOver() method simply tells an outside party if the ship has lost all its lives.

    public void shoot() {
        if (ship.state == Ship.SHIP_EXPLODING)
            return;

        int friendlyShots = 0;
        int len = shots.size();
        for (int i = 0; i < len; i++) {
            if (shots.get(i).velocity.z < 0)
                friendlyShots++;
        }

        if (System.nanoTime() - lastShotTime > 1000000000 || friendlyShots == 0) {
            shots.add(new Shot(ship.position.x, ship.position.y,
                    ship.position.z, -Shot.SHOT_VELOCITY));
            lastShotTime = System.nanoTime();
            listener.shot();
        }
    }
}

Finally, there's the shoot() method. It will be called from outside each time the Fire button is pressed by the user. As noted in the game mechanics section, a shot can be fired by the ship every second, or it can be fired if there's no ship shot on the field yet. The ship can't fire if it explodes, of course, so that's the first thing you check. Next, run through all the Shots and check if one of them is a ship shot. If that's not the case, you can shoot immediately. Otherwise, check when the last shot was fired. If more than a second has passed since the last shot, fire a new one. This time, set the velocity to – Shot.SHOT_VELOCITY so that the shot moves in the direction of the negative z-axis toward the invaders. As always, invoke the listener to inform it of the event.

And that's all the classes that make up the game world! Compare that to what you had in Super Jumper. The principles are nearly the same, and the code looks quite similar. Droid Invaders is, of course, a very simple game, so you can get away with simple solutions, such as using bounding spheres for everything. For many simple 3D games, that's all you need. On to the last two parts of your game: the GameScreen and the WorldRenderer class!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.46.29