From b4cb3f0976c9474fd71c004404e3d827d99c258d Mon Sep 17 00:00:00 2001
From: LiRuoChen <571244399@qq.com>
Date: Fri, 12 Jun 2026 09:12:06 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B0=94=E6=B3=A1=E5=B8=83?=
=?UTF-8?q?=E5=B1=80=E5=AD=98=E5=9C=A8=E7=9A=84=E4=B8=80=E4=BA=9B=E9=97=AE?=
=?UTF-8?q?=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../BubbleLayout/BubbleLayoutEditorStyles.cs | 182 ++++++
.../BubbleLayoutEditorStyles.cs.meta | 11 +
.../BubbleLayout/BubbleLayoutGroupEditor.cs | 16 +-
.../BubbleLayout/BubbleLayoutPluginEditor.cs | 31 +-
Runtime/BubbleLayout/BoundaryConstraint.cs | 330 +++++++++++
.../BubbleLayout/BoundaryConstraint.cs.meta | 11 +
Runtime/BubbleLayout/BubbleItemData.cs | 4 +-
Runtime/BubbleLayout/BubbleLayoutGroup.cs | 9 +-
.../BubbleLayout/BubbleLayoutPluginBase.cs | 12 +-
.../BubbleLayout/CircularLayoutConstraint.cs | 106 ++++
.../HorizontalLayoutConstraint.cs | 155 +++--
Runtime/BubbleLayout/LayoutBox.cs | 82 +++
Runtime/BubbleLayout/LayoutBox.cs.meta | 11 +
.../LinearLayoutConstraintBase.cs | 531 ++++++++++++++++++
.../LinearLayoutConstraintBase.cs.meta | 11 +
Runtime/BubbleLayout/QuadTree.cs | 451 +++++++++++++++
Runtime/BubbleLayout/QuadTree.cs.meta | 3 +
.../BubbleLayout/VerticalLayoutConstraint.cs | 253 +++++++--
Runtime/DynamicActivitiesImpact.meta | 3 -
.../UIDynamicActiveImpact.cs | 325 -----------
.../UIDynamicActiveImpact.cs.meta | 3 -
21 files changed, 2035 insertions(+), 505 deletions(-)
create mode 100644 Editor/BubbleLayout/BubbleLayoutEditorStyles.cs
create mode 100644 Editor/BubbleLayout/BubbleLayoutEditorStyles.cs.meta
create mode 100644 Runtime/BubbleLayout/BoundaryConstraint.cs
create mode 100644 Runtime/BubbleLayout/BoundaryConstraint.cs.meta
create mode 100644 Runtime/BubbleLayout/LayoutBox.cs
create mode 100644 Runtime/BubbleLayout/LayoutBox.cs.meta
create mode 100644 Runtime/BubbleLayout/LinearLayoutConstraintBase.cs
create mode 100644 Runtime/BubbleLayout/LinearLayoutConstraintBase.cs.meta
create mode 100644 Runtime/BubbleLayout/QuadTree.cs
create mode 100644 Runtime/BubbleLayout/QuadTree.cs.meta
delete mode 100644 Runtime/DynamicActivitiesImpact.meta
delete mode 100644 Runtime/DynamicActivitiesImpact/UIDynamicActiveImpact.cs
delete mode 100644 Runtime/DynamicActivitiesImpact/UIDynamicActiveImpact.cs.meta
diff --git a/Editor/BubbleLayout/BubbleLayoutEditorStyles.cs b/Editor/BubbleLayout/BubbleLayoutEditorStyles.cs
new file mode 100644
index 0000000..47f966f
--- /dev/null
+++ b/Editor/BubbleLayout/BubbleLayoutEditorStyles.cs
@@ -0,0 +1,182 @@
+using UnityEngine;
+using UnityEditor;
+
+namespace XericUIEditor.BubbleLayout
+{
+ ///
+ /// BubbleLayout Editor 共享样式,提供白色圆角边框 + 深色底的 GUIStyle。
+ ///
+ internal static class BubbleLayoutEditorStyles
+ {
+ private static GUIStyle s_BoxStyle;
+ private static bool s_Initialized;
+
+ /// 白色圆角边框 + 深灰黑色底的 Box 样式
+ public static GUIStyle BoxStyle
+ {
+ get
+ {
+ EnsureInitialized();
+ return s_BoxStyle;
+ }
+ }
+
+ private static void EnsureInitialized()
+ {
+ if (s_Initialized)
+ return;
+
+ s_BoxStyle = new GUIStyle(GUI.skin.box)
+ {
+ padding = new RectOffset(12, 12, 10, 10),
+ margin = new RectOffset(4, 4, 4, 4),
+ };
+
+ const int borderThickness = 2;
+ const int cornerRadius = 4;
+
+ s_BoxStyle.normal.background = GenerateRoundedBorderTexture(
+ borderThickness, cornerRadius,
+ Color.white,
+ new Color(0.155f, 0.155f, 0.155f, 1f),
+ out var borderInset);
+
+ s_BoxStyle.border = borderInset;
+
+ s_Initialized = true;
+ }
+
+ ///
+ /// 生成一张用于 GUIStyle 9-slice 的圆角边框纹理。
+ ///
+ /// 纹理布局(9-slice):
+ /// borderInset ─┤├─ edge ─┤├─ borderInset
+ ///
+ /// 左上角块 │ 上边缘 │ 右上角块
+ /// ──────────┼─────────┼──────────
+ /// 左边缘 │ 中心 │ 右边缘
+ /// ──────────┼─────────┼──────────
+ /// 左下角块 │ 下边缘 │ 右下角块
+ ///
+ private static Texture2D GenerateRoundedBorderTexture(
+ int borderThickness,
+ int cornerRadius,
+ Color borderColor,
+ Color fillColor,
+ out RectOffset borderInset)
+ {
+ // 角块大小必须能容纳整个圆角装饰 (圆角半径 + 边框宽度)
+ int cornerBlockSize = cornerRadius + borderThickness + 1;
+ // 边缘条带宽度
+ int edgeWidth = 4;
+ // 中心区域最小尺寸
+ int centerSize = 2;
+
+ int texSize = cornerBlockSize * 2 + edgeWidth;
+ // 确保中心区域有像素
+ if (edgeWidth < centerSize)
+ {
+ edgeWidth = centerSize;
+ texSize = cornerBlockSize * 2 + edgeWidth;
+ }
+
+ borderInset = new RectOffset(cornerBlockSize, cornerBlockSize, cornerBlockSize, cornerBlockSize);
+
+ var tex = new Texture2D(texSize, texSize, TextureFormat.RGBA32, false);
+ tex.wrapMode = TextureWrapMode.Clamp;
+ tex.filterMode = FilterMode.Bilinear;
+
+ Color transparent = new Color(0, 0, 0, 0);
+ Color[] pixels = new Color[texSize * texSize];
+
+ int maxCoord = texSize - 1;
+ float outerR = cornerRadius + borderThickness; // 外圆角半径(白色边框的外缘)
+ float innerR = cornerRadius; // 内圆角半径(深色填充的外缘)
+
+ for (int y = 0; y < texSize; y++)
+ {
+ for (int x = 0; x < texSize; x++)
+ {
+ // 像素是否在「外圆角矩形」内(白色边框区域)
+ bool inOuter = IsInsideRoundedRect(x, y, 0, 0, maxCoord, maxCoord, outerR);
+ if (!inOuter)
+ {
+ pixels[y * texSize + x] = transparent;
+ continue;
+ }
+
+ // 像素是否在「内圆角矩形」内(深色填充区域)
+ int innerMin = borderThickness;
+ int innerMax = maxCoord - borderThickness;
+
+ bool inInner = IsInsideRoundedRect(
+ x, y,
+ innerMin, innerMin,
+ innerMax, innerMax,
+ innerR);
+
+ pixels[y * texSize + x] = inInner ? fillColor : borderColor;
+ }
+ }
+
+ tex.SetPixels(pixels);
+ tex.Apply();
+ return tex;
+ }
+
+ ///
+ /// 判断坐标 (x,y) 是否在由 (minX,minY)-(maxX,maxY) 定义的圆角矩形内。
+ ///
+ private static bool IsInsideRoundedRect(
+ int x, int y,
+ int minX, int minY,
+ int maxX, int maxY,
+ float radius)
+ {
+ // 完全在边界外
+ if (x < minX || x > maxX || y < minY || y > maxY)
+ return false;
+
+ // 主体矩形区域(远离四个角)
+ if (x >= minX + radius && x <= maxX - radius) return true;
+ if (y >= minY + radius && y <= maxY - radius) return true;
+
+ // 四个圆角区域 —— 判断是否在对应的四分之一圆内
+ float cx, cy;
+
+ if (x < minX + radius && y < minY + radius)
+ {
+ // 左上角
+ cx = minX + radius;
+ cy = minY + radius;
+ }
+ else if (x > maxX - radius && y < minY + radius)
+ {
+ // 右上角
+ cx = maxX - radius;
+ cy = minY + radius;
+ }
+ else if (x < minX + radius && y > maxY - radius)
+ {
+ // 左下角
+ cx = minX + radius;
+ cy = maxY - radius;
+ }
+ else if (x > maxX - radius && y > maxY - radius)
+ {
+ // 右下角
+ cx = maxX - radius;
+ cy = maxY - radius;
+ }
+ else
+ {
+ // 边缘条带区域(不在任何角区),在矩形边界内
+ return true;
+ }
+
+ float dx = x - cx;
+ float dy = y - cy;
+ return (dx * dx + dy * dy) <= (radius * radius);
+ }
+ }
+}
diff --git a/Editor/BubbleLayout/BubbleLayoutEditorStyles.cs.meta b/Editor/BubbleLayout/BubbleLayoutEditorStyles.cs.meta
new file mode 100644
index 0000000..6c9b636
--- /dev/null
+++ b/Editor/BubbleLayout/BubbleLayoutEditorStyles.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3b3b1ec1e8a20824488b035d1258c2e6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/BubbleLayout/BubbleLayoutGroupEditor.cs b/Editor/BubbleLayout/BubbleLayoutGroupEditor.cs
index 8a05ade..446f7bb 100644
--- a/Editor/BubbleLayout/BubbleLayoutGroupEditor.cs
+++ b/Editor/BubbleLayout/BubbleLayoutGroupEditor.cs
@@ -25,7 +25,6 @@ namespace XericUIEditor.BubbleLayout
private SerializedProperty m_LayoutIgnoreInactive;
// ── 样式 ──
- private static GUIStyle s_BoxStyle;
private static GUIStyle s_PerfHeaderStyle;
private static GUIStyle s_PerfLabelStyle;
private static GUIStyle s_PerfValueStyle;
@@ -36,17 +35,6 @@ namespace XericUIEditor.BubbleLayout
if (s_StylesInitialized)
return;
- s_BoxStyle = new GUIStyle(GUI.skin.box)
- {
- padding = new RectOffset(12, 12, 10, 10),
- margin = new RectOffset(4, 4, 4, 4),
- };
-
- var bgTex = new Texture2D(1, 1);
- bgTex.SetPixel(0, 0, new Color(0.18f, 0.18f, 0.18f, 1f));
- bgTex.Apply();
- s_BoxStyle.normal.background = bgTex;
-
s_PerfHeaderStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 11,
@@ -93,7 +81,7 @@ namespace XericUIEditor.BubbleLayout
EnsureStyles();
// ── 白色圆角边框 + 深色底 ──
- EditorGUILayout.BeginVertical(s_BoxStyle);
+ EditorGUILayout.BeginVertical(BubbleLayoutEditorStyles.BoxStyle);
serializedObject.Update();
@@ -147,7 +135,7 @@ namespace XericUIEditor.BubbleLayout
EditorGUILayout.Space(6);
- EditorGUILayout.BeginVertical(s_BoxStyle);
+ EditorGUILayout.BeginVertical(BubbleLayoutEditorStyles.BoxStyle);
EditorGUILayout.LabelField("性能指标", s_PerfHeaderStyle);
EditorGUILayout.Space(2);
diff --git a/Editor/BubbleLayout/BubbleLayoutPluginEditor.cs b/Editor/BubbleLayout/BubbleLayoutPluginEditor.cs
index 5427cbd..443dadc 100644
--- a/Editor/BubbleLayout/BubbleLayoutPluginEditor.cs
+++ b/Editor/BubbleLayout/BubbleLayoutPluginEditor.cs
@@ -5,42 +5,16 @@ using XericUI.BubbleLayout;
namespace XericUIEditor.BubbleLayout
{
///
- /// 气泡布局插件通用 Inspector —— 白色圆角边框 + 深色底,对所有 BubbleLayoutPluginBase 子类生效
+ /// 气泡布局插件通用 Inspector —— 白色圆角边框 + 深色底,对所有 BubbleLayoutPluginBase 子类生效。
///
[CustomEditor(typeof(BubbleLayoutPluginBase), true)]
[CanEditMultipleObjects]
internal class BubbleLayoutPluginEditor : Editor
{
- private static GUIStyle s_BoxStyle;
- private static bool s_StylesInitialized;
-
- private static void EnsureStyles()
- {
- if (s_StylesInitialized)
- return;
-
- s_BoxStyle = new GUIStyle(GUI.skin.box)
- {
- padding = new RectOffset(12, 12, 10, 10),
- margin = new RectOffset(4, 4, 4, 4),
- };
-
- var bgTex = new Texture2D(1, 1);
- bgTex.SetPixel(0, 0, new Color(0.18f, 0.18f, 0.18f, 1f));
- bgTex.Apply();
- s_BoxStyle.normal.background = bgTex;
-
- s_StylesInitialized = true;
- }
-
public override void OnInspectorGUI()
{
- EnsureStyles();
+ EditorGUILayout.BeginVertical(BubbleLayoutEditorStyles.BoxStyle);
- // ── 白色圆角边框 + 深色底 ──
- EditorGUILayout.BeginVertical(s_BoxStyle);
-
- // 绘制所有序列化字段(包括基类的 Enabled 和 Order)
serializedObject.Update();
SerializedProperty iterator = serializedObject.GetIterator();
@@ -50,7 +24,6 @@ namespace XericUIEditor.BubbleLayout
{
enterChildren = false;
- // 跳过脚本引用字段
if (iterator.name == "m_Script")
continue;
diff --git a/Runtime/BubbleLayout/BoundaryConstraint.cs b/Runtime/BubbleLayout/BoundaryConstraint.cs
new file mode 100644
index 0000000..f7ace6e
--- /dev/null
+++ b/Runtime/BubbleLayout/BoundaryConstraint.cs
@@ -0,0 +1,330 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace XericUI.BubbleLayout
+{
+ ///
+ /// 边界限制模式
+ ///
+ public enum BoundaryShapeMode
+ {
+ /// 矩形范围:尺寸的 x/y 代表矩形的宽度和高度
+ Rectangle,
+
+ /// 椭圆范围:尺寸的 x/y 代表椭圆的横轴半径和纵轴半径
+ Ellipse,
+ }
+
+ ///
+ /// 边界限制约束插件 —— 将气泡元素限制在指定范围内。
+ /// 支持矩形/椭圆两种形状,并记录超出范围的元素。
+ ///
+ [AddComponentMenu("Xeric UI Vessel/Layout/Boundary Constraint", 56)]
+ public class BoundaryConstraint : BubbleLayoutPluginBase, IBubbleConstraintPlugin
+ {
+ [Header("限制范围")]
+ [Tooltip("限制形状模式:矩形或椭圆")]
+ [SerializeField] private BoundaryShapeMode m_ShapeMode = BoundaryShapeMode.Rectangle;
+
+ [Tooltip("范围尺寸。矩形模式 = 宽x高;椭圆模式 = 横轴半径x纵轴半径")]
+ [SerializeField] private Vector2 m_BoundarySize = new Vector2(400f, 300f);
+
+ [Header("行为")]
+ [Tooltip("是否在元素超出边界时将其瞬移到边界上(关闭则仅记录不修正)")]
+ [SerializeField] private bool m_ClampToBoundary = true;
+
+ #region 越界追踪
+
+ [System.NonSerialized]
+ private HashSet m_OutOfBoundsObjects = new HashSet();
+
+ ///
+ /// 记录越界元素:Key=GameObject实例ID,Value=true表示当前越界
+ ///
+ [System.NonSerialized]
+ private Dictionary m_OutOfBoundsCache = new Dictionary();
+
+ ///
+ /// 检查指定 GameObject 是否超出了边界范围
+ ///
+ public bool IsOutOfBounds(GameObject target)
+ {
+ if (target == null)
+ return false;
+ return m_OutOfBoundsCache.TryGetValue(target.GetInstanceID(), out bool value) && value;
+ }
+
+ ///
+ /// 获取当前所有越界 GameObject 的实例 ID 集合(只读)
+ ///
+ public IReadOnlyCollection GetOutOfBoundsInstanceIDs()
+ {
+ return m_OutOfBoundsObjects;
+ }
+
+ ///
+ /// 获取当前越界的元素数量
+ ///
+ public int GetOutOfBoundsCount() => m_OutOfBoundsObjects.Count;
+
+ ///
+ /// 清除越界记录
+ ///
+ public void ClearOutOfBoundsHistory()
+ {
+ m_OutOfBoundsObjects.Clear();
+ m_OutOfBoundsCache.Clear();
+ }
+
+ #endregion
+
+ #region IBubbleConstraintPlugin
+
+#if UNITY_EDITOR
+ protected override int EditorDefaultOrder => 500;
+#endif
+
+ public void ProcessConstraint(List items, Vector2 layoutCenter)
+ {
+ if (items == null || items.Count == 0)
+ return;
+
+ // 本轮重置越界缓存
+ m_OutOfBoundsObjects.Clear();
+ m_OutOfBoundsCache.Clear();
+
+ for (int i = 0; i < items.Count; i++)
+ {
+ var item = items[i];
+ if (item.rectTransform == null)
+ continue;
+
+ int instanceId = item.rectTransform.GetInstanceID();
+ bool outOfBounds = false;
+
+ // 根据形状模式检测 item 矩形是否完全或部分越界
+ switch (m_ShapeMode)
+ {
+ case BoundaryShapeMode.Rectangle:
+ outOfBounds = !IsFullyInsideRect(
+ item.position, item.size, layoutCenter, m_BoundarySize);
+ break;
+
+ case BoundaryShapeMode.Ellipse:
+ outOfBounds = !IsFullyInsideEllipse(
+ item.position, item.size, layoutCenter, m_BoundarySize);
+ break;
+ }
+
+ m_OutOfBoundsCache[instanceId] = outOfBounds;
+
+ if (outOfBounds)
+ {
+ m_OutOfBoundsObjects.Add(instanceId);
+
+ if (m_ClampToBoundary)
+ {
+ // 将元素中心钳制到边界内
+ item.position = ClampToBoundary(item.position, item.size, layoutCenter, m_BoundarySize, m_ShapeMode);
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ #region 碰撞检测
+
+ ///
+ /// 检测矩形元素是否完全在矩形边界内
+ ///
+ private static bool IsFullyInsideRect(Vector2 center, Vector2 size, Vector2 boundaryCenter, Vector2 boundarySize)
+ {
+ float halfBW = boundarySize.x * 0.5f;
+ float halfBH = boundarySize.y * 0.5f;
+ float halfW = size.x * 0.5f;
+ float halfH = size.y * 0.5f;
+
+ return center.x - halfW >= boundaryCenter.x - halfBW
+ && center.x + halfW <= boundaryCenter.x + halfBW
+ && center.y - halfH >= boundaryCenter.y - halfBH
+ && center.y + halfH <= boundaryCenter.y + halfBH;
+ }
+
+ ///
+ /// 检测矩形元素是否完全在椭圆边界内
+ /// 椭圆方程:dx²/rx² + dy²/ry² ≤ 1 时需要元素所有角点均在椭圆内。
+ /// 近似检测:元素中心 + 对角线半长构成的「外接圆」半径 vs 椭圆最大轴向半径。
+ /// 精确检测:依次检查四个角点。
+ ///
+ private static bool IsFullyInsideEllipse(Vector2 center, Vector2 size, Vector2 boundaryCenter, Vector2 boundaryRadius)
+ {
+ float rx = boundaryRadius.x;
+ float ry = boundaryRadius.y;
+
+ if (rx <= 0f || ry <= 0f)
+ return false;
+
+ // 取矩形的四个角偏移量
+ float halfW = size.x * 0.5f;
+ float halfH = size.y * 0.5f;
+
+ // 检查四个角点是否都在椭圆内
+ return IsPointInsideEllipse(center.x - halfW, center.y - halfH, boundaryCenter, rx, ry)
+ && IsPointInsideEllipse(center.x + halfW, center.y - halfH, boundaryCenter, rx, ry)
+ && IsPointInsideEllipse(center.x - halfW, center.y + halfH, boundaryCenter, rx, ry)
+ && IsPointInsideEllipse(center.x + halfW, center.y + halfH, boundaryCenter, rx, ry);
+ }
+
+ ///
+ /// 判断一个点是否在椭圆内:dx²/rx² + dy²/ry² ≤ 1
+ ///
+ private static bool IsPointInsideEllipse(float px, float py, Vector2 center, float rx, float ry)
+ {
+ float dx = (px - center.x) / rx;
+ float dy = (py - center.y) / ry;
+ return dx * dx + dy * dy <= 1f;
+ }
+
+ ///
+ /// 将元素中心钳制到边界内(保留最大可能的尺寸)
+ ///
+ private static Vector2 ClampToBoundary(
+ Vector2 center, Vector2 size, Vector2 boundaryCenter, Vector2 boundarySize, BoundaryShapeMode mode)
+ {
+ float halfW = size.x * 0.5f;
+ float halfH = size.y * 0.5f;
+
+ switch (mode)
+ {
+ case BoundaryShapeMode.Rectangle:
+ {
+ float halfBW = boundarySize.x * 0.5f;
+ float halfBH = boundarySize.y * 0.5f;
+
+ // 确保有足够空间容纳元素
+ float maxW = Mathf.Max(halfBW - halfW, 0f);
+ float maxH = Mathf.Max(halfBH - halfH, 0f);
+
+ center.x = Mathf.Clamp(center.x, boundaryCenter.x - maxW, boundaryCenter.x + maxW);
+ center.y = Mathf.Clamp(center.y, boundaryCenter.y - maxH, boundaryCenter.y + maxH);
+ return center;
+ }
+
+ case BoundaryShapeMode.Ellipse:
+ {
+ float rx = boundarySize.x;
+ float ry = boundarySize.y;
+
+ if (rx <= halfW || ry <= halfH)
+ {
+ // 椭圆太小无法容纳元素,强制居中
+ return boundaryCenter;
+ }
+
+ // 计算元素中心在椭圆内的有效范围
+ float effectiveRx = rx - halfW;
+ float effectiveRy = ry - halfH;
+
+ // 将 center 投影到有效椭圆内
+ float dx = center.x - boundaryCenter.x;
+ float dy = center.y - boundaryCenter.y;
+
+ // 椭圆约束:dx²/effectiveRx² + dy²/effectiveRy² ≤ 1
+ if (effectiveRx > 0f && effectiveRy > 0f)
+ {
+ float nx = dx / effectiveRx;
+ float ny = dy / effectiveRy;
+ float dist = nx * nx + ny * ny;
+
+ if (dist > 1f)
+ {
+ float scale = 1f / Mathf.Sqrt(dist);
+ dx *= scale;
+ dy *= scale;
+ }
+ }
+
+ return boundaryCenter + new Vector2(dx, dy);
+ }
+
+ default:
+ return center;
+ }
+ }
+
+ #endregion
+
+ #region Editor Gizmos
+
+#if UNITY_EDITOR
+ protected virtual void OnDrawGizmosSelected()
+ {
+ if (!isActiveAndEnabled)
+ return;
+
+ var parentRT = transform as RectTransform;
+ Vector2 layoutCenter;
+ if (parentRT != null)
+ {
+ layoutCenter = new Vector2(
+ parentRT.rect.width * (0.5f - parentRT.pivot.x),
+ parentRT.rect.height * (0.5f - parentRT.pivot.y));
+ }
+ else
+ {
+ layoutCenter = Vector2.zero;
+ }
+
+ Vector3 worldCenter = transform.TransformPoint(new Vector3(layoutCenter.x, layoutCenter.y, 0));
+
+ switch (m_ShapeMode)
+ {
+ case BoundaryShapeMode.Rectangle:
+ {
+ Vector3 worldSize = transform.TransformVector(new Vector3(m_BoundarySize.x, m_BoundarySize.y, 0));
+ Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.25f);
+ Gizmos.DrawCube(worldCenter, worldSize);
+ Gizmos.color = new Color(1f, 0.2f, 0.2f, 0.7f);
+ Gizmos.DrawWireCube(worldCenter, worldSize);
+ break;
+ }
+
+ case BoundaryShapeMode.Ellipse:
+ {
+ float rx = m_BoundarySize.x;
+ float ry = m_BoundarySize.y;
+ int segments = 64;
+ float step = 360f / segments * Mathf.Deg2Rad;
+
+ Vector3 prevPoint = worldCenter + new Vector3(Mathf.Cos(0) * rx, Mathf.Sin(0) * ry, 0);
+
+ // 半透明椭圆盘
+ Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.12f);
+ for (int i = 1; i <= segments; i++)
+ {
+ float angle = step * i;
+ Vector3 nextPoint = worldCenter + new Vector3(Mathf.Cos(angle) * rx, Mathf.Sin(angle) * ry, 0);
+ Gizmos.DrawLine(worldCenter, nextPoint); // 辐线填充
+ prevPoint = nextPoint;
+ }
+
+ // 椭圆边框
+ prevPoint = worldCenter + new Vector3(Mathf.Cos(0) * rx, Mathf.Sin(0) * ry, 0);
+ Gizmos.color = new Color(1f, 0.2f, 0.2f, 0.7f);
+ for (int i = 1; i <= segments; i++)
+ {
+ float angle = step * i;
+ Vector3 nextPoint = worldCenter + new Vector3(Mathf.Cos(angle) * rx, Mathf.Sin(angle) * ry, 0);
+ Gizmos.DrawLine(prevPoint, nextPoint);
+ prevPoint = nextPoint;
+ }
+ break;
+ }
+ }
+ }
+#endif
+
+ #endregion
+ }
+}
diff --git a/Runtime/BubbleLayout/BoundaryConstraint.cs.meta b/Runtime/BubbleLayout/BoundaryConstraint.cs.meta
new file mode 100644
index 0000000..bad7f36
--- /dev/null
+++ b/Runtime/BubbleLayout/BoundaryConstraint.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bd7765032236e2c42aea99d854c4e754
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/BubbleLayout/BubbleItemData.cs b/Runtime/BubbleLayout/BubbleItemData.cs
index 3e09f56..14eddd6 100644
--- a/Runtime/BubbleLayout/BubbleItemData.cs
+++ b/Runtime/BubbleLayout/BubbleItemData.cs
@@ -75,7 +75,7 @@ namespace XericUI.BubbleLayout
{
rectTransform = rt,
Key = $"{rt.name}_{rt.GetInstanceID()}",
- position = rt.anchoredPosition,
+ position = rt.localPosition,
size = rt.rect.size,
rotation = rt.localEulerAngles.z,
velocity = Vector2.zero,
@@ -96,7 +96,7 @@ namespace XericUI.BubbleLayout
{
if (rectTransform != null)
{
- rectTransform.anchoredPosition = position;
+ rectTransform.localPosition = new Vector3(position.x, position.y, rectTransform.localPosition.z);
}
}
}
diff --git a/Runtime/BubbleLayout/BubbleLayoutGroup.cs b/Runtime/BubbleLayout/BubbleLayoutGroup.cs
index 202f061..189ae0f 100644
--- a/Runtime/BubbleLayout/BubbleLayoutGroup.cs
+++ b/Runtime/BubbleLayout/BubbleLayoutGroup.cs
@@ -66,7 +66,6 @@ namespace XericUI.BubbleLayout
[Tooltip("是否启用插件系统")]
[SerializeField] private bool m_EnablePlugins = true;
- [Header("物理参数")]
[Tooltip("速度阻尼系数 (0=无阻尼, 1=完全停止)")]
[SerializeField] private float m_PhysicsDamping = 0.9f;
@@ -79,7 +78,6 @@ namespace XericUI.BubbleLayout
[Tooltip("物理时间步长(0表示使用Time.deltaTime)")]
[SerializeField] private float m_PhysicsDeltaTime = 0f;
- [Header("碰撞排挤")]
[Tooltip("最大迭代次数")]
[SerializeField] private int m_MaxIterations = 10;
@@ -394,12 +392,15 @@ namespace XericUI.BubbleLayout
}
///
- /// 获取布局中心点
+ /// 获取布局中心点(localPosition 空间)
+ /// localPosition (0,0) = pivot 位置,此方法返回 rect 中心相对 pivot 的偏移
///
private Vector2 GetLayoutCenter()
{
var rt = rectTransform;
- return new Vector2(rt.rect.width * 0.5f, rt.rect.height * 0.5f);
+ return new Vector2(
+ rt.rect.width * (0.5f - rt.pivot.x),
+ rt.rect.height * (0.5f - rt.pivot.y));
}
///
diff --git a/Runtime/BubbleLayout/BubbleLayoutPluginBase.cs b/Runtime/BubbleLayout/BubbleLayoutPluginBase.cs
index a83ec42..a0082e2 100644
--- a/Runtime/BubbleLayout/BubbleLayoutPluginBase.cs
+++ b/Runtime/BubbleLayout/BubbleLayoutPluginBase.cs
@@ -9,13 +9,13 @@ namespace XericUI.BubbleLayout
public abstract class BubbleLayoutPluginBase : MonoBehaviour, IBubbleLayoutPlugin
{
[Tooltip("是否启用此插件")]
- [SerializeField] protected bool m_Enabled = true;
+ [SerializeField] private bool m_PluginEnabled = true;
[Tooltip("执行优先级,值越大越靠后执行")]
- [SerializeField] protected int m_Order = 10;
+ [SerializeField] private int m_PluginOrder = 10;
- public bool Enabled => m_Enabled;
- public int Order => m_Order;
+ public bool Enabled => m_PluginEnabled;
+ public int Order => m_PluginOrder;
#if UNITY_EDITOR
///
@@ -25,8 +25,8 @@ namespace XericUI.BubbleLayout
protected virtual void Reset()
{
- m_Order = EditorDefaultOrder;
- m_Enabled = true;
+ m_PluginOrder = EditorDefaultOrder;
+ m_PluginEnabled = true;
}
#endif
}
diff --git a/Runtime/BubbleLayout/CircularLayoutConstraint.cs b/Runtime/BubbleLayout/CircularLayoutConstraint.cs
index 7d3823f..a3ca5b7 100644
--- a/Runtime/BubbleLayout/CircularLayoutConstraint.cs
+++ b/Runtime/BubbleLayout/CircularLayoutConstraint.cs
@@ -113,5 +113,111 @@ namespace XericUI.BubbleLayout
break;
}
}
+
+#if UNITY_EDITOR
+ private struct GizmoSlot
+ {
+ public RectTransform rectTransform;
+ public Vector2 size;
+ public int index;
+ }
+
+ private void OnDrawGizmosSelected()
+ {
+ if (!isActiveAndEnabled || transform.childCount == 0)
+ return;
+
+ var slots = new List();
+ for (int i = 0; i < transform.childCount; i++)
+ {
+ var rt = transform.GetChild(i) as RectTransform;
+ if (rt == null || !rt.gameObject.activeInHierarchy)
+ continue;
+ slots.Add(new GizmoSlot { rectTransform = rt, size = rt.rect.size, index = i });
+ }
+ if (slots.Count == 0) return;
+
+ switch (m_SortMode)
+ {
+ case LayoutSortMode.PositionX:
+ slots.Sort((a, b) => a.rectTransform.localPosition.x.CompareTo(b.rectTransform.localPosition.x));
+ break;
+ case LayoutSortMode.PositionY:
+ slots.Sort((a, b) => b.rectTransform.localPosition.y.CompareTo(a.rectTransform.localPosition.y));
+ break;
+ case LayoutSortMode.Size:
+ slots.Sort((a, b) => (b.size.x * b.size.y).CompareTo(a.size.x * a.size.y));
+ break;
+ }
+
+ var parentRT = (RectTransform)transform;
+ Vector2 center = new Vector2(
+ parentRT.rect.width * (0.5f - parentRT.pivot.x),
+ parentRT.rect.height * (0.5f - parentRT.pivot.y));
+
+ // 计算角度步长
+ float angleStep;
+ if (m_AngleMode == AngleMode.Auto)
+ {
+ angleStep = slots.Count > 1 ? 360f / slots.Count : 0f;
+ if (slots.Count > 1)
+ {
+ float minRequired = m_MinAngle;
+ for (int i = 0; i < slots.Count; i++)
+ {
+ float arc = Mathf.Atan2(slots[i].size.x * 0.5f, m_Radius) * 2f * Mathf.Rad2Deg;
+ minRequired = Mathf.Max(minRequired, arc);
+ }
+ angleStep = Mathf.Max(angleStep, minRequired);
+ }
+ }
+ else
+ {
+ angleStep = m_AngleStep;
+ }
+
+ Vector3 worldCenter = transform.TransformPoint(new Vector3(center.x, center.y, 0));
+
+ // 绘制环绕参考圆
+ Color circleColor = new Color(0.2f, 1f, 0.5f, 0.4f);
+ Gizmos.color = circleColor;
+ DrawWireCircle(worldCenter, m_Radius, 64);
+
+ // 绘制元素目标位置标记
+ Color slotColor = new Color(0.2f, 1f, 0.5f, 0.7f);
+
+ for (int i = 0; i < slots.Count; i++)
+ {
+ var slot = slots[i];
+ float angle = m_StartAngle + (m_Clockwise ? -1f : 1f) * angleStep * i;
+ float rad = angle * Mathf.Deg2Rad;
+
+ Vector2 targetLocal = center + new Vector2(Mathf.Cos(rad) * m_Radius, Mathf.Sin(rad) * m_Radius);
+ Vector3 targetWorld = transform.TransformPoint(new Vector3(targetLocal.x, targetLocal.y, 0));
+
+ // 小方框标记
+ Vector3 slotScale = transform.TransformVector(new Vector3(slot.size.x * 0.3f, slot.size.y * 0.3f, 0));
+ Gizmos.color = slotColor;
+ Gizmos.DrawWireCube(targetWorld, slotScale);
+
+ // 从圆心到目标的连线
+ Gizmos.color = circleColor;
+ Gizmos.DrawLine(worldCenter, targetWorld);
+ }
+ }
+
+ private static void DrawWireCircle(Vector3 center, float radius, int segments)
+ {
+ float anglePerSegment = 360f / segments * Mathf.Deg2Rad;
+ Vector3 prevPoint = center + new Vector3(Mathf.Cos(0) * radius, Mathf.Sin(0) * radius, 0);
+ for (int i = 1; i <= segments; i++)
+ {
+ float a = anglePerSegment * i;
+ Vector3 nextPoint = center + new Vector3(Mathf.Cos(a) * radius, Mathf.Sin(a) * radius, 0);
+ Gizmos.DrawLine(prevPoint, nextPoint);
+ prevPoint = nextPoint;
+ }
+ }
+#endif
}
}
diff --git a/Runtime/BubbleLayout/HorizontalLayoutConstraint.cs b/Runtime/BubbleLayout/HorizontalLayoutConstraint.cs
index 870ba65..2cb8149 100644
--- a/Runtime/BubbleLayout/HorizontalLayoutConstraint.cs
+++ b/Runtime/BubbleLayout/HorizontalLayoutConstraint.cs
@@ -5,78 +5,115 @@ namespace XericUI.BubbleLayout
{
///
/// 横排布局约束插件 —— 将气泡元素沿 X 轴排列,并通过弹簧力吸附到目标位置。
+ /// 支持多布局框穿插排列。
///
[AddComponentMenu("Xeric UI Vessel/Layout/Horizontal Layout Constraint", 54)]
- public class HorizontalLayoutConstraint : BubbleLayoutPluginBase, IBubbleConstraintPlugin
+ public class HorizontalLayoutConstraint : LinearLayoutConstraintBase
{
- [Header("吸附力")]
- [Tooltip("弹簧吸附力倍率,值越大吸附越快")]
- [SerializeField] private float m_AttractionForce = 50f;
-
- [Header("排列")]
- [Tooltip("排序模式")]
- [SerializeField] private LayoutSortMode m_SortMode = LayoutSortMode.HierarchyOrder;
-
- [Tooltip("元素间距")]
- [SerializeField] private float m_Spacing = 10f;
-
- [Tooltip("起始偏移(相对于布局中心左侧)")]
- [SerializeField] private float m_StartOffsetX = 0f;
-
+ [Header("横排特有")]
[Tooltip("垂直对齐方式:0=顶部, 1=居中, 2=底部")]
[Range(0f, 2f)]
[SerializeField] private float m_VerticalAlignment = 1f;
- public void ProcessConstraint(List items, Vector2 layoutCenter)
+ [Tooltip("起始水平偏移(相对布局框区域中心)")]
+ [SerializeField] private float m_StartOffsetX = 0f;
+
+ #region LinearLayoutConstraintBase 实现
+
+ ///
+ protected override string PrimaryAxisName => "X";
+
+ ///
+ protected override bool IsPrimaryHorizontal => true;
+
+ ///
+ protected override float GetPrimarySize(BubbleItemData item) => item.size.x;
+
+ ///
+ protected override float GetPrimarySizeFromRT(RectTransform rt) => rt.rect.width;
+
+ ///
+ protected override Vector2 MakeTargetPosition(float primary, float secondary)
{
- if (items == null || items.Count == 0)
- return;
-
- var sorted = new List(items);
- SortItems(sorted);
-
- // 计算总宽度
- float totalWidth = 0f;
- for (int i = 0; i < sorted.Count; i++)
- totalWidth += sorted[i].size.x + m_Spacing;
- totalWidth -= m_Spacing;
-
- float startX = layoutCenter.x + m_StartOffsetX - totalWidth * 0.5f;
-
- float currentX = startX;
- for (int i = 0; i < sorted.Count; i++)
- {
- var item = sorted[i];
-
- // 垂直对齐
- float alignOffset = (m_VerticalAlignment - 1f) * item.size.y * 0.5f;
- float targetY = layoutCenter.y - alignOffset;
-
- Vector2 targetPos = new Vector2(currentX, targetY);
-
- Vector2 springForce = (targetPos - item.position) * m_AttractionForce;
- item.force += springForce;
-
- currentX += item.size.x + m_Spacing;
- }
+ return new Vector2(primary, secondary);
}
- private void SortItems(List items)
+ ///
+ protected override float GetSecondaryAlignOffset(BubbleItemData item, LayoutBox box)
{
- switch (m_SortMode)
+ // 垂直对齐偏移:0=顶部(top), 1=居中, 2=底部(bottom)
+ // 在 localPosition 空间,Y 轴向上,所以 top = 正方向偏移
+ float offset = (1f - m_VerticalAlignment) * (box.Region.height - item.size.y) * 0.5f;
+ return offset;
+ }
+
+ #endregion
+
+ #region Editor Gizmos
+
+#if UNITY_EDITOR
+ ///
+ protected override void DrawItemTargets(List allSlots, Vector2 layoutCenter)
+ {
+ // 将 slots 按 boxIndex 分组
+ var boxSlots = new List[m_LayoutBoxes.Count];
+ for (int b = 0; b < m_LayoutBoxes.Count; b++)
+ boxSlots[b] = new List();
+
+ for (int i = 0; i < allSlots.Count; i++)
{
- case LayoutSortMode.HierarchyOrder:
- break;
- case LayoutSortMode.PositionX:
- items.Sort((a, b) => a.position.x.CompareTo(b.position.x));
- break;
- case LayoutSortMode.PositionY:
- items.Sort((a, b) => b.position.y.CompareTo(a.position.y));
- break;
- case LayoutSortMode.Size:
- items.Sort((a, b) => (b.size.x * b.size.y).CompareTo(a.size.x * a.size.y));
- break;
+ var slot = allSlots[i];
+ int bIdx = Mathf.Clamp(slot.boxIndex, 0, m_LayoutBoxes.Count - 1);
+ boxSlots[bIdx].Add(slot);
+ }
+
+ // 穿插迭代,计算每个 slot 的目标位置
+ int[] indices = new int[boxSlots.Length];
+ float[] advances = new float[boxSlots.Length];
+ for (int b = 0; b < boxSlots.Length; b++)
+ {
+ float total = 0f;
+ for (int i = 0; i < boxSlots[b].Count; i++)
+ total += boxSlots[b][i].size.x + m_Spacing;
+ if (boxSlots[b].Count > 0) total -= m_Spacing;
+ advances[b] = m_LayoutBoxes[b].Region.center.x + m_StartOffsetX - total * 0.5f;
+ }
+
+ Color fillColor = new Color(1f, 0.6f, 0.1f, 0.15f);
+ Color wireColor = new Color(1f, 0.55f, 0f, 0.7f);
+
+ bool anyRemaining = true;
+ while (anyRemaining)
+ {
+ anyRemaining = false;
+ for (int b = 0; b < boxSlots.Length; b++)
+ {
+ if (indices[b] >= boxSlots[b].Count)
+ continue;
+
+ anyRemaining = true;
+ var slot = boxSlots[b][indices[b]];
+ var box = m_LayoutBoxes[b];
+
+ float alignOffset = (1f - m_VerticalAlignment) * (box.Region.height - slot.size.y) * 0.5f;
+ float targetY = box.Region.center.y + alignOffset;
+ float targetX = advances[b];
+
+ Rect localRect = new Rect(
+ targetX - slot.size.x * 0.5f,
+ targetY - slot.size.y * 0.5f,
+ slot.size.x,
+ slot.size.y);
+
+ DrawLocalRectAsWireCube(localRect, wireColor, fillColor);
+
+ advances[b] += slot.size.x + m_Spacing;
+ indices[b]++;
+ }
}
}
+#endif
+
+ #endregion
}
}
diff --git a/Runtime/BubbleLayout/LayoutBox.cs b/Runtime/BubbleLayout/LayoutBox.cs
new file mode 100644
index 0000000..b824d8f
--- /dev/null
+++ b/Runtime/BubbleLayout/LayoutBox.cs
@@ -0,0 +1,82 @@
+using System;
+using UnityEngine;
+
+namespace XericUI.BubbleLayout
+{
+ ///
+ /// 布局框定义 —— 描述一个在父级坐标空间中的矩形区域,
+ /// 用作横向/竖向布局中特定轨道的目标区域。
+ ///
+ [Serializable]
+ public class LayoutBox
+ {
+ ///
+ /// 布局框名称(仅用于 Inspector 标识)
+ ///
+ [Tooltip("布局框名称")]
+ public string Name = "Box";
+
+ ///
+ /// 此框在父级 RectTransform localPosition 空间中的矩形区域
+ ///
+ [Tooltip("布局框的矩形区域(localPosition 空间)")]
+ public Rect Region = new Rect(-100, -100, 200, 200);
+
+ ///
+ /// 矩形区域内使用的枢轴点(0~1),用于计算子项在区域内的排列起点
+ ///
+ [Tooltip("区域内枢轴")]
+ public Vector2 Pivot = new Vector2(0.5f, 0.5f);
+
+ ///
+ /// 每行/列的最大元素数量(0=不限制),仅对穿插模式有意义
+ ///
+ [Tooltip("每轨最大元素数,0=不限制")]
+ public int MaxItemsPerTrack;
+
+ public LayoutBox() { }
+
+ public LayoutBox(string name)
+ {
+ Name = name;
+ }
+
+ public LayoutBox(string name, Rect region, Vector2 pivot = default)
+ {
+ Name = name;
+ Region = region;
+ Pivot = pivot == default ? new Vector2(0.5f, 0.5f) : pivot;
+ }
+
+ ///
+ /// 获取 Region 在 localPosition 空间中的中心点
+ ///
+ public Vector2 GetCenter()
+ {
+ return Region.center;
+ }
+
+ ///
+ /// 获取排列的起始锚点位置(根据 Pivot 在 Region 内的映射)
+ ///
+ public Vector2 GetAnchorPoint()
+ {
+ return new Vector2(
+ Region.x + Region.width * Pivot.x,
+ Region.y + Region.height * Pivot.y);
+ }
+ }
+
+ ///
+ /// 元素到布局框的序列化映射项(仅用于 Editor 持久化)
+ ///
+ [Serializable]
+ public struct LayoutBoxAssignment
+ {
+ /// 目标子项的 Key(name_instanceID)
+ public string ItemKey;
+
+ /// 分配的布局框索引
+ public int BoxIndex;
+ }
+}
diff --git a/Runtime/BubbleLayout/LayoutBox.cs.meta b/Runtime/BubbleLayout/LayoutBox.cs.meta
new file mode 100644
index 0000000..0decc95
--- /dev/null
+++ b/Runtime/BubbleLayout/LayoutBox.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 72b571aa1837cf54ba45b65983f7999f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/BubbleLayout/LinearLayoutConstraintBase.cs b/Runtime/BubbleLayout/LinearLayoutConstraintBase.cs
new file mode 100644
index 0000000..00df63b
--- /dev/null
+++ b/Runtime/BubbleLayout/LinearLayoutConstraintBase.cs
@@ -0,0 +1,531 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace XericUI.BubbleLayout
+{
+ ///
+ /// 横向 / 竖向线性布局约束插件的共用基类。
+ /// 提供布局框管理、元素分组排序、穿插迭代、弹簧吸附/瞬移、Editor Gizmo 框架等复用逻辑。
+ ///
+ public abstract class LinearLayoutConstraintBase : BubbleLayoutPluginBase, IBubbleConstraintPlugin
+ {
+ #region 字段
+
+ [Header("吸附力")]
+ [Tooltip("弹簧吸附力倍率,值越大吸附越快")]
+ [SerializeField] protected float m_AttractionForce = 50f;
+
+ [Header("排列")]
+ [Tooltip("排序模式")]
+ [SerializeField] protected LayoutSortMode m_SortMode = LayoutSortMode.HierarchyOrder;
+
+ [Tooltip("元素间距")]
+ [SerializeField] protected float m_Spacing = 10f;
+
+ [Header("布局框")]
+ [Tooltip("布局框列表(默认拥有一个全区域框)。每个框代表一个独立排列轨道,子项按框穿插排列。")]
+ [SerializeField] protected List m_LayoutBoxes = new List
+ {
+ new LayoutBox("Default", new Rect(-200, -200, 400, 400)),
+ };
+
+ ///
+ /// 元素到布局框的分配表(运行时字典的序列化后备)
+ ///
+ [SerializeField] private List m_SerializedAssignments = new List();
+
+ [NonSerialized]
+ protected Dictionary m_BoxAssignments;
+
+ #endregion
+
+ #region 属性
+
+ /// 主轴名称("X" / "Y"),用于子类在 Gizmo / 日志中标识
+ protected abstract string PrimaryAxisName { get; }
+
+ /// 获取元素沿主轴方向的尺寸
+ protected abstract float GetPrimarySize(BubbleItemData item);
+
+ ///
+ /// 获取元素沿主轴方向的尺寸(用于 Gizmo 预览的 RectTransform 版本)
+ ///
+ protected abstract float GetPrimarySizeFromRT(RectTransform rt);
+
+ ///
+ /// 根据主轴/辅轴坐标合成最终 localPosition
+ ///
+ protected abstract Vector2 MakeTargetPosition(float primary, float secondary);
+
+ #endregion
+
+ #region 布局框管理
+
+ ///
+ /// 惰性初始化运行时字典
+ ///
+ protected void EnsureAssignmentsInitialized()
+ {
+ if (m_BoxAssignments != null)
+ return;
+
+ m_BoxAssignments = new Dictionary();
+ for (int i = 0; i < m_SerializedAssignments.Count; i++)
+ {
+ var entry = m_SerializedAssignments[i];
+ if (!string.IsNullOrEmpty(entry.ItemKey))
+ m_BoxAssignments[entry.ItemKey] = entry.BoxIndex;
+ }
+ }
+
+ ///
+ /// 确保布局框列表至少有一项
+ ///
+ protected void EnsureAtLeastOneBox()
+ {
+ if (m_LayoutBoxes == null)
+ m_LayoutBoxes = new List();
+ if (m_LayoutBoxes.Count == 0)
+ m_LayoutBoxes.Add(new LayoutBox("Default", new Rect(-200, -200, 400, 400)));
+ }
+
+ ///
+ /// 将指定 GameObject 分配到指定编号的布局框。
+ /// 默认所有子项属于第 0 号框。
+ ///
+ /// 布局框索引(从 0 开始),超出范围会被钳制
+ /// 要分配的子项 GameObject
+ public void MarkObject2LayoutBox(int boxIndex, GameObject target)
+ {
+ if (target == null)
+ return;
+
+ EnsureAtLeastOneBox();
+ boxIndex = Mathf.Clamp(boxIndex, 0, m_LayoutBoxes.Count - 1);
+ EnsureAssignmentsInitialized();
+
+ string key = $"{target.name}_{target.GetInstanceID()}";
+ m_BoxAssignments[key] = boxIndex;
+
+ // 同步回序列化列表
+ SyncDictionaryToSerialized();
+ }
+
+ ///
+ /// 清除指定 GameObject 的布局框分配(恢复默认框 0)
+ ///
+ public void ClearBoxAssignment(GameObject target)
+ {
+ if (target == null)
+ return;
+
+ EnsureAssignmentsInitialized();
+ string key = $"{target.name}_{target.GetInstanceID()}";
+ m_BoxAssignments.Remove(key);
+ SyncDictionaryToSerialized();
+ }
+
+ ///
+ /// 根据屏幕空间坐标和指定 Pivot,将布局框的 Region 映射到屏幕对应位置。
+ /// 适用于 Canvas ScreenSpace-Overlay / Camera 模式。
+ ///
+ /// 目标布局框索引
+ /// 屏幕空间矩形(像素坐标)
+ /// 渲染 Canvas 的相机,ScreenSpace-Overlay 时传 null
+ public void SetBoxRegionFromScreenSpace(int boxIndex, Rect screenRect, Camera camera = null)
+ {
+ EnsureAtLeastOneBox();
+ if (boxIndex < 0 || boxIndex >= m_LayoutBoxes.Count)
+ return;
+
+ var parentRT = transform as RectTransform;
+ if (parentRT == null)
+ return;
+
+ Vector2 localCenter;
+ RectTransformUtility.ScreenPointToLocalPointInRectangle(
+ parentRT, screenRect.center, camera, out localCenter);
+
+ Vector2 localSize = screenRect.size / parentRT.lossyScale;
+
+ m_LayoutBoxes[boxIndex].Region = new Rect(
+ localCenter.x - localSize.x * 0.5f,
+ localCenter.y - localSize.y * 0.5f,
+ localSize.x,
+ localSize.y);
+ }
+
+ ///
+ /// 获取指定 GameObject 当前分配的布局框索引
+ ///
+ public int GetBoxIndex(GameObject target)
+ {
+ if (target == null)
+ return 0;
+
+ EnsureAssignmentsInitialized();
+ string key = $"{target.name}_{target.GetInstanceID()}";
+ return m_BoxAssignments.TryGetValue(key, out int idx) ? idx : 0;
+ }
+
+ ///
+ /// 获取所有布局框(只读)
+ ///
+ public IReadOnlyList GetLayoutBoxes()
+ {
+ EnsureAtLeastOneBox();
+ return m_LayoutBoxes;
+ }
+
+ private void SyncDictionaryToSerialized()
+ {
+ m_SerializedAssignments.Clear();
+ if (m_BoxAssignments == null)
+ return;
+
+ foreach (var kv in m_BoxAssignments)
+ {
+ m_SerializedAssignments.Add(new LayoutBoxAssignment
+ {
+ ItemKey = kv.Key,
+ BoxIndex = kv.Value,
+ });
+ }
+ }
+
+ #endregion
+
+ #region IBubbleConstraintPlugin 实现
+
+ public virtual void ProcessConstraint(List items, Vector2 layoutCenter)
+ {
+ if (items == null || items.Count == 0)
+ return;
+
+ EnsureAtLeastOneBox();
+ EnsureAssignmentsInitialized();
+
+ // 1. 将元素按布局框分组
+ var boxGroups = new List[m_LayoutBoxes.Count];
+ for (int b = 0; b < m_LayoutBoxes.Count; b++)
+ boxGroups[b] = new List();
+
+ for (int i = 0; i < items.Count; i++)
+ {
+ var item = items[i];
+ int boxIdx = 0;
+ if (item.rectTransform != null)
+ {
+ string key = $"{item.rectTransform.name}_{item.rectTransform.GetInstanceID()}";
+ if (m_BoxAssignments.TryGetValue(key, out int assigned))
+ boxIdx = Mathf.Clamp(assigned, 0, m_LayoutBoxes.Count - 1);
+ }
+ boxGroups[boxIdx].Add(item);
+ }
+
+ // 2. 各组内部排序
+ for (int b = 0; b < boxGroups.Length; b++)
+ SortItems(boxGroups[b]);
+
+ // 3. 计算各组总主轴尺寸
+ float[] boxTotalPrimarySize = new float[boxGroups.Length];
+ float[] boxAdvance = new float[boxGroups.Length];
+ for (int b = 0; b < boxGroups.Length; b++)
+ {
+ float total = 0f;
+ for (int i = 0; i < boxGroups[b].Count; i++)
+ total += GetPrimarySize(boxGroups[b][i]) + m_Spacing;
+ if (boxGroups[b].Count > 0)
+ total -= m_Spacing;
+ boxTotalPrimarySize[b] = total;
+ }
+
+ // 4. 计算各框的起始位置(主轴方向),位于 Region 中心偏前
+ for (int b = 0; b < boxGroups.Length; b++)
+ {
+ var region = m_LayoutBoxes[b].Region;
+ float primaryCenter = IsPrimaryHorizontal
+ ? region.center.x
+ : region.center.y;
+
+ boxAdvance[b] = primaryCenter - boxTotalPrimarySize[b] * 0.5f;
+ }
+
+ // 5. 穿插迭代:按轨轮询各框
+ int[] boxIndices = new int[boxGroups.Length];
+ bool anyRemaining = true;
+ var targetPositions = new List<(BubbleItemData item, int boxIndex, Vector2 targetPos)>();
+
+ while (anyRemaining)
+ {
+ anyRemaining = false;
+ for (int b = 0; b < boxGroups.Length; b++)
+ {
+ if (boxIndices[b] >= boxGroups[b].Count)
+ continue;
+
+ anyRemaining = true;
+ var item = boxGroups[b][boxIndices[b]];
+ var box = m_LayoutBoxes[b];
+
+ // 计算辅轴位置(box 区域中心 ± 对齐偏移)
+ float secondary = IsPrimaryHorizontal
+ ? box.Region.center.y + GetSecondaryAlignOffset(item, box)
+ : box.Region.center.x + GetSecondaryAlignOffset(item, box);
+
+ Vector2 targetPos = MakeTargetPosition(boxAdvance[b], secondary);
+
+ // 确保目标在 box 区域内
+ targetPos = ClampToBox(targetPos, item, box);
+
+ targetPositions.Add((item, b, targetPos));
+
+ // 推进主轴位置
+ boxAdvance[b] += GetPrimarySize(item) + m_Spacing;
+ boxIndices[b]++;
+ }
+ }
+
+ // 6. 按计算顺序依次施加力(确保穿插顺序被物理系统感知)
+ for (int i = 0; i < targetPositions.Count; i++)
+ {
+ var (item, boxIndex, targetPos) = targetPositions[i];
+ var box = m_LayoutBoxes[boxIndex];
+
+ // 检查是否超出 box 允许范围,超出则瞬移
+ if (IsOutOfBox(item.position, item, box))
+ {
+ item.position = targetPos;
+ item.velocity = Vector2.zero;
+ }
+ else
+ {
+ Vector2 springForce = (targetPos - item.position) * m_AttractionForce;
+ item.force += springForce;
+ }
+ }
+ }
+
+ ///
+ /// 判断主轴是水平还是垂直
+ ///
+ protected abstract bool IsPrimaryHorizontal { get; }
+
+ ///
+ /// 子类提供辅轴对齐偏移量(如横排的垂直对齐、竖排的拉链偏移)
+ ///
+ /// 偏移量(正值 = 远离区域左上角方向)
+ protected virtual float GetSecondaryAlignOffset(BubbleItemData item, LayoutBox box)
+ {
+ return 0f;
+ }
+
+ ///
+ /// 将目标位置钳制到布局框区域内(考虑元素自身尺寸)
+ ///
+ protected Vector2 ClampToBox(Vector2 target, BubbleItemData item, LayoutBox box)
+ {
+ Rect r = box.Region;
+ float halfW = item.size.x * 0.5f;
+ float halfH = item.size.y * 0.5f;
+
+ target.x = Mathf.Clamp(target.x, r.xMin + halfW, r.xMax - halfW);
+ target.y = Mathf.Clamp(target.y, r.yMin + halfH, r.yMax - halfH);
+ return target;
+ }
+
+ ///
+ /// 判断位置是否超出布局框(用于瞬移判断)
+ ///
+ protected bool IsOutOfBox(Vector2 pos, BubbleItemData item, LayoutBox box)
+ {
+ Rect r = box.Region;
+ float halfW = item.size.x * 0.5f;
+ float halfH = item.size.y * 0.5f;
+
+ return pos.x - halfW < r.xMin || pos.x + halfW > r.xMax
+ || pos.y - halfH < r.yMin || pos.y + halfH > r.yMax;
+ }
+
+ ///
+ /// 排序元素列表
+ ///
+ protected void SortItems(List items)
+ {
+ if (items.Count <= 1)
+ return;
+
+ switch (m_SortMode)
+ {
+ case LayoutSortMode.HierarchyOrder:
+ break;
+ case LayoutSortMode.PositionX:
+ items.Sort((a, b) => a.position.x.CompareTo(b.position.x));
+ break;
+ case LayoutSortMode.PositionY:
+ items.Sort((a, b) => b.position.y.CompareTo(a.position.y));
+ break;
+ case LayoutSortMode.Size:
+ items.Sort((a, b) => (b.size.x * b.size.y).CompareTo(a.size.x * a.size.y));
+ break;
+ }
+ }
+
+ #endregion
+
+ #region Editor Gizmos
+
+#if UNITY_EDITOR
+ ///
+ /// Gizmo 预览数据结构
+ ///
+ protected struct GizmoSlot
+ {
+ public RectTransform rectTransform;
+ public Vector2 size;
+ public int index;
+ public int boxIndex;
+ }
+
+ protected virtual void OnDrawGizmosSelected()
+ {
+ if (!isActiveAndEnabled || transform.childCount == 0)
+ return;
+
+ // 收集子元素
+ var allSlots = new List();
+ for (int i = 0; i < transform.childCount; i++)
+ {
+ var rt = transform.GetChild(i) as RectTransform;
+ if (rt == null || !rt.gameObject.activeInHierarchy)
+ continue;
+ allSlots.Add(new GizmoSlot { rectTransform = rt, size = rt.rect.size, index = i, boxIndex = 0 });
+ }
+ if (allSlots.Count == 0) return;
+
+ // 读回分配
+ EnsureAssignmentsInitialized();
+ for (int i = 0; i < allSlots.Count; i++)
+ {
+ var slot = allSlots[i];
+ string key = $"{slot.rectTransform.name}_{slot.rectTransform.GetInstanceID()}";
+ if (m_BoxAssignments.TryGetValue(key, out int bIdx))
+ {
+ slot.boxIndex = Mathf.Clamp(bIdx, 0, m_LayoutBoxes.Count - 1);
+ allSlots[i] = slot;
+ }
+ }
+
+ // 按当前排序模式排序
+ switch (m_SortMode)
+ {
+ case LayoutSortMode.PositionX:
+ allSlots.Sort((a, b) => a.rectTransform.localPosition.x.CompareTo(b.rectTransform.localPosition.x));
+ break;
+ case LayoutSortMode.PositionY:
+ allSlots.Sort((a, b) => b.rectTransform.localPosition.y.CompareTo(a.rectTransform.localPosition.y));
+ break;
+ case LayoutSortMode.Size:
+ allSlots.Sort((a, b) => (b.size.x * b.size.y).CompareTo(a.size.x * a.size.y));
+ break;
+ }
+
+ var parentRT = (RectTransform)transform;
+ Vector2 layoutCenter = new Vector2(
+ parentRT.rect.width * (0.5f - parentRT.pivot.x),
+ parentRT.rect.height * (0.5f - parentRT.pivot.y));
+
+ // 1. 绘制所有布局框区域
+ DrawBoxRegions();
+
+ // 2. 委托子类绘制元素目标位置
+ DrawItemTargets(allSlots, layoutCenter);
+ }
+
+ ///
+ /// 绘制所有布局框的矩形区域(白色虚线框)
+ ///
+ protected virtual void DrawBoxRegions()
+ {
+ Color[] boxColors =
+ {
+ new Color(0.5f, 0.5f, 1f, 0.35f), // 蓝色调
+ new Color(1f, 0.5f, 0.5f, 0.35f), // 红色调
+ new Color(0.5f, 1f, 0.5f, 0.35f), // 绿色调
+ new Color(1f, 1f, 0.5f, 0.35f), // 黄色调
+ new Color(0.5f, 1f, 1f, 0.35f), // 青色调
+ new Color(1f, 0.5f, 1f, 0.35f), // 紫色调
+ };
+
+ for (int b = 0; b < m_LayoutBoxes.Count; b++)
+ {
+ var box = m_LayoutBoxes[b];
+ Color c = boxColors[b % boxColors.Length];
+
+ Rect r = box.Region;
+ Vector3 centerWorld = transform.TransformPoint(new Vector3(r.center.x, r.center.y, 0));
+ Vector3 sizeWorld = transform.TransformVector(new Vector3(r.width, r.height, 0));
+
+ // 半透明填充
+ Gizmos.color = new Color(c.r, c.g, c.b, 0.08f);
+ Gizmos.DrawCube(centerWorld, sizeWorld);
+
+ // 虚线边框
+ Gizmos.color = c;
+ DrawWireRect(centerWorld, sizeWorld);
+
+ // 中心十字
+ float crossLen = Mathf.Min(r.width, r.height) * 0.08f;
+ Vector3 cxWorld = transform.TransformVector(new Vector3(crossLen, 0, 0));
+ Vector3 cyWorld = transform.TransformVector(new Vector3(0, crossLen, 0));
+ Gizmos.DrawLine(centerWorld - cxWorld, centerWorld + cxWorld);
+ Gizmos.DrawLine(centerWorld - cyWorld, centerWorld + cyWorld);
+ }
+ }
+
+ ///
+ /// 子类重写:根据已排序的 slots 和 layoutCenter 绘制各元素的目标位置预览
+ ///
+ protected abstract void DrawItemTargets(List allSlots, Vector2 layoutCenter);
+
+ ///
+ /// 绘制一个线框矩形
+ ///
+ protected static void DrawWireRect(Vector3 center, Vector3 size)
+ {
+ Vector3 half = size * 0.5f;
+ Vector3[] corners = new Vector3[5]
+ {
+ center + new Vector3(-half.x, -half.y, 0),
+ center + new Vector3( half.x, -half.y, 0),
+ center + new Vector3( half.x, half.y, 0),
+ center + new Vector3(-half.x, half.y, 0),
+ center + new Vector3(-half.x, -half.y, 0),
+ };
+ for (int i = 0; i < 4; i++)
+ Gizmos.DrawLine(corners[i], corners[i + 1]);
+ }
+
+ ///
+ /// 将 localPosition 空间的 Rect 绘制为世界空间线框
+ ///
+ protected void DrawLocalRectAsWireCube(Rect localRect, Color wireColor, Color? fillColor = null)
+ {
+ Vector3 worldCenter = transform.TransformPoint(new Vector3(localRect.center.x, localRect.center.y, 0));
+ Vector3 worldSize = transform.TransformVector(new Vector3(localRect.width, localRect.height, 0));
+
+ if (fillColor.HasValue)
+ {
+ Gizmos.color = fillColor.Value;
+ Gizmos.DrawCube(worldCenter, worldSize);
+ }
+
+ Gizmos.color = wireColor;
+ Gizmos.DrawWireCube(worldCenter, worldSize);
+ }
+#endif
+
+ #endregion
+ }
+}
diff --git a/Runtime/BubbleLayout/LinearLayoutConstraintBase.cs.meta b/Runtime/BubbleLayout/LinearLayoutConstraintBase.cs.meta
new file mode 100644
index 0000000..f8caa61
--- /dev/null
+++ b/Runtime/BubbleLayout/LinearLayoutConstraintBase.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0158a0d6480014d46ac381596bfe78fe
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/BubbleLayout/QuadTree.cs b/Runtime/BubbleLayout/QuadTree.cs
new file mode 100644
index 0000000..94ac665
--- /dev/null
+++ b/Runtime/BubbleLayout/QuadTree.cs
@@ -0,0 +1,451 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+using UnityEngine;
+#if Unity_Runtime
+namespace XericLibrary.Runtime.Type.SpatialAlgorithm
+{
+ ///
+ /// 灵活的四叉树实现,支持多种坐标类型
+ ///
+ public class QuadTree
+ {
+ #region 字段属性
+
+ private int maxObjectsPerNode = 5;
+ private int maxDepth = 4;
+
+ private Node root;
+ private Func getRectFunc; // 获取对象边界的委托
+
+ ///
+ /// 获取此四叉树的范围
+ ///
+ public Rect RootRect => root != null ? root.boundary : Rect.zero;
+
+ public int Count { get; private set; }
+
+ ///
+ /// 清空四叉树,保留边界和配置以便重用
+ ///
+ public void Clear()
+ {
+ if (root != null)
+ {
+ root.ClearAll();
+ }
+ Count = 0;
+ }
+
+ ///
+ /// 重建四叉树,使用相同边界和配置
+ ///
+ public void Rebuild(IEnumerable items)
+ {
+ Clear();
+ foreach (var item in items)
+ {
+ Insert(item);
+ }
+ }
+
+ ///
+ /// 获取四叉树中所有元素
+ ///
+ public void GetAllItems(HashSet result)
+ {
+ if (result == null)
+ return;
+ root?.CollectAllItems(result);
+ }
+
+ #endregion
+
+ #region 构造函数
+
+ ///
+ /// 初始化四叉树
+ ///
+ /// 四叉树边界
+ /// 获取对象边界的函数
+ public QuadTree(Rect boundary, Func getRectFunc, int maxObjectsPerNode = 5, int maxDepth = 4)
+ {
+ this.maxObjectsPerNode = Math.Max(1, maxObjectsPerNode);
+ this.maxDepth = Math.Max(1, maxDepth);
+ this.root = new Node(boundary, 0, getRectFunc, this);
+ this.getRectFunc = getRectFunc ?? throw new ArgumentNullException(nameof(getRectFunc));
+ }
+
+ #endregion
+
+ #region 公共方法
+
+ ///
+ /// 插入元素
+ ///
+ public bool Insert(T item)
+ {
+ return root.Insert(item);
+ }
+
+ ///
+ /// 查找范围内的元素
+ ///
+ /// 查找范围
+ /// 建议使用数据容器对象池控制,避免频繁GC,默认空将自动创建新的数据容器
+ ///
+ public HashSet Retrieve(Rect area, HashSet result = null)
+ {
+ return root.Retrieve(area, result);
+ }
+
+ ///
+ /// 查找最近的元素
+ ///
+ public T FindNearest(Vector2 target, float maxDistance = float.MaxValue)
+ {
+ T nearest = default;
+ float minDistanceSquared = maxDistance * maxDistance;
+ root.FindNearest(target, ref nearest, ref minDistanceSquared);
+ return nearest;
+ }
+
+ ///
+ /// 调试绘制网格
+ ///
+ public void DrawGrid()
+ {
+ root.DrawGrid(Color.white);
+ }
+
+ #endregion
+
+ #region 节点类
+
+ private class Node : IEnumerable
+ {
+ internal Rect boundary;
+ internal HashSet objects = new HashSet();
+ internal Node[] children;
+ internal readonly int depth;
+ internal readonly Func getRectFunc;
+ internal readonly QuadTree parent;
+
+ public Node(Rect boundary, int depth, Func getRectFunc, QuadTree parent)
+ {
+ this.boundary = boundary;
+ this.depth = depth;
+ this.getRectFunc = getRectFunc;
+ this.parent = parent;
+ this.children = null;
+ }
+
+ ///
+ /// 插入元素
+ ///
+ public bool Insert(T item)
+ {
+ Rect itemRect = getRectFunc(item);
+
+ // 如果元素与当前节点不重叠,无法插入
+ if (!IsOverlapping(boundary, itemRect))
+ return false;
+
+ // 如果是叶子节点且未达容量上限
+ if (objects != null && (objects.Count < parent.maxObjectsPerNode || depth >= parent.maxDepth))
+ {
+ objects.Add(item);
+ parent.Count++;
+ return true;
+ }
+
+ // 分裂节点
+ if (children == null)
+ Split();
+
+ // 插入到子节点
+ bool inserted = false;
+ foreach (var child in children)
+ {
+ if (child.Insert(item))
+ inserted = true;
+ }
+
+ return inserted;
+ }
+
+ ///
+ /// 分裂为四个子节点
+ ///
+ private void Split()
+ {
+ float halfWidth = boundary.width / 2;
+ float halfHeight = boundary.height / 2;
+ float midX = boundary.x + halfWidth;
+ float midY = boundary.y + halfHeight;
+
+ children = new Node[4];
+ children[0] = new Node(new Rect(boundary.x, boundary.y, halfWidth, halfHeight), depth + 1, getRectFunc, parent); // 左上
+ children[1] = new Node(new Rect(midX, boundary.y, halfWidth, halfHeight), depth + 1, getRectFunc, parent); // 右上
+ children[2] = new Node(new Rect(boundary.x, midY, halfWidth, halfHeight), depth + 1, getRectFunc, parent); // 左下
+ children[3] = new Node(new Rect(midX, midY, halfWidth, halfHeight), depth + 1, getRectFunc, parent); // 右下
+
+ // 移动当前节点的对象到子节点
+ var tempObjects = new List(objects);
+ objects.Clear();
+ objects = null; // 标记为非叶子节点
+
+ foreach (var obj in tempObjects)
+ {
+ Insert(obj);
+ }
+ }
+
+ ///
+ /// 查找最近的元素
+ ///
+ public void FindNearest(Vector2 target, ref T nearest, ref float minDistanceSquared)
+ {
+ // 如果当前节点距离大于已知最小距离,直接返回
+ float distanceToBoundary = GetDistanceToRectSquared(target, boundary);
+ if (distanceToBoundary >= minDistanceSquared)
+ return;
+
+ // 检查当前节点中的对象
+ if (objects != null)
+ {
+ foreach (var obj in objects)
+ {
+ Rect objRect = getRectFunc(obj);
+ float distanceSquared = GetDistanceToRectSquared(target, objRect);
+
+ if (distanceSquared < minDistanceSquared)
+ {
+ minDistanceSquared = distanceSquared;
+ nearest = obj;
+ }
+ }
+ }
+
+ // 递归检查子节点
+ if (children != null)
+ {
+ // 按距离排序子节点,优先检查更近的
+ var childDistances = new List>();
+ foreach (var child in children)
+ {
+ float dist = child.GetDistanceToRectSquared(target, child.boundary);
+ childDistances.Add(Tuple.Create(child, dist));
+ }
+
+ // 按距离升序排序
+ childDistances.Sort((a, b) => a.Item2.CompareTo(b.Item2));
+
+ foreach (var tuple in childDistances)
+ {
+ if (tuple.Item2 < minDistanceSquared)
+ {
+ tuple.Item1.FindNearest(target, ref nearest, ref minDistanceSquared);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 查找范围内的元素
+ ///
+ public HashSet Retrieve(Rect area, HashSet result)
+ {
+ if (result == null)
+ result = new HashSet();
+
+ // 如果与当前节点不重叠,返回空集
+ if (!IsOverlapping(boundary, area))
+ return result;
+
+ // 检查当前节点中的对象
+ if (objects != null)
+ {
+ foreach (var obj in objects)
+ {
+ if (IsOverlapping(getRectFunc(obj), area))
+ {
+ result.Add(obj);
+ }
+ }
+ }
+
+ // 递归查询子节点
+ if (children != null)
+ {
+ foreach (var child in children)
+ {
+ var childResults = child.Retrieve(area, result);
+ foreach (var item in childResults)
+ {
+ result.Add(item);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// 绘制节点网格
+ ///
+ public void DrawGrid(Color color)
+ {
+ Debug.DrawLine(new Vector2(boundary.x, boundary.y), new Vector2(boundary.xMax, boundary.y), color);
+ Debug.DrawLine(new Vector2(boundary.x, boundary.y), new Vector2(boundary.x, boundary.yMax), color);
+ Debug.DrawLine(new Vector2(boundary.xMax, boundary.y), new Vector2(boundary.xMax, boundary.yMax), color);
+ Debug.DrawLine(new Vector2(boundary.x, boundary.yMax), new Vector2(boundary.xMax, boundary.yMax), color);
+
+ if (children != null)
+ {
+ foreach (var child in children)
+ {
+ child.DrawGrid(color);
+ }
+ }
+ }
+
+ #region 辅助方法
+
+ ///
+ /// 判断两个矩形是否重叠
+ ///
+ private bool IsOverlapping(Rect a, Rect b)
+ {
+ return a.x < b.xMax && a.xMax > b.x &&
+ a.y < b.yMax && a.yMax > b.y;
+ }
+
+ ///
+ /// 计算点到矩形的最小距离平方
+ ///
+ private float GetDistanceToRectSquared(Vector2 point, Rect rect)
+ {
+ float dx = Mathf.Max(rect.x - point.x, point.x - rect.xMax, 0);
+ float dy = Mathf.Max(rect.y - point.y, point.y - rect.yMax, 0);
+ return dx * dx + dy * dy;
+ }
+
+ ///
+ /// 递归清空所有节点
+ ///
+ 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();
+ }
+
+ ///
+ /// 递归收集所有元素
+ ///
+ public void CollectAllItems(HashSet result)
+ {
+ if (objects != null)
+ {
+ foreach (var obj in objects)
+ {
+ result.Add(obj);
+ }
+ return;
+ }
+
+ if (children != null)
+ {
+ for (int i = 0; i < children.Length; i++)
+ {
+ children[i]?.CollectAllItems(result);
+ }
+ }
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ if (objects != null)
+ yield return this;
+ else
+ foreach (var child in children)
+ foreach (var node in child)
+ yield return node;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ #endregion
+ }
+
+ #endregion
+ }
+
+ ///
+ /// 四叉树扩展方法,提供不同类型的矩形转换
+ ///
+ public static class QuadTreeExtensions
+ {
+ // Vector2 扩展 - 视为点
+ public static Rect ToQuadTreeRect(this Vector2 vector)
+ {
+ return new Rect(vector, Vector2.zero);
+ }
+
+ // Vector3 扩展 - 使用X和Y坐标,视为点
+ public static Rect ToQuadTreeRect(this Vector3 vector)
+ {
+ return new Rect(vector.x, vector.y, 0, 0);
+ }
+
+ // Rect 扩展 - 直接使用自身作为边界
+ public static Rect ToQuadTreeRect(this Rect rect)
+ {
+ return rect;
+ }
+
+ // RectTransform 扩展 - 使用Rect范围
+ public static Rect ToQuadTreeRect(this RectTransform transform)
+ {
+ return transform.rect;
+ }
+
+ // Transform 扩展 - 使用位置的X和Y坐标,视为点
+ public static Rect ToQuadTreeRect(this Transform transform)
+ {
+ Vector3 pos = transform.position;
+ return new Rect(pos.x, pos.y, 0, 0);
+ }
+
+ // 简化四叉树创建的扩展方法
+ public static QuadTree CreateQuadTree(this IEnumerable collection, Rect boundary, Func getRectFunc)
+ {
+ var quadTree = new QuadTree(boundary, getRectFunc);
+ foreach (var item in collection)
+ {
+ quadTree.Insert(item);
+ }
+ return quadTree;
+ }
+ }
+
+}
+#endif
\ No newline at end of file
diff --git a/Runtime/BubbleLayout/QuadTree.cs.meta b/Runtime/BubbleLayout/QuadTree.cs.meta
new file mode 100644
index 0000000..0b9361b
--- /dev/null
+++ b/Runtime/BubbleLayout/QuadTree.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: eac931867b3f4645a5c0e454b9deb626
+timeCreated: 1781071846
\ No newline at end of file
diff --git a/Runtime/BubbleLayout/VerticalLayoutConstraint.cs b/Runtime/BubbleLayout/VerticalLayoutConstraint.cs
index 07a2898..645f40e 100644
--- a/Runtime/BubbleLayout/VerticalLayoutConstraint.cs
+++ b/Runtime/BubbleLayout/VerticalLayoutConstraint.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using UnityEngine;
@@ -24,89 +23,223 @@ namespace XericUI.BubbleLayout
///
/// 竖排布局约束插件 —— 将气泡元素沿 Y 轴排列,并通过弹簧力吸附到目标位置。
+ /// 支持多布局框穿插排列和拉链模式(一左一右交替)。
///
[AddComponentMenu("Xeric UI Vessel/Layout/Vertical Layout Constraint", 53)]
- public class VerticalLayoutConstraint : BubbleLayoutPluginBase, IBubbleConstraintPlugin
+ public class VerticalLayoutConstraint : LinearLayoutConstraintBase
{
- [Header("吸附力")]
- [Tooltip("弹簧吸附力倍率,值越大吸附越快")]
- [SerializeField] private float m_AttractionForce = 50f;
-
- [Header("排列")]
- [Tooltip("排序模式")]
- [SerializeField] private LayoutSortMode m_SortMode = LayoutSortMode.HierarchyOrder;
-
- [Tooltip("元素间距")]
- [SerializeField] private float m_Spacing = 10f;
-
- [Tooltip("起始偏移(相对于布局中心上方)")]
+ [Header("竖排特有")]
+ [Tooltip("起始竖直偏移(相对布局框区域中心)")]
[SerializeField] private float m_StartOffsetY = 0f;
[Header("拉链模式")]
- [Tooltip("启用拉链排列(偶数索引偏左,奇数索引偏右)")]
+ [Tooltip("启用拉链排列(偶数索引偏左,奇数索引偏右,仅对默认单框布局有效)")]
[SerializeField] private bool m_ZipperMode = false;
[Tooltip("拉链模式下左右间距")]
[SerializeField] private float m_ZipperGap = 20f;
- public void ProcessConstraint(List items, Vector2 layoutCenter)
+ #region LinearLayoutConstraintBase 实现
+
+ ///
+ protected override string PrimaryAxisName => "Y";
+
+ ///
+ protected override bool IsPrimaryHorizontal => false;
+
+ ///
+ protected override float GetPrimarySize(BubbleItemData item) => item.size.y;
+
+ ///
+ protected override float GetPrimarySizeFromRT(RectTransform rt) => rt.rect.height;
+
+ ///
+ protected override Vector2 MakeTargetPosition(float primary, float secondary)
+ {
+ return new Vector2(secondary, primary);
+ }
+
+ ///
+ protected override float GetSecondaryAlignOffset(BubbleItemData item, LayoutBox box)
+ {
+ // 拉链偏移由外部交叉索引决定,这里返回 0
+ return 0f;
+ }
+
+ #endregion
+
+ #region 重写 ProcessConstraint 以支持拉链模式
+
+ ///
+ public override void ProcessConstraint(List items, Vector2 layoutCenter)
{
if (items == null || items.Count == 0)
return;
- // 排序
- var sorted = new List(items);
- SortItems(sorted);
+ EnsureAtLeastOneBox();
+ EnsureAssignmentsInitialized();
- // 计算起始 Y
- float totalHeight = 0f;
- for (int i = 0; i < sorted.Count; i++)
- totalHeight += sorted[i].size.y + m_Spacing;
- totalHeight -= m_Spacing; // 去掉最后一个间距
+ // 分组
+ var boxGroups = new List[m_LayoutBoxes.Count];
+ for (int b = 0; b < m_LayoutBoxes.Count; b++)
+ boxGroups[b] = new List();
- float startY = layoutCenter.y + m_StartOffsetY + totalHeight * 0.5f;
-
- // 逐元素设置目标位置并施加弹簧力
- float currentY = startY;
- for (int i = 0; i < sorted.Count; i++)
+ for (int i = 0; i < items.Count; i++)
{
- var item = sorted[i];
- float targetX = layoutCenter.x;
- float targetY = currentY;
-
- // 拉链模式:交替左右偏移
- if (m_ZipperMode)
+ var item = items[i];
+ int boxIdx = 0;
+ if (item.rectTransform != null)
{
- targetX = layoutCenter.x + (i % 2 == 0 ? -m_ZipperGap : m_ZipperGap) * 0.5f;
+ string key = $"{item.rectTransform.name}_{item.rectTransform.GetInstanceID()}";
+ if (m_BoxAssignments.TryGetValue(key, out int assigned))
+ boxIdx = Mathf.Clamp(assigned, 0, m_LayoutBoxes.Count - 1);
}
-
- Vector2 targetPos = new Vector2(targetX, targetY);
-
- // 弹簧力:F = (target - current) * forceMultiplier
- Vector2 springForce = (targetPos - item.position) * m_AttractionForce;
- item.force += springForce;
-
- currentY -= item.size.y + m_Spacing;
+ boxGroups[boxIdx].Add(item);
}
- }
- private void SortItems(List items)
- {
- switch (m_SortMode)
+ // 排序各组
+ for (int b = 0; b < boxGroups.Length; b++)
+ SortItems(boxGroups[b]);
+
+ // 计算各组总主轴尺寸及起始位置
+ float[] boxTotalPrimary = new float[boxGroups.Length];
+ float[] boxAdvance = new float[boxGroups.Length];
+ for (int b = 0; b < boxGroups.Length; b++)
{
- case LayoutSortMode.HierarchyOrder:
- // 保持原始顺序(即 rectChildren 的顺序)
- break;
- case LayoutSortMode.PositionX:
- items.Sort((a, b) => a.position.x.CompareTo(b.position.x));
- break;
- case LayoutSortMode.PositionY:
- items.Sort((a, b) => b.position.y.CompareTo(a.position.y)); // Y 从大到小(上到下)
- break;
- case LayoutSortMode.Size:
- items.Sort((a, b) => (b.size.x * b.size.y).CompareTo(a.size.x * a.size.y));
- break;
+ float total = 0f;
+ for (int i = 0; i < boxGroups[b].Count; i++)
+ total += boxGroups[b][i].size.y + m_Spacing;
+ if (boxGroups[b].Count > 0) total -= m_Spacing;
+ boxTotalPrimary[b] = total;
+ boxAdvance[b] = m_LayoutBoxes[b].Region.center.y + m_StartOffsetY + total * 0.5f;
+ }
+
+ // 穿插迭代
+ int[] boxIndices = new int[boxGroups.Length];
+ int globalIndex = 0;
+ bool anyRemaining = true;
+ var targetList = new List<(BubbleItemData item, int boxIndex, Vector2 targetPos)>();
+
+ while (anyRemaining)
+ {
+ anyRemaining = false;
+ for (int b = 0; b < boxGroups.Length; b++)
+ {
+ if (boxIndices[b] >= boxGroups[b].Count)
+ continue;
+
+ anyRemaining = true;
+ var item = boxGroups[b][boxIndices[b]];
+ var box = m_LayoutBoxes[b];
+
+ // 辅轴位置:拉链模式下的 X 偏移
+ float secondaryX = box.Region.center.x;
+ if (m_ZipperMode)
+ {
+ secondaryX += (globalIndex % 2 == 0 ? -m_ZipperGap : m_ZipperGap) * 0.5f;
+ }
+
+ // 主轴位置推进:从上往下(Y 减小)
+ float targetY = boxAdvance[b];
+ Vector2 targetPos = new Vector2(secondaryX, targetY);
+ targetPos = ClampToBox(targetPos, item, box);
+
+ targetList.Add((item, b, targetPos));
+
+ boxAdvance[b] -= item.size.y + m_Spacing;
+ boxIndices[b]++;
+ globalIndex++;
+ }
+ }
+
+ // 施加力 / 瞬移
+ for (int i = 0; i < targetList.Count; i++)
+ {
+ var (item, boxIndex, targetPos) = targetList[i];
+ var box = m_LayoutBoxes[boxIndex];
+
+ if (IsOutOfBox(item.position, item, box))
+ {
+ item.position = targetPos;
+ item.velocity = Vector2.zero;
+ }
+ else
+ {
+ Vector2 springForce = (targetPos - item.position) * m_AttractionForce;
+ item.force += springForce;
+ }
}
}
+
+ #endregion
+
+ #region Editor Gizmos
+
+#if UNITY_EDITOR
+ ///
+ protected override void DrawItemTargets(List allSlots, Vector2 layoutCenter)
+ {
+ var boxSlots = new List[m_LayoutBoxes.Count];
+ for (int b = 0; b < m_LayoutBoxes.Count; b++)
+ boxSlots[b] = new List();
+
+ for (int i = 0; i < allSlots.Count; i++)
+ {
+ var slot = allSlots[i];
+ int bIdx = Mathf.Clamp(slot.boxIndex, 0, m_LayoutBoxes.Count - 1);
+ boxSlots[bIdx].Add(slot);
+ }
+
+ int[] indices = new int[boxSlots.Length];
+ float[] advances = new float[boxSlots.Length];
+ for (int b = 0; b < boxSlots.Length; b++)
+ {
+ float total = 0f;
+ for (int i = 0; i < boxSlots[b].Count; i++)
+ total += boxSlots[b][i].size.y + m_Spacing;
+ if (boxSlots[b].Count > 0) total -= m_Spacing;
+ advances[b] = m_LayoutBoxes[b].Region.center.y + m_StartOffsetY + total * 0.5f;
+ }
+
+ Color fillColor = new Color(0.1f, 0.7f, 1f, 0.15f);
+ Color wireColor = new Color(0f, 0.6f, 1f, 0.7f);
+
+ int globalIndex = 0;
+ bool anyRemaining = true;
+ while (anyRemaining)
+ {
+ anyRemaining = false;
+ for (int b = 0; b < boxSlots.Length; b++)
+ {
+ if (indices[b] >= boxSlots[b].Count)
+ continue;
+
+ anyRemaining = true;
+ var slot = boxSlots[b][indices[b]];
+ var box = m_LayoutBoxes[b];
+
+ float targetX = box.Region.center.x;
+ if (m_ZipperMode)
+ targetX += (globalIndex % 2 == 0 ? -m_ZipperGap : m_ZipperGap) * 0.5f;
+
+ float targetY = advances[b];
+
+ Rect localRect = new Rect(
+ targetX - slot.size.x * 0.5f,
+ targetY - slot.size.y * 0.5f,
+ slot.size.x,
+ slot.size.y);
+
+ DrawLocalRectAsWireCube(localRect, wireColor, fillColor);
+
+ advances[b] -= slot.size.y + m_Spacing;
+ indices[b]++;
+ globalIndex++;
+ }
+ }
+ }
+#endif
+
+ #endregion
}
}
diff --git a/Runtime/DynamicActivitiesImpact.meta b/Runtime/DynamicActivitiesImpact.meta
deleted file mode 100644
index ef0553a..0000000
--- a/Runtime/DynamicActivitiesImpact.meta
+++ /dev/null
@@ -1,3 +0,0 @@
-fileFormatVersion: 2
-guid: 73eb6a65472648ddafa15ed1a6f525a2
-timeCreated: 1757990443
\ No newline at end of file
diff --git a/Runtime/DynamicActivitiesImpact/UIDynamicActiveImpact.cs b/Runtime/DynamicActivitiesImpact/UIDynamicActiveImpact.cs
deleted file mode 100644
index dde3a14..0000000
--- a/Runtime/DynamicActivitiesImpact/UIDynamicActiveImpact.cs
+++ /dev/null
@@ -1,325 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-using UnityEngine;
-using UnityEngine.Pool;
-using UnityEngine.Serialization;
-using UnityEngine.UI;
-using XericLibrary.Runtime.MacroLibrary;
-using XericLibrary.Runtime.Type.SpatialAlgorithm;
-using XericUI.Core.Base;
-using XericUI.OverrideLayout;
-
-#if UNITY_EDITOR
-using XericLibraryEditor.Debug;
-#endif
-#if ODIN_INSPECTOR
-using Sirenix.OdinInspector;
-#endif
-
-namespace XericUI.DynamicActivitiesImpact
-{
- ///
- /// 导入动态激活组件(需要外部控制部分检查与生命周期事件)
- ///
- [Obsolete("直接用XericScrollRect")]
- public class UIDynamicActiveImpact : XericUIBehaciour
- {
- #region 字段属性
-
-#if ODIN_INSPECTOR
- [LabelText("主要容器")]
- [BoxGroup("基本容器", centerLabel: true)]
-#endif
- [Tooltip("容器子集应呈现平摊的样式")]
- public RectTransform containerTransform = null;
-
-#if ODIN_INSPECTOR
- [BoxGroup("基本容器")]
- [LabelText("遮罩容器")]
-#endif
- [Tooltip("用作遮罩计算的组件")]
- public RectTransform maskRectTransform = null;
-
-#if ODIN_INSPECTOR
- [BoxGroup("基本容器")]
- [LabelText("容器排版控制")]
-#endif
- public LayoutGroup layoutGroup;
-
-
-#if ODIN_INSPECTOR
- [BoxGroup("容器性能")]
- [LabelText("预加载像素范围")]
-#endif
- public float preloadMargin = 200f;
-
-#if ODIN_INSPECTOR
- [BoxGroup("容器性能")]
- [LabelText("可被察觉的响应变化长度")]
-#endif
- public float discernibleDistance = 10f;
-
-#if ODIN_INSPECTOR
- [BoxGroup("容器性能")]
- [LabelText("每秒检查更新的次数")]
-#endif
- [Range(0.1f, 50)]
- public float updateRateLimit = 2;
-
- #endregion
-
- #region 容器成员
-
- internal bool _layoutDirty;
- internal bool _transformDirty;
- internal int _lastChildrenCount;
- internal Vector2 _lastPosition;
- internal HashSet _activeSet = null;
- internal HashSet _allObjsSet = null;
-
- private HashSetPool _tempQuadUIHashSetPool = new HashSetPool();
-
- ///
- /// 保存所有ui对象的四叉树
- ///
- protected QuadTree QuadUITargetTree;
-
-
- public virtual IEnumerable ContainerChildren => containerTransform.GetChildren().Select(a => a.RectTransform());
- public virtual int ContainerChildrenCount => containerTransform.childCount;
- public virtual Rect ContainerTransformRectRange => containerTransform.ToQuadTreeRect();
- public virtual Rect MaskRectTransformRectRange => maskRectTransform.ToQuadTreeRect();
- public bool IsLayoutDirty => _layoutDirty;
- public bool IsTransformDirty => _transformDirty;
- public float SaftyDiscernibleUpdateTime => Mathf.Max(1f / 60, 1f / updateRateLimit);
-
- public LayoutGroup MainLayoutGroup => transform.GetComponent();
-
- #endregion
-
- #region 生命周期
-
-#if UNITY_EDITOR
- private Vector3[] vertex1 = new Vector3[4];
- private Vector3[] vertex2 = new Vector3[4];
- private Vector3[] vertex3 = new Vector3[4];
- private void OnDrawGizmosSelected()
- {
- var lossyScale = rectTransform.lossyScale;
- var scale = lossyScale * preloadMargin;
- var dis2 = lossyScale * discernibleDistance;
-
- containerTransform.GetWorldCorners(vertex1);
-
- vertex2[0] = new Vector3(vertex1[0].x - scale.x, vertex1[0].y - scale.y, vertex1[0].z);
- vertex2[1] = new Vector3(vertex1[1].x - scale.x, vertex1[1].y + scale.y, vertex1[1].z);
- vertex2[2] = new Vector3(vertex1[2].x + scale.x, vertex1[2].y + scale.y, vertex1[2].z);
- vertex2[3] = new Vector3(vertex1[3].x + scale.x, vertex1[3].y - scale.y, vertex1[3].z);
-
- vertex3[0] = new Vector3(vertex1[0].x - dis2.x, vertex1[0].y - dis2.y, vertex1[0].z);
- vertex3[1] = new Vector3(vertex1[1].x - dis2.x, vertex1[1].y + dis2.y, vertex1[1].z);
- vertex3[2] = new Vector3(vertex1[2].x + dis2.x, vertex1[2].y + dis2.y, vertex1[2].z);
- vertex3[3] = new Vector3(vertex1[3].x + dis2.x, vertex1[3].y - dis2.y, vertex1[3].z);
-
- Gizmos.color = Color.yellow;
- MacroGizmosDraw.DrawPolygonLineByPointArray(vertex3);
- MacroGizmosDraw.DrawGizmos2RectLinkCube(vertex1, vertex2, Color.grey, Color.cyan, true);
-
- }
-#endif
-
- protected override void Awake()
- {
- base.Awake();
- // 容器
- if (containerTransform == null)
- containerTransform = rectTransform;
- // 遮罩
- if (maskRectTransform == null)
- {
- var mask = transform.GetComponentInParent();
- maskRectTransform = mask.RectTransform();
- }
- // 布局
- if (layoutGroup == null)
- layoutGroup = transform.GetComponentInParent();
- if (layoutGroup is XericHorizontalOrVerticalLayoutGroup xericLayoutGroup)
- {
- xericLayoutGroup.layoutIgnoreInactive = false;
- }
-
- CheckIsDirty();
- }
-
- protected override void OnEnable()
- {
- base.OnEnable();
- }
-
- protected override void OnDisable()
- {
- base.OnDisable();
- }
-
- private float accTime = 0;
-
- protected virtual void Update()
- {
- if (accTime < SaftyDiscernibleUpdateTime)
- {
- accTime += Time.unscaledDeltaTime;
- return;
- }
- // 毕竟这里不是粒子计算器
- accTime = accTime % SaftyDiscernibleUpdateTime;
-
- if (IsLayoutDirty)
- {
- ResetDirtyFlag();
- RebuildQurdTree();
- }
- else if (IsTransformDirty)
- {
- ResetDirtyFlag();
- }
- }
-
- protected override void OnRectTransformDimensionsChange()
- {
- base.OnRectTransformDimensionsChange();
- CheckIsDirty();
- }
-
- private void OnDestroy()
- {
- // 恢复所有元素的原始状态
- foreach (var element in _allObjsSet)
- {
- if (element != null && _activeSet.Contains(element))
- {
- element.gameObject.SetActive(true);
- }
- }
- }
-
- #endregion
-
- #region 刷新框架
-
- ///
- /// 设置布局脏
- ///
- public void LayoutDirty()
- => _layoutDirty = true;
-
- ///
- /// 设置变换脏
- ///
- public void TransformDirty()
- => _transformDirty = true;
-
- ///
- /// 重置脏标记
- ///
- private void ResetDirtyFlag()
- {
- _layoutDirty = false;
- _transformDirty = false;
- }
-
- ///
- /// 检查容器脏
- ///
- public virtual void CheckIsDirty()
- {
- if (QuadUITargetTree == null || // 四叉树不存在?
- _lastChildrenCount != ContainerChildrenCount // 成员数量明显不对
- )
- {
- LayoutDirty();
- BackupQuadRectValue();
- return;
- }
-
- // todo 这里暂时只简单考虑直来直去的移动方式,如果有其他复杂移动,需要修改判别条件
- var deltaPosIsDirty = Mathf.Abs(QuadUITargetTree.RootRect.position.sqrMagnitude - _lastPosition.sqrMagnitude) < discernibleDistance;
-
- if (deltaPosIsDirty // 发生不可忽视的变换?
- )
- {
- TransformDirty();
- BackupQuadRectValue();
- return;
- }
- }
-
- ///
- /// 重建四叉树
- ///
- public void RebuildQurdTree()
- {
- // 重建四叉树
- QuadUITargetTree = new QuadTree(ContainerTransformRectRange, a => a.rect);
- // 记录参数
- BackupQuadRectValue();
- }
-
- ///
- /// 保存自动重建四叉树所需的参数
- ///
- ///
- protected virtual void BackupQuadRectValue()
- {
- _lastPosition = ContainerTransformRectRange.position;
- _lastChildrenCount = ContainerChildrenCount;
- }
-
-
-#if ODIN_INSPECTOR
- [Button("更新所有可见性")]
-#endif
- ///
- /// 更新所有成员的可见性
- ///
- public virtual void UpdateAllObjectVisible()
- {
- if (QuadUITargetTree == null)
- {
- Debug.LogError($"在更新成员可见性之前,需要确保四叉树存在!");
- return;
- }
-
- var visibalSet = QuadUITargetTree.Retrieve(MaskRectTransformRectRange);
- ContainerChildren.ForEachDo(obj =>
- {
- obj.gameObject.SetActive(visibalSet.Contains(obj));
- });
- }
-
- ///
- /// 要求控制布局组的激活一次,提供为何激活的原因
- ///
- /// 由于新增成员导致布局更新
- /// 由于销毁成员导致布局更新
- public virtual void UpdateLayoutGroupOnce(bool hasIncreased, bool hasDecreased)
- {
- MainLayoutGroup.enabled = true;
- StartCoroutine(WaitForLayoutDisable());
- return;
- IEnumerator WaitForLayoutDisable()
- {
- if (CanvasUpdateRegistry.IsRebuildingLayout())
- yield return new WaitForEndOfFrame();
- else
- yield return new WaitForFixedUpdate();
- MainLayoutGroup.enabled = false;
- }
- }
-
- #endregion
- }
-}
-
-
diff --git a/Runtime/DynamicActivitiesImpact/UIDynamicActiveImpact.cs.meta b/Runtime/DynamicActivitiesImpact/UIDynamicActiveImpact.cs.meta
deleted file mode 100644
index 1dbfbf5..0000000
--- a/Runtime/DynamicActivitiesImpact/UIDynamicActiveImpact.cs.meta
+++ /dev/null
@@ -1,3 +0,0 @@
-fileFormatVersion: 2
-guid: 48016863e20f4c61a91245c593d575da
-timeCreated: 1757990480
\ No newline at end of file