skills/godot-animation-tree-mastery/SKILL.md
Expert patterns for AnimationTree including StateMachine transitions, BlendSpace2D for directional movement, BlendTree for layered animations, root motion, transition conditions, advance expressions, and state machine sub-states. Use for complex character animation systems with movement blending and state management. Trigger keywords: AnimationTree, AnimationNodeStateMachine, BlendSpace2D, BlendSpace1D, BlendTree, transition_request, blend_position, advance_expression, AnimationNodeAdd2, AnimationNodeBlend2, root_motion.
npx skillsauth add thedivergentai/gd-agentic-skills godot-animation-tree-masteryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Expert guidance for Godot's advanced animation blending and state machines.
play() on AnimationPlayer when using AnimationTree — AnimationTree controls the player. Directly calling play() causes conflicts and jitter. Use set("parameters/transition_request") or travel() instead [12].active = true — AnimationTree is inactive by default. Animations won't play until $AnimationTree.active = true [13]."parameters/StateMachine/transition_request". This ensures compatibility when nodes move in the hierarchy [14].auto_advance enabled for interactive states — It causes immediate transitions. Use it only for automated sequences like combo chains or death-to-respawn [15, 121].BlendSpace2D for 1D blending — Blending only speed? Use BlendSpace1D. Blending only two states? Use Blend2. BlendSpace2D is specifically for X+Y directional inputs (strafe) [16, 142].AnimationTree parameters every frame without a guard — Setting parameters via set() every frame regardless of change causes cache invalidation and potential stutter. Check equality first.BlendTrees for simple logic — Every layer adds CPU overhead. If logic can be handled in a StateMachine or a simple script-driven Blend2, do it there.await get_tree().process_frame when updating parameters synchronously — Sometimes the tree needs one frame to reconcile state before the next parameter change takes effect.auto_advance for long cutscenes — If an animation is interrupted, auto_advance can put the character in a broken state. Use Method Tracks to signal state completion instead.Sync groups for animations with wildly different lengths — It forces one animation to play at an extreme speed. Use TimeScale or separate layers for mismatching cycles.MANDATORY: Read the appropriate script before implementing the corresponding pattern.
Expert management of AnimationTree parameters with guards to prevent redundant updates and GPU cache churn.
Using AnimationNodeOneShot for high-priority reactive animations like recoil, blinks, and hit reactions.
Runtime manipulation of playback speed for bullet-time effects or movement haste multipliers.
Procedural bone filtering (masking) for nodes like Add2 to separate upper/lower body animations.
Programmatic control of AnimationNodeStateMachinePlayback using travel() and start().
Complex mixing patterns for BlendTree nodes to create interactive combat layers.
Expert 3D CharacterBody motion extraction optimized specifically for AnimationTree nodes.
Using Sync Groups to keep multi-layered animations (e.g. walk and reload) perfectly aligned.
Pattern for managing hierarchical State Machines and nested node parameter paths.
Interactive tool for visualizing current states, transition paths, and blend values in real-time.
AnimationTree (node)
├─ Root (assigned in editor)
│ ├─ StateMachine (common)
│ ├─ BlendTree (layering)
│ └─ BlendSpace (directional)
└─ anim_player: NodePath → points to AnimationPlayer
# 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")
# 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")
# 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
# 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
# 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
# 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
# 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
# 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
# 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()
# 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")
# 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
# 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
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)
# 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)
# 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
| 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
Decouple your animation frames from specific gameplay logic by using a generalized dispatcher that passes metadata (e.g., surface type for footsteps) through signals.
class_name AnimationEventDispatcher extends Node
signal animation_event(event_name: String, metadata: Variant)
## Generic function called by AnimationPlayer Method Tracks
func dispatch_event(event_name: String, metadata: Variant) -> void:
animation_event.emit(event_name, metadata)
# Workflow:
# 1. Add Method Track to animation (e.g., "walk")
# 2. Keyframe: method="dispatch_event", args=["footstep", "stone"]
# 3. Audio manager listens to signal and plays correct 'stone' SFX.
Use a BlendTree to procedurally blend turning animations based on rotation input, providing more natural stationary turns than simple state changes.
# Root -> BlendTree
# └─ TurnBlend (AnimationNodeBlend2)
# ├─ Input 0: Idle
# └─ Input 1: TurnRight
func _physics_process(delta: float) -> void:
var turn_input := Input.get_axis("left", "right")
# 0.0 = Idle, 1.0 = Full Turn
var blend_amount := abs(turn_input)
# Update BlendTree parameter
anim_tree.set("parameters/TurnBlend/blend_amount", blend_amount)
# Update physical rotation
rotate_y(-turn_input * turn_speed * delta)
Optimize massive scenes by swapping the AnimationTree.tree_root resource between a complex "Hero" tree and a simplified "Crowd" tree based on visibility.
class_name AnimationComplexityManager extends Node3D
@export var hero_tree: AnimationRootNode # Complex StateMachine
@export var crowd_tree: AnimationRootNode # Simple Looping Idle
@onready var anim_tree: AnimationTree = $AnimationTree
@onready var visibility: VisibleOnScreenNotifier3D = $VisibleOnScreenNotifier3D
func _ready() -> void:
visibility.screen_entered.connect(func(): anim_tree.tree_root = hero_tree)
visibility.screen_exited.connect(func(): anim_tree.tree_root = crowd_tree)
development
Godot Expert Auditor: Aurelius. Exhaustive never-list enforcement and architectural slap-down for Godot 4.6 projects.
development
--- name:# Godot Expert Analyst: Anara ## Visionary Architect of Godot 4.6+ Excellence > "Scale is not a feature; it is a philosophy. I don't look at what your game is today; I look at whether it can survive tomorrow." — Anara You are **Anara**, the visionary architect of Godot 4.6+ excellence. You evaluate projects not for "if they work", but for "how well they scale". Your purpose is to certify professional-grade projects and provide the blueprint for architectural transcendence. Your voice
development
Expert blueprint for AI pathfinding (tower defense, RTS, stealth) using NavigationAgent2D/3D, NavigationServer, avoidance, and dynamic navigation mesh generation. Use when implementing enemy AI, NPC movement, or obstacle avoidance. Keywords NavigationAgent2D, NavigationRegion2D, pathfinding, NavigationServer, avoidance, baking, NavigationObstacle.
development
Expert patterns for Godot audio including AudioStreamPlayer variants (2D positional, 3D spatial), AudioBus mixing architecture, dynamic effects (reverb, EQ,compression), audio pooling for performance, music transitions (crossfade, bpm-sync), and procedural audio generation. Use for music systems, sound effects, spatial audio, or audio-reactive gameplay. Trigger keywords: AudioStreamPlayer, AudioStreamPlayer2D, AudioStreamPlayer3D, AudioBus, AudioServer, AudioEffect, music_crossfade, audio_pool, positional_audio, reverb, bus_volume.