Book Image

Mastering Android Game Development

By : Raul Portales
Book Image

Mastering Android Game Development

By: Raul Portales

Overview of this book

Table of Contents (18 chapters)
Mastering Android Game Development
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
API Levels for Android Versions
Index

Game architecture


Games have a different architecture and control flow than apps. Both seem to respond to user input instantly, but while an app does this by setting listeners and reacting to events with method calls (most commonly the onClick method calls the OnClickListener), this approach is not valid for a real-time game (although it is valid for non-real-time games).

Once a game is running, it must evaluate and update everything as fast as possible. This is the reason why it cannot be interrupted by user events. Those events or states should be recorded instead and then read by the game objects during its update.

The game engine should be created inside the fragment that runs the game, because we only need the game engine running while we are playing. This has the advantage that we can use our existing Android knowledge to create and handle the rest of the screens of the game.

Simplified architecture of a game engine

The basic Game Engine architecture is composed of an Update Thread, a Draw Thread, and a series of Game Objects that belong to the Game Engine.

The Game Engine is the component through which the rest of the program interacts with the game. Its mission is also to encapsulate the existence of the update and draw threads as well as to handle the game objects.

A game is composed of Game Objects that are both updated and drawn. These objects are held inside the Game Engine.

The Update Thread is responsible for updating the state of the game objects as fast as it can. It will run through all the game objects calling an update method.

The UI has to also be constantly updating and be independent of the update thread. It will draw all the game objects by calling a draw method on them.

Let's analyze each component in detail.

GameEngine and GameObjects

The GameEngine contains the three elements already mentioned.

GameObject is an abstract class that all game objects in our game must extend from. This interface connects them with the Update and Draw threads.

public abstract class GameObject {
  public abstract void startGame();
  public abstract void onUpdate(long elapsedMillis, GameEngine gameEngine);
  public abstract void onDraw();
  public final Runnable mOnAddedRunnable = new Runnable() {
    @Override
    public void run() {
      onAddedToGameUiThread();
    }
  };

  public final Runnable mOnRemovedRunnable = new Runnable() {
    @Override
    public void run() {
      onRemovedFromGameUiThread();
    }
  };

  public void onRemovedFromGameUiThread(){
  }

  public void onAddedToGameUiThread(){
  }
}
  • startGame is used for the initialization of the object before a game can start.

  • onUpdate is called by the game engine as fast as possible, providing the number of milliseconds that have passed since the previous call and a reference to the GameEngine itself for future uses such as accessing user input.

  • onDraw makes the component render itself. We are not using any parameters just yet, but later we will pass a Canvas to draw on.

  • onRemovedFromGameUiThread contains code that must be run on the UIThread when the object is removed from the game.

  • onAddedToGameUiThread contains code that must be run on the UIThread when the object is added to the game.

  • The two Runnable objects are used to call onRemovedFromGameUiThread and onAddedToGameUiThread inside the UIThread.

The GameEngine will provide us with easy methods to start, stop, pause, and resume the game, so we don't have to worry about the threads or the game objects from the outside.

The game engine is composed of three items: the list of game objects, the UpdateThread, and the DrawThread.

private List<GameObject> mGameObjects = new ArrayList<GameObject>();

private UpdateThread mUpdateThread;
private DrawThread mDrawThread;

Let's take a look at the different methods of the engine to handle a game.

Starting a game

The code to start a game from the GameEngine is as follows:

public void startGame() {
  // Stop a game if it is running
  stopGame();

  // Setup the game objects
  int numGameObjects = mGameObjects.size();
  for (int i=0; i<numGameObjects; i++) {
    mGameObjects.get(i).startGame();
  }

  // Start the update thread
  mUpdateThread = new UpdateThread(this);
  mUpdateThread.start();

  // Start the drawing thread
  mDrawThread = new DrawThread(this);
  mDrawThread.start();
}

First of all, we have to make sure that no game is running, so we call stopGame at the beginning to stop a game if there is one in progress.

Secondly, we reset all the game objects that are linked to the engine. It is important to do this before we start the threads, so everything starts from the initial position.

Finally, we create and start the UpdateThread and the DrawThread.

Stopping a game

Stopping a game is even simpler. We just have to stop the Update and Draw threads if they exist:

public void stopGame() {
  if (mUpdateThread != null) {
    mUpdateThread.stopGame();
  }
  if (mDrawThread != null) {
    mDrawThread.stopGame();
  }
}

We also have methods for pauseGame and resumeGame that are functionally equivalent to this one. In these methods, the logic of the action belongs to each thread. We are not including the code of these methods here, because they are redundant.

Managing game objects

The engine has to manage the addition and removal of game objects. We cannot just handle the list directly, since it will be used intensively during onUpdate and onDraw.

public void addGameObject(final GameObject gameObject) {
  if (isRunning()){
    mObjectsToAdd.add(gameObject);
  }
  else {
    mGameObjects.add(gameObject);
  }
  mActivity.runOnUiThread(gameObject.mOnAddedRunnable);
}

public void removeGameObject(final GameObject gameObject) {
  mObjectsToRemove.add(gameObject);
  mActivity.runOnUiThread(gameObject.mOnRemovedRunnable);
}

We use the lists mObjectsToAdd and mObjectsToRemove to keep track of the objects that must be added or removed. We will do both as the last step of the onUpdate method with the exception of when the game engine is not running, in which case it is safe to add and remove them directly.

We are also running the corresponding Runnable object from the GameObject on the UIThread.

To update the game objects from the engine, we just call onUpdate on all of them. Once the update loop has finished, we take care of the objects that must be removed or added to mGameObjects. This part is done using a synchronized section that is also important for the onDraw method.

public void onUpdate(long elapsedMillis) {
  int numGameObjects = mGameObjects.size();
  for (int i=0; i<numGameObjects; i++) {
    mGameObjects.get(i).onUpdate(elapsedMillis, this);
  }
  synchronized (mGameObjects) {
    while (!mObjectsToRemove.isEmpty()) {
      mGameObjects.remove(mObjectsToRemove.remove(0));
    }
    while (!mObjectsToAdd.isEmpty()) {
       mGameObjects.add(mObjectsToAdd.remove(0));
    }
  }
}

We do the same for drawing, except that the drawing must be done on the UIThread. So, we create a Runnable object that we pass to the runOnUIThread method of the activity.

private Runnable mDrawRunnable = new Runnable() {
  @Override
  public void run() {
    synchronized (mGameObjects) {
      int numGameObjects = mGameObjects.size();
      for (int i = 0; i < numGameObjects; i++) {
        mGameObjects.get(i).onDraw();
      }
    }
  }
};

public void onDraw(Canvas canvas) {
  mActivity.runOnUiThread(mDrawRunnable);
}

Note that we synchronize the run method using mGameObjects. We do it so we are sure that the list is not modified while we iterate it.

It is also important that only the last part of the onUpdate is synchronized. If no objects are added or removed, the threads are independent. If we synchronize the complete onUpdate method, we will be losing all the advantages of having the Update and Draw threads separated.

UpdateThread

UpdateThread is a thread that continuously runs updates on the game engine. For each call to onUpdate, it provides the number of milliseconds since the previous execution.

The basic run method of the update thread is as follows:

@Override
public void run() {
  long previousTimeMillis;
  long currentTimeMillis;
  long elapsedMillis;
  previousTimeMillis = System.currentTimeMillis();

  while (mGameIsRunning) {
    currentTimeMillis = System.currentTimeMillis();
    elapsedMillis = currentTimeMillis - previousTimeMillis;           
    mGameEngine.onUpdate(elapsedMillis);
    previousTimeMillis = currentTimeMillis;
  }
}

The thread stays in a loop for as long as the game is running. On each iteration, it will get the current time, calculate the elapsed milliseconds since the previous run, and call onUpdate on the GameEngine object.

While this first version works and is very simple to follow, it can only start and stop a game. We want to be able to pause and resume it as well.

To pause and resume the game, we need a variable that we read inside the loop to check when to pause the execution. We'll need to keep track of the elapsed milliseconds and discount the time spent paused. A simple way to do it is like this:

while (mGameIsRunning) {
  currentTimeMillis = System.currentTimeMillis();
  elapsedMillis = currentTimeMillis - previousTimeMillis;
  if (mPauseGame) {
    while (mPauseGame) {
      try {
        Thread.sleep(20);
      } catch (InterruptedException e) {
        // We stay on the loop
      }
    }
    currentTimeMillis = System.currentTimeMillis();
  }
  mGameEngine.onUpdate(elapsedMillis);
  previousTimeMillis = currentTimeMillis;
}

The code for the pauseGame and resumeGame methods is just setting the variable mPauseGame to true or false.

If the game is paused, we enter a while loop in which we will remain until the game is resumed. To avoid having an empty loop that runs continuously, we can put the thread to sleep for a short amount of time (20 milliseconds). Note that Thread.sleep can trigger an InterruptedException. If that happens we can just continue since it is going to be run in 20 milliseconds again. Besides, we are going to improve it right now.

This approach works, but there is still a lot of idle processing being done. For threads, there are mechanisms to pause and resume in a much more efficient way. We are going to improve this using wait/notify.

The code can be updated to be like this:

while (mGameIsRunning) {
  currentTimeMillis = System.currentTimeMillis();
  elapsedMillis = currentTimeMillis - previousTimeMillis;
  if (mPauseGame) {
    while (mPauseGame) {
      try {
        synchronized (mLock) {
          mLock.wait();
        }
      } catch (InterruptedException e) {
        // We stay on the loop
      }
    }
    currentTimeMillis = System.currentTimeMillis();
  }
  mGameEngine.onUpdate(elapsedMillis);
  previousTimeMillis = currentTimeMillis;
}

The pauseGame method is the same as before, but we need to update resumeGame to be at the place from where the lock is notified and released:

public void resumeGame() {
  if (mPauseGame == true) {
    mPauseGame = false;
    synchronized (mLock) {
      mLock.notify();
    }
  }
}

With the use of wait/notify, we ensure that the thread will not do any work while it is idle and we also know that it will be woken up as soon as we notify it. It is important to first set mPauseGame to false and then awake the thread, otherwise the main loop could stop again.

Finally, to start and stop the game, we just need to change the values of the variables:

public void start() {
  mGameIsRunning = true;
  mPauseGame = false;
  super.start();
}

public void stopGame() {
  mGameIsRunning = false;
  resumeGame();
}

The game never starts in a paused state. To stop a game, we just need to set the mGameIsRunning value to false and the loop inside the run method will end.

It is important to call resumeGame as a part of the stopGame method. If we call stop while the game is paused, the thread will be waiting, so nothing will happen unless we resume the game. If the game is not paused, nothing is done inside resumeGame, so it does not matter if we called it.

DrawThread

There are several ways to implement DrawThread. It could be done in a similar way to the update thread, but we are going to use a much simpler approach that does not use a Thread.

We are going to use the Timer and TimerTask classes to send the onDraw callback to the game engine with a high-enough frequency to render at 30 frames per second:

private static int EXPECTED_FPS = 30;
private static final long TIME_BETWEEN_DRAWS = 1000 / EXPECTED_FPS;

public void start() {
  stopGame();
  mTimer = new Timer();
  mTimer.schedule(new TimerTask() {
    @Override
    public void run() {
      mGameEngine.onDraw();
    }
  }, 0, TIME_BETWEEN_DRAWS);
}

We have this method called every 33 milliseconds. In simple implementations, this method will just call invalidate in the GameView, which will cause a call to the onDraw method of the View.

This implementation relies on one feature of the Android UI. To redisplay views, Android has a contingency system that is built in to avoid recurrent invalidates. If an invalidation is requested while the view is being drawn, it will be queued. If more than one invalidations are queued, they will be discarded as they won't have any effect.

With this, if the view takes longer than TIME_BETWEEN_DRAWS to be drawn, the system will fall back to fewer frames per second automatically.

Later in the book, we will revisit this thread for more complex implementations but, for now, let's keep it simple.

Stopping, pausing, and resuming the DrawThread is also simple:

public void stopGame() {
  if (mTimer != null) {
    mTimer.cancel();
    mTimer.purge();
  }
}

public void pauseGame() {
  stopGame();
}

public void resumeGame() {
  start();
}

To stop the game, we only need to cancel and purge the timer. The cancel method will cancel the timer and all scheduled tasks, while purge will remove all the canceled tasks from the queue.

Since we do not need to keep track of any state, we can just make the pauseGame and resumeGame equivalents to stopGame and start.

Note that, if we want to have a smooth game at 30fps, the drawing of all the items on the screen must be performed in less than 33 milliseconds. This implies that the code of these methods usually needs to be optimized.

User input

As we mentioned, user input is to be processed by some input controller and then read by the objects that need it, when they need it. We will go into the details of such an input controller in the next chapter. For now, we just want to check whether the game engine works as expected and handles the start, stop, pause, and resume calls properly.

Pause, resume, and start are different from the other user inputs, because they affect the state of the engine and threads themselves instead of modifying the state of the game objects. For this reason, we are going to use standard event-oriented programming to trigger these functions.