skills/thedivergentai/gd-agentic-skills/godot-animation-tree-mastery

godot-animation-tree-mastery

SKILL.md

AnimationTree Mastery

Expert guidance for Godot's advanced animation blending and state machines.

NEVER Do

  • NEVER call play() on AnimationPlayer when using AnimationTree — AnimationTree controls the player. Directly calling play() causes conflicts. Use set("parameters/transition_request") instead.
  • NEVER forget to set active = true — AnimationTree is inactive by default. Animations won't play until $AnimationTree.active = true.
  • NEVER use absolute paths for transition_request — Use relative paths. "parameters/StateMachine/transition_request", not "/root/.../transition_request".
  • NEVER leave auto_advance enabled unintentionally — Auto-advance transitions fire immediately without conditions. Useful for combo chains, but deadly for idle→walk.
  • NEVER use BlendSpace2D for non-directional blending — Use BlendSpace1D for speed (walk→run) or Blend2 for simple tweens. BlendSpace2D is for X+Y axes (strafe animations).

Available Scripts

MANDATORY: Read the appropriate script before implementing the corresponding pattern.

nested_state_machine.gd

Hierarchical state machine pattern. Shows travel() between sub-states and deep parameter paths (StateMachine/BlendSpace2D/blend_position).

skeleton_ik_lookat.gd

Procedural IK for head-tracking. Drives SkeletonModifier3D look-at parameters from AnimationTree with smooth weight blending.


Core Concepts

AnimationTree Structure

AnimationTree (node)
  ├─ Root (assigned in editor)
  │   ├─ StateMachine (common)
  │   ├─ BlendTree (layering)
  │   └─ BlendSpace (directional)
  └─ anim_player: NodePath → points to AnimationPlayer

Parameter Access

# Set parameters using string paths
$AnimationTree.set("parameters/StateMachine/transition_request", "run")
$AnimationTree.set("parameters/Movement/blend_position", Vector2(1, 0))

# Get current state
var current_state = $AnimationTree.get("parameters/StateMachine/current_state")

StateMachine Pattern

Basic Setup

# Scene structure:
# CharacterBody2D
#   ├─ AnimationPlayer (has: idle, walk, run, jump, land)
#   └─ AnimationTree
#       └─ Root: AnimationNodeStateMachine

# StateMachine nodes (created in AnimationTree editor):
# - Idle (AnimationNode referencing "idle")
# - Walk (AnimationNode referencing "walk")
# - Run (AnimationNode referencing "run")
# - Jump (AnimationNode referencing "jump")
# - Land (AnimationNode referencing "land")

@onready var anim_tree: AnimationTree = $AnimationTree
@onready var state_machine: AnimationNodeStateMachinePlayback = anim_tree.get("parameters/StateMachine/playback")

func _ready() -> void:
    anim_tree.active = true

func _physics_process(delta: float) -> void:
    var velocity := get_velocity()
    
    # State transitions based on gameplay
    if is_on_floor():
        if velocity.length() < 10:
            state_machine.travel("Idle")
        elif velocity.length() < 200:
            state_machine.travel("Walk")
        else:
            state_machine.travel("Run")
    else:
        if velocity.y < 0:  # Rising
            state_machine.travel("Jump")
        else:  # Falling
            state_machine.travel("Land")

Transition Conditions (Advance Expressions)

# In AnimationTree editor:
# Add transition from Idle → Walk
# Set "Advance Condition" to "is_walking"

# In code:
anim_tree.set("parameters/conditions/is_walking", true)

# Transition fires automatically when condition becomes true
# Useful for event-driven transitions (hurt, dead, etc.)

# Example: Damage transition
anim_tree.set("parameters/conditions/is_damaged", false)  # Reset each frame

func take_damage() -> void:
    anim_tree.set("parameters/conditions/is_damaged", true)
    # Transition to "Hurt" state fires immediately

Auto-Advance (Combo Chains)

# In AnimationTree editor:
# Transition from Attack1 → Attack2
# Enable "Auto Advance" (no condition needed)

# Code:
state_machine.travel("Attack1")
# Attack1 animation plays
# When Attack1 finishes, automatically transitions to Attack2
# When Attack2 finishes, transitions to Idle (next auto-advance)

# Useful for:
# - Attack combos
# - Death → Respawn
# - Cutscene sequences

BlendSpace2D (Directional Movement)

8-Way Movement

# Create BlendSpace2D in AnimationTree editor:
# - Add animations at positions:
#   - (0, -1): walk_up
#   - (0, 1): walk_down
#   - (-1, 0): walk_left
#   - (1, 0): walk_right
#   - (-1, -1): walk_up_left
#   - (1, -1): walk_up_right
#   - (-1, 1): walk_down_left
#   - (1, 1): walk_down_right
#   - (0, 0): idle (center)

# In code:
func _physics_process(delta: float) -> void:
    var input := Input.get_vector("left", "right", "up", "down")
    
    # Set blend position (AnimationTree interpolates between animations)
    anim_tree.set("parameters/Movement/blend_position", input)
    
    # BlendSpace2D automatically blends animations based on input
    # input = (0.5, -0.5) → blends walk_right and walk_up

BlendSpace1D (Speed Blending)

# For walk → run transitions
# Create BlendSpace1D:
#   - Position 0.0: walk
#   - Position 1.0: run

func _physics_process(delta: float) -> void:
    var speed := velocity.length()
    var max_speed := 400.0
    var blend_value := clamp(speed / max_speed, 0.0, 1.0)
    
    anim_tree.set("parameters/SpeedBlend/blend_position", blend_value)
    # Smoothly blends from walk → run as speed increases

BlendTree (Layered Animations)

Add Upper Body Animation

# Problem: Want to aim gun while walking
# Solution: Blend upper body (aim) with lower body (walk)

# In AnimationTree editor:
# Root → BlendTree
#   ├─ Walk (lower body animation)
#   ├─ Aim (upper body animation)
#   └─ Add2 node (combines them)
#       - Inputs: Walk, Aim
#       - filter_enabled: true
#       - Filters: Only enable upper body bones for Aim

# Code:
# No code needed! BlendTree auto-combines
# Just ensure animations are assigned

Blend2 (Crossfade)

# Blend between two animations dynamically
# Root → BlendTree
#   └─ Blend2
#       ├─ Input A: idle
#       └─ Input B: attack

# Code:
var blend_amount := 0.0

func _process(delta: float) -> void:
    # Gradually blend from idle → attack
    blend_amount += delta
    blend_amount = clamp(blend_amount, 0.0, 1.0)
    
    anim_tree.set("parameters/IdleAttackBlend/blend_amount", blend_amount)
    # 0.0 = 100% idle
    # 0.5 = 50% idle, 50% attack
    # 1.0 = 100% attack

Root Motion with AnimationTree

# Enable in AnimationTree
anim_tree.root_motion_track = NodePath("CharacterBody3D/Skeleton3D:Root")

func _physics_process(delta: float) -> void:
    # Get root motion
    var root_motion := anim_tree.get_root_motion_position()
    
    # Apply to character (not velocity!)
    global_position += root_motion.rotated(rotation.y)
    
    # For CharacterBody3D with move_and_slide:
    velocity = root_motion / delta
    move_and_slide()

Advanced Patterns

Sub-StateMachines

# Nested state machines for complex behavior
# Root → StateMachine
#   ├─ Grounded (Sub-StateMachine)
#   │   ├─ Idle
#   │   ├─ Walk
#   │   └─ Run
#   └─ Airborne (Sub-StateMachine)
#       ├─ Jump
#       ├─ Fall
#       └─ Glide

# Access nested states:
var sub_state = anim_tree.get("parameters/Grounded/playback")
sub_state.travel("Run")

Time Scale (Slow Motion)

# Slow down specific animation without affecting others
anim_tree.set("parameters/TimeScale/scale", 0.5)  # 50% speed

# Useful for:
# - Bullet time
# - Hurt/stun effects
# - Charge-up animations

Sync Between Animations

# Problem: Switching from walk → run causes foot slide
# Solution: Use "Sync" on transition

# In AnimationTree editor:
# Transition: Walk → Run
# Enable "Sync" checkbox

# Godot automatically syncs animation playback positions
# Feet stay grounded during transition

Debugging AnimationTree

Print Current State

func _process(delta: float) -> void:
    var current_state = anim_tree.get("parameters/StateMachine/current_state")
    print("Current state: ", current_state)
    
    # Print blend position
    var blend_pos = anim_tree.get("parameters/Movement/blend_position")
    print("Blend position: ", blend_pos)

Common Issues

# Issue: Animation not playing
# Solution:
if not anim_tree.active:
    anim_tree.active = true

# Issue: Transition not working
# Check:
# 1. Is advance_condition set?
# 2. Is transition priority correct?
# 3. Is auto_advance enabled unintentionally?

# Issue: Blend not smooth
# Solution: Increase transition xfade_time (0.1 - 0.3s)

Performance Optimization

Disable When Not Needed

# AnimationTree is expensive
# Disable for off-screen entities
extends VisibleOnScreenNotifier3D

func _ready() -> void:
    screen_exited.connect(_on_screen_exited)
    screen_entered.connect(_on_screen_entered)

func _on_screen_exited() -> void:
    $AnimationTree.active = false

func _on_screen_entered() -> void:
    $AnimationTree.active = true

Decision Tree: When to Use AnimationTree

Feature AnimationPlayer Only AnimationTree
Simple state swap ✅ play("idle") ❌ Overkill
Directional movement ❌ Complex ✅ BlendSpace2D
State machine (5+ states) ❌ Messy code ✅ StateMachine
Layered animations ❌ Manual blending ✅ BlendTree
Root motion ✅ Possible ✅ Built-in
Transition blending ❌ Manual ✅ Auto

Use AnimationTree for: Complex characters with 5+ states, directional movement, layered animations Use AnimationPlayer for: Simple animations, UI, cutscenes, props

Reference

Weekly Installs
55
GitHub Stars
35
First Seen
Feb 10, 2026
Installed on
gemini-cli54
opencode54
codex54
amp53
github-copilot53
kimi-cli53