286 lines
9.9 KiB
C#
286 lines
9.9 KiB
C#
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
using System.Collections;
|
|
using TowerClimberChronicles;
|
|
using DG.Tweening;
|
|
[RequireComponent(typeof(RectTransform))]
|
|
public class DragPuzzle : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerDownHandler
|
|
{
|
|
[Header("Snap config")]
|
|
public Vector2 snapTarget;
|
|
public float snapDistance;
|
|
private bool lockOnSnap = true;
|
|
|
|
[Header("State")]
|
|
public bool locked = false; // 默认可交互,若要初始不可拖动改为 true
|
|
|
|
private RectTransform rect;
|
|
private Canvas rootCanvas;
|
|
private Camera uiCamera;
|
|
private ScrollRect scrollRect;
|
|
|
|
private bool isFlying = false;
|
|
private Vector2 pointerOffset; // 在当前父Rect空间的偏移
|
|
private Vector3 cachedLocalScale;
|
|
private Image overlay;
|
|
void Awake()
|
|
{
|
|
rect = GetComponent<RectTransform>();
|
|
rootCanvas = GetRootCanvas(transform);
|
|
scrollRect = GameObject.Find("ScrollView")?.GetComponent<ScrollRect>();
|
|
|
|
// 必须存在:EventSystem + Canvas(含GraphicRaycaster) + 本物体或子物体上有Graphic且RaycastTarget启用
|
|
uiCamera = rootCanvas && rootCanvas.renderMode == RenderMode.ScreenSpaceOverlay
|
|
? null
|
|
: (rootCanvas ? rootCanvas.worldCamera : null);
|
|
|
|
cachedLocalScale = rect.localScale;
|
|
GameDispatcher.Instance.AddListener(GameMsg.PuzzleIn, PlayHighlight);
|
|
overlay = transform.Find("mask_obj/light_obj").GetComponent<Image>();
|
|
}
|
|
void OnDestroy()
|
|
{
|
|
GameDispatcher.Instance.RemoveListener(GameMsg.PuzzleIn, PlayHighlight);
|
|
}
|
|
private Canvas GetRootCanvas(Transform t)
|
|
{
|
|
Canvas c = t.GetComponentInParent<Canvas>();
|
|
return c ? c.rootCanvas : null;
|
|
}
|
|
|
|
public void OnPointerDown(PointerEventData eventData)
|
|
{
|
|
if (locked || isFlying) return;
|
|
|
|
// 不在点击阶段切换父物体,避免点击瞬间跳位/消失
|
|
// 仅计算当前父Rect空间的指针偏移
|
|
Vector2 parentLocalPoint;
|
|
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
|
rect.parent as RectTransform,
|
|
eventData.position,
|
|
uiCamera,
|
|
out parentLocalPoint
|
|
);
|
|
pointerOffset = parentLocalPoint - rect.anchoredPosition;
|
|
}
|
|
|
|
// 替换你当前的 OnBeginDrag(保留核心逻辑,补回“中心锚点”)
|
|
public void OnBeginDrag(PointerEventData eventData)
|
|
{
|
|
if (locked || isFlying) return;
|
|
GameDispatcher.Instance.Dispatch(GameMsg.PuzzleTimeStart);
|
|
|
|
// 提升到根 Canvas,保持世界坐标
|
|
if (rootCanvas && rect.parent != rootCanvas.transform)
|
|
{
|
|
Vector3 worldPos = rect.position;
|
|
rect.SetParent(rootCanvas.transform, true);
|
|
rect.position = worldPos;
|
|
rect.localScale = cachedLocalScale;
|
|
|
|
// 关键:开始拖拽时把锚点/枢轴改为中心,但保持世界位置不变
|
|
SetAnchorPivotCenterKeepWorld(rect);
|
|
|
|
// 重新计算偏移(父变成根 Canvas)
|
|
Vector2 canvasLocalPoint;
|
|
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
|
rootCanvas.transform as RectTransform,
|
|
eventData.position,
|
|
uiCamera,
|
|
out canvasLocalPoint
|
|
);
|
|
pointerOffset = canvasLocalPoint - rect.anchoredPosition;
|
|
}
|
|
|
|
transform.SetAsLastSibling();
|
|
}
|
|
|
|
|
|
public void OnDrag(PointerEventData eventData)
|
|
{
|
|
if (locked || isFlying) return;
|
|
|
|
// 在当前父Rect空间移动(通常是根Canvas)
|
|
RectTransform parentRect = rect.parent as RectTransform;
|
|
Vector2 parentLocalPoint;
|
|
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
|
parentRect,
|
|
eventData.position,
|
|
uiCamera,
|
|
out parentLocalPoint
|
|
);
|
|
rect.anchoredPosition = parentLocalPoint - pointerOffset;
|
|
}
|
|
// 在类里新增一个辅助方法:将锚点/枢轴改为中心,但保持世界位置不变
|
|
private static void SetAnchorPivotCenterKeepWorld(RectTransform rt)
|
|
{
|
|
// 记录世界位置与旋转
|
|
Vector3 worldPos = rt.position;
|
|
Quaternion worldRot = rt.rotation;
|
|
|
|
// 改锚点与枢轴为中心
|
|
rt.anchorMin = new Vector2(0.5f, 0.5f);
|
|
rt.anchorMax = new Vector2(0.5f, 0.5f);
|
|
rt.pivot = new Vector2(0.5f, 0.5f);
|
|
|
|
// 恢复世界位置与旋转(避免位置跳变)
|
|
rt.position = worldPos;
|
|
rt.rotation = worldRot;
|
|
}
|
|
public void PlayHighlight(object obj)
|
|
{
|
|
|
|
|
|
Vector2 vec2 = (Vector2)obj;
|
|
float dist = Vector2.Distance(rect.position, vec2);
|
|
if (!locked && (dist > 0.1f)) return;
|
|
float delay = dist * 0.005f;
|
|
|
|
DOVirtual.DelayedCall(delay, () =>
|
|
{
|
|
// 清理旧动画
|
|
overlay.DOKill(true);
|
|
rect.DOKill(true);
|
|
|
|
// overlay 从透明开始
|
|
var c = overlay.color;
|
|
overlay.color = new Color(c.r, c.g, c.b, 0f);
|
|
|
|
// 高亮动画:淡入再淡出
|
|
overlay.DOFade(0.4f, 0.15f).SetEase(Ease.InOutSine)
|
|
.OnComplete(() =>
|
|
{
|
|
overlay.DOFade(0f, 0.15f).SetEase(Ease.InOutSine);
|
|
});
|
|
|
|
// 缩放动画:先缩小再放大
|
|
rect.localScale = cachedLocalScale;
|
|
rect.DOScale(cachedLocalScale * 0.99f, 0.3f).SetEase(Ease.InOutSine)
|
|
.OnComplete(() =>
|
|
{
|
|
rect.DOScale(cachedLocalScale, 0.3f).SetEase(Ease.InOutSine);
|
|
});
|
|
});
|
|
}
|
|
private ManualHorizontalList manualHorizontalList;
|
|
private void RefereshList()
|
|
{
|
|
if (manualHorizontalList == null) manualHorizontalList = GameObject.Find("puzzle_parent").GetComponent<ManualHorizontalList>();
|
|
manualHorizontalList.Refresh();
|
|
}
|
|
|
|
public void OnEndDrag(PointerEventData eventData)
|
|
{
|
|
if (locked || isFlying) return;
|
|
|
|
// 近吸附
|
|
float dist = Vector2.Distance(rect.anchoredPosition, snapTarget);
|
|
if (dist <= snapDistance)
|
|
{
|
|
rect.anchoredPosition = snapTarget;
|
|
Vector2 snapPos = rect.position;
|
|
GameDispatcher.Instance.Dispatch(GameMsg.PuzzleIn, snapPos);
|
|
rect.SetSiblingIndex(2);
|
|
if (lockOnSnap) locked = true;
|
|
AudioManager.Instance.PlayDynamicEffect(AudioConst.puzzle_click);
|
|
RefereshList();
|
|
return;
|
|
}
|
|
|
|
// 若释放在 ScrollRect viewport 内,归还到 content 并插入排序
|
|
if (scrollRect)
|
|
{
|
|
RectTransform viewport = scrollRect.viewport ? scrollRect.viewport : scrollRect.GetComponent<RectTransform>();
|
|
if (RectTransformUtility.RectangleContainsScreenPoint(viewport, eventData.position, eventData.pressEventCamera))
|
|
{
|
|
RectTransform content = scrollRect.content;
|
|
Vector3 worldPos = rect.position;
|
|
rect.SetParent(content, true); // 保持世界坐标再转本地
|
|
Vector2 localPoint = content.InverseTransformPoint(worldPos);
|
|
rect.anchoredPosition = localPoint;
|
|
|
|
int insertIndex = content.childCount;
|
|
for (int i = 0; i < content.childCount; i++)
|
|
{
|
|
RectTransform child = content.GetChild(i) as RectTransform;
|
|
if (localPoint.x < child.localPosition.x)
|
|
{
|
|
insertIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
rect.SetSiblingIndex(insertIndex);
|
|
rect.localScale = cachedLocalScale;
|
|
}
|
|
RefereshList();
|
|
}
|
|
|
|
}
|
|
|
|
// ---------------- 飞行吸附(忽略锁定即返回) ----------------
|
|
public void FlyAndSnap(float duration = 0.5f)
|
|
{
|
|
if (locked || isFlying) return;
|
|
isFlying = true;
|
|
GameDispatcher.Instance.Dispatch(GameMsg.PuzzleTimeStart);
|
|
|
|
FlyToSnapTarget(duration);
|
|
}
|
|
|
|
private void FlyToSnapTarget(float duration)
|
|
{
|
|
// 在根Canvas中移动
|
|
if (rootCanvas && rect.parent != rootCanvas.transform)
|
|
{
|
|
Vector3 worldPos = rect.position;
|
|
rect.SetParent(rootCanvas.transform, true);
|
|
rect.position = worldPos;
|
|
rect.localScale = cachedLocalScale;
|
|
}
|
|
|
|
// 关键:自动飞行时也改锚点/枢轴为中心,但保持世界位置不变
|
|
Vector3 savedWorldPos = rect.position;
|
|
Quaternion savedWorldRot = rect.rotation;
|
|
rect.anchorMin = rect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
rect.pivot = new Vector2(0.5f, 0.5f);
|
|
rect.position = savedWorldPos;
|
|
rect.rotation = savedWorldRot;
|
|
|
|
Vector2 startPos = rect.anchoredPosition;
|
|
Vector2 target = snapTarget;
|
|
|
|
// 如果不需要动画,直接完成
|
|
if (duration <= 0f || startPos == target)
|
|
{
|
|
rect.anchoredPosition = target;
|
|
DoneFly();
|
|
return;
|
|
}
|
|
|
|
// 使用 DOTween 动画
|
|
rect.DOAnchorPos(target, duration)
|
|
.SetEase(Ease.Linear)
|
|
.OnComplete(() =>
|
|
{
|
|
rect.anchoredPosition = target;
|
|
DoneFly();
|
|
});
|
|
}
|
|
|
|
private void DoneFly()
|
|
{
|
|
if (lockOnSnap) locked = true;
|
|
float dist = Vector2.Distance(rect.anchoredPosition, snapTarget);
|
|
|
|
rect.anchoredPosition = snapTarget;
|
|
Vector2 snapPos = rect.position;
|
|
GameDispatcher.Instance.Dispatch(GameMsg.PuzzleIn, snapPos);
|
|
AudioManager.Instance.PlayDynamicEffect(AudioConst.puzzle_click);
|
|
RefereshList();
|
|
isFlying = false;
|
|
}
|
|
// 外部可显式设置锁定
|
|
public void SetLocked(bool value) => locked = value;
|
|
}
|