Recoil:RmlUI Starter Guide
More actions
Making A Basic Recoil Game
This guide walks you through creating a minimal but functional game with the Recoil engine. By the end you will have a working game archive that loads in the engine, with a playable Commander unit, a simple weapon, and a basic Lua gadget. No prior Spring or Recoil experience is assumed, though familiarity with Lua is helpful.
1. What is Recoil?
Recoil is an open-source, cross-platform RTS game engine descended from Spring (which itself descended from Total Annihilation). It provides out of the box:
- Physics simulation — projectile trajectories, terrain collision, unit movement
- Resource system — metal and energy by default, fully customisable
- Networking — authoritative server, deterministic lockstep simulation, replays
- Lua scripting — every mechanic is overridable via Lua gadgets and widgets
- VFS — a virtual file system that loads content from game archives, maps, and basecontent
Your game is a Lua + asset package that rides on top of the engine. You control the rules, the units, the UI, and everything else; the engine provides the platform.
Key terminology
| Term | Meaning |
|---|---|
| Game | Your content archive loaded by the engine (units, Lua scripts, textures, etc.) |
| Map | The terrain archive (.smf height data + textures). Distributed separately from the game. |
| Gadget | A Lua script that runs game logic (synced or unsynced). Lives in luarules/gadgets/.
|
| Widget | A Lua script that runs UI logic. Lives in luaui/widgets/.
|
| Wupget | Informal collective term for widgets and gadgets. |
| Callin | A function in your Lua code that the engine calls (e.g. gadget:UnitCreated).
|
| Callout | A function the engine exposes to Lua (e.g. Spring.GetUnitPosition).
|
| VFS | Virtual File System — unified view of game archive, map archive, basecontent. |
| Basecontent | Engine-bundled archive with default handlers, water textures, and other bare necessities. |
| .sdd | Development game folder (name must end in .sdd). Equivalent to an archive but uncompressed.
|
| .sdz | Production game archive (renamed .zip). Faster to distribute and load. |
2. Prerequisites
Before starting you need:
- The Recoil engine binary. Download a pre-built release from GitHub Releases or build from source. The main executable is
spring(orspring.exeon Windows). - A map archive (
.smf/.sd7) placed in themaps/subfolder of your Recoil data directory. Any map from an existing Recoil game works for testing. Map archives and game archives live in separate directories. - A text editor. Any editor works. For the best experience, set up the Lua Language Server with Recoil type stubs so you get autocomplete on all
Spring.*functions.
Your Recoil data directory typically looks like:
recoil-data/ ├── spring ← engine binary (linux) or spring.exe (windows) ├── spring_headless ← headless binary (no rendering) ├── spring_dedicated ← dedicated server binary ├── games/ ← your game archives / folders go here ├── maps/ ← map archives go here └── (engine config files)
3. Game Archive Structure
During development use an .sdd folder — it's just a normal directory the engine treats as an archive.
Create it inside your games/ directory:
games/
└── mygame.sdd/
├── modinfo.lua ← required: identifies your game to the engine
├── units/ ← unit definition files
│ └── commander.lua
├── weapons/ ← weapon definition files
│ └── laser.lua
├── features/ ← feature (prop/wreck) definitions
├── gamedata/ ← def pre/post processing, options
│ ├── unitdefs_post.lua
│ └── modrules.lua
├── luarules/
│ └── gadgets/ ← synced and unsynced game logic
│ └── game_end.lua
├── luaui/
│ └── widgets/ ← UI scripts
├── objects3d/ ← unit models (.dae / .obj / .s3o)
├── unittextures/ ← unit icon textures (unitname.png)
└── maps/ ← embedded map resources (optional)
For production, compress the contents into a .sdz (zip) archive or .sd7 (7-zip) archive.
The folder contents become the archive root — modinfo.lua should be at the top level.
VFS and dependencies
Your game archive can declare dependencies on other archives (including other games) via modinfo.lua.
The engine merges all archives into the VFS.
Files in your game take priority over dependencies.
This means you can build on an existing game without copying its assets.
See Recoil:VFS Basics for full details on VFS modes and the load order.
4. modinfo.lua
modinfo.lua is the single most important file — without it the engine will not recognise your archive as a game.
<syntaxhighlight lang="lua"> -- games/mygame.sdd/modinfo.lua
local modinfo = {
-- Displayed in lobbies and the engine title bar
name = "My Recoil Game",
-- Short unique identifier used internally; no spaces, keep it stable
shortName = "MRG",
-- Shown to players in lobby browsers and descriptions
description = "A minimal Recoil game for learning purposes.",
-- Semantic version string
version = "0.1.0",
-- Mutator name; leave empty for base games (not mods on top of other games)
mutator = "",
-- Archive names this game depends on; engine always loads basecontent itself
depend = {},
-- Replaces list: names of games this game supersedes in rapid/lobby
replace = {},
-- URL shown in lobby if a player does not have the game
modtype = 1, -- 1 = game/mod, 0 = hidden
-- Primary unit sides available to players
-- These correspond to the "Side" key in start scripts and unit defs
sides = { "MyFaction" },
-- For each side, which unit the engine spawns as the starting unit
startscreens = {
MyFaction = "commander", -- references a unit def name
},
}
return modinfo </syntaxhighlight>
ModOptions.lua
To expose configurable options in lobbies (e.g. starting resources, game mode), add a gamedata/ModOptions.lua:
<syntaxhighlight lang="lua"> -- gamedata/ModOptions.lua -- Returned options appear as sliders/checkboxes/dropdowns in lobby UIs.
return {
{
key = "startmetal",
name = "Starting Metal",
desc = "Amount of metal each player starts with.",
type = "number",
def = 1000,
min = 0,
max = 10000,
step = 100,
},
{
key = "commanderdie",
name = "Commander Death Ends Game",
desc = "Lose when your Commander is destroyed.",
type = "bool",
def = true,
},
} </syntaxhighlight>
Options are readable in gadgets via Spring.GetModOptions().
5. Your First Unit Definition
Unit defs are Lua files in units/ that return a table of unit type definitions.
The convention is one unit per file, where the filename matches the unit def name.
<syntaxhighlight lang="lua"> -- units/commander.lua -- The starting unit for the MyFaction side.
return {
commander = {
-- ── Display ──────────────────────────────────────────────────
name = "Commander",
description = "The starting unit. Its death ends the game.",
-- Path to the icon shown in the unit HUD (unittextures/commander.png)
buildPic = "commander.png",
-- ── Model ────────────────────────────────────────────────────
-- Path inside the archive (objects3d/commander.dae)
objectName = "commander.dae",
-- ── Physics ──────────────────────────────────────────────────
-- Maximum health points
health = 3000,
-- Armour class name; controls damage reduction from weapon types
armorType = "commander",
-- Size of the footprint in map squares (8 elmos each)
footprintX = 2,
footprintZ = 2,
-- The height of the collision sphere
collisionVolumeScales = "30 50 30",
collisionVolumeType = "ellipsoid",
-- ── Movement ─────────────────────────────────────────────────
-- Movement class name; references a MoveClass def (see MoveClasses below)
moveDef = {
family = "tank",
-- Units above this slope can't be traversed
maxSlope = 36,
-- Turns per second at max speed
turnRate = 1024,
},
-- Maximum speed in elmos/second
maxVelocity = 42,
-- Acceleration in elmos/second²
maxAcc = 0.5,
-- Braking in elmos/second²
maxDec = 1.5,
-- ── Construction ─────────────────────────────────────────────
-- How many build power units this unit provides
workerTime = 200,
-- How much metal it costs to build
buildCostMetal = 0, -- Commanders are free (spawned by engine)
buildCostEnergy = 0,
-- Build time in seconds at 1 build power
buildTime = 1,
-- ── Energy and Metal Production ───────────────────────────────
metalMake = 1, -- passively trickles 1 metal/second
energyMake = 20, -- passively generates 20 energy/second
-- ── Weapons ──────────────────────────────────────────────────
-- Up to 3 weapons can be listed; name references a weapon def
weapons = {
{ def = "COMMANDER_LASER" },
},
-- ── Self-Destruct ─────────────────────────────────────────────
selfDExplosion = "commander_sdc", -- optional; a weapon def name
-- ── Misc ─────────────────────────────────────────────────────
-- Used in start scripts to assign this unit as Commander
commander = true,
-- Sight radius in elmos
sightDistance = 660,
-- Radar range in elmos (0 = no radar)
radarDistance = 0,
},
} </syntaxhighlight>
Notable unit def keys
| Key | Type | Notes |
|---|---|---|
name |
string | Display name in UI. Not the def name (the Lua table key). |
health |
number | Max HP. |
moveDef |
table | Inline move class definition. See engine docs for all fields. |
maxVelocity |
number | Elmos per second. 1 elmo ≈ 8 map height units. |
buildCostMetal |
number | Metal cost. Constructor must have enough resources to start building. |
buildTime |
number | Seconds to build at 1 build power. Actual time = buildTime / workerTime. |
workerTime |
number | Build power this unit contributes when constructing. |
weapons |
table | Up to 3 entries; each entry is a table with a def key naming a WeaponDef.
|
commander |
bool | Marks this unit as a Commander. Lobby UI and engine use this. |
customParams |
table | Arbitrary key/value pairs preserved by the engine for gadget use. |
For the full list of unit def keys and their effects, see the Recoil Lua API documentation.
Def pre- and post-processing
The engine reads gamedata/defs.lua (provided by basecontent) which orchestrates def loading.
It first executes gamedata/unitdefs_pre.lua (if present) to populate a Shared table visible to all unit def files, then loads every units/*.lua file, and finally runs gamedata/unitdefs_post.lua.
<syntaxhighlight lang="lua"> -- gamedata/unitdefs_post.lua -- Normalize key casing and apply game-wide tweaks.
for _, unitDef in pairs(UnitDefs) do
-- lowerkeys() is provided by basecontent; makes all keys lowercase lowerkeys(unitDef)
end
-- Example: give all units 10% extra health for _, unitDef in pairs(UnitDefs) do
if unitDef.health then
unitDef.health = unitDef.health * 1.1
end
end </syntaxhighlight>
6. Weapon Definitions
Weapon defs follow the same pattern as unit defs — Lua files in weapons/ returning tables.
<syntaxhighlight lang="lua"> -- weapons/laser.lua
return {
COMMANDER_LASER = {
name = "Commander Laser",
description = "Short-range direct-fire laser.",
-- Weapon category controls which targets it can engage
-- Options: ground, air, sea, land, building, etc. (combinable)
onlyTargetCategory = "SURFACE",
-- ── Visuals ───────────────────────────────────────────────────
-- Weapon class drives the physics model
-- Options: Cannon, LaserCannon, MissileLauncher, AircraftBomb,
-- BeamLaser, LightningCannon, Flame, TorpedoLauncher, ...
weaponType = "LaserCannon",
-- Colour of the beam/tracer (R G B A each 0-255)
rgbColor = "1 0.2 0.2",
-- Colour at the other end of the beam (optional)
rgbColor2 = "1 0.9 0.9",
-- Visual length of the laser beam in elmos
beamtime = 1,
-- Thickness of the beam in pixels
thickness = 3,
corethickness = 0.3,
laserflaresize = 8,
-- ── Ballistics ────────────────────────────────────────────────
-- Projectile/beam speed in elmos/second
projectilespeed = 800,
-- Range in elmos
range = 350,
-- Reload time in seconds
reloadtime = 1.5,
-- Burst: number of shots per fire command
burst = 1,
-- Spread in degrees (0 = perfectly accurate)
accuracy = 0,
-- ── Damage ────────────────────────────────────────────────────
damage = {
-- 'default' applies to all armour types not explicitly listed
default = 50,
},
-- Area of effect in elmos (0 = single target)
areaOfEffect = 8,
-- Impulse applied to targets on hit (0 = none)
impulse = 0,
-- Does this weapon set things on fire?
firestarter = 0,
},
} </syntaxhighlight>
Like unit defs, you can post-process weapon defs via gamedata/weapondefs_post.lua.
7. Feature Definitions
Features are non-commandable props: trees, rocks, wrecks, and so on. They occupy the map by default via map configuration, or can be spawned by Lua. A minimal feature def:
<syntaxhighlight lang="lua"> -- features/trees.lua
return {
tree = {
description = "A tree.",
-- Model to render
object = "tree.dae",
-- Health before the feature is destroyed
damage = 50,
-- Metal reclaimed when a constructor takes it apart
metal = 0,
-- Energy reclaimed when destroyed or reclaimed
energy = 0,
-- Blocks ground unit pathfinding
blocking = true,
-- Visible on radar/sonar
geoThermal = false,
-- What this feature becomes when destroyed (nil = nothing)
deathFeature = "",
-- Whether this can be reclaimed by a constructor
reclaimable = true,
},
} </syntaxhighlight>
8. Game Rules (modrules.lua)
gamedata/modrules.lua is read by basecontent's defs.lua to configure fundamental engine behaviour that is not driven by unit/weapon defs.
<syntaxhighlight lang="lua"> -- gamedata/modrules.lua
return {
system = {
-- Starting resources (also overridable via ModOptions / start script)
startMetal = 1000,
startEnergy = 1000,
-- Maximum units per team (0 = unlimited)
maxUnits = 1000,
-- Commander handling: 0 = game continues, 1 = game ends, 2 = lineage
gameMode = 1,
-- Limit on repair speed (% of max health per second; 0 = unlimited)
repairSpeed = 0,
-- Whether resurrect restores health
resurrectHealth = false,
-- Resource multiplier for reclaiming metal from living units
reclaimUnitMethod = 1,
-- Whether ground features leave wrecks
featureDeathResurrect = true,
},
sound = {
-- Optional: override specific engine sound events
},
} </syntaxhighlight>
9. Running Your Game
The engine is launched with a start script — a plain text file (conventionally .sdf) that describes who is playing, which game, and which map.
Minimal start script
<syntaxhighlight lang="ini"> [GAME] {
GameType = My Recoil Game; -- must match modinfo.lua name or shortName MapName = my_map.smf; -- the .smf file inside the map archive
IsHost = 1; MyPlayerName = Dev;
[PLAYER0]
{
Name = Dev;
Password = ;
Spectator = 0;
Team = 0;
}
[TEAM0]
{
TeamLeader = 0;
AllyTeam = 0;
RgbColor = 0.1 0.4 1.0;
Side = MyFaction; -- must match a side declared in modinfo.lua
StartPosX = 4000;
StartPosZ = 4000;
}
[ALLYTEAM0]
{
NumAllies = 0;
}
} </syntaxhighlight>
Save this as game.sdf in your Recoil data directory, then launch:
./spring game.sdf
On Windows:
spring.exe game.sdf
The engine will:
- Read the start script
- Mount your game archive from
games/mygame.sdd - Mount the map archive from
maps/my_map.sd7 - Load basecontent (default widget handlers, etc.)
- Start the simulation and call into your Lua scripts
Quick test with headless
For automated testing (or when you don't need graphics), use the headless binary:
./spring_headless game.sdf
Widgets still run, so you can run AI vs AI matches or data-gathering scripts without a window.
10. Lua Scripting Basics
Lua is the scripting language for everything above the C++ engine layer. The engine provides several isolated Lua environments:
| Environment | Location | Purpose |
|---|---|---|
| LuaRules (synced) | luarules/main.lua |
Authoritative game logic. Desync if misused. |
| LuaRules (unsynced) | luarules/draw.lua |
Unsynced effects and UI driven by game logic. |
| LuaUI | luaui/main.lua |
Player-facing UI: HUD, minimap overlays, etc. |
| LuaGaia | luagaia/main.lua |
Logic for the neutral Gaia team (wrecks, features, world events). |
| LuaIntro | luaintro/main.lua |
Loading screen, briefings. |
| LuaMenu | luamenu/main.lua |
Main menu (pre-game). Requires a menu archive. |
Basecontent provides default main.lua entry points and wupget handler boilerplate so you can start writing gadgets and widgets without building the handler yourself.
Your first gadget
A gadget is a Lua file the gadget handler loads from luarules/gadgets/.
<syntaxhighlight lang="lua"> -- luarules/gadgets/game_end.lua -- Ends the game when a player's Commander is destroyed.
local gadget = gadget ---@type Gadget
function gadget:GetInfo()
return {
name = "Game End Conditions",
desc = "Ends the game on Commander death.",
author = "YourName",
date = "2025",
license = "GNU GPL, v2 or later",
layer = 0,
enabled = true,
}
end
-- Only run the logic in synced mode if not gadgetHandler:IsSyncedCode() then return end
-- Track alive Commanders per team local commanderByTeam = {}
function gadget:UnitFinished(unitID, unitDefID, teamID)
local def = UnitDefs[unitDefID]
-- UnitDefs inside wupgets is indexed by numeric ID, not name
if def and def.isCommander then
commanderByTeam[teamID] = unitID
end
end
function gadget:UnitDestroyed(unitID, unitDefID, teamID)
if commanderByTeam[teamID] == unitID then
commanderByTeam[teamID] = nil
-- Check whether any other living Commander still belongs to this allyteam
local allyTeamID = Spring.GetTeamAllyTeamID(teamID)
local hasCommander = false
for _, ally in ipairs(Spring.GetTeamList(allyTeamID)) do
if commanderByTeam[ally] then
hasCommander = true
break
end
end
if not hasCommander then
-- GameOver sends the end-of-game event to all clients
Spring.GameOver({ allyTeamID })
end
end
end </syntaxhighlight>
Your first widget
A widget is a Lua file the widget handler loads from luaui/widgets/.
<syntaxhighlight lang="lua"> -- luaui/widgets/resources_display.lua -- Draws current metal and energy income in the corner of the screen.
local widget = widget ---@type Widget
function widget:GetInfo()
return {
name = "Resources Display",
desc = "Shows current resource income.",
author = "YourName",
date = "2025",
license = "GNU GPL, v2 or later",
layer = 0,
enabled = true,
}
end
function widget:DrawScreen()
local metal, energy = Spring.GetTeamResources(Spring.GetMyTeamID()) if not metal then return end
gl.Color(1, 1, 1, 1)
gl.Text(string.format("Metal: %.0f Energy: %.0f", metal, energy),
10, 40, 16, "s")
end </syntaxhighlight>
Callins and callouts
Callins are functions in your code the engine calls at the right moment: UnitCreated, UnitDestroyed, GameOver, DrawScreen, Update, etc.
Check the full list at runtime with Script.GetCallInList().
Callouts are Spring.* functions you call out to the engine: Spring.GetTeamResources, Spring.GiveOrderToUnit, Spring.Echo, etc.
Full documentation is at beyond-all-reason.github.io/spring/ldoc/.
Communicating between environments
The Lua environments are isolated from each other by design. Common bridges:
| Method | Direction | Notes |
|---|---|---|
WG / GG table |
Within one environment | Widgets share WG; gadgets share GG.
|
SendToUnsynced / RecvFromSynced |
Synced → Unsynced | Pass data from game logic to rendering/UI. |
Spring.SendLuaRulesMsg / gadget:RecvLuaMsg |
LuaUI → LuaRules | UI triggers a game action beyond unit orders. |
Spring.SendLuaUIMsg / widget:RecvLuaMsg |
LuaUI ↔ LuaUI (all players) | Broadcast UI state to other players. |
Rules params (Spring.SetRulesParam) |
LuaRules → all | Expose synced data as read-only key/values. |
See Recoil:Wupget Communication for detailed examples.
11. Useful Debug Commands
In-game chat commands prefixed with / control engine and game state.
With cheats enabled (/cheat) additional commands become available:
| Command | Effect |
|---|---|
/cheat |
Toggle cheats on/off. Required for most debug commands. |
/give [unitname] |
Spawn a unit at the cursor position. |
/destroy |
Destroy selected units immediately. |
/invincible |
Toggle invincibility for selected units. |
/godmode |
Toggle god mode (all units obey all players). |
/speed [x] |
Set game speed multiplier (e.g. /speed 5).
|
/nopause |
Disallow pausing. |
/luarules reload |
Reload LuaRules scripts without restarting. |
/luaui reload |
Reload LuaUI scripts without restarting. |
/editdefs |
Enter def-editing mode (requires cheats). See unit def docs. |
Spring.Echo("msg") |
Print a message to the in-game console from Lua. |
12. Maps and Map Integration
Maps are separate archives (.sd7 or .sdz) placed in maps/.
They are not shipped with your game.
The map defines the terrain height data (.smf), sky, water, and can optionally define starting metal spots and features.
Controlling what maps do to your game
Maps can include a mapinfo.lua which may set atmosphere, gravity, and other properties.
A map can also override unit def values via MapOptions.lua, which your game can read with Spring.GetMapOptions().
You can restrict which maps are compatible by checking map properties in a gadget at startup and calling Spring.GameOver if requirements are not met.
Metal spots
Metal spots (mexes) are typically defined in the map as SMF/SMD metal map data, or via a Lua file in the map archive.
Your gadgets can read them with Spring.GetMetalMapSize() and Spring.ExtractorInRangeMetalMap().
Extractor units that generate metal must have a extractsMetal unit def key > 0 and be placed over a metal spot.
13. Packaging and Distribution
When your game is ready for distribution, compress the .sdd folder contents into an .sdz (zip) or .sd7 (7-zip) file.
# Linux: create .sdz (zip) cd games/mygame.sdd zip -r ../mygame-0.1.0.sdz . # Linux: create .sd7 (7-zip, better compression) cd games/mygame.sdd 7z a ../mygame-0.1.0.sd7 .
Rapid
Rapid is the standard package distribution system for Recoil games. It allows lobby clients (e.g. Chobby) to download and update games automatically. See the Rapid documentation and the start script format for integration details.
14. Next Steps
With a working minimal game, the typical next steps are:
- More unit types
- Add builders, resource extractors, power generators, turrets, and mobile combat units. Follow the same unit def pattern — each is a Lua file in
units/.
- Resource extractors
- A unit with
extractsMetal > 0placed over a metal spot generates metal. Add an energy converter unit to turn metal into energy and vice versa.
- Pathfinding and MoveClasses
- Define
movedefsingamedata/movedefs.luato control how different unit types traverse terrain (hover, boat, amphibious, kbot, tank, aircraft).
- AI support
- The Skirmish AI interface lets players add bot opponents without any extra gadget work. Beyond that, you can write a [Lua AI] for fully custom bot behaviour inside your game's Lua scripting.
- Full UI
- The basecontent widget handler gives you a basic HUD for free. For a polished interface use RmlUI for HTML/CSS-style widgets, or raw OpenGL via
gl.*callouts.
- Maps
- Creating your own map requires tools like MapConv (height and texture compilation) and SpringMapEdit. See the Recoil map documentation for details.
- Replays and dedicated servers
- Replays are recorded automatically when
RecordDemo=1is set in the start script. For large-scale multiplayer use the dedicated binary (spring_dedicated) and an autohost such as SPADS or Springie.
15. Further Reading
On this wiki
- Recoil:VFS Basics — full documentation of the Virtual File System, modes, and load order
- Recoil:Unit Types Basics — deep dive into unit defs, post-processing, and the wupget-facing UnitDefs table
- Recoil:Widgets and Gadgets — the addon (wupget) system in full detail
- Recoil:Wupget Communication — how to pass data between Lua environments
- Recoil:RmlUI Starter Guide — reactive HTML/CSS-style UI for your game
- Recoil:Headless and Dedicated — server binaries and automated testing
- Recoil:Migrating From Spring — for existing Spring 105 games
External resources
- Recoil Lua API reference — complete callout/callin documentation
- RecoilEngine on GitHub — source, releases, and issue tracker
- recoilengine.org — official website with downloads and changelog
- Recoil Discord — community support and engine developer discussions
- Spring Simple Game Tutorial — older but still broadly applicable tutorial (Spring and Recoil are highly compatible)