pin_drop当前位置:知识文库 ❯ 图文

游戏开发:碰撞检测 - 从入门到实践详解

教程简介

碰撞检测是游戏开发中最核心的技术之一,它决定了游戏对象之间何时发生交互。无论是角色碰到墙壁、子弹击中敌人,还是玩家拾取道具,都需要依赖碰撞检测系统。本教程将全面讲解各种碰撞检测算法,包括AABB碰撞、圆形碰撞、像素级碰撞、空间分区(网格/四叉树)、碰撞响应、碰撞分离和物理材质,帮助你构建高效准确的游戏碰撞系统。

核心概念

AABB碰撞检测

AABB(Axis-Aligned Bounding Box,轴对齐包围盒)是最简单高效的碰撞检测方法,适用于大多数2D游戏:

代码示例

class AABB {
    constructor(x, y, width, height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    // 检测两个AABB是否重叠
    static intersects(a, b) {
        return a.x < b.x + b.width &&
               a.x + a.width > b.x &&
               a.y < b.y + b.height &&
               a.y + a.height > b.y;
    }

    // 获取重叠区域
    static overlap(a, b) {
        const x = Math.max(a.x, b.x);
        const y = Math.max(a.y, b.y);
        const width = Math.min(a.x + a.width, b.x + b.width) - x;
        const height = Math.min(a.y + a.height, b.y + b.height) - y;
        if (width > 0 && height > 0) {
            return { x, y, width, height };
        }
        return null;
    }

    // 检测点是否在AABB内
    containsPoint(px, py) {
        return px >= this.x && px <= this.x + this.width &&
               py >= this.y && py <= this.y + this.height;
    }
}

圆形碰撞检测

圆形碰撞检测比AABB更精确,适合球形或圆形对象:

代码示例

class Circle {
    constructor(x, y, radius) {
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    // 检测两个圆是否重叠
    static intersects(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;
        const distSq = dx * dx + dy * dy;
        const radiusSum = a.radius + b.radius;
        return distSq < radiusSum * radiusSum;
    }

    // 获取圆心距离
    static distance(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    // 检测点是否在圆内
    containsPoint(px, py) {
        const dx = px - this.x;
        const dy = py - this.y;
        return dx * dx + dy * dy < this.radius * this.radius;
    }
}

圆与矩形碰撞检测

代码示例

function circleRectCollision(circle, rect) {
    // 找到矩形上离圆心最近的点
    const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width));
    const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height));

    const dx = circle.x - closestX;
    const dy = circle.y - closestY;

    return (dx * dx + dy * dy) < (circle.radius * circle.radius);
}

像素级碰撞检测

像素级碰撞检测是最精确的碰撞检测方法,但性能开销最大:

代码示例

class PixelCollision {
    /**
     * 检测两个精灵的像素级碰撞
     * @param {ImageData} imgA - 精灵A的像素数据
     * @param {number} ax - A的X坐标
     * @param {number} ay - A的Y坐标
     * @param {ImageData} imgB - 精灵B的像素数据
     * @param {number} bx - B的X坐标
     * @param {number} by - B的Y坐标
     */
    static check(imgA, ax, ay, imgB, bx, by) {
        // 先做AABB检测,快速排除
        if (ax >= bx + imgB.width || bx >= ax + imgA.width ||
            ay >= by + imgB.height || by >= ay + imgA.height) {
            return false;
        }

        // 计算重叠区域
        const overlapX = Math.max(ax, bx);
        const overlapY = Math.max(ay, by);
        const overlapW = Math.min(ax + imgA.width, bx + imgB.width) - overlapX;
        const overlapH = Math.min(ay + imgA.height, by + imgB.height) - overlapY;

        // 逐像素检测
        for (let y = 0; y < overlapH; y++) {
            for (let x = 0; x < overlapW; x++) {
                const pixelAX = (overlapX - ax) + x;
                const pixelAY = (overlapY - ay) + y;
                const pixelBX = (overlapX - bx) + x;
                const pixelBY = (overlapY - by) + y;

                const alphaA = imgA.data[((pixelAY * imgA.width) + pixelAX) * 4 + 3];
                const alphaB = imgB.data[((pixelBY * imgB.width) + pixelBX) * 4 + 3];

                if (alphaA > 0 && alphaB > 0) return true;
            }
        }
        return false;
    }
}

空间分区 - 网格

网格分区将空间划分为均匀的网格,只检测同一网格或相邻网格中的对象:

代码示例

class SpatialGrid {
    constructor(cellSize, width, height) {
        this.cellSize = cellSize;
        this.cols = Math.ceil(width / cellSize);
        this.rows = Math.ceil(height / cellSize);
        this.grid = new Array(this.cols * this.rows);
        this.clear();
    }

    clear() {
        for (let i = 0; i < this.grid.length; i++) {
            this.grid[i] = [];
        }
    }

    getCell(x, y) {
        const col = Math.floor(x / this.cellSize);
        const row = Math.floor(y / this.cellSize);
        if (col < 0 || col >= this.cols || row < 0 || row >= this.rows) return -1;
        return row * this.cols + col;
    }

    insert(obj) {
        const minCol = Math.floor(obj.x / this.cellSize);
        const maxCol = Math.floor((obj.x + obj.width) / this.cellSize);
        const minRow = Math.floor(obj.y / this.cellSize);
        const maxRow = Math.floor((obj.y + obj.height) / this.cellSize);

        for (let row = minRow; row <= maxRow; row++) {
            for (let col = minCol; col <= maxCol; col++) {
                const idx = row * this.cols + col;
                if (idx >= 0 && idx < this.grid.length) {
                    this.grid[idx].push(obj);
                }
            }
        }
    }

    query(obj) {
        const result = new Set();
        const minCol = Math.floor(obj.x / this.cellSize);
        const maxCol = Math.floor((obj.x + obj.width) / this.cellSize);
        const minRow = Math.floor(obj.y / this.cellSize);
        const maxRow = Math.floor((obj.y + obj.height) / this.cellSize);

        for (let row = minRow; row <= maxRow; row++) {
            for (let col = minCol; col <= maxCol; col++) {
                const idx = row * this.cols + col;
                if (idx >= 0 && idx < this.grid.length) {
                    for (const other of this.grid[idx]) {
                        if (other !== obj) result.add(other);
                    }
                }
            }
        }
        return result;
    }
}

空间分区 - 四叉树

四叉树递归地将空间划分为四个象限,适合对象分布不均匀的场景:

代码示例

class QuadTree {
    constructor(bounds, maxObjects = 10, maxLevels = 5, level = 0) {
        this.bounds = bounds;
        this.maxObjects = maxObjects;
        this.maxLevels = maxLevels;
        this.level = level;
        this.objects = [];
        this.nodes = [];
    }

    clear() {
        this.objects = [];
        for (const node of this.nodes) {
            node.clear();
        }
        this.nodes = [];
    }

    split() {
        const { x, y, width, height } = this.bounds;
        const hw = width / 2;
        const hh = height / 2;
        const nextLevel = this.level + 1;

        this.nodes = [
            new QuadTree({ x: x + hw, y: y, width: hw, height: hh }, this.maxObjects, this.maxLevels, nextLevel),
            new QuadTree({ x: x, y: y, width: hw, height: hh }, this.maxObjects, this.maxLevels, nextLevel),
            new QuadTree({ x: x, y: y + hh, width: hw, height: hh }, this.maxObjects, this.maxLevels, nextLevel),
            new QuadTree({ x: x + hw, y: y + hh, width: hw, height: hh }, this.maxObjects, this.maxLevels, nextLevel)
        ];
    }

    getIndex(obj) {
        const indices = [];
        const { x, y, width, height } = this.bounds;
        const midX = x + width / 2;
        const midY = y + height / 2;

        const top = obj.y < midY;
        const bottom = obj.y + obj.height > midY;
        const left = obj.x < midX;
        const right = obj.x + obj.width > midX;

        if (top && right) indices.push(0);
        if (top && left) indices.push(1);
        if (bottom && left) indices.push(2);
        if (bottom && right) indices.push(3);

        return indices;
    }

    insert(obj) {
        if (this.nodes.length > 0) {
            const indices = this.getIndex(obj);
            for (const idx of indices) {
                this.nodes[idx].insert(obj);
            }
            return;
        }

        this.objects.push(obj);

        if (this.objects.length > this.maxObjects && this.level < this.maxLevels) {
            if (this.nodes.length === 0) {
                this.split();
            }
            while (this.objects.length > 0) {
                const o = this.objects.pop();
                const indices = this.getIndex(o);
                for (const idx of indices) {
                    this.nodes[idx].insert(o);
                }
            }
        }
    }

    retrieve(obj) {
        const result = [...this.objects];
        if (this.nodes.length > 0) {
            const indices = this.getIndex(obj);
            for (const idx of indices) {
                result.push(...this.nodes[idx].retrieve(obj));
            }
        }
        return result;
    }
}

碰撞响应

碰撞响应决定了碰撞后对象如何运动:

代码示例

class CollisionResponse {
    // AABB碰撞分离
    static resolveAABB(a, b) {
        const overlapX = Math.min(a.x + a.width - b.x, b.x + b.width - a.x);
        const overlapY = Math.min(a.y + a.height - b.y, b.y + b.height - a.y);

        if (overlapX < overlapY) {
            if (a.x < b.x) { a.x -= overlapX / 2; b.x += overlapX / 2; }
            else { a.x += overlapX / 2; b.x -= overlapX / 2; }
        } else {
            if (a.y < b.y) { a.y -= overlapY / 2; b.y += overlapY / 2; }
            else { a.y += overlapY / 2; b.y -= overlapY / 2; }
        }
    }

    // 弹性碰撞(圆形)
    static elasticCollision(a, b, restitution = 1) {
        const dx = b.x - a.x;
        const dy = b.y - a.y;
        const dist = Math.sqrt(dx * dx + dy * dy);

        if (dist === 0) return;

        const nx = dx / dist;
        const ny = dy / dist;

        const dvx = a.vx - b.vx;
        const dvy = a.vy - b.vy;
        const dvn = dvx * nx + dvy * ny;

        if (dvn > 0) return; // 正在远离

        const j = -(1 + restitution) * dvn / 2;
        a.vx += j * nx;
        a.vy += j * ny;
        b.vx -= j * nx;
        b.vy -= j * ny;

        // 分离
        const overlap = (a.radius + b.radius) - dist;
        if (overlap > 0) {
            a.x -= nx * overlap / 2;
            a.y -= ny * overlap / 2;
            b.x += nx * overlap / 2;
            b.y += ny * overlap / 2;
        }
    }
}

语法与用法

碰撞检测综合演示

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>碰撞检测综合演示</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #0a0a1a; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; font-family: 'Segoe UI', sans-serif; color: #fff; }
        canvas { border: 2px solid #2a2a4a; border-radius: 8px; }
        .controls { margin-top: 12px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
        button { padding: 6px 16px; border: 1px solid #4a90d9; background: transparent; color: #4a90d9; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s; }
        button:hover { background: #4a90d922; }
        button.active { background: #4a90d9; color: #fff; }
    </style>
</head>
<body>
    <canvas id="gameCanvas" width="800" height="600"></canvas>
    <div class="controls">
        <button id="btnAABB" class="active">AABB碰撞</button>
        <button id="btnCircle">圆形碰撞</button>
        <button id="btnMixed">混合碰撞</button>
        <button id="btnSpatial">空间分区</button>
    </div>
    <script>
        class CollisionDemo {
            constructor() {
                this.canvas = document.getElementById('gameCanvas');
                this.ctx = this.canvas.getContext('2d');
                this.width = 800;
                this.height = 600;
                this.mode = 'aabb';
                this.objects = [];
                this.collisionPairs = [];
                this.quadTree = null;
                this.lastTime = 0;
                this.mouseX = 400;
                this.mouseY = 300;
                this.dragging = null;

                this.setupInput();
                this.setupButtons();
                this.generateObjects();
            }

            setupInput() {
                this.canvas.addEventListener('mousemove', (e) => {
                    const rect = this.canvas.getBoundingClientRect();
                    this.mouseX = (e.clientX - rect.left) * (this.width / rect.width);
                    this.mouseY = (e.clientY - rect.top) * (this.height / rect.height);
                    if (this.dragging) {
                        this.dragging.x = this.mouseX - this.dragging.offsetX;
                        this.dragging.y = this.mouseY - this.dragging.offsetY;
                    }
                });
                this.canvas.addEventListener('mousedown', (e) => {
                    for (const obj of this.objects) {
                        const dx = this.mouseX - obj.x - obj.width / 2;
                        const dy = this.mouseY - obj.y - obj.height / 2;
                        if (Math.abs(dx) < obj.width / 2 && Math.abs(dy) < obj.height / 2) {
                            this.dragging = obj;
                            this.dragging.offsetX = this.mouseX - obj.x;
                            this.dragging.offsetY = this.mouseY - obj.y;
                            break;
                        }
                    }
                });
                this.canvas.addEventListener('mouseup', () => { this.dragging = null; });
            }

            setupButtons() {
                const modes = { btnAABB: 'aabb', btnCircle: 'circle', btnMixed: 'mixed', btnSpatial: 'spatial' };
                for (const [id, mode] of Object.entries(modes)) {
                    document.getElementById(id).addEventListener('click', () => {
                        this.mode = mode;
                        document.querySelectorAll('button').forEach(b => b.classList.remove('active'));
                        document.getElementById(id).classList.add('active');
                        this.generateObjects();
                    });
                }
            }

            generateObjects() {
                this.objects = [];
                const count = this.mode === 'spatial' ? 80 : 20;
                for (let i = 0; i < count; i++) {
                    const size = 15 + Math.random() * 30;
                    const isCircle = this.mode === 'circle' || (this.mode === 'mixed' && Math.random() > 0.5);
                    this.objects.push({
                        x: 50 + Math.random() * (this.width - 100),
                        y: 50 + Math.random() * (this.height - 100),
                        width: size,
                        height: size,
                        radius: size / 2,
                        vx: (Math.random() - 0.5) * 150,
                        vy: (Math.random() - 0.5) * 150,
                        isCircle: isCircle,
                        color: isCircle ? '#4a90d9' : '#e94560',
                        colliding: false,
                        mass: size * size
                    });
                }
            }

            start() {
                this.lastTime = performance.now();
                requestAnimationFrame((t) => this.loop(t));
            }

            loop(currentTime) {
                const dt = Math.min((currentTime - this.lastTime) / 1000, 0.1);
                this.lastTime = currentTime;
                this.update(dt);
                this.render();
                requestAnimationFrame((t) => this.loop(t));
            }

            update(dt) {
                // 更新位置
                for (const obj of this.objects) {
                    if (obj === this.dragging) continue;
                    obj.x += obj.vx * dt;
                    obj.y += obj.vy * dt;

                    // 边界反弹
                    if (obj.x < 0) { obj.x = 0; obj.vx = Math.abs(obj.vx); }
                    if (obj.x + obj.width > this.width) { obj.x = this.width - obj.width; obj.vx = -Math.abs(obj.vx); }
                    if (obj.y < 0) { obj.y = 0; obj.vy = Math.abs(obj.vy); }
                    if (obj.y + obj.height > this.height) { obj.y = this.height - obj.height; obj.vy = -Math.abs(obj.vy); }

                    obj.colliding = false;
                }

                // 碰撞检测
                this.collisionPairs = [];

                if (this.mode === 'spatial') {
                    this.detectWithSpatial();
                } else {
                    this.detectBruteForce();
                }
            }

            detectBruteForce() {
                for (let i = 0; i < this.objects.length; i++) {
                    for (let j = i + 1; j < this.objects.length; j++) {
                        const a = this.objects[i];
                        const b = this.objects[j];
                        if (this.checkCollision(a, b)) {
                            a.colliding = true;
                            b.colliding = true;
                            this.collisionPairs.push([a, b]);
                            this.resolveCollision(a, b);
                        }
                    }
                }
            }

            detectWithSpatial() {
                // 构建四叉树
                this.quadTree = new QuadTree({ x: 0, y: 0, width: this.width, height: this.height }, 5, 4);
                for (const obj of this.objects) {
                    this.quadTree.insert(obj);
                }

                // 使用四叉树检测
                const checked = new Set();
                for (const obj of this.objects) {
                    const nearby = this.quadTree.retrieve(obj);
                    for (const other of nearby) {
                        if (other === obj) continue;
                        const key = obj.id < other.id ? `${obj.id}-${other.id}` : `${other.id}-${obj.id}`;
                        if (checked.has(key)) continue;
                        checked.add(key);

                        if (this.checkCollision(obj, other)) {
                            obj.colliding = true;
                            other.colliding = true;
                            this.collisionPairs.push([obj, other]);
                            this.resolveCollision(obj, other);
                        }
                    }
                }
            }

            checkCollision(a, b) {
                if (a.isCircle && b.isCircle) {
                    const dx = (a.x + a.radius) - (b.x + b.radius);
                    const dy = (a.y + a.radius) - (b.y + b.radius);
                    const distSq = dx * dx + dy * dy;
                    const radiusSum = a.radius + b.radius;
                    return distSq < radiusSum * radiusSum;
                } else if (a.isCircle || b.isCircle) {
                    const circle = a.isCircle ? a : b;
                    const rect = a.isCircle ? b : a;
                    const cx = circle.x + circle.radius;
                    const cy = circle.y + circle.radius;
                    const closestX = Math.max(rect.x, Math.min(cx, rect.x + rect.width));
                    const closestY = Math.max(rect.y, Math.min(cy, rect.y + rect.height));
                    const dx = cx - closestX;
                    const dy = cy - closestY;
                    return (dx * dx + dy * dy) < (circle.radius * circle.radius);
                } else {
                    return a.x < b.x + b.width && a.x + a.width > b.x &&
                           a.y < b.y + b.height && a.y + a.height > b.y;
                }
            }

            resolveCollision(a, b) {
                if (a === this.dragging || b === this.dragging) return;

                if (a.isCircle && b.isCircle) {
                    // 弹性碰撞
                    const dx = (b.x + b.radius) - (a.x + a.radius);
                    const dy = (b.y + b.radius) - (a.y + a.radius);
                    const dist = Math.sqrt(dx * dx + dy * dy);
                    if (dist === 0) return;

                    const nx = dx / dist;
                    const ny = dy / dist;
                    const dvx = a.vx - b.vx;
                    const dvy = a.vy - b.vy;
                    const dvn = dvx * nx + dvy * ny;
                    if (dvn > 0) return;

                    const restitution = 0.8;
                    const j = -(1 + restitution) * dvn / (1 / a.mass + 1 / b.mass);
                    a.vx += j * nx / a.mass;
                    a.vy += j * ny / a.mass;
                    b.vx -= j * nx / b.mass;
                    b.vy -= j * ny / b.mass;

                    // 分离
                    const overlap = (a.radius + b.radius) - dist;
                    if (overlap > 0) {
                        a.x -= nx * overlap / 2;
                        a.y -= ny * overlap / 2;
                        b.x += nx * overlap / 2;
                        b.y += ny * overlap / 2;
                    }
                } else {
                    // AABB碰撞响应
                    const overlapX = Math.min(a.x + a.width - b.x, b.x + b.width - a.x);
                    const overlapY = Math.min(a.y + a.height - b.y, b.y + b.height - a.y);

                    if (overlapX < overlapY) {
                        if (a.x + a.width / 2 < b.x + b.width / 2) {
                            a.x -= overlapX / 2; b.x += overlapX / 2;
                        } else {
                            a.x += overlapX / 2; b.x -= overlapX / 2;
                        }
                        const tempVx = a.vx;
                        a.vx = b.vx * 0.8;
                        b.vx = tempVx * 0.8;
                    } else {
                        if (a.y + a.height / 2 < b.y + b.height / 2) {
                            a.y -= overlapY / 2; b.y += overlapY / 2;
                        } else {
                            a.y += overlapY / 2; b.y -= overlapY / 2;
                        }
                        const tempVy = a.vy;
                        a.vy = b.vy * 0.8;
                        b.vy = tempVy * 0.8;
                    }
                }
            }

            render() {
                const ctx = this.ctx;
                ctx.fillStyle = '#0a0a1a';
                ctx.fillRect(0, 0, this.width, this.height);

                // 四叉树可视化
                if (this.mode === 'spatial' && this.quadTree) {
                    this.renderQuadTree(ctx, this.quadTree);
                }

                // 碰撞线
                for (const [a, b] of this.collisionPairs) {
                    ctx.strokeStyle = '#e9456044';
                    ctx.lineWidth = 1;
                    ctx.beginPath();
                    ctx.moveTo(a.x + a.width / 2, a.y + a.height / 2);
                    ctx.lineTo(b.x + b.width / 2, b.y + b.height / 2);
                    ctx.stroke();
                }

                // 对象
                for (const obj of this.objects) {
                    if (obj.isCircle) {
                        ctx.fillStyle = obj.colliding ? '#ff6b6b' : obj.color;
                        ctx.beginPath();
                        ctx.arc(obj.x + obj.radius, obj.y + obj.radius, obj.radius, 0, Math.PI * 2);
                        ctx.fill();
                        if (obj.colliding) {
                            ctx.strokeStyle = '#e94560';
                            ctx.lineWidth = 2;
                            ctx.stroke();
                        }
                    } else {
                        ctx.fillStyle = obj.colliding ? '#ff6b6b' : obj.color;
                        ctx.fillRect(obj.x, obj.y, obj.width, obj.height);
                        if (obj.colliding) {
                            ctx.strokeStyle = '#e94560';
                            ctx.lineWidth = 2;
                            ctx.strokeRect(obj.x, obj.y, obj.width, obj.height);
                        }
                    }
                }

                // HUD
                ctx.fillStyle = '#fff';
                ctx.font = '13px monospace';
                ctx.textAlign = 'left';
                const modeNames = { aabb: 'AABB碰撞', circle: '圆形碰撞', mixed: '混合碰撞', spatial: '空间分区' };
                ctx.fillText(`模式: ${modeNames[this.mode]}`, 10, 20);
                ctx.fillText(`对象: ${this.objects.length} | 碰撞对: ${this.collisionPairs.length}`, 10, 40);
                ctx.fillText('拖拽对象移动', 10, 580);
            }

            renderQuadTree(ctx, node) {
                ctx.strokeStyle = '#1a3a5a';
                ctx.lineWidth = 0.5;
                ctx.strokeRect(node.bounds.x, node.bounds.y, node.bounds.width, node.bounds.height);
                for (const child of node.nodes) {
                    this.renderQuadTree(ctx, child);
                }
            }
        }

        // 四叉树(简化版用于演示)
        class QuadTree {
            constructor(bounds, maxObjects, maxLevels, level = 0) {
                this.bounds = bounds;
                this.maxObjects = maxObjects;
                this.maxLevels = maxLevels;
                this.level = level;
                this.objects = [];
                this.nodes = [];
            }
            clear() { this.objects = []; this.nodes.forEach(n => n.clear()); this.nodes = []; }
            split() {
                const { x, y, width, height } = this.bounds;
                const hw = width / 2, hh = height / 2, nl = this.level + 1;
                this.nodes = [
                    new QuadTree({ x: x + hw, y, width: hw, height: hh }, this.maxObjects, this.maxLevels, nl),
                    new QuadTree({ x, y, width: hw, height: hh }, this.maxObjects, this.maxLevels, nl),
                    new QuadTree({ x, y: y + hh, width: hw, height: hh }, this.maxObjects, this.maxLevels, nl),
                    new QuadTree({ x: x + hw, y: y + hh, width: hw, height: hh }, this.maxObjects, this.maxLevels, nl)
                ];
            }
            getIndex(obj) {
                const midX = this.bounds.x + this.bounds.width / 2;
                const midY = this.bounds.y + this.bounds.height / 2;
                const indices = [];
                const top = obj.y < midY, bottom = obj.y + obj.height > midY;
                const left = obj.x < midX, right = obj.x + obj.width > midX;
                if (top && right) indices.push(0);
                if (top && left) indices.push(1);
                if (bottom && left) indices.push(2);
                if (bottom && right) indices.push(3);
                return indices;
            }
            insert(obj) {
                if (this.nodes.length > 0) {
                    for (const idx of this.getIndex(obj)) this.nodes[idx].insert(obj);
                    return;
                }
                this.objects.push(obj);
                if (this.objects.length > this.maxObjects && this.level < this.maxLevels) {
                    if (this.nodes.length === 0) this.split();
                    while (this.objects.length > 0) {
                        const o = this.objects.pop();
                        for (const idx of this.getIndex(o)) this.nodes[idx].insert(o);
                    }
                }
            }
            retrieve(obj) {
                let result = [...this.objects];
                if (this.nodes.length > 0) {
                    for (const idx of this.getIndex(obj)) result = result.concat(this.nodes[idx].retrieve(obj));
                }
                return result;
            }
        }

        const game = new CollisionDemo();
        // 给对象添加ID
        game.generateObjects = function() {
            this.objects = [];
            const count = this.mode === 'spatial' ? 80 : 20;
            for (let i = 0; i < count; i++) {
                const size = 15 + Math.random() * 30;
                const isCircle = this.mode === 'circle' || (this.mode === 'mixed' && Math.random() > 0.5);
                this.objects.push({
                    id: i,
                    x: 50 + Math.random() * (this.width - 100),
                    y: 50 + Math.random() * (this.height - 100),
                    width: size, height: size, radius: size / 2,
                    vx: (Math.random() - 0.5) * 150, vy: (Math.random() - 0.5) * 150,
                    isCircle, color: isCircle ? '#4a90d9' : '#e94560',
                    colliding: false, mass: size * size
                });
            }
        };
        game.generateObjects();
        game.start();
    </script>
</body>
</html>

浏览器兼容性

特性 Chrome Firefox Safari Edge 移动端Chrome 移动端Safari
Canvas getImageData 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
Canvas putImageData 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
OffscreenCanvas 完全支持 完全支持 不支持 完全支持 不支持 不支持
createImageBitmap 完全支持 完全支持 部分支持 完全支持 完全支持 部分支持
Web Workers 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持

注意事项与最佳实践

  1. 先宽后窄:先用简单的AABB/圆形检测做粗筛,再用精确方法做细检,减少计算量。

  2. 空间分区优化:当对象数量超过50时,使用空间分区(网格或四叉树)减少碰撞检测对数。

  3. 避免穿透:高速移动的小物体可能穿过薄障碍物,使用连续碰撞检测(CCD)或增大碰撞体。

  4. 碰撞分层:使用碰撞掩码/碰撞层,只检测需要交互的对象对。

  5. 固定时间步长:物理和碰撞检测使用固定时间步长,确保行为一致性。

  6. 碰撞回调:提供onCollisionEnter/onCollisionStay/onCollisionExit回调,便于游戏逻辑响应。

  7. 对象池管理碰撞:频繁创建销毁的对象使用对象池,避免GC导致的帧率波动。

  8. 调试可视化:开发阶段绘制碰撞体和碰撞法线,便于调试。

代码规范示例

代码示例

/**
 * 碰撞检测系统
 * 管理碰撞检测、响应和事件分发
 */
class CollisionSystem {
    /**
     * @param {Object} config - 配置
     * @param {number} [config.cellSize=100] - 空间分区网格大小
     * @param {number} [config.width=800] - 世界宽度
     * @param {number} [config.height=600] - 世界高度
     */
    constructor(config = {}) {
        this.cellSize = config.cellSize || 100;
        this.width = config.width || 800;
        this.height = config.height || 600;
        this.spatialGrid = new SpatialGrid(this.cellSize, this.width, this.height);
        this.collisionLayers = new Map();
        this.collisionCallbacks = [];
    }

    /**
     * 注册碰撞回调
     * @param {string} layerA - 层A名称
     * @param {string} layerB - 层B名称
     * @param {Function} callback - 碰撞回调 (a, b, collisionInfo) => void
     */
    onCollision(layerA, layerB, callback) {
        this.collisionCallbacks.push({ layerA, layerB, callback });
    }

    /**
     * 检测并处理所有碰撞
     * @param {Array} objects - 游戏对象数组
     * @param {number} dt - 帧间隔时间
     */
    detect(objects, dt) {
        // 重建空间分区
        this.spatialGrid.clear();
        for (const obj of objects) {
            this.spatialGrid.insert(obj);
        }

        // 检测碰撞
        const checked = new Set();
        for (const obj of objects) {
            const nearby = this.spatialGrid.query(obj);
            for (const other of nearby) {
                const key = obj.id < other.id ? `${obj.id}-${other.id}` : `${other.id}-${obj.id}`;
                if (checked.has(key)) continue;
                checked.add(key);

                const collision = this.testCollision(obj, other);
                if (collision) {
                    this.dispatchCallback(obj, other, collision);
                }
            }
        }
    }

    /**
     * 测试两个对象的碰撞
     * @private
     */
    testCollision(a, b) {
        if (a.shape === 'circle' && b.shape === 'circle') {
            return this.testCircleCircle(a, b);
        }
        return this.testAABB(a, b);
    }

    testAABB(a, b) {
        if (a.x < b.x + b.width && a.x + a.width > b.x &&
            a.y < b.y + b.height && a.y + a.height > b.y) {
            const overlapX = Math.min(a.x + a.width - b.x, b.x + b.width - a.x);
            const overlapY = Math.min(a.y + a.height - b.y, b.y + b.height - a.y);
            return { overlapX, overlapY, normal: overlapX < overlapY ? 'x' : 'y' };
        }
        return null;
    }

    testCircleCircle(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        const radiusSum = a.radius + b.radius;
        if (dist < radiusSum) {
            return { distance: dist, overlap: radiusSum - dist, nx: dx / dist, ny: dy / dist };
        }
        return null;
    }

    dispatchCallback(a, b, info) {
        for (const cb of this.collisionCallbacks) {
            if ((a.layer === cb.layerA && b.layer === cb.layerB) ||
                (a.layer === cb.layerB && b.layer === cb.layerA)) {
                cb.callback(a, b, info);
            }
        }
    }
}

常见问题与解决方案

问题1:高速物体穿透碰撞体

原因:物体移动速度过快,一帧内跨越了整个碰撞体。

解决方案:使用连续碰撞检测(CCD),或限制最大速度,或增大碰撞体厚度。

代码示例

// 简单的CCD:射线检测
function sweepTest(startX, startY, endX, endY, obstacle) {
    const steps = Math.ceil(Math.sqrt((endX-startX)**2 + (endY-startY)**2) / 2);
    for (let i = 0; i <= steps; i++) {
        const t = i / steps;
        const x = startX + (endX - startX) * t;
        const y = startY + (endY - startY) * t;
        if (pointInRect(x, y, obstacle)) return t;
    }
    return null;
}

问题2:碰撞检测对象过多导致性能差

原因:暴力检测O(n^2)复杂度,对象多时计算量爆炸。

解决方案:使用空间分区(网格或四叉树),将复杂度降低到接近O(n)。

问题3:碰撞后对象抖动

原因:碰撞分离不彻底,下一帧仍然碰撞,反复分离。

解决方案:确保分离量足够,或在分离后设置标志位跳过几帧检测。

问题4:圆形与矩形碰撞检测不准

原因:简单地将圆形当作AABB检测会丢失精度。

解决方案:使用专门的圆-矩形碰撞算法,找矩形上最近点再判断距离。

问题5:碰撞法线方向错误

原因:AABB碰撞法线判断逻辑有误。

解决方案:比较X和Y方向的重叠量,取较小重叠方向作为法线方向。

总结

本教程全面讲解了游戏碰撞检测的核心技术。我们学习了AABB碰撞、圆形碰撞、圆-矩形碰撞和像素级碰撞等多种检测算法,掌握了空间分区(网格和四叉树)来优化大量对象的碰撞检测,实现了碰撞响应和分离逻辑,了解了物理材质对碰撞效果的影响。

关键要点:

  • AABB碰撞最简单高效,适合大多数2D游戏

  • 圆形碰撞更精确,适合球形对象

  • 先粗后细的检测策略减少计算量

  • 空间分区将O(n^2)降低到接近O(n)

  • 碰撞响应需要同时处理速度变化和位置分离

  • 高速物体需要连续碰撞检测防止穿透

  • 碰撞分层避免不必要的检测

  • 开发阶段可视化碰撞体便于调试

掌握碰撞检测技术后,你将能够构建准确高效的游戏物理交互系统。

常见问题

什么是碰撞检测?

碰撞检测是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。

如何学习碰撞检测的实际应用?

教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。

碰撞检测有哪些注意事项?

常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。

碰撞检测适合初学者吗?

本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。

碰撞检测的核心要点是什么?

核心要点包括教程简介等内容,建议按教程顺序逐步学习。

标签: 碰撞检测 HTML5游戏 Canvas 粒子效果 物理引擎 内存管理 重力 精灵动画

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 游戏开发:游戏输入处理 - 从入门到实践详解 下一篇: 游戏开发:游戏物理基础 - 从入门到实践详解

poll相关推荐