Pull to refresh

Использование Lua и C++ для обработки и хранения данных

C++Game developmentLua
Sandbox
Код статьи можно посмотреть здесь.
Чем так хорош Lua?

Когда-то я разрабатывал свою игру и задался вопросом: а какой формат данных лучше использовать для конфигурационных файлов?
Ведь удобно, когда создаёшь какой-либо объект, задавать различные начальные параметры не в самом коде, а в отдельных файлах. Это позволяет изменять некоторые параметры объектов без рекомпиляции, да и вообще даёт возможность менять их людям далёким от программирования.
Разработчики используют разные форматы: одни используют JSON, другие — XML, либо другие форматы данных. Ну а некоторые вообще хранят данные в .txt файлах или пишут свои парсеры. После рассмотрения различных форматов я остановился на Lua.

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

Вот, что выделяет Lua на фоне других форматов:
  • Lua легко использовать без дополнительных зависимостей (кроме одной библиотеки Lua и трёх .h файлов).
  • В Lua файлах данные можно инициализировать с помощью математических выражений или функций, написанных на Lua. Например:
    some_variable = math.sqrt(2) * 2
    some_variable2 = 64 * 16 - 32
    

  • Lua — очень быстрый язык, который к тому же не занимает много памяти.
  • У Lua лицензия MIT, которая позволяет использовать этот язык как в бесплатных, так и в коммерческих проектах, причём без всякой возни с бумагами. Как написано на сайте: «просто скачайте и пользуйтесь».
  • Lua комплируется практически везде, т.к. он написан на чистом C без использования дополнительных библиотек.
  • Данные можно хранить и сортировать в приятном глазу виде. Их легко читать и модифицировать в любом текстовом редакторе.

Начнём с простого примера, а затем я перейду к реализации класса.
Пример

Допустим, есть файл Player.lua
player = {
    pos = {
         X = 20,
         Y = 30,
    },
    filename = "res/images/player.png",
    HP = 20,
--  а ещё можно комментарии добавлять
}

С простым классом данные можно будет получать так:
LuaScript script("player.lua");
std::string filename = script.get<std::string>("player.filename");
int posX = script.get<std::string>("player.pos.X");


Внимание, чтобы код был понятен, рекомендуется прочесть информацию о том, как работает стек Lua и посмотреть на простейшие примеры.
Почитать можно здесь.

Начнём с создания класса:
#ifndef LUASCRIPT_H
#define LUASCRIPT_H
 
#include <string>
#include <vector>
#include <iostream>
 
// Lua написан на C, поэтому нужно сообщить компилятору, чтобы он воспринимал хэдеры как код на C
extern "C" {
# include "lua.h"
# include "lauxlib.h"
# include "lualib.h"
}
 
class LuaScript {
public:
    LuaScript(const std::string& filename);
    ~LuaScript();
    void printError(const std::string& variableName, const std::string& reason);
 
    template<typename T>
    T get(const std::string& variableName) {
        // реализация функции последует позже в статье
    }
    bool lua_gettostack(const std::string& variableName) { // заносим переменную в вершину стека. Если успешно - возвращаем true
       // реализация позже
    }
    // Возращаем 0 по умолчанию
    template<typename T>
    T lua_get(const std::string& variableName) {
      return 0;
    }
    // Эта функция используется в случае, если не удалось получить значение переменной и нужно вернуть какое-то
    // нулевое стандартное значение
    template<typename T>
    T lua_getdefault(const std::string& variableName) {
      return 0;
    }
private:
    lua_State* L;
    int level; // см. реализацию lug_gettostack
};
 
#endif

Конструктор:
LuaScript::LuaScript(const std::string& filename) {
    L = luaL_newstate();
    if (luaL_loadfile(L, filename.c_str()) || lua_pcall(L, 0, 0, 0)) {
        std::cout<<"Error: script not loaded ("<<filename<<")"<<std::endl;
        L = 0;
    }
}
.
Создаём lua_State, в случае если файл не был найден, либо произошла какая-либо другая ошибка, выводим сообщение об этом.
Деструктор:
LuaScript::~LuaScript() {
    if(L) lua_close(L);
}


Метод printError создан для того, чтобы выводить сообщения об ошибках:
void LuaScript::printError(const std::string& variableName, const std::string& reason) {
    std::cout<<"Error: can't get ["<<variableName<<"]. "<<reason<<std::endl;
}

lua_getdefault используется для того, чтобы вернуть какое-либо нулевое значение, если произошла ошибка. И если для чисел можно вернуть ноль, то для строк, например, это не сработает, поэтому делаем специализацию шаблона (этот код будет в хэдере).
template<>
inline std::string LuaScript::lua_getdefault<std::string>() {
  return "null";
}

А теперь напишем шаблонную функцию get.
Для начала напишем функцию lua_gettostack, которая заносит переменную на вершину стека
Разберём алгоритм на примере. Пусть нужно получить переменную «player.pos.X» из файла Player.lua
Проходим циклом до первой точки, при этом добавляя прочитанные символы в переменную «var».
«player» — таблица, которая является глобальной, поэтому получаем её с помощью lua_getglobal.
«pos» и «X» — это уже данные, которые не являются глобальные, но их можно получить с помощью lua_getfield, т.к. сама таблица player находится в вершине стека.
Затем уже в шаблонной функции get выполняется специфичная для типа данных функция, очищается стек и возвращается искомое значение, а в случае ошибки — вызывается функция lua_getdefault.


bool lua_gettostack(const std::string& variableName) {
      level = 0;
      std::string var = "";
        for(unsigned int i = 0; i < variableName.size(); i++) {
          if(variableName.at(i) == '.') {
            if(level == 0) {
              lua_getglobal(L, var.c_str());
            } else {
              lua_getfield(L, -1, var.c_str());
            }
            
            if(lua_isnil(L, -1)) {
              printError(variableName, var + " is not defined");
              return false;
            } else {
              var = "";
              level++;
            }
          } else {
            var += variableName.at(i);
          }
        }
        if(level == 0) {
          lua_getglobal(L, var.c_str());
        } else {
          lua_getfield(L, -1, var.c_str());
        }
        if(lua_isnil(L, -1)) {
            printError(variableName, var + " is not defined");
            return false;
        }
        // всё ок, возвращаем true
        return true; 
}

Возвращаемся к методу get:
template <typename T>
T get(const std::string& variableName) {
      if(!L) {
        printError(variableName, "Script is not loaded");
        return lua_getdefault<T>();
      }
      
      T result;
      if(lua_gettostack(variableName)) { // всё ок, переменная на вершине стека
        result = lua_get<T>(variableName);
      } else {
        result = lua_getdefault<T>();
      }

      lua_pop(L, level + 1); // очищаем стек
      return result;
    }
}

Осталось лишь добавить специализиации шаблонов(пример для некоторых типов данных):
template <>
inline bool LuaScript::lua_get<bool>(const std::string& variableName) {
    return (bool)lua_toboolean(L, -1);
}
 
template <>
inline float LuaScript::lua_get<float>(const std::string& variableName) {
    if(!lua_isnumber(L, -1)) {
      printError(variableName, "Not a number");
    }
    return (float)lua_tonumber(L, -1);
}
 
template <>
inline int LuaScript::lua_get<int>(const std::string& variableName) {
    if(!lua_isnumber(L, -1)) {
      printError(variableName, "Not a number");
    }
    return (int)lua_tonumber(L, -1);
}
 
template <>
inline std::string LuaScript::lua_get<std::string>(const std::string& variableName) {
    std::string s = "null";
    if(lua_isstring(L, -1)) {
      s = std::string(lua_tostring(L, -1));
    } else {
      printError(variableName, "Not a string");
    }
    return s;
}


На этом всё. Напоминаю, весь код в статье есть здесь. Там же можно найти пример использования класса.

Что дальше?

У Lua ещё много возможностей, которые я опишу во второй части статьи в ближайшем будущем. Например, получение массива данных неопределённой длины, а также получение списка ключей таблицы (например для таблицы Player из примера он был бы таким:[«pos», «filename», «HP»])
А ещё из Lua можно вызывать C++ функции, так же как и из C++ можно вызывать функции Lua, о чём я напишу в третьей части.
Удачного скриптинга!
Tags:gamedevluac++обработка данныххранение данныхразработка игр
Hubs: C++ Game development Lua
Total votes 45: ↑44 and ↓1+43
Views38K

Popular right now

Top of the last 24 hours