353 lines
12 KiB
C#
353 lines
12 KiB
C#
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.UI;
|
||
using TMPro;
|
||
|
||
namespace MeowmentDebugTool
|
||
{
|
||
/// <summary>
|
||
/// 可拖动的悬浮按钮
|
||
/// 点击打开调试工具,可以在屏幕上自由拖动
|
||
/// </summary>
|
||
public class DraggableFloatingButton : MonoBehaviour, IPointerDownHandler, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerClickHandler
|
||
{
|
||
[Header("设置")]
|
||
[Tooltip("是否启用拖动功能")]
|
||
public bool enableDragging = false;
|
||
|
||
[Tooltip("是否限制在屏幕范围内")]
|
||
public bool clampToScreen = true;
|
||
|
||
[Tooltip("与屏幕边缘的最小距离")]
|
||
public float edgePadding = 20f;
|
||
|
||
[Header("自动吸附")]
|
||
[Tooltip("是否自动吸附到屏幕边缘")]
|
||
public bool snapToEdge = true;
|
||
|
||
[Tooltip("吸附动画时间")]
|
||
public float snapDuration = 0.1f;
|
||
|
||
private RectTransform rectTransform;
|
||
private Canvas canvas;
|
||
private Vector2 dragOffset;
|
||
private bool isDragging = false;
|
||
private Vector2 targetPosition;
|
||
private bool isSnapping = false;
|
||
|
||
// 用于区分点击和拖动
|
||
private bool hasDragged = false; // 标记本次操作是否进行了拖动
|
||
private Vector2 dragStartPosition; // 拖动起始位置
|
||
private const float dragThreshold = 5f; // 拖动阈值,移动超过这个距离才算拖动
|
||
private const float FpsRefreshInterval = 0.25f;
|
||
|
||
private TMP_Text titleText;
|
||
private TMP_Text fpsText;
|
||
private TMP_Text fallbackText;
|
||
private float smoothedUnscaledDeltaTime = 1f / 60f;
|
||
private float nextFpsRefreshTime = 0f;
|
||
|
||
private void Awake()
|
||
{
|
||
rectTransform = GetComponent<RectTransform>();
|
||
canvas = GetComponentInParent<Canvas>();
|
||
|
||
if (canvas == null)
|
||
{
|
||
Debug.LogError("DraggableFloatingButton 必须在Canvas下!");
|
||
}
|
||
|
||
targetPosition = rectTransform.anchoredPosition;
|
||
titleText = transform.Find("ContentCanvas/TitleText")?.GetComponent<TMP_Text>()
|
||
?? transform.Find("TitleText")?.GetComponent<TMP_Text>();
|
||
fpsText = transform.Find("ContentCanvas/FpsText")?.GetComponent<TMP_Text>()
|
||
?? transform.Find("FpsText")?.GetComponent<TMP_Text>();
|
||
fallbackText = transform.Find("Text")?.GetComponent<TMP_Text>();
|
||
UpdateFpsDisplay(true);
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
// 平滑移动到目标位置(吸附动画)
|
||
if (isSnapping && !isDragging)
|
||
{
|
||
rectTransform.anchoredPosition = Vector2.Lerp(
|
||
rectTransform.anchoredPosition,
|
||
targetPosition,
|
||
Time.deltaTime / snapDuration
|
||
);
|
||
|
||
if (Vector2.Distance(rectTransform.anchoredPosition, targetPosition) < 0.5f)
|
||
{
|
||
rectTransform.anchoredPosition = targetPosition;
|
||
isSnapping = false;
|
||
}
|
||
}
|
||
|
||
smoothedUnscaledDeltaTime = Mathf.Lerp(smoothedUnscaledDeltaTime, Time.unscaledDeltaTime, 0.12f);
|
||
if (Time.unscaledTime >= nextFpsRefreshTime)
|
||
{
|
||
nextFpsRefreshTime = Time.unscaledTime + FpsRefreshInterval;
|
||
UpdateFpsDisplay(false);
|
||
}
|
||
}
|
||
|
||
private void UpdateFpsDisplay(bool force)
|
||
{
|
||
if (!force && !gameObject.activeInHierarchy)
|
||
{
|
||
return;
|
||
}
|
||
|
||
int fps = Mathf.Clamp(Mathf.RoundToInt(1f / Mathf.Max(0.0001f, smoothedUnscaledDeltaTime)), 0, 999);
|
||
|
||
if (titleText != null)
|
||
{
|
||
titleText.text = "调试";
|
||
}
|
||
|
||
if (fpsText != null)
|
||
{
|
||
fpsText.text = $"{fps} FPS";
|
||
return;
|
||
}
|
||
|
||
if (fallbackText != null)
|
||
{
|
||
fallbackText.text = $"调试\n{fps} FPS";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 每次按下时重置拖动标记(在OnBeginDrag之前触发)
|
||
/// </summary>
|
||
public void OnPointerDown(PointerEventData eventData)
|
||
{
|
||
hasDragged = false; // 重置拖动标记
|
||
}
|
||
|
||
public void OnBeginDrag(PointerEventData eventData)
|
||
{
|
||
if (!enableDragging)
|
||
{
|
||
// 如果禁用拖动,将事件传递给父级,让其他UI元素能够接收拖动事件
|
||
PassEventToParent(eventData, ExecuteEvents.beginDragHandler);
|
||
return;
|
||
}
|
||
|
||
isDragging = true;
|
||
isSnapping = false;
|
||
dragStartPosition = eventData.position; // 记录起始位置
|
||
|
||
// 计算拖动偏移
|
||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||
canvas.transform as RectTransform,
|
||
eventData.position,
|
||
eventData.pressEventCamera,
|
||
out Vector2 localPoint
|
||
);
|
||
|
||
dragOffset = rectTransform.anchoredPosition - localPoint;
|
||
}
|
||
|
||
public void OnDrag(PointerEventData eventData)
|
||
{
|
||
if (!enableDragging)
|
||
{
|
||
// 如果禁用拖动,将事件传递给父级
|
||
PassEventToParent(eventData, ExecuteEvents.dragHandler);
|
||
return;
|
||
}
|
||
|
||
if (!isDragging) return;
|
||
|
||
// 检查是否超过拖动阈值
|
||
float dragDistance = Vector2.Distance(dragStartPosition, eventData.position);
|
||
if (dragDistance > dragThreshold)
|
||
{
|
||
hasDragged = true; // 标记已经拖动
|
||
}
|
||
|
||
// 转换屏幕坐标到Canvas局部坐标
|
||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||
canvas.transform as RectTransform,
|
||
eventData.position,
|
||
eventData.pressEventCamera,
|
||
out Vector2 localPoint
|
||
);
|
||
|
||
// 应用拖动偏移
|
||
Vector2 newPosition = localPoint + dragOffset;
|
||
|
||
// 限制在屏幕范围内
|
||
if (clampToScreen)
|
||
{
|
||
newPosition = ClampToScreen(newPosition);
|
||
}
|
||
|
||
rectTransform.anchoredPosition = newPosition;
|
||
}
|
||
|
||
public void OnEndDrag(PointerEventData eventData)
|
||
{
|
||
if (!enableDragging)
|
||
{
|
||
// 如果禁用拖动,将事件传递给父级
|
||
PassEventToParent(eventData, ExecuteEvents.endDragHandler);
|
||
return;
|
||
}
|
||
|
||
isDragging = false;
|
||
|
||
// 自动吸附到最近的边缘
|
||
if (snapToEdge)
|
||
{
|
||
SnapToNearestEdge();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理点击事件 - 只有在没有拖动时才打开窗口
|
||
/// </summary>
|
||
public void OnPointerClick(PointerEventData eventData)
|
||
{
|
||
// 如果进行了拖动,不执行点击操作
|
||
if (hasDragged)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 纯点击操作,打开调试窗口
|
||
UniversalDebugTool debugTool = FindObjectOfType<UniversalDebugTool>();
|
||
if (debugTool != null)
|
||
{
|
||
debugTool.OpenDebugWindow();
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[X] 未找到 UniversalDebugTool 实例!");
|
||
}
|
||
}
|
||
|
||
private Vector2 ClampToScreen(Vector2 position)
|
||
{
|
||
RectTransform canvasRect = canvas.transform as RectTransform;
|
||
Vector2 canvasSize = canvasRect.sizeDelta;
|
||
Vector2 buttonSize = rectTransform.sizeDelta;
|
||
|
||
// 计算可移动范围
|
||
float minX = -canvasSize.x / 2 + buttonSize.x / 2 + edgePadding;
|
||
float maxX = canvasSize.x / 2 - buttonSize.x / 2 - edgePadding;
|
||
float minY = -canvasSize.y / 2 + buttonSize.y / 2 + edgePadding;
|
||
float maxY = canvasSize.y / 2 - buttonSize.y / 2 - edgePadding;
|
||
|
||
position.x = Mathf.Clamp(position.x, minX, maxX);
|
||
position.y = Mathf.Clamp(position.y, minY, maxY);
|
||
|
||
return position;
|
||
}
|
||
|
||
private void SnapToNearestEdge()
|
||
{
|
||
RectTransform canvasRect = canvas.transform as RectTransform;
|
||
Vector2 canvasSize = canvasRect.sizeDelta;
|
||
Vector2 currentPos = rectTransform.anchoredPosition;
|
||
|
||
// 计算到各个边缘的距离
|
||
float distToLeft = Mathf.Abs(currentPos.x + canvasSize.x / 2);
|
||
float distToRight = Mathf.Abs(currentPos.x - canvasSize.x / 2);
|
||
float distToTop = Mathf.Abs(currentPos.y - canvasSize.y / 2);
|
||
float distToBottom = Mathf.Abs(currentPos.y + canvasSize.y / 2);
|
||
|
||
// 找到最近的边
|
||
float minDist = Mathf.Min(distToLeft, distToRight, distToTop, distToBottom);
|
||
|
||
Vector2 snapPosition = currentPos;
|
||
Vector2 buttonSize = rectTransform.sizeDelta;
|
||
|
||
if (minDist == distToLeft)
|
||
{
|
||
// 吸附到左边
|
||
snapPosition.x = -canvasSize.x / 2 + buttonSize.x / 2 + edgePadding;
|
||
}
|
||
else if (minDist == distToRight)
|
||
{
|
||
// 吸附到右边
|
||
snapPosition.x = canvasSize.x / 2 - buttonSize.x / 2 - edgePadding;
|
||
}
|
||
else if (minDist == distToTop)
|
||
{
|
||
// 吸附到顶部
|
||
snapPosition.y = canvasSize.y / 2 - buttonSize.y / 2 - edgePadding;
|
||
}
|
||
else if (minDist == distToBottom)
|
||
{
|
||
// 吸附到底部
|
||
snapPosition.y = -canvasSize.y / 2 + buttonSize.y / 2 + edgePadding;
|
||
}
|
||
|
||
// 确保在屏幕范围内
|
||
snapPosition = ClampToScreen(snapPosition);
|
||
|
||
targetPosition = snapPosition;
|
||
isSnapping = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置悬浮按钮位置
|
||
/// </summary>
|
||
public void SetPosition(Vector2 position)
|
||
{
|
||
if (clampToScreen)
|
||
{
|
||
position = ClampToScreen(position);
|
||
}
|
||
rectTransform.anchoredPosition = position;
|
||
targetPosition = position;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前位置
|
||
/// </summary>
|
||
public Vector2 GetPosition()
|
||
{
|
||
return rectTransform.anchoredPosition;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将事件传递给父级或下层的UI元素
|
||
/// 当拖动功能被禁用时,确保其他UI元素仍能接收拖动事件
|
||
/// </summary>
|
||
private void PassEventToParent<T>(PointerEventData eventData, ExecuteEvents.EventFunction<T> eventFunction) where T : IEventSystemHandler
|
||
{
|
||
Transform parent = transform.parent;
|
||
while (parent != null)
|
||
{
|
||
// 尝试执行父级对象上的事件
|
||
if (ExecuteEvents.Execute(parent.gameObject, eventData, eventFunction))
|
||
{
|
||
// 如果父级处理了事件,停止传递
|
||
return;
|
||
}
|
||
parent = parent.parent;
|
||
}
|
||
|
||
// 如果父级都没有处理,尝试传递给射线检测到的下一个对象
|
||
var raycastResults = new System.Collections.Generic.List<RaycastResult>();
|
||
EventSystem.current.RaycastAll(eventData, raycastResults);
|
||
|
||
// 跳过当前对象,找到下一个可以处理事件的对象
|
||
foreach (var result in raycastResults)
|
||
{
|
||
if (result.gameObject == gameObject)
|
||
continue;
|
||
|
||
if (ExecuteEvents.Execute(result.gameObject, eventData, eventFunction))
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|