夜裔 (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.
- L0 is pure — No
UnityEngine reference in any L0 source file. L0 compiles as a standalone .NET library.
- L0 doesn't know it's in Unity — L0 communicates only via C# primitives and events. It never calls up.
- L1 is thin — MonoBehaviour classes in L1 contain only Unity lifecycle wiring and event forwarding. No game logic.
- Events go up, methods go down — L0 emits events (L1/L2 subscribe). L1 calls methods on L0. Never the reverse.
- 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<T> |
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
// 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<float,float> 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<DamageResult> ResolveAttacks(List<AttackData> atks, List<EnemyState> 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<FormType,FormType> OnFormChanged;
public event Action<SwitchPhase> 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<EnemySpawnEntry> 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<WaveComposition> OnWaveStart;
public event Action<float> OnWaveBreak;
public event Action OnAllWavesComplete;
}
L1 Adapter Interfaces
// Input Adapter — sole owner of Unity Input System interaction
public class InputAdapter : MonoBehaviour {
public event Action OnAttackPressed, OnAttackReleased, OnDodgePressed, OnPausePressed;
public event Action<FormType> 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
// 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 即可,复杂度不足以拆分