摄像机区域的优化

This commit is contained in:
2026-05-17 07:56:12 +08:00
parent f264329751
commit d25f237e76
62 changed files with 25774 additions and 5450 deletions

View 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
/// ├─ AreaBoundaryPolygonCollider2DisTrigger=true对应旧 Confiner
/// └─ TriggerZoneCameraTriggerZone + 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不依赖 .boundsinactive 对象上 .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),
};
}
}
}