Anchor UI to the World
Put health bars, prompts, name plates and damage numbers above actors as ordinary CalmUI elements, without publishing a position through the Store.
A health bar above an enemy has to move with the enemy, every frame. The obvious
way to build one, having the game project the actor to screen space and write the
coordinates into a Store field bound to an inline left/top, is the wrong
way: every published coordinate is a document reload, a full relayout and a full
style resolution. Forty enemies cost forty of each, per frame. The cost forces a
throttle, and the throttle is what makes the bar lag behind a strafing enemy.
This is the reason over-actor UI is normally built in UMG instead, which splits
your HUD across two UI systems, two stylesheets and two localization paths.
Mark the element instead. The game publishes an anchor table once per frame, and the engine places every anchored element from it:
<template>
<div class="plate" :data-calmui-anchor="anchorId">
<span class="name">{{ name }}</span>
<div class="bar"><div class="fill" :style="`width:${health}%`"></div></div>
</div>
</template>
<style scoped>
.plate {
position: absolute;
pointer-events: none;
--calmui-anchor-offset-y: -24px;
}
</style>The game publishes what the plate says (name, health) through the Store as
usual, and the actor's position through the anchor table. The two never mix.
Requirements
The element must be position: absolute or position: fixed. An anchored
element is placed, not laid out, so it must be out of normal flow, otherwise it
would drag its siblings around as it moves.
Opting in
data-calmui-anchor holds the anchor id the host published. Any non-empty value
anchors the element; an empty value, or no attribute at all, leaves it exactly
where CSS put it. That makes the opt-in bindable, so one component can serve
both a world-anchored and a pinned prompt:
<aside class="prompt" :data-calmui-anchor="pinned ? '' : actorId">Placement
The element's origin point is put on the anchor point plus the offsets, and the box is scaled by the anchor's scale about that point.
| Custom property | Default | Meaning |
|---|---|---|
--calmui-anchor-offset-x | 0 | Horizontal distance in logical px from the anchor point. |
--calmui-anchor-offset-y | 0 | Vertical distance in logical px from the anchor point. |
--calmui-anchor-origin | bottom-center | Which point of the box lands on the anchor point: bottom-center, center, top-center, bottom-left or top-left. |
--calmui-anchor-clamp | none | none lets the box leave the screen with its actor. viewport keeps it on screen. |
--calmui-anchor-edge | 8 | Logical px kept free at the viewport edge while clamping. |
Declare them wherever you would declare any other property: a workspace
stylesheet listed in calmui.config.json, a component's <style scoped>, a
:root token, or an inline style. The engine reads the element's resolved
value, so a plain CSS rule works and the cascade applies as usual. Use a plain
length: a value the engine cannot read as pixels, or an origin/clamp keyword it
does not know, falls back to the default rather than being misread.
bottom-center is the default because the common case is a plate sitting above
a head or socket: the box grows upwards from the anchor instead of straddling it,
so a two-line name plate does not cover the face. A negative
--calmui-anchor-offset-y lifts it clear of the head.
Offsets are authored in UI pixels and are not multiplied by the anchor's scale. A 24px gap above the head stays 24px when the actor is far away and the plate shrinks, instead of collapsing into the head at distance.
Use --calmui-anchor-clamp: viewport for something the player must not lose:
an interaction prompt or an objective marker slides along the screen edge instead
of leaving it. Leave it at none for a damage number, which should fly off with
its actor.
Appearing and disappearing
When the host's table has no entry for an element's anchor id, or the entry is marked invisible, the element is not drawn, and it and everything inside it are excluded from hit testing.
Layout does not change. Nothing reflows when a plate appears or disappears, and a hidden plate costs nothing beyond a table lookup. Keep the whole roster of plates in the document and let the host's table decide which ones are on screen; do not add and remove them through the Store.
Hit testing
An anchored element is hit tested, at its placed position. Unlike a
pointer-following element it is not removed
from hit testing, because an interaction prompt or a badge may legitimately be
clickable. A plate that should never take a click (a health bar, a damage number)
needs pointer-events: none like any other decorative element.
Paint order
Every anchor carries a depth, where a larger value means farther from the camera. Depth is recorded but not used to sort painting yet: overlapping plates paint in document order, exactly as they would without anchoring. If a near plate must cover a far one today, order the elements yourself.
What it costs
Nothing per frame beyond a transform. Anchoring never triggers a layout, a style resolution or a document reload; it composes onto the same per-node transform the renderer already applies, so the element's own transitions and keyframe animations keep working underneath it. A damage number that floats up and fades out still floats up and fades out while it tracks its actor.
Designing without the game
Studio's Store preset carries a mock anchor table under the $anchors key, so a
screen can be designed and screenshotted with its anchored elements in place:
{
"$anchors": {
"enemy-01": { "x": 640, "y": 360, "visible": true, "scale": 1, "depth": 0 }
}
}$anchors is not a Store field. It is stripped before the preset's Store values
are validated, so it never appears in the Store panel and never reaches a
binding, and saving the Store from Studio writes the field values back without
disturbing it. Select an anchored element and the Inspector shows anchor: <id>.
Studio publishes the mocked table to the preview runtime after the package
loads, and again whenever the preset is reloaded or another preset is selected,
so calmui dev screenshot captures anchored elements in place. An id the preset
does not declare behaves exactly as it does in game: the element is not drawn and
does not hit test.
Publishing the table
The game publishes the table from Unreal, once per frame. See Anchor UI above actors.
Related
- Follow the pointer for UI that tracks the mouse rather than an actor.
- World-space and render-target UI for a View rendered into a texture on an in-game monitor. That is a different feature: anchored elements are flat screen UI, drawn in your HUD document.