chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

488
Docs/Design/16_MapSystem.md Normal file
View File

@@ -0,0 +1,488 @@
# 16 · 地图系统
> **命名空间** `BaseGames.World.Map`
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
> **依赖** `BaseGames.Core.Events` · `BaseGames.World`SaveSystem· `BaseGames.UI`
---
## 目录
1. [系统总览](#1-系统总览)
2. [MapRoomDataSO — 房间元数据](#2-maproomdataso--房间元数据)
3. [MapManager — 探索状态管理](#3-mapmanager--探索状态管理)
4. [雾战渲染Fog of War](#4-雾战渲染fog-of-war)
5. [地图 UI — 全屏地图](#5-地图-ui--全屏地图)
6. [小地图 HUD](#6-小地图-hud)
7. [玩家位置追踪](#7-玩家位置追踪)
8. [地图图钉系统](#8-地图图钉系统)
9. [区域颜色与标记](#9-区域颜色与标记)
10. [SaveData 集成](#10-savedata-集成)
11. [场景搭建规范](#11-场景搭建规范)
12. [事件频道](#12-事件频道)
13. [编辑器友好设计](#13-编辑器友好设计)
---
## 1. 系统总览
地图系统是类银河恶魔城游戏的核心特征之一:玩家在黑暗中探索,地图随着进入新房间逐渐揭开。
```
地图系统职责:
├─ MapRoomDataSO → 每个房间的形状/出口/区域元数据Edit Time 配置)
├─ MapManager → 运行时探索状态驱动地图揭露OnRoomEntered 监听)
├─ Fog of War → 未探索黑色,已进入轮廓,已存档实色——三级显示
├─ 全屏地图 UI → 按 Map 键打开,可滚动/缩放
├─ 小地图 HUD → HUD 角落实时显示当前区域缩略图
├─ 玩家位置 → 随房间切换实时更新图标
└─ 地图图钉 → 玩家手动标记感兴趣位置
```
**零耦合原则**`MapManager` 仅监听 `OnRoomEntered` 事件频道,不持有 `PlayerController` 引用。
---
## 2. MapRoomDataSO — 房间元数据
每个房间场景对应一个 `MapRoomDataSO` 资产,**Edit Time 配置**,不在运行时生成。
```csharp
[CreateAssetMenu(menuName = "World/Map/RoomData")]
public class MapRoomDataSO : ScriptableObject
{
[Header("基础信息")]
public string roomId; // 与场景名一致,如 "Room_Forest_01"
public string displayName; // 可选,地图 Tooltip 显示
public string regionId; // 所属区域,如 "Forest"
[Header("地图布局")]
public Vector2Int gridPosition; // 地图格子坐标(手动填写,单位:格)
public Vector2Int gridSize; // 房间占据的格子数,如 (2, 1)
public Texture2D roomOutlineTex; // 房间轮廓贴图Edit Tool 自动生成,见 §13
[Header("出口")]
public RoomExitData[] exits; // 本房间所有出口(方向 + 目标房间 ID
[Header("特殊标记")]
public bool isBossRoom; // Boss 房间(地图特殊图标)
public bool isSavePoint; // 含存档点(地图存档图标)
public bool isShop; // 含商店(地图商店图标)
public Sprite mapIconOverride; // 自定义图标(如 None 则按 isXxx 自动选择)
}
[Serializable]
public struct RoomExitData
{
public ExitDirection direction; // Left / Right / Up / Down
public string targetRoomId;
public Vector2Int exitGridPos; // 出口在格子中的相对位置(用于绘制连接线)
}
```
**资产存放路径**`Assets/ScriptableObjects/World/MapRooms/`
**命名规范**`MR_{SceneName}.asset`,如 `MR_Room_Forest_01.asset`
---
## 3. MapManager — 探索状态管理
`MapManager` 常驻 Persistent 场景,维护运行时探索状态并驱动地图 UI 刷新。
```csharp
namespace BaseGames.World.Map
{
public class MapManager : MonoBehaviour
{
[Header("配置")]
[SerializeField] MapRoomDataSO[] _allRooms; // 所有房间 SO 数组
[SerializeField] MapDatabaseSO _mapDatabase; // 统一索引(见下)
[Header("事件频道(监听)")]
[SerializeField] StringEventChannelSO _onRoomEntered; // 监听:进入新房间(传入 roomId
[SerializeField] VoidEventChannelSO _onSavePointActivated; // 监听:存档点激活
[Header("事件频道(发布)")]
[SerializeField] StringEventChannelSO _onRoomExploredForSave; // 发布新房间已探索SaveSystem 监听并写入 SaveData
// 运行时状态(与 SaveData 同步)
readonly HashSet<string> _exploredRooms = new(); // 已进入房间
readonly HashSet<string> _mappedRooms = new(); // 已存档房间(完整显示)
string _currentRoomId;
// 地图 UI 刷新事件MapUI 订阅)
public event Action<string> OnRoomRevealed; // roomId
public event Action<string> OnRoomMapped; // roomId
public event Action<string> OnCurrentRoomChanged;
void OnEnable()
{
_onRoomEntered.OnEventRaised += HandleRoomEntered;
_onSavePointActivated.OnEventRaised += HandleSavePointActivated;
}
void OnDisable()
{
_onRoomEntered.OnEventRaised -= HandleRoomEntered;
_onSavePointActivated.OnEventRaised -= HandleSavePointActivated;
}
void HandleRoomEntered(string roomId)
{
_currentRoomId = roomId;
OnCurrentRoomChanged?.Invoke(roomId);
if (_exploredRooms.Add(roomId)) // 首次进入
{
OnRoomRevealed?.Invoke(roomId); // 触发地图揭露
// 零耦合:通过事件频道通知 SaveSystem 记录,而非直接调用 SaveManager.Instance
_onRoomExploredForSave.Raise(roomId);
}
}
void HandleSavePointActivated()
{
if (_currentRoomId == null) return;
if (_mappedRooms.Add(_currentRoomId)) // 存档后升级为 Mapped
OnRoomMapped?.Invoke(_currentRoomId);
}
// 启动时从 SaveData 恢复状态
public void Initialize(SaveData data)
{
_exploredRooms.UnionWith(data.world.discoveredRooms);
_mappedRooms.UnionWith(data.world.mappedRooms);
}
// 查询接口(供 MapUI 使用)
public MapRoomState GetRoomState(string roomId)
{
if (_mappedRooms.Contains(roomId)) return MapRoomState.Mapped;
if (_exploredRooms.Contains(roomId)) return MapRoomState.Explored;
return MapRoomState.Unknown;
}
public string CurrentRoomId => _currentRoomId;
}
public enum MapRoomState { Unknown, Explored, Mapped }
}
```
### MapDatabaseSO — 全局房间索引
```csharp
[CreateAssetMenu(menuName = "World/Map/Database")]
public class MapDatabaseSO : ScriptableObject
{
[SerializeField] MapRoomDataSO[] _rooms;
Dictionary<string, MapRoomDataSO> _dict;
public void BuildIndex()
=> _dict = _rooms.ToDictionary(r => r.roomId, r => r);
public MapRoomDataSO GetRoom(string roomId)
=> _dict.TryGetValue(roomId, out var r) ? r : null;
public IEnumerable<MapRoomDataSO> GetRoomsByRegion(string regionId)
=> _rooms.Where(r => r.regionId == regionId);
}
```
---
## 4. 雾战渲染Fog of War
地图采用**三级可见度**区分探索程度:
| 状态 | 触发条件 | 渲染效果 |
|------|---------|---------|
| `Unknown` | 从未进入 | 完全黑色(不显示轮廓)|
| `Explored` | 已进入但未存档 | 显示房间轮廓线(半透明灰色,填充黑色)|
| `Mapped` | 已进入且存档 | 完整显示(填充区域色,出口连接线)|
### 渲染实现
地图使用 **Unity UI Image + RenderTexture** 渲染,而非 World Space GameObject
```
MapUI Canvas
└── MapRawImage (RawImage显示 _mapRenderTexture)
└── 分辨率: 512×512整个大地图
```
`MapRenderer`Editor Script + Runtime Renderer
1. **Edit Time**`MapEditorTool`(见 §13根据 `MapRoomDataSO.roomOutlineTex` + `gridPosition` 将所有房间烘焙到一张 `MapAtlasTexture`
2. **Runtime**`MapRenderer``MapAtlasTexture` 基础上,叠加 `FogLayer`(黑色遮罩 Texture`MapManager` 揭露房间时,擦除对应格子的 `FogLayer`(填充透明)
3. 玩家位置图标通过 UI Image 覆盖在最上层,位置由格子坐标实时映射
### 雾战擦除算法
```csharp
void RevealRoom(string roomId)
{
var data = _database.GetRoom(roomId);
if (data == null) return;
// 将格子坐标映射到贴图像素范围
int px = data.gridPosition.x * CELL_SIZE_PX;
int py = data.gridPosition.y * CELL_SIZE_PX;
int w = data.gridSize.x * CELL_SIZE_PX;
int h = data.gridSize.y * CELL_SIZE_PX;
// 按状态设置不同 Alpha
float alpha = state == MapRoomState.Mapped ? 0f : 0.6f; // Mapped=全透明Explored=半透
Color[] pixels = _fogTexture.GetPixels(px, py, w, h);
for (int i = 0; i < pixels.Length; i++)
pixels[i] = new Color(0, 0, 0, alpha);
_fogTexture.SetPixels(px, py, w, h, pixels);
_fogTexture.Apply();
}
```
常量 `CELL_SIZE_PX = 32`(每个格子对应 32×32 像素)。
---
## 5. 地图 UI — 全屏地图
**Map 键**(默认 Tab打开全屏地图`GameState` 不变(仍为 Gameplay仅叠加地图 UI 层。
### Canvas 结构
```
Canvas_Map (Sorting Order: 25叠加在 HUD 之上)
└── MapPanel (全屏 Panel默认隐藏)
├── MapScrollView ← ScrollRect支持鼠标/摇杆滚动
│ └── MapContent
│ ├── MapRawImage ← 地图渲染RenderTexture
│ └── MapIconLayer ← 图钉/图标 UIRectTransform 层)
│ ├── PlayerIcon ← 玩家当前位置Image跟随格子坐标
│ ├── BossIcons[] ← Boss 房间图标
│ ├── SaveIcons[] ← 存档点图标
│ └── PinIcons[] ← 玩家图钉
├── RegionLegend ← 右侧区域图例(颜色说明)
├── CurrentRoomLabel ← 左上角当前房间名
└── CloseHint ← "按 Tab/B 关闭"提示
```
### 地图交互
| 操作 | 键盘 | 手柄 | 效果 |
|------|------|------|------|
| 打开/关闭地图 | Tab | Select | 切换显示 |
| 移动视角 | WASD / 方向键 | 左摇杆 | 滚动 MapScrollView |
| 缩放 | 鼠标滚轮 / Z/X | L2/R2 | Scale MapContent0.5× ~ 2×|
| 放置图钉 | F | Y| 在当前光标位置放置图钉 |
| 移除图钉 | F在图钉上| Y | 移除选中图钉 |
| 快速居中(玩家)| R | R3 | ScrollView 居中到玩家图标 |
---
## 6. 小地图 HUD
小地图始终显示在 HUD 右上角(游戏中常驻),仅显示当前区域的缩略图:
```
Canvas_HUD
└── MinimapPanel (右上角,固定位置)
├── MinimapMask (圆形 Mask裁剪显示范围)
│ └── MinimapImage (RawImage与全屏地图共享 RenderTexture但 ViewportRect 限制)
└── MinimapBorder (装饰边框)
```
- 小地图使用与全屏地图**相同的 RenderTexture**,通过 UV Offset + Scale 仅显示当前房间周围 5×5 格
- 玩家在当前格子的移动通过 `MinimapImage.uvRect.position` 实时微调(平滑偏移)
- 死亡/Boss 战时:小地图隐藏(`Canvas_HUD` 对应 GameObject 由 `UIManager` 控制)
---
## 7. 玩家位置追踪
`MapPlayerTracker` 维护玩家在地图中的像素坐标,供全屏地图和小地图使用:
```csharp
public class MapPlayerTracker : MonoBehaviour
{
[SerializeField] MapManager _mapManager;
[SerializeField] Transform _player;
// 注入玩家 Transform通过事件频道不直接 Find
void OnEnable() => _onPlayerSpawned.OnEventRaised += t => _player = t;
// 当前玩家在地图中的像素坐标(供 MapUI 设置 Image.rectTransform.anchoredPosition
public Vector2 GetPlayerMapPixelPos()
{
var room = _mapManager.Database.GetRoom(_mapManager.CurrentRoomId);
if (room == null || _player == null) return Vector2.zero;
// 房间格子左下角像素 + 玩家在房间内的相对位置0~1映射到像素范围
Vector2 roomOrigin = (Vector2)room.gridPosition * CELL_SIZE_PX;
Vector2 roomSize = (Vector2)room.gridSize * CELL_SIZE_PX;
// 玩家世界坐标 → 房间内归一化坐标(需要 RoomBoundsData 提供世界坐标范围)
Vector2 normalized = GetNormalizedPosInRoom(_player.position, room);
return roomOrigin + normalized * roomSize;
}
}
```
---
## 8. 地图图钉系统
玩家可在地图任意已探索位置放置图钉,用于标记未完成的密室、待回来的通道等。
### 图钉数据
```csharp
[Serializable]
public class MapPin
{
public string roomId; // 所在房间
public Vector2 normalizedPos; // 在房间内的归一化坐标0~1
public PinType type; // Exclamation / Question / Heart / Star
public string note; // 玩家可选输入的备注文字(最多 20 字)
}
public enum PinType { Exclamation, Question, Heart, Star }
```
### 图钉持久化
图钉数组存入 SaveData
```json
"mapPins": [
{ "roomId": "Room_Cave_03", "normalizedPos": { "x": 0.7, "y": 0.5 },
"type": "Question", "note": "有密室待探索" }
]
```
### 图钉 UI 预制件
```
MapPin_Prefab (World Space UI挂在 MapIconLayer)
├── PinIcon (Image按 PinType 切换 Sprite)
└── NoteTooltip (TextMeshPro鼠标悬停时显示)
```
---
## 9. 区域颜色与标记
每个区域在地图中使用不同颜色区分(颜色定义在 `RegionDefinitionSO.mapColor`
| 区域 | 颜色(参考)| 地图图标 |
|------|-----------|---------|
| Forest扎根森林| 绿色 `#4A7C3F` | 叶片图标 |
| Cave腐蚀洞穴| 紫色 `#6A3FA0` | 骨骼图标 |
| Ruins坍塌废墟| 棕色 `#8B6530` | 石柱图标 |
| Abyss深渊裂隙| 深蓝 `#1A2A4A` | 深渊图标 |
| Core核心熔炉| 红橙 `#C84B20` | 火焰图标 |
**特殊房间标记**(覆盖在格子上的图标层):
| 类型 | 图标 | 触发条件 |
|------|------|---------|
| Boss 房间 | 骷髅/特殊图标 | `MapRoomDataSO.isBossRoom = true` |
| 存档点 | 长椅/火焰图标 | `MapRoomDataSO.isSavePoint = true` |
| 商店 | 硬币图标 | `MapRoomDataSO.isShop = true` |
| 传送点 | 旋涡图标 | 由 `QuickTravelManager` 解锁后标记 |
---
## 10. SaveData 集成
`SaveData.world` 新增字段(在 `08_WorldSystem.md §5` JSON Schema 基础上扩展):
```json
"world": {
"discoveredRooms": ["Room_Forest_01", "Room_Forest_02"],
"mappedRooms": ["Room_Forest_01"],
"mapPins": [
{ "roomId": "Room_Cave_03", "normalizedPos": {"x":0.7,"y":0.5},
"type": "Question", "note": "密室" }
]
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `discoveredRooms` | `string[]` | 已进入的房间(轮廓显示)|
| `mappedRooms` | `string[]` | 存档后升级的房间(完整显示)|
| `mapPins` | `MapPin[]` | 玩家图钉列表 |
---
## 11. 场景搭建规范
每个房间场景添加 `RoomMapSetup` 组件,链接到对应 `MapRoomDataSO`
```
Room_Forest_01 (Scene)
└── [MapSetup] (GameObject)
└── RoomMapSetup.cs
└── _roomData: MapRoomDataSO ← 拖入 MR_Room_Forest_01.asset
```
`RoomMapSetup``Awake` 时通过事件频道向 `MapManager` 注册当前房间(实际是 `OnRoomEntered` 频道由 `RoomTransition` 触发,`RoomMapSetup` 不主动发布,只提供数据查询入口)。
---
## 12. 事件频道
| 频道资产 | 类型 | 发布方 | 主要订阅方 |
|---------|------|--------|----------|
| `OnRoomEntered.asset` | `StringEventChannelSO`roomId| `RoomTransition` | `MapManager``CameraStateController` |
| `OnMapOpened.asset` | `VoidEventChannelSO` | `InputReader` | `MapUI`(显示)、`InputReader`(切 ActionMap|
| `OnMapClosed.asset` | `VoidEventChannelSO` | `MapUI` | `InputReader`(恢复 ActionMap|
| `OnMapPinAdded.asset` | `MapPinEventChannelSO` | `MapUI` | `SaveManager` |
| `OnMapPinRemoved.asset` | `StringEventChannelSO`pinId| `MapUI` | `SaveManager` |
---
## 13. 编辑器友好设计
### MapEditorTool — EditorWindowUI Toolkit
路径:`Tools → Zeling → Map Editor`
实现:继承 `EditorWindow`,覆写 `CreateGUI()`,使用 `TwoPaneSplitView`(左侧房间列表 `ListView` + 右侧属性面板) + 自绘格子地图(`generateVisualContent` + `Painter2D`)。
功能:
```
┌─ Map Editor ──────────────────────────────────────────────┐
│ [自动扫描所有 MapRoomDataSO] [全部重新生成轮廓贴图] │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 地图预览(格子视图,颜色区分区域) │ │
│ │ │ │
│ │ [Forest] [Forest_01] [Forest_02] │ │
│ │ [Cave] [Cave_01] [Cave_02] [Cave_03] │ │
│ │ ... │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 选中 Room_Forest_01: │
│ gridPosition: (0, 0) gridSize: (2, 1) │
│ 区域: Forest 出口: [Right→Forest_02] │
│ [生成轮廓贴图] [定位场景] │
└────────────────────────────────────────────────────────────┘
```
### 轮廓贴图自动生成
`MapEditorTool` 对每个 `MapRoomDataSO` 自动生成 `roomOutlineTex`
1. 打开对应场景(`EditorSceneManager.OpenScene`,仅读取)
2. 在场景 Tilemap 上执行 `Physics2D.OverlapAreaAll` 扫描地形轮廓
3. 将轮廓绘制到 32×n px 的贴图并保存为 `.png``AssetDatabase.CreateAsset`
4. 自动赋值到 `MapRoomDataSO.roomOutlineTex`
### RoomMapSetup Inspector
- Inspector 中显示**预览图**(对应 `MapRoomDataSO.roomOutlineTex`
- 一键按钮「在地图编辑器中定位」,打开 MapEditorTool 并滚动到此房间
-`MapRoomDataSO` 未指定null显示红色警告`⚠ 缺少 MapRoomDataSO房间不会在地图中显示。`