packages feed

mischief-ecs-0.1.0.0: src/Mischief/ECS/Tutorial/Systems.hs

{-# 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
    -- $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
-- Mischief has a couple of primitived that allow running systems asynchronously.
--
-- === runAfter
--
-- Asynchonicity 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
-- @
--
-- === Intervals
--
-- Intervals are another way of running async systems.
--
-- @
-- import "Mischief.ECS.Interval" qualified as [Interval]("Mischief.ECS.Interval")
-- @
--
-- This allows you to set a system to repeatedly run on a fixed interval:
--
--
-- This will print \"Hello!\" once per second:
--
-- @
-- hello <- [Interval]("Mischief.ECS.Interval").'Mischief.ECS.Interval.start' 1000 ('info' \"Hello!\")
-- @
--
-- The interval can be stopped at any time by calling @Interval.stop@ on the returned object:
--
-- @
-- [Interval]("Mischief.ECS.Interval").'Mischief.ECS.Interval.stop' hello
-- @