406 lines
20 KiB
Markdown
406 lines
20 KiB
Markdown
# 夜裔 (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<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
|
||
|
||
```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<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
|
||
|
||
```csharp
|
||
// 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
|
||
|
||
```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 即可,复杂度不足以拆分
|