Unity编辑器拓展精粹学习笔记(SIKI学院)

本篇博客为SIKI学院学习笔记。
功能1:在菜单栏中实现编辑器拓展功能。(可以自定义添加组件等)
例如:软甲开发中为了提高效率,在编辑界面需要快速添加一些层级,每次都手动添加浪费时间,此时则需要用到编辑器拓展。

问题1:
如何拓展编辑器(即添加自己自定义选项):创建任意一个类,添加如下代码:

    [MenuItem("编辑器/Test")]  static void Test(){Debug.Log("调用了自定义编辑器");}

编辑界面效果:
在这里插入图片描述
点击效果如下:
在这里插入图片描述
问题2:
具体使用的时候,不同情况下的参数不同,如何解决。
方法1:手动在Inspector界面改。(挨个找比较麻烦)
在这里插入图片描述
方法2:添加额外的自定义编辑界面(方便直观)
代码如下:

    [MenuItem("编辑器/SetupUiRoot")]static void SetupUiRoot(){var window = GetWindow<CreatrUIRootWindow>();window.Show();}//这部分代码是编辑界面参数设置private void OnGUI(){//下面的所有参数都在同一行显示GUILayout.BeginHorizontal();GUILayout.Label("w", GUILayout.Width(10));GUILayout.TextField("720");GUILayout.Label("h", GUILayout.Width(10));GUILayout.TextField("1280");//同一行显示的参数到此结束GUILayout.EndHorizontal();//编辑界面按钮if (GUILayout.Button("Setup")){Setup();this.Close();}}

实际演示效果:
在这里插入图片描述
至此完成自定义编辑拓展要求。
额外需求1:只能点击一次,当已存在的时候不能再点击。
代码如下:

    //为true时才会执行代码[MenuItem("编辑器/SetupUiRoot", true)]static bool ValidateUIRoot(){//判断条件return !GameObject.Find("UIRoot");}[MenuItem("编辑器/SetupUiRoot")]static void SetupUiRoot(){var window = GetWindow<CreatrUIRootWindow>();window.Show();}

效果如下:存在时则变为黑色。
在这里插入图片描述
额外需求2:有一些属性为私有,但是初始化的时候想赋值该如何设置?
使用序列化,代码如下:

public class UIRoot : MonoBehaviour {public GameObject test1;[SerializeField] private GameObject test2;
}
    void Setup(){var uiRoot= new GameObject("UIRoot");uiRoot.AddComponent<UIRoot>();var test1 = new GameObject("test1");test1.transform.SetParent(uiRoot.transform);//公开变量直接赋值即可。uiRoot.GetComponent<UIRoot>().test1 = test1;var test2 = new GameObject("test2");test2.transform.SetParent(uiRoot.transform);//私有变量比较麻烦,用序列化的方法赋值var uiRootScriptSerializedObj = new SerializedObject(uiRoot.GetComponent<UIRoot>());uiRootScriptSerializedObj.FindProperty("test2").objectReferenceValue = test2;uiRootScriptSerializedObj.ApplyModifiedPropertiesWithoutUndo();}

功能2:在Hierarchy面板右键可以添加自定义功能
功能如下:
1、自动创建一个与当前游戏物体相同名称的脚本并挂载在物体上。
2、获取所有绑定脚本的子类,并将他们作为成员。
3、编辑脚本时只会重新添加成员,不会对已经写得代码功能有改动
在这里插入图片描述

代码如下:

//绑定的信息,保存了绑定脚本的子物体路径
public class BindInfo
{public string FindPath;
}//创建脚本类,其实就是创建了text记事本文件,写入相关代码
public class CompontDesignerTemplate
{public static void Write(string name,string scriptFolder, List<BindInfo>bindInfos){var scriptFile = scriptFolder + "/" + name + "Designer.cs";var writer = File.CreateText(scriptFile);writer.WriteLine("using UnityEngine;");writer.WriteLine();writer.WriteLine("public partial class "+ name+" {");foreach (var bindInfo in bindInfos){writer.WriteLine("\tpublic GameObject " + bindInfo.FindPath.Split('/').Last()+" ;");}writer.WriteLine("}");writer.Close();}
}public class CompontTemplate
{public static void Write(string name, string scriptFolder){var scriptFile = scriptFolder + "/" + name + ".cs";if (File.Exists(scriptFile))return;var writer = File.CreateText(scriptFile);writer.WriteLine("using UnityEngine;");writer.WriteLine();writer.WriteLine("public partial class " + name + " : MonoBehaviour{");writer.WriteLine("//这个是修改也不会被覆盖脚本文件");writer.WriteLine("}");writer.Close();}
}public  static class CreateComponentCode 
{//父物体下所有绑定脚本的子物体信息,父物体脚本会根据这些信息去判断声明几个成员变量public static List<BindInfo> mBindInfos = new List<BindInfo>();[MenuItem("GameObject/CreateCode",false,0)]static void CreateCode(){//获取选中的游戏物体var gameObject = Selection.objects.First() as GameObject;if (!gameObject){Debug.Log("请选择物体后再进行操作");return;}//保存脚本的路径var scriptFolder = Application.dataPath + "/Scripts";//判断路径是否存在if (!Directory.Exists(scriptFolder)){Directory.CreateDirectory(scriptFolder);}mBindInfos.Clear();//搜索所有绑定脚本的子物体,此处用到了递归//绑定信息需要寻找两次,一次用来创建脚本,一次用来赋值。之所以要寻找两次,是因为可能数据由于调用顺序会被覆盖SearchBinds("", gameObject.transform, mBindInfos);//创建父物体的脚本,创建两个,一个用来初始化,一个用来修改,使用了partial的修饰,创建的时候一个会判断当前文件是否存在,一个不会判断直接覆盖CompontTemplate.Write(gameObject.name, scriptFolder);CompontDesignerTemplate.Write(gameObject.name, scriptFolder, mBindInfos);EditorPrefs.SetString("GENERATE_CLASS_NAME", gameObject.name);AssetDatabase.Refresh();}static void SearchBinds(string path,Transform transform,List<BindInfo> binds){//判断该物体是否有绑定相应的脚本var bind = transform.GetComponent<Bind>();var isRoot = string.IsNullOrEmpty(path);//有并且不是根目录,则将这些信息添加到列表中if (bind && !isRoot){binds.Add(new BindInfo(){ FindPath = path });}foreach (Transform childTran in transform){SearchBinds(isRoot ? childTran.name : path + "/"+childTran.name, childTran, binds);}}//这个标签代表编译后再执行[DidReloadScripts]  static void AddComponentToGameObject(){var generateClassName = EditorPrefs.GetString("GENERATE_CLASS_NAME");EditorPrefs.DeleteKey("GENERATE_CLASS_NAME");if(string.IsNullOrEmpty(generateClassName)){return;}else{//固定格式,没有营养的一部分代码,调用相关API,通过序列化给父物体绑定脚本以及初始化。var assemblies = AppDomain.CurrentDomain.GetAssemblies();var defaultAssembly = assemblies.First(assembly => assembly.GetName().Name == "Assembly-CSharp");var type = defaultAssembly.GetType(generateClassName);var gameObject = GameObject.Find(generateClassName);var scriptComponent = gameObject.GetComponent(type);if(!scriptComponent){scriptComponent = gameObject.AddComponent(type);}var serialiedScript = new SerializedObject(scriptComponent);mBindInfos.Clear();//绑定信息需要寻找两次,一次用来创建脚本,一次用来赋值。之所以要寻找两次,是因为可能数据由于调用顺序会被覆盖SearchBinds("", gameObject.transform, mBindInfos);foreach (var bindInfo in mBindInfos){               string name = bindInfo.FindPath.Split('/').Last();//寻找所有的物体serialiedScript.FindProperty(name).objectReferenceValue = GameObject.Find(name);//只寻找自身的子物体,减少性能消耗,有时会提示空引用,原因未知//serialiedScript.FindProperty(name).objectReferenceValue = gameObject.transform.Find(name).gameObject;             }serialiedScript.ApplyModifiedPropertiesWithoutUndo();}}
}

总结:看似很复杂,但是只要拆分成一个一个小部分解决起来其实很简单。
另外,这个例子其实并没有学到多少编辑器相关的知识,反而是学习了不少序列化和反射的用法。
各个部分解决思路:
1、如何通过代码创建脚本?
使用systemIO的相关API,创建text,格式化创建脚本,传入相关参数即可。
2、如何判断所有子物体是否绑定了脚本?
使用递归,挨个判断,如果绑定则将这些子物体的路径添加到路径列表中,以便后续使用
3、父物体如何通过代码绑定并且添加脚本?
这个其实是功能实现的难点,常规使用代码添加脚本都是AddComponent,但是此处脚本不在Asset目录下,更没有编译没有通过常规方法,此处需要用到反射以及序列化进行绑定。详情可见代码CreateComponentCode 类。
PS:这次为什么讲解的没有以往详细?吐个槽,这个老师讲课是自己边讲边琢磨,而且后续改动大,如果按照一步一步来我需要重新将所有的代码全部敲一遍再讲解,过于浪费时间,所以改为在代码上添加详细注释,有不清楚的地方可以评论区评论,一起探讨。

功能3:
在Inspector面板添加自定义组件。
自定义组件添加快捷键功能。
在这里插入图片描述
代码如下:

//哪个类添加,子类也支持
[CustomEditor(typeof(Demo2), editorForChildClasses: true)]
public class CodeGenerateInfoInspector : Editor
{public override void OnInspectorGUI(){//拿到当前的脚本var inspectorTest = target as InspectorTest;GUILayout.BeginVertical("box");base.OnInspectorGUI();GUILayout.EndVertical();GUILayout.Label("代码生成部分", new GUIStyle(){fontStyle = FontStyle.Bold,fontSize = 20,});GUILayout.BeginHorizontal();GUILayout.Label("needText:");//前后参数要一致,否则无法修改inspectorTest.needText = GUILayout.TextField(inspectorTest.needText);GUILayout.EndHorizontal();if (GUILayout.Button("test")){Debug.Log(inspectorTest.needText);}}
}

快捷键功能实现:

    // %是Ctrl,&是Alt,#是Shift[MenuItem("编辑器/Test %&t")]  static void Test(){Debug.Log("调用了自定义编辑器");}

在这里插入图片描述
需要注意的是:在Hierarchy面板是不显示快捷键的,但是依旧有效,如果需要则需要自动手动添加。
在这里插入图片描述
在这里插入图片描述
总结:这部分功能较为简单,调用相关的API即可。

功能4:实现一键重启。
在正常工作中,有时候软件可能会出现意料之外的BUG,通常重启即可,但是手动重启比较麻烦,再此做个一键重启的简易功能。
代码如下:

public class ReOpenProject  {[MenuItem("编辑器拓展/重启项目 &r")]static void DoReOpenProject(){EditorApplication.OpenProject(Application.dataPath.Replace("Assets",string.Empty));}
}

完事。

全部总结:总体来说编辑器这段很简单,但是如果真的想实现一些功能需要用到其他的地方,如果只是简单实现添加组件主要了解一下MenuItem标签即可。
Demo


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部