Systems
A system is code that runs every tick. Its parameters say which entities it runs for, and what it does with their data.
system ClampPlayer(mut Transform trs, with Player)
{
if (trs.position.y < 0)
trs.position.y = 0;
}Parameters are the query
A system's component parameters describe the entities it runs for. Each parameter's modifier says how the system relates to that component or singleton:
| Modifier | Meaning |
|---|---|
| none | Reads it |
mut |
Writes it |
with |
The entity must have it. No data, so no data dependency. |
without |
The entity must not have it. |
So ClampPlayer runs for every entity that has a Transform and a Player, and changes its Transform.
Keeping what a system touches (its access) apart from which entities it picks (its filters) is what lets the engine run systems side by side safely: two systems that only filter on the same component never wait for each other.
Only the amber row has Moving without Frozen, so only those entities match Move. Their positions change by time.dt in the match, and views draw the result. The button sends a command to pause or resume the match's movement.
settings { title = "System queries"; tickRate = 30; maxPlayers = 1; }
component Body { float2 position; float speed = 1; }
component Moving { }
component Frozen { }
singleton Demo { bool running = true; }
command SetRunning { bool value; }
scene Main { }
event(Spawned) Setup(with Main)
{
for (var i = 0; i < 3; i++)
{
Spawn(Body { position = float2(i * 1.2 - 1.2, 1) }, Moving);
Spawn(Body { position = float2(i * 1.2 - 1.2, 0) }, Moving, Frozen);
Spawn(Body { position = float2(i * 1.2 - 1.2, -1) });
}
}
// Only the first row matches both filters.
system Move(mut Body body, Time time, Demo demo, with Moving, without Frozen)
{
if (!demo.running) return;
body.position.x += body.speed * time.dt;
if (body.position.x > 2)
{
body.position.x -= 4;
this.Snap(); // Wrap immediately instead of interpolating across the lane.
}
}
// The button sends a match command instead of changing state in a view.
command(SetRunning change, PlayerID sender) Run(mut Demo demo)
{
demo.running = change.value;
}
view Background()
{
Draw.Clear(Color(0.086, 0.086, 0.094));
Draw.SetCamera(float2(0), 3);
for (var i = -1; i <= 1; i++)
Draw.Line(float2(-2.1, i), float2(2.1, i), 0.025, Color(0.24, 0.25, 0.27));
}
view Active(Body body, with Moving, without Frozen)
{
Draw.Circle(body.position, 0.19, Color(0.98, 0.75, 0.14));
}
view Blocked(Body body, with Moving, with Frozen)
{
Draw.Circle(body.position, 0.19, Color(0.98, 0.44, 0.52));
}
view Excluded(Body body, without Moving)
{
Draw.Circle(body.position, 0.19, Color(0.64, 0.64, 0.68));
}
view Labels(Demo demo)
{
var first = Draw.WorldToScreen(float2(0, 1));
var second = Draw.WorldToScreen(float2(0, 0));
var third = Draw.WorldToScreen(float2(0, -1));
Draw.SetScreen();
Draw.Text("Systems run every tick", float2(16, 14), 18, Color.white);
Draw.Text("with Moving, without Frozen", float2(16, first.y - 34), 14, Color(0.98, 0.75, 0.14));
Draw.Text("Moving + Frozen: excluded", float2(16, second.y - 34), 14, Color(0.98, 0.44, 0.52));
Draw.Text("No Moving: excluded", float2(16, third.y - 34), 14, Color(0.64, 0.64, 0.68));
GUI.SetTheme(GUITheme { radius = 6 });
if (GUI.Button(Rect(16, Screen.height - 48, Screen.width - 32, 32), demo.running ? "Pause the match" : "Resume the match"))
Send(SetRunning { value = !demo.running });
}Other parameters:
-
Singletons, read or
mut, like components:system Score(mut Stats stats, Rules rules). -
The input, or
Devices: what the player who owns the entity does (see Input). -
The
World: any other entity's components (below). -
A read-only
query: another set of entities to inspect (below).
Views take the same parameters, and one filter more: with ownership and without ownership, the entities this machine's player owns, or doesn't (see Your own entities). It's for views only: a system runs the same on every machine, so there it's an error, which points to with Owner, the entities any player owns.
The entity itself isn't a parameter: it's this (see Entity handles).
Another set of entities
A bullet system runs for bullets, but needs to inspect fighters too. A query parameter selects that second set:
system BulletHits(Body shot, Bullet bullet, query (Body body, with Fighter) fighters)
{
foreach (var fighter in fighters)
{
if (Intersects(shot, fighter.body))
fighter.entity.Send(Hit { damage = bullet.damage });
}
}Intersects is a function the game defines. The outer Body and Bullet parameters select each bullet; Body and with Fighter inside the query select the candidates it inspects. The query never requires a bullet to be a fighter.
Inside foreach, fighter.body is read-only component data and fighter.entity is that fighter's entity handle. Add more component bindings when their values are needed, and with or without filters when only presence matters:
query (Body body, Health health, with Fighter, without Invulnerable) fightersSparse components and tags are checked for each candidate, and an empty query runs no iterations. A system with only query parameters runs once per tick.
Queries use normal deterministic ECS traversal. They do not sort by entity ID, distance or hit time. For a swept projectile, calculate each candidate's intersection and keep the earliest one before sending a hit.
Query data reads the current world and participates in the schedule. A system that also changes a queried component runs on one thread, following the same rule as reading its own writes through World: it sees rows changed earlier in that run. Events and structural changes still wait for the end of the tick, so sending a hit or recording a destroy through fighter.entity does not change what this iteration reads.
Queries have read-only component bindings and component filters. Queries and their whole rows are used directly in foreach in the system's ordinary control flow, outside parallel loops and callback blocks; they cannot be stored or passed to functions. A component value such as fighter.body can be copied or passed to a function. It follows the usual component copy rules, so a method that uses this needs the entity's own component parameter.
Systems, views and local handlers take queries. A view's rows are the match as it draws it, blended between ticks, or verified: see Views and the match. A local handler's are the match as the tick that sent its event left it: see Events. The match's own event handlers, command handlers and tasks take none yet.
Other entities: the World
A system often needs another entity's component in the same tick: the target it attacks, the leader it follows. A World parameter gives it, by the entity's handle:
system Attack(Attacker attacker, World world, mut Stats stats)
{
if (!world.Has<Health>(attacker.target)) return;
var target = world.Get<Health>(attacker.target);
if (target.value <= attacker.damage) stats.kills += 1;
}world.Get<Health>(e)is a copy of that entity'sHealth, or its zero value when it has none.world.Has<Health>(e)tells whether it has one.world.TryGet<Health>(e, out var health)is both in one: whether it has one, and the copy, or the component's default when it has none (see Out parameters).world.Set<Health>(e, value)puts one back, and needsmut World. It does nothing for an entity without one.- A system that takes
Healthitself can'tSet<Health>: its own entity's goes through its parameter. The exception is a parameter it only reads, beside whichworld.Set<Health>(this, value)sets this entity's own. That's for a system that looks at every entity and changes a few: the parameter stays as the system came to the entity.
What Get gives is a copy, as a list's element is, so changing another entity's component is taking it out, changing it and putting it back:
system Heal(Healer healer, mut World world)
{
mut var health = world.Get<Health>(healer.patient);
health.value += 1;
world.Set<Health>(healer.patient, health);
}The signature doesn't say which components a system reaches this way: the engine reads it off the calls. Each counts as access to every entity's component, so the system is scheduled like one that touches them all, and one that calls Set runs on one thread. The schedule and your editor show what each system reads and writes through its World. Where the change can wait for the end of the tick, an event (e.Send(Hit { ... })) keeps systems running side by side; see Events.
A function or method can take the World too, and the system passes its own:
bool TryFind(Entity target, World world, out Health health)
{
return world.TryGet<Health>(target, out health);
}
system Aim(mut Attacker attacker, World world)
{
if (TryFind(attacker.target, world, out var health)) attacker.threat = health.value;
}What the function reaches through it counts as the system's own, through however many functions it's passed, so nothing about the schedule changes: Aim reads every entity's Health. A function that calls Set takes mut World, and so does the system that passes it. The rules above still hold, said at the call: a system that takes Health can't call a function that sets Health, and a view can't call one that changes the match.
Views and event handlers take a World too; a view only reads the match through it, and can change local entities with mut World. A local handler of one of the match's events takes none: it hears the match as the event left it, and the World is the match as it is now.
Once per entity, or once per tick
A system with component parameters runs once for every matching entity, and this is the entity it's running for. One without them runs once per tick, for no entity, so it has no this:
system CountDown(mut Round round, Time time)
{
round.timeLeft -= time.dt;
}return; ends the system for the current entity, and it goes on with the next one.
Order
Systems run in one order, the same on every machine. By default, it follows the files, sorted by path, then the order of the systems in each file. A game's packages come first, each after the packages it needs.
[Before] and [After] change that. They take any number of systems:
[After(Gravity, Collisions)]
system Move(mut Body body, Time time)
{
body.position += body.velocity * time.dt;
}Of the systems whose constraints are met, the earliest in the default order runs next. Constraints that form a loop are an error that names the loop. Hover a system in your editor to see its place in the order.
Systems can share a name when their parameters differ, as functions can. Nothing calls a system, so where one of them is named, it's written with its parameters' types, in order, with its filters and without names or mut:
system Move(mut Body body, Speed speed, with Player) { ... }
system Move(mut Body body, Speed speed, with Enemy) { ... }
[After(Move(Body, Speed, with Player)), Before(Move(Body, Speed, with Enemy))]
system Aim(mut Turret turret, Body body) { ... }A name alone is fine while only one system has it. When several share it, [After(Move)] is an error that lists them as they're written, and so is a parameter list that isn't exactly one's. Views and event handlers work the same way, and the schedule and the profiler show a shared name with its parameters.
A system marked virtual can be replaced by another package's override, which runs where it would have: at its place in this order, with its attributes, and under its name where others name it (see Virtual and override).
Named phases
An enum gives shared names to parts of the tick. Assign systems to its phases with Stage:
enum TickStages { Input, Simulation, Late }
[Stage(TickStages.Simulation)]
system Move(mut Body body, Time time)
{
body.position += body.velocity * time.dt;
}
[Stage(TickStages.Simulation, 10)]
system Collisions(...) { ... }
[After(TickStages.Simulation)]
system Finish(...) { ... }Phases follow the enum members' declaration order. The optional comma-separated offset orders systems within a phase: lower first, with 0 as the default. Negative offsets work too, and the enum needs no numeric spacing. Equal offsets keep the usual order and can use Before or After on particular systems.
After(TickStages.Simulation) follows every system in that phase, including its offsets; Before precedes all of them. Empty phases keep their position. A system in a later phase waits for the earlier phases to finish, while systems at the same offset can still run alongside each other. Unassigned systems keep their default order wherever the constraints allow. Separate enums have independent phase orders.
The same attributes order views among views and event handlers among the handlers of one event. They don't move code between the tick, the frame and event handling.
Running side by side
Two systems that write the same component isn't a mistake: it's normal. Such systems run one after the other, in the order above. Systems that share nothing they write can run at the same time. The compiler works this out from the parameters, and can tell you why each system waits (see The schedule):
stage 2 Move (per entity)
after Steer: both write BodyDeclaring more than a system uses makes others wait for nothing, so it's a warning: a parameter that's never used, or a mut one that's never written (a mut World that never calls Set too). A component that's only there to pick entities belongs in with. The editors offer quick fixes for these.
Written in C
A system's code can be C's: a library's, or a loop over every entity's numbers that's worth writing by hand. extern system Move(mut Position position, Velocity velocity); declares one, with no body, and C is handed the components as the arrays the match keeps them in, rather than an entity at a time. It's a system like the others to everything on this page: its parameters pick its entities, and it's ordered and scheduled by them. See Systems written in C.
What systems can't do
Systems run the match, so they can't read this machine's local state, and they can't draw: drawing happens every frame, in views.