Entity Relations

In a basic ECS, relations between entities, like hierarchies, can be represented by storing entities in components. E.g., we could have a child component like this:

1type ChildOf struct {
2    Parent ecs.Entity
3}

Or, alternatively, a parent component with many children:

1type Parent struct {
2    Children []ecs.Entity
3}

In conjunction with World Entity Access, this is often sufficient. However, we are not able to leverage the power of queries to e.g. get all children of a particular parent.

To make entity relations even more useful and efficient, Arche supports them as first class feature. Relations are added to and removed from entities just like components, and hence can be queried like components, with the usual efficiency. This is achieved by creating separate archetypes for relations with different target entities.

Relation components

To use entity relations, create components that have embedded an ecs.Relation as their first member:

1type ChildOf struct {
2    ecs.Relation
3}

That’s all to make a component be treated as an entity relation by Arche. Thus, we have created a relation type. When added to an entity, a target entity for the relation can be defined.

Note

Note that each entity can only have one relation component. See section Limitations.

Creating relations

On new entities

When creating entities, we can use an ecs.Builder to set a relation target. In the generic API, we use a MapX (e.g. generic.Map2 ).

1world := ecs.NewWorld()
2
3// The second argument specifies the relation component.
4builder := generic.NewMap2[Position, ChildOf](&world, generic.T[ChildOf]())
5
6_ = builder.New() // An entity with a zero target.
7
8parent := world.NewEntity()
9_ = builder.New(parent) // An entity with parent as target.
 1world := ecs.NewWorld()
 2
 3posID := ecs.ComponentID[Position](&world)
 4childID := ecs.ComponentID[ChildOf](&world)
 5
 6// We set the relation component with WithRelation.
 7builder := ecs.NewBuilder(&world, posID, childID).WithRelation(childID)
 8
 9_ = builder.New() // An entity with a zero target.
10
11parent := world.NewEntity()
12_ = builder.New(parent) // An entity with parent as target.

When adding components

A relation target can also be given when adding a relation component. With the ID-based API, we use the helper ecs.World.Relations for this, like for most operations on entity relations. In the generic API, we use a MapX (e.g. generic.Map2 ) again.

1world := ecs.NewWorld()
2
3builder := generic.NewMap1[Position](&world)
4adder := generic.NewMap1[ChildOf](&world, generic.T[ChildOf]())
5
6parent := world.NewEntity()
7child := builder.New()
8
9adder.Add(child, parent)
 1world := ecs.NewWorld()
 2
 3posID := ecs.ComponentID[Position](&world)
 4childID := ecs.ComponentID[ChildOf](&world)
 5
 6parent := world.NewEntity()
 7child := world.NewEntity(posID)
 8
 9world.Relations().Exchange(
10	child,             // The entity to modify
11	[]ecs.ID{childID}, // Component(s) to add
12	nil,               // Component(s) to remove
13	childID,           // The relation component of the added components
14	parent,            // The target entity
15)

Alternatively, we can use a generic.Exchange :

 1world := ecs.NewWorld()
 2
 3builder := generic.NewMap1[Position](&world)
 4adder := generic.NewExchange(&world).
 5	Adds(generic.T[ChildOf]()).        // Component(s) to add
 6	WithRelation(generic.T[ChildOf]()) // The relation component of the added components
 7
 8parent := world.NewEntity()
 9child := builder.New()
10
11adder.Add(child, parent)

Set and get relations

We can also change the target of an already assigned relation component. This is done via ecs.Relations.Set or generic.Map.SetRelation :

1world := ecs.NewWorld()
2
3builder := generic.NewMap1[ChildOf](&world)
4mapper := generic.NewMap[ChildOf](&world)
5
6parent := world.NewEntity()
7child := builder.New()
8
9mapper.SetRelation(child, parent)
1world := ecs.NewWorld()
2
3childID := ecs.ComponentID[ChildOf](&world)
4
5parent := world.NewEntity()
6child := world.NewEntity(childID)
7
8world.Relations().Set(child, childID, parent)

Similarly, relation targets can be obtained with ecs.Relations.Get or generic.Map.GetRelation :

1world := ecs.NewWorld()
2
3builder := generic.NewMap1[ChildOf](&world)
4mapper := generic.NewMap[ChildOf](&world)
5
6child := builder.New()
7
8_ = mapper.GetRelation(child)
1world := ecs.NewWorld()
2childID := ecs.ComponentID[ChildOf](&world)
3
4child := world.NewEntity(childID)
5
6_ = world.Relations().Get(child, childID)

Querying relations

And now for the best: querying for entities that have a certain relation and target.

In the ID-based API, relation targets can be queries with ecs.RelationFilter . In the generic API, it is supported by all FilterX via e.g. generic.Filter2.WithRelation .

 1world := ecs.NewWorld()
 2
 3// Two parent entities.
 4parent1 := world.NewEntity()
 5parent2 := world.NewEntity()
 6
 7// A builder with a relation
 8builder := generic.NewMap2[Position, ChildOf](&world, generic.T[ChildOf]())
 9
10// Create 10 entities for each parent.
11for i := 0; i < 10; i++ {
12	builder.New(parent1)
13	builder.New(parent2)
14}
15
16// A filter for all entities with Position,
17// and a ChildOf relation.
18filter := generic.NewFilter2[Position, ChildOf]().
19	WithRelation(generic.T[ChildOf]())
20
21// We specify the target when querying.
22// Alternatively, a fixed target can be specified in WithRelation above.
23query := filter.Query(&world, parent1)
24fmt.Println(query.Count()) // Prints 10
25
26query.Close()
 1world := ecs.NewWorld()
 2posID := ecs.ComponentID[Position](&world)
 3childID := ecs.ComponentID[ChildOf](&world)
 4
 5// Two parent entities.
 6parent1 := world.NewEntity()
 7parent2 := world.NewEntity()
 8
 9// A builder with a relation
10builder := ecs.NewBuilder(&world, posID, childID).
11	WithRelation(childID)
12
13// Create 10 entities for each parent.
14for i := 0; i < 10; i++ {
15	builder.New(parent1)
16	builder.New(parent2)
17}
18
19// A filter for all entities with Position,
20// and ChildOf with target parent1.
21filter := ecs.NewRelationFilter(ecs.All(posID, childID), parent1)
22
23query := world.Query(&filter)
24fmt.Println(query.Count()) // Prints 10
25
26query.Close()

Limitations

Entity relations in Arche are inspired by Flecs. However, the implementation in Arche is currently limited in that it only supports a single relation per entity, and no chained (or nested) relation queries.

When to use, and when not

When using Arche’s entity relations, an archetype is created for each target entity of a relation. Thus, entity relations are not efficient if the number of target entities is high (tens of thousands), while only a low number of entities has a relation to each particular target (less than a few dozens). Particularly in the extreme case of 1:1 relations, storing entities in components as explained in the introduction of this chapter is more efficient.

However, with a moderate number of relation targets, particularly with many entities per target, entity relations are very efficient. See section Benchmarks below, for a comparison of different ways to represent entity relations.

Beyond use cases where the relation target is a “physical” entity that appears in a simulation or game, targets can also be more abstract, like categories. Examples:

  • Different tree species in a forest model
  • Behavioral states in a finite state machine
  • The opposing factions in a strategy game
  • Render layers in a game or other graphical application

This concept is particularly useful for things that would best be expressed by components, but the possible components (or categories) are only known at runtime. Thus, it is not possible to create ordinary components for them. However, these categories can be represented by entities, which are used as relation targets.

See the last section of this chapter (Longer example) for an implementation of the tree species example above.

Benchmarks

The figure below compares the iteration time per entity for different ways of representing entity relations. The task is to sum up a value over the children of each parent.

The following ways to represent entity relations are shown in the figure:

  • ParentList (purple): Children form an implicit linked list. The parent references the first child.
    • Query over parents, inner loop implicit linked list of children, using world access for next child and value component.
  • ParentSlice (red): The parent holds a slice of all its children.
    • Query over parents, inner loop over slice of children using world access for value component.
  • Child (green): Each child references its parent.
    • Query over all child entities and retrieval of the parent sum component using world access.
  • Default (blue): Using Arche’s relations feature without filter caching.
    • Outer query over parents, inner loop over children using relation queries.
  • Cached (black): Using Arche’s relations feature with filter caching.
    • Same as above, using an additional component per parent to store cached filters.

The first three representations are possible in any ECS, while the last two use Arche’s entity relations feature.

Benchmarks Entity relations Benchmarks Entity relations
Iteration time per entity for different ways of representing entity relations. Color: ways to represent entity relations; Line style: total number of child entities; Markers: number of children per parent entity

The benchmarks show that Arche’s relations feature outperforms the other representations, except when there are very few children per parent. Only when there is a huge number of parents and significantly fewer than 100 children per parent, the Child representation should perform better.

The benchmark code can be found in the GitHub repository.

Longer example

To conclude this chapter, here is a longer example that uses Arche’ entity relations feature to represent tree species in a forest model.

 1package main
 2
 3import (
 4	"math/rand"
 5
 6	"github.com/mlange-42/arche/ecs"
 7	"github.com/mlange-42/arche/generic"
 8)
 9
10// SpeciesParams component for species (relation targets).
11type SpeciesParams struct {
12	GrowthRate float64
13}
14
15// Species relation component for tree individuals.
16type Species struct {
17	ecs.Relation
18}
19
20// Biomass component for tree individuals.
21type Biomass struct {
22	BM float64
23}
24
25func main() {
26	world := ecs.NewWorld()
27
28	speciesBuilder := generic.NewMap1[SpeciesParams](&world)
29	treeBuilder := generic.NewMap2[Biomass, Species](&world, generic.T[Species]())
30
31	// Create 10 species.
32	for s := 0; s < 10; s++ {
33		species := speciesBuilder.NewWith(
34			&SpeciesParams{GrowthRate: rand.Float64()},
35		)
36
37		// Create 100 trees per species. Biomass is zero.
38		for t := 0; t < 100; t++ {
39			treeBuilder.New(species)
40		}
41	}
42
43	speciesFilter := generic.NewFilter1[SpeciesParams]()
44	treeFilter := generic.NewFilter1[Biomass](). // We want to access biomass.
45							With(generic.T[Species]()).        // We want this, but will not access it
46							WithRelation(generic.T[Species]()) // Finally, the relation.
47
48	// Time loop.
49	for tick := 0; tick < 100; tick++ {
50		// Query and iterate species.
51		speciesQuery := speciesFilter.Query(&world)
52		for speciesQuery.Next() {
53			// Get species params and entity.
54			params := speciesQuery.Get()
55			species := speciesQuery.Entity()
56
57			// Query and iterate trees for the current species.
58			treeQuery := treeFilter.Query(&world, species)
59			for treeQuery.Next() {
60				bm := treeQuery.Get()
61				// Increase biomass by the species' growth rate.
62				bm.BM += params.GrowthRate
63			}
64		}
65	}
66
67	_ = world
68}