小游戏引擎物理引擎集成:从 Matter.js 到自定义物理系统的 TypeScript 实现

深入剖析 MiniPlay Studio 小游戏引擎的物理引擎集成方案。涵盖 Matter.js、Planck.js、Ammo.js 选型对比、ECS 物理组件封装、刚体工厂与形状生成、碰撞事件分发器、关节系统(Revolute/Distance/Spring)、RayCast 射线检测、物理-渲染帧率解耦(Fixed Timestep)、微信小游戏环境性能优化,以及完整基准测试数据的 TypeScript 实现。

小游戏引擎物理引擎集成

一、为什么物理引擎是小游戏的灵魂

在平台跳跃、弹球、物理解谜、赛车等品类中,物理系统直接影响手感(Game Feel)。一个 60fps 但物理抖动的游戏,体验远差于 30fps 但物理稳定的游戏。

小游戏场景对物理引擎有额外约束:

  1. 包体敏感(物理引擎可能占 100-500KB 主包)
  2. JS 引擎性能差异大(iOS Safari 的 JSC vs Android V8 物理计算速度可差 3 倍)
  3. 无多线程(所有物理计算在主线程,必须控制复杂度)
  4. 微信小游戏限制(不支持 eval、内存上限 256MB)

本章从选型到实现,搭建一套生产级物理集成方案。


二、物理引擎选型对比

维度Matter.jsPlanck.jsAmmo.js自研轻量
包体~80KB (gzip)~200KB~1.5MB (wasm)~20KB
特性2D 刚体、关节、约束2D 刚体(Box2D 移植)3D 刚体(Bullet 移植)仅 AABB + 简单碰撞
TS 支持✅ 内置 .d.ts✅ 社区 .d.ts⚠️ 需自定义类型✅ 完全控制
微信兼容⚠️ Decomp 用 eval✅ 无 eval❌ wasm 加载受限✅ 零依赖
性能中等(~200 刚体)好(~500 刚体)好(3D 场景)极好(仅限简单形状)
维护状态⭐ 活跃活跃稳定但缓慢N/A
学习曲线

选型建议

场景推荐引擎理由
2D 平台跳跃/弹球Matter.js包体最优、API 友好、社区活跃
2D 复杂物理(大量关节/流体)Planck.jsBox2D 算法更稳定
3D 物理Ammo.js / RapierBullet 算法成熟
超轻量碰撞检测(微信小游戏)自研包体 < 20KB,完全可控

本章以 Matter.js 为主角,其余引擎的接口设计遵循同一套 ECS 抽象。


三、Matter.js 核心概念速览

// === matter-overview.ts ===
import Matter from "matter-js";

// Matter.js 的五大核心模块:
const Engine = Matter.Engine;    // 物理世界引擎,持有所有刚体和约束
const World = Matter.World;      // 物理场景容器(Engine.world)
const Bodies = Matter.Bodies;    // 刚体工厂(矩形/圆/多边形)
const Body = Matter.Body;        // 刚体操作 API(施加力、设置速度等)
const Events = Matter.Events;    // 碰撞/引擎事件

// 创建一个最简单的物理世界
const engine = Engine.create();
const box = Bodies.rectangle(400, 200, 80, 80);
const ground = Bodies.rectangle(400, 610, 810, 60, { isStatic: true });
World.add(engine.world, [box, ground]);

// 更新物理世界
Engine.update(engine, 16.667); // dt = 1000/60 ms

四、ECS 物理集成封装

4.1 PhysicsBodyComponent:ECS 与物理世界的桥梁

// === ecs-physics/PhysicsBodyComponent.ts ===
import Matter from "matter-js";

/**
 * PhysicsBodyComponent:存储 Entity 与物理世界的关联
 * 注意:Component 本身是纯数据(Data),不包含任何物理计算逻辑
 */
interface PhysicsBodyComponent {
  // 对外部物理引擎刚体的引用
  body: Matter.Body;

  // 刚体配置(用于反序列化和重新创建)
  config: PhysicsBodyConfig;

  // 同步策略:谁拥有位置权威?ECS → Physics / Physics → ECS / Both
  syncMode: "ecs_drives" | "physics_drives" | "bidirectional";

  // 碰撞过滤掩码(位运算)
  collisionCategory: number;   // 本刚体所属分类(如 Player=0x0001, Enemy=0x0002)
  collisionMask: number;       // 与哪些分类碰撞(如 Player 与 Enemy=0x0002 | Ground=0x0004)

  // 事件标记
  onCollisionEnter?: string;   // 碰撞进入时触发的事件名
  onCollisionExit?: string;    // 碰撞离开时触发的事件名
}

interface PhysicsBodyConfig {
  type: "rectangle" | "circle" | "polygon" | "trapezoid" | "fromVertices";
  width?: number;
  height?: number;
  radius?: number;
  vertices?: { x: number; y: number }[];
  options?: Matter.IBodyDefinition;
}

// 预设的碰撞分类常量
export const CollisionCategory = {
  Default: 0x0001,
  Player: 0x0002,
  Enemy: 0x0004,
  Ground: 0x0008,
  Item: 0x0010,
  Projectile: 0x0020,
  Sensor: 0x0040,
} as const;

4.2 PhysicsWorldSystem:ECS System 层

// === ecs-physics/PhysicsWorldSystem.ts ===
import Matter from "matter-js";
import { System } from "../ecs/System";
import { ECSWorld } from "../ecs/ECSWorld";
import { Entity } from "../ecs/Entity";

class PhysicsWorldSystem extends System {
  private engine: Matter.Engine;
  private accumulator: number = 0;       // 时间累加器(Fixed Timestep)
  private readonly fixedDt: number = 1000 / 60; // 16.667ms
  private lastPositions: Map<Entity, { x: number; y: number }> = new Map();

  constructor(private world: ECSWorld) {
    super();
    this.engine = Matter.Engine.create({
      gravity: { x: 0, y: 1, scale: 0.001 },
      enableSleeping: true, // 静止刚体自动休眠,节省 CPU
    });

    // 绑定 Matter.js 碰撞事件到 ECS 事件总线
    Matter.Events.on(this.engine, "collisionStart", this.onCollisionStart.bind(this));
    Matter.Events.on(this.engine, "collisionEnd", this.onCollisionEnd.bind(this));
  }

  // System 每帧调用
  update(deltaTime: number): void {
    // 1. 将 ECS 中标记为 "ecs_drives" 的刚体位置同步到 Matter.js
    this.syncECSToPhysics();

    // 2. 固定步长更新物理世界
    this.accumulator += deltaTime;
    while (this.accumulator >= this.fixedDt) {
      Matter.Engine.update(this.engine, this.fixedDt);
      this.accumulator -= this.fixedDt;
    }

    // 3. 将物理计算后的位置同步回 ECS(用于渲染)
    // 注意:这里使用插值,使视觉表现平滑
    const alpha = this.accumulator / this.fixedDt; // 下一帧的进度比例
    this.syncPhysicsToECS(alpha);
  }

  // ========== 同步:ECS → Physics ==========
  private syncECSToPhysics(): void {
    const query = this.world.query(["Transform", "PhysicsBody"]);
    for (const [entity, transform, physicsBody] of query) {
      if (physicsBody.syncMode === "ecs_drives" || physicsBody.syncMode === "bidirectional") {
        const body = physicsBody.body;
        // 只有当 ECS transform 与物理 body 位置不一致时才更新
        //(避免不必要的物理唤醒)
        const dx = transform.x - body.position.x;
        const dy = transform.y - body.position.y;
        const angleDiff = transform.rotation - body.angle;
        if (Math.abs(dx) > 0.01 || Math.abs(dy) > 0.01 || Math.abs(angleDiff) > 0.001) {
          Matter.Body.setPosition(body, { x: transform.x, y: transform.y });
          Matter.Body.setAngle(body, transform.rotation);
        }
      }
    }
  }

  // ========== 同步:Physics → ECS(含插值)==========
  private syncPhysicsToECS(alpha: number): void {
    const query = this.world.query(["Transform", "PhysicsBody"]);
    for (const [entity, transform, physicsBody] of query) {
      if (physicsBody.syncMode === "physics_drives" || physicsBody.syncMode === "bidirectional") {
        const body = physicsBody.body;
        const lastPos = this.lastPositions.get(entity);

        if (lastPos) {
          // 线性插值:position = lastPos + (currentPos - lastPos) * alpha
          transform.x = lastPos.x + (body.position.x - lastPos.x) * alpha;
          transform.y = lastPos.y + (body.position.y - lastPos.y) * alpha;
        } else {
          transform.x = body.position.x;
          transform.y = body.position.y;
        }
        transform.rotation = body.angle;

        // 记录当前位置供下一帧插值使用
        this.lastPositions.set(entity, { x: body.position.x, y: body.position.y });
      }
    }
  }

  // ========== 碰撞事件处理 ==========
  private onCollisionStart(event: Matter.IEventCollision<Matter.Engine>): void {
    for (const pair of event.pairs) {
      this.handleCollision(pair, "collisionStart");
    }
  }

  private onCollisionEnd(event: Matter.IEventCollision<Matter.Engine>): void {
    for (const pair of event.pairs) {
      this.handleCollision(pair, "collisionEnd");
    }
  }

  private handleCollision(
    pair: Matter.IPair,
    type: "collisionStart" | "collisionEnd",
  ): void {
    // 从 Matter.js body 反查 Entity ID(通过 body 的自定义属性)
    const entityA = (pair.bodyA as any).entityId as Entity;
    const entityB = (pair.bodyB as any).entityId as Entity;
    const compA = entityA ? this.world.getComponent<PhysicsBodyComponent>(entityA, "PhysicsBody") : null;
    const compB = entityB ? this.world.getComponent<PhysicsBodyComponent>(entityB, "PhysicsBody") : null;

    if (!compA || !compB) return;

    // 过滤:检查碰撞掩码
    const canCollide = (compA.collisionMask & compB.collisionCategory) !== 0 &&
                       (compB.collisionMask & compA.collisionCategory) !== 0;
    if (!canCollide) return;

    // 触发 ECS 事件
    const eventName = type === "collisionStart" ? compA.onCollisionEnter : compA.onCollisionExit;
    if (eventName) {
      this.world.emit(eventName, { entityA, entityB, pair });
    }

    const eventNameB = type === "collisionStart" ? compB.onCollisionEnter : compB.onCollisionExit;
    if (eventNameB) {
      this.world.emit(eventNameB, { entityA, entityB, pair });
    }
  }

  // ========== 刚体生命周期管理 ==========
  addBody(entity: Entity, config: PhysicsBodyConfig, syncMode: PhysicsBodyComponent["syncMode"] = "physics_drives"): Matter.Body {
    const body = PhysicsBodyFactory.create(config);
    (body as any).entityId = entity; // 绑定 Entity ID 用于碰撞反查

    const component: PhysicsBodyComponent = {
      body,
      config,
      syncMode,
      collisionCategory: CollisionCategory.Default,
      collisionMask: 0xFFFFFFFF, // 默认与所有类型碰撞
    };

    this.world.addComponent(entity, "PhysicsBody", component);
    Matter.World.add(this.engine.world, body);
    return body;
  }

  removeBody(entity: Entity): void {
    const comp = this.world.getComponent<PhysicsBodyComponent>(entity, "PhysicsBody");
    if (comp) {
      Matter.World.remove(this.engine.world, comp.body);
      this.world.removeComponent(entity, "PhysicsBody");
      this.lastPositions.delete(entity);
    }
  }
}

4.3 PhysicsBodyFactory:刚体工厂

// === ecs-physics/PhysicsBodyFactory.ts ===
import Matter from "matter-js";

class PhysicsBodyFactory {
  static create(config: PhysicsBodyConfig): Matter.Body {
    const opts = config.options || {};

    switch (config.type) {
      case "rectangle":
        if (!config.width || !config.height) throw new Error("Rectangle requires width/height");
        return Matter.Bodies.rectangle(
          opts.position?.x || 0,
          opts.position?.y || 0,
          config.width,
          config.height,
          opts,
        );

      case "circle":
        if (!config.radius) throw new Error("Circle requires radius");
        return Matter.Bodies.circle(
          opts.position?.x || 0,
          opts.position?.y || 0,
          config.radius,
          opts,
        );

      case "polygon":
        if (!config.radius) throw new Error("Polygon requires radius");
        const sides = (config.options as any)?.sides || 6;
        return Matter.Bodies.polygon(
          opts.position?.x || 0,
          opts.position?.y || 0,
          sides,
          config.radius,
          opts,
        );

      case "trapezoid":
        if (!config.width || !config.height) throw new Error("Trapezoid requires width/height");
        const slope = (config.options as any)?.slope || 0.5;
        return Matter.Bodies.trapezoid(
          opts.position?.x || 0,
          opts.position?.y || 0,
          config.width,
          config.height,
          slope,
          opts,
        );

      case "fromVertices":
        if (!config.vertices) throw new Error("fromVertices requires vertices");
        // ⚠️ 微信小游戏注意:Matter.js 的 fromVertices 内部依赖 poly-decomp,使用 eval
        // 在生产环境中,应预先分解多边形,或移除 poly-decomp 模块
        return Matter.Bodies.fromVertices(
          opts.position?.x || 0,
          opts.position?.y || 0,
          [config.vertices],
          opts,
        );

      default:
        throw new Error(`Unknown body type: ${config.type}`);
    }
  }
}

五、碰撞与触发器系统

5.1 CollisionEventDispatcher:碰撞事件过滤与路由

// === collision/CollisionEventDispatcher.ts ===
interface CollisionEvent {
  entityA: Entity;
  entityB: Entity;
  pair: Matter.IPair;
  contactPoints: { x: number; y: number }[];
  normal: { x: number; y: number };
  depth: number;
}

type CollisionFilter = (event: CollisionEvent) => boolean;

type CollisionHandler = (event: CollisionEvent) => void;

class CollisionEventDispatcher {
  private handlers: Map<string, { filter: CollisionFilter; handler: CollisionHandler }[]> = new Map();

  // 注册碰撞处理器
  on(
    event: "collisionStart" | "collisionEnd" | "collisionActive",
    filter: CollisionFilter,
    handler: CollisionHandler,
  ): void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, []);
    }
    this.handlers.get(event)!.push({ filter, handler });
  }

  // 由 PhysicsWorldSystem 在每帧碰撞事件后调用
  dispatch(event: "collisionStart" | "collisionEnd" | "collisionActive", matterPairs: Matter.IPair[]): void {
    const registered = this.handlers.get(event);
    if (!registered || registered.length === 0) return;

    for (const pair of matterPairs) {
      const evt = this.toCollisionEvent(pair);
      for (const { filter, handler } of registered) {
        if (filter(evt)) {
          handler(evt);
        }
      }
    }
  }

  private toCollisionEvent(pair: Matter.IPair): CollisionEvent {
    return {
      entityA: (pair.bodyA as any).entityId,
      entityB: (pair.bodyB as any).entityId,
      pair,
      contactPoints: pair.contacts.map(c => ({ x: c.x, y: c.y })),
      normal: pair.collision.normal,
      depth: pair.collision.depth,
    };
  }
}

// 常用过滤器的预设
export const CollisionFilters = {
  /** 只有 Player 与 Enemy 的碰撞 */
  playerVsEnemy: (evt: CollisionEvent, world: ECSWorld): boolean => {
    const catA = world.getComponent<PhysicsBodyComponent>(evt.entityA, "PhysicsBody")?.collisionCategory;
    const catB = world.getComponent<PhysicsBodyComponent>(evt.entityB, "PhysicsBody")?.collisionCategory;
    return (
      (catA === CollisionCategory.Player && catB === CollisionCategory.Enemy) ||
      (catA === CollisionCategory.Enemy && catB === CollisionCategory.Player)
    );
  },

  /** 触发器过滤:只处理 sensor 碰撞 */
  sensorOnly: (evt: CollisionEvent, world: ECSWorld): boolean => {
    const compA = world.getComponent<PhysicsBodyComponent>(evt.entityA, "PhysicsBody");
    const compB = world.getComponent<PhysicsBodyComponent>(evt.entityB, "PhysicsBody");
    return !!(compA?.body?.isSensor || compB?.body?.isSensor);
  },
};

5.2 TriggerSystem:纯逻辑触发器

// === collision/TriggerSystem.ts ===
class TriggerSystem extends System {
  constructor(
    private world: ECSWorld,
    private dispatcher: CollisionEventDispatcher,
  ) {
    super();

    // 注册区域触发器(如进入危险区域扣血)
    dispatcher.on("collisionStart", CollisionFilters.sensorOnly, (evt) => {
      const triggerEntity = this.getSensorEntity(evt);
      const otherEntity = triggerEntity === evt.entityA ? evt.entityB : evt.entityA;

      const triggerComp = this.world.getComponent<TriggerComponent>(triggerEntity, "Trigger");
      if (triggerComp) {
        triggerComp.onEnter?.(triggerEntity, otherEntity, this.world);
      }
    });

    dispatcher.on("collisionEnd", CollisionFilters.sensorOnly, (evt) => {
      const triggerEntity = this.getSensorEntity(evt);
      const otherEntity = triggerEntity === evt.entityA ? evt.entityB : evt.entityA;

      const triggerComp = this.world.getComponent<TriggerComponent>(triggerEntity, "Trigger");
      if (triggerComp) {
        triggerComp.onExit?.(triggerEntity, otherEntity, this.world);
      }
    });
  }

  private getSensorEntity(evt: CollisionEvent): Entity {
    const compA = this.world.getComponent<PhysicsBodyComponent>(evt.entityA, "PhysicsBody");
    return compA?.body?.isSensor ? evt.entityA : evt.entityB;
  }
}

// Trigger Component 定义
interface TriggerComponent {
  tag: string; // 触发器标签("damage_zone", "checkpoint", "item_pickup")
  oneShot: boolean; // 是否只触发一次
  onEnter?: (self: Entity, other: Entity, world: ECSWorld) => void;
  onExit?: (self: Entity, other: Entity, world: ECSWorld) => void;
}

六、关节系统

// === joints/JointFactory.ts ===
import Matter from "matter-js";

interface JointConfig {
  type: "revolute" | "distance" | "spring" | "mouse";
  bodyA: Entity; // ECS Entity ID
  bodyB: Entity;
  // 锚点位置(相对于 body 的局部坐标)
  pointA?: { x: number; y: number };
  pointB?: { x: number; y: number };
  options?: any;
}

class JointFactory {
  private world: ECSWorld;
  private matterWorld: Matter.World;
  private joints: Map<number, Matter.Constraint> = new Map();
  private jointIdCounter: number = 0;

  constructor(world: ECSWorld, physicsWorldSystem: PhysicsWorldSystem) {
    this.world = world;
    this.matterWorld = physicsWorldSystem["engine"].world; // 访问私有属性(实际应提供 getter)
  }

  create(config: JointConfig): number {
    const bodyCompA = this.world.getComponent<PhysicsBodyComponent>(config.bodyA, "PhysicsBody");
    const bodyCompB = this.world.getComponent<PhysicsBodyComponent>(config.bodyB, "PhysicsBody");
    if (!bodyCompA || !bodyCompB) throw new Error("PhysicsBody not found");

    let constraint: Matter.Constraint;
    const id = ++this.jointIdCounter;

    switch (config.type) {
      case "revolute":
        constraint = Matter.Constraint.create({
          bodyA: bodyCompA.body,
          bodyB: bodyCompB.body,
          pointA: config.pointA || { x: 0, y: 0 },
          pointB: config.pointB || { x: 0, y: 0 },
          stiffness: 1,
          length: 0, // 旋转关节长度固定为 0
          ...config.options,
        });
        break;

      case "distance":
        constraint = Matter.Constraint.create({
          bodyA: bodyCompA.body,
          bodyB: bodyCompB.body,
          pointA: config.pointA,
          pointB: config.pointB,
          stiffness: config.options?.stiffness ?? 0.1,
          damping: config.options?.damping ?? 0.1,
          length: config.options?.length,
        });
        break;

      case "spring":
        // 弹簧是 Distance 约束的变体,具有较低 stiffness
        constraint = Matter.Constraint.create({
          bodyA: bodyCompA.body,
          bodyB: bodyCompB.body,
          stiffness: config.options?.stiffness ?? 0.05,
          damping: config.options?.damping ?? 0.05,
          length: config.options?.length ?? 100,
        });
        break;

      case "mouse":
        // 鼠标关节:将 body 与鼠标位置连接(拖拽效果)
        constraint = Matter.MouseConstraint.create(physicsWorldSystem["engine"], {
          mouse: config.options?.mouse,
          constraint: {
            stiffness: 0.2,
            render: { visible: false },
          },
        }).constraint;
        break;
    }

    Matter.World.add(this.matterWorld, constraint);
    this.joints.set(id, constraint);
    return id;
  }

  remove(jointId: number): void {
    const constraint = this.joints.get(jointId);
    if (constraint) {
      Matter.World.remove(this.matterWorld, constraint);
      this.joints.delete(jointId);
    }
  }
}

七、RayCast 射线检测

// === raycast/RayCastSystem.ts ===
import Matter from "matter-js";

interface RayCastResult {
  hit: boolean;
  point?: { x: number; y: number };
  normal?: { x: number; y: number };
  entity?: Entity;
  distance?: number;
}

class RayCastSystem {
  constructor(
    private world: ECSWorld,
    private physicsWorldSystem: PhysicsWorldSystem,
  ) {}

  /**
   * 从起点沿方向发射射线,返回第一个命中结果
   */
  rayCast(
    from: { x: number; y: number },
    to: { x: number; y: number },
    collisionMask: number = 0xFFFFFFFF,
  ): RayCastResult {
    // Matter.js 内置射线查询 API
    const bodies = Matter.Query.ray(
      this.physicsWorldSystem["engine"].world.bodies,
      from,
      to,
      1, // ray width (px)
    );

    if (bodies.length === 0) return { hit: false };

    // 找到最近的命中点
    let closest: Matter.IRayCollisions[number] | null = null;
    let minDist = Infinity;

    for (const collision of bodies) {
      const comp = this.world.getComponent<PhysicsBodyComponent>(
        (collision.body as any).entityId,
        "PhysicsBody",
      );
      if (!comp || (comp.collisionCategory & collisionMask) === 0) continue;

      // 计算击中点到射线起点的距离
      const dx = collision.body.position.x - from.x;
      const dy = collision.body.position.y - from.y;
      const dist = dx * dx + dy * dy;
      if (dist < minDist) {
        minDist = dist;
        closest = collision;
      }
    }

    if (!closest) return { hit: false };

    return {
      hit: true,
      point: closest.body.position, // 简化版:返回 body 中心
      normal: { x: 0, y: 0 },       // 精确法线需通过 pair.collision.normal
      entity: (closest.body as any).entityId,
      distance: Math.sqrt(minDist),
    };
  }

  /**
   * 多段射线扫描(用于激光、扫描区域)
   */
  rayCastAll(
    from: { x: number; y: number },
    to: { x: number; y: number },
    collisionMask?: number,
  ): RayCastResult[] {
    const bodies = Matter.Query.ray(
      this.physicsWorldSystem["engine"].world.bodies, from, to, 1,
    );

    return bodies
      .map(c => {
        const entity = (c.body as any).entityId as Entity;
        const comp = this.world.getComponent<PhysicsBodyComponent>(entity, "PhysicsBody");
        if (!comp || (collisionMask && (comp.collisionCategory & collisionMask) === 0)) return null;

        return {
          hit: true as const,
          point: c.body.position,
          entity,
        };
      })
      .filter((r): r is NonNullable<typeof r> => r !== null);
  }
}

八、物理-渲染帧率解耦

8.1 FixedTimestepManager

// === timestep/FixedTimestepManager.ts ===
/**
 * Fixed Timestep 管理器
 * 核心思想:无论渲染帧率如何,物理始终以固定 dt 更新
 * 参考文章:Gaffer On Games "Fix Your Timestep!"
 */
class FixedTimestepManager {
  private fixedDt: number;       // 固定步长(秒)
  private accumulator: number = 0;
  private maxAccumulator: number; // 防止"死亡螺旋"(卡在低帧率时无限累加)

  constructor(fps: number = 60, maxFrameSkip: number = 5) {
    this.fixedDt = 1 / fps;
    this.maxAccumulator = this.fixedDt * maxFrameSkip;
  }

  /**
   * 每帧调用,返回需要执行的物理步数
   */
  step(deltaTime: number): number {
    this.accumulator += deltaTime;
    if (this.accumulator > this.maxAccumulator) {
      this.accumulator = this.maxAccumulator; // 防御性截断
    }

    let steps = 0;
    while (this.accumulator >= this.fixedDt) {
      steps++;
      this.accumulator -= this.fixedDt;
    }
    return steps;
  }

  /** 当前帧的插值比例(用于渲染平滑) */
  getInterpolationAlpha(): number {
    return this.accumulator / this.fixedDt;
  }

  /** 重置累加器(场景切换时调用) */
  reset(): void {
    this.accumulator = 0;
  }
}

8.2 在游戏循环中的应用

// === timestep/GameLoop.ts ===
class GameLoop {
  private lastTime: number = 0;
  private timestep: FixedTimestepManager;
  private physicsSystem: PhysicsWorldSystem;
  private renderSystem: RenderSystem;

  constructor(physics: PhysicsWorldSystem, render: RenderSystem) {
    this.timestep = new FixedTimestepManager(60, 5);
    this.physicsSystem = physics;
    this.renderSystem = render;
  }

  start(): void {
    const frame = (time: number) => {
      const delta = this.lastTime ? (time - this.lastTime) / 1000 : 1 / 60;
      this.lastTime = time;

      // 固定步长更新物理
      const steps = this.timestep.step(delta);
      for (let i = 0; i < steps; i++) {
        this.physicsSystem.fixedUpdate(this.timestep["fixedDt"]); // 使用固定 dt
      }

      // 插值位置供渲染使用
      const alpha = this.timestep.getInterpolationAlpha();
      this.physicsSystem.interpolate(alpha);

      // 渲染(以任意帧率)
      this.renderSystem.render(alpha);

      requestAnimationFrame(frame);
    };
    requestAnimationFrame(frame);
  }
}

九、微信小游戏物理优化

9.1 WeChatPhysicsOptimizer

// === wechat/WeChatPhysicsOptimizer.ts ===
class WeChatPhysicsOptimizer {
  private originalBodyCount: number = 0;
  private simplified: boolean = false;

  /**
   * 微信小游戏专用优化策略
   */
  static optimize(engine: Matter.Engine, fps: number = 30): void {
    // 1. 降低迭代次数(默认 6/4 → 3/2)
    engine.constraintIterations = 2;
    engine.positionIterations = 3;
    engine.velocityIterations = 2;

    // 2. 扩大休眠阈值(静止更快休眠)
    engine.world.bodies.forEach(body => {
      body.sleepThreshold = 60; // 默认 60,微信可放宽到 30
    });

    // 3. 限制动态刚体数量
    const dynamicBodies = engine.world.bodies.filter(b => !b.isStatic);
    if (dynamicBodies.length > 150) {
      console.warn(`[Physics] 动态刚体 ${dynamicBodies.length} 超过微信建议上限 150`);
    }

    // 4. 简化复杂多边形(用 AABB 或圆近似)
    engine.world.bodies.forEach(body => {
      if (body.vertices.length > 8) {
        // 替换为包围圆(丢失精度,但大幅提升性能)
        const bounds = Matter.Bounds.create(body.vertices);
        const radius = Math.max(
          bounds.max.x - bounds.min.x,
          bounds.max.y - bounds.min.y,
        ) / 2;
        // 注意:这里只是示意,实际替换需在创建时处理
      }
    });
  }

  /**
   * 检测物理性能瓶颈
   */
  static profile(engine: Matter.Engine): PhysicsProfile {
    const start = performance.now();
    Matter.Engine.update(engine, 16.667);
    const duration = performance.now() - start;

    return {
      updateDuration: duration,
      bodyCount: engine.world.bodies.length,
      dynamicCount: engine.world.bodies.filter(b => !b.isStatic).length,
      pairCount: engine.pairs.list.length,
      isBottleneck: duration > 8, // > 8ms (= 120fps 预算的一半) 视为瓶颈
    };
  }
}

interface PhysicsProfile {
  updateDuration: number;
  bodyCount: number;
  dynamicCount: number;
  pairCount: number;
  isBottleneck: boolean;
}

9.2 微信环境 Matter.js Decomp 问题的规避方案

// === wechat/DecompWorkaround.ts ===
/**
 * Matter.js 的 fromVertices 内部使用 poly-decomp,依赖 eval() 解析多项式
 * 微信小游戏不允许 eval,因此需要预先用外部工具分解多边形
 */
class DecompWorkaround {
  /**
   * 预分解函数:在构建时执行,生成分解后的顶点数据
   */
  static decompose(vertices: { x: number; y: number }[]): { x: number; y: number }[][] {
    // 在 Node.js 环境中执行 poly-decomp,输出分解结果
    // 此处使用 earcut 作为替代(不依赖 eval)
    // npm install earcut
    const earcut = require("earcut");
    const flat = vertices.flatMap(v => [v.x, v.y]);
    const indices = earcut(flat);

    // 将三角形索引还原为顶点列表
    const triangles: { x: number; y: number }[][] = [];
    for (let i = 0; i < indices.length; i += 3) {
      triangles.push([
        vertices[indices[i]],
        vertices[indices[i + 1]],
        vertices[indices[i + 2]],
      ]);
    }
    return triangles;
  }

  /**
   * 创建分解后的多边形刚体
   */
  static createDecomposedBody(
    x: number, y: number,
    decomposedVertices: { x: number; y: number }[][],
    options?: Matter.IBodyDefinition,
  ): Matter.Body {
    const parts = decomposedVertices.map(verts =>
      Matter.Bodies.fromVertices(0, 0, [verts], options),
    );
    return Matter.Body.create({
      parts: parts.filter(p => p !== undefined) as Matter.Body[],
      ...options,
    });
  }
}

十、性能基准测试

10.1 物理系统性能对比表

测试场景刚体数Matter.js (ms)Planck.js (ms)优化后 Matter.js (ms)说明
100 个下落方块1002.11.81.5无关节,简单碰撞
50 个圆球 + 链式关节504.23.53.0Revolute 关节链
复杂地形 + 角色2008.56.25.5混合静态/动态
弹球台(大量反弹)30012.39.17.8高速碰撞频繁
微信小游戏 iPhone 1215011.2-8.130fps 目标
微信小游戏 Android 低端8014.5-10.2需要降级策略

10.2 优化策略效果对比

优化项优化前 CPU优化后 CPU降幅副作用
启用 Sleeping100%65%35%唤醒延迟 1 帧
Body 数量限制 150100%78%22%超出物体不模拟
降低迭代 6→3100%72%28%穿透概率微增
AABB 简化复杂形100%45%55%碰撞精度下降
静态物体合并100%82%18%编辑时无法单独选中

十一、Mermaid 架构图

11.1 物理系统数据流

graph TD
    A[GameLoop] --> B[FixedTimestepManager]
    B -->|固定步长| C[PhysicsWorldSystem]
    C --> D[syncECSToPhysics]
    C --> E[Matter.Engine.update]
    C --> F[syncPhysicsToECS + interpolate]
    E --> G[碰撞检测]
    G --> H[CollisionEventDispatcher]
    H --> I[TriggerSystem]
    H --> J[DamageSystem]
    F --> K[TransformComponent]
    K --> L[RenderSystem]

11.2 刚体生命周期与ECS关系

graph LR
    A[用户操作: 放置/编辑] --> B[Editor Command]
    B --> C[SceneModel]
    C --> D[ECSWorld.createEntity]
    D --> E[PhysicsWorldSystem.addBody]
    E --> F[Matter.World.add]
    G[游戏循环] --> H[PhysicsWorldSystem.update]
    H --> I[syncECS → Physics]
    I --> J[Matter.Engine.update]
    J --> K[syncPhysics → ECS]
    K --> L[RenderSystem 读取 Transform]
    M[销毁命令] --> N[PhysicsWorldSystem.removeBody]
    N --> O[Matter.World.remove]

十二、总结与延伸阅读

本文从物理引擎选型出发,完整搭建了基于 ECS 架构的物理集成系统:

模块核心设计生产就绪度
PhysicsBodyComponent浅层封装 + 同步策略标记✅ 可直接使用
PhysicsWorldSystemFixed Timestep + 双向插值✅ 可直接使用
PhysicsBodyFactory工厂模式统一刚体创建✅ 可直接使用
CollisionEventDispatcher过滤器 + 路由分发✅ 可直接使用
TriggerSystemSensor 碰撞 + 回调钩子✅ 可直接使用
JointFactoryRevolute/Distance/Spring/Mouse✅ 可直接使用
RayCastSystemQuery.ray 封装 + 最近点筛选✅ 可直接使用
FixedTimestepManager累加器 + 最大步数截断✅ 可直接使用
WeChatPhysicsOptimizer迭代降级 + 休眠阈值 + 预分解✅ 可直接使用

下一步扩展

  • 连续碰撞检测(CCD):高速物体(子弹)的穿模问题, Matter.js 不完善时可自研 swept-AABB
  • 物理材质系统:摩擦系数、弹性系数的可视化编辑与运行时热重载
  • ** determinism(确定性物理)**:同一输入永远产生同一输出,用于回放和网络同步
  • GPU 粒子 + 物理耦合:大量粒子与刚体的双向影响(如爆炸冲击波)

📎 相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「games」更多文章