Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

6 Commits

Repository files navigation

flecs

Idiomatic Zig 0.16 bindings for the flecs entity-component-system (flecs v4.1.1, fetched via build.zig.zon).

constPosition=struct { x: f32, y: f32 };
constVelocity=struct { x: f32, y: f32 };
fnmove(dt: flecs.Delta, pos: *Position, vel: *constVelocity) void {
pos.x+=vel.x*dt.s;
pos.y+=vel.y*dt.s;
}
varworld=tryflecs.World(.{}).init(.{});
deferworld.deinit();
_=world.spawn(.{ Position{ .x=0, .y=0 }, Velocity{ .x=1, .y=2 } });
_=world.system(.on_update, move);
while (world.progress(1.0/60.0)) {}

Design

One rule: the binding translates declarations into flecs calls; it never invents runtime behavior. Everything is a plain Zig container, and everything comptime can derive is derived:

  • Components are plain structs; tags are zero-size structs.
  • A system is a function whose signature is its query.
  • A query is a struct whose fields are its terms (named rows).
  • Configuration is a declaration on the type - pub const flecs_traits, pub const sort, pub fn onAdd, … - never a parallel descriptor bag.
  • Modules and extensions are Zig containers.

Pointer-ness encodes access (*T read-write, *const T read, ?*const T optional); wrapper types encode operators (With, Up, Pair, …).

Build

zig build test# run the test suite (the executable spec)
zig build examples # build every example
zig build run-hello # run one example

Use as a dependency

Fetch it into your project (records the URL + hash in build.zig.zon):

zig fetch --save git+https://github.com/zigsel/flecs

Then wire the flecs module into your build.zig:

constflecs=b.dependency("flecs", .{
.target=target,
.optimize=optimize,
});
exe.root_module.addImport("flecs", flecs.module("flecs"));

That's all — the dependency pulls the flecs amalgamation from its own upstream release, compiles it, and links libc for you. In code: const flecs = @import("flecs");.

Building this package itself extracts the flecs source into a project-local zig-pkg/ (a build cache artifact — gitignored, regenerated on demand).

Tour

Components

constPosition=struct { x: f32, y: f32 }; // plain dataconstActive=struct {}; // zero-size -> tagconstGravity=struct { // singletong: f32=9.81,
pubconstflecs_traits: flecs.Traits= .{ .singleton=true };
};
constMesh=struct {
handle: Handle,
pubfninit(self: *Mesh) void { self.handle=.invalid; } // ctor: fresh storagepubfndeinit(self: *Mesh) void { self.handle.release(); } // dtor: free ownedpubfnonAdd(self: *Mesh, e: flecs.Entity) void { register(e); } // event: added to an entitypubfnonSet(self: *Mesh) void { self.handle.upload(); } // event: value was setpubfnonRemove(self: *Mesh, e: flecs.Entity) void { notify(e); } // event: left an entity
};

Two kinds of hook, don't confuse them:

  • init / deinit are the constructor / destructor (storage lifetime). init(*T) runs on fresh, zeroed storage whenever the component is added without a value (add, ensure) - the place to apply defaults; deinit runs once when the instance's memory is reclaimed (remove, destroy, clear, andworld.deinit()) - the leak-safe place to release resources.
  • onAdd / onSet / onRemove are event hooks (entity transitions). They run with entity context, for reactive logic - not storage cleanup. Releasing the same resource in both onRemoveanddeinit double-frees it; pick one.

init only matches pub fn init(*T) void; a value-returning init (the common Zig convenience constructor) is left alone. Note flecs ignores Zig field defaults on a bare add/ensure - use init if you need them applied.

Configure traits on the type, or explicitly - every flecs trait is a field (sparse, exclusive, transitive, cleanup, one_of, with, …):

_=world.component(Likes, .{ .storage=.sparse, .exclusive=true });

Entities & bundles

conste=world.spawn(.{
Position{ .x=1, .y=2 }, // component valueActive, // tag typeflecs.childOf(parent), // relationships, all in one callflecs.pair(Likes, bob),
});
e.set(Position{ .x=9, .y=9 }); // add-or-write_=e.get(Position); // ?*const Te.ensure(Velocity).x=5; // get-mut, add if missing_=e.getMut(Velocity); // ?*T, null if absent (never adds)e.emplace(Velocity).*= .{...}; // get raw storage, construct in placee.toggle(Velocity, false); // disable without removingvarr=e.ref(Position); // cached ref: fast repeated r.get()_=e.clone(true); e.clear(); e.destroy();
// reusable bundles + bulk SoA spawnconstEnemy=flecs.bundle(.{ Health{ .hp=100 }, Collider{}, Active });
_=world.spawn(Enemy++ .{Position{ .x=0, .y=0 }});
_=world.spawnMany(1000, .{ .pos=positions_slice, .vel=Velocity{} });

Queries

The row struct is the query - named, typed fields, declared inline:

varq=tryworld.query(struct {
e: flecs.Entity,
pos: *Position, // read-writevel: *constVelocity, // readboost: ?*constBoost, // optional_: flecs.Without(Dead), // filterpubconstsort=flecs.ascBy(Position, .y); // config as decls
});
deferq.deinit();
varit=q.iter();
deferit.deinit(); // idempotent; early break is safewhile (it.next()) |row|row.pos.x+=row.vel.x;
vart=q.tableIter(); // per-table SoA sliceswhile (t.next()) |tab|for (tab.pos, tab.vel) |*p, v|p.x+=v.x;

Full operator set as wrapper fields: With · Without · Or · AndFrom/OrFrom/NotFrom · Up/Cascade (traversal) · Singleton · From("Entity", ...) (fixed source) · Scope(.not, .{...}) · Pair(R, ...) (wildcard + matched target). Plus pub const group, pub const cache, the tuple form world.query(.{ *Position, *const Velocity }), change detection (q.changed()), introspection (q.count(), q.isTrue()), JSON (q.toJson), and world.queryExpr("...") (DSL hatch) — which also carries named query variables: q.findVar("friend") + it.setVar(v, entity) to constrain a $var before iterating.

Iteration has more modes: q.pageIter(offset, limit) (a windowed slice), q.workerIter(index, n) (manual sharding across threads), and on a grouped query it.setGroup(id) / it.skip() / q.groupInfo(id).

Systems

fnmove(dt: flecs.Delta, pos: *Position, vel: *constVelocity) void { ... }
_=world.system(.on_update, move); // bare functionconstGravity=struct { // struct form + configpubconstphase=.on_update;
pubfneach(p: *Position, v: *Velocity, dt: flecs.Delta) void { ... }
};
_=world.add(Gravity);
constSpawner=struct { // stateful (closure-like)elapsed: f32=0,
pubfneach(self: *@This(), dt: flecs.Delta, _: *Position) void { self.elapsed+=dt.s; }
};
_=world.add(Spawner{}); // instance persists

Run-systems drive their own iteration with Query/Stage/Res/ResMut params - multi-query, nested loops, deferred structural change:

fncollide(
ships: flecs.Query(struct { e: flecs.Entity, pos: *constPosition, _: flecs.With(Ship) }),
rocks: flecs.Query(struct { pos: *constPosition, _: flecs.With(Asteroid) }),
score: flecs.ResMut(Score),
stage: flecs.Stage,
) void {
varsi=ships.iter(); defersi.deinit();
while (si.next()) |s| { ...stage.destroy(s.e); score.v.points+=10; }
}

Observers & events

fnonSpawn(_: flecs.OnSet(Position), e: flecs.Entity, p: *constPosition) void { ... }
_=world.observe(onSpawn);
// custom events with a payload (the *const E param is the event data)constDamage=struct { amount: u32, crit: bool };
fnonHit(_: flecs.OnEvent(Damage), hp: *Health, d: *constDamage) void { hp.hp-|=d.amount; }
_=world.observe(onHit);
world.emit(Damage{ .amount=10, .crit=true }, .{ .target=e }); // or stage.enqueue// monitors fire on enter/exit a query; observeOpts also has yield_existing_=world.observeOpts(onAlive, .{ .monitor=true });

Relationships, prefabs, pipelines

alice.addPair(Likes, bob);
alice.setPair(Owes{ .gold=30 }, bob); // pair with dataalice.removePair(Likes, bob); // (also ensurePair for get-mut)varts=alice.targets(Likes); // iterate all targetsalice.addEnum(Team.red); // enum components -> (Team, .case)constFighter=world.prefab(.{ .name="Fighter", .with= .{ Health{ .hp=100 }, flecs.autoOverride(Position) } });
constgrunt=world.spawn(.{ flecs.isA(Fighter), Position{ .x=5, .y=5 } });
constai=world.phase(.pre_update); // custom phases (dependency chain)constmovement=world.phaseAfter(ai);
_=world.systemIn(movement, moveSys);
consttimer=world.timer(0.5); // shared tick sourceworld.setTickSource(aiSystem, timer);

Reflection, JSON & Script

constAgent=struct {
name: [:0]constu8, speed: f32, heading: Dir, waypoints: [3]f32,
pubconstflecs_units= .{ .speed=flecs.units.MetersPerSecond };
pubconstflecs_ranges= .{ .speed= .{ .min=0, .max=300 } };
};
_=world.reflect(Agent);
constjson=tryworld.toJson(Agent{ ... }, allocator); // also entity/world JSONtryworld.script(
\\Turret { Position: {10, 20} Velocity: {1, 0} }
); // + managed loadScripttryworld.scriptWith( // bind Zig values as $vars\\Spawn { Position: {$x, $y} }
, .{ .x=@as(f32, 42), .y=@as(f32, 7) });
varcur=world.metaCursor(Agent, &value); // field-by-field at runtimetrycur.push(); trycur.member("speed"); trycur.set(@as(f32, 5)); trycur.pop();

Plus the Explorer: world.enableRest(.{}), world.importStats(), entity.setDoc(.{ .brief = ... }), world.alert(.{...}), world.metric(...).

Modules & extensions

constphysics=struct {
pubconstGravity=struct { g: f32=9.81, pubconstflecs_traits: flecs.Traits= .{ .singleton=true } };
pubfnintegrate(dt: flecs.Delta, p: *Position, v: *Velocity, g: flecs.Singleton(*constGravity)) void { ... }
pubfninit(world: anytype) !void { world.set(Gravity{}); _=world.system(.on_update, integrate); }
};
tryworld.import(physics);
// extensions graft a typed namespace onto the world typevarworld=tryflecs.World(.{ .ext= .{spatial} }).initExt(.{}, .{spatial.Options{ .cell=32 }});
constcs=world.ext(spatial).cellSize();

Threading & runtime

flecs.runtime(...) is process-global; call it once at startup, before any world.

// built-in worker threadsvarworld=tryflecs.World(.{}).init(.{ .threads=4 });
_=world.systemOpts(.on_update, move, .{ .multi_threaded=true });
// route all allocations through a Zig allocatorflecs.runtime(.{ .allocator=my_gpa });
// pure-std platform layer (no libc/pthread threading dependency)flecs.runtime(.{ .threading=.std });
varworld=tryflecs.World(.{}).init(.{ .task_threads=4 });
// run flecs' parallel pipeline on a std.Io executorflecs.runtime(.{ .io=threaded.io() });
varworld=tryflecs.World(.{}).init(.{ .task_threads=4 });
// manual staging: record structural changes off-thread, then mergeworld.setStageCount(n);
_=world.readonlyBegin(true);
// worker i drives world.stage(i) (itself a world handle) ...world.readonlyEnd(); // merges every stage

Other knobs: world.setTimeScale (slow-mo/pause), resetClock, dim (preallocate), measureFrameTime; entity-id management for networking (world.makeAlive, getAlive, e.exists(), e.isValid()); and flecs.log.setLevel(...) / enableColors(...) for flecs' log output.

Advanced / low-level

For systems work that needs to reach under the abstraction:

// async stage: queue commands off-thread, merge when readyvars=world.asyncStage();
flecs.Entity.init(s.raw, e.id).set(Position{ ... });
s.merge(); world.freeStage(s);
// exclusive access: assert single-thread ownership (debugging cross-thread bugs)world.exclusiveAccessBegin("main"); deferworld.exclusiveAccessEnd(false);
// fast locked single-entity access (skips per-component lookups)varw=e.write().?; w.get(Position).?.x+=1; w.end();
// archetype inspection: the table (SoA storage) an entity lives inconstt=e.table().?;
for (t.column(Position).?) |*p|p.x=0; // whole column at once// maintenance / introspectionworld.setWith(Tag); // auto-add Tag to new entities (scoped)_=world.deleteEmptyTables(.{ .delete_generation=5 });
world.shrink(); // return reserved memory to the OSconstall=world.entities(); // snapshot of every id// archetype graph + bulk placement, dynamic value constructionconstt2=e.table().?.with(Tag); // archetype reachable by adding Tag_=world.spawnInTable(t2); // create straight into an archetypee.moveTo(t2); // move an entity between archetypesworld.valueInit(T, &scratch); // run T's ctor on raw storage

The flecs feature set is wrapped end-to-end; the raw C bindings are an internal detail and are not re-exported, so application code never drops to C.

Examples

zig build run-<name> - each is self-contained and prints a verifiable result.

helloentitycomponent
query_basicsquery_advancedrelationship
system_eachsystem_runsystem_threaded
observer_eventsprefabbundle
pipelinemodulereflect_json
scriptexplorer
runtime_allocatorruntime_std_threadsruntime_io

Status

The binding wraps the full flecs 4.1.1 feature set - core ECS, queries, systems/observers, relationships, prefabs, pipelines, reflection/JSON/Script, staging, tables, and the addons - behind idiomatic Zig. The raw C API is not exposed; everything is reachable through the typed surface above.

Known edges:

  • Named query variables live on the queryExpr DSL path (findVar + it.setVar); a variable binds across terms, so it has no place in a single comptime struct field of a typed query.
  • String opaque serializes to JSON but doesn't deserialize (ownership).
  • TreeSpawner has no public C API in flecs 4.1.1 - nothing to bind.
  • Multi-world is supported sequentially; concurrent worlds sharing the static component-id cache are not. The flecs.runtime knobs are one-shot by design.
  • Requires Zig 0.16.

Layout

build.zig module + flecs C compilation + tests/examples
build.zig.zon flecs v4.1.1 pulled as a package dependency
src/
root.zig public API surface
world.zig World(cfg) builder + the world API
entity.zig Entity handle
meta.zig component registration, traits, enum/id helpers
terms.zig query term wrappers + sort specs
bundle.zig spawn-bundle items
compile.zig shared term compiler (queries + systems)
query.zig typed Query(Row), iteration, untyped Expr
system.zig each-/run-/stateful-system & observer trampolines
stage.zig deferred command target
events.zig custom event emission
reflect.zig @typeInfo -> EcsStruct meta, JSON
meta_cursor.zig runtime field-by-field reflection cursor
units.zig typed unit-entity accessors
log.zig flecs logging controls
addons.zig doc / json / app / units / bitmask / alerts / metrics
runtime.zig allocator + pure-std / std.Io platform injection
tests.zig executable spec
examples/ 20 self-contained, runnable examples

License

The binding follows the flecs license (MIT). flecs © Sander Mertens.

About

zig bindings for flecs ecs

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages