Huatuo热更新--如何使用

news2025/5/24 20:14:23

在安装完huatuo热更新插件后就要开始学习如何使用了。

1.创建主框渐Main

新建文件夹Main(可自定义),然后按下图创建文件,注意名称与文件夹名称保持一致

 然后新建场景(Init场景),添加3个空物体分别为LoadDllManager,SceneLoadManager以及PrefabsLoadManager(这部分可根据实际开发需求拓展,此教程只做简单演示,只有切换场景,创建预制体,加载Dll需求),然后在Main文件夹下创建对应名称脚本文件并挂在相应物体上。

注意,Main里的脚本是框架类脚本,不做具体功能需求,所以不支持热更新,一般实现后不会再做修改,一旦修改了就需要重新Build。

下面是3个脚本具体实现。

注意,需要用到一个BetterStreamingAssets加载AB包的类,下载地址:Huatuo热更新使用教程-BetterStreamingAssets资源-CSDN文库

解压后放到Plugins文件夹下即可。

实现Manager脚本时会发现BetterStreamingAssets类提示报错,这是因为Main中没有添加BetterStreamingAssets,进行如下图所示操作即可,之后就会发现报错解决了。同理,其他Assembly Definition文件在使用其他Assembly Definition文件中的类时,也需要进行同样设置,比如之后添加的UIPart需要添加Main。

LoadDllManager

using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 加载Dll的管理器
/// </summary>
public class LoadDllManager : MonoBehaviour
{
    private static LoadDllManager _instance;
    /// <summary>
    /// 单例
    /// </summary>
    public static LoadDllManager Instance
    {
        get
        {
            return _instance;
        }
    }

    private void Awake()
    {
        _instance = this;
    }

    void Start()
    {
        Debug.Log("LoadDllManager start");
        BetterStreamingAssets.Initialize();
        DontDestroyOnLoad(gameObject);
        //加载初始Dll-UIPart
        LoadDll("UIPart", (value) =>
        {
            //找到MainScript脚本,执行LoadMainScene方法
            Type type = value.GetType("MainScript");
            type.GetMethod("LoadMainScene").Invoke(null, null);
        });
    }

    /// <summary>
    /// 加载dll
    /// </summary>
    /// <param name="dllName">dll名称</param>
    /// <param name="callBack">回调</param>
    public void LoadDll(string dllName, UnityAction<Assembly> callBack)
    {
#if !UNITY_EDITOR
        StartCoroutine(OnLoadDll(dllName, callBack));
#else
        var assembly = AppDomain.CurrentDomain.GetAssemblies().First(assembly => assembly.GetName().Name == dllName);
        callBack?.Invoke(assembly);
#endif
    }

    /// <summary>
    /// 协程加载dll
    /// </summary>
    /// <param name="dllName"></param>
    /// <param name="callBack"></param>
    /// <returns></returns>
    private IEnumerator OnLoadDll(string dllName, UnityAction<Assembly> callBack)
    {
        //判断ab包是否存在
        if (File.Exists($"{Application.streamingAssetsPath}/common"))
        {
            //加载ab包
            var dllAB = BetterStreamingAssets.LoadAssetBundleAsync("common");
            yield return dllAB;

            if(dllAB.assetBundle != null)
            {
                //加载dll
                TextAsset dllBytes = dllAB.assetBundle.LoadAsset<TextAsset>($"{dllName}.dll.bytes");
                var assembly = System.Reflection.Assembly.Load(dllBytes.bytes);
                //卸载ab包
                dllAB.assetBundle.Unload(false);
                //回调
                callBack?.Invoke(assembly);
            }
        }
    }
}

SceneLoadManager

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

/// <summary>
/// 加载场景的管理器
/// </summary>
public class SceneLoadManager : MonoBehaviour
{
    private static SceneLoadManager _instance;
    public static SceneLoadManager Instance
    {
        get
        {
            return _instance;
        }
    }

    private void Awake()
    {
        _instance = this;
    }

    private void Start()
    {
        Debug.Log("SceneLoadManager start");
        BetterStreamingAssets.Initialize();
        DontDestroyOnLoad(gameObject);
    }

    /// <summary>
    /// 加载场景
    /// </summary>
    /// <param name="sceneName"></param>
    /// <param name="callBack"></param>
    public void LoadScene(string sceneName, UnityAction callBack = null)
    {
#if !UNITY_EDITOR
        StartCoroutine(OnLoadScene(sceneName, callBack));
#else
        StartCoroutine(OnLoadScene_Noab(sceneName, callBack));
#endif
    }

    /// <summary>
    /// 通过ab包加载场景
    /// </summary>
    /// <param name="sceneName"></param>
    /// <param name="callBack"></param>
    /// <returns></returns>
    private IEnumerator OnLoadScene(string sceneName, UnityAction callBack)
    {
        //判断场景ab包是否存在
        if(File.Exists($"{Application.streamingAssetsPath}/scenes"))
        {
            //加载ab包
            var dllAB = BetterStreamingAssets.LoadAssetBundleAsync("scenes");
            yield return dllAB;

            if(dllAB.assetBundle != null)
            {
                //异步加载场景
                var sceneLoadRequest = SceneManager.LoadSceneAsync(sceneName);
                yield return sceneLoadRequest;

                if(sceneLoadRequest.isDone)
                {
                    //获取加载的场景
                    Scene loadScene = SceneManager.GetSceneByName(sceneName);
                    //跳转场景
                    SceneManager.SetActiveScene(loadScene);
                    //回调
                    callBack?.Invoke();
                }
                //卸载AB包
                dllAB.assetBundle.Unload(false);
            }
        }
    }

    /// <summary>
    /// 加载场景--无需加载ab
    /// </summary>
    /// <param name="sceneName"></param>
    /// <param name="callBack"></param>
    /// <returns></returns>
    private IEnumerator OnLoadScene_Noab(string sceneName, UnityAction callBack)
    {
        //异步加载场景
        var sceneLoadRequest = SceneManager.LoadSceneAsync(sceneName);
        yield return sceneLoadRequest;

        if (sceneLoadRequest.isDone)
        {
            //获取加载的场景
            Scene loadScene = SceneManager.GetSceneByName(sceneName);
            //跳转场景
            SceneManager.SetActiveScene(loadScene);
            //回调
            callBack?.Invoke();
        }
    }
}

PrefabsLoadManager

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

/// <summary>
/// 加载预制体的管理器
/// </summary>
public class PrefabsLoadManager : MonoBehaviour
{
    private static PrefabsLoadManager _instance;
    public static PrefabsLoadManager Instance
    {
        get
        {
            return _instance;
        }
    }

    private void Awake()
    {
        _instance = this;
    }

    private void Start()
    {
        Debug.Log("PrefabsLoadManager start");
        BetterStreamingAssets.Initialize();
        DontDestroyOnLoad(gameObject);
    }

    /// <summary>
    /// 加载预制体
    /// </summary>
    /// <param name="prefabPath"></param>
    /// <param name="callBack"></param>
    public void LoadABPrefab(string prefabPath, UnityAction<GameObject> callBack)
    {
#if !UNITY_EDITOR
        string[] paths = prefabPath.Split('/');
        string prefabName = paths[paths.Length - 1];
        StartCoroutine(OnLoadPrefab(prefabName, callBack));
#else
        prefabPath += ".prefab";
        GameObject loadedPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
        GameObject obj = GameObject.Instantiate(loadedPrefab);
        callBack?.Invoke(obj);
#endif
    }

    /// <summary>
    /// 通过AB包加载预制体
    /// </summary>
    /// <param name="prefabName"></param>
    /// <param name="callBack"></param>
    /// <returns></returns>
    private IEnumerator OnLoadPrefab(string prefabName, UnityAction<GameObject> callBack)
    {
        //判断预制体的ab包是否存在
        if (File.Exists($"{Application.streamingAssetsPath}/prefabs"))
        {
            //加载ab包
            var dllAB = BetterStreamingAssets.LoadAssetBundleAsync("prefabs");
            yield return dllAB;

            if(dllAB.assetBundle != null)
            {
                //创建预制体
                GameObject loadedPrefab = GameObject.Instantiate(dllAB.assetBundle.LoadAsset<UnityEngine.GameObject>($"{prefabName}.prefab"));
                //卸载ab包
                dllAB.assetBundle.Unload(false);
                callBack?.Invoke(loadedPrefab);
            }
        }
    }
}

后续根据需求还会有图集的AB包,材质的AB包等,在此不做详细扩展。

至此一个主要的框架就好了,下面就要开始实现热更新的部分了。

2.实现UIPart热更新部分功能

创建UIPart文件夹(名称及内部脚本名称,方法名称可随意修改,但需要相应修改LoadDllManager对应名称字段),然后创建同名Assembly Definition文件。

创建MainScript脚本,实现如下

MainScript脚本为加载Main场景,创建Main场景,场景中添加一个Canvas,创建MainCanvas脚本,实现如下,创建MainView预制体

using UnityEngine;
using System;
using System.Linq;

public class MainCanvas : MonoBehaviour
{
    public GameObject lay_1;
    public GameObject lay_2;
    public GameObject lay_3;

    public static AssetBundle dllAB;
    private System.Reflection.Assembly gameAss;

    void Start()
    {
        PrefabsLoadManager.Instance.LoadABPrefab("Assets/UIPart/Prefabs/UI/MainView", (mainView) =>
        {
            if (mainView != null)
                mainView.transform.SetParent(lay_1.transform, false);
        });
    }
}

然后创建MainView预制体及脚本,实现自己想实现的测试功能,在此就不具体实现了。注意上述预制体,脚本都需要放在UIPart文件夹下,可自行创建区分的文件夹。

这些都完成后,需要在HybridCLR中配置一下,如图

 之后就可以进行生成DLL,打包AB包等操作

3.生成Dll文件

如图,自行选择平台

 生成Dll文件所在路径为Assets同级目录\HybridCLRData\HotUpdateDlls下对应平台内。

4.复制Dll,方便打AB包

然后就是打包AB包,打包前先将生成的Dll及部分依赖的Dll先复制到Assets内,方便打包成AB包,此处提供一个我简单实现的复制工具(使用UIToolkit实现)

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

public class CopyDllEditor : EditorWindow
{
    public static readonly List<string> aotDlls = new List<string>()
            {
                "mscorlib.dll",
                "System.dll",
                "System.Core.dll",// 如果使用了Linq,需要这个
                // "Newtonsoft.Json.dll",
                // "protobuf-net.dll",
                // "Google.Protobuf.dll",
                // "MongoDB.Bson.dll",
                // "DOTween.Modules.dll",
                // "UniTask.dll",
            };

    /// <summary>
    /// 复制dll相关数据
    /// </summary>
    CopyDllData dllData = null;
    /// <summary>
    /// 用于初始化的json文件路径
    /// </summary>
    private string DllFileJsonPath = "";

    [MenuItem("CopyDllEditor/Settings")]
    public static void ShowExample()
    {
        CopyDllEditor wnd = GetWindow<CopyDllEditor>();
        wnd.titleContent = new GUIContent("CopyDllEditor");
        wnd.minSize = new Vector2(810, 540);
        wnd.maxSize = new Vector2(1910, 810);

        //wnd.position = new Rect(new Vector2(1920, 540), new Vector2(1600, 540));
    }

    public void CreateGUI()
    {
        DllFileJsonPath = $"{Application.dataPath}/Editor/CopyDll/DllFile.json";
        //初始化
        Init();
        if(dllData == null)
        {
            dllData = new CopyDllData();
            dllData.Files = new List<string>();
        }
        
        if (!File.Exists(DllFileJsonPath))
        {
            File.Create(DllFileJsonPath);
        }

        VisualElement root = rootVisualElement;
        //添加平台选择
        EnumField toType = new EnumField("选择平台");
        toType.Init(BuildTarget.StandaloneWindows64);
        //初始化平台选择
        if (!string.IsNullOrEmpty(dllData.PingTaiType))
        {
            //toType.value = (BuildTarget)System.Enum.Parse(typeof(BuildTarget), dllData.PingTaiType);
        }
        else
        {
            dllData.PingTaiType = toType.value.ToString();
        }
        //平台改变监听
        toType.RegisterCallback<ChangeEvent<string>>((evt) =>
        {
            dllData.PingTaiType = evt.newValue;
        });
        root.Add(toType);
        
        //dll原始文件所在路径输入框
        TextField formPathInput = new TextField("dll原始文件路径(无需加平台文件夹名称,末尾加\\)");
        //初始化
        if(!string.IsNullOrEmpty(dllData.FromPath))
        {
            formPathInput.value = dllData.FromPath;
        }
        //监听原始文件路径改变
        formPathInput.RegisterCallback<ChangeEvent<string>>((evt) =>
        {
            dllData.FromPath = evt.newValue;
        });
        root.Add(formPathInput);
        
        //复制到目标目录路径输入框
        TextField toPathInput = new TextField("dll保存文件路径(无需加平台文件夹名称,最好为工程Assets内路径,末尾加\\)");
        //初始化
        if (!string.IsNullOrEmpty(dllData.ToPath))
        {
            toPathInput.value = dllData.ToPath;
        }
        //监听目标路径改变
        toPathInput.RegisterCallback<ChangeEvent<string>>((evt) =>
        {
            dllData.ToPath = evt.newValue;
        });
        root.Add(toPathInput);

        //设置dll文件数量的输入框
        IntegerField filescount = new IntegerField("dll文件数量");
        //初始化
        filescount.value = dllData.Files.Count;
        root.Add(filescount);

        //滑动界面
        ScrollView scrollView = new ScrollView();
        root.Add(scrollView);
        //所有文件名称输入框
        List<TextField> dllFileField = new List<TextField>();

        //初始化文件名称输入框
        foreach (var item in dllData.Files)
        {
            TextField fileName = new TextField("dll文件名称(带后缀)");
            scrollView.Add(fileName);
            fileName.value = item;
            dllFileField.Add(fileName);
        }

        //监听文件数量变化
        filescount.RegisterCallback<ChangeEvent<int>>((evt) =>
        {
            //若资源数量增加
            if (evt.newValue > evt.previousValue)
            {
                int count = evt.newValue - evt.previousValue;
                for (int i = 0; i < count; i++)
                {
                    TextField fileName = new TextField("dll文件名称(带后缀)");
                    scrollView.Add(fileName);

                    dllFileField.Add(fileName);
                }
            }
            else
            {
                int count = evt.previousValue - evt.newValue;
                int index = evt.previousValue - 1;
                //若减少,曾从后往前删除
                for (int i = 0; i < count; i++)
                {
                    scrollView.RemoveAt(index);
                    dllFileField.RemoveAt(index);
                    index--;
                }
            }
        });

        //复制dll文件按钮
        Button copyBtn = new Button(() =>
        {
            BuildTarget v = (BuildTarget)System.Enum.Parse(typeof(BuildTarget), toType.value.ToString());

            string yuanshiPath = GetHotFixDllsOutputDirByTarget(v);
            dllData.Files.Clear();
            foreach (var item in dllFileField)
            {
                //去除未输入的和重复的
                if(!string.IsNullOrEmpty(item.value) && !dllData.Files.Contains(item.value))
                {
                    //去除文件不存在的
                    string filePath = $"{yuanshiPath}/{item.value}";
                    if(File.Exists(filePath))
                        dllData.Files.Add(item.value);
                }
            }

            //保存当前设置结果到json文件中,用于下次打开初始化
            string fileValue = JsonUtility.ToJson(dllData);
            File.WriteAllText(DllFileJsonPath, fileValue);

            
            //选择平台进行文件复制
            switch(v)
            {
                case BuildTarget.StandaloneWindows:
                    CopeByStandaloneWindows32();
                    break;
                case BuildTarget.StandaloneWindows64:
                    CopeByStandaloneWindows64();
                    break;
                case BuildTarget.Android:
                    CopeByAndroid();
                    break;
                case BuildTarget.iOS:
                    CopeByIOS();
                    break;
            }
        });
        copyBtn.text = "复制dll文件";
        root.Add(copyBtn);
    }

    private void Init()
    {
        string value = File.ReadAllText(DllFileJsonPath);
        dllData = JsonUtility.FromJson<CopyDllData>(value);
    }

    private void CopeByStandaloneWindows32()
    {
        Copy(BuildTarget.StandaloneWindows);
    }

    private void CopeByStandaloneWindows64()
    {
        Copy(BuildTarget.StandaloneWindows64);
    }

    private void CopeByAndroid()
    {
        Copy(BuildTarget.Android);
    }

    private void CopeByIOS()
    {
        Copy(BuildTarget.iOS);
    }

    private void Copy(BuildTarget target)
    {
        //复制的dll文件列表
        List<string> copyDlls = dllData.Files;
        //dll原始路径
        string outDir = GetHotFixDllsOutputDirByTarget(target);
        //目标路径
        string exportDir = GetDllToPath(target);

        if (!Directory.Exists(exportDir))
        {
            Directory.CreateDirectory(exportDir);
        }
        //复制
        foreach (var copyDll in copyDlls)
        {
            File.Copy($"{outDir}/{copyDll}", $"{exportDir}/{copyDll}.bytes", true);
        }

        //复制固定需要的依赖dll文件,路径固定
        string AssembliesPostIl2CppStripDir = Application.dataPath.Remove(Application.dataPath.Length - 6, 6) + "HybridCLRData/AssembliesPostIl2CppStrip";
        string aotDllDir = $"{AssembliesPostIl2CppStripDir}/{target}";
        foreach (var dll in aotDlls)
        {
            string dllPath = $"{aotDllDir}/{dll}";
            if (!File.Exists(dllPath))
            {
                Debug.LogError($"ab中添加AOT补充元数据dll:{dllPath} 时发生错误,文件不存在。需要构建一次主包后才能生成裁剪后的AOT dll");
                continue;
            }
            string dllBytesPath = $"{exportDir}/{dll}.bytes";
            File.Copy(dllPath, dllBytesPath, true);
        }
        AssetDatabase.Refresh();
        Debug.Log("热更Dll复制成功!");
    }

    /// <summary>
    /// 获取热更新时输出dll文件的路径
    /// </summary>
    /// <param name="target"></param>
    /// <returns></returns>
    public string GetHotFixDllsOutputDirByTarget(BuildTarget target)
    {
        string path = dllData.FromPath;
        switch (target)
        {
            case BuildTarget.StandaloneWindows:
                path += "StandaloneWindows";
                break;
            case BuildTarget.StandaloneWindows64:
                path += "StandaloneWindows64";
                break;
            case BuildTarget.Android:
                path += "Android";
                break;
            case BuildTarget.iOS:
                path += "iOS";
                break;
        }

        return path;
    }

    /// <summary>
    /// 获取复制文件目标路径
    /// </summary>
    /// <param name="target"></param>
    /// <returns></returns>
    public string GetDllToPath(BuildTarget target)
    {
        string path = dllData.ToPath;
        switch (target)
        {
            case BuildTarget.StandaloneWindows:
                path += "StandaloneWindows";
                break;
            case BuildTarget.StandaloneWindows64:
                path += "StandaloneWindows64";
                break;
            case BuildTarget.Android:
                path += "Android";
                break;
            case BuildTarget.iOS:
                path += "iOS";
                break;
        }

        return path;
    }
}

[SerializeField]
public class CopyDllData
{
    public string FromPath;
    public string ToPath;
    public string PingTaiType;
    public List<string> Files;
}

放到Editor/CopyDll文件夹下即可,打开如下

先选择平台,然后设置原始Dll文件所在路径,再设置输出路径,填入dll文件数量并设置好dll文件名+后缀,最后点击复制即可完成复制。 

5.打AB包

此处同样提供一个我简单实现的打包工具(使用UIToolkit实现),也可使用其他打包的插件。

using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using UnityEngine;
using System.Collections.Generic;
using System.IO;
using System;
using Object = UnityEngine.Object;

public class AssetBundle : EditorWindow
{
    private Dictionary<string, List<Object>> bundles = new Dictionary<string, List<Object>>();

    /// <summary>
    /// ab包设置部分的滑动界面
    /// </summary>
    ScrollView abScr = null;

    [MenuItem("AssetBundle/Setting")]
    public static void ShowExample()
    {
        AssetBundle wnd = GetWindow<AssetBundle>();
        wnd.titleContent = new GUIContent("AssetBundle");
        wnd.minSize = new Vector2(810, 540);
        wnd.maxSize = new Vector2(1910, 810);

        //wnd.position = new Rect(new Vector2(1920, 540), new Vector2(1600, 540));
    }

    public void CreateGUI()
    {
        VisualElement root = rootVisualElement;

        //创建打包按钮,用于打出AB包
        Button btn_Add = new Button(() =>
        {
            //ab包
            List<AssetBundleBuild> abs = new List<AssetBundleBuild>();
            //记录当前打包的ab包信息,用于下次打开时初始化
            ABSaveJsonData saveData = new ABSaveJsonData();
            saveData.ABSave = new List<ABSaveData>();
            //遍历设置的ab包数据
            foreach (var item in bundles)
            {
                //单个ab包文件名与资源文件数据
                ABSaveData data = new ABSaveData();
                data.ABName = item.Key;
                data.ABFilePath = new List<string>();

                List<string> assets = new List<string>();
                foreach (var v in item.Value)
                {
                    if (v == null)
                        continue;
                    //获取资源路径,文件中存储路径信息
                    string filePath = AssetDatabase.GetAssetPath(v);
                    Debug.LogError(filePath);
                    if (assets.Contains(filePath))
                        continue;
                    assets.Add(filePath);
                    data.ABFilePath.Add(filePath);
                }

                AssetBundleBuild abFile = new AssetBundleBuild
                {
                    //包名
                    assetBundleName = item.Key,
                    //资源
                    assetNames = assets.ToArray(),
                };
                abs.Add(abFile);

                //添加每个ab包信息
                saveData.ABSave.Add(data);
            }

            //ab包保存位置
            string streamingAssetPathDst = $"{Application.streamingAssetsPath}";
            CreateDirIfNotExists(streamingAssetPathDst);
            BuildPipeline.BuildAssetBundles(streamingAssetPathDst, abs.ToArray(), BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64);
            AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);

            //ab包信息文件
            string bundleFilePath = $"{Application.dataPath}/Editor/AssetBundleEditor/ABFile.json";
            if (!File.Exists(bundleFilePath))
            {
                File.Create(bundleFilePath);
            }
            //序列化ab包信息
            string value = JsonUtility.ToJson(saveData);
            File.WriteAllText(bundleFilePath, value);
        });

        btn_Add.text = "打包";
        root.Add(btn_Add);

        CreatAddABBtn(root);
    }

    /// <summary>
    /// 创建添加ab包名的按钮
    /// </summary>
    /// <param name="root"></param>
    private void CreatAddABBtn(VisualElement root)
    {
        abScr = new ScrollView();
        abScr.style.width = rootVisualElement.style.width;
        abScr.style.height = rootVisualElement.style.height;

        Button btn_Add = new Button(() =>
        {
            VisualElement abVi = CreataABNameField();
            abScr.Add(abVi);
        });
        btn_Add.text = "添加ab包名称";
        root.Add(btn_Add);
        root.Add(abScr);

        OnInitBundles(abScr);
    }

    /// <summary>
    /// 初始化上次设置的资源数据
    /// </summary>
    /// <param name="root"></param>
    private void OnInitBundles(VisualElement root)
    {
        string bundleFilePath = $"{Application.dataPath}/Editor/AssetBundleEditor/ABFile.json";
        //反序列化文件数据
        string value = File.ReadAllText(bundleFilePath);
        ABSaveJsonData data = JsonUtility.FromJson<ABSaveJsonData>(value);

        foreach (var item in data.ABSave)
        {
            //初始化bundles
            if (!bundles.ContainsKey(item.ABName))
            {
                bundles.Add(item.ABName, new List<Object>());

                foreach (var path in item.ABFilePath)
                {
                    //通过资源路径获取到资源文件
                    bundles[item.ABName].Add(AssetDatabase.LoadAssetAtPath(path, typeof(Object)));
                }
            }
        }

        foreach (var item in bundles)
        {
            //初始化编辑器界面
            VisualElement abVi = CreataABNameField(item.Key, item.Value);
            root.Add(abVi);
        }
    }

    /// <summary>
    /// 创建ab包名称的输入框
    /// </summary>
    /// <param name="root"></param>
    /// <param name="defaultValue">初始包名</param>
    /// <param name="objects">初始资源</param>
    private VisualElement CreataABNameField(string defaultValue = "", List<Object> objects = null)
    {
        VisualElement abVi = new VisualElement();
        TextField field = new TextField("输入ab包名称");
        field.style.width = 610;
        abVi.Add(field);
        //监听内容修改
        field.RegisterCallback<ChangeEvent<string>>((evt) =>
        {
            //修改bundles
            if (bundles.ContainsKey(evt.previousValue))
            {
                bundles.Remove(evt.previousValue);
            }

            if(!bundles.ContainsKey(evt.newValue))
                bundles.Add(evt.newValue, new List<Object>());
        });

        //初始化包名
        if (string.IsNullOrEmpty(defaultValue))
            field.value = $"Default_{bundles.Count}";
        else
            field.value = defaultValue;

        CreateABCountField(abVi, field, objects);

        return abVi;
    }

    /// <summary>
    /// 创建ab包资源数量的输入框
    /// </summary>
    /// <param name="abVi"></param>
    /// <param name="field">用于设置bundles的key值</param>
    /// <param name="objects">初始资源对象</param>
    private void CreateABCountField(VisualElement abVi, TextField field, List<Object> objects = null)
    {
        //资源数量输入框
        IntegerField field_Count = new IntegerField("输入ab资源数量");
        field_Count.style.width = 200;
        field.Add(field_Count);

        Button delBtn = new Button(() =>
        {
            if(bundles.ContainsKey(field.value))
            {
                bundles.Remove(field.value);
            }
            abScr.Remove(abVi);
        });
        delBtn.style.width = 60;
        delBtn.text = "删除ab包";
        field.Add(delBtn);

        VisualElement objVisE = new VisualElement();
        objVisE.style.width = rootVisualElement.style.width;
        //objVisE.style.maxHeight = 100;

        //初始化资源对象
        if (objects != null)
        {
            //初始化数量
            field_Count.value = objects.Count;
            for (int i = 0; i < objects.Count; i++)
            {
                VisualElement objField = CreataABFile(field, objects[i]);
                objVisE.Add(objField);
            }
        }

        //监听数量修改
        field_Count.RegisterCallback<ChangeEvent<int>>((evt) =>
        {
            //若资源数量增加
            if(evt.newValue > evt.previousValue)
            {
                int count = evt.newValue - evt.previousValue;
                for (int i = 0; i < count; i++)
                {
                    VisualElement objField = CreataABFile(field);
                    objVisE.Add(objField);
                }
            }
            else
            {
                int count = evt.previousValue - evt.newValue;
                int index = evt.previousValue - 1;
                //若减少,曾从后往前删除
                for (int i = 0; i < count; i++)
                {
                    objVisE.RemoveAt(index);

                    if (bundles.ContainsKey(field.value) && bundles[field.value].Count > index)
                    {
                        bundles[field.value].RemoveAt(index);
                    }

                    index--;
                }
            }
            
        });
        abVi.Add(objVisE);
    }

    /// <summary>
    /// 创建ab包资源的输入框
    /// </summary>
    /// <param name="root"></param>
    /// <param name="field">用于设置bundles的key值</param>
    /// <param name="obj">初始资源对象</param>
    /// <returns></returns>
    private VisualElement CreataABFile(TextField field, Object obj = null)
    {
        //资源设置框
        ObjectField objField = new ObjectField();
        objField.objectType = typeof(Object);

        //初始化对象内容
        if(obj != null)
            objField.value = obj;

        //监听资源对象改变
        objField.RegisterCallback<ChangeEvent<Object>>((evt) =>
        {
            if (bundles.ContainsKey(field.value))
            {
                var objs = bundles[field.value];
                objs.Remove(evt.previousValue);
                objs.Add(evt.newValue);
            }
        });

        return objField;
    }

    //创建文件夹
    private static void CreateDirIfNotExists(string dirName)
    {
        if (!Directory.Exists(dirName))
        {
            Directory.CreateDirectory(dirName);
        }
    }
}

[Serializable]
public class ABSaveData
{
    [SerializeField]
    public string ABName;
    [SerializeField]
    public List<string> ABFilePath;
}

[Serializable]
public class ABSaveJsonData
{
    [SerializeField]
    public List<ABSaveData> ABSave;
}

放到Editor/AssetBundleEditor文件夹下即可,界面如图

点击添加ab包名称即可添加一个ab包设置,输入ab包名称及资源数量,设置资源对象最后点击打包即可,ab包输出在StreamingAssets文件夹下。

至此一个简单的热更新就实现了,最后Build工程(Build时只需要Build Init场景即可,无需勾选Main场景等AB包中的场景,当然在编辑器中运行时,需要勾选上其他场景,否则无法跳转),然后修改UIPart中的部分代码,之后依次执行生成dll,复制dll,打ab包,最后将StreamingAssets下的ab包替换到Build的工程中运行,就会发现修改的代码生效了。

下面为我实现的演示工程,地址为:Huatuo热更新演示工程资源-CSDN文库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2301803.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Django快递物流管理可视化分析系统(完整系统源码+数据库+详细开发文档+万字详细论文+答辩PPT+详细部署教程等资料)

文章目录 基于Django快递物流管理可视化分析系统&#xff08;完整系统源码数据库详细开发文档万字详细论文答辩PPT详细部署教程等资料&#xff09;一、项目概述二、项目说明三、研究意义四、系统设计技术架构 五、功能实现六、完整系统源码数据库详细开发文档万字详细论文答辩P…

基于射频开关选择的VNA校准设计

活动发起人小虚竹 想对你说&#xff1a; 这是一个以写作博客为目的的创作活动&#xff0c;旨在鼓励大学生博主们挖掘自己的创作潜能&#xff0c;展现自己的写作才华。如果你是一位热爱写作的、想要展现自己创作才华的小伙伴&#xff0c;那么&#xff0c;快来参加吧&#xff01…

解决本地模拟IP的DHCP冲突问题

解决 DHCP 冲突导致的多 IP 绑定失效问题 前言 续接上一篇在本机上模拟IP地址。 在实际操作中&#xff0c;如果本机原有 IP&#xff08;如 192.168.2.7&#xff09;是通过 DHCP 自动获取的&#xff0c;直接添加新 IP&#xff08;如 10.0.11.11&#xff09;可能会导致 DHCP 服…

Elasticsearch7.1.1 配置密码和SSL证书

生成SSL证书 ./elasticsearch-certutil ca -out config/certs/elastic-certificates.p12 -pass 我这里没有设置ssl证书密码&#xff0c;如果需要设置密码&#xff0c;需要再配置给elasticsearch 在之前的步骤中&#xff0c;如果我们对elastic-certificates.p12 文件配置了密码…

毕业项目推荐:基于yolov8/yolo11的100种中药材检测识别系统(python+卷积神经网络)

文章目录 概要一、整体资源介绍技术要点功能展示&#xff1a;功能1 支持单张图片识别功能2 支持遍历文件夹识别功能3 支持识别视频文件功能4 支持摄像头识别功能5 支持结果文件导出&#xff08;xls格式&#xff09;功能6 支持切换检测到的目标查看 二、数据集三、算法介绍1. YO…

自用题库---面试使用

1、css中如何实现水平垂直居中 方法一&#xff1a;flex&#xff1a; display: flex; justify-content: center; align-item: center;方法二&#xff1a;绝对定位margin:auto: position: absolute; left: 0; right: 0; top: 0; bottom: 0; margin:auto;方法三&#xff1a;已…

蓝桥杯好数

样例输入&#xff1a; 24 输出&#xff1a;7 输入&#xff1a;2024 输出&#xff1a; 150 思路&#xff1a;本题朴素方法的时间复杂度是O(n * log10(n)) &#xff0c;不超时。主要考察能否逐位取数&#xff0c;注意细节pi&#xff0c;这样不会改变i,否则会导致循环错误。 #in…

Jenkins 配置 Credentials 凭证

Jenkins 配置 Credentials 凭证 一、创建凭证 Dashboard -> Manage Jenkins -> Manage Credentials 在 Domain 列随便点击一个 (global) 二、添加 凭证 点击左侧 Add Credentials 四、填写凭证 Kind&#xff1a;凭证类型 Username with password&#xff1a; 配置 用…

用openresty和lua实现壁纸投票功能

背景 之前做了一个随机壁纸接口&#xff0c;但是不知道大家喜欢对壁纸的喜好&#xff0c;所以干脆在实现一个投票功能&#xff0c;让用户给自己喜欢的壁纸进行投票。 原理说明 1.当访问http://demo.com/vote/时&#xff0c;会从/home/jobs/webs/imgs及子目录下获取图片列表&…

mysql查看binlog日志

mysql 配置、查看binlog日志&#xff1a; 示例为MySQL8.0 1、 检查binlog开启状态 SHOW VARIABLES LIKE ‘log_bin’; 如果未开启&#xff0c;修改配置my.ini 开启日志 安装目录配置my.ini(mysql8在data目录) log-binmysql-bin&#xff08;开启日志并指定日志前缀&#xff…

BiRefNet C++ TensorRT (二分类图像分割)

BiRefNet C TensorRT &#xff08;二分类图像分割&#xff09; 利用TensorRT和CUDA的双边参考网络&#xff08;BiRefNet&#xff09;的高性能c实现&#xff0c;针对实时高分辨率二分类图像分割进行了优化。 BiRefNet c TENSORRT旨在有效地在GPU上运行双边参考分割任务。通过利…

【ARM】MDK在编译 i.MXRT1芯片的时候出现报错Error: L6079E

1、 文档目标 解决MDK在编译 i.MXRT1芯片的时候出现报错Error: L6079E 2、 问题场景 客户在使用NXP 的NXP i.MXRT1050的芯片进行工程构建的时候出现下面的报错信息&#xff1a; Error: L6079E: Subtool invocation error: Error executing armcc. The system could not find…

论文笔记(七十二)Reward Centering(二)

Reward Centering&#xff08;二&#xff09; 文章概括摘要2 简单的奖励中心 文章概括 引用&#xff1a; article{naik2024reward,title{Reward Centering},author{Naik, Abhishek and Wan, Yi and Tomar, Manan and Sutton, Richard S},journal{arXiv preprint arXiv:2405.0…

推荐几款较好的开源成熟框架

一. 若依&#xff1a; 1. 官方网站&#xff1a;https://doc.ruoyi.vip/ruoyi/ 2. 若依SpringBootVueElement 的后台管理系统&#xff1a;https://gitee.com/y_project/RuoYi-Vue 3. 若依SpringBootVueElement 的后台管理系统&#xff1a;https://gitee.com/y_project/RuoYi-Cl…

基于知识图谱的问答系统:后端Python+Flask,数据库Neo4j,前端Vue3(提供源码)

基于知识图谱的问答系统&#xff1a;后端PythonFlask&#xff0c;数据库Neo4j&#xff0c;前端Vue3 引言 随着人工智能技术的不断发展&#xff0c;知识图谱作为一种结构化的知识表示方式&#xff0c;逐渐成为问答系统的重要组成部分。本文将介绍如何构建一个基于知识图谱的问答…

【华为机试】HJ80 整型数组合并

解法一&#xff1a; HashSet>List列表 Collections.sort(list)对列表进行排序 import java.util.*; import java.util.HashSet;// 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main {public static void main(String[] args) {Scanner sc new Scanner(…

day17-后端Web原理——SpringBoot原理

目录 SpingBoot原理1. 配置优先级2. Bean管理2.1 获取Bean2.2 Bean作用域2.3 第三方Bean 3. SpringBoot原理3.1 起步依赖3.2 自动配置3.2.1 概述3.2.2 常见方案3.2.2.1 概述3.2.2.2 方案一3.2.2.3 方案二 3.2.3 原理分析3.2.3.1 源码跟踪3.2.3.2 Conditional 4. Web后端开发总结…

我们来学nginx -- work process

题记 工作进程能处理大量的并发请求几乎不会阻塞Worker进程可绑定到固定的核&#xff0c;避免CPU频繁地上下文切换看样子&#xff0c;还得转为人话 大白话 作为一般的应用的开发大部分人没有很强的底层技术基础如果深究&#xff0c;涉及复杂技术点&#xff0c;很容易迷惘为什…

【PLL】应用:同步

1. 用于时钟去偏移的PLL 时钟频率增加内部时钟与外部时钟的偏移&#xff0c;在芯片之间通信时很重要时钟偏移可能是由时钟树引起的&#xff0c;该时钟树缓冲外部时钟以驱动大量内部节点 芯片间通信中的时钟偏移问题 芯片1和芯片2共享外部时钟CKext芯片内部逻辑电路操作的实际时…

Go入门之数组与切片

var arr1 [...]int{1, 2, 3}fmt.Println(len(arr1)) 数组长度不能扩展 var arr2 [...]int{0: 100, 5: 101}fmt.Println(len(arr2)) } 指定索引初始化 可以通过for和range遍历 值类型:基本数据类型和数组都是值类型&#xff0c;改变副本的值不会改变本身的值 切片为引用数…