Skip to content

Relationships

Relationships makes it possible to describe entity graphs natively in ECS.

Adding/removing relationships is similar to adding/removing regular components, with as difference that instead of a single component id, a relationship adds a pair of two things to an entity. In this pair, the first element represents the relationship (e.g. "Eats"), and the second element represents the relationship target (e.g. "Apples").

Relationships can be used to describe many things, from hierarchies to inventory systems to trade relationships between players in a game. The following sections go over how to use relationships, and what features they support.

Definitions

NameDescription
IdAn id that can be added and removed
ComponentId with a single element (same as an entity id)
RelationshipUsed to refer to first element of a pair
TargetUsed to refer to second element of a pair
SourceEntity to which an id is added

Relationship queries

There are a number of ways a game can query for relationships. The following kinds of queries are available for all (unidirectional) relationships, and are all constant time:

Test if entity has a relationship pair

luau
world:has(bob, pair(Eats, Apples))
typescript
world.has(bob, pair(Eats, Apples))

Test if entity has a relationship wildcard

luau
world:has(bob, pair(Eats, jecs.Wildcard)
typescript
world.has(bob, pair(Eats, jecs.Wildcard)

Get parent for entity

luau
world:parent(bob)
typescript
world.parent(bob, pair(Eats, jecs.Wildcard)

Find first target of a relationship for entity

luau
world:target(bob, Eats)
typescript
world.target(bob, Eats)

Find first target of a relationship for entity

luau
world:target(bob, Eats)
typescript
world.target(bob, Eats)

Find all entities with a pair

luau
for id in world:query(pair(Eats, Apples)) do
    -- ...
end
typescript
for (const [id] of world.query(pair(Eats, Apples)) {
    // ...
}

Find all entities with a pair wildcard

luau
for id in world:query(pair(Eats, jecs.Wildcard)) do
    local food = world:target(id, Eats) -- Apples, ...
end
typescript
for (const [id] of world.query(pair(Eats, jecs.Wildcard)) {
    const food = world.target(id, Eats) // Apples, ...
}

Iterate all children for a parent

luau
for child in world:query(pair(jecs.ChildOf, parent)) do
    -- ...
end
typescript
for (const [child] of world.query(pair(jecs.ChildOf, parent)) {
    // ...
}

Relationship components

Relationship pairs, just like regular component, can be associated with data.

luau
local Position = world:component()
local Eats = world:component()
local Apples = world:entity()
local Begin = world:entity()
local End = world:entity()

local e = world:entity()
world:set(e, pair(Eats, Apples), { amount = 1 })

world:set(e, pair(Begin, Position), Vector3.new(0, 0, 0))
world:set(e, pair(End, Position), Vector3.new(10, 20, 30))

world:add(e, jecs.ChildOf, Position)
typescript
const Position = world.component()
const Eats = world.component()
const Apples = world.entity()
const Begin = world.entity()
const End = world.entity()

const e = world.entity()
world.set(e, pair(Eats, Apples), { amount: 1 })

world.set(e, pair(Begin, Position), new Vector3(0, 0, 0))
world.set(e, pair(End, Position), new Vector3(10, 20, 30))

world.add(e, jecs.ChildOf, Position)

Relationship wildcards

When querying for relationship pairs, it is often useful to be able to find all instances for a given relationship or target. To accomplish this, an game can use wildcard expressions.

Wildcards may used for the relationship or target part of a pair

luau
pair(Likes, jecs.Wildcard) -- Matches all Likes relationships
pair(jecs.Wildcard, Alice) -- Matches all relationships with Alice as target

Relationship performance

This section goes over the performance implications of using relationships.

Introduction

The ECS storage needs to know two things in order to store components for entities:

  • Which IDs are associated with an entity
  • Which types are associated with those ids Ids represent anything that can be added to an entity. An ID that is not associated with a type is called a tag. An ID associated with a type is a component. For regular components, the ID is a regular entity that has the builtin Component component.

Storing relationships

Relationships do not fundamentally change or extend the capabilities of the storage. Relationship pairs are two elements encoded into a single 53-bit ID, which means that on the storage level they are treated the same way as regular component IDs. What changes is the function that determines which type is associated with an id. For regular components this is simply a check on whether an entity has Component. To support relationships, new rules are added to determine the type of an id.

Because of this, adding/removing relationships to entities has the same performance as adding/removing regular components. This becomes more obvious when looking more closely at a function that adds a relationship pair.

Id ranges

Jecs reserves entity ids under a threshold (HI_COMPONENT_ID, default is 256) for components. This low id range is used by the storage to more efficiently encode graph edges between archetypes. Graph edges for components with low ids use direct array indexing, whereas graph edges for high ids use a hashmap. Graph edges are used to find the next archetype when adding/removing component ids, and are a contributing factor to the performance overhead of add/remove operations.

Because of the way pair IDs are encoded, a pair will never be in the low id range. This means that adding/removing a pair ID always uses a hashmap to find the next archetype. This introduces a small overhead.

Fragmentation

Fragmentation is a property of archetype-based ECS implementations where entities are spread out over more archetypes as the number of different component combinations increases. The overhead of fragmentation is visible in two areas:

  • Archetype creation
  • Queries (queries have to match & iterate more archetypes) Games that make extensive use of relationships might observe high levels of fragmentation, as relationships can introduce many different combinations of components. While the Jecs storage is optimized for supporting large amounts (hundreds of thousands) of archetypes, fragmentation is a factor to consider when using relationships.

Union relationships are planned along with other improvements to decrease the overhead of fragmentation introduced by relationships.

Archetype Creation

When an ID added to an entity is deleted, all references to that ID are deleted from the storage. For example, when the component Position is deleted it is removed from all entities, and all archetypes with the Position component are deleted. While not unique to relationships, it is more common for relationships to trigger cleanup actions, as relationship pairs contain regular entities.

The opposite is also true. Because relationship pairs can contain regular entities which can be created on the fly, archetype creation is more common than in games that do not use relationships. While Jecs is optimized for fast archetypes creation, creating and cleaning up archetypes is inherently more expensive than creating/deleting an entity. Therefore archetypes creation is a factor to consider, especially for games that make extensive use of relationships.

Indexing

To improve the speed of evaluating queries, Jecs has indices that store all archetypes for a given component ID. Whenever a new archetype is created, it is registered with the indices for the IDs the archetype has, including IDs for relationship pairs.

While registering an archetype for a relationship index is not more expensive than registering an archetype for a regular index, an archetype with relationships has to also register itself with the appropriate wildcard indices for its relationships. For example, an archetype with relationship pair(Likes, Apples) registers itself with the pair(Likes, Apples), pair(Likes, jecs.Wildcard) and pair(jecs.Wildcard, Apples) indices. For this reason, creating new archetypes with relationships has a higher overhead than an archetype without relationships.

This page takes wording and terminology directly from Flecs, the first ECS with full support for Entity Relationships.