Bauen auf Voxel-Terrain in Babylon.js · Teil 9 von 10 der Serie
Teil 9: Kettenreaktionen
Lass fallende Stücke die Blöcke zerbrechen, die sie treffen: Merk dir, welchen Block ein Kontakt berührt und wie schnell, mach aus Tempo und Gewicht Schaden, gib jedem Material eine Festigkeit, und lass den Halt den Rest tun, damit ein zerbrochener Block ein ganzes Gebäude einstürzen lassen kann.

Quest-Ziele
- Erkennen, welchen Block ein Kontakt berührt und wie hart er ihn trifft
- Aus dem Tempo eines Treffers und dem Gewicht eines Stücks Schaden machen
- Jedem Material eine Festigkeit geben und Schaden sich summieren lassen
- Einen zerbrochenen Block den Halt mitnehmen lassen, für Kettenreaktionen
Ein fallendes Stück landet in Teil 8 auf dem, was darunter ist, und bleibt dort: Eine Kupferkugel, auf einen Tisch aus Brettern fallen gelassen, ruht auf dem Tisch, als wäre er aus Stahl. Echte Dinge zerbrechen. Dieser Teil lässt ein fallendes Stück die Blöcke zerbrechen, die es hart genug trifft. Ein zerbrochener Block ist weg, also verliert, was er hielt, seinen Halt, und Teil 6 findet es, und Teil 7 lässt es fallen, auf das nächste Ding: eine Kettenreaktion, ohne zusätzlichen Code für die Kette.
Wer getroffen wurde, und wie hart
Der Körper rechnet für jeden Kontakt schon aus, wie schnell sich sein Punkt der Oberfläche nähert: Das nutzt das Abprallen. Er muss auch wissen, was er berührt. Die nächste Oberfläche kann es sagen: Gehört sie einem Block, trägt sie den Block. Ergänze ihn in src/solids.ts in Surface:
/** 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() setzt ihn. Die Oberflächen von Schutt kommen aus dem eigenen Speicher jedes Stücks, und das Stück gibt eine neue Oberfläche ohne den Block zurück: Schutt wird hier nicht beschädigt. Ersetze nearestBlock():
/** 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;
}Der Körper merkt sich die Blöcke, die er trifft, jeden mit dem höchsten Tempo, mit dem sich einer seiner Punkte ihm genähert hat, bis jemand sie liest. Ein Stück, das auf einem Block landet, berührt ihn an mehreren Punkten zugleich, und der härteste zählt. Ergänze in src/body.ts ein Feld in Body und importiere den Typ Block:
/**
* 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>();Und merk dir in contacts() den Treffer direkt nach dem Tempo der Annäherung:
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);Schaden
Wie sehr schadet ein Treffer? Schneller schadet mehr, und ein schweres Stück mehr als ein leichtes. Unter 3 m/s tut ein Treffer nichts: Das ist ein Stück, das aufsetzt oder entlangrutscht, nicht eines, das einschlägt. Darüber wächst der Schaden mit dem Tempo über diesen 3 m/s, mal der Wurzel aus dem Gewicht des Stücks in Tonnen. Mit dem bloßen Gewicht würde ein großes Stück alles zerbrechen, was es streift, und ein einzelner Ziegel gar nichts; die Wurzel hält ein großes Stück gefährlich und lässt ein kleines etwas Schaden tun.
Ein Block erträgt Schaden nach der Festigkeit seines Materials, mal seinem Volumen in Kubikmetern: Ein 2-m-Block erträgt achtmal so viel wie ein 1-m-Block aus demselben Material. Schaden summiert sich, also kann ein Block, der zweimal getroffen wird, beim zweiten Mal zerbrechen. Ergänze die Festigkeiten in 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;Eine Kupferkugel von 1 m wiegt etwa 4,2 Tonnen: Kupfer ist schwer, und die Kugel füllt etwas weniger als die Hälfte ihres Würfels. Aus 3 m fallen gelassen, trifft sie mit etwa 7,7 m/s auf: 4,7 mal √4,2 mal 2 sind etwa 19 Schaden, mehr als die 15, die ein 1-m-Block aus Brettern erträgt, und weniger als die 60 von Stein. Lege src/damage.ts an:
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);
}
}Die Treffer weitergeben
Die Stücke geben die Treffer weiter und löschen sie, einmal pro Frame. Ergänze in src/pieces.ts die Felder von Pieces:
/** Called with every block a falling piece hit, how fast, and the piece's mass. */
onHit: (block: Block, speed: number, mass: number) => void = () => {};Und in update(), nachdem sich der Körper bewegt hat:
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 entfernt ein Treffer, der einen Block zerbricht, ihn aus dem Speicher, wie es ein Linksklick tut. Das Entfernen im Speicher gibt ihn an den Halt, der findet, was seinen Halt verloren hat, und der Schaden vergisst den Block. Ersetze den Handler onRemove des Speichers und ergänze den Schaden davor:
// 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)));
};Das HUD zählt auch die zerbrochenen Blöcke:
(pieces.all.length > 0 ? `${pieces.falling} falling, ${pieces.rubble.length} rubble, ${broken} broken\n` : '') +Importiere Damage aus ./damage.ts.
Der zerbrochene Block verschwindet am Ende des Frames, nach dem Schritt, der ihn getroffen hat. In diesem Schritt wurde das Stück noch von ihm zurückgestoßen, also verliert es etwas Tempo: Einen Block zu zerbrechen, nimmt einem Treffer etwas. Im nächsten Schritt bewegt es sich weiter, in den Raum, den der Block hinterlassen hat, und kann den nächsten zerbrechen.
Ausprobieren
Bau den Balken neben der Säule ab. Die Kugel und der Rest des Balkens fallen auf den Tisch, und die Kugel kracht dort, wo sie landet, durch die Tischplatte aus Brettern. Bau dann selbst: etwas Schweres hoch oben auf einem dünnen Pfosten, über einer Hütte aus Brettern. Bau den Pfosten ab, und wo das Gewicht herunterkommt, geben die Bretter nach, und was sie hielten, folgt. Bau die Hütte aus Stein, und es braucht viermal so viel, um sie zu zerbrechen.
