Compare commits
2 Commits
10ca83ce86
...
1685a14adf
| Author | SHA1 | Date | |
|---|---|---|---|
| 1685a14adf | |||
| 5ad6ed8ae6 |
@@ -12,14 +12,14 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: f7dd720bca19fcc49b22106fb65f7652, type: 3}
|
||||
m_Name: ENM_E001_AnimConfig
|
||||
m_EditorClassIdentifier:
|
||||
Idle: {fileID: 0}
|
||||
Walk: {fileID: 0}
|
||||
Run: {fileID: 0}
|
||||
Turn: {fileID: 0}
|
||||
Idle: {fileID: 7400000, guid: 74d1c2f7f8e5c66409e9090885e7e007, type: 2}
|
||||
Walk: {fileID: 7400000, guid: b6b9e34e957b9fa4b92e95aaa155099f, type: 2}
|
||||
Run: {fileID: 7400000, guid: b6b9e34e957b9fa4b92e95aaa155099f, type: 2}
|
||||
Turn: {fileID: 7400000, guid: c6d78c8270549254f8c777e0c5d4f9bf, type: 2}
|
||||
Attack: {fileID: 0}
|
||||
Hurt: {fileID: 0}
|
||||
Hurt: {fileID: 7400000, guid: 9d5bb5bb32cdb344b80f01d998ed653f, type: 2}
|
||||
Stagger: {fileID: 0}
|
||||
KnockUp: {fileID: 0}
|
||||
Dead: {fileID: 0}
|
||||
Dead: {fileID: 7400000, guid: 7f07e13e67a5aba4b8a27234e5a84ee6, type: 2}
|
||||
Alert: {fileID: 0}
|
||||
Investigate: {fileID: 0}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -420,43 +420,195 @@ namespace BaseGames.Editor
|
||||
/// 构建触发多边形路径(本地坐标,相对于新 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)
|
||||
{
|
||||
// 将世界坐标转换为 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);
|
||||
// 优先:直角多边形精确重建
|
||||
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[] SortPointsByAngle(Vector2[] points)
|
||||
private static Vector2[] TryReconstructRectilinear(Vector2[] points)
|
||||
{
|
||||
// 计算质心
|
||||
Vector2 centroid = Vector2.zero;
|
||||
foreach (var p in points)
|
||||
centroid += p;
|
||||
centroid /= points.Length;
|
||||
// 同轴容差:小于此值的坐标差视为同行/同列(处理浮点误差)
|
||||
const float kTol = 0.1f;
|
||||
int n = points.Length;
|
||||
|
||||
// 按照相对质心的极角升序排列(逆时针)
|
||||
var sorted = new System.Collections.Generic.List<Vector2>(points);
|
||||
sorted.Sort((a, b) =>
|
||||
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++)
|
||||
{
|
||||
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();
|
||||
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>
|
||||
|
||||
@@ -389,7 +389,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E001 草蛭");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_CaoZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E001");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -470,6 +470,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E001 草蛭", go, report);
|
||||
}
|
||||
|
||||
@@ -479,7 +480,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E002 簧蛭");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_HuangZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E002");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -548,6 +549,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E002 簧蛭", go, report);
|
||||
}
|
||||
|
||||
@@ -557,7 +559,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E003 幼蛭");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_YouZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E003");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -631,6 +633,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E003 幼蛭", go, report);
|
||||
}
|
||||
|
||||
@@ -640,7 +643,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E004 蛭母");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_ZhiMu");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E004");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -757,6 +760,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E004 蛭母", go, report);
|
||||
}
|
||||
|
||||
@@ -766,7 +770,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E005 肥蛭");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_FeiZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E005");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -849,6 +853,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E005 肥蛭", go, report);
|
||||
}
|
||||
|
||||
@@ -858,7 +863,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E006 讙");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_Huan");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E006");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -951,6 +956,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("E006 讙", go, report);
|
||||
}
|
||||
|
||||
@@ -960,7 +966,7 @@ namespace BaseGames.Editor
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place Boss 嘲风");
|
||||
|
||||
EnemyBase.SuppressValidationWarnings = true;
|
||||
GameObject go = new GameObject("ENM_ChaoFeng");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place ChaoFeng");
|
||||
go.transform.position = GetDropPosition();
|
||||
@@ -1067,6 +1073,7 @@ namespace BaseGames.Editor
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
EnemyBase.SuppressValidationWarnings = false;
|
||||
MarkDirtyAndLog("Boss 嘲风 (ChaoFeng)", go, report);
|
||||
}
|
||||
|
||||
|
||||
@@ -710,8 +710,12 @@ namespace BaseGames.Enemies
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>Set to true during batch editor placement to suppress mid-wiring OnValidate warnings.</summary>
|
||||
public static bool SuppressValidationWarnings { get; set; }
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
if (SuppressValidationWarnings) return;
|
||||
if (_statsSO == null)
|
||||
Debug.LogWarning($"[EnemyBase] {gameObject.name} 缺少 EnemyStatsSO 配置(运行时会 NullRef)。", this);
|
||||
if (_stats == null)
|
||||
|
||||
Reference in New Issue
Block a user