ECS Overview

SafiEngine's ECS is flecs. flecs is a pure-C archetype ECS with components, queries, systems, pipelines, observers, hierarchies, and prefabs. If you know Bevy, you already know flecs.

#include <safi/ecs/ecs.h>
#include <safi/ecs/components.h>

Bevy → flecs concept map

Bevyflecs (C)
#[derive(Component)] struct Foo;ECS_COMPONENT_DEFINE(world, Foo);
ResourceSingleton: ecs_singleton_set(world, Foo, { ... });
Query<(&Transform, &mut Velocity)>ecs_query(world, { .terms = {...} })
fn system(q: Query<…>)ECS_SYSTEM(world, system, EcsOnUpdate, Transform, Velocity);
App::default().add_systems(Update, s).add = ecs_dependson(EcsOnUpdate)
Stages (PreUpdate, Update, PostUpdate)Phases (EcsOnLoad, EcsOnUpdate, EcsOnStore)
Commandsecs_new, ecs_set, ecs_delete
Entityecs_entity_t (64-bit id)

Pipeline phases

SafiEngine runs three pipelines per frame:

Variable (wall-clock dt):  EcsOnLoad → EcsPreUpdate → EcsOnUpdate
                                     → EcsOnValidate → EcsPostUpdate
Fixed    (looped N×):      SafiFixedUpdate
Render   (wall-clock dt):  EcsPreStore → EcsOnStore

Use EcsOnUpdate for gameplay, SafiFixedUpdate for physics and deterministic simulation, and EcsOnStore for rendering. See Scheduler & Stages for the details and the accumulator model.

:::warning WIP — Scheduling gaps

  • Startup stage — no one-shot startup system registration API
  • System ordering API — only raw flecs ecs_dependson phase ordering; no explicit before/after dependencies :::

Minimal example

ECS_COMPONENT_DEFINE(world, MyTag);

static void hello(ecs_iter_t *it) {
    for (int i = 0; i < it->count; ++i) printf("tick\n");
}

ECS_SYSTEM(world, hello, EcsOnUpdate, MyTag);
ecs_entity_t e = ecs_new(world);
ecs_add(world, e, MyTag);

Parent/child hierarchy

flecs models hierarchy via the built-in EcsChildOf relationship. SafiEngine wraps that with cycle-rejecting helpers in safi/ecs/hierarchy.h so hand-edited scene files, future drag-reparent UI, and tests all go through one code path:

#include <safi/ecs/hierarchy.h>

bool safi_entity_set_parent      (ecs_world_t *, ecs_entity_t child,
                                   ecs_entity_t parent);  /* false → cycle */
void safi_entity_detach_from_parent(ecs_world_t *, ecs_entity_t child);
int  safi_entity_children        (ecs_world_t *, ecs_entity_t parent,
                                   ecs_entity_t *out, int cap);

Passing parent = 0 is equivalent to safi_entity_detach_from_parent. The transform propagation system traverses EcsChildOf in topological order so a legal reparent mid-frame picks up the correct world matrix the next frame.

See also