ECS game development

Structure a game loop with SIECS.

Keep game state in components and run input, simulation, animation, and rendering as scheduled systems.

Compose entities from game data

Position, velocity, health, transforms, meshes, and ownership are separate components. Each system requests only the values it reads or writes.

Adding or removing a component immediately changes which queries match the entity. The current composition is the entity's game state.

Run the world once per frame

Initialize the world, import the game module, and call ecs_progress(). Input, simulation, animation, and rendering stay inside the ECS schedule.

ecs_init();

ECS_MODULE_IMPORT(game, {});

while (ecs_progress()) {}

ecs_fini();

Order systems with phases

Assign each system to a phase such as EcsPreUpdate, EcsOnUpdate, or EcsOnRender. Callback parameters state which components and resources the system reads or writes.

ecs::system("Move")
    .phase(EcsOnUpdate)
    .each([](
        ecs::res<const Time> time,
        Position &position,
        const Velocity &velocity
    ) {
        position.x += velocity.x * time->dt;
    });

This movement system reads frame time and velocity, writes position, and runs during the update phase.

Store frame-wide state in resources

A resource stores one typed value for the world. Use resources for frame time, input state, scene settings, and renderer state.

struct Time { float dt; };

ecs::set_resource(Time{ .dt = 1.0f / 60.0f });

Use relations for scene hierarchy

ChildOf builds parent-child trees. IsA creates instances that inherit shared component values from an abstract entity.

auto ship = ecs::entity::create("ship");
auto turret = ecs::entity::create("turret").child_of(ship);

auto enemy = ecs::entity::create("Enemy");
enemy.set(Health{ 100 }).abstract();

auto boss = ecs::entity::create("Boss").is_a(enemy);

Package game features as modules

Observers react to component changes and custom events. A module registers the components, systems, and observers for one game feature, then imports them through one typed entry point.

#include <cstdio>

struct Game {
    void import() {
        ecs::observe<ecs::OnSet>().each([](const Health &health) {
            std::printf("health: %d\n", health.value);
        });
    }
};

auto game = ecs::import<Game>();

Reflected components can also be serialized to JSON and inspected through the optional REST explorer while the world is running.