You are here

Chapter 27: The Age of Missiles and How to Divide an Asteroid

Adding handling of a missile's "age" is not that complex. We already have the increase of the age-counter taken care of, so all we need to to is to simply call "missile.remove()" if a missile is too old (or has collided with something).

While we edit MissileHandler.update(), we also add the list of all active asteroids as parameter, so we can check for collisions between missiles and asteroids.

Here's the latest MissileHandler:

package com.ajomannen.justroids;

import java.util.ArrayList; // Changed data type to make it easier to remove "old" missiles
import java.util.List;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class MissileHandler {
  private Bitmap bitmap;
  public ArrayList<Missile> missiles;
  private Paint paint;

  public MissileHandler(Resources resources) {
   paint = new Paint();
   bitmap = BitmapFactory.decodeResource(resources, R.drawable.missile_20);
   missiles = new ArrayList<Missile>();
  }

  public void update(float screenWidth, float screenHeight, int pendingEvent,
    float shipX, float shipY, int shipAngle, List<Asteroid> asteroids) { 
   float dX;
   float dY;
   float distance;

   if (pendingEvent == GameEngine.EVENT_FIRE) {
    Missile newMissile = new Missile(bitmap, shipX, shipY, 3, shipAngle);
    missiles.add(newMissile);
   }
   int i = 0;
   for (Missile missile : missiles) {
    missile.age++;
    if (missile.age > 25 * 3 || missile.collided == true) {
     // TODO: Hardcoded max age. Works better with quadratic game
     // area
     missiles.remove(i);
     missiles.trimToSize();
    } else {
     missile.move(screenWidth, screenHeight);
     for (Asteroid asteroid : asteroids) {
      asteroid.collided = false;
      dX = Math.abs(missile.x - asteroid.x);
      dY = Math.abs(missile.y - asteroid.y);
      distance = (float) Math.sqrt(dX * dX + dY * dY);
      if (distance <= (missile.bitmap.getWidth() / 2 + asteroid.bitmap
        .getWidth() / 2)) {
       asteroid.collided = true;
       missile.collided = true;
      }
     }
     i++;
    }
   }
  }

  public void draw(Canvas canvas) {
   for (Missile missile : missiles) {
    missile.draw(canvas, missile.x, missile.y, paint);
   }
  }

}

We must also edit the GameEngine's update method to match this:

public void update() {
  asteroidHandler.update(screenWidth, screenHeight);
  ship.update(screenWidth, screenHeight, asteroidHandler.asteroids,
    pendingEvent);
  missileHandler.update(screenWidth, screenHeight, pendingEvent, ship.x,
    ship.y, ship.angle, asteroidHandler.asteroids);
  pendingEvent = EVENT_NONE;
}

Now we have a state where asteroids "know" when they are hit by a missile, and all that's left is to make them either split in two or completely disappear. We solve this in our AsteroidHandler:

package com.ajomannen.justroids;

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

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class AsteroidHandler {
  private Bitmap[] asteroidBitmaps;
  private Bitmap[] asteroidBitmapsHighlighted;
  public ArrayList<Asteroid> asteroids;
  private Random random;
  private Paint paint;

  public AsteroidHandler(Resources resources, int level) {
   random = new Random();

   paint = new Paint();

   asteroidBitmaps = new Bitmap[3];
   asteroidBitmaps[Asteroid.WHOLE] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_whole);
   asteroidBitmaps[Asteroid.HALF] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_half);
   asteroidBitmaps[Asteroid.QUARTER] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_quarter);

   asteroidBitmapsHighlighted = new Bitmap[3];
   asteroidBitmapsHighlighted[Asteroid.WHOLE] = BitmapFactory
     .decodeResource(resources,
       R.drawable.asteroid_whole_highlighted);
   asteroidBitmapsHighlighted[Asteroid.HALF] = BitmapFactory
     .decodeResource(resources, R.drawable.asteroid_half_highlighted);
   asteroidBitmapsHighlighted[Asteroid.QUARTER] = BitmapFactory
     .decodeResource(resources,
       R.drawable.asteroid_quarter_highlighted);

   asteroids = new ArrayList<Asteroid>();
   float x;
   float y;
   double velocity;
   double direction;
   int rotation;

   for (int i = 0; i < level; i++) {
    x = 240 * random.nextFloat();
    y = 50 * random.nextFloat();
    velocity = 0.2 + 0.5 * i;
    direction = 180 - 45 + 90 * random.nextFloat();
    rotation = -1 - random.nextInt(3);
    asteroids.add(new Asteroid(asteroidBitmaps[Asteroid.WHOLE],
      asteroidBitmapsHighlighted[Asteroid.WHOLE], Asteroid.WHOLE,
      x, y, velocity, direction, rotation));
   }
  }

  public void update(float screenWidth, float screenHeight) {

   int i = 0;
   for (Asteroid asteroid : asteroids) {
    asteroid.move(screenWidth, screenHeight);
    if (asteroid.collided) {
     asteroids.remove(i);
     asteroids.trimToSize();
     if (asteroid.size > Asteroid.QUARTER) {
      int fragmentSize = Asteroid.HALF;
      if (asteroid.size == Asteroid.HALF)
       fragmentSize = Asteroid.QUARTER;
      asteroids.add(new Asteroid(asteroidBitmaps[fragmentSize],
        asteroidBitmapsHighlighted[fragmentSize],
        fragmentSize, asteroid.x, asteroid.y, asteroid
          .getVelocity(), asteroid.getDirection()-45, asteroid.rotation));
      asteroids.add(new Asteroid(asteroidBitmaps[fragmentSize],
        asteroidBitmapsHighlighted[fragmentSize],
        fragmentSize, asteroid.x, asteroid.y, asteroid
          .getVelocity(), asteroid.getDirection()+45, asteroid.rotation));
     }
     break;
    }
    i++;
   }
  }

  public void draw(Canvas canvas) {
   for (Asteroid asteroid : asteroids) {
    asteroid.draw(canvas, asteroid.x, asteroid.y, paint);
   }
  }

}

In the update method we create two new, smaller asteroids for collided asteroids larger than QUARTER, otherwise we remove them. The new, smaller asteroids are given a changed direction in relation to their "parent" (-45° and +45°), and to avoid angles below 0° or above 360°, we make a small modification in the GfxObject's setDirection method:

public void setDirection(double direction) {
  this.direction = direction;
  if (this.direction > 360)
   this.direction -= 360;
  if (this.direction < 0)
   this.direction += 360;
  calculateDXDY();
}

Let's launch the app, fire away a few missiles and see how the asteroids are divided in smaller pieces:

 

If I would happen to be a slightly better coder, we would have been done with these parts here, but when I tried to stress-test the app by firing missiles as fast as I could, the app crashed. Therefore, the next chapter will handle yet a lesson learned. Hang on...

 

Requirements implemented in this chapter

✔ 22. Missiles only "live" for a short period of time so that they can't hit the ship that fired them in the back.

✔ 23. When a "full-size" asteroid is hit by a missile, it shall be split into two "half-size" asteroids.

✔ 24. When a "half-size" asteroid is hit by a missile, it shall be split into two "quarter-size" asteroids.

✔ 25. When a "quarter-size" asteroid id hit by a missile, it shall disappear.

✔ 26. When an asteroid is split into two, both parts shall inherit the "parent's" velocity, but change their direction approximately 45° (clockwise for one of them and counter-clockwise for the other).

Theme by Danetsoft and Danang Probo Sayekti inspired by Maksimer