using System; using UnityEngine; using UnityEngine.EventSystems; namespace UVC.UI { /// /// UI 요소를 마우스로 드래그하여 이동할 수 있게 만드는 컴포넌트입니다. /// IBeginDragHandler, IDragHandler, IEndDragHandler 인터페이스를 구현하여 드래그 이벤트를 처리합니다. /// 이 컴포넌트는 드래그될 UI 요소의 자식 오브젝트(예: 창의 헤더 영역)에 부착되어야 합니다. /// /// /// 사용 예제: /// 1. 드래그하고 싶은 창(Panel) UI를 만듭니다. /// 2. 해당 창의 자식으로 드래그 핸들 역할을 할 UI(예: Image)를 만들고, 이 오브젝트에 `UIDragger` 컴포넌트를 추가합니다. /// 3. 아래와 같은 컨트롤러 스크립트를 만들어 창에 추가하고, Inspector 창에서 `draggerHandle`과 `windowPanel`을 연결해줍니다. /// /// using UnityEngine; /// using UVC.UI; /// ///public class DraggableWindowController : MonoBehaviour ///{ /// [SerializeField] private UIDragger draggerHandle; /// [SerializeField] private RectTransform customDragArea; /// /// private void Start() /// { /// // 커스텀 드래그 영역 설정 /// if (customDragArea != null) /// { /// draggerHandle.SetDragArea(customDragArea); /// } /// /// // 이벤트 구독 /// draggerHandle.OnBeginDragHandler += OnDragStart; /// draggerHandle.OnDragHandler += OnDragging; /// draggerHandle.OnEndDragHandler += OnDragEnd; /// /// // 창을 중앙으로 이동 /// draggerHandle.CenterInDragArea(); /// } /// /// private void OnDragStart() /// { /// Debug.Log("창 드래그 시작!"); /// // 드래그 시작 시 추가 로직 /// } /// /// private void OnDragging(Vector2 position) /// { /// // 드래그 중 실시간 처리 /// Debug.Log($"드래그 중: {position}"); /// } /// /// private void OnDragEnd(Vector2 finalPosition) /// { /// Debug.Log($"드래그 완료! 최종 위치: {finalPosition}"); /// // 위치 저장 등의 후처리 /// } /// /// // 런타임에서 드래그 기능 제어 /// public void ToggleDragging() /// { /// draggerHandle.SetDraggingEnabled(!draggerHandle.enabled); /// } ///} /// /// public class UIDragger : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { [Header("드래그 설정")] [SerializeField] [Tooltip("드래그 가능한 영역을 지정합니다. null인 경우 Canvas를 자동으로 찾습니다.")] private RectTransform dragArea; // 드래그가 가능한 영역 [SerializeField] [Tooltip("드래그할 UI 요소를 지정합니다. null인 경우 부모를 자동으로 설정합니다.")] private RectTransform dragObject; // 실제로 드래그될 UI 요소 (예: 창 전체) [SerializeField] [Tooltip("드래그 시작 시 해당 UI 요소를 맨 앞으로 가져올지 여부를 설정합니다.")] private bool bringToFrontOnDrag = true; // 드래그 시작 시 맨 앞으로 가져올지 여부 [Header("드래그 영역 여백")] [SerializeField] [Tooltip("드래그 영역 하단에 추가할 여백입니다.")] private float bottomPadding = 0; [SerializeField] [Tooltip("드래그 영역 상단에 추가할 여백입니다.")] private float topPadding = 0; [SerializeField] [Tooltip("드래그 영역 좌측에 추가할 여백입니다.")] private float leftPadding = 0; [SerializeField] [Tooltip("드래그 영역 우측에 추가할 여백입니다.")] private float rightPadding = 0; [SerializeField] [Tooltip("드래그 중 실시간으로 영역 제한을 적용할지 여부")] private bool constrainDuringDrag = true; // 이벤트 public Action OnBeginDragHandler { get; set; } public Action OnDragHandler { get; set; } public Action OnEndDragHandler { get; set; } // 캐시된 변수들 private Vector2 originalLocalPointerPosition; private Vector2 originalAnchoredPosition; private int originalSiblingIndex; private Canvas parentCanvas; private Camera canvasCamera; // 프로퍼티 public RectTransform DragObject => dragObject; public RectTransform DragArea => dragArea; public bool IsDragging { private set; get; } private void Start() { InitializeComponents(); ValidateSetup(); } /// /// 컴포넌트들을 초기화합니다. /// private void InitializeComponents() { // dragObject가 설정되지 않았다면, 부모를 드래그 대상으로 설정 if (dragObject == null) { dragObject = transform.parent as RectTransform; } // Canvas와 Camera 캐싱 parentCanvas = GetComponentInParent(); if (parentCanvas != null) { canvasCamera = parentCanvas.worldCamera; } // dragArea가 설정되지 않았다면, Canvas를 드래그 영역으로 설정 if (dragArea == null && parentCanvas != null) { dragArea = parentCanvas.transform as RectTransform; } } /// /// 설정이 올바른지 검증합니다. /// private void ValidateSetup() { if (dragObject == null) { Debug.LogWarning("[UIDragger] dragObject를 찾을 수 없습니다. 부모가 RectTransform이 아닙니다.", this); enabled = false; return; } if (parentCanvas == null) { Debug.LogWarning("[UIDragger] Canvas를 찾을 수 없습니다. 드래그 기능이 제한될 수 있습니다.", this); } if (dragArea == null) { Debug.LogWarning("[UIDragger] dragArea를 찾을 수 없습니다. Canvas를 찾을 수 없습니다.", this); enabled = false; return; } } /// /// 드래그가 허용되는 영역을 설정합니다. /// /// 이 메서드를 활성화하면 지정된 영역 내에서 드래그 기능이 활성화됩니다. /// /// 드래그 영역의 경계를 정의하는 입니다. null일 수 없습니다. public void SetDragArea(RectTransform area) { if (area == null) { Debug.LogWarning("[UIDragger] null인 dragArea를 설정하려고 했습니다.", this); return; } dragArea = area; enabled = true; } /// /// 드래그 대상을 동적으로 설정합니다. /// public void SetDragObject(RectTransform target) { if (target == null) { Debug.LogWarning("[UIDragger] null인 dragObject를 설정하려고 했습니다.", this); return; } dragObject = target; } /// /// 드래그가 시작될 때 호출됩니다. (IBeginDragHandler) /// public void OnBeginDrag(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) return; if (!IsValidForDrag()) return; IsDragging = true; originalAnchoredPosition = dragObject.anchoredPosition; originalSiblingIndex = dragObject.GetSiblingIndex(); // 마우스 포인터의 로컬 위치 계산 RectTransformUtility.ScreenPointToLocalPointInRectangle( dragArea, eventData.position, canvasCamera, out originalLocalPointerPosition); // 맨 앞으로 가져오기 if (bringToFrontOnDrag) { dragObject.SetAsLastSibling(); } OnBeginDragHandler?.Invoke(); } /// /// 드래그 중일 때 매 프레임 호출됩니다. (IDragHandler) /// public void OnDrag(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) return; if (!IsDragging || !IsValidForDrag()) return; if (RectTransformUtility.ScreenPointToLocalPointInRectangle( dragArea, eventData.position, canvasCamera, out Vector2 localPointerPosition)) { Vector2 offsetToOriginal = localPointerPosition - originalLocalPointerPosition; Vector2 newPosition = originalAnchoredPosition + offsetToOriginal; //Debug.Log($"OnDrag originalAnchoredPosition:{originalAnchoredPosition}, newPosition:{newPosition}"); // 실시간 제약 적용 if (constrainDuringDrag) newPosition = ClampToArea(newPosition); //Debug.Log($"OnDrag2 newPosition:{newPosition}"); dragObject.anchoredPosition = newPosition; OnDragHandler?.Invoke(newPosition); } } /// /// 드래그가 끝났을 때 호출됩니다. (IEndDragHandler) /// public void OnEndDrag(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) return; if (!IsDragging) return; IsDragging = false; // 원래 형제 순서로 복원 if (bringToFrontOnDrag) { dragObject.SetSiblingIndex(originalSiblingIndex); } // 최종 위치 제약 적용 //Vector2 finalPosition = ClampToArea(dragObject.anchoredPosition); //dragObject.anchoredPosition = finalPosition; //OnEndDragHandler?.Invoke(finalPosition); } /// /// 드래그가 가능한 상태인지 확인합니다. /// private bool IsValidForDrag() { return dragObject != null && dragArea != null && enabled; } /// /// UI 요소가 드래그 영역 내에 있도록 위치를 제한합니다. /// private Vector2 ClampToArea(Vector2 position) { if (dragArea == null || dragObject == null) return position; Rect dragObjectRect = dragObject.rect; Rect dragAreaRect = dragArea.rect; dragAreaRect.x = 0; dragAreaRect.y = 0; // Pivot과 앵커를 고려한 경계 계산 Vector2 pivot = dragObject.pivot; Vector2 size = dragObjectRect.size; //아래로 내려갈수록 좌상(0,0), 우하(1,-1) float leftBoundary = dragAreaRect.xMin + (size.x * pivot.x) + leftPadding; float rightBoundary = dragAreaRect.xMax - (size.x * (1f - pivot.x)) - rightPadding; float bottomBoundary = -dragAreaRect.height + (size.y * pivot.y) + bottomPadding; float topBoundary = -(size.y * (1f - pivot.y)) - topPadding; position.x = Mathf.Clamp(position.x, leftBoundary, rightBoundary); position.y = Mathf.Clamp(position.y, bottomBoundary, topBoundary); return position; } /// /// 드래그 객체를 특정 위치로 이동시킵니다. /// public void SetPosition(Vector2 position, bool clampToArea = true) { if (dragObject == null) return; if (clampToArea) { position = ClampToArea(position); } dragObject.anchoredPosition = position; } /// /// 드래그 객체를 중앙으로 이동시킵니다. /// public void CenterInDragArea() { if (dragArea == null || dragObject == null) return; Vector2 centerPosition = dragArea.rect.center; SetPosition(centerPosition, false); } /// /// 드래그 기능을 활성화/비활성화합니다. /// public void SetDraggingEnabled(bool enabled) { this.enabled = enabled; } } }