[Unity ECS] 如何使用Unity DOTS DynamicBuffer

63 篇文章 11 订阅

英文原文:

https://snack.planetarium.dev/eng/2020/05/unity-dots-start-to-use-dynamic-buffer/

IBufferElementData 实现

  正如添加到实体的组件必须实现 IComponentData 接口一样,DynamicBuffer 也必须实现 IBufferElementData 接口。

  • 我创建了一个实现 IBufferElementData 的 IntBufferElement 结构。它的格式类似于IComponentData,对吗?
using Unity.Entities;

namespace DOTS_DynamicBuffer
{
  public struct IntBufferElement : IBufferElementData
  {
    public int Value;
  }
}

使用 EntityManager.AddBuffer()

  就像向实体添加组件一样,我们在添加缓冲区时需要使用 EntityManager。下面,我编写了一个名为 PlayModeTest 的组件,它将被添加到游戏对象中,让我们在播放模式下检查Entity Debugger。

  • 让我们向实体添加一个 IntBufferElement 缓冲区并在其中放入一些值。
using UnityEngine;
using Unity.Entities;

namespace DOTS_DynamicBuffer
{
  public class PlayModeTest : MonoBehaviour
  {
    private void Awake()
    {
      var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
      var entity = entityManager.CreateEntity();
      var dynamicBuffer = entityManager.AddBuffer<IntBufferElement>(entity);
      dynamicBuffer.Add(new IntBufferElement { Value = 1 });
      dynamicBuffer.Add(new IntBufferElement { Value = 2 });
      dynamicBuffer.Add(new IntBufferElement { Value = 3 });
    }
  }
}
  • 我创建了 DOTS_DynamicBufferScene 并将 PlayModeTest 脚本添加到同名的游戏对象中。

在这里插入图片描述

  • 在播放模式下,您可以通过 Entity Debugger 查看 PlayModeTest.Awake() 方法创建的实体。你能看到 IntBufferElement 缓冲区有三个值吗?

使用 DynamicBuffer.Reinterpret()

  现在让我们看看如何修改缓冲区结构中包含的值。

  • 我使用了 DynamicBuffer.Reinterpret() 方法,它是对 PlayModeTest.Awake() 的一个小修改。如第 12 行所示,使用索引值访问的结构无法更改,因为它是一个临时值,不属于变量。但是,可以使用 reinterpret 方法修改这些值,如第 14-15 行所示。
private void Awake()
{
    var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
    var entity = entityManager.CreateEntity();
    var dynamicBuffer = entityManager.AddBuffer<IntBufferElement>(entity);
    dynamicBuffer.Add(new IntBufferElement {Value = 1});
    dynamicBuffer.Add(new IntBufferElement {Value = 2});
    dynamicBuffer.Add(new IntBufferElement {Value = 3});

    // ERROR: Indexer access returns temporary value.
    // Cannot modify struct member when accessed struct is not classified as a variable
    // dynamicBuffer[0].Value *= 10;

    var intDynamicBuffer = dynamicBuffer.Reinterpret<int>();
    intDynamicBuffer[0] *= 10;
}
  • 让我们检查播放模式以查看值是否已更改。很好!所以我们的结论是,尽管没有将 intDynamicBuffer[0] 的值放回 dynamicBuffer[0],但缓冲区的值发生了变化。

在这里插入图片描述

使用 EntityManager.GetBuffer()

  我们还需要一种方法来访问实体上的缓冲区。

  • 我修改了 PlayModeTest 类。我通过导入由 Awake() 方法生成的实体和使用 Start() 方法添加到它的缓冲区来更改该值。
public class PlayModeTest : MonoBehaviour
{
  private Entity _entity;

  private void Awake()
  {
    var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
    _entity = entityManager.CreateEntity();

    var dynamicBuffer = entityManager.AddBuffer<IntBufferElement>(_entity);
    dynamicBuffer.Add(new IntBufferElement { Value = 1 });
    dynamicBuffer.Add(new IntBufferElement { Value = 2 });
    dynamicBuffer.Add(new IntBufferElement { Value = 3 });

    // ERROR: Indexer access returns temporary value.
    // Cannot modify struct member when accessed struct is not classified as a variable
    // dynamicBuffer[0].Value *= 10;
    var intDynamicBuffer = dynamicBuffer.Reinterpret<int>();
    intDynamicBuffer[0] *= 10;
  }

  private void Start()
  {
    var entityManger = World.DefaultGameObjectInjectionWorld.EntityManager;
    var dynamicBuffer = entityManger.GetBuffer<IntBufferElement>(_entity);
    var intDynamicBuffer = dynamicBuffer.Reinterpret<int>();
    for (var i = 0; i < intDynamicBuffer.Length; i++)
    {
      intDynamicBuffer[i]++;
    }
  }
}
  • 让我们检查它是否运行良好。缓冲区中的所有值都增加了 1! Reinterpret() 很有趣。
    在这里插入图片描述

Authoring

  GenerateAuthoringComponentAttribute 允许您将创作组件添加到游戏对象以创建实体。 IBufferElementData 可以使用相同的方法。

  • 让我们修改 IntBufferElement 并应用 GenerateAuthoringComponentAttribute。
[GenerateAuthoringComponent]
public struct IntBufferElement : IBufferElementData
{
  public int Value;
}
  • 并且我添加了 IntBufferElementAuthoring 组件,该组件通过修改 Scene 自动生成并为游戏对象添加值。我还添加了 ConvertToEntity 组件来使游戏对象成为一个实体。

在这里插入图片描述

  • Entity Debugger 显示同名的实体被创建为具有 Authoring 组件的游戏对象。

在这里插入图片描述
  对于下一步,让我们编写 UnitTag、PlayerTag 和 EnemyTag 组件,并向包含每个组件的实体添加一个 IntBufferElement 缓冲区。

using Unity.Entities;

namespace DOTS_DynamicBuffer
{
  [GenerateAuthoringComponent]
  public struct UnitTag : IComponentData { }

  [GenerateAuthoringComponent]
  public struct PlayerTag : IComponentData { }

  [GenerateAuthoringComponent]
  public struct EnemyTag : IComponentData { }
}

在这里插入图片描述

在这里插入图片描述

在 ComponentSystem 中使用 DynamicBuffer

  现在,让我们通过创建一个继承 ComponentSystem 的系统来访问包含 UnitTag 组件的实体的 IntBufferElement DynamicBuffer。

  • 我写了一个TestBufferFromEntitySystem。这个逻辑通过访问实体的IntBufferElement类型的DynamicBuffer来改变数值,其中包括UnitTag。像第20行那样实现是行不通的,所以请参考第23-28行。当然,我们也可以使用Reinterpret()。
using Unity.Entities;

namespace DOTS_DynamicBuffer
{
  public class TestBufferFromEntitySystem : ComponentSystem
  {
    protected override void OnUpdate()
    {
      var bufferFromEntity = GetBufferFromEntity<IntBufferElement>();
      Entities
        .WithAll<UnitTag>()
        .ForEach(entity =>
        {
          if (bufferFromEntity.Exists(entity))
          {
            var dynamicBufferFromUnitTag = bufferFromEntity[entity];
            foreach (var intBufferElement in dynamicBufferFromUnitTag)
            {
              // Foreach iteration variable 'intBufferElement' is immutable.
              // Cannot modify struct member when accessed struct is not classified as a variable
              // intBufferElement.Value++;
            }

            for (var i = 0; i < dynamicBufferFromUnitTag.Length; i++)
            {
              var intBufferElement = dynamicBufferFromUnitTag[i];
              intBufferElement.Value++;
              dynamicBufferFromUnitTag[i] = intBufferElement;
            }
          }
        });
    }
  }
}
  • 如果在播放模式下查看 Entity Debugger,可以看到 UnitTag 组件的值包括实体的 IntBufferElement DynamicBuffer 发生了变化。

在这里插入图片描述

在 JobComponentSystem 中使用 DynamicBuffer

  让我们创建一个继承 JobComponentSystem 的系统,并尝试访问包含 PlayerTag 组件的实体的 IntBufferElement DynamicBuffer。

  • 我写了一个 TestBufferFromEntityJobSystem。此逻辑通过访问包含 PlayerTag 的实体的 IntBufferElement 类型 DynamicBuffer 来更改值。这次我尝试了 Reinterpret()。
using Unity.Entities;
using Unity.Jobs;

namespace DOTS_DynamicBuffer
{
  public class TestBufferFromEntityJobSystem : JobComponentSystem
  {
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
      return Entities
        .WithAll<PlayerTag>()
        .ForEach((ref DynamicBuffer<IntBufferElement> dynamicBuffer) =>
        {
          var intDynamicBuffer = dynamicBuffer.Reinterpret<int>();
          for (var i = 0; i < intDynamicBuffer.Length; i++)
          {
            intDynamicBuffer[i]++;
          }
        })
        .Schedule(inputDeps);
    }
  }
}
  • 它起作用了!正如我们想要的那样,value正在增加。

在这里插入图片描述

附加提示

InternalBufferCapacityAttribute

  由于实体通常包含在块中,因此将 InternalBufferCapacityAttribute 应用于实现 IBufferElementData 的结构可以指定块中可以存在的最大元素数。当元素超过指定的限制时,缓冲区移动器转移到堆内存。当然,您也可以通过 DynamicBuffer API 访问缓冲区。

  • 在这里,我将元素的数量设置为 2。
// InternalBufferCapacity specifies how many elements a buffer can have before
// the buffer storage is moved outside the chunk.
[InternalBufferCapacity(2)]
[GenerateAuthoringComponent]
public struct IntBufferElement : IBufferElementData
{
  public int Value;
}
  • 为了在同一块中进行测试,我复制了另外两个包含 EnemyTag 的 Enemy 游戏对象。

在这里插入图片描述

  • 让我们检查一下Entity Debugger。但是看起来IntBufferElement仍然在这个块中。这可能是为了方便,尽管缓冲区已经被移到堆内存中。

在这里插入图片描述

隐式运算符

  为方便起见,我们也可以这样编写代码。

using Unity.Entities;

namespace DOTS_DynamicBuffer
{
  // InternalBufferCapacity specifies how many elements a buffer can have before
  // the buffer storage is moved outside the chunk.
  [InternalBufferCapacity(2)]
  [GenerateAuthoringComponent]
  public struct IntBufferElement : IBufferElementData
  {
    public int Value;

    // The following implicit conversions are optional, but can be convenient.
    public static implicit operator int(IntBufferElement e)

    {
      return e.Value;
    }

    public static implicit operator IntBufferElement(int e)

    {
      return new IntBufferElement { Value = e };
    }
  }
}

结束语

  天,我们快速浏览了 IBufferElementData 和 DynamicBuffer。

  在制作游戏时,您可能听说过很多关于对象池的信息。由于创建一次性对象与创建垃圾基本相同,将它们池化和重用可以减少频繁的垃圾收集并管理实例创建时间,从而最终创建更流畅的游戏。

  下一次,让我们找出如何应用此功能并比较之前和之后,看看您可以获得多少改进。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值