I am making a game and all is going well except for the game loop. I am using a SurfaceView and drawing 2D Sprites (Bitmaps). Currently the game is a ship moving through an asteroid field. The ship stays in the center of the screen and the phone is tilted in either direction to move the the asteroids (asteroids change location instead of player). New asteroids spawn as old ones fall off the screen creating an infinite asteroid field feeling.
Running on my Nexus 5, I noticed that after about 3 seconds, the asteroids moving down the screen became choppy, despite my game loop set to run at 60fps. Here was the code:
@Override
public void run() {
Canvas canvas;
long startTime;
long timeTaken;
long sleepTime;
while (running) {
canvas = null;
//Update and Draw
try {
startTime = System.nanoTime();
canvas = gameView.getHolder().lockCanvas();
//Update
gameModel.update(gameController.getPlayerShots(), gameController.getCurrentTilt());
//Game Over?
if (gameModel.isGameOver()) { running = false; }
//Draw
synchronized (gameView.getHolder()) { gameModel.drawToCanvas(canvas); }
}
finally {
if (canvas != null) {
gameView.getHolder().unlockCanvasAndPost(canvas);
}
}
//Sleep if needed
timeTaken = System.nanoTime() - startTime;
sleepTime = FRAME_TIME - timeTaken;
try {
if (sleepTime > 0) {
sleep(Math.abs((int)sleepTime/1000000), Math.abs((int)sleepTime % 1000000));
}
}
catch (InterruptedException e) {}
}
//Load menu after game over
if (gameModel.isGameOver()) {
gameController.launchContinueMenu();
}
}
I thought the problem may be that my model was doing the drawing to canvas and that I wasn't using my surfaceview's onDraw() method. Refactored to use onDraw() and same result as before. I printed out the sleep time of each frame and my thread was consistently sleeping for 5-10ms (16.667ms in a frame), so my nexus was not short on computing power.
I read on stackoverflow that I shouldn't use synchronize() on the view holder. Still no luck.
Then I read again on stackoverflow that using Thread.sleep() may be causing the issue because it is not always accurate. I did a major refactor as shown below.
@SuppressLint("WrongCall")
@Override
public void run() {
Canvas canvas;
while (running) {
canvas = null;
//Update and Draw
try {
canvas = gameView.getHolder().lockCanvas();
//Calc and smooth time
long currentTimeMS = SystemClock.uptimeMillis();
double currentDeltaTimeMS;
if (lastTimeMS > 0) {
currentDeltaTimeMS = (currentTimeMS - lastTimeMS);
}
else {
currentDeltaTimeMS = smoothedDeltaTimeMS; // just the first time
}
avgDeltaTimeMS = (currentDeltaTimeMS + avgDeltaTimeMS * (avgPeriod - 1)) / avgPeriod;
// Calc a better aproximation for smooth stepTime
smoothedDeltaTimeMS = smoothedDeltaTimeMS + (avgDeltaTimeMS - smoothedDeltaTimeMS) * smoothFactor;
lastTimeMS = currentTimeMS;
//Update
gameModel.update(smoothedDeltaTimeMS / 1000.0d, gameController.getCurrentTilt());
//Game Over?
if (gameModel.isGameOver()) { running = false; }
//Draw
// synchronized (gameView.getHolder()) {
// gameModel.drawToCanvas(canvas);
gameView.onDraw(canvas);
// }
}
//Release canvas
finally {
if (canvas != null) {
gameView.getHolder().unlockCanvasAndPost(canvas);
}
}
}
//Load menu after game over
if (gameModel.isGameOver()) {
gameController.launchContinueMenu();
}
}
This new approach should just draw as fast as possible using delta time to draw next frame instead of a set increment as before. Still no luck.
I changed my bitmap locations from Rect's to a new positions class I created that uses doubles instead of ints to simulate sub-pixel movement. Still no luck.
Recap of things I've tried:
- Use onDraw() instead of gameModel drawing to canvas
- Don't use synchronize() on the view's holder
- Using time since last update to update next frame instead of advancing one frame each update() call.
- Remove Thread.sleep()
- Sub-pixel movement
After all this, I noticed that the game run fine while the phone was charging. 3 seconds after I unplug the phone, the game becomes choppy again. Then I noticed that if I tap the screen when the game becomes choppy, everything smoothes out again for another 3 seconds. I'm guessing that there is some kind of battery saving feature that is affecting my SurfaceView performance? What is going on here, and how can I disable it? This is quite maddening...
I tested out my first game loop again out of curiosity (1st code block) and tapped the screen while playing and everything ran smoothly. Man... all that work adding complexity for nothing. I guess it's a good learning experience. Any ideas to how I can keep my SurfaceView running at full speed? Help is greatly appreciated.
:)