4

I have spent some time learning how to create a 2D rendering game loop on Android anno 2016.

I would like achieve the following:

  • Smooth animations
  • Hardware accelerated
  • Lag-free (60 fps)
  • Using a normal Canvas
  • Simplicity (no OpenGL)

The myth about SurfaceView:

First of all there is several posts recommending SurfaceView. At first glance this seems like a good idea since it uses a seperate rendering thread, but it turns out that the Canvas returned from a SurfaceHolder cannot be hardware accelerated!! Using a SurfaceView with software rendering on a device with QuadHD (2560x1440) resolution is simply horribly inefficient.

Therefore my choice was to extend a basic View and override onDraw(). Calling invalidate() for each update.

Smooth animations:

My next challenge was smooth animations. Turns out reading System.nanoTime() inside onDraw() was a bad idea since it will not be called at excatly 1/60 seconds intervals creating jerky motions on my sprites. Therefore I have used the Choreographer to provide me with the frame time of each VSYNC. This works well.

Current progress:

I feel that I have come pretty close, but I still experience some occasional lagging on older devices. Memory usage is pretty low so I dont think GC is behind this... Seems like my callbacks miss/jumps aoad frame once in a while.

I will post my code and I'm looking forward to read you comments and suggestions.

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.support.v4.content.res.ResourcesCompat;
import android.util.AttributeSet;
import android.view.Choreographer;
import android.view.View;

public class MiniGameView extends View implements Choreographer.FrameCallback
{
    private final float mDisplayDensity;

    private long mFrameTime = System.nanoTime();

    private final Drawable mBackground;
    private final Drawable mMonkey;

    public MiniGameView(Context context)
    {
        this(context, null);
    }

    public MiniGameView(Context context, AttributeSet attrs)
    {
        super(context, attrs);

        mDisplayDensity = getResources().getDisplayMetrics().density;

        // Load graphics
        mBackground = ResourcesCompat.getDrawable(getResources(), R.drawable.background, null);
        mMonkey = ResourcesCompat.getDrawable(getResources(), R.drawable.monkey, null);

        Choreographer.getInstance().postFrameCallback(this);
    }

    // Receive time in nano seconds at last VSYNC. Use this frameTime for smooth animations!
    @Override
    public void doFrame(long frameTimeNanos)
    {
        mFrameTime = frameTimeNanos;

        Choreographer.getInstance().postFrameCallback(this);
        invalidate();
    }

    // Draw game here
    @Override
    protected void onDraw(Canvas canvas)
    {
        drawBackground(canvas);
        drawSprites(canvas);
    }

    private void drawBackground(Canvas canvas)
    {
        mBackground.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        mBackground.draw(canvas);
    }

    private void drawSprites(Canvas canvas)
    {
        double t = mFrameTime * 0.00000001;

        int width = canvas.getWidth();
        int height = canvas.getHeight();

        for(int i=0;i<8;i++)
        {
            double x = width * (1 + Math.sin(-0.181 * t)) * 0.5;
            double y = height * (1 - Math.cos(0.153 * t)) * 0.5;

            int size = (int)Math.round((80 + 40 * Math.cos(0.2 * t)) * mDisplayDensity);

            drawSprite(canvas, mMonkey, (int) x, (int) y, size, size);

            t += 0.8;
        }
    }

    private void drawSprite(final Canvas canvas, final Drawable sprite, int x, int y, int w2, int h2)
    {
        sprite.setBounds(x - w2, y - h2, x + w2, y + h2);
        sprite.draw(canvas);
    }

}

I have also created a systrace file.

Community
  • 1
  • 1
Ludde
  • 527
  • 1
  • 6
  • 12
  • 1
    try looking at this repo :) very simple example of a 2d game loop running 60 fps' https://github.com/matthewlim/MakingPaper – kimchibooty Apr 22 '16 at 20:56

1 Answers1

5

There really isn't a "myth" about SurfaceView. It is the best choice for high-resolution fast animation... but you have to use OpenGL ES. Canvas rendering on Surfaces -- SurfaceView, TextureView, whatever -- is not hardware-accelerated, and becomes increasingly expensive as pixel counts increase.

One useful feature of SurfaceView is that you can set the Surface to be a fixed size, and let the display hardware scale it up. For some types of games this can yield adequate performance. An example of what the scaling looks like is here; note the rendering uses GLES.

The general advice about game loops can be found in this appendix. You seem to be doing the right things. You may want to consider adding a frame-drop counter to see if your animation glitches correlate with dropped frames. (If consecutive times reported by Choreographer jump from 16.7ms to 33ms, you know you've dropped one.)

The best way to track down animation glitches is with systrace. The traces make it very easy to see exactly what all of your threads are doing, and establish cause and effect for pauses.

fadden
  • 51,356
  • 5
  • 116
  • 166
  • Thanks for your suggestions. I can confirm that onFrame will drop a frame once in a while and when it do the sprite animation will jerk! I have also updated the original post with a systrace. – Ludde Apr 23 '16 at 17:55
  • If the updates are working correctly you shouldn't notice much if you drop a single frame at 60fps. The "record GL app" demo in Grafika will reliably drop frames on certain devices (notably qcom chipsets, as they aggressively reduce clocks when your finger is not in contact with the screen), but you won't really notice a hiccup unless the stall is two or three frames long. Chrome is being difficult at the moment so I haven't looked at the trace. – fadden Apr 25 '16 at 05:16
  • Win10 Chrome can't handle the trace file, Linux Chrome can, go figure. It looks like things move along, though you can see a lurch at 2470ms when wpa_supplicant takes over core 0. Was this a noticeable glitch? Sometimes you just have to grab traces over and over until you get lucky and capture a big lurch so you have something to look at. – fadden Apr 25 '16 at 20:40
  • Looks like systrace viewing broke in Chrome 50... http://stackoverflow.com/questions/36865899/ – fadden Apr 27 '16 at 17:52
  • Workaround: open chrome://tracing in a new tab and click "load". (This and other ideas from the previous link.) – fadden Jul 21 '16 at 20:02