从零开始的 Godot 之旅 --- EP8:角色移动和动画切换
上一节我们学会了如何为角色添加待机动画,成功让角色待机时动起来。本节我们将继续完善角色移动功能,并实现角色移动和动画的智能切换,让角色真正"活"起来。
制作完整的角色动画
在上一节中我们留了一个"课后作业",要求将角色向右和向上(背面)的待机动画制作完成。现在我们在完成这个作业的基础上,再制作相应的移动动画,让角色在移动时也能有对应的动画表现。
动画规划
根据我们的素材资源,我们需要制作以下动画:
待机动画:
- 待机向下:idle_down(0~5帧)
- 待机向右:idle_side(6~11帧)
- 待机向上:idle_up(12~17帧)
移动动画:
- 行走向下:walk_down(18~23帧)
- 行走向右:walk_side(24~29帧)
- 行走向上:walk_up(30~35帧)

小贴士:我们只需要制作一个向右方向的移动动画,向左的移动动画可以通过镜像来实现,这样可以节省制作时间,同时保持动画的一致性。
实现角色移动
角色移动的功能与之前的一样,如果你还不了解如何让角色移动,请参考之前的文章:
EP5 控制角色移动和EP6:更优雅地实现角色场景。
整理项目结构
在开始编码之前,我们先调整一下目录结构,让项目更加规范。将 player.tscn 移动到 scenes/entities/player 文件夹下,并在player目录下创建 scripts 和 assets两个子目录。

这样的目录结构有助于项目的组织和管理,随着项目的发展,我们可以根据需要添加更多目录。
添加角色脚本
接下来我们给player角色添加脚本。选中Player节点,然后点击添加脚本,将脚本命名为player.gd,并保存到scenes/entities/player/scripts目录下。

编写移动代码
打开player.gd脚本,添加以下代码:
GDScript
extends CharacterBody2D
# 可在检查器里调整的移动速度
@export var speed: float = 100.0
# 当前输入方向向量(单位化前的原始输入)
var move_direction: Vector2 = Vector2.ZERO
func _physics_process(delta: float) -> void:
# 分别获取水平方向与垂直方向输入(已自动处理互斥与摇杆)
var x_axis: float = Input.get_axis("left", "right")
var y_axis: float = Input.get_axis("up", "down")
move_direction = Vector2(x_axis, y_axis)
velocity = move_direction * speed
move_and_slide() # 内置处理碰撞与滑动
添加完成后,我们保存并运行游戏,可以看到玩家角色已经可以移动了,但是无论我们怎么移动,角色的动画都是向下待机动画。

可以看到,虽然角色能够正常移动,但动画系统还没有与移动逻辑关联起来。接下来我们就来解决这个问题。
实现角色动画切换
理解动画切换逻辑
在具体实现之前,我们先来梳理一下角色动画切换的逻辑。
我们在之前创建了一个AnimationPlayer,并且在其中创建了多个动画。AnimationPlayer每次都只会播放一个动画,那我们只要能控制AnimationPlayer播放哪个动画,就能实现角色动画切换。
第一版实现:基础动画切换
我们修改player.gd脚本,添加以下代码:
GDScript
extends CharacterBody2D
# 可在检查器里调整的移动速度
@export var speed: float = 100.0
# 当前输入方向向量(单位化前的原始输入)
var move_direction: Vector2 = Vector2.ZERO
# 组件引用
## 动画子节点
@onready var animation_player: AnimationPlayer = $AnimationPlayer
## 精灵
@onready var sprite: Sprite2D = $Sprite2D
func _physics_process(_delta: float) -> void:
# 分别获取水平方向与垂直方向输入(已自动处理互斥与摇杆)
var x_axis: float = Input.get_axis("left", "right")
var y_axis: float = Input.get_axis("up", "down")
move_direction = Vector2(x_axis, y_axis)
velocity = move_direction * speed
move_and_slide() # 内置处理碰撞与滑动
# 只有玩家移动的时候才更新移动动画
if move_direction != Vector2.ZERO:
# 根据输入方向播放不同的动画
if x_axis > 0:
animation_player.play("walk_side")
sprite.flip_h = false
elif x_axis < 0:
animation_player.play("walk_side")
sprite.flip_h = true
elif y_axis > 0:
animation_player.play("walk_down")
elif y_axis < 0:
animation_player.play("walk_up")
else:
animation_player.play("idle_down")
关键代码解析
1. 引入子节点引用 在代码中引入子节点,引入节点后我们就可以通过这个变量直接访问或调用子节点的方法和属性了:
GDScript
# 组件引用
## 动画子节点
@onready var animation_player: AnimationPlayer = $AnimationPlayer
## 精灵
@onready var sprite: Sprite2D = $Sprite2D
2. 动画切换逻辑 在移动之后,判断输入方向来调用animation_player.play播放不同的动画。因为横向的移动我们只制作了向右的,所以需要使用sprite.flip_h来实现镜像效果:
GDScript
# 只有玩家移动的时候才更新移动动画
if move_direction != Vector2.ZERO:
# 根据输入方向播放不同的动画
if x_axis > 0:
animation_player.play("walk_side")
sprite.flip_h = false
elif x_axis < 0:
animation_player.play("walk_side")
sprite.flip_h = true
elif y_axis > 0:
animation_player.play("walk_down")
elif y_axis < 0:
animation_player.play("walk_up")
else:
animation_player.play("idle_down")
测试效果
运行查看效果: 
可以看到,玩家在移动时已经能正常切换成对应的动画了。但是还是有一些小问题的:
- 朝向问题:每当我们松开按键时,玩家会回复到面朝我们的方向,我们其实是希望他停留到最后面向的方向的。
- 代码结构:现在的代码太不优雅了,逻辑比较混乱。
第二版实现:优雅的动画切换
我们修改player.gd脚本,添加以下代码:
GDScript
extends CharacterBody2D
# 可在检查器里调整的移动速度
@export var speed: float = 100.0
# 当前输入方向向量(单位化前的原始输入)
var move_direction: Vector2 = Vector2.ZERO
# 玩家朝向,默认朝下
var player_direction: Vector2 = Vector2.DOWN
# 组件引用
## 动画子节点
@onready var animation_player: AnimationPlayer = $AnimationPlayer
## 精灵
@onready var sprite: Sprite2D = $Sprite2D
func _physics_process(_delta: float) -> void:
# 分别获取水平方向与垂直方向输入(已自动处理互斥与摇杆)
var x_axis: float = Input.get_axis("left", "right")
var y_axis: float = Input.get_axis("up", "down")
move_direction = Vector2(x_axis, y_axis)
velocity = move_direction * speed
# 内置处理碰撞与滑动
move_and_slide()
# 更新朝向和动画
_update_direction_and_animation()
# 更新朝向和动画
# @return 无返回值
func _update_direction_and_animation() -> void:
# 更新朝向(只在有移动时更新,使用标准方向)
if move_direction != Vector2.ZERO:
var new_direction: Vector2
if abs(move_direction.x) > abs(move_direction.y):
new_direction = Vector2.RIGHT if move_direction.x > 0 else Vector2.LEFT
else:
new_direction = Vector2.DOWN if move_direction.y > 0 else Vector2.UP
# 如果朝向发生变化,更新玩家朝向
if new_direction != player_direction:
player_direction = new_direction
# 更新动画
update_animation()
# 更新角色动画
# @return 无返回值
func update_animation() -> void:
var current_state = "walk" if move_direction != Vector2.ZERO else "idle"
var direction: String = get_anim_direction()
var animation_name: String = current_state + "_" + direction
if animation_player.has_animation(animation_name):
animation_player.play(animation_name)
else:
print_debug("动画不存在: ", animation_name)
# 获取动画方向字符串
# @return 方向字符串("down", "up", 或 "side")
func get_anim_direction() -> String:
if abs(player_direction.x) > abs(player_direction.y):
sprite.flip_h = player_direction.x < 0
return "side"
if player_direction.y < 0:
return "up"
return "down"
关键代码解析
1. 玩家朝向管理 增加一个player_direction来记录玩家朝向,默认朝下:
GDScript
# 玩家朝向,默认朝下
var player_direction: Vector2 = Vector2.DOWN
2. 朝向更新逻辑 在移动之后,通过计算玩家的移动方向来更新玩家的朝向。如果朝向发生变化,则更新玩家朝向:
GDScript
# 更新朝向(只在有移动时更新,使用标准方向)
if move_direction != Vector2.ZERO:
var new_direction: Vector2
if abs(move_direction.x) > abs(move_direction.y):
new_direction = Vector2.RIGHT if move_direction.x > 0 else Vector2.LEFT
else:
new_direction = Vector2.DOWN if move_direction.y > 0 else Vector2.UP
# 如果朝向发生变化,更新玩家朝向
if new_direction != player_direction:
player_direction = new_direction
3. 动画名称生成 通过get_anim_direction来获取玩家方向的字符串,通过玩家是否移动来获取状态字符串,最后拼接到一起,调用animation_player.play来播放动画:
GDScript
# 更新角色动画
# @return 无返回值
func update_animation() -> void:
var current_state = "walk" if move_direction != Vector2.ZERO else "idle"
var direction: String = get_anim_direction()
var animation_name: String = current_state + "_" + direction
if animation_player.has_animation(animation_name):
animation_player.play(animation_name)
else:
print_debug("动画不存在: ", animation_name)
# 获取动画方向字符串
# @return 方向字符串("down", "up", 或 "side")
func get_anim_direction() -> String:
if abs(player_direction.x) > abs(player_direction.y):
sprite.flip_h = player_direction.x < 0
return "side"
if player_direction.y < 0:
return "up"
return "down"
最终效果
最后我们再来看下成果:

本节小结
在本节中我们成功实现了角色随着移动来切换对应的动画,并且通过梳理动画切换的逻辑,我们发现可以将动画切换的逻辑封装成函数,这样我们就可以在需要的时候调用这个函数来播放动画。
主要收获
- 动画系统集成:成功将移动逻辑与动画系统结合,实现了智能的动画切换
- 代码结构优化:通过函数封装和逻辑分离,让代码更加清晰和可维护
- 朝向管理:解决了角色朝向保持的问题,让角色行为更加自然
- 项目结构:建立了更规范的项目目录结构,为后续开发打下基础
关键知识点回顾
- AnimationPlayer:Godot的动画播放器,可以控制角色动画的播放
- 朝向管理:通过记录玩家朝向,实现动画的智能切换
- 代码封装:将复杂逻辑拆分成小函数,提高代码的可读性和可维护性
- 镜像技术 :通过
sprite.flip_h实现左右方向的动画复用
课后作业
为角色增加攻击动画,并且实现按J键攻击。这将为下一节学习状态机打下基础。
下一节,我们将开始学习一个非常重要的概念:状态机。状态机将帮助我们更好地管理角色的各种状态(移动、攻击、受击等),让游戏逻辑更加清晰和可扩展。