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

    Introduction

    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 (R @Likes alice)
    
    x :: [Result (Rel Likes)]
    

    In quasi-queries, this becomes:

    x <- [q|Likes -> alice|]
    

    Result (Rel c) is the return type of R @c e. It can be used in most Result-based operation discussed in the previous chapter, such as set and delete.

    Querying can also be done using the Any wildcard:

    x <- query (R @Likes Any)
    
    x :: [[Result (Rel Likes)]]
    

    In quasi-queries, Any is symbolized by *:

    x <- [q|Likes -> *|]
    

    As you can see, the return type of R @c Any is [Result (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).

    Remember that the fields of the inner type of a Result are inherited by the Result itself. So we can just use .comp and .target to get the component and target of a Result (Rel c).

    Just x <- get (R @Likes alice) bob
    
    x.target :: Entity
    x.comp   :: Likes
    

    In the case of querying for R c Any, the query will only match entities that have at least one such relationship. The resulting [Result (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 :: [Maybe (Result (Rel c))]
    

    Which is this in quasi form:

    x ['q'|Maybe Likes - alice|]
    

    Transitive Querying

    Transitive queries are a powerful primitive which allow us to easily query components based on relational connections.

    For instance, this is how we can get the name of each entity, along with the names of all entities they like:

    x <- query (C @Name, R @Likes (Q (C @Name)))
    
    x :: [(Result Name, [Result Name])]
    

    They tend to look much better when written as quasi-queries (don't forget the ()!):

    x <- [q|Name, Likes -> (Name)]
    

    Note that transitive queries can be nested as much as you want:

    x <- [q|Name, Likes -> (Name, Likes -> (Name))|]
    
    x :: [(Result Name, [(Result Name, [Result Name])])]
    

    Hooks

    There are a number of predefined hooks that are useful when working with relationships, which can be found in Mischief.ECS.Hooks.

    For instance, relComplementary 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
      hooks = relComplementary (const After)
    
    instance Component After where
      hooks = relComplementary (const Before)
    

    Now, when we do:

    insert (Rel Before a) b
    

    A Rel After b will be inserted automatically on a.

    And when we do:

    remove (R @Before) b
    

    Rel After b will be removed from a.

    And vice versa.

    There are also relCleanupRemove and relCleanupDespawn for automatically removing a relationship (or ,respectively, despawning its entity) when its target has been despawned. And a more generic relCleanup which allows adding custom cleanup behavior.

    Examples

    Example 1

    Expand

    An example showing different operations on relationships, and the relCleanupRemove hook.

    import Mischief.ECS.Prelude
    import Mischief.ECS.Hooks qualified as Hooks
    import Mischief.ECS.Systems qualified as Systems
    
    data Likes = Likes Int deriving (Show)
    
    instance Component Likes where
      hooks = Hooks.relCleanupRemove
    
    main :: IO ()
    main = do
      app <- newApp MainPlugin
      runApp app
    
    data MainPlugin = MainPlugin deriving (Eq)
    
    instance Plugin MainPlugin where
      init _ = Systems.add Startup test
    
    test :: System ()
    test = do
      alice <- spawn (Name "Alice")
      bob <- spawn (Name "Bob")
      charlie <- spawn (Name "Charlie")
    
      insert (Rel (Likes 5) alice, Rel (Likes 10) bob) charlie
      insert (Rel (Likes 7) alice) bob
      insert (Rel (Likes 9) bob, Rel (Likes 5) charlie) alice
    
      (info . text) =<< query (C @Name, R @Likes alice)
      (info . text) =<< query (C @Name, R @Likes bob)
      (info . text) =<< query (C @Name, R @Likes charlie)
    
      remove (R @Likes Any) alice
      despawn bob
    
      (info . text) =<< query (C @Name, R @Likes alice)
      (info . text) =<< query (C @Name, R @Likes bob)
      (info . text) =<< query (C @Name, R @Likes charlie)
    
    > [INFO] [("Bob",Rel {comp = Likes 7, target = 28v1}),("Charlie",Rel {comp = Likes 5, target = 28v1})]
    > [INFO] [("Alice",Rel {comp = Likes 9, target = 29v1}),("Charlie",Rel {comp = Likes 10, target = 29v1})]
    > [INFO] [("Alice",Rel {comp = Likes 5, target = 30v1})]
    
    > [INFO] [("Charlie",Rel {comp = Likes 5, target = 28v1})]
    > [INFO] []
    > [INFO] []
    

    You can see in the second set of prints that nobody likes Bob anymore, since he "died", and the cleanup hook made it so any "Likes -> Bob" relationships were automatically removed.

    Next Chapter: Queries