m02 · Systems · reading · 8 min
Ordering — when systems run, and why it's declared
Order is data flow, not arbitrary sequence
Systems form a pipeline. One system writes Velocity; another reads
Velocity to update LocalTransform; a third reads LocalTransform to
cull what’s off-screen. Run them in the wrong order and the culler sees
last frame’s positions, or the mover integrates a velocity that hasn’t
been computed yet. System order is the order data flows through your
transformations — get it wrong and you get a frame of latency or a
visible glitch, not a crash.
Because it’s data flow, Unity refuses to infer it from something incidental like the order you happened to create systems in, or alphabetical file order. Those would couple correctness to irrelevant details — rename a file and break the sim. Instead you declare the relationships you actually depend on:
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(ComputeVelocitySystem))]
[UpdateBefore(typeof(CullingSystem))]
public partial struct MoveSystem : ISystem { /* ... */ }
You’re not saying “run me fifth.” You’re saying “run me after velocity exists and before culling reads my output.” The runtime topologically sorts these constraints into a concrete order. Add a system later and you don’t renumber anything — you state its dependencies and the sort reflows. This is the same philosophy as the whole architecture: express the real constraint, let the runtime schedule.
The three groups exist because a frame has phases
By default the world runs three groups in sequence every frame, and they map onto the natural shape of a simulation tick:
InitializationSystemGroup— runs first. Set up the frame: apply spawns queued last frame, read input, prepare state the simulation will consume. Things that must be true before you simulate.SimulationSystemGroup— the bulk of gameplay. Movement, physics, AI, combat, anything that advances the world state by one step. Most of your systems live here.PresentationSystemGroup— runs last. Read the now-final simulation state to drive rendering, animation, audio. Consumes; does not advance simulation.
The ordering between groups is fixed and meaningful: you initialize,
then simulate, then present, because presentation must see a completed
simulation and simulation must see a completed setup. Within a group,
your UpdateBefore/UpdateAfter constraints decide local order. Groups
are themselves systems and can nest, which is how you build sub-pipelines
(a whole physics step is a group inside Simulation).
Fixed-step simulation is a group, too
Some logic must run at a fixed timestep regardless of framerate — stable
physics integration is the classic case. A variable frame time makes
position += velocity * dt behave differently at 30 fps than at 144 fps,
which is unacceptable for a deterministic or stable simulation. The
FixedStepSimulationSystemGroup runs its members a whole number of times
per frame to consume a fixed accumulator, so each step always sees the
same dt.
Ordering is a dependency graph you can under- or over-constrain
Two failure modes sit on either side of correct ordering:
Under-constrained: you rely on an order that happens to occur today
but that you never declared. It works until an unrelated change reflows
the sort, and then a subtle one-frame-late bug appears with no obvious
cause. If system B reads what system A writes, that dependency must
be an UpdateAfter, even if it “already runs in the right order” — the
declaration is what keeps it true.
Over-constrained: you slap UpdateAfter/UpdateBefore on
everything defensively, creating a near-total order. Now the systems
can’t be reordered even where they’re genuinely independent, and you’ve
thrown away scheduling freedom the runtime could have used. Declare the
dependencies you have, not a sequence you imagine.
What this buys you
You can now place a system in the right group and constrain it to run after its inputs and before its consumers, and you understand that you’re describing data flow rather than picking a slot. But there’s a category of operation a system cannot do mid-sweep at all — changing which components entities have — and the next lesson is about the boundary that handles it: the command buffer.