Matter.js 物理引擎与微信小程序实战
引言
在前端开发中,物理动效能够极大地提升用户体验——下落的卡片、弹跳的球体、可拖拽的元素等。Matter.js 是一个轻量级的 2D 刚体物理引擎,由 Liam Brummitt 开发,原生 JavaScript 实现(非 C++ 移植),零依赖,MIT 许可证。当前最新版本为 0.20.0(2024-06-23 发布)
但 Matter.js 是为浏览器环境设计的,它内部依赖 window、document、DOM 事件等 Web API。当我们想在微信小程序中使用它时,就会遇到一系列环境适配问题。本文将从 Matter.js 的核心概念出发,重点讲解在微信小程序中使用时必须调整的关键变量和注意事项
模块架构
Matter.js 包含 29 个模块,按职责可分为以下几类:
| 分类 | 模块 | 说明 |
|---|---|---|
| 核心物理 | Engine, Body, Bodies, Composite, Composites | 引擎驱动、刚体创建与管理 |
| 碰撞检测 | Collision, Detector, Pair, Pairs, Resolver | 宽相/窄相碰撞检测与求解 |
| 约束与交互 | Constraint, MouseConstraint, Mouse | 约束关系与鼠标拖拽 |
| 渲染与运行 | Render, Runner | Canvas 渲染与游戏循环 |
| 几何与数学 | Vector, Vertices, Bounds, Axes | 向量运算与几何工具 |
| 工具 | Events, Sleeping, Plugin, Query, Common | 事件系统、休眠、空间查询 |
在微信小程序中,我们主要使用核心物理和碰撞检测模块,而 Render、Runner、Mouse、MouseConstraint 这四个模块需要替换或重写
核心概念
Engine(引擎)
Engine 是整个物理模拟的核心控制器,负责管理世界更新、碰撞检测和求解:
1 | const engine = Engine.create({ |
关键参数说明:
gravity.scale:重力缩放系数,默认0.001。设为0可禁用重力(适合太空场景)。在小程序中如果屏幕尺寸与默认的 800x600 差异较大,可能需要调整此值以获得合适的下落速度positionIterations:值越大碰撞精度越高,但计算开销也越大。小程序中建议降到 4,在精度和性能之间取平衡velocityIterations:同上,小程序中可降到 2timing.timeScale:全局时间缩放,0为暂停,0.5为慢动作,1为正常速度
推进物理模拟的方法:
1 | // delta 参数为时间步长(毫秒),默认 16.666ms(60fps) |
Body 与 Bodies(刚体)
Body 是物理世界中的基本实体,Bodies 是创建预定义形状的工厂方法:
1 | // 矩形 |
物理属性速查
这些参数直接决定了物体的物理行为,是调参的重点:
| 属性 | 默认值 | 范围 | 说明 |
|---|---|---|---|
density |
0.001 | >0 | 密度,质量 = 密度 x 面积 |
friction |
0.1 | 0~1 | 动摩擦系数,两物体取 Math.min |
frictionStatic |
0.5 | >=0 | 静摩擦系数,物体从静止开始运动需要克服的阻力 |
frictionAir |
0.01 | 0~1 | 空气阻力,0 为太空,0.05 为明显阻力 |
restitution |
0 | 0~1 | 弹性恢复系数,两物体取 Math.max。0 为完全无弹性,0.9 为弹力球 |
slop |
0.05 | >=0 | 碰撞容差,允许的轻微穿透量 |
isStatic |
false | - | 静态物体,不受力、不移动 |
isSensor |
false | - | 传感器,触发碰撞事件但无物理反应 |
sleepThreshold |
60 | - | 近零速度持续多少帧后进入休眠 |
timeScale |
1 | >=0 | 单个物体的时间缩放 |
典型场景参数参考:
1 | // 弹力球 |
碰撞过滤器
碰撞过滤系统使用 category、mask 和 group 三个属性控制哪些物体之间可以碰撞:
1 | // category 必须是 2 的幂(位标志),最多支持 32 个 |
碰撞判定规则:(A.category & B.mask) !== 0 && (B.category & A.mask) !== 0,双向检查都必须通过
group 属性优先级高于 category/mask:
group > 0:同组物体始终碰撞group < 0:同组物体永不碰撞group === 0:回退到category/mask判定
Composite(复合体)
Composite 是 Body、Constraint 和子 Composite 的容器,类似于”分组”的概念:
1 | // 添加物体到世界 |
Composites 工厂提供了批量创建的快捷方法:
1 | // 网格排列的物体堆 |
Constraint(约束)
约束维持两个物体(或物体与世界定点)之间的关系:
1 | // 两物体之间的弹簧连接 |
事件系统
Matter.js 使用标准的发布-订阅模式:
1 | // 碰撞开始 |
注意:在碰撞事件回调中不要直接移除物体,应先收集需要移除的物体,待事件处理完成后再统一移除,否则可能导致迭代器异常
1 | // 正确的做法:先收集,后移除 |
微信小程序适配:核心挑战
微信小程序运行在 JSCore 环境中,与浏览器环境存在根本性差异:
| 浏览器特性 | 小程序中的情况 |
|---|---|
window 对象 |
不存在 |
document 对象 |
不存在 |
| DOM 操作 | 不存在 |
requestAnimationFrame |
仅 Canvas 组件上有 canvas.requestAnimationFrame() |
HTMLCanvasElement |
使用 <canvas type="2d"> 组件 |
MouseEvent |
只有 TouchEvent |
Matter.js 内部多处依赖浏览器环境:
- Matter.Render:使用
document.createElement('canvas')创建画布 - Matter.Runner:使用
window.requestAnimationFrame驱动循环 - Matter.Mouse:监听 DOM 的
mousedown/mousemove/mouseup事件 - 内部工具函数:部分使用
window、document做特性检测
适配方案
核心思路:只用 Matter.js 的物理计算能力,完全放弃其渲染和运行模块
第一步:环境 Polyfill
在引入 Matter.js 之前,构造一个假的浏览器环境,让 Matter.js 初始化不报错:
1 | // utils/polyfill.js — 必须在 require('matter') 之前执行 |
第二步:Canvas 2D 初始化与 DPR 处理
DPR(设备像素比)处理是小程序中最常踩的坑。处理不当,Canvas 会出现明显的模糊:
1 | <!-- physics.wxml --> |
必须使用
type="2d"的新版 Canvas 组件,旧版<canvas canvas-id="xxx">性能差且 API 不同
1 | const sysInfo = wx.getSystemInfoSync(); |
核心原则:
- 物理世界坐标 = CSS 逻辑像素坐标(屏幕宽 375px,物理世界就是 375 宽)
- Canvas 实际像素 = 逻辑像素 x DPR
- 触摸事件的
touch.x / touch.y是逻辑像素坐标,可直接用于物理世界
第三步:游戏循环(替代 Matter.Runner)
固定时间步长 + 累加器是最可靠的方案:
1 | startGameLoop() { |
为什么不用简单的
Engine.update(engine, delta)?可变时间步长会导致物理模拟不确定——同样的场景在不同设备上表现不同,甚至可能出现物体穿透
第四步:自定义渲染
完全替代 Matter.Render,使用小程序 Canvas 2D API 绑制物体:
1 | render() { |
第五步:触摸事件适配(替代 MouseConstraint)
Matter.js 的 Mouse 和 MouseConstraint 依赖 DOM 事件,需要手动实现拖拽:
1 | onTouchStart(e) { |
小程序关键调参清单
这是在小程序中使用 Matter.js 时最需要关注的变量:
引擎层面
| 参数 | 浏览器默认值 | 小程序建议值 | 原因 |
|---|---|---|---|
positionIterations |
6 | 4 | 移动端性能有限,降低迭代换取帧率 |
velocityIterations |
4 | 2 | 同上 |
constraintIterations |
2 | 1 | 同上 |
enableSleeping |
false | true | 静止物体暂停计算,显著提升性能 |
gravity.scale |
0.001 | 按屏幕比例调整 | 小程序屏幕尺寸不同于默认 800x600 |
物体层面
| 参数 | 关注点 |
|---|---|
frictionAir |
小屏幕上物体下落很快,适当增大(如 0.02~0.05)让动画更柔和 |
restitution |
弹跳在小屏幕上感知不同,建议实机调试 |
sleepThreshold |
默认 60 帧,快速休眠可降到 30 |
slop |
堆叠抖动时增大到 0.1,精度要求高时减小到 0.01 |
渲染层面
| 参数 | 说明 |
|---|---|
canvas.width/height |
必须 乘以 DPR,否则画面模糊 |
ctx.scale(dpr, dpr) |
必须 设置,否则物理坐标与绘制坐标不一致 |
| 动态物体上限 | 移动端建议 50~100 个 |
时间步进
| 参数 | 说明 |
|---|---|
fixedStep |
固定使用 1000/60(16.666ms),不要用可变步长 |
| 帧间隔上限 | 超过 100ms 截断,防止物理爆炸 |
性能优化
1. 启用休眠(最重要)
1 | const engine = Engine.create({ enableSleeping: true }); |
休眠的物体不参与碰撞检测和求解器计算,是对性能最有效的单一优化
2. 控制物体数量
1 | const MAX_BODIES = 50; |
3. 清理离屏物体
1 | Events.on(engine, 'afterUpdate', () => { |
4. 使用简单形状
碰撞检测开销排序:圆形 < 矩形 < 凸多边形 < 凹多边形。尽量避免 Bodies.fromVertices 创建复杂形状
5. 合并静态物体
不要用 100 个小矩形拼一面墙,用一个大矩形即可。静态物体数量多同样会增加宽相检测开销
6. 渲染只画可见物体
利用 body.bounds 做视口裁剪,跳过屏幕外的物体
内存管理
小程序对内存限制严格,页面销毁时必须彻底清理:
1 | onUnload() { |
可以在 app.js 中监听内存警告,及时减少物体数量:
1 | App({ |
小程序 vs 小游戏
如果你的需求是做一个独立的物理小游戏而非嵌入小程序页面,微信小游戏(Minigame)是更好的选择:
| 特性 | 小程序 | 小游戏 |
|---|---|---|
| Canvas 获取方式 | 组件 query 获取 | 全局 wx.createCanvas() |
| requestAnimationFrame | canvas.requestAnimationFrame() |
全局 requestAnimationFrame() |
| 环境适配 | 需要手动 polyfill | 官方 weapp-adapter 基本覆盖 |
| 包体积限制 | 主包 2MB | 主包 4MB |
| 适配难度 | 较高 | 较低 |
小游戏提供了 weapp-adapter.js,会模拟大部分浏览器 API,Matter.js 基本可以直接运行
常见问题
Q:Matter.js 压缩后多大?会超出小程序包体积限制吗?
未压缩约 500KB,Terser 压缩后约 100KB。小程序主包限制 2MB,通常不会超。如果紧张可放在分包中
Q:圆形碰撞不够精确?
Matter.js 中的圆形实际上是多边形近似(默认约 25 条边)。需要更高精度可增加 Bodies.circle 的 maxSides 参数,但会增加计算开销
Q:物体堆叠时抖动?
增大 positionIterations(10~20)、增大 slop(0.05 -> 0.1)、确保启用 enableSleeping
Q:物体穿过墙壁?
Matter.js 不支持 CCD(连续碰撞检测),小而快的物体可能穿透薄墙。解决方案:增加墙壁厚度、降低物体速度、增加 positionIterations
Q:需要更高性能怎么办?
如果 Matter.js 性能不够,可考虑 Planck.js(Box2D 的 JS 移植,支持 CCD)或 p2.js(更轻量),但同样需要环境适配
完整示例
1 | // pages/physics/physics.js |
参考文献
- [1] Matter.js 官方网站
- [2] Matter.js GitHub 仓库
- [3] Matter.js API 文档
- [4] Matter.js 0.20.0 Changelog
- [5] 微信小程序 Canvas 2D 文档
- [6] Getting Started with Matter.js - Envato Tuts+