bada 2D游戲編程之四——設計游戲循環
上篇文章中提到的時間驅動的游戲機制,就是不斷重復執行游戲中的輸入模塊、邏輯模塊和輸出模塊,這個不斷重復的過程可以通過循環來實現,而這個循環就是所說的游戲循環。我們將輸入模塊、邏輯模塊和輸出模塊的功能抽象為三個處理函數,分別為
HandleEvent(),
UpdateLogic()和
Draw(),將這個三個函數按照先後關系放到游戲循環中就出現了下面的邏輯關系圖:

上面的圖只不過是游戲循環的一個基本邏輯情況,這個圖蘊含著各種變化,每種變化都影響著游戲的性能。如果我們去設計一個游戲循環,會如何設計呢?這篇文章就是由這個基本的游戲循環出發,演變出幾種基本的游戲循環,對這幾個循環進行分析和介紹,並進行優缺點分析,加深大家對游戲循環的理解,並最終能夠應用到游戲開發中去。
1,相關用語解釋
首先解釋一下游戲循環中會提到的幾個用語:
幀:在游戲中幀就是指游戲中的一副畫面,游戲就是由連續的幀組成的,通過不斷更新幀來形成游戲動畫
幀間隔:在游戲中連續顯示兩個幀之間的時間間隔,一般以毫秒作為它的單位。
幀率:游戲中幀率也稱為
FPS(Frame Per Second),它表示在游戲中每秒鐘顯示幀的次數,也可以理解為
Draw()函數被調用的頻率。
游戲速率:它指的是每秒鐘游戲狀態更新的次數,也可以理解為
UpdateLogic()被調用的頻率。
2,游戲循環的實現方式
在設計游戲循環時,時間是影響實現方式的重要元素,因為它可以參與改變游戲狀態,在進行游戲邏輯的運算時可以將時間做為變量加入進去。例如游戲中精靈的移動位移,就可以根據精靈的移動速度乘以移動時間來得到。其中按照游戲的邏輯更新與時間之間的關系可以將游戲循環分為基於幀的循環和基於時間的循環。在基於幀的游戲循環中,游戲的邏輯更新於時間無關,而基於時間的循環是需要在游戲邏輯更新時考慮時間變量。
下面就來設計各種類型的游戲循環。
2.1,基於幀的游戲循環
在基於幀的游戲循環中,游戲邏輯的更新不依賴於時間,而是以一幀為單位來進行計算,這樣在游戲中需要設定在每一幀中游戲狀態變化的單位值,也就是每次調用
UpdateLogic()函數時的變化值。比如說在游戲中的一個精靈,在每一幀中它的位移
(Sprite.step)將增加
1個像素,這樣在
UpdateLogic()函數中可以將它的位移增加
1個像素來改變它的位置狀態。
下面是這種游戲循環實現方式和邏輯更新的代碼:
while(isRunning)
{
HandleEvent();
UpdateLogic();
Draw();
}
void UpdateLogic()
{
Sprite.position += Sprite.step;
}
這個游戲循環實現起來是不是很簡單,和上面的圖示一模一樣。這也往往是剛開始進行游戲開發時常用的設計方式,但它會存在一些問題。因為這樣設計的游戲在不同性能的設備上可能會造成游戲運行的速度不一致的情況。在性能高的設備上,運行
HandleEvent(),UpdateLogic()和
Draw()耗時會很小,表示游戲的幀間隔時間會比較短;而在性能低的設備上,計算比較耗時,這樣游戲的幀間隔時間相對會長,這樣會導致在單位時間內,性能高的設備上
UpdateLogic()調用的次數會高於性能低的設備。
假如在一個高速設備上,游戲的幀間隔為
20毫秒,這樣在
1秒鐘內
UpdateLogic()會被調用
50次,移動的位移則為
50×
1 = 50像素;同樣在低速設備上,游戲的幀間隔為
50毫秒,這樣在
1秒鐘內
UpdateLogic()會被調用
20次,移動的位移則為
20×
1 = 20像素。
這樣出現在不同的設備上運行速度不一致的情況。
設備
幀間隔
游戲速率
單位位移(以幀為單位)
位移位移
效果
高速設備
20
50
1像素/幀
50×1 = 50像素
快
低速設備
40
25
1像素/幀
25×1 = 25像素
慢
還有一個問題就是即時在同一款設備上,也會出現游戲運行的速度時快時慢的情況,因為在不同的時刻,根據
CPU的繁忙程度,處理
HandleEvent(),UpdateLogic()和
Draw()的耗時也會出現不一樣的情況。
2.2,基於時間的游戲循環
為了解決游戲在不同性能的硬件下運行速度不同的問題,在基於時間的游戲循環中引入了時間作為變量來控制游戲的狀態變化,會為游戲添加速度屬性來保持不同設備間的一致性。在這種游戲循環中,需要在
UpdateLogic()函數中傳入時間值用作游戲狀態的計算因子。而根據傳入的時間變量產生的方式不同,又可以分為可變間隔循環和固定間隔循環。可變間隔循環中的時間變量是實時的幀間隔時長,而固定間隔循環中的時間變量是人為設定的一個值。
2.2.1基於時間的可變間隔游戲循環
這種實現方式是在
UpdateLogic()函數中傳入一個時間參數
frameTime,這個值是從開始運行上一次循環到執行當前循環之間的間隔時長,也就是幀時間。這個值在處理能力不同的設備上是不同的,即時在同一設備上也會發生波動,所以是一個可變的值。
還是拿游戲中的一個精靈來說,它的速度為
10像素
/s,則在游戲中通過速度乘以時間的方式來計算它的位移。這樣可以保證即使在不同的設備上,只要經過的時長相等,運動的位移就是一樣的。
下面是這種游戲循環實現方式和邏輯更新的偽代碼:
lastFrameTime = GetCurrentTime();
while(isRunning)
{
currentFrameTime = GetCurrentTime();
frameTime = currentFrameTime – lastFrameTime;
HandleEvent();
UpdateLogic(frameTime);
Draw();
lastFrameTime = currentFrameTime;
}
void UpdateLogic(frameTime)
{
Sprite.position += frameTime/1000*Sprite.velocity;
}
雖然這種方法解決了游戲在不同性能的設備上運行速度不同的問題。但是也還存在一些問題,因為在通常情況下,
frameTime的值都保持平穩,不會有太大的變化,但由於
frameTime值完全依賴於運算效率,所以設備有時會出現
CPU忙不過來,而導致
frameTime增大的情況,比如在玩游戲時,有其它後台程序占用了大量的
CPU而導致運算游戲邏輯的效率降低,處理
HandleEvent(),UpdateLogic(frameTime),Draw()的時間增加,也就是
frameTime增加。這樣如果游戲中有在邏輯更新時進行碰撞檢測的情況,則有可能會出現漏掉部分碰撞點的情況。
給大家用圖來說明一下這個問題,
這種圖示情況下
frame time比較小,游戲中調用
UpdateLogic()函數並進行碰撞檢測的頻率比較高,這樣在
t3時刻進行碰撞檢測時剛好能夠將和
wall的碰撞情況檢測出來。

而在這種情況下由於
frame time比較大,游戲中調用
UpdateLogic()函數並進行碰撞檢測的頻率比較低,次數比較少,所以當在
t3時刻進行碰撞檢測時,
Sprite已經越過
wall了,檢測不到和
wall的碰撞情況。這樣就會出現小球穿牆而過的情況,不符合真實的物理規律。

這樣設計的游戲循環還有一個顯著的缺點,就是游戲
while循環在不停的運行,一直占用
CPU,比較耗
CPU資源。
2.2.2,基於時間的固定間隔游戲循環
前面的兩種游戲循環都是在讓
CPU盡情飛奔,將游戲的幀率發揮到了最大極限。而在游戲中一般
50-60的幀率是最優的,很多情況下下最好將幀率設定為
30,這對復雜的游戲很有幫助,因為這樣可以避免由於幀率無法達到
60,而在游戲過程中幀率發生大幅波動。在這種情況下,把幀率設為可能達到的最低幀率,因為較低但是穩定的幀率可以保證游戲的流暢運行,而平均幀率較高但是幀率可能發生大幅波動的游戲會降低玩家的用戶體驗。
基於時間的固定間隔的游戲循環就是為游戲設定一個理想的幀率,讓游戲邏輯基於固定的幀時間進行計算。
下面是這種游戲循環實現方式的代碼:
const int FAME_RATE = 40;
const long FRAME_TIME = 1000/FRAME_RATE;
while(isRunning)
{
startTime = GetCurrentTime();
HandleEvent();
UpdateLogic(FRAME_TIME);
Draw();
endTime = GetCurrentTime();
deltaTime = endTime – startTime - FRAME_TIME;
if(deltaTime > 0)
{
sleep(deltaTime);
}
Else
{
//發生意外情況,運算超時了
}
}
void UpdateLogic(FRAME_TIME)
{
Sprite.position += Sprite.velocity* FRAME_TIME;
}
這樣如果執行
HandleEvent(),UpdateLogic(),Draw()的時間小於設定的幀時間,則可以通過讓執行循環的線程
sleep,來讓出
CUP的時間片。
在這種循環中,由於向
UpdateLogic()傳入的是固定的
FRAME_TIME值,游戲中依靠時間來進行計算已經失去了意義,反而還會增加計算量,可將它和基於幀的循環結合起來,讓游戲以每一幀為單位進行運算,省去與時間相乘的運算過程,提高運行效率。
這樣就可以簡化為下面的情況。
const int FAME_RATE = 40;
const long FRAME_TIME = 1000/FRAME_RATE;
while(isRunning)
{
startTime = GetCurrentTime();
HandleEvent();
UpdateLogic();
Draw();
endTime = GetCurrentTime();
deltaTime = endTime – startTime - FRAME_TIME;
if(deltaTime > 0)
{
sleep(deltaTime);
}
else
{
//發生意外情況,運算超時了
}
}
void UpdateLogic()
{
Sprite.position += Sprite.step;
}
3,其它的設計方式
上面也只是列舉出了幾個基本的游戲循環,還有很多種不同的設計方式。比如可以將游戲的幀頻率和速率分開處理,就是讓調用
UpdateLogic()的次數和
Draw()的次數不保持一致,這樣在當游戲設定的幀率比較低時,可以通過在同一幀中增加調用
UpdateLogic()次數來增加碰撞檢測的次數,從而可以減少漏掉碰撞檢測的概率。
本文出自 “badaeva” 博客,請務必保留此出處http://badaeva.blog.51cto.com/3433333/936711