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

345 lines
12 KiB
Markdown
Raw Permalink 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.
# 36 · 导航辅助系统Navigation Hint System
> **命名空间** `BaseGames.Navigation`
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
> **依赖** `BaseGames.Core.Events` · `BaseGames.World`(地图/场景系统)· `BaseGames.Progression`(能力/进程状态)
---
## 目录
1. [系统总览](#1-系统总览)
2. [NavHintSO — 路标数据](#2-navhintso--路标数据)
3. [WorldMarker — 世界路标组件](#3-worldmarker--世界路标组件)
4. [HintNPC — NPC 指引系统](#4-hintnpc--npc-指引系统)
5. [BreadcrumbTracker — 面包屑导航](#5-breadcrumbtracker--面包屑导航)
6. [地图图钉联动](#6-地图图钉联动)
7. [SaveData 集成](#7-savedata-集成)
8. [事件频道](#8-事件频道)
9. [编辑器友好设计](#9-编辑器友好设计)
---
## 1. 系统总览
大型银河恶魔城地图容易让玩家迷路。导航辅助系统通过三种手段减少无效探索:**世界路标**(场景中放置的方向指引)、**NPC 指引**NPC 可以告诉玩家下一步去哪)、**面包屑追踪**(自动记录未探索的出口/门)。
```
导航辅助系统职责:
├─ NavHintSO → 路标数据 SO触发条件 + 指引文本 + 目标房间)
├─ WorldMarker → 场景内路标物件(根据进程显示/隐藏方向指示)
├─ HintNPC → NPC 的"去哪里"指引对话(与 DialogueSystem 集成)
└─ BreadcrumbTracker → 追踪玩家发现过但未进入的门/通道,在地图上标记
```
**设计原则**:辅助,不强制。所有导航提示都是可选的,不阻断玩家自由探索。路标只有在"玩家卡住时"才有价值,不应该过于显眼影响探索乐趣。
---
## 2. NavHintSO — 路标数据
```csharp
[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` 条件动态显示或隐藏:
```csharp
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 指引系统
`HintNPC``InteractableNPC``15_DialogueSystem.md`)的扩展,增加"下一步指引"对话集:
```csharp
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 — 面包屑导航
追踪玩家"见到但未进入"的门/通道,在地图上用特殊图标标记,提醒玩家"这里有一条路还没走过"
```csharp
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`)新增面包屑图标层:
- **面包屑标记**:橙色问号图标,覆盖在房间出口位置
- **提示图钉**:蓝色图钉图标,由 `WorldMarker``HintNPC` 触发时放置
- 玩家进入目标房间后,对应标记自动消失
---
## 6. 地图图钉联动
`MapManager``16_MapSystem.md`)需新增两个方法支持导航提示:
```csharp
// 在 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 集成
```json
"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. 若需地图联动,填写 `targetSceneName``targetWorldPos`
6. Play Mode 中可通过 NavHintManager Inspector 验证条件评估结果