上一节,我们讲了脚本语言在游戏开发中的应用,我列举了很多 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 中所使用的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

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 命令操作的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

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++ 里面是如何定义这个类的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20

struct my_make

{

      string target;

      string compiler;

      string linker;

      vector<string> source_code;

      vector<string> source_object;

      vector<string> c_param;

      vector<string> l_param;

};

为了便于理解,我将 C++ 类声明改成了 struct,也就是把成员变量改为公有变量,你可以通过一个对象直接访问到。

随后,我们来看一下如何将 target、compiler 和 linker 传入到 C 函数里面。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

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 也是一样的道理。

我们接着看下面两行。

1
2
3
4

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、TARGET、