跳转至专题目录
专题推荐文章:
- 温故知新——RectTransform成员属性的再认识
- unity Scene View扩展之编辑器扩展总结
- Unity获取鼠标点击ui GameObject
本系列目录
- unity编辑器扩展之SceneUI——贴在Scene View的SceneCanvas
- unity Scene View扩展之屏蔽对Scene的所有操作
- unity Scene View扩展之显示常驻GUI
- unity Scene View扩展之显示网格
- unity Scene View扩展之加载Assets文件夹外的资源
- 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;
}
}