栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 游戏开发 > 其他

Unity自定义Attribute实现下拉菜单场景选择

其他 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Unity自定义Attribute实现下拉菜单场景选择

前几天看麦扣视频里提到一个在Inspector可以把场景选择由输字符串改变为下拉菜单的功能,挺实用的,然后就想自己写一下。

因为之前对自定义编辑器这方面内容经验不是很多,而自定义attribute更是没有涉及过,所以还是走了不少弯路的……其实最主要的还是反射啊attribute啊这些语法没有深入了解过……

需求 首先明确一下需要做到什么。

从视频里可以看到,麦扣导入了一个dll,然后就可以用一个[SceneName]的attribute来修饰string,从而让其在inspector中以下拉菜单的形式进行选择。


这样的功能主要包括两个部分的内容:自定义attribute、获取场景名称。

实现 我在实现的时候整体分为四个部分:首先是怎么自定义出attribute;然后是怎么获取所有场景的名称;最后是怎么让attribute把值反馈回字段,最后的最后是修复一些bug(实际上是完善功能)。 定义attribute

其实定义attribute还是比较简单的,而且网上也有很多资料,这里就贴一下代码吧。

代码包括两部分:定义部分和编辑器扩展部分。

using System;
using UnityEngine;

[AttributeUsage(AttributeTargets.Field)]
public class SceneName : PropertyAttribute
{ }
//就这些,标签就能用了。虽然现在还不起任何作用。

然后,为了在编辑器中得到下拉菜单的效果,编写编辑器扩展部分。同时为了方便之后存储和读取数据,在SceneName类中定义selected和name两个变量,分别表示下拉菜单中选中的场景序号和场景名称。

//SceneName类
using System;
using UnityEngine;

[AttributeUsage(AttributeTargets.Field)]
public class SceneName : PropertyAttribute
{
    int _selected;
    string _name;
    public int selected { get { return _selected; } set { _selected = value; } }
    public string name { get { return _name; } set { _name = value; } }
}

//SceneNameEditor类
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(SceneName))]
public class SceneNameEditor : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        SceneName sceneName = attribute as SceneName;
        sceneName.selected = EditorGUI.Popup(position, label.text, sceneName.selected, new string[] { "a", "b" });
        sceneName.name = scenes[sceneName.selected].text;
    }
}

注意:编辑器扩展部分代码需要放在Editor文件夹内。

上边两个脚本就可以实现在inspector里以下拉菜单形式选择字符串了。

完成这部分之后,就可以开始考虑如何获取场景名称列表了。

获取场景

EditorBuildSettings.scenes这一变量的值正是build settings中的场景列表。所以用它来获取场景名称的列表是非常合适的。

但是有一个问题,它存储的是EditorBuildingSettingsScene类型的数据,而通过它只能获得path,也就是场景路径。所以现在的问题就是,将场景路径转换为场景名称。

我们只需要从路径中裁剪出场景名即可。

由于路径都是以“/”为分隔符,以“.unity”结尾的,所以可以方便地处理出场景名称。

//SceneNameEditor类
    GUIContent[] GetSceneNames()
    {
        GUIContent[] g = new GUIContent[EditorBuildSettings.scenes.Length];
        for(int i=0;i
            string[] splitResult = EditorBuildSettings.scenes[i].path.Split('/');
            string nameWithSuffix = splitResult[splitResult.Length - 1];
            g[i] = new GUIContent(nameWithSuffix.Substring(0, nameWithSuffix.Length - ".unity".Length));
        }
        return g;
    }

这段代码里我返回了GUIContent类型的数据……这是因为,前边用到的Popup函数有一个使用GUIContent类型数据作为参数的重载。为了用那个重载,我就改用GUIContent类型了。(其实我也不太清楚它和string在这里有什么区别……好像效果上没有差别)

下面是更改后的SceneNameEditor类:

//SceneNameEditor类
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(SceneName))]
public class SceneNameEditor : PropertyDrawer
{
    GUIContent[] scenes;
    GUIContent[] GetSceneNames()
    {
        GUIContent[] g = new GUIContent[EditorBuildSettings.scenes.Length];
        for(int i=0;i
            string[] splitResult = EditorBuildSettings.scenes[i].path.Split('/');
            string nameWithSuffix = splitResult[splitResult.Length - 1];
            g[i] = new GUIContent(nameWithSuffix.Substring(0, nameWithSuffix.Length - ".unity".Length));
        }
        return g;
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        SceneName sceneName = attribute as SceneName;
        scenes = GetSceneNames();
        sceneName.selected = EditorGUI.Popup(position, label, sceneName.selected, scenes);
        sceneName.name = scenes[sceneName.selected].text;
    }
}

现在就可以在inspector中得到一个可以选择场景名称的下拉菜单了。

反馈attribute存储的值

这一步我卡了最长时间……看了不少反射、attribute之类的内容,然后发现好像不太好弄……

我的想法是,总要有个可以和被修饰字段之间产生联系的变量吧,不可能没什么联系就说attribute修饰了某个字段吧。

然后就找到了attribute和fieldInfo两个变量。

它俩确实记录了和哪个对象、哪个变量产生了联系,但要获取具体是哪个对象,感觉不好办。

直到最后我才意识到OnGUI还有个property参数…… 它记录了是哪个对象用到了这个attribute。然后就比较简单了,用fieldInfo的SetValue方法即可设置数值。

//SceneNameEditor类
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(SceneName))]
public class SceneNameEditor : PropertyDrawer
{
    static GUIContent[] scenes;
    GUIContent[] GetSceneNames()
    {
        GUIContent[] g = new GUIContent[EditorBuildSettings.scenes.Length];
        for(int i=0;i
            string[] splitResult = EditorBuildSettings.scenes[i].path.Split('/');
            string nameWithSuffix = splitResult[splitResult.Length - 1];
            g[i] = new GUIContent(nameWithSuffix.Substring(0, nameWithSuffix.Length - ".unity".Length));
        }
        return g;
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        SceneName sceneName = attribute as SceneName;
        scenes = GetSceneNames();
        sceneName.selected = EditorGUI.Popup(position, label, sceneName.selected, scenes);
        sceneName.name = scenes[sceneName.selected].text;
        fieldInfo.SetValue(property.serializedObject.targetObject, sceneName.name);
    }
}

这样就可以把值反馈回用attribute修饰的字段了。

但是……

把脚本挂到物体上然后看了看效果后,突然发现这样还不行:attribute会被删除和重新创建。这样一来,每当attribute被重新创建之后,就会导致被标记的内容恢复到默认值。

于是我考虑到在OnGUI里首先用字段已有的值来更新attribute内的存储值。

//SceneNameEditor类
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(SceneName))]
public class SceneNameEditor : PropertyDrawer
{
    static GUIContent[] scenes;
    GUIContent[] GetSceneNames()
    {
        GUIContent[] g = new GUIContent[EditorBuildSettings.scenes.Length];
        for(int i=0;i
            string[] splitResult = EditorBuildSettings.scenes[i].path.Split('/');
            string nameWithSuffix = splitResult[splitResult.Length - 1];
            g[i] = new GUIContent(nameWithSuffix.Substring(0, nameWithSuffix.Length - ".unity".Length));
        }
        return g;
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        SceneName sceneName = attribute as SceneName;
        scenes = GetSceneNames();
        sceneName.selected = 0;
        sceneName.name = scenes[0].text;
        
        string cntString = (string)fieldInfo.GetValue(property.serializedObject.targetObject);
        for(int i=0;i
            if(scenes[i].text.Equals(cntString))
            {
                sceneName.selected = i;
                sceneName.name = cntString;
                break;
            }
        }
        //
        sceneName.selected = EditorGUI.Popup(position, label, sceneName.selected, scenes);
        sceneName.name = scenes[sceneName.selected].text;
        fieldInfo.SetValue(property.serializedObject.targetObject, sceneName.name);
    }
}

上边用“/”框起来的就是做了改动的内容。效果还可以,但是当然,时间开销会增加不少……(目前自己做的东西,还是能跑为第一追求)

结束了吗?

我也以为结束了。

但是在测试的时候,发现如果在inspector中修改字段值之后退出工程再重进,并不会保存。

举个例子,假如上次打开编辑器,我把一个字段从scene1改成了scene2,那么在重新打开的时候,它可能还是scene1。

多次测试+查询资料,发现是这样一回事:自定义编辑器可能不会使场景变dirty,也就是不会让场景知道自己已经做了修改但还没保存。也就是说,需要让编辑器知道自己已经“脏”了,才会使修改可以被保存。

因此目前来说,最终版代码如下:

//SceneNameEditor类
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(SceneName))]
public class SceneNameEditor : PropertyDrawer
{
    static GUIContent[] scenes;
    GUIContent[] GetSceneNames()
    {
        GUIContent[] g = new GUIContent[EditorBuildSettings.scenes.Length];
        for(int i=0;i
            string[] splitResult = EditorBuildSettings.scenes[i].path.Split('/');
            string nameWithSuffix = splitResult[splitResult.Length - 1];
            g[i] = new GUIContent(nameWithSuffix.Substring(0, nameWithSuffix.Length - ".unity".Length));
        }
        return g;
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        SceneName sceneName = attribute as SceneName;
        scenes = GetSceneNames();
        string cntString = (string)fieldInfo.GetValue(property.serializedObject.targetObject);
        sceneName.selected = 0;
        sceneName.name = scenes[0].text;
        for(int i=0;i
            if(scenes[i].text.Equals(cntString))
            {
                sceneName.selected = i;
                sceneName.name = cntString;
                break;
            }
        }
        sceneName.selected = EditorGUI.Popup(position, label, sceneName.selected, scenes);
        sceneName.name = scenes[sceneName.selected].text;
        fieldInfo.SetValue(property.serializedObject.targetObject, sceneName.name);
        ///
        if(GUI.changed)
        {
            EditorUtility.SetDirty(property.serializedObject.targetObject);
        }
        //
    }
}

到此为止,我没有发现有其他什么bug。所以上边的就是目前的最终版代码(SceneName类开始已经写好,后来没有修改)。

在使用的时候,只需要为string类型的字段标上一个[SceneName]即可通过下拉菜单选择场景名。

如果有朋友发现最终版的代码仍然有漏洞,欢迎批评指正。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/901219.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号