Что такое скрипты и с чем их едят — 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> } </lauxlib.h></lualib.h></lua.h>

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

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

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

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

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); </class>
RegisterConstant()

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

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

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

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); } </lua_cfunction></bool></char></double></int>

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

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

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

a, b = Foo() 

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

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

	void Array(int size); 
Array()

Ее описание

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

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

	template<class t> 	void RegisterConstantArray(T value, int index); </class>
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_cfunction></bool></char></double></int>

Если вы не знаете 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); </class>
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); } </bool></char></double></int>

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

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

	template<class t> 	void Return(T value); </class>
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); } </bool></char></double></int>

Боевой код

Пора что-нибудь сделать!
Изменяем 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(); } </lua_cfunction></conio.h>

А в папке с проектом создаем файл 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(); } </lua_cfunction></lua_cfunction></lua_cfunction></lua_cfunction></lua_cfunction></int></int></char></char></time.h></windows.h></conio.h>
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(int size); 	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 </class></class></class></class></lauxlib.h></lualib.h></lua.h>

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

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(int size) {     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); } </bool></char></double></int></bool></char></double></int></lua_cfunction></bool></char></double></int></lua_cfunction></bool></char></double></int>

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/196272/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *