一個典型的遊戲循環是怎樣的流程?

從新的一幀開啟,可能是確定的1/60秒更新一次。到GPU繪製完畢寫入後台緩衝區結束。


這個問題範圍比較大,我覺得 Gregory 寫的30頁內容,會是挺適合作為答案的:

7. The Game Loop and Real-Time Simulation

7.1 The Rendering Loop

7.2 The Game Loop

7.3 Game Loop Architectural Styles

7.4 Abstract Timelines

7.5 Measuring and Dealing with Time

7.6 Multiprocessor Game Loops

7.7 Networked Multiplayer Game Loops

讓我翻譯一下……

7. 遊戲循環及實時模擬

7.1 渲染循環

7.2 遊戲循環

7.3 遊戲循環的架構風格

7.4 抽象時間線

7.5 測量及處理時間

7.6 多處理器的遊戲循環

7.7 網路多人遊戲循環

……因時間關係,全部內容已經預先翻譯好了、審好了、改好了、排版好了、印刷好了、上架了、斷貨了、再版了、再斷貨了,再再版了……


參考下milo大大翻譯的《遊戲引擎架構》吧。

單線程的遊戲循環就比較簡易,這種一路運行下來把該做的做了就好。時間線更新,輸入輸出,人工智慧,碰撞檢測,物理模擬,骨骼動畫更新,場景更新,渲染等等都是一條路這麼run下來。這樣雖然架構挺簡單,但是有些更新頻率不一樣的子模塊,如果處理不當就可能造成資源的浪費,明明不用一幀更新一次結果就在那拚命運行。例如動力學模擬要更頻繁120hz什麼的,渲染就不用那麼猛60hz就夠用,人工智慧這種看不到的其實1hz就差不多了。還有就是I/O,單線程架構下要是IO是阻塞的例如某些文件載入方法,這樣又會更卡。

在多線程下的遊戲循環架構,會更加麻煩但是更靈活。有一種架構是每個子系統獨立運行在一個線程裡面,這樣就可以按照每個子系統的速度來運行,還能充分利用多核處理器。但是這樣如果子系統間有耦合,例如動畫系統可能會需要物理系統的布娃娃模擬,渲染系統也要等動畫系統更新好頂點等數據,這樣處於頂端的線程又要wait了…所以還有一種多線程架構,是把各種要乾的東西分成更小的「任務」(job),這樣小粒度的分割可能更加充分地利用多核處理器,哪個有空給哪個。

( ̄_, ̄ ),其實bb這麼多,有些東西我也還沒實現,但是這個理論指導意義是不錯的


無聊

看見大胸廣告

點擊廣告

進入遊戲

新手教程

各種PK各種收集

被打下排行榜

不爽

充錢

充錢,充錢,充錢

爽,爽,爽

高潮

===賢者時間==

棄坑

無聊

看見大胸廣告

。。。可恥地來搗亂了。。。


這段時間剛好看了一些OGRE的源碼,對OGRE的渲染流程做了一些筆記,可能對題主有些幫助。

本人自學引擎開發不滿一年,有一些概念理解不是很透徹,也有可能完全理解偏了,如果筆記里有錯誤,懇請指出,這對我來說也是一個學習的機會。

另外, @oz01 的答案基本可以看做下表流程的一個概括說明。

在他的基礎上再補充幾點:

1. 在OGRE中,開發者可以通過實現各種監聽器的介面來實現遊戲邏輯,它們遍布在OGRE的整個渲染過程中,一般來說主要的遊戲邏輯都在「幀開始」事件中實現。

2. 正如 @oz01 所說的,一般遊戲邏輯的處理順序是:

處理用戶輸入

用戶輸入反饋到遊戲場景中的對象

根據用戶輸入計算場景對象的物理反饋(作為一個渲染引擎,OGRE沒有涉及這一步,需要開發者自己實現)

更新場景對象的空間變換

將場景對象所綁定的可渲染對象加入到渲染隊列,這個過程涉及對可渲染對象的裁剪、分類以及排序

此外,遊戲的其他邏輯(如戰鬥邏輯更新、伺服器消息處理等)根據需求的不同,可能分布在這個流程的每一個步驟中。

3. 遊戲邏輯處理完畢後,就是渲染的過程了,簡單來說就是遍歷渲染隊列,設置渲染狀態,提交幾何圖元

如果小圖看不清楚,可以直接點擊下面的鏈接查看大圖:

http://images2015.cnblogs.com/blog/628546/201601/628546-20160128205241223-522174856.png


幾種方式

如果渲染和邏輯在一個線程中

1.計算本幀邏輯-渲染本幀

最差的方式 GPU必須等待CPU執行完後才能開始工作

2.渲染上一幀-計算本幀

會有「差一幀」現象

並且都無法解決一個問題 就是渲染和邏輯一一對應並非必要 邏輯幀率和渲染幀率互相綁架了

並且以上方式如果出現渲染壓力過大 超過一幀時間 就會拖慢遊戲邏輯 當然你可以在下一幀多次執行邏輯 來追趕 不過因為每幀總有一次渲染 所以這麼做更可能的情況是 邏輯幀拖欠的債反而越來越高 最終導致遊戲永遠卡死

所以解決辦法是渲染邏輯在兩個獨立的線程中

渲染全力執行

每次都只是採樣場景最新的邏輯狀態而已 並且對於動畫的插值計算 並不需要每個邏輯幀都計算 而是需要渲染的時候才需要計算 邏輯只需要更新時間就可以了

這種情況下就不會出現因為渲染壓力大導致遊戲卡死了 最多渲染跳幀 邏輯依舊正常 不會拖慢 更不會卡死 除非你邏輯慢到無法在一幀完成 那麼就沒救了 只能拖慢了

並且邏輯和渲染各自都能以任意頻率執行

不過這麼做也有難度

就是所有邏輯數據都要做雙緩衝 邏輯更新從讀緩衝讀數據 更新到寫緩衝 處理完交換讀寫緩衝

渲染每次都讀最新的讀緩衝數據


先上代碼

void Render()
{

while (1)
{
rendered++;
WaitForSingleObject(renderLock, -1);
renderMutex.lock();

MDL_spriteRender(gs);

renderMutex.unlock();

if (!WaitForSingleObject(rExitLock, 0))
break;

MDL_RenderPresent();
MDL_clear();

}
ReleaseSemaphore(mExitLock, 1, 0);
}

void renderInit()
{

timeBeginPeriod(1);

renderLock = CreateSemaphore(0, 0, 1, "RenderLock");

gs = new mdl_sprite;
ziji = new player;
gs-&>RenList.reserve(16384);
id = new danmu("DT\bullet1.png");
spriteLst.push_back(ziji);
spriteLst.push_back(id);

std::thread rl(Render);
rl.detach();
gt = new globalttime;
}

void Process(HANDLE frameUpdateNotify)
{

while (1)
{
WaitForSingleObject(frameUpdateNotify, -1);
frameCount++;

gt-&>update();

for (int i = 0; i &< spriteLst.size(); i++) { spriteLst[i]-&>update();
}

if (id-&>waitForIdChange)
{
delete id;
id = new danmu("DT\bullet1.png");
delete gt;
gt = new globalttime;
spriteLst.clear();
spriteLst.push_back(id);
continue;
}
if (renderMutex.try_lock())
{
for (int i = 0; i &< spriteLst.size(); i++) { spriteLst[i]-&>draw();
}
renderMutex.unlock();
ReleaseSemaphore(renderLock, 1, 0);
}

char danmuNum[88] = { 0 };
sprintf_s(danmuNum, "skipped:%u num:%u",frameCount-rendered, id-&>size());
//顯示跳幀數目與渲染的精靈圖數量
SetWindowTextA(g_hWnd, danmuNum);

}
}

void doRenderLoop()
{
renderInit();
HANDLE pplock= CreateSemaphore(0, 0, 1, "PPLock");

std::thread p(Process, pplock);
p.detach();

while (1)
{
Sleep(16);
ReleaseSemaphore(pplock, 1, 0);

MKreflect = 0;
}
}

上面是我半年前寫的遊戲(引擎)的渲染和邏輯更新部分的代碼,

實現下面這效果

http://7xjr4i.com1.z0.glb.clouddn.com/GIF.gif

大概就是實現了所謂的跳幀

然後我學習引擎就卡住了。。

這半年是什麼也沒幹_(:3」∠)_

學習CppCoreGuidelines中,上面的指針換成unique_ptr大概會方便很多。

下一步準備去讀別人開源引擎的代碼了

看到有大牛關注了問題,我先把我的代碼放出來,求指點

大概我以後能寫出一個能看的引擎吧。。


知乎總是把事情搞複雜。人生苦短,盡量KISS。

隨便用幾個開源遊戲引擎做例子,由於函數嵌套等原因,實際情況要複雜些。但可以看到大概。

Gameplay的(見frame函數):

https://github.com/gameplay3d/GamePlay/blob/master/gameplay/src/Game.cpp

Cocos2dx的(見run函數,Windows 平台實現,其他平台同理):

https://github.com/cocos2d/cocos2d-x/blob/v3/cocos/platform/win32/CCApplication-win32.cpp

Urho3d的(見run函數):

https://github.com/urho3d/Urho3D/blob/master/Source/Urho3D/Engine/Application.cpp

商業引擎只是系統更多,實現更複雜,原理是一樣一樣的。要了解一個遊戲怎麼主循環,最直接的辦法是找到main函數,用調試器跳著跑一遍代碼,就明白了。當然前提是知識方面得有所積累,不然看到代碼也不知道是什麼,做什麼用的,這時候看書就有幫助了。學習和實踐的結果就是培養知識和技能,祝君早日成才,煉成頸椎病。

手機打字,我的脖子啊……


Game Loop · Sequencing Patterns · Game Programming Patterns


推薦閱讀:

國內/外的編程圈子有什麼不一樣?
C語言 C++?
為什麼很多大牛在寫題的時候要加一堆宏?
在 Windows 下鍵入 Enter 鍵,是在鍵盤緩衝區中存入
還是
兩個?

為什麼用C/C++編寫的程序只能用鍵盤輸入,而且輸出結果也只能在一個黑屏上顯示是一些字元?

TAG:遊戲開發 | 遊戲引擎 | CC | DirectX | Direct3D |