Godot 4 AnimatedSprite2D 实战:让待机与移动动画自动切换

角色已经能移动,但如果始终显示同一张图片,游戏仍会显得像在拖动贴纸。AnimatedSprite2D 可以用一个 SpriteFrames 资源管理多组逐帧动画,再根据角色的真实速度切换 idle 和 walk。
这篇文章承接普通的 CharacterBody2D 八方向移动脚本,完成素材导入、动画分组、运行时切换、方向翻转和一次性动画恢复。示例保持小而完整,适合直接放进 Godot 4 的 2D 项目。
一、Sprite2D 和 AnimatedSprite2D 的区别
Sprite2D 主要显示单张纹理,适合静态物体或由代码单独控制纹理的场景。
AnimatedSprite2D 使用 SpriteFrames 保存多组逐帧动画。一个节点中可以同时拥有:
idle:待机循环;walk:行走循环;yawn:打哈欠等一次性动作;- 后续还可以加入攻击、跳跃和受伤动画。
不同动作不必拆成多个显示节点。运行时只需要调用 play("动画名")。
二、准备角色节点
角色场景建议保持以下结构:
text
Player (CharacterBody2D)
├── AnimatedSprite2D
└── CollisionShape2D
选中 AnimatedSprite2D,在 Inspector 的 Sprite Frames 属性中选择 New SpriteFrames,再打开底部的 SpriteFrames 编辑器。
新建资源默认内置在当前场景中。如果多个角色需要共享同一套动画,可以再将它保存为独立的 .tres 文件。
三、建立 idle 与 walk 动画
在 SpriteFrames 编辑器中:
- 把默认动画重命名为
idle; - 添加待机帧;
- 打开循环;
- 设置与素材匹配的 FPS;
- 新建
walk; - 添加行走帧并打开循环。
FPS 表示每秒播放多少帧,不会凭空增加原始画面。帧数较少时把 FPS 调得过高,动作反而会显得跳跃。应直接在编辑器中预览,以素材表达的节奏为准。
把 Autoplay 设为 idle,运行场景后角色即使没有脚本切换,也会先播放待机动画。

四、用真实速度驱动动画
假设角色已经通过 CharacterBody2D.velocity 移动,可以在玩家脚本中加入:
gdscript
extends CharacterBody2D
@export var speed: float = 240.0
@onready var animated_sprite: AnimatedSprite2D = $AnimatedSprite2D
func _physics_process(_delta: float) -> void:
var input_direction := Input.get_vector(
"move_left",
"move_right",
"move_up",
"move_down"
)
velocity = input_direction * speed
move_and_slide()
update_animation()
func update_animation() -> void:
if velocity.is_zero_approx():
animated_sprite.play("idle")
else:
animated_sprite.play("walk")
这里使用 velocity,而不是重新读取按键,原因很实际:输入表达的是玩家意图,速度表达的是角色最终是否在移动。以后加入击退、过场控制或移动锁定时,动画仍能跟随真实运动结果。
五、避免每帧重复启动同一动画
Godot 会维护当前动画状态,但显式判断可以让逻辑更清楚,也方便以后加入复杂动作:
gdscript
func play_if_changed(animation_name: StringName) -> void:
if animated_sprite.animation != animation_name:
animated_sprite.play(animation_name)
func update_animation() -> void:
if velocity.is_zero_approx():
play_if_changed(&"idle")
else:
play_if_changed(&"walk")
StringName 写法适合频繁使用的标识符。这里不是性能优化的硬性要求,主要目的是避免无意义的重复状态设置。
六、根据水平方向翻转角色
如果素材只绘制了朝右的行走帧,可以使用 flip_h:
gdscript
func update_facing() -> void:
if velocity.x > 0.0:
animated_sprite.flip_h = false
elif velocity.x < 0.0:
animated_sprite.flip_h = true
把它放在物理更新的动画处理阶段:
gdscript
move_and_slide()
update_facing()
update_animation()
当 velocity.x 等于零时不要改变方向,这样角色停止后会保留最后一次朝向,不会突然回到默认方向。
七、整合后的完整脚本
gdscript
extends CharacterBody2D
@export var max_speed: float = 240.0
@export var acceleration: float = 1200.0
@export var deceleration: float = 1600.0
@onready var animated_sprite: AnimatedSprite2D = $AnimatedSprite2D
func _physics_process(delta: float) -> void:
var input_direction := Input.get_vector(
"move_left",
"move_right",
"move_up",
"move_down"
)
var target_velocity := input_direction * max_speed
var rate := acceleration if not input_direction.is_zero_approx() else deceleration
velocity.x = move_toward(velocity.x, target_velocity.x, rate * delta)
velocity.y = move_toward(velocity.y, target_velocity.y, rate * delta)
move_and_slide()
update_facing()
update_animation()
func update_facing() -> void:
if velocity.x > 0.0:
animated_sprite.flip_h = false
elif velocity.x < 0.0:
animated_sprite.flip_h = true
func update_animation() -> void:
var target_animation: StringName = (
&"idle" if velocity.is_zero_approx() else &"walk"
)
if animated_sprite.animation != target_animation:
animated_sprite.play(target_animation)
如果使用平滑减速,松开按键后速度不会立刻归零,因此 walk 会持续到角色真正停下。这通常比根据按键瞬间切回待机更自然。
八、处理一次性动画
攻击、受伤或打哈欠通常不应循环。可以监听 animation_finished,播放完成后恢复待机:
gdscript
var action_locked := false
func _ready() -> void:
animated_sprite.animation_finished.connect(_on_animation_finished)
func play_one_shot(animation_name: StringName) -> void:
action_locked = true
animated_sprite.play(animation_name)
func _on_animation_finished() -> void:
if action_locked:
action_locked = false
update_animation()
同时在 update_animation() 开头增加保护:
gdscript
func update_animation() -> void:
if action_locked:
return
var target_animation: StringName = (
&"idle" if velocity.is_zero_approx() else &"walk"
)
play_if_changed(target_animation)
循环动画不会发出 animation_finished。因此 idle 和 walk 负责持续状态,一次性动画负责在完成时释放锁定。

九、常见问题
动画一直停在第一帧
检查动画是否添加了多帧、FPS 是否大于零,以及场景运行时是否真的调用了 play()。
walk 动画不断从头开始
不要无条件调用带重置效果的逻辑。只在目标动画与当前动画不同的时候切换。
左右方向快速抖动
只在 velocity.x 明确大于或小于零时修改 flip_h;速度为零时保留原方向。
一次性动画刚播放就被 idle 覆盖
普通移动动画每个物理帧都会更新,因此需要在一次性动作期间设置锁定,并在 animation_finished 后释放。
角色松开按键后仍播放 walk
如果脚本使用平滑减速,这是预期行为。动画依据真实速度切换,角色尚未完全停止时继续走动更符合画面。
十、知识主线与参考
本文的 SpriteFrames 组织方式、循环与一次性动画区别、animation_finished 信号以及角色物理移动逻辑,来自《大鹏 Godot 4 游戏开发入门》第 5 章中"动画精灵""控制 Sprite2D 动画"和"编写物理角色移动代码"三节。示例只在这些已锁定知识上做组合,不把平台同题文章当作技术依据。
十一、验收清单
- 进入场景后自动播放
idle。 - 任意方向移动时切换为
walk。 - 松开输入并完全停止后恢复
idle。 - 左右移动时角色朝向正确。
- 仅上下移动时保留最近一次水平朝向。
- 一次性动画不会被移动动画立即覆盖。
- 一次性动画结束后能回到正确的待机或移动状态。
总结
至此,角色的物理移动和视觉动画已经形成了一个清晰闭环:CharacterBody2D 决定真实速度,AnimatedSprite2D 根据速度选择画面。以后增加攻击、跳跃或受伤行为时,也可以沿用"持续动画循环、一次性动画完成后回退"的原则继续扩展。