Build your first mode
We'll build Skirmish, a small two-team elimination mode, end to end. It is
pure data: a small declarative rules block runs it on the engine's rules runtime,
so we write no game logic at all. By the end you'll understand every field we set
and how the match flows.
This mirrors how the shipping mode_duel (1v1 Duel) is built.
The plan
| Decision | Choice | Field |
|---|---|---|
| Team structure | 2 teams of 3 | teamCount = 2, perTeam = 3 |
| Win condition | best of 7 round wins | maxRound = 7 |
| Round shape | live → ended | rules = { structure = 'rounds', … } |
| Kit | armour + weapons, same for everyone | armor, loadout |
| Maps | from the shared battleground pool | a battleground pack (separate resource) |
Note what is not on the list: team names, colors, or models. Teams are anonymous — the engine renders every match egocentrically (you vs the enemy), and each player keeps the skin they equipped in the lobby.
1. Resource skeleton
[modes]/mode_skirmish/
├── fxmanifest.lua
└── server/
└── main.lua-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
author 'You'
description 'mode: Skirmish -- 3v3 elimination, best of 7'
version '0.1.0'
dependencies { 'sdk', 'engine' }
server_script 'server/main.lua'2. Assemble and register the descriptor
-- server/main.lua
local MODE = {
id = 'skirmish', -- unique
apiVersion = 1, -- pin the contract version
label = 'Skirmish',
teamCount = 2, -- 2 anonymous teams...
perTeam = 3, -- ...of 3 players (3v3)
minToStart = 1, -- 1 per team starts the round
maxRound = 7, -- best of: first to 7 round wins
-- Behaviour: a declarative `rules` block, auto-run on the engine's rules runtime.
rules = {
structure = 'rounds', -- elimination rounds
phases = { live = 120, post = 5 }, -- seconds
roundEnd = { 'elimination', 'timeLimit' }, -- team wipe OR the live timer
win = { roundWins = 7 }, -- best of 7
},
-- The kit is SYMMETRIC: the same armour and weapons for every combatant.
armor = 100,
loadout = { { 'WEAPON_CARBINERIFLE', 150 }, { 'WEAPON_PISTOL', 100 } },
}
CreateThread(function()
while GetResourceState('sdk') ~= 'started' do Wait(100) end
exports.sdk:registerMode(MODE)
end)A loadout is a list of { 'WEAPON_HASH', ammo } pairs. The first entry is
auto-equipped as the player's drawn weapon on spawn.
3. Give it somewhere to fight: an arena block
A mode carries no spawn data — maps live in the shared battleground pool. Any
battleground that declares a modes.skirmish block is automatically offered to your
mode. Add a block to an existing battleground pack (or ship your own — see the
battleground SDK):
-- in a battleground pack (resources/[packs]/battleground_pack_*), not the mode:
defineBattleground{
id = 'yard', apiVersion = 1, label = 'The Yard',
modes = {
skirmish = {
-- ARRAYS indexed by team number: entry 1 = team 1, entry 2 = team 2.
areas = {
{ x = 184.0, y = -921.0, z = 30.7, r = 10.0, face = 'center' },
{ x = 190.0, y = -967.0, z = 30.7, r = 10.0, face = 'center' },
},
},
},
}An area is a drop-in circle: players parachute into it. Swap it for a spawns
array — one list of { x, y, z, w } points per team, one point per seat — and they
start standing on their spot instead, no parachute. The battleground owns that
choice; see the battleground SDK.
Coordinates'
zdoesn't have to be exact: a ground spawn re-probes the ground under the point, and a drop lands the player on whatever ground is there.
That's the entire mode. Add ensure mode_skirmish to server.cfg (after
ensure sdk and ensure engine, with at least one compatible arena pack
ensured) and start the server.
4. What happens at runtime
You wrote no logic, so the rules runtime runs the match for you. With
perTeam = 3, minToStart = 1, win.roundWins = 7:
- A player joins a
skirmisharena from the menu (or opens one). The engine places them onto the least-populated team, moves them to that arena's routing bucket, and spawns them in, waiting. - Once both teams have at least
minToStart(1) players, the runtime starts a round: state →live— everyone (re)spawns by whichever entrance the battleground declares (vulnerable from frame one, no invincibility). - The round ends when one team is wiped out, or when the
livesafety timer (120s) expires (the team with more survivors wins; equal = draw). The winning team's round-win count goes up; state →endedfor 5s. - When a team reaches
win.roundWins(7) round wins, the engine announces the match result egocentrically (each player sees VICTORY or DEFEAT) and resets the score. Rounds keep cycling otherwise.
The default HUD, board, killfeed, and dead-player spectate all work automatically because your mode declared its team structure.
5. Tune it
Everything is data — change the table and restart the mode resource:
| You want… | Change |
|---|---|
| 1v1 instead of 3v3 | perTeam = 1 |
| First to fewer round wins | rules.win.roundWins = 5 |
| A longer live phase | rules.phases.live = 180 |
| More arenas | add modes.skirmish blocks to more arenas |
| Different weapons | edit loadout |
| A Create form (host picks map / loadout / best-of) | add loadouts / roundsRange (player-hosted arenas are on by default) |
Where to go next
- Need respawns and a score limit instead of elimination? See
Choosing how to drive a mode and switch the
rulestostructure = 'continuous'. - Need bespoke rules (objectives, custom scoring)? Add a
tickhook and drive the scene yourself with the write-exports.