Unity3D :在自定义编辑器窗口中创建拖放式 UI

Unity3D :在自定义编辑器窗口中创建拖放式 UI
推荐:将NSDT场景编辑器加入你的3D工具链
3D工具集:NSDT简石数字孪生

在自定义编辑器窗口中创建拖放式 UI

版本: 2021.3+

拖放是 中的常见功能用户界面
设计。您可以使用 UI 工具包在自定义编辑器窗口或由 Unity 构建的应用程序中创建拖放式 UI。此示例演示如何在自定义编辑器窗口中创建拖放 UI。

示例概述

该示例在自定义编辑器窗口中添加多个插槽和一个对象。您可以将对象拖到任何插槽中,如下所示:

拖放式 UI 的预览
拖放式 UI 的预览

您可以在此 GitHub 存储库中找到此示例创建的已完成文件。

先决条件

本指南适用于熟悉 Unity 编辑器、UI 工具包和 C# 脚本的开发人员。在开始之前,请熟悉以下内容:

  • 用户界面生成器
  • 可视化树
  • 用户体验
  • USS
  • 指针事件

创建自定义编辑器窗口

首先,创建一个自定义编辑器窗口来保存拖放式 UI。

  1. 使用任何模板在 Unity 中创建项目。
  2. 创建一个 调用的文件夹来存储您的所有文件。AssetsDragAndDrop
  3. 在该文件夹中,右键单击并选择“>编辑器窗口创建 UI 工具包>”。DragAndDrop
  4. UI 工具包编辑器窗口创建器中,输入 。DragAndDropWindow
  5. 单击确认。这将自动为自定义窗口创建 C# 脚本、UXML 和 USS 文件。
  6. 打开菜单名称和窗口标题并将其更改为 ,并删除默认标签的代码,以使 UI 更加用户友好。DragAndDropWindow.csDrag And Drop

完成的工作应如下所示: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. 在该文件夹中,双击以打开 UI 生成器。DragAndDropDragAndDropWindow.uxml
  2. 样式表中,单击“添加现有 USS”,然后选择 。DragAndDropWindow.uss

添加以下 UI 控件:VisualElement

  • 一个命名,有两个孩子的名字和。每行应有两个名为 和 的子项。slotsslot_row1slot_row2slot1slot2
  • 一个与 同级别命名。 必须在层次结构中排在后面objectslotsobjectslots

UI 控件的样式如下所示:

  • 对于 和 ,将它们样式设置为 80px X 80px 正方形,具有白色背景颜色和圆角。将插槽排列为两行,每行两个插槽。slot1slot2
  • 对于 ,将其样式设置为具有黑色背景颜色的 50px X 50px 圆形点。object

【提示】为了使项目更有趣,您可以为对象使用背景图像。您可以在 GitHub 存储库中找到图像 (Pouch.png)。

有关如何添加 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);
}

定义拖放逻辑

若要定义拖放行为,请扩展指针操纵器类并定义逻辑。

  1. 在该文件夹中,创建另一个名为 的 C# 文件。DragAndDropDragAndDropManipulator.cs
  2. 打开。DragAndDropManipulator.cs
  3. 添加声明。using UnityEngine.UIElements;
  4. 使类扩展而不是 。DragAndDropManipulatorPointerManipulatorMonoBehaviour
  5. 编写构造函数来设置目标并存储对可视化树根的引用。
  6. 编写四个方法,作为 PointerDownEvents、PointerMoveEvent s、PointerUpEvents和 PointerCaptureOutEvents 的回调。
  7. 实现 RegisterCallbacksOnTarget() 和 UnregisterCallbacksOnTarget() 来注册和取消注册这四个来自 的回调。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.csCreateGUI()DragAndDropManipulator

DragAndDropManipulator manipulator =new(rootVisualElement.Q<VisualElement>("object"));

2. 从菜单栏中,选择“窗口> UI 工具包”>“拖放”。在打开的自定义编辑器窗口中,您可以将对象拖到任何插槽中。

3D建模学习工作室整理翻译,转载请注明出处!

上一篇:Unity3D :IMGUI 事件 (mvrlink.com)

下一篇:Unity3D :创建拖放式 UI 以在编辑器窗口之间拖动 (mvrlink.com)

NSDT场景编辑器 | NSDT 数字孪生 | GLTF在线编辑器 | 3D模型在线转换 | UnrealSynth虚幻合成数据生成器 | 3D模型自动纹理化工具
2023 power by nsdt©鄂ICP备2023000829号