본문 바로가기

[Unity] UI DragDrop Class

앤디가이 2022. 6. 8.

Unity UI에서 자주 사용하는 DragDrop 기능을 구현해보자.

 

Unity에서 미니 2D Game 기능 중 DragDrop 기능은 상당히 많이 사용한다. Unity에서 2D Image를 마우스(터치)를 통해 Drag 하고 원하는 위치에 Drop 할 수 있는 클래스를 제작해 보자.

 

 

1. 사전 준비 - LeanTween 플러그인 설치

 - Unity에서 Tween 기능 구현을 위해 LeanTween 라이브러리 에셋을 사용하겠다.

 - LeanTween은 에셋스토어를 통해서 무료로 다운로드할 수 있다.

 - Drop존에 가까이 왔을 때 자동으로 Drop존으로 Tween 하도록 할 때 해당 라이브러리 기능을 사용한다.

 - 다운로드 받으러 가기

 

2. UIDragBehaviour Class 작성

 - UIDragBehavior Class는 Drag 해야 될 Image 객체에 붙어야 하는 컴포넌트이다.

 - 해당 클래스가 붙어 있는 객체는 마우스 또는 터치를 통해 Drag가 가능하게 된다.

 - UnityEngine.EventSystems의 IBeginDragHandler, IDragHandler, IEndDragHandler 인터페이스를 활용하여 제작해보자.

 - 해당 클래스는 Image컴포넌트와 CanvasGroup 컴포넌트를 필수로 필요로 한다.

 - Scripts 폴더를 만든 후 UIDragBehaviour.cs C# Script를 생성하고 아래와 같이 작성하자.

using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace UI
{
    
    [RequireComponent(typeof(Image))]
    [RequireComponent(typeof(CanvasGroup))]
    public class UIDragBehaviour : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
    {
        [System.Serializable]
        public class DropEvent : UnityEvent<UIDropzone> { }

        protected RectTransform rTfm_this;
        protected Vector2 v2_originPos;
        protected Transform tfm_originParent;

        [SerializeField]
        protected UIDropzone[] arr_successZones = null, arr_failZones = null;
        protected UIDropzone obj_curDropZone = null;

        [SerializeField]
        protected UnityEvent obj_beginDrag = null;
        [SerializeField]
        protected DropEvent obj_success = null, obj_fail = null;

        [SerializeField]
        [Range(0.1f, 1f)]
        protected float fMoveReturnTime = 0.4f;

        [SerializeField]
        [Range(0.1f, 1f)]
        protected float fMoveMagnetTime = 0.2f;

        protected bool DragPossible { get { return GetComponent<CanvasGroup>().blocksRaycasts; } }

        public int Index { get; set; }

        [SerializeField]
        protected LeanTweenType obj_MotionType = LeanTweenType.easeInOutSine;


        protected virtual void Awake()
        {
            rTfm_this = GetComponent<RectTransform>();
            v2_originPos = rTfm_this.anchoredPosition;
            tfm_originParent = rTfm_this.parent;

            if (!GetComponent<CanvasGroup>())
                gameObject.AddComponent<CanvasGroup>();

        }

        public void SetListener(UnityAction onBeginDrag, UnityAction<UIDropzone> onSuccess, UnityAction<UIDropzone> onFail)
        {
            obj_beginDrag = new UnityEvent();
            obj_beginDrag.AddListener(onBeginDrag);
            obj_success = new DropEvent();
            obj_success.AddListener(onSuccess);
            obj_fail = new DropEvent();
            obj_fail.AddListener(onFail);
        }

		public void AddSuccessDropZone(UIDropzone zone)
        { 
			UIDropzone[] dropzones = new UIDropzone[arr_successZones.Length+1];
			arr_successZones.CopyTo(dropzones, 0);
			dropzones[arr_successZones.Length] = zone;
			arr_successZones = dropzones;
		}


        public virtual void OnBeginDrag(PointerEventData eventData)
        {
            if (obj_beginDrag != null) obj_beginDrag.Invoke();

            SetDragEnable(false);

            transform.SetAsLastSibling();

            if (obj_curDropZone != null)
            {
                obj_curDropZone.UnsetObject();
                obj_curDropZone = null;
            }

            UpdateDragPosition(eventData);
        }

        protected virtual void UpdateDragPosition(PointerEventData data)
        {
            Vector3 globalMousePos;
            if (RectTransformUtility.ScreenPointToWorldPointInRectangle(rTfm_this, data.position, data.pressEventCamera, out globalMousePos))
            {
                rTfm_this.position = globalMousePos;
                rTfm_this.rotation = rTfm_this.rotation;
            }
        }

        public virtual void OnDrag(PointerEventData eventData)
        {
            UpdateDragPosition(eventData);
        }

        public virtual void OnEndDrag(PointerEventData eventData)
        {
            for (int i = 0; i < arr_successZones.Length; i++)
            {
                if (arr_successZones[i].CheckDrop(rTfm_this))
                {
                
                    //이미 드래그 오브젝트가 붙어 있는 경우, 붙어있는 오브젝트는 원위치로 이동
                    if (arr_successZones[i].IsFill)
                    {
                        arr_successZones[i].DragBehaviour.MoveToOrinPosition();
                    }

                    LeanTween.move(rTfm_this.gameObject, arr_successZones[i].transform.position, fMoveMagnetTime).setOnComplete(() =>
                   {
                       SetDragEnable(true);
                       obj_curDropZone = arr_successZones[i];

                       arr_successZones[i].DropObject(rTfm_this);

                       if (obj_success != null) obj_success.Invoke(arr_successZones[i]);


                    }).setEase(obj_MotionType);

                    return;
                }
            }


            MoveToOrinPosition();


            UIDropzone fail = null;
            for (int i = 0; i < arr_failZones.Length; i++)
            {
                if (arr_failZones[i].CheckDrop(rTfm_this))
                {
                    fail = arr_failZones[i];
                    break;
                }
            }
            if (obj_fail != null)
            {
                if (fail != null)
                {
                    fail.CheckDrop(rTfm_this);
                }

                obj_fail.Invoke(fail);
            }
        }

        public virtual void MoveToOrinPosition(bool isMotion = true)
        {
            if (obj_curDropZone != null)
            {
                obj_curDropZone.UnsetObject();
                obj_curDropZone = null;
            }

            rTfm_this.SetParent(tfm_originParent, true);

            if (isMotion)
            {
				LeanTween.move(rTfm_this, v2_originPos, fMoveReturnTime).setEase(obj_MotionType).setOnComplete(OnReturn);
            }
            else
            {
                rTfm_this.anchoredPosition = v2_originPos;
				OnReturn();
            }
        }
		protected virtual void OnReturn(){
			SetDragEnable(true);
		}


        public UIDropzone GetSuccessDropZone(int index)
        {
            if (arr_successZones.Length > index) return arr_successZones[index];
            return null;
        }

        public void SetDragEnable(bool enable)
        {
            GetComponent<CanvasGroup>().blocksRaycasts = enable;
        }

    }

}

 

3. UIDropzone Class 작성

 - UIDropzone Class는 Drop 돼야 할 Image 객체에 붙어야 하는 컴포넌트이다.

 - UIDragBehaviour를 가지고 있는 객체가 UIDropzone이 붙어 있는 객체에 Drop 될 경우 Drop이 성공하게 된다.

 - Drop에 대한 체크는 거리로 체크하는 방법과 이미지 겹침으로 체크하는 방법을 제공한다.

 - 한번 Drop이 된 후, 다른 드래그 오브젝트가 Drop을 시도할 경우 기존 Drop 되어 있는 오브젝트를 교체할 건지 여부도 선택할 수 있다.

 - Scripts 폴더 내 UIDropzone.cs C# Script를 생성하고 아래와 같이 작성하자.

using UnityEngine;
using UnityEngine.Events;

namespace UI
{
    public enum DropZoneType
    {
        //한번 드랍된 객체를 다른 드래그 객체로 교체 가능한 타입
        Replaceable,
        //한번 드랍된 객체는 교체 불가
        NotReplaceable
    }

    public enum DropCheckType
    {
        //거리로 체크
        Distance,
        //이미지 겹침으로 체크
        Overlap
    }

    public class UIDropzone : MonoBehaviour
    {
        [SerializeField]
        private DropZoneType obj_DropZoneType = DropZoneType.Replaceable;

        [SerializeField]
        private DropCheckType obj_checkType = DropCheckType.Distance;

        [SerializeField]
        [Range(1, 10)]
        private float fCheckDistance = 1f; //DropCheckType 이 Distance 타입일 경우 사용

        [SerializeField]
        UnityEvent obj_dropped = null;

        private RectTransform rTfm_this;

        private RectTransform rTfm_fill;

        public int Index { get; set; }

        public bool IsFill
        {
            get { return rTfm_fill != null; }
        }
        public RectTransform FillRect
        {
            get { return rTfm_fill; }
        }
        public UIDragBehaviour DragBehaviour
        {
            get
            {
                if (FillRect)
                    if (FillRect.GetComponent<UIDragBehaviour>())
                        return FillRect.GetComponent<UIDragBehaviour>();
                return null;
            }
        }

        void Awake()
        {
            rTfm_this = GetComponent<RectTransform>();
        }

        /// <summary>
        /// Adds the drop listener.
        /// </summary>
        /// <param name="dropped">Dropped.</param>
        public void AddDropListener(UnityAction dropped)
        {
            obj_dropped.AddListener(dropped);
        }


        public virtual bool CheckDrop(RectTransform itemRect)
        {
            if (itemRect == null || rTfm_this == null) return false;

            switch (obj_checkType)
            {

                case DropCheckType.Distance:
                    {
                        float distance = Vector2.Distance(itemRect.position, rTfm_this.position);

                        if (distance < fCheckDistance)
                        {
                            if (IsFill && obj_DropZoneType == DropZoneType.NotReplaceable)
                                return false;
                            else
                                return true;
                        }
                        break;
                    }
                case DropCheckType.Overlap:
                    {
                        if (IsOverlaps(rTfm_this, itemRect))
                        {
                            if (IsFill && obj_DropZoneType == DropZoneType.NotReplaceable)
                                return false;
                            else
                                return true;
                        }
                        break;
                    }
            }

            return false;
        }


        public void DropObject(RectTransform itemRect)
        {
            rTfm_fill = itemRect;

            if (DragBehaviour != null)
                if (obj_DropZoneType == DropZoneType.NotReplaceable)
                    DragBehaviour.SetDragEnable(false);

            if (obj_dropped != null) obj_dropped.Invoke();

        }

        public void UnsetObject()
        {
            rTfm_fill = null;
        }

        protected bool IsOverlaps(RectTransform rectTrans1, RectTransform rectTrans2)
        {
            Rect rect1 = new Rect(rectTrans1.localPosition.x, rectTrans1.localPosition.y, rectTrans1.rect.width, rectTrans1.rect.height);
            Rect rect2 = new Rect(rectTrans2.localPosition.x, rectTrans2.localPosition.y, rectTrans2.rect.width, rectTrans2.rect.height);

            return rect1.Overlaps(rect2);
        }

    }
}

 

4. 연결 및 테스트

 - Scene에서 UI Canvas를 생성 후 Panel을 하나 만든 후 이름을 DragDrop으로 변경해준다.

 - DragDrop Panel 밑으로 Image 오브젝트를 생성한다.

 - Drag가 필요한 객체는 DragBehaviour로 이름을 변경 후 UIDragBehaviour 컴포넌트를 연결한다.

 - Drop이 필요한 객체는 Dropzone으로 이름 변경 후 UIDropzone 컴포넌트를 연결한다.

 - 구성은 아래 그림을 참고하자.

UIDropzone
UIDropzone
UIDragBehaviour
UIDragBehaviour

 

5. 결과 화면

 - DragDrop이 잘 되는 걸 확인할 수 있다.

댓글