使用DOTS制作一款第三人称僵尸射击游戏
我們正在使用面向數據技術棧DOTS重構Unity的核心基礎。許多游戲工作室在使用C# Job System、實體組件系統ECS和Burst Compiler后,都無一例外地感受到明顯的性能提升,其中就包含了瑞典游戲工作室Far North Entertainment。
在Unite Copenhagen大會上,我們與Far North Entertainment工作室的成員進行深入交流,了解他們如何在傳統的Unity項目中應用DOTS功能。
?
Far North Entertainment
瑞典的游戲工作室Far North Entertainment是由5位來自工程研究專業的好友共同創建。自2018年初在Gear VR平臺發布《Down to Dungeon》游戲之后,該公司一直致力開發一款末日僵尸生存游戲。
這款末日僵尸生存游戲的獨特之處在于僵尸的數量,開發團隊希望實現成千上萬個饑渴的僵尸追逐玩家的效果。然而在構建原型時,他們遇到了許多性能方面的問題。
開發中主要的瓶頸在于對龐大數量僵尸的進行生成、銷毀、更新和添加動畫,雖然開發團隊嘗試了對象池和動畫實例化等方法,但效果仍不顯著。因此,技術總監Anders Eriksson將目光投向DOTS,從面向對象(Object-oriented)設計轉為面向數據(Data-oriented)設計。
Anders Eriksson表示:促成我們思維模式發生改變的關鍵是停止考慮對象和對象層級,轉為思考數據是如何變換和訪問的。這意味著代碼不必圍繞具體事物來編寫,不用處理過去最常見的情況。
對于同樣在試著轉換思維模式的開發者,Anders Eriksson的建議是:先弄清楚要解決的問題和解決方案的相關數據。是否會對相同數據集執行相同的處理過程?可以把多少關聯數據打包到CPU緩存行中?如果想轉換現有代碼的話,那么要確定會給緩存行加入的垃圾數據量。能否將運算過程分配到多個線程上,能否利用SIMD指令?
在進一步學習后,開發團隊了解到Unity組件系統的實體只是組件流中的查找ID。組件只是數據,而系統包含了所有邏輯,系統會使用稱為“Archetypes(原型)”的特別組件標識來過濾實體。
Anders Eriksson表示:我們將ECS看作SQL數據庫可以幫助我們更好地理解它。每個Archetype原型是一張表格,每行代表一個組件,每列代表一個獨特的實體。我們可以使用系統查詢這些Archetype原型表,在實體上執行操作。
開始使用DOTS
為了更好地理解,Anders Eriksson研究了實體組件系統的文檔和ECS示例項目,以及Unity與Nordeus合作制作的示例項目。
此外,關于面向數據設計的學習材料也對團隊有很大的幫助。CppCon 2014大會上Mike Acton關于面向數據設計的演講開闊了他們的眼界,讓開發團隊了解了這種編程方式。
?
Far North Entertainment的開發團隊在博客上發表了許多學習心得,今年9月,他們在Unite Copenhagen大會上進行演講,介紹了轉換到面向數據思維的經驗。
本文的內容將以這次演講作為基礎,并且詳細地講解該團隊應用ECS、C# Job System和Burst Compiler的具體方法。
排列僵尸數據
Anders Eriksson表示:我們面臨的主要問題是客戶端的轉換信息插入,以及對上千個實體的轉向信息。
開發團隊最初使用面向對象的方法,編寫了ZombieView腳本的抽象,它繼承了更為常用的EntityView父類。EntityView是附加在游戲對象的MonoBehaviour,它會用作游戲模型的可視化展示。每個ZombieView腳本會在Update函數中處理相應的信息轉換和朝向信息插入。
這種方法似乎挺不錯,但問題是每個實體會在內存中占用隨機的位置。這意味著,在訪問數千個實體時,CPU需要從內存中逐個獲取實體數據,這個過程非常耗時。
如果將數據存在整齊的連續內存塊中,CPU則可以同時緩存所有實體數據。現今大多數CPU在每個運行周期中可以從緩存獲取128比特或256比特的數據量。
開發團隊決定改用DOTS系統生成敵人,希望借此解決客戶端的性能瓶頸問題。首先,要轉換的是ZombieView腳本中的Update函數,團隊確定了哪些代碼要劃分到不同的系統中,以及哪些是必要的數據。
游戲世界是一個2D網格,最首要的是對位置和朝向進行插值處理。僵尸的前進方向由兩個浮點值表示,最后的組件是一個目標位置組件,它會跟蹤敵人的服務器位置。
?
然后是為敵人創建Archetype原型。Archetype原型是一組屬于特定實體的組件集,也可以說是一個組件標識。在項目中,由于敵人需要使用更多的組件,而且部分組件需要游戲對象的引用,因此開發團隊使用了預制件來定義Archetype原型。
他們的方法是:在ComponentDataProxy中包裝組件數據,ComponentDataProxy會把數據轉化為可附加到預制件的MonoBehaviour。當調用EntityManager執行實例化操作,并傳入預制件時,系統會創建帶有預制件上所有組件數據的實體。所有組件數據都存儲在稱為“ArchetypeChunks(原型數據塊)”的16kb大小的數據塊中。
下圖展示了原型數據塊中的組件數據流的組織方式。
?
Anders Eriksson解釋說:原型數據塊的一個主要優點是,系統不必在創建新實體時處理新的堆分配,因為內存已預先分配。因此在創建實體時,系統會直接在原型數據塊的組件流末尾處寫入數據。
只有當創建的實體數據不符合數據塊類型時,系統才會需要執行額外的堆分配。在這種情況下,系統會創建新的16kb原型數據塊來進行分配,如果有相同類型的空原型數據塊,則會將其重新利用。隨后,系統會將新實體的數據寫入到新數據塊的組件流中。
對僵尸進行多線程處理
現在數據被緊湊地打包,并在內存中以對緩存友好的方式布局好,開發團隊可以輕易利用C# Job System在多個CPU內核上并行運行代碼。
下一步是創建可以在所有包含PositionData2D、HeadingData2D和TargetPositonData組件的原型數據塊中過濾掉所有實體的系統。
為此,Anders Eriksson及其團隊編寫了JobComponentSystem腳本,在OnCreate函數上構建查詢功能。
代碼如下所示:
?
這些代碼會執行一次查詢,過濾掉所有包含位置、朝向和目標的實體。然后,開發團隊通過C# Job System在每幀上調度任務,將運算過程分配到多個工作線程上。
Andres Eriksson表示:C# Job System的優點在于,C# Job System也在Unity的源碼中使用,因此我們不必擔心出現多個線程在執行過程中同時占用相同CPU內核,游戲賬號交易產生互相阻礙各自執行的性能問題。
由于成千上萬的敵人意味著在運行時會有大量的原型數據塊要匹配查詢過程,所以開發團隊選擇使用IJobChunk,它可以在不同的工作線程上正確地分配各個數據塊。
在每幀上,名稱為“UpdatePositionAndHeadingJob”的新作業會處理游戲中敵人的位置和轉向插值。
調度作業的代碼如下所示:
作業的聲明如下:
?
當一個工作線程從隊列中抽調一個作業時,它會調用該作業的執行核心。
下面是執行核心的代碼:
?
Anders Eriksson指出:你可能注意到我們使用了Select函數而不是Branch函數,這樣做的原因是避免所謂的分支誤預測。
Select函數會在兩種表達式中選擇匹配當前條件的一種,如果表達式并不需要很多的運算量,我建議使用Select,因為它更輕便,不必等待CPU從分支誤預測問題中恢復過來。
使用Burst Compiler提升性能
對于敵人位置和朝向的插值,完成DOTS轉換的最后一步是啟用Burst Compiler。
由于已經在連續數組中排列好數據,又使用了Unity的全新Mathematics庫,因此只需給作業添加上BurstCompile屬性便可啟用該功能。
?
Burst Compiler可以提供單指令多數據流(SIMD),機器指令可以對多個輸入數據集進行操作,通過一個指令產生多個輸出數據集。這樣就可以在128比特大小的緩存中加入更多正確的數據。
通過結合Burst Compiler、易于緩存的數據布局和C# Job System,開發團隊取得了很大的速度提升效果。
下面是性能對比圖表展示了在每個轉換步驟后速度的變化。
?
結果顯示:對于客戶端上僵尸位置和朝向的插值過程上,開發團隊完全擺脫了此前遇到的瓶頸。數據的排布方式會更便于緩存,而且緩存行上只有相關的數據。所有的CPU內核都能夠投入工作,而Burst Compiler的輸出數據都是帶有SIMD指令的高度優化機器代碼。
DOTS使用技巧
下面分享Far North Entertainment開發團隊對DOTS的一些使用技巧:
使用數據流的模式進行思考,因為在ECS中,實體只是用于并行組件數據流的查詢索引。
將ECS看作關系型數據庫,Archetype原型是表格,組件是行,而實體是表格內的索引(列)。
將數據組織到連續的數組中,從而利用好CPU緩存和硬件預取器。
不再以創建對象層級作為第一件事,在弄清楚真正要解決的問題前,制定通用的解決方案。
要考慮垃圾回收過程。對于性能資源緊張的位置,要避免進行過多的堆分配,并利用好Unity的Native容器。但要注意的是,此時需要手動進行清理過程。
了解抽象部分的開銷,注意虛擬函數的調用開銷。
通過使用C# Job System,利用好所有的CPU內核。
熟悉面向的硬件。Burst Compiler是否生成了SIMD指令?此時要使用Burst Inspector進行分析。
避免浪費緩存行。在將數據打包為UDP數據包時,要考慮如何將數據打包存到緩存行上。
針對已經在制作階段的項目,Anders Eriksson的建議是:找出游戲中出現性能問題的具體位置,看看能否在這些位置使用DOTS。開發者沒必要轉換整個代碼庫。
結語
Anders Eriksson總結說:Unite大會上發布的DOTS動畫功能、Unity Physics和Live Link讓我們感到非常興奮,我們會在游戲中更多地利用DOTS功能,希望可以將更多的游戲對象轉換為ECS實體,而且Unity看起來在這個目標上取得了很好的進展。
總結
以上是生活随笔為你收集整理的使用DOTS制作一款第三人称僵尸射击游戏的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 真实感皮肤渲染技术总结
- 下一篇: 用数学方法分析哪类游戏中的AI难度最大