Game programming under Windows using CDX
Abstract: Maintaining constant speed of a game on different computers
Author:
Peter Eichler (mailto:peichler@nikocity.de)
The reader should have average to good knowledge about programming under Windows, especially using the programming language C++. He also should have a basic understanding of Direct X and the free class library CDX. This abstract refers to the CDX library version 2.4. The user should also have a basic understanding on how a game and a Windows program works internally, since this abstract does not discuss the theory of game programming or Windows programming itself.
In this abstract, the term game will refer to all computer games using animated graphics such as sprites drawn in real time. Although this abstract will talk mostly only about 2D based games, the discussed principles can be used for 3D based games also. This abstract does not refer to games, where real time graphics is not at high priority or simply not present, such as chess games or any kind of board games or turn based games. Please note, that this abstract is no replacement for the description of the Windows API.
Current PCs consist of a variety of hardware components, each different in quality, price and speed. For instance, modern microprocessors may vary in speed by roughly 250% (considering 400 MHz as low end processors and 1 GHz as high end). Whereas in most cases the user is happy that a given software will run as fast as possible on his computer, a good game should run with the very same speed, regardless what computer will be used (except that the computer should meet the minimum requirements for the game). Otherwise the game will probably be unplayable on very fast computers.
So how can a computer game achieve this? The magic words in this case are of course timing, frame rates and synchronization.
Under Windows, a usual game loop will mostly do its work after the same scheme. First, it should process any messages (if present) received by the game window (please note, that even a Direct X full screen game has its own window, although this is completely invisible for the user). Then it should check if the game is still the active application and if so, the game may render the current frame and do all other necessary stuff, e.g. checking input from game devices. If the game is not the active application, the game should simply wait for a message to retrieve. In this way, the game consumes no additional computer resources and the computer is still usable for other tasks. If the current frame or scene was rendered successfully, everything will start all over. Let us have a look to such a loop, where the window messages will be processed and the scene gets rendered:
for(;;)
{
MSG msg;
if(PeekMessage(&msg, NULL, 0, 0,
PM_NOREMOVE)) // Do we have a message present?
{
if(!GetMessage(&msg, NULL, 0, 0
)) // Can we process the
message?
return msg.wParam; // No? Return to Windows!
if(msg.message == WM_QUIT) // Check for a quit message
break;
TranslateMessage(&msg); // The usual Windows
stuff...
DispatchMessage(&msg);
}
if(fGameIsActive) // If the game is active either
RenderScene(); // enter the game loop or
else
WaitMessage(); // wait to become active again
}
For the sake of readability I have left out all
initialization of Direct X / CDX, the opening of the game window and the window
callback procedure. Also note that the unknown variable in this example, fGameIsActive,
is set in a different location (usually in the window callback procedure,
processing the message WM_ACTIVATE_APP).
As we can see here, the game will run as fast as possible, when no messages getting processed and the game is active. In this case the loop would reduce (theoretically) itself to a mere:
for(;;)
{
RenderScene();
}
Since this is not what we want, a more sophisticated solution should be found.
In fact there are countless ways to solve this problem, but the abstract will discuss the two most common approaches:
In this case a constant number of frames will be rendered every second. This technique is very similar to a cartoon or movie, where several still pictures combined make the illusion of movement.
To have smooth movement, the number of frames per second must not change over time and the frame rate itself has to be high enough to deceive the human eye (some scientists state, that the human eye can resolve only 24 pictures/second for moving objects, so the frame rate should be higher to fool even very sensitive humans, if possible, try a rate of 30 frames/second or even higher).
The speed of movement (velocity) – e.g. for a sprite or other moving objects – is therefore often described in pixels per frame, instead of pixels per second, although you can derive one from another.
To achieve constant frame rates, one second is divided in many equal sized time slices and the rendering of each frame starts at the beginning of each time slice. The game has to guarantee, that the rendering of each frame takes no longer than one time slice, otherwise the constant frequency of frames will be broken. To calculate the maximum possible frame rate for a game in this case, the amount of time needed to render the most complex scene – the worst case – has to be measured. From this the number of frames per second can be calculated. For example, if the most complex scene in a game needs 25 milliseconds to render, a maximum of 40 frames per second can be achieved (1 second = 1000 milliseconds / 25 milliseconds = 40). Of course you get better frame rates on faster computers but this will not help for slower computers. So always calculate the frame rate on the slowest supported computer system by your game (minimum requirement).
On the other hand, if the game does not need the full time slice to render a frame, this spare time will not be used by the game and either wasted in a busy loop or handed over to the operating system, so other processes can use this amount of time. Although really sophisticated game engines can even use this spare time for other tasks than rendering, this technique will not be discussed in this abstract.
This approach is rather easy to implement, is reliable and looks good (very smooth, the higher the frame rate, the better). The game will have the very same frame rate on every computer matching at least the minimum requirements.
Spare time on faster computers will not be used to increase the frame rate (not the speed). This resource will be wasted. If the game was designed to run on a slow computer, it will always run like using a slow computer. Additional power will not be used.
The best frame rate is not easy to find. Higher frame rates may require faster computers and reduces the time slice for each frame. But the time slice must not be too small; otherwise a complex scene may not get rendered in time.
In this case the game will render each frame as fast as possible, not relying on constant time slices. Since complex scenes take longer to render than less complex scenes, the frame rate may vary heavily, depending on what the game has to do.
To avoid stuttering or other signs of speed difference, the game has to measure the amount of time each frame needed to be rendered and adapt the speed of all moving objects accordingly. This sounds more difficult than it is.
The speed of movement is therefore often described in pixels per second, instead of pixels per frame. In this case you cannot derive one from another.
To achieve dynamic frame rates, the time needed to render a frame will be calculated. This is simply done by getting some kind of a time stamp at the beginning of rendering the frame, and a time stamp after rendering the frame. The difference is the amount of time the frame needed to be rendered. Since the game knows how long it took to render the last frame and it also knows the speed of an object as pixel per second, it can therefore calculate the amount of pixels to move the object in the current frame. For example, if an object moves 200 pixels per second, and the last frame took 15 milliseconds to render, the object has (virtually) moved 3 pixels in this time period or delta (200 pixels / 1 second * 15 milliseconds = 200 / 1000 * 15 = 3 pixels). So if the current frame is rendered, the object appears now 3 pixels away from the old position. The fraction of the real movement has to be calculated every frame again for every object.
The maximum possible frame rate will be achieved on every computer, although the game is running at the same speed (not frame rate) on different systems. The implementation is not much more complex than the first approach with constant frame rates. So the full power of the computer system will be used.
Sadly, this approach is not the right one for every type of game. If the fraction of movement of an object is calculated, the result is mostly not an integral number (e.g. the object has to be moved by 3.226 pixels). Due to rounding problems, an object may jump by one pixel. Even worse, this number changes from frame to frame. In fact, the speed of an object is constant over a period of time, but not from frame to frame. This means, that if an object is moving with a constant speed, this will not appear so, because the frame rate is not constant and therefore not the movement per frame. For objects, which accelerate or slow down, this is mostly not visible, because the speed of the object is changing anyway. But for objects with a constant speed, the movement appears anything else than smooth.
This kind of solution is really great for games where nearly the whole content of the screen is changing for every frame and/or the display hardware can smooth or filter objects. This is true for nearly every 3D ego shooter or similar games. For 2D bitmap based games or for games, where only few objects are moving, or where you have smooth a scrolling background (classic side scroller or arcade games) the result is not smooth and therefore it is not advisable to use this approach.
As we can see, exact measurement of time is the central point in every of these solutions. This abstract will discuss in the next chapters all alternatives of time measurement, timers and counters supported by Windows, while explaining the first solution of constant frame rates. The explanation of dynamic frame rates will use this knowledge and will only show up the differences to avoid double explanations.
As explained, constant frame rates means, that the game will render a fixed number of frames per second, regardless of the speed of the computer and the complexity of the scene. But how do we get the game to render this fixed number of frames per second? Think of the clock frequency of a microprocessor. Fortunately, Windows has several ways to offer us clocked signals.
Do not mistake timers with clocks. Whereas clocks simply measure the time, a timer is like an alarm, sending some kind of a signal, when a given time has been reached or a defined amount of time has been elapsed. Timers can send such signals either once or periodically. Since the game may run a little bit longer than for one frame, the latter should be preferred.
This is the first and easiest way of synchronizing the game to one speed. We can make Windows to send us periodically a message to our window queue. We will render only a frame, if such a message is received by our game.
There are two ways of using the WM_TIMER message. Either Windows will send us the message (as any other window message) to the message queue of the window, or it will enter a special callback function specified in the program. To activate the timer, a call to SetTimer() is necessary. The function looks like this:
SetTimer(WindowHandle,
SelfDefinedTimerID, TimeSlice, TimerFunction);
If you need only one timer, regardless of the window, you can set the window handle to NULL, the timer ID will be ignored then. If you specify a timer function, this one will be called instead of sending a message. Specifying NULL means sending a message. So the simplest call to SetTimer() is:
SetTimer(NULL,
0, TimeSlice, NULL);
The TimeSlice is the amount of time between to signals, measured in milliseconds. If we want to have 25 signals per second, we must therefore define a time slice of 40 milliseconds (1 sec / 25 = 1000 milliseconds / 25 = 40). If we incorporate this into our standard game loop from the first chapters, this would look like this (the changes are marked in red, for those who have a color printer):
SetTimer(NULL,
0, 40, NULL);
for(;;)
{
MSG msg;
if(PeekMessage(&msg, NULL, 0, 0,
PM_NOREMOVE)) // Do we have a message present?
{
if(!GetMessage(&msg, NULL, 0, 0
)) // Can we process the
message?
return msg.wParam; // No? Return to
Windows!
if(msg.message == WM_QUIT) // Check for a quit message
break;
if(msg.message == WM_TIMER && fGameIsActive)//
Render the scene only on timer signal
RenderScene();
TranslateMessage(&msg); // The usual Windows
stuff...
DispatchMessage(&msg);
}
if(!fGameIsActive) // If the game is
not active
WaitMessage(); // wait to
become active again
}
What did we change? First of all, we activate the timer at the beginning of the loop to send us 25 signals per second by defining a time slice of 40 milliseconds. The second change is that we moved the call of RenderScene() from outside the message loop to the inside, since RenderScene()is now called only by receiving the WM_TIMER message. Due to this, we simplified the call to WaitMessage() at the very end of the loop.
Instead of receiving a WM_TIMER message, we can specify a timer function in SetTimer(). In this case, no WM_TIMER message is ever received by the program; instead your timer function will be called directly in a second thread. The advantage of this is that your timer function (actually, this will be in our case RenderScene() itself) is independent of the rest of the program. The disadvantage is, that programming for multiple threads is far more complicated than programming for single threading (think only of concurrent access to the same resources). And, to make it not easier, a second thread makes the game not faster, only slower (even if it is only a mere one or two percent). And since it will not simplify a game or this abstract, this alternative will not be discussed further.
At the end of your program you should call KillTimer() to end the signaling of WM_TIMER messages (not shown in the example).
In principle, we have solved the problem of our abstract, so why it is this not the end of the abstract? Sadly, the usage of WM_TIMER has several drawbacks:
· Inaccuracy: Although you specify the period between to signals in steps of one millisecond, the resolution of the timer is not better than 55 milliseconds. In other words, regardless of the time slice you specify, it is going to be at least 55 milliseconds between two WM_TIMER messages. It may even took much longer before they are processed, because Windows does not give timer messages any priority, they have to wait in line like any other message. In our example, the time slice between two timer messages is not 40 but 55 milliseconds, such changing the frame rate from 25 frames/second to 18 frames/second without our knowledge.
· Missing a beat: To avoid a message queue overflow, Windows only sends a timer message, when there is no other timer message present in your message queue. So if Windows is busy doing other things or your game just have to render a scene more complex than expected, your game simply will skip some message rather than racing to catch up. Since most games depend on a frequent scenery update, missing a beat is often not an option.
Due to this, the usage of SetTimer()is not an option for games which rely on stable timing signals. But – as usually for Windows – you can achieve the same goal on different ways.
Included in the multi media extension to Windows, there is a multimedia timer with a resolution of one millisecond, invoked with timeSetEvent(). Besides the fact, that the resolution is much higher, this timer is even more accurate, because it does not send any timer messages, which may have to wait in a message queue. Every timer activated through timeSetEvent()is being put into a separate thread calling a callback function directly, when the signal occurs.
The usage of timeSetEvent() has the same implications as using a callback function in SetTimer(): the pitfalls of multithreading. Since every timer has its own thread, you cannot avoid the aspects of multithreading. If you use a multi media timer, make sure you understand about synchronization, semaphores and the other aspects of multithread programming.
Although the multi media timer has a resolution high enough to fit the needs for a game, even this timer has at least one drawback:
· Timer latency. The thread containing the multimedia timer is in a blocked state until the specified time is elapsed. It then will change its state to ready, but will not run, until the current running thread has eaten up its time slice or given away the control of the processor for other reasons. Even raising the thread priority of your timer thread will not solve this problem. The amount of the latency depends on what you will do in your other threads and what other programs will do, which may still running. The latency may vary between 0 to several milliseconds, depending on the current load of threads and their amount of work. This problem of latency refers to all kind of multithreading, not only to timers.
Although WM_TIMER is nearly unusable for games, the usage of the multimedia timer may not. This depends on what for an accuracy you will need for your game. If your frame rate is rather low (and therefore the time slice specified in timeSetEvent() rather high) than the latency of the timer is compared to your time slice probably small enough to be negligibly. If you are unsure if the accuracy and the latency of the multimedia timers will fit your needs, simply skip this alternative and try a better solution.
Timers are not the best alternative to be used in games, although they are easy to use and support the event driven programming technique. Either timers have a bad resolution (inaccuracy) or the signal may not arrive to a specific time (latency). They can be still used for games, where precision is not at high priority.
As stated before, clocks only measure the time itself, but have no way of signaling, that a given period of time has elapsed. This is far away of the event driven programming style and the older style of polling data has to be used. Although clocks do not support time slices like timers, this can be easily achieved by calculating the difference between two calls to a clock.
The system clock will return the number of milliseconds since the operating system has been started (the famous system time). The resolution of the timer is different from operating system to operating system. Under Windows 95/98 the resolution is 1 millisecond, under Windows NT/2000 at least 5 milliseconds. And do not expect, that any successor of Windows 2000 have the same behavior. Fortunately, you can change the resolution of the clock under Windows NT/2000.
Be careful, the system clock is (only) a 32 bit counter, so after 232 milliseconds (which is about 49.71 days), the clock wraps around to 0 again. So do not expect, that a latter call to retrieve the system time will result in a bigger number than the first call. To measure a time slice, always calculate it by the absolute value of the difference of both numbers. Do not laugh, about this matter, a lot of people run their computers day and night, so it is really possible, that a system runs for more than 40 days without interruption.
With the function timeBeginPeriod() the resolution of the counter can be changed. This should always be done in the case that the game is running under Windows NT/2000. The resolution of the counter has to be set back, if the counter is not needed anymore. This is done with the function timeEndPeriod(). The system time can be retrieved using timeGetTime(). An example:
timeBeginPeriod(1); // Set clock
resolution to 1 ms
long lStartTime = timeGetTime; // Get the system time
// Imagine, something very time
consuming happens here
long lEndTime = timeGetTime();
// Get the system time again
long lTimeSlice = labs(lEndTime -
lStartTime); // This is the time slice
timeEndPeriod(1); // Reset the resolution of the clock
The calls to timeBeginPeriod() and timeEndPeriod() are only needed once in a program, so do not try to wrap this around every call to timeGetTime(), this is only wasted processor power.
The resolution of 1 millisecond is sufficient for nearly all games, although the functions for retrieving the system time cannot guarantee to support this resolution on each system. So always check the return codes of the functions. And of course, for a complete and detailed explanation of these (and all other) functions, read the documents about the Windows API.
The system clock – and this is valid for all clocks – has much less overhead than timers, because the time is retrieved directly. The operating system does neither need to send any messages nor establish a second thread for any callback procedure. Thus the problem of multithreading programming does not apply to counters. And since the time is retrieved immediately, latency is not a problem either.
Although not present on ancient hardware, modern computer have a high-resolution counter, the so called “performance counter” (named that way, because it was first used under Windows only for software performance measurement techniques). The high-resolution performance counter usually makes about 1.1 to 1.2 million ticks per second, thus having a resolution over 1000 times better than the multi media timer. Due to this the counter is optimal for game programming, since it allows a very fine timing.
With the a call to QueryPerformanceFrequency() we can examine the exact frequency (or the amount of ticks per second) of the counter. This number is rather odd and rather high and may be different from system to system, so do not rely on constant numbers. The call looks like this:
BOOL QueryPerformanceFrequency(LARGE_INTEGER* pFrequency);
The function returns FALSE, if there is no counter present in the system. This happens mainly on older computer systems. The pointer pFrequency points to a variable, where the frequency of the counter should be stored. As you can see, this is a LARGE_INTEGER, which is type compatible with _int64, so 64-bit arithmetic is involved here. LARGE_INTEGER is provided for those compilers, which cannot handle 64-bit arithmetic. 32 bit is much too small for this counter, imagine how long a counter would need to wrap around 0, if over 1 million ticks are added each second (in fact: about one hour only). With 64-bit arithmetic the system need much more than 475,000 years to put the counter to 0 again.
Since the frequency is probably not the same you need to measure on of your time slices, you have to recalculate some numbers a bit, but this is fairly easy.
Imagine that you want to have a frame rate of 40 frames per second. Which is the maximum of time your game can spend in rendering one frame? What is your time slice? Well of course 1/40 second. Converted in milliseconds that is 25 milliseconds. Simplified, you could use this:
#define FRAME_RATE 40
#define
TIME_SLICE (1000 / FRAME_RATE)
Since the performance counter does not use milliseconds but rather an odd frequency, we have to convert these two frequencies:
_int64 iFrequency;
QueryPerformanceFrequency((LARGE_INTEGER*)
&iFrequency);
_int64 iTicksPerTimeSlice = TIME_SLICE
/ 1000 * iFrequency;
Clocks (or counters) are much more effective than timers. First of all, the resolution is at least the same, if not better (the resolution of the performance counter is currently unbeatable). Second, the calls to retrieve the counter values have less overhead and work faster. Counters also do not have any latency problems, since the counter values will be retrieved directly.
If possible, use the high-resolution performance counter, which should be present in all modern computer system. The second best alternative is to use the system time. If you are really clever, implement a small wrapper, which checks for the presence of the performance counter and if not, simply uses the system time. If you make this wrapper transparent, the rest of the game has not to care about timers or clocks anymore and will always use the best possible solution.
After this lengthy discussion about timers and counters we should come back to the original matter of this big chapter. The matter of constant frame rates was explained in theory some chapters ago and we have found a sample implementation for this with the WM_TIMER message, but now it is time to look at the more practical part. First, get rid of the whole discussion about counter and let us try to implement such a wrapper class for counters we discussed earlier in this abstract. This class is so small; it should be selfexplanatory (for readability, both implementation and definition of this class are put together):
class GameClock
{
protected:
BOOL
bHPCpresent; // Is high
resolution performance counter present
_int64 iFrequency; // Frequency of the counter
_int64 iTicksPerTimeSlice; // Ticks/second of the counter
_int64 iTimeStart; // Time where clock has started
_int64 iTimeEnd; // Time where clock has stopped
public:
GameClock(double dTimeSlice)
{
// Check frequency of counter and see if
it is present
bHPCpresent = QueryPerformanceFrequency((LARGE_INTEGER*)
&iFrequency);
// Convert from milliseconds to ticks
per second
if(bHPCpresent)
iTicksPerTimeSlice = (_int64)
(dTimeSlice / 1000.0 * iFrequency);
else
{
timeBeginPeriod(1);
iTicksPerTimeSlice = (_int64)
dTimeSlice;
}
// Init time values
iTimeStart = 0;
iTimeEnd = 0;
}
~GameClock()
{
if(!bHPCpresent)
timeEndPeriod(1);
}
void Start()
{
// Retrieve the start time
if(bHPCpresent)
QueryPerformanceCounter((LARGE_INTEGER*) &iTimeStart);
else
iTimeStart = (_int64) timeGetTime();
}
void Stop()
{
// Retrieve the end time
if(bHPCpresent)
QueryPerformanceCounter((LARGE_INTEGER*)
&iTimeEnd);
else
iTimeEnd = (_int64) timeGetTime();
}
_int64 ElapsedTime()
{
// Calculate the time elapsed between
Start() and Stop()
_int64 iResult = iTimeEnd - iTimeStart;
// For the uncommon case, that the
counter has wrapped around:
if(iResult < 0)
iResult *= -1;
return iResult;
}
BOOL TimeSliceOver()
{
// See, if one time slice is over
return ElapsedTime() >=
iTicksPerTimeSlice;
}
_int64& StartTime() { return iTimeStart; }
_int64& EndTime() { return iTimeEnd; }
_int64& TicksPerTS() { return
iTicksPerTimeSlice; }
};
This class is safe against counters which like to wrap around and feel comfortable to people, who likes to increase the measured time slice by using several Stops() but only one Start().
After we have solved finally the problem of time measurement, we can go now over to our well-known message loop and try to bring it up to date. The interesting parts are marked in red:
#define
FRAME_RATE 40 // 40
frames per second
#define
TIME_SLICE (1000 / FRAME_RATE) // Max. time (in ms) to render 1 frame = 1/40
sec.
GameClock
Clock(TIME_SLICE);
Clock.Start(); // Start the clock.
for(;;)
{
MSG msg;
if(PeekMessage(&msg, NULL, 0, 0,
PM_NOREMOVE)) // Do we have a message present?
{
if(!GetMessage(&msg, NULL, 0, 0
)) // Can we process the
message?
return msg.wParam; // No? Return to
Windows!
if(msg.message == WM_QUIT) // Check for a quit message
break;
TranslateMessage(&msg); // The usual Windows
stuff...
DispatchMessage(&msg);
}
if(fGameIsActive) // Is the game the active application?
{
Clock.Stop(); // Stop the clock to see...
while(Clock.TimeSliceOver()) // ... if one time slice has elapsed
{
RenderScene(); // If so, render
the current frame and
Clock.StartTime() += Clock.TicksPerTS(); // set
the time for the next frame.
}
}
else
WaitMessage();
Let us discuss now, how our new clock was used in this example:
First, the clock starts only once. We do not have to start it more than once, since we only need to have a starting signal for the tact. After the part of message processing the loop determines, if the game is active and if yes, it will Stop() the clock to determine the time difference between the Start() time and the Stop() time. If this difference is equal or greater than our defined time slice, the scene will be rendered. After this, the starting time will be increased by one full time slice, to determine the next starting/stopping period. Please note, that the test, if a time slice has been elapsed is done with a while() loop and not with an if(). This will give the advantage of racing up any frame needed to be rendered, if one or more frames needed more time to render than the time slice allowed. Otherwise we will miss one or more frames.
As you see, our new clock was simply started with a time slice as parameter, whereas the game loop handled the rendering of the scene. In principle, our problem of implementing a (simple) game loop using constant frame rates is solved now. Some additional remarks should be made, though.
Usually, a game will use a double or (more seldom) triple buffering technique to render and display a frame without tearing. If the rendering of a frame is completed, a call to the Flip() method of the screen object will switch front and back buffer, thus displaying the new generated frame. In general, the Flip() method will synchronize itself with the vertical blank of the display hardware, in such a way, that it will wait until the vertical blank occurs and then exchange (flip) both surfaces. But waiting means, that this time reduces our effective time slice to render the next frame.
It is therefore advisable to synchronize the beginning of a time slice with the occurrence of a vertical blank to have the biggest possible time slice between two vertical blanks. In this way the game will miss (if any) as less vertical blanks as possible, making the display and the movement of objects as smooth as possible. To achieve this we start the clock, when a vertical blank occurred. This can be simply done by putting a call to Flip() right in front of starting our clock. We can modify our example with:
#define
FRAME_RATE 40 // 40 frames per second
#define
TIME_SLICE (1000 / FRAME_RATE) // Max. time (in ms) to render 1 frame = 1/40
sec.
extern
CDXScreen* pScreen; // Let’s
assume, this is the pointer to our screen obj.
GameClock
Clock(TIME_SLICE);
pScreen->Flip();
Clock.Start(); // Start the clock.
If the game uses other concepts for rendering a frame (like single buffering and a dirty rectangle management), this concept cannot be applied.
Are there any concept or ideas to improve the approach of constant frame rates? Yes, but not much, because the term “constant” is not greatly expandable. But let us see, how in general the usage of constant frame rates could be improved (see this as hints and not as ready-to-compile solutions):
In our examplatory approach, which was discussed in this abstract, the desired frame rate was directly set in the source code. The first question, which will arise, is: How was this number created? Well, to be honest: In most cases the programmer will only use his experience combined with some trials. But to avoid masses of trials and errors, here are some guidelines to determine the right frame rate:
1. Do not let the frame rate drop too low: The absolute minimum required frame rate is about 25 frames per second, since the human eye can still distinguish single movements at a rate of about 24 frames per second. Since this number is much too close to the human limitation, I would strongly recommend not using this number. The minimum frame rate should be 30 frames per second. But, as in many cases: The more, the better, 40 frames/second is sufficient, 50 is good and 60 is real great. In this case: Just play around with an example scene and choose which frame rate you like most.
2. Specify the minimum requirements of the game: Do not expect that your game will run on any computer system. The more complex your game is and the more special effects it has – besides screen resolution, color depth and other stuff – the better must be the hardware of the system. If you have chosen a desired frame rate, build the most complex scene your game should support and see, if it runs still smooth on your desired computer system. Maybe you have to adapt the frame rate, the minimum requirements or the maximum allowed complexity of a scene. Here you have to fiddle a little bit and play with the numbers. Some figures are more important than others. If possible, do not lower the frame rate. See if you can reduce the complexity of your scene (to have more than 500 alien ships chasing you may seem great, but is this really necessary and playable? Would not be 100 or even 50 alien ships be enough?) or try to lower the resolution or special effects.
In the exemplatory approach, which was discussed in this abstract, our desired frame rate was coded directly into the source code, in our case 40 frames per second. This means, that the game will always run with 40 frames per second, regardless if we have a fast machine or not. “Well”, would you say, “but this is exactly what we want, don’t we?”. In general: yes. But what about the idea, that the game runs with constant 40 frames/second on those machines which fulfill the minimum requirements and – say – 45 constant frames/second on faster machines or even 50? And to make this worse: In any case, regardless of the frame rate chosen – the game should run with the same speed anyway! Do not think that this is impossible; it is far easier than you think:
1. During game initialization, build up the most complex scene you game should support.
2. Render this invisible on the screen and measure the time it needed to get rendered.
3. Add some percentage of this time to it, mainly for sound, special effects and (just) safety.
4. With this number you can determine (with formulas already used in this abstract) the frame rate your game can support.
5. Make sure your game make checks on this calculated frame rate. If the number is less than 25, you may consider aborting the game. And even on very very fast computers there is no need to go beyond 60 frames per second. Maybe your calculated number is higher than the hardware can support, so limit it.
Well, but this was only the first step to solution. If you are smart, than you have already realized, that we have determined the best supportable frame rate for the game, but that we may now get problems with the game speed itself. Because in the explanation, of what constant frame rates are, there was a little sentence that now gets very important:
The speed of movement (velocity) – e.g. for a sprite or other moving objects – is therefore often described in pixels per frame, instead of pixels per second, although you can derive one from another.
If you have specified the speed of moving objects in pixels per frame, you may have now a problem, because the frame rate itself will be determined at run time, not compile time. In this case it is better to define the velocity of moving objects in pixels per second and convert it to pixels per frame after you have determined the frame rate (and with it your time slice). Or, much more easily, define the speed in pixels per frame bound to a certain frame rate. In other words: Define the velocity for an object for a certain frame rate of (as an example) 100 frames per second and convert this to your calculated frame rate. An example. The alien space ship moves with 40 pixels per frame if the frame rate is set to 100 frames per second (the internal default). If the calculated frame rate is 50, the velocity is now 80 pixels per frame instead of 40. If the frame rate is 60, the velocity is now 66,667 pixels per frame.
As you can see, throughout all these calculations, you may get odd numbers. I recommend storing all these numbers in variables of float or even better, in doubles. Otherwise you may get to many rounding errors. Not only speed and time slices but also the pixel coordinates of your sprites and other objects.
Since the frame rate is only calculated once during the initialization phase of the game, it is only necessary to convert the velocity parameters of your objects once. This may save some time during game play.
Not only have a game a certain frame rate, but also the video hardware of the computer has a horizontal refresh rate, mostly ranging between 60 to 120 Hz on current computer systems. It should be very uncommon, that the frame rate of the game matches this frequency or is an integral part of it, this would be a mere coincident. Of course, to make things worse, the refresh rate changes from system to system.
Could this affect the smoothness of the game? Yes, of course. We have synchronized our clock with the vertical blank of the display hardware to get the most out of our time slice. But what happens, if the time slice is a little bit larger than the time between two vertical blanks? In this case the game would miss one vertical blank and will Flip() effectively the surfaces on the occurrence of the next vertical blank. But this will make the time slice nearly as twice as big as expected.
One way to solve this problem would be to determine the refresh rate of the video hardware and choose an internal frame rate, which is the same or an integral part of the refresh rate. For example, if the refresh rate is 90 Hz, possible good frame rates would be 30, 45 or 90 frames/second, whereas an refresh rate of 87 frames per second is not advisable at all.
Only Direct X version 7 or higher support the retrieving of the refresh frequency, but sadly, CDX 2.4 does not support DX 7 or higher. In this case you have to wait for the release of CDX 3.0 or do it your own way. If you have determined your optimal frame rate due to the suggestions in the last chapter, this could get even more complicated.
There is no general solution to this problem, so be warned. But the severity of this problem gets smaller, the smaller your time slice is, since then is the difference between the occurrence of a vertical blank and the end of your time slice not so big. Personal experiences showed, that this problem can be neglected at frame rates of 50 or 60 frames per second, but give your own senses a try.
This example shows a very simple application (not a game) that uses constant frame rates to display a small rectangle moving over the screen, bouncing at the edges. Not included in this source – although used – is the class of the game clock, because the whole class was already well discussed and would bloat this source code too much (so if you want to compile this source, just insert the definition of this class into the source code). Please note that this example program does not make any checks for error conditions. In general, this is a very bad habit for a program (especially if it used Direct X), but this was necessary to keep this example to a reasonable size. You can exit the program by pressing ALT-F4, as by any Windows application.
#include <windows.h>
#define CDXINCLUDEALL
#include <cdx.h>
#include <gameclock.h> // Our previously discussed game clock!
See remark!
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define SCREEN_WIDTH 640
#define SCREEN_HEIGHT 480
#define CORLODEPTH 16
#define RECT_SIZE 32
#define RECT_HALF (RECT_SIZE >> 1)
#define FRAME_RATE 50
#define TIME_SCLICE (1000 / FRAME_RATE)
HWND hGameWin;
CDXScreen* pScreen;
GameClock* pClock;
bool fGameIsActive;
void RenderScene()
{
static long x =
SCREEN_WIDTH >> 1;
static long y = SCREEN_HEIGHT
>> 1;
static long dx = 1;
static long dy = 1;
if(x > RECT_HALFT && x < SCREEN_WIDTH – RECT_HALF – 1)
x += dx;
else
dx *= -1;
if(y > RECT_HALFT && y < SCREEN_HEIGHT – RECT_HALF – 1)
y += dy;
else
dy *= -1;
CDXSurface* pBack = pScreen->GetBack();
pBack->Lock();
pBack->Rect(x – (RECT_SIZE >> 1),
y – (RECT_SIZE >> 1),
x + (RECT_SIZE >> 1),,
y + (RECT_SIZE >> 1),
0xffffffff);
pBack->UnLock();
pScreen->Flip();
}
// The window callback procedure.
LRESULT CALLBACK WndProc(HWND hWnd,
UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_ACTIVATEAPP:
// Set flag, whether the app is active or not.
fGameIsActive = wParam != 0;
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR
lpCmdLine,
int
nCmdShow
)
{
// Register window class
WNDCLASS wc = {CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance,
NULL,
NULL,
NULL,
NULL,
TEXT("Sample CDX App (constant frame
rates)")
};
// Create game window
hGameWin = CreateWindow("Sample CDX App (constant frame
rates)",
"Sample CDX App (constant frame
rates)",
WS_POPUP,
CW_USEDEFAULT, CW_USEDEFAULT,
WINDOW_WIDTH,
WINDOW_HEIGHT,
NULL, NULL,
hInstance,
0);
// Show window and disable cursor
ShowWindow(hGameWin, nCmdShow);
ShowCursor(FALSE);
pScreen = new CDXScreen(hGameWin, SCREEN_WIDTH, SCREEN_HEIGHT,
COLORDEPTH);
// Initialize the clock
pClock = new GameClock(TIME_SLICE);
g_pScreen->Flip();
pClock->Start();
// Loop for handling window messages.
for(;;)
{
MSG msg;
// See, if a message is present for our
window
if(PeekMessage(&msg, NULL, 0, 0,
PM_NOREMOVE))
{
// If unable to process (invalid
message?), leave game
if(!GetMessage(&msg, NULL, 0, 0
))
break;
// If quit message, just leave the
game
if(msg.message == WM_QUIT)
break;
// The usual Windows stuff for
msgs...
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Is game active, enter rendering loop
if(fGameIsActive)
{
pClock->Stop();
// Only render, if at least one time slice is over
while(pClock->TimeSliceOver())
{
RenderScene();
// Set next starting point for time slice
pClock->StartTime() += pClock->TicksPerTS();
}
}
else
WaitMessage();
}
delete pClock;
delete pScreen;
DestroyWindow(g_hGameWin);
return TRUE;
}
As stated before, in the concept of dynamic frame rates the game does not need to render a frame to a specific time, it just tries to render the frames as fast as possible. So terms like time slice or tact are really senseless here. In fact, we can go back, where we started and take the original and unmodified game loop. Since you are already familiar with it, it is not listed here again.
What do we have to add to the game loop to make this loop fit for dynamic frame rates? As mentioned before in the definition of this approach, the game loop has to measure the exact time the game needs to render a frame. This measured time will be used for the rendering of the next frame to calculate, how far movable has moved during the elapsed time period. The implementation of this is fairly simple (as usual, changed or added line will be marked in red and printed in italics):
#define TIME_SLICE 1000
_int64 iElapsedTime = 0;
for(;;)
{
MSG msg;
if(PeekMessage(&msg, NULL, 0, 0,
PM_NOREMOVE)) // Do we have a
message present?
{
if(!GetMessage(&msg, NULL, 0, 0
)) // Can we process the
message?
return msg.wParam; // No? Return to
Windows!
if(msg.message == WM_QUIT) // Check for a quit
message
break;
TranslateMessage(&msg); // The usual Windows
stuff...
DispatchMessage(&msg);
}
if(fGameIsActive) // If the game is active...
{
GameClock Clock(TIME_SLICE);
Clock.Start();
RenderScene(iElapsedTime / Clock.TicksPerTS());
// ...enter the game loop or
Clock.Stop();
iElapsedTime = Clock.TimeElapsed();
}
else
WaitMessage (); // ...wait to become active again
}
Let us have a look to the changes we made to the original loop: First we defined a time slice of 1000 milliseconds (a whole second). Do not get confused, because you were told in the last paragraphs that a time slice is not necessary for dynamic frame rates and even senseless. Remember the following statement from the definition of the approach of dynamic frame rates:
The speed of movement is therefore often described in pixels per second, instead of pixels per frame. In this case you cannot derive one from another.
If we define the speed of movement in pixels per second (although you can take any other units) it would be nice, how much of a second has elapsed through the rendering of the last frame. This is the whole explanation; do not expect any additional magic in there.
The next big thing is that we take our well known game clock and measure the time span the function RenderScene() needs for its completion. The measured time span is now converted to the fraction of our measurement unit (in this case: a second) and a (new) parameter for the rendering function, where we can decide now, how far all the moving objects have moved during this time period. For the first call we initialize this value to zero, because we have no previous frame and therefore no movement.
These were the changes to the game loop, but the real tricky thing happens now in the function, where the current frame gets rendered, the function RenderScene(). It has got a parameter and it has now to do additional computations about the movement of all moving objects. Just let us take the old RenderScene() from the complete example of static frame rate and modify it in a way, that it will now work for the approach of dynamic frame rates. The changes are not as much as you think:
void RenderScene(double
dFractionOfASecond)
{
static double
x = SCREEN_WIDTH >> 1;
static double
y = SCREEN_HEIGHT >> 1;
static long dx = 1;
static long dy = 1;
static
long pixel_per_second = 50;
double
pixel_this_frame = pixel_per_second * dFractionOfASecond;
if(x > RECT_HALFT && x <
SCREEN_WIDTH – RECT_HALF – 1)
x += pixel_this_frame
* dx;
else
dx *= -1;
if(y > RECT_HALFT && y < SCREEN_HEIGHT – RECT_HALF – 1)
y += pixel_this_frame
* dy;
else
dy *= -1;
// Additional checks for clipping, possible coordinate correction
x = max(x,
RECT_HALF);
x = min(x,
SCREEN_WIDTH – RECT_HALF – 1);
y = max(y,
RECT_HALFT);
y = min(y,
SCREEN_HEIGHT – RECT_HALF – 1);
CDXSurface* pBack = pScreen->GetBack();
pBack->Lock();
pBack->Rect((long) (x – (RECT_SIZE >> 1)),
(long) (y – (RECT_SIZE >> 1)),
(long) (x + (RECT_SIZE >> 1)),,
(long) (y + (RECT_SIZE >> 1)),
0xffffffff);
pBack->UnLock();
pScreen->Flip();
}
What has been changed this time? First we changed the data type of the coordinates of the moving rectangle from integer based to floating point based numbers. This is due to the fact that we may get odd numbers of how many pixels the rectangle has to move this frame. To avoid rounding errors and miscalculated coordinates, these values are now from type double instead of long.
Then we introduced a new variable called pixel_per_second, which describes, how many pixels our rectangle shall move over the screen in a whole second. Since our function may be called more than once in a second, we compute in the next line, how many pixels the rectangle has to move in the current frame. Since our base measurement unit is a second and since the parameter is a fraction of a second (so the same units are used here) we can simply multiply this value with the velocity value of our rectangle. Since the rectangle can now move more (or less) than a pixel per frame, we have to change the computation of the new coordinates also a bit.
The last and rather big changes are the additional lines to detect possible clipping problems, since (as stated) the rectangle move eventually more than one pixel per frame, so we have to be careful what coordinates we are getting. Last but not least we changed the call to Rect() a bit, but this is mere type conversion and therefore not a real change
Believe me, these were all changes we have to do to change our small example program from the approach of constant frame rates to the approach of dynamic frame rates. Some notes to the end of this chapter:
It is no coincident, that the velocity of the rectangle was set to 50 pixel per second in this example, because we have chosen in the first example a frame rate of 50 frames per second by a movement of the rectangle of 1 pixel per frame. In theory, both examples should now have the very same running speed.
Do not be disappointed if the rectangle is not moving very smoothly, since this kind of frame rate control is not suitable for every type of game (as already mentioned).
As always, see the explanations in this chapter as hints and not as ready-to-compile solutions.
Our approach to dynamic frame rates has the disadvantage of detecting changes in the frame rate only afterwards. If the frame rate is dropping due to the rendering of very complex scenes, the motions of objects will begin to adjust only in future frames. The frame rate discrepancy is detected after it occurred, not meanwhile. It would be great if the game could detect such discrepancy before it will occur to adjust the fraction of movement of all objects accordingly. This would smoothen the movement of the objects far better. One hint, whether the frame rate will drop or rise is the amount of all objects and their complexity (really important for 3D based games).
Time measurement and the resulting behavior for such an approach is quite different to what was discussed in this chapter and not that easy.
About
the AuthorPeter Eichler, born in 1967, is a senior software developer in a German company, which produces software applications for financial oriented companies like banks or insurance companies. Earlier computer experiences include real time programming, network protocols, natural language analysis and compiler construction. One of his first experiences with computers was at the age of 13 on a Commodore VIC 20, the predecessor of the famous C 64. Since then, he wrote several games for private use. He now lives in Hamburg with his girl friend, a cat and two birds.
If you have questions or suggestions about the abstract or the author, please feel free to contact him under mailto:peichler@nikocity.de