程序响应性
程序响应性
译者:绯红
KING
来源:
http://www.gamasutra.com/view/feature/1942/programming_responsiveness.php
【如果你不能在游戏中控制你的行动,是否应该责怪这个游戏呢?在这篇技术文章中, Neversoft 的合伙人 Mick West 会阐述游戏中造成响应延迟( Lag )的问题,并提出一些必要的解决方案。】
【如果你不能在游戏中控制你的行动,是否应该责怪这个游戏呢?在这篇技术文章中, Neversoft 的合伙人 Mick West 会阐述游戏中造成响应延迟( Lag )的问题,并提出一些必要的解决方案。】
响应性是一种可以在第一时间成就或毁坏一个游戏的东西。在一些游戏的杂志评论中,可以明显体会到响应性差的游戏会被形容为“迟钝的”,“没反应的”,“浮躁的”或“懒散的”。而好的游戏会被评价为“紧凑的”或“有响应的”。
响应性可以从几个方面来理解,本文主要是从一个程序员的角度来阐述,提供一些可以提高游戏响应性的方法。
响应延迟
响应延迟是指从玩家触发一个事件到玩家收到事件发生的反馈(通常是视觉上的)之间的时间迟滞。如果时间迟滞过长,游戏会显得没有响应。有几个因素决定了响应延迟时间长度。
如果你的游戏是没有响应的,这很可能是
4
或
5
个不同因素造成的累积效应。只是单独调整其中一个因素,是不能起到明显作用的。但是找出所有的因素,并调整它们,会带来显而易见的改进。
玩家,有时甚至是游戏设计师,都不能把他们在游戏中操作觉得不适的地方用语言表达出来。通常他们会试着做一些需要同步操作的事情,但是失败了,他们不会告诉你“这个事件在我输入后的
0.10
秒才发生作用”,而是会说这个游戏有些“迟缓”,“不紧凑”或者“难度高”。
或者他们根本不告诉你一些重点,只是单纯说这个游戏很烂,但不知道为什么很烂。
设计师和程序员需要时刻注意响应延迟以及它对游戏的负面作用,甚至在测试玩家没有直接汇报这一点时。
为什么发生延迟
要了解延迟发生的原因,你需要理解事件的发生序列:从用户按下一个按钮,到结果显示在屏幕上。为了理解这一点,我们需要看一下游戏的主循环结构。主循环进行了两个基本任务:逻辑和渲染。
主循环的逻辑部分更新了游戏的状态(游戏对象和环境的内部实现),渲染部分则创建了需要显示在电视上的一帧画面。
在主循环的一些阶段,通常是开始时,我们会接受到用户的输入,作为主循环的第三项任务,但也通常把它作为逻辑任务的一部分。在这里我把输入单独提出来,是为了看清楚事件发生的顺序。
有几种方式来描述主循环结构。最简单的一种如表
1
所示,交替调用逻辑和渲染的代码。我们假设游戏运行在固定帧率,通常是
60fps
或
30fps
的
NTSC
制式家用机游戏,而一些帧的同步发生在调用
Rendering()
中。
表
1
:简单的主循环
while (1) {
Input();
Logic();
Rendering();
}
Input();
Logic();
Rendering();
}
在这里主循环只展现了故事的一半。对
Rendering()
的调用是在
CPU
端处理渲染任务,这些任务包括场景、物体、剔除、动画、排序、设置变换、构造一个供
GPU
处理的显示列表。这是一个迭代的过程。
实际上
GPU
渲染是在
CPU
渲染之后进行的,通常是异步的。所以当主循环开始进行到下一帧时,
GPU
仍然在渲染前一帧。
那么延迟在什么时候发生?要理解造成延迟的原因,你需要理解从用户按键输入到接受反馈之间的事件序列。
在最顶层,用户按下一个按键;游戏逻辑读取这个按键输入,更新游戏状态;
CPU
渲染函数准备好渲染这个新状态的一帧,然后
GPU
将其渲染;最后这个新的一帧被显示在屏幕上。
图
1
:当玩家按下一个键,游戏要占用
3
帧(最理想情况)来创建一个视觉反馈,程序问题会引入多余的帧造成延迟。实际延迟的时间是每一帧时间长度的乘积。
图
1
显示了图形化的序列内容。在第一帧的某一时刻,玩家按下一个键来开枪。输入过程完成后,这个输入会被读取到第二帧。第二帧更新了基于按键输入的逻辑状态(开火)。
仍然在第二帧,渲染的
CPU
端开始执行这个新的逻辑状态。然后在第三帧,
GPU
执行了这个新逻辑状态的实际渲染。最后在第四帧的开始,新渲染出来的一帧画面从帧缓冲中呈现给玩家。
那么这个延迟有多长时间?这要看一帧有多长时间(这里“一帧”是指主循环的一个完整迭代过程)。从用户输入到转换成视觉反馈,需要占用
3
帧。
如果我们的游戏在
30fps
下运行,延迟就是
3/30
,即十分之一秒。如果游戏在
60fps
下运行,那这个延迟会是
3/60
,即二十分之一秒。
这个计算说明了一个关于
60fps
和
30fps
之间的差异的判断错误。因为这两个帧率之间的差异是
1/60
秒,人们推断出响应性之间的差异也是
1/60
。
但实际上,从
60
到
30
并没有给延迟增加一个垂直同步,它的效果是乘积形式的,加倍了延迟响应的过程管线。在我们图
1
中的理想示例中,增加了
3/60
秒,并不是
1/60
。如果事件管线再长一些,这个结果会变得更多,这是极有可能发生的情况。
实际上图
1
展示的是事件序列的最佳状态。按键输入通过最短路径转换为视觉反馈。我们可以在事件序列中清楚的看到这一点。
作为一个程序员,熟悉这些事件发生的顺序,是理解游戏中事物运作原理的重要环节。如果对事件发生的顺序不加注意的话,很容易造成多余帧的延迟(意味着一个
1/60
或
1/30
秒的延迟)。
举一个简单的例子,如果我们把主循环中的
Logic()
和
Rendering()
交换一下,想想会发生什么。来看一下图
1
的第二帧:这里
GPU
逻辑(渲染)发生在
CPU
逻辑之后,所以在第二帧开始时输入会影响
CPU
逻辑,之后是同一帧里的
GPU
逻辑。
然而如果
GPU
逻辑在前面进行,那么输入就得到下一帧才能影响到
GPU
逻辑,因此造成了一个多余帧的延迟。虽然这只是一个初学者犯得错误,但程序员仍要牢记不要让它发生。
造成延迟的多余帧可能在一些微小的行为中产生,但结果会影响游戏逻辑的整个序列。在我们的例子中,是开枪的过程。
假如现在我们的引擎用一个物理引擎来设置场景中物体的位置信息,并且需要处理的事件在更新中增加了(例如碰撞事件)。这种情况下,输入的序列或逻辑如表
2
所示。
表
2
:更新物理之后是事件处理
void Logic() {
HandleInput();
UpdatePhysics();
HandleEvents();
}
HandleInput();
UpdatePhysics();
HandleEvents();
}
基于消息的事件处理(
Event Handling
)是用于低耦合系统的一个不错的手段,程序员也通常会使用它来控制事件。要使枪开火,
HandleInput()
函数会发出一个事件,告诉枪去开火。
HandleEvents()
函数会处理这个事件,并使枪实际开火。但是在这一帧里,物理更新已经发生了,这个效果直到下一帧才能同步,这样就产生了一个多余帧的延迟。
更多延迟的原因
低层的行为顺序会造成更多的延迟。例如,考虑一个跳跃。反馈内容是角色实际的移动。要使游戏中的物体移动,你可以直接设置速度,或者给物体加一个作用力,例如加速度,或者瞬间的推力。
这个情景会产生一个问题,就是如果你的物理引擎在速度改变之前更新位置信息——这是很多游戏编程入门教程中的常见情况。
尽管在处理输入事件后,物体跳跃的速度变化已经在同一帧里更新了,但是物体直到下一帧才能开始改变位置,这样就产生了一个多余帧的延迟。
要记住,问题都是累积起来的,并且很难单独去辨识,这些组合的效果会使你的游戏操作变得很艰难。
假设你同时犯了以上三个错误:你在逻辑之前进行渲染,你在物理状态进行之后处理逻辑事件,并且你在速度变化之前更新位置信息。这是从玩家输入到接受反馈的三个主循环的完整迭代,在这三帧之上,你至少创建了
6
帧的延迟。
在
60fps
的游戏中,这会是
1/10
秒,已经足够糟糕了。但如果你的游戏是在
30fps
下,这个延迟会加倍,变为无法忍受的
1/5
秒,即
200
毫秒。
综合以上的问题,一些其他因素也可以导致延迟。动作可以由动画来驱动,由动画特定时间点的速度变化来实现。例如,如果一个动画师为了让动画与视觉更匹配,在动画中设置了一个不到一秒的跳跃速度变化,这可能看起来挺好,但实际上感觉却很糟。
动画师可以修正这个问题,只要确保速度变化是在动画的第一帧发生,玩家可以迅速得到反馈。但又有一个问题产生了,就是如何触发一个动画,并转换为实际的动作?
看起来动画的更新是由
Render()
函数来处理的。但任何以动画形式触发的事件,都需要在增加一帧后的循环中才能处理。
此外,触发一个动画可能在下一帧才会发生作用,又造成一帧的延迟。我们的延迟会从
6
帧增加到
8
帧。即使在每秒
60
帧的情况下,这也就几乎不能玩了。
这些也并不是全部。还有很多方式使多余的延迟帧潜入一个游戏。
你也许会把你的物理部分单独辟出一个线程(或者一个物理处理单元)。
如果你使用三倍缓冲来使你的帧率更平滑呢?
你也许用抽象事件通过系统的并行过程分解成真实事件。你也许在用一种脚本语言让等待事件时增加额外的一帧。
你可以通过组合各种关于时间和事件的概念,使你的游戏逻辑更灵活,这是非常必要的。但是在实现它们的时候,程序员可能会忽视发生在表面之下的事情,使造成延迟的多余帧悄悄潜入。
响应性,不是反应时间
有一个关于响应性的极大的误解,和人类的反应时间相关。人类不能通过视觉刺激做出身体上的反应,并且在
0.1
秒内移动他们的手指。
游戏玩家的顶峰反应时间分布在
0.15
秒至
0.30
秒,这取决于他有多“高桥名人”(形容按键反应迅速)。像这些可以计量的信息,通常在讨论游戏响应性时会提到。但它们之间是没有联系的。
不是玩家对游戏的反应有多块,而是游戏对玩家的反应有多快。问题并不在于反应时间,而是在于同步性。
用吉他英雄来举例,这个游戏中会有一些符号出现,玩家需要在恰当的时间按下相应的按键(当目标物体在特定区域范围时)。你是在预判断事件,这里不涉及任何反应时间。
缺乏响应性的问题一般是游戏没有及时反馈玩家的操作,并且事件发生时目标物体已经超出目标范围了。
如果你在正确时间按下键,你绝不希望物体会在爆炸前再多移动几个像素。但是物体通常在每帧会移动几个像素,有几帧的延迟使物体滑过目标。
有许多基于预测和按键输入的游戏。在一个滑板游戏中,你想要在到达轨道终点之前跳跃。在一个第一人称射击游戏中,你朝一个前方快速跑动的敌人开枪。
再提一次,响应性并不是反应时间。你通常会在射击的半秒前看见你的目标,或许更长时间,你会移动你的枪,或者等待目标跑到你的准星上。
对响应性问题是不能靠直觉来解决的,程序员全面了解问题本身才是非常重要的。最重要的一点,是要清楚按键触发行为任务到创建视觉反馈这个过程中逻辑和渲染的逐帧过程。一旦你理解了这个过程,你就能够优化它,使它工作在最佳状态。