Использовать Lua c С++ легче, чем вы думаете. Tutorial по LuaBridge

в 15:11, , рубрики: c++, game development, Gamedev, Lua, разработка игр, С++, скрипты

Данная статья — перевод моего туториала, который я изначально писал на английском. Однако этот перевод содержит дополнения и улучшения по сравнению с оригиналом.
Туториал не требует знания Lua, а вот C++ нужно знать на уровне чуть выше базового, но сложного кода здесь нет.

Когда-то я написал статью про использование Lua с C++ с помощью Lua C API. В то время, как написать простой враппер для Lua, поддерживающий простые переменные и функции, не составляет особого труда, написать враппер, который будет поддерживать более сложные вещи (функции, классы, исключения, пространства имён), уже затруднительно.
Врапперов для использования Lua и C++ написано довольно много. С многими из них можно ознакомиться здесь.
Я протестировал многие из них, и больше всего мне понравился LuaBridge. В LuaBridge есть многое: удобный интерфейс, exceptions, namespaces и ещё много всего.
Но начнём по порядку, зачем вообще использовать Lua c С++?

Зачем использовать Lua?

Конфигурационные файлы. Избавление от констант, магических чисел и некоторых define'ов

Данные вещи можно делать и с помощью простых текстовых файлов, но они не так удобны в обращении. Lua позволяет использовать таблицы, математические выражения, комментарии, условия, системные функции и пр. Для конфигурационных файлов это бывает очень полезно.
Например, можно хранить данные в таком виде:

window = {
    title = "Test project",
    width = 800,
    height = 600
}

Можно получать системные переменные:

homeDir = os.getenv("HOME")

Можно использовать математические выражения для задания параметров:

someVariable = 2 * math.pi
Скрипты, плагины, расширение функциональности программы

C++ может вызывать функции Lua, а Lua может вызывать функции C++. Это очень мощный функционал, позволяющий вынести часть кода в скрипты или позволить пользователям писать собственные функции, расширяющие функциональность программы. Я использую функции Lua для различных триггеров в игре, которую я разрабатываю. Это позволяет мне добавлять новые триггеры без рекомпиляции и создания новых функций и классов в C++. Очень удобно.

Немного о Lua. Lua — язык с лицензией MIT, которая позволяет использовать его как в некоммерческих, так и в коммерческих приложениях. Lua написан на C, поэтому Lua работает на большинстве ОС, что позволяет использовать Lua в кросс-платформенных приложениях без проблем.

Установка Lua и LuaBridge

Итак, приступим. Для начала скачайте Lua и LuaBridge
Добавьте include папку Lua и сам LuaBridge в Include Directories вашего проекта
Также добавьте lua52.lib в список библиотек для линковки.

Создайте файл script.lua со следующим содержанием:

-- 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;
}

Скомпилируйте и запустите программу. Вы должны увидеть следующее:

LuaBridge works!
And here's our number:42

Примечание: если программа не компилируется и компилятор жалуется на ошибку “error C2065: ‘lua_State’: undeclared identifier” в файле LuaHelpers.h, то вам нужно сделать следующее:
1) Добавьте эти строки в начало файла LuaHelpers.h

extern "C" {
# include "lua.h"
# include "lauxlib.h"
# include "lualib.h"
}

2) Измените 460ую строку Stack.h с этого:

lua_pushstring (L, str.c_str(), str.size());

На это:

lua_pushlstring (L, str.c_str(), str.size());

Готово!

А теперь подробнее о том, как работает код.

Включаем все необходимые хэдеры:

#include <LuaBridge.h>
#include <iostream>
extern "C" {
# include "lua.h"
# include "lauxlib.h"
# include "lualib.h"
}

Все функции и классы LuaBridge помещены в namespace luabridge, и чтобы не писать «luabridge» множество раз, я использую эту конструкцию (хотя её лучше помещать в те места, где используется сам LuaBridge)

using namespace luabridge;

Создаём lua_State

lua_State* L = luaL_newstate();

Открываем наш скрипт. Для каждого скрипта не нужно создавать новый lua_State, можно использовать один lua_State для множества скриптов. При этом нужно учитывать коллизию переменных в глобальном нэймспейсе. Если в script1.lua и script2.lua будут объявлены переменные с одинаковыми именами, то могут возникнуть проблемы

luaL_dofile(L, "script.lua");

Открываем основные библиотеки Lua(io, math, etc.) и вызываем основную часть скрипта (т.е. если в скрипте были прописаны действия в глобальном нэймспейсе, то они будут выполнены)

luaL_openlibs(L);
lua_pcall(L, 0, 0, 0);

Создаём объект 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>();

Проверка и исправление ошибок

Но некоторые вещи могут пойти не так, и стоит производить проверку и обработку ошибок. Рассмотрим наиболее важные и часто встречающиеся ошибки

Что, если скрипт Lua не найден?
if (luaL_loadfile(L, filename.c_str()) ||
    lua_pcall(L, 0, 0, 0)) {
    ... // скрипт не найден
}
Что, если переменная не найдена?

Переменная может быть не объявлена, либо её значение — nil. Это легко проверить с помощью функции isNil()

if (s.isNil()) {
    std::cout << "Variable not found!" << std::endl;
}

Переменная не того типа, который мы ожидаем получить

Например, ожидается, что переменная имет тип string, тогда можно сделать такую проверку перед тем как делать каст:

if(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

Это не меняет значение в скрипте, а лишь значение, которое содержится в ходе выполнения программы. Т.е. происходит следующее:

int width = t["width"].cast<int>(); // 400
t["width"] = 300
width = t["width"].cast<int>(); // 300

Чтобы сохранить значение, нужно воспользоваться сериализацией таблиц(table serialization), но данный туториал не об этом.

Пусть теперь таблица выглядит так:

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>();

Функции

Давайте напишем простую функции на C++

void printMessage(const std::string& s) {
    std::cout << s << std::endl;
}

И напишем вот это в скрипте на Lua:

printMessage("You can call C++ functions from Lua!")

Затем мы регистрируем функцию в C++

getGlobalNamespace(L).
   addFunction("printMessage", printMessage);

Примечание 1: это нужно делать до вызова «luaL_dofile», иначе Lua попытается вызвать необъявленную функцию
Примечание 2: Функции на C++ и Lua могут иметь разные имена

Данный код зарегистрировал функцию в глобальном namespace Lua. Чтобы зарегистрировать его, например, в namespace «game», нужно написать следующий код:

getGlobalNamespace(L).
    beginNamespace("game")
        .addFunction("printMessage", printMessage)
    .endNamespace();

Тогда функцию printMessage в скриптах нужно будет вызывать данным образом:

game.printMessage("You can call C++ functions from Lua!")

Пространства имён в Lua не имеют ничего общего с пространствами имён C++. Они скорее используются для логического объединения и удобства.

Теперь вызовем функцию Lua из C++

-- 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 сколько и каких аргументов у функции, и какие значения она возвращает.
Но есть одно ограничение: у одной функции Lua не может быть более 8 аргументов. Но это ограничение легко обойти, передав таблицу, как аргумент.

Если вы передаёте в функцию больше аргументов, чем требуется, LuaBridge молча проигнорирует их. Однако, если что-то пойдёт не так, то LuaBridge сгенерирует исключение LuaException. Не забудьте словить его! Поэтому рекомендуется окружать код блоками try/catch

Вот полный код примера с функциями:

-- 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");
}

Что? Есть ещё что-то?

Да. Есть ещё несколько замечательных вещей, о которых я напишу в последующих частях туториала: классы, создание объектов, срок жизни объектов… Много всего!
Также рекомендую прочитать этот dev log, в котором я рассказал о том, как использую скрипты в своей игре, практические примеры всегда полезны.

Автор: eliasdaler

Источник

* - обязательные к заполнению поля