That is the normal shape of gameplay code.
Your HUD does not need a direct reference to every object that can change score. Your audio system does not need to know which exact object caused a hit. Your game flow controller does not need to subscribe directly to every Health instance in the scene.
When EventBus Is the Right Tool
Reach for EventBus when:
- several systems may react to the same event
- listeners should not know the dispatcher ahead of time
- the event belongs to a gameplay moment, not to one owner instance
- you want to pass context through event data instead of through object references
Examples:
game.health.diedgame.score.changedgame.round.startedgame.ball.launchedgame.checkpoint.reached
A Simple Example
Here a Health behaviour emits a gameplay event when it reaches zero:
use Lenga\Engine\Core\Behaviour;
final class Health extends Behaviour
{
public int $current = 100;
public string $kind = 'enemy';
public function applyDamage(int $amount): void
{
$this->current -= $amount;
if ($this->current <= 0) {
$this->emitEvent('game.health.died', [
'source' => $this,
'kind' => $this->kind,
]);
}
}
}
Now a game manager can react without knowing which Health instance dispatched the event:
use Lenga\Engine\Core\Behaviour;
final class GameManager extends Behaviour
{
public function onEnable(): void
{
$this->onEvent('game.health.died', function (?array $payload): void {
if (($payload['kind'] ?? null) === 'player') {
$this->beginGameOverFlow();
}
});
}
private function beginGameOverFlow(): void
{
}
}
An audio controller can listen to the same event independently:
use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\Debug;
final class AudioDirector extends Behaviour
{
public function onEnable(): void
{
$this->onEvent('game.health.died', function (?array $payload): void {
Debug::log('Play death stinger for ' . (string) ($payload['kind'] ?? 'unknown'));
});
}
}
What Goes in the Payload
Put the information listeners need into the event payload.
Common payload fields include:
sourcekindscorepositionroundcheckpointName
Keep the payload practical. Include enough context for listeners to react correctly, but do not treat the payload as a dump of unrelated state.
Listen Through Behaviour
Inside Behaviours, prefer the Behaviour helpers:
emitEvent(...)dispatchEvent(...)onEvent(...)onceEvent(...)
These helpers fit naturally into behaviour lifecycles and make it easier to keep subscriptions tied to the object that owns them.
Built-In Global Events
Some engine events already use EventBus.
For example, UI navigation emits EventBus::UI_SELECTION_CHANGED.
use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\Debug;
use Lenga\Engine\Core\EventBus;
use Lenga\Engine\UI\UIElement;
final class MenuSelectionLogger extends Behaviour
{
public function onEnable(): void
{
$this->onEvent(EventBus::UI_SELECTION_CHANGED, function (?array $payload): void {
$currentLookup = $payload['current'] ?? null;
if (!\is_array($currentLookup)) {
Debug::log('No UI element is selected.');
return;
}
$current = UIElement::fromNativeLookupData($currentLookup);
Debug::log('Selected UI element: ' . $current->name);
});
}
}
Remember
Use EventBus when the important question is:
"Did this happen?"
not:
"Which exact object instance do I already have a reference to?"