{-# OPTIONS_GHC -Wno-unused-imports #-} -- | -- Module: Components Tutorial -- Description: Tutorial on using @Components@ -- -- This module contains a more in-depth tutorial on @Mischief Components@. -- -- [Previous Chapter: App and Plugins]("Mischief.ECS.Tutorial.App") -- -- [Next Chapter: Relationships]("Mischief.ECS.Tutorial.Relationships") -- -- [Main Page]("Mischief.ECS") module Mischief.ECS.Tutorial.Components ( -- * Learn You an ECS for Great Mischief! - 4. Components -- * Introduction -- $introduction -- * The Name Component -- $name -- * Operations -- $ops -- * Query Results -- $results -- * Change Detection -- $change -- * Meta Components -- $meta -- * Resources -- $resources -- * Required Components -- $required -- * Registering Components -- $reg -- * Examples -- $examples -- * [Next Chapter: Relationship]("Mischief.ECS.Tutorial.Relationships") ) where import Control.Monad (void) import Data.Default (Default (def)) import Data.Foldable import GHC.Generics (Generic) import GHC.Records (HasField) import Mischief.ECS -- $introduction -- A component is any type which derives the 'Component' typeclass. They can be both carriers of data or marker components used for querying (@Tags@ from @Flecs@): -- -- Component that carries data. -- -- @ -- data Health = Health 'Int' deriving ('Component') -- @ -- -- Marker components. -- -- @ -- data Player = Player deriving ('Component') -- data Enemy = Enemy deriving ('Component') -- @ -- $name -- @'Name'@ is a special component provided by Mischief that is internally added to every spawned entity, based on its @Entity@ index, -- if none is provided on spawn. It can be, of course, changed at any time. -- -- @ -- newtype Name = Name 'String' deriving ('Component') -- @ -- -- Consider this system that prints the name of a given Entity: -- -- @ -- printName :: 'Entity' -> 'System' () -- printName e = 'info' . 'text' '=<<' 'get' ('C' \@'Name') e -- @ -- -- Notice how the Names behave here: -- -- @ -- foo <- 'spawn' () -- printName foo -- -- insert ('Name' \"Foo\") foo -- printName foo -- -- bar <- 'spawn' ('Name' \"Bar\") -- printName bar -- @ -- -- @ -- >> [INFO] Just \"Entity 15v1\" -- >> [INFO] Just \"Foo\" -- >> [INFO] Just \"Bar\" -- @ -- $ops -- -- Mischief offers various @operations@ for inserting and manipulating data into the ECS: -- -- * You can spawn entities as bundles of components: -- -- @ -- player <- 'spawn' (Name \"Player\", Player) -- @ -- -- * You can insert components on existing entities: -- -- @ -- 'insert' (Name \"New Player Name\", Health 100) -- @ -- -- * You can remove components: -- -- @ -- 'remove' ('C' \@Health, 'C' \@Player) player -- @ -- -- * You can despawn entities: -- -- @ -- 'despawn' player -- @ -- -- Additionally, @insert@ has a couple of variants: -- * @'insertNew'@ only inserts components that aren't already on the entity. -- * @'insertIfNeq'@ only insert components if they aren't on the entity of if their value differs from the current one. -- $results -- A @'Result' c@ is a wrapper around the component @c@ that's produced by a query. We will discuss querying itself more in the [Query Chapter]("Mischief.ECS.Tutorial.Queries"). -- -- @ -- health <- 'get' ('C' \@Health) player -- @ -- -- @ -- health :: 'Result' Health -- @ -- -- There are a number of useful operations that can be done on a @Result@: -- -- * Set a new value for this component. -- -- @ -- 'set' health (Health 100) -- @ -- -- * Modify the value of this component. -- -- @ -- 'modify' health (\(Health x) -> Health (x + 1)) -- @ -- -- * Remove the component from the entity. -- -- @ -- 'delete' health -- @ -- -- Note that these functions just call the @insert@, @remove@, etc. operations. -- -- So these two are equivalent, performance-wise: -- -- @ -- health <- 'query' ('C' \@Health) -- -- 'for_' health $ \(health) -> do -- 'modify' health $ \(Health x) -> Health (x + 1) -- @ -- -- @ -- health <- 'query' ('C' \@Health, 'E') -- -- 'for_' health $ \((Health x), entity) -> do -- 'insert' (Health (x + 1)) entity -- @ -- -- Or, if you prefer compact code: -- -- @ -- 'query' ('C' \@Health) '>>=' 'traverse_' ('`modify`' (\(Health x) -> Health (x + 1))) -- @ -- -- You can use the @value@ function to obtain the inner value of a @Result@. -- -- @ -- 'value' :: 'Result' c -> c -- @ -- -- For instance: -- -- @ -- 'Just' name <- 'get' (C \@'Name') e -- let name' = 'value' name -- @ -- -- @ -- name :: 'Result' 'Name' -- name' :: 'Name' -- @ -- -- If the component has record fields, every field will be inherited by the 'Result' (via a 'HasField' instance) -- -- @ -- data Pos = Pos {x :: 'Float', y :: 'Float'} -- @ -- -- @ -- 'Just' pos <- 'get' @Pos e -- let x = pos.x -- let y = pos.y -- @ -- -- @ -- pos :: 'Result' Pos -- x :: 'Float' -- y :: 'Float' -- @ -- -- Some typeclasses, namely 'Show', 'Eq', 'Ord' are also implemented for a @'Result' c@ if they are for the underlying @c@. -- -- Note that the value of a 'Result' is the value gotten at the time of querying. It could be outdated, in case the live value -- was changed after querying. -- $meta -- 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 -- @ -- -- @res r@ returns a @'Maybe' r@ because it's possible for the resource to not have been inserted yet. -- -- Resources are implemented by inserting a component into its own meta entity. -- -- @'res' \@MyRes@ is the same as doing: -- -- @ -- m <- 'meta' \@MyRes -- 'get' ('C' \@MyRes) m -- @ -- $required -- Each component can @require@ a bundle of other components. -- -- @ -- data Player = Player -- -- instance 'Component' Player where -- 'required' = 'require' \@(Position, Health) -- @ -- -- This means that each time @Player@ is added to an entity, a /default/ @Position@ and @Health@ will also be inserted, if they aren't already present. -- -- In order for a component to be required by another, it must instance the @'Default'@ typeclass, either through a @Generic@ derive or a custom instance. -- -- @ -- data Position = Position 'Int' 'Int' deriving ('Component', 'Generic', 'Default') -- @ -- -- @ -- data Health = Health 'Int' deriving ('Component') -- -- instance 'Default' Health where -- 'def' = Health 100 -- @ -- -- Requirements are transitive (if @A requires B@ and @B requires C@, then @A requires C@) and /can/ contain cycles. -- -- A requirement is added to the ECS as a @'RequiredBy'@ \/ @'Requires'@ relationship between the components' entities. -- $reg -- @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 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 in-between multiple components. -- If a component hasn't been registered yet, it won't show up when you query for components that require a specific component. -- $change -- @Change detection@ can be done in two ways: @Observers@ and @Filters@. -- -- === Observers -- -- Observers can listen to the @'OnInsert'@ and @'OnRemove'@ event: -- -- * @OnInsert c@ is triggered each time @c@ is inserted on an entity. This event is also triggered when a -- component is re-inserted / changed, meaning this isn't a reliable way to determine if a component was just added. -- -- @ -- onNameInsert :: 'OnInsert' 'Name' -> 'System' () -- @ -- -- * @OnRemove@ is triggered when a component is removed from an entity. -- -- @ -- onNameRemove :: 'OnRemove' 'Name' -> 'System' () -- @ -- -- Both events have a @.entity@ field you can use to obtain the entity which it happened on. -- -- @ -- onNameRemove :: 'OnRemove' 'Name' -> 'System' () -- onNameRemove event = 'info' $ show event.entity <> \" has their name removed!\" -- @ -- -- Don't forget to spawn an Observer to listen to each event. -- -- @ -- 'void' $ 'spawn' ('Observer' onNameInsert) -- 'void' $ 'spawn' ('Observer' onNameRemove) -- @ -- -- @OnInsert@ is always triggered /after/ a component has been inserted, while @OnRemove@ is triggered /before/. This -- allows you to query for the component and get its value. -- -- == Filters -- -- Now for the other way of doing change detection: the @'Changed'@ and @'Added'@ query filters. -- -- With the following query: -- -- @ -- 'query' ('C' \@'Name') ('Added' ('C' \@Player)) -- @ -- -- You will only obtain the name of entities which had the @Player@ component added to them since the current (scheduled) system last ran. -- -- @Added c@ will catch entities that just had @c@ added to them, while @Changed c@ will catch any insertion, similar to @OnInsert@. -- If you wish to query for entities that have had a component changed but it wasn't just added, you can do: -- -- @ -- 'query' ('C' \@'Name') ('Changed' ('C' \@Player), 'Not' ('Added' ('C' \@Player))) -- @ -- -- == Note on listening to changes -- -- One essential detail to be aware of here is that insertion (@OnInsert@ 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'@ and @'setIfNeq'@, which will only perform insertion if the value of the component is different -- from the current one. -- $hooks -- @Component hooks@ are events associated directly to a Component instance. -- -- @ -- instance Component Foo where -- hooks :: 'Hooks' Foo -- hooks = 'collect' (onInsertFoo, onRemoveFoo, customFoo) -- -- onInsertFoo :: 'OnInsert' Foo -> 'System' () -- onInsertFoo = ... -- -- onRemoveFoo :: 'OnRemove' Foo -> 'System' () -- onRemoveFoo = ... -- -- data SomeCustomEvent a deriving ('Event') -- customFoo :: SomeCustomEvent Foo -> 'System' () -- customFoo = ... -- @ -- -- A Hook on component @c@ can be any function that looks like @('Event' e) => e c -> 'System' ()@. -- -- Hooks are registered along with their respective component. -- $examples -- -- This section contains a few chunkier examples that combine the notions from this entire chapter. -- -- === __Example 1__ -- -- -- An example showing different operations that spawn and alter entities. -- -- @ -- import "Mischief.ECS.Prelude" -- -- data CompA = CompA 'Int' 'Int' deriving ('Component', 'Show') -- -- data CompB = CompB 'String' deriving ('Component', 'Show') -- -- data CompC = CompC deriving ('Component', 'Show') -- -- main :: 'IO' () -- main = do -- app <- 'newApp' MainPlugin -- 'runApp' app -- -- data MainPlugin = MainPlugin deriving ('Eq') -- -- instance 'Plugin' MainPlugin where -- 'Mischief.ECS.App.Plugins.init' _ = do -- foo <- 'spawn' (Name \"Foo\", CompA 10 10, CompB \"Component B on Foo\", CompC) -- bar <- 'spawn' (Name \"Bar\", CompA 15 3, CompB \"Component B on Bar\") -- baz <- 'spawn' (Name \"Baz\", CompA 0 0, CompB \"Component B on Baz\", CompC) -- -- 'info' . 'text' '=<<' 'query' ('C' \@Name, 'C' \@CompA, 'M' \@CompB, 'M' \@CompC) -- -- 'insert' (Name \"Foo2\", CompA 100 100, CompC) foo -- 'remove' ('C' \@CompC, 'C' \@CompB) baz -- 'despawn' bar -- -- 'info' . 'text' '=<<' 'query' ('C' \@Name, 'C' \@CompA, 'M' \@CompB, 'M' \@CompC) -- @ -- -- @ -- >> [Info] [ -- (\"Foo\", CompA 10 10, Just CompB \"Component B on Foo\", Just CompC), -- (\"Baz\", CompA 0 0, Just CompB \"Component B on Baz\", Just CompC), -- (\"Bar\", CompA 15 3, Just CompB \"Component B on Bar\", Nothing) -- ] -- -- >> [Info] [ -- ("\Foo2\", CompA 100 100, Just CompB \"Component B on Foo\", Just CompC), -- (\"Baz\", CompA 0 0, Nothing, Nothing) -- ] -- @ -- -- === __Example 2__ -- -- This example shows you how to make a resource that independently tracks the number of Players in the World. -- -- @ -- data PlayerCount = PlayerCount 'Int' deriving ('Component', 'Show') -- -- changeCount :: 'Int' -> PlayerCount -> PlayerCount -- changeCount n (PlayerCount x) = PlayerCount (x + n) -- @ -- -- We need to write a system that queries all entities that have had a @Player@ component added to them and updates @PlayerCount@ accordingly: -- -- @ -- updateCount :: 'System' () -- updateCount = do -- x <- 'query'' 'E' ('Added' \@Player) -- -- count <- 'res' \@PlayerCount -- 'modify' count $ changeCount ('length' x) -- @ -- -- We couldn't have used an observer for this since 'OnInsert' also catches re-insertions. -- -- We'll also make an Observer that listens to the @OnRemove@ event to update @PlayerCount@: -- -- @ -- handlePlayerRemove :: 'OnRemove' Player -> 'System' () -- handlePlayerRemove _ = do -- count <- 'res' \@PlayerCount -- 'modify' count $ changecount (-1) -- @ -- -- I also wrote this system that spawns 3 Players and despawns one of them each frame, to make sure both the previous sytems works fine. -- -- @ -- spawnPlayers :: 'System' () -- spawnPlayers = do -- ('info' . 'text') '=<<' 'res' \@PlayerCount -- -- p <- 'spawn' Player -- 'void' $ 'spawn' Player -- 'void' $ 'spawn' Player -- -- 'despawn' p -- @ -- -- Now let's write a a simple app that makes use of these systems: -- -- @ -- import "Mischief.ECS.Systems" qualified as [Systems]("Mischief.ECS.Systems") -- -- data Player = Player deriving ('Component') -- -- main :: 'IO' () -- main = do -- app <- 'newApp' MainPlugin -- 'runApp' app -- -- data MainPlugin = MainPlugin deriving ('Eq') -- -- instance 'Plugin' MainPlugin where -- 'Mischief.ECS.App.Plugins.init' _ = do -- 'insertRes' $ PlayerCount 0 -- [Systems]("Mischief.ECS.Systems").'Mischief.ECS.Systems.add' 'Update' (updateCount, spawnPlayers) -- 'void' . 'spawn' $ 'Observer' handlePlayerRemove -- @ -- -- Running it will result in: -- -- @ -- [INFO] Just (PlayerCount 0) -- [INFO] Just (PlayerCount 2) -- [INFO] Just (PlayerCount 4) -- ... -- @ -- $next -- "Mischief.ECS.Tutorial.Relationships"