跳转至专题目录

专题推荐文章:

  1. 温故知新——RectTransform成员属性的再认识
  2. unity Scene View扩展之编辑器扩展总结
  3. Unity获取鼠标点击ui GameObject

本系列目录

  1. unity编辑器扩展之SceneUI——贴在Scene View的SceneCanvas
  2. unity Scene View扩展之屏蔽对Scene的所有操作
  3. unity Scene View扩展之显示常驻GUI
  4. unity Scene View扩展之显示网格
  5. unity Scene View扩展之加载Assets文件夹外的资源
  6. unity Scene View扩展之编辑器扩展总结

最近一段时间都在搞编辑器,扩展各种功能,如添加Inspector上的Add Component回调功能,Unity Package 一键更新功能, 还有现在的Scene扩展,也算小有心得了,出来总结一下。

一、不运行时也可以运行的类

比较特殊的类,比如说Editor,EditorWindow,ScriptableObject,AssetPostprocessor等特殊一点的就不说了,网上一抓一大把介绍的,主要讲讲两个重要特性ExecuteInEditMode和InitializeOnLoad。

1、ExecuteInEditMode



// ExecuteInEditMode主要是让MonoBehaviour可以在不运行时,也可以执行各个生命周期的特性
// 可以配合Mono单例使用
// 需要把组件拖到GameObject上才能执行
[ExecuteInEditMode]
public class PackageManager : MonoBehaviour
{
    // 其中值得介绍的是OnEnable和OnDisable
    // 因为改了代码之后会将数据清零,而且不会执行Awake和Start
    // 但是会在编译前执行OnDisable,编译后会执行OnEnable
    // 可以用这一个时机,对一些委托的绑定与解绑,或者数据的初始化和销毁
    // 当然正常的SetActive也会触发这两个时机
    void OnEnable()
    {
    }

    void OnDisable()
    {
    }
}



2、InitializeOnLoad



// InitializeOnLoad主要是让一个普通类可以在编辑器下初始化
// 个人认为虽然可以用Mono调用单例也可以达到相同效果,不过有些时候并不需要创建一个GameObject
// 所以直接定义一个这么的类就好了
[InitializeOnLoad]
public static class EditorTiming
{
	// 构造函数可以在编辑器打开,或者编译后执行,放心地把Init或者Reset放在这里
	static EditorTiming()
	{
	}
}



3、单例模式

单例模式没什么好说的,无论时普通的Singleton<T>,还是MonoSingleton<T>,都可以在网上找到,这里只说一下在Unity源码里翻出来的ScriptableObjectSingleton<T>



// 代码有点长了,放文末了,有兴趣的可以去看看



二、几个重要的时间节点

这里的话,就主要讲讲我用的最多的几个委托接口:

1、编辑器Update委托——EditorApplication.update

既然InitializeOnLoad只是初始话普通类,并不能update之类的,所以需要绑定一个update委托给它,让他能自行Update,用的就是这个EditorApplication.update

2、在SceneView上面写写画画——SceneView.duringSceneGui(这个为2019版的,2019前的为SceneView.onSceneGUIDelegate)

主要是处理SceneView的一些Event、操作、只在SceneView里画出各种GUI等等,详见可以看本系列的其他文章

3、代码编译相关

在编辑器编辑状态,肯定需要知道代码是不是在编译,或者编译好之后的回调之类的,这里有几个方法

(1)DidReloadScripts特性



// 注意这个特性需要的函数时静态函数
	// 就算这个类没被使用,甚至没被实例化,都会被执行到
	// 所以用的时候需要注意判断执行条件
	[DidReloadScripts]
	private static void Reload()
	{
	}



(2)AssemblyReloadEvents类

这个主要两个回调:AssemblyReloadEvents.beforeAssemblyReload, AssemblyReloadEvents.afterAssemblyReload。一个在编译前执行,一个在编译后执行。

这三个的执行顺序为:AssemblyReloadEvents.beforeAssemblyReload —》 AssemblyReloadEvents.afterAssemblyReload -》 DidReloadScripts

4、资源导入相关——EditorApplication.projectChanged

既然要知道代码编译了,那么也不能缺资源被修改的回调了。

在人为操作的删除、添加、修改、编译之后,都会执行这个回调,不过坑的是,这里并没有告诉我们什么资源被修改了。

如果在代码中对资源修改之后,需要执行AssetDataBase.Refresh()

5、撤销操作——UnDo

这个暂无太多了解,只是看了这么一个例子,了解到了一些撤回的操作

https://answers.unity.com/questions/975578/undoredo-on-meshes-this-code-works-but-how.html

6、大部分都集中在EditorApplication

7、个人一些建议

个人感觉可以都把这些回调绑定在一个类里面,这样有两个好处

(1)不用每个类都关心绑定、解绑的时机

(2)可以控制每个类的执行顺序

比如说可以这样



[InitializeOnLoad]
public static class EditorTiming
{
	static EditorTiming()
	{
		EditorApplication.update += Update;
	}

	// 所有类都在这个函数里执行,顺序明显,且a、b不需要管绑定与解绑委托的事情
	void Update()
	{
		a.Update();
		b.Update();
	}
}



三、善用反射

毕竟很多时候,Unity提供的接口并不能满足我们的开发需求,虽然有时我们可以将需要用到的代码照抄过来,但若是需要处理的数据牵扯到许多地方,这个时候,用到反射是一个很不错的选择。

这个时候就推销以下我的反射框架了,详细介绍见Unity Package 一键更新功能开发之C#反射框架篇。

然后自己也建了一个对标unity开源代码UnityCsReference的UnityCsReflection,欢迎Star→_→

最后在加一些在看SceneView摄像机移动的几个函数笔记

SceneView.DefaultHandles();
SceneView.s_CurrentTool
Tools.viewoolActive
Tools.s_LockedViewTool
SceneViewMotion.HandleMouseDown

*ScriptableSingleton<T>



[AttributeUsage(AttributeTargets.Class)]
    internal class FilePathAttribute : Attribute
    {
        public enum Location { PreferencesFolder, ProjectFolder }

        private string filePath;
        private string relativePath;
        private Location location;

        public string filepath
        {
            get
            {
                if (filePath == null && relativePath != null)
                {
                    filePath = GetFilePath(relativePath, location);
                    relativePath = null;
                }

                return filePath;
            }

            set
            {
                filePath = value;
            }
        }

        public FilePathAttribute(string relativePath, Location location)
        {
            if (string.IsNullOrEmpty(relativePath))
            {
                Debug.LogError("Invalid relative path! (its null or empty)");
                return;
            }

            this.relativePath = relativePath;
            this.location = location;
        }

        static string GetFilePath(string relativePath, Location location)
        {
            // We do not want a slash as first char
            if (relativePath[0] == '/')
                relativePath = relativePath.Substring(1);

            if (location == Location.PreferencesFolder)
                return InternalEditorUtility.unityPreferencesFolder + "/" + relativePath;
            else //location == Location.ProjectFolder
                return relativePath;
        }
    }

    public class ScriptableSingleton<T> : ScriptableObject where T : ScriptableObject
    {
        static T s_Instance;

        public static T instance
        {
            get
            {
                if (s_Instance == null)
                    CreateAndLoad();

                return s_Instance;
            }
        }

        // On domain reload ScriptableObject objects gets reconstructed from a backup. We therefore set the s_Instance here
        protected ScriptableSingleton()
        {
            if (s_Instance != null)
            {
                Debug.LogError("ScriptableSingleton already exists. Did you query the singleton in a constructor?");
            }
            else
            {
                object casted = this;
                s_Instance = casted as T;
                System.Diagnostics.Debug.Assert(s_Instance != null);
            }
        }

        private static void CreateAndLoad()
        {
            System.Diagnostics.Debug.Assert(s_Instance == null);

            // Load
            string filePath = GetFilePath();
            if (!string.IsNullOrEmpty(filePath))
            {
                // If a file exists the
                InternalEditorUtility.LoadSerializedFileAndForget(filePath);
            }

            if (s_Instance == null)
            {
                // Create
                T t = CreateInstance<T>();
                t.hideFlags = HideFlags.HideAndDontSave;
            }

            System.Diagnostics.Debug.Assert(s_Instance != null);
        }

        protected virtual void Save(bool saveAsText)
        {
            if (s_Instance == null)
            {
                Debug.Log("Cannot save ScriptableSingleton: no instance!");
                return;
            }

            string filePath = GetFilePath();
            if (!string.IsNullOrEmpty(filePath))
            {
                string folderPath = Path.GetDirectoryName(filePath);
                if (!Directory.Exists(folderPath))
                    Directory.CreateDirectory(folderPath);

                InternalEditorUtility.SaveToSerializedFileAndForget(new[] { s_Instance }, filePath, saveAsText);
            }
        }

        private static string GetFilePath()
        {
            Type type = typeof(T);
            object[] atributes = type.GetCustomAttributes(true);
            foreach (object attr in atributes)
            {
                if (attr is FilePathAttribute)
                {
                    FilePathAttribute f = attr as FilePathAttribute;
                    return f.filepath;
                }
            }
            return null;
        }
    }