| Safe Haskell | None |
|---|---|
| Language | GHC2024 |
Mischief.ECS.Tutorial.Components
Description
This module contains a more in-depth tutorial on Mischief Components.
Synopsis
Learn You an ECS for Great Mischief! - 4. Components
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 = HealthIntderiving (Component)
Marker components.
data Player = Player deriving (Component) data Enemy = Enemy deriving (Component)
The Name Component
is a special component provided by Mischief that is internally added to every spawned entity, based on its NameEntity index,
if none is provided on spawn. It can be, of course, changed at any time.
newtype Name = NameStringderiving (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"
Operations
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:
* only inserts components that aren't already on the entity.
* insertNew only insert components if they aren't on the entity of if their value differs from the current one.insertIfNeq
Query Results
A is a wrapper around the component Result cc that's produced by a query. We will discuss querying itself more in the Query Chapter.
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) -> domodifyhealth $ (Health x) -> Health (x + 1)
health <-query(C@Health,E)for_health $ ((Health x), entity) -> doinsert(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::Resultc -> c
For instance:
Justname <-get(C @Name) e let name' =valuename
name ::ResultNamename' ::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}
Justpos <-get@Pos e let x = pos.x let y = pos.y
pos ::ResultPos x ::Floaty ::Float
Some typeclasses, namely Show, Eq, Ord are also implemented for a if they are for the underlying Result cc.
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.
Change Detection
Change detection can be done in two ways: Observers and Filters.
Observers
Observers can listen to the and OnInsert event:OnRemove
OnInsert cis triggered each timecis 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 ::OnInsertName->System()
OnRemoveis triggered when a component is removed from an entity.
onNameRemove ::OnRemoveName->System()
Both events have a .entity field you can use to obtain the entity which it happened on.
onNameRemove ::OnRemoveName->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(ObserveronNameInsert)void$spawn(ObserveronNameRemove)
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 and Changed query filters.Added
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 and insertIfNeq, which will only perform insertion if the value of the component is different
from the current one.setIfNeq
Meta Components
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 = MyResIntderiving (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:
JustmyRes <-res@MyRes
res r returns a because it's possible for the resource to not have been inserted yet.Maybe r
Resources are implemented by inserting a component into its own meta entity.
is the same as doing:res @MyRes
m <-meta@MyResget(C@MyRes) m
Required Components
Each component can require a bundle of other components.
data Player = Player instanceComponentPlayer whererequired=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 typeclass, either through a DefaultGeneric derive or a custom instance.
data Position = PositionIntIntderiving (Component,Generic,Default)
data Health = HealthIntderiving (Component) instanceDefaultHealth wheredef= 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 relationship between the components' entities.Requires
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 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.
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 = CompAIntIntderiving (Component,Show) data CompB = CompBStringderiving (Component,Show) data CompC = CompC deriving (Component,Show) main ::IO() main = do app <-newAppMainPluginrunAppapp data MainPlugin = MainPlugin deriving (Eq) instancePluginMainPlugin whereinit_ = 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) fooremove(C@CompC,C@CompB) bazdespawnbarinfo.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 = PlayerCountIntderiving (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@PlayerCountmodifycount $ changeCount (lengthx)
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 ::OnRemovePlayer ->System() handlePlayerRemove _ = do count <-res@PlayerCountmodifycount $ 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 <-spawnPlayervoid$spawnPlayervoid$spawnPlayerdespawnp
Now let's write a a simple app that makes use of these systems:
import Mischief.ECS.Systems qualified as Systems data Player = Player deriving (Component) main ::IO() main = do app <-newAppMainPluginrunAppapp data MainPlugin = MainPlugin deriving (Eq) instancePluginMainPlugin whereinit_ = doinsertRes$ PlayerCount 0 Systems.addUpdate(updateCount, spawnPlayers)void.spawn$ObserverhandlePlayerRemove
Running it will result in:
[INFO] Just (PlayerCount 0) [INFO] Just (PlayerCount 2) [INFO] Just (PlayerCount 4) ...