mischief-ecs
Safe HaskellNone
LanguageGHC2024

Mischief.ECS.Tutorial.Queries

Description

This module contains a more in-depth tutorial on Mischief Queries.

Previous Chapter: Relationships

Next Chapter: Systems

Main Page

Synopsis

    Learn You an ECS for Great Mischief! - 6. Queries

    Introduction

    Queries are the main way we read component data. They are fully type-checked and based on archetypes, making them quite fast.

    Each query associates an output type to the types you put into it.

    For instance,

    x <- query (C @A, M @B)
    

    Will have an output type of:

    (Result A, Maybe (Result B))
    

    With an actual return type of:

    x :: [Result A, Maybe (Result B)]
    

    Because query actually returns [Output].

    single on the other hand, returns a Maybe Output, only outputting something if there is exactly one entity which matches the query.

    x <- single (C @A, M @B)
    
    x :: Maybe (Result A, Maybe (Result B))
    

    get queries the components of a specific entity:

    x <- get (C @A) e
    
    x :: Maybe (Result A)
    

    get returns Nothing if the entity doesn't exist or if it doesn't contain any of the components.

    Outputs

    Here's a list of the the various markers you can use in queries and each of their output types:

    Entity:

    Components:

    Relationships:

    Transitive:

    Note that the relational queries using Any and the transitive ones will return a single element instead of a list, in the case of that relation being exclusive.

    Val

    Working with Result c can be annoying, especially if you only want to read the value of a component and not mutate anything.

    That's why all query types can be wrapped in a Val in order to extract their values out of the Result:

    query (C @Foo, Val (C @Bar, M @Baz))
    

    Will return:

    [Result Foo, (Bar, Maybe Baz)]
    

    Val maps anything that looks like a Result c to just c, while leaving most other things be as they are.

    Filters

    Filters can be passed to ' variants of query functions, such as query' and single'.

    There is an implicit and between filters. (A, B) means A and B. If you wish to express A or B, you can write it as A |. B. Not can be used to negate filters.

    (A |. Not (B, C)) means A or (not (B and C)).

    Most filters expect a tuple of C and R types. These are all valid filters:

    These filters are:

    The Check Filter

    Check is a special filter which takes a f :: c -> Bool function and only accepts entities for which f applied over the c component is True. Naturally, all entities that don't contain the c component will fail.

    For instance, here's how we can select all entities named "Bob":

    query' E (Check (== Name "Bob"))
    

    For relationships, you must use the dedicated CheckR variant which also expects an entity or Any.

    Selecting all entities which Like alice more than 5:

    query' E (CheckR alice (> Likes 5))
    

    Selecting all entities which like any other entity more than 10:

    query' E (CheckR Any (> Like 10))
    

    Quasi-Queries

    Quasi-Queries are queries written via a special quasi-quoter. Make sure to have the QuasiQuotes and TemplateHaskell langauge extensions enabled in order to use them.

    Components

    Here's how we can rewrite a simple componnet query in quasi form:

    query (C @Foo, C @Bar)
    
    [q|Foo, Bar|]
    

    As you can see, a C @c becomes c.

    Relationships

    What about relationships?

    query (R @Foo e, R @Bar Any)
    
    [q|Foo -> e, Bar -> *|]
    

    R @c a is translated to c -> a, and Any becomes *.

    Transitive

    Transitive queries are written the same as relationship ones, but with () around their target:

    query (R @Foo (Q (C @Bar)))
    
    [q|Foo -> (Bar)]
    

    Modifiers

    Quasi-Queries also accept Maybe and Has modifiers:

    query (M @Foo, HasR @Bar Any)
    
    [q|Maybe Foo, Has Bar -> *|]
    

    Since Quasi-Queries are parsed internally by Mischief, alternative symbols are allowed:

    • Maybe | maybe | M | m
    • Has | has | H | h

    So the above query can also be written as:

    [q|M Foo, H Bar -> *|]
    

    You don't need to worry about the distinction between M and MR and so on, the parer will infer which to use.

    Val

    Val is accepted in quasi notation too:

    query (Val (C @Foo, C @Bar))
    
    [q|Val (Foo, Bar)|]
    

    Val can be written as: Val, val, V, v, *.

    So an equivalent way to write the above would be:

    [q|*(Foo, Bar)|]
    

    Entity

    E can be written as: Entity, entity, E, e.

    Filters

    In order to add a filter to a quasi-query, we must separate it with a / from the rest of the query:

    query' (C @Name) (With (C Foo, R Bar Any), Without(C @Baz))
    
    [q|Name / With (Foo, Bar -> *), Without Baz|]
    

    Added and Changed also exists for quasi-queries. All filters can be written either starting with a lower or uppercase letter. For instance, both with and With are correct. Not can also be written as ! and |. can be written as |., ||, or, Or.

    Check

    In quasi notation, Check is unified for both components and relationships. Simply put -> a after it if it's a relationship!

    Getting all entities named "Bob" which like alice more than 5:

    [q|Entity / Check (== Name "Bob"), Check (> Likes 5) -> alice|]
    

    The argument for Check can be any arbitrary lambda function or a function defined outside the quasi-quote.

    Generics

    In order to use a quasi-query for a type with generic parameters, such as:

    data A a b = A deriving (Component)
    

    The entire type must be put in (). For instance:

    [q|Maybe (A Int Float), Likes -> *|]
    

    Other Quasies

    There is also the g Quasi-Query for get, and s for single:

    [g|Name|] alice
    
    [s|Name / with Player|]
    

    Next Chapter: Systems