第十七章:2D物理系统
"物理引擎让游戏世界充满真实感,让物体运动自然流畅。"
物理系统是游戏开发的核心组件之一。无论是平台跳跃游戏中角色的跳跃与落地,还是愤怒的小鸟中物体的碰撞与飞散,背后都离不开物理引擎的支撑。本章将全面讲解Godot的2D物理引擎,帮助你理解物理模拟的原理,掌握各类物理节点的使用方法。
本章学习目标:
- 理解Godot物理引擎的工作原理和基本概念
- 掌握四种物理体节点的特点与适用场景
- 学会使用Area2D实现触发器、拾取物、伤害区域等功能
- 理解物理材质(摩擦力、弹性)对物体行为的影响
- 掌握射线检测、形状查询等物理查询方法
17.1 物理引擎概述
在开始编写代码之前,我们需要先理解物理引擎的基本概念。物理引擎负责模拟现实世界中的物理规律,包括重力、碰撞、摩擦力、弹性等。Godot内置了强大的2D物理引擎,让开发者无需从零实现这些复杂的物理计算。
17.1.1 Godot物理引擎特点
Godot的2D物理引擎采用了类似Box2D的设计理念,经过优化适配游戏开发需求:
Godot 2D物理系统架构:
├── 内置Box2D风格的物理引擎(无需安装额外插件)
├── 支持四种物理体:刚体、静态体、角色体、区域
├── 自动碰撞检测与响应(引擎自动处理物体间的碰撞)
├── 关节约束系统(可以将多个物体连接在一起,如绳索、铰链)
├── 物理材质系统(控制摩擦力和弹性,模拟不同表面特性)
└── 物理服务器API(底层接口,高级用户可直接访问)
为什么需要物理引擎? 想象一下,如果没有物理引擎,你需要手动计算:物体在重力作用下的加速度、两个物体碰撞后的反弹方向和速度、物体在斜坡上滑动时的摩擦力、复杂形状物体之间的碰撞检测...这些计算非常复杂且容易出错。物理引擎帮我们封装了所有这些计算,让开发变得简单高效。
17.1.2 物理节点类型
Godot提供了几种不同用途的物理节点,它们都继承自CollisionObject2D基类。理解每种节点的特点和适用场景是使用物理引擎的第一步:
# 物理体节点继承层级图(理解这个层级关系很重要)
#
# CollisionObject2D(碰撞对象基类)
# ├── PhysicsBody2D(物理体基类,不直接使用)
# │ ├── StaticBody2D → 静态物体,永不移动(地面、墙壁)
# │ ├── RigidBody2D → 刚体,完全受物理引擎控制(箱子、石头)
# │ ├── CharacterBody2D → 角色体,用代码控制移动(玩家、NPC)
# │ └── AnimatableBody2D → 可动画静态体(移动平台、电梯)
# └── Area2D → 区域检测,不发生碰撞(触发器、拾取物)
如何选择正确的物理节点? 这是初学者常见的困惑,下面的表格可以帮助你快速做出决定:
| 使用场景 | 推荐节点 | 原因说明 |
|---|---|---|
| 玩家角色 | CharacterBody2D | 需要精确控制移动,同时检测碰撞 |
| 可推动的箱子 | RigidBody2D | 需要响应物理力,自动计算碰撞反应 |
| 地面和墙壁 | StaticBody2D | 永远不会移动,只作为碰撞对象 |
| 金币拾取 | Area2D | 只需检测玩家是否接触,不需要物理碰撞 |
| 移动平台 | AnimatableBody2D | 需要移动并能推动玩家 |
| 伤害区域 | Area2D | 检测进入区域的物体,而不阻挡它们 |
17.1.3 物理帧与physicsprocess
在Godot中,有两种主要的处理函数:process()</code>和<code>physics_process()。理解它们的区别对于正确使用物理引擎至关重要。
两种处理函数的区别:
_process(delta):每一帧渲染时调用,调用频率取决于帧率(可能是30fps、60fps甚至更高)physicsprocess(delta):以固定间隔调用(默认每秒60次),与渲染帧率无关
为什么物理计算要使用固定间隔? 因为物理模拟需要稳定性。如果帧率波动,使用_process()会导致物理行为不一致——帧率高时物体移动慢,帧率低时物体移动快。使用固定的物理帧率可以确保游戏在不同性能的设备上表现一致。
extends Node2D
## 物理处理函数 - 以固定频率执行(默认每秒60次)
## 这是所有物理相关代码应该放置的地方
func _physics_process(delta: float) -> void:
# delta参数:距离上一次物理帧的时间间隔
# 在默认60fps的物理帧率下,delta约为0.0167秒(1/60)
# 这个值是固定的,不会像_process中那样波动
# 适合放在这里的代码:
# - 角色移动和跳跃逻辑
# - 力的施加和速度计算
# - 碰撞检测和响应
# - 射线检测
pass
## 普通处理函数 - 每渲染帧执行一次
func _process(delta: float) -> void:
# 这里的delta会随帧率变化
# 适合放在这里的代码:
# - 动画播放和视觉效果
# - UI更新
# - 输入响应(非移动相关)
# - 声音播放
pass
# 【调整物理帧率的方法】
# 路径:项目 → 项目设置 → Physics → Common → Physics Ticks Per Second
# 默认值:60(即每秒60次物理更新)
# 提高此值可获得更精确的物理模拟,但会增加CPU负担
实践建议: 将所有移动和物理相关的代码放在physicsprocess()中是一个好习惯,这样可以确保游戏在不同性能的设备上表现一致。
17.2 StaticBody2D(静态体)
StaticBody2D是最简单的物理体节点,它代表不会移动的物体。在游戏中,地面、墙壁、平台、障碍物等固定不动的元素都应该使用StaticBody2D。
StaticBody2D的特点:
- 不会被物理引擎移动(即使被其他物体撞击)
- 不响应重力和其他力
- 其他物体会与它碰撞并反弹
- 计算成本低,适合大量使用
17.2.1 静态物体基础
# 静态物体示例:地面或墙壁
# StaticBody2D本身不会移动,但可以设置物理材质来影响碰撞效果
extends StaticBody2D
func _ready():
# ═══ 碰撞层设置 ═══
# collision_layer: 这个物体存在于哪些层
# collision_mask: 这个物体会与哪些层发生碰撞
# 层是使用位运算的,第1层=1,第2层=2,第3层=4...
collision_layer = 1 # 物体在第1层
collision_mask = 1 # 检测第1层的碰撞
# ═══ 物理材质设置 ═══
# 创建并配置物理材质,控制碰撞时的行为
physics_material_override = PhysicsMaterial.new()
# friction(摩擦力): 0.0-1.0
# 0.0 = 没有摩擦(像冰面)
# 1.0 = 最大摩擦(物体会快速停下)
physics_material_override.friction = 0.5
# bounce(弹性/反弹系数): 0.0-1.0
# 0.0 = 不反弹
# 1.0 = 完全反弹(理论上物体会弹回原来的高度)
physics_material_override.bounce = 0.2
# ═══ 常量速度设置 ═══
# StaticBody2D可以设置“常量速度”,这不会移动物体本身,
# 但会影响碰撞到它的其他物体(模拟传送带效果)
func setup_moving_platform():
# 水平向右移动的传送带效果
constant_linear_velocity = Vector2(100, 0)
# 旋转速度(弧度/秒)
constant_angular_velocity = 0.5 # 约每秒转29度
重要说明: constantlinearvelocity和constantangularvelocity不会让StaticBody2D本身移动,它们只是影响碰撞到它的物体。如果你需要一个真正移动的平台,应该使用AnimatableBody2D。
17.2.2 单向碰撞平台
在平台跳跃游戏中,经常需要角色可以从下方穿过平台跳上去,但不会从上方穿过。这就是“单向碰撞”的应用场景。
# 单向碰撞平台:可以从下方穿过,但不能从上方穿过
# 常见于平台跳跃游戏中的“跳跃平台”
extends StaticBody2D
func _ready():
# 单向碰撞需要在CollisionShape2D子节点上设置,而不是在StaticBody2D上
var collision_shape = $CollisionShape2D
# 启用单向碰撞
# 当设为true时,物体只会与从上方接近的对象发生碰撞
collision_shape.one_way_collision = true
# 单向碰撞边距(像素)
# 这个值决定了“单向”的容差范围
# 设置太小可能导致角色卡在平台边缘
collision_shape.one_way_collision_margin = 16
如何让角色从单向平台上下落? 在某些游戏中,玩家可以按下键+跳跃来从单向平台上下落。实现方法是临时禁用平台的碰撞:
# 在玩家脚本中实现下落功能
extends CharacterBody2D
func _physics_process(delta):
# 当玩家按下下+跳跃时,下落穿过平台
if Input.is_action_pressed("move_down") and Input.is_action_just_pressed("jump"):
if is_on_floor():
# 获取当前站的平台
var collision = get_last_slide_collision()
if collision:
var platform = collision.get_collider()
# 临时禁用碰撞,0.2秒后恢复
_temporarily_disable_collision(platform, 0.2)
func _temporarily_disable_collision(body: Node2D, duration: float):
# 添加到排除列表,临时不与该物体碰撞
add_collision_exception_with(body)
# 创建计时器恢复碰撞
await get_tree().create_timer(duration).timeout
remove_collision_exception_with(body)
17.3 RigidBody2D(刚体)
RigidBody2D是完全受物理引擎控制的物体。它会响应重力、碰撞、摩擦力等物理效果,你只需要设置它的属性,物理引擎会自动计算它的运动。
RigidBody2D的适用场景:
- 可推动的箱子和物品
- 掉落的石头、破碎的碎片
- 弹球、弹弓物理
- 满足物理规律的招弹
- 纸片、布料等需要自然运动的物体
注意: RigidBody2D不适合用于玩家角色控制,因为它的运动由物理引擎决定,你无法精确控制。玩家角色应使用CharacterBody2D。
17.3.1 刚体基础属性
下面详细介绍刚体的各项属性,理解这些属性对于创建真实的物理效果至关重要:
extends RigidBody2D
func _ready():
# ═══ 质量与惯性 ═══
# mass(质量): 影响物体受力后的加速度
# F = ma,质量越大,同样的力产生的加速度越小
mass = 1.0 # 默认1kg
# inertia(转动惯量): 影响物体旋转的难易程度
# 设为0表示自动根据形状计算
inertia = 0.0
# center_of_mass(质心): 物体的质量中心位置
# 偶心的质心会导致物体旋转
center_of_mass = Vector2.ZERO # 默认在中心
# ═══ 物理材质 ═══
physics_material_override = PhysicsMaterial.new()
physics_material_override.friction = 0.5 # 摩擦力
physics_material_override.bounce = 0.3 # 弹性
# rough(粗糙模式): 两个物体碰撞时如何计算摩擦力
# false: 取平均值 | true: 取最大值
physics_material_override.rough = false
# ═══ 阻尼(物体减速) ═══
# linear_damp(线性阻尼): 物体移动时的减速率
# 0 = 没有阻尼(物体会一直加速)
# 越高 = 物体越快停下来(像在水中移动)
linear_damp = 0.0
# angular_damp(角度阻尼): 物体旋转时的减速率
angular_damp = 1.0
# ═══ 重力设置 ═══
# gravity_scale(重力缩放): 调整该物体受重力影响的程度
# 1.0 = 正常重力
# 0.0 = 无重力(漂浮在空中)
# 负数 = 反向重力(会往上飘)
# 2.0 = 双倍重力(下落更快)
gravity_scale = 1.0
# ═══ 睡眠设置(性能优化) ═══
# 当物体长时间不移动时,物理引擎会让它“睡眠”以节省计算资源
can_sleep = true # 允许睡眠
sleeping = false # 当前是否睡眠
小贴士: 调试物理参数时,可以使用@export将参数暴露到编辑器,方便实时调整。
17.3.2 刚体模式与冻结
RigidBody2D可以在运行时“冻结”,冻结后的刚体会暂时停止响应物理。这在某些游戏机制中很有用,比如暂停游戏或“时间冻结”技能。
extends RigidBody2D
func _ready():
# ═══ 冻结设置 ═══
# freeze: 是否冻结物体
# 冻结后物体不再响应物理,但仍然可以被其他物体碰撞
freeze = false
# freeze_mode: 冻结时的行为模式
# FREEZE_MODE_STATIC: 冻结时表现为静态体(其他物体会反弹)
# FREEZE_MODE_KINEMATIC: 冻结时表现为运动体(可以推动其他物体)
freeze_mode = RigidBody2D.FREEZE_MODE_STATIC
## 常见配置场景
# 场景1:正常的动态刚体(默认状态)
func setup_dynamic_body():
freeze = false # 不冻结,正常响应物理
gravity_scale = 1.0 # 正常重力
# 场景2:临时冻结物体(比如时间暂停技能)
func freeze_in_place():
freeze = true
freeze_mode = RigidBody2D.FREEZE_MODE_STATIC
# 场景3:解除冻结
func unfreeze():
freeze = false
# 可选:给物体一个初始速度
linear_velocity = Vector2(100, -200)
实用示例: 在解谜游戏中,可以先让物体冻结,等玩家解开机关后再解除冻结,让物体落下。
17.3.3 施加力与冲量
要让刚体移动,我们需要向它施加力或冲量。理解“力”和“冲量”的区别非常重要:
- 力(Force):持续作用,每一帧都施加,物体会逐渐加速
- 冲量(Impulse):瞬时作用,只施加一次,物体立即获得速度
比喻:力像你一直推购物车,冲量像你用力踢了一脚。
extends RigidBody2D
func apply_forces():
# ═══ 施加力(持续作用) ═══
# apply_central_force: 在质心施加力
# 需要在_physics_process中每帧调用才能看到持续加速效果
apply_central_force(Vector2(100, 0)) # 向右施加100N的力
# ═══ 施加冲量(瞬时作用) ═══
# apply_central_impulse: 在质心施加冲量
# 只需调用一次,物体就会获得速度
apply_central_impulse(Vector2(0, -500)) # 向上的冲量,实现跳跃效果
# ═══ 在特定点施加力(会产生扭矩/旋转) ═══
# 如果施力点不在质心,物体会边移动边旋转
# 第一个参数:力的大小和方向
# 第二个参数:施力点相对于质心的偏移
var force_offset = Vector2(10, 0) # 在右侧10像素处施力
apply_force(Vector2(0, -100), force_offset)
# ═══ 施加扭矩(纯旋转力) ═══
apply_torque(100) # 持续旋转力
apply_torque_impulse(50) # 瞬时旋转冲量
# ═══ 直接设置速度(不推荐,但有时有用) ═══
# 注意:直接设置速度会绕过物理计算,可能导致不自然的行为
linear_velocity = Vector2(200, -300) # 直接设置线性速度
angular_velocity = PI # 每秒转180度
## 高级:使用_integrate_forces进行更精确的物理控制
## 这个函数在物理引擎计算每一步时调用
func _integrate_forces(state: PhysicsDirectBodyState2D):
# state参数提供了物理状态的直接访问
# 获取当前状态
var current_velocity = state.linear_velocity
var current_transform = state.transform
# 例子:限制最大水平速度
var max_speed = 500.0
if abs(current_velocity.x) > max_speed:
state.linear_velocity.x = sign(current_velocity.x) * max_speed
# 在这里施加力效果更精确
state.apply_central_impulse(Vector2(10, 0))
何时使用<code>integrateforces</code>? 当你需要在物理计算过程中进行干预时,比如限制速度、修正位置等。普通情况下使用applycentralimpulse就足够了。
17.3.4 刚体实例:可推动箱子
下面是一个实用的例子:创建一个可以被玩家推动的箱子。这种箱子在解谜和平台游戏中很常见。
# pushable_box.gd
# 可推动的箱子 - 适合解谜游戏中的机关
extends RigidBody2D
## 可在编辑器中调整的参数
@export var max_push_force: float = 200.0 # 最大推力
@export var push_resistance: float = 0.3 # 推动阻力
func _ready():
# 质量较大,让箱子有“沉重感”
mass = 2.0
# 高线性阻尼,推动后会快速停下
# 这样箱子不会一直滑动,更适合解谜游戏
linear_damp = 3.0
# 高摩擦力,让箱子不容易意外滑动
physics_material_override = PhysicsMaterial.new()
physics_material_override.friction = 0.8
# 禁用旋转,让箱子始终保持正
# 如果希望箱子可以翻滚,就不要设置这个
lock_rotation = true
## 使用_integrate_forces限制箱子的最大速度
func _integrate_forces(state: PhysicsDirectBodyState2D):
var max_speed = 100.0 # 最大移动速度
# 限制水平速度,防止箱子被推得太快
if abs(state.linear_velocity.x) > max_speed:
state.linear_velocity.x = sign(state.linear_velocity.x) * max_speed
# 限制垂直速度(可选,防止掉落太快)
if state.linear_velocity.y > max_speed * 2:
state.linear_velocity.y = max_speed * 2
设计思考: 这个箱子使用了高阻尼和高摩擦,这样玩家推动后箱子会快速停下,让解谜更有可控性。如果你想要“滑滑的”箱子(比如冰块),可以降低阻尼和摩擦力。
17.4 CharacterBody2D
17.4.1 角色体基础
extends CharacterBody2D
@export var speed: float = 300.0
@export var jump_velocity: float = -400.0
@export var gravity: float = 980.0
func _physics_process(delta: float) -> void:
# 应用重力
if not is_on_floor():
velocity.y += gravity * delta
# 跳跃
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
# 水平移动
var direction = Input.get_axis("move_left", "move_right")
if direction:
velocity.x = direction * speed
else:
velocity.x = move_toward(velocity.x, 0, speed)
# 移动并处理碰撞
move_and_slide()
17.4.2 移动参数
extends CharacterBody2D
func _ready():
# 移动模式
motion_mode = MotionMode.MOTION_MODE_GROUNDED # 地面模式
# MOTION_MODE_FLOATING - 浮动模式(俯视角)
# 地面检测
up_direction = Vector2.UP
floor_stop_on_slope = true # 斜坡上停止
floor_constant_speed = true # 斜坡上保持速度
floor_block_on_wall = true # 墙壁阻挡
floor_max_angle = deg_to_rad(45) # 最大斜坡角度
floor_snap_length = 4.0 # 地面吸附长度
# 平台检测
platform_on_leave = PLATFORM_ON_LEAVE_ADD_VELOCITY
# PLATFORM_ON_LEAVE_ADD_VELOCITY - 添加平台速度
# PLATFORM_ON_LEAVE_ADD_UPWARD_VELOCITY - 只添加向上速度
# PLATFORM_ON_LEAVE_DO_NOTHING - 不添加速度
# 墙壁检测
wall_min_slide_angle = deg_to_rad(15)
# 碰撞
slide_on_ceiling = true
max_slides = 6
17.4.3 碰撞状态检测
extends CharacterBody2D
func _physics_process(delta: float) -> void:
move_and_slide()
# 检测碰撞状态
if is_on_floor():
print("在地面上")
if is_on_wall():
print("碰到墙壁")
if is_on_ceiling():
print("碰到天花板")
# 获取碰撞信息
for i in get_slide_collision_count():
var collision = get_slide_collision(i)
print("碰撞对象:", collision.get_collider())
print("碰撞点:", collision.get_position())
print("碰撞法线:", collision.get_normal())
print("碰撞深度:", collision.get_depth())
# 获取地面法线
if is_on_floor():
var floor_normal = get_floor_normal()
var floor_angle = rad_to_deg(floor_normal.angle_to(Vector2.UP))
17.4.4 完整角色控制器
# player_controller.gd
extends CharacterBody2D
# 移动参数
@export_group("Movement")
@export var max_speed: float = 300.0
@export var acceleration: float = 2000.0
@export var friction: float = 1500.0
@export var air_resistance: float = 200.0
# 跳跃参数
@export_group("Jump")
@export var jump_velocity: float = -450.0
@export var jump_cut_multiplier: float = 0.5
@export var coyote_time: float = 0.1
@export var jump_buffer_time: float = 0.1
# 重力
@export_group("Gravity")
@export var gravity: float = 980.0
@export var max_fall_speed: float = 600.0
# 状态
var coyote_timer: float = 0.0
var jump_buffer_timer: float = 0.0
var was_on_floor: bool = false
func _physics_process(delta: float) -> void:
_apply_gravity(delta)
_handle_jump(delta)
_handle_movement(delta)
was_on_floor = is_on_floor()
move_and_slide()
func _apply_gravity(delta: float) -> void:
if not is_on_floor():
velocity.y = min(velocity.y + gravity * delta, max_fall_speed)
func _handle_jump(delta: float) -> void:
# 土狼时间
if is_on_floor():
coyote_timer = coyote_time
else:
coyote_timer = max(0, coyote_timer - delta)
# 跳跃缓冲
if Input.is_action_just_pressed("jump"):
jump_buffer_timer = jump_buffer_time
else:
jump_buffer_timer = max(0, jump_buffer_timer - delta)
# 执行跳跃
if jump_buffer_timer > 0 and coyote_timer > 0:
velocity.y = jump_velocity
jump_buffer_timer = 0
coyote_timer = 0
# 跳跃高度控制(提前松开跳跃键)
if Input.is_action_just_released("jump") and velocity.y < 0:
velocity.y *= jump_cut_multiplier
func _handle_movement(delta: float) -> void:
var input_dir = Input.get_axis("move_left", "move_right")
if input_dir != 0:
# 加速
velocity.x = move_toward(velocity.x, input_dir * max_speed, acceleration * delta)
else:
# 减速
var decel = friction if is_on_floor() else air_resistance
velocity.x = move_toward(velocity.x, 0, decel * delta)
17.5 Area2D
17.5.1 区域基础
extends Area2D
func _ready():
# 监听设置
monitoring = true # 检测其他物体
monitorable = true # 被其他区域检测
# 物理属性覆盖
gravity_space_override = SpaceOverride.SPACE_OVERRIDE_REPLACE
gravity_direction = Vector2.DOWN
gravity = 980.0
linear_damp_space_override = SpaceOverride.SPACE_OVERRIDE_COMBINE
linear_damp = 0.5
# 优先级(重叠区域)
priority = 0
# 连接信号
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
area_entered.connect(_on_area_entered)
area_exited.connect(_on_area_exited)
func _on_body_entered(body: Node2D):
print("物体进入:", body.name)
func _on_body_exited(body: Node2D):
print("物体离开:", body.name)
17.5.2 区域应用实例
# 伤害区域
extends Area2D
@export var damage: int = 10
@export var damage_interval: float = 0.5
var bodies_in_area: Array[Node2D] = []
var damage_timer: float = 0.0
func _ready():
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _physics_process(delta: float):
damage_timer -= delta
if damage_timer <= 0:
damage_timer = damage_interval
for body in bodies_in_area:
if body.has_method("take_damage"):
body.take_damage(damage)
func _on_body_entered(body: Node2D):
if body.has_method("take_damage"):
bodies_in_area.append(body)
body.take_damage(damage) # 立即造成伤害
func _on_body_exited(body: Node2D):
bodies_in_area.erase(body)
# 拾取物品
extends Area2D
signal collected
@export var item_type: String = "coin"
@export var value: int = 1
func _ready():
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D):
if body.is_in_group("player"):
collected.emit()
if body.has_method("collect_item"):
body.collect_item(item_type, value)
queue_free()
17.6 物理材质
17.6.1 PhysicsMaterial
extends RigidBody2D
func setup_physics_material():
var material = PhysicsMaterial.new()
# 摩擦力 (0-1)
material.friction = 0.5
# 弹性 (0-1)
material.bounce = 0.3
# 粗糙模式
material.rough = false
# true: 使用两个物体摩擦力的最大值
# false: 使用平均值
# 吸收模式
material.absorbent = false
# true: 使用两个物体弹性的最小值
# false: 使用平均值
physics_material_override = material
17.6.2 不同材质效果
# 冰面(低摩擦,无弹性)
func create_ice_material() -> PhysicsMaterial:
var mat = PhysicsMaterial.new()
mat.friction = 0.05
mat.bounce = 0.0
return mat
# 橡胶(高摩擦,高弹性)
func create_rubber_material() -> PhysicsMaterial:
var mat = PhysicsMaterial.new()
mat.friction = 0.9
mat.bounce = 0.8
return mat
# 金属(中摩擦,低弹性)
func create_metal_material() -> PhysicsMaterial:
var mat = PhysicsMaterial.new()
mat.friction = 0.4
mat.bounce = 0.1
return mat
17.7 物理查询
17.7.1 射线检测
extends Node2D
func raycast_example():
var space = get_world_2d().direct_space_state
# 创建查询参数
var query = PhysicsRayQueryParameters2D.create(
global_position, # 起点
global_position + Vector2(100, 0), # 终点
1, # 碰撞掩码
[self] # 排除的对象
)
# 可选参数
query.collide_with_bodies = true
query.collide_with_areas = false
query.hit_from_inside = false
# 执行射线检测
var result = space.intersect_ray(query)
if not result.is_empty():
var hit_point = result.position
var hit_normal = result.normal
var hit_collider = result.collider
var hit_shape = result.shape
print("命中:", hit_collider.name, " 位置:", hit_point)
17.7.2 形状查询
extends Node2D
func shape_query_example():
var space = get_world_2d().direct_space_state
# 创建形状
var shape = CircleShape2D.new()
shape.radius = 50.0
# 创建查询参数
var query = PhysicsShapeQueryParameters2D.new()
query.shape = shape
query.transform = global_transform
query.collision_mask = 1
query.exclude = [self]
# 执行形状检测
var results = space.intersect_shape(query, 10) # 最多返回10个结果
for result in results:
print("检测到:", result.collider.name)
func point_query_example():
var space = get_world_2d().direct_space_state
var query = PhysicsPointQueryParameters2D.new()
query.position = get_global_mouse_position()
query.collision_mask = 1
var results = space.intersect_point(query, 5)
for result in results:
print("点击了:", result.collider.name)
17.8 实际案例
17.8.1 物理弹弓
# slingshot.gd
extends Node2D
@export var projectile_scene: PackedScene
@export var max_pull_distance: float = 200.0
@export var force_multiplier: float = 10.0
var is_aiming: bool = false
var pull_start: Vector2
var current_pull: Vector2
func _unhandled_input(event: InputEvent):
if event.is_action_pressed("shoot"):
is_aiming = true
pull_start = get_global_mouse_position()
if event.is_action_released("shoot") and is_aiming:
fire()
is_aiming = false
func _process(delta: float):
if is_aiming:
current_pull = pull_start - get_global_mouse_position()
current_pull = current_pull.limit_length(max_pull_distance)
queue_redraw()
func fire():
if current_pull.length() < 10:
return
var projectile = projectile_scene.instantiate()
get_parent().add_child(projectile)
projectile.global_position = global_position
var force = current_pull * force_multiplier
projectile.apply_central_impulse(force)
func _draw():
if is_aiming:
draw_line(Vector2.ZERO, -current_pull, Color.RED, 3)
17.8.2 物理绳索
# rope.gd
extends Node2D
@export var segment_count: int = 10
@export var segment_length: float = 20.0
@export var segment_mass: float = 0.5
var segments: Array[RigidBody2D] = []
func _ready():
create_rope()
func create_rope():
var prev_body: PhysicsBody2D = null
for i in range(segment_count):
var segment = RigidBody2D.new()
segment.mass = segment_mass
segment.gravity_scale = 1.0
segment.position = Vector2(0, i * segment_length)
# 碰撞形状
var collision = CollisionShape2D.new()
var shape = CapsuleShape2D.new()
shape.radius = 3.0
shape.height = segment_length - 2
collision.shape = shape
segment.add_child(collision)
add_child(segment)
segments.append(segment)
# 创建关节
if prev_body:
var joint = PinJoint2D.new()
joint.node_a = prev_body.get_path()
joint.node_b = segment.get_path()
joint.position = Vector2(0, i * segment_length - segment_length / 2)
add_child(joint)
else:
# 第一段固定
segment.freeze = true
prev_body = segment
本章小结
本章全面学习了Godot的2D物理系统:
- 物理引擎概述:节点类型、物理帧
- StaticBody2D:静态物体、移动平台、单向碰撞
- RigidBody2D:刚体模式、施加力、integrateforces
- CharacterBody2D:移动参数、碰撞检测、完整控制器
- Area2D:区域检测、物理属性覆盖
- 物理材质:摩擦力、弹性、不同材质
- 物理查询:射线检测、形状查询
- 实际案例:物理弹弓、物理绳索
下一章将专门讲解碰撞检测系统。
上一章:2D变换与坐标系
下一章:碰撞检测