chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,922 @@
# 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 _targetSceneName; // 目标场景AddressKeys 常量)
[SerializeField] private string _targetTransitionId; // 目标场景中对应出口的 ID
[SerializeField] private bool _requiresKeyItem; // 是否需要持有钥匙物品
[SerializeField] private string _requiredItemId; // 钥匙物品 ID
[Header("Event Channel")]
[SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest;
// 玩家进入触发器
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); // 查询 PlayerStats 或 Inventory
// 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.PlayerWorld.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<IRestoreOnSave>();
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<PlayerStats>();
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<PlayerController>();
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(); // 归还对象池
// 敌人死亡时生成 Geo Collectible由 EnemyBase 调用)
public static void SpawnGeo(Vector2 position, int amount, ObjectPoolManager pool);
}
public enum CollectibleType { Geo, Item, HPOrb }
```
---
## 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<PlayerController>()?.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>() 获取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); // 传入玩家自身 TransformIInteractable 内部通过 player.GetComponent<PlayerController>() 获取组件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<string> _collectedIds = new();
private HashSet<string> _activatedSavePoints = new();
private HashSet<string> _openedDoors = new();
private HashSet<string> _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非 IsFlagSetSetFlag(key) 单参数添加
private HashSet<string> _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<string> 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<Transform> _passengers = new();
private int _waypointIndex;
private bool _movingForward = true;
private bool _triggered;
private void Awake() => _rb = GetComponent<Rigidbody2D>();
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<Rigidbody2D>()?.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;
// ── IInteractableInteractKey 模式)─────────────────────────────
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 模式 ───────────────────────────────
// PlayerBodyOnTriggerEnter2DCollider 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<BoxCollider2D>();
_sr = GetComponent<SpriteRenderer>();
}
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<Collider2D>();
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<SoftTerrain>() 检测
// GroundDiveStatePlayerFSM在每帧对角色脚下 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 挂载 Rigidbody2DLayer = "PhantomBody"
// 本组件的 ColliderTrigger对 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();
}
}
```
**典型谜题**
```
场景设置:
PhantomInteractablePressurePlate 型)── _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` |