# 夜裔 (Nightborn) — Master Architecture ## Document Status - **Version**: 1.0 - **Last Updated**: 2026-04-27 - **Engine**: Unity 2022.3.62f3c1 (URP 14.0.12) - **Language**: C# (.NET Standard 2.1) - **GDDs Covered**: game-concept.md, systems-index.md - **ADRs Referenced**: None yet — see Required ADRs section - **Review Mode**: Lean (no director sign-off) - **Architecture**: L0 Pure C# / L1 Unity Adapter / L2 Unity Presentation --- ## Engine Knowledge Gap Summary | Risk Level | Domains | Implication | |------------|---------|-------------| | **LOW** | All domains | Unity 2022.3 LTS is within LLM training data (cutoff May 2025). No engine knowledge gaps. | | **Note** | New Input System | Project uses Input System package (not legacy Input Manager), consistent with best practices | | **Note** | URP 14.x | Render pipeline is stable and well-documented | --- ## Architecture Principles These are non-negotiable technical rules derived from the game pillars and design concept. 1. **L0 is pure** — No `UnityEngine` reference in any L0 source file. L0 compiles as a standalone .NET library. 2. **L0 doesn't know it's in Unity** — L0 communicates only via C# primitives and events. It never calls up. 3. **L1 is thin** — MonoBehaviour classes in L1 contain only Unity lifecycle wiring and event forwarding. No game logic. 4. **Events go up, methods go down** — L0 emits events (L1/L2 subscribe). L1 calls methods on L0. Never the reverse. 5. **Data owned by L0, rendered by L1/L2** — All game state lives in L0. L1 and L2 are stateless regarding game rules. --- ## System Layer Map ``` ┌──────────────────────────────────────────────────────────┐ │ L2: Unity 表现层 │ │ ┌─────────────┐ ┌──────────────┐ │ │ │ UI/HUD │ │ Menu System │ │ │ │ Canvas │ │ Canvas │ │ │ └──────┬──────┘ └──────┬───────┘ │ │ │ │ │ │ └───────┬────────┘ │ │ │ 只读L0状态 │ ├─────────────────┼────────────────────────────────────────┤ │ L1: Unity 适配层 │ │ ┌──────────────┐ ┌──────────┐ ┌───────────┐ │ │ │Input Adapter │ │ Camera │ │Level │ │ │ │InputSystem │ │ Ctrl │ │Loader │ │ │ └──────┬───────┘ └────┬─────┘ └─────┬─────┘ │ │ ┌──────┴───────┐ ┌────┴─────┐ │ │ │ VFX Spawner │ │ Audio │ │ │ │ ParticleSys │ │ Player │ │ │ └──────┬───────┘ └────┬─────┘ │ │ │ │ │ │ └──────┬───────┘ │ │ │ 方法调用↓ 事件订阅↑ │ ├────────────────┼────────────────────────────────────────────┤ │ L0: 纯C# 核心逻辑层 (零Unity依赖) │ │ ┌──────────────┐ ┌──────────┐ ┌──────────────┐ │ │ │Blood Energy │ │ Combat │ │Form Switch │ │ │ │Economy │ │ Logic │ │State Machine │ │ │ └──────┬───────┘ └────┬─────┘ └──────┬───────┘ │ │ ┌──────┴───────┐ ┌────┴─────┐ ┌──────┴───────┐ │ │ │Enemy AI │ │Boss AI │ │Wave Manager │ │ │ │Logic │ │Logic │ │Logic │ │ │ └──────────────┘ └──────────┘ └──────────────┘ │ │ ┌──────────────┐ ┌──────────┐ ┌──────────────┐ │ │ │Death/Respawn │ │Skill Tree│ │Save/Load │ │ │ │Rules │ │Rules │ │Logic │ │ │ └──────────────┘ └──────────┘ └──────────────┘ │ │ ┌──────────────┐ │ │ │Score Calc │ │ │ └──────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### Communication Rules | Direction | Allowed | Mechanism | |-----------|---------|-----------| | L0 → L0 | ✓ | Direct method call, C# event | | L0 → L1 | ✗ | L0 does NOT call L1. L0 emits C# events; L1 subscribes. | | L0 → L2 | ✗ | L2 reads L0 state through L1 or direct POCO access | | L1 → L0 | ✓ | Method calls (e.g., `combat.Attack()`), property reads | | L1 → L2 | ✓ | Method calls (e.g., `hud.Refresh(state)`) | | L2 → L0 | ✓ | Read-only access to L0 public properties and state aggregates | --- ## Module Ownership ### L0 — Pure C# Logic Layer | Module | Owns | Exposes | Consumes | Unity Deps | |--------|------|---------|----------|-----------| | **Blood Energy Economy** | Current/max energy, gain/spend rules, decay formula | `float Current`, `float Max`, `void Add(float)`, `bool Spend(float)`, `event OnEnergyChanged` | — | Zero | | **Combat Logic** | Hit detection (geometry intersection), damage formula, crit rules | `DamageResult Attack(AttackData)`, `bool HitTest(Shape, Shape)` | — (uses own geometry lib) | Zero | | **Form Switch SM** | Current form, switch state machine, windup timer, switch conditions | `FormType CurrentForm`, `SwitchResult RequestSwitch(FormType)`, `AttackStyle GetAttackStyle(FormType)`, `event OnFormChanged` | Blood Energy (check/spend) | Zero | | **Enemy AI Logic** | Per-type behavior rules, target selection, attack decisions | `AICommand Update(EnemyState, PlayerState)` | Combat Logic (damage calc) | Zero | | **Boss AI Logic** | Phase logic, phase transition conditions, phase-specific behaviors | `BossCommand Update(BossState, PlayerState)`, `event OnPhaseChanged` | Enemy AI, Combat Logic | Zero | | **Wave Manager Logic** | Wave composition data, spawn timers, wave state | `WaveComposition GetNextWave()`, `bool IsWaveComplete()`, `event OnWaveStart`, `event OnWaveBreak` | Enemy AI (types) | Zero | | **Death/Respawn Rules** | Death trigger, checkpoint state, respawn logic | `bool IsDead(float)`, `Vector3 RespawnPoint`, `event OnDeath`, `event OnRespawn` | Combat Logic (health) | Zero | | **Skill Tree Rules** | Unlock conditions, upgrade formulas, form synergy effects | `bool CanUnlock(SkillNode)`, `void Unlock(SkillNode)` | Form Switch SM | Zero | | **Save/Load Logic** | Serialization format, file paths, version migration | `void Save(GameState)`, `GameState Load()`, `bool SaveExists(int)` | Skill Tree Rules | Zero | | **Score Calculator** | Score formula, combo tracking, per-wave stats | `int Score`, `float StyleRank`, `void OnKill(KillData)` | Combat Logic, Wave Manager | Zero | ### L1 — Unity Adapter Layer | Module | Owns | Exposes | Consumes | Key Unity APIs | |--------|------|---------|----------|---------------| | **Input Adapter** | Input Action bindings, action map config | `event OnAttack`, `event OnSwitch(FormType)`, `Vector2 MoveInput` | — | `InputAction`, `PlayerInput` | | **Camera Controller** | Camera component, follow logic, shake | `void SetTarget(Vector3)`, `void Shake(float)` | L0 player position (read) | `Camera`, `Transform` | | **Level Loader** | Scene refs, load progress | `void LoadLevel(string)`, `event OnLevelLoaded` | — | `SceneManager`, `AsyncOperation` | | **Audio Player** | AudioSource pool, clip mappings | `void Play(SfxType)`, `void SetMusic(MusicTrack)` | L0 events (subscribes) | `AudioSource`, `AudioMixer` | | **VFX Spawner** | ParticleSystem pool, geometric preset library | `void Spawn(VfxType, Vector3)`, `void SetFormColor(FormType)` | L0 events (subscribes) | `ParticleSystem`, `ObjectPool` | ### L2 — Unity Presentation Layer | Module | Owns | Exposes | Consumes | Key Unity APIs | |--------|------|---------|----------|---------------| | **UI/HUD** | Canvas, form indicator, blood energy bar, wave info | `void Refresh(HUDState)` | L0 state (read only) | `Canvas`, `UI.Image`, `TMP_Text` | | **Menu System** | Main menu, pause panel, skill tree UI, settings | `void ShowMainMenu()`, `void ShowPause()` | Input Adapter, Level Loader, Save/Load | `Canvas`, `UI.Button` | --- ## Data Flow ### Combat Frame Path ``` Unity Update (L1) → Input Adapter reads Input System → fires C# events → L1 calls L0: CombatSystem.Attack(), FormSwitchSM.Update() → L0 resolves: hit tests, damage, AI decisions, state machine ticks → L0 fires events: OnHit, OnFormChanged, OnEnergyChanged → L1 subscribers: VFX.Spawn(), Audio.Play() → L1 reads L0 state: Camera.SetTarget(), HUD.Refresh(state) → Unity renders to screen ``` ### Form Switch Event Chain ``` Input → OnSwitch(Wolf) event → FormSwitchSM.RequestSwitch(Wolf) → BloodEnergy.Spend(cost) → Windup timer starts → Windup completes → OnFormChanged(Human→Wolf) → VFX: switch particle burst → Audio: switch SFX → HUD: color update → Enemy AI: re-evaluate threat priority ``` ### Initialization Order ``` 1. Unity Awake: Level Loader → Input Adapter → Camera → Audio → VFX 2. L0 bootstrap (triggered by L1): Blood Energy → Combat Logic → Form Switch SM → Enemy AI → Wave Manager 3. L2 HUD subscribes to L0 events 4. Game start signal ``` --- ## API Boundaries ### Core L0 Interfaces ```csharp // Blood Energy Economy public class BloodEnergyEconomy { public float Current { get; } public float Max { get; } public float GainRate { get; set; } public void Add(float amount); public bool CanSpend(float amount); public bool Spend(float amount); public void Reset(); public event Action OnEnergyChanged; // Invariant: 0 ≤ Current ≤ Max } // Combat Logic public struct AttackData { public Vector3 Origin; public Shape HitShape; public float Damage; public float KnockbackForce; public FormType SourceForm; } public struct DamageResult { public int TargetId; public float FinalDamage; public bool IsCritical; public Vector3 HitPoint; public bool IsKillingBlow; } public class CombatLogic { public static bool TestHit(Shape a, Shape b); public DamageResult CalculateDamage(AttackData atk, EnemyDefenseData def); public List ResolveAttacks(List atks, List enemies); } // Form Switch State Machine public enum FormType { Human, Wolf, Mist } public enum SwitchPhase { Idle, Windup, Switching, Recovery } public enum SwitchResult { Success, InsufficientEnergy, OnCooldown, InvalidTarget } public struct AttackStyle { public float Range, BaseDamage, AttackSpeed, AreaSize; public Shape AttackShape; public int MaxTargets; } public class FormSwitchStateMachine { public FormType CurrentForm { get; } public SwitchPhase Phase { get; } public SwitchResult RequestSwitch(FormType target); public AttackStyle GetAttackStyle(FormType form); public void Update(float deltaTime); public event Action OnFormChanged; public event Action OnPhaseChanged; } // Enemy AI Logic public struct AICommand { public CommandType Type; // Move, Attack, Ability, Flee public Vector3 TargetPosition; public int TargetId, AbilityId; } public class EnemyAILogic { public AICommand ComputeAction(EnemyState self, PlayerState player); } // Wave Manager Logic public struct WaveComposition { public List Enemies; public float DelayBetweenGroups; public string[] SpecialConditions; } public class WaveManagerLogic { public WaveState CurrentState { get; } public int CurrentWave { get; } public WaveComposition GetNextWave(); public void OnEnemyKilled(int enemyId); public void Update(float deltaTime); public event Action OnWaveStart; public event Action OnWaveBreak; public event Action OnAllWavesComplete; } ``` ### L1 Adapter Interfaces ```csharp // Input Adapter — sole owner of Unity Input System interaction public class InputAdapter : MonoBehaviour { public event Action OnAttackPressed, OnAttackReleased, OnDodgePressed, OnPausePressed; public event Action OnSwitchPressed; public Vector2 MoveInput { get; } } // VFX Spawner — subscribes to L0 events, drives Unity ParticleSystem public class VFXSpawner : MonoBehaviour { public void PlayFormSwitchEffect(FormType from, FormType to, Vector3 pos); public void PlayHitEffect(Vector3 pos, float damage); public void PlayKillEffect(Vector3 pos); public void PlayBossPhaseEffect(Vector3 pos); } ``` ### L2 HUD Interface ```csharp // HUDState — pure data aggregate of L0 state for rendering public struct HUDState { public float BloodEnergyCurrent, BloodEnergyMax; public FormType CurrentForm; public float PlayerHealth; public int CurrentWave, TotalWaves; public float WaveProgress; public int StyleRank; public float BossHealth; } public class HUDManager : MonoBehaviour { public void Refresh(HUDState state); // called every frame by L1 } ``` --- ## ADR Audit | Status | Detail | |--------|--------| | Existing ADRs found | 0 | | Traceable TRs from GDDs | 0 (no per-system GDDs authored) | | Architecture decisions in this doc | All API boundaries, layer map, data flow, communication rules | No existing ADRs to audit. All architectural decisions in this document require formal ADR records. --- ## Required ADRs ### Must create before coding (Foundation) | ADR ID | Title | Covers | Priority | |--------|-------|--------|----------| | ADR-001 | Three-Layer Architecture & Code Isolation | L0/L1/L2 directory layout, asmdef configuration, cross-layer reference prohibition | BLOCKING | | ADR-002 | Event Bus & Communication Pattern | C# event as sole L0→L1 mechanism, subscription lifecycle, no direct L0→Unity calls | BLOCKING | | ADR-003 | Pure C# Geometry & Math Library | Custom Vector3, Shape hierarchy, Math utilities — zero UnityEngine dependency | BLOCKING | ### Should create before relevant system (Core) | ADR ID | Title | Covers | Priority | |--------|-------|--------|----------| | ADR-004 | Form Switch State Machine Design | State transitions, timing parameters, windup/invuln/recovery phases, interrupt behavior | HIGH | | ADR-005 | Enemy AI Decision Model | Behavior tree vs state machine vs utility AI — choice and rationale | HIGH | | ADR-006 | Wave Composition Data Format | WaveConfig structure, ScriptableObject vs JSON, editor tooling | MEDIUM | | ADR-007 | Serialization Format & Save Strategy | JSON vs binary, version migration policy, save slot management | MEDIUM | ### Can defer to implementation (Feature) | ADR ID | Title | Covers | Priority | |--------|-------|--------|----------| | ADR-008 | Skill Tree Data Structure | Node graph representation, unlock dependencies, synergy trigger conditions | LOW | | ADR-009 | Score & Style Formula | Kill weighting, combo decay, wave speed bonus, rank thresholds | LOW | --- ## Project Directory Structure ``` Assets/ ├── Scripts/ │ ├── Core/ # L0 — Pure C# (no UnityEngine) │ │ ├── Core.asmdef # asmdef: no Unity refs │ │ ├── Combat/ │ │ │ ├── BloodEnergyEconomy.cs │ │ │ ├── CombatLogic.cs │ │ │ ├── FormSwitchStateMachine.cs │ │ │ └── AttackStyle.cs │ │ ├── Enemy/ │ │ │ ├── EnemyAILogic.cs │ │ │ ├── BossAILogic.cs │ │ │ └── WaveManagerLogic.cs │ │ ├── Player/ │ │ │ └── DeathRespawnRules.cs │ │ ├── Progression/ │ │ │ └── SkillTreeRules.cs │ │ ├── Persistence/ │ │ │ └── SaveLoadLogic.cs │ │ ├── Meta/ │ │ │ └── ScoreCalculator.cs │ │ └── Geometry/ # ADR-003 — Pure C# math types │ │ ├── Vector3.cs │ │ ├── Shape.cs │ │ └── MathUtil.cs │ ├── Adapters/ # L1 — MonoBehaviour wrappers │ │ ├── Adapters.asmdef # asmdef: refs Core + Unity │ │ ├── InputAdapter.cs │ │ ├── CameraController.cs │ │ ├── LevelLoader.cs │ │ ├── AudioPlayer.cs │ │ └── VFXSpawner.cs │ └── Presentation/ # L2 — UI & Rendering │ ├── Presentation.asmdef # asmdef: refs Core + Adapters + Unity │ ├── HUDManager.cs │ ├── HUDState.cs │ └── MenuSystem.cs ├── Scenes/ # Unity场景文件 ├── Settings/ # URP质量配置 └── Tests/ ├── Core.Tests/ # L0 单元测试 (NUnit, 无Unity) │ ├── Core.Tests.asmdef │ ├── BloodEnergyEconomyTests.cs │ ├── CombatLogicTests.cs │ └── FormSwitchSMTests.cs └── Adapter.Tests/ # L1 集成测试 (Unity Test Framework) └── Adapter.Tests.asmdef ``` ### asmdef 配置规则 | 程序集 | 引用 | 禁止引用 | |--------|------|----------| | `Core.asmdef` | (none) | 不可引用任何 Unity 包 | | `Adapters.asmdef` | Core | 不可引用 Presentation | | `Presentation.asmdef` | Core, Adapters | — | | `Core.Tests.asmdef` | Core | 不可引用 Unity (NUnit standalone) | | `Adapter.Tests.asmdef` | Core, Adapters | — | --- ## Open Questions - 是否需要专用的事件总线类管理 L0 事件的订阅/取消,还是直接使用 C# `event` + `Action`?—— ADR-002 中决定 - 敌人的 WaveConfig 用 ScriptableObject (方便策划编辑) 还是纯 JSON (更便携)?—— ADR-006 中决定 - 是否需要支持中途存档(战斗中途保存并恢复)还是仅关卡间存档?—— 在 Save/Load GDD 中决定 - L0 的三个 asmdef 子模块(Combat / Enemy / Player)是否需要独立 asmdef 而非一个 Core.asmdef?—— 当前一个 Core.asmdef 即可,复杂度不足以拆分