This tutorial game project introduces the concept of a viewport. This is the aspect of our game which handles which part of the game-world is drawn to the screen. First, we need to decide what to draw and then we must convert their “real-world” coordinates to the screen coordinates at which to draw them. If we accept that most games we make will have areas which cannot possibly be squeezed onto the player’s phone or tablet, in one go, then a viewport is essential. If you write games using OpenGL then this viewport idea is handled for you by OpenGL. However, the code for implementing a simple viewport is much simpler than handling the additional overhead required to get started with OpenGL. Therefore, it is often worthwhile building your own viewport.

  • The courses above are up to 95% off - only $19 using voucher code APRUDEMY17. Limited time offer.

About this project

Skill level 1
Time to complete 1 hour

New Concepts:

  1. A viewport to control where the game “camera” is looking
  2. Scrolling game-worlds that are bigger than the player’s screen (i.e. most games you will make)
  3. A look at all the other exciting things you can do with a viewport
  4. Build a simple (but more advanced than previous projects) control pad
  5. Introduction to the problem of a low frame rate

Projects that demonstrate these concepts

Before we get started with the project let’s dig a little deeper into this viewport idea.

What exactly is a viewport?

You can think of a viewport more intuitively if you think about it as a camera which moves to keep the most relevant part of the game-world in focus. The most relevant part, in most games, is the area around the player. Therefore, most viewports track the player as its center. This is what we will do in this project. It is more than this, however, because it can also be used to clip, remove from processing, certain game objects that we deem unnecessary to update in a given frame. This is something OpenGL doesn’t do.

About the game

The game will be fully runnable and playable code but won’t be a fully-structured game. You can rotate and thrust your spaceship above and around a randomly generated city full of skyscrapers. You can shoot your rapid fire gun at the skyscrapers and destroy/set fire to them. There isn’t any objective to the game or menu screen. The code was too long and sprawling to add all these features. If you have read the other Android game projects, you can easily add your own enemies etc.

The game world will be “fenced in” by a crude barrier because I couldn’t think of anything more exciting that is also very simple. The game won’t actually have any enemies or score etc, just the scrolling world, burning buildings etc. The main point was to demonstrate the viewport.

Just for fun, we will construct all the buildings with tiny “bricks”. This will demonstrate two things. Firstly, the viewport implementation can not only handle hundreds of objects but, actually, is more efficient the higher the number of game objects. The fun comes because we can destroy and set fire to, individual sections of our buildings. This will allow the player’s spaceship to shoot and even fly through damaged buildings. In this project, we will, however, see that drawing and processing will eventually begin to slow down our game loop. We will talk about solutions when we have built the game.

Starting and planning 2D scrolling shooter project

Create a new project in Android Studio, use the Empty Activity template, and call it Scrolling Shooter 2D. Leave the rest of the settings at their defaults.

 

First I will show you the Viewport class and go into some detail because that is the class that does all the cool stuff.

Next, we will quickly see the code for all the game object classes (Ship, Bullet, Brick and Star)so you can copy & paste it. I won’t go into much depth because there is little new from previous projects.

Then we will quickly code MainActivity. The last thing we will code is the GameView class which draws, updates and uses the Viewport. Much of this code I will just leave comments to remind you what it does but I will go into the ins and outs of the control buttons(HUD) and, of course, the viewport bits.

Finally, in this article, we will discuss more exciting things you can do with a viewport and take a look at what would be a good way to develop your Android game development from here.

Coding the Viewport class

As we have already discussed, a viewport can be thought of as the movie camera that follows the action of our game. It defines the area of the game world that is to be shown to the player. Ours will center on the triangle-shaped spaceship.

It also serves the combined function of making our update method more efficient by flagging objects outside of the player’s field of vision(the viewport). There is no point processing a mountain of Brick instances each frame if they are not relevant in that frame. Of course, if your game wants the player to shoot and destroy things that are off-screen then you have to make a tactical decision about what to exclude. The point is, the viewport gives us the choice. This will significantly speed up tasks like collision detection by implementing the first phase of detection by removing objects off-screen from the list of objects to check for collisions.


Furthermore, our Viewport class will have the task of translating game world coordinates into appropriate pixel coordinates for drawing on the screen. The Viewport class really is an all singing and dancing thing. So let’s get coding.

First, we will declare a whole bunch of useful variables. We have a PointF which will just be used to represent whatever point in the world is currently the central focus in the viewport. Then we have separate int values for pixelsPerMetreX and pixelsPerMetreY.

This point is key! Every game object will have dimensions and a position. Unlike every other Android project(on this site) that came before this one, these dimensions and positions no longer relate to a coordinate on the screen but instead, now relate to a coordinate in the world! That is why we have all these variables with names containing ...metres... etc. Of course, if you sit back and work out the size in meters of any of the game objects, they are tiny compared to say a real skyscraper. You could change the units and therefore the variable names to something like pixelsPerWorldUnit, . I thought meters made the distinction between the game-world we are simulating and the screens pixels more clear and distinct.

In addition, we also have the resolution of the screen in both axes. The variables   screenXResolution and screenYResolution are passed in when the constructor is called. We then have screenCentreX and screenCentreY which are basically the two previous variables divided by 2 to find the middle. Next in our list of declared variables we have metresToShowX and metresToShowY which will be the number of meters we will squash into our viewport. Changing these values will show more or less of the game world on screen.

Create a new class called Viewport and declare the variables and code the constructor.

package com.gamecodeschool.scrollingshooter2d;

import android.graphics.PointF;
import android.graphics.RectF;

public class Viewport {

    private PointF currentViewportWorldCentre;
    private RectF convertedRect;
    private PointF convertedPoint;
    private int pixelsPerMetreX;
    private int pixelsPerMetreY;
    private int screenCentreX;
    private int screenCentreY;
    private int metresToShowX;
    private int metresToShowY;

	// All the rest of the Viewport code goes here

Viewport(int screenXResolution, int screenYResolution){

	screenCentreX = screenXResolution / 2;
	screenCentreY = screenYResolution / 2;

	pixelsPerMetreX = screenXResolution / 90;
	pixelsPerMetreY = screenYResolution / 55;

	metresToShowX = 92;
	metresToShowY = 57;

	convertedRect = new RectF();
	convertedPoint = new PointF();

	currentViewportWorldCentre = new PointF();

	// End of viewport class
}

Note that in a more advanced implementation you could dynamically choose metresToShow... values based on things like resolution, screen size, and ratio of width to height of the screen. In addition, the amount you divide screenResolutionX and ...Y by determines the number of virtual meters displayed on the screen. Adjusting this to get good results on your screen might be necessary and setting them up dynamically based on the specifications of the screen would be appropriate in a real game.

Next, we have a simple method setWorldCentre which does as the name suggests. The value held in this PointF each frame will be significant in every single calculation the Viewport class makes. Our game loop will call it each and every frame(at the beginning) based on the position of the spaceship. Add the setWorldCentre method.

  • The courses above are up to 95% off - only $19 using voucher code APRUDEMY17. Limited time offer.

void setWorldCentre(float x, float y){
	currentViewportWorldCentre.x  = x;
	currentViewportWorldCentre.y  = y;
}

And now we fulfill one of the primary roles of the Viewport class with the worldToScreen method. As the name suggests this is the method that converts the locations of all the objects currently in the visible viewport from world coordinates to pixel coordinates that can actually be drawn to the screen. It returns our previously prepared rectToDraw object as the result.

This is how worldToScreen works. It receives the horizontal and vertical world locations of an object along with that object’s width and height. With these values, each in turn, it subtracts the objects world coordinate multiplied by the pixels per meter for the current screen from the appropriate current world viewport center (x or y) . Then, for the left and top coordinates of the object, the result is subtracted from the pixel screen center value and for the bottom and right coordinates it is added. These values are then packed into the correct variable ( left, top, right and bottom ) of convertedRect and returned to the draw method of GameView. This simple calculation gives the screen pixel coordinates to draw the object.

Note that the code has not only calculated the screen coordinates but has also scaled the object in size(up or down as required).

Add the worldToScreen method to the Viewport class.

public RectF worldToScreen(float objectX, float objectY, float objectWidth, float objectHeight){
	int left = (int) (screenCentreX - ((currentViewportWorldCentre.x - objectX) * pixelsPerMetreX));
	int top =  (int) (screenCentreY - ((currentViewportWorldCentre.y - objectY) * pixelsPerMetreY));
	int right = (int) (left + (objectWidth * pixelsPerMetreX));
	int bottom = (int) (top + (objectHeight * pixelsPerMetreY));
	convertedRect.set(left, top, right, bottom);
	return convertedRect;
}

This next method does exactly the same as the last but with a single point. The spaceship points will be converted using this method. If you’re wondering how the scaling would work on a single point then consider that we will be drawing lines between the three points in the draw method. Therefore, just by moving the points into position the scaling has occurred.

public PointF worldToScreenPoint(float objectX, float objectY){
	int left = (int) (screenCentreX - ((currentViewportWorldCentre.x - objectX) * pixelsPerMetreX));
	int top =  (int) (screenCentreY - ((currentViewportWorldCentre.y - objectY) * pixelsPerMetreY));

	convertedPoint.x = left;
	convertedPoint.y = top;
	return convertedPoint;
}

This next method examines each point of a rectangle (in our case a Brick instance) and if its world position is far enough away (in metres, not pixels) from the centre of the viewport true is returned and the object being acted upon will clip itself internally, thereby excluding itself from the next round of update calls. We will see the simple way this occurs when we code the game loop.

public boolean clipObjects(float objectX,
	float objectY,
	float objectWidth,
	float objectHeight) {

	boolean clipped = true;

	if (objectX - objectWidth < currentViewportWorldCentre.x + (metresToShowX / 2)) { 		if (objectX + objectWidth > currentViewportWorldCentre.x - (metresToShowX / 2)) {
			if (objectY - objectHeight < currentViewportWorldCentre.y + (metresToShowY / 2)) { 				if (objectY + objectHeight > currentViewportWorldCentre.y - (metresToShowY / 2)){
					clipped = false;
				}

			}
		}
	}

	return clipped;
}

More fun than coding the class is seeing it in action.

Coding the game object classes

These next classes are only described briefly because they are well commented and if you have completed the prerequisite projects and tutorials you will already understand much of the code.

Ship

Create a new class called Ship and add the following code. Be sure to read the comments as the code is not discussed. Almost the exact same code is fully explained in the series on calculating heading and the project, Android rotation and heading demo.

package com.gamecodeschool.scrollingshooter2d;

import android.content.Context;
import android.graphics.PointF;

public class Ship {

    PointF a;
    PointF b;
    PointF c;
    PointF centre;

    /*
    Which way is the ship facing
    Straight up to start with
    */

    float facingAngle = 270;

    // How long will our spaceship be
    //private float length;
    //private float width;

    // This will hold the pixels per second speed that the ship can move at
    private float speed = 0;
    //private final float MAX_SPEED = 80;
    //private final float ACCELERATION_RATE = 40;
    //private final float BREAK_RATE = 30;

    /*
    These next two variables control the actual movement rate per frame
    their values are set each frame based on speed and heading
    */

    private float horizontalVelocity;
    private float verticalVelocity;

    /*
    How fast does the ship rotate?
    1 complete circle per second
    */

    //private final float ROTATION_SPEED = 200;

    // Which ways can the ship move
    public final int STOPPING = 0;
    public final int LEFT = 1;
    public final int RIGHT = 2;
    public final int THRUSTING = 3;

    // Is the ship moving and in which direction
    private int shipMoving = STOPPING;

    /*
    This the the constructor method
    When we create an object from this class we will pass
    in the screen width and height
    */

    //public Ship(Context context, int screenX, int screenY){

    public Ship(){

        //length = screenX / 25;
        //width = screenY / 25;

        float length = 2.5f;
        float width = 1.25f;

        a = new PointF();
        b = new PointF();
        c = new PointF();
        centre = new PointF();

        centre.x = 50;
        centre.y = 50;

        a.x = centre.x;
        a.y = centre.y - length / 2;

        b.x = centre.x - width / 2;
        b.y = centre.y + length / 2;

        c.x = centre.x + width / 2;
        c.y = centre.y + length / 2;

    }

    public PointF getCentre(){
        return  centre;
    }

    public PointF getA(){
        return  a;
    }

    public PointF getB(){
        return  b;
    }

    public PointF getC(){
        return  c;
    }

    float getFacingAngle(){
        return facingAngle;
    }

    public void bump(){

        speed = 0;

        // Move back
        centre.x = centre.x - horizontalVelocity * 2;
        centre.y = centre.y - verticalVelocity * 2;

        a.x = a.x - horizontalVelocity * 2;
        a.y = a.y - verticalVelocity * 2;

        b.x = b.x - horizontalVelocity * 2;
        b.y = b.y - verticalVelocity * 2;

        c.x = c.x - horizontalVelocity * 2;
        c.y = c.y - verticalVelocity * 2;

    }

    /*
    This method will be used to change/set if the
    ship is rotating left, right or thrusting
    */

    public void setMovementState(int state){
        shipMoving = state;
    }

    /*
    This update method will be called from update in HeadingAndRotationView
    It determines if the player ship needs to move and changes the coordinates
    and rotation when necessary.
    */

    public void update(long fps){

        final float ROTATION_SPEED = 200;
        final float BREAK_RATE = 30;

        /*
        Where are we facing at the moment
        Then when we rotate we can work out
        by how much
        */

        float previousFA = facingAngle;

        if(shipMoving == LEFT){

            facingAngle = facingAngle - ROTATION_SPEED / fps;

            if(facingAngle < 1){                 facingAngle = 360;             }         }         if(shipMoving == RIGHT){             facingAngle = facingAngle + ROTATION_SPEED / fps;             if(facingAngle > 360){
                facingAngle = 1;
            }
        }

        if(shipMoving == THRUSTING){

            final float MAX_SPEED = 80;
            final float ACCELERATION_RATE = 40;

            /*
            facingAngle can be any angle between 1 and 360 degrees
            the Math.toRadians method simply converts the more conventional
            degree measurements to radians which are required by
            the cos and sin methods.
            */

            if(speed < MAX_SPEED){                 speed = speed + ACCELERATION_RATE / fps;             }             horizontalVelocity = (float)(Math.cos(Math.toRadians(facingAngle)));             verticalVelocity = (float)(Math.sin(Math.toRadians(facingAngle)));         }         centre.x = centre.x + horizontalVelocity * speed / fps;         centre.y = centre.y + verticalVelocity * speed / fps;         a.x = a.x + horizontalVelocity * speed / fps;         a.y = a.y + verticalVelocity * speed / fps;         b.x = b.x + horizontalVelocity * speed / fps;         b.y = b.y + verticalVelocity * speed / fps;         c.x = c.x + horizontalVelocity * speed / fps;         c.y = c.y + verticalVelocity * speed / fps;         if(shipMoving != THRUSTING){             if(speed > 0) {
                speed = speed - (BREAK_RATE / fps);
            }
        }

        /*
        Now rotate each of the three points by
        the change in rotation this frame
        facingAngle - previousFA
        */

        float tempX;
        float tempY;

        // rotate point a
        a.x = a.x - centre.x;
        a.y = a.y - centre.y;

        tempX = (float)(a.x * Math.cos(Math.toRadians(facingAngle - previousFA)) -
                a.y * Math.sin(Math.toRadians(facingAngle - previousFA)));

        tempY = (float)(a.x * Math.sin(Math.toRadians(facingAngle - previousFA)) +
                a.y * Math.cos(Math.toRadians(facingAngle - previousFA)));

        a.x = tempX + centre.x;
        a.y = tempY + centre.y;

        // rotate point b
        b.x = b.x - centre.x;
        b.y = b.y - centre.y;

        tempX = (float)(b.x * Math.cos(Math.toRadians(facingAngle - previousFA)) -
                b.y * Math.sin(Math.toRadians(facingAngle - previousFA)));

        tempY = (float)(b.x * Math.sin(Math.toRadians(facingAngle - previousFA)) +
                b.y * Math.cos(Math.toRadians(facingAngle - previousFA)));

        b.x = tempX + centre.x;
        b.y = tempY + centre.y;

        // rotate point c
        c.x = c.x - centre.x;
        c.y = c.y - centre.y;

        tempX = (float)(c.x * Math.cos(Math.toRadians(facingAngle - previousFA)) -
                c.y * Math.sin(Math.toRadians(facingAngle - previousFA)));

        tempY = (float)(c.x * Math.sin(Math.toRadians(facingAngle - previousFA)) +
                c.y * Math.cos(Math.toRadians(facingAngle - previousFA)));

        c.x = tempX + centre.x;
        c.y = tempY + centre.y;

    }
}

Bullet

Create a new class called Bullet and add the following code.

package com.gamecodeschool.scrollingshooter2d;

import android.graphics.PointF;

public class Bullet {

    private PointF point;

    /*
    These next two variables control the actual movement rate per frame
    their values are set each frame based on speed and heading
    */

    private float horizontalVelocity;
    private float verticalVelocity;
    float speed = 60;

    // Is the bullet currently in action?
    private boolean isActive;

    public Bullet() {

        // Inactive until fired
        isActive = false;

        // World coordinates contained in a point
        point = new PointF();
    }

    public boolean shoot(float startX, float startY, float direction) {
        if (!isActive) {
            point.x = startX;
            point.y = startY;

            // How much to move horizontally, each frame
            horizontalVelocity =
                    (float)(Math.cos(Math.toRadians(direction)));

            // How much to move vertically, each frame
            verticalVelocity =
                    (float)(Math.sin(Math.toRadians(direction)));

            isActive = true;
            return true;
        }

        // Bullet already active
        return false;
    }

    public void update(long fps){
        // Move the bullet
        point.x = point.x + horizontalVelocity * speed / fps;
        point.y = point.y + verticalVelocity * speed / fps;
    }

    public PointF getPoint(){
        return  point;
    }

    public boolean getStatus(){
        return isActive;
    }

    public void setInactive(){
        isActive = false;
    }

}

The Bullet class is even simpler than the Space Invaders’ Bullet class. The constructor sets it up and the update method moves the bullet each frame. The class also has a method so that the main game loop can tell whether or not a particular instance is currently active. If the math in the shoot method looks a bit weird then it is explained in the tutorial series that starts with lCalculating heading.

Star

In Android Studio, create a new class called Star and add the following code. All this class does is hold a single point which is randomly generated. The update method then infrequently and randomly switches on and off which creates a twinkling effect when viewed with hundreds of Star instances.

package com.gamecodeschool.scrollingshooter2d;

import java.util.Random;

public class Star{

    private int x;
    private int y;

    private boolean isVisible = true;

    // Declare a random object here because
    // we will use it in the update() method
    // and we don't want the garbage collector to have to keep clearing it up
    Random random;

    public Star(int mapWidth, int mapHeight){
        random = new Random();
        x= random.nextInt(mapWidth);
        y = random.nextInt(mapHeight);

    }

    public int getX(){
        return x;
    }

    public int getY(){
        return y;
    }

    public void update(){

        // Randomly twinkle the stars
        int n = random.nextInt(5000);
        if(n == 0){
            // Switch on or off
            isVisible = !isVisible;
        }

    }

    public boolean getVisibility(){
        return isVisible;
    }

}

Brick

Add a class called Brick to the project. Brick is simple but there are some things in here which other tutorials(on this site) haven’t covered. In the constructor, as well as positioning the current brick relative to the previous brick and deciding whether it is an edge brick and therefore needs an edge line drawn, an alpha level is randomly generated. This has the effect of some bricks looking like they are lit windows. The varying alpha makes different brightnesses of light. The update method infrequently and randomly varies these values further looking like lights are being turned on and off.

What is most interesting in the context of this project is that the class holds a variable called clipped, which we will see working in conjunction with the Viewport class. When a Brick instance is clipped, there is no need to process it each frame or attempt to draw it either.

package com.gamecodeschool.scrollingshooter2d;

import android.graphics.Color;
import android.graphics.RectF;
import java.util.Random;

public class Brick {

    private RectF rect;
    Random random = new Random();

    //private boolean isVisible;
    boolean destroyed;

    private boolean isRight;
    private boolean isLeft;
    private boolean isTop;
    private int color;
    private boolean clipped;

    public Brick(int columnNum, int rowNum,
                 boolean isLeft, boolean isRight,
                 boolean isTop){

        int brickWidth = 1;
        int brickHeight = 1;

        this.isLeft = isLeft;
        this.isRight = isRight;
        this.isTop = isTop;

        float brickXPadding = .0f;
        float brickYPadding = .0f;

        rect = new RectF(
            (columnNum * (brickXPadding  + brickWidth +
                    brickXPadding) + brickXPadding),
            ((rowNum * (brickYPadding + brickHeight +
                    brickYPadding) ) + brickYPadding),
            (columnNum * (brickXPadding  + brickWidth +
                    brickXPadding) + brickXPadding + brickWidth),
            ((rowNum * (brickYPadding + brickHeight +
                    brickYPadding) ) + brickYPadding + brickHeight)
        );

        // Assign a color
        if(random.nextInt(9) == 0){
            // Vary the alpha for effect
            int alpha = random.nextInt(256);
            color = Color.argb(alpha, 255, 255, 0);
        }else{
            color = Color.argb(255, 0, 0, 0);
        }
    }

    public void update(){

        // Assign a color
        if(!destroyed){// flicker
            if (random.nextInt(6000) == 0) {
                if (random.nextInt(9) == 0) {
                    // Vary the alpha for effect
                    int alpha = random.nextInt(256);
                    color = Color.argb(alpha, 255, 255, 0);
                } else {
                    color = Color.argb(255, 0, 0, 0);
                }
            }
        }else{// fire
            int whichColor = random.nextInt(3);
            switch (whichColor){
                case 0:
                    color = Color.argb(255, 255, 0, 0);
                    break;

                case 1:
                    color = Color.argb(255, 245, 143, 10);
                    break;

                case 2:
                    color = Color.argb(255, 250, 250, 10);
                    break;
            }
        }
    }

    public void destroy(){
        destroyed = true;
    }

    public boolean isDestroyed(){ return destroyed; }

    public void clip(){ clipped = true; }

    public void unClip(){ clipped = false; }

    public boolean isClipped(){ return clipped; }

    public RectF getRect(){
        return this.rect;
    }

    public boolean getLeft(){
        return isLeft;
    }

    public int getColor(){
        return color;
    }

    public boolean getRight(){
        return isRight;
    }

    public boolean getTop(){
        return isTop;
    }
}

 Coding the MainActivity

Simply edit your MainActivity class to look like this code. There is nothing new here. If anything looks unusual check back to the Breakout or SpaceInvaders  projects. If you have done any Android projects on this site before then this code will look very familiar. It simply creates an  GameView instance and passes the screen resolution to its constructor. The onPause and onResume methods call corresponding methods from the GameView instance to start and stop the thread that runs the main game loop.

package com.gamecodeschool.scrollingshooter2d;

import android.app.Activity;
import android.graphics.Point;
import android.os.Bundle;
import android.view.Display;

public class MainActivity extends Activity {

    // View will be the view of the game
    // It will also hold the logic of the game
    // and respond to screen touches as well
    GameView view;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Get a Display object to access screen details
        Display display = getWindowManager().
                getDefaultDisplay();

        // Load the resolution into a Point object
        Point size = new Point();
        display.getSize(size);

        // Initialize gameView and set it as the view
        view = new GameView(this, size.x, size.y);
        setContentView(view);
    }

    // This method executes when the player starts the game
    @Override
    protected void onResume() {
        super.onResume();

        // Tell the gameView resume method to execute
        view.resume();
    }

    // This method executes when the player quits the game
    @Override
    protected void onPause() {
        super.onPause();

        // Tell the gameView pause method to execute
        view.pause();
    }
}

 

Coding the GameView

This is the longest and most complicated class. I have heavily commented it. If I don’t explain something, that is because the details were given in one of the prerequisite projects. The list of these projects is at the top of the page. I will go into detail when we use the Viewport class and when we code the HUD inner class.

The code that follows is quite lengthy but follows in order. Note that I have broken it up into chunks with a bit of discussion and I don’t always show the full level of indentation, to make the code more readable.

Create a new class called GameView. First, the class declaration, into which all the rest of the code that follows will go.

package com.gamecodeschool.scrollingshooter2d;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.media.AudioManager;
import android.media.SoundPool;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;

public class GameView extends SurfaceView implements Runnable {

    // A handy Random instance
    Random random = new Random();

    // For sound FX
    private SoundPool soundPool;
    private int shootID = -1;
    private int damageBuildingID = -1;

    // This is an instance of an inner class
    // See end of this class for code
    HUD hud;

    // How big is the world?
    // Change these for lots of fun...
    // And a slow game
    int worldWidth = 0;
    int targetWorldWidth = 500;
    int targetWorldHeight = 150;
    int groundLevel = 145;

    // For the returned rectangle from the viewport
    // These we will use in the main game loop, multiple times
    // Saves creating new instances each frame
    RectF convertedRect = new RectF();
    PointF convertedPointA = new PointF();
    PointF convertedPointB = new PointF();
    PointF convertedPointC = new PointF();
    PointF tempPointF = new PointF();

    // This is our thread
    private Thread gameThread = null;

    // Our SurfaceHolder to lock the surface before we draw our graphics
    private SurfaceHolder ourHolder;

    // A boolean which we will set and unset
    // when the game is running- or not.
    private volatile boolean playing;

    // Game is paused at the start
    private boolean paused = true;

    // A Canvas and a Paint object
    private Canvas canvas;
    private Paint paint;

    // This variable tracks the game frame rate
    private long fps;

    // The city is built from bricks
    private Brick[] bricks = new Brick[20000];
    private int numBricks;

    // Twinkling stars in the sky above the city
    private Star[] stars = new Star[5000];
    private int numStars;

    // The player's ship
    Ship player;

    // The player's bullets
    private Bullet[] playerBullets = new Bullet[10];
    private int nextPlayerBullet;
    private int maxPlayerBullets = 10;

    // Our neat viewport/camera/clipping machine
    Viewport vp;

	// All code including HUD inner class goes here

	// End of GameView
}

The previous code declares all the game objects, Stars, Bricks and a Ship called player. Notice also we declare an instance of Viewport called vp.

// The constructor
public GameView(Context context, int screenX, int screenY) {

	// The next line of code asks the
	// SurfaceView class to set up our object.
	// How kind.
	super(context);

	// Initialize ourHolder and paint objects
	ourHolder = getHolder();
	paint = new Paint();

	// Initialize the Viewport
	vp = new Viewport(screenX, screenY);

	hud = new HUD(screenX, screenY);

	// This SoundPool is deprecated but don't worry
	soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC,0);

	try{
		// Create objects of the 2 required classes
		AssetManager assetManager = context.getAssets();
		AssetFileDescriptor descriptor;

		// Load our fx in memory ready for use
		descriptor = assetManager.openFd("shoot.ogg");
		shootID = soundPool.load(descriptor, 0);

		descriptor = assetManager.openFd("damagebuilding.ogg");
		damageBuildingID = soundPool.load(descriptor, 0);

	}catch(IOException e){
		// Print an error message to the console
		Log.e("error", "failed to load sound files");
	}

	prepareLevel();
}

The previous code is the GameView constructor. It sets up the Paint and Holder objects ready for drawing. It also gets the sound effects ready to play. In this code, we also call the vp constructor passing in the screen resolution as is required. The code also creates a new HUD instance, again passing in the screen resolution. We will see the HUD inner class soon. “Release your inner HUD”. Too much? Here is the prepareLevel code.

private void prepareLevel() {

	player = new Ship();

	// Initialize the playerBullets array
	for (int i = 0; i < playerBullets.length; i++) {
		playerBullets[i] = new Bullet();
	}

	//Reset the players location as the world centre of the viewport
	//if game is playing
	vp.setWorldCentre(player.getCentre().x, player.getCentre().y);

	Random random = new Random();
	int gapFromLastBuilding;
	int maxGap = 25;
	int buildingWidth;
	int maxBuildingWidth = 10;
	int buildingHeight;
	int maxBuildingHeight = 85;

	for (worldWidth = 0;
		 worldWidth < targetWorldWidth;
		 worldWidth += buildingWidth + gapFromLastBuilding) {

		buildingWidth = random.nextInt(maxBuildingWidth) + 3;
		buildingHeight = random.nextInt(maxBuildingHeight) + 1;
		gapFromLastBuilding = random.nextInt(maxGap) + 1;

		for (int x = 0; x < buildingWidth; x++) { 			for (int y = groundLevel; y > groundLevel - buildingHeight; y--) {

				boolean isLeft = false;
				boolean isRight = false;
				boolean isTop = false;

				// Is this brick on left, right or top?
				if (x == 0) {
					isLeft = true;
				}
				if (x == buildingWidth - 1) {
					isRight = true;
				}
				if (y == (groundLevel - buildingHeight) + 1) {
					isTop = true;
				}

				bricks[numBricks] = new Brick(x + worldWidth, y,
						isLeft, isRight, isTop);

				numBricks++;
			}
		}
	}

	// Instantiate some stars
	for (int i = 0; i < 500; i++) {

		stars[i] = new Star(targetWorldWidth, targetWorldHeight);
		numStars++;
	}

}

In the prepareLevel method, most of the code initializes the Brick instances. Also notice, however, we call the setWorldCentre method on our vp object and pass in the center of the player ship. The Viewport instance is now fully configured and ready to convert all world coordinates to screen coordinates, relative to the center of the player’s spaceship.

@Override
public void run() {
	while (playing) {

		// Capture the current time in milliseconds in startFrameTime
		long startFrameTime = System.currentTimeMillis();

		// Update the frame
		if (!paused) {
			update();
		}

		// Draw the frame
		draw();

		// Calculate the fps this frame
		// We can then use the result to
		// time animations and more.
		long timeThisFrame = System.currentTimeMillis() - startFrameTime;
		if (timeThisFrame >= 1) {
			fps = 1000 / timeThisFrame;
		}
	}
}

The run method above simply calls update and draw while keeping track of the frames per second.

The next method  update is quite huge(really it should be split up a bit). I encourage you to read the code thoroughly paying special attention to the use of vp.clipObjects which sets any Brick instances that are outside the viewport to clipped = true. Notice that the way we handle collision detection is unaffected by the fact we are using a viewport. If two objects collide in the game world it doesn’t matter if they are at x one million or x 10 – they collide. The viewport has not complicated this task in any way except that we only update and check for collisions on unclipped bricks.

private void update() {

	//Reset the players location as the world centre of the viewport
	//if game is playing
	vp.setWorldCentre(player.getCentre().x, player.getCentre().y);

	// Clip all off screen bricks
	for (int i = 0; i < numBricks; i++) {
		if (vp.clipObjects(bricks[i].getRect().left,
				bricks[i].getRect().top, 1, 1)) {

			bricks[i].clip();
		} else {
			bricks[i].unClip();
		}
	}

	player.update(fps);

	// Update all the player bullets if active
	for (int i = 0; i < playerBullets.length; i++) {
		if (playerBullets[i].getStatus()) {
			playerBullets[i].update(fps);
		}
	}

	// Have the player's bullets hit a building?
	// Update all the player bullets if active
	for (int i = 0; i < maxPlayerBullets; i++) {
		if (playerBullets[i].getStatus()) {
			for (int j = 0; j < numBricks; j++) {
				if (!bricks[j].isClipped()) {

					// Only process this brick if not destroyed
					if(!bricks[j].isDestroyed()) {
						if (bricks[j].getRect().contains(
								playerBullets[i].getPoint().x,
								playerBullets[i].getPoint().y)) {

							playerBullets[i].setInactive();
							soundPool.play(damageBuildingID, 1, 1, 0, 0, 1);
							bricks[j].destroy();

							int chainReactionSize = random.nextInt(6);
							for(int k = 1; k < chainReactionSize; k++){
								bricks[j+k].destroy();
								bricks[j-k].destroy();
							}

						}
					}

				}
			}
		}
	}

	// set bullets inactive when they go out of view
	// Clip all off screen bricks
	for (int i = 0; i < maxPlayerBullets; i++) {
		if (playerBullets[i].getStatus()) {

			if(playerBullets[i].getPoint().x < 0){ 				playerBullets[i].setInactive(); 			} 			else if(playerBullets[i].getPoint().x > targetWorldWidth){
				playerBullets[i].setInactive();
			}

			else if(playerBullets[i].getPoint().y < 0){ 				playerBullets[i].setInactive(); 			} 			else if(playerBullets[i].getPoint().y > targetWorldHeight){
				playerBullets[i].setInactive();
			}
		}
	}

	// Update the stars
	for (int i = 0; i < numStars; i++) {
		stars[i].update();
	}

	// Update the bricks
	for (int i = 0; i < numBricks; i++) {
		if (!bricks[i].isClipped()) {
			bricks[i].update();
		}
	}

	// Has the ship collided with a top, left or right brick?
	// that isn't destroyed
	for (int i = 0; i < numBricks; i++) { 		if (!bricks[i].isClipped() && !bricks[i].isDestroyed()) { 			if (bricks[i].getRect().contains(player.getA().x, player.getA().y) || 					bricks[i].getRect().contains(player.getB().x, player.getB().y) || 					bricks[i].getRect().contains(player.getC().x, player.getC().y)) { 				player.bump(); 			} 		} 	} 	// Has the ship collided with the floor? 	if (player.getA().y > groundLevel ||
			player.getB().y > groundLevel ||
			player.getC().y > groundLevel) {

		player.bump();
	}

	// Has the ship collided with the game world ceiling?
	if (player.getA().y < 0 ||
			player.getB().y < 0 ||
			player.getC().y < 0) {

		player.bump();
	}

	// Has the ship collided with the game world left?
	if (player.getA().x < 0 ||
			player.getB().x < 0 ||
			player.getC().x < 0) { 		player.bump(); 	} 	// Has the ship collided with the game world right? 	if (player.getA().x > worldWidth ||
			player.getB().x > worldWidth ||
			player.getC().x > worldWidth) {

		player.bump();
	}

}

This next method called  draw is where the Viewport class does its most interesting work and calls worldToScreen and worldToScreenPoint.

private void draw() {
	// Make sure our drawing surface is valid or we crash
	if (ourHolder.getSurface().isValid()) {
		// Lock the canvas ready to draw
		canvas = ourHolder.lockCanvas();

		// Draw the background color
		//canvas.drawColor(Color.argb(255, 87, 73, 122));
		canvas.drawColor(Color.argb(255, 0, 0, 0));

		// Draw the game world ceiling
		paint.setColor(Color.argb(255, 255, 255, 255));
		convertedRect = vp.worldToScreen(
				0,
				0,
				targetWorldWidth,
				1
		);

		canvas.drawRect(convertedRect, paint);

		// Draw the left game world barrier
		convertedRect = vp.worldToScreen(
				0,
				0,
				1,
				targetWorldHeight
		);

		canvas.drawRect(convertedRect, paint);

		// Draw the right game world barrier
		convertedRect = vp.worldToScreen(
				targetWorldWidth,
				0,
				1,
				targetWorldHeight
		);

		canvas.drawRect(convertedRect, paint);

		// Draw stars if visible
		// Update the stars
		paint.setColor(Color.argb(255, 255, 255, 255));
		for (int i = 0; i < numStars; i++) {

			if (stars[i].getVisibility()) {
				tempPointF = vp.worldToScreenPoint(stars[i].getX(), stars[i].getY());
				canvas.drawPoint(tempPointF.x, tempPointF.y, paint);
			}
		}

		// Draw the bricks if visible
		for (int i = 0; i < numBricks; i++) {

			if (!bricks[i].isClipped()) {

				// Draw the bricks
				// Choose the brush color for drawing
				paint.setColor(bricks[i].getColor());
					convertedRect = vp.worldToScreen(
							bricks[i].getRect().left,
							bricks[i].getRect().top,
							bricks[i].getRect().right - bricks[i].getRect().left,
							bricks[i].getRect().bottom - bricks[i].getRect().top
					);
					canvas.drawRect(convertedRect, paint);

				// Draw building outline
				// Choose the brush color for drawing
				paint.setColor(Color.argb(255, 190, 190, 190));
				if (bricks[i].getLeft()) {
					canvas.drawLine(
							convertedRect.left,
							convertedRect.top,
							convertedRect.left,
							convertedRect.bottom,
							paint
					);

				}

				if (bricks[i].getRight()) {
					canvas.drawLine(
							convertedRect.right,
							convertedRect.top,
							convertedRect.right,
							convertedRect.bottom,
							paint
					);
				}

				if (bricks[i].getTop()) {
					canvas.drawLine(
							convertedRect.left,
							convertedRect.top,
							convertedRect.right,
							convertedRect.top,
							paint
					);
				}

			}
		}

		tempPointF = vp.worldToScreenPoint(player.getA().x, player.getA().y);
		convertedPointA.x = tempPointF.x;
		convertedPointA.y = tempPointF.y;
		tempPointF = vp.worldToScreenPoint(player.getB().x, player.getB().y);
		convertedPointB.x = tempPointF.x;
		convertedPointB.y = tempPointF.y;
		tempPointF = vp.worldToScreenPoint(player.getC().x, player.getC().y);
		convertedPointC.x = tempPointF.x;
		convertedPointC.y = tempPointF.y;

		paint.setColor(Color.argb(255, 255, 255, 255));
		canvas.drawLine(convertedPointA.x, convertedPointA.y,
				convertedPointB.x, convertedPointB.y,
				paint);

		canvas.drawLine(convertedPointB.x, convertedPointB.y,
				convertedPointC.x, convertedPointC.y,
				paint);

		canvas.drawLine(convertedPointC.x, convertedPointC.y,
				convertedPointA.x, convertedPointA.y,
				paint);

		canvas.drawPoint(convertedPointA.x, convertedPointA.y, paint);

		// Update all the player bullets if active
		for (int i = 0; i < playerBullets.length; i++) {
			if (playerBullets[i].getStatus()) {

				tempPointF = vp.worldToScreenPoint(
						playerBullets[i].getPoint().x, playerBullets[i].getPoint().y);

				canvas.drawRect(tempPointF.x, tempPointF.y,
						tempPointF.x+4,tempPointF.y+4, paint);
			}
		}

		// Draw some debugging info
		// Choose the brush color for drawing
		paint.setColor(Color.argb(255, 255, 255, 255));
		paint.setTextSize(20);
		canvas.drawText("FPS = " + fps, 20, 70, paint);

		// Draw the floor
		convertedRect = vp.worldToScreen(
				-10,
				groundLevel,
				targetWorldWidth + 10,
				targetWorldHeight - groundLevel
		);

		paint.setColor(Color.argb(255, 5, 66, 9));
		canvas.drawRect(convertedRect, paint);

		// Change paint color
		// Low alpha value to make buttons transparent
		paint.setColor(Color.argb(80, 255, 255, 255));

		for (Rect rect : hud.currentButtonList) {
			RectF rf = new RectF(rect.left, rect.top, rect.right, rect.bottom);
			canvas.drawRoundRect(rf, 15f, 15f, paint);
		}

		// Draw everything to the screen
		ourHolder.unlockCanvasAndPost(canvas);
	}

}

The next three methods handle starting and stopping the thread when prompted to by the MainActivity class. The onTouchEvent method, however, is surprisingly short. It calls hud.handleInput and passes in motionEvent. All the input handling as well as the preparation of the on-screen buttons will be handled by the inner HUD class which we will discuss next.

// If SpaceInvadersActivity is paused/stopped
// shutdown our thread.
public void pause() {
	playing = false;
	try {
		gameThread.join();
	} catch (InterruptedException e) {
		Log.e("Error:", "joining thread");
	}
}

// If SpaceInvadersActivity is started then
// start our thread.
public void resume() {
	playing = true;
	gameThread = new Thread(this);
	gameThread.start();
}

// The SurfaceView class implements onTouchListener
// So we can override this method and detect screen touches.
@Override
public boolean onTouchEvent(MotionEvent motionEvent) {

	hud.handleInput(motionEvent);
	return true;
}

The HUD class is simple and effective. It handles the screen presses as well as defining the position of all the transparent buttons the player will be able to press. Each button is defined by a RectF and the position and size of each calculated based on the resolution of the screen. The .contains method is used with each of the RectF objects to determine when any of the buttons are pressed and then they can be responded to.

class HUD {

	Rect left;
	Rect right;
	Rect thrust;
	Rect shoot;
	Rect pause;

	//create an array of buttons for the draw method
	public ArrayList<Rect> currentButtonList = new ArrayList<>();

	HUD(int screenWidth, int screenHeight) {

		//Configure the player buttons
		int buttonWidth = screenWidth / 8;
		int buttonHeight = screenHeight / 7;
		int buttonPadding = screenWidth / 80;

		left = new Rect(buttonPadding,
				screenHeight - buttonHeight - buttonPadding,
				buttonWidth,
				screenHeight - buttonPadding);

		right = new Rect(buttonWidth + buttonPadding,
				screenHeight - buttonHeight - buttonPadding,
				buttonWidth + buttonPadding + buttonWidth,
				screenHeight - buttonPadding);

		thrust = new Rect(screenWidth - buttonWidth - buttonPadding,
				screenHeight - buttonHeight - buttonPadding - buttonHeight - buttonPadding,
				screenWidth - buttonPadding,
				screenHeight - buttonPadding - buttonHeight - buttonPadding);

		shoot = new Rect(screenWidth - buttonWidth - buttonPadding,
				screenHeight - buttonHeight - buttonPadding,
				screenWidth - buttonPadding,
				screenHeight - buttonPadding);

		pause = new Rect(screenWidth - buttonPadding - buttonWidth,
				buttonPadding,
				screenWidth - buttonPadding,
				buttonPadding + buttonHeight);

		// Add the rect objects in the same order as the static final values
		currentButtonList.add(left);
		currentButtonList.add(right);
		currentButtonList.add(thrust);
		currentButtonList.add(shoot);
		currentButtonList.add(pause);
	}

	public void handleInput(MotionEvent motionEvent) {

			int x = (int) motionEvent.getX(0);
			int y = (int) motionEvent.getY(0);

			switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {

				case MotionEvent.ACTION_DOWN:
					if (right.contains(x, y)) {
						player.setMovementState(player.RIGHT);
					} else if (left.contains(x, y)) {
						player.setMovementState(player.LEFT);
					} else if (thrust.contains(x, y)) {
						player.setMovementState(player.THRUSTING);
					} else if (shoot.contains(x, y)) {
						playerBullets[nextPlayerBullet].shoot(
								player.getA().x,player.getA().y, player.getFacingAngle());
						nextPlayerBullet++;
						if (nextPlayerBullet == maxPlayerBullets) {
							nextPlayerBullet = 0;
						}
						soundPool.play(shootID, 1, 1, 0, 0, 1);

					}else if(pause.contains(x, y)) {
						paused = !paused;
					}
					break;

				case MotionEvent.ACTION_UP:

						player.setMovementState(player.STOPPING);

			}

	}
}// End of HUD

// End of GameView

You can now run the game. Note that the button in the top-right is a pause button and you need to press it to make the other buttons work. But there’s more!

What else can you do with a viewport

We could easily add a “map screen” to our game by having the screen presses call these methods (while in “map mode”) instead of moving the ship.

public void moveViewportRight(int maxWidth){
    if(currentViewportWorldCentre.x < maxWidth - (metresToShowX/2)+3) { 		currentViewportWorldCentre.x += 1; 	} }  public void moveViewportLeft(){ if(currentViewportWorldCentre.x > (metresToShowX/2)-3){

        currentViewportWorldCentre.x -= 1;
    }
}

public void moveViewportUp(){
    if(currentViewportWorldCentre.y < (metresToShowY /2)-3) {
        currentViewportWorldCentre.y -= 1;
    }
}

public void moveViewportDown(int maxHeight){
    if(currentViewportWorldCentre.y < maxHeight - (metresToShowY / 2)+3) {
        currentViewportWorldCentre.y += 1;
    }
}

We could output debugging to the screen regarding the number of clipped game objects. We could internally clip new game objects with a simple isClipped variable. We could then monitor the number of clipped game objects by calling these two methods, once each per frame:

public int getNumClipped(){

	return numClipped;
}

public void resetNumClipped(){

	numClipped = 0;
}

All we need to do is vary the metresToShow... variables and we can instantly zoom our game world in and out. Furthermore, if we have multiple instances of Viewport, we can also pass in different resolutions for each instance. Consider passing the full-screen resolution as we did in this project and then a smaller, say 100 x 100-pixel resolution but with larger values for the metresToShow variables. You could then make two passes through the draw method and you have a map of the game world drawn on top of the current view around the player.

We could even use the same math we use to rotate the spaceship, on each and every pixel via the Viewport class. We could then rotate our entire game-world with ease. Or, a more likely scenario, we could rotate a shrunken view of a game-world, perhaps drawn in the top corner, and we will have ourselves a rotating mini-map without breaking a sweat.

Problems with the design

Anyone with a device that is more than a few years old will probably notice the frame rate drops lower as there are more objects on the screen. My 4-year-old Samsung Note tablet drops as low as 30 frames per second at times. There are a number of things we could do to improve the efficiency of the code. We could optimize our collision detection. We could find a totally new way to represent the buildings. The bricks were just a bit of fun. We don’t really need to construct each building out of dozens of game objects! But what if we really do need vast numbers of game objects?

Quite soon we are going to need to adopt a new development paradigm. We can achieve this by using a game library like LibGDX or we can stick with the Android API and optimize our game code, at the same time as moving to OpenGL ES to do all our drawing. The next series of Android game projects will introduce the basics of making a game, with OpenGL ES doing all the drawing and eventually lead up to a full, 2D, OpenGL ES game project.