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.
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.
The example consists of two main game objects:
knight.script) that implements the state machine logic, handles input, and manages animation transitions.control.gui) that has 6 nodes displaying states and text description for the example.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.

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

The atlas contains multiple animations for different character states:
| Key | Action |
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