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

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_enteredbody: Node物理体进入区域
body_exitedbody: Node物理体离开区域
area_enteredarea: Area2D另一个 Area2D 进入
area_exitedarea: Area2D另一个 Area2D 离开
body_shape_enteredbody_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(掩码)“我检测谁” - 物体会与哪些层发生碰撞

推荐层设置

层号名称说明
1World静态世界(墙壁、地面)
2Player玩家
3Enemy敌人
4Projectile子弹/投射物
5Pickup拾取物
6Trigger触发器
7Platform单向平台

碰撞矩阵示例

物体         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

常见材质配置

材质bouncefriction适用场景
橡胶0.80.9弹力球
冰面0.10.05滑冰关卡
泥地0.01.0沼泽地形
金属0.30.3金属弹球
超级弹力1.00.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()

扩展阅读