How Unciv's EventBus Works for Game Notifications: A Technical Deep Dive
Unciv's EventBus is a lightweight publish-subscribe singleton that decouples game event generation from consumption using a type-safe listener registry in core/src/com/unciv/logic/event/EventBus.kt.
The yairm210/Unciv codebase implements a custom event bus to handle game notifications without tight coupling between systems. This Kotlin-based implementation allows UI components, game logic, and multiplayer handlers to communicate through a centralized dispatcher. Understanding this mechanism reveals how the 4X strategy game manages state changes across its architecture.
Core Architecture of the EventBus
The Singleton Pattern Implementation
The EventBus operates as a Kotlin object singleton, ensuring exactly one global dispatcher exists per game session. This design eliminates synchronization concerns while providing universal access to event routing.
In core/src/com/unciv/logic/event/EventBus.kt, the singleton maintains internal state through a mutable map that associates event types with their listeners:
object EventBus {
private val listeners = mutableMapOf<KClass<out Event>, MutableList<(Event) -> Unit>>()
// ...
}
Type-Safe Listener Registry
The registry uses Kotlin's reified generics to maintain type safety across event subscriptions. The addListener method accepts a KClass reference and a lambda callback, storing them in the internal map:
fun <T : Event> addListener(eventClass: KClass<T>, listener: (T) -> Unit)
This implementation stores listeners in a MutableMap<KClass<out Event>, MutableList<(Event) -> Unit>>, allowing multiple handlers for a single event type.
How Events Flow Through the System
Registering Listeners with addListener
Components subscribe to notifications by calling addListener with the specific event class and a handler lambda. The bus supports polymorphic dispatch, meaning listeners registered for parent event classes receive child event notifications.
// In a UI screen or game logic component
EventBus.addListener(CityCapturedEvent::class) { event ->
toast("City captured: ${event.cityName}")
}
Posting Events via post()
When game systems generate notifications, they instantiate concrete Event subclasses and dispatch them through the post method. The bus looks up registered listeners by the event's runtime class and invokes each callback synchronously:
fun post(event: Event) {
listeners[event::class]?.forEach { it(event) }
}
This synchronous execution ensures immediate state propagation, critical for turn-based game logic where sequence matters.
Cleanup with removeAllListeners
To prevent memory leaks when UI screens dispose, the bus provides removeAllListeners(owner), which removes all callbacks associated with a specific owner object:
override fun dispose() {
EventBus.removeAllListeners(this)
}
Event Types and Multiplayer Integration
Base Event Class and Concrete Implementations
All game notifications extend the base Event class defined in core/src/com/unciv/models/ruleset/Event.kt. Concrete implementations carry specific payload data relevant to the notification type.
The JSON schema in docs/Modders/schemas/Events.schema.json defines the structure for moddable events, allowing community extensions without core code modification.
Multiplayer-Specific Events
Multiplayer functionality leverages the same bus architecture through core/src/com/unciv/logic/multiplayer/OnlineMultiplayerEvents.kt. Events like ReceiveTurn travel through the standard bus, enabling uniform handling of network messages:
// OnlineMultiplayerEvents.kt defines events such as:
class ReceiveTurn(val gameId: String) : Event()
This consolidation allows both local and networked games to use identical notification patterns.
Testing and Validation
The implementation includes comprehensive unit tests in tests/src/com/unciv/logic/event/EventBusTest.kt. These tests verify listener registration, event dispatch ordering, and proper cleanup:
@Test
fun testListenerReceivesEvent() {
var received = false
EventBus.addListener(TestEvent::class) { received = true }
EventBus.post(TestEvent())
assertTrue(received)
}
Summary
- Singleton Architecture: The EventBus uses a Kotlin
objectsingleton to provide global access while maintaining single-instance guarantees. - Type-Safety: Generic
addListenermethods withKClassparameters ensure compile-time safety for event handlers. - Synchronous Dispatch: Events process immediately via the
postmethod, maintaining deterministic ordering for turn-based gameplay. - Memory Management: The
removeAllListenersmethod prevents leaks by allowing components to deregister during disposal. - Multiplayer Unification: Network events use the same infrastructure as local events, simplifying cross-platform logic.
Frequently Asked Questions
How does Unciv's EventBus prevent memory leaks in UI components?
The removeAllListeners(owner) method in core/src/com/unciv/logic/event/EventBus.kt accepts an owner object reference and removes all listeners associated with that instance. UI screens call this in their dispose() lifecycle method, ensuring callbacks don't retain references to destroyed views.
Can mods create custom events using the EventBus?
Yes, mods can define custom events through the JSON schema specified in docs/Modders/schemas/Events.schema.json. These events extend the base Event class from core/src/com/unciv/models/ruleset/Event.kt and integrate with the bus using the same addListener and post methods as core events.
Are EventBus listeners executed on a background thread?
No, listeners execute synchronously on the same thread that calls post(event). This design ensures immediate consistency for game state changes, which is essential for turn-based mechanics where event ordering affects gameplay logic.
What is the performance overhead of the EventBus implementation?
The implementation uses a simple MutableMap lookup by KClass key, providing O(1) complexity for listener retrieval. Since the bus avoids reflection during dispatch and uses direct lambda invocation, overhead remains minimal even with frequent game notifications.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →