12 KiB
19 · 难度系统模块(Difficulty Module)
命名空间
BaseGames.Core
程序集BaseGames.Core(Assets/Scripts/Core/,并入核心程序集)
依赖BaseGames.Core.Events·BaseGames.Player(PlayerStats)·BaseGames.Enemies(EnemyStats)
Design 来源 29_DifficultyModesGuide
目录
- 模块职责
- DifficultyLevel 枚举
- DifficultyScalerSO
- DifficultyManager
- 各系统集成钩子
- 钢铁之魂模式特殊规则
- SaveData 集成
- 事件频道
- 资产路径
1. 模块职责
难度系统职责:
├─ DifficultyLevel enum → 四档难度标识
├─ DifficultyScalerSO → 各难度的数值缩放配置(四份资产,分别对应四档)
├─ DifficultyManager → 常驻 Persistent 场景,持有当前难度,广播变更
└─ 系统集成钩子 → PlayerStats / EnemyStats / ShopController
在 Initialize 时注入缩放系数
零耦合原则:各系统不持有 DifficultyManager 引用,只在初始化时读取对应难度的 DifficultyScalerSO,或订阅 EVT_DifficultyChanged 事件频道动态更新。
2. DifficultyLevel 枚举
namespace BaseGames.Core
{
public enum DifficultyLevel
{
Easy = 0, // 协助模式
Normal = 1, // 标准模式(默认)
Hard = 2, // 穿刺模式
SteelSoul = 3, // 钢铁之魂(一命,选择后不可降级)
}
}
3. DifficultyScalerSO
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
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 等属性。
// 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。
// 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
// 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):
// 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 后调用:
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