《遊戲程序設計模式》 2.2 - 遊戲循環

intent
程序員

    把用戶輸入、處理器速率與遊戲時間解耦合。
shell

motivation編程

    若是有一種這本書不能不講的模式,那麼就是這個模式。遊戲循環(Game Loop)是遊戲程序設計模式的精粹。幾乎每一個遊戲都使用它,還並不徹底同樣,而相對的,遊戲以外的程序不多使用這個模式。
windows

    爲了看它到底多有用,咱們快速回憶下。在過去的電腦編程中,程序的工做就行洗碗機。你傾倒一大堆代碼進去,按一個按鈕,等着,而後獲得結果。完畢。這些是批處理程序-一旦工做完成,程序結束。
設計模式

    今天你仍然能看到它,只是沒必要寫到打孔卡上了。shell腳本,命令行,甚至把一堆markdown變成這本書的Python小腳本都是批處理程序。
api

interview with a cpu
瀏覽器

    最終,程序員意識到把一批代碼留在辦公室,幾個小時後回來取結果是一個找出程序bug的很可怕很慢的方法。他們想要即時反饋。交互式程序出現了。首先出現的一部分交互式程序就是遊戲:markdown

YOU ARE STANDING AT THE END OF A ROAD BEFORE A SMALL BRICKBUILDING . AROUND YOU IS A FOREST. A SMALLSTREAM FLOWS OUT OF THE BUILDING AND DOWN A GULLY.網絡

> GO IN函數

YOU ARE INSIDE A BUILDING, A WELL HOUSE FOR A LARGE SPRING.

    你會有一個與程序的實時對話。它等待輸入,而後響應。而後你回覆,如此反覆。當輪到你時,它什麼都不作。就像:

while (true)
{
  char* command = readCommand();
  handleCommand(command);
}

Event loops

    現代圖形應用,若是你剝掉它的外殼,與之前文字冒險遊戲是同樣的。文本處理器在你按下一個鍵或點擊一些東西以前,什麼都不作:

while (true)
{
  Event* event = waitForEvent();
  dispatchEvent(event);
}

    主要的不一樣就是text command換成了user input event-鼠標點擊和鍵盤事件。它仍然像文字冒險遊戲,程序會由於等待輸入而阻塞,這是個問題。

    不像其它大多數軟件,遊戲在沒有輸入的狀況下仍然運行。若是你盯着看,遊戲畫面不會凍結。動畫會一直播放。視覺效果飛舞閃爍。若是你不走運,怪物會啃你的英雄。

    這是遊戲循環的第一個關鍵部分:它等待輸入,可是不能阻塞。循環老是繼續:

while (true)
{
  processInput();
  update();
  render();
}

    後面咱們將會改進它,可是基本步驟仍是都在的。processInput處理上次調用以來的輸入。update更新一次遊戲。它處理AI和物理檢測(一般按此順序)。最後render繪製遊戲,這樣玩家就知道發生了什麼。

a world out of time

    若是循環不會由於輸入阻塞,那麼將會致使一個明顯的問題:以多快的速度循環?每一次遊戲循環會更新必定量的遊戲狀態。從遊戲中居民角度來看,它們的時鐘已經向前走了一下。

    同時玩家的時鐘也在走。若是以真實時間測量遊戲循環的次數,咱們就獲得了「每秒幀數」(fps)。若是遊戲循環快,fps就高,遊戲運行平滑流暢。若是慢,遊戲就會抽搐像定格動畫。

    經過原始的遊戲循環,它能儘量快地運行,影響幀率的有兩個因素。第一個是,每一幀要作多少工做。複雜的物理計算,大量的遊戲對象,和許多圖像細節會使你的CPU和GPU忙碌,會花費更長時間完成一幀。

    第二個是,底層平臺的速度。更快的芯片能夠在相同時間處理更多代碼。多核CPU,GPU,專用音頻硬件和操做系統的調度,都會影響一幀的工做量。

seconds per second

    在早期的視頻遊戲中,第二個因素是固定的。若是你爲NES或APPLE IIe寫遊戲,你須要確切知道CPU型號,而後專門爲其編碼。全部你須要擔憂的是,每一幀能作多少工做。

    舊的遊戲被當心編碼,每一幀作足夠的工做使能夠以須要的速度運行。若是你在一個更快或更慢的機器上運行遊戲,遊戲速度會加快或減慢。

    如今,不多開發者知道遊戲運行的硬件。相反,遊戲必須智能地適應不一樣的設備。

    這就是另外一個關鍵的部分:遊戲無論什麼設備都要以固定速度運行。

the pattern

    遊戲循環在遊戲運行中會持續不斷的執行。每一次循環,它不阻塞的處理用戶輸入,改變遊戲狀態,渲染遊戲。它追蹤時間的流逝控制遊戲的速度。

when to use it

    使用錯的模式比不使用更糟,因此這章正常提醒不要過分熱情。設計模式的目標不是儘量將模式塞滿代碼。

    可是這個模式不一樣。我能夠確定你會使用這個模式。若是你使用一個遊戲引擎,即便不本身寫,它仍然被使用了。

    你可能覺得若是你寫一個回合制遊戲不會用到它。即便遊戲狀態不變,視覺的和音頻的部分也會更新。動畫和音樂都會運行,當遊戲等待玩家回合時。

keep in mind

    咱們這裏討論的是遊戲最重要的一部分代碼。有句話說「90%的時間花費在10%的代碼上」。遊戲循環的代碼絕對在那10%中。注意這些代碼,注意它的效率。

you may coordinate with the platform's event loop

    若是你爲一個有內置消息循環os或平臺寫遊戲,你會有兩個循環。你須要使兩個協調運行。

    有時,你能夠掌控只使用你本身的循環。例如,若是你用windows api寫遊戲,你的main只能有一個循環。裏面,你能夠調用PeekMessage處理分發系統消息。不像GetMessage,PeekMessage獲取用戶輸入不會阻塞,你的循環會一直運行。

    其餘平臺不會讓你輕易退出消息循環。若是你的目標是瀏覽器,消息循環是深深地內置在執行模型裏的。你要使用內置循環做爲循環。你會調用相似requestAnimationFrame函數,這個函數調用你的代碼,保證遊戲運行。

sample code

    對於這麼長的介紹,遊戲循環的代碼其實很是直白。咱們將會看看幾個變種,分析優勢和缺點。

    遊戲循環驅動AI,繪製和其它遊戲系統,可是這不是這個模式的重點,因此咱們直接調用虛構的函數。實現render,update還有其它的留給讀者當作練習。

run,run as fast as you can

    咱們已經看過最簡單的遊戲循環:

while (true)
{
  processInput();
  update();
  render();
}

    這個的問題是你沒法控制遊戲循環的速度。在快機器上,它運行的很快。在慢機器上,它運行的像龜速。若是,你在一幀還有大量工做,像ai或者物理等,要作,那麼還會更慢。

take a little nap

    第一個變種,咱們添加一個簡單的修改。假設你想讓遊戲有60fps。一幀有16毫秒。只要你能夠在這時間內完成遊戲處理和繪製的工做,你就能夠保證一個穩定的幀率。全部你須要作的就是處理一幀,等待下一幀的繪製,就像:

    

    代碼像這樣:

while (true)
{
  double start = getCurrentTime();
  processInput();
  update();
  render();
  sleep(start + MS_PER_FRAME - getCurrentTime());
}

    sleep保證了,若是一幀處理的很快,循環不會執行太快。可是,若是遊戲運行太慢,它就毫無用處。若是update和render花費時間超過16ms,sleep時間將會是負值。若是,咱們能使電腦時間回退,一切都會很簡單,很惋惜,咱們不能。

    相反,遊戲慢下來了。你能夠經過減小一幀的工做量解決此問題-減小圖形和特效或者簡化AI。可是,這會影響遊戲質量,甚至在快機器上。

one small step,one giant step

    讓咱們嘗試一些更復雜的方法。咱們的問題基本上歸結爲:

    1.每一次update都會更新必定量的遊戲時間

    2.會花費必定量的現實時間來處理update

    若是,第二步比第一步用時長,遊戲就會慢下來。若是咱們想經過16ms來更新超過16ms的遊戲內容,那麼咱們將沒法保持。可是,咱們能夠經過超過16ms的時間,更新超過16ms的遊戲內容,下降update的頻率,這樣仍能保持。

    主意就是根據自上一幀依賴通過的現實時間來更新遊戲時間。一幀須要的時間越長,遊戲更新的時間也就越長。遊戲老是能跟上現實時間,由於它一次更新的遊戲時間就是根據現實時間來的。它們被稱爲可變或流動時間步長。像這樣:

double lastTime = getCurrentTime();
while (true)
{
  double current = getCurrentTime();
  double elapsed = current - lastTime;
  processInput();
  update(elapsed);
  render();
  lastTime = current;
}

    每一幀,咱們計算從上一幀以來,流逝了多少現實時間。當咱們更新遊戲狀態,咱們將這個時間傳進去。引擎根據這個時間更新遊戲。

    假設有一顆子彈從屏幕射過。經過固定時間步長,每一幀,子彈根據速度移動。經過可變時間步長,你能夠根據流逝的時間縮放子彈速度。隨着時間步長變大,子彈一幀移動的距離也會變大。子彈將會在相同現實時間內經過屏幕,不論是20小步仍是4大步。這看起來像個勝利者:

  •     遊戲以一致的速率運行在不一樣的硬件上。

  •     玩家使用快機器會獲得更流暢的效果。

    可是,有一個潛伏的嚴重問題:遊戲不肯定也不穩定。這裏有一個陷阱:

    假設有一個二人網絡遊戲,fred有一個高性能遊戲機,george有一個老古董pc。上述子彈從兩人的屏幕上飛過。在fred的機器上,遊戲運行很快,因此每一個時間步長很小。咱們假設,子彈用50幀穿過屏幕。在George的機器上可能只有5幀。

    這說明在fred的機器上,物理引擎更新子彈位置50次,可是George只有5次。大多數遊戲使用浮點數,容易產生舍入偏差。每一次你相加兩個浮點數,你獲得的答案會有一點偏差。fred計算的次數是George的10倍,因此fred的偏差會比George大。同一個子彈在不一樣的機器上會到達不一樣的位置。

    這只是可變時間步長致使的一個棘手問題,還有不少問題。爲了以現實時間運行,遊戲物理引擎逼近真實力學定律。爲了使模擬不飛起,會使用阻力。阻力當心地調到一個肯定時間步長。步長不一樣,物理就變得不穩定。

    不穩定是很噁心的,這裏的例子只是一個反面例子,這引導咱們走向更好……

play catch up

    不受可變時間步長影響的部分一般是渲染。由於渲染引擎捕獲的是一瞬,並不關心通過了多長時間。它繪製碰巧出現的東西。

    咱們能夠利用這個事實。咱們將會以固定時間步長更新遊戲,由於這樣更簡單也更穩定。可是,什麼時候渲染能夠有靈活性,爲了釋放處理器時間。

    就像這樣:必定量的現實時間從上一幀流逝。這就是咱們須要模擬的遊戲時間,以遇上現實時間。咱們以固定時間步長作這些事。就像這樣:

double previous = getCurrentTime();
double lag = 0.0;
while (true)
{
  double current = getCurrentTime();
  double elapsed = current - previous;
  previous = current;
  lag += elapsed;
  processInput();
  while (lag >= MS_PER_UPDATE)
  {
    update();
    lag -= MS_PER_UPDATE;
  }
  render();
}

    還有一些東西。在每一幀開始,咱們更新lag根據流逝的現實時間。這個用來計算遊戲時間落後現實時間多少。咱們再寫一個內部循環更新遊戲,一步是固定時間,直到遇上現實時間。一旦咱們要遇上,咱們渲染,而後從頭再來。你能夠想象成這樣:

    

    注意,這裏的時間步長再也不是可見的幀。MS_PER_UPDATE是咱們更新遊戲的粒度。步長越短,想遇上現實時間須要處理的時間越長。所需時間越長,遊戲波動越大。理想狀況下,你想它很短,快過60fps,這樣遊戲在快機器上能夠模擬得高保真。

    可是不能過短。你必須確保時間步長大於update所需的時間,甚至在最慢的機器上。不然,你的遊戲不可能趕得上現實時間。

    幸運的是,咱們有一些喘息的空間。訣竅是,把渲染從update中拿出來。這將節省大量cpu時間。最終結果就是遊戲在不一樣的硬件上以恆定速度運行。只是在慢機器上,遊戲會波動。

(未完)

相關文章
相關標籤/搜索