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.