Выбор скриптового языка для игрового движка
Добрый день. продолжаем разрабатывать свой игровой движок 2D.
Что у меня есть сейчас:
Графика на OpenGL
Система ресурсов
Компонентная модель (аля юнити)
Скрипты для игровой логики на Питоне.
В этом списке мне меньше всего нравится Питон. Почему:
Сам синтаксис языка с этими его отступами. Да я могу на наем писать код,ю и даже использую в рутинных и рабочих задачах, но в пет проекте для хобби мне язык должен доставлять удовольствие, а не вот это все.
Язык не очень быстрый с точки зрения интерпретатора
Приходится таскать практически полную инсталляцию Питона вместе с приложением.
Следовательно я решил рассмотреть альтернативы для скриптового языка игрового движка.
Итак список языков вашему вниманию
LUA
C#
ChaiScript
AngelScript
Squirrel
Python
Как мы сравниваем:
Размер комьюнити. где спросит готовое решение
Отладка скриптов
Быстродействие
Порог вхождения
Быстродействие будем замерять написав экспортную процедуру в скрипте и вызвав ее 10 000 раз в с++ приложении (аля игровой цикл) в микросекундах (us)
Для сравнения напишем эталон на нативном с++ метод и замеряем его время
int add(int a, int b)
{
return a + b;
}
int main()
{
auto s = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
volatile int r = n_add(i, i * 2);
}
auto e = std::chrono::high_resolution_clock::now();
auto t = std::chrono::duration_cast<std::chrono::microseconds>(e - s);
std::cout << std::endl;
std::cout << "native: \t" << t.count() << " us" << std::endl;
}
результат: native: 22 us.
И пойдем по порядку.
LUA с ним я знаком даже первая версия движка была именно с LUA. но в нем есть один минус. нет ООП. Есть извращения с мета таблицами. но зачем оно нам, если есть куча других языков.
Его сразу исключил из сравнения из за синтаксиса, но претензий к языку нет. в нем есть все, что нужно.
Большое комьюнити
Полноценный отладчик скриптов
Высокое быстродействие 1413 us
Низкий порог вхождения.
Скрипт
function Add(a, b)
return a + b
end
// 1. Создаём Lua состояние
lua_State* L = luaL_newstate();
luaL_openlibs(L);
// 3. Загружаем скрипт
const char* script = R"(
function Add(a, b)
return a + b
end
)";
if (luaL_dostring(L, script) != LUA_OK)
{
printf("Error: %s\n", lua_tostring(L, -1));
lua_close(L);
return 1;
}
// Кэшируем функцию
lua_getglobal(L, "Add");
int addRef = luaL_ref(L, LUA_REGISTRYINDEX); // Сохраняем ссылку
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++)
{
lua_rawgeti(L, LUA_REGISTRYINDEX, addRef); // Функция из кэша
lua_pushinteger(L, i);
lua_pushinteger(L, i * 2);
lua_pcall(L, 2, 1, 0);
int result = (int)lua_tointeger(L, -1);
lua_pop(L, 1);
}
auto end = std::chrono::high_resolution_clock::now();
auto time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
luaL_unref(L, LUA_REGISTRYINDEX, addRef); // Освобождаем ссылку
std::cout << "LUA:" << time.count() << std::endl;
// 5. Закрываем Lua
lua_close(L);
Результат LUA: 1413 us
C# - отличный язык с точки зрения использования. но внедрение его в мое с++ приложение как скриптового - это оказалась очень сложной задачей. по сути это работа с загрузкой dll и получения в нем нужных методов полюс очень много разновидностей всяких версий net.
Большое комьюнити
У меня так и не получилось настроить отладку моих скриптов с Visual Studio Code. причем точки остановки в виде Debugger.Break(); но точки остановки установленные интерактивно не работали.
Неплохое быстродействие 8342 us
Низкий порог вхождения.
public class TestClass
{
public static int Add(int a, int b)
{
int result = a + b;
return result;
}
}
// Загружаем C# сборку
MonoAssembly* assembly = mono_domain_assembly_open(domain, "scripts/GameScript.dll");
if (!assembly)
{
printf("Failed to load GameScript.dll\n");
mono_jit_cleanup(domain);
return 1;
}
//Получаем класс TestClass
MonoImage* image = mono_assembly_get_image(assembly);
MonoClass* klass = mono_class_from_name(image, "GameScript", "TestClass");
if (!klass)
{
std::cout << "Class GameScript.TestClass not found" << std::endl;
mono_jit_cleanup(domain);
return 1;
}
//Получаем статический метод Add класса TestClass
MonoMethod* method = mono_class_get_method_from_name(klass, "Add", 2);
if (!method)
{
std::cout << "Method Add not found" << std::endl;
mono_jit_cleanup(domain);
return 1;
}
//Вызываем этот метод 10 000 раз и замеряем время
auto s = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++)
{
// Вызываем C# метод
int args[2] = { 10, 20 };
void* pargs[2] = { &args[0], &args[1] };
MonoObject* result = mono_runtime_invoke(method, NULL, pargs, NULL);
if (result)
{
int value = *(int*)mono_object_unbox(result);
}
}
auto e = std::chrono::high_resolution_clock::now();
auto t1 = std::chrono::duration_cast<std::chrono::microseconds>(e - s);
std::cout << "C#: \t" << t1.count() << " us" << std::endl;
Результат выполнения C#: 8342 us*
ChaiScript - неплохой язык разработанный на с++ для с++. Так называемый header-only - метод встраивания, то есть он очень маленький и никаких доп зависимостей не требует. но как выяснилось он очень медленный самый медленный из всего списка.
Итоги:
Малоизвестен
в связи с его очень низкой производительностью. отладку даже не стал рассматривать
Очень медленный 1123729 us
Низкий порог вхождения.
файл main.chai
def Add(a, b)
{
return a + b;
}
chaiscript::ChaiScript chai;
std::string filename = "main.chai";
std::string script = LoadScript(filename);
try
{
chai.eval(script);
auto add = chai.eval<std::function<int(int, int)>>("Add");
auto s = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
int r = add(i, i * 2);
}
auto e = std::chrono::high_resolution_clock::now();
auto t = std::chrono::duration_cast<std::chrono::microseconds>(e - s);
std::cout << "ChaiScript: \t" << t.count() << " us" << std::endl;
}
Результат: ChaiScript: 1123729 us
AngelScript - Еще один С++ - подобный язык строго типизированный есть.
Довольно таки распространен (по крайней мере в статьях в интернете) даже есть сборка Unreal Engine с AngelScript.
Полноценного отладчика не обнаружено. есть методы встроенные, нго чтобы тот же VSCode умел отлаживать скрипты - нужно дорабатывать.
Очень быстрый(лидер нашего списка по скорости) 1848 us
Средний порог вхождения.
Файл script.as
float add(float a, float b)
{
return a + b;
}
// Создаем asIScriptEngine структуру
asIScriptEngine* engine = asCreateScriptEngine();
if (engine == 0)
{
cout << "Failed to create script engine." << endl;
return -1;
}
// Тут всякие конфигурации скриптового движка. не имеет смысла его тут описывать
ConfigureEngine(engine);
// Компиляция скрипта
std::string script = LoadScript("script.as");
if (script == "")
{
cout << "Failed to load script file." << endl;
engine->Release();
return -1;
}
asIScriptModule* mod = engine->GetModule(0, asGM_ALWAYS_CREATE);
r = mod->AddScriptSection("script", script.c_str(), script.length());
if (r < 0)
{
cout << "AddScriptSection() failed" << endl;
engine->Release();
return -1;
}
r = mod->Build();
if (r < 0)
{
cout << "Build() failed" << endl;
engine->Release();
return -1;
}
// Создание контекста для выполнения скрипта
asIScriptContext* ctx = engine->CreateContext();
if (ctx == 0)
{
cout << "Failed to create the context." << endl;
engine->Release();
return -1;
}
// Найти функцию add и запоминить указатель на нееasIScriptFunction* func = engine->GetModule(0)->GetFunctionByDecl("float add(float, float)");
if (func == 0)
{
cout << "The function 'float add(float, float)' was not found." << endl;
ctx->Release();
engine->Release();
return -1;
}
auto s = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++)
{
//Вызов функции с аргументами
r = ctx->Prepare(func);
if (r < 0)
{
cout << "Failed to prepare the context." << endl;
ctx->Release();
engine->Release();
return -1;
}
// Now we need to pass the parameters to the script function.
ctx->SetArgFloat(0, 10);
ctx->SetArgFloat(1, 20);
// Set the timeout before executing the function. Give the function 1 sec
// to return before we'll abort it.
timeOut = timeGetTime() + 1000;
r = ctx->Execute();
//Обработку ошибок не стал писать, чтобы экономить место
if (r == asEXECUTION_FINISHED)
{
// Возвращаем значени функции
float returnValue = ctx->GetReturnFloat();
}
}
auto e = std::chrono::high_resolution_clock::now();
auto t = std::chrono::duration_cast<std::chrono::microseconds>(e - s);
std::cout << "AngelScript: \t" << t.count() << " us" << std::endl;
Результат: AngelScript: 1848 us
Squirrel - Скриптовой язык высокого уровня принципом работы вдохновляемый LUA взаимодействие через стек, но синтаксисом похожим на с++. файлы скрипта имеют расширение nut. Белка грызет орехи, как это мило)
Не очень большое комьюнити, но есть неплохая справка.
Есть отладчик для VSCode с небольшим плагином и шаманством удалось научиться отлаживать скрипты
Быстрый по производительности 4241 us
Средний порог вхождения.
файл script.nut
function Add(a, b)
{
print("Exe Add Method\n");
return a + b;
}
// 1. Создаем VM
HSQUIRRELVM v = sq_open(1024);
if (!v)
{
std::cout<<"Failed to create VM"<<std::endl;
return;
}
// 2. Устанавливаем обработчик ошибок компиляции
sq_setcompilererrorhandler(v, CompileErrorHandler);
// 3. Регистрируем стандартные библиотеки
sq_pushroottable(v);
sqstd_register_iolib(v);
sq_pop(v, 1);
// 5. Загружаем скрипт
string filename = "script.nut";
string scriptContent = LoadScript(filename);
if (scriptContent.empty()) {
std::cout<<"ERROR: script.nut not found or empty"<<std::endl;
sq_close(v);
return 1;
}
// 6. Компилируем скрипт
SQRESULT compileRes = sq_compilebuffer(v, wideScript.c_str(), size - 1, L"script.nut", SQTrue);
if (compileRes != SQ_OK)
{
std::cout<<"Compilation failed!"<<std::endl;
sq_close(v);
return 1;
}
// 7. Выполняем скрипт
sq_pushroottable(v);
SQRESULT execRes = sq_call(v, 1, SQFalse, SQTrue);
if (execRes != SQ_OK)
{
std::cout<<"[C++] Execution ERROR: unknown error"<<std::endl;
sq_close(v);
return 1;
}
sq_pop(v, 1);
HSQOBJECT addFunc;
sq_pushroottable(v);
sq_pushstring(v, L"Add", -1);
DumpStack(v, "");
sq_get(v, -2);
sq_getstackobj(v, -1, &addFunc);
sq_pop(v, 2);
auto s = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++)
{
sq_pushobject(v, addFunc);
sq_pushroottable(v); // this
sq_pushinteger(v, i);
sq_pushinteger(v, i * 2);
if (SQ_SUCCEEDED(sq_call(v, 3, SQTrue, SQTrue)))
{
SQInteger result;
sq_getinteger(v, -1, &result);
sq_pop(v, 1);
}
sq_pop(v, 1);
}
auto e = std::chrono::high_resolution_clock::now();
auto t1 = std::chrono::duration_cast<std::chrono::microseconds>(e - s);
std::cout << "squrel: \t" << t1.count() << " us" << std::endl;
Результат squrel: 4241 us
Python - Питон выше я описал почему он мне не нравиться, но не могу не привести пример скорости. Так будет справедливо.
Итоги:
Очень большой комьюнити даже есть фреймворк python boost
Есть отладчик для VSCode.
Быстрый по производительности 4383 us
Cредний порог вхождения.
Итоги
native 22 us - как эталон производительности
LUA 1413 us - максимальная скорость из всех представленных скриптовых языков, но нет ООП
AngelScript 1848 us - очень быстрый но нет отладки
squrel: 4241 us - максимально быстрый с отладкой и ООП
Python 4383 us - тоже хорошая скорость, но это Питон
C# 8342 us - Оказался медленней питона, что меня разочаровало и не получилось разобраться с отладкой
ChaiScript 1123729 us - Оказался очень медленным, и вылетел из соревнования на первом кругу.
Получается пока останавливаемся на squrel. и ковыряем его. спасибо всем удачи.





































