强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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()

扩展阅读