强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Pixi.js 游戏开发教程 / Pixi.js 物理引擎集成(Matter.js)

19. 物理引擎集成(Matter.js)

概述

Pixi.js 负责渲染,但不包含物理模拟。Matter.js 是轻量级的 2D 刚体物理引擎,与 Pixi.js 集成可以实现物理驱动的游戏玩法。

Matter.js 简介与安装

npm install matter-js
npm install @types/matter-js  # TypeScript 类型

Matter.js 核心模块

模块说明
Engine物理引擎实例,管理物理世界
World物理世界容器(已废弃,直接用 Engine)
Body刚体
Bodies创建刚体的工厂方法
Composite复合体/容器
Constraint约束(弹簧、铰链等)
Runner物理循环驱动
Events事件系统
import Matter from 'matter-js';

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

// 创建物理引擎
const engine = Engine.create({
    gravity: { x: 0, y: 1 }, // 重力
});

// 创建物理世界运行器
const runner = Runner.create();
Runner.run(runner, engine);

刚体创建

基本形状

// 矩形刚体
const box = Bodies.rectangle(x, y, width, height, {
    isStatic: false,       // 是否静态
    mass: 1,               // 质量
    friction: 0.3,         // 摩擦力
    restitution: 0.6,      // 弹性
    label: 'box',          // 标签(用于碰撞过滤)
});

// 圆形刚体
const circle = Bodies.circle(x, y, radius, {
    restitution: 0.8,
    friction: 0.1,
});

// 多边形刚体
const polygon = Bodies.polygon(x, y, 6, radius); // 六边形

// 静态地面
const ground = Bodies.rectangle(400, 580, 800, 40, {
    isStatic: true,
    friction: 0.8,
});

// 添加到物理世界
Composite.add(engine.world, [box, circle, polygon, ground]);

刚体选项详解

选项类型默认值说明
isStaticbooleanfalse静态物体不受力和碰撞影响
isSensorbooleanfalse传感器只检测碰撞不产生物理响应
massnumber自动质量
frictionnumber0.1摩擦力 (0-1)
frictionAirnumber0.01空气阻力
restitutionnumber0弹性 (0-1)
collisionFilterobject-碰撞过滤
labelstring''标签
anglenumber0初始角度(弧度)
velocity{x, y}{0,0}初始速度

约束(Constraint)

约束用于连接两个物体或固定物体到空间中的点:

// 连接两个物体的弹簧
const spring = Constraint.create({
    bodyA: body1,
    bodyB: body2,
    length: 100,           // 自然长度
    stiffness: 0.01,       // 刚度 (0-1)
    damping: 0.1,          // 阻尼
    render: { visible: true }, // 调试渲染
});

// 固定到空间点(摆锤)
const pendulum = Constraint.create({
    pointA: { x: 400, y: 100 }, // 固定点
    bodyB: ball,
    length: 150,
    stiffness: 1,  // 刚性连接
});

// 距离约束(链条效果)
function createChain(links, startX, startY) {
    const constraints = [];
    for (let i = 0; i < links.length - 1; i++) {
        constraints.push(Constraint.create({
            bodyA: links[i],
            bodyB: links[i + 1],
            length: 30,
            stiffness: 0.9,
        }));
    }
    return constraints;
}

物理世界配置

const engine = Engine.create({
    gravity: { x: 0, y: 1, scale: 0.001 },
    timing: { timeScale: 1 },
    constraintIterations: 2,   // 约束求解迭代次数
    positionIterations: 6,     // 位置求解迭代次数
    velocityIterations: 4,     // 速度求解迭代次数
});

// 时间步控制(固定时间步更稳定)
const FIXED_DT = 1000 / 60; // 60 Hz

app.ticker.add(() => {
    Engine.update(engine, FIXED_DT);
});

// 暂停/恢复
runner.enabled = false;  // 暂停
runner.enabled = true;   // 恢复

// 修改重力
engine.gravity.y = 0.5;   // 减小重力(月球效果)
engine.gravity.x = 0;     // 水平重力(侧面滚动游戏)

碰撞检测与响应

// 碰撞事件
Events.on(engine, 'collisionStart', (event) => {
    for (const pair of event.pairs) {
        const { bodyA, bodyB } = pair;

        // 根据标签判断碰撞类型
        if (bodyA.label === 'bullet' && bodyB.label === 'enemy') {
            onBulletHitEnemy(bodyA, bodyB);
        }

        if (bodyA.label === 'player' && bodyB.label === 'coin') {
            onCollectCoin(bodyA, bodyB);
        }
    }
});

// 碰撞进行中
Events.on(engine, 'collisionActive', (event) => {
    // 角色站在地面上时可以跳跃
    for (const pair of event.pairs) {
        if (isPlayerGround(pair)) {
            player.canJump = true;
        }
    }
});

// 碰撞结束
Events.on(engine, 'collisionEnd', (event) => {
    // 离开地面
    for (const pair of event.pairs) {
        if (isPlayerGround(pair)) {
            player.canJump = false;
        }
    }
});

function isPlayerGround(pair) {
    const { bodyA, bodyB } = pair;
    return (bodyA.label === 'player' && bodyB.isStatic) ||
           (bodyB.label === 'player' && bodyA.isStatic);
}

物理材质

摩擦力

// 冰面:低摩擦
const ice = Bodies.rectangle(400, 500, 300, 20, {
    isStatic: true,
    friction: 0.02,
    label: 'ice',
});

// 橡胶:高摩擦高弹性
const rubber = Bodies.rectangle(400, 500, 300, 20, {
    isStatic: true,
    friction: 0.9,
    restitution: 0.9,
    label: 'rubber',
});

弹性材质预设

const MATERIALS = {
    wood:    { friction: 0.5, restitution: 0.3 },
    metal:   { friction: 0.3, restitution: 0.2 },
    rubber:  { friction: 0.9, restitution: 0.8 },
    ice:     { friction: 0.02, restitution: 0.1 },
    bouncy:  { friction: 0.1, restitution: 0.95 },
    sticky:  { friction: 1.0, restitution: 0 },
};

function createBody(x, y, w, h, materialName) {
    return Bodies.rectangle(x, y, w, h, {
        ...MATERIALS[materialName],
        label: materialName,
    });
}

复合体(Composites)

import { Composites } from 'matter-js';

// 网格排列的物体堆
const stack = Composites.stack(200, 100, 5, 4, 0, 0, (x, y) => {
    return Bodies.rectangle(x, y, 40, 40);
});
Composite.add(engine.world, stack);

// 链条
const chain = Composites.chain(stack, 0.5, 0, -0.5, 0, {
    stiffness: 0.8,
    length: 2,
});

// 软体(布料模拟)
const cloth = Composites.softBody(200, 100, 10, 8, 0, 0, false, 10, {
    friction: 0.5,
    render: { visible: true },
}, {
    stiffness: 0.2,
});

传感器(Sensor)

传感器检测碰撞但不产生物理响应,用于触发区域:

// 陷阱区域传感器
const trapZone = Bodies.rectangle(600, 550, 100, 20, {
    isStatic: true,
    isSensor: true,
    label: 'trap',
});

// 检查点传感器
const checkpoint = Bodies.rectangle(1000, 300, 50, 100, {
    isStatic: true,
    isSensor: true,
    label: 'checkpoint',
});

Events.on(engine, 'collisionStart', (event) => {
    for (const pair of event.pairs) {
        if (pair.bodyA.label === 'trap' || pair.bodyB.label === 'trap') {
            const player = pair.bodyA.label === 'player' ? pair.bodyA : pair.bodyB;
            if (player.label === 'player') {
                onTrapTrigger();
            }
        }

        if (pair.bodyA.label === 'checkpoint' || pair.bodyB.label === 'checkpoint') {
            saveCheckpoint();
        }
    }
});

物理调试渲染

Matter.js 自带调试渲染器,但我们可以用 Pixi.js 绘制调试信息:

class PhysicsDebugRenderer {
    constructor(container, engine) {
        this.container = container;
        this.engine = engine;
        this.graphics = new Graphics();
        container.addChild(this.graphics);
    }

    update() {
        this.graphics.clear();

        const bodies = Composite.allBodies(this.engine.world);
        const constraints = Composite.allConstraints(this.engine.world);

        // 绘制刚体
        for (const body of bodies) {
            const vertices = body.vertices;

            this.graphics.moveTo(vertices[0].x, vertices[0].y);
            for (let i = 1; i < vertices.length; i++) {
                this.graphics.lineTo(vertices[i].x, vertices[i].y);
            }
            this.graphics.closePath();

            const color = body.isStatic ? 0x666666 :
                          body.isSensor ? 0x00ff00 : 0x3498db;
            this.graphics.stroke({ color, width: 1 });
        }

        // 绘制约束
        for (const constraint of constraints) {
            const { bodyA, bodyB, pointA, pointB } = constraint;
            const startX = bodyA ? bodyA.position.x + (pointA?.x || 0) : pointA.x;
            const startY = bodyA ? bodyA.position.y + (pointA?.y || 0) : pointA.y;
            const endX = bodyB ? bodyB.position.x + (pointB?.x || 0) : pointB.x;
            const endY = bodyB ? bodyB.position.y + (pointB?.y || 0) : pointB.y;

            this.graphics.moveTo(startX, startY);
            this.graphics.lineTo(endX, endY);
            this.graphics.stroke({ color: 0xff6600, width: 1 });
        }
    }
}

Pixi.js 与 Matter.js 同步

关键:每帧将 Matter.js 的位置/角度同步到 Pixi.js 精灵:

interface PhysicsSprite {
    body: Matter.Body;
    sprite: Container;
}

class PhysicsSync {
    private entities: PhysicsSprite[] = [];

    add(body: Matter.Body, sprite: Container) {
        this.entities.push({ body, sprite });
    }

    remove(body: Matter.Body) {
        this.entities = this.entities.filter(e => e.body !== body);
    }

    update() {
        for (const { body, sprite } of this.entities) {
            sprite.x = body.position.x;
            sprite.y = body.position.y;
            sprite.rotation = body.angle;
        }
    }
}

// 使用
const physicsSync = new PhysicsSync();

// 创建物体并关联精灵
const ballBody = Bodies.circle(400, 100, 20, { restitution: 0.7 });
const ballSprite = Sprite.from('/image/ball.png');
ballSprite.anchor.set(0.5);

Composite.add(engine.world, ballBody);
world.addChild(ballSprite);
physicsSync.add(ballBody, ballSprite);

// 游戏循环
app.ticker.add(() => {
    Engine.update(engine, FIXED_DT);
    physicsSync.update();
});

⚠️ 注意:Pixi.js 的 anchor 和 Matter.js 的 position 都是中心点,但要注意精灵原点是否对齐。建议统一使用 anchor.set(0.5)

实战:物理弹球

import { Application, Container, Graphics, Sprite } from 'pixi.js';
import Matter from 'matter-js';

const { Engine, Bodies, Composite, Events, Body } = Matter;

const app = new Application();
await app.init({ width: 400, height: 700 });
document.body.appendChild(app.canvas);

const engine = Engine.create({ gravity: { y: 0.8 } });
const world = new Container();
app.stage.addChild(world);
const physicsSync = new PhysicsSync();

// 边界
const walls = [
    Bodies.rectangle(200, -10, 400, 20, { isStatic: true }),     // 上
    Bodies.rectangle(200, 710, 400, 20, { isStatic: true }),     // 下
    Bodies.rectangle(-10, 350, 20, 700, { isStatic: true }),     // 左
    Bodies.rectangle(410, 350, 20, 700, { isStatic: true }),     // 右
];
Composite.add(engine.world, walls);

// 弹球
const ball = Bodies.circle(200, 100, 12, {
    restitution: 0.8,
    friction: 0.01,
    label: 'ball',
});
Composite.add(engine.world, ball);

const ballSprite = new Graphics();
ballSprite.circle(0, 0, 12);
ballSprite.fill(0xff4444);
ballSprite.anchor = { x: 0.5, y: 0.5 };
world.addChild(ballSprite);
physicsSync.add(ball, ballSprite);

// 弹板(挡板)
const paddle = Bodies.rectangle(200, 650, 80, 15, {
    isStatic: true,
    label: 'paddle',
    chamfer: { radius: 7 },
});
Composite.add(engine.world, paddle);

const paddleSprite = new Graphics();
paddleSprite.roundRect(-40, -7, 80, 15, 7);
paddleSprite.fill(0x3498db);
world.addChild(paddleSprite);
physicsSync.add(paddle, paddleSprite);

// 销钉
const pins = [];
for (let row = 0; row < 5; row++) {
    for (let col = 0; col < 8; col++) {
        const x = 50 + col * 40 + (row % 2) * 20;
        const y = 150 + row * 60;
        const pin = Bodies.circle(x, y, 6, {
            isStatic: true,
            restitution: 1,
            label: 'pin',
        });
        pins.push(pin);

        const pinSprite = new Graphics();
        pinSprite.circle(0, 0, 6);
        pinSprite.fill(0x2ecc71);
        pinSprite.x = x;
        pinSprite.y = y;
        world.addChild(pinSprite);
    }
}
Composite.add(engine.world, pins);

// 鼠标控制挡板
app.canvas.addEventListener('mousemove', (e) => {
    const rect = app.canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    Body.setPosition(paddle, { x: Math.max(40, Math.min(360, x)), y: 650 });
});

// 碰撞计分
let score = 0;
Events.on(engine, 'collisionStart', (event) => {
    for (const pair of event.pairs) {
        if (pair.bodyA.label === 'pin' || pair.bodyB.label === 'pin') {
            score += 10;
            // 弹球与销钉碰撞时弹力加强
        }
    }
});

// 游戏循环
app.ticker.add(() => {
    Engine.update(engine, 1000 / 60);
    physicsSync.update();
});

实战:布娃娃效果

function createRagdoll(x, y) {
    const group = { group: -1 }; // 同组不碰撞

    const head = Bodies.circle(x, y - 40, 15, { ...group, label: 'head' });
    const torso = Bodies.rectangle(x, y, 15, 40, { ...group, label: 'torso' });
    const leftArm = Bodies.rectangle(x - 25, y - 10, 30, 10, { ...group });
    const rightArm = Bodies.rectangle(x + 25, y - 10, 30, 10, { ...group });
    const leftLeg = Bodies.rectangle(x - 8, y + 40, 10, 30, { ...group });
    const rightLeg = Bodies.rectangle(x + 8, y + 40, 10, 30, { ...group });

    const constraints = [
        // 头 - 躯干
        Constraint.create({ bodyA: head, bodyB: torso, pointA: { y: 15 }, pointB: { y: -20 }, length: 0, stiffness: 0.5 }),
        // 手臂
        Constraint.create({ bodyA: torso, bodyB: leftArm, pointA: { x: -7, y: -15 }, length: 0, stiffness: 0.3 }),
        Constraint.create({ bodyA: torso, bodyB: rightArm, pointA: { x: 7, y: -15 }, length: 0, stiffness: 0.3 }),
        // 腿
        Constraint.create({ bodyA: torso, bodyB: leftLeg, pointA: { x: -5, y: 20 }, length: 0, stiffness: 0.3 }),
        Constraint.create({ bodyA: torso, bodyB: rightLeg, pointA: { x: 5, y: 20 }, length: 0, stiffness: 0.3 }),
    ];

    const bodies = [head, torso, leftArm, rightArm, leftLeg, rightLeg];
    Composite.add(engine.world, [...bodies, ...constraints]);

    return { bodies, constraints };
}

扩展阅读