godot-2d-animation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese2D Animation
2D动画
Expert-level guidance for frame-based and skeletal 2D animation in Godot.
Godot中基于帧和骨骼的2D动画专家级指南。
NEVER Do
绝对不要做的事
- NEVER use for looping animations — The signal only fires on non-looping animations. Use
animation_finishedinstead for loop detection.animation_looped - NEVER call and expect instant state changes — AnimatedSprite2D applies
play()on the next process frame. Callplay()immediately afteradvance(0)if you need synchronous property updates (e.g., when changing animation + flip_h simultaneously).play() - NEVER set directly when preserving animation progress — Setting
frameresetsframeto 0.0. Useframe_progressto maintain smooth transitions when swapping animations mid-frame.set_frame_and_progress(frame, progress) - NEVER forget to cache — The node lookup getter is surprisingly slow in hot paths like
@onready var anim_sprite. Always use_physics_process().@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 - 绝对不要调用后期望状态立即改变 — AnimatedSprite2D会在下一个处理帧应用
play()。如果需要同步更新属性(例如同时更改动画和flip_h),请在调用play()后立即调用play()。advance(0) - 绝对不要在保留动画进度时直接设置— 设置
frame会将frame重置为0.0。在帧中途切换动画时,请使用frame_progress来保持平滑过渡。set_frame_and_progress(frame, progress) - 绝对不要忘记缓存— 在
@onready var anim_sprite这类热路径中,节点查找的getter速度出奇地慢。请始终使用_physics_process()。@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
undefinedgdscript
undefinedFrame-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., + animation change), doesn't apply until next frame, causing a 1-frame visual glitch.
flip_hplay()gdscript
undefined当同时更新动画和精灵属性(例如 + 动画更改)时,要到下一帧才会生效,导致1帧的视觉故障。
flip_hplay()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
undefinedExample: 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
| Scenario | Use |
|---|---|
| Simple frame-based sprite swapping | AnimatedSprite2D |
| Need to animate other properties (position, scale, rotation) | AnimationPlayer |
| Character with swappable skins/palettes | AnimatedSprite2D (swap SpriteFrames) |
| Cutout animation with 10+ bones | AnimationPlayer (cleaner track management) |
| Need to blend/crossfade animations | AnimationPlayer (AnimationTree support) |
| Pixel-perfect retro game | AnimatedSprite2D (simpler frame control) |
| 场景 | 使用 |
|---|---|
| 简单的基于帧的精灵切换 | AnimatedSprite2D |
| 需要为其他属性(位置、缩放、旋转)设置动画 | AnimationPlayer |
| 可切换皮肤/调色板的角色 | AnimatedSprite2D(切换SpriteFrames) |
| 包含10+骨骼的镂空动画 | AnimationPlayer(更简洁的轨道管理) |
| 需要混合/交叉淡入淡出动画 | AnimationPlayer(支持AnimationTree) |
| 像素完美的复古游戏 | AnimatedSprite2D(更简单的帧控制) |
Expert Pattern: Procedural Squash & Stretch
专家模式:程序化挤压与拉伸
gdscript
undefinedgdscript
undefinedPhysics-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
undefinedgdscript
undefinedKey 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
undefinedgdscript
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
undefinedgdscript
undefinedPixel 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
undefinedundefinedSpriteFrames Texture Filtering
SpriteFrames纹理过滤
gdscript
undefinedgdscript
undefinedProblem: 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
undefinedundefinedReference
参考
- Master Skill: godot-master
- 大师技能:godot-master