January 9, 2016

Android 2D RPG - Part 6: A Good Game Loop

Greetings, readers! Today I'm gonna show you some updated code for our classes. After this changes, you'll not only put a cap on the FPS, but also make sure your game is actively trying to catch up with it, if it ever falls behind.


MainThread.java
package ve.com.biocraft.biocraft;

import android.graphics.Canvas;
import android.util.Log;
import android.view.SurfaceHolder;

public class MainThread extends Thread {

    // Constant for logging
    private static final String TAG = MainThread.class.getSimpleName();

    // Frame Period
    private static final long FRAME_PERIOD = 1000 / 25;

    private SurfaceHolder surfaceHolder;
    private GamePanel gamePanel;

    // This is the flag to check if the game is running
    private boolean running;

    // This is the void that sets the flag to either true or false
    public void setRunning(boolean running) {
        this.running = running;
    }

    public MainThread(SurfaceHolder surfaceHolder, GamePanel gamePanel) {
        super();
        this.surfaceHolder = surfaceHolder;
        this.gamePanel = gamePanel;
    }

    @Override
    public void run() {
        Canvas canvas;
        Log.d(TAG, "Starting Game Loop");

        long loopTime = FRAME_PERIOD;
        long beginTime;
        long sleepTime;
        int framesSkipped;

        while (running) {
            beginTime = System.currentTimeMillis();
            // Update Game State
            this.gamePanel.update();
            canvas = null;
            // Try to lock the canvas for pixel editing
            try {
                canvas = this.surfaceHolder.lockCanvas();
                synchronized (surfaceHolder) {
                    // Render
                    this.gamePanel.render(canvas);
                }
            } finally {
                // Catching exceptions
                if (canvas != null) {
                    surfaceHolder.unlockCanvasAndPost(canvas);
                }
            }
            // loopTime is the amount of ms we want each frame to take
            sleepTime = loopTime - System.currentTimeMillis() + beginTime;
            // If we got some time to sleep, everything is ok
            if (sleepTime >= 0) {
                // Set loopTime to expected Frame Period
                loopTime = FRAME_PERIOD;
                // Send the thread to sleep for a short period
                // Good for battery saving
                try {
                    Thread.sleep(sleepTime);
                } catch (InterruptedException e) {
                    Log.d(TAG, "Exception when attempting to sleep... :(");
                }
            } else {
                // If we're over our frame period we need to catch up
                // We'll update Game State without rendering
                // Until we get to a loopTime of, at least, the Frame Period
                framesSkipped = 0;
                loopTime = FRAME_PERIOD + sleepTime;
                while (loopTime < FRAME_PERIOD) {
                    beginTime = System.currentTimeMillis();
                    this.gamePanel.update();
                    framesSkipped++;
                    loopTime += FRAME_PERIOD - System.currentTimeMillis() + beginTime;
                }
                Log.d(TAG, "Frames skipped: " + framesSkipped);
            }
        }
    }

}

Ok. Let's check the changes... First, we added a constant long called FRAME_PERIOD that we set up on 1000/25 (1000 ms, aka 1 second, divided by 25, the amount of FPS we want, the result of which is 40ms), that's where we will store the expected amount of time each loop will take in milliseconds. Mainly used for comparisons, useful if we ever want to change the FPS since we will only need to change it's value.

The rest of the changes were on the run() method. We added some variables: loopTime will tell how much time a loop will have to complete (no, it will not always be 40ms, more on this later), and framesSkipped will count how many renders we miss due to a loop taking longer than expected.

The first part of the code, up to when we have some free time to sleep, remains the same. But now we will try to catch up if we fall behind. How? Skipping renders. Since our game speed depends on the number of times update() is called, we must call that method without rendering or we will make the game run slower. Our priority will always be to call update() those 25 times per second, it might not look nice if we sacrifice too many renders, but at least our Game State will be always updated and the game won't go slower, if only it'll look laggy.

The catch is, after calling a couple updates one after the other, we need to make sure we don't go ahead of the expected loop time, that's why, after a catch up, our time frame will be over 40ms.

Let's try with an example: Each loop should take 40ms, so we have 25 FPS at the end.
  1. First loop takes 30ms, we sleep 10ms, all good.
  2. Second loop takes 50ms, we're behind 10ms!
  3. Our third loop only has 30ms to complete! We'll tell it to skip the render() method to try to catch up. We update our Game State, it took 10ms, we still got 20ms left of our 3rd loop. We won't render this time, but we shouldn't sleep either. Instead, 3rd loop will give it's remaining 20ms to the 4th loop.
  4. 4th loop (update and render) starts with a time frame of 60ms (the usual 40, plus 20 that the 3rd loop had extra).
This way we make sure to not only cap at 25 FPS, but to STAY at 25 FPS. Note: in this example we only missed one render() call, on the 3rd loop. That's what we call a skipped frame.

As it stands, we got a pretty solid Game Loop that will do it's best to stay at our set FPS, this will allow us to have a smooth game that doesn't run slow nor too fast.

Now let's make an eye-candy change to our hero, instead of displaying a whole picture, we'll draw an animation. Since it's moving from top to bottom, we'll use the top line of sprites (using the little guy from Part 4) and it'll look like it's walking, desperately fast at 25 pixels (and about as many renders) per second xD

Hero.java
package ve.com.biocraft.biocraft;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.Log;

public class Hero {

    // Constant for logging
    private static String TAG = Hero.class.getSimpleName();

    private Bitmap bitmap;    // The image of our character sprites
    private int x;            // The x coordinate of our character
    private int y;            // The y coordinate of our character
    private int direction;    // The direction the character is facing (0 down, 1 left, 2 right, 3 up. The order of our sprite image)
    private int sprite;       // The animation sprite, since our characters have 3 sprites per animation, this ranges from 0 to 2
    private int spriteHeight; // Size (in pixels) of a single sprite height
    private int spriteWidth;  // Size (in pixels) of a single sprite width
    private Rect src;         // Square of the image to be drawn
    private Rect dst;         // Square of the screen to draw unto

    public Hero(Bitmap bitmap, int x, int y, int direction){
        this.bitmap = bitmap;
        this.x = x;
        this.y = y;
        this.direction = direction;
        this.sprite = 0;
        this.spriteHeight = bitmap.getHeight() / 4; // 4 directions
        this.spriteWidth = bitmap.getWidth() / 3;   // 3 sprites per direction
        this.src = new Rect(0, direction * spriteHeight, spriteWidth, (direction + 1) * spriteHeight);
        this.dst = new Rect(x, y, x + spriteWidth, y + spriteHeight);
        Log.d(TAG, "Hero created!");
    }

    public Bitmap getBitmap() {
        return bitmap;
    }

    public void setBitmap(Bitmap bitmap) {
        this.bitmap = bitmap;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getDirection() {
        return direction;
    }

    public void setDirection(int direction) {
        this.direction = direction;
    }

    public void draw(Canvas canvas) {
        canvas.drawBitmap(bitmap, src, dst, null);
    }

    public void update() {
        y++;
        dst.top = y;
        dst.bottom = y + spriteHeight;

        sprite++;
        src.left += spriteWidth;
        src.right += spriteWidth;
        if (sprite > 2) {
            sprite = 0;
            src.left = 0;
            src.right = spriteWidth;
        }
    }

}

Well, we added a lot of variables here, they're all to be used for rendering. Each one has a comment next to it that explains what it does. We updated our draw(Canvas canvas) and update() methods to reflect the changes on what we want to draw now. Basically, we're choosing a rectangle of our image (a sprite) and painting it on a rectangle from our canvas (the drawing space), those rectangles are of the same area now, but they don't have to be. When updating, we make sure to change the coordinates of the rectangles to pick the next sprite and to keep moving our hero to the bottom.

We did a small change to the class constructor here (so we give it the direction the hero is facing), which means we have to change a line in our GamePanel's GamePanel(Context context) method (the constructor).

GamePanel.java
// Create our hero and load it's bitmap
hero = new Hero(BitmapFactory.decodeResource(getResources(), R.drawable.lyon), 0, 0, 0);

That's it! Run the game now and check out the changes! Try setting a different FRAME_PERIOD and you'll see how the game goes faster or slower :)

Hope you guys enjoyed it, please share the posts and follow me on my social networks, I appreciate it :)

Take care!!!

No comments: