Skip to main content

Event Systems

Spinorium includes two built-in event systems: the classic event dispatcher and signals. These systems enable loose coupling between components, allowing them to communicate without direct dependencies.

The System object extends EventDispatcher and acts as a global event dispatcher, facilitating seamless communication between different parts of the game and the engine. This is the primary event system used in most cases. Events also play a crucial role in transferring execution control between game components.

Using the Event Dispatcher

The event dispatcher is implemented via the EventDispatcher and Event classes. Typically, you will use the System object as a global event dispatcher without needing to create a separate instance of EventDispatcher, unless you require isolated event handling. The EventDispatcher provides various methods to manage event listeners and dispatch events efficiently:

  • appendListener(type, listener, context?, priority?) – Attaches a listener for a specific event type, with optional context and priority.
  • removeAllListeners(type?) – Removes all listeners for a given event type (or all listeners if no type is provided).
  • removeListener(type, listener, context?) – Removes a specific listener from an event type.
  • dispatchByType(type, data?) – Creates and dispatches an Event instance with an optional data payload, notifying all listeners.
  • dispatch(event) – Dispatches a predefined Event instance, notifying all associated listeners.

Event Listeners Priorities

Event listeners can be assigned priorities to control their execution order. Listeners with higher priority values execute first.

// High priority listener executes first
System.appendListener(RoundEvent.STOP, this.criticalHandler, this, 100);

// Default priority listener executes after high priority ones
System.appendListener(RoundEvent.STOP, this.normalHandler, this);

// Low priority listener executes last
System.appendListener(RoundEvent.STOP, this.cleanupHandler, this, -50);

Defining Typed Events with SystemEvent

Spinorium allows defining event types with expected listener signatures using SystemEvent. This ensures type safety and consistency in event handling.

Definition Example

export const enum UpdateEvent {
UPDATE = 'system/update_frame'
}

declare global {
interface SystemEvent {
[UpdateEvent.UPDATE]:Event<number>; // Enforces type safety
}
}

This declaration ensures that when an UpdateEvent.UPDATE event is dispatched, it carries a number payload, enforcing strict type safety across the event system.

Usage Example

// Works fine
System.appendListener(UpdateEvent.UPDATE, (event:Event<number>) => console.log(event.data));
System.dispatchByType(UpdateEvent.UPDATE, 10);

// Type error: expected number, got string
System.appendListener(UpdateEvent.UPDATE, (event:Event<string>) => console.log(event.data));
System.dispatchByType(UpdateEvent.UPDATE, 'test');

Custom Event Classes and Type Safety

While it is highly recommended to specify listener signatures for each event, it is not strictly required. However, omitting type definitions can result in missing type safety checks. To ensure type safety when using custom event classes, define the event type as a constant and register it in SystemEvent.

Missing Type Safety Example

export class CustomEvent extends Event<string> {
public static readonly CUSTOM:string = 'custom';
}

// No error, but incorrect usage
System.dispatchByType(CustomEvent.CUSTOM, 10);

Correct Type Safety Example

export class CustomEvent extends Event<string> {
// Defining event type as a constant
public static readonly CUSTOM = 'custom' as const;
}

declare global {
interface SystemEvent {
[CustomEvent.CUSTOM]:CustomEvent;
}
}

// Type error: expected string, got number
System.dispatchByType(CustomEvent.CUSTOM, 10);

Events in Timelines

Spinorium actively uses events within timelines. For example, AutoplayManager listens for the RoundEvent.BEFORE_STOP event and, if autoplay is still available, enhances the timeline with actions to start the next round. TimelineEvent is an effective way to customize game flow. To attach one or more timelines to an event, use event.timeline property.

There are several predefined actions in Actions factory to integrate the event dispatcher into a timeline:

  • dispatchEventByType(type, data?) – Creates and dispatches an Event by type with the provided data payload.
  • dispatchEvent(event) – Dispatches a provided Event instance.
  • waitEventByType(type, data?) – Creates and dispatches a TimelineEvent by type with an optional data payload. If any listener attaches a timeline to this event, it will also be executed.
  • waitEvent(event) – Dispatches a provided TimelineEvent instance.

Timeline Events Usage Example

export const enum CustomEvent {
CUSTOM = 'custom'
}

declare global {
interface SystemEvent {
[CustomEvent.CUSTOM]:TimelineEvent;
}
}

System.appendListener(CustomEvent.CUSTOM, (event:TimelineEvent) => {
event.timeline = Actions.utils.consoleLog('listener_1');
});

System.appendListener(CustomEvent.CUSTOM, (event:TimelineEvent) => {
event.timeline = Actions.utils.consoleLog('listener_2');
});

// You'll see in the console: start, listener_1, listener_2, and stop
Actions.utils.consoleLog('start')
.waitEventByType(CustomEvent.CUSTOM)
.utils.consoleLog('stop')
.start();

Using Signals

Signals are implemented via the Signal<Type> class. They are mainly designed for internal usage, where direct object access is available. For instance, signals are utilized in ResourceFetcher/ResourceLoader classes and UI components such as Button and CheckBox.

The Signal class provides the following methods:

  • appendListener(listener, context?, priority) – Attaches a listener to a signal with optional context and priority.
  • removeAllListeners() – Clears all listeners from the signal.
  • removeListener(listener, context?) – Removes a specific listener.
  • appendSignal(signal) – Links another signal of the same type, enabling chained signal dispatching.
  • removeAllSignals() – Detaches all linked signals.
  • removeSignal(signal) – Removes a specific linked signal.
  • removeAllLinks() – Clears all attached listeners and linked signals.
  • dispatch(value) – Notifies all listeners with the given value.

Usage Example

class Button {
public readonly onClick:Signal = new Signal();
}

const button = new Button();

button.onClick.appendListener(() => {
console.log('Button clicked');
});

// Simulating a button click
button.onClick.dispatch();