Skip to main content

Database Management & Runtime Query

GES Β· Runtime API

Register and unregister GameEventDatabase assets at runtime, and locate events by GUID, name, category, or type without serialized references. Covers Addressables, DLC, mods, and save/load.

For programmers driving events from pure C# code, non-MonoBehaviour services, or dynamically loaded content.

The static workflow ties events to scripts via [SerializeField] GameEvent references. That works well for scene-bound logic, but it breaks down when events live behind dynamic content boundaries such as Addressables, DLC, mods, save/load systems, or pure C# domain code that cannot carry serialized fields.

The GameEventManager.Instance exposes two complementary API families to bridge that gap:

  • Database Lifecycle: Register / Unregister / SetActive databases 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​

ScenarioRecommended Approach
Logic on a scene-bound MonoBehaviourStatic workflow β€” [SerializeField] GameEvent
Pure C# class / service / domain layerRuntime Query by GUID or Name
Save/load β€” re-raise an event from persisted dataRuntime Query by GUID (stable identity)
Scene/DLC/mod adds new events at runtimeDatabase Lifecycle β€” RegisterDatabase
Pause/cutscene wants to mute a whole event groupDatabase Lifecycle β€” SetDatabaseActive(false)
Plugin needs every event matching a name patternRuntime 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);
OperationList MembershipEvent InvocationUse For
RegisterDatabase(db)βœ… Addedβœ… ActiveLoading new content
UnregisterDatabase(db)❌ Removed❌ InactiveUnloading content (full cleanup)
SetDatabaseActive(db, false)βœ… Stays❌ MutedTemporary gating without losing state
SetDatabaseActive(db, true)βœ… Staysβœ… ActiveResume 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:

ArityMatchesReturns
Non-genericAny event typeGameEventBase
<T>GameEvent<T> onlyGameEvent<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.