Chapter 4. Java Games Continued: Fun with Polygons

In this chapter, we take things to the next level with an arcade classic: Asteroids. The goal of this chapter is to illustrate a polygon-based game (Asteroids), as opposed to the previous chapter's sprite-based game (Space Blaster). Using polygons presents a new set of challenges due to the limited polygon capabilities of Android, as you'll see in the next section. Nevertheless, Asteroids is a relatively simple game that takes advantage of the high portability of the Java language by reusing code from the standard Java SE Abstract Windowing Toolkit (AWT). Let's get started.

About the Chapter Layout

This chapter has a different layout than previous chapters, as it doesn't start with a description of the game implementation but a series of caveats I found when trying to create Asteroids:

  • Caveats of drawing polygons and rectangles: It turns out that, at the time of this writing, there is no polygon support in Android. This made creating this game tougher than the previous chapter's, as I found the need to create three brand new classes: Rectangle, Polygon, and PolygonSripte. Lucky for me, I was able to take advantage of the high portability of the Java language and reuse most of the code of the Rectangle and Polygon classes of Java SE (this is one of the reasons I like the Java language). PolygonSprite is a class I created from the ground up.

  • Game architecture and implementation: In this section, the actual game implementation starts. I chose to do this because the game cannot be described without the previous foundation classes. Here you will learn about resource description, game life cycle, processing key press and touch events plus Testing in the emulator. Let's get started.

Understanding the Caveats of Drawing Polygons in Android

Before starting work on Asteroids, let's take a look at the caveats of drawing polygons in Android. This section represents the foundation over which the game itself will be built. There is a little problem when thinking of building a polygon-based game in Android—the API has no polygons. This would be like designing a house just to find out there is no land to build on. In the previous chapter, we used the onDraw method of the LinearLayout class to render bitmaps. Asteroids will use the same technique to draw polygons instead of bitmaps. To illustrate this concept, consider the following code snippet to draw the Android canvas:

class MyLayout extends Linearlayout {
    // ...
  protected void onDraw(Canvas canvas)
  {
     // draw a point
     canvas.drawPoint(x, y, aPaint);

     // Draw lines
     canvas.drawLines(float[] points, aPaint);
  }
}

The Canvas class holds many draw calls. To draw something, you need four basic components: a bitmap to hold the pixels, a Canvas to host the draw calls, a drawing primitive (e.g., Rect, Path, Text, or Bitmap), and a Paint to describe the colors and styles for the drawing. The basic primitives that could help when drawing polygons follow:

  • drawPoint(float x, float y, Paint paint) draws a single point given X and Y coordinates and a paint style.

  • drawArc (RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint) draws the specified arc, which will be scaled to fit inside the specified rectangle. oval defines the bounds of the rectangle used to define the shape and size of the arc. startAngle is the starting angle (in degrees) where the arc begins. sweepAngle is the sweep angle (in degrees) measured clockwise. useCenter, if true, includes the center of the oval in the arc and closes it if it is being stroked drawing a wedge.

  • drawCircle (float cx, float cy, float radius, Paint paint) draws a circle using the specified paint. cx and cy define the X and Y coordinates of the center. The circle will be filled or framed based on the Style in the paint.

  • drawLine (float startX, float startY, float stopX, float stopY, Paint paint) draws a line segment with the specified start and stop coordinates, using the specified paint. The style paint is ignored because a line is always framed.

  • drawLines (float[] pts, int offset, int count, Paint paint) draws a series of lines. Each line is taken from four consecutive values in the pts array. Thus to draw one line, the array must contain at least four values. These are the X and Y coordinates of each point: X0, Y0, X1, Y1,...,Xn,Yn.

  • drawRect (Rect r, Paint paint) draws the specified Rect using the specified Paint. The rectangle will be filled or framed based on the style in the paint.

  • drawRoundRect (RectF rect, float rx, float ry, Paint paint) draws the specified rounded rectangle using the specified paint. The rounded rectangle will be filled or framed based on the style in the paint. rx and ry define the xy radius of the oval used to round the corners.

These are the most important methods for drawing shapes in the Canvas class, but we need more. For example, for a polygon-based game, a basic technique is to inscribe shapes within rectangles. In this way, operations such as collision detection and rendering can be applied. Thus, we need three basic shapes for our Asteroids game that are missing from the Android API: a rectangle, a polygon, and a polygon sprite.

Tip

The Android API defines the classes Rect and RectF for rectangles, but it does not define polygons or polygon sprites.

Understanding the Caveats of Drawing Rectangles

From the drawing methods in the previous section, we could use Canvas.drawLines to draw our polygons directly. However, we also need extra functionally:

  • A way to detect if a polygon is inside another in the (X, Y) coordinate system, which is necessary to detect collisions within the game

  • A way to assign style and color to the polygon

Thus, using Canvas.drawLines is insufficient to manipulate polygons directly. We need a more elegant solution; we need to create Rectangle, Polygon, and PolygonSprite classes.

Android already has the classes Rect and RectF that could be reused. Nevertheless these classes lack the functionality to check if the rectangle is inscribed within another (a critical requirement for Asteroids).

Listing 4-1 shows the class Rectangle capable of remembering its (X, Y) coordinates plus width and height. It can also check if its bounds contain or are inside another rectangle. As a matter of fact this code is taken from the Java SE java.awt.Rectangle class (taking advantage of the high portability of Java).

The Rectangle class in Listing 4-1 is the basis for the polygon sprite we need for Asteroids.

Example 4-1. The Rectangle Class Used by Polygon

package ch04.common;

public class Rectangle {

    public int x;
    public int y;
    public int width;
    public int height;

    public Rectangle() {
        this(0, 0, 0, 0);
    }
/**
     * Constructs a new <code>Rectangle</code> whose top-left corner is
     * specified as (<code>x</code>,&nbsp;<code>y</code>) and whose width
     * and height are specified by the arguments of the same name.
     *
     * @param x
     *            the specified x coordinate
     * @param y
     *            the specified y coordinate
     * @param width
     *            the width of the <code>Rectangle</code>
     * @param height
     *            the height of the <code>Rectangle</code>
     */
    public Rectangle(int x, int y, int width, int height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    public boolean contains(int X, int Y, int W, int H) {
        int w = this.width;
        int h = this.height;

        if ((w | h | W | H) < 0) {
            // At least one of the dimensions is negative...
            return false;
        }
        // Note: if any dimension is zero, tests below must return false...
        int x = this.x;
        int y = this.y;
        if (X < x || Y < y) {
            return false;
        }
        w += x;
        W += X;
        if (W <= X) {
            // X+W overflowed or W was zero, return false if...
            // either original w or W was zero or
            // x+w did not overflow or
            // the overflowed x+w is smaller than the overflowed X+W
            if (w >= x || W > w) {
                return false;
            }
        } else {
            // X+W did not overflow and W was not zero, return false if...
            // original w was zero or
// x+w did not overflow and x+w is smaller than X+W
            if (w >= x && W > w) {
                return false;
            }
        }
        h += y;
        H += Y;
        if (H <= Y) {
            if (h >= y || H > h) {
                return false;
            }
        } else {
            if (h >= y && H > h) {
                return false;
            }
        }
        return true;
    }

    public boolean contains(int x, int y) {
        return inside(x, y);
    }

    public boolean inside(int X, int Y) {
        int w = this.width;
        int h = this.height;
        if ((w | h) < 0) {
            // At least one of the dimensions is negative...
            return false;
        }
        // Note: if either dimension is zero, tests below must return false...
        int x = this.x;
        int y = this.y;
        if (X < x || Y < y) {
            return false;
        }
        w += x;
        h += y;
        // overflow || intersect
        return ((w < x || w > X) && (h < y || h > Y));
    }
}

Creating a Polygon Class for Asteroids

The standard J2SE API has the neat class Polygon that we can reuse for Asteroids. It is hard to understand why Google has not included such useful classes from java.awt into the Android API. Listing 4-2 shows the modified Polygon class (it is the same as the Java SE Polygon stripped for running in Android and Asteroids). Some of the most interesting methods of this class follow:

  • Constructors: The default constructor will create a four-sided polygon. You can also define an N-sided polygon by giving two arrays representing the X0,X1,...Xn and Y0,Y1,...Yn coordinates of the vertices where (X0,Y0) represents the coordinates of the first vertex and so forth, The constructor also takes the number of sides.

  • calculateBounds(int xpoints[], int ypoints[], int npoints): This method calculates the bounds of the polygon given a set of X and Y coordinates and number of points with respect to maximum integer values.

  • addPoint(int x, int y): This one adds a point to the polygon given its X and Y coordinates.

  • updateBounds(int x, int y): This method updates the bounds of the polygon to include a new point (X,Y).

  • getBoundingBox(): This returns the rectangular bounds of the polygon.

  • contains(int x, int y): This one checks if the polygon contains a given point (X,Y).

  • float[] getPoints(): This method returns the X and Y coordinates of the vertices of the polygon.

Both the Polygon and Rectangle classes are used by the next class, PolygonSprite, to define all game objects.

Example 4-2. The Polygon Class for Asteroids

package ch04.common;

public class Polygon {

    public int npoints;
    public int[] ypoints;
    public int[] xpoints;

    protected Rectangle bounds;

    public Polygon() {
        xpoints = new int[4];
        ypoints = new int[4];
    }

    public Polygon(int xpoints[], int ypoints[], int npoints) {
        // Fix 4489009: should throw IndexOutofBoundsException instead
        // of OutofMemoryException if npoints is huge
        // and > {x,y}points.length
        if (npoints > xpoints.length || npoints > ypoints.length) {
            throw new IndexOutOfBoundsException(
                    "npoints > xpoints.length || npoints > ypoints.length");
        }
this.npoints = npoints;
        this.xpoints = new int[npoints];
        this.ypoints = new int[npoints];
        System.arraycopy(xpoints, 0, this.xpoints, 0, npoints);
        System.arraycopy(ypoints, 0, this.ypoints, 0, npoints);
    }

    void calculateBounds(int xpoints[], int ypoints[], int npoints) {
        int boundsMinX = Integer.MAX_VALUE;
        int boundsMinY = Integer.MAX_VALUE;
        int boundsMaxX = Integer.MIN_VALUE;
        int boundsMaxY = Integer.MIN_VALUE;

        for (int i = 0; i < npoints; i++) {
            int x = xpoints[i];
            boundsMinX = Math.min(boundsMinX, x);
            boundsMaxX = Math.max(boundsMaxX, x);
            int y = ypoints[i];
            boundsMinY = Math.min(boundsMinY, y);
            boundsMaxY = Math.max(boundsMaxY, y);
        }

        bounds = new Rectangle(boundsMinX, boundsMinY, boundsMaxX
                - boundsMinX, boundsMaxY - boundsMinY);
    }

    public void reset() {
        npoints = 0;
        bounds = null;
    }

    /**
     * Appends the specified coordinates to this <code>Polygon</code>.
     *
     * @param x
     * @param y
     */
    public void addPoint(int x, int y) {
        if (npoints == xpoints.length) {
            int tmp[];

            tmp = new int[npoints * 2];
            System.arraycopy(xpoints, 0, tmp, 0, npoints);
            xpoints = tmp;

            tmp = new int[npoints * 2];
            System.arraycopy(ypoints, 0, tmp, 0, npoints);
            ypoints = tmp;
        }
        xpoints[npoints] = x;
        ypoints[npoints] = y;
npoints++;
        if (bounds != null) {
            updateBounds(x, y);
        }
    }

    void updateBounds(int x, int y) {
        if (x < bounds.x) {
            bounds.width = bounds.width + (bounds.x - x);
            bounds.x = x;
        } else {
            bounds.width = Math.max(bounds.width, x - bounds.x);
        }

        if (y < bounds.y) {
            bounds.height = bounds.height + (bounds.y - y);
            bounds.y = y;
        } else {
            bounds.height = Math.max(bounds.height, y - bounds.y);
        }
    }

    public Rectangle getBoundingBox() {
        if (npoints == 0) {
            return new Rectangle();
        }
        if (bounds == null) {
            calculateBounds(xpoints, ypoints, npoints);
        }
        return bounds;
    }

    /**
     * Determines if the specified coordinates are inside this
     * <code>Polygon</code>.
     *
     * @param x
     * @param y
     * @return
     */
    public boolean contains(int x, int y) {
        if (npoints <= 2 || !getBoundingBox().contains(x, y)) {
            return false;
        }
        int hits = 0;

        int lastx = xpoints[npoints − 1];
        int lasty = ypoints[npoints − 1];
        int curx, cury;
// Walk the edges of the polygon
        for (int i = 0; i < npoints; lastx = curx, lasty = cury, i++) {
            curx = xpoints[i];
            cury = ypoints[i];

            if (cury == lasty) {
                continue;
            }

            int leftx;
            if (curx < lastx) {
                if (x >= lastx) {
                    continue;
                }
                leftx = curx;
            } else {
                if (x >= curx) {
                    continue;
                }
                leftx = lastx;
            }

            double test1, test2;
            if (cury < lasty) {
                if (y < cury || y >= lasty) {
                    continue;
                }
                if (x < leftx) {
                    hits++;
                    continue;
                }
                test1 = x - curx;
                test2 = y - cury;
            } else {
                if (y < lasty || y >= cury) {
                    continue;
                }
                if (x < leftx) {
                    hits++;
                    continue;
                }
                test1 = x - lastx;
                test2 = y - lasty;
            }

            if (test1 < (test2 / (lasty - cury) * (lastx - curx))) {
                hits++;
            }
        }
return ((hits & 1) != 0);
    }

    @Override
    public String toString() {
        if (npoints == 0)
            return null;
        String s = ""; //

        for (int i = 0; i < xpoints.length; i++) {
            s += "(" + xpoints[i] + "," + ypoints[i] + ") ";
        }
        return s;
    }

    /**
     * Get polygon points (x0y0, x1y1,.....) for rendering Each point pair will
     * render 1 line so the total # of points must be num sides * 4
     *
     * @return
     */
    public float[] getPoints() {
        int size = npoints * 4;
        float[] points = new float[size];
        int j = 1;

        if (size == 0 || xpoints == null || ypoints == null)
            return null;

        points[0] = xpoints[0];
        points[1] = ypoints[0];

        for (int i = 2; i < points.length − 2; i += 4) {

            points[i] = xpoints[j];
            points[i + 1] = ypoints[j];
            points[i + 2] = xpoints[j];
            points[i + 3] = ypoints[j];
            j++;
        }
        points[size − 2] = xpoints[0];
        points[size − 1] = ypoints[0];

        return points;
    }
}

Creating a PolygonSprite Class for Asteroids

PolygonSprite is the final foundation class of the game (see Listing 4-3). It is used to describe all game objects including the ship, asteroids, and flying saucer. Furthermore, it tracks information such as the following for all of these game objects:

  • X and Y coordinates

  • Angle of rotation

  • Position in the screen

In this class, two polygons are used: one to keep the basic shape, and the other to apply the final translation and rotation and to paint on screen.

Example 4-3. The PolygonSprite Class Used by Asteroids

package ch04.game;

import ch04.common.Polygon;
import android.graphics.Canvas;
import android.graphics.Paint;

/************************************************************
 * The PolygonSprite class defines a game object, including it's shape,
 * position, movement and rotation. It also can determine if two objects
 * collide.
 ************************************************************/

public class PolygonSprite
{

    // Base sprite shape, centered at the origin (0,0).
    public Polygon shape;

    // Final location and shape of sprite after
    // applying rotation and translation to get screen
    // position. Used for drawing on the screen and in
    // detecting collisions.
    public Polygon sprite;

    boolean active; // Active flag.
    double angle; // Current angle of rotation.
    double deltaAngle; // Amount to change the rotation angle.
    double deltaX, deltaY; // Amount to change the screen position.

    // coords
    int x;
    int y;

    // Constructors:
public PolygonSprite() {

        this.shape = new Polygon();
        this.active = false;
        this.angle = 0.0;
        this.deltaAngle = 0.0;
        this.x = 0;
        this.y = 0;
        this.deltaX = 0.0;
        this.deltaY = 0.0;
        this.sprite = new Polygon();
    }

    public void render(int width, int height) {

        int i;

        // Render the sprite's shape and location by rotating it's
        // base shape and moving it to its proper screen position.

        this.sprite = new Polygon();
        for (i = 0; i < this.shape.npoints; i++) {
            this.sprite.addPoint((int) Math.round(this.shape.xpoints[i]
                    * Math.cos(this.angle) + this.shape.ypoints[i]
                    * Math.sin(this.angle))
                    + (int) Math.round(this.x) + width / 2,
                    (int) Math.round(this.shape.ypoints[i]
                            * Math.cos(this.angle) - this.shape.xpoints[i]
                            * Math.sin(this.angle))
                            + (int) Math.round(this.y) + height / 2);
        }
    }

    public boolean isColliding(PolygonSprite s) {

        int i;

        // Determine if one sprite overlaps with another, i.e., if any vertice
        // of one sprite lands inside the other.

        for (i = 0; i < s.sprite.npoints; i++) {
            if (this.sprite.contains(s.sprite.xpoints[i],
                    s.sprite.ypoints[i])) {
                return true;
            }
        }
        for (i = 0; i < this.sprite.npoints; i++) {
            if (s.sprite.contains(this.sprite.xpoints[i],
                    this.sprite.ypoints[i])) {
                return true;
            }
}

        return false;
    }

    /**
     * Advance Sprite
     *
     * @param width screen width
     * @param height screen height
     * @return
     */
    public boolean advance(int width, int height) {

        boolean wrapped;

        // Update the rotation and position of the sprite based on the delta
        // values. If the sprite moves off the edge of the screen, it is wrapped
        // around to the other side and TRUE is returned.

        this.angle += this.deltaAngle;
        if (this.angle < 0)
            this.angle += 2 * Math.PI;
        if (this.angle > 2 * Math.PI)
            this.angle -= 2 * Math.PI;
        wrapped = false;
        this.x += this.deltaX;
        if (this.x < -width / 2) {
            this.x += width;
            wrapped = true;
        }
        if (this.x > width / 2) {
            this.x -= width;
            wrapped = true;
        }
        this.y -= this.deltaY;
        if (this.y < -height / 2) {
            this.y += height;
            wrapped = true;
        }
        if (this.y > height / 2) {
            this.y -= height;
            wrapped = true;
        }
        return wrapped;
    }

    @Override
    public String toString() {
        return "Sprite: " + sprite + " Shape:" + shape;
    }
/**
     * Draw Sprite using polygon points
     *
     * @param canvas
     * @param paint
     */
    void draw(Canvas canvas, Paint paint) {
        float[] points = sprite.getPoints();

        if (points != null) {
            canvas.drawLines(points, paint);
        }
    }
}

PolygonSprite defines the following methods:

  • render(int width, int height) computes the shape of the sprite given the width and height of the screen. It also translates the polygon to the proper screen position and applies rotation. Note that this method has nothing to do with the actual screen painting (so perhaps "render" was not the best name choice).

  • isColliding(PolygonSprite s) checks if the sprite is colliding with another by checking if the vertices of the sprite are inside the other (see Figure 4-1).

  • advance(int width, int height) moves the sprite around based on the delta X and Y values. It also updates the sprites rotation values. The width and height arguments represent the size of the screen. Note that the sprite will be wrapped around if it moves off the screen.

  • draw(Canvas canvas, Paint paint) does the actual drawing of the sprite in the Android layout. Its arguments are a Canvas object where the points will be drawn and a Paint object for style and color information.

Polygon sprites about to collide

Figure 4-1. Polygon sprites about to collide

We have discussed the caveats of using polygons in Android. The goal of this section has been to illustrate the missing pieces we need to start building the game itself. We now have the foundation classes for Asteroids, so let's look at the actual game architecture.

Understanding the Game's Architecture

The architecture of Asteroids is almost identical to Space Blaster from the last chapter. When the program starts, the main class AsteroidsActivity will be loaded by Android. This activity will load the user-defined layout in the Asteroids class. This class, in turn, inherits from the abstract class ArcadeGame, which extends LinearLayout. Note that the user-defined layout is bound with the system by the XML file asteroids.xml (see Figure 4-2).

Here, you realize why ArcadeGame has been defined as Abstract. By incorporating common functionality in this class, we can implement multiple arcade games in the same project. As a matter of fact, you can merge SpaceBlaster and Asteroids into the same project and build your own arcade system.

The Asteroids game architecture

Figure 4-2. The Asteroids game architecture

Let's take a closer look at the actual project.

Creating the Project

We'll start by creating an Android project called Asteroids. Here is how:

  1. Click the New Android Project button from the Eclipse toolbar.

  2. Enter the project information:

    • Name: ch04.Asteroids

    • Build Target: Android 1.5

    • Application Name: Asteroids

    • Package name: ch04.game

    • Create Activity: AsteroidsActivity

    • Min SDK Version: 3

  3. Click Finish (see Figure 4-3).

    The Asteroids project information

    Figure 4-3. The Asteroids project information

Creating the Game Layout

We must create the user-defined layout using XML and the Asteroids class. Rename the main.xml file created by the wizard as asteroids.xml and insert the code in Listing 4-4.

Example 4-4. The XML Layout of Asteroids

<?xml version="1.0" encoding="utf-8"?>

<ch04.game.Asteroids
 xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/ll_asteroids"
   android:orientation="vertical"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   android:background="#FF000000">
</ch04.game.Asteroids>

Note that ch04.game.Asteroids is a required Java class that extends ArcadeGame, which in turn extends LinearLayout. A detailed list of files and resources is shown in the next section.

Looking at the Resources

Besides code, we need resources such as sounds, XML string messages, and a game icon (see Figure 4-4). Here is a list of resources used by Asteroids and what they do:

  • Java classes

    • AsteroidsActivity: This is the master class called by Android when the user starts the game. It contains life cycle events such as onCreate, onResume, and onStop.

    • Asteroids: This class contains all game code. It extends ArcadeGame.

    • ArcadeGame: This is an abstract class with the game loop and game life cycle events initialize, updatePhysics, getScore, halt, and resume.

    • Constants: This class contains many game constants such as game delay, frames per second, number of ships, and sprite speeds.

  • Polygon classes

    • PolygonSprite: This class represents any game sprite. It has methods to detect collisions using the classes Polygon and Rectangle.

    • Polygon: This class represents an N-sided polygon. It code is taken from the Java SE java.awt.Polygon class with most of the code stripped for simplicity.

    • Rectangle: This is also a stripped version of the Java SE java.awt.Rectangle class. It describes a rectangle by X and Y coordinates, as well as width and height. It can also check if the rectangle contains or is inside another.

  • Other classes

    • AudioClip: This class deals with the Android MediaPlayer and is capable of playing, stopping, or looping a sound clip.

    • Tools: This class contains miscellaneous help methods such as message boxes, dialogs, and a browser launcher.

  • Other resources

    • Asteroids.xml: This is the user-defined layout of the game. It points to the ch04.game.Asteroids class.

    • Asteroids.png: This is the game icon that you will see in your phone window. Clicking this icon will start the game.

    • Strings.xml: This file contains game strings, such as the game name and other messages.

  • Raw sounds

    • a_crash.mp3: Played when an asteroid explodes

    • a_explosion.mp3: Played when the ship explodes

    • a_fire.mp3: Played when the ship fires the gun

    • a_missile.mp3: Played when the ship fires the missile

    • a_saucer.mp3: Played when the UFO shows up on screen

    • a_thrusters.mp3: Played when the ship moves around

    • a_warp.mp3: Played when the ship jumps into hyperspace (To avoid a collision, a ship vanishes from one point and reappears in another.)

The Asteroids resources

Figure 4-4. The Asteroids resources

Understanding the Game Life Cycle

As mentioned earlier, we use a LinerLayout to paint sprites on screen using a continuous game loop. The layout and loop—both controlled using Timers—are described by the abstract class ArcadeGame (see Listing 4-5). This class also defines the life cycle of the game using abstract methods that must be overridden by the child class Asteroids. This life cycle contains the following methods:

  • void initialize() fires on game initialization. This is where sounds are loaded and sprites are created.

  • void onDraw() must be overridden to paint sprites on screen. All painting is done using a Canvas and one or more Paint objects for font, color, and style information.

  • void updatePhysics() fires once on every step of the loop. Use this method to update the sprites on the game.

  • boolean gameOver() can be used to check if the user has terminated the game and perhaps get a final score. Its implementation is mostly optional.

  • long getScore() can be used to get the user's final score.

Example 4-5. The Game Loop Using Timer Tasks

public abstract class ArcadeGame extends LinearLayout
{
    // Update timer used to invalidate the view
    private Timer mUpdateTimer;

    // Timer period
    private long mPeriod = 1000;

    // ....

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        try {
            // Init game
            initialize();

            /**
             * start update task. Which will fire onDraw in the future
             */
            startUpdateTimer();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // ...

    /**
     * A timer is used to move the sprite around
     */
    protected void startUpdateTimer() {
        mUpdateTimer = new Timer();
        mUpdateTimer.schedule(new UpdateTask(), 0, mPeriod);
    }

    private class UpdateTask extends TimerTask {
        @Override
        public void run() {
            updatePhysics();

            /**
             * Cause an invalidate to happen on a subsequent cycle through
* the event loop. Use this to invalidate the View from
             * a non-UI thread. onDraw will be called sometime
             * in the future.
             */
            postInvalidate();
        }
    }

    /**
     * Overridden these to process game events (life-cycle)
     */

    // Update sprites here
    abstract protected void updatePhysics();

    // Init game
    abstract protected void initialize();

    // Game over
    abstract protected boolean gameOver();

    // Get score
    abstract protected long getScore();

}

Let's take a closer look at the game life cycle of Asteroids.

Initializing the Game

Game initialization occurs by overriding the initialize method in the Asteroids class (see Listing 4-6). It starts by loading sounds from the res/raw folder as follows:

Context ctx = getContext();
crashSound = new AudioClip(ctx, R.raw.a_crash);
clipTotal++;
explosionSound = new AudioClip(ctx, R.raw.a_explosion);
clipTotal++;

Here is where we use the AudioClip class to quickly load the raw resource using its ID. Note that we also need the application Context. The game also keeps track of the total number of clips in the game.

Next, we create the star background using an array of Points and the size of the screen:

void createStarts() {
    int width = getWidth();
    int height = getHeight();

    // Generate the starry background.
    numStars = width * height / 5000;
    stars = new Point[numStars];
for (i = 0; i < numStars; i++) {
        // random XY Point
        stars[i] = new Point((int) (Math.random() * width)
                , (int) (Math.random() * height) );
    }
}

The stars are created using random Points with X and Y coordinates defined using the getWidth and getHeight methods. Note that getWidth and getHeight are built-in methods provided by the LinerLayout class.

Next, we create all polygon sprites in the game:

  • The ship

  • The ship's thrusters

  • Photons (the ship's bullets)

  • The flying saucer

  • Asteroids and asteroid explosions

Note that all elements in the game are polygons, even the tiny photons and asteroid explosions. Finally, miscellaneous data is initialized and the game is put in Game Over mode.

Example 4-6. Game Initialization

protected void initialize() {

    // Load sounds
    try {
        loadSounds();
        loaded = true;
    } catch (Exception e) {
        Tools.MessageBox(mContex, "Sound Error: " + e.toString());
    }

    // create star background
createStarts();

    // Create shape for the ship sprite.
    ship = new PolygonSprite();
    ship.shape.addPoint(0, −10);
    ship.shape.addPoint(7, 10);
    ship.shape.addPoint(−7, 10);

    // Create thruster shapes
    createThrusters();

    // Create shape for each photon sprites.
    for (i = 0; i < Constants.MAX_SHOTS; i++) {
photons[i] = new PolygonSprite();
        photons[i].shape.addPoint(1, 1);
        photons[i].shape.addPoint(1, −1);
        photons[i].shape.addPoint(−1, 1);
        photons[i].shape.addPoint(−1, −1);
    }

    // Create shape for the flying saucer.
    ufo = new PolygonSprite();
    ufo.shape.addPoint(−15, 0);
    ufo.shape.addPoint(−10, −5);
    ufo.shape.addPoint(−5, −5);
    ufo.shape.addPoint(−5, −8);
    ufo.shape.addPoint(5, −8);
    ufo.shape.addPoint(5, −5);
    ufo.shape.addPoint(10, −5);
    ufo.shape.addPoint(15, 0);
    ufo.shape.addPoint(10, 5);
    ufo.shape.addPoint(−10, 5);

    // Create shape for the guided missile.
    missile = new PolygonSprite();
    missile.shape.addPoint(0, −4);
    missile.shape.addPoint(1, −3);
    missile.shape.addPoint(1, 3);
    missile.shape.addPoint(2, 4);
    missile.shape.addPoint(−2, 4);
    missile.shape.addPoint(−1, 3);
    missile.shape.addPoint(−1, −3);

    // Create asteroid sprites.
    for (i = 0; i < Constants.MAX_ROCKS; i++)
        asteroids[i] = new PolygonSprite();

    // Create explosion sprites.
    for (i = 0; i < Constants.MAX_SCRAP; i++)
        explosions[i] = new PolygonSprite();
    // Initialize game data and put it in 'game over' mode.
    highScore = 0;
    sound = true;
    detail = true;
    initGame();
    endGame();
}

Drawing Sprites

Drawing 23.70the sprites is the next step in the game life cycle and the most important too. Here is where all polygon sprites are drawn (see Listing 4-7). The most important steps are as follows:

  1. The size of the screen is queried using the layout's getWidth and getHeight methods. The size is required to render elements on screen

  2. The stars are drawn using an array of star sprites and calling canvas.drawPoint. Each sprite is drawn using its X and Y coordinates and a Paint object for style information.

  3. Photons are drawn only if they are active. A photon becomes active when the user presses the space bar to fire the gun. Each photon is aware of its position on the canvas.

  4. Missiles, a UFO, and the user's ship are drawn if they are active. When the user reaches a score threshold, the UFO will show up. Users must watch out for the missiles fired against them by the UFO. Note that the ship's thrusters must be drawn when the user presses the arrow keys to move the ship around.

  5. Asteroids and explosion debris are drawn. Note that each bit of debris is an independent sprite.

  6. Status messages are drawn. Messages include the score in the upper-left corner, the number of ships left in the lower-left, and the high score in the upper-right.

  7. Other messages are displayed depending on the state of the game, including Game Over, Game Paused, or Sound Muted.

Tip

Remember that each sprite is aware of its X and Y coordinates on screen and angle of rotation? The key method that performs all the magic is PolygonSprite.draw(Canvas canvas, Paint paint). Within this method, the polygon lines are drawn using canvas.drawLines(float[] points, Paint paint).

Example 4-7. The Drawing Subroutine for Asteroids

protected void onDraw(Canvas canvas)
{
    // get screen size
    int width = getWidth();
    int height = getHeight();

    // draw stars
    for (i = 0; i < numStars; i++) {
        canvas.drawPoint(stars[i].x, stars[i].y, mPaint);
    }

    // Draw photon bullets.
    for (i = 0; i < Constants.MAX_SHOTS; i++) {
        if (photons[i].active) {
            photons[i].draw(canvas, mPaint);
}
    }

    // Draw the guided missile,
    if (missile.active) {
        missile.draw(canvas, mPaint);
    }

    // Draw the flying saucer.
    if (ufo.active) {
        ufo.draw(canvas, mRedPaint);
    }

    // Draw the ship
    if (ship.active) {
        // draw ship
        ship.draw(canvas, mPaint);

        // Draw thruster exhaust if thrusters are on. Do it randomly to get
        // a flicker effect.

        if (!paused && Math.random() < 0.5) {
            if (up) {
                fwdThruster.draw(canvas, mPaint);
            }
            if (down) {
                revThruster.draw(canvas, mPaint);
            }
        }
    }

    // Draw the asteroids.
    for (i = 0; i < Constants.MAX_ROCKS; i++) {
        if (asteroids[i].active) {
            asteroids[i].draw(canvas, mGreenPaint);
        }
    }
    // Draw any explosion debris.
    for (i = 0; i < Constants.MAX_SCRAP; i++) {
        if (explosions[i].active) {
            explosions[i].draw(canvas, mGreenPaint);
        }
    }

    // Display status messages.
    float fontSize = mPaint.getTextSize();

    // upper left
    canvas.drawText("Score: " + score, fontSize, mPaint.getTextSize(),
            mPaint);
// Lower Left
    canvas.drawText("Ships: " + shipsLeft, fontSize, height - fontSize,
            mPaint);

    // Upper right
    String str = "High: " + highScore;

    canvas.drawText("High: " + highScore, width
            - (fontSize / 1.2f * str.length()), fontSize, mPaint);

    if (!sound) {
        str = "Mute";
        canvas.drawText(str, width - (fontSize * str.length()), height
                - fontSize, mPaint);
    }

    if ( ! playing) {
        if (loaded) {
            str = "Game Over";
            final float x = (width - (str.length() * fontSize / 2)) / 2;

            canvas.drawText(str, x, height / 4, mPaint);

        }
    }
    else if ( paused ) {
        str = "Game Paused";

        final float x = (width - (str.length() * fontSize / 2)) / 2;
        canvas.drawText(str, x, height / 4, mPaint);
    }
}

Updating Game Physics

The third step in the game life cycle is to update the game physics. This simply means updating the positions of all sprites in the game. Listing 4-8 shows the implementation for Asteroids. Let's take a closer look at this method:

  1. First, update the game sprites including the ship, photons, UFO, missile, asteroids, and explosions.

  2. Update the scores.

  3. Start the UFO if the score reaches the UFO score threshold.

  4. Create a new batch of asteroids if all have been destroyed.

Listing 4-8 also shows the method used to update the ship: updateShip() (note that sections have been stripped from Listing 4-8 for simplicity). This method performs the following tasks:

  1. Rotate or move the ship on screen depending on the value of the class variables: left, right, up, or down. These variables are updated depending on the key pressed by the user. The actual movement or rotation is performed by updating the delta values of the ship's X and Y coordinates and its angle of rotation.

  2. Update other sprites, like the thrusters (which are independent sprites), as the ship moves around.

  3. Perform other physics checks:

    • Do not let the ship exceed a speed limit.

    • If the ship has exploded, update the ship counter, and create a new ship or end the game depending on the number of ships left.

Example 4-8. Updating the Game Physics

protected void updatePhysics() {
    // Update Sprites
    updateShip();
    updatePhotons();
    updateUfo();
    updateMissile();
    updateAsteroids();
    updateExplosions();

    // Check the score and advance high score,
    if (score > highScore)
        highScore = score;

    // add a new ship if score reaches Constants.NEW_SHIP_POINTS
    if (score > newShipScore) {
        newShipScore += Constants.NEW_SHIP_POINTS;
        shipsLeft++;
    }

    // start the flying saucer as necessary.
    if (playing && score > newUfoScore && ! ufo.active) {
        newUfoScore += Constants.NEW_UFO_POINTS;
        ufoPassesLeft = Constants.UFO_PASSES;
        initUfo();
    }

    // If all asteroids have been destroyed create a new batch.
    if (asteroidsLeft <= 0)
        if (—asteroidsCounter <= 0)
            initAsteroids();

}

// Update Ship
public void updateShip() {
    int width = getWidth();
    int height = getHeight();

    double dx, dy, speed;

    if (!playing)
        return;

    /**
     * Rotate the ship if left or right cursor key is down.
     */
    if (left) {
        ship.angle += Constants.SHIP_ANGLE_STEP;
        if (ship.angle > 2 * Math.PI)
            ship.angle -= 2 * Math.PI;
    }
    if (right) {
        ship.angle -= Constants.SHIP_ANGLE_STEP;
        if (ship.angle < 0)
            ship.angle += 2 * Math.PI;
    }

    /**
     * Fire thrusters if up or down cursor key is down.
     */
    dx = Constants.SHIP_SPEED_STEP * -Math.sin(ship.angle);
    dy = Constants.SHIP_SPEED_STEP * Math.cos(ship.angle);
    if (up) {
        ship.deltaX += dx;
        ship.deltaY += dy;
    }
    if (down) {
        ship.deltaX -= dx;
        ship.deltaY -= dy;
    }

    /**
     * Don't let ship go past the speed limit.
     */
    if (up || down) {
        speed = Math.sqrt(ship.deltaX * ship.deltaX + ship.deltaY
                * ship.deltaY);
        if (speed > Constants.MAX_SHIP_SPEED) {
            dx = Constants.MAX_SHIP_SPEED * -Math.sin(ship.angle);
            dy = Constants.MAX_SHIP_SPEED * Math.cos(ship.angle);
            if (up)
                ship.deltaX = dx;
            else
                ship.deltaX = -dx;
            if (up)
ship.deltaY = dy;
            else
                ship.deltaY = -dy;
        }
    }

    /**
     * Move the ship. If it is currently in hyper space, advance the
     * count down.
     */
    if (ship.active) {
        ship.advance(width, height);
        ship.render(width, height);
        if (hyperCounter > 0)
            hyperCounter—;

        // Update the thruster sprites to match the ship sprite.

        fwdThruster.x = ship.x;
        fwdThruster.y = ship.y;
        fwdThruster.angle = ship.angle;
        fwdThruster.render(width, height);
        revThruster.x = ship.x;
        revThruster.y = ship.y;
        revThruster.angle = ship.angle;
        revThruster.render(width, height);
    }

    /**
     * Ship is exploding, advance the countdown or create a new ship if it
     * is done exploding. The new ship is added as though it were in
     * hyper space. This gives the player time to move the ship if it is
     * in imminent danger. If that was the last ship, end the game.
     */
    else {
        if (—shipCounter <= 0) {
            if (shipsLeft > 0) {
                initShip();
                hyperCounter = Constants.HYPER_COUNT;
            } else {
                endGame();
            }
        }
    }
}

Getting Scores

Scoring is the final step in the life cycle of Asteroids and is usually an optional step. In this step, a check is performed too see if the game is over and, if so, get a final score and do something with it (such as create a high score listing). Listing 4-9 shows two boolean variables used to check if the game is over and a long variable used to return the score.

Example 4-9. Getting the Game Score

protected boolean gameOver() {
    return loaded && !playing;
}

@Override
protected long getScore() {
    return score;
}

Responding to Key Press and Touch Events

The final piece of the puzzle is listening for user events such as key press and touch events and taking the appropriate action. We do this by overriding the onKeyUp, onKeyDown, and onTouchEvent methods. The following tasks are performed:

  • Update the class variables: up, down, left, or right depending on which arrow key is pressed.

  • Fire photons when the space bar is pressed.

  • Jump into hyperspace when H is pressed. The ship will disappear from one point and appear in a random location on the screen.

  • Toggle the pause mode when P is pressed.

  • Toggle sound (mute the game) when M is pressed.

  • Start the game when S is pressed.

  • Quit the game when E is pressed.

When the screen is tapped, the game will start if and only if the game resources are loaded and the game is not playing already.

Example 4-10. Responding to Key Press and Touch Events

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
    keyReleased(event);
    return true;
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    keyPressed(event);
    return true;
}

/**
 * OnTap Start Game
 */
@Override
public boolean onTouchEvent(MotionEvent event) {
    if (loaded && !playing) {
        initGame();
    }
    return true;
}

public void keyPressed(KeyEvent e) {
    final int keyCode = e.getKeyCode();

    /**
     * Check if any cursor keys have been pressed and set flags.
     */
    if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
            || keyCode == KeyEvent.KEYCODE_Q)
        left = true;
    if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
            || keyCode == KeyEvent.KEYCODE_W)
        right = true;
    if (keyCode == KeyEvent.KEYCODE_DPAD_UP
            || keyCode == KeyEvent.KEYCODE_O)
        up = true;
    if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN
            || keyCode == KeyEvent.KEYCODE_L)
        down = true;

    if ((up || down) && ship.active  && !thrustersPlaying) {
        if (sound && !paused) {
            thrustersSound.loop();
        }
        thrustersPlaying = true;
    }

    /**
     * Spacebar: fire a photon and start its counter.
     */
    if ( (keyCode == KeyEvent.KEYCODE_SPACE) && ship.active)
    {
        if (sound & !paused) {
            fireSound.play();
        }

        photonTime = System.currentTimeMillis();
        photonIndex++;
if (photonIndex >= Constants.MAX_SHOTS)
            photonIndex = 0;

        photons[photonIndex].active = true;
        photons[photonIndex].x = ship.x;
        photons[photonIndex].y = ship.y;
        photons[photonIndex].deltaX = 2 * Constants.MAX_ROCK_SPEED
                * -Math.sin(ship.angle);
        photons[photonIndex].deltaY = 2 * Constants.MAX_ROCK_SPEED
                * Math.cos(ship.angle);
    }

    /**
     * 'H' key: warp ship into hyperspace by moving to a random location and
     * starting counter. Note: keys are case independent
     */
    if (keyCode == KeyEvent.KEYCODE_H && ship.active && hyperCounter <= 0) {
        ship.x = (int) (Math.random() * getWidth());
        ship.y = (int) (Math.random() * getHeight());
        hyperCounter = Constants.HYPER_COUNT;

        if (sound & !paused)
            warpSound.play();
    }

    /**
     * 'P' key: toggle pause mode and start or stop any active looping sound
     * clips.
     */
    if (keyCode == KeyEvent.KEYCODE_P) {
        if (paused) {
            if (sound && misslePlaying)
                missileSound.loop();
            if (sound && saucerPlaying)
                saucerSound.loop();
            if (sound && thrustersPlaying)
                thrustersSound.loop();
        } else {
            if (misslePlaying)
                missileSound.stop();
            if (saucerPlaying)
                saucerSound.stop();
            if (thrustersPlaying)
                thrustersSound.stop();
        }
        paused = !paused;
    }

    /**
     * 'M' key: toggle sound on or off and stop any looping sound clips.
     */
if (keyCode == KeyEvent.KEYCODE_M && loaded) {
        if (sound) {
            crashSound.stop();
            explosionSound.stop();
            fireSound.stop();
            missileSound.stop();
            saucerSound.stop();
            thrustersSound.stop();
            warpSound.stop();
        } else {
            if (misslePlaying && !paused)
                missileSound.loop();
            if (saucerPlaying && !paused)
                saucerSound.loop();
            if (thrustersPlaying && !paused)
                thrustersSound.loop();
        }
        sound = !sound;
    }

    /**
     * 'S' key: start the game, if not already in progress.
     */
    if (keyCode == KeyEvent.KEYCODE_S && loaded && !playing) {
        initGame();
    }

    /**
     * 'E' Exit game
     */
    if (keyCode == KeyEvent.KEYCODE_E ) {
        stopUpdateTimer();
        releaseSounds();

        System.exit(0); // Ouch!
    }
}

Our arcade game is now complete, and we can run it in the emulator.

Testing Asteroids on the Emulator

Let's fire the emulator and play some Asteroids! Here is how:

  1. Create a new launch configuration. From the Eclipse main menu, click Run

    Testing Asteroids on the Emulator
  2. Enter a configuration name, Asteroids.

  3. Select the project, ch04.Asteroids.

  4. Set the Launch Action as Launch Default Activity, and click Run.

Stand by for the emulator window. You should see the game starting with the message Game Over and some asteroids floating around the screen. Press S to start the game and try some of the game's features (see Figure 4-5):

  • Move the ship around using the arrow keys. You should see the thrusters fire and hear the thrusters' sound. The ship should wrap around when it goes off the screen.

  • Press the space bar to fire the gun. Make sure the photons display (and that the sound plays) and the asteroids are destroyed when hit.

  • Press the H to jump into hyperspace. The ship should disappear and reappear in a random location. Make sure the sound effect is played.

  • Try to get a high score, and make sure the UFO shows up on screen and fires the missile against you.

  • Press E to terminate the game when you get bored.

The Asteroids game in action

Figure 4-5. The Asteroids game in action

What's Next?

In this chapter, we looked at the polygon-based game Asteroids. This game presented new challenges due to the lack of polygon support in the Android API. To overcome this limitation, we created two helper classes: Rectangle and Polygon. This code has been ported from the Java SE API and slightly modified for this game. You also learned how to build a PolygonSprite capable of remembering X and Y coordinates and an angle of rotation. PolygonSprite is also capable of detecting collisions with other PolygonSprites and drawing itself in the Android canvas.

With these classes, we have built the arcade classic Asteroids. Furthermore, we have looked at the game's inner workings such as a user-defined XML layout and manipulation of game resources such as audio files and icons.

You have also taken a look at the critical steps in the game life cycle: initialization, drawing, and updating physics, as well as the caveats of drawing the polygons in the Android canvas. Finally, you learned at how to process key and touch events, and you tested Asteroids in the emulator.

I hope that you have learned new interesting techniques in building pure Java games. In the following chapters, I switch gears to concentrate in hybrid games, which mix Java and C thru JNI for maximum performance. We'll begin with the always-interesting subject of OpenGL.

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

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