{{notification.text}}

MirGames

 
18.03.06 04:50, опубликовал (Автор оригинала: Юрий "universal" Иванов)

Сразу хочу предупредить, что являюсь далеко не экспертом в этой области, однако, надеюсь, мой опыт написания собственного обработчика скриптов натолкнет Вас на верные мысли, и, конечно же, рекомендую ознакомиться с другими источниками.

Какие-то термины могут не совпадать с подобными терминами из других источников, некоторые из них, возможно, используются в непривычном контексте, если вы обнаружите явные ошибки в статье – просьба сообщить автору, и они по возможности будут исправлены.

В данной статье обсуждаются важнейшие вопросы и проблемы, встающие при написании собственного обработчика скриптов, не зависимо от языка скрипта и языка программирования (хотя все language-specific высказывания относятся к C++, большинство из них переносится и на другие языки). Вы не найдете здесь готовой реализации скрипт-процессора или каких-то его составляющих, но, надеюсь, статья поможет Вам самим написать обработчик скриптов.

Script itself
Скрипты – нужны ли они мне вообще?

Это действительно важный вопрос. Перед тем, как использовать некую технологию, необходимо решить – а действительно ли она нужна нам в данном случае?

Скрипты в игре могут использоваться для различных целей, вот основные из них:

  • Удобная и гибкая инициализация ресурсов для уровня, т.е. в данном смысле скрипт используется как часть формата карты, которая сама создает свои объекты по произвольному сценарию;
  • Скрипт как внутренний язык, описывающий такие ресурсы как GUI, стратегию поведения AI или силу выстрела оружия.
  • Высокоуровневое управление игровым процессом, например управление квестом или команды для AI, такие, как «следовать за игроком» или «начать атаку» и т.д.
  • Отличный способ задать событийную систему поведения для некоторых алгоритмов путем расстановки на карте заскриптованных триггеров или динамической привязке скрипта к событиям отдельных объектов (например, установить скрипт провала миссии в случае смерти ключевого персонажа или по нажатию кнопки «Отменить миссию» из меню).
  • Удобный способ тестирования движка (особенно, при возможности исполнять скрипты сразу из консоли)
  • Временные алгоритмы, при реализации которых можно забыть о тонкостях организации архитектуры программы и очистке ресурсов, легко изменяемые, даже во время исполнения программы (что также, несомненно, экономит нервы и время, особенно при медленной загрузке игры и уровней)

Теперь решим, нужен ли Вам для этих целей скрипт.

  • Если вашим ресурсам не требуется нетривиальная инициализация, а, например, каждый из объектов карты должен знать лишь свои координаты и еще несколько полей (идентичных), то обычного формата карты Вам будет вполне достаточно, к тому же так инициализация карты будет происходить быстрее.
  • Если такими элементами гейм – дизайна, как GUI у Вас занимается программист, то, при правильной архитектуре программы от внесения таких вещей в исходный код программы вы получите почти сплошные плюсы. До некоторых пор J. Переменные стратегии поведения AI или параметров оружия можно хранить в вашем собственном формате данных или ini-файле, однако это сильно уменьшает гибкость программы и ее лояльность к серьезным внутренним изменениям.
  • Если у Вас нет квестов или динамичного поведения AI, скрипт - процессор для этих целей Вам не нужен. Кроме того, при ограниченности подобных вариантов их также можно отдать в хардкод + данные из файла.
  • У Вас нет нетривиальных событий? GUI находится в исходном коде? Значит для этих целей можно обойтись и без скриптов.
  • У вас ведь есть debugger? Все преимущество скриптов для этих целей заключается в возможности проверить что-то прямо «здесь и сейчас», без перекомпиляции и перезапуска программы, меня какие-то детали в выполнении алгоритма «на лету». Возможности встроенного отладчика среды программирования значительно выше.
  • Такие временные алгоритмы – очень медленные и в любом случае их придется переносить в код. Вы можете сделать это сразу.
Почему свой скрипт-процессор?

Допустим, Вы все же решили, что скрипты пригодятся. Но можно ведь взять готовые обработчики скриптов, такие как LUA (см. список литературы), Angel Script и многих других. Действительно, обычно так и стоит сделать, если вас полностью удовлетворяет скриптовый язык и обработчик скриптов. Но если они Вас по каким либо причинам не устраивают, либо же Вы хотите лучшей совместимости именно с вашим проектом, а может быть просто научиться – что ж, приступим!

Скриптовый язык

В конечном итоге, выбор/разработка скриптового языка является решающим фактором и определяет гибкость и возможности Вашего скрипт – процессора, удобство и скорость написания скриптов и их последующего сопровождения, а также от части скорость подготовки скрипта к выполнению (об этом дальше) и скорость выполнения скриптов.

Например, если основная задача Вашего скрипта инициализировать ресурсы и задавать параметры объектов, вероятно будет достаточно скриптов как последовательности инструкций, например:

LoadMap "Map01.map"
Player–>SetHealth 200
InitPhysics
StartLevel

Достаточно удобно для поставленной цели, к тому же просто в реализации – не требует переменных (можно, впрочем, добавить), скриптовых функций и всех остальных проблем, свойственных алгоритмическим скриптам.

Другим вариантом может быть скрипт, напоминающий процедурно-ориентированный язык программирования (даже, возможно, с поддержкой рекурсии и т.п.), но без возможности объявлять свои типы данных и их объединения. Возможно, также обеспечить доступ к экземплярам классов программы. Такого рода скрипт как раз использую я.

Тут можно поиграть с синтаксисом языка, выбрав язык, наиболее привычный команде разработки проекта, или придумать собственный, более, по Вашему мнению, совершенный язык. Но главное не увлекаться – не забывайте, что кроме того, что это все надо еще реализовать в скрипт – процессоре, многие лишние «навороты» замедлят Ваш скрипт – процессор. В нашем проекте использован С – подобный скрипт (т.н. GODScript), поскольку этот язык является наиболее привычным для команды.

Пример кода [GODScript]:

while (sleep(100))
{
     indoorHint.Show(false);
     if (!(IndoorName() == "LighthouseIndoor[0]") &&
         (fabsf(Player.GetPosX() - 152) + fabsf(Player.GetPosZ() - 113.7f) < 5))
            {
                  indoorHint.SetCaption(l.hint.Press_E_to_get_into_the_house());
                  indoorHint.Show(true);
                  if (KeyPressed("E")) {
                  SetIndoor(true, "LighthouseIndoor[0]");
                        Player.SetPosition(155, 24, 114);
                        indoorHint.Show(false);
                        AudioManager.AddToPrimary("DoorOpen");
                        sleep(1000);                
                 }
           }
}

Языки такого типа могут удовлетворить почти всем потребностям, поставленным перед скрипт–языком выше. Скрипт – обработчик именно для такого языка мы и рассмотрим.

Кроме того, особо прихотливым геймдизайнерам или спортивно-настроенным программистам может захотеться добавить в скрипт еще и элементы ООП, например:

class Weapon {
      public int GetStrength();
};

Действительно, подобные ООП возможности скрипта дают непревзойденную гибкость и возможности, однако существенно осложняют реализацию скрипт-процессора, особенно, учитывая сложность поддержания достойной скорости выполнения.

Думаю, возможности обобщенного или макро-программирования в скрипте абсолютно излишни.

Некоторые используют необычные языки в качестве скриптов. Довольно распространено использование xml для данных целей. У xml здесь, действительно, есть ряд преимуществ, например наличие готовых отличных парсеров (подробнее, ниже) и редакторов для них, однако есть и множество минусов, в числе которых, например, неудобный синтаксис.

Проблемы проектирования
Взаимодействие с исходным кодом программы

Тут, на самом деле вариантов не так много. Наилучший способ – регистрировать необходимые экземпляры классов, функции и переменные для скрипт - процессора при инициализации или в какой-то другой момент (указателем на соответствующий объект и именем).

Другой способ – отдавать все обязанности по обработке запросов скрипта самим объектам.

Например, в GODScript использован последний способ для доступа к функциям-членам классов:

class GODObject
{
public:
//…
//Defines script interface
virtual GODScriptEngine::Variable Execute(  const uniString& methodName,
GODScriptEngine::ExpressionTreeNode** scriptCodeTreeNode,
GODScriptEngine::ExpressionTree* scriptCodeTree, GODScript::Script* clientScript) = 0;
//…
};

Т.е. любой класс, к методам которого нужен доступ из скрипта, должен быть потомком абстрактного родителя GODObject, и соответствующим образом определять свой Execute.

Основные минусы такого подхода заключаются в нетривиальном определении Execute для каждого класса, а также в резком уменьшении гибкости во взаимодействии объектов программы и скрипт – процессором.

Кроме того, необходимо регистрировать классы программы, объекты которых могут создаваться динамически из скрипта. Это – задача проектирования взаимодействия фабрики объектов (т.е. объекта программы, основной целью которого является создание других объектов программы). Необходимо, чтобы при регистрации класса в фабрике, для данного класса устанавливалось бы строковое имя, под которым класс будет «проходить» для скриптов.

У меня для имени класса используется это же имя в коде C++ (конец имени typeid(YourClass).name() после пробела), т.е. используем RTTI (Run-Time Type Identification). Вообще говоря, использование RTTI для подобных целей не является хорошим стилем, так как имя, генерируемое по typeid(YourClass).name() в стандарте C++ не определено, кроме того при агрессивной оптимизации компилятор может выбросить RTTI вообще, даже если само по себе RTTI включено в настройках компиляции.

Вы можете, например, задавать строковое представление имени класса в виде константы-члена этого класса или как-то еще.

Скрипт как ресурс

Если Вы задумали возможность установки прерываний в скрипте (т.е. когда скрипт выполняется не от начала до оконца, останавливая при этом выполнение программы, а может отложить продолжение своего выполнения, скажем, на следующую итерацию цикла обновления программы), или же если предусмотрена возможность рекурсивного вызова скрипта (из самого себя) на выполнение, возникает возможность выполнения одного скрипта в «двух экземплярах». Однако, неразумно парсить (подробности ниже) и, вероятно, компилировать, скрипт каждый раз отдельно для каждой сессии его исполнения.

Значит имеет смысл рассматривать скриптовый код, как ресурс, используемый различными разделенными сессиями исполнения скрипта. Тогда скрипт-код может быть получен скрипт-сессией точно таким же образом, как у Вас в программе вы получаете все остальные ресурсы, например, текстуры.

У нас для этого используются менеджеры ресурсов (шаблонная инстанциация для каждого из типов ресурсов). Если у Вас нет подобных менеджеров, пора хорошенько задуматься над перепроектированием программы.

Вот мы и получаем отличное применение принципа «разделяй и властвуй» для разделения кода скрипта, от исполняющей этот код сессии.

Тип переменной

Особенность программирования скрипт–процессора заключается в том, что типы переменных скрипта неизвестны на момент компиляции программы, но наш язык программирования должен знать типы на этом этапе (вы ведь используете типизированный язык?).

С этим можно справиться несколькими способами. Первый - класс переменной хранит указатель на свое значение в виде:

void* value;

и свой тип. Тогда получение значения сводится к двум этапам:

  1. Узнать тип;
  2. Преобразовать value в соотв. тип.

Упрощенный пример:

enum ValueType { VT_BOOL, VT_INT, VT_FLOAT };
unsigned short ValueSize(ValueType type)
{
       switch (ValueType) {
              case VT_BOOL:
                     return sizeof(bool);
              case VT_INT:
                     return sizeof(int);              
              case VT_FLOAT:
                     return sizeof(float);
              default:
                     return 0;
       }
}

class Variable
{
public:
       Variable(const void* val, ValueType tpe)
              : value(0)
       {
              Set(val, tpe);
       }

       ~Variable() {
              free(value);
       }

       void Set(const void* val, ValueType tpe)
       {
              if (value)
                     free(value);

              unsigned short sz = ValueSize(type = tpe);
              value = new BYTE[sz];
              memcpy(value, val, sz);
       }

       inline ValueType GetType() const { return type; }
       inline const void* GetValue() const { return value; }

private:
       void* value;
       ValueType type;
};

template<class T>
void GetVariableValue(const Variable& var, T* outVal)
{
       T* tmp;
       if ( tmp = dynamic_cast<T*>(var.GetValue()) )
              *outVal = *tmp;
       else
              throw(IncorrectValueTypeException);
}

При таком способе хранения переменных скрипт получается жестко – типизированным, и любая попытка получить значение переменной, по типу отличное от настоящего типа даст нам исключение “IncorrectValueTypeException”.

Есть другой, более медленный, но и более лояльный к несоответствию типов метод.

Можно хранить переменные в их строковом представлении, а при запросе конвертировать строку в соответствующий тип (в C++ для этого можно воспользоваться функций sscanf, например). Таким образом, если в переменной сохранено значение “128”, то его можно прочесть как int (128), float (128.0f), string (“128”). Часто это вполне оправдано, действительно, Вы ведь можете написать:

int a = 20;
float b = a;

Кроме того, таким образом переменная может менять свой тип, в зависимости от использования, не меняя своего значения. Так сделано в нашем GODScript или MaxScript 3DMax’a, например.

Но все же, если Ваш скриптовый язык предполагает жестко-типизированные переменные – первый способ предпочтительнее. Просто вместо float b = a у Вас будет: float b = IntToFloat(a)

А можно также и определить функцию float, как явное (explicit) преобразование (произвольного) типа в float, как в C++. А если приложить еще немного мозговых усилий, вы добьетесь также и эффекта умных неявных (implicit) преобразований типа.

В общем – полный простор для творчества.

Пулы переменных и уровень доступа

Может оказаться очень удобным разделить разные переменные по различным областям их хранения – пулам переменных.

Варианты:

  • Глобальный пул переменных. Переменные, сохраненные здесь могут быть доступны любому скрипту (или программе) с момента определения до конца работы программы (если не будут удалены вручную)
  • Статический пул. Переменные из статического пула доступны с момента первого определения в для любой сессии исполнения одного скриптового кода, что может быть удобно для сохранения значений от одного вызова скрипта к другому, и также может использоваться в качестве общих переменных, например, при скриптовой рекурсии (как глубина рекурсии на данный момент или как-то еще). Принадлежит представлению кода скрипта.
  • Локальный пул. Принадлежит отдельной сессии исполнения скрипта (скрипт-клиенту), такие переменные доступны в любом месте скрипта, после определения в данной сессии его исполнения, даже, например:

    if (10 > 2) {
       local int a = 1000;
    }
    ShowMessage(string(a), “Debug”);
    

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

  • Пул секции. Переменные, определенные в этой секция (скорее всего, сюда по умолчанию попадают все переменные без спецификатора пула) будут доступны только в пределах одной секции, например, внутри тела цикла или тела if-else. Реализуется стеком.

В случае наличия, например, глобального пула стоит продумать интерфейс взаимодействия программы с классом пула переменных.

Реализация скрипт-процессора и проектирование подсистем
Парсер

Парсер – это лексический анализатор, выделяющий из сплошного кода скрипта как вектора (массива) символов отдельные лексемы. Т.е. из текста скрипта путем парсинга мы получим вектор лексем для последующей обработки компилятором скрипта.

Например, на входе:

2 + 2.0f == /*А это – комментарий*/ 4;

На выходе

Lexeme { value = "2",   lexType = LT_CONST_VARIABLE,  varType = VT_INT  }
Lexeme { value = "+",   lexType = LT_BINARY_OPERATOR, varType = VT_NONE }
Lexeme { value = "2.0f",lexType = LT_CONST_VARIABLE,  varType = VT_FLOAT}
Lexeme { value = "==",  lexType = LT_BINARY_OPERATOR, varType = VT_NONE }
Lexeme { value = "4",   lexType = LT_CONST_VARIABLE,  varType = VT_INT  }
Lexeme { value = ";",   lexType = LT_DIVIDER_OPERATOR,varType = VT_NONE }

Реализация парсера полностью зависит от синтаксиса выбранного Вами скриптового языка.

Существуют и готовые универсальные парсеры (если я не ошибаюсь, есть, например, в библиотеке boost для C++), которые по высокоуровневой спецификации синтаксиса Вашего языка, сами за Вас сгенерируют парсер (реализуется с использованием шаблонов).

Но Вы также можете написать свой собственный парсер, который, скорее всего, будет работать быстрее сгенерированного.

Проектирование выходит на первое место при разработке парсера, ибо Вам может захотеться добавить несколько операторов или языковых конструкций, не предусмотренных вначале или изменить существующие (именно из-за ошибок в проектировании мой парсер трудно назвать очень удачным).

Здесь, как всегда, действует принцип «Разделяй и властвуй». Вбивая каждый из возможных типов лексем прямо в код, Вы рискуете получить запутанный, нечитаемый, трудно отлаживаемый парсер с кучей ошибок.

За информацией по проектированию вообще обратитесь к списку рекомендуемой литературы в конце статьи. Я же советую Вам воспользоваться большим листом бумаги и карандашом для наглядного представления компонентов системы парсинга и их взаимодействий. Представьте, что хотите добавить новый тип лексем (например, новый оператор, спецификатор или управляющую конструкцию). Ваша задача – сделать это (на бумаге) не затронув (или почти не затронув) парсинг уже существующих лексем.

Компилятор

Итак, на выходе парсера мы получили список лексем. Пора подумать, как мы будем выполнять скрипт. Если у Вас нет вложенного использования функций и операторов, например, как:

ShowMessage( “Error detected: ” + string( GetLastErrorCode() ), “Error” );

и количество операндов каждой функции и оператора фиксировано, Вы можете обойтись и вовсе без какой-либо предварительной компиляции, так как тогда выполнение скрипта сводится к последовательному выполнению каждой активной лексемы (оператора или функции), после которой через разделители следуют операнды. Однако, это достаточно неудобно, так как для записи представленного выше скрипта Вам придется написать что-то такое:

string errorCode;
GetLastErrorCode();       // Вернет результат в стек
pop errorCode;
string errorMsg;
Sum(errorMsg, "Error detected!", errorCode);
ShowMessage(errorMsg, "Error");

Очень неудобно, особенно если учесть, что подобный код можно сгенерировать и автоматически (это мы рассмотрим чуть ниже).

Путем преобразования, мы можем привести такой первоначальный код к удобному для исполнения виду полиз (польская инверсная запись). Тогда, наш первоначальный код будет выглядеть так:

“Error detected” GetLastError() toString +  “Error” ShowMessage,

т.е. прямые запросы на выполнение идут в порядке убывания приоритета, именно так, как их и следует считать. Т.е. дойдя в выполнении до активной лексемы, мы уже вычислили все ее операнды и можем выполнять указанное действие. Подробнее о полиз можно прочесть здесь: http://algolist.ncstu.ru/syntax/revpn.php.

Однако разнообразие всевозможных операций (из-за необходимости учитывать количество операндов каждой из них, а ведь количество аргументов, скажем, скриптовой функции на данном этапе определить трудно) будет усложнять нам выполнение, к тому же, изменяется порядок следования лексем, что может дать осложнения для реализации управляющих конструкций, таких как операторы цикла или оператор goto.

Избавиться от проблем с необходимостью всегда помнить количество операндов каждого оператора или функции мы можем путем построения дерева скрипта, когда в узле стоит операция с наименьшим приоритетом, а в ветвях данного узла – поддеревья операндов.

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

Полученное количество будет знать данный узел дерева. Но это не избавляет нас от всех сложностей в проектировании и реализации скрипт-клиента, т.е. объекта, представляющего собой сессию выполнения скрипта (ведь ему необходимо уметь обрабатывать огромное количество высокоуровневых операций, причем список таких операций может сильно изменяться и дополняться).

Можно также генерировать бинарный код, последовательность «опкодов» элементарных операций, напоминающий код на ассемблере после замены каждой из операций на ее бинарный код (в .obj файле). Таким образом, мы избавляемся от необходимости уметь обрабатывать множество высокоуровневых операций на этапе исполнения, что значительно упрощает нам проектирование и реализацию скрипт-клиента.

Минусы этого метода в необходимости разработки своего низкоуровневого ассемблер-подобного языка.

Скрипт – клиент

Наиболее простая реализация скрипт – клиента будет в случае компиляции в список бинарных опкодов: тогда, при правильном проектировании мы получаем независимость реализации обработки одного опкода от остальных, и такая обработка не требует никакой лишней информации, кроме стека (куда отправляются все аргументы функций или временные переменные), пулов с переменными и списка своих операндов.

Выполнение такого кода сводится к последовательной обработке каждого из опкодов, переходя к соответствующим опкодам по операциям переходов (наподобие, jmp, je, jz и т.д. ассемблера x86).

Примерно так же выполняются и скипты как прямая последовательность инструкций, вида:

LoadMap "Map01.map"
Player–>SetHealth 200
InitPhysics
StartLevel

Только уровень сложности каждой операции (здесь представляемой одной строкой скрипта, в отличие от одного байт-кода (опкода) в предыдущем способе) выше.

В случае выполнения кода, представленного в виде дерева из-за высокого уровня операций в узлах дерева нам необходимо хорошенько спроектировать весь скрипт-процессор так, чтобы, например, при добавлении нового оператора циклов не пришлось бы производить серьезные изменения сначала в парсер, затем в компилятор, а позже и писать уникальный, нетривиальный обработчик цикла в коде скрипт-клиента. Само же выполнение кода сводится, тогда, к выполнению каждого узла дерева в концевом порядке обхода (сначала ветви узла, затем сам узел).

Выполнение скрипта в виде полиз похоже на выполнение дерева, ибо концевой обход дерева равносилен последовательному выполнению операций, записанных в порядке определения польской нотации слева-направо, однако сложность заключается в том, что чтобы узнать количество указанных в скрипте операндов для данной операции нам придется вычислять глубину последовательности операнд: операнды с одинаковой глубиной (на 1 меньшей глубины рассматриваемой операции) и образуют все множество операндов для данной операции.

Обработка ошибок

Ранее мы рассмотрели возможное устройство обработчика скриптов и этапы его создания/исполнения (Парсинг –> Компиляция –> Исполнение скриптовых сессий).

Но здесь мы не учитывали возможные ошибки на каждом из этапов.

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

Если учесть сложность и множество вариантов реализации каждой из подсистем, нужно хорошенько обдумать и реализовать обработку ошибок. Ну, во-первых, при проектировании деталей реализации каждой из подсистем желательно выписать список всех ошибок или исключительных ситуаций, которые могут возникнуть.

Затем мы должны спроектировать подсистемы так, чтобы определить ошибку на наиболее раннем этапе в подготовке/выполнении скрипта. Например, одну из ошибок компиляции – недостаточное количество закрывающихся скобок, можно без труда определить уже на этапе парсинга, ведь уже парсер знает все лексемы, и нам достаточно будет всего лишь ввести счетчик открывающихся/закрывающихся скобок (максимум – стека, при наличии различного вида скобочных конструкций, наподобие {}, () и []) для определения подобных ошибок уже на этом этапе.

Кроме того, если компилятор имеет доступ к информации о требуемых аргументах функций программы (а такой доступ точно будет в случае регистрации функций программы для скрипт-процессора), то не стоит даже и пытаться выполнить скрипт, передающий в функцию строку вместо float-числа – такую ошибку можно обнаружить при компиляции кода, вызывающего функцию.

В общем, попробуйте проанализировать весь список возможных ошибок сверху - вниз (исполнение – компиляция - парсинг) и подумайте, обработку каких из ошибок можно «спустить» ниже.

Далее, очевидно для независимости обработчика (уже перехваченных) ошибок от подсистем движка, стоит воспользоваться механизмом исключений. Иерархию классов исключений разработайте сами как удобно Вам и как принято вообще в Вашей программе.

Информация, содержащаяся в исключениях, тоже может быть различной, начиная от текстового сообщения, конкретизирующего ошибку и место ее появления, до исключений, содержащих всю информацию, которую обычно вы получаете при отладке программы (не скрипта) из отладчика вашей среды разработки.

Можно также предусмотреть разную «опасность» исключений – от советов (наподобие, переменная не используется) и warning’ов (возможен бесконечный цикл, например), до опасных (функция программы вернула код неудачного завершения) и критических (деление на нуль) ошибок.

Оптимизация

Хорошо было бы научить наш скрипт-процессор (а точнее его компилятор) производить некоторую оптимизацию скриптов. Тут важно не увлечься, помните, что Вы пишете не какой-нибудь там сверх-оптимизированный компилятор, а всего-навсего обработчик скриптов (стоит периодически обращаться к списку требований, которые Вы поставили перед своим скрипт-процессором).

Но вот мелкую оптимизацию, не требующую больших затрат реализовать стоит.

Например, если Вы выбрали представление кода в виде низкоуровневых инструкций, оптимизацию можно свести к нахождению «клеше» (т.е. подобных фрагментов кода, подвергающихся оптимизации) и их замене на заранее написанные оптимизированные версии.

В случае представления кода деревьями, можно, к примеру, без труда определять и вычислять константные выражения на этапе компиляции. Для этого достаточно отслеживать константные лексемы и в случае, если у какого-то узла-оператора лишь константные ветви – этот узел можно вычислить «на месте» и заменить его константной лексемой и продолжать процесс далее.

В общем, подумайте, все зависит от Вас.

Заключение

Что же мы получили, в конце концов?

Схема подготовки скриптового кода

Схема выполнения

Это лишь один из возможных вариантов, Ваша схема может существенно отличаться.

Список литературы

«Реализация скрипт-движка» перевод Дмитрия Гавриленко статьи автора Jan Niestadt.

«Написание интерпретатора скриптов на C++». Часть I (см. и последующие части)

«Введение в LUA». Перевод Александра Федотовских статьи автора Ash Matheson.

Польская инверсная запись.

«Язык программирования C++». Bjarne Stroustrup.

Рекомендую для ознакомления всем, даже и тем, кто не собирается использовать C++. В книге хорошо рассмотрены вопросы локального проектирования. Кроме того, автор описывает написание простого скриптового калькулятора (с возможностью работы с переменными) с парсером и аналогом компилятора, а также обработкой ошибок.

Кроме того, рассматривается использование RTTI для определения типа во время исполнения при проектировании слабосвязанных систем.

Как основная литература по проектированию (независимо от языка программирования):

«Объектно-ориентированный анализ и проектирование». Гради Буч.

«Приемы объектно-ориентированного проектирования. Паттерны проектирования». Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес.

Специально для www.mirgames.ru

Иванов “universal” Юрий

Тэги

Последние комментарии

Привет, Dan, сорри, что отвлекаю. Т.к. в чате не было десктоп-уведомлений, удалось собрать людей тут в Telegram-конфе. https://t.me/joinchat...
20.08.17 09:23, egslava
Адрес чата
Есть. XProger, Spose, MeF, HEX, я, еще кто-то был
18.08.17 12:01, Daddy
Адрес чата
В чате кроме тебя-то кто-то есть?
17.08.17 19:06, Vga
Normal Mapping visualization
@egslava, не там проверяешь, проверять надо в городе Ф.
19.04.17 19:42, nbaksalyar
Normal Mapping visualization
@Said, ну, я проверил, чот не особо.
19.04.17 17:59, egslava
Normal Mapping visualization
В оффлайне. Там тоже есть жизнь.
17.04.17 07:51, Said
Normal Mapping visualization
@Dron
16.04.17 14:07, egslava
Normal Mapping visualization
А-то! ;-) Ты лучше скажи, куда вы все подевались и где жизнь теперь? :-)
15.04.17 20:18, egslava
Normal Mapping visualization
На мирге еще теплится жизнь 0_о
14.04.17 12:15, DRON
История mirgames в лицах #2.
Ох! Spose, XProger, спасибо большое! Просто невероятное удовольствие от прочтения! Блин, а ведь я всё время думал, что XProger не играл особ...
05.02.17 02:21, egslava