Subsystem Overview
The Relationship System uses a facade pattern where the public RelationshipManager delegates to 11 internal subsystems. This design improves testability, performance, and extensibility while keeping the public API unchanged.
Facade Pattern
RelationshipManager (MonoBehaviour Singleton — Public Facade)
│
├─ EntityRegistry Entity registration & lookup
├─ RelationshipStore CRUD for faction relationships & entity overrides
├─ RelationshipResolver Hierarchical lookup with inheritance
├─ RelationshipEventBus Central event aggregation & distribution
├─ DecayProcessor Interval-batched decay with dirty-set tracking
├─ ModifierProcessor Temporary buffs/debuffs with stacking
├─ InheritancePropagator Push-based parent→child propagation
├─ HistoryTracker Per-relationship audit log
├─ CapProcessor Min/max value caps per relationship
├─ DiplomacyManager Discrete faction states & transitions
└─ RuntimeFactionRegistry Dynamic faction creation & lifecycleAll subsystems are internal — users interact only with the RelationshipManager facade or GC2 visual scripting components. Each subsystem can be unit-tested in isolation using mock interfaces.
Subsystem Details
Manages entity registration, lookup by ID, and faction resolution. Tracks which entities are active and provides GetPrimaryFaction() for hierarchical resolution.
Storage layer with separate dictionaries for faction relationships and entity overrides. Handles multi-dimensional queries (F1), the pending relationship queue (R1), and save/load serialization.
Implements the hierarchical lookup chain. Supports symmetric, directional, scoped, and inherited lookups. Returns a RelationshipResult with source information and inheritance multiplier.
Central event hub. All subsystems fire events through the bus, and the manager exposes them publicly. Events include value changes, level transitions, faction membership, modifier lifecycle, cap reached, diplomacy transitions, and runtime faction creation/dissolution.
Processes relationship decay using interval batching and a dirty-set. Only relationships with active decay are tracked, and processing happens at configurable intervals (default: 5 seconds) instead of every frame. Supports Linear, Curve, and Asymmetric modes.
Manages temporary modifiers (buffs/debuffs). Processes modifier stacking in order: Flat (additive) → Percentage (multiplicative) → Override (last wins). Handles time-based expiry and target filtering by entity, definition, or faction.
Push-based inheritance propagation. When a parent faction's relationship changes, the propagator applies scaled deltas to child factions based on theirinheritanceRate andPropagationMode.
Optional per-definition audit log. Uses circular buffers to track relationship changes with timestamps, old/new values, delta, source labels, and level transitions. Enable viaenableHistory on the definition.
Enforces min/max value caps per relationship. Caps can be added and removed dynamically (e.g., quest completion removes a cap). Fires events when a modification is constrained.
Manages discrete faction-to-faction states (War, Peace, Alliance, Ceasefire, Embargo). Supports auto-transitions based on relationship value thresholds and applies state-specific modifiers to relationship changes.
Manages factions created at runtime (player guilds, dynamic alliances). RuntimeFactions implement IRelationshipEntity and integrate fully with the existing hierarchy, save/load, and resolution systems.
Hierarchical Resolution
When you query a relationship, the resolver walks a prioritized lookup chain. The first match wins. This allows entity-level overrides to take priority over faction defaults, and faction relationships to serve as fallbacks.
Lookup Chain (Symmetric)
GetRelationship("player", "merchant", definition)
1. Entity Override (scoped) → player↔merchant [definition_id]
2. Entity Override (default) → player↔merchant [null]
3. Faction Relationship (scoped) → factionA↔factionB [definition_id]
4. Faction Relationship (default) → factionA↔factionB [null]
5. Parent Faction Inheritance → parentA↔factionB (×inheritanceRate)
└─ Walks up hierarchy: grandparent×rate×rate (max depth: 3)
6. None → No relationship foundThe result includes a RelationshipSource enum indicating where the value came from (EntityOverride, Faction, FactionInherited, or None) and, for inherited results, the InheritanceMultiplier and originating faction.
Multi-Dimensional Lookup
When a RelationshipDefinition is specified, the lookup is scoped to that dimension. Without a definition, the resolver falls back to the default (unscoped) relationship — a simple one-value-per-pair lookup.
// Two independent relationship dimensions between the same entities:
GetRelationship("player", "merchant", trustDefinition) → 75 (Trusted)
GetRelationship("player", "merchant", tradeDefinition) → 30 (Neutral)
GetRelationship("player", "merchant") → Default/legacy lookupEvent System
The RelationshipEventBus aggregates events from all subsystems. The manager exposes these as standard C# events on its public API.
| Event | Signature |
|---|---|
| OnRelationshipValueChanged | (entityAId, entityBId, oldValue, newValue) |
| OnRelationshipLevelChanged | (entityAId, entityBId, oldLevel, newLevel) |
| OnRelationshipCreated | (entityAId, entityBId, state) |
| OnFactionMemberAdded | (entity, faction) |
| OnFactionMemberRemoved | (entity, faction) |
| OnCapReached | (entityAId, entityBId, cap) |
| OnRuntimeFactionCreated | (runtimeFaction) |
| OnRuntimeFactionDissolved | (factionId, memberIds) |
RelationshipKey
The RelationshipKey struct is the unified key used throughout the system for identifying relationships. It supports four modes:
Legacy mode. Entities sorted lexicographically (A↔B == B↔A). No definition scope.
Multi-dimensional. Same entity pair, different definition scopes (F1).
Asymmetric. Order preserved: A→B ≠ B→A (F2).
Asymmetric without definition scope.
Keys serialize as pipe-separated strings for save/load:"entityA|entityB","entityA|entityB|defId", or"entityA|entityB|defId|D" for directional. Old format keys are automatically recognized for backward compatibility.