{-# OPTIONS_GHC -Wno-unused-imports #-} -- | -- Module: Relationships Tutorial -- Description: Tutorial on using @Relationships@ -- -- This module contains a more in-depth tutorial on @Mischief Relationships@. -- -- [Previous Chapter: Components]("Mischief.ECS.Tutorial.Components") -- -- [Next Chapter: Queries]("Mischief.ECS.Tutorial.Queries") -- -- [Main Page]("Mischief.ECS") module Mischief.ECS.Tutorial.Relationships ( -- * Learn You an ECS for Great Mischief! - 5. Relationships -- * Introduction -- $intro -- * Insertion -- $insertion -- * Removal -- $removal -- * Querying -- $query -- * Transitive Querying -- $trans -- * Hooks -- $hooks -- * Examples -- $examples -- * [Next Chapter: Queries]("Mischief.ECS.Tutorial.Queries") ) where import Mischief.ECS -- $intro -- 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. -- $query -- 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|] -- @ -- $exclusive -- A relationship can be made exclusive by setting the following associated type on its component instance: -- -- @ -- instance 'Component' FooRel where -- type 'RelExclusivity' FooRel = 'Exclusive' -- @ -- -- 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@ queries to be of the form: -- -- @ -- 'Result ('Rel' FooRel) -- @ -- -- Instead of: -- -- @ -- ['Result ('Rel' FooRel)] -- @ -- $trans -- 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. -- The 'WithR' query filter lets us easily query for components of entities that have a certain relationship with a certain entity. -- -- Getting a list of all entities that like bob. -- -- @ -- x <- 'query'' 'E' ('WithR' @Likes bob) -- @ -- -- Getting a list of all entities that like anyone. -- -- @ -- x <- 'query'' 'E' ('WithR' @Likes Any) -- @ -- -- We can also modify @Likes@ to have an @Int@ as well, representing how much an entity likes another: -- -- @ -- data Likes = Likes 'Int' deriving ('Component') -- @ -- -- @ -- 'insert' ('Rel' (Likes 5, alice), 'Rel' (Likes 8, charlie)) bob -- @ -- -- The 'R' marker type can be used in a query to get a @['Result' ('Rel' c)]@ for each entity. -- -- Getting the name and all the Likes relationships of all entities. -- -- @ -- x <- query (C \@Name, 'R' \@Likes Any) -- @ -- -- @ -- x :: [('Result' Name, ['Result' ('Rel' Likes)])] -- @ -- -- -- Getting the name and the Like relationship with bob for all entities. -- -- @ -- x <- query (C \@Name, 'R' \@Likes bob) -- @ -- -- @ -- x :: [('Result' Name, 'Result' ('Rel' Likes))] -- @ -- -- Note that @'R' \@Likes@ will limit the query to only the archetypes that contain any relation with @Likes@. -- You can also use @'MR'@ (Maybe relationship) to also include the entities that don't contain such relationships. -- -- A component can be made @exclusive@ by setting the following 'Bool' in the 'Component' instance: -- -- @ -- instance 'Component' Likes where -- 'isExclusiveRel' = 'True' -- @ -- -- If a component is exclusive, there can only be one relationship containing it on an entity at once. -- -- For instance, if we do: -- -- @ -- 'insert' ('Rel' (Likes, alice) bob -- 'insert' ('Rel' (Likes, charlie)) bob -- @ -- -- @(Likes, charlie)@ will overwrite @(Likes, alice)@. -- -- This is useful for relationships such as 'ChildOf', since an entity can only have one parent at a time. -- $examples -- -- === __Example 1__ -- -- -- An example showing different operations on relationships, and the @relCleanupRemove@ hook. -- -- @ -- import "Mischief.ECS.Prelude" -- import "Mischief.ECS.Hooks" qualified as [Hooks]("Mischief.ECS.Hooks") -- import "Mischief.ECS.Systems" qualified as [Systems]("Mischief.ECS.Systems") -- -- data Likes = Likes 'Int' deriving ('Show') -- -- instance 'Component' Likes where -- 'hooks' = [Hooks]("Mischief.ECS.Hooks").'Mischief.ECS.Hooks.relCleanupRemove' -- -- main :: 'IO' () -- main = do -- app <- 'newApp' MainPlugin -- 'runApp' app -- -- data MainPlugin = MainPlugin deriving ('Eq') -- -- instance 'Plugin' MainPlugin where -- init _ = [Systems]("Mischief.ECS.Systems").'Mischief.ECS.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.