摄像机区域的架构改动
This commit is contained in:
277
Assets/_Game/Scripts/Editor/Camera/CameraAreaEditor.cs
Normal file
277
Assets/_Game/Scripts/Editor/Camera/CameraAreaEditor.cs
Normal 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._vcamA(Persistent 场景)
|
||||
/// → 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Camera/CameraAreaEditor.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Camera/CameraAreaEditor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3f7c4193e749e54b85c7ca1b31f0783
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
268
Assets/_Game/Scripts/Editor/Camera/RoomCameraEditor.cs
Normal file
268
Assets/_Game/Scripts/Editor/Camera/RoomCameraEditor.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Camera/RoomCameraEditor.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Camera/RoomCameraEditor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e733a7cb718909842b12f5994eb841c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
866
Assets/_Game/Scripts/Editor/Camera/RoomCameraSetupTool.cs
Normal file
866
Assets/_Game/Scripts/Editor/Camera/RoomCameraSetupTool.cs
Normal 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("CameraStateController(Persistent 场景)");
|
||||
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 的对象。" +
|
||||
"请先放置 Player(BaseGames → 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd2d2ca2985a57d4ea78c6c509b0dec1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user