[Unity] UI DragDrop Class
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 컴포넌트를 연결한다.
- 구성은 아래 그림을 참고하자.
5. 결과 화면
- DragDrop이 잘 되는 걸 확인할 수 있다.
댓글