Components
Component用来表示你的数据。Entity只是一个标识,用来将Component收集到一起。System提供行为。
实际上,Component是个struct结构体,这些结构体继承以下接口:
- IComponentData – 用于general purpose 及 chunk components。
- IBufferElementData – 用于为Entity创建dynamic buffer数据。
- ISharedComponentData – 用于在一个archetype内的entities根据值来分类或分组(共享同一个ISharedComponentData的Entity会被组织到一起)。
- ISystemStateComponentData – 用于标识Entity的特定系统状态,以及用来检测Entity的创建,销毁。
- ISharedSystemStatecomponentData – 组合了共享,及系统状态数据。
以上组件类型的描述,比较抽象,不明白可以继续,后面会针对每一种做介绍。
Entity Manager根据Entity上的组件的组合来定义Archetype,并按照Archetype组织entities。同一个Archetype的所有entities的components被存储在一个叫做chunk的内存区域。一个chunk中的所有的entities拥有相同的component archetype(组件原型)。
这张图片展示了chunk是如何根据archetype来存储组件的数据的。Shared components和chunk components不在这张图中,因为他们存储在chunk外。一个这种类型的数据对象,被所有适用的entities使用。此外,也可以在 chunk外存储dynamic buffers。尽管这些类型的components不被存储在chunk中,你依然可以用其它components的查询遍历方式访问它们。
General Purpose Components
Unity中的ComponentData(ECS中的component)对象,是个仅存储一个Entity数据的结构体。ComponentData不能包含方法,除了访问数据的函数。所有的游戏逻辑都应该在System中实现。这相当于是老的Unity中的Component,只是只包含变量。
ECS提供了 IComponentData,可以实现该接口。
IComponentData
传统的Unity components(包括MonoBehaviour)是面向对象的,包含了数据和方法。IComponentData是纯粹的ECS类型的组件,只有数据,没有方法。同时它是结构体,因此默认赋值是通过值拷贝,而不是引用。修改它的数据通常要像下面这样:
var transform = group.transform[index]; // 读取数据
transform.heading = playerInput.move; // 修改数据
transform.position += deltaTime * playerInput.move * settings.playerMoveSpeed;
group.transform[index] = transform; // 将数据写回去
IComponentData结构体不能持有托管对象的引用,因为所有的ComponentData都是创建在无垃圾回收的chunk memory上(不需要)。
Shared Component Data
Shared components 是一种特殊的数据组件,可以根据shared componet的不同数值,来将entities进一步细分。当将一个shared component添加到一个entity,Entity Manager会将所有共享该shared component(值相同,则是一个),存储到一个Chunk。Shared components允许你的系统一起处理处理相似的entities。例如,Rendering.RenderMesh,定义在Hybrid.rendering包中的shared component,还包括mesh, material, receiveShadows等。渲染时,同时处理拥有相同的该类组件的3D对象会大大提升效率。因为这些组件时shared components,所以Entity Manager会把匹配的entities放到同一个chunk中,这样进行遍历渲染时会更加有效率。
注意:过度使用shared component会导致Chunk利用率降低。因为这引入了Archetype和每个值的shared component之间的组合数量的急剧扩大,而每种组合都要分配Chunk,导致分配更多的内存。避免添加不是必须的shared components。可以利用Entity Debugger来查看当前的Chunk利用率。
向一个Entity添加,删除component,或者改变SharedComponent的值,Entity Manage都会将该Entity移动到其它匹配的Chunk,或者创建新的Chunk。
IComponentData通常适用于entities之间不同的数据,比如世界位置,打击点,粒子存活时间,等。ISharedComponentData适用于多个entities共享的数据。例如RenderMesh,所有实例化自同一个模型的对象,共享同一个RenderMesh。
[System.Serializable]
public struct RenderMesh : ISharedComponentData
{
public Mesh mesh;
public Material material;
public ShadowCastingMode castShadows;
public bool receiveShadows;
}
ISharedComponentData的最大好处,是每个对象在共享数据上的内存占用为0。
使用ISharedComponentData将使用同样的InstanceRenderer(渲染数据)的entities组织到一起,来更加高效地进行渲染。因为这些数据是线性排布的。
参考:RenderMeshSystemV2
Some important notes about SharedComponentData
使用SharedComponentData需要重点注意的几点:
- 引用同一个SharedComponentData的entities被组织到同一个Chunk中。存储SharedComponentData的索引,只在Chunk中保存,而不是在entities中保存。所以SharedComponentData对于entities的内存占用为0.
- 使用EntityQuery可以遍历所有相同类型的entities。
- 此外,可以通过调用EntityQuery.SetFilter()来遍历指定SharedComponentData的值的entities。基于数据的排布,这种遍历消耗并不高。
- 使用EntityManager.GetAllUniqueSharedComponents可以得到所有添加到entities上的唯一的SharedComponentData(其值唯一,没有变体)。(大概是这个意思,不太理解这句话)。
- SharedComponentData自动维护引用计数。
- SharedComponentData应该尽量少的改变。改变一个SharedComponentData会导致调用memcpy来将引用它的entities的Component Data拷贝到其它Chunk。
System State Components
SystemStateComponentData的作用,是跟踪资源在系统内部的状态,以提供机会在适当的时机创建和销毁资源,而不是依靠某个回调函数。
SystemStateComponentData和SystemStateSharedComponentData跟ComponentData以及SharedComponentData一样,各自都有一个重要的概念:
SystemStateComponentData在销毁entity时,不会被销毁。
销毁的简单流程如下:
找到引用该entity ID的所有的Component Data
删除这些components
回收entity ID,以重复使用。
然而,如果entity有SystemStateComponentData,它不会被移除。这给system机会来清理该entity ID的资源以及相关状态。只有当SystemStateComponentData被移除后,entity ID才能被重复使用。
Motivation
目的
- System可能需要维持基于ComponentData的一个内部状态。例如,资源是否分配。
- System需要能管理由其它系统对该值或状态的更新。例如,值改变了,或者相关组件添加或删除。
- “无回调”,时ECS设计准则的重要概念。
Concept
一个用法是镜像一个用户的组件的内部状态。
例如:
- FooComponent(ComponentData,用户创建)
- FooStateComponent(SystemComponentData,系统创建)
Detecting Component Add
监测添加组件
当添加FooComponent时,FooStateComponent还不存在。Foo System查询到添加了FooComponent但是没有FooStateComponent,则可以推断出该FooComponent是新添加的。同时Foo System会添加FooStateComponent及其它需要的内部状态。
Detecting Component Remove
检测删除组件
当删除FooComponent组件时,FooStateComponent依然存在。Foo System更新时发现有FooStateComponent但是没有FooComponent,则可以推断出FooComponent被删除了。这时Foo System会删除FooStateComponent并根据需要恢复其它内部状态。
Detecting Destroy Entity
监测销毁实体
实体的销毁,可以简化为步骤:
- 查找到引用该entity ID的所有的components
- 删除这些components
- 回收 entity ID
然而,调用Destroy Entity时,SystemStateComponentData没有被移除,entity ID也不会被回收,直到最后一个组件被删除。这让系统可以用与删除组件相同的方式,清理内部状态。
SystemStateComponent
SystemStateComponent和ComponentData类似,用法也类似:
struct FooStateComponent : ISystemStateComponent
{
}
对于成员,也可以用public,private,protected来修饰可访问性。但是,我们最好在创建该组件的系统内更新,改变它的值,而在该系统之外,是只读的。
SystemStateSharedComponent
SystemStateSharedComponent与SharedComponentData用法类似:
struct FooStateSharedComponent : ISystemStateSharedComponentData
{
public int Value;
}
Example system using state components
下面的例子,用一个简单的系统,展示了如何利用system state component来管理entities。例子定义了一个普通的IComponentData和它的ISystemStateComponentData实例,还定义了三个对该类entities的query查询:
- m_newEntities 选择有普通component但是没有system state component的entities,这些entities是新创建的。系统执行job,为它们添加system state component。
- m_activeEneities选择同时有component和system state component的entities。在实际应用中,其它系统也可能会处理或者销毁这些entities。
- m_destroyedEntities选择了有system state component但是没有component的entities,这些实体是被本系统,或者其它系统删除的entities。该系统运行一个job将system state component从entities上删除,以便ECS可以回收该entity ID。
注意我们的这个简化的例子,并没有处理任何状态,system state component的一个作用就是跟踪资源的分配和清理。
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;
public struct GeneralPurposeComponentA : IComponentData
{
public bool IsAlive;
}
public struct StateComponentB : ISystemStateComponentData
{
public int State;
}
public class StatefulSystem : JobComponentSystem
{
private EntityQuery m_newEntities;
private EntityQuery m_activeEntities;
private EntityQuery m_destroyedEntities;
private EntityCommandBufferSystem m_ECBSource;
protected override void OnCreate()
{
// Entities with GeneralPurposeComponentA but not StateComponentB
m_newEntities = GetEntityQuery(new EntityQueryDesc()
{
All = new ComponentType[] {ComponentType.ReadOnly<GeneralPurposeComponentA>()},
None = new ComponentType[] {ComponentType.ReadWrite<StateComponentB>()}
});
// Entities with both GeneralPurposeComponentA and StateComponentB
m_activeEntities = GetEntityQuery(new EntityQueryDesc()
{
All = new ComponentType[]
{
ComponentType.ReadWrite<GeneralPurposeComponentA>(),
ComponentType.ReadOnly<StateComponentB>()
}
});
// Entities with StateComponentB but not GeneralPurposeComponentA
m_destroyedEntities = GetEntityQuery(new EntityQueryDesc()
{
All = new ComponentType[] {ComponentType.ReadWrite<StateComponentB>()},
None = new ComponentType[] {ComponentType.ReadOnly<GeneralPurposeComponentA>()}
});
m_ECBSource = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
}
struct NewEntityJob : IJobForEachWithEntity<GeneralPurposeComponentA>
{
public EntityCommandBuffer.Concurrent ConcurrentECB;
public void Execute(Entity entity, int index, [ReadOnly] ref GeneralPurposeComponentA gpA)
{
// Add an ISystemStateComponentData instance
ConcurrentECB.AddComponent<StateComponentB>(index, entity, new StateComponentB() {State = 1});
}
}
struct ProcessEntityJob : IJobForEachWithEntity<GeneralPurposeComponentA>
{
public EntityCommandBuffer.Concurrent ConcurrentECB;
public void Execute(Entity entity, int index, ref GeneralPurposeComponentA gpA)
{
// Process entity, possibly setting IsAlive false --
// In which case, destroy the entity
if (!gpA.IsAlive)
{
ConcurrentECB.DestroyEntity(index, entity);
}
}
}
struct CleanupEntityJob : IJobForEachWithEntity<StateComponentB>
{
public EntityCommandBuffer.Concurrent ConcurrentECB;
public void Execute(Entity entity, int index, [ReadOnly] ref StateComponentB state)
{
// This system is responsible for removing any ISystemStateComponentData instances it adds
// Otherwise, the entity is never truly destroyed.
ConcurrentECB.RemoveComponent<StateComponentB>(index, entity);
}
}
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var newEntityJob = new NewEntityJob()
{
ConcurrentECB = m_ECBSource.CreateCommandBuffer().ToConcurrent()
};
var newJobHandle = newEntityJob.ScheduleSingle(m_newEntities, inputDependencies);
m_ECBSource.AddJobHandleForProducer(newJobHandle);
var processEntityJob = new ProcessEntityJob()
{ConcurrentECB = m_ECBSource.CreateCommandBuffer().ToConcurrent()};
var processJobHandle = processEntityJob.Schedule(m_activeEntities, newJobHandle);
m_ECBSource.AddJobHandleForProducer(processJobHandle);
var cleanupEntityJob = new CleanupEntityJob()
{
ConcurrentECB = m_ECBSource.CreateCommandBuffer().ToConcurrent()
};
var cleanupJobHandle = cleanupEntityJob.ScheduleSingle(m_destroyedEntities, processJobHandle);
m_ECBSource.AddJobHandleForProducer(cleanupJobHandle);
return cleanupJobHandle;
}
protected override void OnDestroy()
{
// Implement OnDestroy to cleanup any resources allocated by this system.
// (This simplified example does not allocate any resources.)
}
}
Dynamic Buffer Components
Dynamic Buffers
DynamicBuffer是一种支持可变大小的弹性buffer component data,可以存放一定数量的元素数据。如果空间不足会分配堆内存块来扩充。
该方法的内存管理是自动的。DynamicBuffer的内存是由EntityManager管理的,所以当DynamicBuffer component删除时,其内存也会被释放。
Fixed Array 固定长度数组已经被Dynamicbuffer替代并移除。
Declaring Buffer Element Types
声明元素类型
需要用指定的类型来声明buffer:
// This describes the number of buffer elements that should be reserved
// in chunk data for each instance of a buffer. In this case, 8 integers
// will be reserved (32 bytes) along with the size of the buffer header
// (currently 16 bytes on 64-bit targets)
[InternalBufferCapacity(8)]
public struct MyBufferElement : IBufferElementData
{
// These implicit conversions are optional, but can help reduce typing.
public static implicit operator int(MyBufferElement e) { return e.Value; }
public static implicit operator MyBufferElement(int e) { return new MyBufferElement { Value = e }; }
// Actual value each buffer element will store.
public int Value;
}
以上看起来是定义了一个元素类型,而不是一个buffer,这种设计由2个好处:
- 通过派生IBufferElementData,我们可以支持更多类型的buffer。而且这些类型可以有更多的数据成员。
- 我们可以将buffer定义到EntityArchetype原型中,就像一个comopent。
Adding Buffer Types To Entities
添加buffer
调用方法AddBuffer():
entityManager.AddBuffer<MyBufferElement>(entity);
利用原型:
Entity e = entityManager.CreateEntity(typeof(MyBufferElement));
Accessing Buffers
有多种方法可以访问buffers,
直接在主线程访问:
DynamicBuffer<MyElementBuffer> buffer = entityManager.GetBuffer<MyElementBuffer>(entity);
基于Entity访问
可以在JobComponentSystem中基于每个entity进行访问
var lookup = GetBufferFromEntity<MyBufferElement>();
var buffer = lookup[myEntity];
buffer.Append(7);
buffer.RemoveAt(0);
Reinterpreting Buffers (Experimental)
Buffer类型强制转换(实验特性)
Buffer可以被强制转换成长度相同的类型的Buffer:
var intBuffer = entityManager.GetBuffer<MyBufferElement>().Reinterpret<int>();
将MyBufferElement类型的Buffer,转换成int类型。因为它们的长度一致。
需要注意的是,因为没有类型检查,所以转成float也不会报错,但是操作数据会产生不可预料的结果。
Chunk Components
Chunk component data
可以使用chunk components将特定的chunk(内的entities)和data关联起来。
Chunk components包含的数据,将应用到指定chunk中的所有entities上。例如,如果有一些表示3D对象的entities(它们在一个或者及个chunk里),你可以将它们的bounding box,存储到一个chunk component里。
接口:IComponentData
Chunk components是chunk内的entities的archetype原型的一部分。所以当向一个entity添加,或者删除chunk component时,该entity会被移动到其它的chunk,因为它的archetype改变了。当然,该改变不会作用到该chunk的其它entity上。
如果在访问entity时改变了chunk component的值,那么,该改变将应用到该entity chunk上的所有的entities(其实是因为chunk component data是共享的)。如果为一个entity添加了一个chunk component改变了它的archetype,导致该entity被移动到一个已有的chunk中,不会改变新的chunk中的chunk component data的值。如果entity是被移动到了一个新创建的chunk总,则该新chunk中的chunk component data 保留第一个entity的值。
使用ComponentData 和Chunk Component Data之间,主要的区别,是添加,设置,移除时调用的接口不同。Chunk component也由相应的ComponentType函数,用来定义entity archetype和queries。
相关的APIs:
Purpose | Function |
Declaration | IComponentData |
| ArchetypeChunk methods |
Read | GetChunkComponentData(ArchetypeChunkComponentType) |
Check | HasChunkComponent(ArchetypeChunkComponentType) |
Write | SetChunkComponentData(ArchetypeChunkComponentType, T) |
| EntityManager methods |
Create | AddChunkComponentData(Entity) |
Create | AddChunkComponentData(EntityQuery, T) |
Create | AddComponents(Entity,ComponentTypes) |
Get type info | GetArchetypeChunkComponentType(Boolean) |
Read | GetChunkComponentData(ArchetypeChunk) |
Read | GetChunkComponentData(Entity) |
Check | HasChunkComponent(Entity) |
Delete | RemoveChunkComponent(Entity) |
Delete | RemoveChunkComponentData(EntityQuery) |
Write | EntityManager.SetChunkComponentData(ArchetypeChunk, T) |
Declaring a chunk component
声明IComponentData来定义chunk components。
public struct ChunkComponentA : IComponentData
{
public float Value;
}
Creating a chunk component
可以直接添加chunk component,利用目标chunk里的一个entity或者利用entity query选择一组目标chunks。不能在Job内添加Chunk components,也不能用EntityCommandBuffer创建。
还可以将chunk component作为EntityArchetype的一部分,或者添加到ComponentType list中来创建entities的同时,为存储该原型的entities chunk创建chunk component,类型参数定义为:ComponentType.ChunkComponent<T>或者ComponentType.ChunkComponentReadOnly<T>。
用目标chunk的一个entity创建:
EntityManager.AddChunkComponentDat<ChunkComponentA>(oneEntity);
用这种方法,不能马上为chunk component设置值。
用EntityQuery创建:
用给定的entity query选择你想要添加chunk component的所有的entity chunk,并用EntityManager.AddChunkComponentData<T>()方法
EntityQueryDesc ChunksWithoutComponentADesc = new EntityQueryDesc()
{
None = new ComponentType[] {ComponentType.ChunkComponent<ChunkComponentA>()}
};
ChunksWithoutChunkComponentA = GetEntityQuery(ChunksWithoutComponentADesc);
EntityManager.AddChunkComponentData<ChunkComponentA>(ChunksWithoutChunkComponentA,
new ChunkComponentA() {Value = 4});
用这种方法,可以为所有的chunk创建chunk components并用同样的值进行初始化。
用EntityArchetype:
用archetype或者component type列表创建entities时,将chunk component类型添加到archetype中。
ArchetypeWithChunkComponent = EntityManager.CreateArchetype(
ComponentType.ChunkComponent(typeof(ChunkComponentA)),
ComponentType.ReadWrite<GeneralPurposeComponentA>());
var entity = EntityManager.CreateEntity(ArchetypeWithChunkComponent);
或者component type列表:
ComponentType[] compTypes = {ComponentType.ChunkComponent<ChunkComponentA>(),
ComponentType.ReadOnly<GeneralPurposeComponentA>()};
var entity = EntityManager.CreateEntity(compTypes);
用上面这些方法,如果创建新的entity时创建了新的chunk,则新创建的chunk component是默认值。如果是已经存在的chunk components(只是改变了现有entity的archetype导致移动到新的chunk时),其值不会改变。
Reading a chunk component
可以用目标chunk的一个entity,或者chunk的ArchetypeChunk对象来访问chunk component。
用chunk中的entity: EntityManager.GetChunkComponentData<T>:
if(EntityManager.HasChunkComponent<ChunkComponentA>(entity))
chunkComponentValue = EntityManager.GetChunkComponentData<MyChunkComponent>(entity);
可以用一下方法选择所有特定的entities来访问:
Entities.WithAll(ComponentType.ChunkComponent<MyChunkComponent>().ForEach(
(Entity entity_=>
{
var compValue = EntityManger.GetChunkComponentData<MyChunkComponent>(entity);
}
需要注意的是,不能直接将chunk component传递给query的for-each逻辑,而应该传递Entity对象,并通过EntityManger来访问chunk component。
用ArchetypeChunk实例
给定chunk,可以通过调用EntityManger.GetChunkComponentData<T>来访问chunk component。下面的例子,遍历了所有匹配query的chunks并访问它们的chunk component:ChunkComponentA
var chunks = ChunksWithChunkComponentA.CreateArchetypeChunkArray(Allocator.TempJob);
foreach (var chunk in chunks)
{
var compValue = EntityManager.GetChunkComponentData<ChunkComponentA>(chunk);
//..
}
chunks.Dispose();
Updating a chunk component
可以更新给定的chunk的chunk component。在IJobChunk Job里,可以调用ArchetypeChunk.SetChunkComponentData。在主线程内,可以调用EntityManager.SetChunkComponentData。需要注意的,不能再IJobForEach内访问chunk components,因为不能访问ArchetypeChunk和EntityManager。
用ArchetypeChunk实例:
ArchetypeChunk chunk;
EntityManager.SetChunkComponentData<ChunkComponentA>(chunk,
new ChunkComponentA({Value=7});
用entity:
Entity entity;
EntityManger.SetChunkComponentData<ChunkComponentA>(entity,
new ChunkComponentA({Value=8});
Reading and writing in a JobComponentSystem
在JobComponentSystem内的IJobChunk,可以将chunk作为参数传递给IJobChunk的Execute方法,来访问chunk components。像IJobChunk Job的任何componen data一样,需要将ArchetypeChunkComponentType<T>对象作为参数,传递给IJobChunk 的数据成员来访问component。
下面的系统定义了一个Query,来选择包含ChunkComponentA的所有的entities和chunks。然后用一个IJobChunk来遍历chunks并访问每个chunk components。Job用ArchetypeChunk的GetChunkComponentData和SetChunkComponentData来读写chunk component data。
using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
public class ChunkComponentChecker : JobComponentSystem
{
private EntityQuery ChunksWithChunkComponentA;
protected override void OnCreate()
{
EntityQueryDesc ChunksWithComponentADesc = new EntityQueryDesc()
{
All = new ComponentType[]{ComponentType.ChunkComponent<ChunkComponentA>()}
};
ChunksWithChunkComponentA = GetEntityQuery(ChunksWithComponentADesc);
}
[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});
}
}
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var job = new ChunkComponentCheckerJob()
{
ChunkComponentATypeInfo = GetArchetypeChunkComponentType<ChunkComponentA>()
};
return job.Schedule(ChunksWithChunkComponentA, inputDependencies);
}
}
如果只需要读取,而不写数据,则用ComponentType.ChunkComponentReadOnly,可以提高效率。
Deleting a chunk component
调用EntityManager.RemoveChunkComponent方法来删除chunk component。可以移除指定entity的chunk component,或通过entity query来选择chunks,并移除所有chunks的chunk components。如果删除一个entity的chunk component,该entity会被移动到其它chunk,因为它的archetype改变了。
Using a chunk component in a query
在query中使用chunk component,需要用ComponentType.ChunkComponent<T>或者ComponentType.ChunkComponentReadOnly<T>来指定类型。
EntityQueryDesc
下面的query描述,可以用来创建entity query 来选择所有包含ChunkComponentA的chunks以及entities。
EntityQueryDesc ChunkWithChunkComponentADesc = new EntityQueryDesc()
{
All = new ComponentType[]{ComponentType.ChunkComponent<ChunkComponentA>()}
}
EntityQueryBuilder lambda函数:
下面的query遍历所有entities
Entities.WithAll(ComponentType.ChunkComponentReadOnly<ChunkCompA>())
.ForEach((Entity ent)=>
{
var chunkComponentA = EntityManager.GetChunkComponentData<ChunkCmpA>(ent);
}
);
注:不能将一个chunk component直接作为lambda的参数,只能通过传递entity,用ComponentSystem.EntityManager来访问chunk components。改变chunk component的值,会改变该chunk的所有的entities的值,不会导致entity移动。