godot-2d-animation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

2D Animation

2D动画

Expert-level guidance for frame-based and skeletal 2D animation in Godot.
Godot中基于帧和骨骼的2D动画专家级指南。

NEVER Do

绝对不要做的事

  • NEVER use
    animation_finished
    for looping animations
    — The signal only fires on non-looping animations. Use
    animation_looped
    instead for loop detection.
  • NEVER call
    play()
    and expect instant state changes
    — AnimatedSprite2D applies
    play()
    on the next process frame. Call
    advance(0)
    immediately after
    play()
    if you need synchronous property updates (e.g., when changing animation + flip_h simultaneously).
  • NEVER set
    frame
    directly when preserving animation progress
    — Setting
    frame
    resets
    frame_progress
    to 0.0. Use
    set_frame_and_progress(frame, progress)
    to maintain smooth transitions when swapping animations mid-frame.
  • NEVER forget to cache
    @onready var anim_sprite
    — The node lookup getter is surprisingly slow in hot paths like
    _physics_process()
    . Always use
    @onready
    .
  • NEVER mix AnimationPlayer tracks with code-driven AnimatedSprite2D — Choose one animation authority per sprite. Mixing causes flickering and state conflicts.

  • 绝对不要对循环动画使用
    animation_finished
    — 该信号仅在非循环动画结束时触发。请改用
    animation_looped
    来检测循环。
  • 绝对不要调用
    play()
    后期望状态立即改变
    — AnimatedSprite2D会在下一个处理帧应用
    play()
    。如果需要同步更新属性(例如同时更改动画和flip_h),请在调用
    play()
    后立即调用
    advance(0)
  • 绝对不要在保留动画进度时直接设置
    frame
    — 设置
    frame
    会将
    frame_progress
    重置为0.0。在帧中途切换动画时,请使用
    set_frame_and_progress(frame, progress)
    来保持平滑过渡。
  • 绝对不要忘记缓存
    @onready var anim_sprite
    — 在
    _physics_process()
    这类热路径中,节点查找的getter速度出奇地慢。请始终使用
    @onready
  • 绝对不要混合使用AnimationPlayer轨道和代码驱动的AnimatedSprite2D — 每个精灵选择一种动画控制方式。混合使用会导致闪烁和状态冲突。

Available Scripts

可用脚本

MANDATORY: Read the appropriate script before implementing the corresponding pattern.
必须:在实现相应模式前阅读对应的脚本。

animation_sync.gd

animation_sync.gd

Method track triggers for frame-perfect logic (SFX/VFX hitboxes), signal-driven async gameplay orchestration, and AnimationTree blend space management. Use when syncing gameplay events to animation frames.
用于帧完美逻辑(音效/特效碰撞箱)、信号驱动的异步游戏编排以及AnimationTree混合空间管理的方法轨道触发器。在将游戏事件与动画帧同步时使用。

animation_state_sync.gd

animation_state_sync.gd

Frame-perfect state-driven animation with transition queueing - essential for responsive character animation.
带过渡队列的帧完美状态驱动动画 — 对响应式角色动画至关重要。

shader_hook.gd

shader_hook.gd

Animating ShaderMaterial uniforms via AnimationPlayer property tracks. Covers hit flash, dissolve effects, and instance uniforms for batched sprites. Use for visual feedback tied to animation states.

通过AnimationPlayer属性轨道为ShaderMaterial uniforms设置动画。涵盖击中闪光、溶解效果以及批量精灵的实例uniforms。在将视觉反馈与动画状态绑定使用。

AnimatedSprite2D Signals (Expert Usage)

AnimatedSprite2D信号(专家用法)

animation_looped vs animation_finished

animation_looped vs animation_finished

gdscript
extends CharacterBody2D

@onready var anim: AnimatedSprite2D = $AnimatedSprite2D

func _ready() -> void:
    # ✅ Correct: Use animation_looped for repeating animations
    anim.animation_looped.connect(_on_loop)
    
    # ✅ Correct: Use animation_finished ONLY for one-shots
    anim.animation_finished.connect(_on_finished)
    
    anim.play("run")  # Looping animation

func _on_loop() -> void:
    # Fires every loop iteration
    emit_particle_effect("dust")

func _on_finished() -> void:
    # Only fires for non-looping animations
    anim.play("idle")
gdscript
extends CharacterBody2D

@onready var anim: AnimatedSprite2D = $AnimatedSprite2D

func _ready() -> void:
    # ✅ 正确:对重复动画使用animation_looped
    anim.animation_looped.connect(_on_loop)
    
    # ✅ 正确:仅对一次性动画使用animation_finished
    anim.animation_finished.connect(_on_finished)
    
    anim.play("run")  # 循环动画

func _on_loop() -> void:
    # 每次循环迭代时触发
    emit_particle_effect("dust")

func _on_finished() -> void:
    # 仅对非循环动画触发
    anim.play("idle")

frame_changed for Event Triggering

frame_changed用于事件触发

gdscript
undefined
gdscript
undefined

Frame-perfect event system (attacks, footsteps, etc.)

帧完美事件系统(攻击、脚步声等)

extends AnimatedSprite2D
signal attack_hit signal footstep
extends AnimatedSprite2D
signal attack_hit signal footstep

Define event frames per animation

定义每个动画的事件帧

const EVENT_FRAMES := { "attack": {3: "attack_hit", 7: "attack_hit"}, "run": {2: "footstep", 5: "footstep"} }
func _ready() -> void: frame_changed.connect(_on_frame_changed)
func _on_frame_changed() -> void: var events := EVENT_FRAMES.get(animation, {}) if frame in events: emit_signal(events[frame])

---
const EVENT_FRAMES := { "attack": {3: "attack_hit", 7: "attack_hit"}, "run": {2: "footstep", 5: "footstep"} }
func _ready() -> void: frame_changed.connect(_on_frame_changed)
func _on_frame_changed() -> void: var events := EVENT_FRAMES.get(animation, {}) if frame in events: emit_signal(events[frame])

---

Advanced Pattern: Animation State Sync

高级模式:动画状态同步

Problem: play() Timing Glitch

问题:play()计时故障

When updating both animation and sprite properties (e.g.,
flip_h
+ animation change),
play()
doesn't apply until next frame, causing a 1-frame visual glitch.
gdscript
undefined
当同时更新动画和精灵属性(例如
flip_h
+ 动画更改)时,
play()
要到下一帧才会生效,导致1帧的视觉故障。
gdscript
undefined

❌ BAD: Glitches for 1 frame

❌ 错误:会出现1帧的故障

func change_direction(dir: int) -> void: anim.flip_h = (dir < 0) anim.play("run") # Applied NEXT frame # Result: 1 frame of wrong animation with correct flip
func change_direction(dir: int) -> void: anim.flip_h = (dir < 0) anim.play("run") # 下一帧才会应用 # 结果:1帧的错误动画但翻转正确

✅ GOOD: Force immediate sync

✅ 正确:强制立即同步

func change_direction(dir: int) -> void: anim.flip_h = (dir < 0) anim.play("run") anim.advance(0) # Force immediate update

---
func change_direction(dir: int) -> void: anim.flip_h = (dir < 0) anim.play("run") anim.advance(0) # 强制立即更新

---

set_frame_and_progress() for Smooth Transitions

set_frame_and_progress()实现平滑过渡

Use when changing animations mid-animation without visual stutter:
gdscript
undefined
在中途更改动画且不出现视觉卡顿时使用:
gdscript
undefined

Example: Skin swapping without animation reset

示例:不重置动画的皮肤切换

func swap_skin(new_skin: String) -> void: var current_frame := anim.frame var current_progress := anim.frame_progress
# Load new SpriteFrames resource
anim.sprite_frames = load("res://skins/%s.tres" % new_skin)

# ✅ Preserve exact animation state
anim.play(anim.animation)  # Re-apply animation
anim.set_frame_and_progress(current_frame, current_progress)
# Result: Seamless skin swap mid-animation

---
func swap_skin(new_skin: String) -> void: var current_frame := anim.frame var current_progress := anim.frame_progress
# 加载新的SpriteFrames资源
anim.sprite_frames = load("res://skins/%s.tres" % new_skin)

# ✅ 保留精确的动画状态
anim.play(anim.animation)  # 重新应用动画
anim.set_frame_and_progress(current_frame, current_progress)
# 结果:中途无缝切换皮肤

---

Decision Tree: AnimatedSprite2D vs AnimationPlayer

决策树:AnimatedSprite2D vs AnimationPlayer

ScenarioUse
Simple frame-based sprite swappingAnimatedSprite2D
Need to animate other properties (position, scale, rotation)AnimationPlayer
Character with swappable skins/palettesAnimatedSprite2D (swap SpriteFrames)
Cutout animation with 10+ bonesAnimationPlayer (cleaner track management)
Need to blend/crossfade animationsAnimationPlayer (AnimationTree support)
Pixel-perfect retro gameAnimatedSprite2D (simpler frame control)

场景使用
简单的基于帧的精灵切换AnimatedSprite2D
需要为其他属性(位置、缩放、旋转)设置动画AnimationPlayer
可切换皮肤/调色板的角色AnimatedSprite2D(切换SpriteFrames)
包含10+骨骼的镂空动画AnimationPlayer(更简洁的轨道管理)
需要混合/交叉淡入淡出动画AnimationPlayer(支持AnimationTree)
像素完美的复古游戏AnimatedSprite2D(更简单的帧控制)

Expert Pattern: Procedural Squash & Stretch

专家模式:程序化挤压与拉伸

gdscript
undefined
gdscript
undefined

Physics-driven squash/stretch for game feel

物理驱动的挤压/拉伸提升游戏手感

extends CharacterBody2D
@onready var sprite: Sprite2D = $Sprite2D var _base_scale := Vector2.ONE
func _physics_process(delta: float) -> void: var prev_velocity := velocity move_and_slide()
# Squash on landing
if not is_on_floor() and is_on_floor():
    var impact_strength := clamp(abs(prev_velocity.y) / 800.0, 0.0, 1.0)
    _squash_and_stretch(Vector2(1.0 + impact_strength * 0.3, 1.0 - impact_strength * 0.3))

# Stretch during jump
elif velocity.y < -200:
    sprite.scale = _base_scale.lerp(Vector2(0.9, 1.1), delta * 5.0)
else:
    sprite.scale = sprite.scale.lerp(_base_scale, delta * 10.0)
func _squash_and_stretch(target_scale: Vector2) -> void: var tween := create_tween().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tween.tween_property(sprite, "scale", target_scale, 0.08) tween.tween_property(sprite, "scale", _base_scale, 0.12)

---
extends CharacterBody2D
@onready var sprite: Sprite2D = $Sprite2D var _base_scale := Vector2.ONE
func _physics_process(delta: float) -> void: var prev_velocity := velocity move_and_slide()
# 落地时挤压
if not is_on_floor() and is_on_floor():
    var impact_strength := clamp(abs(prev_velocity.y) / 800.0, 0.0, 1.0)
    _squash_and_stretch(Vector2(1.0 + impact_strength * 0.3, 1.0 - impact_strength * 0.3))

# 跳跃时拉伸
elif velocity.y < -200:
    sprite.scale = _base_scale.lerp(Vector2(0.9, 1.1), delta * 5.0)
else:
    sprite.scale = sprite.scale.lerp(_base_scale, delta * 10.0)
func _squash_and_stretch(target_scale: Vector2) -> void: var tween := create_tween().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tween.tween_property(sprite, "scale", target_scale, 0.08) tween.tween_property(sprite, "scale", _base_scale, 0.12)

---

Cutout Animation (Bone2D Skeleton)

镂空动画(Bone2D骨骼)

For complex skeletal animation, use Bone2D instead of manual Sprite2D parenting:
对于复杂的骨骼动画,使用Bone2D而非手动Sprite2D父子绑定:

Skeleton Setup

骨骼设置

Player (Node2D)
  └─ Skeleton2D
      ├─ Bone2D (Root - Torso)
      │   ├─ Sprite2D (Body)
      │   └─ Bone2D (Head)
      │       └─ Sprite2D (Head)
      ├─ Bone2D (ArmLeft)
      │   └─ Sprite2D (Arm)
      └─ Bone2D (ArmRight)
          └─ Sprite2D (Arm)
Player (Node2D)
  └─ Skeleton2D
      ├─ Bone2D (根 - 躯干)
      │   ├─ Sprite2D (身体)
      │   └─ Bone2D (头部)
      │       └─ Sprite2D (头部)
      ├─ Bone2D (左臂)
      │   └─ Sprite2D (手臂)
      └─ Bone2D (右臂)
          └─ Sprite2D (手臂)

AnimationPlayer Tracks

AnimationPlayer轨道

gdscript
undefined
gdscript
undefined

Key bone rotations in AnimationPlayer

在AnimationPlayer中设置关键骨骼旋转

Tracks:

轨道:

- "Skeleton2D/Bone2D:rotation"

- "Skeleton2D/Bone2D:rotation"

- "Skeleton2D/Bone2D/Bone2D2:rotation" (head)

- "Skeleton2D/Bone2D/Bone2D2:rotation" (头部)

- "Skeleton2D/Bone2D3:rotation" (arm left)

- "Skeleton2D/Bone2D3:rotation" (左臂)


**Why Bone2D over manual parenting?**
- Forward Kinematics (FK) and Inverse Kinematics (IK) support
- Easier to rig and weight paint
- Better integration with animation retargeting

---

**为什么使用Bone2D而非手动父子绑定?**
- 支持正向运动学(FK)和反向运动学(IK)
- 更易于绑定和权重绘制
- 与动画重定向的集成更好

---

Performance: SpriteFrames Optimization

性能优化:SpriteFrames优化

gdscript
undefined
gdscript
undefined

✅ GOOD: Share SpriteFrames resource across instances

✅ 推荐:在实例间共享SpriteFrames资源

const SHARED_FRAMES := preload("res://characters/player_frames.tres")
func _ready() -> void: anim_sprite.sprite_frames = SHARED_FRAMES # All player instances share same resource in memory
const SHARED_FRAMES := preload("res://characters/player_frames.tres")
func _ready() -> void: anim_sprite.sprite_frames = SHARED_FRAMES # 所有玩家实例在内存中共享同一资源

❌ BAD: Each instance loads separately

❌ 不推荐:每个实例单独加载

func _ready() -> void: anim_sprite.sprite_frames = load("res://characters/player_frames.tres") # Duplicates resource in memory per instance

---
func _ready() -> void: anim_sprite.sprite_frames = load("res://characters/player_frames.tres") # 每个实例在内存中复制资源

---

Edge Case: Pixel Art Centering

边缘情况:像素艺术居中

gdscript
undefined
gdscript
undefined

Pixel art textures can appear blurry when centered between pixels

像素艺术纹理在像素之间居中时可能会模糊

Solution 1: Disable centering

解决方案1:禁用居中

anim_sprite.centered = false anim_sprite.offset = Vector2.ZERO
anim_sprite.centered = false anim_sprite.offset = Vector2.ZERO

Solution 2: Enable global pixel snapping (Project Settings)

解决方案2:启用全局像素对齐(项目设置)

rendering/2d/snap/snap_2d_vertices_to_pixel = true

rendering/2d/snap/snap_2d_vertices_to_pixel = true

rendering/2d/snap/snap_2d_transforms_to_pixel = true

rendering/2d/snap/snap_2d_transforms_to_pixel = true

undefined
undefined

SpriteFrames Texture Filtering

SpriteFrames纹理过滤

gdscript
undefined
gdscript
undefined

Problem: SpriteFrames uses bilinear filtering (blurry for pixel art)

问题:SpriteFrames使用双线性过滤(对像素艺术来说模糊)

Solution: In Import tab for each texture:

解决方案:在每个纹理的导入选项卡中:

- Filter: Nearest (for pixel art)

- 过滤:最近邻(适用于像素艺术)

- Mipmaps: Off (prevents blending at distance)

- 多级渐远纹理:关闭(防止远距离混合)

Or set globally in Project Settings:

或在项目设置中全局设置:

rendering/textures/canvas_textures/default_texture_filter = Nearest

rendering/textures/canvas_textures/default_texture_filter = Nearest

undefined
undefined

Reference

参考

  • Master Skill: godot-master
  • 大师技能:godot-master