游戲主循環是游戲的心跳,一般使用while循環進行主動刷新。
一次循環由獲取用戶輸入、更新游戲狀態、處理AI、播放音樂和繪制畫面組成。
這些行為可以分成兩類:
update_game(); // 更新游戲狀態(邏輯幀),一般不耗時
display_game(); // 更新顯示(顯示幀),耗時(場景越復雜越耗時)
幾個概念
游戲速度:每秒調用update_game的次數。
FPS:即幀率;每秒調用display_game的次數。
可變顯示FPS:即可變顯示幀率,每秒調用display_game且顯示畫面有變化的次數。
最簡單的游戲循環
bool game_is_running = true; while( game_is_running ) { update_game(); display_game(); }
該循環主要的問題是忽略了時間,游戲會盡情的飛奔,能有多快就運行多快
我們會看到在性能好的機器上,物體運動得更快一些
FPS依賴恆定的游戲速度
const int FRAMES_PER_SECOND = 25; const int SKIP_TICKS = 1000 / FRAMES_PER_SECOND; DWORD next_game_tick = GetTickCount(); // 返回當前的系統已經運行的毫秒數 int sleep_time = 0; bool game_is_running = true; while( game_is_running ) { update_game(); display_game(); next_game_tick += SKIP_TICKS; sleep_time = next_game_tick - GetTickCount(); if( sleep_time >= 0 ) { Sleep( sleep_time ); } }
優點:重新播放游戲會顯得簡單(因為每幀時間間隔固定,只需要記錄下每一幀游戲的狀態,回放時按照25幀的速度播放即可)
配置差的機器的表現:到某些復雜的游戲場景時,display_game繪制會耗費大量時間,影響游戲輸入和AI的響應,游戲會變得很慢(卡)
當場景變得簡單時,游戲會加速運行,直到match到正常的步伐,然后穩定到25幀
牛逼的機器的表現:對於高速移動的物體,對視覺效果有一些影響(原來可以跑300幀,現在被強制只能運行25幀);另外,由於調用了Sleep,會比較省電一些
結論:FPS閾值定義得太高會使得配置差的機器機不堪重負,定義得太低則會使得高端硬件損失太多視覺效果
可變FPS決定游戲速度
DWORD prev_frame_tick; DWORD curr_frame_tick = GetTickCount(); // 返回當前的系統已經運行的毫秒數 bool game_is_running = true; while( game_is_running ) { prev_frame_tick = curr_frame_tick; curr_frame_tick = GetTickCount(); // 返回當前的系統已經運行的毫秒數 update_game( curr_frame_tick - prev_frame_tick ); display_game(); }
這種方案在update_game時需要考慮當前幀與上一幀的時間差。
配置差的機器的表現:到某些復雜的游戲場景時,display_game繪制會耗費大量時間,影響游戲輸入和AI的響應,游戲會卡頓
然而在下一幀,就會強制match到正常的步伐,這樣我們就會看到一些跳變(經常發生一些違反物理規律的怪事)
牛逼的機器的表現:也可能會出現問題,原因是update_game的調用次數存在差異;越牛逼的機器,update_game的調用次數越多。這種差異引起的浮點數誤差,會導致致命的錯誤
結論:該游戲模型只能用於單機游戲和狀態同步網游,不能用於幀同步網游
注:幀同步以幀為基本計時單位的一個同步方案,具體來說每台機器都必須運行一樣的邏輯幀順序(如同看視頻,允許有緩沖,但是幀序列都是一樣的);
每台機器只用發送其所有的輸入事件給其他機器,就可以在其他機器上得到與本機相同的運行結果。
優點:簡單、網絡流量只與輸入事件的多少有關;可以將多人單機游戲(黑盒)改造成網絡游戲。缺點:不允許有隨機邏輯,且反外掛困難
最大FPS和恆定速度游戲
const int TICKS_PER_SECOND = 50; const int SKIP_TICKS = 1000 / TICKS_PER_SECOND; const int MAX_FRAMESKIP = 10; DWORD next_game_tick = GetTickCount();// 返回當前的系統已經運行的毫秒數 int loops; bool game_is_running = true; while( game_is_running ) { loops = 0; while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) { update_game(); next_game_tick += SKIP_TICKS; loops++; } display_game(); }
配置差的機器的表現:當渲染幀率下降到5(TICKS_PER_SECOND/MAX_FRAMESKIP)即:loops>=MAX_FRAMESKIP,游戲才會變慢(卡)
當場景變得簡單時,游戲會加速運行,直到match到正常的步伐,然后穩定到50幀
牛逼的機器的表現:游戲會以穩定的50幀速度更新,渲染速度也盡可能的快;但渲染速度超過了50幀時,有一些幀的畫面將會完全相同,所以顯示FPS實際上也等同於最快50幀
結論:如果定義過高的FPS閾值,會讓配置差的機器吃不消,過低則會讓牛逼的機器難以發揮性能
獨立的可變顯示FPS和恆定的游戲速度
const int TICKS_PER_SECOND = 25; const int SKIP_TICKS = 1000 / TICKS_PER_SECOND; const int MAX_FRAMESKIP = 5; DWORD next_game_tick = GetTickCount();// 返回當前的系統已經運行的毫秒數 int loops; float interpolation; bool game_is_running = true; while( game_is_running ) { loops = 0; while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP ) { update_game(); next_game_tick += SKIP_TICKS; loops++; } interpolation = float( GetTickCount() + SKIP_TICKS - next_game_tick ) / float( SKIP_TICKS ); display_game( interpolation ); }
邏輯幀(玩家輸入、AI)本身並不需要很高的速度,25幀就足夠了
渲染則放任不管,任其飛奔;與前面的方案相比,display_game多了一個插值參數,我們需要在display_game里面實現一個接受插值參數的預言函數
邏輯幀為25幀,如果渲染時不時用插值計算,顯示幀會被限定在25幀。25幀可以很好的展示游戲畫面,不過對於高速的物體,更高的幀率會有更好的效果
所以,我們需要一個插值和預言函數讓高速移動的物體在顯示幀之間平滑的過度
插值和預言函數
游戲狀態更新在一個恆定的幀率下運行着,當你渲染畫面的時刻,很有可能就在兩個邏輯幀之間
假設你已經第10次更新了你的游戲狀態,現在你需要渲染你的場景,這次渲染就會出現在第10次和第11次邏輯幀之間
很有可能出現在第10.3幀的位置。那么插值的值就是0.3。舉個例子說,一輛賽車以下面的方式計算位置
position = position + speed;
如果第10次邏輯幀后賽車的位置是500,速度是100,那么第11幀的位置就會是600. 那么在10.3幀的時候你會在什么位置渲染你的賽車呢?
顯而易見,應該像下面這樣:
view_position = position + (speed * interpolation)
注:position=500,speed=100,interpolation = 0.3
現在,賽車將會被正確地渲染在530這個位置。基本上,插值的值就是渲染發生在前一幀和后一幀中的位置。
你需要做的就是寫出預言函數來預計你的賽車/攝像機或者其他物件在渲染時刻的正確位置。
你可以根據物件的速度來計算預計的位置。這些並不復雜。
對於某些預計后的幀中出現的錯誤現象,如某個物體被渲染到了某個物體之中的情況的確會出現。
由於游戲速度恆定在25幀,那么這種錯誤停留在畫面上的時間極短,難以發現,並無大礙。
配置差的機器的表現:當渲染幀率下降到5(TICKS_PER_SECOND/MAX_FRAMESKIP),此時loops>=MAX_FRAMESKIP,游戲才會變慢
當場景變得簡單時,游戲會加速運行,直到match到正常的步伐,然后邏輯幀穩定到25幀
牛逼的機器的表現:邏輯幀會保持25幀,插值的方案可以讓游戲在高幀率中有更好的畫面表現。
結論:最好的游戲主循環實現。不過,必須實現一個插值計算函數
整體總結
討論了4個可能的實現方法,其中有一個方案是要堅決避免的,那就是可變FPS決定游戲速度的方案。
恆定的幀率對移動設備而言,可能是一個很好的實現;如果你想展示你的硬件全部的實力,那么最好使用獨立的可變顯示幀率和恆定的游戲速度的實現方案。
如果不想麻煩的實現一個預言函數,那么可以使用最大FPS和恆定的游戲速度的實現方案,唯一需要考慮的是找到一個合適的FPS閾值。
參考