From 6b22c139216fb52803a4d5fc80e250d02f8624c6 Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Tue, 16 Jun 2026 15:14:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=A2=B0=E6=92=9E=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E7=9A=84=E5=86=85=E5=AE=B9=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=9B=B4=E9=80=82=E5=90=88rect=E7=9A=84=E5=9B=9B=E5=8F=89?= =?UTF-8?q?=E6=A0=91=EF=BC=8C=E7=A2=B0=E6=92=9E=E8=AE=A1=E7=AE=97=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=8A=9F=E8=83=BD=E6=8F=90=E5=8D=87=E5=88=B0=E9=AB=98?= =?UTF-8?q?=E7=BA=A7=E5=9D=9E=E9=83=A8=E5=88=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Runtime/VisualForm/AdvancedAnchorDock.cs | 309 ++++++++++++++++++ Runtime/VisualForm/AdvancedAnchorDock.cs.meta | 11 + Runtime/VisualForm/AnchorDock.cs | 207 +++++++++++- Runtime/VisualForm/AnchorWindow.cs | 19 ++ Runtime/VisualForm/CollisionUtils.cs | 55 ++++ Runtime/VisualForm/CollisionUtils.cs.meta | 11 + Runtime/VisualForm/QuadTree.cs | 194 +++++++++++ Runtime/VisualForm/QuadTree.cs.meta | 11 + Runtime/VisualForm/WindowEntry.cs | 51 ++- Runtime/VisualForm/WindowState.cs | 12 + 10 files changed, 863 insertions(+), 17 deletions(-) create mode 100644 Runtime/VisualForm/AdvancedAnchorDock.cs create mode 100644 Runtime/VisualForm/AdvancedAnchorDock.cs.meta create mode 100644 Runtime/VisualForm/CollisionUtils.cs create mode 100644 Runtime/VisualForm/CollisionUtils.cs.meta create mode 100644 Runtime/VisualForm/QuadTree.cs create mode 100644 Runtime/VisualForm/QuadTree.cs.meta diff --git a/Runtime/VisualForm/AdvancedAnchorDock.cs b/Runtime/VisualForm/AdvancedAnchorDock.cs new file mode 100644 index 0000000..a04c52e --- /dev/null +++ b/Runtime/VisualForm/AdvancedAnchorDock.cs @@ -0,0 +1,309 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace XericUI.VisualForm +{ + /// + /// 高级锚点坞 — 在 AnchorDock 基础上扩展屏幕空间碰撞分离功能。 + /// 通过四叉树加速碰撞检测,迭代推挤分离重叠的UI窗口条目。 + /// + /// 核心流程: + /// 1. 判断是否有脏标记(坐标变化),无脏则跳过碰撞 + /// 2. 重置所有条目碰撞状态(从原始 ScreenOffset 重新计算,不依赖上一帧) + /// 3. 刷新各 WindowState 中条目的 Canvas 本地空间碰撞矩形缓存 + /// 4. 构建四叉树 → 迭代推挤分离 → 写入 CollisionOffset + /// 5. 下一帧 UpdateStatePosition 通过 EffectiveOffset 自动应用碰撞偏移 + /// + [AddComponentMenu("Xeric UI Vessel/AnchorWindow/AdvancedAnchorDock")] + public class AdvancedAnchorDock : AnchorDock + { + [Header("碰撞分离")] + [SerializeField, Tooltip("启用屏幕空间碰撞分离计算")] + private bool m_EnableCollision = false; + + [SerializeField, Tooltip("碰撞计算执行间隔(秒),值越小越频繁")] + private float m_CollisionInterval = 0.04f; + + [SerializeField, Tooltip("碰撞分离最大迭代次数(10为推荐值)")] + private int m_CollisionIterations = 10; + + [SerializeField, Tooltip("碰撞分离推力系数(0-1,0.5为推荐值)")] + private float m_CollisionPushForce = 0.5f; + + [SerializeField, Tooltip("四叉树每节点最大容纳数")] + private int m_QuadTreeMaxPerNode = 5; + + [SerializeField, Tooltip("四叉树最大深度")] + private int m_QuadTreeMaxDepth = 4; + + // --- 运行时 --- + + private float _collisionTimer; + private QuadTree _quadTree; + private readonly List _collisionEntries = new List(); + private readonly HashSet _overlapResults = new HashSet(); + + #region 生命周期 + + protected override void OnEnable() + { + base.OnEnable(); + _collisionTimer = 0f; + _quadTree = null; + } + + /// + /// 在基类位置更新之前注入碰撞分离计算。 + /// 仅在启用碰撞且存在脏标记时才执行。 + /// 摄像机变化、坐标变化或首次激活时立即触发,其余情况按间隔节流。 + /// + protected override void OnBeforePositionUpdates() + { + if (!m_EnableCollision) return; + + _collisionTimer += Time.unscaledDeltaTime; + + // 摄像机变化时立即触发碰撞(如场景启动) + bool forceCheck = IsCameraDirty; + bool timerElapsed = _collisionTimer >= m_CollisionInterval; + + if (!forceCheck && !timerElapsed) return; + + if (timerElapsed) + _collisionTimer %= m_CollisionInterval; + + // 仅在有脏标记时才计算(摄像机变化或坐标变化触发,碰撞偏移本身不作为触发条件) + if (!NeedsCollisionRecalculation()) return; + + ComputeCollisionSeparation(); + } + + #endregion + + #region 碰撞流程 — 封装方法 + + /// + /// 是否需要重新计算碰撞。仅在摄像机变化或窗口坐标变化时返回 true。 + /// 使用独立的 CollisionDirty 标记(不被 UpdateStatePosition 清除),而非 CoordinateDirty。 + /// + private bool NeedsCollisionRecalculation() + { + if (IsCameraDirty) return true; + + foreach (var kvp in ActiveStates) + { + var state = kvp.Value; + if (state == null || !state.IsEnabled) continue; + if (state.CollisionDirty) + return true; + } + return false; + } + + /// + /// 主碰撞分离计算入口。 + /// 流程:重置 → 刷新矩形 → 构建四叉树 → 迭代推挤 → 应用结果。 + /// + private void ComputeCollisionSeparation() + { + // 1. 重置所有条目碰撞状态(从原始 ScreenOffset 重新计算) + foreach (var kvp in ActiveStates) + { + if (kvp.Value == null) continue; + kvp.Value.ResetCollisionState(); + } + + // 2. 刷新所有条目的 Canvas 本地空间碰撞矩形缓存 + RefreshAllCollisionRects(); + + // 3. 收集参与碰撞的条目 + CollectCollisionEntries(); + if (_collisionEntries.Count < 2) return; + + // 4. 构建四叉树 + BuildQuadTree(); + + // 5. 迭代推挤分离 + IterateCollisionPush(); + + // 6. 标记碰撞结果,让 EffectiveOffset 在下次 UpdateStatePosition 中生效 + foreach (var kvp in ActiveStates) + { + if (kvp.Value == null) continue; + kvp.Value.ApplyCollisionResult(); + } + } + + /// + /// 刷新所有活跃 WindowState 中各条目的碰撞矩形缓存。 + /// 每个矩形位于 Canvas 本地空间,中心 = 锚点世界→Canvas坐标 + ScreenOffset, + /// 尺寸 = 条目实例 RectTransform.rect.size。 + /// + private void RefreshAllCollisionRects() + { + var camWorld = WorldCamera; + var camUI = UICamera; + var canvas = Canvas; + var container = RectTransform; + + if (camWorld == null || canvas == null || container == null) return; + + foreach (var kvp in ActiveStates) + { + var state = kvp.Value; + if (state == null || !state.IsEnabled || !state.HasAnyEnabled) continue; + if (!state.HasAnyCollisionEntry) continue; + + var anchor = state.Anchor; + if (anchor == null) continue; + + state.ClearCollisionRects(); + + Vector3 worldPos = anchor.CachedTransform.position; + if (!TryWorldToScreenPoint(worldPos, camWorld, camUI, canvas, container, out Vector2 canvasPos)) + continue; + + for (int i = 0; i < state.Entries.Count; i++) + { + var entry = state.Entries[i]; + if (!entry.EnableCollision || !entry.Enabled || entry.Instance == null) continue; + + RectTransform rt = entry.Instance.transform as RectTransform; + Vector2 size = (rt != null) ? rt.rect.size : Vector2.one * 10f; + Vector2 center = canvasPos + entry.ScreenOffset; + Rect rect = new Rect(center - size * 0.5f, size); + + state.SetCollisionRect(entry, rect); + } + } + } + + /// + /// 收集所有参与碰撞的活跃条目到一个扁平列表中。 + /// + private void CollectCollisionEntries() + { + _collisionEntries.Clear(); + + foreach (var kvp in ActiveStates) + { + var state = kvp.Value; + if (state == null || !state.IsEnabled || !state.HasAnyEnabled) continue; + if (!state.HasAnyCollisionEntry) continue; + + for (int i = 0; i < state.Entries.Count; i++) + { + var entry = state.Entries[i]; + if (!entry.EnableCollision || !entry.Enabled || entry.Instance == null) continue; + + Rect rect = state.GetCollisionRect(entry); + if (rect.width <= 0f || rect.height <= 0f) continue; + + _collisionEntries.Add(entry); + } + } + } + + /// + /// 构建四叉树:计算所有条目包围盒 → 清空重建 → 插入所有条目。 + /// 树中存储的是 WindowEntry,getRectFunc 返回的是含 CollisionOffset 和 Margin 的实时矩形。 + /// + private void BuildQuadTree() + { + // 计算包围盒 + Rect bounds = GetEntryPushedRect(_collisionEntries[0]); + for (int i = 1; i < _collisionEntries.Count; i++) + { + var r = GetEntryPushedRect(_collisionEntries[i]); + bounds = Encapsulate(bounds, r); + } + + // 略微扩展边界 + bounds = new Rect(bounds.x - 1f, bounds.y - 1f, bounds.width + 2f, bounds.height + 2f); + + if (_quadTree == null) + _quadTree = new QuadTree(bounds, GetEntryPushedRect, m_QuadTreeMaxPerNode, m_QuadTreeMaxDepth); + else + _quadTree.Rebuild(_collisionEntries); + } + + /// + /// 迭代推挤分离算法。 + /// 对每个条目从四叉树查询重叠对象,沿最小重叠轴推开,重复直到无重叠或达到最大迭代次数。 + /// + private void IterateCollisionPush() + { + float pushForce = Mathf.Clamp01(m_CollisionPushForce); + int maxIter = Mathf.Clamp(m_CollisionIterations, 1, 50); + + for (int iter = 0; iter < maxIter; iter++) + { + bool anyOverlap = false; + + for (int i = 0; i < _collisionEntries.Count; i++) + { + var entryA = _collisionEntries[i]; + var stateA = entryA.State; + if (stateA == null) continue; + + Rect rectA = stateA.GetEntryPushedRect(entryA); + + _quadTree.Retrieve(rectA, _overlapResults); + + foreach (var entryB in _overlapResults) + { + if (entryB == entryA) continue; + + var stateB = entryB.State; + if (stateB == null) continue; + + Rect rectB = stateB.GetEntryPushedRect(entryB); + + if (!CollisionUtils.Overlaps(rectA, rectB)) continue; + + anyOverlap = true; + + Vector2 push = CollisionUtils.CalculateRepulsion(rectA, rectB, pushForce); + if (push == Vector2.zero) continue; + + stateA.SetCollisionOffset(entryA, entryA.CollisionOffset + push); + stateB.SetCollisionOffset(entryB, entryB.CollisionOffset - push); + + // 更新 rectA 以反映新的碰撞偏移(同一次迭代内继续使用) + rectA = stateA.GetEntryPushedRect(entryA); + } + } + + if (!anyOverlap) break; + } + } + + #endregion + + #region 辅助方法 + + /// + /// 获取条目当前的推挤位置矩形(含 CollisionOffset + CollisionMargin)。 + /// 作为四叉树的 getRectFunc 委托,实时反映条目最新碰撞偏移。 + /// + private static Rect GetEntryPushedRect(WindowEntry entry) + { + if (entry?.State == null) return Rect.zero; + return entry.State.GetEntryPushedRect(entry); + } + + /// + /// 计算两个矩形的包围盒。 + /// + private static Rect Encapsulate(Rect a, Rect b) + { + float xMin = Mathf.Min(a.xMin, b.xMin); + float yMin = Mathf.Min(a.yMin, b.yMin); + float xMax = Mathf.Max(a.xMax, b.xMax); + float yMax = Mathf.Max(a.yMax, b.yMax); + return new Rect(xMin, yMin, xMax - xMin, yMax - yMin); + } + + #endregion + } +} diff --git a/Runtime/VisualForm/AdvancedAnchorDock.cs.meta b/Runtime/VisualForm/AdvancedAnchorDock.cs.meta new file mode 100644 index 0000000..81c9ffb --- /dev/null +++ b/Runtime/VisualForm/AdvancedAnchorDock.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f267706e785728a4abedc6424329a08d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/VisualForm/AnchorDock.cs b/Runtime/VisualForm/AnchorDock.cs index c0e3695..6820064 100644 --- a/Runtime/VisualForm/AnchorDock.cs +++ b/Runtime/VisualForm/AnchorDock.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using XericLibrary.Runtime.MacroLibrary; +using XericLibraryEditor.Debug; namespace XericUI.VisualForm { @@ -37,6 +38,19 @@ namespace XericUI.VisualForm [SerializeField, Tooltip("安全区坐标单位")] private SafeZoneUnit m_SafeZoneUnit = SafeZoneUnit.Ratio; + [Header("碰撞分离")] + [SerializeField, Tooltip("启用屏幕空间碰撞分离计算")] + private bool m_EnableCollision = false; + + [SerializeField, Tooltip("碰撞计算执行间隔(秒),值越小越频繁,0为每帧计算")] + private float m_CollisionInterval = 0.04f; + + [SerializeField, Tooltip("碰撞分离最大迭代次数")] + private int m_CollisionIterations = 10; + + [SerializeField, Tooltip("碰撞分离推力系数")] + private float m_CollisionPushForce = 0.5f; + #region 静态单例 private static AnchorDock s_Main; @@ -125,6 +139,11 @@ namespace XericUI.VisualForm /// private Quaternion _lastUICameraRotation; + /// + /// 碰撞计算计时器(每帧累加deltaTime,超过CollisionInterval时触发计算) + /// + private float _collisionTimer; + #endregion #region 公开属性 @@ -253,7 +272,18 @@ namespace XericUI.VisualForm state.CheckPositionDirty(m_PositionThreshold); } - // 4. 遍历所有中间类,根据脏标记更新坐标(跳过无启用条目的状态) + // 4. 碰撞分离计算(按时间间隔执行,必须在位置更新之前,确保同帧生效) + if (m_EnableCollision) + { + _collisionTimer += Time.unscaledDeltaTime; + if (_collisionTimer >= m_CollisionInterval) + { + _collisionTimer %= m_CollisionInterval; + ComputeCollisionSeparation(); + } + } + + // 5. 遍历所有中间类,根据脏标记更新坐标(跳过无启用条目的状态) foreach (var kvp in _activeStates) { var state = kvp.Value; @@ -267,7 +297,7 @@ namespace XericUI.VisualForm } } - // 5. 复位脏标记 + // 6. 复位脏标记 _cameraDirty = false; } @@ -485,9 +515,13 @@ namespace XericUI.VisualForm Vector2 rawPoint = basePoint + entry.ScreenOffset; Vector2 finalPoint; + // 碰撞偏移:条目参与碰撞且有碰撞偏移时,叠加 CollisionOffset + Vector2 collisionOffset = (entry.EnableCollision && entry.IsColliding) ? entry.CollisionOffset : Vector2.zero; + rawPoint += collisionOffset; + if (entry.ClampToSafeZone && baseOverflow != OverflowState.InRange) { - finalPoint = clampedBase + entry.ScreenOffset; + finalPoint = clampedBase + entry.ScreenOffset + collisionOffset; } else { @@ -514,6 +548,159 @@ namespace XericUI.VisualForm #endregion + #region 碰撞分离计算 + + /// + /// 碰撞条目数据结构(用于碰撞分离计算时的临时存储) + /// + private struct CollisionItem + { + public WindowEntry Entry; + public Rect Rect; + public int StateIndex; // 所属WindowState在列表中的索引 + } + + /// + /// 屏幕空间碰撞分离计算。 + /// 收集所有启用了碰撞的条目,通过迭代推挤算法将它们分开,避免重叠。 + /// 结果写入各条目的 CollisionOffset(相对 ScreenOffset 的偏移量)。 + /// + private void ComputeCollisionSeparation() + { + // 0. 重置所有条目的碰撞状态(上一帧的碰撞数据需要清零) + foreach (var kvp in _activeStates) + { + var state = kvp.Value; + if (state == null) continue; + foreach (var entry in state.Entries) + { + entry.CollisionOffset = Vector2.zero; + entry.IsColliding = false; + } + } + + // 1. 收集所有参与碰撞的条目及其屏幕Rect + var items = new List(); + foreach (var kvp in _activeStates) + { + var state = kvp.Value; + if (state == null || !state.IsEnabled || !state.HasAnyEnabled) continue; + + var anchor = state.Anchor; + if (anchor == null) continue; + + foreach (var entry in state.Entries) + { + if (!entry.EnableCollision || !entry.Enabled || entry.Instance == null) continue; + + // 获取条目当前屏幕Rect + Rect entryRect = GetEntryScreenRect(entry); + if (entryRect.width <= 0 || entryRect.height <= 0) continue; + + items.Add(new CollisionItem { Entry = entry, Rect = entryRect }); + } + } + + if (items.Count < 2) return; + + // 2. 迭代推挤分离 + int iterations = Mathf.Clamp(m_CollisionIterations, 1, 50); + for (int iter = 0; iter < iterations; iter++) + { + bool anyOverlap = false; + + for (int i = 0; i < items.Count; i++) + { + for (int j = i + 1; j < items.Count; j++) + { + var itemA = items[i]; + var itemB = items[j]; + + Rect rectA = OffsetRect(itemA.Rect, itemA.Entry.CollisionOffset); + Rect rectB = OffsetRect(itemB.Rect, itemB.Entry.CollisionOffset); + + if (!rectA.Overlaps(rectB)) continue; + + anyOverlap = true; + + // 计算推挤方向 + Vector2 centerA = rectA.center; + Vector2 centerB = rectB.center; + Vector2 delta = centerA - centerB; + float halfW = (rectA.width + rectB.width) * 0.5f; + float halfH = (rectA.height + rectB.height) * 0.5f; + + float overlapX = halfW - Mathf.Abs(delta.x); + float overlapY = halfH - Mathf.Abs(delta.y); + + if (overlapX <= 0 || overlapY <= 0) continue; + + Vector2 push; + if (overlapX < overlapY) + { + push = new Vector2(Mathf.Sign(delta.x) * overlapX * m_CollisionPushForce, 0f); + } + else + { + push = new Vector2(0f, Mathf.Sign(delta.y) * overlapY * m_CollisionPushForce); + } + + itemA.Entry.CollisionOffset += push; + itemB.Entry.CollisionOffset -= push; + + items[i] = itemA; + items[j] = itemB; + } + } + + if (!anyOverlap) break; + } + + // 3. 标记参与碰撞的条目 + for (int i = 0; i < items.Count; i++) + { + if (items[i].Entry.CollisionOffset != Vector2.zero) + { + items[i].Entry.IsColliding = true; + items[i].Entry.State.CoordinateDirty = true; + } + } + } + + /// + /// 获取条目的屏幕矩形(基于其WorldAnchor的世界坐标 + ScreenOffset 换算到Canvas本地空间)。 + /// 矩形中心 = World→Canvas坐标 + ScreenOffset,尺寸 = Instance的RectTransform.rect.size。 + /// + private Rect GetEntryScreenRect(WindowEntry entry) + { + if (entry == null || entry.Instance == null) return Rect.zero; + + var state = entry.State; + if (state?.Anchor == null) return Rect.zero; + + Vector3 worldPos = state.Anchor.CachedTransform.position; + if (!TryWorldToScreenPoint(worldPos, _worldCamera, _uiCamera, _canvas, RectTransform, out Vector2 canvasPos)) + return Rect.zero; + + Vector2 center = canvasPos + entry.ScreenOffset; + + // 获取实例的RectTransform尺寸 + RectTransform rt = entry.Instance.transform as RectTransform; + if (rt == null) return new Rect(center, Vector2.one * 10f); + return new Rect(center - rt.rect.size * 0.5f, rt.rect.size); + } + + /// + /// 将Rect按偏移量平移 + /// + private static Rect OffsetRect(Rect r, Vector2 offset) + { + r.position += offset; + return r; + } + + #endregion + #region 子对象哈希集 /// @@ -764,7 +951,7 @@ namespace XericUI.VisualForm #endregion #if UNITY_EDITOR - private void OnDrawGizmosSelected() + protected virtual void OnDrawGizmosSelected() { Canvas canvas = _canvas; if (canvas == null) @@ -779,17 +966,9 @@ namespace XericUI.VisualForm Vector2 center = pixelSafeZone.center; Vector3 size = new Vector3(pixelSafeZone.width, pixelSafeZone.height, 0.001f); - // 填充 — 橘黄色半透明 - Gizmos.color = new Color(1f, 0.65f, 0.15f, 0.25f); - Gizmos.matrix = canvas.transform.localToWorldMatrix; - Gizmos.DrawCube(center, size); - - // 边框 — 橘红色 - Gizmos.color = new Color(1f, 0.35f, 0.05f, 0.9f); - Gizmos.DrawWireCube(center, size); - - Gizmos.matrix = Matrix4x4.identity; + canvas.DrawGizmosCanvasRect_Warn(pixelSafeZone); } + #endif } } diff --git a/Runtime/VisualForm/AnchorWindow.cs b/Runtime/VisualForm/AnchorWindow.cs index 75c9cbb..7b91062 100644 --- a/Runtime/VisualForm/AnchorWindow.cs +++ b/Runtime/VisualForm/AnchorWindow.cs @@ -104,6 +104,25 @@ namespace XericUI.VisualForm GetWindowState()?.RefreshFlags(); } + /// + /// 设置指定索引条目的碰撞参与标记。写入后自动同步 WindowState 的 HasAnyCollisionEntry。 + /// 锚点坞在启用碰撞计算时会对标记为参与碰撞的条目进行屏幕空间碰撞分离。 + /// + /// 条目索引 + /// 是否参与碰撞 + public void SetEntryEnableCollision(int index, bool enable) + { + if (index < 0 || index >= m_WindowEntries.Count) return; + + var entry = m_WindowEntries[index]; + if (entry == null) return; + + entry.SetEnableCollisionInternal(enable); + + // 同步到WindowState + GetWindowState()?.RefreshFlags(); + } + /// /// 从所属锚点坞获取自己的中间窗口状态类 /// diff --git a/Runtime/VisualForm/CollisionUtils.cs b/Runtime/VisualForm/CollisionUtils.cs new file mode 100644 index 0000000..73830f9 --- /dev/null +++ b/Runtime/VisualForm/CollisionUtils.cs @@ -0,0 +1,55 @@ +using UnityEngine; + +namespace XericUI.VisualForm +{ + /// + /// 碰撞分离静态工具类。 + /// 提供两个矩形之间的排斥力计算,用于屏幕空间UI碰撞分离。 + /// 算法参考 BubblePhysicsSolver.CalculateRepulsionForce。 + /// + public static class CollisionUtils + { + /// + /// 计算两个重叠矩形的相互排斥向量(从A推离B的方向)。 + /// 沿最小重叠轴方向推开,推力大小 = 重叠深度 × 推力系数。 + /// + /// 矩形A + /// 矩形B + /// 推力系数(0到1),默认0.5 + /// A应受到的推离向量(B应施加相反向量) + public static Vector2 CalculateRepulsion(Rect rectA, Rect rectB, float pushForce = 0.5f) + { + Vector2 centerA = rectA.center; + Vector2 centerB = rectB.center; + Vector2 delta = centerA - centerB; + + float halfW = (rectA.width + rectB.width) * 0.5f; + float halfH = (rectA.height + rectB.height) * 0.5f; + + float overlapX = halfW - Mathf.Abs(delta.x); + float overlapY = halfH - Mathf.Abs(delta.y); + + if (overlapX <= 0f || overlapY <= 0f) + return Vector2.zero; + + // 沿最小重叠轴推开 + if (overlapX < overlapY) + { + return new Vector2(Mathf.Sign(delta.x) * overlapX * pushForce, 0f); + } + else + { + return new Vector2(0f, Mathf.Sign(delta.y) * overlapY * pushForce); + } + } + + /// + /// 判断两个矩形是否重叠 + /// + public static bool Overlaps(Rect a, Rect b) + { + return a.xMin < b.xMax && a.xMax > b.xMin && + a.yMin < b.yMax && a.yMax > b.yMin; + } + } +} diff --git a/Runtime/VisualForm/CollisionUtils.cs.meta b/Runtime/VisualForm/CollisionUtils.cs.meta new file mode 100644 index 0000000..d5b67c3 --- /dev/null +++ b/Runtime/VisualForm/CollisionUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1c28b27bd8c8d7d4aabd87c571406e5e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/VisualForm/QuadTree.cs b/Runtime/VisualForm/QuadTree.cs new file mode 100644 index 0000000..e490cd8 --- /dev/null +++ b/Runtime/VisualForm/QuadTree.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace XericUI.VisualForm +{ + /// + /// 轻量泛型四叉树,用于空间加速碰撞检测。 + /// 通过 Func{T, Rect} 委托获取任意类型对象的边界矩形。 + /// 委托在每次查询时实时调用,因此对象可在树重建后移动位置,查询仍能获得准确结果。 + /// + /// 典型用法: + /// 1. 构造 QuadTree{T}(bounds, getRectFunc) + /// 2. Rebuild(items) — 清空并重新插入 + /// 3. Retrieve(area, result) — 查询与area重叠的所有对象 + /// + public class QuadTree + { + private readonly int _maxPerNode; + private readonly int _maxDepth; + private readonly Func _getRectFunc; + + private Node _root; + + public int Count { get; private set; } + + /// + /// 初始化四叉树。 + /// + /// 根节点边界矩形 + /// 获取对象边界的委托(每次查询实时调用) + /// 每节点最大容纳对象数 + /// 最大递归深度 + public QuadTree(Rect boundary, Func getRectFunc, int maxPerNode = 5, int maxDepth = 4) + { + _maxPerNode = Math.Max(1, maxPerNode); + _maxDepth = Math.Max(1, maxDepth); + _getRectFunc = getRectFunc ?? throw new ArgumentNullException(nameof(getRectFunc)); + _root = new Node(boundary, 0, this); + } + + /// + /// 清空四叉树,保留边界和配置以便重用。 + /// + public void Clear() + { + _root.ClearAll(); + Count = 0; + } + + /// + /// 清空并重新插入所有对象。 + /// + public void Rebuild(IEnumerable items) + { + Clear(); + foreach (var item in items) + Insert(item); + } + + /// + /// 插入一个对象。 + /// + public bool Insert(T item) + { + return _root.Insert(item); + } + + /// + /// 查询与指定区域重叠的所有对象。 + /// + /// 查询区域 + /// 结果容器(会先清空再填入) + public void Retrieve(Rect area, HashSet result) + { + if (result == null) return; + result.Clear(); + _root.Retrieve(area, result); + } + + #region 内部节点 + + private class Node + { + private readonly Rect _boundary; + private readonly int _depth; + private readonly QuadTree _owner; + private HashSet _objects; + private Node[] _children; + + public Node(Rect boundary, int depth, QuadTree owner) + { + _boundary = boundary; + _depth = depth; + _owner = owner; + _objects = new HashSet(); + } + + public bool Insert(T item) + { + Rect itemRect = _owner._getRectFunc(item); + if (!Overlaps(_boundary, itemRect)) + return false; + + if (_objects != null && (_objects.Count < _owner._maxPerNode || _depth >= _owner._maxDepth)) + { + _objects.Add(item); + _owner.Count++; + return true; + } + + if (_children == null) + Split(); + + bool inserted = false; + for (int i = 0; i < 4; i++) + { + if (_children[i].Insert(item)) + inserted = true; + } + + return inserted; + } + + private void Split() + { + float hw = _boundary.width * 0.5f; + float hh = _boundary.height * 0.5f; + float mx = _boundary.x + hw; + float my = _boundary.y + hh; + + _children = new Node[4]; + _children[0] = new Node(new Rect(_boundary.x, _boundary.y, hw, hh), _depth + 1, _owner); + _children[1] = new Node(new Rect(mx, _boundary.y, hw, hh), _depth + 1, _owner); + _children[2] = new Node(new Rect(_boundary.x, my, hw, hh), _depth + 1, _owner); + _children[3] = new Node(new Rect(mx, my, hw, hh), _depth + 1, _owner); + + var temp = new List(_objects); + _objects.Clear(); + _objects = null; + + for (int i = 0; i < temp.Count; i++) + Insert(temp[i]); + } + + public void Retrieve(Rect area, HashSet result) + { + if (!Overlaps(_boundary, area)) + return; + + if (_objects != null) + { + foreach (var obj in _objects) + { + if (Overlaps(_owner._getRectFunc(obj), area)) + result.Add(obj); + } + return; + } + + if (_children != null) + { + for (int i = 0; i < 4; i++) + _children[i].Retrieve(area, result); + } + } + + public void ClearAll() + { + if (_objects != null) + { + _objects.Clear(); + } + + if (_children != null) + { + for (int i = 0; i < _children.Length; i++) + _children[i]?.ClearAll(); + _children = null; + } + + _objects = new HashSet(); + } + + private static bool Overlaps(Rect a, Rect b) + { + return a.xMin < b.xMax && a.xMax > b.xMin && + a.yMin < b.yMax && a.yMax > b.yMin; + } + } + + #endregion + } +} diff --git a/Runtime/VisualForm/QuadTree.cs.meta b/Runtime/VisualForm/QuadTree.cs.meta new file mode 100644 index 0000000..d748f5c --- /dev/null +++ b/Runtime/VisualForm/QuadTree.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3118a5748ae80344a8bb370bf67eacbd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/VisualForm/WindowEntry.cs b/Runtime/VisualForm/WindowEntry.cs index 0789e1e..1c2704c 100644 --- a/Runtime/VisualForm/WindowEntry.cs +++ b/Runtime/VisualForm/WindowEntry.cs @@ -23,6 +23,9 @@ namespace XericUI.VisualForm [SerializeField, Tooltip("超出安全区后是否钳制在安全区内")] private bool m_ClampToSafeZone; + [SerializeField, Tooltip("是否参与屏幕空间碰撞分离计算")] + private bool m_EnableCollision; + /// /// 窗口对象引用(实例或预制体) /// @@ -38,7 +41,11 @@ namespace XericUI.VisualForm public Vector2 ScreenOffset { get => m_ScreenOffset; - set => m_ScreenOffset = value; + set + { + m_ScreenOffset = value; + MarkDirty(); + } } /// @@ -51,6 +58,11 @@ namespace XericUI.VisualForm /// public bool ClampToSafeZone => m_ClampToSafeZone; + /// + /// 是否参与屏幕空间碰撞分离计算。代码修改请通过 AnchorWindow.SetEntryEnableCollision() + /// + public bool EnableCollision => m_EnableCollision; + // --- 运行时状态(非序列化) --- /// @@ -71,10 +83,43 @@ namespace XericUI.VisualForm [System.NonSerialized] public OverflowState CurrentOverflow; + /// + /// 碰撞偏移量 — 由高级锚点坞碰撞计算后写入,与 ScreenOffset 在同一坐标空间。 + /// 当 EnableCollision 为 true 且存在重叠时,坞用 (ScreenOffset + CollisionOffset) 作为最终偏移。 + /// + [System.NonSerialized] + public Vector2 CollisionOffset; + + /// + /// 是否正在参与碰撞(由锚点坞标记,当前帧存在重叠时为true) + /// + [System.NonSerialized] + public bool IsColliding; + // --- 内部方法(供 AnchorWindow 调用) --- - internal void SetEnabledInternal(bool value) => m_Enabled = value; - internal void SetClampToSafeZoneInternal(bool value) => m_ClampToSafeZone = value; + internal void SetEnabledInternal(bool value) { m_Enabled = value; MarkDirty(); } + internal void SetClampToSafeZoneInternal(bool value) { m_ClampToSafeZone = value; MarkDirty(); } + internal void SetEnableCollisionInternal(bool value) { m_EnableCollision = value; MarkDirty(); } + + // --- 依赖脏标记 --- + + /// + /// 关联的WindowState(由WindowState在初始化时设置) + /// + internal WindowState State { get; set; } + + /// + /// 标记此条目为脏,通知锚点坞在下一帧更新中处理此条目。 + /// 在运行时修改ScreenOffset、Enabled、ClampToSafeZone等属性时自动调用。 + /// + public void MarkDirty() + { + if (State != null) + { + State.CoordinateDirty = true; + } + } // --- 实例生命周期虚方法(重写以自定义窗口行为) --- diff --git a/Runtime/VisualForm/WindowState.cs b/Runtime/VisualForm/WindowState.cs index 4fbb931..ee16859 100644 --- a/Runtime/VisualForm/WindowState.cs +++ b/Runtime/VisualForm/WindowState.cs @@ -52,6 +52,11 @@ namespace XericUI.VisualForm /// public bool HasAnyEnabled { get; private set; } + /// + /// 是否存在至少一个参与碰撞的条目 + /// + public bool HasAnyCollisionEntry { get; private set; } + /// /// 坐标脏标记 - 当锚点世界坐标发生变化时标记 /// @@ -124,6 +129,9 @@ namespace XericUI.VisualForm // 通知条目实例已创建(无论是新实例化还是从池中复用的) entry.OnInstanceCreated(this, entry.Instance); + // 将WindowState引用注入条目,使其能通过MarkDirty通知坞 + entry.State = this; + Entries.Add(entry); } @@ -209,6 +217,7 @@ namespace XericUI.VisualForm Overflow = OverflowState.InRange; HasAnyClamp = false; HasAnyEnabled = false; + HasAnyCollisionEntry = false; CoordinateDirty = false; _lastPosition = Vector3.zero; } @@ -221,15 +230,18 @@ namespace XericUI.VisualForm { bool anyClamp = false; bool anyEnabled = false; + bool anyCollision = false; foreach (var entry in Entries) { if (entry.ClampToSafeZone) anyClamp = true; if (entry.Enabled) anyEnabled = true; + if (entry.EnableCollision) anyCollision = true; } HasAnyClamp = anyClamp; HasAnyEnabled = anyEnabled; + HasAnyCollisionEntry = anyCollision; } ///