1.19 从0开始学习Unity游戏开发--扩展编辑器

news2024/10/7 18:32:05

在之前的篇章里面,我们一直在编辑器里面干活,然后做好资源的编辑和代码开发后,我们可以直接在编辑器内点击那个播放按钮就能真实的把游戏跑起来,但是有时候,我们可能希望在菜单里面加个按钮,这样我们可以直接执行一些批量的编辑动作,又或者我们希望像数组元素显示在Inspector面板上的效果一样,为我们的自定义数据结构也画一个特殊的编辑界面,那么这个时候我们就需要扩展Unity编辑器。

给编辑器加个菜单

我们知道Unity编辑器窗口顶部有一系列菜单,我们可以通过编写C#代码来增加我们自定义的菜单,现在我们新建一个脚本资源,叫MenuTest吧,代码如下:

using UnityEditor;
using UnityEngine;

public static class MenuTest
{
    [MenuItem("MenuTest/Say Hello World")]
    public static void SayHelloWorld()
    {
        Debug.Log("Hello World!");
    }
}

可以看到我们新建了一个C#类,但是并不继承自MonoBehaviour,而是直接是纯粹的C#类,写不写成static没关系,但是我们用来作为菜单的方法得是一个static方法,所以我们写了一个SayHelloWorld方法,执行后会输出一行日志到Console窗口中,为了让这个功能在菜单里面出现,我们加了一个属性MenuItem,这个属性来自UnityEditor模块,属性的内容就是我们菜单所在的位置,以/分割,第一部分就是直接显示在界面上的菜单名,后面每个/都是这个菜单内部的选项层级,我们看一下效果:

如果我们点击一下这个菜单,就会执行我们编写的那个static函数,输出一个Hello World!到我们的Console窗口中。

如果我们希望有更深层级的菜单,我们可以写成类似

[MenuItem("MenuTest/Say Hello World/Say1/Say2")]

那么我们就会看到这样的菜单:

OK,我们现在知道了菜单如何制作,这样我们就可以做一些我们想要的批量操作,比如说我们之前给游戏打包的时候,总是要一个个场景打开,然后添加到我们的Build Settings里面,很难受,那我们是不是可以直接通过一个菜单去做这个事情呢?当然可以!我们稍微修改一下我们的代码:

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public static class MenuTest
{
    [MenuItem("MenuTest/Add All Scenes to Build Settings")]
    public static void AddAllScenesToBuildSettings()
    {
        //获取所有场景
        string[] scenes = AssetDatabase.FindAssets("t:Scene");
        if (scenes == null || scenes.Length == 0)
        {
            Debug.Log("No scenes found.");
            return;
        }

        List<EditorBuildSettingsScene> editorBuildSettingsScenes = new List<EditorBuildSettingsScene>();

        //将所有场景添加到BuildSettings中
        foreach (string sceneGuid in scenes)
        {
            string scenePath = AssetDatabase.GUIDToAssetPath(sceneGuid);
            EditorBuildSettingsScene buildScene = new EditorBuildSettingsScene(scenePath, true);
            editorBuildSettingsScenes.Add(buildScene);
        }

        EditorBuildSettings.scenes = editorBuildSettingsScenes.ToArray();
        Debug.Log("All scenes have been added to Build Settings.");
    }
}

这里我们调用了AssetDatabase.FindAssets这个API,这是专门用来在编辑器内查找资源的方法,类似Resources.Load,但是范围是工程内的全体资源,参数t:Scene表示类型是场景资源,关于这里面的搜索用的魔法字符串,Unity官方的API文档其实说的很模糊,但是基本上可以理解要怎么写。

找到所有工程里面的场景类型的资源后,得到的是一堆guid字符串,因为我们要设置打包所包含场景的API要求我们提供的是场景资源的路径,所以我们需要通过guid字符串转换资源路径,也就是通过AssetDatabase.GUIDToAssetPath这个API,然后通过这个路径新建一个EditorBuildSettingsScene结构体。

为每个场景资源都新建好这个结构体后传递给EditorBuildSettings.scenes这个数组赋值。

OK,我们点一下这个菜单执行后再打开Build Settings看看发生了什么:

好家伙,不仅是我们Assets文件夹下的进来了,连Packages里面其他功能包带的场景资源也一起放进来了,当然我们不需要这些资源,可以手动删一下。

由此我们可以了解到,Unity除了提供给我们游戏运行时所需的API之外,还提供了一系列API帮助我们调用编辑器的功能,这样能够更好的完成我们的工作,而这些编辑器相关的API都在UnityEditor这个模块下。

UnityEngin和UnityEditor

既然讲到UnityEditor,我们需要明确一下UnityEditor的概念和使用范围,如果我们现在直接打包游戏,会得到一个编译报错:

Assets\MenuTest.cs(7,6): error CS0246: The type or namespace name 'MenuItemAttribute' could not be found (are you missing a using directive or an assembly reference?)
Assets\MenuTest.cs(7,6): error CS0246: The type or namespace name 'MenuItem' could not be found (are you missing a using directive or an assembly reference?)

初看起来很奇怪,报错提示MenuItemAttribute和MenuItem的定义在我们刚写的MenuTest代码里面没找到,但是我们刚刚明显跑起来我们的代码了呀!

但是仔细想想也合理,因为我们调用的是UnityEditor模块提供的功能,而不是UnityEngine,这意味着我们的功能只在编辑器下可用,但是我们游戏打包肯定不会带上编辑器的功能,其结果就显然会报错找不到相关类型定义。

那么为了解决这个问题,Unity提供了两个方案:

  1. 将所有涉及编辑器功能的代码,放在Editor文件夹下,这个跟Resources文件夹类似的规则,只要父目录叫Editor就行了,不管是几级父目录,也就是说任何叫Editor的文件夹下面所有的代码都不会在打包的时候打包进去。
  2. 使用C#里面预处理器宏,跟C++很类似,但是Unity会预先定义一些,UNITY_EDITOR就是其中之一,例如:
#if UNITY_EDITOR
// 这里面的代码打包不会带上
#endif

这种写法非常适合在游戏需要执行的代码例如某个组件里面,调用一些编辑器功能,以方便提供在编辑器下跑起来的debug能力,但是又不会带到游戏打包里面去,当然这样写的时候要注意,using UnityEditor;这个部分也得用#if UNITY_EDITOR包裹起来,这个很容易忘记。

扩展编辑器界面

Unity也支持开发者自己绘制编辑器界面,例如数组序列化出来的编辑界面就是以前一个三方组件的功能,后来纳入到了官方引擎内作为默认的展示方法。

在学习如何扩展之前,我们需要了解编辑器用的是什么绘制方案:IMGUI,如果没接触过的话可能会比较懵逼,我们来了解一下官方的说明:

    void OnGUI() {
        if (GUILayout.Button("Press Me"))
            Debug.Log("Hello!");
    }

上面这个代码就可以在界面中绘制这样一个按钮,而这个按钮的点击状态会直接通过绘制函数返回回来,进而直接当场判断并执行输出一个Hello的逻辑。

当然这样说其实是不准确的,这样的UI绘制方法其实会将OnGUI跑多遍,第一遍进行布局计算,第二遍处理用户输入之类的事件,第三遍提交渲染,当然我说的可能也会随着不同的IMGUI的实现而不同,但是大同小异。

如果对Unity的这么小点代码还是不太能理解,可以直接看IMGUI中比较有名的GUI库:

https://github.com/ocornut/imgui​github.com/ocornut/imgui

OK,这种UI写作方式,其实不去深入了解的话只是用,还是上手很快。

Unity提供了多种方法给我们扩展编辑器:

  1. 直接自己画一个窗口出来,就像画一个Inspector面板一样,这个窗口内部所有的内容都是我们自己画的
  2. 在已有的窗口画额外的内容,其实和1类似,只是我们需要先找到这个窗口,然后继续画
  3. 对序列化的成员的编辑器下编辑控件展示进行自定义

1和2,都可以直接通过参考官方文档(Unity - Manual: Editor Windows)可以学习到,因为都是GUI.xxx接口绘制,相对来说很好理解,这里就让大家自己学了。

我们这里主要讲解3。

针对我们在组件里面写的成员变量,我们是可以通过写成public来让它参与到序列化中,这样Inspector面板上就会显示编辑控件,例如Int就会显示出来一个输入框给我们输入数据等。

回想一下,我们之前有一个FireController需要填写Bullet的资源路径,从而能用Resources.Load加载它,但是这个路径就是一个纯粹的字符串,我们很容易写错,如果写错了,跑到游戏里面触发对应逻辑才能发现,如果游戏流程比较长,这无疑会增加返工的成本。

所以我们期望像类似拖拽prefab赋值一样去赋值这个路径,但是存储到序列化的文件中我们希望还是路径,不能是资源引用(这样做看起来似乎没意义,但是对于资源的动态加载来说是必须的)。

OK,我们现在了解一下Unity提供的方法:Property Drawers

我们新建一个脚本叫PrefabPathDrawer,同时我们也需要新建一个脚本叫PrefabPathVariableAttribute

PrefabPathVariableAttribute的代码比较简单:

using UnityEngine;

public class PrefabPathVariableAttribute : PropertyAttribute
{
}

继承自PropertyAttribute这个类,这个PropertyAttribute其实继承自C#的Attribute,顾名思义就是一个标记用的属性,因为我们也没什么额外需求,所以这个属性没有参数,就是个空的,只用来标记需要这样自定义绘制的成员变量。

然后我们需要编写PrefabPathDrawer:

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(PrefabPathVariableAttribute))]
public class PrefabPathDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        base.OnGUI(position, property, label);
    }
}

PrefabPathDrawer继承自PropertyDrawer,并且有一个属性CustomPropertyDrawer,参数就是我们刚才用来标记的属性类型PrefabPathVariableAttribute,显然Unity也是通过CustomPropertyDrawer这个属性来收集到我们所有需要自定义绘制的PropertyDrawer以及这个自定义绘制影响的范围。

我们要重写的回调函数是OnGUI,这个函数给了我们几个参数,position表示目前界面绘制到哪里了,我们应该从position的位置继续绘制,property则是受我们重绘逻辑影响的数据序列化的体现,通过操作property能够直接操作数据序列化的结果,label则是上层传来的label字段,我们一般也不需要动。

OK,想象一下,我们现在为了达到目的,需要做什么事情:

  1. 通过SerializedProperty获取到当前填的是啥路径字符串
  2. 通过这个路径拿到资源本身
  3. 绘制一个拖拽赋值Prefab的输入框,而不是文本输入框,这个输入框以2里面找到的资源当作当前的输入
  4. 拿到3的输入框的当前值,对比是否变更,如果变更了则获取这个资源的路径
  5. 通过4的路径更新SerializedProperty的数据

那么我们修改一下代码:

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(PrefabPathVariableAttribute))]
public class PrefabPathDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // 1. 获取当前填的字符串是啥
        string prefabPath = property.stringValue;
        // 2. 找资源
        GameObject res = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
        // 3. 绘制Object的输入框,而非文本的,同时开启检查变更
        EditorGUI.BeginChangeCheck();
        Object newRes = EditorGUI.ObjectField(position, label, res, typeof(GameObject), false);

        if (EditorGUI.EndChangeCheck())
        {
            // 4. 如果发生变更了,获取这个新资源的路径
            string newPath = AssetDatabase.GetAssetPath(newRes);
            // 5. 更新数据
            property.stringValue = newPath;
        }
    }
}

这里用了AssetDatabase来做资源的加载和路径查找,EditorGUI.ObjectField则是可以帮我们绘制一个拖拽放入GameObject对象的输入框,这个API最后一个false比较重要,代表允不允许赋值场景内的物体,由于我们需要赋值的是资源,所以填不允许,如果填允许的话,那么带有这个成员的组件所在物体只能在场景里面存在,毕竟给一个脱离场景存在的资源赋值场景内的物体,本身这个引用就有问题。

EditorGUI.BeginChangeCheck()和EditorGUI.EndChangeCheck()可以帮我们检查中间有没有发生GUI的内容变化,如果有的话我们才尝试更新序列化的值。

接下来我们需要将我们要自定义绘制的那个成员变量加上这个属性:

public class FireController : MonoBehaviour
{
    private bool isMouseDown = false;
    private float lastFireTime = 0f;
    private Vector3 fireDirection;
    private AddVelocity bullet;
    [PrefabPathVariable]
    public string bulletResourcesPath;
    public float fireInterval = 0.1f;
    public Transform fireBeginPosition;

这里我们将bulletResourcesPath加上了PrefabPathVariable属性,注意看,Attribute没有了,其实这就是C#的一个规则,属性的类型声明是PrefabPathVariableAttribute,但是实际用来标记的时候需要省略尾巴上的Attribute。

然后我们不用跑游戏,直接在场景里面选中FireController就可以看到效果:

原本这里第一个是输入字符串的输入框,现在已经变成了可以拖拽放入Prefab的输入框,我们可以随意拖放GameObject上去,保存后,然后可以查看这个场景序列化出来的结果:

可以看到界面上我们虽然是赋值物体的形式,但是实际上我们序列化存储的确实是资源路径。

但是这里有个问题,我们使用AssetDatabase系列API获取的资源路径其实是Assets目录下的,但是我们的资源加载却是用的Resrouces.Load,这样路径会不符合要求,所以我们在资源加载的地方也需要做一下适配:

  1. 裁掉前面的Resources文件夹路径
  2. 去掉文件后缀

我们稍微调整一下FireController里面加载资源的代码:

    private void Start()
    {
        if (bulletResourcesPath != null)
        {
            int lastIndex = bulletResourcesPath.LastIndexOf("Resources/", StringComparison.OrdinalIgnoreCase);
            if (lastIndex != -1)
            {
                bulletResourcesPath = bulletResourcesPath.Substring(lastIndex + "Resources/".Length);
                string fileExtension = Path.GetExtension(bulletResourcesPath);
                bulletResourcesPath = bulletResourcesPath.Substring(0, bulletResourcesPath.Length - fileExtension.Length);
                bullet = Resources.Load<AddVelocity>(bulletResourcesPath);
            }
        }
    }

同样的Fire函数里面也记得给bullet资源判空。

OK,现在再跑一下游戏,就应该可以正常发射子弹了。

One more thing:

PropertyDrawer也来自UnityEditor,我们需要把这个代码也移动到Editor文件夹下:

下一章

基本的引擎使用方法其实已经教了不少,通过这些使用方法的学习,其实可以整理出一些学习的方法论,下一章我们将会整理一下本大章所学的所有内容,拼凑出一些基本的思路和方法,这样我们的引擎入门基本算是摸到了一定的方向,为后面更深入的学习也能打下更好的基础。

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

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

相关文章

C++ STL学习之【容器适配器】

✨个人主页&#xff1a; 夜 默 &#x1f389;所属专栏&#xff1a; C修行之路 &#x1f38a;每篇一句&#xff1a; 图片来源 A year from now you may wish you had started today. 明年今日&#xff0c;你会希望此时此刻的自己已经开始行动了。 文章目录 &#x1f307;前言&a…

内存泄漏动态检测(valgrind)

初步判断是否有泄漏 Linux 内存性能优化 —— 高内存使用及内存泄漏排查 比如该文的介绍&#xff0c;需要初步了解top free -h等命令&#xff1b; 主要看free 内存泄漏检测方法&#xff1a; 静态我常用的是cppcheck&#xff1b; 动态的 Linux下内存泄漏定位方法 这个文章…

Node.js 一:基础

1.node.js是什么&#xff1a; 2.node.js的作用&#xff1a; 1.服务器应用&#xff1a;b站.. 2.开发工具类应用&#xff1a;webpack&#xff0c;bable等等 3.桌面端应用&#xff1a;vscode&#xff0c;postman等 这些应用都是基于electron框架开发&#xff0c;electron基于node…

【设计模式】23种设计模式之行为型模式

一、模板方法模式 1、豆浆制作问题 编写制作豆浆的程序&#xff0c;说明如下: 1&#xff09;、制作豆浆的流程 选材--->添加配料--->浸泡--->放到豆浆机打碎 2&#xff09;、通过添加不同的配料&#xff0c;可以制作出不同口味的豆浆 3&#xff09;、选材、浸泡和放…

拥抱还是革命,ChatGPT时代 AI专家给出15条科研生存之道

来源&#xff1a;专知 微信号&#xff1a;Quan_Zhuanzhi 你是学术机构的人工智能研究员吗?你是否担心自己无法应对当前人工智能的发展步伐?您是否觉得您没有(或非常有限)访问人工智能研究突破所需的计算和人力资源?你并不孤单; 我们有同样的感觉。越来越多的人工智能学者不…

vuepress-yarn-nodes-静态网页_个人博客搭建

nodes官网&#xff1a;https://nodejs.org/en 先下载nodes进行安装&#xff0c;一般nodes会自带包管理器npm&#xff0c;注意npm与nodes的对应关系&#xff0c;除了npm之外还有yarn包管理器&#xff0c;一般会用npm安装这个包 npm install --global yarnnpm降低版本的方法 np…

LeetCode 1026. 节点与其祖先之间的最大差值

【LetMeFly】1026.节点与其祖先之间的最大差值 力扣题目链接&#xff1a;https://leetcode.cn/problems/maximum-difference-between-node-and-ancestor/ 给定二叉树的根节点 root&#xff0c;找出存在于 不同 节点 A 和 B 之间的最大值 V&#xff0c;其中 V |A.val - B.val…

《花雕学AI》24:如何用万能Prompt公式与ChatGPT进行高效的对话测试

引言 你是否想要与人工智能进行有趣、有价值、有说服力的对话&#xff1f;你是否想要使用ChatGPT这个强大而灵活的对话生成器来创造出任何类型和主题的对话&#xff1f;如果是这样&#xff0c;那么你需要了解一个简单而强大的工具&#xff0c;就是万能Prompt公式。 万能Promp…

微软office认证课程

感谢阅读 非原创声明&#xff0c;根据微软课程学习理解而已Get started with Microsoft 365Work Smarter with Microsoft Word与 Microsoft Word 更加智能地工作Microsoft 365 SubscriptionsMicrosoft 365 订阅个人补充Exercise: Finding Help and changing account settings练…

cpp 程序与 c 程序的相互调用(extern “C“ 的使用)

实际在编译的过程中&#xff0c;.cpp 文件调用 .c文件中的函数会出错。假设代码结构如下&#xff1a; 目录 一、编译过程分析 1、预处理 2、编译 3、汇编 4、链接 二、问题解决 1、解决方案 2、解决思路 一、编译过程分析 1、预处理 该阶段头文件会被展开&#xff0c…

FPGA学习笔记(二):时序逻辑之计数器

与组合逻辑&#xff08;给定输入&#xff0c;输出是确定的&#xff0c;与时间无关&#xff09;相比较&#xff0c;时序逻辑不仅仅与输入信号相关&#xff0c;还与时钟信号相关。 D触发器&#xff1a;在上升沿时&#xff08;CLK&#xff09;才将输出&#xff08;Q&#xff09;修…

Failed to load property source from location ‘classpath:/application.yaml‘

问题 项目起动时&#xff0c;找不到yaml&#xff0c;各种处理还是没有解决&#xff0c;比如&#xff0c;clean&#xff0c;重启电脑&#xff0c;检查utf8等。 同事拉的新代码又是正常可以跑&#xff0c;这就有点奇怪了。 11:07:39.682 [main] ERROR org.springframework.boot.…

【19】linux进阶——后台运行()和nohup命令

大家好&#xff0c;这里是天亮之前ict&#xff0c;本人网络工程大三在读小学生&#xff0c;拥有锐捷的ie和红帽的ce认证。每天更新一个linux进阶的小知识&#xff0c;希望能提高自己的技术的同时&#xff0c;也可以帮助到大家 另外其它专栏请关注&#xff1a; 锐捷数通实验&…

分享自己接私活常用的开源系统

目前开源系统是JNPF框架。技术栈上是SpringBoot、SpringCloud、Spring Web、MyBatis、Swagger、Vue、Element。 这些都是比较主流的技术&#xff0c;无论是技术层面的先进性还是学习难度都是比较低的&#xff0c;目前网络上有大量可供参考学习的资料。 并且它支持前后端分离和…

Ceph入门到精通-Ceph如何扩展到超过十亿个对象?

Ceph如何扩展到超过十亿个对象&#xff1f;-ceph部署多少个节点 (51cto.com) 越来越多的组织被要求管理数十亿个文件和几百上千PB的数据。无论是在公共云还是本地环境中&#xff0c;Ceph对象存储都是值得考虑的一个选项。本篇文章将通过七部分的精选内容为下面这些问题提供答案…

vue3+echarts实现世界地图以及轨线(label使用fomatter+rich动态添加图片及背景色,以及label如何添加动态边框色)

背景 最近项目开发&#xff0c;需要开发一个世界地图&#xff0c;并且实现经纬度对应的点对点轨线动效&#xff0c;效果如下&#xff1a; 问题 如何在刚打开页面的时候就显示地点名称label如何同时添加图片和背景色label怎么动态修改字体颜色及图片以及动态边框色添加动效及…

YumRepo Error: All mirror URLs are not using ftp, http[s] or file解决办法

文章目录 一、问题背景二、问题原因三、解决方法 一、问题背景 CentOS6.5利用yum -y install gcc命令安装程序报错YumRepo Error: All mirror URLs are not using ftp, http[s] or file.Eg. Invalid release/repo/arch combination/ removing mirrorlist with no valid mirror…

maven安装教程(结合eclipse和IDEA)

一.安装maven 本文须知:安装maven环境之前要先安装java jdk环境(没有安装java环境的可以先去看安装JAVA环境的教程)Maven 3.3+ require JDK 1.7 及以上。 第一步:下载maven(本教程安装的是3.8.4) 官方下载链接:https://maven.apache.org/download.cgi Binary是可执行版本…

电子台账:生成的数据和图表导出到一个excel表中

目录 1 数据选择 1.1 选择1行数据 1.2 选择1列数据 2 图表设置 3 数据导出 为了便于进行数据分析和数据展示&#xff0c;可以把生成的汇总数据生成图表&#xff0c;然后对图表进行定制修改&#xff0c;最后把数据和图表一起导出到一个excel表中。 程序目前支持两种数据作…

【C++学习笔记】函数

值传递 值传递&#xff1a;函数调用时实参将数值传入给形参 做值传递时函数的形参发生改变&#xff0c;并不会影响实参 因为形参的作用域在函数内只有在调用函数时才会为其分配内存&#xff0c;函数调用结束后释放函数内的变量内存。 #include<iostream> using namespa…