6 · The HUD
A screen is a device like the strip is a device. You do not push pixels at it — you declare what is on it, and the engine redraws the whole thing from your numbers, sixty times a second.
1 · Declare a screen
Section titled “1 · Declare a screen”SETUP ▸ Devices ▸ + new device ▸ hud. Set kind to screen.
Set width 320 and height 170 — the coordinate space you will draw
in. Match your panel, or leave them at 0 for the backend default. The
rotation dropdown says (build), meaning “whatever the firmware was built
for”; change it and set width and height to match, or the drawing and the panel
will disagree about which way up they are.
Watch the palette while you do this. The whole Screen group and the Widgets group were dimmed; the moment the screen device exists, fifteen verbs light up. That is the palette telling you what your show can now do.
2 · One script, many layers
Section titled “2 · One script, many layers”Scripts ▸ + new script ▸ hudView. On its card, tick autostart.
Three things are doing the work here.
autostart starts the script when the show loads, without any state having
to. The HUD is not part of the blaster’s behaviour; it is always on.
Every child’s flow is parallel. They are layers, drawn top to bottom in
list order, not steps. Drag a row to a different position (the ⠿ handle on the
left of every row) and you change what is drawn over what.
The first layer is a fill. The framebuffer is persistent — it is not
cleared for you — so without a black fill at the top of the list, last frame’s
reticle stays on screen and the display smears. Clear first, always.
3 · The verbs you now have
Section titled “3 · The verbs you now have”| Palette group | Verbs | Draws |
|---|---|---|
| Screen | fill |
the whole canvas one colour |
| Screen | rect circle line pixel polygon polyline |
shapes, each with an outline checkbox |
| Screen | text |
a fixed string |
| Screen | number |
a live var or expression, with dig to zero-pad |
| Screen | image anim |
a still, or a baked clip |
| Screen | paint |
a per-pixel expression over the whole canvas |
| Widgets | gauge meter graph |
a radial dial, a bar, a scrolling plot |
Each of those rows carries its own geometry boxes — circle gets x, y, r
and an outline tickbox; line gets x0,y0 → x1,y1; meter gets val, a
[min..max] pair and x, y, w, h.
4 · The reticle
Section titled “4 · The reticle”
Add a circle: x 215, y 85, r 42, outline ticked. Then in its
colour box, instead of picking a swatch, type:
hsv(0.33 - heat*0.33, 0.9, 0.9)Hue 0.33 is green; 0.33 - heat*0.33 walks it through amber to red as the
barrel heats. Paste the same expression into the four line ticks and the
centre dot, and the whole sight changes colour together with nothing
coordinating it.
5 · A live number
Section titled “5 · A live number”text takes a fixed string. number takes a var or an expression and
formats it — which is the difference between a label and an instrument.
Add a number with ammo in its value box, dig 2 to zero-pad, size 4,
and this in its colour box:
ammo > 0 ? 0x00e0a0 : 0xff3b30An empty magazine turns the counter red, with no state involved.
A meter does the same job as a bar — val heat, [0..1], and a colour
expression of hsv(0.33 - heat*0.33, 1, 1).
6 · The annunciator row
Section titled “6 · The annunciator row”
Because text cannot change its string, a line that has to change is several
text rows, each lit by its own colour expression:
| Row | value | x | colour expression |
|---|---|---|---|
| 1 | READY |
150 | venting == 0 && ammo > 0 ? 0x00e0a0 : 0x1d2a24 |
| 2 | EMPTY |
200 | ammo <= 0 ? 0xff3b30 : 0x2a1d1d |
| 3 | VENT |
252 | venting ? 0xffb000 : 0x2a2418 |
7 · Report the state, not a proxy for it
Section titled “7 · Report the state, not a proxy for it”
The first version of that row read heat >= 1 for VENT. It was wrong in a way
worth showing: the vent script starts pulling heat down immediately, so heat
drops below 1 on the very next frame, VENT flickers once, and the HUD goes back
to claiming READY while the barrel is still venting.
The fix is a var the state machine owns. Add venting in Variables, then on
the Overheated card:
- on enter —
setventing=1, thenrunvent - on exit —
setventing=0
A var is how a state tells the display what it is doing. Any time a readout infers the mode from a physical quantity, it will eventually infer it wrong.
What the editor wrote
Section titled “What the editor wrote”{
// ===========================================================
// BLASTER · STEP 6 — The HUD
// A screen is a device like any other. You do not blit
// images at it; you declare what is drawn on it, and the
// engine redraws every frame from the numbers you track.
// ===========================================================
"schemaVersion": 1,
"name": "Blaster 06 - The HUD",
"board": "t-display-s3",
"vars": { "heat": 0, "ambient": 0, "ammo": 12, "venting": 0 },
"devices": {
"barrel": { "kind": "ledstrip", "count": 8, "pin": 16, "order": "GRB", "brightness": 160 },
// No pin: a screen is a framebuffer, and the board knows
// which panel it is wired to.
"hud": { "kind": "screen", "width": 320, "height": 170 }
},
"inputs": {
"trigger": { "label": "TRIGGER", "color": "#ff3b30", "pin": 14, "activeLow": true, "pull": "up" },
"reload": { "label": "RELOAD", "color": "#2ca7ff", "pin": 0, "activeLow": true }
},
"scripts": {
// The HUD is one script, started once and left running. Every
// child is `parallel`: they are LAYERS drawn in order, not
// steps, so all of them are on screen at the same time.
//
// It starts with a full-canvas fill because the framebuffer
// is persistent — without it, last frame's reticle smears.
"hudView": {
"autostart": true,
"children": [
{ "fill": "@hud", "color": "#000000", "flow": "parallel" },
// ---- left column: ammo ----------------------------------
{ "text": "@hud", "value": "AMMO", "x": 12, "y": 14, "size": 1,
"color": "#4a5560", "flow": "parallel" },
{ "number": "@hud", "value": "ammo", "digits": 2, "x": 12, "y": 28, "size": 4,
"color": "ammo > 0 ? 0x00e0a0 : 0xff3b30", "flow": "parallel" },
// ---- left column: heat ----------------------------------
{ "text": "@hud", "value": "HEAT", "x": 12, "y": 118, "size": 1,
"color": "#4a5560", "flow": "parallel" },
{ "meter": "@hud", "value": "heat", "min": 0, "max": 1,
"x": 12, "y": 132, "w": 120, "h": 16,
"color": "hsv(0.33 - heat*0.33, 1, 1)", "flow": "parallel" },
// ---- the reticle ----------------------------------------
// Ring, four ticks, centre dot. The whole thing is one
// colour expression, so it goes green -> red with heat and
// the reader can see WHY by reading one line.
{ "circle": "@hud", "x": 215, "y": 85, "r": 42, "outline": true, "flow": "parallel",
"color": "hsv(0.33 - heat*0.33, 0.9, 0.9)" },
{ "circle": "@hud", "x": 215, "y": 85, "r": 3, "flow": "parallel",
"color": "hsv(0.33 - heat*0.33, 0.9, 1)" },
{ "line": "@hud", "x0": 215, "y0": 30, "x1": 215, "y1": 39, "flow": "parallel",
"color": "hsv(0.33 - heat*0.33, 0.9, 0.9)" },
{ "line": "@hud", "x0": 215, "y0": 131, "x1": 215, "y1": 140, "flow": "parallel",
"color": "hsv(0.33 - heat*0.33, 0.9, 0.9)" },
{ "line": "@hud", "x0": 160, "y0": 85, "x1": 169, "y1": 85, "flow": "parallel",
"color": "hsv(0.33 - heat*0.33, 0.9, 0.9)" },
{ "line": "@hud", "x0": 261, "y0": 85, "x1": 270, "y1": 85, "flow": "parallel",
"color": "hsv(0.33 - heat*0.33, 0.9, 0.9)" },
// ---- annunciator row ------------------------------------
// `text` draws a FIXED string, so a line that has to change
// is several lines, each lit by its own colour expression.
//
// Give every word its OWN SLOT and dim the inactive ones,
// rather than stacking all three in one place and blacking
// two of them out. A glyph is drawn wherever its bits are
// set — including when that colour is black — so a black
// word on top punches holes in the bright one underneath.
{ "text": "@hud", "value": "READY", "x": 150, "y": 150, "size": 1, "flow": "parallel",
"color": "venting == 0 && ammo > 0 ? 0x00e0a0 : 0x1d2a24" },
{ "text": "@hud", "value": "EMPTY", "x": 200, "y": 150, "size": 1, "flow": "parallel",
"color": "ammo <= 0 ? 0xff3b30 : 0x2a1d1d" },
{ "text": "@hud", "value": "VENT", "x": 252, "y": 150, "size": 1, "flow": "parallel",
"color": "venting ? 0xffb000 : 0x2a2418" }
]
},
"idle": {
"children": [
{ "follow": "heat", "target": "ambient", "rate": 0.22 },
{ "pattern": "@barrel", "loop": true,
"color": "hsv(0.09 - heat*0.09, 1, 0.16 + 0.55*heat + 0.10*sin(time*3 + i*0.7))" }
]
},
"shot": {
"children": [
{ "tone": 720, "wave": "square", "seconds": 0.09, "volume": 0.35, "flow": "detached" },
{ "set": "@barrel", "color": "#ffffff", "sec": 0.04 },
{ "comet": "@barrel", "color": "hsv(0.09 - heat*0.09, 0.9, 1)", "lapSec": 0.2, "tail": 5, "for": 0.2 },
{ "wait": 0.07 }
]
},
"vent": {
"children": [
{ "play": "powerdown.wav", "volume": 0.6, "flow": "detached" },
{ "ramp": "heat", "to": 0, "rate": 0.5, "flow": "detached" },
{ "blink": "@barrel", "color": "#4d8cff", "off": "#02060f", "sec": 0.3, "for": 2.2 }
]
},
"reloadSeq": {
"children": [
{ "play": "powerup.wav", "volume": 0.6, "flow": "detached" },
{ "chase": "@barrel", "color": "#00e0a0", "off": "#001a12", "sec": 0.06, "for": 0.7 },
{ "set": "ammo", "to": 12 }
]
}
},
"rules": [
{ "when": "heat >= 1", "to": "Overheated" }
],
"states": {
"initial": "Ready",
"Ready": {
"enter": [ { "start": "idle" } ],
"exit": [ { "stop": "idle" } ],
"on": {
"trigger.tapped": { "to": "Firing", "when": "heat < 1 && ammo > 0" },
"reload.tapped": { "to": "Reloading", "when": "ammo < 12" }
}
},
"Firing": {
"enter": [
{ "set": "heat", "to": "min(heat + 0.17, 1)" },
{ "set": "ammo", "to": "max(ammo - 1, 0)" },
{ "run": "shot" }
],
"on": { "shot.done": "Ready" }
},
"Reloading": {
"enter": [ { "run": "reloadSeq" } ],
"on": { "reloadSeq.done": "Ready" }
},
"Overheated": {
// A var is how a state tells the display what it is doing.
// The annunciator used to read `heat >= 1`, which is dark
// again a frame after the vent starts pulling heat down —
// so VENT flickered once and the HUD went back to claiming
// READY while the prop was still venting.
"enter": [ { "set": "venting", "to": 1 }, { "run": "vent" } ],
"exit": [ { "set": "venting", "to": 0 } ],
"on": { "vent.done": "Ready" }
}
}
}
Next: 7 · Sound — and a charge-up whose pitch is an expression.