Choosing how to drive a mode
The engine ticks each scene every 500 ms. You give a mode its behaviour in one of three ways, in order of precedence:
mode.tick— a custom hook that replaces everything below (full imperative control).mode.rules— a declarative block, auto-run on the built-in rules runtime. The common way; no code.mode.driver— a named native driver registered viaregisterDriver(an escape hatch for a shaperulescan't express).
A mode with none of these does not tick. See the Drivers reference and Hooks.
Most modes use rules. The two classic shapes — elimination rounds and team
deathmatch — are rules blocks; so is zone control (King of the Hill, Domination).
rules — elimination rounds
Round-based, where a death takes you out for the round: 1v1 Duel,
search-and-destroy styles. Flow: waiting → live → ended → …
- A round starts when both teams have at least
minToStartplayers — live immediately. The battleground decides the entrance: groundspawnsplace each player standing on their own spot;areasparachute them in and the fight can start mid-air. - The round ends on a team wipe, or when the
livetimer expires (more survivors wins; tie = draw). - A team reaching
win.roundWinstakes the match. - Dead players spectate their living teammates until the next round.
rules = {
structure = 'rounds',
phases = { live = 150, post = 5 },
roundEnd = { 'elimination', 'timeLimit' },
win = { roundWins = 5 },
},
-- plus: teamCount, perTeam, armor, loadoutrules — continuous team deathmatch
Players respawn and keep fighting; the match is won on score.
- Continuous
live; dead players respawn afterphases.respawnseconds. Setrespawns = trueso the engine does not send the dead to spectate — the runtime brings them back. - Team score = the sum of its members' kills, accrued into each team's score so
the default HUD/board render it unchanged. First to
win.score(or the lead atwin.timeLimit) wins.
respawns = true,
rules = {
structure = 'continuous',
phases = { live = 600, post = 8, respawn = 3 },
score = { kill = 1 },
win = { score = 50, timeLimit = 600 },
},
-- plus: teamCount, perTeam, armor, loadoutAdd a
zonesblock (a radius a lone team controls to scorescore.zoneTick) and the same continuous shape becomes King of the Hill / Domination — see the mode descriptor reference for the fullrulesvocabulary.
When to write a custom tick
Reach for a tick hook only when no rules shape fits — objective modes with
client interaction, bespoke scoring, phase logic the runtime doesn't model. A tick
replaces rules: the engine calls it every 500 ms with the scene id and a
read-only snapshot, and you drive the match yourself
through the write-exports:
local MODE = {
id = 'myobjective', label = 'My Objective Mode',
teamCount = 2, perTeam = 4,
tick = function(sceneId, snap)
if snap.state == 'waiting' and snap.bothPresent then
exports.engine:spawnAll(sceneId, { alive = true })
exports.engine:setState(sceneId, 'live', 300)
exports.engine:announce(sceneId, 'engine:live')
elseif snap.state == 'live' then
-- your scoring / win logic here, using snap + setVar/setRound/...
end
end,
}A tick hook runs in your mode's VM, so it can hold private state and call your
own helpers — but your resource must stay started for it to keep running (the engine
drops the mode when its resource stops).
You can also register a shared native driver other modes can select by name with
exports.engine:registerDriver(name, { tick = ... }). See
Drivers.
Quick comparison
rules rounds | rules continuous | custom tick | |
|---|---|---|---|
| Respawn during play | No (spectate) | Yes (respawns = true) | You decide |
| Win condition | win.roundWins | win.score / timeLimit | You decide |
| Phases | live→post | continuous live | You decide |
| Code in the mode | none | none | a tick |