Godot 3 GDScript 教程 / 物理系统(2D)
物理系统(2D)
物理系统概述
Godot 的 2D 物理引擎负责碰撞检测、刚体模拟和运动学计算。理解物理节点的类型和用法是开发 2D 游戏的核心技能。
物理节点类型对比
| 节点 | 移动方式 | 碰撞响应 | 适用场景 |
|---|---|---|---|
RigidBody2D | 物理引擎控制 | 自动 | 碰球、碎片、布娃娃 |
KinematicBody2D | 代码控制 | 手动 | 玩家、NPC、平台 |
StaticBody2D | 不移动 | 自动 | 墙壁、地面、障碍物 |
Area2D | 不移动 | 无物理碰撞 | 拾取物、触发器、检测区 |
RigidBody2D
RigidBody2D 由物理引擎完全控制,受重力、外力和碰撞影响。
基本属性
| 属性 | 说明 | 默认值 |
|---|---|---|
mass | 质量(千克) | 1.0 |
gravity_scale | 重力倍率 | 1.0 |
linear_damp | 线性阻尼 | 0.0 |
angular_damp | 角阻尼 | 0.0 |
mode | 模式 | Rigid |
bounce | 弹性系数 | 0.0 |
friction | 摩擦力 | 1.0 |
RigidBody2D 模式
| 模式 | 说明 |
|---|---|
Rigid | 标准刚体,完全物理模拟 |
Static | 静态,不移动 |
Character | 角色模式,不受旋转力影响 |
Kinematic | 运动学,与 KinematicBody 类似 |
基本使用
extends RigidBody2D
export var jump_force: float = -400.0
export var move_force: float = 200.0
func _integrate_forces(state: Physics2DDirectBodyState) -> void:
# 在物理回调中施加力(更安全)
var velocity = state.get_linear_velocity()
# 水平移动
var input_x = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
velocity.x = input_x * move_force
# 应用速度
state.set_linear_velocity(velocity)
func apply_impulse_force(force: Vector2) -> void:
# 施加冲量(瞬间力)
apply_central_impulse(force)
func jump() -> void:
apply_central_impulse(Vector2(0, jump_force))
弹力球示例
extends RigidBody2D
export var initial_speed: float = 300.0
export var max_speed: float = 600.0
func _ready() -> void:
# 设置物理材质(控制弹性)
var physics_mat = Physics2DServer.body_get_shape_physics_material(get_rid())
if not physics_mat:
physics_mat = Physics2DServer.phys_material_create()
# 创建高弹性材质
physics_mat.bounce = 0.8
physics_mat.friction = 0.1
# 初始速度
linear_velocity = Vector2(
rand_range(-1, 1),
-1
).normalized() * initial_speed
func _physics_process(delta: float) -> void:
# 限制最大速度
if linear_velocity.length() > max_speed:
linear_velocity = linear_velocity.normalized() * max_speed
⚠️ 注意:RigidBody2D 的直接属性修改应放在 _integrate_forces() 中,而不是 _physics_process()。
StaticBody2D
StaticBody2D 是不移动的碰撞体,用于墙壁、地面等。
创建方式
场景树:
StaticBody2D
├── Sprite (显示外观)
└── CollisionShape2D (碰撞形状)
代码设置
extends StaticBody2D
# 设置碰撞层和掩码
func _ready() -> void:
# 层 = 1(在第1层)
collision_layer = 1
# 掩码 = 0(不检测任何层,因为是静态的)
collision_mask = 0
# 设置物理材质
var mat = PhysicsMaterial.new()
mat.bounce = 0.5
mat.friction = 0.8
physics_material_override = mat
# 可移动的平台(KinematicBody 更适合,但 StaticBody 也可以)
func move_platform(new_pos: Vector2) -> void:
# 使用 Tween 移动
$Tween.interpolate_property(
self, "position",
position, new_pos, 2.0,
Tween.TRANS_SINE, Tween.EASE_IN_OUT
)
$Tween.start()
KinematicBody2D
KinematicBody2D 是最常用的物理节点,完全由代码控制移动,但可以检测碰撞。
核心方法
| 方法 | 说明 |
|---|---|
move_and_slide() | 移动并沿表面滑动 |
move_and_slide_with_snap() | 移动并吸附到表面 |
move_and_collide() | 移动并返回碰撞信息 |
is_on_floor() | 是否在地面上 |
is_on_wall() | 是否在墙上 |
is_on_ceiling() | 是否在天花板上 |
move_and_slide 详解
extends KinematicBody2D
export var speed: float = 300.0
export var gravity: float = 980.0
export var jump_force: float = -500.0
var velocity: Vector2 = Vector2.ZERO
func _physics_process(delta: float) -> void:
# 重力
velocity.y += gravity * delta
# 水平输入
var input_x = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
velocity.x = input_x * speed
# 跳跃
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_force
# 移动并滑动
# 参数: velocity, up_direction, stop_on_slope, max_slides, floor_max_angle, infinite_inertia
velocity = move_and_slide(velocity, Vector2.UP)
# 检测地面状态
if is_on_floor():
print("在地面上")
elif is_on_wall():
print("碰到了墙")
elif is_on_ceiling():
print("碰到了天花板")
move_and_slide_with_snap
extends KinematicBody2D
var velocity: Vector2 = Vector2.ZERO
var snap_vector: Vector2 = Vector2.ZERO
func _physics_process(delta: float) -> void:
velocity.y += gravity * delta
var input_x = _get_input_x()
velocity.x = input_x * speed
# 跳跃时禁用吸附
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_force
snap_vector = Vector2.ZERO # 跳跃时不要吸附
else:
snap_vector = Vector2.DOWN * 16 # 向下吸附 16 像素
velocity = move_and_slide_with_snap(
velocity,
snap_vector, # 吸附向量
Vector2.UP, # 地面方向
true, # 停止在斜坡上
4, # 最大滑动次数
deg2rad(45) # 地面最大角度
)
💡 提示:snap 参数使角色在下坡时不会跳起,在平台游戏中非常有用。
move_and_collide
extends KinematicBody2D
func _physics_process(delta: float) -> void:
var motion = velocity * delta
var collision = move_and_collide(motion)
if collision:
# 碰撞发生
var collider = collision.collider
var normal = collision.normal
var position = collision.position
print("碰撞到: ", collider.name)
print("碰撞法线: ", normal)
print("碰撞位置: ", position)
# 手动反弹
velocity = velocity.bounce(normal)
# 沿表面滑动
velocity = velocity.slide(normal)
Area2D(区域检测)
Area2D 用于检测物体进入/离开特定区域,不参与物理碰撞。
常见用途
- 拾取物(金币、道具)
- 陷阱区域
- 传送门
- 伤害区域
- 触发器(剧情、机关)
信号
| 信号 | 参数 | 说明 |
|---|---|---|
body_entered | body: Node | 物理体进入区域 |
body_exited | body: Node | 物理体离开区域 |
area_entered | area: Area2D | 另一个 Area2D 进入 |
area_exited | area: Area2D | 另一个 Area2D 离开 |
body_shape_entered | body_id, body, body_shape_index, area_shape_index | 精确碰撞形状 |
拾取物示例
extends Area2D
export var score_value: int = 10
export var heal_amount: int = 0
func _ready() -> void:
connect("body_entered", self, "_on_body_entered")
func _on_body_entered(body: Node) -> void:
if body.is_in_group("players"):
# 加分
GameManager.add_score(score_value)
# 回血
if heal_amount > 0:
body.heal(heal_amount)
# 播放拾取音效
$PickupSound.play()
# 隐藏并销毁
visible = false
$CollisionShape2D.set_deferred("disabled", true)
yield($PickupSound, "finished")
queue_free()
伤害区域示例
extends Area2D
export var damage: int = 20
export var knockback_force: float = 300.0
func _ready() -> void:
connect("body_entered", self, "_on_body_entered")
func _on_body_entered(body: Node) -> void:
if body.has_method("take_damage"):
body.take_damage(damage)
# 击退效果
var knockback_dir = (body.global_position - global_position).normalized()
if body.has_method("apply_knockback"):
body.apply_knockback(knockback_dir * knockback_force)
# 启用/禁用伤害区域
func enable_damage() -> void:
$CollisionShape2D.set_deferred("disabled", false)
func disable_damage() -> void:
$CollisionShape2D.set_deferred("disabled", true)
CollisionShape2D
CollisionShape2D 定义了碰撞体的形状。
常用碰撞形状
| 形状 | 适用场景 |
|---|---|
CircleShape2D | 圆形物体、子弹 |
RectangleShape2D | 矩形物体、角色、墙壁 |
CapsuleShape2D | 角色胶囊体 |
SegmentShape2D | 线段、平台边缘 |
RayShape2D | 射线形状 |
ConvexPolygonShape2D | 凸多边形 |
ConcavePolygonShape2D | 凹多边形(仅静态体) |
代码设置碰撞形状
extends CollisionShape2D
func _ready() -> void:
# 设置圆形
var circle = CircleShape2D.new()
circle.radius = 32.0
shape = circle
# 设置矩形
var rect = RectangleShape2D.new()
rect.extents = Vector2(32, 48) # 半尺寸
shape = rect
# 设置胶囊体
var capsule = CapsuleShape2D.new()
capsule.radius = 16.0
capsule.height = 64.0
shape = capsule
# 动态启用/禁用碰撞
func disable() -> void:
set_deferred("disabled", true)
func enable() -> void:
set_deferred("disabled", false)
⚠️ 注意:不要在物理回调中直接设置 disabled,使用 set_deferred() 以避免物理引擎冲突。
碰撞层与掩码
碰撞层/掩码概念
| 概念 | 说明 |
|---|---|
| Layer(层) | “我是什么” - 物体自身所在的层 |
| Mask(掩码) | “我检测谁” - 物体会与哪些层发生碰撞 |
推荐层设置
| 层号 | 名称 | 说明 |
|---|---|---|
| 1 | World | 静态世界(墙壁、地面) |
| 2 | Player | 玩家 |
| 3 | Enemy | 敌人 |
| 4 | Projectile | 子弹/投射物 |
| 5 | Pickup | 拾取物 |
| 6 | Trigger | 触发器 |
| 7 | Platform | 单向平台 |
碰撞矩阵示例
物体 Layer Mask 说明
────────────────────────────────────────────────
Player 2 1,3,5,7 检测世界、敌人、拾取物、平台
Enemy 3 1,2,4 检测世界、玩家、子弹
Bullet 4 1,3 检测世界、敌人
Coin 5 2 只检测玩家
Wall 1 0 不检测任何东西(静态)
Trigger 6 2 只检测玩家
Platform 7 2 只检测玩家
代码设置
# 玩家设置
extends KinematicBody2D
func _ready() -> void:
# Layer 2(玩家层)
collision_layer = 0b0000_0010 # 2
# Mask 1,3,5,7(世界、敌人、拾取物、平台)
collision_mask = 0b0101_0101 # 85
# 敌人设置
extends KinematicBody2D
func _ready() -> void:
collision_layer = 0b0000_0100 # 3
collision_mask = 0b0000_1011 # 1,2,4
运行时修改碰撞层
# 玩家无敌时忽略敌人
func enable_invincibility() -> void:
# 关闭敌人层检测
set_collision_mask_bit(2, false) # 第3层(敌人)
func disable_invincibility() -> void:
set_collision_mask_bit(2, true)
物理材质(PhysicsMaterial)
物理材质控制物体的弹性和摩擦力。
创建与使用
extends RigidBody2D
func _ready() -> void:
var mat = PhysicsMaterial.new()
mat.bounce = 0.8 # 弹性系数 (0-1)
mat.friction = 0.2 # 摩擦力 (0-1)
mat.rough = false # 粗糙度(增加摩擦)
mat.absorbent = false # 吸收性(减少弹性)
physics_material_override = mat
常见材质配置
| 材质 | bounce | friction | 适用场景 |
|---|---|---|---|
| 橡胶 | 0.8 | 0.9 | 弹力球 |
| 冰面 | 0.1 | 0.05 | 滑冰关卡 |
| 泥地 | 0.0 | 1.0 | 沼泽地形 |
| 金属 | 0.3 | 0.3 | 金属弹球 |
| 超级弹力 | 1.0 | 0.0 | 弹射器 |
RayCast2D(射线检测)
RayCast2D 沿一条射线检测碰撞体,用于视线检测、武器射线等。
基本使用
extends Node2D
onready var ray = $RayCast2D
func _ready() -> void:
# 设置射线方向和长度
ray.cast_to = Vector2(200, 0) # 向右 200 像素
ray.enabled = true
func _physics_process(delta: float) -> void:
# 强制更新(如果射线不是每帧需要,可关闭 enabled)
ray.force_raycast_update()
if ray.is_colliding():
var collider = ray.get_collider()
var collision_point = ray.get_collision_point()
var collision_normal = ray.get_collision_normal()
print("碰撞到: ", collider.name)
print("碰撞点: ", collision_point)
print("碰撞法线: ", collision_normal)
视线检测
extends KinematicBody2D
onready var sight_ray = $SightRay
export var sight_range: float = 300.0
var can_see_player: bool = false
func _physics_process(delta: float) -> void:
var player = get_tree().get_nodes_in_group("players")
if player.size() == 0:
return
var player_pos = player[0].global_position
var direction = (player_pos - global_position).normalized()
sight_ray.cast_to = direction * sight_range
sight_ray.force_raycast_update()
if sight_ray.is_colliding():
var collider = sight_ray.get_collider()
can_see_player = collider.is_in_group("players")
else:
can_see_player = false
if can_see_player:
_chase_player(player_pos)
武器射线检测
extends Node2D
export var damage: int = 50
export var range_distance: float = 1000.0
var beam_color: Color = Color.red
onready var ray = $RayCast2D
func fire(direction: Vector2) -> void:
ray.cast_to = direction * range_distance
ray.force_raycast_update()
if ray.is_colliding():
var hit_pos = ray.get_collision_point()
var target = ray.get_collider()
# 造成伤害
if target.has_method("take_damage"):
target.take_damage(damage)
# 绘制激光线
_draw_beam(global_position, hit_pos)
# 生成命中效果
_spawn_hit_effect(hit_pos)
else:
_draw_beam(global_position, global_position + direction * range_distance)
func _draw_beam(from: Vector2, to: Vector2) -> void:
$BeamLine.points = [from - global_position, to - global_position]
$BeamLine.visible = true
yield(get_tree().create_timer(0.05), "timeout")
$BeamLine.visible = false
物理最佳实践
选择正确的节点类型
需要物理碰撞?
├── 是 → 需要自己控制移动?
│ ├── 是 → KinematicBody2D(玩家、NPC)
│ └── 否 → RigidBody2D(物理对象)
└── 否 → 只需要检测?
└── Area2D(拾取物、触发器)
碰撞层管理
# 使用常量或枚举管理层号
class_name CollisionLayers
const WORLD = 0 # Layer 1
const PLAYER = 1 # Layer 2
const ENEMY = 2 # Layer 3
const BULLET = 3 # Layer 4
const PICKUP = 4 # Layer 5
const TRIGGER = 5 # Layer 6
const PLATFORM = 6 # Layer 7
# 使用
func setup_player() -> void:
set_collision_layer_bit(CollisionLayers.PLAYER, true)
set_collision_mask_bit(CollisionLayers.WORLD, true)
set_collision_mask_bit(CollisionLayers.ENEMY, true)
性能优化建议
| 建议 | 说明 |
|---|---|
| 使用简单碰撞形状 | 圆形和矩形比多边形快得多 |
| 减少活跃刚体数量 | 不在视野内的刚体设为 Static 模式 |
| 关闭不必要的 RayCast | 只在需要时启用射线检测 |
| 使用 Area2D 替代 RayCast | 如果只需区域检测,Area2D 更高效 |
| 碰撞层精确设置 | 减少不必要的碰撞检测对 |
单向平台
# 单向平台(只能从下方穿过,站在上面)
extends StaticBody2D
func _ready() -> void:
# 设置为单向碰撞(仅在法线朝上时碰撞)
# 在 Inspector 中设置 CollisionShape2D 的 One Way Collision 为 true
pass
# 或通过代码设置 One Way Collision
# CollisionShape2D.one_way_collision = true
# CollisionShape2D.one_way_collision_margin = 4.0
游戏开发场景
场景:完整的平台游戏物理
extends KinematicBody2D
# 导出参数
export var speed: float = 250.0
export var acceleration: float = 1500.0
export var friction: float = 2000.0
export var jump_force: float = -520.0
export var gravity: float = 1400.0
export var max_fall: float = 900.0
export var wall_jump_force: Vector2 = Vector2(300, -450)
export var wall_slide_speed: float = 80.0
# 内部状态
var velocity: Vector2 = Vector2.ZERO
var snap: Vector2 = Vector2.ZERO
var facing: int = 1
var jump_count: int = 0
var max_jumps: int = 2 # 二段跳
var was_on_floor: bool = false
var coyote_timer: float = 0.0
var jump_buffer: float = 0.0
func _physics_process(delta: float) -> void:
_apply_gravity(delta)
_process_input(delta)
_process_jump()
_process_wall_slide(delta)
velocity = move_and_slide_with_snap(velocity, snap, Vector2.UP, true)
_update_timers(delta)
_check_floor_state()
func _apply_gravity(delta: float) -> void:
if is_on_floor():
velocity.y = 0
jump_count = 0
else:
velocity.y += gravity * delta
velocity.y = min(velocity.y, max_fall)
func _process_input(delta: float) -> void:
var input_x = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
if input_x != 0:
velocity.x = move_toward(velocity.x, input_x * speed, acceleration * delta)
facing = 1 if input_x > 0 else -1
$Sprite.flip_h = facing == -1
else:
velocity.x = move_toward(velocity.x, 0, friction * delta)
# 吸附设置
snap = Vector2.DOWN * 8 if not Input.is_action_pressed("jump") else Vector2.ZERO
func _process_jump() -> void:
# 土狼时间 + 输入缓冲
var can_jump = (is_on_floor() or coyote_timer > 0) or jump_count < max_jumps
if Input.is_action_just_pressed("jump"):
jump_buffer = 0.1
if jump_buffer > 0 and can_jump:
velocity.y = jump_force
jump_count += 1
jump_buffer = 0
coyote_timer = 0
func _process_wall_slide(delta: float) -> void:
if is_on_wall() and not is_on_floor() and velocity.y > 0:
var wall_normal = get_slide_collision(0).normal
var input_away = (facing == 1 and Input.is_action_pressed("move_left")) or \
(facing == -1 and Input.is_action_pressed("move_right"))
velocity.y = min(velocity.y, wall_slide_speed)
# 墙跳
if Input.is_action_just_pressed("jump"):
velocity = Vector2(-wall_normal.x * wall_jump_force.x, wall_jump_force.y)
facing = -facing
func _update_timers(delta: float) -> void:
coyote_timer -= delta
jump_buffer -= delta
func _check_floor_state() -> void:
if is_on_floor() and not was_on_floor:
# 着陆时重置跳跃
jump_count = 0
was_on_floor = is_on_floor()