Core Principles of the Flecs ECS Pattern in Equilibrium Engine

The Equilibrium Engine implements the Flecs ECS pattern through a world-centric architecture that emphasizes explicit component registration, modular system imports, declarative queries with change detection, and data-oriented design for cache-friendly iteration.

The Equilibrium Engine leverages flecs—a fast, data-oriented Entity-Component-System library—to power its game architecture. Understanding how this engine applies the Flecs ECS pattern reveals a design built on strict separation of data and behavior, hierarchical relationships, and high-performance query filters. By examining the source code in clibequilibrium/equilibriumengine, we can identify the foundational concepts that govern entity lifecycle, component storage, and system execution.

World-Centric Architecture and Explicit Registration

At the heart of the Equilibrium Engine's Flecs implementation lies the world as the central container. The engine creates a single ecs_world_t (aliased as world_t in equilibrium/world.h) that owns all entities, components, and systems. All API calls route through this object, ensuring a unified state management context.

Before any component can be used, it must be explicitly registered with the world. The engine follows a two-phase registration pattern using ECS_COMPONENT_DECLARE in headers and ECS_COMPONENT_DEFINE in module implementation files. In equilibrium/components/transform.c, components like Position, Rotation, and Transform are defined within the TransformComponents module import function, making them known to the world and enabling fast lookups.

/* In transform.c */
ECS_COMPONENT_DECLARE(Position);
ECS_COMPONENT_DECLARE(Rotation);
ECS_COMPONENT_DECLARE(Transform);

void TransformComponentsImport(world_t *world) {
    ECS_MODULE(world, TransformComponents);
    
    ECS_COMPONENT_DEFINE(world, Position);
    ECS_COMPONENT_DEFINE(world, Rotation);
    ECS_COMPONENT_DEFINE(world, Transform);
}

Modular Composition with Flecs Imports

The Equilibrium Engine isolates functionality through Flecs modules that are imported using the ECS_IMPORT macro. This pattern prevents circular dependencies and allows the engine to compose features on demand. For example, TransformSystem and TransformComponents are separate modules that can be imported independently based on the application's needs.

In equilibrium/systems/transform_system.c, the TransformSystemImport function registers both the component definitions and the systems that operate on them. This modular approach is visible in the directory structure, where simulation systems live in equilibrium/engine-simulation/ while rendering systems reside in equilibrium/systems/rendering/, yet both operate on the same ECS world.

world_t *world = world_create();

/* Import the component module so the world knows about Position, Transform, … */
ECS_IMPORT(world, TransformComponents);

/* Import the system that will compute world-space matrices */
ECS_IMPORT(world, TransformSystem);

Declarative Query-Driven Systems

Systems in the Equilibrium Engine are defined by declarative queries rather than imperative entity iteration. Using the ECS_SYSTEM macro, developers specify filter expressions that select entities based on component presence, optionality (marked with ?), and relationships. The ApplyTransform system in equilibrium/systems/transform_system.c demonstrates this pattern by querying entities that have Transform, optionally have Rotation and Scale, and participate in hierarchical relationships.

The engine also leverages query change detection to avoid unnecessary computation. Systems can early-out when data remains static by calling ecs_query_changed, as implemented in the ApplyTransform system's early-exit logic at lines 15–18 of transform_system.c.

void ApplyTransform(ecs_iter_t *it) {
    /* Early exit if nothing changed */
    if (!ecs_query_changed(NULL, it)) {
        return;
    }
    
    Transform *t = ecs_field(it, Transform, 1);
    // ... processing logic
}

/* Registration with filter expression */
ECS_SYSTEM(world, ApplyTransform, EcsOnValidate,
    [out] Transform,
    [in] ?Transform(parent|cascade),
    [in] Position(self|up),
    [in] ?Rotation,
    [in] ?Scale);

Data-Oriented Optimization Techniques

The Flecs ECS pattern in Equilibrium Engine adheres to strict data-oriented design principles where components are plain data structs and behavior lives exclusively in systems. Components like Position and Transform (defined in equilibrium/components/transform.h using cglm wrappers) contain no methods—only raw data fields.

To maximize performance, the engine utilizes instanced queries for cache-friendly iteration. By setting .query.filter.instanced = true in system definitions (visible at lines 89–90 of transform_system.c), flecs packs matching components tightly in memory, improving data locality during system execution. This is particularly critical for transform calculations that process large numbers of entities per frame.

Hierarchical Relationships via Cascade Queries

The engine models parent-child transforms using cascade queries that leverage flecs relationship traversal. The (parent|cascade) syntax in query expressions tells flecs to walk the entity hierarchy and supply the parent's transform to child systems. This is implemented in the ApplyTransform system's component list, where [in] ?Transform(parent|cascade) receives the parent's transformation matrix if it exists, while [in] Position(self|up) accesses the entity's own position or searches upward in the hierarchy.

The entity_create macro in equilibrium/entity.h (lines 23–31) further simplifies entity spawning by encapsulating ecs_new, entity naming, component assignment, and tagging with the generic Entity component into a single, readable operation.

/* Spawning a hierarchical entity */
entity_t child = entity_create(
    world,
    "ChildObject",
    Transform, {},
    Position, { .x = 1.0f, .y = 0.0f, .z = 0.0f });

entity_set_parent(child, parent_entity);

Summary

  • World-centric design: A single world_t instance in equilibrium/world.c manages all ECS state, ensuring centralized entity and component lifecycle management.
  • Explicit registration: Components require ECS_COMPONENT_DECLARE in headers and ECS_COMPONENT_DEFINE in modules (as seen in transform.c), establishing type safety and fast lookups.
  • Modular architecture: The ECS_IMPORT macro enables clean separation between components, simulation systems, and rendering systems while sharing a single world instance.
  • Declarative systems: ECS_SYSTEM macros with query filters determine iteration sets, with support for optional components (?) and relationship traversal (parent|cascade).
  • Performance optimization: Instanced queries (.instanced = true) and change detection (ecs_query_changed) minimize CPU overhead and maximize cache locality.
  • Data-oriented approach: Components are pure data structs (defined in transform.h) with behavior isolated in systems, following strict ECS principles.

Frequently Asked Questions

What is the difference between ECS_COMPONENT_DECLARE and ECS_COMPONENT_DEFINE?

ECS_COMPONENT_DECLARE is used in header files (such as equilibrium/components/transform.h) to declare the component type identifier, while ECS_COMPONENT_DEFINE is called within a module's import function (like TransformComponentsImport in transform.c) to register the component with a specific world instance. This separation allows headers to reference component types without requiring the full world context, while implementation files handle the actual registration logic that maps types to flecs internal storage.

How does Equilibrium Engine handle parent-child relationships in Flecs?

The engine utilizes cascade queries with the (parent|cascade) relationship specifier in system definitions. In equilibrium/systems/transform_system.c, the ApplyTransform system declares [in] ?Transform(parent|cascade) to receive the parent's transform matrix when iterating child entities. This declarative approach lets flecs automatically traverse the entity hierarchy and supply parent data during system execution, eliminating manual tree-walking code while maintaining cache-friendly iteration patterns.

What is the purpose of instanced queries in the Flecs ECS pattern?

Instanced queries (enabled by setting .query.filter.instanced = true in system definitions) instruct flecs to pack matching components into contiguous memory arrays during iteration. In the Equilibrium Engine, this is applied to the ApplyTransform system to ensure that Transform, Position, and Rotation components accessed via ecs_field reside in cache-friendly layouts. This optimization reduces memory latency and improves throughput when processing large numbers of entities, which is critical for real-time simulation and rendering loops.

How does the engine separate simulation logic from rendering?

The Equilibrium Engine maintains physical directory separation while using a unified ECS world. Simulation systems reside in equilibrium/engine-simulation/ and rendering systems in equilibrium/systems/rendering/, yet both import the same world_t instance created via world_create() in equilibrium/world.c. Because all systems register with the same world through ECS_IMPORT, simulation updates and rendering queries operate on consistent entity state without requiring manual synchronization glue code, allowing the renderer to access transform data immediately after simulation systems update it.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →