使用OS定时器处理游戏循环

一个简单的游戏循环运行如下:

MSG msg; while (running){ if (PeekMessage(&msg, hWnd, 0, 0, PM_REMOVE)){ TranslateMessage(&msg); DispatchMessage(&msg); } else try{ onIdle(); } catch(std::exception& e){ onError(e.what()); close(); } } 

(来自这个问题 )

我在这里使用Windows作为例子,它可以是任何平台。 纠正我,如果我错了,但这样一个循环将使用100%的CPU /核心(使用我的电脑一个核心的50%),因为它总是检查runningvariables的状态。

我的问题是,使用操作系统实现游戏循环(在Windows示例中)是否更好(性能方面),根据每秒想要的游戏逻辑的所需刻度数设置所需的时间间隔? 我问这是因为我假设定时器function使用CPU的RTC中断。

通常情况下,即使用户没有做任何事情,游戏也会不断绘制新的帧。 这通常会发生在你有onIdle()调用的地方。 如果你的游戏只是在用户按下一个按钮或者偶尔的时候更新窗口/屏幕,那么MSGWaitForMultipleObjects是一个不错的选择。

但是,在连续动画游戏中,如果可以提供帮助,通常不会想要在渲染线程中阻塞或休眠 ,而是希望以最高速度进行渲染,并将垂直同步作为节流。 其原因是,时间,阻塞和睡眠充其量是不健全的,最坏的情况是不可靠的,而且很可能会给您的动画添加令人不安的人为因素。
你通常想要做的就是尽可能快地把属于一个框架的所有东西都推送到图形API(很可能是OpenGL,因为你说的是​​“任何平台”),并指示一个工作线程开始执行游戏逻辑和物理等等。然后阻塞SwapBuffers。

所有的定时器,超时和睡眠都受到调度程序的分辨率的限制,在Windows下是15ms(使用timeBeginPeriod可以设置为1ms)。 在60fps时,一个帧是16.666ms,所以阻塞15ms是灾难性的,但是即使是1ms也是相当长的时间。 没有什么可以做到的,以获得更好的解决方案(这在Linux下是相当好的)。

Sleep或任何具有超时功能的阻塞功能可以保证你的进程睡眠的时间至少和你所要求的一样长(事实上,在Posix系统上,如果发生中断,睡眠可能会更少 )。 它不能保证你的线程一运行,或者有其他的保证。
Windows下的睡眠(0)更糟糕。 该文件说: “线程将放弃其时间片的剩余部分,但仍然准备就绪,请注意,准备好的线程不保证立即运行” 。 现实情况是,大多数情况下它的工作方式都是可行的,阻止任何地方从“完全不”到5-10ms,但有时候我也看到了睡眠(0)阻止了100ms,这种情况在发生时是一种灾难。

使用QueryPerformanceCounter是在寻求麻烦 – 只是不这样做 。 在一些系统上(最近有一个CPU的Windows 7),它可以正常工作,因为它使用了可靠的HPET,但是在许多其他系统上,你会看到各种奇怪的“时间旅行”或“反向时间”调试。 这是因为在这些系统上,QPC的结果是读取取决于CPU频率的TSC计数器。 CPU频率不是恒定的,并且多核/多处理器系统上的TSC值不需要一致。

然后是同步问题。 两个单独的计时器,即使以相同的频率滴答,也必然会变得不同步(因为没有“相同”的东西)。 你的显卡中有一个定时器已经在进行垂直同步了。 使用这个同步将意味着一切将工作,使用不同的意味着事情将最终不同步。 不同步可能根本没有任何作用,可能会画出一半的完成帧,或者可能会阻塞一个完整的帧,或者其他什么。
虽然错过一个框架通常不是什么大不了的事情(如果只是一个而且很少发生),在这种情况下,首先你可以完全避免这种情况。

取决于你想优化的表现。 如果你想为你的游戏提供最快的帧率,那么使用你发布的循环。 如果你的游戏是全屏的,这可能是正确的。

如果你想限制帧速率,并允许桌面和其他应用程序获得一些cyles,那么一个计时器方法就足够了。 在Windows中,您只需使用隐藏窗口,SetTimer调用,并将PeekMessage更改为GetMessage。 只要记住,WM_TIMER间隔不准确。 当你需要计算自上一帧以来已经过去了多少实时时间时,请调用GetTickCount,而不是假设你在时间间隔内完全醒来。

适用于游戏和桌面应用程序的混合方法是使用上面所示的循环,但在OnIdle函数返回后插入Sleep(0)。

  1. MSGWaitForMultipleObjects应该在Windows游戏消息循环中使用,因为您可以根据可定义的超时自动停止等待消息。 这可以大大节省您的OnIdle调用,同时确保窗口快速响应消息。

  2. 如果你的窗口不是全屏,那么SetTimer调用一个定时器proc,或者把WM_TIMER消息发送到你游戏的WindowProc是必需的 。 除非你不介意每当用户停止动画,例如,点击你窗口的标题栏。 或者你弹出某种模式的对话框。

  3. 在Windows上,无法访问实际的定时器中断。 这些东西被隐藏在许多硬件抽象层之下。 定时器中断由驱动程序处理以发送内核对象的信号,用户模式代码可以在对WaitForMultipleObjects的调用中使用。 这意味着当你调用SetTimer()时,内核会唤醒你的线程去处理与你的线程相关的内核定时器,此时,如果消息队列是空的,GetMessage / PeekMessage将合成一个WM_TIMER消息。

不要在游戏中使用GetTickCount或WM_TIMER进行计时,他们有可怕的解决方案。 而是使用QueryPerformanceCounter。