Better Off Android – 1.2 Bit:GameLoop

Android-logo

Until now the ‘GameThread‘ loop I’d been using was based on the code provided by Against The Grain in their article The Game Loop

This provided a good loop, with some overtime & undertime management. Though, with fast moving games (using basic Android graphics, not OpenGL), I was experiencing excessive blurring and jittering when drawing to the screen.

This wasn’t necessarily a issue with the game loop – Since I had written these games in order to teach myself Android, using sample code and best guess work – there were always going to be issue with the code (too much garbage collection, not enough optimisation, etc). Equally, there does seem to be a problem with Android allowing background-processes to soak up too many resources from the focused app – which can cause issues – especially with scrolling games-  when too much CPU/Memory/Bus time is handed over to them. Also; I’m using AdMob which seems to be quite heavy when updating ads – Plenty of things keeping even a simple game from running well on the system.

 


 

All Android apps must face the same issues, but they don’t seem to have the same problem – so I stated googling about to see what I could find…

Theory:
GAFFER ON GAMES – Fix Your Timestep!
Game Programming Patterns – Game Loop
Bad Logic Games – Separating logic and render code

Practical:
Stack Overflow – Android 2d canvas game: FPS Jitter problem
Stack Overflow – Android smooth game loop
Against the Grain – The Game Loop
Stack Exchange – Tips for writing the main game loop?
Stack Overflow – Android fixed delta time still choppy?
Java-Gaming.org – Game loops!

 


 

I tried implementing as many of the alternatives as possible, with varying success from the results; ultimatly I settled with the variable timestep loop provided by Java-Gaming.org  – For these titles, it provided the the most consistant performance.

To smooth things out even more, I also applied a suggestion made by Jason on the Stack Overflow – Android fixed delta time still choppy? thread. He explains;

I was able to cut down quite a bit of the choopy effect that my app was displaying, I think it was mostly due to varying deltaTimes. My solution for now is to calculate the average delta time from every 5 iterations in my main loop and pass that for each update / draw call. I then continue doing this for every 5 iterations, but I also compare with the last iteration, effectively cutting in half any discrepencies between each deltaTime calculation.

With my code, I double down on this, instead of using the average delta for every 5 frames, I use the average for every 60 frames (linked directly to the games FPS variable).

 


 

A couple of final edits;

Changing Thread.Sleep with surfaceHolder.wait (A Toast timer worked pretty well too).

Using a Boolean to turn using deltaTime on/off – Scrolling games work better with deltaTime – Single screen games seemed to prefer not using it.

Separating Update from Draw – The draw method, locks the canvas, draws the frame then unlocks the canvas again  – drawing the image to the screen. Most ‘GameThread‘ samples I saw ran update after locking the screen – Which suggests this is probably the best approach to take. But I went against the grain anyway – Running update before draw, in a separate method – I thought it looked cleaner & gave me more flexibility; though if it turns out that updating after locking the canvas is a better choice, I’ll move things back in the next update.

 


 

The replacement GameThread;

package com.tgt.bitshift;

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

/**
 * Created by SCOTT on 27/04/2015.
 */
public class GameThread extends Thread {

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

    //---

    // Surface holder that can access the physical surface
    private final SurfaceHolder surfaceHolder;
    // The actual view that handles inputs
    // and draws to the surface
    private final GameSurface gameSurface;

    //---

    // flag to hold game state
    private static boolean running;
    public void setRunning(boolean running) {
        GameThread.running = running;
    }

    //---

    private static Canvas canvas = null;

    //---

    public static final boolean useDeltaTime = true;
    public static double deltaTime = 1.0;

    //---

    public final static int TARGET_FPS = 60;
    private final static long OPTIMAL_TIME = 1000000000 / TARGET_FPS;

    //---

    private static long lastLoopTime = System.nanoTime();
    private static long now = System.nanoTime();
    private static long updateLength = now - lastLoopTime;
    private static double delta = updateLength / ((double)OPTIMAL_TIME);

    //---
    //---

    public GameThread(GameSurface inGameSurface) {
        super();
        this.surfaceHolder = inGameSurface.getHolder();
        this.gameSurface = inGameSurface;
    }

    public static void Reset(){
        deltaTime = 1;
        averageCounter = 0;
        sum = 0;
    }

    //---
    //---

    //Variable TimeStep - http://www.java-gaming.org/index.php?topic=24220.0
    @Override
    public void run() {
        Log.d(TAG, "Starting game loop");

        Reset();

        // keep looping round til the game ends
        while (running)
        {
            // work out how long its been since the last update, this
            // will be used to calculate how far the entities should
            // move this loop
            now = System.nanoTime();
            updateLength = now - lastLoopTime;
            lastLoopTime = now;

            if(useDeltaTime) {
                delta = updateLength / ((double)OPTIMAL_TIME);
                //deltaTime = delta;
                CalculateAverage(delta);
            }

            // update the game logic
            Update();

            // draw everyting
            //if(delta <= 1)
            Draw();

            // we want each frame to take 10 milliseconds, to do this
            // we've recorded when we started the frame. We add 10 milliseconds
            // to this and then factor in the current time to give
            // us our final value to wait for
            // remember this is in ms, whereas our lastLoopTime etc. vars are in ns.
            //try{Thread.sleep( (lastLoopTime-System.nanoTime() + OPTIMAL_TIME)/1000000 );}catch(Exception e){};
            try{surfaceHolder.wait( (lastLoopTime-System.nanoTime() + OPTIMAL_TIME)/1000000 );}catch(Exception ignored){}
            //ToastTimed(( (lastLoopTime-System.nanoTime() + OPTIMAL_TIME)/1000000 ));
            //do{}while(!wait);         
        }
    }
    
    //---
    //---

    private static double sum = 0;
    private static double averageCounter = 0;
    private static final double averageCount = TARGET_FPS ;
    private void CalculateAverage(double inValue){
        sum += inValue;
        averageCounter++;

        if(averageCounter >= averageCount){
            deltaTime = sum/(averageCounter+1.0);

            averageCounter = 0;
            sum = 0;
        }
    }

    //---
    //---

    private void Update() {
        // update game state
        this.gameSurface.update();
    }

    //---
    //---

    private void Draw() {

        //canvas = null;

        // try locking the canvas for exclusive pixel editing
        // in the surface
        try {

            canvas = this.surfaceHolder.lockCanvas();

            synchronized (surfaceHolder) {

                // render state to the screen
                // draws the canvas on the panel
                this.gameSurface.render(canvas);

            }

        } finally {

            // in case of an exception the surface is not left in
            // an inconsistent state
            if (canvas != null) {
                surfaceHolder.unlockCanvasAndPost(canvas);
            }

        } // end finally
    }

    //---
    //---

}

 


 

This has helped provide a much visually cleaner scrolling effect – At the expense of smoothness – Bit:Shift especially can experience jittering during gameplay – Especially noticeable on the GooglePlay version of the game, hardly ever occurring in the Amazon release – Something that needs to be fixed.

For the next step, I’m intending to give the ‘GameThread‘ a little space (well, maybe a small tweak) – focus instead more on what’s happening around that code – Are achievements, adverts, Google services, etc weighing too heavily on the system’s resources (They certainly need to be tidied and updated) – Are there still changes in the game code I can make to speed things up?

Hopefully one more push should get things in running nicely at last.

Advertisements