Lesson 20
Game UI: tell the player what matters without covering the game.
A good HUD answers the player's current questions: What is my health? What is the objective? Can I afford this upgrade? What happens if I press this button?
1. Keep HUD state separate from rendering
Canvas can draw UI, but HTML is often better for persistent HUD elements. Health bars, gold counters, buttons and panels are easier to style, easier to make accessible and easier to update with text content. The game state remains in JavaScript; the HUD mirrors it.
const hud = {
hpBar: document.getElementById("hpBar"),
hpVal: document.getElementById("hpVal"),
floor: document.getElementById("floor"),
kills: document.getElementById("kills"),
objective: document.getElementById("objective")
};
function updateHUD() {
hud.hpBar.style.width = (player.hp / player.maxHp * 100) + "%";
hud.hpVal.textContent = Math.floor(player.hp) + "/" + player.maxHp;
hud.floor.textContent = state.floor;
hud.kills.textContent = state.kills + "/" + state.totalKills;
hud.objective.textContent = state.objective;
}
Source pattern: Supagames dungeon and tower games update health, floor, kills, gold, lives and map text through explicit HUD update functions.
Call `updateHUD` when state changes, not from every random helper. A single update function prevents missing UI elements and makes null checks easier.
2. UI should explain decisions
Buttons need more than labels when the choice has cost. Upgrade panels should show price, current ownership and what the upgrade changes. If the player cannot afford something, disable it visually and explain why.
function renderUpgradeButton(upgrade) {
const owned = save.upgrades[upgrade.id];
const affordable = save.coins >= upgrade.cost;
return `
<button
class="${owned || !affordable ? "secondary disabled" : "primary"}"
data-upgrade="${upgrade.id}"
${owned || !affordable ? "disabled" : ""}>
${owned ? "Owned" : "Buy for " + upgrade.cost}
</button>
`;
}
Disabled buttons should not be mysterious. Add a short description near the button: "More fuel for long routes" or "Shield reduces hard landing damage." This turns the shop into strategy instead of a price list.
3. Pause menus must actually pause
A pause menu is not just a visible overlay. It should stop dangerous gameplay updates, keep UI responsive and avoid stacking multiple panels. Store one clear paused flag and let the update loop respect it.
function openPauseMenu() {
state.paused = true;
pausePanel.hidden = false;
}
function closePauseMenu() {
pausePanel.hidden = true;
state.paused = false;
}
function gameLoop(time) {
const dt = Math.min(0.033, (time - lastTime) / 1000);
lastTime = time;
if (!state.paused) updateGame(dt);
renderGame();
requestAnimationFrame(gameLoop);
}
Rendering while paused is useful because the player can see the scene behind the menu. Updating enemies while paused is usually not useful.
4. Modal controls should not fight game controls
If a modal is open, gameplay keys should not also trigger attacks, jumps or purchases. Route input through UI state. Escape can close a panel; Enter can confirm a focused button; Space should not scroll the page or advance gameplay while a form is focused.
function isUiOpen() {
return !pausePanel.hidden || !upgradePanel.hidden || !dialoguePanel.hidden;
}
document.addEventListener("keydown", function (event) {
if (event.target.matches("input, textarea, select")) return;
if (event.key === "Escape" && isUiOpen()) {
closeAllPanels();
event.preventDefault();
return;
}
if (!isUiOpen()) {
keys[event.key.toLowerCase()] = true;
}
});
This protects comment forms, search boxes and in-game text input. It also prevents a player from accidentally attacking while trying to close a menu.
5. Put urgent information near the action
The player should not need to scan the entire page during a difficult moment. Health, timer and immediate objective should be visible at a glance. Long descriptions belong in help panels or quest logs. For mobile, keep the HUD compact and avoid covering the bottom corners if touch buttons live there.
.game-hud {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 10px 14px;
background: rgba(5, 10, 24, 0.82);
border: 1px solid rgba(255, 217, 61, 0.2);
}
@media (max-width: 700px) {
.game-hud {
flex-wrap: wrap;
font-size: 0.85rem;
}
}
Readable UI is part of game difficulty. If the timer is hard to see, the game feels unfair even if the mechanics are balanced.
6. UI QA checklist
- Every button has a clear result and visible disabled state.
- The game pauses when upgrade, pause or dialogue panels are open.
- Keyboard shortcuts do not trigger while typing in forms.
- HUD values update after damage, reward, upgrade, level change and restart.
- Mobile controls and HUD do not cover critical gameplay.
- Menus work if ads are blocked or delayed.
Good UI lowers confusion. It does not make the game easier; it makes the challenge legible.
7. Separate permanent HUD from temporary prompts
Permanent HUD elements should stay stable: health, score, timer, objective and key resources. Temporary prompts should appear only when relevant, such as "Press E to dock" or "Enter the exit." Mixing them creates noise. If a prompt is always visible, players stop reading it; if it appears at the right moment, it becomes useful.
function updatePrompt() {
let prompt = "";
if (state.dialogue) prompt = "Press Enter to continue";
else if (player.nearExit && state.floorCleared) prompt = "Enter the exit";
else if (player.nearShop) prompt = "Press U for upgrades";
promptBox.textContent = prompt;
promptBox.hidden = !prompt;
}
This small pattern makes a game feel guided without turning it into a wall of instructions. It also helps mobile users because contextual prompts can match visible touch buttons.