修复气泡布局存在的一些问题
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.BubbleLayout
|
||||
{
|
||||
/// <summary>
|
||||
/// 边界限制模式
|
||||
/// </summary>
|
||||
public enum BoundaryShapeMode
|
||||
{
|
||||
/// <summary>矩形范围:尺寸的 x/y 代表矩形的宽度和高度</summary>
|
||||
Rectangle,
|
||||
|
||||
/// <summary>椭圆范围:尺寸的 x/y 代表椭圆的横轴半径和纵轴半径</summary>
|
||||
Ellipse,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 边界限制约束插件 —— 将气泡元素限制在指定范围内。
|
||||
/// 支持矩形/椭圆两种形状,并记录超出范围的元素。
|
||||
/// </summary>
|
||||
[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<int> m_OutOfBoundsObjects = new HashSet<int>();
|
||||
|
||||
/// <summary>
|
||||
/// 记录越界元素:Key=GameObject实例ID,Value=true表示当前越界
|
||||
/// </summary>
|
||||
[System.NonSerialized]
|
||||
private Dictionary<int, bool> m_OutOfBoundsCache = new Dictionary<int, bool>();
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定 GameObject 是否超出了边界范围
|
||||
/// </summary>
|
||||
public bool IsOutOfBounds(GameObject target)
|
||||
{
|
||||
if (target == null)
|
||||
return false;
|
||||
return m_OutOfBoundsCache.TryGetValue(target.GetInstanceID(), out bool value) && value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前所有越界 GameObject 的实例 ID 集合(只读)
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<int> GetOutOfBoundsInstanceIDs()
|
||||
{
|
||||
return m_OutOfBoundsObjects;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前越界的元素数量
|
||||
/// </summary>
|
||||
public int GetOutOfBoundsCount() => m_OutOfBoundsObjects.Count;
|
||||
|
||||
/// <summary>
|
||||
/// 清除越界记录
|
||||
/// </summary>
|
||||
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<BubbleItemData> 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 碰撞检测
|
||||
|
||||
/// <summary>
|
||||
/// 检测矩形元素是否完全在矩形边界内
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测矩形元素是否完全在椭圆边界内
|
||||
/// 椭圆方程:dx²/rx² + dy²/ry² ≤ 1 时需要元素所有角点均在椭圆内。
|
||||
/// 近似检测:元素中心 + 对角线半长构成的「外接圆」半径 vs 椭圆最大轴向半径。
|
||||
/// 精确检测:依次检查四个角点。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断一个点是否在椭圆内:dx²/rx² + dy²/ry² ≤ 1
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将元素中心钳制到边界内(保留最大可能的尺寸)
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd7765032236e2c42aea99d854c4e754
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取布局中心点
|
||||
/// 获取布局中心点(localPosition 空间)
|
||||
/// localPosition (0,0) = pivot 位置,此方法返回 rect 中心相对 pivot 的偏移
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
@@ -25,8 +25,8 @@ namespace XericUI.BubbleLayout
|
||||
|
||||
protected virtual void Reset()
|
||||
{
|
||||
m_Order = EditorDefaultOrder;
|
||||
m_Enabled = true;
|
||||
m_PluginOrder = EditorDefaultOrder;
|
||||
m_PluginEnabled = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -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<GizmoSlot>();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,78 +5,115 @@ namespace XericUI.BubbleLayout
|
||||
{
|
||||
/// <summary>
|
||||
/// 横排布局约束插件 —— 将气泡元素沿 X 轴排列,并通过弹簧力吸附到目标位置。
|
||||
/// 支持多布局框穿插排列。
|
||||
/// </summary>
|
||||
[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<BubbleItemData> items, Vector2 layoutCenter)
|
||||
[Tooltip("起始水平偏移(相对布局框区域中心)")]
|
||||
[SerializeField] private float m_StartOffsetX = 0f;
|
||||
|
||||
#region LinearLayoutConstraintBase 实现
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string PrimaryAxisName => "X";
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool IsPrimaryHorizontal => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override float GetPrimarySize(BubbleItemData item) => item.size.x;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override float GetPrimarySizeFromRT(RectTransform rt) => rt.rect.width;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Vector2 MakeTargetPosition(float primary, float secondary)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return;
|
||||
|
||||
var sorted = new List<BubbleItemData>(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<BubbleItemData> items)
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
/// <inheritdoc/>
|
||||
protected override void DrawItemTargets(List<GizmoSlot> allSlots, Vector2 layoutCenter)
|
||||
{
|
||||
// 将 slots 按 boxIndex 分组
|
||||
var boxSlots = new List<GizmoSlot>[m_LayoutBoxes.Count];
|
||||
for (int b = 0; b < m_LayoutBoxes.Count; b++)
|
||||
boxSlots[b] = new List<GizmoSlot>();
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.BubbleLayout
|
||||
{
|
||||
/// <summary>
|
||||
/// 布局框定义 —— 描述一个在父级坐标空间中的矩形区域,
|
||||
/// 用作横向/竖向布局中特定轨道的目标区域。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class LayoutBox
|
||||
{
|
||||
/// <summary>
|
||||
/// 布局框名称(仅用于 Inspector 标识)
|
||||
/// </summary>
|
||||
[Tooltip("布局框名称")]
|
||||
public string Name = "Box";
|
||||
|
||||
/// <summary>
|
||||
/// 此框在父级 RectTransform localPosition 空间中的矩形区域
|
||||
/// </summary>
|
||||
[Tooltip("布局框的矩形区域(localPosition 空间)")]
|
||||
public Rect Region = new Rect(-100, -100, 200, 200);
|
||||
|
||||
/// <summary>
|
||||
/// 矩形区域内使用的枢轴点(0~1),用于计算子项在区域内的排列起点
|
||||
/// </summary>
|
||||
[Tooltip("区域内枢轴")]
|
||||
public Vector2 Pivot = new Vector2(0.5f, 0.5f);
|
||||
|
||||
/// <summary>
|
||||
/// 每行/列的最大元素数量(0=不限制),仅对穿插模式有意义
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Region 在 localPosition 空间中的中心点
|
||||
/// </summary>
|
||||
public Vector2 GetCenter()
|
||||
{
|
||||
return Region.center;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取排列的起始锚点位置(根据 Pivot 在 Region 内的映射)
|
||||
/// </summary>
|
||||
public Vector2 GetAnchorPoint()
|
||||
{
|
||||
return new Vector2(
|
||||
Region.x + Region.width * Pivot.x,
|
||||
Region.y + Region.height * Pivot.y);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 元素到布局框的序列化映射项(仅用于 Editor 持久化)
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct LayoutBoxAssignment
|
||||
{
|
||||
/// <summary>目标子项的 Key(name_instanceID)</summary>
|
||||
public string ItemKey;
|
||||
|
||||
/// <summary>分配的布局框索引</summary>
|
||||
public int BoxIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72b571aa1837cf54ba45b65983f7999f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,531 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.BubbleLayout
|
||||
{
|
||||
/// <summary>
|
||||
/// 横向 / 竖向线性布局约束插件的共用基类。
|
||||
/// 提供布局框管理、元素分组排序、穿插迭代、弹簧吸附/瞬移、Editor Gizmo 框架等复用逻辑。
|
||||
/// </summary>
|
||||
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<LayoutBox> m_LayoutBoxes = new List<LayoutBox>
|
||||
{
|
||||
new LayoutBox("Default", new Rect(-200, -200, 400, 400)),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 元素到布局框的分配表(运行时字典的序列化后备)
|
||||
/// </summary>
|
||||
[SerializeField] private List<LayoutBoxAssignment> m_SerializedAssignments = new List<LayoutBoxAssignment>();
|
||||
|
||||
[NonSerialized]
|
||||
protected Dictionary<string, int> m_BoxAssignments;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性
|
||||
|
||||
/// <summary>主轴名称("X" / "Y"),用于子类在 Gizmo / 日志中标识</summary>
|
||||
protected abstract string PrimaryAxisName { get; }
|
||||
|
||||
/// <summary>获取元素沿主轴方向的尺寸</summary>
|
||||
protected abstract float GetPrimarySize(BubbleItemData item);
|
||||
|
||||
/// <summary>
|
||||
/// 获取元素沿主轴方向的尺寸(用于 Gizmo 预览的 RectTransform 版本)
|
||||
/// </summary>
|
||||
protected abstract float GetPrimarySizeFromRT(RectTransform rt);
|
||||
|
||||
/// <summary>
|
||||
/// 根据主轴/辅轴坐标合成最终 localPosition
|
||||
/// </summary>
|
||||
protected abstract Vector2 MakeTargetPosition(float primary, float secondary);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 布局框管理
|
||||
|
||||
/// <summary>
|
||||
/// 惰性初始化运行时字典
|
||||
/// </summary>
|
||||
protected void EnsureAssignmentsInitialized()
|
||||
{
|
||||
if (m_BoxAssignments != null)
|
||||
return;
|
||||
|
||||
m_BoxAssignments = new Dictionary<string, int>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保布局框列表至少有一项
|
||||
/// </summary>
|
||||
protected void EnsureAtLeastOneBox()
|
||||
{
|
||||
if (m_LayoutBoxes == null)
|
||||
m_LayoutBoxes = new List<LayoutBox>();
|
||||
if (m_LayoutBoxes.Count == 0)
|
||||
m_LayoutBoxes.Add(new LayoutBox("Default", new Rect(-200, -200, 400, 400)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将指定 GameObject 分配到指定编号的布局框。
|
||||
/// 默认所有子项属于第 0 号框。
|
||||
/// </summary>
|
||||
/// <param name="boxIndex">布局框索引(从 0 开始),超出范围会被钳制</param>
|
||||
/// <param name="target">要分配的子项 GameObject</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除指定 GameObject 的布局框分配(恢复默认框 0)
|
||||
/// </summary>
|
||||
public void ClearBoxAssignment(GameObject target)
|
||||
{
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
EnsureAssignmentsInitialized();
|
||||
string key = $"{target.name}_{target.GetInstanceID()}";
|
||||
m_BoxAssignments.Remove(key);
|
||||
SyncDictionaryToSerialized();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据屏幕空间坐标和指定 Pivot,将布局框的 Region 映射到屏幕对应位置。
|
||||
/// 适用于 Canvas ScreenSpace-Overlay / Camera 模式。
|
||||
/// </summary>
|
||||
/// <param name="boxIndex">目标布局框索引</param>
|
||||
/// <param name="screenRect">屏幕空间矩形(像素坐标)</param>
|
||||
/// <param name="camera">渲染 Canvas 的相机,ScreenSpace-Overlay 时传 null</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定 GameObject 当前分配的布局框索引
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有布局框(只读)
|
||||
/// </summary>
|
||||
public IReadOnlyList<LayoutBox> 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<BubbleItemData> items, Vector2 layoutCenter)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return;
|
||||
|
||||
EnsureAtLeastOneBox();
|
||||
EnsureAssignmentsInitialized();
|
||||
|
||||
// 1. 将元素按布局框分组
|
||||
var boxGroups = new List<BubbleItemData>[m_LayoutBoxes.Count];
|
||||
for (int b = 0; b < m_LayoutBoxes.Count; b++)
|
||||
boxGroups[b] = new List<BubbleItemData>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断主轴是水平还是垂直
|
||||
/// </summary>
|
||||
protected abstract bool IsPrimaryHorizontal { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 子类提供辅轴对齐偏移量(如横排的垂直对齐、竖排的拉链偏移)
|
||||
/// </summary>
|
||||
/// <returns>偏移量(正值 = 远离区域左上角方向)</returns>
|
||||
protected virtual float GetSecondaryAlignOffset(BubbleItemData item, LayoutBox box)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将目标位置钳制到布局框区域内(考虑元素自身尺寸)
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断位置是否超出布局框(用于瞬移判断)
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 排序元素列表
|
||||
/// </summary>
|
||||
protected void SortItems(List<BubbleItemData> 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
|
||||
/// <summary>
|
||||
/// Gizmo 预览数据结构
|
||||
/// </summary>
|
||||
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<GizmoSlot>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制所有布局框的矩形区域(白色虚线框)
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 子类重写:根据已排序的 slots 和 layoutCenter 绘制各元素的目标位置预览
|
||||
/// </summary>
|
||||
protected abstract void DrawItemTargets(List<GizmoSlot> allSlots, Vector2 layoutCenter);
|
||||
|
||||
/// <summary>
|
||||
/// 绘制一个线框矩形
|
||||
/// </summary>
|
||||
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]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 localPosition 空间的 Rect 绘制为世界空间线框
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0158a0d6480014d46ac381596bfe78fe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 灵活的四叉树实现,支持多种坐标类型
|
||||
/// </summary>
|
||||
public class QuadTree<T>
|
||||
{
|
||||
#region 字段属性
|
||||
|
||||
private int maxObjectsPerNode = 5;
|
||||
private int maxDepth = 4;
|
||||
|
||||
private Node root;
|
||||
private Func<T, Rect> getRectFunc; // 获取对象边界的委托
|
||||
|
||||
/// <summary>
|
||||
/// 获取此四叉树的范围
|
||||
/// </summary>
|
||||
public Rect RootRect => root != null ? root.boundary : Rect.zero;
|
||||
|
||||
public int Count { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 清空四叉树,保留边界和配置以便重用
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
if (root != null)
|
||||
{
|
||||
root.ClearAll();
|
||||
}
|
||||
Count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重建四叉树,使用相同边界和配置
|
||||
/// </summary>
|
||||
public void Rebuild(IEnumerable<T> items)
|
||||
{
|
||||
Clear();
|
||||
foreach (var item in items)
|
||||
{
|
||||
Insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取四叉树中所有元素
|
||||
/// </summary>
|
||||
public void GetAllItems(HashSet<T> result)
|
||||
{
|
||||
if (result == null)
|
||||
return;
|
||||
root?.CollectAllItems(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
|
||||
/// <summary>
|
||||
/// 初始化四叉树
|
||||
/// </summary>
|
||||
/// <param name="boundary">四叉树边界</param>
|
||||
/// <param name="getRectFunc">获取对象边界的函数</param>
|
||||
public QuadTree(Rect boundary, Func<T, Rect> 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 公共方法
|
||||
|
||||
/// <summary>
|
||||
/// 插入元素
|
||||
/// </summary>
|
||||
public bool Insert(T item)
|
||||
{
|
||||
return root.Insert(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找范围内的元素
|
||||
/// </summary>
|
||||
/// <param name="area">查找范围</param>
|
||||
/// <param name="result">建议使用数据容器对象池控制,避免频繁GC,默认空将自动创建新的数据容器</param>
|
||||
/// <returns></returns>
|
||||
public HashSet<T> Retrieve(Rect area, HashSet<T> result = null)
|
||||
{
|
||||
return root.Retrieve(area, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找最近的元素
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调试绘制网格
|
||||
/// </summary>
|
||||
public void DrawGrid()
|
||||
{
|
||||
root.DrawGrid(Color.white);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 节点类
|
||||
|
||||
private class Node : IEnumerable<Node>
|
||||
{
|
||||
internal Rect boundary;
|
||||
internal HashSet<T> objects = new HashSet<T>();
|
||||
internal Node[] children;
|
||||
internal readonly int depth;
|
||||
internal readonly Func<T, Rect> getRectFunc;
|
||||
internal readonly QuadTree<T> parent;
|
||||
|
||||
public Node(Rect boundary, int depth, Func<T, Rect> getRectFunc, QuadTree<T> parent)
|
||||
{
|
||||
this.boundary = boundary;
|
||||
this.depth = depth;
|
||||
this.getRectFunc = getRectFunc;
|
||||
this.parent = parent;
|
||||
this.children = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 插入元素
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分裂为四个子节点
|
||||
/// </summary>
|
||||
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<T>(objects);
|
||||
objects.Clear();
|
||||
objects = null; // 标记为非叶子节点
|
||||
|
||||
foreach (var obj in tempObjects)
|
||||
{
|
||||
Insert(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找最近的元素
|
||||
/// </summary>
|
||||
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<Tuple<Node, float>>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找范围内的元素
|
||||
/// </summary>
|
||||
public HashSet<T> Retrieve(Rect area, HashSet<T> result)
|
||||
{
|
||||
if (result == null)
|
||||
result = new HashSet<T>();
|
||||
|
||||
// 如果与当前节点不重叠,返回空集
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制节点网格
|
||||
/// </summary>
|
||||
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 辅助方法
|
||||
|
||||
/// <summary>
|
||||
/// 判断两个矩形是否重叠
|
||||
/// </summary>
|
||||
private bool IsOverlapping(Rect a, Rect b)
|
||||
{
|
||||
return a.x < b.xMax && a.xMax > b.x &&
|
||||
a.y < b.yMax && a.yMax > b.y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算点到矩形的最小距离平方
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归清空所有节点
|
||||
/// </summary>
|
||||
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<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归收集所有元素
|
||||
/// </summary>
|
||||
public void CollectAllItems(HashSet<T> 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<Node> 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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 四叉树扩展方法,提供不同类型的矩形转换
|
||||
/// </summary>
|
||||
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<T> CreateQuadTree<T>(this IEnumerable<T> collection, Rect boundary, Func<T, Rect> getRectFunc)
|
||||
{
|
||||
var quadTree = new QuadTree<T>(boundary, getRectFunc);
|
||||
foreach (var item in collection)
|
||||
{
|
||||
quadTree.Insert(item);
|
||||
}
|
||||
return quadTree;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eac931867b3f4645a5c0e454b9deb626
|
||||
timeCreated: 1781071846
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -24,89 +23,223 @@ namespace XericUI.BubbleLayout
|
||||
|
||||
/// <summary>
|
||||
/// 竖排布局约束插件 —— 将气泡元素沿 Y 轴排列,并通过弹簧力吸附到目标位置。
|
||||
/// 支持多布局框穿插排列和拉链模式(一左一右交替)。
|
||||
/// </summary>
|
||||
[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<BubbleItemData> items, Vector2 layoutCenter)
|
||||
#region LinearLayoutConstraintBase 实现
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string PrimaryAxisName => "Y";
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool IsPrimaryHorizontal => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override float GetPrimarySize(BubbleItemData item) => item.size.y;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override float GetPrimarySizeFromRT(RectTransform rt) => rt.rect.height;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Vector2 MakeTargetPosition(float primary, float secondary)
|
||||
{
|
||||
return new Vector2(secondary, primary);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override float GetSecondaryAlignOffset(BubbleItemData item, LayoutBox box)
|
||||
{
|
||||
// 拉链偏移由外部交叉索引决定,这里返回 0
|
||||
return 0f;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 重写 ProcessConstraint 以支持拉链模式
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ProcessConstraint(List<BubbleItemData> items, Vector2 layoutCenter)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return;
|
||||
|
||||
// 排序
|
||||
var sorted = new List<BubbleItemData>(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<BubbleItemData>[m_LayoutBoxes.Count];
|
||||
for (int b = 0; b < m_LayoutBoxes.Count; b++)
|
||||
boxGroups[b] = new List<BubbleItemData>();
|
||||
|
||||
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<BubbleItemData> 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
|
||||
/// <inheritdoc/>
|
||||
protected override void DrawItemTargets(List<GizmoSlot> allSlots, Vector2 layoutCenter)
|
||||
{
|
||||
var boxSlots = new List<GizmoSlot>[m_LayoutBoxes.Count];
|
||||
for (int b = 0; b < m_LayoutBoxes.Count; b++)
|
||||
boxSlots[b] = new List<GizmoSlot>();
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73eb6a65472648ddafa15ed1a6f525a2
|
||||
timeCreated: 1757990443
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 导入动态激活组件(需要外部控制部分检查与生命周期事件)
|
||||
/// </summary>
|
||||
[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<RectTransform> _activeSet = null;
|
||||
internal HashSet<RectTransform> _allObjsSet = null;
|
||||
|
||||
private HashSetPool<RectTransform> _tempQuadUIHashSetPool = new HashSetPool<RectTransform>();
|
||||
|
||||
/// <summary>
|
||||
/// 保存所有ui对象的四叉树
|
||||
/// </summary>
|
||||
protected QuadTree<RectTransform> QuadUITargetTree;
|
||||
|
||||
|
||||
public virtual IEnumerable<RectTransform> 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<LayoutGroup>();
|
||||
|
||||
#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<Mask>();
|
||||
maskRectTransform = mask.RectTransform();
|
||||
}
|
||||
// 布局
|
||||
if (layoutGroup == null)
|
||||
layoutGroup = transform.GetComponentInParent<LayoutGroup>();
|
||||
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 刷新框架
|
||||
|
||||
/// <summary>
|
||||
/// 设置布局脏
|
||||
/// </summary>
|
||||
public void LayoutDirty()
|
||||
=> _layoutDirty = true;
|
||||
|
||||
/// <summary>
|
||||
/// 设置变换脏
|
||||
/// </summary>
|
||||
public void TransformDirty()
|
||||
=> _transformDirty = true;
|
||||
|
||||
/// <summary>
|
||||
/// 重置脏标记
|
||||
/// </summary>
|
||||
private void ResetDirtyFlag()
|
||||
{
|
||||
_layoutDirty = false;
|
||||
_transformDirty = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查容器脏
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重建四叉树
|
||||
/// </summary>
|
||||
public void RebuildQurdTree()
|
||||
{
|
||||
// 重建四叉树
|
||||
QuadUITargetTree = new QuadTree<RectTransform>(ContainerTransformRectRange, a => a.rect);
|
||||
// 记录参数
|
||||
BackupQuadRectValue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存自动重建四叉树所需的参数
|
||||
/// </summary>
|
||||
/// <param name="target"></param>
|
||||
protected virtual void BackupQuadRectValue()
|
||||
{
|
||||
_lastPosition = ContainerTransformRectRange.position;
|
||||
_lastChildrenCount = ContainerChildrenCount;
|
||||
}
|
||||
|
||||
|
||||
#if ODIN_INSPECTOR
|
||||
[Button("更新所有可见性")]
|
||||
#endif
|
||||
/// <summary>
|
||||
/// 更新所有成员的可见性
|
||||
/// </summary>
|
||||
public virtual void UpdateAllObjectVisible()
|
||||
{
|
||||
if (QuadUITargetTree == null)
|
||||
{
|
||||
Debug.LogError($"在更新成员可见性之前,需要确保四叉树存在!");
|
||||
return;
|
||||
}
|
||||
|
||||
var visibalSet = QuadUITargetTree.Retrieve(MaskRectTransformRectRange);
|
||||
ContainerChildren.ForEachDo(obj =>
|
||||
{
|
||||
obj.gameObject.SetActive(visibalSet.Contains(obj));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 要求控制布局组的激活一次,提供为何激活的原因
|
||||
/// </summary>
|
||||
/// <param name="hasIncreased">由于新增成员导致布局更新</param>
|
||||
/// <param name="hasDecreased">由于销毁成员导致布局更新</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48016863e20f4c61a91245c593d575da
|
||||
timeCreated: 1757990480
|
||||
Reference in New Issue
Block a user