Skip to content
DasPeTe Tutorials

Building on Voxel Terrain in Babylon.js · Part 9 of 10 in the series

Part 9: Chain Reactions

Let falling pieces break the blocks they hit: note which block each contact touches and how fast it closes in, turn speed and weight into damage, give each material a strength, and let the support do the rest, so that one broken block can bring down a whole building.

Quest objectives

  • Tell which block a contact touches, and how fast it hits it
  • Turn the speed of a hit and the weight of a piece into damage
  • Give each material a strength, and let damage add up
  • Let a broken block take the support with it, for chain reactions

A falling piece in part 8 lands on whatever is below and stays there: a copper ball dropped on a table of planks rests on the table, as if the table were made of steel. Real things break. This part lets a falling piece break the blocks it hits hard enough. A broken block is gone, so whatever it held loses its hold, and part 6 finds it, and part 7 lets it fall, onto the next thing: a chain reaction, with no extra code for the chain.

Who was hit, and how hard

The body already works out, for every contact, how fast its point closes in on the surface: that's what the bounce uses. It also needs to know what it touches. The nearest surface can say: when it is a block's, it carries the block. In src/solids.ts, add it to Surface:

src/solids.ts
/** The nearest surface to a point: how far away it is, and which way is out. */
export interface Surface {
  /** Meters from the point to the surface, below 0 inside a solid. */
  distance: number;
  /** One meter long, pointing out of the solid. */
  normal: [number, number, number];
  /** The block whose surface it is, if it is a block's. */
  block?: Block;
}

nearestBlock() sets it. The surfaces of rubble come from each piece's own store, and the piece hands back a new surface without the block: rubble isn't damaged here. Replace nearestBlock():

src/solids.ts
/** The nearest surface of the blocks in a store, if one is within `reach` meters. */
export function nearestBlock(store: BlockStore, x: number, y: number, z: number, reach: number): Surface | null {
  let nearest: Surface | null = null;
  // The blocks with a cell within reach, each once.
  const seen = new Set<Block>();
  const from = (c: number) => Math.floor((c - reach) / CELL);
  const to = (c: number) => Math.floor((c + reach) / CELL);
  for (let k = from(z); k <= to(z); k += 1) {
    for (let j = from(y); j <= to(y); j += 1) {
      for (let i = from(x); i <= to(x); i += 1) {
        const block = store.at(i, j, k);
        if (!block || seen.has(block)) continue;
        seen.add(block);
        // Out of a block only where no other block fills the cells beyond.
        const surface = blockDistance(block, x, y, z, (side) => !covered(store, block, SIDES[side]));
        if (surface.distance < reach && (!nearest || surface.distance < nearest.distance)) nearest = { ...surface, block };
      }
    }
  }
  return nearest;
}

The body notes the blocks it hits, each with the fastest any of its points closed in on it, until someone reads them. A piece landing on a block touches it at several points at once, and the hardest of them counts. In src/body.ts, add a field to Body, and import the type Block:

src/body.ts
/**
 * The blocks the body hit, each with the fastest a point closed in on it: kept until
 * someone reads and clears them.
 */
readonly hits = new Map<Block, number>();

And in contacts(), note the hit right after the closing speed:

src/body.ts
const velocity = this.velocity.add(Vector3.Cross(this.spin, lever));
const closing = -Vector3.Dot(velocity, normal);
if (surface.block && closing > (this.hits.get(surface.block) ?? 0)) this.hits.set(surface.block, closing);

Damage

How much does a hit hurt? Faster hurts more, and a heavier piece more than a light one. Below 3 m/s, a hit does nothing: that is a piece setting down or sliding along, not crashing. Above it, the damage grows with the speed beyond those 3 m/s, times the square root of the piece's weight in tonnes. With the plain weight, a big piece would break everything it brushes, and a single brick nothing at all; the square root keeps a big piece dangerous, and a small one able to do some harm.

A block bears damage by its material's strength, times its volume in cubic meters: a 2 m block takes eight times what a 1 m block of the same material takes. Damage adds up, so a block hit twice may break the second time. Add the strengths to src/materials.ts:

src/materials.ts
/**
 * What blocks can be made of, in the order of the number keys and the texture layers,
 * with how heavy each is, in kilograms per cubic meter, and how much damage a cubic meter
 * of it bears.
 */
export const MATERIALS = [
  { name: 'Stone', color: '#9a9c8e', density: 2600, strength: 60 },
  { name: 'Bricks', color: '#b5523b', density: 1900, strength: 40 },
  { name: 'Planks', color: '#b98a52', density: 600, strength: 15 },
  { name: 'Plaster', color: '#d6dcc6', density: 1500, strength: 20 },
  { name: 'Slate', color: '#4f4a63', density: 2800, strength: 50 },
  { name: 'Copper', color: '#cf7a3f', density: 8900, strength: 80 },
] as const;

A copper ball of 1 m weighs about 4.2 tonnes: copper is heavy, and the ball fills a little less than half its cube. Dropped 3 m, it hits at about 7.7 m/s: 4.7 times √4.2 times 2 is about 19 damage, more than the 15 a 1 m block of planks bears, and less than the 60 of stone. Create src/damage.ts:

src/damage.ts
import { CELL, type Block } from './blocks.ts';
import { MATERIALS } from './materials.ts';

/** A hit slower than this, in meters per second, does no harm. */
const HARMLESS_SPEED = 3;
/** Damage per meter per second above HARMLESS_SPEED, for a tonne. */
const DAMAGE = 2;

/**
 * The damage blocks took from falling pieces. A hit does more the faster it is, and the
 * heavier the piece: with the square root of its weight, so that a big piece is
 * dangerous without breaking everything it brushes. A block breaks when it has taken more
 * than its material can bear, times its size in cubic meters.
 */
export class Damage {
  private readonly taken = new Map<Block, number>();

  /** A hit on a block at `speed` by a piece of `mass` kilograms. Returns whether it broke. */
  hit(block: Block, speed: number, mass: number): boolean {
    if (speed <= HARMLESS_SPEED) return false;
    const damage = (speed - HARMLESS_SPEED) * Math.sqrt(mass / 1000) * DAMAGE;
    const taken = (this.taken.get(block) ?? 0) + damage;
    this.taken.set(block, taken);
    return taken >= MATERIALS[block.material].strength * (block.size * CELL) ** 3;
  }

  /** Forgets a block that is gone. */
  forget(block: Block): void {
    this.taken.delete(block);
  }
}

Passing the hits on

The pieces hand the hits on and clear them, once per frame. In src/pieces.ts, add to the fields of Pieces:

src/pieces.ts
/** Called with every block a falling piece hit, how fast, and the piece's mass. */
onHit: (block: Block, speed: number, mass: number) => void = () => {};

And in update(), after the body moved:

src/pieces.ts
piece.body.update(dt, this.solids);
for (const [block, speed] of piece.body.hits) this.onHit(block, speed, piece.body.mass);
piece.body.hits.clear();

In src/main.ts, a hit that breaks a block removes it from the store, as a left click does. The store's removal hands it to the support, which finds what lost its hold, and the damage forgets the block. Replace the store's onRemove handler, and add the damage before it:

src/main.ts
// Falling pieces break the blocks they hit hard enough, which may let more fall.
const damage = new Damage();
let broken = 0;
pieces.onHit = (block, speed, mass) => {
  if (!damage.hit(block, speed, mass)) return;
  blocks.store.remove(block);
  broken += 1;
};
blocks.store.onRemove = (block) => {
  support.removed(block);
  damage.forget(block);
  const low = new Vector3(block.x, block.y, block.z).scaleInPlace(CELL);
  pieces.wakeNear(low, low.add(new Vector3(1, 1, 1).scaleInPlace(block.size * CELL)));
};

The HUD counts the broken blocks too:

src/main.ts
(pieces.all.length > 0 ? `${pieces.falling} falling, ${pieces.rubble.length} rubble, ${broken} broken\n` : '') +

Import Damage from ./damage.ts.

The broken block disappears at the end of the frame, after the step that met it. In that step the piece was still pushed back by it, so it loses some speed: breaking a block takes something out of a hit. In the next step it moves on, into the space the block left, and may break the next.

Try it

Break the beam next to the pillar. The ball and the rest of the beam drop onto the table, and the ball crashes through the plank top where it lands. Then build your own: something heavy high up on a thin post, above a hut of planks. Break the post, and where the weight comes down, the planks give, and whatever they held follows. Build the hut of stone, and it takes four times as much to break it.

The copper ball went through the table top where it landed; the beam lies across the table.
Chain reactions. Click, then: break the beam next to the pillar and watch the ball crash through the table. Left click breaks, right click places, C chisels, T digs, F flies.
Open in a new tab

Comments

The comments are GitHub Discussions: sign in with GitHub to write one.

The comments come from GitHub, through giscus. Loading them sends your IP address to giscus.app and GitHub.