Blog Tutorial

Part 10 — Build the HUD and menus

In this part, we will show the game state to the player.

The core loop already exists: the ball launches, bounces, falls out of bounds, and breaks bricks. Now we will display score and lives, then add menus for pause, game over, and win.

Build the HUD Canvas

Create a canvas named HUD.

Under it, create two text elements:

Score
Lives

Set their starting text values to:

  • Score: 000
  • Lives: x3

Assign the pixel font to both text elements.

Grow the Level Controller with HUD References

Open LevelController.php and add UI references:

use Lenga\Engine\UI\Canvas;
use Lenga\Engine\UI\Text;

#[Header("UI")]
public ?Canvas $hud = null;
public ?Text $scoreText = null;
public ?Text $livesText = null;

Initialize the HUD in start:

public function start(): void
{
    $this->initializeBricks();
    $this->initializeHud();
}

private function initializeHud(): void
{
    $this->setCanvasEnabled($this->hud, true);
    $this->updateScoreText();
    $this->updateLivesText();
}

private function setCanvasEnabled(?Canvas $canvas, bool $enabled): void
{
    if ($canvas instanceof Canvas) {
        $canvas->enabled = $enabled;
    }
}

Select Level Manager, then assign these fields in the Inspector:

  • Hud: HUD
  • Score Text: Score
  • Lives Text: Lives

Add the text helpers:

private function updateScoreText(): void
{
    if ($this->scoreText instanceof Text) {
        $this->scoreText->text = str_pad((string) $this->score, 3, "0", STR_PAD_LEFT);
    }
}

private function updateLivesText(): void
{
    if ($this->livesText instanceof Text) {
        $this->livesText->text = "x" . $this->lives;
    }
}

Update scorePoints and loseLife so the HUD refreshes when values change:

private function scorePoints(int $row): void
{
    $this->score += match ($row) {
        4 => 7,
        3 => 5,
        2 => 3,
        default => 1,
    };

    $this->updateScoreText();
}

private function loseLife(): bool
{
    $this->lives = max(0, $this->lives - 1);
    $this->updateLivesText();

    if ($this->lives === 0) {
        $this->showGameOver();
    }

    return true;
}

Add Game Over and Win Menus

Create two more canvases:

Game Over Menu
Win Menu

Each menu should have a title and buttons for restart and quit. Disable both canvases by default.

Add references to LevelController:

public ?Canvas $gameOverMenu = null;
public ?Canvas $winMenu = null;

Select Level Manager, then assign:

  • Game Over Menu: Game Over Menu
  • Win Menu: Win Menu

Then add menu helpers:

private function showGameOver(): void
{
    $this->emitEvent(EventType::GAME_OVER->value);
    $this->setCanvasEnabled($this->hud, false);
    $this->setCanvasEnabled($this->gameOverMenu, true);
}

private function showWinMenu(): void
{
    $this->emitEvent(EventType::GAME_WON->value);
    $this->setCanvasEnabled($this->hud, false);
    $this->setCanvasEnabled($this->winMenu, true);
}

In onBrickDestroyed, call showWinMenu() when the final brick is gone.

Add Pause

Create a Pause Menu canvas with a title and buttons for resume, restart, and quit. Disable it by default.

Add the reference:

public ?Canvas $pauseMenu = null;

Assign Pause Menu on the Level Manager in the Inspector.

Add imports for input, application control, and scene loading:

use Lenga\Engine\Core\Application;
use Lenga\Engine\Core\Input;
use Lenga\Engine\SceneManagement\SceneManager;

Add pause methods:

public function update(): void
{
    if (Input::getButtonDown("Pause")) {
        $this->togglePause();
    }
}

public function togglePause(): void
{
    if (Application::isPaused()) {
        $this->resumeGame();
        return;
    }

    $this->pauseGame();
}

public function pauseGame(): void
{
    Application::pause();
    $this->setCanvasEnabled($this->hud, false);
    $this->setCanvasEnabled($this->pauseMenu, true);
}

public function resumeGame(): void
{
    Application::resume();
    $this->setCanvasEnabled($this->pauseMenu, false);
    $this->setCanvasEnabled($this->hud, true);
}

public function restartGame(): void
{
    SceneManager::loadScene("Level1");
}

public function quitGame(): void
{
    Application::quit();
}

Wire the menu buttons to these public methods in the Inspector.

Tip: UI comes late in this series on purpose. It is easier to build useful UI after the game has real state to display.

Checkpoint

At the end of this part:

  • score and lives update in the HUD
  • game over appears at zero lives
  • win appears after the last brick is broken
  • pause can hide gameplay UI and show the pause menu
  • menu buttons call methods on LevelController

Next, we will add polish and export the game.