| Safe Haskell | None |
|---|---|
| Language | GHC2024 |
Mischief.ECS.Tutorial.Systems
Description
This module contains a more in-depth tutorial on using Mischief Systems.
Synopsis
Learn You an ECS for Great Mischief! - 7. Systems
Introduction
A System in Mischief is a Monad that executes operations on a World.
Unlike other ECS's, systems here are fully composable, In fact, most of the functions discussed in this tutorial so far were systems.
For instance, the type of spawn is:
spawn:: (Bundleb) => b ->System()@
Systems can either be ran directly, or they can be added scheduled.
Scheduling
Any System () can be added to a Schedule. This will make the system run when that schedule is ran.
There are two ways of scheduling systems, an automatic and a manual way. Tools for using both are found in Mischief.ECS.Systems.
Automatic
In order to schedule a system we just use the add function:
data SomeSchedule = SomeSchedule deriving (Schedule) systemA ::System()
Systems.add SomeSchedule systemA
This will register systemA and schedule it to run in SomeSchedule.
Systems scheduled this way are considered a unique combination of the actual system and the schedule.
If we were to add systemA to another schedule, it would be considered a different system.
You can also add a tuple of systems directly:
Systems.add SomeSchedule (systemA, systemB)
This function also allows custom ordering between systems. For instance, if we want to schedule a new systemC that happens before systemA and after SystemB:
Systems.addSomeSchedule $ systemC`before`systemA`after`systemB
One very important thing to keep in mind is that a scheduled system is only unique as long as the type you're registering is System ().
For instance, scheduling this system:
systemA ::Int->System()
Systems.add SomeSchedule (systemA 5)
And then another system which we want to run after:
Systems.addSomeSchedule $ systemB`after`(systemA 4)
Will compile fine, but since systemA 5 and systemA 4 are different systems, we've basically told systemB to happen after a system that's not even running.
Both systemB and systemA 5 will run, but there won't be any explicit ordering between them.
A system that's been added with Systems.add can be removed using Systems.remove:
Systems.remove SomeSchedule systemA
Note however, that this will also erase all orderings systemA had with other systems at that point.
If you wish to just temporarily disable a system while keeping its configuration, you can use
Systems. instead. And then use unscheduleSystems. to
re-enable it.schedule
Manual
In order to schedule a system manually you just spawn an entity for it:
foo :: System ()
fooEntity <- Systems.spawn foo
And then link that entity to a schedule via the ScheduledIn relationship.
someSchedule <- Schedules.getSomeScheduleinsert(RelScheduledInsomeSchedule) fooEntity
This will make make your system run along with SomeSchedule. Compared to using Systems.add, this method will not do any sort of bookkeeping for you.
Is is your job to keep track of the spawned system's entity.
Two systems can be ordered by using the Before relationship:
fooEntity <- Systems.spawnfoo barEntity <- Systems.spawnbarinsert(RelBeforefooEntity) barEntity
The above will order bar to happen before foo.
Schedules
Same as systems, Schedules are entities. Each schedule has an associated type, usually empty:
data Update = Update deriving (Schedule)
You can both register and get the the entity of a schedule using Schedules.get:
update <- Schedules.get Update
You can run a schedule using Schedules.run:
Schedules.run Update
This will run all systems currently linked to that Schedule, respecting their ordering.
Mischief has two components: and StartupSchedule which you can add to a schedule to make it automatically run on app startup, respectively each frame.UpdateSchedule
These schedules can also be ordered via (same relationship used for ordering systems).Before
The systems Mischief has by default in Startup:
And in Update:
First is usually reserved for internal systems (such as updating time).
Deferring
Time to learn a very powerful and important primitive:
defer::Systema ->System()
All the systems presented so far in this tutorial had their effect applied immediately. When you write set Name $ Name "Player",
you are immediately mutating the respective component. When you do e <- , you are immediately spawning that entity into the World.spawn ()
takes a system and adds it to an internal list instead of applying it.defer
defer$spawn()
defer$ do e <-spawn()insert(Name"Name") e
You can then use to empty the list of deferred systems, applying all of them.
Mischief automatically runs flush at each flushsync point, usually at the end of each scheduled system.
forkDeref is a useful function that temporarily restricts flush to just the current context:
defer$ aforkDefer$ dodefer$ do b cflush
The above flush will just run b and c. forkDeref will drain all non-flushed systems into the outer context.
There is also a special primitive that immediately returns an deferSpawnEntity you can use but defers the actual spawn.
Parallelism
Parallelism in Mischief happens through the monad.ParSystem
ParSystem is a special variant of System that forbids any mutations to the World.
This will throw a compilation error:
s ::ParSystem() s =void$spawn()
There are generally 2 types of operations allowed in a ParSystem:
- Queries
- Deferred Systems
So for instance, if we want to read and change the name of the player in a ParSystem:
changeName ::ParSystem() changeName = doJustname <-single'(C@Name) (With(C@Player))defer$setname (Name "New Name")
so how can we actually run systems in parallel? There are two main primitives used for it: par and parIter:
par is given a list of and will run each of them in parallel:ParSystem ()
par [foo, bar, baz]
parIter (and parIter_ which ignores the result) applies a ParSystem over the elements of a list. Given a list of Entities, this is how we can get their names in parallel:
entities :: [Entity]
names <-parIterentities $get(C@Name)
These primtiives should only be used in performance which are at the risk of bottlenecking performance.
Asynchronicity
Async in Mischief can be achieved using the runAfter primtitive.
You provide it an IO action that returns an a, and a system which consumes that a.
The IO will be ran fully asychrnously and then will add the system to a special async-friendly deferred list that
will be applied at the first available sync point.
We can look at delay as an example of how this may be useful:
delayd system =runAfter(threadDelayd) (constsystem)
Whcih allows delaying any system by an amount of time:
delay500 $insert(Health 100) player