{-# OPTIONS_GHC -Wno-unused-imports #-} -- | -- Module: Systems Tutorial -- Description: Tutorial on using @Systems@ -- -- This module contains a more in-depth tutorial on using @Mischief Systems@. -- -- [Previous Chapter: Queries]("Mischief.ECS.Tutorial.Queries") -- -- [Next Chapter: Events and Messages]("Mischief.ECS.Tutorial.Events") -- -- [Main Page]("Mischief.ECS") module Mischief.ECS.Tutorial.Systems ( -- * Learn You an ECS for Great Mischief! - 7. Systems -- * Introduction -- $intro -- * Scheduling -- $scheduling -- * Schedules -- $schedules -- * Deferring -- $deferring -- * Parallelism -- $par -- * Asynchronicity -- $async -- * [Next Chapter: Events and Messages]("Mischief.ECS.Tutorial.Events") ) where import Control.Concurrent (threadDelay) import Control.Monad.Reader import Data.Foldable (for_) import Mischief.ECS -- $intro -- 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' :: ('Bundle' b) => 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]("Mischief.ECS.Systems").'Mischief.ECS.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]("Mischief.ECS.Systems").'Mischief.ECS.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]("Mischief.ECS.Systems").'Mischief.ECS.Systems.add' SomeSchedule $ 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]("Mischief.ECS.Systems").'Mischief.ECS.Systems.add' SomeSchedule (systemA 5) -- @ -- -- And then another system which we want to run after: -- -- @ -- [Systems]("Mischief.ECS.Systems").'Mischief.ECS.Systems.add' SomeSchedule $ 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]("Mischief.ECS.Systems").'Mischief.ECS.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]("Mischief.ECS.Systems").'Mischief.ECS.Systems.unschedule'@ instead. And then use @[Systems]("Mischief.ECS.Systems").'Mischief.ECS.Systems.schedule'@ to -- re-enable it. -- -- == Manual -- -- In order to schedule a system manually you just spawn an entity for it: -- -- @ -- foo :: 'System' () -- @ -- -- @ -- fooEntity <- [Systems]("Mischief.ECS.Systems").'Mischief.ECS.Systems.spawn' foo -- @ -- -- And then link that entity to a schedule via the @ScheduledIn@ relationship. -- -- @ -- someSchedule <- [Schedules]("Mischief.ECS.Schedules").'Mischief.ECS.Schedules.get' SomeSchedule -- 'insert' ('Rel' 'ScheduledIn' someSchedule) 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]("Mischief.ECS.Systems").'Mischief.ECS.Systems.spawn' foo -- barEntity <- [Systems]("Mischief.ECS.Systems").'Mischief.ECS.Systems.spawn' bar -- -- 'insert' ('Rel' 'Before' fooEntity) 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]("Mischief.ECS.Schedules").'Mischief.ECS.Schedules.get' Update -- @ -- -- You can run a schedule using @Schedules.run@: -- -- @ -- [Schedules]("Mischief.ECS.Schedules").'Mischief.ECS.Schedules.run' Update -- @ -- -- This will run all systems currently linked to that Schedule, respecting their ordering. -- -- Mischief has two components: @'StartupSchedule'@ and @'UpdateSchedule'@ which you can add to a schedule to make it automatically run on app startup, respectively each frame. -- -- These schedules can also be ordered via @'Before'@ (same relationship used for ordering systems). -- -- The systems Mischief has by default in Startup: -- -- * 'PreStartup' -- * 'Startup' -- * 'PostStartup' -- -- And in Update: -- -- * 'First' -- * 'PreUpdate' -- * 'Update' -- * 'PostUpdate' -- -- @First@ is usually reserved for internal systems (such as updating time). -- @The startup schedules@ run once at the start of the app, before any Update, in this order: -- * 'PreStartup' -- * 'Startup' -- * 'PostStartup -- -- @The update shchedules@ run once every frame, in this order: -- -- * 'First' -- * 'PreUpdate' -- * 'Update' -- * 'PostUpdate' -- 'Last' -- -- Note that @First@ and @Last@ are reserved mostly for ECS internal logic and should generally be avoided. -- -- In order to add a system @s :: 'System' ()@ to a schedule @sc@, you can use this 'Plugin': -- -- @ -- 'addSystems' :: ('Schedule' sc, 'SystemConfig' s) => sc -> s -> 'Plugin' () -- @ -- -- @'SystemConfig'@ is a typeclass that allows you to provide stuff like a @'System' ()@, a tuple of systems, or a system ordered to happen after / before another system. -- -- @ -- 'addSystems' 'Update' a -- @ -- -- @ -- 'addSystems' 'Update' (a, b) -- @ -- -- @ -- 'addSystems' 'Update' $ a '`after`' b -- @ -- -- @ -- 'addSystems' 'Update' $ (a, b) '`after`' c '`before`' d -- @ -- -- Keep in mind that when you add @a '`after`' b@, this will just add @a@ to the schedule; @b@ needs to be added separately, and doesn't need -- to be in the same plugin. It's even possible to create and order a system to run before a system defined internally by Mischief, as long as -- that system is exported and visible to you. -- -- Additionally, these are equivalent: -- -- @ -- 'addSystems' b -- 'addSystems' 'Update' $ a `'after'` b -- @ -- -- @ -- 'addSystems' 'Update' $ a `'after'` b -- 'addSystems' b -- @ -- -- Note that when you order systems, they will be ordered only in the context of the current schedule. -- So @'addSystems' 'Update' $ a '`after`' b@ will order @a@ after the system @b@ that's in Update, if there is one at the point of running the app. -- -- Also, a system can be added to any number of schedules. Adding a system twice to the same schedule will have no effect. -- $deferring -- Time to learn a very powerful and important primitive: -- -- @ -- 'defer' :: 'System' a -> '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 <- 'spawn' ()@, you are /immediately/ spawning that entity into the World. -- -- @'defer'@ takes a system and adds it to an internal list instead of applying it. -- -- @ -- 'defer' $ 'spawn' () -- @ -- -- @ -- 'defer' $ do -- e <- 'spawn' () -- 'insert' ('Name' \"Name\") e -- @ -- -- You can then use @'flush'@ to empty the list of deferred systems, applying all of them. -- Mischief automatically runs @'flush'@ at each @sync point@, usually at the end of each scheduled system. -- -- @forkDeref@ is a useful function that temporarily restricts @flush@ to just the current context: -- -- @ -- 'defer' $ a -- -- 'forkDefer' $ do -- 'defer' $ do -- b -- c -- 'flush' -- @ -- -- 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 @'deferSpawn'@ primitive that immediately returns an @Entity@ you can use but defers the actual spawn. -- $par -- @Parallelism@ in Mischief happens through the @'ParSystem'@ monad. -- -- @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 = do -- 'Just' name <- 'single'' ('C' \@Name) ('With' ('C' \@Player)) -- 'defer' $ 'set' name (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 @'ParSystem' ()@ and will run each of them in parallel: -- -- @ -- '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 <- 'parIter' entities $ 'get' ('C' \@Name) -- @ -- -- These primtiives should only be used in performance which are at the risk of bottlenecking performance. -- $async -- 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: -- -- @ -- 'delay' d system = 'runAfter' ('threadDelay' d) ('const' system) -- @ -- -- Whcih allows delaying any system by an amount of time: -- -- @ -- 'delay' 500 $ 'insert' (Health 100) player -- @