Version: 2023.1
언어: 한국어
커스텀 에디터 창에서 전환 만들기
에디터 창 간에 드래그할 수 있는 드래그 앤 드롭 UI 만들기

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

버전:2021.3+

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

개요 예시

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

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

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

선행 조건

이 가이드는 Unity 에디터, UI 툴킷, C# 스크립팅에 익숙한 개발자용입니다.시작하기 전에 먼저 다음을 숙지하십시오.

커스텀 에디터 창 생성

시작하려면 메뉴에서 기본 커스텀 에디터 창을 생성합니다. UI를 좀더 사용자 친화적으로 만들기 위해 메뉴 이름과 창 제목을 Drag And Drop으로 변경하고 기본 레이블에 대한 코드를 제거합니다.

  1. 템플릿을 사용하여 Unity에서 프로젝트를 생성합니다.

  2. Assets 폴더를 오른쪽 클릭하고 Create > UI Toolkit > Editor Window를 선택합니다.

  3. UI Toolkit Editor Window Creator에서 DragAndDropWindow를 입력합니다.

  4. Confirm을 클릭합니다. 이렇게 하면 자동으로 다음 3개의 파일이 생성됩니다. DragAndDropWindow.cs, DragAndDropWindow.uxml, DragAndDropWindow.uss.

  5. 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/Editor/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/Editor/DragAndDropWindow.uss");
        }
    }
    

슬롯과 오브젝트 생성

다음으로 커스텀 창에 UI 컨트롤을 추가합니다.

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

UI 컨트롤을 다음과 같이 스타일 지정합니다.

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

:프로젝트를 더 재미있게 만들기 위해 오브젝트에 배경 이미지를 사용할 수 있습니다.이미지(Pouch.png)는 GitHub 저장소에서 확인할 수 있습니다.

  1. 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="DragAndDropWindow.uss" />
        <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>
    
  2. DragAndDropWindow.uss의 콘텐츠를 다음으로 교체합니다.

    .slot {
    width: 80px;
    height: 80px;
    margin: 5px;
    background-color: rgb(255, 255, 255);
    border-top-radius: 10px;
    border-top-left-radius: 10px;
    border-bottom-left-radius: 10px;
    border-top-right-radius: 10px;
    border-bottom-right-radius: 10px;
    }
    
    .slot_row {
        flex-direction: row;
    }
    
    .object {
        width: 50px;
        height: 50px;
        position: absolute;
        left: 20px;
        top: 20px;
        border-radius: 30px;
        background-color: rgb(0, 0, 0);
    }
    

드래그 앤 드롭 로직 정의

드래그 앤 드롭 동작을 정의하려면 PointerManipulator 클래스를 확장하고 로직을 정의합니다. target을 설정하는 생성자를 작성하고 비주얼 트리의 루트에 레퍼런스를 저장합니다. PointerDownEvent, PointerMoveEvent, PointerUpEvent, PointerCaptureOutEvent에 대한 콜백 역할을 하는 4개의 메서드를 작성합니다. RegisterCallbacksOnTarget()UnregisterCallbacksOnTarget()을 구현하여 이 4개의 콜백을 target에 등록 및 등록 해제합니다.

  1. Editor 폴더에서 DragAndDropManipulator.cs라는 다른 C# 파일을 생성합니다.

  2. 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. 메뉴 바에서 Window > UI Toolkit > Drag And Drop을 선택합니다.열린 커스텀 에디터 창에서 오브젝트를 아무 슬롯에나 드래그할 수 있습니다.

추가 리소스

커스텀 에디터 창에서 전환 만들기
에디터 창 간에 드래그할 수 있는 드래그 앤 드롭 UI 만들기