10. 2.5D物理系统
10. 2.5D物理系统
物理系统是什么?
想象你把一个球扔出去:它会受到重力往下落,碰到墙壁会弹开,落到地面会停下来。这些"现实世界的规律",在游戏里就是物理系统负责模拟的。
物理系统让游戏世界有了"重量感"和"真实感"。没有物理系统,角色会穿过地板,子弹会穿过墙壁,一切都像幽灵一样。
Godot 内置了一个完整的 3D 物理引擎(基于 Jolt Physics),你不需要自己写碰撞检测代码,只需要告诉 Godot"这个物体有物理属性"就行了。
2.5D 物理与纯 3D 物理的区别
纯 3D 游戏中,物体可以在 X、Y、Z 三个方向自由运动。但 2.5D 游戏通常只在一个平面内运动:
- 横版游戏(如《空洞骑士》):角色只在 X(左右)和 Y(上下)方向移动,Z 轴(前后)被锁定
- 俯视游戏(如《星露谷物语》3D 版):角色只在 X(左右)和 Z(前后)方向移动,Y 轴(上下)被锁定
这就是 2.5D 物理的核心技巧:在 3D 空间中人为限制运动轴,让游戏感觉像 2D。
Godot 中的物理节点
Godot 提供了三种主要的物理节点,就像三种不同"性格"的物体:
StaticBody3D(静态物体)
## 定义: 完全不动的物体,只负责"挡住"其他物体。
比喻: 就像地面、墙壁、固定的平台——它们不会被推动,也不会掉落。
适合: 地面、墙壁、固定障碍物、建筑物
特点:
- 不受重力影响
- 不会被其他物体推动
- 性能消耗最小
RigidBody3D(刚体)
## 定义: 完全受物理引擎控制的物体,有质量、受重力、会碰撞弹开。
比喻: 就像一个真实的球或箱子——扔出去会飞,落地会弹,被推会滚。
适合: 可以被推动的箱子、掉落的道具、弹射物、台球
特点:
- 自动受重力影响
- 碰撞时会产生真实的物理反应
- 可以设置质量、弹性、摩擦力
CharacterBody3D(角色体)
## 定义: 专门为游戏角色设计的物理节点,介于 StaticBody3D 和 RigidBody3D 之间。
比喻: 就像游戏里的主角——你控制它移动,但它不会像真实物体那样乱滚,而是按照你的意图精确移动。
适合: 玩家角色、敌人、NPC
特点:
- 不自动受重力(需要手动在代码中添加重力)
- 碰撞时不会乱弹,而是停下来
- 提供
move_and_slide()方法处理移动和碰撞
碰撞形状(CollisionShape3D)
物理节点本身只是一个"逻辑容器",它需要配合 CollisionShape3D 才能真正检测碰撞。碰撞形状就是物体的"碰撞边界"。
常用碰撞形状:
| 形状 | 适用场景 | 性能 |
|---|---|---|
| BoxShape3D(盒子) | 方形物体、地面、墙壁 | 最快 |
| SphereShape3D(球体) | 圆形物体、子弹 | 很快 |
| CapsuleShape3D(胶囊) | 人形角色(最常用) | 快 |
| CylinderShape3D(圆柱) | 柱状物体 | 快 |
| ConvexPolygonShape3D(凸多边形) | 不规则物体 | 较慢 |
为什么角色用胶囊形状?
胶囊形状(上下两个半球 + 中间圆柱)非常适合人形角色:
- 底部圆弧可以平滑地爬上台阶
- 不会卡在墙角
- 计算效率高
限制运动轴:2.5D 的关键技巧
横版游戏:锁定 Z 轴
在横版 2.5D 游戏中,角色只在 X-Y 平面移动,Z 轴保持固定。
using Godot;
public partial class PlatformerCharacter : CharacterBody3D
{
[Export] private float _speed = 5.0f;
[Export] private float _jumpVelocity = 8.0f;
// 重力值(从项目设置中获取)
private float _gravity = ProjectSettings.GetSetting("physics/3d/default_gravity").AsSingle();
// 锁定Z轴的位置(角色始终在这个Z坐标上)
private float _lockedZ = 0.0f;
public override void _Ready()
{
// 记录初始Z坐标
_lockedZ = GlobalPosition.Z;
}
public override void _PhysicsProcess(double delta)
{
Vector3 velocity = Velocity;
// 添加重力(Y轴向下)
if (!IsOnFloor())
{
velocity.Y -= _gravity * (float)delta;
}
// 跳跃
if (Input.IsActionJustPressed("ui_accept") && IsOnFloor())
{
velocity.Y = _jumpVelocity;
}
// 左右移动(只修改X轴)
float direction = Input.GetAxis("ui_left", "ui_right");
velocity.X = direction * _speed;
// 关键:强制锁定Z轴,不允许Z方向移动
velocity.Z = 0.0f;
Velocity = velocity;
MoveAndSlide();
// 额外保险:每帧强制修正Z坐标
Vector3 pos = GlobalPosition;
pos.Z = _lockedZ;
GlobalPosition = pos;
}
}extends CharacterBody3D
@export var speed: float = 5.0
@export var jump_velocity: float = 8.0
# 重力值(从项目设置中获取)
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
# 锁定Z轴的位置
var _locked_z: float = 0.0
func _ready() -> void:
# 记录初始Z坐标
_locked_z = global_position.z
func _physics_process(delta: float) -> void:
var vel = velocity
# 添加重力(Y轴向下)
if not is_on_floor():
vel.y -= gravity * delta
# 跳跃
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
vel.y = jump_velocity
# 左右移动(只修改X轴)
var direction = Input.get_axis("ui_left", "ui_right")
vel.x = direction * speed
# 关键:强制锁定Z轴,不允许Z方向移动
vel.z = 0.0
velocity = vel
move_and_slide()
# 额外保险:每帧强制修正Z坐标
var pos = global_position
pos.z = _locked_z
global_position = pos俯视游戏:锁定 Y 轴
在俯视 2.5D 游戏中,角色在 X-Z 平面移动,Y 轴(高度)保持固定。
using Godot;
public partial class TopDownCharacter : CharacterBody3D
{
[Export] private float _speed = 5.0f;
// 锁定Y轴的高度
private float _lockedY = 0.0f;
public override void _Ready()
{
_lockedY = GlobalPosition.Y;
}
public override void _PhysicsProcess(double delta)
{
// 获取输入方向(X-Z平面)
Vector2 inputDir = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
// 将2D输入转换为3D移动(注意:上下对应Z轴)
Vector3 direction = new Vector3(inputDir.X, 0, inputDir.Y).Normalized();
Vector3 velocity = direction * _speed;
// 关键:Y轴速度为0,不受重力影响
velocity.Y = 0.0f;
Velocity = velocity;
MoveAndSlide();
// 强制修正Y坐标
Vector3 pos = GlobalPosition;
pos.Y = _lockedY;
GlobalPosition = pos;
}
}extends CharacterBody3D
@export var speed: float = 5.0
# 锁定Y轴的高度
var _locked_y: float = 0.0
func _ready() -> void:
_locked_y = global_position.y
func _physics_process(delta: float) -> void:
# 获取输入方向(X-Z平面)
var input_dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
# 将2D输入转换为3D移动(注意:上下对应Z轴)
var direction = Vector3(input_dir.x, 0, input_dir.y).normalized()
var vel = direction * speed
# 关键:Y轴速度为0,不受重力影响
vel.y = 0.0
velocity = vel
move_and_slide()
# 强制修正Y坐标
var pos = global_position
pos.y = _locked_y
global_position = pos碰撞层和碰撞掩码
碰撞层(Collision Layer)和碰撞掩码(Collision Mask)是控制"哪些物体之间会发生碰撞"的系统。
比喻: 想象一个多层停车场:
- 碰撞层 = 你在哪一层(你属于哪个类别)
- 碰撞掩码 = 你能看到哪些层(你会和哪些类别碰撞)
常见设置:
| 层编号 | 用途 |
|---|---|
| 层 1 | 玩家 |
| 层 2 | 敌人 |
| 层 3 | 地形/环境 |
| 层 4 | 道具/拾取物 |
| 层 5 | 子弹/投射物 |
示例: 玩家子弹只打敌人,不打玩家自己:
- 子弹的碰撞层 = 层 5
- 子弹的碰撞掩码 = 层 2(只检测敌人)、层 3(检测地形)
在 Godot 编辑器中,在 Inspector 面板的 Collision 部分可以直接点击方块来设置层和掩码。
物理材质:摩擦力与弹性
PhysicsMaterial 可以给物体添加摩擦力和弹性属性:
- Friction(摩擦力):0 = 像冰面,1 = 像橡胶
- Bounce(弹性):0 = 不弹,1 = 完全弹回
在 StaticBody3D 或 RigidBody3D 的 Inspector 中,找到 Physics Material Override,新建一个 PhysicsMaterial 即可设置。
射线检测(RayCast3D)
射线检测就像从一个点发射一条"看不见的激光",检测它碰到了什么。
常见用途:
- 检测角色是否站在地面上
- 瞄准系统(子弹会打到哪里)
- 视线检测(敌人能否看到玩家)
using Godot;
public partial class GroundChecker : Node3D
{
[Export] private RayCast3D _groundRay;
public override void _Ready()
{
// 设置射线方向(向下检测地面)
_groundRay.TargetPosition = new Vector3(0, -1.1f, 0);
_groundRay.Enabled = true;
}
public override void _Process(double delta)
{
if (_groundRay.IsColliding())
{
// 获取碰撞点
Vector3 hitPoint = _groundRay.GetCollisionPoint();
// 获取碰撞的物体
Node3D hitObject = _groundRay.GetCollider() as Node3D;
// 获取碰撞法线(地面朝向)
Vector3 hitNormal = _groundRay.GetCollisionNormal();
GD.Print($"站在地面上,碰撞点:{hitPoint}");
}
}
// 用代码发射射线(不使用节点)
public void CastRayFromCode()
{
var spaceState = GetWorld3D().DirectSpaceState;
var query = PhysicsRayQueryParameters3D.Create(
GlobalPosition, // 起点
GlobalPosition + Vector3.Down * 10.0f, // 终点
0b0100 // 碰撞掩码:只检测层3(地形)
);
var result = spaceState.IntersectRay(query);
if (result.Count > 0)
{
Vector3 hitPos = result["position"].AsVector3();
GD.Print($"射线击中:{hitPos}");
}
}
}extends Node3D
@export var ground_ray: RayCast3D
func _ready() -> void:
# 设置射线方向(向下检测地面)
ground_ray.target_position = Vector3(0, -1.1, 0)
ground_ray.enabled = true
func _process(delta: float) -> void:
if ground_ray.is_colliding():
# 获取碰撞点
var hit_point = ground_ray.get_collision_point()
# 获取碰撞的物体
var hit_object = ground_ray.get_collider()
# 获取碰撞法线(地面朝向)
var hit_normal = ground_ray.get_collision_normal()
print("站在地面上,碰撞点:", hit_point)
# 用代码发射射线(不使用节点)
func cast_ray_from_code() -> void:
var space_state = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(
global_position, # 起点
global_position + Vector3.DOWN * 10.0, # 终点
0b0100 # 碰撞掩码:只检测层3(地形)
)
var result = space_state.intersect_ray(query)
if result:
var hit_pos = result["position"]
print("射线击中:", hit_pos)综合示例:台球碰撞
下面是一个简单的台球示例,展示 RigidBody3D 的使用:
using Godot;
public partial class BilliardBall : RigidBody3D
{
// 当这个球碰到其他物体时触发
public override void _Ready()
{
// 监听碰撞事件
ContactMonitor = true;
MaxContactsReported = 4;
BodyEntered += OnBodyEntered;
}
private void OnBodyEntered(Node body)
{
GD.Print($"台球碰到了:{body.Name}");
// 可以在这里播放碰撞音效
}
// 给球施加一个冲击力(模拟球杆击球)
public void Strike(Vector3 direction, float force)
{
ApplyImpulse(direction.Normalized() * force);
}
}extends RigidBody3D
func _ready() -> void:
# 监听碰撞事件
contact_monitor = true
max_contacts_reported = 4
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node) -> void:
print("台球碰到了:", body.name)
# 可以在这里播放碰撞音效
# 给球施加一个冲击力(模拟球杆击球)
func strike(direction: Vector3, force: float) -> void:
apply_impulse(direction.normalized() * force)小结
| 节点类型 | 特点 | 适用场景 |
|---|---|---|
| StaticBody3D | 不动,挡住其他物体 | 地面、墙壁 |
| RigidBody3D | 完全受物理控制 | 箱子、球、道具 |
| CharacterBody3D | 手动控制,精确移动 | 玩家、敌人 |
2.5D 物理的核心是限制运动轴:横版游戏锁 Z 轴,俯视游戏锁 Y 轴。掌握这个技巧,就能在 3D 空间中实现流畅的 2D 感游戏体验。
