Unity study notes xLua and AB package hot update demo

This case takes UI hot update as an example

Preliminary preparation

Create a project and create a folder. The file structure and resources of the entire Demo are as follows:

Among them, MenuCanvas is a resource to be loaded through the AB package, which is made into a prefab and temporarily placed in the AB folder; Bootstrap is an empty object used to mount the boot Lua script, and UIRoot is used to place the empty object of UICanvas;

Download the xLua compressed package from GitHub, unzip and copy the files in the Assets to the project Assets, and then move the XLua folder into the ThirdParty, leaving the Plugins unchanged;

Create a DataPath folder at the same level as Assets. Create two folders, AB and Lua, to store the exported AB package resources and Lua scripts respectively.

C# script

ExportAB script

For exporting AB package:

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包成功");
    }
}

When I was testing, I exported both prefabs and image resources to the test package.

xLuaEnvScript

Used to create a globally unique Lua running environment:

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 boot script

Because the most basic implementation of Unity is C#, if you want to use Lua, you should use C# to call 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 script

The Lua script used in this Demo is as follows:

At first, I wanted to build it according to the MVC pattern, but this demo only used a simple UI prefab, so the UI modifications that should be in the View were written directly in the corresponding Controller.

For the modification of local data, we need a JSON parser used in Lua. Here we choose dkjson, download URL:dkjson - dkjson; After downloading, put dkjson.lua into the Lua folder. When using it, you only need to obtain it. It has encode serialization and decode deserialization methods.

PrefabsScript

For instantiating the passed in prefab:

-- 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 script

Global configuration file, only the storage path of one test resource is used here:

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

ABManager Screenplay

Used to manage AB package resources:

-- 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

Used to handle the persistence of gold coin text data in the 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

Controllers for the UI system tested:

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 script

The most critical script, used to guide other required scripts and simulate the implementation of life cycle functions

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

 Now it is basically ready to run. The prefab resources are loaded from the AB package in the AB folder. The Lua script is also stored in the Lua folder. It is loaded and run through a custom loader. They are all stored in the DataPath outside of Assets. , no longer relies on the Resources folder. You can then modify the DataPath path according to your own project, and add or modify AB packages and Lua scripts.

There is a bug in this Demo that has not been resolved, that is, an error will be reported when exiting the run.

After checking, it was mentioned in the official 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

But I made the life cycle function blank in C# and removed the button monitor in Lua's Destroy, but it still didn't solve the problem;

Some bloggers also said: The operation of cleaning up the agent and calling LuaEntry.Dispose are in the same frame stack. There is no guarantee that the release of the related agent has been completed when evn.dispose. One solution is to use another function to clean up the agent. Wrap it up and call a few more layers.

I tried putting Dispose into a separate function and calling it last, but still got an error. In the end, I couldn't find a solution. Since this error will only appear at the end and will not affect the game content, I put it aside for now. If anyone knows the solution, please answer it in the comment area and help me solve my doubts. (Hold your fists).

Guess you like

Origin blog.csdn.net/qq_63122730/article/details/132700379