A WTL Game Loop






4.43/5 (5 votes)
A message loop class that is suitable for game programming in WTL.
Introduction
WTL is great for developing light-weight Windows applications because of the shallow wrapper classes that it provides. WTL is easily extendable as well. I am currently working on a simple game to learn the basics of DirectX. I am very comfortable with WTL and I thought that it would be a good framework to use to develop my game with.
I have written a new message loop class called CGameLoop
that derives from
CMessageLoop
, and
is more suitable for game programming with WTL window support.
Design
Once I looked at how the CMessageLoop
class implemented its message loop, I realized that
it would not be good enough to use in a game, simply because CMessageLoop
uses
GetMessage
.
GetMessage
blocks with a call to WaitMessage
whenever the message queue becomes empty to
keep the current process from using all of the CPU cycles. At that point I replaced the
CMessageLoop::Run
command in WinMain
with my own loop that looks similar to what DirectX
samples use. Here it is:
while( TRUE ) { MSG msg; if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ) { // Check for a quit message if( msg.message == WM_QUIT ) break; TranslateMessage( &msg ); DispatchMessage( &msg ); } else if(wndMain.IsPaused()) { WaitMessage(); } else { wndMain.UpdateFrame(); } }
This loop worked perfectly fine for what I was doing. However, this loop is a down-grade
from the loop found in CMessageLoop
. This is because there is no mechanism for the users of
the message loop to pre-translate the messages, or process idle messages. These are nice
features of CMessageLoop
. Therefore I decided to create CGameLoop.
CGameLoop
CGameLoop
derives directly from CMessageLoop
, and it provides all of the functionality
of CMessageLoop
. This class also adds the functionality required to support games.
class CGameLoop : public CMessageLoop { public: ... };
Features
Here is a list of the features that CGameLoop
provides.
- PeekMessage:
PeekMessage
is used to remove messages from the queue rather thanGetMessage
because it does not block when the queue is empty. This will allow processing to fall through to the game when the message queue is empty. - PreTranslate Messages: Messages can still be pre-translated by the windows that depend on the game loop.
- Idle Message Handler: Idle messages are generated when the message queue becomes empty.
This differs from
CMessageLoop
. BecauseCMessageLoop
generates an idle message after every message that is removed from the queue, except for mouse move and paint messages. - Pause: The loop will call
WaitMessage
if the game handler indicates that the game is paused. - UpdateFrame: This is a function that the game loop will call to update the next frame of the game. This is where most of the game processing will occur.
CGameLoop::Run, (Run game loop run!)
The new game loop that is located in Run, manages all of the features for CGameLoop
. Here is
the code that is contained in CGameLoop::Run
:
virtual int Run() { bool isActive = true; while (TRUE) { if (PeekMessage( &m_msg, NULL, 0, 0, PM_REMOVE)) { //C: Check for the WM_QUIT message to exit the loop. if (WM_QUIT == m_msg.message) { ATLTRACE2(atlTraceUI, 0, _T("CGameLoop::Run - exiting\n")); break; // WM_QUIT, exit message loop } //C: Flag the loop as active only if one of the active messages // is processed. if (IsIdleMessage(&m_msg)) { isActive = true; } //C: Attmpt to translate and dispatch the messages. if(!PreTranslateMessage(&m_msg)) { ::TranslateMessage(&m_msg); ::DispatchMessage(&m_msg); } } else if (isActive) { //C: Perform idle message processing. OnIdle(0); //C: Flag the loop as inactive. This will prevent other // idle messages from being processed while no messages // are occurring. isActive = false; } else if (m_gameHandler) { //C: Is the game paused. if (m_gameHandler->IsPaused()) { //C: To keep the program from spinning needlessly, wait until // the next message enters the queue before processing any // more data. WaitMessage(); } else { //C: All other activities are taken care of, update the current // frame of the game. m_gameHandler->OnUpdateFrame(); } } } //C: Returns the exit code for the loop. return (int)m_msg.wParam; }
Warning!: CMessageLoop::Run
is not a virtual
function.
Therefore if CGameLoop::Run
is to be used polymorphically, then you will need to modify the WTL header file
ATLAPP.H, in order to make CMessageLoop::Run
a virtual
function. This will allow
CGameLoop::Run
to function
properly when used in a polymorphic setting. Fortunately for most regular uses, this will not need
to be done.
PeekMessage
PeekMessage
allows the message loop to see if there are any messages currently in the queue, and
to continue processing even if there are none. The key to using PeekMessage
is to use the
PM_REMOVE
flag. This allows PeekMessage
to function like GetMessage
without blocking.
PreTranslate Messages
One of the neat features of CMessageLoop
, is the ability for a window to register a
PreTranslate
handler with the message loop, and allow that window to filter the messages that are processed.
By deriving CGameLoop
from CMessageLoop
, this functionality is automatically inherited.
Idle Message Handler
One other feature of CMessageLoop
that is not suitable for game programming is the way that
the Idle message handler was implemented. Here is the code from CMessageLoop::Run
:
... while(!::PeekMessage(&m_msg, NULL, 0, 0, PM_NOREMOVE) && bDoIdle) { if(!OnIdle(nIdleCount++)) bDoIdle = FALSE; } bRet = ::GetMessage(&m_msg, NULL, 0, 0); // Translate and Dispatch the message. ... if(IsIdleMessage(&m_msg)) { bDoIdle = TRUE; nIdleCount = 0; }
With this code, PeekMessage
would be called, until a message was found that handled the Idle message.
Then GetMessage
is called, and the message is dispatched. At the end of the loop, the ID of
the message is tested in IsIdleMessage
. If this message is determined to be an Idle message, then the
idle bit is reset, and the next message to pass through the message queue will generate a second
idle processor.
The good thing about IsIdleMessage
, is that it tests if the current message is a mouse move message,
a paint message, or a timer message. If one of these messages is processed, then it will not reset
the idle bit. The bad thing is that along with a WM_MOUSEMOVE
message, comes a
WM_NCHITTEST
and
WM_SETCURSOR
message. These are two messages that are still not filtered off. If your application
has a long OnIdle
processing function, this could waste serious processing cycles that would be
better spent on your graphics.
I have done two things to solve this problem, and still allow OnIdle
processing to exist.
- Idle processing is only generated when the message queue is empty, rather than once after every message that is not a mouse move, paint or timer message.
- The
WM_NCHITTEST
andWM_SETCURSOR
messages are added to the IsIdleMessage function test in order prevent an idle update from being generated when just the mouse is moved.
This small piece of code illustrates the changes made in CGameLoop
:
if (PeekMessage( &m_msg, NULL, 0, 0, PM_REMOVE)) { ... //C: Flag the loop as active only if an active message // is processed. if (IsIdleMessage(&m_msg)) { isActive = true; } ... } else if (isActive) { //C: Perform idle message processing. OnIdle(0); //C: Flag the loop as inactive. This will prevent other // idle messages from being processed while the message // queue is empty. isActive = false; } else if (...) { ... }
Pause & OnUpdateFrame
These functions can be systematically added to the GameLoop
at runtime by
registering with the game loop in the same way that OnIdle
handlers register with
CMessageLoop
. The object that is used to register with the game loop is CGameHandler
.
However, only one CGameHandler
object can be registered with the game loop.
This differs from the OnIdle
handler because CMessageLoop
imposes no limit to the
number of registered OnIdle
handlers.
CGameHandler
CGameHandler is an abstract interface, that your window should derive from. Two functions are provided, and required to be implemented. Here is the prototype for CGameHandler:
class CGameHandler { public: virtual BOOL IsPaused() = 0; virtual HRESULT OnUpdateFrame() = 0; };
IsPaused
This function will report if the game is currently in a paused state. This will have the effect of blocking the message queue from spinning, if the game is currently paused. If you want to handle the logic for your pause state in your game, simply return FALSE for the implementation of this function. You may want to do this if you would like to display animations in your paused state.
OnFrameUpdate
This is where the game state will be updated. When ever the message queue is not processing messages, and the idle handler has been processed, this function will be called. All of your game state, animations, and display updates should occur in this function.
Register CGameHandler
In order to get updates from the CGameLoop
, a window must register itself with the game loop
class. Only one window can be registered with a class at a time. Therefore, it may be
wise to check if another window is receiving frame updates, and make sure that you call that
windows OnUpdateFrame
handler after you are finished processing your data. You can use the
GetGameHandler
and SetGameHandler
functions to register your game
handler with CGameLoop
.
Here is an example of the code found in a windows OnCreate
handler, that registers their
CGameHandler
object with the CGameLoop
:
LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { // Perform other initializations here. ... // register object for message filtering and idle updates CMessageLoop* pLoop = _Module.GetMessageLoop(); ATLASSERT(pLoop != NULL); pLoop->AddMessageFilter(this); pLoop->AddIdleHandler(this); //C: Register this object as the UpdateFrame as well. // But we need the CGameLoop object to do that. CGameLoop *pGameLoop = dynamic_cast<CGameLoop*>(pLoop); ATLASSERT(pGameLoop); pGameLoop->SetGameHandler(this); return 0; }
Improvements
Improvements can be made to the design of this class. But I chose not to implement them
at this time, because they were not important to me. I had thought about allowing
the developer to chose the messages that are considered active messages inside of the
IsIdleMessage
test. I also thought about converting that test to a table based implementation
in order to speed up the lookup at the cost of memory space.
CGameHandler::OnUpdateFrame
returns a HRESULT
, but currently this value is not tested
inside of CGameLoop::Run
. Another possible improvement is to test this value in a debug
mode and emit a TRACE statement when the OnUpdateFrame
handler fails.
Please let me know if you think these features would be useful, or if you have any other ideas for improvements.
Demonstration
The demonstration application was created to simply show how the CGameLoop
class
replaces the CMessageLoop
for a WTL based game application. The shortest, fastest thing
that I could think of was a monitor to show which keys are currently pressed. The output is
not entirely accurate because GetKeyboardState
has been used instead of
DirectInput. GetKeyboardState
only recognizes that keys have been pressed if
they have been processed in a message queue. Also, the menus and toolbar buttons do not
perform any actions.
However, this application does illustrate how to setup the CGameLoop
, register it with the
_Module
instance and register the CGameHandle
object.
Conclusion
CGameLoop
has been a useful replacement for CMessageLoop
in the current game that I am
developing, and it also allows me to take advantages of the features that are provided
in CMessageLoop
. I hope that you find it useful.