Component traits are tags and pairs that can be added to components to modify their behavior. This manual contains an overview of all component traits supported by Flecs.
Cleanup traits
When entities that are used as tags, components, relationships or relationship targets are deleted, cleanup traits ensure that the store does not contain any dangling references. Any cleanup policy provides this guarantee, so while they are configurable, applications cannot configure traits that allows for dangling references.
Note: this only applies to entities (like tags, components, relationships) that are added to other entities. It does not apply to components that store an entity value, so:
-
C
struct MyComponent {
}
ecs_id_t ecs_entity_t
An entity identifier.
-
C++
struct MyComponent {
}
const Self & add() const
Add a component to an entity.
-
C#
public struct MyComponent
{
public Entity e;
}
e.Add(Ecs.ChildOf, parent);
-
Rust
#[derive(Component)]
struct MyComponent {
e: Entity, // Not covered by cleanup traits
}
e.child_of_id(parent); // Covered by cleanup traits
The default policy is that any references to the entity will be removed. For example, when the tag Archer
is deleted, it will be removed from all entities that have it, which is similar to invoking the remove_all
operation:
-
C
void ecs_remove_all(ecs_world_t *world, ecs_id_t id)
Remove all instances of the specified (component) id.
-
C++
world.remove_all(Archer);
-
C#
-
Rust
world.remove_all_id(archer);
Since entities can be used in relationship pairs, just calling remove_all
on just the entity itself does not guarantee that no dangling references are left. A more comprehensive description of what happens is:
-
C
-
C++
world.remove_all(Archer);
world.remove_all(Archer, flecs::Wildcard);
world.remove_all(flecs::Wildcard, Archer);
-
C#
world.RemoveAll(archer);
world.RemoveAll(archer, Ecs.Wildcard);
world.RemoveAll(Ecs.Wildcard, archer);
-
Rust
world.remove_all_id(archer);
world.remove_all_id((archer, flecs::Wildcard::ID));
world.remove_all_id((flecs::Wildcard::ID, archer));
This succeeds in removing all possible references to Archer
. Sometimes this behavior is not what we want however. Consider a parent-child hierarchy, where we want to delete the child entities when the parent is deleted. Instead of removing (ChildOf, parent)
from all children, we need to delete the children.
We also want to specify this per relationship. If an entity has (Likes, parent)
we may not want to delete that entity, meaning the cleanup we want to perform for Likes
and ChildOf
may not be the same.
This is what cleanup traits are for: to specify which action needs to be executed under which condition. They are applied to entities that have a reference to the entity being deleted: if I delete the Archer
tag I remove the tag from all entities that have it.
To configure a cleanup policy for an entity, a (Condition, Action)
pair can be added to it. If no policy is specified, the default cleanup action (Remove
) is performed.
There are three cleanup actions:
Remove
: as if doing remove_all(entity)
(default)
Delete
: as if doing delete_with(entity)
Panic
: throw a fatal error (default for components)
There are two cleanup conditions:
OnDelete
: the component, tag or relationship is deleted
OnDeleteTarget
: a target used with the relationship is deleted
Policies apply to both regular and pair instances, so to all entities with T
as well as (T, *)
.
Examples
The following examples show how to use cleanup traits
(OnDelete, Remove)
-
C
ecs_entity_t ecs_new_w_id(ecs_world_t *world, ecs_id_t id)
Create new entity with (component) id.
void ecs_delete(ecs_world_t *world, ecs_entity_t entity)
Delete an entity.
#define ECS_TAG(world, id)
Declare & define a tag.
-
C++
world.component<Archer>()
.add(flecs::OnDelete, flecs::Remove);
auto e = world.entity().add<Archer>();
world.component<Archer>().destruct();
-
C#
world.Component<Archer>().Entity
.Add(Ecs.OnDelete, Ecs.Remove);
Entity e = world.Entity().Add<Archer>();
world.Component<Archer>().Entity.Destruct();
-
Rust
// Remove Archer from entities when Archer is deleted
world
.component::<Archer>()
.add_trait::<(flecs::OnDelete, flecs::Remove)>();
let e = world.entity().add::<Archer>();
(OnDelete, Delete)
-
C
-
C++
world.component<Archer>()
.add(flecs::OnDelete, flecs::Delete);
auto e = world.entity().add<Archer>();
world.component<Archer>().destruct();
-
C#
world.Component<Archer>()
.Add(Ecs.OnDelete, Ecs.Delete);
Entity e = world.Entity().Add<Archer>();
world.Component<Archer>().Entity.Destruct();
-
Rust
// Remove Archer from entities when Archer is deleted
world
.component::<Archer>()
.add_trait::<(flecs::OnDelete, flecs::Remove)>();
let e = world.entity().add::<Archer>();
// This will remove Archer from e
world.component::<Archer>().destruct();
(OnDeleteTarget, Delete)
-
C
ecs_entity_t ecs_new(ecs_world_t *world)
Create new entity id.
-
C++
world.component<ChildOf>()
.add(flecs::OnDeleteTarget, flecs::Delete);
auto p = world.entity();
auto e = world.entity().add<ChildOf>(p);
p.destruct();
-
C#
world.Component<ChildOf>().Entity
.Add(Ecs.OnDeleteTarget, Ecs.Delete);
Entity p = world.Entity();
Entity e = world.Entity().Add<ChildOf>(p);
p.Destruct();
-
Rust
// Delete children when deleting parent
world
.component::<flecs::ChildOf>()
.add_trait::<(flecs::OnDeleteTarget, flecs::Delete)>();
let p = world.entity();
let e = world.entity().add_first::<flecs::ChildOf>(p);
// This will delete both p and e
p.destruct();
Cleanup order
While cleanup actions allow for specifying what needs to happen when a particular entity is deleted, or when an entity used with a particular relationship is deleted, they do not enforce a strict cleanup order. The reason for this is that there can be many orderings that satisfy the cleanup traits.
This is important to consider especially when writing OnRemove
triggers or hooks, as the order in which they are invoked highly depends on the order in which entities are cleaned up.
Take an example with a parent and a child that both have the Node
tag:
-
C
.query.terms = {{ Node }},
.callback = ...
});
#define ecs_observer(world,...)
Shorthand for creating an observer with ecs_observer_init().
-
C++
world.observer<Node>()
.event(flecs::OnRemove)
-
C#
world.Observer<Node>()
.Event(Ecs.OnRemove)
.Each((Entity e) => { });
Entity p = world.Entity().Add<Node>();
Entity c = world.Entity().Add<Node>().ChildOf(p);
-
Rust
world
.observer::<flecs::OnRemove, &Node>()
.each_entity(|e, node| {
// This observer will be invoked when a Node is removed
});
let p = world.entity().add::<Node>();
let c = world.entity().add::<Node>().child_of_id(p);
In this example, when calling p.destruct()
the observer is first invoked for the child, and then for the parent, which is to be expected as the child is deleted before the parent. Cleanup traits do not however guarantee that this is always the case.
An application could also call world.component<Node>().destruct()
which would delete the Node
component and all of its instances. In this scenario the cleanup traits for the ChildOf
relationship are not considered, and therefore the ordering is undefined. Another typical scenario in which ordering is undefined is when an application has cyclical relationships with a Delete
cleanup action.
Cleanup order during world teardown
Cleanup issues often show up during world teardown as the ordering in which entities are deleted is controlled by the application. While world teardown respects cleanup traits, there can be many entity delete orderings that are valid according to the cleanup traits, but not all of them are equally useful. There are ways to organize entities that helps world cleanup to do the right thing. These are:
Organize components, triggers, observers and systems in modules. Storing these entities in modules ensures that they stay alive for as long as possible. This leads to more predictable cleanup ordering as components will be deleted as their entities are, vs. when the component is deleted. It also ensures that triggers and observers are not deleted while matching events are still being generated.
Avoid organizing components, triggers, observers and systems under entities that are not modules. If a non-module entity with children is stored in the root, it will get cleaned up along with other regular entities. If you have entities such as these organized in a non-module scope, consider adding the EcsModule
/flecs::Module
/Ecs.Module
tag to the root of that scope.
The next section goes into more detail on why this improves cleanup behavior and what happens during world teardown.
World teardown sequence
To understand why some ways to organize entities work better than others, having an overview of what happens during world teardown is useful. Here is a list of the steps that happen when a world is deleted:
- Find all root entities World teardown starts by finding all root entities, which are entities that do not have the builtin
ChildOf
relationship. Note that empty entities (entities without any components) are not found during this step.
- Query out modules, components, observers and systems This ensures that components are not cleaned up before the entities that use them, and triggers, observers and systems are not cleaned up while there are still conditions under which they could be invoked.
- Query out entities that have no children If entities have no children they cannot cause complex cleanup logic. This also decreases the likelihood of initiating cleanup actions that could impact other entities.
- Delete root entities The root entities that were not filtered out will be deleted.
- Delete everything else The last step will delete all remaining entities. At this point cleanup traits are no longer considered and cleanup order is undefined.
Trait trait
The trait trait marks an entity as a trait, which is any tag that is added to another tag/component/relationship to modify its behavior. All traits in this manual are marked as trait. It is not required to mark a trait as a trait before adding it to another tag/component/relationship. The main reason for the trait trait is to ease some of the constraints on relationships (see the Relationship trait).
-
C
void ecs_add_id(ecs_world_t *world, ecs_entity_t entity, ecs_id_t id)
Add a (component) id to an entity.
-
C++
struct Serializable { };
world.component<Serializable>().add(flecs::Trait);
-
C#
public struct Serializable { }
world.Component<Serializable>().Entity.Add(Ecs.Trait);
-
Rust
#[derive(Component)]
struct Serializable;
world
.component::<Serializable>()
.add_trait::<flecs::Trait>();
}
Relationship trait
The relationship trait enforces that an entity can only be used as relationship. Consider the following example:
-
C
ecs_add(world, Likes);
ecs_add_pair(world, Apples, Likes);
ecs_add_pair(world, Likes, Apples);
-
C++
struct Likes { };
struct Apples { };
world.component<Likes>().add(flecs::Relationship);
.add<Likes>()
.add<Apples, Likes>()
.add<Likes, Apples>();
-
C#
public struct Likes { }
public struct Apples { }
world.Component<Likes>().Entity.Add(Ecs.Relationship);
Entity e = ecs.Entity()
.Add<Likes>()
.Add<Apples, Likes>()
.add<Likes, Apples>();
-
Rust
#[derive(Component)]
struct Likes;
#[derive(Component)]
struct Apples;
world
.component::<Likes>()
.add_trait::<flecs::Relationship>();
let e = world
.entity()
.add::<Likes>() // Panic, 'Likes' is not used as relationship
.add::<(Apples, Likes)>() // Panic, 'Likes' is not used as relationship, but as target
.add::<(Likes, Apples)>(); // OK
Entities marked with Relationship
may still be used as target if the relationship part of the pair has the Trait
trait. This ensures the relationship can still be used to configure the behavior of other entities. Consider the following code example:
-
C
ecs_add_pair(world, Loves,
EcsWith, Likes);
-
C++
struct Likes { };
struct Loves { };
world.component<Likes>().add(flecs::Relationship);
world.component<Loves>().add(flecs::With, world.component<Likes>());
-
C#
public struct Likes { }
public struct Loves { }
world.Component<Likes>().Entity.Add(Ecs.Relationship);
world.Component<Loves>().Entity.Add(Ecs.With, world.Component<Likes>());
-
Rust
#[derive(Component)]
struct Likes;
#[derive(Component)]
struct Loves;
world
.component::<Likes>()
.add_trait::<flecs::Relationship>();
// Even though Likes is marked as relationship and used as target here, this
// won't panic as With is marked as trait.
world
.component::<Loves>()
.add_trait::<(flecs::With, Likes)>();
Target trait
The target trait enforces that an entity can only be used as relationship target. Consider the following example:
-
C
ecs_add(world, Apples);
ecs_add_pair(world, Apples, Likes);
ecs_add_pair(world, Likes, Apples);
-
C++
struct Likes { };
struct Apples { };
world.component<Apples>().add(flecs::Target);
.add<Apples>()
.add<Apples, Likes>()
.add<Likes, Apples>();
-
C#
public struct Likes { }
public struct Apples { }
world.Component<Apples>().Entity.Add(Ecs.Target);
Entity e = ecs.Entity()
.Add<Apples>()
.Add<Apples, Likes>()
.Add<Likes, Apples>();
-
Rust
#[derive(Component)]
struct Likes;
#[derive(Component)]
struct Apples;
world.component::<Apples>().add_trait::<flecs::Target>();
let e = world
.entity()
.add::<Apples>() // Panic, 'Apples' is not used as target
.add::<(Apples, Likes)>() // Panic, 'Apples' is not used as target, but as relationship
.add::<(Likes, Apples)>(); // OK
PairIsTag trait
A relationship can be marked with PairIsTag in which case a pair with the relationship will never contain data. By default the data associated with a pair is determined by whether either the relationship or target are components. For some relationships however, even if the target is a component, no data should be added to the relationship. Consider the following example:
-
C
typedef struct {
float x;
float y;
} Position;
ecs_set(world, e, Position, {10, 20});
ecs_add_pair(world, e, Serializable,
ecs_id(Position));
const Position *p = ecs_get(world, e, Position);
const Position *p = ecs_get_pair_second(world, e, Serializable, Position);
FLECS_API const ecs_entity_t ecs_id(EcsDocDescription)
Component id for EcsDocDescription.
#define ECS_COMPONENT(world, id)
Declare & define a component.
-
C++
struct Serializable { };
struct Position {
float x, y;
};
auto e = ecs.entity()
.set<Position>({10, 20})
.add<Serializable, Position>();
const Position *p = e.
get<Position>();
const Position *p = e.
get<Serializable, Position>();
const T * get() const
Get component value.
-
C#
public struct Serializable { }
public record struct Position(float X, float Y);
Entity e = ecs.Entity()
.Set<Position>(new(10, 20))
.Add<Serializable, Position>();
ref readonly Position p = ref e.Get<Position>();
ref readonly Position p = ref e.GetSecond<Serializable, Position>();
-
Rust
#[derive(Component)]
struct Serializable; // Tag, contains no data
impl flecs::FlecsTrait for Serializable {}
#[derive(Component)]
struct Position {
x: f32,
y: f32,
}
let e = world
.entity()
.set(Position { x: 10.0, y: 20.9 })
.add_trait::<(Serializable, Position)>(); // Because Serializable is a tag, the pair
// has a value of type Position
// Gets value from Position component
e.get::<&Position>(|pos| {
println!("Position: ({}, {})", pos.x, pos.y);
});
// Gets (unintended) value from (Serializable, Position) pair
e.get::<&(Serializable, Position)>(|pos| {
println!("Serializable Position: ({}, {})", pos.x, pos.y);
});
To prevent data from being associated with pairs that can apply to components, the Tag
trait can be added to relationships:
-
C
ecs_add_pair(world, e, Serializable,
ecs_id(Position));
const Position *p = ecs_get(world, e, Position);
const Position *p = ecs_get_pair_second(world, e, Serializable, Position);
-
C++
ecs.component<Serializable>()
.add<flecs::PairIsTag>();
auto e = ecs.entity()
.set<Position>({10, 20})
.add<Serializable, Position>();
const Position *p = e.
get<Position>();
const Position *p = e.
get<Serializable, Position>();
-
C#
ecs.Component<Serializable>().Entity
.Add(Ecs.PairIsTag);
Entity e = ecs.Entity()
.Set<Position>(new(10, 20))
.Add<Serializable, Position>();
ref readonly Position p = ref e.Get<Position>();
ref readonly Position p = ref e.GetSecond<Serializable, Position>();
-
Rust
// This is currently not supported in Rust due to safety concerns.
The Tag
trait is only interpreted when it is added to the relationship part of a pair.
Final trait
Entities can be annotated with the Final
trait, which prevents using them with IsA
relationship. This is similar to the concept of a final class as something that cannot be extended. The following example shows how use Final
:
-
C
ecs_add_pair(world, e, i,
EcsIsA, e);
-
C++
auto e = ecs.entity()
.add(flecs::Final);
auto i = ecs.entity()
.is_a(e);
-
C#
Entity e = ecs.Entity()
.Add(Ecs.Final);
Entity i = ecs.Entity()
.IsA(e);
-
Rust
let e = world.entity().add_trait::<flecs::Final>();
let i = world.entity().is_a_id(e); // not allowed
Queries may use the final trait to optimize, as they do not have to explore subsets of a final entity. For more information on how queries interpret final, see the Query manual. By default, all components are created as final.
OnInstantiate trait
The OnInstantiate
trait configures the behavior of components when an entity is instantiated from another entity (usually a prefab). Instantiation happens when an IsA
pair is added to an entity.
By default, when an entity is instantiated, the components from the base entity (the IsA
target) are copied to the instance. This behavior can be modified with the OnInstantiate
trait, which can be used as pair in combination with three targets:
Target | C | C++ | C# | Description |
Override | EcsOverride | flecs::Override | Ecs.Override | Copy component from base to instance (default) |
Inherit | EcsInherit | flecs::Inherit | Ecs.Inherit | Inherit component from base |
DontInherit | EcsDontInherit | flecs::DontInherit | Ecs.DontInherit | Don't inherit (and don't copy) component from base |
Override
The default behavior for OnInstantiate
is Override
, which means that the component is copied to the instance. This means that after instantiation, the instance has an owned copy for the component that masks the base component (the "override").
Note that for an override to work correctly, a component has to be copyable.
The following example shows how to use the Override
trait:
-
C
assert(ecs_owns(ecs, inst, Mass));
assert(ecs_get(ecs, base, Mass) != ecs_get(ecs, inst, Mass));
#define ecs_value(T,...)
Convenience macro for creating compound literal value literal.
-
C++
ecs.component<Mass>().add(flecs::OnInstantiate, flecs::Override);
assert(inst.owns<Mass>());
assert(base.get<Mass>() != inst.get<Mass>());
-
C#
ecs.Component<Mass>().Entity
.Add(Ecs.OnInstantiate, Ecs.Override);
Entity base = ecs.Entity()
.Set<Mass>(new(100));
Entity inst = ecs.Entity()
.IsA(base);
Debug.Assert(inst.Owns<Mass>());
-
Rust
// Register component with trait. Optional, since this is the default behavior.
world
.component::<Mass>()
.add_trait::<(flecs::OnInstantiate, flecs::Override)>();
let base = world.entity().set(Mass { value: 100.0 });
let inst = world.entity().is_a_id(base); // Mass is copied to inst
assert!(inst.owns::<Mass>());
assert!(base.cloned::<&Mass>() != inst.cloned::<&Mass>());
Inherit
Components with the Inherit
trait are inherited from a base entity (the IsA
target) on instantiation. Inherited components are not copied to the instance, and are only stored once in memory. Operations such as get
and has
, and queries will automatically lookup inheritable components by following the IsA
relationship.
Inheritable components can be overridden manually by adding the component to the instance. This results in the same behavior as the Override
trait, where the component is copied from the base entity.
The following example shows how to use the Inherit
trait:
-
C
assert(ecs_has(ecs, inst, Mass));
assert(!ecs_owns(ecs, inst, Mass));
assert(ecs_get(ecs, base, Mass) != ecs_get(ecs, inst, Mass));
-
C++
ecs.component<Mass>().add(flecs::OnInstantiate, flecs::Inherit);
assert(inst.has<Mass>());
assert(!inst.owns<Mass>());
assert(base.get<Mass>() != inst.get<Mass>());
-
C#
ecs.Component<Mass>().Entity
.Add(Ecs.OnInstantiate, Ecs.Inherit);
Entity base = ecs.Entity()
.Set<Mass>(new(100));
Entity inst = ecs.Entity()
.IsA(base);
Debug.Assert(inst.Has<Mass>());
Debug.Assert(!inst.Owns<Mass>());
-
Rust
// Register component with trait
world
.component::<Mass>()
.add_trait::<(flecs::OnInstantiate, flecs::Inherit)>();
let base = world.entity().set(Mass { value: 100.0 });
let inst = world.entity().is_a_id(base);
assert!(inst.has::<Mass>());
assert!(!inst.owns::<Mass>());
assert!(base.cloned::<&Mass>() != inst.cloned::<&Mass>());
DontInherit
Components with the DontInherit
trait are not inherited from a base entity (the IsA
target) on instantiation, and are not copied to the instance. Operations such as has
and get
will not find the component, and queries will not match it.
Components with the DontInherit
cannot be overridden manually. When a component is added to an instance and the base also has the component, the base component is ignored and its value is not copied to the instance.
The following example shows how to use the DontInherit
trait:
-
C
assert(!ecs_has(ecs, inst, Mass));
assert(!ecs_owns(ecs, inst, Mass));
assert(ecs_get(ecs, inst, Mass) == NULL);
-
C++
ecs.component<Mass>().add(flecs::OnInstantiate, flecs::DontInherit);
assert(!inst.has<Mass>());
assert(!inst.owns<Mass>());
assert(inst.get<Mass>() == nullptr);
-
C#
ecs.Component<Mass>().Entity
.Add(Ecs.OnInstantiate, Ecs.DontInherit);
Entity base = ecs.Entity()
.Set<Mass>(new(100));
Entity inst = ecs.Entity()
.IsA(base);
Debug.Assert(!inst.Has<Mass>());
Debug.Assert(!inst.Owns<Mass>());
-
Rust
// Register component with trait
world
.component::<Mass>()
.add_trait::<(flecs::OnInstantiate, flecs::DontInherit)>();
let base = world.entity().set(Mass { value: 100.0 });
let inst = world.entity().is_a_id(base);
assert!(!inst.has::<Mass>());
assert!(!inst.owns::<Mass>());
assert!(!inst.try_get::<&Mass>(|mass| {}));
Transitive trait
Relationships can be marked as transitive. A formal-ish definition if transitivity in the context of relationships is:
If Relationship(EntityA, EntityB)
And Relationship(EntityB, EntityC)
Then Relationship(EntityA, EntityC)
What this means becomes more obvious when translated to a real-life example:
If Manhattan is located in New York, and New York is located in the USA, then Manhattan is located in the USA.
In this example, LocatedIn
is the relationship and Manhattan
, New York
and USA
are entities A
, B
and C
. Another common example of transitivity is found in OOP inheritance:
If a Square is a Rectangle and a Rectangle is a Shape, then a Square is a Shape.
In this example IsA
is the relationship and Square
, Rectangle
and Shape
are the entities.
When relationships in Flecs are marked as transitive, queries can follow the transitive relationship to see if an entity matches. Consider this example dataset:
-
C
ecs_add_pair(world, Manhattan, LocatedIn, NewYork);
ecs_add_pair(world, NewYork, LocatedIn, USA);
-
C++
auto LocatedIn = world.entity();
auto Manhattan = world.entity();
auto NewYork = world.entity();
auto USA = world.entity();
Manhattan.add(LocatedIn, NewYork);
NewYork.add(LocatedIn, USA);
-
C#
Entity locatedin = world.Entity();
Entity manhattan = world.Entity();
Entity newyork = world.Entity();
Entity usa = world.Entity();
Manhattan.Add(locatedin, newyork);
NewYork.Add(locatedin, usa);
-
Rust
let locatedin = world.entity();
let manhattan = world.entity();
let newyork = world.entity();
let usa = world.entity();
manhattan.add_id((locatedin, newyork));
newyork.add_id((locatedin, usa));
If we were now to query for (LocatedIn, USA)
we would only match NewYork
, because we never added (LocatedIn, USA)
to Manhattan
. To make sure queries Manhattan
as well we have to make the LocatedIn
relationship transitive. We can simply do this by adding the transitive trait to the relationship entity:
-
C
-
C++
LocatedIn.add(flecs::Transitive);
-
C#
locatedIn.Add(Ecs.Transitive);
-
Rust
locatedin.add_trait::<flecs::Transitive>();
When now querying for (LocatedIn, USA)
, the query will follow the LocatedIn
relationship and return both NewYork
and Manhattan
. For more details on how queries use transitivity, see the Transitive Relationships section in the query manual.
Reflexive trait
A relationship can be marked reflexive which means that a query like Relationship(Entity, Entity)
should evaluate to true. The utility of Reflexive
becomes more obvious with an example:
Given this dataset:
we can ask whether an oak is a tree:
IsA(Oak, Tree)
- Yes, an Oak is a tree (Oak has (IsA, Tree))
We can also ask whether a tree is a tree, which it obviously is:
IsA(Tree, Tree)
- Yes, even though Tree does not have (IsA, Tree)
However, this does not apply to all relationships. Consider a dataset with a LocatedIn
relationship:
LocatedIn(SanFrancisco, UnitedStates)
we can now ask whether SanFrancisco is located in SanFrancisco, which it is not:
LocatedIn(SanFrancisco, SanFrancisco)
- No
In these examples, IsA
is a reflexive relationship, whereas LocatedIn
is not.
Acyclic trait
A relationship can be marked with the Acyclic
trait to indicate that it cannot contain cycles. Both the builtin ChildOf
and IsA
relationships are marked acyclic. Knowing whether a relationship is acyclic allows the storage to detect and throw errors when a cyclic relationship is introduced by accident.
Note that because cycle detection requires expensive algorithms, adding Acyclic
to a relationship does not guarantee that an error will be thrown when a cycle is accidentally introduced. While detection may improve over time, an application that runs without errors is no guarantee that it does not contain acyclic relationships with cycles.
Traversable trait
Traversable relationships are allowed to be traversed automatically by queries, for example using the up
bit flag (upwards traversal, see query traversal flags). Traversable relationships are also marked as Acyclic
, which ensures a query won't accidentally attempt to traverse a relationship that contains cycles.
Events are propagated along the edges of traversable relationships. A typical example of this is when a component value is changed on a prefab. The event of this change will be propagated by traversing the IsA
relationship downwards, for all instances of the prefab. Event propagation does not happen for relationships that are not marked with Traversable
.
Exclusive trait
The Exclusive
trait enforces that an entity can have only a single instance of a relationship. When a second instance is added, it replaces the first instance. An example of a relationship with the Exclusive
trait is the builtin ChildOf
relationship:
-
C
-
C++
const Self & child_of(entity_t second) const
Shortcut for add(ChildOf, entity).
-
C#
e.ChildOf(parentA);
e.ChildOf(parentB);
-
Rust
let parent_a = world.entity();
let parent_b = world.entity();
e.child_of_id(parent_a);
e.child_of_id(parent_b); // replaces (ChildOf, parent_a)
To create a custom exclusive relationship, add the Exclusive
trait:
-
C
-
C++
-
C#
Entity marriedTo = world.Entity()
.Add(Ecs.Exclusive);
-
Rust
let married_to = world.entity().add_trait::<flecs::Exclusive>();
CanToggle trait
The CanToggle
trait allows a component to be toggled. Component toggling can (temporarily) disable a component, which excludes it from queries. Component toggling can be used as a cheaper alternative to adding/removing as toggling relies on setting a bitset, and doesn't require the entity to be moved between tables. Component toggling can also be used to restore a component with its old value.
Queries treat a disabled component as if the entity doesn't have it. CanToggle
components add a small amount of overhead to query evaluation, even for entities that did not toggle their component.
The following example shows how to use the CanToggle
trait:
-
C
ecs_enable_component(world, e, Position, false);
assert(!ecs_is_enabled(world, e, Position));
ecs_enable_component(world, e, Position, true);
assert(ecs_is_enabled(world, e, Position));
-
C++
world.component<Position>().add(flecs::CanToggle);
assert(!e.is_enabled<Position>());
assert(e.is_enabled<Position>());
const Self & disable() const
Disable an entity.
const Self & enable() const
Enable an entity.
-
C#
ecs.Component<Position>().Entity
.Add(Ecs.CanToggle);
Entity e = world.Entity()
.Set<Position>(new(10, 20));
e.Disable<Position>();
Debug.Assert(!e.IsEnabled<Position>());
e.Enable<Position>();
Debug.Assert(e.IsEnabled<Position>());
-
Rust
world
.component::<Position>()
.add_trait::<flecs::CanToggle>();
let e = world.entity().set(Position { x: 10.0, y: 20.0 });
e.disable::<Position>(); // Disable component
assert!(!e.is_enabled::<Position>());
e.enable::<Position>(); // Enable component
assert!(e.is_enabled::<Position>());
Union trait
The Union
is similar to Exclusive
in that it enforces that an entity can have only a single instance of a relationship. The difference between Exclusive
and Union
is that Union
combines different relationship targets in a single table. This reduces table fragmentation, and as a result speeds up add/remove operations. This increase in add/remove speed does come at a cost: iterating a query with union terms is more expensive than iterating a regular relationship.
The API for using the Union
trait is similar to regular relationships, as this example shows:
-
C
ecs_add_pair(world, e, Movement, Running);
ecs_add_pair(world, e, Movement, Walking);
-
C++
e.
add(Movement, Walking);
-
C#
Entity movement = world.Entity().Add(Ecs.Union);
Entity walking = world.Entity();
Entity running = world.Entity();
Entity e = world.Entity().Add(movement, running);
e.Add(movement, walking);
-
Rust
let movement = world.entity().add_trait::<flecs::Union>();
let walking = world.entity();
let running = world.entity();
let e = world.entity().add_id((movement, running));
e.add_id((movement, walking)); // replaces (Movement, Running)
When compared to regular relationships, union relationships have some differences and limitations:
- Relationship cleanup does not work yet for union relationships
- Removing a union relationship removes any target, even if the specified target is different
- Union relationships cannot have data
Sparse trait
The Sparse
trait configures a component to use sparse storage. Sparse components are stored outside of tables, which means they do not have to be moved. Sparse components are also guaranteed to have stable pointers, which means that a component pointer is not invalidated when an entity moves to a new table. ECS operations and queries work as expected with sparse components.
Sparse components trade in query speed for component add/remove speed. Adding and removing sparse components still requires an archetype change.
They also enable storage of non-movable components. Non-movable components in the C++ API are automatically marked as sparse.
The following code example shows how to mark a component as sparse:
-
C
-
C++
world.component<Position>().add(flecs::Sparse);
-
C#
ecs.Component<Position>().Entity
.Add(Ecs.Sparse);
-
Rust
world.component::<Position>().add_trait::<flecs::Sparse>();
Symmetric trait
The Symmetric
trait enforces that when a relationship (R, Y)
is added to entity X
, the relationship (R, X)
will be added to entity Y
. The reverse is also true, if relationship (R, Y)
is removed from X
, relationship (R, X)
will be removed from Y
.
The symmetric trait is useful for relationships that do not make sense unless they are bidirectional. Examples of such relationships are AlliesWith
, MarriedTo
, TradingWith
and so on. An example:
-
C
ecs_add_pair(world, Bob, MarriedTo, Alice);
-
C++
auto MarriedTo = world.entity().add(flecs::Symmetric);
auto Bob = ecs.entity();
auto Alice = ecs.entity();
Bob.add(MarriedTo, Alice);
-
C#
Entity marriedTo = world.Entity().Add(Ecs.Symmetric);
Entity bob = ecs.Entity();
Entity alice = ecs.Entity();
Bob.Add(marriedTo, alice);
-
Rust
let married_to = world.entity().add_trait::<flecs::Symmetric>();
let bob = world.entity();
let alice = world.entity();
bob.add_id((married_to, alice)); // Also adds (MarriedTo, Bob) to Alice
With trait
The With
relationship can be added to components to indicate that it must always come together with another component. The following example shows how With
can be used with regular components/tags:
-
C
-
C++
auto Responsibility = world.entity();
auto Power = world.entity().add(flecs::With, Responsibility);
auto e = world.entity().add(Power);
-
C#
Entity responsibility = world.Entity();
Entity power = world.Entity().Add(Ecs.With, responsibility);
Entity e = world.Entity().Add(power);
-
Rust
let responsibility = world.entity();
let power = world.entity().add_first::<flecs::With>(responsibility);
// Create new entity that has both Power and Responsibility
let e = world.entity().add_id(power);
When the With
relationship is added to a relationship, the additional id added to the entity will be a relationship pair as well, with the same target as the original relationship:
-
C
-
C++
auto Likes = world.entity();
auto Loves = world.entity().add(flecs::With, Likes);
auto Pears = world.entity();
auto e = world.entity().add(Loves, Pears);
-
C#
Entity likes = world.Entity();
Entity loves = world.Entity().Add(Ecs.With, likes);
Entity pears = world.Entity();
Entity e = world.Entity().Add(loves, pears);
-
Rust
let likes = world.entity();
let loves = world.entity().add_trait::<(flecs::With, Likes)>();
let pears = world.entity();
// Create new entity with both (Loves, Pears) and (Likes, Pears)
let e = world.entity().add_id((loves, pears));
OneOf trait
The OneOf
trait enforces that the target of the relationship is a child of a specified entity. OneOf
can be used to indicate that the target needs to be either a child of the relationship (common for enum relationships), or of another entity.
The following example shows how to constrain the relationship target to a child of the relationship:
-
C
-
C++
auto Food = world.entity().add(flecs::OneOf);
auto Apples = world.entity().child_of(Food);
auto Fork = world.entity();
auto a = world.entity().add(Food, Apples);
auto b = world.entity().add(Food, Fork);
-
C#
Entity food = world.Entity().Add(Ecs.OneOf);
Entity apples = world.Entity().ChildOf(food);
Entity fork = world.Entity();
Entity a = world.Entity().Add(food, apples);
Entity b = world.Entity().Add(food, fork);
-
Rust
// Enforce that target of relationship is child of Food
let food = world.entity().add_trait::<flecs::OneOf>();
let apples = world.entity().child_of_id(food);
let fork = world.entity();
// This is ok, Apples is a child of Food
let a = world.entity().add_id((food, apples));
// This is not ok, Fork is not a child of Food
let b = world.entity().add_id((food, fork));
The following example shows how OneOf
can be used to enforce that the relationship target is the child of an entity other than the relationship:
-
C
ecs_add_pair(world, Eats,
EcsOneOf, Food);
-
C++
auto Food = world.entity();
auto Eats = world.entity().add(flecs::OneOf, Food);
auto Apples = world.entity().child_of(Food);
auto Fork = world.entity();
auto a = world.entity().add(Eats, Apples);
auto b = world.entity().add(Eats, Fork);
-
C#
Entity food = world.Entity();
Entity eats = world.Entity().Add(Ecs.OneOf, food);
Entity apples = world.Entity().ChildOf(food);
Entity fork = world.Entity();
Entity a = world.Entity().Add(eats, apples);
Entity b = world.Entity().Add(eats, fork);
-
Rust
// Enforce that target of relationship is child of Food
let food = world.entity();
let eats = world.entity().add_first::<flecs::OneOf>(food);
let apples = world.entity().child_of_id(food);
let fork = world.entity();
// This is ok, Apples is a child of Food
let a = world.entity().add_id((eats, apples));
// This is not ok, Fork is not a child of Food
let b = world.entity().add_id((eats, fork));