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的使用上我還是一個新手,只是剛剛開始使用,如果有什么寫的不對的地方,各位大佬可以留言指正。