地图系统

This commit is contained in:
2026-06-05 18:41:33 +08:00
parent 613f2a4d13
commit fe4fd60083
234 changed files with 33090 additions and 4899 deletions

View File

@@ -0,0 +1,394 @@
# 地图系统制作手册(策划 / 开发)
> 文件位置:`Docs/Guides/06_MapSystem_Authoring_Guide.md`
> 版本1.0 · 适用项目zeling_v2 · 对标丝之歌Hollow Knight: Silksong的全屏大地图
本手册指导**策划与开发人员**完成银河恶魔城地图系统的全流程制作:从环境装配、单个房间的地图数据制作、世界布局,到本地化 / 输入提示 / 资源合规,并附运行时验证与常见问题排查。
---
## 目录
1. [架构概览](#1-架构概览)
2. [一次性环境准备(每个工程做一次)](#2-一次性环境准备每个工程做一次)
3. [快速开始:为一个房间制作地图(端到端)](#3-快速开始为一个房间制作地图端到端)
4. [工具速查表](#4-工具速查表)
5. [MapRoomDataSO 字段详解](#5-maproomdataso-字段详解)
6. [世界布局与格子校准](#6-世界布局与格子校准)
7. [底图美术管线](#7-底图美术管线)
8. [文字本地化接入](#8-文字本地化接入)
9. [输入提示图标接入](#9-输入提示图标接入)
10. [资源管理合规(必须经 AssetLoader](#10-资源管理合规必须经-assetloader)
11. [运行时验证](#11-运行时验证)
12. [常见问题排查](#12-常见问题排查)
---
## 1. 架构概览
地图系统是**事件驱动 + ServiceLocator 解耦**的,分三层:
```
数据层Edit Time 配置)
├── MapRoomDataSO 每个房间一个RoomId / RegionId / GridPosition / GridSize / RoomOutlineTex / Exits / RoomFlags
└── MapDatabaseSO 全局房间索引(所有 MapRoomDataSO 的数组 + O(1) 空间索引)
服务层Persistent 场景 [Services]boot 注册)
├── MapManager IMapService —— 探索状态Unknown/Explored/Mapped、ISaveable
├── MapPinManager IPinService —— 玩家自定义标记
├── TeleportService ITeleportService —— 传送点解锁/传送
└── MapPlayerTracker IPlayerPositionProvider —— 挂在【玩家】上,世界坐标→房间格;进房广播 EVT_RoomEntered
UI 层Persistent 场景UIManager PanelStack 管理)
└── Map Canvas / MapPanel 全屏大地图:平移/缩放、房间格、当前房间高亮、玩家点、Pin、迷雾、探索进度、区域名、传送选点、关闭提示
```
**核心数据流:**
```
玩家移动
└─ MapPlayerTracker挂玩家按世界坐标算出所在房间格
├─ OnRoomChanged → 小地图/UI 刷新(如启用)
└─ Raise EVT_RoomEntered(roomId)
├─ MapManager → 标记该房间 Explored地图揭示
├─ RoomStreamingManager → 重算流式集
└─ EventChainManager → 触发房间条件
按地图键
└─ Raise EVT_MapOpen
└─ UIManager.OpenPanel(PanelId.Map)
└─ MapPanel 渲染所有房间格(按 MapDatabase + 各房间可见性)
```
> **不使用右上角小地图**:本项目按需求只保留全屏大地图,地图 UI 脚手架不再搭建 MinimapHUD。
---
## 2. 一次性环境准备(每个工程做一次)
地图系统的服务与 UI 都需先装配进 **Persistent 场景**,且**必须保存到磁盘**。打开 `Persistent.unity` 后依次执行:
### 2.1 创建事件频道与基础资产(若尚未创建)
```
菜单栏 → BaseGames → Scene → Setup → Create All Event Channel Assets
菜单栏 → BaseGames → Setup → Create Project Assets
```
确认存在以下事件频道(`Assets/_Game/Data/Events/``EVT_RoomEntered``EVT_MapUpdated``EVT_RegionChanged``EVT_MapOpen``EVT_LanguageChanged``EVT_InputDeviceChanged`
### 2.2 装配地图运行时管理器
```
菜单栏 → BaseGames → Scene → Setup → Scaffold Map Managers
```
`[Persistent]/[Services]` 下创建并绑定:`MapManager`(绑 MapDatabase + EVT_RoomEntered/MapUpdated/RegionChanged`MapPinManager``TeleportService`
### 2.3 装配本地化与输入图标服务
```
菜单栏 → BaseGames → Scene → Setup → Scaffold Localization & Input Services
```
`[Services]` 下创建并绑定:`LocalizationManager`(文字本地化)、`InputDeviceDetector` + `InputIconService`(按键提示图标,绑 EVT_InputDeviceChanged
> ⚠ **没有这一步,运行时所有 UI 文字会显示成本地化 Key如 `MAP_CLOSE_HINT`)、按键图标不显示。**
### 2.4 装配地图 UI
```
菜单栏 → BaseGames → Scene → Setup → Scaffold Map UI
```
创建 `Map Canvas → MapPanel`含探索进度、关闭提示、Tooltip、传送确认框 + MapTeleportConfirmController、HUD 下区域名横幅),并登记 `UIManager._panels[Map]`。同时生成占位预制 `UI_Map_RoomCell` / `UI_Map_Pin` / `UI_Map_ExitConnector``MapPinConfig.asset`
### 2.5 给【玩家】挂 MapPlayerTracker
地图玩家定位与进房自动揭示依赖 `MapPlayerTracker`,它**挂在玩家对象上**(如 `Assets/_Game/Prefabs/Player/Player.prefab` 根节点):
| 字段 | 值 |
|---|---|
| `_playerTransform` | 玩家根 Transform |
| `_databaseOverride` | 与 MapManager 同一个 `MapDatabaseSO`(可留空,运行时从 IMapService 取) |
| `_onRoomEntered` | `EVT_RoomEntered`(进房自动揭示的关键) |
| `_worldUnitsPerCell` | 1 格对应的世界单位(默认 18见 [§6 校准](#6-世界布局与格子校准) |
| `_worldOriginOffset` | 世界原点偏移(默认 (0,0) |
### 2.6 ★ 保存 Persistent 场景(务必)
```
File → Save或 Ctrl+S保存 Persistent.unity
```
> ⚠ **极重要**:上面装配的服务/UI 都是场景对象,**必须保存到磁盘**。否则任何"重载场景"的操作(包括 Room Capture Baker 烘焙时的场景切换/恢复)都会丢失未保存的改动,导致运行时服务消失、地图与文字失效。
---
## 3. 快速开始:为一个房间制作地图(端到端)
`TestRoomA` 为例。前提:房间场景已搭好(含 `RoomController``CameraArea`且环境准备§2已完成并保存。
### 步骤一览
| # | 操作 | 工具 | 产出 |
|---|---|---|---|
| 1 | 创建房间数据 SO | 见下 | `Assets/_Game/Data/Map/Rooms/{RoomId}.asset` |
| 2 | 派生格子尺寸 / 布局位置 | Room Capture Baker / Map Layout Editor | `GridPosition` / `GridSize` |
| 3 | 烘焙底图 | Room Capture Baker | PNG + `RoomOutlineTex` |
| 4 | 加入数据库 | Map Layout Editor / 手动 | `MapDatabase.AllRooms` |
| 5 | 设场景房间 ID | 房间场景 Inspector | `RoomController._roomId` |
| 6 | 实测 | Debug 入口 + Play | 全屏地图渲染 |
### 3.1 创建 MapRoomDataSO
- 路径 / 命名:`Assets/_Game/Data/Map/Rooms/{RoomId}.asset`**RoomId 必须与场景文件名一致**(如 `TestRoomA`)。
- 创建方式:`Project 窗口右键 → Create → BaseGames → World → Map → RoomData`,或由 `SceneScaffoldTools.ScaffoldGameRoom` 搭房间时自动生成模板。
- 填写:`RoomId``RegionId`(如 `FengXian` / `Test`)。`GridPosition/GridSize` 下一步自动派生。
### 3.2 打开地图制作工具并指定数据库
```
菜单栏 → BaseGames → Map → Room Capture Baker
```
`MapDatabase``Assets/_Game/Data/World/Map/MapDatabase.asset`)拖入顶部字段。窗口列出库内所有房间。
### 3.3 派生格子尺寸GridSize
在窗口「格子布局派生」区,设 `世界单位/格``世界原点偏移`**与 MapPlayerTracker 一致**(默认 18 / (0,0)),然后:
- 点某房间行的 **「派生格子」**,或顶部 **「派生全部房间格子布局」**。
- 工具会打开房间场景,取 `CameraArea.VisibleBounds` 并集 ÷ 世界单位/格floor 下界、ceil 上界),写入 `GridPosition` / `GridSize`
> 派生得到的 **GridSize尺寸可直接用****GridPosition位置仅在房间场景本身就摆在世界真实坐标时才正确**。多个房间若各自摆在场景原点附近,派生位置会重叠——此时位置改用 [§6 世界布局](#6-世界布局与格子校准) 手动摆放。
### 3.4 烘焙底图RoomOutlineTex
在 Room Capture Baker
- 确认参数:`输出目录`(默认 `Assets/_Game/Art/Map/RoomCaptures`)、`每世界单位像素``透明背景`(推荐)、`临时全局光`(推荐,避免 URP 2D 离屏渲染偏黑)、`回填 RoomOutlineTex`(推荐)。
- 点房间行 **「烘焙」** 或顶部 **「烘焙全部房间」**。
- 工具会逐房间打开场景、用正交相机按可视范围渲染,输出 `{RoomId}.png` 并回填到 `MapRoomDataSO.RoomOutlineTex`
> 这张 PNG 是**给美术加工的底图**(不是最终美术)。美术在其上描绘 / 风格化成清晰的地图块后,**覆盖同名 PNG** 即自动回到游戏。
### 3.5 加入 MapDatabase
-`Map Layout Editor`(见 §4或在 `MapDatabase` Inspector 的 `AllRooms` 数组中加入该 SO。
- 新建房间 SO 时,若勾选了某个 MapDatabase 为默认库,`MapRoomAutoRegister` 会自动注册。
### 3.6 设置场景房间 ID进房自动揭示
打开房间场景,选中 `RoomController`,在 `_roomId` 填写与 RoomId 一致的值(如 `TestRoomA`)。**保存该场景。**
> 缺这一步:进入该房间不会自动标记为已探索(地图上显示为未知/迷雾)。
### 3.7 实测
见 [§11 运行时验证](#11-运行时验证)。
---
## 4. 工具速查表
| 工具 | 菜单 | 用途 |
|---|---|---|
| **Room Capture Baker** | `BaseGames → Map → Room Capture Baker` | 烘焙房间底图 + **派生 GridPosition/GridSize** |
| **Map Layout Editor** | `BaseGames → Map → Map Layout Editor` | 全局俯视布局:拖拽摆放房间、按区域着色、红色高亮重叠/重复、画出口连线、Play 时叠玩家位置 |
| **MapRoomDataSO 编辑器** | 选中 `MapRoomDataSO` | Scene View 中拖拽角点改 GridPosition/GridSize一键居中 |
| **MapDatabase 编辑器** | 选中 `MapDatabaseSO` | 房间列表、`ValidateAll`(重复 ID / 格子重叠 / 出口悬空)、打开布局编辑器 |
| **Scaffold Map Managers** | `BaseGames → Scene → Setup → Scaffold Map Managers` | 装配 MapManager/MapPinManager/TeleportService |
| **Scaffold Localization & Input Services** | `BaseGames → Scene → Setup → Scaffold Localization & Input Services` | 装配 LocalizationManager/InputDeviceDetector/InputIconService |
| **Scaffold Map UI** | `BaseGames → Scene → Setup → Scaffold Map UI` | 搭建 MapPanel 全屏地图 + 传送确认 + 区域名横幅 |
| **资源用法校验** | `BaseGames → Tools → Validation → Validate Resource Usage` | 扫描禁止的 `Resources.Load` / 散落 `Addressables.*` |
| **调试进首关** | `BaseGames → Debug → Enter First Room (Play)` | Play 模式下绕过菜单直接进首关,用于测地图 |
---
## 5. MapRoomDataSO 字段详解
| 字段 | 类型 | 说明 |
|---|---|---|
| `RoomId` | string | **与场景文件名一致**,全库唯一。空格会被自动修剪 |
| `RegionId` | string | 所属区域(用于区域名横幅与区域探索进度)。建议本地化(见 §8 |
| `DisplayName` | string | 地图 Tooltip 显示名。**填本地化 Key**(如 `ROOM_TESTROOMA_NAME`UI 自动解析;非 Key 则原样显示 |
| `GridPosition` | Vector2Int | 房间在世界地图格上的左下角坐标(单位:格)。可派生或手动布局 |
| `GridSize` | Vector2Int | 房间占据的格数(宽×高),每轴最小 1。建议由工具派生 |
| `RoomOutlineTex` | Texture2D | 房间在地图上显示的底图(截图或美术加工后的图)。空则回退到纯色格 |
| `RoomFlags` | RoomType[Flags] | 房间类型可多选BossRoom/SavePoint/Shop/Merchant/Challenge/TeleportStation。决定地图图标 |
| `MapIconOverride` | Sprite | 自定义图标,覆盖按 RoomFlags 的自动选择 |
| `Exits` | RoomExitData[] | 出口(目标房间 ID / 方向 / 格子位置 / 过渡类型),用于全屏地图画连接线 |
| `EstimatedMemoryKB` | int | 流式内存预算估值(与地图渲染无关) |
---
## 6. 世界布局与格子校准
### 6.1 GridSize 与 GridPosition 的来源
- **GridSize尺寸**:来自房间实际大小,建议用 Room Capture Baker 的「派生格子」自动算。
- **GridPosition位置**:是**世界地图布局的设计决策**——决定房间在整张地图上相对其它房间的位置。
- 若房间场景内容就摆在世界真实坐标,派生即正确。
- 否则(各房间都摆在场景原点附近)→ 用 **Map Layout Editor** 拖拽摆放,或在 SO/Scene View 中手动设置,**确保房间之间不重叠**(用 MapDatabase 的 `ValidateAll` 检查)。
### 6.2 worldUnitsPerCell 校准
`_worldUnitsPerCell`MapPlayerTracker 上,默认 18决定"多少世界单位 = 1 个地图格",直接影响房间在地图上的占格大小与玩家点定位精度。
- **必须三处一致**MapPlayerTracker、Room Capture Baker 的派生参数、以及实际关卡设计。
- 房间占格"偏大/偏小"时,调大/调小该值后**重新派生格子**即可校准。
- `_worldOriginOffset` 用于关卡世界原点不在地图 (0,0) 时对齐。
---
## 7. 底图美术管线
```
策划:搭房间场景(含 CameraArea / Ground Tilemap
→ Room Capture Baker「烘焙」正交相机渲染场景 → 透明底 PNG给美术的底图
→ 美术:在 PNG 上描绘 / 风格化成清晰的地图块(对标丝之歌手绘观感)
→ 覆盖同名 PNG → 运行时由 RoomOutlineTex 自动显示
```
- 底图默认透明背景,便于抠出房间形状。
- 截图偏暗时,提高 Baker 的「全局光强度」。
- 大房间提高「每世界单位像素」或调大「最大边像素」以保证清晰度(注意纹理内存)。
---
## 8. 文字本地化接入
项目使用**自研本地化系统**`BaseGames.Localization`**禁止硬编码面向玩家的文字**。
### 8.1 数据位置与格式
- JSON 表:`Assets/_Game/Data/Localization/{语言}/{表}.json`(经 Addressables地址 `Localization/{语言}/{表}`**不在 Resources**)。
- 语言:`ChineseSimplified` / `English` / `Japanese` / `Korean`。表:`UI` / `Dialogue` / `Quest`
- 格式:`{ "entries": [ { "key": "MAP_CLOSE_HINT", "value": "关闭地图" } ] }`Key 用 `UPPER_SNAKE_CASE`
### 8.2 地图相关 Key已内置于 UI 表,按需补全各语言)
| Key | 含义 |
|---|---|
| `MAP_PROGRESS_GLOBAL` | 全局探索进度格式(如 `已探索 {0:P0}` |
| `MAP_PROGRESS_REGION` | 区域进度格式(如 `{1}{0:P0}` |
| `MAP_CLOSE_HINT` | 关闭地图提示标签 |
| `TELEPORT_CONFIRM_TITLE` / `TELEPORT_CONFIRM_BODY` | 传送确认框标题 / 正文前缀 |
| `CONFIRM_YES` / `CONFIRM_NO` | 确认 / 取消按钮 |
| `REGION_{REGIONID}_NAME` | 区域显示名(区域名横幅 / 进度) |
| `ROOM_{ROOMID}_NAME` | 房间显示名Tooltip填到 `MapRoomDataSO.DisplayName` |
### 8.3 接入方式
- **静态 UI 文本**:在 TMP_Text 上挂 `LocalizedText` 组件,填 `Key`/`Table`,语言切换自动刷新。
- **代码动态文本**`LocalizationManager.Get(key, table)` / `GetFormat(key, table, args)`
- **房间 / 区域名**:把本地化 Key 填进 `MapRoomDataSO.DisplayName` / `RegionDefinitionSO`UI 自动解析。
### 8.4 中文字体CJK
> ⚠ 中文显示成 □ 方块时,是**缺中文 TMP 字体**所致(英文不受影响)。需导入 CJK 字体生成 TMP SDF 字体、创建 `LanguageFontConfigSO``BaseGames → Localization → Language Font Config`),并在 `LocalizedText._fontConfig` 引用。这是字体资产配置,非地图逻辑。
---
## 9. 输入提示图标接入
地图里的操作提示(关闭 / 缩放 / 放置标记等)须用**输入图标系统**(随键鼠/手柄自适应),**不硬编码"按 Tab"之类文字**。
- 提示行结构:`InputIconImage`(按键图标,随设备自适应) + `LocalizedText`(本地化标签)。地图脚手架的关闭提示已是此结构(动作名 `Cancel`)。
- `InputIconImage`ByActionName 模式)填 InputActions 中的动作名(如 `Cancel` / `Interact`),运行时由 `InputIconService` 解析当前设备的按键 Sprite。
- 图标资源4 套 `InputDeviceIconSetSO`(键鼠 / Xbox / PlayStation / Switch**Input Icon Studio** 编辑器工具配置 `绑定路径 → Sprite`,并赋给 `InputIconService` 的对应字段。
> ⚠ 未配置图标集 SO 时,图标位置显示为空白/占位框(标签仍正确)。这是美术资产配置。
---
## 10. 资源管理合规(必须经 AssetLoader
项目统一用 **Addressables****禁止 `Resources.Load`**,且不直接散落调用 `Addressables.*`——一律经门面 `BaseGames.Core.Assets.AssetLoader`
- 地图相关资源(房间底图、本地化表、配置)均已 Addressable 化、经 AssetLoader 加载。
- 提交前运行 `BaseGames → Tools → Validation → Validate Resource Usage` 确认 **0 违规**
- 设计师在 Inspector 拖拽的 `AssetReference` 引用属正常用法,不算违规。
---
## 11. 运行时验证
### 11.1 用调试入口快速进房(推荐)
1. 编辑器进入 **Play**(默认从 Persistent 启动到主菜单)。
2. 菜单 `BaseGames → Debug → Enter First Room (Play)` —— 绕过新游戏菜单,直接加载首关(`Scene_Game_Chapter1`)并生成玩家。
3. 玩家进房 → `MapPlayerTracker` 自动广播 `EVT_RoomEntered` → 房间标记为已探索。
4. 触发 `EVT_MapOpen`(或对应输入)打开全屏地图,应看到:房间格(带底图)、玩家点、探索进度、关闭提示。
### 11.2 通过正式流程
主菜单 → New Game → 选存档槽/模式 → 进入首关 → 游玩进房 → 按地图键看全屏地图(房间随探索逐格揭示)。
### 11.3 检查清单
- [ ] 进房后该房间在地图上由迷雾变为已探索。
- [ ] 房间格显示底图RoomOutlineTex玩家点位置正确。
- [ ] 探索进度文字正确(如 `已探索 X%`),非 Key。
- [ ] 关闭/操作提示显示按键图标 + 本地化标签。
- [ ] 多房间时相对位置正确、无重叠(`ValidateAll` 通过)。
---
## 12. 常见问题排查
### Q运行时 UI 文字显示成 `MAP_CLOSE_HINT` / 进度只显示 `100%` 没有前缀
**原因:** Persistent 场景里**没有 LocalizationManager**(本地化服务未装配或未保存)。
**解决:** 执行 `Scaffold Localization & Input Services` 装配,**并保存 Persistent**。确认 `[Services]` 下存在 `LocalizationManager`
---
### Q装配过的服务运行时又消失了
**原因:** 装配后**未保存 Persistent 场景**;随后任何重载场景的操作(如 Room Capture Baker 烘焙时切换/恢复场景)会从磁盘重载,丢失未保存改动。
**解决:** 装配后**立即保存 Persistent**§2.6)。这是最容易踩的坑。
---
### Q中文显示成 □ 方块(英文正常)
**原因:** 缺中文 TMP 字体(与本地化逻辑无关)。
**解决:** 导入 CJK 字体生成 TMP SDF`LanguageFontConfigSO`,在 `LocalizedText._fontConfig` 引用§8.4)。
---
### Q地图打开后房间是黑的 / 不显示底图
**可能原因 1** 房间未探索 → 显示未知(黑/迷雾)。确认 `RoomController._roomId` 已填且进过该房间。
**可能原因 2** `RoomOutlineTex` 未赋值 → 显示纯色格。用 Room Capture Baker 烘焙并回填。
**可能原因 3** 底图本身偏暗、单房间在地图上占比小。由美术加工底图,或调高 Baker 全局光。
---
### Q多个房间在地图上叠在一起
**原因:** 各房间的 `GridPosition` 重叠(常见于"派生位置"时各房间场景都摆在原点附近)。
**解决:****Map Layout Editor** 拖拽摆放到不重叠的位置;`MapDatabase``ValidateAll` 会红色高亮重叠房间。
---
### Q房间在地图上占格过大/过小
**原因:** `worldUnitsPerCell` 与关卡尺度不匹配。
**解决:** 调整 MapPlayerTracker 与 Room Capture Baker 的 `世界单位/格`**重新派生格子**§6.2)。
---
### Q按键提示图标不显示只有空白框 + 文字)
**原因:** `InputDeviceIconSetSO` 图标集未创建/未配置 Sprite`InputIconService` 未装配。
**解决:** 装配输入服务§2.3);用 Input Icon Studio 配置 4 套图标集并赋给 InputIconService§9
---
### Q打开全屏地图没反应
**可能原因 1** `UIManager._panels` 未登记 `Map`。重跑 `Scaffold Map UI`(会自动登记)。
**可能原因 2** `EVT_MapOpen` 未被任何输入触发。确认输入绑定会 Raise 该频道UIManager 已订阅)。