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

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

角色已经能移动,但如果始终显示同一张图片,游戏仍会显得像在拖动贴纸。AnimatedSprite2D 可以用一个 SpriteFrames 资源管理多组逐帧动画,再根据角色的真实速度切换 idlewalk

这篇文章承接普通的 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 编辑器中:

  1. 把默认动画重命名为 idle
  2. 添加待机帧;
  3. 打开循环;
  4. 设置与素材匹配的 FPS;
  5. 新建 walk
  6. 添加行走帧并打开循环。

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。因此 idlewalk 负责持续状态,一次性动画负责在完成时释放锁定。

九、常见问题

动画一直停在第一帧

检查动画是否添加了多帧、FPS 是否大于零,以及场景运行时是否真的调用了 play()

walk 动画不断从头开始

不要无条件调用带重置效果的逻辑。只在目标动画与当前动画不同的时候切换。

左右方向快速抖动

只在 velocity.x 明确大于或小于零时修改 flip_h;速度为零时保留原方向。

一次性动画刚播放就被 idle 覆盖

普通移动动画每个物理帧都会更新,因此需要在一次性动作期间设置锁定,并在 animation_finished 后释放。

角色松开按键后仍播放 walk

如果脚本使用平滑减速,这是预期行为。动画依据真实速度切换,角色尚未完全停止时继续走动更符合画面。

十、知识主线与参考

本文的 SpriteFrames 组织方式、循环与一次性动画区别、animation_finished 信号以及角色物理移动逻辑,来自《大鹏 Godot 4 游戏开发入门》第 5 章中"动画精灵""控制 Sprite2D 动画"和"编写物理角色移动代码"三节。示例只在这些已锁定知识上做组合,不把平台同题文章当作技术依据。

十一、验收清单

  • 进入场景后自动播放 idle
  • 任意方向移动时切换为 walk
  • 松开输入并完全停止后恢复 idle
  • 左右移动时角色朝向正确。
  • 仅上下移动时保留最近一次水平朝向。
  • 一次性动画不会被移动动画立即覆盖。
  • 一次性动画结束后能回到正确的待机或移动状态。

总结

至此,角色的物理移动和视觉动画已经形成了一个清晰闭环:CharacterBody2D 决定真实速度,AnimatedSprite2D 根据速度选择画面。以后增加攻击、跳跃或受伤行为时,也可以沿用"持续动画循环、一次性动画完成后回退"的原则继续扩展。

相关推荐
新手unity自用笔记4 小时前
unity网络基础_1
网络·unity·游戏引擎
SmalBox13 小时前
【节点】[IsInfinite节点]原理解析与实际应用
unity3d·游戏开发·图形学
闪亮的路灯1 天前
威联通QTS使用自带web服务期代理前端(类似nginx)
前端·nginx·威联通
難釋懷1 天前
Nginx外置缓存-error_page
运维·nginx·缓存
hsw8157739431 天前
第10章 软件架构的演化和维护 — 系统架构设计师
unity·系统架构·游戏引擎
SmalBox1 天前
【节点】[IsFrontFace节点]原理解析与实际应用
unity3d·游戏开发·图形学
tealcwu2 天前
【尝鲜】Unity 6.7alpha版本测试
unity·游戏引擎
怪味趣多2 天前
r创建动画。 . 使用TFloatAnimation通过改变图片的位置创建动车。 . 使用TPathAnimation创建路径动画。 ...
运维·nginx