GECS: Godot Entity Component System - Relationships
Relationships
Link entities together
Think of components as the data that makes up the state of an entity and relationships as the links that connect entity to entity(s)
Components are for storing data about the entity and relationships are for storing how entities interact or relate to other entities.
They can just be a direct link or they can carry some data about the relationship.
What are relationships in GECS?
In GECS relationships consist of three parts, a source(Entity), target(Entity) and relation (Component).
Relationships allow you to easily associate things together and simplify querying for data by being able to use relationships as a way to search in addition to normal query methods.
Definitions
Name | Description |
---|---|
Relationship | A relationship that can be added and removed |
Pair | Relationship with two elements |
Relation | The first element of a pair |
Target | The second element of a pair |
Source | Entity which a relationship is added |
Examples
Let's take a look at some classic Alice Bob and Heather examples that seem like they're straight out of your high-school math book.
# Create a new relationship (Shortened to Rel)
Relationship.new(C_Relation.new(), e_target)
# c_likes.gd
class_name C_Likes
extends Component
# c_loves.gd
class_name C_Loves
extends Component
# c_eats.gd
class_name C_Eats
extends Component
@export var quantity :int = 1
func _init(qty: int = quantity):
quantity = qty
# e_food.gd
class_name Food
extends Entity
# example.gd
# Create our entities
var e_bob = Entity.new()
var e_alice = Entity.new()
var e_heather = Entity.new()
var e_apple = Food.new()
# Create our relationships
# bob likes alice
e_bob.add_relationship(Relationship.new(C_Likes.new(), e_alice))
# alice loves heather
e_alice.add_relationship(Relationship.new(C_Loves.new(), e_heather))
# heather likes food
e_heather.add_relationship(Relationship.new(C_Likes.new(), Food))
# heather eats 5 apples
e_heather.add_relationship(Relationship.new(C_Eats.new(5), e_apple))
# alice no longer loves heather
alice.remove_relationship(Relationship.new(C_Loves.new(), e_heather))
Relationship Queries
Now that we've created these entities and add the relationships we can query for these entities based on their relationships in the following ways
- Relation: A component type, or an instanced component with data
- Target: Either a reference to an entity, or the entity archetype.
# Any entity that likes alice
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), e_alice)])
# Any entity with any relations toward heather
ECS.world.query.with_relationship([Relationship.new(ECS.wildcard, e_heather)])
# Any entity with any relations toward heather that don't have any relationships with bob
ECS.world.query.with_relationship([Relationship.new(ECS.wildcard, e_heather)]).without_relationship([Relationship.new(C_Likes.new(), e_bob)])
# Any entity that eats 5 apples
ECS.world.query.with_relationship([Relationship.new(C_Eats.new(5), e_apple)])
# any entity that likes the food entity archetype
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), Food)])
# Any entity that likes anything
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), ECS.wildcard)])
ECS.world.query.with_relationship([Relationship.new(C_Likes.new())])
# Any entity with any relation to Enemy Entity archetype
ECS.world.query.with_relationship([Relationship.new(ECS.wildcard, Enemy)])
Relationship Wildcard
When querying for relationship pairs, it can be helpful to find all entities for a given relation or target. To accomplish this, we can use a wildcard constant.
ECS.wildcard = null
Using null will work as well but this constant keeps it semantic.
Omitting the target in a a pair implicitly indicates ECS.WildCard
Naming and Best Practice
Relationships should be named in snake case based on the Relationship that they provide. You can store relationships and reuse them rather then creating them each time for best performance.
# This stored relationship is cheaper to reuse then recreating it each time
var r_likes_apples = Relationship.new(C_Likes.new(), e_apple)
It can also be helpful to have a Relationships static class with functions to retrieve these relationships to encourage reuse like. Then you can have a big file filled with all your relationships on autocomplete.
class_name Relationships
static func attacking_players():
return Relationship.new(C_IsAttacking.new(), Player)
static func attacking_anything():
return Relationship.new(C_IsAttacking.new(), ECS.wildcard)
static func range_attacking_players():
return Relationship.new(C_IsAttackingRanged.new(), Player)
static func range_attacking_anything():
return Relationship.new(C_IsAttackingRanged.new(), ECS.wildcard)
static func chasing_players():
return Relationship.new(C_IsChasing.new(), Player)
static func chasing_anything():
return Relationship.new(C_IsChasing.new(), ECS.wildcard)