mischief-ecs
Safe HaskellNone
LanguageGHC2024

Mischief.ECS.Tutorial.App

Description

This module contains a more in-depth look into the Mischief App and Plugins.

It isn't as technically interesting as the other chapters of the tutorial, focusing more on organization and high-level logic.

Previous Chapter: Coding a Dungeon Game

Next Chapter: Components

Main Page

Synopsis

    Learn You an ECS for Great Mischief! - 3. App and Plugins

    The App is a thin wrapper around the World that works as an interface for plugging in various behavior and features in modular fashion, via Plugins.

    A Mischief game / app usually starts by creating an app with a main plugin and then running it.

    main :: IO ()
    main = do
      app <- newApp myPlugin
      runApp app
    
    data MainPlugin = MainPlugin deriving (Eq)
    
    instance Plugin MainPlugin where
      init _ = info "Hello!"
    

    From that initial plugin you can add other plugins, each adding new features to your game.

    A Plugin p instance has two optional functions:

    1. An initialization system that will be ran at the very beginning of the app.
    init :: p -> System ()
    
    1. A collection of plugins that will be added along with this plugin.
    plugins :: p -> Plugins
    

    The collection of plugins can be created using the plug helper function.

    plugins _ = plug (FooPlugin, BarPlugin, BazPlugin)
    

    Additionally, each Plugin also requires an Eq instance, which we'll talk about later.

    Modular Features

    Mischief is intended to let you cleanly separate and organize your logic.

    For instance, you may have a PhysicsPlugin that adds physics to your game, a RenderPlugin which renders objects, a LevelPlugin that spawns your levels.

    instance Plugin MainPlugin where
      plugins _ = plug (PhysicsPlugin, RenderPlugin, LevelPlugin)
    

    Ideally, each of these plugins would add their own independent features. So, if you were to remove physicsPlugin, your entities simply wouldn't move and collide anymore, but the rest of the app would work just fine.

    Note that, internally, PhsyicsPlugin could also be subdivided into its own plugins with separate roles:

    data PhysicsPlugin = PhysicsPlugin deriving (Eq)
    
    instance Plugin PhysicsPlugin where
      plugins _ = collect (CollisionPlugin, MovePlugin)
    

    This also lets the various packages of Mischief be modular, and makes it easy for third party library developers to create plugins that you just plug into your app with ease!

    Initialization

    So what exactly can you do within the init function of a Plugin?

    The answer is, pretty much anything.

    As the name suggests, it is typically used for initializing some sort of logic or behavior within your app. This is usually done by scheduling systems, inserting some resource, and spawning important entities, such as observers.

    init _ = do
      Systems.add $ Update foo `after` bar
      Systems.add $ Startup baz
    
      insertRes $ SomeRes 5
    

    Note that the initialization systems are guaranteed to run before everything else in the app, but there is no guarantee of order between them. So, a Plugin should never depend on another Plugin being added before it.

    Additionally, everything that can be done in init can be done the same in a normal system. There's nothing making it unique!

    Plugin Resolution

    Have you noticed that the Plugin typeclass requires the type to also derive Eq? Let's dig into that.

    To give a practical example, think of an SDLPlugin which initializes an SDL program, opening an actual window and allowing for things such as collecting input and rendering stuff.

    data SDLPlugin = SDLPlugin deriving (Eq)
    
    instance Plugin SDLPlugin where
      init _ = createSDLWindow (Resolution (500, 500))
    

    Two separate Plugins exist, an InputPlugin which handles reading input events, and a RenderPlugin which handles processing shaders and rendering. Both those Plugins need SDLPlugin to function properly.

    data InputPlugin = InputPlugin deriving (Eq)
    
    instance Plugin InputPlugin where
      plugins _ = plug SDLPlugin
    
    data RenderPlugin = RenderPlugin deriving (Eq)
    
    instance Plugin RenderPlugin where
      plugins _ = plug SDLPlugin
    

    So it's a handy feature to have multiple plugins depend on the same plugin. But what if SDLPlugin looked like this:

    data SDLPlugin = SDLPlugin {res :: (Int, Int)} deriving (Eq)
    
    instance Plugin SDLPlugin where
      init (SDLPlugin {res}) = createSDLWindow (Resolution res)
    

    SDLPlugin now produces different effects based on its value. This means there is a possibility that the following happens:

    data InputPlugin = InputPlugin deriving (Eq)
    
    instance Plugin InputPlugin where
      plugins _ = plug (SDLPlugin (300, 300))
    
    data RenderPlugin = RenderPlugin deriving (Eq)
    
    instance Plugin RenderPlugin where
      plugins _ = plug (SDLPlugin (500, 500))
    

    What if you were to add both InputPlugin and RenderPlugin to your app? Which variant of SDLPlugin would it pick?

    This conflict is why the Plugin typeclass requires Eq. When the same plugin is added into the app twice, their two values will be compared and, if equal, the plugin will be added, otherwise the app will panic.

    This ensures a level of control over how plugins do their initialization, without forcing a plugin to only be added from one source.

    Next chapter: Components