mischief-render-0.1.0.0: src/Mischief/Render/Tutorial/Start/Animation.hs
{-# OPTIONS_GHC -Wno-unused-imports #-}
-- |
-- [Previous Chapter: Starting Out]("Mischief.Render.Tutorial.Start.Start")
--
-- [Next Chapter: Your First Shader]("Mischief.Render.Tutorial.Start.Shader")
--
-- [Main Page]("Mischief.Render")
module Mischief.Render.Tutorial.Start.Animation
( -- * 1.2. Your First Animation
-- $intro
-- * Looking At The Spritesheet
-- $spritesheet
-- * Making an Animator
-- $animator
-- * Your First Transition
-- $trans
-- * A Better Animator
-- $animator2
-- * Flipping Your Sprites
-- $flip
-- * [Next Chapter: Your First Shader]("Mischief.Render.Tutorial.Start.Shader")
)
where
import Control.Monad (void)
import Data.Text (Text)
import Mischief.Animation
import Mischief.Assets
import Mischief.ECS
import Mischief.Materials.Sprite (SpriteMaterial)
import Mischief.Math
import Mischief.Render
import Mischief.Render.Plugin
-- $intro
-- This is a continuation of the previous chapter where we'll be animating our sprites.
-- $spritesheet
-- First, grab the @spritesheet.kdl@ and @animations.kdl@ from the [repo](https://codeberg.org/mischief/mischief)
-- 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\"
-- @
-- $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:
--
-- 
-- $trans
-- 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).
-- $animator2
-- 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
-- @
--
-- 
-- $flip
-- 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!
--
-- 