Matter.js 碰撞事件监听与自定义响应逻辑实现
深入讲解 Matter.js 物理引擎中的碰撞事件监听机制与自定义响应逻辑实现,涵盖 collisionStart/Active/End 触发时机、event.pairs 解包与刚体标签匹配、物理更新时机与延迟修改队列设计,以及传感器(isSensor)实战技巧。
在 Matter.js 2D 物理世界中,仅靠刚体之间的物理反弹无法满足子弹命中、道具拾取、伤害扣除和关卡触发等具体业务需求。实现自定义碰撞响应的核心路径是:通过监听 collisionStart、collisionActive 与 collisionEnd 事件捕获碰撞集合,从 event.pairs 中提取刚体对并进行双向标签匹配,并在物理引擎的 afterUpdate 阶段安全执行增删刚体与状态变更。
一、Matter.js 碰撞事件的三种类型与触发时机
Matter.js 的事件派发中心挂载于 Matter.Events 模块。碰撞相关的全局事件主要由 engine 触发,包含三种粒度的事件类型:
collisionStart:在两个刚体初次相交的第一个物理帧触发。适合处理一次性业务,例如子弹击中怪物、拾取掉落物、播放碰撞音效。collisionActive:在两个刚体保持接触重叠的每一个后续物理步中持续触发。适合处理持续性作用,例如传送带驱动推力、火海/酸液持续扣血、按压开关联动。collisionEnd:在两个刚体完全脱离接触的第一个物理帧触发。适合处理离开区域检测、重置主角着地与起跳状态。
基础监听语法如下:
import Matter from 'matter-js';
const { Engine, Events } = Matter;
const engine = Engine.create();
// 监听刚开始碰撞的瞬间
Events.on(engine, 'collisionStart', (event) => {
const pairs = event.pairs;
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i];
// 处理初次接触逻辑
}
});
// 监听持续贴合阶段
Events.on(engine, 'collisionActive', (event) => {
// 持续计算滑动、推力或累加伤害
});
// 监听脱离碰撞
Events.on(engine, 'collisionEnd', (event) => {
// 重置离开状态
});
二、解包 event.pairs 与刚体双向识别策略
事件回调函数的参数包含 event.pairs 数组,其中每个元素都是一个 Pair 对象。在 Pair 中,最核心的属性是 pair.bodyA 和 pair.bodyB。
注意顺序的不确定性:Matter.js 并不保证哪个物体一定是 bodyA、哪个物体一定是 bodyB。当子弹与敌人相撞时,可能是 bodyA 是子弹,也可能是 bodyB 是子弹。直接硬编码 if (pair.bodyA.label === 'bullet') 会导致漏判 50% 的碰撞情况。
1. 为刚体注入标签与自定义元数据
在创建刚体时,通常使用 label 属性区分类型,或挂载自定义游戏数据对象(例如 customData):
import { Bodies } = Matter;
// 创建子弹刚体
const bullet = Bodies.circle(100, 200, 6, {
label: 'bullet',
frictionAir: 0,
plugin: {
damage: 25,
ownerId: 'player_1'
}
});
// 创建敌人刚体
const enemy = Bodies.rectangle(400, 200, 40, 60, {
label: 'enemy',
plugin: {
hp: 100,
maxHp: 100
}
});
2. 编写通用的碰撞匹配辅助函数
为消除 bodyA 与 bodyB 颠倒对业务代码的干扰,可以封装一个工具函数,自动识别对应身份并解构返回:
/**
* 检查碰撞对是否包含指定的两个标签,并按顺序返回对应的刚体
* @param {object} pair - Matter.js 碰撞对
* @param {string} labelA - 期望的实体 A 标签
* @param {string} labelB - 期望的实体 B 标签
* @returns {[object, object] | null} 匹配成功则返回 [entityA, entityB],否则返回 null
*/
function matchPair(pair, labelA, labelB) {
const { bodyA, bodyB } = pair;
if (bodyA.label === labelA && bodyB.label === labelB) {
return [bodyA, bodyB];
}
if (bodyA.label === labelB && bodyB.label === labelA) {
return [bodyB, bodyA];
}
return null;
}
使用该工具函数后,在 collisionStart 中处理逻辑就会非常直观:
Events.on(engine, 'collisionStart', (event) => {
for (const pair of event.pairs) {
// 检测子弹击中敌人
const bulletEnemy = matchPair(pair, 'bullet', 'enemy');
if (bulletEnemy) {
const [bulletBody, enemyBody] = bulletEnemy;
handleBulletHitEnemy(bulletBody, enemyBody);
continue;
}
// 检测角色拾取金币
const playerCoin = matchPair(pair, 'player', 'coin');
if (playerCoin) {
const [playerBody, coinBody] = playerCoin;
handleCollectCoin(playerBody, coinBody);
continue;
}
}
});
三、物理更新循环与安全延迟修改机制
在实现刚体销毁(如子弹击中后消失、金币被吃掉)时,很多开发者会直接在 collisionStart 回调里调用 Composite.remove(world, body)。这会导致不可预知的错误。
避坑指南:collisionStart 是在引擎物理步求解(Engine.update)内部循环中派发的。此时引擎正在遍历碰撞对并维持空间网格(Broadphase/Narrowphase)。如果在循环中直接从 World 或 Composite 中移出刚体,会导致内部数组索引错位、后续计算引用空指针或物理穿透异常。
推荐实现:缓冲队列 + afterUpdate 统一执行
正确的做法是采用“标记-延迟执行”模式:在碰撞回调中只记录需要销毁或创建的实体,待当前帧的物理计算全部结束后,在 afterUpdate 事件中集中处理修改:
// 维护待删除刚体集合(使用 Set 防止同一帧内重复添加)
const bodiesToRemove = new Set();
// 碰撞发生时仅入队标记
function handleBulletHitEnemy(bullet, enemy) {
// 1. 扣减敌人血量
enemy.plugin.hp -= bullet.plugin.damage;
// 2. 标记子弹销毁
bodiesToRemove.add(bullet);
// 3. 若敌人阵亡,同样加入销毁队列
if (enemy.plugin.hp <= 0) {
bodiesToRemove.add(enemy);
}
}
// 在物理步结束后安全移除
Events.on(engine, 'afterUpdate', () => {
if (bodiesToRemove.size > 0) {
bodiesToRemove.forEach((body) => {
// 从物理世界中彻底移除
Matter.Composite.remove(engine.world, body);
});
bodiesToRemove.clear();
}
});
四、传感器刚体(Sensor)在自定义响应中的运用
很多交互逻辑并不需要真实的物理撞击弹开效果,比如:角色走入传送门、拾取悬浮金币、草丛隐蔽检测或脚底着地探测。如果给这些物体使用普通刚体,角色撞上去就会被弹开或者发生阻挡。
Matter.js 提供了 isSensor: true 配置项,专门用于解决这一需求。
带有 isSensor: true 的刚体具有以下特性:
- 无碰撞反作用力:两个物体相交时不会产生阻挡推力,物体可以自由穿透。
- 事件依然精准触发:完全参与广相与窄相检测,正常派发
collisionStart、collisionActive和collisionEnd。 - 计算效率更高:引擎跳过接触面法线冲量与速度求解步骤,性能开销小。
创建传感器区域与脚底探测器的示例:
// 1. 创建终点传送门传感器
const portalZone = Bodies.rectangle(700, 300, 80, 120, {
isStatic: true,
isSensor: true, // 核心:开启传感器模式
label: 'portal_zone',
render: {
fillStyle: 'rgba(67, 138, 128, 0.3)',
strokeStyle: '#438A80',
lineWidth: 2
}
});
// 2. 角色脚底接地探测器(用于精准判断跳跃)
const footSensor = Bodies.rectangle(playerX, playerY + 30, 20, 6, {
isSensor: true,
label: 'foot_sensor'
});
五、综合实战示例:完整的碰撞响应系统
下面给出一个结构完整、可直接执行的 Matter.js 碰撞响应系统范例,串联了实体定义、碰撞对解包、延迟销毁与传感器判定:
import Matter from 'matter-js';
const { Engine, Render, Runner, Bodies, Composite, Events } = Matter;
// 1. 初始化引擎与渲染器
const engine = Engine.create();
const world = engine.world;
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 500,
wireframes: false,
background: '#F7F9FB'
}
});
Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);
// 2. 创建游戏实体
const ground = Bodies.rectangle(400, 480, 810, 40, { isStatic: true, label: 'ground' });
const player = Bodies.rectangle(150, 400, 40, 50, { label: 'player', restitution: 0 });
const coin = Bodies.circle(300, 420, 12, { isStatic: true, isSensor: true, label: 'coin' });
const enemy = Bodies.rectangle(600, 420, 40, 50, { label: 'enemy', plugin: { hp: 50 } });
Composite.add(world, [ground, player, coin, enemy]);
// 3. 待处理销毁队列
const removalQueue = new Set();
let score = 0;
// 4. 双向匹配函数
function match(pair, typeA, typeB) {
if (pair.bodyA.label === typeA && pair.bodyB.label === typeB) return [pair.bodyA, pair.bodyB];
if (pair.bodyA.label === typeB && pair.bodyB.label === typeA) return [pair.bodyB, pair.bodyA];
return null;
}
// 5. 监听碰撞开始事件
Events.on(engine, 'collisionStart', (event) => {
for (const pair of event.pairs) {
// 玩家吃金币
const coinMatch = match(pair, 'player', 'coin');
if (coinMatch) {
const [, coinBody] = coinMatch;
score += 100;
removalQueue.add(coinBody);
console.log(`金币已拾取!当前得分: ${score}`);
continue;
}
// 玩家踩踏敌人弱点
const enemyMatch = match(pair, 'player', 'enemy');
if (enemyMatch) {
const [playerBody, enemyBody] = enemyMatch;
// 若玩家下落速度大于 1,判定为踩踏
if (playerBody.velocity.y > 1) {
enemyBody.plugin.hp -= 50;
Matter.Body.setVelocity(playerBody, { x: playerBody.velocity.x, y: -8 }); // 踏击反弹
if (enemyBody.plugin.hp <= 0) {
removalQueue.add(enemyBody);
}
} else {
console.log('玩家受到正面撞击!');
}
}
}
});
// 6. 在安全时机集中执行刚体增删
Events.on(engine, 'afterUpdate', () => {
if (removalQueue.size > 0) {
removalQueue.forEach((body) => Composite.remove(world, body));
removalQueue.clear();
}
});
六、关键注意事项与常见问题排查
| 问题现象 | 常见根因 | 排查与解决方案 |
|---|---|---|
| 高速子弹穿透目标(漏判碰撞) | 物理引擎按离散帧更新,高速物体在单步位移内直接跨过了目标几何体(隧道效应)。 | 增大刚体厚度;或将 engine.positionIterations 与 engine.velocityIterations 提高到 8~10;对于超高速子弹改用射线检测(Raycast)预判。 |
| 同一对物体反复触发 collisionStart | 物体在微小重力或弹力作用下发生轻微抖动,短时间内反复进入与脱离碰撞。 | 为接触判定增加冷却时间(Cooldown),或利用复合刚体并将 restitution 设为 0。 |
| 静止物体不响应传感器碰撞 | 物体进入了休眠状态(Sleeping),引擎跳过了休眠实体的碰撞检测。 | 若启用了 enableSleeping,在移动传感器或玩家靠近时调用 Matter.Sleeping.set(body, false) 显式唤醒刚体。 |
| 自定义属性读取为 undefined | 直接在 body 根对象上挂载属性,可能与 Matter.js 内部字段冲突或被深拷贝覆盖。 | 统一将业务数据挂载在 body.plugin 对象下,或维护一个独立的 Map<number, CustomData>,通过 body.id 映射。 |
初学者怎么通过《JavaScript DOM编程艺术》建立原生DOM操作思维
2026-11-05 19:41:41
JS监听滚动事件:回到顶部按钮的显示与隐藏逻辑
2026-11-05 16:19:37
JavaScript怎么读写LocalStorage与SessionStorage
2026-11-05 15:40:11
anime.js 与 GSAP 对比:轻量级项目该怎么选择动画库
2026-11-05 11:24:55
flv.js 与 hls.js 在直播场景下的选型对比
2026-11-05 08:18:52








