scene_dash_v2 0.2.0
scene_dash_v2: ^0.2.0 copied to clipboard
ECS gameplay architecture for flutter_scene 3D games: entities, systems, schedules, events, state machines, input, physics, reactive widgets, headless tests.
Scene-Dash v2 #

Scene-Dash is an ECS-driven gameplay architecture for flutter_scene. It gives Flutter games a structured runtime for gameplay state, system orchestration, custom and frame-driven schedules, resources/DI, events and observers, state machines, entity lifecycles, and headless testing. Its Flutter builders expose world, entity, and resource state directly to the widget tree, so gameplay logic and UI stay connected.
- 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 behavior and scoped entities and state machines
- Routines a reusable sequencer for gameplay with an ordered flow: wave directors, objectives, encounters, tutorials
- Input buttons, axes, and buffered presses as resources; widgets write, systems read
- Widgets
WorldBuilder,EntityBuilder,GameStateBuilderread world state straight into the widget tree - Debug tooling entity debug, gizmos, and a live inspector overlay
- 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 #
A widget selects one value out of the world and rebuilds only when that value changes:
EntityBuilder<Health, double>(
entity: player,
select: (h) => h.current, // compared once per frame
builder: (context, hp) => HealthBar(hp), // runs only when it changed
absent: const RespawnCountdown(), // entity dead / component gone
)
Same frame tick, same select-and-compare:
WorldBuilder<int>(select: (w) => w.query<Health>(require: const [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 resolves the entity through the world, .pulse drives transient
feedback, every: throttles a heavy select, GameScope reaches the game
from any context.
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 master # flutter_scene needs Flutter GPU
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, gizmos, widget layer. Re-exports core, so one import covers both. |
packages/scene_dash_inspector |
Optional debug overlay: live entities, resources, system timings, event channels. Read-only, polled at 4 Hz. |
examples/scene_game |
Complete game: Rapier physics, one feature per folder. |
examples/headless_example |
The core without Flutter. |
examples/scene_benchmark |
On-device render benchmark: static vs mount-only vs ECS vs instanced. |
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