Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WTL
Article

A WTL Game Loop

Rate me:
Please Sign up or sign in to vote.
4.43/5 (5 votes)
27 Apr 2002CPOL7 min read 90.4K   1.9K   30   8
A message loop class that is suitable for game programming in WTL.

Demonstration app that illustrates the use of the CGameLoop class.

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 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.

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.

  1. 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.
  2. 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))
{
    ...
    //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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer
United States United States
I am a software architect and I have been developing software for nearly two decades. Over the years I have learned to value maintainable solutions first. This has allowed me to adapt my projects to meet the challenges that inevitably appear during development. I use the most beneficial short-term achievements to drive the software I develop towards a long-term vision.

C++ is my strongest language. However, I have also used x86 ASM, ARM ASM, C, C#, JAVA, Python, and JavaScript to solve programming problems. I have worked in a variety of industries throughout my career, which include:
• Manufacturing
• Consumer Products
• Virtualization
• Computer Infrastructure Management
• DoD Contracting

My experience spans these hardware types and operating systems:
• Desktop
o Windows (Full-stack: GUI, Application, Service, Kernel Driver)
o Linux (Application, Daemon)
• Mobile Devices
o Windows CE / Windows Phone
o Linux
• Embedded Devices
o VxWorks (RTOS)
o Greenhills Linux
o Embedded Windows XP

I am a Mentor and frequent contributor to CodeProject.com with tutorial articles that teach others about the inner workings of the Windows APIs.

I am the creator of an open source project on GitHub called Alchemy[^], which is an open-source compile-time data serialization library.

I maintain my own repository and blog at CodeOfTheDamned.com/[^], because code maintenance does not have to be a living hell.

Comments and Discussions

 
QuestionEither WTL message loop has been updated, or you misunderstand the way idle processing works in WTL... Pin
Defenestration1-May-09 13:28
Defenestration1-May-09 13:28 
AnswerRe: Either WTL message loop has been updated, or you misunderstand the way idle processing works in WTL... Pin
Paul M Watt1-May-09 15:06
mentorPaul M Watt1-May-09 15:06 
GeneralRe: Either WTL message loop has been updated, or you misunderstand the way idle processing works in WTL... Pin
Defenestration4-May-09 8:57
Defenestration4-May-09 8:57 
GeneralRe: Either WTL message loop has been updated, or you misunderstand the way idle processing works in WTL... Pin
Paul M Watt6-May-09 5:11
mentorPaul M Watt6-May-09 5:11 
GeneralMore info on game loops Pin
deWiTTERS26-Jul-07 23:50
deWiTTERS26-Jul-07 23:50 
GeneralMay sound obvious : define loop as CGameLoop ! Pin
Rasqual Twilight6-Feb-04 5:18
Rasqual Twilight6-Feb-04 5:18 
GeneralMessage queue only has posted messages, not sent messages Pin
Daniel Bowen17-Feb-03 11:31
Daniel Bowen17-Feb-03 11:31 
GeneralRe: Message queue only has posted messages, not sent messages Pin
Paul M Watt17-Feb-03 17:33
mentorPaul M Watt17-Feb-03 17:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.