Android 2D RPG - Part 5: FPS

Hello fellow readers! Here I am bringing you part 5 of the 2D RPG for Android series! Today we'll be discussing Frames Per Second (FPS). But, before we go any further, I need to share some bug fixing I did on our work in Part 4.


MainActivity.java
package ve.com.biocraft.biocraft;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.WindowManager;

public class MainActivity extends AppCompatActivity {

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

    private GamePanel gamePanel;

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

        // Making App fullscreen
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

        // Set our GamePanel as the View
        gamePanel = new GamePanel(this);
        setContentView(gamePanel);
        Log.d(TAG, "View added!");
    }

    @Override
    protected void onDestroy() {
        Log.d(TAG, "Destroying!!!");
        super.onDestroy();
    }

    @Override
    protected void onStop() {
        Log.d(TAG, "Stopping...");
        super.onStop();
    }

    @Override
    protected void onPause() {
        Log.d(TAG, "Pause!");
        gamePanel.setRunningFalse();
        super.onPause();
    }
}

The main change here is that we now declare our GamePanel as a private object before setting it as our View. I also added the onPause() method, this is going to fix a bug that was happening when we were leaving our app (pressing the back or home buttons) that was causing it to crash. I also got rid of the part that was hiding our title bar, we'll instead change the theme of our app for one that includes no bar.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="ve.com.biocraft.biocraft">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.AppCompat.NoActionBar">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Here we simply changed the theme we had for "@style/Theme.AppCompat.NoActionBar".

And finally, some changes on our GamePanel.

GamePanel.java
package ve.com.biocraft.biocraft;

import android.app.Activity;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class GamePanel extends SurfaceView implements SurfaceHolder.Callback {

    private MainThread thread;

    public GamePanel(Context context) {
        super(context);
        // Adding callback (this) to surface holder to catch events
        getHolder().addCallback(this);

        // Create the Game Loop (the thread)
        thread = new MainThread(getHolder(), this);

        // Make GamePanel able to focus so it can handle events
        setFocusable(true);
    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {

    }

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        // Check if its the first time the thread starts
        if (thread.getState() == Thread.State.NEW) {
            // When the surface is created we set the running flag to true
            thread.setRunning(true);
            // And we start the Game Loop
            thread.start();
        } else if (thread.getState() == Thread.State.TERMINATED) {
            // Start the thread again after a pause
            thread = new MainThread(getHolder(), this);
            thread.setRunning(true);
            thread.start();
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
        boolean retry = true;
        while (retry) {
            try {
                thread.join();
                retry = false;
            } catch (InterruptedException e) {
                // Try again to shut down the thread
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            thread.setRunning(false);
            ((Activity)getContext()).finish();
        }
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {

    }

    public void render(Canvas canvas) {
        canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.lyon), 0, 0, null);
    }

    public void setRunningFalse() {
        thread.setRunning(false);
    }
}

I changed the surfaceCreated(SurfaceHolder surfaceHolder) method so that it checks whether it is the first time it's created. In case it had already been created (and terminated) it'll start a new one. I also added the setRunningFalse() method, which, well, sets the running flag of our MainThread to false, obviously xD

- Bug fixing is over. Test the app now, it shouldn't crash :P

It's finally time to start with what this post is all about: frames per second. FPS, that thing you hear the kid with the brand new PC bragging on the General or Trade chat... "I'm running at 120fps on Orgrimmar, nubs". What does it mean? Well, it's simply the amount of times the screen refreshes (renders an image) in a second. But it's not only the screen refreshing, it's also the Game State updating, cause there's not much really to look at if we just draw the same thing infinite times (as we're doing right now).

This means in order for this post to make sense, we need to draw different things. To make things easier to see and understand, we won't go complex and draw multiples images moving and whatnot, we'll just take the image we already are drawing and move it, let's say, upside-down. When it reaches the bottom we'll just draw it again on the top, cool? Let's rock!

Let's start by making our image a game object, since it's supposed to be our character, let's go ahead and create a new Java Class and name it Hero.java, cause there's already a Character class xD

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

import android.graphics.Bitmap;
import android.graphics.Canvas;

public class Hero {

    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

    public Hero(Bitmap bitmap, int x, int y){
        this.bitmap = bitmap;
        this.x = x;
        this.y = y;
    }

    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 void draw(Canvas canvas) {
        canvas.drawBitmap(bitmap, x, y, null);
    }

    public void update() {
        y += 1;
    }

}

This is our Hero class. It has a Bitmap that is the image where we have our sprites (the guy we've been using), and two integers x and y that are the coordinates on which the image will be drawn. Nothing complicated here. A simple constructor, and methods to get and set the values of our hero, we won't be using all of them for now, but they might come in handy in the future.

The last 2 methods, draw(Canvas canvas) and update() are the Hero's render and Game State update, respectively. These methods will need to be called from the Game Loop, let's get to it. First, we'll modify our GamePanel.

First thing, we need to add a Hero object to it. Now our declared variables will look like this.

GamePanel.java
private static final String TAG = GamePanel.class.getSimpleName();

private MainThread thread;
private Hero hero;

There's our hero! Now let's change the render(Canvas canvas) method.

GamePanel.java
public void render(Canvas canvas) {
    // Fill screen with black
    canvas.drawColor(Color.BLACK);
    // Tell our hero to draw itself
    hero.draw(canvas);
}

The code speaks for itself (it has nice comments xD). We'll basically let every object render itself from this method, while making sure the screen is empty before they do.

Finally, we'll add a new method called update() that is our Game State updater, we didn't have one yet because there was nothing to update, now we'll be updating our Hero's position. If you check the Hero's update() method, you'll see every time it's called it'll increase it's vertical coordinate by 1.

GamePanel.java
public void update() {
    //Check if hero reached bottom
    if (hero.getY() + hero.getBitmap().getHeight() >= getHeight()) {
        hero.setY(0);
    }
    // Tell our hero to update itself
    hero.update();
}

It also checks if the Hero is already at the bottom, if it is, then it moves it back to the top.

Now we just have to add a line to our run() method from MainThread because it's not calling our GamePanel's update() method yet. We can't allow that! xD

MainThread.java
@Override
public void run() {
    Canvas canvas;
    Log.d(TAG, "Starting Game Loop");
    while (running) {
        // 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);
            }
        }
    }
}

All we did was add "this.gamePanel.update();". Now run that app and you will see the same image we had before, except this time it moves down and resets to top after it reaches bottom. Now that we got something moving, it's time to check our FPS!

Since our update and render methods are called one after the other, they execute the same amount of times. Let's "calculate" the FPS we got right now. You'll need your device/emulator screen size, mine is an emulated Nexus S, which makes the screen 480x800, my image is 96x128, which means it moves 800-128 = 672 pixels before reaching bottom, that means 672 game updates and renders. Let's measure how much time that takes! Chronometer in hand (ok, it's my phone's clock app)... 10.52s!!! Now let's divide 672/10.52 = 63.88. That's my FPS, almost 64.

Now, depending on the device (or PC if using an emulator), this speed will change. Try this app on a brand new ultra-awesome phone that has almost every service disabled and it's only running this game, and you'll have a HUGE FPS, try it on a emulated phone on a Pentium IV machine overloaded with malware, and you might never see your guy reach the bottom xD

It is important to know an ugly truth about this before going forward: The most amazingly eye-gifted humans can't distinguish anything over 60 FPS, with the average one not noticing any differences with 25+. If you're wondering, I can't see any differences over 35 FPS, last time I checked, it's probably a bit lower now :(. This means the annoying brat in chat isn't really getting more than you, if your game runs at 30 FPS, and this is why I always recommend to cap a game's max FPS to 60.

Mind you, when you tell a game to "cap" the FPS, it'll simply wait before starting a new cycle of the game loop, this will prevent a lot of overheating, specially on laptops, and will consume less power as your cpu and gpu will work less. Save the planet, dude! Cap that FPS!

For us, it's much worse, we're working on phones and tablets, and making sure to give our cpus and gpus some sleeping time will not only prevent our phones from cooking our hands, but will also save A LOT of battery.

My mean and evil game is now going at around 64 FPS, never stopping for a millisecond on it's quest to find my phone charger, we MUST change that. Now, for phones and tablets I recommend to set our cap to 25 FPS, so I'll work with that in mind. We'd have to update and render 25 times in a second, that means 1000ms/25 = 40ms. Each cycle will have to last 40ms for this to work.

Note: We can't just run the cycle 25 times and wait for a second to pass, we need to make those frames last displayed the same time so it looks smooth, otherwise you might end with 25 frames in half a second, and then half a second of a static image, horrible!

The good thing is, we're just going to need to add a few lines to our run() method to achieve this. Take a look.

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

    long beginTime;
    int sleepTime;

    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);
            }
        }
        // 40 is the amount of ms we want each frame to take
        sleepTime = (int)(40 - (System.currentTimeMillis() - beginTime));
        // If we got some time to sleep, everything is ok
        if (sleepTime > 0) {
            // 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... :(");
            }
        }
    }
}

Again, the changes are explained in the code with comments. Basically we declared 2 variables, one to store the system time when a Game Loop starts, and another to store the amount of time the thread can go to sleep. Then, we take the system time at the very start of our Game Loop and calculate the the sleep time, if any, at the very end.

Now run the app, noticed the difference? For me, the 672 times it takes the image to go from top to bottom should become 672/25 = 26.88 seconds, let me check! Ok, it took 29.30. That's close! Remember, the FASTEST it can go is 25FPS, but it doesn't have to be that fast. It just means, other than human error on taking time, that at some point the FPS dropped below 25.

That's not good, right? We WANT to reach 25FPS, what do we do when a loop takes longer than those 40ms we gave it? What happens when Mr. Phone decides to sync your data, or receive some Facebook notification? That all requires cpu and will slow us down! We'll deal with all that in the next post, Part 6: A Good Game Loop. We'll also stop drawing our whole image and instead draw our hero sprite by sprite, which will teach us a bit more about the rendering process.

Thanks for reading! Sorry if it is a bit too long, I hope you guys have as much fun as I did while doing this. Take care!

Please share! And subscribe! And follow me! :P