Files
zeling_v2/Docs/Architecture/19_DifficultyModule.md
2026-05-08 11:04:00 +08:00

12 KiB
Raw Permalink Blame History

19 · 难度系统模块Difficulty Module

命名空间 BaseGames.Core
程序集 BaseGames.CoreAssets/Scripts/Core/,并入核心程序集)
依赖 BaseGames.Core.Events · BaseGames.PlayerPlayerStats· BaseGames.EnemiesEnemyStats
Design 来源 29_DifficultyModesGuide


目录

  1. 模块职责
  2. DifficultyLevel 枚举
  3. DifficultyScalerSO
  4. DifficultyManager
  5. 各系统集成钩子
  6. 钢铁之魂模式特殊规则
  7. SaveData 集成
  8. 事件频道
  9. 资产路径

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;  // SteelSoulHP 归零直接清档
        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;
            // 默认初始化为 NormalSaveData 加载后由 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.DifficultyLevelint,存枚举原始值)在 GameManager.LoadGame 后调用:

DifficultyManager.Instance.Apply((DifficultyLevel)saveData.DifficultyLevel);

新游戏开始时(角色创建界面选择难度后)同样调用 Apply()


8. 事件频道

频道 SO Payload 发布者 订阅者
EVT_DifficultyChanged DifficultyScalerSO DifficultyManager PlayerStatsEnemyStats(动态调整)、HUDController(刷新难度标识)

9. 资产路径

Assets/ScriptableObjects/Core/
└── Difficulty/
    ├── Difficulty_Easy.asset
    ├── Difficulty_Normal.asset
    ├── Difficulty_Hard.asset
    └── Difficulty_SteelSoul.asset