Systems and tick phases
A system is a function the engine calls once per tick, in a chosen phase, with the tick’s delta time in seconds. Systems are how you run per-tick logic over many entities (with a query), and how you hook, reorder, replace or switch off the server’s own behaviour. Your systems live in the engine’s own system registry: they run in the same pipelines as native systems, are ordered against them by name, and show up with timings in the engine’s system list.
[PluginInfo("ManaDemo", Version = "1.0.0")]public sealed class ManaPlugin : Plugin{ private Query<Mana> _mana = null!;
protected override void OnEnable() { Ecs.RegisterComponent<Mana>("manademo:mana"); _mana = Ecs.Query<Mana>();
Systems.Add("regen", Phase.Tick, dt => { foreach (var chunk in _mana) { var mana = chunk.Field<Mana>(0); for (int i = 0; i < mana.Length; i++) mana[i].Value += 1; } }); }}
public struct Mana{ public int Value;}Phases
Section titled “Phases”Phase values, in the order they run each tick:
| Phase | What happens there |
|---|---|
NetworkReceive |
drain inbound packets, apply them to entities |
PreTick |
timers, input integration |
Tick |
game logic: entities, blocks … (the default for Add) |
PostTick |
removals, collision resolution, derived state |
Tracking |
spatial index update, view diffs |
NetworkSend |
build and encode outbound packets |
AsyncIntegration |
integrate worker results (chunk loads, lighting) |
Deferred changes (entity creation, component add/set/remove) made in one phase are merged at the phase boundary and are visible to every later phase of the same tick. A Tick system sees an entity a native NetworkReceive system created in the same tick; the other way round only from the next phase on.
Ordering against other systems
Section titled “Ordering against other systems”Systems.Register(Phase.Tick, "regen", dt => ...); // end of the phase (same as Add)Systems.After("vanilla:living_physics", "after_physics", dt => ...); // right behind a native or scripted systemSystems.Before("mobs:ai", "before_ai", dt => ...); // right in front of onevar h = Systems.Replace("mobs:ai", "my_ai", dt => ...); // disables mobs:ai, runs in its placeh.Dispose(); // ... and restores mobs:aiSystems.SetEnabled("mobs:spawner", false); // re-enabled when the plugin unloadsThe names you can anchor on are listed on the generated native systems page.
- Names are the engine registry’s names and are unique. Yours are namespaced by the plugin:
Add("regen", ...)in a plugin calledHelloregistersHello:regen(a name that already starts withHello:is kept). Registering a name that a native system or another plugin holds throws.SystemHandle.Namereturns the effective name. - Anchors for
After/Before/Replaceare full registry names (not namespaced), and the new system takes the anchor’s phase.Registerwith a placement other thanAppendneeds an anchor. Replaceis reversible by design: the original is disabled and yours runs right after it. Dispose the handle or unload the plugin and the original comes back. Two plugins replacing or disabling the same system do not fight: it is re-enabled only when the last holder goes.- Registry changes made from inside a system (
Register,Dispose,SetEnabled) are queued and applied right after the tick; the handle is valid immediately. SetEnabled(name, bool)works on native and scripted systems, returnsfalsefor an unknown name, and is undone for a system your plugin disabled when it unloads.
Systems in the plugin worker
Section titled “Systems in the plugin worker”Your systems run in the plugin worker, inside the window of the phase they were registered for: after the engine’s systems of earlier phases and before the later ones. A system positioned next to an engine system (After, Before, Replace) runs where the engine’s registry puts it, through a proxy. The dt a system receives is the engine’s frame delta.
Two things differ from in-process plugins:
- A component your plugin defines lives in the worker. Engine systems and engine queries do not see it.
- An engine component can be read from the worker, but not written: a write is ignored with a warning.
Hot reload and system names
Section titled “Hot reload and system names”A reloaded plugin registers its successor before the predecessor is released. For that moment the new system runs as Hello:regen#2, and is renamed back to Hello:regen (at the same position) once the old one is gone.
Systems versus events versus the scheduler
Section titled “Systems versus events versus the scheduler”| You want | Use |
|---|---|
| react to something that happened | an event |
| run logic every tick over many entities | a system and a query |
| run something later or periodically, not per entity | the scheduler |
| change how vanilla behaves | Replace, Before, After or SetEnabled on a native system |
Managed systems are not multithreaded: they run on the tick thread, one after another, never at the same time as each other or as your event handlers.