Files
zeling_v2/Docs/Design/35_PuzzleArchitecture.md
2026-05-08 11:04:00 +08:00

485 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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));
}
// IInteractableInteract 模式)
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]** 按钮,可跳过交互直接测试下游效果。