zoukankan      html  css  js  c++  java
  • [整理]Unity3D游戏开发之Lua

    原文1:[Unity3D]Unity3D游戏开发之Lua与游戏的不解之缘(上)

     各位朋友,大家好,我是秦元培,欢迎大家关注我的博客,我地博客地址是blog.csdn.net/qinyuanpei。如果提到游戏开发,大家一定会想到C/C++、DirectX、OpenGL等这些东西,可是众所周知,游戏行业是一个需求变化极快地行业,如果我们采用编译型的语言,那么我们可能很难跟上这个时代的步伐,因为编译型的语言每经历一次重大地更新,整个项目都需要重新编译,这样无疑会影响我们的开发效率。那么,有没有一种更为高效的游戏开发模式呢?或许答案大家已经看到了。现在在游戏界普遍采用的方式是将游戏的底层逻辑交给C/C++这样的底层语言,而将游戏的上层逻辑交给脚本语言。因为底层逻辑更看重效率而上层逻辑更注重灵活、便捷地使用。例如我们熟知的Unreal引擎是采用UnrealScripts,这是一种类似于Java/C语法地语言;Unity3D引擎是采用的C#/javaScript/Boo这三种脚本语言;cocos2d-x采用地是Lua/javaScript这两种脚本语言,未来可能会支持更多的语言。大家可能想问一个问题:什么是脚本语言?所谓脚本语言是一种用来控制软件应用程序且只在被调用时进行解释或编译的编程语言,这种语言通常以文本的形式来存储脚本代码。换句话说,脚本语言类似于一种指令,它缩短了传统应用程序的编写-编译-链接-运行(edit-compile-link-run)这个过程,是一种解释执行的程序。或许人们发明脚本语言的那一刻起,从未想过要将脚本语言和游戏开发联系在一起,不过脚本语言注定会因为游戏开发而开拓出更为广阔的世界。本文将以目前游戏开发领域较为流行的Lua语言为线索,深度解密游戏开发领域与脚本语言之间千丝万缕的联系。

         一、什么是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
     
    1. require("Necessary")  
    2. require("StoryUtility")  
    3. require("BigMap")  
    4. require("Script_DLC4")  
    5.   
    6. --------------以下为初始化函数-------------  
    7.   
    8. function OnEnterLevel()  
    9.      if GetStoryVersion() == 2 then  
    10.          OnDLCEnterLevelM01()  
    11.      else  
    12.          if GetMainStory() == 10100 then  
    13.              callTaskFunction("story10100")  
    14.          elseif GetMainStory() == 161900 then  
    15.              callTaskFunction("story161900")  
    16.          end  
    17.   
    18.          if gValue.MK == 1 then  
    19.              showNPC("NPC 06", false)  
    20.              showNPC("NPC 07", false)  
    21.              enableTrigger("Tri_MK",false)  
    22.          elseif gValue.MK >1 then  
    23.              showNPC("NPC 04", false)  
    24.              showNPC("NPC 05", false)  
    25.              showNPC("NPC 06", false)  
    26.              showNPC("NPC 07", false)  
    27.              enableTrigger("Tri_MK",false)  
    28.              enableTrigger("Tri_MK 02",false)  
    29.          end  

           4、仙剑奇侠传

           既然提到了古剑奇谭,怎么能不提仙剑奇侠传呢?虽然和古剑奇谭初代作品发布时间仅仅相差一年的《仙剑奇侠传五》市场反响并没有像游戏制作方所预料地那样成功,不过这部作品值得称赞地地方还是蛮多的,因为进步总是要比缺点多的嘛,毕竟时代在进步,我们不能总是拿仙剑初代作品的高度去要求后续作品,因为我们已经不再是那个年龄的人,而仙剑依然要不断地突破自身、大胆创新和进取。好了,我们暂时先感慨到这里,仙剑四、仙剑五以及仙剑五前传都使用了RenderWare引擎,可能唯一的不同就是仙剑五和仙剑五前传都使用了Lua吧,下面同样是一段从游戏中提取的脚本:

    [plain] view plain copy
     
    1. function baoxiang(id,npcID)  
    2.       
    3.     player.Control(0)  
    4.     pid=player.GetMainPlayer()  
    5.     player.SetAnim(pid,203)   
    6.     global.Print(id)  
    7.     global.Wait(1)  
    8.     y=flag.GetValue(15093)  
    9.       
    10.     ---------江洋大盗称号获得-------------    
    11.     jyd=flag.GetValue(15255)  
    12.     jyd=jyd+1  
    13.     flag.SetValue(15255,jyd)  
    14.     global.Print(jyd)  
    15.     global.AddTimer(0.5,13279)  
    16. -----------------------------------------  
    17.       
    18.     if id~=17711 then  
    19.         npc.SetAnim(npcID,501)  
    20.         global.Wait(1)  
    21.     end  
    22.           

         5、金庸群侠传Lua复刻版

        四、带你走进Lua的世界

        最后想和大家分享是Lua语言编程的一个简单的示例,因为博主觉得以后做游戏用脚本语言的场景会越来越多,所以能学会一门脚本语言能为你的游戏开发之路增色不少。因为博主刚开始学,所以脚本中有不足之处,希望大家能谅解,在学校的时间一天天地在减少,博主希望能和大家共同度过最后的这段时间。博主使用的是Lua5.2,使用的Sublime Text2作为脚本编辑器配合LuaDev插件进行编程的,如果大家想用懒惰点的办法,可以使用Lua for Windows这个集成环境。好了,下面开始吧,作为第一个Lua程序,我们直接给出代码,具体的语法及API大家可以自己去查阅。
    [plain] view plain copy
     
    1. --while-do示例代码  
    2. myValue=10  
    3. while(myValue <= 20) do  
    4.     print(myValue)  
    5.     myValue=myValue+1  
    6. end  
    7. --sample table && for-do示例代码  
    8. myTables={"Item0","Item1","Item2","Item3"}  
    9. for i=1,table.maxn(myTables) do  
    10.     print(myTables[i])  
    11. end  
    12. --complex table示例代码  
    13. myTables={}  
    14. myTables["A"]="ItemA"  
    15. myTables["B"]="ItemA"  
    16. myTables["C"]="ItemA"  
    17. myTables["D"]="ItemA"  
    18. print(myTables["A"])--"ItemA"  
    19. --function示例代码  
    20. function fib(n)  
    21.   if(n<2) then   
    22.     return n  
    23.   else  
    24.     return fib(n-1)+fib(n-2)  
    25.   end  
    26. end  
    27. --math示例代码  
    28. maxValue=math.max(12,23,56,18,10)--56  
    29. minValue=math.min(25,34,12,75,8)--8  
    30. print(maxValue-minValue)--48  
    31. --字符串演示  
    32. myString="Hello this is the cool program language called Lua";    
    33. print(string.find(myString,"Lua"))--48,50  
    34. --io演示  
    35. io.write("Hello I get a powerful program language called Lua  ")  
    36. io.write(string.format("This Lua is %s and now is %s  ",_VERSION,os.date()))  
     
    运行结果是:
     
     
      通过前面的学习,我们知道设计Lua语言的目的是为了将Lua嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua语言本身没有像其它语言提供丰富的类库,因此Lua语言必须依赖于其它语言来完成功能上的扩展(可是正是在功能上牺牲才换来了Lua精简而稳定的核心)。如果我们要深入了解Lua语言的话,就必须要了解Lua语言与其它语言的交互接口,因为这将是我们使用Lua语言的基础。那么,今天就让博主来带领大家一起学习Lua语言与其它语言的交互吧!

     一、Lua堆栈

        如果我们想要理解Lua语言与其它语言交互的实质,我们首先就要理解Lua堆栈。简单来说,Lua语言之所以能和C/C++进行交互,主要是因为存在这样一个无处不在的虚拟栈。栈的特点是先进后出,在Lua语言中,Lua堆栈是一种索引可以是正数或者负数的结构,并规定正数1永远表示栈底,负数-1永远表示栈顶。换句话说呢,在不知道栈大小的情况下,我们可以通过索引-1取得栈底元素、通过索引1取得栈顶元素。下面呢,我们通过一个实例来加深我们对于这段话的理解:

    [cpp] view plain copy
     
    1. #include <iostream>  
    2.   
    3. extern "C" {  
    4. #include "lua.h"  
    5. #include "lualib.h"  
    6. #include "lauxlib.h"  
    7. }  
    8.   
    9. using namespace std;  
    10.   
    11. int main()  
    12. {  
    13.     //创建Lua环境  
    14.     lua_State* L=lua_open();  
    15.     //打开Lua标准库,常用的标准库有luaopen_base、luaopen_package、luaopen_table、luaopen_io、  
    16.     //luaopen_os、luaopen_string、luaopen_math、luaopen_debug  
    17.     luaL_openlibs(L);  
    18.     //压入一个数字20  
    19.     lua_pushnumber(L,20);  
    20.     //压入一个数字15  
    21.     lua_pushnumber(L,15);  
    22.     //压入一个字符串Lua  
    23.     lua_pushstring(L,"Lua");  
    24.     //压入一个字符串C  
    25.     lua_pushstring(L,"C");  
    26.     //获取栈元素个数  
    27.     int n=lua_gettop(L);  
    28.     //遍历栈中每个元素  
    29.     for(int i=1;i<=n;i++)  
    30.     {  
    31.         cout << lua_tostring(L ,i) << endl;  
    32.     }  
    33.     return 0;  
    34. }  

    在上面的这段代码中,我们可以可以看到我们首先创建了一个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
     
    1. #include <iostream>  
    2.   
    3. extern "C" {  
    4. #include "lua.h"  
    5. #include "lualib.h"  
    6. #include "lauxlib.h"  
    7. }  
    8.   
    9. using namespace std;  
    10.   
    11. int main()  
    12. {  
    13.     //创建Lua环境  
    14.     lua_State* L=lua_open();  
    15.     //打开Lua标准库,常用的标准库有luaopen_base、luaopen_package、luaopen_table、luaopen_io、  
    16.     //luaopen_os、luaopen_string、luaopen_math、luaopen_debug  
    17.     luaL_openlibs(L);  
    18.     //压入一个数字20  
    19.     lua_pushnumber(L,20);  
    20.     //压入一个字符串15  
    21.     lua_pushnumber(L,15);  
    22.     //压入一个字符串Lua  
    23.     lua_pushstring(L,"Lua");  
    24.     //压入一个字符串C  
    25.     lua_pushstring(L,"C");  
    26.     //获取栈中元素个数  
    27.     int n=lua_gettop(L);  
    28.     //遍历栈中每个元素  
    29.     for(int i=1;i<=n;i++)  
    30.     {  
    31.         //类型判断  
    32.         switch(lua_type(L,i))  
    33.        {  
    34.           case LUA_TSTRING:  
    35.             cout << "This value's type is string" << endl;  
    36.           break;  
    37.           case LUA_TNUMBER:  
    38.             cout << "This value's type is number" << endl;  
    39.           break;  
    40.         }  
    41.         //输出值  
    42.         cout << lua_tostring(L ,i) << endl;  
    43.     }  
    44.   
    45.     //释放Lua  
    46.     lua_close(L);  
    47. }  

        二、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
     
    1. #include <iostream>  
    2.   
    3. using namespace std;  
    4.   
    5. #include <iostream>  
    6.   
    7. extern "C" {  
    8. #include "lua.h"  
    9. #include "lualib.h"  
    10. #include "lauxlib.h"  
    11. }  
    12.   
    13. using namespace std;  
    14.   
    15. int main()  
    16. {  
    17.     //创建Lua环境  
    18.     lua_State* L=luaL_newstate();  
    19.     //打开Lua标准库,常用的标准库有luaopen_base、luaopen_package、luaopen_table、luaopen_io、  
    20.     //luaopen_os、luaopen_string、luaopen_math、luaopen_debug  
    21.     luaL_openlibs(L);  
    22.   
    23.     //下面的代码可以用luaL_dofile()来代替  
    24.     //加载Lua脚本  
    25.     luaL_loadfile(L,"script.lua");  
    26.     //运行Lua脚本  
    27.     lua_pcall(L,0,0,0);  
    28.   
    29.     //将变量arg1压入栈顶  
    30.     lua_getglobal(L,"arg1");  
    31.     //将变量arg2压入栈顶  
    32.     lua_getglobal(L,"arg2");  
    33.   
    34.     //读取arg1、arg2的值  
    35.     int arg1=lua_tonumber(L,-1);  
    36.     int arg2=lua_tonumber(L,-2);  
    37.   
    38.     //输出Lua脚本中的两个变量  
    39.     cout <<"arg1="<<arg1<<endl;  
    40.     cout <<"arg2="<<arg2<<endl;  
    41.   
    42.     //将函数printf压入栈顶  
    43.     lua_getglobal(L,"printf");  
    44.     //调用printf()方法  
    45.     lua_pcall(L,0,0,0);  
    46.   
    47.     //将函数sum压入栈顶  
    48.     lua_getglobal(L,"sum");  
    49.     //传入参数  
    50.     lua_pushinteger(L,15);  
    51.     lua_pushinteger(L,25);  
    52.     //调用sum()方法  
    53.     lua_pcall(L,2,1,0);//这里有2个参数、1个返回值  
    54.     //输出求和结果  
    55.     cout <<"sum="<<lua_tonumber(L,-1)<<endl;  
    56.   
    57.     //将表table压入栈顶  
    58.     lua_getglobal(L,"table");  
    59.     //获取表内a的值  
    60.    lua_getfield(L,-1,"a"); 
    61.     //输出表中元素  
    62.     cout <<"table.a="<<lua_tostring(L,-1)<<endl;  
    63.   
    64. }  
    在这段代码中我们调用了一个外部的文件script.lua。这是一个Lua脚本文件,在调试阶段,我们需要将其放置在和C++项目源文件同级的目录下,而在正式运行阶段,我们只需要将其和最终的可执行文件放在同一个目录下就好了。下面是脚本代码:
    [cpp] view plain copy
     
    1. --在Lua中定义两个变量  
    2. arg1=15  
    3. arg2=20  
    4.   
    5. --在Lua中定义一个表  
    6. table=  
    7. {  
    8.     a=25,  
    9.     b=30  
    10. }  
    11.   
    12. --在Lua中定义一个求和的方法  
    13. function sum(a,b)  
    14.   return a+b  
    15. end  
    16.   
    17. --在Lua中定义一个输出的方法  
    18. function printf()  
    19.   print("This is a function declared in Lua")  
    20. 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
     
    1. static int AverageAndSum(lua_State *L)  
    2. {  
    3.     //返回栈中元素的个数  
    4.     int n = lua_gettop(L);  
    5.     //存储各元素之和  
    6.     double sum = 0;  
    7.     for (int i = 1; i <= n; i++)  
    8.     {  
    9.         //参数类型处理  
    10.         if (!lua_isnumber(L, i))  
    11.         {  
    12.             //传入错误信息  
    13.             lua_pushstring(L, "Incorrect argument to 'average'");  
    14.             lua_error(L);  
    15.         }  
    16.         sum += lua_tonumber(L, i);  
    17.     }  
    18.     //传入平均值  
    19.     lua_pushnumber(L, sum / n);  
    20.     //传入和  
    21.     lua_pushnumber(L, sum);  
    22.   
    23.     //返回值的个数,这里为2  
    24.     return 2;  
    25. }  
    接下来我们在C++中使用lua_register()方法完成对该方法的注册
    [cpp] view plain copy
     
    1. lua_register(L, "AverageAndSum", AverageAndSum);  
    这样我们就可以在Lua环境中使用这个方法啦,前提是定义必须在执行代码之前完成,我们在Lua脚本文件下加入对该方法的调用:
    [plain] view plain copy
     
    1. --在Lua中调用C++中定义并且注册的方法  
    2. average,sum=AverageAndSum(20,52,75,14)  
    3. print("Average=".average)  
    4. print("Sum=".sum)  
    如果我们需要在C++中查看该方法调用的结果,那么这个在C++中调用Lua是一样的。好了,C++和Lua的交互终于讲完了,被这块的代码纠结了好几天,这下总算是搞明白了。当然这只是对原理的一种学习和理解啦,如果希望更好的使用Lua调用C++,建议了解这几个项目:

    LuaPlusLuaBind。这样相信大家对于C++中的方法如何在Lua中绑定会有更好的认识吧!

        三、Lua与C#交互

    首先看下不同版本Lua介绍:

    luainterface、nlua、ulua、unilua、cstolua、slua

    luainterface:LuaInterface是开源的C#的lua桥接库,配合开源库luanet,能轻松实现Lua,C#相互调用和参数事件传递。但作者仅完成了windows程序的功能实现,跨平台并没有完成,作者于2013年4月30日停止更新luainterface,并推荐大家关注luainterface的一个分支Nlua。Nlua是实现跨平台的Luainterface的升级版,uLua和NLua都是基于此库升级编写

    nlua:是LuaInterface的一个分支,继承了Luainterface的所有优点,并将Luanet库功能集成到代码中,实现跨平台(Windows, Linux, Mac, iOS , Android, Windows Phone 7 and 8),对ios平台做了特殊处理,如支持了委托的桥接。
    配合NLua有2种Lua实现,第一种是KeraLua,基于原生Lua,将C API 进行简单的包装,使C# 可以方便使用 Lua C API,第二种是KopiLua,C#实现的Lua vm(对,和UniLua一样也是纯C#实现的Lua vm)。以下为关于两种方案的比较。
    使用KeraLua,必须将lua 编译成 Unity3D Plugin,并将编译好的文件放到Plugins文件夹下相应的平台文件夹中。并定义#define USE_KERALUA
    使用KopiLua,定义#define USE_KERALUA即可

    ulua:基于luainterface升级版,uLua = Lua + LuaJIT + LuaInterface,全平台支持。在原生C的基础上使用LuaJit进行加速,如果uLua效率高,LuaJit有很大功劳,作者仅仅提供了uLua插件包,并未提供整套插件源码。此外,作者重写了loadfile、print等api,使用非常简单,导入package,就可以开始编写代码了。

    unilua:是云风团队阿南的作品,是lua5.2的C#版,

    纯C#的Lua 5.2实现,是不是感觉似曾相识,对的,KopiLua也是纯C#实现的Lua vm,虽然Unilua出名,但是没有KopiLua的配套库好用,其自身同的Ffi库,是实验性质的库,不完善,作者不推荐使用,虽然作者在其商业项目中使用,但是这只是其中一部分代码,Unilua和C#中间层的代码作者并没有开源。UniLua仅仅提供了Lua原生的接口,如果要在Lua代码中调用C#,使用就需要把Luanet 移植到Unilua代码中,总的来说很蛋疼,据推测Unilua方法都是使用Lua标准的命名方式,所以将luanet源码中所有C接口全部手动改写成Unilua 的接口,就可以使用,这个工作量,等闲的时候把玩比较好。

    cstolua:cstolua是作者对ulua的扩展,提高了效率

    slua:也是从ulua扩展而来,官方说效率比cstolua还高,不过也有很多人质疑过 http://www.ulua.org/cstolua.html    http://www.slua.net/   http://www.sineysoft.com/post/164

    效率

    cstolua > ulua > nlua > luainterface > unilua

      

    既然我们已经知道了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的版本不一致造成的吧。

    LuaInterface中的核心就是C#通过Pinvoke对Lua C库调用的封装,所以,在Unity中,LuaInterface就是C#与Lua进行交互的接口。

    Lua是一种很好的扩展性语言,Lua解释器被设计成一个很容易嵌入到宿主程序的库。LuaInterface则用于实现Lua和CLR的混合编程。

    LuaInterface.Lua类是CLR访问Lua解释器的主要接口,一个LuaInterface.Lua类对象就代表了一个Lua解释器(或Lua执行环境),Lua解释器可以同时存在多个,并且它们之间是完全相互独立的。

    下面的这个项目是可以使用的,博主这里写了一个简单的示例:

    [csharp] view plain copy
     
    1. //------------------------------------------------------------------------------  
    2. // <summary>  
    3. //     这是一个用以演示LuaInterface的简单程序,通过LuaInterface我们可以实现在C#与Lua的  
    4. //     的相互通信。Lua是一个轻巧而高效的语言,它可以和任何语言混合使用。Lua语言最初并不是  
    5. //     为游戏开发而诞生,却是因为游戏开发而成名。目前,在世界上有大量的游戏使用了Lua作为它  
    6. //     的脚本语言。如图Unity使用了C#作为它的语言,Lua在游戏开发领域发挥着不可忽视的重要作  
    7. //     用。使用LuaInterface的方法如下:  
    8. //     1.C#  
    9. //     注册Lua中可调用方法:  
    10. //    mLua.RegisterFunction(Lua调用方法名, 类, 类.GetMethod(C#方法名));  
    11. //    注:C#不要使用方法级泛型,即 void Fun<T>(string str);,如果使用,系统自动判定T为第一个参数的类型。  
    12. //     加载Lua代码  
    13. //     mLua.DoString(Lua代码);  
    14. //    mLua.DoFile(Lua文件绝对路径);  
    15. //     调用Lua方法  
    16. //     mLua.GetFunction(Lua方法).Call(参数);  注:此处参数不要传递dynamic类型的类,否则Lua中无法获取属性值  
    17. //     2.Lua  
    18. //     调用C#方法时需要先注册注册后按照Lua方法处理  
    19. // </summary>  
    20. //------------------------------------------------------------------------------  
    21. using System;  
    22. using LuaInterface;  
    23. namespace LuaExample  
    24. {  
    25.     public class LuaScript  
    26.     {  
    27.         //定义LuaFile属性以便于从外部调用一个Lua脚本  
    28.         private string mLuaFile;  
    29.         public string LuaFile {  
    30.             get {  
    31.                 return mLuaFile;  
    32.             }  
    33.             set {  
    34.                 mLuaFile = value;  
    35.             }  
    36.         }  
    37.   
    38.         //Lua虚拟机  
    39.         private Lua mLua;  
    40.   
    41.         //构造函数  
    42.         public LuaScript ()  
    43.         {  
    44.             //初始化Lua虚拟机  
    45.             mLua=new Lua();  
    46.             //注册Printf方法  
    47.             mLua.RegisterFunction("Printf",this,this.GetType().GetMethod("Printf"));  
    48.         }  
    49.   
    50.         //定义一个C#方法供Lua使用  
    51.         public void Printf(string str)  
    52.         {  
    53.             Console.WriteLine("This Method is Invoked by Lua:" + str);  
    54.         }  
    55.   
    56.         //在C#中调用Lua方法  
    57.         public void DoFile()  
    58.         {  
    59.             if(mLuaFile!="")  
    60.                 //执行Lua脚本中的代码  
    61.                 mLua.DoFile(mLuaFile);  
    62.         }  
    63.   
    64.         //在C#中调用Lau方法  
    65.         public void DoString()  
    66.         {  
    67.             //以字符串形式定义的Lua脚本  
    68.             string mFuncString="function Add(a,b) io.write(a+b) end";  
    69.             //在Lua中定义该方法  
    70.             mLua.DoString(mFuncString);  
    71.             //调用该方法  
    72.             mLua.GetFunction("Add").Call(4,8);  
    73.         }  
    74.   
    75.         //在Lua中调用C#脚本  
    76.         public void Invoke()  
    77.         {  
    78.             //调用注册的Printf方法  
    79.             mLua.GetFunction("Printf").Call("Hello Lua");  
    80.         }  
    81.     }  
    82. }  
    接下来我们编写一个主类来调用这个类:
    [csharp] view plain copy
     
    1. using System;  
    2. using LuaInterface;  
    3.   
    4. namespace LuaExample  
    5. {  
    6.     class MainClass  
    7.     {  
    8.         public static void Main (string[] args)  
    9.         {  
    10.             //实例化LuaSxript  
    11.             LuaScript mLua=new LuaScript();  
    12.             //设置LuaFile  
    13.             mLua.LuaFile="D:\test.lua";  
    14.             //调用字符串中定义的Lua方法  
    15.             mLua.DoString();  
    16.             //为美观考虑增加一个空行  
    17.             Console.WriteLine();  
    18.             //执行Lua文件中定义的脚本  
    19.             mLua.DoFile();  
    20.             //调用C#中定义的方法  
    21.             mLua.Invoke();  
    22.         }  
    23.     }  
    24. }  
    好了,C#与Lua的交互解决了,更多的内容期待着大家自行到该项目源代码中去寻找。好了,先这样吧!

    Lua和C++交互的文章可以看另外一篇:Lua和C++交互详细总结

    C#和Lua相互调用看文章:

    1、 C#与Lua相互调用

    2、在Unity中使用Lua脚本:语言层和游戏逻辑粘合层处理

    LuaInterface简介:

    LuaInterface.Lua类是CLR访问Lua解释器的主要接口,一个LuaInterface.Lua类对象就代表了一个Lua解释器(或Lua执行环境),Lua解释器可以同时存在多个,并且它们之间是完全相互独立的。

      下面的简单代码展示了以下功能:

      (1)CLR访问Lua的全局域: 下标/索引操作[]

      (2)CLR新建Lua的table:NewTable

      (3)CLR中执行Lua脚本代码或脚本文件:DoFile、DoString

    复制代码
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using LuaInterface;
    
    namespace TestCSharpAndLuaInterface
    {
            static void Main(string[] args)
            {
                // 新建一个Lua解释器,每一个Lua实例都相互独立
                Lua lua = new Lua();
    
                // Lua的索引操作[]可以创建、访问、修改global域,括号里面是变量名
                // 创建global域num和str
                lua["num"] = 2;
                lua["str"] = "a string";
                
                // 创建空table
                lua.NewTable("tab");
    
                // 执行lua脚本,着两个方法都会返回object[]记录脚本的执行结果
                lua.DoString("num = 100; print("i am a lua string")");
                lua.DoFile("C:\luatest\testLuaInterface.lua");
                object[] retVals = lua.DoString("return num,str");
    
                // 访问global域num和str
                double num = (double)lua["num"];
                string str = (string)lua["str"];
    
                Console.WriteLine("num = {0}", num);
                Console.WriteLine("str = {0}", str);
                Console.WriteLine("width = {0}", lua["width"]);
                Console.WriteLine("height = {0}", lua["height"]);
            }
        }
    }
    复制代码

    LuaIntrface自动对应Lua和CLR中的一些基础类型
      [nil, null]
      [string, System.String]
      [number, System.Double]
      [boolean, System.Boolean]
      [table, LuaInterface.LuaTable]
      [function, LuaInterface.LuaFunction]
    以上对应关系反之亦然。

    特殊类型:userdata

      (1)CLR中不能自动匹配Lua类型的对象(以上基础类型之外的类型)传给Lua时,转换为userdata,当Lua把这些userdata传回给CLR时,这些userdata又转换回原类型对象;
      (2)Lua里面生成的userdata从Lua传到CLR时转换为LuaInterface.LuaUserData。

      LuaTable和LuaUserData都有索引操作[],用来访问或修改域值,索引可以为string或number。
      LuaFunction和LuaUserData都有call方法用来执行函数,可以传入任意多个参数并返回多个值。

    Lua调用CLR的函数:RegisterFunction方法用来将CLR函数注册进Lua解释器,供Lua代码调用,看下面这个例子:

    复制代码
    namespace TestCSharpAndLuaInterface
    {
        class TestClass
        {
            private int value = 0;
    
            public void TestPrint(int num)
            {
                Console.WriteLine("TestClass.TestPrint Called! value = {0}", value = num);
            }
    
            public static void TestStaticPrint()
            {
                Console.WriteLine("TestClass.TestStaticPrint Called!");
            }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                Lua lua = new Lua();
                
                TestClass obj = new TestClass();
                // 注册CLR对象方法到Lua,供Lua调用
                lua.RegisterFunction("LuaTestPrint", obj, obj.GetType().GetMethod("TestPrint"));    // 也可用 typeof(TestClass).GetMethod("TestPrint")
                // 注册CLR静态方法到Lua,供Lua调用
                lua.RegisterFunction("LuaStaticPrint", null, typeof(TestClass).GetMethod("TestStaticPrint"));
    
                lua.DoString("LuaTestPrint(10)");
                lua.DoString("LuaStaticPrint()");
            }
        }
    }
    复制代码

    (二)CLR from Lua

    (1)加载和实例化CLR类型

    测试环境有两种方式:

      第一种:纯Lua文件中进行测试

      将LuaForWindows安装的LuaInterface.dll和luanet.dll都拷贝到自己注册的环境变量的目录下,比如我的是"C:\luatest\",然后就可以在Lua编辑器中编写测试代码了,如下:

    复制代码
    --package.cpath  = "C:\luatest\?.dll"
    
    require "luanet"
    
    --加载CLR的类型、实例化CLR对象
    luanet.load_assembly("System.Windows.Forms")
    luanet.load_assembly("System.Drawing")
    Form = luanet.import_type("System.Windows.Forms.Form")
    StartPosition = luanet.import_type("System.Windows.Forms.FormStartPosition")
    
    print(Form)
    print(StartPosition)
    复制代码

      上面的代码演示了如果利用LuaInterface的luanet在Lua中加载CLR的类型。在配置编译环境的时候一定要注意将两个dll同时拷贝到一个目录下,因为luanet.dll是依赖LuaInterfce.dll的。

      第二种:在C#工程中测试

      还是在外部单独编写lua代码文件,然后在C#工程中使用lua.DoFile接口运行lua代码。这种方式比较灵活并且能够更方便的测试LuaInterface所提供的各项功能,我们后面的测试代码均是在这种模式系下进行测试。

      这种模式下就不需要在lua脚本中手动require "luanet"了,因为已经手动将LuaInterface的引用添加到工程中了,lua脚本中直接使用luanet就可以访问各接口了。

      luanet.load_assembly函数:加载CLR程序集;

      luanet.import_type函数:加载程序集中的类型;

      luanet.get_constructor_bysig函数:显示获取某个特定的构造函数;

      c#主要代码如下:

    复制代码
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using LuaInterface;
    
    namespace TesLuaInterface
    {
        class TestClass2
        {
            public TestClass2(string str)
            {
                Console.WriteLine("called TestClass2(string str) str = {0}", str);
            }
    
            public TestClass2(int n)
            {
                Console.WriteLine("called TestClass2(int n) n = {0}", n);
            }
    
            public TestClass2(int n1, int n2)
            {
                Console.WriteLine("called TestClass2(int n1, int n2) n1 = {0}, n2 = {1}", n1, n2);
            }
        }
    
            // 加载和实例化CLR类型
            static void Main(string[] args)
            {
                Lua lua = new Lua();
    
                lua.DoFile("C:\luatest\testLuaNet.lua");
            }
        }
    }
    复制代码

      lua主要代码如下:

    复制代码
    -- 加载自定义类型,先加载程序集,在加载类型
    luanet.load_assembly("TestEnvi")
    TestClass = luanet.import_type("TesLuaInterface.TestClass2")
    
    obj1 = TestClass(2, 3)    -- 匹配public TestClass2(int n1, int n2)
    obj2 = TestClass("x")    -- 匹配public TestClass2(string str)
    obj3 = TestClass(3)        -- 匹public TestClass2(string str)
    
    TestClass_cons2 = luanet.get_constructor_bysig(TestClass, 'System.Int32')
    obj3 = TestClass_cons2(3)    -- 匹配public TestClass2(int n)
    复制代码

      TestEnvi为我建的工程代码的程序集名字,这一项是可以在工程属性中进行设置的,TestLuaInterface为测试代码的命名空间。

      从上面的构造函数的匹配可以看出,LuaInterface匹配构造函数的规律:

      LuaInterface匹配第一个能够匹配的构造函数,在这个过程中,numerical string(数字字符串)会自动匹配number,而number可以自动匹配string,所以TestClass(3)匹配到了参数为string的构造函数。

      如果一定要手动匹配某个构造函数,则可以使用luanet.get_constructor_bysic函数。

    (2)访问CLR类型对象的字段和方法

      Lua代码中,访问CLR类型对象的字段的方式和访问table的键索引一样,比如button1.Text、button["Text"];

      Lua代码中,访问CLR类型对象的函数的方式和调用table的函数一样,比如form:ShowDialog()。

      规则很简单,但在访问函数的时候,有以下几种情况需要注意的:

      (a)当有重载函数时,函数的匹配过程和上面提到的构造函数的匹配过程一样,自动匹配第一个能够匹配的函数。如果一定要手动调用某个特定参数的函数,可以使用luanet.get_method_bysig函数来制定,比如:

      setMethod=get_method_bysig(obj,'setValue','System.String')"
      setMethod('str')

      (b)当函数有out或ref参数时,out参数和ref参数和函数的返回值一起返回,并且调用函数时out参数不需要传入,比如:

        -- calling int obj::OutMethod1(int,out int,out int)
        retVal,out1,out2 = obj:OutMethod1(inVal)
        -- calling void obj::OutMethod2(int,out int)
        retVal,out1 = obj:OutMethod2(inVal) -- retVal ser´a nil
        -- calling int obj::RefMethod(int,ref int)

      (c)如果一个对象有两个Interface,并且两个Interface都有某个同名函数比如,IFoo.method()和IBar.method(),这种情况下obj["IFoo.method"]表示访问前者。

      访问CLR类型对象的字段和函数的示例代码如下:

    复制代码
    luanet.load_assembly("System.Windows.Forms")
    luanet.load_assembly("System.Drawing")
    Form = luanet.import_type("System.Windows.Forms.Form")
    Button = luanet.import_type("System.Windows.Forms.Button")
    Point = luanet.import_type("System.Drawing.Point")
    StartPosition = luanet.import_type("System.Windows.Forms.FormStartPosition")
    
    form1 = Form()
    button1 = Button()
    button2 = Button()
    position = Point(10, 10)
    start_position = StartPosition.CenterScreen
    
    button1.Text = "OK"
    button2["Text"] = "Cancel"
    button1.Location = position
    button2.Location = Point(button1.Left, button1.Height + button1.Top + 10)
    form1.Controls:Add(button1)
    form1.Controls:Add(button2)
    form1.StartPosition = start_position
    form1:ShowDialog()
    复制代码

    (3)事件处理,添加和删除事件委托

      LuaInterface为Event提供了Add和Remove函数来注册和移除事件处理函数。Add函数传入一个Lua函数,将其转换为一个CLR委托(delegate),并返回这个委托。

    function handle_mouseup(sender,args)
      print(sender:ToString() .. ’ MouseUp!’)
      button.MouseUp:Remove(handler)
    end
    handler = button.MouseUp:Add(handle_mouseup)

    (4)LuaInterface三种扩展CLR的方法

      LuaInterface提供了三种扩展CLR的方法,第一种就是上面提到的添加委托的方式,在需要delegate的地方传入Lua function,LuaInterface利用Lua function创建一个CLR delegate 并传入CLR。  

      第二种是在需要CLR Interface实例的地方传入一个Lua Table,比如:

    复制代码
    -- interface ISample { int DoTask(int x, int y); }
    -- SomeType.SomeMethod signature: int SomeMethod(ISample i)
    -- obj is instance of SomeType
    sum = {}
    function sum:DoTask(x,y)
    return x+y
    end
    -- sum is converted to instance of ISample
    res = obj:SomeMethod(sum)
    复制代码

      如果Interface里面有多个重载接口,那么Lua Table需要实现每一个版本的接口函数,并且要注意out和ref参数的处理:

    复制代码
    -- interface ISample2 { void DoTask1(ref int x, out int y);
    -- int DoTask2(int x, out int y); }
    -- SomeType.SomeMethod signature: int SomeMethod(ISample i)
    -- obj is instance of SomeType
    inc = {}
    function inc:DoTask1(x)
    return x+1,x
    end
    function inc:DoTask2(x)
    return x+1,x
    end
    res = obj:SomeMethod(sum)
    复制代码

      第三种是利用Lua Table继承CLR Class,也就是用Table作为其子类,这里CLR Class必须拥有virtual函数,并且Lua Table必须至少重写一个virtual函数。主要相关函数是luanet.make_object。

    复制代码
    -- class SomeObject {
    -- public virtual int SomeMethod(int x, int y) { return x+y; } }
    -- SomeType.SomeMethod signature: int SomeMethod(SomeObject o)
    -- obj is instance of SomeType
    some_obj = { const = 4 }
    function some_obj:SomeMethod(x,y)
    local z = self.base:SomeMethod(x,y)
    return z*self.const
    end
    SomeObject = luanet.import_type(’SomeObject’)
    luanet.make_object(some_obj,SomeObject)
    res = some_obj:SomeMethod(2,3) -- returns 20
    res = some_obj:ToString() -- calls base method
    res = obj:SomeMethod(some_obj) -- passing as argument
    复制代码

      因为Table作为子类实例,那么就可以在需要Class的地方传入这个Table实例。注意,如果Table没有重写任何virtual函数,则直接返回父类对象。当然,作为子类,可以直接访问父类中其他的还接口。

      以上三种归纳起来就是:Lua Function-->CLR delegate、Lua Table-->CLR Interface、 Lua Table-->CLR Class。

     Unity3D基于Mono虚拟机,所以理论上.NET的类库是可以直接在Unity3D中使用的。可是考虑到Unity3D跨平台的需要,我们选择的工具必须在各个平台获得良好的支持。在前文中提到的LuaInterface理论上是可以在Unity3D中使用的,可是由于IOS不支持反射机制,所以这个类库我们无法直接在Unity3D中使用的。

     ulua:基于luainterface升级版,uLua = Lua + LuaJIT + LuaInterface,全平台支持。在原生C的基础上使用LuaJit进行加速,如果uLua效率高,LuaJit有很大功劳,作者仅仅提供了uLua插件包,并未提供整套插件源码。此外,作者重写了loadfile、print等api,使用非常简单,导入package,就可以开始编写代码了。

    uLua = Lua + LuaJit(解析器、解释器) +LuaInterface。

    uLua方案比较成熟,它并没有太多自己的代码,主要是把LuaInterface和Lua解释器整合了一下,都是比较成熟的代码,相对会稳定一些。

    原文:Unity3D 预备知识:C#与Lua相互调用在Unity中使用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
    function LuaMethod(i)
    s = CSharpMethod(i); --调用C#方法
    return i,s;
    end

    (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)。    

  • 相关阅读:
    css属性操作2(外边距与内边距<盒子模型>)
    css的属性操作1
    css伪类
    属性选择器二
    属性选择器1
    03_MySQL重置root密码
    02_Mysql用户管理之Navicat下载及安装
    18.扩散模型
    17.广播模型
    16.友谊悖论
  • 原文地址:https://www.cnblogs.com/slysky/p/7919114.html
Copyright © 2011-2022 走看看