ECS+Job实例总结
环境: Unity2022.3.5f + com.unity.entities 1.0.14 + URP
entities 不支持Builtin管线 需要切换URP or HDRP 否则表现上Game视图中不会显示Subscene中物体
在空工程导入entities包后 Collections中存在多项Native数据类型报错 重启工程后解决
实例工程采用Subscene方式 借助Subscene以及实现的相应组件的Baker将组件与实体进行绑定
Subscene 会将子场景内的对象转为实体, 并依据对象当前挂载的组件进行对应组件的关系绑定。 节省了手动AddComponent操作。 但用户自定义组件。需要相应继承Mono的Authoring类挂载在相应对象身上 并实现一个继承Baker的绑定类,将Authoring类型中的属性与组件属性绑定关联起来。
ECS
非托管对象
内存连续 避免缓存未命中 避免访问主存以及检查缓存是否有空余快可装进
避免GC
Entity
实体
public struct Entity : IEquatable<Entity>, IComparable<Entity>
存在Index Version 属性
Index相当于实体ID。
Version记录当前实体版本号
实体被销毁时,索引会被回收,并自增版本号。
当判断两个实体是否为同一实体时,需要同时判断Index和Version。
创建以及销毁实体
EntityManager
// Creates an entity that contains a cleanup component.
Entity e = EntityManager.CreateEntity(
typeof(Translation), typeof(Rotation), typeof(ExampleCleanup));
// Attempts to destroy the entity but, because the entity has a cleanup component, Unity doesn't actually destroy the entity. Instead, Unity just removes the Translation and Rotation components.
EntityManager.DestroyEntity(e);
// The entity still exists so this demonstrates that you can still use the entity normally.
EntityManager.AddComponent<Translation>(e);
// Removes all the components from the entity. This destroys the entity.
EntityManager.DestroyEntity(e, new ComponentTypes(typeof(ExampleCleanup), typeof(Translation)));
// Demonstrates that the entity no longer exists. entityExists is false.
bool entityExists = EntityManager.Exists(e);
EntityCommandBuffer
可以通过Buffer控制行为在指定位置响应 例如指定在开始模拟或模拟之后触发
EntityCommandBuffer entityCommandBuffer =
SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
EntityCommandBuffer entityCommandBuffer =
SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
Entity spawnedEntity = entityCommandBuffer.CreateEntity();
// 或者通过Entity进行实例化
Entity spawnedEntity =
entityCommandBuffer.Instantiate(redSpawnerComponent.PlayerPrefab);
// 为实体设置LocalTransofrm组件(即Transofrm)
entityCommandBuffer.SetComponent(spawnedEntity, new LocalTransform()
{
Position = new float3(50f, 0.171f, 50f),
Rotation = new quaternion(0f,0f,0f,0f),
Scale = 1f
});
但是要注意代码中CommandBuffer中创建的实体时还未正式创建实例化。所以此时spawnedEntity的Index值此时还为-1。真实的Index需要在响应位置真正创建好后才会设置为一个>0的数值 并更新版本
获取实体
// 获取当前Manager中所有拥有PlayerTag组件的实体队列
EntityQuery playerTagEntityQuery =
EntityManager.CreateEntityQuery(typeof(PlayerTag));
// 将实体队列转换为一个实体数组副本
NativeArray<Entity> entityNativeArray =
playerTagEntityQuery.ToEntityArray(Unity.Collections.Allocator.Temp);
// 后续操作
实体单例
对于获取全局只会存在一个的实例实体 可以使用
SystemAPI.GetSingletonEntity<Entity>();
Component
组件
组件起标记作用
创建新的组件时,需要为struct并继承 IComponentData
组件可以为空,也可以设置需要标记的属性
空组件时,其并不占用存储控件。但也可以像存在属性的组件一样做响应操作
// 借助Baker 将Target组件与实体进行关联
public class TargetAuthoring : MonoBehaviour
{
class Baker : Baker<TargetAuthoring>
{
public override void Bake(TargetAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent<Target>(entity);
}
}
}
public struct Target : IComponentData
{
public Entity Value;
}
创建 销毁 修改
同样可以使用EntityManager和EntityCommandBuffer来进行操作
EntityManager.AddComponent<Translation>(entity);
EntityCommandBuffer entityCommandBuffer =
SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
// 为实体设置LocalTransofrm组件(即Transofrm)
entityCommandBuffer.SetComponent(spawnedEntity, new LocalTransform()
{
Position = new float3(50f, 0.171f, 50f),
Rotation = new quaternion(0f,0f,0f,0f),
Scale = 1f
});
RefRO<> RefRW<>
限制组件中属性为只读或可读写
ComponentTypeHandle
ComponentLookup
组件单例
// 获取一个可读写的组件单例
RefRW<RandomComponent> randomComponent = SystemAPI.GetSingletonRW<RandomComponent>();
System
驱动所有有需求的组件执行行为逻辑
SystemBase
public partial class MySystem : SystemBase
{
protected override void OnUpdate()
{
throw new System.NotImplementedException();
}
}
限制只能运行在主线程
ISystem
public partial struct MySystem : ISystem
{
// Called once when the system is created.
// Can be omitted when empty.
[BurstCompile]
public void OnCreate(ref SystemState state)
{
}
// Called once when the system is destroyed.
// Can be omitted when empty.
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
// Usually called every frame. When exactly a system is updated
// is determined by the system group to which it belongs.
// Can be omitted when empty.
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
}
可以配合Job进行多线程执行任务
执行顺序
可以为System添加标签指定其相对于某一System的执行顺序
[UpdateBefore(typeof(AfterSystem))]
[UpdateAfter(typeof(AfterSystem))]
操作组件
官方示例 获取及操作组件队列数据
namespace ExampleCode.Queries
{
public partial struct MySystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
EntityQuery myQuery = SystemAPI.QueryBuilder().WithAll<Foo, Bar, Apple>().WithNone<Banana>().Build();
ComponentTypeHandle<Foo> fooHandle = SystemAPI.GetComponentTypeHandle<Foo>();
ComponentTypeHandle<Bar> barHandle = SystemAPI.GetComponentTypeHandle<Bar>();
EntityTypeHandle entityHandle = SystemAPI.GetEntityTypeHandle();
// Getting array copies of component values and entity ID's:
{
// Remember that Temp allocations do not need to be manually disposed.
// Get an array of the Apple component values of all
// entities that match the query.
// This array is a *copy* of the data stored in the chunks.
NativeArray<Apple> apples = myQuery.ToComponentDataArray<Apple>(Allocator.Temp);
// Get an array of the ID's of all entities that match the query.
// This array is a *copy* of the data stored in the chunks.
NativeArray<Entity> entities = myQuery.ToEntityArray(Allocator.Temp);
}
// Getting the chunks matching a query and accessing the chunk data:
{
// Get an array of all chunks matching the query.
NativeArray<ArchetypeChunk> chunks = myQuery.ToArchetypeChunkArray(Allocator.Temp);
// Loop over all chunks matching the query.
for (int i = 0, chunkCount = chunks.Length; i < chunkCount; i++)
{
ArchetypeChunk chunk = chunks[i];
// The arrays returned by `GetNativeArray` are the very same arrays
// stored within the chunk, so you should not attempt to dispose of them.
NativeArray<Foo> foos = chunk.GetNativeArray(ref fooHandle);
NativeArray<Bar> bars = chunk.GetNativeArray(ref barHandle);
// Unlike component values, entity ID's should never be
// modified, so the array of entity ID's is always read only.
NativeArray<Entity> entities = chunk.GetNativeArray(entityHandle);
// Loop over all entities in the chunk.
for (int j = 0, entityCount = chunk.Count; j < entityCount; j++)
{
// Get the entity ID and Foo component of the individual entity.
Entity entity = entities[j];
Foo foo = foos[j];
Bar bar = bars[j];
// Set the Foo value.
foos[j] = new Foo { };
}
}
}
// SystemAPI.Query:
{
// SystemAPI.Query provides a more convenient way to loop
// through the entities matching a query. Source generation
// translates this foreach into the functional equivalent
// of the prior section. Understand that SystemAPI.Query
// should ONLY be called as the 'in' clause of a foreach.
// Each iteration processes one entity matching a query
// that includes Foo, Bar, Apple and excludes Banana:
// - 'foo' is assigned a read-write reference to the Foo component
// - 'bar' is assigned a read-only reference to the Bar component
// - 'entity' is assigned the entity ID
foreach (var (foo, bar, entity) in
SystemAPI.Query<RefRW<Foo>, RefRO<Bar>>()
.WithAll<Apple>()
.WithNone<Banana>()
.WithEntityAccess())
{
foo.ValueRW = new Foo { };
}
}
}
}
}
SystemGroup
System的上层管理节点 作用为对System做分类分组
可以对System添加标签 进入指定分组
[UpdateInGroup(typeof(MySystemGroup))]
Job + Burst
JobSystem 工作任务 可多线程并行执行任务
Job主要是利用CPU来降低计算负载,数量级上远不如GPU,但是比GPU更适合处理复杂逻辑
或者如果数据是从CPU发起也更适合,避免了数据拷贝到GPU的开销
Burst 会把IL变成使用LLVM优化的CPU代码,执行效率会大幅提升
主线程执行单一Job
namespace ExampleCode.IJobs
{
// An example job which increments all the numbers of an array.
public struct IncrementJob : IJob
{
// The data which a job needs to use should all
// be included as fields of the struct.
public NativeArray<float> Nums;
public float Increment;
// Execute() is called when the job runs.
public void Execute()
{
for (int i = 0; i < Nums.Length; i++)
{
Nums[i] += Increment;
}
}
}
// A system that schedules the IJob.
public partial struct MySystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var job = new IncrementJob
{
Nums = CollectionHelper.CreateNativeArray<float>(1000, state.WorldUpdateAllocator),
Increment = 5f
};
JobHandle handle = job.Schedule();
handle.Complete();
}
}
}
根据输入粒度切割的并行Job
namespace ExampleCode.IJobParallelFors
{
// An example job which increments all the numbers of an array in parallel.
public struct IncrementParallelJob : IJobParallelFor
{
// The data which a job needs to use must all
// be included as fields of the struct.
public NativeArray<float> Nums;
public float Increment;
// Execute(int) is called when the job runs.
public void Execute(int index)
{
Nums[index] += Increment;
}
}
// A system that schedules the IJobParallelFor.
public partial struct MySystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var job = new IncrementParallelJob
{
Nums = new NativeArray<float>(1000, state.WorldUpdateAllocator),
Increment = 5f
};
JobHandle handle = job.Schedule(
job.Nums.Length, // number of times to call Execute
64); // split the calls into batches of 64
handle.Complete();
}
}
}
Chunk粒度的Job
namespace ExampleCode.IJobChunks
{
// An example IJobChunk.
[BurstCompile]
public struct MyIJobChunk : IJobChunk
{
// The job needs type handles for each component type
// it will access from the chunks.
public ComponentTypeHandle<Foo> FooHandle;
// Handles for components that will only be read should be
// marked with [ReadOnly].
[ReadOnly] public ComponentTypeHandle<Bar> BarHandle;
// The entity type handle is needed if we
// want to read the entity ID's.
public EntityTypeHandle EntityHandle;
// Jobs should not use an EntityManager to create and modify
// entities directly. Instead, a job can record commands into
// an EntityCommandBuffer to be played back later on the
// main thread at some point after the job has been completed.
// If the job will be scheduled with ScheduleParallel(),
// we must use an EntityCommandBuffer.ParallelWriter.
public EntityCommandBuffer.ParallelWriter Ecb;
// When this job runs, Execute() will be called once for each
// chunk matching the query that was passed to Schedule().
// The useEnableMask param is true if any of the
// entities in the chunk have disabled components
// of the query. In other words, this param is true
// if any entities in the chunk should be skipped over.
// The chunkEnabledMask identifies which entities
// have all components of the query enabled, i.e. which entities
// should be processed:
// - A set bit indicates the entity should be processed.
// - A cleared bit indicates the entity has one or more
// disabled components and so should be skipped.
// The `unfilteredChunkIndex` is the index of the chunk in the sequence of all chunks matching the query: the first
// chunk matching the query is index 0, the second is index 1, and so forth. This value is mainly useful as
// a *sort key* passed to the methods of `EntityCommandBuffer.ParallelWriter`. Each recorded command includes
// a sortKey, and in playback, the commands are sorted by these keys before the commands are executed.
// This sorting effectively guarantees the commands will execute in a deterministic order even though the
// original recorded order of the commands was non-deterministic.
[BurstCompile]
public void Execute(in ArchetypeChunk chunk,
int unfilteredChunkIndex,
bool useEnableMask,
in v128 chunkEnabledMask)
{
// Get the entity ID and component arrays from the chunk.
NativeArray<Entity> entities = chunk.GetNativeArray(EntityHandle);
NativeArray<Foo> foos = chunk.GetNativeArray(ref FooHandle);
NativeArray<Bar> bars = chunk.GetNativeArray(ref BarHandle);
// The ChunkEntityEnumerator helps us loop over
// the entities of the chunk, but only those that
// match the query (accounting for disabled components).
var enumerator = new ChunkEntityEnumerator(useEnableMask, chunkEnabledMask, chunk.Count);
// Loop over all entities in the chunk that match the query.
while (enumerator.NextEntityIndex(out var i))
{
// Read the entity ID and component values.
var entity = entities[i];
var foo = foos[i];
var bar = bars[i];
// If the Bar value meets a criteria, we
// record a command in the ECB to remove it.
if (bar.Value < 0)
{
Ecb.RemoveComponent<Bar>(unfilteredChunkIndex, entity);
}
// Set the Foo value.
foos[i] = new Foo { };
}
}
}
// A system that schedules and completes the above IJobChunk.
public partial struct MySystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Get an EntityCommandBuffer from
// the BeginSimulationEntityCommandBufferSystem.
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
// Create the job.
var job = new MyIJobChunk
{
FooHandle = state.GetComponentTypeHandle<Foo>(false),
BarHandle = state.GetComponentTypeHandle<Bar>(true),
Ecb = ecb.AsParallelWriter()
};
var myQuery = SystemAPI.QueryBuilder().WithAll<Foo, Bar, Apple>().WithNone<Banana>().Build();
// Schedule the job.
// By calling ScheduleParallel() instead of Schedule(),
// the chunks matching the job's query will be split up
// into batches, and these batches may be processed
// in parallel by the worker threads.
// We pass state.Dependency to ensure that this job depends upon
// any overlapping jobs scheduled in prior system updates.
// We assign the returned handle to state.Dependency to ensure
// that this job is passed as a dependency to other systems.
state.Dependency = job.ScheduleParallel(myQuery, state.Dependency);
}
}
}
实体粒度的Job
namespace ExampleCode.IJobEntitys
{
// An example IJobEntity that is functionally equivalent to the IJobChunk above.
// An `IJobEntity` is more concise than its `IJobChunk` equivalent because
// its source generation takes care of some boilerplate.
// Only entities having the Apple component type will match the job's implicit query
// even though the job does not access the Apple component values.
[WithAll(typeof(Apple))]
// Only entities NOT having the Banana component type will match the job's implicit query.
[WithNone(typeof(Banana))]
[BurstCompile]
public partial struct MyIJobEntity : IJobEntity
{
// Thanks to source generation, an IJobEntity gets the type handles
// it needs automatically, so we do not include them manually.
// EntityCommandBuffers and other fields still must
// be included manually.
public EntityCommandBuffer.ParallelWriter Ecb;
// Source generation will create an EntityQuery based on the
// parameters of Execute(). In this case, the generated query will
// match all entities having a Foo and Bar component.
// - When this job runs, Execute() will be called once
// for each entity matching the query.
// - Any entity with a disabled Foo or Bar will be skipped.
// - 'ref' param components are read-write
// - 'in' param components are read-only
// - We need to pass the chunk index as a sortKey to methods of
// the EntityCommandBuffer.ParallelWriter, so we include an
// int parameter with the [ChunkIndexInQuery] attribute.
[BurstCompile]
public void Execute([ChunkIndexInQuery] int chunkIndex, Entity entity, ref Foo foo, in Bar bar)
{
// If the Bar value meets this criteria, we
// record a command in the ECB to remove it.
if (bar.Value < 0)
{
Ecb.RemoveComponent<Bar>(chunkIndex, entity);
}
// Set the Foo value.
foo = new Foo { };
}
}
// A system that schedules and completes the above IJobEntity.
public partial struct MySystem : ISystem
{
// We don't need to create the query manually because source generation
// creates one inferred from the IJobEntity's attributes and Execute params.
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Get an EntityCommandBuffer from the BeginSimulationEntityCommandBufferSystem.
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
// Create the job.
var job = new MyIJobEntity
{
Ecb = ecb.AsParallelWriter()
};
// Schedule the job. Source generation creates and passes the query implicitly.
state.Dependency = job.Schedule(state.Dependency);
}
}
}
Job的依赖
var job = new MyJob{ Nums = myArray};
var otherJob = new MyJob{ Nums = myArray};
JobHandle handle = job.Schedule(); //Schedule 和Complete 只能在主线程执行
handle.Complete(); //Job完成后才会返回
JobHandle otherHandle = otherJob.Schedule();
//必须要在handle.Complete 后执行 因为引用同一数组 同时Schedule 会发生竞争行为
或者
将第一个handle 作为句柄传递给第二个调度调用来使第二个Job依赖第一个Job的完成。所以当第一个Job未完成时 第二个Job不会从Job队列中取出
JobHandle otherHandle = otherJob.Schedule(handle);
handle.Complete();
otherHandle.Complete();
// 可以使用CombineDependencies 时单个Job依赖多个JobHandle
JobHandle combined = JobHandle.CombineDependencies(…);
JobHandle handleA = jobA.Schedule(combined);
注意事项
注意避免循环依赖,造成死锁
Job只能依赖于在其之前调度的Job
并且一旦调度 Job无法更改依赖关系
不允许在Native容器里添加托管类型
不可以访问静态变量
暂时先写这些大体 后期再补充细节吧