# 地图系统制作手册(策划 / 开发) > 文件位置:`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 已订阅)。