Tools, Game Loop, Keyboard Input, and Timing
Moving Your Game to Windows, Part I
By Peter Donnelly
Abstract
You're finally ready to take the big step—porting your MS-DOS® game to
Microsoft® Windows 95®. You've learned the basics of Windows programming but
you're not sure where to begin. This series of articles will guide you through
some of the broad concepts, and a few of the finer points, of programming games
under 32-bit Windows.
This first article will concentrate on two of the most fundamental issues
confronting you as a designer of real-time games: building the main game
loop—the engine that drives the action—and timing events so that things proceed
at a pace chosen by you rather than by the hardware or operating system. Along
the way we'll also touch on the most basic form of input, the keyboard.
I focus on real-time games because these are the most challenging from the
programmer's point of view. Much of the information in this and succeeding
articles will apply to turn-based games as well, but I emphasize games where
something is going on at all times, regardless of user input.
It is assumed that you have a general understanding of game design theory,
along with MS-DOS programming skills with C or C++, and have at least read an
introduction to Windows programming.
Tools
As you migrate from MS-DOS® to Microsoft® Windows®-based programming, no
doubt you will be shopping for a new development environment as well.
A key consideration in choosing your main programming tool is how well it
works with the Microsoft DirectX™ Software Development Kit (SDK). Direct X has
made high-performance animation under Windows possible for the first time—to
say nothing of the services it offers for sound playback, user input, and
multiplayer games. (If you're not yet familiar with DirectX, read Mark
McCulley's technical overview, "A Road Map of Game and Interactive Media
Technologies.")
Microsoft Visual C++® is the tool that offers the most seamless
integration with DirectX, and in fact version 4.1 comes with the DirectX
SDK. However, you can use any other 32-bit Windows C or C++ compiler. It is
possible to work with DirectX using Borland's Delphi 2, but you will need a
Pascal interface. (Blake Stone's interface library at http://www.dkw.com/bstone/ is one option,
and is available free.)
I see someone's hand up. "But lots of gamers still have Windows 3.1.
Shouldn't I stick with the 16-bit environment?" In a word, no. Apart from
the speed advantages inherent in 32-bit applications, DirectX is strictly
32-bit and will compile and run only under Windows 95 and Windows NT® 4.0.
Almost all Windows games hitting the marketplace today are exclusively for Win32®,
and this trend will be even stronger by the time your game hits the market.
Another hand is waving. "Okay, I've got Visual C++. Do I use MFC or
not?" For those of you who are new to Visual C++, MFC stands for Microsoft
Foundation Classes. MFC is an object-oriented programming framework that not
only encapsulates a lot of low-level Windows applications programming interface
(API) stuff but also provides a useful paradigm for Windows programs structured
around the "document" (the data) and the "view" (the way
the user is seeing and manipulating the data). In conjunction with the
AppWizard built into Microsoft Developer Studio, it can greatly speed up the
process of creating a skeleton application and adding things like docking
toolbars and status bars. It also virtually automates many tedious programming
chores like file input and output.
Judging from a straw poll I conducted recently, most Windows game developers
are not using MFC. Developers are in general aiming for fast execution and are
concerned that the MFC adds unnecessary overhead. And as one developer put it,
"There's just too much there that we don't need and gets in our way."
Also, MFC is largely aimed at creating attractive and consistent user
interfaces. Game developers want to create their own interfaces and don't
necessarily want their games to look like standard Windows applications.
Moby Dick: An Example
In order to clarify some issues involved in going from MS-DOS to
Windows, I created a simple MS-DOS game with VGA graphics and then ported
it. Both versions are included here, along with their source code. In
subsequent articles I hope to enhance at least the Windows version with mouse
and joystick input, sound, and a more sophisticated animation system, using the
DirectX SDK.
Moby Dick is an arcade-style game that (for now) accepts input only from the
keyboard. A whale (that's Moby Dick, for those of you who skipped American
Literature), controlled by the computer, moves around the map more or less at
random and occasionally "blows" to signal his position. The player
moves the ship (under the command of the obsessive Captain Ahab) with the
cursor keys or numerical keypad. Diagonal moves are possible with the corner
keypad keys or by holding down two cursor keys simultaneously. Wherever the
ship moves it leaves a wake, and Moby Dick appears in cells that are so marked.
The player wins by moving the ship onto Moby Dick in a marked cell. The Windows
version has one further feature: a cloud that moves across the map on a
randomly selected row and erases the marked cells. I added this feature to
illustrate multithreading and setting thread priorities.
The MS-DOS game has a time limit, but this feature has been omitted from the
Windows version so that you won't be interrupted while experimenting with the
settings.
The source for the MS-DOS version was developed with Borland Turbo C++
version 3.0 but should compile with minimal changes in just about any C or C++
compiler. Be sure to turn off register variables. The Windows version is in the
form of a Visual C++ version 4.0 project; again, since it doesn't use classes
you should be able to modify it for other compilers without too much effort.
Also included is a little program called Time Waster, whose sole function is
to use up CPU time. It does this by repeatedly allocating memory until no more
is available, then deallocating it while also doing some floating-point
arithmetic. Be warned: because Windows treats the swap file as just an
extension of available RAM, Time Waster does beat on the hard drive. We'll use
this program when we start experimenting with timing and priorities.
Architecture of the MS-DOS Version
At the heart of the MS-DOS Moby Dick game are two interrupt-service routines
that intercept the normal timer and keyboard interrupts. (If you haven't used
the technique of driving a game with ISRs, it's explained very well in Secrets
of the Game Programming Gurus [see the bibliography], chapter 12.)
Figure 1. Program flow interrupted by an ISR
The keyboard ISR grabs every press and release and updates a key state table
that holds the current status of all the direction keys. The program consults
the table whenever it's time for Ahab (or the Pequod, if you prefer) to
move. This routine needs a little more work to ensure that no keystrokes are
lost—as we'll see later, we have to consider quick keystrokes in the Windows
version as well—but for purposes of illustration it works well enough. (We missed
the submission deadline for Game of the Year anyway.)
The timer ISR does nothing but increment a counter every time there's a tick
of the system clock—every 55 milliseconds or so. We check this counter on each
pass through the game loop to see whether it's time to do something. This check
is done in each of the two responder routines, Move_Ahab and Move_Moby.
Then, at the end of the game loop, we check whether anything interesting has
happened and, if necessary, redraw the screen.
The graphics system is minimal. All the sprites are kept in individual .PCX
files and decompressed into variables at load time. Updates are done directly
to the screen, without buffering or flipping or even waiting for the vertical
retrace. Incredibly cheesy, I know, but it doesn't matter, because the whole
system is going to be irrelevant under Windows, especially when we implement
DirectDraw.
MS-DOS vs. Windows: The Basics
No More Polling
As you probably know by now, a fundamental difference between MS-DOS and
Windows-based programs is that MS-DOS programs are procedural (that is,
they follow the procedures laid out by you, the developer, and in the order you
specify) while Windows programs are event driven. If programs were
insurance agents, the MS-DOS program would be working its way down a list of
prospects, calling every one. The Windows program, meanwhile, would be sitting
with its feet up waiting for the phone to ring.
Actually, the MS-DOS version of Moby Dick blurs the distinction a little,
because it uses interrupt handlers: rather than repeatedly checking for a
keystroke or a tick of the system clock, it reacts immediately to these
system-generated events. In fact, Moby Dick MS-DOS actually employs a primitive
form of multitasking, since it interrupts whatever it is doing to respond to
certain events.
Windows takes the same concept much further. It does not use interrupt
routines—at least, not in a way visible to the applications programmer—but it
keeps track of a host of events including not only input but things that happen
in response to input (such as resizing a window) and communicates necessary
information about the event to the appropriate window in the form of a message.
Almost everything in a Windows program happens, directly or indirectly, in
response to a message. In a game, as we'll see in a moment, the main
loop—checking and responding to input—has to be built around the
message-receiving and -dispatching mechanism.
Figure 2. Program flow in a Windows game
Forget About the Hardware
Probably the hardest part of the migration to Windows programming is letting
go of control. Games programmers in particular are expert at low-level fiddling
with the hardware—grabbing interrupts, reading ports, coaxing video cards into
"Mode X"—and are accustomed to having total control over program
flow. Windows initially appears like a sinister black box that is getting input
at one end and spitting out who-knows-what at the other. Figuring out what is
coming from the black box, and learning to use it, is a big leap.
But the first step is letting go. So sit back, close your eyes, take 50
long, deep breaths, and let go of all concern about registers, interrupts, and
color planes. Windows will take that burden from you. Don't worry. Be happy.
All you have to do is write great games.
Okay, maybe the philosophy is a bit simplistic. In fact, we're going to
bypass the Windows message system when we come to keyboard input. But even then
we'll be relying on Windows to read the keyboard state for us.
Windows Handles Picture Files
A considerable part of Moby Dick MS-DOS is devoted to loading and
decompressing the .PCX graphics files. Most of the work is done in in-line
assembler, for the simple reason that I wrote these routines back when a fast
PC was 12 MHz and loading graphics was a major performance bottleneck.
If you've done an MS-DOS game with bitmaps, you've had to develop, buy, or
steal a routine just to get an image from the disk into usable form in memory.
Throw it out. Windows has functions to load .BMP files, so it takes a couple of
lines to do what all that code in Moby Dick MS-DOS does. In fact, you can put
bitmaps into the resource file, as we've done with Moby Dick Windows, so
they'll be built right into the executable.
Enough on that topic for now—we'll come back to it in a later article. For
now, all you need to know is that you won't be needing any of your old code to
get bitmaps into memory.
Just One Task in the List
One of the great benefits of Windows, its ability to run several programs at
once, can also be a bit of a headache, especially for the game programmer who
is used to taking complete control of the machine, down to the timer frequency,
in complete confidence that no one will mind. (Well, we did mind about those
ill-mannered games that used to exit without restoring the correct system time,
but we can forget aboutthat now.)
Three major side effects of the multitasking environment have to be
considered by game programmers.
- When your game is moved into
the background, execution may have to be suspended. (You can see how this
works in Moby Dick Windows, with the use of the "paused"
variable.) You will certainly want to suspend the action in a real-time
game. In turn-based games, you might not want the computer to make a move
while the player is doing something else, though you probably want the
artificial intelligence (AI) to continue being intelligent.
- Other tasks take up CPU time,
and as a result we can't always control the speed at which things happen
in the game. We'll get into this thorny issue later.
- You have to take
responsibility for redrawing the game window whenever it returns to the
foreground. Windows does not take responsibility for remembering the
contents of windows it covers or hides; the most it will do is notify a
window that it needs to repaint its client area. This topic is covered in
every Windows textbook (see under WM_PAINT), and we won't go into it here.
In fact, Moby Dick Windows does not restore its own window; we'll
implement this when we get into double-buffering under DirectDraw.
Multitasking Within Your Program
Although Moby Dick MS-DOS exhibits a primitive form of internal
multitasking, or multithreading, in its use of interrupt handlers, the program
is still tied to the single-minded nature of MS-DOS, which only likes to do one
thing at a time. Some MS-DOS programs do use true multithreading, but that's a
big coding job. The Windows 95 SDK makes it a lot easier, putting threads into
every game developer's bag of tricks. (If you're not yet familiar with the
concept, a thread is part of a program that executes independently from, and
not necessarily in synchronization with, other parts. Threads are not driven by
interrupts; they just continue executing whenever Windows gives them CPU time.)
Here are some areas where you might consider implementing separate threads:
- Allowing for background AI.
Make it possible for the computer to think about its next move even while
the user is busy moving pieces around, opening dialogs, etc. Threads are
great for processes that don't have to be synchronized with other things.
- Preloading data. Make a
thread responsible for reading files and preparing the game world while
the player is, for example, on the stairs going to the next level.
- Giving priority to
time-critical tasks. We'll return to this topic later.
The Game Loop
The concept of the game loop is pretty much the same no matter what the
programming environment. First, you get input—either by polling for it, waiting
for it, or grabbing it on the fly through interrupts or a message queue.
Second, you process the input and turn it into a meaningful action in the game
context—banking the airplane or moving the pawn forward. Then you display the results.
Of course, there are elaborations and variations on the theme, including the
computation of the AI's move, passing control from one player to another,
checking for victory, and so on.
The mechanics of implementing the loop, however, can be much different in
Windows than under MS-DOS. As you probably know by now, every Windows program
is built around a message loop. Although a game loop can be built around a
message loop, the two are by no means the same.
The Moby Dick MS-DOS Loop
Moby Dick MS-DOS illustrates a simple game loop in which we (a) check to see
whether anything is due to move, (b) move it, and (c) display the results.
while (!gamedone)
{
// Call timed routines -- no response if it's not time yet.
AhabMoved = Move_Ahab();
// Move Moby Dick only if Ahab hasn't moved. Otherwise they can
// cross paths without intercepting.
if (!AhabMoved) Move_Moby();
// If anyone has moved, update the screen and check for
// victory or loss.
if ((MobyX != OldMobyX) || (MobyY != OldMobyY)
|| (AhabMoved))
{
UpdateScreen();
if ((MobyX == AhabX) && (MobyY == AhabY)
&& (painted[MobyX][MobyY]))
{
gamedone = 1;
cprintf("\a");
cprintf("You win!");
}
if (TimesUp <= 0)
{
cprintf("\a");
cprintf("Time's up!");
gamedone = 1;
}
if (raw_key == MAKE_ESC)
{
gamedone = 1;
progdone = 1;
}
} // end update
} // end of inner game loop (while !gamedone)
The Moby Dick Windows Loop
On the face of it, things don't look that much different:
do
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT) break; // the only way out of the loop
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
if ((MobyX != OldMobyX) || (MobyY != OldMobyY)
|| (AhabMoved))
{
UpdateScreen();
if ((AhabX == MobyX) && (AhabY == MobyY) && (painted[AhabY][AhabX]))
{
Control = MessageBoxEx(hwnd, "You caught Moby! Play again?",
"Call Me Ishmael", MB_ICONQUESTION | MB_YESNO, 0);
if (Control == IDYES) InitializeGame();
else break;
}
} // if anybody moved
} // if screen updated
} // end of loop
while (TRUE);
Ignore the fact that there's no check for running out of time; as mentioned
above, I left this out of the Windows version to avoid annoying interruptions.
The mechanics of breaking out of the endless loop are also a bit different but
not significant. We want to focus on the message loop itself, so let's cut the
code down to the bare minimum:
do
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT) break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else DoSomething();
}
while (TRUE)
This is a fairly typical message loop. The only thing unusual about it is
that it uses PeekMessage instead of GetMessage.
GetMessage vs. PeekMessage
Why the difference? Simply because GetMessage waits for a message
(rather like _getch) but PeekMessage doesn't (like _kbhit).
Consider the following loop:
while (GetMessage(&msg, NULL, 0, 0))
{
// We don't get inside the braces till there's a message.
TranslateMessage(&msg);
DispatchMessage(&msg);
DoSomething()
}
// Quit the program when GetMessage returns NULL.
return msg.wParam;
Here, DoSomething won't get done until a message—any message—is put
on the queue and processed. If DoSomething happens to generate a
message—for instance, if it updates the screen and so produces a WM_PAINT
message—then fine, the pump will keep running after it's primed. This is a bad
way of getting DoSomething to do its thing reliably, and makes the code
confusing, but it works well enough to appear in at least one textbook of
advanced Windows programming.
PeekMessage, by contrast, yields the floor as soon as it has checked
the queue, regardless of whether there were any messages waiting. In our
example, we're actually using PeekMessage to process the messages as
well (by dispatching each message it finds and using the PM_REMOVE argument to
clear it from the queue). This is a bit more straightforward than the
following, equally valid, code:
if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
{
if (!GetMessage(&msg, NULL, 0, 0)) break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else DoSomething();
It's important to note that our hypothetical DoSomething is
independent of messages; it will be executed regardless of what message just
came off the queue or whether there was any message there at all. In Moby Dick
we've placed the screen update and victory check here simply because it's a
convenient place to check on whether anything significant has happened in any
of several routines.
So is the message loop the game loop? In an abstract sense, yes, since it is
the big gear that sets the little gears spinning. But though it may be
convenient to put some function calls here, sound Windows programming decrees
that any action taken in response to a message should be placed in the message
handler (that is, the window procedure). In a real-time game, most of the
action may take place in one or more WM_TIMER handlers. Turn-based games are
likely to put a heavy load on the handlers for input messages.
In fact, you may find yourself doing nothing at all within the message loop
itself, other than the standard task of translating and dispatching messages.
If that's the case, you can go back to using GetMessage, since nothing
needs to happen except in response to an existing message.
Loops: Conclusions
In summary, two main points emerge:
- The Windows message loop is not
the same as the game loop. The game loop still exists (at least
conceptually), but its components are likely to be more widely dispersed
through your code than they were under MS-DOS.
- Use PeekMessage rather
than GetMessage if you want to execute any code within the message
loop, independent of timer or input messages.
Keyboard Input
In both versions of Moby Dick, we maintain a table of key states that tells
us which arrow or numerical keypad keys are pressed when a call is made to Move_Ahab.
But in the Windows version we can't update this table in a keyboard interrupt
handler—that code is inside the black box, which we're not allowed to open.
In recompense, Windows gives us two ways to keep track of the keyboard
state—through messages generated when a key is pressed or released and through
function calls to check the keyboard state. Let's examine these two approaches
to updating the key table in Moby Dick.
Respond to Messages
The most obvious method of dealing with keyboard input is similar to what we
did in the interrupt handler in the MS-DOS version of Moby Dick. Taking
advantage of the Windows messaging system, we look at every press and release
of a directional key and immediately update the key table in response. This is
done, of course, in the handler for WM_KEYDOWN and WM_KEYUP messages—the
"Good Windows Citizen" approach to handling input.
The problem with this method is that a quick tap on a key may allow the
key's state to return to "up" by the time the key table is consulted
in Move_Ahab; in other words, the keystroke is lost. This is a definite
no-no in any application, and no less so in a game where the player may want to
give a playing piece or the point of view a little nudge by lightly tapping a
key.
A solution is to preserve the "down" state of the key until we've
actually responded to the keystroke. Continue to update the key table when
WM_KEYDOWN messages are received, but don't handle WM_KEYUP messages. Instead,
use GetAsyncKeyState within Move_Moby to check whether each key
is currently down. If it isn't, clear the table entry for that key.
This is not the way we've actually done things in Moby Dick, but the
following fragments show how it could have been done:
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
.
.
.
case WM_KEYDOWN:
switch( wParam )
{
case VK_LEFT:
key_table[INDEX_LEFT] = 1;
break;
// and so on with the other keys
.
.
.
break;
.
.
.
} // end of WindowProc
int Move_Ahab();
{
.
.
.
if (key_table[INDEX_LEFT])
{
AhabX--
if (!GetAsyncKeyState(VK_LEFT)) key_table[INDEX_LEFT] = 0;
}
// and so on with the rest of the key table
.
.
.
} // end of Move_Ahab
Check the Keyboard State
But if we're going to use GetAsyncKeyState, which goes (almost)
straight to the hardware, why not bypass the message system altogether? That's
what we've actually done in Moby Dick Windows.
The handy thing about GetAsyncKeyState is that it returns two items
of information. The most significant bit of the return value tells us whether
the key is now down—period. That's fine for when we check the keyboard, but
what about the times in between—how can we keep from losing that quick
keystroke? That's where the other item of information comes in. The least
significant bit is set if the key has ever been down since the last time
we looked—even if it's now up. So by simply looking for a nonzero return value
from GetAsyncKeyState, we can tell whether a key is down or has been
down during the most recent pass through the game loop. (Of course, we can't
tell whether the key has been tapped twice since we last checked it, but
the game loop should check the keyboard often enough to keep that from being an
issue.)
There's one important thing about GetAsyncKeyState that is not well
explained in the API reference. The least significant bit of the return value
is set only by an "official" keystroke—that is, a key that is
considered to be in a down state after taking into account the user's settings
for repeat delay and repeat rate. This is the bit you would check if you were
writing a word processing application and didn't want things like ttthhhiiisss
to happen. Obviously we can't rely on the least significant bit by itself for
real-time keyboard monitoring; if we did, Ahab would respond to a held-down key
by moving a square, hesitating, then continuing in the same direction at
whatever rate was defined in Control Panel. Fortunately, the most significant
bit reflects the current, uninterpreted state of the key, and we use this for
monitoring continuous presses.
One more warning. Remember that your application is still working even when
it's in the background. If it's keeping track of the keyboard the Good Citizen
way, you don't have to worry, because it won't be receiving any WM_KEYDOWN or
WM_KEYUP messages. But if you're monitoring the keyboard directly, your game
will still be processing every keystroke, even though the keystrokes are
intended for another app. Be sure to turn off keystroke processing when your
game loses the input focus.
Timing
Using Timers
Moby Dick MS-DOS uses a timer-interrupt service routine to ensure that Ahab
and the whale move at a steady rate independent of processor speed. Windows
doesn't give you this kind of access to the hardware, but it provides a couple
of timers to let you accomplish the same thing.
When you create a timer in your program, you are telling Windows to do
something at a specified interval. What Windows does is a little different
depending on which type of timer you use. The standard timer, invoked with SetTimer,
simply posts a WM_TIMER message that is dispatched to either the standard
window procedure or a special callback function you've defined. The multimedia
timer bypasses the message queue altogether.
Limitations of the Standard Timer. The standard timer has the same
resolution as the normal timer interrupt we used in Moby Dick MS-DOS—about 55
milliseconds. In other words, regardless of the time-out value you set, it is
going to be at least 55 milliseconds between WM_TIMER messages. It may be even
longer before they are processed, because Windows doesn't give timer messages
any priority—they have to wait in line like anybody else.
Also, WM_TIMER messages are like WM_PAINT messages: if there's already one
in the queue, no more get added. So if Windows happens to be busy doing other
things, and a few ticks of the clock go by, your program doesn't get a chance
to respond—it will simply skip a few beats rather than racing to catch up. For
real-time games that depend on frequent world updates, skipping a beat is not
an option.
The Multimedia Timer. The multimedia extensions to Windows include a
high-resolution timer, invoked with timeSetEvent, that has a resolution
of 1 millisecond. Besides having this higher resolution, the timer can produce
more accurate results because it does not rely on WM_TIMER messages sent
through the queue.
In fact, each multimedia timer is put in its own thread, and the callback
function is invoked directly regardless of any pending messages or other things
that may be going on. That's an important point, because it means you have to
be careful about accessing global data—a variable could be changed by your
timed procedure, for example, just as some other function called in the normal
message-processing loop is using that data. If you do set up a multimedia
timer, make sure you understand about synchronization, semaphores, and all the
other apparatus of multithread programming. (Schildt [see bibliography] gives a
good overview.)
To use timeSetEvent you must include MMSYSTEM.H and link in
WINMM.LIB.
Timer Latency Under Windows 95. Despite its high resolution, the
multimedia timer can suffer from delays, and this "latency" can be
considerably greater under Windows 95 than under 3.1. For applications
requiring very high timer accuracy, you may have to implement the multimedia
timer in a 16-bit DLL. Mark McCulley leads you through the fancy footwork in
his article "Overcoming Timer-Latency Problems in MIDI Sequencers."
Doing Without Timers
As we've seen, using a multimedia timer automatically creates a separate
thread in your program, which may also create the need for safeguards against
untimely access to data. But you don't really have to go through all the
hassles of setting up semaphores and critical sections, because it's just as
easy to program your game without using timers at all.
Let's say you have to feed your parking meter every hour. You can set your
wristwatch to beep at hourly intervals, or you can check your watch every five
minutes or so to see if it's time to go out yet. The first is the timer method;
the second amounts to polling the system clock. On the face of it, polling
might seem to be less efficient, but Windows has its own overhead in
determining when to trigger a timer, so the difference is probably not that
great.
Two high-resolution polling functions are available. The first is timeGetTime,
which returns the number of milliseconds that have elapsed since Windows
started. The default resolution is 1 millisecond, except under Windows NT (see
the API reference). As with timeSetEvent, you need to link to the
multimedia library to use this function.
A resolution of 1 millisecond would seem more than adequate for any
real-time game. However, the problems of latency associated with timeSetEvent
apply to timeGetTime as well. With Time Waster running in the
background, I have recorded delays of up to 100 milliseconds before timeGetTime
reports a 1-millisecond "tick." Then, on a subsequent call, the
missing time is made up. The result is a stutter that can affect timed events.
For instance, if your game is updating the world 30 times per second, a single
delay of even 40 milliseconds can cause the update routine to skip a beat and
then take two quick beats to catch up. If the stutter happens to correspond to
a screen update, the animation will not be smooth.
For high-performance games, the solution is either to use the multimedia
timer in a 16-bit DLL or to turn to the highest-resolution time service of all,
QueryPerformanceCounter. This function was designed mainly for
profiling, but there's no reason we can't use it as a general-purpose clock.
Like timeGetTime, QueryPerformanceCounter returns the time
elapsed since the system started. The unit of measurement is determined by the
hardware; on Intel-based CPUs it is about 0.8 microseconds.
The following fragment of a WinMain function shows how you might use QueryPerformanceCounter
to update your game world every tenth of a second.
#define UPDATE_TICKS_MS 100 // milliseconds per world update
_int64 start, end, freq, update_ticks_pc
MSG msg;
// Get ticks-per-second of the performance counter.
// Note the necessary typecast to a LARGE_INTEGER structure
if (!QueryPerformanceFrequency((LARGE_INTEGER*)&freq))
return -1; // error – hardware doesn't support performance counter
// Convert milliseconds per move to performance counter units per move.
update_ticks_pc = UPDATE_TICKS_MS * freq / 1000;
// Initialize the counter.
QueryPerformanceCounter((LARGE_INTEGER*)&start);
{
// the main message loop begins here -- while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
QueryPerformanceCounter((LARGE_INTEGER*)&end);
// The inner loop ensures that the world gets updated more than
// once if need be.
while ((end - start) >= update_ticks_pc)
{
UpdateWorld();
start += update_ticks_pc;
}
} // End of message loop.
return msg.wParam;
Note that QueryPerformanceFrequency will return FALSE if the hardware
does not support the performance counter. It's unlikely that this will be the
case in any machine capable of running Windows 95, but I don't know this for a
fact. You may want to have a fallback routine that uses timeGetTime
instead.
Getting a Bigger Slice of the Time Pie
The Competition. Sometimes it seems that Windows giveth with one hand
and taketh away with the other. With the right hand it provides the ability to
measure time down to a millionth of a second, and then the left hand comes and
grabs the CPU just when you need it, so that your sprites end up lurching
across the screen like so many ballerinas in clogs.
To see a graphic illustration of the problem, start up Moby Dick and then
run a few instances of Time Waster. Click on the Moby Dick window to reactivate
the game, and observe the unsteady progress of the cloud. If Time Waster is
really gobbling up CPU time, even the ship animation (at the incredibly taxing
rate of five frames per second) becomes disrupted.
By the way, you may notice that Time Waster continues to mire your system's
performance even after all instances have been closed. This is because all the
virtual memory it has allocated on the hard drive has to be freed up. Windows
is probably doing some swap-file cleanup as well.
Even though it is unlikely that players are going to be running
processor-intensive tasks in the background, your game still has to be prepared
to share time with network activity and Windows housekeeping. You cannot
monopolize the CPU as you did under MS-DOS. The best you can hope for is to
give your game a bigger share of the time pie.
Adjusting Priorities. Windows doesn't necessarily allot an equal
amount of CPU time to each of the tasks it is handling. It divvies up the pie
according to the overall priority class of applications and the thread
priority of individual threads within those applications. Both of these are
determined by the application developer.
However, Windows also boosts a thread's priority whenever, as the API
reference says, "significant things happen to the thread." You can
see this quite graphically in Moby Dick by opening a menu or dialog box when
the cloud thread is having trouble getting enough CPU time: the movement of the
cloud immediately becomes smoother. Windows is dynamically boosting the
priority of all threads in the application because it is guessing that the user
wants to do something and is expecting a quick response.
The Settings command on the View menu of Moby Dick Windows
allows you to alter the priority class of the application. You can also set the
priority of the main thread and the thread that moves the cloud across the
screen. Be warned: certain combinations of priorities will make it impossible
for Windows to function properly, and you will have to shut down Moby Dick with
CTRL+ALT+DEL.
Changing priority classes and thread priorities is unquestionably a risky
business, and you should carefully study the API reference for SetPriorityClass
before making any design decisions. In particular, note that "a thread
with a base priority level above 11 interferes with the normal operation of the
operating system." This means that you can't safely go above a combination
of HIGH_PRIORITY_CLASS and THREAD_PRIORITY_LOWEST, or NORMAL_PRIORITY_CLASS and
THREAD_PRIORITY_HIGHEST. Not much room for maneuver, I'm afraid.
By experimenting with the priorities in Moby Dick Windows while running one
or more instances of Time Waster in the background, you should be able to gauge
the effect on the game and on the system as a whole. In the end, you may come
to the conclusion that resetting priorities for the life of the program causes
more problems than it solves. Temporarily boosting priorities for certain
time-critical tasks may, however, still be a useful technique.
Acknowledgments
Thanks to Ken Lemieux of Deep River Publishing, who generously shared his
knowledge of game structure and timing issues.
Some of the MS-DOS graphics routines were adapted from the "VGA
Tutorial" by Grant Smith (a.k.a. Denthor) and Christopher G. Mann.
The germ of the MS-DOS interrupt-handling routines was taken from Secrets
of the Game Programming Gurus (see the bibliography).
Bibliography
This short list includes books and articles particularly relevant to this
article. It is not intended as a complete bibliography of games programming
under Windows.
Deobald, Martyn. Windows Game SDK Developer's Guide. Coriolis Group
Books, 1996. A good introduction to DirectX. Includes a class-based library and
a CD with much shareware and public-domain material related to game
development.
"High-Precision Timing Under Windows, Windows NT, and Windows 95."
Microsoft Knowledge Base, PSS ID Number Q148404.
LaMothe, André, et al. Tricks of the Game Programming Gurus. Sams
Publishing, 1994. Mostly MS-DOS–specific but does cover basic design topics
like 3-D modeling and artificial intelligence.
Lyons, Eric R. Black Art of Windows Game Programming. Waite Group
Press, 1995. Does not take Windows 95 and DirectX into account but is still a
useful general introduction.
McCulley, Mark. "Overcoming Timer-Latency Problems in MIDI
Sequencers." (MSDN Library, Technical Articles)
Microsoft Corporation. "Real-time Systems and Microsoft Windows
NT." Covers the question of timer latency. (MSDN Library, Backgrounders)
Morrison, Michael, and Randy Weems. Windows 95 Game Developer's Guide
Using the Game SDK. Sams Publishing, 1996. An MFC-oriented approach to game
development.
Schildt, Herbert. Advanced Windows 95 Programming in C and C++.
Osborne McGraw Hill, 1996. Contains a good introduction to programming with
threads.
Peter Donnelly has been a game designer since 1972 and a programmer since
1984. He has authored four "paper" games with historical and fantasy
themes, and his computer design credits include three interactive comic-book
adventures and one full-scale adventure game. He has also developed shareware
games and game editors for MS-DOS and Windows. |