chore: initial commit
This commit is contained in:
328
Docs/Architecture/19_DifficultyModule.md
Normal file
328
Docs/Architecture/19_DifficultyModule.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# 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
|
||||
{
|
||||
/// <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;
|
||||
// 默认初始化为 Normal;SaveData 加载后由 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
|
||||
```
|
||||
Reference in New Issue
Block a user