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.
On this page
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 class | Draws | You implement |
|---|---|---|
TextHudElement | lines of text with an optional title | lines(), and usually sampleLines() and title() |
BarHudElement | a label and a value above a rounded progress bar | progress(), and usually label() and valueText() |
IconTextHudElement | a 16-pixel item icon beside one or more lines | icon() and lines() |
HudElement | anything | width(), 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:
| Part | Why |
|---|---|
| an inner class of the feature | it 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:
| Helper | Gives |
|---|---|
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 oneaddHud 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 1There 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 row | Positive x / y moves the element |
|---|---|
| left column, top row | right / down, away from the left or top edge |
| centre column, centre row | right / down from the centred position |
| right column, bottom row | left / 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 manager | Details |
|---|---|
| places it | anchor, offset and scale, clamped to the screen |
| frames it | padding (4 by default), a background in the theme's HUD tone, a corner radius |
| styles it | text shadow, alignment (left, centre or right), an optional glow, all per element |
| hides it | while the element's own switch is off, shouldRender() is false, or the player has hidden the HUD with F1 |
| arranges it | inside the containers and pins the player builds in the editor |
| layers it | in the draw order the player sets |
| saves it | in 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()orsampleProgress()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 rawHudElementchecks theeditorargument ofrenderand 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.
| Kind | Measures |
|---|---|
WARNING | a condition that has fired or is about to |
PHASE | a fight or run entering a new phase |
RESOURCE | a pool that is emptying: fuel, mana, air |
TIMER | a countdown; the default kind |
PROGRESS | something 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()andrender()share the result.- In a raw element,
width()andheight()are called more than once a frame. Cache anything expensive with theFrameCachehelperHudElementprovides, 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 fromrender().