9. 2D碰撞与物理
2026/4/14大约 7 分钟
2D碰撞与物理
本章是游戏互动的基础——碰撞检测和物理系统。角色踩地面、子弹打敌人、角色被推开,这些都需要碰撞和物理来实现。
碰撞检测——一切互动的基础
在游戏世界里,两个东西"撞在一起"就叫碰撞。碰撞是战斗、拾取物品、触发事件的基础。
碰撞层与碰撞掩码
Godot 中碰撞检测的核心概念:
| 概念 | 比喻 | 说明 |
|---|---|---|
| 碰撞层(Collision Layer) | 身份标签 | "我是什么"——决定别人能不能检测到我 |
| 碰撞掩码(Collision Mask) | 感知范围 | "我能感知谁"——决定我能和谁碰撞 |
用生活比喻理解
想象一个邮局:
- 碰撞层 = 你的身份标签(你是"普通信件"还是"包裹")
- 碰撞掩码 = 你的投递范围(你能送到"本市"还是"全国")
只有当 A 的碰撞掩码包含 B 的碰撞层时,A 才能检测到和 B 碰撞。
常用碰撞层设置
| 层编号 | 名称 | 说明 |
|---|---|---|
| 1 | 玩家 | 玩家角色 |
| 2 | 敌人 | 敌方单位 |
| 3 | 世界 | 墙壁、地面、障碍物 |
| 4 | 子弹 | 玩家和敌人的子弹 |
| 5 | 拾取物 | 金币、血瓶、道具 |
设置路径:选中节点 → 属性面板 → Collision → Layer 和 Mask
三种物理节点
Godot 提供三种 2D 物理节点,各有各的用途:
| 节点 | 比喻 | 用途 | 受力影响 |
|---|---|---|---|
| CharacterBody2D | 你自己走路 | 玩家、敌人(需要主动控制的) | 不受,靠代码控制移动 |
| RigidBody2D | 掉落的球 | 箱子、球、可以被推的东西 | 受重力和碰撞力影响 |
| StaticBody2D | 地面/墙壁 | 地面、墙壁、平台(不会动的) | 不受 |
CharacterBody2D —— 主动控制的角色
CharacterBody2D 是最常用的物理节点,玩家和 AI 敌人一般都用它。它的移动完全由代码控制,不会被别的物体撞飞。
C#
// 玩家角色移动(最基础的版本)
using Godot;
public partial class Player : CharacterBody2D
{
private const float Speed = 300.0f;
private const float JumpVelocity = -400.0f;
private float _gravity = ProjectSettings.GetSetting("physics/2d/default_gravity").AsSingle();
public override void _PhysicsProcess(double delta)
{
Vector2 velocity = Velocity;
// 重力
if (!IsOnFloor())
velocity.Y += _gravity * (float)delta;
// 跳跃
if (Input.IsActionJustPressed("ui_accept") && IsOnFloor())
velocity.Y = JumpVelocity;
// 左右移动
Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
velocity.X = direction.X * Speed;
Velocity = velocity;
MoveAndSlide();
}
}GDScript
# 玩家角色移动(最基础的版本)
extends CharacterBody2D
const SPEED = 300.0
const JUMP_VELOCITY = -400.0
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
func _physics_process(delta):
var velocity = velocity
# 重力
if not is_on_floor():
velocity.y += gravity * delta
# 跳跃
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = JUMP_VELOCITY
# 左右移动
var direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
velocity.x = direction.x * SPEED
velocity = velocity
move_and_slide()RigidBody2D —— 被物理推动的物体
RigidBody2D 会受重力和碰撞力的影响,像真实世界中的物体一样运动。适合用来做可以被推动的箱子、掉落的石头、弹飞的物体。
C#
// 可推动的箱子
using Godot;
public partial class PushableBox : RigidBody2D
{
// 设置模式为 Rigid(刚体),让它受力影响
// 在编辑器中设置:Mass = 5(比较重)、Gravity Scale = 1
public override void _Ready()
{
// 确保不会一直旋转
AngularDamp = 10.0f;
}
}GDScript
# 可推动的箱子
extends RigidBody2D
# 在编辑器中设置:Mass = 5(比较重)、Gravity Scale = 1
func _ready():
# 确保不会一直旋转
angular_damp = 10.0StaticBody2D —— 不会动的环境
地面、墙壁、平台都用 StaticBody2D。它们不会移动,但其他物体会和它们碰撞。
场景结构示例:
Level (Node2D)
├── Ground (StaticBody2D) ← 地面
│ ├── CollisionShape2D ← 碰撞形状
│ └── Sprite2D ← 外观
├── Wall (StaticBody2D) ← 墙壁
│ ├── CollisionShape2D
│ └── Sprite2D
└── Platform (StaticBody2D) ← 平台
├── CollisionShape2D
└── Sprite2DArea2D —— 不会阻挡但能检测
Area2D 不会产生物理碰撞(角色不会撞到 Area2D 停下来),但能检测到"有什么东西进入了这个区域"。适合用来做:
- 触发区域:玩家走到某个位置触发事件
- 攻击判定:攻击时在前方生成一个短暂的区域
- 拾取物:金币、道具的检测范围
- 陷阱:玩家踩到后触发伤害
C#
// 金币拾取区域
using Godot;
public partial class Coin : Area2D
{
public override void _Ready()
{
// 连接信号:有物体进入这个区域时触发
BodyEntered += OnBodyEntered;
}
private void OnBodyEntered(Node2D body)
{
if (body.IsInGroup("players"))
{
// 给玩家加分
body.Call("AddScore", 10);
// 金币消失
QueueFree();
}
}
}GDScript
# 金币拾取区域
extends Area2D
func _ready():
# 连接信号:有物体进入这个区域时触发
body_entered.connect(_on_body_entered)
func _on_body_entered(body):
if body.is_in_group("players"):
# 给玩家加分
body.add_score(10)
# 金币消失
queue_free()碰撞形状(CollisionShape2D)
每个物理节点都需要一个 CollisionShape2D 来定义碰撞的"形状"。
常用形状
| 形状 | 适用场景 | 说明 |
|---|---|---|
| RectangleShape2D | 方块角色、地面、墙壁 | 最常用,性能最好 |
| CircleShape2D | 球、圆形角色 | 性能好,适合弹幕类 |
| CapsuleShape2D | 人形角色 | 头圆脚圆中间方,最接近人形 |
| PolygonShape2D | 不规则地形 | 可以画任意形状,但性能较差 |
提示
碰撞形状不需要和图片形状完全一致。用一个简化的形状(比如用矩形代替复杂角色),既能保证碰撞准确,又能提升性能。
一面穿过的平台(单向平台)
很多平台跳跃游戏需要"从下面可以跳上去,从上面会踩住"的平台:
- 给平台的 CollisionShape2D 勾选 One Way Collision
- 角色从下方跳上来时能穿过,落在上面时正常站立
C#
// 按下键穿过平台(下跳)
public override void _PhysicsProcess(double delta)
{
// 按住"下"键时,暂时禁用与平台的碰撞
if (Input.IsActionPressed("ui_down"))
{
// 设置所有单向平台在这个帧不产生碰撞
SetDeferred("collision_mask", 0); // 临时禁用
}
}GDScript
# 按下键穿过平台(下跳)
func _physics_process(_delta):
# 按住"下"键时,暂时禁用与平台的碰撞
if Input.is_action_pressed("ui_down"):
# 设置所有单向平台在这个帧不产生碰撞
set_deferred("collision_mask", 0) # 临时禁用战斗系统中的碰撞
近战碰撞(平台跳跃/动作游戏)
玩家攻击时,在角色前方生成一个短暂的"攻击判定区域":
C#
// 攻击判定
public void Attack()
{
var hitbox = GetNode<Area2D>("HitboxArea2D");
var bodies = hitbox.GetOverlappingBodies();
foreach (var body in bodies)
{
if (body.IsInGroup("enemies"))
{
body.Call("take_damage", 10);
}
}
}GDScript
# 攻击判定
func attack():
var hitbox = $HitboxArea2D
var bodies = hitbox.get_overlapping_bodies()
for body in bodies:
if body.is_in_group("enemies"):
body.take_damage(10)子弹碰撞(射击游戏)
子弹飞行并检测碰撞:
C#
// 子弹飞行与碰撞
using Godot;
public partial class Bullet : Area2D
{
private const float Speed = 600.0f;
public override void _PhysicsProcess(double delta)
{
Position = new Vector2(Position.X + Speed * (float)delta, Position.Y);
}
// 当子弹碰到其他物体时触发
private void OnBodyEntered(Node2D body)
{
if (body.IsInGroup("enemies"))
{
body.Call("take_damage", 25);
}
QueueFree(); // 子弹消失
}
}GDScript
# 子弹飞行与碰撞
extends Area2D
const SPEED = 600.0
func _physics_process(delta):
position.x += SPEED * delta
# 当子弹碰到其他物体时触发
func _on_body_entered(body):
if body.is_in_group("enemies"):
body.take_damage(25)
queue_free() # 子弹消失回合制战斗(RPG/卡牌)
回合制不需要实时碰撞检测,而是通过 UI 按钮触发:
C#
// 简单的回合制攻击
public async void PlayerAttack()
{
int damage = _playerAttackPower + (int)GD.RandRange(-3, 3);
_enemyHp -= damage;
ShowDamageText(damage);
if (_enemyHp <= 0)
{
ShowMessage("敌人被击败了!");
GainExperience(50);
}
else
{
await ToSignal(GetTree().CreateTimer(0.5), SceneTreeTimer.SignalName.Timeout);
EnemyTurn();
}
}
public void EnemyTurn()
{
int damage = _enemyAttackPower + (int)GD.RandRange(-2, 2);
_playerHp -= damage;
ShowDamageText(damage);
if (_playerHp <= 0)
{
ShowGameOver();
}
}GDScript
# 简单的回合制攻击
func player_attack():
var damage = player_attack_power + randi_range(-3, 3)
enemy_hp -= damage
show_damage_text(damage)
if enemy_hp <= 0:
show_message("敌人被击败了!")
gain_experience(50)
else:
await get_tree().create_timer(0.5).timeout
enemy_turn()
func enemy_turn():
var damage = enemy_attack_power + randi_range(-2, 2)
player_hp -= damage
show_damage_text(damage)
if player_hp <= 0:
show_game_over()胜利与失败判定
失败条件
C#
// 生命值归零
public void TakeDamage(int amount)
{
_hp -= amount;
UpdateHpBar();
if (_hp <= 0)
{
GameOver();
}
}
// 敌人到达终点(塔防类)
public void OnEnemyReachedEnd(Node2D enemy)
{
_lives -= 1;
enemy.QueueFree();
if (_lives <= 0)
{
GameOver();
}
}GDScript
# 生命值归零
func take_damage(amount):
hp -= amount
update_hp_bar()
if hp <= 0:
game_over()
# 敌人到达终点(塔防类)
func _on_enemy_reached_end(enemy):
lives -= 1
enemy.queue_free()
if lives <= 0:
game_over()胜利条件
C#
// 所有敌人消灭
public void OnEnemyDied()
{
_enemyCount -= 1;
if (_enemyCount <= 0)
{
Victory();
}
}
// 到达终点(平台跳跃)
public void OnGoalReached()
{
Victory();
}GDScript
# 所有敌人消灭
func _on_enemy_died():
enemy_count -= 1
if enemy_count <= 0:
victory()
# 到达终点(平台跳跃)
func _on_goal_reached():
victory()下一章
碰撞和物理搞定了,接下来是游戏的 UI 界面。
