Draw multi-vehicle trajectories on a map in CARLA, save the whole thing as one JSON model, and run it end to end with one button.
This repo accompanies the paper CARLA Scenario Editor (MODELS Companion '26). On disk the package is seditor; the browser tool calls itself Scenario Editor.
Most CARLA tooling snaps routes to the road network. This editor doesn't: a drawn path can cut across medians, swerve late, or drive the wrong way, which is exactly what you need to stress a safety framework like RSS. Three things set it apart:
- Direct trajectory editing. Drag multi-vehicle paths on the real map instead of typing coordinates.
- Freedom from the road network. A drawn path isn't constrained to lane geometry.
- One workflow. A single model drives everything, and one button launches the server, the scenario, and the drivable ego.
- Architecture
- Requirements
- Setup
- Run it
- Command reference
- The scenario model
- Triggers and actions
- Worked example: an offramp cut-in
- Configuration
- Repository layout
- Citation
Everything hangs off one declarative scenario model. That model is the only authoritative artifact. The py_trees behavior tree and the ScenarioRunner XML are both generated from it, which is what lets a multi-vehicle scenario run from a single press. See the paper for the full treatment.
| Layer | Package | Responsibility |
|---|---|---|
| Authoring | seditor/backend/static |
Browser editor: SVG map canvas, actor and step panels, run cockpit. |
| Schema | seditor/schema |
The scenario model, the parameter catalog, and validation. |
| Runtime | seditor/runtime |
Turns the model into a py_trees behavior tree. |
| Orchestration | seditor/orchestrator |
One-action startup and teardown, plus process supervision. |
| Execution | seditor/srunner, seditor/rss |
ScenarioRunner scenario, path follower, and the RSS ego client. |
flowchart LR
subgraph create["1 - Author (this tool)"]
editor["Browser editor"]
json[("scenario.json")]
editor -->|save| json
end
subgraph exec["2 - Execute"]
orch["Orchestrator"]
runner["ScenarioRunner client"]
ego["Ego client (RSS)"]
server["CARLA server"]
orch --> runner --> server
orch --> ego --> server
end
json -->|Run| orch
The one piece worth calling out: a custom PathFollower and PID controller let a vehicle steer toward any point, not just on-lane waypoints, so a maneuver is a drawn path instead of a stack of atomic behaviors.
Linux only. The tool launches CarlaUE4.sh and tears runs down by process group.
- CARLA 0.9.14, the RSS-enabled build (for the RSS ego client)
- ScenarioRunner 0.9.13
- A Python env with CARLA importable, plus
py_treesandpygame. Defaults expect a conda env namedcarla_0.9.14.
The backend and UI are pure standard library and a browser. No build step, no npm.
Point the tool at your installs with three environment variables (drop them in ~/.bashrc):
export CARLA_ROOT=/home/user/Documents/CARLA_0.9.14_RSS/
export SCENARIO_RUNNER_ROOT=/home/user/Documents/scenario_runner-0.9.13/
export SCENARIO_EDITOR_ROOT=/home/user/Documents/scenario_editor/Then sanity check the paths:
python -m seditor doctordoctor prints the resolved interpreter and roots and flags anything missing.
python -m seditor up # opens the editor at http://localhost:8123In the browser: pick a Town and click Load map, add vehicles and drag their spawn markers, draw paths, give each actor a sequence of steps, then Save (Ctrl+S). Hit Run.
Run needs no second terminal. It starts CARLA if the RPC port is closed (otherwise it connects to what's already there), launches the ScenarioRunner client, then opens the drivable ego window, with live logs streaming into the right panel. The editor and the CLI drive the same Orchestrator, so a headless seditor run does the identical thing.
python -m seditor run <scenario.json> # run a scenario end to end
python -m seditor stop # stop a running simulation
python -m seditor restart [<scenario>] # restart runner + ego, keep the server warm
python -m seditor status # server + last-run status
python -m seditor validate <scenario> # validate a file, report issues
python -m seditor doctor # check paths and configuration
python -m seditor up # launch the editor + backend| Flag | Commands | Effect |
|---|---|---|
--host, --port |
run, restart, status, doctor, up | CARLA RPC host and port (default 127.0.0.1:2000) |
--keep-server |
run, stop | leave CARLA running on exit |
--quality Low|Epic |
run, restart | quality level when the tool launches the server (default Epic) |
--nvidia-offload |
run, restart | force the discrete NVIDIA GPU via PRIME offload |
--ui-port, --no-browser |
up | editor port (default 8123) / don't auto-open a browser |
One JSON file (SCHEMA_VERSION = 3). Distances and coordinates are meters, speeds m/s, times seconds. The UI may show km/h, but disk and runtime are always m/s.
- Spawn (every actor, ego included): a
{ point: [x, y], yaw_deg }pose in world coordinates, the single source of truth for where an actor starts. Yaw is CARLA's convention (clockwise positive from +x). - Ego: driven by hand in the pygame window. Its
pathis an optional reference for hands off assist, with no steps and no triggers. - Vehicle: a non-ego actor with an ordered list of steps.
- Step:
{ wait_for?, action, until?, name? }. Thewait_fortrigger gates the start, theactionruns, theuntiltrigger interrupts it. - Path: 2D points in the
worldframe (absolute) or thelocalframe (relative to the actor's live transform when the action starts). A local path may carry ananchorpose, which is authoring metadata the runtime ignores. - Path library:
pathsholds named paths afollow_pathaction can reuse bypath_ref.
Each step compiles to a py_trees subtree:
wait_for + action + until -> Sequence(wait_for, Parallel_ONE(action, until))
wait_for + action -> Sequence(wait_for, action)
action + until -> Parallel_ONE(action, until)
action -> action
A vehicle's steps become a Sequence, and all vehicles run under one Parallel.
Both are stored as a type plus a parameter dict, which keeps the model off ScenarioRunner's class signatures. The catalog lives in one file (seditor/schema/specs.py) that drives validation, the UI forms, and the runtime adapters at once, so adding one is a catalog entry plus a factory.
Triggers (usable as wait_for or until, each concerning the step's own actor unless it names a reference):
| Type | Parameters | Fires when |
|---|---|---|
in_distance_to_vehicle |
reference, distance_m |
within distance_m of another actor |
in_distance_to_location |
location, distance_m |
within distance_m of a fixed point |
velocity_above / velocity_below |
speed_ms |
speed crosses the threshold |
time_to_arrival_vehicle |
reference, time_s |
time to collision with an actor drops below time_s |
time_to_arrival_location |
location, time_s |
time to reach a location drops below time_s |
stand_still |
duration_s |
(near) stationary for duration_s |
drive_distance |
distance_m |
driven distance_m since the trigger started |
timer |
duration_s |
duration_s seconds elapsed |
Actions:
| Type | Parameters | Behavior |
|---|---|---|
follow_path |
path or path_ref, target_speed_ms |
drive a drawn path (the star primitive: off-lane allowed) |
keep_velocity |
speed_ms, duration_s?, distance_m? |
hold a speed; ends on duration, distance, or until |
follow_lane |
speed_ms?, avoid_collision? |
follow real lanes until until fires |
lane_change |
direction, speed_ms, distance_other_lane_m? |
change one lane along road geometry |
stop |
brake |
brake to a stop (brake in (0, 1]) |
idle |
duration_s? |
do nothing, optionally for a fixed time |
The rule: free-form paths for the interesting moves, atomics for the boring ones. follow_lane and lane_change lean on CARLA's waypoint API, while a drawn follow_path obeys nothing but the points.
The paper's demo (scenarios/cut_in_town02.json): other1 waits until the ego closes to 15 m, then follows a drawn path that cuts in front of it toward an offramp. The ego runs the RSS layer and brakes early, because RSS clamps its acceleration in response. The whole actor is one step:
{
"action": {
"type": "follow_path",
"path": {
"frame": "world",
"points": [[131.86, 191.5], [145, 190], [155, 188], [170, 187.8]]
},
"target_speed_ms": 12
},
"name": "wait then cut in",
"wait_for": { "type": "in_distance_to_vehicle", "reference": "ego", "distance_m": 15 }
}python -m seditor validate scenarios/cut_in_town02.json
python -m seditor run scenarios/cut_in_town02.jsonWith the RSS ego selected, the brake indicator lights up early as other1 cuts in, showing RSS force a safe response the ego's own navigation wouldn't have.
Settings resolve defaults → environment variables → a JSON file ($SEDITOR_SETTINGS, or ~/.config/seditor/settings.json) → CLI flags, in that order. Paths always come from the environment, so a stale file can't clobber a working setup. The ones you'll actually touch:
| Setting | Default | Notes |
|---|---|---|
host, port |
127.0.0.1, 2000 |
CARLA RPC connection |
ego_client |
rss |
rss (safety sensor + visualization) or plain (lighter, no RSS) |
carla_quality |
Epic |
Low segfaults camera sensors, so the tool forces it back to Epic |
force_nvidia_offload |
false |
PRIME offload for hybrid-graphics laptops |
Towns: Town01–Town07 and Town10HD. The tiled maps (Town11–Town13) are separate CARLA downloads and aren't listed by default. Tests run without CARLA: pytest tests/.
seditor/
backend/ HTTP app, session, and the static browser editor
schema/ scenario model, catalog (specs), validation, JSON I/O
runtime/ behavior-tree builder, action + trigger factories
orchestrator/ CLI, core orchestrator, settings, supervisor, XML gen
srunner/ the custom ScenarioRunner scenario (main_scenario.py)
rss/ ego clients (RSS and plain) + RSS sensor
helpers/ probe.py (server health), extract_geometry.py
PathFollower.py custom path follower behavior
scenarios/ example scenarios
tests/ headless tests (no CARLA required)
@inproceedings{carla_scenario_editor_2026,
title = {CARLA Scenario Editor},
author = {Bozkurt, Emre and Chang, Kehang and Gowland, Ryan and
Ramdhan, Stefan and Dagenais, Kyanna and Pantelic, Vera and
Paige, Richard and Lawford, Mark},
booktitle = {Proceedings of the ACM/IEEE 29th International Conference on
Model Driven Engineering Languages and Systems (MODELS Companion '26)},
year = {2026},
address = {Malaga, Spain},
publisher = {ACM}
}Built at the McMaster Centre for Software Certification (McSCert) with Stellantis, on CARLA, ScenarioRunner, py_trees, pygame, and ad-rss-lib.
{ "version": 3, "name": "cut_in_town02", "map": "Town02", "timeout_s": 120.0, "criteria": { "collision_test": true }, "ego": { "model": "vehicle.lincoln.mkz_2020", "spawn": { "point": [28.32, 187.95], "yaw_deg": 0.0 }, "path": { "frame": "world", "points": [[28.32, 187.95], [159.76, 187.73]] }, "target_speed_ms": 10.0 }, "vehicles": [ { "id": "other1", "model": "vehicle.dodge.charger_2020", "spawn": { "point": [131.86, 191.5], "yaw_deg": 0.0 }, "steps": [ /* see below */ ] } ], "paths": {} }