Unity3d开发监听编辑器状态改变 制定自定义回调

2016年11月29日 10:22 0 点赞 0 评论 更新于 2025-11-21 15:19
Unity3d开发监听编辑器状态改变 制定自定义回调

在进行Unity3D编辑器插件开发时,我们常常需要监听编辑器的状态变化。例如,在打开编辑器时启动自定义服务,这就需要捕获编辑器打开事件;又或者在游戏退出运行模式前,缓存一些编辑内容并进行自动化处理,这就需要监听退出运行模式的事件。

另一方面,我们希望采用观察者模式实现自动化注册。以资源导入时的AssetImporter回调为例,用户只需实现一个接口,就能收到相应回调,大大简化了扩展流程。由于编辑器代码对效率的要求相对较低,借助C#的反射机制,可以轻松实现这一功能。

整体框架概述

整套框架的启动核心是InitializeOnLoad属性。当Unity3D运行或启动时,会重新加载脚本。使用该属性标注的类,编辑器会自动将其实例化到内存中。因此,我们可以利用这一特性,在类的构造函数中启动整个服务。

不过,这里有个小问题。在启动Unity编辑器时,如果在构造函数中创建对象,可能会被其他清除函数清除。推测这可能是由于脚本初始化顺序或场景切换导致的,具体原因有待向Unity官方进一步确认。为了解决这个问题,我们借助update函数,延迟一帧执行相应逻辑。

自动注册功能借助C#的反射机制实现。通过GetAssembliesGetTypes方法获取所有类,然后创建对应的实例。

框架包装

我们将这个类命名为NightsWatch。如果您不熟悉《冰与火之歌》,理解这个框架可能会有一定难度。在原著中,“守夜人”是一个守护长城的组织,这个框架的命名灵感就来源于此。

代码实现

接口类

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();
}

实例类

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刚刚加入“守夜人”,抵达黑城堡(Castle Black)。
  • 点击Play按钮开始运行游戏时,会先调用PrepareForBattle函数,就像在城堡中为出征做准备。
  • 游戏开始运行时,会调用FaceWeirwood函数,对应着在城外的鱼梁木前祈祷。
  • 游戏运行结束时,会调用OpenTheGate函数,相当于出征归来,在长城下呼喊开门。
  • Vow接口用于点名,城堡里的“乌鸦”(实现了ICrow接口的类实例)都要列队应答。

使用示例

创建实例类

JonSnow类

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类

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");
}
}

测试

代码编写并编译完成后,在输出窗口中可以看到JonSnowSamwell加入“守夜人”的日志信息。点击运行程序和停止运行程序时,会分别输出相应的日志。其中,点击Play操作对应红线标记,停止Unity运行操作对应绿线标记,红线以上的日志是在打开Unity或重新编译时输出的。测试结果表明,整个框架按照预期正常运行。