Version: 2023.1
언어: 한국어
Create a transition in a custom Editor window
Create a drag-and-drop UI to drag between Editor windows

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

Version: 2021.3+

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

개요 예시

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

드래그 앤 드롭 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를 담을 커스텀 에디터 창을 만듭니다.

  1. Create a project in Unity with any template.
  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픽셀의 정사각형으로 스타일을 지정합니다. 각 행에 두 개의 슬롯이 있는 두 개의 행으로 슬롯을 정렬합니다.
    • For 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&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. Make the DragAndDropManipulator class extend PointerManipulator rather than MonoBehaviour.
  5. Write a constructor to set target and store a reference to the root of the visual tree.
  6. Write four methods that act as callbacks for PointerDownEvents, PointerMoveEvents, PointerUpEvents, and PointerCaptureOutEvents.
  7. Implement RegisterCallbacksOnTarget() and UnregisterCallbacksOnTarget() to register and un-register these four callbacks from target.

완성된 DragAndDropManipulator.cs는 다음과 같이 작성되어야 합니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

public class DragAndDropManipulator : PointerManipulator
{
    // Write a constructor to set target and store a reference to the 
    // root of the visual tree.
    public DragAndDropManipulator(VisualElement target)
    {
        this.target = target;
        root = target.parent;
    }

    protected override void RegisterCallbacksOnTarget()
    {
        // Register the four callbacks on target.
        target.RegisterCallback<PointerDownEvent>(PointerDownHandler);
        target.RegisterCallback<PointerMoveEvent>(PointerMoveHandler);
        target.RegisterCallback<PointerUpEvent>(PointerUpHandler);
        target.RegisterCallback<PointerCaptureOutEvent>(PointerCaptureOutHandler);
    }

    protected override void UnregisterCallbacksFromTarget()
    {
        // Un-register the four callbacks from target.
        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; }

    // This method stores the starting position of target and the pointer, 
    // makes target capture the pointer, and denotes that a drag is now in progress.
    private void PointerDownHandler(PointerDownEvent evt)
    {
        targetStartPosition = target.transform.position;
        pointerStartPosition = evt.position;
        target.CapturePointer(evt.pointerId);
        enabled = true;
    }

    // This method checks whether a drag is in progress and whether target has captured the pointer. 
    // If both are true, calculates a new position for target within the bounds of the window.
    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));
        }
    }

    // This method checks whether a drag is in progress and whether target has captured the pointer. 
    // If both are true, makes target release the pointer.
    private void PointerUpHandler(PointerUpEvent evt)
    {
        if (enabled && target.HasPointerCapture(evt.pointerId))
        {
            target.ReleasePointer(evt.pointerId);
        }
    }

    // This method checks whether a drag is in progress. If true, queries the root 
    // of the visual tree to find all slots, decides which slot is the closest one 
    // that overlaps target, and sets the position of target so that it rests on top 
    // of that slot. Sets the position of target back to its original position 
    // if there is no overlapping slot.
    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. 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.

추가 리소스

Create a transition in a custom Editor window
Create a drag-and-drop UI to drag between Editor windows
Copyright © 2023 Unity Technologies
优美缔软件(上海)有限公司 版权所有
"Unity"、Unity 徽标及其他 Unity 商标是 Unity Technologies 或其附属机构在美国及其他地区的商标或注册商标。其他名称或品牌是其各自所有者的商标。
公安部备案号:
31010902002961