:::info
Lua存在两种加载器,一种默认加载器(env.DoString("require(‘test’)"直接用了默认加载其),直接调用StreamingAssets中的脚本);一种是自定义加载器(env.AddLoader(Envpath)),优先于默认加载器(下文DoString就是从自定义加载器的路径读取的),并且当Lua代码执行require函数时,自定义加载器会尝试获得文件的内容,并通过虚拟机解析执行。
:::
注意:BuildPipeline.BuildAssetBundles没法build构建.lua文件,只能构建.bytes
在xLua加自定义loader是很简单的,只涉及到一个接口:
:::info
public delegate byte[] CustomLoader(ref string filepath);
public void LuaEnv.AddLoader(CustomLoader loader)
:::
通过AddLoader可以注册个回调,该回调参数是字符串,lua代码里头调用require时,参数将会透传给回调,回调中就可以根据这个参数去加载指定文件,如果需要支持调试,需要把filepath修改为真实路径传出。该回调返回值是一个byte数组,如果为空表示该loader找不到,否则则为lua文件的内容。
public class Manager : MonoBehaviour
{
private static LuaManager _lua;
public static LuaManager Lua
{
get { return _lua; }
}
public void Awake()
{
_resource = this.gameObject.AddComponent<ResourceManager>();
_lua = this.gameObject.AddComponent<LuaManager>();
}
}
lua管理:
所有lua(bundle)先加载出来,文件内容全部缓存在内存中,使用时直接调用。
public void ParseVersionFile()
{
//拿到版本文件路径
string url = Path.Combine(PathUtil.BundleResourcePath, AppConst.FileListName);
//对文件进行读取
string[] data = File.ReadAllLines(url);
//解析文件信息
for (int i = 0; i < data.Length; i++)
{
BundleInfo bundleInfo = new BundleInfo();
string[] info = data[i].Split('|');
bundleInfo.AssetName = info[0];
bundleInfo.BundleName = info[1];
//list特性:本质是数组,可动态扩容
bundleInfo.Dependeces = new List<string>(info.Length - 2);
for (int j = 2; j < info.Length; j++)
{
bundleInfo.Dependeces.Add(info[j]);
}
m_BundleInfos.Add(bundleInfo.AssetName, bundleInfo);
//查找luaScripts下的lua文件,添加到luamanager中
if(info[0].IndexOf("LuaScripts") > 0)
{
Manager.Lua.LuaNames.Add(info[0]);
}
}
}
public static readonly string LuaPath = "Assets/BuildResources/LuaScripts/";
//为什么lua不要pathUtil传回一个相对路径?因为lua调用脚本就是用的相对路径调用,传进来的路径就是相对路径
public void LoadLua(string assetName, Action<UnityEngine.Object> action = null)
{
LoadAsset(assetName, action);
}
关闭物体挂载hotUpdate脚本,修改为EditorMode
调用Lua的函数,要么是Lua文件中直接function();要么通过C#调用lua的函数访问LuaEnv.Global就可以了,例如:luaenv.Global.Get(“a”)
XLua.LuaFunction func = Manager.Lua.LuaEnv.Global.Get
(“main”);
func.Call();
//不能这么用,因为全局查找效率比较低,后续脚本中有更好的用法
using System;
using System.Collections;
using System.IO;
using System.Collections.Generic;
using UnityEngine;
using XLua;
///
/// LuaNames是ResourceManager中解析版本文件ParseVersionFile时赋值的,根据LuaNames,从Bundle(或Editor本地)加载LoadLua,加载好就放入m_LuaScripts,全部加载完清空LuaNames
/// 初始化Init:添加一个回调,创建new LuaEnv()虚拟机,虚拟机LuaEnv.AddLoader(loader)(外部StartLua调用DoString找lua,loader调用GetLuaScript找到lua脚本),按模式加载lua(bundle或者editor)
/// 调用的前提是全部加载完了,执行了回调通知加载完毕,才可以调用。即InitOk
/// 调用:1.Init;2.StartLua(内部是LuaEnv.DoStringloader调用GetLuaScript找到lua脚本);3.调用函数XLua.LuaFunction func = Global.Get("xxx");func.Call();
///
public class LuaManager : MonoBehaviour
{
//所有的lua文件名,获取所有lua,然后进行预加载,ResourceManager查找lua文件放进来
public List<string> LuaNames = new List<string>();
//缓存lua脚本内容
private Dictionary<string, byte[]> m_LuaScripts;
//定义一个lua虚拟机,消耗比较大,,全局只需要一个,,需要using XLua;
public LuaEnv LuaEnv;
Action InitOK;
//如果是Editor模式下,直接从luapath就把所有lua读取到字典中了,然后在调用,属于同步加载后同步使用的情况
//但是在其他模式下,需要从bundle异步加载lua需要等待,如果等待时start就调用了,属于异步加载同步使用的情况,需要预加载
//需要创建一个回调通知
public void Init(Action init)
{
InitOK += init;
//初始化虚拟机
LuaEnv = new LuaEnv();
//外部调用require时,会自动调用loader来获取文件
LuaEnv.AddLoader(Loader);
m_LuaScripts = new Dictionary<string, byte[]>();
#if UNITY_EDITOR
if (AppConst.GameMode == GameMode.EditorMode)
EditorLoadLuaScript();
else
#endif
LoadLuaScript();
}
///
/// 启动对应脚本的lua文件,实际上是吧启动文件的string传给Loader去加载,然后Loader通过GetLuaScript加载出来lua
///
///
public void StartLua(string name)
{
LuaEnv.DoString(string.Format("require '{0}'",name));
}
///
/// lua里面调用require后面的参数会传到name
///
///
///
byte[] Loader(ref string name)
{
return GetLuaScript(name);
}
///
/// 和Loader配合使用找到要调用的指定目录lua文件
///
///
///
public byte[] GetLuaScript(string name)
{
//为什么替换掉.换成/,,因为一般使用require ui.login.register
name = name.Replace(".", "/");
//自定义的lua后缀名是.bytes
string fileName = PathUtil.GetLuaPath(name);
byte[] luaScript = null;
//从集合中拿到缓存的lua内容
if(!m_LuaScripts.TryGetValue(fileName, out luaScript))
{
Debug.LogError("lua script is not exist:" + fileName);
}
return luaScript;
}
///
/// 非编辑器模式下,从路径中获取ab包,从ab包拿到文件
///
void LoadLuaScript()
{
foreach (var name in LuaNames)
{
//异步的需要一个回调(=>后面那一坨,当LoadLua执行时完,执行回调并invoke把结果返回),obj就是返回的lua的对象
Manager.Resource.LoadLua(name, (UnityEngine.Object obj) =>
{
//LoadLua调用完会把bundle加载好的bundleRequest.asset传进来用obj接受
//把这个lua根据名称添加到m_LuaScripts中
AddLuaScript(name, (obj as TextAsset).bytes);
//在ResourceManager中解析版本文件时加载所有lua文件到LuaNames
//如果LuaNames全部都加载到m_LuaScripts集合中,就清空LuaNames,退出循环
if (m_LuaScripts.Count >= LuaNames.Count)
{
//所有lua文件加载完成了。就可以执行使用lua函数的方法了
InitOK?.Invoke();
LuaNames.Clear();
LuaNames = null;
}
});
}
}
///
/// 把LuaNames里的名字对应lua文件本身里面的内容,放到集合内
///
///
///
public void AddLuaScript(string assetsName, byte[] luaScript)
{
//为了放置重复添加,用这种方式可以直接覆盖
m_LuaScripts[assetsName] = luaScript;
}
#if UNITY_EDITOR
//编辑器模式下直接加载lua文件,并把lua名字和内容放到集合内
void EditorLoadLuaScript()
{
//搜索所有lua文件
string[] luaFiles = Directory.GetFiles(PathUtil.LuaPath, "*.bytes", SearchOption.AllDirectories);
for (int i = 0; i < luaFiles.Length; i++)
{
string fileName = PathUtil.GetStandardPath(luaFiles[i]);
//读取lua文件
byte[] file = File.ReadAllBytes(fileName);
//把读取的lua文件添加进去
AddLuaScript(PathUtil.GetUnityPath(fileName), file);
}
InitOK?.Invoke();
}
#endif
private void Update()
{
//释放lua内存
if (LuaEnv != null)
{
LuaEnv.Tick();
}
}
private void OnDestroy()
{
//虚拟机需要销毁掉
if (LuaEnv != null)
{
LuaEnv.Dispose();
LuaEnv = null;
}
}
}
public class GameStart : MonoBehaviour
{
public GameMode GameMode;
// Start is called before the first frame update
void Start()
{
AppConst.GameMode = this.GameMode;
DontDestroyOnLoad(this);
Manager.Resource.ParseVersionFile();
Manager.Lua.Init(
() =>
{
//初始化完成之后(lua都加载完),在执行回调
Manager.Lua.StartLua("main"); //输入的文件名
//输入的是函数名
XLua.LuaFunction func = Manager.Lua.LuaEnv.Global.Get<XLua.LuaFunction>("Main");
func.Call();
}
);
}
}
逻辑分离
绑定:写C#函数的习惯分离到Lua中,把Lua的函数写成和C#一样的,当执行C#时就知道执行哪个Lua脚本离的哪个逻辑
xLua官方demo
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using XLua;
using System;
namespace XLuaTest
{
//拖拽进来的灯
[System.Serializable]
public class Injection
{
public string name;
public GameObject value;
}
[LuaCallCSharp]
public class LuaBehaviour : MonoBehaviour
{
//读取的lua脚本的内容
public TextAsset luaScript;
public Injection[] injections;
internal static LuaEnv luaEnv = new LuaEnv(); //all lua behaviour shared one luaenv only!
internal static float lastGCTime = 0;//GC的计时
internal const float GCInterval = 1;//1 second GC的间隔
//action引用lua的委托
private Action luaStart;
private Action luaUpdate;
private Action luaOnDestroy;
//脚本的运行环境
private LuaTable scriptEnv;
void Awake()
{
scriptEnv = luaEnv.NewTable();
// 为每个脚本设置一个独立的环境,可一定程度上防止脚本间全局变量、函数冲突
LuaTable meta = luaEnv.NewTable();
meta.Set("__index", luaEnv.Global);
scriptEnv.SetMetaTable(meta);
meta.Dispose();
//把这个脚本的实例注入到lua的self,让self = this
scriptEnv.Set("self", this);
foreach (var injection in injections)
{
//令injection.name = injection.value
//即lightObject = light对象,因此lua可以直接用
scriptEnv.Set(injection.name, injection.value);
}
//把LuaTestScript脚本绑定到scriptEnv的运行环境
luaEnv.DoString(luaScript.text, "LuaTestScript", scriptEnv);
//如果lua的awake不为空,C#的awake执行时调用lua的awake
Action luaAwake = scriptEnv.Get<Action>("awake");
//上下两种定义一样,重载类型不同
scriptEnv.Get("start", out luaStart);
scriptEnv.Get("update", out luaUpdate);
scriptEnv.Get("ondestroy", out luaOnDestroy);
if (luaAwake != null)
{
luaAwake();
}
}
// Use this for initialization
void Start()
{
if (luaStart != null)
{
luaStart();
}
}
// Update is called once per frame
void Update()
{
if (luaUpdate != null)
{
luaUpdate();
}
if (Time.time - LuaBehaviour.lastGCTime > GCInterval)
{
luaEnv.Tick();
LuaBehaviour.lastGCTime = Time.time;
}
}
void OnDestroy()
{
if (luaOnDestroy != null)
{
luaOnDestroy();
}
luaOnDestroy = null;
luaUpdate = null;
luaStart = null;
scriptEnv.Dispose();
injections = null;
}
}
}
这个脚本内的Awake调用LuaAwake,Start调用LuaStart,Update调用LuaUpdate,Destroy调用LuaDestroy,,,前面通过Env.Get拿到函数
local speed = 10
local lightCpnt = nil
function start()
print("lua start...")
print("injected object", lightObject)
--C#里的Injection变量定义了lightObject,Lua没有定义,但可以直接用
--见上面代码块蓝色部分
lightCpnt= lightObject:GetComponent(typeof(CS.UnityEngine.Light))
end
function update()
local r = CS.UnityEngine.Vector3.up * CS.UnityEngine.Time.deltaTime * speed
self.transform:Rotate(r)
lightCpnt.color = CS.UnityEngine.Color(CS.UnityEngine.Mathf.Sin(CS.UnityEngine.Time.time) / 2 + 0.5, 0, 0, 1)
end
function ondestroy()
print("lua destroy")
end
创建我们自己的lua运行脚本
LuaBehaviour是个父类,将来会有别的脚本继承他,例如UI,3DObject,他们的逻辑不同,父类只提供Unity的生命周期,特定的方法在子类中实现。
using System;
using UnityEngine;
using XLua;
public class LuaBehaviour : MonoBehaviour
{
//全局只能有一个LuaEnv
private LuaEnv m_LuaEnv = Manager.Lua.LuaEnv;
protected LuaTable m_ScriptEnv;
private Action m_LuaAwake;
private Action m_LuaStart;
private Action m_LuaUpdate;
private Action m_LuaOnDestroy;
void Awake()
{
m_ScriptEnv = m_LuaEnv.NewTable();
// 为每个脚本设置一个独立的环境,可一定程度上防止脚本间全局变量、函数冲突
LuaTable meta = m_LuaEnv.NewTable();
meta.Set("__index", m_LuaEnv.Global);
m_ScriptEnv.SetMetaTable(meta);
meta.Dispose();
m_ScriptEnv.Set("self", this);
m_ScriptEnv.Get("Awake", out m_LuaAwake);
m_ScriptEnv.Get("Start", out m_LuaStart);
m_ScriptEnv.Get("Update", out m_LuaUpdate);
m_LuaAwake?.Invoke();
}
// Start is called before the first frame update
void Start()
{
m_LuaStart?.Invoke();
}
// Update is called once per frame
void Update()
{
m_LuaUpdate?.Invoke();
}
//父类的需要是保护级,因为子类更特殊有其他需要进行的操作
protected virtual void Clear()
{
m_LuaOnDestroy = null;
m_LuaAwake = null;
m_LuaStart = null;
//运行环境释放掉
m_ScriptEnv?.Dispose();
m_ScriptEnv = null;
}
//两个不一定同时触发,退出的时候不会调用OnDestroy,所以都要写Clear
private void OnDestroy()
{
m_LuaOnDestroy?.Invoke();
Clear();
}
private void OnApplicationQuit()
{
Clear();
}
}
如何能在Awake之前把变量传进去?prefab添加脚本手动赋值可以,,但是框架不能这样做,不能全都手动赋值,,,只有这一种办法,,只能舍弃unity中使用awake,start,需要在Unity中模拟出awake和start的特性并提供给Lua使用这种特性(awake实例化之后最先调用,实例化到销毁周期内只触发一次,隐藏和激活不会触发awake;;;;start加载和激活都会从触发),start刷新UI,awake加载一些配置和数据
开发者:调用API,传入一个UI预设的名字和lua脚本的名字,自动绑定C#脚本在UI预设上,并且执行Lua脚本
public class LuaBehaviour : MonoBehaviour
{
//全局只能有一个LuaEnv
private LuaEnv m_LuaEnv = Manager.Lua.LuaEnv;
protected LuaTable m_ScriptEnv;
private Action m_LuaInit;
private Action m_LuaUpdate;
private Action m_LuaOnDestroy;
public string luaName;
void Awake()
{
m_ScriptEnv = m_LuaEnv.NewTable();
// 为每个脚本设置一个独立的环境,可一定程度上防止脚本间全局变量、函数冲突
LuaTable meta = m_LuaEnv.NewTable();
meta.Set("__index", m_LuaEnv.Global);
m_ScriptEnv.SetMetaTable(meta);
meta.Dispose();
m_ScriptEnv.Set("self", this);
}
//用Init来代替unity的awake
public virtual void Init(string luaName)
{
m_LuaEnv.DoString(Manager.Lua.GetLuaScript(luaName), luaName, m_ScriptEnv);
m_ScriptEnv.Get("Update", out m_LuaUpdate);
m_ScriptEnv.Get("OnInit", out m_LuaInit);
m_LuaInit?.Invoke();
}
// Start直接删掉
// Update is called once per frame
void Update()
{
m_LuaUpdate?.Invoke();
}
//父类的需要是保护级,因为子类更特殊有其他需要进行的操作
protected virtual void Clear()
{
m_LuaOnDestroy = null;
//运行环境释放掉
m_ScriptEnv?.Dispose();
m_ScriptEnv = null;
m_LuaInit = null;
m_LuaUpdate = null;
}
//两个不一定同时触发,退出的时候不会调用OnDestroy,所以都要写Clear
private void OnDestroy()
{
m_LuaOnDestroy?.Invoke();
Clear();
}
private void OnApplicationQuit()
{
Clear();
}
}
创建两个脚本Scripts/Framework/Behaviour/UILogic.cs和Scripts/Framework/Manager/UIManager.cs(对外提供OpenUI的接口)
UILogic继承LuaBehaviour,实现UI自己的Open和Close方法
using System;
//继承LuaBehaviour
public class UILogic : LuaBehaviour
{
Action m_LuaOnOpen;
Action m_LuaOnClose;
public override void Init(string luaName)
{
base.Init(luaName);
m_ScriptEnv.Get("OnOpen", out m_LuaOnOpen);
m_ScriptEnv.Get("OnClose", out m_LuaOnClose);
}
public void OnOpen()
{
m_LuaOnOpen?.Invoke();
}
public void OnClose()
{
m_LuaOnClose?.Invoke();
}
protected override void Clear()
{
base.Clear();
m_LuaOnOpen = null;
m_LuaOnClose = null;
}
}
using System.Collections.Generic;
using UnityEngine;
public class UIManager : MonoBehaviour
{
//用名字作为key,对象作为value,,,缓存UI,,后期会用对象池管理
Dictionary<string, GameObject> m_UI = new Dictionary<string, GameObject>();
///
/// 传入一个ui名字和lua名字,自动给ui预制体绑定C#脚本,自动执行lua脚本
///
/// ui名字
/// lua名字
public void OpenUI(string uiName, string luaName)
{
GameObject ui = null;
//如果ui已经加载过了(从ab包取出放到Dictionary中),就只执行OnOpen(Start),不在执行Init(Awake)
if(m_UI.TryGetValue(uiName, out ui))
{
UILogic uiLogic = ui.GetComponent<UILogic>();
uiLogic.OnOpen();
return;
}
Manager.Resource.LoadUI(uiName, (UnityEngine.Object obj) =>
{
ui = Instantiate(obj) as GameObject;
m_UI.Add(uiName, ui);
//给UI预制体绑定UILogic的C#脚本
UILogic uiLogic = ui.AddComponent<UILogic>();
//初始化这个lua脚本(Awake)
uiLogic.Init(luaName);
//UI的Start
uiLogic.OnOpen();
});
}
}
private static UIManager _ui;
public static UIManager UI
{
get { return _ui; }
}
public void Awake()
{
_ui = this.gameObject.AddComponent<UIManager>();
}
创建BuildResources/LuaScripts/ui/testUI.bytes
function OnInit()
print("lua OnInit")
end
function OnOpen()
print("lua OnOpen")
end
function Update()
print("lua Update")
end
function OnClose()
print("lua OnClose")
end
Manager = CS.Manager --引用C#里面定义的类实例
function Main()
print("hello main")
--"TestUI"传的是BuildResources/UI/Prefabs/TestUI.prefab
--"ui.TestUI"是lua脚本的名字
Manager.UI:OpenUI("TestUI","ui.TestUI")
end
GameStart.cs主脚本调用main.lua主脚本,main.lua执行Manager.UI.OpenUI调用UIManager.cs的OpenUI函数执行Manager.Resource.LoadUI加载了一个TestUI.prefab,并绑定了一个UILogic脚本,执行Init和OnOpen两个自定义的Awake和Start,UILogic脚本绑定在物体上之后,还会执行OnUpdate\OnDestrou等函数分别取调用Test UI.bytes的lua脚本
UIManager
UI界面层级:
public class UIManager : MonoBehaviour
{
//用名字作为key,对象作为value,,,缓存UI,,后期会用对象池管理
Dictionary<string, GameObject> m_UI = new Dictionary<string, GameObject>();
//UI分组
Dictionary<string, Transform> m_UIGroups = new Dictionary<string, Transform>();
private Transform m_UIParent;
private void Awake()
{
m_UIParent = this.transform.parent.Find("UI");
}
///
/// 给Lua提供接口,方便添加分组("第一界面"" 二级弹窗"xxxxx),用于热更给Canvas UI节点添加包含UI层级组
///
/// 要添加的UI层级名称的list
public void SetUIGroup(List<string> group)
{
for (int i = 0; i < group.Count; i++)
{
GameObject go = new GameObject("Group-" + group[i]);
go.transform.SetParent(m_UIParent, false);
m_UIGroups.Add(group[i], go.transform);
}
}
///
/// 返回指定层级的transform
///
/// ui层级名称
/// 返回字典中对应层级名称的transform
Transform GetUIGroup(string group)
{
if (!m_UIGroups.ContainsKey(group))
{
Debug.LogError("group is not exist");
}
return m_UIGroups[group];
}
///
/// 传入一个ui名字和lua名字,以及ui要放到的组中,自动给ui预制体绑定C#脚本,自动执行lua脚本
///
/// ui名字
/// lua名字
public void OpenUI(string uiName, string group, string luaName)
{
GameObject ui = null;
//如果ui已经加载过了(从ab包取出放到Dictionary中),就只执行OnOpen(Start),不在执行Init(Awake)
if(m_UI.TryGetValue(uiName, out ui))
{
UILogic uiLogic = ui.GetComponent<UILogic>();
uiLogic.OnOpen();
return;
}
Manager.Resource.LoadUI(uiName, (UnityEngine.Object obj) =>
{
ui = Instantiate(obj) as GameObject;
m_UI.Add(uiName, ui);
//加载ui成功后,设置父节点
Transform parent = GetUIGroup(group);
ui.transform.SetParent(parent, false);
//给UI预制体绑定UILogic的C#脚本
UILogic uiLogic = ui.AddComponent<UILogic>();
//初始化这个lua脚本(Awake)
uiLogic.Init(luaName);
//UI的Start
uiLogic.OnOpen();
});
}
}
Manager = CS.Manager --引用C#里面定义的类
--定义UI层级
local group =
{
"Main",
"UI",
"Box",
}
Manager.UI:SetUIGroup(group)
function Main()
print("hello main")
--"TestUI"传的是BuildResources/UI/Prefabs/TestUI.prefab
--"UI"是要把这个TestUI.prefab放到的层级父对象下面
--"ui.TestUI"是lua脚本的名字
Manager.UI:OpenUI("TestUI", "UI", "ui.TestUI")
Manager.UI:OpenUI("Login/LoginUI", "Main", "ui.TestUI")
end