Что такое скрипты и с чем их едят — Lua & C++

в 16:13, , рубрики: c plus plus, c++, game development, Lua, метки: , ,

Добрый день!
Решил написать этот топик на тему скриптов

Что такое скрипты и с чем их едят — Lua & C++

Что нужно знать?

  • С++ на приличном уровне (в уроке будут шаблоны — template)
  • Lua, очень легкий скриптовый язык. Советую этот урок.

Почему писать диалоги игры в .cpp файле было большой ошибкой

Если вы разрабатывали большие проекты (к примеру, масштабные игры), замечали, что с каждой новой сотней строк кода компиляция идет медленней?
В игре создается больше оружия, больше диалогов, больше меню, больше etc.
Одна из самых главных проблем, возникающих в связи с нововведениями — поддерживать бессчетное множество оружия и бейджиков довольно сложное занятие.
В ситуации, когда просьба друга/босса/напарника изменить диалог или добавить новый вид оружия занимает слишком много времени, приходится прибегать к каким-то мерам — например, записи всей этой фигни в отдельные текстовые файлы.
Почти каждый геймдевелопер когда-нибудь делал карту уровней или диалоги в отдельном текстовом файле и потом их считывал. Взять хотя бы простейший вариант — олимпиадные задачи по информатике с файлом ввода

Но есть способ, на голову выше — использование скриптов.

Решение проблемы

«Окей, для таких дел хватает обычного файла с описанием характеристиков игрока. Но что делать, если в бурно развивающемся проекте почти каждый день приходится немножко изменять логику главного игрока, и, следовательно, много раз компилировать проект?»
Хороший вопрос. В этом случае нам на помощь приходят скрипты, держащие именно логику игрока со всеми характеристиками либо какой-либо другой части игры.
Естественно, удобнее всего держать, логику игрока в виде кода какого-нибудь языка программирования.
Первая мысль — написать свой интерпретатор своего скриптового языка, выкидывается из мозга через несколько секунд. Логика игрока определенно не стоит таких жутких затрат.
К счастью, есть специальные библиотеки скриптовых языков для С++, которые принимают на вход текстовый файл и выполняют его.

Об одном таком скриптовом языке Lua пойдет речь.

Как это работает?

Прежде чем начать, важно понимать, как работает скриптовый язык. Дело в том, что в скриптовых языках есть очень мало функций, при наличии конструкций for, while, if, прочих.
В основном это функции вывода текста в консоль, математические функции и функции для работы с файлами.
Как же тогда можно управлять игроком через скрипты?

Мы в С++-программе делаем какие-либо функции, «регистрируем» их под каким-нибудь именем в скрипте и вызываем в скрипте. То есть если мы зарегистрировали функцию SetPos(x,y) для определения позиции игрока в С++-программе, то, встретив эту функцию в скрипте, «интерпретатор» из библиотеки скриптового языка вызывает эту функцию в С++-программе, естественно, с передачей всех методов.
Удивительно, да? :)

Я готов!

Когда вы поняли преимущества скриптовых языков программирования, самое время начать работать!
Скачайте из репозитория на гитхабе (низ топика) lib'у и includ'ы Lua, либо возмите их на официальном сайте.

Создаем консольный проект либо Win32 (это неважно) в Visual Studio (у меня стоит версия 2012)

Заходим в Проект->Свойства->Свойства конфигурации->Каталоги VC++ и в «каталоги включения» и «каталоги библиотек» добавьте папку Include и Lib из репозитория соответственно.

Теперь создаем файл main.cpp, пишем в нем:

int main()
{
	return 0;
}

Как вы догадались, у меня консольное приложение.

Теперь переходим к кодингу

Обещаю, что буду тщательно объяснять каждый момент

У нас за скрипты будет отвечать класс Script. Я буду объявлять и одновременно реализовывать функции в Script.h/.cpp
Создаем Script.cpp и пишем в нем

#include "Script.h"

Создаем Script.h и пишем в нем

#ifndef _SCRIPT_H_
#define _SCRIPT_H_

#endif

После 2 строчки и перед #endif мы определяем класс скриптов
Этот код пишется для предотвращения взаимного включения файлов. Допустим, что файл Game.h подключает Script.h, а Script.h подключает Game.h — непорядок! А с таким кодом включение выполняется только 1 раз

Теперь пишем внутри этого кода вот это

#pragma comment(lib,"lua.lib")
extern "C"
{
	#include <lua.h>
	#include <lualib.h>
	#include <lauxlib.h>
}

Первая строчка подключает сам lua.lib из архива.
Для чего нужен extern «C»? Дело в том, что lua написан на С и поэтому такой код необходим для подключения библиотек.

Дальше идет подключение хорошо известных многим файлов для работы с консолью

#include <stdio.h>
#include <iostream>
#include <sstream>
using namespace std;

Теперь приступим к определению класса

class Script
{

Самый главный объект библиотеки Lua для C++ — lua_State, он необходим для выполнения скриптов

private:
    lua_State *lua_state;

Дальше идут публичные функции

public:
	void Create();

Эта функция инициализирует lua_State

Create()

Его определение в Script.cpp

void Script::Create()
{
	lua_state = luaL_newstate();
	
	static const luaL_Reg lualibs[] = 
	{
		{"base", luaopen_base},
		{"io", luaopen_io},
		{NULL, NULL}
	};

	for(const luaL_Reg *lib = lualibs; lib->func != NULL; lib++)
	{
		luaL_requiref(lua_state, lib->name, lib->func, 1);
		lua_settop(lua_state, 0);
	}
}

Первой строчкой мы инициализируем наш lua_State.
Потом мы объявляем список «подключенных библиотек». Дело в том, что в «чистом» виде в луа есть только функция print(). Для математических и прочих функций требуется подключать специальные библиотеки и потом вызывать их как math.foo, base.foo, io.foo. Для подключения других библиотек добавьте в lualibs, например, {«math», luaopen_math}. Все названия библиотек начинаются с luaopen_..., в конце lialibs должен стоять {NULL,NULL}

	void Close();

Эта функция освобождает ресурсы Lua

Close()

Ее определение

void Script::Close()
{
	lua_close(lua_state);
}

Просто используем lua_close()

int DoFile(char* ScriptFileName);

А эта функция выполняет файл. На вход она принимает название файла, например, «C:\script.lua».
Почему она возвращает int? Просто некоторые скрипты могут содержать return, прерывая работу скрипта и возвращая какое-нибудь значение.

DoFile()

Ее определение

int Script::DoFile(char* ScriptFileName)
{
	luaL_dofile(lua_state,ScriptFileName);

	return lua_tointeger(lua_state, lua_gettop(lua_state));
}

Как вы видите, я выполняю скрипт и возвращаю int. Но возращать функция может не только int, но еще и bool и char*, просто я всегда возвращаю числа (lua_toboolean, lua_tostring)

Теперь мы сделаем функцию, регистрирующую константы (числа, строки, функции)

	template<class t>
	void RegisterConstant(T value, char* constantname);
RegisterConstant()

Мы действуем через шаблоны. Пример вызова функции:

RegisterConstant<int>(13,"goodvalue");

Ее определение

template
void Script::RegisterConstant<int>(int value, char* constantname)
{
	lua_pushinteger(lua_state, value);
	lua_setglobal(lua_state,constantname);
}

template
void Script::RegisterConstant<double>(double value, char* constantname)
{
	lua_pushnumber(lua_state, value);
	lua_setglobal(lua_state,constantname);
}

template
void Script::RegisterConstant<char>(char* value, char* constantname)
{
	lua_pushstring(lua_state, value);
	lua_setglobal(lua_state,constantname);
}

template
void Script::RegisterConstant<bool>(bool value, char* constantname)
{
	lua_pushboolean(lua_state, value);
	lua_setglobal(lua_state,constantname);
}

template
void Script::RegisterConstant<lua_cfunction>(lua_CFunction value, char* constantname)
{
	lua_pushcfunction(lua_state, value);
	lua_setglobal(lua_state,constantname);
}

Для каждого возможного значения class T мы определяем свои действия.
*Капитан* последнее определение — регистрация функции
Функции, годные для регистрации, выглядят так:

int Foo(lua_State*)
{
   // ...
   return n;
}

Где n — количество возвращаемых значений. Если n = 2, то в Луа можно сделать так:

a, b = Foo()

Читайте мануалы по Луа, если были удивлены тем, что одна функция возвращает несколько значений :)

Следующая функция создает таблицу для Луа. Если непонятно, что это значит, то тамошная таблица все равно что массив

	void Array();
Array()

Ее описание

void Script::Array()
{
    lua_createtable(lua_state, 2, 0);
}

Следующая функция регистрирует элемент в таблице.

	template<class t>
	void RegisterConstantArray(T value, int index);
RegisterConstantArray()

Ее описание

template
void Script::RegisterConstantArray<int>(int value, int index)
{
	lua_pushnumber(lua_state, index);
	lua_pushinteger(lua_state, value);
	lua_settable(lua_state, -3);
}

template
void Script::RegisterConstantArray<double>(double value, int index)
{
	lua_pushnumber(lua_state, index);
	lua_pushnumber(lua_state, value);
	lua_settable(lua_state, -3);
}

template
void Script::RegisterConstantArray<char>(char* value, int index)
{
	lua_pushnumber(lua_state, index);
	lua_pushstring(lua_state, value);
	lua_settable(lua_state, -3);
}

template
void Script::RegisterConstantArray<bool>(bool value, int index)
{
	lua_pushnumber(lua_state, index);
	lua_pushboolean(lua_state, value);
	lua_settable(lua_state, -3);
}

template
void Script::RegisterConstantArray<lua_cfunction>(lua_CFunction value, int index)
{
	lua_pushnumber(lua_state, index);
	lua_pushcfunction(lua_state, value);
	lua_settable(lua_state, -3);
}

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

Наконец, заполненную таблицу нужно зарегистрировать

	void RegisterArray(char* arrayname);
RegisterArray()

Ее описание

void Script::RegisterArray(char* arrayname)
{
	lua_setglobal(lua_state, arrayname);
}

Ничего особенного нет

Следующие функции предназначены в основном только для функций типа int foo(lua_State*), которые нужны для регистрации в Луа.

Первая из них — получает количество аргументов

	int GetArgumentCount();
Create()

Ее описание

int Script::GetArgumentCount()
{
	return lua_gettop(lua_state);
}

Эта функция нужна, например, для функции Write(), куда можно запихать сколь угодно аргументов, а можно и ни одного
Подобную функцию мы реализуем позже

Следующая функция получает аргумент, переданный функции в скрипте

	template<class t>
	T GetArgument(int index);
GetArgument()

Ее описание

template
int Script::GetArgument<int>(int index)
{
	return lua_tointeger(lua_state,index);
}

template
double Script::GetArgument<double>(int index)
{
	return lua_tonumber(lua_state,index);
}

template
char* Script::GetArgument<char>(int index)
{
	return (char*)lua_tostring(lua_state,index);
}

template
bool Script::GetArgument<bool>(int index)
{
	return lua_toboolean(lua_state,index);
}

Можно получить все типы, описывавшиеся ранее, кроме таблиц и функций
index — это номер аргумента. И первый аргумент начинается с 1.

Наконец, последняя функция, которая возвращает значение в скрипт

	template<class t>
	void Return(T value);
Return()

Ее описание

template
void Script::Return<int>(int value)
{
	lua_pushinteger(lua_state,value);
}

template
void Script::Return<double>(double value)
{
	lua_pushnumber(lua_state,value);
}

template
void Script::Return<char>(char* value)
{
	lua_pushstring(lua_state,value);
}

template
void Script::Return<bool>(bool value)
{
	lua_pushboolean(lua_state,value);
}

Боевой код

Пора что-нибудь сделать!
Изменяем main.cpp

#include "Script.h"

int main()
{
	return 0;
}

Компилируем. Теперь можно приступить к тестированию нашего класса

Помните, я обещал сделать функцию Write? :)
Видоизменяем main.cpp

#include "Script.h"

// Нужен для _getch()
#include <conio.h>

// Объект скрипта
Script script;

// Функция Write для текста
int Write(lua_State*)
{
	// Тут мы считываем количество аргументов и каждый аргумент выводим
	for(int i = 1; i (i);

	// После вывода ставим консоль на паузу
	_getch();

	return 0;
}

int main()
{
	script.Create();
	// Имя у луашной функции такое же, как у сишной
	script.RegisterConstant<lua_cfunction>(Write,"Write");
	script.DoFile("script.lua");
	script.Close();
}

А в папке с проектом создаем файл script.lua

Write(1,2,3,4);

image

Компилируем и запускаем проект.

image

Теперь изменяем script.lua

for i = 1, 4 do
	Write(i, "n", "Hier kommt die Sonne", "n")
end

Теперь программа будет выводить по 2 строки ("n" — создание новой строки), ждать нажатия Enter и снова выводить строки.

image

Экспериментируйте со скриптами!

Вот пример main.cpp с функциями и пример script.lua

#include "Script.h"

#include <conio.h>
#include <windows.h>
#include <time.h>

Script script;

int Write(lua_State*)
{
	// Тут мы считываем количество аргументов и каждый аргумент выводим
	for(int i = 1; i (i);
	cout > str;
	script.Return<char>(str);

	// Не забудьте! У нас возвращается 1 результат -> return 1
	return 1;
}

int Message(lua_State*)
{
	// Выводим обычное сообщение MessageBox из Windows.h
	// Кстати, вам домашнее задание - сделайте возможность вывода сообщений с несколькими аргументами :)

	char* msg = script.GetArgument<char>(1);

	MessageBox(0,msg,"Сообщение",MB_OK);

	return 0;
}

int GetTwoRandomNumbers(lua_State*)
{
	// Возвращаем два рандомных числа до 1000

	srand(time(NULL));
	for(int i = 0; i (rand()%1000);

	// Вовзращаем 2 значения
	return 2;
}

int GetLotOfRandomNumbers(lua_State*)
{
	// Возвращаем много рандомных чисел до 1000

	srand(time(NULL));
	for(int i = 0; i (1); i++)
		script.Return<int>(rand()%1000);

	// Вовзращаем столько значений, сколько задано в аргументе
	return script.GetArgument<int>(1);
}

int main()
{
	script.Create();

	script.RegisterConstant<lua_cfunction>(Write,"Write");
	script.RegisterConstant<lua_cfunction>(GetString,"GetString");
	script.RegisterConstant<lua_cfunction>(Message,"Message");
	script.RegisterConstant<lua_cfunction>(GetTwoRandomNumbers,"Rand1");
	script.RegisterConstant<lua_cfunction>(GetLotOfRandomNumbers,"Rand2");

	script.DoFile("script.lua");
	script.Close();

	// Пауза после скрипта
	_getch();
}
for i = 1, 4 do
	Write(i, "n", "Hier kommt die Sonne", "n")
end

Write(2*100-1)

Message("Привет!")

a, b = Rand1()
Write(a, "n", b, "n")
Write(Rand1(), "n")

a, b, c, d = Rand2(4)
Write(a, "n", b, "n", c, "n", d, "n")

return 1
Полезные советы

  • Для класса Script все равно, в каком расширении находится скрипт, хоть в .txt, хоть в .lua, хоть в .bmp, просто .lua открывается множеством редакторов именно ЯП Луа
  • Используйте редакторы Lua кода, очень трудно писать код, можно забыть написать end, do, либо что-нибудь еще. Программа из-за ошибки в луа скрипте не вылетит, но просто не выполнит код
  • Lua может оказаться намного гибче, чем вам могло показаться. К примеру, числа свободно преобразуются в строки, он нетипизирован. Если передать в функцию 100 параметров, а она в С++ считывает только первые 2, то программа не вылетит. Есть еще много подобных допущений.

Вопросы и ответы

  • Вопрос: Почему мы не используем луа стейт, который есть в каждой подобной функции — int foo(lua_State* L)?
    Ответ: За всю программу мы используем только один стейт в Script, где регистрируем функции, инициализируем его и делаем прочие штучки. К тому же просто невыгодно было бы, написав целый класс, опять обращаться начистоту к lua_State через lua_pushboolean и прочие функции.

Полный листинг Script.h и Script.cpp

Script.h

#ifndef _SCRIPT_H_
#define _SCRIPT_H_

#pragma comment(lib,"lua.lib")
extern "C"
{
	#include <lua.h>
	#include <lualib.h>
	#include <lauxlib.h>
}

class Script
{
private:
    lua_State *lua_state;

public:
	void Create();
	void Close();
	int DoFile(char* ScriptFileName);
	template<class t>
	void RegisterConstant(T value, char* constantname);

	void Array();
	template<class t>
	void RegisterConstantArray(T value, int index);
	void RegisterArray(char* arrayname);

	int GetArgumentCount();
	template<class t>
	T GetArgument(int index);
	template<class t>
	void Return(T value);
};

#endif

Я удалил инклуды для работы с консолью

Script.cpp

#include "Script.h"

void Script::Create()
{
	lua_state = luaL_newstate();
	
	static const luaL_Reg lualibs[] = 
	{
		{"base", luaopen_base},
		{"io", luaopen_io},
		{NULL, NULL}
	};

	for(const luaL_Reg *lib = lualibs; lib->func != NULL; lib++)
	{
		luaL_requiref(lua_state, lib->name, lib->func, 1);
		lua_settop(lua_state, 0);
	}
}

void Script::Close()
{
	lua_close(lua_state);
}

int Script::DoFile(char* ScriptFileName)
{
	luaL_dofile(lua_state,ScriptFileName);

	return lua_tointeger(lua_state, lua_gettop(lua_state));
}

template
void Script::RegisterConstant<int>(int value, char* constantname)
{
	lua_pushinteger(lua_state, value);
	lua_setglobal(lua_state,constantname);
}

template
void Script::RegisterConstant<double>(double value, char* constantname)
{
	lua_pushnumber(lua_state, value);
	lua_setglobal(lua_state,constantname);
}

template
void Script::RegisterConstant<char>(char* value, char* constantname)
{
	lua_pushstring(lua_state, value);
	lua_setglobal(lua_state,constantname);
}

template
void Script::RegisterConstant<bool>(bool value, char* constantname)
{
	lua_pushboolean(lua_state, value);
	lua_setglobal(lua_state,constantname);
}

template
void Script::RegisterConstant<lua_cfunction>(int(*value)(lua_State*), char* constantname)
{
	lua_pushcfunction(lua_state, value);
	lua_setglobal(lua_state,constantname);
}


void Script::Array()
{
    lua_createtable(lua_state, 2, 0);
}


template
void Script::RegisterConstantArray<int>(int value, int index)
{
	lua_pushnumber(lua_state, index);
	lua_pushinteger(lua_state, value);
	lua_settable(lua_state, -3);
}

template
void Script::RegisterConstantArray<double>(double value, int index)
{
	lua_pushnumber(lua_state, index);
	lua_pushnumber(lua_state, value);
	lua_settable(lua_state, -3);
}

template
void Script::RegisterConstantArray<char>(char* value, int index)
{
	lua_pushnumber(lua_state, index);
	lua_pushstring(lua_state, value);
	lua_settable(lua_state, -3);
}

template
void Script::RegisterConstantArray<bool>(bool value, int index)
{
	lua_pushnumber(lua_state, index);
	lua_pushboolean(lua_state, value);
	lua_settable(lua_state, -3);
}

template
void Script::RegisterConstantArray<lua_cfunction>(lua_CFunction value, int index)
{
	lua_pushnumber(lua_state, index);
	lua_pushcfunction(lua_state, value);
	lua_settable(lua_state, -3);
}

void Script::RegisterArray(char* arrayname)
{
	lua_setglobal(lua_state, arrayname);
}

int Script::GetArgumentCount()
{
	return lua_gettop(lua_state);
}

template
int Script::GetArgument<int>(int index)
{
	return lua_tointeger(lua_state,index);
}

template
double Script::GetArgument<double>(int index)
{
	return lua_tonumber(lua_state,index);
}

template
char* Script::GetArgument<char>(int index)
{
	return (char*)lua_tostring(lua_state,index);
}

template
bool Script::GetArgument<bool>(int index)
{
	return lua_toboolean(lua_state,index);
}


template
void Script::Return<int>(int value)
{
	lua_pushinteger(lua_state,value);
}

template
void Script::Return<double>(double value)
{
	lua_pushnumber(lua_state,value);
}

template
void Script::Return<char>(char* value)
{
	lua_pushstring(lua_state,value);
}

template
void Script::Return<bool>(bool value)
{
	lua_pushboolean(lua_state,value);
}

Репозиторий с lib'ой и includ'ами: https://github.com/Izaron/LuaForHabr

Все вопросы посылайте мне в ЛС, либо в этот топик, либо, если вам не повезло быть зарегистрированным на хабре — на мейл izarizar@mail.ru

Автор: Izaron

Источник

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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js