Start

Theory

Reference

Examples

Entity-Component-System

An ECS splits a game object into identity, data and behaviour, so behaviour can be written once and applied to every object that has the right data.

The concept

  • Entity - just an id. It carries no data and no code.
  • Component - a plain bundle of data (a position, a sprite, a collider) attached to an entity.
  • System - a function that walks every entity owning a given set of components and acts on them.

Storing each component type in its own densely packed array keeps iteration cache-friendly, and composing objects from components avoids deep inheritance hierarchies.

Why not inheritance?

The classic alternative is a class tree: Enemy derives from Actor, a flying enemy from Enemy, and so on. It works until a new object needs a mix of branches (a flying, shooting, destructible crate) and the tree has to be reshuffled. With components, that crate is just an entity carrying the right bundle of data; no class had to change.

Data-oriented layout

CPUs are fast at reading memory in order and slow at chasing pointers. A system that touches only Position and Velocity reads two tightly packed arrays and never loads the sprite, health or audio data of the same entities. That is the main performance argument for an ECS, and it grows with the entity count.

Systems and ordering

Systems hold no state of their own; everything they need is in components or shared resources. That makes them easy to test and to reason about, but it means order matters: gravity must run before movement, and movement before collision, or objects react to last frame's positions. Engines therefore fix the system order in one place.

Trade-offs

The price is indirection. Finding "the one entity with component X" means a lookup, behaviour is spread across systems instead of living in one class, and removing an entity while iterating needs care. For small, singular objects (the player controller, say) plain code is often clearer; the ECS earns its keep for the many similar things in a scene.

How ASGE does it

Generational entity handles

An asge::ecs::Entity is an index plus a generation counter. When an entity is destroyed its slot is recycled and its generation is bumped, so any stale handle still held elsewhere fails IsAlive() instead of silently pointing at an unrelated new entity. A slot whose generation reaches its maximum is retired rather than reused. Indices come from a fixed-capacity EntityAllocator built on mem::FreeList (see Memory & threading).

Sparse-set component pools

Each component type lives in its own ComponentPool<T>, a sparse set: a sparse array maps an entity index to a slot in a dense array of components. Insert, Get, Remove and Contains are O(1), and iteration only touches live components. Removal is swap-and-pop, so iteration order may change after a removal.

The Registry

asge::ecs::Registry is the single entry point. It owns the allocator and one pool per component type, created lazily on the first AddComponent<T>. DestroyEntity() strips the entity from every pool. The registry also holds per-registry resources (SetResource<T> / GetResource<T>): at most one value of a type that is not tied to any entity.

Views

Registry::View<Ts...>() returns a lazy, non-owning View over every entity that has all of Ts. It iterates the smallest contributing pool and filters the rest. Entities carrying the Disable marker, or whose ancestor in the Hierarchy does, are skipped unless you call IncludeDisabled(). Use View<T const> for read-only access.

ecs.cpp
struct Health { int m_Hp{ 100 }; };

asge::ecs::Registry registry;

auto entity = registry.CreateEntity();          // Result<Entity>
if ( !entity ) return;
registry.AddComponent( entity.Value(), Health{ 50 } );

// Every entity that has a Health; components arrive as std::reference_wrapper<T>.
for ( auto [e, health] : registry.View<Health>() )
{
    health.get().m_Hp -= 1;
}

Systems in the Game layer

Systems are plain functions over a Registry, such as MovementSystem, PathFollowingSystem and TransformPropagationSystem. Game runs them in one fixed order every frame - see Game loop & timing.

Things to know

  • A view built for a component type that has never been used is permanently empty, even if the type is added later.
  • Views hold raw pointers into the pools and must not outlive the Registry.
  • A pool is not told when an entity dies; Registry::DestroyEntity handles that, so destroy through the registry.

See also

Source: Core/ECS/Registry.hpp, Core/ECS/View.hpp, Core/ECS/ComponentPool.hpp. ECS demo · API reference