DOTS//CORE local · not synced

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.

m02.l02