4.3 Timing and Animation
To do animation correctly, we need to keep track of the time. In particular, we need to measure the amount of time that elapses between frames of animation. If the frame rate is high, these time intervals between frames will be very short; therefore, we need a timer with a high level of accuracy.
4.3.1 The Performance Timer
For accurate time measurements, we use the performance timer (or performance counter). To use the Win32 functions for querying the performance timer, we must #include <windows.h>.
The performance timer measures time in units called counts. We obtain the current time value, measured in counts, of the performance timer with the QueryPerformanceCounter function like so:
__int64 currTime; QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
Observe that this function returns the current time value through its parameter, which is a 64-bit integer value.
To get the frequency (counts per second) of the performance timer, we use the QueryPerformanceFrequency function:
__int64 countsPerSec; QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
Then the number of seconds (or fractions of a second) per count is just the reciprocal of the counts per second:
mSecondsPerCount = 1.0 / (double)countsPerSec;
Thus, to convert a time reading valueInCounts to seconds, we just multiply it by the conversion factor mSecondsPerCount
valueInSecs = valueInCounts * mSecondsPerCount;
The values returned by the QueryPerformanceCounter function are not particularly interesting in and of themselves. What we do is get the cur rent time value using QueryPerformanceCounter, and then get the current time value a little later using QueryPerformanceCounter again. Then the time that elapsed between those two time calls is just the difference. That is, we always look at the relative difference between two time stamps to measure time, not the actual values returned by the performance counter. The following better illustrates the idea:
2 QueryPerformanceCounter((LARGE_INTEGER * ) & A);
3
4 /* Do work */
5
6 __int64 B = 0 ;
7 QueryPerformanceCounter((LARGE_INTEGER * ) & B);
8
So it took (B−A) counts to do the work, or (B−A)*mSecondsPerCount seconds to do the work.
Note | MSDN has the following remark about QueryPerformanceCounter: “On a multiprocessor computer, it should not matter which processor is called. However, you can get different results on different processors due to bugs in the basic input/output system (BIOS) or the hardware abstraction layer (HAL).” You can use the SetThreadAffinityMask function so that the main application thread does not get switched to another processor. |
4.3.2 Game Timer Class
In the next two sections, we will discuss the implementation of the following GameTimer class.
2 {
3 public :
4 GameTimer();
5
6 float getGameTime() const ; // in seconds
7 float getDeltaTime() const ; // in seconds
8
9 void reset(); // Call before message loop.
10 void start(); // Call when unpaused.
11 void stop(); // Call when paused.
12 void tick(); // Call every frame.
13
14 private :
15 double mSecondsPerCount;
16 double mDeltaTime;
17
18 __int64 mBaseTime;
19 __int64 mPausedTime;
20 __int64 mStopTime;
21 __int64 mPrevTime;
22 __int64 mCurrTime;
23
24 bool mStopped;
25 };
The constructor, in particular, queries the frequency of the performance counter. The other member functions are discussed in the next two sections.
2 : mSecondsPerCount( 0.0 ), mDeltaTime( - 1.0 ), mBaseTime( 0 ),
3 mPausedTime( 0 ), mPrevTime( 0 ), mCurrTime( 0 ), mStopped( false )
4 {
5 __int64 countsPerSec;
6 QueryPerformanceFrequency((LARGE_INTEGER * ) & countsPerSec);
7 mSecondsPerCount = 1.0 / ( double )countsPerSec;
8 }
Note | The GameTimer class and implementations are in the GameTimer.h and GameTimer.cpp files, which can be found in the Common directory of the sample code. |
4.3.3 Time Elapsed between Frames
When we render our frames of animation, we need to know how much time has elapsed between frames so that we can update our game objects based on how much time has passed. Computing the time elapsed between frames proceeds as follows: Let ti be the time returned by the performance counter during the ith frame and let ti−1 be the time returned by the performance counter during the previous frame. Then the time elapsed between the ti−1 reading and the ti reading is Δt = ti− ti−1. For real-time rendering, we typically require at least 30 frames per second for smooth animation (and we usually have much higher rates); thus, Δt = ti− ti−1 tends to be a relatively small number.
The following code shows how Δt is computed:
2 {
3 if ( mStopped )
4 {
5 mDeltaTime = 0.0 ;
6 return ;
7 }
8
9 // Get the time this frame.
10 __int64 currTime;
11 QueryPerformanceCounter((LARGE_INTEGER * ) & currTime);
12 mCurrTime = currTime;
13
14 // Time difference between this frame and the previous.
15 mDeltaTime = (mCurrTime - mPrevTime) * mSecondsPerCount;
16
17 // Prepare for next frame.
18 mPrevTime = mCurrTime;
19 // Force nonnegative. The DXSDK's CDXUTTimer mentions that if the
20 // processor goes into a power save mode or we get shuffled to
21 // another processor, then mDeltaTime can be negative.
22 if (mDeltaTime < 0.0 )
23 {
24 mDeltaTime = 0.0 ;
25 }
26 }
27
28 float GameTimer::getDeltaTime() const
29 {
30 return ( float )mDeltaTime;
31 }
The function tick is called in the application message loop as follows:
2 {
3 MSG msg = { 0 };
4
5 mTimer.reset();
6
7 while (msg.message != WM_QUIT)
8 {
9 // If there are Window messages then process them.
10 if (PeekMessage( & msg, 0 , 0 , 0 , PM_REMOVE ))
11 {
12 TranslateMessage( & msg );
13 DispatchMessage( & msg );
14 }
15 // Otherwise, do animation/game stuff.
16 else
17 {
18 mTimer.tick();
19
20 if ( ! mAppPaused )
21 updateScene(mTimer.getDeltaTime());
22 else
23 Sleep( 50 );
24
25 drawScene();
26 }
27 }
28 return ( int )msg.wParam;
29 }
2 {
3 __int64 currTime;
4 QueryPerformanceCounter((LARGE_INTEGER * ) & currTime);
5
6 mBaseTime = currTime;
7 mPrevTime = currTime;
8 mStopTime = 0 ;
9 mStopped = false ;
10 }
4.3.4 Game Time
Another time measurement that can be useful is the amount of time that has elapsed since the application start, not counting paused time; we will call this game time. The following situation shows how this could be useful. Suppose the player has 300 seconds to complete a level. When the level starts, we can get the time tstart, which is the time elapsed since the application started. Then after the level has started, we can check the time t since the application started every so often. If t − tstart > 300s (see Figure 4.7) then the player has been in the level for over 300 seconds and loses. Obviously in this situation we do not want to count any time the game was paused against the player.
![](https://i-blog.csdnimg.cn/blog_migrate/69d1b493de3369a5446d9bb54bc12e8f.jpeg)
Figure 4.7: Computing the time since the level started. Note that we choose the application start time as the origin (zero), and measure time values relative to that frame of reference.
Another application of game time is when we want to animate a quantity as a function of time. For instance, suppose we wish to have a light orbit the scene as a function of time. Its position can be described by the parametric equations:
![](https://i-blog.csdnimg.cn/blog_migrate/1e76c39b30235930966c73878ae6fa4a.jpeg)
Here t represents time, and as t (time) increases, the coordinates of the light are updated so that it moves in a circle with radius 10 in the y = 20 plane. Chapter 5 shows a sample program that animates the camera by describing its position as a function of time. For this kind of animation, we also do not want to count paused time; see Figure 4.8.
![](https://i-blog.csdnimg.cn/blog_migrate/8df73762ce957c86427479907abb1664.jpeg)
Figure 4.8: If we paused at t1 and unpaused at t2, and counted paused time, then when we unpause, the position will jump abruptly from p(t1) to p(t2).
To implement game time, we use the following variables:
__int64 mPausedTime;
__int64 mStopTime;
As we saw in §4.3.3, mBaseTime is initialized to the current time when reset was called. We can think of this as the time when the application started. In most cases, you will only call reset once before the message loop, so mBaseTime stays constant throughout the application’s lifetime. The variable mPausedTime accumulates all the time that passes while we are paused. We need to accumulate this time, so we can subtract it from the total running time in order to not count paused time. The mStopTime variable gives us the time when the timer is stopped (paused); this is used to help us keep track of paused time.
Two important methods of the GameTimer class are stop and start. They should be called when the application is paused and unpaused, respectively, so that the GameTimer can keep track of paused time. The code comments explain the details of these two methods.
2 {
3 // If we are already stopped, then don't do anything.
4 if ( ! mStopped )
5 {
6 __int64 currTime;
7 QueryPerformanceCounter((LARGE_INTEGER * ) & currTime);
8
9 // Otherwise, save the time we stopped at, and set
10 // the Boolean flag indicating the timer is stopped.
11 mStopTime = currTime;
12 mStopped = true ;
13 }
14 }
15 void GameTimer::start()
16 {
17 __int64 startTime;
18 QueryPerformanceCounter((LARGE_INTEGER * ) & startTime);
19
20 // Accumulate the time elapsed between stop and start pairs.
21 //
22 // |<-------d------->|
23 // ---------------*-----------------*------------> time
24 // mStopTime startTime
25
26 // If we are resuming the timer from a stopped state...
27 if ( mStopped )
28 {
29 // then accumulate the paused time.
30 mPausedTime += (startTime - mStopTime);
31
32 // Since we are starting the timer back up, the current
33 // previous time is not valid, as it occurred while paused.
34 // So reset it to the current time.
35 mPrevTime = startTime;
36
37 // no longer stopped...
38 mStopTime = 0 ;
39 mStopped = false ;
40 }
41 }
2 {
3 // If we are stopped, do not count the time that has passed since
4 // we stopped.
5 //
6 // ----*---------------*------------------------------*------> time
7 // mBaseTime mStopTime mCurrTime
8
9 if ( mStopped )
10 {
11 return ( float )((mStopTime - mBaseTime) * mSecondsPerCount);
12 }
13
14 // The distance mCurrTime - mBaseTime includes paused time,
15 // which we do not want to count. To correct this, we can subtract
16 // the paused time from mCurrTime:
17 //
18 // (mCurrTime - mPausedTime) - mBaseTime
19 //
20 // |<-------d------->|
21 // ----*---------------*-----------------*------------*------> time
22 // mBaseTime mStopTime startTime mCurrTime
23 else
24 {
25 return ( float )
26 (((mCurrTime - mPausedTime) - mBaseTime) * mSecondsPerCount);
27 }
28 }