unity assetbundle + xlua 热更框架

本文详细介绍了在Unity中使用AssetBundleBrowser插件进行资源打包,包括如何处理Lua文件,设置打包流程,以及如何实现热更新。通过创建和修改脚本,实现了Lua脚本的加载和卸载,并提供了远程资源下载与热更的实现,包括检查更新、下载资源、保存到本地的过程。最后展示了热更新测试的结果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

assetbundle打包

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

打包lua文件

unity是默认不支持.lua文件类型的,所以也不能对后缀为.lua的文件打包,但是unity官方是有相关接口的。具体实现代码如下ST_LuaImporter.cs

[ScriptedImporter(1, ".lua")]
public class ST_LuaImporter : ScriptedImporter
{
    public override void OnImportAsset(AssetImportContext ctx)
    {
        var luaTxt = File.ReadAllText(ctx.assetPath);
        var assetText = new TextAsset(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()
end
function TestMono:Update()
    --self.transform:Rotate(CS.UnityEngine.Vector3(0,1,0));
    self.transform:Rotate(CS.UnityEngine.Vector3(1,0,0));
end
return TestMono

同样给TestMono脚本打上lua标签
在这里插入图片描述
开始打包
在这里插入图片描述
打包输出路径是自定的,可上传至服务器方便热更新,这个后面介绍。压缩方式随意,勾选拷贝到StreamAssets方便本地测试(如果用服务器数据,那么不勾选)。打包成功如下
在这里插入图片描述
可以看到除了lua包和model包还有一个StandloneWindows包,这个包里面包含了bundle各个模块的信息以及依赖。Md5FileList.txt为了方便后面做热更对比文件对比而生成的,打包时生成这个文件需要修改assetbundle browser源码,具体修改如下
在这里插入图片描述
assetbundle browser找到AssetBundleBuildTab文件,添加以下代码

 /// <summary>
 /// Create FileList
 /// </summary>
 void CreateFileList()
 {
     string filePath = m_UserData.m_OutputPath + "\\Md5FileList.txt";
     if (File.Exists(filePath))
     {
         File.Delete(filePath);
     }

     StreamWriter streamWriter = new StreamWriter(filePath);

     string[] files = Directory.GetFiles(m_UserData.m_OutputPath);
     for (int i = 0; i < files.Length; i++)
     {
         string tmpfilePath = files[i];
         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>
 string GetFileMD5(string filePath)
 {
     System.Security.Cryptography.MD5 MD5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
     System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
     FileStream fileStream = new FileStream(filePath, FileMode.Open);
     byte[] bytes = MD5.ComputeHash(fileStream);
     fileStream.Close();

     for (int i = 0; i < bytes.Length; i++)
     {
         stringBuilder.Append(bytes[i].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>
public T LoadResource<T>(string assetBundleName, string assetBundleGroupName) where T : UnityEngine.Object
{
    if (string.IsNullOrEmpty(assetBundleGroupName))
    {
        return default(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 as T;
    return one;
}
/// <summary>
/// 卸载资源组
/// </summary>
/// <param name="assetBundleGroupName">组名</param>
public void UnLoadResource(string assetBundleGroupName)
{
    if (DicAssetBundle.TryGetValue(assetBundleGroupName, out assetbundle))
    {
        Debug.Log(assetBundleGroupName);
        assetbundle.Unload(false);
        if (assetbundle != null)
        {
            assetbundle = null;
        }
        DicAssetBundle.Remove(assetBundleGroupName);
        Resources.UnloadUnusedAssets();
    }
}

public string GetStreamingAssetsPath()
{
    string StreamingAssetsPath =
#if UNITY_EDITOR || UNITY_STANDALONE_WIN
    Application.streamingAssetsPath + "/";
#elif UNITY_ANDROID
    "jar:file://" + Application.dataPath + "!/assets/";
#elif UNITY_IPHONE
    Application.dataPath + "/Raw/";
#else
    string.Empty;
#endif
    return StreamingAssetsPath;
}

修改Lua自定义Loader,require时使用Bundle资源
ST_LuaManger.cs

public static byte[] LuaLoader(ref string filename)
{
    TextAsset ta = ST_BundleManager.Instance.LoadResource<TextAsset>(filename,"lua");
    return System.Text.Encoding.UTF8.GetBytes(ta.text);
}

//移除requier(lua环境只会Require 文件一次,移除后会从新require)
//require 会调用自定义Loader
public void RemoveRequire(string name)
{
    MyLuaEnv.DoString($@"
    for key, _ in pairs(package.preload) do
        if string.find(tostring(key),'{name}') == 1 then
            package.preload[key] = nil
        end
    end
    for key, _ in pairs(package.loaded) do
        if string.find(tostring(key), '{name}') == 1 then
            package.loaded[key] = nil
        end
    end");
}

测试脚本

if (GUILayout.Button("Cube"))
{
    GameObject cube = ST_BundleManager.Instance.LoadResource<GameObject>("Cube", "model");
    Instantiate(cube,new Vector3(0,y++,0),Quaternion.identity);
}

测试结果
请添加图片描述
可以看到资源被正确加载了。

远程资源下载与热更

第一次使用服务器数据的时候要清空本地StreamAsset文件夹。热更新的思路是对比本地的Md5FileList.txt,和服务器上的,如果有md5不同的包,或者新的包那么就请求下载,并保存到本地(也可以用先对比版本号的方法。可以将打包的资源放到了[GitCode]方便下载(https://about.gitcode.net/)
在这里插入图片描述
右键点击这个按钮即可复制原始下载链接,亲测可用
ST_AssetBundleManager.cs 完整代码

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;


public static class BundleConfig{
    public static readonly string AssetBundlePath = "https://。。。。";
    public static readonly string Md5FileName = "Md5FileList.txt";
    public static readonly string OnFireUpdate = "OnFireUpdate";
    public static readonly string OnStarDownLoad = "OnStarDownLoad";
    public static readonly string OnStarSave = "OnStarSave";
    public static readonly string OnUpdateEnd = "OnUpdateEnd";

}

public class ST_BundleManager:MonoBehaviour
{
    static bool _isDesdroy = false;
    private static readonly object padlock = new object();
    private static ST_BundleManager instance;
    static AssetBundle assetbundle = null;
    static Dictionary<string, AssetBundle> DicAssetBundle;
    
    public static ST_BundleManager Instance
    {
        get
        {
            if (_isDesdroy)
            {
                return null;
            }
            if (instance == null)
            {
                lock (padlock)
                {
                    if (instance == null)
                    {
                        GameObject go = new GameObject("AssetBundleManager");
                        DontDestroyOnLoad(go);
                        instance = go.AddComponent<ST_BundleManager>();
                        DicAssetBundle = new Dictionary<string, AssetBundle>();
                    }
                }
            }
            return instance;
        }
    }
    
    /// <summary>
    /// bundel资源加载
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="assetBundleName">资源名</param>
    /// <param name="assetBundleGroupName">资源组</param>
    /// <returns></returns>
    public T LoadResource<T>(string assetBundleName, string assetBundleGroupName) where T : UnityEngine.Object
    {
        if (string.IsNullOrEmpty(assetBundleGroupName))
        {
            return default(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 as T;
        return one;
    }

    /// <summary>
    /// 资源卸载
    /// </summary>
    /// <param name="assetBundleGroupName">资源名</param>
    public void UnLoadResource(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>
    public string GetStreamingAssetsPath()
    {
        string StreamingAssetsPath =
#if UNITY_EDITOR || UNITY_STANDALONE_WIN
        Application.streamingAssetsPath + "/";
#elif UNITY_ANDROID
        "jar:file://" + Application.dataPath + "!/assets/";
#elif UNITY_IPHONE
        Application.dataPath + "/Raw/";
#else
        string.Empty;
#endif
        return StreamingAssetsPath;
    }

    //下载跟新资源
    public IEnumerator DownloadAssetBundles(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[i]);
            //触发下载事件
            ST_EventManager.PostEvent(BundleConfig.OnStarDownLoad, downLoads[i]);
            yield return www.SendWebRequest();
            if(www.result != UnityWebRequest.Result.ConnectionError)
            {
                //触发保存事件
                ST_EventManager.PostEvent(BundleConfig.OnStarSave, downLoads[i]);
                SaveAssetBundle(downLoads[i],www.downloadHandler.data);
            }
            else
            {
                throw new Exception(www.error);
            }
            i++;
        }
        //触发跟新完事件
        ST_EventManager.PostEvent(BundleConfig.OnUpdateEnd);
    }

    /// <summary>
    /// 检查跟新
    /// </summary>
    /// <param name="callback">回调函数</param>
    /// <returns></returns>
    public IEnumerator CheckUpdate(Action<bool,UnityWebRequest> callback)
    {
        UnityWebRequest www = UnityWebRequest.Get(BundleConfig.AssetBundlePath + BundleConfig.Md5FileName);
        yield return www.SendWebRequest();
        if (www.result != UnityWebRequest.Result.ConnectionError)
        {
            List<string> downLoads = GetDownloadMoudle(www.downloadHandler.text);
            callback(downLoads.Count > 0, www);
        }
        else
        {
            throw new Exception(www.error);
        }
    }

    /// <summary>
    /// 根据Md5FileList获取需要跟新的模块
    /// </summary>
    /// <param name="webFileList"></param>
    /// <returns></returns>
    List<string> GetDownloadMoudle(string webFileList)
    {
        List<string> ret = new List<string>();
        string[] sArray = webFileList.Split(new char[2] { '\r', '\n' });
        //AssetBundleManifest assetBundleManifest;
        if(!File.Exists(GetStreamingAssetsPath() + BundleConfig.Md5FileName))
        {
            foreach(string str in sArray)
            {
                if (str != "")
                {
                    ret.Add(str.Split('|')[0]);
                }
            }
        }
        else
        {
            string pastMd5 = File.ReadAllText(GetStreamingAssetsPath() + BundleConfig.Md5FileName);
            Dictionary<string, string> dict = new Dictionary<string, string>();
            string[] psArray = pastMd5.Split(new char[2] { '\r', '\n' });
            foreach (string str in psArray)
            {
                if (str != "")
                {
                    var nv = str.Split('|');
                    dict.Add(nv[0], nv[1]);
                }
            }
            foreach (string str in sArray)
            {
                if (str != "")
                {
                    var nv = str.Split('|');
                    if (!dict.ContainsKey(nv[0]) || dict[nv[0]] != nv[1])
                    {
                        ret.Add(nv[0]);
                    }
                }
            }
        }
        return ret;
    }
    
    /// <summary>
    /// 保存资源到本地
    /// </summary>
    /// <param name="fileName">资源名</param>
    /// <param name="bytes">数据</param>
    /// <param name="saveLocalComplate">完成回调</param>
    void SaveAssetBundle(string fileName, byte[] bytes, Action saveLocalComplate = null)
    {
        string path = GetStreamingAssetsPath() + fileName;
        FileInfo fileInfo = new FileInfo(path);
        FileStream fs = fileInfo.Create();

        fs.Write(bytes, 0, bytes.Length);
        fs.Flush();
        fs.Close();
        fs.Dispose();

        if (saveLocalComplate != null)
        {
            saveLocalComplate();
        }
    }
    
    private void OnDestroy()
    {
        _isDesdroy = true;
    }
}

测试热更

int y = 0;
void Start()
{
    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");
    });
}
private void OnGUI()
{
    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,new Vector3(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
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值