Unity编辑器扩展之Scene视图扩展

news2024/9/20 22:51:13

内容将会持续更新,有错误的地方欢迎指正,谢谢!
 

Unity编辑器扩展之Scene视图扩展
     
TechX 坚持将创新的科技带给世界!

拥有更好的学习体验 —— 不断努力,不断进步,不断探索
TechX —— 心探索、心进取!

助力快速掌握 Scene 视图扩展

为初学者节省宝贵的学习时间,避免困惑!


文章目录

  • 一、通过Gizmo绘制辅助元素
  • 二、通过Handles绘制辅助元素
  • 三、常驻辅助UI 之场景视图菜单系统
    • 1、场景视图菜单系统
    • 2、自定义场景菜单特性SceneViewMenuAttribute
    • 3、添加菜单到场景中
  • 四、项目地址


一、通过Gizmo绘制辅助元素


Gizmo 是 Unity 中一种用于在编辑器中可视化调试和编辑的工具。

它们允许开发者在 Scene 视图中绘制各种几何形状或其他可视化元素,这些元素只在编辑器中可见,不会在游戏运行时显示。

之前已经写过相关文章:
https://blog.csdn.net/caiprogram123/article/details/135448539#GizmosDrawLine_176

在这里插入图片描述


二、通过Handles绘制辅助元素


在 Unity 编辑器中,Handles 类提供了一组用于在场景视图中绘制图形和元素的工具。

它主要用于在编辑模式下为开发者提供可视化辅助,帮助开发和调试过程。

Handles 可以用来绘制各种辅助图形,如点、线、矩形、圆形、立方体等。

例如,可以用 Handles.DrawLine 来显示两个点之间的连线,或者用 Handles.DrawWireCube 绘制一个立方体的轮廓。

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(Transform))]
public class HandlesExample : Editor
{
    private void OnSceneGUI()
    {
    	// 绘制红色实心圆盘
        Handles.color = Color.red;
        Handles.DrawSolidDisc(new Vector3(0, 0, 0), Vector3.up, 0.5f); 

		// 绘制绿色线段
        Handles.color = Color.green;
        Handles.DrawLine(Vector3.zero, new Vector3(1, 1, 0)); 
		
		// 绘制蓝色矩形
        Handles.color = Color.blue;
        Handles.DrawSolidRectangleWithOutline(new Rect(1, 0, 1, 1), Color.blue, Color.black); 

		// 绘制圆环
        Handles.color = Color.cyan;
        Handles.DrawWireDisc(new Vector3(3, 0, 0), Vector3.up, 0.5f); 

		//绘制三维坐标
        Handles.color = Color.red;
        Handles.ArrowHandleCap(0, new Vector3(4, 0, 0), Quaternion.LookRotation(Vector3.right), 1.0f, EventType.Repaint); 
        Handles.color = Color.green;
        Handles.ArrowHandleCap(0, new Vector3(4, 0, 0), Quaternion.LookRotation(Vector3.up), 1.0f, EventType.Repaint); 
        Handles.color = Color.blue;
        Handles.ArrowHandleCap(0, new Vector3(4, 0, 0), Quaternion.LookRotation(Vector3.forward), 1.0f, EventType.Repaint); 

		// 绘制立方体轮廓
        Handles.color = Color.magenta;
        Handles.DrawWireCube(new Vector3(6, 0, 0), new Vector3(1, 1, 1)); 

		// 绘制文本标签
        Handles.color = Color.white;
        Handles.Label(Vector3.zero, "Hello, Handles!"); 
    }
}

以下案例中展示了一些常见的元素的绘制,比如:线段、立方体、圆环、矩形、圆盘、坐标箭头、标签等元素

在这里插入图片描述



三、常驻辅助UI 之场景视图菜单系统


1、场景视图菜单系统


场景视图菜单系统实现了一个自定义的菜单系统,可以在 Unity 编辑器的场景视图中动态添加、管理和显示菜单项。

通过这个系统,开发者可以利用 SceneViewMenuAttribute 特性标记静态方法,这些方法会被自动收集,并作为菜单项添加到场景视图中。

特性:

  • 自动收集和添加菜单项

    在编辑器启动时,通过扫描所有程序集的静态方法,找到使用 SceneViewMenuAttribute 特性标记的方法,并将这些方法注册为菜单项。

  • 场景视图中的菜单绘制

    使用 SceneView.duringSceneGui 事件,每次场景视图刷新时,自动绘制菜单。
    菜单支持多级结构,一级菜单显示在场景视图中,当选择某个一级菜单时,会显示其对应的二级菜单。

  • 菜单项的管理

    菜单项使用 Dictionary 进行存储,每个一级菜单项对应一个子菜单项列表。
    可以通过简单的字符串定义菜单的层级关系,并为每个菜单项绑定一个 Action,以便在点击菜单项时执行相应的逻辑。

  • 菜单状态控制

    每个菜单项有一个 isToggle 属性,表示当前菜单项是否被选中。选中的菜单项会以不同的颜色显示,便于用户区分当前激活的菜单。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

/// <summary>
/// 场景视图中绘制行为菜单
/// </summary>
public static class SceneViewMenuDrawer
{
    private static Dictionary<ActionMenuItem, WindowActionItemList> MenuItems = new();

	 /// <summary>
	 /// 初始化场景视图菜单
	 /// </summary>
    [InitializeOnLoadMethod]
    private static void InitializeSceneMenuItems()
    {
        AppDomain.CurrentDomain.GetAssemblies()
        .SelectMany(assembly => assembly.GetTypes())
        .SelectMany(type => type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
        .Where(method => method.GetCustomAttributes(typeof(SceneViewMenuAttribute), false).Length > 0).ToList()
        .ForEach(method => {
            var attribute = method.GetCustomAttribute<SceneViewMenuAttribute>();
            var action = (Action)Delegate.CreateDelegate(typeof(Action), method);
            Add(attribute.MenuName, action);
        });
    }
    
    /// <summary>
    /// 绘制菜单
    /// </summary>
    [InitializeOnLoadMethod]
    private static void DrawActionMenus()
    {
        SceneView.duringSceneGui += (SceneView sceneView) =>
        {
            Handles.BeginGUI();

            EditorGUILayout.BeginHorizontal();

            DrawActionMenuFirstLevel();

            DrawActionMenuSecondLevel();

            GUILayout.FlexibleSpace();

            EditorGUILayout.EndHorizontal();

            Handles.EndGUI();
        };
    }

    /// <summary>
    /// 绘制一级菜单
    /// </summary>
    private static void DrawActionMenuFirstLevel()
    {
        GUIStyle style = new GUIStyle("Button") { wordWrap = true, contentOffset = new Vector2(0, -6), };

        EditorGUILayout.BeginVertical();
        foreach (var item in MenuItems)
        {
            GUIContent content = new GUIContent(item.Key.actionName);

            GUI.color = item.Key.isToggle ? Color.white : new Color(0.6f, 0.6f, 0.6f);

            if (GUILayout.Button(content, style, GUILayout.Width(22), GUILayout.Height(style.CalcHeight(content, 22))))
            {
                SelectToggleChanged(item.Key);

                if (item.Value.Count == 0)
                    item.Key.action?.Invoke();
            }

            GUI.color = Color.white;
        }
        EditorGUILayout.EndVertical();
    }

    /// <summary>
    /// 绘制二级菜单
    /// </summary>
    private static void DrawActionMenuSecondLevel()
    {
        foreach (var item in MenuItems)
        {
            if (item.Key.isToggle)
                item.Value.DrawActionItems();
        }
    }

    /// <summary>
    /// 选择改变
    /// </summary>
    /// <param name="actionMenu"></param>
    public static void SelectToggleChanged(ActionMenuItem actionMenu)
    {
        foreach (var item in MenuItems)
        {
            item.Key.isToggle = item.Value.Count == 0 ? false : (item.Key == actionMenu ? !item.Key.isToggle : false);
        }
    }

    /// <summary>
    /// 添加菜单
    /// </summary>
    /// <param name="menu"></param>
    /// <param name="action"></param>
    public static void Add(string menu, Action action = null)
    {
        Match match = Regex.Match(menu, @"^(.*)/(.*)$|^(.*)$");
        if (match.Success)
        {
            List<Group> groups = match.Groups.Where(item => !string.IsNullOrEmpty(item.Value)).ToList();

            ActionMenuItem item = MenuItems.Keys.FirstOrDefault(item => item.actionName == groups[1].Value);

            if (item == null)
            {
                item = new ActionMenuItem(groups[1].Value, action);
                MenuItems[item] = new WindowActionItemList();
            }

            if (groups.Count > 2)
            {
                if (!MenuItems[item].IsContainActionItem(groups[2].Value))
                {
                    MenuItems[item].Add(new ActionMenuItem(groups[2].Value, action));
                }
            }
        }
    }

    /// <summary>
    /// 行为菜单项
    /// </summary>
    public class ActionMenuItem
    {
        public bool isToggle { get; set; }
        public string actionName { get; set; }
        public Action action { get; set; }

        public ActionMenuItem(string name, Action action)
        {
            this.actionName = name;
            this.action = action;
        }
    }
}

/// <summary>
/// 行为菜单列表
/// </summary>
public class WindowActionItemList : List<SceneViewMenuDrawer.ActionMenuItem>
{
	/// <summary>
	/// 绘制行为菜单项
	/// </summary>
    public void DrawActionItems()
    {
        if (Count == 0) return;

        EditorGUILayout.BeginVertical(new GUIStyle("Label") { normal = new GUIStyle("TextField").normal }, GUILayout.Height(24 * Count + 5 * Count));

        GUI.color = new Color(0, 1, 0, 0.8f);

        foreach (var component in this)
        {
            EditorGUILayout.Space(2.5F);

            if (GUILayout.Button(component.actionName, GUILayout.Width(120), GUILayout.Height(24)))
            {
                component.action?.Invoke();
            }
            EditorGUILayout.Space(2.5F);
        }

        GUI.color = Color.white;

        EditorGUILayout.EndVertical();
    }
	
	/// <summary>
	/// 是否包含行为菜单
	/// </summary>
	/// <param name="itemName"></param>
	/// <returns></returns>
    public bool IsContainActionItem(string itemName)
    {
        return this.FirstOrDefault(itme => itme.actionName == itemName) != null;
    }
}

这个系统通过自动化和结构化的方式,简化了在 Unity 场景视图中添加和管理自定义菜单的过程。

开发者只需要在方法上添加 SceneViewMenuAttribute 特性,就能轻松将功能集成到场景视图的菜单中,大大提高了开发效率。

2、自定义场景菜单特性SceneViewMenuAttribute


添加一个属性特性SceneViewMenuAttribute ,用于标记需要被添加到场景菜单中的静态方法,其中MenuName为菜单路径。

当静态方法被该特性标记时,并传入菜单路径,会在场景中创建该菜单,当点击该菜单时会执行这个静态方法。

using System;
using UnityEngine;

/// <summary>
/// 自定义场景菜单特性
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class SceneViewMenuAttribute : PropertyAttribute
{
    public string MenuName { get; }

    public SceneViewMenuAttribute(string menuName)
    {
        MenuName = menuName;
    }
}

3、添加菜单到场景中


可以通过SceneViewMenuDrawer.Add()直接向场景中添加菜单,同时也可以在静态方法上添加特性SceneViewMenu来实现向场景中添加菜单,与特性MenuItem的用法类似。

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using SceneViewMenu.Editor;
using SceneViewMenu.Runtime;

/// <summary>
/// 场景菜单测试
/// </summary>
public class SceneViewMenuTest
{
    [InitializeOnLoadMethod]
    static void SceneExtension()
    {
        SceneViewMenuDrawer.Add("一级菜单", () => Debug.Log("一级菜单"));
        SceneViewMenuDrawer.Add("一级菜单1/设备树生成窗口", () => Debug.Log("设备树生成窗口"));
        SceneViewMenuDrawer.Add("一级菜单1/设备树信息同步窗口", () => Debug.Log("设备树信息同步窗口"));
        //SceneViewMenuDrawer.Add("一级菜单1/路径生成窗口", () => Debug.Log("路径生成窗口"));
        SceneViewMenuDrawer.Add("一级菜单1/组件存储重置窗口", () => Debug.Log("组件存储重置窗口"));
        //SceneViewMenuDrawer.Add("一级菜单2/测试读取数据");
        SceneViewMenuDrawer.Add("一级菜单2/测试Icon获取", () => Debug.Log("测试Icon获取"));
        SceneViewMenuDrawer.Add("一级菜单3/右键菜单功能", () => Debug.Log("右键菜单功能"));
        //SceneViewMenuDrawer.Add("一级菜单4/一级菜单5/只能显示二级菜单", () => Debug.Log("只能显示二级菜单"));
    }

    [SceneViewMenu("一级菜单1/路径生成窗口")]
    static void Test1()
    {
        Debug.Log("路径生成窗口");
    }

    [SceneViewMenu("一级菜单2/测试读取数据")]
    static void Test2()
    {
        Debug.Log("测试读取数据");
    }

    [SceneViewMenu("一级菜单4/一级菜单5/只能显示二级菜单")]
    static void Test3()
    {
        Debug.Log("只能显示二级菜单");
    }
}
#endif

通过Add和特性往场景中添加菜单:

  • 一级菜单
  • 一级菜单1/设备树生成窗口
  • 一级菜单1/设备树信息同步窗口
  • 一级菜单1/路径生成窗口
  • 一级菜单1/组件存储重置窗口
  • 一级菜单2/测试读取数据
  • 一级菜单2/测试Icon获取
  • 一级菜单3/右键菜单功能
  • 一级菜单4/一级菜单5/只能显示二级菜单

在这里插入图片描述


四、项目地址


以下是项目地址,已经整理成了Package包,有需要的小伙伴门可以自取:

https://gitcode.com/CTLittleNewbie/com.fxb.sceneviewmenuext_v1.0.0/overview




TechX —— 心探索、心进取!

每一次跌倒都是一次成长

每一次努力都是一次进步


END
感谢您阅读本篇博客!希望这篇内容对您有所帮助。如果您有任何问题或意见,或者想要了解更多关于本主题的信息,欢迎在评论区留言与我交流。我会非常乐意与大家讨论和分享更多有趣的内容。
如果您喜欢本博客,请点赞和分享给更多的朋友,让更多人受益。同时,您也可以关注我的博客,以便及时获取最新的更新和文章。
在未来的写作中,我将继续努力,分享更多有趣、实用的内容。再次感谢大家的支持和鼓励,期待与您在下一篇博客再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2094884.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

农产品智慧物流系统论文

摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对信息管理混乱&#xff0c;出错率高&#xff0c;信息安全性差&#x…

【比较】数据字节串/字串比较指令 (CMPSB/CMPSW),数据字节串/字串检索指令(SCASB/SCASW)的区别

&#x1f31f; 嗨&#xff0c;我是命运之光&#xff01; &#x1f30d; 2024&#xff0c;每日百字&#xff0c;记录时光&#xff0c;感谢有你一路同行。 &#x1f680; 携手启航&#xff0c;探索未知&#xff0c;激发潜能&#xff0c;每一步都意义非凡。 数据字节串/字串比较…

【卡码网C++基础课 14.链表的基础操作2】

目录 题目描述与分析代码编写 题目描述与分析 题目描述&#xff1a; 请编写一个程序&#xff0c;实现以下操作&#xff1a; 构建一个单向链表&#xff0c;链表中包含一组整数数据&#xff0c;输出链表中的第 m 个元素&#xff08;m 从 1 开始计数&#xff09;。 要求&#xf…

python-数组距离

题目描述 已知元素从小到大排列的两个数组 x[] 和 y[]&#xff0c;请写出一个程序算出两个数组彼此之间差的绝对值中最小的一个&#xff0c;这叫做数组的距离。输入格式&#xff1a; 输入共 3 行。 第一行为两个整数 m,n&#xff0c;分别代表数组 f[],g[] 的长度。 第二行有 m …

32力扣 最长有效括号

dp方法&#xff1a; class Solution { public:int longestValidParentheses(string s) {int ns.size();vector<int> dp(n,0);if(n0 || n1) return 0;if(s[0]( && s[1])){dp[1]2;}for(int i2;i<n;i){if(s[i])){if(s[i-1](){dp[i]dp[i-2]2;}else if(s[i-1])){i…

ESXi 失败 – “scsi0:0”的磁盘类型 2 不受支持或无效。请确保磁盘已导入

在导入vm虚拟机到exsi时导入后报错了 解决方法&#xff1a; 连接到exsi 进入到数据存储虚拟机所在的文件夹后 然后输入以下命令 vmkfstools -i oldfile.vmdk newfile.vmdk -d thin 转换完成后会显示Clone 100% done。 以下为具体详细的步骤 需要用VMware的工具”vmkfstoo…

《机器学习》周志华-CH5(神经网络)

5.1神经元模型 机器学习中谈论神经网络指“神经网络学习”。 神经网络基本成分是神经元(neuron)和模型 1943年&#xff0c;McCulloch and Pitts:M-P神经元模型 5.2感知机与多层网络 感知机(Perceptron)由两层神经元组成&#xff0c;又称“阈值逻辑单元(threshold logic unit)”…

Spring Cloud Alibaba 快速学习之 Gateway

1 引言 Gateway顾名思义就是“网关”的意思&#xff0c;旨在为微服务提供统一的访问入口&#xff0c;然后转发到各个微服务&#xff0c;通常可以在网关中统一做安全认证、监控、限流等等功能&#xff0c;避免每个微服务都重复实现这些功能。 2 代码 本章演示的项目基于Sprin…

如何使用MabatisPlus

一. 引入相关的Maven依赖 例如下面我所引用的依赖 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency>二.将写好的mapper继承BaseMap…

ref 和 reactive 区别

前言 ref 和 reactive是Vue 3中响应式编程的核心。在Vue中&#xff0c;响应式编程是一种使数据与UI保持同步的方式。当数据变化时&#xff0c;UI会自动更新&#xff0c;反之亦然。这种机制大大简化了前端开发&#xff0c;使我们能够专注于数据和用户界面的交互&#xff0c;而不…

【Spring】Spring Boot入门(1)

本系列共涉及4个框架&#xff1a;Sping,SpringBoot,Spring MVC,Mybatis。 博客涉及框架的重要知识点&#xff0c;根据序号学习即可。 目录 1、什么是Spring 1.1 什么是Spring 1.2 Spring与Spring Boot&#xff08;Spring 脚手架&#xff09;的关系 2、了解Maven 2.1 什…

好用的宠物浮毛清理神器,希喂、IAM、范罗士宠物空气净化器大揭秘

最近宠物空气净化器在养宠家庭中的讨论度一直很高&#xff0c;产品主打可以吸附宠物浮毛和异味的功能。养了三只小猫的我对此也很感兴趣&#xff0c;准备入手一台试试。可我没有想到宠物空气净化器的品牌有这么多&#xff0c;功课都做了好久。看了好几天&#xff0c;最后在希喂…

【Python报错已解决】`SyntaxError: can‘t assign to function call`

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 引言&#xff1a;一、问题描述&#xff1a;1.1 报错示例&#xff1a;1.2 报错分析&#xff1a;1.3 解决思路&#xff…

Quartz任务调度框架

文章目录 前言一、介绍二、使用步骤1.创建maven工程&#xff0c;添加依赖2.创建任务3.启动任务 三、基本实现原理1. Scheduler任务调度器2. Triggers触发器2.1 SimpleTrigger2.2 CronTirgger 3. Misfire策略4 任务Job4.1 Job4.2 JobDetail4.3 JobDataMap 前言 最近跟的一个系统…

洞见数据价值,激活组织活力,让决策更精准的智慧地产开源了

智慧地产视觉监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;从而大大减少企业级应用约95%的开发成本。通过计算机视觉和…

Sql查询优化--索引设计与sql优化(包含慢查询定位+explain解释计划+左匹配原则+索引失效)

本文介绍了数据库查询的索引优化方法&#xff0c;依次介绍了慢查询语句定位方法、索引设计与sql语句优化方法&#xff0c;并介绍了左匹配原则和索引失效的场景&#xff0c;最后介绍了explain执行计划要怎么看以调整检验索引设计是否生效和效率情况&#xff0c;创新介绍了如何以…

AWS api数据信息获取(boto3)

GitHub - starsliao/TenSunS: &#x1f984;后羿 - TenSunS(原ConsulManager)&#xff1a;基于Consul的运维平台&#xff1a;更优雅的Consul管理UI&多云与自建ECS/MySQL/Redis同步Prometheus/JumpServer&ECS/MySQL/Redis云监控指标采集&Blackbox站点监控维护&漏…

4家国产数据库上市公司:最好的盈利1个亿,最惨亏8000w

目前国产数据库xc目录中大概有11家公司&#xff0c;其中多家公司已经上市了&#xff0c;且公布了最新的半年报&#xff01; 这里尝试分析一下几家国产数据库上市公司的发展潜力和情况。 达梦数据库 达梦数据库作为国产数据库第一股&#xff0c;业绩增长还是一如既往的猛&…

【零知识证明】通读Tornado Cash白皮书(并演示)

1 Protocol description 协议描述有以下功能&#xff1a; 1.insert&#xff1a;向智能合约中存入资金&#xff0c;通过固定金额的单笔交易完成&#xff0c;金额由N表示&#xff08;演示时用1 ETH&#xff09; 2.remove&#xff1a;从智能合约中提取资金&#xff0c;交易由收…

ncnn之yolov5(7.0版本)目标检测pnnx部署

一、pnxx介绍与使用 pnnx安装与使用参考&#xff1a; https://github.com/pnnx/pnnxhttps://github.com/Tencent/ncnn/wiki/use-ncnn-with-pytorch-or-onnxhttps://github.com/Tencent/ncnn/tree/master/tools/pnnx 支持python的首选pip&#xff0c;否则就源码编译。 pip3 …