Blog Tutorial

Part 9 — Break bricks and score points

In this part, we will turn brick collisions into gameplay.

A brick will report that it was hit, then deactivate itself. The level controller will listen for that event, add points, and check whether the player has cleared the level.

Add Row Data to Bricks

Create Assets/Scripts/Brick.php:

<?php

declare(strict_types=1);

namespace BreakOut\Game\Scripts;

use BreakOut\Game\Scripts\Enumeration\EventType;
use Lenga\Engine\Attributes\AddComponentMenu;
use Lenga\Engine\Attributes\Range;
use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\Collision2D;

#[AddComponentMenu('Gameplay/Brick')]
final class Brick extends Behaviour
{
    #[Range(1, 4)]
    public int $row = 1;

    public function onCollisionEnter2D(Collision2D $collision): void
    {
        $this->emitEvent(EventType::BRICK_DESTROYED->value, [
            'brick' => $this->gameObject,
            'row' => $this->row,
            'collision' => $collision,
        ]);

        $this->gameObject->setActive(false);
    }
}

Attach Brick to the brick prefab, then assign row values in the scene:

  • bottom row: 1
  • second row: 2
  • third row: 3
  • top row: 4

The row value is small, but it matters. It lets the level controller award different points for different rows.

Grow the Level Controller

Open LevelController.php and add these fields:

use Lenga\Engine\Core\GameObject;

private array $bricks = [];
private int $totalActiveBricks = 0;
private int $score = 0;

Add brick initialization:

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

private function initializeBricks(): void
{
    $this->bricks = GameObject::findGameObjectsWithTag('Brick');
    $this->totalActiveBricks = count($this->bricks);
}

This works because every brick was tagged Brick in the previous part.

Listen for Broken Bricks

Extend onEnable so the controller listens for brick events too:

public function onEnable(): void
{
    $this->onEvent(EventType::BALL_WENT_OUT_OF_BOUNDS->value, fn (): bool => $this->loseLife());
    $this->onEvent(EventType::BRICK_DESTROYED->value, fn (array $payload): bool => $this->onBrickDestroyed($payload));
}

Then add the handler:

private function onBrickDestroyed(array $payload): bool
{
    $this->scorePoints((int) ($payload['row'] ?? 1));
    $this->totalActiveBricks = max(0, $this->totalActiveBricks - 1);

    if ($this->totalActiveBricks === 0) {
        $this->emitEvent(EventType::GAME_WON->value);
    }

    return true;
}

Score by Row

Add row-based scoring:

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

Higher rows are worth more because they are harder to reach.

Reactivate Bricks on Restart

Because bricks are deactivated instead of destroyed, restarting the round can reuse the same scene objects.

Add this helper for later:

private function activateAllBricks(): void
{
    foreach ($this->bricks as $brick) {
        if ($brick instanceof GameObject) {
            $brick->setActive(true);
        }
    }

    $this->totalActiveBricks = count($this->bricks);
}

Checkpoint

At the end of this part:

  • bricks deactivate when hit
  • the level controller tracks the score
  • rows award different points
  • the level controller knows when every brick is gone

Next, we will build the HUD and menus.