Skip to content

Command Buffer

v2.3.0+

CommandBuffer provides a mechanism for deferred execution of entity operations. When you need to destroy entities or perform other operations that might affect iteration during processing, CommandBuffer allows you to defer these operations to the end of the frame.

Every EntitySystem has a built-in commands property:

@ECSSystem('Damage')
class DamageSystem extends EntitySystem {
constructor() {
super(Matcher.all(Health, DamageReceiver));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
const damage = entity.getComponent(DamageReceiver);
if (health && damage) {
health.current -= damage.amount;
// Use command buffer to defer component removal
this.commands.removeComponent(entity, DamageReceiver);
if (health.current <= 0) {
// Defer adding death marker
this.commands.addComponent(entity, new Dead());
// Defer entity destruction
this.commands.destroyEntity(entity);
}
}
}
}
}
MethodDescription
addComponent(entity, component)Defer adding component
removeComponent(entity, ComponentType)Defer removing component
destroyEntity(entity)Defer destroying entity
setEntityActive(entity, active)Defer setting entity active state

Commands in the buffer are automatically executed after the lateUpdate phase of each frame. Execution order matches the order commands were queued.

Scene Update Flow:
1. onBegin()
2. process()
3. lateProcess()
4. onEnd()
5. flushCommandBuffers() <-- Commands execute here

CommandBuffer is suitable for:

Avoid modifying collection being traversed:

@ECSSystem('EnemyDeath')
class EnemyDeathSystem extends EntitySystem {
constructor() {
super(Matcher.all(Enemy, Health));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health && health.current <= 0) {
// Play death animation, spawn loot, etc.
this.spawnLoot(entity);
// Defer destruction, doesn't affect current iteration
this.commands.destroyEntity(entity);
}
}
}
private spawnLoot(entity: Entity): void {
// Loot spawning logic
}
}

Merge multiple operations to execute at end of frame:

@ECSSystem('Cleanup')
class CleanupSystem extends EntitySystem {
constructor() {
super(Matcher.all(MarkedForDeletion));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
// Batch collect entities to delete
this.commands.destroyEntity(entity);
}
// All destruction operations execute at frame end
}
}

One system marks, another system responds:

@ECSSystem('Combat')
class CombatSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
if (this.shouldDie(entity)) {
// Mark as dead
this.commands.addComponent(entity, new Dead());
}
}
}
}
@ECSSystem('DeathHandler')
class DeathHandlerSystem extends EntitySystem {
constructor() {
super(Matcher.all(Dead));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
// Handle death logic
this.playDeathAnimation(entity);
this.commands.destroyEntity(entity);
}
}
}
  • Commands skip already destroyed entities (safety check)
  • Single command failure doesn’t affect other commands
  • Commands execute in queue order
  • Command queue clears after each flush()
OperationDirect ModificationCommand Buffer
Add component✅ Safe✅ Safe
Remove component✅ Safe✅ Safe
Destroy entity⚠️ May cause issues✅ Recommended
Execution timingImmediateEnd of frame