# 35 · 谜题架构(Puzzle Architecture) > **命名空间** `BaseGames.Puzzle` > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `BaseGames.Core.Events` · `BaseGames.World`(IInteractable)· `BaseGames.Player`(能力查询) --- ## 目录 1. [系统总览](#1-系统总览) 2. [核心接口:ISwitchable / IMovable / IActivatable](#2-核心接口iswitchable--imovable--iactivatable) 3. [PuzzleSwitch — 开关组件](#3-puzzleswitch--开关组件) 4. [PuzzleReceiver — 接收器组件](#4-puzzlereceiver--接收器组件) 5. [PuzzleWire — 开关到接收器的连接](#5-puzzlewire--开关到接收器的连接) 6. [内置谜题类型](#6-内置谜题类型) 7. [组合谜题(多开关联动)](#7-组合谜题多开关联动) 8. [SaveData 集成](#8-savedata-集成) 9. [事件频道](#9-事件频道) 10. [编辑器友好设计](#10-编辑器友好设计) --- ## 1. 系统总览 谜题架构提供一套通用的环境互动框架,让关卡设计师无需编程即可通过 Inspector 拼装各种谜题:拉杆开门、踩压板触发平台、推重物压下机关、与 NPC 对话解锁秘道。 ``` 谜题架构职责: ├─ ISwitchable → 任何"可被切换状态"的物件接口(开关/压板/杠杆) ├─ IMovable → 任何"可被玩家推动"的物件接口(木箱/重物) ├─ IActivatable → 任何"接受激活信号"的物件接口(门/平台/光源) ├─ PuzzleSwitch → 通用开关组件(玩家交互或踩踏触发) ├─ PuzzleReceiver → 接收器组件(门/平台等挂载,监听激活信号) └─ PuzzleWire → 连接开关→接收器,支持多输入 AND/OR/XOR 逻辑 ``` **设计原则**:谜题逻辑完全通过 Inspector 配置,无脚本耦合。一个 `PuzzleWire` SO 描述"当 N 个开关同时激活时执行动作",关卡设计师只需拖拽引用。 --- ## 2. 核心接口 ### ISwitchable ```csharp namespace BaseGames.Puzzle { /// /// 任何可被切换激活/停用状态的谜题元素实现此接口。 /// 典型实现:PressurePlate、PullLever、TimedButton /// public interface ISwitchable { bool IsActive { get; } event Action OnStateChanged; // 状态变化时发布(true=激活,false=停用) void ForceState(bool active); // 供 SaveData 恢复时调用 } } ``` ### IMovable ```csharp /// /// 可被玩家推动的物件(需要 Rigidbody2D)。 /// 典型实现:PushBox、PushStone /// public interface IMovable { bool CanBePushed { get; } void OnPushStart(Vector2 direction); void OnPushEnd(); } ``` ### IActivatable ```csharp /// /// 接受激活信号后改变自身状态的物件。 /// 典型实现:PuzzleDoor、MovingPlatform、SpikeTrap /// public interface IActivatable { void Activate(); void Deactivate(); bool IsActivated { get; } } ``` --- ## 3. PuzzleSwitch — 开关组件 挂载在谜题触发物件上(压板、杠杆、按钮),实现 `ISwitchable`,并支持三种触发模式: ```csharp [RequireComponent(typeof(Collider2D))] public class PuzzleSwitch : MonoBehaviour, ISwitchable, IInteractable { public enum TriggerMode { Interact, // 玩家交互(按 F) PressurePlate, // 玩家/可移动物踩踏(Collider 进入) Timed, // 触发后 X 秒恢复(弹簧式按钮) } [Header("触发模式")] [SerializeField] TriggerMode _mode = TriggerMode.Interact; [SerializeField] float _resetTime = 3f; // 仅 Timed 模式有效 [Header("视觉")] [SerializeField] Animator _animator; // 参数: "Active" bool [SerializeField] MMF_Player _activateFeedback; [SerializeField] MMF_Player _deactivateFeedback; [Header("持久化")] [SerializeField] string _switchId; // 唯一 ID,用于 SaveData [SerializeField] bool _persistState; // 状态是否持久化(false = 每次进场景重置) public bool IsActive { get; private set; } public event Action OnStateChanged; public bool CanInteract => !IsActive || _mode == TriggerMode.Interact; void Start() { if (_persistState && !string.IsNullOrEmpty(_switchId)) ForceState(SaveManager.Instance.GetPuzzleState(_switchId)); } // IInteractable(Interact 模式) public void Interact() => SetActive(!IsActive); // PressurePlate 模式:碰撞触发 void OnTriggerEnter2D(Collider2D other) { if (_mode != TriggerMode.PressurePlate) return; if (other.CompareTag("Player") || other.GetComponent() != null) SetActive(true); } void OnTriggerExit2D(Collider2D other) { if (_mode != TriggerMode.PressurePlate) return; SetActive(false); } void SetActive(bool active) { if (IsActive == active) return; IsActive = active; _animator?.SetBool("Active", active); (active ? _activateFeedback : _deactivateFeedback)?.PlayFeedbacks(); if (_persistState && !string.IsNullOrEmpty(_switchId)) SaveManager.Instance.SetPuzzleState(_switchId, active); OnStateChanged?.Invoke(active); if (active && _mode == TriggerMode.Timed) StartCoroutine(ResetAfterDelay()); } IEnumerator ResetAfterDelay() { yield return new WaitForSeconds(_resetTime); SetActive(false); } public void ForceState(bool active) { IsActive = active; _animator?.SetBool("Active", active); } } ``` --- ## 4. PuzzleReceiver — 接收器组件 挂载在被控制的物件上(门、平台、陷阱),实现 `IActivatable`: ```csharp public class PuzzleReceiver : MonoBehaviour, IActivatable { public enum ReceiverBehaviour { OpenDoor, // 开关门(Y 轴移动/Animation) MovePlatform, // 移动平台到目标位置 ToggleObject, // 激活/停用 GameObject PlayAnimation, // 播放一次性动画 RaiseEvent, // 触发 VoidEventChannelSO } [Header("行为")] [SerializeField] ReceiverBehaviour _behaviour; [SerializeField] Transform _targetTransform; // MovePlatform/OpenDoor 目标位置 [SerializeField] float _moveSpeed = 3f; [SerializeField] GameObject _toggleTarget; // ToggleObject 目标 [SerializeField] VoidEventChannelSO _raiseOnActivate; // RaiseEvent 目标 [SerializeField] MMF_Player _feedback; public bool IsActivated { get; private set; } public void Activate() { if (IsActivated) return; IsActivated = true; _feedback?.PlayFeedbacks(); ExecuteBehaviour(true); } public void Deactivate() { if (!IsActivated) return; IsActivated = false; ExecuteBehaviour(false); } void ExecuteBehaviour(bool activate) { switch (_behaviour) { case ReceiverBehaviour.OpenDoor: StartCoroutine(MoveTo(activate ? _targetTransform.position : _startPos, _moveSpeed)); break; case ReceiverBehaviour.ToggleObject: _toggleTarget.SetActive(activate); break; case ReceiverBehaviour.RaiseEvent: if (activate) _raiseOnActivate?.Raise(); break; case ReceiverBehaviour.PlayAnimation: GetComponent()?.SetTrigger(activate ? "Activate" : "Deactivate"); break; } } Vector3 _startPos; void Awake() => _startPos = transform.position; IEnumerator MoveTo(Vector3 target, float speed) { while (Vector3.Distance(transform.position, target) > 0.01f) { transform.position = Vector3.MoveTowards(transform.position, target, speed * Time.deltaTime); yield return null; } transform.position = target; } } ``` --- ## 5. PuzzleWire — 开关到接收器的连接 `PuzzleWire` 是纯数据组件,挂载在空 GameObject 上,描述"N 个开关 + 逻辑 → 激活 M 个接收器": ```csharp public class PuzzleWire : MonoBehaviour { public enum LogicType { AND, OR, XOR } [Header("输入开关")] public PuzzleSwitch[] inputs; public LogicType logic = LogicType.AND; [Header("输出接收器")] public PuzzleReceiver[] outputs; [Header("持久化")] [SerializeField] bool _persistActivation; // 接收器激活是否永久保持 void OnEnable() { foreach (var sw in inputs) sw.OnStateChanged += _ => Evaluate(); } void OnDisable() { foreach (var sw in inputs) sw.OnStateChanged -= _ => Evaluate(); } void Evaluate() { bool shouldActivate = logic switch { LogicType.AND => Array.TrueForAll(inputs, s => s.IsActive), LogicType.OR => Array.Exists(inputs, s => s.IsActive), LogicType.XOR => inputs.Count(s => s.IsActive) % 2 == 1, _ => false }; foreach (var receiver in outputs) { if (shouldActivate) receiver.Activate(); else if (!_persistActivation) receiver.Deactivate(); } } } ``` --- ## 6. 内置谜题类型 ### 6.1 压板开门 ``` 场景层级: ├── Pressure_Plate_01 ← PuzzleSwitch (PressurePlate 模式) ├── Door_Forest_Gate_01 ← PuzzleReceiver (OpenDoor 行为) └── [PuzzleWire_01] ← PuzzleWire {inputs=[Plate_01], logic=OR, outputs=[Door_01]} ``` ### 6.2 三杠杆全激活开门(AND 逻辑) ``` 场景层级: ├── Lever_A ← PuzzleSwitch (Interact 模式, persistState=true) ├── Lever_B ← PuzzleSwitch (Interact 模式, persistState=true) ├── Lever_C ← PuzzleSwitch (Interact 模式, persistState=true) ├── Boss_Gate ← PuzzleReceiver (OpenDoor 行为) └── [PuzzleWire_BossGate] inputs = [Lever_A, Lever_B, Lever_C] logic = AND outputs = [Boss_Gate] persistActivation = true ← 开门后永久保持打开 ``` ### 6.3 推箱压板 ``` 场景层级: ├── PushBox_01 ← PushBox.cs + Rigidbody2D (IMovable) ├── PressurePlate_Heavy_01 ← PuzzleSwitch (PressurePlate,仅对 IMovable 响应) ├── Gate_Secret_01 ← PuzzleReceiver (ToggleObject) └── [PuzzleWire_Secret] inputs = [PressurePlate_Heavy_01] logic = OR outputs = [Gate_Secret_01] ``` ### 6.4 限时弹簧按钮谜题(踩下后 3 秒关闭) ``` ├── TimedButton_01 ← PuzzleSwitch (Timed 模式, resetTime=3s) ├── Platform_01 ← PuzzleReceiver (MovePlatform 行为) └── [PuzzleWire_TimedPlatform] ``` ### 6.5 PushBox — 可推动箱子 ```csharp [RequireComponent(typeof(Rigidbody2D))] public class PushBox : MonoBehaviour, IMovable { [SerializeField] float _pushForce = 5f; Rigidbody2D _rb; bool _isPushing; void Awake() => _rb = GetComponent(); public bool CanBePushed => true; public void OnPushStart(Vector2 direction) { _isPushing = true; _rb.velocity = new Vector2(direction.x * _pushForce, _rb.velocity.y); } public void OnPushEnd() { _isPushing = false; _rb.velocity = new Vector2(0f, _rb.velocity.y); } } ``` **玩家推箱逻辑**:在 `PlayerMovementState` 中,检测面前碰撞物是否实现 `IMovable`,若是则调用 `OnPushStart`/`OnPushEnd`,玩家进入推箱动画。 --- ## 7. 组合谜题(多开关联动) 支持事件链(`34_EventChainSystem.md`)与谜题架构结合,实现"谜题完成 → 触发过场/改变NPC对话": ``` 流程: PuzzleWire 激活全部接收器 ↓ PuzzleReceiver.RaiseEvent → OnPuzzleCompleted.asset ↓ EventChainManager 接收 ↓ Chain_RuinsPuzzle_Solved.asset: [0] PlayCutsceneAction (揭示秘密的演出) [1] SpawnObjectAction (生成魅力 / 奖励) [2] SetFlagAction (标记谜题已解) ``` --- ## 8. SaveData 集成 谜题状态追踪(开关激活状态、门是否已开)存入 SaveData: ```json "puzzles": { "switchStates": { "Lever_A": true, "Lever_B": false, "Lever_C": true }, "solvedPuzzles": [ "PuzzleWire_BossGate", "PuzzleWire_Secret" ] } ``` ```csharp // SaveManager 扩展 public bool GetPuzzleState(string switchId) => _saveData.puzzles.switchStates.TryGetValue(switchId, out bool v) && v; public void SetPuzzleState(string switchId, bool state) { _saveData.puzzles.switchStates[switchId] = state; WriteDirty(); } ``` --- ## 9. 事件频道 | 频道资产 | 类型 | 发布方 | 主要订阅方 | |---------|------|--------|----------| | `OnPuzzleActivated.asset` | `StringEventChannelSO` | `PuzzleReceiver`(RaiseEvent 行为)| `EventChainManager`、`AchievementManager` | | `OnSwitchToggled.asset` | `StringEventChannelSO` | `PuzzleSwitch`(可选,调试/音效用)| `AudioManager`(播放机关音效)| --- ## 10. 编辑器友好设计 ### Scene 视图 Gizmos ```csharp void OnDrawGizmos() { // PuzzleWire:用黄线连接 inputs → outputs if (inputs == null || outputs == null) return; Gizmos.color = Color.yellow; foreach (var inp in inputs) foreach (var outp in outputs) if (inp != null && outp != null) Gizmos.DrawLine(inp.transform.position, outp.transform.position); } ``` ### PuzzleSwitch 状态可视化 - 未激活:Scene 视图显示**灰色圆圈** - 已激活:Scene 视图显示**黄色填充圆** - Timed 模式倒计时:圆弧进度条(用 Handles.DrawSolidArc) ### 谜题搭建 SOP(零代码) 1. 创建触发物 → 挂载 `PuzzleSwitch`,设置 `TriggerMode` 与 `_switchId` 2. 创建受控物 → 挂载 `PuzzleReceiver`,配置 `ReceiverBehaviour` 与目标 3. 创建空 GameObject → 挂载 `PuzzleWire`,拖入输入开关和输出接收器数组 4. 设置 `LogicType`(AND/OR/XOR),勾选 `persistActivation`(门是否永久开) 5. 若需要联动故事演出 → 参考 `34_EventChainSystem.md`,创建配套事件链 ### 快速测试 Play Mode 中 `PuzzleSwitch` Inspector 显示 **[Force Activate] / [Force Deactivate]** 按钮,可跳过交互直接测试下游效果。