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.
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 ) )
{
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
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 than
GetMessage
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
. Because CMessageLoop
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.
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))
{
if (WM_QUIT == m_msg.message)
{
ATLTRACE2(atlTraceUI, 0, _T("CGameLoop::Run - exiting\n"));
break;
}
if (IsIdleMessage(&m_msg))
{
isActive = true;
}
if(!PreTranslateMessage(&m_msg))
{
::TranslateMessage(&m_msg);
::DispatchMessage(&m_msg);
}
}
else if (isActive)
{
OnIdle(0);
isActive = false;
}
else if (m_gameHandler)
{
if (m_gameHandler->IsPaused())
{
WaitMessage();
}
else
{
m_gameHandler->OnUpdateFrame();
}
}
}
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
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.
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.
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);
...
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
and WM_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))
{
...
if (IsIdleMessage(&m_msg))
{
isActive = true;
}
...
}
else if (isActive)
{
OnIdle(0);
isActive = false;
}
else if (...)
{
...
}
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 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.
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 , WPARAM , LPARAM ,
BOOL& )
{
...
CMessageLoop* pLoop = _Module.GetMessageLoop();
ATLASSERT(pLoop != NULL);
pLoop->AddMessageFilter(this);
pLoop->AddIdleHandler(this);
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.