Claude
Skills
Sign in
Back

godot-animation-player

Included with Lifetime
$97 forever

Expert patterns for AnimationPlayer including track types (Value, Method, Audio, Bezier), root motion extraction, animation callbacks, procedural animation generation, call mode optimization, and RESET tracks. Use for timeline-based animations, cutscenes, or UI transitions. Trigger keywords: AnimationPlayer, Animation, track_insert_key, root_motion, animation_finished, RESET_track, call_mode, animation_set_next, queue, blend_times.

Designscripts

What this skill does


# 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](scripts/method_track_logic.gd)
Expert logic triggers using `CALL_MODE_DISCRETE` for high-precision hitbox and state management.

### [runtime_anim_lib_swapper.gd](scripts/runtime_anim_lib_swapper.gd)
Managing multiple `AnimationLibrary` resources (Stances, Weapons) on a single `AnimationPlayer`.

### [dynamic_shader_animation.gd](scripts/dynamic_shader_animation.gd)
Animating shader uniforms (e.g., dissolve, glow) in sync with timeline keyframes.

### [procedural_track_modifier.gd](scripts/procedural_track_modifier.gd)
Runtime modification of existing tracks (e.g., jump height tweaking) without creating new Animation resources.

### [reset_track_orchestrator.gd](scripts/reset_track_orchestrator.gd)
Pattern for forced, immediate state resets across complex multi-track node setups.

### [bezier_curve_extraction.gd](scripts/bezier_curve_extraction.gd)
Extracting numeric data from Bezier tracks at runtime to drive procedural VFX or physics.

### [active_animation_culler.gd](scripts/active_animation_culler.gd)
Performance optimization: using `VisibleOnScreenNotifier` to disable `AnimationPlayer.active`.

### [root_motion_physics_sync.gd](scripts/root_motion_physics_sync.gd)
Expert 3D CharacterBody motion extraction using `get_root_motion_position`.

### [character_part_swapper_tracks.gd](scripts/character_part_swapper_tracks.gd)
Character customization (equipment/slots) managed entirely through Animation timeline tracks.

### [precise_audio_sync.gd](scripts/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)

```gdscript
# 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)

```gdscript
# 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

```gdscript
# 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)

```gdscript
# 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

```gdscript
# Character walks in animation, but position doesn't change in world
# Animation modifies Skeleton bone, not CharacterBody3D root
```

### Solution: Root Motion

```gdscript
# 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

```gdscript
# 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 

Related in Design