- Implemented a new method for reconstructing rectilinear polygons from trigger points. - Added a fallback mechanism using nearest neighbor ordering and 2-opt optimization for irregular shapes. - Updated point sorting to ensure counter-clockwise orientation for polygon colliders. fix: Suppress validation warnings during batch enemy placement in SceneObjectPlacerTool - Introduced a static property in EnemyBase to suppress validation warnings during editor object placement. - Updated SceneObjectPlacerTool to utilize this property when creating enemy game objects. refactor: Clean up OnValidate method in EnemyBase to respect suppression flag - Modified OnValidate to check for the suppression flag before logging warnings about missing configurations.
638 lines
30 KiB
C#
638 lines
30 KiB
C#
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(BoxCollider,对应旧 Confiner)
|
||
/// ├─ TriggerZone(CameraTriggerZone + PolygonCollider2D,对应旧 TriggerRegion)
|
||
/// └─ VCam_xxx(CinemachineCamera + 所有扩展组件,专属虚拟相机)
|
||
///
|
||
/// 菜单: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 则兜底使用可视矩形。
|
||
///
|
||
/// 处理流程:
|
||
/// 1. 优先尝试直角多边形重建(坐标分组配对算法)→ 精确还原横竖边构成的触发区域
|
||
/// 2. 降级:最近邻贪心 + 2-opt 消除自相交 → 适用于含斜边的多边形
|
||
/// 3. Shoelace 叉积校正绕向 → 确保 CCW(PolygonCollider2D 要求)
|
||
/// </summary>
|
||
private static Vector2[] BuildTriggerPath(ZoneEntry entry, Vector3 areaWorldPos, Rect fallback)
|
||
{
|
||
if (entry.TriggerWorldPts.Count >= 3)
|
||
{
|
||
var localPts = new Vector2[entry.TriggerWorldPts.Count];
|
||
for (int i = 0; i < localPts.Length; i++)
|
||
localPts[i] = entry.TriggerWorldPts[i] - (Vector2)areaWorldPos;
|
||
|
||
// 优先:直角多边形精确重建
|
||
var rectilinear = TryReconstructRectilinear(localPts);
|
||
if (rectilinear != null)
|
||
return EnsureCounterClockwise(rectilinear);
|
||
|
||
// 降级:最近邻 + 2-opt(适用于含斜边或点分布不规则的情况)
|
||
Debug.LogWarning($"[迁移工具] {entry.ZoneObj.name} 无法识别为直角多边形,已使用 2-opt 降级处理,请手动检查形状。");
|
||
var ordered = NearestNeighborOrder(localPts);
|
||
TwoOptFix(ordered);
|
||
return EnsureCounterClockwise(ordered);
|
||
}
|
||
return RectToPolygon(fallback);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 直角多边形(Rectilinear Polygon)顶点排序。
|
||
///
|
||
/// 算法原理:
|
||
/// 简单直角多边形中,每列(同 x)的顶点按 y 升序两两配对为纵向边,
|
||
/// 每行(同 y)的顶点按 x 升序两两配对为横向边(Scanline 奇偶规则)。
|
||
/// 每个顶点恰好有 1 条纵向边 + 1 条横向边,即恰好 2 个邻居,可遍历为完整环路。
|
||
///
|
||
/// 返回 null 表示点集不满足直角多边形条件(奇数分组、邻居数 ≠ 2 等),调用方应降级处理。
|
||
/// </summary>
|
||
private static Vector2[] TryReconstructRectilinear(Vector2[] points)
|
||
{
|
||
// 同轴容差:小于此值的坐标差视为同行/同列(处理浮点误差)
|
||
const float kTol = 0.1f;
|
||
int n = points.Length;
|
||
|
||
var adj = new List<int>[n];
|
||
for (int i = 0; i < n; i++) adj[i] = new List<int>();
|
||
|
||
// 按 x 分组 → 纵向边;按 y 分组 → 横向边
|
||
if (!AddAxisEdges(points, adj, groupByX: true, tol: kTol)) return null;
|
||
if (!AddAxisEdges(points, adj, groupByX: false, tol: kTol)) return null;
|
||
|
||
// 每个顶点必须恰好有 2 个邻居(1 横 + 1 纵)才构成合法简单多边形
|
||
for (int i = 0; i < n; i++)
|
||
if (adj[i].Count != 2) return null;
|
||
|
||
// 从顶点 0 出发遍历环路
|
||
var result = new Vector2[n];
|
||
var visited = new bool[n];
|
||
result[0] = points[0];
|
||
visited[0] = true;
|
||
int prev = -1, curr = 0;
|
||
for (int step = 1; step < n; step++)
|
||
{
|
||
int next = -1;
|
||
foreach (int nb in adj[curr])
|
||
if (nb != prev && !visited[nb]) { next = nb; break; }
|
||
if (next == -1) return null; // 环路断裂
|
||
result[step] = points[next];
|
||
visited[next] = true;
|
||
prev = curr;
|
||
curr = next;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按指定轴(x 或 y)将顶点分组,每组内按垂直轴坐标排序后两两配对添加边。
|
||
/// 若任意组的顶点数为奇数(无法完整配对)则返回 false。
|
||
/// </summary>
|
||
private static bool AddAxisEdges(Vector2[] pts, List<int>[] adj, bool groupByX, float tol)
|
||
{
|
||
var groups = new Dictionary<int, List<int>>();
|
||
for (int i = 0; i < pts.Length; i++)
|
||
{
|
||
// 将坐标映射到整数 key,容差范围内的值归入同组
|
||
int key = Mathf.RoundToInt((groupByX ? pts[i].x : pts[i].y) / tol);
|
||
if (!groups.TryGetValue(key, out var list))
|
||
groups[key] = list = new List<int>();
|
||
list.Add(i);
|
||
}
|
||
foreach (var group in groups.Values)
|
||
{
|
||
if (group.Count % 2 != 0) return false; // 奇数个顶点无法配对
|
||
// 按垂直轴升序排列后两两配对为边
|
||
group.Sort((a, b) =>
|
||
(groupByX ? pts[a].y : pts[a].x).CompareTo(groupByX ? pts[b].y : pts[b].x));
|
||
for (int k = 0; k + 1 < group.Count; k += 2)
|
||
{
|
||
adj[group[k]].Add(group[k + 1]);
|
||
adj[group[k + 1]].Add(group[k]);
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 最近邻贪心排序(降级路径):从最左侧点出发,每步选取欧氏距离最近的未访问点。
|
||
/// </summary>
|
||
private static Vector2[] NearestNeighborOrder(Vector2[] points)
|
||
{
|
||
int n = points.Length;
|
||
var visited = new bool[n];
|
||
var result = new Vector2[n];
|
||
|
||
int startIdx = 0;
|
||
for (int i = 1; i < n; i++)
|
||
if (points[i].x < points[startIdx].x) startIdx = i;
|
||
|
||
result[0] = points[startIdx];
|
||
visited[startIdx] = true;
|
||
for (int step = 1; step < n; step++)
|
||
{
|
||
float minSqDist = float.MaxValue;
|
||
int nearest = -1;
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
if (visited[i]) continue;
|
||
float sq = (points[i] - result[step - 1]).sqrMagnitude;
|
||
if (sq < minSqDist) { minSqDist = sq; nearest = i; }
|
||
}
|
||
result[step] = points[nearest];
|
||
visited[nearest] = true;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 2-opt 优化(降级路径):检测并翻转所有自相交边对,直到无交叉为止。
|
||
/// </summary>
|
||
private static void TwoOptFix(Vector2[] pts)
|
||
{
|
||
int n = pts.Length;
|
||
bool improved = true;
|
||
while (improved)
|
||
{
|
||
improved = false;
|
||
for (int i = 0; i < n - 1; i++)
|
||
for (int j = i + 2; j < n; j++)
|
||
{
|
||
if (i == 0 && j == n - 1) continue;
|
||
if (SegmentsIntersect(pts[i], pts[(i + 1) % n], pts[j], pts[(j + 1) % n]))
|
||
{
|
||
System.Array.Reverse(pts, i + 1, j - i);
|
||
improved = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>用叉积判断线段 AB 与 CD 是否严格相交(不含端点接触)。</summary>
|
||
private static bool SegmentsIntersect(Vector2 a, Vector2 b, Vector2 c, Vector2 d)
|
||
{
|
||
float d1 = Cross2D(d - c, a - c), d2 = Cross2D(d - c, b - c);
|
||
float d3 = Cross2D(b - a, c - a), d4 = Cross2D(b - a, d - a);
|
||
return ((d1 > 0f && d2 < 0f) || (d1 < 0f && d2 > 0f)) &&
|
||
((d3 > 0f && d4 < 0f) || (d3 < 0f && d4 > 0f));
|
||
}
|
||
|
||
private static float Cross2D(Vector2 u, Vector2 v) => u.x * v.y - u.y * v.x;
|
||
|
||
/// <summary>
|
||
/// 使用 Shoelace 公式(叉积累加)验证绕向:
|
||
/// 2A = Σ (xᵢ · yᵢ₊₁ − xᵢ₊₁ · yᵢ)
|
||
/// Unity Y 轴朝上:A > 0 = CCW,A < 0 = CW。
|
||
/// 若为顺时针则翻转(不改变形状,只调整绕向)。
|
||
/// </summary>
|
||
private static Vector2[] EnsureCounterClockwise(Vector2[] points)
|
||
{
|
||
float signedArea = 0f;
|
||
int n = points.Length;
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
Vector2 curr = points[i];
|
||
Vector2 next = points[(i + 1) % n];
|
||
signedArea += curr.x * next.y - next.x * curr.y;
|
||
}
|
||
if (signedArea < 0f)
|
||
System.Array.Reverse(points);
|
||
return points;
|
||
}
|
||
|
||
/// <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),
|
||
};
|
||
}
|
||
}
|
||
}
|