- 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_color
- creates 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_looped
- or check
- current_animation
- in _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