FERRETGame engine over Babylon.js
Ferret

Strength

Package @ferret/physics-strength, capability name strength, phase strength (order 0, after friction and impact).

What it is for#

Things that break: a crate that splinters when it hits a wall at speed, a rope that parts under a heavy load, a bridge bracket that fails, a statue that falls apart in designed pieces. It turns impulse (the N·s a hit delivers) into damage, damage into a break, and a break into pieces that carry the parent's mass and momentum. It also makes links between bodies break at the force their material can hold (σ·A).

A body with no toughness is unbreakable. This is deliberate: nothing in a game breaks unless you said it can.

The model#

per contact:  damage = max(0, impulse − threshold) × scale      (only for bodies whose toughness is finite)
health        = toughness − damage taken so far
health ≤ 0    → the body breaks: `physics:break`, and `pieces` fragments replace it
ValueDefaultEffect
toughness∞impulse (N·s) a body absorbs before breaking. ∞ = unbreakable
threshold0impulse of one hit below which nothing is taken (brushes and resting contact)
scale1multiplies the damage
pieces0fragments that replace a broken body; 0 = the event only, the body stays
burst1m/s the fragments fly apart at, on top of the parent's velocity
maxPieces200pieces alive at once over the whole world; past it a body still breaks (the event) and leaves no new pieces

All except maxPieces are scoped per world, level, zone and entity. maxPieces is the number the tier table sets (budgets).

Events on the world's bus: physics:damage { entity, body, amount, health }, physics:break { entity, body, point, impulse, pieces, seed }, physics:joint-break { constraint, force, userData }.

First use: a crate that breaks at speed#

import { PhysicsHost, RigidBody } from '@ferret/physics';
import { strength, type BreakEvent } from '@ferret/physics-strength';

const cap = strength();
const host = world.addService(new PhysicsHost({ capabilities: [cap] }));
host.kernel.gravity = [0, 0, 0];
host.materials.add('m', { restitution: 0, friction: 0.5 });
host.enable('strength');
host.config('strength').set('world', 'toughness', 10);               // absorbs 10 N·s
host.config('strength').set('world', 'pieces', 5);

const wall = world.spawn('wall');
wall.add('RigidBody', { shape: 'plane', normal: [-1, 0, 0], offset: 0, kind: 'static', material: 'm' });
const crate = (name: string, z: number, v: number) => {
  const e = world.spawn(name, { position: new BABYLON.Vector3(-3, 0, z) });
  e.add('RigidBody', { shape: 'sphere', radius: 0.5, mass: 1, material: 'm', velocity: [v, 0, 0] });
  return e;
};
const breaks: BreakEvent[] = [];
world.events.on('physics:break', (e: BreakEvent) => breaks.push(e));
const slow = crate('slow', 0, 3), fast = crate('fast', 5, 20);
world.run(3);

A 3 m/s hit takes some of the toughness away; a 20 m/s hit exceeds it. Damage accumulates: ten small taps break what one hard hit breaks.

Recipes#

Make one kind of thing breakable#

Set toughness at entity scope for the breakable ones and leave the world value at ∞: host.config('strength').set('entity', 'toughness', 800, crate.id). Or give the whole level a value and set ∞ on the things that must not break. A level can do it with fractured (below).

What the pieces are#

  • Light kernel, a sphere: spheres of the parent's mass (volume split), flying apart by burst.
  • Havok, any shape (box, capsule, hull, compound): the shape is cut into boxes where the body stood; each piece has the velocity of its own point of the parent (v + ω × r) plus the outward push; masses sum to the parent's and the pushes are arranged to add no momentum, so momentum is conserved apart from the burst itself.
  • Pre-fractured (the level's fractured): the artist's pieces take over, see below.

The same seed gives the same pieces and the same scatter on every machine: breakNow(target, point, seed) is how a remote copy of a body breaks identically when its owner's did (see net). BreakEvent.seed is what to send.

A wall that breaks in designed pieces#

Author the whole wall and its pieces as nodes; list them in the level (level block):

"fractured": [{ "node": "Wall", "pieces": ["Wall_a", "Wall_b", "Wall_c"], "toughness": 800 }]

The pieces wait disabled until the wall breaks; then each becomes a box body of its own bounds with its share of the wall's mass.

cap.joint({ type, a, b, tensile, area }) joins two bodies with a constraint that breaks at tensile × area newtons (σ·A). Give maxForce directly when you do not have the material numbers. A 40 MPa rod with a 1 cm² section (10⁻⁴ m²) holds 4 kN.

const rod = cap.joint({ type: 'rod', a: anchorEntity, b: loadEntity, tensile: 40e6, area: 1e-4 });   // holds 4 kN
world.events.on('physics:joint-break', e => console.log('snapped at', e.force));

Every constraint also reports its live force, so you can draw strain: green to red as force / maxForce goes from 0 to 1.

Damage that is not an impact#

cap.damage(target, amount, point?) takes health off (an explosion, a poison, a command) and breaks the body at zero; cap.breakNow(target, point?, seed?) breaks it regardless of toughness; cap.health(target) is what is left. Commands: strength.damage {entity, amount}, strength.health {entity}.

Keeping debris under control#

maxPieces bounds the total alive. When a break would exceed it, the body still breaks (its event fires, the game reacts) but leaves no pieces. Combine with a short lifetime on your own debris and the governor (its first rule halves maxPieces).

Parameters#

ValueUnitSafe rangeNote
toughnessN·s1 – 10⁶ or ∞a 1 kg crate hit at 10 m/s against a wall delivers about 10 N·s
thresholdN·s0 – toughness/10set above the resting-contact impulse so stacks do not wear away
scale–0.1 – 10
piecescount0 – 12each piece is a body; budget it
burstm/s0 – 10
maxPiecescountby tier200 desktop … 30 low

Interplay#

  • Reads physics:contact impulses from the kernel: friction and impact run before it in the same phase.
  • Materials: the capability reads toughness, tensile etc. from scope values, not from the material table; the material gives the pieces their mass (density) and their surface.
  • net replicates breaks by seed; editor builds fractured blocks; explosions calls breakNow on bodies a blast passes its rupture limit on.

Budget#

One number per body that has been hit; a force check per link per step. The cost that matters is the pieces: each is a body. Keep pieces at 4–8, and maxPieces at the tier's value.

Debugging#

SymptomCause
Nothing ever breakstoughness is ∞ at that scope (the default). Set it at entity or world scope
It breaks from a gentle touchthreshold is 0 and resting contact accumulates: raise it
physics:break fires but no piecespieces is 0, or maxPieces is used up, or (light kernel) the body is not a sphere
Pieces fly through each otherburst too high for the shape size
strength is not enabled from jointenable the capability first; joint needs the host
The remote copy breaks differentlypass the owner's BreakEvent.seed to breakNow

Reference#

strength, StrengthCapability, JointDef, DamageEvent, BreakEvent, STRENGTH_DEFAULTS, LevelFracture, @ferret/physics-strength.