28 KiB
28 KiB
39 · 挑战房间与 Boss Rush 系统(Challenge Room System)
命名空间
BaseGames.Challenge
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Core.Events·BaseGames.World(SceneLoader、SaveManager)·BaseGames.Boss(BossBase)·BaseGames.Player(PlayerStats)·BaseGames.UI(HUD)
目录
- 系统总览
- ChallengeRoomSO — 挑战定义
- BossRushSequenceSO — Boss Rush 序列
- ChallengeRoomManager — 核心管理器
- 挑战类型详解
- 奖励系统
- 排行榜(本地)
- SaveData 集成
- 事件频道
- HUD 扩展
- 编辑器友好设计
- ChallengeRoomTrigger — 场景触发器
- 程序集依赖
1. 系统总览
挑战房间是独立于主线的高难度可重复内容:封闭区域内完成限定条件(时限/无伤/连击数),获取专属奖励。Boss Rush 是挑战房间的特殊变体,允许玩家按原始顺序重新挑战所有已击败 Boss。
挑战房间系统职责:
├─ ChallengeRoomSO → 挑战数据(敌人波次/时限/奖励/解锁条件)
├─ BossRushSequenceSO → Boss Rush 专属数据(Boss 序列/难度系数)
├─ ChallengeRoomManager → 运行时管理挑战流程(进入/波次推进/成功/失败)
├─ ChallengeEncounterSO → 单波敌人配置(生成点/敌人类型/数量)
├─ ChallengeLeaderboard → 本地最佳成绩记录(时间/无伤/连击)
└─ ChallengeHUD → 计时器 / 波次进度 / 连击数屏幕显示
核心规则:
- 进入挑战房间时自动存档当前位置和状态,退出/失败后原地读档
- 挑战期间禁止手动存档(存档点不可用)
- 挑战期间死亡 = 挑战失败(不触发正常死亡流程)
- Boss Rush 特殊规则:每场 Boss 战之间恢复 30% HP,不恢复 Soul
2. ChallengeRoomSO — 挑战定义
[CreateAssetMenu(menuName = "Challenge/ChallengeRoom")]
public class ChallengeRoomSO : ScriptableObject
{
[Header("标识")]
public string challengeId; // 唯一 ID,如 "Challenge_Forest_Elite"
public string displayName;
[TextArea(1, 4)]
public string description;
public ChallengeType challengeType; // Survival / TimeTrial / BossRush / NoHit
[Header("波次(非 BossRush 使用)")]
public ChallengeEncounterSO[] encounters; // 按顺序执行的敌人波次
[Header("Boss Rush 序列(BossRush 专用)")]
public BossRushSequenceSO bossRushSequence;
[Header("限制条件")]
public float timeLimit; // 秒,0 = 无时限
public bool requireNoHit; // 无伤挑战
public int minComboRequired; // 最低连击数(0 = 无要求)
[Header("奖励")]
public ChallengeRewardSO firstClearReward; // 首次完成奖励
public ChallengeRewardSO repeatedReward; // 再次完成奖励(时间/评级奖励)
[Header("解锁条件")]
public string[] prerequisiteBossIds; // 需击败的 Boss ID
public int minGeo; // 最低 Geo(可选门槛)
}
public enum ChallengeType
{
Survival, // 消灭所有波次敌人(可有时限)
TimeTrial, // 限时完成所有波次
BossRush, // 连续重战 Boss 序列
NoHit, // 无伤通关
Endless, // 无限波次,追求最高波次记录
}
2.1 ChallengeEncounterSO — 单波次配置
/// <summary>单波次敌人配置(ChallengeRoomSO.encounters[] 的元素)</summary>
[CreateAssetMenu(menuName = "Challenge/Encounter")]
public class ChallengeEncounterSO : ScriptableObject
{
[Header("敌人生成列表")]
public ChallengeSpawnInfo[] spawnInfos;
[Header("波次延迟(秒)")]
[Tooltip("上一波全歼后等待该时间再生成本波,0 = 立即生成")]
[Range(0f, 5f)]
public float waveDelay = 0.5f;
/// <summary>本波总敌人数(Editor & Runtime 均可访问)</summary>
public int TotalEnemyCount => spawnInfos.Sum(s => s.count);
}
[Serializable]
public class ChallengeSpawnInfo
{
public EnemyBase enemyPrefab; // 敌人预制件(含 EnemyBase 组件)
public Transform spawnPoint; // 引用场景 SpawnPoints/SP_XX 下的 Transform
[Min(1)]
public int count = 1; // 该类型生成数量
}
3. BossRushSequenceSO — Boss Rush 序列
[CreateAssetMenu(menuName = "Challenge/BossRushSequence")]
public class BossRushSequenceSO : ScriptableObject
{
[Header("Boss 序列")]
public BossRushEntry[] entries; // 按顺序排列的 Boss 战
[Header("难度系数")]
[Range(1f, 3f)]
public float difficultyMultiplier = 1.2f; // 基础属性倍率(Boss Rush 中 Boss 更强)
[Header("间歇期配置")]
public float interludeHPRestorePercent = 0.3f; // 每场 Boss 战后恢复 30% HP
public float interludeDuration = 5f; // 5 秒过渡动画时间(不可操作)
public bool restoreSoulBetweenBosses = false; // 是否恢复灵魂(通常否)
}
[Serializable]
public class BossRushEntry
{
public string bossId; // 对应主线击败的 Boss ID,用于检查是否已解锁
public string bossSceneName; // Boss 场景(Additive 加载,独立封闭区域)
public string bossName; // 显示名称
public Sprite bossPortrait; // 过渡画面肖像
}
4. ChallengeRoomManager — 核心管理器
namespace BaseGames.Challenge
{
public class ChallengeRoomManager : MonoBehaviour
{
[Header("事件频道—订阅")]
[SerializeField] VoidEventChannelSO _onPlayerDied;
[SerializeField] StringEventChannelSO _onEnemyDefeated;
[SerializeField] VoidEventChannelSO _onPlayerSaveRequested; // 拦截并阻止存档
[SerializeField] VoidEventChannelSO _onPlayerHurt; // 玩家受击:累计受击 & NoHit 判定
[SerializeField] IntEventChannelSO _onComboChanged; // 连击数变化:跟踪最高连击
[SerializeField] BossEventChannelSO _onBossActivated; // BossBase 激活通知(BossRush 专用)
[Header("事件频道—发布")]
[SerializeField] ChallengeEventChannelSO _onChallengeStarted;
[SerializeField] ChallengeEventChannelSO _onChallengeCompleted;
[SerializeField] ChallengeEventChannelSO _onChallengeFailed;
[SerializeField] IntEventChannelSO _onWaveChanged; // 当前波次编号
[SerializeField] FloatEventChannelSO _onTimerUpdated; // 计时器
// 运行时状态
ChallengeRoomSO _current;
ChallengeState _state;
int _currentWave;
int _enemiesRemainingInWave;
float _elapsedTime;
int _hitsTaken;
int _maxCombo;
bool _inChallenge;
// 进入挑战(由 ChallengeRoomTrigger 调用)
public void BeginChallenge(ChallengeRoomSO challenge)
{
if (_inChallenge) return;
_current = challenge;
_state = ChallengeState.Running;
_inChallenge = true;
_currentWave = 0;
_elapsedTime = 0f;
_hitsTaken = 0;
_maxCombo = 0;
// 存档当前状态(挑战失败时回档)
SaveManager.Instance.CreateChallengeCheckpoint();
// 封锁出口
LockExits(true);
_onChallengeStarted.Raise(challenge);
if (challenge.challengeType == ChallengeType.BossRush)
StartBossRush();
else
SpawnNextWave();
}
void Update()
{
if (!_inChallenge || _state != ChallengeState.Running) return;
_elapsedTime += Time.deltaTime;
_onTimerUpdated.Raise(_elapsedTime);
// 超时判定
if (_current.timeLimit > 0 && _elapsedTime >= _current.timeLimit)
FailChallenge(ChallengeFailReason.Timeout);
}
void HandleEnemyDefeated(string enemyId)
{
if (!_inChallenge) return;
_enemiesRemainingInWave--;
if (_enemiesRemainingInWave <= 0)
{
_currentWave++;
bool isLastWave = _currentWave >= _current.encounters.Length;
if (isLastWave)
CompleteChallenge();
else
SpawnNextWave();
}
}
void HandlePlayerDied()
{
if (!_inChallenge) return;
FailChallenge(ChallengeFailReason.Death);
}
void HandleSaveRequested()
{
if (_inChallenge)
{
// 静默拦截,阻止存档,可选弹出"挑战中无法存档"提示
UIManager.Instance.ShowNotification("挑战进行中,无法存档。");
}
}
void HandlePlayerHurt()
{
if (!_inChallenge) return;
_hitsTaken++;
// NoHit 类型:受击立即判定失败
if (_current.requireNoHit)
FailChallenge(ChallengeFailReason.ConditionNotMet);
}
void HandleComboChanged(int combo)
{
if (!_inChallenge) return;
if (combo > _maxCombo) _maxCombo = combo;
}
async void SpawnNextWave()
{
var encounter = _current.encounters[_currentWave];
_enemiesRemainingInWave = encounter.TotalEnemyCount;
// 波次延迟(关卡设计师可在 SO 中调整,0 = 立即生成)
if (encounter.waveDelay > 0)
await UniTask.Delay(TimeSpan.FromSeconds(encounter.waveDelay));
foreach (var spawnInfo in encounter.spawnInfos)
for (int i = 0; i < spawnInfo.count; i++)
Instantiate(spawnInfo.enemyPrefab, spawnInfo.spawnPoint.position, Quaternion.identity);
_onWaveChanged.Raise(_currentWave + 1);
}
void CompleteChallenge()
{
_state = ChallengeState.Completed;
// 无伤校验
bool noHitSuccess = !_current.requireNoHit || _hitsTaken == 0;
// 连击校验
bool comboSuccess = _current.minComboRequired == 0 || _maxCombo >= _current.minComboRequired;
if (!noHitSuccess || !comboSuccess)
{
FailChallenge(ChallengeFailReason.ConditionNotMet);
return;
}
// 记录成绩
ChallengeLeaderboard.Instance.Submit(
_current.challengeId, _elapsedTime, _hitsTaken, _maxCombo);
// 发放奖励
bool isFirstClear = !SaveManager.Instance.IsChallengeCleared(_current.challengeId);
var reward = isFirstClear ? _current.firstClearReward : _current.repeatedReward;
RewardHandler.Instance.GrantChallengeReward(reward, _elapsedTime);
if (isFirstClear)
SaveManager.Instance.SetChallengeCleared(_current.challengeId);
LockExits(false);
_inChallenge = false;
_onChallengeCompleted.Raise(_current);
}
void FailChallenge(ChallengeFailReason reason)
{
_state = ChallengeState.Failed;
_inChallenge = false;
LockExits(false);
_onChallengeFailed.Raise(_current);
// 回档至挑战检查点
SaveManager.Instance.LoadChallengeCheckpoint();
}
void StartBossRush() { /* Boss Rush 专属流程,见 §4.1 */ }
void LockExits(bool locked) { /* 激活/停用场景内所有 ChallengeExitDoor */ }
void OnEnable()
{
_onPlayerDied.OnEventRaised += HandlePlayerDied;
_onEnemyDefeated.OnEventRaised += HandleEnemyDefeated;
_onPlayerSaveRequested.OnEventRaised += HandleSaveRequested;
_onPlayerHurt.OnEventRaised += HandlePlayerHurt;
_onComboChanged.OnEventRaised += HandleComboChanged;
}
void OnDisable()
{
_onPlayerDied.OnEventRaised -= HandlePlayerDied;
_onEnemyDefeated.OnEventRaised -= HandleEnemyDefeated;
_onPlayerSaveRequested.OnEventRaised -= HandleSaveRequested;
_onPlayerHurt.OnEventRaised -= HandlePlayerHurt;
_onComboChanged.OnEventRaised -= HandleComboChanged;
}
}
public enum ChallengeState
{
Idle, Running, Completed, Failed,
}
public enum ChallengeFailReason
{
Death, Timeout, ConditionNotMet,
}
}
4.1 Boss Rush 流程
async void StartBossRush()
{
var seq = _current.bossRushSequence;
// 过滤出已击败(已解锁)的 Boss
var entries = seq.entries.Where(e => SaveManager.Instance.IsBossDefeated(e.bossId)).ToArray();
if (entries.Length == 0) { FailChallenge(ChallengeFailReason.ConditionNotMet); return; }
for (int i = 0; i < entries.Length; i++)
{
var entry = entries[i];
// 显示过渡画面(肖像 + 名字 + 倒计时)
await UIManager.Instance.ShowBossRushTransition(entry, i + 1, entries.Length);
// Additive 加载 Boss 场景
await SceneLoader.Instance.LoadSceneAdditiveAsync(entry.bossSceneName);
// 等待 BossBase 通过事件频道注册自身(零 FindObjectOfType)
// BossBase.OnEnable() 中应调用:_onBossActivated.Raise(this)
BossBase activeBoss = null;
void OnBossReg(BossBase b) => activeBoss = b;
_onBossActivated.OnEventRaised += OnBossReg;
await UniTask.WaitUntil(() => activeBoss != null || _state == ChallengeState.Failed);
_onBossActivated.OnEventRaised -= OnBossReg;
if (_state == ChallengeState.Failed) return;
// 将 Boss 属性乘以难度系数
activeBoss.ApplyDifficultyMultiplier(seq.difficultyMultiplier);
// 等待 Boss 被击败
bool bossDefeated = false;
void OnBossEnd() => bossDefeated = true;
activeBoss.OnDefeated += OnBossEnd;
await UniTask.WaitUntil(() => bossDefeated || _state == ChallengeState.Failed);
activeBoss.OnDefeated -= OnBossEnd;
if (_state == ChallengeState.Failed) return;
// 卸载 Boss 场景
await SceneLoader.Instance.UnloadSceneAsync(entry.bossSceneName);
// 间歇期恢复
if (i < entries.Length - 1)
{
PlayerStats.Instance.RestoreHPPercent(seq.interludeHPRestorePercent);
await UniTask.Delay(TimeSpan.FromSeconds(seq.interludeDuration));
}
}
CompleteChallenge();
}
5. 挑战类型详解
5.1 Survival — 消灭所有敌人
- 无时限(或宽松时限 = 奖励条件,不影响通关)
- 多波次,每波全歼后自动刷新下一波
- 通关条件:所有波次清空
5.2 TimeTrial — 时限挑战
- 严格时限:超时 = 失败
- 根据完成时间评级(S/A/B/C),不同评级给予不同重复奖励
- 排行榜记录最佳时间
// 评级计算
static ChallengeGrade EvaluateGrade(float time, ChallengeRoomSO room)
{
float ratio = time / room.timeLimit;
return ratio switch
{
<= 0.5f => ChallengeGrade.S,
<= 0.7f => ChallengeGrade.A,
<= 0.85f => ChallengeGrade.B,
_ => ChallengeGrade.C,
};
}
5.3 BossRush — Boss Rush
- 顺序重战所有已击败 Boss
- 每场 Boss 间恢复 30% HP,不恢复 Soul
- 完整通关(全 Boss)给予专属护符奖励
- 排行榜记录总用时
5.4 NoHit — 无伤挑战
- 受任何来自敌人的伤害 = 立即失败
- 通常是已有挑战房间的"无伤变体"(同场景,更严格条件,更好奖励)
5.5 Endless — 无限波次
- 波次无上限,随波次增加难度系数(敌人 HP/ATK × 1 + 波次 × 0.1)
- 排行榜记录最高到达波次
- 无通关概念,玩家死亡即结束
6. 奖励系统
[CreateAssetMenu(menuName = "Challenge/ChallengeReward")]
public class ChallengeRewardSO : ScriptableObject
{
[Header("固定奖励(首次通关)")]
public ItemSO[] exclusiveItems; // 专属护符/工具(不可从其他途径获得)
public int geoBonus;
[Header("分级奖励(重复挑战)")]
public GradeReward[] gradeRewards; // 不同评级对应不同 Geo 奖励
[Header("排行榜条件奖励(可选)")]
public int rankRewardGeo; // 连续 3 次破纪录时额外奖励
}
[Serializable]
public class GradeReward
{
public ChallengeGrade grade;
public int geo;
public string unlockedTitle; // 解锁标题(HUD 显示,如 "快手剑士")
}
public enum ChallengeGrade { S, A, B, C, F }
7. 排行榜(本地)
public class ChallengeLeaderboard : MonoBehaviour
{
// 每个挑战的最佳记录(本地,不联网)
readonly Dictionary<string, LeaderboardEntry> _records = new();
public void Submit(string challengeId, float time, int hits, int maxCombo)
{
if (!_records.TryGetValue(challengeId, out var best) || time < best.time)
{
_records[challengeId] = new LeaderboardEntry(challengeId, time, hits, maxCombo);
SaveManager.Instance.SetLeaderboardData(_records);
}
}
public LeaderboardEntry GetBest(string challengeId)
=> _records.TryGetValue(challengeId, out var e) ? e : null;
}
[Serializable]
public class LeaderboardEntry
{
public string challengeId;
public float time; // 最佳时间(秒)
public int hits; // 受击次数(无伤 = 0)
public int maxCombo; // 最高连击
public ChallengeGrade grade;
public string timestamp; // ISO 8601 日期字符串
}
SaveData 中排行榜字段
"leaderboard": {
"Challenge_Forest_Elite": {
"time": 87.4,
"hits": 0,
"maxCombo": 34,
"grade": "S",
"timestamp": "2025-06-01T12:00:00"
}
}
8. SaveData 集成
"challenges": {
"cleared": ["Challenge_Forest_Elite", "Challenge_BossRush"],
"inProgress": null
}
// SaveManager 扩展
public bool IsChallengeCleared(string id)
=> _saveData.challenges.cleared.Contains(id);
public void SetChallengeCleared(string id)
{
if (!_saveData.challenges.cleared.Contains(id))
{
_saveData.challenges.cleared.Add(id);
WriteDirty();
}
}
// 挑战检查点(失败时回档)
public void CreateChallengeCheckpoint()
{
_challengeCheckpoint = JsonConvert.SerializeObject(_saveData);
}
public void LoadChallengeCheckpoint()
{
if (_challengeCheckpoint == null) return;
_saveData = JsonConvert.DeserializeObject<SaveData>(_challengeCheckpoint);
_challengeCheckpoint = null;
ApplySaveDataToGame();
}
9. 事件频道
| 频道资产 | 类型 | 发布方 | 主要订阅方 |
|---|---|---|---|
OnChallengeStarted.asset |
ChallengeEventChannelSO |
ChallengeRoomManager |
ChallengeHUD(显示计时器)、AudioManager(播放挑战音乐) |
OnChallengeCompleted.asset |
ChallengeEventChannelSO |
ChallengeRoomManager |
ChallengeHUD(完成演出)、AchievementManager |
OnChallengeFailed.asset |
ChallengeEventChannelSO |
ChallengeRoomManager |
ChallengeHUD(失败提示)、AudioManager |
OnWaveChanged.asset |
IntEventChannelSO |
ChallengeRoomManager |
ChallengeHUD(波次显示) |
OnTimerUpdated.asset |
FloatEventChannelSO |
ChallengeRoomManager |
ChallengeHUD(计时器刷新) |
[CreateAssetMenu(menuName = "Events/ChallengeEventChannel")]
public class ChallengeEventChannelSO : ScriptableObject
{
public event Action<ChallengeRoomSO> OnEventRaised;
public void Raise(ChallengeRoomSO challenge) => OnEventRaised?.Invoke(challenge);
}
10. HUD 扩展
挑战 HUD 在进入挑战时由 ChallengeHUD 组件动态覆盖在常规 HUD 之上:
┌─ 挑战 HUD(锁定在屏幕上方中央)────────────────────────┐
│ ► 森林精英挑战 · 波次 2/4 · ⏱ 01:27.4 │
│ 连击:×23 │
└────────────────────────────────────────────────────────┘
Boss Rush 专用 HUD 显示 Boss 序列进度:
┌─ Boss Rush HUD ─────────────────────────────────────────┐
│ [✓小鬼将军] [►毒蜘蛛女王] [ 深渊守卫] [ 破晓术士] │
│ ⏱ 总用时:03:14.8 │
└────────────────────────────────────────────────────────┘
11. 编辑器友好设计
挑战房间场景搭建规范
场景结构(Hierarchy):
├─ ChallengeRoomManager ← 挂载 ChallengeRoomManager.cs
├─ ChallengeRoomTrigger ← 触发挑战的入口碰撞体
│ └─ TriggerZone.cs(进入时调用 BeginChallenge)
├─ ChallengeExitDoors ← 出口碰撞体组(挑战中锁定)
│ ├─ ExitDoor_Left
│ └─ ExitDoor_Right
├─ SpawnPoints ← 敌人生成点(由 Encounter 引用)
│ ├─ SP_01 ~ SP_08
└─ Environment ← 场景装饰(可与主场景共用 Tilemap)
新增挑战 SOP(零代码)
Create → Challenge/ChallengeRoom→ 填写 ID、类型、奖励- 为每波创建
Create → Challenge/Encounter,拖入敌人预制件和生成点 - 在场景中放置
ChallengeRoomTrigger,引用该 ChallengeRoomSO - 配置
ChallengeExitDoors(自动由 Manager 锁定)
ChallengeRoomManager Inspector(Play Mode)
┌─ ChallengeRoomManager ─────────────────────────────────┐
│ 挑战: Forest_Elite 状态: Running │
│ 波次: 2/4 剩余敌人: 3 用时: 01:27.4 │
│ 受击: 0 最高连击: 23 │
│ ─────────────────────────────────────────────────────│
│ [强制成功] [强制失败] [跳过当前波次] │
└────────────────────────────────────────────────────────┘
11.1 ChallengeEncounterSO Inspector
┌─ ChallengeEncounterSO ─────────────────────────────────┐
│ 波次延迟: 0.5 秒 │
│ 敌人列表: │
│ [0] 精英骑士 × 2 生成点: SP_01 │
│ [1] 弓箭手 × 3 生成点: SP_03 │
│ 总敌人数: 5(只读) │
└────────────────────────────────────────────────────────┘
12. ChallengeRoomTrigger — 场景触发器
ChallengeRoomTrigger 是放置在挑战房间入口处的碰撞触发器,负责在玩家进入时启动挑战流程。
namespace BaseGames.Challenge
{
/// <summary>
/// 挂载于挑战房间入口碰撞体(Trigger)。
/// 玩家进入后调用 ChallengeRoomManager.BeginChallenge()。
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class ChallengeRoomTrigger : MonoBehaviour
{
[SerializeField] ChallengeRoomSO _challenge; // 此触发器对应的挑战数据
[SerializeField] ChallengeRoomManager _manager; // 同场景的 Manager 引用
[Header("解锁门(可选)")]
[Tooltip("未达到解锁条件时显示的阻挡碰撞体,条件满足后自动禁用")]
[SerializeField] GameObject _lockBarrier;
void Start()
{
// 检查解锁条件,未解锁则显示阻挡门
bool unlocked = IsUnlocked();
if (_lockBarrier != null)
_lockBarrier.SetActive(!unlocked);
// 触发器本体也跟随解锁状态
GetComponent<Collider2D>().enabled = unlocked;
}
void OnTriggerEnter2D(Collider2D other)
{
if (!other.CompareTag("Player")) return;
_manager.BeginChallenge(_challenge);
}
bool IsUnlocked()
{
// Boss 前置条件
foreach (var bossId in _challenge.prerequisiteBossIds)
if (!SaveManager.Instance.IsBossDefeated(bossId)) return false;
// Geo 门槛
if (_challenge.minGeo > 0 &&
PlayerStats.Instance.CurrentGeo < _challenge.minGeo)
return false;
return true;
}
#if UNITY_EDITOR
void OnDrawGizmos()
{
// 在 Scene 视图中高亮触发区和解锁状态
Gizmos.color = IsUnlocked() ? new Color(0f, 1f, 0.5f, 0.25f)
: new Color(1f, 0.2f, 0.2f, 0.25f);
var col = GetComponent<Collider2D>();
if (col != null) Gizmos.DrawCube(col.bounds.center, col.bounds.size);
GizmosHelper.Label(transform.position + Vector3.up * 1.2f,
$"[挑战] {_challenge?.displayName ?? "未配置"}");
}
#endif
}
}
场景放置规范
ChallengeRoomTrigger 组件设置:
├─ _challenge → 拖入对应 ChallengeRoomSO 资产
├─ _manager → 拖入同场景 ChallengeRoomManager 对象
└─ _lockBarrier → 可选,拖入阻挡玩家的门碰撞体 GameObject
Collider2D 设置:
├─ Is Trigger: ✓
├─ Layer: Interaction(仅与 Player Layer 相交)
└─ 尺寸建议: 宽覆盖整个入口通道(防止贴墙穿越)
13. 程序集依赖
BaseGames.Challenge
├── BaseGames.Core.Events (SO 事件频道基类)
├── BaseGames.World (SceneLoader、SaveManager)
├── BaseGames.Boss (BossBase、BossEventChannelSO)
├── BaseGames.Player (PlayerStats、PlayerCombat — 受击/连击事件)
└── BaseGames.UI (UIManager、HUD 接口)
BaseGames.Challenge 对上述程序集只持有接口/事件频道引用,不直接 using 具体实现类型(除 BossBase 类型引用用于事件参数)。
关键事件频道资产清单
| 资产路径 | SO 类型 | 方向 |
|---|---|---|
Events/Challenge/OnChallengeStarted.asset |
ChallengeEventChannelSO |
Manager → HUD / Audio |
Events/Challenge/OnChallengeCompleted.asset |
ChallengeEventChannelSO |
Manager → HUD / Achievement |
Events/Challenge/OnChallengeFailed.asset |
ChallengeEventChannelSO |
Manager → HUD / Audio |
Events/Challenge/OnWaveChanged.asset |
IntEventChannelSO |
Manager → HUD |
Events/Challenge/OnTimerUpdated.asset |
FloatEventChannelSO |
Manager → HUD |
Events/Player/OnPlayerHurt.asset |
VoidEventChannelSO |
PlayerCombat → Manager |
Events/Player/OnComboChanged.asset |
IntEventChannelSO |
PlayerCombat → Manager |
Events/Boss/OnBossActivated.asset |
BossEventChannelSO |
BossBase → Manager |
BossEventChannelSO 定义
[CreateAssetMenu(menuName = "Events/BossEventChannel")]
public class BossEventChannelSO : ScriptableObject
{
public event Action<BossBase> OnEventRaised;
public void Raise(BossBase boss) => OnEventRaised?.Invoke(boss);
}
BossBase 接入点:
BossBase.OnEnable()中调用_onBossActivated.Raise(this), 使ChallengeRoomManager在 Boss Rush 中可零耦合获取 Boss 引用。