13 KiB
13 KiB
29 · 难度模式指南
命名空间
BaseGames.Core
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Core.Events·BaseGames.Player(PlayerStats)·BaseGames.Enemies(EnemyStats)
目录
- 系统总览
- 难度等级定义
- DifficultyScalerSO — 难度缩放配置
- DifficultyManager — 运行时管理
- 钢铁之魂模式特殊规则
- 敌人 AI 行为差异
- 与各系统的集成
- SaveData 集成
- UI 选择界面
- 编辑器友好设计
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 — 难度缩放配置
[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 — 运行时管理
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 动态刷新:
// 示例: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 右上角显示「◆ 钢铁之魂」专属图标(金色)
- 开始界面存档槽显示「◆」钢铁之魂标记
- 游戏中暂停菜单隐藏难度切换选项(一旦选择不可更改)
存档保护
// 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:
// 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
// 受击时乘以难度伤害系数
int scaledDamage = Mathf.RoundToInt(info.Damage * _difficultyScaler.EnemyDamageMultiplier);
TakeDamage(scaledDamage);
EnemyStats(EnemyStatsSO)
// EnemyBase.Awake() 中应用难度缩放
int scaledHP = Mathf.RoundToInt(_statsSO.BaseHP * _scaler.EnemyHPMultiplier);
int scaledDamage = Mathf.RoundToInt(_statsSO.BaseDamage * _scaler.EnemyDamageMultiplier);
CurrentHP = MaxHP = scaledHP;
_scaledAttackDamage = scaledDamage;
ShopController
public int GetActualPrice(ShopItemSO item)
{
float scale = _difficultyManager.CurrentScaler.ShopPriceMultiplier;
return Mathf.RoundToInt(item.basePrice * scale);
}
Collectible(Geo 掉落)
// Collectible.OnPickup() 中
int scaledGeo = Mathf.RoundToInt(_value * _difficultyManager.CurrentScaler.GeoDropMultiplier);
_stats.AddGeo(scaledGeo);
8. SaveData 集成
难度设置保存在 SaveData 中,确保读档后恢复正确难度:
{
"difficulty": "Normal",
"isSteelSoul": false
}
// 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
[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;
}
}