在游戏中如何来使用LUA是本文要介绍的内容,主要是来学习游戏中lua的使用方法,具体内容的实现来看本文详解。首先,让我来简单的解释一下Lua解释器的工作机制,Lua解释器自身维护一个运行时栈,通过这个运行时栈,Lua解释器向主机程序传递参数,所以我们可以这样来得到一个脚本变量的值:
获取脚本的变量的值
- lua_pushstring(L, "var"); //将变量的名字放入栈
- lua_gettatbl(L, LUA_GLOBALSINDEX);变量的值现在栈顶
假设你在脚本中有一个变量 var = 100
你可以这样来得到这个变量值:
- int var = lua_tonumber(L, -1);
怎么样,是不是很简单?
Lua定义了一个宏让你简单的取得一个变量的值:
- lua_getglobal(L, name)
我们可以这样来取得一个变量的值:
- lua_getglobal(L, "var"); //变量的值现在栈顶
- int var = lua_tonumber(L, -1);
完整的测试代码如下:
- #include "lua.h"
- #inculde "lauxlib.h"
- #include "lualib.h"
- int main(int argc, char *argv[])
- {
- lua_State *L = lua_open();
- luaopen_base(L);
- luaopen_io(L);
- const char *buf = "var = 100";
- lua_dostring(L, buf);
- lua_getglobal(L, "var");
- int var = lua_tonumber(L, -1);
- assert(var == 100);
- lua_close(L);
- return 0;
- }
关于全局变量:
如上面我们所看到的,lua_getglobal()将Lua的一个全局变量放至栈顶,假如我们的脚本包含一个全局变量z,下面这段代码将获取z的值,代码:
- lua_getglobal(L, "z");
- z = (int)lua_tonumber(L, -1);
- lua_pop(L, 1);
与之对应的lua_setglobal()用来设置Lua的一个全局变量的值,下面的这段代码将全局变量z的值设置为10,代码:
- lua_pushnumber(L, 10);
- lua_setglobal(L, "z");
注意,不需要在你的Lua脚本中显式的全局变量,如果全局变量不存在,lua_setglobal()将创建一个新的全局变量。
在程序中调用脚本的函数
在你的游戏中应用Lua(1):调用函数
假设你在脚本中定义了一个函数:
- function main(number)
- numbernumber = number + 1
- return number
- end
在你的游戏代码中,你希望在某个时刻调用这个函数取得它的返回值。
在Lua中,函数等同于变量,所以你可以这样来取得这个函数:
- lua_getglobal(L, "main");//函数现在栈顶
现在,我们可以调用这个函数,并传递给它正确的参数:
- lua_pushnumber(L, 100); //将参数压栈
- lua_pcall(L, 1, 1, 0); //调用函数,有一个参数,一个返回值
- //返回值现在栈顶
- int result = lua_tonumber(L, -1);
result 就是函数的返回值
完整的测试代码如下:
- #include "lua.h"
- #include "lauxlib.h"
- #include "lualib.h"
- int main(int argc, char *argv[])
- {
- lua_State *L = lua_open();
- luaopen_base(L);
- const char *buf = "function main(number) number = number + 1 return number end";
- lua_dostring(buf);
- lua_getglobal(L, "main");
- lua_pushnumber(L, 100);
- lua_pcall(L, 1, 1, 0);
- int result = lua_tonumber(L, -1);
- assert(result == 101);
- lua_close(L);
- return 0;
}
在你的游戏中应用Lua(2):扩展Lua
Lua本身定位在一种轻量级的,灵活的,可扩充的脚本语言,这意味着你可以自由的扩充Lua,为你自己的游戏量身定做一个脚本语言。
你可以在主机程序中向脚本提供你自定的api,供脚本调用。
Lua定义了一种类型:lua_CFunction,这是一个函数指针,它的原型是:
- typedef int (*lua_CFunction) (lua_State *L);
这意味着只有这种类型的函数才能向Lua注册。
首先,我们定义一个函数
- int foo(lua_State *L)
- {
- //首先取出脚本执行这个函数时压入栈的参数
- //假设这个函数提供一个参数,有两个返回值
- //get the first parameter
- const char *par = lua_tostring(L, -1);
- printf("%s\n", par);
- //push the first result
- lua_pushnumber(L, 100);
- //push the second result
- lua_pushnumber(L, 200);
- //return 2 result
- return 2;
- }
我们可以在脚本中这样调用这个函数
- r1, r2 = foo("hello")
- print(r1..r2)
完整的测试代码如下:
- #include "lua.h"
- #include "lauxlib.h"
- #include "lualib.h"
- int foo(lua_State *L)
- {
- //首先取出脚本执行这个函数时压入栈的参数
- //假设这个函数提供一个参数,有两个返回值
- //get the first parameter
- const char *par = lua_tostring(L, -1);
- printf("%s\n", par);
- //push the first result
- lua_pushnumber(L, 100);
- //push the second result
- lua_pushnumber(L, 200);
- //return 2 result
- return 2;
- }
- int main(int argc, char *argv[])
- {
- lua_State *L = lua_open();
- luaopen_base(L);
- luaopen_io(L);
- const char *buf = "r1, r2 = foo("hello") print(r1..r2)";
- lua_dostring(L, buf);
- lua_close(L);
- return 0;
- }
程序输出:
- hello
- 100200
在你的游戏中应用Lua(3):using lua in cpp
lua和主机程序交换参数是通过一个运行时栈来进行的,运行时栈的信息放在一个lua_State的结构中,lua提供的api都需要一个lua_State*的指针,除了一个:
- lua_open();
这个函数将返回一个lua_State*型的指针,在你的游戏代码中,你可以仅仅拥有一个这样的指针,也可以有多个这样的指针。
***,你需要释放这个指针,通过函数:
- lua_close(L);
注意这个事实,在你的主机程序中,open()与close()永远是成对出现的,在c++中,如果有一些事情是成对出现的,这通常意味着你需要一个构造函数和一个析构函数,所以,我们首先对lua_State做一下封装:
- #ifndef LUA_EXTRALIBS
- #define LUA_EXTRALIBS /* empty */
- #endif
- static const luaL_reg lualibs[] =
- {
- {"base", luaopen_base},
- {"table", luaopen_table},
- {"io", luaopen_io},
- {"string", luaopen_string},
- {"math", luaopen_math},
- {"debug", luaopen_debug},
- {"loadlib", luaopen_loadlib},
- /* add your libraries here */
- LUA_EXTRALIBS
- {NULL, NULL}
- };
这是lua提供给用户的一些辅助的lib,在使用lua_State的时候,你可以选择打开或者关闭它。
完整的类实现如下:
- //lua_State
- class state
- {
- public:
- state(bool bOpenStdLib = false)
- :
- err_fn(0)
- {
- L = lua_open();
- assert(L);
- if (bOpenStdLib)
- {
- open_stdlib();
- }
- }
- ~state()
- {
- lua_setgcthreshold(L, 0);
- lua_close(L);
- }
- void open_stdlib()
- {
- assert(L);
- const luaL_reg *lib = lualibs;
- for (; lib->func; lib++)
- {
- lib->func(L); /* open library */
- lua_settop(L, 0); /* discard any results */
- }
- }
- lua_State* get_handle()
- {
- return L;
- }
- int error_fn()
- {
- return err_fn;
- }
- private:
- lua_State *L;
- int err_fn;
- };
通常我们仅仅在游戏代码中使用一个lua_State*的指针,所以我们为它实现一个单件,默认打开所有lua提供的lib:
- //return the global instance
- state* lua_state()
- {
- static state L(true);
- return &L;
- }
在你的游戏中应用Lua(3):using lua in cpp(封装栈操作)
前面提到了lua与主机程序是通过一个运行时栈来交换信息的,所以我们把对栈的访问做一下简单的封装。
我们利用从c++的函数重载机制对这些操作做封装,重载提供给我们一种以统一的方式来处理操作的机制。
向lua传递信息是通过压栈的操作来完成的,所以我们定义一些Push()函数:
- inline void Push(lua_State *L, int value);
- inline void Push(lua_State *L, bool value);
...
对应简单的c++内建类型,我们实现出相同的Push函数,至于函数内部的实现是非常的简单,只要利用lua提供的api来实现即可,例如:
- inline void Push(lua_State *L, int value)
- {
- lua_pushnumber(L, value);
- }
这种方式带来的好处是,在我们的代码中我们可以以一种统一的方式来处理压栈操作,如果有一种类型没有定义相关的压栈操作,将产生一个编译期错误。
后面我会提到,如何将一个用户自定义类型的指针传递到lua中,在那种情况下,我们的基本代码无须改变,只要添加一个相应的Push()函数即可。
记住close-open原则吧,它的意思是对修改是封闭的,对扩充是开放的,好的类库设计允许你扩充它,而无须修改它的实现,甚至无须重新编译。
《c++泛型设计新思维》一书提到了一种技术叫type2type,它的本质是很简单:
- template <typename T>
- struct type2type
- {
- typedef T U;
- };
正如你看到的,它并没有任何数据成员,它的存在只是为了携带类型信息。
类型到类型的映射在应用于重载函数时是非常有用的,应用type2type,可以实现编译期的分派。
下面看看我们如何在从栈中取得lua信息时应用type2type:
测试类型:由于lua的类型系统与c++是不相同的,所以,我们要对栈中的信息做一下类型检测。
- inline bool Match(type2type<bool>, lua_State *L, int idx)
- {
- return lua_type(L, idx) == LUA_TBOOLEAN;
- }
类似的,我们要为cpp的内建类型提供相应的Match函数:
- inline bool Match(type2type<int>, lua_State *L, int idx);
- inline bool Match(type2type<const char*>, lua_State *L, int idx);
可以看出,type2type的存在只是为了在调用Match时决议到正确的函数上,由于它没有任何成员,所以不存在运行时的成本。
同样,我们为cpp内建类型提供Get()函数:
- inline bool Get(type2type<bool>, lua_State *L, int idx)
- {
- return lua_toboolean(L, idx);
- }
- inline int Get(type2type<int>, lua_State *L, int idx)
- {
- return static_cast<int>(lua_tonumber(L, idx));
- }
我想你可能注意到了,在int Get(type2type<int>)中有一个转型的动作,由于lua的类型系统与cpp的类型不同,所以转型动作必须的。
除此之外,在Get重载函数(s)中还有一个小小的细节,每个Get的函数的返回值是不相同的,因为重载机制是依靠参数的不同来识别的,而不是返回值。
前面说的都是一些基础的封装,下来我们将介绍如何向lua注册一个多参数的c函数。还记得吗?利用lua的api只能注册int (*ua_CFunction)(lua_State *)型的c函数,别忘记了,lua是用c写的。
在你的游戏中应用Lua(3):using lua in cpp(注册不同类型的c函数)之一
前面说到,我们可以利用lua提供的api,向脚本提供我们自己的函数,在lua中,只有lua_CFunction类型的函数才能直接向lua注册,lua_CFunction实际上是一个函数指针:
- typedef int (*lua_CFunction)(lua_State *L);
而在实际的应用中,我们可能需要向lua注册各种参数和返回值类型的函数,例如,提供一个add脚本函数,返回两个值的和:
- int add(int x, int y);
为了实现这个目的,首先,我们定义个lua_CFunction类型的函数:
- int add_proxy(lua_State *L)
- {
- //取得参数
- if (!Match(TypeWrapper<int>(), L, -1))
- return 0;
- if (!Match(TypeWrapper<int>(), L, -2))
- return 0;
- int x = Get(TypeWrapper<int>(), L, -1);
- int y = Get(TypeWrapper<int>(), L, -1);
- //调用真正的函数
- int result = add(x, y);
- //返回结果
- Push(result);
- return 1;
- }
现在,我们可以向lua注册这个函数:
- lua_pushstring(L, “add”);
- lua_pushcclosure(L, add_proxy, 0);
- lua_settable(L, LUA_GLOBALINDEX);
在脚本中可以这样调用这个函数:
- print(add(100, 200))
从上面的步骤可以看出,如果需要向lua注册一个非lua_CFunction类型的函数,需要:
1、为该函数实现一个封装调用。
2、在封装调用函数中从lua栈中取得提供的参数。
3、使用参数调用该函数。
4、向lua传递其结果。
注意,我们目前只是针对全局c函数,类的成员函数暂时不涉及,在cpp中,类的静态成员函数与c函数类似。
假设我们有多个非lua_CFunction类型的函数向lua注册,我们需要为每一个函数重复上面的步骤,产生一个封装调用,可以看出,这些步骤大多是机械的,因此,我们需要一种方式自动的实现上面的步骤。
首先看步骤1,在cpp中,产生这样一个封装调用的函数的***的方式是使用template,我们需要提供一个lua_CFunction类型的模板函数,在这个函数中调用真正的向脚本注册的函数,类似于这样:
- template <typename Func>
- inline int register_proxy(lua_State *L)
现在的问题在于:我们要在这个函数中调用真正的函数,那么我们必须要在这个函数中取得一个函数指针,然而,lua_CFunction类型的函数又不允许你在增加别的参数来提供这个函数指针,现在该怎么让regisger_proxy函数知道我们真正要注册的函数呢?
在oop中,似乎可以使用类来解决这个问题:
- template <Func>
- struct register_helper
- {
- explicit register_helper(Func fn) : m_func(fn)
- {}
- int register_proxy(lua_State *L);
- protected:
- Func m_func;
- };
可是不要忘记,lua_CFunction类型指向的是一个c函数,而不是一个成员函数,他们的调用方式是不一样的,如果将上面的int register_proxy()设置为静态成员函数也不行,因为我们需要访问类的成员变量m_func;
让我们再观察一下lua_CFunction类型的函数:
- int register_proxy(lua_State *L);
我们看到,这里面有一个lua_State*型的指针,我们能不能将真正的函数指针放到这里面存储,到真正调用的时候,再从里面取出来呢?
Lua提供了一个api可以存储用户数据:
- Lua_newuserdata(L, size)
在适当的时刻,我们可以再取出这个数据:
- lua_touserdata(L, idx)
ok,现在传递函数指针的问题我们已经解决了,后面再看第二步:取得参数。
在你的游戏中应用Lua(3):using lua in cpp(注册不同类型的c函数)之二
在解决了传递函数指针的问题之后,让我们来看看调用函数时会有一些什么样的问题。
首先,当我们通过函数指针调用这个函数的时候,由于我们面对的是未知类型的函数,也就是说,我们并不知道参数的个数,参数的类型,还有返回值的类型,所以我们不能直接从lua栈中取得参数,当然,我们可以通过运行时测试栈中的信息来得到lua传递进来的参数的个数和类型,这意味着我们在稍后通过函数指针调用函数时也需要动态的根据参数的个数和类型来决议到正确的函数,这样,除了运行时的成本,cpp提供给我们的强类型检查机制的好处也剩不了多少了,我们需要的是一种静态的编译时的“多态”。
在cpp中,至少有两种方法可以实现这点。最直接简单的是使用函数重载,还有一种是利用模板特化机制。
简单的介绍一下模板特化:
在cpp中,可以针对一个模板函数或者模板类写出一些特化版本,编译器在匹配模板参数时会寻找最合适的一个版本。类似于这样:
- templat <typename T>
- T foo()
- {
- T tmp();
- return tmp;
- }
- //提供特化版本
- template <>
- int foo()
- {
- return 100;
- }
在main()函数中,我们可以显示指定使用哪个版本的foo:
- int main(int argc, char **argv)
- {
- cout << foo<int>() << endl;
- return 0;
- }
程序将输出100,而不是0,以上代码在 g++中编译通过,由于vc6对于模板的支持不是很好,所以有一些模板的技术在vc6中可能不能编译通过。
所以***使用重载来解决这个问题,在封装函数调用中,我们首先取得这个函数指针,然后,我们要提供一个Call函数来真正调用这个函数,类似于这样:
- //伪代码
- int Call(pfn, lua_State *L, int idx)
可是我们并不知道这个函数指针的类型,现在该怎么写呢?别忘记了,我们的register_proxy()是一个模板函数,它有一个参数表示了这个指针的类型:
- template <typename Func>
- int register_proxy(lua_State *L)
- {
- //伪代码,通过L参数取得这个指针
- unsigned char *buffer = get_pointer(L);
- //对这个指针做强制类型转化,调用Call函数
- return Call(*(Func*)buffer, L, 1);
- }
由重载函数Call调用真正的函数,这样,我们可以使用lua api注册相关的函数,下来我们提供一个注册的函数:
- template <typename Func>
- void lua_pushdirectclosure(Func fn, lua_State *L, int nUpvalue)
- {
- //伪代码,向L存储函数指针
- save_pointer(L);
- //向lua提供我们的register_proxy函数
- lua_pushcclosure(L, register_proxy<Func>, nUpvalue + 1);
- }
再定义相关的注册宏:
- #define lua_register_directclosure(L, func) \
- lua_pushstring(L, #func);
- lua_pushdirectclosure(func, L, 1);
- lua_settable(L, LUA_GLOBALINDEX)
现在,假设我们有一个int add(int x, int y)这样的函数,我们可以直接向lua注册:
- lua_register_directclosure(L, add);
看,***使用起来很方便吧,我们再也不用手写那么多的封装调用的代码啦,不过问题还没有完,后面我们还得解决Call函数的问题。
在你的游戏中应用Lua(3):using lua in cpp(注册不同类型的c函数)之三
下面,让我们集中精力来解决Call重载函数的问题吧。
前面已经说过来,Call重载函数接受一个函数指针,然后从lua栈中根据函数指针的类型,取得相关的参数,并调用这个函数,然后将返回值压入lua栈,类似于这样:
- //伪代码
- int Call(pfn, lua_State *L, int idx)
现在的问题是pfn该如何声明?我们知道这是一个函数指针,然而其参数,以及返回值都是未知的类型,如果我们知道返回值和参数的类型,我们可以用一个typedef来声明它:
- typedef void (*pfn)();
- int Call(pfn fn, lua_State *L, int idx);
我们知道的返回值以及参数的类型只是一个模板参数T,在cpp中,我们不能这样写:
- template <typename T>
- typedef T (*Func) ();
一种解决办法是使用类模板:
- template <typename T>
- struct CallHelper
- {
- typedef T (*Func) ();
- };
然后在Call中引用它:
- template <typename T>
- int Call(typename CallHelper::Func fn, lua_State *L, int idx)
注意typename关键字,如果没有这个关键字,在g++中会产生一个编译警告,它的意思是告诉编译器,CallHelper::Func是一个类型,而不是变量。
如果我们这样来解决,就需要在CallHelper中为每种情况大量定义各种类型的函数指针,还有一种方法,写法比较古怪,考虑一个函数中参数的声明:
- void (int n);
首先是类型,然后是变量,而应用于函数指针上:
- typedef void (*pfn) ();
- void (pfn fn);
事实上,可以将typedef直接在参数表中写出来:
- void (void (*pfn)() );
这样,我们的Call函数可以直接这样写:
- //针对没有参数的Call函数
- template <typename RT>
- int Call(RT (*Func) () , lua_State *L, int idx);
- {
- //调用Func
- RT ret = (*Func)();
- //将返回值交给lua
- Push(L, ret);
- //告诉lua有多少个返回值
- return 1;
- }
- //针对有一个参数的Call
- template <typename T, typename P1>
- int Call(RT (*Func)(), lua_State *L, int idx)
- {
- //从lua中取得参数
- if (!Match(TypeWrapper<P1>(), L, -1)
- return 0;
- RT ret = (*Func) (Get(TypeWrapper<P1>(), L, -1));
- Push(L, ret);
- return 1;
- }
按照上面的写法,我们可以提供任意参数个数的Call函数,现在回到最初的时候,我们的函数指针要通过lua_State *L来存储,这只要利用lua提供的api就可以了,还记得我们的lua_pushdirectclosure函数吗:
- template <typename Func>
- void lua_pushdirectclosure(Func fn, lua_State *L, int nUpvalue)
- {
- //伪代码,向L存储函数指针
- save_pointer(L);
- //向lua提供我们的register_proxy函数
- lua_pushcclosure(L, register_proxy<Func>, nUpvalue + 1);
- }
其中,save_pointer(L)可以这样实现:
- void save_pointer(lua_State *L)
- {
- unsigned char* buffer = (unsigned char*)lua_newuserdata(L, sizeof(func));
- memcpy(buffer, &func, sizeof(func));
- }
而在register_proxy函数中:
- template <typename Func>
- int register_proxy(lua_State *L)
- {
- //伪代码,通过L参数取得这个指针
- unsigned char *buffer = get_pointer(L);
- //对这个指针做强制类型转化,调用Call函数
- return Call(*(Func*)buffer, L, 1);
- }
- get_pointer函数可以这样实现:
- unsigned char* get_pointer(lua_State *L)
- {
- return (unsigned char*) lua_touserdata(L, lua_upvalueindex(1));
- }
这一点能够有效运作主要依赖于这样一个事实:
我们在lua栈中保存这个指针之后,在没有对栈做任何操作的情况下,又把它从栈中取了出来,所以不会弄乱lua栈中的信息,记住,lua栈中的数据是由用户保证来清空的。
到现在,我们已经可以向lua注册任意个参数的c函数了,只需简单的一行代码:
- lua_register_directclosure(L, func)就可以啦
在你的游戏中应用Lua(3):Using Lua in cpp(基本数据类型、指针和引用)之一
- Using Lua in cpp(基本数据类型、指针和引用)
前面介绍的都是针对cpp中的内建基本数据类型,然而,即使是这样,在面对指针和引用的时候,情况也会变得复杂起来。
使用前面我们已经完成的宏lua_register_directclosure只能注册by value形式的参数的函数,当参数中存在指针和引用的时候(再强调一次,目前只针对基本数据类型):
1、如果是一个指针,通常实现函数的意图是以这个指针传递出一个结果来。
2、如果是一个引用,同上。
3、如果是一个const指针,通常只有面对char*的时候才使用const,实现函数的意图是,不会改变这个参数的内容。其它情况一般都避免出现使用const指针。
4、如果是一个const引用,对于基本数据类型来说,一般都避免出现这种情况。
Lua和cpp都允许函数用某种方式返回多个值,对于cpp来说,多个返回值是通过上述的第1和第2种情况返回的,对于lua来说,多个返回值可以直接返回:
- --in Lua
- function swap(x, y)
- tmp = x
- x = y
- y = tmp
- return x, y
- end
- x = 100
- y = 200
- x, y = swap(x, y)
- print(x..y)
- 程序输出:200100
同样的,在主机程序中,我们也可以向Lua返回多个值:
- int swap(lua_State *L)
- {
- //取得两个参数
- int x = Get(TypeWrapper<int>(), L, -1);
- int y = Get(TypeWrapper<int>(), L, -2);
- //交换值
- int tmp = x;
- x = y;
- y = tmp;
- //向Lua返回值
- Push(L, x);
- Push(L, y);
- //告诉Lua我们返回了多少个值
- return 2;
- }
现在我们可以在Lua中这样调用这个函数:
- x = 100
- y = 200
- x, y = swap(x, y)
在我们的register_proxy函数中只能对基本数据类型的by value方式有效,根据我们上面的分析,如果我们能够在编译期知道,对于一个模板参数T:
1、这是一个基本的数据类型,还是一个用户自定义的数据类型?
2、这是一个普通的指针,还是一个iterator?
3、这是一个引用吗?
4、这是一个const 普通指针吗?
5、这是一个const 引用吗?
如果我们能知道这些,那么,根据我们上面的分析,我们希望:(只针对基本数据类型)
1、 如果这是一个指针,我们希望把指针所指的内容返回给Lua。
2、 如果这是一个引用,我们希望把引用的指返回给Lua。
3、 如果这是const指针,我们希望将从Lua栈中取得的参数传递给调用函数。
4、 如果这是一个const引用,我们也希望把从Lua栈中取得的参数传递给调用函数。
小结:在游戏中如何来使用LUA的内容介绍完了,希望通过本文的学习能对你有所帮助!