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時間。最終結果就是遊戲在不一樣的硬件上以恆定速度運行。只是在慢機器上,遊戲會波動。
(未完)