Database Management & Runtime Query
Register GameEventDatabase assets at runtime and locate events by GUID, name, category, or type across Addressables, DLC, mods, and save data.
For programmers driving events from pure C# code, non-MonoBehaviour services, or dynamically loaded content.When content loads at runtime, call RegisterDatabase on GameEventManager.Instance. Code can then find events in active databases by GUID, name, category, or type. Use SetDatabaseActive to control which registered databases are searchable, and UnregisterDatabase when the content is removed.
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.