mischief-assets-0.1.0.0: src/Mischief/Assets/Tutorial/Introduction.hs
{-# OPTIONS_GHC -Wno-unused-imports #-}
-- |
-- [Next Chapter: Asset Refs]("Mischief.Assets.Tutorial.Refs")
--
-- [Main Page]("Mischief.Assets")
module Mischief.Assets.Tutorial.Introduction
( -- * Chapter 1: Introduction to Assets
-- $intro
-- * Defining an Asset
-- $defining
-- * Loading the Asset
-- $loading
-- * Hot-Reloading the Asset
-- $hot
-- * Integrating Assets Into a Game
-- $integrating
-- * [Next Chapter: Asset Refs]("Mischief.Assets.Tutorial.Refs")
)
where
import Control.Monad
import Mischief.Assets
import Mischief.ECS
-- $intro
-- This chapter will walk you through creating and loading your first asset, as well as hot-reloading it and integrating it into the ECS.
--
-- We'll start by creating an app:
--
-- @
-- import Mischief.Assets
-- import Mischief.ECS
--
-- main :: 'IO' ()
-- main = do
-- app <- 'newApp'
-- 'addPlugin' \@MyPlugin app
-- 'runApp' app
--
-- data MyPlugin
--
-- instance 'Plugin' MyPlugin
-- @
--
-- It's good practice to set an /asset source/ at the start of your program. This will be the place where you'll be loading and saving assets.
--
-- @
-- app <- 'newApp'
-- 'setAssetSource' \"assets/\" app
-- ...
-- @
--
-- Make sure to create an @assets@ folder. This is generally relative to where you're executing the program from.
-- $defining
-- For this tutorial we'll use @.kdl@ (pronounced '\cuddle'\), as it is the official file format used by Mischief. You can read more about it [here](https://kdl.dev/spec/).
--
-- We'll define an asset that looks like this:
--
-- @
-- heal {
-- amount 5
-- }
-- @
--
-- Just a simple @heal@ node with an @amount@ child containing an integer.
--
-- Create a @heal.kdl@ file in your assets folder and copy the above content into it.
--
-- Next, we need a type in Haskell to represent that data. @Component@ is necessary for all assets.
-- @KdlAsset@ is a tool we use to conver the kdl into this form for us.
--
-- @
-- newtype Heal = Heal {amount :: 'Int'}
-- deriving ('Generic', 'KdlAsset', 'Show', 'Component')
-- @
--
-- Now to write the actual @Asset@ instance:
--
-- @
-- instance 'Asset' Heal where
-- type 'Saveable' Heal = 'False'
-- 'loadAsset' = 'loadKdl' \"heal\"
-- @
--
-- We do two things here: First, we set @Saveable@ to False. We just don't care about saving this asset. Second,
-- we tell it how to actually load the asset. We use the @loadKdl@ function provided by the earlier @KdlAsset@ instance.
-- We need to also give it the name of the node we want it to load. In our case that's \"heal\".
-- $loading
-- We can load an asset using the @load@ function! Let's write a system that does that.
--
-- @
-- loadHeal :: 'System' ()
-- loadHeal = 'load_' \@Heal \"heal.kdl\"
-- @
--
-- Since assets are loaded aysnchronously, we also need a way to react to the asset being loaded if we want to print it and
-- check that everything's working. We can do that using the @OnLoad@ event:
--
-- @
-- onLoadHeal :: 'OnLoad' Heal -> 'System' ()
-- onLoadHeal ('OnLoad' e) =
-- ['q'|e. Heal|]
-- & 'qinfo' (\\h -\> ['i'|Loaded #{h}|])
-- & 'query_'
-- @
--
-- And of course, we need to set these to run:
--
-- @
-- instance 'Plugin' MyPlugin where
-- 'init' = do
-- 'systems' loadHeal
-- & 'schedule' \@'Startup'
--
-- 'spawn_' $ 'Observer' onLoadHeal
-- @
--
-- When running your app, you should now see this printed to your screen:
--
-- @
-- [INFO] Loaded Heal {amount = 5}
-- @
-- $hot
-- Pretty annoying that we need to restart our program after modifying our asset in order to load it again..
--
-- But that's why we have hot-reloading! Just add the @FileWatcherPlugin@ as a dependency to your plugin:
--
-- @
-- instance 'Plugin' MyPlugin where
-- 'deps' = ['dep' \@'FileWatcherPlugin']
-- @
--
-- Note that, on Windows, you may need to run your project with the @-fthreaded@ flag for this to work properly.
--
-- Now, while running your app, try changing the amount in the @heal.kdl@ file. You'll see the new value printed
-- to the screen immediately!
-- $integrating
-- Let's spawn a few players, each having a @Health@ component:
--
-- @
-- newtype Health = Health 'Int' deriving ('Component')
--
-- spawnPlayers :: 'System' ()
-- spawnPlayers = do
-- 'spawn_' (Name \"A\", Health 20)
-- 'spawn_' (Name \"B\", Health 30)
-- 'spawn_' (Name \"C\", Health 60)
-- @
--
-- Now let's write a system that, when it runs, increases each player's health by the value currently loaded inside Heal:
--
-- @
-- healPlayers :: 'System' ()
-- healPlayers = 'query_' $ do
-- Heal heal <- 'qasset' \@Heal \"heal.kdl\"
-- (player, name, Health hp) \<- ['q'|Entity, Name, Health|]
-- 'qlift' $ do
-- 'insert' (Health $ hp + heal) player
-- 'info' ['i'|Player #{name} is now at #{hp + heal} hp|]
-- @
--
-- Inside a query, we are grabbing each player's name and health, along with the Heal asset. We are then inserting the updated health
-- on the player and printing it.
--
-- All that's left is scheduling this to run at a fixed interval:
--
-- @
-- import "Mischief.ECS.Interval" qualified as Interval
-- @
--
-- @
-- instance 'Plugin' MyPlugin where
-- 'init' = do
-- 'systems' spawnPlayers
-- & 'schedule' \@'Startup'
--
-- 'void' $ Interval.'Mischief.ECS.Interval.start' 1000000 healPlayers
-- @
--
-- Note that we don't need to manually load the file anymore. Asset queries are /active/ by default,
-- meaning they will trigger a load for us if the asset we're asking for is not currently loaded.
--
-- You should see a bunch of these printed to your screen:
--
-- @
-- [Info] Player A is now at 24 hp
-- [Info] Player B is now at 34 hp
-- [Info] Player C is now at 64 hp
-- @
--
-- Try toying with the heal amount in the file, and see how that affects the amount players heal each second!