Architecture Overview
Architecture Overview
High-Level Flow
Player types message
│
▼
AiNpcBrain.SendAsync()
│
├─ 1. Append player turn to ConversationState
├─ 2. Query relevant memories from IMemoryStore
├─ 3. IPromptPolicy.BuildPrompt() → (system, developer, user)
├─ 4. INpcModelProvider.GenerateAsync() → raw LLM text
├─ 5. IResponseValidator.TryParseAndValidate() → NpcModelResponse
├─ 6. Apply relationship delta (clamped)
├─ 7. Store new memory facts (bounded)
├─ 8. Append NPC turn, persist to disk
├─ 9. FireEvents() → validate against EventCatalog → dispatch payloads
└─ 10. Fire Unity events (OnNpcReply, OnEmotionChanged)
│
┌───────────┴───────────┐
▼ ▼
NpcAnimationReceiver AureliaDemoReceiver
(triggers Animator) (opens/closes UI panels)
Core Components
AiNpcBrain (MonoBehaviour)
The central orchestrator. Lives on the NPC's GameObject. Accepts player text, drives the full pipeline, and emits results via UnityEvents.
References (assigned in Inspector):
- PersonaDefinition — who the NPC is
- NpcBrainConfig — how the brain behaves
- NpcEventCatalog — what events are allowed
- NpcAnimationMap — what animations are available (optional)
- NpcUiBindings — canonical window IDs for this NPC (optional)
Internal dependencies (created in Awake):
- IMemoryStore → JsonFileMemoryStore
- IPromptPolicy → DefaultPromptPolicy
- IResponseValidator → DefaultResponseValidator
- INpcModelProvider → OpenAiChatProvider, OllamaProvider, MockProvider
Provider System
Three providers implement INpcModelProvider:
| Provider | Backend | When Used |
|---|---|---|
OpenAiChatProvider |
OpenAI /chat/completions |
Cloud LLM (default) |
OllamaProvider |
Ollama /api/generate |
Local LLM |
MockProvider |
Keyword matching | Offline fallback |
The brain picks a primary provider based on NpcBrainConfig.providerMode, then falls back to the other if the primary fails, and finally to MockProvider as a last resort.
Prompt System
DefaultPromptPolicy builds a three-part prompt:
| Section | Content |
|---|---|
| System | Immutable rules: stay in character, return JSON only, PG-13 content |
| Developer | NPC persona, relationship state, known facts, recent conversation, world state, event catalog, animation guidance, output JSON schema |
| User | Player's utterance + metadata (player ID, location, time of day) |
Response Validation
DefaultResponseValidator parses the raw LLM output:
1. Strips optional code fences (```json ... ```)
2. Deserializes via JsonUtility.FromJson<NpcModelResponse>()
3. Validates required fields (reply_text must exist)
4. Clamps/sanitizes values (emotion tag whitelist, text length cap, importance 0–1)
Event System
The LLM suggests events as part of its JSON response. Before any event fires:
- Confidence gate — event must meet
fireConfidenceThreshold - Normalization —
AiNpcBrain.NormalizeEventSuggestion()fills in NPC-specific IDs (shop_id, quest_board_id, etc.) - EventGate validation — checks that the event exists in the catalog, all required args are present, types match, values are in the allowed list, and no extra args are included
- Dispatch — validated JSON payload is fired via
OnAnyNpcEventPayloadUnityEvent
Memory System
JsonFileMemoryStore persists per-NPC, per-player state to disk:
{persistentDataPath}/AiNpcBrain/{playerId}/{npcId}.json
Each file contains:
- RelationshipState — affinity, trust, fear, respect, flags, stage
- List<Turn> — rolling conversation history (max 30 turns)
- List<MemoryFact> — long-lived facts (max 50)
Data Flow Diagram
ScriptableObject Assets (design-time)
┌─────────────────┐ ┌──────────────┐ ┌────────────────┐ ┌────────────────┐
│PersonaDefinition│ │NpcBrainConfig│ │NpcEventCatalog │ │NpcAnimationMap │
└────────┬────────┘ └──────┬───────┘ └───────┬────────┘ └───────┬────────┘
│ │ │ │
└──────────────────┴─────────┬─────────┴───────────────────┘
│
▼
┌─────────────┐
│ AiNpcBrain │ (MonoBehaviour on NPC)
└──────┬──────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│PromptPolicy │ │MemoryStore │ │ ModelProvider│
│(builds prompt)│ │(load/save) │ │(calls LLM) │
└──────────────┘ └─────────────┘ └──────────────┘
Runtime State (per player)
┌──────────────────────────────────────────────────┐
│ ConversationState │
│ ├─ RelationshipState (affinity, trust, ...) │
│ ├─ List<Turn> (conversation history) │
│ └─ List<MemoryFact> (learned facts) │
│ │
│ Persisted to: {persistentDataPath}/AiNpcBrain/ │
└──────────────────────────────────────────────────┘
Namespace Structure
| Namespace | Location | Contents |
|---|---|---|
AiNpcBrain |
Runtime/, Scripts/ |
All runtime classes, interfaces, and DTOs |
AiNpcBrain.Editor |
Editor/ |
Editor windows, custom inspectors, sync utilities |
| (global) | Scripts/DemoCursorUnlock.cs |
Demo utility (no namespace) |
Folder Structure
Assets/
├── Documentation/ ← You are here
├── Editor/ ← Editor-only scripts (custom inspectors, tools)
├── Runtime/ ← Core library
│ ├── Core/ ← AiNpcBrain, DTOs, interfaces, ScriptableObjects
│ ├── Dialogue/ ← Prompt building, response validation
│ ├── Memory/ ← JSON file persistence
│ ├── Npc/ ← NPC UI bindings
│ ├── Providers/ ← LLM provider implementations
│ ├── Samples/ ← Sample chat UI controllers
│ └── UI/ ← WindowPanelId component
├── Scripts/ ← Demo-specific scripts (animation, receivers)
├── Samples/ ← Pre-built ScriptableObject assets for Aurelia demo
├── Prefabs/UI/ ← Chat message prefab
├── Scenes/ ← Demo scene
├── Mixamo Character/ ← Character model + animations
└── Settings/ ← URP rendering settings
Extension Points
The system uses interfaces for its core strategies. You can replace any of them:
| Interface | Default Implementation | Purpose |
|---|---|---|
INpcModelProvider |
OpenAiChatProvider, OllamaProvider, MockProvider |
Add a new LLM backend (Anthropic, Gemini, etc.) |
IPromptPolicy |
DefaultPromptPolicy |
Change how prompts are assembled |
IResponseValidator |
DefaultResponseValidator |
Change how LLM output is parsed/validated |
IMemoryStore |
JsonFileMemoryStore |
Switch to a database, cloud storage, etc. |
To use a custom implementation, modify AiNpcBrain.Awake() to instantiate your class instead of the default.