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

游戏开发:游戏物理基础 - 从入门到实践详解

教程简介

物理模拟是让游戏世界感觉真实和有趣的关键因素。从简单的重力下落到复杂的弹跳和摩擦,物理系统赋予游戏对象符合直觉的运动行为。本教程将全面讲解游戏物理的基础知识,包括运动学(速度/加速度)、重力模拟、弹跳、摩擦力、简单物理引擎的实现,以及物理引擎的集成(Matter.js/Cannon.js),帮助你构建具有真实物理行为的游戏。

核心概念

运动学基础

运动学描述物体的运动状态,不涉及力的原因:

代码示例

class Kinematics {
    constructor() {
        this.position = { x: 0, y: 0 };
        this.velocity = { x: 0, y: 0 };
        this.acceleration = { x: 0, y: 0 };
    }

    update(dt) {
        // v = v0 + a * t
        this.velocity.x += this.acceleration.x * dt;
        this.velocity.y += this.acceleration.y * dt;

        // s = s0 + v * t
        this.position.x += this.velocity.x * dt;
        this.position.y += this.velocity.y * dt;
    }
}

重力模拟

重力是最基本的物理效果,让物体向下加速:

代码示例

const GRAVITY = 980; // 像素/秒^2

class PhysicsBody {
    constructor(x, y, width, height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.vx = 0;
        this.vy = 0;
        this.gravity = GRAVITY;
        this.isOnGround = false;
        this.restitution = 0.5; // 弹性系数
        this.friction = 0.8;    // 摩擦系数
    }

    update(dt) {
        // 应用重力
        this.vy += this.gravity * dt;

        // 更新位置
        this.x += this.vx * dt;
        this.y += this.vy * dt;

        // 地面碰撞
        if (this.y + this.height > groundY) {
            this.y = groundY - this.height;
            this.vy = -this.vy * this.restitution; // 弹跳
            this.vx *= this.friction; // 摩擦
            this.isOnGround = true;

            // 速度过小时停止弹跳
            if (Math.abs(this.vy) < 10) {
                this.vy = 0;
            }
        }
    }
}

弹跳

弹跳通过弹性系数(restitution)控制反弹高度:

代码示例

// 弹性系数范围:0(完全非弹性)到1(完全弹性)
// 弹跳高度 = 下落高度 * restitution^2

function applyBounce(body, surfaceNormal, restitution) {
    // 计算速度在法线方向的分量
    const dot = body.vx * surfaceNormal.x + body.vy * surfaceNormal.y;

    // 反射速度
    body.vx -= (1 + restitution) * dot * surfaceNormal.x;
    body.vy -= (1 + restitution) * dot * surfaceNormal.y;
}

摩擦力

摩擦力减缓物体在接触面上的运动:

代码示例

function applyFriction(body, friction) {
    if (body.isOnGround) {
        // 动摩擦
        if (Math.abs(body.vx) > 1) {
            body.vx *= friction;
        } else {
            body.vx = 0; // 静摩擦
        }
    }
}

简单物理引擎实现

代码示例

class SimplePhysicsEngine {
    constructor(width, height) {
        this.width = width;
        this.height = height;
        this.bodies = [];
        this.gravity = 980;
    }

    addBody(body) {
        this.bodies.push(body);
        return body;
    }

    removeBody(body) {
        const idx = this.bodies.indexOf(body);
        if (idx >= 0) this.bodies.splice(idx, 1);
    }

    update(dt) {
        for (const body of this.bodies) {
            if (body.isStatic) continue;

            // 应用重力
            body.vy += this.gravity * dt;

            // 应用阻力
            body.vx *= (1 - body.drag * dt);

            // 更新位置
            body.x += body.vx * dt;
            body.y += body.vy * dt;

            // 边界碰撞
            this.resolveBoundaries(body);

            // 更新旋转
            body.rotation += body.angularVelocity * dt;
        }

        // 碰撞检测与响应
        this.resolveCollisions();
    }

    resolveBoundaries(body) {
        if (body.x < 0) {
            body.x = 0;
            body.vx = -body.vx * body.restitution;
        }
        if (body.x + body.width > this.width) {
            body.x = this.width - body.width;
            body.vx = -body.vx * body.restitution;
        }
        if (body.y < 0) {
            body.y = 0;
            body.vy = -body.vy * body.restitution;
        }
        if (body.y + body.height > this.height) {
            body.y = this.height - body.height;
            body.vy = -body.vy * body.restitution;
            body.vx *= body.friction;
            body.isOnGround = Math.abs(body.vy) < 5;
            if (body.isOnGround) body.vy = 0;
        }
    }

    resolveCollisions() {
        for (let i = 0; i < this.bodies.length; i++) {
            for (let j = i + 1; j < this.bodies.length; j++) {
                const a = this.bodies[i];
                const b = this.bodies[j];
                if (a.isStatic && b.isStatic) continue;
                this.resolveBodyCollision(a, b);
            }
        }
    }

    resolveBodyCollision(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);

            if (overlapX < overlapY) {
                const sign = (a.x + a.width / 2) < (b.x + b.width / 2) ? -1 : 1;
                if (!a.isStatic) a.x += sign * overlapX / 2;
                if (!b.isStatic) b.x -= sign * overlapX / 2;

                const restitution = Math.min(a.restitution, b.restitution);
                if (!a.isStatic && !b.isStatic) {
                    const tempVx = a.vx;
                    a.vx = b.vx * restitution;
                    b.vx = tempVx * restitution;
                } else if (a.isStatic) {
                    b.vx = -b.vx * restitution;
                } else {
                    a.vx = -a.vx * restitution;
                }
            } else {
                const sign = (a.y + a.height / 2) < (b.y + b.height / 2) ? -1 : 1;
                if (!a.isStatic) a.y += sign * overlapY / 2;
                if (!b.isStatic) b.y -= sign * overlapY / 2;

                const restitution = Math.min(a.restitution, b.restitution);
                if (!a.isStatic && !b.isStatic) {
                    const tempVy = a.vy;
                    a.vy = b.vy * restitution;
                    b.vy = tempVy * restitution;
                } else if (a.isStatic) {
                    b.vy = -b.vy * restitution;
                } else {
                    a.vy = -a.vy * restitution;
                }
            }
        }
    }
}

Matter.js集成

Matter.js是一个功能强大的2D物理引擎:

代码示例

// 引入Matter.js
// <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>

const { Engine, Render, Runner, Bodies, Composite, Events } = Matter;

// 创建引擎
const engine = Engine.create();
const render = Render.create({
    element: document.body,
    engine: engine,
    canvas: document.getElementById('gameCanvas'),
    options: { width: 800, height: 600, wireframes: false }
});

// 创建地面和墙壁
const ground = Bodies.rectangle(400, 580, 800, 40, { isStatic: true });
const wallLeft = Bodies.rectangle(10, 300, 20, 600, { isStatic: true });
const wallRight = Bodies.rectangle(790, 300, 20, 600, { isStatic: true });

Composite.add(engine.world, [ground, wallLeft, wallRight]);

// 创建动态物体
for (let i = 0; i < 20; i++) {
    const body = Bodies.rectangle(
        200 + Math.random() * 400,
        50 + Math.random() * 200,
        30 + Math.random() * 40,
        30 + Math.random() * 40,
        { restitution: 0.6, friction: 0.3 }
    );
    Composite.add(engine.world, body);
}

// 运行
Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);

语法与用法

完整的物理引擎演示

代码示例

<!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; }
        button:hover { background: #4a90d922; }
        label { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #aaa; }
        input[type="range"] { width: 80px; }
    </style>
</head>
<body>
    <canvas id="gameCanvas" width="800" height="600"></canvas>
    <div class="controls">
        <button id="addBox">添加方块</button>
        <button id="addBall">添加球</button>
        <button id="addPlatform">添加平台</button>
        <button id="reset">重置</button>
        <label>重力: <input type="range" id="gravitySlider" min="0" max="2000" value="980"> <span id="gravityVal">980</span></label>
        <label>弹性: <input type="range" id="bounceSlider" min="0" max="100" value="60"> <span id="bounceVal">0.6</span></label>
    </div>
    <script>
        class PhysicsBody {
            constructor(x, y, width, height, options = {}) {
                this.x = x;
                this.y = y;
                this.width = width;
                this.height = height;
                this.vx = options.vx || 0;
                this.vy = options.vy || 0;
                this.restitution = options.restitution !== undefined ? options.restitution : 0.6;
                this.friction = options.friction !== undefined ? options.friction : 0.95;
                this.isStatic = options.isStatic || false;
                this.isCircle = options.isCircle || false;
                this.radius = options.radius || width / 2;
                this.mass = options.mass || (width * height);
                this.color = options.color || this.randomColor();
                this.rotation = 0;
                this.angularVelocity = options.angularVelocity || 0;
                this.isOnGround = false;
                this.id = PhysicsBody.nextId++;
            }

            randomColor() {
                const colors = ['#4a90d9', '#e94560', '#16c79a', '#f0a500', '#533483', '#ff6b6b', '#4ecdc4'];
                return colors[Math.floor(Math.random() * colors.length)];
            }
        }
        PhysicsBody.nextId = 0;

        class PhysicsEngine {
            constructor(width, height) {
                this.width = width;
                this.height = height;
                this.bodies = [];
                this.gravity = 980;
                this.platforms = [];
            }

            addBody(body) {
                this.bodies.push(body);
                return body;
            }

            addPlatform(x, y, width, height) {
                const platform = new PhysicsBody(x, y, width, height, {
                    isStatic: true,
                    color: '#2a4a6a'
                });
                this.platforms.push(platform);
                return platform;
            }

            clear() {
                this.bodies = [];
                this.platforms = [];
            }

            update(dt) {
                const fixedDt = 1 / 60;
                this.step(fixedDt);
            }

            step(dt) {
                for (const body of this.bodies) {
                    if (body.isStatic) continue;

                    // 重力
                    body.vy += this.gravity * dt;

                    // 空气阻力
                    body.vx *= 0.999;

                    // 更新位置
                    body.x += body.vx * dt;
                    body.y += body.vy * dt;

                    // 旋转
                    body.rotation += body.angularVelocity * dt;
                    body.angularVelocity *= 0.99;

                    body.isOnGround = false;

                    // 边界碰撞
                    this.resolveBoundary(body);

                    // 平台碰撞
                    for (const platform of this.platforms) {
                        this.resolvePlatformCollision(body, platform);
                    }
                }

                // 动态体之间碰撞
                this.resolveBodyCollisions();
            }

            resolveBoundary(body) {
                const r = body.isCircle ? body.radius : 0;
                if (body.x - r < 0) {
                    body.x = r;
                    body.vx = Math.abs(body.vx) * body.restitution;
                    body.angularVelocity += body.vy * 0.01;
                }
                if (body.x + body.width + r > this.width) {
                    body.x = this.width - body.width - r;
                    body.vx = -Math.abs(body.vx) * body.restitution;
                    body.angularVelocity -= body.vy * 0.01;
                }
                if (body.y - r < 0) {
                    body.y = r;
                    body.vy = Math.abs(body.vy) * body.restitution;
                }
                if (body.y + body.height > this.height) {
                    body.y = this.height - body.height;
                    body.vy = -Math.abs(body.vy) * body.restitution;
                    body.vx *= body.friction;
                    body.isOnGround = Math.abs(body.vy) < 5;
                    if (body.isOnGround) {
                        body.vy = 0;
                        body.angularVelocity *= 0.9;
                    }
                }
            }

            resolvePlatformCollision(body, platform) {
                if (body.isCircle) {
                    this.resolveCircleRect(body, platform);
                } else {
                    this.resolveRectRect(body, platform);
                }
            }

            resolveRectRect(body, platform) {
                if (body.x < platform.x + platform.width &&
                    body.x + body.width > platform.x &&
                    body.y < platform.y + platform.height &&
                    body.y + body.height > platform.y) {

                    const overlapLeft = body.x + body.width - platform.x;
                    const overlapRight = platform.x + platform.width - body.x;
                    const overlapTop = body.y + body.height - platform.y;
                    const overlapBottom = platform.y + platform.height - body.y;

                    const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);

                    if (minOverlap === overlapTop && body.vy > 0) {
                        body.y = platform.y - body.height;
                        body.vy = -body.vy * body.restitution;
                        body.vx *= body.friction;
                        body.isOnGround = Math.abs(body.vy) < 5;
                        if (body.isOnGround) body.vy = 0;
                    } else if (minOverlap === overlapBottom && body.vy < 0) {
                        body.y = platform.y + platform.height;
                        body.vy = -body.vy * body.restitution;
                    } else if (minOverlap === overlapLeft) {
                        body.x = platform.x - body.width;
                        body.vx = -Math.abs(body.vx) * body.restitution;
                    } else if (minOverlap === overlapRight) {
                        body.x = platform.x + platform.width;
                        body.vx = Math.abs(body.vx) * body.restitution;
                    }
                }
            }

            resolveCircleRect(circle, rect) {
                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;
                const dist = Math.sqrt(dx * dx + dy * dy);

                if (dist < circle.radius && dist > 0) {
                    const nx = dx / dist;
                    const ny = dy / dist;
                    const overlap = circle.radius - dist;

                    circle.x += nx * overlap;
                    circle.y += ny * overlap;

                    const dot = circle.vx * nx + circle.vy * ny;
                    circle.vx -= (1 + circle.restitution) * dot * nx;
                    circle.vy -= (1 + circle.restitution) * dot * ny;

                    if (ny < -0.5) {
                        circle.isOnGround = true;
                        circle.vx *= circle.friction;
                    }

                    circle.angularVelocity += (circle.vx * ny - circle.vy * nx) * 0.01;
                }
            }

            resolveBodyCollisions() {
                for (let i = 0; i < this.bodies.length; i++) {
                    for (let j = i + 1; j < this.bodies.length; j++) {
                        const a = this.bodies[i];
                        const b = this.bodies[j];
                        if (a.isStatic && b.isStatic) continue;

                        if (a.isCircle && b.isCircle) {
                            this.resolveCircleCircle(a, b);
                        } else if (a.isCircle || b.isCircle) {
                            const circle = a.isCircle ? a : b;
                            const rect = a.isCircle ? b : a;
                            this.resolveCircleRectDynamic(circle, rect);
                        } else {
                            this.resolveRectRectDynamic(a, b);
                        }
                    }
                }
            }

            resolveCircleCircle(a, b) {
                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);
                const radiusSum = a.radius + b.radius;

                if (dist < radiusSum && dist > 0) {
                    const nx = dx / dist;
                    const ny = dy / dist;
                    const overlap = radiusSum - dist;

                    const totalMass = a.mass + b.mass;
                    if (!a.isStatic) { a.x -= nx * overlap * (b.mass / totalMass); a.y -= ny * overlap * (b.mass / totalMass); }
                    if (!b.isStatic) { b.x += nx * overlap * (a.mass / totalMass); b.y += ny * overlap * (a.mass / totalMass); }

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

                    if (dvn > 0) {
                        const restitution = Math.min(a.restitution, b.restitution);
                        const j = -(1 + restitution) * dvn / (1 / a.mass + 1 / b.mass);
                        if (!a.isStatic) { a.vx += j * nx / a.mass; a.vy += j * ny / a.mass; }
                        if (!b.isStatic) { b.vx -= j * nx / b.mass; b.vy -= j * ny / b.mass; }
                    }
                }
            }

            resolveCircleRectDynamic(circle, rect) {
                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;
                const dist = Math.sqrt(dx * dx + dy * dy);

                if (dist < circle.radius && dist > 0) {
                    const nx = dx / dist;
                    const ny = dy / dist;
                    const overlap = circle.radius - dist;

                    if (!circle.isStatic) { circle.x += nx * overlap * 0.5; circle.y += ny * overlap * 0.5; }
                    if (!rect.isStatic) { rect.x -= nx * overlap * 0.5; rect.y -= ny * overlap * 0.5; }

                    const restitution = Math.min(circle.restitution, rect.restitution);
                    const dvx = circle.vx - rect.vx;
                    const dvy = circle.vy - rect.vy;
                    const dvn = dvx * nx + dvy * ny;

                    if (dvn > 0) {
                        const j = -(1 + restitution) * dvn / 2;
                        if (!circle.isStatic) { circle.vx += j * nx; circle.vy += j * ny; }
                        if (!rect.isStatic) { rect.vx -= j * nx; rect.vy -= j * ny; }
                    }
                }
            }

            resolveRectRectDynamic(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);

                    if (overlapX < overlapY) {
                        const sign = (a.x + a.width / 2) < (b.x + b.width / 2) ? -1 : 1;
                        if (!a.isStatic) a.x += sign * overlapX * 0.5;
                        if (!b.isStatic) b.x -= sign * overlapX * 0.5;
                        const restitution = Math.min(a.restitution, b.restitution);
                        const tempVx = a.vx;
                        if (!a.isStatic) a.vx = b.vx * restitution;
                        if (!b.isStatic) b.vx = tempVx * restitution;
                    } else {
                        const sign = (a.y + a.height / 2) < (b.y + b.height / 2) ? -1 : 1;
                        if (!a.isStatic) a.y += sign * overlapY * 0.5;
                        if (!b.isStatic) b.y -= sign * overlapY * 0.5;
                        const restitution = Math.min(a.restitution, b.restitution);
                        const tempVy = a.vy;
                        if (!a.isStatic) a.vy = b.vy * restitution;
                        if (!b.isStatic) b.vy = tempVy * restitution;
                    }
                }
            }
        }

        // ========== 主程序 ==========
        class PhysicsDemo {
            constructor() {
                this.canvas = document.getElementById('gameCanvas');
                this.ctx = this.canvas.getContext('2d');
                this.width = 800;
                this.height = 600;

                this.engine = new PhysicsEngine(this.width, this.height);
                this.setupDefaultScene();
                this.setupControls();

                this.lastTime = 0;
                this.mouseX = 0;
                this.mouseY = 0;
                this.dragBody = null;
                this.dragOffsetX = 0;
                this.dragOffsetY = 0;
            }

            setupDefaultScene() {
                // 默认平台
                this.engine.addPlatform(200, 450, 200, 15);
                this.engine.addPlatform(450, 350, 200, 15);
                this.engine.addPlatform(100, 250, 150, 15);
                this.engine.addPlatform(550, 200, 180, 15);

                // 默认物体
                for (let i = 0; i < 5; i++) {
                    this.engine.addBody(new PhysicsBody(
                        200 + Math.random() * 400, 50 + Math.random() * 100,
                        25 + Math.random() * 25, 25 + Math.random() * 25,
                        { restitution: 0.6, angularVelocity: (Math.random() - 0.5) * 2 }
                    ));
                }
                for (let i = 0; i < 3; i++) {
                    const r = 12 + Math.random() * 15;
                    this.engine.addBody(new PhysicsBody(
                        250 + Math.random() * 300, 30 + Math.random() * 80,
                        r * 2, r * 2,
                        { isCircle: true, radius: r, restitution: 0.7, angularVelocity: (Math.random() - 0.5) * 3 }
                    ));
                }
            }

            setupControls() {
                document.getElementById('addBox').addEventListener('click', () => {
                    this.engine.addBody(new PhysicsBody(
                        300 + Math.random() * 200, 30,
                        25 + Math.random() * 30, 25 + Math.random() * 30,
                        { restitution: parseFloat(document.getElementById('bounceSlider').value) / 100,
                          angularVelocity: (Math.random() - 0.5) * 3 }
                    ));
                });

                document.getElementById('addBall').addEventListener('click', () => {
                    const r = 12 + Math.random() * 18;
                    this.engine.addBody(new PhysicsBody(
                        300 + Math.random() * 200, 30,
                        r * 2, r * 2,
                        { isCircle: true, radius: r, restitution: parseFloat(document.getElementById('bounceSlider').value) / 100,
                          angularVelocity: (Math.random() - 0.5) * 3 }
                    ));
                });

                document.getElementById('addPlatform').addEventListener('click', () => {
                    this.engine.addPlatform(100 + Math.random() * 500, 200 + Math.random() * 300, 100 + Math.random() * 150, 15);
                });

                document.getElementById('reset').addEventListener('click', () => {
                    this.engine.clear();
                    this.setupDefaultScene();
                });

                document.getElementById('gravitySlider').addEventListener('input', (e) => {
                    this.engine.gravity = parseInt(e.target.value);
                    document.getElementById('gravityVal').textContent = e.target.value;
                });

                document.getElementById('bounceSlider').addEventListener('input', (e) => {
                    document.getElementById('bounceVal').textContent = (parseInt(e.target.value) / 100).toFixed(2);
                });

                // 拖拽
                this.canvas.addEventListener('mousedown', (e) => {
                    const rect = this.canvas.getBoundingClientRect();
                    const mx = (e.clientX - rect.left) * (this.width / rect.width);
                    const my = (e.clientY - rect.top) * (this.height / rect.height);
                    for (const body of this.engine.bodies) {
                        if (body.isStatic) continue;
                        if (body.isCircle) {
                            const dx = mx - (body.x + body.radius);
                            const dy = my - (body.y + body.radius);
                            if (dx * dx + dy * dy < body.radius * body.radius) {
                                this.dragBody = body;
                                this.dragOffsetX = mx - body.x;
                                this.dragOffsetY = my - body.y;
                                break;
                            }
                        } else {
                            if (mx > body.x && mx < body.x + body.width && my > body.y && my < body.y + body.height) {
                                this.dragBody = body;
                                this.dragOffsetX = mx - body.x;
                                this.dragOffsetY = my - body.y;
                                break;
                            }
                        }
                    }
                });

                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.dragBody) {
                        const targetX = this.mouseX - this.dragOffsetX;
                        const targetY = this.mouseY - this.dragOffsetY;
                        this.dragBody.vx = (targetX - this.dragBody.x) * 10;
                        this.dragBody.vy = (targetY - this.dragBody.y) * 10;
                        this.dragBody.x = targetX;
                        this.dragBody.y = targetY;
                    }
                });

                this.canvas.addEventListener('mouseup', () => {
                    this.dragBody = null;
                });
            }

            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.engine.update(dt);
                this.render();

                requestAnimationFrame((t) => this.loop(t));
            }

            render() {
                const ctx = this.ctx;

                // 背景
                ctx.fillStyle = '#0a0a1a';
                ctx.fillRect(0, 0, this.width, this.height);

                // 网格
                ctx.strokeStyle = '#0f0f2a';
                ctx.lineWidth = 0.5;
                for (let x = 0; x < this.width; x += 50) {
                    ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, this.height); ctx.stroke();
                }
                for (let y = 0; y < this.height; y += 50) {
                    ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(this.width, y); ctx.stroke();
                }

                // 平台
                for (const p of this.engine.platforms) {
                    ctx.fillStyle = p.color;
                    ctx.fillRect(p.x, p.y, p.width, p.height);
                    ctx.fillStyle = '#4a90d9';
                    ctx.fillRect(p.x, p.y, p.width, 3);
                }

                // 动态体
                for (const body of this.engine.bodies) {
                    if (body.isCircle) {
                        ctx.save();
                        ctx.translate(body.x + body.radius, body.y + body.radius);
                        ctx.rotate(body.rotation);
                        ctx.fillStyle = body.color;
                        ctx.beginPath();
                        ctx.arc(0, 0, body.radius, 0, Math.PI * 2);
                        ctx.fill();
                        // 旋转指示线
                        ctx.strokeStyle = 'rgba(255,255,255,0.4)';
                        ctx.lineWidth = 2;
                        ctx.beginPath();
                        ctx.moveTo(0, 0);
                        ctx.lineTo(body.radius * 0.7, 0);
                        ctx.stroke();
                        ctx.restore();
                    } else {
                        ctx.save();
                        ctx.translate(body.x + body.width / 2, body.y + body.height / 2);
                        ctx.rotate(body.rotation);
                        ctx.fillStyle = body.color;
                        ctx.fillRect(-body.width / 2, -body.height / 2, body.width, body.height);
                        ctx.restore();
                    }
                }

                // 拖拽线
                if (this.dragBody) {
                    ctx.strokeStyle = '#fff4';
                    ctx.lineWidth = 1;
                    ctx.setLineDash([4, 4]);
                    ctx.beginPath();
                    ctx.moveTo(this.mouseX, this.mouseY);
                    ctx.lineTo(this.dragBody.x + this.dragBody.width / 2, this.dragBody.y + this.dragBody.height / 2);
                    ctx.stroke();
                    ctx.setLineDash([]);
                }

                // HUD
                ctx.fillStyle = '#fff';
                ctx.font = '13px monospace';
                ctx.textAlign = 'left';
                ctx.fillText(`物体: ${this.engine.bodies.length} | 平台: ${this.engine.platforms.length}`, 10, 20);
                ctx.fillText(`重力: ${this.engine.gravity}`, 10, 40);
                ctx.fillText('拖拽物体移动', 10, 580);
            }
        }

        const game = new PhysicsDemo();
        game.start();
    </script>
</body>
</html>

浏览器兼容性

特性 Chrome Firefox Safari Edge 移动端Chrome 移动端Safari
Canvas 2D 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
requestAnimationFrame 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
performance.now() 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
Web Workers 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持
SharedArrayBuffer 完全支持 完全支持 部分支持 完全支持 不支持 不支持
WASM 完全支持 完全支持 完全支持 完全支持 完全支持 完全支持

注意事项与最佳实践

  1. 使用固定时间步长:物理模拟必须使用固定时间步长(通常1/60秒),确保不同帧率下物理行为一致。

  2. 限制最大速度:防止物体速度过大导致穿透碰撞体或数值溢出。

  3. 弹性系数范围:restitution应在0到1之间,超过1会导致能量增加(超弹性),物理行为不稳定。

  4. 多次迭代求解:碰撞响应可能一次无法完全分离,需要多次迭代。

  5. 物理与渲染分离:物理更新使用固定步长,渲染使用实际帧时间插值。

  6. 避免每帧创建对象:物理计算中避免创建临时对象,减少GC压力。

  7. 使用Matter.js等成熟引擎:对于复杂物理需求,使用Matter.js比自己实现更可靠高效。

  8. 调试可视化:绘制速度向量、碰撞法线、包围盒等辅助调试。

代码规范示例

代码示例

/**
 * 物理体组件
 * 描述游戏对象的物理属性
 */
class PhysicsComponent {
    /**
     * @param {Object} config - 物理配置
     * @param {boolean} [config.isStatic=false] - 是否为静态体
     * @param {number} [config.mass=1] - 质量
     * @param {number} [config.restitution=0.5] - 弹性系数 (0-1)
     * @param {number} [config.friction=0.3] - 摩擦系数
     * @param {number} [config.drag=0.01] - 空气阻力
     */
    constructor(config = {}) {
        this.isStatic = config.isStatic || false;
        this.mass = config.isStatic ? Infinity : (config.mass || 1);
        this.invMass = this.isStatic ? 0 : (1 / this.mass);
        this.restitution = Math.max(0, Math.min(1, config.restitution || 0.5));
        this.friction = Math.max(0, Math.min(1, config.friction || 0.3));
        this.drag = config.drag || 0.01;

        this.velocity = { x: 0, y: 0 };
        this.acceleration = { x: 0, y: 0 };
        this.angularVelocity = 0;
        this.force = { x: 0, y: 0 };
    }

    /**
     * 施加力
     * @param {number} fx - X方向力
     * @param {number} fy - Y方向力
     */
    applyForce(fx, fy) {
        this.force.x += fx;
        this.force.y += fy;
    }

    /**
     * 施加冲量
     * @param {number} ix - X方向冲量
     * @param {number} iy - Y方向冲量
     */
    applyImpulse(ix, iy) {
        this.velocity.x += ix * this.invMass;
        this.velocity.y += iy * this.invMass;
    }

    /**
     * 物理更新
     * @param {number} dt - 固定时间步长
     */
    integrate(dt) {
        if (this.isStatic) return;

        // a = F/m
        this.acceleration.x = this.force.x * this.invMass;
        this.acceleration.y = this.force.y * this.invMass;

        // v += a * dt
        this.velocity.x += this.acceleration.x * dt;
        this.velocity.y += this.acceleration.y * dt;

        // 阻力
        this.velocity.x *= (1 - this.drag);
        this.velocity.y *= (1 - this.drag);

        // 清除力
        this.force.x = 0;
        this.force.y = 0;
    }
}

常见问题与解决方案

问题1:物体穿透地面

原因:速度过快或时间步长过大,一帧内物体跨越了碰撞体。

解决方案:使用固定时间步长,限制最大速度,或实现连续碰撞检测。

问题2:物体堆叠时抖动

原因:碰撞分离不彻底,或重力持续作用导致反复碰撞。

解决方案:增加碰撞迭代次数,设置速度阈值(低于阈值时归零),使用睡眠机制。

问题3:物理模拟不稳定

原因:时间步长过大、弹性系数过高、力计算错误。

解决方案:使用固定时间步长,限制弹性系数不超过1,检查力的方向和大小。

问题4:圆形物体滚动不自然

原因:没有正确实现角速度和摩擦力矩。

解决方案:在碰撞时根据切线速度计算角速度变化,添加滚动摩擦。

问题5:Matter.js性能问题

原因:物体过多、碰撞分类不当、渲染开销大。

解决方案:使用碰撞过滤减少检测对数,启用睡眠机制,使用wireframe模式调试。

总结

本教程全面讲解了游戏物理的基础知识。我们学习了运动学的基本公式(速度、加速度、位移的关系),实现了重力模拟和弹跳效果,掌握了摩擦力的应用,构建了一个完整的简单物理引擎(支持矩形和圆形碰撞、弹性碰撞响应、平台碰撞),并了解了如何集成Matter.js等专业物理引擎。

关键要点:

  • 物理模拟必须使用固定时间步长确保行为一致

  • 重力是持续向下的加速度

  • 弹性系数控制反弹高度,范围0-1

  • 摩擦力减缓接触面上的运动

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

  • 复杂物理需求建议使用Matter.js等成熟引擎

  • 调试时可视化速度向量和碰撞法线

  • 限制最大速度防止穿透和数值不稳定

掌握物理基础后,你将能够为游戏添加真实的物理交互,让游戏世界更加生动有趣。

常见问题

什么是游戏物理基础?

游戏物理基础是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。

如何学习游戏物理基础的实际应用?

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

游戏物理基础有哪些注意事项?

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

游戏物理基础适合初学者吗?

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

游戏物理基础的核心要点是什么?

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

标签: 碰撞检测 HTML5游戏 Canvas 物理引擎 内存管理 重力 场景管理 帧率优化

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

本文涉及AI创作

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

list快速访问

上一篇: 游戏开发:碰撞检测 - 从入门到实践详解 下一篇: 游戏开发:游戏音效与Web Audio - 从入门到实践详解

poll相关推荐