Unity 编辑器之 EditorTool、EditorToolBar、Overlay属性

Unity面板栏

EditorTool

  • 注意
    • 全局工具,默认收纳在全局工具的图标内
    • 组件工具,按照组件类型,在全局工具图标后边,依次排列

全局工具示例

cs 复制代码
// 放在 Assets/Editor/ 下
using UnityEngine;
using UnityEditor;
using UnityEditor.EditorTools;

[EditorTool("My Slider Tool")]
class MySliderTool : EditorTool
{
    GUIContent m_Icon;

    public override GUIContent toolbarIcon
    {
        get
        {
            if (m_Icon == null)
                m_Icon = new GUIContent("📏", "My Slider Tool");
            return m_Icon;
        }
    }

    public override void OnToolGUI(EditorWindow window)
    {
        if (Selection.transforms.Length == 0) return;

        EditorGUI.BeginChangeCheck();
        Vector3 pos = Tools.handlePosition;

        using (new Handles.DrawingScope(Color.green))
        {
            pos = Handles.Slider(pos, Vector3.right);
        }

        if (EditorGUI.EndChangeCheck())
        {
            Vector3 delta = pos - Tools.handlePosition;
            Undo.RecordObjects(Selection.transforms, "Slider Tool Move");
            foreach (var t in Selection.transforms)
                t.position += delta;
        }
    }
}

组件工具示例

cs 复制代码
// 运行时组件(普通脚本,不在 Editor 文件夹)
using UnityEngine;

public class PlatformPath : MonoBehaviour
{
    public Vector3 start = new Vector3(-5, 0, 0);
    public Vector3 end   = new Vector3( 5, 0, 0);
}
cs 复制代码
using UnityEngine;
using UnityEditor;
using UnityEditor.EditorTools;

// ★ 第二个参数让它变成"组件工具",只有选中带 PlatformPath 的物体才出现
[EditorTool("Path Tool", typeof(PlatformPath))]
class PathTool : EditorTool // 可选实现 IDrawSelectedHandles
{
    public override GUIContent toolbarIcon
        => new GUIContent("⟿", "Edit Platform Path");

    public override void OnActivated()
        => SceneView.lastActiveSceneView?
            .ShowNotification(new GUIContent("Path Tool Active"), 1f);

    public override void OnToolGUI(EditorWindow window)
    {
        if (!(window is SceneView)) return;

        // targets 来自 EditorTool,类型是 Object[]
        foreach (var o in targets)
        {
            if (!(o is PlatformPath p)) continue;

            EditorGUI.BeginChangeCheck();

            var ws = p.transform.TransformPoint(p.start);
            var we = p.transform.TransformPoint(p.end);

            ws = Handles.PositionHandle(ws, Quaternion.identity);
            we = Handles.PositionHandle(we, Quaternion.identity);

            // 画连线便于观察
            Handles.DrawLine(ws, we);

            if (EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(p, "Edit Path");
                p.start = p.transform.InverseTransformPoint(ws);
                p.end   = p.transform.InverseTransformPoint(we);
                EditorUtility.SetDirty(p);
            }
        }
    }
}

代码中需要注意的点

  • OnToolGui 默认是在3D空间,使用世界坐标绘制,如果想切换2D空间,需要使用Handles.BeginGui/EndGui 开启关闭2D空间
  • TransformPoint 把坐标转换成世界坐标,InverseTransformPoint 把世界坐标转换成局部坐标
  • Handles.DrawingScope:这是一个"状态管理器"。当创建一个它的实例时,它会保存当前的 Gizmos/Handles 绘制状态(包括颜色 Handles.color、矩阵 Handles.matrix 等)
    using ( ... ) { ... }:这是 C# 的语法。当代码执行进入 { } 时,DrawingScope 生效;当代码执行离开 { }(即使是因为报错跳出),Dispose() 方法会被自动调用,DrawingScope 会把之前保存的状态恢复回去。

EditorToolBar

这四个类和EditorTool类似,都是防止在Scene工具栏中,但是可以自定义浮动栏,一般和overlay配合使用

继承自ToolBarOverlay,一次性添加(浮动条)

cs 复制代码
using UnityEditor;
using UnityEditor.Overlays;
using UnityEditor.Toolbars;
using UnityEngine;


[EditorToolbarElement("CustomEditorToolbarButton", typeof(SceneView))]
public class CustomEditorToolbarButton : EditorToolbarButton
{
    public const string ID = "CustomEditorToolbarButton";

    public CustomEditorToolbarButton()
    {
        text = "Custom Editor Toolbar Button";
        clickable.clicked += () => Debug.Log("Custom Editor Toolbar Button Clicked!");
    }
}


[Overlay(typeof(SceneView), "EditorToolBarOverlayExample", true)]
public class EditorToolBarOverlayExample : ToolbarOverlay
{
    public EditorToolBarOverlayExample() : base(CustomEditorToolbarButton.ID)
    {
    }
}

使用Overlay属性,主动添加(面板)

cs 复制代码
using UnityEditor;
using UnityEditor.Overlays;
using UnityEditor.Toolbars;
using UnityEngine;
using UnityEngine.UIElements;


[Overlay(typeof(SceneView), "EditorToolBarExample", true)]
public class EditorToolBarExample : Overlay
{
    public override VisualElement CreatePanelContent()
    {
        base.CreatePanelContent();

        var root = new VisualElement();
        
        // 添加button
        root.Add(new EditorToolbarButton(() => { Debug.Log("Editor Toolbar Button Clicked!"); }));

        // 添加dropdown
        var dropdownButton = new EditorToolbarDropdown(() => { Debug.Log("Editor Toolbar Dropdown Clicked!"); });
        dropdownButton.text = "Dropdown";
        dropdownButton.icon = EditorGUIUtility.IconContent("d_winbtn_win_max").image as Texture2D;
        dropdownButton.clicked += () =>
        {
            GenericMenu menu = new GenericMenu();
            menu.AddItem(new GUIContent("Option 1"), false, () => { Debug.Log("Option 1 Selected"); });
            menu.AddItem(new GUIContent("Option 2"), false, () => { Debug.Log("Option 2 Selected"); });
            menu.AddItem(new GUIContent("Option 3"), false, () => { Debug.Log("Option 3 Selected"); });
            menu.DropDown(dropdownButton.worldBound);
        };
        root.Add(dropdownButton);

        // 添加toggle
        var editorToolbarToggle = new EditorToolbarToggle();
        editorToolbarToggle.text = "Toggle";
        editorToolbarToggle.icon = EditorGUIUtility.IconContent("d_winbtn_win_max").image as Texture2D;
        editorToolbarToggle.RegisterValueChangedCallback(evt =>
        {
            Debug.Log($"Editor Toolbar Toggle Value Changed: {evt.newValue}");
        });
        root.Add(editorToolbarToggle);

        return root;
    }
}

EditorTool 与 Overlay的示例

Unity 官方在 Overlay 脚本参考里给出了一个教科书级示例------核心思路是 EditorTool 内部嵌套定义一个 Overlay 子类 ,然后在工具的 OnActivated / OnWillBeDeactivated 里控制这个 Overlay 的显示与移除。

主要方法:

  • SceneView.AddOverlayToActiveView(m_Overlay)
  • SceneView.RomoveOverlayToActiveView(m_Overlay)
cs 复制代码
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEditor.Overlays;

[EditorTool("Offset", typeof(Transform))]
public class OffsetTool : EditorTool
{
    // ★ Overlay 作为 EditorTool 的内部类
    // defaultDisplay = true:第一次被加到画布时就可见
    [Overlay(defaultDisplay = true)]
    class OffsetToolOverlay : Overlay
    {
        Transform[] selection;
        public OffsetToolOverlay(Transform[] targets) =>
            selection = targets;

        public override VisualElement CreatePanelContent()
        {
            var root = new VisualElement();
            root.Add(new Label("Offset Tool Settings"));

            var field = new TextField("Offset");
            field.RegisterValueChangedCallback(evt =>
            {
                // 处理用户输入的逻辑
            });
            root.Add(field);
            return root;
        }
    }

    OffsetToolOverlay m_Overlay;

    // ★ 工具被激活时
    public override void OnActivated()
    {
        m_Overlay = new OffsetToolOverlay(targets.Cast<Transform>().ToArray());

        // ★★ 关键 API:把 Overlay 加到当前激活的 SceneView 上
        SceneView.AddOverlayToActiveView(m_Overlay);
    }

    // ★ 工具被停用时
    public override void OnWillBeDeactivated()
    {
        // ★★ 关键 API:从 SceneView 移除 Overlay
        if (m_Overlay != null)
            SceneView.RemoveOverlayFromActiveView(m_Overlay);
    }

    // 3D 手柄绘制
    public override void OnToolGUI(EditorWindow window)
    {
        if (!(window is SceneView)) return;
        // Handles.Slider(...) 等
    }
}

Q&A

Q:除了SceneWindow,还有别的View可以使用吗?

A:目前只支持SceneWindow,以及实现了ISupportsOverlays接口的自定义EditorWindow

cs 复制代码
using UnityEditor;
using UnityEditor.Overlays;
using UnityEngine.UIElements;

public class MyOverlayWindow : EditorWindow, ISupportsOverlays
{
    [MenuItem("Window/My Overlay Window")]
    public static void ShowWindow() => GetWindow<MyOverlayWindow>();
    
    // 实现 ISupportsOverlays 接口
    // (该接口是 marker interface,不需要实现任何方法)
}

// 然后就可以把 Overlay 注册到这个窗口了
[Overlay(typeof(MyOverlayWindow), "My Panel")]
public class MyOverlay : Overlay
{
    public override VisualElement CreatePanelContent()
    {
        return new Label("Hello from MyOverlayWindow!");
    }
}

Q:Handles类都有什么方法,还有别的类,用来在SceneWindow中操作吗?

A:Handles类,负责绘画3D相关的图形,使用世界坐标,除了它,还有HandleUtility\Tool类,辅助

相关推荐
星羽XingYu2 小时前
esp32 vscode clangd 无法索引库函数解决方案
ide·vscode·编辑器
北漂燕郊杨哥2 小时前
DotMD:一款基于 Go + Wails 的轻量开源 Markdown 桌面编辑器
golang·开源·编辑器·markdown·md
末代iOS程序员华仔4 小时前
Cursor 编辑器 + Figma‑MCP 服务器(真正让 Codex 直接读取 Figma 链接,高级方案)
服务器·编辑器·figma
平头某4 小时前
DSH:Agent 的 VSCode 时刻?
ide·vscode·编辑器
Dontla6 小时前
VSCode打开关闭控制台面板快捷键ctrl+`(terminal快捷键、vscode控制台快捷键)
ide·vscode·编辑器
笨笨饿1 天前
#109_BK7258 蓝牙 A2DP 音频数据通路深度解析
python·stm32·单片机·mcu·物联网·编辑器·音视频
带娃的IT创业者2 天前
Emacs 31:一场静默而深刻的范式演进
编辑器·文本编辑器·开源软件·emacs·lisp·软件架构
●VON2 天前
芯笺 Markdown:面向 HarmonyOS PC 的本地优先 Markdown 编辑器
华为·编辑器·harmonyos·鸿蒙