Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A WTL Game Loop

0.00/5 (No votes)
27 Apr 2002 1  
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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here