mischief-assets
Safe HaskellNone
LanguageGHC2024

Mischief.Assets.Tutorial.Introduction

Description

Synopsis

    Chapter 1: Introduction to Assets

    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 an Asset

    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.

    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 the Asset

    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-Reloading the Asset

    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 Assets Into a Game

    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.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!

    Next Chapter: Asset Refs