1

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

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 中断。

4

4 回答 4

3

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

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

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

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

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

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

于 2011-04-06T18:45:19.013 回答
1

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

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

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

于 2011-04-06T06:44:24.067 回答
1
  1. MSGWaitForMultipleObjects应该在 Windows 游戏消息循环中使用,因为您可以让它根据可定义的超时自动停止等待消息。这可以大大限制您的 OnIdle 调用,同时确保窗口快速响应消息。

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

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

于 2011-04-06T12:51:07.420 回答
0

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

于 2011-04-06T06:53:53.127 回答