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.

165 lines
8.4 KiB
Markdown

2 years ago
# 第20讲 | 脚本语言在游戏开发中有哪些应用?
上一次我们谈到了如何在游戏中嵌入脚本语言我们用的语言是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, "something");
return 1;
}
```
这里面lua\_tosting 就是这个函数的传入参数,传入的是一个字符串的参数;第二个参数也是字符串参数,其中 lua\_tosting 的第二个参数1或者2表明的是在Lua虚拟机的堆栈中从栈底到栈顶开始计数一般先压入的参数在第一个后压入的在第二个以此类推。返回1的意思是这个函数会返回一个参数这个参数就是我们前面 lua\_pushstring 后压入的这个内容something这就是返回的参数。
那么这个函数究竟怎么注册成为Lua函数呢我们来看这段代码。
```
lua_register(L, "test", &test_func);
```
lua\_register函数的功能是注册C函数到Lua虚拟机。其中L是虚拟机指针。这个在前面的代码都有说到而第二个参数test就是注册在Lua虚拟机中的函数名所以这个函数名叫test。第三个参数是函数指针我们把test\_func这个函数传入到lua\_register函数中。这样一个函数就注册好了。
那么如果我们在游戏中有许多许多的函数需要注册到Lua中那么这种写法是不是太慢了有没有一种快捷的写法来支持注册等操作呢
如果你没有C/C++的语言基础或者C/C++语言基础比较薄弱下面的内容可能需要花一点时间消化我也会竭尽所能解释清楚代码的意思但如果你已经是个C/C++程序员,那么下面的代码对你来说应该不会太难。
我们需要使用lua\_register我们先看它里面有什么参数。第一个是**字符串**,也就是**char\***第二个是**函数指针**,也就是**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, &ph_##fname
#define func_lua(fname) int ph_##fname(lua_State* L)
```
其中func\_reg是在给前面那个结构体初始化赋值的时候使用的因为我们知道如果我们需要给这个结构体赋值看起来的代码是这样
```
ph_func pobj = {"test", &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, "something");
return 1;
}
void register_func(lua_State* L)
{
int i;
for(i=0; i<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”以及&ph\_test\_func函数指针。
最后我们来看一个重要的函数,**register\_func**这个函数在后续将会输入一个Lua虚拟机指针但是我们要知道它在函数内部它做了什么东西。
```
int i;
for(i=0; i<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进行互通了。
## 小结
我总结一下今天所讲的内容。
* 在Lua与C的结合过程中C语言需要新建一个Lua虚拟机然后使用虚拟机的指针来操作Lua函数。
* 在程序的应用中使用C语言中的一些宏的技巧可以使代码能够便利地应用在程序里。
最后,给你留一个小问题。
如果使用Lua往C语言传递一些内容比如从C语言获取Lua脚本中某个变量的值应该怎么做
欢迎留言说出你的看法。我在下一节的挑战中等你!