Huatuo热更新--如何使用

在安装完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-UIPartLoadDll("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_EDITORStartCoroutine(OnLoadDll(dllName, callBack));
#elsevar 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){//加载dllTextAsset 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_EDITORStartCoroutine(OnLoadScene(sceneName, callBack));
#elseStartCoroutine(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_EDITORstring[] paths = prefabPath.Split('/');string prefabName = paths[paths.Length - 1];StartCoroutine(OnLoadPrefab(prefabName, callBack));
#elseprefabPath += ".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){//初始化bundlesif (!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) =>{//修改bundlesif (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}";elsefield.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.rhkb.cn/news/20971.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DeepSeek 和 ChatGPT 在特定任务中的表现:逻辑推理与创意生成

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;Linux网络编程 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 ​ Linux网络编程笔记&#xff1a; https://blog.cs…

车载音频配置(二)

目录 OEM 自定义的车载音频上下文 动态音频区配置 向前兼容性 Android 14 车载音频配置 在 Android 14 中,AAOS 引入了 OEM 插件服务,使你可以更主动地管理由车载音频服务监督的音频行为。 随着新的插件服务的引入,车载音频配置文件中添加了以下更改: • OEM 自定义的车…

【SQL】SQL多表查询

多表查询案例联系点击此处 &#x1f384;概念 一般我们说的多表查询都涉及外键和父子表之间的关系。比如一对多:一般前面指的是父表后面指的是子表。 ⭐分类 一对多(多对一) 多对多 一对一 ⭐一对多 &#x1f4e2;案例&#xff1a;部门与员工的关系 &#x1f4e2;关系&…

存储区域网络(SAN)管理

存储区域网络&#xff08;Storage Area Network&#xff0c;SAN&#xff09;采用网状通道&#xff08;Fibre Channel &#xff0c;简称FC&#xff09;技术&#xff0c;通过FC交换机连接存储阵列和服务器主机&#xff0c;建立专用于数据存储的区域网络。SAN提供了一种与现有LAN连…

导出指定文件夹下的文件结构 工具模块-Python

python模块代码 import os import json import xml.etree.ElementTree as ET from typing import List, Optional, Dict, Union from pathlib import Path class DirectoryTreeExporter:def __init__(self,root_path: str,output_file: str,fmt: str txt,show_root: boo…

PyCharm Terminal 自动切换至虚拟环境

PyCharm 虚拟环境配置完毕后&#xff0c;打开终端&#xff0c;没有跟随虚拟环境切换&#xff0c;如图所示&#xff1a; 此时&#xff0c;需要手动将终端切换为 Command Prompt 模式 于是&#xff0c;自动切换至虚拟环境 每次手动切换&#xff0c;比较麻烦&#xff0c;可以单…

Vue 实现通过URL浏览器本地下载 PDF 和 图片

1、代码实现如下&#xff1a; 根据自己场景判断 PDF 和 图片&#xff0c;下载功能可按下面代码逻辑执行 const downloadFile async (item: any) > {try {let blobUrl: any;// PDF本地下载if (item.format pdf) {const response await fetch(item.url); // URL传递进入i…

【前端】使用WebStorm创建第一个项目

文章目录 前言一、步骤1、启动2、创建项目3、配置Node.js4、运行项目 二、Node.js介绍 前言 根据前面文章中记录的步骤&#xff0c;已经安装好了WebStorm开发软件&#xff0c;接下来我们就用这个IDE开发软件创建第一个项目。 一、步骤 1、启动 启动软件。 2、创建项目 新建…

遥感与GIS在滑坡、泥石流风险普查中的实践技术应用

原文>>> 遥感与GIS在滑坡、泥石流风险普查中的实践技术应用 我国是地质灾害多发国家&#xff0c;地质灾害的发生无论是对于地质环境还是人类生命财产的安全都会带来较大的威胁&#xff0c;因此需要开展地质灾害风险普查。利用遥感&#xff08;RS&#xff09;技术进行地…

EasyExcel 自定义头信息导出

需求&#xff1a;需要在导出 excel时&#xff0c;合并单元格自定义头信息(动态生成)&#xff0c;然后才是字段列表头即导出数据。 EasyExcel - 使用table去写入&#xff1a;https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write#%E4%BD%BF%E7%94%A8table%E…

QT异步编程之QMetaObject::invokeMethod

一、概述 1、QMetaObject::invokeMethod是Qt的一个功能强大的方法&#xff0c;它用于动态地调用一个对象地槽函数或成员函数。 2、这个方法允许你在运行时通过对象地元对象系统调用函数&#xff0c;而无需直接使用函数指针或其它静态机制。 3、元对象系统是一个基于C的扩展…

斐波那契数列模型:在动态规划的丝绸之路上追寻斐波那契的足迹(上)

文章目录 引言递归与动态规划的对比递归解法的初探动态规划的优雅与高效自顶向下的记忆化搜索自底向上的迭代法 性能分析与比较小结 引言 斐波那契数列&#xff0c;这一数列如同一条无形的丝线&#xff0c;穿越千年时光&#xff0c;悄然延续其魅力。其定义简单而优美&#xff…

php 系统命令执行及绕过

文章目录 php的基础概念php的基础语法1. PHP 基本语法结构2. PHP 变量3.输出数据4.数组5.超全局变量6.文件操作 php的命令执行可以执行命令的函数命令执行绕过利用代码中命令&#xff08;如ls&#xff09;执行命令替换过滤过滤特定字符串神技&#xff1a;利用base64编码解码的绕…

使用vscode调试transformers源码

简要介绍如何使用vscode调试transformers源码 以源码的方式安装transformers&#xff08;官方手册为Editable install&#xff09; 优先参考官方手册 git clone https://github.com/huggingface/transformers.git cd transformers pip install -e .以下展示transformers/exa…

macos安装jmeter测试软件

java环境安装 a. 验证安装环境 java -version # 如果有版本信息&#xff0c;说明已安装 b. 安装jdk # 安装 Homebrew&#xff08;如未安装&#xff09; /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # 安装 O…

2023年全国职业院校技能大赛GZ073网络系统管理赛项赛题第10套模块A:网络构建

​有问题请留言或主页私信咨询 2023年全国职业院校技能大赛 GZ073网络系统管理赛项 赛题第10套 模块A&#xff1a;网络构建 ​ ​ **目 **录 任务清单 &#xff08;一&#xff09;基础配置 &#xff08;二&#xff09;有线网络配置 &#xff08;三&#xff09;无线…

若依-@Excel新增注解numberFormat

Excel注解中原本的scale会四舍五入小数&#xff0c;导致进度丢失 想要的效果 显示的时候保留两个小数真正的数值是保留之前的数值 还原过程 若以中有一個專門的工具类&#xff0c;用来处理excel的 找到EXCEL导出方法exportExcel()找到writeSheet,写表格的方法找到填充数据的方法…

动静态链接与加载

目录 静态链接 ELF加载与进程地址空间&#xff08;静态链接&#xff09; 动态链接与动态库加载 GOT表 静态链接 对于多个.o文件在没有链接之前互相是不知到对方存在的&#xff0c;也就是说这个.o文件中调用函数的的跳转地址都会被设定为0&#xff08;当然这个函数是在其他.…

python-leetcode 33.排序链表

题目&#xff1a; 给定链表的头结点head,请将其按升序排列&#xff0c;并返回排序后的链表 方法一&#xff1a;自顶向下归并排序 链表自顶向下归并排序的过程&#xff1a; 1.找到链表的中点&#xff0c;以中点为分界&#xff0c;将链表拆分成两个子链表。寻找链表的中点可以…

PyQt加载UI文件

1.动态加载 import sys from PySide6 import QtCore,QtWidgets from PySide6.QtWidgets import * from PySide6.QtUiTools import QUiLoaderclass readfile(QWidget):def __init__(self):super().__init__()self.uiQUiLoader().load("test.ui",self) self.__c…