mischief-ecs
Safe HaskellNone
LanguageGHC2024

Mischief.ECS.Tutorial.Guides.Basics

Description

Synopsis

    Learn You an ECS for Great Mischief! - 2.2. Entities and Components Basics

    Data in an ECS is manipulated (stored, mutated, destroyed), by performing operations on entities. These operations usually involve bundles of components.

    What's in a Bundle?

    A bundle is a tuple formed of "things" that can be attached (or, inserted) to an Entity.

    A bundle can contain:

    • Component Values: Will attach the component.
    (Name "A", Health 100) bob
    
    • Relationships: Will attach a relationship with the given entity, containing the given component.
    (Rel Likes charlie) bob
    
    • Resources: Will insert the given resource into the world. Is agnostic to the entity it's inserted on.
    (Res (FooRes "A"))
    
    • From: Will insert the given component on the given entity. Is agnostic to the entity it's inserted on.
    (From alice (Name "Alice"))
    

    Manipulating Data

    In order to create new data, you spawn an entity with the given data stored in components (through a bundle):

    bob <- spawn (Name "Bob", Player)
    

    You can also attach new data to an entity:

    insert (Health 100, Rel Likes alice) bob
    

    To change data, you re-insert the relevant components to the entity:

    insert (Name "Bob's New Name") bob
    

    To erase data, you remove the components from the entity:

    remove (C @Health, R Likes alice) bob
    

    You can also despawn an entity, erasing all its data:

    despawn bob
    

    Reading Data

    Data can be retrieved through queries.

    You can get Bob's name and health like this:

    Just (name, health) <- single [q|bob. Name, Health|]
    

    You can grab each entity's name and health like this:

    x <- query [q|Name, Health|]
    

    More on queries in the next chapter.

    Required Components

    A component A can require any number of other components, as long as they all instance the Default typeclass.

    data Player
    
    instance Component Player where
      required = require @Health
    
    data Health = Health Int deriving (Component)
    
    instance Default Health where
      def = Health 100
    

    When inserting Player on an entity, Health 100 will be immediately inserted as well, as long as the Health component wasn't already inserted.

    Metadata

    Each component has a corresponding entity in the World. The components on that entity store information about the component itself. Such as which archetypes it is part of.

    A component's entity can be accessed by using meta.

    Getting the entities of the Name and Player components:

    x <- meta @Name
    y <- meta @Player
    

    Most users should avoid tinkering with Meta Components unless they have a good reason to, and should absolutely never remove or change any components added to them by the ECS.

    Resources

    Resources are singleton components that can be easily accessed and modified from any system.

    Any component can be used as a resource.

    data MyRes = MyRes Int deriving (Component)
    

    You can insert a resource into the World using insertRes.

    insertRes $ MyRes 5
    

    And you can query for the value of a resource using res:

    Just myRes <- res @MyRes
    

    Resources are implemented by inserting a component's value on its own meta entity. Which means:

    res @MyRes
    

    Is equivalent to:

    m <- meta @MyRes
    single $ [q|m. MyRes|]
    

    Besides using insertRes, resources can be inserted as part of a bundle through the Res type.

    The following will spawn an entity, insert a Name on it, and additionally insert a resource into the world.

    a <- spawn ()
    insert (Name A, Res $ SomeRes 5) a
    

    It's equivalent to:

    a <- spawn ()
    insert (Name "A") a
    insertRes (SomeRes 5)
    

    Res can also be used in queries to grab a resource:

    mkQuery (C @Name, Res @SomeRes)
    

    Or

    [q|Name, Res SomeRes|]
    

    This will query the name of all entities, and attach Res SomeRes to all of them.

    From

    From is a special type in Mischief:

    data From c = From {entity :: Entity, comp :: c}
    

    It symbolizes the idea of a foreign component. A component belonging to an external entity that we store alongside it.

    From can be inserted in any bundle, inserting the component c on the entity stored inside it. For instance, the following code will insert the name "B2" on b and the name "A2" on a:

    a <- spawn (Name "A")
    b <- spawn (Name "B")
    
    insert (Name "B2", From a (Name "A2")) b
    

    Equivalent to:

    a <- spawn (Name "A")
    b <- spawn (Name "B")
    
    insert (Name "B2") b
    insert (Name "A2") a
    

    From is generally returned from traversal queries such as:

    x <- query [q|Name, ChildOf -> (Name)|]
    
    x :: [Name, From Name]
    

    Where Name will be the name of each entity and From Name will be the name of their parent.

    Registering Components

    Registering a component involves spawning its meta entity and adding the corresponding data.

    Each component is registered automatically the first time it is inserted on an Entity, so you don't usually have to worry about registration.

    Queries are also smart about components; if you query or filter for a component that hasn't been registered yet, they will just assume that component can't be be on any Entity. Queries can't perform registration themselves, because they're not allowed to mutate the world in any way.

    However, there may be extremely niche situations where you want to register components earlier than normal, which is where manual registration comes in:

    register @(Player, Health, Position)
    

    One such situation could be wanting to check the requirements between multiple components on runtime. If a component hasn't been registered yet, it won't show up when you query for components that require a specific component.

    Next Chapter: Query Basics