WTL游戏循环






4.43/5 (5投票s)
一个适用于 WTL 游戏编程的消息循环类。
引言
WTL 因其提供的浅层包装器类而非常适合开发轻量级 Windows 应用程序。WTL 也易于扩展。我目前正在开发一个简单的游戏来学习 DirectX 的基础知识。我对 WTL 非常熟悉,并认为它是一个很好的框架来开发我的游戏。
我编写了一个新的消息循环类,名为 CGameLoop
,它派生自 CMessageLoop
,并且更适合使用 WTL 窗口支持进行游戏编程。
设计
一旦我查看了 CMessageLoop
类如何实现其消息循环,我就意识到它不足以用于游戏,仅仅因为 CMessageLoop
使用 GetMessage
。GetMessage
在消息队列变空时会阻塞并调用 WaitMessage
,以防止当前进程占用所有 CPU 周期。此时,我将 WinMain
中的 CMessageLoop::Run
命令替换为我自己的循环,该循环类似于 DirectX 示例使用的。
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(); } }
这个循环对我正在做的事情来说效果很好。然而,这个循环比 CMessageLoop
中的循环有所降级。这是因为没有机制供消息循环的用户预处理消息或处理空闲消息。这些是 CMessageLoop
的一些不错的功能。因此,我决定创建 CGameLoop。
CGameLoop
CGameLoop
直接派生自 CMessageLoop
,并提供 CMessageLoop
的所有功能。此类还增加了支持游戏所需的功能。
class CGameLoop : public CMessageLoop { public: ... };
特点
以下是CGameLoop
提供功能的列表。- PeekMessage:
PeekMessage
用于从队列中删除消息,而不是GetMessage
,因为它在队列为空时不会阻塞。这将允许处理在消息队列为空时直接传递到游戏。 - 预处理消息:依赖于游戏循环的窗口仍然可以预处理消息。
- 空闲消息处理程序:当消息队列变空时会生成空闲消息。这与
CMessageLoop
不同。因为CMessageLoop
在从队列中删除每条消息后都会生成一条空闲消息,除了鼠标移动和绘制消息。 - 暂停:如果游戏处理程序指示游戏已暂停,循环将调用
WaitMessage
。 - UpdateFrame:这是一个游戏循环用于更新下一帧游戏的功能。大部分游戏处理将在此处进行。
CGameLoop::Run,(运行游戏循环!)
位于 Run 中的新游戏循环管理着 CGameLoop
的所有功能。这是 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; }
警告!:CMessageLoop::Run
不是一个 virtual
函数。因此,如果 CGameLoop::Run
要在多态地使用,那么您将需要修改 WTL 头文件 ATLAPP.H,以便将 CMessageLoop::Run
设为 virtual
函数。这将允许 CGameLoop::Run
在多态环境中使用时正常工作。幸运的是,对于大多数常规用途,这并不需要。
PeekMessage
PeekMessage
允许消息循环查看队列中当前是否有任何消息,并在没有消息时继续处理。使用 PeekMessage
的关键是使用 PM_REMOVE
标志。这使得 PeekMessage
的功能类似于 GetMessage
,但不会阻塞。
预处理消息
CMessageLoop
的一个很棒的功能是,窗口能够向消息循环注册一个 PreTranslate
处理程序,并允许该窗口过滤要处理的消息。通过将 CGameLoop
派生自 CMessageLoop
,此功能会自动继承。
空闲消息处理程序
CMessageLoop
的另一个不适合游戏编程的特性是空闲消息处理程序的实现方式。这是来自 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; }
使用此代码,将调用 PeekMessage
,直到找到一条处理空闲消息的消息。然后调用 GetMessage
,消息将被分派。在循环结束时,将在 IsIdleMessage
中测试消息的 ID。如果此消息被确定为空闲消息,则空闲位将被重置,下一条通过消息队列的消息将生成第二个空闲处理器。
IsIdleMessage
的好处是它测试当前消息是否是鼠标移动消息、绘制消息或计时器消息。如果处理了其中一条消息,它就不会重置空闲位。坏处是,除了 WM_MOUSEMOVE
消息,还会伴随 WM_NCHITTEST
和 WM_SETCURSOR
消息。这两条消息仍未被过滤掉。如果您的应用程序有一个很长的 OnIdle
处理函数,这可能会浪费宝贵的处理周期,这些周期可以更好地用于您的图形。
为了解决这个问题,我做了两件事,同时仍然允许 OnIdle
处理存在。
- 空闲处理仅在消息队列为空时生成,而不是在每条非鼠标移动、绘制或计时器消息之后生成一次。
WM_NCHITTEST
和WM_SETCURSOR
消息被添加到IsIdleMessage
函数测试中,以防止在仅仅移动鼠标时生成空闲更新。
这段小代码说明了 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 (...) { ... }
暂停 & OnUpdateFrame
这些函数可以通过在运行时注册到游戏循环来系统地添加到 GameLoop
中,这与 OnIdle
处理程序注册到 CMessageLoop
的方式相同。用于注册到游戏循环的对象是 CGameHandler
。但是,一次只能将一个 CGameHandler
对象注册到游戏循环。这与 OnIdle
处理程序不同,因为 CMessageLoop
对已注册的 OnIdle
处理程序的数量没有限制。
CGameHandler
CGameHandler 是一个抽象接口,您的窗口应该从中派生。提供了两个函数,并且需要实现。以下是 CGameHandler 的原型。
class CGameHandler { public: virtual BOOL IsPaused() = 0; virtual HRESULT OnUpdateFrame() = 0; };
IsPaused
此函数将报告游戏当前是否处于暂停状态。这将有效地阻止消息队列旋转,如果游戏当前已暂停。如果您想在暂停状态下处理游戏的逻辑,只需为该函数的实现返回 FALSE。您可能希望这样做,如果您想在暂停状态下显示动画。
OnFrameUpdate
游戏状态将在此时更新。当消息队列不处理消息,并且空闲处理程序已处理后,将调用此函数。您的所有游戏状态、动画和显示更新都应在此函数中进行。
注册 CGameHandler
为了从 CGameLoop
获取更新,窗口必须将自身注册到游戏循环类。一次只能有一个窗口注册到类。因此,在您完成处理数据后,最好检查是否有其他窗口正在接收帧更新,并确保调用该窗口的 OnUpdateFrame
处理程序。您可以使用 GetGameHandler
和 SetGameHandler
函数将您的游戏处理程序注册到 CGameLoop
。
以下是一个窗口 OnCreate
处理程序中代码的示例,该代码将其 CGameHandler
对象注册到 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; }
改进
可以对该类的设计进行改进。但此时我选择不实现它们,因为它们对我来说并不重要。我曾考虑允许开发人员选择在 IsIdleMessage
测试中被视为活动消息的消息。我还考虑将该测试转换为基于表的实现,以牺牲内存空间来加快查找速度。
CGameHandler::OnUpdateFrame
返回一个 HRESULT
,但目前这个值在 CGameLoop::Run
中没有被测试。另一个可能的改进是在调试模式下测试此值,并在 OnUpdateFrame
处理程序失败时发出 TRACE 语句。
如果您认为这些功能有用,或者您有其他改进想法,请告诉我。
演示
演示应用程序的创建只是为了展示 CGameLoop
类如何替换基于 WTL 的游戏应用程序的 CMessageLoop
。我能想到的最短、最快的东西是一个监视器,用于显示当前按下的键。输出并不完全准确,因为使用了 GetKeyboardState
而不是 DirectInput。GetKeyboardState
仅在按键已在消息队列中处理时才识别按键。此外,菜单和工具栏按钮不执行任何操作。
然而,此应用程序确实说明了如何设置 CGameLoop
,将其注册到 _Module
实例,以及注册 CGameHandle
对象。
结论
CGameLoop
一直是当前我正在开发的と言う游戏中 CMessageLoop
的有用替代品,并且它还允许我利用 CMessageLoop
中提供的功能。我希望您觉得它也很有用。