{-# 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! -- -- 