编辑器拓展(入门与实践)
学习目标:入门编辑器并实现几个简单的工具
菜单编辑器
MenuItem
[MenuItem("编辑器拓展/MenuItem")]static void MenuItem(){Debug.Log("这是编辑器拓展");}
}
案例 1:在场景中的 GameObject 设置
- 1. @设置面板
- 2. @直接创建 GameObject 结构
- 3. @生成的 GameObject 结构进行赋值
- 4. @制作成 Prefab
当场景中没有UIRoot物体时方法可用,有了之后方法不启用
当第二个参数是true时表示是一个验证函数,用来验证这个功能是否启用
[MenuItem("编辑器拓展/SetupUIRoot", true)]static bool ValidateUIRoot(){var gameObject = GameObject.Find("UIRoot");return !gameObject;}[MenuItem("编辑器拓展/SetupUIRoot", false)]static void SetupUIRoot(){Debug.Log("这是编辑器拓展");var uiRootObj = new GameObject("UIRoot");var canvas = new GameObject("Canvas");canvas.transform.SetParent(uiRootObj.transform);canvas.AddComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;canvas.AddComponent<CanvasScaler>();canvas.AddComponent<GraphicRaycaster>();var eventSystem = new GameObject("EventSystem");eventSystem.transform.SetParent(uiRootObj.transform);eventSystem.AddComponent<EventSystem>();eventSystem.AddComponent<StandaloneInputModule>();}
第3个参数:优先级,主要影响菜单出现的先后顺序,不填默认是1000. 值越小,出现在越上层的位置。当一个菜单的优先级 - 它上一个菜单的优先级 >= 11,菜单之间还能看到分界线
快捷键 :可以为MenuItem可以添加快捷键。ctrl,shift,alt 都有对应的字符,比如表示shift键的字符是#。那么快捷键shift + q,可以写成 #q。
//注意:路径和快捷键之间有个空格[MenuItem("Learn/Log #q",false)]public static void Log(){Debug.Log(1);}
如果你想直接按一个键就触发,不想多按ctrl/shift/alt,那么可以用 ‘_’来表示。比如你想按F1就触发,那么可以写成 _F1
在Hierarchy层级窗口增加右键菜单
MenuItem还可以增加到右键菜单里面。其实用法和上面差不多,但有两个要求。
- MenuItem(“GameObject/xxxx”,false,0)命名时,名字必须以GameObject/开头
在Assets资源窗口增加右键菜单
- [MenuItem(“Assets/自定义菜单_Assets”)] 命名时,以Assets/开头 优先级可以不填
[MenuItem("Assets/自定义菜单_Assets")]public static void CustomMenu_Assets(){}
创建一个窗口
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.IO;public class CreateUIRootWindow : EditorWindow
{[MenuItem("编辑器拓展/SetupUIRoot", true)]static bool ValidateUIRoot(){var gameObject = GameObject.Find("UIRoot");return !gameObject;}private string mWidth = "720";private string mHeight = "1280";private void OnGUI(){GUILayout.BeginHorizontal();GUILayout.Label("width:", GUILayout.Width(45));mWidth = GUILayout.TextField(mWidth);GUILayout.Label("x", GUILayout.Width(10));GUILayout.Label("height", GUILayout.Width(50));mHeight = GUILayout.TextField(mHeight);GUILayout.EndHorizontal();if(GUILayout.Button("Setup")){var width = float.Parse(mWidth);var height = float.Parse(mHeight);Setup(width, height);Close();}}[MenuItem("编辑器拓展/SetupUIRoot", false)]static void SetupUIRoot(){var window = GetWindow<CreateUIRootWindow>();window.Show();}static void Setup(float width,float height){//UIRootvar uiRootObj = new GameObject("UIRoot");var uirootScript = uiRootObj.AddComponent<UIRoot>();uiRootObj.layer = LayerMask.NameToLayer("UI");//Canvasvar canvas = new GameObject("Canvas");canvas.transform.SetParent(uiRootObj.transform);canvas.AddComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;// CanvasScalervar canvasScaler = canvas.AddComponent<CanvasScaler>();canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;canvasScaler.referenceResolution = new Vector2(width, height);canvas.AddComponent<GraphicRaycaster>();canvas.layer = LayerMask.NameToLayer("UI");// EventSystemvar eventSystem = new GameObject("EventSystem");eventSystem.transform.SetParent(uiRootObj.transform);eventSystem.AddComponent<EventSystem>();eventSystem.AddComponent<StandaloneInputModule>();eventSystem.layer = LayerMask.NameToLayer("UI");// Bgvar bgObj = new GameObject("Bg");bgObj.AddComponent<RectTransform>();bgObj.transform.SetParent(canvas.transform);bgObj.transform.localPosition = Vector3.zero;uirootScript.Bg = bgObj.transform;// Commonvar commonObj = new GameObject("Common");commonObj.AddComponent<RectTransform>();commonObj.transform.SetParent(canvas.transform);commonObj.transform.localPosition = Vector3.zero;uirootScript.Common = commonObj.transform;// PopUIvar popUp = new GameObject("PopUp");popUp.AddComponent<RectTransform>();popUp.transform.SetParent(canvas.transform);popUp.transform.localPosition = Vector3.zero;uirootScript.PopUp = popUp.transform;// Forwardvar forwardObj = new GameObject("Forward");forwardObj.AddComponent<RectTransform>();forwardObj.transform.SetParent(canvas.transform);forwardObj.transform.localPosition = Vector3.zero;uirootScript.Forward = forwardObj.transform;var uirootScriptSerializedObj = new SerializedObject(uirootScript);uirootScriptSerializedObj.FindProperty("mRootCanvas").objectReferenceValue = canvas.GetComponent<Canvas>();uirootScriptSerializedObj.ApplyModifiedPropertiesWithoutUndo();// 制作 prefabvar savedFolder = Application.dataPath + "/Resources";if (!Directory.Exists(savedFolder)){Directory.CreateDirectory(savedFolder);}var savedFilePath = savedFolder + "/UIRoot.prefab";PrefabUtility.SaveAsPrefabAssetAndConnect(uiRootObj, savedFilePath, InteractionMode.AutomatedAction);}
}
效果:
自动生成代码
案例2 : 根据GameObject结构生成代码工具
- 在 Hierarchy上生成菜单
- 生成脚本
- 添加标记
- 搜索并生成脚本
EditorPrefs.GetString
public static string GetString (string key);
public static string GetString (string key, string defaultValue= "");
返回偏好设置文件中与 key
对应的值(如果存在)。
如果该值不存在,则返回 defaultValue
。请注意,EditorPrefs 不支持 null 字符串,因此如果 defaultValue
为 null,则返回空字符串。
[DidReloadScripts]
DidReloadScripts 是Unity引擎提供的一种事件,用于在脚本重新编译后自动执行指定的操作。这种事件非常适合用于在脚本编译完成后执行一些初始化或重置操作,以确保项目在编译后能够正常运行。
案例:自动刷新资源列表
在某些情况下,我们需要在脚本重新编译后自动刷新资源列表
using UnityEditor;
using UnityEngine;public class ResourceListRefresher
{[InitializeOnLoad]public static class ScriptReloader{static ScriptReloader(){EditorApplication.delayCall += () =>{EditorApplication.update += OnUpdate;};}private static void OnUpdate(){if (EditorApplication.isCompiling){return;}EditorApplication.update -= OnUpdate;RefreshResourceList();}}private static void RefreshResourceList(){Debug.Log("Resource list refreshed.");}
}
程序集
程序集定义 - Unity 手册
Unity关于程序集(Assembly )的那些事_unity assembly-CSDN博客
程序集定义和程序集引用是可创建用于将脚本组织为程序集的资源。
程序集是 C# 代码库,其中包含由脚本定义的已编译类和结构并且还定义了对其他程序集的引用。有关 C# 中的程序集的一般信息
程序集是代码进行编译是的一个逻辑单元,把相关的代码和类型进行组合,然后生成PE文件(例如可执行文件**.exe和类库文件.dll**)。由于程序集在编译后并不一定会生成单个文件,而可能会生成多个物理文件,甚至可能会生成分布在不同位置的多个物理文件,所以程序集是一个逻辑单元,而不是一个物理单元。即程序集在逻辑上是一个编译单元,但在物理储存上可以有多种存在形式。对于静态程序集可以生成单个或多个文件,而动态程序集是存在于内存中的。在C#中程序集处处可见,因为任何基于.NET的代码在编译时都至少存在一个程序集。
基于.NET框架的.dll库是一个完整的程序集,需要事先引用对应的类库。从代码的结构上看,一个程序集可以包含一个或多个命名空间,而每个命名空间中又可以包含子命名空间或类型列表。
应用程序结构:包含 应用程序域(AppDomain),程序集(Assembly),模块(Module),类型(Type),成员(EventInfo、FieldInfo、MethodInfo、PropertyInfo) 几个层次。
默认情况下,Unity 几乎将所有游戏脚本都编译到预定义 程序集 Assembly-CSharp.dll 中。(Unity 还会创建[一些较小的专用预定义程序集]。)
为什么使用程序集
这种安排对于小型项目而言可以接受,但是在向项目添加更多代码时会有一些缺点:
- 每次更改一个脚本时,Unity 都必须重新编译所有其他脚本,从而增加迭代代码更改的整体编译时间。
- 任何脚本都可以直接访问任何其他脚本中定义的类型,这样可能更加难以重构和改进代码。
- 所有脚本都针对所有平台进行编译。
通过定义程序集,可以组织代码以促进模块化和可重用性。为项目定义的程序集中的脚本不再添加到默认程序集中,并且只能访问指定的其他程序集中的脚本。
- 开发者可以自定义程序集,定义明晰的依赖关系,可以确保脚本更改后,只会重新生成必需的程序集,减少编译时间。
- 可以跨项目进行程序的复用,加快开发效率
- 支持跨语言的编程,例如可以在unity中使用C++语言编辑的DLL文件
如何在Unity中使用程序集
在Assets文件夹中编写脚本,如果没有进行自定义操作,会默认编译到 Assembly-CSharp.dll 中。
开发过程中如果进行了编辑器扩展,创建了Editor文件夹,并在该目录下编写了脚本,则默认编译到Assembly-CSharp-Editor.dll文件
DLL文件的生成路径:项目路径\Library\ScriptAssemblies\xxxx.dll
通过 Assets > Create > Assembly Definition,也可以直接通过右键菜单创建。,根目录下创建的程序集会取代Assembly-CSharp.dll
与新建的程序集处于同一层级或者处于子层级的所有脚本都编译到该程序集
Assembly Definition Reference
- 每个文件夹只能创建一个“Assembly Definition”或“Assembly Definition Reference”。
- Use GUID 选项
AddComponentMenu
[AddComponentMenu("EditorExtension/Bind")]
使用 AddComponentMenu 属性可在“Component”菜单中的任意位置放置脚本,而不仅是“Component > Scripts”菜单。
使用此属性可以更好地组织 Component 菜单,从而改进添加脚本时的工作流程
[ExecuteInEditMode]
Unity里继承MonoBehaviour的脚本,并不是一直都会执行的。Unity默认只有在Play模式下,游戏当前运行场景里的GameObject挂载的脚本才会执行,也就是说,我们必须按下Play按钮进入游戏,上述脚本才会执行。Unity还有另外的两种模式:Edit Mode和Prefab Mode,Edit Mode就是正常的Unity编辑状态,Prefab Mode是进入Prefab对其进行编辑时对应的模式。
为了满足使用的需求,Unity支持通过[ExecuteInEditMode]
或[ExecuteAlways]
两种参数使脚本在Play Mode以外的状态下被执行,[ExecuteEditMode]
支持脚本在Edit Mode下运行,[ExecuteAlways]
是在Unity2018.3及以后的版本新加入的功能,能够支持脚本一直运行。
值得注意的是,与PlayMode不同的是,函数并不会不停的执行。
Awake
和Start
:加载时调用,也就是脚本赋给物体的时候被调用Update
: 只有当场景中的某个物体发生变化时,才调用,当进程切出去再回来,也会调用一次。OnGUI
: 当GameView接收到一个Event时才调用。OnRenderObject
和其他的渲染回调函数 : SceneView或者GameView重绘时,比如,一直移动鼠标的时候OnRenderObject
会被调用
ExecuteAlways
可以看作是ExecuteInEditMode
的改进版,支持Prefab Mode下的脚本调用,其余的与ExecuteInEditMode
基本完全相同,具体代码如下:
[ExecuteAlways]
public class Example : MonoBehaviour {void Start(){if (Application.IsPlaying(gameObject)){// 若在Play Mode下// Play logic}else{// 若在Edit Mode下// Editor logic}}
}
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;namespace EditorExtension
{[CustomEditor(typeof(CodeGenerateInfo), editorForChildClasses: true)]public class CodeGenerateInfoInspector : Editor{public override void OnInspectorGUI(){base.OnInspectorGUI();var codeGenerateInfo = target as CodeGenerateInfo;GUILayout.BeginVertical("box");GUILayout.Label("代码生成部分", new GUIStyle(){fontStyle = FontStyle.Bold,fontSize = 15});GUILayout.BeginHorizontal();GUILayout.Label("Scripts Generate Folder:", GUILayout.Width(150));codeGenerateInfo.ScriptsFolder = GUILayout.TextField(codeGenerateInfo.ScriptsFolder);GUILayout.EndHorizontal();GUILayout.BeginHorizontal();codeGenerateInfo.GeneratePrefab = GUILayout.Toggle(codeGenerateInfo.GeneratePrefab, "Generete Prefab");GUILayout.EndHorizontal();if (codeGenerateInfo.GeneratePrefab){GUILayout.BeginHorizontal();GUILayout.Label("Prefab Generate Folder:", GUILayout.Width(150));codeGenerateInfo.PrefabFolder = GUILayout.TextField(codeGenerateInfo.PrefabFolder);GUILayout.EndHorizontal();}GUILayout.EndHorizontal();}}
}
实现效果:
其中一些知识点介绍:
CustomEditor(typeof(CodeGenerateInfo)
特性来描述要自定义的是哪个类, 第二个参数代表是否对其子类起效.这个描述类需要继承Editor类
CustomEditor 类有两个关键的属性 target 和 serializedObject ,前者代表想要自定义的类的实例化对象,默认是 object 类型。我们一般在OnEnable里将其转换为对应的类: _target = target as MonoTest;, serializedObject属性封装了对应类上所有的序列化字段和一些有用的方法, 每个字段对应一个序列化属性SerializedProperty, 并在使用之前把两者做绑定: m_IntValue = serializedObject.FindProperty("intValue");
然后重写OnInspectorGUI方法, 这个方法会在对象获得焦点或者对象属性变化或者其他一些情况下调用, base.OnInspectorGUI();表示按照Editor默认的行为将所有的序列化属性绘制出来. 意思就是如果没有其他代码的情况下, 属性面板和默认的绘制一致. 如果去掉这句话, 属性面板上只会绘制脚本的名字
负责 Write 代码的代码:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;namespace EditorExtension
{public class ComponentDesignerTemplate{public static void Write(string name, string scriptsFolder, List<BindInfo> bindInfos){var scriptFile = scriptsFolder + $"/{name}.Designer.cs";var writer = File.CreateText(scriptFile);writer.WriteLine($"// Generate Id:{Guid.NewGuid().ToString()}");writer.WriteLine("using UnityEngine;");writer.WriteLine();if (NameSpaceSettingsData.IsDefaultNamespace){writer.WriteLine("// 1.请在菜单 编辑器扩展/Namespace Settings 里设置命名空间");writer.WriteLine("// 2.命名空间更改后,生成代码之后,需要把逻辑代码文件(非 Designer)的命名空间手动更改");}writer.WriteLine($"namespace {NameSpaceSettingsData.Namespace}");writer.WriteLine("{");writer.WriteLine($"\tpublic partial class {name}");writer.WriteLine("\t{");foreach (var bindInfo in bindInfos){writer.WriteLine($"\t\tpublic {bindInfo.ComponentName} {bindInfo.Name};");}writer.WriteLine();writer.WriteLine("\t}");writer.WriteLine("}");writer.Close();}}public class ComponentTemplate{public static void Write(string name, string scriptsFolder){var scriptFile = scriptsFolder + $"/{name}.cs";if (File.Exists(scriptFile)){return;}var writer = File.CreateText(scriptFile);writer.WriteLine("using UnityEngine;");writer.WriteLine("using EditorExtension;");writer.WriteLine();if (NameSpaceSettingsData.IsDefaultNamespace){writer.WriteLine("// 1.请在菜单 编辑器扩展/Namespace Settings 里设置命名空间");writer.WriteLine("// 2.命名空间更改后,生成代码之后,需要把逻辑代码文件(非 Designer)的命名空间手动更改");}writer.WriteLine($"namespace {NameSpaceSettingsData.Namespace}");writer.WriteLine("{");writer.WriteLine($"\tpublic partial class {name} : CodeGenerateInfo");writer.WriteLine("\t{");writer.WriteLine("\t\tvoid Start()");writer.WriteLine("\t\t{");writer.WriteLine("\t\t\t// Code Here");writer.WriteLine("\t\t}");writer.WriteLine("\t}");writer.WriteLine("}");writer.Close();}}
}
实现功能的主要代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.IO;
using System;
using UnityEditor.Callbacks;namespace EditorExtension
{public class CreateComponentCode : EditorWindow{[MenuItem("编辑器拓展/NameSpaceSetting %t")]static void Open(){var window = GetWindow<CreateComponentCode>();window.Show();}private void OnGUI(){GUILayout.BeginHorizontal();GUILayout.Label("NameSpace:");NameSpaceSettingsData.Namespace = GUILayout.TextField(NameSpaceSettingsData.Namespace);GUILayout.EndHorizontal();}private static List<BindInfo> mBindInfos = new List<BindInfo>();[MenuItem("GameObject/@(Alt+T) EditorExtension-Bind &t",false,0)]static void Bind(){var gameObject = Selection.objects.First() as GameObject;if (!gameObject){Debug.LogWarning("需要选择 GameObject");return;}var view = gameObject.GetComponent<Bind>();if(!view){gameObject.AddComponent<Bind>();}}[MenuItem("GameObject/@(Alt+G)EditorExtension-Add Code Generate Info &g",false,0)]static void AddView(){var gameObject = Selection.objects.First() as GameObject;if (!gameObject){Debug.LogWarning("需要选择 GameObject");return;}var view = gameObject.GetComponent<CodeGenerateInfo>();if (!view){gameObject.AddComponent<CodeGenerateInfo>();}}[MenuItem("GameObject/@(Alt+C)EditorExtension-Create Code &c", false, 0)]static void CreateCode(){var gameObject = Selection.objects.First() as GameObject;if (!gameObject){Debug.LogWarning("需要选择 GameObject");return;}Debug.Log("Create Code");var generateInfo = gameObject.GetComponent<CodeGenerateInfo>();var scriptsFolder = Application.dataPath + "/Scripts";if(generateInfo){scriptsFolder = generateInfo.ScriptsFolder;}if(!Directory.Exists(scriptsFolder)){Directory.CreateDirectory(scriptsFolder);}mBindInfos.Clear();//搜索所有绑定SearchBinds("", gameObject.transform, mBindInfos);ComponentTemplate.Write(gameObject.name, scriptsFolder);ComponentDesignerTemplate.Write(gameObject.name, scriptsFolder, 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.IsNullOrWhiteSpace(path);if (bind && !isRoot){binds.Add(new BindInfo(){FindPath = path,Name = transform.name,ComponentName = bind.ComponentName});}foreach (Transform childTrans in transform){SearchBinds(isRoot ? childTrans.name : path + "/" + childTrans.name, childTrans, binds);}}[DidReloadScripts]static void AddComponent2GameObject(){Debug.Log("DidReloadScripts");var generateClassName = EditorPrefs.GetString("GENERATE_CLASS_NAME");Debug.Log(generateClassName);if (string.IsNullOrWhiteSpace(generateClassName)){Debug.Log("不继续操作");EditorPrefs.DeleteKey("GENERATE_CLASS_NAME");}else{Debug.Log("继续操作");var assemblies = AppDomain.CurrentDomain.GetAssemblies();var defaultAssembly = assemblies.First(assembly => assembly.GetName().Name == "Assembly-CSharp");var typeName = NameSpaceSettingsData.Namespace + "." + generateClassName;var type = defaultAssembly.GetType(typeName);if (type == null){Debug.Log("编译失败");return;}Debug.Log(type);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){var name = bindInfo.Name;Debug.Log(bindInfo.FindPath);Debug.Log(name);Debug.Log(serialiedScript.FindProperty(name));Debug.Log(gameObject.transform.Find(bindInfo.FindPath));serialiedScript.FindProperty(name).objectReferenceValue =gameObject.transform.Find(bindInfo.FindPath).GetComponent(bindInfo.ComponentName);}var codeGenerateInfo = gameObject.GetComponent<CodeGenerateInfo>();if (codeGenerateInfo){serialiedScript.FindProperty("ScriptsFolder").stringValue = codeGenerateInfo.ScriptsFolder;serialiedScript.FindProperty("PrefabFolder").stringValue = codeGenerateInfo.PrefabFolder;serialiedScript.FindProperty("GeneratePrefab").boolValue = codeGenerateInfo.GeneratePrefab;var generatePrefab = codeGenerateInfo.GeneratePrefab;var prefabFolder = codeGenerateInfo.PrefabFolder;var fullPrefabFolder = prefabFolder.Replace("Assets", Application.dataPath);if (codeGenerateInfo.GetType() == type){}else{DestroyImmediate(codeGenerateInfo, false);}serialiedScript.ApplyModifiedPropertiesWithoutUndo();if (generatePrefab){if (!Directory.Exists(fullPrefabFolder)){Directory.CreateDirectory(fullPrefabFolder);}PrefabUtility.SaveAsPrefabAssetAndConnect(gameObject, fullPrefabFolder + "/" + gameObject.name + ".prefab",InteractionMode.AutomatedAction);}}else{serialiedScript.FindProperty("ScriptsFolder").stringValue = "Assets/Scripts";serialiedScript.ApplyModifiedPropertiesWithoutUndo();}EditorPrefs.DeleteKey("GENERATE_CLASS_NAME");}}}
}
大概就是 :会检测生成代码的对象的所有子物体,如果子物体挂载了 bind 脚本,就会生成一个 bindinfo信息存储起来,然后将这个子物体作为要生成的代码的 组件写进生成的代码中,并将子物体的引用挂载上去。也可以通过勾选toggle 来决定是否生成 prefab
制作一个TODOList
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;namespace EGO
{public class MainWindow : EditorWindow{[MenuItem("EGO/MainWindow %t")] static void Open(){var window = GetWindow<MainWindow>();if(!window.mShowing){window.mTodoList = TODOList.Load();var texture = Resources.Load<Texture2D>("kangna");window.titleContent = new GUIContent("EGO Window", texture);window.Show();window.mShowing = true;window.mTodoList.todos.ForEach(todo => todo.Finished.RegisterValueChanged(window.mTodoList.Save));}else{window.mTodoList.todos.ForEach(todo => todo.Finished.UnRegisterValueChanged(window.mTodoList.Save));window.mShowing = false;window.Close();}}private bool mShowing = false;TODOList mTodoList = new TODOList();private string mInputContent = string.Empty;private void OnGUI(){GUILayout.Space(10); //十个像素的空间GUILayout.BeginVertical("box"); //vertical 效果mInputContent = EditorGUILayout.TextArea(mInputContent);if (GUILayout.Button("添加")){if (!string.IsNullOrEmpty(mInputContent)){var newTodo = new Todo() { Content = mInputContent };newTodo.Finished.RegisterValueChanged(mTodoList.Save);mTodoList.todos.Add(newTodo);mInputContent = string.Empty;mTodoList.Save();}}GUILayout.EndVertical();GUILayout.Space(10);GUILayout.BeginVertical("box");for (var i = mTodoList.todos.Count - 1;i >= 0; i--){var todo = mTodoList.todos[i];EditorGUILayout.BeginHorizontal();todo.Finished.value = GUILayout.Toggle(todo.Finished.value, todo.Content);if(GUILayout.Button("删除")){var newTodo = mTodoList.todos[i];newTodo.Finished.UnRegisterValueChanged(mTodoList.Save);mTodoList.todos.RemoveAt(i);mTodoList.Save();}EditorGUILayout.EndHorizontal();}GUILayout.EndVertical();}}
}
学习课程链接:siki学院课程,直接贴链接会被识别成广告
Unity编辑器拓展(一)-MenuItem的使用_unity menuitem-CSDN博客
Unity中的DidReloadScripts事件:深入解析与实践-CSDN博客
Unity中的CustomEditor(自定义编辑器)_unity customeditor-CSDN博客