Patch Matter.js Engine Solver Using Custom Plugins

This article provides a practical guide on how to patch internal engine solver steps in Matter.js using its built-in plugin architecture. Matter.js delegates collision resolution and constraint solving to internal sub-modules during each engine update tick. By leveraging the Matter.Plugin API, you can intercept, extend, or replace core solver routines—such as position and velocity resolution passes—without modifying the upstream library source code.

Understanding the Matter.js Plugin Mechanism

Matter.js includes a modular plugin interface defined under Matter.Plugin. Plugins define an install function that executes when the plugin is registered with the engine using Matter.use() or Matter.Plugin.use().

The install function receives the root Matter object, granting direct mutable access to internal namespaces, including Matter.Resolver, Matter.Engine, and Matter.Constraint. This access allows you to intercept internal physics solver functions safely before instances of an engine start updating.

Identifying Key Solver Methods

During each call to Engine.update, collision impulses and separations are resolved by the Resolver module. The primary functions responsible for the solver passes are:

Modifying solvePosition affects stability and anti-tunneling behavior, while modifying solveVelocity alters restitution, friction, and impulse propagation.

Creating and Installing the Custom Plugin

To patch one of these internal passes, define a plugin object with name, version, and install properties. Inside install, cache the reference to the original method, implement your custom logic, and reassign the solver function on the target module.

// Define the custom solver plugin
const CustomSolverPlugin = {
  name: 'matter-custom-solver',
  version: '1.0.0',
  install: function(base) {
    // 1. Cache the original solver methods
    const originalSolvePosition = base.Resolver.solvePosition;
    const originalSolveVelocity = base.Resolver.solveVelocity;

    // 2. Patch the position solver step
    base.Resolver.solvePosition = function(pairs) {
      // Execute pre-solver logic or custom positional constraints
      for (let i = 0; i < pairs.length; i++) {
        const pair = pairs[i];
        if (pair.bodyA.isCustomSolverIgnored || pair.bodyB.isCustomSolverIgnored) {
          continue;
        }
      }

      // Execute original algorithm or supply a custom impulse formula
      originalSolvePosition(pairs);

      // Execute post-solver hooks
    };

    // 3. Patch the velocity solver step
    base.Resolver.solveVelocity = function(pairs) {
      // Custom velocity resolution, e.g., custom damping or directional stiffness
      originalSolveVelocity(pairs);
    };
  }
};

// Register the plugin globally with Matter.js
Matter.Plugin.register(CustomSolverPlugin);

// Apply the plugin to the active Matter namespace
Matter.use(CustomSolverPlugin);

Hooking into Engine Lifecycle Events

If you need your solver patch to execute custom passes outside the default Resolver execution loop without replacing the mathematical core entirely, hook into internal engine events within your plugin's install method:

install: function(base) {
  // Intercept the update loop right before constraints are resolved
  base.Events.on(base.Engine, 'beforeUpdate', function(event) {
    const engine = event.source;
    // Perform custom constraint relaxation or solver pre-passes here
  });

  // Intercept right after collisions are computed but before solving
  base.Events.on(base.Engine, 'collisionActive', function(event) {
    const pairs = event.pairs;
    // Apply custom bias adjustments to the contact pairs
  });
}

Best Practices for Solver Patching

  1. Retain Signatures: Always match the argument lists of the native functions. Resolver.solvePosition and Resolver.solveVelocity both expect an array of active collision pairs.
  2. Handle Performance: Solver steps run multiple times per frame (governed by engine.positionIterations and engine.velocityIterations). Avoid allocations, object creation, and garbage-collection triggers inside patched loops.
  3. Preserve Idempotency: Ensure the install method does not re-wrap functions multiple times if Matter.use() is called concurrently across different application modules. Check for an internal flag before reassigning functions.