Skip to main content

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 / 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.