Addressables+ILRuntime Hot Update of Unity 3D Model Display Framework (End)

Series Article Directory

Unity 3D model display framework project organization
Unity 3D model display framework framework use
Unity 3D model display framework free observation (Cinemachine)
Unity 3D model display framework resource packaging, loading, hot update (Addressable Asset System | Abbreviation AA)
Unity 3D Model Display Framework: Resource Packaging, Loading, and Hot Update (2)
Unity 3D Model Display Framework: ILRuntime Quick Start
Unity 3D Model Display Framework: ILRuntime Integration and Application



foreword

This project will integrate the previous Unity program basic small framework column on the basis of the Unity3D model display project, and record the adjustment process of the original script during the integration process. Added Asset Bundle + ILRuntime hot update technology process.


This article mainly introduces the process and usage examples of how to use Addressable and ILRuntime project integration in Unity projects.

1. Why Addressables+ILRuntime is needed for hot update?

ILRuntimeThe project provides a pure C# implementation for C#-based platforms (such as Unity), fast, convenient and reliable IL runtime, enabling hot updates in hardware environments that do not support JIT (such as iOS) 代码.
AddressablesIt is the official addressable asset system that provides an easy way to load assets by "address". It handles asset management overhead by simplifying the creation and deployment of content packs. The addressable asset system uses asynchronous loading to support loading any collection of dependencies from anywhere. Whether you use direct references, traditional asset bundles, or ResourceFolders for asset management, addressable assets provide an easier way to make your game more dynamic. What is an asset? Assets are what you use to create your game or application. Common examples of assets include prefabs, textures, materials, audio clips, and animations. What are addressable assets? Setting an asset as "addressable" allows you to use that asset's unique address to call it anywhere.

Simply put, ILRuntime+ Addressablescovers code updates and resource hot updates in hot updates.

For details on ILRuntime, see the official document
Addressables official document

2. Integration steps

Since AddressablesDLL resource packaging is not supported, it is necessary to convert the DLL file into a binary file, and then Addressablespackage the resource hot update, and load the ILRuntimeentry application domain initialized through the DLL file stream AppDomain. Completed ILRuntimethe update to the code project.

1. The ILRuntime project packs the DLL into a binary file

  • Modify ILRuntimethe DLL output directory of the hot update project.
    The reason for modifying the directory is that an error is reported StreamingAssetsduring Addressablesgrouping. Assets/ILRunTimeAndAddressable/AssetsPackage/HotFixDllTherefore , create a directory for generating DLL files, and Assets/ILRunTimeAndAddressable/AssetsPackage/HotFixDllToBytesa directory for storing binary files converted by DLL.HotFixDllHotFixDllToBytes
    insert image description here
    insert image description here

  • Create a DLL to binary file editor
    Create in the Editor folderDllToBytes.cs
    insert image description here

DllToBytes.cscode show as below:


using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;


public class DllToBytes 
{
    
    
    public static string normalPath = Application.dataPath + "/ILRunTimeAndAddressable/AssetsPackage/HotFixDll";
    public static string normalPathToSave = Application.dataPath + "/ILRunTimeAndAddressable/AssetsPackage/HotFixDllToBytes";
    [MenuItem("MyMenu/ILRuntime/DllToByte")]
    public static void DllToByte()
    {
    
    
        DllToByte(true);
    }
    [MenuItem("MyMenu/ILRuntime/DllToByteChoose")]
    public static void DllToByteChoose()
    {
    
    
        DllToByte(false);
    }

    private static void DllToByte(bool autoChoosePath)
    {
    
    
        string folderPath, savePath;
        if (autoChoosePath)
        {
    
    
            folderPath = normalPath;
        }
        else
        {
    
    
            folderPath = EditorUtility.OpenFolderPanel("dll所在的文件夹", Application.dataPath + "/addressable/IlRuntime", string.Empty);
        }

        if (string.IsNullOrEmpty(folderPath))
        {
    
    
            return;
        }
        DirectoryInfo directoryInfo = new DirectoryInfo(folderPath);
        FileInfo[] fileInfos = directoryInfo.GetFiles();
        List<FileInfo> listDll = new List<FileInfo>();
        List<FileInfo> listPdb = new List<FileInfo>();

        for (int i = 0; i < fileInfos.Length; i++)
        {
    
    
            if (fileInfos[i].Extension == ".dll")
            {
    
    
                listDll.Add(fileInfos[i]);
            }

            else if (fileInfos[i].Extension == ".pdb")
            {
    
    
                listPdb.Add(fileInfos[i]);
            }
        }

        if (listDll.Count + listPdb.Count <= 0)
        {
    
    
            Debug.Log("文件夹下没有dll文件");
        }
        else
        {
    
    
            Debug.Log("路径为:" + folderPath);
        }

        if (autoChoosePath)
        {
    
    
            savePath = normalPathToSave;
        }
        else
        {
    
    
            savePath = EditorUtility.OpenFolderPanel("dll要保存的文件夹", Application.dataPath + "/addressable/IlRuntime", string.Empty);
        }
        Debug.Log("-----开始转换dll文件------");
        string path = string.Empty;
        for (int i = 0; i < listDll.Count; i++)
        {
    
    
            //$ 符号的作用:等同于string.Format(),不用写占位符了,直接拼起来就可以了

            path = $"{
      
      savePath}/{
      
      Path.GetFileNameWithoutExtension(listDll[i].Name)}_dll_res.bytes";
            Debug.Log(path);
            BytesToFile(path, FileToBytes(listDll[i]));
        }
        Debug.Log("------dll文件转换结束---------");

        Debug.Log("-----开始转换pdb文件------");
        for (int i = 0; i < listPdb.Count; i++)
        {
    
    
            //$ 符号的作用:等同于string.Format(),不用写占位符了,直接拼起来就可以了

            path = $"{
      
      savePath}/{
      
      Path.GetFileNameWithoutExtension(listPdb[i].Name)}_pdb_res.bytes";
            BytesToFile(path, FileToBytes(listPdb[i]));
        }
        Debug.Log("------pdb文件转换结束---------");
        AssetDatabase.Refresh();

    }
    /// <summary>
    /// file转byte
    /// </summary>
    /// <param name="fileInfo"></param>
    /// <returns></returns>
    private static byte[] FileToBytes(FileInfo fileInfo)
    {
    
    
        return File.ReadAllBytes(fileInfo.FullName);
    }
    /// <summary>
    /// byte转文件
    /// </summary>
    /// <param name="path"></param>
    /// <param name="bytes"></param>
    private static void BytesToFile(string path, byte[] bytes)
    {
    
    
        Debug.Log($"路径为:{
      
      path}\nlength:{
      
      bytes.Length}");
        File.WriteAllBytes(path, bytes);
    }

}

insert image description here
insert image description here

2. Unity project loads hot update DLL binary files

HotFixMgr.csAdded LoadHotFixAssemblyloading hotfix DLL via binary file. Modify the previously loaded path of the DLL Application.streamingAssetsPathto the newly changed path. The overall code is as follows:
insert image description here

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class HotFixMgr : MonoBehaviour
{
    
    
    public  static HotFixMgr instance;
    public ILRuntime.Runtime.Enviorment.AppDomain appdomain;
    private System.IO.MemoryStream m_fs, m_p;
    public static HotFixMgr Instance
    {
    
    
        get
        {
    
    
            if (instance==null)
            {
    
    
                instance=new GameObject("HotFixMgr").AddComponent<HotFixMgr>();
                instance.LoadHotFixAssembly();
            }
            return instance;
        }
    }

    // Start is called before the first frame update
    void Start()
    {
    
    
    }
        public void LoadHotFixAssembly() {
    
    
        dllPath = Application.dataPath + "/ILRunTimeAndAddressable/AssetsPackage/HotFixDll";
        appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
#if UNITY_ANDROID
    WWW www = new WWW(Application.streamingAssetsPath + "/Hotfix.dll");
#else
        WWW www = new WWW("file:///" + dllPath + "/HotFix_Project.dll");
#endif
        while (!www.isDone)
            //yield return null;
            System.Threading.Thread.Sleep(100);
        if (!string.IsNullOrEmpty(www.error))
            Debug.LogError(www.error);
        byte[] dll = www.bytes;
        www.Dispose();
#if UNITY_ANDROID
    www = new WWW(Application.streamingAssetsPath + "/Hotfix.pdb");
#else
        www = new WWW("file:///" + dllPath + "/HotFix_Project.pdb");
#endif
        while (!www.isDone)
            //yield return null;
            System.Threading.Thread.Sleep(100);
        if (!string.IsNullOrEmpty(www.error))
            Debug.LogError(www.error);
        byte[] pdb = www.bytes;
        System.IO.MemoryStream fs = new MemoryStream(dll);
        System.IO.MemoryStream p = new MemoryStream(pdb);
        appdomain.LoadAssembly(fs, p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());

        OnILRuntimeInitialized();
    }


    /// <summary>
    /// 加载dll,pdb
    /// </summary>
    /// <param name="dll"></param>
    /// <param name="pdb"></param>
    public void LoadHotFixAssembly(byte[] dll, byte[] pdb)
    {
    
    
        m_fs = new MemoryStream(dll);
        m_p = new MemoryStream(pdb);
        try
        {
    
    
            appdomain.LoadAssembly(m_fs, m_p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
        }
        catch
        {
    
    
            Debug.LogError("加载热更DLL失败,请确保已经通过VS打开Assets/Samples/ILRuntime/1.6/Demo/HotFix_Project/HotFix_Project.sln编译过热更DLL");
            return;
        }
        appdomain.DebugService.StartDebugService(56000);
        OnILRuntimeInitialized();
    }

    void OnILRuntimeInitialized()
    {
    
    
        //appdomain.Invoke("Hotfix.Game", "Initialize", null, null);

#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
        //由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
        appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif

        //下面再举一个这个Demo中没有用到,但是UGUI经常遇到的一个委托,例如UnityAction<float>
        appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction>((action) =>
        {
    
    
            return new UnityEngine.Events.UnityAction(() =>
            {
    
    
                ((System.Action)action)();
            });
        });
    }
    // Update is called once per frame
    void Update()
    {
    
    

    }
}

Modify ProLaunch.csand add the loading entry for hot update DLL.
insert image description here
The specific code is as follows

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
/// <summary>
/// 加载方式
/// </summary>
public enum LoadMode
{
    
    
    ByLocalDll,
    ByLocalAddressable
}


public class ProLaunch : MonoBehaviour
{
    
    

    /// <summary>
    /// 显示下载状态和进度
    /// </summary>
    public Text UpdateText;
    public Text DownText;

    public Button btnCheckAndUpdate;
    public Button btnUpdate;
    public Button btnDown;
    public Button btnLogin;
    public Slider Slider;//滑动条组件

    private List<object> _updateKeys = new List<object>();

    //public Transform father;
    [Tooltip("dll文件的加载方式")]
    public LoadMode loadingMode = LoadMode.ByLocalAddressable;
    // Start is called before the first frame update
    void Start()
    {
    
    
        //retryBtn.gameObject.SetActive(false);
        btnCheckAndUpdate.onClick.AddListener(() =>
        {
    
    
            StartCoroutine(DoUpdateAddressadble());
        });
        btnUpdate.onClick.AddListener(() =>
        {
    
    
            UpdateCatalog();
        });
        // 默认自动执行一次更新检测
        //StartCoroutine(DoUpdateAddressadble());
        StartHotFix();
        btnDown.onClick.AddListener(() =>
        {
    
    
            DownLoad();
        });



        btnLogin.onClick.AddListener(() =>
        {
    
    
            SceneManager.LoadScene(1);
            //StartCoroutine(LoadScene("Test2"));

        });
    }

    // Update is called once per frame
    void Update()
    {
    
    
        
    }

    /// <summary>
    /// 加载dll
    /// </summary>
    /// <returns></returns>
    public async System.Threading.Tasks.Task StartHotFix()
    {
    
    

        //去服务器上下载最新的aa包资源

        //下载热更代码
        //string m_url=null;
        byte[] dll = new byte[] {
    
     };
        byte[] pdb = new byte[] {
    
     };
        if (loadingMode == LoadMode.ByLocalDll)
        {
    
    
            //StartCoroutine(CheckHotUpdate(dll,pdb));

           HotFixMgr.instance.LoadHotFixAssembly();
        }
        else if (loadingMode == LoadMode.ByLocalAddressable)
        {
    
    
            TextAsset assetDll = await Addressables.LoadAssetAsync<TextAsset>("HotFix_Project_dll_res").Task;
            dll = assetDll.bytes;
            TextAsset assetPdb = await Addressables.LoadAssetAsync<TextAsset>("HotFix_Project_pdb_res").Task;
            pdb = assetPdb.bytes;
            HotFixMgr.instance.LoadHotFixAssembly(dll, pdb);


        }

    }
    public async void UpdateCatalog()
    {
    
    
        //初始化Addressable
        var init = Addressables.InitializeAsync();
        await init.Task;

        //开始连接服务器检查更新
        var handle = Addressables.CheckForCatalogUpdates(false);
        await handle.Task;
        Debug.Log("check catalog status " + handle.Status);
        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
    
    
            List<string> catalogs = handle.Result;
            if (catalogs != null && catalogs.Count > 0)
            {
    
    
                foreach (var catalog in catalogs)
                {
    
    
                    Debug.Log("catalog  " + catalog);
                }
                Debug.Log("download catalog start ");

                UpdateText.text = UpdateText.text + "\n下载更新catalog";
                var updateHandle = Addressables.UpdateCatalogs(catalogs, false);
                await updateHandle.Task;
                foreach (var item in updateHandle.Result)
                {
    
    
                    Debug.Log("catalog result " + item.LocatorId);

                    foreach (var key in item.Keys)
                    {
    
    
                        Debug.Log("catalog key " + key);
                    }
                    _updateKeys.AddRange(item.Keys);
                }
                Debug.Log("download catalog finish " + updateHandle.Status);

                UpdateText.text = UpdateText.text + "\n更新catalog完成" + updateHandle.Status;
            }
            else
            {
    
    
                Debug.Log("dont need update catalogs");
                UpdateText.text = "没有需要更新的catalogs信息";
            }
        }
        Addressables.Release(handle);

    }


    public void DownLoad()
    {
    
    
        StartCoroutine(DownAssetImpl());
    }

    public IEnumerator DownAssetImpl()
    {
    
    
        var downloadsize = Addressables.GetDownloadSizeAsync(_updateKeys);
        yield return downloadsize;
        Debug.Log("start download size :" + downloadsize.Result);
        UpdateText.text = UpdateText.text + "\n更新文件大小" + downloadsize.Result;

        if (downloadsize.Result > 0)
        {
    
    
            var download = Addressables.DownloadDependenciesAsync(_updateKeys, Addressables.MergeMode.Union);
            yield return download;
            //await download.Task;
            Debug.Log("download result type " + download.Result.GetType());
            UpdateText.text = UpdateText.text + "\n下载结果类型 " + download.Result.GetType();


            foreach (var item in download.Result as List<UnityEngine.ResourceManagement.ResourceProviders.IAssetBundleResource>)
            {
    
    
                var ab = item.GetAssetBundle();
                Debug.Log("ab name " + ab.name);

                UpdateText.text = UpdateText.text + "\n ab名称 " + ab.name;


                foreach (var name in ab.GetAllAssetNames())
                {
    
    
                    Debug.Log("asset name " + name);
                    UpdateText.text = UpdateText.text + "\n asset 名称 " + name;

                }
            }
            Addressables.Release(download);
        }
        Addressables.Release(downloadsize);
    }


    IEnumerator LoadScene(string senceName)
    {
    
    
        // 异步加载场景(如果场景资源没有下载,会自动下载),

        var handle = Addressables.LoadSceneAsync(senceName);
        if (handle.Status == AsyncOperationStatus.Failed)
        {
    
    
            Debug.LogError("场景加载异常: " + handle.OperationException.ToString());
            yield break;
        }
        while (!handle.IsDone)
        {
    
    
            // 进度(0~1)
            float percentage = handle.PercentComplete;
            Debug.Log("进度: " + percentage);
            yield return null;
        }

        Debug.Log("场景加载完毕");
    }


    IEnumerator DoUpdateAddressadble()
    {
    
    
        AsyncOperationHandle<IResourceLocator> initHandle = Addressables.InitializeAsync();
        yield return initHandle;

        // 检测更新
        var checkHandle = Addressables.CheckForCatalogUpdates(false);
        yield return checkHandle;
        if (checkHandle.Status != AsyncOperationStatus.Succeeded)
        {
    
    
            OnError("CheckForCatalogUpdates Error\n" + checkHandle.OperationException.ToString());
            yield break;
        }

        if (checkHandle.Result.Count > 0)
        {
    
    
            var updateHandle = Addressables.UpdateCatalogs(checkHandle.Result, false);
            yield return updateHandle;

            if (updateHandle.Status != AsyncOperationStatus.Succeeded)
            {
    
    
                OnError("UpdateCatalogs Error\n" + updateHandle.OperationException.ToString());
                yield break;
            }

            // 更新列表迭代器
            List<IResourceLocator> locators = updateHandle.Result;
            foreach (var locator in locators)
            {
    
    
                List<object> keys = new List<object>();
                keys.AddRange(locator.Keys);
                // 获取待下载的文件总大小
                var sizeHandle = Addressables.GetDownloadSizeAsync(keys);
                yield return sizeHandle;
                if (sizeHandle.Status != AsyncOperationStatus.Succeeded)
                {
    
    
                    OnError("GetDownloadSizeAsync Error\n" + sizeHandle.OperationException.ToString());
                    yield break;
                }

                long totalDownloadSize = sizeHandle.Result;
                UpdateText.text = UpdateText.text + "\ndownload size : " + totalDownloadSize;
                Debug.Log("download size : " + totalDownloadSize);
                if (totalDownloadSize > 0)
                {
    
    
                    // 下载
                    var downloadHandle = Addressables.DownloadDependenciesAsync(keys, Addressables.MergeMode.Union, false);
                    //yield return downloadHandle;
                    while (!downloadHandle.IsDone)
                    {
    
    
                        if (downloadHandle.Status == AsyncOperationStatus.Failed)
                        {
    
    
                            OnError("DownloadDependenciesAsync Error\n" + downloadHandle.OperationException.ToString());
                            yield break;
                        }
                        // 下载进度
                        float percentage = downloadHandle.PercentComplete;


                        Debug.Log($"已下载: {
      
      percentage}");
                        DownText.text = $"已下载: {
      
      Mathf.Round(percentage * 100)}%";
                        Slider.value = percentage;
                        if (percentage >= 0.98f)//如果进度条已经到达90%
                        {
    
    
                          
                            Slider.value = 1; //那就让进度条的值编变成1
                        }

                        yield return null;
                    }
                    yield return downloadHandle;
                    if (downloadHandle.Status == AsyncOperationStatus.Succeeded)
                    {
    
    
                        Debug.Log("下载完毕!");
                        DownText.text = DownText.text + " 下载完毕";
                    }
                }
            }
        }
        else
        {
    
    
            UpdateText.text = UpdateText.text + "\n没有检测到更新";
        }

        // 进入游戏
        EnterPro();
    }
    // 进入游戏
    void EnterPro()
    {
    
    
        // TODO
        UpdateText.text = UpdateText.text + "\n进入游戏场景";
        Debug.Log("进入游戏");
    }
    private void OnError(string msg)
    {
    
    
        UpdateText.text = UpdateText.text + $"\n{
      
      msg}\n请重试! ";
    }
}

3. Engineering integration and testing

Test: Add a new button in the prefab LoginUI 退出, and create a registration event for binding the button click in the hot update project. Use Addressables for grouping, build update packages for testing. Specific steps are as follows:

  • Modify the hot update project HotFix.csand add a static method for registering button click events. code show as below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

namespace HotFix_Project
{
    
    
    public class HotFix
    {
    
    
        void Awake()
        {
    
    
            UnityEngine.Debug.Log("Awake");
        }
        public  static void HelloWorld(string name,int age) {
    
    
            UnityEngine.Debug.Log("HelloWorld "+name+" "+ age);
        }

        public string Hello(string name) {
    
    
            return "hello " + name;
        }
        public static void bindClick(GameObject btn) {
    
    
            btn.transform.GetComponent<Button>().onClick.AddListener(()=> {
    
    
                UnityEngine.Debug.Log("Hot click");
            });
        }
    }
}

  • Modify the prefab LoginUI to add an exit button, create login.csand call the code in the hot update project to register, and hang this script in the loginUI. code show as below:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Login : MonoBehaviour
{
    
    
    public GameObject btn;

    // Start is called before the first frame update
    void Start()
    {
    
    
        string className = "HotFix_Project.HotFix";
        string funName = "bindClick";
        btn = gameObject.transform.Find("btnClose").gameObject;
        HotFixMgr.Instance.appdomain.Invoke(className, funName, null, btn);
    }
    // Update is called once per frame
    void Update()
    {
    
    
    }
}

  • Test locally

insert image description here
Run the code, after clicking the exit button:
insert image description here

  • Build the hot update package and publish it
    insert image description here
    . Modify the way to load the DLL in the startup program as shown in the figure:
    insert image description here

Build and copy the updated files to the server.
insert image description here
Modify Play Mode Scriptthe settings as shown in the figure
insert image description here

4. Effect demonstration

Unity 3D model display framework article effect demonstration


Summarize

The above is what I want to talk about today. This article introduces how to complete the integration and testing of hot update cases through ILRuntime+Adressables technology. This is also the last article in the series of articles on the Unity 3D model display framework. During the reading process, it is found that there are omissions or deficiencies. Welcome Everyone leave a message to correct me.


点赞过千公布项目案例
insert image description here

Guess you like

Origin blog.csdn.net/yxl219/article/details/126304884