mischief-render
Safe HaskellNone
LanguageGHC2024

Mischief.Render.Tutorial.Start.Start

Description

Synopsis

    1.1. Starting Out

    This chapter will walk you through rendering and moving a few sprites.

    Throughout this tutorial we will be using various resources from the assets/ folder in the repo.

    You can find the entire code here.

    Your First Sprite

    Download characters.png from the above link and place it in an assets/ folder at the root of your project. These are sprites from a free asset pack on Kenney.

    Create a new Mischief app, such as this:

    import Mischief.ECS
    import Mischief.Math
    import Mischief.Assets
    import Mischief.Render
    import Mischief.Input
    import Mischief.Materials.Sprite(SpriteMaterial)
    
    main :: IO ()
    main = do
      app <- newApp
      setAssetSource "assets/" app
      addPlugin @MainPlugin app
      runApp app
    
    data MainPlugin
    
    instance Plugin MainPlugin where
      init = pure ()
    
      deps = [dep @RenderPlugin, dep @InputPlugin, dep @TimePlugin]
    

    RenderPlugin is necessary for rendering. InputPlugin and TimePlugin we'll be using later for movement. .

    Now let's write a setup system that spawns a window, camera, and a sprite:

    setup :: System ()
    setup = do
      image <- load @Image "characters.png"
    
      window <- spawn Window
      spawn_ (Camera, Rel OutputTo window)
    
      spawn_
        ( Sprite {image},
          WithMaterial @SpriteMaterial
        )
    

    Don't forget to schedule it in Startup!

    Additionally, we also need to create a system that renders the sprites:

    renderSprites :: System ()
    renderSprites = renderMaterial @SpriteMaterial def
    

    And schedule it to run as well:

    init = do
      systems setup
        & schedule @Startup
    
      systems renderSprites
        & schedule @RenderUpdate
    

    If you run your app now, you'll see our sprite rendered, but there's an issue! The characters.png file is a spriesheet containing multiple characters and animations. The whole thing is being rendered!

    In order to limit our sprite to just a part of the texture, we can insert a SpriteSlice. Inside it, we say where the frame we want to select starts, along its size, in pixels.

    spawn_
      ( Sprite {image},
        SpriteSlice (V2 0 387) (V2 128 128),
        WithMaterial @SpriteMaterial
      )
    

    0 387 is the position of the idle frame of the pink character in our spritesheet. Don't worry about getting the pixels yourself, we'll soon be doing this automatically through the animator.

    Alright, we are now properly rendering a sprite!

    Your Second Sprite

    Let's give our sprite a friend! In order to position them nicely and symmetrically, we should change the transform of the first sprite to be left by some amount, and the transform of the second to be right by that same amount.

    This is our new spawn logic:

    spawn_
      ( Sprite {image},
        SpriteSlice (V2 0 387) (V2 128 128),
        WithMaterial @SpriteMaterial,
        def {translation = V3 (-60) 0 0}
      )
    
    spawn_
      ( Sprite {image},
        SpriteSlice (V2 645 129) (V2 128 128),
        WithMaterial @SpriteMaterial,
        def {translation = V3 60 0 0}
      )
    

    Alright we've successfully rendered another sprite!

    Moving Your Sprite

    Next, let's set up a system that moves the left sprite based on keyboard input.

    First, we should make sure to mark it in some way to identify it. We can do so by creating and inserting a Player component.

    data Player = Player deriving (Component)
    

    spawn_ ( Sprite {image}, SpriteSlice (V2 0 387) (V2 128 128), WithMaterial @SpriteMaterial, def {translation = V3 (-60) 0 0}, Player ) @

    You should now qualify these 3 modules. They contain utilities for processing input, working with transforms, and getting the time.

    import Mischief.Input.Keys qualified as Keys
    import Mischief.Math.Transform qualified as Transform
    import Mischief.ECS.Time qualified as Time
    

    Alright now we can finally write our movement system:

    movement :: System ()
    movement = do
      Just keys <- res @Keys
      Just (player, transform) <- single [q|Entity, Transform / With Player|]
    
      delta <- Time.delta
      let speed = 150
    
      let dirX =
            if
              | Keys.pressed keys Keys.A -> -1
              | Keys.pressed keys Keys.D -> 1
              | otherwise -> 0
    
      let dirY =
            if
              | Keys.pressed keys Keys.S -> -1
              | Keys.pressed keys Keys.W -> 1
              | otherwise -> 0
    
      let dir = normalize $ V3 dirX dirY 0
    
      insert (Transform.move (dir ^* delta * speed) transform) player
    

    It is relatively straightfoward. We get the Keys resources, along with the entity and transform of our player. The current delta time. We also set an arbitrary speed. Based on the input we set the directions on the X and Y axis respectively. We put them in a V3 and normalize them. We then move the player's transform by that V3 multiplied by the delta and the speed.

    Don't forget to schedule this:

    systems movement
      & schedule @Update
    

    If you run the app now, you should be able to move the sprite!

    Sorting Your Sprites

    You may have noticed a small problem with our current app. If you overlap the two sprites, one of them will always be behind the other.

    The solution is to render the material as sorted instead. We need to provide a function that, for each entity, will pick a key to sort by. The simplest value to use is the sprites' y transform. That will make a sprite render under another if it is placed higher.

    Let's write a function that picks the y translation of each sprite:

    getY :: Entity -> Query System Float
    getY entity = do
      Transform {translation} <- [q|entity. Transform|]
      pure (-translation.y)
    

    Now we can replace our sprite rendering system with this:

    renderSprites :: System ()
    renderSprites = renderSortedMaterial @SpriteMaterial def getY
    

    Your sprites should now be getting sorted as you move them around. However, you will notice they still don't look quite right. Each sprite's transform is realtive to the center of the frame by default. Meaning they get sorted by their centers, rather than by the position of their feet, as you'd expect.

    We can solve this easily by inserting an Anchor on each sprite:

    Anchor $ V2 0.5 0.9
    

    Everything should be perfect now!

    Next Chapter: Your First Animation