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