您好,登錄后才能下訂單哦!
小編給大家分享一下Unity中DOTS要實現的特點有哪些,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
DOTS 要實現的特點有:
性能的準確性。我們希望的效果是:如果循環因為某些原因無法向量化(Vectorization,可以參考 StackOverflow 的問題),它應該會出現編譯器錯誤,而不是使代碼運行速度慢8倍,并得到正確結果,完全不報錯。
跨平臺架構特性。我們編寫的輸入代碼無論是面向 iOS 系統還是 Xbox,都應該是相同的。
我們應該有不錯的迭代循環。在修改代碼時,可以輕松查看為所有架構生成的機器代碼。機器代碼“查看器”應該很好地說明或解釋所有機器指令的行為。
安全性。大多數游戲開發者不把安全性放在很高的優先級,但我們認為,解決Unity出現內存損壞問題是關鍵特性之一。在運行代碼時應該有一個特別模式,如果讀取或寫入到內存界限外或取消引用Null時,它能夠提供我們明確的錯誤信息。
當我們使用 C# 語言時,仍然無法控制數據在內存中如何進行分布,但這是我們提升性能的關鍵點。
除此之外,標準庫面向的是“堆上的對象”和“具有其它對象指針引用的對象”。
也就是意味著,當處理性能敏感代碼時,我們可以放棄使用大部分標準庫,例如:Linq、StringFormatter、List、Dictionary。禁止內存分配,即不使用類,只使用結構、映射、垃圾回收器和虛擬調用,并添加可使用的部分新容器,例如:NativeArray 和其他集合類型。
我們可以在越界訪問時得到錯誤和錯誤信息,以及使用 C++ 代碼時的調試器支持和編譯速度。我們通常把該子集稱為高性能 C# 或 HPC#。
它可以被總結為:
大部分的原始類型(float、int、uint、short、bool…),enums,structs 和其他類型的指針
集合:用 NavtiveArray<T>
代替 T[]
所有的控制流語句(除了 try、finally、foreach、using)
對 throw new XXXException(...)
給予基礎支持
Unity 構建了名為 Burst 的代碼生成器和編譯器。
Burst 有時運行速度比 C++ 快,有時也會比 C++ 慢。后面的情況源于性能問題,Unity 已經開始著手解決。
當使用 C# 時,我們對整個流程有完整的控制,包括從源代碼編譯到機器代碼生成,如果有我們不想要的部分,我們會找到并修復它。我們會逐漸把 C++ 語言的性能敏感代碼移植為 HPC# 代碼,這樣會更容易得到想要的性能,更難出現 Bug,更容易進行處理。
如果 Asset Store 資源插件的開發者在資源中使用 HPC# 代碼,資源插件在運行時代碼會運行得更快。除此之外,高級用戶也會通過使用 HPC# 編寫出自定義高性能代碼而受益。
ECS Track: Deep Dive into the Burst Compiler - Unite LA
Burst 對于 HPC# 更詳細的支持可以在下面找到:
Burst User Guide
C++ 和 C# 都無法為開發者編寫線程安全代碼提供太多幫助。即使在今天,擁有多個核心游戲消費級硬件發展至今已經過去了十年,但依舊很難有效處理使用多個核心的程序。
數據沖突,不確定性和死鎖是使多線程代碼難以編寫的挑戰。Unity 想要的特性是“確保代碼調用的函數和所有內容不會在全局狀態下讀取或寫入”。Unity 希望應該讓編譯器拋出錯誤來提醒,而不是屬于“程序員應遵守的準則”,Burst 則會提供編譯器錯誤。
Unity 鼓勵 Unity 用戶編寫 “Jobified” 代碼:將所有需要發生的數據轉換劃分為 Job。
每個 Job 都具有“功能性”,因為 Job 沒有副作用。 Job 會明確指定使用的只讀緩沖區和讀寫緩沖區,嘗試訪問其它數據會出現編譯器錯誤。
Job 調度程序會確保在 Job 運行時,任何程序都不會寫入只讀緩沖區。我們會確保在 Job 運行時,任何程序都不會讀取讀寫緩沖區。
如果調度的 Job 違反了這些規則,我們會得到運行時錯誤。這種錯誤不僅在競態條件下得到,錯誤信息會說明,你正在嘗試調度的 Job 想要讀取緩沖區 A,但你之前已經調度了會寫入緩沖區 A 的 Job ,所以如果想要執行該操作,需要把之前的 Job 指定為依賴。
由于能夠處理所有組件,我們可以使這些組件了解各自的存在。例如:向量化(Vectorization)無法進行的常見情況是,編譯器無法確保二個指針不指向相同的內存,即混淆情況(Alias)。
而兩個集合庫中的 NativeArray 從不會混淆,我們可以在 Burst 中運用這個知識,使它不會由于害怕兩個數組指針指向相同內存而放棄優化(Alias)。Alias 的問題在 Unity GDC 中也有一個演講提到過:Unity at GDC - C# to Machine Code
Unity 還編寫了 Unity.Mathemetics 數學庫,提供了很多像 Shader 代碼的數據結構。Burst 也能和這數學庫很好的工作,未來 Burst 將能夠為 math.sin()
等計算作出犧牲精度的優化。
對于 Burst 而言,math.sin()
不僅是要編譯的 C# 方法,Burst 還能理解出 sin()
的三角函數屬性,同時知道 x 值較小時會出現 sin(x)
等于 x 的情況,并了解它能替換為泰勒級數展開,以便犧牲特定精度。
跨平臺和架構的浮點準確性是 Burst 未來的目標。
Unity 一直以組件的概念為中心,例如:我們可以添加 Rigidbody 組件到游戲對象上,使對象能夠向下掉落。我們也可以添加 Light 組件到游戲對象上,使它可以發射光線。我們添加 AudioEmitter 組件,可以使游戲對象發出聲音。
我們實現組件系統的方法并沒有很好地演變。我們過去使用面向對象的思維編寫組件系統。組件和游戲對象都是“大量使用 C++ 代碼”的對象,創建或銷毀它們需要使用互斥鎖修改“id 到對象指針”的全局列表。
通過使用面向數據的思維方式,我們可以更好地處理這種情況。我們可以保留用戶眼中的優良特性,即只需添加組件就可以實現功能,而同時通過新組件系統取得出色的性能和并行效果。
這個全新的組件系統就是實體組件系統 ECS。簡單來說,如今我們對游戲對象進行的操作可用于處理新系統的實體,組件仍稱作組件。那么區別是什么?區別在于數據布局。
class Orbit : MonoBehaviour{ public Transform _objectToOrbitAround; void Update() { var currentPos = GetComponent<Transform>().position; var targetPos = _objectToOrbitAround.position; GetComponent<RigidBody>().velocity += SomehowSteerTowards(currentPos, targetPos) }}
這種模式會被反復使用:組件必須找到相同游戲對象上的一個或多個組件,然后給它讀取或寫入數值。
這種方法存在很多問題:
Update()
方法被一個單獨的 Orbit 組件調用,下次調用 Update()
的會是完全不同的組件,它很可能造成代碼從緩存移出。
Update()
必須使用 GetComponent()
來找到 Rigidbody 組件。該組件也可以被緩存起來,但必須保證 Rigidbody 組件不被銷毀。
我們要處理的其它組件位于內存中完全不同的位置。
ECS 使用的數據布局會把這些情況看作一種非常常見的模式,并優化內存布局,使類似操作更加快捷。
離散的數據導致搜索效率十分低下,還有 Cache Miss 的問題,這個問題可以參考:ECS的泛泛之談
ECS 會在內存中對帶有相同組件(Component)集的所有實體(Entity)進行組合。ECS 把這類組件集稱為原型(Archetype)。
下圖的原型就是由 Position 組件、Velocity 組件、Rigidbody 組件和 Renderer 組件組成的。
如果一個實體只有三個組件(不同于前面提到的原型),那么那三個組件就組成了一個新的原型。
下面的圖來自 Unite LA 的一次演講的講義, 很遺憾那次演講沒有錄制下來。講義可以在這里找到。
ECS 以 16k 大小的塊(Chunk)來分配內存,每個塊僅包含單個原型中所有實體的組件數據。
一個帖子中有人提供了更加形象的內存布局圖,例如上半部分的原型由 Position 組件和 Rock 組件組成,其中整個原型占了一個塊(Chunk),兩個組件的數據分別存在兩個數組中,里面還帶著組件數據對應的實體的信息。
每個原型都有一個 Chunks 塊列表,用來保存原型的實體。我們會循環所有塊,并在每個塊中,對緊湊的內存進行線性循環處理,以讀取或寫入組件數據。該線性循環會對每個實體運行相同的代碼,同時為 Burst 創造向量化(Vectorization)處理的機會。
每個塊會被安排好內存中的位置,以便于快速從內存得到想要的數據,詳情可以參考下面的文章。
Unity2018 ECS框架Entities源碼解析(二)組件與Chunk的內存布局 - 大鵬的專欄 - CSDN博客
實體是什么?實體只是一個 32 位的整數 key (和一些額外的數據例如 index 和 version 實體版本,不過在這里不重要),所以除了實體的組件數據外,不必為實體保存或分配太多內存。實體可以實現游戲對象的所有功能,甚至更多功能,因為實體非常輕量。
實體的性能消耗很低,所以我們可以把實體用在不適合游戲對象的情況,例如:為粒子系統內的每個單獨粒子使用一個實體。
實體本身不是對象,也不是一個容器,它的作用是把其組件的數據關聯到一起。
我們不必使用用戶的 Update 方法搜索組件,然后在運行時對每個實例進行操作,使用 ECS 時我們只需靜態地聲明:我想對同時附帶 Velocity 組件和 Rigidbody 組件的所有實體進行操作。為了找到所有實體,我們只需找到所有符合特定“組件搜索查詢”的原型即可,而這個過程就是由系統(System)來完成的。
很多情況下,這個過程會分成多個 Job ,使處理ECS組件的代碼達到幾乎100%的核心利用率。ECS 會完成所有工作,我們只需要提供對每個實體運行的代碼即可。我們也可以手動處理塊迭代過程(IJobChunk)。
當我們從實體添加或移除組件時,ECS會切換原型。我們會把它從當前塊移動到新原型的塊,然后交換之前塊的最后實體來“填補空缺”。
在 ECS 中,我們還要靜態聲明要對組件數據進行什么處理,是 ReadOnly 只讀還是 ReadWrite 讀寫。通過確定僅對 Position 組件進行讀取,ECS 可以更高效地調度 Job ,其它需要讀取 Position 組件的 Job 不必進行等待。
這種數據布局也處理了 Unity 長期以來的困擾,即:加載時間和序列化的性能。為大型場景加載或流式處理 ECS 數據時,主要的操作是從硬盤加載和使用原始字節。
對 ECS 的常見觀點是:ECS 需要編寫很多代碼。因此,實現想要的功能需要處理很多樣板代碼。現在針對移除多數樣板代碼需求的大量改進即將推出,這些改進會使開發者更簡單地表達自己的目的。
我們暫時沒有實現太多這類改進,因為我們現在正專注于處理基礎性能,但我們知道:太多樣板代碼對 ECS 游戲代碼沒有好處,我們不能讓編寫 ECS 代碼比編寫 MonoBehaviour 更麻煩。
Project Tiny 已經實現了部分改進,例如:基于 lambda 函數的迭代 API。
以上是“Unity中DOTS要實現的特點有哪些”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。