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

12 KiB
Raw Permalink Blame History

36 · 导航辅助系统Navigation Hint System

命名空间 BaseGames.Navigation
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.Core.Events · BaseGames.World(地图/场景系统)· BaseGames.Progression(能力/进程状态)


目录

  1. 系统总览
  2. NavHintSO — 路标数据
  3. WorldMarker — 世界路标组件
  4. HintNPC — NPC 指引系统
  5. BreadcrumbTracker — 面包屑导航
  6. 地图图钉联动
  7. SaveData 集成
  8. 事件频道
  9. 编辑器友好设计

1. 系统总览

大型银河恶魔城地图容易让玩家迷路。导航辅助系统通过三种手段减少无效探索:世界路标(场景中放置的方向指引)、NPC 指引NPC 可以告诉玩家下一步去哪)、面包屑追踪(自动记录未探索的出口/门)。

导航辅助系统职责:
  ├─ NavHintSO          → 路标数据 SO触发条件 + 指引文本 + 目标房间)
  ├─ WorldMarker        → 场景内路标物件(根据进程显示/隐藏方向指示)
  ├─ HintNPC            → NPC 的"去哪里"指引对话(与 DialogueSystem 集成)
  └─ BreadcrumbTracker  → 追踪玩家发现过但未进入的门/通道,在地图上标记

设计原则:辅助,不强制。所有导航提示都是可选的,不阻断玩家自由探索。路标只有在"玩家卡住时"才有价值,不应该过于显眼影响探索乐趣。


2. NavHintSO — 路标数据

[CreateAssetMenu(menuName = "Navigation/NavHint")]
public class NavHintSO : ScriptableObject
{
    [Header("触发条件")]
    [Tooltip("满足哪些条件时,这条路标才应该显示")]
    public NavHintCondition[] showConditions;   // 全部满足才显示AND

    [Header("指引内容")]
    [TextArea(1, 3)]
    public string   hintText;           // 如"东边的岩洞深处有可以突破障碍的力量"

    [Header("目标位置(可选)")]
    public string   targetSceneName;    // 目标房间(留空=不绑定地图图钉)
    public Vector2  targetWorldPos;     // 目标世界坐标(用于地图图钉)

    [Header("优先级")]
    [Range(1, 5)]
    public int      priority = 3;       // 1=最低5=最高(多条提示时显示最高优先级)
}

NavHintCondition内置类型

条件类 参数 满足时机
HasAbilityCondition abilityType 玩家拥有指定能力
BossDefeatedCondition bossId 指定 Boss 已被击败
AreaEnteredCondition regionId 玩家已进入指定区域
TimeInAreaCondition regionId, minMinutes 玩家在某区域停留超过N分钟防止新手卡关
AlwaysShowCondition 始终显示(调试/强制提示)
DifficultyCondition maxDifficulty 仅在简单模式及以下显示

3. WorldMarker — 世界路标组件

场景中放置的物理路标(石碑、箭头标记、光柱),根据 NavHintSO 条件动态显示或隐藏:

public class WorldMarker : MonoBehaviour
{
    [Header("路标数据")]
    [SerializeField] NavHintSO _hint;

    [Header("显示控制")]
    [SerializeField] GameObject _markerVisual;   // 路标视觉Sprite/粒子)
    [SerializeField] string     _markerId;       // 唯一 ID用于追踪玩家是否已见过

    [Header("交互(可选)")]
    [SerializeField] bool       _interactable;   // 玩家靠近可读取路标文字
    [SerializeField] MMF_Player _interactFeedback;

    bool _isVisible;

    void Start() => EvaluateVisibility();

    // 当进程变化时重新评估
    void OnEnable()
    {
        NavHintManager.Instance.RegisterMarker(this);
        EvaluateVisibility();
    }

    void OnDisable() => NavHintManager.Instance.UnregisterMarker(this);

    public void EvaluateVisibility()
    {
        bool shouldShow = _hint != null &&
            Array.TrueForAll(_hint.showConditions, c => c.IsMet());

        if (_isVisible == shouldShow) return;
        _isVisible = shouldShow;
        _markerVisual.SetActive(shouldShow);
    }

    // IInteractable 实现(若 _interactable = true
    public void Interact()
    {
        if (_hint == null) return;
        _interactFeedback?.PlayFeedbacks();
        // 将 hintText 发送至 DialogueManager 显示(单行气泡)
        DialogueManager.Instance.ShowHintBubble(_hint.hintText);
        // 标记地图图钉
        if (!string.IsNullOrEmpty(_hint.targetSceneName))
            MapManager.Instance.SetHintPin(_markerId, _hint.targetSceneName, _hint.targetWorldPos);
    }
}

4. HintNPC — NPC 指引系统

HintNPCInteractableNPC15_DialogueSystem.md)的扩展,增加"下一步指引"对话集:

public class HintNPC : InteractableNPC
{
    [Header("导航指引")]
    [SerializeField] NavHintSO[] _hints;    // 按优先级顺序,取第一个条件满足的

    protected override DialogueSequenceSO GetCurrentDialogue()
    {
        // 先检查常规进程对话(父类逻辑)
        var baseDialogue = base.GetCurrentDialogue();

        // 再检查导航提示(若有更高优先级)
        NavHintSO bestHint = null;
        foreach (var hint in _hints)
        {
            if (Array.TrueForAll(hint.showConditions, c => c.IsMet()))
            {
                if (bestHint == null || hint.priority > bestHint.priority)
                    bestHint = hint;
            }
        }

        if (bestHint != null)
        {
            // 将 hintText 包装为临时 DialogueSequenceSO或直接返回 baseDialogue + Append
            return WrapHintAsDialogue(bestHint);
        }

        return baseDialogue;
    }

    DialogueSequenceSO WrapHintAsDialogue(NavHintSO hint)
    {
        // 动态创建单行对话,内容来自 hint.hintText
        // 同时在地图上标注提示位置
        if (!string.IsNullOrEmpty(hint.targetSceneName))
            MapManager.Instance.SetHintPin(npcId + "_hint", hint.targetSceneName, hint.targetWorldPos);

        return DialogueSequenceSO.CreateRuntime(npcId, hint.hintText);
    }
}

5. BreadcrumbTracker — 面包屑导航

追踪玩家"见到但未进入"的门/通道,在地图上用特殊图标标记,提醒玩家"这里有一条路还没走过"

public class BreadcrumbTracker : MonoBehaviour
{
    [Header("事件频道")]
    [SerializeField] StringEventChannelSO _onRoomEntered;

    // Key = doorId / portalId, Value = 目标场景名
    readonly Dictionary<string, string> _seenButUnvisited = new();

    void OnEnable() => _onRoomEntered.OnEventRaised += HandleRoomEntered;
    void OnDisable() => _onRoomEntered.OnEventRaised -= HandleRoomEntered;

    /// <summary>
    /// 由 RoomTransition 在玩家*看见*门时调用(不需要进入)。
    /// </summary>
    public void RegisterSeen(string doorId, string targetScene)
    {
        if (!SaveManager.Instance.HasVisitedRoom(targetScene))
            _seenButUnvisited[doorId] = targetScene;
    }

    /// <summary>
    /// 玩家进入新场景后,从未访问列表中移除。
    /// </summary>
    void HandleRoomEntered(string sceneName)
    {
        var toRemove = _seenButUnvisited
            .Where(kv => kv.Value == sceneName)
            .Select(kv => kv.Key)
            .ToList();

        foreach (var key in toRemove)
        {
            _seenButUnvisited.Remove(key);
            MapManager.Instance.ClearBreadcrumb(key);
        }

        // 在地图上显示未访问的门标记
        foreach (var kv in _seenButUnvisited)
            MapManager.Instance.SetBreadcrumb(kv.Key, kv.Value);

        SaveToData();
    }

    void SaveToData()
    {
        SaveManager.Instance.SetBreadcrumbs(_seenButUnvisited
            .Select(kv => new BreadcrumbEntry { doorId = kv.Key, targetScene = kv.Value })
            .ToList());
    }
}

[Serializable]
public struct BreadcrumbEntry
{
    public string doorId;
    public string targetScene;
}

地图中的面包屑图标

地图系统(16_MapSystem.md)新增面包屑图标层:

  • 面包屑标记:橙色问号图标,覆盖在房间出口位置
  • 提示图钉:蓝色图钉图标,由 WorldMarkerHintNPC 触发时放置
  • 玩家进入目标房间后,对应标记自动消失

6. 地图图钉联动

MapManager16_MapSystem.md)需新增两个方法支持导航提示:

// 在 MapManager 中新增
public void SetHintPin(string pinId, string targetScene, Vector2 worldPos)
{
    // 在全屏地图对应位置放置蓝色图钉 Sprite
    _hintPins[pinId] = new HintPinData { targetScene = targetScene, worldPos = worldPos };
    RefreshMapUI();
}

public void SetBreadcrumb(string doorId, string targetScene)
{
    _breadcrumbs[doorId] = targetScene;
    RefreshMapUI();   // 在地图边缘显示橙色问号
}

public void ClearBreadcrumb(string doorId)
{
    _breadcrumbs.Remove(doorId);
    RefreshMapUI();
}

7. SaveData 集成

"navigation": {
  "breadcrumbs": [
    { "doorId": "Door_Cave_East_01", "targetScene": "Room_Cave_03" },
    { "doorId": "Door_Ruins_North", "targetScene": "Room_Ruins_01" }
  ],
  "hintPins": [
    { "pinId": "NPC_Elder_hint", "targetScene": "Room_Ruins_01", "worldPos": { "x": 25, "y": -8 } }
  ]
}

8. 事件频道

频道资产 类型 发布方 主要订阅方
OnNavHintActivated.asset StringEventChannelSO WorldMarker(交互时) MapManager(放置图钉)
OnBreadcrumbAdded.asset StringEventChannelSO BreadcrumbTracker MapManager
OnBreadcrumbCleared.asset StringEventChannelSO BreadcrumbTracker MapManager

9. 编辑器友好设计

WorldMarker Gizmos

Scene 视图中 WorldMarker 显示:

  • 绿色箭头:当前条件满足(路标可见)
  • 灰色箭头:条件未满足(路标隐藏)
  • 箭头指向 _hint.targetWorldPos 方向

NavHintManager InspectorPlay Mode

┌─ NavHintManager ──────────────────────────────────────┐
│  激活路标: 2 / 5                                       │
│  ┌────────────────────────────────────────┐           │
│  │ WorldMarker_Forest_East   ✅ 已显示    │           │
│  │ WorldMarker_Cave_Entrance ⬜ 条件未满足 │           │
│  │   └ AreaEnteredCondition: Ruins ✗      │           │
│  └────────────────────────────────────────┘           │
│  面包屑: 3 个未访问门                                  │
│  ┌─────────────────────────────────┐                  │
│  │ Door_Cave_East_01 → Room_Cave_03│                  │
│  │ Door_Ruins_North → Room_Ruins_01│                  │
│  └─────────────────────────────────┘                  │
└───────────────────────────────────────────────────────┘

路标放置 SOP

  1. 选择场景中合适的位置创建空 GameObject命名 NavMarker_{描述}
  2. 挂载 WorldMarker 组件
  3. 创建 NavHintSO(右键 Assets/Data/Navigation/Create → Navigation/NavHint
  4. 配置条件(何时显示)和指引文本
  5. 若需地图联动,填写 targetSceneNametargetWorldPos
  6. Play Mode 中可通过 NavHintManager Inspector 验证条件评估结果