引言:碰撞检测在游戏开发中的核心地位
在游戏开发中,角色碰撞检测与交互是构建沉浸式体验的基础技术。无论是2D平台跳跃游戏中的角色与平台碰撞,还是3D开放世界中的复杂物理交互,碰撞系统都扮演着至关重要的角色。根据2023年游戏开发者大会(GDC)的统计,超过65%的物理相关bug源于碰撞检测实现不当,而优化的碰撞系统可以提升游戏性能达30%以上。
碰撞检测的核心挑战在于平衡精确性与性能。过于精确的检测(如像素级)会消耗大量计算资源,而过于粗略的检测则会导致不真实的交互体验。现代游戏引擎(如Unity、Unreal Engine)提供了内置的碰撞系统,但理解底层原理对于解决复杂问题和实现自定义需求至关重要。
本文将从基础概念出发,逐步深入到高级优化技术,涵盖2D和3D场景下的碰撞检测与交互实现。我们将使用JavaScript(Web游戏开发)和C#(Unity)作为示例语言,因为它们分别代表了轻量级和重量级游戏开发的典型场景。通过完整的代码示例和实际案例,读者将掌握从简单AABB检测到复杂物理交互的全流程解决方案。
基础概念:理解碰撞检测的核心原理
什么是碰撞检测?
碰撞检测(Collision Detection)是判断两个或多个游戏对象在空间中是否发生重叠或接触的过程。而碰撞响应(Collision Response)则是在检测到碰撞后,如何改变对象状态(如反弹、停止、触发事件)的机制。
在游戏循环中,碰撞检测通常发生在更新阶段:
// 伪代码:游戏循环中的碰撞检测
function gameLoop() {
updatePhysics(); // 更新物理状态
detectCollisions(); // 检测碰撞
resolveCollisions(); // 解决碰撞
render(); // 渲染画面
}
常见碰撞形状
为了高效检测,游戏对象通常被简化为几何形状:
AABB(Axis-Aligned Bounding Box):轴对齐包围盒,适用于2D和3D
- 优点:计算简单,速度快
- 缺点:无法旋转,不精确
圆形/球体:适用于角色、弹珠等圆形物体
- 优点:旋转不变性,距离计算快
- 缺点:无法精确匹配复杂形状
胶囊体:Unity中角色控制器的常用形状
- 优点:适合角色,支持高度和半径
- 缺点:计算相对复杂
凸多边形/凸多面体:精确但计算量大
- 优点:精确匹配
- 缺点:分离轴定理(SAT)计算复杂
碰撞检测的数学基础
距离检测(圆形碰撞):
// 两个圆形之间的距离检测
function checkCircleCollision(circle1, circle2) {
const dx = circle1.x - circle2.x;
const dy = circle1.y - circle2.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance < (circle1.radius + circle2.radius);
}
AABB检测:
// 两个AABB的碰撞检测
function checkAABBCollision(aabb1, aabb2) {
return aabb1.x < aabb2.x + aabb2.width &&
aabb1.x + aabb1.width > aabb2.x &&
aabb1.y < aabb2.y + aabb2.height &&
aabb1.y + aabb1.height > aabb2.y;
}
基础实现:从零构建2D碰撞系统
1. 创建游戏对象和碰撞器
让我们从一个简单的2D平台跳跃游戏开始,使用纯JavaScript实现。
// 游戏对象基类
class GameObject {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.vx = 0; // 速度X
this.vy = 0; // 速度Y
this.isStatic = false; // 是否静态(如地面)
}
// 获取AABB边界
getBounds() {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height
};
}
}
// 玩家类
class Player extends GameObject {
constructor(x, y) {
super(x, y, 32, 32);
this.speed = 5;
this.jumpForce = -12;
this.grounded = false;
}
update(keys) {
// 水平移动
if (keys['ArrowLeft']) this.vx = -this.speed;
else if (keys['ArrowRight']) this.vx = this.speed;
else this.vx = 0;
// 跳跃
if (keys['ArrowUp'] && this.grounded) {
this.vy = this.jumpForce;
this.grounded = false;
}
// 重力
this.vy += 0.5;
// 更新位置
this.x += this.vx;
this.y += this.vy;
}
}
// 平台类
class Platform extends GameObject {
constructor(x, y, width, height) {
super(x, y, width, height);
this.isStatic = true;
}
}
2. 碰撞检测系统
// 碰撞管理器
class CollisionManager {
constructor() {
this.objects = [];
}
addObject(obj) {
this.objects.push(obj);
}
// 检测所有碰撞
detectCollisions() {
for (let i = 0; i < this.objects.length; i++) {
for (let j = i + 1; j < this.objects.length; j++) {
const obj1 = this.objects[i];
const obj2 = this.objects[j];
// 跳过静态对象之间的检测
if (obj1.isStatic && obj2.isStatic) continue;
if (this.checkCollision(obj1, obj2)) {
this.resolveCollision(obj1, obj2);
}
}
}
}
// 通用碰撞检测
checkCollision(obj1, obj2) {
const b1 = obj1.getBounds();
const b2 = obj2.getBounds();
return b1.x < b2.x + b2.width &&
b1.x + b1.width > b2.x &&
b1.y < b2.y + b2.height &&
b1.y + b1.height > b2.y;
}
// 碰撞解决
resolveCollision(obj1, obj2) {
// 确定哪个是动态对象
const dynamic = obj1.isStatic ? obj2 : obj1;
const staticObj = obj1.isStatic ? obj1 : obj2;
// 计算重叠
const b1 = dynamic.getBounds();
const b2 = staticObj.getBounds();
// 计算四个方向的穿透深度
const overlapLeft = (b1.x + b1.width) - b2.x;
const overlapRight = b2.x + b2.width - b1.x;
const overlapTop = (b1.y + b1.height) - b2.y;
const overlapBottom = b2.y + b2.height - b1.y;
// 找到最小的穿透方向
const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
// 根据最小穿透方向修正位置和速度
if (minOverlap === overlapLeft) {
dynamic.x -= overlapLeft;
dynamic.vx = 0;
} else if (minOverlap === overlapRight) {
dynamic.x += overlapRight;
dynamic.vx = 0;
} else if (minOverlap === overlapTop) {
dynamic.y -= overlapTop;
dynamic.vy = 0;
dynamic.grounded = true; // 顶部碰撞,站在平台上
} else if (minOverlap === overlapBottom) {
dynamic.y += overlapBottom;
dynamic.vy = 0;
}
}
}
3. 游戏主循环集成
// 游戏主类
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.keys = {};
this.collisionManager = new CollisionManager();
this.player = new Player(100, 100);
this.platforms = [
new Platform(0, 400, 800, 50), // 地面
new Platform(200, 300, 150, 20), // 中间平台
new Platform(400, 200, 100, 20) // 高处平台
];
// 注册对象
this.collisionManager.addObject(this.player);
this.platforms.forEach(p => this.collisionManager.addObject(p));
// 键盘事件
window.addEventListener('keydown', e => this.keys[e.key] = true);
window.addEventListener('keyup', e => this.keys[e.key] = false);
}
update() {
// 更新玩家
this.player.update(this.keys);
// 检测碰撞
this.collisionManager.detectCollisions();
// 边界检查(防止掉出世界)
if (this.player.y > this.canvas.height) {
this.player.x = 100;
this.player.y = 100;
this.player.vx = 0;
this.player.vy = 0;
}
}
render() {
// 清空画布
this.ctx.fillStyle = '#87CEEB';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制平台
this.ctx.fillStyle = '#654321';
this.platforms.forEach(p => {
this.ctx.fillRect(p.x, p.y, p.width, p.height);
});
// 绘制玩家
this.ctx.fillStyle = '#FF0000';
this.ctx.fillRect(this.player.x, this.player.y, this.player.width, this.player.height);
// 绘制玩家中心点(调试用)
this.ctx.fillStyle = '#FFFFFF';
this.ctx.fillRect(this.player.x + 14, this.player.y + 14, 4, 4);
}
loop() {
this.update();
this.render();
requestAnimationFrame(() => this.loop());
}
}
// 启动游戏
window.onload = () => {
const game = new Game();
game.loop();
};
4. HTML结构
<!DOCTYPE html>
<html>
<head>
<title>2D碰撞检测示例</title>
<style>
body { margin: 0; padding: 20px; background: #222; }
canvas { border: 2px solid #fff; background: #87CEEB; }
</style>
</head>
<body>
<canvas id="gameCanvas" width="800" height="450"></canvas>
<script src="game.js"></script>
</body>
</html>
基础实现的问题与局限
这个基础系统虽然能工作,但存在几个关键问题:
- 隧道效应(Tunneling):高速移动时可能穿过薄物体
- 摩擦力缺失:角色在平台上的移动没有摩擦感
- 无旋转支持:无法处理旋转的物体
- 性能问题:O(n²)的检测复杂度在对象多时会变慢
- 精度问题:AABB在旋转时不再精确
中级技术:解决常见问题与挑战
1. 解决隧道效应:连续碰撞检测(CCD)
隧道效应发生在物体速度过快,在一帧内移动距离超过碰撞体尺寸时。
解决方案:射线投射(Raycasting)
// 射线投射检测
class Raycast {
// 检查射线与AABB的交点
static raycastAABB(origin, direction, aabb, maxDistance = 100) {
const invDir = {
x: 1 / direction.x,
y: 1 / direction.y
};
let t1 = (aabb.x - origin.x) * invDir.x;
let t2 = (aabb.x + aabb.width - origin.x) * invDir.x;
let t3 = (aabb.y - origin.y) * invDir.y;
let t4 = (aabb.y + aabb.height - origin.y) * invDir.y;
const tmin = Math.max(Math.min(t1, t2), Math.min(t3, t4));
const tmax = Math.min(Math.max(t1, t2), Math.max(t3, t4));
if (tmax < 0 || tmin > tmax || tmin > maxDistance) {
return null; // 无碰撞
}
return {
distance: tmin,
point: {
x: origin.x + direction.x * tmin,
y: origin.y + direction.y * tmin
}
};
}
// 改进的玩家移动检测
static moveWithCCD(player, targetX, targetY, collisionManager) {
const direction = {
x: targetX - player.x,
y: targetY - player.y
};
const distance = Math.sqrt(direction.x * direction.x + direction.y * direction.y);
if (distance === 0) return { x: player.x, y: player.y, collided: false };
// 归一化方向
direction.x /= distance;
direction.y /= distance;
// 检查路径上的所有碰撞体
let closestHit = null;
let minDistance = distance;
for (const obj of collisionManager.objects) {
if (obj === player) continue;
const hit = Raycast.raycastAABB(
{ x: player.x + player.width/2, y: player.y + player.height/2 },
direction,
obj.getBounds(),
distance
);
if (hit && hit.distance < minDistance) {
minDistance = hit.distance;
closestHit = { hit, obj };
}
}
if (closestHit) {
// 在碰撞点前停止
return {
x: player.x + direction.x * (minDistance - 1), // -1 防止z-fighting
y: player.y + direction.y * (minDistance - 1),
collided: true,
hitObject: closestHit.obj
};
}
return { x: targetX, y: targetY, collided: false };
}
}
2. 添加摩擦力和物理材质
// 物理材质
class PhysicsMaterial {
constructor(friction = 0.5, bounciness = 0.0) {
this.friction = friction; // 摩擦系数 0-1
this.bounciness = bounciness; // 弹性系数 0-1
}
}
// 改进的玩家类
class AdvancedPlayer extends GameObject {
constructor(x, y) {
super(x, y, 32, 32);
this.speed = 5;
this.jumpForce = -12;
this.grounded = false;
this.material = new PhysicsMaterial(0.8, 0.1);
this.frictionForce = 0;
}
update(keys, deltaTime) {
// 水平移动(考虑摩擦力)
let targetVx = 0;
if (keys['ArrowLeft']) targetVx = -this.speed;
else if (keys['ArrowRight']) targetVx = this.speed;
// 应用摩擦力
if (this.grounded && Math.abs(targetVx) < 0.1) {
// 地面静止摩擦
this.vx *= 0.85;
if (Math.abs(this.vx) < 0.1) this.vx = 0;
} else {
// 空中或移动时的摩擦
const frictionFactor = 1 - (this.material.friction * 0.1);
this.vx *= frictionFactor;
if (Math.abs(this.vx) < 0.01) this.vx = 0;
}
// 应用输入
if (Math.abs(targetVx) > 0) {
this.vx = targetVx;
}
// 跳跃
if (keys['ArrowUp'] && this.grounded) {
this.vy = this.jumpForce;
this.grounded = false;
}
// 重力
this.vy += 0.5;
// 更新位置
this.x += this.vx;
this.y += this.vy;
}
}
3. 空间分区优化性能
当对象数量增加时,O(n²)的检测复杂度会成为瓶颈。使用空间分区(如网格或四叉树)可以显著优化。
// 简单的网格分区
class SpatialGrid {
constructor(cellSize, worldWidth, worldHeight) {
this.cellSize = cellSize;
this.cols = Math.ceil(worldWidth / cellSize);
this.rows = Math.ceil(worldHeight / cellSize);
this.grid = new Array(this.cols * this.rows).fill(null).map(() => []);
}
// 清空网格
clear() {
for (let i = 0; i < this.grid.length; i++) {
this.grid[i] = [];
}
}
// 将对象插入网格
insert(obj) {
const bounds = obj.getBounds();
const startCol = Math.floor(bounds.x / this.cellSize);
const endCol = Math.floor((bounds.x + bounds.width) / this.cellSize);
const startRow = Math.floor(bounds.y / this.cellSize);
const endRow = Math.floor((bounds.y + bounds.height) / this.cellSize);
for (let col = startCol; col <= endCol; col++) {
for (let row = startRow; row <= endRow; row++) {
if (col >= 0 && col < this.cols && row >= 0 && row < this.rows) {
const index = row * this.cols + col;
this.grid[index].push(obj);
}
}
}
}
// 获取可能碰撞的对象
getPotentialCollisions(obj) {
const bounds = obj.getBounds();
const startCol = Math.floor(bounds.x / this.cellSize);
const endCol = Math.floor((bounds.x + bounds.width) / this.cellSize);
const startRow = Math.floor(bounds.y / this.cellSize);
const endRow = Math.floor((bounds.y + bounds.height) / this.cellSize);
const candidates = new Set();
for (let col = startCol; col <= endCol; col++) {
for (let row = startRow; row <= endRow; row++) {
if (col >= 0 && col < this.cols && row >= 0 && row < this.rows) {
const index = row * this.cols + col;
const cellObjects = this.grid[index];
for (const other of cellObjects) {
if (other !== obj) {
candidates.add(other);
}
}
}
}
}
return Array.from(candidates);
}
}
// 改进的碰撞管理器
class OptimizedCollisionManager {
constructor(worldWidth, worldHeight) {
this.objects = [];
this.spatialGrid = new SpatialGrid(100, worldWidth, worldHeight); // 100px网格
}
addObject(obj) {
this.objects.push(obj);
}
detectCollisions() {
// 重建空间网格
this.spatialGrid.clear();
this.objects.forEach(obj => this.spatialGrid.insert(obj));
// 只检测同一网格内的对象
const processed = new Set();
for (const obj of this.objects) {
if (obj.isStatic) continue;
const candidates = this.spatialGrid.getPotentialCollisions(obj);
for (const other of candidates) {
const pairKey = [obj, other].sort().join('-');
if (processed.has(pairKey)) continue;
processed.add(pairKey);
if (this.checkCollision(obj, other)) {
this.resolveCollision(obj, other);
}
}
}
}
checkCollision(obj1, obj2) {
// 同上基础实现
const b1 = obj1.getBounds();
const b2 = obj2.getBounds();
return b1.x < b2.x + b2.width &&
b1.x + b1.width > b2.x &&
b1.y < b2.y + b2.height &&
b1.y + b1.height > b2.y;
}
resolveCollision(obj1, obj2) {
// 同上基础实现
const dynamic = obj1.isStatic ? obj2 : obj1;
const staticObj = obj1.isStatic ? obj1 : obj2;
const b1 = dynamic.getBounds();
const b2 = staticObj.getBounds();
const overlapLeft = (b1.x + b1.width) - b2.x;
const overlapRight = b2.x + b2.width - b1.x;
const overlapTop = (b1.y + b1.height) - b2.y;
const overlapBottom = b2.y + b2.height - b1.y;
const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
if (minOverlap === overlapLeft) {
dynamic.x -= overlapLeft;
dynamic.vx = 0;
} else if (minOverlap === overlapRight) {
dynamic.x += overlapRight;
dynamic.vx = 0;
} else if (minOverlap === overlapTop) {
dynamic.y -= overlapTop;
dynamic.vy = 0;
dynamic.grounded = true;
} else if (minOverlap === overlapBottom) {
dynamic.y += overlapBottom;
dynamic.vy = 0;
}
}
}
4. Unity中的碰撞系统(C#)
在Unity中,碰撞系统已经高度优化,但理解其工作原理对于调试和自定义至关重要。
// Unity 2D碰撞组件
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(BoxCollider2D))]
public class PlayerController2D : MonoBehaviour
{
[Header("Movement Settings")]
public float moveSpeed = 5f;
public float jumpForce = 10f;
public float groundCheckRadius = 0.2f;
public LayerMask groundLayer;
private Rigidbody2D rb;
private BoxCollider2D col;
private bool isGrounded;
private bool facingRight = true;
void Awake()
{
rb = GetComponent<Rigidbody2D>();
col = GetComponent<BoxCollider2D>();
// 冻结旋转,防止角色翻倒
rb.freezeRotation = true;
}
void Update()
{
// 检查是否在地面
CheckGrounded();
// 处理输入
HandleMovement();
HandleJump();
}
void CheckGrounded()
{
// 使用射线投射检查地面(比OverlapCircle更精确)
Vector2 origin = new Vector2(col.bounds.center.x, col.bounds.min.y);
Vector2 direction = Vector2.down;
float distance = groundCheckRadius;
// 可视化调试射线
Debug.DrawRay(origin, direction * distance, Color.red);
RaycastHit2D hit = Physics2D.Raycast(origin, direction, distance, groundLayer);
isGrounded = hit.collider != null;
}
void HandleMovement()
{
float moveInput = Input.GetAxis("Horizontal");
// 应用速度
rb.velocity = new Vector2(moveInput * moveSpeed, rb.velocity.y);
// 翻转角色
if (moveInput > 0 && !facingRight)
Flip();
else if (moveInput < 0 && facingRight)
Flip();
}
void HandleJump()
{
if (Input.GetButtonDown("Jump") && isGrounded)
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
}
}
void Flip()
{
facingRight = !facingRight;
Vector3 theScale = transform.localScale;
theScale.x *= -1;
transform.localScale = theScale;
}
// 碰撞事件处理
void OnCollisionEnter2D(Collision2D collision)
{
// 检查碰撞标签
if (collision.gameObject.CompareTag("Enemy"))
{
// 玩家受伤逻辑
TakeDamage();
}
else if (collision.gameObject.CompareTag("Coin"))
{
// 收集金币
CollectCoin(collision.gameObject);
}
}
void OnTriggerEnter2D(Collider2D other)
{
// 触发器(非物理碰撞)
if (other.CompareTag("LevelExit"))
{
// 加载下一关
LoadNextLevel();
}
}
void TakeDamage()
{
// 受伤逻辑
Debug.Log("Player took damage!");
// 添加无敌时间、动画等
}
void CollectCoin(GameObject coin)
{
// 销毁金币
Destroy(coin);
// 增加分数
GameManager.Instance.AddScore(10);
}
void LoadNextLevel()
{
// 场景加载逻辑
Debug.Log("Loading next level...");
}
}
Unity碰撞矩阵配置
在Unity中,合理的碰撞矩阵配置可以大幅提升性能:
// 碰撞矩阵配置示例(在Project Settings > Physics 2D中)
// Layer Collision Matrix:
// Layer 0: Default
// Layer 1: Player
// Layer 2: Enemy
// Layer 3: Ground
// Layer 4: Trigger
// 配置规则:
// Player ↔ Ground: true (需要物理碰撞)
// Player ↔ Enemy: true (需要伤害检测)
// Player ↔ Trigger: false (使用Trigger事件)
// Enemy ↔ Ground: true
// Enemy ↔ Enemy: false (避免互相推开)
// Ground ↔ Ground: false (静态物体无需检测)
高级优化:性能与精度的完美平衡
1. 分离轴定理(SAT):精确凸多边形检测
对于需要精确碰撞的复杂形状(如旋转的矩形、多边形),SAT是最佳选择。
// 向量数学库
class Vector2 {
constructor(x, y) {
this.x = x;
this.y = y;
}
dot(v) {
return this.x * v.x + this.y * v.y;
}
perpendicular() {
return new Vector2(-this.y, this.x);
}
normalize() {
const mag = Math.sqrt(this.x * this.x + this.y * this.y);
return mag === 0 ? new Vector2(0, 0) : new Vector2(this.x / mag, this.y / mag);
}
static subtract(v1, v2) {
return new Vector2(v1.x - v2.x, v1.y - v2.y);
}
}
// 多边形碰撞器
class PolygonCollider {
constructor(vertices) {
this.vertices = vertices; // 顶点数组 [{x, y}, ...]
this.position = { x: 0, y: 0 };
this.rotation = 0;
}
// 获取世界坐标下的顶点
getWorldVertices() {
const cos = Math.cos(this.rotation);
const sin = Math.sin(this.rotation);
return this.vertices.map(v => {
// 旋转
const rx = v.x * cos - v.y * sin;
const ry = v.x * sin + v.y * cos;
// 平移
return {
x: rx + this.position.x,
y: ry + this.position.y
};
});
}
// 获取投影轴
getAxes() {
const axes = [];
const verts = this.getWorldVertices();
for (let i = 0; i < verts.length; i++) {
const p1 = verts[i];
const p2 = verts[(i + 1) % verts.length];
const edge = Vector2.subtract(p2, p1);
const normal = edge.perpendicular().normalize();
axes.push(normal);
}
return axes;
}
// 投影到轴上
project(axis) {
const verts = this.getWorldVertices();
let min = axis.dot(new Vector2(verts[0].x, verts[0].y));
let max = min;
for (let i = 1; i < verts.length; i++) {
const p = axis.dot(new Vector2(verts[i].x, verts[i].y));
if (p < min) min = p;
if (p > max) max = p;
}
return { min, max };
}
}
// SAT碰撞检测
class SATCollision {
static check(poly1, poly2) {
const axes1 = poly1.getAxes();
const axes2 = poly2.getAxes();
const axes = [...axes1, ...axes2];
for (const axis of axes) {
const p1 = poly1.project(axis);
const p2 = poly2.project(axis);
// 检查投影是否重叠
if (p1.max < p2.min || p2.max < p1.min) {
return false; // 无碰撞
}
}
return true; // 碰撞
}
// 带分离向量的碰撞检测(用于解决碰撞)
static checkWithSeparation(poly1, poly2) {
const axes1 = poly1.getAxes();
const axes2 = poly2.getAxes();
const axes = [...axes1, ...axes2];
let minOverlap = Infinity;
let smallestAxis = null;
for (const axis of axes) {
const p1 = poly1.project(axis);
const p2 = poly2.project(axis);
if (p1.max < p2.min || p2.max < p1.min) {
return { collided: false };
}
// 计算重叠
const overlap = Math.min(p1.max, p2.max) - Math.max(p1.min, p2.min);
if (overlap < minOverlap) {
minOverlap = overlap;
smallestAxis = axis;
}
}
// 确保分离向量指向poly1
const center1 = { x: poly1.position.x, y: poly1.position.y };
const center2 = { x: poly2.position.x, y: poly2.position.y };
const direction = Vector2.subtract(center2, center1);
if (direction.dot(smallestAxis) < 0) {
smallestAxis.x *= -1;
smallestAxis.y *= -1;
}
return {
collided: true,
separation: {
x: smallestAxis.x * minOverlap,
y: smallestAxis.y * minOverlap
}
};
}
}
// 使用示例
function testSAT() {
// 创建两个旋转的矩形
const rect1 = new PolygonCollider([
{ x: -20, y: -15 },
{ x: 20, y: -15 },
{ x: 20, y: 15 },
{ x: -20, y: 15 }
]);
rect1.position = { x: 100, y: 100 };
rect1.rotation = Math.PI / 6; // 30度
const rect2 = new PolygonCollider([
{ x: -25, y: -20 },
{ x: 25, y: -20 },
{ x: 25, y: 20 },
{ x: -25, y: 20 }
]);
rect2.position = { x: 150, y: 120 };
rect2.rotation = -Math.PI / 4; // -45度
const result = SATCollision.checkWithSeparation(rect1, rect2);
console.log("SAT Collision Result:", result);
}
2. 连续碰撞检测(CCD)高级实现
对于高速物体(如子弹),使用扫掠体(Swept AABB)检测。
// 扫掠AABB检测
class SweptAABB {
// 计算移动物体的扫掠体与静态物体的碰撞
static sweep(movingObj, staticObj, velocity) {
const entryDelta = {
x: staticObj.x - (movingObj.x + movingObj.width),
y: staticObj.y - (movingObj.y + movingObj.height)
};
const exitDelta = {
x: (staticObj.x + staticObj.width) - movingObj.x,
y: (staticObj.y + staticObj.height) - movingObj.y
};
// 如果已经重叠,返回立即碰撞
if (movingObj.x < staticObj.x + staticObj.width &&
movingObj.x + movingObj.width > staticObj.x &&
movingObj.y < staticObj.y + staticObj.height &&
movingObj.y + movingObj.height > staticObj.y) {
return { collided: true, time: 0, normal: { x: 0, y: 0 } };
}
// 计算进入和退出时间
const entryTime = {
x: velocity.x > 0 ? entryDelta.x / velocity.x : exitDelta.x / velocity.x,
y: velocity.y > 0 ? entryDelta.y / velocity.y : exitDelta.y / velocity.y
};
const exitTime = {
x: velocity.x > 0 ? exitDelta.x / velocity.x : entryDelta.x / velocity.x,
y: velocity.y > 0 ? exitDelta.y / velocity.y : entryDelta.y / velocity.y
};
const entryTimeMax = Math.max(entryTime.x, entryTime.y);
const exitTimeMin = Math.min(exitTime.x, exitTime.y);
// 检查是否会发生碰撞
if (entryTimeMax > exitTimeMin ||
(entryTime.x < 0 && entryTime.y < 0) ||
entryTime.x > 1 || entryTime.y > 1) {
return { collided: false };
}
// 计算碰撞法线
let normalX = 0, normalY = 0;
if (entryTime.x > entryTime.y) {
normalX = velocity.x < 0 ? 1 : -1;
normalY = 0;
} else {
normalX = 0;
normalY = velocity.y < 0 ? 1 : -1;
}
return {
collided: true,
time: entryTimeMax,
normal: { x: normalX, y: normalY }
};
}
// 解决扫掠碰撞
static resolveSweptCollision(movingObj, staticObj, velocity) {
const result = SweptAABB.sweep(movingObj, staticObj, velocity);
if (result.collided) {
// 移动到碰撞点
movingObj.x += velocity.x * result.time;
movingObj.y += velocity.y * result.time;
// 根据法线修正速度
if (result.normal.x !== 0) {
movingObj.vx = 0;
// 可选:应用弹性
if (movingObj.material && movingObj.material.bounciness > 0) {
movingObj.vx = -velocity.x * movingObj.material.bounciness;
}
}
if (result.normal.y !== 0) {
movingObj.vy = 0;
if (movingObj.material && movingObj.material.bounciness > 0) {
movingObj.vy = -velocity.y * movingObj.material.bounciness;
}
}
return true;
}
return false;
}
}
3. Unity中的高级优化技术
3.1 碰撞层级与过滤
// 使用LayerMask进行高效过滤
public class OptimizedCollision : MonoBehaviour
{
public LayerMask enemyLayer;
public LayerMask groundLayer;
void Update()
{
// 只检测特定层,避免不必要的计算
Collider2D[] enemies = Physics2D.OverlapCircleAll(
transform.position,
5f,
enemyLayer
);
foreach (var enemy in enemies)
{
// 处理敌人碰撞
}
}
// 使用Physics2D.IgnoreLayerCollision避免层间检测
void SetupCollisionMatrix()
{
// 忽略Player层和Enemy层之间的物理碰撞(使用Trigger事件)
Physics2D.IgnoreLayerCollision(6, 7, true);
}
}
3.2 Job System与Burst编译器(Unity 2019+)
对于大规模碰撞检测(如弹幕游戏),使用Unity的Job System进行并行计算。
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
[BurstCompile]
struct CollisionJob : IJobParallelFor
{
[ReadOnly] public NativeArray<Vector2> positions;
[ReadOnly] public NativeArray<float> radii;
[WriteOnly] public NativeArray<bool> results;
public float detectionRadius;
public void Execute(int index)
{
bool collided = false;
Vector2 myPos = positions[index];
float myRadius = radii[index];
for (int i = 0; i < positions.Length; i++)
{
if (i == index) continue;
float distance = Vector2.Distance(myPos, positions[i]);
if (distance < (myRadius + radii[i] + detectionRadius))
{
collided = true;
break;
}
}
results[index] = collided;
}
}
public class ParallelCollisionDetector : MonoBehaviour
{
public int objectCount = 1000;
private NativeArray<Vector2> positions;
private NativeArray<float> radii;
private NativeArray<bool> collisionResults;
void Start()
{
positions = new NativeArray<Vector2>(objectCount, Allocator.Persistent);
radii = new NativeArray<float>(objectCount, Allocator.Persistent);
collisionResults = new NativeArray<bool>(objectCount, Allocator.Persistent);
// 初始化数据
for (int i = 0; i < objectCount; i++)
{
positions[i] = Random.insideUnitCircle * 100f;
radii[i] = Random.Range(0.5f, 2f);
}
}
void Update()
{
// 创建Job
var job = new CollisionJob
{
positions = positions,
radii = radii,
results = collisionResults,
detectionRadius = 0.1f
};
// 调度并行任务
JobHandle handle = job.Schedule(objectCount, 64);
handle.Complete();
// 处理结果
for (int i = 0; i < objectCount; i++)
{
if (collisionResults[i])
{
// 处理碰撞对象
Debug.Log($"Object {i} collided!");
}
}
}
void OnDestroy()
{
// 释放原生内存
positions.Dispose();
radii.Dispose();
collisionResults.Dispose();
}
}
4. 物理引擎集成:Box2D与Bullet
对于复杂物理交互,集成成熟物理引擎是最佳选择。
4.1 Box2D Web示例
// 使用Box2D.js进行物理模拟
// 需要引入Box2D库
// Box2D物理世界管理器
class Box2DWorld {
constructor() {
// Box2D世界设置
const gravity = new b2Vec2(0, 9.8); // 重力
this.world = new b2World(gravity);
// 碰撞监听器
this.setupCollisionListener();
}
setupCollisionListener() {
// 创建接触监听器
const listener = new b2ContactListener();
listener.BeginContact = (contact) => {
const fixtureA = contact.GetFixtureA();
const fixtureB = contact.GetFixtureB();
const bodyA = fixtureA.GetBody();
const bodyB = fixtureB.GetBody();
// 获取自定义数据
const userDataA = bodyA.GetUserData();
const userDataB = bodyB.GetUserData();
if (userDataA && userDataB) {
this.handleCollision(userDataA, userDataB, 'begin');
}
};
listener.EndContact = (contact) => {
// 处理碰撞结束
};
this.world.SetContactListener(listener);
}
handleCollision(objA, objB, type) {
// 根据自定义数据处理碰撞
if (objA.type === 'player' && objB.type === 'coin') {
if (type === 'begin') {
objB.collect(); // 收集金币
}
}
}
// 创建动态物体(玩家)
createPlayer(x, y) {
const bodyDef = new b2BodyDef();
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(x, y);
bodyDef.fixedRotation = true;
const body = this.world.CreateBody(bodyDef);
// 碰撞形状(矩形)
const shape = new b2PolygonShape();
shape.SetAsBox(0.5, 0.5); // 半宽,半高
// 夹具定义
const fixtureDef = new b2FixtureDef();
fixtureDef.shape = shape;
fixtureDef.density = 1.0;
fixtureDef.friction = 0.3;
fixtureDef.restitution = 0.1; // 弹性
body.CreateFixture(fixtureDef);
// 存储自定义数据
body.SetUserData({
type: 'player',
ref: this // 引用回JS对象
});
return body;
}
// 创建静态物体(地面)
createGround(x, y, width, height) {
const bodyDef = new b2BodyDef();
bodyDef.type = b2_staticBody;
bodyDef.position.Set(x, y);
const body = this.world.CreateBody(bodyDef);
const shape = new b2PolygonShape();
shape.SetAsBox(width / 2, height / 2);
body.CreateFixture(shape, 0); // 密度为0(静态)
body.SetUserData({ type: 'ground' });
return body;
}
// 更新物理世界
update(deltaTime) {
// 步进物理世界
this.world.Step(deltaTime, 8, 3);
// 清理力
this.world.ClearForces();
}
}
常见问题与解决方案
问题1:角色卡在墙角(Corner Snagging)
原因:同时与两个垂直表面碰撞时,解决顺序导致错误。
解决方案:使用分离轴修正或多次迭代解决。
// 改进的碰撞解决:多次迭代
class IterativeCollisionResolver {
constructor(maxIterations = 4) {
this.maxIterations = maxIterations;
}
resolve(obj1, obj2) {
for (let i = 0; i < this.maxIterations; i++) {
const b1 = obj1.getBounds();
const b2 = obj2.getBounds();
// 检查是否还有重叠
if (!this.checkCollision(b1, b2)) break;
// 计算穿透
const overlapLeft = (b1.x + b1.width) - b2.x;
const overlapRight = b2.x + b2.width - b1.x;
const overlapTop = (b1.y + b1.height) - b2.y;
const overlapBottom = b2.y + b2.height - b1.y;
// 选择最小穿透方向
const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
// 应用修正
if (minOverlap === overlapLeft) {
obj1.x -= overlapLeft * 0.5; // 使用0.5系数减少抖动
obj1.vx = 0;
} else if (minOverlap === overlapRight) {
obj1.x += overlapRight * 0.5;
obj1.vx = 0;
} else if (minOverlap === overlapTop) {
obj1.y -= overlapTop * 0.5;
obj1.vy = 0;
obj1.grounded = true;
} else if (minOverlap === overlapBottom) {
obj1.y += overlapBottom * 0.5;
obj1.vy = 0;
}
// 如果修正后仍有重叠,继续迭代
}
}
checkCollision(b1, b2) {
return b1.x < b2.x + b2.width &&
b1.x + b1.width > b2.x &&
b1.y < b2.y + b2.height &&
b1.y + b1.height > b2.y;
}
}
问题2:性能瓶颈(大量对象)
原因:O(n²)检测复杂度。
解决方案:结合空间分区与对象池。
// 对象池管理
class ObjectPool {
constructor(createFn, initialSize = 50) {
this.createFn = createFn;
this.pool = [];
this.active = new Set();
for (let i = 0; i < initialSize; i++) {
this.pool.push(createFn());
}
}
get() {
let obj = this.pool.pop();
if (!obj) {
obj = this.createFn();
}
this.active.add(obj);
return obj;
}
release(obj) {
if (this.active.has(obj)) {
this.active.delete(obj);
// 重置对象状态
obj.reset();
this.pool.push(obj);
}
}
getActiveCount() {
return this.active.size;
}
}
// 使用对象池的碰撞管理器
class PooledCollisionManager {
constructor(worldWidth, worldHeight) {
this.spatialGrid = new SpatialGrid(100, worldWidth, worldHeight);
this.bulletPool = new ObjectPool(() => new Bullet(), 100);
}
spawnBullet(x, y, vx, vy) {
const bullet = this.bulletPool.get();
bullet.x = x;
bullet.y = y;
bullet.vx = vx;
bullet.vy = vy;
bullet.active = true;
return bullet;
}
update() {
// 更新所有活动对象
this.spatialGrid.clear();
// 只处理活动对象
this.bulletPool.active.forEach(bullet => {
if (bullet.active) {
bullet.update();
this.spatialGrid.insert(bullet);
}
});
// 碰撞检测
this.bulletPool.active.forEach(bullet => {
if (!bullet.active) return;
const candidates = this.spatialGrid.getPotentialCollisions(bullet);
for (const other of candidates) {
if (other === bullet) continue;
if (this.checkCollision(bullet, other)) {
this.resolveCollision(bullet, other);
}
}
});
// 回收超出边界的子弹
this.bulletPool.active.forEach(bullet => {
if (bullet.x < 0 || bullet.x > 800 || bullet.y < 0 || bullet.y > 600) {
bullet.active = false;
this.bulletPool.release(bullet);
}
});
}
}
问题3:网络同步中的碰撞不一致
原因:客户端与服务器物理模拟不同步。
解决方案:使用确定性物理或状态同步。
// Unity中的网络同步示例(使用Mirror网络库)
using Mirror;
public class NetworkedPlayer : NetworkBehaviour
{
[SyncVar] public Vector3 syncPosition;
[SyncVar] public Quaternion syncRotation;
private Vector3 lastPosition;
private NetworkTransform networkTransform;
void Start()
{
networkTransform = GetComponent<NetworkTransform>();
// 客户端只处理自己的输入
if (isLocalPlayer)
{
// 启用本地物理模拟
GetComponent<Rigidbody>().isKinematic = false;
}
else
{
// 其他玩家使用插值
GetComponent<Rigidbody>().isKinematic = true;
}
}
void Update()
{
if (isLocalPlayer)
{
// 本地控制
HandleInput();
// 定期同步位置(减少带宽)
if (Vector3.Distance(transform.position, lastPosition) > 0.1f)
{
CmdSyncPosition(transform.position, transform.rotation);
lastPosition = transform.position;
}
}
else
{
// 远程玩家插值
transform.position = Vector3.Lerp(transform.position, syncPosition, Time.deltaTime * 10);
transform.rotation = Quaternion.Lerp(transform.rotation, syncRotation, Time.deltaTime * 10);
}
}
[Command]
void CmdSyncPosition(Vector3 pos, Quaternion rot)
{
// 服务器验证并广播
syncPosition = pos;
syncRotation = rot;
// 服务器端碰撞检测(权威)
ServerSideCollisionCheck();
}
void ServerSideCollisionCheck()
{
// 服务器执行精确碰撞检测
// 如果检测到作弊或非法位置,进行纠正
}
void HandleInput()
{
// 本地输入处理
float moveX = Input.GetAxis("Horizontal");
// ... 移动逻辑
}
}
问题4:斜坡与不规则地形处理
原因:AABB无法处理斜坡。
解决方案:使用射线投射或多点检测。
// 斜坡检测与处理
class SlopeHandler {
constructor(player) {
this.player = player;
this.groundRaycasts = 5; // 底部5条射线
}
checkGround() {
const rays = [];
const playerWidth = this.player.width;
const spacing = playerWidth / (this.groundRaycasts - 1);
for (let i = 0; i < this.groundRaycasts; i++) {
const offsetX = -playerWidth / 2 + i * spacing;
const origin = {
x: this.player.x + this.player.width / 2 + offsetX,
y: this.player.y + this.player.height
};
// 向下投射射线
const hit = this.castRay(origin, { x: 0, y: 1 }, 10);
rays.push(hit);
}
// 分析射线结果
const groundHits = rays.filter(hit => hit !== null);
if (groundHits.length > 0) {
this.player.grounded = true;
// 计算平均高度和法线(用于斜坡)
const avgHeight = groundHits.reduce((sum, hit) => sum + hit.point.y, 0) / groundHits.length;
// 如果在斜坡上,调整垂直位置
if (Math.abs(avgHeight - (this.player.y + this.player.height)) > 0.1) {
this.player.y = avgHeight - this.player.height;
this.player.vy = 0;
}
// 计算斜坡角度
if (groundHits.length >= 2) {
const first = groundHits[0];
const last = groundHits[groundHits.length - 1];
const slope = Math.atan2(
last.point.y - first.point.y,
last.point.x - first.point.x
);
// 应用斜坡滑动
if (Math.abs(slope) > 0.1) {
this.player.vx += Math.sin(slope) * 0.5;
}
}
} else {
this.player.grounded = false;
}
}
castRay(origin, direction, maxDistance) {
// 简化的射线投射(实际应检测所有碰撞体)
for (const obj of collisionManager.objects) {
if (obj === this.player) continue;
const hit = Raycast.raycastAABB(origin, direction, obj.getBounds(), maxDistance);
if (hit) return hit;
}
return null;
}
}
测试与调试
1. 可视化调试工具
// 调试渲染器
class DebugRenderer {
constructor(ctx) {
this.ctx = ctx;
this.debug = true;
}
drawBounds(obj, color = 'rgba(255, 0, 0, 0.3)') {
if (!this.debug) return;
const b = obj.getBounds();
this.ctx.strokeStyle = color;
this.ctx.lineWidth = 1;
this.ctx.strokeRect(b.x, b.y, b.width, b.height);
}
drawRay(origin, direction, distance, color = 'rgba(0, 255, 0, 0.5)') {
if (!this.debug) return;
this.ctx.strokeStyle = color;
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.moveTo(origin.x, origin.y);
this.ctx.lineTo(origin.x + direction.x * distance, origin.y + direction.y * distance);
this.ctx.stroke();
}
drawGrid(grid, color = 'rgba(255, 255, 0, 0.1)') {
if (!this.debug) return;
this.ctx.strokeStyle = color;
this.ctx.lineWidth = 1;
for (let col = 0; col < grid.cols; col++) {
for (let row = 0; row < grid.rows; row++) {
const x = col * grid.cellSize;
const y = row * grid.cellSize;
this.ctx.strokeRect(x, y, grid.cellSize, grid.cellSize);
}
}
}
drawCollisionNormal(point, normal, length = 20, color = 'rgba(255, 0, 255, 0.8)') {
if (!this.debug) return;
this.ctx.strokeStyle = color;
this.ctx.lineWidth = 3;
this.ctx.beginPath();
this.ctx.moveTo(point.x, point.y);
this.ctx.lineTo(point.x + normal.x * length, point.y + normal.y * length);
this.ctx.stroke();
}
}
2. 单元测试
// 使用Jest风格的测试
class CollisionTests {
static run() {
console.log("Running collision tests...");
// 测试1: AABB碰撞
const aabb1 = { x: 0, y: 0, width: 10, height: 10 };
const aabb2 = { x: 5, y: 5, width: 10, height: 10 };
const result1 = checkAABBCollision(aabb1, aabb2);
console.assert(result1 === true, "AABB碰撞测试失败");
// 测试2: 圆形碰撞
const circle1 = { x: 0, y: 0, radius: 5 };
const circle2 = { x: 8, y: 0, radius: 5 };
const result2 = checkCircleCollision(circle1, circle2);
console.assert(result2 === true, "圆形碰撞测试失败");
// 测试3: 无碰撞
const circle3 = { x: 0, y: 0, radius: 5 };
const circle4 = { x: 12, y: 0, radius: 5 };
const result3 = checkCircleCollision(circle3, circle4);
console.assert(result3 === false, "无碰撞测试失败");
// 测试4: SAT精确碰撞
const poly1 = new PolygonCollider([
{ x: -10, y: -10 }, { x: 10, y: -10 },
{ x: 10, y: 10 }, { x: -10, y: 10 }
]);
poly1.position = { x: 0, y: 0 };
poly1.rotation = 0.1;
const poly2 = new PolygonCollider([
{ x: -10, y: -10 }, { x: 10, y: -10 },
{ x: 10, y: 10 }, { x: -10, y: 10 }
]);
poly2.position = { x: 15, y: 0 };
poly2.rotation = -0.1;
const result4 = SATCollision.check(poly1, poly2);
console.assert(result4 === true, "SAT碰撞测试失败");
console.log("All tests passed!");
}
}
// 运行测试
// CollisionTests.run();
总结与最佳实践
核心原则
- 分层检测:先用粗略形状(AABB)快速排除,再用精确形状(SAT)检测
- 空间分区:对象超过50个时必须使用网格或四叉树
- 性能监控:使用
performance.now()监控碰撞检测耗时 - 调试可视化:始终保留调试绘制开关
- 物理材质:统一管理摩擦、弹性等参数
性能优化清单
- [ ] 使用空间分区(网格/四叉树)
- [ ] 对象池管理动态对象
- [ ] 静态物体不参与每帧检测
- [ ] 使用Job System并行化(Unity)
- [ ] 碰撞矩阵过滤(Unity)
- [ ] LOD(细节层次)用于远距离物体
- [ ] 缓存结果(上一帧未移动的物体)
调试技巧
- 可视化:绘制碰撞体、射线、法线
- 日志:记录碰撞事件和解决过程
- 断点:在
resolveCollision设置断点 - 慢动作:降低时间缩放观察问题
- 隔离测试:单独测试碰撞组件
未来趋势
- 机器学习碰撞预测:使用ML预测碰撞,减少计算
- GPU加速:使用Compute Shader进行大规模碰撞检测
- 确定性物理:网络游戏中保证物理一致性
- 物理材质AI:自动优化物理参数
通过本文的指南,您应该能够构建一个健壮、高效的碰撞检测系统,解决从简单2D平台游戏到复杂3D物理模拟的各种挑战。记住,优秀的碰撞系统是游戏流畅体验的基石,持续优化和测试是成功的关键。
