1. 程式人生 > >ILRuntime熱更案例學習(四) ------ Coroutine/MonoBehaviour/LitJson

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庫的文件即可