# 19 · 难度系统模块(Difficulty Module) > **命名空间** `BaseGames.Core` > **程序集** `BaseGames.Core`(`Assets/Scripts/Core/`,并入核心程序集) > **依赖** `BaseGames.Core.Events` · `BaseGames.Player`(PlayerStats)· `BaseGames.Enemies`(EnemyStats) > **Design 来源** [29_DifficultyModesGuide](../Design/29_DifficultyModesGuide.md) --- ## 目录 1. [模块职责](#1-模块职责) 2. [DifficultyLevel 枚举](#2-difficultylevel-枚举) 3. [DifficultyScalerSO](#3-difficultyscalerso) 4. [DifficultyManager](#4-difficultymanager) 5. [各系统集成钩子](#5-各系统集成钩子) 6. [钢铁之魂模式特殊规则](#6-钢铁之魂模式特殊规则) 7. [SaveData 集成](#7-savedata-集成) 8. [事件频道](#8-事件频道) 9. [资产路径](#9-资产路径) --- ## 1. 模块职责 ``` 难度系统职责: ├─ DifficultyLevel enum → 四档难度标识 ├─ DifficultyScalerSO → 各难度的数值缩放配置(四份资产,分别对应四档) ├─ DifficultyManager → 常驻 Persistent 场景,持有当前难度,广播变更 └─ 系统集成钩子 → PlayerStats / EnemyStats / ShopController 在 Initialize 时注入缩放系数 ``` **零耦合原则**:各系统**不持有** `DifficultyManager` 引用,只在初始化时读取对应难度的 `DifficultyScalerSO`,或订阅 `EVT_DifficultyChanged` 事件频道动态更新。 --- ## 2. DifficultyLevel 枚举 ```csharp namespace BaseGames.Core { public enum DifficultyLevel { Easy = 0, // 协助模式 Normal = 1, // 标准模式(默认) Hard = 2, // 穿刺模式 SteelSoul = 3, // 钢铁之魂(一命,选择后不可降级) } } ``` --- ## 3. DifficultyScalerSO ```csharp namespace BaseGames.Core { [CreateAssetMenu(menuName = "Core/DifficultyScaler")] public class DifficultyScalerSO : ScriptableObject { [Header("标识")] public DifficultyLevel level; [Header("玩家属性缩放")] [Range(0.1f, 3.0f)] public float PlayerMaxHPMultiplier = 1.0f; // 最大 HP 倍率 [Range(0.1f, 3.0f)] public float PlayerDamageMultiplier = 1.0f; // 玩家造成的伤害倍率 [Range(0.0f, 2.0f)] public float InvincibilityFrameScale = 1.0f; // 无敌帧时长倍率 [Header("敌人属性缩放")] [Range(0.1f, 3.0f)] public float EnemyDamageMultiplier = 1.0f; // 敌人造成的伤害倍率 [Range(0.1f, 3.0f)] public float EnemyHPMultiplier = 1.0f; // 敌人 HP 倍率 [Range(0.1f, 3.0f)] public float BossDamageMultiplier = 1.0f; // Boss 伤害单独控制 [Range(0.1f, 3.0f)] public float BossHPMultiplier = 1.0f; [Header("商店价格")] [Range(0.5f, 2.0f)] public float ShopPriceMultiplier = 1.0f; // 商品价格倍率(Easy 可折扣) [Header("游戏规则")] public bool CanReviveWithGeoLoss = true; // 死亡时 Geo 掉落至遗骸 public bool InstantDeathOnZeroHP = false; // SteelSoul:HP 归零直接清档 public bool GeoPenaltyOnDeath = true; // false = Easy 无 Geo 损失 [Header("AI 行为(Behavior Designer 黑板变量名)")] public float EnemyAttackIntervalScale = 1.0f; // 攻击间隔倍率(Hard < 1 = 更频繁) public float EnemyAggroRangeScale = 1.0f; // 感知范围倍率 [Range(0.3f, 2.0f)] public float EnemyReactionTimeScale = 1.0f; // 反应时间倍率(>1 = 更慢 = 更简单) [Range(0, 5)] public int EnemyAggressionLevel = 2; // 0=被动 … 5=全力出击(影响 BT 决策权重) [Header("掉落与奖励")] [Range(0.0f, 3.0f)] public float GeoDropMultiplier = 1.0f; // Geo 掉落量倍率(Easy 可给更多) } } ``` **四档预设资产**(`Assets/ScriptableObjects/Core/Difficulty/`): | 资产 | 玩家HP | 玩家伤害 | 敌人伤害 | 敌人HP | 反应时间 | 侵略等级 | Geo倍率 | 规则 | |------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|------| | `Difficulty_Easy.asset` | ×1.5 | ×1.0 | ×0.7 | ×0.9 | ×1.4 | 1 | ×1.2 | 无 Geo 损失 | | `Difficulty_Normal.asset` | ×1.0 | ×1.0 | ×1.0 | ×1.0 | ×1.0 | 2 | ×1.0 | 标准 | | `Difficulty_Hard.asset` | ×0.75 | ×1.0 | ×1.3 | ×1.2 | ×0.7 | 3 | ×1.0 | 攻击间隔 ×0.8 | | `Difficulty_SteelSoul.asset` | ×1.2 | ×1.0 | ×1.5 | ×1.5 | ×0.6 | 4 | ×1.0 | `InstantDeathOnZeroHP=true` | --- ## 4. DifficultyManager ```csharp namespace BaseGames.Core { /// /// 全局难度管理器,挂在 Persistent 场景 [GameManagers] 下。 /// 持有当前难度 ScalerSO,提供静态访问入口,广播难度变更事件。 /// DefaultExecutionOrder(-900):确保在 PlayerStats(-800)/EnemyStats(-800) 的 /// Awake 之前完成初始化,使它们能在 Start 时通过 DifficultyManager.Instance.CurrentScaler /// 读取到正确的难度系数(无需等待 EVT_DifficultyChanged 广播)。 /// [DefaultExecutionOrder(-900)] public class DifficultyManager : MonoBehaviour { // ── Inspector ──────────────────────────────────────── [SerializeField] DifficultyScalerSO[] _allScalers; // 4 档资产 [SerializeField] DifficultyChangedEventChannel _onDifficultyChanged; // ── Singleton ──────────────────────────────────────── public static DifficultyManager Instance { get; private set; } // ── Runtime State ──────────────────────────────────── public DifficultyLevel CurrentLevel { get; private set; } = DifficultyLevel.Normal; public DifficultyScalerSO CurrentScaler { get; private set; } void Awake() { Instance = this; // 默认初始化为 Normal;SaveData 加载后由 GameManager.Start 调用 Apply(saveData.DifficultyLevel) // 注意:因 DefaultExecutionOrder(-900) 早于 PlayerStats(-800)/EnemyStats(-800), // 它们的 Awake 执行时 DifficultyManager.Instance 已就绪,可直接读取 CurrentScaler。 Apply(DifficultyLevel.Normal); } /// /// 应用难度。新游戏开始/读档时由 GameManager 调用。 /// public void Apply(DifficultyLevel level) { // SteelSoul 不可降级 if (CurrentLevel == DifficultyLevel.SteelSoul && level != DifficultyLevel.SteelSoul) { Debug.LogWarning("[DifficultyManager] SteelSoul 模式不可降级"); return; } CurrentLevel = level; CurrentScaler = GetScaler(level); _onDifficultyChanged.Raise(CurrentScaler); } public DifficultyScalerSO GetScaler(DifficultyLevel level) { foreach (var s in _allScalers) if (s.level == level) return s; Debug.LogError($"[DifficultyManager] 找不到 {level} 的 ScalerSO"); return _allScalers[0]; // fallback } /// /// 游戏进行中切换难度(仅允许 Easy ↔ Normal ↔ Hard)。 /// public void ChangeDifficulty(DifficultyLevel newLevel) { if (newLevel == DifficultyLevel.SteelSoul) { Debug.LogWarning("[DifficultyManager] 游戏进行中不可切换到 SteelSoul"); return; } Apply(newLevel); } } } ``` --- ## 5. 各系统集成钩子 ### PlayerStats > ⚠️ `PlayerStats` **无** `Initialize(PlayerStatsSO, DifficultyScalerSO)` 方法。 > `PlayerStatsSO _config` 通过 Inspector `[SerializeField]` 注入(见 `05_PlayerModule §4`)。 > 难度集成**纯事件驱动**:订阅 `_onDifficultyChanged` 频道,在回调中按比例调整 HP 等属性。 ```csharp // PlayerStats.cs(见 05_PlayerModule §4): // _config 为 Inspector 注入的 PlayerStatsSO,无 Initialize 方法 [SerializeField] DifficultyChangedEventChannel _onDifficultyChanged; void OnEnable() => _onDifficultyChanged.OnEventRaised += OnDifficultyChanged; void OnDisable() => _onDifficultyChanged.OnEventRaised -= OnDifficultyChanged; void OnDifficultyChanged(DifficultyScalerSO scaler) { // 按比例调整当前 HP float hpRatio = (float)CurrentHP / MaxHP; MaxHP = Mathf.RoundToInt(_config.BaseMaxHP * scaler.PlayerMaxHPMultiplier); CurrentHP = Mathf.RoundToInt(MaxHP * hpRatio); _damageMultiplier = scaler.PlayerDamageMultiplier; _iFrameScale = scaler.InvincibilityFrameScale; } ``` ### EnemyStats > ⚠️ `EnemyStats.Initialize` 签名为 `Initialize(EnemyStatsSO so)`(仅 1 个参数,见 `07_EnemyModule §2`)。 > 难度缩放通过订阅 `_onDifficultyChanged` 事件频道动态应用,**不通过 Initialize 注入 scaler**。 ```csharp // EnemyStats.cs(见 07_EnemyModule §2): // Initialize 签名:public void Initialize(EnemyStatsSO so); [SerializeField] DifficultyChangedEventChannel _onDifficultyChanged; void OnEnable() => _onDifficultyChanged.OnEventRaised += OnDifficultyChanged; void OnDisable() => _onDifficultyChanged.OnEventRaised -= OnDifficultyChanged; void OnDifficultyChanged(DifficultyScalerSO scaler) { MaxHP = Mathf.RoundToInt(_config.BaseHP * scaler.EnemyHPMultiplier); CurrentHP = Mathf.Min(CurrentHP, MaxHP); // AI 黑板变量(AttackIntervalScale) _behaviorTree.SetVariableValue("AttackIntervalScale", scaler.EnemyAttackIntervalScale); } ``` ### ShopController ```csharp // ShopController.GetPrice() 中: public int GetPrice(ShopItemSO item) { var scaler = DifficultyManager.Instance.CurrentScaler; return Mathf.RoundToInt(item.BasePrice * scaler.ShopPriceMultiplier); } ``` --- ## 6. 钢铁之魂模式特殊规则 | 规则 | 实现位置 | |------|---------| | HP 归零立即清档(删除存档文件) | `GameManager.HandlePlayerDeath()` 检查 `DifficultyScalerSO.InstantDeathOnZeroHP` | | 死亡界面显示"钢铁之魂终结"专属 UI | `DeathScreen` 读取 `DifficultyManager.CurrentLevel` 选择显示内容 | | 存档 UI 显示钢铁之魂徽章 | `SaveSlotUI` 读取 `SaveData.DifficultyLevel` | | 不可降级 | `DifficultyManager.Apply()` 中强制校验 | **SteelSoul 死亡流程(GameManager)**: ```csharp // GameManager.HandlePlayerDeath()(伪码) if (DifficultyManager.Instance.CurrentScaler.InstantDeathOnZeroHP) { // 1. 黑屏淡出(CameraStateController 广播黑屏事件) _onPlayerDied.Raise(); // 2. 等待动画结束(UniTask) await UniTask.Delay(TimeSpan.FromSeconds(2f), cancellationToken: _cts.Token); // 3. 删除存档文件 SaveManager.Instance.DeleteSave(SaveManager.Instance.CurrentSlotIndex); // 4. 返回主菜单(SceneLoader) SceneLoader.Instance.LoadScene("MainMenu"); } else { // 普通死亡:显示死亡 UI,等待复活 _onPlayerDied.Raise(); } ``` --- ## 7. SaveData 集成 `SaveData.DifficultyLevel`(`int`,存枚举原始值)在 `GameManager.LoadGame` 后调用: ```csharp DifficultyManager.Instance.Apply((DifficultyLevel)saveData.DifficultyLevel); ``` 新游戏开始时(角色创建界面选择难度后)同样调用 `Apply()`。 --- ## 8. 事件频道 | 频道 SO | Payload | 发布者 | 订阅者 | |--------|---------|--------|--------| | `EVT_DifficultyChanged` | `DifficultyScalerSO` | `DifficultyManager` | `PlayerStats`、`EnemyStats`(动态调整)、`HUDController`(刷新难度标识) | --- ## 9. 资产路径 ``` Assets/ScriptableObjects/Core/ └── Difficulty/ ├── Difficulty_Easy.asset ├── Difficulty_Normal.asset ├── Difficulty_Hard.asset └── Difficulty_SteelSoul.asset ```