You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

11 KiB

第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<string> source_code;
      vector<string> source_object;
      vector<string> c_param;
      vector<string> 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<c_param.size(); j++)
                        {
                                 if(c_param[j] == src)
                                 {
                                 command_line += source_code[i];
                                        .....
                                         }
                       }
          }

在这部分的代码里面可以看到我们将压入的source_code内容进行循环。在循环之后必须对c_paramcompile_param也就是编译参数进行循环。当我们发现编译参数里面出现了$SRC这个替代字符串的时候就将source_code的内容其实就是源代码文件合并到command_line命令行里面去然后整合成为一个完整的、可以运行的命令行。

随后我再贴一部分代码,可以看到别的可替代字符串是怎么做的。

else if(c_param[j] == obj)
{
      command_line += source_object[i];
}
else if(c_param[j] == tar)
{
      command_line += target;
}

我们对替代字符串做了相同的比较如果是一致的话就将被替代内容添加到command_line变量里面组成一个完整的可运行命令行。

这个run函数其实就是在make的时候调用的函数。至于如何调用这一串command命令在C里面最简单的方式就是调用system函数或者使用execl函数系列。注意这个execl并不是来自微软的excel表格而是C语言的函数。

我们封装完了Lua部分的代码之后就需要将Lua的函数注册到Lua虚拟机里面这个我上一节已经具体说过了。

最后由于我们的Lua源代码本身就是一个Makefile文件所以我们不需要做过多的解析直接将这个源代码输入给Lua虚拟机即可。

string makefile;
ifstream in("my_makefile");
makefile = "my_makefile";
if(!in.is_open())
{
in.close();
}
else luaL_dofile(L, makefile.c_str());

在这段代码里面我们首先使用C++的fstream库中的ifstream来尝试读取是不是有这个my_makefile文件如果没有的话就跳过并且关闭文件句柄如果存在的话就把这个文件填入到Lua虚拟机中让Lua虚拟机直接运行这个源文件。所以这种方式是最简单快捷的。

代码有点多,不要担心,我带你梳理一下今天的内容。

  1. 利用C/C++语言和Lua源代码进行交互从Lua代码中获取数据并且在C语言里面进行算法的封装和计算最后将结果返回给Lua。 我们在C/C++语言里面进行大量的封装和算法提取并且也利用C/C++进行调用和结果的呈现这是一种常用的方式也就是C语言占比60%70%Lua代码占比30%40%。

  2. 另一种比较好的方式是,使用C/C++编写底层实现逻辑随后将数据传输给Lua让Lua来做逻辑运算最终将结果返回给C语言并且呈现出来。这是很多人在游戏开发中都会做的事情比如我们编写地图编辑器先在Lua中编写好逻辑用C语言在界面中呈现出来即可。如果反过来做的话那就会出现大量的硬代码是很不合适的。所以这种情况下C语言占比30%40%Lua代码占比60%70%。

  3. Lua可以是一种胶水语言。严谨地说像Python、Ruby等脚本语言都是合格的胶水语言。 在这种情况下胶水语言起到的作用就是粘合系统语言C/C++)和上层脚本逻辑。所以,使用胶水语言,就像是一种动态的配置文件。
    按照普通的配置文件来讲你需要手工解析比如类似INI、XML、JSON等配置文件随后按照这些文件的内容来做出一系列的配置但是胶水语言不需要它本身就是一种动态的语言。
    你也可以把它当作一种配置的文件就像今天讲的Makefile它可以不需要你检测语法问题这些问题在Lua虚拟机本身就已经做掉了你需要做的就是将我们脑海里想让它做的事情通过C和Lua的库代码进行整合直接使用就可以了。所以胶水语言的本身就是一个配置文件,同时它也是一个脚本语言源代码。

小结

在使用C/C++结合脚本语言的时候需要梳理这些内容比如哪些是放在C/C++硬代码里写的那些可以放到脚本语言里写梳理完后就可以将脚本语言和C/C++结合起来编写出易于修改脚本逻辑如果有不同需求可以很方便地改写脚本而不需要动C/C++硬代码)、易于使用的工具。

现在给你留一个小问题吧。

在Lua当中有table表的存在如何在C语言中给Lua源代码生成一个table表并且可以在Lua中正常使用呢

欢迎留言说出你的看法。我在下一节的挑战中等你!