引言:碰撞检测在游戏开发中的核心地位

在游戏开发中,角色碰撞检测与交互是构建沉浸式体验的基础技术。无论是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();             // 渲染画面
}

常见碰撞形状

为了高效检测,游戏对象通常被简化为几何形状:

  1. AABB(Axis-Aligned Bounding Box):轴对齐包围盒,适用于2D和3D

    • 优点:计算简单,速度快
    • 缺点:无法旋转,不精确
  2. 圆形/球体:适用于角色、弹珠等圆形物体

    • 优点:旋转不变性,距离计算快
    • 缺点:无法精确匹配复杂形状
  3. 胶囊体:Unity中角色控制器的常用形状

    • 优点:适合角色,支持高度和半径
    • 缺点:计算相对复杂
  4. 凸多边形/凸多面体:精确但计算量大

    • 优点:精确匹配
    • 缺点:分离轴定理(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>

基础实现的问题与局限

这个基础系统虽然能工作,但存在几个关键问题:

  1. 隧道效应(Tunneling):高速移动时可能穿过薄物体
  2. 摩擦力缺失:角色在平台上的移动没有摩擦感
  3. 无旋转支持:无法处理旋转的物体
  4. 性能问题:O(n²)的检测复杂度在对象多时会变慢
  5. 精度问题: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();

总结与最佳实践

核心原则

  1. 分层检测:先用粗略形状(AABB)快速排除,再用精确形状(SAT)检测
  2. 空间分区:对象超过50个时必须使用网格或四叉树
  3. 性能监控:使用performance.now()监控碰撞检测耗时
  4. 调试可视化:始终保留调试绘制开关
  5. 物理材质:统一管理摩擦、弹性等参数

性能优化清单

  • [ ] 使用空间分区(网格/四叉树)
  • [ ] 对象池管理动态对象
  • [ ] 静态物体不参与每帧检测
  • [ ] 使用Job System并行化(Unity)
  • [ ] 碰撞矩阵过滤(Unity)
  • [ ] LOD(细节层次)用于远距离物体
  • [ ] 缓存结果(上一帧未移动的物体)

调试技巧

  1. 可视化:绘制碰撞体、射线、法线
  2. 日志:记录碰撞事件和解决过程
  3. 断点:在resolveCollision设置断点
  4. 慢动作:降低时间缩放观察问题
  5. 隔离测试:单独测试碰撞组件

未来趋势

  • 机器学习碰撞预测:使用ML预测碰撞,减少计算
  • GPU加速:使用Compute Shader进行大规模碰撞检测
  • 确定性物理:网络游戏中保证物理一致性
  • 物理材质AI:自动优化物理参数

通过本文的指南,您应该能够构建一个健壮、高效的碰撞检测系统,解决从简单2D平台游戏到复杂3D物理模拟的各种挑战。记住,优秀的碰撞系统是游戏流畅体验的基石,持续优化和测试是成功的关键。