ILRuntime熱更案例學習(四) ------ Coroutine/MonoBehaviour/LitJson
官方示例下載地址1: https://github.com/Ourpalm/ILRuntime
官方示例下載地址2 : https://github.com/Ourpalm/ILRuntimeU3D
官方文件地址 : https://ourpalm.github.io/ILRuntime/public/v1/guide/tutorial.html
一.Coroutine案例
此案例向我們展示了在unity主工程中如何呼叫熱更dll中的協程,類似於ILRuntime熱更案例學習(二) ------ Invocation/Delegate/Inheritance/錯誤提醒文中Inheritance的使用
1.建立協程的繼承介面卡
public class CoroutineAdapter : CrossBindingAdaptor { public override Type BaseCLRType { get { return null; } } public override Type[] BaseCLRTypes { get { //跨域繼承只能有1個Adapter,因此應該儘量避免一個類同時實現多個外部介面,對於coroutine來說是IEnumerator<object>,IEnumerator和IDisposable, //ILRuntime雖然支援,但是一定要小心這種用法,使用不當很容易造成不可預期的問題 //日常開發如果需要實現多個DLL外部介面,請在Unity這邊先做一個基類實現那些個介面,然後繼承那個基類 return new Type[] { typeof(IEnumerator<object>), typeof(IEnumerator), typeof(IDisposable) }; } } public override Type AdaptorType { get { return typeof(Adaptor); } } public override object CreateCLRInstance(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance) { return new Adaptor(appdomain, instance); } //Coroutine生成的類實現了IEnumerator<System.Object>, IEnumerator, IDisposable,所以都要實現,這個可以通過reflector之類的IL反編譯軟體得知 internal class Adaptor : IEnumerator<System.Object>, IEnumerator, IDisposable, CrossBindingAdaptorType { ILTypeInstance instance; ILRuntime.Runtime.Enviorment.AppDomain appdomain; public Adaptor() { } public Adaptor(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance) { this.appdomain = appdomain; this.instance = instance; } public ILTypeInstance ILInstance { get { return instance; } } IMethod mCurrentMethod; bool mCurrentMethodGot; public object Current { get { if (!mCurrentMethodGot) { mCurrentMethod = instance.Type.GetMethod("get_Current", 0); if (mCurrentMethod == null) { //這裡寫System.Collections.IEnumerator.get_Current而不是直接get_Current是因為coroutine生成的類是顯式實現這個介面的,通過Reflector等反編譯軟體可得知 //為了相容其他只實現了單一Current屬性的,所以上面先直接取了get_Current mCurrentMethod = instance.Type.GetMethod("System.Collections.IEnumerator.get_Current", 0); } mCurrentMethodGot = true; } if (mCurrentMethod != null) { var res = appdomain.Invoke(mCurrentMethod, instance, null); return res; } else { return null; } } } IMethod mDisposeMethod; bool mDisposeMethodGot; public void Dispose() { if (!mDisposeMethodGot) { mDisposeMethod = instance.Type.GetMethod("Dispose", 0); if (mDisposeMethod == null) { mDisposeMethod = instance.Type.GetMethod("System.IDisposable.Dispose", 0); } mDisposeMethodGot = true; } if (mDisposeMethod != null) { appdomain.Invoke(mDisposeMethod, instance, null); } } IMethod mMoveNextMethod; bool mMoveNextMethodGot; public bool MoveNext() { if (!mMoveNextMethodGot) { mMoveNextMethod = instance.Type.GetMethod("MoveNext", 0); mMoveNextMethodGot = true; } if (mMoveNextMethod != null) { return (bool)appdomain.Invoke(mMoveNextMethod, instance, null); } else { return false; } } IMethod mResetMethod; bool mResetMethodGot; public void Reset() { if (!mResetMethodGot) { mResetMethod = instance.Type.GetMethod("Reset", 0); mResetMethodGot = true; } if (mResetMethod != null) { appdomain.Invoke(mResetMethod, instance, null); } } public override string ToString() { IMethod m = appdomain.ObjectType.GetMethod("ToString", 0); m = instance.Type.GetVirtualMethod(m); if (m == null || m is ILMethod) { return instance.ToString(); } else return instance.Type.FullName; } } }
2.註冊介面卡
appdomain.RegisterCrossBindingAdaptor(new CoroutineAdapter());
3.呼叫
unsafe void OnHotFixLoaded() { appdomain.Invoke("HotFix_Project.TestCoroutine", "RunTest", null, null); } public void DoCoroutine(IEnumerator coroutine) { StartCoroutine(coroutine); } 熱更dll : public static void RunTest() { CoroutineDemo.Instance.DoCoroutine(Coroutine()); } static System.Collections.IEnumerator Coroutine() { Debug.Log("開始協程,t=" + Time.time); yield return new WaitForSeconds(3); Debug.Log("等待了3秒,t=" + Time.time); }
二.MonoBehaviour
MonoBehaviour官方並不推薦我們跨域使用,因為即便能做到使用,要完全支援MonoBehaviour的所有特性,會需要很多額外的工作量,而且通過MonoBehaviour做遊戲邏輯當專案規模大到一定程度之後會是個噩夢,因此應該儘量避免,官方提供的案例裡面只實現了AddComponent及GetComponent
1.建立MonoBehaviour介面卡,實現基本流程以及tostring()方法
public class MonoBehaviourAdapter : CrossBindingAdaptor
{
public override Type BaseCLRType
{
get
{
return typeof(MonoBehaviour);
}
}
public override Type AdaptorType
{
get
{
return typeof(Adaptor);
}
}
public override object CreateCLRInstance(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance)
{
return new Adaptor(appdomain, instance);
}
//為了完整實現MonoBehaviour的所有特性,這個Adapter還得擴充套件,這裡只拋磚引玉,只實現了最常用的Awake, Start和Update
public class Adaptor : MonoBehaviour, CrossBindingAdaptorType
{
ILTypeInstance instance;
ILRuntime.Runtime.Enviorment.AppDomain appdomain;
public Adaptor()
{
}
public Adaptor(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance)
{
this.appdomain = appdomain;
this.instance = instance;
}
public ILTypeInstance ILInstance { get { return instance; } set { instance = value; } }
public ILRuntime.Runtime.Enviorment.AppDomain AppDomain { get { return appdomain; } set { appdomain = value; } }
IMethod mAwakeMethod;
bool mAwakeMethodGot;
public void Awake()
{
//Unity會在ILRuntime準備好這個例項前呼叫Awake,所以這裡暫時先不掉用
if (instance != null)
{
if (!mAwakeMethodGot)
{
mAwakeMethod = instance.Type.GetMethod("Awake", 0);
mAwakeMethodGot = true;
}
if (mAwakeMethod != null)
{
appdomain.Invoke(mAwakeMethod, instance, null);
}
}
}
IMethod mStartMethod;
bool mStartMethodGot;
void Start()
{
if (!mStartMethodGot)
{
mStartMethod = instance.Type.GetMethod("Start", 0);
mStartMethodGot = true;
}
if (mStartMethod != null)
{
appdomain.Invoke(mStartMethod, instance, null);
}
}
IMethod mUpdateMethod;
bool mUpdateMethodGot;
void Update()
{
if (!mUpdateMethodGot)
{
mUpdateMethod = instance.Type.GetMethod("Update", 0);
mUpdateMethodGot = true;
}
if (mStartMethod != null)
{
appdomain.Invoke(mUpdateMethod, instance, null);
}
}
public override string ToString()
{
IMethod m = appdomain.ObjectType.GetMethod("ToString", 0);
m = instance.Type.GetVirtualMethod(m);
if (m == null || m is ILMethod)
{
return instance.ToString();
}
else
return instance.Type.FullName;
}
}
}
2.註冊並重定向方法
unsafe void SetupCLRRedirection()
{
//這裡面的通常應該寫在InitializeILRuntime,這裡為了演示寫這裡
var arr = typeof(GameObject).GetMethods();
foreach (var i in arr)
{
if (i.Name == "AddComponent" && i.GetGenericArguments().Length == 1)
{
appdomain.RegisterCLRMethodRedirection(i, AddComponent);
}
}
}
unsafe void SetupCLRRedirection2()
{
//這裡面的通常應該寫在InitializeILRuntime,這裡為了演示寫這裡
var arr = typeof(GameObject).GetMethods();
foreach (var i in arr)
{
if (i.Name == "GetComponent" && i.GetGenericArguments().Length == 1)
{
appdomain.RegisterCLRMethodRedirection(i, GetComponent);
}
}
}
MonoBehaviourAdapter.Adaptor GetComponent(ILType type)
{
var arr = GetComponents<MonoBehaviourAdapter.Adaptor>();
for(int i = 0; i < arr.Length; i++)
{
var instance = arr[i];
if(instance.ILInstance != null && instance.ILInstance.Type == type)
{
return instance;
}
}
return null;
}
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;
}
3.為了顯示dll指令碼中的公共屬性實現方法
[CustomEditor(typeof(MonoBehaviourAdapter.Adaptor), true)]
public class MonoBehaviourAdapterEditor : UnityEditor.UI.GraphicEditor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
MonoBehaviourAdapter.Adaptor clr = target as MonoBehaviourAdapter.Adaptor;
var instance = clr.ILInstance;
if (instance != null)
{
EditorGUILayout.LabelField("Script", clr.ILInstance.Type.FullName);
foreach (var i in instance.Type.FieldMapping)
{
//這裡是取的所有欄位,沒有處理不是public的
var name = i.Key;
var type = instance.Type.FieldTypes[i.Value];
var cType = type.TypeForCLR;
if (cType.IsPrimitive)//如果是基礎型別
{
if (cType == typeof(float))
{
instance[i.Value] = EditorGUILayout.FloatField(name, (float)instance[i.Value]);
}
else
throw new System.NotImplementedException();//剩下的大家自己補吧
}
else
{
object obj = instance[i.Value];
if (typeof(UnityEngine.Object).IsAssignableFrom(cType))
{
//處理Unity型別
var res = EditorGUILayout.ObjectField(name, obj as UnityEngine.Object, cType, true);
instance[i.Value] = res;
}
else
{
//其他型別現在沒法處理
if (obj != null)
EditorGUILayout.LabelField(name, obj.ToString());
else
EditorGUILayout.LabelField(name, "(null)");
}
}
}
}
}
}
4.呼叫
unsafe void OnHotFixLoaded()
{
Debug.Log("直接呼叫GameObject.AddComponent<T>會報錯,這是因為這個方法是Unity實現的,他並不可能取到熱更DLL內部的型別");
Debug.Log("因此我們需要挾持AddComponent方法,然後自己實現");
Debug.Log("我們先銷燬掉之前建立的不合法的MonoBehaviour");
SetupCLRRedirection();
appdomain.Invoke("HotFix_Project.TestMonoBehaviour", "RunTest", null, gameObject);
SetupCLRRedirection2();
appdomain.Invoke("HotFix_Project.TestMonoBehaviour", "RunTest2", null, gameObject);
Debug.Log("成功了");
Debug.Log("那我們怎麼從Unity主工程獲取熱更DLL的MonoBehaviour呢?");
Debug.Log("這需要我們自己實現一個GetComponent方法");
var type = appdomain.LoadedTypes["HotFix_Project.SomeMonoBehaviour2"] as ILType;
var smb = GetComponent(type);
var m = type.GetMethod("Test2");
Debug.Log("現在來試試呼叫");
appdomain.Invoke(m, smb, null);
Debug.Log("呼叫成功!");
Debug.Log("我們點一下左邊列表裡的GameObject,檢視一下我們剛剛掛的指令碼");
Debug.Log("預設情況下是無法顯示DLL裡面定義的public變數的值的");
Debug.Log("這個Demo我們寫了一個自定義Inspector來檢視變數,同樣只是拋磚引玉");
Debug.Log("要完整實現MonoBehaviour所有功能得大家自己花功夫了,最好還是避免腳本里使用MonoBehaviour");
Debug.Log("具體實現請看MonoBehaviourAdapterEditor");
Debug.Log("特別注意,現在僅僅是執行時可以看到和編輯,由於沒有處理序列化的問題,所以並不可能儲存到Prefab當中,要想實現就得靠大家自己了");
}
可以看到,光兩個方法就這麼多程式碼,太繁瑣了,能不用就不用吧
三.LitJson整合
如果想在熱更dll中使用litjson,需要將案例demo中的LitJson資料夾拷貝至你的專案中
1.對LitJson進行註冊(在註冊CLR繫結之前,執行下面這行程式碼)
LitJson.JsonMapper.RegisterILRuntimeCLRRedirection(appdomain);
2.使用
LitJson的使用非常簡單,將一個物件轉換成json字串,只需要下面這行程式碼即可
string json = JsonMapper.ToJson(obj);
將json字串反序列化成物件也同樣只需要一行程式碼
JsonTestClass obj = JsonMapper.ToObject<JsonTestClass>(json);
其他具體使用方法請參考LitJson庫的文件即可