ECS架構(gòu)的核心是數(shù)據(jù),這也是為什么Unity會(huì)將這一套技術(shù)棧命名為DOTS。System會(huì)讀取entity上面的component的數(shù)據(jù)流,并處理數(shù)據(jù)。實(shí)體在這里其實(shí)更像是索引,它本身并不包含任何數(shù)據(jù)和邏輯。
ECS包含三個(gè)部分:實(shí)體(entities)、數(shù)據(jù)(components)、行為(system)。具體看下圖:
這個(gè)圖中,System讀取了多個(gè)實(shí)體的Translation和Rotation組件,然后經(jīng)過(guò)計(jì)算處理,將結(jié)果更新到LocalToWorld組件中。
從圖中你可以看到,實(shí)體A和B還有Renderer組件,但是C并沒(méi)有。不過(guò)這并不會(huì)影響System的計(jì)算邏輯,因?yàn)檫@個(gè)系統(tǒng)不關(guān)心Renderer組件。
你還可以寫一個(gè)系統(tǒng),需要處理Renderer組件,這樣系統(tǒng)就會(huì)忽略實(shí)體C。你還可以寫一個(gè)系統(tǒng)排除包含Renderer組件的實(shí)體,這樣系統(tǒng)就會(huì)忽略實(shí)體A和B。
下面對(duì)ECS中比較重要的幾個(gè)核心概念做一個(gè)梳理:
原型 Archetypes
多個(gè)組件的組合叫做一個(gè)原型。
比如一個(gè)3D物體可能會(huì)包含用于transform的組件,包括移動(dòng)、旋轉(zhuǎn)、渲染,每個(gè)3D物體對(duì)應(yīng)一個(gè)實(shí)體,但是他們都有同樣的組件,所以ECS會(huì)把他們分類成是一類原型。
在上圖中,實(shí)體A和B的原型都是M,實(shí)體C的原型是N。
你也可以通過(guò)在運(yùn)行時(shí)添加或者移除component來(lái)改變一個(gè)實(shí)體的原型。例如:如果將實(shí)體B的Renderer組件移除,實(shí)體B的原型就會(huì)變成N。
內(nèi)存塊 Memory Chunks
為什么要先講原型的概念呢,因?yàn)橐粋€(gè)實(shí)體的原型是什么,決定了ECS會(huì)將實(shí)體的components也就是數(shù)據(jù)存在什么地方。ECS按塊分配內(nèi)存,每塊用一個(gè)ArchetypeChunk對(duì)象表示。
一個(gè)塊只包含一種原型,可以包含的多個(gè)實(shí)體的數(shù)據(jù)。如果一個(gè)塊的內(nèi)存滿了,ECS會(huì)分配一個(gè)新的塊來(lái)存儲(chǔ)新的實(shí)體的components。
如果你修改了實(shí)體的組件,那就相當(dāng)于修改了實(shí)體的原型,這時(shí)候ECS會(huì)將實(shí)體的組件數(shù)據(jù)移到另外一個(gè)塊中。
原型和內(nèi)存塊的關(guān)系是一對(duì)多的關(guān)系。這就意味著,如果想查詢給定的一組component類型的所有實(shí)體,只需要在這些原型中搜索即可。這樣會(huì)比在所有的實(shí)體中查找效率高很多。
ECS在存儲(chǔ)實(shí)體到內(nèi)存塊中沒(méi)有特殊的排序,當(dāng)創(chuàng)建一個(gè)實(shí)體或者實(shí)體的原型發(fā)生變化時(shí),ECS會(huì)將它放到對(duì)應(yīng)原型的第一個(gè)還有空間的內(nèi)存塊中。內(nèi)存塊中的數(shù)據(jù)會(huì)緊密排列。如果一個(gè)實(shí)體要被移出當(dāng)前原型的內(nèi)存塊,這時(shí)候會(huì)有個(gè)空位,ECS會(huì)把這個(gè)內(nèi)存塊最后的實(shí)體數(shù)據(jù)移動(dòng)到這個(gè)空位中。
注意:原型中的共享組件(后面會(huì)具體說(shuō)這是個(gè)什么東東)的數(shù)據(jù)也會(huì)影響實(shí)體會(huì)被存在哪個(gè)內(nèi)存塊。同一個(gè)內(nèi)存塊中的所有實(shí)體的共享組件中的數(shù)據(jù)值都是相同的。如果你修改了共享組件中的數(shù)據(jù),這個(gè)實(shí)體會(huì)被移到另外一個(gè)塊中,有點(diǎn)類似修改了實(shí)體的原型。
將共享組件的實(shí)體分到一個(gè)內(nèi)存塊中會(huì)提高處理他們的速度。比如Hybird Renderer(混合渲染)定義了RenderMesh組件來(lái)達(dá)成這個(gè)目的。
實(shí)體查詢
一個(gè)System根據(jù)什么來(lái)決定處理哪些實(shí)體呢?這時(shí)候會(huì)用到一個(gè)叫實(shí)體查詢(Entity Query)的東西。實(shí)體查詢首先需要一些組件類型,然后根據(jù)你傳入的組件類型的組合,在包含這些組件的原型中查詢符合要求的實(shí)體。查詢時(shí)可以指定下面三種類型:
All 必須包含All中所有的組件類型
Any 必須包含Any中至少一個(gè)組件類型
None 不能包含None中任意一個(gè)組件類型
一次實(shí)體查詢的結(jié)果會(huì)返回所有符合查詢要求的內(nèi)存塊,你可以使用IJobChunk來(lái)迭代遍歷所有的組件。(IJobChunk后面會(huì)講。)
Jobs 作業(yè)
之前說(shuō)過(guò),ECS配合Job使用才能發(fā)揮多線程的威力。ECS提供了SystemBase類,其中包含Entities.ForEach方法,還包含了IJobChunk的Schedule()和ScheduleParallel()方法,可以在子線程中處理數(shù)據(jù)。Entities.ForEach是最簡(jiǎn)單的方法,只需要幾行代碼就能實(shí)現(xiàn)。IJobChunk可以用來(lái)處理比較復(fù)雜的情況。
ECS會(huì)在主線程調(diào)度Job,根據(jù)System的順序。當(dāng)job調(diào)度后,ECS會(huì)追蹤哪些job在讀寫哪些組件。需要讀權(quán)限的job需要等待前面寫權(quán)限的job執(zhí)行完,反之亦然。Job調(diào)度器會(huì)使用job依賴來(lái)決定哪些job可以并行,哪些必須串行。
System的組織
ECS通過(guò)World和group來(lái)組織system。默認(rèn)情況下,ECS會(huì)創(chuàng)建一個(gè)默認(rèn)的World,包含一些預(yù)定義的group組。它會(huì)找到工程中所有的System,實(shí)例化他們,并添加到預(yù)定義的group中。
你可以指定同一個(gè)group中system的Update的執(zhí)行順序。Group也是一種system,所以你可以將一個(gè)group添加到另外一個(gè)group中。如果你沒(méi)有指定順序,system的執(zhí)行順序會(huì)不太確定,并不會(huì)按照它們創(chuàng)建的順序。不過(guò),同一個(gè)group中的所有system都會(huì)比下一個(gè)group中的system先執(zhí)行。
System的Update是在主線程中執(zhí)行的,不過(guò)可以使用Job將工作分配到子線程中。
ECS世界的可視化創(chuàng)建(authoring)
在Unity中沒(méi)辦法直接可視化地創(chuàng)建DOTS世界,但是,可以先用GameOjbect和MonoBehaviour來(lái)創(chuàng)建,然后通過(guò)轉(zhuǎn)換系統(tǒng)將GameObject轉(zhuǎn)換成實(shí)體和組件。這個(gè)我們后面也會(huì)細(xì)講。
來(lái)源:Unity官方平臺(tái)
原文:https://mp.weixin.qq.com/s/Tvtqz51Np7vWeYwKcQtHdQ