Agent skill
animation-system
Implements animation systems using AnimationPlayer, AnimationTree, blend trees, and procedural animation. Use when creating character animations and visual effects.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/animation-system
SKILL.md
Godot Animation System
When implementing animations, use these patterns for smooth and responsive character movement.
AnimationPlayer Basics
Playing Animations
gdscript
extends CharacterBody2D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
func _ready() -> void:
# Connect to animation finished signal
anim_player.animation_finished.connect(_on_animation_finished)
func play_animation(anim_name: String, speed: float = 1.0) -> void:
if anim_player.current_animation != anim_name:
anim_player.play(anim_name)
anim_player.speed_scale = speed
func play_backwards(anim_name: String) -> void:
anim_player.play_backwards(anim_name)
func stop_animation() -> void:
anim_player.stop()
func pause_animation() -> void:
anim_player.pause()
func resume_animation() -> void:
anim_player.play()
func _on_animation_finished(anim_name: String) -> void:
match anim_name:
"attack":
play_animation("idle")
"death":
queue_free()
Animation Blending
gdscript
extends AnimationPlayer
func crossfade_to(anim_name: String, duration: float = 0.2) -> void:
if current_animation == anim_name:
return
# Queue the new animation with crossfade
queue(anim_name)
advance(0) # Start immediately
# Use AnimationPlayer's built-in blending
set_blend_time(current_animation, anim_name, duration)
# Manual crossfade using AnimationTree is preferred for complex blending
Animation Callbacks
gdscript
extends CharacterBody2D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
func _ready() -> void:
# Add method track call in animation
# Or use animation_finished signal
pass
# Called from animation track
func spawn_projectile() -> void:
var projectile := preload("res://projectile.tscn").instantiate()
projectile.global_position = $ProjectileSpawn.global_position
get_parent().add_child(projectile)
func play_sound(sound_name: String) -> void:
var sound := $Sounds.get_node(sound_name) as AudioStreamPlayer2D
if sound:
sound.play()
func enable_hitbox() -> void:
$Hitbox/CollisionShape2D.disabled = false
func disable_hitbox() -> void:
$Hitbox/CollisionShape2D.disabled = true
AnimationTree
State Machine Setup
gdscript
extends CharacterBody2D
@onready var anim_tree: AnimationTree = $AnimationTree
@onready var state_machine: AnimationNodeStateMachinePlayback = \
anim_tree.get("parameters/playback")
func _ready() -> void:
anim_tree.active = true
func travel_to_state(state_name: String) -> void:
state_machine.travel(state_name)
func force_state(state_name: String) -> void:
state_machine.start(state_name)
func get_current_state() -> String:
return state_machine.get_current_node()
func is_playing(state_name: String) -> bool:
return state_machine.get_current_node() == state_name
func _physics_process(_delta: float) -> void:
update_animation_state()
func update_animation_state() -> void:
if not is_on_floor():
if velocity.y < 0:
travel_to_state("Jump")
else:
travel_to_state("Fall")
elif velocity.length() > 10:
travel_to_state("Run")
else:
travel_to_state("Idle")
Blend Tree Setup
gdscript
extends CharacterBody2D
@onready var anim_tree: AnimationTree = $AnimationTree
func _physics_process(_delta: float) -> void:
update_blend_parameters()
func update_blend_parameters() -> void:
# For BlendSpace2D (8-directional movement)
var input := Input.get_vector("left", "right", "up", "down")
anim_tree.set("parameters/Move/blend_position", input)
# For BlendSpace1D (speed-based)
var speed_ratio := velocity.length() / max_speed
anim_tree.set("parameters/Speed/blend_position", speed_ratio)
# For animation transitions
anim_tree.set("parameters/conditions/is_jumping", not is_on_floor() and velocity.y < 0)
anim_tree.set("parameters/conditions/is_falling", not is_on_floor() and velocity.y > 0)
anim_tree.set("parameters/conditions/is_grounded", is_on_floor())
One-Shot Animations
gdscript
extends CharacterBody2D
@onready var anim_tree: AnimationTree = $AnimationTree
func play_attack() -> void:
# Trigger one-shot animation
anim_tree.set("parameters/Attack/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
func abort_attack() -> void:
anim_tree.set("parameters/Attack/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_ABORT)
func is_attacking() -> bool:
return anim_tree.get("parameters/Attack/active")
func _on_attack_finished() -> void:
# Called when one-shot completes
pass
Layered Animations
gdscript
# AnimationTree structure:
# - AnimationNodeBlendTree
# - Add2 (blend lower and upper body)
# - Input 0: State Machine (lower body: idle, walk, run)
# - Input 1: State Machine (upper body: idle, aim, shoot)
# - Filter: Upper body bones only
extends CharacterBody2D
@onready var anim_tree: AnimationTree = $AnimationTree
func _ready() -> void:
# Set up bone filter for upper body layer
# This is usually done in editor, but can be done in code
pass
func update_animations() -> void:
# Lower body follows movement
var move_state := "idle" if velocity.length() < 10 else "run"
anim_tree.set("parameters/LowerBody/playback").travel(move_state)
# Upper body independent
if is_aiming:
anim_tree.set("parameters/UpperBody/playback").travel("aim")
elif is_shooting:
anim_tree.set("parameters/UpperBody/playback").travel("shoot")
else:
anim_tree.set("parameters/UpperBody/playback").travel("idle")
# Blend amount
anim_tree.set("parameters/Add2/add_amount", 1.0 if is_aiming else 0.0)
Procedural Animation
Look At / Head Tracking
gdscript
extends Node3D
@export var head_bone: String = "Head"
@export var max_angle := 70.0
@export var look_speed := 5.0
var skeleton: Skeleton3D
var head_bone_idx: int
var target: Node3D
func _ready() -> void:
skeleton = $Skeleton3D
head_bone_idx = skeleton.find_bone(head_bone)
func _process(delta: float) -> void:
if not target or head_bone_idx < 0:
return
var head_transform := skeleton.get_bone_global_pose(head_bone_idx)
var head_position := skeleton.to_global(head_transform.origin)
var target_direction := (target.global_position - head_position).normalized()
var local_direction := skeleton.global_transform.basis.inverse() * target_direction
# Calculate rotation to look at target
var target_rotation := Quaternion(Vector3.FORWARD, local_direction)
# Clamp rotation
var angle := target_rotation.get_euler()
angle.x = clamp(angle.x, deg_to_rad(-max_angle), deg_to_rad(max_angle))
angle.y = clamp(angle.y, deg_to_rad(-max_angle), deg_to_rad(max_angle))
var clamped_rotation := Quaternion.from_euler(angle)
# Apply smooth rotation
var current := skeleton.get_bone_pose_rotation(head_bone_idx)
var new_rotation := current.slerp(clamped_rotation, look_speed * delta)
skeleton.set_bone_pose_rotation(head_bone_idx, new_rotation)
Procedural Walk Cycle
gdscript
extends CharacterBody2D
@export var leg_length := 20.0
@export var step_height := 10.0
@export var step_duration := 0.3
@onready var left_foot: Node2D = $LeftFoot
@onready var right_foot: Node2D = $RightFoot
var left_foot_target: Vector2
var right_foot_target: Vector2
var step_progress := 0.0
var is_left_stepping := true
func _physics_process(delta: float) -> void:
if velocity.length() > 10:
update_procedural_walk(delta)
else:
reset_feet()
func update_procedural_walk(delta: float) -> void:
step_progress += delta / step_duration
if step_progress >= 1.0:
step_progress = 0.0
is_left_stepping = not is_left_stepping
calculate_next_step()
# Interpolate foot positions
var stepping_foot := left_foot if is_left_stepping else right_foot
var grounded_foot := right_foot if is_left_stepping else left_foot
var target := left_foot_target if is_left_stepping else right_foot_target
# Arc motion for stepping foot
var t := step_progress
var horizontal := stepping_foot.position.lerp(target, t)
var vertical_offset := sin(t * PI) * step_height
stepping_foot.position = horizontal + Vector2(0, -vertical_offset)
func calculate_next_step() -> void:
var forward := velocity.normalized()
var step_distance := velocity.length() * step_duration
if is_left_stepping:
left_foot_target = left_foot.position + forward * step_distance
else:
right_foot_target = right_foot.position + forward * step_distance
Squash and Stretch
gdscript
extends Sprite2D
@export var squash_amount := 0.3
@export var stretch_amount := 0.2
@export var return_speed := 10.0
var target_scale := Vector2.ONE
func squash() -> void:
target_scale = Vector2(1 + squash_amount, 1 - squash_amount)
func stretch() -> void:
target_scale = Vector2(1 - stretch_amount, 1 + stretch_amount)
func _process(delta: float) -> void:
scale = scale.lerp(target_scale, return_speed * delta)
target_scale = target_scale.lerp(Vector2.ONE, return_speed * delta)
# Usage with physics
extends CharacterBody2D
func _physics_process(delta: float) -> void:
var was_on_floor := is_on_floor()
move_and_slide()
# Landing squash
if is_on_floor() and not was_on_floor:
$Sprite2D.squash()
# Jumping stretch
if Input.is_action_just_pressed("jump") and is_on_floor():
$Sprite2D.stretch()
Screen Shake
gdscript
extends Camera2D
var shake_amount := 0.0
var shake_decay := 5.0
func shake(amount: float, duration: float = 0.2) -> void:
shake_amount = amount
var tween := create_tween()
tween.tween_property(self, "shake_amount", 0.0, duration)
func _process(delta: float) -> void:
if shake_amount > 0:
offset = Vector2(
randf_range(-shake_amount, shake_amount),
randf_range(-shake_amount, shake_amount)
)
else:
offset = Vector2.ZERO
Sprite Animation
AnimatedSprite2D Controller
gdscript
extends CharacterBody2D
@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D
func _physics_process(_delta: float) -> void:
update_animation()
update_facing()
func update_animation() -> void:
if not is_on_floor():
if velocity.y < 0:
sprite.play("jump")
else:
sprite.play("fall")
elif abs(velocity.x) > 10:
sprite.play("run")
else:
sprite.play("idle")
func update_facing() -> void:
if velocity.x > 0:
sprite.flip_h = false
elif velocity.x < 0:
sprite.flip_h = true
func play_attack() -> void:
sprite.play("attack")
await sprite.animation_finished
# Return to idle or movement animation
Frame-Based Events
gdscript
extends AnimatedSprite2D
signal attack_frame
signal step_frame
func _ready() -> void:
frame_changed.connect(_on_frame_changed)
func _on_frame_changed() -> void:
var current_anim := animation
match current_anim:
"attack":
if frame == 3: # Attack connects on frame 3
attack_frame.emit()
"run":
if frame == 2 or frame == 6: # Footstep frames
step_frame.emit()
Animation Tips
Animation Speed Based on Movement
gdscript
extends CharacterBody2D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
@export var walk_speed := 100.0
@export var run_speed := 200.0
func _physics_process(_delta: float) -> void:
var speed := velocity.length()
if speed > 10:
# Scale animation speed with movement speed
var anim_speed := speed / walk_speed
anim_player.speed_scale = clamp(anim_speed, 0.5, 2.0)
anim_player.play("walk")
else:
anim_player.speed_scale = 1.0
anim_player.play("idle")
Root Motion
gdscript
extends CharacterBody2D
@onready var anim_tree: AnimationTree = $AnimationTree
var root_motion_position := Vector2.ZERO
func _physics_process(delta: float) -> void:
# Get root motion from animation
var root_motion := anim_tree.get_root_motion_position()
# Apply root motion as velocity
if root_motion.length() > 0:
velocity = Vector2(root_motion.x, root_motion.z) / delta
else:
# Normal movement when no root motion
apply_movement_input()
move_and_slide()
Animation Retargeting
gdscript
# When sharing animations between different skeletons
extends Skeleton3D
@export var source_skeleton: Skeleton3D
@export var bone_mapping: Dictionary # {source_bone: target_bone}
func _process(_delta: float) -> void:
if not source_skeleton:
return
for source_bone in bone_mapping:
var target_bone: String = bone_mapping[source_bone]
var source_idx := source_skeleton.find_bone(source_bone)
var target_idx := find_bone(target_bone)
if source_idx >= 0 and target_idx >= 0:
var pose := source_skeleton.get_bone_pose(source_idx)
set_bone_pose_rotation(target_idx, pose.basis.get_rotation_quaternion())
Didn't find tool you were looking for?