跳转至

02-玩家角色动画与移动基础

1 玩家场景搭建

我们先来创建玩家角色的游戏场景。在界面左下角的我呢见系统的scene目录上右键,选择新建->场景,在弹出面板的根节点类型中选择最后一项。然后点击右侧的选择按钮,在搜索框中输入character,选择CaharacterBody2D选项。


Pasted image 20260504124121.png


Pasted image 20260504124158.png


Pasted image 20260504124231.png

这个类是针对用户控制的物理体的特化类,它们不会受到物理等等影响,但会影响路径上的其他物理体。
关于 Node 2D, CharacterBody2DRigidBody2D 的区别:

  • Node 2D 就是一个纯粹的transform节点,描述旋转、缩放、坐标等位置信息,不具备物理模拟或碰撞处理能力。
  • CharacterBody2D天生就有完善的移动、碰撞及滑行处理能力,只需设置速度并调用移动函数即可。
  • RigidBody2D 更偏向纯物理模拟,如箱子、石头等,运动完全托管给物理引擎。
  • 所以,如果希望通过脚本控制并响应输入,就使用 CharacterBody2D。 如果纯粹让它拥有物理规则,那么就简单的使用 RigigdBody2D。而 Node2D 则是更基础的功能节点。
    按照蛇性命名的规范,我们将场景命名为 player。点击确定,完成节点创建。

Pasted image 20260504125118.png


Pasted image 20260504125158.png

创建节点后,Player 根节点右侧会出现黄色警告,提示该节点没有描述形状的子节点,因此无法产生碰撞。
这涉及 Godot 的组合式设计理念:玩家场景不必靠一个巨大的类实现所有功能,而是拆分成多个子节点,如负责图像显示的、负责碰撞范围的、负责声音的等,它们组合在一起构成了完整的玩家角色。

我们在根节点上右键选择“添加子节点”,搜索并添加 CollisionShape2D。此时这个节点右边出现感叹号


Pasted image 20260504125501.png


Pasted image 20260504125640.png

在右侧检查器面板的 Shape 字段选择“新建 CircleShape2D”,此时原点处会出现一个蓝色的圆形碰撞区域。


Pasted image 20260504125741.png


Pasted image 20260504125839.png

接下来创建动画。在 Player 根节点上添加一个 AnimatedSprite2D 节点,这是 Godot 中实现动画最简单的方案。我们将其重命名为 BodySprite,并以相同步骤创建另一个名为 ArmEffectSprite 的节点


Pasted image 20260504125942.png

能够看到警告信息:必须添加SpriteFrame资源才能显示动画帧


Pasted image 20260504130011.png

把这两个节点拖拽到最上方,这并不必要,不过是推荐做法。我们在编辑器中预览的画面绘制顺序,一般来说也是按照节点的先后顺序的。把角色动画放在碰撞外形前面,那么在场景编辑的预览或者显示碰撞区域进行调试的时候,就能更清晰地看到碰撞外形有多大。


Pasted image 20260504130303.png

2 实现玩家角色动画

在左边的场景树种选中BodySprite节点,可以看到右侧SpriteFrames字段是空的,点击新建SpriteFrames,然后点击新添加的字段,Godot界面最下方就会弹出一个新的面板,让我们来设置动画帧。


Pasted image 20260504131844.png


Pasted image 20260504131908.png

在左侧的动画列表中默认有一个叫做default的空动画。把它命名为normal_right, 指玩家角色默认状态向右朝向的动画。然后点击网格图标,从精灵表种添加帧


Pasted image 20260504132046.png


Pasted image 20260504132214.png

输入水平5垂直9,godot就会自动切分网格,之后选择第一行的这四帧。按住左键拖拽选取。然后点击“添加4帧”


Pasted image 20260504132334.png

然后就看到这几帧被添加到了动画内容中了。


Pasted image 20260504132401.png

然后将动画帧率设置为8FPS,然后点击标签一样的图标,把当前动画设置为默认动画


Pasted image 20260504132548.png

同样操作添加几个剩余朝向的动画,包括normal和armed分别的4个动画以及一个death动画。注意下图中的normal_top应该是normal_up才对


Pasted image 20260504133024.png

然后把每个动画都设置为8FPS

之后选中ArmedEffectSprite,同样方法添加浮游炮的动画,设置播放速度为8FPS


Pasted image 20260504133413.png

点击播放按钮可以单独预览浮游炮的动画。


Pasted image 20260504133541.png

点击左侧节点旁的眼睛按钮可以切换可见性,我们可以暂时把ArmedEffectSprite可见性关掉


Pasted image 20260504133630.png

3 GDScript

学习路线:

4 实现角色移动控制

4.1 新建脚本

我们在Player根节点右键,选择添加脚本,可以看到默认的继承类和文件名路径都已经写好了,直接点击创建即可。


Pasted image 20260504134522.png

4.2 设置输入映射

在开始编写代码之前,要先给项目设置“输入映射”。点击项目->项目设置->属于映射


Pasted image 20260504134719.png


Pasted image 20260504135227.png

通俗理解“输入映射”就是一个翻译表,来解耦玩家的按键按钮和代表的含义逻辑。这这里配置玩家按键代表的功能,在代码中只需要去检测什么功能被触发即可。而不需要关注实际按下了哪个键。
输入新动作“move_left”,然后点击添加,就可以看到新添加的动作了。然后点击动作右侧的加号,配置我们要把什么按键或者鼠标等操作映射成为这个动作的语义,


Pasted image 20260504135543.png

在我们的游戏中WASD控制角色移动,上下左右方向键控制射击方向。所以这里直接按下键盘的A键,之后点击确定。添加4个move的按键映射


Pasted image 20260504135838.png

4.3 编写脚本

然后编写下面这样的代码

extends CharacterBody2D

# 玩家移动速度,单位是像素/秒
@export var move_speed: float = 120.0

func _physics_process(delta: float) -> void:
    # 读取四个方向的输入,并得到标准化后的八向输入向量
    var move_input := Input.get_vector("move_let", "move_right", "move_up", "move_down")

    # CharacterBody2D 通过 velocity 配合 move_and_slide() 完成移动
    velocity = move_input * move_speed
    move_and_slide()

首先@export这一行,通过导出属性,让我们这个定义的 move_speed 变量可以暴露在编辑器中方便我们之间进行编辑。可以在右侧的检查器面板看到这个属性。
一般来说,速度、防御值等随着系统开发及时调整的数值,都可以考虑暴露到编辑器界面中方便修改。


Pasted image 20260504140514.png

然后_physics_process这个函数,可以理解为Godot会反复调用的一个“固定节奏约定”函数。还有一个很相似的函数叫_process,
二者的区别是

  • _process的调用跟着屏幕刷新走,适合处理偏视觉表现得内容,比如界面变化,镜头效果这些不十分依赖物理精度的逻辑。
  • _physics_process的调用则与屏幕刷新率无关。他自己背后有个固定的计时刷新器,一般来说默认是每秒60次。专门处理与物理世界强相关的内容。

move_input这一行是我们输入映射真正起作用的地方,传入我们刚才配置的各个动作的名字。按照左右上下的顺序写进去,这个函数就会自动帮我们计算得到标准化的移动向量。
之后把这个方向和前面对外暴露的move_speed变量想乘,就得到了velocity这个速度值。

需要注意的是,velocity不是通过var语法定义的局部变量,而是一个位于CharacterBody2D内部的成员变量。

最后一行代码则是通过move_and_slide方法,让玩家角色在游戏世界中真正地使用当前速度改变当前的位置进行移动。

4.4 初步效果

之后点击顶部的按钮,切换到2d视图,点击打开game场景


Pasted image 20260504141605.png

然后在根节点右键,选择实例化子场景,然后点击player子场景,之后就能看到场景中出现了我们的角色。如果看不到的话,记得切换到player场景,使用ctrl s快捷键保存。


Pasted image 20260504141802.png


Pasted image 20260504142014.png

然后可以拖动玩家角色到游戏场景的合适位置,点击右上角的运行


Pasted image 20260504142158.png

然后使用WASD键就可以控制我们的角色自由移动了,可以看到一直会播放朝右的角色动画。

4.5 播放不同朝向的动画

首先在开头创建一个常量

const NORMAL_ANIMATION_PREFIX := &"normal"

  • :=赋值的作用是告诉Godot在赋值的时候自动推断静态类型。
  • 字符串常量的&符号,在Godot种叫做StringName字面量。可以把它理解成一种内部标签名或者名字编号。而不是单纯的字符串变量。
    StringName和普通字符串的区别出去性能稍好外,更多是语义上告诉引擎和开发者,这是一个固有的名字。

# 角色动画节点,负责播放四方向的移动动画
@onready var body_sprite: AnimatedSprite2D = $BodySprite

这行代码的作用是在代码中获取我们场景树中的BodySprite节点的引用。这样就可以更方便地在后面使用代码来控制动画了。
编写这行代码还有一个更快捷的方式,点击上方的标签栏切换到player场景树,拖拽需要获取的节点到代码编辑器中,按住ctrl释放鼠标左键,这样godot就会自动帮我们生成这样的一行代码

# 当前动画时候用的后缀,对应动画命中的 up/down/left/right
var facing_suffix: StringName = &"right"

这一行便是设置当前动画使用的后缀。“状态+朝向”决定了我们具体使用哪一个动画序列帧。

func _ready() -> void:
    _update_animation()

之后编写一个_ready函数,在里面调用_update_animation(我们后续实现这个函数)。_ready函数也是godot提供的函数,在节点就绪时会被调用。一般来说,就是节点本身和它的子节点场景树,被实例化后实际进入到其他场景中的时候会被调用。
我们在这里更新一下角色状态进行兜底,避免前面遗漏了设置默认播放的动画。

然后在_physics_process种添加下面几行代码。如果存在玩家按键输入的情况下,我们就重新根据当前玩家按键输入方向,计算动画所使用的facing_suffix值。这是根据案件方向切换动画朝向的关键一步。

if move_input != Vector2.ZERO:
    facing_suffix = _vector_to_facing_suffix(move_input)

_update_animation()

最后我们实现这两个函数

先是_update_animation, 使用将前后缀进行拼接,赋值给animation_name,之后判断这个动画是否存在,不存在则抛出warning。最后判断当前动画是否是要播放的动画,如果不是则播放需要的动画。

# 根据当前朝向拼出动画名,并在动画实际变化是再切换动画
func _update_animation() -> void:
    var animation_name := StringName("%s_%s" % [NORMAL_ANIMATION_PREFIX, facing_suffix])
    if not body_sprite.sprite_frames.has_animation(animation_name):
        push_warning("Missing player animation: %s" % animation_name)
        return

    if body_sprite.animation != animation_name:
        body_sprite.play(animation_name)

第二个函数是_vector_to_facing_suffix,根据运动朝向的向量,去切换动画名字的后缀。首先分别计算x轴和y轴两个方向上,哪个坐标轴上的移动分量更大,然后就优先使用哪个轴方向上的动画。这是一个通用且十分成熟的方案

# 将任意而为向量映射为四方向动画
# 对角线输入会优先取绝对值更大的周,避免再四方向动画中出现歧义
func _vector_to_facing_suffix(direction: Vector2) -> StringName:
    if abs(direction.x) >= abs(direction.y):
        return &"right" if direction.x > 0.0 else &"left"
    return &"down" if direction.y > 0.0 else &"up"

最后完整代码如下:

extends CharacterBody2D

const NORMAL_ANIMATION_PREFIX := &"normal"

# 角色动画节点,负责播放四方向的移动动画
@onready var body_sprite: AnimatedSprite2D = $BodySprite

# 玩家移动速度,单位是像素/秒
@export var move_speed: float = 120.0

# 当前动画时候用的后缀,对应动画命中的 up/down/left/right
var facing_suffix: StringName = &"right"

func _ready() -> void:
    _update_animation()

func _physics_process(delta: float) -> void:
    # 读取四个方向的输入,并得到标准化后的八向输入向量
    var move_input := Input.get_vector("move_let", "move_right", "move_up", "move_down")

    # CharacterBody2D 通过 velocity 配合 move_and_slide() 完成移动
    velocity = move_input * move_speed
    move_and_slide()

    if move_input != Vector2.ZERO:
        facing_suffix = _vector_to_facing_suffix(move_input)

    _update_animation()

# 根据当前朝向拼出动画名,并在动画实际变化是再切换动画
func _update_animation() -> void:
    var animation_name := StringName("%s_%s" % [NORMAL_ANIMATION_PREFIX, facing_suffix])
    if not body_sprite.sprite_frames.has_animation(animation_name):
        push_warning("Missing player animation: %s" % animation_name)
        return

    if body_sprite.animation != animation_name:
        body_sprite.play(animation_name)

# 将任意而为向量映射为四方向动画
# 对角线输入会优先取绝对值更大的周,避免再四方向动画中出现歧义
func _vector_to_facing_suffix(direction: Vector2) -> StringName:
    if abs(direction.x) >= abs(direction.y):
        return &"right" if direction.x > 0.0 else &"left"
    return &"down" if direction.y > 0.0 else &"up"

运行项目,不同朝向的动画能够正确播放。但是与场景的碰撞还没有做。

评论

如果你已登录 GitHub,就可以直接在这里评论。