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.
- Add an AnimationTree node
- In the Inspector, set Tree Root > AnimationNodeStateMachine
- In the AnimationTree panel at the bottom:
- Use "Add Animation" to add
idle,walk, andrunnodes - Right-click each node and select "Connect to..." to create transitions
- Select a transition line and set "Xfade Time" to 0.2
- Use "Add Animation" to add
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 requiresactive = trueto work. Settingactive = falsecompletely stops the AnimationTree, so you cannot combine it withadvance(). 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()andset_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_finishedsignal to detect when an animation ends and transition to the next action