You are here

Chapter 21: Lessons learned #2 and #3 - Scaling and bouncing

Eager to get going, I just started coding three new "features" into the code:

  1. The property "size" for an asteroid. I forgot that earlier. (I just included a pointer to the "whole" asteroid image when they are created.)
  2. The collision detection itself, consisting of a loop that checks if the distance between all previously handles asteroids and the current one, is larger than the sum of their radiuses.
  3. A simple bounce-effect, by reverting the direction of the current asteroid.

 

It didn't work quite the way I hoped it to:

First, asteroids collide way before they actually meet.

Secondly, they get stuck on each other.

It looks really terrible. Worse than the bad rotation.

 

This is what I did in AsteroidHandler.java. (Bold parts are to handle the concept of "size", red parts are the collision detection and bouncing.):

package com.ajomannen.justroids;

import java.util.ArrayList;
import java.util.List;
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 float[] asteroidRadiuses;
  private List<Asteroid> asteroids;
  private Random random;
  private Paint paint;

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

   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);

   asteroidRadiuses = new float[3];
   asteroidRadiuses[Asteroid.WHOLE] = 50;
   asteroidRadiuses[Asteroid.HALF] = 30;
   asteroidRadiuses[Asteroid.QUARTER] = 15;

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

   for (int i = 0; i < level; i++) {
    x = 160 * random.nextFloat();
    y = 320 * random.nextFloat();
    // No random velocity anymore. Each asteroid is faster than the
    // previous.
    velocity = 0.2 + 0.5 * i;
    direction = 360 * random.nextFloat();
    rotation = -1 - random.nextInt(3);
    asteroids.add(new Asteroid(asteroidBitmaps[Asteroid.WHOLE],
      Asteroid.WHOLE, x, y, velocity, direction, rotation));
   }
  }

  public void update(float screenWidth, float screenHeight) {
   int index = 0;
   Asteroid otherAsteroid;
   float dX;
   float dY;
   float distance;

   for (Asteroid thisAsteroid : asteroids) {
    thisAsteroid.move(screenWidth, screenHeight);
    for (int i = 0; i < index; i++) {
     otherAsteroid = asteroids.get(i);
     dX = Math.abs(thisAsteroid.x - otherAsteroid.x);
     dY = Math.abs(thisAsteroid.y - otherAsteroid.y);
     distance = (float) Math.sqrt(dX * dX + dY * dY);
     if (distance <= (asteroidRadiuses[thisAsteroid.size] + asteroidRadiuses[otherAsteroid.size])) {
      double newDirection = thisAsteroid.getDirection() + 160
        + 40 * random.nextFloat();
      if (newDirection >= 360)
       newDirection -= 360;
      thisAsteroid.setDirection(newDirection);
      break;
     }
    }
    index++;
   }
  }

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

}

As we call getDirection() and getVelocity() for an asteroid above, we have to add those two methods in GfxObject.java also:

public double getVelocity() {
  return velocity;
}

public double getDirection() {
  return direction;
}

We also have to add "size" to Asteroid.java:

package com.ajomannen.justroids;

import android.graphics.Bitmap;

public class Asteroid extends GfxObject {
  static final int WHOLE = 2;
  static final int HALF = 1;
  static final int QUARTER = 0;

  int size;

  public Asteroid(Bitmap bitmap, int size, float x, float y, double velocity,
    double direction, int rotation) {
   this.bitmap = bitmap;
   this.size = size;
   this.x = x;
   this.y = y;
   setVelocity(velocity);
   setDirection(direction);
   this.rotation = rotation;
  }

}

My mistakes here are two; 

  1. Just because the asteroid-images are a certain number of pixels wide, it doesn't mean that they are that wide on the device. Android scales images at will.  What actually happens is that Android will consider my only version of asteroid_whole to be a "medium-density" (mdpi) version, and if the device we use is ldpi or hdpi it will be scaled unless we specifically ask it to not scale. Hence, we can not simply assume a certain radius in our code - we must check the width/height of the bitmap image after​ it has been loaded.
  2. Real collisions does not often lead to that the fastest object simply turn back at the same speed. And if they do, they don't turn back again and stick to the slower object if the collision is still ongoing. 

I think I will have to solve the issues one at a time, and start with disabling "bouncing" and just highlight the collided asteroids in a separate color, while I fix the scaling issue. 

Let's start with adding a "collided" property to GfxObject:

  protected boolean collided = false;

  public boolean isCollided() {
   return collided;
  }

  public void setCollided(boolean collided) {
   this.collided = collided;
  }

Then we comment out the crappy bouncing code and instead set "collided" for both asteroids in AsteroidHandler update() method:

 public void update(float screenWidth, float screenHeight) {
  int index = 0;
  Asteroid otherAsteroid;
  float dX;
  float dY;
  float distance;

  for (Asteroid thisAsteroid : asteroids) {
   thisAsteroid.move(screenWidth, screenHeight);
   thisAsteroid.setCollided(false);
   for (int i = 0; i < index; i++) {
    otherAsteroid = asteroids.get(i);
    dX = Math.abs(thisAsteroid.x - otherAsteroid.x);
    dY = Math.abs(thisAsteroid.y - otherAsteroid.y);
    distance = (float) Math.sqrt(dX * dX + dY * dY);
    if (distance <= (asteroidRadiuses[thisAsteroid.size] + asteroidRadiuses[otherAsteroid.size])) {
     thisAsteroid.setCollided(true);
     otherAsteroid.setCollided(true);
     // double newDirection = thisAsteroid.getDirection() + 160
     // + 40 * random.nextFloat();
     // if (newDirection >= 360)
     // newDirection -= 360;
     // thisAsteroid.setDirection(newDirection);
     // break;
    }
   }
   index++;
  }
}

After that, we can decide how this collision should be illustrated.

I choose to make a very red copy of all three asteroid images and call them "asteroid_xxxx_highlight.png", add a second Bitmap in GfxObject:

protected Bitmap bitmapHighlighted;

and also add it in the constructor in Asteroid:

 public Asteroid(Bitmap bitmap, Bitmap bitmapHilighted, int size, float x,
   float y, double velocity, double direction, int rotation) {
  this.bitmap = bitmap;
  this.bitmapHighlighted = bitmapHighlighted;
  this.size = size;
  this.x = x;
  this.y = y;
  setVelocity(velocity);
  setDirection(direction);
  this.rotation = rotation;
}

Time to get another Bitmap array into AsteroidHandler:

  private Bitmap[] asteroidBitmaps;
  private Bitmap[] asteroidBitmapsHighlighted;

Populate it with:

  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);

And include one of them when creating the asteroids:

   asteroids.add(new Asteroid(asteroidBitmaps[Asteroid.WHOLE],
     asteroidBitmapsHighlighted[Asteroid.WHOLE], Asteroid.WHOLE,
     x, y, velocity, direction, rotation));

And finally, get the draw() method in GfxObject to choose between the two images:

 public void draw(Canvas canvas, float x, float y, Paint paint) {
  canvas.save(Canvas.MATRIX_SAVE_FLAG);
  canvas.rotate(angle, x, y);
  if (collided) {
   canvas.drawBitmap(bitmapHighlighted,
     x - bitmapHighlighted.getWidth() / 2,
     y - bitmapHighlighted.getHeight() / 2, paint);
  } else {
   canvas.drawBitmap(bitmap, x - bitmap.getWidth() / 2,
     y - bitmap.getHeight() / 2, paint);
  }
  canvas.restore();
}

Well, this worked like a charm:

 

Now we have a good trouble-shooting scenario to fix the scaling issue.

Let's do that in next chapter.

Theme by Danetsoft and Danang Probo Sayekti inspired by Maksimer