游戏循环中更新独立渲染的目的是什么?


74

游戏循环中有许多文章,书籍和讨论。但是,我经常碰到这样的事情:

while(running)
{
    processInput();
    while(isTimeForUpdate)
    {
        update();
    }
    render();
}

这种方法基本上困扰我的是“独立于更新”的渲染,例如,在完全没有变化的情况下渲染帧。所以我的问题是,为什么经常教这种方法?


6
就个人而言,我发现gameprogrammingpatterns.com/game-loop.html是一个有用的解释
Niels

12
并非所有渲染更改都反映在游戏状态中。我怀疑您误解了这段代码的要点-它允许您每次渲染多次更新,而不是每次更新多次渲染。
a安

13
请注意,代码读取的while (isTimeForUpdate)不是if (isTimeForUpdate)。主要目标不是render()在没有时update(),而是update()render()s 之间重复。无论如何,两种情况都有有效的用途。如果状态可以在您的update函数外部更改,例如,根据隐式状态(例如当前时间)更改呈现的内容,则前者将是有效的。后者是有效的,因为它使您的物理引擎可以进行许多小的,精确的更新,例如,减少了因障碍而“翘曲”的机会。
蒂埃里

一个更合乎逻辑的问题是“ 依赖更新的渲染游戏循环的意义是什么”
user1306322

1
您多久没有执行一次更新?甚至不更新背景动画或屏幕时钟?
pjc50 '16

Answers:


113

关于我们如何达成这项共同的公约已有很长的历史,在此过程中还面临着许多令人着迷的挑战,因此,我将分阶段尝试激发它:

1.问题:设备以不同的速度运行

您是否曾经尝试过在现代PC上玩旧的DOS游戏,但它运行起来却速度很快,只是模糊而已?

许多旧游戏的更新循环非常幼稚-他们收集输入,更新游戏状态并以硬件允许的速度进行渲染,而无需考虑已花费了多少时间。这意味着一旦硬件发生变化,游戏玩法就会发生变化。

我们通常希望玩家使用的是去年的手机或最新型号,高端游戏台式机或台式机,只要在一系列设备上(只要符合最低要求),就可以获得一致的体验和游戏体验。中端笔记本电脑。

特别是对于具有竞争力的游戏(无论是多人游戏还是通过排行榜),我们都不希望在特定设备上运行的玩家比其他玩家具有优势,因为它们可以运行得更快或有更多的时间做出反应。

这里的surefire解决方案是锁定我们进行游戏状态更新的速率。这样我们可以保证结果将始终相同。

2.那么,为什么不只是锁定帧速率(例如使用VSync),而仍以锁步的方式运行游戏状态更新和渲染?

这可以奏效,但并不总是对听众可口。长时间以30 fps的速度稳定运行被认为是游戏的黄金标准。现在,玩家通常期望60 fps作为最低限制,尤其是在多人动作游戏中,随着我们对期望值的改变,一些较早的游戏现在看起来也很不稳定。特别是还有一群PC播放器的声音播放器,他们完全反对对锁进行帧速率处理。他们为尖端的硬件付出了高昂的代价,并希望能够使用该计算能力来实现其能够提供的最平滑,最高保真度的渲染。

特别是在VR中,帧速率为王,标准不断提高。在最近VR兴起的早期,游戏通常以60 fps的速度运行。现在更标准的是90,而像PSVR这样的硬件开始支持120。这可能还会继续增加。因此,如果VR游戏将其帧速率限制在今天可以接受和接受的水平,则随着硬件和期望的进一步发展,它有可能被抛在后面。

(通常,当被告知“玩家无法以比XXX更快的速度感知任何东西”时请多加注意,因为它通常基于特定类型的“感知”,例如按顺序识别帧。感知运动连续性通常要远得多)敏感。)

这里的最后一个问题是,使用锁定帧率的游戏也需要保持保守-如果您在游戏中碰巧要更新和显示异常多的物体的那一刻,就不想错过帧了截止日期,并导致明显的结结或结结。因此,您要么需要将内容预算设置得足够低以留有余量,要么需要投资更复杂的动态质量调整功能,以避免将整个播放体验与最小规格硬件上最差的性能挂钩。

如果性能问题出现在开发的后期,当所有现有系统的构建和调试都假设采用了一个锁步渲染帧速率(现在您无法总是做到)时,这可能会特别成问题。解耦更新和渲染速率为处理性能变化提供了更大的灵活性。

3.在固定的时间步上更新是否不具有与(2)相同的问题?

我认为这是原始问题的实质:如果我们将更新解耦,有时渲染两个帧之间没有游戏状态更新,那么这与以较低帧速率进行锁步渲染不一样,因为它没有可见的变化屏幕?

实际上,游戏使用几种不同的方式将这些更新解耦以达到良好效果:

a)更新速率可以比渲染的帧速率更快

正如tyjkenn在另一个答案中指出的那样,尤其是物理学,通常以比渲染更高的频率步进,这有助于最小化积分误差并给出更准确的碰撞。因此,您可能需要5或10或50,而不是在渲染的帧之间进行0或1更新。

现在,以120 fps渲染的玩家每帧可以获得2次更新,而使用低规格硬件以30 fps渲染的玩家则每帧获得8次更新,并且它们的游戏均以相同的每秒钟实时游戏速度运行。更好的硬件使其看上去更流畅,但不会从根本上改变游戏的工作方式。

如果更新速率与帧速率不匹配,就有可能在两者之间获得“拍频”。例如。在大多数帧中,我们有足够的时间来更新4个游戏状态并有少量剩余,因此每隔一段时间,我们就有足够的存储空间来在一个帧中进行5个更新,从而在运动中产生一些跳跃或停顿。这可以通过...解决

b)在更新之间内插(或外推)游戏状态

在这里,我们经常让游戏状态在未来保持一个固定的时间步长,并存储来自两个最新状态的足够信息,以便我们可以在它们之间绘制任意点。然后,当我们准备在屏幕上显示一个新的帧时,我们会融合到适当的时刻,仅用于显示目的(即,我们在这里不修改基本的游戏状态)

只要做得正确,只要我们不会跌落太低,它就可以使运动感觉像黄油一样平滑,甚至有助于掩盖帧率的某些波动。

c)增加非游戏状态变化的流畅度

即使不插值游戏状态,我们仍然可以获得一定程度的流畅性。

纯粹的视觉变化(例如角色动画,粒子系统或VFX)以及用户界面元素(例如HUD)通常独立于游戏状态的固定时间步进行更新。这意味着,如果我们每帧多次对游戏状态进行滴答,我们就不必为每次滴答付出代价,而只是在最终渲染阶段才付出。取而代之的是,我们缩放这些效果的播放速度以匹配帧的长度,从而使它们在渲染帧速率允许的情况下流畅播放,而不会影响游戏速度或公平性,如(1)所述。

摄像机移动也可以做到这一点- 特别是在VR中,有时我们会多次显示同一帧,但是要重新投影以考虑到玩家之间的头部移动,即使我们可以改善延迟和舒适度不会以如此快的速度自然渲染所有内容。一些游戏流媒体系统(游戏在服务器上运行,而播放器仅运行瘦客户端)也使用了该版本。

4.为什么不对所有内容都使用(c)样式?如果它适用于动画和UI,我们是否不能简单地缩放游戏状态更新以匹配当前帧速率?

是*这是可能的,但是不简单。

这个答案已经有点长了,所以我将不做所有详细的细节,只是一个简短的摘要:

  • 通过乘以deltaTime作品来适应可变长度更新以进行 线性更改(例如,以恒定速度移动,计时器的倒数或沿动画时间轴的进度)

  • 不幸的是,游戏的许多方面都是非线性的。即使是像重力这样简单的事情,也需要更复杂的集成技术或更高分辨率的子步骤,以避免在变化的帧频下产生不同的结果。播放器的输入和控制本身就是非线性的巨大来源。

  • 特别是,离散冲突检测和解决方案的结果取决于更新速率,如果帧太长,则会导致隧穿和抖动错误。因此,可变的帧速率迫使我们对更多内容使用更复杂/更昂贵的连续碰撞检测方法,或者容忍我们物理学中的可变性。当物体沿弧线移动时,即使是连续的碰撞检测也面临挑战,需要更短的时间步...

因此,在一般情况下,对于中等复杂度的游戏,完全通过deltaTime缩放来保持一致的行为和公平性是介于非常困难和维护密集到完全不可行之间的。

标准化更新速率使我们可以保证在一定范围内(通常使用更简单的代码)更多情况下的行为一致

保持更新速度与渲染脱钩,可以让我们灵活地控制体验的流畅性和性能,而无需更改游戏逻辑

即使那样,我们也永远无法获得真正的“完美”帧速率独立性,但是就像游戏中的许多方法一样,它为我们提供了一种可控制的方法,可以根据给定游戏的需求向“足够好”拨号。因此,通常将其作为有用的起点。


2
如果一切都将使用相同的帧速率,则同步所有内容可以最大程度地减少采样控制器到游戏状态做出反应之间的延迟。对于某些较旧机器上的游戏,最坏的情况是在17ms以下(在垂直空白开始时读取控件,然后应用游戏状态更改,并在光束沿屏幕向下移动时呈现下一帧) 。去耦事物通常会导致最坏情况下的时间显着增加。
超级猫

3
的确,更复杂的更新管道可以更轻松地无意引入延迟,但如果正确实现,则这并不是分离方法的必要结果。实际上,我们甚至可以减少延迟。让我们以60 fps渲染的游戏为例。紧跟着一步read-update-render,我们最坏情况下的延迟是17ms(暂时不考虑图形管道和显示延迟)。在具有(read-update)x(n>1)-render相同帧率的去耦环路下,我们最坏情况下的延迟只能相同或更好,因为我们经常检查或作用于输入,或对其进行更多操作。:)
DMGregory

3
有趣的是,在不考虑实时性的旧游戏中,最初的《太空侵略者》街机有渲染能力引起的故障,当玩家摧毁敌方船只时,渲染会加速游戏更新,从而导致意外更新,在游戏的标志性难度曲线中。
Oskuro '16

1
@DMGregory:如果事情是异步完成的,则有可能在游戏循环轮询控件之后,当前事件之后的游戏事件周期在渲染循环获取游戏状态后立即完成,控件更改就发生。在视频输出系统抓住当前帧缓冲区之后,当前循环之后的渲染循环结束,因此最坏情况下的时间最终仅差两个游戏循环时间加上两个渲染时间加上两个帧周期。适当地同步事物可以将其减少一半。
超级猫

1
@Oskuro:那不是小故障。无论屏幕上有多少个入侵者,更新循环的速度都保持恒定,但是游戏不会在每个更新循环上吸引所有入侵者。
超级猫

8

其他答案很好,并讨论游戏循环为何存在并且应与渲染循环分开的原因。但是,关于“为什么没有任何变化时为什么渲染框架?”的具体示例?实际上,这仅取决于硬件和复杂性。

视频卡是状态机,它们非常擅长于一遍又一遍地执行相同的操作。如果仅渲染已更改的内容,则实际上更昂贵,而不是更少。在大多数情况下,没有什么是静态的,如果在FPS游戏中稍微向左移动,您已经更改了屏幕上98%的内容的像素数据,则可能会渲染整个帧。

但主要是复杂性。跟踪更新时更改的所有内容的成本要高得多,因为您必须重新处理所有内容或跟踪某个算法的旧结果,将其与新结果进行比较,并且仅在更改不同时才渲染该像素。这取决于系统。

硬件等的设计在很大程度上针对当前约定进行了优化,并且状态机一直是一个很好的模型。


5
这里只有在发生变化时才渲染(所有内容)(问题所要问的)与仅渲染发生变化的部分(答案所描述的)之间存在区别。
DMGregory

是的,我试图在第一段和第二段之间进行区分。框架完全不变几乎是很少见的,因此我认为将这种观点与您的综合答案一起考虑很重要。
Waddles

除此之外,我还要注意没有理由渲染。您知道您总是有时间在每个帧上进行渲染调用(最好知道您总是有时间在每个帧上进行渲染调用!)因此进行“不必要的”渲染几乎没有害处-尤其是因为这种情况会实际上从不提出。
史蒂文·斯塔德尼基

@StevenStadnicki的确,在每一帧渲染所有内容并不会花费很多时间(因为无论何时一次状态发生许多变化,您都需要有预算的时间来执行此操作)。但是对于便携式计算机,笔记本电脑,平板电脑,电话或便携式游戏系统之类的便携式设备,应考虑避免多余的渲染以有效利用播放器的电池。这主要适用于用户界面繁重的游戏,其中屏幕的大部分内容可能在玩家动作之间保持不变,并且视游戏的架构而定,并非总是可行。
DMGregory

6

渲染通常是游戏循环中最慢的过程。人们不容易注意到帧速率高于60的差异,因此浪费时间进行渲染通常不那么重要。但是,还有其他过程将从更快的速度中受益更多。物理是一。一个循环中的太大变化会导致物体从墙壁上突然出现故障。也许可以通过较大的增量来解决简单的碰撞错误,但是对于许多复杂的物理相互作用,您将不会获得相同的精度。但是,如果物理循环运行得更频繁,则出现毛刺的可能性就会减少,因为可以以较小的增量移动对象,而不必每次都进行渲染。更多的资源用于灵敏的物理引擎,而在绘制用户看不见的更多帧上浪费了更少的资源。

这在图形密集型游戏中尤其重要。如果每个游戏循环都有一个渲染,而玩家没有最强大的游戏机,则游戏中可能会有一些点帧速下降到30或40。尽管这仍然不是一个完全可怕的帧速率,但是如果我们尝试将每个物理变化保持在较小范围内以避免毛刺,则游戏开始会变得相当缓慢。玩家会生气,他的角色只走正常速度的一半。但是,如果渲染速率与循环的其余部分无关,则尽管帧速率下降,玩家仍可以保持固定的步行速度。


1
或者,例如,测量刻度之间的时间并计算该时间中角色应该走多远?固定精灵动画的时代已经过去了!
格雷厄姆

2
没有时间混叠,人类无法以60fps以上的速度感知事物,但是在没有运动模糊的情况下实现平滑运动所需的帧速率可能要高得多。超过一定速度时,旋转的车轮应显示为模糊,但如果软件未应用运动模糊并且车轮每帧旋转超过半个辐条,即使帧速为1000fps,车轮也可能看起来很糟糕。
超级猫

1
@Graham,然后您在我的第一段中遇到了问题,其中物理等问题的行为与每次刻度的较大变化有关。如果帧速率下降到足够低的水平,则补偿较大的变化可能会导致玩家直接穿过墙壁,完全失去其击中框。
tyjkenn '16

1
人类通常无法以高于60 fps的速度感知,因此浪费资源进行渲染的速度没有意义。我对该声明表示怀疑。VR HMD的渲染频率为90Hz,并且还在不断上升。相信我,当我告诉您您确实可以感觉到耳机上的90Hz和60Hz之间的差异时。另外,最近我看到CPU限制和GPU限制一样多的游戏。说“渲染通常是最慢的过程”不一定是正确的。
3Dave

@DavidLively,所以“毫无意义”可能太夸张了。我的意思是渲染往往会成为瓶颈,大多数游戏在60 fps时看起来都不错。当然,在某些类型的游戏(例如VR)中,有些效果很重要,您只能以更快的帧速率才能获得效果,但这些效果似乎是例外,而不是标准。如果我是在较慢的机器上运行游戏,那么我宁愿拥有能正常工作的物理能力,也不愿拥有几乎没有注意到的快速帧速率。
tyjkenn '16

4

如果渲染子系统具有“自上次渲染以来已经过的时间”的概念,则与您问题中的结构类似的结构可能很有意义。

例如,考虑一种方法,在该方法中,通过固定(x,y,z)坐标表示游戏世界中对象的位置,以及另外存储当前运动矢量的方法(dx,dy,dz)。现在,您可以编写游戏循环,以便必须在方法中发生位置变化update,但是您也可以设计它,以便在期间发生运动变化update。使用后一种方法,即使您的游戏状态实际上直到下update一个都不会改变,render-以较高频率调用的功能可能已经在稍微更新的位置绘制了对象。虽然从技术上讲,这会导致您看到的内容与内部表示的内容之间存在差异,但是差异很小,对于大多数实际方面来说都无关紧要,但可以使动画看起来更加流畅。

当您考虑网络输入的延迟时,预测游戏状态的“未来”(尽管有犯错的危险)可能是个好主意。


4

除了其他答案...

检查状态变化需要大量处理。与实际执行处理相比,如果需要花费类似(或更多!)的处理时间来检查更改,则实际上情况没有得到改善。如@Waddles所说,在渲染图像的情况下,视频卡真的很善于一遍又一遍地做同样的笨拙的事情,并且检查每个数据块的更改要比直接在各个数据块之间传输要昂贵得多。到视频卡进行处理。此外,如果渲染游戏那么它的屏幕真的不可能没有在最后一跳已经改变。

您还假设渲染需要大量的处理器时间。这在很大程度上取决于您的处理器和图形卡。多年来,重点一直放在逐步将更复杂的渲染工作转移到图形卡上,并减少处理器所需的渲染输入。理想情况下,处理器的render()调用应该简单地设置DMA传输,仅此而已。然后将获取数据到图形卡的任务委托给内存控制器,并将产生图像的数据委托给图形卡。他们可以在自己的时间内完成此任务,而处理器可以并行执行进行物理,游戏引擎以及处理器性能更好的所有其他工作。显然,实际情况要比这复杂得多,但是能够将工作分流到系统的其他部分也是一个重要因素。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.