在你的游戲中應用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棧中取得的參
? ? ? ? ? 數傳遞給調用函數。