Files

331 lines
9.4 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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实例IDValue=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
}
}