Stabilizing a pile with physics constraints: “sticky” mikans

2025-12-22~7 min read

Problem: stacks collapse too easily

In a pile-building physics game, small disturbances can cause the whole stack to slide and fall. I wanted a mechanic to stabilize the pile while still feeling physical.

Idea: connect objects with constraints

  • Use cannon-es constraints to “connect” a green mikan to nearby orange mikans
  • Connected objects move together, reducing accidental sliding
  • Avoid connecting green↔green to prevent over-stiff structures

Joint Class Implementation

  • Joint class responsibilities
    • Implemented as a GameObject that connects two rigid bodies
    • Handles adding and removing constraints from the physics world
  • Lifecycle management
    • start() adds constraint to physics world
    • onRemove() removes constraint from physics world
    • Proper cleanup prevents memory leaks
  • Automatic constraint maintenance
    • PointToPointConstraint automatically maintains distance via physics simulation
    • No additional processing needed in update() (physics engine handles it)

Joint Class Implementation Code

export class Joint extends GameObject {
  private parentBody: CANNON.Body;
  private childBody: CANNON.Body;
  private constraint: CANNON.Constraint;
  private physicsWorld: CANNON.World | null = null;

  constructor(
    parentBody: CANNON.Body,
    childBody: CANNON.Body,
    worldAnchor: CANNON.Vec3
  ) {
    super('Joint');
    this.parentBody = parentBody;
    this.childBody = childBody;

    // Convert contact point (world coordinates) to local coordinates for both bodies
    const pivotA = new CANNON.Vec3();
    const pivotB = new CANNON.Vec3();
    parentBody.pointToLocalFrame(worldAnchor, pivotA);
    childBody.pointToLocalFrame(worldAnchor, pivotB);

    // PointToPointConstraint is like an "invisible chain" connecting at contact points
    this.constraint = new CANNON.PointToPointConstraint(
      parentBody,
      pivotA,
      childBody,
      pivotB,
      1e5  // stiffness (constraint strength)
    );
  }

  start(): void {
    super.start();
    const scene = this.getScene();
    if (!scene) return;

    this.physicsWorld = scene.getPhysicsWorld();
    if (!this.physicsWorld) return;

    // Add constraint to physics world
    this.physicsWorld.addConstraint(this.constraint);
  }

  onRemove(): void {
    super.onRemove();
    // Remove constraint from physics world
    if (this.physicsWorld && this.constraint) {
      this.physicsWorld.removeConstraint(this.constraint);
    }
  }
}

Usage Example in BallBlue Class

// Process where green mikan (BallBlue) sticks to other mikans
private createJoint(targetBody: CANNON.Body, worldAnchor: CANNON.Vec3): void {
  const blueBody = this.getCannonBody();
  if (!blueBody) return;

  const scene = this.getScene();
  if (!scene) return;

  // Create and connect Joint
  const joint = new Joint(blueBody, targetBody, worldAnchor);
  scene.addGameObject(joint);
  joint.callStart();

  // Store Joint with target as key
  this.joints.set(targetBody, joint);
}

Gameplay tuning

  • Limit connection radius to keep the behavior predictable
  • Cap the number of connections to avoid turning the pile into a rigid body
  • Prefer "soft" stability (still wobbles) over "glued" behavior