游戏主循环(GameLoop)的详细解析
⼀.引⾔
游戏主循环是每个游戏的⼼跳,输送着整个游戏需要的养分。不幸的是没有任何⼀篇好的⽂章来指导⼀个菜鸟游戏程序员如何为⾃⼰的
程序供养。不过不⽤担⼼,因为你刚好不⼩⼼看到了这篇,也是唯⼀⼀篇给予这个话题⾜够重视的⽂章。
由于我⾝为游戏程序员,我见过许许多多的⼿机⼩游戏的代码。这些代码给我展⽰了五彩缤纷的游戏主循环实现⽅法。你可能要
问:“这么简单的⼀个⼩玩意还能做到千奇百怪?”事实就是这样,我就会在此⽂中讨论⼀些主流实现的优缺点,并且给你介绍在我看来
最好的输送养分的解决⽅案。
1.最基本的游戏主循环
每⼀个游戏都是由获得⽤户输⼊,更新游戏状态,处理AI,播放⾳乐和⾳效,还有画⾯显⽰这些⾏为组成。游戏主循环就是⽤来处理这
个⾏为序列。如我在引⾔中所说,游戏主循环是每⼀个游戏的⼼跳。在此⽂中我不会深⼊讲解上⾯提到的任何⼀个⾏为,⽽只详细介绍游戏
主循环。所以我把这些⾏为简化为了两个函数:
update_game();//更新游戏状态(后⽂可能翻译为逻辑帧)
display_game();//更新显⽰(显⽰帧)
下⾯是最简单的游戏主循环:
boolgame_is_running=true;
while(game_is_running){
update_game();
display_game();
}
这个简单循环的主要问题是它忽略了时间,游戏会尽情的飞奔。在⼩霸王机器上运⾏会使玩家有极强的挫败感,在⽜逼的机器上运⾏则
会要求玩家有超⼈的判断⼒和APM(原意为慢的机器上运⾏慢,快的机器上运⾏快)。在远古时代,硬件的速度已知的情况下,这不算什
么,但是⽬前有如此多的硬件平台使得我们不得不去处理时间这个重要因素。对于时间的处理有很多的⽅法,接下来我会⼀⼀奉上。
⾸先我会解释两个贯穿全⽂的术语:
每秒帧数(后简称FPS)
FPS是FramesPerSecond的缩写。在此⽂的上下⽂中它意味着display_game()每秒被调⽤的次数。
游戏速度
游戏速度是每秒更新游戏状态的速度,换⾔之,即update_game()每秒被调⽤的次数。
依赖于恒定的游戏速度
实现
⼀个让游戏每秒稳定运⾏在25帧的解决⽅案如下:
constintFRAMES_PER_SECOND=25;
constintSKIP_TICKS=1000/FRAMES_PER_SECOND;
DWORDnext_game_tick=GetTickCount();
//GetTickCount()returnsthecurrentnumberofmilliconds
//thathaveelapdsincethesystemwasstarted
intsleep_time=0;
boolgame_is_running=true;
while(game_is_running){
update_game();
display_game();
next_game_tick+=SKIP_TICKS;
sleep_time=next_game_tick-GetTickCount();
if(sleep_time>=0){
Sleep(sleep_time);
}el{
//Shit,wearerunningbehind!
}
}
注:
两个重要变量来控制恒定帧数:
next_game_tick:这⼀帧完成的时间点
sleep_time:若⼤于0,则⽬前时间没到完成这⼀帧的时间点。启⽤Sleep等待时间点的到达;若⼩于0,则该帧的⼯作没完成。
恒定帧数的好处:防⽌整个游戏因为跳帧⽽引起画⾯的撕裂。性能较低的硬件会显得更慢;⽽性能⾼的硬件则浪费了硬件资源。
这个⽅案有⼀个⾮常⼤的优点:简单!因为你知道update_game()每秒被调⽤25次,那么你的游戏的逻辑部分代码编写将⾮常直⽩。
⽐如说在这种主循环实现的游戏中实现⼀个重放函数将⾮常简单(译者注:因为每帧的间隔时间已知,只需要记录每⼀帧游戏的状态,回
放时按照恒定的速度播放即可。就像电影胶⽚⼀样)。如果在游戏中没有受到随机值的影响,只需要记录玩家的输⼊就可以实现重放。
在你实现这个循环的硬件上你可以按需要调整FRAMES_PER_SECOND到⼀个理想的值,但是这个游戏主循环实现会在各种硬件上表
现得怎么样呢?
⼩霸王机
如果硬件可以应付指定的FPS,那么不会有什么事情发⽣。但是⼩霸王通常是应付不了的,游戏就会卡。在极端情况下就会卡得掉渣,
或者⼀步⼗卡、⼀卡⼗步(原意为某些情况下游戏速度很慢,有⼀些情况下⼜⽐较正常)。这样的问题会毁掉你的游戏,使得玩家及其挫
败。
⽜逼的机器
在⽜逼的机器上似乎不会有任何问题,但是这样的游戏主循环浪费⼤量的时钟循环!⽜逼的机器运⾏这个游戏可以轻松的跑到300帧,
却每秒只运⾏了25或者30帧~那么这个主循环实现会让拥有⽜逼硬件的玩家⽆法尽情发挥其硬件效果产⽣极⼤的挫败感(原意为这样的实
现会让你的视觉效果受到影响,尤其是⾼速移动物体)。
从另外⼀个⾓度来说,在移动设备上,这⼀点可能会是⼀个优点。游戏持续的⾼速运⾏会很快地消耗电池。
结论
基于恒定游戏速度的FPS的主循环实现⽅案简单易学。但是存在⼀些问题,⽐如定义的FPS太⾼会使得⽼爷机不堪重负,定义的FPS太
低则会使得⾼端硬件损失太多视觉效果。
3.基于可变FPS的游戏速度
实现
另外⼀种游戏实现可以让游戏尽可能的飞奔,并且让依据FPS来决定游戏速度。游戏状态会根据每⼀显⽰帧消耗的时间来进⾏更新。
DWORDprev_frame_tick;
DWORDcurr_frame_tick=GetTickCount();
boolgame_is_running=true;
while(game_is_running){
prev_frame_tick=curr_frame_tick;
curr_frame_tick=GetTickCount();
update_game(curr_frame_tick-prev_frame_tick);
display_game();
}
注:
prev_frame_tick:上⼀帧完成的时间点
curr_frame_tick:⽬前的时间点
curr_frame_tick和prev_frame_tick的差即为⼀帧所需的时间,根据这⼀个变量,每⼀帧的时间是不⼀样的,FPS也是可变的
缓慢的硬件有时会导致某些点的某些延迟,游戏变得卡顿。
快速的硬件也会出现问题,帧数可变意味着在计算时不可避免地会有计算误差。
这种游戏循环⼀见钟情似乎很好,但不要被愚弄。慢速和快速硬件都可能导致游戏出现严重问题。此外,游戏更新功能的实现⽐使⽤固定帧速率时更难,为什么要使⽤
这个游戏主循环的代码⽐起之前稍微复杂⼀些,因为我们必须去考虑两次update_game()调⽤之间的时间差。不过,好在这并不算复
杂。
初窥这个实现的代码好像是⼀个理想的实现⽅案。我已经见过许多聪明的游戏程序员⽤这种⽅式来书写游戏主循环。但是我会给你展⽰
这个实现⽅案在⼩霸王和⽜逼的机器上的严重问题!是的,包括⾮常职业⾮常娴熟⾮常⽜逼的玩家的机器。
⼩霸王
⼩霸王会在某些运算复杂的地⽅出现卡的情况,尤其在3D游戏中的复杂场景更是如此。帧率的降低会影响游戏输⼊的响应,同时降低
玩家的反应速度。游戏状态更新也会因此突然受到影响。这样的情况会使得玩家和AI的反应速度减慢,造成玩家挫败感加剧。⽐如⼀个在正
常帧率下可以轻松越过的障碍会在低帧率下⽆法逾越。更严重的问题是在⼩霸王上会经常发⽣⼀些违反物理规律的怪事,如果这些运算涉及
到物理模拟的话。
⽜逼的机器
你可能会好奇,为什么刚才的游戏循环在飞快的机器上会出现问题。不幸的是,这个⽅案的确如此,⾸先,让我给你介绍⼀些计算机数
学知识。
浮点数类型占⽤内存⼤⼩是有限的,那么有⼀些数值就⽆法被呈现。⽐如0.1就不能⽤2进制表⽰,所以会被近似的存储在⼀个浮点数
中。我⽤python给你们展⽰⼀下。
>>>0.1=>0.10001
这个问题本⾝并不怎么具有戏剧性,但是这样的后果却截然相反。⽐⽅说你的赛车的速度是0.001个单元每微秒。那么正确的结果是在
10秒后你的赛车会移动10个单位,那么我们这样来实现⼀下:
defget_distance(fps):
skip_ticks=1000/fps
total_ticks=0
distance=0.0
speed_per_tick=0.001
whiletotal_ticks<10000:
distance+=speed_per_tick*skip_ticks
total_ticks+=skip_ticks
returndistance
现在我们来得到40帧每秒时运⾏10秒后的结果
>>>get_distance(40)得到10.075
等等~怎么不是10呢?发⽣了什么?嗯,400次加法后的误差就有这么⼤,每秒运⾏100次加法后⼜会是怎么⼀个样⼦呢?
>>>get_distance(100)得到9.9999999999998312
误差越来越⼤了!那么40帧每秒的结果和100帧每秒之间误差差距是多⼤呢?
>>>get_distance(40)-get_distance(100)得到2.4336e-13
你可能会想这样的误差可以忽略。但是真正的问题出现在你使⽤这些错误的值去进⾏更多的运算。⼩的误差会被扩⼤为致命的错误!然
后这些错误会在游戏飞奔的同时毁掉它!这些问题发⽣的⼏率绝对⼤到⾜够引起你的注意。我有见过因为这个原因在⾼帧率出现问题得游
戏。之后那个游戏程序员发现这些问题出现在游戏的核⼼部分,只有重写⼤部分代码才能修复它。
结论
这样的游戏主循环看上起不错,但是并不怎么样。不管运⾏它的硬件怎样,都可能出现严重的问题。另外,游戏实现的代码相对于固定
游戏速度的主循环⽽⾔更加复杂,那你还有什么使⽤它的理由呢?
4.最⼤FPS和恒定游戏速度
实现
我们的第⼀个实现中,FPS依赖于恒定的游戏速度,在低端的机器上会出现问题。游戏速度和游戏显⽰都会出现掉帧。⼀个可⾏的解决
⽅案是牺牲显⽰帧率的来保持恒定的游戏速度。下⾯就实现了这种⽅案:
constintTICKS_PER_SECOND=50;
constintSKIP_TICKS=1000/TICKS_PER_SECOND;
constintMAX_FRAMESKIP=10;
DWORDnext_game_tick=GetTickCount();
intloops;
boolgame_is_running=true;
while(game_is_running){
loops=0;
while(GetTickCount()>next_game_tick&&loops
update_game();
next_game_tick+=SKIP_TICKS;
loops++;
}
display_game();
}
注:
MAX_FRAMESKIP:帧数缩⼩的最低倍数
最⾼帧数为50帧,当帧数过低时,渲染帧数将降低(display_game()),最低可⾄5帧(最⾼帧数缩⼩10倍),更新帧数保持不变(update_game())
在慢速硬件上,每秒帧数会下降,但游戏本⾝有望以正常速度运⾏。
游戏在快速硬件上没有问题,但是像第⼀个解决⽅案⼀样,你浪费了很多宝贵的时钟周期,可以⽤于更⾼的帧速率。在快速更新速率和能够在慢速硬件上运⾏之间找到
缺点与第⼆种⽅案相似。
游戏会以稳定的50(逻辑)帧每秒的速度更新,渲染速度也尽可能的快。需要注意的是,如果渲染速度超过了50帧每秒的话,有⼀些
帧的画⾯将会是完全相同的。所以显⽰帧率实际上也等同于最快每秒50帧。在⼩霸王上运⾏的话,显⽰帧率会在更新游戏状态循环达到
MAX_FRAMESKIP时下降。从上⾯这个例⼦来说就是当渲染帧率下降到5(FRAMES_PER_SECOND/MAX_FRAMESKIP)以下时,
游戏速度会变慢。
⼩霸王
在⼩霸王上运⾏这样的游戏循环会出现掉帧,但是游戏速度不受到影响。如果硬件还是没有办法处理这样的循环,那么游戏速度和游戏
帧率都会受到影响。
⽜逼的机器
在⽜逼的机器上这个游戏循环不会出现问题,但是如同第⼀个解决⽅案⼀样,还是浪费了太多的时钟周期。找到⼀个快速更新并且依然
能够在⼩霸王上运⾏游戏的平衡点是⾄关重要的!
结论
使⽤上⾯的这个⽅案可以使游戏的实现代码⽐较简单。但是仍然有⼀些问题:如果定义了⼀个过⾼的FPS会让⼩霸王吃不消,如果过低
则会让⽜逼的机器难以发挥性能。
5.独⽴的可变显⽰帧率和恒定的游戏速度
实现
有没有可能对之前的那种⽅案进⾏优化使得它在各种平台上都有⾜够好的表现呢?当然是有的!游戏状态本⾝并不需要每秒更新60
次。玩家输⼊,AI信息等都不需要如此⾼的帧率来进⾏更新,⼤约每秒25次就⾜够了。所以,我们可以试着让update_game()每秒不多不
少的被调⽤25次。渲染则放任不管,让其飞奔。但是不能让⼩霸王的低渲染帧率影响到游戏状态更新的速度。下⾯就是这个⽅案的实现:
constintTICKS_PER_SECOND=25;
constintSKIP_TICKS=1000/TICKS_PER_SECOND;
constintMAX_FRAMESKIP=5;
DWORDnext_game_tick=GetTickCount();
intloops;
floatinterpolation;
boolgame_is_running=true;
while(game_is_running){
loops=0;
while(GetTickCount()>next_game_tick&&loops
update_game();
next_game_tick+=SKIP_TICKS;
loops++;
}
interpolation=float(GetTickCount()+SKIP_TICKS-next_game_tick)/float(SKIP_TICKS);
display_game(interpolation);
}
注:
渲染帧数是预测与插值实现的。interpolation计算等价的帧数
使⽤这种⽅案的update_game()实现会⽐较简单,相对⽽⾔,display_game()则会变得稍许复杂。你需要实现⼀个接收插值参数的预
⾔函数,这并不是什么难事,只是需要做⼀些额外的⼯作。我会接着解释这个预⾔函数是如何⼯作的,不过⾸先让我告诉你为什么需要这样
的⼀个函数。
游戏状态每秒被更新25次,如果你渲染的时候不使⽤插值计算,渲染帧率就会被限定在25帧。需要注意的是,25帧并没有⼈们想象中
的糟糕,电影画⾯在每秒24帧的情况下依然流畅。所以25帧可以很好的展⽰游戏画⾯,不过对于⾼速移动的物体,更⾼的帧率会带来更好
的效果。所以我们要做的是,在显⽰帧之间让⾼速移动的物体平滑过度。这就是我们需要⼀个插值和预⾔函数的原因。
插值和预⾔函数
如我之前所说,游戏状态更新在⼀个恒定的帧率下运⾏着,当你渲染画⾯的时刻,很有可能就在两个逻辑帧之间。假设你已经第10次
更新了你的游戏状态,现在你需要渲染你的场景。这次渲染就会出现在第10次和第11次逻辑帧之间。很有可能出现在第10.3帧的位置。那
么插值的值就是0.3。举个例⼦说,我的⼀辆赛车以下⾯的⽅式计算位置。
position=position+speed;
如果第10次逻辑帧后赛车的位置是500,速度是100,那么第11帧的位置就会是600.那么在10.3帧的时候你会在什么位置渲染你的
赛车呢?显⽽易见,应该像下⾯这样:
view_position=position+(speed*interpolation)
现在,赛车将会被正确地渲染在530这个位置。
基本上,插值的值就是渲染发⽣在前⼀帧和后⼀帧中的位置。你需要做的就是写出预⾔函数来预计你的赛车/摄像机或者其他物件在渲
染时刻的正确位置。你可以根据物件的速度来计算预计的位置。这些并不复杂。对于某些预计后的帧中出现的错误现象,如某个物体被渲染
到了某个物体之中的情况的确会出现。由于游戏速度恒定在每秒更新25次状态,那么这种错误停留在画⾯上的时间极短,难以发现,并⽆
⼤碍。
⼩霸王
⼤多数情况下,update_game()执⾏需要的时间⽐display_game()少得多。实际上,我们可以假设在⼩霸王上update_game()每秒还
是能运⾏25次。所以游戏的逻辑状态不会受到太⼤的影响,即使FPS⾮常低。
⽜逼的机器
在⽜逼的硬件上,游戏速度会保持每秒25次,屏幕更新却可以⾮常快。插值的⽅案可以让游戏在⾼帧率中有更好的画⾯表现。但实质
上游戏的状态每秒只更新了25次。
结论
使游戏状态的更新独⽴于FPS的解决⽅案似乎是最好的游戏主循环实现。不过,你必须实现⼀个插值计算函数。
6.整体总结
游戏主循环对游戏的影响远远超乎你的想象。我们讨论了4个可能的实现⽅法,其中有⼀个⽅案是要坚决避免的,那就是可变帧率来决
定游戏速度的⽅案(第3点)。
⼀个恒定的帧率对移动设备⽽⾔可能是⼀个很好的实现,如果你想展⽰你的硬件全部的实⼒,那么最好使⽤FPS独⽴于游戏速度的实现
⽅案(第5点)。
如果你不想⿇烦的实现⼀个预⾔函数则可以使⽤最⼤帧率的实现⽅案(第4点),只是要找到⼀个帧率⼤⼩的平衡点。
本文发布于:2023-01-03 22:07:45,感谢您对本站的认可!
本文链接:http://www.wtabcd.cn/fanwen/fan/90/86820.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |