Event System
Event System
The event system is how NPC dialogue translates into gameplay actions. The LLM suggests events as part of its structured response, and the system validates and dispatches them through a strict pipeline before anything happens in-game.
Event Lifecycle
LLM Response
└─ event_suggestions: [
{ "event_id": "shop.open", "confidence": 0.95, "args": { "shop_id": "..." } },
{ "event_id": "anim.trigger", "confidence": 0.8, "args": { "anim_id": "nod" } }
]
│
▼
┌─────────────────────────────┐
│ 1. Confidence Gate │ event.confidence >= config.fireConfidenceThreshold?
│ (NpcBrainConfig) │ No → event silently skipped
└──────────────┬──────────────┘
│ Yes
▼
┌─────────────────────────────┐
│ 2. Normalization │ AiNpcBrain fills in NPC-specific args:
│ (AiNpcBrain) │ - shop_id from NpcUiBindings
│ │ - quest_board_id from NpcUiBindings
│ │ - relationship_id from NpcUiBindings
│ │ - anim_id fallback from context
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ 3. EventGate Validation │ Checks against NpcEventCatalog:
│ (EventGate.cs) │ - Event exists in catalog?
│ │ - All required args present?
│ │ - Arg types correct?
│ │ - Values in allowed list?
│ │ - No unexpected extra args?
└──────────────┬──────────────┘
│ Pass
▼
┌─────────────────────────────┐
│ 4. Payload Dispatch │ JSON string fired via:
│ (UnityEvent<string>) │ OnAnyNpcEventPayload.Invoke(payload)
└──────────────┬──────────────┘
│
┌────────┴────────┐
▼ ▼
NpcAnimationReceiver AureliaDemoReceiver
(or your receiver) (or your receiver)
Event Catalog Setup
The NpcEventCatalog ScriptableObject is the whitelist. Every event the NPC can suggest must be defined here.
Defining an Event
Each EventDefinition has:
| Field | Example | Purpose |
|---|---|---|
eventId |
shop.open |
The unique identifier used in payloads. |
description |
"Opens the NPC's shop for the player to browse and buy items." | Included in the LLM prompt so it understands when to suggest this event. |
args |
[{ name: "shop_id", type: String, allowedValues: ["npc_aurelia_alchemist.shop"] }] |
Schema for required arguments. |
Standard Events
These are the events used in the demo. You can add, remove, or modify them.
Shop Events
| Event ID | Args | Description |
|---|---|---|
shop.open |
shop_id (String) |
Opens the NPC's shop inventory. |
shop.sell_mode |
shop_id (String) |
Opens the shop in sell/buyback mode. |
Quest Events
| Event ID | Args | Description |
|---|---|---|
quest.offer |
quest_id (String) |
The NPC offers a specific quest. |
quest.open_board |
quest_board_id (String) |
Opens the quest board panel. |
quest.turn_in |
quest_id (String) |
The player completes a quest. |
Relationship Events
| Event ID | Args | Description |
|---|---|---|
relationship.open_status |
relationship_id (String) |
Opens the relationship status display. |
relationship.request_stage_up |
relationship_id (String) |
NPC requests advancing the relationship stage. |
relationship.require_item |
relationship_id (String) |
NPC requires an item to progress the relationship. |
relationship.give_gift |
relationship_id (String) |
Player gives a gift to the NPC. |
Animation Events
| Event ID | Args | Description |
|---|---|---|
anim.trigger |
anim_id (String, constrained to allowed values) |
Triggers an NPC animation. |
anim.move_to |
move_id (String, constrained to allowed values) |
Moves the NPC to a predefined position. |
UI Events
| Event ID | Args | Description |
|---|---|---|
ui.show_hint |
hint_id (String) |
Shows a UI hint or tooltip. |
EventGate Validation Rules
EventGate.TryBuildPayload() performs these checks in order:
- Catalog exists — returns false if no catalog is assigned.
- Event ID exists — the
event_idmust match an entry inNpcEventCatalog.events. - Args object exists — the suggestion must have a non-null args dictionary.
- Required args present — every arg defined in the catalog schema must be provided.
- Type validation — each arg value must parse as the expected type (
Stringalways passes,Int/Float/Boolmust parse). - Allowed values — if the schema arg has a non-empty
allowedValueslist, the provided value must be in that list. - No extra args — any arg key not in the schema is rejected.
If all checks pass, a minimal JSON payload is built and returned:
{"event_id":"shop.open","args":{"shop_id":"npc_aurelia_alchemist.shop"}}
Event Normalization
Before validation, AiNpcBrain.NormalizeEventSuggestion() patches the event args to ensure consistency:
| Event ID | Normalization |
|---|---|
shop.open, shop.sell_mode |
Forces shop_id to the value from NpcUiBindings.shopWindowId. |
quest.open, quest.board |
Forces quest_board_id to NpcUiBindings.questBoardWindowId. |
relationship.* |
Forces relationship_id to NpcUiBindings.relationshipWindowId. |
anim.trigger |
If anim_id is missing, auto-selects one based on reply text, emotion, and user input. |
anim.move_to |
If move_id is missing, defaults to step_forward. |
This means even if the LLM omits or gets an arg wrong, the system fills in the correct NPC-specific value.
Auto-Animation Fallback
If the LLM doesn't suggest any anim.trigger event in a turn, AiNpcBrain automatically picks an animation based on context:
| Context | Animation Chosen |
|---|---|
| Reply contains "hello", "hi", "greetings" | wave or smile |
| Reply contains "yes", "of course", "certainly" | nod or smile |
| Reply contains "maybe", "i'm not sure" | shrug or think |
Emotion = happy |
smile or nod |
Emotion = curious |
nod or point |
Emotion = sad or afraid |
shrug |
| Generic fallback | nod → smile → blink → idle_shift |
This ensures the NPC always has some body language, even without explicit LLM guidance.
Receiving Events
NpcAnimationReceiver
Handles anim.trigger and anim.move_to events.
Animation triggers: Looks up the anim_id in NpcAnimationMap to find the actual Animator trigger name, then calls Animator.SetTrigger().
Movement: Supports these built-in move_id values:
| move_id | Behavior |
|---|---|
step_forward |
Moves the NPC forward by stepDistance units. |
step_back |
Moves the NPC backward by stepDistance units. |
approach_player |
Moves to the playerAnchor transform. |
leave_player |
Moves to the idleAnchor transform. |
AureliaDemoReceiver
Routes non-animation events to UI panels via WindowRegistryRuntime. It:
- Parses the JSON payload to extract
event_idandargs. - Normalizes common model aliases (e.g.
quest.open→quest.open_board). - Looks up the target window ID (from args or
NpcUiBindingsfallback). - Closes all panels, then opens the matching one.
- Supports Escape to close all panels.
Writing a Custom Event Receiver
To handle events your own way:
public class MyEventReceiver : MonoBehaviour
{
public void OnEventPayload(string payload)
{
// Parse the JSON payload
// payload format: {"event_id":"...","args":{"key":"value"}}
// Route to your game systems
}
}
Then wire AiNpcBrain.OnAnyNpcEventPayload → MyEventReceiver.OnEventPayload in the Inspector.
Adding a Custom Event
- Open your
NpcEventCatalogasset in the Inspector. - Add a new entry to the
eventslist: eventId: your unique ID, e.g.trade.barterdescription: explain to the LLM when to suggest itargs: define required arguments with types and optional allowed values- Write a receiver that handles this event ID.
- Wire it to
OnAnyNpcEventPayload.
The LLM will automatically see the new event in its prompt and can start suggesting it in contextually appropriate moments.