第十八章:碰撞检测

第十八章:碰撞检测

"精确的碰撞检测是游戏交互的基石。"

碰撞检测决定了游戏中物体如何相互作用。本章将深入讲解Godot的碰撞系统,包括碰撞形状、碰撞层、碰撞响应等核心内容。


18.1 碰撞系统概述

18.1.1 碰撞检测流程

碰撞检测流程:
1. 宽相检测(Broad Phase)- 快速筛选可能碰撞的物体
2. 窄相检测(Narrow Phase)- 精确计算碰撞
3. 碰撞响应 - 处理碰撞结果

Godot碰撞组件:
├── CollisionShape2D - 定义碰撞形状
├── CollisionPolygon2D - 定义多边形碰撞
├── collision_layer - 物体所在层
├── collision_mask - 物体检测的层
└── 碰撞信号/回调

18.1.2 碰撞形状类型

# 基本形状
CircleShape2D        # 圆形 - 最快
RectangleShape2D     # 矩形 - 常用
CapsuleShape2D       # 胶囊体 - 角色常用
SegmentShape2D       # 线段
SeparationRayShape2D # 分离射线

# 复杂形状
ConvexPolygonShape2D # 凸多边形
ConcavePolygonShape2D # 凹多边形(仅静态体)
WorldBoundaryShape2D  # 世界边界(无限平面)

18.2 CollisionShape2D

18.2.1 创建碰撞形状

extends CharacterBody2D

func _ready():
    # 代码创建碰撞形状
    var collision = CollisionShape2D.new()
    
    # 圆形
    var circle = CircleShape2D.new()
    circle.radius = 32.0
    
    # 矩形
    var rect = RectangleShape2D.new()
    rect.size = Vector2(64, 64)
    
    # 胶囊体
    var capsule = CapsuleShape2D.new()
    capsule.radius = 16.0
    capsule.height = 48.0
    
    collision.shape = capsule
    add_child(collision)

18.2.2 碰撞形状属性

extends CollisionShape2D

func _ready():
    # 禁用碰撞
    disabled = false
    
    # 单向碰撞
    one_way_collision = true
    one_way_collision_margin = 16.0
    
    # 仅调试显示
    debug_color = Color(0, 1, 0, 0.5)

18.2.3 动态调整碰撞形状

extends CharacterBody2D

@onready var standing_collision: CollisionShape2D = $StandingCollision
@onready var crouching_collision: CollisionShape2D = $CrouchingCollision

var is_crouching: bool = false

func set_crouch(value: bool):
    is_crouching = value
    standing_collision.disabled = is_crouching
    crouching_collision.disabled = not is_crouching

# 运行时修改形状大小
func resize_collision(new_radius: float):
    var shape = $CollisionShape2D.shape as CircleShape2D
    shape.radius = new_radius

18.3 CollisionPolygon2D

18.3.1 多边形碰撞

extends StaticBody2D

func _ready():
    var polygon = CollisionPolygon2D.new()
    
    # 设置顶点(顺时针或逆时针)
    polygon.polygon = PackedVector2Array([
        Vector2(-32, -32),
        Vector2(32, -32),
        Vector2(32, 32),
        Vector2(-32, 32)
    ])
    
    # 构建模式
    polygon.build_mode = CollisionPolygon2D.BUILD_SOLIDS  # 实心
    # BUILD_SEGMENTS - 只有边缘
    
    add_child(polygon)

18.3.2 从精灵生成碰撞

# 在编辑器中:
# 1. 选择Sprite2D
# 2. 工具栏 → Sprite2D → Create CollisionPolygon2D Sibling

# 代码生成(简化版)
func generate_collision_from_sprite(sprite: Sprite2D) -> PackedVector2Array:
    var image = sprite.texture.get_image()
    var bitmap = BitMap.new()
    bitmap.create_from_image_alpha(image)
    
    var polygons = bitmap.opaque_to_polygons(Rect2(Vector2.ZERO, image.get_size()))
    if polygons.size() > 0:
        return polygons[0]
    return PackedVector2Array()

18.4 碰撞层与掩码

18.4.1 层系统基础

# 碰撞层系统(32层可用)
# collision_layer - 物体所在的层(我是什么)
# collision_mask - 物体检测的层(我能碰到什么)

# 示例层定义:
# 层1:玩家
# 层2:敌人
# 层3:地形
# 层4:玩家子弹
# 层5:敌人子弹
# 层6:可拾取物品

extends CharacterBody2D

func _ready():
    # 设置玩家
    collision_layer = 1  # 在层1
    collision_mask = 2 | 3 | 5 | 6  # 检测敌人、地形、敌人子弹、物品

# 使用位运算
func setup_layers():
    # 设置在多个层
    collision_layer = (1 << 0) | (1 << 2)  # 层1和层3
    
    # 检测特定层
    collision_mask = (1 << 1) | (1 << 2)   # 层2和层3

18.4.2 层操作方法

extends PhysicsBody2D

func layer_operations():
    # 设置特定层
    set_collision_layer_value(1, true)   # 启用层1
    set_collision_layer_value(2, false)  # 禁用层2
    
    # 检查层
    var is_on_layer1 = get_collision_layer_value(1)
    
    # 设置特定掩码
    set_collision_mask_value(1, true)
    set_collision_mask_value(3, true)
    
    # 检查掩码
    var detects_layer1 = get_collision_mask_value(1)

# 实用函数
func can_collide_with(other: PhysicsBody2D) -> bool:
    # 检查两个物体是否可能碰撞
    return (collision_mask & other.collision_layer) != 0 or \
           (other.collision_mask & collision_layer) != 0

18.4.3 项目层设置

在项目设置中定义层名称:
项目设置 → Layer Names → 2D Physics

Layer 1: player
Layer 2: enemy
Layer 3: terrain
Layer 4: player_projectile
Layer 5: enemy_projectile
Layer 6: pickup
Layer 7: trigger
Layer 8: destructible

18.5 碰撞检测方法

18.5.1 CharacterBody2D碰撞

extends CharacterBody2D

func _physics_process(delta: float):
    velocity.y += 980 * delta
    move_and_slide()
    
    # 检测所有碰撞
    for i in get_slide_collision_count():
        var collision = get_slide_collision(i)
        handle_collision(collision)

func handle_collision(collision: KinematicCollision2D):
    var collider = collision.get_collider()
    
    # 按类型处理
    if collider is RigidBody2D:
        # 推动刚体
        var push_force = -collision.get_normal() * 100
        collider.apply_central_impulse(push_force)
    
    if collider.is_in_group("enemy"):
        take_damage(10)
    
    if collider.is_in_group("destructible"):
        collider.destroy()

18.5.2 Area2D碰撞

extends Area2D

signal enemy_detected(enemy: Node2D)

func _ready():
    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):
    if body.is_in_group("enemy"):
        enemy_detected.emit(body)

func _on_body_exited(body: Node2D):
    pass

func _on_area_entered(area: Area2D):
    # 区域与区域的碰撞
    pass

func _on_area_exited(area: Area2D):
    pass

# 手动检测
func get_overlapping_bodies_of_type(type: String) -> Array:
    var result = []
    for body in get_overlapping_bodies():
        if body.is_in_group(type):
            result.append(body)
    return result

18.5.3 RigidBody2D碰撞

extends RigidBody2D

func _ready():
    # 启用碰撞监听
    contact_monitor = true
    max_contacts_reported = 4
    
    # 连接信号
    body_entered.connect(_on_body_entered)
    body_exited.connect(_on_body_exited)

func _on_body_entered(body: Node):
    print("碰撞:", body.name)
    
    # 获取碰撞速度
    var impact_velocity = linear_velocity.length()
    if impact_velocity > 500:
        # 高速碰撞效果
        spawn_impact_effect()

func _integrate_forces(state: PhysicsDirectBodyState2D):
    # 获取接触点信息
    for i in state.get_contact_count():
        var contact_pos = state.get_contact_local_position(i)
        var contact_normal = state.get_contact_local_normal(i)
        var collider = state.get_contact_collider_object(i)

18.6 射线检测

18.6.1 RayCast2D节点

extends CharacterBody2D

@onready var ground_ray: RayCast2D = $GroundRay
@onready var wall_ray: RayCast2D = $WallRay

func _ready():
    # 射线设置
    ground_ray.target_position = Vector2(0, 50)
    ground_ray.collision_mask = 1  # 检测地形
    ground_ray.enabled = true
    ground_ray.exclude_parent = true
    
    # 碰撞异常
    ground_ray.add_exception(self)

func _physics_process(delta: float):
    # 检测地面
    if ground_ray.is_colliding():
        var ground = ground_ray.get_collider()
        var collision_point = ground_ray.get_collision_point()
        var collision_normal = ground_ray.get_collision_normal()
        
        print("地面类型:", ground.name)

18.6.2 代码射线查询

extends Node2D

func cast_ray(from: Vector2, to: Vector2) -> Dictionary:
    var space = get_world_2d().direct_space_state
    
    var query = PhysicsRayQueryParameters2D.new()
    query.from = from
    query.to = to
    query.collision_mask = 0xFFFFFFFF  # 检测所有层
    query.exclude = []
    query.collide_with_bodies = true
    query.collide_with_areas = false
    query.hit_from_inside = false
    
    return space.intersect_ray(query)

func check_line_of_sight(from: Node2D, to: Node2D) -> bool:
    var result = cast_ray(from.global_position, to.global_position)
    
    if result.is_empty():
        return true  # 没有障碍物
    
    return result.collider == to  # 直接看到目标

# 扇形射线检测
func cast_cone(origin: Vector2, direction: Vector2, angle: float, count: int, distance: float) -> Array:
    var results = []
    var start_angle = direction.angle() - angle / 2
    var angle_step = angle / (count - 1)
    
    for i in range(count):
        var ray_angle = start_angle + angle_step * i
        var ray_dir = Vector2.from_angle(ray_angle)
        var result = cast_ray(origin, origin + ray_dir * distance)
        if not result.is_empty():
            results.append(result)
    
    return results

18.7 形状检测

18.7.1 ShapeCast2D节点

extends CharacterBody2D

@onready var shape_cast: ShapeCast2D = $ShapeCast2D

func _ready():
    # 设置形状
    var shape = CircleShape2D.new()
    shape.radius = 30.0
    shape_cast.shape = shape
    
    # 设置目标
    shape_cast.target_position = Vector2(0, 100)
    shape_cast.collision_mask = 1
    shape_cast.max_results = 10

func _physics_process(delta: float):
    if shape_cast.is_colliding():
        var collision_count = shape_cast.get_collision_count()
        
        for i in range(collision_count):
            var collider = shape_cast.get_collider(i)
            var point = shape_cast.get_collision_point(i)
            var normal = shape_cast.get_collision_normal(i)

18.7.2 代码形状查询

extends Node2D

func query_circle(center: Vector2, radius: float) -> Array:
    var space = get_world_2d().direct_space_state
    
    var shape = CircleShape2D.new()
    shape.radius = radius
    
    var query = PhysicsShapeQueryParameters2D.new()
    query.shape = shape
    query.transform = Transform2D(0, center)
    query.collision_mask = 0xFFFFFFFF
    
    return space.intersect_shape(query, 32)

func query_rect(rect: Rect2) -> Array:
    var space = get_world_2d().direct_space_state
    
    var shape = RectangleShape2D.new()
    shape.size = rect.size
    
    var query = PhysicsShapeQueryParameters2D.new()
    query.shape = shape
    query.transform = Transform2D(0, rect.get_center())
    
    return space.intersect_shape(query, 32)

# 获取范围内的敌人
func get_enemies_in_range(center: Vector2, radius: float) -> Array[Node2D]:
    var results: Array[Node2D] = []
    var hits = query_circle(center, radius)
    
    for hit in hits:
        var collider = hit.collider
        if collider.is_in_group("enemy"):
            results.append(collider)
    
    return results

18.8 碰撞优化

18.8.1 性能优化策略

# 1. 使用简单形状
# 圆形 > 矩形 > 胶囊 > 凸多边形 > 凹多边形

# 2. 合理使用碰撞层
# 减少不必要的碰撞检测

# 3. 禁用不需要的碰撞
func toggle_collision(enabled: bool):
    $CollisionShape2D.disabled = not enabled

# 4. 使用Area2D预筛选
extends Area2D

var potential_targets: Array[Node2D] = []

func _ready():
    body_entered.connect(func(body): potential_targets.append(body))
    body_exited.connect(func(body): potential_targets.erase(body))

# 5. 分帧处理
var check_index: int = 0

func _physics_process(delta):
    if potential_targets.size() > 0:
        check_index = (check_index + 1) % potential_targets.size()
        var target = potential_targets[check_index]
        # 只检测一个目标
        process_target(target)

18.8.2 碰撞调试

# 在项目设置中启用碰撞调试
# Debug → Visible Collision Shapes

# 自定义调试绘制
extends Node2D

@export var show_debug: bool = true

func _draw():
    if not show_debug:
        return
    
    # 绘制碰撞范围
    draw_circle(Vector2.ZERO, 50, Color(1, 0, 0, 0.3))
    
    # 绘制射线
    draw_line(Vector2.ZERO, Vector2(100, 0), Color.GREEN, 2)

func _process(delta):
    if show_debug:
        queue_redraw()

18.9 实际案例

18.9.1 受击框/攻击框系统

# hitbox.gd
class_name Hitbox
extends Area2D

signal hit(hurtbox: Hurtbox)

@export var damage: int = 10
@export var knockback_force: float = 200.0
@export var knockback_direction: Vector2 = Vector2.RIGHT

var _owner_node: Node2D

func _ready():
    collision_layer = 0
    collision_mask = 4  # 检测Hurtbox层
    area_entered.connect(_on_area_entered)

func setup(owner: Node2D):
    _owner_node = owner

func _on_area_entered(area: Area2D):
    if area is Hurtbox and area.owner_node != _owner_node:
        hit.emit(area)
        area.receive_hit(self)

# hurtbox.gd
class_name Hurtbox
extends Area2D

signal hurt(hitbox: Hitbox)

var owner_node: Node2D
var invincible: bool = false

func _ready():
    collision_layer = 4  # Hurtbox层
    collision_mask = 0

func setup(owner: Node2D):
    owner_node = owner

func receive_hit(hitbox: Hitbox):
    if invincible:
        return
    
    hurt.emit(hitbox)
    
    if owner_node.has_method("take_damage"):
        var direction = (owner_node.global_position - hitbox.global_position).normalized()
        owner_node.take_damage(hitbox.damage, direction * hitbox.knockback_force)

18.9.2 地形检测系统

# terrain_detector.gd
extends Node2D

@onready var ground_left: RayCast2D = $GroundLeft
@onready var ground_right: RayCast2D = $GroundRight
@onready var wall_left: RayCast2D = $WallLeft
@onready var wall_right: RayCast2D = $WallRight
@onready var ceiling: RayCast2D = $Ceiling
@onready var ledge_left: RayCast2D = $LedgeLeft
@onready var ledge_right: RayCast2D = $LedgeRight

func is_on_ground() -> bool:
    return ground_left.is_colliding() or ground_right.is_colliding()

func is_touching_wall() -> int:
    if wall_left.is_colliding():
        return -1
    if wall_right.is_colliding():
        return 1
    return 0

func is_touching_ceiling() -> bool:
    return ceiling.is_colliding()

func can_grab_ledge(direction: int) -> bool:
    if direction < 0:
        return wall_left.is_colliding() and not ledge_left.is_colliding()
    elif direction > 0:
        return wall_right.is_colliding() and not ledge_right.is_colliding()
    return false

func get_ground_normal() -> Vector2:
    if ground_left.is_colliding():
        return ground_left.get_collision_normal()
    if ground_right.is_colliding():
        return ground_right.get_collision_normal()
    return Vector2.UP

本章小结

本章深入学习了Godot的碰撞检测系统:

  1. 碰撞系统概述:检测流程、形状类型
  2. CollisionShape2D:创建形状、动态调整
  3. CollisionPolygon2D:多边形碰撞、从精灵生成
  4. 碰撞层与掩码:层系统、层操作、项目设置
  5. 碰撞检测方法:CharacterBody2D、Area2D、RigidBody2D
  6. 射线检测:RayCast2D、代码射线查询
  7. 形状检测:ShapeCast2D、代码形状查询
  8. 碰撞优化:性能策略、调试工具
  9. 实际案例:受击框系统、地形检测

下一章将学习2D动画系统。


上一章:2D物理系统

下一章:2D动画系统

← 返回目录