赞
踩
:::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<XLua.LuaFunction>(“main”);
func.Call();
//不能这么用,因为全局查找效率比较低,后续脚本中有更好的用法
using System; using System.Collections; using System.IO; using System.Collections.Generic; using UnityEngine; using XLua; /// <summary> /// 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>("xxx");func.Call(); /// </summary> 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(); } /// <summary> /// 启动对应脚本的lua文件,实际上是吧启动文件的string传给Loader去加载,然后Loader通过GetLuaScript加载出来lua /// </summary> /// <param name="name"></param> public void StartLua(string name) { LuaEnv.DoString(string.Format("require '{0}'",name)); } /// <summary> /// lua里面调用require后面的参数会传到name /// </summary> /// <param name="name"></param> /// <returns></returns> byte[] Loader(ref string name) { return GetLuaScript(name); } /// <summary> /// 和Loader配合使用找到要调用的指定目录lua文件 /// </summary> /// <param name="name"></param> /// <returns></returns> 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; } /// <summary> /// 非编辑器模式下,从路径中获取ab包,从ab包拿到文件 /// </summary> 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; } }); } } /// <summary> /// 把LuaNames里的名字对应lua文件本身里面的内容,放到集合内 /// </summary> /// <param name="assetsName"></param> /// <param name="luaScript"></param> 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>(); /// <summary> /// 传入一个ui名字和lua名字,自动给ui预制体绑定C#脚本,自动执行lua脚本 /// </summary> /// <param name="uiName">ui名字</param> /// <param name="luaName">lua名字</param> 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"); } /// <summary> /// 给Lua提供接口,方便添加分组("第一界面"" 二级弹窗"xxxxx),用于热更给Canvas UI节点添加包含UI层级组 /// </summary> /// <param name="group">要添加的UI层级名称的list</param> 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); } } /// <summary> /// 返回指定层级的transform /// </summary> /// <param name="group">ui层级名称</param> /// <returns>返回字典中对应层级名称的transform</returns> Transform GetUIGroup(string group) { if (!m_UIGroups.ContainsKey(group)) { Debug.LogError("group is not exist"); } return m_UIGroups[group]; } /// <summary> /// 传入一个ui名字和lua名字,以及ui要放到的组中,自动给ui预制体绑定C#脚本,自动执行lua脚本 /// </summary> /// <param name="uiName">ui名字</param> /// <param name="luaName">lua名字</param> 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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。