- C# 100%
- package.json: at.kairoscope.kairoengine.events v1.0.0 - Zero dependencies - Git URL: https://forgejo.jamesperet.com/kairoengine/events.git - Ready for UPM git URL installation |
||
|---|---|---|
| Runtime | ||
| ChangeLog.md | ||
| package.json | ||
| Readme.md | ||
| Readme.md.meta | ||
| Runtime.meta | ||
KairoEngine Events v 1.0.0
Overview
The Events module provides a lightweight, string-based event system for decoupled communication between game systems. It implements both broadcast (fire-and-forget) and request/response patterns, enabling components to communicate without direct references.
Installation
Add to your Unity project's Packages/manifest.json:
"at.kairoscope.kairoengine.events": "https://forgejo.jamesperet.com/kairoengine/events.git"
Or use the Unity Package Manager: Add package from git URL → https://forgejo.jamesperet.com/kairoengine/events.git
Purpose
The Events module serves as the foundation for:
- Decoupled Communication: Allow systems to communicate without tight coupling
- Broadcast Events: Fire-and-forget event notifications with typed payloads
- Request/Response Pattern: Query data from listeners and receive typed responses
- Type-Safe Messaging: Support for various payload types while maintaining type safety
Dependencies
- UnityEngine - Core Unity framework
Architecture
The Events module is accessed through the static EventManager class, which provides two communication subsystems:
public static class EventManager
{
public static RequestEvents request; // Request/response pattern
public static GenericEvents broadcast; // Fire-and-forget pattern
}
Classes
EventManager
Purpose: Static entry point for the event system.
Usage:
// Broadcast an event
EventManager.broadcast.Trigger("PlayerDied");
// Request data
var response = EventManager.request.GetBool("IsGamePaused");
if (response.status == EventResponseStatus.OK)
{
bool isPaused = response.value;
}
EventResponse
Purpose: Wrapper for request responses that includes status information.
Structure:
public class EventResponse<T>
{
public EventResponseStatus status;
public T value;
}
public enum EventResponseStatus
{
OK, // Request successful, value is valid
NotFound, // No listener registered for this event
EmptyResponse // Listener exists but returned null/empty
}
Usage Pattern:
var response = EventManager.request.GetGameObject("GetPlayerObject");
switch (response.status)
{
case EventResponseStatus.OK:
// Use response.value safely
GameObject player = response.value;
break;
case EventResponseStatus.NotFound:
Debug.LogWarning("No listener for GetPlayerObject");
break;
case EventResponseStatus.EmptyResponse:
Debug.LogWarning("Listener returned null");
break;
}
GenericEvents
Purpose: Implements broadcast/observer pattern for fire-and-forget event notifications.
Key Features:
- Multiple Payload Types: Supports various parameter combinations
- Method Overloading: Automatic type resolution based on listener signature
- Multi-Cast Delegates: Multiple listeners can subscribe to the same event
Supported Event Types:
Empty Events (No Parameters)
// Listen
EventManager.broadcast.StartListening("GameStarted", OnGameStarted);
void OnGameStarted() { }
// Trigger
EventManager.broadcast.Trigger("GameStarted");
// Stop listening
EventManager.broadcast.StopListening("GameStarted", OnGameStarted);
Single Parameter Events
Supported types: string, float, int, Vector3, Vector3Int, GameObject, MonoBehaviour, ScriptableObject, object
// String event
EventManager.broadcast.StartListening("ShowMessage", OnShowMessage);
void OnShowMessage(string message) { }
EventManager.broadcast.Trigger("ShowMessage", "Hello World");
// Float event
EventManager.broadcast.StartListening("HealthChanged", OnHealthChanged);
void OnHealthChanged(float newHealth) { }
EventManager.broadcast.Trigger("HealthChanged", 75.5f);
// Vector3 event
EventManager.broadcast.StartListening("PlayerMoved", OnPlayerMoved);
void OnPlayerMoved(Vector3 position) { }
EventManager.broadcast.Trigger("PlayerMoved", new Vector3(1, 2, 3));
Multi-Parameter Events
Supported combinations:
(float, float)- Two floats(string, int)- String and int(string, float)- String and float(string, string)- Two strings(string, Vector3)- String and Vector3(string, bool)- String and bool
// String + Int event
EventManager.broadcast.StartListening("ItemCollected", OnItemCollected);
void OnItemCollected(string itemName, int quantity) { }
EventManager.broadcast.Trigger("ItemCollected", "Gold", 50);
// String + Vector3 event
EventManager.broadcast.StartListening("SpawnEffect", OnSpawnEffect);
void OnSpawnEffect(string effectName, Vector3 position) { }
EventManager.broadcast.Trigger("SpawnEffect", "Explosion", playerPos);
// Float + Float event
EventManager.broadcast.StartListening("RangeUpdated", OnRangeUpdated);
void OnRangeUpdated(float min, float max) { }
EventManager.broadcast.Trigger("RangeUpdated", 0f, 100f);
Special Object Event
For generic object payloads, use the explicit methods:
EventManager.broadcast.StartListeningForObject("DataReceived", OnDataReceived);
void OnDataReceived(object data) { }
EventManager.broadcast.TriggerForObject("DataReceived", customData);
EventManager.broadcast.StopListeningForObject("DataReceived", OnDataReceived);
RequestEvents
Purpose: Implements request/response pattern for querying data from registered listeners.
Key Features:
- Type-Safe Responses: Returns
EventResponse<T>with status codes - Single Listener: Only one listener can be bound per event title
- Error Handling: Status codes indicate success or failure reasons
Supported Request Types:
Boolean Requests
// Bind a listener
EventManager.request.Bind("IsPlayerAlive", () => playerHealth > 0);
// Request the value
EventResponse<bool> response = EventManager.request.GetBool("IsPlayerAlive");
if (response.status == EventResponseStatus.OK)
{
if (response.value)
{
// Player is alive
}
}
// Unbind
EventManager.request.Unbind("IsPlayerAlive", listenerMethod);
GameObject Requests
// Bind a listener
EventManager.request.Bind("GetMainCamera", () => Camera.main.gameObject);
// Request the value
EventResponse<GameObject> response = EventManager.request.GetGameObject("GetMainCamera");
if (response.status == EventResponseStatus.OK)
{
GameObject camera = response.value;
}
else if (response.status == EventResponseStatus.EmptyResponse)
{
Debug.LogWarning("Camera returned null");
}
Common Patterns
Event-Driven UI Updates
public class HealthUI : MonoBehaviour
{
void OnEnable()
{
EventManager.broadcast.StartListening("PlayerHealthChanged", UpdateHealthBar);
}
void OnDisable()
{
EventManager.broadcast.StopListening("PlayerHealthChanged", UpdateHealthBar);
}
void UpdateHealthBar(float health)
{
// Update UI
}
}
// Trigger from player
public class Player : MonoBehaviour
{
void TakeDamage(float damage)
{
currentHealth -= damage;
EventManager.broadcast.Trigger("PlayerHealthChanged", currentHealth);
}
}
Cross-System Queries
// GameManager provides data
public class GameManager : MonoBehaviour
{
void Start()
{
EventManager.request.Bind("IsGamePaused", () => isPaused);
EventManager.request.Bind("GetPlayer", () => playerObject);
}
}
// Other systems query data
public class EnemyAI : MonoBehaviour
{
void Update()
{
var pauseResponse = EventManager.request.GetBool("IsGamePaused");
if (pauseResponse.status == EventResponseStatus.OK && pauseResponse.value)
{
return; // Game is paused, skip AI update
}
// AI logic...
}
}
Level Loading Notifications
public class LevelManager : MonoBehaviour
{
async UniTask LoadLevel(string levelName)
{
EventManager.broadcast.Trigger("LevelLoadStarted", levelName);
// Load level...
await SceneManager.LoadSceneAsync(levelName);
EventManager.broadcast.Trigger("LevelLoadComplete", levelName);
}
}
public class LoadingScreen : MonoBehaviour
{
void OnEnable()
{
EventManager.broadcast.StartListening("LevelLoadStarted", OnLoadStarted);
EventManager.broadcast.StartListening("LevelLoadComplete", OnLoadComplete);
}
void OnLoadStarted(string levelName)
{
// Show loading screen
}
void OnLoadComplete(string levelName)
{
// Hide loading screen
}
}
Best Practices
-
Use Consistent Event Names: Define event names as constants to avoid typos
public static class GameEvents { public const string PLAYER_DIED = "PlayerDied"; public const string GAME_PAUSED = "GamePaused"; } -
Clean Up Listeners: Always unsubscribe in
OnDisable()orOnDestroy()to prevent memory leaksvoid OnDestroy() { EventManager.broadcast.StopListening("MyEvent", OnMyEvent); } -
Check Response Status: Always check
EventResponse.statusbefore using valuesvar response = EventManager.request.GetGameObject("GetPlayer"); if (response.status == EventResponseStatus.OK && response.value != null) { // Safe to use response.value } -
Prefer Specific Types: Use specific event types over generic
objectwhen possible for type safety -
Document Event Contracts: Document what parameters events expect and when they're triggered
-
Consider Performance: String-based lookups have overhead; avoid triggering events in tight loops
-
Use Broadcasts for Notifications: Use
GenericEventswhen you don't need a return value -
Use Requests for Queries: Use
RequestEventswhen you need to get data from other systems
Integration with KairoEngine Modules
The Events module is used throughout KairoEngine and World Dweller:
- InitializationManager: Broadcasts initialization lifecycle events
- UI Module: Triggers view change events
- Game Systems: Cross-system communication without dependencies
- Save/Load Systems: Notifies listeners of save/load operations
Limitations
- String-Based Lookups: Performance overhead compared to direct references
- No Type Safety at Compile Time: Event names and parameter types verified at runtime
- Single Listener for Requests:
RequestEventsonly supports one listener per event name - No Event History: Events are not queued or recorded
- No Priority System: Listeners are called in registration order
Version History
- v1.0.0: Initial version
- Generic broadcast events with multiple payload types
- Request/response pattern with status codes
- Support for Unity types (Vector3, GameObject, etc.)