Entity Command Buffers
An EntityCommandBuffer (ECB) records entity data changes to be enacted later. Commands are recorded with methods of EntityCommandBuffer
which mirror many (but not all) of the EntityManager
methods, for example:
CreateEntity(EntityArchetype)
: Create a new entity with the specified archetypeDestroyEntity(Entity)
: Destroy the entitySetComponent<T>(Entity, T)
: Set value for component of typeT
on the entityAddComponent<T>(Entity)
: Add component of typeT
to the entityRemoveComponent<T>(EntityQuery)
: Remove component of typeT
from all entities matching the query.
None of the changes recorded in an EntityCommandBuffer
are enacted until its Playback
method is called on the main thread.
After playback of an ECB, attempting to record more changes throws an exception.
Like a native container, an ECB has a job safety handle. While a job that uses an ECB is scheduled but not yet completed, the safety checks will throw an exception if if the main thread tries to:
- Access the ECB (calls its
AddComponent
,Playback
,Dispose
, or other methods) - Schedule another job that accesses the same ECB (unless this new job depends upon the already scheduled job).
ECB use in a single-threaded job
Structural changes cannot be performed in a job, so an ECB is very useful for deferring structural changes until the job is completed. For example:
// ... in a system update
// You don't specify a size because the buffer will grow as needed.
EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob);
// The ECB is captured by the ForEach job.
// Until completed, the job owns the ECB's job safety handle.
Entities
.ForEach((Entity e, in FooComp foo) =>
{
if (foo.Value > 0)
{
// Record a command that will later add
// BarComp to the entity.
ecb.AddComponent<BarComp>(e);
}
}).Schedule();
this.Dependency.Complete();
// Now that the job is completed, you can enact the changes.
// Note that Playback can only be called on the main thread.
ecb.Playback(this.EntityManager);
// You are responsible for disposing of any ECB you create.
ecb.Dispose();
ECB use in a parallel job
When recording in a parallel job, you need an EntityCommandBuffer.ParallelWriter
, which allows for thread-safe concurrent recording to an ECB:
EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob);
// Methods of this writer record commands to
// the ECB in a thread-safe way.
EntityCommandBuffer.ParallelWriter parallelEcb = ecb.AsParallelWriter();
Note
Be clear that only recording needs to be safe for concurrency: playback is always just single-threaded on the main thread.
Because recording of the commands is split across threads, the order of recorded commands depends upon the happenstance of scheduling and thus is non-deterministic. Indeterminate order of recording cannot be avoided, but we can make the playback order of the commands deterministic:
- Each command records a 'sort key' int (passed as the first argument to each command method).
- Upon playback, the commands are sorted by their sort keys before the commands are enacted.
As long as the recorded sort keys are independent from the happenstance of scheduling, the sorting makes the playback order deterministic.
In a parallel job, the sort key you need for each entity is a number that has a fixed and unique association with that entity in the job's query. The entityInQueryIndex
value provided in a parallel job meets those criteria: in the list of chunks matching the job's query...
- the first entity of the first chunk has
entityInQueryIndex
0 - the second entity of the first chunk has
entityInQueryIndex
1 - the first entity of the second chunk has an
entityInQueryIndex
which is the count of the first chunk - the first entity of the third chunk has an
entityInQueryIndex
which is the sum of the counts of the first two chunks - ...and so forth.
Note
Determinism isn't always essential, but code which produces deterministic results can be much easier to debug. There are also networking scenarios which require consistent results across different machines. Understand however that determinism can have a cost, so accepting indeterminism may be appropriate in some projects.
Example use:
// ... in a system update
EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob);
// We need to write to the ECB concurrently across threads.
EntityCommandBuffer.ParallelWriter ecbParallel = ecb.AsParallelWriter();
// The entityInQueryIndex is unique for each entity and will be
// consistent for each particular entity regardless of scheduling.
Entities
.ForEach((Entity e, int entityInQueryIndex, in FooComp foo) => {
if (foo.Value > 0)
{
// The first arg is the 'sort key' recorded with the command.
ecbParallel.AddComponent<BarComp>(entityInQueryIndex, e);
}
}).Schedule();
// Playback is single-threaded as normal.
this.Dependency.Complete();
// To ensure deterministic playback order,
// the commands are first sorted by their sort keys.
ecb.Playback(this.EntityManager);
ecb.Dispose();
Note
Note that the lambda parameter must be called entityInQueryIndex
. Otherwise, Entities.ForEach
doesn't know what int you're talking about.
Warning
For a regular (non-parallel) ECB, it's OK to use the same ECB in multiple jobs as long as those jobs don't overlap in scheduling. For a parallel ECB, however, this can lead to unexpected (and potentially undesirable) sort orders of the commands in playback...unless the sort keys used in each job fall in different ranges. Recording a set of commands across several ECB's has very little overhead compared to recording the same set of commands to a single ECB, so it's generally best to simply give each job its own ECB, especially for parallel jobs.
Multi-playback
The Playback
method throws an exception if called more than once unless the ECB is created with the PlaybackPolicy.MultiPlayback
option:
// ... in a system update
EntityCommandBuffer ecb =
new EntityCommandBuffer(Allocator.TempJob, PlaybackPolicy.MultiPlayback);
// ... record commands
ecb.Playback(this.EntityManager);
// Additional playbacks are OK because this ECB is MultiPlayback.
ecb.Playback(this.EntityManager);
ecb.Dispose();
The main use case for multi-playback is repeatedly spawning a set of entities: after using an ECB to create and configure a set of new entities, you can repeat playback to respawn another matching set of entities.
After an ECB's first playback, attempting to record additional commands throws an exception.
ECB use on the main thread
You sometimes might want to record ECB commands on the main thread because:
- You want to delay your changes.
- You want to play back a set of changes multiple times.
- It can be more efficient to play back many changes in one consolidated place rather than interspersing the changes across different parts of the frame.
Regarding this last point, keep in mind that every structural change operation triggers a sync point, meaning the operation must wait for some or all currently scheduled jobs to complete. If you consolidate your structural changes with ECB's, your frame can have fewer sync points.
EntityCommandBufferSystem
Rather than manually play back and dispose a command buffer yourself, you can have an EntityCommandBufferSystem do those things for you by following these steps:
- Get the instance of the ECB system which you want to do the playback.
- Create an ECB via the system.
- Schedule a job that will write commands to the ECB.
- Register the scheduled job to be completed by the system.
For example:
// ... in a system
// Assume an EntityCommandBufferSystem exists named FooECBSystem.
EntityCommandBufferSystem sys =
this.World.GetExistingSystem<FooECBSystem>();
// Create a command buffer that will be played back
// and disposed by MyECBSystem.
EntityCommandBuffer ecb = sys.CreateCommandBuffer();
// A ForEach with no argument to Schedule implicitly
// assigns its returned JobHandle to this.Dependency
Entities
.ForEach((Entity e, in FooComp foo) => {
// ... record to the ECB
}).Schedule();
// Register the job so that it gets completed by the ECB system.
sys.AddJobHandleForProducer(this.Dependency);
Note
You should not manually play back and dispose an ECB created by an ECB system. The ECB system will do both for you.
In each update, an EntityCommandBufferSystem
will:
- Complete all registered jobs (thereby ensuring that they have finished their recording).
- Playback all ECB's created via the system (in the same order they were created).
- Dispose of the ECB's.
The standard ECB systems
The default World is automatically given five ECB systems:
- BeginInitializationEntityCommandBufferSystem
- EndInitializationEntityCommandBufferSystem
- BeginSimulationEntityCommandBufferSystem
- EndSimulationEntityCommandBufferSystem
- BeginPresentationEntityCommandBufferSystem
These update at the begin and end of the three standard system groups. See Default System Groups.
Note
Because structural changes cannot happen in the frame after the rendering data has been handed off to the renderer, there is no EndPresentationEntityCommandBufferSystem
at the end of the frame. BeginInitializationEntityCommandBufferSystem
at the start of the frame should suit most of the same purposes: after all, the end of one frame is the beginning of the next frame.
These standard five ECB systems should suffice for most purposes, but you can create your own ECB systems if necessary:
// You should specify where exactly in the frame
// that the ECB system should update.
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(FooSystem))]
public class MyECBSystem : EntityCommandBufferSystem {
// This class is intentionally empty. There is generally no
// reason to put any code in an EntityCommandBufferSystem.
}
Deferred entities
The ECB methods CreateEntity
and Instantiate
record commands to create entities. Because these methods just record commands instead of immediately creating entities, they return Entity
values with negative indexes representing placeholder entities that do not yet exist. These placeholder Entity
values are only meaningful in recorded commands of the same ECB.
// ... in a system
EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob);
Entity placeholderEntity = ecb.CreateEntity();
// Valid to use placeholderEntity in later commands of same ECB.
ecb.AddComponent<FooComp>(placeholderEntity);
// The real entity is created, and
// FooComp is added to the real entity.
ecb.Playback(this.EntityManager);
// Exception! The placeholderEntity has no meaning outside
// the ECB which created it, even after playback.
this.EntityManager.AddComponent<BarComp>(placeholderEntity);
ecb.Dispose();
Values recorded in an AddComponent
, SetComponent
, or SetBuffer
command might have Entity
fields. In playback, any placeholder Entity
values in these components or buffers will be remapped to the corresponding actual entities.
// ... in a system
EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob);
// For all entities with a FooComp component...
Entities
.WithAll<FooComp>()
.ForEach((Entity e) =>
{
// In playback, an actual entity will be created
// that corresponds to this placeholder entity.
Entity placeholderEntity = ecb.CreateEntity();
// (Assume BarComp has an Entity field called TargetEnt.)
BarComp bar = new BarComp { TargetEnt = placeholderEntity };
// In playback, TargetEnt will be assigned the
// actual Entity that corresponds to placeholderEntity.
ecb.AddComponent(e, bar);
}).Run();
// After playback, each entity with FooComp now has a
// BarComp component whose TargetEnt references a new entity.
ecb.Playback(this.EntityManager);
ecb.Dispose();