ecsly (Entity Component System) for Dart apps

⚠️ This package is published as super-experimental prerelease (0.0.1-dev.8). APIs are actively changing and may break across releases.

ecsly logo

ecsly is a high-performance Dart ECS runtime focused on deterministic simulation and low-GC hot loops. It owns entities, archetypes, resources, events, commands, and schedules.

Use it when you want a small pure-Dart ECS core under games, simulations, tools, tests, Flutter hosts, Jaspr sites, or plugin packages. Start with normal Dart object components, then move hot numeric state to typed extension components when GC pressure or iteration speed matters.

Use

Add to pubspec.yaml:

dependencies:
  ecsly: ^0.0.1-dev.8

Import path:

import 'package:ecsly/ecsly.dart';

Supported platforms: Android, iOS, Linux, macOS, web, and Windows.

Because the core package has no Flutter dependency, it can also be used from Jaspr and other Dart-first hosts. Keep simulation in ecsly systems, then let the host layer decide how to render, hydrate, or expose the resulting state.

Tiny story

Define a component as ordinary Dart data:

class CounterComponent extends Component {
  CounterComponent(this.value);
  int value;
}

Create a world and register the component storage:

final world = World();
world.components.registerObjectComponent<CounterComponent>();

Spawn an entity with a component bundle:

final entity = world.reserveEmptyEntity().entity;
world.spawnBundle(entity, ComponentBundle.fromLists([CounterComponent(1)]));

// Structural changes are queued; flush makes them visible to queries.
world.flush();

Read or mutate it:

for (final (_, counter) in world.queryMut<CounterComponent>()) {
  counter.value += 1;
}

Put logic into a schedule when you want named update stages:

world.createSchedule('Update').add((world) {
  for (final (_, counter) in world.queryMut<CounterComponent>()) {
    counter.value += 1;
  }
});

world.runSchedule('Update');
world.flush();

For hot numeric data, use extension components backed by typed columns. See example/scheduled_run.dart for a compact EnergyComponent example.

A few more patterns

Extension components split identity from storage. The marker type lets ecsly register/query a component; the facade gives typed access to packed column data:

world.components.registerExtension<EnergyComponent, Energy>(
  columnFactory: MyEnergyColumnFactory(),
  facadeFactory: MyEnergyFacadeFactory(),
);

for (final (_, energy) in world.queryExt<EnergyComponent, Energy>()) {
  energy.current += energy.regenPerTick;
}

The runnable example/extension_component.dart uses the concrete factories defined in example/components.dart.

Commands let you request structural changes while iteration is still running:

for (final (entity, counter) in world.query<CounterComponent>()) {
  if (counter.value >= 2) {
    entity.remove<CounterComponent>(); // queued until flush
  }
}

world.flush();

Resources are global singleton state for systems:

class FrameClockResource extends Resource {
  FrameClockResource(this.deltaSeconds);
  final double deltaSeconds;
  int frame = 0;
}

world.upsertResource(FrameClockResource(1 / 60));
world.getResource<FrameClockResource>().frame += 1;

Resources can be immutable snapshots, mutable frame state, or a mix of both. Choose mutability by purpose: fixed inputs stay final, counters and caches can mutate, and behavior still belongs in systems.

SIMD-style hot paths use typed columns with stride 4, then operate on the column's Float32x4List view. See example/simd_columns.dart.

SIMD columns

Use an extension component when the game-facing API should look like fields, but the storage should stay packed for hot loops. A stride-4 FloatColumn can hold (x, y, z, w) rows and expose a Float32x4List view:

import 'dart:typed_data';

import 'package:ecsly/ecsly.dart';

class Velocity4Component extends Component {
  const Velocity4Component();
}

extension type Velocity4._(int index) {
  static late FloatColumn column;

  double get x => column.getValueUnsafe(index, 0);
  double get y => column.getValueUnsafe(index, 1);

  set x(double value) => column.setValue(index, 0, value);
  set y(double value) => column.setValue(index, 1, value);
}

final class Velocity4ColumnFactory extends ColumnFactory {
  @override
  DataColumn createColumn(
    ComponentId componentId, {
    int initialCapacity = 8,
  }) {
    return FloatColumn(stride: 4, initialCapacity: initialCapacity);
  }
}

final class Velocity4FacadeFactory
    extends ComponentFacadeFactory<Velocity4> {
  @override
  Velocity4 create(int index) => Velocity4._(index);

  @override
  void initialize(covariant FloatColumn column) {
    Velocity4.column = column;
  }
}

Register the extension component, spawn rows, then use the SIMD view when you want to update the packed column in batches:

final world = World();
world.components.registerExtension<Velocity4Component, Velocity4>(
  columnFactory: Velocity4ColumnFactory(),
  facadeFactory: Velocity4FacadeFactory(),
);

final entity = world.reserveEmptyEntity().entity;
world.spawnBundle(
  entity,
  ComponentBundle.fromExtensionList(const [
    (Velocity4Component, Velocity4),
  ]),
);
world.flush();

for (final (_, velocity) in world.queryExt<Velocity4Component, Velocity4>()) {
  velocity.x = 3;
  velocity.y = 4;
}

final simd = Velocity4.column.simdView;
if (simd != null) {
  final gravity = Float32x4(0, -9.8, 0, 0);
  for (var i = 0; i < Velocity4.column.length; i++) {
    simd[i] = simd[i] + gravity;
  }
}

The facade keeps ordinary systems readable. The column keeps numeric state dense, reusable, and cheap to scan when a game or simulation starts pushing more entities through the same operation.

Why it is useful

  • Deterministic structural changes through command queues and explicit flushes.
  • Fast iteration through archetypes, query masks, and column storage.
  • Flexible object components for simple/cold data.
  • Low-GC extension components for compact hot data.
  • Schedules, resources, events, and plugins without depending on Flutter.
  • Usable from Flutter, Jaspr, CLI tools, tests, and other Dart runtimes.

Examples

  • example/basic_world.dart: smallest object-component flow.
  • example/scheduled_run.dart: schedule plus custom FloatColumn extension component.
  • example/extension_component.dart: marker component plus typed facade.
  • example/commands_and_resources.dart: deferred commands and global resources.
  • example/simd_columns.dart: stride-4 FloatColumn and SIMD view.
  • example/components.dart: tiny components with dartdoc explaining the storage choices.

Run an example from this package directory:

dart run example/main.dart

Core examples avoid owning gameplay-space components such as PositionComponent and VelocityComponent. Use sibling plugin packages, such as game2d_plugin, for shared game concepts.

Benchmarks

Latest generated report: benchmark/results/latest.md

Recorded environment: MacBook Air (M2, 2022), 8 GB RAM, macOS, Dart 3.12.1, 8 processors. Treat these as machine-specific evidence, not universal guarantees.

Metric Recorded result Signal
Mutable typed-column query 66.0M ops/sec low-GC in-place mutation
Raw chunk query 53.8M ops/sec strongest hot query path
Render-packet-like extract 72.9M ops/sec prototype-style packet extraction
20k game-frame p95 893 us frame-shaped ECS update + extract
Command flush 4.1M ops/sec structural/object-component bottleneck signal
Spawn/despawn churn 2.5M ops/sec frame-style entity lifecycle pressure

Generate a local report:

dart run benchmark/run.dart --limits \
  --markdown-out=benchmark/results/latest.md \
  --json-out=benchmark/results/latest.json

The benchmark suite reports hot-path strengths and bottleneck signals: query iteration, raw chunk iteration, game-frame p95/p99, render-packet-like extract, command flush, migration, spawn/despawn churn, cache hit/miss, events, memory delta, and optional entity-count scaling. Results depend on machine, OS, Dart SDK, and runtime mode, so compare reports from the same environment.

What this package is and is not

  • ✅ Core runtime package: entities, archetypes, resources, events, plugins, and system schedules.
  • ✅ Runtime-first with minimal external dependencies and early-prerelease status.
  • ❌ Not a code-generator package: use ecs_codegen only where annotation-based code generation is needed.
  • ❌ Not a full application stack: graphics/input/collision/camera plugins live in sibling package folders.

Documentation

Acknowledgements

ecsly stands on prior ECS and simulation work. We appreciate ideas and lessons from Bevy, EnTT, and many other open-source contributors.

Libraries

ecsly
Entity Component System (ECS) library for Dart and Flutter.