Dynamic buffer components(动态缓冲区组件)
使用动态缓冲区组件将类似数组的数据与一个实体联系起来。动态缓冲区是ECS组件,可以容纳可变数量的元素,并在必要时自动调整大小。
要创建一个动态缓冲区,首先声明一个实现IBufferElementData的结构,并定义存储在缓冲区的元素。例如,你可以使用以下结构来创建一个存储整数的缓冲区组件:
public struct IntBufferElement : IBufferElementData
{
public int Value;
}
要将一个动态缓冲区与一个实体联系起来,请直接向实体添加一个IBufferElementData组件,而不是添加动态缓冲区容器本身。
ECS管理着这个容器。在大多数情况下,你可以使用已声明的IBufferElementData类型来处理动态缓冲区,与任何其他ECS组件一样。例如,你可以在实体查询中使用IBufferElementData类型,也可以在添加或删除缓冲器组件时使用。然而,你必须使用不同的函数来访问缓冲区组件,这些函数提供了DynamicBuffer实例,它为缓冲区数据提供了一个类似数组的接口。
要为一个动态缓冲区组件指定一个 “内部容量”,请使用InternalBufferCapacity属性。内部容量定义了动态缓冲区与实体的其他组件一起存储在ArchetypeChunk中的元素的数量。如果你增加的缓冲区的大小超过了内部容量,缓冲区会在当前chunk之外分配一个堆内存块,并移动所有现有的元素。ECS会自动管理这个外部缓冲区内存,并在缓冲区组件被移除时释放该内存。
NOTE
如果缓冲区中的数据不是动态的,你可以使用blob assets来代替动态缓冲区。Blob assets可以存储结构化的数据,包括数组。多个实体可以共享blob assets。
Declaring buffer element types(声明缓冲区元素类型)
若要声明缓冲区,请声明一个struct,该struct定义要放入缓冲区的元素类型。该结构必须实现IBufferElementData,如下所示:
// 内部缓冲区容量(InternalBufferCapacity)指定一个缓冲区可以有多少个元素,然后才是
// 缓冲区的存储被移到块之外。
[InternalBufferCapacity(8)]
public struct MyBufferElement : IBufferElementData
{
// 每个缓冲区元素将存储的实际值。
public int Value;
// 以下的隐式转换是可选的,但可能很方便。
public static implicit operator int(MyBufferElement e)
{
return e.Value;
}
public static implicit operator MyBufferElement(int e)
{
return new MyBufferElement { Value = e };
}
}
Adding buffer types to entities(向实体添加缓冲区类型)
要将缓冲区添加到实体中,请添加定义缓冲区元素的数据类型的IBufferElementData struct,然后将该类型直接添加到实体或原型中:
使用EntityManager.AddBuffer()
有关详细信息,请参阅EntityManager.AddBuffer()上的文档。
EntityManager.AddBuffer<MyBufferElement>(entity);
使用原型
Entity e = EntityManager.CreateEntity(typeof(MyBufferElement));
使用 [GenerateAuthoringComponent] 特性
你可以使用 [GenerateAuthoringComponent] 来为只包含一个字段的简单IBufferElementData实现生成 authoring components。设置这个属性可以让你在GameObject上添加ECS IBufferElementData组件,这样你就可以在编辑器中设置缓冲区元素。
例如,如果你声明了以下类型,你可以在编辑器中直接将其添加到一个GameObject上:
[GenerateAuthoringComponent]
public struct IntBufferElement: IBufferElementData
{
public int Value;
}
在后台,Unity生成了一个名为IntBufferElementAuthoring的类(它继承自MonoBehaviour),它暴露了一个List类型的公共字段。当包含这个生成的authoring component的GameObject被转换为实体时,该List被转换为DynamicBuffer,然后被添加到转换后的实体。
请注意以下限制:
- 在一个C#文件中,只有一个组件可以有一个生成的authoring component,而且该C#文件中不能有另一个MonoBehaviour。
- 对于包含一个以上字段的类型,不能自动生成IBufferElementData编写组件。
- 对于有明确布局的类型,IBufferElementData编写组件不能自动生成。
使用EntityCommandBuffer
当你向实体命令缓冲区添加命令时,你可以添加或设置一个缓冲区组件。
使用AddBuffer为实体创建一个新的缓冲区,从而改变实体的原型。使用SetBuffer来清除现有的缓冲区(必须存在),并在其位置上创建一个新的、空的缓冲区。这两个函数都返回一个DynamicBuffer实例,你可以用它来填充新的缓冲区。你可以立即向缓冲区添加元素,但是在执行命令缓冲区时,在缓冲区被添加到实体之前,这些元素是无法被访问的。
下面这个Job使用命令缓冲区创建了一个新的实体,然后使用EntityCommandBuffer.AddBuffer添加了一个动态缓冲区组件。该Job还向动态缓冲区添加了一些元素。
using Unity.Entities;
using Unity.Jobs;
public partial class CreateEntitiesWithBuffers : SystemBase
{
// 命令缓冲区系统在其自己的OnUpdate中执行命令缓冲区
public EntityCommandBufferSystem CommandBufferSystem;
protected override void OnCreate()
{
// 获取命令缓冲系统
CommandBufferSystem
= World.DefaultGameObjectInjectionWorld.GetExistingSystem<EndSimulationEntityCommandBufferSystem>();
}
protected override void OnUpdate()
{
// 用于记录命令的命令缓冲器,
// 在帧的后面由命令缓冲器系统执行
EntityCommandBuffer.ParallelWriter commandBuffer
= CommandBufferSystem.CreateCommandBuffer().AsParallelWriter();
//DataToSpawn组件告诉我们要创建多少个带缓冲区的实体
Entities.ForEach((Entity spawnEntity, int entityInQueryIndex, in DataToSpawn data) =>
{
for (int e = 0; e < data.EntityCount; e++)
{
//为命令缓冲区创建新实体
Entity newEntity = commandBuffer.CreateEntity(entityInQueryIndex);
//创建动态缓冲区并将其添加到新实体
DynamicBuffer<MyBufferElement> buffer =
commandBuffer.AddBuffer<MyBufferElement>(entityInQueryIndex, newEntity);
//重新解释为纯int缓冲区
DynamicBuffer<int> intBuffer = buffer.Reinterpret<int>();
//可选, 填充动态缓冲区
for (int j = 0; j < data.ElementCount; j++)
{
intBuffer.Add(j);
}
}
//销毁DataToSpawn实体,因为它已完成其工作
commandBuffer.DestroyEntity(entityInQueryIndex, spawnEntity);
}).ScheduleParallel();
CommandBufferSystem.AddJobHandleForProducer(this.Dependency);
}
}
NOTE
你不需要立即向动态缓冲区添加数据。但是,在你使用的实体命令缓冲区被执行后,你才可以再次访问该缓冲区。
访问缓冲区
你可以使用EntityManager、System和Job来访问DynamicBuffer实例,其方式与访问其他组件类型的实体大致相同。
EntityManager
你可以使用EntityManager的一个实例来访问一个动态缓冲区:
DynamicBuffer<MyBufferElement> dynamicBuffer
= EntityManager.GetBuffer<MyBufferElement>(entity);
Looking up buffers of another entity(查找另一实体的缓冲区)
当你需要在Job中查询属于另一个实体的缓冲区数据时,你可以向Job传递一个BufferFromEntity变量。
BufferFromEntity<MyBufferElement> lookup = GetBufferFromEntity<MyBufferElement>();
var buffer = lookup[entity];
buffer.Add(17);
buffer.RemoveAt(0);
SystemBase Entities.ForEach
你可以通过传递缓冲区作为你的lambda函数参数之一来访问与你用Entities.ForEach处理的实体相关的动态缓冲区。下面的例子添加了存储在类型为MyBufferElement的缓冲区中的所有值:
public partial class DynamicBufferSystem : SystemBase
{
protected override void OnUpdate()
{
var sum = 0;
Entities.ForEach((DynamicBuffer<MyBufferElement> buffer) =>
{
for(int i = 0; i < buffer.Length; i++)
{
sum += buffer[i].Value;
}
}).Run();
Debug.Log("Sum of all buffers: " + sum);
}
}
请注意,在这个例子中我们可以直接写到捕获的sum变量,因为我们用Run()执行代码。如果我们把这个函数安排在一个作业中运行,我们只能写到一个本地容器,如NativeArray,尽管结果是一个单一的值。
IJobChunk
要访问IJobChunk作业中的单个缓冲区,请将缓冲区的数据类型传递给Job,并使用它来获得一个BufferAccessor。缓冲区访问器是一个类似数组的结构,它提供对当前块中所有动态缓冲区的访问。
和前面的例子一样,下面的例子将所有包含MyBufferElement类型元素的动态缓冲区的内容相加。IJobChunk作业也可以在每个块上并行运行,所以在这个例子中,它首先在一个本地数组中存储每个缓冲区的中间和,然后使用第二个Job来计算最终和。在这种情况下,中间数组为每个chunk存储一个结果,而不是为每个实体存储一个结果。
public partial class DynamicBufferJobSystem : SystemBase
{
private EntityQuery query;
protected override void OnCreate()
{
//创建查询以查找具有包含MyBufferElement的动态缓冲区的所有实体
EntityQueryDesc queryDescription = new EntityQueryDesc();
queryDescription.All = new[] {ComponentType.ReadOnly<MyBufferElement>()};
query = GetEntityQuery(queryDescription);
}
public struct BuffersInChunks : IJobEntityBatch
{
//数据类型和安全对象
public BufferTypeHandle<MyBufferElement> BufferTypeHandle;
//保存输出、中间和的数组
public NativeArray<int> sums;
public void Execute(ArchetypeChunk batchInChunk, int batchIndex)
{
//缓冲区访问器是块中所有缓冲区的列表
BufferAccessor<MyBufferElement> buffers
= batchInChunk.GetBufferAccessor(BufferTypeHandle);
for (int c = 0; c < batchInChunk.Count; c++)
{
//特定实体的单个动态缓冲区
DynamicBuffer<MyBufferElement> buffer = buffers[c];
for(int i = 0; i < buffer.Length; i++)
{
sums[batchIndex] += buffer[i].Value;
}
}
}
}
//将中间结果相加为最终总数
public struct SumResult : IJob
{
[DeallocateOnJobCompletion] public NativeArray<int> sums;
public NativeArray<int> result;
public void Execute()
{
for(int i = 0; i < sums.Length; i++)
{
result[0] += sums[i];
}
}
}
protected override void OnUpdate()
{
//创建一个native array来保存中间和
int chunksInQuery = query.CalculateChunkCount();
NativeArray<int> intermediateSums
= new NativeArray<int>(chunksInQuery, Allocator.TempJob);
//计划第一个Job以添加所有缓冲区元素
BuffersInChunks bufferJob = new BuffersInChunks();
bufferJob.BufferTypeHandle = GetBufferTypeHandle<MyBufferElement>();
bufferJob.sums = intermediateSums;
this.Dependency = bufferJob.ScheduleParallel(query, 1, this.Dependency);
//计划第二个Job,它取决于第一个Job
SumResult finalSumJob = new SumResult();
finalSumJob.sums = intermediateSums;
NativeArray<int> finalSum = new NativeArray<int>(1, Allocator.Temp);
finalSumJob.result = finalSum;
this.Dependency = finalSumJob.Schedule(this.Dependency);
this.CompleteDependency();
Debug.Log("Sum of all buffers: " + finalSum[0]);
finalSum.Dispose();
}
}
Reinterpreting buffers(重新解释缓冲区)
缓冲器可以被重新解释为相同大小的类型。这样做的目的是为了允许可控的类型运行,并在包装元素类型碍事的时候摆脱它们。要重新解释,请调用Reinterpret:
DynamicBuffer<int> intBuffer
= EntityManager.GetBuffer<MyBufferElement>(entity).Reinterpret<int>();
重新解释的Buffer实例保留了原始缓冲区的安全句柄,可以安全使用。重新解释的缓冲区引用原始数据,因此对一个重新解释的缓冲区的修改会立即反映在其他缓冲区中。
注意:reinterpret函数只强制要求所涉及的类型具有相同的长度。例如,你可以别名一个uint和float缓冲区而不产生错误,因为这两种类型都是32位长。你必须确保重新解释在逻辑上是合理的。
Buffer reference invalidation(缓冲区引用无效)
每个结构变化都会使所有对动态缓冲区的引用失效。结构变化通常会导致实体从一个块移动到另一个块。小的动态缓冲区可以引用一个块内的内存(相对于从主内存),因此,它们需要在结构变化后被重新获取。
var entity1 = EntityManager.CreateEntity();
var entity2 = EntityManager.CreateEntity();
DynamicBuffer<MyBufferElement> buffer1
= EntityManager.AddBuffer<MyBufferElement>(entity1);
// 此行导致结构更改,并使以前获取的动态缓冲区无效
DynamicBuffer<MyBufferElement> buffer2
= EntityManager.AddBuffer<MyBufferElement>(entity1);
// 此行将导致错误:
buffer1.Add(17);