Добрый день!
Решил написать этот топик на тему скриптов
Что нужно знать?
- С++ на приличном уровне (в уроке будут шаблоны — 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
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);
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();
void Script::Array()
{
lua_createtable(lua_state, 2, 0);
}
Следующая функция регистрирует элемент в таблице.
template<class t>
void RegisterConstantArray(T value, int index);
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);
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);
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);
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);
Компилируем и запускаем проект.
Теперь изменяем 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();
}
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();
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
Я удалил инклуды для работы с консолью
#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