Lua, С++ и подклассы бедных
Я возглавляю dev для Bitfighter, и мы работаем с сочетанием Lua и С++, используя Lunar (вариант Luna, доступно здесь), чтобы связать их вместе.
Я знаю, что эта среда не имеет хорошей поддержки ориентации объектов и наследования, но я хотел бы найти способ хотя бы частично обойти эти ограничения.
Вот что у меня есть:
Структура класса С++
GameItem
|---- Rock
|---- Stone
|---- RockyStone
Robot
Robot реализует метод под названием getFiringSolution (элемент GameItem), который смотрит на положение и скорость item и возвращает угол, в который роботу нужно будет стрелять hit item.
-- This is in Lua
angle = robot:getFiringSolution(rock)
if(angle != nil) then
robot:fire(angle)
end
Итак, моя проблема в том, что я хочу передать камни, камни или rockyStones в метод getFiringSolution, и я не уверен как это сделать.
Это работает только для Rocks:
// C++ code
S32 Robot::getFiringSolution(lua_State *L)
{
Rock *target = Lunar<Rock>::check(L, 1);
return returnFloat(L, getFireAngle(target)); // returnFloat() is my func
}
В идеале, я хочу сделать что-то вроде этого:
// This is C++, doesn't work
S32 Robot::getFiringSolution(lua_State *L)
{
GameItem *target = Lunar<GameItem>::check(L, 1);
return returnFloat(L, getFireAngle(target));
}
Это потенциальное решение не работает, потому что функция проверки луны хочет, чтобы объект в стеку имел имя класса, которое соответствует определению, определенному для GameItem. (Для каждого типа объекта, который вы регистрируете в Lunar, вы указываете имя в виде строки, которую использует Lunar для обеспечения того, чтобы объекты имели правильный тип.)
Я бы согласился на что-то вроде этого, где мне нужно проверить все возможные подклассы:
// Also C++, also doesn't work
S32 Robot::getFiringSolution(lua_State *L)
{
GameItem *target = Lunar<Rock>::check(L, 1);
if(!target)
target = Lunar<Stone>::check(L, 1);
if(!target)
target = Lunar<RockyStone>::check(L, 1);
return returnFloat(L, getFireAngle(target));
}
Проблема с этим решением заключается в том, что функция проверки генерирует ошибку, если элемент в стеке не соответствует правильному типу и, я считаю, удаляет объект интереса из стека, поэтому у меня есть только одна попытка захвата он.
Я думаю, мне нужно получить указатель на объект Rock/Stone/RockyStone из стека, выяснить, какой тип он есть, а затем применить его к правильной вещи, прежде чем работать с ним.
Ключевым битом Lunar, который выполняет проверку типа, является следующее:
// from Lunar.h
// get userdata from Lua stack and return pointer to T object
static T *check(lua_State *L, int narg) {
userdataType *ud =
static_cast<userdataType*>(luaL_checkudata(L, narg, T::className));
if(!ud) luaL_typerror(L, narg, T::className);
return ud->pT; // pointer to T object
}
Если я назову это так:
GameItem *target = Lunar<Rock>::check(L, 1);
то luaL_checkudata() проверяет, является ли элемент в стеке камнем. Если это так, все персик, и он возвращает указатель на мой объект Rock, который возвращается к методу getFiringSolution(). Если в стеке есть элемент, отличный от Rock, он возвращает null и вызывается luaL_typerror(), который отправляет приложение в lala land (где обработка ошибок выводит диагностику и завершает работу робота с крайним предрассудком).
Любые идеи о том, как продвигаться вперед с этим?
Большое спасибо!
Лучшее решение, которое я придумал... уродливо, но работает
Основываясь на приведенных ниже предложениях, я придумал следующее:
template <class T>
T *checkItem(lua_State *L)
{
luaL_getmetatable(L, T::className);
if(lua_rawequal(L, -1, -2)) // Lua object on stack is of class <T>
{
lua_pop(L, 2); // Remove both metatables
return Lunar<T>::check(L, 1); // Return our object
}
else // Object on stack is something else
{
lua_pop(L, 1); // Remove <T> metatable, leave the other in place
// for further comparison
return NULL;
}
}
Затем, позже...
S32 Robot::getFiringSolution(lua_State *L)
{
GameItem *target;
lua_getmetatable(L, 1); // Get metatable for first item on the stack
target = checkItem<Rock>(L);
if(!target)
target = checkItem<Stone>(L);
if(!target)
target = checkItem<RockyStone>(L);
if(!target) // Ultimately failed to figure out what this object is.
{
lua_pop(L, 1); // Clean up
luaL_typerror(L, 1, "GameItem"); // Raise an error
return returnNil(L); // Return nil, but I don't think this
// statement will ever get run
}
return returnFloat(L, getFireAngle(target));
}
Есть, возможно, дальнейшие оптимизации, которые я могу сделать с этим... Мне бы очень хотелось разобраться, как свернуть это в цикл, потому что на самом деле у меня будет более чем три класса, с которыми нужно иметь дело, и этот процесс немного громоздкий.
Незначительное улучшение вышеприведенного решения
С++:
GameItem *LuaObject::getItem(lua_State *L, S32 index, U32 type)
{
switch(type)
{
case RockType:
return Lunar<Rock>::check(L, index);
case StoneType:
return Lunar<Stone>::check(L, index);
case RockyStoneType:
return Lunar<RockyStone>::check(L, index);
default:
displayError();
}
}
Затем, позже...
S32 Robot::getFiringSolution(lua_State *L)
{
S32 type = getInteger(L, 1); // My fn to pop int from stack
GameItem *target = getItem(L, 2, type);
return returnFloat(L, getFireAngle(target)); // My fn to push float to stack
}
Вспомогательная функция Lua, включаемая в виде отдельного файла, чтобы избежать необходимости добавления этого пользователя вручную в свой код:
function getFiringSolution( item )
type = item:getClassID() -- Returns an integer id unique to each class
if( type == nil ) then
return nil
end
return bot:getFiringSolution( type, item )
end
Пользователь вызывает этот путь из Lua:
angle = getFiringSolution( item )
Ответы
Ответ 1
Вы должны сообщить нам, что именно не работает в вашем коде. Я полагаю, что это Lunar<Rock>::check(L, 1)
не подходит для всех не-Rocks. Правильно ли я?
Также было бы хорошо, если бы вы указали, какую версию Lunar вы используете (ссылка на нее была бы большой).
Если это этот, то тип класса сохраняется в объекте Lua metatable (можно сказать, что этот метатебель является типом),
Похоже, что самый простой способ проверить, является ли объект Rock без исправления Lunar, - это вызвать luaL_getmetatable(L, Rock::className)
, чтобы получить класс metatable и сравнить его с lua_getmetatable (L, 1) вашего первого аргумента (примечание lua L в первом имени функции). Это немного хакерское, но должно работать.
Если вы воспользуетесь патчей Lunar, одним из возможных способов является добавление некоторого поля __lunarClassName
в metatable и сохранение T::name
там. Предоставьте lunar_typename()
функцию С++ (вне класса Lunar template, так как нам там не нужен T
), а затем возвращаем из нее значение этого поля аргумента __lunarClassName
. (Не забудьте проверить, имеет ли объект metatable, и что метатабельное имеет такое поле.) Вы можете проверить тип объекта Lua, вызывая lunar_typename()
тогда.
Немного советов из личного опыта: чем больше бизнес-логики вы нажимаете на Lua, тем лучше. Если вам не нужны жесткие ограничения производительности, вероятно, вам стоит подумать о том, чтобы переместить всю эту иерархию в Lua - ваша жизнь стала бы намного проще.
Если я могу помочь вам, скажите об этом.
Обновление: Решение, в котором вы обновили сообщение, выглядит правильно.
Чтобы выполнить отправку на основе метаданных в C, вы можете использовать, например, карту интегрального значения lua_topointer()
значения luaL_getmetatable()
для типа к объекту/указателю функции, который знает, как справиться с этим типом.
Но, опять же, я предлагаю перенести эту часть на Lua. Например: экспортировать типы специфических функций getFiringSolutionForRock()
, getFiringSolutionForStone()
и getFiringSolutionForRockyStone()
из С++ в Lua. В Lua сохраните таблицу методов по метатебельному:
dispatch =
{
[Rock] = Robot.getFiringSolutionForRock;
[Stone] = Robot.getFiringSolutionForStone;
[RockyStone] = Robot.getFiringSolutionForRockyStone;
}
Если я прав, следующая строка должна вызывать правильный специализированный метод объекта робота.
dispatch[getmetatable(rock)](robot, rock)
Ответ 2
Я думаю, вы пытаетесь отправить метод в неправильном месте. (Эта проблема является симптомом сложности со всеми этими "автоматизированными" способами взаимодействия Lua с C или С++: с каждым из них происходит некоторая магия за кулисами, и не всегда очевидно, как заставить ее работать. Я не понимаю, почему больше людей не просто используют Lua C API.)
Я просмотрел веб-страницы Lunar, и мне кажется, что вам нужно создать таблицу methods
по типу T
, а затем вызвать метод Luna<T>::Register
. Там простой пример в Интернете. Если я правильно читаю код, ни один из кода клей в вашем вопросе на самом деле не рекомендуется делать с Лунным. (Я также предполагаю, что вы можете реализовать эти методы полностью как вызовы С++.)
Это все довольно хитроумно, потому что документация на Лунной тонкая.
Разумной альтернативой было бы сделать всю работу самостоятельно и просто связать каждый тип С++ с таблицей Lua, содержащей ее методы. Тогда у вас есть метаметод Lua __index
, который проконсультируется с этой таблицей, а Боб - ваш дядя. Лунный делает что-то близко к ним, но он достаточно одет в С++-шаблоны, которые другие goo, что я не уверен, как заставить его работать.
Материал шаблона очень умный. Возможно, вы захотите либо потратить время, чтобы глубоко понять, как это работает, либо пересмотреть, если и как вы хотите его использовать.
Сводка: для каждого класса создайте таблицу явных методов и зарегистрируйте каждый класс с помощью метода Lunar Register
. Или сверните свой собственный.
Ответ 3
Я предлагаю вам определить объектно-ориентированную систему в чистом lua, а затем написать пользовательскую привязку к С++ для этого аспекта API.
Lua хорошо подходит для прототипов OO-реализаций, где таблицы используются для эмуляции классов, в которых одна запись имеет функцию new, которая при вызове возвращает соответствующую таблицу того же типа.
Из С++, однако, создайте LuaClass, который имеет метод .invoke, принимающий строку C (т.е. массив const char с нулевым завершением), чтобы указать имя функции-члена, которую вы хотите вызвать, и в зависимости от о том, как вы хотите обрабатывать переменные аргументы, имеете несколько шаблонных версий этого метода .invoke для нулевого, одного, двух,... N аргументов как необходимых или определить метод передачи в него переменного количества аргументов, и есть многие способы сделать это.
Для Lua я предлагаю сделать два метода .invoke, один из которых ожидает std::vector, а другой, который ожидает std:: map, но я оставлю это до вас.:)
В моем последнем проекте Lua/С++ я использовал только массивы с C-строками с нулевым завершением, требуя, чтобы lua преобразовал строку в соответствующее значение.
Enjoy.
Ответ 4
Я столкнулся с такими же потребностями, и вот что я придумал.
(Мне пришлось сделать некоторые незначительные изменения в лунном заголовке)
Во-первых, я добавил глобальный "интерфейс" для всех классов, которые будут содержать методы Lua.
Я понимаю, что это может показаться менее гибким, чем "оригинальный" способ, но, на мой взгляд, это яснее, и мне нужно, чтобы он выполнял динамические трансляции.
class LuaInterface
{
public:
virtual const char* getClassName() const=0;
};
Да, он содержит только один чистый виртуальный метод, который, очевидно, вернет статический атрибут "className" в производных классах. Таким образом, вы можете иметь полиморфизм, сохраняя этот статический член имени необходимым для шаблонных лунных классов.
Чтобы облегчить мою жизнь, я также добавил некоторые определения:
#define LuaClass(T) private: friend class Lunar<T>; static const char className[]; static Lunar<T>::RegType methods[]; public: const char* getClassName() const { return className; }
Поэтому вам просто нужно объявить класс следующим образом:
class MyLuaClass: public LuaInterface
{
LuaClass(MyLuaClass)
public:
MyLuaMethod(lua_State* L);
};
Ничего особенного здесь.
Мне также нужен "синглтон" (ох, я знаю: на самом деле это не должно быть одноэлементным, просто делайте все, что захотите)
class LuaAdapter
{
//SINGLETON part : irrelevant
public:
const lua_State* getState() const { return _state; }
lua_State* getState() { return _state; }
template <class T>
void registerClass(const std::string &name)
{
Lunar<T>::Register(_state);
_registeredClasses.push_back(name);
}
void registerFunction(const std::string &name, lua_CFunction f)
{
lua_register(_state, name.c_str(), f);
_registeredFunctions.push_back(name);
}
bool loadScriptFromFile(const std::string &script);
bool loadScript(const std::string &script);
const StringList& getRegisteredClasses() const { return _registeredClasses; }
const StringList& getRegisteredFunctions() const { return _registeredFunctions; }
LuaInterface* getStackObject() const;
private:
lua_State* _state;
StringList _registeredClasses;
StringList _registeredFunctions;
};
Теперь просто взгляните на метод registerClass: мы сохраняем его имя здесь в StringList (только список строк)
Теперь идея состоит в том, чтобы реализовать прокси для регистрации наших классов:
template<class _Type>
class RegisterLuaClassProxy
{
public:
RegisterLuaClassProxy(const std::string &name)
{
LuaAdapter::instance()->registerClass<_Type>(name);
}
~RegisterLuaClassProxy()
{
}
};
Нам нужно создать один экземпляр каждого прокси для каждого класса LuaInterface.
т.е.: в MyClass.cpp, после стандартного объявления метода "Лунный":
RegisterLuaClass(MyClass)
С, опять же, пара определяет:
#define RegisterLuaClassWithName(T, name) const char T::className[] = name; RegisterLuaClassProxy<T> T ## _Proxy(name);
#define RegisterLuaClass(T) RegisterLuaClassWithName(T, #T)
Сделайте то же самое с методами "функции" /прокси.
Теперь некоторые небольшие изменения в заголовке Lunar:
удалите структуру класса userdataType из класса и определите одну структуру вне класса:
typedef struct { LuaInterface *pT; } userdataType;
(обратите внимание, что вам также нужно добавить некоторый static_cast внутри класса Lunar)
Хорошо, хорошо. Теперь у нас есть все структуры, необходимые для выполнения нашей операции, я определил ее в методе getStackObject() моего LuaAdapter на основе вашего кода.
LuaInterface* LuaAdapter::getStackObject() const
{
lua_getmetatable(_state, 1);
for(StringList::const_iterator it = _registeredClasses.begin(); it != _registeredClasses.end(); ++it)
{
// CHECK ITEM
luaL_getmetatable(_state, it->c_str());
if(lua_rawequal(_state, -1, -2)) // Lua object on stack is of class <T>
{
lua_pop(_state, 2); // Remove both metatables
userdataType *ud = static_cast<userdataType*>(luaL_checkudata(_state, 1, it->c_str()));
if(!ud) luaL_typerror(_state, 1, it->c_str());
return ud->pT;
}
else // Object on stack is something else
{
// Remove <T> metatable, leave the other in place for further comparison
lua_pop(_state, 1);
}
}
return NULL;
}
Вот трюк: поскольку возвращаемый указатель указывает на абстрактный класс, вы можете безопасно использовать dynamic_cast < > с ним. И добавьте некоторые "промежуточные" абстрактные классы с хорошими виртуальными методами, например:
int fire(lua_State *L)
{
GameItem *item = dynamic_cast<GameItem*>(LuaAdapter::instance()->getStackObject());
if( item!= NULL)
{
item->fire();
}
return 0;
}
... Надеюсь, это поможет. Не стесняйтесь корректировать меня/добавлять материал/отзывы.
Приветствия:)