Guides Behaviours Use EventBus for Gameplay Events

Behaviours 2 min read Updated Apr 2026

Use EventBus for Gameplay Events

Use `EventBus` when you want systems to react to gameplay without holding references to the object that dispatched the event.

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.died
  • game.score.changed
  • game.round.started
  • game.ball.launched
  • game.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:

  • source
  • kind
  • score
  • position
  • round
  • checkpointName

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?"

What Comes Next