【Godot】3D Skeletal Animation in Godot: AnimationPlayer and AnimationTree

Created: 2026-02-08

Learn how to play 3D character animations with AnimationPlayer, manage states and blending with AnimationTree, and adjust poses dynamically with SkeletonIK3D in Godot 4.

Overview

Tested with: Godot 4.3+

Want to add walking, running, and attacking animations to your 3D character but struggling with "choppy transitions" or "overly complex state management"?

This article covers playing basic animations with AnimationPlayer, managing states and blending with AnimationTree, and dynamically adjusting poses with SkeletonIK3D -- all with practical examples from Godot 4.

Skeleton3D and Bone Hierarchy

Before working with animations, it helps to understand the underlying skeleton. All skeletal animation is built on top of this bone hierarchy.

Skeleton3D is the node that represents a 3D character's skeletal structure. Like a human skeleton, it's composed of hierarchically arranged bones. Animations work by moving these bones over time.

Basic Structure

CharacterBody3D (Root)
├─ MeshInstance3D (Visual)
│  └─ Skeleton3D (Skeleton)
│     ├─ BoneAttachment3D (Attach weapons, etc.)
│     └─ ...
└─ AnimationPlayer (Animation playback)

Retrieving Bone Information

If you need to check what bones your imported model has, you can list them all from script.

var skeleton = $MeshInstance3D.find_child("Skeleton3D", true, false)
print("Bone count: ", skeleton.get_bone_count())
print("Bone names:")
for i in skeleton.get_bone_count():
    print("  ", skeleton.get_bone_name(i))

# Get the index of a specific bone
var head_bone_idx = skeleton.find_bone("Head")
if head_bone_idx != -1:
    var pose = skeleton.get_bone_pose(head_bone_idx)
    print("Head position: ", pose.origin)

Playing Animations with AnimationPlayer

Now that you understand the skeleton, let's bring your character to life by playing some animations.

AnimationPlayer is the most fundamental animation playback node. When you import a 3D model, an AnimationPlayer is automatically created and ready to use.

Basic Playback

@onready var anim_player = $AnimationPlayer

func _ready():
    # List all registered animations
    print("Registered animations:")
    for anim_name in anim_player.get_animation_list():
        print("  ", anim_name)

    # Play an animation
    if anim_player.has_animation("idle"):
        anim_player.play("idle")

func walk():
    anim_player.play("walk")

func run():
    anim_player.play("run")

Setting Blend Times

If transitions look jerky, it's because the animation switches instantly with no blending. set_blend_time() creates smooth crossfades between animations.

# Set blend time between animations (in seconds)
anim_player.set_blend_time("idle", "walk", 0.2)
anim_player.set_blend_time("walk", "run", 0.3)
anim_player.set_blend_time("run", "idle", 0.4)

func change_to_walk():
    anim_player.play("walk")  # Transitions from idle to walk over 0.2 seconds

Adjusting Playback Speed

# Normal speed
anim_player.play("walk", -1, 1.0)

# Double speed
anim_player.play("walk", -1, 2.0)

# Half speed (slow motion)
anim_player.play("walk", -1, 0.5)

# Reverse playback
anim_player.play_backwards("walk")

Signal-Based Control

Signals are handy for sequencing actions -- for example, returning to idle after an attack animation finishes.

func _ready():
    anim_player.animation_finished.connect(_on_animation_finished)
    anim_player.play("attack")

func _on_animation_finished(anim_name: String):
    if anim_name == "attack":
        print("Attack animation finished")
        anim_player.play("idle")  # Return to idle state

State Management with AnimationTree

AnimationPlayer works fine for simple playback, but as you add more states -- walk, run, jump, attack -- managing transitions in code quickly becomes unwieldy. That's where AnimationTree comes in.

AnimationTree is an advanced system for managing multiple animations through state machines and blending. It lets you handle complex transitions like walk, run, and attack with minimal code.

Basic Setup

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

func _ready():
    anim_tree.active = true  # Enable the AnimationTree

func _process(delta):
    var velocity = get_velocity()

    if velocity.length() < 0.1:
        state_machine.travel("idle")
    elif Input.is_action_pressed("sprint"):
        state_machine.travel("run")
    else:
        state_machine.travel("walk")

Building a State Machine (Editor)

You can design state transitions visually in the editor.

  1. Add an AnimationTree node
  2. In the Inspector, set Tree Root > AnimationNodeStateMachine
  3. In the AnimationTree panel at the bottom:
    • Use "Add Animation" to add idle, walk, and run nodes
    • Right-click each node and select "Connect to..." to create transitions
    • Select a transition line and set "Xfade Time" to 0.2

Programmatic State Transitions

# Force a state change
state_machine.travel("attack")

# Get the current state
var current_state = state_machine.get_current_node()
print("Current state: ", current_state)

# Set parameters (for BlendSpace)
anim_tree.set("parameters/movement/blend_position", velocity.length())

Directional Control with BlendSpace

State machines handle discrete state transitions, but for continuously varying values like movement speed or direction, BlendSpace is the right tool.

BlendSpace1D/2D blends multiple animations based on input values. For example, idle at speed 0, walk at speed 5, run at speed 10 -- smoothly transitioning between them.

BlendSpace1D Example (Movement Speed)

# Create a BlendSpace1D node in the editor with these settings:
# - Min/Max: 0.0 / 10.0
# - Add Blend Point:
#   - 0.0 = idle
#   - 3.0 = walk
#   - 10.0 = run

# Control from code
var speed = velocity.length()
anim_tree.set("parameters/movement_blend/blend_position", speed)

BlendSpace2D Example (8-Directional Movement)

For action RPGs or other games where characters move in all directions, a 2D blend space lets you smoothly interpolate between directional animations.

# Create a BlendSpace2D node in the editor with these settings:
# - X/Y axis: -1.0 / 1.0
# - Add Blend Point:
#   - (0, 1) = walk_forward
#   - (0, -1) = walk_backward
#   - (1, 0) = walk_right
#   - (-1, 0) = walk_left
#   - (1, 1) = walk_forward_right
#   # ...other directions

# Control from code
var direction = Vector2(
    Input.get_axis("move_left", "move_right"),
    Input.get_axis("move_back", "move_forward")
).normalized()

anim_tree.set("parameters/locomotion/blend_position", direction)

Dynamic Pose Adjustment with SkeletonIK3D

Animation data alone can't handle situations like planting feet on uneven terrain or reaching toward a moving object. This is where Inverse Kinematics (IK) comes in for real-time pose adjustment.

tips: SkeletonIK3D is being deprecated in Godot 4.x in favor of the SkeletonModifier3D system. For new projects, consider using SkeletonModifier3D instead. SkeletonIK3D still works in existing projects but may be removed in future versions.

SkeletonIK3D uses Inverse Kinematics to dynamically adjust poses -- for example, planting feet on uneven ground or reaching hands toward a target. Use this for situations that animation alone can't handle, like adapting to terrain variations.

Basic Setup

# Scene structure
# Skeleton3D
# └─ SkeletonIK3D (Foot IK)
#    - Root Bone: "Hips"
#    - Tip Bone: "FootL"
#    - Target: Node3D node (ground position)

@onready var foot_ik = $Skeleton3D/FootIK_L
@onready var ground_target = $GroundTarget_L

func _ready():
    foot_ik.start()  # Start IK

func _process(delta):
    # Detect ground position
    var space_state = get_world_3d().direct_space_state
    var query = PhysicsRayQueryParameters3D.create(
        global_position + Vector3.UP,
        global_position + Vector3.DOWN * 10
    )
    var result = space_state.intersect_ray(query)

    if result:
        ground_target.global_position = result.position

Controlling IK

# Adjust IK influence (0.0 = disabled, 1.0 = fully applied)
foot_ik.interpolation = 1.0  # Fully applied
foot_ik.interpolation = 0.5  # 50% blend

# Temporarily disable IK
foot_ik.stop()
# ...
foot_ik.start()

Performance Optimization

With all these features, you can build rich animations, but in scenes with many characters, the processing cost adds up fast. Here are some techniques to keep things running smoothly.

LOD-Based Animation Update Frequency

A character barely visible at the edge of the screen doesn't need the same animation precision as the player character. Reduce the update rate based on camera distance.

# Reduce skeleton update frequency based on distance
func _process(delta):
    var distance = global_position.distance_to(camera.global_position)

    if distance > 50.0:
        # Far distance: completely stop animation updates
        anim_tree.active = false
    elif distance > 20.0:
        # Mid distance: skip every other frame to halve update frequency
        anim_tree.active = true
        if Engine.get_process_frames() % 2 != 0:
            return
    else:
        # Close distance: normal updates
        anim_tree.active = true

tips: The advance() method requires active = true to work. Setting active = false completely stops the AnimationTree, so you cannot combine it with advance(). For reduced update rates at medium distance, keep the tree active and skip frames instead.

Disabling Unnecessary IK

# Disable IK for non-player characters
if not is_player_character:
    foot_ik.stop()
    hand_ik.stop()

Animation Compression

# Enable compression in the import settings
# Select .glb file > Import > Animation:
# - Compression: Lossy
# - Optimize: true
# - Position Error: 0.01
# - Rotation Error: 0.01

Summary

  • AnimationPlayer handles basic animation playback, controlled via play() and set_blend_time()
  • AnimationTree manages complex animation transitions through a state machine, switching states with travel()
  • BlendSpace1D/2D blends animations based on speed or direction, controlled via blend_position
  • SkeletonIK3D enables dynamic pose adjustments like foot planting and hand reaching (deprecated in Godot 4.x -- consider SkeletonModifier3D for new projects)
  • For better performance, use distance-based LOD, disable unnecessary IK, and enable animation compression
  • Use the animation_finished signal to detect when an animation ends and transition to the next action

Further Reading