Поделиться 

Автор: Константин Гиляровский
В данной статье мне хотелось бы показать почему мне понравился скриптовый язык AngelScript. Когда я выбирал язык для своего движка, я просмотрел множество кандидатур, и для подробного рассмотрения выбрал два скриптовых языка, ими были LUA и Python.
Введение.
Превью.
Особенности.
Проблемы.
Регистрация перегруженных функций.
Регистрация перегруженных методов.
Получения адреса на переменную объявленную в классе.
Полезные ссылки.
В процессе рассмотрения LUA и Python я выделил для себя, что LUA является достаточно быстрым, но с немного непривычным синтаксисом. Python же обладает очень простым синтаксисом и массой полезных библиотек, но, к сожалению, он оказался довольно медленным, и его довольно тяжело привязывать к С++. И тут на работе мне подсказали использовать AngelScript, мол, он удобный для связки, быстрее LUA и имеет С-подобный синтаксис. Как только я начал его изучать, я понял, что это тот самый скриптовый язык моей мечты.

Вот что можно прочитать про этот язык на Википедии:
AngelScript представляет собой движок, в котором приложение может регистрировать функции, свойства и типы, которые могут использоваться в скриптах. Скрипты компилируются в модули. Количество используемых модулей варьрируется в зависимости от нужд. Приложение может также использовать различные интерфейсы для каждого модуля с помощью групп конфигурации. Это особенно полезно, когда приложение работает с несколькими типами скриптов, например, GUI, AI и т.д.
Программа «Hello, world» в простейшем случае выглядит так:
void main() { print("Hello world\n"); }
Да, синтаксис языка радует с самого начала. Язык поддерживает как методы функционального программирования, так и ООП. С самого начала он подкупает своей простотой регистрации функций, переменных, типов.
Например, регистрация глобальной переменной:
g_Engine->RegisterGlobalProperty("int SomeVal",&SomeVal);
Регистрация глобальной функции:
g_Engine->RegisterGlobalFunction("void Print(string val)", asFUNCTION(Print), asCALL_CDECL); void Print(string val) { cout<<val.data(); }
Да, для AngelScript не нужно писать функции биндинга, что является огроменным плюсом в сравнении с другими языками. Для регистрации своих типов придётся написать парочку функций. Фабрику для создания экземпляров и счётчик ссылок, для типа Тип-ссылка, и вызовы конструктора и деструктора, для объекта типа Тип-значение.
Например, у нас есть класс float3, который мы хотели бы зарегистрировать.
// класс счётчик ссылок class RefC { private: int refC; public: RefC(){refC=1;} void AddRef(){refC++;} void Release() { if(!--refC) delete this; } }; // Класс, который мы хотим зарегистрировать class float3:public RefC { public: float x; float y; float z; float3(){x=y=z=0;} void Normalize() { float Len=sqrt(x*x+y*y+z*z); Len=Len?Len:1; x/=Len; y/=Len; z/=Len; } } // Фабрика float3* Float3FactoryE() { return new float3(); } // Функция вывода на экран void PrintF3(float3* val) { cout<<"x="<<val->x<<",y="<<val->y<<",z="<<val->z; }
Для этого мы регистрируем объект как Тип-ссылка и указываем ему фабрику, счётчик ссылок, метод и функцию вывода данных на экран, и вот как это выглядит.
Мы на этом конечно же не остановимся, так как нам нужен доступ к значениям xyz, поэтому их мы тоже должны зарегистрировать, что мы и делаем написав.
g_Engine->RegisterObjectProperty("float3","float x",offsetof(float3,x)); g_Engine->RegisterObjectProperty("float3","float y",offsetof(float3,y)); g_Engine->RegisterObjectProperty("float3","float z",offsetof(float3,z));
Всё предельно просто и понятно. Теперь в скрипте можно написать
float3@ ObjPos; ObjPos.x=1; ObjPos.y=2; ObjPos.z=3; ObjPos.Normalize(); Print( ObjPos );
Выполнив этот скрипт, мы увидим на экране значение нормализованного вектора.
Меня очень порадовала возможность перегрузки операторов в AngelScript. В С++ для это существует ключевое слово operator и символ оператора. AngelScript для этого использует определённые функции. Вот полный список соответствий.
♦ – opNeg
♦ ~ opCom
♦ ++ opPreInc
♦ -- opPreDec
♦ ++ opPostInc
♦ -- opPostDec
♦ == opEquals
♦ != opEquals
♦ < opCmp
♦ <= opCmp
♦ > opCmp
♦ >= opCmp
♦ = opAssign
♦ += opAddAssign
♦ -= opSubAssign
♦ *= opMulAssign
♦ /= opDivAssign
♦ &= opAndAssign
♦ |= opOrAssign
♦ ^= opXorAssign
♦ %= opModAssign
♦ <<= opShlAssign
♦ >>= opShrAssign
♦ >>>= opUShrAssign
♦ + opAdd opAdd_r
♦ - opSub opSub_r
♦ * opMul opMul_r
♦ / opDiv opDiv_r
♦ % opMod opMod_r
♦ & opAnd opAnd_r
♦ | opOr opOr_r
♦ ^ opXor opXor_r
♦ << opShl opShl_r
♦ >> opShr opShr_r
♦ >>> opUShr opUShr_r
♦ [] opIndex
Допустим, мы хотим сделать возможность, чтобы наш вектор поддерживал прибавление к себе другого вектора, для этого слегка модифицируем наш класс.
class float3:public RefC { public: float x; float y; float z; float3(){x=y=z=0;} void Normalize() { float Len=sqrt(x*x+y*y+z*z); Len=Len?Len:1; x/=Len; y/=Len; z/=Len; } float3* operator+=(float3* _rval) { x+=_rval->x; y+=_rval->y; z+=_rval->z; this->AddRef(); return this; } };
Осталось только зарегистрированный новый метод.
g_Engine->RegisterObjectMethod("float3", "float3@ opAddAssign(float3@ _rval)", asMETHOD(float3, operator+=), asCALL_THISCALL);
И теперь можно спокойно писать так:
float3@ ObjPos; ObjPos.x=1; ObjPos.y=2; ObjPos.z=3; float3@ ObjOffset; ObjOffset .x=3; ObjOffset .y=1; ObjOffset .z=5; ObjPos+=ObjOffset ; Print( ObjPos );
AngelScript поддерживает свойства. Выглядит это так:
class MyObj { type get_ValueName(); type set_ValueName(type Val); } MyObj a; type tmp=a.ValueName;// вызовется get_ValueName a.ValueName = tmp; // вызовется set_ValueName
Также свойства поддерживаются для оператора индекса:
class MyObj { float get_opIndex(int idx) ; void set_opIndex(int idx, float value); } MyObj a; float val=a[1];// вызовется get_opIndex a[2]=val;// вызовется set_opIndex
Во время изучения языка AngelScript, появилось несколько проблем, решение которых отняло у меня довольно много времени. Вот список проблем, о которых я хотел бы рассказать:
♦ Регистрация перегруженных функций.
♦ Регистрация перегруженных методов.
♦ Получения адреса на переменную объявленную в классе.
Очень часто возникает необходимость объявить перегруженную функцию для удобства чтения и понимания кода.
Например:
void Print(string val) { cout<<val.data(); } void Print(int val) { cout<<val; } void Print(float val) { cout<<val; } void Print(float3* val) { cout<<"x="<<val->x<<",y="<<val->y<<",z="<<val->z; }
Обычная регистрация вызовет ошибку.
g_Engine->RegisterGlobalFunction("void Print(string val)", asFUNCTION(Print), asCALL_CDECL); // Ошибка
Так как компилятор не понимает адрес какой из четырёх функций нужно передать, то на ум приходит два решения:
♦ Создать typedef нужной функции и осуществить приведение типов.
♦ Создать переменную указатель на нужную функцию и передать его.
вот как выглядит второе решение:
void (*PrintS)(string val)=&Print; void (*PrintI)(int val)=&Print; void (*PritnF)(float val)=&Print; void (*PrintF3)(float3* val)=&Print; g_Engine->RegisterGlobalFunction("void Print(string val)",asFUNCTION(PrintS),asCALL_CDECL); g_Engine->RegisterGlobalFunction("void Print(int val)",asFUNCTION(PrintI),asCALL_CDECL); g_Engine->RegisterGlobalFunction("void Print(float val)",asFUNCTION(PritnF),asCALL_CDECL); g_Engine->RegisterGlobalFunction("void Print(float3@ val)",asFUNCTION(PrintF3),asCALL_CDECL); |
С данной проблемой вы столкнётесь, если захотите зарегистрировать оператор присваивания. Давайте для примера модифицируем наш класс.
class float3:public RefC { public: float x; float y; float z; float3(){x=y=z=0;} void Normalize() { float Len=sqrt(x*x+y*y+z*z); Len=Len?Len:1; x/=Len; y/=Len z/=Len; } float3* operator+=(float3* _rval) { x+=_rval->x; y+=_rval->y; z+=_rval->z; this->AddRef(); return this; } float3* operator=(float3* _rval) { x=_rval->x; y=_rval->y; z=_rval->z; this->AddRef(); return this; } };
Обычная регистрация вызовет ошибку.
g_Engine->RegisterObjectMethod("float3", "float3@ opAssign(float3@ _rval)", asMETHOD(float3, operator=), asCALL_THISCALL); // Ошибка |
Решения, которые подходят для функций, тут не подойдут. Для решения проблемы надо внимательно взглянуть на макрос asMETHOD он выглядит так:
#define asMETHOD(c,m) asSMethodPtr::Convert((void (c::*)())(&c::m))
Соответственно, чтобы нам зарегистрировать метод, нужно конвертировать его. Тогда наша регистрация будет выглядеть так:
g_Engine->RegisterObjectMethod("float3", "float3@ opAssign(float3@ _rval)", asSMethodPtr::Convert((float3* (float3::*)(float3*))(&float3::operator=)), asCALL_THISCALL);
Если с глобальными переменными дела обстоят предельно просто, то с переменными в пределах класса всё немного сложнее. Стандартные средства не позволяют получить ID переменной по её имени или объявлению, поэтому для этого необходимо пройтись по всем переменным в классе самому и сравнить имена. Для поиска адреса по имени переменной нам потребуется само имя и указатель на экземпляр класса скрипта.
Функция будет выглядеть так:
void* GetPropAddress(const char* Name,asIScriptObject* ScriptObject) { for(int i=0;iGetPropertyCount();i++) if(!strcmp(ScriptObject->GetPropertyName(i),Name)) return ScriptObject->GetAddressOfProperty(i); return 0; }
Вот и всё, что мне хотелось бы рассказать, да я не осветил моменты касающиеся JIT и Шаблонов, но это только потому, что ещё не разбирался с этим. По мере изучения буду обновлять статью.
2 ноября 2011
Категории: AngelScript, скрипты
Обновление: 1 марта 2012