Create a physics and render loop with Xamarin Forms and SkiaSharp
In the previous part we created a basic animation loop where the position is updated with the elapsed time. In this part we are going to extend the loop with a physics update. In Game Programming Patterns at the paragraph Play catchup and algorithm is described of taking fixed steps per update. The advantage of fixed steps per update is that the positions and velocities are updated in a predictable way. A step will always result in the same animation, the only thing that differs is the number of steps that are done per update because the time between updates varies depending on where the code runs on. This does require that the steps don't take more time then the update would take.
In this example we are going to run the physics steps at 100 steps per second or a single step is 0.01 seconds. This is about 3 times faster as 30 fps for the timer trigger. Because the time per step is smaller, also the distance travelled per step will be smaller. The advantage of this is that the amount of overlap in a collision with the wall for instance will be smaller. Also when applying forces, a smaller step often results in a more accurate trajectory as the force can change over time and with a smaller step you capture this change better.
public partial class MainPage : ContentPage { // Initialization and variable declarations removed. private bool TimerLoop() { var dt = _stopWatch.Elapsed.TotalSeconds; _stopWatch.Restart(); var fps = dt > 0 ? 1.0 / dt : 0; // When the fps is too low reduce the load by skipping if (fps > 0 && fps < 7) { Debug.WriteLine("Skipping Frame"); return _pageActive; } var width = canvasView.CanvasSize.Width - r; var height = canvasView.CanvasSize.Height - r; _remainingTime += dt; // If the remaining time is too large limit it to a smaller value if (_remainingTime > 0.133333) _remainingTime = 0.033333; while (remainingTime >= _dt) { // Update position based on velocity x += _dt * vx; y += _dt * vy; // Check collision with side of the screen and reverse velocity if ((x < r && vx < 0) || (x > width && vx > 0)) vx = -vx; if ((y < r && vy < 0) || (y > height && vy > 0)) vy = -vy; _remainingTime -= _dt; } _fpsAverage += fps; _fpsCount++; if (_fpsCount == _fpsWanted) { fps = _fpsAverage / _fpsCount; fpsLabel.Text = fps.ToString("N3") + " " + dt.ToString(); _fpsCount = 0; _fpsAverage = 0.0; } // Trigger the OnPainting event canvasView.InvalidateSurface(); return _pageActive; } private void OnPainting(object sender, SKPaintSurfaceEventArgs e) { var surface = e.Surface; var canvas = surface.Canvas; // Extra elapsed time since last stopwatch restart and the left over not processed time var dt = _stopWatch.Elapsed.TotalSeconds + _remainingTime; canvas.Clear(_fillColor); using (SKPaint skPaint = new SKPaint()) { skPaint.Style = SKPaintStyle.Fill; skPaint.IsAntialias = true; skPaint.Color = SKColors.Blue; skPaint.StrokeWidth = 10; // Update the position with the extra elapsed time distance based on the current velocity float xp = (float)(x + dt * vx); float yp = (float)(y + dt * vy); canvas.DrawCircle(xp, yp, r, skPaint); } }
When everything is working you should see the same blue circle bouncing around on the screen as in the previous tutorial. Note that sometimes the circle gets clipped with the wall a bit because we only check if the distance is less than the radius but it should be less as in the previous post.