Unity3d开发(十八) 监听编辑器状态改变,制定自定义回调
做编辑器插件时,我总是想要拿到监听编辑器的状态变化。比如在打开编辑器开始运行自己的服务。这时就需要用户打开编辑器的事件。再比如我希望在游戏退出运行模式之前,把一些编辑的东西缓存出来,然后对这些数据做自动化处理,那么我就需要退出运行模式的事件。
另一方面,我希望用观察者模式,并且能自动化注册。因为我注意到,导入资源时的AssetImporter
回调就是这样做的。用户只需要实现一个接口,就可以收到回调。极大的简化了扩展流程。编辑器代码又不必考虑效率问题,借助C#的反射,可以很容易的实现这种功能。
概述
整套框架的启动核心是属性InitializeOnLoad
。当Unity3D运行或启动时,会重新加载有脚本。当使用这个宏时,编辑器会自动将被标注的类实例化到内存中。因此我们可以利用这个特性,在它的构造函数中拉起我们整个服务。 这里有个小技巧。在启动Unity编辑器的情况下,如果在构造函数中创建对象,会被其他清除函数干掉。我认为是脚本初始化顺序,或是场景切换引起的,具体原因得问Unity了。为了解决这个问题,我借助了update
函数,跳了一帧执行应有的逻辑。
自动注册是借助C#的反射,通过GetAssemblies
和GetTypes
获取到所有的类,然后创建出对应的实例。
包装
这个类我觉得有个特别适合的名字——NightWatch
。如果你没看过冰与火之歌,可能理解这个框架还算有点难度。总的说来,这个框架讲述了一个少年加入守夜人队伍,并去长城之外战斗的故事...
实现
接口类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | [csharp] view plain copy print?在CODE上查看代码片派生到我的代码片 public interface ICrow { /// /// Join the Nights Watch /// void Enroll(); /// /// Before to Enter Wild /// void PrepareForBattle(); /// /// To the Weirwood outside the wall /// void FaceWeirwood(); /// /// Back To the Castle Black /// void OpenTheGate(); /// /// Tell Vow to the Old God /// void Vow(); } |
实例类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | [csharp] view plain copy print?在CODE上查看代码片派生到我的代码片 using UnityEngine; using System.Collections; using System.Collections.Generic; using UnityEditor; [InitializeOnLoad] public class NightsWatch { #region Public Attributes #endregion #region Private Attributes private static List m_crows = new List(); #endregion #region Public Methods static NightsWatch() { if (!EditorApplication.isPlayingOrWillChangePlaymode) { EditorApplication.update += WelcomeToCastleBlack; } else { EditorApplication.update += BeyondTheWall; } } static void WelcomeToCastleBlack() { EditorApplication.update -= WelcomeToCastleBlack; //Debug.Log("Welcome To castle black"); m_crows.Clear(); var crows = GetAllImplementTypes(System.AppDomain.CurrentDomain); foreach (var eachCrow in crows) { eachCrow.Enroll(); m_crows.Add(eachCrow); } EditorApplication.update += WaitForWild; } static void WaitForWild() { if (EditorApplication.isPlayingOrWillChangePlaymode) { foreach (var eachCrow in m_crows) { eachCrow.PrepareForBattle(); } EditorApplication.update -= WaitForWild; } } static void BeyondTheWall() { EditorApplication.update -= BeyondTheWall; //Debug.Log("Welcome To The Wild"); m_crows.Clear(); var crows = GetAllImplementTypes(System.AppDomain.CurrentDomain); foreach (var eachCrow in crows) { eachCrow.FaceWeirwood(); m_crows.Add(eachCrow); } EditorApplication.update += WaitForCrowReturn; } static void WaitForCrowReturn() { if (!EditorApplication.isPlayingOrWillChangePlaymode ) { //Debug.Log("Open the Door"); EditorApplication.update -= WaitForCrowReturn; foreach (var eachCrow in m_crows) { eachCrow.OpenTheGate(); } EditorApplication.update += WelcomeToCastleBlack; } } public static void CrowsVow() { foreach (var eachCrow in m_crows) { eachCrow.Vow(); } } [MenuItem( "Land/CastleBlack" )] public static void MakeVow() { NightsWatch.CrowsVow(); } #endregion #region Override Methods #endregion #region Private Methods public static T[] GetAllImplementTypes(System.AppDomain aAppDomain) where T : class { var result = new List(); var assemblies = aAppDomain.GetAssemblies(); foreach (var assembly in assemblies) { var types = assembly.GetTypes(); foreach (var type in types) { if ( typeof (T).IsAssignableFrom(type)) { if (!type.IsAbstract) { var tar = assembly.CreateInstance(type.FullName) as T; result.Add(tar); } } } } return result.ToArray(); } #endregion } |
简单解释一下,所有的接口都是按照冰与火之歌中的剧情定义。当在编辑状态下时,会创建对应的实例类,并调用Enroll
函数,这相当于Jon刚刚进入CastleBlack。当点击Play运行时,会先调用PrepareForBattle
,相当于在城堡中准备出征。当游戏开始运行时,会调用FaceToWeirWood
,这里对应的是城外那颗鱼梁木,一般出征之前都是要去祈祷一下。然后当游戏运行结束时,会调用OpenTheGate
,对应出征回来,在长城下面喊门。然后有个Vow
接口,这个是用来点名的,城堡里的乌鸦都要列队答“道”。
使用
新建两个实例: 一个是JonSnow:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | [csharp] view plain copy print?在CODE上查看代码片派生到我的代码片 public class JonSnow : ICrow { public void Enroll() { Debug.Log( this + " join the NightWatch!" ); } public void PrepareForBattle() { Debug.Log( this + " follow your lead!" ); } public void FaceWeirwood() { Debug.Log( "I'm the wolf in the north" ); } public void OpenTheGate() { Debug.Log( this + " request enter Castle Black" ); } public void Vow() { Debug.Log( this + " For The Watch" ); } } |
一个是Samwell:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | [csharp] view plain copy print?在CODE上查看代码片派生到我的代码片 public class Samwell : ICrow { public void Enroll() { Debug.Log( this + " I came form Lord Randyll Tarly,and I even his oldest son ..." ); } public void PrepareForBattle() { Debug.Log( this + " is not ready yet..." ); } public void FaceWeirwood() { Debug.Log( "I'm a useless warrior,but may be ... helpful" ); } public void OpenTheGate() { Debug.Log( this + " also want enter" ); } public void Vow() { Debug.Log( this + " For The ... alive" ); } } |
测试
当写好代码编译完成时,就能在输出中看到他俩到长城去报道了。点击运行程序,关闭运行程序,会分别有日志输出,效果如下:
其中红线是点击Play操作,绿线是停止Unity运行的操作,红线以上的日志是打开unity或重新编译时输出的。一切按照预期实现,收工。