From 8dfcaba43a59c91d9cc7e3ce77e9630e355f9248 Mon Sep 17 00:00:00 2001
From: LiRuoChen <571244399@qq.com>
Date: Tue, 16 Jun 2026 17:52:15 +0800
Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=A4=84=E7=90=86=E7=AA=97?=
=?UTF-8?q?=E4=BD=93=E6=B3=A8=E9=94=80=E7=9A=84=E9=80=BB=E8=BE=91=E3=80=82?=
=?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86=20=E6=BF=80=E6=B4=BB?=
=?UTF-8?q?=E6=B5=81=E7=A8=8B=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Runtime/VisualForm/AnchorDock.cs | 243 +++++++----------------------
Runtime/VisualForm/AnchorWindow.cs | 43 +++--
Runtime/VisualForm/WindowEntry.cs | 16 +-
Runtime/VisualForm/WindowState.cs | 158 ++++++++++++++++++-
Runtime/VisualForm/WorldAnchor.cs | 14 ++
5 files changed, 267 insertions(+), 207 deletions(-)
diff --git a/Runtime/VisualForm/AnchorDock.cs b/Runtime/VisualForm/AnchorDock.cs
index 6820064..ebd62d4 100644
--- a/Runtime/VisualForm/AnchorDock.cs
+++ b/Runtime/VisualForm/AnchorDock.cs
@@ -38,19 +38,6 @@ 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;
@@ -119,6 +106,11 @@ namespace XericUI.VisualForm
///
private bool _cameraDirty;
+ ///
+ /// 子类可读的摄像机脏标记
+ ///
+ protected bool IsCameraDirty => _cameraDirty;
+
///
/// 上帧世界摄像机坐标
///
@@ -139,10 +131,33 @@ namespace XericUI.VisualForm
///
private Quaternion _lastUICameraRotation;
+ #endregion
+
+ #region 子类可访问属性
+
///
- /// 碰撞计算计时器(每帧累加deltaTime,超过CollisionInterval时触发计算)
+ /// 活跃的窗口状态字典(供子类如AdvancedAnchorDock进行碰撞计算)
///
- private float _collisionTimer;
+ protected Dictionary ActiveStates => _activeStates;
+
+ ///
+ /// Canvas本地空间Rect(供子类获取安全区等)
+ ///
+ protected Rect CanvasLocalRect => ((RectTransform)_canvas.transform).rect;
+
+ ///
+ /// 像素安全区
+ ///
+ protected Rect PixelSafeZone
+ {
+ get
+ {
+ if (_canvas == null) return Rect.zero;
+ return m_SafeZoneUnit == SafeZoneUnit.Ratio
+ ? RatioSafeZoneToPixel(m_SafeZone, CanvasLocalRect)
+ : m_SafeZone;
+ }
+ }
#endregion
@@ -264,7 +279,7 @@ namespace XericUI.VisualForm
// 2. 检查摄像机变化
CheckCameraDirty();
- // 3. 遍历所有中间类,检查坐标脏(比较上帧和本帧位置差异)
+ // 3. 检查所有中间类的位置脏(必须在碰撞钩子之前,供碰撞判断是否需要计算)
foreach (var kvp in _activeStates)
{
var state = kvp.Value;
@@ -272,18 +287,28 @@ namespace XericUI.VisualForm
state.CheckPositionDirty(m_PositionThreshold);
}
- // 4. 碰撞分离计算(按时间间隔执行,必须在位置更新之前,确保同帧生效)
- if (m_EnableCollision)
- {
- _collisionTimer += Time.unscaledDeltaTime;
- if (_collisionTimer >= m_CollisionInterval)
- {
- _collisionTimer %= m_CollisionInterval;
- ComputeCollisionSeparation();
- }
- }
+ // 4. 碰撞分离计算(子类重写钩子,基类为空)
+ OnBeforePositionUpdates();
- // 5. 遍历所有中间类,根据脏标记更新坐标(跳过无启用条目的状态)
+ // 5. 遍历所有中间类,更新坐标
+ UpdateAllStatePositions();
+
+ // 6. 复位脏标记
+ _cameraDirty = false;
+ }
+
+ ///
+ /// 位置更新前的钩子,子类可在此注入碰撞分离等逻辑
+ ///
+ protected virtual void OnBeforePositionUpdates() { }
+
+ ///
+ /// 遍历所有中间类:更新坐标(跳过无启用条目的状态)。
+ /// 单独抽取为虚方法,便于子类重写。
+ /// CheckPositionDirty 已在 LateUpdate 步骤3 统一执行,此处不再重复。
+ ///
+ protected virtual void UpdateAllStatePositions()
+ {
foreach (var kvp in _activeStates)
{
var state = kvp.Value;
@@ -296,9 +321,6 @@ namespace XericUI.VisualForm
UpdateStatePosition(state);
}
}
-
- // 6. 复位脏标记
- _cameraDirty = false;
}
#endregion
@@ -512,16 +534,12 @@ namespace XericUI.VisualForm
if (entry.Instance == null) continue;
if (!entry.Enabled) continue;
- Vector2 rawPoint = basePoint + entry.ScreenOffset;
+ Vector2 rawPoint = basePoint + entry.EffectiveOffset;
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 + collisionOffset;
+ finalPoint = clampedBase + entry.ScreenOffset;
}
else
{
@@ -548,159 +566,6 @@ 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 子对象哈希集
///
diff --git a/Runtime/VisualForm/AnchorWindow.cs b/Runtime/VisualForm/AnchorWindow.cs
index 7b91062..c918c73 100644
--- a/Runtime/VisualForm/AnchorWindow.cs
+++ b/Runtime/VisualForm/AnchorWindow.cs
@@ -69,14 +69,21 @@ namespace XericUI.VisualForm
public void SetEntryEnabled(int index, bool enabled)
{
if (index < 0 || index >= m_WindowEntries.Count) return;
+ SetEntryEnabled(m_WindowEntries[index], enabled);
+ }
- var entry = m_WindowEntries[index];
+ ///
+ /// 直接通过条目引用设置启用状态。无需索引查找。
+ ///
+ /// 条目引用
+ /// 是否启用
+ public void SetEntryEnabled(T entry, bool enabled)
+ {
if (entry == null) return;
-
entry.SetEnabledInternal(enabled);
-
- // 同步到WindowState
- GetWindowState()?.RefreshFlags();
+ var state = GetWindowState();
+ state?.RefreshFlags();
+ state?.ApplyEntryEnabled(entry, enabled);
}
public void SetAllEntryEnabled(bool enabled)
@@ -94,13 +101,18 @@ namespace XericUI.VisualForm
public void SetEntryClampToSafeZone(int index, bool clamp)
{
if (index < 0 || index >= m_WindowEntries.Count) return;
+ SetEntryClampToSafeZone(m_WindowEntries[index], clamp);
+ }
- var entry = m_WindowEntries[index];
+ ///
+ /// 直接通过条目引用设置安全区钳制标记。无需索引查找。
+ ///
+ /// 条目引用
+ /// 是否钳制
+ public void SetEntryClampToSafeZone(T entry, bool clamp)
+ {
if (entry == null) return;
-
entry.SetClampToSafeZoneInternal(clamp);
-
- // 同步到WindowState
GetWindowState()?.RefreshFlags();
}
@@ -113,13 +125,18 @@ namespace XericUI.VisualForm
public void SetEntryEnableCollision(int index, bool enable)
{
if (index < 0 || index >= m_WindowEntries.Count) return;
+ SetEntryEnableCollision(m_WindowEntries[index], enable);
+ }
- var entry = m_WindowEntries[index];
+ ///
+ /// 直接通过条目引用设置碰撞参与标记。无需索引查找。
+ ///
+ /// 条目引用
+ /// 是否参与碰撞
+ public void SetEntryEnableCollision(T entry, bool enable)
+ {
if (entry == null) return;
-
entry.SetEnableCollisionInternal(enable);
-
- // 同步到WindowState
GetWindowState()?.RefreshFlags();
}
diff --git a/Runtime/VisualForm/WindowEntry.cs b/Runtime/VisualForm/WindowEntry.cs
index 1c2704c..1f2858b 100644
--- a/Runtime/VisualForm/WindowEntry.cs
+++ b/Runtime/VisualForm/WindowEntry.cs
@@ -18,7 +18,7 @@ namespace XericUI.VisualForm
private Vector2 m_ScreenOffset;
[SerializeField, Tooltip("是否启用此窗口")]
- private bool m_Enabled = true;
+ private bool m_Enabled;
[SerializeField, Tooltip("超出安全区后是否钳制在安全区内")]
private bool m_ClampToSafeZone;
@@ -96,6 +96,20 @@ namespace XericUI.VisualForm
[System.NonSerialized]
public bool IsColliding;
+ ///
+ /// 碰撞覆盖偏移量 — 在碰撞检测时,为每边的碰撞矩形扩展的像素边距。
+ /// 此值在Canvas本地空间(像素),用于在检测重叠时增加额外间距。
+ /// 默认为0,即使用实例RectTransform的原始尺寸检测碰撞。
+ ///
+ [System.NonSerialized]
+ public float CollisionMargin;
+
+ ///
+ /// 实际生效的偏移量:碰撞中时返回 ScreenOffset + CollisionOffset,否则仅返回 ScreenOffset。
+ /// 锚点坞在更新位置时使用此属性获取最终偏移。
+ ///
+ public Vector2 EffectiveOffset => (EnableCollision && IsColliding) ? ScreenOffset + CollisionOffset : ScreenOffset;
+
// --- 内部方法(供 AnchorWindow 调用) ---
internal void SetEnabledInternal(bool value) { m_Enabled = value; MarkDirty(); }
diff --git a/Runtime/VisualForm/WindowState.cs b/Runtime/VisualForm/WindowState.cs
index ee16859..30c3149 100644
--- a/Runtime/VisualForm/WindowState.cs
+++ b/Runtime/VisualForm/WindowState.cs
@@ -57,6 +57,20 @@ namespace XericUI.VisualForm
///
public bool HasAnyCollisionEntry { get; private set; }
+ ///
+ /// 是否存在任何条目当前处于碰撞中(CollisionOffset 非零)。
+ /// 由 SetCollisionOffset 自动维护,供外部查询。
+ ///
+ public bool HasAnyCollisionDirty { get; private set; }
+
+ ///
+ /// 碰撞重算标记 — 坐标自上次碰撞后是否变化,用于触发碰撞重新计算。
+ /// 由 CheckPositionDirty 设置,碰撞计算后由 ResetCollisionState 复位。
+ /// 与 CoordinateDirty 分离:CoordinateDirty 在 UpdateStatePosition 后被清除,
+ /// 而此标记保留到碰撞计算完成。
+ ///
+ public bool CollisionDirty { get; set; }
+
///
/// 坐标脏标记 - 当锚点世界坐标发生变化时标记
///
@@ -132,6 +146,12 @@ namespace XericUI.VisualForm
// 将WindowState引用注入条目,使其能通过MarkDirty通知坞
entry.State = this;
+ // 立即按entry配置的Enabled决定实例的active状态。
+ // OnInstanceCreated可能已修改Enabled为false,必须在此处应用,
+ // 避免实例在后续OnEnable中被跳过激活但保持active的问题。
+ if (entry.Instance != null)
+ entry.Instance.SetActive(entry.Enabled);
+
Entries.Add(entry);
}
@@ -149,18 +169,21 @@ namespace XericUI.VisualForm
if (Entries.Count == 0)
ResolveWindowEntries();
- // 激活所有启用的条目实例
+ // 按每个条目的Enabled配置统一设置实例active状态
foreach (var entry in Entries)
{
- if (entry.Instance != null && entry.Enabled)
+ if (entry.Instance != null)
{
- entry.Instance.SetActive(true);
- entry.OnInstanceEnable(this, entry.Instance);
+ bool shouldActive = entry.Enabled;
+ entry.Instance.SetActive(shouldActive);
+ if (shouldActive)
+ entry.OnInstanceEnable(this, entry.Instance);
}
}
// 激活时强制标记坐标脏,确保下一帧更新位置
CoordinateDirty = true;
+ CollisionDirty = true;
}
///
@@ -219,6 +242,7 @@ namespace XericUI.VisualForm
HasAnyEnabled = false;
HasAnyCollisionEntry = false;
CoordinateDirty = false;
+ CollisionDirty = false;
_lastPosition = Vector3.zero;
}
@@ -244,6 +268,131 @@ namespace XericUI.VisualForm
HasAnyCollisionEntry = anyCollision;
}
+ ///
+ /// 立即将条目的Enabled状态应用到实例并触发对应生命周期回调。
+ /// 仅在锚点处于激活状态(IsEnabled)时生效,否则仅刷新标记等待下次OnEnable。
+ ///
+ internal void ApplyEntryEnabled(WindowEntry entry, bool enabled)
+ {
+ if (entry == null || entry.Instance == null) return;
+ if (!IsEnabled) return;
+
+ bool wasActive = entry.Instance.activeSelf;
+ entry.Instance.SetActive(enabled);
+
+ if (enabled && !wasActive)
+ entry.OnInstanceEnable(this, entry.Instance);
+ else if (!enabled && wasActive)
+ entry.OnInstanceDisable(this, entry.Instance);
+ }
+
+ #region 碰撞偏移管理
+
+ ///
+ /// 逐条目的碰撞矩形缓存(Canvas本地空间,不含碰撞偏移,不含Margin)。
+ /// 在碰撞计算前由 AdvancedAnchorDock 负责分批刷新。
+ ///
+ private readonly Dictionary _collisionRects = new Dictionary();
+
+ ///
+ /// 获取条目的原始碰撞矩形缓存(Canvas本地空间,不含CollisionOffset和Margin)。
+ ///
+ public Rect GetCollisionRect(WindowEntry entry)
+ {
+ _collisionRects.TryGetValue(entry, out var r);
+ return r;
+ }
+
+ ///
+ /// 设置条目的碰撞矩形缓存。
+ ///
+ internal void SetCollisionRect(WindowEntry entry, Rect rect)
+ {
+ _collisionRects[entry] = rect;
+ }
+
+ ///
+ /// 清空所有碰撞矩形缓存。
+ ///
+ internal void ClearCollisionRects()
+ {
+ _collisionRects.Clear();
+ }
+
+ ///
+ /// 获取条目用于碰撞检测的矩形(Canvas本地空间,含 CollisionMargin 扩展)。
+ /// 每次碰撞检测时以此扩展后的矩形判断是否重叠。
+ ///
+ public Rect GetEntryCollisionRect(WindowEntry entry)
+ {
+ if (entry == null || !_collisionRects.TryGetValue(entry, out Rect baseRect))
+ return Rect.zero;
+
+ float m = entry.CollisionMargin;
+ if (m > 0f)
+ return new Rect(baseRect.x - m, baseRect.y - m, baseRect.width + m * 2f, baseRect.height + m * 2f);
+ return baseRect;
+ }
+
+ ///
+ /// 获取条目在当前碰撞推挤位置下的矩形(Canvas本地空间,含 CollisionOffset + CollisionMargin)。
+ /// 供碰撞计算迭代过程中使用。
+ ///
+ public Rect GetEntryPushedRect(WindowEntry entry)
+ {
+ Rect rect = GetEntryCollisionRect(entry);
+ if (rect == Rect.zero) return Rect.zero;
+ rect.position += entry.CollisionOffset;
+ return rect;
+ }
+
+ ///
+ /// 设置条目的碰撞偏移量。自动标记 IsColliding=true 和 HasAnyCollisionDirty。
+ /// 碰撞偏移量相对于 ScreenOffset 空间(Canvas本地空间)。
+ ///
+ public void SetCollisionOffset(WindowEntry entry, Vector2 offset)
+ {
+ if (entry == null) return;
+ entry.CollisionOffset = offset;
+ if (offset != Vector2.zero)
+ {
+ entry.IsColliding = true;
+ HasAnyCollisionDirty = true;
+ }
+ }
+
+ ///
+ /// 重置所有条目碰撞状态(碰撞偏移清零,IsColliding置false)。
+ /// 由 AdvancedAnchorDock 在每次碰撞计算开始时调用,确保从原始位置重新计算。
+ ///
+ public void ResetCollisionState()
+ {
+ CollisionDirty = false;
+ HasAnyCollisionDirty = false;
+ foreach (var entry in Entries)
+ {
+ entry.CollisionOffset = Vector2.zero;
+ entry.IsColliding = false;
+ }
+ }
+
+ ///
+ /// 碰撞计算完成后调用:标记 CoordinateDirty,让下一帧位置更新通过 EffectiveOffset 自动获取碰撞偏移。
+ ///
+ public void ApplyCollisionResult()
+ {
+ foreach (var entry in Entries)
+ {
+ if (entry.IsColliding)
+ {
+ CoordinateDirty = true;
+ break;
+ }
+ }
+ }
+
+ #endregion
+
///
/// 检查锚点坐标是否发生变化,超过阈值则标记脏
/// 由锚点坞在每帧调用
@@ -256,6 +405,7 @@ namespace XericUI.VisualForm
if (Vector3.Distance(_lastPosition, currentPos) > threshold)
{
CoordinateDirty = true;
+ CollisionDirty = true; // 独立的碰撞重算标记,不会在 UpdateStatePosition 后被清除
_lastPosition = currentPos;
}
}
diff --git a/Runtime/VisualForm/WorldAnchor.cs b/Runtime/VisualForm/WorldAnchor.cs
index aadabc5..9f9039e 100644
--- a/Runtime/VisualForm/WorldAnchor.cs
+++ b/Runtime/VisualForm/WorldAnchor.cs
@@ -61,6 +61,20 @@ namespace XericUI.VisualForm
}
protected void OnDisable()
+ {
+ UnregisterFromDock();
+ }
+
+ protected void OnDestroy()
+ {
+ UnregisterFromDock();
+ }
+
+ ///
+ /// 从锚点坞或待注册列表中注销此锚点。
+ /// OnDisable 和 OnDestroy 共用,确保所有退出路径都执行清理。
+ ///
+ private void UnregisterFromDock()
{
if (m_TargetDock != null)
m_TargetDock.RemoveAnchor(this);