Files
zeling_v2/Docs/Architecture/19_DifficultyModule.md
2026-05-08 11:04:00 +08:00

329 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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; // SteelSoulHP 归零直接清档
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
{
/// <summary>
/// 全局难度管理器,挂在 Persistent 场景 [GameManagers] 下。
/// 持有当前难度 ScalerSO提供静态访问入口广播难度变更事件。
/// DefaultExecutionOrder(-900):确保在 PlayerStats(-800)/EnemyStats(-800) 的
/// Awake 之前完成初始化,使它们能在 Start 时通过 DifficultyManager.Instance.CurrentScaler
/// 读取到正确的难度系数(无需等待 EVT_DifficultyChanged 广播)。
/// </summary>
[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;
// 默认初始化为 NormalSaveData 加载后由 GameManager.Start 调用 Apply(saveData.DifficultyLevel)
// 注意:因 DefaultExecutionOrder(-900) 早于 PlayerStats(-800)/EnemyStats(-800)
// 它们的 Awake 执行时 DifficultyManager.Instance 已就绪,可直接读取 CurrentScaler。
Apply(DifficultyLevel.Normal);
}
/// <summary>
/// 应用难度。新游戏开始/读档时由 GameManager 调用。
/// </summary>
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
}
/// <summary>
/// 游戏进行中切换难度(仅允许 Easy ↔ Normal ↔ Hard
/// </summary>
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
```