The HUD
Lesson 14.3
The HUD is everything drawn over the world while you play: hearts, the hotbar, the crosshair. Mods can add their own HUD layers: a counter in the corner, a compass bar, a cooldown icon.
A first layer
cl.on("lifecycle.client_setup", function()
cl.hud.register_layer{
id = "hello",
render = function(g)
g:text("Hello, HUD!", 4, 4, 0xFFFFFF, true)
end,
}
end)
idnames the layer (it becomesmy_mod:hello).renderis called every frame with a drawing context,g.g:text(text, x, y, colour, shadow)draws text.xandyare in GUI pixels from the top-left corner, andshadow = truegives the vanilla text shadow.
GUI pixels follow the player’s GUI scale setting, so your layer has the same size as the vanilla HUD at every scale. g:width() and g:height() give the screen size in those pixels.
Layers are hidden with F1 like the rest of the HUD.
Colours
Colours are 0xAARRGGBB: alpha (opacity), then red, green and blue. If you leave out the alpha, as in 0xFFFFFF, the colour is fully opaque. For see-through colours, write the alpha: 0x80000000 is black at half opacity.
Drawing
| Method | Draws |
|---|---|
g:text(s, x, y, colour, shadow) |
Text; returns the x where it ended. |
g:text_centered(s, cx, y, colour, shadow) |
Text centred on cx. |
g:fill(x0, y0, x1, y1, colour) |
A filled rectangle from corner to corner. |
g:fill_gradient(x0, y0, x1, y1, top, bottom) |
A rectangle fading from the top colour to the bottom one. |
g:outline(x, y, w, h, colour) |
A 1-pixel frame. |
g:item_by_name(id, x, y) |
An item icon, 16 x 16. |
g:item(stack, x, y, decorations) |
A stack’s icon, with count and durability bar if decorations is true. |
g:sprite(id, x, y, w, h) |
A GUI sprite, like "minecraft:hud/heart/full". |
g:tooltip(s, x, y) |
A vanilla tooltip box. |
cl.gui.text_width(s) gives a text’s width in pixels, and cl.gui.line_height() the height of a line. Together they make layouts that fit:
render = function(g)
local label = "Rubies: 12"
local w = cl.gui.text_width(label)
local x = g:width() - w - 6 -- right edge, 6 pixels in
g:fill(x - 3, 3, x + w + 3, 15, 0x80000000)
g:text(label, x, 5, 0xE0115F, true)
end
Your own sprites go into assets/my_mod/textures/gui/sprites/. assets/my_mod/textures/gui/sprites/ruby_icon.png is drawn with g:sprite("my_mod:ruby_icon", x, y, 16, 16).
Scaling and moving
g:push_pose() saves the current position and size, g:translate(x, y) and g:scale(sx, sy) change them, and g:pop_pose() goes back:
g:push_pose()
g:scale(2, 2)
g:text("BIG", 10, 10, 0xFFFF55, true) -- drawn at 20, 20 and twice the size
g:pop_pose()
Every push_pose needs a pop_pose, or everything drawn after it is moved as well.
Keep render cheap
render runs every frame: 60, 144 or more times a second. Don’t search the inventory or the world in it. Work out values in tick.client_post (20 times a second) and only draw them in render:
local rubies = 0
local function count_rubies(player)
local n = 0
for slot = 0, 40 do
local stack <close> = player:inventory_get(slot)
if stack and stack:is("my_mod:ruby") then n = n + stack:count() end
end
return n
end
cl.on("lifecycle.client_setup", function()
cl.on("tick.client_post", function()
local me = cl.entity.client_player()
rubies = me and count_rubies(me) or 0
end)
cl.hud.register_layer{
id = "rubies",
visible = function() return rubies > 0 end,
render = function(g)
g:item_by_name("my_mod:ruby", 4, 4)
g:text("x" .. rubies, 22, 8, 0xFFFFFF, true)
end,
}
end)
visible is asked before drawing. While it returns false, render isn’t called at all, which costs nothing.
Where the layer goes
Without more options, a layer is drawn on top of the vanilla HUD. above and below put it between vanilla layers:
cl.hud.register_layer{
id = "mana",
above = "minecraft:hotbar",
render = function(g) ... end,
}
To sit just above the hearts and the food bar like vanilla’s armour and air bubbles, ask how high they reach: cl.hud.left_height() (hearts side) and cl.hud.right_height() (food side) are the heights above the bottom of the screen.
register_layer returns an object; call :cancel() on it to remove the layer again.
Try it
Draw a small clock in the top-right corner: a dark box with the time of day as HH:MM, where 0 ticks is 06:00 and the day has 24000 ticks. Compute the text in the client tick.
Show a solution
local clock = ""
cl.on("lifecycle.client_setup", function()
cl.on("tick.client_post", function()
local world = cl.level.client_level()
if not world then clock = "" return end
local t = world:day_time() % 24000
local minutes = math.floor((t / 1000 + 6) * 60) % (24 * 60)
clock = ("%02d:%02d"):format(minutes // 60, minutes % 60)
end)
cl.hud.register_layer{
id = "clock",
visible = function() return clock ~= "" end,
render = function(g)
local w = cl.gui.text_width(clock)
local x = g:width() - w - 6
g:fill(x - 3, 3, x + w + 3, 15, 0x90000000)
g:text(clock, x, 5, 0xFFFFFF, true)
end,
}
end)
1000 ticks are one in-game hour, so t / 1000 + 6 is the hour, and * 60 turns it into minutes.
Recap: cl.hud.register_layer{ id, render, visible } draws every frame with g:text, g:fill and friends; compute values in the client tick and keep render cheap.