原文地址
http://dewitters.koonsolo.com/gameloop.html
游戲主循環(huán)
引言
游戲主循環(huán)是每個游戲的心跳,輸送著整個游戲需要的養(yǎng)分。不幸的是沒有任何一篇好的文章來指導(dǎo)一個菜鳥游戲程序員如何為自己的程序供養(yǎng)。不過不用擔(dān)心,因?yàn)槟銊偤貌恍⌒目吹搅诉@篇,也是唯一一篇給予這個話題足夠重視的文章。
由于我身為游戲程序員,我見過許許多多的手機(jī)小游戲的代碼。這些代碼給我展示了五彩繽紛的游戲主循環(huán)實(shí)現(xiàn)方法。你可能要問:“這么簡單的一個小玩意還能做到千奇百怪?” 事實(shí)就是這樣,我就會在此文中討論一些主流實(shí)現(xiàn)的優(yōu)缺點(diǎn),并且給你介紹在我看來最好的輸送養(yǎng)分的解決方案。
游戲主循環(huán)
每一個游戲都是由獲得用戶輸入,更新游戲狀態(tài),處理AI,播放音樂和音效,還有畫面顯示這些行為組成。游戲主循環(huán)就是用來處理這個行為序列。如我在引言中所說,游戲主循環(huán)是每一個游戲的心跳。在此文中我不會深入講解上面提到的任何一個行為,而只詳細(xì)介紹游戲主循環(huán)。所以我把這些行為簡化為了兩個函數(shù):
update_game(); //更新游戲狀態(tài) (后文可能翻譯為邏輯幀)
display_game(); //更新顯示 (顯示幀)
下面是最簡單的游戲主循環(huán):
bool game_is_running = true;
while( game_is_running ) {
update_game();
display_game();
}
這個簡單循環(huán)的主要問題是它忽略了時間,游戲會盡情的飛奔。在小霸王機(jī)器上運(yùn)行會使玩家有極強(qiáng)的挫敗感,在牛逼的機(jī)器上運(yùn)行則會要求玩家有超人的判斷力和APM(原意為慢的機(jī)器上運(yùn)行慢,快的機(jī)器上運(yùn)行快……)。在遠(yuǎn)古時代,硬件的速度已知的情況下,這不算什么,但是目前有如此多的硬件平臺使得我們不得不去處理時間這個重要因素。對于時間的處理有很多的方法,接下來我會一一奉上。
首先我會解釋兩個貫穿全文的術(shù)語:
每秒幀數(shù)(后簡稱FPS)
FPS是Frames Per Second的縮寫。在此文的上下文中它意味著display_game()每秒被調(diào)用的次數(shù)。
游戲速度
游戲速度是每秒更新游戲狀態(tài)的速度,換言之,即update_game()每秒被調(diào)用的次數(shù)。
FPS依賴于恒定的游戲速度
實(shí)現(xiàn)
一個讓游戲每秒穩(wěn)定運(yùn)行在25幀的解決方案如下:
const int FRAMES_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / FRAMES_PER_SECOND;
DWORD next_game_tick = GetTickCount();
// GetTickCount() returns the current number of milliseconds
// that have elapsed since the system was started
int sleep_time = 0;
bool game_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 );
}
else {
// Shit, we are running behind!
}
}
這個方案有一個非常大的優(yōu)點(diǎn):簡單!因?yàn)槟阒纔pdate_game()每秒被調(diào)用25次,那么你的游戲的邏輯部分代碼編寫將非常直白。比如說在這種主循環(huán)實(shí)現(xiàn)的游戲中實(shí)現(xiàn)一個 重放 函數(shù)將非常簡單(譯者注:因?yàn)槊繋拈g隔時間已知,只需要記錄每一幀游戲的狀態(tài),回放時按照恒定的速度播放即可。就像電影膠片一樣)。如果在游戲中沒有受到隨機(jī)值的影響,只需要記錄玩家的輸入就可以實(shí)現(xiàn)重放。
在你實(shí)現(xiàn)這個循環(huán)的硬件上你可以按需要調(diào)整FRAMES_PER_SECOND到一個理想的值,但是這個游戲主循環(huán)實(shí)現(xiàn)會在各種硬件上表現(xiàn)得怎么樣呢?
小霸王機(jī)
如果硬件可以應(yīng)付指定的FPS,那么不會有什么事情發(fā)生。但是小霸王通常是應(yīng)付不了的,游戲就會卡。在極端情況下就會卡得掉渣,或者一步十卡、一卡十步(原意為某些情況下游戲速度很慢,有一些情況下又比較正常)。這樣的問題會毀掉你的游戲,使得玩家及其挫敗。
牛逼的機(jī)器
在牛逼的機(jī)器上似乎不會有任何問題,但是這樣的游戲主循環(huán)浪費(fèi)大量的時鐘循環(huán)!牛逼的機(jī)器運(yùn)行這個游戲可以輕松的跑到300幀,卻每秒只運(yùn)行了25或者30幀~ 那么這個主循環(huán)實(shí)現(xiàn)會讓擁有牛逼硬件的玩家無法盡情發(fā)揮其硬件效果產(chǎn)生極大的挫敗感(原意為這樣的實(shí)現(xiàn)會讓你的視覺效果受到影響,尤其是高速移動物體)。
從另外一個角度來說,在移動設(shè)備上,這一點(diǎn)可能會是一個優(yōu)點(diǎn)。游戲持續(xù)的高速運(yùn)行會很快地消耗電池……
結(jié)論
基于恒定游戲速度的FPS的主循環(huán)實(shí)現(xiàn)方案簡單易學(xué)。但是存在一些問題,比如定義的FPS太高會使得老爺機(jī)不堪重負(fù),定義的FPS太低則會使得高端硬件損失太多視覺效果。
基于可變FPS的游戲速度
實(shí)現(xiàn)
另外一種游戲?qū)崿F(xiàn)可以讓游戲盡可能的飛奔,并且讓依據(jù)FPS來決定游戲速度。游戲狀態(tài)會根據(jù)每一顯示幀消耗的時間來進(jìn)行更新。
DWORD prev_frame_tick;
DWORD curr_frame_tick = GetTickCount();
bool game_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();
}
這個游戲主循環(huán)的代碼比起之前稍微復(fù)雜一些,因?yàn)槲覀儽仨毴タ紤]兩次update_game()調(diào)用之間的時間差。不過,好在這并不算復(fù)雜。
初窺這個實(shí)現(xiàn)的代碼好像是一個理想的實(shí)現(xiàn)方案。我已經(jīng)見過許多聰明的游戲程序員用這種方式來書寫游戲主循環(huán)。但是我會給你展示這個實(shí)現(xiàn)方案在小霸王和牛逼的機(jī)器上的嚴(yán)重問題!是的,包括非常職業(yè)非常嫻熟非常牛逼的玩家的機(jī)器。
小霸王
小霸王會在某些運(yùn)算復(fù)雜的地方出現(xiàn)卡的情況,尤其在3D游戲中的復(fù)雜場景更是如此。幀率的降低會影響游戲輸入的響應(yīng),同時降低玩家的反應(yīng)速度。游戲狀態(tài)更新也會因此突然受到影響。這樣的情況會使得玩家和AI的反應(yīng)速度減慢,造成玩家挫敗感加劇。比如一個在正常幀率下可以輕松越過的障礙會在低幀率下無法逾越。更嚴(yán)重的問題是在小霸王上會經(jīng)常發(fā)生一些違反物理規(guī)律的怪事,如果這些運(yùn)算涉及到物理模擬的話。
牛逼的機(jī)器
你可能會好奇,為什么剛才的游戲循環(huán)在飛快的機(jī)器上會出現(xiàn)問題。不幸的是,這個方案的確如此,首先,讓我給你介紹一些計(jì)算機(jī)數(shù)學(xué)知識。
浮點(diǎn)數(shù)類型占用內(nèi)存大小是有限的,那么有一些數(shù)值就無法被呈現(xiàn)。比如0.1就不能用2進(jìn)制表示,所以會被近似的存儲在一個浮點(diǎn)數(shù)中。我用python給你們展示一下。
>>> 0.1
0.10000000000000001
這個問題本身并不怎么具有戲劇性,但是這樣的后果卻截然相反。比方說你的賽車的速度是0.001個單元每微秒。那么正確的結(jié)果是在10秒后你的賽車會移動10個單位,那么我們這樣來實(shí)現(xiàn)一下:
>>> def get_distance( fps ):
... skip_ticks = 1000 / fps
... total_ticks = 0
... distance = 0.0
... speed_per_tick = 0.001
... while total_ticks < 10000:
... distance += speed_per_tick * skip_ticks
... total_ticks += skip_ticks
... return distance
現(xiàn)在我們來得到40幀每秒時運(yùn)行10秒后的結(jié)果
>>> get_distance( 40 )
10.000000000000075
等等~怎么不是10呢?發(fā)生了什么?嗯,400次加法后的誤差就有這么大,每秒運(yùn)行100次加法后又會是怎么一個樣子呢?
>>> get_distance( 100 )
9.9999999999998312
誤差越來越大了!那么,40幀每秒的結(jié)果和100幀每秒之間誤差差距是多大呢?
>>> get_distance( 40 ) - get_distance( 100 )
2.4336088699783431e-13
你可能會想這樣的誤差可以忽略。但是真正的問題出現(xiàn)在你使用這些錯誤的值去進(jìn)行更多的運(yùn)算。小的誤差會被擴(kuò)大為致命的錯誤!然后這些錯誤會在游戲飛奔的同時毀掉它!這些問題發(fā)生的幾率絕對大到足夠引起你的注意。我有見過因?yàn)檫@個原因在高幀率出現(xiàn)問題得游戲。之后那個游戲程序員發(fā)現(xiàn)這些問題出現(xiàn)在游戲的核心部分,只有重寫大部分代碼才能修復(fù)它。
結(jié)論
這樣的游戲主循環(huán)看上起不錯,但是并不怎么樣。不管運(yùn)行它的硬件怎樣,都可能出現(xiàn)嚴(yán)重的問題。另外,游戲?qū)崿F(xiàn)的代碼相對于固定游戲速度的主循環(huán)而言更加復(fù)雜,那你還有什么使用它的理由呢?
最大FPS和恒定游戲速度
實(shí)現(xiàn)
我們的第一個實(shí)現(xiàn)中,F(xiàn)PS依賴于恒定的游戲速度,在低端的機(jī)器上會出現(xiàn)問題。游戲速度和游戲顯示都會出現(xiàn)掉幀。一個可行的解決方案是犧牲顯示幀率的來保持恒定的游戲速度。下面就實(shí)現(xiàn)了這種方案:
const int TICKS_PER_SECOND = 50;
const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = 10;
DWORD next_game_tick = GetTickCount();
int loops;
bool game_is_running = true;
while( game_is_running ) {
loops = 0;
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
update_game();
next_game_tick += SKIP_TICKS;
loops++;
}
display_game();
}
游戲會以穩(wěn)定的50(邏輯)幀每秒的速度更新,渲染速度也盡可能的快。需要注意的是,如果渲染速度超過了50幀每秒的話,有一些幀的畫面將會是完全相同的。所以顯示幀率實(shí)際上也等同于最快每秒50幀。在小霸王上運(yùn)行的話,顯示幀率會在更新游戲狀態(tài)循環(huán)達(dá)到MAX_FRAMESKIP時下降。從上面這個例子來說就是當(dāng)渲染幀率下降到5(FRAMES_PER_SECOND / MAX_FRAMESKIP)以下時,游戲速度會變慢。
小霸王
在小霸王上運(yùn)行這樣的游戲循環(huán)會出現(xiàn)掉幀,但是游戲速度不受到影響。如果硬件還是沒有辦法處理這樣的循環(huán),那么游戲速度和游戲幀率都會受到影響。
牛逼的機(jī)器
在牛逼的機(jī)器上這個游戲循環(huán)不會出現(xiàn)問題,但是如同第一個解決方案一樣,還是浪費(fèi)了太多的時鐘周期。找到一個快速更新并且依然能夠在小霸王上運(yùn)行游戲的平衡點(diǎn)是至關(guān)重要的!
結(jié)論
使用上面的這個方案可以使游戲的實(shí)現(xiàn)代碼比較簡單。但是仍然有一些問題:如果定義了一個過高的FPS會讓小霸王吃不消,如果過低則會讓牛逼的機(jī)器難以發(fā)揮性能。
獨(dú)立的可變顯示幀率和恒定的游戲速度
實(shí)現(xiàn)
有沒有可能對之前的那種方案進(jìn)行優(yōu)化使得它在各種平臺上都有足夠好的表現(xiàn)呢?當(dāng)然是有的!游戲狀態(tài)本身并不需要每秒更新60次。玩家輸入,AI信息等都不需要如此高的幀率來進(jìn)行更新,大約每秒25次就足夠了。所以,我們可以試著讓update_game()每秒不多不少的被調(diào)用25次。渲染則放任不管,讓其飛奔。但是不能讓小霸王的低渲染幀率影響到游戲狀態(tài)更新的速度。下面就是這個方案的實(shí)現(xiàn):
const int TICKS_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = 5;
DWORD next_game_tick = GetTickCount();
int loops;
float interpolation;
bool game_is_running = true;
while( game_is_running ) {
loops = 0;
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
update_game();
next_game_tick += SKIP_TICKS;
loops++;
}
interpolation = float( GetTickCount() + SKIP_TICKS - next_game_tick )
/ float( SKIP_TICKS );
display_game( interpolation );
}
使用這種方案的update_game()實(shí)現(xiàn)會比較簡單,相對而言,display_game()則會變得稍許復(fù)雜。你需要實(shí)現(xiàn)一個接收插值參數(shù)的預(yù)言函數(shù),這并不是什么難事,只是需要做一些額外的工作。我會接著解釋這個預(yù)言函數(shù)是如何工作的,不過首先讓我告訴你為什么需要這樣的一個函數(shù)。
游戲狀態(tài)每秒被更新25次,如果你渲染的時候不使用插值計(jì)算,渲染幀率就會被限定在25幀。需要注意的是,25幀并沒有人們想象中的糟糕,電影畫面在每秒24幀的情況下依然流暢。所以25幀可以很好的展示游戲畫面,不過對于高速移動的物體,更高的幀率會帶來更好的效果。所以我們要做的是,在顯示幀之間讓高速移動的物體平滑過度。這就是我們需要一個插值和預(yù)言函數(shù)的原因。
插值和預(yù)言函數(shù)
如我之前所說,游戲狀態(tài)更新在一個恒定的幀率下運(yùn)行著,當(dāng)你渲染畫面的時刻,很有可能就在兩個邏輯幀之間。假設(shè)你已經(jīng)第10次更新了你的游戲狀態(tài),現(xiàn)在你需要渲染你的場景。這次渲染就會出現(xiàn)在第10次和第11次邏輯幀之間。很有可能出現(xiàn)在第10.3幀的位置。那么插值的值就是0.3。舉個例子說,我的一輛賽車以下面的方式計(jì)算位置。
position = position + speed;
如果第10次邏輯幀后賽車的位置是500,速度是100,那么第11幀的位置就會是600. 那么在10.3幀的時候你會在什么位置渲染你的賽車呢?顯而易見,應(yīng)該像下面這樣:
view_position = position + (speed * interpolation)
現(xiàn)在,賽車將會被正確地渲染在530這個位置。
基本上,插值的值就是渲染發(fā)生在前一幀和后一幀中的位置。你需要做的就是寫出預(yù)言函數(shù)來預(yù)計(jì)你的賽車/攝像機(jī)或者其他物件在渲染時刻的正確位置。你可以根據(jù)物件的速度來計(jì)算預(yù)計(jì)的位置。這些并不復(fù)雜。對于某些預(yù)計(jì)后的幀中出現(xiàn)的錯誤現(xiàn)象,如某個物體被渲染到了某個物體之中的情況的確會出現(xiàn)。由于游戲速度恒定在每秒更新25次狀態(tài),那么這種錯誤停留在畫面上的時間極短,難以發(fā)現(xiàn),并無大礙。
小霸王
大多數(shù)情況下,update_game()執(zhí)行需要的時間比display_game()少得多。實(shí)際上,我們可以假設(shè)在小霸王上update_game()每秒還是能運(yùn)行25次。所以游戲的邏輯狀態(tài)不會受到太大的影響,即使FPS非常低。
牛逼的機(jī)器
在牛逼的硬件上,游戲速度會保持每秒25次,屏幕更新卻可以非??臁2逯档姆桨缚梢宰層螒蛟诟邘手杏懈玫漠嬅姹憩F(xiàn)。但實(shí)質(zhì)上游戲的狀態(tài)每秒只更新了25次。
結(jié)論
使游戲狀態(tài)的更新獨(dú)立于FPS的解決方案似乎是最好的游戲主循環(huán)實(shí)現(xiàn)。不過,你必須實(shí)現(xiàn)一個插值計(jì)算函數(shù)。
整體總結(jié)
游戲主循環(huán)對游戲的影響遠(yuǎn)遠(yuǎn)超乎你的想象。我們討論了4個可能的實(shí)現(xiàn)方法,其中有一個方案是要堅(jiān)決避免的,那就是可變幀率來決定游戲速度的方案。
一個恒定的幀率對移動設(shè)備而言可能是一個很好的實(shí)現(xiàn),如果你想展示你的硬件全部的實(shí)力,那么最好使用FPS獨(dú)立于游戲速度的實(shí)現(xiàn)方案。
如果你不想麻煩的實(shí)現(xiàn)一個預(yù)言函數(shù),那么可以使用最大幀率的實(shí)現(xiàn)方案,只是要找到一個幀率大小的平衡點(diǎn)。
現(xiàn)在,你可以著手編寫你夢寐以求的游戲了!
Koen Witters
posted on 2009-08-25 21:59
Charlie 侯杰 閱讀(4661)
評論(9) 編輯 收藏 引用