Matter.js 碰撞事件监听与自定义响应逻辑实现

作者:页面旅途

时间:2026-11-05

浏览:413

深入讲解 Matter.js 物理引擎中的碰撞事件监听机制与自定义响应逻辑实现,涵盖 collisionStart/Active/End 触发时机、event.pairs 解包与刚体标签匹配、物理更新时机与延迟修改队列设计,以及传感器(isSensor)实战技巧。

文章目录
Matter.js 游戏与交互开发

在 Matter.js 2D 物理世界中,仅靠刚体之间的物理反弹无法满足子弹命中、道具拾取、伤害扣除和关卡触发等具体业务需求。实现自定义碰撞响应的核心路径是:通过监听 collisionStart、collisionActive 与 collisionEnd 事件捕获碰撞集合,从 event.pairs 中提取刚体对并进行双向标签匹配,并在物理引擎的 afterUpdate 阶段安全执行增删刚体与状态变更。

Matter.js 碰撞事件监听与自定义响应逻辑实现
图 0:Matter.js 碰撞事件监听与自定义逻辑流转全景

一、Matter.js 碰撞事件的三种类型与触发时机

Matter.js 的事件派发中心挂载于 Matter.Events 模块。碰撞相关的全局事件主要由 engine 触发,包含三种粒度的事件类型:

  • collisionStart:在两个刚体初次相交的第一个物理帧触发。适合处理一次性业务,例如子弹击中怪物、拾取掉落物、播放碰撞音效。
  • collisionActive:在两个刚体保持接触重叠的每一个后续物理步中持续触发。适合处理持续性作用,例如传送带驱动推力、火海/酸液持续扣血、按压开关联动。
  • collisionEnd:在两个刚体完全脱离接触的第一个物理帧触发。适合处理离开区域检测、重置主角着地与起跳状态。
Matter.js 碰撞事件监听与自定义响应逻辑实现
图 1:两个刚体从相交到分离的碰撞事件生命周期

基础监听语法如下:

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% 的碰撞情况。

Matter.js 碰撞事件监听与自定义响应逻辑实现
图 2:碰撞对(Collision Pair)遍历与双向标签匹配步骤

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 中移出刚体,会导致内部数组索引错位、后续计算引用空指针或物理穿透异常。

Matter.js 碰撞事件监听与自定义响应逻辑实现
图 3:物理单帧更新循环与延迟修改缓冲队列架构

推荐实现:缓冲队列 + 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 配置项,专门用于解决这一需求。

Matter.js 碰撞事件监听与自定义响应逻辑实现
图 4:普通实体刚体与传感器刚体(isSensor: true)对比总结

带有 isSensor: true 的刚体具有以下特性:

  1. 无碰撞反作用力:两个物体相交时不会产生阻挡推力,物体可以自由穿透。
  2. 事件依然精准触发:完全参与广相与窄相检测,正常派发 collisionStart、collisionActive 和 collisionEnd。
  3. 计算效率更高:引擎跳过接触面法线冲量与速度求解步骤,性能开销小。

创建传感器区域与脚底探测器的示例:

// 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 映射。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章