Введение в ChaiScript

Введение в ChaiScript

Также скрипты есть важной частью игровых движков. Там их используют для самых разнообразных целей:

Загрузка уровня

AI

Управление игровым состоянием (объектами, музыкой, …)

Применить скрипты можно практически везде, была бы фантазия и потребность в этом.

В качестве скриптового движка можно выбрать большинство скриптовых языков программирования. Самые популярные это Lua и Python. Игроделы всегда спорят, что лучше. В этой статье я хочу познакомить вас с другим, более “лёгким” скриптовым языком. Если вас всё-же интересуют вышеназванные, можно использовать lua + luabind, python + boost. python. С некоторыми из них я работал, поэтому, если кто-то захочет, я смогу описать работу и с ними.

Итак, я хочу вас познакомить со сравнительно молодым языком — Chaiscript. Он отличается сравнительной простотой и независимостью. Он специально разрабатывается под с++ и имеет схожий синтакс. Последняя версия 2.3.2 вышла сравнительно недавно. Начиная со второй ветки, разработчики порадовали нас блоками try/catch и безопасными потоками!

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

Chaiscript это скриптовый язык, который легко встраивается в любое с++ приложение. Он сделан гибким и динамичным, не смотря на это, поддерживает безопасность типов. Он может напрямую использовать классы, функции, методы, атрибуты, даже если класс наследует функциональность от родительского.

Chaiscript распространяется исключительно в h-файлах и зависит от boost. В поставке также идёт что-то типа интерактивной консоли, какие есть в python, lua. Чтобы работать с ней нужно запустить бинарный файл chaiscript_eval (под нужную платформу).

Объявление переменных

Переменные создаются ключевым словом var, названием и значением: var i = 5;

Наименование переменных схожее с с++.

Объявление функций

Поддерживается 2 вида функций: простые и лямбда-функции.

Def sum(x, y) { x + y

}

Также стандартные функции поддерживают условия, которые могут запретить выполнение функции: def print_num(x) : x < 5 { print(x)

}

В этом случае, число x будет выведено только в том случае, если оно меньше 5.

Лямбда-функции можно присвоить переменным или передавать их в аргументах функции: var res = fun(x, y) { x + y } print(res(5, 2))

Циклы абсолютно идентичны тем, которые есть в с++.

Методы

Chaiscript позволяет вызывать функции, как методы объектов:

5.to_string() to_string(5)

Сразу вспоминается ruby :)

Контейнеры

Поддерживается 2 вида контейнеров: vector и map.

Var vector = Vector() var vector2 = [] var vector3 = [1, 2, 3]

Поддерживает 3 метода: push_back, pop_back, back (последний элемент в контейнере).

Var map = ["name": 1, "surname": 2]

И расширенный тип vector, где мы указываем диапазон значений: var x3[1..10]

Доступ к отдельным элементам через оператор [].

Исключения абсолютно идентичны тем, что в с++. Поддерживаются блоки: try, catch, finally, throw.

Самое скучное закончилось, теперь перейдём к привязыванию скриптов в с++. Первое, что нужно сделать, это подключить нужные headers и создать глобальный скриптовой объект:

#include <chaiscript/chaiscript. hpp> chaiscript::Chaiscript chai;

Chaiscript::add

Существует для связывания разнообразных объектов из с++ в скрипты. Теперь рассмотрим то, что мы можем связать:

Fun. Fun используется для добавления функции в скриптовую систему. Мы можем использовать просто с++ функцию, boost::function<>, методы класса.

Class MyClass

{ public:

MyClass(int t_value) : m_value(t_value) {} int get_value() { return m_value; } void multiply_value(int v) { m_value *= v; } std::string data; private: int m_value;

}; std::string concat(const std::string &first, const std::string &second)

{ return first + second;

}

// Передача методов класса chai. add(fun(&MyClass:get_value), "get_value"); chai. add(fun(&MyClass::multiply_value), "multiply_value");

// Также мы можем связать любой член класса chai. add(fun(&MyClass::data), "data");

// Передача конструктора chai. add(constructor<MyClass (int)>(), "MyClass");

// Передача простой с++ функции chai. add(fun(&concat), "my_concat");

// Передача boost::function chai. add(boost::function <std::string (const std::string)>(boost::bind(&concat, "Hello ", _1)), "my_bound_concat");

// Чтобы привязать функцию к конкретному объекту (bounded function)

MyClass object(10); chai. add(fun(&MyClass::add_value, &object), "add_value");

//Теперь add_value привязана к object-объкту.

Var. Передача переменных. Можно использовать boost::shared_ptr, boost::ref, указатель на память.

Int i = 5; chai. add(var(i), "i");

В этом примере мы создали копию локального объекта в Chaiscript.

Самый надёжный путь для разделения информации — использование boost::shared_ptr<>: boost::shared_ptr<MyClass> myclass(new MyClass(5)); chai. add(var(myClass), "myclass");

Другой способ: int i = 5; chai. add(var(&i), "i");

Но этот вариант опасный из-за возможности утечки памяти.

Для создания констант в скриптовом движке нужно использовать вместо var const_var.

User_type. Для передачи своих объектов, существует user_type функция: chai. add(user_type<MyClass>(), "MyClass");

Chaiscript::eval

Для выполнения Chaiscript-кода в с++ программе существует функция eval: chai. eval("var j = 5 + 7; print(j)");

//Для сохранения результата можно сделать int i = chai. eval<int>("5 + 7");

Код Chaiscript может хранится в файлах, для его выполнения нужно просто вызвать функцию Chaiscript::eval_file(file_name).

Теперь перейдите к примеру, где мы использовали user_type. Давайте получим наш объект из скриптового движка: typedef boost::shared_ptr<MyClass> SharedMyClass;

SharedMyClass obj = chai. eval<SharedMyClass>("var ownObj = MyClass(2)"); obj. multiply_value(3); // Вызываем метод объекта из с++ chai. eval("print(obj. get_value())"); // Проверяем значение из скрипта (оно изменилось) chai. eval("obj. multiply_value(2)"); // Теперь вызываем метод из скрипта std::cout << obj. get_value(); // Значение опять изменилось

Chaiscript::functor

Позволяет нам использовать Chaiscript код как нативный с++. Эта шаблонная функция принимает сигнатуру функции и строку для парсинга: boost::function<void ()> print_value = chai. functor<void ()>("fun() {print obj. get_value()}");

Здесь мы задали сигнатуру функции, которая возвращает void и имеет 0 аргументов, получили такую функцию из Chaiscript, присовили указатель на функцию к print_value.

Boost::function<int (int, int)> int_max = chai. functor<int (int, int)>("fun(x, y) { if (x>y) {x} else {y} }"); int m = int_max(6, 7);

В этом примере задаём другую сигнатуру: возвращает число и имеет 2 числа-аргумента.

Chaiscript::State

Для сохранения стека скриптовой системы можно использовать функции get_state & set_state.

Chaiscript::State state = chai. get_state(); chai. set_state(state); functor

Свободная функция, схожая с Chaiscript::functor методом в том, что она безопасно вызывает с++ функцию из Chaiscript-объекта. Отличие в том, что функция ожидает объект chaiscript::Proxy_Function. Всё это используется для создания c++ callback’ов из скрипта.

Struct Callback_Handler

{ std::vector<boost::function<int (const std::string &)> > m_funcs; // Сигнатура callback void add(const chaiscript::Proxy_Function &f)

{

// Создаем boost::function по сигнатуре из переданного Proxy_Function объекта

// Созданную функцию добавляем в список callback’ов m_funcs. push_back(chaiscript::functor<int (const std::string &)>(f));

}

}; int main()

{ chaiscript::Chaiscript chai; chai. add(fun(&Callback_Handler::add), "add"); // Создаём функцию добавления коллбэков

Callback_Handler ch; chai. add(var(&ch), "callback_handler"); chai. eval("callback_handler. add(fun(x) { x. size(); })"); // Добавляем Chaiscript-функцию в список return ;

}

Object Model

В Chaiscript вы можете создавать собственные объекты, добавлять им методы.

Def MyClass::MyClass() {} var myobject = MyClass()

//Аттрибуты. Это аналог членов класса в с++ attr Rectangle::height attr Rectangle::width def Rectangle::Rectangle() { this. height = 10; this. width = 20 } def Rectangle::area() { this. height * this. width } var rect = Rectangle() rect. height = 30 print(rect. area())

Chaiscript содержит собственную библиотеку по работе с разными объектами. Для примера: перевод объекта в строку, поиск подстроки, удаление лишних пробелов, кратность чисел, for_each цикл, фильтр значений. Чтобы не описывать каждый из них, я просто покажу вам пару примеров по работе с некоторыми функциями. Их также можно найти на сервисе YouTube, где разработчик выложил видео по работе.

Запустите интерактивную консоль, о которой я уже говорил выше. Все действия происходят в ней.

> 3 + 4

7

> def sum(x, y) { x + y }

> sum (3, 4)

7

>3.sum(4)

7

>filter([1, 2, 3, 4], odd)

[1, 3]

> [1, 2, 3, 4].filter(odd)

[1, 3]

> [1..100].filter(odd)

1, 3, …, 99

> [1..100].filter(odd).size()

50

> [1..100].filter(fun(x) {x < 10} )

[1, …, 9]

Практика

Перейдём к практике. Наша задача — рисовать разные примитивы на окне, используя скрипты. Мы должны определить что-то типа интерфейса функций рисования, которые должны быть доступными, а потом просто использовать их в скрипте. Чтобы не отвлекаться на конкретное API рисования, я взял Qt. В этой библиотеке это очень просто.

Создайте новый Qt Gui проект. Мы имеем 3 файла. Main. cpp, mainwindow. cpp/h. Первый нам не понадобится. Добавьте новые файлы: ChaiPainter. h/cpp. В первом и будет реализация интерфейса:

#ifndef CHAIPAINTER_H

#define CHAIPAINTER_H

#define CHAIscript_NO_THREADS

#include <chaiscript/chaiscript. hpp>

#include <QPainter> class ChaiPainter

{ public:

ChaiPainter();

// Получить указатель на объект, который будет отрисовывать

QPainter *getPainter() { return painter; }

// Функции рисования, которые будем использовать в скрипте void drawRectangle(QPainter *painter, int x, int y, int width, int height); void drawEllipse(QPainter *painter, int x, int y, int width, int height); void drawLine(QPainter *painter, int x1, int x2, int y1, int y2);

// Основный скриптовый объект chaiscript::Chaiscript mChai;

// Сохраняем сюда копию стека в нужный момент chaiscript::Chaiscript::State mSavedState;

// Этот объект и будет рисовать

QPainter *painter;

};

#endif // CHAIPAINTER_H

Реализация, проходим по пунктам:

#include "ChaiPainter. h" void ChaiPainter::drawRectangle(QPainter *painter, int x, int y, int width, int height)

{ painter->drawRect(x, y, width, height);

} void ChaiPainter::drawEllipse(QPainter *painter, int x, int y, int width, int height)

{ painter->drawEllipse(x, y, width, height);

} void ChaiPainter::drawLine(QPainter *painter, int x1, int x2, int y1, int y2)

{ painter->drawLine(x1, y1, x2, y2);

}

Всё это стандартные методы рисования, на них не будем отвлекаться. Теперь самое интересное — конструктор:

ChaiPainter::ChaiPainter()

{ using namespace chaiscript; mChai. add(chaiscript::fun(&ChaiPainter::drawRectangle, this), "drawRect"); mChai. add(chaiscript::fun(&ChaiPainter::drawEllipse, this), "drawEllipse"); mChai. add(chaiscript::fun(&ChaiPainter::drawLine, this), "drawLine");

}

Инициализируем для скрипта все наши функции рисования.

MChai. add(var(QColor(, , )), "Black"); mChai. add(var(QColor(255, , )), "Red"); mChai. add(var(QColor(, 255, )), "Green"); mChai. add(var(QColor(, , 255)), "Blue");

Определим некоторые цвета.

MChai. add(fun(&ChaiPainter::getPainter, this), "getPainter");

Привязываем функцию получения указателя на painter.

Теперь общая схема рисования: создаем Pen-объект, в конструктор ему передаем цвет, задаем ширину пера функцией setWidth, задаём тип рисования пера, назначаем перо painter’у.

Сначала, передадим Qpen тип в скрипт: mChai. add(chaiscript::user_type<QPen>(), "QPen"); mChai. add(chaiscript::bootstrap::copy_constructor<QPen>("QPen"));

// QPen QPen::QPen(QColor color) mChai. add(chaiscript::constructor<QPen (QColor)>(), "Qpen"); user_type передаёт пользователский тип, bootstrap::copy_constructor передаёт конструктор копирования, constructor< signature> создаёт конструктор с заданной сигнатурой.

Добавляем дополнительные функции и значения для пера: mChai. add(chaiscript::fun(&QPen::setStyle), "setStyle"); mChai. add(chaiscript::fun(&QPen::setWidth), "setWidth"); mChai. add(chaiscript::var(Qt::DotLine), "penStyleDotLine"); mChai. add(chaiscript::var(Qt::DashLine), "penStyleDashLine"); mChai. add(chaiscript::var(Qt::DashDotDotLine), "penStyleDashDotDotLine");

С QPen разобрались. Теперь нужно передать QColor: mChai. add(chaiscript::user_type<QColor>(), "QColor"); mChai. add(chaiscript::bootstrap::copy_constructor<QColor>("QColor"));

// QColor Qcolor::QColor(int r, int g, int b, int a = 255) mChai. add(chaiscript::constructor<QColor (int, int, int, int)>(), "QColor");

Теперь нужно назначить перо объекту QPainter. Но, глянув в документацию, мы видим, что функция QPainter::setPen перегруженная, поэтому нам нужно помочь с++ компилятору выбрать нужную. Для этого нужно создать указатель на эту функцию с нужной сигнатурой. setPen, в нашем случае, должна принимать QPen-объект, поэтому делаем: typedef void (QPainter::*setPenPtr)(const QPen &); setPenPtr ptr = &QPainter::setPen; mChai. add(chaiscript::fun(ptr), "setPen");

Создаем указатель, привязываем его к setPen, передаём в скрипт.

На этом конструктор заканчивается, поэтому нам нужно обязательно сохранить стек chai, иначе в других методах всё это затеряется.

MSavedState = mChai. get_state();

Наш интерфейс готов, теперь нужно применить его. Переходим в файл mainwindow. h, подключаем include: #include <ChaiPainter. h> и в секцию private добавляем ChaiPainter *chaiPainter. Не забываем, что нам нужно рисовать, поэтому нужно переопределить событие рисования. В protected добавляем void paintEvent(QPaintEvent *event). В этом методы мы и будем рисовать скриптом. Переходим в mainwindow. cpp, пишем: void MainWindow::paintEvent(QPaintEvent *event)

{ chaiPainter->mChai. set_state(chaiPainter->mSavedState); chaiPainter->painter = new QPainter(this); chaiPainter->mChai. eval_file("draw. chai"); delete chaiPainter->painter;

}

Загружаем состояние стека скрипта, создаём новый объект QPainter, запускаем на выполнение файл со скриптом, удаляем указатель на QPainter, иначе, при закрытии, будет вылетать ошибка освобождения памяти. Теперь сам скрипт. Здесь всё ваша фантазия. Чтобы не усложнять пример, я сделал небольшого хабра-человечка :)

// [Chaiscript код]

// We have to call getPainter each time

// cause we can’t copy QPainter constructor

// it’s private, so calling:

// var painter = QPainter(getPainter())

// is impossible in our case var headPen = QPen(QColor(240, 160, 160, 220)) headPen. setStyle(penStyleDashLine) headPen. setWidth(2) var bodyPen = QPen(Blue) bodyPen. setStyle(penStyleDashDotDotLine) bodyPen. setWidth(3) var legPen = QPen(Black) legPen. setWidth(4)

// Draw human’s head — getPainter().setPen(headPen) drawEllipse(getPainter(), 50, 50, 55, 50)

// Draw human’s body — getPainter().setPen(bodyPen) drawRect(getPainter(), 50, 100, 55, 90)

// Draw hands — getPainter().setPen(headPen) drawLine(getPainter(), 50, 25, 100, 180) drawLine(getPainter(), 105, 135, 100, 20)

// Draw legs — getPainter().setPen(legPen) drawLine(getPainter(), 50, 25, 190, 250) drawLine(getPainter(), 105, 135, 190, 240)


Карта сайта


Информационный сайт Webavtocat.ru