Create / Create your first scenario
Create your first scenario
This tutorial builds a small shooting-range scenario in RON. You will start with the working Example Mod, follow one objective from start to victory, make it your own, and run it in the game. No code changes are required.
This page teaches one happy path. Use the modding reference when you need the complete list of events, filters, actions, objects, and expression nodes.
There is a second way in. Sandbox, on the main menu, opens the editor. SCENE and
EVENTS are two MODES of it: SCENE lays out the range beside the Inspector, and
EVENTS gives the whole window to the same script this page writes by hand - a
row per handler, its filters and actions under it, and a condition as the
operator with its two sides hanging beneath. Save writes it as an ordinary mod. The editor owns one
save slot (the editor_save mod) and never writes a hand-authored mod like the
one below, so the two paths do not fight over a file. Read on for what the rows
mean either way.
1. Start with the working example
The repository includes a playable starter at assets/mods/example/. It
already contains the long but routine scene setup: lights, a player ship, and
two asteroid targets. This tutorial focuses on the mission script around that
setup.
From the repository root, first confirm that the starter is valid:
nix develop --command cargo run content lint --target example
The two files you will use are:
assets/mods/example/
|- example.bundle.ron # lists the mod's content and resources
`- example.content.ron # sections and scenarios
Open example.content.ron and find id: "example_arena". That Scenario
item is the shooting range used below.
2. Understand the scenario file
A content file is a list. Each scenario is one Scenario((...)) item:
[
Scenario((
id: "example_arena",
name: "Example Arena",
description: "Destroy two drifting rocks under a mod-shipped skybox.",
cubemap: "self://textures/nebula.png",
events: [
// The mission handlers go here.
],
)),
]
For this tutorial, each field has one job:
idis the stable name used by the game and other scenarios.nameanddescriptionappear in the Scenarios menu.cubemapsets the background image.eventscontains the mission logic.
The complete field table, including thumbnails, hidden scenarios, and menu backdrops, is in Scenario files.
3. Plan one short story
Keep a first scenario small. Write three beats before you write RON:
- Setup: Range Control welcomes the player.
- Goal: Destroy two derelict rocks.
- Payoff: Complete the objective and show victory.
One line of story text per beat is enough. Objectives tell the player what to do. Story messages provide voice and context. Add more dialogue only after the playable flow works.
The finished scenario follows this path:
flowchart TD
Start["OnStart: spawn scene, set destroyed = 0, add objective"] --> Play
Play["Player destroys a target"] --> Destroyed["OnDestroyed: target filter passes"]
Destroyed --> Count["VariableSet: destroyed += 1"]
Count --> Check{"OnUpdate: destroyed > 1 and arena_done == 0?"}
Check -->|no| Play
Check -->|yes| Finish["Mark done, complete objective, show Victory"]
4. Events, filters, and actions
A handler has three parts:
- The event says when to check, such as
OnStartorOnDestroyed. - Every filter must pass.
- The actions then run in order.
A handler can also say once: true, which retires it the first time its
filters pass - see once.
This handler reacts only when the first target is destroyed:
(
name: OnDestroyed,
once: true,
filters: [
Entity((
id: Some("example_target_1"),
)),
],
actions: [
VariableSet((
key: "destroyed",
expression: Add(
Factor(Name("destroyed")),
Term(Factor(Literal(Number(1.0)))),
),
)),
],
),
Read it as: "When an object is destroyed, continue only if it is
example_target_1, then add one to destroyed."
The example has a matching handler for example_target_2. Separate filters
make sure unrelated asteroids do not count.
See Events, Filters, and Actions for every available construct.
5. Start the objective
OnStart runs once when the scenario loads. The example uses it to spawn the
scene and initialize the mission. Near the end of its action list, it sets the
counter and adds a HUD objective:
VariableSet((
key: "destroyed",
expression: Term(Factor(Literal(Number(0.0)))),
)),
VariableSet((
key: "arena_done",
expression: Term(Factor(Literal(Number(0.0)))),
)),
Objective((
id: "clear_range",
message: "Destroy the two derelict rocks. Aim with the mouse, [Left Mouse] fires.",
)),
This introduces two useful actions:
VariableSetstores mission state.Objectiveadds a goal to the HUD.
Variables must be initialized before a filter reads them. The full expression grammar is in Variables and expressions.
6. Finish the objective
The victory handler checks the counter every frame. It runs only after both rocks are gone and only once:
(
name: OnUpdate,
once: true,
filters: [
Expression((GreaterThan(
Term(Factor(Name("destroyed"))),
Term(Factor(Literal(Number(1.0)))),
))),
],
actions: [
VariableSet((
key: "arena_done",
expression: Term(Factor(Literal(Number(1.0)))),
)),
ObjectiveComplete((
id: "clear_range",
)),
Outcome((
outcome: Victory,
message: Some("Range cleared. Nice shooting."),
)),
],
),
destroyed > 1 means two or more targets are gone. once: true is what
stops this repeating OnUpdate event from showing victory again: the handler
retires the moment the count gate passes. arena_done stays a variable
because the TIMED beats below read it - they must not nag a player who has
already finished.
The remaining actions complete the HUD objective and show the victory screen. This is enough for a complete first scenario.
7. Make it yours
Edit the existing example_arena before adding new mechanics:
- Change the scenario
nameanddescription. - Change the
StoryMessagespeaker and text inOnStart. - Change the
Objectivemessage. - Change the two asteroid names, positions, or radii. A rock's radius is its durability and it is cubic, so raise it in small steps - see sizing a rock you want shot.
- Change the
Outcomemessage.
For example:
StoryMessage((
speaker: "Harbor Master",
text: "Two wrecks block the lane. Clear them before the convoy arrives.",
)),
Keep the setup, goal, and payoff connected. If you add a third required
target, add its OnDestroyed handler and change the win check from
destroyed > 1 to destroyed > 2.
Do not try to learn every action here. Useful next steps include markers, beacons, dialogue, enemy ships, and scenario transitions. Browse them in the Actions reference after this version runs.
8. Load and play it
The Example Mod is already listed in assets/mods.catalog.ron. Use this exact
iteration loop from the repository root.
- Check the mod:
nix develop --command cargo run content lint --target example
- Start the game:
nix develop --command cargo run --features dev
- In the main menu, open Mods.
- Select Example Mod and enable it.
- Return to the main menu and open Scenarios.
- Select your renamed scenario and press Play.
- Destroy both targets. Confirm that the objective completes and the Victory screen appears.
- Stop the game, edit the RON, and repeat the two commands.
When the scenario works, give it its own folder and id with
Mod files, then follow Publish a mod to
make it installable by other players. Keep hidden unset so players can find
it in the Scenarios menu.
9. Common mistakes
- RON is strict. A misspelled field stops the file from loading.
- Keep double parentheses around action and object variants, such as
Objective((...)). - Write optional values as
Some(value), not as a bare value. - Initialize a variable before a filter reads it.
- Spawn an object before an action targets its id.
- Give every visible scene lights. Copy the light setup from the Example Mod.
- Use unique ids for scenarios, objectives, objects, and variables.
For syntax and field details, use the modding reference.