Unity学习笔记之xLua与AB包热更新Demo

该案例以UI热更新为例

前期准备

创建项目,建立文件夹,整个Demo的文件结构和资源如下:

其中MenuCanvas是将要通过AB包加载的资源,将其做成预制件暂时放入AB文件夹下;Bootstrap是用于挂载引导Lua脚本的空物体,UIRoot用于放UICanvas的空物体;

从GitHub下载xLua压缩包,解压后将Assets内文件复制到项目Assets中,然后可将XLua文件夹移入ThirdParty,Plugins不动;

建立与Assets同级的DataPath文件夹,其中建立AB和Lua两个文件夹,分别存放导出的AB包资源和Lua脚本

C#脚本

ExportAB脚本

用于导出AB包:

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

public class ExportAB
{
    [MenuItem("AB包导出/Windows")]
    public static void ForWindows()
    {
        Export(BuildTarget.StandaloneWindows);
    }

    [MenuItem("AB包导出/Mac")]
    public static void ForMac()
    {
        Export(BuildTarget.StandaloneOSX);
    }

    [MenuItem("AB包导出/iOS")]
    public static void ForiOS()
    {
        Export(BuildTarget.iOS);
    }

    [MenuItem("AB包导出/Android")]
    public static void ForAndroid()
    {
        Export(BuildTarget.Android);
    }

    private static void Export(BuildTarget platform)
    {
        //项目的Assets目录的路径
        string path = Application.dataPath;
        //将AB包存放到与Assets同级的DataPath中的AB目录下
        path = path.Substring(0, path.Length - 7) + "/DataPath/AB/";

        //防止路径不存在
        if(!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }

        //导出ab包的核心代码,生成ab包文件
        //参数1:ab包文件存储路径
        //参数2:导出选项
        //参数3:平台(不同平台的ab包是不一样的)
        BuildPipeline.BuildAssetBundles(
            path,
            BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.ForceRebuildAssetBundle,
            platform
        );

        Debug.Log("导出ab包成功");
    }
}

我在测试时,是将预制件和图片资源都导出到 test 包。

xLuaEnv脚本

用于创建全局唯一的Lua运行环境:

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using XLua;

public class xLuaEnv
{
    #region Singleton

    private static xLuaEnv _Instance = null;

    //单例
    public static xLuaEnv Instance
    {
        get
        {
            if (_Instance == null)
            {
                _Instance = new xLuaEnv();
            }

            return _Instance;
        }
    }

    #endregion

    #region Create LuaEnv

    private LuaEnv _Env;

    //创建单例的时候,Lua运行环境,会一起被创建
    private xLuaEnv()
    {
        _Env = new LuaEnv();    //LuaEnv 是XLua提供的内置Lua运行环境,应保持全局只有一个
        _Env.AddLoader(_ProjectLoader);
    }

    #endregion

    #region Loader

    //创建自定义Lua加载器,这样就可以任意订制项目的Lua脚本的存储位置
    private byte[] _ProjectLoader(ref string filepath)
    {
        string path = Application.dataPath;
        path = path.Substring(0, path.Length - 7) + "/DataPath/Lua/" + filepath + ".lua";

        if (File.Exists(path))
        {
            return File.ReadAllBytes(path);
        }
        else
        {
            return null;
        }
    }

    #endregion

    #region Free LuaEnv

    public void Free()
    {
        //释放LuaEnv,同时也释放单例对象,这样下次调单例对象,会再次产生Lua运行环境
        _Env.Dispose();
        _Instance = null;
    }

    #endregion

    #region Run Lua

    public object[] DoString(string code)
    {
        return _Env.DoString(code);
    }

    //返回Lua环境的全局变量
    public LuaTable Global
    {
        get {
            return _Env.Global;
        }
    }

    #endregion
}

Bootstrap引导脚本

因为Unity最基本的实现是C#,如果想使用Lua,应该用C#去调用Lua:

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

//lua实现生命周期的方法是定义各生命周期函数,然后在c#中分别调用
//用于接收方法的委托,lua表中的方法可以同类型的委托接收
public delegate void LifeCycle();

//与Lua表映射的结构体,要求变量名要与lua表中一致,且为public
[GCOptimize]
public struct LuaBootstrap
{
    public LifeCycle Start;
    public LifeCycle Update;
    public LifeCycle OnDestroy;
}

public class Bootstrap : MonoBehaviour
{
    //Lua的核心table
    private LuaBootstrap _Bootstrap;

    //调用起Lua代码
    void Start()
    {
        //防止切换场景时,脚本对象丢失
        DontDestroyOnLoad(gameObject);

        xLuaEnv.Instance.DoString("require('Bootstrap')");

        //将Lua的核心table,导入映射到C#端
        _Bootstrap = xLuaEnv.Instance.Global.Get<LuaBootstrap>("Bootstrap");
        _Bootstrap.Start();
    }

    void Update()
    {
        _Bootstrap.Update();
    }

    //释放Lua的代码
    void OnDestroy()
    {
        _Bootstrap.OnDestroy();
        //释放Lua环境前,需要将导出到C#的Lua回调函数进行释放
        _Bootstrap.Start = null;
        _Bootstrap.Update = null;
        _Bootstrap.OnDestroy = null;

        //打印所有被C#引用着的Lua函数
        xLuaEnv.Instance.DoString("require('xlua.util').print_func_ref_by_csharp()");
        xLuaEnv.Instance.Free();
    }
}

Lua脚本

本Demo所用Lua脚本如下:

一开始想按照MVC模式来构建,但本Demo中仅用到了一个简单的UI预制件,于是把本应在View中的对于ui的修改直接写在了对应的Controller中。

对于本地数据的修改,我们需要一个Lua中使用的JSON解析器,此处选用的是dkjson,下载网址:dkjson - dkjson;下载后将dkjson.lua放入Lua文件夹下即可,使用时只需require获取,其带有encode序列化和decode反序列化方法。

Prefabs脚本

用于实例化传入的预制件:

-- UI预制体加载
Prefabs = {}

function Prefabs:Instantiate(prefab)
    local go = CS.UnityEngine.Object.Instantiate(prefab)
    go.name = prefab.name
    local canvas = CS.UnityEngine.GameObject.Find("UIRoot").transform
    
    local trs = go.transform
    trs:SetParent(canvas)
    trs.localPosition = CS.UnityEngine.Vector3.zero
    trs.localRotation = CS.UnityEngine.Quaternion.identity
    trs.localScale = CS.UnityEngine.Vector3.one

    trs.offsetMin = CS.UnityEngine.Vector2.zero
    trs.offsetMax = CS.UnityEngine.Vector2.zero

    return go
end

Config脚本

全局配置文件,这里只用到了一个测试资源的存放路径:

-- 全局配置文件 ,用一个全局表存储
Config = {}
-- 资源存放路径,根据实际修改
local path = CS.UnityEngine.Application.dataPath
Config.ABPath = string.sub(path, 1, #path-7) .."/DataPath/AB"

ABManager脚本

用于管理AB包资源:

-- AB包管理器
ABManager = {}
ABManager.Path = Config.ABPath
-- 保存所有被加载过的包
ABManager.Files = {}

-- 总包
local master = CS.UnityEngine.AssetBundle.LoadFromFile(ABManager.Path .. "/AB")
-- 总AB包的Manifest
ABManager.Manifest = master:LoadAsset("AssetBundleManifest",typeof(CS.UnityEngine.AssetBundleManifest))
master:Unload(false)

--加载AB包文件,并处理依赖关系
function ABManager:LoadFile(name)
    if(ABManager.Files[name] ~= nil)
    then
        return
    end

    local dependencies = ABManager.Manifest:GetAllDependencies(name)
    for i = 0,dependencies.Length - 1
    do
        local file = dependencies[i]
        --如果依赖的包没有被加载过,则调用递归加载
        if(ABManager.Files[file] == nil)
        then
            ABManager:LoadFile(file)
        end
    end

    --将AB包加载,并放入管理器的Files变量下
    ABManager.Files[name] = CS.UnityEngine.AssetBundle.LoadFromFile(ABManager.Path .. "/" .. name);
end

--卸载AB包文件
function ABManager:UnloadFile(file)
    if(ABManager.Files[file] ~= nil)
    then
        ABManager.Files[file].Unload(false)
    end
end

--加载资源
--参数1:AB包文件名
--参数2:资源名称
--参数3:资源的类型
function ABManager:LoadAsset(file, name, t)
	--判断AB包是否加载过
	if(ABManager.Files[file] == nil)
	then
		return nil
	else
		return ABManager.Files[file]:LoadAsset(name, t)
	end
end

TestDataModel

用于处理UI中金币文本数据的持久化:

TestDataModel = {}
TestDataModel.Path = CS.UnityEngine.Application.persistentDataPath .. "/Test.json"

function TestDataModel:New()
    if(not CS.System.IO.File.Exists(TestDataModel.Path))
    then
        -- 写入测试Json数据
        CS.System.IO.File.WriteAllText(TestDataModel.Path,'{"Gold" : 49}')
    end
end

function TestDataModel:ReadAllData()
    if(CS.System.IO.File.Exists(TestDataModel.Path))
    then
        return LuaJson.decode(CS.System.IO.File.ReadAllText(TestDataModel.Path))
    else
        return nil
    end
end

-- 测试方法,用于增加金币数
function TestDataModel:AddGold(num)
    if(CS.System.IO.File.Exists(TestDataModel.Path))
    then
        local data = LuaJson.decode(CS.System.IO.File.ReadAllText(TestDataModel.Path))
        data.Gold = data.Gold + num
        CS.System.IO.File.WriteAllText(TestDataModel.Path, LuaJson.encode(data))
        
        return data
    end    
end

MenuCanvasController

所测试UI系统的控制器:

local Controller = {}
Controller.Page = nil

-- Lua模拟Start生命周期函数
-- 控制器被加载时,Start生命周期函数执行
function Controller:Start()
    print("MenuCanvas:Start()")
    ABManager:LoadFile("test")
    local obj = ABManager:LoadAsset(
        "test",
        "MenuCanvas",
        typeof(CS.UnityEngine.Object)
    )

    --加载预制体
    local page = Prefabs:Instantiate(obj)
    Controller.Page = page

    --读取数据模型
    require("DataModel/TestDataModel")
    TestDataModel:New()
    local data = TestDataModel:ReadAllData()

    --修改ui页面内容,不过这里应该放到View中
    page.transform:Find("Gold"):GetComponent(typeof(CS.UnityEngine.UI.Text)).text = data.Gold
    
    page.transform:Find("Add"):GetComponent(typeof(CS.UnityEngine.UI.Button)).onClick:AddListener(Controller.AddGold)
end

function Controller:AddGold()
    local data = TestDataModel:AddGold(10)
    Controller.Page.transform:Find("Gold"):GetComponent(typeof(CS.UnityEngine.UI.Text)).text = data.Gold
end

-- 模拟Update
function Controller:Update()
	print("MenuCanvas:Update()")
end

function Controller:OnDestroy()
    print("MenuCanvas:OnDestroy()")
	Controller.Page.transform:Find("Add"):GetComponent(typeof(CS.UnityEngine.UI.Button)).onClick:RemoveListener(Controller.AddGold)
end

return Controller

Bootstrap脚本

最为关键的脚本,用于引导其他所需脚本,并模拟实现生命周期函数

Bootstrap = {}

-- 核心table,存储所有控制器
Bootstrap.Controllers = {}

require("Config")
require("ABManager")
require("Prefabs")
LuaJson = require("dkjson")

Bootstrap.Start = function ()
    --加载所有控制器,这里我只有一个
    Bootstrap.Load("MenuCanvasController")
    
end

-- 加载控制器,参数是脚本名称
Bootstrap.Load = function (name)
    local c = require("Controller/" .. name)
    Bootstrap.Controllers[name] = c
    c:Start()
end

Bootstrap.Update = function ()
    -- 遍历所有已注册脚本,执行各自的Update
    for k, v in pairs(Bootstrap.Controllers) do
        if(v.Update ~= nil)
        then
            v:Update()
        end
    end
end

Bootstrap.OnDestroy = function ()
    for k, v in pairs(Bootstrap.Controllers) do
        if(v.OnDestroy ~= nil)
        then
            v:OnDestroy()
        end
    end
end

 至此基本可以运行了,预制体资源是从AB文件夹中的AB包中加载的,Lua脚本也是存放在Lua文件夹下,通过自定义加载器加载运行,都是存放在Assets之外的DataPath中,不再依赖Resources文件夹,到时可根据自己项目修改DataPath路径,以及增加或修改AB包和Lua脚本。

本Demo有个bug没有解决,就是退出运行时会报错

查了一下,官方FAQ中有提及:

调用LuaEnv.Dispose时,报“try to dispose a LuaEnv with C# callback!”错是什么原因?

这是由于C#还存在指向lua虚拟机里头某个函数的delegate,为了防止业务在虚拟机释放后调用这些无效(因为其引用的lua函数所在虚拟机都释放了)delegate导致的异常甚至崩溃,做了这个检查。

怎么解决?释放这些delegate即可,所谓释放,在C#中,就是没有引用:

你是在C#通过LuaTable.Get获取并保存到对象成员,赋值该成员为null;

你是在lua那把lua函数注册到一些事件事件回调,反注册这些回调;

如果你是通过xlua.hotfix(class, method, func)注入到C#,则通过xlua.hotfix(class, method, nil)删除;

要注意以上操作在Dispose之前完成。

xlua提供了一个工具函数来帮助你找到被C#引用着的lua函数,util.print_func_ref_by_csharp,使用很简单,执行如下lua代码:

local util = require 'xlua.util'
util.print_func_ref_by_csharp()

可以看到控制台有类似这样的输出,下面第一行表示有一个在main.lua的第2行定义的函数被C#引用着

LUA: main.lua:2
LUA: main.lua:13

但是我在C#中将生命周期函数置空以及在lua的Destroy中将按钮监听都去掉了,依然没解决;

也有博主说是:清理代理的操作和调LuaEntry.Dispose在同一帧栈,不能保证在evn.dispose时已经完成相关化代理的释放工作,一种解决方案就是把清理代理的过程再用一个函数包一下,多调用几层。

我试着把Dispose单独放到一个函数并最后调用,依然报错。最后我也没找着解决方法,由于这个错误只会在结束时出现且不会影响游戏内容,就暂且搁置了,如果有知道解决方法的老哥请在评论区解答一下,帮帮小弟解惑(抱拳)。

猜你喜欢

转载自blog.csdn.net/qq_63122730/article/details/132700379