20 KiB
地图系统制作手册(策划 / 开发)
文件位置:
Docs/Guides/06_MapSystem_Authoring_Guide.md版本:1.0 · 适用项目:zeling_v2 · 对标:丝之歌(Hollow Knight: Silksong)的全屏大地图
本手册指导策划与开发人员完成银河恶魔城地图系统的全流程制作:从环境装配、单个房间的地图数据制作、世界布局,到本地化 / 输入提示 / 资源合规,并附运行时验证与常见问题排查。
目录
- 架构概览
- 一次性环境准备(每个工程做一次)
- 快速开始:为一个房间制作地图(端到端)
- 工具速查表
- MapRoomDataSO 字段详解
- 世界布局与格子校准
- 底图美术管线
- 文字本地化接入
- 输入提示图标接入
- 资源管理合规(必须经 AssetLoader)
- 运行时验证
- 常见问题排查
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 校准) |
_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 世界布局 手动摆放。
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)或在MapDatabaseInspector 的AllRooms数组中加入该 SO。 - 新建房间 SO 时,若勾选了某个 MapDatabase 为默认库,
MapRoomAutoRegister会自动注册。
3.6 设置场景房间 ID(进房自动揭示)
打开房间场景,选中 RoomController,在 _roomId 填写与 RoomId 一致的值(如 TestRoomA)。保存该场景。
缺这一步:进入该房间不会自动标记为已探索(地图上显示为未知/迷雾)。
3.7 实测
见 §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 用调试入口快速进房(推荐)
- 编辑器进入 Play(默认从 Persistent 启动到主菜单)。
- 菜单
BaseGames → Debug → Enter First Room (Play)—— 绕过新游戏菜单,直接加载首关(Scene_Game_Chapter1)并生成玩家。 - 玩家进房 →
MapPlayerTracker自动广播EVT_RoomEntered→ 房间标记为已探索。 - 触发
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 已订阅)。