Database Management & Runtime Query
The static workflow ties events to scripts via [SerializeField] GameEvent references. That works perfectly for scene-bound logic, but breaks down when events live behind dynamic content boundaries β Addressables, DLC, mods, save/load systems, or pure C# domain code that can't carry serialized fields.
The GameEventManager.Instance exposes two complementary API families to bridge that gap:
- Database Lifecycle β
Register/Unregister/SetActivedatabases at runtime - Runtime Query β locate events by GUID, Name, Category, or Type without direct asset references
Together they let any code (even non-MonoBehaviour) discover and use events as data, not as wired references.
π― When to Use Thisβ
| Scenario | Recommended Approach |
|---|---|
| Logic on a scene-bound MonoBehaviour | Static workflow β [SerializeField] GameEvent |
| Pure C# class / service / domain layer | Runtime Query by GUID or Name |
| Save/load β re-raise an event from persisted data | Runtime Query by GUID (stable identity) |
| Scene/DLC/mod adds new events at runtime | Database Lifecycle β RegisterDatabase |
| Pause/cutscene wants to mute a whole event group | Database Lifecycle β SetDatabaseActive(false) |
| Plugin needs every event matching a name pattern | Runtime Query β GetGameEvents(name) |
If you find yourself writing a static Dictionary<string, GameEvent> cache populated by hand-rolled scene-load code, you're rebuilding the Query API. Use it directly instead.
1. Database Lifecycleβ
A GameEventDatabase is a ScriptableObject that holds a list of GameEvent sub-assets. The GameEventManager keeps an ordered list of registered databases β only events from active registered databases participate in raising and binding.
Register & Unregisterβ
using TinyGiants.GES.Runtime;
using UnityEngine;
public class CombatModuleLoader : MonoBehaviour
{
[SerializeField] private GameEventDatabase combatDB;
private void OnEnable()
{
GameEventManager.Instance.RegisterDatabase(combatDB);
}
private void OnDisable()
{
GameEventManager.Instance.UnregisterDatabase(combatDB);
}
}
RegisterDatabase automatically creates EventBinding entries for events that don't already have one. Re-registering an already-registered database is a safe no-op. UnregisterDatabase cleans up bindings and runtime callbacks; calling it on a null or unregistered database is also safe.
Active vs Inactiveβ
SetDatabaseActive(db, false) keeps the database registered, but its events silently no-op when raised. Calling SetDatabaseActive(db, true) resumes invocation.
// Mute all combat events during a cutscene
GameEventManager.Instance.SetDatabaseActive(combatDB, false);
PlayCutscene();
// Resume normal invocation when cutscene ends
GameEventManager.Instance.SetDatabaseActive(combatDB, true);
| Operation | List Membership | Event Invocation | Use For |
|---|---|---|---|
RegisterDatabase(db) | β Added | β Active | Loading new content |
UnregisterDatabase(db) | β Removed | β Inactive | Unloading content (full cleanup) |
SetDatabaseActive(db, false) | β Stays | β Muted | Temporary gating without losing state |
SetDatabaseActive(db, true) | β Stays | β Active | Resume after temporary gate |
Pattern: Loading via Resourcesβ
var db = Resources.Load<GameEventDatabase>("Databases/CombatEvents");
GameEventManager.Instance.RegisterDatabase(db);
Pattern: Loading via Addressablesβ
var handle = Addressables.LoadAssetAsync<GameEventDatabase>("CombatEvents");
var db = await handle.Task;
GameEventManager.Instance.RegisterDatabase(db);
Pattern: Mod Systemβ
// Mods ship as standalone GameEventDatabase assets
foreach (var modDB in installedMods)
GameEventManager.Instance.RegisterDatabase(modDB);
// User toggles a mod off without unloading
GameEventManager.Instance.SetDatabaseActive(disabledMod, false);
2. Runtime Queryβ
The query API has three lookup styles, each with three type arities. The lookup style answers how to find the event; the arity answers what type to enforce.
Lookup Style 1: By GUID (Unique)β
A GameEvent asset's Guid is set on creation and never changes β it's the only identity that's safe to persist (saves, network, mod metadata).
var mgr = GameEventManager.Instance;
// Existence check
if (mgr.HasGameEvent(savedGuid))
{
// Get
var evt = mgr.GetGameEvent(savedGuid);
evt?.Raise();
// Or with try-pattern
if (mgr.TryGetGameEvent(savedGuid, out var found))
found.Raise();
}
Lookup Style 2: By Name (Multi-Match)β
Event names are not unique β the same name can exist under different categories or in different databases. The plural API returns every match:
List<GameEventBase> jumps = mgr.GetGameEvents("OnJump");
foreach (var e in jumps) e.Raise();
// With optional category filter
List<GameEventBase> movementJumps = mgr.GetGameEvents("OnJump", "Movement");
GetGameEvents always returns a List<T> (empty if none match) β never null, so you can foreach directly without a null check.
Lookup Style 3: By Name, First Match (Convenience)β
When you know there's only one match (or you accept any), use the convenience method:
mgr.GetFirstGameEventByName("OnPlayerSpawn")?.Raise();
// With category filter
mgr.GetFirstGameEventByName("OnJump", "Movement")?.Raise();
// Try-pattern
if (mgr.TryGetFirstGameEventByName("OnPlayerSpawn", out var evt))
evt.Raise();
"First" is whichever event the iterator encounters first β arbitrary if duplicates exist. Don't rely on a specific order; if order matters, use GetGameEvents and pick deliberately.
Type-Strict Filtering: 3 Aritiesβ
Every Get/Has/Try method has three arities:
| Arity | Matches | Returns |
|---|---|---|
| Non-generic | Any event type | GameEventBase |
<T> | GameEvent<T> only | GameEvent<T> |
<TSender, TArgs> | GameEvent<TSender, TArgs> | GameEvent<TSender, TArgs> |
Filtering is strict β is GameEvent<T> pattern matching, not assignability. Three event classes (void, single-arg, sender) are siblings (all directly inherit GameEventBase), so there are no accidental cross-type matches.
// Database contents:
// - "OnJump" (void, Movement)
// - "OnJump" (void, UI)
// - "OnScoreChanged" (int, Gameplay)
mgr.GetGameEvents("OnJump").Count; // β 2 (both void)
mgr.GetGameEvents<int>("OnJump").Count; // β 0 (OnJump is void, not int)
mgr.GetGameEvents("OnJump", "Movement").Count; // β 1 (Movement only)
mgr.GetFirstGameEventByName<int>("OnScoreChanged")?.Raise(100);
Active Database Scopeβ
All query methods walk only registered AND active databases. Events in unregistered databases or in SetDatabaseActive(false) databases are invisible to query β same as they're invisible to Raise().
This is intentional: the query reflects what would actually fire if you called Raise() on the result.
3. Recipesβ
Recipe: Save & Loadβ
Persist event GUIDs, restore at load time:
[Serializable]
public class QuestState
{
public string completionEventGuid; // serialized to disk
}
public class QuestSystem
{
public void OnQuestComplete(QuestState quest)
{
var mgr = GameEventManager.Instance;
if (mgr.TryGetGameEvent(quest.completionEventGuid, out var evt))
evt.Raise();
else
Debug.LogWarning($"Quest event GUID '{quest.completionEventGuid}' missing β DB unloaded?");
}
}
GUIDs survive code refactors, asset moves, and even renames β only deletion or unregistering invalidates them.
Recipe: Addressables Lazy Loadingβ
Load a database on demand, raise an event from it, unload when done:
public async Task FireBossDefeatedSequence()
{
var handle = Addressables.LoadAssetAsync<GameEventDatabase>("BossEvents");
var db = await handle.Task;
GameEventManager.Instance.RegisterDatabase(db);
// Now events from this DB are queryable
if (GameEventManager.Instance.TryGetFirstGameEventByName("OnBossDefeated", out var evt))
evt.Raise();
// Optional: unload after the cutscene
GameEventManager.Instance.UnregisterDatabase(db);
Addressables.Release(handle);
}
Recipe: Pure C# Service Layerβ
Non-MonoBehaviour code can drive events through the manager:
public class AnalyticsService
{
private const string AnalyticsCategory = "Analytics";
public void TrackPlayerAction(string actionName)
{
var mgr = GameEventManager.Instance;
if (mgr == null) return;
// Look up the event by convention: name+category
if (mgr.TryGetFirstGameEventByName<string>(actionName, out var evt, AnalyticsCategory))
evt.Raise(actionName);
}
}
No [SerializeField], no MonoBehaviour, no scene wiring β the service finds events by metadata.
Recipe: Batch Raising by Conventionβ
Raise every event matching a name across all active databases (e.g., a "phase change" broadcast):
foreach (var e in GameEventManager.Instance.GetGameEvents("OnPhaseStart"))
e.Raise();
Combine with category filter to scope the broadcast:
foreach (var e in GameEventManager.Instance.GetGameEvents("OnTurnStart", "AI"))
e.Raise();
4. Common Pitfallsβ
Pitfall: GUID lookup returns null after a refactorβ
Cause: the event was deleted or moved to an unregistered database. GUIDs are stable for an asset's lifetime, but if the asset itself is gone, the GUID is dead.
Fix: validate GUIDs at load time and log missing ones. For migrations, build a oldGuid β newGuid lookup.
Pitfall: Inactive database events are invisibleβ
Cause: SetDatabaseActive(db, false) makes the database's events disappear from query results.
Fix: if you want to introspect inactive content, temporarily activate, query, then deactivate. Don't rely on inactive events being queryable.
Pitfall: Name lookups silently return wrong eventβ
Cause: two events share a name across categories or databases; GetFirstGameEventByName picked an unexpected one.
Fix: always pass the category filter when ambiguity is possible. For full safety, use GUID. If you need all matches, use the plural GetGameEvents.
Pitfall: Type-arity mismatch returns 0β
Cause: GetGameEvents<int>("OnJump") returns 0 because OnJump is a void event, not GameEvent<int>.
Fix: this is by design β strict type filtering prevents cross-type accidents. If you actually want any type, drop the generic argument.
Pitfall: Querying in Awake of a non-singleton scriptβ
Cause: GameEventManager.Instance may not have completed its own Awake yet, so registered databases haven't initialized.
Fix: query in Start or later. For domain-load-time queries, hook RuntimeInitializeOnLoadMethod(SubsystemRegistration).
Pitfall: Re-querying every frameβ
Cause: treating the query API like a property lookup β calling GetFirstGameEventByName in Update.
Fix: cache the resolved GameEvent reference once (e.g., in Start or after database registration), and reuse it. The query iterates the database list β cheap, but not zero-cost.