mischief-render
Safe HaskellNone
LanguageGHC2024

Mischief.Render.Tutorial.Start.Animation

Description

Synopsis

    1.2. Your First Animation

    This is a continuation of the previous chapter where we'll be animating our sprites.

    Looking At The Spritesheet

    First, grab the spritesheet.kdl and animations.kdl from the repo and place them in your assets folder.

    Let's take a moment to understand what these files contain.

    The spritesheet.kdl one describes our spritesheet. It specifies its size, the size of each frame, the padding between frames, and it links to the image from before.

    spritesheet {
      size 896 896
      frameSize 128 128
      padding 0 0
      image "characters.png"
    }
    

    animations.kdl is a bit more intimidating, but it's way simpler than it appears.

    First, it links to the spritesheet:

    spritesheet link="spritesheet.kdl"
    

    Then, it contains a collection of Animation assets:

    collection {
      animation1 {...}
      animation2 {...}
    }
    

    Each animation specifies the indices of its spritesheet frames, the speed of the animation, and specifies whether it loops or not.

    For instance, this is the idle frame of the green character. If you look at frame 14 in the spritesheet, you'll see it is in fact the green character standing still.

    green_idle {
      frames 14
      fps 10
    }
    

    Meanwhile, the walking animation of the green character looks like this:

    green_walk {
      range 16 17
      fps 4
      loop #true
    }
    

    In the following section, we'll need to create various AssetRef Animation to point to each animation. So let's practice that for a bit.

    This is how you can point to green's walk animation:

    greenWalk :: AssetRef Animation
    greenWalk = do
      animations :: Animations <- "animations.kdl"
      pure $ colGetU "green_walk" animations.collection
    

    We can also use the selAnim function to do this more easily:

    greenWalk = selAnim "animations.kdl" "green_walk"
    

    Making an Animator

    In order to make an Animator, we need to make an instance of the AnimatorTag typeclass.

    data MyAnimator = MyAnimator
    
    instance AnimatorTag MyAnimator where
      data Input MyAnimator = Input deriving (Generic, Default)
      data Shift MyAnimator = Shift
      data State MyAnimator = GreenIdle
    
      animation GreenIdle = selAnim "animations.kdl" "green_idle"
    
      start = GreenIdle
    

    The animator has three components: Input, Shift, and State. We'll learn more about the first two later, but for now what you need to know is that State is the type describing the state of the animator at each given point. In start we need to specify what state the animator is in when it starts. In animation we need to map each possible state to an animation.

    For now we just have one state corresponding to the idle animation of the green character.

    Our setup system is now this:

    setup :: System ()
    setup = do
      window <- spawn Window
      spawn_ (Camera, Rel OutputTo window)
    
      spawn_
        ( Animator MyAnimator,
          Anchor $ V2 0.5 0.9,
          WithMaterial @SpriteMaterial,
          def {translation = V3 (-60) 0 0},
          Player
        )
    
      spawn_
        ( Animator MyAnimator,
          Anchor $ V2 0.5 0.9,
          WithMaterial @SpriteMaterial,
          def {translation = V3 60 0 0}
        )
    

    There's no need to insert a Sprite or SpriteSlice component anymore. The Animator will add them for us. It will also load all the assets necessary to play the animations.

    If you run the app now, you should see that both characters are playing the green idle animation:

    Your First Transition

    Next, let's try passing some input into the animator and creating a transition.

    instance AnimatorTag MyAnimator where
      data Input MyAnimator = Input {walk :: Bool} deriving (Generic, Default)
      data Shift MyAnimator = Shift
      data State MyAnimator = GreenIdle | GreenWalk
    
      animation GreenIdle = selAnim "animations.kdl" "green_idle"
      animation GreenWalk = selAnim "animations.kdl" "green_walk"
    
      transition GreenIdle Input {walk = True} _ _ = Just GreenWalk
      transition GreenWalk Input {walk = False} _ _ = Just GreenIdle
      transition _ _ _ _ = Nothing
    
      start = GreenIdle
    

    We have added a walk :: Bool field to our Input, a new state, and we have set up two transitions such that, if our character is in the idle state but the input says they're walking, they switch to the walk state, and vice versa.

    All that's left is to actually set the input from outside. We can easily do that from our movement system by adding these lines after calculating dir:

    let walk = norm dir > 0
    input @MyAnimator (\i -> i {walk}) player
    

    You should now see your character walking when you move them!

    What's more, the animations are hot-reloadable. Try modifying the animation in animations.kdl and you should see the changes immediately! (Don't forget to add FileWatcherPlugin to your app).

    A Better Animator

    Let's expand our animator to be more generic and able to handle multiple characters:

    data Character = Green | Pink
    
    charName :: Character -> Text
    charName Green = "green"
    charName Pink = "pink"
    
    data AnimationType = Idle | Walk
    
    instance AnimatorTag MyAnimator where
      data Input MyAnimator = Input {walk :: Bool} deriving (Generic, Default)
      data Shift MyAnimator = ChangeCharacter Character
      data State MyAnimator = Start | CharacterAnimation Character AnimationType
    
      animation (CharacterAnimation c Idle) = selAnim "animations.kdl" (charName c <> "_idle")
      animation (CharacterAnimation c Walk) = selAnim "animations.kdl" (charName c <> "_walk")
      animation Start = noAnimation
    
      transition (CharacterAnimation c Idle) Input {walk = True} _ _ = Just (CharacterAnimation c Walk)
      transition (CharacterAnimation c Walk) Input {walk = False} _ _ = Just (CharacterAnimation c Idle)
      transition Start _ (Just (ChangeCharacter c)) _ = Just (CharacterAnimation c Idle)
      transition _ _ _ _ = Nothing
    
      start = Start
    

    First, we've added a type to define our characters, and a function to convert their name to Text. This is so we can more easily grab each of their animations without having to match on each individual character.

    data Character = Green | Pink
    
    charName :: Character -> Text
    charName Green = "green"
    charName Pink = "pink"
    

    We've added a new Start state that has no associated animation. The other state, CharacterAnimation c a, is the state corresponding to animation a of character c.

    data State MyAnimator = Start | CharacterAnimation Character AnimationType
    

    The transitions are now generic. We are telling it to transition from the Idle animation of character c to the Walk animation of character c and vice versa.

    transition (CharacterAnimation c Idle) Input {walk = True} _ _ = Just (CharacterAnimation c Walk)
    transition (CharacterAnimation c Walk) Input {walk = False} _ _ = Just (CharacterAnimation c Idle)
    

    We have also defined a Shift. This is esentially an event we can send to the animator to trigger a transition.

    data Shift MyAnimator = ChangeCharacter Character
    

    When this is triggered, it should follow this new transition:

    transition Start _ (Just (ChangeCharacter c)) _ = Just (CharacterAnimation c Idle)
    

    When we tell it to change character to c, it will transition from Start to the Idle animation of c.

    All that's left is to actually trigger that shift when we spawn our characters, otherwise they will be stuck in the Start state:

    e <- spawn
      ( Animator MyAnimator,
        Anchor $ V2 0.5 0.9,
        WithMaterial @SpriteMaterial,
        def {translation = V3 (-60) 0 0},
        Player
      )
    
    shift (ChangeCharacter Pink) e
    
    e <- spawn
      ( Animator MyAnimator,
        Anchor $ V2 0.5 0.9,
        WithMaterial @SpriteMaterial,
        def {translation = V3 60 0 0}
      )
    
    shift (ChangeCharacter Green) e
    

    Flipping Your Sprites

    Alright our characters are now properly animated but there's still something about them. When you move left, the character is still facing right!

    The solution is simple: flip the sprite when they're moving to the left. We can do this through the SpriteFlip component in our movement system:

    let flip = dir.x < 0
    insert (SpriteFlip {x = False, y = flip}) player
    

    That's all!

    Next Chapter: Your First Shader