The HUD
Lesson 9.2
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
static void hello_render(cl_gui g, float partial_tick, void* user) {
(void)partial_tick;
(void)user;
cl_gui_text(g, CL_STR("Hello, HUD!"), 4, 4, 0xFFFFFFFF, true);
}
static void on_client_setup(void* payload, void* user) {
(void)payload;
(void)user;
cl_hud_layer_desc d = CL_HUD_LAYER_DESC_INIT;
d.id = CL_STR("my_mod:hello");
d.render = hello_render;
cl_hud_layer_register(&d, NULL);
}
idnames the layer, in your namespace.renderis called every frame with a drawing context,g. The context is only valid inside that call.cl_gui_text(g, 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. cl_gui_width(g) and cl_gui_height(g) give the screen size in those pixels.
Layers are hidden with F1 like the rest of the HUD.
Colours
Drawing colours are 0xAARRGGBB: alpha (opacity), then red, green and blue. 0xFFFFFFFF is opaque white, 0xFFE0115F opaque ruby red, and 0x80000000 black at half opacity. If you leave out the alpha, as in 0xFFFFFF, the colour is fully opaque, like vanilla’s text colours. So a colour can’t be completely invisible; to hide something, don’t draw it.
Drawing
| Function | Draws |
|---|---|
cl_gui_text(g, s, x, y, argb, shadow) |
Text; returns the x where it ended. |
cl_gui_text_centered(g, s, cx, y, argb, shadow) |
Text centred on cx. |
cl_gui_text_component(g, text, x, y, argb, shadow) |
A cl_text with its styles and translation. |
cl_gui_fill(g, x0, y0, x1, y1, argb) |
A filled rectangle from corner to corner. |
cl_gui_fill_gradient(g, x0, y0, x1, y1, top, bottom) |
A rectangle fading from the top colour to the bottom one. |
cl_gui_outline(g, x, y, w, h, argb) |
A 1-pixel frame. |
cl_gui_item_by_name(g, id, x, y) |
An item icon, 16 x 16. |
cl_gui_item(g, stack, x, y, decorations) |
A stack’s icon, with count and durability bar if decorations is true. |
cl_gui_sprite(g, id, x, y, w, h) |
A GUI sprite, like "minecraft:hud/heart/full". |
cl_gui_blit(g, texture, x, y, w, h, u0, v0, u1, v1, argb) |
Part of any texture, tinted. |
cl_gui_tooltip(g, s, x, y) |
A vanilla tooltip box (\n for more lines). |
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:
static void label_render(cl_gui g, float partial_tick, void* user) {
(void)partial_tick;
(void)user;
cl_str label = CL_STR("Rubies: 12");
int32_t w = cl_gui_text_width(label);
int32_t x = cl_gui_width(g) - w - 6; // right edge, 6 pixels in
cl_gui_fill(g, x - 3, 3, x + w + 3, 15, 0x80000000);
cl_gui_text(g, label, x, 5, 0xFFE0115F, true);
}
Your own sprites go into assets/my_mod/textures/gui/sprites/. assets/my_mod/textures/gui/sprites/ruby_icon.png is drawn with cl_gui_sprite(g, CL_STR("my_mod:ruby_icon"), x, y, 16, 16).
Scaling and moving
cl_gui_push_pose saves the current position and size, cl_gui_translate and cl_gui_scale change them, and cl_gui_pop_pose goes back:
cl_gui_push_pose(g);
cl_gui_scale(g, 2.0f, 2.0f);
cl_gui_text(g, CL_STR("BIG"), 10, 10, 0xFFFFFF55, true); // drawn at 20, 20 and twice the size
cl_gui_pop_pose(g);
Every push needs a pop, or everything drawn after it is moved as well. cl_gui_push_scissor and cl_gui_pop_scissor work the same way and clip drawing to a rectangle.
Keep render cheap
render runs every frame: 60, 144 or more times a second. Don’t search the inventory or the world in it, and don’t format strings you could format once. Work out values in CL_EV_TICK_CLIENT_POST (20 times a second) and only draw them in render:
static int32_t g_rubies = 0;
static char g_label[32] = "";
static void count_tick(void* payload, void* user) {
(void)payload;
(void)user;
cl_entity me = cl_client_player();
int32_t n = CL_IS_NULL(me) ? 0 : cl_easy_count(me, "my_mod:ruby");
if (n != g_rubies) {
g_rubies = n;
snprintf(g_label, sizeof g_label, "x%d", (int)n);
}
}
static bool rubies_visible(void* user) {
(void)user;
return g_rubies > 0;
}
static void rubies_render(cl_gui g, float partial_tick, void* user) {
(void)partial_tick;
(void)user;
cl_gui_item_by_name(g, CL_STR("my_mod:ruby"), 4, 4);
cl_gui_text(g, CL_STR_C(g_label), 22, 8, 0xFFFFFFFF, true);
}
static void on_client_setup(void* payload, void* user) {
(void)payload;
(void)user;
CL_ON(CL_EV_TICK_CLIENT_POST, count_tick);
cl_hud_layer_desc d = CL_HUD_LAYER_DESC_INIT;
d.id = CL_STR("my_mod:rubies");
d.render = rubies_render;
d.visible = rubies_visible;
cl_hud_layer_register(&d, NULL);
}
Counting a whole inventory is cheap 20 times a second, but not 300 times. visible is asked before drawing. While it returns false, render isn’t called at all, which costs nothing. The label text is only rebuilt when the count changes.
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:
d.above = CL_STR("minecraft:hotbar");
Some vanilla layer ids: minecraft:hotbar, minecraft:food, minecraft:chat. 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.
cl_hud_layer_register(&d, &layer) gives you a cl_hud_layer handle; cl_hud_layer_remove(layer) removes 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
#include <clight_easy.h>
static char g_clock[8] = "";
static void clock_tick(void* payload, void* user) {
(void)payload;
(void)user;
cl_level world = cl_client_level();
if (CL_IS_NULL(world)) {
g_clock[0] = '\0';
return;
}
int64_t t = cl_level_day_time(world) % 24000;
int minutes = (int)((t * 60 / 1000 + 6 * 60) % (24 * 60));
snprintf(g_clock, sizeof g_clock, "%02d:%02d", minutes / 60, minutes % 60);
}
static bool clock_visible(void* user) {
(void)user;
return g_clock[0] != '\0';
}
static void clock_render(cl_gui g, float partial_tick, void* user) {
(void)partial_tick;
(void)user;
cl_str s = CL_STR_C(g_clock);
int32_t w = cl_gui_text_width(s);
int32_t x = cl_gui_width(g) - w - 6;
cl_gui_fill(g, x - 3, 3, x + w + 3, 15, 0x90000000);
cl_gui_text(g, s, x, 5, 0xFFFFFFFF, true);
}
static void on_client_setup(void* payload, void* user) {
(void)payload;
(void)user;
CL_ON(CL_EV_TICK_CLIENT_POST, clock_tick);
cl_hud_layer_desc d = CL_HUD_LAYER_DESC_INIT;
d.id = CL_STR("my_mod:clock");
d.render = clock_render;
d.visible = clock_visible;
cl_hud_layer_register(&d, NULL);
}
static cl_result mod_main(void) {
return CL_ON(CL_EV_LIFECYCLE_CLIENT_SETUP, on_client_setup);
}
CLIGHT_MOD_ENTRY(mod_main)
1000 ticks are one in-game hour, so t * 60 / 1000 is the minutes since 06:00. Multiplying before dividing keeps it in whole numbers without losing precision.
Recap: cl_hud_layer_register with a cl_hud_layer_desc (id, render, visible) draws every frame with cl_gui_text, cl_gui_fill and friends in 0xAARRGGBB colours; compute values in the client tick and keep render cheap.