Agent-based pedestrian crowd dynamics in Godot 4, with a tunable aggressive / neutral / passive personality mix, RVO avoidance via
NavigationServer3D, a side-by-side tournament view that runs 15 simulations in parallel, and a headless Monte Carlo batch runner with a published research note included below.
A two-way crosswalk simulation. Two crowds (north-bound, south-bound) cross a shared crosswalk after the walk signal turns green. Agents are stored in packed arrays and rendered through MultiMeshInstance3D, so a thousand pedestrians is cheap. Local avoidance is delegated to Godot 4's built-in RVO via NavigationServer3D.agent_set_avoidance_callback, then post-processed in the callback to constrain forward and lateral motion to plausible human gait.
- Features
- Quickstart
- Two scenes
- The personality model
- Research note: personality mix vs crossing time (N=10)
- Headless batch runner
- Project structure
- Performance notes
- Roadmap
- Acknowledgments
- License
- Godot 4.6, GDScript only, no add-ons or external dependencies.
- Per-agent personality: aggressive / neutral / passive profiles drive RVO time horizon, padding, lateral cap, preferred speed, and a new
rvo_complianceblend factor that controls how much an agent yields to the avoidance callback. - Live runtime sliders that set the personality mix (auto-rebalanced to sum to 100%, applied on slider release).
- Per-instance MultiMesh tinting so each capsule's shade reflects its personality (darker = aggressive, lighter = passive).
- Tournament view —
scenes/tournament.tscnlays out a 5×3 grid of 15 tiles sweeping the personality spectrum end-to-end with a single ortho camera and a live finishers leaderboard. - Headless Monte Carlo batch runner —
--batchmode runs N seeds × M configs sequentially, writes a CSV (aggressive,neutral,passive,seed,agents,cross_time,arrived,timed_out,wall_msec). - Deterministic largest-remainder personality allocation + seeded shuffle so the visible distribution exactly matches the requested percentages and is reproducible from
simulation_seed. - Cross-time tracker in the debug overlay — pause-aware live timer, frozen final value when the last agent reaches the opposite sidewalk.
Requires Godot 4.6+ (the Mono build is fine — the project uses GDScript only).
git clone https://github.com/phalladar/crowd-simulator
cd crowd-simulator
# Open in editor:
godot --path .
# Or run a scene directly:
godot --path . scenes/main.tscn
godot --path . scenes/tournament.tscn
# Headless Monte Carlo batch (writes res://batch_results.csv):
godot --headless --path . -- --batch --seeds 10 --timeout 75A 100-agent crosswalk with the personality slider HUD and debug overlay. Free orbit camera, cross-time tracker, all the tuning knobs exposed on the CrowdSimulation node in the inspector.
| Key | Action |
|---|---|
R |
Reset the run |
Space |
Pause / resume |
[ / ] |
Decrease / increase agent count |
| Left-mouse drag | Rotate camera |
| Middle-mouse drag | Pan camera |
| Mouse wheel | Zoom (orthographic) |
15 simulations in a 5×3 grid, all running in lockstep with seed=42, sweeping the personality spectrum from 100/0/0 (top-left) through 0/100/0 (centre) to 0/0/100 (bottom-right). Each tile shows its config and live cross time floating above the south sidewalk; the top-left HUD lists finishers as they complete.
| Key | Action |
|---|---|
R |
Reset all 15 tiles |
Tweak tile_agent_count, tile_spacing_x/z, camera_orthographic_size, camera_pitch_degrees, camera_distance on the CrowdTournament node in the inspector.
Each agent gets a discrete personality at spawn drawn from the requested mix:
| Profile | rvo_compliance |
time_horizon_mul |
padding_mul |
max_lateral_mul |
speed_mul |
Visual tint |
|---|---|---|---|---|---|---|
| Aggressive | 0.20 | 0.35 | 0.4 | 0.30 | 1.15 | 30% darker |
| Neutral | 1.00 | 1.00 | 1.0 | 1.00 | 1.00 | base colour |
| Passive | 1.00 | 1.60 | 1.4 | 1.60 | 0.90 | 30% lighter |
The key behavioural lever is rvo_compliance. In the avoidance callback we compute final = lerp(preferred_velocity, rvo_safe_velocity, rvo_compliance). Aggressive agents at compliance 0.20 use 80% preferred velocity and 20% RVO correction — they barely deviate. NavigationServer3D still sees them as moving obstacles, so neutral and passive agents route around them.
With sliders at 0/100/0 (the default), the simulation is bit-identical to the pre-personality build at the same seed: the personality bag's shuffle is a no-op when uniform, so it doesn't perturb the RNG sequence consumed by spawn jitter.
This section is a short research note based on a controlled headless batch experiment on this build. The full dataset is committed as batch_n10.csv.
We ran 70 simulated crosswalk crossings — 7 personality-mix configurations × 10 random seeds at 100 agents each — to characterize how the aggressive / neutral / passive blend affects time-to-clear-the-crosswalk. All 70 runs finished naturally within the 75-second timeout (zero timeouts, 100% arrival). The 100/0/0 (all aggressive) configuration was significantly faster than every other tested mix (mean 30.6 s vs 40–46 s; KS test p < 0.001 in every comparison). Within the non-aggressive regime, cross time grew roughly monotonically with passive fraction, but adjacent mixes were not always statistically separable at this sample size. We discuss two important caveats: a model artifact at the aggressive endpoint, and statistical power.
Build. Godot 4.6.1 mono running headless on Windows 11. Simulation logic is pure GDScript; avoidance via NavigationServer3D.agent_set_avoidance_callback. Reproduction command:
godot --headless --path . -- --batch --seeds 10 --timeout 75 --out res://batch_n10.csvConfigurations. Seven points sampling the personality simplex:
| ID | Aggressive % | Neutral % | Passive % |
|---|---|---|---|
| C1 | 100 | 0 | 0 |
| C2 | 50 | 50 | 0 |
| C3 | 50 | 0 | 50 |
| C4 | 33 | 34 | 33 |
| C5 | 0 | 100 | 0 |
| C6 | 0 | 50 | 50 |
| C7 | 0 | 0 | 100 |
Per-run setup. agent_count = 100 evenly split between the two crowds. Crosswalk 16 m wide × 18 m long, 8 m sidewalks. Spawn area 7 m wide on each side. Per-agent start delay sampled uniformly from [0, 1.2 s] so crowds entered the crosswalk over the first ~1.2 s rather than en masse. Seeds 1..10 per config. Personality assignment used a deterministic largest-remainder allocation followed by a seeded shuffle, so the visible mix exactly matches the requested percentages.
Outcome metric. cross_time — wall time from the walk signal turning green to the last agent crossing the opposite curb line, accumulated from physics deltas during the WALKING state (pause-aware). Hard timeout at 75 s.
Statistical test. Two-sample Kolmogorov-Smirnov on per-config cross-time distributions (scipy.stats.ks_2samp).
Means sorted fastest to slowest:
| Rank | Config | n | Mean (s) | Median (s) | Std (s) | Min (s) | Max (s) | Timeouts |
|---|---|---|---|---|---|---|---|---|
| 1 | 100/0/0 | 10 | 30.57 | 30.54 | 1.74 | 27.52 | 33.00 | 0 |
| 2 | 50/50/0 | 10 | 40.42 | 39.43 | 3.24 | 36.57 | 46.10 | 0 |
| 3 | 50/0/50 | 10 | 40.91 | 40.69 | 2.10 | 37.60 | 44.90 | 0 |
| 4 | 33/34/33 | 10 | 41.39 | 41.45 | 2.28 | 37.17 | 45.33 | 0 |
| 5 | 0/100/0 | 10 | 43.61 | 43.14 | 2.30 | 40.80 | 47.98 | 0 |
| 6 | 0/50/50 | 10 | 44.77 | 44.76 | 2.26 | 40.68 | 49.25 | 0 |
| 7 | 0/0/100 | 10 | 46.16 | 46.33 | 1.72 | 43.10 | 48.68 | 0 |
Pairwise KS tests. * = p < 0.05; n.s. = not significant; (.05) = borderline (0.05 ≤ p < 0.10):
| 50/50/0 | 50/0/50 | 33/34/33 | 0/100/0 | 0/50/50 | 0/0/100 | |
|---|---|---|---|---|---|---|
| 100/0/0 | * | * | * | * | * | * |
| 50/50/0 | n.s. | n.s. | * | * | * | |
| 50/0/50 | n.s. | (.05) | * | * | ||
| 33/34/33 | n.s. | * | * | |||
| 0/100/0 | n.s. | (.05) | ||||
| 0/50/50 | n.s. |
Findings:
- All-aggressive is unambiguously fastest. Every comparison against
100/0/0returns p ≈ 0 and KS statistic D = 1.0 (completely non-overlapping distributions). The all-aggressive mix completes the crossing 9.85 seconds faster than the next-best configuration. - Cross time grows roughly monotonically with passive fraction in the non-aggressive half: 40.4 → 40.9 → 41.4 → 43.6 → 44.8 → 46.2 s along the
50/50/0 → 0/0/100path. - Mid-range mixes form an indistinguishable cluster at N=10.
50/50/0,50/0/50, and33/34/33all sit in the 40–41 s band with overlapping distributions (pairwise KS p > 0.4). - Pure neutral vs pure passive is borderline.
0/100/0vs0/0/100returns p = 0.052 — directionally clear (3-second gap in means) but not significant at α = 0.05 with this sample size.
The aggressive lead is partially an artifact of how aggressive agents interact with NavigationServer3D's RVO in this build. With rvo_compliance = 0.20, the avoidance callback returns lerp(preferred, safe, 0.20), i.e. 80% of the velocity comes from the agent's preferred (straight-toward-destination) vector and only 20% from RVO's collision-avoiding correction. Aggressive agents barely deviate. Two opposing aggressive agents end up walking through each other (modulo a small overlap-resolution pass), which is not what real crowds do. The 100/0/0 advantage therefore overstates the gain a similar mix would provide under a more realistic compliance model. The ranking among mixed configs is more representative than the 100/0/0 outlier.
N=10 cleanly separates widely-spaced configs (any vs all-aggressive) but is insufficient for finer distinctions. To resolve adjacent mixes at α = 0.05 you would want N ≥ 30; to map the simplex finely enough to estimate an optimization landscape you would want N ≥ 100 per cell. The current sequential-run wall budget for N = 100 over a 7-config sweep is roughly an order of magnitude beyond practical without rewriting the avoidance step (see Roadmap).
Only one regime of agent count, crosswalk geometry, and personality-profile multipliers was tested. Higher densities triggering the pedestrian fundamental diagram, or different walk-signal duration, may rerank the configs.
- Simulation time is bound to the engine physics tick (60 Hz). The headless batch runs at 1× wall clock — 70 runs took 48 minutes.
NavigationServer3Dcannot be stepped manually faster than the engine main loop.- The avoidance callback receives one safe velocity per agent per tick; multi-step lookahead is not modelled.
- Agents are point-masses with capped lateral speed; no rotational state, group-formation behaviour, or fatigue.
right_pass_probability = 0.8bakes in a right-handed convention.
godot --headless --path . -- --batch --seeds 10 --timeout 75 --out res://batch_n10.csvThe dataset analyzed above is batch_n10.csv in the repo root. Analysis was done with pandas/scipy; the snippet is short enough to inline in any notebook.
Activated by passing --batch after the GDScript-arg separator --:
godot --headless --path . -- --batch [options]| Option | Default | Effect |
|---|---|---|
--seeds N |
100 | Number of random seeds per config |
--timeout S |
30 | Per-run wall-time cutoff in simulation seconds |
--out PATH |
res://batch_results.csv |
Output CSV path |
--agents N |
100 | Override agent_count for all runs |
The default sweep uses 7 representative configurations × --seeds seeds. The CSV columns are:
aggressive, neutral, passive, seed, agents, cross_time, arrived, timed_out, wall_msec
Smoke-test first with --seeds 2 (14 runs, ~10 minutes) before kicking off a full overnight run.
.
├── README.md
├── batch_n10.csv # the research-note dataset
├── docs/
│ └── crowd_simulator_design.md
├── scenes/
│ ├── main.tscn # interactive single-sim with sliders
│ └── tournament.tscn # 5x3 grid sweeping the personality spectrum
├── scripts/
│ ├── crowd_simulation.gd # the sim itself + tile + batch runner
│ └── tournament.gd # grid orchestrator
├── icon.svg
└── project.godot
The whole simulation is a single CrowdSimulation Node3D (scripts/crowd_simulation.gd). It owns the world geometry, the per-agent packed arrays, the RVO setup, the rendering MultiMesh, and either the slider HUD or a tile-label depending on whether embedded_in_grid is set.
The interactive single sim runs at 60 fps with 100 agents on mid-range hardware. The 5×3 tournament with 15 tiles × 70 agents = 1050 agents may dip below 60 fps; if so, lower tile_agent_count to 50 in the tournament inspector. The simulation is correct regardless of render fps because Godot keeps physics ticks fixed at 60 Hz.
- Inline avoidance step — replace
NavigationServer3Dcalls with a small ORCA / velocity-obstacle implementation in pure GDScript (or GDExtension). Decouples sim time from engine clock. Unlocks 100–1000× wall-time speedup for Monte Carlo sweeps. The currentbatch_n10.csvis the ground-truth baseline against which the inline implementation will be calibrated (KS test over the same 7 configs). - Higher-N simplex sweep (N ≥ 100, finer grid) to map the personality-mix optimization landscape and estimate the Pareto front of crossing time vs realism.
- Calibration to real pedestrian flow data. Adjust personality multipliers (especially
rvo_compliancefor aggressive) until the model reproduces published flow-density curves before drawing crowd-management conclusions. - Multi-process parallelism for the batch runner — one Godot process per CPU core, disjoint seed ranges, cuts wall time linearly.
The model draws on standard literature for multi-agent pedestrian dynamics:
- van den Berg, J., Lin, M., Manocha, D. (2008). Reciprocal Velocity Obstacles for Real-Time Multi-Agent Navigation.
- van den Berg, J., Guy, S., Lin, M., Manocha, D. (2011). Reciprocal n-body Collision Avoidance. (ORCA.)
- Helbing, D., Molnár, P. (1995). Social force model for pedestrian dynamics.
- Moussaïd, M., Helbing, D., Theraulaz, G. (2011). How simple rules determine pedestrian behaviour and crowd disasters.
Godot 4 crowd simulation · Godot 4.6 pedestrian dynamics · NavigationServer3D RVO avoidance · ORCA reciprocal velocity obstacle · agent-based pedestrian dynamics · multi-agent simulation · GDScript · MultiMesh crowd rendering · Monte Carlo simulation · personality mix · social force model · pedestrian fundamental diagram · headless batch runner · Kolmogorov-Smirnov test.
MIT © 2026 Josh Auriemma.