{-# 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 -- * The Name Component -- $name -- * Operations -- $ops -- * Querying -- $query -- * Change Detection -- $change -- * Meta Components -- $meta -- * Resources -- $resources -- * From -- $from -- * Required Components -- $required -- * Registering Components -- $reg -- * [Next Chapter: Relationships]("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' . T.'Data.Text.show' '=<<' 'get' e ['q'|Name|] -- @ -- -- 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 World: -- -- * 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. -- -- Note that, unlike other ECS's, Mischief exposes an immutable API to the user. This means that component values cannot be -- mutated in any other way, other than re-inserting them to change their previous value. -- $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@. -- $query -- Components can be queried using the @'C'@, @'M'@, and @'Has'@ markers. -- -- @'C'@ simply returns the component, and makes the query ignore all entities that don't have it: -- -- @ -- x \<- 'query' $ 'mkQuery' ('C' \@Name, 'C' \@Health) -- @ -- -- @ -- x \<- ['q'|Name, Health|] -- @ -- -- @ -- x :: [(Name, Health)] -- @ -- -- @'M'@ is short for @Maybe@ and makes the component optional. It may return Nothing, and the query will also include entities that don't have it. -- -- @ -- x \<- 'query' $ 'mkQuery' ('C' \@Name, 'M' \@Health) -- @ -- -- @ -- x \<- ['q'|Name, Maybe Health|] -- @ -- -- @ -- x :: [(Name, 'Maybe' Health)] -- @ -- -- @'Has'@ is similar to @'M'@ but it will return a bool saying whether the component is present or not. -- -- @ -- x \<- 'query' $ 'mkQuery' ('C' \@Name, 'Has' \@Health) -- @ -- -- @ -- x \<- ['q'|Name, Has Health] -- @ -- -- @ -- x :: [(Name, 'Bool')] -- @ -- -- Additionally, the @mkQuery'@ function (or @q@ quasi-quoter, with a separating @\/@) accepts an expression of @With@ / @Without@ filters that limit the entities looked at by the query. -- -- Getting the name of all entities that are @Player@ and either don't have @Enemy@ or have @Health@: -- -- @ -- x \<- 'query' $ 'mkQuery'' ('C' \@Name) ('With' ('C' \@Player) '`And`' ('Without' ('C' \@Enemy) '`Or`' 'With' ('C' \@Health))) -- @ -- -- Or -- -- @ -- x \<- 'query' ['q'|Name / With Player, (Without Enemy || With Health)|] -- @ -- -- Note that these filters limit the /archetypes/ that the query will look at, rather than filtering the entities themselves, making them significantly faster than other filters. -- $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's value on its own meta entity. -- -- @ -- 'res' \@MyRes -- @ -- -- Is equivalent to: -- -- @ -- m <- 'meta' \@MyRes -- 'get' m [q|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 additioanlly 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. -- $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' meta 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 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. -- $change -- Change detection can be done in two ways: @Observers@ and @Filters@. -- -- === Observers -- -- Observers can listen to the @OnAdd@, @OnSet@, and @OnRemove@ event: -- -- * @OnAdd c@ is triggered when the component c is added to an entity that didn't previously have it. -- -- @ -- onNameAdd :: 'OnAdd' 'Name' -> 'System' () -- @ -- -- * @OnSet c@ is triggered each time @c@ is inserted on an entity, whether it's for the first time or it's a -- re-insertion to change its value. -- -- @ -- onNameSet :: 'OnInsert' 'Name' -> 'System' () -- @ -- -- * @OnRemove@ is triggered when a component is removed from an entity. -- -- @ -- onNameRemove :: 'OnRemove' 'Name' -> 'System' () -- @ -- -- All of these events have a @.entity@ field you can use to obtain the entity which the event was triggered on. -- -- @ -- onNameRemove :: 'OnRemove' 'Name' -> 'System' () -- onNameRemove event = 'info' $ T.'Data.Text.show' event.entity <> \" had their name removed!\" -- @ -- -- Don't forget to spawn an Observer to listen for each event. -- -- @ -- 'void' $ 'spawn' ('Observer' onNameAdd) -- 'void' $ 'spawn' ('Observer' onNameRemove) -- @ -- -- The order these events are triggered in is also an important detail: -- -- * /After/ a component has been added, @OnAdd@ is triggered, followed by @OnSet@. -- * /After/ a component has been re-inserted, @OnSet@ is triggered. -- * /Before/ a component is removed, @OnRemove@ is triggered. -- -- You can find more details on Observers and Events in the corresponding [Chapter]("Mischief.ECS.Tutorial.Events"). -- -- === Filters -- -- Now for the other way of reacting to changes: the @Changed@ and @Added@ filters. -- -- With the following query we can query the name of all entities that have had the @Player@ component added in the last frame: -- -- @ -- 'mkQuery' ('C' \@Name) -- & 'qcheck' ('Added' ('C' \@Player)) -- & 'query' -- @ -- -- Or -- -- @ -- ['q'|Name|] -- & 'qcheck' ['f'|Added Player|] -- & 'query' -- @ -- -- @Added c@ will catch entities that just had @c@ added to them, while @Changed c@ will catch any insertion. -- If you wish to query for entities that have had a component changed but it wasn't just added, you can do: -- -- @ -- 'mkQuery' ('C' \@Name) -- & 'qcheck' ('Changed' ('C' \@Player) '`And`' (`Not` ('Added' ('C' \@Player)))) -- & 'query' -- @ -- -- Or -- -- @ -- ['q'|Name|] -- & 'qcheck' ['f'|Changed Player, !Added Player|] -- & 'query' -- @ -- -- === Note on listening to changes -- -- One essential detail to be aware of here is that insertion (@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. -- $hooks -- @Component hooks@ are 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 a @.entity@ field. -- -- The @onAdd@ and @ohSet@ hooks will always run before @OnAdd@ and @OnSet@ events on that component. -- @onRemove@ hooks will always run after @OnRemove@ events.