Scene-Dash v2

Scene-Dash is an ECS-driven gameplay architecture for flutter_scene. It gives Flutter games a runtime for gameplay state, and a way to test that state with nothing rendering.
- Entities and components sparse sets, queries, deferred structural changes, lifecycle hooks
- Systems plain functions, ordered by sets and run conditions
- Schedules startup, update, fixed step, custom run conditions, plus custom schedules you run yourself
- Resources shared services and state in the world, injected into systems
- Events and observers decoupled communication between systems
- States and machines game states with enter/exit systems and state-scoped entities, plus per-entity state machines
- Routines a reusable sequencer for gameplay with an ordered flow: wave directors, objectives, encounters, tutorials
- Tweens and smoothing
GameTweenfor a value over a duration,smoothTofor a target that keeps moving, both on game time so they pause with it - Input buttons, axes, and buffered presses as resources. Widgets write, systems read
- Widgets
WorldBuilder,EntityBuilder,GameStateBuilderread world state straight into the widget tree - Scene components components authored in a
.fsceneread straight off the scene graph, or baked into entities - Debug tooling entity descriptions and system profiling
- Headless testing run systems, schedules, and whole features with no rendering
flutter_scene keeps doing the rendering. Scene-Dash is the gameplay layer on top.
World-reactive widgets
Use select to read the value your UI needs. Each frame, the widget reads
that value again and compares it with the value it last kept. If it changed,
the widget rebuilds. For example, this health bar updates when health.current
changes:
EntityBuilder<Health, double>(
entity: player,
select: (health) => health.current,
builder: (context, hp) => HealthBar(hp),
absent: const RespawnCountdown(), // Entity is gone or has no Health.
)
You can also display a value from the whole world, display game state, or react to an event:
WorldBuilder<int>(select: (w) => w.query<Health>().having<Enemy>().count(),
builder: (ctx, n) => Text('$n enemies')) // any world-derived value
GameStateBuilder<GameStatus>(builder: (ctx, s) => switch (s) { ... })
// a subtree per game state
WorldEventListener<EnemyKilled>(onEvent: (ctx, e) => shakeScore(ctx),
child: const ScorePanel()) // world events into UI
The rest of the widget layer:
.matching finds the entity by its components, .pulse drives short-lived
feedback, every: checks a heavy select less often, and GameScope
reaches the game from any context.
What counts as a change
select decides when to rebuild. Values compare with ==. Lists, sets, maps
and iterables compare by contents, one level deep, so you can return a live
list:
EntityBuilder<Inventory, List<String>>(
entity: player,
select: (inventory) => inventory.items, // no copy needed
builder: (context, items) => Text(items.join(', ')),
)
WorldBuilder<(int, int)>( // a record of values compares with ==
select: (w) => (w.resource<Score>().points, w.resource<Score>().combo),
builder: (context, score) => ScoreLine(points: score.$1, combo: score.$2),
)
Only one level is compared. A list inside a record, or mutable objects
inside a list, still compare by identity. Select smaller pieces (one
builder per slot), or return a class with its own ==. A parent rebuild
can still run the builder.
A complete game in one file
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_scene/scene.dart';
import 'package:scene_dash_v2/scene_dash_v2.dart';
import 'package:vector_math/vector_math.dart' show Vector3;
Future<void> main() async {
final game = await SceneGame.boot(features: [installCubes]);
runApp(
GameScope( // provides the game to the subtree
game: game,
child: MaterialApp(
home: Scaffold(
body: SceneView( // flutter_scene widget; not wrapped
game.scene,
cameraBuilder: _camera,
onTick: game.onTick, // forwards frame ticks to the game
),
),
),
),
);
}
Camera _camera(Duration elapsed) =>
PerspectiveCamera(position: Vector3(0, 3, -6), target: Vector3.zero());
void installCubes(GameBuilder game) { // a feature: a plain function
game
..addSystem(Schedules.startup, spawnCube, writes: {Orbit, SceneTransform})
..addSystem(Schedules.update, orbitCubes, writes: {Orbit, SceneTransform});
}
void spawnCube(World world) => world.spawn(cubeBundle());
void orbitCubes(World world) { // a system: a plain function
world.query2<Orbit, SceneTransform>().each((entity, orbit, transform) {
orbit.phase += orbit.speed * world.dt; // dt is schedule-aware
transform
..x = orbit.radius * cos(orbit.phase)
..z = orbit.radius * sin(orbit.phase);
});
}
final class Orbit { // a component: a plain class
final double radius;
final double speed;
double phase;
Orbit({required this.radius, required this.speed, this.phase = 0});
}
List<Object> cubeBundle() => [ // a bundle: a function → the spawn list
Orbit(radius: 2, speed: 1),
SceneTransform.zero(),
NodeRef(Node(mesh: Mesh(CuboidGeometry(Vector3.all(0.8)), UnlitMaterial()))),
];
Hot reload applies edits to system bodies. There is no build step.
Quick start
flutter channel stable # flutter_scene needs Flutter 3.47+
flutter config --enable-dart-data-assets
flutter pub get # resolve the workspace (repo root)
cd examples/combat_sample
flutter run --enable-flutter-gpu
Reference
- UI
- Boot
- World
- Frame
- Coordination
- flutter_scene
- Tooling
docs/concept.md for the architecture,
docs/integration.md for the flutter_scene bridge.
Packages and examples
| Path | Purpose |
|---|---|
packages/scene_dash_v2_core |
Pure-Dart ECS runtime, authoring surface, headless TestGame. |
packages/scene_dash_v2 |
flutter_scene integration: SceneGame.boot, mounting, transform sync, physics bridge, widget layer. Re-exports core, so one import covers both. |
examples/scene_game |
Complete game: Rapier physics, one feature per folder. |
examples/headless_example |
The core without Flutter. |
examples/basic_example_tower_defense |
The middle example: a small complete game, no assets. Three features that never call each other, talking through events and queries. Gameplay pinned headless. |
examples/combat_sample |
Combat slice: KayKit knight against waves of barbarians, lock-on, buyable skills, giants, Rapier ragdolls, authored .fmat materials. Gameplay pinned headless. |
benchmarks |
Query, structural, and record-overhead benchmarks. |
examples/scene_game/lib/features/ # feature based structure
├── player/ # else you keep beside it (hud/, fx/,
├── projectiles/ # common/, main.dart) is your call
├── rocks/
├── collectables/
├── rules/
├── world/
└── decor/
examples/scene_game/lib/features/player/ # every feature, same shape
├── player.dart # the Feature function: installs this feature's systems
├── data/ # components, bundles, config, resources
├── systems/ # one file per concern, not one file per feature
└── animation/ # optional; clip selection lives with its feature
Libraries
- scene_dash_v2
- Flutter Scene support for Scene Dash.