376 lines
13 KiB
Markdown
376 lines
13 KiB
Markdown
# 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);
|
||
```
|
||
|
||
### EnemyStats(EnemyStatsSO)
|
||
|
||
```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);
|
||
}
|
||
```
|
||
|
||
### Collectible(Geo 掉落)
|
||
|
||
```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;
|
||
}
|
||
}
|
||
```
|