Create a HUD with TGUI
Add a score label and health bar to the shared TGUI session established in Create a UIController. The game owns health/score values, the HUD manages their widgets and layout, and the UI controller owns session input, time, and scene publication. Menus and chat can share that same session.
Start with First Game or the Character example, then complete the UIController lesson’s module selection and game integration. This lesson replaces First Game’s direct score display. For RML document authoring, use Create an interactive GUI with RmlUi.
Select TGUI
Complete Select TGUI in the UIController lesson for the module, dependency, and link setup. The shared controller supplies the session and upload path used below.
Give the HUD its own class
Create hud.h:
#pragma once
#include <cheryl/ui/tgui/session.h>
#include <TGUI/Widgets/Label.hpp>
#include <TGUI/Widgets/ProgressBar.hpp>
#include <algorithm>
#include <format>
namespace Tutorial {
class Hud final {
CE::UI::TGUI::Session& session_;
tgui::Label::Ptr score_;
tgui::ProgressBar::Ptr health_;
public:
explicit Hud(CE::UI::TGUI::Session& session)
: session_(session) {
create_widgets();
}
void update(int score, unsigned int health) {
health = std::min(health, 100u);
score_->setText(std::format("Score: {}", score));
health_->setValue(health);
health_->setText(std::format("Health: {}/100", health));
}
// Remove this component during play, on the session's UI owner.
// Release the Hud afterwards; the shared session remains usable.
void remove() {
auto& gui = session_.gui();
static_cast<void>(gui.remove(score_));
static_cast<void>(gui.remove(health_));
}
private:
void create_widgets() {
score_ = tgui::Label::create("Score: 0");
score_->setTextSize(24);
score_->setSize({220, 32});
session_.gui().add(score_);
session_.set_layout(
score_, {
.anchor = {0, 0},
.offset = {.fixed = {24, 16}},
}
);
health_ = tgui::ProgressBar::create();
health_->setSize({220, 32});
health_->setMaximum(100);
session_.gui().add(health_);
session_.set_layout(
health_, {
.anchor = {1, 0},
.offset = {.fixed = {-24, 16}},
}
);
}
};
}
Hud borrows a Session&. Construct it on the simulation/UI owner after the
session’s initial view is set. Its native widget handles share toolkit ownership
with the GUI; the session must remain alive while they are used. The session’s
lifetime stays with the UI controller and requires no shared_ptr<Session>.
update(score, health) receives only the presentation values this component
needs. It formats the score and bounds the health display to 0..100; game rules
decide when those values change. Widgets inherit the session’s configured default
font. Configure Engine fonts
before creating the session when a different default is wanted.
Add widgets to the GUI before calling set_layout(). The label attaches its
top-left corner to the parent’s top-left; the bar attaches its top-right corner
to the parent’s top-right. Coordinates are logical UI units with downward-positive
Y. The bindings follow the shared view as it changes. See
typed layout for proportional sizing and offsets.
Add the HUD to the UI owner
Extend the initial ui-controller.h from the preceding lesson. Include hud.h,
retain std::optional<Hud> hud_ after the session member, and add score/health
values with setters. After creating the session with its initial view in
init_simulation(), construct the HUD with hud_.emplace(*session_). Update it before
recording the combined scene. During final cleanup, reset hud_ before closing
the session.
The complete controller is maintained with the tutorial game:
#pragma once
#include "hud.h"
#include <cheryl/core/engine/engine-context.h>
#include <cheryl/core/game-framework/tick-context.h>
#include <cheryl/ui/tgui/scene.h>
#include <cheryl/ui/tgui/session.h>
#include <future>
#include <memory>
#include <optional>
namespace Tutorial {
class UIController final {
static constexpr CE::Input::Routing::TargetId tgui_target_ = 2;
CE::Runtime::Context& engine_;
CE::Runtime::PlatformDispatcher::Submission platform_;
CE::UI::TGUI::SceneUploader uploader_;
CE::UI::TGUI::Scene scene_;
std::future<CE::UI::TGUI::Scene> pending_;
std::unique_ptr<CE::UI::TGUI::Session> session_;
std::optional<Hud> hud_;
int score_ = 0;
unsigned int health_ = 100;
public:
// Construct during Game::init_platform(); create toolkit objects on simulation.
explicit UIController(CE::Runtime::Context& engine)
: engine_(engine), platform_(engine.platform_dispatcher().submission()), uploader_(engine.resources()) {}
void set_score(int value) { score_ = value; }
void set_health(unsigned int value) { health_ = value; }
void init_simulation() {
session_ = std::make_unique<CE::UI::TGUI::Session>(engine_, tgui_target_, CE::UI::TGUI::SessionOptions{.automatic_view = true});
hud_.emplace(*session_);
}
void update(const CE::Game::TickContext& tick) {
// This example has a passive HUD. Interactive components choose focus/pointer policy here.
session_->handle_input(tick.input.records(), false);
session_->update_time(tick.delta_seconds);
hud_->update(score_, health_);
static_cast<void>(CE::UI::TGUI::adopt_scene(pending_, scene_));
if (!pending_.valid())
pending_ = uploader_.submit(platform_, session_->record());
}
void append(CE::Rendering::FrameWriter& frame) const { scene_.write(frame); }
void close_after_quiescence() {
// Drop component-held toolkit references before the GUI/session releases its own.
hud_.reset();
if (session_) {
session_->close_after_quiescence();
session_.reset();
}
scene_ = CE::UI::TGUI::Scene{};
}
};
}
This controller has one passive HUD. It feeds the complete input batch, selects
no pointer delivery, and requests no keyboard focus, so gameplay actions stay
available. Add a menu/chat component beside hud_, passing the same session.
Use the shared owner’s focus policy
when interactive components need keyboard or pointer input. TGUI permits one
session per process; component ownership fits within that single session.
Connect game state to the widgets
Game rules supply presentation values; the shared UI owner then records the updated widgets as part of the whole interface:
flowchart LR
accTitle: From game values to HUD presentation
accDescr: The game passes health and score to the UIController, which updates the HUD's widgets before recording and publishing the shared session's scene.
game["Health and score"] --> controller["UIController"]
controller -->|update values| hud["HUD widgets"]
hud -->|record| scene["Shared UI scene"]
Keep the game’s ui_ member and lifecycle calls from the UIController lesson.
Remove First Game’s raw score_ generation, font/layout/upload block, direct text
draw, and generation cleanup. Keep an application score and health value:
int points_ = 0;
unsigned int health_ = 100;
After quit handling and gameplay updates, set the presentation values before the
existing ui_->update(tick) call:
if (tick.input.button(next_cell_).pressed() && points_ < std::numeric_limits<int>::max())
++points_;
ui_->set_score(points_);
ui_->set_health(health_);
ui_->update(tick);
Include <limits> for the score bound. In First Game, Space is next_cell_;
the completed tutorial game calls its score action score_. The maintained
game implementation
shows the complete controller/Character/HUD composition.
Append the retained UI scene
The game still calls ui_->append(frame) after the world pass. One UI scene
contains all attached widgets and supplies its own screen projection, so camera
movement does not move the HUD. The first scene can be empty until upload finishes.
The controller retains the preceding complete scene while a replacement is pending.
Remove a component during play
When removing a HUD during play, call hud_->remove() on simulation, then reset
the component and stop its update branch in the enclosing owner. That
detaches only its widgets and keeps the session available to
menus, chat, and other components. Destroying a component’s handles alone does not
detach widgets still owned by the GUI.
Release and observe
Final teardown follows ui_->close_after_quiescence(): simulation has joined,
the controller drops component-held widget references, and session closure releases
the GUI’s remaining ownership. This final path does not call live session methods
from the platform thread. The input and graphics dependencies remain alive through
runtime cleanup, including partial initialization.
Expect Score: 0 at the top-left and a full health bar at the top-right. Space
advances the score; setting health_ to 25 shows a quarter-full bar. Resize the
window and move the world camera: the widgets should follow their screen corners,
while gameplay keys continue to work. Check both runtime modes for input and cleanup.
The maintained tutorial game implements this shared session and widget component. For interactive widgets, use the TGUI demo. Continue with abstract controller interfaces when the game needs a common contract for its concrete controller roles.