The World Class

The World class is the mastermind in all of this. It stores the ship, the invaders, and the shots and is responsible for updating them and checking collisions. It's exactly the same thing as in Super Jumper with some minor differences. The initial placement of the shield blocks as well as the invaders is also a responsibility of the World class. We also 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 us to play sound effects, just like in Super Jumper. Let's go through its 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();

We want outside parties to know when an explosion took place or a shot was fired. For this we define a listener interface, which we can implement and register with a World instance that will be called when one of these events happen. Exactly like in 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;

We also have a couple of constants that define the extents of our 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. We have a listener that we'll invoke when an explosion happens or a shot is fired. We also keep track of how many waves of invaders the player already has destroyed. The score member keeps track of the current score, and the speedMultiplier allows us to speed up the movement of the invaders (remember the Invaders.update() method). We also store lists of shots, invaders, and shield blocks currently alive in our world. Finally we have an instance of a Ship and also store the last time a shot was fired by the ship. We will store this time in nanoseconds as returned by System.nanoTime(), hence the long data type. The Random instance will come in handy when we want to decide whether an invader should fire a shot or not.

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

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

private void generateInvaders() {

Invader invader = new Invader(-WORLD_MAX_X / 2 + column * 2f, 0, WORLD_MIN_Z + row * 2f);

invaders.add(invader);

The generateInvaders() method just creates a grid of eight by four invaders, arranged as in Figure 12-11.

private void generateShields() {

for (int shield = 0; shield < 3; shield++) { shields.add(new Shield(-10 + shield * 10 shields.add(new Shield(-10 + shield * 10 shields.add(new Shield(-10 + shield * 10 shields.add(new Shield(-10 + shield * 10 shields.add(new Shield(-10 + shield * 10

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

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

We also have a setter method to set the listener of the World.

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

checkShotCollisions(); checkInvaderCollisions();

if (invaders.size() == 0) { generateInvaders(); waves++;

speedMultiplier += 0.5f;

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

Finally, we check whether all invaders are dead, in which case we regenerate a new wave of invaders. For the love of the garbage collector we could have reused the old Invader instances, for example via a Pool. However, to keep it simple we just create new instances. The same is true for shots as well, by the way. Given the small number of objects we create in one game session, the GC is unlikely to fire. If you want to make the

-

1,

0,

-3))

+

0,

0,

-3))

+

1,

0,

-3))

-

1,

0,

-2))

+

1,

0,

-2))

GC really happy, just use a Pool to reuse dead invaders and shots. Also note that we 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,

Shot. SHOT_VELOCITY); shots.add(shot); listener.shot();

invader.position.z,

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 we check whether it is alive. In that case we 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% change of firing a shot each frame. If that happens we instantiate a new shot, set its velocity so that it moves in the direction of the positive z-axis, and inform that listener of that event. If the Invader is dead and is done exploding, we simply remove it from our 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. We loop through all shots, update them and check whether each one has left the playing field, in which case we remove it from our 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.overIapSpheres(ship.bounds, invader.bounds)) { ship.lives = 1; ship.kill(); return;

In the checkInvaderCollisions() method we check whether any of the invaders has collided with the ship. That's a pretty simple affair since all we need to do is loop through all invaders and check for overlap between each one's bounding sphere and the ship's bounding sphere. According to our game mechanics definition, the game ends if the ship collides with an invader. That's why we set the ship's lives to 1 before we call the Ship.kill() method. After that call the ship's live member is set to 0, which we'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.overIapSpheres(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);

if (OverlapTester.overIapSpheres(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 either fired by the ship or 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 invader, all we 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 just 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;

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. In the game mechanics section we said that a shot can be fired by the ship every second or 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 we check. Next we run through all the Shots and check if one of them is a ship shot. If that's not the case we can immediately shoot. Otherwise we check when the last shot was fired. If more than a second has passed since the last shot, we fire a new one. This time we 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 we also invoke the listener to inform it of the event.

And that's all the classes making up our game world! Compare that to what we 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 we can get away with simple solutions like using bounding spheres for everything. For many simple 3D games that's all we need, though. On to the last two bits of our game: the GameScreen and the WorldRenderer class!

0 0

Post a comment