Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
thedivergentai avatar

Godot Animation Player

  • 227 installs
  • 454 repo stars
  • Updated July 28, 2026
  • thedivergentai/gd-agentic-skills

Use godot-animation-player for development tasks

About

godot-animation-player: A skill for development. This provides functionality for development workflows.

  • godot-animation-player

Godot Animation Player by the numbers

  • 227 all-time installs (skills.sh)
  • +14 installs in the week ending Jul 27, 2026 (Skillselion tracking)
  • Ranked #1,701 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-animation-player

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs227
repo stars454
Last updatedJuly 28, 2026
Repositorythedivergentai/gd-agentic-skills

What it does

Use godot-animation-player for development tasks

Files

SKILL.mdMarkdownGitHub ↗

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 [12].
  • NEVER use `Animation.CALL_MODE_CONTINUOUS` for function calls — This calls the method EVERY frame during the keyframe. Use CALL_MODE_DISCRETE (calls once) to avoid logic spam [13, 77].
  • NEVER animate resource properties directly — Animating material.albedo_color creates embedded resources that bloat file size. Store the material in a variable or use instance uniform instead [14].
  • NEVER use `animation_finished` for looping animations — This signal doesn't fire for looped animations. Use animation_looped or check current_animation in _process().
  • NEVER hardcode animation names as strings across large codebases — Use constants or enums. Typos cause silent failures.
  • NEVER use `seek()` without `update=true` for same-frame logic — If you need properties to update immediately (e.g., for physics checks), you MUST set the update parameter to true.
  • NEVER leave unnecessary AnimationPlayers `active` — If an entity is off-screen and its animation is purely visual (no logic tracks), set active = false to save significant CPU/GPU processing [317].
  • NEVER change `AnimationLibrary` content while it is playing — This causes immediate crashes or undefined transform states. Stop the player or wait for the finished signal before swapping libraries.
  • NEVER rely on `speed_scale` for long-term synchronization — For multiplayer or rhythm games, use seek() with a global time reference to prevent frame-drift.

---

Available Scripts

MANDATORY: Read the appropriate script before implementing the corresponding pattern.

method_track_logic.gd

Expert logic triggers using CALL_MODE_DISCRETE for high-precision hitbox and state management.

runtime_anim_lib_swapper.gd

Managing multiple AnimationLibrary resources (Stances, Weapons) on a single AnimationPlayer.

dynamic_shader_animation.gd

Animating shader uniforms (e.g., dissolve, glow) in sync with timeline keyframes.

procedural_track_modifier.gd

Runtime modification of existing tracks (e.g., jump height tweaking) without creating new Animation resources.

reset_track_orchestrator.gd

Pattern for forced, immediate state resets across complex multi-track node setups.

bezier_curve_extraction.gd

Extracting numeric data from Bezier tracks at runtime to drive procedural VFX or physics.

active_animation_culler.gd

Performance optimization: using VisibleOnScreenNotifier to disable AnimationPlayer.active.

root_motion_physics_sync.gd

Expert 3D CharacterBody motion extraction using get_root_motion_position.

character_part_swapper_tracks.gd

Character customization (equipment/slots) managed entirely through Animation timeline tracks.

precise_audio_sync.gd

Perfectly timed SFX using TYPE_AUDIO tracks with volume, pitch, and start-offset control.

---

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

FeatureAnimationPlayerTween
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

---

Expert Pattern: Shared-Animation-Library

Efficiently reuse animation data across multiple different models (e.g., all humanoid NPCs) by decoupling animations into an AnimationLibrary resource. This prevents VRAM and memory bloat from duplicated tracks.

class_name SharedAnimationManager extends Node

@export var shared_library: AnimationLibrary
@onready var anim_player: AnimationPlayer = $AnimationPlayer

func _ready() -> void:
    # 1. Inject the shared library under a unique key
    anim_player.add_animation_library(&"shared_human", shared_library)
    
    # 2. Access animations using the 'library/animation' syntax
    anim_player.play(&"shared_human/walk")

---

Expert Pattern: Animation-Event-Signaling

Instead of calling hardcoded functions directly from method tracks, use a generalized "Signaler" pattern to decouple the animation timeline from gameplay logic.

class_name AnimationSignaler extends Node

## Emitted when the animation reaches a marked event key
signal animation_event(event_type: String)

## Generic receiver for AnimationPlayer Method Tracks
func emit_event(event_type: String) -> void:
    animation_event.emit(event_type)

# Setup in AnimationPlayer:
# 1. Add Method Track pointing to this node
# 2. Keyframe: method="emit_event", args=["spawn_footstep_vfx"]
# 3. Other systems connect to 'animation_event' signal

---

Expert Pattern: Animation-Budget-Manager

Save significant CPU time in scenes with many characters by manually controlling the animation processing frequency based on visibility.

class_name AnimationBudgetManager extends Node3D

@onready var anim_player: AnimationPlayer = $AnimationPlayer
@onready var visibility_notifier: VisibleOnScreenNotifier3D = $VisibleOnScreenNotifier3D

func _ready() -> void:
    # 1. Disable automatic engine processing
    anim_player.callback_mode_process = AnimationMixer.ANIMATION_CALLBACK_MODE_PROCESS_MANUAL

func _process(delta: float) -> void:
    # 2. Cull updates for off-screen entities
    if not visibility_notifier.is_on_screen():
        return
        
    # 3. Manually step the animation forward
    # Optional: Throttle updates (e.g., only call every 2nd frame) for distant entities
    anim_player.advance(delta)

Reference

  • Master Skill: godot-master

Related skills

Backend & APIsbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.