
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-playerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 227 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-animation-player for development tasks
Files
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_colorcreates embedded resources that bloat file size. Store the material in a variable or useinstance uniforminstead [14]. - NEVER use `animation_finished` for looping animations — This signal doesn't fire for looped animations. Use
animation_loopedor checkcurrent_animationin_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
updateparameter totrue. - NEVER leave unnecessary AnimationPlayers `active` — If an entity is off-screen and its animation is purely visual (no logic tracks), set
active = falseto 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
finishedsignal 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) # QuieterBezier 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 rootSolution: 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_2Blend 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 = falseAnimation 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
---
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
# active_animation_culler.gd
# High-performance: Disabling AnimationPlayers when not visible [317]
extends VisibleOnScreenNotifier3D
@onready var anim_player: AnimationPlayer = get_node("../AnimationPlayer")
func _ready() -> void:
screen_entered.connect(_on_visible)
screen_exited.connect(_on_invisible)
func _on_visible() -> void:
# Resume processing
anim_player.active = true
# Optional: speed up to catch up to global sync time if needed
# anim_player.advance(delta_since_exit)
func _on_invisible() -> void:
# Disabling 'active' stops all track processing saving CPU/GPU
# significantly more than 'pause()'.
anim_player.active = false
# skills/animation-player/scripts/animation_sequencer.gd
extends AnimationPlayer
## Animation Sequencer Expert Pattern
## Advanced animation chaining with callbacks and branching logic.
class_name AnimationSequencer
signal sequence_started(sequence_name: String)
signal sequence_completed(sequence_name: String)
signal animation_in_sequence_finished(anim_name: String, index: int)
var _current_sequence: Array[Dictionary] = []
var _sequence_index := 0
var _is_sequence_playing := false
func play_sequence(animations: Array[String], sequence_name := "") -> void:
if animations.is_empty():
return
_current_sequence.clear()
for anim_name in animations:
_current_sequence.append({"animation": anim_name, "callback": Callable()})
_sequence_index = 0
_is_sequence_playing = true
sequence_started.emit(sequence_name)
_play_next_in_sequence()
func play_sequence_with_callbacks(sequence: Array[Dictionary]) -> void:
# sequence = [{animation: "walk", callback: Callable}, ...]
_current_sequence = sequence.duplicate()
_sequence_index = 0
_is_sequence_playing = true
_play_next_in_sequence()
func _play_next_in_sequence() -> void:
if _sequence_index >= _current_sequence.size():
_is_sequence_playing = false
sequence_completed.emit("")
return
var entry: Dictionary = _current_sequence[_sequence_index]
var anim_name: String = entry.animation
if not has_animation(anim_name):
push_warning("Animation '%s' not found, skipping" % anim_name)
_sequence_index += 1
_play_next_in_sequence()
return
play(anim_name)
# Execute callback if provided
if entry.has("callback") and entry.callback.is_valid():
entry.callback.call()
# Wait for completion
if not animation_finished.is_connected(_on_sequence_anim_finished):
animation_finished.connect(_on_sequence_anim_finished)
func _on_sequence_anim_finished(anim_name: String) -> void:
if not _is_sequence_playing:
return
animation_in_sequence_finished.emit(anim_name, _sequence_index)
_sequence_index += 1
_play_next_in_sequence()
func stop_sequence() -> void:
_is_sequence_playing = false
_current_sequence.clear()
_sequence_index = 0
stop()
## EXPERT USAGE:
## var sequencer := AnimationSequencer.new()
##
## # Simple sequence
## sequencer.play_sequence(["attack_1", "attack_2", "idle"])
##
## # With callbacks
## sequencer.play_sequence_with_callbacks([
## {animation: "windup", callback: func(): print("Winding up!")},
## {animation: "strike", callback: func(): deal_damage()},
## {animation: "recovery", callback: Callable()}
## ])
# skills/animation-player/code/audio_sync_tracks.gd
extends AnimationPlayer
## Sub-Frame Audio Sync Expert Pattern
## Technical blueprints for integrating AudioStreamPlayer tracks.
func setup_footstep_audio(sfx_node: AudioStreamPlayer2D) -> void:
var anim: Animation = get_animation("walk")
if not anim: return
# 1. Add an Audio Track
var track_index := anim.add_track(Animation.TYPE_AUDIO)
anim.track_set_path(track_index, str(get_path_to(sfx_node)))
# 2. Insert the Sound Trigger at the foot-fall frame (e.g. 0.3s)
# The 'stream' is used from the AudioStreamPlayer2D node.
anim.audio_track_insert_key(track_index, 0.3, sfx_node.stream)
# 3. Enable 'Use Blend' to handle cross-fades between animations
anim.audio_track_set_use_blend(track_index, true)
## EXPERT NOTE:
## Using Audio Tracks is superior to 'get_node().play()' via Method Tracks
## because Audio Tracks automatically handle stopping/fading when
## animations are interrupted or blended.
# bezier_curve_extraction.gd
# Extracting Bezier track data for custom physics/logic [115]
extends Node
func get_bezier_at_runtime(anim: Animation, track_path: String, time: float) -> float:
var track_idx = anim.find_track(track_path, Animation.TYPE_BEZIER)
if track_idx == -1: return 0.0
# Expert: bezier_track_interpolate returns the exact value at 'time'
# accounting for handle lengths and angles.
return anim.bezier_track_interpolate(track_idx, time)
# Example: Use Bezier to drive a custom particle emission rate
func _process(delta: float) -> void:
if $AnimationPlayer.is_playing():
var anim = $AnimationPlayer.get_animation($AnimationPlayer.current_animation)
var t = $AnimationPlayer.current_animation_position
var rate = get_bezier_at_runtime(anim, ".:emission_rate", t)
$GPUParticles3D.amount_ratio = clamp(rate, 0.0, 1.0)
# character_part_swapper_tracks.gd
# Swapping meshes/textures via AnimationPlayer tracks for customization
extends Node3D
# This pattern uses Value tracks with NodePath properties
# to enable/disable specific character accessories during animations.
func setup_sheathe_track(anim: Animation) -> void:
var track_idx = anim.add_track(Animation.TYPE_VALUE)
# At the start of 'sheathe', sword in hand is visible
anim.track_set_path(track_idx, "Skeleton3D/HandSlot/Sword:visible")
anim.track_insert_key(track_idx, 0.0, true)
anim.track_insert_key(track_idx, 0.5, false) # Hidden when put away
var sheath_track = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(sheath_track, "Skeleton3D/BackSlot/SwordSheath:visible")
anim.track_insert_key(sheath_track, 0.0, false)
anim.track_insert_key(sheath_track, 0.5, true) # Shown on back
# dynamic_shader_animation.gd
# Animating Shader Uniforms via AnimationPlayer for VFX sync
extends MeshInstance3D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
func setup_dissolve_track(anim: Animation) -> void:
# Note: Shader uniforms are accessed via the material property path
var track_idx = anim.add_track(Animation.TYPE_VALUE)
# Path format: "MeshInstance3D:material_override:shader_parameter/dissolve_amount"
# Or use index 0 for the first material
anim.track_set_path(track_idx, ".:material_override:shader_parameter/dissolve_amount")
anim.track_insert_key(track_idx, 0.0, 0.0)
anim.track_insert_key(track_idx, 1.0, 1.0)
# Use Cubic interpolation for smoother visual transitions
anim.track_set_interpolation_type(track_idx, Animation.INTERPOLATION_CUBIC)
# method_track_logic.gd
# Using Method Tracks for high-precision game logic triggers [13]
extends Node
# EXPERT NOTE: Always use CALL_MODE_DISCRETE for logic to avoid
# accidental double-triggers on frame boundaries.
func setup_method_track(anim: Animation) -> void:
var track_idx = anim.add_track(Animation.TYPE_METHOD)
anim.track_set_path(track_idx, ".")
# Trigger a logic event at 0.5s
anim.track_insert_key(track_idx, 0.5, {
"method": "_on_hitbox_active",
"args": [true]
})
# Deactivate at 0.8s
anim.track_insert_key(track_idx, 0.8, {
"method": "_on_hitbox_active",
"args": [false]
})
# CRITICAL: Discrete mode ensures the method is called exactly once.
anim.track_set_call_mode(track_idx, Animation.CALL_MODE_DISCRETE)
func _on_hitbox_active(active: bool) -> void:
print("Hitbox state changed: ", active)
# Logic for enabling/disabling Area3D/2D hitboxes
# precise_audio_sync.gd
# Using TYPE_AUDIO tracks for perfect timing with pitch/volume control [93]
extends AnimationPlayer
func add_dynamic_sfx(anim: Animation, stream: AudioStream, time: float) -> void:
var track_idx = anim.add_track(Animation.TYPE_AUDIO)
track_set_path(track_idx, "AudioStreamPlayer")
# Expert: Audio tracks handle polyphony and volume ramping internally
anim.audio_track_insert_key(track_idx, time, stream)
# Control volume (-60 to 24 dB)
anim.audio_track_set_key_volume(track_idx, 0, -3.0)
# Use START_OFFSET to skip introductory silence in long files
anim.audio_track_set_key_start_offset(track_idx, 0, 0.1)
# procedural_track_modifier.gd
# Modifying specific animation tracks via code at runtime [5]
extends AnimationPlayer
func tweak_jump_height(new_height: float) -> void:
var anim: Animation = get_animation("jump")
# Find the track index for the Y position
var track_idx = anim.find_track(".:position:y", Animation.TYPE_VALUE)
if track_idx != -1:
# Update the peak keyframe (usually in the middle)
# Expert: Use track_set_key_value instead of deleting/re-adding
var key_idx = 1 # Assume index 1 is the peak
anim.track_set_key_value(track_idx, key_idx, -new_height)
# If you need immediate visual feedback while paused:
if not is_playing():
seek(current_animation_position, true)
# skills/animation-player/code/programmatic_anim.gd
extends Node
## Programmatic Track Generation Expert Pattern
## Technical blueprints for building Animation resources via code.
func create_procedural_transition(node: Node3D, target_pos: Vector3) -> Animation:
var anim := Animation.new()
anim.length = 1.0
# 1. Add a Property Track for 'position'
var track_index := anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(track_index, str(get_path_to(node)) + ":position")
# 2. Insert Keyframes
anim.track_insert_key(track_index, 0.0, node.position)
anim.track_insert_key(track_index, 1.0, target_pos)
# 3. Set Easing (Cubic Bezier)
anim.track_set_key_transition(track_index, 0, 0.5) # Smoothing-in
# 4. Expert: Enable Storage Compression (Godot 4+)
# This reduces memory footprint for long procedural animations.
# anim.step = 0.01
return anim
## WHY THIS WAY?
## Procedural animations allow for dynamic transitions that can't be baked
## into FBX files, such as moving to a dynamically calculated point in world space.
# reset_track_orchestrator.gd
# Robust RESET track management for multi-layered animations [12]
extends Node
@onready var anim_player: AnimationPlayer = $AnimationPlayer
# EXPERT NOTE: If you have many entities, manually calling RESET
# can be faster than letting Godot's auto-reset trigger layout updates.
func force_hard_reset() -> void:
if anim_player.has_animation("RESET"):
# Seek(0, true) forces immediate property application
# even if the player is paused.
anim_player.play("RESET")
anim_player.advance(0) # Immediate update
anim_player.stop()
func play_safe(anim_name: String) -> void:
# Always ensure we start from a clean slate if the previous
# animation modified persistent state.
force_hard_reset()
anim_player.play(anim_name)
# root_motion_physics_sync.gd
# Expert Root Motion extraction for CharacterBody3D [155]
extends CharacterBody3D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
func _physics_process(delta: float) -> void:
# 1. Fetch the delta transform from the root bone animation
var motion_pos = anim_player.get_root_motion_position()
var motion_rot = anim_player.get_root_motion_rotation()
# 2. Transform the animation delta into world space relative to current orientation
var v = (quaternion * motion_pos) / delta
# 3. Apply horizontal velocity while preserving vertical (gravity)
velocity.x = v.x
velocity.z = v.z
# 4. Integrate rotation (usually only Y axis for 3D characters)
quaternion *= motion_rot
if not is_on_floor():
velocity.y -= 9.8 * delta
move_and_slide()
# runtime_anim_lib_swapper.gd
# Swapping AnimationLibraries at runtime for massive character variety
extends AnimationPlayer
# AnimationLibraries allow grouping animations. Swapping them
# allows different "Stances" or "Weapon Sets" to share the same
# AnimationPlayer node.
func load_stance_library(lib_path: String, stance_name: String) -> void:
var new_lib: AnimationLibrary = load(lib_path)
# Remove old if it exists
if has_animation_library(stance_name):
remove_animation_library(stance_name)
# Add the new set (e.g., "sword_stance", "bow_stance")
add_animation_library(stance_name, new_lib)
# Play from the specific library
# Format: "lib_name/anim_name"
play(stance_name + "/idle")