Examples
Examples

Animation State Machine


Setup

A simple Finite State Machine for animations.

This example shows how to create a responsive character animation system using a Finite State Machine (FSM). The character can smoothly transition between different states like idle, running, jumping, attacking, and crouching based on player input. This is a fundamental technique used in most 2D platformers and action games.

What You’ll Learn?

  • How to implement a state machine for character animations
  • How to handle complex input combinations and priorities## Key Concepts

State Machine: A design pattern where an object can be in only one state at a time, with clear rules for transitioning between states.

Input Priority: A system that determines which actions take precedence when multiple keys are pressed simultaneously.

Animation Transitions: Smooth changes between different animations, often with intermediate “transition” animations.## Key Concepts

State Machine: A design pattern where an object can be in only one state at a time, with clear rules for transitioning between states.

Input Priority: A system that determines which actions take precedence when multiple keys are pressed simultaneously.

Animation Transitions: Smooth changes between different animations, often with intermediate “transition” animations.

  • How to create smooth transitions between animation states
  • How to make sprites flip direction based on movement
  • How to add visual effects (like jump animations)
  • How to communicate between game objects using messages

Setup

The example consists of two main game objects:

knight
The animated character. Contains:
  • A Sprite component with the knight character image and animations.
  • A Script component (knight.script) that implements the state machine logic, handles input, and manages animation transitions.
gui
The user interface. Contains:
  • A GUI component (control.gui) that has 6 nodes displaying states and text description for the example.
  • A GUI Script component (control.gui_script) that receives messages from the knight and updates the visual state indicators.

Note:
The GUI in this example is not required for understanding the state machine logic, it only visually shows the active animation state. You can view the GUI source in the project files on Github still though.

animation_states_collection

Animation Atlas

The sprite component uses a flipbook animation that is set up in an atlas:

For this example we used the Free Knight Character by Nauris ‘aamatniekss’ available here: https://aamatniekss.itch.io/fantasy-knight-free-pixelart-animated-character

atlas

The atlas contains multiple animations for different character states:

  • idle: Standing still animation
  • run: Running animation (looped)
  • jump: Jumping animation (plays once)
  • attack: Attacking animation (plays once)
  • turn_around: Turning animation (plays once)
  • crouch_idle: Crouching idle anim Note: ation
  • crouch_walk: Crouch walking animation
  • crouch_attack: Crouch attacking animation
  • to_crouch: Transition from standing to crouching
  • from_crouch: Transition from crouching to standing

Input Bindings

Key Action
   

Scripts

knight.script

-- ============================================================================
-- KNIGHT ANIMATION STATE MACHINE - Beginner Friendly Example
-- ============================================================================
-- This script demonstrates how to create a character animation system using
-- a simple implementation of a Finite State Machine (FSM) in Defold.

-- Input action hashes - these connect keyboard/gamepad buttons to our code
-- In Defold, we use hash() to convert strings to efficient identifiers
local INPUT = {
    JUMP = hash("jump"),
    CROUCH = hash("crouch"),
    ATTACK = hash("attack"),
    LEFT = hash("left"),
    RIGHT = hash("right")
}

-- ============================================================================
-- STATE MACHINE CONFIGURATION
-- ============================================================================
-- This table defines ALL possible states our character can be in.
-- Think of it as a "rule book" that tells the game:
-- - What animation to play in each state
-- - Whether the animation should loop or play once
-- - What should happen when the player presses different buttons
--
-- Each state is like a "mode" the character is in. For example:
-- - "standing_idle" = character is standing still, playing idle animation, looped
--
-- The "on_" properties define what happens when inputs are pressed, e.g.:
-- - on_attack = what state to go to when attack button is pressed
-- - on_move = what state to go to when movement keys are pressed
-- - default_next = what state to go to when animation finishes (for non-looped animations)

local STATE_CONFIG = {
    -- STANDING STATES - Character is upright and can move freely
    -- These are the "normal" states when the character is standing
    standing_idle = {
        animation = "idle",      -- Play the "idle" animation from the sprite atlas
        is_looped = true,        -- Keep playing this animation over and over
        on_crouch = "to_crouch", -- If crouch key pressed, go to "to_crouch" state
        on_attack = "standing_attack", -- If attack key pressed, go to "standing_attack" state
        on_jump = "standing_jump", -- If jump key pressed, go to "standing_jump" state
        on_move = "standing_run", -- If movement keys pressed, go to "standing_run" state
        on_turn = "standing_turn" -- If character turns around, go to "standing_turn" state
    },
    standing_run = {
        animation = "run",       -- Play the running animation
        is_looped = true,        -- Loop the running animation continuously
        on_crouch = "to_crouch", -- Can still crouch while running
        on_attack = "standing_attack", -- Can attack while running
        on_jump = "standing_jump", -- Can jump while running
        on_stop = "standing_idle", -- When movement stops, go back to idle
        on_turn = "standing_turn" -- When turning around, play turn animation
    },
    standing_jump = {
        animation = "jump",      -- Play the jump animation
        is_looped = false,       -- Play jump animation only once
        default_next = "standing_idle" -- When jump animation finishes, go back to idle
    },
    standing_attack = {
        animation = "attack",    -- Play the attack animation
        is_looped = false,       -- Play attack animation only once
        default_next = "standing_idle" -- When attack finishes, go back to idle
    },
    standing_turn = {
        animation = "turn_around", -- Play the turn around animation
        is_looped = false,        -- Play turn animation only once
        default_next = "standing_idle", -- When turn finishes, go to idle
        on_turn = "standing_turn" -- If turning again while already turning, keep turning
    },

    -- CROUCHING STATES - Character is in low position, limited movement
    -- When crouching, the character can't jump but can still move and attack
    crouching_idle = {
        animation = "crouch_idle", -- Play the crouching idle animation
        is_looped = true,         -- Loop the crouch idle animation
        on_stand = "to_standing", -- If crouch key released, start standing up
        on_attack = "crouching_attack", -- Can attack while crouching
        on_move = "crouching_run" -- Can move while crouching (crouch walk)
    },
    crouching_run = {
        animation = "crouch_walk", -- Play the crouch walking animation
        is_looped = true,         -- Loop the crouch walk animation
        on_stand = "to_standing", -- Can stand up while crouch walking
        on_attack = "crouching_attack", -- Can attack while crouch walking
        on_stop = "crouching_idle" -- When movement stops, go to crouch idle
    },
    crouching_attack = {
        animation = "crouch_attack", -- Play the crouch attack animation
        is_looped = false,         -- Play attack animation only once
        default_next = "crouching_idle", -- When attack finishes, go to crouch idle
        on_stand = "to_standing",  -- Can stand up even while attacking
    },

    -- TRANSITION STATES - Intermediate animations between major state changes
    -- These states handle the smooth transition between standing and crouching
    to_crouch = {
        animation = "to_crouch",  -- Play the "going into crouch" animation
        is_looped = false,        -- Play transition animation only once
        default_next = "crouching_idle" -- When transition finishes, go to crouch idle
    },
    to_standing = {
        animation = "from_crouch", -- Play the "standing up from crouch" animation
        is_looped = false,       -- Play transition animation only once
        default_next = "standing_idle" -- When transition finishes, go to standing idle
    }
}

-- ============================================================================
-- MOVEMENT AND DIRECTION LOGIC
-- ============================================================================

--- Updates movement state and sprite direction based on input
--- This function figures out:
--- 1. Is the character moving? (left or right key pressed)
--- 2. Which direction is the character facing? (left or right)
--- 3. Did the character just turn around? (for turn animation)
--- @param self table Script instance with input flags
local function update_movement_state(self)
    -- Start by assuming the character is not moving
    self.is_moving = false

    -- Remember the previous facing direction to detect turns
    local previous_is_flipped = self.is_flipped

    -- Check movement input and update facing direction
    if self[INPUT.LEFT] and not self[INPUT.RIGHT] then
        -- Left key is pressed and right key is not pressed
        self.is_moving = true
        self.is_flipped = true -- Character faces left (sprite is flipped)
    elseif self[INPUT.RIGHT] and not self[INPUT.LEFT] then
        -- Right key is pressed and left key is not pressed
        self.is_moving = true
        self.is_flipped = false -- Character faces right (sprite is not flipped)
    end
    -- If both keys are pressed or neither is pressed, character doesn't move

    -- Detect if the character just turned around - used to trigger the "turn around" animation
    self.is_turning = self.is_flipped ~= previous_is_flipped
end


-- ============================================================================
-- STATE TRANSITION LOGIC
-- ============================================================================

--- Determines the next state based on current input and state configuration
--- This is the "brain" of our state machine - it decides what state to go to next
---
--- INPUT PRIORITY SYSTEM (in order of importance):
--- 1. Attack - Highest priority, can interrupt most other actions
--- 2. Jump - High priority, can interrupt movement
--- 3. Movement - Medium priority, handles start/stop moving
--- 4. Crouch/Stand - Medium priority, changes posture
--- 5. Turn - Lowest priority, only when changing direction
---
--- @param self table Script instance with input flags and current state
--- @return string|nil Next state name or nil if no transition needed
local function get_next_state(self)
    -- Get current input state and configuration
    local is_crouching = self[INPUT.CROUCH] -- Is crouch key currently pressed?
    local config = STATE_CONFIG[self.state] -- Get rules for current state
    local next_state = nil               -- Will hold the next state to go to

    -- PRIORITY 1: ATTACK INPUT (Highest Priority)
    -- Attack can interrupt almost any other action
    if self[INPUT.ATTACK] then
        next_state = config.on_attack -- Go to attack state if current state allows it
    end

    -- PRIORITY 2: JUMP INPUT (High Priority)
    -- Jump can interrupt movement but not attack
    if self[INPUT.JUMP] then
        next_state = config.on_jump -- Go to jump state if current state allows it
    end

    -- PRIORITY 3: MOVEMENT STATE CHANGES (Medium Priority)
    -- Handle starting to move or stopping movement
    if self.is_moving and config.on_move then
        next_state = config.on_move -- Character is moving and current state has a "move" transition
    elseif not self.is_moving and config.on_stop then
        next_state = config.on_stop -- Character stopped moving and current state has a "stop" transition
    end

    -- PRIORITY 4: CROUCH/STAND STATE CHANGES (Medium Priority)
    -- Handle posture changes (standing vs crouching)
    if is_crouching and config.on_crouch then
        next_state = config.on_crouch -- Crouch key is pressed and current state allows crouching
    elseif not is_crouching and config.on_stand then
        next_state = config.on_stand -- Crouch key is released and current state allows standing
    end

    -- PRIORITY 5: DIRECTION CHANGE (Lowest Priority)
    -- Handle turning around (only when changing direction)
    if self.is_turning and config.on_turn then
        next_state = config.on_turn -- Character just turned around and current state has turn animation
    end

    -- Return the next state (or nil if no transition is needed)
    return next_state
end

-- ============================================================================
-- VISUAL LAYER - Handles all visual effects and animations
-- ============================================================================

--- Updates all visual elements based on current character state
--- This function is responsible for making the character look correct on screen:
--- - Playing the right animation for the current state
--- - Flipping the sprite to face the right direction
--- - Creating special effects (like the jump animation)
--- - Updating the GUI to show current state
--- @param self table Script instance with current state and flip information
local function update_visuals(self)
    -- Get the configuration for the current state
    local config = STATE_CONFIG[self.state]

    -- Play the animation for the current state
    sprite.play_flipbook("#sprite", config.animation)

    -- Visualize the jump effect
    -- (When jumping, we add a visual effect by moving the character up and down)
    if self.state == "standing_jump" then
        local pos = go.get_position()
        -- Animate the Y position to simulate a jump visually
        go.animate(".", "position.y", go.PLAYBACK_ONCE_PINGPONG, pos.y + 50, go.EASING_INOUTCUBIC, 0.6)
    else
        -- If not jumping, make sure any jump animation is cancelled and reset the character to ground level
        go.cancel_animations(".", "position.y")
        local pos = go.get_position()
        go.set_position(vmath.vector3(pos.x, 600, pos.z)) -- 600 is our ground level Y position
    end

    -- Update the GUI - send a message to the GUI component to update the UI
    msg.post("gui", "animation_state_changed", {
        state = self.state
    })
end

-- ============================================================================
-- DEFOLD LIFECYCLE FUNCTIONS
-- ============================================================================

--- Initializes the knight character when the game starts
--- It sets up the initial state and prepares the character for input
--- @param self table Script instance - this is automatically provided by Defold
function init(self)
    -- Set up initial state machine state as "standing_idle"
    self.state = "standing_idle"

    -- Set up movement and direction flags
    self.is_flipped = false -- Character starts facing right (not flipped)
    self.is_moving = false -- Character starts not moving
    self.is_turning = false -- Character starts not turning

    -- Initialize all input flags - start with all keys "not pressed" (false)
    self[INPUT.LEFT] = false -- Left arrow key
    self[INPUT.RIGHT] = false -- Right arrow key
    self[INPUT.JUMP] = false -- Space bar
    self[INPUT.ATTACK] = false -- Attack button (X)
    self[INPUT.CROUCH] = false -- Crouch button (C)

    -- Display the initial state visually
    update_visuals(self)

    -- Enable input handling
    msg.post(".", "acquire_input_focus")
end

--- Handles input events from keyboard every time the player presses or releases a key
--- It updates our input tracking and triggers state transitions
--- @param self table Script instance
--- @param action_id hash Which input was pressed (like "jump", "attack", etc.)
--- @param action table Contains information about the input (pressed/released)
function on_input(self, action_id, action)
    -- Update input state - keep track of which keys are currently being pressed:
    if action.pressed then
        self[action_id] = true -- Key was just pressed down
    elseif action.released then
        self[action_id] = false -- Key was just released
    end

    -- Process state machine:
    update_movement_state(self)          -- Update movement and direction state based on input
    local next_state = get_next_state(self) -- Decide what state to go to next

    -- If we determined a new state is needed, switch to it and update visuals:
    if next_state then
        self.state = next_state -- Change to the new state
        update_visuals(self) -- Update the visual appearance
    end
end

--- Handles messages from other game objects
--- We use it to handle messages that comes to the script when animations finish playing
--- @param self table Script instance
--- @param message_id hash What type of message this is
function on_message(self, message_id, message)
    -- This message is sent when a non-looped animation finishes playing (like attack, jump, or turn animations)
    if message_id == hash("animation_done") then

        -- Flip the sprite horizontally when the character just finished turning
        if message.id == hash("turn_around") then
            sprite.set_hflip("#sprite", self.is_flipped)
        end

        -- Process state machine:
        update_movement_state(self)       -- Update movement and direction state based on input
        local next_state = get_next_state(self) -- Decide what state to go to next

        -- Switch to the next state (or default next) and update visuals
        self.state = next_state or STATE_CONFIG[self.state].default_next
        update_visuals(self)
    end
end

This example was created by The Defold Foundation.

DOWNLOAD

 

Do you want to see more examples? Why not write a few yourself and submit a pull request? We love contributions.

GITHUB