本文转载自秦元培博客:blog.csdn.net/qinyuanpei
一、什么是Lua?
Lua 是一个小巧的脚本语言,巴西里约热内卢天主教大学里的一个研究小组于1993年开发,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。一个完整的Lua解释器不过200k,在目前所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。相比Python和Per的内核,Lua的内核小于120KB,而Python的内核大约860KB,Perl的内核大约1.1MB。Lua语言支持面向对象编程和函数式编程,它提供了一个通用类型的表table,可以实现数组、哈希表、集合、对象的功能。Lua支持协同进程机制。作为一门可扩展的语言,Lua提供简单而稳定的交互接口,如Lua和C程序可通过一个堆栈交换数据,这使得Lua语言可以快速地和其它语言实现整合。总体来说,Lua语言具备以下优点:(1)语言优美、轻巧 (2)性能优良、速度快 (3)可扩展性强。正因为Lua语言具备了这样的特点,使得它能和游戏开发领域的需求完美地结合起来,因为我们需要这样的一门语言,它能够和C/C++进行完美地交互,因为我们需要它对底层进行封装。它需要足够地简单,因为我们需要简单、灵活、快速地编写代码。那么显然Lua就是我们一直在寻找地这种语言。
二、Lua可以做什么?
尽管博主已经告诉了大家太多的关于Lua语言的优秀特性,相信大家仍然会对Lua语言的能力存在怀疑。大家或许会想,Lua到底可以做什么呢?在《Lua游戏开发》一书中作者已经告诉了我们答案:
1、编辑游戏的用户界面
2、定义、存储和管理基础游戏数据
3、管理实时游戏事件
4、创建和维护开发者友好的游戏存储和载入系统
5、编写游戏的人工智能系统
6、创建功能原型,可以之后用高性能语言移植
这时候我们似乎觉得Lua语言在某种程度上就是专门为游戏开发而诞生的,因为它将大量的优秀特性全部指向了游戏开发领域,因此Lua语言走进走进游戏开发领域变得顺利成章,那么,让我们接着往下看吧,Lua在游戏开发领域有那些成熟的案例吧。
三、哪些游戏使用了Lua?
1、魔兽世界
如果提到Lua在游戏领域中第一次崭露头角,我们就不能不说《魔兽世界》这款游戏,由于《魔兽世界》在其客户端中使用了Lua,使得Lua在游戏领域的作用第一次被展示出来,Lua语言因此在游戏开发领域成名。Lua语言的虚拟机很轻巧,可以很容易地嵌入到客户端程序中。如果需要更新客户端,只需要更新脚本程序即可,无需重新编译整个客户端。这样地优点使得Lua在游戏开发领域一战成名,可以说是《魔兽世界》为游戏开发领域带来了这样激动人心的伟大语言,作为Lua在游戏领域攻城略地的尝试,《魔兽世界》功不可没。
2、大话西游2
如果说《魔兽世界》开辟Lua在国外游戏领域地战场,那么网易的《大话西游2》无疑是开启了国内游戏制作公司使用Lua的先河。2002年网易开发《大话西游2》时,决定在客户端内嵌入新的脚本语言,因为当时使用的微软JScript存在较多Bug、维护不便、兼容性差。当时该项目技术负责人云风吸取了《大话西游1》时外挂泛滥的教训,决定选择一个新的语言,这样既能摆脱对JScript的依赖,又能有效地打击外挂制作者,权衡再三,最终选择了Lua 4.0。后来《大话西游2》在市场上取得了成功,国内游戏开发行业纷纷受此影响采用Lua,可以说是网易Lua走进了国内开发者的视野,不过到今天为止,Lua在国内仍然是一门较为小众的语言,从《大话西游2》引领国内开发者将视角转向Lua到今天将近10余年地时间,此中缘由,只有大家自己去想个清楚啦。
3、古剑奇谭
《古剑奇谭》系列游戏是由上海烛龙信息科技有限公司研发的大型3DRPG单机游戏。游戏设定源自于《山海经》,故事则以武侠和仙侠为创作题材,以中国神话时代为背景,讲述了中国古代侠骨柔情的仙侠文化。《古剑奇谭》系列游戏初代作品与二代作品采用的是不同的游戏引擎和不同的战斗模式,尽管如此,我们依然能从中找到一个共同点,那就是在初代作品和二代作品中都毫无例外的使Lua作为游戏地脚本语言。例如下面是《古剑奇谭》红叶湖迷宫场景的Lua脚本节选:
[plain] view plain copy
- require("Necessary")
- require("StoryUtility")
- require("BigMap")
- require("Script_DLC4")
- --------------以下为初始化函数-------------
- function OnEnterLevel()
- if GetStoryVersion() == 2 then
- OnDLCEnterLevelM01()
- else
- if GetMainStory() == 10100 then
- callTaskFunction("story10100")
- elseif GetMainStory() == 161900 then
- callTaskFunction("story161900")
- end
- if gValue.MK == 1 then
- showNPC("NPC 06", false)
- showNPC("NPC 07", false)
- enableTrigger("Tri_MK",false)
- elseif gValue.MK >1 then
- showNPC("NPC 04", false)
- showNPC("NPC 05", false)
- showNPC("NPC 06", false)
- showNPC("NPC 07", false)
- enableTrigger("Tri_MK",false)
- enableTrigger("Tri_MK 02",false)
- end
4、仙剑奇侠传
既然提到了古剑奇谭,怎么能不提仙剑奇侠传呢?虽然和古剑奇谭初代作品发布时间仅仅相差一年的《仙剑奇侠传五》市场反响并没有像游戏制作方所预料地那样成功,不过这部作品值得称赞地地方还是蛮多的,因为进步总是要比缺点多的嘛,毕竟时代在进步,我们不能总是拿仙剑初代作品的高度去要求后续作品,因为我们已经不再是那个年龄的人,而仙剑依然要不断地突破自身、大胆创新和进取。好了,我们暂时先感慨到这里,仙剑四、仙剑五以及仙剑五前传都使用了RenderWare引擎,可能唯一的不同就是仙剑五和仙剑五前传都使用了Lua吧,下面同样是一段从游戏中提取的脚本:
[plain] view plain copy
- function baoxiang(id,npcID)
- player.Control(0)
- pid=player.GetMainPlayer()
- player.SetAnim(pid,203)
- global.Print(id)
- global.Wait(1)
- y=flag.GetValue(15093)
- ---------江洋大盗称号获得-------------
- jyd=flag.GetValue(15255)
- jyd=jyd+1
- flag.SetValue(15255,jyd)
- global.Print(jyd)
- global.AddTimer(0.5,13279)
- -----------------------------------------
- if id~=17711 then
- npc.SetAnim(npcID,501)
- global.Wait(1)
- end
5、金庸群侠传Lua复刻版
四、带你走进Lua的世界
最后想和大家分享是Lua语言编程的一个简单的示例,因为博主觉得以后做游戏用脚本语言的场景会越来越多,所以能学会一门脚本语言能为你的游戏开发之路增色不少。因为博主刚开始学,所以脚本中有不足之处,希望大家能谅解,在学校的时间一天天地在减少,博主希望能和大家共同度过最后的这段时间。博主使用的是Lua5.2,使用的Sublime Text2作为脚本编辑器配合LuaDev插件进行编程的,如果大家想用懒惰点的办法,可以使用Lua for Windows这个集成环境。好了,下面开始吧,作为第一个Lua程序,我们直接给出代码,具体的语法及API大家可以自己去查阅。
[plain] view plain copy
- --while-do示例代码
- myValue=10
- while(myValue <= 20) do
- print(myValue)
- myValue=myValue+1
- end
- --sample table && for-do示例代码
- myTables={"Item0","Item1","Item2","Item3"}
- for i=1,table.maxn(myTables) do
- print(myTables[i])
- end
- --complex table示例代码
- myTables={}
- myTables["A"]="ItemA"
- myTables["B"]="ItemA"
- myTables["C"]="ItemA"
- myTables["D"]="ItemA"
- print(myTables["A"])--"ItemA"
- --function示例代码
- function fib(n)
- if(n<2) then
- return n
- else
- return fib(n-1)+fib(n-2)
- end
- end
- --math示例代码
- maxValue=math.max(12,23,56,18,10)--56
- minValue=math.min(25,34,12,75,8)--8
- print(maxValue-minValue)--48
- --字符串演示
- myString="Hello this is the cool program language called Lua";
- print(string.find(myString,"Lua"))--48,50
- --io演示
- io.write("Hello I get a powerful program language called Lua \n")
- io.write(string.format("This Lua is %s and now is %s \n",_VERSION,os.date()))
一、Lua堆栈
如果我们想要理解Lua语言与其它语言交互的实质,我们首先就要理解Lua堆栈。简单来说,Lua语言之所以能和C/C++进行交互,主要是因为存在这样一个无处不在的虚拟栈。栈的特点是先进后出,在Lua语言中,Lua堆栈是一种索引可以是正数或者负数的结构,并规定正数1永远表示栈底,负数-1永远表示栈顶。换句话说呢,在不知道栈大小的情况下,我们可以通过索引-1取得栈底元素、通过索引1取得栈顶元素。下面呢,我们通过一个实例来加深我们对于这段话的理解:
[cpp] view plain copy
- #include
- extern "C" {
- #include "lua.h"
- #include "lualib.h"
- #include "lauxlib.h"
- }
- using namespace std;
- int main()
- {
- //创建Lua环境
- lua_State* L=lua_open();
- //打开Lua标准库,常用的标准库有luaopen_base、luaopen_package、luaopen_table、luaopen_io、
- //luaopen_os、luaopen_string、luaopen_math、luaopen_debug
- luaL_openlibs(L);
- //压入一个数字20
- lua_pushnumber(L,20);
- //压入一个数字15
- lua_pushnumber(L,15);
- //压入一个字符串Lua
- lua_pushstring(L,"Lua");
- //压入一个字符串C
- lua_pushstring(L,"C");
- //获取栈元素个数
- int n=lua_gettop(L);
- //遍历栈中每个元素
- for(int i=1;i<=n;i++)
- {
- cout << lua_tostring(L ,i) << endl;
- }
- return 0;
- }
在上面的这段代码中,我们可以可以看到我们首先创建了一个lua_State类型的变量L,我们可以将它理解成一个Lua运行环境的上下文(Context),这里我们在Lua堆栈中压入了四个元素:20、15、"Lua"、"C"然后将其输出,如果大家理解了Lua堆栈中的索引,那么最终输出的结果应该是:20、15、"Lua"、"C",因为索引1始终指向栈底,最先入栈的元素会处于栈底。因此当我们按照递增的索引顺序来输出栈中的元素的话,实际上是自下而上输出,这样我们就能得到这样的结果了。
好了,如果这段代码没有什么问题的话,接下来我们来讲解Lua为C/C++提供的接口,它们均被定义在lua.h文件中。Lua提供的C/C++接口大部分与栈操作有关,因此深入理解Lua堆栈是学习Lua语言的重点和难点。通过数据结构的知识,我们可以知道栈有出栈和入栈两种基本操作,Lua提供的C API中入栈可以通过push系列的方法来实现,如下图所示:
而出栈或者说查询的方法则可以通过to系列的方法来实现,如下图:
这两部分是学习Lua语言一定要去了解的内容,因为以后如果需要我们将Lua整合到其它项目中这些内容,这些东西可以说是原理性、核心性的东西。好了,下面我们利用这里的API对一个示例代码进行改造,这里加入了对栈中元素类型的判断:
[cpp] view plain copy
- #include
- extern "C" {
- #include "lua.h"
- #include "lualib.h"
- #include "lauxlib.h"
- }
- using namespace std;
- int main()
- {
- //创建Lua环境
- lua_State* L=lua_open();
- //打开Lua标准库,常用的标准库有luaopen_base、luaopen_package、luaopen_table、luaopen_io、
- //luaopen_os、luaopen_string、luaopen_math、luaopen_debug
- luaL_openlibs(L);
- //压入一个数字20
- lua_pushnumber(L,20);
- //压入一个字符串15
- lua_pushnumber(L,15);
- //压入一个字符串Lua
- lua_pushstring(L,"Lua");
- //压入一个字符串C
- lua_pushstring(L,"C");
- //获取栈中元素个数
- int n=lua_gettop(L);
- //遍历栈中每个元素
- for(int i=1;i<=n;i++)
- {
- //类型判断
- switch(lua_type(L,i))
- {
- case LUA_TSTRING:
- cout << "This value's type is string" << endl;
- break;
- case LUA_TNUMBER:
- cout << "This value's type is number" << endl;
- break;
- }
- //输出值
- cout << lua_tostring(L ,i) << endl;
- }
- //释放Lua
- lua_close(L);
- }
二、Lua与C++交互
Lua与C++的交互从宿主语言的选择划分上可以分为C++调用Lua和Lua调用C++两中类型:
1、C++调用Lua
使用C++调用Lua时我们可以直接利用C++中的Lua环境来直接Lua脚本,例如我们在外部定义了一个lua脚本文件,我们现在需要使用C++来访问这个脚本该怎么做呢?在这里我们可以使用luaL_loadfile()、luaL_dofile()这两个方法个方法来实现,其区别是前者仅加载脚本文件而后者会在加载的同时调用脚本文件。我们一起来看下面的代码:
[cpp] view plain copy
- #include
- using namespace std;
- #include
- extern "C" {
- #include "lua.h"
- #include "lualib.h"
- #include "lauxlib.h"
- }
- using namespace std;
- int main()
- {
- //创建Lua环境
- lua_State* L=luaL_newstate();
- //打开Lua标准库,常用的标准库有luaopen_base、luaopen_package、luaopen_table、luaopen_io、
- //luaopen_os、luaopen_string、luaopen_math、luaopen_debug
- luaL_openlibs(L);
- //下面的代码可以用luaL_dofile()来代替
- //加载Lua脚本
- luaL_loadfile(L,"script.lua");
- //运行Lua脚本
- lua_pcall(L,0,0,0);
- //将变量arg1压入栈顶
- lua_getglobal(L,"arg1");
- //将变量arg2压入栈顶
- lua_getglobal(L,"arg2");
- //读取arg1、arg2的值
- int arg1=lua_tonumber(L,-1);
- int arg2=lua_tonumber(L,-2);
- //输出Lua脚本中的两个变量
- cout <<"arg1="<
- cout <<"arg2="<
- //将函数printf压入栈顶
- lua_getglobal(L,"printf");
- //调用printf()方法
- lua_pcall(L,0,0,0);
- //将函数sum压入栈顶
- lua_getglobal(L,"sum");
- //传入参数
- lua_pushinteger(L,15);
- lua_pushinteger(L,25);
- //调用printf()方法
- lua_pcall(L,2,1,0);//这里有2个参数、1个返回值
- //输出求和结果
- cout <<"sum="<
- //将表table压入栈顶
- lua_getglobal(L,"table");
- //获取表
- lua_gettable(L,-1);
- //输出表中第一个元素
- cout <<"table.a="<
- }
在这段代码中我们调用了一个外部的文件script.lua。这是一个Lua脚本文件,在调试阶段,我们需要将其放置在和C++项目源文件同级的目录下,而在正式运行阶段,我们只需要将其和最终的可执行文件放在同一个目录下就好了。下面是脚本代码:
[cpp] view plain copy
- --在Lua中定义两个变量
- arg1=15
- arg2=20
- --在Lua中定义一个表
- table=
- {
- a=25,
- b=30
- }
- --在Lua中定义一个求和的方法
- function sum(a,b)
- return a+b
- end
- --在Lua中定义一个输出的方法
- function printf()
- print("This is a function declared in Lua")
- end
我们注意到在脚本文件中我们定义了一些变量和方法,在C++代码中我们首先用lua_getglobal()方法来讲Lua脚本中的变量或函数压入栈顶,这样我们就可以使用相关的to系列方法去获取它们,由于每次执行lua_getglobal()都是在栈顶,因为我们使用索引值-1来获取栈顶的元素。C++可以调用Lua中的方法,第一步和普通的变量相同,是将Lua中定义的方法压入栈顶,因为只有压入栈中,我们才能够使用这个方法,接下来,我们需要通过push系列的方法为栈中的方法传入参数,在完成参数传入后,我们可以使用一个lua_pcall()的方法来执行栈中的方法,它有四个参数,第一个参数是Lua环境状态Lua_State,第二个参数是要传入的参数个数,第三个参数是要返回的值的数目,第四个参数一般默认为0。由于Lua支持返回多个结果,因此,我们可以充分利用Lua的这一特点来返回多个值。执行该方法后,其结果会被压入栈顶,所以我们可以索引值-1来获取函数的结果。如果函数有多个返回值,则按照函数中定义的return 顺序,依次入栈,索引值-1代表最后一个返回值。好了,这就是C++调用Lua的具体实现了。
2、Lua调用C++
首先我们在C++中定义一个方法,该方法必须以Lua_State作为参数,返回值类型为int,表示要返回的值的数目。
[cpp] view plain copy
- static int AverageAndSum(lua_State *L)
- {
- //返回栈中元素的个数
- int n = lua_gettop(L);
- //存储各元素之和
- double sum = 0;
- for (int i = 1; i <= n; i++)
- {
- //参数类型处理
- if (!lua_isnumber(L, i))
- {
- //传入错误信息
- lua_pushstring(L, "Incorrect argument to 'average'");
- lua_error(L);
- }
- sum += lua_tonumber(L, i);
- }
- //传入平均值
- lua_pushnumber(L, sum / n);
- //传入和
- lua_pushnumber(L, sum);
- //返回值的个数,这里为2
- return 2;
- }
接下来我们在C++中使用lua_register()方法完成对该方法的注册
[cpp] view plain copy
- lua_register(L, "AverageAndSum", AverageAndSum);
这样我们就可以在Lua环境中使用这个方法啦,前提是定义必须在执行代码之前完成,我们在Lua脚本文件下加入对该方法的调用:
[plain] view plain copy
- --在Lua中调用C++中定义并且注册的方法
- average,sum=AverageAndSum(20,52,75,14)
- print("Average=".average)
- print("Sum=".sum)
如果我们需要在C++中查看该方法调用的结果,那么这个在C++中调用Lua是一样的。好了,C++和Lua的交互终于讲完了,被这块的代码纠结了好几天,这下总算是搞明白了。当然这只是对原理的一种学习和理解啦,如果希望更好的使用Lua调用C++,建议了解这几个项目:
LuaPlus、LuaBind。这样相信大家对于C++中的方法如何在Lua中绑定会有更好的认识吧!
三、Lua与C#交互
既然我们已经知道了C++是怎样和Lua完成交互的,理论上我们可以通过编写dll的方式将前面完成的工作继续在C#中运行,可是这样做我们需要花费大量时间在三种语言之间纠结,因为这样会增加调试的难度。之前有个做coco2dx的朋友抱怨要在C++、Javascript、Lua之间来回跑,我当时没觉得有什么,因为我最困难的时候就是C#和Java项目混合的情形,如今我算是深有体会了啊,这算是报应吗?哈哈,好了,不说这个了,好在C#与Lua的交互目方面前已经有了较好的解决方案,在开源社区我们可以找到很多的支持在C#中调用Lua的工具库,博主这里向大家推荐的是LuaInterface这个开源项目,这个开源项目我找到了两个地址:
1、https://github.com/Jakosa/LuaInterface
2、http://code.google.com/p/luainterface
博主个人感觉这应该是同一个项目,因为两个项目的源代码是一样的,不过从Github上下载的项目在使用的时候会报错,估计是我电脑里的Lua版本和它项目里所用的Lua的版本不一致造成的吧。下面的这个项目是可以使用的,博主这里写了一个简单的示例:
[csharp] view plain copy
- //------------------------------------------------------------------------------
- //
- // 这是一个用以演示LuaInterface的简单程序,通过LuaInterface我们可以实现在C#与Lua的
- // 的相互通信。Lua是一个轻巧而高效的语言,它可以和任何语言混合使用。Lua语言最初并不是
- // 为游戏开发而诞生,却是因为游戏开发而成名。目前,在世界上有大量的游戏使用了Lua作为它
- // 的脚本语言。如图Unity使用了C#作为它的语言,Lua在游戏开发领域发挥着不可忽视的重要作
- // 用。使用LuaInterface的方法如下:
- // 1.C#
- // 注册Lua中可调用方法:
- // mLua.RegisterFunction(Lua调用方法名, 类, 类.GetMethod(C#方法名));
- // 注:C#不要使用方法级泛型,即 void Fun
(string str);,如果使用,系统自动判定T为第一个参数的类型。 - // 加载Lua代码
- // mLua.DoString(Lua代码);
- // mLua.DoFile(Lua文件绝对路径);
- // 调用Lua方法
- // mLua.GetFunction(Lua方法).Call(参数); 注:此处参数不要传递dynamic类型的类,否则Lua中无法获取属性值
- // 2.Lua
- // 调用C#方法时需要先注册注册后按照Lua方法处理
- //
- //------------------------------------------------------------------------------
- using System;
- using LuaInterface;
- namespace LuaExample
- {
- public class LuaScript
- {
- //定义LuaFile属性以便于从外部调用一个Lua脚本
- private string mLuaFile;
- public string LuaFile {
- get {
- return mLuaFile;
- }
- set {
- mLuaFile = value;
- }
- }
- //Lua虚拟机
- private Lua mLua;
- //构造函数
- public LuaScript ()
- {
- //初始化Lua虚拟机
- mLua=new Lua();
- //注册Printf方法
- mLua.RegisterFunction("Printf",this,this.GetType().GetMethod("Printf"));
- }
- //定义一个C#方法供Lua使用
- public void Printf(string str)
- {
- Console.WriteLine("This Method is Invoked by Lua:" + str);
- }
- //在C#中调用Lua方法
- public void DoFile()
- {
- if(mLuaFile!="")
- //执行Lua脚本中的代码
- mLua.DoFile(mLuaFile);
- }
- //在C#中调用Lau方法
- public void DoString()
- {
- //以字符串形式定义的Lua脚本
- string mFuncString="function Add(a,b) io.write(a+b) end";
- //在Lua中定义该方法
- mLua.DoString(mFuncString);
- //调用该方法
- mLua.GetFunction("Add").Call(4,8);
- }
- //在Lua中调用C#脚本
- public void Invoke()
- {
- //调用注册的Printf方法
- mLua.GetFunction("Printf").Call("Hello Lua");
- }
- }
- }
接下来我们编写一个主类来调用这个类:
[csharp] view plain copy
- using System;
- using LuaInterface;
- namespace LuaExample
- {
- class MainClass
- {
- public static void Main (string[] args)
- {
- //实例化LuaSxript
- LuaScript mLua=new LuaScript();
- //设置LuaFile
- mLua.LuaFile="D:\\test.lua";
- //调用字符串中定义的Lua方法
- mLua.DoString();
- //为美观考虑增加一个空行
- Console.WriteLine();
- //执行Lua文件中定义的脚本
- mLua.DoFile();
- //调用C#中定义的方法
- mLua.Invoke();
- }
- }
- }
好了,C#与Lua的交互解决了,更多的内容期待着大家自行到该项目源代码中去寻找。好了,先这样吧!
四、Lua与Java交互
和C#类似的一点是在Java中我们可以使用JNI来调用C++代码,因此理论上Lua和Java应该是可以通过JNI来交互的,这块博主目前没有展开研究。这里只给大家推荐以下工具库:
1、LuaJava
2、luaj
好了,下面我们就来一起学习在Unity3D项目中如何使用Lua语言吧,Unity3D基于Mono虚拟机,所以理论上.NET的类库是可以直接在Unity3D中使用的。可是考虑到Unity3D跨平台的需要,我们选择的工具必须在各个平台获得良好的支持。在前文中提到的LuaInterface理论上是可以在Unity3D中使用的,可是由于IOS不支持反射机制,所以这个类库我们无法直接在Unity3D中使用的。在开源社区中,博主发现了云风团队的阿楠开发的UniLua,云风团队的风云是一个在国内游戏开发领域比较著名的人物,那么我们今天就来选择UniLua来作为我们的一个工具库吧,该项目是一个开源项目,参考了LuaInterface项目,不过在处理反射这个问题上使用了新的方法,所以目前可以完美地支持各个平台。相信大家有了前面两篇文章的基础,现在已经可以从容地面对Lua API了吧。好了,我们,现在来创建一个简单地Unity项目:
第一步是下载UniLua:http://github.com/xebecnan/UniLua。将UniLua引用到项目中有两种方法,一种是将该项目中的UniLua编译成dll然后在Unity项目中使用,一种是将该项目中的UniLua直接复制到Unity 项目中,我们这里使用第二种方法,因为博主比较懒,呵呵。将UniLua的命名空间添加到我们项目中,我们就可以开始动手写程序了。不过这里,博主想说的是Mono可能会导致的一个错误,估计是阿楠在写这个项目的时候使用了.NET4.0以上的版本,而在.NET4.0以上的版本是支持默认参数的构造函数的。可是由于Mono默认使用的是.NET3.5,所以在编译项目的时候就会报错,我们可以通过Project->Assembly-CSharp->Build->General将.NET的目标框架设为4.0,这样就可以解决这个问题了。好了,下面我们开始写代码啦,首先创建一个InvokeScript.cs的脚本:
[csharp] view plain copy
- using UnityEngine;
- using System.Collections;
- using UniLua;
- public class InvokeScript : MonoBehaviour {
- //Lua脚本文件,我们将在C#调用该脚本
- public TextAsset LuaFile;
- //Lua虚拟机
- private ILuaState mLua;
- void Start()
- {
- //初始化Lua虚拟机
- mLua=LuaAPI.NewState();
- //加载Lua标准库
- mLua.L_OpenLibs();
- //引用一个静态地C#库
- mLua.L_RequireF(CSharpLib.CLASSNAME,CSharpLib.InitLib,false);
- //执行Lua脚本
- mLua.L_DoString(LuaFile.text);
- }
- void OnGUI()
- {
- if(GUILayout.Button("调用Lua脚本",GUILayout.Height(30)))
- {
- InvokeLua();
- }
- if(GUILayout.Button("调用C#脚本",GUILayout.Height(30)))
- {
- InvokeCSharp();
- }
- }
- #region 调用C#脚本
- void InvokeCSharp()
- {
- //获取方法并传入参数
- mLua.GetGlobal("SumAndSub");
- mLua.PushInteger(12);
- mLua.PushInteger(8);
- mLua.PCall(2,4,0);
- }
- #endregion
- #region 调用Lua脚本
- void InvokeLua()
- {
- //获取Lua脚本中的arg1参数
- mLua.GetGlobal("arg1");
- //输出arg1
- Debug.Log("Lua脚本中的变量arg1="+mLua.L_ToString(-1));
- //获取Lua脚本中的arg2参数
- mLua.GetGlobal("arg2");
- //输出arg2
- Debug.Log("Lua脚本中的变量arg2="+mLua.L_ToString(-1));
- //获取Lua脚本中的Printf方法
- mLua.GetGlobal("Printf");
- //调用Lua脚本中的Printf方法
- mLua.PCall(0,0,0);
- //获取Lua脚本中的Sum方法
- mLua.GetGlobal("Sum");
- //传入参数12和25
- mLua.PushInteger(12);
- mLua.PushInteger(25);
- //调用此方法
- mLua.PCall(2,3,0);
- //获取传入的两个参数及求和结果
- int a=mLua.ToInteger(-3);
- int b=mLua.ToInteger(-2);
- int sum=mLua.ToInteger(-1);
- //输出
- Debug.Log("调用Lua脚本中的Sum方法:"+a+"+"+b+"="+sum);
- }
- #endregion
- }
[csharp] view plain copy
在这段脚本中,我们首先初始化了Lua环境,这一点和我们在C++中使用Lua是一样的,因为UniLua在设计API的时候在命名上和LuaAPI保持了高度的一致,如果你对Lua API足够熟悉的话,那么现在这一切对你而言应该会很简单的。接下来,我们通过Require的形式引入了我们编写的一个C#库,它是一个静态库,目的是封装C#方法以便于Lua脚本来调用,这一部分我们稍后会讲到。接下来,我们通过Unity的AssetText加载了一个Lua脚本文件,该脚本的文件的扩展名是.txt,因为我们只需要Lua脚本的内容。在脚本中我们定义了两个方法InvokeLua和InvokeSharp来分别调用Lua脚本和C#脚本。好了,接下来,我们重点来讲Lua调用C#脚本的这部分,因为UniLua在调用函数这块儿和LuaInterface不太一样,所以我们不能再用原来的先注册C#方法然后再像Lua脚本方法一样,不过博主觉得这里的原理是一样的,不过UniLua提供了更好的方法绑定机制,我们来看下面的脚本:
[csharp] view plain copy
- using UnityEngine;
- using System.Collections;
- using UniLua;
- public static class CSharpLib
- {
- //当前类文件名称,我们将在Lua脚本中使用这个名称
- public const string CLASSNAME="CSharpLib.cs";
- //C#库初始化
- public static int InitLib(ILuaState lua)
- {
- NameFuncPair[] define=new NameFuncPair[]
- {
- new NameFuncPair("SumAndSub",SumAndSub),
- };
- lua.L_NewLib(define);
- return 1;
- }
- //我们在C#中定义一个求和差的方法
- public static int SumAndSub(ILuaState lua)
- {
- //第一个参数
- int a=lua.L_CheckInteger(1);
- //第二个参数
- int b=lua.L_CheckInteger(2);
- //计算和
- int c=a+b;
- //计算差
- int d=a-b;
- //将参数及计算结果压入栈
- lua.PushInteger(a);
- lua.PushInteger(b);
- lua.PushInteger(c);
- lua.PushInteger(d);
- //有四个返回值, 尽管在C#中不支持返回多个值,可是在Lua中这样是支持的
- return 4;
- }
- }
大家一定注意到这里有个NameFuncPair类吧,这就是在UniLua中用来将一个C#方法和Lua方法进行绑定的方法,我们首先构造这样一个NameFuncPair数组,然后将其加入到lua_L_NewLib()的参数中,这样相当于是注册了一个库,我觉得应该就是注册了一个方法集合吧.而CLASSNAME是一个表示当前类名称的常量,可以取任意字符,这里我们使用该类的文件名我们将在Lua脚本是用这个值来查找当前类.接下来,我们可以看到博主构造了一个求和差的C#方法,这个方法和Lua API中定义的方法是一致的,即我们需要指定该方法会返回的值得数目.如果我们需要返回一个值,就要把它通过push系列的方法压入栈中.这里我们返回了四个值,大家一定会问好是C#还支持返回多个值啊,其实呢,这是Lua语言提供给我们的一个福利啊,比如我们需要返回一个物体在3D世界里的坐标,通常情况下,我们需要用三个赋值语句才能获取吧,可是你用Lua的话,一行代码就可以搞定啦.好,现在我们回到InvokeScript脚本的Start方法中,大家可以注意到这里有一个L_RequireF()的方法,前面只是轻描淡写地说它引入了一个库,那么现在我们看看它具体做了什么吧,第一个参数表示这个类的名字,指向我们定义好的CLASSNAME,第二个参数是这个类的初始化方法指向InitLib()方法,第三个参数是是否要在全局空间中使用这个库,这里我们选在false.好了,这样,我们就完成了C#脚本的编写.好了,下面我们在项目中创建一个纯文本文件,我们输入如下代码:
[plain] view plain copy
- local csharplib=require"CSharpLib.cs"
- arg1="Unity3D"
- arg2="Unreal"
- arg3="Coco2dX"
- function Printf()
- print("This is the methods invoked in Lua")
- end
- function Sum(a,b)
- return a,b,a+b
- end
- function SumAndSub(a,b)
- print(csharplib.SumAndSub(a,b))
- end
第一行代码同样是一个require的方法,这是Lua脚本中引用一个库的方法,该方法可以引用Lua的标准库,同样可以引用我们定义的外部库,大家注意到这里的名字和我们之前定义的CLASSNAME是一样的,因为我们就是通过这个名字来查询这个库的,我们在Lua环境中注册了这个库,所以现在才可以引用这个库.在这段脚本中我们定义了几个字符型的变量,两个Lua方法,一个用Lua包装的C#方法.好了,现在我们将这个文本文件指定到InvokeScript的LuaFile字段,我们通过LuaFille的text获取脚本内容,然后通过DoString()方法来执行脚本中的内容,注意这里要先对C#库进行注册,然后再执行脚本中的内容,否则会出现错误.好了,最后,我们来一起看看运行效果吧:
大家可以看到C#调用的Lua脚本中我们获取了脚本中的两个变量arg1、arg2,调用了Lua中定义的两个方法,而最后一个方法,如我们所愿,它返回了四个值,这正是我们所希望的结果.这里顺便说一下啊,在Lua中的print方法和return在Call以后是可以直接在Debug中输出结果的,无需我们再去做Log。好了,今天的内容就是这样啦,希望大家喜欢啊,欢迎大家关注我的博客,在下一篇文章中,博主将带领大家继续Lua到底,请关注博主的下一篇文章《Unity3D游戏开发之Lua与游戏的不解之缘终结篇:UniLua热更新完全解读》,谢谢大家.关于UniLua调用非静态的类和方法,大家可以参考这篇文章:http://www.cnblogs.com/cqgreen/p/3483026.html。
我们来说说Unity3D配合AssetBundle和;Lua实现热更新。
首先,我们来了解一下什么是热更新吧!所谓热更新是指在不停机的状态下对系统进行更改,例如我们的Windows可以在不重启的状态下完成补丁的更新、Web服务器在 不重启的前提下完成对数据和文件的替换等都是热更新的经典实例。那么对于Unity3D而言,什么是热更新呢?如果我们最终发布的Unity3D游戏是一个Web游戏,那么每次游戏加载的过程中实现对资源代码的更新就是热更新。如果我们最终发布的Unity3D游戏是一个客户端游戏,那么我们在重重启客户端以后实现对资源代码的更新就是热更新。为什么这么说呢?因为Web游戏需要保证玩家能够及时快速地进入游戏,因此在游戏加载完之前,我们必须完成对游戏资源和代码的更新。可是对于客户端游戏而言,玩家可以在等待本次更新结束后再进入游戏,而且大部分的客户端程序在更新完后都会要求玩家重启客户端,所以对于客户端游戏而言,热更新并非是严格意义上的热更新。那么,我们为什么要进行热更新呢?答案是为了缩短用户获取新版本客户端的流程、改进用户体验。这其实就是博主在前文中提到的传统单机游戏依靠光盘载体进行发售所面临的问题,玩家为了获取最新版本的游戏,需要下载全新的客户端并将它安装到计算机或者手机设备上。在互联网产品开发中有一种称为快速迭代的理念,试想如果我们每次对客户端进行局部的调整,就需要玩家去下载新版本的客户端,试问这样的用户体验真得能让用户满意吗?所以现在为了方便用户、留住用户、进而从留住的用户身上赚到钱,我们总能在游戏产品中找到热更新的影子。我们知道在Unity3D中可以通过AssetBundle来实现对游戏中资源的更新,在http://blog.csdn.net/janeky/article/details/17666409这篇文章中作者janeky已经给出了较为完美地解决方案,因为博主使用的Unity3D免费版无法使用AssetBundle的功能,而博主本人不愿意使用破解版,因为这是一个程序员的良心,所以本文更多的是从代码更新的这个角度来讲Unity3D的热更新,对于资源的热更新大家建议大家还是去看janeky的这篇文章吧。好了,下面正式开始Unity3D代码级的热更新之旅!
在Unity官方的API中官方给出了一种基于反射的思路,即将C#脚本存储为文本文件,然后将其转化为byte字节,再通过反射技术取得该类型及其方法。理论上这样当然没有问题,可是我们知道由于IOS是一个封闭的系统,设计者出于安全的考虑不允许在该平台下使用反射技术。那么问题来了,反射并不是一个完美地解决方案。关于反射技术实现Unity3D的热更新,大家可以参考这篇文章:http://blog.csdn.net/janeky/article/details/25923151。好了,下面我们来说说博主是如何通过Lua实现Unity3D的热更新的吧。我们知道在Lua提供的C#接口中有一个DoString()的方法,该方法可以直接利用Lua虚拟机执行字符串中的脚本。所以,我们可以通过在本地读取Lua脚本来执行脚本中的命令,如果我们脚本中的命令可以直接对Unity3D进行操作,那么我们就可以通过Lua脚本来更新游戏中的代码逻辑。那么,我们怎么能让Lua脚本操作Unity3D呢?在前一篇文章中,我们介绍了一种Require的方法,该方法可以将C#库引入到Lua脚本中并通过Lua来执行C#库中的方法。顺着这样的思路,博主便有了下面的设想:
在这个设想中,我们首先需要将Unity API封装成一个C#类库,在这个类库中我们将会涉及动态加载场景和动态创建场景,因为我们更新游戏的逻辑的时候将会用到这些方法。这些方法通过封装后我们便可以在Lua脚本通过Require方式来引用,进而我们就可以通过Lua脚本来动态地进行设计。我们设计一个固定的位置来存储Lua脚本更新文件,这样我们只需要对比本地版本和服务器版本是否相同就可以知道我们是否需要更新。这里我们通过WWW来从远程服务器上下载最新的Lua脚本更新文件,下载下来的Lua脚本处于项目外部,我们无法使用Resource.Load()这样的方法来加载,可是我们可以通过WWW来加载一个本地文件,这样我们就实现了Lua脚本文件的更新。当然,我们可以使用AssetBundle来更新Lua脚本文件,可是博主的免费版不支持AssetBundle,所以博主想出了这样一个曲线救国的方法。当Lua脚本文件更新后,我们就可以在游戏主逻辑里通过DoString()方法来执行脚本文件中的代码。在游戏主逻辑里主要的任务是比较当前版本号和服务器版本号来判断是否需要更新,如果需要更新就下载Lua脚本更新文件然后执行脚本中的代码,这样我们就实现了客户端程序的更新。好了,下面我们继续以前一篇文章中的项目为例来将博主的这个设想变成现实。
首先,我们在CSharpLib.cs这个类中增加下面两个方法并完成方法的注册:
[csharp] view plain copy
- ///
- /// 设置场景中物体的坐标
- ///
- ///
返回当前坐标 - /// Lua.
- public static int SetPosition(ILuaState lua)
- {
- //物体的名称
- string mName=lua.L_CheckString(1);
- //传入参数x,y,z
- float mX=(float)lua.L_CheckNumber(2);
- float mY=(float)lua.L_CheckNumber(3);
- float mZ=(float)lua.L_CheckNumber(4);
- //获取物体
- GameObject go=GameObject.Find(mName);
- //获取Transform
- Transform mTrans=go.transform;
- //设置游戏体的位置
- mTrans.position=new Vector3(mX,mY,mZ);
- //返回游戏体当前坐标
- lua.PushNumber(mTrans.position.x);
- lua.PushNumber(mTrans.position.y);
- lua.PushNumber(mTrans.position.z);
- return 3;
- }
- ///
- /// 使用本地预设创建一个物体
- ///
- ///
The resource. - /// Lua.
- public static int CreateResource(ILuaState lua)
- {
- //传入资源名称
- string mName=lua.L_CheckString(1);
- //加载本地资源
- GameObject go=(GameObject)Resources.Load(mName);
- //传入坐标参数x,y,z
- float mX=(float)lua.L_CheckNumber(2);
- float mY=(float)lua.L_CheckNumber(3);
- float mZ=(float)lua.L_CheckNumber(4);
- //创建一个新物体
- Object.Instantiate(go,new Vector3(mX,mY,mZ),Quaternion.identity);
- //返回该物体的名称
- lua.PushString(go.name);
- return 1;
- }
好了,这样我们就完成了一个简单的C#类库,下面我们来在主逻辑代码中增加一个更新脚本的方法UpdateScripts():
[csharp] view plain copy
- void UpdateScript()
- {
- StartCoroutine("Download");
- }
- ///
- /// 下载Lua脚本更新文件
- ///
- IEnumerator Download()
- {
- //从本地加载Lua脚本更新文件,假设文件已经从服务器下载下来
- WWW _WWW=new WWW(mUpdateFilesPath);
- yield return _WWW;
- //读取服务器版本
- mLua.L_DoString(_WWW.text);
- }
这里的代码逻辑很简单,就是读取脚本更新本地文件然后执行脚本,其中mUpdateFilePath是脚本更新文件路径:
[csharp] view plain copy
- //初始化路径
- mUpdateFilesPath="file://D:\\lua_update.txt";
这里博主设想的是在本地存储一个版本号,每次更新前先获取服务器端的版本号,如果两个版本号不同则需要从服务器上下载更新脚本文件进行更新。不过博主这里没有想到什么好方法来获取版本号,所以这里就只写了更新。那么,我们来看看更新脚本文件都做了哪些事情吧!
[csharp] view plain copy
- local csharplib=require"CSharpLib.cs"
- csharplib.SetPosition("Cube",2,1,0)
- csharplib.CreateResource("Sphere",0,0,0)
- csharplib.CreateResource("Cube",1,1,0)
首先我们通过Require引入了CSharpLib.cs 这个类库,接下来,我们将场景中名称为Cube的物体的位置设为(2,1,0)、 利用本地的两个Prefab资源创建了一个Cube和一个Sphere。那么,我们的设想能不能实现呢?我们一起来看最终效果吧!
执行Lua脚本更新前:
执行Lua脚本更新后:
如我们所愿,Lua脚本成功地对场景实现了一次更新。可能有的朋友会问,这里用的是本地资源,如果我想用服务器上的资源怎么办呢?答案是博主最不愿意提及的AssetBundle,即利用AssetBundle加载远程资源,然后用Lua实现更新,这些逻辑可以添加到CSharpLib这个类库中。大家可以设想一下,如果有一天我们能够将Unity的所有方法都封装起来,那么我们就可以直接用Lua来创建场景了,如果要更新客户端,只要更换Lua文件就可以了,怎么样是不是很简单呢?可是Unity不开源啊,这些想法终究只是想法啦。
C#与Lua是互相调用
一.基本原理
简单地说,c#调用lua, 是c# 通过Pinvoke方式调用了lua的dll(一个C库),然后这个dll执行了lua脚本。
ULua = Lua + LuaJit(解析器、解释器) +LuaInterface。
其中,LuaInterface中的核心就是C#通过Pinvoke对Lua C库调用的封装,所以,在Unity中,LuaInterface就是C#与Lua进行交互的接口。
下面我们以一个简单的例子来演示C#与Lua的相互调用。
二.入门例子
如下是构建这个例子的步骤。
(1)下载ULua源码。
(2)在Unity中新建一个项目,并将ULua源码拷贝到Assets目录下。
(3)将ulua.dll(就是上面提到的C库)放到Assets下的Plugins文件夹中。(没有Plugins文件夹就新建一个)
(4)在Assets下的Script文件夹中新建一个脚本CSharpLuaTest.cs,并将该脚本绑定到Main Camera上。
(5)在CSharpLuaTest.cs中编辑以下内容:
public class CSharpLuaTest : MonoBehaviour { private LuaState luaState = new LuaState(); // 创建lua虚拟机 void Start () { // 在lua虚拟机(全局)中注册自定义函数 this.luaState.RegisterFunction("CSharpMethod", this, this.GetType().GetMethod("CSharpMethod")); // 加载lua文件(绝对路径) this.luaState.DoFile(Application.streamingAssetsPath + "/Test.lua"); // 加载完文件后,使用GetFunction获取lua脚本中的函数,再调用Call执行。 object[] objs = luaState.GetFunction("LuaMethod").Call(999); Debug.Log(string.Format("{0} - {1}" ,objs[0], objs[1])); } //自定义功能函数,将被注册到lua虚拟机中 public string CSharpMethod(int num) { return string.Format("Hello World {0} !" , num+1); } void Update () { }}
(6)在Assets下的StreamingAssets文件夹中新建一个Lua脚本文件Test.lua,打开Test.lua文件,并编辑如下内容:
1 2 3 4 |
|
(7)运行Unity项目,则可以看到输出:999 - Hello World 1000 !
三.要点说明
最后简单说一下上面代码的要点:
1.如果一个C#方法要被Lua调用,则首先要将其注册到Lua虚拟机中(LuaState.RegisterFunction)。之后,在Lua中就可以通过注册的名称来调用这个C#方法。
2.如果C#要调用Lua中的函数,则
(1)首先要在Lua虚拟机中加载该函数(LuaState.DoFile)。
(2)拿到目标函数(LuaState.GetFunction)。
(3)执行目标函数(LuaFunction.Call)。