Version: 2021.3
언어: 한국어
런타임용 탭 메뉴 생성
마이그레이션 가이드

커스텀 에디터 창 내부에 드래그 앤 드롭 UI 생성

드래그 앤 드롭은 UI 디자인의 흔한 기능입니다. UI 툴킷을 사용하여 커스텀 에디터 창 또는 Unity에서 빌드한 애플리케이션 내부에 드래그 앤 드롭 UI를 만들 수 있습니다. 다음 예시는 커스텀 에디터 창 내에서 드래그 앤 드롭 UI를 만드는 방법을 보여줍니다.

개요 예시

예시에서는 커스텀 에디터 창에서 여러 슬롯과 하나의 오브젝트를 추가합니다. 아래 보이는 대로 오브젝트를 모든 슬롯으로 드래그할 수 있습니다.

드래그 앤 드롭 UI 미리보기
드래그 앤 드롭 UI 미리보기

이 예시에서 생성한 완성된 파일은 GitHub 저장소에서 확인할 수 있습니다.

선행 조건

이 고급 예시는 Unity 에디터, UI 툴킷, C# 스크립팅에 익숙한 개발자용입니다. 다음 개념에 대한 기본적인 이해가 있는 것이 좋습니다.

커스텀 에디터 창 생성

시작하려면 드래그 앤 드롭 UI를 담을 커스텀 에디터 창을 만듭니다.

  1. 템플릿을 사용하여 Unity에서 프로젝트를 만듭니다.
  2. AssetsDragAndDrop이라는 폴더를 만들고 모든 파일을 저장합니다.
  3. DragAndDrop 폴더에서 마우스 오른쪽 버튼을 클릭하고 Create > UI Toolkit > Editor Window를 선택합니다.
  4. UI Toolkit Editor Window Creator에서 DragAndDropWindow를 입력합니다.
  5. Confirm을 클릭합니다. 이렇게 하면 커스텀 창에 대한 C# 스크립트, UXML, USS 파일이 자동으로 생성됩니다.
  6. 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 컨트롤을 추가합니다.

  1. DragAndDrop 폴더에서 DragAndDropWindow.uxml을 더블 클릭하여 UI 빌더를 엽니다.

  2. StyleSheet에서 Add Existing USS를 클릭하고 DragAndDropWindow.uss를 선택합니다.

  3. 다음 VisualElement UI 컨트롤을 추가합니다.

    • slots이라는 슬롯 한 개에 slot_row1slot_row2라는 자식 슬롯이 있습니다. 각 행에는 slot1slot2라는 자식 슬롯이 각각 있습니다.
    • slots과 같은 수준에 object라는 오브젝트가 한 개 있습니다. object계층 구조에서 slots 다음에 와야 합니다.
  4. UI 컨트롤을 다음과 같이 스타일링합니다.

    • slot1slot2의 경우 흰색 배경에 둥근 모서리를 가진 80픽셀 X 80픽셀의 정사각형으로 스타일을 지정합니다. 각 행에 두 개의 슬롯이 있는 두 개의 행으로 슬롯을 정렬합니다.
    • object의 경우 검은색 배경에 50픽셀 X 50픽셀의 둥근 점으로 스타일을 지정합니다.

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&amp;guid=3d86870c8637c4a3c979a8b4fe0cba4c&amp;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 클래스를 확장하고 로직을 정의합니다.

  1. DragAndDrop 폴더에서 DragAndDropManipulator.cs라는 또 다른 C# 파일을 만듭니다.

  2. DragAndDropManipulator.cs 파일을 엽니다.

  3. using UnityEngine.UIElements; 선언을 추가합니다.

  4. DragAndDropManipulator 클래스가 MonoBehaviour가 아닌 PointerManipulator를 확장하도록 만들고 다음을 수행합니다.

    • 상속된 RegisterCallbacksOnTarget() 메서드를 구현하여 필요한 모든 콜백을 등록합니다.
    • UnregisterCallbacksOnTarget() 메서드를 구현하여 해당 콜백의 등록을 취소합니다.
    • target을 설정하고 시각적 트리의 루트에 대한 레퍼런스를 저장하는 생성자를 작성합니다.
  5. PointerDownEvent, PointerMoveEvent, PointerUpEvent, PointerCaptureOutEvent에 대한 콜백 역할을 하는 네 가지 메서드를 작성합니다.

    • PointerDownHandler(): target의 시작 위치와 포인터를 저장하고 target이 포인터를 캡처하도록 하며 드래그가 현재 진행 중임을 나타냅니다.
    • PointerMoveHandler(): 드래그가 진행 중인지 target이 포인터를 캡처했는지 확인합니다. 두 가지 모두 true이면 창의 경계 내에서 target에 대한 새로운 위치를 계산합니다.
    • PointerUpHandler(): 드래그가 진행 중인지 target이 포인터를 캡처했는지 확인합니다. 두 가지 모두 true이면 target이 포인터를 해제합니다.
    • PointerCaptureOutHandler(): 드래그가 진행 중인지 확인합니다. true인 경우 시각적 트리의 루트를 쿼리하여 모든 슬롯을 찾고 target과 가장 가까운 슬롯을 결정하고 target이 해당 슬롯의 맨 위에 놓이도록 위치를 설정합니다. 겹치는 슬롯이 없으면 target의 위치를 원래 위치로 다시 설정합니다.
  6. RegisterCallbacksOnTarget()에서 이 네 가지 콜백을 target에 등록합니다.

  7. 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);
    }
}

드래그 앤 드롭 동작 인스턴스화

커스텀 창에서 드래그 앤 드롭을 활성화하려면 창이 열릴 때 인스턴스화합니다.

  1. DragAndDropWindow.cs에서 CreateGUI() 메서드에 다음을 추가하여 DragAndDropManipulator 클래스를 인스턴스화합니다.

    DragAndDropManipulator manipulator =
        new(rootVisualElement.Q<VisualElement>("object"));
    
  2. Unity 메뉴바로 이동하여 Window > UI Toolkit > Drag And Drop을 클릭합니다. 열린 커스텀 창에서 오브젝트를 아무 슬롯에나 드래그할 수 있습니다.


Unity 2021.2에서 드래그 앤 드롭 UI 예시 추가됨NewIn20212

런타임용 탭 메뉴 생성
마이그레이션 가이드