动态缓冲组件
动态缓冲组件
使用动态缓冲区组件将类数组数据与实体相关联。动态缓冲区是 ECS 组件,可以容纳可变数量的元素,并根据需要自动调整大小。
要创建动态缓冲区,首先声明一个实现IBufferElementData并定义存储在缓冲区中的元素的结构。例如,您可以将以下结构用于存储整数的缓冲区组件:
public struct IntBufferElement : IBufferElementData
{
public int Value;
}
要将动态缓冲区与实体关联,请将IBufferElementData组件直接添加到实体,而不是添加动态缓冲区容器本身。
ECS 管理容器。大多数情况下,您可以使用声明的IBufferElementData
类型将动态缓冲区视为与任何其他 ECS 组件相同。例如,您可以IBufferElementData
在实体查询中以及在添加或删除缓冲区组件时使用该类型。但是,您必须使用不同的函数来访问缓冲区组件,并且这些函数提供了DynamicBuffer实例,它为缓冲区数据提供了类似数组的接口。
要为动态缓冲区组件指定“内部容量”,请使用InternalBufferCapacity 属性。内部容量定义动态缓冲区与实体的其他组件一起存储在ArchetypeChunk 中的元素数量。如果增加缓冲区的大小超出内部容量,buffer会在当前chunk之外分配一个堆内存块并移动所有存在的元素,ECS自动管理这个外部buffer memory,当buffer组件被移除时释放内存。
笔记
如果缓冲区中的数据不是动态的,则可以使用blob 资产而不是动态缓冲区。Blob 资产可以存储结构化数据,包括数组。多个实体可以共享 Blob 资产。
声明缓冲区元素类型
要声明缓冲区,请声明一个结构体,该结构体定义要放入缓冲区的元素类型。该结构必须实现IBufferElementData,如下所示:
// InternalBufferCapacity specifies how many elements a buffer can have before
// the buffer storage is moved outside the chunk.
[InternalBufferCapacity(8)]
public struct MyBufferElement : IBufferElementData
{
// Actual value each buffer element will store.
public int Value;
// The following implicit conversions are optional, but can be convenient.
public static implicit operator int(MyBufferElement e)
{
return e.Value;
}
public static implicit operator MyBufferElement(int e)
{
return new MyBufferElement { Value = e };
}
}
向实体添加缓冲区类型
要将缓冲区添加到实体,请添加IBufferElementData
定义缓冲区元素数据类型的结构,然后将该类型直接添加到实体或原型:
使用 EntityManager.AddBuffer()
有关更多信息,请参阅有关EntityManager.AddBuffer()的文档。
EntityManager.AddBuffer<MyBufferElement>(entity);
使用原型
Entity e = EntityManager.CreateEntity(typeof(MyBufferElement));
使用[GenerateAuthoringComponent]
属性
您可以使用[GenerateAuthoringComponent]
为仅包含一个字段的简单 IBufferElementData 实现生成创作组件。设置此属性允许您将 ECS IBufferElementData 组件添加到游戏对象,以便您可以在编辑器中设置缓冲区元素。
例如,如果您声明以下类型,则可以将其直接添加到编辑器中的 GameObject 中:
[GenerateAuthoringComponent]
public struct IntBufferElement: IBufferElementData
{
public int Value;
}
在后台,Unity 生成一个名为IntBufferElementAuthoring
(继承自MonoBehaviour
)的类,该类公开了一个公共字段List<int>
类型。当包含此生成的创作组件的 GameObject 转换为实体时,列表将转换为DynamicBuffer<IntBufferElement>
,然后添加到转换后的实体中。
请注意以下限制:
- 单个 C# 文件中只有一个组件可以有一个生成的创作组件,并且 C# 文件中不能有另一个 MonoBehaviour。
IBufferElementData
无法为包含多个字段的类型自动生成创作组件。IBufferElementData
无法为具有显式布局的类型自动生成创作组件。
使用EntityCommandBuffer
您可以在向实体命令缓冲区添加命令时添加或设置缓冲区组件。
使用AddBuffer为实体创建一个新缓冲区,这会更改实体的原型。使用SetBuffer清除现有缓冲区(必须存在)并在其位置创建一个新的空缓冲区。这两个函数都返回一个DynamicBuffer实例,您可以使用它来填充新缓冲区。您可以立即将元素添加到缓冲区,但在执行命令缓冲区时将缓冲区添加到实体之前,它们无法以其他方式访问。
以下作业使用命令缓冲区创建一个新实体,然后使用EntityCommandBuffer.AddBuffer添加动态缓冲区组件。该作业还向动态缓冲区添加了许多元素。
using Unity.Entities;
using Unity.Jobs;
public partial class CreateEntitiesWithBuffers : SystemBase
{
// A command buffer system executes command buffers in its own OnUpdate
public EntityCommandBufferSystem CommandBufferSystem;
protected override void OnCreate()
{
// Get the command buffer system
CommandBufferSystem
= World.DefaultGameObjectInjectionWorld.GetExistingSystem<EndSimulationEntityCommandBufferSystem>();
}
protected override void OnUpdate()
{
// The command buffer to record commands,
// which are executed by the command buffer system later in the frame
EntityCommandBuffer.ParallelWriter commandBuffer
= CommandBufferSystem.CreateCommandBuffer().AsParallelWriter();
//The DataToSpawn component tells us how many entities with buffers to create
Entities.ForEach((Entity spawnEntity, int entityInQueryIndex, in DataToSpawn data) =>
{
for (int e = 0; e < data.EntityCount; e++)
{
//Create a new entity for the command buffer
Entity newEntity = commandBuffer.CreateEntity(entityInQueryIndex);
//Create the dynamic buffer and add it to the new entity
DynamicBuffer<MyBufferElement> buffer =
commandBuffer.AddBuffer<MyBufferElement>(entityInQueryIndex, newEntity);
//Reinterpret to plain int buffer
DynamicBuffer<int> intBuffer = buffer.Reinterpret<int>();
//Optionally, populate the dynamic buffer
for (int j = 0; j < data.ElementCount; j++)
{
intBuffer.Add(j);
}
}
//Destroy the DataToSpawn entity since it has done its job
commandBuffer.DestroyEntity(entityInQueryIndex, spawnEntity);
}).ScheduleParallel();
CommandBufferSystem.AddJobHandleForProducer(this.Dependency);
}
}
笔记
您不需要立即将数据添加到动态缓冲区。但是,在执行您正在使用的实体命令缓冲区之前,您将无法再次访问该缓冲区。
访问缓冲区
您可以使用EntityManager、systems和 jobs 以与访问其他组件类型的实体大致相同的方式访问DynamicBuffer实例。
实体管理器
您可以使用EntityManager 的实例来访问动态缓冲区:
DynamicBuffer<MyBufferElement> dynamicBuffer
= EntityManager.GetBuffer<MyBufferElement>(entity);
查找另一个实体的缓冲区
当您需要在作业中查找属于另一个实体的缓冲区数据时,您可以将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
作业中的单个缓冲区,请将缓冲区数据类型传递给作业并使用它来获取BufferAccessor。缓冲区访问器是一个类似数组的结构,它提供对当前块中所有动态缓冲区的访问。
与前面的示例一样,以下示例将包含类型为 的元素的所有动态缓冲区的内容相加MyBufferElement
。IJobChunk
作业也可以在每个块上并行运行,因此在示例中,它首先将每个缓冲区的中间总和存储在本机数组中,然后使用第二个作业来计算最终总和。在这种情况下,中间数组为每个块保存一个结果,而不是为每个实体保存一个结果。
public partial class DynamicBufferJobSystem : SystemBase
{
private EntityQuery query;
protected override void OnCreate()
{
//Create a query to find all entities with a dynamic buffer
// containing MyBufferElement
EntityQueryDesc queryDescription = new EntityQueryDesc();
queryDescription.All = new[] {ComponentType.ReadOnly<MyBufferElement>()};
query = GetEntityQuery(queryDescription);
}
public struct BuffersInChunks : IJobEntityBatch
{
//The data type and safety object
public BufferTypeHandle<MyBufferElement> BufferTypeHandle;
//An array to hold the output, intermediate sums
public NativeArray<int> sums;
public void Execute(ArchetypeChunk batchInChunk, int batchIndex)
{
//A buffer accessor is a list of all the buffers in the chunk
BufferAccessor<MyBufferElement> buffers
= batchInChunk.GetBufferAccessor(BufferTypeHandle);
for (int c = 0; c < batchInChunk.Count; c++)
{
//An individual dynamic buffer for a specific entity
DynamicBuffer<MyBufferElement> buffer = buffers[c];
for(int i = 0; i < buffer.Length; i++)
{
sums[batchIndex] += buffer[i].Value;
}
}
}
}
//Sums the intermediate results into the final total
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()
{
//Create a native array to hold the intermediate sums
int chunksInQuery = query.CalculateChunkCount();
NativeArray<int> intermediateSums
= new NativeArray<int>(chunksInQuery, Allocator.TempJob);
//Schedule the first job to add all the buffer elements
BuffersInChunks bufferJob = new BuffersInChunks();
bufferJob.BufferTypeHandle = GetBufferTypeHandle<MyBufferElement>();
bufferJob.sums = intermediateSums;
this.Dependency = bufferJob.ScheduleParallel(query, 1, this.Dependency);
//Schedule the second job, which depends on the first
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();
}
}
重新解释缓冲区
缓冲区可以重新解释为相同大小的类型。目的是允许受控类型双关并在它们妨碍时摆脱包装元素类型。要重新解释,请调用Reinterpret<T>:
DynamicBuffer<int> intBuffer
= EntityManager.GetBuffer<MyBufferElement>(entity).Reinterpret<int>();
重新解释的缓冲区实例保留了原始缓冲区的安全句柄,可以安全使用。重新解释的缓冲区引用原始数据,因此对一个重新解释的缓冲区的修改会立即反映在其他缓冲区中。
注意: reinterpret 函数仅强制所涉及的类型具有相同的长度。例如,您可以在不引发错误的情况下为auint
和float
buffer 设置别名,因为这两种类型都是 32 位长。您必须确保重新解释在逻辑上有意义。
缓冲区引用失效
每次结构更改都会使对动态缓冲区的所有引用无效。结构变化通常会导致实体从一个块移动到另一个块。小的动态缓冲区可以引用块内的内存(与主内存相反),因此,它们需要在结构更改后重新获取。
var entity1 = EntityManager.CreateEntity();
var entity2 = EntityManager.CreateEntity();
DynamicBuffer<MyBufferElement> buffer1
= EntityManager.AddBuffer<MyBufferElement>(entity1);
// This line causes a structural change and invalidates
// the previously acquired dynamic buffer
DynamicBuffer<MyBufferElement> buffer2
= EntityManager.AddBuffer<MyBufferElement>(entity1);
// This line will cause an error:
buffer1.Add(17);