Part 7 — Handle out-of-bounds and lives
In this part, we will add the floor trigger and the first version of the level controller.
The floor does not bounce the ball. It detects when the ball falls below the paddle and emits an event. The level controller listens for that event and subtracts a life.
Add Tags and Events
Create two small enums under Assets/Scripts/Enumeration.
<?php
declare(strict_types=1);
namespace BreakOut\Game\Scripts\Enumeration;
enum Tag: string
{
case Ball = 'Ball';
case Floor = 'Floor';
case Brick = 'Brick';
}
<?php
declare(strict_types=1);
namespace BreakOut\Game\Scripts\Enumeration;
enum EventType: string
{
case BALL_WENT_OUT_OF_BOUNDS = 'game.ballWentOutOfBounds';
case GAME_OVER = 'game.over';
case BRICK_DESTROYED = 'game.brickDestroyed';
case GAME_WON = 'game.won';
}
We are adding a couple of events early because the level controller will grow into them over the next few parts.
Create the Floor Trigger
Under Boundaries, create a wide rectangle below the paddle and rename it Floor.
Add or confirm these components:
Rectangle Renderer, so you can see it while positioningBoxCollider2D
Set the BoxCollider2D as a trigger and set the GameObject tag to Floor.
Also set the ball's tag to Ball.
Add the Floor Script
Create Assets/Scripts/Floor.php:
<?php
declare(strict_types=1);
namespace BreakOut\Game\Scripts;
use BreakOut\Game\Scripts\Enumeration\EventType;
use BreakOut\Game\Scripts\Enumeration\Tag;
use Lenga\Engine\Attributes\AddComponentMenu;
use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\Collision2D;
#[AddComponentMenu('Gameplay/Floor')]
final class Floor extends Behaviour
{
public function onTriggerEnter2D(Collision2D $collision): void
{
if ($collision->gameObject?->tag !== Tag::Ball->value) {
return;
}
$this->emitEvent(EventType::BALL_WENT_OUT_OF_BOUNDS->value, [
'ball' => $collision->gameObject,
'collision' => $collision,
]);
}
}
Attach it to Floor.
The floor only reports the event. It does not reset the ball or change lives directly.
Grow the Ball Script
In Ball.php, add this method:
public function onEnable(): void
{
$this->onEvent(EventType::BALL_WENT_OUT_OF_BOUNDS->value, fn (): bool => $this->resetBall());
}
Then import the event enum:
use BreakOut\Game\Scripts\Enumeration\EventType;
Now the ball resets itself when the floor reports it went out of bounds.
Grow the Paddle Script
The paddle also needs to become launch-ready again.
Add this to Paddle.php:
public function onEnable(): void
{
$this->onEvent(EventType::BALL_WENT_OUT_OF_BOUNDS->value, fn (): bool => $this->resetPaddle());
}
private function resetPaddle(): bool
{
$this->canLaunch = true;
$this->transform->position = clone $this->startPosition;
if ($this->body instanceof Rigidbody2D) {
$this->body->velocity = Vector3::zero();
}
return true;
}
Then import EventType:
use BreakOut\Game\Scripts\Enumeration\EventType;
onEvent tracks the subscription for the Behaviour, so it is cleaned up when the Behaviour is disabled.
Add the Level Controller
Create an empty GameObject called Level Manager, then create Assets/Scripts/LevelController.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\Header;
use Lenga\Engine\Core\Behaviour;
#[AddComponentMenu('Level/Level Controller')]
final class LevelController extends Behaviour
{
#[Header('Gameplay')]
public int $lives = 3;
public function onEnable(): void
{
$this->onEvent(EventType::BALL_WENT_OUT_OF_BOUNDS->value, fn (): bool => $this->loseLife());
}
private function loseLife(): bool
{
$this->lives = max(0, $this->lives - 1);
if ($this->lives === 0) {
$this->emitEvent(EventType::GAME_OVER->value);
}
return true;
}
}
Attach LevelController to Level Manager.
For now, lives are just data on the controller. We will display them in the HUD later.
Checkpoint
At the end of this part:
- the floor is a trigger below the paddle
- the floor emits an out-of-bounds event for the ball
- the ball resets after crossing the floor
- the paddle becomes launch-ready again
LevelControllertracks lives
Next, we will build the brick field.