飞道的博客

Unity使用tolua框架教程: LuaFramewrk

729人阅读  评论(0)

一、tolua下载

toluaGitHub下载地址:https://github.com/topameng/tolua

假设我们下载的是LuaFramework_UGUI,它是基于Unity 5.0 + UGUI + tolua构建的工程

下载下来得到一个LuaFramework_UGUI-master.zip

二、运行Demo

1、生成注册文件

解压之后就是一个Unity的工程,直接用Unity打开,首次打开工程会询问生成注册文件,点击确定即可

2、将lua打成AssetBundle

首先要执行lua资源的生成(打AssetBundle),点击菜单【LuaFramework】-【Build Windows Resource】

会把lua代码打成AssetBundle放在StreamingAssets中。

3、解决报错

如果你用的不是Unity5.x,而是Unity2020,那么可能会报错:

这是因为新版本的Unity有些属性和接口已经废弃了的原因,我们需要特殊处理一下
一个是Light类,一个是QualitySettings类,这两个类我们一般不需要在lua中使用,所以我们不对他们生产Wrap即可:

  1. 打开CustomSettings.cs,把 _GT(typeof(Light)),_GT(typeof(QualitySettings)),这两行注释掉
  2. 然后单击菜单【Lua】-【Clear wrap files】清理掉Wrap
  3. 然后再单击菜单【Lua】-【Generate All】重新生成Wrap
  4. 然后再重新点击菜单【LuaFramework】-【Build Windows Resource】生成lua资源。

执行【Lua】-【Generate All】菜单的时候,你可能会报错

定位到报错的位置

添加判空

重新执行【Lua】-【Generate All】菜单
生成后应该还有报错

这是因为新版的ParticleSystem类新增了一些接口,我们可以定位到对应报错的地方,把报错的地方注释掉。
不过为了防止下次执行【Lua】-【Generate All】菜单时又被覆盖导致报错,我们可以把UnityEngine_ParticleSystemWrap.cs移动到BaseType目录中

并把CustomSettings.cs中的_GT(typeof(ParticleSystem)),注释掉,并在LuaState.cs注册ParticleSystemWrap类

同理,UnityEngine_MeshRendererWrap.cs可能也会报错,按上面的处理方式处理。

4、顺利生成AssetBundle

最后,【LuaFramework】-【Build Windows Resource】成功生成AssetBundle,我们可以在StreamingAssets中看到很多AssetBundle文件。

5、运行Demo场景

接下来,我们就可以运行Demo场景了。打开main场景

运行效果

6、Unity2020无报错版LuaFramework-UGUI

如果你不想手动修复上报的报错,我将修复好的版本上传到了GitHub,使用Unity2020可以直接运行。
GitHub工程地址:https://github.com/linxinfa/Unity2020-LuaFramework-UGUI

三、开发环境IDE

可以使用subline,也可以使用visual studio,个人偏好使用visual studio,配合插件BabeLua

Unity写lua代码的vs插件:BabeLua: https://blog.csdn.net/linxinfa/article/details/88191485

四、接口讲解

1、MVC框架


上面这个Lua动态创建出来的面板的控制逻辑在PromptCtrl.lua脚本中,我们可以看到lua工程中使用了经典的MVC框架。

MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

所有的controlerCtrlManager中注册

-- CtrlManager.lua
function CtrlManager.Init()
	logWarn("CtrlManager.Init----->>>");
	ctrlList[CtrlNames.Prompt] = PromptCtrl.New();
	ctrlList[CtrlNames.Message] = MessageCtrl.New();
	return this;
end

通过CtrlManager获取对应的controler对象,调用Awake()方法

-- CtrlManager.lua
local ctrl = CtrlManager.GetCtrl(CtrlNames.Prompt);
if ctrl ~= nil then
    ctrl:Awake();
end

controler类中,Awake()方法中调用C#PanelManagerCreatePanel方法

-- PromptCtrl.lua
function PromptCtrl.Awake()
	logWarn("PromptCtrl.Awake--->>");
	panelMgr:CreatePanel('Prompt', this.OnCreate);
end

C#PanelManagerCreatePanel方法去加载界面预设,并挂上LuaBehaviour脚本

这个LuaBehaviour脚本,主要是管理panel的生命周期,调用luapanelAwake,获取UI元素对象

-- PromptPanel.lua

local transform;
local gameObject;

PromptPanel = {
   };
local this = PromptPanel;

--启动事件--
function PromptPanel.Awake(obj)
	gameObject = obj;
	transform = obj.transform;

	this.InitPanel();
	logWarn("Awake lua--->>"..gameObject.name);
end

--初始化面板--
function PromptPanel.InitPanel()
	this.btnOpen = transform:Find("Open").gameObject;
	this.gridParent = transform:Find('ScrollView/Grid');
end

--单击事件--
function PromptPanel.OnDestroy()
	logWarn("OnDestroy---->>>");
end

panelAwake执行完毕后,就会执行controlerOnCreate(),在controler中对UI元素对象添加一些事件和控制

-- PromptCtrl.lua
--启动事件--
function PromptCtrl.OnCreate(obj)
	gameObject = obj;
	transform = obj.transform;

	panel = transform:GetComponent('UIPanel');
	prompt = transform:GetComponent('LuaBehaviour');
	logWarn("Start lua--->>"..gameObject.name);

	prompt:AddClick(PromptPanel.btnOpen, this.OnClick);
	resMgr:LoadPrefab('prompt', {
    'PromptItem' }, this.InitPanel);
end

2、StartUp启动框架

AppFacade.Instance.StartUp();   //启动游戏

这个接口会抛出一个NotiConst.START_UP事件,对应的响应类是StartUpCommand

using UnityEngine;
using System.Collections;
using LuaFramework;

public class StartUpCommand : ControllerCommand {
   

    public override void Execute(IMessage message) {
   
        if (!Util.CheckEnvironment()) return;

        GameObject gameMgr = GameObject.Find("GlobalGenerator");
        if (gameMgr != null) {
   
            AppView appView = gameMgr.AddComponent<AppView>();
        }
        //-----------------关联命令-----------------------
        AppFacade.Instance.RegisterCommand(NotiConst.DISPATCH_MESSAGE, typeof(SocketCommand));

        //-----------------初始化管理器-----------------------
        AppFacade.Instance.AddManager<LuaManager>(ManagerName.Lua);
        AppFacade.Instance.AddManager<PanelManager>(ManagerName.Panel);
        AppFacade.Instance.AddManager<SoundManager>(ManagerName.Sound);
        AppFacade.Instance.AddManager<TimerManager>(ManagerName.Timer);
        AppFacade.Instance.AddManager<NetworkManager>(ManagerName.Network);
        AppFacade.Instance.AddManager<ResourceManager>(ManagerName.Resource);
        AppFacade.Instance.AddManager<ThreadManager>(ManagerName.Thread);
        AppFacade.Instance.AddManager<ObjectPoolManager>(ManagerName.ObjectPool);
        AppFacade.Instance.AddManager<GameManager>(ManagerName.Game);
    }
}

这里初始化了各种管理器,我们可以根据具体需求进行改造和自定义。

3、LuaManager核心管理器

LuaManager这个管理器是必须的,掌管整个lua虚拟机的生命周期。它主要是加载lua库,加载lua脚本,启动lua虚拟机,执行Main.lua

4、AppConst常量定义

AppConst定义了一些常量。
其中AppConst.LuaBundleModelua代码AssetBundle模式。它会被赋值给LuaLoaderbeZip变量,在加载lua代码的时候,会根据beZip的值去读取lua文件,false则去search path中读取lua文件,否则从外部设置过来的bundle文件中读取lua文件。默认为true在Editor环境下,建议把AppConst.LuaBundleMode设为false,这样方便运行,否则写完lua代码需要生成AssetBundle才可以运行到。

#if UNITY_EDITOR
        public const bool LuaBundleMode = false;                    //Lua代码AssetBundle模式
#else
        public const bool LuaBundleMode = true;                    //Lua代码AssetBundle模式
#endif

5、Lua代码的读取

LuaLoaderLuaResLoader都继承LuaFileUtilslua代码会先从LuaFramework.Util.AppContentPath目录解压到LuaFramework.Util.DataPath目录中,lua文件列表信息记录在files.txt中,此文件也会拷贝过去。然后从LuaFramework.Util.DataPath目录中读取lua代码。

/// LuaFramework.Util.DataPath

/// <summary>
/// 应用程序内容路径
/// AppConst.AssetDir = "StreamingAssets"
/// </summary>
public static string AppContentPath() {
   
    string path = string.Empty;
    switch (Application.platform) {
   
        case RuntimePlatform.Android:
            path = "jar:file://" + Application.dataPath + "!/assets/";
        break;
        case RuntimePlatform.IPhonePlayer:
            path = Application.dataPath + "/Raw/";
        break;
        default:
            path = Application.dataPath + "/" + AppConst.AssetDir + "/";
        break;
    }
    return path;
}

/// <summary>
/// 取得数据存放目录
/// </summary>
public static string DataPath {
   
    get {
   
        string game = AppConst.AppName.ToLower();
        if (Application.isMobilePlatform) {
   
            return Application.persistentDataPath + "/" + game + "/";
        }
        if (AppConst.DebugMode) {
   
            return Application.dataPath + "/" + AppConst.AssetDir + "/";
        }
        if (Application.platform == RuntimePlatform.OSXEditor) {
   
            int i = Application.dataPath.LastIndexOf('/');
            return Application.dataPath.Substring(0, i + 1) + game + "/";
        }
        return "c:/" + game + "/";
    }
}

完了之后,再进行远程的更新检测,看看用不用热更lua代码,远程url就是AppConst.WebUrl,先下载files.txt,然后再读取lua文件列表进行下载。

6、GameManager游戏管理器

启动框架后,会创建GameManager游戏管理器,它负责检测lua逻辑代码的更新检测和加载(Main.lua是在LuaManager中执行的),我们可以在GameManagerDoFile我们自定义的lua脚本,比如Game.lua脚本。

7、C#中如何直接调用lua的某个方法

GameManager可以获取到LuaManager对象,通过LuaManager.CallFunction接口调用。
也可以用Util.CallMethod接口调用,两个接口的参数有差异,需要注意。

/// LuaManager.CallFunction接口
public object[] CallFunction(string funcName, params object[] args) {
   
      LuaFunction func = lua.GetFunction(funcName);
      if (func != null) {
   
          return func.LazyCall(args);
      }
      return null;
  }

/// Util.CallMethod接口
public static object[] CallMethod(string module, string func, params object[] args) {
   
    LuaManager luaMgr = AppFacade.Instance.GetManager<LuaManager>(ManagerName.Lua);
    if (luaMgr == null) return null;
    return luaMgr.CallFunction(module + "." + func, args);
}

8、lua中如何调用C#的方法

假设现在我们有一个C#

using UnityEngine;

public class MyTest : MonoBehaviour
{
   
    public int myNum;

    public void SayHello()
    {
   
        Debug.Log("Hello,I am MyTest,myNum: " + myNum);
    }

    public static void StaticFuncTest()
    {
   
        Debug.Log("I am StaticFuncTest");
    }
}

我们想在lua中访问这个MyTest类的函数。首先,我们需要在CustomSettings.cs中的customTypeList数组中添加类的注册:
_GT(typeof(MyTest)),
然后然后再单击菜单【Lua】-【Generate All】生成Wrap,生成完我们会看到一个MyTestWrap

接下来就可以在lua中访问了。(注意AppConst.LuaBundleMode的值要设为false,方便Editor环境下运行lua代码,否则需要先生成AssetBundle才能运行)

function Game.TestFunc()
    -- 静态方法访问
    MyTest.StaticFuncTest()
   
    local go = UnityEngine.GameObject("go")
    local myTest = go:AddComponent(typeof(MyTest))
    
    -- 成员变量
    myTest.myNum = 5
    -- 成员方法
    myTest:SayHello()
end

调用Game.TestFunc()

注意,静态方法、静态变量、成员变量、成员属性使用 “.” 来访问,比如上面的 myTest.myNum,成员函数使用 “:” 来访问,比如上面的 myTest:SayHello()

9、lua中如何使用协程

function fib(n)
    local a, b = 0, 1
    while n > 0 do
        a, b = b, a + b
        n = n - 1
    end

    return a
end

function CoFunc()
    print('Coroutine started')    
    for i = 0, 10, 1 do
        print(fib(i))                    
        coroutine.wait(0.1)                     
    end 
    print("current frameCount: "..Time.frameCount)
    coroutine.step()
    print("yield frameCount: "..Time.frameCount)

    local www = UnityEngine.WWW("http://www.baidu.com")
    coroutine.www(www)
    local s = tolua.tolstring(www.bytes)
    print(s:sub(1, 128))
    print('Coroutine ended')
end

调用

coroutine.start(CoFunc)


如果要stop协程,则需要这样

local co = coroutine.start(CoFunc)
coroutine.stop(co)

10、lua解析json

假设现在有这么一份json文件

{
   
    "glossary": {
   
        "title": "example glossary",
                "GlossDiv": {
   
            "title": "S",
                        "GlossList": {
   
                "GlossEntry": {
   
                    "ID": "SGML",
                                        "SortAs": "SGML",
                                        "GlossTerm": "Standard Generalized Mark up Language",
                                        "Acronym": "SGML",
                                        "Abbrev": "ISO 8879:1986",
                                        "GlossDef": {
   
                        "para": "A meta-markup language, used to create markup languages such as DocBook.",
                                                "GlossSeeAlso": ["GML", "XML"]
                    },
                                        "GlossSee": "markup"
                }
            }
        }
    }
}

假设我们已经把上面的json文件的内容保存到变量jsonStr字符串中,现在在lua中要解析它

local json = require 'cjson'

function Test(str)
    local data = json.decode(str)
    print(data.glossary.title)
    s = json.encode(data)
    print(s)
end

调用Test(jsonStr)

11、lua调用C#的托管

// c#传托管给lua
System.Action<string> cb = (s) => {
    Debug.Log(s); };
Util.CallMethod("Game", "TestCallBackFunc", cb);
-- lua调用C#的托管
function Game.TestCallBackFunc(cb)
    if nil ~= cb then
       System.Delegate.DynamicInvoke(cb,"Hello, I am lua, I call Delegate")
    end
end

12、lua通过反射调用C#

有时候,我们没有把我们的C#类生成Wrap,但是又需要在lua中调用,这个时候,可以通过反射来调用。
假设我们有一个C#类:MyClass

// MyClass.cs
public sealed class MyClass
{
   
    //字段
    public string myName;
    //属性
    public int myAge {
    get; set; }
	
	//静态方法
    public static void SayHello()
    {
   
        Debug.Log("Hello, I am MyClass's static func: SayHello");
    }

    public void SayNum(int n)
    {
   
        Debug.Log("SayNum: " + n);
    }

    public void SayInfo()
    {
   
        Debug.Log("SayInfo, myName: " + myName + ",myAge: " + myAge);
    }
}

lua

-- Game.lua
function Game.TestReflection()
    require 'tolua.reflection'
    tolua.loadassembly('Assembly-CSharp')
    local BindingFlags = require 'System.Reflection.BindingFlags'

    local t = typeof('MyClass')
    -- 调用静态方法
    local func = tolua.getmethod(t, 'SayHello')
    func:Call()
    func:Destroy()
    func = nil

    -- 实例化
    local obj = tolua.createinstance(t)
    -- 字段
    local field = tolua.getfield(t, 'myName')
    -- 字段Set
    field:Set(obj, "linxinfa")
    -- 字段Get
    print('myName: ' .. field:Get(obj))
	field:Destroy()
	
    -- 属性
    local property = tolua.getproperty(t, 'myAge')
    -- 属性Set
    property:Set(obj, 29, null)
    -- 属性Get
    print('myAge: ' .. property:Get(obj, null))
    property:Destroy()
    
    --public成员方法SayNum
    func = tolua.getmethod(t, 'SayNum', typeof('System.Int32'))
    func:Call(obj, 666)
	func:Destroy()
	
    --public成员方法SayInfo
    func = tolua.getmethod(t, 'SayInfo')
    func:Call(obj)
    func:Destroy()
end

调用Game.TestReflection()

13、nil和null

nillua对象的空,null表示c#对象的空。假设我们在c#中有一个GameObject对象传递给了lua的对象a,接下来我们把这个GameObject对象Destroy了,并在c#中把这个GameObject对象赋值为null,此时lua中的对象a并不会等于nil
如果要在lua中判断一个对象是否为空,安全的做法是同时判断nilnull

-- lua中对象判空
function IsNilOrNull(o)
	return nil == o or null == o
end

14、获取今天是星期几

-- 1是周日,2是周一,以此类推
function GetTodayWeek()
	local t = os.date("*t", math.floor(os.time()))
	return t.wday
end

15、获取今天的年月日

方法一

function GetTodayYMD()
	local t = os.date("*t", math.floor(os.time()))
	return t.year .. "/" .. t.month .. "/" .. t.day
end

方法二

function GetTodayYMD()
	-- 如果要显示时分秒,则用"%H:%M:%S"
	return os.date("%Y/%m%d", math.floor(os.time()))
end

16、字符串分割

-- 参数str是你的字符串,比如"小明|小红|小刚"
-- 参数sep是分隔符,比如"|"
-- 返回值为{
   "小明","小红","小刚"}
function SplitString(str, sep)
	local sep = sep or " "
	local result = {
   }
	local pattern = string.format("([^%s]+)", sep)
	string.gsub(s, pattern, function(c) result[#result + 1] = c end)
	return result 
end

17、大数字加逗号分割(数字会转成字符串)

-- 参数num是数字,如3428439,转换结果"3,428,439"
function FormatNumStrWithComma(num)
	local numstr = tostring(num)
	local strlen = string.len(numstr)
	local splitStrArr = {
   }
	for i = strlen, 1, -3 do
		local beginIndex = (i - 2 >= 1) and (i - 2) or 1
		table.insert(splitStrArr, string.sub(numstr, beginIndex, i))
	end
	local cnt = #splitStrArr
	local result = ""
	for i = cnt, 1, -1 do
		if i == cnt then
			result = result .. splitStrArr[i]
		else
			result = result .. "," .. splitStrArr[i]
		end
	end
	return result
end

18、通过组件名字添加组件

-- 缓存
local name2Type = {
   }
-- 参数gameObject物体对象
-- 参数componentName,组件名字,字符串
function AddComponent(gameObject, componentName)
	local component = gameObject:GetComponent(componentName)
	if nil ~= component then return component end

	local componentType = name2Type[componentName]
	if nil == componentType then
		componentType  = System.Type.GetType(componentName)
		if nil == componentType then
			print("AddComponent Error: " .. componentName)
			return nil
		else
			name2Type[componentName] = componentType 
		end
	end
	return gameObject:AddComponent(componentType)
end

19、深拷贝

lua中的table是引用类型,有时候我们为了不破坏原有的table,可能要用到深拷贝

function DeepCopy(t)
	if nil == t then return nil end
	local result = ()
	for k, v in pairs(t) do
		if "table" == type(v) then
			result[k] = DeepCopy(v)
		else
			result[k] = v
		end
	end
	return result
end

20、四舍五入

function Round(fnum)
	return math.floor(fnum + 0.5)
end

21、检测字符串是否含有中文

-- 需要把C#的System.Text.RegularExpressions.Regex生成Wrap类
function CheckIfStrContainChinese(str)
	return System.Text.RegularExpressions.Regex.IsMatch(str, "[\\u4e00-\\u9fa5]")
end

22、数字的位操作get、set

-- 通过索引获取数字的某一位,index从1开始
function GetBitByIndex(num, index)
    if nil == index then
        print("LuaUtil.GetBitByIndex Error, nil == index")
        return 0
    end
    local b = bit32.lshift(1,(index - 1))
    if nil == b then
        print("LuaUtil.GetBitByIndex Error, nil == b")
        return 0
    end
    return bit32.band(num, b)
end

-- 设置数字的某个位为某个值,num:目标数字,index:第几位,从1开始,v:要设置成的值,01
function SetBitByIndex(num, index, v)
    local b = bit32.lshift(1,(index - 1))
    if v > 0 then
        num = bit32.bor(num, b)
    else
        b = bit32.bnot(b)
        num = bit32.band(num, b)
    end
    return num
end

23、限制字符长度,超过进行截断

有时候,字符串过长需要截断显示,比如有一个昵称叫“我的名字特别长一行显示不下”,需求上限制最多显示5个字,超过的部分以…替代,即"我的名字特…"。首先要计算含有中文的字符串长度,然后再进行截断

-- 含有中文的字符串长度
function StrRealLen(str)
    if str == nil then return 0 end
    local count = 0
    local i = 1
    while (i < #str) do
        local curByte = string.byte(str, i)
        local byteCount = 1
        if curByte >= 0 and curByte <= 127 then
            byteCount = 1
        elseif curByte >= 192 and curByte <= 223 then
            byteCount = 2
        elseif curByte >= 224 and curByte <= 239 then
            byteCount = 3
        elseif curByte >= 240 and curByte <= 247 then
            byteCount = 4
        end
        local char = string.sub(str, i, i + byteCount - 1)
        i = i + byteCount
        count = count + 1
    end
    return count
end

-- 限制字符长度(多少个字)
-- 参数str,为字符串
-- 参数limit为限制的字数,如8
-- 参数extra为当超过字数时,在尾部显示的字符串,比如"..."
function LimitedStr(str, limit, extra)
    limit = limit or 8
    extra = extra or ""
    local text = ""
	-- 含有中文的字符串长度
    if StrRealLen(str) > limit then
        text = LuaUtil.sub_chars(str, limit) .. "..." .. extra
    else
        text = str .. extra
    end
    return text
end

24、判断字符串A是否已某个字符串B开头

-- 判断字符串str是否是以某个字符串start开头
function StringStartsWith(str, start)
    return string.sub(str, 1, string.len(start)) == start
end

五、热更lua与资源

1、热更lua

app整包的时候,备份一份lua全量文件,后面打lua增量包的时候,根据文件差异进行比对,新增和差异的lua文件打成一个lua_update.bundle,放在一个update文件夹中,并压缩成zip,放到服务器端,客户端通过https下载增量包并解压到Application.persistentDataPath目录。游戏加载lua文件的时候,优先从update文件夹中的lua_update.bundle中查找lua脚本。

2、热更资源热更资源

做个编辑器工具,指定某个或某些资源文件(预设、音频、动画、材质等),打成多个assetbundle,放在一个update文件夹中,并压缩成一个zip,放到服务器端,客户端通过https下载增量包并解压到Application.persistentDataPath目录。
游戏加载资源文件的时候,优先从update文件夹中查找对应的资源文件。

3、真机热更资源存放路径

persistentDataPath/res/
                      ├──/update/
                      │       ├──/lua/   
                      │       │    └──lua_update.bundle            #lua增量bundle
                      │       ├──/res/
                      │       │    ├──aaa.bundle                   #预设aaa的bundle
                      │       │    ├──bbb.bundle                   #音频bbb的bundle
                      │       │    └──...                          #其他各种格式的资源bundle
                      │       └──/cfg/
                      │            ├──cfg.bundle                   #配置增量bundle
                      │            └──...                          #其他文本或二进制文件增量bundle
                      ├──out_put.log                               #游戏日志
                      └──...

关于persistentDataPath,可以参见我这篇博客:https://blog.csdn.net/linxinfa/article/details/51679528


转载:https://blog.csdn.net/linxinfa/article/details/88246345
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场