# 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]** 按钮,可跳过交互直接测试下游效果。