mischief-ecs
Safe HaskellNone
LanguageGHC2024

Mischief.ECS.Tutorial.Guides.Change

Description

Synopsis

    Learn You an ECS for Great Mischief! - 2.11. Change Detection

    This chapter will show you ways of listening and reacting to changes in the World.

    Events

    There are various pre-defined events that can be used to listen to when a component is added, inserted, or removed:

    onAddName :: OnAdd Name -> System ()
    onAddName OnAdd {entity} = do
      Just name <- single [q|entity. Name|]
      info [i|#{entity} was just added the name #{name}|]
    

    For insertion and removal you can use OnSet and OnRemove respectively.

    There are also OnAddRel, OnSetRel, OnRemoveRel which act the same but can listen to relationship changes.

    For instance:

    onRemoveChildOf :: OnRemoveRel ChildOf -> System ()
    onRemoveChildOf OnRemoveRel {entity, target} =
      info [i|#{entiy} is no longer the child of #{target}|]
    

    Query Filters

    You can use the Changed filter in a query to select the entities that have had a component changed.

    With the following query we can get the name of all entities that have had the Health component changed in the last frame:

    [q|Name / Changed Health|]
      & query
    

    Note however, that the changed marker is set per-archetype table. This means that you might get extra entities, not just those which have had that component changed. This is intended to be used for optimization purposes, and not for reacting to individual changes. Use events for that instead.

    Avoiding False Changes

    One essential detail to be aware of here is that listening to insertion (through OnSet or Changed) doesn't necessarily mean a component's value has been changed!

    The following insert will trigger change detection:

    p <- spawn (Health 100)
    insert (Health 100) p
    

    To avoid this, you can derive Eq on your components and use insertIfNeq, which will only perform insertion if the value of the component is different from the current one.

    Component Hooks

    Hooks are specialized events associated directly to a Component instance.

    instance Component Foo where
      onAdd = [hook onAddFoo]
      onSet = [hook onSetFoo]
      onRemove = [hook onRemoveFoo]
    
    onAddFoo :: HookContext -> System ()
    onAddFoo = ...
    
    onSetFoo :: HookContext -> System ()
    onSetFoo = ...
    
    onRemoveFoo :: HookContext -> System ()
    onRemoveFoo = ...
    

    Similar to change events, HookContext has an .entity field you can use to access the entity the hook was triggered on.

    Relationship support dedicated hooks, named onAddRel, onSetRel, onRemoveRel, following the same rules as the normal hooks.

    This is how we can log a message each time Likes is added between two entities:

    instance Component Likes where
      onAddRel = [hookRel onLikesAdd]
    
    onLikesAdd :: HookContextRel -> System ()
    onLikesAdd HookContextRel {entity, target} = info [i|#{entity} now likes #{target}!|]
    

    There are a number of predefined hooks that are useful when working with relationships, which can be found in Mischief.ECS.HooksRel. For instance, addOther can be used to automate adding a complementary relationship on the target of a relationship.

    As a quick example of why this is useful, this is how you'd create a Before/After relationship between entities:

    data Before = Before
    data After = After
    
    instance Component Before where
      onAddRel =  [HooksRel.addOther (const After)]
    
    instance Component After where
      hooks = [HooksRel.addOther (const Before)]
    

    Now, when you do:

    insert (Rel Before a) b
    

    A Rel After b will be inserted automatically on a.

    And vice versa.

    There are other interesting hooks, such as ones for automatically cleaning up a relationship when an entity is despawned. Check out Mischief.ECS.HooksRel for details!