Решение данного вопроса я тоже нашел в трудах вышеупомянутого eliasdaler, но конретно про данный нюанс он нигде не писал. Чтобы найти это решение, потребовалось поковыряться в исходниках игры, которую он пишет. Анализ фрагментов кода помог, и я за 15 минут решил проблему, которую до этого почти месяц безуспешно пытался решить. Самое главное, что про биндинг методов из С++ в скрипты Lua нигде подробно не написано. Везде ограничиваются общими словами. Ну да, теперь то мне тоже понятно, как это сделать, но тогда — месяц я бился над данным вопросом. Ну конечно не 7 дней в неделю по 12 часов, параллельно я занимался и другими делами, но данный вопрос периодически возникал и я его никак не мог решить, что меня безмерно расстраивало.
Итак, в начале практически полная перепечатка (а точнее копипаст) его статьи с Хабра, чтобы разобраться с основами. Там всё очень хорошо и подробно описано:
Данная статья — перевод моего туториала, который я изначально писал на английском. Однако этот перевод содержит дополнения и улучшения по сравнению с оригиналом.
Туториал не требует знания 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
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();
Код: Выделить всё
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
Код: Выделить всё
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
Но есть одно ограничение: у одной функции 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
Код: Выделить всё
#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, в котором я рассказал о том, как использую скрипты в своей игре, практические примеры всегда полезны.
Теперь снова моё:
Кстати, обещание он свое не выполнил, и про упомянутые несколько замечательных вещей так и не написал. Хотя обещанного три года ждут, а когда я искал, прошло всего два с половиной года, может потом он это и дописал. Но было уже поздно, решение было найдено и без его записей.
Кстати, есть такая замечательная вещь, как паттерны программирования. И там есть такой паттерн, как «прокси» - структурный шаблон проектирования, предоставляющий объект, который контролирует доступ к другому объекту, перехватывая все вызовы (выполняет функцию контейнера). Написано страшно и непонятно, но именно его мы и будем использовать при обращении из скриптов к классам в С++.
Будет создан отдельный класс, который и будет биндить(переносить) все нужные нам методы в скрипт Lua, причем все эти методы будут содержаться в этом же самом классе. Т.е по сути этот класс будет дублировать 100, 200 или более необходимых нам методов других классов. С одной стороны, это излишне — лишний расход памяти, ресурсов процессора и т. п. С другой стороны, если у нас изменится какой-то метод в основном классе, скажем, станет принимать не 3, а 2 параметра, то у нас не будет необходимости перелопачивать десятки килобайт наших скриптов в поисках обращения к этому методу, чтобы там поправить должным образом наш скрипт. Нам будет достаточно в нашем прокси-классе найти этот метод и поправить обращение к методу основного класса только там. Скрипты Lua будут по прежнему передавать три параметра, но в нашу программу на С++ пойдут только два из них.
Итак, для начала создаем наш прокси-класс. Я его назвал LuaAdapter — и это было основной большой ошибкой, т. к. название класса нам придется писать по многих местах и не по одному разу, и, согласитесь, что написать La, например, гораздо проще, чем LuaAdapter. Есть конечно различные помощники с автозаполнением в средствах разработки, но всё равно, чем короче будет название класса, тем проще будет в дальнейшем.
В LuaAdapter.h в разделе public, к примеру, прописываем следующие методы (в дальнейшем буду приводить куски моего рабочего кода):
Код: Выделить всё
int GetGamerX () const;
void SetGamerX ( int a);
int GetGamerY () const;
void SetGamerY (int a);
int GetGamerHP () const;
void SetGamerHP (int a);
Код: Выделить всё
int LuaAdapter::GetGamerX () const
{
return MyGamer->GetCoordX ();
}
int LuaAdapter::GetGamerY () const
{
return MyGamer->GetCoordY ();
}
void LuaAdapter::SetGamerX (int a)
{
MyGamer->SetCoordX (a);
return;
}
void LuaAdapter::SetGamerY (int a)
{
MyGamer->SetCoordY (a);
return;
}
Ну и теперь самое интересное — непосредственно биндинг нужных нам функций в скрипты Lua. Там опять есть два варианта — такие простые функции с одним аргументом и без него мы можем передать собственно как отдельные функции, а можем как свойства — properties. Во втором случае ими будет удобнее пользоваться в скриптах Lua, но если функции принимают несколько параметров, то передать их как свойства уже не получится. Я приведу оба способа для понимания.
Первый — как свойства. Создаем метод с указанным синтаксисом и прописываем всё как на примере. Метод этот тоже надо не забыть объявить в заголовочном файле.
Код: Выделить всё
void LuaAdapter::LuaDesc (lua_State *L)
{
getGlobalNamespace(L)
.beginClass <LuaAdapter> ("Game")
.addProperty ("GamerX", &LuaAdapter::GetGamerX, &LuaAdapter::SetGamerX)
.addProperty ("GamerY", &LuaAdapter::GetGamerY, &LuaAdapter::SetGamerY)
.addProperty ("GamerHP", &LuaAdapter::GetGamerHP, &LuaAdapter::SetGamerHP)
.endClass();
}
Код: Выделить всё
A = Game:GamerX
Game:GamerY = 56
Game:GamerHP = 20
Второй способ — передаем как функции, ничего не изменяя, соответственно и обращаться с ними будем в дальнейшем как с функциями:
Код: Выделить всё
void LuaAdapter::LuaDesc (lua_State *L)
{
getGlobalNamespace(L)
.beginClass <LuaAdapter> ("Game")
.addFunction ("GetGamerX", &LuaAdapter::GetGamerX)
.addFunction ("SetGamerX", &LuaAdapter::SetGamerX)
.addFunction ("GetGamerY", &LuaAdapter::GetGamerY)
.addFunction ("SetGamerY", &LuaAdapter::SetGamerY)
.addFunction ("GetGamerHP", &LuaAdapter::GetGamerHP)
.addFunction ("SetGamerHP", &LuaAdapter::SetGamerHP)
.endClass();
}
Код: Выделить всё
A = Game:GetGamerX()
Game:SetGamerY (56)
Game:SetGamerHP (20)
Итак, класс-адаптер мы создали, теперь же осталось решить вопрос с передачей из программы на С++ управления нашему скрипту на Lua. Для этого в методе, где надо передать управление, пишется нижеследующая конструкция. По выходу из скрипта Lua управление опять передается следующему оператору в программе.
Код: Выделить всё
using namespace luabridge;
lua_State* L = luaL_newstate();
luaL_openlibs(L);
LuaAdapter Luaad;
Luaad.LuaDesc(L);
luaL_dofile(L, ".\\Files\\lua\\MyScript.lua");
lua_pcall(L, 0, 0, 0);
LuaRef MyScript = getGlobal(L, "MyScript");
MyScript (Luaad);
Код: Выделить всё
MyScript = function (Game)
Game:SetGamerY(56)
NewCoord = Game:GetCoordX()
…...
return
Т.е. В данном скрипте мы в качестве одного из параметров получили наш класс-прокси и успешно обратились к нему, вызвав необходимые функции. Что в общем то и требовалось получить.
И еще одно дополнение. Этот самый luabridge уже не поддерживается и вообще несколько кривоват, при каждом вызове lua_State* L = luaL_newstate(); идет утечка памяти примерно в районе 10 килобайт. Соответственно, если при каждом обращении к скриптам создавать эту самую виртуальную машину Lua заново, то каждый раз из памяти будет тратиться около 10 кб памяти. А у меня, например, на каждый ход идет как минимум одно обращение, анализирующее ход игрока, а так же по обращению от каждого моба на уровне — т. е. каждый ход утекало около 200 кб памяти. Потому удобнее будет создание этой переменной, так же как и создание экземпляра нашего прокси-класса, сделать один раз в начале выполнения нашей программы, а потом просто получать указатели на них в нужный момент времени.