摄像机区域的优化
This commit is contained in:
@@ -9,14 +9,15 @@ 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 场景)
|
||||
/// → CameraLensConfigSO.fieldOfView(单一来源,无跨场景依赖)
|
||||
/// → CameraStateController._vcamA(Persistent 场景已加载时)
|
||||
/// → Camera.main.fieldOfView
|
||||
/// → 60f(默认)
|
||||
/// </summary>
|
||||
@@ -24,51 +25,270 @@ namespace BaseGames.Editor
|
||||
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);
|
||||
private static readonly Color kVisibleFill = new Color(1.00f, 0.85f, 0.15f, 0.06f);
|
||||
private static readonly Color kVisibleOutline = new Color(1.00f, 0.85f, 0.15f, 1.00f);
|
||||
private static readonly Color kConfinerFill = new Color(0.20f, 0.75f, 1.00f, 0.08f);
|
||||
private static readonly Color kConfinerLine = new Color(0.20f, 0.75f, 1.00f, 0.85f);
|
||||
private static readonly Color kTriggerFill = new Color(0.30f, 1.00f, 0.50f, 0.07f);
|
||||
private static readonly Color kTriggerLine = new Color(0.30f, 1.00f, 0.50f, 0.85f);
|
||||
private static readonly Color kHeaderBg = new Color(0.18f, 0.18f, 0.23f, 1f);
|
||||
private static readonly Color kOk = new Color(0.30f, 0.82f, 0.30f, 1f);
|
||||
private static readonly Color kError = new Color(0.90f, 0.28f, 0.28f, 1f);
|
||||
private static readonly Color kMuted = new Color(0.55f, 0.55f, 0.60f, 1f);
|
||||
|
||||
// ── 折叠状态(每个 CameraArea 实例独立) ─────────────────────────────
|
||||
private bool _foldBase = true;
|
||||
private bool _foldFollow = true;
|
||||
private bool _foldLens = false;
|
||||
private bool _foldCamera = false;
|
||||
private bool _foldTools = false;
|
||||
|
||||
// ── 折叠标题样式缓存(深色背景 + 白色文字)────────────────────────────
|
||||
private static GUIStyle _foldoutHeaderStyle;
|
||||
|
||||
// ── Scene 视图叠加面板样式缓存 ────────────────────────────────────────
|
||||
private static GUIStyle _sceneOverlayBoldStyle;
|
||||
private static GUIStyle _sceneLabelStyle;
|
||||
private static GUIStyle _gizmoTagStyle;
|
||||
// ── 常显标签样式([DrawGizmo])─────────────────────────────────────
|
||||
private static GUIStyle _alwaysLabelShadowStyle;
|
||||
private static GUIStyle _alwaysLabelMainStyle;
|
||||
|
||||
// ══ Inspector ═════════════════════════════════════════════════════════
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
EditorGUILayout.Space(8f);
|
||||
EditorGUILayout.LabelField("── 可视区域工具 ──", EditorStyles.boldLabel);
|
||||
|
||||
serializedObject.Update();
|
||||
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))
|
||||
// ── 基础设置 ──────────────────────────────────────────────────────
|
||||
_foldBase = DrawFoldoutHeader("基础设置", _foldBase);
|
||||
if (_foldBase)
|
||||
{
|
||||
EditorGUILayout.FloatField("垂直 FOV(来源见工具提示)", vFOV);
|
||||
EditorGUILayout.FloatField("有效深度", depth);
|
||||
EditorGUILayout.FloatField("视口半高(世界单位)", halfH);
|
||||
EditorGUILayout.FloatField("视口半宽(世界单位)", halfW);
|
||||
using (new EditorGUI.IndentLevelScope())
|
||||
{
|
||||
var confinerProp = serializedObject.FindProperty("_confinerCollider");
|
||||
bool confinerOk = confinerProp.objectReferenceValue != null;
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
Color prev = GUI.color;
|
||||
GUI.color = confinerOk ? kOk : kError;
|
||||
GUILayout.Label(confinerOk ? "●" : "✗", GUILayout.Width(14f));
|
||||
GUI.color = prev;
|
||||
EditorGUILayout.PropertyField(confinerProp, new GUIContent("Confiner Collider"));
|
||||
}
|
||||
if (!confinerOk)
|
||||
EditorGUILayout.HelpBox("必须绑定子节点 PolygonCollider2D(AreaBoundary),否则 Cinemachine 无法限位。", MessageType.Error);
|
||||
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_visibleBounds"), new GUIContent("Visible Bounds(本地坐标)"));
|
||||
}
|
||||
}
|
||||
|
||||
bool canSync = area.ConfinerCollider != null;
|
||||
if (!canSync)
|
||||
EditorGUILayout.HelpBox("ConfinerCollider 未绑定,无法同步限位区域。", MessageType.Warning);
|
||||
EditorGUILayout.Space(2f);
|
||||
|
||||
using (new EditorGUI.DisabledScope(!canSync))
|
||||
// ── 跟随参数覆盖 ─────────────────────────────────────────────────
|
||||
var overrideProp = serializedObject.FindProperty("_overrideFollowBehaviour");
|
||||
bool overrides = overrideProp.boolValue;
|
||||
_foldFollow = DrawFoldoutHeader(
|
||||
overrides ? "跟随参数覆盖 ●" : "跟随参数覆盖 ○ (使用全局默认)", _foldFollow);
|
||||
if (_foldFollow)
|
||||
{
|
||||
if (GUILayout.Button("从可视区域更新限位区域(透视)", GUILayout.Height(28f)))
|
||||
SyncConfinerFromVisibleBounds(area, vFOV, aspect);
|
||||
using (new EditorGUI.IndentLevelScope())
|
||||
{
|
||||
EditorGUILayout.PropertyField(overrideProp, new GUIContent("Override Follow Behaviour"));
|
||||
if (overrides)
|
||||
{
|
||||
EditorGUILayout.Space(2f);
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_screenPosition"), new GUIContent("Screen Position"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_damping"), new GUIContent("Damping"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_dampingDown"), new GUIContent("Damping Down"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_dampingUp"), new GUIContent("Damping Up"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_deadZoneSize"), new GUIContent("Dead Zone Size"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lookaheadTime"), new GUIContent("Lookahead Time"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lookaheadSmoothing"),new GUIContent("Lookahead Smoothing"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lockHorizontal"), new GUIContent("Lock Horizontal"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lockVertical"), new GUIContent("Lock Vertical"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 图例说明 ─────────────────────────────────────────────────────
|
||||
EditorGUILayout.Space(4f);
|
||||
DrawLegend("■ 黄色矩形(Scene 视图)", kVisibleOutline, "可视区域 — 摄像机视口永不超出此范围");
|
||||
DrawLegend("■ 蓝色多边形(Scene 视图)", kConfinerColor, "限位区域 — CinemachineConfiner2D 的运动边界");
|
||||
EditorGUILayout.Space(2f);
|
||||
|
||||
// ── 镜头 & 混合 ──────────────────────────────────────────────────
|
||||
_foldLens = DrawFoldoutHeader("镜头 & 混合配置", _foldLens);
|
||||
if (_foldLens)
|
||||
{
|
||||
using (new EditorGUI.IndentLevelScope())
|
||||
{
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lensSize"), new GUIContent("Lens Size"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_lensSizeDuration"), new GUIContent("Lens Size Duration"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_blendProfile"), new GUIContent("Blend Profile"));
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(2f);
|
||||
|
||||
// ── 专有相机(可选) ──────────────────────────────────────────────
|
||||
_foldCamera = DrawFoldoutHeader("专有相机(可选)", _foldCamera);
|
||||
if (_foldCamera)
|
||||
{
|
||||
using (new EditorGUI.IndentLevelScope())
|
||||
{
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_dedicatedCamera"), new GUIContent("Dedicated Camera"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_dedicatedPriority"), new GUIContent("Dedicated Priority"));
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(2f);
|
||||
|
||||
// ── 可视区域工具 ──────────────────────────────────────────────────
|
||||
_foldTools = DrawFoldoutHeader("可视区域工具", _foldTools);
|
||||
if (_foldTools)
|
||||
{
|
||||
using (new EditorGUI.IndentLevelScope())
|
||||
{
|
||||
// ── 镜头配置 SO ─────────────────────────────────────────
|
||||
EditorGUILayout.PropertyField(
|
||||
serializedObject.FindProperty("_lensConfig"),
|
||||
new GUIContent("镜头配置 (SO)",
|
||||
"与 CameraStateController 引用同一个 CameraLensConfigSO,\n" +
|
||||
"保证限位计算 FOV 与运行时 VCam 一致。\n" +
|
||||
"SO 中 FOV 修改同时自动重新同步限位多边形。"));
|
||||
|
||||
float vFOV = GetFOV(area);
|
||||
float aspect = GetAspect();
|
||||
|
||||
// ── FOV 来源说明与过期警告 ──────────────────────────
|
||||
string fovNote;
|
||||
Color noteColor;
|
||||
if (area.DedicatedCamera != null)
|
||||
{
|
||||
fovNote = $"来源:专有 VCam ({area.DedicatedCamera.name}) FOV = {vFOV:F1}°";
|
||||
noteColor = kOk;
|
||||
}
|
||||
else if (area.LensConfig != null)
|
||||
{
|
||||
bool isStale = area.ConfinerCollider != null
|
||||
&& area.LastSyncFOV > 0f
|
||||
&& Mathf.Abs(area.LastSyncFOV - vFOV) > 0.05f;
|
||||
if (isStale)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
$"FOV 已从 {area.LastSyncFOV:F1}° 改为 {vFOV:F1}°,限位多边形需要重新同步。",
|
||||
MessageType.Warning);
|
||||
}
|
||||
fovNote = isStale
|
||||
? $"⚠ SO FOV 已从 {area.LastSyncFOV:F1}° 改为 {vFOV:F1}°,限位需重新同步"
|
||||
: $"来源:CameraLensConfigSO FOV = {vFOV:F1}°";
|
||||
noteColor = isStale ? new Color(1f, 0.7f, 0.1f) : kOk;
|
||||
}
|
||||
else
|
||||
{
|
||||
fovNote = $"⚠ 未绑定 LensConfig SO,使用备用来源 FOV = {vFOV:F1}°";
|
||||
noteColor = new Color(1f, 0.7f, 0.1f);
|
||||
EditorGUILayout.HelpBox("建议绑定 CameraLensConfigSO,保证跨场景 FOV 一致。", MessageType.Warning);
|
||||
}
|
||||
|
||||
Color prevC = GUI.color;
|
||||
GUI.color = noteColor;
|
||||
EditorGUILayout.LabelField(fovNote, EditorStyles.miniLabel);
|
||||
GUI.color = prevC;
|
||||
|
||||
EditorGUILayout.Space(4f);
|
||||
|
||||
// ── 只读推算值 ──────────────────────────────────────────
|
||||
float depth = area.CameraDepth;
|
||||
|
||||
// 深度来源说明
|
||||
string depthNote;
|
||||
Color depthColor;
|
||||
var depthProp = serializedObject.FindProperty("_cameraDepth");
|
||||
if (depthProp != null && depthProp.floatValue > 0f)
|
||||
{
|
||||
depthNote = $"来源:区域专有 _cameraDepth = {depth:F1}";
|
||||
depthColor = kOk;
|
||||
}
|
||||
else if (area.LensConfig != null)
|
||||
{
|
||||
depthNote = $"来源:CameraLensConfigSO.cameraDepth = {depth:F1}";
|
||||
depthColor = kOk;
|
||||
}
|
||||
else
|
||||
{
|
||||
depthNote = "⚠ 未绑定 LensConfig SO,CameraDepth = 0,限位同步无效";
|
||||
depthColor = new Color(1f, 0.3f, 0.3f);
|
||||
}
|
||||
{
|
||||
Color prevC2 = GUI.color;
|
||||
GUI.color = depthColor;
|
||||
EditorGUILayout.LabelField(depthNote, EditorStyles.miniLabel);
|
||||
GUI.color = prevC2;
|
||||
}
|
||||
if (area.LensConfig == null)
|
||||
EditorGUILayout.HelpBox("请绑定 CameraLensConfigSO 以提供 cameraDepth,否则限位多边形无法正确生成。", MessageType.Error);
|
||||
|
||||
float halfH = depth * Mathf.Tan(vFOV * 0.5f * Mathf.Deg2Rad);
|
||||
float halfW = halfH * aspect;
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
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(26f)))
|
||||
SyncConfinerFromVisibleBounds(area, vFOV, aspect);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(4f);
|
||||
DrawLegend("■ 黄色矩形(Scene 视图)", kVisibleOutline, "可视区域 — 摄像机视口永不超出此范围");
|
||||
DrawLegend("■ 蓝色多边形(Scene 视图)", kConfinerLine, "限位区域 — CinemachineConfiner2D 的运动边界");
|
||||
}
|
||||
}
|
||||
|
||||
if (serializedObject.hasModifiedProperties)
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
// ══ 全局 Gizmo(非选中时也显示)═══════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 在 Scene 视图中始终显示 CameraArea 名称标签(可视区域内部居中)。
|
||||
/// 利用 [DrawGizmo] 实现非选中状态下也常显。
|
||||
/// </summary>
|
||||
[DrawGizmo(GizmoType.NotInSelectionHierarchy | GizmoType.InSelectionHierarchy | GizmoType.Active | GizmoType.Pickable)]
|
||||
private static void DrawAreaNameGizmo(CameraArea area, GizmoType gizmoType)
|
||||
{
|
||||
Rect worldR = area.VisibleBounds;
|
||||
if (worldR.width <= 0f || worldR.height <= 0f) return;
|
||||
|
||||
Vector3 center = new Vector3(worldR.center.x, worldR.center.y, 0f);
|
||||
string text = area.gameObject.name;
|
||||
float sz = HandleUtility.GetHandleSize(center) * 0.028f;
|
||||
|
||||
if (_alwaysLabelShadowStyle == null)
|
||||
_alwaysLabelShadowStyle = new GUIStyle(EditorStyles.boldLabel)
|
||||
{
|
||||
normal = { textColor = new Color(0f, 0f, 0f, 0.85f) },
|
||||
fontSize = 14,
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
};
|
||||
if (_alwaysLabelMainStyle == null)
|
||||
_alwaysLabelMainStyle = new GUIStyle(EditorStyles.boldLabel)
|
||||
{
|
||||
normal = { textColor = Color.white },
|
||||
fontSize = 14,
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
};
|
||||
|
||||
// 阴影偏移,增强居中标签对任意背景的对比度
|
||||
Handles.Label(center + new Vector3(sz, -sz, 0f), text, _alwaysLabelShadowStyle);
|
||||
Handles.Label(center, text, _alwaysLabelMainStyle);
|
||||
}
|
||||
// ══ Scene GUI ════════════════════════════════════════════════════════
|
||||
|
||||
private void OnSceneGUI()
|
||||
@@ -77,23 +297,41 @@ namespace BaseGames.Editor
|
||||
serializedObject.Update();
|
||||
|
||||
SerializedProperty boundsP = serializedObject.FindProperty("_visibleBounds");
|
||||
Rect r = boundsP.rectValue;
|
||||
Rect localR = boundsP.rectValue;
|
||||
// 本地坐标 → 世界坐标,可视区域随 CameraArea GameObject 一同移动
|
||||
Vector2 areaPos = area.transform.position;
|
||||
Rect r = new Rect(localR.x + areaPos.x, localR.y + areaPos.y, localR.width, localR.height);
|
||||
|
||||
// ── 绘制限位多边形(蓝色,参考用) ──────────────────────────────
|
||||
// ── 触发区域(绿色,只读) ─────────────────────────────────────────
|
||||
DrawTriggerZoneGizmos(area);
|
||||
|
||||
// ── 限位多边形(蓝色,只读) ─────────────────────────────────────
|
||||
DrawConfinerGizmo(area);
|
||||
|
||||
// ── 绘制可视区域填充 + 边框 ──────────────────────────────────────
|
||||
// ── 可视区域(黄色) + 尺寸标注 ──────────────────────────────
|
||||
DrawVisibleRect(r);
|
||||
DrawDimensionLabels(r);
|
||||
|
||||
// ── 四条边的拖拽 Handle ──────────────────────────────────────────
|
||||
// ── Handle 编辑(四角 + 四边中点 + 中心移动) ─────────────────────
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditRectEdges(ref r);
|
||||
EditRectHandles(ref r);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
Undo.RecordObject(area, "Edit Visible Bounds");
|
||||
boundsP.rectValue = r;
|
||||
if (area.ConfinerCollider != null)
|
||||
Undo.RecordObject(area.ConfinerCollider, "Sync Confiner");
|
||||
|
||||
// 世界坐标 → 本地坐标,存入序列化字段
|
||||
boundsP.rectValue = new Rect(r.x - areaPos.x, r.y - areaPos.y, r.width, r.height);
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
// 拖拽时自动同步限位多边形(不输出日志)
|
||||
if (area.ConfinerCollider != null)
|
||||
SyncConfinerQuiet(area, GetFOV(area), GetAspect());
|
||||
}
|
||||
|
||||
// ── 叠加信息面板(屏幕空间) ───────────────────────────────────────
|
||||
DrawSceneInfoOverlay(area, r);
|
||||
}
|
||||
|
||||
// ══ 绘制辅助 ═════════════════════════════════════════════════════════
|
||||
@@ -108,13 +346,36 @@ namespace BaseGames.Editor
|
||||
new Vector3(r.xMax, r.yMin, 0f),
|
||||
};
|
||||
|
||||
Handles.DrawSolidRectangleWithOutline(corners, kVisibleFill, kVisibleOutline);
|
||||
// 半透明填充
|
||||
Handles.DrawSolidRectangleWithOutline(corners, kVisibleFill, Color.clear);
|
||||
|
||||
// 2.5px 抗锯齿粗轮廓
|
||||
Handles.color = kVisibleOutline;
|
||||
Handles.DrawAAPolyLine(2.5f,
|
||||
corners[0], corners[1], corners[2], corners[3], corners[0]);
|
||||
|
||||
// 四角小圆点(提示可交互的边界点)
|
||||
float dot = HandleUtility.GetHandleSize(r.center) * 0.04f;
|
||||
foreach (var c in corners)
|
||||
Handles.DrawSolidDisc(c, Vector3.back, dot);
|
||||
}
|
||||
|
||||
/// <summary>在 Scene 视图中标注可视区域的宽高。</summary>
|
||||
private static void DrawDimensionLabels(Rect r)
|
||||
{
|
||||
if (_sceneLabelStyle == null)
|
||||
_sceneLabelStyle = new GUIStyle(EditorStyles.miniLabel)
|
||||
{ normal = { textColor = new Color(1f, 0.85f, 0.15f, 1f) } };
|
||||
|
||||
// 宽度:底边中心正下方
|
||||
Handles.Label(
|
||||
new Vector3(r.xMin + 0.15f, r.yMax - 0.15f, 0f),
|
||||
"Visible Area",
|
||||
EditorStyles.miniLabel);
|
||||
new Vector3(r.center.x - 0.4f, r.yMin - 0.55f, 0f),
|
||||
$"← {r.width:F1} →", _sceneLabelStyle);
|
||||
|
||||
// 高度:右边中心右侧
|
||||
Handles.Label(
|
||||
new Vector3(r.xMax + 0.2f, r.center.y - 0.1f, 0f),
|
||||
$"{r.height:F1}", _sceneLabelStyle);
|
||||
}
|
||||
|
||||
private static void DrawConfinerGizmo(CameraArea area)
|
||||
@@ -123,66 +384,131 @@ namespace BaseGames.Editor
|
||||
if (poly == null || poly.pathCount == 0) return;
|
||||
|
||||
int ptCount = poly.GetTotalPointCount();
|
||||
if (ptCount < 2) return;
|
||||
if (ptCount < 3) return;
|
||||
|
||||
var pts2 = new System.Collections.Generic.List<Vector2>(ptCount);
|
||||
poly.GetPath(0, pts2);
|
||||
var pts3 = new Vector3[ptCount + 1];
|
||||
var pts3 = new Vector3[ptCount];
|
||||
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);
|
||||
DrawPolyGizmo(pts3, kConfinerFill, kConfinerLine, 2.0f);
|
||||
|
||||
if (_gizmoTagStyle == null)
|
||||
_gizmoTagStyle = new GUIStyle(EditorStyles.miniLabel)
|
||||
{ normal = { textColor = Color.white }, fontStyle = FontStyle.Bold };
|
||||
|
||||
Handles.color = kConfinerLine;
|
||||
Handles.Label(pts3[0] + new Vector3(0.15f, 0.15f, 0f), "限位", _gizmoTagStyle);
|
||||
}
|
||||
|
||||
/// <summary>绘制四条边的滑动 Handle,允许用户直接拖拽修改可视区域。</summary>
|
||||
private static void EditRectEdges(ref Rect r)
|
||||
private static void DrawTriggerZoneGizmos(CameraArea area)
|
||||
{
|
||||
float hs = HandleUtility.GetHandleSize(r.center) * 0.10f;
|
||||
var zones = area.GetComponentsInChildren<CameraTriggerZone>(true);
|
||||
foreach (var zone in zones)
|
||||
{
|
||||
var poly = zone.GetComponent<PolygonCollider2D>();
|
||||
if (poly == null || poly.pathCount == 0) continue;
|
||||
|
||||
int ptCount = poly.GetTotalPointCount();
|
||||
if (ptCount < 3) continue;
|
||||
|
||||
var pts2 = new System.Collections.Generic.List<Vector2>(ptCount);
|
||||
poly.GetPath(0, pts2);
|
||||
var pts3 = new Vector3[ptCount];
|
||||
for (int i = 0; i < ptCount; i++)
|
||||
pts3[i] = poly.transform.TransformPoint(pts2[i]);
|
||||
|
||||
DrawPolyGizmo(pts3, kTriggerFill, kTriggerLine, 1.5f);
|
||||
|
||||
if (_gizmoTagStyle == null)
|
||||
_gizmoTagStyle = new GUIStyle(EditorStyles.miniLabel)
|
||||
{ normal = { textColor = Color.white }, fontStyle = FontStyle.Bold };
|
||||
|
||||
Handles.color = kTriggerLine;
|
||||
Handles.Label(pts3[0] + new Vector3(0.15f, 0.15f, 0f), "触发", _gizmoTagStyle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>绘制闭合多边形 Gizmo:半透明填充(凸多边形)+ 抗锯齿粗轮廓。</summary>
|
||||
private static void DrawPolyGizmo(Vector3[] pts, Color fill, Color line, float lineWidth)
|
||||
{
|
||||
// 凸多边形填充
|
||||
Handles.color = fill;
|
||||
Handles.DrawAAConvexPolygon(pts);
|
||||
|
||||
// 闭合轮廓线
|
||||
var closed = new Vector3[pts.Length + 1];
|
||||
System.Array.Copy(pts, closed, pts.Length);
|
||||
closed[pts.Length] = pts[0];
|
||||
Handles.color = line;
|
||||
Handles.DrawAAPolyLine(lineWidth, closed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制可视区域的交互 Handle:4 个角点(对角缩放)+ 4 个边中点(单轴缩放)+ 中心点(整体移动)。
|
||||
/// </summary>
|
||||
private static void EditRectHandles(ref Rect r)
|
||||
{
|
||||
float hs = HandleUtility.GetHandleSize(r.center) * 0.09f;
|
||||
float hsDot = hs * 0.60f;
|
||||
|
||||
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);
|
||||
Vector3 oldC = new Vector3(r.center.x, r.center.y, 0f);
|
||||
Vector3 newC = Handles.FreeMoveHandle(oldC, hs * 1.3f, Vector3.zero, Handles.CircleHandleCap);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
r.xMin = Mathf.Min(lp.x, r.xMax - 0.1f);
|
||||
r.position += new Vector2(newC.x - oldC.x, newC.y - oldC.y);
|
||||
|
||||
// 右边 —— 沿 X 轴滑动
|
||||
// ── 四角:对角缩放(方形 cap,易抓取) ────────────────────────────
|
||||
// 左下
|
||||
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);
|
||||
Vector3 bl = Handles.FreeMoveHandle(new Vector3(r.xMin, r.yMin, 0f), hs, Vector3.zero, Handles.RectangleHandleCap);
|
||||
if (EditorGUI.EndChangeCheck()) { r.xMin = Mathf.Min(bl.x, r.xMax - 0.1f); r.yMin = Mathf.Min(bl.y, r.yMax - 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);
|
||||
Vector3 br = Handles.FreeMoveHandle(new Vector3(r.xMax, r.yMin, 0f), hs, Vector3.zero, Handles.RectangleHandleCap);
|
||||
if (EditorGUI.EndChangeCheck()) { r.xMax = Mathf.Max(br.x, r.xMin + 0.1f); r.yMin = Mathf.Min(br.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);
|
||||
Vector3 tl = Handles.FreeMoveHandle(new Vector3(r.xMin, r.yMax, 0f), hs, Vector3.zero, Handles.RectangleHandleCap);
|
||||
if (EditorGUI.EndChangeCheck()) { r.xMin = Mathf.Min(tl.x, r.xMax - 0.1f); r.yMax = Mathf.Max(tl.y, r.yMin + 0.1f); }
|
||||
|
||||
// 右上
|
||||
EditorGUI.BeginChangeCheck();
|
||||
Vector3 tr = Handles.FreeMoveHandle(new Vector3(r.xMax, r.yMax, 0f), hs, Vector3.zero, Handles.RectangleHandleCap);
|
||||
if (EditorGUI.EndChangeCheck()) { r.xMax = Mathf.Max(tr.x, r.xMin + 0.1f); r.yMax = Mathf.Max(tr.y, r.yMin + 0.1f); }
|
||||
|
||||
// ── 四边中点:单轴缩放(点 cap,视觉上比角小) ────────────────────
|
||||
// 左边
|
||||
EditorGUI.BeginChangeCheck();
|
||||
Vector3 lp = Handles.Slider(new Vector3(r.xMin, r.center.y, 0f), Vector3.right, hsDot, Handles.DotHandleCap, EditorSnapSettings.move.x);
|
||||
if (EditorGUI.EndChangeCheck()) r.xMin = Mathf.Min(lp.x, r.xMax - 0.1f);
|
||||
|
||||
// 右边
|
||||
EditorGUI.BeginChangeCheck();
|
||||
Vector3 rp = Handles.Slider(new Vector3(r.xMax, r.center.y, 0f), Vector3.right, hsDot, Handles.DotHandleCap, EditorSnapSettings.move.x);
|
||||
if (EditorGUI.EndChangeCheck()) r.xMax = Mathf.Max(rp.x, r.xMin + 0.1f);
|
||||
|
||||
// 下边
|
||||
EditorGUI.BeginChangeCheck();
|
||||
Vector3 bp = Handles.Slider(new Vector3(r.center.x, r.yMin, 0f), Vector3.up, hsDot, Handles.DotHandleCap, EditorSnapSettings.move.y);
|
||||
if (EditorGUI.EndChangeCheck()) r.yMin = Mathf.Min(bp.y, r.yMax - 0.1f);
|
||||
|
||||
// 上边
|
||||
EditorGUI.BeginChangeCheck();
|
||||
Vector3 tp = Handles.Slider(new Vector3(r.center.x, r.yMax, 0f), Vector3.up, hsDot, Handles.DotHandleCap, 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)
|
||||
internal static void SyncConfinerFromVisibleBounds(CameraArea area, float vFOV, float aspect)
|
||||
{
|
||||
var poly = area.ConfinerCollider;
|
||||
if (poly == null)
|
||||
@@ -191,6 +517,75 @@ namespace BaseGames.Editor
|
||||
return;
|
||||
}
|
||||
|
||||
// VisibleBounds 已含 transform.position,为世界坐标。
|
||||
// 限位多边形 = 相机中心运动范围 = VisibleBounds 向内收缩视口半尺寸。
|
||||
// 运行时 ConfigureSlot 设置 OversizeWindow.MaxWindowSize ≈ 0,
|
||||
// 阻止 Cinemachine 再次收缩此多边形,确保边界精确匹配可视区域。
|
||||
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;
|
||||
|
||||
// 小房间:视口大于可视区域时收缩至中心点,相机固定在可视区域中心
|
||||
const float kMinSize = 0.001f;
|
||||
if (xMin > xMax) { float cx = visible.center.x; xMin = cx - kMinSize * 0.5f; xMax = cx + kMinSize * 0.5f; }
|
||||
if (yMin > yMax) { float cy = visible.center.y; yMin = cy - kMinSize * 0.5f; yMax = cy + kMinSize * 0.5f; }
|
||||
|
||||
Transform polyT = poly.transform;
|
||||
Vector2 Local(Vector3 w) => polyT.InverseTransformPoint(w);
|
||||
|
||||
Undo.RecordObject(poly, "Sync Confiner from Visible Bounds");
|
||||
// 顶点必须 CCW(逆时针):Clipper 对 CW 多边形(area<0)会取反 delta,
|
||||
// 导致 Confiner 向外膨胀而非向内收缩,相机完全不受限。
|
||||
// CCW 顺序:BL → BR → TR → TL
|
||||
poly.SetPath(0, new[]
|
||||
{
|
||||
Local(new Vector3(xMin, yMin, 0f)), // BL
|
||||
Local(new Vector3(xMax, yMin, 0f)), // BR
|
||||
Local(new Vector3(xMax, yMax, 0f)), // TR
|
||||
Local(new Vector3(xMin, yMax, 0f)), // TL
|
||||
});
|
||||
EditorUtility.SetDirty(poly);
|
||||
|
||||
// 记录本次同步所用的 FOV,供编辑器过期检测使用
|
||||
var areaSO = new SerializedObject(area);
|
||||
var lastFovProp = areaSO.FindProperty("_lastSyncFOV");
|
||||
if (lastFovProp != null)
|
||||
{
|
||||
lastFovProp.floatValue = vFOV;
|
||||
areaSO.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
Debug.Log(
|
||||
$"[CameraAreaEditor] {area.name}:限位区域已同步。\n" +
|
||||
$" 可视区域:({visible.xMin:F2}, {visible.yMin:F2}) ~ ({visible.xMax:F2}, {visible.yMax:F2})\n" +
|
||||
$" FOV={vFOV:F1}° Depth={depth:F1} HalfView=({halfW:F2}, {halfH:F2})\n" +
|
||||
$" 限位区域(相机中心运动范围):({xMin:F2}, {yMin:F2}) ~ ({xMax:F2}, {yMax:F2})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动检测 FOV 和宽高比,将 <paramref name="area"/> 的限位多边形同步到可视区域。
|
||||
/// 可从其他编辑器工具(如 CameraAreaSetupTool)一键批量调用。
|
||||
/// </summary>
|
||||
internal static void SyncConfinerAuto(CameraArea area) =>
|
||||
SyncConfinerFromVisibleBounds(area, GetFOV(area), GetAspect());
|
||||
|
||||
/// <summary>
|
||||
/// 与 <see cref="SyncConfinerFromVisibleBounds"/> 逻辑相同,但不记录 Undo、不输出日志。
|
||||
/// 供拖拽 Handle 时每帧调用,避免 Undo 堆积和 Console 刷屏。
|
||||
/// 调用方须在调用前自行执行 Undo.RecordObject(poly)。
|
||||
/// </summary>
|
||||
private static void SyncConfinerQuiet(CameraArea area, float vFOV, float aspect)
|
||||
{
|
||||
var poly = area.ConfinerCollider;
|
||||
if (poly == null) return;
|
||||
|
||||
// VisibleBounds 已含 transform.position,为世界坐标。
|
||||
Rect visible = area.VisibleBounds;
|
||||
float depth = area.CameraDepth;
|
||||
float halfH = depth * Mathf.Tan(vFOV * 0.5f * Mathf.Deg2Rad);
|
||||
@@ -201,28 +596,68 @@ namespace BaseGames.Editor
|
||||
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; }
|
||||
const float kMinSize = 0.001f;
|
||||
if (xMin > xMax) { float cx = visible.center.x; xMin = cx - kMinSize * 0.5f; xMax = cx + kMinSize * 0.5f; }
|
||||
if (yMin > yMax) { float cy = visible.center.y; yMin = cy - kMinSize * 0.5f; yMax = cy + kMinSize * 0.5f; }
|
||||
|
||||
Transform polyT = poly.transform;
|
||||
Vector2 Local(Vector3 w) => polyT.InverseTransformPoint(w);
|
||||
|
||||
Undo.RecordObject(poly, "Sync Confiner from Visible Bounds");
|
||||
// CCW 顺序:BL → BR → TR → TL(同 SyncConfinerFromVisibleBounds)
|
||||
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)),
|
||||
Local(new Vector3(xMin, yMin, 0f)), // BL
|
||||
Local(new Vector3(xMax, yMin, 0f)), // BR
|
||||
Local(new Vector3(xMax, yMax, 0f)), // TR
|
||||
Local(new Vector3(xMin, yMax, 0f)), // TL
|
||||
});
|
||||
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>在 Scene 视图左上角绘制叠加信息面板(屏幕空间)。</summary>
|
||||
private static void DrawSceneInfoOverlay(CameraArea area, Rect bounds)
|
||||
{
|
||||
Handles.BeginGUI();
|
||||
|
||||
bool hasConfiner = area.ConfinerCollider != null;
|
||||
float panelH = hasConfiner ? 82f : 62f;
|
||||
var panel = new Rect(8f, 8f, 192f, panelH);
|
||||
|
||||
// 半透明深色背景
|
||||
GUI.color = new Color(0f, 0f, 0f, 0.65f);
|
||||
GUI.DrawTexture(panel, Texture2D.whiteTexture);
|
||||
GUI.color = Color.white;
|
||||
|
||||
if (_sceneOverlayBoldStyle == null)
|
||||
_sceneOverlayBoldStyle = new GUIStyle(EditorStyles.boldLabel)
|
||||
{ normal = { textColor = new Color(1f, 0.85f, 0.15f) }, fontSize = 11 };
|
||||
|
||||
if (_sceneLabelStyle == null)
|
||||
_sceneLabelStyle = new GUIStyle(EditorStyles.miniLabel)
|
||||
{ normal = { textColor = new Color(0.85f, 0.85f, 0.85f) } };
|
||||
|
||||
// 区域名称(白色加粗,顶部第一行)
|
||||
var nameStyle = new GUIStyle(EditorStyles.boldLabel)
|
||||
{ normal = { textColor = Color.white }, fontSize = 12 };
|
||||
GUI.Label(new Rect(panel.x + 6, panel.y + 4, panel.width - 12, 18),
|
||||
area.name, nameStyle);
|
||||
|
||||
GUI.Label(new Rect(panel.x + 6, panel.y + 25, panel.width - 12, 17),
|
||||
$"可视 {bounds.width:F1} × {bounds.height:F1}", _sceneOverlayBoldStyle);
|
||||
GUI.Label(new Rect(panel.x + 6, panel.y + 43, panel.width - 12, 14),
|
||||
$"中心 ({bounds.center.x:F1}, {bounds.center.y:F1})", _sceneLabelStyle);
|
||||
|
||||
if (hasConfiner)
|
||||
{
|
||||
if (GUI.Button(new Rect(panel.x + 6, panel.y + 61, panel.width - 12, 16),
|
||||
"↺ 同步限位区域", EditorStyles.miniButton))
|
||||
{
|
||||
Undo.RecordObject(area.ConfinerCollider, "Sync Confiner");
|
||||
SyncConfinerFromVisibleBounds(area, GetFOV(area), GetAspect());
|
||||
}
|
||||
}
|
||||
|
||||
Handles.EndGUI();
|
||||
}
|
||||
|
||||
// ══ 工具方法 ══════════════════════════════════════════════════════════
|
||||
@@ -232,11 +667,15 @@ namespace BaseGames.Editor
|
||||
/// </summary>
|
||||
private static float GetFOV(CameraArea area)
|
||||
{
|
||||
// 1. 区域专有 VCam
|
||||
// 1. 专有 VCam(同一场景,优先级最高)
|
||||
if (area.DedicatedCamera != null)
|
||||
return area.DedicatedCamera.Lens.FieldOfView;
|
||||
|
||||
// 2. Persistent 场景中的 CameraStateController._vcamA(通过反射读取私有字段)
|
||||
// 2. CameraLensConfigSO(单一来源,无跨场景依赖)
|
||||
if (area.LensConfig != null)
|
||||
return area.LensConfig.fieldOfView;
|
||||
|
||||
// 3. Persistent 场景已加载时,实时读取全局 VCamA(兆底)
|
||||
#pragma warning disable UNT0023 // FindObjectOfType 在编辑器工具中可接受
|
||||
var ctrl = Object.FindObjectOfType<CameraStateController>();
|
||||
#pragma warning restore UNT0023
|
||||
@@ -248,11 +687,11 @@ namespace BaseGames.Editor
|
||||
return vcamA.Lens.FieldOfView;
|
||||
}
|
||||
|
||||
// 3. Camera.main
|
||||
// 4. Camera.main
|
||||
if (UnityEngine.Camera.main != null)
|
||||
return UnityEngine.Camera.main.fieldOfView;
|
||||
|
||||
// 4. 默认
|
||||
// 5. 默认
|
||||
return 60f;
|
||||
}
|
||||
|
||||
@@ -273,5 +712,29 @@ namespace BaseGames.Editor
|
||||
EditorGUILayout.LabelField(new GUIContent(text, tooltip));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool DrawFoldoutHeader(string title, bool expanded)
|
||||
{
|
||||
if (_foldoutHeaderStyle == null)
|
||||
_foldoutHeaderStyle = new GUIStyle(EditorStyles.foldout)
|
||||
{
|
||||
fontStyle = FontStyle.Bold,
|
||||
normal = { textColor = Color.white },
|
||||
hover = { textColor = Color.white },
|
||||
active = { textColor = Color.white },
|
||||
focused = { textColor = Color.white },
|
||||
onNormal = { textColor = Color.white },
|
||||
onHover = { textColor = Color.white },
|
||||
onActive = { textColor = Color.white },
|
||||
onFocused = { textColor = Color.white },
|
||||
};
|
||||
|
||||
Rect r = EditorGUILayout.GetControlRect(false, 22f);
|
||||
EditorGUI.DrawRect(r, new Color(0.22f, 0.22f, 0.28f, 1f));
|
||||
bool newExpanded = EditorGUI.Foldout(
|
||||
new Rect(r.x + 4f, r.y + 3f, r.width - 4f, 18f),
|
||||
expanded, title, true, _foldoutHeaderStyle);
|
||||
return newExpanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using BaseGames.Camera;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// CameraLensConfigSO 自定义 Inspector。
|
||||
///
|
||||
/// 功能:
|
||||
/// - 修改 <see cref="CameraLensConfigSO.fieldOfView"/> 后,
|
||||
/// 自动遍历所有已加载场景中引用该 SO 的 <see cref="CameraArea"/>,
|
||||
/// 重新同步限位多边形,避免 FOV 改动后各场景出现错位。
|
||||
/// - 提供手动批量同步按钮,用于已打开但尚未触发自动同步的情形。
|
||||
///
|
||||
/// 未打开的场景无法自动同步。建议工作流:
|
||||
/// 1. 修改 SO 中的 FOV。
|
||||
/// 2. 依次打开各 Room 场景并加载 — 每次打开场景后点击「同步所有已加载场景」,
|
||||
/// 或直接在 CameraArea Inspector「可视区域工具」区域点击同步按钮。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(CameraLensConfigSO))]
|
||||
internal sealed class CameraLensConfigSOEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
DrawDefaultInspector();
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
// FOV 发生变化时,立即重新同步所有已加载场景中的 CameraArea
|
||||
SyncAllLoadedCameraAreas((CameraLensConfigSO)target);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(8f);
|
||||
EditorGUILayout.HelpBox(
|
||||
"修改 FOV 后,编辑器会自动同步当前已打开场景中的 CameraArea 限位区域。\n" +
|
||||
"未打开的场景请手动打开后重新同步。",
|
||||
MessageType.Info);
|
||||
|
||||
if (GUILayout.Button("同步所有已加载场景的 CameraArea 限位区域", GUILayout.Height(26f)))
|
||||
SyncAllLoadedCameraAreas((CameraLensConfigSO)target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 遍历所有已加载场景,对引用了指定 SO 且拥有 ConfinerCollider 的 CameraArea 重新同步限位多边形。
|
||||
/// 由 <see cref="CameraLensConfigSOEditor"/> 的 OnInspectorGUI 以及外部批量工具调用。
|
||||
/// </summary>
|
||||
internal static void SyncAllLoadedCameraAreas(CameraLensConfigSO so)
|
||||
{
|
||||
float fov = so.fieldOfView;
|
||||
float aspect = UnityEngine.Camera.main != null
|
||||
? UnityEngine.Camera.main.aspect
|
||||
: 16f / 9f;
|
||||
|
||||
int count = 0;
|
||||
for (int i = 0; i < SceneManager.sceneCount; i++)
|
||||
{
|
||||
Scene scene = SceneManager.GetSceneAt(i);
|
||||
if (!scene.isLoaded) continue;
|
||||
|
||||
foreach (GameObject root in scene.GetRootGameObjects())
|
||||
{
|
||||
foreach (CameraArea area in root.GetComponentsInChildren<CameraArea>(true))
|
||||
{
|
||||
if (area.LensConfig != so) continue;
|
||||
if (area.ConfinerCollider == null) continue;
|
||||
|
||||
CameraAreaEditor.SyncConfinerFromVisibleBounds(area, fov, aspect);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
Debug.Log($"[CameraLensConfigSO] 已同步 {count} 个 CameraArea 的限位区域(FOV = {fov:F1}°)。");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e733a7cb718909842b12f5994eb841c4
|
||||
guid: 48768cad22a696a4582e9dbc2c100194
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
439
Assets/_Game/Scripts/Editor/Camera/CameraZoneMigrationTool.cs
Normal file
439
Assets/_Game/Scripts/Editor/Camera/CameraZoneMigrationTool.cs
Normal file
@@ -0,0 +1,439 @@
|
||||
using System.Collections.Generic;
|
||||
using BaseGames.Camera;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 将旧格式相机区域批量迁移到新架构。
|
||||
///
|
||||
/// 旧格式:
|
||||
/// Zone_xxx(挂 BoxCollider2D,定义相机可视矩形)
|
||||
/// ├─ Zone_xxx_TriggerRegion
|
||||
/// │ ├─ Zone_xxx_TriggerRegion_Point_0 … (多边形顶点)
|
||||
/// └─ Zone_xxx_Confiner(挂 BoxCollider2D / PolygonCollider2D,定义限位边界)
|
||||
///
|
||||
/// 新格式:
|
||||
/// [新 CameraArea GO](CameraArea 组件,_visibleBounds = 本地 Rect)
|
||||
/// ├─ AreaBoundary(PolygonCollider2D,isTrigger=true,对应旧 Confiner)
|
||||
/// └─ TriggerZone(CameraTriggerZone + PolygonCollider2D,对应旧 TriggerRegion)
|
||||
///
|
||||
/// 菜单:BaseGames → Camera → 相机区域迁移工具
|
||||
/// </summary>
|
||||
public class CameraZoneMigrationTool : EditorWindow
|
||||
{
|
||||
[MenuItem("BaseGames/Camera/相机区域迁移工具", priority = 110)]
|
||||
public static void Open()
|
||||
{
|
||||
var win = GetWindow<CameraZoneMigrationTool>("相机区域迁移工具");
|
||||
win.minSize = new Vector2(500f, 440f);
|
||||
}
|
||||
|
||||
// ── 设置字段 ──────────────────────────────────────────────────────────
|
||||
private Transform _sourcesParent; // 旧 Zone_xxx 的父节点(通常名为 Zones)
|
||||
private Transform _targetParent; // 新对象放置位置(留空 = 与旧区域同级)
|
||||
private CameraLensConfigSO _lensConfig; // 绑定到新 CameraArea._lensConfig
|
||||
|
||||
|
||||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||||
private readonly List<ZoneEntry> _entries = new List<ZoneEntry>();
|
||||
private Vector2 _scroll;
|
||||
private bool _scanned;
|
||||
private int _lastMigratedCount;
|
||||
|
||||
// ── 单条目数据 ────────────────────────────────────────────────────────
|
||||
private class ZoneEntry
|
||||
{
|
||||
public GameObject ZoneObj;
|
||||
public BoxCollider2D VisibleBox; // Zone_xxx 上的 BoxCollider2D
|
||||
public List<Vector2> TriggerWorldPts = new List<Vector2>(); // TriggerRegion 各点世界坐标
|
||||
public Collider2D ConfinerCollider; // Zone_xxx_Confiner 上的碰撞体(可空)
|
||||
public bool AlreadyMigrated;
|
||||
public bool Selected = true;
|
||||
}
|
||||
|
||||
// ── 颜色 ─────────────────────────────────────────────────────────────
|
||||
private static readonly Color kOk = new Color(0.30f, 0.85f, 0.30f);
|
||||
private static readonly Color kWarning = new Color(1.00f, 0.75f, 0.10f);
|
||||
private static readonly Color kMuted = new Color(0.55f, 0.55f, 0.60f);
|
||||
|
||||
// ══ GUI ═══════════════════════════════════════════════════════════════
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.Space(6);
|
||||
EditorGUILayout.LabelField("旧格式相机区域迁移工具", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox(
|
||||
"将旧架构(Zone_xxx + BoxCollider2D + TriggerRegion 点集 + Confiner)" +
|
||||
"批量转换为新架构(CameraArea + AreaBoundary + CameraTriggerZone)。\n" +
|
||||
"迁移结果可在 Scene 视图直接预览,原旧对象可选择禁用。",
|
||||
MessageType.Info);
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
// ── 配置 ──────────────────────────────────────────────────────────
|
||||
_sourcesParent = (Transform)EditorGUILayout.ObjectField(
|
||||
new GUIContent("旧区域父节点 (Zones)", "包含所有 Zone_xxx 的父对象"),
|
||||
_sourcesParent, typeof(Transform), true);
|
||||
|
||||
_targetParent = (Transform)EditorGUILayout.ObjectField(
|
||||
new GUIContent("新区域父节点(留空 = 同级)", "生成的 CameraArea 放在此节点下;留空则与旧 Zone_xxx 同级"),
|
||||
_targetParent, typeof(Transform), true);
|
||||
|
||||
_lensConfig = (CameraLensConfigSO)EditorGUILayout.ObjectField(
|
||||
new GUIContent("镜头配置 SO", "赋给所有新 CameraArea._lensConfig;留空则不赋值"),
|
||||
_lensConfig, typeof(CameraLensConfigSO), false);
|
||||
|
||||
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
|
||||
using (new EditorGUI.DisabledScope(_sourcesParent == null))
|
||||
{
|
||||
if (GUILayout.Button("扫描区域", GUILayout.Height(30)))
|
||||
ScanZones();
|
||||
}
|
||||
|
||||
if (!_scanned) return;
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
if (_entries.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"未检测到旧格式区域。\n条件:BoxCollider2D + 含 \"TriggerRegion\" 字样的子节点。",
|
||||
MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 统计行 ─────────────────────────────────────────────────────────
|
||||
int migrated = 0, pending = 0;
|
||||
foreach (var e in _entries) { if (e.AlreadyMigrated) migrated++; else if (e.Selected) pending++; }
|
||||
|
||||
EditorGUILayout.LabelField(
|
||||
$"共 {_entries.Count} 个区域 | 已迁移 {migrated} | 已选待迁移 {pending}",
|
||||
EditorStyles.miniBoldLabel);
|
||||
|
||||
EditorGUILayout.Space(2);
|
||||
|
||||
// ── 条目列表 ───────────────────────────────────────────────────────
|
||||
_scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.MaxHeight(240));
|
||||
foreach (var entry in _entries)
|
||||
DrawEntryRow(entry);
|
||||
EditorGUILayout.EndScrollView();
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
|
||||
// ── 批量迁移 ───────────────────────────────────────────────────────
|
||||
int selectedPending = _entries.FindAll(e => e.Selected && !e.AlreadyMigrated).Count;
|
||||
using (new EditorGUI.DisabledScope(selectedPending == 0))
|
||||
{
|
||||
if (GUILayout.Button($"迁移已选中区域({selectedPending} 个)", GUILayout.Height(36)))
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var e in _entries)
|
||||
if (e.Selected && !e.AlreadyMigrated) { MigrateZone(e); count++; }
|
||||
_lastMigratedCount = count;
|
||||
ScanZones();
|
||||
Debug.Log($"[迁移工具] 完成迁移 {count} 个相机区域。");
|
||||
}
|
||||
}
|
||||
|
||||
if (_lastMigratedCount > 0)
|
||||
EditorGUILayout.HelpBox(
|
||||
$"上次迁移完成 {_lastMigratedCount} 个。请在 Scene 视图确认后保存场景(Ctrl+S)。",
|
||||
MessageType.Info);
|
||||
}
|
||||
|
||||
private void DrawEntryRow(ZoneEntry entry)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox))
|
||||
{
|
||||
// 选择框(已迁移的不可再选)
|
||||
using (new EditorGUI.DisabledScope(entry.AlreadyMigrated))
|
||||
entry.Selected = EditorGUILayout.Toggle(entry.Selected, GUILayout.Width(16));
|
||||
|
||||
// 名称
|
||||
Color prev = GUI.color;
|
||||
GUI.color = entry.AlreadyMigrated ? kMuted : kOk;
|
||||
EditorGUILayout.LabelField(entry.ZoneObj.name, GUILayout.Width(170));
|
||||
GUI.color = prev;
|
||||
|
||||
// 可视框尺寸
|
||||
if (entry.VisibleBox != null)
|
||||
{
|
||||
Vector2 sz = entry.VisibleBox.size;
|
||||
EditorGUILayout.LabelField($"Box {sz.x:F0}×{sz.y:F0}", EditorStyles.miniLabel, GUILayout.Width(72));
|
||||
}
|
||||
else
|
||||
{
|
||||
GUI.color = kWarning;
|
||||
EditorGUILayout.LabelField("无 Box2D", EditorStyles.miniLabel, GUILayout.Width(72));
|
||||
GUI.color = prev;
|
||||
}
|
||||
|
||||
// 触发点数
|
||||
Color ptColor = entry.TriggerWorldPts.Count >= 3 ? kOk : kWarning;
|
||||
GUI.color = ptColor;
|
||||
EditorGUILayout.LabelField($"触发 {entry.TriggerWorldPts.Count}pt", EditorStyles.miniLabel, GUILayout.Width(54));
|
||||
GUI.color = prev;
|
||||
|
||||
// 限位来源
|
||||
string confLabel = entry.ConfinerCollider is PolygonCollider2D ? "Poly限位"
|
||||
: entry.ConfinerCollider is BoxCollider2D ? "Box限位"
|
||||
: "默认矩形";
|
||||
EditorGUILayout.LabelField(confLabel, EditorStyles.miniLabel, GUILayout.Width(54));
|
||||
|
||||
// 状态 / 单独迁移按钮
|
||||
if (entry.AlreadyMigrated)
|
||||
{
|
||||
GUI.color = kMuted;
|
||||
EditorGUILayout.LabelField("已迁移", EditorStyles.miniLabel, GUILayout.Width(46));
|
||||
GUI.color = prev;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("迁移", GUILayout.Width(44), GUILayout.Height(16)))
|
||||
{
|
||||
MigrateZone(entry);
|
||||
_lastMigratedCount = 1;
|
||||
ScanZones();
|
||||
}
|
||||
}
|
||||
|
||||
// Ping 旧对象
|
||||
if (GUILayout.Button("●", GUILayout.Width(20), GUILayout.Height(16)))
|
||||
EditorGUIUtility.PingObject(entry.ZoneObj);
|
||||
}
|
||||
}
|
||||
|
||||
// ══ 扫描 ══════════════════════════════════════════════════════════════
|
||||
|
||||
private void ScanZones()
|
||||
{
|
||||
_entries.Clear();
|
||||
_scanned = true;
|
||||
|
||||
if (_sourcesParent == null) return;
|
||||
|
||||
foreach (Transform child in _sourcesParent)
|
||||
{
|
||||
// 旧 Zone 的标识:子节点直属,且挂有 BoxCollider2D
|
||||
var box = child.GetComponent<BoxCollider2D>();
|
||||
if (box == null) continue;
|
||||
|
||||
var entry = new ZoneEntry { ZoneObj = child.gameObject, VisibleBox = box };
|
||||
|
||||
// 收集触发多边形顶点(TriggerRegion 子节点的各个点对象)
|
||||
Transform triggerRoot = FindChildContaining(child, "TriggerRegion");
|
||||
if (triggerRoot != null)
|
||||
foreach (Transform pt in triggerRoot)
|
||||
entry.TriggerWorldPts.Add((Vector2)pt.position);
|
||||
|
||||
// 读取限位碰撞体(Zone_xxx_Confiner 上的 Collider2D)
|
||||
Transform confinerT = FindChildContaining(child, "Confiner");
|
||||
if (confinerT != null)
|
||||
entry.ConfinerCollider = confinerT.GetComponent<Collider2D>();
|
||||
|
||||
// 是否已经完成迁移(自身或子节点含 CameraArea)
|
||||
entry.AlreadyMigrated =
|
||||
child.GetComponent<CameraArea>() != null ||
|
||||
child.GetComponentInChildren<CameraArea>(true) != null;
|
||||
|
||||
_entries.Add(entry);
|
||||
}
|
||||
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private static Transform FindChildContaining(Transform parent, string keyword)
|
||||
{
|
||||
foreach (Transform child in parent)
|
||||
if (child.name.IndexOf(keyword, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return child;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ══ 迁移 ══════════════════════════════════════════════════════════════
|
||||
|
||||
private void MigrateZone(ZoneEntry entry)
|
||||
{
|
||||
GameObject zoneGO = entry.ZoneObj;
|
||||
Transform parent = _targetParent != null ? _targetParent : zoneGO.transform.parent;
|
||||
Vector3 worldPos = zoneGO.transform.position;
|
||||
|
||||
// ── 1. 计算本地可视 Rect(相对于新 CameraArea 的世界位置)────────
|
||||
Rect localBounds;
|
||||
if (entry.VisibleBox != null)
|
||||
{
|
||||
// 注意:BoxCollider2D.bounds 在 inactive 对象上无效,必须手动计算
|
||||
Bounds wb = GetColliderWorldBounds(entry.VisibleBox);
|
||||
localBounds = new Rect(
|
||||
wb.min.x - worldPos.x,
|
||||
wb.min.y - worldPos.y,
|
||||
wb.size.x, wb.size.y);
|
||||
}
|
||||
else
|
||||
{
|
||||
localBounds = new Rect(-12f, -6f, 24f, 12f);
|
||||
}
|
||||
|
||||
// ── 2. 创建 CameraArea 根节点 ─────────────────────────────────────
|
||||
GameObject areaGO = new GameObject(zoneGO.name);
|
||||
Undo.RegisterCreatedObjectUndo(areaGO, "Migrate Camera Zone");
|
||||
areaGO.transform.SetParent(parent, worldPositionStays: false);
|
||||
areaGO.transform.position = worldPos;
|
||||
|
||||
// 紧跟旧对象之后(同级排列时保持顺序直观)
|
||||
if (parent == zoneGO.transform.parent)
|
||||
areaGO.transform.SetSiblingIndex(zoneGO.transform.GetSiblingIndex() + 1);
|
||||
|
||||
CameraArea area = areaGO.AddComponent<CameraArea>();
|
||||
|
||||
var soArea = new SerializedObject(area);
|
||||
soArea.FindProperty("_visibleBounds").rectValue = localBounds;
|
||||
if (_lensConfig != null)
|
||||
soArea.FindProperty("_lensConfig").objectReferenceValue = _lensConfig;
|
||||
soArea.ApplyModifiedProperties();
|
||||
|
||||
// ── 3. 创建 AreaBoundary(限位多边形,isTrigger = true)──────────
|
||||
GameObject boundaryGO = new GameObject($"{zoneGO.name}_AreaBoundary");
|
||||
Undo.RegisterCreatedObjectUndo(boundaryGO, "Migrate Camera Zone");
|
||||
boundaryGO.transform.SetParent(areaGO.transform, worldPositionStays: false);
|
||||
boundaryGO.transform.localPosition = Vector3.zero;
|
||||
|
||||
PolygonCollider2D confinerPoly = boundaryGO.AddComponent<PolygonCollider2D>();
|
||||
confinerPoly.isTrigger = true;
|
||||
confinerPoly.pathCount = 1;
|
||||
confinerPoly.SetPath(0, BuildConfinerPath(entry, worldPos, localBounds));
|
||||
|
||||
// 绑定 _confinerCollider
|
||||
soArea.Update();
|
||||
soArea.FindProperty("_confinerCollider").objectReferenceValue = confinerPoly;
|
||||
soArea.ApplyModifiedProperties();
|
||||
EditorUtility.SetDirty(area);
|
||||
|
||||
// 绑定完成后立即按 FOV/Depth 公式同步限位多边形,
|
||||
// 确保与 LensConfig 参数一致(创建时不会自动同步)
|
||||
float syncFOV = _lensConfig != null ? _lensConfig.fieldOfView : 60f;
|
||||
float syncAspect = UnityEngine.Camera.main != null ? UnityEngine.Camera.main.aspect : 16f / 9f;
|
||||
CameraAreaEditor.SyncConfinerFromVisibleBounds(area, syncFOV, syncAspect);
|
||||
|
||||
// ── 4. 创建 TriggerZone(相机激活触发器)───────────────────────
|
||||
GameObject triggerGO = new GameObject($"{zoneGO.name}_TriggerZone");
|
||||
Undo.RegisterCreatedObjectUndo(triggerGO, "Migrate Camera Zone");
|
||||
triggerGO.transform.SetParent(areaGO.transform, worldPositionStays: false);
|
||||
triggerGO.transform.localPosition = Vector3.zero;
|
||||
|
||||
// AddComponent 会因 [RequireComponent] 自动添加 PolygonCollider2D
|
||||
CameraTriggerZone triggerComp = triggerGO.AddComponent<CameraTriggerZone>();
|
||||
PolygonCollider2D triggerPoly = triggerGO.GetComponent<PolygonCollider2D>();
|
||||
triggerPoly.isTrigger = true;
|
||||
triggerPoly.pathCount = 1;
|
||||
triggerPoly.SetPath(0, BuildTriggerPath(entry, worldPos, localBounds));
|
||||
|
||||
// _targetArea → 指向刚创建的 CameraArea
|
||||
var soTrigger = new SerializedObject(triggerComp);
|
||||
soTrigger.FindProperty("_targetArea").objectReferenceValue = area;
|
||||
soTrigger.ApplyModifiedProperties();
|
||||
EditorUtility.SetDirty(triggerComp);
|
||||
|
||||
// ── 5. 处理旧对象 ──────────────────────────────────────────────
|
||||
// 先记录原始激活状态,再对旧对象做处理,避免 SetActive(false) 后误读
|
||||
bool wasActive = zoneGO.activeSelf;
|
||||
|
||||
|
||||
|
||||
// 同步旧区域的激活状态(旧 Zone_xxx 若为禁用,新对象同样禁用)
|
||||
if (!wasActive)
|
||||
areaGO.SetActive(false);
|
||||
|
||||
EditorUtility.SetDirty(areaGO);
|
||||
EditorSceneManager.MarkSceneDirty(zoneGO.scene);
|
||||
EditorGUIUtility.PingObject(areaGO);
|
||||
|
||||
Debug.Log($"[迁移工具] {zoneGO.name} → {areaGO.name} " +
|
||||
$"可视 {localBounds.width:F0}×{localBounds.height:F0} " +
|
||||
$"触发 {triggerPoly.GetTotalPointCount()} pt " +
|
||||
$"限位 {confinerPoly.GetTotalPointCount()} pt");
|
||||
}
|
||||
|
||||
// ── 限位多边形路径 ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 按优先级构建限位多边形路径(本地坐标,相对于新 CameraArea):
|
||||
/// 1. Zone_xxx_Confiner 上的 PolygonCollider2D → 直接转换
|
||||
/// 2. Zone_xxx_Confiner 上的 BoxCollider2D → 取 AABB 四角
|
||||
/// 3. 兜底 → 使用可视矩形
|
||||
/// </summary>
|
||||
private static Vector2[] BuildConfinerPath(ZoneEntry entry, Vector3 areaWorldPos, Rect fallback)
|
||||
{
|
||||
if (entry.ConfinerCollider is PolygonCollider2D poly && poly.pathCount > 0)
|
||||
{
|
||||
var pts = new List<Vector2>();
|
||||
poly.GetPath(0, pts);
|
||||
var result = new Vector2[pts.Count];
|
||||
for (int i = 0; i < pts.Count; i++)
|
||||
result[i] = (Vector2)poly.transform.TransformPoint(pts[i]) - (Vector2)areaWorldPos;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (entry.ConfinerCollider is BoxCollider2D box)
|
||||
{
|
||||
Bounds b = GetColliderWorldBounds(box);
|
||||
return new Vector2[]
|
||||
{
|
||||
new Vector2(b.min.x - areaWorldPos.x, b.min.y - areaWorldPos.y),
|
||||
new Vector2(b.min.x - areaWorldPos.x, b.max.y - areaWorldPos.y),
|
||||
new Vector2(b.max.x - areaWorldPos.x, b.max.y - areaWorldPos.y),
|
||||
new Vector2(b.max.x - areaWorldPos.x, b.min.y - areaWorldPos.y),
|
||||
};
|
||||
}
|
||||
|
||||
// 兜底:可视矩形
|
||||
return RectToPolygon(fallback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建触发多边形路径(本地坐标,相对于新 CameraArea):
|
||||
/// 取 TriggerRegion 各点的世界坐标减去 areaWorldPos;
|
||||
/// 若点数不足 3 则兜底使用可视矩形。
|
||||
/// </summary>
|
||||
private static Vector2[] BuildTriggerPath(ZoneEntry entry, Vector3 areaWorldPos, Rect fallback)
|
||||
{
|
||||
if (entry.TriggerWorldPts.Count >= 3)
|
||||
{
|
||||
var path = new Vector2[entry.TriggerWorldPts.Count];
|
||||
for (int i = 0; i < path.Length; i++)
|
||||
path[i] = entry.TriggerWorldPts[i] - (Vector2)areaWorldPos;
|
||||
return path;
|
||||
}
|
||||
return RectToPolygon(fallback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动计算 BoxCollider2D 的世界 AABB,不依赖 .bounds(inactive 对象上 .bounds 无效)。
|
||||
/// </summary>
|
||||
private static Bounds GetColliderWorldBounds(BoxCollider2D box)
|
||||
{
|
||||
Vector2 worldCenter = (Vector2)box.transform.TransformPoint(box.offset);
|
||||
Vector2 worldSize = new Vector2(
|
||||
box.size.x * Mathf.Abs(box.transform.lossyScale.x),
|
||||
box.size.y * Mathf.Abs(box.transform.lossyScale.y));
|
||||
return new Bounds(worldCenter, worldSize);
|
||||
}
|
||||
|
||||
private static Vector2[] RectToPolygon(Rect r)
|
||||
{
|
||||
return new Vector2[]
|
||||
{
|
||||
new Vector2(r.xMin, r.yMin),
|
||||
new Vector2(r.xMin, r.yMax),
|
||||
new Vector2(r.xMax, r.yMax),
|
||||
new Vector2(r.xMax, r.yMin),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3578efbe9a4a182448fa721fcbe75853
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,268 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
109
Assets/_Game/Scripts/Editor/Scene/PersistentSceneAutoLoader.cs
Normal file
109
Assets/_Game/Scripts/Editor/Scene/PersistentSceneAutoLoader.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 编辑器 Edit Mode 辅助:打开任意场景时自动将 Persistent 场景 Additive 加入 Hierarchy。
|
||||
///
|
||||
/// 职责范围(仅限 Edit Mode):
|
||||
/// 让设计师在编辑房间场景时,Inspector 中可直接看到并配置 GameManager / SceneService
|
||||
/// 等 Persistent 场景内的组件,无需手动 Open Additive。
|
||||
///
|
||||
/// 运行时(Play Mode / 发行版 Build)的保证由 GameBootstrap(Runtime 程序集)负责,
|
||||
/// 本脚本与 Play Mode 状态无关,不监听 playModeStateChanged。
|
||||
///
|
||||
/// 菜单:BaseGames/Tools/Edit Mode: Auto-Open Persistent Scene
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
public static class PersistentSceneAutoLoader
|
||||
{
|
||||
// ── 常量 ─────────────────────────────────────────────────────────────
|
||||
private const string MenuPath = "BaseGames/Tools/Edit Mode: Auto-Open Persistent Scene";
|
||||
private const string PrefKey = "BaseGames_EditAutoOpen_Persistent";
|
||||
private const string PersistentSceneName = "Scene_Persistent";
|
||||
|
||||
// ── 构造(Editor 启动时执行)──────────────────────────────────────────
|
||||
static PersistentSceneAutoLoader()
|
||||
{
|
||||
EditorSceneManager.sceneOpened += OnSceneOpened;
|
||||
|
||||
// 启动时补一次检查:Editor 已打开但 Persistent 不在 Hierarchy 的场景
|
||||
EditorApplication.delayCall += EnsurePersistentInHierarchyEditMode;
|
||||
}
|
||||
|
||||
// ── 菜单 ─────────────────────────────────────────────────────────────
|
||||
[MenuItem(MenuPath, validate = false, priority = 301)]
|
||||
private static void ToggleEnabled()
|
||||
{
|
||||
bool current = EditorPrefs.GetBool(PrefKey, true);
|
||||
EditorPrefs.SetBool(PrefKey, !current);
|
||||
}
|
||||
|
||||
[MenuItem(MenuPath, validate = true)]
|
||||
private static bool ToggleEnabledValidate()
|
||||
{
|
||||
Menu.SetChecked(MenuPath, EditorPrefs.GetBool(PrefKey, true));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── 场景打开回调 ──────────────────────────────────────────────────────
|
||||
private static void OnSceneOpened(Scene scene, OpenSceneMode mode)
|
||||
{
|
||||
// 若是 Persistent 本身被打开,无需额外处理
|
||||
if (IsPersistentScene(scene.name)) return;
|
||||
|
||||
// Single 模式(替换当前场景)或 Additive 加入新场景时,确保 Persistent 也在 Hierarchy 中
|
||||
// 使用 delayCall 避免在场景加载中途调用 OpenScene
|
||||
EditorApplication.delayCall += EnsurePersistentInHierarchyEditMode;
|
||||
}
|
||||
|
||||
// ── 核心逻辑 ──────────────────────────────────────────────────────────
|
||||
private static void EnsurePersistentInHierarchyEditMode()
|
||||
{
|
||||
// 仅在 Edit Mode 执行(Play Mode 由 GameBootstrap 负责)
|
||||
if (Application.isPlaying) return;
|
||||
if (!EditorPrefs.GetBool(PrefKey, true)) return;
|
||||
|
||||
// 若 Persistent 已在 Hierarchy,无需操作
|
||||
for (int i = 0; i < SceneManager.sceneCount; i++)
|
||||
if (IsPersistentScene(SceneManager.GetSceneAt(i).name)) return;
|
||||
|
||||
// 查找并 Additive 打开 Persistent 场景
|
||||
string path = FindPersistentScenePath();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[PersistentAutoLoader] 未找到 '{PersistentSceneName}' 场景。" +
|
||||
"请确认场景已添加到 Build Settings 或可在 Assets 中搜索到。");
|
||||
return;
|
||||
}
|
||||
|
||||
EditorSceneManager.OpenScene(path, OpenSceneMode.Additive);
|
||||
}
|
||||
|
||||
// ── 工具函数 ──────────────────────────────────────────────────────────
|
||||
private static bool IsPersistentScene(string sceneName)
|
||||
=> sceneName == PersistentSceneName || sceneName == "Persistent";
|
||||
|
||||
private static string FindPersistentScenePath()
|
||||
{
|
||||
// 优先从 Build Settings 查找(保证与 GameBootstrap 使用同一文件)
|
||||
foreach (var buildScene in EditorBuildSettings.scenes)
|
||||
{
|
||||
if (!buildScene.enabled) continue;
|
||||
string name = System.IO.Path.GetFileNameWithoutExtension(buildScene.path);
|
||||
if (IsPersistentScene(name)) return buildScene.path;
|
||||
}
|
||||
|
||||
// 回退:在 AssetDatabase 中搜索
|
||||
string[] guids = AssetDatabase.FindAssets($"t:Scene {PersistentSceneName}");
|
||||
if (guids.Length > 0) return AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
|
||||
guids = AssetDatabase.FindAssets("t:Scene Persistent");
|
||||
return guids.Length > 0 ? AssetDatabase.GUIDToAssetPath(guids[0]) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1b5ab9e5f153fb148817239307245e00
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -385,46 +385,101 @@ namespace BaseGames.Editor
|
||||
MarkDirtyAndLog("Room Transition", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Room Camera", priority = 140)]
|
||||
public static void PlaceRoomCamera()
|
||||
[MenuItem("BaseGames/Scene/Place/Camera Area", priority = 140)]
|
||||
public static void PlaceCameraArea() => PlaceCameraArea("CameraArea");
|
||||
|
||||
/// <param name="areaName">
|
||||
/// 生成的 CameraArea GameObject 名称。
|
||||
/// 子节点 AreaBoundary 和 TriggerZone 将以此为前缀命名(如 MyZone_AreaBoundary)。
|
||||
/// </param>
|
||||
/// <param name="parent">生成的 GameObject 所挂载的父节点(为 null 时放置于场景根节点)。</param>
|
||||
public static void PlaceCameraArea(string areaName, Transform parent = null)
|
||||
{
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place Camera Area (+ TriggerZone)");
|
||||
|
||||
GameObject go = new GameObject("RoomCamera");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place Room Camera");
|
||||
go.transform.position = GetDropPosition();
|
||||
Vector3 pos = GetDropPosition();
|
||||
|
||||
CinemachineCamera cinemachine = GetOrAddComponent<CinemachineCamera>(go);
|
||||
RoomCamera roomCamera = GetOrAddComponent<RoomCamera>(go);
|
||||
CinemachineConfiner2D confiner = GetOrAddComponent<CinemachineConfiner2D>(go);
|
||||
// ── CameraArea ─────────────────────────────────────────────────────
|
||||
GameObject go = new GameObject(areaName);
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place Camera Area");
|
||||
go.transform.position = pos;
|
||||
if (parent != null)
|
||||
Undo.SetTransformParent(go.transform, parent, "Parent Camera Area");
|
||||
|
||||
// RoomBoundary child — defines the camera confinement area
|
||||
Transform boundaryT = GetOrCreateChild(go.transform, "RoomBoundary");
|
||||
CameraArea cameraArea = GetOrAddComponent<CameraArea>(go);
|
||||
|
||||
// AreaBoundary child — 提供 CinemachineConfiner2D 所需的限位多边形(isTrigger = true,仅作为相机约束边界)
|
||||
Transform boundaryT = GetOrCreateChild(go.transform, $"{areaName}_AreaBoundary");
|
||||
PolygonCollider2D boundaryCollider = GetOrAddComponent<PolygonCollider2D>(boundaryT.gameObject);
|
||||
boundaryCollider.isTrigger = true;
|
||||
boundaryCollider.pathCount = 1;
|
||||
// 顶点必须逆时针(CCW)排列:Cinemachine 底层 Clipper 库对 CW 多边形(area<0)会取反 delta,
|
||||
// 导致向外膨胀而非向内收缩,相机将不受限制地跑出边界。
|
||||
boundaryCollider.SetPath(0, new Vector2[]
|
||||
{
|
||||
new Vector2(-12f, -6f),
|
||||
new Vector2(-12f, 6f),
|
||||
new Vector2( 12f, 6f),
|
||||
new Vector2( 12f, -6f),
|
||||
new Vector2(-12f, -6f), // BL
|
||||
new Vector2( 12f, -6f), // BR
|
||||
new Vector2( 12f, 6f), // TR
|
||||
new Vector2(-12f, 6f), // TL
|
||||
});
|
||||
RoomVisibleArea visibleArea = GetOrAddComponent<RoomVisibleArea>(boundaryT.gameObject);
|
||||
|
||||
AssignReference(roomCamera, "_visibleArea", visibleArea, report);
|
||||
AssignReference(confiner, "m_BoundingShape2D", boundaryCollider, report);
|
||||
AssignReference(cameraArea, "_confinerCollider", boundaryCollider, report);
|
||||
|
||||
// Disable any Camera and AudioListener added by Cinemachine
|
||||
UnityEngine.Camera cam = go.GetComponent<UnityEngine.Camera>();
|
||||
if (cam != null) cam.enabled = false;
|
||||
AudioListener al = go.GetComponent<AudioListener>();
|
||||
if (al != null) { Undo.DestroyObjectImmediate(al); }
|
||||
// ── CameraTriggerZone(配对)─────────────────────────────────────────
|
||||
GameObject zoneGo = new GameObject($"{areaName}_TriggerZone");
|
||||
Undo.RegisterCreatedObjectUndo(zoneGo, "Place Camera Trigger Zone");
|
||||
zoneGo.transform.position = pos;
|
||||
SetLayer(zoneGo, "TriggerZone", report);
|
||||
|
||||
report.Add("将 Player/CameraFollowTarget Transform 拖入 CinemachineCamera.Follow 字段以跟随玩家(或使用 Room Camera Setup 工具批量赋值)。");
|
||||
report.Add("调整 RoomBoundary PolygonCollider2D 顶点以匹配房间边界。");
|
||||
PolygonCollider2D col = GetOrAddComponent<PolygonCollider2D>(zoneGo);
|
||||
col.isTrigger = true;
|
||||
// 默认矩形轮廓(CCW),与 AreaBoundary 默认尺寸一致(可在 Inspector 中编辑顶点调整为任意多边形)
|
||||
col.SetPath(0, new Vector2[]
|
||||
{
|
||||
new Vector2(-12f, -6f), // BL
|
||||
new Vector2( 12f, -6f), // BR
|
||||
new Vector2( 12f, 6f), // TR
|
||||
new Vector2(-12f, 6f), // TL
|
||||
});
|
||||
|
||||
CameraTriggerZone zone = GetOrAddComponent<CameraTriggerZone>(zoneGo);
|
||||
AssignReference(zone, "_targetArea", cameraArea, report);
|
||||
// TriggerZone 归入 CameraArea 节点,方便统一调整与查找
|
||||
Undo.SetTransformParent(zoneGo.transform, go.transform, "Parent TriggerZone to CameraArea");
|
||||
zoneGo.transform.localPosition = Vector3.zero;
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
|
||||
report.Add($"调整 {areaName}_AreaBoundary PolygonCollider2D 顶点以匹配区域边界。");
|
||||
report.Add($"调整 {areaName}_TriggerZone PolygonCollider2D 顶点以匹配入口走廊(支持任意多边形)。");
|
||||
|
||||
// ── 自动关联到同场景 RoomController(若其 _cameraArea 为空)────────
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
var roomControllers = Object.FindObjectsByType<RoomController>(FindObjectsSortMode.None);
|
||||
#else
|
||||
var roomControllers = Object.FindObjectsOfType<RoomController>();
|
||||
#endif
|
||||
bool autoAssigned = false;
|
||||
foreach (var rc in roomControllers)
|
||||
{
|
||||
// 仅使用反射检查,避免每次都覆盖已绑定的引用
|
||||
var fi = typeof(RoomController).GetField("_cameraArea",
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||||
if (fi == null) continue;
|
||||
if (fi.GetValue(rc) != null) continue;
|
||||
|
||||
Undo.RecordObject(rc, "Auto-assign CameraArea to RoomController");
|
||||
fi.SetValue(rc, cameraArea);
|
||||
EditorUtility.SetDirty(rc);
|
||||
report.Add($"✅ 已自动将 {areaName} 关联到 {rc.gameObject.name}.RoomController._cameraArea。");
|
||||
autoAssigned = true;
|
||||
}
|
||||
if (!autoAssigned)
|
||||
report.Add("将此 CameraArea 拖入 RoomController._cameraArea 字段(未找到空 _cameraArea 的 RoomController)。");
|
||||
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("Room Camera", go, report);
|
||||
MarkDirtyAndLog($"Camera Area (+ TriggerZone): {areaName}", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Ground Platform", priority = 150)]
|
||||
@@ -534,28 +589,6 @@ namespace BaseGames.Editor
|
||||
MarkDirtyAndLog("Nav Surface", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Camera Trigger Zone", priority = 180)]
|
||||
public static void PlaceCameraTriggerZone()
|
||||
{
|
||||
var report = new List<string>();
|
||||
|
||||
GameObject go = new GameObject("CameraTriggerZone");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place Camera Trigger Zone");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "TriggerZone", report);
|
||||
|
||||
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(go);
|
||||
col.isTrigger = true;
|
||||
col.size = new Vector2(2f, 2f);
|
||||
|
||||
GetOrAddComponent<CameraTriggerZone>(go);
|
||||
|
||||
report.Add("将目标 RoomCamera 拖入 CameraTriggerZone._targetCamera 字段。");
|
||||
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("Camera Trigger Zone", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Obstacle (Static)", priority = 190)]
|
||||
public static void PlaceObstacle()
|
||||
{
|
||||
|
||||
@@ -86,6 +86,29 @@ namespace BaseGames.Editor
|
||||
CameraStateController cameraStateController = GetOrAddComponent<CameraStateController>(cameraStateGo);
|
||||
CinemachineImpulseSource impulseSource = GetOrAddComponent<CinemachineImpulseSource>(cameraStateGo);
|
||||
|
||||
// 垂直窥视系统:独立节点,CameraStateController 持引用
|
||||
GameObject lookSystemGo = GetOrCreateChild(camera, "CameraLookSystem").gameObject;
|
||||
CameraLookSystem lookSystem = GetOrAddComponent<CameraLookSystem>(lookSystemGo);
|
||||
|
||||
GameObject vcamAGo = GetOrCreateChild(camera, "VCamA").gameObject;
|
||||
CinemachineCamera vcamA = GetOrAddComponent<CinemachineCamera>(vcamAGo);
|
||||
GetOrAddComponent<CinemachineConfiner2D>(vcamAGo);
|
||||
GetOrAddComponent<CameraAxisLockExtension>(vcamAGo);
|
||||
GetOrAddComponent<CameraAsymmetricDampingExtension>(vcamAGo);
|
||||
GetOrAddComponent<CameraAdaptiveLookaheadExtension>(vcamAGo);
|
||||
// CinemachinePositionComposer:Body 阶段组件,必须存在;ConfigureSlot 依赖它写入所有相机跟随参数
|
||||
var composerA = GetOrAddComponent<CinemachinePositionComposer>(vcamAGo);
|
||||
ApplyComposerDefaults(composerA);
|
||||
|
||||
GameObject vcamBGo = GetOrCreateChild(camera, "VCamB").gameObject;
|
||||
CinemachineCamera vcamB = GetOrAddComponent<CinemachineCamera>(vcamBGo);
|
||||
GetOrAddComponent<CinemachineConfiner2D>(vcamBGo);
|
||||
GetOrAddComponent<CameraAxisLockExtension>(vcamBGo);
|
||||
GetOrAddComponent<CameraAsymmetricDampingExtension>(vcamBGo);
|
||||
GetOrAddComponent<CameraAdaptiveLookaheadExtension>(vcamBGo);
|
||||
var composerB = GetOrAddComponent<CinemachinePositionComposer>(vcamBGo);
|
||||
ApplyComposerDefaults(composerB);
|
||||
|
||||
GameObject uiRootGo = GetOrCreateChild(ui, "UIRoot").gameObject;
|
||||
UIManager uiManager = GetOrAddComponent<UIManager>(uiRootGo);
|
||||
|
||||
@@ -146,6 +169,11 @@ namespace BaseGames.Editor
|
||||
|
||||
AssignReference(cameraStateController, "_brain", brain);
|
||||
AssignReference(cameraStateController, "_impulseSource", impulseSource);
|
||||
AssignReference(cameraStateController, "_lookSystem", lookSystem);
|
||||
AssignReference(cameraStateController, "_vcamA", vcamA);
|
||||
AssignReference(cameraStateController, "_vcamB", vcamB);
|
||||
AssignAsset(cameraStateController, "_onPlayerSpawned", report, true, "EVT_PlayerSpawned");
|
||||
AssignAsset(cameraStateController, "_lensConfig", report, false, "CAM_LensConfig", "LensConfig", "CameraLensConfig");
|
||||
|
||||
AssignReference(uiManager, "_hudRoot", hudRootGo);
|
||||
AssignReference(uiManager, "_pauseMenuRoot", pauseRootGo);
|
||||
@@ -189,13 +217,12 @@ namespace BaseGames.Editor
|
||||
// ── [Camera] ───────────────────────────────────────────────────
|
||||
Transform cameraGroup = GetOrCreateChild(root.transform, "[Camera]");
|
||||
|
||||
GameObject roomCameraGo = GetOrCreateChild(cameraGroup, "RoomCamera").gameObject;
|
||||
CinemachineCamera cinemachineCamera = GetOrAddComponent<CinemachineCamera>(roomCameraGo);
|
||||
RoomCamera roomCamera = GetOrAddComponent<RoomCamera>(roomCameraGo);
|
||||
CinemachineConfiner2D confiner = GetOrAddComponent<CinemachineConfiner2D>(roomCameraGo);
|
||||
// CameraArea — 定义相机区域(限位 + 混合配置 + 可选专有 VCam)
|
||||
GameObject cameraAreaGo = GetOrCreateChild(cameraGroup, "CameraArea").gameObject;
|
||||
CameraArea cameraArea = GetOrAddComponent<CameraArea>(cameraAreaGo);
|
||||
|
||||
// RoomBoundary — defines visible area and confiner polygon
|
||||
Transform boundaryT = GetOrCreateChild(roomCameraGo.transform, "RoomBoundary");
|
||||
// AreaBoundary — 提供 CinemachineConfiner2D 所需的限位多边形
|
||||
Transform boundaryT = GetOrCreateChild(cameraAreaGo.transform, "AreaBoundary");
|
||||
PolygonCollider2D boundaryCollider = GetOrAddComponent<PolygonCollider2D>(boundaryT.gameObject);
|
||||
boundaryCollider.pathCount = 1;
|
||||
boundaryCollider.SetPath(0, new Vector2[]
|
||||
@@ -203,16 +230,8 @@ namespace BaseGames.Editor
|
||||
new Vector2(-12f, -6f), new Vector2(-12f, 6f),
|
||||
new Vector2( 12f, 6f), new Vector2( 12f, -6f),
|
||||
});
|
||||
RoomVisibleArea visibleArea = GetOrAddComponent<RoomVisibleArea>(boundaryT.gameObject);
|
||||
|
||||
AssignReference(roomCamera, "_visibleArea", visibleArea);
|
||||
AssignReference(confiner, "m_BoundingShape2D", boundaryCollider);
|
||||
|
||||
// Disable stray Camera / AudioListener components sometimes added by Cinemachine
|
||||
UnityEngine.Camera staleCam = roomCameraGo.GetComponent<UnityEngine.Camera>();
|
||||
if (staleCam != null) staleCam.enabled = false;
|
||||
AudioListener staleAl = roomCameraGo.GetComponent<AudioListener>();
|
||||
if (staleAl != null) { Undo.DestroyObjectImmediate(staleAl); }
|
||||
AssignReference(cameraArea, "_confinerCollider", boundaryCollider);
|
||||
|
||||
// ── [SpawnPoints] ──────────────────────────────────────────────
|
||||
Transform spawnGroup = GetOrCreateChild(root.transform, "[SpawnPoints]");
|
||||
@@ -250,7 +269,7 @@ namespace BaseGames.Editor
|
||||
GetOrCreateChild(root.transform, "[Transitions]");
|
||||
|
||||
// ── Wire RoomController ────────────────────────────────────────
|
||||
AssignReference(roomController, "_roomCamera", roomCamera);
|
||||
AssignReference(roomController, "_cameraArea", cameraArea);
|
||||
|
||||
SerializedObject roomSO = new SerializedObject(roomController);
|
||||
SerializedProperty spawnArrayProp = roomSO.FindProperty("_spawnPoints");
|
||||
@@ -263,8 +282,7 @@ namespace BaseGames.Editor
|
||||
|
||||
// ── Report ─────────────────────────────────────────────────────
|
||||
report.Add("在 RoomController._roomId 填写唯一房间 ID(如 \"Room_Forest_01\")。");
|
||||
report.Add("将 Player/CameraFollowTarget Transform 拖入 CinemachineCamera.Follow 字段以跟随玩家(或使用 BaseGames → Camera → Room Camera Setup 工具批量赋值)。");
|
||||
report.Add("调整 RoomBoundary PolygonCollider2D 顶点以匹配实际房间大小。");
|
||||
report.Add("调整 AreaBoundary PolygonCollider2D 顶点以匹配实际房间大小。");
|
||||
report.Add("使用 Tile Palette 在 Ground Tilemap 上绘制地形,然后在 NavSurface Inspector 中点击 Bake。");
|
||||
report.Add("[Transitions] 子节点下使用 BaseGames/Scene/Place/Room Transition 添加过渡点。");
|
||||
|
||||
@@ -561,6 +579,34 @@ namespace BaseGames.Editor
|
||||
|
||||
Debug.LogWarning($"[SceneScaffoldTools] {scaffoldName} 完成,但仍有 {report.Count} 项需要手工确认:\n- {string.Join("\n- ", report)}", root);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 VCam 上的 CinemachinePositionComposer 写入初始默认展示参数。
|
||||
/// 这些値与 <see cref="CameraArea"/> 的默认値一致,确保脆架生成后 Scene 预览即有正确感觉。
|
||||
/// 运行时 CameraStateController.ConfigureSlot 会在每次 SwitchArea 时用 per-area 配置覆写。
|
||||
/// </summary>
|
||||
private static void ApplyComposerDefaults(CinemachinePositionComposer composer)
|
||||
{
|
||||
if (composer == null) return;
|
||||
|
||||
// 屏幕位置:玩家稍低于中心,上方有更多视野
|
||||
var comp = composer.Composition;
|
||||
comp.ScreenPosition = new Vector2(0f, -0.15f);
|
||||
comp.DeadZone.Enabled = true;
|
||||
comp.DeadZone.Size = new Vector2(0.15f, 0.05f);
|
||||
composer.Composition = comp;
|
||||
|
||||
// 阻尼:X 轻度缓冲,Y = 0(由 CameraAsymmetricDampingExtension 接管非对称 Y 阻尼)
|
||||
composer.Damping = new Vector3(0.5f, 0f, 0f);
|
||||
|
||||
// Lookahead:水平引领预测开启,IgnoreY = true(平台游戏 Y 轴不预测,避免起跳时镜头猛拉)
|
||||
var lah = composer.Lookahead;
|
||||
lah.Enabled = true;
|
||||
lah.Time = 0.28f;
|
||||
lah.Smoothing = 5f;
|
||||
lah.IgnoreY = true;
|
||||
composer.Lookahead = lah;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user