Lesson 8
Systems and the pipeline
A system is a query, a function and a phase. What order systems run in, why the function is called once per table rather than once per entity, and what writing through a field does and doesn't do.
A system is three things: a query, a function, and a place in the frame. world.progress() walks through the systems in order, and for each one iterates its query and calls the function. Like everything else in flecs, a system is an entity, so it can be named, looked up, disabled and inspected.
The pipeline decides the order
Systems are grouped by phase, and phases run in a fixed order each frame:
OnLoad → PostLoad → PreUpdate → OnUpdate → OnValidate → PostUpdate → PreStore → OnStore
In C++ a system without a phase goes in OnUpdate (in C, a system without a phase isn’t part of the pipeline, and only runs when you call ecs_run). Within a phase, systems run in the order they were created. That’s the whole rule: it doesn’t matter which file a system lives in, only its phase and, within the phase, when the system entity was created. Module import order matters only because importing a module is what creates its systems. (You can add your own phases with DependsOn, to slot them between the built-in ones.)
Watch a frame
Render is declared first but lives in OnStore. Input is declared last but lives in PreUpdate. Teleport matches nothing yet. Step through the first world.progress(), then add Teleporting to Rock and run another frame.
Loading the flecs debugger…
What to look for:
- Phase first, then declaration order.
Inputruns first, thenMoveandTeleport(bothOnUpdate, in creation order), thenRender. - One call per table, not per entity.
Movematches two tables,[Position, Velocity, Player]and[Position, Velocity], so its function is called twice, with three entities in total.Rendermatches every table withPosition. With.each()flecs loops over the rows for you, but underneath it’s still one batch per table. - No matching tables, no call. In the first frame
Teleport’s function doesn’t run at all. Afterrock.add<Teleporting>()Rock moves to a new table, the query matches it, and the next frame callsTeleportonce. Rock’s old table is now empty, and empty tables are skipped. - Writes go straight into the column.
InputandMovechange values in place: no command, no move, no event. The locals panel is the iterator:it.table,it.countand each field’s values, as a debugger would show theecs_iter_tin your callback.
Say what you read and write
Declare fields you only read as const in C++ (or [in] in the query language). It documents the system, and it’s information flecs uses: the pipeline needs to know what each system reads and writes to insert sync points in the right places (next lesson). It has nothing to do with threading: a multi_threaded system splits its own rows across the worker threads, and systems still run one after another. A system that writes everything it touches gives the pipeline nothing to work with.