All pages

HUD elements

How a feature puts something on the HUD that players can move, scale and restyle - the three ready-made element shapes and the raw one, anchors and positions, what the HUD manager does for you, what the HUD editor expects, urgency for the Director, and how positions are saved.

A HUD element is anything a feature draws on the in-game HUD that the player can move. Vantage gives every element the same treatment for free: the HUD editor can drag, scale, restyle, lock, group and pin it, its position is saved per config profile, and its look follows the theme. This page is for someone writing one. It covers the base classes, how an element is registered and positioned, what the manager and the editor do with it, and the rules that keep a few hundred of them cheap. The code is in core/hud/. As of 2.0.1.

Three shapes, and the raw one

Pick the base class that matches what you draw. Most elements in the mod are text.

Base classDrawsYou implement
TextHudElementlines of text with an optional titlelines(), and usually sampleLines() and title()
BarHudElementa label and a value above a rounded progress barprogress(), and usually label() and valueText()
IconTextHudElementa 16-pixel item icon beside one or more linesicon() and lines()
HudElementanythingwidth(), height() and render(graphics, editor)

Every one of them takes the same two constructor arguments: a display name, shown in the HUD editor's element list, and a default Position.

protected TextHudElement(String name, Position defaultPosition)

A text element, step by step

This is a complete element from the mod, the readout of the Volcano explosivity feature on the Crimson Isle:

@Override
protected void onInit() { addHud(new VolcanoHud()); }

private final class VolcanoHud extends TextHudElement {
    private VolcanoHud() {
        super("Volcano explosivity", new Position(Anchor.TOP_LEFT, 6, 6, 1f));
    }

    @Override protected @Nullable Component title() { return null; }

    @Override protected List<Component> lines() {
        if (!isActive()) return List.of();
        String status = status();
        if (status == null) return List.of();
        return List.of(pair("Volcano explosivity: ", status));
    }

    @Override protected List<Component> sampleLines() { return List.of(pair("Volcano explosivity: ", "56%")); }
}

What each part is for:

PartWhy
an inner class of the featureit can read the feature's fields and call isActive() directly
lines()what to show this frame. An empty list hides the element, unless it overrides renderWhenEmpty()
if (!isActive()) return List.of()the HUD manager draws registered elements whether or not their feature is active, so the element hides itself
sampleLines()what the HUD editor shows, so the player can place the element before there is anything real to show
title()an optional first line drawn in the theme's title tone; null for none

TextHudElement measures its width and height from the font it draws with, so you never compute a size. A line that throws while it is being built is shown as a red Error: <exception> line instead of taking the HUD down.

Building lines

TextHudElement has static helpers that give lines the theme's colours, read fresh every frame, so a theme change repaints every element that did not choose its own colours:

HelperGives
line(text), line(text, argb), line(text, ChatFormatting)a plain line, or one in a fixed colour
pair(label, value)a muted label and a bright value, the shape of most HUD lines
pair(label, value, valueColor), pair(label, Component)a pair whose value carries its own colour or component
pairColumn(label, value, px)a pair whose values line up in a column, padded with measured spaces
join(components…)facts on one line separated by a muted " · "
progressBar(fraction, segments)a bar made of coloured segments, for a text line
appendMuted(component, suffix), emptyState(text)a muted suffix, or a muted "nothing yet" line

Two hooks shape the block. minWidth() reserves a width so a number that changes length every tick does not make the element twitch. visible() adds a condition of your own on top of the element's switch.

Registering it

An element is registered from the feature's onInit():

addHud(new VolcanoHud());            // id: crimson_volcano_explosivity
addHud(new BossTimer(), "timer");    // id: <feature id>.timer, for a feature with more than one

addHud gives the element its id, which is the feature's id, or <feature id>.<suffix> when you pass a suffix. Only features that own more than one element need a suffix. Keep it short and never rename it casually: the id is the key the player's position and style are saved under.

addHud also files the element under its feature's group, so the HUD editor lists it under the same heading the menu uses, with the category's icon. The registry hands every element to the HUD manager after the feature's onInit() returns.

Position and anchors

A Position is an anchor, an offset and a scale:

new Position(Anchor.TOP_RIGHT, 6, 60, 1f)   // 6 px in from the right edge, 60 px down, scale 1

There are nine anchors, the corners, the edge midpoints and the centre: TOP_LEFT, TOP_CENTER, TOP_RIGHT, CENTER_LEFT, CENTER, CENTER_RIGHT, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT. The offsets are GUI pixels measured from the anchor and pointing inward:

Anchor column or rowPositive x / y moves the element
left column, top rowright / down, away from the left or top edge
centre column, centre rowright / down from the centred position
right column, bottom rowleft / up, away from the right or bottom edge

So an element anchored TOP_RIGHT with x = 6 keeps its right edge six pixels from the right of the screen at every resolution. Whatever the numbers, the manager clamps the element back onto the screen. Scale is applied around the anchor and is held between 0.5 and 3, the same limits the editor's scale control has.

What the manager does for you

Your element draws its content at its own origin, unscaled. The HUD manager does everything around it:

The managerDetails
places itanchor, offset and scale, clamped to the screen
frames itpadding (4 by default), a background in the theme's HUD tone, a corner radius
styles ittext shadow, alignment (left, centre or right), an optional glow, all per element
hides itwhile the element's own switch is off, shouldRender() is false, or the player has hidden the HUD with F1
arranges itinside the containers and pins the player builds in the editor
layers itin the draw order the player sets
saves itin hud.json, per config profile

Rendering happens through Fabric's HUD element registry, after the vanilla HUD and before chat, only while a world is loaded and never while the HUD editor itself is open. An element that throws while drawing is skipped, and the error is logged at most once in a while rather than every frame.

Style defaults and reset

Every element carries the same style fields, and the editor's inspector edits them: enabled, locked, background, backgroundColor, textShadow, align, padding, cornerRadius, glow, glowStrength and glowColor. Set different defaults in your element's constructor or field initialisers:

PaceHud() {
    super("Purse pace", new Position(Anchor.TOP_RIGHT, 6, 60, 1f));
    background = false;
    align = Align.RIGHT;
}

When the element is registered, the manager records those values as this element's own defaults before it applies anything saved. A reset in the editor restores them, not the base class's. backgroundColor and glowColor left null follow the theme; glow is off by default and meant for the few elements that genuinely deserve to stand out.

For colours inside the element, the getters textColor() and titleColor() (and on bars barColorTop(), trackColor() and the rest) return the theme's tone unless you set an override, and are read every frame. Read colours through them rather than storing a hex value; The UI toolkit explains why the theme is read at draw time.

What the editor expects

The HUD editor draws every element with editor set to true, and it ignores shouldRender(). Four things follow:

  • Sample content. sampleLines(), sampleIcon() or sampleProgress() must return something representative, because the player places the element in the editor, usually far from where its real content appears. The defaults fall back to the live content, then to the element's name. A raw HudElement checks the editor argument of render and draws a sample itself.
  • A stable size. Containers re-flow their members every frame from their sizes, so an element whose width jumps makes its neighbours jump too. Use minWidth() on text that changes length.
  • The right settings. Right-clicking an element opens the inspector, which shows the style fields and the options of the feature that registered the element. If the element belongs to a different feature's settings, override extraOptions() to return that feature.
  • A name. The display name passed to the constructor is the element's label in the editor's list, and the editor's search matches it along with the id and the group.

The same sample content is used on the feature's page in the menu, where the first HUD element of a feature is drawn live as its preview. The player's side of all this is on The HUD editor.

Urgency and the Director

The Director is an optional feature, off by default, that ranks elements by how much they need the player's attention and can hide or dim the ones that do not. An element takes part by answering two questions:

@Override public float urgency() {
    if (!spawnTimer || !spawnCountdown.isRunning() || spawnDelayMs <= 0) return 0f;
    long remaining = spawnCountdown.remaining(spawnDelayMs);
    return 1f - Math.min(1f, remaining / (float) spawnDelayMs);
}

@Override public UrgencyKind urgencyKind() { return UrgencyKind.TIMER; }

That is the Arachne spawn timer. urgency() returns 0 for "nothing to say" up to 1 for the terminal value: the timer reached zero, the tank emptied, the warning fired. Be honest within the kind; the Director decides how kinds compare.

KindMeasures
WARNINGa condition that has fired or is about to
PHASEa fight or run entering a new phase
RESOURCEa pool that is emptying: fuel, mana, air
TIMERa countdown; the default kind
PROGRESSsomething filling up; never urgent

The default urgency() returns URGENCY_UNCLASSIFIED, and the Director never touches an unclassified element, so taking part is opt-in. urgency() is called about five times a second for every element, so it must be cheap and must not allocate; an override that throws is logged once and treated as unclassified for the rest of the session. urgentLine() may return a short line, at most 24 characters, that the Director's stage shows instead of the element itself.

The Director's gate runs after the element's own visibility check. It only ever hides or dims something that would have drawn, never shows something that would not, and the HUD editor bypasses it entirely.

Ids and saved positions

The layout lives in config/vantage/hud.json, one layout per config profile. Abridged, with one element:

{
  "version": 3,
  "profiles": {
    "default": {
      "crimson_volcano_explosivity": {
        "anchor": "TOP_LEFT", "x": 6.0, "y": 6.0, "scale": 1.0,
        "enabled": true, "background": true, "textShadow": true,
        "align": "LEFT", "padding": 4
      },
      "__order": ["crimson_volcano_explosivity"]
    }
  }
}

Each entry is keyed by element id and holds the position and the style fields, its corner radius included; a colour, glow, lock or pin is written only when it is set. __order is the layer order, and __groups, written once the player has made a container, holds the containers. An entry for an id the running build does not register is kept and written back, so removing an element does not throw away its saved layout.

Because the id is the key, renaming an element's id, or its feature's id, sends every player's element back to its default position. HudIdMigration is the precedent: when element ids stopped being written by hand and started coming from the feature, it rewrote the old ids everywhere they appear: the element keys, the layer order, each container's members and every pin. A rename of your own needs the same treatment.

Keeping it cheap

The HUD draws every frame, and a full HUD can hold a few hundred elements.

  • lines() on a text element is called at most once per frame and cached; width(), height() and render() share the result.
  • In a raw element, width() and height() are called more than once a frame. Cache anything expensive with the FrameCache helper HudElement provides, keyed on the frame and on whether the editor is open.
  • Do not allocate or look things up in urgency(); return a number the element already has.
  • Read game state from SkyblockState, whose getters are cheap enough to call every frame, rather than scanning the world from render().