第十八章:碰撞检测
"精确的碰撞检测是游戏交互的基石。"
碰撞检测决定了游戏中物体如何相互作用。本章将深入讲解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的碰撞检测系统:
- 碰撞系统概述:检测流程、形状类型
- CollisionShape2D:创建形状、动态调整
- CollisionPolygon2D:多边形碰撞、从精灵生成
- 碰撞层与掩码:层系统、层操作、项目设置
- 碰撞检测方法:CharacterBody2D、Area2D、RigidBody2D
- 射线检测:RayCast2D、代码射线查询
- 形状检测:ShapeCast2D、代码形状查询
- 碰撞优化:性能策略、调试工具
- 实际案例:受击框系统、地形检测
下一章将学习2D动画系统。
上一章:2D物理系统
下一章:2D动画系统