chore: initial commit
This commit is contained in:
344
Docs/Design/36_NavigationHintSystem.md
Normal file
344
Docs/Design/36_NavigationHintSystem.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# 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 Inspector(Play 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 验证条件评估结果
|
||||
Reference in New Issue
Block a user