地图系统

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,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>MapManagerIMapServiceISaveable— 绑 MapDatabase + EVT_RoomEntered/MapUpdated/RegionChanged</item>
/// <item>MapPinManagerIPinServiceISaveable— 无序列化字段</item>
/// <item>TeleportServiceITeleportServiceISaveable— 无序列化字段</item>
/// </list>
/// <para>
/// MapPlayerTrackerIPlayerPositionProvider须挂在<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("⚠ MapPlayerTrackerIPlayerPositionProvider未放置它依赖玩家 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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7769d4ff7ea7ab499e78bea580eb99f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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 并写入房间 SOfloor 下界、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 离屏渲染:优先官方 SubmitRenderRequestURP 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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d75062a836c9174597612aad318aa60
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: