A Better Game Loop
Source code: http://silverlightrocks.com/Community/files/folders/slg101_tutorials/entry21.aspx
While working on my game, I found that especially when in full screen mode, if there are a lot of things in motion, the frames per second can decrease significantly. Now some of that I'm sure is because it is Alpha code and contains tons of debugging information. Still, in general, it is good to base your updates on the time since the last update, that way, your game will run at a consistent speed even if the frame rate is slower on a particular system, or based on how much you have going on.
So to improve our game loop, we can keep track of when we last called Storyboard.Begin and then when Completed fires, we can subtract this from the current time to get our elapsed time.
To make things a bit more reusable, we can do like we did with the KeyHandler class and create a new GameLoop class in the SLG101Utilities library.
Breaking Change: Code updated for breaking change in Silverlight 1.1 Alpha refresh where all storyboards now need to be named.
The GameLoop class will look like this:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace SilverlightGames101.Utilities
{
public class GameLoop
{
Canvas targetCanvas = null;
Storyboard storyboard;
DateTime lastUpdateTime = DateTime.MinValue;
TimeSpan elapsedTime;
public delegate void UpdateDelegate(TimeSpan ElapsedTime);
public event UpdateDelegate Update;
public void Attach(Canvas canvas)
{
targetCanvas = canvas;
storyboard = new Storyboard();
storyboard.SetValue<string>(Storyboard.NameProperty, "gameloop");
canvas.Resources.Add(storyboard);
lastUpdateTime = DateTime.Now;
storyboard.Completed += new EventHandler(storyboard_Completed);
storyboard.Begin();
}
public void Detach(Canvas canvas)
{
storyboard.Stop();
canvas.Resources.Remove(storyboard);
}
void storyboard_Completed(object sender, EventArgs e)
{
elapsedTime = DateTime.Now - lastUpdateTime;
lastUpdateTime = DateTime.Now;
if (Update!=null) Update(elapsedTime);
storyboard.Begin();
}
}
}
And the GameLoop object will fire an Update event which can then be handled by the game canvas.
We need to make some changes to the Page class to use this GameLoop instead of the one it was using. First, change the declaration of the gameLoop field to look like this:
GameLoop gameLoop = new GameLoop();
Then remove the gameLoop related code from the Page_Loaded method and replace it with the following:
gameLoop.Attach(this);
gameLoop.Update += new GameLoop.UpdateDelegate(gameLoop_Update);
And then, replace the gameLoop_Completed method with the following:
void gameLoop_Update(TimeSpan ElapsedTime)
{
if (keyHandler.IsKeyPressed(Key.A))
{
ship.RotationAngle -= rotationSpeed * ElapsedTime.TotalSeconds;
}
if (keyHandler.IsKeyPressed(Key.D))
{
ship.RotationAngle += rotationSpeed * ElapsedTime.TotalSeconds;
}
}
And since we are now multiplying the rotationSpeed by the total seconds since the last update, and the frame rate is about 60 frames per second, change the rotationSpeed field's value from 3 to 180. This will give us a rotation speed about the same as what we had before.
So this is a bit cleaner and a lot more reusable, and it gives the added benefit of keeping the game moving at a constant speed.