Dynamic buffer components
Use dynamic buffer components to associate array-like data with an entity. Dynamic buffers are ECS components that can hold a variable number of elements, and automatically resize as necessary.
To create a dynamic buffer, first declare a struct that implements IBufferElementData and defines the elements stored in the buffer. For example, you can use the following struct for a buffer component that stores integers:
public struct IntBufferElement : IBufferElementData
{
public int Value;
}
To associate a dynamic buffer with an entity, add an IBufferElementData component directly to the entity rather than adding the dynamic buffer container itself.
ECS manages the container. For most purposes, you can use a declared IBufferElementData
type to treat a dynamic buffer the same as any other ECS component. For example, you can use the IBufferElementData
type in entity queries as well as when you add or remove the buffer component. However, you must use different functions to access a buffer component and those functions provide the DynamicBuffer instance, which gives an array-like interface to the buffer data.
To specify an “internal capacity" for a dynamic buffer component, use the InternalBufferCapacity attribute. The internal capacity defines the number of elements the dynamic buffer stores in the ArchetypeChunk along with the other components of an entity. If you increase the size of a buffer beyond the internal capacity, the buffer allocates a heap memory block outside the current chunk and moves all existing elements. ECS manages this external buffer memory automatically, and frees the memory when the buffer component is removed.
Note: If the data in a buffer is not dynamic, you can use a blob asset instead of a dynamic buffer. Blob assets can store structured data, including arrays. Multiple entities can share blob assets.
Declaring buffer element types
To declare a buffer, declare a struct that defines the type of element that you want to put into the buffer. The struct must implement IBufferElementData, like so:
// 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 };
}
}
Adding buffer types to entities
To add a buffer to an entity, add the IBufferElementData
struct that defines the data type of the buffer element, and then add that type directly to an entity or to an archetype:
Using EntityManager.AddBuffer()
For more information, see the documentation on EntityManager.AddBuffer().
EntityManager.AddBuffer<MyBufferElement>(entity);
Using an archetype
Entity e = EntityManager.CreateEntity(typeof(MyBufferElement));
Using the [GenerateAuthoringComponent]
attribute
You can use [GenerateAuthoringComponent]
to generate authoring components for simple IBufferElementData implementations that contain only one field. Setting this attribute allows you add an ECS IBufferElementData component to a GameObject so that you can set the buffer elements in the Editor.
For example, if you declare the following type, you can add it directly to a GameObject in the Editor:
[GenerateAuthoringComponent]
public struct IntBufferElement: IBufferElementData
{
public int Value;
}
In the background, Unity generates a class named IntBufferElementAuthoring
(which inherits from MonoBehaviour
), which exposes a public field of List<int>
type. When the GameObject containing this generated authoring component is converted into an entity, the list is converted into DynamicBuffer<IntBufferElement>
, and then added to the converted entity.
Note the following restrictions:
- Only one component in a single C# file can have a generated authoring component, and the C# file must not have another MonoBehaviour in it.
IBufferElementData
authoring components cannot be automatically generated for types that contain more than one field.IBufferElementData
authoring components cannot be automatically generated for types that have an explicit layout.
Using an EntityCommandBuffer
You can add or set a buffer component when you add commands to an entity command buffer.
Use AddBuffer to create a new buffer for the entity, which changes the entity's archetype. Use SetBuffer to wipe out the existing buffer (which must exist) and create a new, empty buffer in its place. Both functions return a DynamicBuffer instance that you can use to populate the new buffer. You can add elements to the buffer immediately, but they are not otherwise accessible until the buffer is added to the entity when the command buffer is executed.
The following job creates a new entity using a command buffer and then adds a dynamic buffer component using EntityCommandBuffer.AddBuffer. The job also adds a number of elements to the dynamic buffer.
using Unity.Entities;
using Unity.Jobs;
public 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);
}
}
Note: You are not required to add data to the dynamic buffer immediately. However, you won't have access to the buffer again until after the entity command buffer you are using is executed.
Accessing buffers
You can use EntityManager, systems, and jobs to access the DynamicBuffer instance in much the same way as you access other component types of entities.
EntityManager
You can use an instance of the EntityManager to access a dynamic buffer:
DynamicBuffer<MyBufferElement> dynamicBuffer
= EntityManager.GetBuffer<MyBufferElement>(entity);
Looking up buffers of another entity
When you need to look up the buffer data belonging to another entity in a job, you can pass a BufferFromEntity variable to the job.
BufferFromEntity<MyBufferElement> lookup = GetBufferFromEntity<MyBufferElement>();
var buffer = lookup[entity];
buffer.Add(17);
buffer.RemoveAt(0);
SystemBase Entities.ForEach
You can access dynamic buffers associated with the entities you process with Entities.ForEach by passing the buffer as one of your lambda function parameters. The following example adds all the values stored in the buffers of type, MyBufferElement
:
public 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);
}
}
Note that we can write directly to the captured sum
variable in this example because we execute the code with Run()
. If we scheduled the function to run in a job, we could only write to a native container such as NativeArray, even though the result is a single value.
IJobChunk
To access an individual buffer in an IJobChunk
job, pass the buffer data type to the job and use that to get a BufferAccessor. A buffer accessor is an array-like structure that provides access to all of the dynamic buffers in the current chunk.
Like the previous example, the following example adds up the contents of all dynamic buffers that contain elements of type, MyBufferElement
. IJobChunk
jobs can also run in parallel on each chunk, so in the example, it first stores the intermediate sum for each buffer in a native array and then uses a second job to calculate the final sum. In this case, the intermediate array holds one result for each chunk, rather than one result for each entity.
public 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 : IJobChunk
{
//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 chunk,
int chunkIndex,
int firstEntityIndex)
{
//A buffer accessor is a list of all the buffers in the chunk
BufferAccessor<MyBufferElement> buffers
= chunk.GetBufferAccessor(BufferTypeHandle);
for (int c = 0; c < chunk.Count; c++)
{
//An individual dynamic buffer for a specific entity
DynamicBuffer<MyBufferElement> buffer = buffers[c];
for(int i = 0; i < buffer.Length; i++)
{
sums[chunkIndex] += 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, 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();
}
}
Reinterpreting buffers
Buffers can be reinterpreted as a type of the same size. The intention is to allow controlled type-punning and to get rid of the wrapper element types when they get in the way. To reinterpret, call Reinterpret<T>:
DynamicBuffer<int> intBuffer
= EntityManager.GetBuffer<MyBufferElement>(entity).Reinterpret<int>();
The reinterpreted buffer instance retains the safety handle of the original buffer, and is safe to use. Reinterpreted buffers reference original data, so modifications to one reinterpreted buffer are immediately reflected in others.
Note: The reinterpret function only enforces that the types involved have the same length. For example, you can alias a uint
and float
buffer without raising an error because both types are 32-bits long. You must make sure that the reinterpretation makes sense logically.
Buffer reference invalidation
Every structural change invalidates all references to dynamic buffers. Structural changes generally cause entities to move from one chunk to another. Small dynamic buffers can reference memory within a chunk (as opposed to from main memory) and therefore, they need to be reacquired after a structural change.
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);