Entitas實現簡析
這裡主要講Entitas的執行原理,不講Entitas的程式碼生成方面。
ECS簡介
ECS(實體-元件-系統)是一種常用於遊戲開發的架構模式。
實體: 實體只是一個ID或一個容器,用來標記或儲存一系列元件。
元件: 沒有任何邏輯,單純用來儲存資料。
系統: 迴圈處理特定的元件。
ECS主要強調了兩個方面:
1.用資料的組合去描述物件,而不是繼承。
2.資料和邏輯的分離。
Unity中的EC
Unity採用了EC的設計思路,和傳統ECS不同,Unity的Component除了儲存資料,還保留了操作Component中資料的方法。
Unity中的Entiy就是GameObject,Component就是GameObject上掛載的元件各種元件,如Transform。
GameObject是各種Component的容器,本身並沒有實際意義(與ECS中Entity的定義略有不同,GameObject包含了tag、name、activeSelf等屬性。如果是在純粹的ECS系統中,tag等屬性應該作為Component掛載在GameObject)。
比如場景中的一個Cube,由Transform、MeshFilter、MeshRenderer、BoxCollider四個元件組成。我們能在場景中看到這個Cube是因為Unity從MeshFilter得到了Mesh資訊,告訴了GPU這是一個立方體,從MeshRenderer中的到了渲染這個Mesh的資訊,告訴GPU這個Mesh上的UV對應的是哪張貼圖的座標,渲染成什麼顏色等資訊。從Transform中得知了該將這個Cube渲染在哪個位置,旋轉多少度等。Unity通過BoxCollider和Transform資訊去做碰撞檢測。(在Cube的渲染這個例子中,可以把Unity自身看作ECS中的System的集合,因為Unity中的各個模組獲取了這個Cube中特定Component中的資訊,根據這些資訊做一些事情)
Unity中的EC與傳統ECS最大的兩個區別就是:
1.Entity上帶著一些屬性資料name、tag等,Component不僅有資料,還整合了大量的方法。比如在Unity中希望旋轉一個Transform會直接呼叫Transform的Rotate方法,而在傳統ECS中,很可能是在Cube這個Entity上掛載一個Rotate元件,然後由專門的RotateSystem去處理這個轉動。
2.沒有System對Component進行統一的處理。
Entitas簡介
Entitas是一個用C#實現的ECS框架,提供了方便的程式碼生成功能。
用法的介紹官方專案寫的比較詳細,這裡就不多做介紹。
Entitas執行流程
也就是說整個ECS系統的內部資料維護(Group、Collector、EntityIndex)複雜度主要放在Entity的修改上了。
在給一個Entity新增一個Component時,不僅僅是對Entity進行了修改,還會通過事件將這個新增傳遞給Context,Context遍歷所有Group,找到滿足這次修改條件的Group,對所有受到影響的Group進行修改。然後再通過Group將這次修改事件分發到Collector或其他監聽該Group的模組中去。
這種方式帶來的好處十分明顯,那就是獲取一種型別的Entity(也就是一個Group),只有第一次會遍歷所有的Entity生成這個Group,之後再獲取該型別Entity的複雜度就只有O(1)。
但是也有一定的隱患,當Group和Collector比較少時,這不是一個高消耗操作,但是Group、Collector很多,且在每一幀對Entity進行頻繁修改的時候。這可能會成為一個高消耗操作。
Tips
1.在銷燬一個Entity時,會移除Entity身上所有的Component,然後再進行回收。在移除Component時可能會通過Group把這個移除事件傳送到監聽Remove行為的Collector中,Collector會持有這個被銷燬的Entity。所以在filter、或execute時不能直接依賴Collector的收集條件,還需要對Entity的Component做獨立的判斷。
其實任何時候filter都需要對Entity的Component做判斷,因為Collector收集的Entity很可能在其他地方被改變。
2.Entity不應該被ECS系統外的模組持有,因為系統外對Entity的持有不會被自動引用計數(可以自己新增)。可能會導致一個Entity被銷燬然後又從池子中重新取出來, 外部模組對這個Entity的引用沒有改變,但已經可能不是自己持有的那個Entity了。
需要避免在外界持有Entity或通過持有uuid間接從context中持有這個Entity。
3.在replaceComponent時,傳送了Remove、Add、Update三個事件,而不是隻傳送了Update事件。
4.在程式碼生成時,對單Componet的Matcher進行了快取,如遊戲中常用的Postion和Name等Component,但是對組合Component的Matcher沒有進行快取。所在在兩個不同的ReactiveSystem中使用Matcher相同的Collector時,如:
//1,2代表Postion和Name的Index
//在使用程式碼生成時會生成類似Matcher.Position、Matcher.Name的靜態函式,方便開發者使用
context.CreateCollector(Matcher.AllOf(1,2));
這樣會生成兩個Matcher相同的Group例項。
如果在意這一點的話可以自己對Matcher進行快取。
寫在後面
在對ECS架構模式的理解和Entitas的使用上我還是一個新手,只是剛剛開始使用,如果有什麼寫的不對的地方,各位大佬可以留言指正。