在Unity中使用 AssetBundles 与 xLua 热更新的入门实验
本文最后更新于24 天前,其中的信息可能已经过时,如有错误请发送邮件到1077511618@qq.com

在Unity中使用 AssetBundles 与 xLua 热更新的入门探索

前言

现在正是中秋佳节,天气炎热,是学习热更新的完美时机!在先前学习了lua的语法后,是时候正式打开热更新的大门了!于是我写下了此文章,记录知识点、个人学习道路,并希望能通过此文初步给自己和读者直观展示出Unity中不同热更新方案的使用。在此次入门探索中我们将一步步实现一个简单的小案例,希望你我都能有所收获~

示例项目目标

在本次示例项目中,我们将实现一个游戏主菜单界面的基础功能及更新流程,具体要求如下:

开始之前,先讲讲AssetBundles

AssetBundles(也就是我们有时候听到的AB包)是Unity官方提供的一个热更新方案,虽然现在官方已经有了新的框架Addressables,但是AssetBundles能让开发者自定义一套热更新架构,在众多企业新老项目中仍会使用到,所以AssetBundles仍然是Unity客户端开发工程师必须掌握的,下面我们就来讲讲AssetBundles中你需要知道的一些小知识(这里仅仅作为简单参考概览,有关于AB包更详细的信息请参阅官方文档或其他文献):

AssetBundles 参数相关

Build Target:目标平台;
Output Path:目标输出路径;
Clear Folders:是否清空文件夹重新打包;
Copy To StreamingAssets:是否拷贝到StreamingAssets;
Compression:
NoCompression:不压缩,解压快,但是当包比较大的时候不推荐;
LZMA:压缩最小,解压慢;缺点:用一个资源时需要解压所有资源;
LZ4:压缩相对于LZMA大一些,用什么就解压什么,内存占用底;
ET1:在资源包中不包含资源的类型信息:
FR:重新打包时需要重新构建包,同ClearFolders不同,其不会删除不存在的包;
ITIC:增量结构检查时忽略类型的修改;
Append Hash:将文件哈希值附加到资源包名上;
SM:严格模式:如果打包时报错了,则打开包裹是直接无效的;
DRB:运行时构建。

Unity 提供的 AssetBundles 相关API使用

加载、使用、卸载AB包资源需要我们进行脚本的编写来控制相关流程,以下是示例(留意代码中的注释哦):

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

public class AssetBundlesTest : MonoBehaviour
{
    void Awake()
    {
        // 1.加载AB包
        AssetBundle playerModel_AB = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "playermodel");
        // 2.加载AB包中的资源
        // playerModel_AB.LoadAsset(""); // 只使用名字加载会出现同名不同类型分不清的情况

        // 因此建议使用泛型加载 或者 是Type指定类型
        GameObject obj = playerModel_AB.LoadAsset<GameObject>("Player_Cube");
        // or: GameObject obj = playerModel_AB.LoadAsset("Cube" , typeof(GameObject)) as GameObject; // 这种方式需要掌握,因为在lua中只能使用type来实现泛型

        // AB包不能够重复加载,否则会报错!

        // 加载完后进行实例化
        Instantiate(obj);
    }

    #region 异步加载AB包资源
    IEnumerator LoadABAsync(string ABName , string resName)
    {
        // 异步加载AB包
        AssetBundleCreateRequest abcr = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/" + ABName);
        yield return abcr;
        // 异步加载资源
        AssetBundleRequest abq = abcr.assetBundle.LoadAssetAsync(resName , typeof(GameObject));
        yield return abq;
        Instantiate(abq.asset as GameObject);
    }
    #endregion

    #region 卸载AB包
    void DeleteAB(AssetBundle ab_toUnload)
    {
        // 卸载所有加载的AB包,参数为 false 则不干扰场景上已经加载的资源
        // 而若参数为 true 则会将场景上已经加载的资源也一同进行删除
        AssetBundle.UnloadAllAssetBundles(false);

        // 卸载指定AB包(参数效果同上)
        ab_toUnload.Unload(false);
    }
    #endregion
}

AB 包依赖

关于AB包的依赖————一个资源身上用到了别的AB包中的资源,这个时候,如果我们只加载自己的AB包,通过它创建对象,会出现资源丢失的情况。
这种时候,我们需要把依赖包一起进行加载才能正常运行。
但是如果一个资源有许多依赖包需要加载(用到了多个不同AB包中的资源),手动编写单个依赖包加载的话非常麻烦且容易少加载依赖包,这时候就引出了“主包”的概念:
这里直接给出示例:

// 利用主包 获取依赖信息
// 加载主包
AssetBundle AB_main = AssetBundle.LoadFromFile(Application.streamingAssetPath + "/" + "PC");
// 加载主包中的固定文件
AssetBundleManifest AB_manifest = AB_main.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 从固定文件中得到依赖信息
string[] strs = AB_manifest.GetAllDependencies("playermodel");
// 通过所得依赖包名字加载依赖包
List<AssetBundle> AB_denpendency;
for (int i = 0 ; i < strs.Length ; i++)
{
    AB_denpendency.Add(AssetBundle.LoadFromFile(Application.streamingAssetPath + "/" + strs[i])); 
}

正式开始

因此文章 & 项目在假期抽空搓,南京的天气让笔者鼻炎又又又犯了,所以可能会省略一些细节或步骤过程orz

准备HFS本地服务器

我希望尝试从远程服务器上加载资源内容而非上面所述的从本地路径中进行加载,于是我了解到了HFS————一个轻量的本地HTTP文件服务器软件,通过HFS我可以在此次实验中模拟线上下载流程、验证热更逻辑。
HFS下载链接:https://github.com/rejetto/hfs/releases
下载完成后打开HFS.exe,就会看到跳转出网页面板,我们先放着不动,稍后我们会将打包好的AB包资源文件放到HFS中。

打开HFS.exe

HFS网页面板

搭建UI界面、准备资源并打包AB包

搭建好游戏UI界面如图所示:

搭建好的游戏UI界面

在unity中准备好资源,我这里有几个ScriptableObject、人物角色预制体、背景图片sprite、lua脚本,随后在AssetBundle的面板中添加好文件夹并分类拖入我们需要打包的资源:
准备好资源并配置

编写AB包管理器脚本代码

代码示例(是自己头脑发热写出来的史山):

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using XLua;
using XLua.LuaDLL;

[CSharpCallLua]
    public delegate void BtnFunc(float maxHealth , float attack , float actSpeed , float critRate , float critDamage , float defence);

public class AssetBundleManager : MonoBehaviour
{
    #region 单例实现
    private static AssetBundleManager _instance;
    public static AssetBundleManager Instance
    {
        get
        {
            _instance = FindObjectOfType<AssetBundleManager>();
            if (_instance == null)
            {
                if (GameObject.Find("AssetBundleManager") == null)
                {
                    GameObject go = new GameObject("AssetBundleManager");
                    _instance = go.AddComponent<AssetBundleManager>();
                    DontDestroyOnLoad(go);
                }
            }
            return _instance;
        }
    }
    #endregion

    #region 成员
    public string dataSO_url;
    public string characterPrefabPack_url;
    public string uiSprite_url;
    public string luaScript_url;
    public UIDataScriptObject uIDataScriptObject;
    public DataScriptableObject dataScriptableObject;
    public TextAsset menuUILuaScript;
    public static event Action OnAssetBundleLoaded;

    private LuaEnv luaEnv = new LuaEnv();
    public BtnFunc btnFunc;
    #endregion

    #region 加载AB包
    IEnumerator LoadAssetBundle(string dataSO_url , string characterPrefabPack_url , string uiSprite_url , string luaScript_url)
    {
        // 从HFS中加载AB包
        #region 加载Sprite资源包
        UnityWebRequest webRequest_sprite = UnityWebRequestAssetBundle.GetAssetBundle(uiSprite_url);
        yield return webRequest_sprite.SendWebRequest();

        if (webRequest_sprite.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("UISprite AB包下载失败:" + webRequest_sprite.error);
            yield break;
        }

        // 从请求中取出 AssetBundle
        AssetBundle AssetBundle_UISprite = DownloadHandlerAssetBundle.GetContent(webRequest_sprite);

        // 加载 Sprite 资源
        AssetBundleRequest uiSpriteRequest = AssetBundle_UISprite.LoadAllAssetsAsync(typeof(Sprite));
        yield return uiSpriteRequest;
        #endregion

        #region 加载dataSO资源包
        // 发出请求,加载dataSO资源包
        UnityWebRequest webRequest = UnityWebRequestAssetBundle.GetAssetBundle(dataSO_url);
        yield return webRequest.SendWebRequest();

        if (webRequest.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("DataScriptableObject AB包下载失败:" + webRequest.error);
            yield break;
        }

        // 从请求中取出 AssetBundle
        AssetBundle AssetBundle_DataSO = DownloadHandlerAssetBundle.GetContent(webRequest);

        // 加载DataSO资源
        AssetBundleRequest dataSORequest = AssetBundle_DataSO.LoadAssetAsync("DataScriptableObject" , typeof(DataScriptableObject));
        yield return dataSORequest;
        dataScriptableObject = dataSORequest.asset as DataScriptableObject;
        if (dataScriptableObject == null)
        {
            Debug.Log("DataSO 不存在!");
            yield break;
        }
        // 加载UIDataSO资源
        AssetBundleRequest uiStyleSORequest = AssetBundle_DataSO.LoadAssetAsync("UIDataScriptableObject" , typeof(UIDataScriptObject));
        yield return uiStyleSORequest;
        uIDataScriptObject = uiStyleSORequest.asset as UIDataScriptObject;
        if (uIDataScriptObject == null)
        {
            Debug.Log("UIDataSO 不存在!");
            yield break;
        }
        #endregion

        #region 加载人物资源包
        // 发出请求,加载人物资源包
        UnityWebRequest webRequest_2 = UnityWebRequestAssetBundle.GetAssetBundle(characterPrefabPack_url);
        yield return webRequest_2.SendWebRequest();

        if (webRequest_2.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("CharacterPrefab AB包下载失败:" + webRequest_2.error);
            yield break;
        }

        // 从请求中取出 AssetBundle
        AssetBundle AssetBundle_MenuCharacterPrefab = DownloadHandlerAssetBundle.GetContent(webRequest_2);

        // 加载角色预制体资源
        AssetBundleRequest characterPrefabRequest = AssetBundle_MenuCharacterPrefab.LoadAssetAsync(dataScriptableObject.characterPrefabName , typeof(GameObject));
        yield return characterPrefabRequest;
        GameObject characterPrefab = characterPrefabRequest.asset as GameObject;
        // 实例化角色预制体资源
        Instantiate(characterPrefab);
        #endregion

        #region 加载Lua脚本资源包
        // 发出请求加载脚本资源包
        UnityWebRequest luaScriptWebRequest = UnityWebRequestAssetBundle.GetAssetBundle(luaScript_url);
        yield return luaScriptWebRequest.SendWebRequest();

        if (luaScriptWebRequest.result != UnityWebRequest.Result.Success)
        {
            Debug.Log("Lua脚本资源 AB包下载失败");
            yield break;
        }

        // 从请求中取出 AssetBundle
        AssetBundle AssetBundle_LuaScript = DownloadHandlerAssetBundle.GetContent(luaScriptWebRequest);

        // 加载lua脚本资源
        AssetBundleRequest luaScriptRequest = AssetBundle_LuaScript.LoadAssetAsync("MenuUIScript.lua" , typeof(TextAsset));
        yield return luaScriptRequest;
        menuUILuaScript = luaScriptRequest.asset as TextAsset;

        luaEnv.DoString(menuUILuaScript.text, "MenuUIScript");

        btnFunc = luaEnv.Global.Get<BtnFunc>("ShowData");
        GameObject.Find("DataButton").GetComponent<Button>().onClick.AddListener(() => btnFunc(dataScriptableObject.MaxHealth , dataScriptableObject.Attack , dataScriptableObject.ActSpeed , dataScriptableObject.CritRate , dataScriptableObject.CritDamage , dataScriptableObject.Defence));
        #endregion

        OnAssetBundleLoaded?.Invoke();
    }
    #endregion

    void Awake()
    {
        // 开启协程加载AB包资源
        StartCoroutine(LoadAssetBundle(dataSO_url , characterPrefabPack_url , uiSprite_url , luaScript_url));
    }
}

编写UI相关代码

lua脚本:

-- 主菜单UI脚本
-- 显示数值按钮事件
local tmpText = CS.UnityEngine.GameObject.Find("DataText"):GetComponent("TextMeshProUGUI");
if tmpText == nil then
    CS.UnityEngine.Debug.Log("TMP Text UI Missing!");
end

tmpText.gameObject:SetActive(false);
local hasButtonClicked = false;
function ShowData(maxHealth , Attack , ActSpeed , CritRate , CritDamage , Defence)
    hasButtonClicked = not hasButtonClicked;
    if (hasButtonClicked) then
        CS.UnityEngine.Debug.Log("显示数据按钮被按下");
        tmpText.gameObject:SetActive(true);
        tmpText.text = "生命值:"..maxHealth.."n攻击力:"..Attack.."n行动速度:"..ActSpeed.."n暴击率:"..CritRate.."n暴击伤害:"..CritDamage.."n防御力:"..Defence;
    elseif (not hasButtonClicked) then
        tmpText.gameObject:SetActive(false);
    end
end

配置UI界面的SO:

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

[CreateAssetMenu(fileName = "UIDataScriptableObject", menuName = "SO/UIDataScriptableObject", order = 0)]
public class UIDataScriptObject : ScriptableObject
{
    [Header("开始菜单-按钮字体颜色")]
    public Color btnTextColor;
    [Header("开始菜单-按钮底色")]
    public Color btnColor;
    [Header("开始菜单-背景图片")]
    public Sprite bgSprite;
}

配置数据显示及人物模型加载的SO:

using System.Collections;
using System.Collections.Generic;
using Org.BouncyCastle.Bcpg.OpenPgp;
using UnityEngine;

[CreateAssetMenu(fileName = "DataScriptableObject", menuName = "SO/DataScriptableObject", order = 0)]
public class DataScriptableObject : ScriptableObject
{
    [Header("要加载的 CharacterPrefab")]
    public string characterPrefabName;
    [Header("生命值")]
    public float MaxHealth = 100f;
    [Header("攻击力")]
    public float Attack = 500f;
    [Header("行动速度")]
    public float ActSpeed = 10f;
    [Header("暴击率")]
    public float CritRate = 20f;
    [Header("暴击伤害")]
    public float CritDamage = 100f;
    [Header("防御力")]
    public float Defence = 50f;
}

将准备好的AB包资源进行打包-Build

打开 Window -> AssetBundle Browser:

打开AB包面板

点击 Build 按钮进行打包,相关设置可以参考我的:
进行打包

将打包后文件添加到HFS中

在HFS中添加共享文件

添加完成后,记得点击保存按钮!否则添加完后的文件将不会被记录(没有真正被添加):
不要忘了保存

随后选中我们将要进行加载的资源文件,复制其URL并粘贴到Unity编辑器中我们暴露出来的string中:
复制文件URL

添加文件URL

在编辑器中运行游戏进行测试

先来看看我们现在的配置:

DataSO配置

UIDataSO配置

运行游戏,我们可以看到资源都正常加载并且lua编写的UI按钮脚本正常执行:
测试结果

打包游戏进行测试

在编辑器中测试完后,我们就可以打包进行测试了。因打包过于基础,这里省略打包过程,直接来运行部分:

首次运行测试

随后我们在编辑器中进行修改要加载的资源,然后再次进行AB包的Build:
更改加载资源

更改加载资源

再次运行游戏执行文件,可以看到在不用重新打包发布的情况下,游戏的内容得到了更新:
再次运行测试

就这样,我们的实验成功了!

结语

关于AB包和xLua还有许多需要我继续探索的地方,日后我还会继续 Addressables 和 PuerTS 的学习。而在示例代码中有关 Unity WebRequest等的部分,可以前往Unity官方网站查阅相关文档(对于一些过了初学阶段的同学来说,本人是很推荐通过看文档来学习的!)。

有关此实验及先前文章中的实验示例项目都在此:Sosyaa-Yanzhi/MySQL_TestProject: 针对于不同技术在Unity中进行简单的实验探索

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
正在播放
0:00 0:00