Files
XericUIActionVessel/Runtime/VisualForm/AdvancedAnchorDock.cs

310 lines
12 KiB
C#
Raw Permalink 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 UnityEngine;
namespace XericUI.VisualForm
{
/// <summary>
/// 高级锚点坞 — 在 AnchorDock 基础上扩展屏幕空间碰撞分离功能。
/// 通过四叉树加速碰撞检测迭代推挤分离重叠的UI窗口条目。
///
/// 核心流程:
/// 1. 判断是否有脏标记(坐标变化),无脏则跳过碰撞
/// 2. 重置所有条目碰撞状态(从原始 ScreenOffset 重新计算,不依赖上一帧)
/// 3. 刷新各 WindowState 中条目的 Canvas 本地空间碰撞矩形缓存
/// 4. 构建四叉树 → 迭代推挤分离 → 写入 CollisionOffset
/// 5. 下一帧 UpdateStatePosition 通过 EffectiveOffset 自动应用碰撞偏移
/// </summary>
[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-10.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<WindowEntry> _quadTree;
private readonly List<WindowEntry> _collisionEntries = new List<WindowEntry>();
private readonly HashSet<WindowEntry> _overlapResults = new HashSet<WindowEntry>();
#region
protected override void OnEnable()
{
base.OnEnable();
_collisionTimer = 0f;
_quadTree = null;
}
/// <summary>
/// 在基类位置更新之前注入碰撞分离计算。
/// 仅在启用碰撞且存在脏标记时才执行。
/// 摄像机变化、坐标变化或首次激活时立即触发,其余情况按间隔节流。
/// </summary>
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
/// <summary>
/// 是否需要重新计算碰撞。仅在摄像机变化或窗口坐标变化时返回 true。
/// 使用独立的 CollisionDirty 标记(不被 UpdateStatePosition 清除),而非 CoordinateDirty。
/// </summary>
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;
}
/// <summary>
/// 主碰撞分离计算入口。
/// 流程:重置 → 刷新矩形 → 构建四叉树 → 迭代推挤 → 应用结果。
/// </summary>
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();
}
}
/// <summary>
/// 刷新所有活跃 WindowState 中各条目的碰撞矩形缓存。
/// 每个矩形位于 Canvas 本地空间,中心 = 锚点世界→Canvas坐标 + ScreenOffset
/// 尺寸 = 条目实例 RectTransform.rect.size。
/// </summary>
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);
}
}
}
/// <summary>
/// 收集所有参与碰撞的活跃条目到一个扁平列表中。
/// </summary>
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);
}
}
}
/// <summary>
/// 构建四叉树:计算所有条目包围盒 → 清空重建 → 插入所有条目。
/// 树中存储的是 WindowEntrygetRectFunc 返回的是含 CollisionOffset 和 Margin 的实时矩形。
/// </summary>
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<WindowEntry>(bounds, GetEntryPushedRect, m_QuadTreeMaxPerNode, m_QuadTreeMaxDepth);
else
_quadTree.Rebuild(_collisionEntries);
}
/// <summary>
/// 迭代推挤分离算法。
/// 对每个条目从四叉树查询重叠对象,沿最小重叠轴推开,重复直到无重叠或达到最大迭代次数。
/// </summary>
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
/// <summary>
/// 获取条目当前的推挤位置矩形(含 CollisionOffset + CollisionMargin
/// 作为四叉树的 getRectFunc 委托,实时反映条目最新碰撞偏移。
/// </summary>
private static Rect GetEntryPushedRect(WindowEntry entry)
{
if (entry?.State == null) return Rect.zero;
return entry.State.GetEntryPushedRect(entry);
}
/// <summary>
/// 计算两个矩形的包围盒。
/// </summary>
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
}
}