{-# OPTIONS_GHC -Wno-unused-imports #-} -- | -- [Previous Chapter: Quasi-Query Conversion]("Mischief.ECS.Tutorial.Guides.QuasiConversion") -- -- [Next Chapter: Monadic Queries]("Mischief.ECS.Tutorial.Guides.MonadicQueries") -- -- [Main Page]("Mischief.ECS") module Mischief.ECS.Tutorial.Guides.QueryPiping ( -- * Learn You an ECS for Great Mischief! - 2.5. Query Piping -- $intro -- * Map, Filters, and Insertion -- $map -- * Extending Queries -- $extend -- * Hierarchy Traversal -- $trav -- * Impure Piping -- $impure -- * Making Your Own Pipes! -- $custom -- * [Next Chapter: Monadic Queries]("Mischief.ECS.Tutorial.Guides.MonadicQueries") ) where import Mischief.ECS -- $intro -- Query piping is the process of chaining multiple query-returning functions together to produce a -- computation that can be ran in a system. -- -- All of these functions are technically pure. Only the @query_@, @query@ and @single@ functions are impure. They take a query computation and run it in the World. -- -- A piping functions looks something like: -- -- @ -- qmap :: (a -> b) -> 'Query' m a -> 'Query' m b -- @ -- -- And they are idiomatically chained using @&@. -- -- There are many piping functions, all of which you can find in "Mischief.ECS.World.Query.Pipe". This chapter will go over the most important ones. -- $map -- @qmap@ is a function which maps each set of components flowing through the query to something else. For instance, this is -- how you get the health of all players, with @1@ added to it: -- -- @ -- healths \<- -- ['q'|Health / With Player|] -- & 'qmap' (\\(Health x) -\> Health (x + 1)) -- & 'query' -- @ -- -- Note that @qmap@ only changed the local values of the components returned from the query. It does not automatically update their values in the ECS! -- -- In order to insert them, you can use @qinsert@: -- -- @ -- healths \<- -- ['q'|Health / With Player|] -- & 'qinsert' (\\(Health x) -\> Health (x + 1)) -- & 'query' -- @ -- -- You can also use @query_@ to only perform the insertion and discard the results: -- -- @ -- ['q'|Health / With Player|] -- & 'qinsert' (\\(Health x) -\> Health (x + 1)) -- & 'query_' -- @ -- -- @qfilter@ can filter entities. This is how you can update the position of all players that have a velocity higher than 0: -- -- @ -- ['q'|Position, Velocity / With Player|] -- & 'qfilter' (\\(_, Velocity v) -\> v \> 0) -- & 'qinsert' (\\(Position p, Velocity v) -\> Position (p + v)) -- & 'query_' -- @ -- -- @qcheck@ is a special function which takes an Entity Filter. It lets you easily filter out entities that haven't had a specific component be changed or added: -- -- @ -- ['q'|Name|] -- & 'qcheck' ['qf'|Changed Name, !Added Name|] -- & 'qinfo' (\\name -> ['i'|#{name} has changed their name!|]) -- & 'query_' -- @ -- -- Note that all filters should be moved to @[q|...|]@ where possible. There they act as Archetype Filters and are much faster. -- -- @qmapMaybe@ is also worth mentioning, as it combines @qmap@ and @qfilter@ and can be very useful. -- $impure -- There are query pipes that accept impure functions. They typically receive both a set of the components in the query, and the Entity associated with -- that data, to allow for easier mutation. -- -- @qtap@ simply receives the data and attaches an impure computation: -- -- @ -- ['q'|Health|] -- & 'qtap' -- (\\entity (Health h) -\> do -- 'info' ['i'|#{entity} has #{h} HP!|] -- 'insert' (Health $ h + 1) entity -- ) -- & 'query_' -- @ -- -- @qtraverse@ is similar, but also maps the data. -- -- @ -- ['q'|Health|] -- & 'qtraverse' -- (\\entity (Health h) -\> do -- takeDamage \<- someImpureFunction entity -- if takeDamage then (Health $ h - 1) else Health h -- ) -- & 'query_' -- @ -- -- Some of the pure functions also have impure variants. Such as @qmap@ has @qmapM@ (equivalent to @qtraverse@), and @qfilter@ has @qfilterM@. -- $extend -- Extensions are a particular category of pipes that extend each set of components in the Query. -- -- @qentity@ pairs the components with their source entity: -- -- @ -- ['q'|Name|] -- & 'qentity' -- & 'qinfo' (\\(entity, name) -> ['i'|The name of #{entity} is #{name}|]) -- & 'query_' -- @ -- -- @qextend@ takes a Query Data and a function to map it into the existing query: -- -- @ -- ['q'|Name, Player|] -- & 'qextend' ['qd'|Health|] (\\(name, player) health -> (name, health)) -- & 'qinfo' (\\(name, health) -> ['i'|#{name} has #{health}|]) -- & 'query_' -- @ -- $trav -- There are two built-in pipes to help you traverse entity hierarchies: @qrelateOne@ and @qrelateMany@. -- -- First, take a moment to familiarize yourself with the functions in @Mischief.ECS.Relationships.Graph@ and -- @Mischief.ECS.Relationships.Tree@. They all look either like @Entity -> m (Maybe Entity)@, or like -- @Entity -> m [Entity]@ (sometimes depending on the exclusivity of the relationship). -- -- In @Graph@ you have generic graph traversals, going from a node to all nodes that go into it, or to all nodes that -- it goes into (@incoming@ and @outgoing@ respectively). In @Tree@, there are functions to traverse a tree-like hierarchy, -- such as getting the @root@ of the tree, or getting all the @leaves@ that are connected to the current node. It is your -- responsibility to ensure the relationship is a tree! (i.e. has no cycles). -- -- Both traversal pipes take a traversal function, a Query Data, and a function to map the new components into the current one. One such traversal may look like this: -- -- @ -- ['q'|Name|] -- & 'qrelateOne' (Tree.'Mischief.ECS.Relationships.Tree.root' \@Likes) \['qd'|Name|] (,) -- & 'qinfo' (\\(a, b) -> ['i'|The root of #{a} is #{b}|]) -- & 'query_' -- @ -- -- @qrelateMany@ works the same but expects a @Entity -> m [Entity]@ traversal instead. In both cases, if the traversal results in no elements -- (the function returns either Nothing or an empty list), the query will be nulled. -- -- Take note that the components returned from the targets of the traversal will be wrapped in a @From@. -- $custom -- Writing your own pipes is easy! -- -- For instance, let's say you want a function that damages an entity, and returns their new health. -- Obviously this is trivial and not worth its own separate function, but try to consider it: -- -- @ -- qdamage :: 'Int' -> 'Query' 'System' Health -> 'Query' 'System' Health -- qdamage dmg x = -- x -- & 'qmap' (\\(Health x) -\> Health (x - dmg)) -- & 'qinsert' id -- @ -- -- Which you can then use like so: -- -- @ -- healths <- -- ['q'|Health / With Player|] -- & qdamage 5 -- & 'query' -- @ -- -- The names of these should generally start with @q@. And they should -- contain as few side-effects as possible. A good rule to follow -- is to try to have each pipe apply a single meaningful change to the data, -- which is obvious from the name.