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()
次數來增長碰撞檢測的次數,從而能夠減小漏掉碰撞檢測的機率。