3

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:

  1. Use onDraw() instead of gameModel drawing to canvas
  2. Don't use synchronize() on the view's holder
  3. Using time since last update to update next frame instead of advancing one frame each update() call.
  4. Remove Thread.sleep()
  5. 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.

:)

descuder
  • 31
  • 2

2 Answers2

0

Your observation is correct. In short, the power management in the N5 aggressively throttles the power down. If your finger is in contact with the screen, the clock speeds are increased to ensure that interactions are smooth. (For more, see my answer to this question.)

The best way to handle this is to recognize that you will need to drop frames sometimes. The "Record GL app" activity in Grafika uses a simple trick with Choreographer to do this. See also this article for some additional thoughts about game loops.

Community
  • 1
  • 1
fadden
  • 48,613
  • 5
  • 104
  • 152
-1

I think you should try to make your game more battery saving,darker the game(using background layout #000) more power for game , also you can use Intent flags like clear top to kill previous activity for saving battery , clearing ram(avoid out of memory)

but for your situation you can use wakelock

Community
  • 1
  • 1
alp
  • 1,643
  • 1
  • 18
  • 33
  • Killing previous activities has no effect on this, and (unless the previous app is determined to run in the background) will have no impact on battery life. Wake locks keep the device from falling asleep entirely; they don't prevent it from throttling down to a lower clock frequency. – fadden Jun 06 '14 at 18:17