Understanding the High-Resolution Timer


All games today require at least some semblance of a timer. Back in the "early days" of game development, using a timer wasn't considered important. Computer systems were still all relatively the same speed (slow), and any calculations that needed to be performed could be based on the number of frames that had passed. If the code was only ever going to run on a single computer system, or if every system it was running on was identical, this might be a valid way to perform these calculationsbut even in that case, it's not normally the best-case scenario.

Imagine the situation where you are designing a physical limit on a car. Does it make more sense to think "The maximum speed of the car is 250 units per 6 frames," or instead, "The maximum speed of the car is 250 miles per hour (0.7 miles per second)?" Most calculations for things like physics are based on values over time, so it makes more sense to actually use time.

Another reason to avoid using frame-based calculations concerns the vast differences in computing systems nowadays. Imagine developing your game on a 2GHz processor. Through some trial and error, you've got your car physics perfect. Now, you give your game to your two buddies, one of whom has a 1GHz machine, the other a 3GHz machine. The one with the slower machine complains that everything goes too slow; the other complains about how things move too fast to control. It's never a good idea to rely on processing speed for any important calculations. Even on identical systems, other running applications could affect the running speed of your application.

Construction Cue

The .NET runtime comes with a property, System.Environment.Tickcount, which you can use to calculate time. This property returns the number of ticks (milliseconds) that have elapsed since the computer was last restarted. At first glance, it probably looks like the perfect answer; however, it has a pretty glaring drawback, which is that the property isn't updated every millisecond.

How often the property is updated is often referred to as the resolution of the timer. In this case, the resolution of the tick-count property is on average 15 milliseconds (ms). If you access this property continuously in a loop, it returns the same value for 15ms before updating, and then it returns that new value for another 15ms before updating again. In modern computers that can perform unheard-of amounts of calculations per second, a 15ms resolution can cause your calculations to appear "jerky." No one wants to play a game like that.


Caution

If you decide that using the TickCount property is what you want to do, make sure you realize that it will return a signed integer value. Because the property is the number of ticks since the computer was started, if the computer is on for an extremely long time (say more than 25 days), you start getting negative numbers returned from the property. If you do not take this into account, it can mess up the formulas you're using. After an even longer period of time, the values "wrap" and go back to 0.


What you need here is a timer that has a much higher resolution. A resolution of 1ms would be perfect. The sample framework has such a timer built in, located in the dxmutmisc.cs code file. Because it is an important topic, I briefly discuss this timer now.

Because there is no high-resolution timer built into the .NET runtime, and you need it for your game, you need to use the DllImport attribute to call two particular Win32 APIs, QueryPerformanceFrequency and QueryPerformanceCounter. You see the declarations for these two external methods in the NativeMethods class, such as what appears in Listing 5.1.

Listing 5.1. Declaring External Functions
 [System.Security.SuppressUnmanagedCodeSecurity] // We won't use this maliciously [DllImport("kernel32")] private static extern bool QueryPerformanceFrequency( ref long PerformanceFrequency); [System.Security.SuppressUnmanagedCodeSecurity] // We won't use this maliciously [DllImport("kernel32")] private static extern bool QueryPerformanceCounter(ref long PerformanceCount); 

You'll notice that not only are you using the DllImport attribute, but you are also using the SuppressUnmanagedCodeSecurity attribute. Because you are calling a method that isn't controlled by the .NET runtime, in the default case it does a stack walk and ensures that your process has enough privileges to run unmanaged code (which is what the Win32 API calls are)and it performs this check every time you call this method. This security check is expensive and time consuming, and this attribute ensures that the check happens only once. Aside from that fix, this is a simple case where you declare a call into a Win32 API. Also notice a few instance variables in the FrameworkTimer class so that it can calculate the total time or the time elapsed since the last update:

 private static bool isUsingQPF; private static bool isTimerStopped; private static long ticksPerSecond; private static long stopTime; private static long lastElapsedTime; private static long baseTime; 

Here you can see that you want to store the time the timer has started, the amount of time that has elapsed, and the time that was stored at the last update. Because the high-resolution timer can have a varying number of ticks per second, you also want to store that as well. Finally, you store the actual state of the timer. Notice that all the variables are marked static. This move ensures that there is only one high-resolution timer per application domain. (You can read more about application domains in the .NET documentation.) With the state variables declared, you now need to initialize your data in the constructor for this class, as shown in Listing 5.2.

Listing 5.2. Initializing the High-Resolution Timer
 private FrameworkTimer() { } // No creation /// <summary> /// Static creation routine /// </summary> static FrameworkTimer() {     isTimerStopped = true;     ticksPerSecond = 0;     stopTime = 0;     lastElapsedTime = 0;     baseTime = 0;     // Use QueryPerformanceFrequency to get frequency of the timer     isUsingQPF = NativeMethods.QueryPerformanceFrequency(ref ticksPerSecond); } 

There are two things to see in the initialization here. First, there is a static constructor where the real initialization takes place, and second, the normal constructor is private. Because this class is going to contain only static methods, you don't want anyone to be able to create an instance of the class. The only initialization you need here is to determine the amount of ticks per second. If this function returns false, the system does not support a high-resolution timer. You could spend the time writing code to fall back to the less reliable TickCount property, but that work goes beyond the scope of this book. Now you add the code in Listing 5.3 to the timer code, which finishes up the timer class.

Listing 5.3. Implementation of the High-Resolution Timer
 public static void Reset() {     if (!isUsingQPF)         return; // Nothing to do     // Get either the current time or the stop time     long time = 0;     if (stopTime != 0)         time = stopTime;     else         NativeMethods.QueryPerformanceCounter(ref time);     baseTime = time;     lastElapsedTime = time;     stopTime = 0;     isTimerStopped = false; } public static void Start() {     if (!isUsingQPF)         return; // Nothing to do     // Get either the current time or the stop time     long time = 0;     if (stopTime != 0)         time = stopTime;     else         NativeMethods.QueryPerformanceCounter(ref time);     if (isTimerStopped)         baseTime += (time - stopTime);     stopTime = 0;     lastElapsedTime = time;     isTimerStopped = false; } public static void Stop() {     if (!isUsingQPF)         return; // Nothing to do     if (!isTimerStopped)     {         // Get either the current time or the stop time         long time = 0;         if (stopTime != 0)             time = stopTime;         else             NativeMethods.QueryPerformanceCounter(ref time);         stopTime = time;         lastElapsedTime = time;         isTimerStopped = true;     } } public static void Advance() {     if (!isUsingQPF)         return; // Nothing to do     stopTime += ticksPerSecond / 10; } public static double GetAbsoluteTime() {     if (!isUsingQPF)         return -1.0; // Nothing to do     // Get either the current time or the stop time     long time = 0;     if (stopTime != 0)         time = stopTime;     else         NativeMethods.QueryPerformanceCounter(ref time);     double absoluteTime = time / (double)ticksPerSecond;     return absoluteTime; } public static double GetTime() {     if (!isUsingQPF)         return -1.0; // Nothing to do     // Get either the current time or the stop time     long time = 0;     if (stopTime != 0)         time = stopTime;     else         NativeMethods.QueryPerformanceCounter(ref time);     double appTime = (double)(time - baseTime) / (double)ticksPerSecond;     return appTime; } public static double GetElapsedTime() {     if (!isUsingQPF)         return -1.0; // Nothing to do     // Get either the current time or the stop time     long time = 0;     if (stopTime != 0)         time = stopTime;     else         NativeMethods.QueryPerformanceCounter(ref time);     double elapsedTime = (double)(time - lastElapsedTime) /  (double)ticksPerSecond;     lastElapsedTime = time;     return elapsedTime; } public static bool IsStopped {     get { return isTimerStopped; } } 

This is a relatively simple implementation. Everything you need to know about the state of the timer you have here, including starting, stopping, and getting the elapsed time or the total time. Each of these properties returns a float value based in seconds; for example, 1.0f is exactly 1 second, and 1.5f is exactly a second and a half. With that, you have a generic high-resolution timer available for your games.



Beginning 3D Game Programming
Beginning 3D Game Programming
ISBN: 0672326612
EAN: 2147483647
Year: 2003
Pages: 191
Authors: Tom Miller

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net