第十七章:2D物理系统

第十七章: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度

重要说明: constantlinearvelocityconstantangularvelocity不会让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物理系统:

  1. 物理引擎概述:节点类型、物理帧
  2. StaticBody2D:静态物体、移动平台、单向碰撞
  3. RigidBody2D:刚体模式、施加力、integrateforces
  4. CharacterBody2D:移动参数、碰撞检测、完整控制器
  5. Area2D:区域检测、物理属性覆盖
  6. 物理材质:摩擦力、弹性、不同材质
  7. 物理查询:射线检测、形状查询
  8. 实际案例:物理弹弓、物理绳索

下一章将专门讲解碰撞检测系统。


上一章:2D变换与坐标系

下一章:碰撞检测

← 返回目录