做编辑器插件时,我总是想要拿到监听编辑器的状态变化。比如在打开编辑器开始运行自己的服务。这时就需要用户打开编辑器的事件。再比如我希望在游戏退出运行模式之前,把一些编辑的东西缓存出来,然后对这些数据做自动化处理,那么我就需要退出运行模式的事件。诸如此类吧。
另一方面,我希望用观察者模式,并且能自动化注册。因为我注意到,导入资源时的AssetImporter回调就是这样做的。用户只需要实现一个接口,就可以收到回调。极大的简化了扩展流程。编辑器代码又不必考虑效率问题,借助C#的反射,可以很容易的实现这种功能。
概述
整套框架的启动核心是属性InitializeOnLoad。当Unity3D运行或启动时,会重新加载有脚本。当使用这个宏时,编辑器会自动将被标注的类实例化到内存中。因此我们可以利用这个特性,在它的构造函数中拉起我们整个服务。 这里有个小技巧。在启动Unity编辑器的情况下,如果在构造函数中创建对象,会被其他清除函数干掉。我认为是脚本初始化顺序,或是场景切换引起的,具体原因得问Unity了。为了解决这个问题,我借助了update函数,跳了一帧执行应有的逻辑。
自动注册是借助C#的反射,通过GetAssemblies和GetTypes获取到所有的类,然后创建出对应的实例。
包装
这个类我觉得有个特别适合的名字——NightWatch。如果你没看过冰与火之歌,可能理解这个框架还算有点难度。总的说来,这个框架讲述了一个少年加入守夜人队伍,并去长城之外战斗的故事... 

实现
接口类如下:
[csharp] view plain copy print?在CODE上查看代码片派生到我的代码片
public interface ICrow  
{  
    /// <summary>  
    /// Join the Nights Watch   
    /// </summary>  
    void Enroll();  
  
    /// <summary>  
    /// Before to Enter Wild  
    /// </summary>  
    void PrepareForBattle();  
  
    /// <summary>  
    /// To the Weirwood outside the wall  
    /// </summary>  
    void FaceWeirwood();  
  
    /// <summary>  
    /// Back To the Castle Black  
    /// </summary>  
    void OpenTheGate();  
  
    /// <summary>  
    /// Tell Vow to the Old God  
    /// </summary>  
    void Vow();  
}  
实例类如下:
[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<ICrow> m_crows = new List<ICrow>();  
    #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<ICrow>(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<ICrow>(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<T>(System.AppDomain aAppDomain) where T : class  
    {  
        var result = new List<T>();  
        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:
泰课在线  
[csharp] view plain copy print
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: 

泰课在线

[csharp] view plain copy print
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或重新编译时输出的。一切按照预期实现,收工。