1

The app I'm developing is a Flappy Bird clone. I'm using a surfaceView object in which I have a gameThread and inside of its run method I draw the various components of the game on the canvas.

Everything runs smoothly as long as I just draw Rects to represent the objects, but as soon as I added the first Drawables i noticed a little bit of a loss in smoothness. If I try to draw the background as a Drawable the game suffers very significant frame rate loss.

What I tried:

  • Using png and all different kinds of bitmap as assets
  • Resizing the asset to fit the canvas perfectly, thus avoiding a rescale

None of this had any tangible effect.

Basically:

  • If I only use drawRect: 60fps
  • If I draw the back with drawRect and the other components with drawable.draw(canvas): 57fps
  • If I draw everything (background included) with drawable.draw(canvas): 15fps

Somewhat relevant code:

public class CannonView extends SurfaceView
        implements SurfaceHolder.Callback {
    private CannonThread cannonThread; // controls the game loop
    private Drawable background;

    // constructor
    public CannonView(Context context, AttributeSet attrs) {
        super(context, attrs); // call superclass constructor
        getHolder().addCallback(this);
        background= ResourcesCompat.getDrawable(getResources(), R.drawable.background, null);
    }
    public void newGame() {
        background.setBounds(0,0, getScreenWidth(),getScreenHeight());
    }
    public void drawGameElements(Canvas canvas) {
        background.draw(canvas);
    }
    public void stopGame() {
        if (cannonThread != null)
            cannonThread.setRunning(false); // tell thread to terminate
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (!dialogIsDisplayed) {
            newGame(); // set up and start a new game
            cannonThread = new CannonThread(holder); // create thread
            cannonThread.setRunning(true); // start game running
            cannonThread.start(); // start the game loop thread
        }
    }
    private class CannonThread extends Thread {
        private SurfaceHolder surfaceHolder; // for manipulating canvas
        private boolean threadIsRunning = true; // running by default

        // initializes the surface holder
        public CannonThread(SurfaceHolder holder) {
            surfaceHolder = holder;
            setName("CannonThread");
        }

        // changes running state
        public void setRunning(boolean running) {
            threadIsRunning = running;
        }
        // controls the game loop
        @Override
        public void run() {
            Canvas canvas = null; // used for drawing
            while (threadIsRunning) {
                try {
                    // get Canvas for exclusive drawing from this thread
                    canvas = surfaceHolder.lockCanvas(null);
                    synchronized(surfaceHolder) {
                        drawGameElements(canvas);
                    }
                }
                finally {
                    if (canvas != null)
                        surfaceHolder.unlockCanvasAndPost(canvas);
                }
            }
        }
    }

}
Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
fasiume
  • 88
  • 8
  • Please add drawGameElements code in your question,so we can know which part of your code can be optimized. – aolphn Nov 05 '18 at 15:27
  • It is actually included, you can find it in line 15. I omitted the drawing of elements other than the background as, according to my tests, they cause only a fraction of the frame loss. – fasiume Nov 05 '18 at 15:43
  • Seems a bit strange. I recommend you pre-render your `Drawable` into a `Bitmap` (see [here](https://stackoverflow.com/a/46531354/6759241)), and then use that `Bitmap` for rendering, since it's hard to know how much work the `Drawable` is doing in `onDraw()`. – greeble31 Nov 05 '18 at 17:52
  • This helped a good deal, the game now reaches ~42 fps. Still, it feels like there should be no problem running at 60fps for a game that only includes 5 sprites total. – fasiume Nov 05 '18 at 19:57
  • @fasiume a couple thoughts: It could be doing alpha computations in software. `Bitmap.setHasAlpha(false)` might help. And, a professional would probably use OpenGL, for max sprite performance. That aside, I'm wondering if it's an interaction between the timing of `drawGameElements()` and the screen refresh rate. Just curious, how are you measuring fps? I assume you've determined that `drawGameElements()` executes 42 times a second, meaning it takes roughly 23ms from start to finish? – greeble31 Nov 06 '18 at 04:07
  • Using setHasAlpha doesn't help much. What I found to be effective is using Bitmap.copy(Bitmap.Config.RGB_565, true) for the bitmaps that don't need alpha, this change leaves me with ~56 fps. setHasAlpha has a detrimental effect in this case. The way I measure fps isn't the best but it shouldn't cause any problems, basically I measure totalElapsedTime/framesDrawn where frameDrawn is a variable that is increased in drawGameElements(). As the game always has the same amount of sprites drawn, if i take my measurement after >10 seconds it should e accurate enough to represent the average fps value – fasiume Nov 06 '18 at 16:32
  • @fasiume That's interesting. The highest framerate you reported was 60. Does that mean 60 is the max, even if `drawGameElements()` was a trivial function that could run 1,000,000 times per second? – greeble31 Nov 06 '18 at 17:21
  • @greeble31 I'm quite sure 60 is the max, leaving the draw and update functions basically empty still allows for max 60fps which, reading up on it seems to be the limit for most devices. – fasiume Nov 06 '18 at 19:30

1 Answers1

1

It seems apparent that the dominant cause of the low frame rate is background.draw(). Switching to a Bitmap improves this somewhat, probably since it cached the output of draw(), and because it can be used with Canvas functions that are guaranteed not to need scaling (e.g., drawBitmap( Bitmap, float, float, Paint))

You also found that switching to RGB_565 as an intermediate format improves performance quite a bit, presumably because it throws away the alpha. (Otherwise, I would've expected this to be somewhat slower, b/c the format has to be converted back to RGBA_8888 as it's blitted into the SurfaceView.)

It's also apparent that Android won't let you go over 60fps. This is almost certainly because lockCanvas() takes part in a triple buffering scheme that throttles the drawing rate, to prevent you from submitting frames that could never be displayed (due to your device's fixed screen refresh rate of 60Hz).

This leaves the question of why you don't get a full 60fps, but something close to it. If drawGameElements() takes the same amount of time to run each time, and it's less than 16ms, then lockCanvas() should be throttling you, and no frames should ever get dropped (60fps continuously). It seems likely that there is a burble in the thread scheduler or something, and every so often, the CannonThread does not execute quickly enough to provide the frame before the triple-buffering scheme needs to page-flip. In this event, the frame must be delayed until the next screen refresh. You might try increasing CannonThread's thread priority, removing any extra processing in drawGameElements() that doesn't absolutely need to happen on CannonThread, or closing other apps running on your device.

As mentioned, OpenGL is the standard way of getting max sprite performance for games like these, because it is able to offload many operations to hardware. You may be approaching the performance limit of a drawBitmap()-based game.

greeble31
  • 4,894
  • 2
  • 16
  • 30