摄像机区域的架构改动

This commit is contained in:
2026-05-15 14:47:24 +08:00
parent 1b37297585
commit f264329751
3591 changed files with 1687228 additions and 446503 deletions

View File

@@ -0,0 +1,277 @@
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Unity.Cinemachine;
using BaseGames.Camera;
namespace BaseGames.Editor
{
/// <summary>
/// CameraArea 自定义 Inspector + Scene GUI。
///
/// 功能与 <see cref="RoomCameraEditor"/> 一致:
/// 1. Scene 视图中直接拖拽黄色矩形四条边,编辑「可视区域」(_visibleBounds)。
/// 2. Inspector 按钮「从可视区域更新限位区域(透视)」:
/// 根据 FOV 和相机深度计算 PolygonCollider2D 限位范围并写入。
///
/// FOV 优先级(降序):
/// 专有 DedicatedCamera.Lens.FieldOfView
/// → CameraStateController._vcamAPersistent 场景)
/// → Camera.main.fieldOfView
/// → 60f默认
/// </summary>
[CustomEditor(typeof(CameraArea))]
public class CameraAreaEditor : UnityEditor.Editor
{
// ── 颜色常量 ──────────────────────────────────────────────────────────
private static readonly Color kVisibleFill = new Color(1f, 0.85f, 0.15f, 0.08f);
private static readonly Color kVisibleOutline = new Color(1f, 0.85f, 0.15f, 0.90f);
private static readonly Color kConfinerColor = new Color(0.2f, 0.8f, 1.0f, 0.80f);
// ══ Inspector ═════════════════════════════════════════════════════════
public override void OnInspectorGUI()
{
DrawDefaultInspector();
EditorGUILayout.Space(8f);
EditorGUILayout.LabelField("── 可视区域工具 ──", EditorStyles.boldLabel);
CameraArea area = (CameraArea)target;
float vFOV = GetFOV(area);
float aspect = GetAspect();
float depth = area.CameraDepth;
float halfH = depth * Mathf.Tan(vFOV * 0.5f * Mathf.Deg2Rad);
float halfW = halfH * aspect;
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.FloatField("垂直 FOV来源见工具提示", vFOV);
EditorGUILayout.FloatField("有效深度", depth);
EditorGUILayout.FloatField("视口半高(世界单位)", halfH);
EditorGUILayout.FloatField("视口半宽(世界单位)", halfW);
}
bool canSync = area.ConfinerCollider != null;
if (!canSync)
EditorGUILayout.HelpBox("ConfinerCollider 未绑定,无法同步限位区域。", MessageType.Warning);
using (new EditorGUI.DisabledScope(!canSync))
{
if (GUILayout.Button("从可视区域更新限位区域(透视)", GUILayout.Height(28f)))
SyncConfinerFromVisibleBounds(area, vFOV, aspect);
}
// ── 图例说明 ─────────────────────────────────────────────────────
EditorGUILayout.Space(4f);
DrawLegend("■ 黄色矩形Scene 视图)", kVisibleOutline, "可视区域 — 摄像机视口永不超出此范围");
DrawLegend("■ 蓝色多边形Scene 视图)", kConfinerColor, "限位区域 — CinemachineConfiner2D 的运动边界");
}
// ══ Scene GUI ════════════════════════════════════════════════════════
private void OnSceneGUI()
{
CameraArea area = (CameraArea)target;
serializedObject.Update();
SerializedProperty boundsP = serializedObject.FindProperty("_visibleBounds");
Rect r = boundsP.rectValue;
// ── 绘制限位多边形(蓝色,参考用) ──────────────────────────────
DrawConfinerGizmo(area);
// ── 绘制可视区域填充 + 边框 ──────────────────────────────────────
DrawVisibleRect(r);
// ── 四条边的拖拽 Handle ──────────────────────────────────────────
EditorGUI.BeginChangeCheck();
EditRectEdges(ref r);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(area, "Edit Visible Bounds");
boundsP.rectValue = r;
serializedObject.ApplyModifiedProperties();
}
}
// ══ 绘制辅助 ═════════════════════════════════════════════════════════
private static void DrawVisibleRect(Rect r)
{
Vector3[] corners =
{
new Vector3(r.xMin, r.yMin, 0f),
new Vector3(r.xMin, r.yMax, 0f),
new Vector3(r.xMax, r.yMax, 0f),
new Vector3(r.xMax, r.yMin, 0f),
};
Handles.DrawSolidRectangleWithOutline(corners, kVisibleFill, kVisibleOutline);
Handles.color = kVisibleOutline;
Handles.Label(
new Vector3(r.xMin + 0.15f, r.yMax - 0.15f, 0f),
"Visible Area",
EditorStyles.miniLabel);
}
private static void DrawConfinerGizmo(CameraArea area)
{
var poly = area.ConfinerCollider;
if (poly == null || poly.pathCount == 0) return;
int ptCount = poly.GetTotalPointCount();
if (ptCount < 2) return;
var pts2 = new System.Collections.Generic.List<Vector2>(ptCount);
poly.GetPath(0, pts2);
var pts3 = new Vector3[ptCount + 1];
for (int i = 0; i < ptCount; i++)
pts3[i] = poly.transform.TransformPoint(pts2[i]);
pts3[ptCount] = pts3[0];
Handles.color = kConfinerColor;
Handles.DrawPolyLine(pts3);
Handles.Label(
(Vector3)poly.transform.TransformPoint(pts2[0]) + new Vector3(0.1f, 0.1f),
"Confiner",
EditorStyles.miniLabel);
}
/// <summary>绘制四条边的滑动 Handle允许用户直接拖拽修改可视区域。</summary>
private static void EditRectEdges(ref Rect r)
{
float hs = HandleUtility.GetHandleSize(r.center) * 0.10f;
Handles.color = kVisibleOutline;
// 左边 —— 沿 X 轴滑动
EditorGUI.BeginChangeCheck();
Vector3 lp = Handles.Slider(
new Vector3(r.xMin, r.center.y, 0f),
Vector3.right, hs, Handles.RectangleHandleCap, EditorSnapSettings.move.x);
if (EditorGUI.EndChangeCheck())
r.xMin = Mathf.Min(lp.x, r.xMax - 0.1f);
// 右边 —— 沿 X 轴滑动
EditorGUI.BeginChangeCheck();
Vector3 rp = Handles.Slider(
new Vector3(r.xMax, r.center.y, 0f),
Vector3.right, hs, Handles.RectangleHandleCap, EditorSnapSettings.move.x);
if (EditorGUI.EndChangeCheck())
r.xMax = Mathf.Max(rp.x, r.xMin + 0.1f);
// 下边 —— 沿 Y 轴滑动
EditorGUI.BeginChangeCheck();
Vector3 bp = Handles.Slider(
new Vector3(r.center.x, r.yMin, 0f),
Vector3.up, hs, Handles.RectangleHandleCap, EditorSnapSettings.move.y);
if (EditorGUI.EndChangeCheck())
r.yMin = Mathf.Min(bp.y, r.yMax - 0.1f);
// 上边 —— 沿 Y 轴滑动
EditorGUI.BeginChangeCheck();
Vector3 tp = Handles.Slider(
new Vector3(r.center.x, r.yMax, 0f),
Vector3.up, hs, Handles.RectangleHandleCap, EditorSnapSettings.move.y);
if (EditorGUI.EndChangeCheck())
r.yMax = Mathf.Max(tp.y, r.yMin + 0.1f);
}
// ══ 透视同步逻辑 ══════════════════════════════════════════════════════
private static void SyncConfinerFromVisibleBounds(CameraArea area, float vFOV, float aspect)
{
var poly = area.ConfinerCollider;
if (poly == null)
{
Debug.LogWarning($"[CameraAreaEditor] {area.name}ConfinerCollider 未绑定,无法同步。");
return;
}
Rect visible = area.VisibleBounds;
float depth = area.CameraDepth;
float halfH = depth * Mathf.Tan(vFOV * 0.5f * Mathf.Deg2Rad);
float halfW = halfH * aspect;
float xMin = visible.xMin + halfW;
float xMax = visible.xMax - halfW;
float yMin = visible.yMin + halfH;
float yMax = visible.yMax - halfH;
// 房间小于单屏 → 相机锁定在可视区域中心
if (xMin > xMax) { float cx = visible.center.x; xMin = xMax = cx; }
if (yMin > yMax) { float cy = visible.center.y; yMin = yMax = cy; }
Transform polyT = poly.transform;
Vector2 Local(Vector3 w) => polyT.InverseTransformPoint(w);
Undo.RecordObject(poly, "Sync Confiner from Visible Bounds");
poly.SetPath(0, new[]
{
Local(new Vector3(xMin, yMin, 0f)),
Local(new Vector3(xMin, yMax, 0f)),
Local(new Vector3(xMax, yMax, 0f)),
Local(new Vector3(xMax, yMin, 0f)),
});
EditorUtility.SetDirty(poly);
Debug.Log(
$"[CameraAreaEditor] {area.name}:限位区域已同步。\n" +
$" 可视区域:{visible}\n" +
$" FOV={vFOV:F1}° Depth={depth:F1} HalfView=({halfW:F2}, {halfH:F2})\n" +
$" 限位区域:({xMin:F2}, {yMin:F2}) ~ ({xMax:F2}, {yMax:F2})");
}
// ══ 工具方法 ══════════════════════════════════════════════════════════
/// <summary>
/// 获取用于透视计算的 FOV优先级专有 VCam → 全局 VCamA → Camera.main → 60f
/// </summary>
private static float GetFOV(CameraArea area)
{
// 1. 区域专有 VCam
if (area.DedicatedCamera != null)
return area.DedicatedCamera.Lens.FieldOfView;
// 2. Persistent 场景中的 CameraStateController._vcamA通过反射读取私有字段
#pragma warning disable UNT0023 // FindObjectOfType 在编辑器工具中可接受
var ctrl = Object.FindObjectOfType<CameraStateController>();
#pragma warning restore UNT0023
if (ctrl != null)
{
var fi = typeof(CameraStateController).GetField(
"_vcamA", BindingFlags.Instance | BindingFlags.NonPublic);
if (fi != null && fi.GetValue(ctrl) is CinemachineCamera vcamA && vcamA != null)
return vcamA.Lens.FieldOfView;
}
// 3. Camera.main
if (UnityEngine.Camera.main != null)
return UnityEngine.Camera.main.fieldOfView;
// 4. 默认
return 60f;
}
private static float GetAspect()
{
if (UnityEngine.Camera.main != null) return UnityEngine.Camera.main.aspect;
return 16f / 9f;
}
private static void DrawLegend(string text, Color color, string tooltip)
{
using (new EditorGUILayout.HorizontalScope())
{
Color prev = GUI.color;
GUI.color = color;
GUILayout.Label("■", GUILayout.Width(14f));
GUI.color = prev;
EditorGUILayout.LabelField(new GUIContent(text, tooltip));
}
}
}
}

View File

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

View File

@@ -0,0 +1,268 @@
using UnityEditor;
using UnityEngine;
using Unity.Cinemachine;
using BaseGames.Camera;
namespace BaseGames.Editor
{
/// <summary>
/// RoomCamera 自定义 Inspector + Scene GUI。
///
/// 功能:
/// 1. Scene 视图中直接拖拽黄色矩形的四条边,编辑「可视区域」(_visibleBounds)。
/// 2. Inspector 按钮「从可视区域更新限位区域(透视)」:
/// 根据 CinemachineCamera.Lens.FieldOfView 和摄像机深度,计算出
/// CinemachineConfiner2D 所需的限位多边形并写入子节点 PolygonCollider2D。
///
/// 透视相机限位公式:
/// halfH = depth × tan(vFOV / 2)
/// halfW = halfH × aspectRatio
/// confiner = visibleBounds inset by (halfW, halfH)
/// → 相机视口边缘恰好与可视区域边框对齐。
/// → 若房间小于单屏,限位收缩为中心点(相机固定居中)。
/// </summary>
[CustomEditor(typeof(RoomCamera))]
public class RoomCameraEditor : UnityEditor.Editor
{
// ── 颜色常量 ──────────────────────────────────────────────────────────
private static readonly Color kVisibleFill = new Color(1f, 0.85f, 0.15f, 0.08f);
private static readonly Color kVisibleOutline = new Color(1f, 0.85f, 0.15f, 0.90f);
private static readonly Color kConfinerColor = new Color(0.2f, 0.8f, 1.0f, 0.80f);
// ══ Inspector ═════════════════════════════════════════════════════════
public override void OnInspectorGUI()
{
DrawDefaultInspector();
EditorGUILayout.Space(8f);
EditorGUILayout.LabelField("── 可视区域工具 ──", EditorStyles.boldLabel);
RoomCamera rc = (RoomCamera)target;
var vcam = rc.GetComponent<CinemachineCamera>();
var confiner = rc.GetComponent<CinemachineConfiner2D>();
// ── 透视参数预览 ─────────────────────────────────────────────────
float vFOV = vcam != null ? vcam.Lens.FieldOfView : 60f;
float aspect = GetAspect();
float depth = rc.CameraDepth;
float halfH = depth * Mathf.Tan(vFOV * 0.5f * Mathf.Deg2Rad);
float halfW = halfH * aspect;
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.FloatField("垂直 FOV来自 Lens", vFOV);
EditorGUILayout.FloatField("有效深度", depth);
EditorGUILayout.FloatField("视口半高(世界单位)", halfH);
EditorGUILayout.FloatField("视口半宽(世界单位)", halfW);
}
bool canSync = rc.ConfinerCollider != null;
if (!canSync)
EditorGUILayout.HelpBox("ConfinerCollider 未绑定_visibleArea 为空),无法同步限位区域。", MessageType.Warning);
using (new EditorGUI.DisabledScope(!canSync))
{
if (GUILayout.Button("从可视区域更新限位区域(透视)", GUILayout.Height(28f)))
SyncConfinerFromVisibleBounds(rc, vFOV, aspect);
}
// ── 图例说明 ─────────────────────────────────────────────────────
EditorGUILayout.Space(4f);
DrawLegend("■ 黄色矩形Scene 视图)", kVisibleOutline, "可视区域 — 摄像机视口永不超出此范围");
DrawLegend("■ 蓝色多边形Scene 视图)", kConfinerColor, "限位区域 — CinemachineConfiner2D 的运动边界");
}
// ══ Scene GUI ════════════════════════════════════════════════════════
private void OnSceneGUI()
{
RoomCamera rc = (RoomCamera)target;
serializedObject.Update();
SerializedProperty boundsP = serializedObject.FindProperty("_visibleBounds");
Rect r = boundsP.rectValue;
// ── 绘制限位多边形(蓝色,参考用) ──────────────────────────────
DrawConfinerGizmo(rc);
// ── 绘制可视区域填充 + 边框 ──────────────────────────────────────
DrawVisibleRect(r);
// ── 四条边的拖拽 Handle ──────────────────────────────────────────
EditorGUI.BeginChangeCheck();
EditRectEdges(ref r);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(rc, "Edit Visible Bounds");
boundsP.rectValue = r;
serializedObject.ApplyModifiedProperties();
}
}
// ══ 绘制辅助 ═════════════════════════════════════════════════════════
private static void DrawVisibleRect(Rect r)
{
Vector3[] corners =
{
new Vector3(r.xMin, r.yMin, 0f),
new Vector3(r.xMin, r.yMax, 0f),
new Vector3(r.xMax, r.yMax, 0f),
new Vector3(r.xMax, r.yMin, 0f),
};
Handles.DrawSolidRectangleWithOutline(corners, kVisibleFill, kVisibleOutline);
// 标签
Handles.color = kVisibleOutline;
Handles.Label(
new Vector3(r.xMin + 0.15f, r.yMax - 0.15f, 0f),
"Visible Area",
EditorStyles.miniLabel);
}
private static void DrawConfinerGizmo(RoomCamera rc)
{
var poly = rc.ConfinerCollider;
if (poly == null || poly.pathCount == 0) return;
int ptCount = poly.GetTotalPointCount();
if (ptCount < 2) return;
var pts2 = new System.Collections.Generic.List<Vector2>(ptCount);
poly.GetPath(0, pts2);
var pts3 = new Vector3[ptCount + 1];
for (int i = 0; i < ptCount; i++)
pts3[i] = poly.transform.TransformPoint(pts2[i]);
pts3[ptCount] = pts3[0];
Handles.color = kConfinerColor;
Handles.DrawPolyLine(pts3);
Handles.Label(
(Vector3)poly.transform.TransformPoint(pts2[0]) + new Vector3(0.1f, 0.1f),
"Confiner",
EditorStyles.miniLabel);
}
/// <summary>绘制四条边的滑动 Handle允许用户直接拖拽修改可视区域。</summary>
private static void EditRectEdges(ref Rect r)
{
float hs = HandleUtility.GetHandleSize(r.center) * 0.10f;
Handles.color = kVisibleOutline;
// 左边 —— 沿 X 轴滑动
EditorGUI.BeginChangeCheck();
Vector3 lp = Handles.Slider(
new Vector3(r.xMin, r.center.y, 0f),
Vector3.right, hs, Handles.RectangleHandleCap, EditorSnapSettings.move.x);
if (EditorGUI.EndChangeCheck())
{
// 保持 xMax 不变xMin 向右最多到 xMax-0.1
r.xMin = Mathf.Min(lp.x, r.xMax - 0.1f);
}
// 右边 —— 沿 X 轴滑动
EditorGUI.BeginChangeCheck();
Vector3 rp = Handles.Slider(
new Vector3(r.xMax, r.center.y, 0f),
Vector3.right, hs, Handles.RectangleHandleCap, EditorSnapSettings.move.x);
if (EditorGUI.EndChangeCheck())
{
r.xMax = Mathf.Max(rp.x, r.xMin + 0.1f);
}
// 下边 —— 沿 Y 轴滑动
EditorGUI.BeginChangeCheck();
Vector3 bp = Handles.Slider(
new Vector3(r.center.x, r.yMin, 0f),
Vector3.up, hs, Handles.RectangleHandleCap, EditorSnapSettings.move.y);
if (EditorGUI.EndChangeCheck())
{
r.yMin = Mathf.Min(bp.y, r.yMax - 0.1f);
}
// 上边 —— 沿 Y 轴滑动
EditorGUI.BeginChangeCheck();
Vector3 tp = Handles.Slider(
new Vector3(r.center.x, r.yMax, 0f),
Vector3.up, hs, Handles.RectangleHandleCap, EditorSnapSettings.move.y);
if (EditorGUI.EndChangeCheck())
{
r.yMax = Mathf.Max(tp.y, r.yMin + 0.1f);
}
}
// ══ 透视同步逻辑 ══════════════════════════════════════════════════════
/// <summary>
/// 根据可视区域矩形与透视参数计算限位多边形,写入 PolygonCollider2D。
/// </summary>
private static void SyncConfinerFromVisibleBounds(RoomCamera rc, float vFOV, float aspect)
{
var poly = rc.ConfinerCollider;
if (poly == null)
{
Debug.LogWarning($"[RoomCameraEditor] {rc.name}ConfinerCollider 未绑定,无法同步。");
return;
}
Rect visible = rc.VisibleBounds;
float depth = rc.CameraDepth;
float halfH = depth * Mathf.Tan(vFOV * 0.5f * Mathf.Deg2Rad);
float halfW = halfH * aspect;
float xMin = visible.xMin + halfW;
float xMax = visible.xMax - halfW;
float yMin = visible.yMin + halfH;
float yMax = visible.yMax - halfH;
// 房间小于单屏 → 相机锁定在可视区域中心
if (xMin > xMax) { float cx = visible.center.x; xMin = xMax = cx; }
if (yMin > yMax) { float cy = visible.center.y; yMin = yMax = cy; }
Transform polyT = poly.transform;
Vector2 Local(Vector3 w) => polyT.InverseTransformPoint(w);
Undo.RecordObject(poly, "Sync Confiner from Visible Bounds");
poly.SetPath(0, new[]
{
Local(new Vector3(xMin, yMin, 0f)),
Local(new Vector3(xMin, yMax, 0f)),
Local(new Vector3(xMax, yMax, 0f)),
Local(new Vector3(xMax, yMin, 0f)),
});
EditorUtility.SetDirty(poly);
Debug.Log(
$"[RoomCameraEditor] {rc.name}:限位区域已同步。\n" +
$" 可视区域:{visible}\n" +
$" FOV={vFOV:F1}° Depth={depth:F1} HalfView=({halfW:F2}, {halfH:F2})\n" +
$" 限位区域:({xMin:F2}, {yMin:F2}) ~ ({xMax:F2}, {yMax:F2})");
}
// ══ 工具方法 ══════════════════════════════════════════════════════════
/// <summary>
/// 获取 Game 视图宽高比。编辑器中优先用 Camera.main否则回退到 16:9。
/// </summary>
private static float GetAspect()
{
if (UnityEngine.Camera.main != null) return UnityEngine.Camera.main.aspect;
return 16f / 9f;
}
private static void DrawLegend(string text, Color color, string tooltip)
{
using (new EditorGUILayout.HorizontalScope())
{
Color prev = GUI.color;
GUI.color = color;
GUILayout.Label("■", GUILayout.Width(14f));
GUI.color = prev;
EditorGUILayout.LabelField(new GUIContent(text, tooltip));
}
}
}
}

View File

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

View File

@@ -0,0 +1,866 @@
using System.Collections.Generic;
using BaseGames.Camera;
using Unity.Cinemachine;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Tilemaps;
namespace BaseGames.Editor
{
/// <summary>
/// 相机区域配置工具窗口。
/// 扫描当前已加载场景中的所有 CameraArea / CameraTriggerZone / CameraStateController
/// 显示各组件的绑定状态,并提供一键修复快捷操作。
///
/// 菜单BaseGames → Camera → Camera Area Setup
/// </summary>
public class CameraAreaSetupTool : EditorWindow
{
// ── 状态 ──────────────────────────────────────────────────────────────
private Vector2 _scroll;
private List<CameraArea> _cameraAreas = new List<CameraArea>();
private List<CameraTriggerZone> _triggerZones = new List<CameraTriggerZone>();
private CameraStateController _controller;
// ── GUI 样式缓存 ──────────────────────────────────────────────────────
private GUIStyle _boxStyle;
// ══ 菜单入口 ══════════════════════════════════════════════════════════
[MenuItem("BaseGames/Camera/Camera Area Setup", priority = 100)]
public static void ShowWindow()
{
var win = GetWindow<CameraAreaSetupTool>("Camera Area Setup");
win.minSize = new Vector2(420f, 300f);
win.Show();
}
// ══ EditorWindow 生命周期 ═════════════════════════════════════════════
private void OnEnable() => RescanScene();
private void OnHierarchyChange() => RescanScene();
private void OnFocus() => RescanScene();
// ══ 场景扫描 ══════════════════════════════════════════════════════════
private void RescanScene()
{
_cameraAreas.Clear();
_triggerZones.Clear();
_controller = null;
for (int i = 0; i < SceneManager.sceneCount; i++)
{
Scene scene = SceneManager.GetSceneAt(i);
if (!scene.isLoaded) continue;
foreach (GameObject root in scene.GetRootGameObjects())
{
_cameraAreas.AddRange(root.GetComponentsInChildren<CameraArea>(true));
_triggerZones.AddRange(root.GetComponentsInChildren<CameraTriggerZone>(true));
if (_controller == null)
_controller = root.GetComponentInChildren<CameraStateController>(true);
}
}
Repaint();
}
// ══ GUI ═══════════════════════════════════════════════════════════════
private void OnGUI()
{
EnsureStyles();
// ── 工具栏 ─────────────────────────────────────────────────────
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
if (GUILayout.Button("↻ 刷新", EditorStyles.toolbarButton, GUILayout.Width(56)))
RescanScene();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Place Camera Area", EditorStyles.toolbarButton))
EditorApplication.ExecuteMenuItem("BaseGames/Scene/Place/Camera Area");
if (GUILayout.Button("Place Trigger Zone", EditorStyles.toolbarButton))
EditorApplication.ExecuteMenuItem("BaseGames/Scene/Place/Camera Trigger Zone");
}
_scroll = EditorGUILayout.BeginScrollView(_scroll);
// ── CameraStateController ───────────────────────────────────────
DrawSectionHeader("CameraStateControllerPersistent 场景)");
DrawControllerSection();
EditorGUILayout.Space(8f);
// ── CameraArea 列表 ─────────────────────────────────────────────
DrawSectionHeader($"Camera Areas [{_cameraAreas.Count}]");
DrawCameraAreasSection();
EditorGUILayout.Space(8f);
// ── CameraTriggerZone 列表 ──────────────────────────────────────
DrawSectionHeader($"Camera Trigger Zones [{_triggerZones.Count}]");
DrawTriggerZonesSection();
EditorGUILayout.EndScrollView();
}
// ── CameraStateController ──────────────────────────────────────────
private void DrawControllerSection()
{
if (_controller == null)
{
EditorGUILayout.HelpBox(
"当前已加载场景中未找到 CameraStateController正常。\n" +
"该组件位于 Persistent 场景,单独编辑房间场景时不会加载。\n" +
"进入 Play Mode 前请确保 Persistent 场景已一同加载。",
MessageType.Info);
return;
}
using (new EditorGUILayout.VerticalScope(_boxStyle))
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.LabelField("GameObject", GUILayout.Width(120f));
EditorGUILayout.ObjectField(_controller.gameObject, typeof(GameObject), true);
}
SerializedObject so = new SerializedObject(_controller);
DrawFieldCheck(so, "_vcamA", "全局 VCam A (CinemachineCamera)");
DrawFieldCheck(so, "_vcamB", "全局 VCam B (CinemachineCamera)");
DrawFieldCheck(so, "_brain", "CinemachineBrain");
DrawFieldCheck(so, "_impulseSource", "CinemachineImpulseSource", optional: true);
DrawFieldCheck(so, "_defaultBlendProfile","默认混合配置 (CameraBlendProfileSO)", optional: true);
EditorGUILayout.Space(4f);
if (GUILayout.Button("为全局 VCam 赋值 Follow 目标Player/CameraFollowTarget", GUILayout.Height(24f)))
AssignFollowToGlobalVCams(so);
}
}
// ── CameraArea 列表 ────────────────────────────────────────────────
private void DrawCameraAreasSection()
{
if (_cameraAreas.Count == 0)
{
EditorGUILayout.HelpBox(
"场景中未找到 CameraArea 组件。\n使用工具栏 \"Place Camera Area\" 快速生成。",
MessageType.Info);
return;
}
foreach (var area in _cameraAreas)
{
if (area == null) continue;
DrawCameraAreaEntry(area);
EditorGUILayout.Space(2f);
}
}
private void DrawCameraAreaEntry(CameraArea area)
{
SerializedObject so = new SerializedObject(area);
bool confinerOk = so.FindProperty("_confinerCollider").objectReferenceValue != null;
bool dedicatedSet = so.FindProperty("_dedicatedCamera").objectReferenceValue != null;
bool allOk = confinerOk;
using (new EditorGUILayout.VerticalScope(_boxStyle))
{
// 标题行
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label(allOk ? "✅" : "⚠", GUILayout.Width(20f));
if (GUILayout.Button(area.gameObject.name, EditorStyles.boldLabel, GUILayout.ExpandWidth(true)))
Selection.activeGameObject = area.gameObject;
if (GUILayout.Button("选中", GUILayout.Width(40f)))
Selection.activeGameObject = area.gameObject;
}
EditorGUILayout.Space(2f);
DrawCheckRow("_confinerCollider (PolygonCollider2D)", confinerOk);
DrawCheckRow("_dedicatedCamera专有 VCam可选", dedicatedSet, optional: true);
DrawCheckRow("_blendProfile可选未设则用全局默认",
so.FindProperty("_blendProfile").objectReferenceValue != null, optional: true);
if (!confinerOk)
{
EditorGUILayout.Space(2f);
if (GUILayout.Button("修复:绑定子节点 PolygonCollider2D", GUILayout.Height(20f)))
FixConfinerBinding(area);
}
}
}
// ── CameraTriggerZone 列表 ─────────────────────────────────────────
private void DrawTriggerZonesSection()
{
if (_triggerZones.Count == 0)
{
EditorGUILayout.HelpBox(
"场景中未找到 CameraTriggerZone。\n" +
"至少需要一个触发器来在运行时激活 CameraArea。\n" +
"使用工具栏 \"Place Trigger Zone\" 快速生成。",
MessageType.Info);
return;
}
foreach (var zone in _triggerZones)
{
if (zone == null) continue;
DrawTriggerZoneEntry(zone);
}
}
private void DrawTriggerZoneEntry(CameraTriggerZone zone)
{
SerializedObject so = new SerializedObject(zone);
bool hasTarget = so.FindProperty("_targetArea").objectReferenceValue != null;
using (new EditorGUILayout.HorizontalScope(_boxStyle))
{
GUILayout.Label(hasTarget ? "✅" : "❌", GUILayout.Width(20f));
if (GUILayout.Button(zone.gameObject.name, EditorStyles.label, GUILayout.ExpandWidth(true)))
Selection.activeGameObject = zone.gameObject;
if (!hasTarget)
EditorGUILayout.LabelField("⚠ _targetArea 未绑定!", GUILayout.Width(160f));
if (GUILayout.Button("选中", GUILayout.Width(40f)))
Selection.activeGameObject = zone.gameObject;
}
}
// ══ 自动修复操作 ═══════════════════════════════════════════════════════
/// <summary>
/// 查找场景中 tag=Player 的 Player/CameraFollowTarget
/// 写入 CameraStateController._vcamA 和 _vcamB 的 Follow 字段。
/// </summary>
private static void AssignFollowToGlobalVCams(SerializedObject controllerSO)
{
GameObject player = GameObject.FindWithTag("Player");
if (player == null)
{
Debug.LogWarning("[CameraAreaSetupTool] 场景中未找到 tag=Player 的对象。" +
"请先放置 PlayerBaseGames → Scene → Place → Player。");
return;
}
const string followNodeName = "CameraFollowTarget";
Transform followTarget = player.transform.Find(followNodeName);
if (followTarget == null)
{
var go = new GameObject(followNodeName);
Undo.RegisterCreatedObjectUndo(go, "Create CameraFollowTarget");
Undo.SetTransformParent(go.transform, player.transform, "Parent CameraFollowTarget");
go.transform.localPosition = Vector3.zero;
go.transform.localRotation = Quaternion.identity;
go.transform.localScale = Vector3.one;
followTarget = go.transform;
Debug.Log($"[CameraAreaSetupTool] 已在 Player 下自动创建 {followNodeName} 子节点。");
}
int count = 0;
foreach (string fieldName in new[] { "_vcamA", "_vcamB" })
{
var vcamProp = controllerSO.FindProperty(fieldName);
if (vcamProp?.objectReferenceValue is CinemachineCamera vcam)
{
Undo.RecordObject(vcam, "Assign Camera Follow Target");
vcam.Follow = followTarget;
EditorUtility.SetDirty(vcam);
count++;
}
}
if (count > 0)
Debug.Log($"[CameraAreaSetupTool] 已为 {count} 台全局 VCam 赋值 Follow → {followTarget.name}。");
else
Debug.LogWarning("[CameraAreaSetupTool] _vcamA/_vcamB 均未绑定,无法赋值 Follow。请先在 Inspector 中绑定。");
}
/// <summary>将子节点中找到的第一个 PolygonCollider2D 绑定到 CameraArea._confinerCollider。</summary>
private static void FixConfinerBinding(CameraArea area)
{
PolygonCollider2D poly = area.GetComponentInChildren<PolygonCollider2D>(true);
if (poly == null)
{
Debug.LogWarning($"[CameraAreaSetupTool] {area.name}:子节点中未找到 PolygonCollider2D。");
return;
}
SerializedObject so = new SerializedObject(area);
so.FindProperty("_confinerCollider").objectReferenceValue = poly;
so.ApplyModifiedProperties();
Debug.Log($"[CameraAreaSetupTool] {area.name}_confinerCollider → {poly.gameObject.name}");
}
// ══ GUI 辅助 ═══════════════════════════════════════════════════════════
private void EnsureStyles()
{
if (_boxStyle == null)
{
_boxStyle = new GUIStyle(GUI.skin.box)
{
padding = new RectOffset(6, 6, 4, 4),
};
}
}
private static void DrawSectionHeader(string title)
{
EditorGUILayout.Space(4f);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label(title, EditorStyles.boldLabel);
}
Rect r = EditorGUILayout.GetControlRect(false, 1f);
EditorGUI.DrawRect(r, new Color(0.4f, 0.4f, 0.4f, 1f));
EditorGUILayout.Space(2f);
}
private static void DrawFieldCheck(SerializedObject so, string propName, string displayName, bool optional = false)
{
var prop = so.FindProperty(propName);
bool ok = prop != null && prop.objectReferenceValue != null;
DrawCheckRow(displayName, ok, optional);
}
private static void DrawCheckRow(string label, bool ok, bool optional = false)
{
using (new EditorGUILayout.HorizontalScope())
{
Color prev = GUI.color;
GUI.color = ok
? new Color(0.4f, 1f, 0.4f)
: (optional ? new Color(0.8f, 0.8f, 0.4f) : new Color(1f, 0.4f, 0.4f));
GUILayout.Label(ok ? "●" : (optional ? "◌" : "✗"), GUILayout.Width(16f));
GUI.color = prev;
EditorGUILayout.LabelField(label);
}
}
}
}
namespace BaseGames.Editor
{
/// <summary>
/// 区域相机配置工具窗口。
/// 扫描当前已加载场景中的所有 RoomCamera / CameraTriggerZone / CameraStateController
/// 显示各组件的绑定状态,并提供一键修复快捷操作。
///
/// 菜单BaseGames → Camera → Room Camera Setup
/// </summary>
public class RoomCameraSetupTool : EditorWindow
{
// ── 状态 ──────────────────────────────────────────────────────────────
private Vector2 _scroll;
private List<RoomCamera> _roomCameras = new List<RoomCamera>();
private List<CameraTriggerZone> _triggerZones = new List<CameraTriggerZone>();
private CameraStateController _controller;
// ── GUI 样式缓存 ──────────────────────────────────────────────────────
private GUIStyle _boxStyle;
// ══ 菜单入口 ══════════════════════════════════════════════════════════
[MenuItem("BaseGames/Camera/Room Camera Setup", priority = 100)]
public static void ShowWindow()
{
var win = GetWindow<RoomCameraSetupTool>("Room Camera Setup");
win.minSize = new Vector2(420f, 300f);
win.Show();
}
// ══ EditorWindow 生命周期 ═════════════════════════════════════════════
private void OnEnable() => RescanScene();
private void OnHierarchyChange() => RescanScene();
private void OnFocus() => RescanScene();
// ══ 场景扫描 ══════════════════════════════════════════════════════════
private void RescanScene()
{
_roomCameras.Clear();
_triggerZones.Clear();
_controller = null;
for (int i = 0; i < SceneManager.sceneCount; i++)
{
Scene scene = SceneManager.GetSceneAt(i);
if (!scene.isLoaded) continue;
foreach (GameObject root in scene.GetRootGameObjects())
{
_roomCameras.AddRange(root.GetComponentsInChildren<RoomCamera>(true));
_triggerZones.AddRange(root.GetComponentsInChildren<CameraTriggerZone>(true));
if (_controller == null)
_controller = root.GetComponentInChildren<CameraStateController>(true);
}
}
Repaint();
}
// ══ GUI ═══════════════════════════════════════════════════════════════
private void OnGUI()
{
EnsureStyles();
// ── 工具栏 ─────────────────────────────────────────────────────
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
if (GUILayout.Button("↻ 刷新", EditorStyles.toolbarButton, GUILayout.Width(56)))
RescanScene();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Place Room Camera", EditorStyles.toolbarButton))
EditorApplication.ExecuteMenuItem("BaseGames/Scene/Place/Room Camera");
if (GUILayout.Button("Place Trigger Zone", EditorStyles.toolbarButton))
EditorApplication.ExecuteMenuItem("BaseGames/Scene/Place/Camera Trigger Zone");
}
_scroll = EditorGUILayout.BeginScrollView(_scroll);
// ── CameraStateController ───────────────────────────────────────
DrawSectionHeader("CameraStateController持久场景");
DrawControllerSection();
EditorGUILayout.Space(8f);
// ── RoomCamera 列表 ─────────────────────────────────────────────
DrawSectionHeader($"Room Cameras [{_roomCameras.Count}]");
DrawRoomCamerasSection();
EditorGUILayout.Space(8f);
// ── CameraTriggerZone 列表 ──────────────────────────────────────
DrawSectionHeader($"Camera Trigger Zones [{_triggerZones.Count}]");
DrawTriggerZonesSection();
EditorGUILayout.EndScrollView();
}
// ── CameraStateController ──────────────────────────────────────────
private void DrawControllerSection()
{
if (_controller == null)
{
EditorGUILayout.HelpBox(
"当前已加载场景中未找到 CameraStateController正常。\n" +
"该组件位于 Persistent 场景,单独编辑房间场景时不会加载。\n" +
"进入 Play Mode 前请确保 Persistent 场景已一同加载。",
MessageType.Info);
return;
}
using (new EditorGUILayout.VerticalScope(_boxStyle))
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.LabelField("GameObject", GUILayout.Width(120f));
EditorGUILayout.ObjectField(_controller.gameObject, typeof(GameObject), true);
}
SerializedObject so = new SerializedObject(_controller);
DrawFieldCheck(so, "_brain", "CinemachineBrain");
DrawFieldCheck(so, "_impulseSource", "CinemachineImpulseSource");
DrawFieldCheck(so, "_defaultBlendProfile","默认混合配置 (CameraBlendProfileSO)", optional: true);
}
}
// ── RoomCamera 列表 ────────────────────────────────────────────────
private void DrawRoomCamerasSection()
{
if (_roomCameras.Count == 0)
{
EditorGUILayout.HelpBox(
"场景中未找到 RoomCamera 组件。\n使用工具栏 \"Place Room Camera\" 快速生成。",
MessageType.Info);
return;
}
// 批量操作
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("批量赋值 Follow (Player)", GUILayout.Height(22f)))
BatchAssignFollowTarget();
if (GUILayout.Button("批量修复 Confiner 绑定", GUILayout.Height(22f)))
BatchFixConfinerBinding();
}
EditorGUILayout.Space(4f);
foreach (var cam in _roomCameras)
{
if (cam == null) continue;
DrawRoomCameraEntry(cam);
EditorGUILayout.Space(2f);
}
}
private void DrawRoomCameraEntry(RoomCamera cam)
{
if (cam == null) return;
SerializedObject camSO = new SerializedObject(cam);
CinemachineCamera vcam = cam.GetComponent<CinemachineCamera>();
CinemachineConfiner2D confiner = cam.GetComponent<CinemachineConfiner2D>();
bool visibleAreaOk = camSO.FindProperty("_visibleArea").objectReferenceValue != null;
bool confinerCompOk = confiner != null;
bool confinerBoundOk = confiner != null &&
new SerializedObject(confiner).FindProperty("m_BoundingShape2D").objectReferenceValue != null;
bool followOk = vcam != null && vcam.Follow != null;
bool blendOk = camSO.FindProperty("_blendProfile").objectReferenceValue != null;
bool allOk = visibleAreaOk && confinerCompOk && confinerBoundOk && followOk;
using (new EditorGUILayout.VerticalScope(_boxStyle))
{
// 标题行
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label(allOk ? "✅" : "⚠", GUILayout.Width(20f));
if (GUILayout.Button(cam.gameObject.name, EditorStyles.boldLabel, GUILayout.ExpandWidth(true)))
Selection.activeGameObject = cam.gameObject;
if (GUILayout.Button("选中", GUILayout.Width(40f)))
Selection.activeGameObject = cam.gameObject;
}
EditorGUILayout.Space(2f);
// 状态行
DrawCheckRow("_visibleArea (RoomVisibleArea)", visibleAreaOk);
DrawCheckRow("CinemachineConfiner2D 组件", confinerCompOk);
DrawCheckRow("Confiner2D.m_BoundingShape2D 已绑定", confinerBoundOk);
DrawCheckRow("CinemachineCamera.Follow (Player/CameraFollowTarget)", followOk);
DrawCheckRow("_blendProfile (未设则用全局默认,可选)", blendOk, optional: true);
// 修复按钮区
bool needFix = !followOk || !confinerBoundOk || !visibleAreaOk;
if (needFix)
{
EditorGUILayout.Space(2f);
using (new EditorGUILayout.HorizontalScope())
{
if (!followOk)
if (GUILayout.Button("赋值 Follow", GUILayout.Height(20f)))
AssignFollowTarget(cam);
if (!confinerBoundOk && confiner != null)
if (GUILayout.Button("修复 Confiner 绑定", GUILayout.Height(20f)))
FixConfinerBinding(cam, confiner);
if (!visibleAreaOk)
if (GUILayout.Button("修复 VisibleArea", GUILayout.Height(20f)))
FixVisibleArea(cam);
}
}
// Tilemap 适配按钮(始终可见,因为有时需要重新适配)
EditorGUILayout.Space(2f);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("以 Ground Tilemap 范围调整边界", GUILayout.Height(20f)))
FitConfinerToGroundTilemaps(cam);
if (!blendOk)
{
EditorGUILayout.HelpBox(
"_blendProfile 未设置,切换时使用控制器全局默认混合配置。",
MessageType.None);
}
}
}
}
// ── CameraTriggerZone 列表 ─────────────────────────────────────────
private void DrawTriggerZonesSection()
{
if (_triggerZones.Count == 0)
{
EditorGUILayout.HelpBox(
"场景中未找到 CameraTriggerZone。\n" +
"至少需要一个触发器来在运行时激活 RoomCamera。\n" +
"使用工具栏 \"Place Trigger Zone\" 快速生成。",
MessageType.Info);
return;
}
foreach (var zone in _triggerZones)
{
if (zone == null) continue;
DrawTriggerZoneEntry(zone);
}
}
private void DrawTriggerZoneEntry(CameraTriggerZone zone)
{
SerializedObject so = new SerializedObject(zone);
bool hasTarget = so.FindProperty("_targetCamera").objectReferenceValue != null;
using (new EditorGUILayout.HorizontalScope(_boxStyle))
{
GUILayout.Label(hasTarget ? "✅" : "❌", GUILayout.Width(20f));
if (GUILayout.Button(zone.gameObject.name, EditorStyles.label, GUILayout.ExpandWidth(true)))
Selection.activeGameObject = zone.gameObject;
if (!hasTarget)
EditorGUILayout.LabelField("⚠ _targetCamera 未绑定!", GUILayout.Width(160f));
if (GUILayout.Button("选中", GUILayout.Width(40f)))
Selection.activeGameObject = zone.gameObject;
}
}
// ══ 自动修复操作 ═══════════════════════════════════════════════════════
/// <summary>为所有未设置 Follow 的 RoomCamera 自动绑定场景中 tag=Player 的 Transform。</summary>
private void BatchAssignFollowTarget()
{
int count = 0;
foreach (var cam in _roomCameras)
{
if (cam == null) continue;
if (AssignFollowTarget(cam)) count++;
}
if (count > 0)
Debug.Log($"[RoomCameraSetupTool] 批量赋值:已为 {count} 台 RoomCamera 赋值 Follow 目标。");
else
Debug.Log("[RoomCameraSetupTool] 批量赋值:所有 RoomCamera 均已设置 Follow无需修改。");
}
/// <summary>为所有 Confiner2D.m_BoundingShape2D 未绑定的相机自动绑定子节点 PolygonCollider2D。</summary>
private void BatchFixConfinerBinding()
{
int count = 0;
foreach (var cam in _roomCameras)
{
if (cam == null) continue;
var confiner = cam.GetComponent<CinemachineConfiner2D>();
if (confiner == null) continue;
var so = new SerializedObject(confiner);
if (so.FindProperty("m_BoundingShape2D").objectReferenceValue == null)
{
FixConfinerBinding(cam, confiner);
count++;
}
}
Debug.Log($"[RoomCameraSetupTool] 批量修复:已修复 {count} 台 RoomCamera 的 Confiner 绑定。");
}
/// <summary>
/// 在场景中查找 tag=Player 的 GameObject
/// 再在其下寻找名为 "CameraFollowTarget" 的子节点并赋给 CinemachineCamera.Follow。
/// 子节点不存在时会自动创建。
/// </summary>
private bool AssignFollowTarget(RoomCamera cam)
{
CinemachineCamera vcam = cam.GetComponent<CinemachineCamera>();
if (vcam == null || vcam.Follow != null) return false;
GameObject player = GameObject.FindWithTag("Player");
if (player == null)
{
Debug.LogWarning("[RoomCameraSetupTool] 场景中未找到 tag=Player 的对象,无法自动赋值 Follow。" +
"请先放置 Player 对象BaseGames → Scene → Place → Player。");
return false;
}
const string followNodeName = "CameraFollowTarget";
Transform followTarget = player.transform.Find(followNodeName);
if (followTarget == null)
{
// 子节点不存在则自动创建,位置归零
var go = new GameObject(followNodeName);
Undo.RegisterCreatedObjectUndo(go, "Create CameraFollowTarget");
Undo.SetTransformParent(go.transform, player.transform, "Parent CameraFollowTarget");
go.transform.localPosition = Vector3.zero;
go.transform.localRotation = Quaternion.identity;
go.transform.localScale = Vector3.one;
followTarget = go.transform;
Debug.Log($"[RoomCameraSetupTool] 已在 Player 下自动创建 {followNodeName} 子节点。");
}
Undo.RecordObject(vcam, "Assign Camera Follow Target");
vcam.Follow = followTarget;
EditorUtility.SetDirty(vcam);
return true;
}
/// <summary>将子节点中找到的第一个 PolygonCollider2D 绑定到 CinemachineConfiner2D。</summary>
private void FixConfinerBinding(RoomCamera cam, CinemachineConfiner2D confiner)
{
PolygonCollider2D poly = cam.GetComponentInChildren<PolygonCollider2D>(true);
if (poly == null)
{
Debug.LogWarning($"[RoomCameraSetupTool] {cam.name}:子节点中未找到 PolygonCollider2D。" +
"请确保 RoomBoundary 子对象存在(使用 Place Room Camera 创建)。");
return;
}
SerializedObject so = new SerializedObject(confiner);
so.FindProperty("m_BoundingShape2D").objectReferenceValue = poly;
so.ApplyModifiedProperties();
Debug.Log($"[RoomCameraSetupTool] {cam.name}Confiner2D.m_BoundingShape2D → {poly.gameObject.name}");
}
/// <summary>将子节点中找到的 RoomVisibleArea 绑定到 RoomCamera._visibleArea。</summary>
private void FixVisibleArea(RoomCamera cam)
{
RoomVisibleArea existing = cam.GetComponentInChildren<RoomVisibleArea>(true);
if (existing == null)
{
Debug.LogWarning($"[RoomCameraSetupTool] {cam.name}:子节点中未找到 RoomVisibleArea。" +
"请确保 RoomBoundary 子对象存在(使用 Place Room Camera 创建)。");
return;
}
SerializedObject so = new SerializedObject(cam);
so.FindProperty("_visibleArea").objectReferenceValue = existing;
so.ApplyModifiedProperties();
Debug.Log($"[RoomCameraSetupTool] {cam.name}_visibleArea → {existing.gameObject.name}");
}
/// <summary>
/// 以场景中所有 Ground 层 Tilemap 的世界空间包围盒(合并后)来调整
/// RoomCamera 子节点 RoomBoundary 的 PolygonCollider2D 顶点,实现一键适配房间边界。
/// </summary>
private void FitConfinerToGroundTilemaps(RoomCamera cam)
{
PolygonCollider2D poly = cam.GetComponentInChildren<PolygonCollider2D>(true);
if (poly == null)
{
Debug.LogWarning($"[RoomCameraSetupTool] {cam.name}:子节点中未找到 PolygonCollider2D无法适配。");
return;
}
int groundLayer = LayerMask.NameToLayer("Ground");
var tilemaps = FindObjectsOfType<Tilemap>();
Bounds? combined = null;
foreach (var tm in tilemaps)
{
if (tm.gameObject.layer != groundLayer) continue;
tm.CompressBounds();
Bounds worldBounds = TransformBounds(tm.transform, tm.localBounds);
combined = combined.HasValue ? Combine(combined.Value, worldBounds) : worldBounds;
}
if (!combined.HasValue)
{
Debug.LogWarning("[RoomCameraSetupTool] 场景中未找到 Ground 层 Tilemap无法自动适配。");
return;
}
Bounds b = combined.Value;
// Convert to local space of PolygonCollider2D's transform
Transform polyT = poly.transform;
Vector2 LocalPt(Vector3 world) => polyT.InverseTransformPoint(world);
Undo.RecordObject(poly, "Fit Confiner to Tilemap Bounds");
poly.SetPath(0, new Vector2[]
{
LocalPt(new Vector3(b.min.x, b.min.y)),
LocalPt(new Vector3(b.min.x, b.max.y)),
LocalPt(new Vector3(b.max.x, b.max.y)),
LocalPt(new Vector3(b.max.x, b.min.y)),
});
EditorUtility.SetDirty(poly);
Debug.Log($"[RoomCameraSetupTool] {cam.name}RoomBoundary 已适配至 Ground Tilemap 合并范围 " +
$"({b.min.x:F1},{b.min.y:F1}) ~ ({b.max.x:F1},{b.max.y:F1})。");
}
// ══ 工具方法 ═══════════════════════════════════════════════════════════
private static Bounds TransformBounds(Transform t, Bounds localBounds)
{
Bounds world = new Bounds(t.TransformPoint(localBounds.center), Vector3.zero);
// 变换 8 个角点取包围
foreach (Vector3 corner in new[]
{
localBounds.min,
localBounds.max,
new Vector3(localBounds.min.x, localBounds.max.y, 0f),
new Vector3(localBounds.max.x, localBounds.min.y, 0f),
})
world.Encapsulate(t.TransformPoint(corner));
return world;
}
private static Bounds Combine(Bounds a, Bounds b)
{
a.Encapsulate(b.min);
a.Encapsulate(b.max);
return a;
}
// ══ GUI 辅助 ═══════════════════════════════════════════════════════════
private void EnsureStyles()
{
if (_boxStyle == null)
{
_boxStyle = new GUIStyle(GUI.skin.box)
{
padding = new RectOffset(6, 6, 4, 4),
};
}
}
private static void DrawSectionHeader(string title)
{
EditorGUILayout.Space(4f);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Label(title, EditorStyles.boldLabel);
}
Rect r = EditorGUILayout.GetControlRect(false, 1f);
EditorGUI.DrawRect(r, new Color(0.4f, 0.4f, 0.4f, 1f));
EditorGUILayout.Space(2f);
}
private static void DrawFieldCheck(SerializedObject so, string propName, string displayName, bool optional = false)
{
var prop = so.FindProperty(propName);
bool ok = prop != null && prop.objectReferenceValue != null;
DrawCheckRow(displayName, ok, optional);
}
private static void DrawCheckRow(string label, bool ok, bool optional = false)
{
using (new EditorGUILayout.HorizontalScope())
{
Color prev = GUI.color;
GUI.color = ok ? new Color(0.4f, 1f, 0.4f) : (optional ? new Color(0.8f, 0.8f, 0.4f) : new Color(1f, 0.4f, 0.4f));
GUILayout.Label(ok ? "●" : (optional ? "◌" : "✗"), GUILayout.Width(16f));
GUI.color = prev;
EditorGUILayout.LabelField(label);
}
}
}
}

View File

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