英文原文:
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。
在制作游戏时,您可能听说过很多关于对象池的信息。由于创建一次性对象与创建垃圾基本相同,将它们池化和重用可以减少频繁的垃圾收集并管理实例创建时间,从而最终创建更流畅的游戏。
下一次,让我们找出如何应用此功能并比较之前和之后,看看您可以获得多少改进。