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

14 KiB
Raw Permalink Blame History

35 · 谜题架构Puzzle Architecture

命名空间 BaseGames.Puzzle
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.Core.Events · BaseGames.WorldIInteractable· BaseGames.Player(能力查询)


目录

  1. 系统总览
  2. 核心接口ISwitchable / IMovable / IActivatable
  3. PuzzleSwitch — 开关组件
  4. PuzzleReceiver — 接收器组件
  5. PuzzleWire — 开关到接收器的连接
  6. 内置谜题类型
  7. 组合谜题(多开关联动)
  8. SaveData 集成
  9. 事件频道
  10. 编辑器友好设计

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));
    }

    // 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

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 PuzzleReceiverRaiseEvent 行为) EventChainManagerAchievementManager
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零代码

  1. 创建触发物 → 挂载 PuzzleSwitch,设置 TriggerMode_switchId
  2. 创建受控物 → 挂载 PuzzleReceiver,配置 ReceiverBehaviour 与目标
  3. 创建空 GameObject → 挂载 PuzzleWire,拖入输入开关和输出接收器数组
  4. 设置 LogicTypeAND/OR/XOR勾选 persistActivation(门是否永久开)
  5. 若需要联动故事演出 → 参考 34_EventChainSystem.md,创建配套事件链

快速测试

Play Mode 中 PuzzleSwitch Inspector 显示 [Force Activate] / [Force Deactivate] 按钮,可跳过交互直接测试下游效果。