Game Loop的幾種實現方式

http://www.bennychen.cn/2011/06/game-loop-model/html

——————————————————————————————網絡

寫這篇博客的目的是爲了對game loop(遊戲主循環)作一個全面的總結和介紹,包括它的定義,與之相關的專業術語(terminology),以及最重要的,對它的幾種實現方式,從代碼層次作一些介紹以及優缺點分析。多線程

1.什麼是Game Loop

對任何一個遊戲開發者來講,game loop都應該不是一個陌生的概念。任何一款遊戲都會有一個本身的game loop,它是整個遊戲的核心,是遊戲的心臟。具體什麼是game loop,這是來自於Game Engine Architecture[2]上對於game loop的定義:game loop是對於一個遊戲(或者一個遊戲引擎)的全部子系統(subsystem)的週期性的更新。一個遊戲會包含各類不一樣的子系統(好比渲染,物理,動 畫,AI等等),這些不一樣的子系統負責處理不一樣的遊戲任務,game loop的實現方式就決定了這些子系統的任務執行的組織方式。而這些就決定了整個遊戲的基礎架構,同時也將決定game在不一樣的機器上將如何運行,在下面 介紹game loop的不一樣實現方式時會對此有詳細介紹。架構

game loop中所處理的邏輯和操做一應俱全,但通常可抽象爲如下這幾種模塊:ide

  • 處理輸入:這些輸入包括,來自於交互設備(鍵盤,鼠標,遊戲控制器等等)的輸入,來自於網絡的輸入;
  • 系統更新(update):(根據輸入或者自發的)對各個子系統進行更新,以決定當前的遊戲狀態;
  • 渲染(render):對整個遊戲場景進行渲染。

因此一個最最簡單的game loop能夠抽象爲以下的這些僞代碼:函數

while ( game is running )
{
    processInput();
    update();
    render();
}

2.相關術語

經常和渲染緊密相連的一個參數是FPS(frame per second),它指的是每秒顯示設備在屏幕上進行繪製的頻率。對於遊戲來講,通常50-60的FPS是最優,最低也不要低於16幀[1]。遊戲中還有一 個容易被忽略的重要參數能夠被稱爲遊戲速度(game speed)[3],它指的是遊戲狀態每秒被更新的次數,容易與FPS混淆。一個通俗的理解和區別它們的方式能夠是,FPS是render函數被調用的頻 率,而game speed則是update函數被調用的頻率。工具

遊戲是對實時性(real-time)要求很高的系統,因此在game loop中,time是尤爲重要的一個因素。這裏須要說的是,遊戲中會包含好幾種不一樣的時間,這些時間有着各自的時間線(timeline),而且在遊戲中發揮着各自不一樣的重要做用[2]。oop

  • 系統的真實時間(real time)。真實時間通常在操做系統被定義爲從過去某個固定時間點(好比常見的一個時間點被稱之爲Epoch, 是1970年1月1日的0:00:00, UTC時間)到當前點總共逝去的時間。
  • 遊戲時間(game time)。通常狀況下,遊戲時間等於系統時間。但在某些特殊狀況下,遊戲時間能夠發揮不一樣的做用:好比在遊戲暫停的時候,須要中止更新遊戲時間;在遊戲 須要以慢速運行時,能夠用比真實時間低的頻率來更新遊戲時間,這在咱們須要進行一些遊戲調試的時候,尤爲有用。下面會介紹到,一個好的game loop結構,能夠有助於遊戲調試,這對開發人員來講是很是重要的。
  • 局部時間線(local timeline)。不論是一個動畫,仍是一個音頻或者視頻片斷,都會有一個局部時間線。經過將此局部時間線以不一樣的方式映射到全局的時間線,能夠靈活的控制片斷以特定的方式進行播放。[2]
  • 幀時間(frame time)。兩次幀循環之間所間隔的時間,具體可看本文章的3.2.1節。

3.Game Loop的實現方式

OK,介紹了這麼多跟game loop相關的概念,接下來進入正題,game loop的各類實現方式大盤點。性能

有一點須要強調的是,game loop在單線程的遊戲系統中是一種狀況,而在多線程的系統中又是另外一個不一樣的故事。多線程對於系統會引入異常的複雜性,關於這個,能夠看我以前的一篇關於遊戲引擎多線程的文章:http://www.bennychen.cn/2011/01/關於遊戲引擎多線程的一些整理和思考/。相比來說,單線程要簡單不少,這篇文章裏如下的一些game loop的實現方式也主要是針對在單線程環境中的。動畫

在單線程環境中,game loop按照實現方式能夠分爲如下這幾種類型:

  • 基於幀的(frame-based)game loop
  • 基於時間的(time-based)game loop
    • 可變頻率(variable-step)game loop
    • 固定頻率(fixed-step)game loop

3.1 基於幀的(frame-based)game loop

這是一種最簡單的game loop,就如上面文章剛開始的僞代碼所描述的同樣,在這種結構下,遊戲只是重複的不間隔的進行processInput,update,render操 做。能夠看到,這種game loop的實現異常簡單,但缺點也是異常明顯的。由於它缺乏了一個很重要的控制因素——時間,因此在不一樣配置的機器環境下,遊戲將以不一樣的速度運行。好比 說,遊戲中的一個物體,在每次的update操做中將它的位移增長5(單位能夠是米,英尺,像素,或者其它某種單位,每一個遊戲都使用一個特定的距離單位)

void update()
{
    object.position += 5.0f;
}

在一個較快速的機器上,update被執行的次數會更多,因此物體移動的速度就會更快,這就致使了遊戲在不一樣機器運行速度的不一致,這是不能接受的。

優勢:簡單;
缺點:在不一樣機器上,遊戲的運行速度不一致。

3.2 基於時間的(time-based)game loop

爲了讓不一樣機器的遊戲運行速度一致,就須要引入時間,這就帶來了基於時間的game loop實現方式。這種方式下,又能夠分爲兩種,可變頻率(variable step)的和固定頻率的(fixed step)。在[5]中,這兩種方式則分別被稱爲可變間隔的(variable interval)和固定間隔的(fixed interval)。

3.2.1 可變頻率的game loop
這種方式的實現只須要爲update函數引入一個時間參數elapsedTime便可,elapsedTime指的是從上一次loop的執行到當前 loop所過去的時間,一般稱之爲幀時間(frame time),或者time delta。它的單位通常使用毫秒,這是在遊戲中進行時間相關操做所使用的標準單位[2],固然其它好比在作profiling時爲了計算某個函數的執行 時間,則用的是機器週期(machine cycle)這樣更精確的時間機制。

這是這種game loop實現的僞代碼,也很是簡單:

lastFrameTime = getCurrentTime();
while ( game is running )
{
    processInput();

    currentFrameTime = getCurrentTime();
    elapsedTime = currentFrameTime - lastFrameTime;
    update( elapsedTime );
    lastFrameTime = currentFrameTime;

    render();
}

這時,再看前面的那個例子,對遊戲中的一個物體,在每次的update操做中不是將它的位移絕對增長5,而是要乘以時間這個因子,這樣5實際表明的是物體的速度。速度乘以時間,就獲得了物體在這段時間內的位移:

void update( float elapsedTime )
{
    object.position += 5.0f * elapsedTime;
}

這樣就解決了在不一樣配置的機器上運行速度不一致的問題。由於在更快的機器上,update執行的頻率高,但frame time的間隔時間較短,因此物體位置更新的頻率高,但每次更新的位移幅度小。反之,在較慢的機器上,update函數的執行頻率較低,但frame time間隔時間較長,則每次物體將以較大幅度的更新位移。正是由於引入了時間因素,因此在不一樣配置的機器上,遊戲的運行速度將會看起來一致。

不過這種game loop在實現時須要解決一個長時間暫停的問題,因此咱們在暫停時,要同時中止更新遊戲時間,以避免在暫停後恢復時,將獲得一個超級大的elapsedTime值。

但隨之而來,這種game loop實現方式的缺點也暴露出來了。好比考慮這樣一種場景,物體繞着一個弧形的軌跡進行移動。在正常的速率下,物體的運行軌跡幾乎是弧形的。以下圖,圖片來自於[5]。

可是在較慢的機器上,雖然物體的移動位置點能保持同步,由於更新的頻率較低,物體的移動軌跡就變得很是離散,以致於不是按照一個弧形在移動,以下圖。

其它的效果也有相似的問題,好比說動畫,雖說動畫的播放速率是一致的,可是在較慢的機器上,會出現比較嚴重的掉幀現象,這就是咱們俗稱的「卡了」。再好比說物理,在一個正常的機器上,一個障礙物可以完美的被避開,但在一個較慢的機器上,這就很差說了。

對於這種game loop,就算是較快的機器上,也是有問題的,雖然好像update被執行的越快越多,遊戲運行的就越流暢,用戶的體驗應該越好纔對。但其實否則,兩點原 因,首先就算在較快的機器上,也可能會遭遇到運算的高峯期,這時因爲對比明顯,遊戲性能的降低會很是明顯,遊戲用戶就很容易察覺到這種性能降級 (performance degradation),這並非好的體驗;再一點,對於手機等移動設備上的遊戲,update速率執行過快不是好事,這對電池是一種消耗。事實是,遊 戲只須要必定範圍內的update頻率就能夠達到流暢而令用戶接受的效果。

優勢:簡單且不一樣機器上的遊戲運行速度是一致的;
缺點:在較慢的機器上,物體的更新頻率慢會致使各類效果失真(distortion);在較快的機器上,更新太快的話會更容易讓用戶察覺到性能降級,且對於移動設備,更新太快會下降電池的使用時間。

3.2.2 固定頻率的game loop
由此,爲了解決上面的問題,就有了這種固定頻率的game loop,讓遊戲的更新速度保持在一個特定的恆定值。好比下面的這段僞代碼,讓遊戲恆定運行在FPS(或者game speed,此時FPS等於game speed)爲25的速度下。代碼參考自[3]。

#define FRAME_RATE 25
#define FRAME_TIME ( 1000 / FRAME_RATE )

nextFrameTime = getCurrentTime();
while ( game is running )
{
    processInput();
    update( FRAME_TIME );
    render();

    nextFrameTime +=  FRAME_TIME;
    currentFrameTime = getCurrentTime();
    if ( nextFrameTime >= currentFrameTime )
    {
        sleep( nextFrameTime - currentFrameTime );
    }
    else
    {
        // 咱們已經跟不上幀速率了
    }
}

能夠看到,若是一次loop執行完的持續時間小於固定幀時間,則直接讓主線程sleep便可。可是若是在較慢的機器上(或者是設定的固定幀速率過 高),執行完一幀的時間會超過固定幀時間,致使沒法達到所目標的幀速率,則只能忍受這種狀況了。最差狀況下,若是在某一時期內遊戲遭遇到了很是巨大的運算 壓力,則遊戲將會變得異常緩慢到沒法忍受的地步。

保持固定的更新頻率有一個很是重要的優勢,由於它帶來了遊戲執行的肯定性(game execution determinism)[1],因此以這種機制所實行的game loop能夠被稱爲肯定性的game loop(deterministic game loop)。反之,可變頻率的game loop則是非肯定性的(non-deterministic),由於它依賴於系統每一幀運行的時間,這在遊戲每次運行時是變化不定的,這就致使遊戲的行 爲也是不定的。

肯定性機制可以爲系統帶來一個很是重要的特色——錄製和回放功能(record and replay)[2]。所謂錄製回放功能,就是可以將玩家在進行遊戲的時候的各類操做記錄下來,以便在下一次運行時,就可以經過回放將遊戲以一樣的方式執 行。這會成爲一個很好的debugging工具,由於經過回放功能,會讓一些難以發現的bug得以垂手可得的復現,這是很是珍貴的。甚至咱們還能夠支持單 步調試(single-stepping)功能[2],單步調試指的是當遊戲處於暫停狀態時,能夠經過某個按鍵,讓遊戲一次執行一個frame time,這在調試遊戲時都是很是有用的。

這種game loop仍是有一個問題,它緊耦合了update和render的執行頻率,update的頻率(即game speed)保持在25基本能知足遊戲運行的流暢需求,但讓render的更新頻率(即FPS)也保持在25,對於配置好的機器實在是有些浪費,咱們能夠 讓渲染的更快,以得到更好的畫面效果。

優勢:在不一樣機器上游戲效果一致,同時爲遊戲帶來肯定性。
缺點:一個更好的機器並不能帶來更好的遊戲畫面,擴展性(scalability)差。

3.2.3 得到最大FPS的固定頻率的game loop
解決上一個game loop缺點的辦法就是解耦update和render,讓它們以各自不一樣的頻率運行。這就帶來了這個固定頻率的game loop的變種,稱之爲「得到最大FPS的固定頻率的game loop」(Constant Game Speed with Maximum FPS)[3]。

同時這種方法還處理另外一個問題,當update處理時間太久時,這種game loop會暫時不進行render而再次update。換言之,當update執行時間長於所指望的幀時間時,遊戲會丟棄繪製幀並調用額外屢次 update函數,以讓遊戲從一個慢速(slowdown)狀態中追遇上並恢復過來[4]。

這是該game loop的代碼:

#define MAXIMUM_FRAME_RATE 45;
#define MINIMUM_FRAME_RATE = 15;
#define UPDATE_INTERVAL ( 1000 / MAXIMUM_FRAME_RATE )
#define MAX_CYCLES_PER_FRAME ( MAXIMUM_FRAME_RATE / MINIMUM_FRAME_RATE )

nextFrameTime = getCurrentTime();
while ( game is running ) 
{
    loops = 0;
    while( getCurrentTime() > nextFrameTime 
          && loops < MAX_CYCLES_PER_FRAME ) 
    {
        update( UPDATE_INTERVAL );
        nextFrameTime += UPDATE_INTERVAL;
        loops++;
    }

    render();
}

從上面的代碼中看出,當出現update處理時間太久時,game loop並非一直重複執行update而不渲染,update頻率被控制在15-45之間,因此在一次while循環中,最多隻會執行3次 update(MAX_CYCLES_PER_FRAME=MAXIMUM_FRAME_RATE / MINIMUM_FRAME_RATE=45/15)。

固然FPS也不老是能夠爲任意值,有時爲了解決顯示設備上的一種叫作tearing[2](屏幕的上半部分顯示的是上一幀的畫面,而下半部分是當前 幀的畫面)的問題,須要將FPS設置爲顯示設備刷新頻率的倍數。在手機等這種移動設備上,一樣爲了省電,也須要將FPS固定在一個恆定值。但無論怎 樣,update和render仍然是以各自不一樣的頻率運行。最近在讀的一本關於iOS遊戲開發的書上[6]的game loop,做者使用的就是這種game loop,它的game speed被設置在15-45,而FPS則採用的是iOS上默認的60(iOS上使用CADisplayLink的frameInterval屬性來設置 繪製幀率,frameInterval默認是1,表示顯示1秒鐘會被刷新60次)。

優勢:擁有固定頻率的game loop的優勢,同時解耦update和render,而且當幀速率下降時,能夠經過丟棄繪製幀來保持遊戲的速度;
缺點:在高配置的機器上依然有些浪費資源。

3.3 其它方式

game loop還能夠有更多的變化,好比在[1]中提到,對於遊戲中的不一樣子系統,update的頻率也是不一致的。好比,爲了得到很好的動畫效果,須要一個較 高的頻率來更新動畫,而對於AI系統,用同等高頻率的速度來更新就是浪費計算資源了。因此這篇文章提出了一種更好的update機制,將update分隔 爲兩部分,一部分以最快的速度運行,而另外一部分以某種預設的固定頻率運行。

在[3]中提出,爲了得到一種更平滑的畫面效果,能夠對frame-time進行插值(interpolate),而且爲update函數提供預測函數(prediction function)。

4.總結

一個看似簡單的game loop,也能夠有這麼多的變數。這篇文章主要基於時間因素列出了4種實現方式:第1種是基於幀的game loop,通常而言要避免採用這種方式,而應該選用後面3種基於時間的game loop。基於時間的可變頻率的game loop是一種常見的實現方式,不過爲了得到穩定的畫面效果和遊戲運行的肯定性,可使用固定頻率的game loop。最後還能夠將update和render解耦以各自頻率運行,以得到最優的組織結構運行遊戲。

OK,這裏就是我有史以來最長的一篇博客的結尾。

5.參考

[1]LUIS VALENTE, Real Time Game Loop Models for Single-Player Computer Games
[2]Jason Gregory, Game Engine Architecture
[3]http://www.koonsolo.com/news/dewitters-gameloop/
[4]http://msdn.microsoft.com/en-us/library/bb203873.aspx
[5]http://sacredsoftware.net/tutorials/Animation/TimeBasedAnimation.xhtml [6]Michael Daley, Learning iOS Game Programming - A Hands-On Guide to Building Your First iPhone Game

相關文章
相關標籤/搜索