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