151 lines
4.0 KiB
C#
151 lines
4.0 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using XericLibrary.Runtime;
|
|
|
|
namespace XericUI.BubbleLayout
|
|
{
|
|
/// <summary>
|
|
/// 气泡碰撞约束插件 —— 使用四叉树检测重叠并推挤分离。
|
|
/// 默认 Order = 1000,在约束阶段最后执行,确保碰撞排挤在所有其他约束之后进行。
|
|
/// </summary>
|
|
[AddComponentMenu("Xeric UI Vessel/Layout/Bubble Collision Constraint", 52)]
|
|
public class BubbleCollisionConstraint : BubbleLayoutPluginBase, IBubbleConstraintPlugin
|
|
{
|
|
#if UNITY_EDITOR
|
|
protected override int EditorDefaultOrder => 1000;
|
|
#endif
|
|
|
|
[Tooltip("最大迭代次数,防止无限循环")]
|
|
[SerializeField] private int m_MaxIterations = 10;
|
|
|
|
[Tooltip("排斥力刚度系数")]
|
|
[SerializeField] private float m_Stiffness = 1f;
|
|
|
|
[Tooltip("最小分离距离(像素)")]
|
|
[SerializeField] private float m_MinSeparation = 2f;
|
|
|
|
[Tooltip("阻尼系数,每次迭代递减")]
|
|
[SerializeField] private float m_IterationDamping = 0.85f;
|
|
|
|
/// <summary>
|
|
/// 最大迭代次数
|
|
/// </summary>
|
|
public int MaxIterations
|
|
{
|
|
get => m_MaxIterations;
|
|
set => m_MaxIterations = Mathf.Max(1, value);
|
|
}
|
|
|
|
public void ProcessConstraint(List<BubbleItemData> items, Vector2 layoutCenter)
|
|
{
|
|
if (items == null || items.Count < 2)
|
|
return;
|
|
|
|
// 计算布局边界(包含所有元素的最小包围矩形 + 边距)
|
|
Rect layoutBounds = CalculateLayoutBounds(items);
|
|
|
|
// 为碰撞检测创建委托
|
|
System.Func<BubbleItemData, Rect> getRectFunc = item => item.GetRect();
|
|
|
|
float currentStiffness = m_Stiffness;
|
|
|
|
for (int iteration = 0; iteration < m_MaxIterations; iteration++)
|
|
{
|
|
// 每轮迭代重建四叉树
|
|
var quadTree = new QuadTree<BubbleItemData>(layoutBounds, getRectFunc,
|
|
maxObjectsPerNode: 5, maxDepth: 5);
|
|
|
|
// 插入所有元素
|
|
for (int i = 0; i < items.Count; i++)
|
|
{
|
|
quadTree.Insert(items[i]);
|
|
}
|
|
|
|
bool anyOverlap = false;
|
|
var overlapSet = new HashSet<BubbleItemData>();
|
|
|
|
// 检测所有重叠对
|
|
for (int i = 0; i < items.Count; i++)
|
|
{
|
|
var itemA = items[i];
|
|
Rect rectA = itemA.GetRect();
|
|
|
|
// 使用四叉树查找与itemA重叠的元素
|
|
overlapSet.Clear();
|
|
quadTree.Retrieve(rectA, overlapSet);
|
|
|
|
foreach (var itemB in overlapSet)
|
|
{
|
|
// 跳过自身
|
|
if (ReferenceEquals(itemA, itemB))
|
|
continue;
|
|
|
|
// 确保每对只处理一次(利用HashSet的确定性避免重复处理)
|
|
if (itemA.GetHashCode() > itemB.GetHashCode())
|
|
continue;
|
|
|
|
Rect rectB = itemB.GetRect();
|
|
|
|
// 再次确认重叠
|
|
if (!RectsOverlap(rectA, rectB))
|
|
continue;
|
|
|
|
anyOverlap = true;
|
|
|
|
// 计算排斥力
|
|
Vector2 force = BubblePhysicsSolver.CalculateRepulsionForce(
|
|
itemA.position, itemA.size,
|
|
itemB.position, itemB.size,
|
|
currentStiffness);
|
|
|
|
if (force != Vector2.zero)
|
|
{
|
|
// 两个元素各退一半
|
|
itemA.position += force * 0.5f;
|
|
itemB.position -= force * 0.5f;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!anyOverlap)
|
|
break;
|
|
|
|
// 阻尼递减
|
|
currentStiffness *= m_IterationDamping;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 计算所有元素的包围矩形
|
|
/// </summary>
|
|
private Rect CalculateLayoutBounds(List<BubbleItemData> items)
|
|
{
|
|
if (items.Count == 0)
|
|
return new Rect(0, 0, 100, 100);
|
|
|
|
float minX = float.MaxValue, minY = float.MaxValue;
|
|
float maxX = float.MinValue, maxY = float.MinValue;
|
|
|
|
for (int i = 0; i < items.Count; i++)
|
|
{
|
|
Rect r = items[i].GetRect();
|
|
minX = Mathf.Min(minX, r.xMin);
|
|
minY = Mathf.Min(minY, r.yMin);
|
|
maxX = Mathf.Max(maxX, r.xMax);
|
|
maxY = Mathf.Max(maxY, r.yMax);
|
|
}
|
|
|
|
// 添加边距
|
|
float margin = 100f;
|
|
return new Rect(minX - margin, minY - margin,
|
|
(maxX - minX) + margin * 2f, (maxY - minY) + margin * 2f);
|
|
}
|
|
|
|
private static bool RectsOverlap(Rect a, Rect b)
|
|
{
|
|
return a.xMin < b.xMax && a.xMax > b.xMin &&
|
|
a.yMin < b.yMax && a.yMax > b.yMin;
|
|
}
|
|
}
|
|
}
|