Scene-Dash v2

Scene-Dash v2: the combat sample

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
  • Tweens and smoothing GameTween for a value over a duration, smoothTo for 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, GameStateBuilder read world state straight into the widget tree
  • Scene components components authored in a .fscene read straight off the scene graph, or baked into entities
  • Debug tooling entity debug, debug draw, 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

docs/reference.md

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, debug draw, 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

Libraries

scene_dash_v2
Flutter Scene support for Scene Dash.