在你的游戏中应用LUA

本文转自zx9597446的Blog,
欢迎对Lua感兴趣的朋友互相交流, mailto: [email protected] 

在你的游戏中应用Lua(1):在你的游戏代码中运行解释器

  通常,你希望在你的游戏开始的时候读取一些信息,以配置你的游戏,这些信息通常都是放到一个文本文件中,在你的游戏启动的时候,你需要打开这个文件,然后解析字符串,找到所需要的信息。

  是的,或许你认为这样就足够了,为什么还要使用Lua呢?

  应用于“配置”这个目的,Lua提供给你更为强大,也更为灵活的表达方式,在上一种方式中,你无法根据某些条件来配置你的游戏,Lua提供给你灵活的表达方式,你可以类似于这样来配置你的游戏:

if player:is_dead() then
do_something()
else
do_else()
end

更为重要的是,在你做了一些修改之后,完全不需要重新编译你的游戏代码。

通常,在游戏中你并不需要一个单独的解释器,你需要在游戏来运行解释器,下面,让我们来看看,如何在你的代码中运行解释器:

//这是lua所需的三个头文件
//当然,你需要链接到正确的lib
#include "lua.h"
#include "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 = "print('hello, world!')";

lua_dostring(buf);

lua_close(L);

return 0;
}

程序输出:hello, world!

有时你需要执行一段字符串,有时你可能需要执行一个文件,当你需要执行一个文件时,你可以这么做:
lua_dofile(L, "test.lua");

看,非常简单吧。







在你的游戏中应用Lua(1):Getting Value

在上一篇文章我们能够在我们的游戏代码中执行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(1):调用函数

假设你在脚本中定义了一个函数:

function main(number)
number = 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栈中取得的参
          数传递给调用函数。

你可能感兴趣的:(游戏,api,脚本,测试,lua,buffer)