logo
SpaceShooterHUD

ui · space-combat-sim

SpaceShooterHUD

Listens toRoundReset
2
Unlock

Install with the CLI:

bjs download SpaceShooterHUD

SpaceShooterHUD

The pilot's instruments — reticle, lead diamond, target brackets, the escort marker, objective banners and a live view of the ship you're shooting at.

What it does

SpaceShooterHUD reads the world each frame, projects it to screen pixels, and writes positioned DOM over the canvas. It owns no game state: the lock it draws was decided by TargetingSystem (which lives in MissileLauncher), the hull it reads belongs to SpaceShooterHealth, the track counter belongs to Waypoint, the countdown it shows came from RoundReset.

DOM rather than engine-drawn UI is deliberate. Text that changes every frame is cheap in DOM and costs a texture re-raster in 3D, CSS gives transitions and glow for free, and — the part that matters for a marketplace component — it means the whole HUD runs under MockRendererAdapter in tests without a canvas. The one call it makes into the renderer is worldToScreen, which is the adapter's own projection and exists precisely for this.

Colour answers "can I shoot yet" before any number does — cyan tracking, amber locked, green when the crosshair sits on the lead point.

The gunsight is a ring with four radial ticks, not the bare circle the reference draws. That was a deliberate departure after the copy proved unreadable: against a starfield full of round pale rocks a thin circle is the least distinguishable shape available, and the reticle was reported missing while it was on screen the whole time. Nothing else in a rock field is a straight radial line, so the ticks make the mark identifiable by shape rather than by spotting a slightly brighter circle.

The crosshair is projected at the locked target's distance, not at a fixed range. With a chase camera sitting behind the ship, a point on the nose ray lands on a different pixel depending how far along you sample it — there is no single "forward" pixel. Sample at the target and crosshair-on-target means bullets hit; sample anywhere else and it only means roughly aligned. With no lock it falls back to crosshairFallbackRange.

The lead diamond solves where to aim so bullet and target arrive together. The honest solve is a quadratic; two fixed-point iterations on flight time converge close enough for a dogfight. Effective bullet speed includes the shooter's own velocity along the nose, because bullets inherit ship motion — a ship burning forward genuinely throws them faster.

Use it in a scene

A scene is just data — a list of entities, each with its components. At startup the SceneLoader turns this JSON into a live world; edit the file and reload to rebuild it.

Fastest path

bjs download scene SpaceShooterHUD

Pulls this exact scene into your project and runs it — no copy-paste. Add --all to grab the components and assets it needs too.

Or build it from scratch with the bjs CLI:

  1. 1Scaffold a project
    npm create @babylonjsmarket/arcade@latest my-game

    World, renderer, and dev server — ready to run.

  2. 2Install dependencies
    cd my-game && npm install
  3. 3Add SpaceShooterHUD
    bjs download SpaceShooterHUD

    Copies its source into src/ so the scene resolves.

    First time? Run bjs login once.

  4. 4Paste the scene into src/scenes/arcade-room.ts and run
    npm run dev

    SceneLoader builds the world from the JSON; reload to rebuild.

JSON
{
  "World": {
    "tags": ["world"],
    "components": {
      "SpaceShooterHUD": { "onTargetPx": 36, "sunMarker": true }
    }
  }
}

Props

  • crosshairFallbackRange (number, default 600) — distance to sample the nose ray when nothing is locked.
  • onTargetPx (number, default 36) — crosshair-to-lead pixel gap that counts as "guns on" and turns both green.
  • playerOcclusionRadius (number, default 65) — screen radius around your own ship where an enemy bracket is suppressed, so a marker never floats on your own hull.
  • shipVisualSize (number, default 5.5) — FALLBACK ship envelope in world units, used to size a bracket only when the renderer has no bounds for that entity (no model loaded yet). With a model, the bracket is sized from the hull's own extent.
  • bracketMinPx / bracketMaxPx (number, defaults 14 / 140) — bracket half-width clamp. Below the floor a distant fighter's bracket collapses to an invisible dot; above the ceiling a close pass fills the screen.
  • fov (number, default 1.05) — fallback vertical FOV for the apparent-size maths. The scene camera's CameraState6DOF.fov wins when there is one.
  • sunMarker (boolean, default true) — mark the sun's projected position.
  • freighterMarker (boolean, default true) — track a freighter-head-tagged escort with a green marker, clamped to the frame edge when it leaves view. An escort run is the one case where the thing you must not lose is usually off screen.
  • edgeMarginPx (number, default 20) — how close to the viewport edge a clamped marker sits.
  • bannerSeconds (number, default 3) — how long TRACK COMPLETE / OFF TRACK stay up.
  • toggleKey (string, default 'KeyH') — KeyboardEvent.code that hides and restores the whole overlay, for screenshots and for just watching the scene. Empty string disables it.
  • targetView (boolean, default true) — draw the locked ship itself in the target panel through the adapter plugin, instead of the hitbox schematic.

The target view

The panel in the bottom-left shows the ship you are locked onto, and only that — with no lock it goes away rather than quietly switching to your own hull. A readout that keeps showing something when its subject is gone teaches you to distrust it, and "am I locked" is the exact question it exists to answer. Your own damage is already on the hull bar.

There are two ways it draws, and the adapter decides which:

  • With the plugin registered (adapter/register.ts), it renders the real hull from above — a second camera parked over a clone of the ship, into a render target displayed on a camera-parented plane. Parts you have damaged pulse amber, destroyed parts pulse red, and a round landing flashes one white. Register it once at bootstrap, before the scene loads:
    const adapter = new BabylonAdapter();
    registerSpaceShooterHUDExtension(adapter);
    const game = new ArcadeGame(adapter, …);
    
  • With no plugin — the mock, a headless test, an engine with no implementation — it falls back to a DOM plan view of the ship's hitboxes. That fallback is honest rather than decorative: the rectangles are the same local-space boxes SpaceShooterBullet tests rounds against, so a part that flashes really was crossed.

Rendering the ship is engine work — an off-screen camera, a render target, a mesh clone, a light that reaches nothing else — so it goes out through an adapter plugin rather than widening the shared RendererAdapter for one component's trick. Unregistered is a supported state, not an error.

Events

  • listens for round.reset.pending to show the death overlay and count the restart down, and round.reset.request to clear it.
  • listens for spaceshooter.track.completed and spaceshooter.waypoint.missed to raise the objective banners.
  • listens for spaceshooter.gun.hit to flash the part a round landed on.
  • emits nothing. A HUD is a pure consumer.

Dependencies

Transform6DOF, Velocity6DOF, MachineGun, MissileLauncher, SpaceShooterHealth on the player; RoundReset for the death countdown; Parts and Renderable on whatever you lock onto, for the target view. WaypointTrack on the player is optional — the gate counter appears only when a track exists, because an instrument reading 0 / 0 is worse than no instrument.

Notes

  • The bracket tracks the ART, not the transform. A glTF's origin sits wherever the artist left it — on these hulls about a tenth of a unit off the visible centre. That is nothing at range and grows as you close, because the screen error scales with 1/distance, so it is worst exactly in a knife-fight when you are staring at the mark. The bracket is centred on renderer.getMeshWorldBounds(...) and SIZED from the same box, so shipVisualSize is now only the fallback for a ship with no model loaded rather than a claim about art it has never seen. Measured on the live cabinet: 31px off the hull before, 0.3px after.
  • Enemy brackets filter on .active. Enemies are pooled and a parked slot keeps its tags, so without the filter the HUD brackets every dead ship sitting in the pool at the origin.
  • A bracket is suppressed only when it overlaps your ship AND has clear line of sight. The second half matters: something tracked behind an asteroid must still draw (dashed, since you can't lock through rock), which is why the lock is line-of-sight-gated separately.
  • FOV is read from the scene camera rather than duplicated. The apparent-size maths is only right if it matches what's actually rendering, and a hardcoded copy with a "must match" comment is the kind of pairing that drifts.
  • Unpauseable, so the death countdown keeps running if the host pauses the world on death.
  • The sun marker dims rather than disappears behind a rock — the sun is the only fixed landmark in an empty sky, so it must not vanish exactly when you want to find it. It's drawn as a starburst because every star in the backdrop is a plain dot; a cross means one thing.
  • The overlay is torn out of the document on shutdown, and guarded by a document check so the System is safe headlessly.
  • The escort marker survives occlusion too, dimming rather than vanishing. An asteroid sliding in front of your convoy is the worst possible moment to stop telling you where it is.
  • The target-view plugin may refuse, and that is ordinary: the game camera is minted on the first live frame and a hull may still be loading. The System takes the schematic for that frame and asks again on the next one. A plugin that remembered a failed id as "shown" would never be asked again, and the panel would stay empty for the rest of the run with nothing logged anywhere — which is exactly the bug this contract is shaped to prevent.

More like this

CameraState6DOF
MachineGun
MissileLauncher
RoundReset
SpaceShooterHealth
Transform6DOF
Velocity6DOF
Asteroid
AsteroidField
CardFan
ChaseCamera6DOF
ChaseCameraTarget
EngineFlame
FlightIntent
FreighterCar
FreighterChain
FreighterHead
GameConfig
HealthBar
LensFlare
NebulaSky
OilBlob
PlayerFlightInput
PokerHUD
PokerTableCards
Renderable
Score
Scoreboard
ShipDamageFX
ShipFlight
SpaceDust
SpaceShooterBullet
SpaceShooterFX
SpaceShooterScore
WaveDirector
Waypoint
WaypointTrack
WorldOriginAnchor

Was this page helpful?

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

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search