Решил написать этот топик на тему скриптов
Что нужно знать?
- С++ на приличном уровне (в уроке будут шаблоны — 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
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
void Script::Close() { lua_close(lua_state); }
Просто используем lua_close()
int DoFile(char* ScriptFileName);
А эта функция выполняет файл. На вход она принимает название файла, например, «C:\\script.lua».
Почему она возвращает int? Просто некоторые скрипты могут содержать return, прерывая работу скрипта и возвращая какое-нибудь значение.
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<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);
void Script::Array(int size) { lua_createtable(lua_state, 2, 0); }
Следующая функция регистрирует элемент в таблице.
template<class t> void RegisterConstantArray(T value, int index); </class>
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);
void Script::RegisterArray(char* arrayname) { lua_setglobal(lua_state, arrayname); }
Ничего особенного нет
Следующие функции предназначены в основном только для функций типа int foo(lua_State*), которые нужны для регистрации в Луа.
Первая из них — получает количество аргументов
int GetArgumentCount();
int Script::GetArgumentCount() { return lua_gettop(lua_state); }
Эта функция нужна, например, для функции Write(), куда можно запихать сколь угодно аргументов, а можно и ни одного
Подобную функцию мы реализуем позже
Следующая функция получает аргумент, переданный функции в скрипте
template<class t> T GetArgument(int index); </class>
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>
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);
Компилируем и запускаем проект.
Теперь изменяем script.lua
for i = 1, 4 do Write(i, "\n", "Hier kommt die Sonne", "\n") end
Теперь программа будет выводить по 2 строки ("\n" — создание новой строки), ждать нажатия Enter и снова выводить строки.
Экспериментируйте со скриптами!
Вот пример 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
#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>
Я удалил инклуды для работы с консолью
#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/
Добавить комментарий