Now that we have learned about the game loop and threads, we can put it all together to implement our game loop in the Living Drawing project.
We will add the entire code for the game loop, including writing two methods in the LiveDrawingActivity
class, to start and stop the thread that will control the loop.
Update the class declaration by implementing Runnable
, just like we discussed previously, and as shown in the following highlighted code:
class LiveDrawingView extends SurfaceView implements Runnable{
Notice that we have a new error in the code. Hover the mouse pointer over the word Runnable
and you will see a message informing you that we need to implement the run
method, again, just as we discussed during the discussion on interfaces and threads in the previous section. Add the empty run
method, including the @override
label, as demonstrated shortly.
It doesn't matter where you add it, provided it is within the LiveDrawingView
class's curly braces and not inside another method. I added mine right after the constructor method because it is near the top and easy to get to. We will be editing this quite a bit in this chapter. Add the empty run
method, as shown here:
// When we start the thread with: // mThread.start(); // the run method is continuously called by Android // because we implemented the Runnable interface // Calling mThread.join(); // will stop the thread @Override public void run() { }
The error is gone and now we can declare and initialize a Thread
object.
Declare some variables and instances, shown as follows, underneath all our other members in the LiveDrawingView
class:
// Here is the Thread and two control variables private Thread mThread = null; // This volatile variable can be accessed // from inside and outside the thread private volatile boolean mDrawing; private boolean mPaused = true;
Now, we can start and stop the thread. Have a think about where we might do this. Remember that the app needs to respond to the operating system starting and stopping the app.
Now, we need to start and stop the thread. We have seen the code we need, but when and where should we do it? Let's write two methods, one to start and one to stop, and then we can consider further when and where to call these methods from. Add these two methods inside the LiveDrawingView
class. If their names sound familiar, it is not by chance:
// This method is called by LiveDrawingActivity // when the user quits the app public void pause() { // Set mDrawing to false // Stopping the thread isn't // always instant mDrawing = false; try { // Stop the thread mThread.join(); } catch (InterruptedException e) { Log.e("Error:", "joining thread"); } } // This method is called by LiveDrawingActivity // when the player starts the app public void resume() { mDrawing = true; // Initialize the instance of Thread mThread = new Thread(this); // Start the thread mThread.start(); }
What is happening is slightly given away by the comments. You did read the comments, right? We now have a pause
and resume
method that stop and start the Thread
object using the same code we discussed previously.
Notice that the new methods are public
and therefore accessible from outside the class to any other class that has an instance of LiveDrawingView
.
Remember that LiveDrawingActivity
has the fully declared and initialized instance of LiveDrawingView
?
Let's use the Android Activity life cycle to call these two new methods.
Update the overridden onResume
and onPause
methods in LiveDrawingActivity,
as shown in the highlighted lines of code:
@Override protected void onResume() { super.onResume(); // More code here later in the chapter mLiveDrawingView.resume(); } @Override protected void onPause() { super.onPause(); // More code here later in the chapter mLiveDrawingView.pause(); }
Now, our thread will be started and stopped when the operating system is resuming and pausing our app. Remember that onResume
is called after onCreate
the first time an app is created, not just after resuming from a pause. The code inside onResume
and onPause
uses the mLiveDrawingView
object to call its resume
and pause
methods, which, in turn, has the code to start and stop the thread. This code then triggers the thread's run
method to execute. It is in this run
method (in LiveDrawingView
) that we will code our game loop. Let's do that now.
Although our thread is set up and ready to go, nothing happens because the run
method is empty. Code the run
method, as shown in the following code:
@Override public void run() { // mDrawing gives us finer control // rather than just relying on the calls to run // mDrawing must be true AND // the thread running for the main // loop to execute while (mDrawing) { // What time is it now at the start of the loop? long frameStartTime = System.currentTimeMillis(); // Provided the app isn't paused // call the update method if(!mPaused){ update(); // Now the particles are in // their new positions } // The movement has been handled and now // we can draw the scene. draw(); // How long did this frame/loop take? // Store the answer in timeThisFrame long timeThisFrame = System.currentTimeMillis() - frameStartTime; // Make sure timeThisFrame is at least 1 millisecond // because accidentally dividing // by zero crashes the app if (timeThisFrame > 0) { // Store the current frame rate in mFPS // ready to pass to the update methods of // of our particles in the next frame/loop mFPS = MILLIS_IN_SECOND / timeThisFrame; } } }
Notice that there are two errors in Android Studio. This is because we have not written the update
method yet. Let's quickly add an empty method (with a comment) for it. I added mine after the run
method:
private void update() { // Update the particles }
Now, let's discuss in detail how the code in the run
method achieves the aims of our game loop by looking at the whole thing a bit at a time.
This first part initiates a while
loop with the mDrawing
condition and wraps the rest of the code inside run
so that the thread will need to be started (for run
to be called) and mDrawing
will need to be true for the while
loop to execute:
@Override public void run() { // mPlaying gives us finer control // rather than just relying on the calls to run // mPlaying must be true AND // the thread running for the main // loop to execute while (mPlaying) {
The first line of code inside the while
loop declares and initializes a local variable, frameStartTime
, with whatever the current time is. The static method, currentTimeMillis
, of the System
class returns this value. If we want to measure how long a frame has taken later on, then we need to know what time it started:
// What time is it now at the start of the loop? long frameStartTime = System.currentTimeMillis();
Next, still inside the while
loop, we need to check whether the app is paused, and only if the app is not paused does the following code get executed. If the logic allows execution inside this block, then update
is called:
// Provided the app isn't paused // call the update method if(!mPaused){ update(); // Now the particles are in // their new positions }
Outside of the previous if
statement, the draw
method is called to draw all the objects in the just-updated positions. At this point, another local variable is declared and initialized with the length of time it took to complete the entire frame (updating and drawing). This value is calculated by getting the current time, once again with currentTimeMillis
, and subtracting frameStartTime
from it, as follows:
// The movement has been handled and collisions // detected now we can draw the scene. draw(); // How long did this frame/loop take? // Store the answer in timeThisFrame long timeThisFrame = System.currentTimeMillis() - frameStartTime;
The next if
statement detects whether timeThisFrame
is greater than zero. It is possible for the value to be zero if the thread runs before objects are initialized. If you look at the code inside the if
statement, it calculates the frame rate by dividing the elapsed time by MILLIS_IN_SECOND
. If you divide by zero, the app will crash, which is why we do the check.
Once mFPS
gets the value assigned to it, we can use it in the next frame to pass to the update
method all the particles that we will code in the next chapter. They will use the value to make sure that they move by precisely the correct amount based on their target speed and the length of time the frame has taken:
// Make sure timeThisFrame is at least 1 millisecond // because accidentally dividing // by zero crashes the app if (timeThisFrame > 0) { // Store the current frame rate in mFPS // ready to pass to the update methods of // the particles in the next frame/loop mFPS = MILLIS_IN_SECOND / timeThisFrame; } } }
The result of the calculation that initializes mFPS
each frame is that mFPS
will hold a fraction of 1. As the frame rate fluctuates, mFPS
will hold a different value and supply the game objects with the appropriate number to calculate each move.
3.144.222.149