ecsly 0.0.1-dev.8
ecsly: ^0.0.1-dev.8 copied to clipboard
Experimental ECS core for Dart apps, including Flutter and Jaspr (early prerelease).
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.
Visual guide #
The package ships one canonical raster logo plus deterministic SVG reference cards. The cards keep API examples truthful instead of using generated screenshots:
assets/brand/ecsly-logo.png- canonical soft typography-based package logo.assets/brand/ecsly-logo.svg- SVG compatibility copy that embeds the same raster logo.assets/brand/ecsly-usage-card.svg- exact import path and tiny usage flow.assets/brand/ecsly-api-story-card.svg- World, components, commands, flush, query, and schedule mental model.assets/brand/ecsly-performance-card.svg- object components, extension facades, and SIMD-friendly columns.
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 customFloatColumnextension 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-4FloatColumnand 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_codegenonly where annotation-based code generation is needed. - ❌ Not a full application stack: graphics/input/collision/camera plugins live in sibling package folders.
Documentation #
- DX_FAQ.md — how to start, examples, terminology, and practical usage.
- DESIGN_FAQ.md — architecture, trade-offs, and why the API works this way.
- Autogenerated API docs
- CHANGELOG.md
Acknowledgements #
ecsly stands on prior ECS and simulation work. We appreciate ideas and lessons from
Bevy, EnTT, and many other open-source contributors.