Commands¶
Inside a running system, mutating the world directly is unsafe. The
scheduler may be running other systems in parallel, archetypes may be
mid-iteration, and even in single-threaded worlds a spawn during iteration
would invalidate the chunk you are walking. The solution is Commands: a
buffer you push into from your system body that gets replayed, in order,
at the next stage boundary.
One rule to internalise
Any time you want to spawn, despawn, add, remove, or replace a
component from inside a @System method, do it through a
Commands parameter. Direct world.spawn(...) /
world.despawn(...) calls belong in your main / test code, not
inside systems.
Getting a Commands parameter¶
Declare a parameter of type Commands on your @System method. The
scheduler creates a fresh Commands per system and passes the same
instance to every invocation of that system during a tick:
import zzuegg.ecs.command.Commands;
import zzuegg.ecs.entity.Entity;
import zzuegg.ecs.system.*;
public class Reaper {
@System
void reap(@Read Health h, Entity self, Commands cmds) {
if (h.current() <= 0) {
cmds.despawn(self);
}
}
}
Reaper.reap asks for Health, the current entity, and a Commands
handle. When health has dropped to zero it enqueues a despawn. Nothing
happens to the world right then — the call returns, iteration continues,
and at the end of the stage the world processes every queued command in
one batch.
The eight commands¶
Commands exposes exactly eight operations, backed by a sealed
Command interface and eight record types. From Commands.java:
public sealed interface Command permits
SpawnCommand,
DespawnCommand,
AddCommand,
RemoveCommand,
SetCommand,
InsertResourceCommand,
SetRelationCommand,
RemoveRelationCommand
{}
Each record mirrors a method on the Commands instance:
| Method | Effect at flush |
|---|---|
spawn(Record... components) |
world.spawn(...) with the given components |
despawn(Entity entity) |
world.despawn(entity) — silently no-ops if already dead |
add(Entity entity, Record component) |
world.addComponent(entity, component) if the entity is alive |
remove(Entity entity, Class<? extends Record> type) |
world.removeComponent(entity, type) if alive |
set(Entity entity, Record component) |
world.setComponent(entity, component) if alive |
insertResource(T resource) |
Calls world.setResource(resource) |
setRelation(Entity source, Entity target, T value) |
Enqueues a relation write — teaser for the relations chapter |
removeRelation(Entity source, Entity target, Class<T>) |
Removes a relation edge |
That's the whole surface. Anything structural you might want to do to the
world from inside a system has a Commands method for it.
Spawning from a system¶
The classic use of Commands: producing new entities in response to what
the system sees.
public record Spawner(float rate, float cooldown) {}
public record Bullet() {}
public class Shooter {
@System
void fire(@Read Position p, @Write Mut<Spawner> s, Commands cmds) {
var spawner = s.get();
if (spawner.cooldown() > 0) {
s.set(new Spawner(spawner.rate(), spawner.cooldown() - 1));
return;
}
cmds.spawn(
new Position(p.x(), p.y()),
new Velocity(0, 50),
new Bullet()
);
s.set(new Spawner(spawner.rate(), spawner.rate()));
}
}
Every bullet is enqueued during the Update stage. When the stage
finishes, each SpawnCommand is handed to World.spawn(components) in
order. None of the newly-spawned bullets are visible to any system in the
same stage — the next stage is the earliest point they can be iterated.
Adding, removing, and setting¶
public record Stunned(int ticksLeft) {}
public class StunApplier {
@System
void applyStun(@Read Health h, Entity self, Commands cmds) {
if (h.current() < 10) {
cmds.add(self, new Stunned(60)); // attach Stunned
}
}
}
public class StunTicker {
@System
void tick(@Write Mut<Stunned> s, Entity self, Commands cmds) {
var cur = s.get();
if (cur.ticksLeft() <= 1) {
cmds.remove(self, Stunned.class); // detach Stunned
} else {
s.set(new Stunned(cur.ticksLeft() - 1));
}
}
}
add(entity, component)attaches a component and moves the entity into the archetype that includes it.remove(entity, componentClass)detaches a component and moves the entity to the archetype without it.set(entity, component)replaces the value of a component the entity already has. For simple in-system mutations, prefer@Write Mut<T>—setis for the case where you want the write to apply at flush time instead of in place.
Each of these is safe to issue even if the entity might be dead by the
time flush runs. The CommandProcessor checks world.isAlive(entity)
before every mutation and silently drops commands for entities that
didn't survive.
Commands flush at stage boundaries¶
The one timing rule you must internalise:
Commands enqueued during stage S are flushed at the end of stage S, before stage S+1 begins.
That means:
- A system in
"Update"that spawns an entity will see the new entity iterated by systems in"PostUpdate"— not by any other"Update"system, even ones scheduledafterit. - If you despawn from
"Update", the entity still exists for the rest of"Update". TheisAlivecheck on flush prevents double-despawn errors but does not bring the entity back during the stage. - Resources inserted via
cmds.insertResource(...)are visible to the next stage, not the rest of the current one.
The flush happens in World.executeStage(...), immediately after the
executor finishes running every system in the stage:
private void executeStage(ScheduleGraph graph) {
executor.execute(graph, this::executeSystem);
flushPendingCommands(); // here — between stages
}
So if you need a command's effect to be visible within the same stage, split your logic across two stages.
Why commands exist¶
The short version: thread safety. When the scheduler runs systems in
parallel, every worker thread owns its own slice of the iteration. If one
of those workers called world.spawn directly, it would mutate the
archetype graph that another worker is iterating, and you would either
crash or silently lose work.
Commands sidesteps the whole problem:
- Each system gets its own
Commandsbuffer — no contention between parallel systems. - All buffers are drained serially between stages on the main thread.
- Every mutation is re-validated against the live world at flush time.
Even in a single-threaded executor, using Commands is still the right
habit — it keeps your systems portable to parallel execution with zero
refactoring, and it makes "spawn during iteration" trivially safe.
A teaser for relations¶
Commands also has two methods for a feature you have not seen yet:
cmds.setRelation(source, target, new ChildOf());
cmds.removeRelation(source, target, ChildOf.class);
These let you create and destroy edges between entities (parent/child,
owner/owned, targeting, follower-of, whatever you model) from inside a
system, with the same "apply at stage boundary" semantics as the other
commands. The relations chapter covers the full story; for now, just
know that structural relation changes also go through Commands.
A full example¶
import zzuegg.ecs.command.Commands;
import zzuegg.ecs.component.Mut;
import zzuegg.ecs.entity.Entity;
import zzuegg.ecs.system.*;
public record Health(int current, int max) {}
public record DamageOverTime(int perTick, int ticksRemaining) {}
public class DotProcessor {
@System
void tick(
@Write Mut<DamageOverTime> dot,
@Write Mut<Health> hp,
Entity self,
Commands cmds
) {
var d = dot.get();
var h = hp.get();
int newHp = h.current() - d.perTick();
hp.set(new Health(Math.max(0, newHp), h.max()));
if (newHp <= 0) {
cmds.despawn(self);
return;
}
if (d.ticksRemaining() <= 1) {
cmds.remove(self, DamageOverTime.class);
} else {
dot.set(new DamageOverTime(d.perTick(), d.ticksRemaining() - 1));
}
}
}
This one system applies damage, removes the DoT once its duration ticks
out, and despawns entities it kills — all without ever touching the world
directly. The scheduler is free to run DotProcessor.tick in parallel
with any other system that does not write Health or DamageOverTime,
and every mutation lands safely at the end of the Update stage.
Quick recap¶
Commandsis a service parameter:@System void foo(..., Commands cmds).- The eight methods cover every structural mutation you can do from a system body.
- All commands flush at the end of the stage they were enqueued in.
- Use
Commandsany time you mutate the world from inside a system — never callworld.spawn/world.despawn/world.addComponentdirectly from a system. setRelation/removeRelationare how relations plug into the same flush pipeline; more on that in the relations chapter.
What's next¶
You have reached the end of the Basics section. You can now:
- Design components as records and understand how archetypes form.
- Spawn, inspect, and despawn entities.
- Write systems with
@Read,@Write Mut<T>,Res<T>,ResMut<T>,Entity,Commands, andRemovedComponents<T>. - React to newly-added and recently-changed components with
@Filter. - Stay safely scheduled under parallel execution by enqueueing structural
changes through
Commands.
From here you are ready to move on to the advanced tutorials — stages and
scheduling, events, local state, the @Where predicate language, and
relations, which generalise parent/child and targeting into a
first-class concept.