logo
By Lawrence

5 minutes

Marks on the Hull

Deaths look right now. Everything short of a death still doesn't. You put two rounds into a fighter's left wing, the wing reaches zero HP, spaceshooter.part.destroyed fires, and the ship flies on, unmarked and symmetrical, with a wing that is destroyed only in an array somewhere.

        "ShipDamageFX": {}

This one needs something no component in the series has needed yet.

Engine code that ships with the component

Cutting a wing off a loaded GLB means finding the child mesh named -lwing under that ship's root, detaching it from its parent without moving it, revealing it, and animating it away. Every one of those is a Babylon call, and the rule that has held for three courses is that a component never imports @babylonjs.

The rule doesn't bend. Instead the engine code moves, into an adapter/ folder inside the component, shipped with it, registered at bootstrap. The component declares what it needs done as an interface:

src/components/ShipDamageFX/ShipDamageFX.defs.ts
/** What the engine plugin has to be able to do. */
export interface ShipDamageFXExtension {
  /**
   * Detach every child of `meshId` whose name ends with one of `suffixes` and set
   * it tumbling. On a GLB ship the primitives are hidden (the model is what you
   * see), so the plugin must reveal them — otherwise a wing "comes off" invisibly.
   */
  shedSubparts(meshId: string, suffixes: readonly string[], spec: DebrisSpec): void;
  /**
   * Clone `meshId`'s own primitives into wreckage without removing them, for a
   * ship that has to survive the effect (the player, who respawns).
   */
  blowApart(meshId: string, spec: DebrisSpec): void;
  /**
   * Stick a crater at a world point on the child nearest it. `suffixes` narrows the
   * search to one part when the hit named one; null means "anywhere on the hull".
   */
  addHole(
    meshId: string,
    suffixes: readonly string[] | null,
    x: number, y: number, z: number,
    diameter: number,
    maxPerPart: number,
  ): void;
  /** Stick a crater on the nearest piece of already-detached wreckage. */
  addHoleOnDebris(x: number, y: number, z: number, diameter: number, maxPerPart: number): void;
  /** Advance wreckage and expire it. Driven by the System, so it respects pause. */
  tick(dt: number): void;
  dispose(): void;
}

The System looks that extension up on the adapter by name and calls it. Nothing found is a valid state, not an error: under the mock adapter, in a headless render, or on an engine with no plugin written yet, damage leaves no marks and everything else keeps working.

Continue reading

Unlock the Full Course

Every lesson, the runnable examples, and the finished build — yours to keep.

$9one-time

Was this page helpful?

We read every note — tell us what's working and what isn't.

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search