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.
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.
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 theGameEngine
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 aCanvas
to draw on.onRemovedFromGameUiThread
contains code that must be run on theUIThread
when the object is removed from the game.onAddedToGameUiThread
contains code that must be run on theUIThread
when the object is added to the game.The two
Runnable
objects are used to callonRemovedFromGameUiThread
andonAddedToGameUiThread
inside theUIThread
.
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.
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 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.
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
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.
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.
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.