14 KiB
14 KiB
35 · 谜题架构(Puzzle Architecture)
命名空间
BaseGames.Puzzle
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Core.Events·BaseGames.World(IInteractable)·BaseGames.Player(能力查询)
目录
- 系统总览
- 核心接口:ISwitchable / IMovable / IActivatable
- PuzzleSwitch — 开关组件
- PuzzleReceiver — 接收器组件
- PuzzleWire — 开关到接收器的连接
- 内置谜题类型
- 组合谜题(多开关联动)
- SaveData 集成
- 事件频道
- 编辑器友好设计
1. 系统总览
谜题架构提供一套通用的环境互动框架,让关卡设计师无需编程即可通过 Inspector 拼装各种谜题:拉杆开门、踩压板触发平台、推重物压下机关、与 NPC 对话解锁秘道。
谜题架构职责:
├─ ISwitchable → 任何"可被切换状态"的物件接口(开关/压板/杠杆)
├─ IMovable → 任何"可被玩家推动"的物件接口(木箱/重物)
├─ IActivatable → 任何"接受激活信号"的物件接口(门/平台/光源)
├─ PuzzleSwitch → 通用开关组件(玩家交互或踩踏触发)
├─ PuzzleReceiver → 接收器组件(门/平台等挂载,监听激活信号)
└─ PuzzleWire → 连接开关→接收器,支持多输入 AND/OR/XOR 逻辑
设计原则:谜题逻辑完全通过 Inspector 配置,无脚本耦合。一个 PuzzleWire SO 描述"当 N 个开关同时激活时执行动作",关卡设计师只需拖拽引用。
2. 核心接口
ISwitchable
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
/// <summary>
/// 可被玩家推动的物件(需要 Rigidbody2D)。
/// 典型实现:PushBox、PushStone
/// </summary>
public interface IMovable
{
bool CanBePushed { get; }
void OnPushStart(Vector2 direction);
void OnPushEnd();
}
IActivatable
/// <summary>
/// 接受激活信号后改变自身状态的物件。
/// 典型实现:PuzzleDoor、MovingPlatform、SpikeTrap
/// </summary>
public interface IActivatable
{
void Activate();
void Deactivate();
bool IsActivated { get; }
}
3. PuzzleSwitch — 开关组件
挂载在谜题触发物件上(压板、杠杆、按钮),实现 ISwitchable,并支持三种触发模式:
[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:
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 个接收器":
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 — 可推动箱子
[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:
"puzzles": {
"switchStates": {
"Lever_A": true,
"Lever_B": false,
"Lever_C": true
},
"solvedPuzzles": [
"PuzzleWire_BossGate",
"PuzzleWire_Secret"
]
}
// 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
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(零代码)
- 创建触发物 → 挂载
PuzzleSwitch,设置TriggerMode与_switchId - 创建受控物 → 挂载
PuzzleReceiver,配置ReceiverBehaviour与目标 - 创建空 GameObject → 挂载
PuzzleWire,拖入输入开关和输出接收器数组 - 设置
LogicType(AND/OR/XOR),勾选persistActivation(门是否永久开) - 若需要联动故事演出 → 参考
34_EventChainSystem.md,创建配套事件链
快速测试
Play Mode 中 PuzzleSwitch Inspector 显示 [Force Activate] / [Force Deactivate] 按钮,可跳过交互直接测试下游效果。