简要说明
在热更工程中使用Unity的MonoBehaviour相关调用,虽然是可以实现的,但是官方不推荐这样去使用,如果非要使用,还是基于Unity内部自身脚本MonoBehaviour配合委托或者回调去实现。
即便能做到使用,要完全支持MonoBehaviour的所有特性,会需要很多额外的工作量;不仅可维护性与性能都很差,而且通过MonoBehaviour做游戏逻辑当项目规模大到一定程度之后会是个噩梦,因此应该尽量避免。
具体使用流程
- 和之前文章一样初始化appdomain
//AppDomain是ILRuntime的入口,最好是在一个单例类中保存,整个游戏全局就一个,这里为了示例方便,每个例子里面都单独做了一个
//大家在正式项目中请全局只创建一个AppDomain
AppDomain appdomain;
System.IO.MemoryStream fs;
System.IO.MemoryStream p;
void Start()
{
instance = this;
StartCoroutine(LoadHotFixAssembly());
}
IEnumerator LoadHotFixAssembly()
{
//首先实例化ILRuntime的AppDomain,AppDomain是一个应用程序域,每个AppDomain都是一个独立的沙盒
appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
//正常项目中应该是自行从其他地方下载dll,或者打包在AssetBundle中读取,平时开发以及为了演示方便直接从StreammingAssets中读取,
//正式发布的时候需要大家自行从其他地方读取dll
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//这个DLL文件是直接编译HotFix_Project.sln生成的,已经在项目中设置好输出目录为StreamingAssets,在VS里直接编译即可生成到对应目录,无需手动拷贝
#if UNITY_ANDROID
WWW www = new WWW(Application.streamingAssetsPath + "/HotFix_Project.dll");
#else
WWW www = new WWW("file:///" + Application.streamingAssetsPath + "/HotFix_Project.dll");
#endif
while (!www.isDone)
yield return null;
if (!string.IsNullOrEmpty(www.error))
UnityEngine.Debug.LogError(www.error);
byte[] dll = www.bytes;
www.Dispose();
//PDB文件是调试数据库,如需要在日志中显示报错的行号,则必须提供PDB文件,不过由于会额外耗用内存,正式发布时请将PDB去掉,下面LoadAssembly的时候pdb传null即可
#if UNITY_ANDROID
www = new WWW(Application.streamingAssetsPath + "/HotFix_Project.pdb");
#else
www = new WWW("file:///" + Application.streamingAssetsPath + "/HotFix_Project.pdb");
#endif
while (!www.isDone)
yield return null;
if (!string.IsNullOrEmpty(www.error))
UnityEngine.Debug.LogError(www.error);
byte[] pdb = www.bytes;
fs = new MemoryStream(dll);
p = new MemoryStream(pdb);
try
{
appdomain.LoadAssembly(fs, p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
}
catch
{
Debug.LogError("加载热更DLL失败,请确保已经通过VS打开Assets/Samples/ILRuntime/1.6/Demo/HotFix_Project/HotFix_Project.sln编译过热更DLL");
}
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
//由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
InitializeILRuntime();
Debug.Log("调用热更方法前,我们先销毁掉之前创建的不合法的MonoBehaviour");
SetupCLRRedirection();
OnHotFixLoaded();
}
- Unity类中注册MonoBehaviour,同时绑定一些Unity常用的变量类型(例如Vector3)
unsafe void InitializeILRuntime()
{
//这里做一些ILRuntime的注册
appdomain.RegisterCrossBindingAdaptor(new MonoBehaviourAdapter());
appdomain.RegisterValueTypeBinder(typeof(Vector3), new Vector3Binder());
//ILRuntime.Runtime.Generated.CLRBindings.Initialize(appdomain);
}
- Unity类中销毁之前注册的不合法MonoBehaviour
unsafe void SetupCLRRedirection()
{
var arr = typeof(GameObject).GetMethods();
foreach (var i in arr)
{
if (i.Name == "AddComponent" && i.GetGenericArguments().Length == 1)
{
appdomain.RegisterCLRMethodRedirection(i, AddComponent);
}
}
}
- Unity类中调用热更中实现MonoBehaviour调用的方法方法和类方法
unsafe void OnHotFixLoaded()
{
Debug.Log("热更没法自己调用MonoBehaviour方法,需要挟持重定向自己在Unity中自己方法去实现");
Debug.Log("调用热更代码中的AddComponent");
appdomain.Invoke("HotFix_Project.TestMonoBehaviour", "RunTest", null, gameObject);
Debug.Log("调用热更代码中的GetComponent");
SetupCLRRedirection2();
appdomain.Invoke("HotFix_Project.TestMonoBehaviour", "RunTest2", null, gameObject);
Debug.Log("从Unity主工程获取热更DLL的MonoBehaviour");
var type = appdomain.LoadedTypes["HotFix_Project.SomeMonoBehaviour2"] as ILType;
var smb = GetComponent(type);
var m = type.GetMethod("Test2");
appdomain.Invoke(m, smb, null);
}
- Unity工程中实现对AddComponent和GetComponent的CLR重定向,热更中调用的MonoBehaviour方法就是重定向后的方法
unsafe static StackObject* AddComponent(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
//CLR重定向的说明请看相关文档和教程,这里不多做解释
ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
var ptr = __esp - 1;
//成员方法的第一个参数为this
GameObject instance = StackObject.ToObject(ptr, __domain, __mStack) as GameObject;
if (instance == null)
throw new System.NullReferenceException();
__intp.Free(ptr);
var genericArgument = __method.GenericArguments;
//AddComponent应该有且只有1个泛型参数
if (genericArgument != null && genericArgument.Length == 1)
{
var type = genericArgument[0];
object res;
if(type is CLRType)
{
//Unity主工程的类不需要任何特殊处理,直接调用Unity接口
res = instance.AddComponent(type.TypeForCLR);
}
else
{
//热更DLL内的类型比较麻烦。首先我们得自己手动创建实例
var ilInstance = new ILTypeInstance(type as ILType, false);//手动创建实例是因为默认方式会new MonoBehaviour,这在Unity里不允许
//接下来创建Adapter实例
var clrInstance = instance.AddComponent<MonoBehaviourAdapter.Adaptor>();
//unity创建的实例并没有热更DLL里面的实例,所以需要手动赋值
clrInstance.ILInstance = ilInstance;
clrInstance.AppDomain = __domain;
//这个实例默认创建的CLRInstance不是通过AddComponent出来的有效实例,所以得手动替换
ilInstance.CLRInstance = clrInstance;
res = clrInstance.ILInstance;//交给ILRuntime的实例应该为ILInstance
clrInstance.Awake();//因为Unity调用这个方法时还没准备好所以这里补调一次
}
return ILIntepreter.PushObject(ptr, __mStack, res);
}
return __esp;
}
unsafe static StackObject* GetComponent(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
//CLR重定向的说明请看相关文档和教程,这里不多做解释
ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
var ptr = __esp - 1;
//成员方法的第一个参数为this
GameObject instance = StackObject.ToObject(ptr, __domain, __mStack) as GameObject;
if (instance == null)
throw new System.NullReferenceException();
__intp.Free(ptr);
var genericArgument = __method.GenericArguments;
//AddComponent应该有且只有1个泛型参数
if (genericArgument != null && genericArgument.Length == 1)
{
var type = genericArgument[0];
object res = null;
if (type is CLRType)
{
//Unity主工程的类不需要任何特殊处理,直接调用Unity接口
res = instance.GetComponent(type.TypeForCLR);
}
else
{
//因为所有DLL里面的MonoBehaviour实际都是这个Component,所以我们只能全取出来遍历查找
var clrInstances = instance.GetComponents<MonoBehaviourAdapter.Adaptor>();
for(int i = 0; i < clrInstances.Length; i++)
{
var clrInstance = clrInstances[i];
if (clrInstance.ILInstance != null)//ILInstance为null, 表示是无效的MonoBehaviour,要略过
{
if (clrInstance.ILInstance.Type == type)
{
res = clrInstance.ILInstance;//交给ILRuntime的实例应该为ILInstance
break;
}
}
}
}
return ILIntepreter.PushObject(ptr, __mStack, res);
}
return __esp;
}
- 热更工程中实现MonoBehaviour的AddComponent和GetComponent类方法
public class TestMonoBehaviour
{
public static void RunTest(GameObject go)
{
go.AddComponent<SomeMonoBehaviour>();
}
public static void RunTest2(GameObject go)
{
go.AddComponent<SomeMonoBehaviour2>();
var mb = go.GetComponent<SomeMonoBehaviour2>();
Debug.Log("!!!TestMonoBehaviour.RunTest2 mb= " + mb);
mb.Test2();
}
}
- 热更工程中继承MonoBehaviour生命周期类和使用了MonoBehaviour特定变量类
class SomeMonoBehaviour : MonoBehaviour
{
float time;
void Awake()
{
Debug.Log("!! SomeMonoBehaviour.Awake");
}
void Start()
{
Debug.Log("!! SomeMonoBehaviour.Start");
}
void Update()
{
if(Time.time - time > 1)
{
Debug.Log("!! SomeMonoBehaviour.Update, t=" + Time.time);
time = Time.time;
}
}
public void Test()
{
Debug.Log("SomeMonoBehaviour");
}
}
class SomeMonoBehaviour2 : MonoBehaviour
{
public GameObject TargetGO;
public Texture2D Texture;
public void Test2()
{
Debug.Log("!!! SomeMonoBehaviour2.Test2");
}
}
本章比较难,但是一般情况较少使用,大家理解就行。