{-# 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 -- $intro -- * Insertion -- $insertion -- * Removal -- $removal -- * Querying -- $query -- * Transitive Querying -- $trans -- * Exclusivity -- $exclusive -- * Hooks -- $hooks -- * [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' $ '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 -\> *|] -- @ -- $exclusive -- 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. -- $trans -- 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. -- $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.'Mischief.ECS.HooksRel.addOther' ('const' After)] -- -- instance 'Component' After where -- 'hooks' = [HooksRel.'Mischief.ECS.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! -- $exclusive -- 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.