Unity ECS Manual [8]

63 篇文章 11 订阅

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);
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值