地图系统
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using BaseGames.World.Map;
|
||||
|
||||
namespace BaseGames.Editor.Map
|
||||
{
|
||||
/// <summary>
|
||||
/// 地图运行时管理器脚手架(与 <see cref="BaseGames.Editor.UI.MapUIScaffoldWizard"/> 配套)。
|
||||
/// 在当前活动场景(应为 Persistent)的 [Services] 下放置并绑定地图服务,使其在 boot 注册:
|
||||
/// <list type="bullet">
|
||||
/// <item>MapManager(IMapService,ISaveable)— 绑 MapDatabase + EVT_RoomEntered/MapUpdated/RegionChanged</item>
|
||||
/// <item>MapPinManager(IPinService,ISaveable)— 无序列化字段</item>
|
||||
/// <item>TeleportService(ITeleportService,ISaveable)— 无序列化字段</item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// MapPlayerTracker(IPlayerPositionProvider)须挂在<strong>玩家</strong>对象上(依赖 _playerTransform),
|
||||
/// 不在此脚手架放置——见执行后的报告说明。缺它时地图仍会渲染房间,但无玩家定位点 / 传送完成回调。
|
||||
/// </para>
|
||||
/// 执行路径:BaseGames ▸ Scene ▸ Setup ▸ Scaffold Map Managers
|
||||
/// </summary>
|
||||
public static class MapManagersScaffoldWizard
|
||||
{
|
||||
[MenuItem("BaseGames/Scene/Setup/Scaffold Map Managers", priority = 205)]
|
||||
public static void ScaffoldMapManagers()
|
||||
{
|
||||
var report = new List<string>();
|
||||
Undo.SetCurrentGroupName("Scaffold Map Managers");
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
|
||||
Transform host = FindServicesHost(report);
|
||||
if (host == null)
|
||||
{
|
||||
Debug.LogWarning("[MapManagersScaffold] 未找到 [Services]/[GameManagers] 宿主,已中止。请先执行 Persistent 场景脚手架。");
|
||||
return;
|
||||
}
|
||||
|
||||
// ── MapManager ────────────────────────────────────────────────────
|
||||
var mapMgrGo = GetOrCreateChild(host, "MapManager").gameObject;
|
||||
var mapMgr = GetOrAddComponent<MapManager>(mapMgrGo);
|
||||
|
||||
var db = ResolveMapDatabase(report);
|
||||
if (db != null) AssignRef(mapMgr, "_database", db);
|
||||
AssignAsset(mapMgr, "_onRoomEntered", report, true, "EVT_RoomEntered");
|
||||
AssignAsset(mapMgr, "_onMapUpdated", report, false, "EVT_MapUpdated");
|
||||
AssignAsset(mapMgr, "_onRegionChanged", report, false, "EVT_RegionChanged");
|
||||
|
||||
// ── MapPinManager ─────────────────────────────────────────────────
|
||||
var pinGo = GetOrCreateChild(host, "MapPinManager").gameObject;
|
||||
GetOrAddComponent<MapPinManager>(pinGo);
|
||||
|
||||
// ── TeleportService ───────────────────────────────────────────────
|
||||
var teleGo = GetOrCreateChild(host, "TeleportService").gameObject;
|
||||
GetOrAddComponent<TeleportService>(teleGo);
|
||||
|
||||
report.Add("MapManager / MapPinManager / TeleportService 已放置于 " + GetPath(host) +
|
||||
"(boot 时注册 IMapService / IPinService / ITeleportService)。");
|
||||
report.Add("⚠ MapPlayerTracker(IPlayerPositionProvider)未放置:它依赖玩家 Transform,须挂在玩家对象/预制上," +
|
||||
"并设置 _playerTransform(玩家根)与 _databaseOverride(同一 MapDatabase)。缺它时无玩家定位点与传送完成回调。");
|
||||
report.Add("提示:MapDatabase 当前若为空(0 房间),地图不渲染任何房间属正常——需先用 Room Capture Baker + 建 MapRoomDataSO 填充。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
MarkDirtyAndLog("Map Managers 脚手架", host.gameObject, report);
|
||||
}
|
||||
|
||||
// ── 宿主 / 资产解析 ───────────────────────────────────────────────────
|
||||
|
||||
private static Transform FindServicesHost(List<string> report)
|
||||
{
|
||||
Scene scene = SceneManager.GetActiveScene();
|
||||
foreach (GameObject root in scene.GetRootGameObjects())
|
||||
{
|
||||
var s = root.transform.Find("[Services]");
|
||||
if (s != null) return s;
|
||||
var g = root.transform.Find("[GameManagers]");
|
||||
if (g != null) return g;
|
||||
if (root.name == "[Services]" || root.name == "[GameManagers]") return root.transform;
|
||||
}
|
||||
report.Add("未找到 [Services]/[GameManagers]。");
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MapDatabaseSO ResolveMapDatabase(List<string> report)
|
||||
{
|
||||
// 优先复用流式系统使用的 Database,保证 MapManager 与 RoomStreamingManager 同源
|
||||
var rsmType = System.Type.GetType("BaseGames.World.Streaming.RoomStreamingManager, BaseGames.World.Streaming");
|
||||
if (rsmType != null)
|
||||
{
|
||||
var rsm = Object.FindFirstObjectByType(rsmType) as Component;
|
||||
if (rsm != null)
|
||||
{
|
||||
var so = new SerializedObject(rsm);
|
||||
var it = so.GetIterator();
|
||||
while (it.NextVisible(true))
|
||||
{
|
||||
if (it.propertyType == SerializedPropertyType.ObjectReference &&
|
||||
it.objectReferenceValue is MapDatabaseSO foundDb)
|
||||
{
|
||||
report.Add($"MapManager._database 复用 RoomStreamingManager 的库:{AssetDatabase.GetAssetPath(foundDb)}");
|
||||
return foundDb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 退回:显式路径 / 全局搜索
|
||||
var byPath = AssetDatabase.LoadAssetAtPath<MapDatabaseSO>("Assets/_Game/Data/World/Map/MapDatabase.asset");
|
||||
if (byPath != null) { report.Add("MapManager._database = Assets/_Game/Data/World/Map/MapDatabase.asset"); return byPath; }
|
||||
|
||||
foreach (var guid in AssetDatabase.FindAssets("t:MapDatabaseSO"))
|
||||
{
|
||||
var p = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<MapDatabaseSO>(p);
|
||||
if (asset != null) { report.Add($"MapManager._database = {p}"); return asset; }
|
||||
}
|
||||
|
||||
report.Add("未找到任何 MapDatabaseSO,请手动给 MapManager._database 赋值。");
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── 通用辅助(对照 HUDScaffoldWizard)─────────────────────────────────
|
||||
|
||||
private static Transform GetOrCreateChild(Transform parent, string name)
|
||||
{
|
||||
var child = parent.Find(name);
|
||||
if (child != null) return child;
|
||||
var go = new GameObject(name);
|
||||
Undo.RegisterCreatedObjectUndo(go, $"Create {name}");
|
||||
go.transform.SetParent(parent, false);
|
||||
return go.transform;
|
||||
}
|
||||
|
||||
private static T GetOrAddComponent<T>(GameObject go) where T : Component
|
||||
{
|
||||
var c = go.GetComponent<T>();
|
||||
return c != null ? c : Undo.AddComponent<T>(go);
|
||||
}
|
||||
|
||||
private static void AssignRef(Object target, string propertyName, Object value)
|
||||
{
|
||||
var so = new SerializedObject(target);
|
||||
var prop = so.FindProperty(propertyName);
|
||||
if (prop == null)
|
||||
{
|
||||
Debug.LogWarning($"[MapManagersScaffold] 未找到属性 {target.GetType().Name}.{propertyName}", target);
|
||||
return;
|
||||
}
|
||||
prop.objectReferenceValue = value;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static void AssignAsset(Object target, string propertyName, List<string> report,
|
||||
bool required, params string[] candidates)
|
||||
{
|
||||
Object asset = FindFirstAsset(candidates);
|
||||
if (asset == null)
|
||||
{
|
||||
if (required)
|
||||
report.Add($"未找到 {target.GetType().Name}.{propertyName} 所需资产: {string.Join(" / ", candidates)}");
|
||||
return;
|
||||
}
|
||||
AssignRef(target, propertyName, asset);
|
||||
}
|
||||
|
||||
private static Object FindFirstAsset(params string[] candidates)
|
||||
{
|
||||
foreach (string candidate in candidates)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(candidate)) continue;
|
||||
foreach (string guid in AssetDatabase.FindAssets(candidate))
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
Object asset = AssetDatabase.LoadMainAssetAtPath(path);
|
||||
if (asset != null && asset.name == candidate) return asset;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetPath(Transform t)
|
||||
{
|
||||
var stack = new Stack<string>();
|
||||
for (var cur = t; cur != null; cur = cur.parent) stack.Push(cur.name);
|
||||
return string.Join("/", stack);
|
||||
}
|
||||
|
||||
private static void MarkDirtyAndLog(string scaffoldName, GameObject root, List<string> report)
|
||||
{
|
||||
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
|
||||
if (root != null) Selection.activeGameObject = root;
|
||||
if (report.Count == 0) { Debug.Log($"[MapManagersScaffold] {scaffoldName} 完成。", root); return; }
|
||||
Debug.LogWarning($"[MapManagersScaffold] {scaffoldName} 完成,以下 {report.Count} 项需手动确认:\n- {string.Join("\n- ", report)}", root);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7769d4ff7ea7ab499e78bea580eb99f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
468
Assets/_Game/Scripts/Editor/World/Map/MapRoomCaptureWindow.cs
Normal file
468
Assets/_Game/Scripts/Editor/World/Map/MapRoomCaptureWindow.cs
Normal file
@@ -0,0 +1,468 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using UnityEngine.SceneManagement;
|
||||
using BaseGames.Camera;
|
||||
using BaseGames.World.Map;
|
||||
|
||||
namespace BaseGames.Editor.Map
|
||||
{
|
||||
/// <summary>
|
||||
/// 房间底图截图烘焙器(地图系统美术管线第一步)。
|
||||
/// <para>
|
||||
/// 逐房间打开其对应场景,用正交相机按"玩家可视范围"(<see cref="CameraArea.VisibleBounds"/>)
|
||||
/// 渲染一张截图,输出 PNG 作为<strong>美术加工的底图</strong>——美术在此基础上描绘/风格化后,
|
||||
/// 覆盖同名 PNG 即可回到游戏(运行时由 <see cref="MapRoomDataSO.RoomOutlineTex"/> 显示)。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 本工具<strong>不</strong>生成风格化美术,只提供"分块场景截图"底图。
|
||||
/// 渲染走 URP(<see cref="UniversalRenderPipeline.SingleCameraRequest"/>),编辑器下离屏渲染。
|
||||
/// </para>
|
||||
/// 菜单:BaseGames/Map/Room Capture Baker
|
||||
/// </summary>
|
||||
public class MapRoomCaptureWindow : EditorWindow
|
||||
{
|
||||
[MenuItem("BaseGames/Map/Room Capture Baker", priority = 101)]
|
||||
public static void ShowWindow() => GetWindow<MapRoomCaptureWindow>("房间底图烘焙器");
|
||||
|
||||
// ── 配置 ──────────────────────────────────────────────────────────────
|
||||
private MapDatabaseSO _database;
|
||||
private string _outputFolder = "Assets/_Game/Art/Map/RoomCaptures";
|
||||
private float _pixelsPerUnit = 16f;
|
||||
private int _maxDimension = 2048;
|
||||
private bool _transparentBackground = true;
|
||||
private bool _assignToOutlineTex = true;
|
||||
private bool _addTempGlobalLight = true; // URP 2D 离屏渲染默认偏黑,截图时临时加全局光还原精灵真实色
|
||||
private float _lightIntensity = 1.2f;
|
||||
private float _worldUnitsPerCell = 18f; // 与 MapPlayerTracker 一致:1 格对应的世界单位
|
||||
private Vector2 _worldOriginOffset = Vector2.zero; // 与 MapPlayerTracker 一致:世界原点偏移
|
||||
private Vector2 _scroll;
|
||||
|
||||
// ── GUI ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("房间底图截图烘焙", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox(
|
||||
"逐房间打开其场景,用正交相机按可视范围(CameraArea)渲染截图 → 输出 PNG(默认透明背景)。\n" +
|
||||
"这是给美术加工的『底图』,不是最终美术;美术加工后覆盖同名 PNG 即回到游戏。\n" +
|
||||
"⚠ 烘焙会临时切换当前打开的场景,完成后自动恢复;请先保存未保存的修改。",
|
||||
MessageType.Info);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
_database = (MapDatabaseSO)EditorGUILayout.ObjectField("Map Database", _database, typeof(MapDatabaseSO), false);
|
||||
if (EditorGUI.EndChangeCheck()) Repaint();
|
||||
|
||||
_outputFolder = EditorGUILayout.TextField("输出目录", _outputFolder);
|
||||
_pixelsPerUnit = EditorGUILayout.Slider("每世界单位像素", _pixelsPerUnit, 4f, 64f);
|
||||
_maxDimension = EditorGUILayout.IntSlider("最大边像素", _maxDimension, 256, 4096);
|
||||
_transparentBackground = EditorGUILayout.Toggle("透明背景", _transparentBackground);
|
||||
_assignToOutlineTex = EditorGUILayout.Toggle(new GUIContent("回填 RoomOutlineTex(占位)",
|
||||
"勾选后将截图直接赋给房间的 RoomOutlineTex 作占位;美术覆盖同名 PNG 后自动生效。"), _assignToOutlineTex);
|
||||
_addTempGlobalLight = EditorGUILayout.Toggle(new GUIContent("临时全局光(推荐)",
|
||||
"URP 2D 离屏渲染下精灵偏黑;截图时临时加一盏全局 Light2D 还原真实色,结束即销毁。"), _addTempGlobalLight);
|
||||
using (new EditorGUI.DisabledScope(!_addTempGlobalLight))
|
||||
_lightIntensity = EditorGUILayout.Slider("全局光强度", _lightIntensity, 0.5f, 2f);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
bool hasRooms = _database != null && _database.AllRooms != null && _database.AllRooms.Length > 0;
|
||||
using (new EditorGUI.DisabledScope(!hasRooms))
|
||||
{
|
||||
if (GUILayout.Button("烘焙全部房间", GUILayout.Height(28)))
|
||||
BakeAll();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("格子布局派生(GridPosition / GridSize)", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox(
|
||||
"按房间场景的可视范围(CameraArea,缺则用渲染包围盒)÷ 世界单位/格,自动推导 GridPosition/GridSize 写入 MapRoomDataSO,免去手填。\n" +
|
||||
"下方参数须与 Persistent 中 MapPlayerTracker 的 _worldUnitsPerCell / _worldOriginOffset 一致;调整后重新派生即可校准房间占格大小。",
|
||||
MessageType.None);
|
||||
_worldUnitsPerCell = EditorGUILayout.FloatField(new GUIContent("世界单位/格", "与 MapPlayerTracker._worldUnitsPerCell 一致"), _worldUnitsPerCell);
|
||||
_worldOriginOffset = EditorGUILayout.Vector2Field(new GUIContent("世界原点偏移", "与 MapPlayerTracker._worldOriginOffset 一致"), _worldOriginOffset);
|
||||
if (_worldUnitsPerCell < 0.01f) _worldUnitsPerCell = 0.01f;
|
||||
using (new EditorGUI.DisabledScope(!hasRooms))
|
||||
{
|
||||
if (GUILayout.Button("派生全部房间格子布局", GUILayout.Height(24)))
|
||||
DeriveGridForRooms(AllRoomsList());
|
||||
}
|
||||
|
||||
if (!hasRooms)
|
||||
{
|
||||
EditorGUILayout.HelpBox("请指定含有房间的 MapDatabaseSO。", MessageType.None);
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField($"房间({_database.AllRooms.Length})", EditorStyles.boldLabel);
|
||||
_scroll = EditorGUILayout.BeginScrollView(_scroll);
|
||||
foreach (var room in _database.AllRooms)
|
||||
{
|
||||
if (room == null) continue;
|
||||
DrawRoomRow(room);
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawRoomRow(MapRoomDataSO room)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
|
||||
|
||||
// 预览缩略图
|
||||
Rect thumb = GUILayoutUtility.GetRect(40, 40, GUILayout.Width(40), GUILayout.Height(40));
|
||||
if (room.RoomOutlineTex != null) GUI.DrawTexture(thumb, room.RoomOutlineTex, ScaleMode.ScaleToFit);
|
||||
else EditorGUI.DrawRect(thumb, new Color(0f, 0f, 0f, 0.2f));
|
||||
|
||||
string scenePath = ResolveScenePath(room.RoomId);
|
||||
|
||||
EditorGUILayout.BeginVertical();
|
||||
EditorGUILayout.LabelField(string.IsNullOrEmpty(room.RoomId) ? "(无 RoomId)" : room.RoomId, EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField(scenePath != null ? Path.GetFileName(scenePath) : "⚠ 未找到同名场景", EditorStyles.miniLabel);
|
||||
EditorGUILayout.LabelField($"格子 ({room.GridPosition.x},{room.GridPosition.y}) / {room.GridSize.x}×{room.GridSize.y}", EditorStyles.miniLabel);
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
using (new EditorGUI.DisabledScope(scenePath == null))
|
||||
{
|
||||
if (GUILayout.Button("烘焙", GUILayout.Width(52), GUILayout.Height(50)))
|
||||
BakeRooms(new List<MapRoomDataSO> { room });
|
||||
if (GUILayout.Button("派生格子", GUILayout.Width(60), GUILayout.Height(50)))
|
||||
DeriveGridForRooms(new List<MapRoomDataSO> { room });
|
||||
}
|
||||
if (GUILayout.Button("定位", GUILayout.Width(44), GUILayout.Height(50)))
|
||||
{
|
||||
Selection.activeObject = room;
|
||||
EditorGUIUtility.PingObject(room);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// ── 烘焙流程 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void BakeAll()
|
||||
{
|
||||
BakeRooms(AllRoomsList());
|
||||
}
|
||||
|
||||
private List<MapRoomDataSO> AllRoomsList()
|
||||
{
|
||||
var rooms = new List<MapRoomDataSO>();
|
||||
if (_database?.AllRooms != null)
|
||||
foreach (var r in _database.AllRooms)
|
||||
if (r != null) rooms.Add(r);
|
||||
return rooms;
|
||||
}
|
||||
|
||||
// ── 格子布局派生 ──────────────────────────────────────────────────────
|
||||
|
||||
private void DeriveGridForRooms(List<MapRoomDataSO> rooms)
|
||||
{
|
||||
if (rooms == null || rooms.Count == 0) return;
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return;
|
||||
|
||||
var setup = EditorSceneManager.GetSceneManagerSetup();
|
||||
int done = 0, skipped = 0;
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < rooms.Count; i++)
|
||||
{
|
||||
var room = rooms[i];
|
||||
if (room == null) { skipped++; continue; }
|
||||
|
||||
string scenePath = ResolveScenePath(room.RoomId);
|
||||
if (scenePath == null)
|
||||
{
|
||||
Debug.LogWarning($"[RoomGrid] '{room.RoomId}':未找到同名场景,跳过。");
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
EditorUtility.DisplayProgressBar("派生格子布局",
|
||||
$"{room.RoomId}({i + 1}/{rooms.Count})", (float)i / rooms.Count);
|
||||
|
||||
var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
|
||||
if (DeriveGrid(room, scene)) done++; else skipped++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
if (setup != null && setup.Length > 0)
|
||||
EditorSceneManager.RestoreSceneManagerSetup(setup);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
Debug.Log($"[RoomGrid] 派生完成:成功 {done},跳过 {skipped}(世界单位/格={_worldUnitsPerCell},原点偏移={_worldOriginOffset})。");
|
||||
Repaint();
|
||||
}
|
||||
|
||||
/// <summary>按场景可视范围 ÷ 世界单位/格,推导 GridPosition/GridSize 并写入房间 SO(floor 下界、ceil 上界,取最小覆盖格)。</summary>
|
||||
private bool DeriveGrid(MapRoomDataSO room, Scene scene)
|
||||
{
|
||||
Rect b = ResolveCaptureBounds(scene);
|
||||
if (b.width <= 0.01f || b.height <= 0.01f)
|
||||
{
|
||||
Debug.LogWarning($"[RoomGrid] '{room.RoomId}':场景中无 CameraArea / 可见 Renderer,无法推导,跳过。");
|
||||
return false;
|
||||
}
|
||||
|
||||
int gx = Mathf.FloorToInt((b.xMin - _worldOriginOffset.x) / _worldUnitsPerCell);
|
||||
int gy = Mathf.FloorToInt((b.yMin - _worldOriginOffset.y) / _worldUnitsPerCell);
|
||||
int gx2 = Mathf.CeilToInt ((b.xMax - _worldOriginOffset.x) / _worldUnitsPerCell);
|
||||
int gy2 = Mathf.CeilToInt ((b.yMax - _worldOriginOffset.y) / _worldUnitsPerCell);
|
||||
|
||||
var pos = new Vector2Int(gx, gy);
|
||||
var size = new Vector2Int(Mathf.Max(1, gx2 - gx), Mathf.Max(1, gy2 - gy));
|
||||
|
||||
Undo.RecordObject(room, "Derive Map Grid Layout");
|
||||
var so = new SerializedObject(room);
|
||||
so.FindProperty("GridPosition").vector2IntValue = pos;
|
||||
so.FindProperty("GridSize").vector2IntValue = size;
|
||||
so.ApplyModifiedProperties();
|
||||
EditorUtility.SetDirty(room);
|
||||
Debug.Log($"[RoomGrid] '{room.RoomId}' → GridPos=({pos.x},{pos.y}) GridSize=({size.x},{size.y})");
|
||||
return true;
|
||||
}
|
||||
|
||||
private void BakeRooms(List<MapRoomDataSO> rooms)
|
||||
{
|
||||
if (rooms == null || rooms.Count == 0) return;
|
||||
|
||||
// 未保存修改保护:让用户决定保存/取消
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
|
||||
return;
|
||||
|
||||
var setup = EditorSceneManager.GetSceneManagerSetup(); // 记录当前场景布局,结束后恢复
|
||||
EnsureFolder(_outputFolder);
|
||||
|
||||
int done = 0, skipped = 0;
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < rooms.Count; i++)
|
||||
{
|
||||
var room = rooms[i];
|
||||
if (room == null) { skipped++; continue; }
|
||||
|
||||
string scenePath = ResolveScenePath(room.RoomId);
|
||||
if (scenePath == null)
|
||||
{
|
||||
Debug.LogWarning($"[RoomCapture] '{room.RoomId}':未找到同名场景,跳过。");
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
EditorUtility.DisplayProgressBar("房间底图烘焙",
|
||||
$"{room.RoomId}({i + 1}/{rooms.Count})", (float)i / rooms.Count);
|
||||
|
||||
var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
|
||||
if (CaptureRoom(room, scene)) done++; else skipped++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
if (setup != null && setup.Length > 0)
|
||||
EditorSceneManager.RestoreSceneManagerSetup(setup);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
Debug.Log($"[RoomCapture] 完成:成功 {done},跳过 {skipped}。输出 → {_outputFolder}");
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private bool CaptureRoom(MapRoomDataSO room, Scene scene)
|
||||
{
|
||||
Rect bounds = ResolveCaptureBounds(scene);
|
||||
if (bounds.width <= 0.01f || bounds.height <= 0.01f)
|
||||
{
|
||||
Debug.LogWarning($"[RoomCapture] '{room.RoomId}':场景中无 CameraArea / 可见 Renderer,无法取景,跳过。");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 分辨率:按比例 + 限制最大边
|
||||
int w = Mathf.Max(1, Mathf.RoundToInt(bounds.width * _pixelsPerUnit));
|
||||
int h = Mathf.Max(1, Mathf.RoundToInt(bounds.height * _pixelsPerUnit));
|
||||
float clamp = Mathf.Min(1f, (float)_maxDimension / Mathf.Max(w, h));
|
||||
w = Mathf.Max(1, Mathf.RoundToInt(w * clamp));
|
||||
h = Mathf.Max(1, Mathf.RoundToInt(h * clamp));
|
||||
|
||||
RenderTexture rt = null;
|
||||
GameObject camGo = null;
|
||||
GameObject lightGo = null;
|
||||
RenderTexture prevActive = RenderTexture.active;
|
||||
try
|
||||
{
|
||||
rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32);
|
||||
|
||||
// URP 2D 离屏渲染默认偏黑:临时全局 Light2D 还原精灵真实色,finally 中销毁
|
||||
if (_addTempGlobalLight)
|
||||
{
|
||||
lightGo = new GameObject("__MapCaptureLight") { hideFlags = HideFlags.HideAndDontSave };
|
||||
var l2d = lightGo.AddComponent<Light2D>();
|
||||
l2d.lightType = Light2D.LightType.Global;
|
||||
l2d.intensity = _lightIntensity;
|
||||
l2d.color = Color.white;
|
||||
}
|
||||
|
||||
camGo = new GameObject("__MapCaptureCamera") { hideFlags = HideFlags.HideAndDontSave };
|
||||
var cam = camGo.AddComponent<UnityEngine.Camera>();
|
||||
cam.orthographic = true;
|
||||
cam.orthographicSize = bounds.height * 0.5f;
|
||||
cam.aspect = bounds.width / bounds.height;
|
||||
cam.transform.position = new Vector3(bounds.center.x, bounds.center.y, -100f);
|
||||
cam.nearClipPlane = 0.01f;
|
||||
cam.farClipPlane = 1000f;
|
||||
cam.clearFlags = CameraClearFlags.SolidColor;
|
||||
cam.backgroundColor = _transparentBackground
|
||||
? new Color(0f, 0f, 0f, 0f)
|
||||
: new Color(0.10f, 0.10f, 0.12f, 1f);
|
||||
int uiMask = LayerMask.GetMask("UI");
|
||||
cam.cullingMask = uiMask != 0 ? ~uiMask : ~0; // 不拍 UI 层
|
||||
cam.targetTexture = rt;
|
||||
|
||||
RenderCameraURP(cam, rt);
|
||||
|
||||
RenderTexture.active = rt;
|
||||
var tex = new Texture2D(w, h, TextureFormat.RGBA32, false);
|
||||
tex.ReadPixels(new Rect(0f, 0f, w, h), 0, 0);
|
||||
tex.Apply();
|
||||
byte[] png = tex.EncodeToPNG();
|
||||
Object.DestroyImmediate(tex);
|
||||
|
||||
string assetPath = $"{_outputFolder}/{room.RoomId}.png";
|
||||
File.WriteAllBytes(Path.Combine(Directory.GetCurrentDirectory(), assetPath), png);
|
||||
|
||||
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
|
||||
ConfigureTextureImport(assetPath);
|
||||
|
||||
if (_assignToOutlineTex)
|
||||
{
|
||||
var imported = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
|
||||
if (imported != null)
|
||||
{
|
||||
room.RoomOutlineTex = imported;
|
||||
EditorUtility.SetDirty(room);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[RoomCapture] '{room.RoomId}' 截图失败:{ex.Message}\n{ex.StackTrace}");
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderTexture.active = prevActive;
|
||||
if (rt != null) Object.DestroyImmediate(rt);
|
||||
if (camGo != null) Object.DestroyImmediate(camGo);
|
||||
if (lightGo != null) Object.DestroyImmediate(lightGo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>URP 离屏渲染:优先官方 SubmitRenderRequest(URP 14),回退 Camera.Render()。</summary>
|
||||
private static void RenderCameraURP(UnityEngine.Camera cam, RenderTexture rt)
|
||||
{
|
||||
var request = new UniversalRenderPipeline.SingleCameraRequest { destination = rt };
|
||||
if (RenderPipeline.SupportsRenderRequest(cam, request))
|
||||
RenderPipeline.SubmitRenderRequest(cam, request);
|
||||
else
|
||||
cam.Render(); // 非 URP 或不支持时回退
|
||||
}
|
||||
|
||||
// ── 取景范围 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 解析场景取景范围(世界 Rect):
|
||||
/// ① 全部 CameraArea.VisibleBounds 的并集(最贴近玩家所见);
|
||||
/// ② 退回到场景内所有可见 Renderer 的包围盒。
|
||||
/// </summary>
|
||||
private static Rect ResolveCaptureBounds(Scene scene)
|
||||
{
|
||||
bool has = false;
|
||||
Rect union = default;
|
||||
foreach (var area in GetComponentsInScene<CameraArea>(scene))
|
||||
{
|
||||
if (area == null) continue;
|
||||
var vb = area.VisibleBounds;
|
||||
union = has ? Encapsulate(union, vb) : vb;
|
||||
has = true;
|
||||
}
|
||||
if (has) return union;
|
||||
|
||||
bool hb = false;
|
||||
Bounds b = default;
|
||||
foreach (var r in GetComponentsInScene<Renderer>(scene))
|
||||
{
|
||||
if (r == null || !r.enabled) continue;
|
||||
if (r.bounds.size == Vector3.zero) continue;
|
||||
if (!hb) { b = r.bounds; hb = true; }
|
||||
else b.Encapsulate(r.bounds);
|
||||
}
|
||||
return hb ? new Rect(b.min.x, b.min.y, b.size.x, b.size.y) : new Rect(0f, 0f, 0f, 0f);
|
||||
}
|
||||
|
||||
// ── 工具方法 ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>按 RoomId 解析同名场景资产路径;未找到返回 null。</summary>
|
||||
private static string ResolveScenePath(string roomId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(roomId)) return null;
|
||||
foreach (var guid in AssetDatabase.FindAssets($"t:Scene {roomId}"))
|
||||
{
|
||||
string p = AssetDatabase.GUIDToAssetPath(guid);
|
||||
if (Path.GetFileNameWithoutExtension(p) == roomId) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ConfigureTextureImport(string assetPath)
|
||||
{
|
||||
if (AssetImporter.GetAtPath(assetPath) is not TextureImporter imp) return;
|
||||
imp.textureType = TextureImporterType.Default;
|
||||
imp.alphaIsTransparency = true;
|
||||
imp.mipmapEnabled = false;
|
||||
imp.SaveAndReimport();
|
||||
}
|
||||
|
||||
private static void EnsureFolder(string folder)
|
||||
{
|
||||
if (string.IsNullOrEmpty(folder) || AssetDatabase.IsValidFolder(folder)) return;
|
||||
var parts = folder.Split('/');
|
||||
string cur = parts[0]; // "Assets"
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
string next = $"{cur}/{parts[i]}";
|
||||
if (!AssetDatabase.IsValidFolder(next))
|
||||
AssetDatabase.CreateFolder(cur, parts[i]);
|
||||
cur = next;
|
||||
}
|
||||
}
|
||||
|
||||
private static Rect Encapsulate(Rect a, Rect b)
|
||||
{
|
||||
float xMin = Mathf.Min(a.xMin, b.xMin);
|
||||
float yMin = Mathf.Min(a.yMin, b.yMin);
|
||||
float xMax = Mathf.Max(a.xMax, b.xMax);
|
||||
float yMax = Mathf.Max(a.yMax, b.yMax);
|
||||
return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
|
||||
}
|
||||
|
||||
private static List<T> GetComponentsInScene<T>(Scene scene) where T : Component
|
||||
{
|
||||
var result = new List<T>();
|
||||
if (!scene.IsValid()) return result;
|
||||
foreach (var go in scene.GetRootGameObjects())
|
||||
result.AddRange(go.GetComponentsInChildren<T>(true));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d75062a836c9174597612aad318aa60
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user