485 lines
14 KiB
Markdown
485 lines
14 KiB
Markdown
# 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
|
||
{
|
||
/// <summary>
|
||
/// 任何可被切换激活/停用状态的谜题元素实现此接口。
|
||
/// 典型实现:PressurePlate、PullLever、TimedButton
|
||
/// </summary>
|
||
public interface ISwitchable
|
||
{
|
||
bool IsActive { get; }
|
||
event Action<bool> OnStateChanged; // 状态变化时发布(true=激活,false=停用)
|
||
void ForceState(bool active); // 供 SaveData 恢复时调用
|
||
}
|
||
}
|
||
```
|
||
|
||
### IMovable
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// 可被玩家推动的物件(需要 Rigidbody2D)。
|
||
/// 典型实现:PushBox、PushStone
|
||
/// </summary>
|
||
public interface IMovable
|
||
{
|
||
bool CanBePushed { get; }
|
||
void OnPushStart(Vector2 direction);
|
||
void OnPushEnd();
|
||
}
|
||
```
|
||
|
||
### IActivatable
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// 接受激活信号后改变自身状态的物件。
|
||
/// 典型实现:PuzzleDoor、MovingPlatform、SpikeTrap
|
||
/// </summary>
|
||
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<bool> 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<IMovable>() != 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<Animator>()?.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<Rigidbody2D>();
|
||
|
||
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]** 按钮,可跳过交互直接测试下游效果。
|