| 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
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
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 = NameStringderiving (Component)
Consider this system that prints the name of a given Entity:
printName ::Entity->System() printName e =info. T.show=<<gete [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"
Operations
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:
only inserts components that aren't already on the entity.insertNewonly insert components if they aren't on the entity of if their value differs from the current one.insertIfNeq
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.
Querying
Components can be queried using the , C, and M markers.Has
simply returns the component, and makes the query ignore all entities that don't have it:C
x <-query$mkQuery(C@Name,C@Health)
x <- [q|Name, Health|]
x :: [(Name, Health)]
is short for MMaybe 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)]
is similar to Has but it will return a bool saying whether the component is present or not.M
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.
Change Detection
Change detection can be done in two ways: Observers and Filters.
Observers
Observers can listen to the OnAdd, OnSet, and OnRemove event:
OnAdd cis triggered when the component c is added to an entity that didn't previously have it.
onNameAdd ::OnAddName->System()
OnSet cis triggered each timecis inserted on an entity, whether it's for the first time or it's a re-insertion to change its value.
onNameSet ::OnInsertName->System()
OnRemoveis triggered when a component is removed from an entity.
onNameRemove ::OnRemoveName->System()
All of these events have a .entity field you can use to obtain the entity which the event was triggered on.
onNameRemove ::OnRemoveName->System() onNameRemove event =info$ T.showevent.entity <> " had their name removed!"
Don't forget to spawn an Observer to listen for each event.
void$spawn(ObserveronNameAdd)void$spawn(ObserveronNameRemove)
The order these events are triggered in is also an important detail:
- After a component has been added,
OnAddis triggered, followed byOnSet. - After a component has been re-inserted,
OnSetis triggered. - Before a component is removed,
OnRemoveis triggered.
You can find more details on Observers and Events in the corresponding Chapter.
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 , which will only perform insertion if the value of the component is different
from the current one.insertIfNeq
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's value on its own meta entity.
res @MyRes
Is equivalent to:
m <-meta@MyResgetm [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") ainsertRes(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",Froma (Name "A2")) b
Equivalent to:
a <-spawn(Name "A") b <-spawn(Name "B")insert(Name "B2") binsert(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 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 Default typeclass, either through a Generic 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' meta 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 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.