Game Loop

摘要:Game Loop

 

Welcome
If you’re reading this document you are interested in developing Silverlight games. So am I!
My name is Nikola, and I’m a fan of Silverlight games, currently employed in Microsoft as a QA on the XAML parser for the Silverlight/WPF Designer in Visual Studio. All opinions expressed in this document are mine, and not the ones of my employer. Pay me a visit at http://nokola.com or http://blogs.msdn.com/nikola. I appreciate feedback of any kind! J
This document assumes you have a working knowledge of .NET, C#, Silverlight and XAML. If you don’t you are still invited to read it, and go to MSDN or search the Internet if you need more information about certain topic (or ask me).
 
The sample is not intended to replace this document, just augment it. As you read through you will find explanations about the various numbers in the sample’s UI and what it is supposed to do.
Note: A real-life game will probably have some more tweaks than the sample, such as FPS bumped up. You can find explanations about using FPS to better control animations further down in this document.
 
The source code demonstrates the methods described below.
What Are Some Things Important For A Game?
·Smooth animations
·Low CPU usage (allows more nice things to happen, such as AI, computations, more effects)
·Control over many objects of the screen
Why Are We Discussing Game Loops?
Choosing the right method for a game loop can make a difference from a sluggish to top performance game.
How do I know? I’ve been working and released two complete versions of the game Shock over the past year. I tuned and tuned the game loop (and other aspects of the game), and every time it got more performing I added some new animations and effects (such as the ball bouncing animations). The game loop made a huge difference.
In Silverlight, there are several ways to do game loops, each yielding different results.
Depending on your game:
·Some loops are better suitable for lots of “stock” (<Storyboard />) animations.
·Other loops are more suitable for custom-controlled per-frame animations.
·Other loops are suitable for lots of background computations (e.g. chess).
Side Note: I hope I finally fixed up Shock to be smooth and fast…please let me know! J
Methods
Note: in all the code samples below, GetImageLocation() returns the correct image vertical location for the current time.
Here are the 5 methods we’ll discuss, in no particular order yet:
1.       Dispatcher timer
The dispatcher timer calls a function on the UI thread of the application every X milliseconds.
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromMilliseconds(10);
_timer.Tick += new EventHandler(_timer_Tick);
_timer.Start();
 
void _timer_Tick(object sender, EventArgs e) {
Canvas.SetTop(imgTimer, GetImageLocation());
}
 
 
2.       Storyboard
This is the standard Silverlight animation (note the second “Storyboard as Trigger” method below). Not a game loop per se, it needs some supporting code to control the animation. You can setup the animation once and have it play without the need to control it on every frame.
<Storyboard x:Name="animTimer" RepeatBehavior="Forever" AutoReverse="True">
      <DoubleAnimation From="170" To="670" Duration="00:00:01.00"        
      Storyboard.TargetName="imgStoryboard"
Storyboard.TargetProperty="(Canvas.Top)" />
</Storyboard>
...
<Image x:Name="imgStoryboard" Canvas.Left="285" Canvas.Top="170" Source="normal.png" />
 
3.       Thread
The thread calls a function on the UI thread every X milliseconds. Similar to DispatcherTimer, except it allows us to do some computations as well and/or make use of multiple CPUs for computations. The test results below show that threads are significantly different than timers in some respects.
_thread = new Thread(new ThreadStart(ThreadProc));
_thread.Start();
void ThreadProc() {
     while (true) {
        Thread.Sleep(10);
        Dispatcher.BeginInvoke(ThreadUpdate);
     }
}
void ThreadUpdate() {
Canvas.SetTop(imgThread, GetImageLocation());
}
 
4.       CompositionTarget.Rendering
The application will update the animation on every frame drawn on screen. The difference in this method is that you don’t have direct control at runtime over the frequency of each frame and rely on the Silverlight runtime for that.
You can setup your control to have higher or lower FPS though (see http://msdn.microsoft.com/en-us/library/cc838147(VS.95).aspx) achieving some control before the Silverlight application is started.
The biggest benefit of this method is that you can achieve fine control over animating objects along a curved or algorithmically generated path. For example, modeling moving planets under gravity, or some kind of special non-linear force field is good candidate for CompositionTarget.Rendering. Controlling a Storyboard to achieve the same curved/algorithmic motion will be harder and probably not as performant.
CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);
void CompositionTarget_Rendering(object sender, EventArgs e) {
Canvas.SetTop(imgRendering, GetImageLocation());
}
 
5.       Storyboard as Trigger
This method is used by the Farseer physics engine as far as I know (might change by the time you read this doc). In this method, a Storyboard is used instead of a timer to call a function in code behind. As we’ll see from the performance measurements below, this method does not have significant advantages over DispatcherTimer (at least for Silverlight 3).
_sbTrigger = new Storyboard();
_sbTrigger.Duration = TimeSpan.FromMilliseconds(10);
_sbTrigger.Completed += new EventHandler(_sbTrigger_Completed);
_sbTrigger.Begin();
 
void _sbTrigger_Completed(object sender, EventArgs e) {
Canvas.SetTop(imgStoryboardTrigger, GetImageLocation());
      _sbTrigger.Begin(); // continue storyboard timer
}
Test Setup
The test compares all 5 methods outlined above.
The sample contains 5 balls moving side-by-side, each using a different method.
Each method is set-up to request a frame every 10 msec, except for the CompositionTarget.Rendering that works with 1000/<MaxFrameRate from HTML> msec.
For each method I capture this data:
·Average time between frames in milliseconds (“Avg:”)
·Minimum time between frames (“Min:”)
·Maximum time between frames (“Max:”)
·Stability (flakiness factorJ) – this is a graph that shows the history of the time between the frames. The top of the graph is 32 msec, the bottom is 0 msec. This means that if you see a line right in the middle (such as the one in CompositionTarget.Rendering), the time between frames is about 16 msec.
Flakiness is very important since for some games (Chess?) you may be OK frames being “doubled”. “Doubled” means that every second frame is coming 0 msec after the first, such as in the Thread method under 60 FPS and 10 msec Sleep(). For other games (e.g. space shooter) smoothness is a key, and you need to know then your frames come by so that the UI does not look “jumpy”.
 
To show the power of CompositionTarget.Rendering and show ability to control animations under different FPS (frame-per-second) rates, I also ran the sample at both 60 (default) and 300 FPS.
To change the FPS, you have to change your hosting HTML where you instantiate the Silverlight object:
<paramname="maxFrameRate" value="60" />
Test Results
MaxFrameRate Unset (Defaults to 60):
 
Analysis of the 60 FPS default case:
·DispatcherTimer, CompositionTarget.Rendering, and Storyboard as Trigger are pretty close to each other
o        CompositionTarget.Rendering is the smoothest (if you don’t believe from the picture, run the sample and see for yourself)
·Only Thread gets to 10 msec average (note that CompositionTarget.Rendering runs at 1000/60=16.6 msec)
o        Even though it hits the average, it has many “doubled” frames. It seems that the Thread method goes back and forth from 0 to 16 msec between frames
·No measureable method gets us consistently below 16 msec
o        1000/60=16.6 msec is the time between frames in order to get 60 FPS. It seems like the FPS setting controls how often dispatcher messages get processed as well.
o        I wrote “measureable” in italics above, because the default Storyboard looks smoother than 16 msec, although it’s unmeasured. Again, run the sample to see for yourself
·The Storyboard (second) ball, starts a little earlier than the other balls. Remember this for the next 5 minutes, because it’s important for seeing the connection between FPS and animation control.
MaxFrameRate=300:
 
Analysis of the 300 FPS case:
·The CompositionTarget.Rendering is super stable at 3 ms (1000/300) – pretty cool, since it allows fine control over animations and predictable behavior
·The DispatcherTimer and Storyboard as Timer can now achieve close to the requested time (11 msec average).
·The balls are aligned in the max 300 FPS version. The Storyboard ball starts at the same time as the other balls. What does it mean?
o        The time between dispatcher messages is now roughly 3 msec (based on FPS)
o        If you call storyboard.Begin() or Stop() from code-behind, it will take about 3 msec, not 16.6 msec as in the default case: this is very important for games where you have Storyboard-s that are controlled by timer on the back (e.g. in Shock).
 
With 60 FPS, the balls in Shock sometimes seem to pass through bricks. With 300 FPS, even though I set my timer to 10 msec, changing Storyboards is more responsive, and the balls no longer appear to pass through bricks.
·The thread is more stable now, ranging from 6 to 12 msec between frames.
·Storyboard as Trigger is more stable than DispatcherTimer under 300 FPS, although I wouldn’t say the difference is significant.
Additional Tips
I did other tests that are not included in the sample above, with these results:
·Handling CompositionTarget.Rendering many times per second could be expensive. It’s better to create about 10 or 15 msec timer under high FPS (300+) instead of handling CompositionTarget.Rendering. Shock 2 uses this method at the time of this writing (I updated it last week).
·When doing animation, animate transforms instead of Canvas.Top or Canvas.Left attached properties. In the sample that goes with this document I animate the attached properties since it’s simpler. But it runs faster to add a TranslateTransform to your object and animate that.
o        TranslateTransform-s are also hardware acceleration friendly in Silverlight 3

 


Method
Benefits
Drawbacks
When to use? (suggestions, not rules)
When NOT to use?
Used By
DispatcherTimer
Relatively stable
Min resolution of about ~15 msec
Slow moving objects; in conjunction with the Storyboard method to control animations
Whenever you have very fast moving objects: in this case use either the Storyboard method (which can be controlled from a DispatcherTimer or Storyboard as Trigger; or CompositionTarget.Rendering with high frame rate)
Shock v2 for SL 2
Storyboard
Smooth Silverlight runtime-controlled animation
No control over complex movements (e.g. move an object on a custom curve or a path that is generated in code behind based on other game states (e.g. planet positions or ships moving)
Whenever you have straight motion that is easy to model with Storyboard (e.g. doesn't change often and does not need special code-behind handling during the motion)
When you want more code-behind control of the motion (e.g. if you simulate gravity in space game): use CompositionTarget.Rendering instead
Shock
Thread
Consistent average frame-per-sec; Can make use of multiple processors/cores
Inconsistent stability in short intervals (2 frames)
Whenever you have lots of computations for the game or when you want to off-balance some computation to another processor/core
Whenever you need only 1 CPU or when you have fast animations; in most cases replace with another method
Raytracing sample on nokola.com
CompositionTarget.Rendering
Extremely stable, guaranteed once-per-frame
Fixed resolution of 1000/<frames-per-sec-specified-in-html> [msec]
For curved or complex motion that is generated as the game progresses (gravity or force field, or enemy ship behaviour);
When you have trivial, straight motion (e.g. for clouds in a game or monsters moving monotonously from left to right and back): in this case use Storyboard
Shock v2 for SL 3
Storyboard as Trigger
Same as DispatcherTimer; except under 300 FPS it's smoother
Same as DispatcherTimer
Use DispatcherTimer, since it's a little bit easier to set up
Same as DispatcherTimer
Farseer physics engine


Summary & Conclusion
Back to our initial question: which is the best method?
It depends on your game. If I had to choose one, I would go for Dispatcher Timer (or Storyboard as Trigger) and about 200 or 300 FPS, where I control Storyboard-s from code behind – for games needing fast animations. It’s really a mixture of several, isn’t it? J
For games that need a lot of custom animations (gravity or enemy changing behavior too often), I’d go for 100-300 FPS, using a lot of optimized code in CompositionTarget.Rendering.
For games that have slow animations and predictable animations (puzzle, chess, tetris), I’d go for the default 60 FPS and use DispatcherTimer or not use timer at all (just events) and Storyboard-s in code behind.
I hope this read has been helpful. If you liked it please let me know in the comments of my blog at http://blogs.msdn.com/nikola or send me an email at nokola@nokola.com. I appreciate your opinion as well – if you find any discrepancies or have a better way to do things, please publish them and send me a link.