<audio id="audio" title="第20讲 | 脚本语言在游戏开发中有哪些应用？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/de/3b/dee9c9b0ac13b8adef126d53d397b53b.mp3"></audio>

上一次，我们谈到了如何在游戏中嵌入脚本语言，我们用的语言是Lua。Lua语言具有轻量级、速度快的特点，而且API的调用也非常方便和直观。现在，我们仍然拿Lua脚本，试着把它应用在我们开发的游戏中。

我们使用C语言来对Lua脚本的绑定做一次深入的剖析，然后来看一下，在游戏开发中绑定了脚本语言后，脚本语言能做些什么事情。

首先，我们要明白一点，事实上**任何模块都可以使用脚本语言编写**。当然在游戏开发的过程中，需要分工明确，如果不分工的话，效率可能会比较低。

在需要某些效率要求非常高的情况下，一般是用C、C++或ASM语言，将底层模块搭建好，然后将一些逻辑部分分出来，给脚本语言处理。比如我们熟知的服务器端，可以使用C/C++来编写服务器端的IOCP或者epoll处理；而接收、发送、逻辑处理等等，都可以使用绑定脚本的方式编写。

我们在编写的过程中，需要对C/C++的语言和代码有个了解，我们需要先考虑这个函数。

```
int test_func(lua_State *L)    
{    
     return 0;    
}

```

这只是一个空的C函数，在这个函数里面，我们看到它的传入参数是lua_State，接受一个指针L。随后，这个函数返回一个0。

lua_State是Lua虚拟机的对象指针，也就是我们需要把前面new出来的一个虚拟机传进去，才可以保证在这个函数里面，使用的是一致的虚拟机。

这个函数的作用是，**只要注册到了Lua虚拟机里面，它就是lua的一个函数，其中在lua函数中，传入的参数由函数内部决定**。

比如我可以这么写：

```
int test_func(lua_State *L)    
{    
     const char *p1 = lua_tostring(L, 1);    
     const char *p2 = lua_tostring(L, 2);    
     // .... do something    
     lua_pushstring(L, &quot;something&quot;);    
     return 1;    
}

```

这里面，lua_tosting 就是这个函数的传入参数，传入的是一个字符串的参数；第二个参数也是字符串参数，其中 lua_tosting 的第二个参数1或者2，表明的是在Lua虚拟机的堆栈中从栈底到栈顶开始计数，一般先压入的参数在第一个，后压入的在第二个，以此类推。返回1的意思是，这个函数会返回一个参数，这个参数就是我们前面 lua_pushstring 后压入的这个内容something，这就是返回的参数。

那么这个函数究竟怎么注册成为Lua函数呢？我们来看这段代码。

```
lua_register(L, &quot;test&quot;, &amp;test_func); 

```

lua_register函数的功能是，注册C函数到Lua虚拟机。其中L是虚拟机指针。这个在前面的代码都有说到，而第二个参数test就是注册在Lua虚拟机中的函数名，所以这个函数名叫test。第三个参数是函数指针，我们把test_func这个函数传入到lua_register函数中。这样，一个函数就注册好了。

那么，如果我们在游戏中有许多许多的函数需要注册到Lua中，那么这种写法是不是太慢了，有没有一种快捷的写法来支持注册等操作呢？

如果你没有C/C++的语言基础，或者C/C++语言基础比较薄弱，下面的内容可能需要花一点时间消化，我也会竭尽所能解释清楚代码的意思，但如果你已经是个C/C++程序员，那么下面的代码对你来说应该不会太难。

我们需要使用lua_register，我们先看它里面有什么参数。第一个是**字符串**，也就是**char*；<strong>第二个是**函数指针</strong>，也就是**int (**)(lua_State**)** 这种形式的。

那么，我们需要定义一个struct结构，这个结构可以这么写：

```
   #define _max 256    
    typedef struct _ph_func    
    {    
          char ph_name[_max];    
          int (*ph_p_func)(lua_State*);    
    } ph_func;


```

我们定义了一个struct结构，这个结构的名字叫_ph_func，名字叫什么并没有关系，但是最开始有一个typedef，这说明在这个结构声明完后，接下来最后一行ph_func就是替代最初定义的那个_ph_func的名字，替代的结果是，**ph_func 等同于struct _ph_func**，这在很多C语言的代码里面经常能见到。

接下来，我们看到char ph_name[_max]。其中_max的值为256。我相信你应该能理解这句话。第二个变量就是我们所看到的函数指针，其中ph_p_func是函数指针，其中函数指针指向的内容目前暂时还没有确定，我们将在后续初始化这个结构变量的时候进行赋值。

我们来仔细看一下这两段宏的内容。

```
#define func_reg(fname) #fname, &amp;ph_##fname
#define func_lua(fname) int ph_##fname(lua_State* L)

```

其中func_reg是在给前面那个结构体初始化赋值的时候使用的，因为我们知道，如果我们需要给这个结构体赋值，看起来的代码是这样：

```
ph_func pobj =  {&quot;test&quot;, &amp;test_func};

```

那么由于我们有大量的函数需要注册，所以我们将之拆分为宏，其中#fname的意思是，将fname变为字符串，而ph_##fname的意思是使用##字符，将前后内容连接起来。

通过这个宏，比如我们输入一个a赋值给 fname，那么#fname就变成字符串"a"，通过 ph_##fname，结果就是ph_a。

接下来的代码，是方便在代码中编写一个一个lua注册函数用的，所以很明显，和上述的宏一样，我们只需要输入a，那么这个函数就变成了 int ph_a(lua_State* L)；

定义好了这两个宏，我们怎么来应用呢？

```
func_lua(test_func);
          
ph_func p_funcs[] =    
{    
      { func_reg(test_func) },    
};    
func_lua(test_func)    
{    
     const char *p1 = lua_tostring(L, 1);    
     const char *p2 = lua_tostring(L, 2);    
     // .... do something    
     lua_pushstring(L, &quot;something&quot;);    
     return 1;    
}    
void register_func(lua_State* L)    
{    
      int i;    
      for(i=0; i&lt;sizeof(p_funcs)/sizeof(p_funcs[0]); i++)    
      lua_register(L, p_funcs[i].ph_name,  p_funcs[i].ph_p_func);    
}

```

首先，联系上面的宏，第一行代码是使用func_lua，所以func_lua输入的宏参数是test_func。于是，通过这个宏，我们最终得到的函数名字是int ph_test_func(lua_State* L); 。

```
ph_func p_funcs[] =  
{    
      { func_reg(test_func) },    
};

```

这段代码，使用的是func_reg的宏。test_func最终在宏里面，变成了 “test_func”，以及&amp;ph_test_func函数指针。

最后我们来看一个重要的函数，**register_func**，这个函数在后续将会输入一个Lua虚拟机指针，但是我们要知道它在函数内部它做了什么东西。

```
int i;    
for(i=0; i&lt;sizeof(p_funcs)/sizeof(p_funcs[0]); i++)    
      lua_register(L, p_funcs[i].ph_name,  p_funcs[i].ph_p_func)

```

在循环里面，我们计算p_funcs的结构数组的长度，怎么计算的呢？

首先，我们使用sizeof编译器内置函数，来取得p_funcs整个数组的长度，这整个长度等于sizeof(ph_func)的值乘以数组长度。而ph_func结构体拥有一个字符串数组，每个数组长度是256，加上一个函数指针为4字节长，所以长度是260。而如果有两个数组元素，那就是520的长度。

以此类推，/sizeof(p_funcs[0]的意思是，我们取出第一个数组的长度作为被除数。事实上就是结构体本身的长度，所以就是结构体数组总长度除以结构体长度，就是一共有多少数组元素，随后进行循环。

在循环的过程中，我们看到，我们填入了结构体里面的两个变量ph_name以及ph_p_func，这样一来，我们只需要通过宏加上一些小技巧，就可以把Lua的函数都注册到C程序里面，我们假设这个C程序就是游戏的话，那么我们很容易就可以和Lua进行互通了。

## 小结

我总结一下今天所讲的内容。

<li>
在Lua与C的结合过程中，C语言需要新建一个Lua虚拟机，然后使用虚拟机的指针来操作Lua函数。
</li>
<li>
在程序的应用中，使用C语言中的一些宏的技巧，可以使代码能够便利地应用在程序里。
</li>

最后，给你留一个小问题。

如果使用Lua往C语言传递一些内容，比如从C语言获取Lua脚本中某个变量的值，应该怎么做？

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