{-# OPTIONS_GHC -Wno-unused-imports #-} -- | -- Module: Queries Tutorial -- Description: How To Mischief. -- -- [Previous Chapter: Query Piping]("Mischief.ECS.Tutorial.Guides.QueryPiping") -- -- [Next Chapter: Scheduling and Running Systems]("Mischief.ECS.Tutorial.Guides.Scheduling") -- -- [Main Page]("Mischief.ECS") module Mischief.ECS.Tutorial.Guides.MonadicQueries ( -- * Learn You an ECS for Great Mischief! - 2.6. Monadic Queries -- $intro -- * Binding -- $bind -- * Sources -- $source -- * Accumulating and Aggregating Queries -- $accumulation -- * Grabbing New Components and Resources -- $extend -- * Lifting -- $lifting -- * [Next Chapter: Scheduling and Running Systems]("Mischief.ECS.Tutorial.Guides.Scheduling") ) where import Control.Monad (void) import Data.Foldable (for_) import Data.Traversable (for) import Mischief.ECS -- $intro -- Finally we get to the most powerful and complete ability of Queries: monadic operations! -- $bind -- Monadic queries are usually written by using one of the query runners (@query_@, @query@, @single@), followed by a @do@: -- -- @ -- 'query_' $ do -- playerName <- ['q'|Name / With Player|] -- 'pure' () -- @ -- -- When you bind a value out of a query (the @<-@ above), you are immediately iterating over all elements of a query and applying operations to them. -- It is in a way similar to the @List@ monad if you are familiar with it (if you are not, don't worry!). -- -- Essentially, in pseudo-imperative, python-eseque code, the above is equivalent to: -- -- @ -- for playerName in [q|Name / With Player|]: -- pure () -- @ -- -- There are many great uses to this, which you'll see in this chapter. -- The most naive one is that it naturally allows you to write sql-like joins: -- -- @ -- 'query_' $ do -- playerName <- ['q'|Name / With Player|] -- ['q'|Name / With Enemy|] -- & 'qinfo' (\\enemyName -> ['i'|#{playerName}, #{enemyName}|]) -- @ -- -- Which is, in essence: -- -- @ -- for playerName in [q|Name / With Player|]: -- for enemyName in [q|Name / With Enemy|]: -- print (playerName + \", \" + enemyName) -- @ -- $source -- There is one detail about the section above that may not make much sense at first. Essentially, you may be wondering what happens if you do this: -- -- @ -- 'query_' $ do -- Health health <- ['q'|Health / With Player|] -- ['q'|Name / With Enemy|] -- & 'qinsert' (\_ -> (Health $ health - 1)) -- @ -- -- It does, in fact, insert the player's health (minus 1) to each enemy! But why, and how can we do an insertion on the player? -- -- The answer are Sources. Each time you start a new query (e.g. the @[q|Name / With Enemy|]@ above), you turn your \"attention\" to the -- new entiites. The source of the query are the enemies. -- -- You can change the source of a query using @qrefocus@; this code will iterate over all enemies but insert the Health component on the player: -- -- @ -- 'query_' $ do -- (player, Health health) <- ['q'|Entity, Health / With Player|] -- ['q'|Name / With Enemy|] -- & 'qrefocus' player -- & 'qinsert' (\_ -> (Health $ health - 1)) -- @ -- -- Keeping track of sources can be a significant footgun and one of the main disadvantages to writing monadic queries. -- -- One other way to bring the \"attention\" back to the players is by using @qpure@ (or @pure ()@ followed by @qrefocus@): -- -- @ -- 'query_' $ do -- (player, Health health) <- ['q'|Entity, Health / With Player|] -- enemyName <- ['q'|Name / With Enemy|] -- 'qpure' player -- & 'qinsert' (\_ -> (Health $ health - 1)) -- @ -- -- @qpure@ essentially creates a one-element query with the current player as the source, you can think of this as: -- -- @ -- for (player, Health health) in ['q'|Entity, Health / With Player|]: -- for enemyName in ['q'|Name / With Enemy|] -- for _ in [()]: -- insert (Health $ health - 1) player -- @ -- -- Note that using just @pure ()@ without refocusing is completely valid. But keep in mind that the source of the computation you're -- writing will be the \"null entity\". If you want to perform any sourced operations such as @qinsert@, you'll need to refocus first. -- $accumulation -- To re-use the context from the previous chapter, consider that you have players marked by a @Player@ component, each with -- a @Coins Int@ component storing how many coins they have collected. You have coins in the world, marked by @Coin Int@, and want to -- iterate over all players, then over all coins, and, if the coin is in range of the player, despawn the entity and add its value to the -- total collected by the player. -- -- This \"nested query\" is a problem you may encounter in a lot of places. You've already seen how it's solved imperatively, by actually nesting the queries, -- but that solution isn't great. It's highly imperative and mutable, and the indentation gets bad as you need to nest more and more queries. -- -- This is where the monadic part of the queries comes in. The goal is to perform all those operations /inside/ the same query, smartly chaining them using /do/. -- -- As a first step it's often helpful to start with a code like this: -- -- @ -- 'query_' $ do -- (entity, 'From' _ tile, Coins coins) \<- ['q'|Entity, OnTile -\> (Entity), Coins / With Player|] -- 'pure' () -- @ -- -- Just copy the main query that you're iterating over. And /bind/ the data out of it using @<-@. -- Wrap it in a @do@ folllwing a @query_@ or @query@. -- -- At this point, you've put your hand on a slice of the data flowing through the query. What you do with this data -- dictates what will happen with all data. -- -- Next, try replacing the @pure@ with another query, typically the second query that you're iterating over: -- -- @ -- 'query_' $ do -- (entity, 'From' _ tile, Coins coins) \<- ['q'|Entity, OnTile -\> (Entity), Coins / With Player|] -- -- ['q'|Coin / With OnTile -\> tile|] -- & 'qtap' (\\e _ -> 'despawn' e) -- @ -- -- This is already a valid query. You're iterating over all the coins that are on each player's tile. You've changed the source, or subject, -- from being the players to being the coins. But in this case that's not really what you want. You want to somehow collect or aggregate -- the coins to use them in the larger query. -- -- Let's try collecting them first: -- -- @ -- 'query_' $ do -- (entity, 'From' _ tile, Coins coins) \<- ['q'|Entity, OnTile -\> (Entity), Coins / With Player|] -- -- collectedCoins <- 'qcollect' $ do -- ['q'|Coin / With OnTile -\> tile|] -- & 'qtap' (\\e _ -> 'despawn' e) -- -- pure () -- @ -- -- The @qcollect@ function is, in a way, the opposite of the bind operation. It returns from a loop by aggregating all its results into a list. -- You may see that @collectedCoins@ has type @[From Coin]@. -- -- Now we need to calculate the actual sum of all coins. We can use a fold for that: -- -- @ -- let totalCoins = 'foldr' (\\('From' _ (Coin x)) -> (+ x)) coins collectedCoins -- @ -- -- In order to update the Coins component you have a few options. -- -- Firstly, you can use @qpure entity@ (or @pure () & qrefocus entity@) to bring the attention back to the player and run a @qinsert@ on it: -- -- @ -- 'query_' $ do -- (entity, 'From' _ tile, Coins coins) \<- ['q'|Entity, OnTile -\> (Entity), Coins / With Player|] -- -- collectedCoins <- 'qcollect' $ do -- ['q'|Coin / With OnTile -\> tile|] -- & 'qtap' (\\e _ -> 'despawn' e) -- -- pure () -- @ -- -- So this is what you want to be doing instead: -- -- @ -- 'query_' $ do -- (entity, 'From' _ tile, Coins coins) \<- ['q'|Entity, OnTile -\> (Entity), Coins / With Player|] -- -- ['q'|Coin / With OnTile -\> tile|] -- & 'qtap' (\\e _ -> 'despawn' e) -- & 'qcollect' entity -- & 'qinsert' (\\collectedCoins -> Coins $ 'foldr' (\\('From' _ (Coin x)) -> (+ x)) coins collectedCoins) -- -- let totalCoins = 'foldr' (\\('From' _ (Coin x)) -> (+ x)) coins collectedCoins -- -- 'qpure' entity -- & qinsert (const $ Coins totalCoins) -- @ -- -- That's it! The coins are now folded and added to the total held by the player. -- -- You can also iterate over the \"null entity\" and perform an insertion manually: -- -- @ -- 'pure' () -- & 'qtap' (\\_ _ -> 'insert' (Coins totalCoins) entity) -- @ -- -- Or you can use @qlift@ which does exactly this! -- -- @ -- 'qlift' $ 'insert' (Coins totalCoins) entity -- @ -- $extend -- One powerup monadic queries give you is that they let you seamlessly read components and resources without unwrapping Maybes. -- -- @ -- 'query_' $ do -- (entity, health) <- ['q'|Entity, Health|] -- (names, player) <- 'qget' entity ['qd'|Likes -> (Names), Player|] -- someRes <- 'qres' \@SomeRes -- 'pure' () -- @ -- -- @qget@ will grab the respective Query Data from the given entity. Due to the nature of the monad, your query will be nullified -- if any of these componetns don't exist. You would be iterating over an empty list! -- -- @qres@ works the same way but grabs a resource. -- $lifting -- You can lift systems into the query monad by using @qlift@: -- -- @ -- 'query_' $ do -- (player, Health x) <- ['q'|Entity, Health / With Player|] -- 'qlift' $ 'insert' (Health $ x + 1) player -- @ -- -- This, essentially, allows you to switch between the two /access modes/. One where you can easily query and grab new data, and one -- where you can perform fine-grained changes on said data.