Files
zeling_v2/Docs/Design/29_DifficultyModesGuide.md
2026-05-08 11:04:00 +08:00

376 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# 29 · 难度模式指南
> **命名空间** `BaseGames.Core`
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
> **依赖** `BaseGames.Core.Events` · `BaseGames.Player`PlayerStats· `BaseGames.Enemies`EnemyStats
---
## 目录
1. [系统总览](#1-系统总览)
2. [难度等级定义](#2-难度等级定义)
3. [DifficultyScalerSO — 难度缩放配置](#3-difficultyscalerso--难度缩放配置)
4. [DifficultyManager — 运行时管理](#4-difficultymanager--运行时管理)
5. [钢铁之魂模式特殊规则](#5-钢铁之魂模式特殊规则)
6. [敌人 AI 行为差异](#6-敌人-ai-行为差异)
7. [与各系统的集成](#7-与各系统的集成)
8. [SaveData 集成](#8-savedata-集成)
9. [UI 选择界面](#9-ui-选择界面)
10. [编辑器友好设计](#10-编辑器友好设计)
---
## 1. 系统总览
```
难度模式职责:
├─ DifficultyLevel 枚举 → Easy / Normal / Hard / SteelSoul
├─ DifficultyScalerSO → 各难度的数值缩放配置
├─ DifficultyManager → 全局难度状态,应用缩放到各系统
└─ 系统集成 → PlayerStats / EnemyStats / ShopController 读取缩放系数
```
**零耦合**:各系统**不持有** `DifficultyManager` 引用,而是在初始化时读取 `DifficultyScalerSO` 中对应难度的缩放系数并缓存,或监听 `OnDifficultyChanged` 事件频道动态更新。
---
## 2. 难度等级定义
| 等级 | 名称 | 目标玩家 | 核心差异 |
|------|------|---------|---------|
| `Easy` | **协助模式** | 休闲玩家 / 无障碍 | 玩家 HP × 1.5,敌人伤害 × 0.7,死亡无 Geo 损失 |
| `Normal` | **标准模式** | 大多数玩家(默认)| 基准数值,死亡掉落 Geo 至遗骸 |
| `Hard` | **穿刺模式** | 挑战玩家 | 玩家 HP × 0.75,敌人伤害 × 1.3,敌人 AI 更激进 |
| `SteelSoul` | **钢铁之魂** | 极限玩家 | 仅一命,死亡即清档;钢铁之魂专属 UI 标识 |
> 难度在**新游戏开始时**选择,游戏进行中可在 Easy / Normal / Hard 之间切换(`SteelSoul` 一旦选择不可降级)。
---
## 3. DifficultyScalerSO — 难度缩放配置
```csharp
[CreateAssetMenu(menuName = "Core/DifficultyScaler")]
public class DifficultyScalerSO : ScriptableObject
{
[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, 1.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("游戏规则")]
public bool CanReviveWithGeoLoss = true; // 死亡时 Geo 掉落至遗骸
public bool InstantDeathOnZeroHP = false; // 钢铁之魂HP 归零直接清档
public bool GeoPenaltyOnDeath = true; // false = Easy 无 Geo 损失
[Header("AI 行为(用于 BehaviorDesigner 黑板变量)")]
[Range(0.1f, 2.0f)]
public float EnemyReactionTimeScale = 1.0f; // 1.0 = 正常,< 1 = 更快反应
[Range(0f, 1f)]
public float EnemyAggressionLevel = 0.5f; // 0 = 最保守1 = 最激进
[Header("经济")]
[Range(0.5f, 2.0f)]
public float ShopPriceMultiplier = 1.0f; // 商店价格倍率
[Range(0.5f, 2.0f)]
public float GeoDropMultiplier = 1.0f; // 击杀敌人 Geo 掉落倍率
}
```
**资产存放路径**`Assets/ScriptableObjects/Config/Difficulty/`
### 四种难度预设值
| 参数 | Easy | Normal | Hard | SteelSoul |
|------|------|--------|------|-----------|
| `PlayerMaxHPMultiplier` | 1.5 | 1.0 | 0.75 | 1.0 |
| `PlayerDamageMultiplier` | 1.0 | 1.0 | 1.0 | 1.0 |
| `InvincibilityFrameScale` | 1.3 | 1.0 | 0.8 | 1.0 |
| `EnemyDamageMultiplier` | 0.7 | 1.0 | 1.3 | 1.5 |
| `EnemyHPMultiplier` | 0.9 | 1.0 | 1.15 | 1.2 |
| `BossDamageMultiplier` | 0.6 | 1.0 | 1.4 | 1.6 |
| `BossHPMultiplier` | 0.8 | 1.0 | 1.2 | 1.3 |
| `GeoPenaltyOnDeath` | false | true | true | true |
| `InstantDeathOnZeroHP` | false | false | false | **true** |
| `EnemyReactionTimeScale` | 1.4 | 1.0 | 0.7 | 0.6 |
| `EnemyAggressionLevel` | 0.3 | 0.5 | 0.75 | 0.9 |
| `ShopPriceMultiplier` | 0.9 | 1.0 | 1.1 | 1.0 |
| `GeoDropMultiplier` | 1.0 | 1.0 | 1.2 | 1.5 |
---
## 4. DifficultyManager — 运行时管理
```csharp
namespace BaseGames.Core
{
public class DifficultyManager : MonoBehaviour
{
// ── 配置 ─────────────────────────────────────────────────
[SerializeField] DifficultyScalerSO _easySO;
[SerializeField] DifficultyScalerSO _normalSO;
[SerializeField] DifficultyScalerSO _hardSO;
[SerializeField] DifficultyScalerSO _steelSoulSO;
[SerializeField] StringEventChannelSO _onDifficultyChanged; // payload = 枚举名
// ── 当前难度 ──────────────────────────────────────────────
public DifficultyLevel CurrentLevel { get; private set; } = DifficultyLevel.Normal;
public DifficultyScalerSO CurrentScaler { get; private set; }
void Awake() => ApplyDifficulty(DifficultyLevel.Normal);
/// <summary>切换难度SteelSoul 不可降级)</summary>
public bool TrySetDifficulty(DifficultyLevel level)
{
if (CurrentLevel == DifficultyLevel.SteelSoul &&
level != DifficultyLevel.SteelSoul)
return false; // 钢铁之魂不可降级
ApplyDifficulty(level);
return true;
}
void ApplyDifficulty(DifficultyLevel level)
{
CurrentLevel = level;
CurrentScaler = level switch
{
DifficultyLevel.Easy => _easySO,
DifficultyLevel.Hard => _hardSO,
DifficultyLevel.SteelSoul => _steelSoulSO,
_ => _normalSO,
};
_onDifficultyChanged.Raise(level.ToString());
}
}
public enum DifficultyLevel { Easy, Normal, Hard, SteelSoul }
}
```
### 系统集成方式
各系统在 `OnEnable` / 初始化时读取 `DifficultyManager.CurrentScaler`,并监听 `OnDifficultyChanged` 动态刷新:
```csharp
// 示例PlayerStats 集成难度缩放
void OnEnable()
{
_onDifficultyChanged.OnEventRaised += OnDifficultyChanged;
ApplyDifficultyScaling(_difficultyManager.CurrentScaler);
}
void ApplyDifficultyScaling(DifficultyScalerSO scaler)
{
MaxHP = Mathf.RoundToInt(_statsSO.BaseMaxHP * scaler.PlayerMaxHPMultiplier);
// 无敌帧缩放由 BeginInvincibility 方法内部乘以 scaler.InvincibilityFrameScale
}
```
---
## 5. 钢铁之魂模式特殊规则
### 死亡处理
```
HP 归零
PlayerStats.TakeDamage()
├─ Normal/Hard: 触发 OnPlayerDied → 读档 → 复活
└─ SteelSoul: 触发 OnSteelSoulDeath
GameManager 监听 OnSteelSoulDeath
├─ 播放专属死亡动画(黑屏 + 钢铁之魂 UI
├─ DeleteSaveSlot(slotIndex) // 清除存档
└─ LoadScene("Scene_MainMenu") // 返回主菜单
```
### 钢铁之魂 UI 标识
- HUD 右上角显示「◆ 钢铁之魂」专属图标(金色)
- 开始界面存档槽显示「◆」钢铁之魂标记
- 游戏中暂停菜单**隐藏**难度切换选项(一旦选择不可更改)
### 存档保护
```csharp
// SaveManager
void DeleteSaveSlot(int slotIndex)
{
string path = GetSavePath(slotIndex);
if (File.Exists(path))
{
// 先备份(可选,用于后期统计或调试)
File.Copy(path, path + ".dead", overwrite: true);
File.Delete(path);
}
}
```
---
## 6. 敌人 AI 行为差异
`DifficultyScalerSO.EnemyReactionTimeScale``EnemyAggressionLevel` 通过 **BehaviorDesigner 黑板变量** 注入敌人 AI
```csharp
// EnemyBase.Start() 或 OnDifficultyChanged 时注入
public void ApplyDifficultyToBehaviorTree(DifficultyScalerSO scaler)
{
var tree = GetComponent<BehaviorTree>();
if (tree == null) return;
tree.SetVariableValue("ReactionTimeScale", scaler.EnemyReactionTimeScale);
tree.SetVariableValue("AggressionLevel", scaler.EnemyAggressionLevel);
}
```
### AI 行为差异示例
| 行为 | Easy | Normal | Hard |
|------|------|--------|------|
| 攻击前摇时长 | × 1.4(更慢)| × 1.0 | × 0.7(更快)|
| Boss 攻击连段数 | 减少 1 段 | 正常 | 增加 1 段 |
| 弹射物速度 | × 0.8 | × 1.0 | × 1.2 |
| 追击范围 | × 0.8 | × 1.0 | × 1.3 |
| Boss 进入第 2 阶段 HP 阈值 | 35% | 50% | 60% |
---
## 7. 与各系统的集成
### PlayerStats
```csharp
// 受击时乘以难度伤害系数
int scaledDamage = Mathf.RoundToInt(info.Damage * _difficultyScaler.EnemyDamageMultiplier);
TakeDamage(scaledDamage);
```
### EnemyStatsEnemyStatsSO
```csharp
// EnemyBase.Awake() 中应用难度缩放
int scaledHP = Mathf.RoundToInt(_statsSO.BaseHP * _scaler.EnemyHPMultiplier);
int scaledDamage = Mathf.RoundToInt(_statsSO.BaseDamage * _scaler.EnemyDamageMultiplier);
CurrentHP = MaxHP = scaledHP;
_scaledAttackDamage = scaledDamage;
```
### ShopController
```csharp
public int GetActualPrice(ShopItemSO item)
{
float scale = _difficultyManager.CurrentScaler.ShopPriceMultiplier;
return Mathf.RoundToInt(item.basePrice * scale);
}
```
### CollectibleGeo 掉落)
```csharp
// Collectible.OnPickup() 中
int scaledGeo = Mathf.RoundToInt(_value * _difficultyManager.CurrentScaler.GeoDropMultiplier);
_stats.AddGeo(scaledGeo);
```
---
## 8. SaveData 集成
难度设置保存在 `SaveData` 中,确保读档后恢复正确难度:
```json
{
"difficulty": "Normal",
"isSteelSoul": false
}
```
```csharp
// DifficultyManager
public DifficultyLevel GetDifficultyFromSave(SaveData data)
{
if (data.IsSteelSoul) return DifficultyLevel.SteelSoul;
return Enum.Parse<DifficultyLevel>(data.Difficulty);
}
```
---
## 9. UI 选择界面
### 新游戏难度选择界面
玩家在主菜单点击「新游戏」后,进入难度选择界面(单独 Panel
```
┌─────────────────────────────────────────┐
│ 选择难度 │
│ │
│ ○ 协助模式 「从容体验完整故事」 │
│ ● 标准模式 「推荐,经典挑战」(默认)│
│ ○ 穿刺模式 「更高难度,更多奖励」 │
│ ○ 钢铁之魂 「仅一命,死亡即清档」 │
│ │
│ [开始游戏] │
└─────────────────────────────────────────┘
```
- `SteelSoul` 选项有警示图标和确认二次弹窗
- 所有选项均有简短描述说明(见上)
### 进行中难度切换Easy/Normal/Hard
**暂停菜单 → 设置 → 游戏** 中提供难度下拉框,`SteelSoul` 进行中隐藏此选项。
---
## 10. 编辑器友好设计
### DifficultyScalerSO 自定义 Inspector
```csharp
[CustomEditor(typeof(DifficultyScalerSO))]
public class DifficultyScalerSOEditor : Editor
{
public override VisualElement CreateInspectorGUI()
{
var root = new VisualElement();
InspectorElement.FillDefaultInspector(root, serializedObject, this);
// 数值摘要
var summary = new Foldout { text = "数值摘要" };
var scaler = (DifficultyScalerSO)target;
summary.Add(new Label($"玩家最大 HP: × {scaler.PlayerMaxHPMultiplier:F2}"));
summary.Add(new Label($"敌人伤害: × {scaler.EnemyDamageMultiplier:F2}"));
summary.Add(new Label($"Boss 伤害: × {scaler.BossDamageMultiplier:F2}"));
summary.Add(new Label($"敌人 AI 反应: × {scaler.EnemyReactionTimeScale:F2}(数越低越快)"));
summary.Add(new Label($"死亡规则: {(scaler.InstantDeathOnZeroHP ? " " : scaler.GeoPenaltyOnDeath ? " Geo" : "")}"));
root.Add(summary);
return root;
}
}
```