3. 角色移动与跳跃
角色移动与跳跃
理解 CharacterBody3D
什么是 CharacterBody3D?
CharacterBody3D 是 Godot 4 中专门用来做"角色控制"的节点类型。你可以把它想象成一个"有碰撞检测的盒子"——它能自动和墙壁、地面碰撞,不会穿过去。和普通的 RigidBody3D(物理刚体)不同,CharacterBody3D 不受物理引擎控制,而是由你的代码来决定它往哪走。
为什么用 CharacterBody3D?
| 节点类型 | 特点 | 适合做什么 |
|---|---|---|
| Node3D | 什么都没有,就是个空节点 | 容器、管理器 |
| RigidBody3D | 受重力、碰撞影响,物理引擎控制 | 球、石头、可推动的箱子 |
| StaticBody3D | 不动,但是有碰撞 | 墙壁、地面、平台 |
| CharacterBody3D | 有碰撞,但由代码控制移动 | 玩家角色、NPC、敌人 |
我们选 CharacterBody3D,因为角色的移动应该由玩家输入来控制,而不是物理引擎。
横版游戏的坐标系
重要:横版游戏用 XY 平面
大多数3D游戏用 XZ 平面(X是左右,Z是前后,Y是上下)。但横版游戏不一样——我们用 XY 平面(X是左右,Y是上下),Z轴基本不用(或者说所有东西都在 Z=0 这一层)。
Y轴(上下/跳跃)
↑
│
│ ★ 玩家(x=5, y=1, z=0)
│
│
│
└────────────────→ X轴(左右移动)
Z轴(深度)→ 不用管,所有物体都在 Z=0为什么不用 XZ?
如果你用 XZ 平面做横版游戏,那"跳跃"就是沿着Y轴——但这会让摄像机设置变得很麻烦。用 XY 平面,摄像机只需要从Z轴正方向看过来就行了,简单很多。
创建玩家场景
场景结构
Player (CharacterBody3D) # 玩家根节点
├── CollisionShape3D # 碰撞体(胶囊形)
├── MeshInstance3D # 角色模型(暂时用立方体代替)
├── Sprite3D # 角色贴图(可选)
├── CameraMount (Marker3D) # 摄像机挂载点
└── Muzzle (Marker3D) # 枪口位置(发射子弹的位置)角色参数设计
| 参数 | 值 | 说明 |
|---|---|---|
| 移动速度 | 8.0 | 每秒移动8米 |
| 跳跃力 | 12.0 | 跳跃初速度 |
| 重力 | 30.0 | 重力加速度 |
| 最大跳跃次数 | 2 | 支持二段跳 |
| 摩擦力 | 10.0 | 松开按键后的减速速度 |
参数怎么调?
这些数值不是固定不变的,需要在实际测试中反复调整。你可以先设定一个初始值,运行游戏试一下手感,然后根据感觉微调。游戏开发中管这叫"手感调优(game feel tuning)"。
基础移动脚本
下面是完整的玩家移动控制脚本。这段代码实现了:左右移动、跳跃、重力、二段跳。
using Godot;
/// <summary>
/// 玩家控制器 - 处理角色移动和跳跃
/// </summary>
public partial class Player : CharacterBody3D
{
// ===== 可调参数 =====
[ExportGroup("移动参数")]
[Export] public float MoveSpeed = 8.0f; // 移动速度
[Export] public float JumpForce = 12.0f; // 跳跃力
[Export] public float Gravity = 30.0f; // 重力加速度
[Export] public int MaxJumps = 2; // 最大跳跃次数(含二段跳)
[Export] public float Friction = 10.0f; // 摩擦力(松开按键后的减速)
[ExportGroup("摄像机参数")]
[Export] public float CameraSmoothSpeed = 5.0f; // 摄像机跟随平滑速度
// ===== 内部状态 =====
private int _jumpCount = 0; // 当前已跳跃次数
private Vector3 _velocity = Vector3.Zero; // 当前速度
private Camera3D _camera; // 摄像机引用
private bool _isFacingRight = true; // 是否面朝右方
// ===== 属性 =====
/// <summary>角色是否在地面上</summary>
public bool IsOnFloor => IsOnFloor();
/// <summary>角色面朝的方向(1=右,-1=左)</summary>
public int FacingDirection => _isFacingRight ? 1 : -1;
public override void _Ready()
{
// 查找场景中的摄像机
_camera = GetParent().GetNode<Camera3D>("CameraRig/MainCamera");
}
public override void _PhysicsProcess(double delta)
{
float dt = (float)delta;
// 1. 处理水平移动
HandleMovement(dt);
// 2. 处理跳跃
HandleJump();
// 3. 应用重力
ApplyGravity(dt);
// 4. 移动角色
Velocity = _velocity;
MoveAndSlide();
// 5. 更新角色朝向
UpdateFacing();
// 6. 摄像机跟随
UpdateCamera(dt);
}
/// <summary>
/// 处理水平移动输入
/// </summary>
private void HandleMovement(float dt)
{
// 读取左右输入(-1到1之间)
float inputDirection = 0;
if (Input.IsActionPressed("move_right"))
inputDirection += 1;
if (Input.IsActionPressed("move_left"))
inputDirection -= 1;
// 设置水平速度
float targetSpeedX = inputDirection * MoveSpeed;
// 平滑加减速(让移动更有手感)
if (inputDirection != 0)
{
_velocity.X = Mathf.MoveToward(
_velocity.X, targetSpeedX, MoveSpeed * dt * 5
);
}
else
{
// 松开按键时用摩擦力减速
_velocity.X = Mathf.MoveToward(_velocity.X, 0, Friction * dt);
}
}
/// <summary>
/// 处理跳跃输入
/// </summary>
private void HandleJump()
{
// 按下跳跃键
if (Input.IsActionJustPressed("jump"))
{
if (_jumpCount < MaxJumps)
{
// 跳跃!给一个向上的速度
_velocity.Y = JumpForce;
_jumpCount++;
GD.Print($"跳跃!第 {_jumpCount} 次跳跃");
}
}
// 着地时重置跳跃次数
if (IsOnFloor())
{
_jumpCount = 0;
}
}
/// <summary>
/// 应用重力
/// </summary>
private void ApplyGravity(float dt)
{
// 如果在地面上,就不需要额外加重力
// (MoveAndSlide 已经处理了地面碰撞)
if (!IsOnFloor())
{
_velocity.Y -= Gravity * dt;
}
}
/// <summary>
/// 更新角色朝向(翻转角色模型)
/// </summary>
private void UpdateFacing()
{
// 根据移动方向翻转
if (_velocity.X > 0.1f)
_isFacingRight = true;
else if (_velocity.X < -0.1f)
_isFacingRight = false;
// 翻转模型(通过Y轴旋转180度)
var mesh = GetNode<MeshInstance3D>("MeshInstance3D");
if (mesh != null)
{
float targetRotationY = _isFacingRight ? 0 : Mathf.Pi;
mesh.Rotation = new Vector3(
mesh.Rotation.X,
targetRotationY,
mesh.Rotation.Z
);
}
}
/// <summary>
/// 摄像机跟随玩家
/// </summary>
private void UpdateCamera(float dt)
{
if (_camera == null) return;
// 目标位置:跟随玩家X,保持Y不变
Vector3 targetPos = new Vector3(
Position.X,
_camera.Position.Y,
_camera.Position.Z
);
// 平滑跟随
_camera.Position = _camera.Position.Lerp(targetPos, CameraSmoothSpeed * dt);
}
}extends CharacterBody3D
## 玩家控制器 - 处理角色移动和跳跃
# ===== 可调参数 =====
@export_group("移动参数")
@export var move_speed: float = 8.0 # 移动速度
@export var jump_force: float = 12.0 # 跳跃力
@export var gravity: float = 30.0 # 重力加速度
@export var max_jumps: int = 2 # 最大跳跃次数(含二段跳)
@export var friction: float = 10.0 # 摩擦力(松开按键后的减速)
@export_group("摄像机参数")
@export var camera_smooth_speed: float = 5.0 # 摄像机跟随平滑速度
# ===== 内部状态 =====
var _jump_count: int = 0 # 当前已跳跃次数
var _velocity: Vector3 = Vector3.ZERO # 当前速度
var _camera: Camera3D # 摄像机引用
var _is_facing_right: bool = true # 是否面朝右方
# ===== 属性 =====
## 角色面朝的方向(1=右,-1=左)
var facing_direction: int:
get:
return 1 if _is_facing_right else -1
func _ready():
# 查找场景中的摄像机
_camera = get_parent().get_node("CameraRig/MainCamera")
func _physics_process(delta):
# 1. 处理水平移动
_handle_movement(delta)
# 2. 处理跳跃
_handle_jump()
# 3. 应用重力
_apply_gravity(delta)
# 4. 移动角色
velocity = _velocity
move_and_slide()
# 5. 更新角色朝向
_update_facing()
# 6. 摄像机跟随
_update_camera(delta)
## 处理水平移动输入
func _handle_movement(dt: float):
# 读取左右输入(-1到1之间)
var input_direction: float = 0
if Input.is_action_pressed("move_right"):
input_direction += 1
if Input.is_action_pressed("move_left"):
input_direction -= 1
# 设置水平速度
var target_speed_x = input_direction * move_speed
# 平滑加减速(让移动更有手感)
if input_direction != 0:
_velocity.x = move_toward(
_velocity.x, target_speed_x, move_speed * dt * 5
)
else:
# 松开按键时用摩擦力减速
_velocity.x = move_toward(_velocity.x, 0, friction * dt)
## 处理跳跃输入
func _handle_jump():
# 按下跳跃键
if Input.is_action_just_pressed("jump"):
if _jump_count < max_jumps:
# 跳跃!给一个向上的速度
_velocity.y = jump_force
_jump_count += 1
print("跳跃!第 %d 次跳跃" % _jump_count)
# 着地时重置跳跃次数
if is_on_floor():
_jump_count = 0
## 应用重力
func _apply_gravity(dt: float):
# 如果不在地面上,加重力
if not is_on_floor():
_velocity.y -= gravity * dt
## 更新角色朝向(翻转角色模型)
func _update_facing():
# 根据移动方向翻转
if _velocity.x > 0.1:
_is_facing_right = true
elif _velocity.x < -0.1:
_is_facing_right = false
# 翻转模型(通过Y轴旋转180度)
var mesh = get_node("MeshInstance3D")
if mesh:
var target_rotation_y = 0.0 if _is_facing_right else PI
mesh.rotation = Vector3(
mesh.rotation.x,
target_rotation_y,
mesh.rotation.z
)
## 摄像机跟随玩家
func _update_camera(dt: float):
if not _camera:
return
# 目标位置:跟随玩家X,保持Y不变
var target_pos = Vector3(
position.x,
_camera.position.y,
_camera.position.z
)
# 平滑跟随
_camera.position = _camera.position.lerp(target_pos, camera_smooth_speed * dt)关键概念详解
MoveAndSlide() 是什么?
MoveAndSlide() 是 CharacterBody3D 最重要的方法。它做了三件事:
- 移动角色——按照
Velocity的值移动 - 碰撞检测——如果撞墙了,自动停下来
- 滑动——如果斜着撞墙,会沿着墙壁表面滑动(而不是卡住)
通俗理解
想象你在推一个箱子往前走。如果前面有墙,箱子撞到墙就停了(碰撞检测)。如果前面有一个斜坡,箱子不会卡住,而是沿着斜坡滑上去(滑动)。MoveAndSlide() 就是自动帮你处理这些。
二段跳的实现原理
二段跳的核心就是用一个计数器:
着地时:跳跃计数 = 0
第一次跳:跳跃计数 = 1(允许,因为 1 < 2)
空中再跳:跳跃计数 = 2(允许,因为 2 < 2)
空中还想跳:跳跃计数 = 3(不允许,因为 3 >= 2)
着地后:跳跃计数 = 0(重置,又可以跳两次了)二段跳的手感调优
原版魂斗罗没有二段跳。但很多现代横版射击游戏都有,因为它增加了操作深度。如果你想要更经典的感觉,把 MaxJumps 设为 1 就行。
重力和跳跃力的关系
重力参数很关键
重力太大,角色像石头一样掉下来,操作感很差;重力太小,角色飘在空中像宇航员。一般建议:
- 重力(Gravity)= 跳跃力(JumpForce)的 2~3 倍
- 这样角色跳跃的最高点大约在 0.4~0.6 秒时到达,感觉比较自然
| 参数组合 | 效果 | 适合 |
|---|---|---|
| 重力20,跳跃力10 | 慢节奏,飘浮感 | 平台跳跃游戏(马里奥) |
| 重力30,跳跃力12 | 中等节奏,平衡 | 横版射击游戏(魂斗罗) |
| 重力45,跳跃力15 | 快节奏,紧凑 | 快速动作游戏 |
摄像机跟随详解
为什么需要平滑跟随?
如果摄像机直接"粘"在角色身上,角色一移动摄像机就跟着动,画面会非常抖——就像用手机拍视频时手抖一样。所以我们需要"平滑跟随":摄像机不是立刻到角色位置,而是慢慢追过去。
Lerp 函数
Lerp 是"线性插值(Linear Interpolation)"的缩写,意思是"在两个值之间慢慢过渡":
结果 = 当前值 + (目标值 - 当前值) * 比例
比例越大 → 跟随越快 → 画面越紧
比例越小 → 跟随越慢 → 画面越松| CameraSmoothSpeed | 效果 |
|---|---|
| 2.0 | 慢跟随,有拖尾感,适合悠闲游戏 |
| 5.0 | 中等跟随,平衡感好,适合大多数游戏 |
| 10.0 | 快跟随,紧贴角色,适合高速游戏 |
| 50.0 | 几乎直接跟随,没有平滑效果 |
角色翻转技巧
当玩家向左走时,角色应该面朝左边。最简单的做法是把模型绕Y轴旋转180度。
另一种翻转方法
除了旋转模型,你还可以用 Scale 来翻转:scale.x = -1。但这种方法有时候会让贴图也翻过去,看起来像"镜像"效果。旋转Y轴是更安全的选择。
常见问题
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 角色一直往下掉 | 没有地面碰撞体 | 确保地面有 CollisionShape3D |
| 角色穿墙 | 速度太快,一帧跳过了墙 | 降低速度或开启连续碰撞检测 |
| 跳跃后不会落地 | 重力方向不对 | 检查 Gravity 值是否为正数 |
| 移动时卡顿 | 用了 _Process 而不是 _PhysicsProcess | 把移动代码放在 _PhysicsProcess 中 |
| 二段跳按不出 | 计数器没重置 | 检查 IsOnFloor 判断是否正确 |
_Process vs _PhysicsProcess
_Process:每帧执行一次,帧率不稳定时执行频率也不稳定_PhysicsProcess:按固定时间步长执行(默认60次/秒),适合物理和移动- 永远把移动代码放在
_PhysicsProcess中,不然不同帧率下移动速度会不一样
本章小结
本章我们实现了完整的角色移动系统:
- 使用 CharacterBody3D 作为角色根节点
- XY 平面移动,适合横版游戏
- 左右移动,带平滑加减速和摩擦力
- 跳跃系统,支持二段跳
- 重力系统,角色会自然下落
- 角色翻转,面朝移动方向
- 摄像机跟随,平滑过渡不抖动
→ 4. 射击系统
