Systems
Stability: Stable.
A system is a cross-cutting gameplay module — a shrinking zone, a loot layer, a hazard — that attaches to a mode and runs alongside its runtime. It is not part of the mode and not part of the engine core: it lives in its own resource, registers once with the engine, and any mode opts in by name.
Systems exist so a mechanic that spans modes isn't copy-pasted into each one or
welded into the engine. A duel, a deathmatch, and a future battle-royale can all
attach the same zone system; you can ship your own, or replace one wholesale by
registering the same id.
System vs driver
A driver (or a tick hook) is the mode's runtime — it owns the
whole match loop, and a mode has exactly one. A system composes alongside
whatever runtime the mode uses, ticks independently, and a mode can attach
several. Crucially, a system ticks even for a mode with no runtime at all (no
rules, no driver, no tick) — which is exactly what lets a teamless,
free-for-all mode get a working zone without writing a bespoke loop.
Driver / tick | System | |
|---|---|---|
| Owns the match loop | yes (one per mode) | no — composes alongside |
| How many per mode | one | any (systems = { 'zone', 'loot' }) |
| Ticks for a runtime-less mode | n/a | yes |
| Registered with | engine (registerDriver) | engine (registerSystem) |
| Named by the mode via | driver = '…' | systems = { '…' } |
registerSystem(id, def)
Register a system from its own resource (it dependency-s on engine). Like
registerDriver, this is an engine export — a
system is imperative logic, not the pure data a mode descriptor is.
-- resources/[systems]/system_zone/server.lua
local STATE = {} -- your per-scene state, keyed by sceneId — the engine keeps none for you
exports.engine:registerSystem('zone', {
init = function(sceneId, snapshot, config)
STATE[sceneId] = {} -- seed from snapshot.map + config
end,
tick = function(sceneId, snapshot)
-- shrink the ring; push it to clients; hurt whoever is outside
exports.engine:announce(sceneId, 'system_zone:update', { --[[ ring ]] })
end,
teardown = function(sceneId)
STATE[sceneId] = nil
end,
})def is a table of lifecycle hooks (all optional; you must supply at least
one). Each runs in your VM, receives a scene id plus read-only data, and
acts on the scene through the write-exports — the same
boundary as a mode hook. You keep your own per-scene state keyed by
sceneId; the engine stores none for you.
| Hook | Fired | Signature |
|---|---|---|
init | scene created, after the mode's setup | (sceneId, snapshot, config) |
tick | every 500 ms, after the mode runtime — for every mode, runtime or not | (sceneId, snapshot) |
onSpawn | a combatant (re)spawned (ground spawn or air drop) | (sceneId, src) |
onDeath | a combatant died (killerSrc nil = world / suicide / timeout) | (sceneId, victimSrc, killerSrc) |
teardown | scene destroyed — free your per-scene state | (sceneId) |
A system whose owning resource stops is dropped automatically (a dead callback is never invoked).
Attaching a system to a mode
A mode names the systems it wants in its descriptor — pure
data, exactly like driver = '…'. Behaviour stays in the system resource.
defineMode{
id = 'br', label = 'Battle Royale',
-- ...
systems = {
'loot', -- bare id
{ id = 'zone', config = { dps = 2 } }, -- id + a per-mode config block
},
}Each entry is a system id string, or a { id, config } table. The optional
config is handed back to that system's init for this mode — the same system
serves several modes with different tuning. The id is validated for shape only
at the mode's load (the SDK can't see the engine's system registry); if the named
system isn't registered when the mode loads, the engine prints a "not registered
yet" note and attaches it once its resource comes up — benign, like the
unknown-driver case.
What a system reads and writes
Reads. init/tick get the scene snapshot. For a zone, the
snapshot's map carries the arena placements so you can derive the play area:
tick = function(sceneId, snapshot)
local areas = snapshot.map.areas -- per-team drop circles { x, y, z, r, ... }
-- average them for a centre; live ped positions come from the standard
-- server natives in your VM: GetEntityCoords(GetPlayerPed(src))
endWrites. Everything in write-exports — most often
announce (push your own client event to the scene; your system's client script
renders it) and the hazard primitives for a zone:
exports.engine:hurt(sceneId, src, hp)— environmental damage to a combatant.exports.engine:kill(sceneId, src)— instant elimination.
Player peds are client-authoritative, so these route through the client and the resulting death flows through the engine's normal death funnel — elimination, killfeed, spectate and stats all fire unchanged; your system never special-cases dying. See Hazards.
Client side
A system renders client-side in its own resource. The server side fans a
client event with announce; the system's client script listens and draws. Hook
engine:left to clear your visuals/props when a player leaves a match (the engine
fans it). Nothing engine-client is required.
Example skeletons
The repo ships two empty example systems under resources/[systems]/ —
system_zone and system_loot. They register, attach, and receive the full
lifecycle, but implement no behaviour: copy one as a starting point, or replace it
with your own resource that registers the same id.
See also
- Mode descriptor — the
systemsfield. - Write-exports —
announce,hurt,kill, and the rest. - Hooks & the snapshot — what
init/tickreceive. - Drivers — the mode's own runtime, which a system composes alongside.