Vampire-Act-Mono/docs/architecture/architecture.md

20 KiB
Raw Permalink Blame History

夜裔 (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

// 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 即可,复杂度不足以拆分