用操作系统计时器处理游戏循环



一个简单的游戏循环是这样运行的:

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%的一个核心),因为它总是检查running变量的状态。

我的问题是,如果使用操作系统的计时器功能(游戏邦注:以Windows为例)来执行游戏循环,并根据每秒所需的游戏逻辑滴答数来设置所需的间隔,是否会更好?我问这个问题是因为我假设定时器函数使用CPU的RTC中断。

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

然而,在连续动画游戏中,你通常不希望在渲染线程中阻塞或休眠,如果你可以帮助它,相反,你希望以最大速度渲染并依赖垂直同步作为节流阀。这样做的原因是,计时、阻塞和睡眠在最好的情况下是不精确的,在最坏的情况下是不可靠的,而且它很可能会在你的动画中添加令人不安的伪影。
你通常想要做的是尽可能快地将属于一帧的所有内容推送到图形API(很可能是OpenGL,因为你说"任何平台"),通知工作线程开始执行游戏逻辑和物理等,然后阻塞SwapBuffers。

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

Sleep或任何具有超时的阻塞函数,保证您的进程至少在上休眠,只要您要求(实际上,在Posix系统上,如果发生中断,它可能会休眠更少)。它不给你一个保证,你的线程会在时间一到就运行,或者任何其他保证。
Windows下的Sleep(0)更糟糕。文档中说"线程将放弃剩余的时间片,但仍然准备就绪。注意,一个就绪的线程不能保证立即运行。现实情况是,它在大多数情况下工作得很好,从"根本没有"到5-10毫秒,但有时我也看到Sleep(0)阻塞100毫秒,当它发生时,这是一场灾难。

使用QueryPerformanceCounter是自找麻烦——千万不要这样做。在一些系统上(Windows 7与最新的CPU),它会工作得很好,因为它使用可靠的HPET,但在许多其他系统上,你会看到各种奇怪的"时间旅行"或"反向时间"效果,没有人能解释或调试。这是因为在这些系统上,QPC的结果读取TSC计数器,这取决于CPU的频率。CPU频率不是恒定的,多核/多处理器系统上的TSC值不必一致。

然后是同步问题。两个独立的计时器,即使以相同的频率滴答,也必然会变得不同步(因为没有"相同"这样的东西)。你的显卡中已经有一个计时器在做垂直同步了。使用这个进行同步意味着一切都可以正常工作,而使用另一个则意味着事情最终会不同步。不同步可能根本没有影响,可能会绘制完成一半的帧,或者可能会阻塞一整帧,等等。
虽然丢失一个帧通常不是什么大问题(如果只有一个并且很少发生),但在这种情况下,你可以首先完全避免它。

取决于你想优化谁的性能。如果你想让你的游戏获得最快的帧率,那就使用你发布的循环。如果你的游戏是全屏的,这可能是正确的选择。

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

对于游戏和桌面应用来说,一个很好的混合方法是使用你上面发布的循环,但是在你的OnIdle函数返回后插入一个Sleep(0)。

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

  2. 如果你的窗口不是全屏,那么SetTimer调用定时器进程或发布WM_TIMER消息到你的游戏的WindowProc是需要。除非你不介意动画在用户点击你的窗口标题栏时停止。或者弹出一个模态对话框

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

永远不要在游戏中使用GetTickCount或WM_TIMER来计时,它们的分辨率很差。相反,使用QueryPerformanceCounter

相关内容

  • 没有找到相关文章

最新更新