535 lines
15 KiB
C#
535 lines
15 KiB
C#
using CustomComponent;
|
|
using DG.Tweening;
|
|
using Definition.Enum;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
|
|
namespace UI
|
|
{
|
|
/// <summary>
|
|
/// 可拖拽拼装部件:负责拖拽交互、出生点回退与放置到槽位。
|
|
/// </summary>
|
|
[RequireComponent(typeof(RectTransform))]
|
|
public class CombineDraggablePart : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerEnterHandler, IPointerExitHandler
|
|
{
|
|
#region Inspector Config
|
|
|
|
[SerializeField] private CombinePartType _partType = CombinePartType.Dou;
|
|
|
|
[SerializeField] private bool _lockAfterPlaced = true;
|
|
|
|
[SerializeField] private RectTransform _rectTransform;
|
|
|
|
[SerializeField] private CanvasGroup _canvasGroup;
|
|
|
|
[SerializeField] private Image _partImage;
|
|
|
|
[SerializeField] private float _defaultScaleMultiplier = 0.8f;
|
|
|
|
[SerializeField] private float _hoverScaleMultiplier = 1f;
|
|
|
|
[SerializeField] private float _hoverTweenDuration = 0.15f;
|
|
|
|
[SerializeField] private Ease _hoverTweenEase = Ease.OutQuad;
|
|
|
|
[SerializeField] private float _returnToSpawnDuration = 0.4f;
|
|
|
|
[SerializeField] private Ease _returnToSpawnEase = Ease.OutCubic;
|
|
|
|
#endregion
|
|
|
|
#region Spawn State
|
|
|
|
private bool _spawnStateCached;
|
|
|
|
private Transform _spawnParent;
|
|
|
|
private Vector3 _spawnWorldPosition = Vector3.zero;
|
|
|
|
private Quaternion _spawnWorldRotation = Quaternion.identity;
|
|
|
|
private Vector3 _spawnWorldScale = Vector3.one;
|
|
|
|
private int _spawnSiblingIndex;
|
|
|
|
#endregion
|
|
|
|
#region Runtime State
|
|
|
|
private CombineComponent _controller;
|
|
|
|
private CombineSlot _currentSlot;
|
|
|
|
private bool _isPlaced;
|
|
|
|
private bool _isLocked;
|
|
|
|
private bool _isDragging;
|
|
|
|
private Vector3 _originalLocalScale = Vector3.one;
|
|
|
|
private Tween _scaleTween;
|
|
|
|
private Tween _returnTween;
|
|
|
|
private Vector2 _dragPointerOffset = Vector2.zero;
|
|
|
|
#endregion
|
|
|
|
#region Public Query
|
|
|
|
public CombinePartType PartType => _partType;
|
|
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
|
|
private void Awake()
|
|
{
|
|
if (_rectTransform == null)
|
|
{
|
|
_rectTransform = transform as RectTransform;
|
|
}
|
|
|
|
if (_canvasGroup == null)
|
|
{
|
|
_canvasGroup = gameObject.GetOrAddComponent<CanvasGroup>();
|
|
}
|
|
|
|
_originalLocalScale = _rectTransform.localScale;
|
|
ApplyScaleImmediate(_defaultScaleMultiplier);
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
KillScaleTween();
|
|
KillReturnTween();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Setup
|
|
|
|
/// <summary>
|
|
/// 绑定玩法控制器。
|
|
/// </summary>
|
|
public void BindController(CombineComponent controller)
|
|
{
|
|
_controller = controller;
|
|
}
|
|
|
|
public void ConfigureRuntime(CombinePartType partType, Sprite sprite)
|
|
{
|
|
_partType = partType;
|
|
|
|
if (_partImage == null)
|
|
{
|
|
_partImage = GetComponentInChildren<Image>(true);
|
|
}
|
|
|
|
if (_partImage != null)
|
|
{
|
|
_partImage.sprite = sprite;
|
|
if (sprite != null)
|
|
{
|
|
_partImage.SetNativeSize();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 缓存出生点状态(仅首次缓存)。
|
|
/// </summary>
|
|
public void CacheSpawnState()
|
|
{
|
|
if (_spawnStateCached)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_spawnStateCached = true;
|
|
_spawnParent = _rectTransform.parent;
|
|
_spawnWorldPosition = _rectTransform.position;
|
|
_spawnWorldRotation = _rectTransform.rotation;
|
|
_spawnWorldScale = _rectTransform.localScale;
|
|
_spawnSiblingIndex = _rectTransform.GetSiblingIndex();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重置部件到出生点,并清理槽位占用状态。
|
|
/// </summary>
|
|
public void ResetToSpawn()
|
|
{
|
|
ClearSlotOccupancy();
|
|
_isDragging = false;
|
|
_isPlaced = false;
|
|
_isLocked = false;
|
|
ReturnToSpawn();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Drag Flow
|
|
|
|
/// <summary>
|
|
/// 开始拖拽:关闭射线阻挡并切到拖拽层级。
|
|
/// </summary>
|
|
public void OnBeginDrag(PointerEventData eventData)
|
|
{
|
|
if (!CanStartDrag())
|
|
{
|
|
return;
|
|
}
|
|
|
|
KillReturnTween();
|
|
_isDragging = true;
|
|
_canvasGroup.blocksRaycasts = false;
|
|
MoveToDragRoot();
|
|
_rectTransform.SetAsLastSibling();
|
|
CacheDragOffset(eventData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 拖拽中:按屏幕坐标映射到父节点局部坐标更新位置。
|
|
/// </summary>
|
|
public void OnDrag(PointerEventData eventData)
|
|
{
|
|
if (!CanDrag(eventData))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (TryGetDragParent(out RectTransform dragParent) &&
|
|
TryGetPointerLocalPosition(eventData, dragParent, out Vector2 pointerLocalPosition))
|
|
{
|
|
_rectTransform.anchoredPosition = pointerLocalPosition + _dragPointerOffset;
|
|
return;
|
|
}
|
|
|
|
_rectTransform.anchoredPosition += eventData.delta;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 结束拖拽:恢复射线阻挡;未成功放置则回到出生点。
|
|
/// </summary>
|
|
public void OnEndDrag(PointerEventData eventData)
|
|
{
|
|
_isDragging = false;
|
|
|
|
if (_isLocked)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_canvasGroup.blocksRaycasts = true;
|
|
|
|
if (!_isPlaced)
|
|
{
|
|
if (ShouldKeepDroppedInPartArea(eventData))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ReturnToSpawnAnimated();
|
|
}
|
|
}
|
|
|
|
public void OnPointerEnter(PointerEventData eventData)
|
|
{
|
|
if (_isLocked || _isPlaced || _isDragging)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PlayScaleTween(_hoverScaleMultiplier);
|
|
}
|
|
|
|
public void OnPointerExit(PointerEventData eventData)
|
|
{
|
|
if (_isLocked || _isPlaced || _isDragging)
|
|
{
|
|
return;
|
|
}
|
|
|
|
PlayScaleTween(_defaultScaleMultiplier);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 校验是否允许开始拖拽。
|
|
/// </summary>
|
|
private bool CanStartDrag()
|
|
{
|
|
return !_isLocked && !_isPlaced;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 校验拖拽中是否可更新位置。
|
|
/// </summary>
|
|
private bool CanDrag(PointerEventData eventData)
|
|
{
|
|
return !_isLocked && !_isPlaced && eventData != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 将部件切到控制器提供的拖拽根节点。
|
|
/// </summary>
|
|
private void MoveToDragRoot()
|
|
{
|
|
if (_controller == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Transform dragRoot = _controller.GetDragRoot();
|
|
if (dragRoot != null)
|
|
{
|
|
_rectTransform.SetParent(dragRoot, true);
|
|
}
|
|
}
|
|
|
|
private void CacheDragOffset(PointerEventData eventData)
|
|
{
|
|
_dragPointerOffset = Vector2.zero;
|
|
|
|
if (!TryGetDragParent(out RectTransform dragParent))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (TryGetPointerLocalPosition(eventData, dragParent, out Vector2 pointerLocalPosition))
|
|
{
|
|
_dragPointerOffset = _rectTransform.anchoredPosition - pointerLocalPosition;
|
|
}
|
|
}
|
|
|
|
private bool TryGetDragParent(out RectTransform dragParent)
|
|
{
|
|
dragParent = _rectTransform.parent as RectTransform;
|
|
return dragParent != null;
|
|
}
|
|
|
|
private bool TryGetPointerLocalPosition(PointerEventData eventData, RectTransform targetRect,
|
|
out Vector2 localPosition)
|
|
{
|
|
localPosition = Vector2.zero;
|
|
if (eventData == null || targetRect == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Camera eventCamera = ResolveEventCamera(eventData);
|
|
return RectTransformUtility.ScreenPointToLocalPointInRectangle(targetRect, eventData.position, eventCamera,
|
|
out localPosition);
|
|
}
|
|
|
|
private Camera ResolveEventCamera(PointerEventData eventData)
|
|
{
|
|
if (eventData != null)
|
|
{
|
|
if (eventData.pressEventCamera != null)
|
|
{
|
|
return eventData.pressEventCamera;
|
|
}
|
|
|
|
if (eventData.enterEventCamera != null)
|
|
{
|
|
return eventData.enterEventCamera;
|
|
}
|
|
}
|
|
|
|
Canvas canvas = GetComponentInParent<Canvas>();
|
|
if (canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay)
|
|
{
|
|
return canvas.worldCamera;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private bool ShouldKeepDroppedInPartArea(PointerEventData eventData)
|
|
{
|
|
if (eventData == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!TryGetDragArea(out RectTransform dragArea))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Camera eventCamera = ResolveEventCamera(eventData);
|
|
if (!RectTransformUtility.RectangleContainsScreenPoint(dragArea, eventData.position, eventCamera))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ClampInsideDragArea(dragArea);
|
|
return true;
|
|
}
|
|
|
|
private bool TryGetDragArea(out RectTransform dragArea)
|
|
{
|
|
dragArea = null;
|
|
if (_controller == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
dragArea = _controller.GetDragRoot() as RectTransform;
|
|
return dragArea != null;
|
|
}
|
|
|
|
private void ClampInsideDragArea(RectTransform dragArea)
|
|
{
|
|
if (dragArea == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector2 current = _rectTransform.anchoredPosition;
|
|
Rect areaRect = dragArea.rect;
|
|
Rect partRect = _rectTransform.rect;
|
|
Vector2 pivot = _rectTransform.pivot;
|
|
Vector3 scale = _rectTransform.localScale;
|
|
|
|
float width = Mathf.Abs(partRect.width * scale.x);
|
|
float height = Mathf.Abs(partRect.height * scale.y);
|
|
float minX = areaRect.xMin + width * Mathf.Clamp01(pivot.x);
|
|
float maxX = areaRect.xMax - width * (1f - Mathf.Clamp01(pivot.x));
|
|
float minY = areaRect.yMin + height * Mathf.Clamp01(pivot.y);
|
|
float maxY = areaRect.yMax - height * (1f - Mathf.Clamp01(pivot.y));
|
|
|
|
if (minX > maxX)
|
|
{
|
|
float centerX = (areaRect.xMin + areaRect.xMax) * 0.5f;
|
|
minX = centerX;
|
|
maxX = centerX;
|
|
}
|
|
|
|
if (minY > maxY)
|
|
{
|
|
float centerY = (areaRect.yMin + areaRect.yMax) * 0.5f;
|
|
minY = centerY;
|
|
maxY = centerY;
|
|
}
|
|
|
|
current.x = Mathf.Clamp(current.x, minX, maxX);
|
|
current.y = Mathf.Clamp(current.y, minY, maxY);
|
|
_rectTransform.anchoredPosition = current;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Placement
|
|
|
|
/// <summary>
|
|
/// 将部件恢复到出生点状态。
|
|
/// </summary>
|
|
public void ReturnToSpawn()
|
|
{
|
|
KillReturnTween();
|
|
|
|
if (_spawnParent == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_rectTransform.SetParent(_spawnParent, true);
|
|
_rectTransform.position = _spawnWorldPosition;
|
|
_rectTransform.rotation = _spawnWorldRotation;
|
|
_rectTransform.localScale = _spawnWorldScale;
|
|
_rectTransform.SetSiblingIndex(_spawnSiblingIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 应用成功放置到槽位后的表现与状态。
|
|
/// </summary>
|
|
internal void PlaceToSlot(CombineSlot slot)
|
|
{
|
|
KillScaleTween();
|
|
KillReturnTween();
|
|
_isDragging = false;
|
|
_currentSlot = slot;
|
|
_isPlaced = true;
|
|
_isLocked = _lockAfterPlaced;
|
|
_canvasGroup.blocksRaycasts = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 清理当前槽位占用记录。
|
|
/// </summary>
|
|
private void ClearSlotOccupancy()
|
|
{
|
|
if (_currentSlot != null)
|
|
{
|
|
_currentSlot.ClearOccupiedPart(this);
|
|
_currentSlot = null;
|
|
}
|
|
}
|
|
|
|
private void ApplyScaleImmediate(float multiplier)
|
|
{
|
|
_rectTransform.localScale = _originalLocalScale * Mathf.Max(0f, multiplier);
|
|
}
|
|
|
|
private void PlayScaleTween(float targetMultiplier)
|
|
{
|
|
KillScaleTween();
|
|
_scaleTween = _rectTransform
|
|
.DOScale(_originalLocalScale * Mathf.Max(0f, targetMultiplier), _hoverTweenDuration)
|
|
.SetEase(_hoverTweenEase)
|
|
.SetUpdate(true);
|
|
}
|
|
|
|
public void ReturnToSpawnAnimated()
|
|
{
|
|
if (_spawnParent == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_returnToSpawnDuration <= 0f)
|
|
{
|
|
ReturnToSpawn();
|
|
return;
|
|
}
|
|
|
|
KillReturnTween();
|
|
_rectTransform.SetParent(_spawnParent, true);
|
|
_rectTransform.SetSiblingIndex(_spawnSiblingIndex);
|
|
|
|
_returnTween = DOTween.Sequence()
|
|
.Append(_rectTransform.DOMove(_spawnWorldPosition, _returnToSpawnDuration))
|
|
.Join(_rectTransform.DORotateQuaternion(_spawnWorldRotation, _returnToSpawnDuration))
|
|
.Join(_rectTransform.DOScale(_spawnWorldScale, _returnToSpawnDuration))
|
|
.SetEase(_returnToSpawnEase)
|
|
.SetUpdate(true)
|
|
.OnKill(() => _returnTween = null);
|
|
}
|
|
|
|
private void KillScaleTween()
|
|
{
|
|
if (_scaleTween != null && _scaleTween.IsActive())
|
|
{
|
|
_scaleTween.Kill();
|
|
}
|
|
|
|
_scaleTween = null;
|
|
}
|
|
|
|
private void KillReturnTween()
|
|
{
|
|
if (_returnTween != null && _returnTween.IsActive())
|
|
{
|
|
_returnTween.Kill();
|
|
}
|
|
|
|
_returnTween = null;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|