Flecs v3.2
A fast entity component system (ECS) for C & C++
Loading...
Searching...
No Matches
Systems

Systems are queries + a function that can be ran manually or get scheduled as part of a pipeline. To use systems, applications must build Flecs with the FLECS_SYSTEM addon (enabled by default).

An example of a simple system:

  • C

    // System implementation
    void Move(ecs_iter_t *it) {
    // Get fields from system query
    Position *p = ecs_field(it, Position, 1);
    Velocity *v = ecs_field(it, Velocity, 2);
    // Iterate matched entities
    for (int i = 0; i < it->count, i++) {
    p[i].x += v[i].x;
    p[i].y += v[i].y;
    }
    }
    // System declaration
    ECS_SYSTEM(world, Move, EcsOnUpdate, Position, [in] Velocity);
    #define ECS_SYSTEM(world, id, phase,...)
    Declare & define a system.
    Definition system.h:137

    In C, a system can also be created with the ecs_system_init function / ecs_system shorthand which provides more flexibility. The same system can be created like this:

    ecs_entity_t ecs_id(Move) = ecs_system(world, {
    .entity = ecs_entity(world, { /* ecs_entity_desc_t */
    .name = "Move",
    .add = { ecs_dependson(EcsOnUpdate) }
    }),
    .query.filter.terms = { /* ecs_filter_desc_t::terms */
    { ecs_id(Position) },
    { ecs_id(Velocity), .inout = EcsIn }
    }
    .callback = Move
    })
    #define ecs_system(world,...)
    Shorthand for creating a system with ecs_system_init().
    Definition system.h:161
    ecs_id_t ecs_entity_t
    An entity identifier.
    Definition flecs.h:318
    #define ecs_entity(world,...)
    Shorthand for creating an entity with ecs_entity_init().
    Definition flecs_c.h:200
    @ EcsIn
    Term is only read.
    Definition flecs.h:691
  • C++

    // System declaration
    flecs::system sys = world.system<Position, const Velocity>("Move")
    .each([](Position& p, const Velocity &v) {
    // Each is invoked for each entity
    p.x += v.x;
    p.y += v.y;
    });
  • C#

    // System declaration
    Routine sys = world.Routine<Position, Velocity>("Move")
    .Each((ref Position p, ref Velocity v) =>
    {
    // Each is invoked for each entity
    p.X += v.X;
    p.Y += v.Y;
    });

To manually run a system, do:

  • C

    ecs_run(world, ecs_id(Move), 0.0 /* delta_time */, NULL /* param */)
    FLECS_API ecs_entity_t ecs_run(ecs_world_t *world, ecs_entity_t system, ecs_ftime_t delta_time, void *param)
    Run a specific system manually.
  • C++

    flecs::system sys = ...;
    sys.run();
  • C#

    Routine sys = ...;
    sys.Run();

By default systems are registered for a pipeline which orders systems by their "phase" (EcsOnUpdate). To run all systems in a pipeline, do:

To run systems as part of a pipeline, applications must build Flecs with the FLECS_PIPELINE addon (enabled by default). To prevent a system from being registered as part of a pipeline, specify 0 as phase:

  • C

    ECS_SYSTEM(world, Move, 0, Position, [in] Velocity);
  • C++

    flecs::system sys = world.system<Position, const Velocity>("Move")
    .kind(0)
    .each([](Position& p, const Velocity &v) { /* ... */ });
  • C#

    Routine sys = world.Routine<Position, Velocity>("Move")
    .Kind(0)
    .Each((ref Position p, ref Velocity v) => { /* ... */ });

System Iteration

Because systems use queries, the iterating code looks similar:

  • C

    // Query iteration
    ecs_iter_t it = ecs_query_iter(world, q);
    // Iterate tables matched by query
    while (ecs_query_next(&it)) {
    // Get fields from query
    Position *p = ecs_field(it, Position, 1);
    Velocity *v = ecs_field(it, Velocity, 2);
    // Iterate matched entities
    for (int i = 0; i < it->count, i++) {
    p[i].x += v[i].x;
    p[i].y += v[i].y;
    }
    }
    // System code
    void Move(ecs_iter_t *it) {
    // Get fields from system query
    Position *p = ecs_field(it, Position, 1);
    Velocity *v = ecs_field(it, Velocity, 2);
    // Iterate matched entities
    for (int i = 0; i < it->count, i++) {
    p[i].x += v[i].x;
    p[i].y += v[i].y;
    }
    }
    ecs_iter_t ecs_query_iter(const ecs_world_t *world, ecs_query_t *query)
    Return a query iterator.
    bool ecs_query_next(ecs_iter_t *iter)
    Progress the query iterator.
  • C++

    // Query iteration (each)
    q.each([](Position& p, const Velocity &v) { /* ... */ });
    // System iteration (each)
    world.system<Position, const Velocity>("Move")
    .each([](Position& p, const Velocity &v) { /* ... */ });
    // Query iteration (iter)
    q.iter([](flecs::iter& it, Position *p, const Velocity *v) {
    for (auto i : it) {
    p[i].x += v[i].x;
    p[i].y += v[i].y;
    }
    });
    // System iteration (iter)
    world.system<Position, const Velocity>("Move")
    .iter([](flecs::iter& it, Position *p, const Velocity *v) {
    for (auto i : it) {
    p[i].x += v[i].x;
    p[i].y += v[i].y;
    }
    });
    Class for iterating over query results.
    Definition iter.hpp:68

    The iter function can be invoked multiple times per frame, once for each matched table. The each function is called once per matched entity.

    Note that there is no significant performance difference between iter and each, which can both be vectorized by the compiler. By default each can actually end up being faster, as it is instanced (see query manual).

  • C#

    // Query iteration (Each)
    q.Each((ref Position p, ref Velocity v) => { /* ... */ });
    // System iteration (Each)
    world.Routine<Position, Velocity>("Move")
    .Each((ref Position p, ref Velocity v) => { /* ... */ });
    // Query iteration (Iter)
    q.Iter((Iter it, Field<Position> p, Field<Velocity> v) =>
    {
    foreach (int i in it)
    {
    p[i].X += v[i].X;
    p[i].Y += v[i].Y;
    }
    });
    // System iteration (Iter)
    world.Routine<Position, Velocity>("Move")
    .Iter((Iter it, Field<Position> p, Field<Velocity> v) =>
    {
    foreach (int i in it)
    {
    p[i].X += v[i].X;
    p[i].Y += v[i].Y;
    }
    });

    The Iter function can be invoked multiple times per frame, once for each matched table. The Each function is called once per matched entity.

Note how query iteration has an outer and an inner loop, whereas system iteration only has the inner loop. The outer loop for systems is iterated by the ecs_run function, which invokes the system function. When running a pipeline, this means that a system callback can be invoked multiple times per frame, once for each matched table.

Using delta_time

A system provides a delta_time which contains the time passed since the last frame:

  • C

    Position *p = ecs_field(it, Position, 1);
    Velocity *v = ecs_field(it, Velocity, 2);
    for (int i = 0; i < it->count, i++) {
    p[i].x += v[i].x * it->delta_time;
    p[i].y += v[i].y * it->delta_time;
    }
  • C++

    world.system<Position, const Velocity>("Move")
    .each([](flecs::iter& it, size_t, Position& p, const Velocity &v) {
    p.x += v.x * it.delta_time();
    p.y += v.y * it.delta_time();
    });
    world.system<Position, const Velocity>("Move")
    .iter([](flecs::iter& it, Position *p, const Velocity *v) {
    for (auto i : it) {
    p[i].x += v[i].x * it.delta_time();
    p[i].y += v[i].y * it.delta_time();
    }
    });
  • C#

    world.Routine<Position, Velocity>("Move")
    .Each((Iter it, int i, ref Position p, ref Velocity v) =>
    {
    p.X += v.X * it.DeltaTime();
    p.Y += v.Y * it.DeltaTime();
    });
    world.Routine<Position, Velocity>("Move")
    .Iter((Iter it, Field<Position> p, Field<Velocity> v) =>
    {
    foreach (int i in it)
    {
    p[i].X += v[i].X * it.DeltaTime();
    p[i].Y += v[i].Y * it.DeltaTime();
    }
    });

This is the value passed into ecs_progress:

  • C

    ecs_progress(world, delta_time);
  • C++

    world.progress(delta_time);
  • C#

    world.Progress(deltaTime);

Passing a value for delta_time is useful if you're running progress() from within an existing game loop that already has time management. Providing 0 for the argument, or omitting it in the C++ API will cause progress() to measure the time since the last call and use that as value for delta_time:

  • C

    ecs_progress(world, 0);
  • C++

    world.progress();
  • C#

    world.Progress();

A system may also use delta_system_time, which is the time elapsed since the last time the system was invoked. This can be useful when a system is not invoked each frame, for example when using a timer.

Tasks

A task is a system that matches no entities. Tasks are ran once per frame, and are useful for running code that is not related to entities. An example of a task system:

  • C

    // System function
    void PrintTime(ecs_iter_t *it) {
    printf("Time: %f\n", it->delta_time);
    }
    // System declaration using the ECS_SYSTEM macro
    ECS_SYSTEM(world, PrintTime, EcsOnUpdate, 0);
    // System declaration using the descriptor API
    ecs_system(world, {
    .entity = ecs_entity(world, {
    .name = "PrintTime",
    .add = { ecs_dependson(EcsOnUpdate) }
    }),
    .callback = PrintTime
    });
    // Runs PrintTime
    ecs_progress(world, 0);
  • C++

    world.system("PrintTime")
    .kind(flecs::OnUpdate)
    .iter([](flecs::iter& it) {
    printf("Time: %f\n", it.delta_time());
    });
    // Runs PrintTime
    world.progress();
  • C#

    world.Routine("PrintTime")
    .Kind(Ecs.OnUpdate)
    .Iter((Iter it) =>
    {
    Console.WriteLine($"Time: {it.DeltaTime()}");
    });
    // Runs PrintTime
    world.progress();

Tasks may query for components from a fixed source or singleton:

  • C

    // System function
    void PrintTime(ecs_iter_t *it) {
    // Get singleton component
    Game *g = ecs_field(it, Game, 1);
    printf("Time: %f\n", g->time);
    }
    // System declaration using the ECS_SYSTEM macro
    ECS_SYSTEM(world, PrintTime, EcsOnUpdate, Game($));
    // System declaration using the descriptor API
    ecs_system(world, {
    .entity = ecs_entity(world, {
    .name = "PrintTime",
    .add = { ecs_dependson(EcsOnUpdate) }
    }),
    .query.filter.terms = {
    { .id = ecs_id(Game), .src.id = ecs_id(Game) } // Singleton source
    },
    .callback = PrintTime
    });
  • C++

    world.system<Game>("PrintTime")
    .term_at(1).singleton()
    .kind(flecs::OnUpdate)
    .iter([](flecs::iter& it, Game *g) {
    printf("Time: %f\n", g->time);
    });
  • C#

    world.Routine<Game>("PrintTime")
    .TermAt(1).Singleton()
    .Kind(Ecs.OnUpdate)
    .Each((ref Game g) =>
    {
    Console.WriteLine($"Time: {g.Time}");
    });

Note that it->count is always 0 for tasks, as they don't match any entities. In C++ this means that the function passed to each is never invoked for tasks. Applications will have to use iter instead when using tasks in C++.

Pipelines

A pipeline is a list of systems that is executed when the ecs_progress/world::progress function is invoked. Which systems are part of the pipeline is determined by a pipeline query. A pipeline query is a regular ECS query, which matches system entities. Flecs has a builtin pipeline with a predefined query, in addition to offering the ability to specify a custom pipeline query.

A pipeline by default orders systems by their entity id, to ensure deterministic order. This generally means that systems will be ran in the order they are declared, as entity ids are monotonically increasing. Note that this is not guaranteed: when an application deletes entities before creating a system, the system can receive a recycled id, which means it could be lower than the last issued id. For this reason it is recommended to prevent entity deletion while registering systems. When this can't be avoided, an application can create a custom pipeline with a user-defined order_by function (see custom pipeline).

Pipelines may utilize additional query mechanisms for ordering, such as cascade or group_by.

In addition to a system query, pipelines also analyze the components that systems are reading and writing to determine where to insert sync points. During a sync point enqueued commands are ran, which ensures that systems after a sync point can see all mutations from before a sync point.

Builtin Pipeline

The builtin pipeline matches systems that depend on a phase. A phase is any entity with the EcsPhase/flecs::Phase tag. To add a dependency on a phase, the DependsOn relationship is used. This happens automatically when using the ECS_SYSTEM macro/flecs::system::kind method:

  • C

    // System is created with (DependsOn, OnUpdate)
    ECS_SYSTEM(world, Move, EcsOnUpdate, Position, Velocity);
  • C++

    // System is created with (DependsOn, OnUpdate)
    world.system<Position, Velocity>("Move")
    .kind(flecs::OnUpdate)
    .each([](Position& p, Velocity& v) {
    // ...
    });
  • C#

    // System is created with (DependsOn, OnUpdate)
    world.Routine<Position, Velocity>("Move")
    .Kind(Ecs.OnUpdate)
    .Each((ref Position p, ref Velocity v) =>
    {
    // ...
    });

Systems are ordered using a topology sort on the DependsOn relationship. Systems higher up in the topology are ran first. In the following example the order of systems is InputSystem, MoveSystem, CollisionSystem:

PreUpdate
/ \
InputSystem OnUpdate
/ \
MoveSystem PostUpdate
/
CollisionSystem

Flecs has the following builtin phases, listed in topology order:

  • C

    • EcsOnStart
    • EcsOnLoad
    • EcsPostLoad
    • EcsPreUpdate
    • EcsOnUpdate
    • EcsOnValidate
    • EcsPostUpdate
    • EcsPreStore
    • EcsOnStore
  • C++

    • flecs::OnStart
    • flecs::OnLoad
    • flecs::PostLoad
    • flecs::PreUpdate
    • flecs::OnUpdate
    • flecs::OnValidate
    • flecs::PostUpdate
    • flecs::PreStore
    • flecs::OnStore
  • C#

    • Ecs.OnStart
    • Ecs.OnLoad
    • Ecs.PostLoad
    • Ecs.PreUpdate
    • Ecs.OnUpdate
    • Ecs.OnValidate
    • Ecs.PostUpdate
    • Ecs.PreStore
    • Ecs.OnStore

The EcsOnStart/flecs::OnStart phase is a special phase that is only ran the first time progress() is called.

An application can create custom phases, which can be (but don't need to be) branched off of existing ones:

// Phases must have the EcsPhase tag
ecs_entity_t Physics = ecs_new_w_id(ecs, EcsPhase);
ecs_entity_t Collisions = ecs_new_w_id(ecs, EcsPhase);
// Phases can (but don't have to) depend on other phases which forces ordering
ecs_add_pair(ecs, Physics, EcsDependsOn, EcsOnUpdate);
ecs_add_pair(ecs, Collisions, EcsDependsOn, Physics);
// Custom phases can be used just like regular phases
ECS_SYSTEM(world, Collide, Collisions, Position, Velocity);
const ecs_entity_t EcsDependsOn
Used to express dependency relationships.
ecs_entity_t ecs_new_w_id(ecs_world_t *world, ecs_id_t id)
Create new entity with (component) id.

Builtin Pipeline Query

The builtin pipeline query looks like this:

  • C

    .query.filter.terms = {
    { .id = EcsSystem },
    { .id = EcsPhase, .src.flags = EcsCascade, .src.trav = EcsDependsOn },
    { .id = EcsDisabled, .src.flags = EcsUp, .src.trav = EcsDependsOn, .oper = EcsNot },
    { .id = EcsDisabled, .src.flags = EcsUp, .src.trav = EcsChildOf, .oper = EcsNot }
    }
    });
    const ecs_entity_t EcsChildOf
    Used to express parent-child relationships.
    const ecs_entity_t EcsDisabled
    When this tag is added to an entity it is skipped by queries, unless EcsDisabled is explicitly querie...
    FLECS_API ecs_entity_t ecs_pipeline_init(ecs_world_t *world, const ecs_pipeline_desc_t *desc)
    Create a custom pipeline.
    #define EcsCascade
    Sort results breadth first.
    Definition flecs.h:711
    #define EcsUp
    Match by traversing upwards.
    Definition flecs.h:708
    @ EcsNot
    The term must not match.
    Definition flecs.h:699
  • C++

    world.pipeline()
    .with(flecs::System)
    .with(flecs::Phase).cascade(flecs::DependsOn)
    .without(flecs::Disabled).up(flecs::DependsOn)
    .without(flecs::Disabled).up(flecs::ChildOf)
    .build();
  • Query DSL

    flecs.system.System, Phase(cascade(DependsOn)), !Disabled(up(DependsOn)), !Disabled(up(ChildOf))
  • C#

    world.Pipeline()
    .With(Ecs.System)
    .With(Ecs.Phase).Cascade(Ecs.DependsOn)
    .Without(Ecs.Disabled).Up(Ecs.DependsOn)
    .Without(Ecs.Disabled).Up(Ecs.ChildOf)
    .Build();

Custom pipeline

Applications can create their own pipelines which fully customize which systems are matched, and in which order they are executed. Custom pipelines can use phases and DependsOn, or they may use a completely different approach. This example shows how to create a pipeline that matches all systems with the Foo tag:

  • C

    ECS_TAG(world, Foo);
    // Create custom pipeline
    .query.filter.terms = {
    { .id = EcsSystem }, // mandatory
    { .id = Foo }
    }
    });
    // Configure the world to use the custom pipeline
    ecs_set_pipeline(world, pipeline);
    // Create system
    ECS_SYSTEM(world, Move, Foo, Position, Velocity);
    // Runs the pipeline & system
    ecs_progress(world, 0);
    FLECS_API void ecs_set_pipeline(ecs_world_t *world, ecs_entity_t pipeline)
    Set a custom pipeline.
    #define ECS_TAG(world, id)
    Declare & define a tag.
    Definition flecs_c.h:86
  • C++

    // Create custom pipeline
    flecs::entity pipeline = world.pipeline()
    .with(flecs::System)
    .with(Foo) // or .with<Foo>() if a type
    .build();
    // Configure the world to use the custom pipeline
    world.set_pipeline(pipeline);
    // Create system
    auto move = world.system<Position, Velocity>("Move")
    .kind(Foo) // or .kind<Foo>() if a type
    .each(...);
    // Runs the pipeline & system
    world.progress();
    Self & with(const Func &func)
    Entities created in function will have the current entity.
    Definition builder.hpp:906
    Entity.
    Definition entity.hpp:30
  • C#

    // Create custom pipeline
    Pipeline pipeline = world.Pipeline()
    .With(Ecs.System)
    .With(foo) // or .With<Foo>() if a type
    .Build();
    // Configure the world to use the custom pipeline
    world.SetPipeline(pipeline);
    // Create system
    Routine move = world.Routine<Position, Velocity>("Move")
    .Kind(foo) // or .Kind<Foo>() if a type
    .Each(...);
    // Runs the pipeline & system
    world.Progress();

Note that ECS_SYSTEM kind parameter/flecs::system::kind add the provided entity both by itself as well as with a DependsOn relationship. As a result, the above Move system ends up with both:

  • Foo
  • (DependsOn, Foo)

This allows applications to still use the macro/builder API with custom pipelines, even if the custom pipeline does not use the DependsOn relationship. To avoid adding the DependsOn relationship, 0 can be passed to ECS_SYSTEM or flecs::system::kind followed by adding the tag manually:

  • C

    ecs_add(world, Move, Foo);
  • C++

    move.add(Foo);
  • C#

    move.Entity.Add(foo);

Pipeline switching performance

When running a multithreaded application, switching pipelines can be an expensive operation. The reason for this is that it requires tearing down and recreating the worker threads with the new pipeline context. For this reason it can be more efficient to use queries that allow for enabling/disabling groups of systems vs. switching pipelines.

For example, the builtin pipeline excludes groups of systems from the schedule that:

  • have the Disabled tag
  • have a parent (module) with the Disabled tag
  • depend on a phase with the Disabled tag

Disabling systems

Because pipelines use regular ECS queries, adding the EcsDisabled/flecs::Disabled tag to a system entity will exclude the system from the pipeline. An application can use the ecs_enable function or entity::enable/entity::disable methods to enable/disable a system:

  • C

    // Disable system in C
    ecs_enable(world, Move, false);
    // Enable system in C
    ecs_enable(world, Move, true);
    void ecs_enable(ecs_world_t *world, ecs_entity_t entity, bool enabled)
    Enable or disable entity.
  • C++

    // Disable system in C++
    s.disable();
    // Enable system in C++
    s.enable();
  • C#

    // Disable system in C#
    s.Entity.Disable();
    // Enable system in C#
    s.Entity.Enable();

Additionally the EcsDisabled/flecs::Disabled tag can be added/removed directly:

  • C

    ecs_add_id(world, Move, EcsDisabled);
    void ecs_add_id(ecs_world_t *world, ecs_entity_t entity, ecs_id_t id)
    Add a (component) id to an entity.
  • C++

    s.add(flecs::Disabled);
  • C#

    s.Entity.Add(Ecs.Disabled);

Note that this applies both to builtin pipelines and custom pipelines, as entities with the Disabled tag are ignored by default by queries.

Phases can also be disabled when using the builtin pipeline, which excludes all systems that depend on the phase. Note that is transitive, if PhaseB depends on PhaseA and PhaseA is disabled, systems that depend on both PhaseA and PhaseB will be excluded from the pipeline. For this reason, the builtin phases don't directly depend on each other, so that disabling EcsOnUpdate does not exclude systems that depend on EcsPostUpdate.

When the parent of a system is disabled, it will also be excluded from the builtin pipeline. This makes it possible to disable all systems in a module with a single operation.

Staging

When calling progress() the world enters a readonly state in which all ECS operations like add, remove, set etc. are enqueued as commands (called "staging"). This makes sure that it is safe for systems to iterate component arrays while enqueueing operations. Without staging, component storage arrays could be reallocated to a different memory location, which could cause system code to crash. Additionally, enqueueing operations makes it safe for multiple threads to iterate the same world without taking locks as thread gets its own command queue.

In general the framework tries its best to make sure that running code inside a system doesn't have different results than running it outside of a system, but because operations are enqueued as commands, this is not always the case. For example, the following code would return true outside of a system, but false inside of a system:

if (!e.has<Tag>()) {
e.add<Tag>();
return e.has<Tag>();
}

Note that commands are only enqueued for ECS operations like add, remove, set etc. Reading or writing a queried for component directly does not enqueue commands. As a rule of thumb, anything that does not require calling an ECS function/method does not enqueue a command.

There are a number of things applications can do to force merging of operations, or to prevent operations from being enqueued as commands. To decide which mechanism to use, an application has to decide whether it needs:

  1. Commands to be merged before another system
  2. Operations not to be enqueued as commands.

The mechanisms to accomplish this are sync points for 1), and no_readonly systems for 2).

Sync points

Sync points are moments during the frame where all commands are flushed to the storage. Systems that run after a sync point will be able to see all operations that happened up until the sync point. Sync points are inserted automatically by analyzing which commands could have been inserted and which components are being read by systems.

Because Flecs can't see inside the implementation of a system, pipelines can't know for which components a system could insert commands. This means that by default a pipeline assumes that systems insert no commands / that it is OK for commands to be merged at the end of the frame. To get commands to merge sooner, systems must be annotated with the components they write.

A pipeline tracks on a per-component basis whether commands could have been inserted for it, and when a component is being read. When a pipeline sees a read for a component for which commands could have been inserted, a sync point is inserted before the system that reads. This ensures that sync points are only inserted when necessary:

  • Multiple systems that enqueue commands can run before a sync point, possibly combining commands for multiple reads
  • When a system is inactive (e.g. it doesn't match any entities) or is disabled, it will be ignored for sync point insertion
  • Different combinations of modules have different sync requirements, automatic sync point insertion ensures that sync points are only inserted for the set of systems that are imported and are active.

To make the scheduler aware that a system can enqueue commands for a component, use the out modifier in combination with matching a component on an empty entity (0). This tells the scheduler that even though a system is not matching with the component, it is still "writing" it:

  • C

    // The '()' means, don't match this component on an entity, while `[out]` indicates
    // that the component is being written. This is interpreted by pipelines as a
    // system that can potentially enqueue commands for the Transform component.
    ECS_SYSTEM(world, SetTransform, EcsOnUpdate, Position, [out] Transform());
    // When using the descriptor API for creating the system, set the EcsIsEntity
    // flag while leaving the id field to 0. This is equivalent to doing `()` in the DSL.
    ecs_system(world, {
    .query.filter.terms = {
    { ecs_id(Position) },
    {
    .inout = EcsOut,
    .id = ecs_id(Transform),
    .src.flags = EcsIsEntity,
    .src.id = 0 /* Default value */
    }
    },
    /* ... */
    });
    #define EcsIsEntity
    Term id is an entity.
    Definition flecs.h:715
    @ EcsOut
    Term is only written.
    Definition flecs.h:692
  • C++

    // In the C++ API, use the write method to indicate commands could be inserted.
    world.system<Position>()
    .write<Transform>()
    .each( /* ... */);
  • C#

    // In the C# API, use the write method to indicate commands could be inserted.
    world.Routine<Position>()
    .Write<Transform>()
    .Each( /* ... */);

This will cause insertion of a sync point before the next system that reads Transform. Similarly, a system can also be annotated with reading a component that it doesn't match with, which is useful when a system calls get:

  • C

    ECS_SYSTEM(world, SetTransform, EcsOnUpdate, Position, [in] Transform());
    ecs_system(world, {
    .query.filter.terms = {
    { ecs_id(Position) },
    {
    .inout = EcsIn,
    .id = ecs_id(Transform),
    .src.flags = EcsIsEntity,
    .src.id = 0 /* Default value */
    }
    },
    /* ... */
    });
  • C++

    // In the C++ API, use the read method to indicate a component is read using .get
    world.system<Position>()
    .read<Transform>()
    .each( /* ... */);
  • C#

    // In the C# API, use the read method to indicate a component is read using .Get
    world.Routine<Position>()
    .Read<Transform>()
    .Each( /* ... */);

No readonly systems

By default systems are ran while the world is in "readonly" mode, where all ECS operations are enqueued as commands. Note that readonly mode only applies to "structural" changes, such as changing the components of an entity or other operations that mutate ECS data structures. Systems can still write component values while in readonly mode.

In some cases however, operations need to be immediately visible to a system. A typical example is a system that assigns tasks to resources, like assigning plates to a waiter. A system should only assign plates to a waiter that hasn't been assigned any plates yet, but to know which waiters are free, the operation that assigns a plate to a waiter must be immediately visible.

To accomplish this, systems can be marked with the no_readonly flag, which signals that a system should be ran while the world is not in readonly mode. This causes ECS operations to not get enqueued, and allows the system to directly see the results of operations. There are a few limitations to no_readonly systems:

  • no_readonly systems are always single threaded
  • operations on the iterated over entity must still be deferred

The reason for the latter limitation is that allowing for operations on the iterated over entity would cause the system to modify the storage it is iterating, which could cause undefined behavior similar to what you'd see when changing a vector while iterating it.

The following example shows how to create a no_readonly system:

  • C

    ecs_system(ecs, {
    .entity = ecs_entity(ecs, {
    .name = "AssignPlate",
    .add = { ecs_dependson(EcsOnUpdate) }
    }),
    .query.filter.terms = {
    { .id = Plate },
    },
    .callback = AssignPlate,
    .no_readonly = true // disable readonly mode for this system
    });
  • C++

    ecs.system("AssignPlate")
    .with<Plate>()
    .no_readonly() // disable readonly mode for this system
    .iter([&](flecs::iter& it) { /* ... */ })
  • C#

    ecs.Routine("AssignPlate")
    .With<Plate>()
    .NoReadonly() // disable readonly mode for this system
    .Iter((Iter it) => { /* ... */ })

This ensures the world is not in readonly mode when the system is ran. Operations are however still enqueued as commands, which ensures that the system can enqueue commands for the entity that is being iterated over. To prevent commands from being enqueued, a system needs to suspend and resume command enqueueing. This is an extra step, but makes it possible for a system to both enqueue commands for the iterated over entity, as well as do operations that are immediately visible. An example:

  • C

    void AssignPlate(ecs_iter_t *it) {
    for (int i = 0; i < it->count; i ++) {
    // ECS operations ran here are visible after running the system
    ecs_defer_suspend(it->world);
    // ECS operations ran here are immediately visible
    ecs_defer_resume(it->world);
    // ECS operations ran here are visible after running the system
    }
    }
    void ecs_defer_resume(ecs_world_t *world)
    Resume deferring.
    void ecs_defer_suspend(ecs_world_t *world)
    Suspend deferring but do not flush queue.
  • C++

    .iter([](flecs::iter& it) {
    for (auto i : it) {
    // ECS operations ran here are visible after running the system
    it.world().defer_suspend();
    // ECS operations ran here are immediately visible
    it.world().defer_resume();
    // ECS operations ran here are visible after running the system
    }
    });
    void defer_suspend() const
    Suspend deferring operations.
    Definition world.hpp:989
    void defer_resume() const
    Resume deferring operations.
    Definition world.hpp:997
  • C#

    .Iter((Iter it) =>
    {
    foreach (int i in it)
    {
    // ECS operations ran here are visible after running the system
    it.World().DeferSuspend();
    // ECS operations ran here are immediately visible
    it.World().DeferResume();
    // ECS operations ran here are visible after running the system
    }
    });

Note that defer_suspend and defer_resume may only be called from within a no_readonly system.

Threading

Systems in Flecs can be multithreaded. This requires both the system to be created as a multithreaded system, as well as configuring the world to have a number of worker threads. To create worker threads, use the set_threads function:

  • C

    ecs_set_threads(world, 4); // Create 4 worker threads
    FLECS_API void ecs_set_threads(ecs_world_t *world, int32_t threads)
    Set number of worker threads.
  • C++

    world.set_threads(4);
  • C#

    world.SetThreads(4);

To create a multithreaded system, use the multi_threaded flag:

  • C

    ecs_system(ecs, {
    .entity = ecs_entity(ecs, {
    .add = { ecs_dependson(EcsOnUpdate) }
    }),
    .query.filter.terms = {
    { .id = ecs_id(Position) }
    },
    .callback = Dummy,
    .multi_threaded = true // run system on multiple threads
    });
  • C++

    world.system<Position>()
    .multi_threaded()
    .each( /* ... */ );
  • C#

    world.Routine<Position>()
    .MultiThreaded()
    .Each( /* ... */ );

By default systems are created as single threaded. Single threaded systems are always ran on the main thread. Multithreaded systems are ran on all worker threads. The scheduler runs each multithreaded system on all threads, and divides the number of matched entities across the threads. The way entities are allocated to threads ensures that the same entity is always processed by the same thread, until the next sync point. This means that in an ideal case, all systems in a frame can run until completion without having to synchronize.

The way the scheduler ensures that the same entities are processed by the same threads is by slicing up the entities in a table into N slices, where N is the number of threads. For a table that has 1000 entities, the first thread will process entities 0..249, thread 2 250..499, thread 3 500..749 and thread 4 entities 750..999. For more details on this behavior, see ecs_worker_iter/flecs::iterable::worker_iter.

Threading with Async Tasks

Systems in Flecs can also be multithreaded using an external asynchronous task system. Instead of creating regular worker threads using set_threads, use the set_task_threads function and provide the OS API callbacks to create and wait for task completion using your job system. This can be helpful when using Flecs within an application which already has a job queue system to handle multithreaded tasks.

  • C

    ecs_set_task_threads(world, 4); // Create 4 worker task threads for the duration of each ecs_progress update
    FLECS_API void ecs_set_task_threads(ecs_world_t *world, int32_t task_threads)
    Set number of worker task threads.
  • C++

    world.set_task_threads(4);
  • C#

    world.SetTaskThreads(4);

For simplicity, these task callbacks use the same format as Flecs ecs_os_api_t thread APIs. In fact, you could provide your regular os thread api functions to create short-duration threads for multithreaded system processing. Create multithreaded systems using the multi_threaded flag as with ecs_set_threads above.

When ecs_progress is called, your ecs_os_api.task_new_ callback will be called once for every task thread needed to create task threads on demand. When ecs_progress is complete, your ecs_os_api.task_join_ function will be called to clean up each task thread. By providing callback functions which create and remove tasks for your specific asynchronous task system, you can use Flecs with any kind of async task management scheme. The only limitation is that your async task manager must be able to create and execute the number of simultaneous tasks specified in ecs_set_task_threads and must exist for the duration of ecs_progress.

Timers

When running a pipeline, systems are ran each time progress() is called. The FLECS_TIMER addon makes it possible to run systems at a specific time interval or rate.

Interval

The following example shows how to run systems at a time interval:

  • C

    // Using the ECS_SYSTEM macro
    ECS_SYSTEM(world, Move, EcsOnUpdate, Position, [in] Velocity);
    ecs_set_interval(world, ecs_id(Move), 1.0); // Run at 1Hz
    FLECS_API ecs_entity_t ecs_set_interval(ecs_world_t *world, ecs_entity_t tick_source, ecs_ftime_t interval)
    Set timer interval.
    // Using the ecs_system_init function/ecs_system macro:
    ecs_system(world, {
    .entity = ecs_entity(world, {
    .name = "Move",
    .add = { ecs_dependson(EcsOnUpdate) }
    }),
    .query.filter.terms = {
    { ecs_id(Position) },
    { ecs_id(Velocity), .inout = EcsIn }
    },
    .callback = Move,
    .interval = 1.0 // Run at 1Hz
    });
  • C++

    world.system<Position, const Velocity>()
    .interval(1.0) // Run at 1Hz
    .each(...);
  • C#

    world.Routine<Position, Velocity>()
    .Interval(1.0f) // Run at 1Hz
    .Each(...);

Rate

The following example shows how to run systems every Nth tick with a rate filter:

  • C

    // Using the ECS_SYSTEM macro
    ECS_SYSTEM(world, Move, EcsOnUpdate, Position, [in] Velocity);
    ecs_set_rate(world, ecs_id(Move), 2); // Run every other frame
    FLECS_API ecs_entity_t ecs_set_rate(ecs_world_t *world, ecs_entity_t tick_source, int32_t rate, ecs_entity_t source)
    Set rate filter.
    // Using the ecs_system_init function/ecs_system macro:
    ecs_system(world, {
    .entity = ecs_entity(world, {
    .name = "Move",
    .add = { ecs_dependson(EcsOnUpdate) }
    }),
    .query.filter.terms = {
    { ecs_id(Position) },
    { ecs_id(Velocity), .inout = EcsIn }
    },
    .callback = Move,
    .rate = 2.0 // Run every other frame
    });
  • C++

    world.system<Position, const Velocity>()
    .rate(2) // Run every other frame
    .each(...);
  • C#

    world.Routine<Position, Velocity>()
    .Rate(2) // Run every other frame
    .Each(...);

Tick source

Instead of setting the interval or rate directly on the system, an application may also create a separate entity that holds the time or rate filter, and use that as a "tick source" for a system. This makes it easier to use the same settings across groups of systems:

  • C

    // Using the ECS_SYSTEM macro
    ECS_SYSTEM(world, Move, EcsOnUpdate, Position, [in] Velocity);
    // Passing 0 to entity argument will create a new entity. Similarly a rate
    // filter entity could be created with ecs_set_rate(world, 0, 2)
    ecs_entity_t tick_source = ecs_set_interval(world, 0, 1.0);
    // Set tick source for system
    ecs_set_tick_source(world, ecs_id(Move), tick_source);
    FLECS_API void ecs_set_tick_source(ecs_world_t *world, ecs_entity_t system, ecs_entity_t tick_source)
    Assign tick source to system.
    // Passing 0 to entity argument will create a new entity. Similarly a rate
    // filter entity could be created with ecs_set_rate(world, 0, 2)
    ecs_entity_t tick_source = ecs_set_interval(world, 0, 1.0);
    // Using the ecs_system_init function/ecs_system macro:
    ecs_system(world, {
    .entity = ecs_entity(world, {
    .name = "Move",
    .add = { ecs_dependson(EcsOnUpdate) }
    }),
    .query.filter.terms = {
    { ecs_id(Position) },
    { ecs_id(Velocity), .inout = EcsIn }
    },
    .callback = Move,
    .tick_source = tick_source // Set tick source for system
    });
  • C++

    // A rate filter can be created with .rate(2)
    flecs::entity tick_source = world.timer()
    .interval(1.0);
    world.system<Position, const Velocity>()
    .tick_source(tick_source) // Set tick source for system
    .each(...);
    void each(const Func &func) const
    Iterate (component) ids of an entity.
    Definition impl.hpp:117
  • C#

    // A rate filter can be created with .Rate(2)
    TimerEntity tickSource = world.Timer()
    .Interval(1.0f);
    world.Routine<Position, Velocity>()
    .TickSource(tickSource) // Set tick source for system
    .Each(...);

Interval filters can be paused and resumed:

  • C

    // Pause timer
    ecs_stop_timer(world, tick_source);
    // Resume timer
    ecs_start_timer(world, tick_source);
    FLECS_API void ecs_start_timer(ecs_world_t *world, ecs_entity_t tick_source)
    Start timer.
    FLECS_API void ecs_stop_timer(ecs_world_t *world, ecs_entity_t tick_source)
    Stop timer This operation stops a timer from triggering.
  • C++

    // Pause timer
    tick_source.stop();
    // Resume timer
    tick_source.start();
  • C#

    // Pause timer
    tickSource.Stop();
    // Resume timer
    tickSource.Start();

An additional advantage of using shared interval/rate filter between systems is that it guarantees that systems are ran at the same tick. When a system is disabled, its interval/rate filters aren't updated, which means that when the system is reenabled again it would be out of sync with other systems that had the same interval/rate specified. When using a shared tick source however the system is guaranteed to run at the same tick as other systems with the same tick source, even after the system is reenabled.

Nested tick sources

One tick source can be used as the input of another (rate) tick source. The rate tick source will run at each Nth tick of the input tick source. This can be used to create nested tick sources, like in the following example:

  • C

    // Tick at 1Hz
    ecs_entity_t each_second = ecs_set_interval(world, 0, 1.0);
    // Tick each minute
    ecs_entity_t each_minute = ecs_set_rate(world, 0, 60);
    ecs_set_tick_source(world, each_minute, each_second);
    // Tick each hour
    ecs_entity_t each_hour = ecs_set_rate(world, 0, 60);
    ecs_set_tick_source(world, each_hour, each_minute);
  • C++

    // Tick at 1Hz
    flecs::entity each_second = world.timer()
    .interval(1.0);
    // Tick each minute
    flecs::entity each_minute = world.timer()
    .rate(60, each_second);
    // Tick each hour
    flecs::entity each_hour = world.timer()
    .rate(60, each_minute);
  • C#

    // Tick at 1Hz
    TimerEntity eachSecond = world.Timer()
    .Interval(1.0f);
    // Tick each minute
    TimerEntity eachMinute = world.Timer()
    .Rate(60, eachSecond);
    // Tick each hour
    TimerEntity eachHour = world.Timer()
    .Rate(60, eachMinute);

Systems can also act as each others tick source:

  • C

    // Tick at 1Hz
    ecs_entity_t each_second = ecs_system(world, {
    .entity = ecs_entity(world, {
    .name = "EachSecond",
    .add = { ecs_dependson(EcsOnUpdate) }
    }),
    .callback = Dummy,
    .interval = 1.0
    });
    // Tick each minute
    ecs_entity_t each_minute = ecs_system(world, {
    .entity = ecs_entity(world, {
    .name = "EachMinute",
    .add = { ecs_dependson(EcsOnUpdate) }
    }),
    .callback = Dummy,
    .tick_source = each_second,
    .rate = 60
    });
    // Tick each hour
    ecs_entity_t each_hour = ecs_system(world, {
    .entity = ecs_entity(world, {
    .name = "EachHour",
    .add = { ecs_dependson(EcsOnUpdate) }
    }),
    .callback = Dummy,
    .tick_source = each_minute,
    .rate = 60
    });
  • C++

    // Tick at 1Hz
    flecs::entity each_second = world.system("EachSecond")
    .interval(1.0)
    .iter([](flecs::iter& it) { /* ... */ });
    // Tick each minute
    flecs::entity each_minute = world.system("EachMinute")
    .tick_source(each_second)
    .rate(60)
    .iter([](flecs::iter& it) { /* ... */ });
    // Tick each hour
    flecs::entity each_hour = world.system("EachHour")
    .tick_source(each_minute)
    .rate(60)
    .iter([](flecs::iter& it) { /* ... */ });
  • C#

    // Tick at 1Hz
    Routine eachSecond = world.Routine("EachSecond")
    .Interval(1.0f)
    .Iter((Iter it) => { /* ... */ });
    // Tick each minute
    Routine eachMinute = world.Routine("EachMinute")
    .TickSource(eachSecond)
    .Rate(60)
    .Iter((Iter it) => { /* ... */ });
    // Tick each hour
    Routine eachHour = world.Routine("EachHour")
    .TickSource(eachMinute)
    .Rate(60)
    .Iter((Iter it) => { /* ... */ });