# 39 · 挑战房间与 Boss Rush 系统(Challenge Room System) > **命名空间** `BaseGames.Challenge` > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `BaseGames.Core.Events` · `BaseGames.World`(SceneLoader、SaveManager)· `BaseGames.Boss`(BossBase)· `BaseGames.Player`(PlayerStats)· `BaseGames.UI`(HUD) --- ## 目录 1. [系统总览](#1-系统总览) 2. [ChallengeRoomSO — 挑战定义](#2-challengeroomso--挑战定义) 3. [BossRushSequenceSO — Boss Rush 序列](#3-bossrushsequenceso--boss-rush-序列) 4. [ChallengeRoomManager — 核心管理器](#4-challengeroommanager--核心管理器) 5. [挑战类型详解](#5-挑战类型详解) 6. [奖励系统](#6-奖励系统) 7. [排行榜(本地)](#7-排行榜本地) 8. [SaveData 集成](#8-savedata-集成) 9. [事件频道](#9-事件频道) 10. [HUD 扩展](#10-hud-扩展) 11. [编辑器友好设计](#11-编辑器友好设计) 12. [ChallengeRoomTrigger — 场景触发器](#12-challengeroomtrigger--场景触发器) 13. [程序集依赖](#13-程序集依赖) --- ## 1. 系统总览 挑战房间是独立于主线的**高难度可重复内容**:封闭区域内完成限定条件(时限/无伤/连击数),获取专属奖励。Boss Rush 是挑战房间的特殊变体,允许玩家按原始顺序重新挑战所有已击败 Boss。 ``` 挑战房间系统职责: ├─ ChallengeRoomSO → 挑战数据(敌人波次/时限/奖励/解锁条件) ├─ BossRushSequenceSO → Boss Rush 专属数据(Boss 序列/难度系数) ├─ ChallengeRoomManager → 运行时管理挑战流程(进入/波次推进/成功/失败) ├─ ChallengeEncounterSO → 单波敌人配置(生成点/敌人类型/数量) ├─ ChallengeLeaderboard → 本地最佳成绩记录(时间/无伤/连击) └─ ChallengeHUD → 计时器 / 波次进度 / 连击数屏幕显示 ``` **核心规则**: - 进入挑战房间时**自动存档当前位置和状态**,退出/失败后原地读档 - 挑战期间**禁止手动存档**(存档点不可用) - 挑战期间**死亡 = 挑战失败**(不触发正常死亡流程) - Boss Rush 特殊规则:每场 Boss 战之间恢复 30% HP,不恢复 Soul --- ## 2. ChallengeRoomSO — 挑战定义 ```csharp [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 — 单波次配置 ```csharp /// 单波次敌人配置(ChallengeRoomSO.encounters[] 的元素) [CreateAssetMenu(menuName = "Challenge/Encounter")] public class ChallengeEncounterSO : ScriptableObject { [Header("敌人生成列表")] public ChallengeSpawnInfo[] spawnInfos; [Header("波次延迟(秒)")] [Tooltip("上一波全歼后等待该时间再生成本波,0 = 立即生成")] [Range(0f, 5f)] public float waveDelay = 0.5f; /// 本波总敌人数(Editor & Runtime 均可访问) 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 序列 ```csharp [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 — 核心管理器 ```csharp 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 流程 ```csharp 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),不同评级给予不同重复奖励 - 排行榜记录最佳时间 ```csharp // 评级计算 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. 奖励系统 ```csharp [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. 排行榜(本地) ```csharp public class ChallengeLeaderboard : MonoBehaviour { // 每个挑战的最佳记录(本地,不联网) readonly Dictionary _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 中排行榜字段 ```json "leaderboard": { "Challenge_Forest_Elite": { "time": 87.4, "hits": 0, "maxCombo": 34, "grade": "S", "timestamp": "2025-06-01T12:00:00" } } ``` --- ## 8. SaveData 集成 ```json "challenges": { "cleared": ["Challenge_Forest_Elite", "Challenge_BossRush"], "inProgress": null } ``` ```csharp // 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(_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`(计时器刷新)| ```csharp [CreateAssetMenu(menuName = "Events/ChallengeEventChannel")] public class ChallengeEventChannelSO : ScriptableObject { public event Action 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(零代码) 1. `Create → Challenge/ChallengeRoom` → 填写 ID、类型、奖励 2. 为每波创建 `Create → Challenge/Encounter`,拖入敌人预制件和生成点 3. 在场景中放置 `ChallengeRoomTrigger`,引用该 ChallengeRoomSO 4. 配置 `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` 是放置在挑战房间入口处的碰撞触发器,负责在玩家进入时启动挑战流程。 ```csharp namespace BaseGames.Challenge { /// /// 挂载于挑战房间入口碰撞体(Trigger)。 /// 玩家进入后调用 ChallengeRoomManager.BeginChallenge()。 /// [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().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(); 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 定义 ```csharp [CreateAssetMenu(menuName = "Events/BossEventChannel")] public class BossEventChannelSO : ScriptableObject { public event Action OnEventRaised; public void Raise(BossBase boss) => OnEventRaised?.Invoke(boss); } ``` > **BossBase 接入点**:`BossBase.OnEnable()` 中调用 `_onBossActivated.Raise(this)`, > 使 `ChallengeRoomManager` 在 Boss Rush 中可零耦合获取 Boss 引用。