RivalityFX Docsrivalityfx.com ↗

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

DecisionChoiceField
Team structure2 teams of 3teamCount = 2, perTeam = 3
Win conditionbest of 7 round winsmaxRound = 7
Round shapelive → endedrules = { structure = 'rounds', … }
Kitarmour + weapons, same for everyonearmor, loadout
Mapsfrom the shared battleground poola 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' z doesn'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:

  1. A player joins a skirmish arena 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.
  2. 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).
  3. The round ends when one team is wiped out, or when the live safety timer (120s) expires (the team with more survivors wins; equal = draw). The winning team's round-win count goes up; state → ended for 5s.
  4. 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 3v3perTeam = 1
First to fewer round winsrules.win.roundWins = 5
A longer live phaserules.phases.live = 180
More arenasadd modes.skirmish blocks to more arenas
Different weaponsedit 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 rules to structure = 'continuous'.
  • Need bespoke rules (objectives, custom scoring)? Add a tick hook and drive the scene yourself with the write-exports.