-
Notifications
You must be signed in to change notification settings - Fork 0
Event Handler
Map tags to behaviors in a DataTable. Change what happens without changing code.
Every game has a collection of "when X happens, do Y" rules. When the player enters a zone, trigger a cutscene. When a boss dies, drop loot and open the door. When a quest completes, grant XP and update the journal.
In code, this starts clean:
if (Event == "BossDied")
{
SpawnLoot();
OpenDoor();
PlayVictoryMusic();
}
Then it grows. 10 events become 50. Each event has conditions: only spawn loot if the player has enough inventory space. Only open the door if all bosses in the zone are dead. Only play music if the audio system is not in a loading-screen mute state.
Now add the requirement that designers can change behaviors without recompilation. "When the boss dies, we no longer want to open the door. We want to spawn a portal instead." That requires either a code change, a complex scripting system, or a data-driven architecture that maps events to behaviors.
Most projects build this ad-hoc. The inventory system has its own event dispatch. The quest system has another. The combat system has a third. None share infrastructure. None share telemetry. When production asks "how many times did event X fire, and how long did it take to execute?" the answer is "we would need to add logging and deploy a new build."
The deeper issue: hardcoded behaviors make games rigid during the phase where they most need to be flexible -- production. The earlier in development that event-to-behavior mappings become data-driven, the more iteration the design team gets before ship.
The Event Handler is a data-driven behavior resolution bus. It maps GameplayTags to handler classes via DataTable rows, resolving and executing behaviors at runtime.
The model is deliberately 1:1 -- one tag maps to one handler. This complements the Message System's 1:N notification model. Together with the Message System and the Data Registry, the Event Handler forms the three-bus L1 infrastructure:
+------------------+ +------------------+ +------------------+
| Message System | | Event Handler | | Data Registry |
| 1:N Notify | | 1:1 Execute | | O(1) Store |
| | | | | |
| "Something | | "When this tag | | "What is the |
| happened, | | fires, run | | data for this |
| anyone who | | THIS specific | | tag?" |
| cares: here | | behavior." | | |
| is the data." | | | | |
+------------------+ +------------------+ +------------------+
Game Code / L2 Plugin
|
v "Execute tag PGX.Event.BossDied with context + payload"
+-----------------------------------------------------------+
| Event Handler Subsystem (GameInstance scope) |
| |
| 1. RESOLVE ------> Lookup tag in Handler Tables |
| | (DataTable rows: tag --> class) |
| | |
| 2. INSTANTIATE --> Get or create handler instance |
| | (respecting Lifecycle mode) |
| | |
| | +-- Singleton: one instance forever |
| | +-- Cached: reused until LRU eviction |
| | +-- Ephemeral: new instance, destroyed after |
| | |
| 3. VALIDATE -----> Handler.CanExecute(Context)? |
| | (conditions check, prerequisites) |
| | |
| 4. EXECUTE ------> Handler.Execute(Context) |
| | (the actual behavior) |
| | |
| 5. RECORD -------> Telemetry + Blackbox + Delegates |
| (execution count, time, success/fail) |
+-----------------------------------------------------------+
Each row in a handler DataTable maps a tag to a handler:
| Field | Purpose |
|---|---|
| Event Tag | The GameplayTag that triggers this handler |
| Handler Class | The class to instantiate (soft reference -- loaded on demand) |
| Lifecycle | How instances are managed: Singleton, Cached, or Ephemeral |
| Category Tag | Grouping for telemetry and budget management |
| Description | Human-readable explanation (for editor tooling) |
| Expected Payload Type | Optional type hint for validation |
| Enabled | Toggle without removing the row |
Multiple DataTables can be registered. The subsystem merges them into a unified lookup. Tables from a DLC pack can add handlers for DLC-specific events without modifying base game tables.
The lifecycle mode determines how handler instances are created and destroyed:
SINGLETON CACHED EPHEMERAL
+----------+ +----------+ +----------+
| Instance | | Instance | | Instance |
| created | | created | | created |
| once | | on first | | per call |
| | | use | | |
| lives | | lives in | | destroyed|
| forever | | LRU cache| | after |
| | | | | execute |
| never | | evicted | | |
| evicted | | when | +----------+
+----------+ | cache |
| full |
+----------+
| Mode | When To Use |
|---|---|
| Singleton | Handlers that maintain state across executions. A score tracker, a session recorder, a persistent state machine. One instance for the entire game session. Never evicted. |
| Cached | Handlers that are expensive to create but stateless between executions. A pathfinding handler, a complex condition evaluator. Reused until the LRU cache needs space. |
| Ephemeral | Handlers that should be fresh every time. A one-shot effect, a randomized spawn. Created, executed, destroyed. No cache footprint. |
The LRU cache has a configurable maximum capacity (default: 128). When full, the least-recently-used Cached handler is evicted. Singleton handlers are never evicted. Category budgets can further limit how many handlers of a specific category reside in cache simultaneously.
Handlers can declare prerequisites via a condition check method. Before execution, the subsystem calls the handler's condition check with the execution context. If the check returns false, execution is skipped and the result reflects the condition failure.
ResolveAndExecute("SpawnLoot")
|
v
Handler.CanExecute(Context)?
|
+-- Checks: Does the player have inventory space?
+-- Checks: Is the loot table configured for this zone?
+-- Checks: Is the difficulty setting above minimum?
|
+-- All pass? --> Execute
+-- Any fail? --> Skip (logged, telemetry recorded as condition failure)
Built-in condition handlers provide common checks: "not currently loading," "not currently saving." Game code adds domain-specific conditions.
A handler can dispatch sub-events during execution:
Handler for "BossDied"
|
+-- Execute sub-handler: "SpawnLoot"
+-- Execute sub-handler: "OpenPortal"
+-- Execute sub-handler: "PlayVictoryMusic"
This enables composite behaviors without monolithic handler classes. Each sub-behavior is its own handler, independently configurable and replaceable.
A depth guard prevents infinite recursion. The maximum recursion depth is configurable (default: 8). If exceeded, execution is aborted and logged as an error. This catches circular dispatch chains at runtime rather than allowing stack overflows.
Every handler execution is tracked per-tag:
| Metric | What It Measures |
|---|---|
| Execution Count | How many times this tag has been executed |
| Total Time | Cumulative execution time across all invocations |
| Average Time | Mean execution time per invocation |
| Last Executed | Timestamp of most recent execution |
| Failure Count | How many times execution failed (condition or runtime error) |
This data is available at runtime via API, console commands, and the inspector. When production asks "how often does event X fire?" the answer is in the telemetry, not in a feature request.
A circular buffer records the most recent executions (configurable, default: 256 entries):
| Field | Recorded |
|---|---|
| Timestamp | When the execution occurred |
| Event Tag | Which tag was executed |
| Handler Class Name | Which handler ran |
| Duration | How long execution took |
| Success/Failure | Whether execution completed or failed |
| Error Message | If failed, the reason |
The blackbox is the "flight recorder" of your event system. When something goes wrong in a play session, the last 256 executions are available for inspection -- even if no logging was enabled.
A full diagnostic report exports as a Markdown document:
- All registered handler tags with classes and lifecycles
- Per-tag telemetry (execution count, average time, failure rate)
- Cache statistics (size, hit rate, evictions)
- Recent blackbox entries
The report can be triggered manually, via console command, or automatically when a PIE session ends (configurable).
Multiple event tags can be executed in sequence with a single call. Optionally, the sequence stops on the first failure. This supports scripted multi-step behaviors: "open door, then play sound, then spawn enemies" as a single atomic operation.
A config Data Asset controls system behavior:
| Setting | Default | Purpose |
|---|---|---|
| Handler Tables | [] | DataTables to auto-register at initialization |
| Max Cached Handlers | 128 | LRU cache capacity |
| Max Execution Depth | 8 | Recursion guard for sub-handler dispatch |
| Category Budgets | {} | Max cached handlers per category tag |
| Blackbox Buffer Size | 256 | Circular buffer capacity |
| Log Executions | false | Log every execution at verbose level |
| Log Cache Misses | false | Log cache misses and evictions |
| Auto Export On PIE End | false | Export Markdown report when editor play session ends |
Zero-config default: if no config DA exists, the system initializes with default values and no handler tables. Tables can be registered at runtime by any system.
The framework ships with a set of utility handlers that demonstrate the architecture and provide common functionality:
| Handler | Tag | What It Does |
|---|---|---|
| Auto Save | PGX.Event.AutoSave | Broadcasts a save-trigger message via the Message Bus. The Save system (if present) listens and acts. No direct dependency. |
| Flush Config | PGX.Event.FlushConfig | Forces a configuration Data Asset reload. Useful after hot-reload of data. |
| Not Loading Condition | PGX.Event.Condition.NotLoading | Condition check: returns false if a loading screen is active. Used as a pre-condition for handlers that should not run during loads. |
| Not Saving Condition | PGX.Event.Condition.NotSaving | Condition check: returns false if a save operation is in progress. |
| Audio Ceremony | PGX.Event.Audio.* | Audio event triggers via the Message Bus. Plays ceremony sounds, transitions music, without direct Audio system dependency. |
All built-in handlers communicate with L2 systems exclusively through the Message Bus. They never call another system's API directly. This preserves the star topology and demonstrates the intended pattern for all game handlers.
Handler Browser:
+---------------------------------------------------------------+
| Event Tag | Handler | Lifecycle | On |
|--------------------------+------------------+-----------+------|
| PGX.Event.AutoSave | AutoSaveHandler | Singleton | [x] |
| PGX.Event.BossDied | BossDeathHandler | Ephemeral | [x] |
| PGX.Event.SpawnLoot | LootSpawner | Cached | [x] |
| PGX.Event.OpenPortal | PortalHandler | Ephemeral | [ ] |
| | | | |
| [Search: _________] [Filter: Category v] | |
+---------------------------------------------------------------+
Lists all registered handlers with their tags, handler class names, lifecycle modes, and enabled states. Search and category filter for large handler tables.
Telemetry Dashboard:
+---------------------------------------------------------------+
| Event Tag | Executions | Avg Time | Failures | Last |
|---------------------+------------+----------+----------+------|
| Event.BossDied | 47 | 2.3 ms | 0 | 12s |
| Event.SpawnLoot | 193 | 0.8 ms | 3 | 4s |
| Event.AutoSave | 12 | 15.1 ms | 0 | 45s |
| Event.OpenPortal | 0 | - | 0 | - |
+---------------------------------------------------------------+
Per-tag execution metrics with sortable columns. Immediately answers "which handlers are slow?" and "which handlers fail?"
Blackbox Viewer:
+---------------------------------------------------------------+
| Time | Tag | Duration | Result |
|-------------+-------------------+----------+------------------|
| 14:32:01.8 | Event.SpawnLoot | 0.7 ms | Success |
| 14:32:01.2 | Event.BossDied | 2.1 ms | Success |
| 14:31:59.9 | Event.OpenPortal | - | Condition Failed |
| 14:31:55.4 | Event.AutoSave | 14.8 ms | Success |
+---------------------------------------------------------------+
Scrollable log of recent executions. Timestamped, with duration and success/failure status. Error messages visible on selection.
pgx.event.status Registered handlers, cache usage, tables loaded
pgx.event.list All registered tags with handler classes
pgx.event.cache Cache contents: handler, lifecycle, hit count
pgx.event.cache.clear Force-clear entire handler cache
pgx.event.telemetry Per-tag execution stats
pgx.event.blackbox Recent blackbox records
pgx.event.report Export full Markdown report
The Event Handler and Message System are complementary:
Message System (1:N) Event Handler (1:1)
"Something happened. "When this tag fires,
Here's the data. run THIS specific
Anyone who cares, behavior."
react."
combined
+-----------+ +-----------------+ +----------+
| Broadcast | | Event triggers | | Handler |
| to all |--->| handler, which |--->| broadcasts|
| listeners | | executes and | | results |
+-----------+ | then broadcasts | | back via |
| results via | | Message |
| Message Bus | | Bus |
+-----------------+ +----------+
A typical flow: game code fires an event tag. The Event Handler resolves and executes the handler. The handler broadcasts its results via the Message Bus. Multiple systems react to those results without knowing about each other.
Handler DataTables are themselves data assets. They can be registered in the Data Registry for indexed lookup. A DLC pack registers its handler table, which the subsystem auto-discovers and merges into the unified lookup.
The 27 Blueprint nodes cover all common operations:
| Category | What It Covers |
|---|---|
| Core (2) | Execute an event (simple and with typed payload) |
| Query (10) | Check conditions, test registration, inspect handlers, get categories |
| Advanced (9) | Register/unregister tables and handlers, sequence execution, cache management |
| Debug (6) | Telemetry queries, blackbox access, report export |
| Event | When It Fires |
|---|---|
| Handler Executed | After a handler completes execution (includes tag and handler reference) |
| Handler Not Found | When a tag has no registered handler (useful for debugging missing configurations) |
| Cache Changed | When the cache size changes (handler added or evicted) |
The Event Handler addresses a structural problem in game development: the gap between "the design team wants to change game behaviors daily" and "every behavior change requires a code change."
By mapping GameplayTags to handler classes via DataTables, the Event Handler makes behavior resolution a data authoring problem. Designers add a row to a DataTable. Programmers write handler classes. Neither blocks the other.
The 1:1 model is a deliberate constraint. The Message System already handles 1:N notification. The Event Handler focuses on what to DO when an event occurs, not who to NOTIFY. This separation of concerns keeps both systems simple and composable.
The telemetry and blackbox are not afterthoughts -- they are core features. In production, the most common question about event systems is not "does it work?" but "what actually happened?" The blackbox answers that question for the last 256 executions, with timestamps, durations, and success/failure status. The telemetry answers "how does it perform?" with per-tag execution counts and average times.
For projects with dozens or hundreds of game events, the Event Handler provides the infrastructure that otherwise gets built ad-hoc in each system. One resolution mechanism, one cache, one telemetry pipeline, one debugging interface. Applied uniformly across the entire project.
- Development Preview
- Getting Started
- Release branch catalog
- Public Plugin Matrix
- Early Preview Plugins
- Known Issues
- Architecture Overview
- Plugin Topology
- Module Reference
- Configuration and Registry
- Data-Driven Design
- Profiles and Budgets
- Gameplay Tag Architecture
- Initialization Pipeline
- Cross-Plugin Communication
- Message System
- Event Handlers
- Logging and Trace
- Runtime Flows
- Blueprint API Design
- Editor Integration
- Editor Visual System