mischief-ecs
Safe HaskellNone
LanguageGHC2024

Mischief.ECS.Tutorial.Guides.QueryPiping

Description

Synopsis

    Learn You an ECS for Great Mischief! - 2.5. Query Piping

    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, Filters, and Insertion

    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.

    Extending Queries

    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_
    

    Hierarchy Traversal

    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.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.

    Impure Piping

    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.

    Making Your Own Pipes!

    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.

    Next Chapter: Monadic Queries