# 第21讲 | 如何使用脚本语言编写周边工具? 上一节,我们讲了脚本语言在游戏开发中的应用,我列举了很多C语言代码,这些代码做了这样一些事情: 1. 使用C语言和Lua语言进行沟通; 2. 在C语言代码里,使用了宏和结构,方便批量注册和导入C语言函数; 3. Lua代码如何传输内容给C语言; 4. Lua虚拟机堆栈的使用。 这一节,我们要用Lua脚本来编写一个游戏周边工具Makefile。游戏周边工具有很多种,并没有一个统一的说法,比如在线更新工具、补丁打包工具、人物模型编辑工具、游戏环境设置工具等等。 你或许就会问了,那我为什么选择Makefile工具来编写,而不选择别的周边工具来编写呢? 因为这个工具简单、小巧,我们可以将Lua脚本语句直接拿来用作Makefile语句,而在这个过程中,我们同时还可以通过Lua语句来了解Lua的工作机理。 而且这个编写过程我们一篇文章差不多就可以说清楚。 而别的周边工具编写起来可能会比较复杂,比如如果要编写类似Awk的工具的话,就要编写文本解析和文件查找功能;如果编写游戏更新工具的话,就必须涉及网络基础以及压缩解压缩的功能。 简单直白地说,Makefile是一种编译器的配置脚本文件。这个文件被GNU Make命令读取,并且解析其中的意义,调用C/C++(绝大部分时候)或者别的编译器(小部分)来将源代码编译成为执行文件或者动态、静态链接库。 我们可以自己定义一系列的规则,然后通过顺利地运行gcc、cl 等命令来进行源代码编译。 我们先定义一系列函数,来固定我们在Lua中所使用的函数。 ``` int compiler(lua_State*); int linker(lua_State*); int target(lua_State*); int source_code(lua_State*); int source_object(lua_State*); int shell_command(lua_State*); int compile_param(lua_State*); int link_param(lua_State*); int make(lua_State*); ``` 这些都是注册到Lua内部的C/C++函数。我们现在要将这些函数封装给Lua使用,但是在这之前,我们要将大部分的功能都在C/C++里编写好。 随后,我们来看一下,在Lua脚本里面,具体是怎么实现Make命令操作的。 ``` target("test.exe"); linker("c:\\develop\\dm\\bin\\dmc.exe"); compiler("c:\\develop\\dm\\bin\\dmc.exe"); source_code("c.cpp", "fun.cpp", "x.cpp"); source_object("c.obj", "fun.obj", "x.obj"); compile_param( "$SRC", "-c", "-Ic:/develop/dm/stlport/stlport", "c:/develop/dm/lib/stlp45dm_static.lib"); link_param("$TARGET", "$OBJ"); make(); shell_command("del *.obj"); ``` 首先,第一行对应的就是目标文件target函数,后续的每一个Lua函数都能在最初的函数定义里找到。 在这个例子当中,我们使用的是DigitalMars的C/C++编译器,执行文件叫dmc.exe。我们可以看到,在linker和compiler函数里都填写了dmc.exe,说明编译器和链接器都是dmc.exe文件。 现在来看一下在C/C++里面是如何定义这个类的。 ``` struct my_make { string target; string compiler; string linker; vector source_code; vector source_object; vector c_param; vector l_param; }; ``` 为了便于理解,我将C++类声明改成了struct,也就是把成员变量改为公有变量,你可以通过一个对象直接访问到。 随后,我们来看一下如何将target、compiler和linker传入到C函数里面。 ``` int compiler(lua_State* L) { string c = lua_tostring(L, 1); get_my_make().compiler = c; return 0; } int linker(lua_State* L) { string l = lua_tostring(L, 1); get_my_make().linker = l; return 0; } int target(lua_State* L) { string t = lua_tostring(L, 1); get_my_make().target = t; return 0; } ``` 在这三个函数里面,我们看到,get\_my\_make函数就是返回一个my\_make类的对象。这个具体就不进行说明了,因为返回对象有多种方式,比如new一个对象并且return,或者直接返回一个静态对象。 随后,我们直接使用了Lua函数lua\_tostring,来得到Lua传入的参数,比如如果是target的话,我们就会得到”test.exe”,并且将这个字符串传给my\_make对象的 string target 变量。后续的compiler、linker也是一样的道理。 我们接着看下面两行。 ``` source_code("c.cpp", "fun.cpp", "x.cpp"); source_object("c.obj", "fun.obj", "x.obj"); ``` 这两行填入了cpp源文件以及obj中间文件,这些填入的参数并没有一个固定值,可能是1个,也可能是100个,那在C/C++和Lua的结合里面,我们应该怎么做呢? 我们看到一个函数lua\_gettop。这个函数是取得在当前函数中,虚拟机中堆栈的大小,所以返回的值,就是堆栈的大小值,比如我们传入3个参数,那么返回的就是3。 接下来可以看到,使用Lua的计数方式,从1开始计数,并且循环结束的条件是和堆栈大小一样大,然后就在循环内,将传入的参数字符串,压入到C++的vector中。 随后的source\_object、compile\_param和link\_param都是相同的方法,将传入的参数压入到vector中。 你可能要问了,我在Lua的代码中看到了$TARGET、$OBJ、$SRC等字样的字符串,这些字符串的处理在哪里,这些字符串又是做什么的呢? 这些字符串是替代符号,你可以理解为C语言中printf函数的格式化符号,例如 “%d %s”等等,虽然在这里,这些符号都是自己定义的,但是我们仍然需要解析它们。 其实解析的步骤并不难,我们只需要将vector内的内容提取出来,对比是不是字符串$TARGET等,如果是的话,就被替代为前面我们在target函数或者source\_code函数中所定义的内容。 我们拿source\_code部分来举例,来看一下部分代码。 ``` void run() { string command_line; string src = "$SRC"; string tar = "$TARGET"; string obj = "$OBJ"; for(int i = 0; i < source_code.size(); i++) { .............. for(int j=0; j