Writing the Game
Loop
Written by Robert
Dunlop
Microsoft DirectX MVP |
|
What Is It?
At the core of every complex application is a loop - a portion of code that
gets executed time and again throughout the life of the program. This
loop, often referred to as the "Main" loop, is responsible for
coordinating the programs actions, and establishes the backbone of an
application.
The concept of a main loop has been present since the beginning, but their
form has changed much as the world has moved from DOS style operating systems
into the multi-threaded realm. These changes have some important
ramifications for high performance applications such as games, and since this is
the core that your application will be built upon it is important to pick the
right path early on.
|
The Good Old Days
Many game developers, even though they may be happily entrenched in
the latest technology, will reminisce fondly over the day when DOS
ruled the land. Even though the hardware had a fraction of
today's power, and there was no common interface to access the
features of a user's system, one thing it definitely did provide - a
sense of control.
The flowchart on the left illustrates a typical game loop that
you would find in a DOS based game.
Unlike the multi-threaded applications of today, where an
application must contend for a time slice, the programmer has full
control over virtually every operation the processor executes.
|
The Windows Message Loop
Now that we have taken a look at the way things were handled in the DOS days,
a quick look that the structure of a typical Windows application will show a
different world altogether :
|
After looking at the game loop in DOS, our first glance at the main
loop in a Windows application may be a little confusing.
First of all, where did everything go??? Nothing of any real
importance seems to be happening here!
Well, this is because Windows is an "Event Driven"
operating system. Rather than being a based upon a constant
course of action, a Windows program consists of routines that are
responsible for handing various events, such as the mouse moving, the
screen being redrawn, etc.
Under this structure, then, the sole job of the main loop is to
make sure that these events get handled. It waits for
Windows to send a message that an even has occurred, and then turns
around and send it to the appropriate handler.
|
Can This Work for Games??
In its default for, the message loop model of a Windows program is not one
that will lend itself to game performance. The reason is that the design
philosophy behind a typical Windows application and a high performance game are
totally different:
As you can see, these are two very different ways of thinking. So, do
we just replace our message loop with a game loop akin to the days of old?
The answer is a resounding NO!!!
Taking complete and utter control of the system in the Windows world will not
provide the performance we need. Rather, it will bring the system to its
knees. The reason for this is simple : we are reliant on other services to
provide access to the system, so we must let Windows run on.
So, is there a happy compromise to be found? A way to insure our
performance, while allowing the system to take care of things? Yes, there
is, but it will require a few changes.
Dealing With Messages
A typical message loop in Windows looks something like this :
MSG msg;
while( GetMessage( &msg, NULL, 0, 0 ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg );
}
The function of this loop, known as the "message pump", can be
broken down by looking at each of its functions:
GetMessage : This function is used to retrieve a message from the Message
Queue. If there is not a message available, calling this function causes
the thread to sleep until a message is received. If the message is
WM_QUIT, then GetMessage() will return zero, causing this loop to exit.
TranslateMessage : This function provides translation of keyboard messages,
and must be called on all messages before dispatching them.
DispatchMessage : Calls the appropriate handler or sends the message to the
appropriate child window.
Keeping Awake
The problem with using such a message pump to drive a game is that the
GetMessage function will cause the thread to sleep until a message is available.
This puts us at the mercy of Windows, causing our main loop only to execute when
Windows has a message for us.
Below is a revised message loop that is better suited to our needs :
MSG mssg;
// prime the message structure
PeekMessage( &mssg, NULL, 0, 0,
PM_NOREMOVE);
// run till completed
while (mssg.message!=WM_QUIT) {
// is there a message to process?
if (PeekMessage( &mssg, NULL, 0, 0, PM_REMOVE)) {
// dispatch the message
TranslateMessage(&mssg);
DispatchMessage(&mssg);
} else {
//
our stuff will go here!!
}
}
Looking at the revised code, you should note several major differences :
| PeekMessage is being used in place of GetMessage, to avoid being put to
sleep |
| There is now a test for the WM_QUIT message. This is because
PeekMessage does not test for this itself, but rather returns whether it
found a message to process. |
| There is now an outer loop encompassing the message pump. This is so
that we can process the game loop, and allow for processing even when there
is no message available. |
Alternatives we have Avoided
Before we proceed to fill out our loop, let's take a brief look at some of
the other possible methods we have avoided :
| OnIdle : This method is intended for handling of low overhead background
tasks that are not time-critical. Obviously this does not describe a
game! Some of the pitfalls that prevent OnIdle from being useful to
us:
| OnIdle will only be called when there are no messages available. |
| Once OnIdle has been started, no more messages can be processed until
OnIdle returns. |
| Once OnIdle lets go of its time slice, the thread will sleep.
OnIdle will not be called again until the thread has received new
messages and finished processing them |
|
| OnTimer : OnTimer and other event based timing techniques are reliant upon
the message loop to activate an event handler in the application. The
messages used for timer completion have a low priority, and may be preempted
by other messages - resulting in delayed notification and inconsistent
timing. |
Applying a Throttle
Now that we have a tight inner loop to control our game, let's take a look at
how to control the rate of the game play. First you may want to review the
timer functions available, which can be found in our article Selecting
Timer Functions
The loop below provides an example of how to create a throttled game loop,
which is limited to 25 frames per second. To keep things simple, we
will limit our game loop to 2 functions :
| MoveObjects(), which will be called once per frame and can be started any
time after the render function is called. |
| RenderFrame(), which is called each frame to draw the scene. This
function will be the pacing function, that we will call every 40 ms. |
MSG mssg;
// message from queue
LONGLONG cur_time; // current time
DWORD time_count=40; // ms per frame, default if no
performance counter
LONGLONG perf_cnt; // performance timer
frequency
BOOL perf_flag=FALSE; // flag determining which timer to use
LONGLONG next_time=0; // time to render next frame
BOOL move_flag=TRUE; // flag noting if we have moved yet
// is there a performance counter available?
if (QueryPerformanceFrequency((LARGE_INTEGER *) &perf_cnt))
{
// yes, set time_count and timer choice flag
perf_flag=TRUE;
time_count=perf_cnt/25; // calculate
time per frame based on frequency
QueryPerformanceCounter((LARGE_INTEGER *) &next_time);
} else {
// no performance counter, read in using timeGetTime
next_time=timeGetTime();
}
// prime the message structure
PeekMessage( &mssg, NULL, 0, 0,
PM_NOREMOVE);
// run till completed
while (mssg.message!=WM_QUIT) {
// is there a message to process?
if (PeekMessage( &mssg, NULL, 0, 0, PM_REMOVE)) {
// dispatch the message
TranslateMessage(&mssg);
DispatchMessage(&mssg);
} else {
// do we need to move?
if (move_flag) {
// yes, move and clear flag
MoveObjects();
move_flag=FALSE;
}
// use the appropriate method to get time
if (perf_flag)
QueryPerformanceCounter((LARGE_INTEGER *) &cur_time);
else
cur_time=timeGetTime();
// is it time to render the frame?
if (cur_time>next_time) {
// yes, render the frame
RenderFrame();
// set time for next frame
next_time += time_count;
// If we get more than a frame ahead, allow us to drop one
// Otherwise, we will never catch up if we let the error
// accumulate, and message handling will suffer
if (next_time < cur_time)
next_time = cur_time + time_count;
// flag that we need to move objects again
move_flag=TRUE;
}
}
}
|