mischief-ecs
Safe HaskellNone
LanguageGHC2024

Mischief.ECS.Tutorial.Relationships

Description

This module contains a more in-depth tutorial on Mischief Relationships.

Previous Chapter: Components

Next Chapter: Queries

Main Page

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:

    data ComponentId = ComponentId {id :: Entity, entity :: Maybe Entity}
    

    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:

    data Rel c = 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 (Rel ChildOf a)
    

    Insertion

    Let's consider the following component, which will symbolize that an entity likes another, and by how much:

    data Likes = Likes Int deriving (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) bob
    insert (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 C marker, we will use the R marker.

    Making bob stop liking alice.

    remove (R @Likes alice) bob
    

    The R marker takes a type hint of the relationship's type (@Likes), and a target Entity (alice). But it can also be given the Any wildcard instead:

    remove (R @Likes Any) 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 R @c Any is [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 MR (short for Maybe Relationships), the relational equivalent of M.

    x <- query (MR @Likes alice)
    
    x <- [q|Maybe Likes -> alice|]
    
    x :: [Maybe (Result (Rel c))]
    

    HasR (or just "Has"" in quasi-notation) is also supported as the relational equivalent of 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:

    instance Component FooRel where
      type IsExclusiveRel FooRel = True
    

    This will make it so only one instance of a relationship can exist on an entity at once.

    insert (Rel (FooRel, a)) c
    insert (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:

    instance Component Likes where
      onAddRel = [hookRel onLikesAdd]
    
    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
    
    instance Component Before where
      onAddRel =  [HooksRel.addOther (const After)]
    
    instance Component After where
      hooks = [HooksRel.addOther (const Before)]
    

    Now, when we do:

    insert (Rel Before 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!

    Next Chapter: Queries