ECS Architecture
Estella uses an Entity-Component-System (ECS) architecture for game object management. ECS separates identity (Entity), data (Component), and behavior (System).
What is ECS?
| Concept | Role | Example |
|---|---|---|
| Entity | A unique identifier (just a number) | Player, Enemy, Bullet |
| Component | Data attached to an entity | Transform, Sprite, Health |
| System | Logic that operates on entities with specific components | MovementSystem, RenderSystem |
Why ECS?
Traditional OOP approach:
GameObject├── Player extends GameObject│ ├── FlyingPlayer extends Player│ └── SwimmingPlayer extends Player└── Enemy extends GameObject └── FlyingEnemy extends EnemyECS approach — compose behaviors freely:
Player = Entity + Transform + Sprite + Health + PlayerInputEnemy = Entity + Transform + Sprite + Health + AIBullet = Entity + Transform + Sprite + Velocity + DamageBenefits:
- Composition — mix and match components without rigid hierarchies
- Data locality — components of the same type are stored contiguously, improving cache performance
- Separation of concerns — systems contain logic, components hold data
- Flexibility — add or remove components at runtime to change behavior
Entities
An entity is a lightweight handle with two parts: an index and a generation. The index identifies the slot in memory, while the generation tracks how many times that slot has been recycled.
This generational handle design means that when an entity is despawned and its slot is reused by a new entity, any stale references to the old entity are automatically detected — their generation won’t match. This prevents bugs where code accidentally operates on a recycled entity.
In practice, you don’t need to think about generations — just use the Entity type as an opaque handle. Entities are typically created in the scene editor, or spawned at runtime with Commands:
import { defineSystem, addStartupSystem, Commands, type Entity } from 'esengine';
addStartupSystem(defineSystem([Commands()], (cmds) => { const bullet: Entity = cmds.spawn().id(); cmds.despawn(bullet);}));Components
Components are data attached to entities. You define components in code, then attach them to entities in the scene editor.
- Builtin components — Transform, Sprite, Camera, etc. (always available in the editor)
- Custom components — defined with
defineComponent/defineTag(appear in the editor after saving)
import { defineComponent, defineTag } from 'esengine';
const Health = defineComponent('Health', { current: 100, max: 100 });const Player = defineTag('Player');After defining these, open the editor, select an entity, and add Health or Player from the “Add Component” menu.
See Components for the full API.
Systems
Systems are functions that query entities with specific components and operate on them. They run automatically on all matching entities in the scene.
import { defineSystem, addSystem, Res, Time, Query, Mut, Transform } from 'esengine';import { Health } from './components';
addSystem(defineSystem( [Res(Time), Query(Mut(Transform), Health)], (time, query) => { for (const [entity, transform, health] of query) { if (health.current <= 0) { transform.position.y -= 100 * time.delta; } } }));See Systems for the full API.
ECS Flow
┌──────────────────────┐ │ Editor: Scene Setup │ Place entities, attach components └──────────┬───────────┘ ▼ ┌──────────────────────┐ │ Scripts: Define │ defineComponent + defineSystem + addSystem └──────────┬───────────┘ ▼ ┌──────────┐ │ Startup │ Startup systems run once └────┬─────┘ ▼ ┌──────────┐ │ Update │◄─┐ Systems query scene entities every frame └────┬─────┘ │ ▼ │ ┌──────────┐ │ │ Render │ │ C++ backend draws all Sprites/Cameras └────┬─────┘ │ └────────┘- Scene setup — place entities and attach components in the editor
- Scripts — define custom components and register systems
- Startup — startup systems run once
- Update loop — systems query and process scene entities every frame
- Render — the C++ backend automatically renders all entities with
SpriteandCameracomponents
How You Interact with ECS
The World stores all entities and components. In your scripts, you interact with it through system parameters:
| What you want to do | System parameter |
|---|---|
| Iterate entities with specific components | Query(ComponentA, ComponentB) |
| Read component data | Query(Component) — data comes with iteration |
| Mutate component data | Query(Mut(Component)) — wrap with Mut |
| Spawn a new entity | Commands() → cmds.spawn() |
| Despawn an entity | Commands() → cmds.despawn(entity) |
| Add a component at runtime | Commands() → cmds.entity(e).insert(Component, data) |
| Remove a component at runtime | Commands() → cmds.entity(e).remove(Component) |
| Read a resource | Res(ResourceType) |
| Mutate a resource | ResMut(ResourceType) |
| React to added components | Query(Added(Component)) |
| React to changed components | Query(Changed(Component)) |
| React to removed components | Removed(Component) |
| Send events between systems | EventWriter(Event) / EventReader(Event) |
| Set parent-child hierarchy | GetWorld() → world.setParent(child, parent) |
| Remove parent | GetWorld() → world.removeParent(entity) |
See Systems and Queries for detailed usage.
Next Steps
- Components — builtin and custom components
- Systems — defining and scheduling systems
- Queries — querying entities
- Resources — global singleton data
- Change Detection — reacting to component changes
- Events — decoupled inter-system communication