Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Hud

<Hud> marks UI for the screen. Put one at any level: inside Game, a Scene, a World or an Entity. The engine draws what it holds into the screen layer that Game owns, over the canvas.

A Hud inside the world renders under three’s reconciler, where HTML cannot draw. The Hud carries its children out to the screen layer, and they keep every context around it. So useEntity() inside a Hud reads the Entity the Hud sits in, and the Hud’s UI lives as long as that entity.

A Hud holds Panels. Each Panel names the slot it stands in.

In the devtools tree, a Hud and the Panels and Buttons inside it have rows of their own under the scene; see the tree.

Under a camera that captures the cursor, as in a shooter, no click reaches the HUD. Each control a player presses there needs one of the following ways in:

  • A key, which the game binds and names on the control with aria-keyshortcuts, or keyShortcuts on a Button.
  • A screen that frees the cursor while it shows: a Modal, a Panel with freeCursor, or useFreeCursor. An inventory such as the Bag is one.
  • A place in the engine’s Play menu, which Escape opens, as the microphone button has.

In development, the engine warns of a control that has none of them; see the camera page.

import { Hud, Panel, Slot, Text, useEntity } from "@spawnite/engine";
export function Scoreboard() {
const entity = useEntity();
return (
<Hud>
<Panel slot={Slot.TopLeft}>
<Text>Entity {entity.id()}</Text>
</Panel>
</Hud>
);
}

Game draws the screen layer after its canvas and isolates the canvas, so the screen layer always draws over the world, including over a nameplate. The layer takes no pointer events itself: only a control such as a Button takes one, so a drag that starts between two buttons still turns the camera.

The layer holds the Huds in the order they stand in the tree. A Hud whose content changes is drawn again where it stands, and never moved, since a browser takes a moved dialog out of the top layer and a moved button loses its focus.

The layer keeps clear of the screen’s edges by four CSS variables, --game-inset-top, --game-inset-right, --game-inset-bottom and --game-inset-left. Each defaults to the device’s safe area. In the player app’s frame, the engine sets each edge the app’s own controls take from the app’s init, and again from each insets the app sends when its controls move, so the slots and a Modal stay clear of them; see the trust boundary.

Chrome builds a GPU program the first time each kind of effect in the page’s HTML and CSS draws: a blur behind a pane, a drop shadow, a gradient under a mask, a fading or scaling element, and the radii an animated filter: blur() passes through. On a player’s first visit each build holds the game’s next frames for 15 to 30 ms; a returning player’s browser keeps them on disk. So once the start scene has mounted, while the loading screen still covers the game and its models load, the engine draws the HUD’s effects out of sight:

  • What a Hud holds at load. The screen layer draws above the loading screen, at the lowest opacity Chrome still draws, so nothing in it takes a pointer, focus or a screen reader, and no pixel moves by more than 2 of 255. When the warm-up ends it is hidden until the screen fades, and it shows under the screen as the screen fades. Chrome skips drawing what an opaque layer covers, so a layer drawn under the screen would build nothing.
  • The engine’s and ui’s own effects. Game draws one of each in the same way, first: the panes, the menus, the Buttons, the action bar’s cooldown rim, the bars and the menu’s backdrop.
  • What the HUD shows later, such as a wave’s banner, a hit marker or a card. Put a copy of it in a <WarmHud>. Each WarmHud joins the warm-up a frame after the one before it, in the order they mounted, and stays until the warm-up ends. From the frame after it mounts, each animation in it that moves a blur’s or a shadow’s radius, such as a banner’s blurred entrance, is paused at each of its keyframes in turn, one a frame, and at four of them, evenly spread, when it has more, warmSteps; an animation of an opacity or a transform draws once, at its middle.

The order in the tree is the priority: put first what the player meets first, because the warm-up stops at Game’s warmUpMilliseconds, 6000 by default, and a WarmHud it has not drawn whole by then builds its programs on first use in play, as it would with no warm-up. 0 turns the warm-up off. The engine’s loading screen covers only the first load, so a later scene’s HUD builds on first use.

import { Game, Scene, Text, WarmHud } from "@spawnite/engine";
function Siege() {
return null;
}
export function App() {
return (
<Game name="siege" start="siege">
<Scene name="siege" component={Siege} />
{/* A copy of the banner each wave opens with. */}
<WarmHud>
<Text className="animate-banner-slam text-4xl">Wave 1</Text>
</WarmHud>
</Game>
);
}

A copy carries the real element’s classes, and its own short text. A component that reads the world or plays a sound is copied as plain elements with its classes, rather than rendered. A WarmHud sits anywhere and reads the contexts around it, as a Hud’s children do; beside the scenes, as above, it is ready when the warm-up starts. Copies outside the screen’s bounds build nothing, since Chrome skips drawing them too, so lay them out inside it; they may overlap. On a page that spawnite play profile opens, the engine marks what the warm-up drew, such as 1 of 3 WarmHuds drawn in 6012 ms, 2 dropped by the budget, or dropped as the screen lifted where the load ended first, and the profile prints it; the console warns whenever the budget drops one. spawnite play profile --trace counts the programs the page created after the loading screen lifted, as the frame page says: a copy belongs in a WarmHud for each effect left.

Building a program takes the GPU process’s time whenever it happens, so the warm-up moves that time from the first minute of play into the load, where it runs beside the models loading. Holdfast’s production build, headless at 120 Hz on a fresh profile, a first visit, three alternating runs each at 18 to 22% machine load:

Loading screen lifted Skia programs after the lift GPU-process tasks of 10 ms or more Slow frames a minute
No warm-up 7.75, 7.56, 8.74 s 72, 82, 24 16, 17, 11 5.0, 7.8, 6.4
The engine’s warm-up alone 7.93, 7.55, 8.86 s 29, 59, 59 9, 14, 15 2.1, 6.4, 6.4
With Holdfast’s copies 8.20, 8.90, 8.91 s 31, 23, 23 4, 5, 7 2.1, 2.8, 4.3
  • The engine’s part adds about 0.2 s to the lift, by the medians; Holdfast’s copies about 1 s more. Its warm-up drew both in 2.0 s.
  • A return visit pays nothing: Chrome loads the programs from its disk cache, the warm-up runs in about 0.9 s, and the screen lifted at 3.60, 3.42 and 3.42 s against 4.86, 3.35 and 3.35 s with none.
  • The budget holds a long list back: with five copies of Holdfast’s WarmHud, the warm-up dropped two of six in three of four runs, and the screen lifted at 8.65 to 9.74 s against 7.40 to 8.37 s with none. The warm-up checks its budget once a frame, and a loading page’s frame can take a second or two, so it can run past the budget by that much.