Shared component data(共享组件数据)
共享组件是一种特殊的数据组件,你可以用它来根据共享组件中的特定值(除了它们的原型之外)对实体进行细分。当你给一个实体添加共享组件时,EntityManager会将所有具有相同共享数据值的实体放入同一个块中。
共享组件允许你的系统一起处理类似的实体。例如,共享组件Rendering.RenderMesh是Hybrid.rendering包的一部分,它定义了几个字段,包括网格、材质和接收阴影。当你的应用程序渲染时,最有效的方法是一起处理所有对这些字段有相同值的3D对象。因为一个共享组件指定了这些属性,所以EntityManager将匹配的实体一起放在内存中,这样渲染系统就可以有效地迭代它们。
NOTE
如果你过度使用共享组件,可能会导致块利用率低下。这是因为当你使用共享组件时,它涉及到基于原型和每个共享组件字段的每个独特值的内存块数的组合扩展。因此,避免在共享组件中添加任何不需要将实体分类的字段。要查看块的利用率,请使用Entity Debugger。
如果你从一个实体中添加或删除一个组件,或者改变一个共享组件的值,The EntityManager会将该实体移动到一个不同的块中,如果有必要,还会创建一个新的块。
你应该使用IComponentData来处理实体之间不同的数据,例如存储世界的位置、agent的命中率或粒子的生存时间。相反,当很多实体有共同的东西时,你应该使用ISharedComponentData。例如,在DOTS包中的Boids演示中,许多实体从同一个Prefab实例化出来,因此,许多Boid实体之间的RenderMesh是完全一样的。
[System.Serializable]
public struct RenderMesh : ISharedComponentData
{
public Mesh mesh;
public Material material;
public ShadowCastingMode castShadows;
public bool receiveShadows;
}
ISharedComponentData在每个实体的基础上没有内存成本。可以使用ISharedComponentData将具有相同InstanceRenender数据的所有实体组合在一起,然后高效地提取所有矩阵进行渲染。生成的代码简单而高效,因为数据是在ECS访问数据时布局的。
有关这种情况的示例,请参阅RenderMeshSystemV2文件Packages/com.unity.entities/Unity.Rendering.Hybrid/RenderMeshSystemV2.cs.
有关SharedComponentData的重要说明:
-
ECS将具有相同SharedComponentData的实体分组在同一块中。它对SharedComponentData的索引在每个块中存储一次,而不是每个实体。因此,SharedComponentData在每个实体上的内存开销为零。
-
你可以使用EntityQuery来遍历所有具有相同类型的实体。你也可以使用EntityQuery.SetFilter()来专门迭代具有特定SharedComponentData值的实体。由于数据布局的原因,这种迭代的开销很低。
-
你可以使用EntityManager.GetAllUniqueSharedComponents来检索所有添加到任何活着的实体中的唯一SharedComponentData。
-
ECS自动引用计数SharedComponentData。
-
SharedComponentData应该很少改变。如果你想改变SharedComponentData,需要使用memcpy将该实体的所有ComponentData复制到不同的块中。
System State Components(系统状态组件)
您可以使用SystemStateComponentData跟踪 system 内部的资源,并根据需要创建和销毁这些资源,而无需依赖单个回调。
SystemStateComponentData和SystemStateSharedComponentData与ComponentData和SharedComponentData类似,但ECS在实体被销毁时不会删除SystemStateComponentData。
当实体被销毁时,ECS通常:
- 查找引用特定实体ID的所有组件。
- 删除这些组件。
- 回收实体ID以供重复使用。
然而,如果SystemStateComponentData存在,ECS不会回收该ID。这给了系统一个机会来清理与实体ID相关的任何资源或状态。ECS只有在SystemStateComponentData被移除后才会重新使用该实体ID。
When to use system state components(何时使用系统状态组件)
系统可能需要在ComponentData的基础上保持一个内部状态。例如,资源可能被分配。
系统还需要能够将状态作为数值进行管理,其他系统可能会进行状态的改变。例如,当组件中的值发生变化,或者相关组件被添加或删除时。
"不回调 "是ECS设计规则的一个重要元素。
SystemStateComponentData的一般使用被期望反映一个用户组件,提供内部状态。
例如,假设:
- FooComponent(ComponentData,用户分配)
- FooStateComponent(SystemComponentData,系统分配)
检测何时添加组件
当你创建一个组件时,系统状态组件并不存在。系统更新查询没有系统状态组件的组件,并可以推断出它们已经被添加。这时,系统会添加一个系统状态组件和任何需要的内部状态。
检测何时删除组件
当你删除一个组件时,系统状态组件仍然存在。系统更新对没有组件的系统状态组件的查询,可以推断出它们已经被删除。在这一点上,系统删除了系统状态组件,并修复了任何需要的内部状态。
检测实体何时被销毁
DestroyEntity是一种速记工具,用于:
- 查找引用给定实体ID的组件。
- 删除找到的组件。
- 回收实体ID。
然而,在DestroyEntity时,SystemStateComponentData并没有被删除,实体ID也不会被回收,直到最后一个组件被删除。这让系统有机会以与删除组件完全相同的方式来清理内部状态。
SystemStateComponent
一个SystemStateComponentData与一个ComponentData类似。
struct FooStateComponent : ISystemStateComponentData
{
}
SystemStateComponentData的可见性也可以用与组件相同的方式来控制(使用private、public、internal)。然而,一般来说,SystemStateComponentData在创建它的系统之外应该是只读的。
SystemStateSharedComponent
SystemStateSharedComponentData类似于SharedComponentData。
struct FooStateSharedComponent : ISystemStateSharedComponentData
{
public int Value;
}
Example system using state components(使用状态组件的示例系统)
下面的例子展示了一个简化的System,说明了如何用system state components来管理实体。这个例子定义了一个通用的IComponentData实例和一个系统状态,ISystemStateComponentData实例。它还定义了三个基于这些实体的查询。
-
m_newEntities 选择具有通用的,但不具有system state component的实体。这个查询找到了System以前没有见过的新实体。System使用新的实体查询运行一个Job,增加system state component。
-
m_activeEntities 选择同时具有通用和system state component的实体。在实际应用中,其他System可能是处理或销毁实体的System。
-
m_delestyedEntities 选择具有system state,但不具有通用组件的实体。由于system state component本身从未被添加到实体中,所以这个查询所选择的实体一定已经被删除了,可能是被这个system或其他system删除了。system重用被销毁的实体查询来运行一个Job,并从实体中删除system state component,这使得ECS代码可以回收实体标识符。
NOTE
这个简化的例子并没有维护系统内的任何状态。system state components 的一个目的是跟踪持久性资源何时需要被分配或清理。
using Unity.Entities;
using Unity.Jobs;
using Unity.Collections;
public struct GeneralPurposeComponentA : IComponentData
{
public int Lifetime;
}
public struct StateComponentB : ISystemStateComponentData
{
public int State;
}
public partial class StatefulSystem : SystemBase
{
private EntityCommandBufferSystem ecbSource;
protected override void OnCreate()
{
ecbSource = World.GetExistingSystem<EndSimulationEntityCommandBufferSystem>();
// 创建一些测试实体
// 这在主线程上运行,但使用命令缓冲区仍然更快。
EntityCommandBuffer creationBuffer = new EntityCommandBuffer(Allocator.Temp);
EntityArchetype archetype = EntityManager.CreateArchetype(typeof(GeneralPurposeComponentA));
for (int i = 0; i < 10000; i++)
{
Entity newEntity = creationBuffer.CreateEntity(archetype);
creationBuffer.SetComponent<GeneralPurposeComponentA>
(
newEntity,
new GeneralPurposeComponentA() { Lifetime = i }
);
}
//执行命令缓冲区
creationBuffer.Playback(EntityManager);
}
protected override void OnUpdate()
{
EntityCommandBuffer.ParallelWriter parallelWriterECB = ecbSource.CreateCommandBuffer().AsParallelWriter();
// 具有GeneralPurposeComponentA但没有StateComponentB的实体
Entities
.WithNone<StateComponentB>()
.ForEach(
(Entity entity, int entityInQueryIndex, in GeneralPurposeComponentA gpA) =>
{
// 添加一个ISystemStateComponentData实例
parallelWriterECB.AddComponent<StateComponentB>
(
entityInQueryIndex,
entity,
new StateComponentB() { State = 1 }
);
})
.ScheduleParallel();
ecbSource.AddJobHandleForProducer(this.Dependency);
// 创建新的命令缓冲区
parallelWriterECB = ecbSource.CreateCommandBuffer().AsParallelWriter();
// 同时拥有GeneralPurposeComponentA和StateComponentB的实体
Entities
.WithAll<StateComponentB>()
.ForEach(
(Entity entity,
int entityInQueryIndex,
ref GeneralPurposeComponentA gpA) =>
{
// 处理实体,在这种情况下,是通过递减生命期计数来实现的
gpA.Lifetime--;
// 如果没有时间了,就销毁这个实体
if (gpA.Lifetime <= 0)
{
parallelWriterECB.DestroyEntity(entityInQueryIndex, entity);
}
})
.ScheduleParallel();
ecbSource.AddJobHandleForProducer(this.Dependency);
// 创建新的命令缓冲区
parallelWriterECB = ecbSource.CreateCommandBuffer().AsParallelWriter();
// 拥有StateComponentB但没有GeneralPurposeComponentA的实体
Entities
.WithAll<StateComponentB>()
.WithNone<GeneralPurposeComponentA>()
.ForEach(
(Entity entity, int entityInQueryIndex) =>
{
// 该系统负责删除其添加的任何ISystemStateComponentData实例
// 否则,该实体永远不会被真正摧毁。
parallelWriterECB.RemoveComponent<StateComponentB>(entityInQueryIndex, entity);
})
.ScheduleParallel();
ecbSource.AddJobHandleForProducer(this.Dependency);
}
protected override void OnDestroy()
{
// 实现OnDestroy,清理本系统分配的任何资源。
// (这个简化的例子没有分配任何资源,所以没有什么需要清理的。)
}
}