动态缓冲组件

动态缓冲组件

使用动态缓冲区组件将类数组数据与实体相关联。动态缓冲区是 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);
    }
}
笔记

您不需要立即将数据添加到动态缓冲区。但是,在执行您正在使用的实体命令缓冲区之前,您将无法再次访问该缓冲区。

访问缓冲区

您可以使用EntityManagersystems和 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缓冲区访问器是一个类似数组的结构,它提供对当前块中所有动态缓冲区的访问。

与前面的示例一样,以下示例将包含类型为 的元素的所有动态缓冲区的内容相加MyBufferElementIJobChunk作业也可以在每个块上并行运行,因此在示例中,它首先将每个缓冲区的中间总和存储在本机数组中,然后使用第二个作业来计算最终总和。在这种情况下,中间数组为每个块保存一个结果,而不是为每个实体保存一个结果。


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 函数仅强制所涉及的类型具有相同的长度。例如,您可以在不引发错误的情况下为auintfloatbuffer 设置别名因为这两种类型都是 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);


 

posted @ 2021-09-24 00:47  alps_01  阅读(136)  评论(0编辑  收藏  举报