Version: 2021.3+
드래그 앤 드롭은 UI 디자인의 흔한 기능입니다. UI 툴킷을 사용하여 커스텀 에디터 창 또는 Unity에서 빌드한 애플리케이션 내부에 드래그 앤 드롭 UI를 만들 수 있습니다. 다음 예시는 커스텀 에디터 창 내에서 드래그 앤 드롭 UI를 만드는 방법을 보여줍니다.
예시에서는 커스텀 에디터 창에서 여러 슬롯과 하나의 오브젝트를 추가합니다. 아래 보이는 대로 오브젝트를 모든 슬롯으로 드래그할 수 있습니다.
이 예시에서 생성한 완성된 파일은 GitHub 저장소에서 확인할 수 있습니다.
This guide is for developers familiar with the Unity Editor, UI Toolkit, and C# scripting. Before you start, get familiar with the following:
시작하려면 드래그 앤 드롭 UI를 담을 커스텀 에디터 창을 만듭니다.
Assets
에 DragAndDrop
이라는 폴더를 만들고 모든 파일을 저장합니다.DragAndDrop
폴더에서 마우스 오른쪽 버튼을 클릭하고 Create > UI Toolkit > Editor Window를 선택합니다.DragAndDropWindow
를 입력합니다.DragAndDropWindow.cs
를 열고 메뉴 이름과 창 제목을 Drag And Drop
으로 변경하고 기본 레이블에 대한 코드를 제거하여 UI를 더욱 사용자 친화적으로 만듭니다.완성된 DragAndDropWindow.cs
는 다음과 같이 작성되어야 합니다.
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
public class DragAndDropWindow : EditorWindow
{
[MenuItem("Window/UI Toolkit/Drag And Drop")]
public static void ShowExample()
{
DragAndDropWindow wnd = GetWindow<DragAndDropWindow>();
wnd.titleContent = new GUIContent("Drag And Drop");
}
public void CreateGUI()
{
// Each editor window contains a root VisualElement object
VisualElement root = rootVisualElement;
// Import UXML
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Drag and Drop/DragAndDropWindow.uxml");
VisualElement labelFromUXML = visualTree.Instantiate();
root.Add(labelFromUXML);
// A stylesheet can be added to a VisualElement.
// The style will be applied to the VisualElement and all of its children.
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Drag and Drop/DragAndDropWindow.uss");
}
}
다음으로 커스텀 창에 UI 컨트롤을 추가합니다.
DragAndDrop
폴더에서 DragAndDropWindow.uxml
을 더블 클릭하여 UI 빌더를 엽니다.
StyleSheet에서 Add Existing USS를 클릭하고 DragAndDropWindow.uss
를 선택합니다.
다음 VisualElement
UI 컨트롤을 추가합니다.
slots
이라는 슬롯 한 개에 slot_row1
과 slot_row2
라는 자식 슬롯이 있습니다. 각 행에는 slot1
과 slot2
라는 자식 슬롯이 각각 있습니다.slots
과 같은 수준에 object
라는 오브젝트가 한 개 있습니다. object
는 계층 구조에서 slots
다음에 와야 합니다.UI 컨트롤을 다음과 같이 스타일링합니다.
slot1
과 slot2
의 경우 흰색 배경에 둥근 모서리를 가진 80픽셀 X 80픽셀의 정사각형으로 스타일을 지정합니다. 각 행에 두 개의 슬롯이 있는 두 개의 행으로 슬롯을 정렬합니다.object
, style it as a 50px X 50px round spot with a black background color.Tip: To make your project more fun, you can use a background image for the object. You can find the image (Pouch.png) in the GitHub repository.
UI 컨트롤을 추가하고 스타일을 지정하는 방법에 대한 지침은 UI 빌더를 참조하십시오.
완성된 DragAndDropWindow.uxml
은 다음과 같이 작성되어야 합니다.
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<Style src="project://database/Assets/DragAndDrop/DragAndDropWindow.uss?fileID=7433441132597879392&guid=3d86870c8637c4a3c979a8b4fe0cba4c&type=3#DragAndDrop" />
<ui:VisualElement name="slots">
<ui:VisualElement name="slot_row1" class="slot_row">
<ui:VisualElement name="slot1" class="slot" />
<ui:VisualElement name="slot2" class="slot" />
</ui:VisualElement>
<ui:VisualElement name="slot_row2" class="slot_row">
<ui:VisualElement name="slot1" class="slot" />
<ui:VisualElement name="slot2" class="slot" />
</ui:VisualElement>
</ui:VisualElement>
<ui:VisualElement name="object" class="object" />
</ui:UXML>
완성된 DragAndDropWindow.uss
는 다음과 같이 작성되어야 합니다.
.slot {
width: 80px;
height: 80px;
margin: 5px;
background-color: rgb(255, 255, 255);
border-top-radius: 10px;
}
.slot_row {
flex-direction: row;
}
.object {
width: 50px;
height: 50px;
position: absolute;
left: 10px;
top: 10px;
border-radius: 30px;
background-color: rgb(0, 0, 0);
}
드래그 앤 드롭 동작을 정의하려면 PointerManipulator
클래스를 확장하고 로직을 정의합니다.
DragAndDrop
폴더에서 DragAndDropManipulator.cs
라는 또 다른 C# 파일을 만듭니다.
DragAndDropManipulator.cs
파일을 엽니다.
using UnityEngine.UIElements;
선언을 추가합니다.
DragAndDropManipulator
클래스가 MonoBehaviour
가 아닌 PointerManipulator
를 확장하도록 만들고 다음을 수행합니다.
RegisterCallbacksOnTarget()
메서드를 구현하여 필요한 모든 콜백을 등록합니다.UnregisterCallbacksOnTarget()
메서드를 구현하여 해당 콜백의 등록을 취소합니다.target
을 설정하고 시각적 트리의 루트에 대한 레퍼런스를 저장하는 생성자를 작성합니다.PointerDownEvent
, PointerMoveEvent
, PointerUpEvent
, PointerCaptureOutEvent
에 대한 콜백 역할을 하는 네 가지 메서드를 작성합니다.
PointerDownHandler()
: target
의 시작 위치와 포인터를 저장하고 target
이 포인터를 캡처하도록 하며 드래그가 현재 진행 중임을 나타냅니다.PointerMoveHandler()
: 드래그가 진행 중인지 target
이 포인터를 캡처했는지 확인합니다. 두 가지 모두 true이면 창의 경계 내에서 target
에 대한 새로운 위치를 계산합니다.PointerUpHandler()
: 드래그가 진행 중인지 target
이 포인터를 캡처했는지 확인합니다. 두 가지 모두 true이면 target
이 포인터를 해제합니다.PointerCaptureOutHandler()
: 드래그가 진행 중인지 확인합니다. true인 경우 시각적 트리의 루트를 쿼리하여 모든 슬롯을 찾고 target
과 가장 가까운 슬롯을 결정하고 target
이 해당 슬롯의 맨 위에 놓이도록 위치를 설정합니다. 겹치는 슬롯이 없으면 target
의 위치를 원래 위치로 다시 설정합니다.RegisterCallbacksOnTarget()
에서 이 네 가지 콜백을 target
에 등록합니다.
UnregisterCallbacksOnTarget()
에서 이 네 가지 콜백을 target
에서 등록 취소합니다.
완성된 DragAndDropManipulator.cs
는 다음과 같이 작성되어야 합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
public class DragAndDropManipulator : PointerManipulator
{
public DragAndDropManipulator(VisualElement target)
{
this.target = target;
root = target.parent;
}
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<PointerDownEvent>(PointerDownHandler);
target.RegisterCallback<PointerMoveEvent>(PointerMoveHandler);
target.RegisterCallback<PointerUpEvent>(PointerUpHandler);
target.RegisterCallback<PointerCaptureOutEvent>(PointerCaptureOutHandler);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<PointerDownEvent>(PointerDownHandler);
target.UnregisterCallback<PointerMoveEvent>(PointerMoveHandler);
target.UnregisterCallback<PointerUpEvent>(PointerUpHandler);
target.UnregisterCallback<PointerCaptureOutEvent>(PointerCaptureOutHandler);
}
private Vector2 targetStartPosition { get; set; }
private Vector3 pointerStartPosition { get; set; }
private bool enabled { get; set; }
private VisualElement root { get; }
private void PointerDownHandler(PointerDownEvent evt)
{
targetStartPosition = target.transform.position;
pointerStartPosition = evt.position;
target.CapturePointer(evt.pointerId);
enabled = true;
}
private void PointerMoveHandler(PointerMoveEvent evt)
{
if (enabled && target.HasPointerCapture(evt.pointerId))
{
Vector3 pointerDelta = evt.position - pointerStartPosition;
target.transform.position = new Vector2(
Mathf.Clamp(targetStartPosition.x + pointerDelta.x, 0, target.panel.visualTree.worldBound.width),
Mathf.Clamp(targetStartPosition.y + pointerDelta.y, 0, target.panel.visualTree.worldBound.height));
}
}
private void PointerUpHandler(PointerUpEvent evt)
{
if (enabled && target.HasPointerCapture(evt.pointerId))
{
target.ReleasePointer(evt.pointerId);
}
}
private void PointerCaptureOutHandler(PointerCaptureOutEvent evt)
{
if (enabled)
{
VisualElement slotsContainer = root.Q<VisualElement>("slots");
UQueryBuilder<VisualElement> allSlots =
slotsContainer.Query<VisualElement>(className: "slot");
UQueryBuilder<VisualElement> overlappingSlots =
allSlots.Where(OverlapsTarget);
VisualElement closestOverlappingSlot =
FindClosestSlot(overlappingSlots);
Vector3 closestPos = Vector3.zero;
if (closestOverlappingSlot != null)
{
closestPos = RootSpaceOfSlot(closestOverlappingSlot);
closestPos = new Vector2(closestPos.x - 5, closestPos.y - 5);
}
target.transform.position =
closestOverlappingSlot != null ?
closestPos :
targetStartPosition;
enabled = false;
}
}
private bool OverlapsTarget(VisualElement slot)
{
return target.worldBound.Overlaps(slot.worldBound);
}
private VisualElement FindClosestSlot(UQueryBuilder<VisualElement> slots)
{
List<VisualElement> slotsList = slots.ToList();
float bestDistanceSq = float.MaxValue;
VisualElement closest = null;
foreach (VisualElement slot in slotsList)
{
Vector3 displacement =
RootSpaceOfSlot(slot) - target.transform.position;
float distanceSq = displacement.sqrMagnitude;
if (distanceSq < bestDistanceSq)
{
bestDistanceSq = distanceSq;
closest = slot;
}
}
return closest;
}
private Vector3 RootSpaceOfSlot(VisualElement slot)
{
Vector2 slotWorldSpace = slot.parent.LocalToWorld(slot.layout.position);
return root.WorldToLocal(slotWorldSpace);
}
}
커스텀 창에서 드래그 앤 드롭을 활성화하려면 창이 열릴 때 인스턴스화합니다.
DragAndDropWindow.cs
에서 CreateGUI()
메서드에 다음을 추가하여 DragAndDropManipulator
클래스를 인스턴스화합니다.
DragAndDropManipulator manipulator =
new(rootVisualElement.Q<VisualElement>("object"));
From the menu bar, select Window > UI Toolkit > Drag And Drop. In the opened custom Editor window, you can drag the object into any slot.