Dynamic Buffers
Use dynamic buffers to associate array-like data with an entity. Dynamic buffers are ECS components that can hold a variable number of elements, automatically resizing as necessary.
To create a dynamic buffer, first declare a struct that implements IBufferElementData and which defines the elements stored in the buffer. For example, you could 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 treat a dynamic buffer the same as any other ECS component by using your declared IBufferElementData type. For example, you can use the IBufferElementData type in entity queries as well as when adding or removing the buffer component. However, you use different functions to access a buffer component and those functions provide the DynamicBuffer instance, which provides an array-like interface to the buffer data.
You can specify an “internal capacity" for a dynamic buffer component using 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, freeing the memory when the buffer component is removed.
Note that 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 and can be shared by multiple entities.
Declaring Buffer Element Types
To declare a buffer, declare a struct defining the type of element that you are putting into the buffer. The struct must implement 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};
}
}
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 add that type directly to an entity or to an archetype:
Using EntityManager.AddBuffer()
EntityManager.AddBuffer<MyBufferElement>(entity);
Using an archetype
Entity e = EntityManager.CreateEntity(typeof(MyBufferElement));
Using an EntityCommandBuffer
You can add or set a buffer component when adding commands to an entity command buffer. Use AddBuffer to create a new buffer for the entity, changing 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 actually 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.
struct DataSpawnJob : IJobForEachWithEntity<DataToSpawn>
{
// A command buffer marshals structural changes to the data
public EntityCommandBuffer.Concurrent CommandBuffer;
//The DataToSpawn component tells us how many entities with buffers to create
public void Execute(Entity spawnEntity, int index, [ReadOnly] ref DataToSpawn data)
{
for (int e = 0; e < data.EntityCount; e++)
{
//Create a new entity for the command buffer
Entity newEntity = CommandBuffer.CreateEntity(index);
//Create the dynamic buffer and add it to the new entity
DynamicBuffer<MyBufferElement> buffer =
CommandBuffer.AddBuffer<MyBufferElement>(index, 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(index, spawnEntity);
}
}
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 access the DynamicBuffer instance using EntityManager, systems, and jobs, in much the same way as you access other component types of your entities.
EntityManager
You can use an instance of the EntityManager to access a dynamic buffer:
DynamicBuffer<MyBufferElement> dynamicBuffer
= EntityManager.GetBuffer<MyBufferElement>(entity);
Component System Entities.ForEach
You can also access dynamic buffers in a component system:
public class DynamicBufferSystem : ComponentSystem
{
protected override void OnUpdate()
{
var sum = 0;
Entities.ForEach((DynamicBuffer<MyBufferElement> buffer) =>
{
foreach (var integer in buffer.Reinterpret<int>())
{
sum += integer;
}
});
Debug.Log("Sum of all buffers: " + sum);
}
}
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 You can also look up buffers on a per-entity basis from a JobComponentSystem:
BufferFromEntity<MyBufferElement> lookup = GetBufferFromEntity<MyBufferElement>();
var buffer = lookup[entity];
buffer.Add(17);
buffer.RemoveAt(0);
IJobForEach
You can access dynamic buffers associated with the entities you process with an IJobForEach job.
In the IJobforEach or IJobForEachWithEntity declaration, use the IBufferElementData type contained in the buffer as one of the generic parameters. For example:
public struct BuffersByEntity : IJobForEachWithEntity_EB<MyBufferElement>
However, in the Execute()
method of the job struct, use the DynamicBuffer<T> as the parameter type. For example:
public void Execute(Entity entity, int index, DynamicBuffer<MyBufferElement> buffer)
The following example adds up the contents of all dynamic buffers containing elements of type, MyBufferElement
. Because IJobForEach jobs process entities in parallel, the example first stores the intermediate sum for each buffer in a native array and uses a second job to calculate the final sum.
public class DynamicBufferForEachSystem : JobComponentSystem
{
private EntityQuery query;
protected override void OnCreate()
{
EntityQueryDesc queryDescription = new EntityQueryDesc();
queryDescription.All = new[] {ComponentType.ReadOnly<MyBufferElement>()};
query = GetEntityQuery(queryDescription);
}
//Sums the elements of individual buffers of each entity
public struct BuffersByEntity : IJobForEachWithEntity_EB<MyBufferElement>
{
public NativeArray<int> sums;
public void Execute(Entity entity,
int index,
DynamicBuffer<MyBufferElement> buffer)
{
foreach (int integer in buffer.Reinterpret<int>())
{
sums[index] += integer;
}
}
}
//Sums the intermediate results into the final total
public struct SumResult : IJob
{
[DeallocateOnJobCompletion] public NativeArray<int> sums;
public void Execute()
{
int sum = 0;
foreach (int integer in sums)
{
sum += integer;
}
//Note: Debug.Log is not burst-compatible
Debug.Log("Sum of all buffers: " + sum);
}
}
//Schedules the two jobs with a dependency between them
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
//Create a native array to hold the intermediate sums
int entitiesInQuery = query.CalculateEntityCount();
NativeArray<int> intermediateSums
= new NativeArray<int>(entitiesInQuery, Allocator.TempJob);
//Schedule the first job to add all the buffer elements
BuffersByEntity bufferJob = new BuffersByEntity();
bufferJob.sums = intermediateSums;
JobHandle intermediateJob = bufferJob.Schedule(this, inputDeps);
//Schedule the second job, which depends on the first
SumResult finalSumJob = new SumResult();
finalSumJob.sums = intermediateSums;
return finalSumJob.Schedule(intermediateJob);
}
}
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, IJobForEach example, the following example adds up the contents of all dynamic buffers containing elements of type, MyBufferElement
. IJobChunk jobs can also run in parallel on each chunk so the example first stores the intermediate sum for each buffer in a native array and 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 : JobComponentSystem
{
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 ArchetypeChunkBufferType<MyBufferElement> BufferType;
//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(BufferType);
for (int c = 0; c < chunk.Count; c++)
{
//An individual dynamic buffer for a specific entity
DynamicBuffer<MyBufferElement> buffer = buffers[c];
foreach (MyBufferElement element in buffer)
{
sums[chunkIndex] += element.Value;
}
}
}
}
//Sums the intermediate results into the final total
public struct SumResult : IJob
{
[DeallocateOnJobCompletion] public NativeArray<int> sums;
public void Execute()
{
int sum = 0;
foreach (int integer in sums)
{
sum += integer;
}
//Note: Debug.Log is not burst-compatible
Debug.Log("Sum of all buffers: " + sum);
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
//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.BufferType = GetArchetypeChunkBufferType<MyBufferElement>();
bufferJob.sums = intermediateSums;
JobHandle intermediateJob = bufferJob.Schedule(query, inputDeps);
//Schedule the second job, which depends on the first
SumResult finalSumJob = new SumResult();
finalSumJob.sums = intermediateSums;
return finalSumJob.Schedule(intermediateJob);
}
}
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 to original data, so modifications to one reinterpreted buffer are immediately reflected in others.
Note that the reinterpret function only enforces that the types involved have the same length; you could alias a uint
and float
buffer without raising an error since both types are 32-bits long. It is your responsibility to make sure that the reinterpretation makes sense logically.