【问题标题】:Handling the game loop with OS timers使用操作系统计时器处理游戏循环
【发布时间】:2011-04-05 23:54:02
【问题描述】:

一个简单的游戏循环运行有点像这样:

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();
        }
}

(取自this question

为了举例,我在这里以 windows 为例,它可以是任何平台。如果我弄错了,请纠正我,但这样的循环将使用 100% 的 cpu/核心(使用我计算机上一个核心的 50%),因为它总是检查 running 变量的状态。

我的问题是,使用操作系统的(在示例 Windows 中)计时器函数来实现游戏循环会更好(在性能方面)吗,根据所需的游戏逻辑滴答数设置所需的间隔第二?我问这个是因为我假设定时器函数使用 CPU 的 RTC 中断。

【问题讨论】:

    标签: windows timer while-loop game-loop


    【解决方案1】:

    通常情况下,即使用户不执行任何操作,游戏也会一直绘制新帧。这通常会发生在您调用 onIdle() 的地方。如果您的游戏仅在用户按下按钮等时或偶尔更新窗口/屏幕,那么MSGWaitForMultipleObjects 是一个不错的选择。

    但是,在连续动画游戏中,如果可以提供帮助,您通常根本不想在渲染线程中阻塞或休眠,而是希望以最大速度渲染并依赖在垂直同步上作为油门。这样做的原因是,时间、阻塞和睡眠充其量是不精确的,最坏的情况是不可靠的,并且很可能会给您的动画添加令人不安的伪影。
    您通常想要做的是将属于帧的所有内容尽可能快地推送到图形 API(很可能是 OpenGL,因为您说“任何平台”),向工作线程发出信号以开始执行游戏逻辑和物理等,并且然后阻塞 SwapBuffers。

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

    Sleep,或任何有超时的阻塞函数,保证你的进程睡眠至少只要你要求(事实上,在Posix系统上,它可能睡眠less 如果发生中断)。它不保证您的线程将在时间一到时立即运行,也不保证任何其他保证。
    Windows 下的 Sleep(0) 更糟糕。文档说“线程将放弃其时间片的剩余部分但保持就绪状态。请注意,就绪线程不能保证立即运行”。现实情况是,它在大多数情况下都可以正常工作,从“根本没有”到 5-10 毫秒的任何地方都阻塞,但有时我也看到 Sleep(0) 阻塞 100 毫秒,当它发生时这是一场灾难.

    使用QueryPerformanceCounter 是自找麻烦——just don't do it。在某些系统上(具有最新 CPU 的 Windows 7)它可以正常工作,因为它使用可靠的 HPET,但在许多其他系统上,您会看到各种奇怪的“时间旅行”或“逆时针”效果,没有人可以解释或调试。那是因为在那些系统上,QPC 的结果读取取决于 CPU 频率的 TSC 计数器。 CPU 频率不是恒定的,多核/多处理器系统上的 TSC 值不需要一致。

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

    【讨论】:

    • 很有道理!另外,很高兴知道QueryPerformanceCounter(),我相信我一直在使用它(这里是win7),没有任何问题,但很高兴知道它不可靠。
    【解决方案2】:

    取决于您要优化谁的性能。如果您想要游戏的最快帧速率,请使用您发布的循环。如果您的游戏是全屏的,这可能是正确的。

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

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

    【讨论】:

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

      2. 如果您的窗口不是全屏的,那么SetTimer 调用一个计时器进程或将 WM_TIMER 消息发布到您游戏的 WindowProc 是必需的。除非您不介意每当用户单击窗口的标题栏时动画就会停止。或者你会弹出某种模式对话框。

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

      【讨论】:

        【解决方案4】:

        切勿在游戏中使用 GetTickCount 或 WM_TIMER 进行计时,它们的分辨率很差。相反,请使用 QueryPerformanceCounter。

        【讨论】:

        • 我会使用一个紧凑的游戏循环,就像他为此发布的那样。我不会碰我提到的分辨率低的计时方法。
        • 这适用于 Windows 95。除非您将 10 毫秒视为“可怕”。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-04-11
        • 1970-01-01
        相关资源
        最近更新 更多