Skip to content

Latest commit

 

History

History
806 lines (596 loc) · 23.9 KB

File metadata and controls

806 lines (596 loc) · 23.9 KB

Flecs Quickstart

This document provides a quick overview of Flecs features.

World

The world is the container for all of your ECS data. An application can have multiple worlds. To create & delete a world, simply do:

ecs_world_t *world = ecs_init();

/* Application */

ecs_fini(world);
flecs::world world();

/* Application */

Entities

An entity (ecs_entity_t) is a 64-bit integer that uniquely identifies a thing or object in your application. Entities are created like this:

ecs_entity_t e = ecs_new(world, 0);
auto e = flecs::entity(world);

You can also use plain numbers:

ecs_entity_t e = 1000;
auto e = flecs::entity(world, 1000);

When not using an explicit id, the framework guarantees that the returned id is not in use.

Components

A component is a plain datatype that can be attached to an entity. An entity can contain any number of components. Components must be registered with the world like this:

// Components can be defined from regular types
typedef struct Position {
    float x, y;
} Position;

int main() {
    ecs_world_t *world = ecs_init();

    // Register the component with the world
    ECS_COMPONENT(world, Position);
}
// Components can be defined from regular types
struct Position {
    float x, y;
};

int main() {
    flecs::world world();

    // Register the component with the world
    flecs::component<Position>(world);
}

Once registered, a component can be added to an entity using add:

ecs_add(world, e, Position);
e.add<Position>();

You can also create an entity with a component already added:

ecs_entity_t e = ecs_new(world, Position);
auto e = flecs::entity(world).add<Position>();

An application can also use set to assign a value to the component. If the component was not added yet, set will add it implicitly:

ecs_set(world, e, Position, {10, 20});
e.set<Position>({10, 20});

The value of a component can be requested with ecs_get, which will return NULL if the entity does not have the component:

const Position *p = ecs_get(world, e, Position);

If you need a pointer that you can modify, use ecs_get_mut:

Position *p = ecs_get_mut(world, e, Position);

Components can be removed with ecs_remove:

ecs_remove(world, e, Position);

Tags

Tags are much like components, but they are not associated with a data type. Tags are typically used to add a flag to an entity, for example to indicate that an entity is an Enemy:

int main() {
    ecs_world_t *world = ecs_init();

    // Register the tag with the world. There is no Enemy type
    ECS_TAG(world, Enemy);
}

Once registered, you can now add & remove the tag just like a regular component:

// Add the Enemy tag
ecs_add(world, e, Enemy);

// Remove the Enemy tag
ecs_remove(world, e, Enemy);

Note that since a tag does not have data, you cannot use ecs_set or ecs_get.

Entity names

Entities can have an optional name. The easiest way to create a named entity is with ECS_ENTITY:

ECS_ENTITY(world, MyEntity, Position, Velocity);

This creates an entity called MyEntity with components Position, Velocity. To get the name of an entity, do:

const char *name = ecs_get_name(world, MyEntity);

Alternatively an application can set the name of an entity by using the EcsName component:

ecs_set(world, e, EcsName, {"MyName"});

Applications can lookup an entity by name with the ecs_lookup function:

ecs_entity_t e = ecs_lookup(world, "MyName");

Types

Types are vectors that contain multiple components. Each entity has a type which describes the components it has. To request the type of an entity, do:

ecs_type_t type = ecs_get_type(world, e);

There are many things you can do with a type. One useful thing you can do is print it:

char *str = ecs_type_str(world, type);
printf("type = %s\n", str);
free(str);

This will print something like:

Position,Velocity

You can add and remove types to entities in the same way that you can add and remove components. This lets you add/remove multiple components at the same time. One common way to do that is like this:

ECS_COMPONENT(world, Position);
ECS_COMPONENT(world, Velocity);
ECS_TYPE(world, MyType, Position, Velocity);

// Add both Position and Velocity to entity e
ecs_add(world, e, MyType);

Systems

Systems are functions that are matched with entities that have a specific set of components. A typical system is defined like this:

ECS_SYSTEM(world, Move, EcsOnUpdate, Position, Velocity);

Move is the system name and also the name of the system function, EcsOnUpdate indicates when the system runs (see Pipelines). Position, Velocity is the system signature, and indicates the components the system is subscribed for.

The implementation of this system could look like this:

void Move(ecs_iter_t *it) {
    Position *p = ecs_column(it, Position, 1);
    Velocity *v = ecs_column(it, Velocity, 2);

    for (int i = 0; i < it->count, i ++) {
        p[i].x += v[i].x;
        p[i].y += v[i].y;
    }
}

The it argument contains all the information the system needs to iterate the components. The ecs_column function returns a C array for the subscribed for component. The numbers 1 and 2 indicate where in the system signature the components can be found.

The system will be invoked by ecs_progress, which runs the main loop:

// Progress frame, exit loop when application should quit
while (ecs_progress(word, 0)) { }

The Move function will be invoked once per unique type that matched with the system. For example if a system matched with entities that have Position, Velocity and Position, Velocity, Mass, the function would be invoked twice.

Instancing

Components from a single entity can be reused across many entities by using instancing. Setting up instancing is similar to adding components and tags to an entity:

// Create a base entity with Position
ecs_entity_t base = ecs_new(world, Position);

// Create an instance of base. Position will be shared with instance
ecs_entity_t instance = ecs_new_w_entity(world, ECS_INSTANCEOF | base);

Note that instead of adding a component to an entity, we add an entity to an entity with the ecs_new_w_entity function. ECS_INSTANCEOF is the "role" of the entity, in this case we indicate that instance is an instance of base. We can now get Position from instance:

// Retrieves Position from base
const Position *p = ecs_get(world, instance, Position);

ecs_add_entity and ecs_remove_entity can be used to add and remove instancing relationships:

ecs_remove_entity(world, instance, ECS_INSTANCEOF | base);

Instances can override components, which copies the value from the base to the instance component:

// Create base with initialized Position component
ecs_entity_t base = ecs_new(world, 0);
ecs_set(world, base, Position, {10, 20});

// Create instance of base
ecs_entity_t instance = ecs_new_w_entity(world, ECS_INSTANCEOF | base);

// Override Position. Base value will be copied to instance
ecs_add(world, instance, Position);

// Get Position from instance, will be {10, 20}
const Position *p = ecs_get(world, e, Position);

Instance relationships and component overrides can be encoded in a type:

ECS_COMPONENT(world, Position);
ECS_ENTITY(world, base, Position);
ECS_TYPE(world, MyType, INSTANCEOF | base, Position);

// Create instance of base, with override for Position
ecs_entity_t instance = ecs_new(world, MyType);

Hierarchies

Entities can be created in a hierarchy. Setting up hierarchies is similar to adding components / tags to an entity:

// Create regular entity that we'll use as parent
ecs_entity_t parent = ecs_new(world, 0);

// Create child entity of parent
ecs_entity_t child = ecs_new_w_entity(world, ECS_CHILDOF | parent);

One useful application of hierarchies is CASCADE systems. CASCADE systems iterate entities ordered by depth, which can be used for (for example) transform systems:

ECS_SYSTEM(world, Transform, EcsOnUpdate, CASCADE:Position, Position);

Which could be implemented like this:

void Transform(ecs_iter_t *it) {
    // Get the parent component array
    Position *p_parent = ecs_column(it, Position, 1);
    
    // Get the entity component array
    Position *p = ecs_column(it, Position, 2);

    // The root doesn't have a parent, so check if it's NULL
    if (!p_parent)
        return;

    // Add parent position to entity position. Note that the parent
    // Position is not passed an array
    for (int i = 0; i < it->count; i ++) {
        p[i].x += p_parent->x;
        p[i].y += p_parent->y;
    }
}

Applications can iterate a hierarchy depth-first, using a tree iterator:

ECS_ENTITY(world, Parent, 0);
ECS_ENTITY(world, Child, CHILDOF | Parent);

ecs_iter_t it = ecs_scope_iter(world, Parent);

while (ecs_scope_next(&it)) {
    for (int i = 0; i < it.count; i ++) {
        printf("%s\n", it.entities[i]);
    }
}

It is possible to lookup entities by their full path, and to get the full path from an entity:

// Returns Child
ecs_entity_t e = ecs_lookup_fullpath(world, "Parent.Child");

// Returns "Parent.Child"
char *path = ecs_get_fullpath(world, e);

Prefabs

Hierachies and instancing, when combined, let applications create templates for entity hierarchies. Consider the following example of a hierarchy template:

// Create top-level template entity. The ECS_PREFAB macro creates a
// regular entity with the EcsPrefab tag, which hides it from systems.
ECS_PREFAB(world, Destroyer);

    // Create a child prefab. Children are instantiated for an instance
    // whenever their parent is instantiated
    ECS_PREFAB(world, FrontTurret, CHILDOF | Destroyer, Position);
        ecs_set(world, FrontTurret, Position, {-10, 0});

    // Create another child prefab
    ECS_PREFAB(world, BackTurret, CHILDOF | Destroyer, Position);
        ecs_set(world, BackTurret, Position, {10, 0});        

Because these entities are defined as prefabs, they will not be matched with systems. The following code instantiates the template hierarchy by using regular instancing:

// Instantiates the Destroyer base and its children. After this operation the instance will have two children, one for each turret.
ecs_entity_t e = ecs_new_w_entity(world, ECS_INSTANCEOF | Destroyer);

Traits

Traits are a special kind of component that is added to an entity,component tuple. Trait components can be useful for implementing functionality that is not specific to one component. A typical example is implementing a timer after which a component should be deleted. We can define the trait component type like this:

typedef struct ExpiryTimer {
    float expiry_time;
    float t;
} ExpiryTimer;

We then create a system that increases the value of t every frame until it matches or exceeds expiry_time, after which we will remove our component. Before looking at the system, let's first look at how we can add a trait to an entity:

ecs_entity_t e = ecs_new(world, 0);

// Add HealthBuff, set the ExpiryTimer trait for HealthBuff to 10 seconds
ecs_add(world, e, HealthBuff);
ecs_set_trait(world, e, HealthBuff, ExpiryTimer, {
    .expiry_time = 10
});

// Add StaminaBuff, set the ExpiryTimer trait for StaminaBuff to 5 seconds
ecs_set_trait(world, e, StaminaBuff, ExpiryTimer, {
    .expiry_time = 5
});

Now we need to write a system to increase the timer and execute the remove logic. The system definition looks almost like a regular system:

ECS_SYSTEM(world, ExpireComponents, EcsOnUpdate, TRAIT | ExpiryTimer);

Note that the ExpiryTimer has the TRAIT role. This lets the system know it should match this component as a trait, not as a regular component. Now lets look at the implementation of this system:

void ExpireComponents(ecs_iter_t *it) {
    /* Get the trait component just like a normal component */
    ExpiryTimer *et = ecs_column(it, ExpiryTimer, 1);

    /* Get the trait handle */
    ecs_entity_t trait = ecs_column_entity(it, 1);

    /* Obtain the component handlem, which is the lower 32 bits
     * of the trait handle, which can be obtained with the 
     * ecs_entity_t_lo macro. */
    ecs_entity_t comp = ecs_entity_t_lo(trait);

    /* Iterate trait component as usual ... */
    int32_t i;
    for (i = 0; i < it->count; i ++) {
        /* When timer hits expiry time, remove component */
        et[i].t += it->delta_time;
        if (et[i].t >= et[i].expiry_time) {
            /* Remove component */
            ecs_remove_entity(it->world, it->entities[i], comp);

            /* Removes trait, so system won't be invoked again */
            ecs_remove_entity(it->world, it->entities[i], trait);
        }
    }

Note that this system doesn't contain any code that is specific for the components to which the traits were added. This means this system can be applied to any component.

Switchable tags

Switchable tags are sets of regular tags that can be added to an entity, except that only one of the set can be active at the same time. This is particularly useful when storing state machines. Consider the following example:

/* Create a Movement switch machine with 3 cases */
ECS_TAG(world, Standing);
ECS_TAG(world, Walking);
ECS_TAG(world, Running);
ECS_TYPE(world, Movement, Standing, Walking, Running); 

/* Create a few entities with various state combinations */
ecs_entity_t e = ecs_new(world, 0);

/* Add the switch to the entity. This lets Flecs know that only one of the tags
 * in the Movement type may be active at the same time. */
ecs_add_entity(world, e, ECS_SWITCH | Movement);

/* Add the Standing case to the entity */
ecs_add_entity(world, e, ECS_CASE | Standing);

/* Add the Walking case to the entity. This removes Standing */
ecs_add_entity(world, e, ECS_CASE | Walking);

/* Add the Running case to the entity. This removes Walking */
ecs_add_entity(world, e, ECS_CASE | Running);

Switchable tags aren't just convenient, they are also very fast, as changing a case does not move the entity between archetypes like regular tags do. This makes switchable components particularly useful for fast-changing data, like states in a state machine. Systems can query for switchable tags by using the SWITCH and CASE roles:

/* Subscribe for all entities that are Walking, and have the switch Direction */
ECS_SYSTEM(world, Walk, EcsOnUpdate, CASE | Walking, SWITCH | Direction);

See the switch example for more details.

Queries

Queries are like systems in that they let applications iterate over entities, but without having to create a separate function. Systems use queries internally however, so their APIs are similar:

A query can be used like this:

// Create a query for all entities with Position, Velocity
ecs_query_t *query = ecs_query_new(world, "Position, Velocity");

// Create iterator for query
ecs_iter_t it = ecs_query_iter(query);

// Iterate all the matching archetypes
while (ecs_query_next(&it)) {
    // Get the component arrays
    Position *p = ecs_column(it, Position, 1);
    Velocity *v = ecs_column(it, Velocity, 2);

    // Iterate the entities in the archetype
    for (int i = 0; i < it->count, i ++) {
        p[i].x += v[i].x;
        p[i].y += v[i].y;
    }
}

Queries are registered with the world, and entities (types) are continuously matched with a query. This means that when an application iterates over a query, matching has already happened, which makes it very fast.

Monitors

A monitor is a special kind of system that is executed once when a condition becomes true. A monitor is created just like a regular system, but with the EcsMonitor tag:

ECS_SYSTEM(world, OnPV, EcsMonitor, Position, Velocity);

This example illustrates when the monitor is invoked:

// Condition is not true: monitor is not invoked
ecs_entity_t e = ecs_new(world, Position);

// Condition is true for the first time: monitor is invoked!
ecs_add(world, e, Velocity);

// Condition is still true: monitor is not invoked
ecs_add(world, e, Mass);

// Condition is no longer true: monitor is not invoked
ecs_remove(world, e, Position);

// Condition is true again: monitor is invoked!
ecs_add(world, e, Position);

Note that monitors are never invoked by ecs_progress.

An monitor is implemented the same way as a regular system:

void OnPV(ecs_iter_t *it) {
    Position *p = ecs_column(it, Position, 1);
    Velocity *v = ecs_column(it, Velocity, 2);

    for (int i = 0; i < it->count; i ++) {
        /* Monitor code. Note that components may not have
         * been initialized when the monitor is invoked */
    }
}

OnSet Systems

OnSet systems are ran whenever the value of one of the components the system subscribes for changes. An OnSet system is created just like a regular system, but with the EcsOnSet tag:

ECS_SYSTEM(world, OnSetPV, EcsOnSet, Position, Velocity);

This example illustrates when the monitor is invoked:

ecs_entity_t e = ecs_new(world, 0);

// The entity does not have Velocity, so system is not invoked
ecs_set(world, e, Position, {10, 20});

// The entity has both components, but Velocity is not set
ecs_add(world, e, Velocity);

// The entity has both components, so system is invoked!
ecs_set(world, e, Velocity, {1, 2});

// The entity has both components, so system is invoked!
ecs_set(world, e, Position, {11, 22});

An OnSet system is implemented the same way as a regular system:

void OnSetPV(ecs_iter_t *it) {
    Position *p = ecs_column(it, Position, 1);
    Velocity *v = ecs_column(it, Velocity, 2);

    for (int i = 0; i < it->count; i ++) {
        /* Trigger code */
    }
}

The opposite of an EcsOnSet system is an EcsUnSet system:

ECS_SYSTEM(world, UnSetP, EcsUnSet, Position);

An UnSet system is invoked when an entity no longer has a value for the specified component:

ecs_entity_t e = ecs_set(world, 0, Position, {10, 20});

// The UnSet system is invoked
ecs_remove(world, e, Position);

OnSet and UnSet systems are typically invoked when components are set and removed, but there are two edge cases:

  • A component is removed but the entity inherits a value for the component from a base entity. In this case OnSet is invoked, because the value for the component changed.
  • The entity does not have the component, but the base that has the component is removed. In this case UnSet is invoked, since the entity no longer has the component.

Triggers

Triggers are callbacks that are invoked when a component is added or removed to/from an entity. Triggers can only be defined on a single component:

ECS_TRIGGER(world, OnAddP, EcsOnAdd, Position);

This example illustrates when the trigger is invoked:

// Position is added, trigger is invoked
ecs_entity_t e = ecs_new(world, Position);

// Entity already has Position, trigger is not invoked
ecs_add(world, e, Position);

A trigger is implemented the same way as a system:

void OnAddP(ecs_iter_t *it) {
    Position *p = ecs_column(it, Position, 1);

    for (int i = 0; i < it->count; i ++) {
        /* Trigger code. Note that components may not have
         * been initialized when the trigger is invoked */
    }
}

Component Lifecycle

An application can register callbacks for when a component is constructed, destructed, copied or moved. This allows applications to manage resources and prevent memory leaks.

This is an example implementation for the component lifecycle callbacks:

typedef struct String {
    char *value;
} String;

// Component constructor
ECS_CTOR(String, ptr, {
    ptr->value = NULL;
});

// Component destructor
ECS_DTOR(String, ptr, {
    free(ptr->value);
});

// Component copy
ECS_COPY(String, dst, src, {
    free(dst->value);
    dst->value = strdup(src->value);
});

// Component move
ECS_MOVE(String, dst, src, {
    dst->value = src->value;
    src->value = NULL;
});

The component lifecycle callbacks can be registered like this:

ECS_COMPONENT(world, String);

ecs_set_component_actions(world, ecs_entity(String), 
    &(EcsComponentLifecycle){
        .ctor = ecs_ctor(String),
        .dtor = ecs_dtor(String),
        .copy = ecs_copy(String),
        .move = ecs_move(String)
    });

Modules

Modules are used to add organization to components and systems, and to enable reusability across multiple applications. Modules are typically imported at the start of an application like this:

// Import module called MyModule. This declares handles to the
// module contents in the current C scope.
ECS_IMPORT(world, MyModule);

Modules often have a header and a source file. This is an example module header:

// The module type that stores handles to the module contents
typedef struct MyModule {
    ECS_DECLARE_COMPONENT(world, Position);
    ECS_DECLARE_COMPONENT(world, Velocity);
    ECS_DECLARE_ENTITY(world, Move);
} MyModule;

// The function that is executed when the module is imported
void MyModuleImport(
    ecs_world_t *world);

// This macro declares the module handles in the current scope
#define MyModuleImportHandles(module)\
    ECS_IMPORT_COMPONENT(module, Position);\
    ECS_IMPORT_COMPONENT(module, Position);\
    ECS_IMPORT_ENTITY(module, Move);

The corresponding module source file looks like this:

void MyModuleImport(
    ecs_world_t *world)
{
    // Declare the module
    ECS_MODULE(world, MyModule);

    // Create the components as usual
    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    // Create the system as usual
    ECS_SYSTEM(world, Move, EcsOnUpdate, Position, Velocity);

    // Export components and system to module (this sets the
    // handles in the module type)
    ECS_EXPORT_COMPONENT(Position);
    ECS_EXPORT_COMPONENT(Velocity);
    ECS_EXPORT_ENTITY(Move);
}

Modules are namespaced, which means that the components and system identifiers within a module will be prefixed with the module name. The module name is translated from PascalCase into lowercase separated by dots, such that MyModule becomes my.module.

If an application wants to create a system that uses components from the module, it has to include the namespace in the signature, for example:

ECS_SYSTEM(world, Move, EcsOnUpdate, 
    my.module.Position, 
    my.module.Velocity);

This prevents modules from accidentally creating components or systems that clash with each other. If a module wants however, it can override the scope:

ECS_MODULE(world, MyModule);

// Set scope to root
ecs_entity_t old_scope = ecs_set_scope(world, 0);

// Define components as usual
ECS_COMPONENT(world, Position);
ECS_COMPONENT(world, Velocity);

// Change scope back to the module scope
ecs_set_scope(world, old_scope);

Pipelines

A pipeline defines the different phases that are executed for each frame. By default an application uses the builtin pipeline which has the following phases:

  • EcsOnLoad
  • EcsPostLoad
  • EcsPreUpdate
  • EcsOnUpdate
  • EcsOnValidate
  • EcsPostUpdate
  • EcsPreStore
  • EcsOnStore

These phases can be provided as an argument to the ECS_SYSTEM macro:

// System ran in the EcsOnUpdate phase
ECS_SYSTEM(world, Move, EcsOnUpdate, Position, Velocity);

// System ran in the EcsOnValidate phase
ECS_SYSTEM(world, DetectCollisions, EcsOnValidate, Position);

An application can create a custom pipeline, like is shown here:

// Create a tag for each phase in the custom pipeline.
// The tags must be created in the phase execution order.
ECS_TAG(world, BeforeFrame);
ECS_TAG(world, OnFrame);
ECS_TAG(world, AfterFrame);

// Create the pipeline
ECS_PIPELINE(world, MyPipeline, BeforeFrame, OnFrame, AfterFrame);

// Make sure the world uses the correct pipeline
ecs_set_pipeline(world, MyPipeline);

Now the application can create systems for the custom pipeline:

// System ran in the OnFrame phase
ECS_SYSTEM(world, Move, OnFrame, Position, Velocity);

// System ran in the AfterFrame phase
ECS_SYSTEM(world, DetectCollisions, AfterFrame, Position);

// This will now run systems in the custom pipeline
ecs_progress(world, 0);