Create a game loop with Xamarin Forms and SkiaSharp

In the previous part we created a basic Xamarin Forms application with a SkiaSharp canvas to draw on. In this part we are going to extend the application with animation. To animate we need to have a way to update the screen with smooth motion. The screen will be redrawn several times per second and on each redraw a shape is moved on the screen suggesting animation. To have a smooth animation we need to know how time has elapsed between two render frames. A DeviceTimer will be used to call an update and draw method every couple of milliseconds. The DeviceTimer triggers in an unpredictable way with an inaccuracy of a couple of ms. To measure the elapsed time more accurately we use the StopWatch class. See Xamarin Forms SkiaSharp documentation for more details.

One way to animate many objects is through the game loop or animation loop. In this part we are investigating the basic algorithm described in Game Programming Patterns The first way is the based on getting the elapsed time for each frame and update every object's position and then render. To get the frames a device timer is used which triggers a method to be called each frame. Then with the stop watch the exact elapsed time is calculated and this is used to update the position of the circle with its constant velocity.

    public partial class MainPage : ContentPage
    {
        private readonly Stopwatch _stopWatch = new Stopwatch();
        private double _fpsAverage = 0.0;
        private const double _fpsWanted = 30.0;
        private int _fpsCount = 0;
        private SKColor _fillColor = new SKColor(20, 20, 40);
        private double x;
        private double y;
        private double vx;
        private double vy;
        private int r = 50;        

        protected override void OnAppearing()
        {
            base.OnAppearing();

            Init();
        }
		
        private void Init()
        {
            var ms = 1000.0/_fpsWanted;		
            var ts = TimeSpan.FromMilliseconds(ms);

            // Create a timer that triggers roughly every 1/30 seconds
            Device.StartTimer(ts, TimerLoop);
			
            // Initialize position
            x = 300.0;
            y = 200.0;

            // Initialize velocity
            vx = 100.0;
            vy = 200.0;			
        }		
	
        private bool TimerLoop()
        {
            // Get the elapsed time from the stopwatch because the 1/30 timer interval is not accurate and can be off by 2 ms
            var dt = _stopWatch.Elapsed.TotalSeconds;

            // Restart the time measurement for the next time this method is called
            _stopWatch.Restart();

            var width = canvasView.CanvasSize.Width -r;
            var height = canvasView.CanvasSize.Height -r;

            // Update position based on velocity and the delta time
            x += dt * vx;
            y += dt * vy;

            // Check collision with side of the screen and reverse velocity
            // We also use the velocity component to see if the circle is moving in the direction of the boundary
            // Otherwise it could try to reverse the direction again while the circle is moving away from the wall.
            if ( (x < r && vx < 0) || (x > width && vx > 0) )
                vx = -vx;

            if ( (y < r && vy < 0) || (y > height && vy > 0) )
                vy = -vy;

            // Calculate current fps
            var fps = dt > 0 ? 1.0 / dt : 0;

            // When the fps is too low reduce the load by skipping the frame
            if (fps < _fpsWanted/2)
                return _pageActive;

            // Calculate an averaged fps
            _fpsAverage += fps;
            _fpsCount++;

            if (_fpsCount == 20)
            {
                fps = _fpsAverage / _fpsCount;
                fpsLabel.Text = fps.ToString("N3", CultureInfo.InvariantCulture);

                _fpsCount = 0;
                _fpsAverage = 0.0;
            }

            // Trigger the redrawing of the view
            canvasView.InvalidateSurface();

            return _pageActive;
        }

        private void OnPainting(object sender, SKPaintSurfaceEventArgs e)
        {
            var surface = e.Surface;
            var canvas = surface.Canvas;

            canvas.Clear(_fillColor);

            // Draw a circle
            using (SKPaint skPaint = new SKPaint())
            {
                skPaint.Style = SKPaintStyle.Fill;
                skPaint.IsAntialias = true;
                skPaint.Color = SKColors.Blue;
                skPaint.StrokeWidth = 10;

                canvas.DrawCircle((float)x, (float)y, r, skPaint);
            }
        }		
	

When everything is working you should see a blue circle bouncing around on the screen. Note that sometimes the circle gets clipped with the wall a bit because we only check if the distance is less than the radius. To prevent clipping we need to find the time of contact with the wall and then calculate where the circle would need to be.