为什么使用Lua?
C++和脚本结合使用是非常好的实践,这种用法提供了非常大的灵活度和自由空间。脚本文件能够作配置文件和编写复杂的函数。更重要的一点是,修改脚本文件后无需重新编译,它能够帮助你集中精力和避免精力分散。
你甚至可以设计这样的一个系统,在不修改源码的情况下,其他人根本不知道怎么去添加一个新的脚本或者修改现有对象的功能。如果你想知道更多Lua为什么这么棒的原因,可以去阅读这篇文章:《The Beginning of This Article》
在上一篇文章中,我写了要怎么去绑定库,但这个库的功能是非常基础的,它并没有其他库所具有的能力。编写自己的(C++到Lua)绑定真的非常难,因为这涉及到了非常多的模版知识和元编程知识。
我测试过许多(C++到Lua的绑定)库,发现在LuaBridge是最好用的。它基于MIT许可,没有其他的任何依赖,同时也不要求C++11。你不需要去构建它,你只需要将LuaBridge文件夹拖到你的工程文件夹中,将一个头文件包含进来就可以了。
最小工程设置
从仓库中下载LuaBridge:仓库地址
同时也要去下载Lua
添加Lua的include目录和LuaBridge到你的工程的include目录,同时将lua52.lib链接到你的项目中。
新建一个script.lua文件,写下如下代码:
testString = "LuaBridge works!"
number = 42
添加main.cpp到你的项目中:
// main.cpp
#include <LuaBridge.h>
#include <iostream>
extern "C" {
# include "lua.h"
# include "lauxlib.h"
# include "lualib.h"
}
using namespace luabridge;
int main() {
lua_State* L = luaL_newstate();
luaL_dofile(L, "script.lua");
luaL_openlibs(L);
lua_pcall(L, 0, 0, 0);
LuaRef s = getGlobal(L, "testString");
LuaRef n = getGlobal(L, "number");
std::string luaString = s.cast<std::string>();
int answer = n.cast<int>();
std::cout << luaString << std::endl;
std::cout << "And here's our number:" << answer << std::endl;
}
注:如果你的程序编译失败并出现这样的错误:“error C2065: ‘lua_State’ : undeclared identifier” in luahelpers.h,你需要做如下的修改:
- 添加下面的代码段到LuaHelpers.h的开始部分
extern "C" {
# include "lua.h"
# include "lauxlib.h"
# include "lualib.h"
}
2.将Stack.h文件中第460行的代码从
lua_pushstring (L, str.c_str(), str.size());
改为
lua_pushlstring (L, str.c_str(), str.size());
好了,再将main.cpp修改下
#include <LuaBridge.h>
#include <iostream>
extern "C" {
# include "lua.h"
# include "lauxlib.h"
# include "lualib.h"
}
我们将所有重要的头文件都包含进来,没有其他例外的情况。
using namespace luabridge;
LuaBridge有其自己的命名空间(当你使用luabridge时,才将命名空间引进来)
lua_State* L = luaL_newstate();
这行代码新建了一个Lua虚拟机
luaL_dofile(L, "script.lua");
这行代码打开了我们的脚本。
你不需要每次打开一个lua脚本的时候都去打开一个新的虚拟机(lua_State),这可以仅仅使用一个。
然后我们加载lua库并调用脚本
luaL_openlibs(L);
lua_pcall(L, 0, 0, 0);
然后创建一个LuaRef对象,LuaRef可以持有Lua的一切类型变量,包括:
int,float,bool,string,table等等。
LuaRef s = getGlobal(L, "testString");
LuaRef n = getGlobal(L, "number");
然后我们可以将LuaRef转换为C++中的字符串:
std::string luaString = s.cast<std::string>();
int answer = n.cast<int>();
错误检查
有些时候有出错,我们应该学会优雅地处理这些错误。让我们开始检查一些最重要的错误
找不到脚本
if (luaL_loadfile(L, filename.c_str()) ||
lua_pcall(L, 0, 0, 0)) {
... // script is not loaded
}
找不到变量
可以检查变量是否为空
if (s.isNil()) {
std::cout << "Variable not found!" << std::endl;
}
变量不是我们想要的类型
如果你想确保变量是string,这可以这样做
f(s.isString()) {
luaString = s.cast<std::string>();
}
表
表不仅仅是数组:它令人惊讶地可以保存一切的Lua类型,甚至表。在script.lua中写下如下代码
window = {
title = "Window v.0.1",
width = 400,
height = 500
}
在C++中添加如下代码
LuaRef t = getGlobal(L, "window");
LuaRef title = t["title"];
LuaRef w = t["width"];
LuaRef h = t["height"];
std::string titleString = title.cast<std::string>();
int width = w.cast<int>();
int height = h.cast<int>();
std::cout << titleString << std::endl;
std::cout << "width = " << width << std::endl;
std::cout << "height = " << height << std::endl;
你会得到下面的结果
Window v.0.1
width = 400
height = 500
如果你所见,获得表元素的方法是使用[]操作符,我们甚至可以写得更短:
int width = t["width"].cast<int>();
也可以这样更改表的内容
t["width"] = 300
这并没有更改脚本,但如果你再次获得这个值时,会得到一个不一样的结果(事实上就是我们用C++设置的结果):
int width = t["width"].cast<int>(); // 400
t["width"] = 300
width = t["width"].cast<int>(); // 300
假设你有如下的表:
window = {
title = "Window v.0.1",
size = {
w = 400,
h = 500
}
}
我们要怎么得到window.size.w的值?
像下面这样做:
LuaRef t = getGlobal(L, "window");
LuaRef size = t["size"];
LuaRef w = size["w"];
int width = w.cast<int>();
作者注:这是我发现在的最短的写法,如果有人发现在了更好的做法,给我发封邮件或者写个评论吧,我将会非常感激
你可以去我教程的第一部分找到我实现的代码,它能够更方便地获得表中的数据,因为你可以这样做:
int width = script->get<int>("window.size.w");
译者注:这是可能是作者自己实现的功能,并非LuaBridge的功能。
函数
在C++中实现一个简单的函数
void printMessage(const std::string& s) {
std::cout << s << std::endl;
}
在script.lua中写下如下代码
printMessage("You can call C++ functions from Lua!")
现在我们需要在C++中注册这个函数,像下面这样:
getGlobalNamespace(L).
addFunction("printMessage", printMessage);
注:你必须在调用luaL_dofile之间调用上面的代码
注:C++和Lua可以有不同的名字
你刚刚注册了printMessage函数,如果你希望将它注册在基本个命名空间中,可以这样做:
getGlobalNamespace(L).
beginNamespace("game")
.addFunction("printMessage", printMessage)
.endNamespace();
你需要在Lua中这样调用
game.printMessage("You can call C++ functions from Lua!")
Lua中的命名空间与C++的命名空间没有什么联系,使用它们仅仅出于管理和便利的目的。
现在在C++中调用Lua函数
-- script.lua
sumNumbers = function(a,b)
printMessage("You can still call C++ functions from Lua functions!")
return a + b
End
// main.cpp
LuaRef sumNumbers = getGlobal(L, "sumNumbers");
int result = sumNumbers(5, 4);
std::cout << "Result:" << result << std::endl;
你将看到下面的输出:
You can still call C++ functions from Lua functions!
Result:9
好,是不是很酷?你并不需要告诉LuaBridge函数的参数和返回类型。
但还有一些事情是需要考虑的
一个函数不能有超过8个参数
当传递超过8个参数时,LuaBridge会将它无声地忽略。一但出错了,LuaBridge会抛出LuaException异常,要确保捕获并处理它。
下面是完整的函数例子代码:
-- script.lua
printMessage("You can call C++ functions from Lua!")
sumNumbers = function(a,b)
printMessage("You can still call C++ functions from Lua functions!")
return a + b
End
// main.cpp
#include <LuaBridge.h>
#include <iostream>
extern "C" {
# include "lua.h"
# include "lauxlib.h"
# include "lualib.h"
}
using namespace luabridge;
void printMessage(const std::string& s) {
std::cout << s << std::endl;
}
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
getGlobalNamespace(L).addFunction("printMessage", printMessage);
luaL_dofile(L, "script.lua");
lua_pcall(L, 0, 0, 0);
LuaRef sumNumbers = getGlobal(L, "sumNumbers");
int result = sumNumbers(5, 4);
std::cout << "Result:" << result << std::endl;
system("pause");
}
接下来做什么?
是的,接下来一章将会有非常有趣的内容:类、对象创建、对象生命周期……,反正很多内容就是了。