最近為了后續(xù)游戲開發(fā)工作涝滴,自己實(shí)現(xiàn)了一套ECS架構(gòu)涮瞻。
雖然前些年的開發(fā)經(jīng)歷鲤拿,以及剛接觸ECS時(shí)的思路整理,已經(jīng)讓我對(duì)他有了明確的認(rèn)知署咽,但是在具體實(shí)現(xiàn)層面近顷,終究還是有很多需要考慮的地方。尤其考慮到當(dāng)一個(gè)架構(gòu)需要面臨工業(yè)級(jí)開發(fā)所帶來(lái)的壓力的時(shí)候宁否,如何確保ECS概念的完整以及滿足工業(yè)開發(fā)中的各種壓力窒升,成為了真正設(shè)計(jì)實(shí)現(xiàn)ECS架構(gòu)時(shí)的主要矛盾。
結(jié)合這些年的開發(fā)經(jīng)歷慕匠,以及我對(duì)ECS概念的認(rèn)知和極致效率的追求饱须,我設(shè)計(jì)實(shí)現(xiàn)了最新一般的ECS架構(gòu),現(xiàn)分享開發(fā)時(shí)的思路歷程台谊。
PS:本ECS架構(gòu)依托于Unity蓉媳,使用C#實(shí)現(xiàn),文中實(shí)現(xiàn)均在此基礎(chǔ)上進(jìn)行锅铅。
ECS的核心實(shí)現(xiàn)
ECS的核心實(shí)現(xiàn)自然就是實(shí)體(Entity)酪呻、組件(Component)、系統(tǒng)(System)的主體實(shí)現(xiàn)盐须。
ECS核心說明
實(shí)體 在ECS核心思路中玩荠,僅承載標(biāo)識(shí)作用,簡(jiǎn)單點(diǎn)說實(shí)體甚至就只是一個(gè)id,所以實(shí)現(xiàn)實(shí)體較為簡(jiǎn)單阶冈,完全可以在架構(gòu)中定義為一個(gè)不可派生類型屉凯。
組件 作為具體業(yè)務(wù)開發(fā)時(shí)實(shí)際存儲(chǔ)數(shù)據(jù)的類型,因組件的不同而有不同的實(shí)現(xiàn)眼溶,故而組件需要設(shè)計(jì)為抽象類以供ECS進(jìn)行管理悠砚,最佳實(shí)現(xiàn)便是ECS核心不需了解組件的具體實(shí)現(xiàn)便可完成其功能。
系統(tǒng) 與組件類似堂飞,也是僅在具體開發(fā)時(shí)才會(huì)擁有其具體邏輯灌旧,所以系統(tǒng)也需設(shè)計(jì)為抽象類,但與 組件 所不同之處在于 系統(tǒng) 僅有方法绰筛,組件 僅有數(shù)據(jù)枢泰。所以還需在某一處對(duì) 系統(tǒng) 進(jìn)行尋輪執(zhí)行系統(tǒng)自身的心跳函數(shù)。
因此铝噩,ECS的核心實(shí)現(xiàn)大致有如下幾部分:
- 實(shí)體管理器衡蚂,用于存儲(chǔ) 實(shí)體 數(shù)組并進(jìn)行添加刪除以及賦值操作。
- 組件管理器骏庸,管理不同的組件類型毛甲,且不同組件類型內(nèi)部通過id將具體 組件 與 實(shí)體 相連。
- 系統(tǒng)管理器具被,用于管理所有 系統(tǒng) 玻募,按序循環(huán)執(zhí)行其心跳。
ECS核心實(shí)現(xiàn)說明
Entity
實(shí)體 作為標(biāo)識(shí)一姿,最核心數(shù)據(jù)便是其唯一id漫玄。此外孝鹊,為了在ECS中可以立即獲取某實(shí)體所擁有所有數(shù)據(jù)類型,需要有專門的組件掩碼 (可以用哈希表抬虽,但下文會(huì)提供我的解決方案) 癌椿,用于存儲(chǔ)實(shí)體所擁有的的組件類型诬留,并提供相關(guān)的快速運(yùn)算尤筐。此外實(shí)體擁有 有效標(biāo)識(shí)符 以標(biāo)明實(shí)體的有效性矾端。
實(shí)體管理器 存儲(chǔ) 實(shí)體 時(shí),由于每個(gè) 實(shí)體 擁有自己唯一id蜂林,因此可以將其存儲(chǔ)在數(shù)組中遥诉,并利用數(shù)組下標(biāo)表示其唯一id。此外噪叙,由于存在 實(shí)體 的添加、刪除操作霉翔,因此刪除 實(shí)體 時(shí)并不將其從數(shù)組中移除睁蕾,而改為設(shè)置其 有效標(biāo)識(shí)符 ,這樣在每一幀中依舊可以正常遍歷這一幀初始時(shí)有效的 實(shí)體 ,而不必每一幀運(yùn)算中都要考慮 實(shí)體 的有效性而帶來(lái)的計(jì)算邏輯的不同子眶。
Component
組件 因自身內(nèi)容的不同瀑凝,所以需要一個(gè) 組件枚舉 對(duì)不同的組件類型進(jìn)行標(biāo)識(shí)。而在不同的枚舉類型下臭杰,有需要存在對(duì)應(yīng)的 組件 實(shí)體類型粤咪。所以對(duì) 組件 進(jìn)行抽象,提供出基礎(chǔ)的類型渴杆、生命周期函數(shù)等接口寥枝。
組件池 作為單一類型 組件 的集合,使用模板實(shí)現(xiàn)磁奖。在 組件池 中囊拜,以數(shù)組的形式對(duì) 組件 進(jìn)行存儲(chǔ),其在數(shù)組中的id便是該 組件 對(duì)應(yīng)的 實(shí)體 的唯一id比搭。除此之外冠跷,池中還實(shí)現(xiàn)組件實(shí)體的緩沖機(jī)制,避免實(shí)體的頻繁創(chuàng)建帶來(lái)的GC身诺。
組件管理器 內(nèi)部以 組件枚舉 為索引構(gòu)建數(shù)組蜜托,存儲(chǔ)所有的 組件池 。這樣通過id和類型霉赡,便可從 組建管理器 中獲取相應(yīng)的 組件 實(shí)體盗冷,同時(shí)實(shí)現(xiàn) 組件池 對(duì)ECS核心外部的隱藏,避免直接訪問可能帶來(lái)的不可預(yù)知的問題同廉。
System
系統(tǒng) 雖相互之間內(nèi)容也不同仪糖,但因?yàn)椴淮嬖诟叩膶蛹?jí)對(duì)其進(jìn)行調(diào)用,因此可以將 系統(tǒng) 單純抽象出心跳接口即可迫肖。這樣 系統(tǒng) 每幀只需要實(shí)現(xiàn)對(duì)其關(guān)注的 組件 組合體進(jìn)行遍歷并操作即可锅劝。
系統(tǒng)管理器 通過存儲(chǔ)所有 系統(tǒng) 實(shí)例,將其存儲(chǔ)至數(shù)組中蟆湖,便可通過對(duì)數(shù)組遍歷實(shí)現(xiàn)所有 系統(tǒng) 的心跳故爵。
初版ECS核心實(shí)現(xiàn)的問題
雖然已經(jīng)實(shí)現(xiàn)了第一版的ECS核心,但是有不少問題是可以第一時(shí)間進(jìn)行設(shè)計(jì)改進(jìn)的隅津。問題如下:
- 組件掩碼:作為系統(tǒng)篩選關(guān)注實(shí)體所用的標(biāo)識(shí)诬垂,每幀都可能存在的大量調(diào)用,哈希運(yùn)算效率不盡人意伦仍。
- 組件枚舉:組件因?yàn)榭蓴U(kuò)展的關(guān)系结窘,組件枚舉和組件的對(duì)應(yīng)關(guān)系以及組件池的創(chuàng)建均需要人為管理維護(hù),可以考慮自動(dòng)化管理充蓝。
- 系統(tǒng)順序:系統(tǒng)的先后關(guān)系可能帶來(lái)邏輯運(yùn)算的不一致隧枫,因而如何確保系統(tǒng)運(yùn)算的一致喉磁,需要考慮系統(tǒng)的排序機(jī)制。
修復(fù)初版ECS核心問題的實(shí)現(xiàn)
新的組件掩碼實(shí)現(xiàn)
組件掩碼 的實(shí)現(xiàn)原本是借助于每一個(gè)實(shí)體中安插一個(gè)哈希表官脓,但是哈希算法本身需要消耗一定的計(jì)算資源协怒。此外已知,我們的組件類型是由0開始遞增的卑笨,且總長(zhǎng)度可知孕暇。
由此,組件掩碼 可以設(shè)計(jì)采用位運(yùn)算的思路赤兴,使用封裝 n
個(gè) ulong
的結(jié)構(gòu)妖滔,內(nèi)部通過位運(yùn)算存儲(chǔ)組件數(shù)據(jù)。這樣搀缠,無(wú)論對(duì) 組件掩碼 進(jìn)行遍歷铛楣、增減、包含判斷艺普,都可以以原子操作的形式進(jìn)行簸州,相比哈希運(yùn)算可以對(duì)算力進(jìn)行大量節(jié)省。
組件枚舉維護(hù)自動(dòng)化
由于 組件枚舉 歧譬、 組件 岸浑、 組件管理器 相互之間的關(guān)聯(lián)性,使用人工管理維護(hù)必然存在紕漏瑰步,所以自動(dòng)化工具提上日程矢洲。
自動(dòng)化組件 工具核心負(fù)責(zé)一件事,便是檢測(cè) 組件枚舉 的變動(dòng)缩焦。一旦枚舉發(fā)生變動(dòng)读虏,那么 自動(dòng)化組件 工具就需要第一時(shí)間 生成新組件文件 、 組建管理器中新增新組件的組件池 袁滥、 判斷組件掩碼可提供組件長(zhǎng)度并進(jìn)行刷新 盖桥。
其中,組件掩碼 內(nèi)部可提供位運(yùn)算的數(shù)據(jù)長(zhǎng)度是固定的题翻,而組件類型數(shù)量本身是變動(dòng)的揩徊,因此也需要在 組件枚舉 發(fā)生變動(dòng)時(shí),由 自動(dòng)化組件 工具生成新的 組件掩碼 結(jié)構(gòu)內(nèi)部實(shí)現(xiàn)嵌赠。
固定系統(tǒng)順序
因?yàn)橄到y(tǒng)執(zhí)行的先后順序會(huì)影響邏輯計(jì)算的唯一性塑荒,因此只有當(dāng)系統(tǒng)排序固定后,才可以確保邏輯運(yùn)算的穩(wěn)定性姜挺。
所以針對(duì)這個(gè)問題齿税,我設(shè)計(jì) 系統(tǒng)屬性 用于協(xié)助系統(tǒng)排序。
系統(tǒng)屬性 當(dāng)中包含兩個(gè)字段:系統(tǒng)組id 初家、 系統(tǒng)組內(nèi)id 偎窘。排序時(shí)對(duì)系統(tǒng)獲取 系統(tǒng)屬性 乌助,之后先按照系統(tǒng)組id進(jìn)行排序溜在,當(dāng)系統(tǒng)同屬一組時(shí)陌知,再按照組內(nèi)id進(jìn)行排序。若兩者完全一致掖肋,則按照系統(tǒng)名進(jìn)行排序仆葡。
這樣的設(shè)計(jì),既可以滿足系統(tǒng)固定排序的需求志笼,同時(shí)作為開發(fā)者沿盅,也可以通過調(diào)整屬性數(shù)據(jù)實(shí)現(xiàn)控制系統(tǒng)的排序。
新的問題
在針對(duì)上一版的問題通過新的設(shè)計(jì)進(jìn)行解決后纫溃,新的問題就會(huì)接踵而至了腰涧。
現(xiàn)在的實(shí)現(xiàn),可以讓我們開始ECS模式的開發(fā)紊浩,但是依舊有性能問題需要我們持續(xù)關(guān)注:
- 系統(tǒng)每一幀的遍歷窖铡,雖然依托新版本的 組件掩碼 可以快速完成篩選運(yùn)算,但是頻繁的篩選依舊是性能熱點(diǎn)坊谁。
- 系統(tǒng)中獲取到 實(shí)體 后费彼,如何優(yōu)雅的獲取組件也是一個(gè)問題。現(xiàn)在的版本需要依據(jù)實(shí)體id再通過ECS核心對(duì) 組件 進(jìn)行獲取實(shí)在不算太優(yōu)雅口芍。
- 針對(duì)ECS實(shí)現(xiàn)同步策略箍铲,需要底層擁有對(duì)快照系統(tǒng)的支持。
ECS核心擴(kuò)展版實(shí)現(xiàn)
解決系統(tǒng)性能熱點(diǎn)的緩存機(jī)制
從幀說起
ECS中的一幀鬓椭,可以這么理解:
進(jìn)入一幀時(shí)颠猴,所有的數(shù)據(jù)是經(jīng)過了上一幀計(jì)算后,數(shù)據(jù)已經(jīng)產(chǎn)生變更的全新狀態(tài)小染。
在一幀中翘瓮,系統(tǒng)負(fù)責(zé)的是流式計(jì)算數(shù)據(jù)的變更,而系統(tǒng)與系統(tǒng)之間應(yīng)當(dāng)保持獨(dú)立氧映,不會(huì)因某一系統(tǒng)的執(zhí)行而導(dǎo)致另一系統(tǒng)的運(yùn)算出錯(cuò)(雖然這一點(diǎn)很難保證春畔,但實(shí)現(xiàn)這一點(diǎn)可以讓兩個(gè)人同時(shí)殺死對(duì)方的想法成為可能)。在保證了系統(tǒng)的相互獨(dú)立性之后岛都,我們便可以進(jìn)行面向數(shù)據(jù)的編程律姨,而不必理會(huì)對(duì)象方法執(zhí)行先后的問題。
在一幀運(yùn)算后臼疫,所有的數(shù)據(jù)便完成了一次完整的運(yùn)算择份,下文的快照模塊便可以執(zhí)行對(duì)數(shù)據(jù)進(jìn)行最終的記錄。
緩存引入
在描述了ECS的一幀概念后烫堤,緩存便可以被我們加入到ECS核心中了荣赶。
由于系統(tǒng)之間的獨(dú)立性以及幀與幀之間的數(shù)據(jù)獨(dú)立性凤价,所以可以引入緩存機(jī)制來(lái)解決系統(tǒng)中對(duì)實(shí)體篩選的性能熱點(diǎn)。
緩存 依據(jù)各系統(tǒng)所需的組件掩碼作為鍵拔创,對(duì)實(shí)體進(jìn)行緩存利诺。各系統(tǒng)運(yùn)行時(shí)由緩存中獲取需要的組件實(shí)體。每一幀當(dāng)中對(duì)實(shí)體進(jìn)行的增減操作或?qū)?shí)體組件的增減操作剩燥,由 緩存管理器 進(jìn)行記錄慢逾。在一幀運(yùn)算后 緩存管理器 則進(jìn)行緩存刷新。
緩存 的引入可帶來(lái)每幀效率的大幅改善灭红。而且由于系統(tǒng)之間的獨(dú)立侣滩,可以保證緩存不會(huì)因數(shù)據(jù)的非實(shí)時(shí)性而對(duì)系統(tǒng)的運(yùn)算造成破壞性影響。
由緩存引來(lái)的新功能
因?yàn)橐?緩存 变擒,實(shí)體 與 組件 有效性的運(yùn)算放在了幀尾君珠,這也就造成了數(shù)據(jù)的非實(shí)時(shí)性。
而在業(yè)務(wù)的開發(fā)中娇斑,難免會(huì)牽扯到一些運(yùn)算策添,需要在數(shù)據(jù)的有效性發(fā)生變更時(shí)進(jìn)行操作,所以緩存內(nèi)部記錄的變更數(shù)據(jù)對(duì)需要這些數(shù)據(jù)的系統(tǒng)而言至關(guān)重要又存在缺陷悠菜。重要在于這些有效性的變更數(shù)據(jù)在某些系統(tǒng)中會(huì)引起反饋舰攒;而缺陷在于有效性變更會(huì)隨機(jī)分布在系統(tǒng)中,如果關(guān)注這些數(shù)據(jù)的系統(tǒng)排序不合理悔醋,便會(huì)造成部分?jǐn)?shù)據(jù)的丟失摩窃,且在非實(shí)時(shí)性運(yùn)算中引入實(shí)時(shí)數(shù)據(jù),本身也會(huì)存在兼容問題芬骄。
基于此猾愿,又引入了ECS核心的 通知中心,專供記錄數(shù)據(jù)有效性的變更消息账阻。而且對(duì)于消息保留其擴(kuò)展性蒂秘,便于在工業(yè)開發(fā)中依據(jù)自身需求新增特定消息供系統(tǒng)獲取。
通知中心 收集消息淘太,并在下一幀提供給系統(tǒng)姻僧。系統(tǒng)通過 通知中心 獲取數(shù)據(jù)變動(dòng)消息,且消息都是上一幀的運(yùn)算結(jié)果蒲牧。以此保證系統(tǒng)和數(shù)據(jù)的兼容性撇贺。
優(yōu)雅獲取組件
優(yōu)雅地獲取 組件 ,其實(shí)可以理解為更簡(jiǎn)單去獲取 組件 冰抢。
現(xiàn)有機(jī)制下松嘶,由于系統(tǒng)已經(jīng)改為從 緩存 中獲取 實(shí)體 ,因此可以對(duì) 緩存 中的 實(shí)體 進(jìn)行封裝挎扰,使得遍歷 實(shí)體 時(shí)返回的結(jié)構(gòu)提供方法進(jìn)行組件獲取翠订、增巢音、刪等操作。
底層支持的快照
ECS本身是依托輸入進(jìn)行運(yùn)算的尽超,所以針對(duì)復(fù)盤的問題官撼,記錄操作隊(duì)列是可以達(dá)到目的的。但是針對(duì)同步橙弱,由于存在預(yù)判的機(jī)制歧寺,因此在預(yù)判失敗返回正常狀態(tài)時(shí)燥狰,便需要運(yùn)用快照實(shí)現(xiàn)數(shù)據(jù)的快速回滾棘脐,這也就有了 快照 的引入。
快照 針對(duì)的是ECS中的 實(shí)體 和 組件 進(jìn)行操作龙致,而其他模塊因沒有數(shù)據(jù)存在蛀缝,故而無(wú)需進(jìn)行 快照。因 快照 需要存儲(chǔ)數(shù)據(jù)目代,所以就需要可以對(duì)數(shù)據(jù)進(jìn)行序列化與反序列化操作屈梁。
為此,對(duì)原有的 實(shí)體 和 組件 添加序列化與反序列化方法榛了。其中在讶,由于 組件 的可擴(kuò)展性,序列化與反序列化作為 組件 的接口而存在霜大。每次 快照 時(shí)對(duì)所有數(shù)據(jù)進(jìn)行序列化并存儲(chǔ)即可构哺。當(dāng)需要數(shù)據(jù)回滾時(shí),則取出對(duì)應(yīng) 快照 數(shù)據(jù)并進(jìn)行反序列化战坤。
最終的ECS
經(jīng)過上述開發(fā)歷程曙强,一個(gè)ECS架構(gòu)便算完成了。
這個(gè)ECS架構(gòu)提供基礎(chǔ)的 實(shí)體 途茫、 組件 碟嘴、 系統(tǒng) 模塊,同時(shí)帶有 緩存 囊卜、 通知 娜扇、 快照 等輔助模塊,已經(jīng)足夠迎接挑戰(zhàn)栅组。
此外雀瓢,現(xiàn)有的自動(dòng)化工具可以在一定程度上輔助代碼的生成與管理,盡可能減少人為因素導(dǎo)致的Bug笑窜。
不完美
當(dāng)一套架構(gòu)誕生后致燥,如果沒有經(jīng)歷實(shí)測(cè),終究也只是紙上談兵排截。只有經(jīng)歷實(shí)際項(xiàng)目的檢驗(yàn)嫌蚤,才能確認(rèn)架構(gòu)的成熟性辐益。
現(xiàn)有框架下,代碼的自動(dòng)化生成是一個(gè)重要的可開發(fā)點(diǎn)脱吱。此外對(duì)于運(yùn)算效率以及空間效率的追求是永無(wú)止境的智政,這一點(diǎn)上,由于使用數(shù)組存儲(chǔ) 組件 且 組件 自身的離散型箱蝠,注定這里依舊可以尋求更優(yōu)解续捂。
最關(guān)鍵 的是,ECS架構(gòu)或許是面向?qū)ο蟮幕掳幔鞘褂肊CS卻是面向數(shù)據(jù)的牙瓢。如果思維沒有轉(zhuǎn)變,那么一切架構(gòu)也只能朝著最壞的方向去使用间校。
結(jié)語(yǔ)
初始版本的ECS構(gòu)筑完了矾克,如果有更多不完美,也歡迎大家熱烈吐槽憔足。