Files
zeling_v2/Assets/_Game/Scripts/Editor/Camera/CameraZoneMigrationTool.cs
2026-05-19 11:50:21 +08:00

486 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
/// ├─ AreaBoundaryBoxCollider对应旧 Confiner
/// ├─ TriggerZoneCameraTriggerZone + PolygonCollider2D对应旧 TriggerRegion
/// └─ VCam_xxxCinemachineCamera + 所有扩展组件,专属虚拟相机)
///
/// 菜单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 bool _createDedicatedVCam = true; // 为每个区域创建专属 CinemachineCamera
// ── 运行时状态 ────────────────────────────────────────────────────────
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);
_createDedicatedVCam = EditorGUILayout.Toggle(
new GUIContent("创建专属 VCam",
"为每个迁移区域创建子节点 CinemachineCamera含所有扩展组件\n" +
"并绑定到 CameraArea._dedicatedCamera。"),
_createDedicatedVCam);
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)
{
Debug.Log($"[root]:{child.name}");
// 旧 Zone 的标识:子节点直属,且挂有 BoxCollider2D
var box = child.GetComponent<BoxCollider2D>();
if (box == null) continue;
var entry = new ZoneEntry { ZoneObj = child.gameObject, VisibleBox = box };
// 收集触发多边形顶点——xxx_TriggerRegion 下每个子节点的世界坐标即一个顶点,
// 按子节点顺序依次连线围成多边形触发区域。
Transform triggerRoot = FindChildContaining(child, "Trigger");
if (triggerRoot != null){
Debug.Log($"[trigger]:{triggerRoot.name}");
foreach (Transform pt in triggerRoot){
Debug.Log($"{pt.name}");
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限位体积 BoxCollider─────────────────────────────
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;
BoxCollider confinerBox = boundaryGO.AddComponent<BoxCollider>();
confinerBox.isTrigger = true;
confinerBox.center = new Vector3(0f, 0f, -10f); // Z 占位符SyncConfiner 会立即重算
confinerBox.size = new Vector3(localBounds.width, localBounds.height, 1f);
// 绑定 _confinerCollider
soArea.Update();
soArea.FindProperty("_confinerCollider").objectReferenceValue = confinerBox;
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>();
// 将旧触发多边形路径(本地坐标,相对于新 CameraArea 世界位置)直接赋给 PolygonCollider2D
Vector2[] triggerPath = BuildTriggerPath(entry, worldPos, localBounds);
triggerPoly.isTrigger = true;
triggerPoly.SetPath(0, triggerPath);
// _targetArea → 指向刚创建的 CameraArea
var soTrigger = new SerializedObject(triggerComp);
soTrigger.FindProperty("_targetArea").objectReferenceValue = area;
soTrigger.ApplyModifiedProperties();
EditorUtility.SetDirty(triggerComp);
// ── 5. 创建专属 VCam每区域独立相机─────────────────────────────
if (_createDedicatedVCam)
CameraAreaEditor.CreateDedicatedVCamForArea(area);
// ── 6. 处理旧对象 ──────────────────────────────────────────────
// 先记录原始激活状态,再对旧对象做处理,避免 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} " +
$"触发 PolygonCollider2D ({triggerPath.Length} 点) " +
$"限位 BoxCollider ({confinerBox.size.x:F1}×{confinerBox.size.y:F1})");
}
// ── 限位多边形路径 ────────────────────────────────────────────────────
/// <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)
{
// 将世界坐标转换为 areaGO 本地坐标
var localPts = new Vector2[entry.TriggerWorldPts.Count];
for (int i = 0; i < localPts.Length; i++)
localPts[i] = entry.TriggerWorldPts[i] - (Vector2)areaWorldPos;
// 按照质心角度排序,确保顶点顺序能够围成合法多边形
return SortPointsByAngle(localPts);
}
return RectToPolygon(fallback);
}
/// <summary>
/// 将一组点按照围绕质心的角度(逆时针)排序,使其能够围成合法的简单多边形。
/// 适用于凸多边形及质心在多边形内部的凹多边形。
/// </summary>
private static Vector2[] SortPointsByAngle(Vector2[] points)
{
// 计算质心
Vector2 centroid = Vector2.zero;
foreach (var p in points)
centroid += p;
centroid /= points.Length;
// 按照相对质心的极角升序排列(逆时针)
var sorted = new System.Collections.Generic.List<Vector2>(points);
sorted.Sort((a, b) =>
{
float angleA = Mathf.Atan2(a.y - centroid.y, a.x - centroid.x);
float angleB = Mathf.Atan2(b.y - centroid.y, b.x - centroid.x);
return angleA.CompareTo(angleB);
});
return sorted.ToArray();
}
/// <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),
};
}
}
}