前几天看麦扣视频里提到一个在Inspector可以把场景选择由输字符串改变为下拉菜单的功能,挺实用的,然后就想自己写一下。
因为之前对自定义编辑器这方面内容经验不是很多,而自定义attribute更是没有涉及过,所以还是走了不少弯路的……其实最主要的还是反射啊attribute啊这些语法没有深入了解过……
需求 首先明确一下需要做到什么。从视频里可以看到,麦扣导入了一个dll,然后就可以用一个[SceneName]的attribute来修饰string,从而让其在inspector中以下拉菜单的形式进行选择。
这样的功能主要包括两个部分的内容:自定义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]即可通过下拉菜单选择场景名。
如果有朋友发现最终版的代码仍然有漏洞,欢迎批评指正。



