godot-animation-player
SKILL.md
AnimationPlayer
Expert guidance for Godot's timeline-based keyframe animation system.
NEVER Do
- NEVER forget RESET tracks — Without a RESET track, animated properties don't restore to initial values when changing scenes. Create RESET animation with all default states.
- NEVER use Animation.CALL_MODE_CONTINUOUS for function calls — Calls method EVERY frame during keyframe. Use CALL_MODE_DISCRETE (calls once). Continuous causes spam.
- NEVER animate resource properties directly — Animating
material.albedo_colorcreates embedded resources, bloating file size. Store material in variable, animate variable's properties. - NEVER use animation_finished for looping animations — Signal doesn't fire for looped animations. Use
animation_loopedor checkcurrent_animationin _process(). - NEVER hardcode animation names as strings everywhere — Use constants or enums. Typos cause silent failures.
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
audio_sync_tracks.gd
Sub-frame audio synchronization via Animation.TYPE_AUDIO tracks. Footstep setup with automatic blend handling for cross-fades.
programmatic_anim.gd
Procedural animation generation: creates Animation resources via code with keyframes, easing, and transition curves for dynamic runtime animations.
Track Types Deep Dive
Value Tracks (Property Animation)
# Animate ANY property: position, color, volume, custom variables
var anim := Animation.new()
anim.length = 2.0
# Position track
var pos_track := anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(pos_track, ".:position")
anim.track_insert_key(pos_track, 0.0, Vector2(0, 0))
anim.track_insert_key(pos_track, 1.0, Vector2(100, 0))
anim.track_set_interpolation_type(pos_track, Animation.INTERPOLATION_CUBIC)
# Color track (modulate)
var color_track := anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(color_track, "Sprite2D:modulate")
anim.track_insert_key(color_track, 0.0, Color.WHITE)
anim.track_insert_key(color_track, 2.0, Color.TRANSPARENT)
$AnimationPlayer.add_animation("fade_move", anim)
$AnimationPlayer.play("fade_move")
Method Tracks (Function Calls)
# Call functions at specific timestamps
var method_track := anim.add_track(Animation.TYPE_METHOD)
anim.track_set_path(method_track, ".") # Path to node
# Insert method calls
anim.track_insert_key(method_track, 0.5, {
"method": "spawn_particle",
"args": [Vector2(50, 50)]
})
anim.track_insert_key(method_track, 1.5, {
"method": "play_sound",
"args": ["res://sounds/explosion.ogg"]
})
# CRITICAL: Set call mode to DISCRETE
anim.track_set_call_mode(method_track, Animation.CALL_MODE_DISCRETE)
# Methods must exist on target node:
func spawn_particle(pos: Vector2) -> void:
# Spawn particle at position
pass
func play_sound(sound_path: String) -> void:
$AudioStreamPlayer.stream = load(sound_path)
$AudioStreamPlayer.play()
Audio Tracks
# Synchronize audio with animation
var audio_track := anim.add_track(Animation.TYPE_AUDIO)
anim.track_set_path(audio_track, "AudioStreamPlayer")
# Insert audio playback
var audio_stream := load("res://sounds/footstep.ogg")
anim.audio_track_insert_key(audio_track, 0.3, audio_stream)
anim.audio_track_insert_key(audio_track, 0.6, audio_stream) # Second footstep
# Set volume for specific key
anim.audio_track_set_key_volume(audio_track, 0, 1.0) # Full volume
anim.audio_track_set_key_volume(audio_track, 1, 0.7) # Quieter
Bezier Tracks (Custom Curves)
# For smooth, custom interpolation curves
var bezier_track := anim.add_track(Animation.TYPE_BEZIER)
anim.track_set_path(bezier_track, ".:custom_value")
# Insert bezier points with handles
anim.bezier_track_insert_key(bezier_track, 0.0, 0.0)
anim.bezier_track_insert_key(bezier_track, 1.0, 100.0,
Vector2(0.5, 0), # In-handle
Vector2(-0.5, 0)) # Out-handle
# Read value in _process
func _process(delta: float) -> void:
var value := $AnimationPlayer.get_bezier_value("custom_value")
# Use value for custom effects
Root Motion Extraction
Problem: Animated Movement Disconnected from Physics
# Character walks in animation, but position doesn't change in world
# Animation modifies Skeleton bone, not CharacterBody3D root
Solution: Root Motion
# Scene structure:
# CharacterBody3D (root)
# ├─ MeshInstance3D
# │ └─ Skeleton3D
# └─ AnimationPlayer
# AnimationPlayer setup:
@onready var anim_player: AnimationPlayer = $AnimationPlayer
func _ready() -> void:
# Enable root motion (point to root bone)
anim_player.root_motion_track = NodePath("MeshInstance3D/Skeleton3D:root")
anim_player.play("walk")
func _physics_process(delta: float) -> void:
# Extract root motion
var root_motion_pos := anim_player.get_root_motion_position()
var root_motion_rot := anim_player.get_root_motion_rotation()
var root_motion_scale := anim_player.get_root_motion_scale()
# Apply to CharacterBody3D
var transform := Transform3D(basis.rotated(basis.y, root_motion_rot.y), Vector3.ZERO)
transform.origin = root_motion_pos
global_transform *= transform
# Velocity from root motion
velocity = root_motion_pos / delta
move_and_slide()
Animation Sequences & Queueing
Chaining Animations
# Play animations in sequence
@onready var anim: AnimationPlayer = $AnimationPlayer
func play_attack_combo() -> void:
anim.play("attack_1")
await anim.animation_finished
anim.play("attack_2")
await anim.animation_finished
anim.play("idle")
# Or use queue:
func play_with_queue() -> void:
anim.play("attack_1")
anim.queue("attack_2")
anim.queue("idle") # Auto-plays after attack_2
Blend Times
# Smooth transitions between animations
anim.play("walk")
# 0.5s blend from walk → run
anim.play("run", -1, 1.0, 0.5) # custom_blend = 0.5
# Or set default blend
anim.set_default_blend_time(0.3) # 0.3s for all transitions
anim.play("idle")
RESET Track Pattern
Problem: Properties Don't Reset
# Animate sprite position from (0,0) → (100, 0)
# Change scene, sprite stays at (100, 0)!
Solution: RESET Animation
# Create RESET animation with default values
var reset_anim := Animation.new()
reset_anim.length = 0.01 # Very short
var track := reset_anim.add_track(Animation.TYPE_VALUE)
reset_anim.track_set_path(track, "Sprite2D:position")
reset_anim.track_insert_key(track, 0.0, Vector2(0, 0)) # Default position
track = reset_anim.add_track(Animation.TYPE_VALUE)
reset_anim.track_set_path(track, "Sprite2D:modulate")
reset_anim.track_insert_key(track, 0.0, Color.WHITE) # Default color
anim_player.add_animation("RESET", reset_anim)
# AnimationPlayer automatically plays RESET when scene loads
# IF "Reset on Save" is enabled in AnimationPlayer settings
Procedural Animation Generation
Generate Animation from Code
# Create bounce animation programmatically
func create_bounce_animation() -> void:
var anim := Animation.new()
anim.length = 1.0
anim.loop_mode = Animation.LOOP_LINEAR
# Position track (Y bounce)
var track := anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(track, ".:position:y")
# Generate sine wave keyframes
for i in range(10):
var time := float(i) / 9.0 # 0.0 to 1.0
var value := sin(time * TAU) * 50.0 # Bounce height 50px
anim.track_insert_key(track, time, value)
anim.track_set_interpolation_type(track, Animation.INTERPOLATION_CUBIC)
$AnimationPlayer.add_animation("bounce", anim)
$AnimationPlayer.play("bounce")
Advanced Patterns
Play Animation Backwards
# Play animation in reverse (useful for closing doors, etc.)
anim.play("door_open", -1, -1.0) # speed = -1.0 = reverse
# Pause and reverse
anim.pause()
anim.play("current_animation", -1, -1.0, false) # from_end = false
Animation Callbacks (Signal-Based)
# Emit custom signal at specific frame
func _ready() -> void:
$AnimationPlayer.animation_finished.connect(_on_anim_finished)
func _on_anim_finished(anim_name: String) -> void:
match anim_name:
"attack":
deal_damage()
"die":
queue_free()
Seek to Specific Time
# Jump to 50% through animation
anim.seek(anim.current_animation_length * 0.5)
# Scrub through animation (cutscene editor)
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion and scrubbing:
var normalized_pos := event.position.x / get_viewport_rect().size.x
anim.seek(anim.current_animation_length * normalized_pos)
Performance Optimization
Disable When Off-Screen
extends VisibleOnScreenNotifier2D
func _ready() -> void:
screen_exited.connect(_on_screen_exited)
screen_entered.connect(_on_screen_entered)
func _on_screen_exited() -> void:
$AnimationPlayer.pause()
func _on_screen_entered() -> void:
$AnimationPlayer.play()
Edge Cases
Animation Not Playing
# Problem: Forgot to add animation to player
# Solution: Check if animation exists
if anim.has_animation("walk"):
anim.play("walk")
else:
push_error("Animation 'walk' not found!")
# Better: Use constants
const ANIM_WALK = "walk"
const ANIM_IDLE = "idle"
if anim.has_animation(ANIM_WALK):
anim.play(ANIM_WALK)
Method Track Not Firing
# Problem: Call mode is CONTINUOUS
# Solution: Set to DISCRETE
var method_track_idx := anim.find_track(".:method_name", Animation.TYPE_METHOD)
anim.track_set_call_mode(method_track_idx, Animation.CALL_MODE_DISCRETE)
Decision Matrix: AnimationPlayer vs Tween
| Feature | AnimationPlayer | Tween |
|---|---|---|
| Timeline editing | ✅ Visual editor | ❌ Code only |
| Multiple properties | ✅ Many tracks | ❌ One property |
| Reusable | ✅ Save as resource | ❌ Create each time |
| Dynamic runtime | ❌ Static | ✅ Fully dynamic |
| Method calls | ✅ Method tracks | ❌ Use callbacks |
| Performance | ✅ Optimized | ❌ Slightly slower |
Use AnimationPlayer for: Cutscenes, character animations, complex UI Use Tween for: Simple runtime effects, one-off transitions
Reference
- Master Skill: godot-master
Weekly Installs
51
Repository
thedivergentai/…c-skillsGitHub Stars
35
First Seen
Feb 10, 2026
Security Audits
Installed on
gemini-cli50
opencode50
codex50
kimi-cli49
amp49
github-copilot49