本篇博客是给读者介绍引擎底层如何与Lua进行结合,方便开发者直接使用脚本编程,给读者介绍的是最基本的C++与Lua的交互,引擎的封装会在下篇博客中具体讲解。
为什么选择Lua,通常开发者会使用Json,XML,txt等等。相比Lua有哪些优点呢?
a、除了Lua库,在没有使用其他库可以使用。
b、可以在文件中使用不同的公式,例如:some_variable = math.sqrt(2)* 2
c、它非常轻巧,速度快
d、它是在MIT许可下换句话说代码是开源的,因此我们可以以任何想要的方式使用它
e、它是用C语言编写的,几乎可以编译任何C编译器
f、可以使用表对数据进行分类,易于编辑和阅读
既然这么多优点,我们就采用Lua作为脚本使用,Lua与引擎的结合还是非常重要的。我们下面先从基础的讲起,慢慢给读者深入。先看看Lua语言编写的脚本:
player = { pos = { X = 20, Y = 30, }, filename = "res/images/player.png", HP = 20,-- you can also have comments}
我们要使用Lua脚本中的内容,需要执行下面的语句:
LuaScript script("player.lua");
std::string filename = script.get("player.filename");
int posX = script.get("player.pos.X");
如何使用Lua与C++绑定,读者可以参考网址:
http://lua-users.org/wiki/BindingCodeToLua
接下来我们分析一下上面的Lua脚本,Player表是全局的,因此需要通过lua_getglobal方法获取它, 现在Player表将位于堆栈顶部,使用lua_getfield函数获取pos表,然后使用变量x,如下图所示:
对应的代码下载地址如下所示:
https://github.com/EliasD/unnamed_lua_binder
使用上面的代码时,不要忘记把Lua的库加进工程里面。我们可以使用Lua做很多事情,如下所示:
-- somefile.luasome_array = { 1, 2, 3, 4, 5, 6}
std::vector v = script.getIntVector("some_array")
如何清理Lua堆栈?我们可以使用lua_gettop函数返回数组中元素的数量,从而弄清楚我们必须弹出多少项。
void clean()
{
int n = lua_gettop(L);
lua_pop(L, n);
}
下面实现的接口:
std::vector LuaScript::getIntVector(const std::string& name)
{
std::vector v;
lua_getglobal(L, name.c_str());
if(lua_isnil(L, -1))
{
return std::vector();
}
lua_pushnil(L);
while(lua_next(L, -2))
{
v.push_back((int)lua_tonumber(L, -1));
lua_pop(L, 1);
}
clean();
return v;
}
它们是如何工作的?首先,我们获取全局表并检查是否找到它。 如果它是nil(尚未定义,或者…,nil),我们只返回一个空向量。
然后我们将nil值推到Lua堆栈的顶部, 这是因为lua_next的作用,它从堆栈中弹出键值,然后将键值对推送到堆栈。 如果数组中没有更多元素,我们清理堆栈并返回结果向量。
还可以创建一个函数来获取字符串或浮点数组, 这需要更改的是矢量类型和一些强制转换(请不要忘记将lua_tonumber更改为lua_tostring)
使用Lua脚本编程,因为它是脚本,对性能要求比较高的代码建议不要使用Lua脚本,直接使用C或者C++编程。
通过案例的方式给读者介绍,比如下面代码:
function sum(x, y)
return x + y
end
对应的C++代码如下所示:
int sum(int x, int y)
{
lua_State* L = luaL_newstate();
if (luaL_loadfile(L, "sum.lua") || lua_pcall(L, 0, 0, 0))
{
std::cout<<"Error: failed to load sum.lua"<
接下来给读者分析一下,首先,我们创建新的Lua状态并加载文件。注意:这只是一个示例,我们应该将状态与加载的文件保持在某个位置,以防止每次使用函数时重新加载,因为这样做效率不高。
然后我们在Lua堆栈的顶部得到名为sum的全局函数。 使用lua_pushnumber函数然后我们推送2个变量,现在我们的堆栈看起来像这样:
第一个是lua_State,第二个是你要调用的函数中的参数个数, 第三是你希望返回的功能。 第四是错误代码(应该在Lua参考手册中阅读)
在我们调用一个函数之后,它会从它的参数中弹出堆栈。 堆栈中剩下的唯一东西是值sum函数返回,所以现在我们可以用lua_tonumber获取它的值并弹出它。
说了这么多,现在给读者介绍如何使用它们?
假设我们在游戏中实现NPC, 当玩家靠近NPC时,NPC会做不同的事情。
我们经常会安排玩家与NPC的一些对话,比如说“让我帮助你”,而另一个NPC只是说“你好”并且什么都不做。我们的交互代码可能如下所示:
if(isPressed(ACTIVATION_BUTTON))
{
Character* character = find_nearby_character(player);
if(character)
{
character->interact(player);
}
}
如何实现这个交互方法?我们很容易想到使用枚举,如下所示:
enum CharacterType { Player, Talker, Healer };
CharacterType type;
函数如下所示:
void Character::interact(Character* secondCharacter)
{
switch(type)
{
case Character::Player:
break;
case Character::Talker:
say("Hello");
break;
case Character::Healer:
say("Let me help you");
heal(secondCharacter);
break;
}
}
通过代码我们可以看出,这么设计非常不利于扩展,我们还可以想到使用另一种解决方案是使交互虚拟功能和使用继承, 但是我们会去实现每种类型的NPC,这种方案也是不可取的。
或者有读者可以使用更好的策略模式,用C ++编码的,必须重新编译代码,也是不可取的。
最终的解决方案就想到了Lua的编写,在使用Lua之前,我们首先要创建一个Character类,如下所示:
class Character
{
public:
Character(const char* name, int hp);
void say(const char* text);
void heal(Character* character);
const char* getName() { return name; }
int getHealth() { return health; }
void setHealth(int hp) { health = hp; }
// will be implemented later
void interact(Character* character);
private:
const char* name;
int health;
};
Character::Character(const char* name, int hp)
{ this->name = name; health = hp;}
void Character::say(const char* text)
{ std::cout << name << ":" << text << std::endl;}
void Character::heal(Character* character)
{ character->setHealth(100);}
我们遇到了一个问题, 如果Lua现在没有关于这种类型,我们如何将Character *作为参数传递? 我们如何在Lua中注册非静态成员函数并调用它们?Lua包装器可以参考网址:http://lua-users.org/wiki/BindingCodeToLua
决定使用LuaWrapper,它没有额外的依赖关系而且不需要构建, 只需将一个头文件复制到项目中即可开始使用。
使用LuaWrapper,函数的编写如下所示:
int Character_getName(lua_State* L)
{
Character* character = luaW_check(L, 1); lua_pushstring(L, character->getName());
return 1;
}
int Character_getHealth(lua_State* L)
{
Character* character = luaW_check(L, 1); lua_pushnumber(L, character->getHealth());
return 1;
}
int Character_setHealth(lua_State* L)
{
Character* character = luaW_check(L, 1); int hp = luaL_checknumber(L, 2);
character->setHealth(hp);
return 0;
}
从现在开始,我们将使用checknumber而不是tonumber。 它基本相同,但如果出现问题,它会抛出错误信息。
LuaWrapper提供了相同的方法,可以用它来获取C ++对象,还可以创建对象并调用它们的方法,如下所示:
player = Character.new(“Hero”, 100)
player:getHealth()
使用luaW_check(L,1)可以获得玩家对象在C ++中使用它,余下的代码如下所示:
static luaL_Reg Character_table[] = { { NULL, NULL }};
static luaL_Reg Character_metatable[] = {
{ "getName", Character_getName },
{ "getHealth", Character_getHealth },
{ "setHealth", Character_setHealth },
{ NULL, NULL }
};
static int luaopen_Character(lua_State* L)
{
luaW_register(L, "Character", Character_table, Character_metatable, Character_new);
return 1;
}
Character_table用于静态函数, 我们在Character类中没有它们,所以这个结构是空的。
Character_metatable用于设置将在Lua中使用的函数名称。
luaopen_Character注册一个类, 第一个参数是lua_State *,第二个参数是如何在Lua脚本中命名我们的类。 其他参数是静态表,元表和构造函数。
我们的测试脚本如下所示:
player = Character.new("Hero", 100)
player:setHealth(80)
hp = player:getHealth()
name = player:getName()
print("Character name: "..name..". HP = "..hp)
最后代码下载地址如下所示:
链接:https://pan.baidu.com/s/1Rn3WwXYVLA-t79s0MFFarQ
提取码:mtgr
参考网址:https://eliasdaler.wordpress.com/2013/10/11/lua_cpp_binder/