TECS โ€” Tremble ECS

An archetype-based Entity Component System written in Dart.

๐Ÿ“ฆ pub.dev/packages/tecs ๎‚› github.com/Descrout/tecs
What is ECS? ECS (Entity Component System) is a data-oriented architectural pattern that separates data (Components) from behaviour (Systems) and identity (Entities). Entities are lightweight IDs, components are plain data bags, and systems contain the logic.
Lunapulse Showcase

A space shmup built with TECS + Tremble. Play on itch.io โ†’

Watch Trailer

Core Principles


Installation

Add to your pubspec.yaml:

dependencies:
  tecs: ^1.0.12

Or via git:

dependencies:
  tecs:
    git:
      url: https://github.com/Descrout/tecs.git

World

The World is the central container. It owns all entities, components, archetypes, systems, and resources.

Creating a World

import 'package:tecs/tecs.dart';

final world = World();

Lifecycle Methods

MethodDescription
clearAll()Removes all entities, systems and resources. Resets world state completely.
clearEntities()Clears all entities, archetypes and component type registrations. Increments the internal version, invalidating query caches.
clearSystems()Removes all registered systems.
clearResources()Removes all resources.
applyCommands()Forces execution of all buffered commands.
update(args, {tag})Runs all systems matching the given tag, then calls applyCommands().
versionReturns the current world version (used internally for cache invalidation).
entityCountNumber of alive entities.
archetypeCountNumber of unique archetypes.

Entities

An entity is a lightweight integer ID (EntityID is a typedef for int).

Creating Entities

// Create an empty entity (no components)
final entity = world.createEntity();

// Create an entity with components (immediate)
final e = world.instant.createEntityWith([
  PositionComponent(x: 10, y: 20),
  VelocityComponent(dx: 0.5, dy: 1.0),
]);

// Bulk create multiple entities (immediate)
final ids = world.instant.createEntities([
  [PositionComponent(x: 10, y: 20), ColorComponent(r: 255, g: 0, b: 0)],
  [PositionComponent(x: 30, y: 40)],
]);

// Deferred creation (queued, applied at end of frame)
world.commands.createEntityWith([PositionComponent(x: 5, y: 5)]);
world.commands.createEntities([...]);

Removing Entities

// Immediate removal
world.instant.removeEntity(entity);  // returns true if alive
world.instant.removeEntities([e1, e2, e3]);  // batch removal (more efficient)

// Deferred removal (queued, applied at end of frame)
world.commands.removeEntity(entity);

Checking Status

if (world.isAlive(entity)) {
  print("Entity is alive");
}

Components

Components are plain data classes that extend Component. Each component stores an entityID that is set automatically when attached.

Defining Components

class PositionComponent extends Component {
  PositionComponent({required this.x, required this.y});

  double x;
  double y;
}

class VelocityComponent extends Component {
  VelocityComponent({required this.dx, required this.dy});

  double dx;
  double dy;
}

Adding Components

// Immediate โ€” single
world.instant.addComponent(entity, PositionComponent(x: 150, y: 150));

// Immediate โ€” bulk
world.instant.addComponents(entity, components: [
  PositionComponent(x: 150, y: 150),
  VelocityComponent(dx: -800, dy: 400),
]);

// Deferred
world.commands.addComponent(entity, PositionComponent(x: 5, y: 5));
world.commands.addComponents(entity, [PositionComponent(x: 5, y: 5), ...]);

Getting Components

final pos = world.getComponent<PositionComponent>(entity);
if (pos != null) {
  print(pos.x);
}

Removing Components

// Immediate โ€” single
world.instant.removeComponent<PositionComponent>(entity);

// Immediate โ€” bulk
world.instant.removeComponents(entity, components: [PositionComponent, VelocityComponent]);

// Deferred
world.commands.removeComponent(entity, PositionComponent);
world.commands.removeComponents(entity, [PositionComponent, VelocityComponent]);
Generic components are fully supported. Use the concrete generic type when querying:
world.getComponent<TComponent<String>>(entity);
world.query([TComponent<int>]);

Queries

TECS provides several query strategies. The recommended hot-path approach is queryEach.

Simple Query (One-off)

final results = world.query([PositionComponent, VelocityComponent]);

for (final row in results) {
  final pos = row.get<PositionComponent>();
  final vel = row.get<VelocityComponent>();
  pos.x += vel.dx;
  pos.y += vel.dy;
}

Cached Query (QueryParams)

Create QueryParams once and reuse. Component IDs and type indices are cached internally.

final params = QueryParams([PositionComponent, VelocityComponent]);

// Inside update loop:
final results = world.queryWithParams(params);
for (final row in results) {
  final pos = row.get<PositionComponent>();
  final vel = row.get<VelocityComponent>();
  pos.x += vel.dx;
}

Allocation-free Iteration (queryEach) recommended

Uses a single QueryRowView instance. Zero allocations per frame on the hot path. Do not store the row reference โ€” it is reused on every callback invocation.

final params = QueryParams([PositionComponent, VelocityComponent]);

world.queryEach(params, (row) {
  final pos = row.get<PositionComponent>();
  final vel = row.get<VelocityComponent>();
  pos.x += vel.dx;
  pos.y += vel.dy;
});

Pair Iteration

Iterate over every combination of entities from two different query sets, or all unique pairs within the same set.

queryEachPairs (cross pairs)

final aParams = QueryParams([AComponent]);
final bParams = QueryParams([BComponent]);

world.queryEachPairs(aParams, bParams, (a, b) {
  // a has AComponent, b has BComponent
  // a.entity != b.entity is guaranteed
});

queryEachPairsSelf (intra-set pairs)

world.queryEachPairsSelf(
  QueryParams([AComponent]),
  (a, b) {
    // Unique unordered pairs: n * (n - 1) / 2 total
    // a.entity != b.entity is guaranteed
  },
);

Raw Buffer & Count

queryRaw

Returns a flat List<Component> with components interleaved per entity. Useful for low-level access or serialisation.

final buffer = world.queryRaw(QueryParams([PositionComponent, ColorComponent]));
// buffer[0] = PositionComponent (entity 1)
// buffer[1] = ColorComponent   (entity 1)
// buffer[2] = PositionComponent (entity 2)
// buffer[3] = ColorComponent   (entity 2)

queryCount

int count = world.queryCount([PositionComponent]);
int count2 = world.queryCountWithParams(params);

Systems

Systems contain the logic that operates on entities. Extend System<T> where T is the type of the argument passed to update().

Defining Systems

class MoveSystem extends System<double> {
  final params = QueryParams([PositionComponent, VelocityComponent]);

  @override
  void init() {
    // Called once when the system is added to the world.
  }

  @override
  void update(double deltaTime) {
    world.queryEach(params, (row) {
      final pos = row.get<PositionComponent>();
      final vel = row.get<VelocityComponent>();
      pos.x += vel.dx * deltaTime;
      pos.y += vel.dy * deltaTime;
    });
  }
}

Systems can also accept record types (Dart 3+) for multiple arguments:

class RenderSystem extends System<({Canvas canvas, Size size})> {
  @override
  void update(args) {
    final canvas = args.canvas;
    final size = args.size;
    // ...
  }
}

System Tags

Tags let you group systems and update them independently with different argument types.

world.addSystem(MoveSystem());               // tag: "" (default)
world.addSystem(CollisionSystem());         // tag: "" (default)
world.addSystem(RenderSystem(), tag: "render");

Updating Systems

// Game loop โ€” runs default-tag systems, then applies commands
world.update(deltaTime);

// Render loop โ€” runs "render"-tag systems, then applies commands
world.update((canvas: canvas, size: size), tag: "render");

// Run a system once without registering it
world.runSystemOnce(MoveSystem(), args: 0.016);

Resources

Resources are singleton objects attached to the world. Useful for shared state like input, timers, configuration, etc.

class GameTime {
  double seconds = 0;
}

// Add
world.addResource(GameTime());

// Get & mutate
final time = world.getResource<GameTime>()!;
time.seconds += 1;

// Remove
world.removeResource<GameTime>();

Resource Tags

Tags allow multiple instances of the same type without key collision:

world.addResource(GameTime(), tag: "menu");
final t = world.getResource<GameTime>(tag: "menu");
world.removeResource<GameTime>(tag: "menu");
Note: Adding a resource with the same type and tag replaces the old one. Generic types like FooGeneric<String> and FooGeneric<double> are distinct keys.

Commands vs Instant

TECS exposes two ways to mutate the world. Understanding the difference is critical for correctness.

CommandBuffer โ€” Deferred Changes world.commands.*

Commands are queued and applied later, either at the end of world.update() or when world.applyCommands() is called manually.

Use inside queries and systems. When iterating entities with queryEach, queryEachPairs, etc., you must use deferred commands โ€” never world.instant.* โ€” to avoid concurrent modification of the archetype storage.
world.queryEach(
  QueryParams([HealthComponent]),
  (row) {
    if (row.get<HealthComponent>().health <= 0) {
      world.commands.removeEntity(row.entity);       // โœ“ deferred
    }
  },
);

world.applyCommands();  // now the entity is actually removed
MethodDescription
addComponent(entityID, component)Queues component addition.
removeComponent(entityID, componentType)Queues component removal.
addComponents(entityID, components)Queues bulk addition.
removeComponents(entityID, componentTypes)Queues bulk removal.
removeEntity(entityID)Queues entity removal. Deduplicated.
createEntityWith(list)Queues entity creation.
createEntities(list)Queues bulk entity creation.

The command buffer is smart about ordering:

InstantChanges โ€” Immediate Changes world.instant.*

Changes take effect immediately. The archetype storage is modified on the spot.

Never use inside query iteration. Calling world.instant.* while iterating with queryEach, queryEachPairs, etc. will corrupt the iteration because the archetype's component lists are structurally modified.
MethodDescription
addComponent(entityID, component)Adds a component immediately.
addComponents(entityID, components)Adds multiple components immediately.
removeComponent<T>(entityID)Removes a component by type immediately.
removeComponents(entityID, componentTypes)Removes multiple components immediately.
removeEntity(entityID)Removes entity immediately. Returns false if already dead.
removeEntities(entityIDs)Batch-removes entities (more efficient than individual calls).
createEntityWith(components)Creates entity with components immediately.
createEntities(entitiesComponents)Creates multiple entities immediately.

When to Use Which

SituationUse
Inside queryEach / queryEachPairs / queryEachPairsSelfworld.commands.*
Inside a System's update() (during iteration)world.commands.*
Setup / initialisation (outside iteration)world.instant.*
After world.update() (post-frame) or between framesworld.instant.*
When you need the change to be visible immediatelyworld.instant.*
When you want changes to flush at frame boundaryworld.commands.*

API Reference

World

World()
Creates a new empty world.
isAlive(EntityID)
Returns whether the entity exists.
createEntity()
Creates a new empty entity, returns its ID.
getComponent<T>(EntityID)
Gets a component by type, or null.
addResource(T, {tag})
Adds a resource singleton.
getResource<T>(tag)
Gets a resource by type and optional tag.
removeResource<T>(tag)
Removes and returns a resource, or null.
addSystem(System, {tag})
Registers a system, calls init().
runSystemOnce(System, args)
Runs a system once without registering.
update(T args, {tag})
Runs matching systems, then applies commands.
applyCommands()
Flushes the command buffer.
clearAll()
Resets world completely.
clearEntities()
Clears all entities & archetypes.
clearSystems()
Removes all systems.
clearResources()
Removes all resources.
componentID<T>()
Returns the internal numeric ID for a component type.
findMatchingArchetypes(SetHash)
Finds all archetypes whose set contains the given hash.

Queries

query(List<Type>)
Returns List<QueryRow> for the given component types.
queryWithParams(QueryParams)
Cached variant โ€” uses pre-resolved QueryParams.
queryEach(QueryParams, fn)
Allocation-free callback-based iteration.
queryEachPairs(a, b, fn)
Cross-iteration over two query sets.
queryEachPairsSelf(p, fn)
Unique unordered pairs within same set.
queryRaw(QueryParams)
Returns flat List<Component> buffer.
queryCount(List<Type>)
Counts entities matching the component set.
queryCountWithParams(QueryParams)
Cached count variant.

QueryParams

QueryParams(List<Type>)
Creates cached query parameters. Builds type-index map.
activate(World)
Resolves component IDs and validates all types exist. Returns false if any type is unregistered.
isActivated
Whether the params have been resolved against a world.
hash
The SetHash representing the component composition.
componentIDs
Resolved numeric IDs for each component type.

QueryRow / QueryRowView

.entity
The EntityID this row represents.
get<T>()
Retrieves a component by type from the current row.

CommandBuffer

addComponent(entityID, component)
Queue component addition.
removeComponent(entityID, componentType)
Queue component removal.
addComponents(entityID, components)
Queue bulk component addition.
removeComponents(entityID, componentTypes)
Queue bulk component removal.
removeEntity(entityID)
Queue entity removal.
createEntityWith(components)
Queue entity creation.
createEntities(entities)
Queue bulk entity creation.
apply(World)
Execute all queued commands.
clear()
Clear the buffer without applying.
isEmpty
Whether the buffer has any pending commands.
length
Total number of pending operations.

InstantChanges (world.instant)

addComponent(entityID, component)
Immediately add a component.
addComponents(entityID, components)
Immediately add multiple components.
removeComponent<T>(entityID)
Immediately remove a component by type.
removeComponents(entityID, componentTypes)
Immediately remove multiple components.
removeEntity(entityID)
Immediately remove an entity. Returns bool.
removeEntities(entityIDs)
Immediately batch-remove entities.
createEntityWith(components)
Immediately create entity with components.
createEntities(entitiesComponents)
Immediately bulk-create entities.

Types

NameDefinitionDescription
EntityIDintUnique entity identifier.
ComponentIDintInternal numeric component type identifier.

Component

MemberDescription
entityIDThe EntityID this component belongs to. Set automatically on attachment.

System<T>

MemberDescription
worldReference to the World. Set by addSystem().
tagThe tag assigned by addSystem().
init()Override for one-time setup when system is registered.
update(T args)Called every frame. Contains your system logic.

TECS โ€” Tremble ECS ยท Built with Dart