Timer Control for Games


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
/// 
/// Static creation routine
/// 
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.

Handling Lost Devices

Before you start using that timer, you need to take care of something more pressing. To see the problem in action, change your project to run in release mode. You can do so by selecting Build, Configuration Manager.
Running the application now renders the fledgling game in full-screen mode. On the surface, everything should appear the same; however, you might notice quickly that with the window border no longer around, you can't close the application. Luckily, you are using the sample framework; otherwise, if you decided to minimize the window (for example, by using the Alt+Tab key combination to switch to the next application) or close the window, the game would throw an exception as soon as it lost focus. Even though you don't have this problem because you're using the sample framework, it's important to know what causes this.
When you are running the application in full-screen mode, Direct3D takes exclusive mode over the monitor that is being used to render the full screen image.
When you switch to a different application, that exclusive mode must be released to allow the other application to run, so the current application is minimized and the device is lost. Once a device is lost, you cannot render to it any longer, and trying to do so throws an exception (specifically the DeviceLostException).
Because this is a common problem that every game has to solve, you wouldn't want to clutter your rendering method with code to handle the situation, which is why the sample framework handles it for you. Look at the code snippet in Listing 5.4 for more information.
Listing 5.4. Handling Lost Devices
int result;
// Check the cooperative level to see if it's ok to render
if (!device.CheckCooperativeLevel(out result))
{
    if (result == (int)ResultCode.DeviceLost)
    {
        // The device has been lost but cannot be reset at this time.
        // So wait until it can be reset.
        System.Threading.Thread.Sleep(50);
        return;
    }
    // Other code snippets
}
// Other code snippets
// Show the frame on the primary surface
try
{
    device.Present();
}
catch (DeviceLostException)
{
    // Whoops, device is lost now
    State.IsDeviceLost = true;
}
catch (DriverInternalErrorException)
{
    // When DriverInternalErrorException is thrown from Present(),
    // the application can do one of the following:
    //
    // - End, with the pop-up window saying that the application cannot continue
    //   because of problems in the display adapter and that the user should
    //   contact the adapter manufacturer.
    //
    // - Attempt to restart by calling Device.Reset, which is essentially the
    //   same path as recovering from a lost device. If Device.Reset throws the
    //   DriverInternalErrorException, the application should end immediately
    //   with the message that the user should contact the adapter manufacturer.
    //
    // The framework attempts the path of resetting the device
    //
    State.IsDeviceLost = true;
}

You'll notice immediately that this method updates the variable IsDeviceLost on the State class. In this method, you check to see whether the device is currently lost, and if it is, you call the CheckCooperativeLevel method on the device. This call returns a Boolean value, true if the device is available and ready to use and false otherwise. Notice that an integer value is returned as an out parameter, which is the reason why the device is still not available.
After this call (assuming it returned false), you want to check the reason why the device is not available to render. The two likely reasons are that the device is still lost (the application still doesn't have focus) or that the application has regained focus, but the device hasn't been reset. If the device is still lost, there's nothing you can do right now, so you simply return from the method. However, if the device needs to be reset (which it always does after regaining focus), you simply call the Reset method, which should return the device back to the exclusive mode and be ready for rendering (the framework handles this for you).
Assuming you made it past that section, either the device was never lost to begin with or it is reset and ready to use now. Next, you are ready to render your scene. The framework calls the render method you have written and then makes the Present call. This call is the one that throws the exception when the device is lost, so after the render method is called, Present is called, wrapped in a try/catch block. Notice that the only exceptions caught are DeviceLostException and DriverInternalErrorException and the only action taken is to set the Boolean variable to TRue to start the check of the current cooperative level.

Adding Frame Rate Output

One more of the many things the sample framework does is keep track of the game's frame rate. Frame rate is something that most 3D games closely track because it deals directly with the performance of the game. It is simply the number of frames rendered in any given second, and the framework calculates it using the high-resolution timer discussed earlier in this chapter.
The code included on the CD only displays the frame rate in debug mode so it doesn't clutter the screen in full-screen mode. Therefore, the code in this section assumes this behavior as well. Add the following variable to your game engine class declarations:
#if (DEBUG)
private Direct3D.Font debugFont = null;
#endif

The first thing you'll notice here is that you've declared a new type of variable, the Direct3D.Font object. This object is used to render text onscreen as part of your scene.
Construction Cue

You'll also notice the 'alias' for the Direct3D namespace used here. It allows you to reference the items in the Direct3D namespace without having to type the full Microsoft.DirectX.Direct3D namespace (which is fully qualified). Using this alias is required for the Font class because more than one Font class exists, the one you just declared and another in the System.Drawing namespace. Make sure that you add the using alias at the top of your game engine code file:
using Direct3D = Microsoft.DirectX.Direct3D;

Now that the variable is declared, in your OnCreateDevice method add the following code to create a new font object:
#if (DEBUG)
debugFont = new Direct3D.Font(device, new System.Drawing.Font("Arial", 12));
#endif

The only parameters you need for this object are the rendering device and the font you want to render with. Using a default font is adequate for this scenario. Because the font object here is also a resource, make sure that you clean up that object in your OnDestroyDevice method. Add this code to that method now:
#if (DEBUG)
if (debugFont != null)
{
    // Clean up the font
    debugFont.Dispose();
    debugFont = null;
}
#endif

One thing you might notice on the font object is that it has two "special" methods for use when a device has just been reset or is about to be lost, OnDeviceReset and OnDeviceLost, respectively. You already have methods to handle when the device has just been reset and lost, so you want to call these font methods there. Add this section of code to the beginning of your OnResetDevice method to make sure that the font behaves correctly during a device reset:
#if (DEBUG)
// Make sure the debug font resets correctly
debugFont.OnResetDevice();
#endif

You want to handle the lost case as well, so add this section of code to the OnLostDevice method:
#if (DEBUG)
if (debugFont != null)
{
    // Make sure the debug font gets rid of any resources
    debugFont.OnLostDevice();
}
#endif

Now that your font is created, you can look at the implementation of the UpdateFrameStats method from the framework, which is called every frame and calculates the current frame rate. See the code in Listing 5.5 from the framework.
Listing 5.5. Calculating Frame Rate
private void UpdateFrameStats()
{
    // Keep track of frame count
    double time = FrameworkTimer.GetAbsoluteTime();
    State.LastStatsUpdateFrames++;

    if (time - State.LastStatsUpdateTime > 1.0)
    {
        float fps = (float)(State.LastStatsUpdateFrames /
             (time - State.LastStatsUpdateTime));
        State.CurrentFrameRate = fps;
        State.LastStatsUpdateFrames = 0;
        State.LastStatsUpdateTime = time;

        State.FrameStats = string.Format(State.StaticFrameStats,
            fps.ToString("f2",
            System.Globalization.CultureInfo.CurrentUICulture));
    }
}

Now that you know how the frame rate is calculated, the last thing you need to do is actually update your scene by rendering this text. Add this code to the end of your render method, directly before your EndScene call (but not inside the finally block).
#if (DEBUG)
// Show debug stats (in debug mode)
debugFont.DrawText(null, sampleFramework.FrameStats,
    new System.Drawing.Rectangle(2,0,0,0),
    DrawTextFormat.NoClip, unchecked((int)0xffffff00));
debugFont.DrawText(null, sampleFramework.DeviceStats,
    new System.Drawing.Rectangle(2,15,0,0),
    DrawTextFormat.NoClip, unchecked((int)0xffffff00));
#endif

Look at the prototype for the DrawText call; it is more or less self-explanatory. The prototype looks like this:
public System.Int32 DrawText ( Microsoft.DirectX.Direct3D.Sprite sprite ,
    System.String text , System.Drawing.Rectangle rect ,
    Microsoft.DirectX.Direct3D.DrawTextFormat format ,
    System.Drawing.Color color )

Because the first parameter is null, you are specifying that you want to draw directly onscreen. Sprites are used to render 2D images onto the screen, and you use them for rendering the UI of the game later. For now, however, because you are just rendering debug information, you want it rendered directly to the screen. The second parameter is the string you will be rendering, and the third parameter is the rectangle you want to render the text into. The client rectangle is perfect here, even though you wouldn't need to use it all. The last parameter is the color you want the text to be rendered. It really is as simple as that.


Comments

Popular Posts