ECS systems的主要任務就是讀取一系列的components數據,計算以后將結果寫入其他的components。那么如何高效地讀寫數據就是很重要的,而有效的方法就是利用cpu的多核特性來並行地讀取和處理數據。
ECS提供了多種完成這個任務的方式,每一種都有自己的規則和約束:
API
|
說明
|
JobComponentSystem Entities.ForEach
|
最簡單的方式
|
IJobForEach
|
相當於Entities.ForEach,但需要寫更多代碼
|
IJobForEachWithEntity
|
比IJobForEach稍微復雜點,提供了訪問entity handle和entity array index
|
IJobChunk
|
基於chunk的訪問
|
ComponentSystem.ForEach
|
main thread工作模式
|
Manual iteration
|
手動遍歷entities或chunks
|
EntityQuery:用來根據條件查詢entities,上述的各種訪問entities的方式,基本都顯示或隱式地依賴EntityQuery。WithStoreEntityQueryInField(ref EntityQuery)可用來訪問隱式EntityQuery對象。
JobComponentSystem lambda 函數
兩種方式的lambda表達式:
JobComponentSystem.Entities.ForEach(lambda):針對每個entity的job
JobComponentSystem.Job.WithCode(lambda):針對某一段邏輯代碼的job
函數
|
說明
|
WithAll<T>
|
全包含
|
WithAny<T, U>
|
包含任意
|
WithNone<T>
|
不包含
|
WithChangeFilter<T>
|
特定component變更時包含,和上一次update相比
目標component要么在參數列表中,要么在WithAny里面
一次最多同時支持2個components類型的變更
基於chunk級別的變更
如果有代碼以write的方式訪問chunk中的component,也會被標記為changed,不管是否實際改變任何數據
|
WithSharedComponentFilter
|
含有特定值的shared component
調用時傳入shared component object
|
WithStoreEntityQueryInField
|
可以將EntityQuery對象提前顯式地存在一個變量里面
在第JCS創建時賦值
第一次執行之前就可以使用了
可用來查詢entities的數量
|
重要:不能將ForEach參數列表里面的components類型,重復放在WithAll、WithAny、WithNone里面。
Entities.ForEach lambda函數參數列表:
(1)最多8個參數
(2)參數順序規則:
a. 按值傳遞的參數(copy-by-value的方式拷貝)
b. 可寫,ref參數
c. 只讀,in參數
(3)所有的components參數要么是ref要么是in
(4)特殊固定名字參數:
a. Entity entity
b. int entityInQueryIndex
c. int nativeThreadIndex,通過Run()函數手動執行lambda函數時,始終為0
Capturing variables 變量修飾:
函數名
|
說明
|
WithReadOnly(myvar)
|
只讀修飾
|
WithDeallocateOnJobCompletion(myvar)
|
釋放native container
|
WithNativeDisableParallelForRestriction(myvar)
|
保證多個線程訪問同一個可寫naive container。
並行訪問只有在每個線程都訪問自己獨有的數據(或native container中的某一段)時才是安全的。多個線程同時訪問同一個額數據會導致race condition。
|
WithNativeDisableContainerSafetyRestriction(myvar)
|
禁用訪問native container的安全限制。
|
WithNativeDisableUnsafePtrRestrictionAttribute(myvar)
|
允許使用unsafe指針。
|
Job選項:
JobHandle Scedule(JobHandle):以job方式執行lambda
Entities.ForEach:在job threads上並行執行,每個job遍歷query到多chunks上的所有的entities。
Job.WithCode:在一個job thread上執行一個lambda函數的實例。
void Run():主線程同步執行lambda函數,不以job的方式運行
Entities.ForEach:lambda在每個query出來的chunks的entity上都執行一次。
Job.WithCode:lambda函數執行一次。
WithBurst(FloatMode, FloatPrecision, bool):Burst編譯器參數
floatMode:Default Deterministic Fase Strict
floatPrecision:High Low Medium Standard
synchronousCompilation:
WithoutBurst():關閉Burst編譯。當lambda表達式里面有不支持Burst的代碼時使用。
WithStructuralChanges():在主線程以關閉Burst的方式運行,這時候可以做一些structural changes的操作。建議使用EntityCommandBuffer來代替這種用法。
WithName(string):給job起名字,方便調試的時候用。
不能在job中對entity做結構性變更,比如創建entity、添加或移除component、銷毀entity等。正確的方式是使用ECB來緩存並延遲處理這些變更操作。
使用IJobForEach訪問Entity
IJobForEach以chunk為單位處理entities,同一個chunk中的所有entities當做一個批次來處理。當entities分布在不同的chunk中是,不同chunk的處理是並行的。這樣以chunk為單位的處理方式效率非常高,因為單個線程只處理自己的chunk中的數據,不會有多個線程同時處理一個chunk中的數據。
但是,如果沒一個entity的處理都非常耗時,就有可能會帶來性能問題,這種情況下可以使用IJobParallelFor來進行手動方式的遍歷。
定義:
[ExcludeComponent(typeof(Frozen))] [RequireComponentTag(typeof(Gravity))] [BurstCompile] struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed> {}
模板參數表示需要操作的components。
屬性標簽:
標簽 |
說明
|
[ExcludeComponent(typeof(T))]
|
不包含
|
[RequireComponentTag(typeof(T))]
|
需要包含
|
[BurstCompile]
|
Burst編譯加速
|
執行函數Execute():
對於每個有效的entity,都會執行Execute()函數。
public void Execute(ref RotationQuaternion rotationQuaternion, [ReadOnly] ref RotationSpeed rotSpeed){}
參數列表必須和類型的模板參數列表匹配,同時可以使用以下修飾符:
標簽
|
說明
|
[ReadOnly]
|
只讀
|
[WriteOnly]
|
只寫
|
[ChangedFilter]
|
chunks中的component變更時處理chunks中所有的entities,否則就完全不處理
|
IJobForEachWithEntity:
提供額外的參數:Entity對象和index。
可以使用提供得entity對象結合ECB,來操作entity或添加刪除components。
使用ComponentSystem訪問Entity
首選可以利用CPU多核特性的JobComponentSystem,而ComponentSystem一般只會用在以下幾種情況:
(1)調試或測試性開發;
(2)當需要調用只能在main thread上調用的一些API時使用;
(3)代碼的復雜甚至低於創建和調度一個job時使用;
(4)當需要在遍歷entities時直接執行一些結構性修改時,可以直接在ForEach中同步執行。
注意:在主線程的結構性修改會同步等待所有的jobs執行完成,所以會造成卡頓。這種情況下可以使用一個post-update command buffer,提前收集所有的修改操作,然后在某個時間點一次執行。
注意:ForEach lambda最多支持6個類型的components參數。
使用IJobChunk訪問Entity
JCS會針對每一個chunk執行一遍Execute()函數,然后在函數中逐個entity處理。
比IJobForEach代碼更復雜,需要更多代碼。但是要更加的明確和直接地訪問數據。
使用chun還有一個方便之處就是,可以在代碼中檢查optionl component是否存在,然后細化處理。
(1)創建EntityQuery
使用JobComponentSystem.GetEntityQurey()來創建EnityQuery;
一個system定義一次然后存成變量。
(2)定義IJobChunk
ArchetyChunkComponentType<T>:每個需要訪問的component都需要定一個該類型對象,用來獲得T的列表數組。在OnUpdate()中每幀賦值,然后在Job.Execute()中使用。
注意:ArchetyChunkComponentType<T>不能緩存,必須每幀在使用之前更新。
(3)Eexcute()執行函數
使用chunk.Has<T>來判斷是否包含某個可選的組件,每個chunk在一次執行中只需要check一次;
基於版本號是否變更來決定是否執行job;
(4)實例化job、給字段賦值、並調度
public class TestSystem : JobComponentSystem { private EntityQuery m_Query; protected override void OnCreate() { m_Query = GetEntityQuery( ComponentType.ReadWrite<Output>(), ComponentType.ReadOnly<InputA>(), ComponentType.ReadOnly<InputB>()); m_Query.SetChangedVersionFilter( new ComponentType[] { ComponentType.ReadWrite<InputA>(), ComponentType.ReadWrite<InputB>() }); } [BurstCompile] struct UpdateJob : IJobChunk { public ArchetypeChunkComponentType<InputA> InputAType; public ArchetypeChunkComponentType<InputB> InputBType; [ReadOnly] public ArchetypeChunkComponentType<Output> OutputType; public uint LastSystemVersion; public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) { var inputAChanged = chunk.DidChange(InputAType, LastSystemVersion); var inputBChanged = chunk.DidChange(InputBType, LastSystemVersion); // If neither component changed, skip the current chunk if (!(inputAChanged || inputBChanged)) return; var inputAs = chunk.GetNativeArray(InputAType); var inputBs = chunk.GetNativeArray(InputBType); var outputs = chunk.GetNativeArray(OutputType); for (var i = 0; i < outputs.Length; i++) { outputs[i] = new Output{ Value = inputAs[i].Value + inputBs[i].Value }; } } } protected override JobHandle OnUpdate(JobHandle inputDependencies) { var job = new UpdateJob(); job.LastSystemVersion = this.LastSystemVersion; job.InputAType = GetArchetypeChunkComponentType<InputA>(true); job.InputBType = GetArchetypeChunkComponentType<InputB>(true); job.OutputType = GetArchetypeChunkComponentType<Output>(false); return job.Schedule(m_Query, inputDependencies); } }
手動迭代遍歷Entity
在JobComponentSystem中使用IJobParallelFor:
public class RotationSpeedSystem : JobComponentSystem { [BurstCompile] struct RotationSpeedJob : IJobParallelFor { [DeallocateOnJobCompletion] public NativeArray<ArchetypeChunk> Chunks; public ArchetypeChunkComponentType<RotationQuaternion> RotationType; [ReadOnly] public ArchetypeChunkComponentType<RotationSpeed> RotationSpeedType; public float DeltaTime; public void Execute(int chunkIndex) { var chunk = Chunks[chunkIndex]; var chunkRotation = chunk.GetNativeArray(RotationType); var chunkSpeed = chunk.GetNativeArray(RotationSpeedType); var instanceCount = chunk.Count; for (int i = 0; i < instanceCount; i++) { var rotation = chunkRotation[i]; var speed = chunkSpeed[i]; rotation.Value = math.mul(math.normalize(rotation.Value), quaternion.AxisAngle(math.up(), speed.RadiansPerSecond * DeltaTime)); chunkRotation[i] = rotation; } } } EntityQuery m_Query; protected override void OnCreate() { var queryDesc = new EntityQueryDesc { All = new ComponentType[]{ typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>() } }; m_Query = GetEntityQuery(queryDesc); } protected override JobHandle OnUpdate(JobHandle inputDeps) { var rotationType = GetArchetypeChunkComponentType<RotationQuaternion>(); var rotationSpeedType = GetArchetypeChunkComponentType<RotationSpeed>(true); var chunks = m_Query.CreateArchetypeChunkArray(Allocator.TempJob); var rotationsSpeedJob = new RotationSpeedJob { Chunks = chunks, RotationType = rotationType, RotationSpeedType = rotationSpeedType, DeltaTime = Time.deltaTime }; return rotationsSpeedJob.Schedule(chunks.Length,32,inputDeps); } }
在ComponentSystem里面手動執行:
遍歷所有的entities:
var entityManager = World.Active.EntityManager; var allEntities = entityManager.GetAllEntities(); foreach (var entity in allEntities) { //... } allEntities.Dispose();
遍歷所有的chunks:
var entityManager = World.Active.EntityManager; var allChunks = entityManager.GetAllChunks(); foreach (var chunk in allChunks) { //... } allChunks.Dispose();
EntityQuery的用法
最簡單的方式:
在System內部創建:
EntityQuery m_Query = GetEntityQuery(typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>());
在System外部創建:
EntityQuery m_Query = CreateEntityQuery(typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>());
使用EntityQueryDesc:
var query = new EntityQueryDesc { None = new ComponentType[]{ typeof(Frozen) }, All = new ComponentType[]{ typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>() } Options = EntityQueryDescOptions.FilterWriteGroup } EntityQuery m_Query = GetEntityQuery(query);
查詢選項:(一般不需要設置)
選項
|
說明
|
Deault
|
不設置
|
IncludePrefab
|
包含特定Prefab tag的compnent
|
IncludeDisable
|
包含特定Disabled tag的component
|
FilterWriteGroup
|
query時考慮component的WirteGroup設置
|
組合查詢:
var query0 = new EntityQueryDesc { All = new ComponentType[] {typeof(RotationQuaternion)} }; var query1 = new EntityQueryDesc { All = new ComponentType[] {typeof(RotationSpeed)} }; // query0或query1 EntityQuery m_Query = GetEntityQuery(new EntityQueryDesc[] {query0, query1});
使用filter:
(1)Shared component filters:篩選出擁有特定值的sharedcomponent的entities
最多同時支持2個shared component filters
struct SharedGrouping : ISharedComponentData { public int Group; } class ImpulseSystem : ComponentSystem { EntityQuery m_Query; protected override void OnCreate(int capacity) { m_Query = GetEntityQuery(typeof(Position), typeof(Displacement), typeof(SharedGrouping)); } protected override void OnUpdate() { // Only iterate over entities that have the SharedGrouping data set to 1 m_Query.SetFilter(new SharedGrouping { Group = 1 }); var positions = m_Query.ToComponentDataArray<Position>(Allocator.Temp); var displacememnts = m_Query.ToComponentDataArray<Displacement>(Allocator.Temp); for (int i = 0; i != positions.Length; i++) positions[i].Value = positions[i].Value + displacememnts[i].Value; } }
(2)Change filters:
protected override void OnCreate(int capacity) { m_Query = GetEntityQuery(typeof(LocalToWorld), ComponentType.ReadOnly<Translation>()); m_Query.SetFilterChanged(typeof(Translation)); }
使用查詢結果:
函數
|
說明
|
ToEntityArray()
|
轉為entities數組
|
ToComponentDataArray<T>()
|
轉為T的數組
|
CreateArchetypeChunkArray()
|
轉為Chunk數組
|
還可以直接將query傳給job.Shedule()函數:
var job = new RotationSpeedJob() { RotationType = rotationType, RotationSpeedType = rotationSpeedType, DeltaTime = Time.deltaTime }; return job.Schedule(m_Query, inputDependencies);