Engine Modules and Their Integration in Equilibrium Engine: Structure and Patterns
Equilibrium Engine modules follow a strict flecs-based pattern requiring a header with component declarations, a source file with an import function using ECS_MODULE, and integration via either static ECS_IMPORT or runtime ECS_IMPORT_HOT_RELOADABLE macros.
The clibequilibrium/equilibriumengine repository implements a modular Entity Component System (ECS) architecture built on top of the flecs library. Understanding the expected structure for engine modules and their integration is essential for extending the engine with new components, systems, or utilities while maintaining support for both static linking and runtime hot-reloading.
Module Structure and Conventions
Every module in the Equilibrium Engine adheres to a four-part convention that separates public interfaces from implementation details.
Header Files and Component Declarations
Header files located in equilibrium/components/*.h or equilibrium/systems/*.h provide the public API and forward-declare flecs components using ECS_COMPONENT_DECLARE. For example, equilibrium/components/transform.h declares transform-related components and types, establishing the contract that other modules or game code will depend on.
Source Files and Module Registration
Source files located in equilibrium/components/*.c or equilibrium/systems/*.c define the module implementation. They call ECS_MODULE(world, <ModuleName>) to register the module with the flecs world, use ECS_IMPORT to pull in dependencies, and create components via ECS_COMPONENT_DEFINE. The equilibrium/components/transform.c file demonstrates this pattern by registering the TransformComponents module and defining its associated component storage.
The Import Function Pattern
Each module must export a single import function named <ModuleName>Import that accepts a world_t *world parameter. This function serves as the entry point for the module. For instance, TransformComponentsImport in equilibrium/components/transform.c initializes the module when called by the engine or a parent module.
Hot-Reload Support
Modules intended for dynamic reloading implement the HotReloadableModule component defined in equilibrium/components/hot_reloadable_module.h. This optional marker allows the engine to track module entities and clean them up when the underlying shared library changes, enabling iterative development without restarting the application.
Integration Patterns
The engine supports two distinct integration paths depending on whether the module is built into the binary or loaded as a plugin.
Static Compilation and Linking
For modules compiled directly into the engine, integration occurs at engine initialization using the ECS_IMPORT macro. In equilibrium/engine.c, the engine_init() function imports built-in modules like FlecsMonitor and custom engine modules before the main loop begins. This approach requires only that the module's import function be linked into the final executable.
/* equilibrium/engine.c */
ECS_TAG_DEFINE(engine.world, Entity);
ECS_IMPORT(engine.world, FlecsMonitor); // built-in flecs monitoring
ECS_IMPORT(engine.world, TransformComponents); // custom engine module
Runtime Hot-Reloading
For plugin-style development, the engine provides the ECS_IMPORT_HOT_RELOADABLE macro defined in equilibrium/components/hot_reloadable_module.h. This macro performs three critical actions: it imports the specified module and the hot-reloadable helper component, creates a hidden entity storing a HotReloadableModule component linked to the plugin's cr_plugin context, and tracks the module name for cleanup during reload cycles.
/* editor/editor.c */
ECS_IMPORT_HOT_RELOADABLE(ctx, ImguiBgfxSdlSystem);
ECS_IMPORT_HOT_RELOADABLE(ctx, ImguiDockspaceSystem);
When the plugin receives a CR_LOAD operation, the macro expands to call reload_modules(ctx), which iterates over all HotReloadableModule entities, deletes their associated flecs entities to prevent stale data, and re-imports the fresh code.
Engine Entry Points and Lifecycle
Understanding where modules fit into the engine lifecycle requires examining three key files that manage initialization, updating, and plugin bridging.
Core Engine Initialization
The equilibrium/engine.c file defines the engine_t façade and contains engine_init(), which creates the flecs world, registers core tags like Entity, and stores the world pointer used by all subsequent module imports. The companion engine_update() function runs system queries each frame, processing all active ECS systems.
Plugin Simulation Bridge
The equilibrium/engine-simulation/engine_simulation.c file provides the glue between the engine and the cr plugin system. It forwards CR_STEP events to engine_update(), allowing hot-reloadable modules to participate in the frame update loop while maintaining their dynamic loading capabilities.
Editor Integration Example
The editor/editor.c file demonstrates practical hot-reloadable usage in a real application. It imports UI systems as plugins and performs cleanup routines during reload events, serving as a reference implementation for developers building their own hot-reloadable tools.
Practical Implementation Examples
The following examples illustrate creating a new physics module and integrating it through both static and hot-reloadable paths.
Example 1: Creating a Component Module
First, define the header in physics_component.h:
#ifndef PHYSICS_COMPONENT_H
#define PHYSICS_COMPONENT_H
#include "base.h"
ECS_COMPONENT_DECLARE(RigidBody);
EQUILIBRIUM_API void PhysicsComponentsImport(world_t *world);
#endif
Next, implement the module in physics_component.c:
#include "physics_component.h"
ECS_COMPONENT_DEFINE(world, RigidBody);
void PhysicsComponentsImport(world_t *world) {
ECS_MODULE(world, PhysicsComponents);
ECS_IMPORT(world, CglmComponents); // reuse glm types
ECS_COMPONENT_DEFINE(world, RigidBody);
}
Integrate statically in engine_init:
ECS_IMPORT(engine.world, PhysicsComponents);
Or integrate as hot-reloadable in a plugin:
ECS_IMPORT_HOT_RELOADABLE(ctx, PhysicsComponents);
Example 2: Implementing a Dependent System
Create a system that operates on the physics component in physics_system.c:
#include "physics_component.h"
void SimulatePhysics(ecs_iter_t *it) {
RigidBody *rb = ecs_field(it, RigidBody, 1);
for (int i = 0; i < it->count; ++i) {
/* simple Euler integration */
rb[i].position[0] += rb[i].velocity[0] * it->delta_time;
rb[i].position[1] += rb[i].velocity[1] * it->delta_time;
rb[i].position[2] += rb[i].velocity[2] * it->delta_time;
}
}
void PhysicsSystemImport(world_t *world) {
ECS_MODULE(world, PhysicsSystem);
ECS_IMPORT(world, PhysicsComponents);
ECS_SYSTEM(world, SimulatePhysics, EcsOnUpdate,
[in] physics.components.RigidBody);
}
Summary
- Module structure requires a header with
ECS_COMPONENT_DECLARE, a source file withECS_MODULEandECS_COMPONENT_DEFINE, and an import function named<ModuleName>Import. - Static integration uses
ECS_IMPORT(world, ModuleName)during engine initialization inequilibrium/engine.c. - Hot-reloadable integration uses
ECS_IMPORT_HOT_RELOADABLE(ctx, ModuleName)to enable runtime reloading via theHotReloadableModulecomponent. - Dependencies between modules are resolved using
ECS_IMPORTinside the import function before defining components or systems. - Key files include
equilibrium/components/transform.cfor component patterns,equilibrium/components/hot_reloadable_module.cfor reloading infrastructure, andeditor/editor.cfor practical usage examples.
Frequently Asked Questions
What is the minimum required structure for a new Equilibrium Engine module?
Every module requires a header file containing ECS_COMPONENT_DECLARE statements and an import function prototype, plus a source file implementing the import function with ECS_MODULE(world, ModuleName) and ECS_COMPONENT_DEFINE calls. This pattern is visible in equilibrium/components/transform.h and equilibrium/components/transform.c.
How does hot-reloading work with engine modules?
Hot-reloading leverages the ECS_IMPORT_HOT_RELOADABLE macro from equilibrium/components/hot_reloadable_module.h, which creates a tracking entity with a HotReloadableModule component. When the plugin reloads, the engine iterates these entities, cleans up stale flecs data, and re-imports the fresh code without restarting the application.
Can modules depend on other modules?
Yes, modules declare dependencies by calling ECS_IMPORT(world, DependencyName) inside their import function before using components from that dependency. For example, PhysicsComponentsImport imports CglmComponents to access math types before defining physics components that use them.
Where is the best place to look for a complete module example?
The equilibrium/components/transform.c file provides the canonical pattern for component modules, while equilibrium/systems/transform_system.c demonstrates system modules that import component modules. For hot-reloadable integration patterns, examine editor/editor.c which imports UI systems dynamically.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →