A match is a state machine —
WARMUP → COUNTDOWN → LIVE → INTERMISSION → MATCH_END — and one call moves
it. No Timer, no _process, no wall clock.
Requires dot-core.
var match_node := DotMatch.new()
match_node.rules = DotMatchRules.deathmatch(30)
match_node.position_fn = func(key): return player_position(key)
add_child(match_node)
match_node.respawn_due.connect(func(key, spawn, tick):
spawn_player(key, spawn.spawn_transform()))
match_node.tick(tick) # once per simulated tick
match_node.report_kill(killer, victim, &"rifle", tick, headshot)
A match that ticks itself ticks on whatever schedule the engine gives it, which is not the schedule the simulation runs on — and a server whose round timer and whose netcode disagree about what time it is produces a round that ends on a different tick for every client.
What winning means lives in a DotMatchRules resource. DotMatch never asks
what mode it is running.
Where a game plugs in
- DotMatchRules subclass
_points_for_kill,_round_outcome,_round_winner,_respawn_delay.DotRulesEliminationis a worked example — and it treats zero teams standing as the end of the round, not as “keep going”, because a grenade that kills the last player on both sides happens often enough to matter and waiting for a side that no longer exists is a server that has to be restarted.- DotSpawnPoint / DotSpawnSelector
danger_fnandfilter_fnare the two hooks. The default rule is furthest-from-the-nearest-threat among the available points, and threats are enemies only — spawning people away from their own team scatters a squad across the map. Points have a cooldown, which is what stops two players spawning inside each other.- DotMatch.position_fnCallable
Where a player is.
dot-matchdoes not know about your scene.- DotTeam / DotTeamManager
Sides, assignment, deterministic autobalance, and
spawn_tagso each team spawns in its own base.- DotScoreboard / DotPlayerScore / DotKillFeed
Who is playing and how they are doing. The kill feed is data, not formatted strings.
- DotMatchNetSync
What to replicate, without naming a
dot-nettype.- Signals
state_changed,round_started,round_ended,match_ended,round_preparing,players_released,freeze_changed,player_added,player_left,player_idle,score_changed,team_changed,balanced,kill_recorded,respawn_due.
The countdown places everyone, then lets go
Every round is preceded by a countdown, entered from warmup and from an intermission or a match end with players present. The round is reset and everyone placed when the countdown begins; round_started at LIVE places nobody. A game builds its map or round layout on round_preparing(round_number), which increments the round number and fires just before the placement, and holds bodies still while is_frozen() is true (the rules’ freeze_during_countdown; freeze_changed(frozen) is the edge, and a player spawned mid-countdown is spawned frozen too). DotMatchNetSync replicates the flag as net_frozen, so a client holds its own player without asking the server. countdown_sec of 0 keeps the older live-then-place order for games that build their round in round_started.
players_released(tick) fires just before round_started when players were placed in a countdown. Spawn protection granted at placement can run out inside the freeze, so a game re-arms it here, counting from this tick.
begin_round_now() starts a fresh round on this tick from any state but idle, which is what a map change needs. It emits no round_ended for the round it replaces, because that round was called off, not won, and an ended round reaches progress, achievements and the “X wins” banner. Inside a countdown it just goes live on the prepared round, with no second reset or placement. stop() in a countdown hands the round number back and clears the placement queue.
A spawn point freed under the match is forgotten rather than reset. With no spawns_ref, the match collects every DotSpawnPoint under the current scene, which in a process holding two games includes the other one’s; resetting a freed point crashed the engine.
Three failure modes it is built around
A scoreboard that rewards rage-quitting. Records survive a disconnection and a reconnecting player gets their kills back. They are keyed by a stable player key, never a peer id — a peer id is reassigned the moment somebody reconnects, and a scoreboard keyed by one hands the next player to join the previous player’s kills.
Two machines that disagree. Every ordering decision has an explicit tie-break: ranked scoreboards fall back to the player key, spawn selection to the node name, team assignment to the lower team id, respawn batches to a sort. Left to dictionary iteration order, a server and a client produce different answers — and for team assignment that means they disagree about friendly fire.
A round that never ends. See DotRulesElimination above.
Spawning badly beats not spawning at all
When every point is on cooldown or occupied, the selector falls back and spawns somebody anyway. The alternative is a player who is dead until a point frees up, which on a busy map with a short round is the rest of the match.
`respawn_due` only ever fires on the authority
It comes out of the respawn queue, which runs on the server — and so does any player_spawned a game emits from it. A mirroring client
waiting for one to find its own player waits for ever — and the symptom is
misleading: the HUD binds by id and works, the scoreboard works, the connection
is live, and there is no camera and no world at all. Use the game’s own
player_added on a client.
