Systems
A system is a function that runs game logic in specific phases of the World.
Creating a system
Add systems to the world with world:addSystem(), passing a configuration table. The run function receives the current frame delta time and the world.
world:addSystem({
phase = tecs.phases.Startup,
run = function(dt: number, world: tecs.World)
print("The game is starting up!")
end
})Organize systems with plugins
Systems are typically grouped into plugins so related queries, resources, and systems can be installed together with a single world:addPlugin(...) call. addSystem works anywhere you have a world reference, but plugins keep related code localized.
World methods
These methods are available on every World.
| Method | Description |
|---|---|
world:addSystem | Add a system to the world's pipeline. |
world:removeSystem | Remove a named system from the world's pipeline. |
world:addSystem
Adds a system to the world's pipeline.
function World:addSystem(config: SystemConfig)Parameters:
config: System configuration.
world:removeSystem
Removes a named system from the world's pipeline.
function World:removeSystem(systemName: string)Parameters:
systemName: Name of the system to remove.
WARNING
Systems need an explicit name to be removable. Auto-named systems cannot be removed from user code.
System configuration settings
| Field | Type | Required | Description |
|---|---|---|---|
phase | Phase | Yes | The phase the system runs in. |
run | function(dt: number, world: tecs.World) | Yes | The function to call each time the system runs. |
name | string | No | Name of the system, used for debugging, ordering, and removeSystem. |
runIf | function(dt: number, world: tecs.World, name: string): boolean | No | Predicate that determines if the system should run this frame. |
before | {string} | No | System names this one should run before (soft; ignored if missing). |
after | {string} | No | System names this one should run after (soft; ignored if missing). |
Systems without an explicit name are auto-named on insertion; removeSystem(name) only works when a name was declared explicitly.
Within a phase, systems run in the order they were added unless before / after constraints re-sort them. See Deferred Operations for the rules about when mutations inside a system apply instantly versus stage.
Naming systems
You can give systems a name using the name property. This makes it easier to debug, and also allows other systems to be added relative to the system by referencing the name.
world:addSystem({
phase = tecs.phases.Update,
name = "MyUpdateSystem",
run = function(dt: number, world: tecs.World)
-- update logic...
end
})Conditionally running systems
To conditionally skip a system, provide a runIf predicate. It receives the frame delta, the world, and the system's name, and returns true if the system should run this frame.
runIf = function(dt: number, world: tecs.World, systemName: string): boolean
return world:peekState() == "game"
endTecs provides built-in scheduling helpers that cover the common cases.
Scheduling helpers
tecs.runif.after(delay)
Runs a system once after a delay (in seconds), then automatically removes it from the world.
world:addSystem({
phase = tecs.phases.Update,
name = "DelayedMessage",
runIf = tecs.runif.after(2.0), -- Run after 2 seconds
run = function(_dt: number, _world: tecs.World)
print("This runs 2 seconds after the world started")
end
})tecs.runif.every(interval, jitter?)
Runs a system repeatedly at regular intervals.
world:addSystem({
phase = tecs.phases.Update,
name = "PeriodicUpdate",
runIf = tecs.runif.every(1.0), -- Run every second
run = function(_dt: number, _world: tecs.World)
print("One second has passed")
end
})Provide optional jitter in the second argument to desynchronize systems using the same interval. Jitter is the ± number of adjusted seconds.
tecs.runif.cooldown(duration)
Fires immediately on the first update, then suppresses execution for the cooldown duration before firing again.
world:addSystem({
phase = tecs.phases.Update,
name = "HealthRegen",
-- Run immediately, then every 5 seconds
runIf = tecs.runif.cooldown(5.0),
run = function(dt: number, world: tecs.World)
-- Regenerate health for all entities
end
})tecs.runif.inState(name)
Runs a system only when the given state is on top of the state stack.
world:addSystem({
phase = tecs.phases.Update,
name = "GameplaySystem",
runIf = tecs.runif.inState("game"),
run = function(dt: number, world: tecs.World)
-- Only runs when "game" is the current state
end
})tecs.runif.negate(predicate)
Inverts another runIf predicate, allowing you to compose conditional logic.
-- Run when NOT in game state
world:addSystem({
phase = tecs.phases.Update,
name = "PauseMenuSystem",
runIf = tecs.runif.negate(tecs.runif.inState("game")),
run = function(dt: number, world: tecs.World)
-- Only runs when NOT in game state
end
})tecs.runif.both(lhs, rhs)
Combines two runIf predicates with logical AND. The system will only run if both predicates return true.
world:addSystem({
phase = tecs.phases.Update,
name = "PeriodicGameplayUpdate",
runIf = tecs.runif.both(
tecs.runif.inState("game"),
tecs.runif.every(2.0)
),
run = function(dt: number, world: tecs.World)
-- Runs every 2 seconds, but only when in "game" state
end
})tecs.runif.either(lhs, rhs)
Combines two runIf predicates with logical OR. The system will run if either predicate returns true.
world:addSystem({
phase = tecs.phases.FixedUpdate,
name = "AnimateWater",
runIf = tecs.runif.either(
tecs.runif.inState("game"),
tecs.runif.inState("editor")
),
run = function(dt: number, world: tecs.World)
-- animate water in the game or editor states
end
})Custom runIf predicates
You can also write your own runIf predicates for more complex logic:
local health: number = 100
world:addSystem({
phase = tecs.phases.Update,
name = "LowHealthWarning",
runIf = function(_dt: number, _world: tecs.World, _systemName: string): boolean
return health < 25
end,
run = function(_dt: number, _world: tecs.World)
print("Warning: Low health!")
end
})Adding systems before or after other systems
Add systems before or after other systems in the same phase. Tecs topologically sorts systems to ensure correct ordering with no cycles.
These ordering constraints are soft: if a referenced system doesn't exist in the phase, the constraint is silently ignored. This makes plugins composable; a system can declare after = {"physics.applyTransform"} without requiring the physics plugin to be present. If the referenced system is added later, the pipeline automatically re-sorts to respect the constraint.
Add a system before another named system:
world:addSystem({
phase = tecs.phases.Update,
name = "MyOtherUpdateSystem",
run = function(dt: number, world: tecs.World)
-- update logic...
end,
before = {"MyUpdateSystem"}
})Add a system after another named system:
world:addSystem({
phase = tecs.phases.Update,
name = "YetAnotherUpdateSystem",
run = function(dt: number, world: tecs.World)
-- update logic...
end,
after = {"MyUpdateSystem"}
})Removing systems
Call world:removeSystem(name) to pull a system out of the pipeline. The system must have been registered with an explicit name; auto-named systems aren't removable from user code.
world:removeSystem("MyUpdateSystem")tecs.runif.after(delay) takes advantage of this to clean itself up after firing: the predicate calls removeSystem internally once the delay elapses.