# 08 · 世界模块 > **命名空间** `BaseGames.World` > **程序集** `BaseGames.World` > **路径** `Assets/Scripts/World/` > **依赖** `BaseGames.Core`、`BaseGames.Core.Events`、`BaseGames.Core.Save` > (通过 `IRestoreOnSave`(定义于 Core)与玩家层解耦,无需直接引用 `BaseGames.Player`) --- ## 目录 1. [场景结构规范(总览)](#1-场景结构规范) 2. [RoomTransition](#2-roomtransition) 3. [SavePoint](#3-savepoint) 4. [HazardZone](#4-hazardzone) 5. [Collectible](#5-collectible) 6. [AbilityUnlock](#6-abilityunlock) 7. [IInteractable 接口](#7-iinteractable-接口) 8. [InteractableDetector](#8-interactabledetector) 9. [WorldStateRegistry](#9-worldstateregistry) 10. [DestructibleTile](#10-destructibletile) 11. [MovingPlatform](#11-movingplatform) 12. [DirectionalDestructible — 单向可破坏墙](#12-directionaldestructible--单向可破坏墙) 13. [DirectionalInteractable — 单向触发机关](#13-directionalinteractable--单向触发机关) 14. [CrumblePlatform — 碎裂平台](#14-crumbleplatform--碎裂平台) 15. [SkillInteractable — 技能专属交互物](#15-skillinteractable--技能专属交互物) 16. [世界事件频道清单](#16-世界事件频道清单) --- ## 1. 场景结构规范 **场景命名**(见 [01_ProjectStructure.md](./01_ProjectStructure.md) §8)。 **房间场景标准层级**(详见 Architecture README)。 重要约束: - 每个房间场景必须包含 `RoomController` 组件(挂在 `[RoomRoot]` GameObject 上) - 必须有至少一个 `RoomTransition`(出入口) - 玩家出生点:`PlayerSpawnPoint` 组件,由 `SceneLoadRequest.EntryTransitionId` 匹配 --- ## 2. RoomTransition ```csharp // 路径: Assets/Scripts/World/RoomTransition.cs // 挂在出入口 Trigger Collider2D 上 [RequireComponent(typeof(Collider2D))] public class RoomTransition : MonoBehaviour { [Header("Config")] [SerializeField] private string _transitionId; // 唯一 ID(目标出口用于匹配出生点) [SerializeField] private string _targetSceneAddress; // 目标场景 Addressable key(AddressKeys 常量) [SerializeField] private string _targetTransitionId; // 目标场景中对应出口的 ID [SerializeField] private bool _autoTrigger = true; // true = 进入触发器自动触发;false = 需交互 [SerializeField] private bool _requiresKeyItem; // 是否需要持有鑰匙物品 [SerializeField] private string _requiredItemId; // 鑰匙物品 ID [Header("Event Channel")] [SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest; [Header("世界状态")] [SerializeField] private WorldStateRegistry _worldState; // 持有物品检查 // 玩家进入触发器 private void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag("Player")) return; if (_requiresKeyItem && !HasItem(_requiredItemId)) return; _onSceneLoadRequest.Raise(new SceneLoadRequest { SceneName = _targetSceneName, EntryTransitionId = _targetTransitionId, ShowLoadingScreen = false, IsRespawn = false }); } private bool HasItem(string itemId) { if (string.IsNullOrEmpty(itemId)) return true; if (_worldState == null) return false; // 未配置则拦截(警告日志) return _worldState.IsCollected(itemId); } // Editor:在 Scene View 显示箭头 Gizmo private void OnDrawGizmos(); } // 玩家出生点,与 RoomTransition.transitionId 对应 public class PlayerSpawnPoint : MonoBehaviour { public string TransitionId; public Vector2 SpawnPosition => transform.position; public int FacingDirection = 1; // +1 右, -1 左 private void OnDrawGizmos() { /* 绿色标记 */ } } ``` --- ## 3. SavePoint ```csharp // 路径: Assets/Scripts/World/SavePoint.cs // 实现 IInteractable + ISaveable,玩家交互时触发存档 // 架构决策:通过 IRestoreOnSave(定义于 BaseGames.Core)调用玩家回血/灵泉, // 避免 World 层反向依赖 BaseGames.Player,World.asmdef 无需引用 Player 程序集。 public class SavePoint : MonoBehaviour, IInteractable, ISaveable { [Header("Config")] [SerializeField] private string _savePointId; [SerializeField] private bool _restoreSpring = true; [Header("Event Channels")] [SerializeField] private StringEventChannelSO _onSavePointActivated; [SerializeField] private VoidEventChannelSO _onFastTravelOpen; private bool _isActivated; // IInteractable(⚠️ 参数为 Transform,与 14_NarrativeModule §1 / 07 §7 对齐) public bool CanInteract => true; public string InteractPrompt => _isActivated ? "休息" : "激活"; public void Interact(Transform player) { _isActivated = true; // 1. 通过 IRestoreOnSave 恢复玩家(World 不感知具体 Player 类型) var restorer = player.GetComponentInChildren(); if (restorer != null) { restorer.FullRestore(); if (_restoreSpring) restorer.RestoreSpring(); } // 2. 广播存档点激活(GameManager 响应并调用 SaveManager.SaveAsync) _onSavePointActivated?.Raise(_savePointId); // 3. 若该场景已有多个存档点激活,打开快速旅行 UI // 4. 播放激活动画 / 特效 } // ISaveable 存档集成 public bool IsActivated => _isActivated; public void SetActivated(bool val) => _isActivated = val; } ``` > **IRestoreOnSave 接口**(`Assets/Scripts/Core/IRestoreOnSave.cs`,命名空间 `BaseGames.Core`) > `PlayerStats` 显式实现:`FullRestore()` → `FullHeal()`,`RestoreSpring()` → `RestoreSpringCharges()`。 > 同一接口可扩展至其他可被存档点恢复的对象(伙伴、坐骑等),无需修改 SavePoint 本身。 --- ## 4. HazardZone ```csharp // 路径: Assets/Scripts/World/HazardZone.cs // 即死区域(深坑、岩浆等) [RequireComponent(typeof(Collider2D))] public class HazardZone : MonoBehaviour { public enum RespawnType { AtLastSavePoint, AtRoomEntry } [SerializeField] private bool _isInstantKill = true; [SerializeField] private int _damage = 9999; [SerializeField] private RespawnType _respawnType = RespawnType.AtLastSavePoint; private void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag("Player")) return; var stats = other.GetComponentInParent(); if (stats == null) return; if (_isInstantKill) stats.TakeDamage(stats.MaxHP * 2); // 确保即死 else stats.TakeDamage(_damage); } } ``` --- ## 5. Collectible ```csharp // 路径: Assets/Scripts/World/Collectible.cs // Geo 货币 / 物品掉落 public class Collectible : MonoBehaviour { [Header("Config")] [SerializeField] private CollectibleType _type; [SerializeField] private int _geoAmount; // type = Geo 时 [SerializeField] private string _itemId; // type = Item 时 [SerializeField] private bool _isPersistent; // false = 敌人掉落(不存档); true = 固定位置(存档) [Header("Physics")] [SerializeField] private float _bounceForce = 5f; [Header("Event Channel")] [SerializeField] private StringEventChannelSO _onCollectiblePickup; private string _collectibleId; // 用于存档(持久化 Collectible 专用) private void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag("Player")) return; var player = other.GetComponentInParent(); if (player == null) return; switch (_type) { case CollectibleType.Geo: player.Stats.AddGeo(_geoAmount); break; case CollectibleType.Item: // 通知 Inventory / QuestManager _onCollectiblePickup.Raise(_itemId); break; } if (_isPersistent) _onCollectiblePickup.Raise(_collectibleId); // 存档标记 Despawn(); } private void Despawn(); // 归还对象池 // ―― 运行时配置(由 CollectibleSpawner 在实例化后调用)―――――――――――――――― /// 将此 Collectible 配置为 Geo 掉落。 public void SetGeo(int amount); /// 将此 Collectible 配置为道具掉落。 public void SetItem(string itemId); } public enum CollectibleType { Geo, Item, HPOrb } ``` --- ## 5b. CollectibleSpawner ```csharp // 路径: Assets/Scripts/World/CollectibleSpawner.cs + CollectibleSpawnerConfig.cs // 封装 Collectible Prefab 实例化逻辑,供 LootResolver 等静态调用。 // CollectibleSpawnerConfig 挂在 Persistent 场景 [World] GameObject 上持有 Prefab 引用, // Awake 调用 CollectibleSpawner.Register(this) 注入静态配置。 public static class CollectibleSpawner { internal static void Register(CollectibleSpawnerConfig config); public static void SpawnGeo(Vector2 position, int amount); // 实例化 GeoPrefab 并调用 SetGeo(amount) public static void SpawnItem(Vector2 position, string itemId); // 实例化 ItemPrefab 并调用 SetItem(itemId) } public class CollectibleSpawnerConfig : MonoBehaviour { [SerializeField] internal GameObject GeoPrefab; [SerializeField] internal GameObject ItemPrefab; private void Awake() => CollectibleSpawner.Register(this); } ``` --- ## 6. AbilityUnlock ```csharp // 路径: Assets/Scripts/World/AbilityUnlock.cs // 世界中固定位置的能力解锁物(获取新技能) public class AbilityUnlock : MonoBehaviour, IInteractable { [SerializeField] private AbilityType _abilityToUnlock; [SerializeField] private string _unlockId; // 存档用 [Header("Event Channels")] [SerializeField] private StringEventChannelSO _onCollectiblePickup; // 通知存档已拾取 private bool _isCollected = false; public bool CanInteract => !_isCollected; public string InteractPrompt => "获得能力"; public void Interact(Transform player) { if (_isCollected) return; _isCollected = true; // ⚠️ PlayerController 无 Instance;通过 player 参数获取 player.GetComponent()?.Stats.UnlockAbility(_abilityToUnlock); _onCollectiblePickup.Raise(_unlockId); // 触发解锁演出(Cutscene / UI 提示) gameObject.SetActive(false); } public void SetCollected(bool val) { _isCollected = val; if (val) gameObject.SetActive(false); } } ``` --- ## 7. IInteractable 接口 ```csharp // 路径: Assets/Scripts/World/IInteractable.cs // ⚠️ 与 14_NarrativeModule §1 对齐(权威定义):Transform 参数 + 5 成员 namespace BaseGames.World { public interface IInteractable { bool CanInteract { get; } // 当前是否可交互 string InteractPrompt { get; } // 显示在交互提示 UI 上的文字 void Interact(Transform player); // 传入玩家 Transform;需要 PlayerController 时通过 player.GetComponent() 获取(PlayerController 无 Instance) void OnPlayerEnterRange(Transform player); // 进入检测范围 void OnPlayerExitRange(); // 离开检测范围 } } ``` --- ## 8. InteractableDetector ```csharp // 路径: Assets/Scripts/World/InteractableDetector.cs // 挂在玩家上,检测周围可交互物并驱动交互 UI public class InteractableDetector : MonoBehaviour { [SerializeField] private float _detectRadius = 1.5f; [SerializeField] private LayerMask _interactableLayer; [SerializeField] private InputReaderSO _inputReader; [SerializeField] private StringEventChannelSO _onShowInteractPrompt; // 发布:显示交互提示 [SerializeField] private VoidEventChannelSO _onHideInteractPrompt; // 发布:隐藏交互提示 private IInteractable _nearest; private IInteractable _previousNearest; private void OnEnable() => _inputReader.InteractEvent += TryInteract; private void OnDisable() => _inputReader.InteractEvent -= TryInteract; private void Update() { // OverlapCircle → 找最近 IInteractable var hits = Physics2D.OverlapCircleAll(transform.position, _detectRadius, _interactableLayer); _nearest = FindNearest(hits); if (_nearest != _previousNearest) { if (_previousNearest != null) { _previousNearest.OnPlayerExitRange(); _onHideInteractPrompt.Raise(); } if (_nearest != null) { _nearest.OnPlayerEnterRange(transform); _onShowInteractPrompt.Raise(_nearest.InteractPrompt); } _previousNearest = _nearest; } } private void TryInteract() { _nearest?.Interact(transform); // 传入玩家自身 Transform;IInteractable 内部通过 player.GetComponent() 获取组件(PlayerController 无 Instance) } } ``` --- ## 9. WorldStateRegistry > **⚠️ 与 `14_NarrativeModule §8` 统一**:`WorldStateRegistry` 已改为 **ScriptableObject**(`CreateAssetMenu`), > 通过 `[SerializeField]` 注入,不再使用静态 `Instance`。`HasFlag` / `SetFlag(key)` 接口与 Architecture 14 §8 保持一致。 ```csharp // 路径: Assets/Scripts/World/Narrative/WorldStateRegistry.cs // 管理世界持久化状态(已收集物、已激活存档点、已开门、已销毁对象、通用标志) // ScriptableObject 形式,各组件通过 [SerializeField] 注入,零耦合;与 14_NarrativeModule §8 统一 namespace BaseGames.World { [CreateAssetMenu(menuName = "World/WorldStateRegistry")] public class WorldStateRegistry : ScriptableObject { private HashSet _collectedIds = new(); private HashSet _activatedSavePoints = new(); private HashSet _openedDoors = new(); private HashSet _destroyedObjects = new(); public bool IsCollected(string id) => _collectedIds.Contains(id); public void MarkCollected(string id) => _collectedIds.Add(id); public bool IsSavePointActivated(string id) => _activatedSavePoints.Contains(id); public void MarkSavePointActivated(string id) => _activatedSavePoints.Add(id); public bool IsDestroyed(string id) => _destroyedObjects.Contains(id); public void MarkDestroyed(string id) => _destroyedObjects.Add(id); public bool IsDoorOpened(string id) => _openedDoors.Contains(id); public void MarkDoorOpened(string id) => _openedDoors.Add(id); // 通用世界状态标记(过场记录、剧情事件等) // ⚠️ 接口与 14_NarrativeModule §8 统一:HasFlag(非 IsFlagSet);SetFlag(key) 单参数添加 private HashSet _flags = new(); public bool HasFlag(string key) => _flags.Contains(key); public void SetFlag(string key) => _flags.Add(key); // SaveManager 集成(非 ISaveable,由 SaveManager 在 SaveAsync/LoadAsync 中直接调用) public void LoadFromSave(WorldSaveData data); public HashSet GetAllFlags(); } } ``` > **SaveManager 集成**:`WorldStateRegistry` 不实现 `ISaveable` 接口(ScriptableObject,非 MonoBehaviour)。 > `SaveManager` 在保存/加载时直接调用: > ```csharp > // SaveManager.CollectAllData() 内: > saveData.World = WorldStateRegistry.Instance.GetSaveData(); // 通过 SO 引用而非静态 Instance > > // SaveManager.ApplyLoadedData() 内: > WorldStateRegistry.Instance.LoadFromSave(saveData.World); > ``` > `WorldStateRegistry` SO 资产路径:`Assets/Data/World/WorldStateRegistry.asset`。 --- ## 10. DestructibleTile ```csharp // 路径: Assets/Scripts/World/DestructibleTile.cs // 可被攻击破坏的地形块(影响导航网格) public class DestructibleTile : MonoBehaviour, IDamageable { [SerializeField] private int _maxHP = 1; [SerializeField] private string _destructedId; // 存档唯一 ID private bool _isDestroyed = false; // IDamageable public bool IsInvincible => _isDestroyed; public int Defense => 0; public void TakeDamage(DamageInfo info) { if (_isDestroyed) return; if (!CheckDestroyCondition(info)) return; // 子类可覆盖(DirectionalDestructible 方向校验) _isDestroyed = true; // 禁用 Renderer + 碰撞体 // 通知 PathBerserker2d 重新烘焙局部导航网格 // 记录到 WorldStateRegistry } // 子类覆盖以附加销毁前提条件(默认:无条件销毁) protected virtual bool CheckDestroyCondition(DamageInfo info) => true; } ``` --- ## 11. MovingPlatform ```csharp // 路径: Assets/Scripts/World/MovingPlatform.cs // 动态移动平台:Kinematic Rigidbody2D,乘客自动跟随(Passenger Pattern) [RequireComponent(typeof(Rigidbody2D))] public class MovingPlatform : MonoBehaviour { public enum MoveType { LinearAB, WayPoints, TriggeredLinear } [Header("移动配置")] [SerializeField] private MoveType _moveType = MoveType.LinearAB; [SerializeField] private Transform[] _wayPoints; // LinearAB 仅用 [0][1] [SerializeField] private float _speed = 3f; // u/s [SerializeField] private float _waitAtEndpoint = 0.5f; // 端点停留秒数 [Header("TriggeredLinear 模式")] [SerializeField] private VoidEventChannelSO _activationChannel; // 接收信号后单程运动 // 乘客检测:顶面上方 0.05f 的 IsTrigger BoxCollider2D,检测到 Player/Enemy 层 [Header("乘客检测")] [SerializeField] private BoxCollider2D _passengerSensor; // Trigger,仅用于检测 private Rigidbody2D _rb; private List _passengers = new(); private int _waypointIndex; private bool _movingForward = true; private bool _triggered; private void Awake() => _rb = GetComponent(); private void FixedUpdate() { if (_moveType == MoveType.TriggeredLinear && !_triggered) return; MoveTowardsNextWaypoint(); } private void MoveTowardsNextWaypoint() { var target = _wayPoints[_waypointIndex].position; var next = Vector2.MoveTowards(_rb.position, target, _speed * Time.fixedDeltaTime); _rb.MovePosition(next); if (Vector2.Distance(_rb.position, target) < 0.02f) StartCoroutine(WaitAndAdvance()); } private IEnumerator WaitAndAdvance() { yield return new WaitForSeconds(_waitAtEndpoint); AdvanceWaypoint(); } private void AdvanceWaypoint() { // LinearAB: 往返; WayPoints: 环形; TriggeredLinear: 到达终点后停止 if (_moveType == MoveType.TriggeredLinear) { _waypointIndex = Mathf.Min(_waypointIndex + 1, _wayPoints.Length - 1); if (_waypointIndex == _wayPoints.Length - 1) _triggered = false; return; } if (_moveType == MoveType.LinearAB) { _movingForward = !_movingForward; _waypointIndex = _movingForward ? 1 : 0; } else // WayPoints { _waypointIndex = (_waypointIndex + 1) % _wayPoints.Length; } } // ── 乘客跟随(Passenger Pattern)───────────────────────────────── private void OnTriggerEnter2D(Collider2D other) { if ((1 << other.gameObject.layer & LayerMask.GetMask("Player", "Enemy")) == 0) return; other.transform.SetParent(transform); _passengers.Add(other.transform); } private void OnTriggerExit2D(Collider2D other) { if (!_passengers.Contains(other.transform)) return; other.transform.SetParent(null); _passengers.Remove(other.transform); // 继承平台当前速度(仅玩家) if (other.CompareTag("Player")) other.GetComponentInParent()?.AddForce( _rb.velocity, ForceMode2D.Impulse); } private void OnEnable() { if (_activationChannel != null) _activationChannel.OnEventRaised += OnTriggered; } private void OnDisable() { if (_activationChannel != null) _activationChannel.OnEventRaised -= OnTriggered; } private void OnTriggered() => _triggered = true; } ``` **MoveType 说明**: | 类型 | 行为 | |------|------| | `LinearAB` | `_wayPoints[0]` ↔ `[1]` 往返循环 | | `WayPoints` | 按 `_wayPoints[]` 顺序环形循环 | | `TriggeredLinear` | 监听 `_activationChannel`,收到信号后单程 `[0]→[n-1]`,到达后停止 | > **NavSurface 联动**:每个移动平台挂载独立 `LocalNavSurface`(局部坐标系),附着其上的敌人 NavAgent 使用该 LocalNavSurface 寻路;参见 Guides/PathBerserker2d_Technical_Evaluation.md §5。 --- ## 12. DirectionalDestructible — 单向可破坏墙 继承 `DestructibleTile`,在其基础上增加**攻击方向校验**: ```csharp // 路径: Assets/Scripts/World/DirectionalDestructible.cs public class DirectionalDestructible : DestructibleTile { public enum AttackSide { Left, Right, Top, Bottom, Any } [SerializeField] private AttackSide _validAttackSide = AttackSide.Any; protected override bool CheckDestroyCondition(DamageInfo info) { if (_validAttackSide == AttackSide.Any) return base.CheckDestroyCondition(info); // 判断攻击来源方向(info.SourcePosition 由 HitBox 传入) var dir = (info.SourcePosition - (Vector2)transform.position).normalized; bool valid = _validAttackSide switch { AttackSide.Left => dir.x < -0.5f, AttackSide.Right => dir.x > 0.5f, AttackSide.Top => dir.y > 0.5f, AttackSide.Bottom => dir.y < -0.5f, _ => true }; return valid && base.CheckDestroyCondition(info); } #if UNITY_EDITOR private void OnDrawGizmos() { // 有效攻击方向:橙色箭头;无效方向:灰色叉号 var arrow = _validAttackSide switch { AttackSide.Left => Vector2.left, AttackSide.Right => Vector2.right, AttackSide.Top => Vector2.up, AttackSide.Bottom => Vector2.down, _ => Vector2.zero }; if (arrow == Vector2.zero) return; Gizmos.color = new Color(1f, 0.5f, 0f, 0.9f); var origin = (Vector2)transform.position; Gizmos.DrawLine(origin, origin + arrow * 0.8f); } #endif } ``` | 典型场景 | 配置 | |---------|------| | 地板薄板:只能从下方砸穿 | `_validAttackSide = Bottom` | | 密室封墙:仅能从房间内打开 | `_validAttackSide = Right`(依据朝向) | | 普通脆弱墙 | 基类 `DestructibleTile`(`AnyAttack`)即可,不需此子类 | --- ## 13. DirectionalInteractable — 单向触发机关 ```csharp // 路径: Assets/Scripts/World/DirectionalInteractable.cs // 可从特定方向触发的单向机关(零耦合:通过 SO 事件频道连接受体) [RequireComponent(typeof(Collider2D))] public class DirectionalInteractable : MonoBehaviour, IInteractable { public enum TriggerSide { Left, Right, Top, Any } public enum TriggerCondition { PlayerAttack, PlayerBody, InteractKey } [Header("触发条件")] [SerializeField] private TriggerSide _triggerSide = TriggerSide.Any; [SerializeField] private TriggerCondition _triggerCondition = TriggerCondition.InteractKey; [Header("行为")] [SerializeField] private bool _isOneShot; // 一次性,触发后永久激活 [SerializeField] private string _interactableId; // 存档用唯一 ID [Header("事件频道(零耦合连接关卡受体)")] [SerializeField] private VoidEventChannelSO _activationChannel; [SerializeField] private VoidEventChannelSO _deactivationChannel; // 非 OneShot 离开时 [Header("反馈")] [SerializeField] private MMF_Player _activateFeedback; private bool _activated; // ── IInteractable(InteractKey 模式)───────────────────────────── public string InteractPrompt => _activated ? "已激活" : "交互"; public void Interact(Transform player) // ⚠️ Transform 参数(与 §7 IInteractable 对齐) { if (_triggerCondition != TriggerCondition.InteractKey) return; if (!CheckSide(player.position)) return; TryActivate(); } // ── PlayerBody / PlayerAttack 模式 ─────────────────────────────── // PlayerBody:OnTriggerEnter2D(Collider IsTrigger) // PlayerAttack:挂配套 HurtBox → DamageInfo → TryInteractFromDamage private void OnTriggerEnter2D(Collider2D other) { if (_triggerCondition != TriggerCondition.PlayerBody) return; if (!other.CompareTag("Player")) return; if (!CheckSide(other.transform.position)) return; TryActivate(); } private void OnTriggerExit2D(Collider2D other) { if (_triggerCondition != TriggerCondition.PlayerBody) return; if (!other.CompareTag("Player") || _isOneShot) return; _activated = false; _deactivationChannel?.Raise(); } // 由外部 HurtBox 转发(PlayerAttack 模式) public void TryInteractFromDamage(DamageInfo info) { if (_triggerCondition != TriggerCondition.PlayerAttack) return; if (!CheckSide(info.SourcePosition)) return; TryActivate(); } private void TryActivate() { if (_isOneShot && _activated) return; _activated = true; _activateFeedback?.PlayFeedbacks(); _activationChannel?.Raise(); if (_isOneShot) { // 持久化 SaveManager.Instance?.SetMechanismState(_interactableId, true); } } private bool CheckSide(Vector2 sourcePos) { if (_triggerSide == TriggerSide.Any) return true; var dir = (sourcePos - (Vector2)transform.position).normalized; return _triggerSide switch { TriggerSide.Left => dir.x < -0.4f, TriggerSide.Right => dir.x > 0.4f, TriggerSide.Top => dir.y > 0.4f, _ => true }; } private void Start() { // 读档恢复 if (_isOneShot && !string.IsNullOrEmpty(_interactableId) && (SaveManager.Instance?.GetMechanismState(_interactableId) ?? false)) { _activated = true; _activationChannel?.Raise(); // 静默恢复联动状态 } } } ``` **零耦合连接示例**(Inspector 拖入同一 SO 资产): ``` Switch_Forest_01._activationChannel ──► MovingPlatform._activationChannel ──► Door_Locked._openChannel ──► HazardZone_Spikes._disableChannel ``` --- ## 14. CrumblePlatform — 碎裂平台 ```csharp // 路径: Assets/Scripts/World/CrumblePlatform.cs [RequireComponent(typeof(BoxCollider2D))] public class CrumblePlatform : MonoBehaviour { [SerializeField] private float _warningDuration = 0.6f; // 踩上后警告时长(抖动) [SerializeField] private float _crumbleDuration = 0.3f; // 碎裂动画时长 [SerializeField] private float _respawnDelay = 3.0f; // 0 = 永久消失 [SerializeField] private bool _isOneShot = false; // true = 碎裂后永久消失 [SerializeField] private MMF_Player _crumbleFeedback; // 预警震动 + 碎裂粒子 + 音效 [SerializeField] private BoxCollider2D _passengerSensor; // Trigger,检测玩家踩踏 private BoxCollider2D _col; private SpriteRenderer _sr; private bool _isCrumbling; private static readonly int[] StateFrames = { 0, 1, 2, 3 }; // Idle/Warning/Crumbling/Gone private void Awake() { _col = GetComponent(); _sr = GetComponent(); } private void OnTriggerEnter2D(Collider2D other) { if (_isCrumbling || !other.CompareTag("Player")) return; StartCoroutine(CrumbleSequence()); } private IEnumerator CrumbleSequence() { _isCrumbling = true; // 1. Warning(抖动) _crumbleFeedback?.PlayFeedbacks(); yield return new WaitForSeconds(_warningDuration); // 2. Crumbling yield return new WaitForSeconds(_crumbleDuration); // 3. Gone:禁用碰撞体 + 隐藏 Sprite _col.enabled = false; _sr.enabled = false; _passengerSensor.enabled = false; if (_isOneShot || _respawnDelay <= 0f) { yield break; // 永久消失 } // 4. Respawn yield return new WaitForSeconds(_respawnDelay); _col.enabled = true; _sr.enabled = true; _passengerSensor.enabled = true; _isCrumbling = false; } } ``` **状态机**: ``` [玩家踩上] Idle ────────────► Warning ──[warningDuration]──► Crumbling ──[动画]──► Gone 抖动 │ ←──[respawnDelay,非 OneShot]─┘ ``` --- ## 15. SkillInteractable — 技能专属交互物 > 这类物体不走伤害管线,而是监听**角色技能状态**或**物理层叠加**实现交互。 > 三种类型对应游戏内三个形态的专属技能机关。 ### 15.1 MagicWall — 魔法障壁(太虚斩专属) 太虚斩(命魂 SoulSkill)施放时,玩家进入 `PhysicsLayer: Ghost`;`MagicWall` 对 `Ghost` 层**无碰撞**,允许穿越。 ```csharp // 路径: Assets/Scripts/World/MagicWall.cs // 组件挂法:MagicWall GO 同时挂 TilemapCollider2D / BoxCollider2D // 不与 Ghost 层碰撞(Physics Layer Matrix 配置,非代码控制) // 脚本职责:Gizmo 可视化 + 颜色联动(普通/幽灵两态视觉区分) [ExecuteAlways] public class MagicWall : MonoBehaviour { [SerializeField] private Color _normalColor = new(0.4f, 0.2f, 1f, 0.8f); // 紫色(可见) [SerializeField] private Color _ghostColor = new(0.4f, 0.2f, 1f, 0.15f); // 淡紫(穿越提示) // 在 FormSkillSO(太虚斩)canPassMagicWalls = true 时, // SkillManager 在技能开始/结束时切换玩家的物理层: // 开始: player.gameObject.layer = LayerMask.NameToLayer("Ghost") // 结束: player.gameObject.layer = LayerMask.NameToLayer("Player") // Physics Layer Matrix 设置: Ghost vs MagicWall = IgnoreCollision // 因此 MagicWall 本身无需额外代码,只靠层矩阵实现穿越。 #if UNITY_EDITOR private void OnDrawGizmos() { Gizmos.color = _normalColor; var b = GetComponent(); if (b != null) Gizmos.DrawWireCube(transform.position, b.bounds.size); } #endif } ``` **Physics Layer Matrix 配置**: | Layer A | Layer B | 碰撞 | |---------|---------|------| | `Player` | `MagicWall` | ✅ 碰撞(正常阻挡)| | `Ghost` | `MagicWall` | ❌ 忽略(太虚斩穿越)| | `Enemy` | `MagicWall` | ✅ 碰撞(敌人不能穿越)| > 参见 [57_PhysicsLayerMatrix.md](../Design/57_PhysicsLayerMatrix.md)。 > `SkillManager.TrySoulSkill()` 在技能激活/结束时调用 `SetPlayerLayer("Ghost"/"Player")`。 --- ### 15.2 SoftTerrain — 松软地面(地行术专属) 地行术(地魂 SoulSkill)的 `GroundDive` 状态中,玩家进入地面移动。`SoftTerrain` 地块降低地行术**灵力消耗速率**(松软地面不消耗灵力)。 ```csharp // 路径: Assets/Scripts/World/SoftTerrain.cs // 挂在松软地面的 Tilemap/GameObject 上 // GroundDiveState 通过 OverlapPoint 检测当前站立/穿行瓦片,查询是否 IsSoftTerrain public class SoftTerrain : MonoBehaviour { // Marker 组件——无逻辑,仅用于 GetComponent() 检测 // GroundDiveState(PlayerFSM)在每帧对角色脚下 Physics2D.OverlapPoint() 检测: // 若碰到实现了 SoftTerrain 的 Tilemap → SetSoulDrainRate(0) // 否则 → SetSoulDrainRate(FormSkillSO.soulCostPerSecond) } ``` **关卡搭建**: - 在 `[Level]` 下新增 `Tilemap_SoftGround` 层,铺设松软地面 Tile - 该 Tilemap GameObject 挂载 `SoftTerrain` 组件 - `TilemapCollider2D.isTrigger = false`(正常地面碰撞,`GroundDiveState` 穿越时物理层切换为 `Ghost` 忽略该层) **与 MagicWall 的关键区别**: | | MagicWall | SoftTerrain | |-|-----------|-------------| | 穿越条件 | 太虚斩激活(`Ghost` 层)| 地行术激活(另一 `Ghost` 变体层)| | 其余情况 | 实体阻挡 | 实体地面 | | 游戏效果 | 到达秘密区域 / 跑图捷径 | 降低灵力消耗 / 速度加成 | --- ### 15.3 PhantomInteractable — 幻影机关(残阴术专属) 残阴术(命魂 SpiritSkill1)在原地留下灵体,灵体可代替玩家触发特定机关。 普通 `PressurePlate` 仅响应玩家,`PhantomInteractable` 额外响应 `PhantomBody` 层。 ```csharp // 路径: Assets/Scripts/World/PhantomInteractable.cs // 继承 DirectionalInteractable,额外监听 PhantomBody 层的 Collider 进入 // 用途:需要延迟触发的机关(先放灵体踩住,再操控玩家本体做其他事) public class PhantomInteractable : DirectionalInteractable { // 残阴术(SpiritSkill1)实例化 PhantomBody Prefab, // PhantomBody 挂载 Rigidbody2D,Layer = "PhantomBody" // 本组件的 Collider(Trigger)对 PhantomBody 层同样响应 private new void OnTriggerEnter2D(Collider2D other) { bool isPlayer = other.CompareTag("Player"); bool isPhantom = other.gameObject.layer == LayerMask.NameToLayer("PhantomBody"); if (!isPlayer && !isPhantom) return; // 方向校验:幻影机关通常 TriggerSide = Any(灵体无方向约束) TryActivate(); } } ``` **典型谜题**: ``` 场景设置: PhantomInteractable(PressurePlate 型)── _activationChannel ──► Door 解谜流程: 1. 玩家施放残阴术 → 留下灵体踩住 PhantomInteractable → 门打开 2. 玩家快速通过门洞 3. 残阴术持续时间结束 → 灵体消失 → PhantomInteractable 失活 → 门关闭(若非 OneShot) ``` --- ## 16. 世界事件频道清单 | 资产名 | 类型 | Raise 方 | Subscribe 方 | |--------|------|---------|-------------| | `EVT_SavePointActivated` | `StringEventChannelSO` | `SavePoint` | `GameManager`(触发存档)、`HUDController`(显示提示) | | `EVT_RoomTransitionRequest` | `SceneLoadRequestEventChannelSO` | `RoomTransition` | `SceneLoader` | | `EVT_CollectiblePickup` | `StringEventChannelSO` | `Collectible`、`AbilityUnlock` | `WorldStateRegistry`、`QuestManager`、`AnalyticsManager` | | `EVT_FastTravelOpen` | `VoidEventChannelSO` | `SavePoint` | `UIManager`(显示 FastTravel 面板) | | `EVT_ShowInteractPrompt` | `StringEventChannelSO` | `InteractableDetector` | `HUDController` | | `EVT_HideInteractPrompt` | `VoidEventChannelSO` | `InteractableDetector` | `HUDController` |