{-# 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!