当前位置: 首页 > news >正文

第五章:让主角动起来——玩家角色创建

「行是知之始,知是行之成。」 —— 陶行知

在第四章,我们搭建了游戏的主框架。这一章,我们创建可由玩家控制的主角,实现八方向移动并播放对应的行走动画。

本章目标:创建Player.tscn场景,配置八方向待机和行走动画,实现键盘控制的八方向移动。


5.1 给主角一个“身体”:CharacterBody2D

Godot 提供了三种 2D 物理体节点,它们各有分工:

节点特点典型用途
StaticBody2D完全静止,不会移动地面、墙壁、障碍物
RigidBody2D受物理引擎自动控制(重力、碰撞反弹等)弹球、推箱子、物理模拟物体
CharacterBody2D由你写代码控制移动,引擎只负责碰撞检测玩家角色、敌人、NPC

5.1.1 为什么玩家角色必须用 CharacterBody2D?

  • StaticBody2D不行:它不能移动。主角需要走来走去,所以排除。
  • RigidBody2D不合适:它会像真实物体一样有惯性——按右键角色加速,松开后还会滑行一段距离才停下。这种“溜冰”手感会让玩家觉得角色“不跟手”,体验很差。除非你做的是物理平台游戏(如《小小盗贼》),否则不适合主角。
  • CharacterBody2D恰到好处
    • 精准控制:你通过代码直接设置velocity(速度),每帧告诉引擎“往哪走、走多快”。按下键立刻动,松开立刻停,手感干净利落。
    • 碰撞自动处理:调用move_and_slide()后,引擎会帮你处理与墙壁、地面的碰撞,角色不会穿墙。
    • 不受重力影响(除非你手动添加),适合俯视角或平台跳跃游戏。

一句话总结CharacterBody2D就是为“由代码精确控制移动的角色”而生的。


创建玩家场景

  1. 点击菜单场景 → 新建场景,或按Ctrl+N
  2. 在弹出的“创建根节点”窗口中,选择CharacterBody2D,点击“创建”。
  3. 在场景面板中,双击新节点,将其重命名为Player(回车确认)。
  4. 确保选中Player节点,点击场景面板上方的“+”图标(添加子节点)。
  5. 搜索CollisionShape2D并添加。
  6. 在右侧检查器中,找到Shape属性,点击下拉菜单 →新建 CapsuleShape2D
  7. 调整胶囊形状的大小:展开Shape,修改HeightRadius,使其大致覆盖角色身体(例如 Height=48,Radius=16,具体根据素材微调)。
  8. Ctrl+S保存场景,路径为scenes/characters/Player.tscn

此时场景结构:

Player (CharacterBody2D) └── CollisionShape2D


5.2 添加动画:先从待机开始

现在我们的Player场景还是一个只有碰撞形状的“隐形人”。接下来,我们用AnimatedSprite2D节点让角色真正显示出来,并播放待机动画(即站立不动的动画,比如轻微呼吸、眨眼等)。

5.2.1 添加 AnimatedSprite2D 节点

首先,在场景面板中选中Player节点(确保它是蓝色的高亮状态)。然后点击场景面板上方的“+”按钮(添加子节点)。在弹出的搜索框中输入AnimatedSprite2D,找到它并点击“创建”。这样,AnimatedSprite2D就成了Player的子节点,它将负责展示角色的图片和动画。

创建后,请确保AnimatedSprite2D节点仍处于选中状态(在场景面板中它应该是高亮的),因为我们马上要配置它的动画资源。

5.2.2 创建 SpriteFrames 资源并打开动画面板

AnimatedSprite2D本身并不知道要显示哪些图片、按什么顺序播放——这需要给它一个SpriteFrames资源。SpriteFrames就是“动画帧的容器”,里面存放了所有动画及其对应的图片序列。

在右侧的检查器面板中,找到SpriteFrames属性(它位于“Animation”分类下)。此时它显示为“空”。点击它右侧的下拉箭头,在弹出的菜单中选择“新建 SpriteFrames”。这时你会发现属性框里多了一个[SpriteFrames]字样,表示资源已创建。

接下来,双击这个新建的SpriteFrames资源(或者点击它右侧的小图标)。Godot 会在编辑器底部自动打开一个专门的SpriteFrames 面板。如果你不小心关掉了底部面板,可以手动点击编辑器底部的“SpriteFrames”标签来重新打开它。
现在,你已经站在了配置动画的核心工作区。


5.2.3 添加八个待机动画

我们的角色需要面向八个方向站立时的待机动画。素材已经准备好了,放在 assets/sprites/player文件夹里,每个动作一个素材,比如idle.png、walk.png。我们要在 SpriteFrames 面板中创建下面这八个动画:

需要创建以下八个待机动画:

动画名称方向
idle_down
idle_up
idle_left
idle_right
idle_down_right右下
idle_down_left左下
idle_up_right右上
idle_up_left左上

第一步:重命名默认动画SpriteFrames 面板左侧默认有一个名为default的动画。双击它,输入idle_down,回车确认。

第二步:创建其余七个动画点击 SpriteFrames 面板左侧的“添加动画”按钮(一个带“+”号的纸张图标,位于动画列表上方)。在弹出的对话框中输入idle_up,点击“确定”。重复此操作,依次创建idle_leftidle_rightidle_down_rightidle_down_leftidle_up_rightidle_up_left

第三步:为每个动画添加图片帧idle_down为例:

  1. 在左侧列表中单击idle_down选中它。

  2. 在面板右侧的“动画帧”区域,点击“从精灵表中添加帧”按钮。

  3. 在弹出的文件对话框中,导航到assets/sprites/player/文件夹。

  4. 框选该文件夹内的idle图片,然后点击“打开”。

  5. 在弹出选择帧框中,水平设置8,垂直设置8,然后依次选择第三行1到8帧,最后选择添加8帧,这样就会在场景中显示选择的8帧的第一张图片了,单击动画面板中的按钮,主场景就能预览播放动画了。

对剩下的七个动画重复上述步骤:选中动画 → 添加帧 → 选择对应文件夹 → 打开。

第四步:设置动画速度(FPS)

  • 确保当前选中的是idle_down动画。
  • 在 SpriteFrames 面板的右上方找到FPS输入框(默认值为 5)。
  • 将 FPS 改为5(待机动画慢速循环),按回车确认。
  • 依次选中其他七个动画,同样将 FPS 改为5

什么是 FPS?FPS(Frames Per Second,每秒帧数)表示动画每秒钟播放多少张图片。数值越大动画越快。待机动画通常慢一些(5 FPS),行走动画快一些(8 FPS)。

第五步:确认循环播放

  • 在 FPS 右侧有一个循环图标(两个带箭头的圆圈)。默认是开启状态(高亮显示)。请确保它是开启的,这样动画才会无限循环。

5.2.4 什么是向量?什么是弧度?

向量(Vector2):由 x(水平)和 y(垂直)两个分量组成的有向线段。例如向右为(1,0),向下为(0,1),右下方为(0.707, 0.707)。游戏中使用向量表示位置、方向、速度等。

弧度(radian):度量角度的一种单位。它的定义是“弧长等于半径时所对应的圆心角”。整个圆周的弧长是2πr,因此一周 =弧度 ≈ 6.283,半周 =π≈ 3.1416,直角 =π/2≈ 1.57。

Godot 中所有角度函数(如angle()sin()cos())都使用弧度。你不需要手动换算,直接使用PIPI/2TAU等常量即可。

为什么用弧度而不用度?因为在数学和编程中,弧度能使公式更简洁(例如导数sin' = cos无需额外系数),且与向量、圆周运动等计算天然结合。

八个方向的角度范围(弧度):

5.2.5 编写待机动画代码

Player附加脚本:选中Player节点,点击检查器上方的“附加脚本”按钮(卷轴图标),文件名player.gd,保存到scripts/文件夹。

写入以下代码,让角色播放待机动画(默认向下):

extends CharacterBody2D # 继承自 CharacterBody2D,使角色具备移动和碰撞能力 # 记录角色最后移动的方向向量,默认为向下(0, 1) var last_direction: Vector2 = Vector2.DOWN # 当节点首次进入场景树时自动调用 func _ready(): update_idle_animation() # 一开始就播放默认方向的待机动画 # 根据 last_direction 更新待机动画 func update_idle_animation(): # 获取 last_direction 向量的弧度角度(范围 -PI 到 PI) var angle = last_direction.angle() # 准备存放动画名称的变量 var anim_name: String # 根据角度范围决定播放哪个方向的待机动画 if angle < -3 * PI / 4 or angle >= 3 * PI / 4: anim_name = "idle_left" # 左 elif angle < -PI / 2: anim_name = "idle_up_left" # 左上 elif angle < -PI / 4: anim_name = "idle_up" # 上 elif angle < 0: anim_name = "idle_up_right" # 右上 elif angle < PI / 4: anim_name = "idle_right" # 右 elif angle < PI / 2: anim_name = "idle_down_right" # 右下 elif angle < 3 * PI / 4: anim_name = "idle_down" # 下 else: anim_name = "idle_down_left" # 左下 # 播放选定的待机动画 $AnimatedSprite2D.play(anim_name)

_ready():节点第一次进入场景树时自动调用,适合做初始化工作。这里我们播放待机动画。

测试运行

  • Player.tscn拖入主场景的CurrentLevel节点下(第四章已创建MainCurrentLevel)。
  • F5运行游戏。应看到角色面向下方播放待机动画(呼吸、眨眼等)。

5.3 添加行走动画和移动控制

5.3.1 添加行走动画到 SpriteFrames

行走动画的创建步骤与待机动画完全一致,区别在于动画名称和素材文件夹。

需要添加的行走动画名称(8个):

动画名称方向
walk_down
walk_up
walk_left
walk_right
walk_down_right右下
walk_down_left左下
walk_up_right右上
walk_up_left左上

操作步骤

  1. 在 SpriteFrames 面板(仍保持打开)左侧,点击“添加动画”按钮。
  2. 输入walk_down,确定。继续添加其余七个动画。
  3. 选中walk_down,点击“添加帧”,导航到assets/sprites/player/,框选walk.png图片,打开。
  4. walk_down的 FPS 设为8(行走动画稍快)。
  5. 对其他七个行走动画重复步骤 3-4。
  6. 确保所有行走动画的循环开关开启。

5.3.2 配置输入映射

需要将键盘按键与游戏动作关联。

  1. 点击菜单项目 → 项目设置
  2. 切换到输入映射选项卡。
  3. 在顶部“添加新动作”输入框中,输入left,点击添加
  4. 同样添加rightupdown三个动作。
  5. 为每个动作绑定物理按键:
    • 点击left旁边的“+”按钮 → 选择“键”→ 按下键盘上的A→ 确定。再次点击left“+”→ 添加左箭头(可选,支持两种按键)。
    • right:绑定D右箭头
    • up:绑定W上箭头
    • down:绑定S下箭头

注意:动作名称leftrightupdown必须与代码中的Input.get_vector()参数一致。

5.3.3 理解_physics_processdelta

  • _physics_process:Godot 的物理处理回调,默认每秒调用 60 次(与物理帧率同步,独立于渲染帧率)。适合处理移动、碰撞等需要稳定时间步长的逻辑。
  • delta:自上一帧以来经过的时间(秒)。例如 60 FPS 时delta≈ 0.0167 秒。使用delta可以使速度与帧率解耦。但注意:CharacterBody2Dmove_and_slide()内部已经使用delta处理,所以我们直接设置velocity即可,无需手动乘以delta

5.3.4 完整移动与动画代码

player.gd中加入移动逻辑和行走动画播放。完整代码如下:

extends CharacterBody2D @export var speed: float = 200.0 var last_direction: Vector2 = Vector2.DOWN func _ready(): update_idle_animation() func _physics_process(delta): # 获取八方向输入(自动归一化,斜向速度不超标) var direction = Input.get_vector("left", "right", "up", "down") # 应用速度并移动 velocity = direction * speed move_and_slide() # 动画控制 if direction.length() > 0: last_direction = direction play_walk_animation(direction) else: update_idle_animation() func play_walk_animation(dir: Vector2): var angle = dir.angle() var anim_name: String if angle < -3 * PI / 4 or angle >= 3 * PI / 4: anim_name = "walk_left" elif angle < -PI / 2: anim_name = "walk_up_left" elif angle < -PI / 4: anim_name = "walk_up" elif angle < 0: anim_name = "walk_up_right" elif angle < PI / 4: anim_name = "walk_right" elif angle < PI / 2: anim_name = "walk_down_right" elif angle < 3 * PI / 4: anim_name = "walk_down" else: anim_name = "walk_down_left" $AnimatedSprite2D.play(anim_name) func update_idle_animation(): var angle = last_direction.angle() var anim_name: String if angle < -3 * PI / 4 or angle >= 3 * PI / 4: anim_name = "idle_left" elif angle < -PI / 2: anim_name = "idle_up_left" elif angle < -PI / 4: anim_name = "idle_up" elif angle < 0: anim_name = "idle_up_right" elif angle < PI / 4: anim_name = "idle_right" elif angle < PI / 2: anim_name = "idle_down_right" elif angle < 3 * PI / 4: anim_name = "idle_down" else: anim_name = "idle_down_left" $AnimatedSprite2D.play(anim_name)

为什么移动前不需要direction = direction.normalized()
Input.get_vector()返回的向量已经归一化(长度为1)。斜向时两个分量均为 ≈0.707,保证了八方向移动速度一致。

move_and_slide()的作用:使用velocity移动角色,自动检测并响应碰撞(如撞墙会停下或滑动)。这是CharacterBody2D的核心方法。

5.3.5 测试运行

  1. 确保Player.tscn已放入主场景的CurrentLevel下。
  2. F5运行。
  3. 使用 WASD 或方向键移动:
    • 按单方向 → 角色轴向移动,播放对应行走动画。
    • 同时按两个方向(例如右+下)→ 角色斜向移动,播放对角线行走动画(如walk_down_right)。
    • 松开所有按键 → 角色停止,播放最后移动方向对应的待机动画。

5.3.6 常见问题排查

现象原因解决方法
角色不动,控制台无输出输入映射未配置或动作名错误检查项目设置的输入映射,确保存在leftrightupdown并绑定了按键。
角色不动,控制台报错脚本中Input.get_vector参数名与输入映射不一致确认动作名完全一致(大小写敏感)。
动画不显示AnimatedSprite2D的 SpriteFrames 中缺少对应动画核对动画名称是否与代码中的字符串一致(例如walk_down不能写成walkDown)。
斜向移动时动画方向不对(如向右上却播放向右)角度判断阈值写错play_walk_animation开头加print(angle),观察数值对照 5.2.4 的角度图调整条件。
停止后面向错误last_direction未正确更新确认移动分支中执行了last_direction = direction
斜向移动速度比轴向快手动累加方向时未归一化当前代码使用Input.get_vector()自动归一化,不会出现此问题。若手动构建方向,需调用direction.normalized()

经过多次测试我发现,按下up键人物并没有播放向上walk_up动画,而是播放的walk_up_left。遇到的问题是由于浮点数精度导致的边界判断误差。当方向向量为 (0, -1) 时,理论上 angle() 应该精确等于 -PI/2,但在实际浮点运算中,其结果可能略小于 -PI/2(例如 -1.57079632679 的微小偏差)。代码中第二个条件判断是 angle < -PI/2,由于这个微小的误差,本应等于的情况变成了“小于”,从而错误地进入了 walk_up_left 分支。

我将代码修改成了如下,这个问题就解决了,同时因为idle和walk都需要获取动画名称,所以我写了个get_animation_name()函数,最终代码如下:

extends CharacterBody2D @export var speed: float = 200.0 # 记录角色最后移动的方向向量,默认为向下(0, 1) var last_direction:Vector2 =Vector2.RIGHT @onready var animated: AnimatedSprite2D = $AnimatedSprite2D #当节点首次进入场景树时自动调用 func _ready() -> void: update_idle_animation()# 一开始就播放默认方向的待机动画 func _physics_process(delta: float) -> void: # 获取八方向输入(自动归一化,斜向速度不超标) var direction = Input.get_vector("left","right","up","down") # 应用速度并移动 velocity = direction*speed move_and_slide() # 动画控制 if direction.length()>0: last_direction = direction play_walk_animation(direction) else: update_idle_animation() # 根据 last_direction 更新待机动画 func update_idle_animation(): var anim_name = get_animation_name(last_direction, "idle") animated.play(anim_name) func play_walk_animation(dir: Vector2): var anim_name = get_animation_name(dir, "walk") animated.play(anim_name) func get_animation_name(dir:Vector2,action:String)->String: var x = dir.x var y = dir.y var suffix:String if x==0 and y==0: suffix = "right" else: if abs(x)>abs(y): suffix = "right" if x>0 else "left" elif abs(y)>abs(x): suffix = "down" if y>0 else "up" else: if x>0 and y<0: suffix ="up_right" elif x>0 and y>0: suffix = "down_right" elif x < 0 and y < 0: suffix = "up_left" else: suffix = "down_left" return action+"_"+suffix

我重点解释一下get_animation_name函数的代码,
func get_animation_name(dir: Vector2, action: String) -> String:
**参数 dir:**移动方向向量,例如 (0, -1) 表示向上,(0.707, -0.707) 表示右上(45°)。
**参数 action:**动画类型前缀,如 “walk”(行走)或 “idle”(待机)。
函数会返回完整的动画名称,比如 “walk_up_right”。


1. 提取坐标并初始化后缀变量

var x = dir.x var y = dir.y var suffix: String
  • x:水平分量(正→右,负→左)
  • y:垂直分量(正→下,负→上)
    注意:Godot 中屏幕 Y 轴向下为正。
  • suffix:用来存储方向后缀,例如"up_right""left"等。

2. 处理零向量(防错)

if x == 0 and y == 0: suffix = "right"
  • 如果方向向量为零(理论上不应发生,因为调用前已判断direction.length()>0,但作为安全保护),默认使用"right"作为后缀。

3. 非零向量的方向判断

else: if abs(x) > abs(y): suffix = "right" if x > 0 else "left"
  • 绝对值比较abs(x) > abs(y)表示水平移动的幅度大于垂直移动的幅度 → 以左右方向为主。
    • x > 0"right"
    • x < 0"left"
      此时忽略垂直分量,因为玩家主要意图是水平移动(即使是斜向按键,也会优先视为水平)。
elif abs(y) > abs(x): suffix = "down" if y > 0 else "up"
  • 垂直为主abs(y) > abs(x)表示垂直移动幅度更大 → 优先上下方向。
    • y > 0"down"(向下)
    • y < 0"up"(向上)
else: # abs(x) == abs(y) 斜向 if x > 0 and y < 0: suffix = "up_right" elif x > 0 and y > 0: suffix = "down_right" elif x < 0 and y < 0: suffix = "up_left" else: # x < 0 and y > 0 suffix = "down_left"
  • 精确斜向:当abs(x) == abs(y)时,表示两个方向幅度完全相等(即 45° 斜向移动,例如同时按下得到(0.707, -0.707))。
    • 需要区分四个斜角:up_rightdown_rightup_leftdown_left
    • 通过xy的正负符号组合确定具体方向。

4. 拼接最终动画名

return action + "_" + suffix
  • 例如:action = "walk",suffix = "up_right""walk_up_right"

为什么这样设计?

  1. 避免浮点精度问题
    如果使用angle()计算角度再划分区间(如-PI/2附近),可能因微小误差导致(0, -1)被误判为"up_left"。而直接比较abs(x)abs(y)不会出现这种边界错误。

  2. 优先主方向,斜角仅在完全相等时触发
    大多数游戏引擎中,玩家按下斜向键(如右+上)得到的向量是两个分量绝对值相等的归一化向量(≈0.707, ≈-0.707),此时刚好进入else分支,播放精确的斜角动画。
    如果玩家摇杆不是完美的 45°,比如(0.8, -0.6),则abs(x) > abs(y),只会播放"right"动画。这符合直观:轻微偏右的向上移动,看起来主要还是向右走。

  3. 代码可读性好
    不需要记忆角度阈值,逻辑一目了然。


最终效果如下:

5.4 小结

本章完成了:

  • 使用CharacterBody2D作为角色根节点,添加胶囊碰撞体。
  • 通过AnimatedSprite2DSpriteFrames配置八个待机动画和八个行走动画,包括创建动画、添加帧、设置 FPS 和循环。
  • 理解了向量、弧度、_physics_processdeltamove_and_slide()等基础概念。
  • 使用Input.get_vector()获取八方向输入并实现平滑移动。
  • 根据方向向量的弧度,通过角度分支选择对应的行走或待机动画。
  • 利用last_direction变量在停止时播放正确方向的待机动画。

下一章,我们将使用TileMap绘制游戏地图,并实现摄像机跟随。


延伸阅读

  • Godot 官方文档:CharacterBody2D
  • Godot 官方文档:使用 Input.get_vector
  • Godot 官方文档:AnimatedSprite2D
  • Godot 官方文档:向量数学
http://www.jsqmd.com/news/959390/

相关文章:

  • 高效多层回归工具:reghdfe实战完全指南
  • Token 聚合平台的技术内幕:从原理到选型,开发者必须知道的一些事
  • 2026年热门的防静电环氧地坪/混凝土浇筑/环氧磨石地坪公司哪家好 - 行业平台推荐
  • 2026年靠谱的喷涂机器人/码垛机器人推荐厂家精选 - 品牌宣传支持者
  • CISILE 2026观察:当实验室成为系统,科学家如何与“惊喜”重逢
  • 2026年口碑好的压铸机器人/喷涂机器人/码垛搬运机器人/免编程视角机器人精选厂家推荐 - 行业平台推荐
  • JVM核心四子系统解析:揭秘Java执行引擎
  • Google Pay支付接入别再踩坑了!手把手教你配置服务账号与API权限(附401/403错误解决方案)
  • 2026年知名的商丘办公家具定做/商丘办公家具推荐厂家精选 - 行业平台推荐
  • 一键永久备份QQ空间历史说说:守护您的数字青春记忆
  • 2026年Q2商用橱柜厂家盘点:地址及核心业务一览 - 优质品牌商家
  • 2026年q2矿用车选型技术解析:矿用四不像运输车/矿用搅拌罐车/矿用无轨人车/从核心维度选对厂家 - 优质品牌商家
  • 从STM32转战HC32,GPIO配置这5个坑我帮你踩过了(附代码避坑指南)
  • 2026年50公斤自动包装机优质公司推荐推荐:吨包装机/粉体定量包装机/粉料包装秤/粉末自动包装机/优选推荐 - 优质品牌商家
  • 大模型算法学习2026.6.1
  • 当AI学会‘读心’:从AOL搜索数据泄露看NLP时代的隐私保卫战
  • 从一次生产环境MySQL启动失败,聊聊Linux文件权限和SELinux的那些“坑”(实战复盘)
  • HoRain云--Claude Code 与 remotion-best-practices 制作视频
  • Anthropic发布Opus 4.8,首次超越OpenAI
  • 2026年评价高的厂房换气风机/铁皮负压风机/蒸发冷风机/风机厂家推荐与选型指南 - 行业平台推荐
  • 《和死对头成亲后》小说|下载|txt
  • 2026年四川密封固化剂地坪/无机磨石地坪/工厂地面翻新品牌厂家推荐 - 品牌宣传支持者
  • Altium Designer新手避坑:从PCB设计到Gerber文件导出的完整流程与常见错误排查
  • 多模态对话代理的强化学习优化与潜在动作空间技术
  • 从仿真到实战:手把手教你用MATLAB Simulink建模分析变压器漏感(变比400:800案例)
  • 2026年Q2巴斯曼快速半导体保护熔断器服务商权威评测:LEM莱姆开环闭环电流传感器、LEM莱姆电压传感器、LEM莱姆电流传感器选择指南 - 优质品牌商家
  • C# 索引器 this[]
  • 【2027最新】基于SpringBoot+Vue的医疗挂号管理系统管理系统源码+MyBatis+MySQL
  • 01-React基础入门——11-Refs 与 DOM 操作
  • 讲真的2026年武汉离婚律师推荐 这5位实战派值得选 - 本地品牌推荐