KaaPexei 发表于 2023-4-8 20:58

unity assetbundle + xlua 热更框架

assetbundle打包

打包使用assetbundle browser 插件即可,具体打包流程可参考Unity3d的AssetBundle打包,这个工具的好处在于方便打包,能更好的解决资源依赖复用的问题。

打包lua文件

unity是默认不支持.lua文件类型的,所以也不能对后缀为.lua的文件打包,但是unity官方是有相关接口的。具体实现代码如下ST_LuaImporter.cs
publicclassST_LuaImporter:ScriptedImporter{publicoverridevoidOnImportAsset(AssetImportContext ctx){var luaTxt = File.ReadAllText(ctx.assetPath);var assetText =newTextAsset(luaTxt);
      ctx.AddObjectToAsset("main obj", assetText);//将对象assetText作为导入操作的主要对象。
      ctx.SetMainObject(assetText);}}将此代码放到Editor目录下即可正常打包lua文件。

开始打包

创建一个Cube预制体并给他绑定一个Lua脚本,设置他的bundle标签model


至于Cube对象是如何绑定TestMono脚本的,可以参见文章Unity + XLua 简单框架搭建的详细过程,这里不做过多描述
TestMono.lua文件内容如下
local TestMono =class()-- 自定义数据-- 构造函数function TestMono:Ctor(params)end-- 开始函数function TestMono:Start()endfunction TestMono:Update()--self.transform:Rotate(CS.UnityEngine.Vector3(0,1,0));
    self.transform:Rotate(CS.UnityEngine.Vector3(1,0,0));endreturn TestMono同样给TestMono脚本打上lua标签


开始打包


打包输出路径是自定的,可上传至服务器方便热更新,这个后面介绍。压缩方式随意,勾选拷贝到StreamAssets方便本地测试(如果用服务器数据,那么不勾选)。打包成功如下


可以看到除了lua包和model包还有一个StandloneWindows包,这个包里面包含了bundle各个模块的信息以及依赖。Md5FileList.txt为了方便后面做热更对比文件对比而生成的,打包时生成这个文件需要修改assetbundle browser源码,具体修改如下


在assetbundle browser找到AssetBundleBuildTab文件,添加以下代码
/// <summary>/// Create FileList/// </summary>voidCreateFileList(){string filePath = m_UserData.m_OutputPath +"\\Md5FileList.txt";if(File.Exists(filePath)){
         File.Delete(filePath);}StreamWriter streamWriter =newStreamWriter(filePath);string[] files = Directory.GetFiles(m_UserData.m_OutputPath);for(int i =0; i < files.Length; i++){string tmpfilePath = files;if(tmpfilePath.Equals(filePath)|| tmpfilePath.EndsWith(".manifest"))continue;
         Debug.Log(tmpfilePath);
         tmpfilePath.Replace("\\","/");
         streamWriter.WriteLine(tmpfilePath.Substring(m_UserData.m_OutputPath.Length +1)+"|"+GetFileMD5(tmpfilePath));}
   streamWriter.Close();
   streamWriter.Dispose();}/// <summary>/// 获取文件的MD5/// </summary>stringGetFileMD5(string filePath){System.Security.Cryptography.MD5 MD5 =newSystem.Security.Cryptography.MD5CryptoServiceProvider();System.Text.StringBuilder stringBuilder =newSystem.Text.StringBuilder();FileStream fileStream =newFileStream(filePath, FileMode.Open);byte[] bytes = MD5.ComputeHash(fileStream);
   fileStream.Close();for(int i =0; i < bytes.Length; i++){
         stringBuilder.Append(bytes.ToString("x2"));}return stringBuilder.ToString();}
在Build 事件里加上函数即可。这样就可以在打包的同时生成包体MD5,Md5FileList.txt内容如下,格式是包名加生成的16进制哈希码。



加载本地Bundle资源测试

/// <summary>/// 加载资源/// </summary>/// <typeparam name="T">资源类型</typeparam>/// <param name="assetBundleName">资源名</param>/// <param name="assetBundleGroupName">组名</param>/// <returns></returns>publicTLoadResource<T>(string assetBundleName,string assetBundleGroupName)whereT:UnityEngine.Object{if(string.IsNullOrEmpty(assetBundleGroupName)){returndefault(T);}if(!DicAssetBundle.TryGetValue(assetBundleGroupName,out assetbundle)){
      assetbundle = AssetBundle.LoadFromFile(GetStreamingAssetsPath()+ assetBundleGroupName);
      DicAssetBundle.Add(assetBundleGroupName, assetbundle);}object obj = assetbundle.LoadAsset(assetBundleName,typeof(T));var one = obj asT;return one;}/// <summary>/// 卸载资源组/// </summary>/// <param name="assetBundleGroupName">组名</param>publicvoidUnLoadResource(string assetBundleGroupName){if(DicAssetBundle.TryGetValue(assetBundleGroupName,out assetbundle)){
      Debug.Log(assetBundleGroupName);
      assetbundle.Unload(false);if(assetbundle !=null){
            assetbundle =null;}
      DicAssetBundle.Remove(assetBundleGroupName);
      Resources.UnloadUnusedAssets();}}publicstringGetStreamingAssetsPath(){string StreamingAssetsPath =#if UNITY_EDITOR || UNITY_STANDALONE_WIN
    Application.streamingAssetsPath +"/";#elif UNITY_ANDROID"jar:file://"+ Application.dataPath +"!/assets/";#elif UNITY_IPHONE
    Application.dataPath +"/Raw/";#elsestring.Empty;#endifreturn StreamingAssetsPath;}修改Lua自定义Loader,require时使用Bundle资源
ST_LuaManger.cs
publicstaticbyte[]LuaLoader(refstring filename){TextAsset ta = ST_BundleManager.Instance.LoadResource<TextAsset>(filename,"lua");return System.Text.Encoding.UTF8.GetBytes(ta.text);}//移除requier(lua环境只会Require 文件一次,移除后会从新require)//require 会调用自定义LoaderpublicvoidRemoveRequire(string name){
    MyLuaEnv.DoString($@"
    for key, _ in pairs(package.preload) do
      if string.find(tostring(key),'{name}') == 1 then
            package.preload = nil
      end
    end
    for key, _ in pairs(package.loaded) do
      if string.find(tostring(key), '{name}') == 1 then
            package.loaded = nil
      end
    end");}测试脚本
if(GUILayout.Button("Cube")){GameObject cube = ST_BundleManager.Instance.LoadResource<GameObject>("Cube","model");Instantiate(cube,newVector3(0,y++,0),Quaternion.identity);}测试结果


可以看到资源被正确加载了。

远程资源下载与热更

第一次使用服务器数据的时候要清空本地StreamAsset文件夹。热更新的思路是对比本地的Md5FileList.txt,和服务器上的,如果有md5不同的包,或者新的包那么就请求下载,并保存到本地(也可以用先对比版本号的方法。可以将打包的资源放到了方便下载(https://about.gitcode.net/)


右键点击这个按钮即可复制原始下载链接,亲测可用
ST_AssetBundleManager.cs 完整代码
usingSystem;usingSystem.Collections;usingSystem.Collections.Generic;usingSystem.IO;usingSystem.Threading.Tasks;usingUnityEditor;usingUnityEngine;usingUnityEngine.Networking;publicstaticclassBundleConfig{publicstaticreadonlystring AssetBundlePath ="https://。。。。";publicstaticreadonlystring Md5FileName ="Md5FileList.txt";publicstaticreadonlystring OnFireUpdate ="OnFireUpdate";publicstaticreadonlystring OnStarDownLoad ="OnStarDownLoad";publicstaticreadonlystring OnStarSave ="OnStarSave";publicstaticreadonlystring OnUpdateEnd ="OnUpdateEnd";}publicclassST_BundleManager:MonoBehaviour{staticbool _isDesdroy =false;privatestaticreadonlyobject padlock =newobject();privatestaticST_BundleManager instance;staticAssetBundle assetbundle =null;staticDictionary<string, AssetBundle> DicAssetBundle;publicstaticST_BundleManager Instance
    {get{if(_isDesdroy){returnnull;}if(instance ==null){lock(padlock){if(instance ==null){GameObject go =newGameObject("AssetBundleManager");DontDestroyOnLoad(go);
                        instance = go.AddComponent<ST_BundleManager>();
                        DicAssetBundle =newDictionary<string, AssetBundle>();}}}return instance;}}/// <summary>/// bundel资源加载/// </summary>/// <typeparam name="T">资源类型</typeparam>/// <param name="assetBundleName">资源名</param>/// <param name="assetBundleGroupName">资源组</param>/// <returns></returns>publicTLoadResource<T>(string assetBundleName,string assetBundleGroupName)whereT:UnityEngine.Object{if(string.IsNullOrEmpty(assetBundleGroupName)){returndefault(T);}if(!DicAssetBundle.TryGetValue(assetBundleGroupName,out assetbundle)){
            assetbundle = AssetBundle.LoadFromFile(GetStreamingAssetsPath()+ assetBundleGroupName);
            DicAssetBundle.Add(assetBundleGroupName, assetbundle);}object obj = assetbundle.LoadAsset(assetBundleName,typeof(T));var one = obj asT;return one;}/// <summary>/// 资源卸载/// </summary>/// <param name="assetBundleGroupName">资源名</param>publicvoidUnLoadResource(string assetBundleGroupName){if(DicAssetBundle.TryGetValue(assetBundleGroupName,out assetbundle)){
            assetbundle.Unload(false);if(assetbundle !=null){
                assetbundle =null;}
            DicAssetBundle.Remove(assetBundleGroupName);
            Resources.UnloadUnusedAssets();}}/// <summary>/// 获取本地资源路径/// </summary>/// <returns></returns>publicstringGetStreamingAssetsPath(){string StreamingAssetsPath =#if UNITY_EDITOR || UNITY_STANDALONE_WIN
      Application.streamingAssetsPath +"/";#elif UNITY_ANDROID"jar:file://"+ Application.dataPath +"!/assets/";#elif UNITY_IPHONE
      Application.dataPath +"/Raw/";#elsestring.Empty;#endifreturn StreamingAssetsPath;}//下载跟新资源publicIEnumeratorDownloadAssetBundles(UnityWebRequest www){List<string> downLoads =GetDownloadMoudle(www.downloadHandler.text);SaveAssetBundle(BundleConfig.Md5FileName,www.downloadHandler.data);int i =0;if(downLoads.Count >0){//触发热更事件
            ST_EventManager.PostEvent(BundleConfig.OnFireUpdate, downLoads.Count);}while(i < downLoads.Count){
            www = UnityWebRequest.Get(BundleConfig.AssetBundlePath + downLoads);//触发下载事件
            ST_EventManager.PostEvent(BundleConfig.OnStarDownLoad, downLoads);yieldreturn www.SendWebRequest();if(www.result != UnityWebRequest.Result.ConnectionError){//触发保存事件
                ST_EventManager.PostEvent(BundleConfig.OnStarSave, downLoads);SaveAssetBundle(downLoads,www.downloadHandler.data);}else{thrownewException(www.error);}
            i++;}//触发跟新完事件
      ST_EventManager.PostEvent(BundleConfig.OnUpdateEnd);}/// <summary>/// 检查跟新/// </summary>/// <param name="callback">回调函数</param>/// <returns></returns>publicIEnumeratorCheckUpdate(Action<bool,UnityWebRequest> callback){UnityWebRequest www = UnityWebRequest.Get(BundleConfig.AssetBundlePath + BundleConfig.Md5FileName);yieldreturn www.SendWebRequest();if(www.result != UnityWebRequest.Result.ConnectionError){List<string> downLoads =GetDownloadMoudle(www.downloadHandler.text);callback(downLoads.Count >0, www);}else{thrownewException(www.error);}}/// <summary>/// 根据Md5FileList获取需要跟新的模块/// </summary>/// <param name="webFileList"></param>/// <returns></returns>List<string>GetDownloadMoudle(string webFileList){List<string> ret =newList<string>();string[] sArray = webFileList.Split(newchar{'\r','\n'});//AssetBundleManifest assetBundleManifest;if(!File.Exists(GetStreamingAssetsPath()+ BundleConfig.Md5FileName)){foreach(string str in sArray){if(str !=""){
                  ret.Add(str.Split('|'));}}}else{string pastMd5 = File.ReadAllText(GetStreamingAssetsPath()+ BundleConfig.Md5FileName);Dictionary<string,string> dict =newDictionary<string,string>();string[] psArray = pastMd5.Split(newchar{'\r','\n'});foreach(string str in psArray){if(str !=""){var nv = str.Split('|');
                  dict.Add(nv, nv);}}foreach(string str in sArray){if(str !=""){var nv = str.Split('|');if(!dict.ContainsKey(nv)|| dict]!= nv){
                        ret.Add(nv);}}}}return ret;}/// <summary>/// 保存资源到本地/// </summary>/// <param name="fileName">资源名</param>/// <param name="bytes">数据</param>/// <param name="saveLocalComplate">完成回调</param>voidSaveAssetBundle(string fileName,byte[] bytes,Action saveLocalComplate =null){string path =GetStreamingAssetsPath()+ fileName;FileInfo fileInfo =newFileInfo(path);FileStream fs = fileInfo.Create();

      fs.Write(bytes,0, bytes.Length);
      fs.Flush();
      fs.Close();
      fs.Dispose();if(saveLocalComplate !=null){saveLocalComplate();}}privatevoidOnDestroy(){
      _isDesdroy =true;}}
测试热更

int y =0;voidStart(){
    ST_EventManager.RegisterEvent<int>(BundleConfig.OnFireUpdate,(e)=>{
      Debug.Log("开始热跟新,资源数:"+ e);});
    ST_EventManager.RegisterEvent<string>(BundleConfig.OnStarDownLoad,(e)=>{
      Debug.Log("开始下载资源:"+ e);});
    ST_EventManager.RegisterEvent<string>(BundleConfig.OnStarSave,(e)=>{
      Debug.Log("开始保存资源:"+ e);});
    ST_EventManager.RegisterEvent(BundleConfig.OnUpdateEnd,()=>{
      Debug.Log("更新完成");//取消TestMono的Requrie,卸载lua,保证使用新的代码
      ST_LuaMannager.Instance.RemoveRequire("TestMono");
      ST_BundleManager.Instance.UnLoadResource("lua");});}privatevoidOnGUI(){if(GUILayout.Button("开始下载")){StartCoroutine(ST_BundleManager.Instance.CheckUpdate((needUp, www)=>{if(needUp){
                Debug.Log("检测到资源更新");StartCoroutine(ST_BundleManager.Instance.DownloadAssetBundles(www));}else{
                Debug.Log("无更新");}}));}if(GUILayout.Button("Cube")){GameObject cube = ST_BundleManager.Instance.LoadResource<GameObject>("Cube","model");Instantiate(cube,newVector3(0,y++,0),Quaternion.identity);
      ST_LuaMannager.Instance.RemoveRequire("TestMono");}}
点击开始下载,输出如下:


点击cube正常加载:


修改TestMono的Update函数,原来Cube是沿着X轴旋转,现在让他沿着Y周旋转,从新打包并上传到服务器
function TestMono:Update()
    self.transform:Rotate(CS.UnityEngine.Vector3(0,1,0));--self.transform:Rotate(CS.UnityEngine.Vector3(1,0,0));end
可以看到只跟新了lua和standlone,并且跟新后创建的Cube是按Y轴旋转的,热更大功告成,,写完博客11点半了,睡觉睡觉。。。
页: [1]
查看完整版本: unity assetbundle + xlua 热更框架