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);