| Safe Haskell | None |
|---|---|
| Language | GHC2024 |
Mischief.ECS.Tutorial.Relationships
Description
This module contains a more in-depth tutorial on Mischief Relationships.
Synopsis
Learn You an ECS for Great Mischief! - 5. Relationships
Mischief implement Relationships in a similar way to Flecs.
When a component is added to an entity, it is actually indexed by a ComponentId:
dataComponentId= ComponentId {id ::Entity, entity ::MaybeEntity}
The first field, id, is the entity corresponding to the component, while the second field, entity, is an optional reference to another entity.
This means that each ComponentId can either be a simple component, or a pair between a component and an entity (technically even between
two components or two entities but that's not directly allowed by the API).
So a relationship in Mischief is a pair between a component and an entity. It can be inserted on entities using Rel:
dataRelc = Rel {comp :: c, target ::Entity}
For instance, this is how we spawn an entity b that's a child of a:
a <-spawn() b <-spawn(RelChildOfa)
Insertion
Let's consider the following component, which will symbolize that an entity likes another, and by how much:
data Likes = LikesIntderiving (Component)
And three spawned entities: alice, bob, charlie.
As mentioned before, we can insert a relationship using the Rel type.
insert(Rel(Likes 3) alice,Rel(Likes 5) charlie) bobinsert(Rel(Likes 2) bob) alice
If we insert a second relationship with the same component and the same target, its value will overwrite the other. For instance, the following code
will make alice like bob by 3 instead of 2:
insert(Rel(Likes 3) bob) alice
Removal
Removing relationships can be done through the remove function, same as normal components. But instead of using the marker, we will use the C marker.R
Making bob stop liking alice.
remove(R@Likes alice) bob
The marker takes a type hint of the relationship's type (R@Likes), and a target Entity (alice). But it can also be given the Any wildcard instead:
remove(R@LikesAny) bob
This will remove all Likes relationships from bob, making him not like anyone.
Querying
Relationships can be queried using the R marker.
x <-query$mkQuery(R@Likes alice)
x :: [Rel Likes]
In quasi-notation, this becomes:
x <-query[q|Likes -> alice|]
Querying can also be done using the Any wildcard:
x <-query$mkQuery(R@Likes Any)
x :: [[Rel Likes]]
In quasi-queries, Any is symbolized by *:
x <-query[q|Likes -> *|]
As you can see, the return type of is R @c Any[Rel c]. It's returning a list of relationships, rather than a single relationship (unless the
relationship is exclusive, but more on that in a bit).
In the case of querying for R c Any, the query will only match entities that have at least one such relationship. The resulting [Rel c] should never be empty.
If you wish to also include entities that do not have those relationships, you can use (short for MRMaybe Relationships), the relational equivalent of .M
x <-query(MR@Likes alice)
x <- [q|Maybe Likes -> alice|]
x :: [Maybe(Result(Relc))]
(or just "Has"" in quasi-notation) is also supported as the relational equivalent of HasR.Has
Filter-wise, relationships allow the same archetype filters as normal components, through the R marker.
This is how we get the name of all entities that like alice:
x <-query$mkQuery'(C@Name) (With(R@Likes alice))
x <-query[q|Name / With Likes -> alice|]
This is how we get the name of all entities that like at least one other entity:
x <-query$mkQuery'(C@Name) (With(R@Likes Any))
x <-query[q|Name / With Likes -> *|]
Transitive Querying
Transitive queries are a powerful primitive which allow us to easily query components based on relationships between them.
For instance, this is how we can get the name of each entity, along with the names of all entities they like:
x <-query$mkQuery(C@Name,R@Likes (Q(C@Name)))
x :: [(Name, [Name])]
They tend to look much better when written as quasi-queries (don't forget the ()!):
x <-query[q|Name, Likes -> (Name)]
Note that transitive queries can be nested indefinitely:
x <- query [q|Name, Likes -> (Name, Likes -> (Name))|]
x :: [(Name, [(Name, [Name])])]
Same as querying for R @Likes Any (Likes -> *), entities that don't have any entity matching the relationship will be ignored by the query.
Exclusivity
A relationship can be made exclusive by setting the following associated Bool on its component instance:
instanceComponentFooRel where typeIsExclusiveRelFooRel =True
This will make it so only one instance of a relationship can exist on an entity at once.
insert(Rel(FooRel, a)) cinsert(Rel(FooRel, b)) c
Will result in just Rel (FooRel, b) being on c.
It also changes the result of R @FooRel Any and R @FooRel (Q (...)) queries to return a single item rather than a list.
Hooks
Relationship support dedicated hooks, named onAddRel, onSetRel, onRemoveRel, following the same rules as the normal hooks.
This is how we can log a message each time Likes is added between two entities:
instanceComponentLikes where onAddRel = [hookRelonLikesAdd] onLikesAdd ::HookContextRel->System() onLikesAdd HookContextRel {entity, target} =info[i|#{entity} now likes #{target}!|]
There are a number of predefined hooks that are useful when working with relationships, which can be found in Mischief.ECS.HooksRel.
For instance, addOther can be used to automate adding a complementary relationship on the target of a relationship.
As a quick example of why this is useful, let's create a Before/After relationship between entities:
data Before = Before data After = After instanceComponentBefore where onAddRel = [HooksRel.addOther(constAfter)] instanceComponentAfter wherehooks= [HooksRel.addOther(constBefore)]
Now, when we do:
insert(RelBefore a) b
A Rel After b will be inserted automatically on a.
And vice versa.
There are other interesting hooks, such as ones for automatically cleaning up a relationship when an entity is despawned. Check out Mischief.ECS.HooksRel for details!