Examples & benchmarks
Playable scenes in examples/ plus the headless benchmark harness in tests/bench/. Demos double as integration references: each shows the exact wiring (world → systems → sync → scene nodes) for one slice of the engine.
How to run
Demos are regular Godot scenes — open any .tscn in the editor and press Play, or run headless (logic only, dummy renderer):
godot --headless --path . res://examples/SpriteSwarm2D.tscn
godot --headless --path . res://examples/VerifyDemos.tscn # exits 0 on PASSBenchmarks go through tests/bench/BenchRunner.tscn (main.tscn queues it by default). Always measure in Release; the numbers quoted below are Debug (headless dummy, pessimistic) unless noted:
godot --headless --path . res://tests/bench/BenchRunner.tscn -- --bench=all --n=50000 --frames=600
# filters: --bench=integrate|node3d|multimesh3d|node2d|multimesh2d|physics
# (plus jobs_*, render3d_*, render2d_sync, physics_ecs, serialize_*)
godot --headless --path . res://tests/unit/UnitRunner.tscn # exit 0 = PASSEach bench warms up (--warmup=120 default), then records per frame avg/p50/p95/min/max ms, fps, and alloc_kb (GC delta per frame — must tend to 0 in hot paths) to CSV in user://bench/*.csv plus a GD.Print summary. Reference machine: 6-core Linux, 23 GB RAM. Full methodology, soil conditions, and per-phase tables: docs/BENCHMARKS.md and docs/BENCHMARKS_RESULTS.md in the plugin repo.
Demos
SpriteSwarm2D — ~2000 bulk sprites
Three behaviors in one 1920×1080 viewport: 1200 swirl (Swirl2D vortices with SpriteFrame animation on a 2×2 atlas), 600 drift (Velocity bouncing off viewport edges), 200 YSort rows (5 layers, stably sorted before sync). Two AtlasGroupManager groups with procedural GradientTexture2Ds, bulk sync per group with Transform2D from position + uniform scale, plus a BatchCanvas layer of 24 pulsing heterogeneous rects. Input: Space +200 drift, X −200 drift. Shows: atlas groups, anim, YSort, both 2D sync paths, runtime spawn/kill.
BulletHell3D — ~1200 bulk entities with culling + LOD
600 orbiting boxes (Orbit + RenderColor, LOD chain across 3 box groups), 400 bouncing spheres (Velocity in a ±40 arena), 200 cylinders (Bob + Spin), orbiting camera, floor + sun, one HeroFallback hero on a sine path. Presentation each frame: RenderFrustum.FromCamera → CullingSystem → LodSystem → TransformSync3DSystem. LOD chain thresholds 30/60 m. Input: Space +100 spheres, X −100, C toggle culling. Shows: mesh groups, frustum culling, distance LOD, hero fallback, group attach after first sync.
PhysicsSwarm2D — 300 bouncing balls, pure ECS physics
Top-down arena with 4 walls baked as StaticRegistry boxes. Balls integrate on chunk spans, then BroadphaseSystem → NarrowphaseSystem; the arcade response handles static contacts only (Other.IsNull): velocity reflection on the normal + push-out by penetration. Ball-ball contacts emit events but get no positional response (documented arcade choice). Rendered via AtlasGroupManager + TransformSync2D on a radial-gradient texture. Input: Space +100 balls, X −100. Shows: the full ECS physics tick and the canonical response snippet (copied in the physics guide).
PhysicsArena3D — ECS cannon vs Godot brick wall
Hybrid pattern for real rigid-body behavior the arcade solver can't do (torque, inertia, toppling): ECS bullets (LocalTransform + PhysicsVelocity + PhysicsRadius, bulk-rendered, no nodes/bodies per bullet) vs a 6×4 wall of RigidBody3D bricks (mass 2, friction/bounce, 3 HP each, green→yellow→red). Per tick, bullets advance on chunk spans, then sphere-vs-oriented-box tests in each brick's local space (precomputed AffineInverse) with ApplyImpulse(vel·Kick + up·bias, contact − brickPos) — offset application gives torque for free. Input: A/D or arrows traverse, W/S elevate, Space 10-round burst, R rebuild wall. Shows: ECS↔Godot coupling, the reason the engine stays arcade (outsource torque), lazy MultiMesh attach for groups that start empty.
HybridDemo — static Godot map + 600 ECS entities + save/load
Floor + 4 walls (MeshInstance3D + StaticBody3D, logical bounds ±38) with 400 gravity cubes (light arcade gravity + floor/wall bounce), 150 spinners (Spin), 50 orbiters (Orbit) in 3 MeshGroupManager groups, presentation via FromCamera → culling → sync (no interpolation components — the sync's LocalTransform fallback path). A HeroFallback hero walks the plane (WASD, Q/E yaw, Space jump with gravity); T toggles follow/orbit camera; F5 saves / F9 loads a snapshot with before/after FNV-1a hash on the HUD. Shows: hybrid static/dynamic worlds, hero control, snapshot round-trip in a live scene.
VerifyDemos — headless verification scene
Not a visual demo: runs every slice headless and prints [VERIFY] name=ok/FAIL per check, exiting 0 on PASS (VerifyDemos.tscn). Covers: 3D register + 1000-entity sync (count, bounds), culling near/far box, SelectLod pure + live LOD group move, hero register/sync/hide/unregister, 2D atlas sync of 130 instances, anim step (Fps=10, dt=0.25 → index 2 on a 2×2 grid), YSort stability on {2,0,2,1,0,1} → {1,4,3,5,0,2}, BatchCanvas layer creation, a 5000-entity parallel job move over 120 fixed ticks, Prev/Render ensure + snapshot round-trip (hash, count, tick), and the physics chain (wall hit, far entity clean, damage kill tag, selective destroy).
Shared demo components (DemoComponents.cs, namespace GodotECS.Examples): Spin { Axis, DegPerSec }, Bob { Amp, Freq, Phase, BaseY, Time }, Pulse { Speed, MinS, MaxS, Phase, Time }, Orbit { Center, Radius, Speed, Angle, Height }, Swirl2D { Center, Radius, Speed, Angle } — all unmanaged IComponentData.
Benchmark harness
Files in tests/bench/: EcsBenchBase (warmup, Stopwatch, CSV), Bench_IntegrateJobs (scalar vs SIMD vs parallel), Bench_NodePerEntity3D (negative control), Bench_MultiMeshBulk3D (correct pattern), Bench_SpriteBulk2D, Bench_PhysicsGrid, Bench_Jobs, Bench_ChunkVsArray, render (render3d_sync/upload/fallback, render2d_sync), physics_ecs, serialize_save/load, plus BenchRunner.cs (sequential headless runner with a results table).
v1 gates
From docs/BENCHMARKS.md (Release, 6-core reference machine). Status quotes the latest Debug headless runs in BENCHMARKS_RESULTS.md — treat them as lower bounds (Release is faster; dummy-driver numbers measure logic only).
| Gate | Threshold | Latest signal |
|---|---|---|
Integrate 100k Pos += Vel·dt, parallel | < 2.0 ms, speedup ≥ 4× vs scalar | 0.82 ms Debug, pooled 5.1× — OK (dedicated TPL 2.9×) |
| Render 50k 3D cubes, bulk | > 60 fps, draw calls = groups (< 20), sync < 1.0 ms | 50k render3d_sync 13.96 ms Debug logic-only; honest caveat: on the dummy driver (no draw) bulk loses to per-instance (7.2 vs 4.6 ms) because managed fill dominates and dummy calls are cheap — the ratio inverts on a real GPU (1 upload vs 100k driver calls) |
| Render 50k 2D sprites, bulk | > 60 fps, 1 MultiMesh per texture | 50k render2d_sync 8.62 ms Debug; true 2D bulk buffer still TODO (layout hypothesis unvalidated on GPU) |
| Node-per-entity 10k (negative control) | ≥ 10× slower than bulk, else the bulk is wrong | Excluded from the all queue headless at 10k — too slow, which is the point; never nodes for masses |
| Physics 10k projectiles vs statics | broad + narrow < 1.5 ms, Jolt bridge batch < 0.5 ms | Not met Debug single-thread: 118 ms at 10k vs 5k dense (was 418 ms at 1k pre-grid); dense-stress + Debug + single-thread — Release (~3–4×), chunk parallelism (~5×), and SIMD still open; real maps (walls/floors, 0–2 candidates) are far cheaper than the dense stress |
| Serialize 100k | save < 200 ms, load < 200 ms, stable replay hash over 600 ticks | save 14.5 ms / load 52 ms Debug — OK; replay + round-trip + corrupt-stream rejection PASS |
Rule from the bench docs: if a gate fails, stop and optimize the bridge before the core. Always publish commit, SDK (4.8.0-dev.4), net10, OS/CPU/ GPU/driver, N, frames, and CSVs with any numbers you quote.