ECS:Components
Components
ECS的Component基于以下这些interface来实现:
类型
|
说明
|
IComponentData
|
常用组件,或者chunk components
|
IBufferElementData
|
用于将dynamic buffer和entity关联
DynamicBuffer<T>
|
ISharedComponentData
|
共享组件,用于按archetype中的值来对entity分类或分组
|
ISystemStateComponentData
|
用于将system-specific state与entity关联
用于检测单个entity的创建或销毁
|
ISharedSystemStateComponentData
|
shared和system state的组合 |
Blob assets
|
不算是component,但可以用来存储data。Blob assets可以被一个或多个component通过BlobAssetReference使用,并且是immutable的。
Blob assets允许你在assets之间共享data,并在c# job中访问这个数据。
|
存储在chunk外的component:
(1)shared components
(2)chunk components
(3)dynamic buffers 超出capacity的部分存储在chunk外部
这些组件的的单个instance应用于所有对应块中的entities。
另外,可以将dynamic buffers的一些数据存储在chunk外面。
虽然这些类型的component存储在chunk之外,在query entities时,你可以把它们看作和普通的component一样处理。
大小限制:一个entity所有的components必须放在同一个chunk中,因此不能超过16K。
有一些component,比如DynamicBuffer<T>和BlobArray<T>,因为存储在chunk外,所以没有此限制。
通用components
ComponentData只能包含数据或对数据进行访问的util函数,不能包含任何其他行为函数。
所有的game logic和behaviour都应该在systems里面实现。
IComponentData:general-purpose component type
(1)使用struct实现
(2)只能包含如下如数据类型:
c# blttable types
|
bool
|
char
|
a fixed-sized character buffer
|
BlobAssetReference<T>(a reference to a Blob data structure)
|
fixed arrays(in an unsafe context)
|
包含这些unmanaged、blitable fields的struct
|
注意:还可以使用包含一个单独的IBufferElementData component的DynamicBuffer<T>作为array-like数据结构。
(3)不能包含对managed object的引用
(4)值拷贝,数据修改方式:
var transform = group.transform[index]; // Read transform.heading = playerInput.move; // Modify transform.position += deltaTime * playerInput.move * settings.playerMoveSpeed; group.transform[index] = transform; // Write
(5)功能原子化:一个IComponentData实现,包含的数据都应该是同时访问的,也就是说功能原子化。通常来说,使用许多小型的component比使用少量的大型的component效率要高。
(6)大多数component基本都会基于IComponentData来实现。
Managed IComponentData:托管组件
用途:用于将现有代码逐步转移到ECS架构
用法:
(1)使用class实现IComponentData,用起来和value type IComponentData一样
(2)必须实现IEquatable<T>
(3)必须override Object.GetHashCode()
(4)必须提供默认构造函数
限制:
(1)不能使用Burst编译器加速
(2)不能使用Job
(3)不能使用chunk memory(内存非连续)
(4)需要gc
禁用:UNITY_DISABLE_MANAGED_COMPONENTS
Shared Components
ISharedComponentData:value is shared by all entities in the same chunk.
SharedComponent的值会影响Entity存储的Chunk,但不影响其ArcheType。
优先使用SharedComponent,但是不能过度使用,因为每一个不同的值都会导致重新分配一个chunk。
尽量不要给ShardComponent添加不必要的数据,因为可能会导致更多的状态膨胀。
一些重要的点:
(1)同一个ArcheType里面,拥有相同值的ShardedComponentData的Entitiesa放在同样的Chunks里面;
(2)SharedComponentData数据不存在每个Entity里面,而是存在Chunk上,每个Chunk记录它所关联的SharedComponentData的index,所以SharedComponentData对于每个Entity来说是zero memory overhead;
(3)可以使用EntityQuery遍历所有有用相同type的entities;
(4)可以使用EntityQuery.SetFilter()来遍历具有特定SharedComponentData值的Entities,由于其内存布局是基于Chunk的,所以这个遍历是很快的;
(5)使用EntityManager.GetAllUniqueSharedComponents()获取所有状态唯一的SharedComponentData;
(6)SharedComponentData自动计算引用计数;
(7)SharedComponentData应该尽量不要修改其值,因为修改值因为这通过memcpy在不同的chunk间拷贝entity的componentdata。
System State Components
ISystemStateSharedComponentData,系统状态组件,可以用来跟踪system资源的内部状态,并有机会创建、销毁这些资源,而不需要依赖单独的回调。
重点:当一个entity销毁时,SystemStateComponentData不会被删除。
DestroyEntity的行为是:
(1)找到所有引用entity ID的components;
(2)Delete这些components;
(3)回收entity ID用来复用;
但是,如果有SystemStateComponetData,它将不会被删除。
这就给了system用来清理entity ID相关联的资源或状态的机会。同时,也只有当这些SystemStateComponentData都被删除以后,entity ID才能够再次被复用。
设计意图:
(1)systems需要维护一个基于ComponentData的状态,比如资源分配;
(2)systems需要能够管理这个状态,尤其是当其它system修改了相关数据的某些值或状态的时候,比如,当compoents.values变更,或有相关联的components添加或删除的时候;
(3)“No callbakcs”没有回调,是ECS很重要的一个设计元素。
用法:
通常的用法是和用户自定义组件一对一,用来提供用户组建的状态跟踪。
比如,可以这样同时提供两个组件:
FooComponent(ComponentData,用户分配)
FooStateComponent (SystemComponentData,system分配)
检测组件添加:相当于FooComponent.OnAdded()
当用户add FooComponent,这个时候FooStateCompoent还没有添加。所以system可以Query有FooComponent但是没有FooStateComponent的entities,那么这些查询到的entities,就是新添加FooComponent的entities,在这个时间点上,就可以做一些相当于是OnAdded回调会做的事情,然后FooSystem为entity添加FooStateComponent,这样OnAdded就不会重复执行了。
检测组件移除:相当于FooComponent.OnRemove()
当用户remove FooComponent,这时候FooStateComponent不会被remove。所以system可以Query有FooStateComponent但没有FooComponent的entities,然后做一些OnRemove()相关的事情,同时FooSystem为entity remove FooStateComponent。
检测Entity销毁:
和remove component一样的方式。
注意:DestroyEntity以后,SystemStateComponentData并没有被移除,这时候entity id还不能被复用。
实现接口ISystemStateComponentData:
struct FooStateComponent : ISystemStateComponentData { } struct FooStateSharedComponent : ISystemStateSharedComponentData { public int Value; }
作为一个常用规范,在创建SystemStateComponentData的system外部都应该以ReadOnly方式来访问。
Dynamic Buffers
IBufferElementData && DynamicBuffer<T>提供类似动态数组的数据能力。如果不需要数据是动态长度,可以使用blob asset来代替dynamic buffer。
使用方式:
(1)定义一个struct,实现接口IBufferElementData,里面包含了需要存储在buffer中的元素;
(2)将该IBufferElementData像普通Component一样添加到Eneity(不是添加DynamicBuffer<T>对象);
(3)访问数据的方式和普通Component不一样,需要使用特定的函数返回该buffer的DynamicBuffer实例,然后使用该实例的接口操作数据(类似于array数据的方式);
(4)设置Capacity,小于该值的数据和普通Component一样存储在chunk里面,超过该值的值,会存储在heap上,ECS会自动维护这些内存数据;
[InternalBufferCapacity(8)] public struct FloatBufferElement : IBufferElementData { // Actual value each buffer element will store. public float Value; // The following implicit conversions are optional, but can be convenient. public static implicit operator float(FloatBufferElement e) { return e.Value; } public static implicit operator FloatBufferElement(float e) { return new FloatBufferElement {Value = e}; } } public class DynamicBufferExample : ComponentSystem { protected override void OnUpdate() { float sum = 0; Entities.ForEach((DynamicBuffer<FloatBufferElement> buffer) => { foreach (var element in buffer.Reinterpret<float>()) { sum += element; } }); Debug.Log("Sum of all buffers: " + sum); } }
给Entity添加Buffer的方式:
(1)通过Entity添加:
EntityManager.AddBuffer<MyBufferElement>(entity);
(2)通过ArcheType添加:
Entity e = EntityManager.CreateEntity(typeof(MyBufferElement));
(3)通过EntityCommandBuffer添加
struct DataSpawnJob : IJobForEachWithEntity<DataToSpawn> { public EntityCommandBuffer.Concurrent CommandBuffer; public void Execute(Entity spawnEntity, int index, [ReadOnly] ref DataToSpawn data) { for (int e = 0; e < data.EntityCount; e++) { Entity newEntity = CommandBuffer.CreateEntity(index); DynamicBuffer<MyBufferElement> buffer = CommandBuffer.AddBuffer<MyBufferElement>(index, newEntity); DynamicBuffer<int> intBuffer = buffer.Reinterpret<int>(); for (int j = 0; j < data.ElementCount; j++) { intBuffer.Add(j); } } CommandBuffer.DestroyEntity(index, spawnEntity); } }
EntityCommanBuffer.AddBuffer<T>(Entity):添加IBufferElementData;
EntityCommandBuffer.SetBuffer<T>(Entity):替换现有IBufferElementData,entity必须已有一个buffer。
只有在command buffer执行以后,这个buffer才能访问到。
访问Buffers:
可以使用以下方式来访问buffer:EntityManager,systems,jobs。
(1)EntityManager
DynamicBuffer<MyBufferElement> dynamicBuffer = EntityManager.GetBuffer<MyBufferElement>(entity);
(2)Component System Entities.ForEach
public class DynamicBufferSystem : ComponentSystem { protected override void OnUpdate() { var sum = 0; Entities.ForEach((DynamicBuffer<MyBufferElement> buffer) => { foreach (var integer in buffer.Reinterpret<int>()) { sum += integer; } }); Debug.Log("Sum of all buffers: " + sum); } }
访问其他entity的buffer data:
BufferFromEntity<MyBufferElement> lookup = GetBufferFromEntity<MyBufferElement>(); var buffer = lookup[entity]; buffer.Add(17); buffer.RemoveAt(0);
(3)Job
IJobForEach或IJobForEachWithEntity:
声明一个以BufferElement为模板类型的结构体:
public struct BuffersByEntity : IJobForEachWithEntity_EB<MyBufferElement>
但注意job.execute第三个参数为DynamicBuffer:
public void Execute(Entity entity, int index, DynamicBuffer<MyBufferElement> buffer)
IChunkJob && BufferAccessor<T>:
public struct BuffersInChunks : IJobChunk { //The data type and safety object public ArchetypeChunkBufferType<MyBufferElement> BufferType; //An array to hold the output, intermediate sums public NativeArray<int> sums; public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) { //A buffer accessor is a list of all the buffers in the chunk BufferAccessor<MyBufferElement> buffers = chunk.GetBufferAccessor(BufferType); for (int c = 0; c < chunk.Count; c++) { //An individual dynamic buffer for a specific entity DynamicBuffer<MyBufferElement> buffer = buffers[c]; foreach (MyBufferElement element in buffer) { sums[chunkIndex] += element.Value; } } } }
Buffer操作:
DynamicBuffer<int> intBuffer = EntityManager.GetBuffer<MyBufferElement>(entity).Reinterpret<int>();
int类型的buffer,也可以使用float类型来访问,只要内存大小一致即可。
Chunk Components
块数据组件,这个component属于一个特定的chunk,而不是chunk中的某个entity(也可以理解为属于chunk中所有的entities,但是只有一份实例)。
定义:没有专门的Interface,直接使用IComponentData,也就是通用Component。但是添加删除和操作这个Component的接口使用另外一套基于Chunk的。
ChunkComponent也会影响这个chunk中entities的archetype,所以添加和删除entity.chunkcomponent,也会导致entity移动到不同的chunk中。
创建:
注意:不能在job中添加chunkcomponent,也不能使用EntityCommndBuffer来添加。
// entity直接add EntityManager.AddChunkComponentData<ChunkComponentA>(entity);
// 使用EntityQuery EntityQueryDesc ChunksWithoutComponentADesc = new EntityQueryDesc() { None = new ComponentType[] {ComponentType.ChunkComponent<ChunkComponentA>()} }; ChunksWithoutChunkComponentA = GetEntityQuery(ChunksWithoutComponentADesc); EntityManager.AddChunkComponentData<ChunkComponentA>(ChunksWithoutChunkComponentA,ew ChunkComponentA() {Value = 4}); // 使用EntityArchetype ArchetypeWithChunkComponent = EntityManager.CreateArchetype( ComponentType.ChunkComponent(typeof(ChunkComponentA)), ComponentType.ReadWrite<GeneralPurposeComponentA>()); var entity = EntityManager.CreateEntity(ArchetypeWithChunkComponent);
// 使用components列表 ComponentType[] compTypes = {ComponentType.ChunkComponent<ChunkComponentA>(), ComponentType.ReadOnly<GeneralPurposeComponentA>()}; var entity = EntityManager.CreateEntity(compTypes);
读取:
// With the ArchetypeChunk instance var chunks = ChunksWithChunkComponentA.CreateArchetypeChunkArray(Allocator.TempJob); foreach (var chunk in chunks) { var compValue = EntityManager.GetChunkComponentData<ChunkComponentA>(chunk); //.. } chunks.Dispose(); // 直接使用entity if(EntityManager.HasChunkComponent<ChunkComponentA>(entity)) chunkComponentValue = EntityManager.GetChunkComponentData<ChunkComponentA>(entity);
// fluent query Entities.WithAll(ComponentType.ChunkComponent<ChunkComponentA>()).ForEach( (Entity entity) => { var compValue = EntityManager.GetChunkComponentData<ChunkComponentA>(entity); //... });
说明:一次修改一个chunk的chunk component效率会比单独修改一个entity的chunk component效率要高,因为不需要移动entity。
更新:
// With the ArchetypeChunk instance EntityManager.SetChunkComponentData<ChunkComponentA>(chunk, new ChunkComponentA(){Value = 7}); // 直接使用entity var entityChunk = EntityManager.GetChunk(entity); EntityManager.SetChunkComponentData<ChunkComponentA>(entityChunk, new ChunkComponentA(){Value = 8});
检查更新条件:何时chunk component的数据需要更新?
ChunkComponent的Version变化时,ECS会自动维护version的变更。
在JobComponentSystem中进行读写操作:
在IJobChunk中,使用传入的chunk参数,然后调用其GetChunkComponentData和SetChunkComponentData来进行数据读写:
[BurstCompile] struct ChunkComponentCheckerJob : IJobChunk { public ArchetypeChunkComponentType<ChunkComponentA> ChunkComponentATypeInfo; public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) { var compValue = chunk.GetChunkComponentData(ChunkComponentATypeInfo); //... var squared = compValue.Value * compValue.Value; chunk.SetChunkComponentData(ChunkComponentATypeInfo, new ChunkComponentA(){Value= squared}); } }
删除:
EntityManager.RemoveChunkComponent<T>(Entity)
Query chunk component:
ComponentType.ChunkComponent<T> ComponentType.ChunkComponentReadOnly<T> // In an EntityQueryDesc EntityQueryDesc ChunksWithChunkComponentADesc = new EntityQueryDesc() { All = new ComponentType[]{ComponentType.ChunkComponent<ChunkComponentA>()} }; // In an EntityQueryBuilder lambda function Entities.WithAll(ComponentType.ChunkComponentReadOnly<ChunkCompA>()) .ForEach((Entity ent) => { var chunkComponentA = EntityManager.GetChunkComponentData<ChunkCompA>(ent); });