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]);
刚体选项详解
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
isStatic | boolean | false | 静态物体不受力和碰撞影响 |
isSensor | boolean | false | 传感器只检测碰撞不产生物理响应 |
mass | number | 自动 | 质量 |
friction | number | 0.1 | 摩擦力 (0-1) |
frictionAir | number | 0.01 | 空气阻力 |
restitution | number | 0 | 弹性 (0-1) |
collisionFilter | object | - | 碰撞过滤 |
label | string | '' | 标签 |
angle | number | 0 | 初始角度(弧度) |
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 };
}