| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- -- ============================================================================
- -- 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
|