TECS โ Tremble ECS
An archetype-based Entity Component System written in Dart.
Core Principles
- Archetype-based storage โ Entities with the same component composition are stored together in contiguous arrays for cache-friendly iteration.
- Two mutation paths โ Immediate changes via
world.instant.*and deferred changes viaworld.commands.*(see Commands vs Instant). - Allocation-free queries โ
queryEachreuses a single row view to avoid GC pressure on hot paths. - Cached query resolution โ
QueryParamscaches component IDs and archetype lookups between frames.
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
| Method | Description |
|---|---|
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(). |
version | Returns the current world version (used internally for cache invalidation). |
entityCount | Number of alive entities. |
archetypeCount | Number 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]);
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]); void init() { // Called once when the system is added to the world. } 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})> { 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");
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.
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
| Method | Description |
|---|---|
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:
- Entities to remove are tracked in a set (deduplicated).
- Any command targeting a removed entity is skipped during
apply(). - Entities are created first, then all component mutations, then removals.
InstantChanges โ Immediate Changes world.instant.*
Changes take effect immediately. The archetype storage is modified on the spot.
world.instant.* while
iterating with queryEach, queryEachPairs, etc. will corrupt the iteration
because the archetype's component lists are structurally modified.
| Method | Description |
|---|---|
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
| Situation | Use |
|---|---|
Inside queryEach / queryEachPairs / queryEachPairsSelf | world.commands.* |
Inside a System's update() (during iteration) | world.commands.* |
| Setup / initialisation (outside iteration) | world.instant.* |
After world.update() (post-frame) or between frames | world.instant.* |
| When you need the change to be visible immediately | world.instant.* |
| When you want changes to flush at frame boundary | world.commands.* |
API Reference
World
Queries
QueryParams
QueryRow / QueryRowView
CommandBuffer
InstantChanges (world.instant)
Types
| Name | Definition | Description |
|---|---|---|
EntityID | int | Unique entity identifier. |
ComponentID | int | Internal numeric component type identifier. |
Component
| Member | Description |
|---|---|
entityID | The EntityID this component belongs to. Set automatically on attachment. |
System<T>
| Member | Description |
|---|---|
world | Reference to the World. Set by addSystem(). |
tag | The 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