Version: 2021.3
言語: 日本語
ランタイム用タブメニューの作成
エディターウィンドウ間をドラッグできるドラッグアンドドロップ UI の作成

カスタムエディターウィンドウ内にドラッグアンドドロップ UI を作成する

バージョン: 2021.3 以降

ドラッグアンドドロップは UI デザインにおいて一般的な機能です。UI Toolkit を使用すると、カスタムエディターウィンドウ内または Unity でビルドされたアプリケーション内にドラッグアンドドロップ UI を作成することができます。この例では、カスタムエディターウィンドウの中にドラッグアンドドロップのUIを作成する方法を説明します。

例の概要

この例では、カスタムエディターウィンドウに複数のスロットと 1 つのオブジェクトを加えます。下図のように、オブジェクトを任意のスロットにドラッグすることができます。

ドラッグアンドドロップ UI のプレビュー
ドラッグアンドドロップ UI のプレビュー

この例で作成するすべてのファイルは、GitHub リポジトリ にあります。

要件

This is an advanced example for developers familiar with Unity Editor, UI Toolkit, and C# scripting. You are recommended to have a basic understanding of the following concepts:

カスタムエディターウィンドウの作成

まず、ドラッグアンドドロップ UI を保持するために、カスタムエディターウィンドウを作成します。

  1. Unity で任意のテンプレートでプロジェクトを作成します。
  2. AssetsDragAndDrop という名前のフォルダーを作成し、すべてのファイルを保存します。
  3. DragAndDrop フォルダーを右クリックし、Create > UI Toolkit > Editor Window の順に選択します。
  4. UI Toolkit Editor Window CreatorDragAndDropWindow と入力します。
  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 Builder を開きます。

  2. StyleSheetAdd Existing USS をクリックし、DragAndDropWindow.uss を選択します。

  3. 以下の VisualElement UI コントロールを加えます。

    • slot_row1slot_row2 という 2 つの子を持つ slots という名のコントロール。各行には、slot1slot2 という名前の2つの子供があります。
    • slot と同じレベルにある object という名前のもの。 objectHierarchyslots の後に来る必要があります。
  4. UI コントロールを以下のようにスタイル設定します。

    • slot1slot2 は、80px X 80px の正方形で、背景色が白、コーナーが丸いスタイルにします。スロットは 2 行に並べ、各行に 2 つのスロットを配置します。
    • object の場合、50px X 50px の円形スポットにし、背景色が黒のスタイルにします。

ヒント: プロジェクトをもっと楽しくするために、オブジェクトに背景画像を使用できます。その画像 (Pouch.png) は GitHub リポジトリ にあります。

UI コントロールを加えてスタイルを設定する方法については、UI Builder を参照してください。

完成した 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, and do the following:

  5. Write four methods that act as callbacks for PointerDownEvents, PointerMoveEvents, PointerUpEvents, and PointerCaptureOutEvents:

    • PointerDownHandler(): Stores the starting position of target and the pointer, makes target capture the pointer, and denotes that a drag is now in progress.
    • PointerMoveHandler(): 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.
    • PointerUpHandler(): Checks whether a drag is in progress and whether target has captured the pointer. If both are true, makes target release the pointer.
    • PointerCaptureOutHandler(): 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.
  6. In RegisterCallbacksOnTarget(), register these four callbacks on target.

  7. In UnregisterCallbacksOnTarget(), 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
{
    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. メニューバーから、 Window > UI Toolkit > Drag And Drop を選択します。開いたカスタムエディターウィンドウで、オブジェクトを任意のスロットにドラッグすることができます。

その他の参考資料

ランタイム用タブメニューの作成
エディターウィンドウ間をドラッグできるドラッグアンドドロップ UI の作成