ECS:访问Entity数据
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);