Unity Xlua热更新框架(五):Lua和UI管理

8. Lua管理器

image.png
:::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管理:

  • 异步加载(bundle是异步加载,如果上一步是require加载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();
            }
            );        
    }
}

9. LuaBehaviour

9-1. LuaBehaviour

逻辑分离
绑定:写C#函数的习惯分离到Lua中,把Lua的函数写成和C#一样的,当执行C#时就知道执行哪个Lua脚本离的哪个逻辑
Unity Xlua热更新框架(五):Lua和UI管理_第1张图片
rec-screen-_1_.gifimage.pngimage.pngxLua官方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运行脚本
image.pngLuaBehaviour是个父类,将来会有别的脚本继承他,例如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();
    }
}

9-2. UILogic

如何能在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

image.png
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脚本

9-3. UI层级

UIManager

  • 加载UI
  • 绑定和执行Lua脚本
  • UI对象管理(委托给对象池)(该部分暂时不写)
  • 层级管理

UI界面层级:

  • 界面类型:
    • 一级界面(主界面,渲染层级最低)
    • 二级弹窗
    • 三级弹窗(渲染层级最高)
    • 特殊界面(跑马灯之类的,永远在最上面)
  • UGUI层级特点
    • 根据节点顺序渲染(Canvas谁越靠下越先渲染)
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

你可能感兴趣的:(Unity,Xlua,lua,unity)