1.9 Добавляем поддержку скриптов (scripting)
Содержание
- 1.9 Добавляем поддержку скриптов (scripting)
- Intro. Преимущества скриптов
- Система скриптов
- Строение скрипта (script composition)
- Добавляем Scripting.h (Проект Engine)
- Исследуем Scripting.h
- Добавляем Scripting.cpp (Проект Engine)
- Исследуем Scripting.cpp
- Интегрируем систему скриптов в движок
- Тестовая перекомпиляция Engine.lib
- Применение системы скриптов
- Исходные коды
- Итоги
- Источники
Intro. Преимущества скриптов
Мы начнём с определения, что же собой представляет скрипт.1 К этой главе ты уже хорошо знаком со стандартным кодом языка C++, и если посмотришь на написанный ранее код, ты отметишь для себя что, помимо всего прочего, он в основном состоит из двух фундаментальных аспектов: переменных и команд для их обработки. Это означает, что твой код использует команды, чтобы вызывать различные события (events) в компиляторе с целью заставить его работать определённым образом. Для достижения этой цели ты также используешь в коде переменные, которые затем передаются в различные функции и участвуют в расчётах (в зависимости от того, какая команда была вызвана в компиляторе).Взгляни на следующий псевдокод:
myVariable=5 if myVariable > 5 then Call CommandA else // (то есть, если myVariable меньше или равно 5) Call CommandB
А сейчас, не вдаваясь в детали работы компилятора, представим что данный псевдо-код по сути является скриптом для компилятора. Компилятор считывает его строка за строкой и использует для вызова соответствующих функций операционной системы и любых других систем, с которыми ты работаешь в данный момент. myVariable - это не более чем обычная переменная, которую ты используешь для своих расчётов. Весь функционал скрипта заключён в работе двух функций: CommandA и CommandB. Взяв за основу эту модель, ты можешь создать свою собственную систему скриптов (scrypting system), которая позволит использовать переменные и команды для работы с ними. На деле переменные могут даже генерироваться этой системой. Это означает, что ты получаешь доступ к переменным через действительный код движка/игры, можешь использовать переменные скрипта и даже изменять их в реальном времени!
Что касается команд, они будут ссылаться на различные функции в коде движка/игры. Например, у тебя есть команда в скрипте, которая говорит игре "вызвать взрыв". Команда может выглядеть так:
CreateEffect_Explosion( location )
Когда система скриптов считывает эту команду, она информирует игру о необходимости вызвать необходимые функции для создания эффекта взрыва в данном месте (например, по данным 3D-координатам). Всё это звучит круто, но у скриптов есть один существенный недостаток - они сравнительно медленно работают, особенно если неверно составлены. Больше всего замедление заетно при обработке скриптов в реальном времени. То есть система скриптов (а вместе с ней и вся игра) замедляется до той скорости, на которой может быть прочитан скрипт. Это похоже на то, когда ты читаешь кигу вслух. Если ты читаешь не вслух (не проговаривая написанный текст), то замечаешь, что чтение при этом происходит намного быстрее, чем если бы ты проговаривал каждое прочитанное слово. Причина этого заключается в том, что твой мозг способен обрабатывать текст намного быстрее, чем ты можешь говорить. Со скриптовыми движками та же история. В то время как команды и внутренние структуры системы скриптов могут выполняться очень быстро, операции файлового ввода/вывода (IO) существенно замедляют процесс, образуя так называемое "бутылочное горлышко". К счастью, нам не понадобится такое полнофункциональное решение.
В Главе 1.1 вкратце упоминалось, что мы будем использовать упрощённую систему скриптов, которую самостоятельно разработаем. Мы уже кратко рассмотрели использование скриптов свойств (property scrypt). Теперь изучим их более подробно. Если вспомнить, как мы определяли обычный скрипт (normal scrypt) и убрать из него раздел команд, то в результате получится скрипт свойств. Другими словами, скрипт свойств содержит только список переменных с присвоенными им значениями. Игра имеет к скрипту свойств полный доступ и использует его для самых разных целей. Скрипты свойств часто используются для определения различных объектов в игровом мире (= сцене). Например, у тебя есть объект оружие, обладающий такими свойствами, как скорострельность (кол-во выстрелов в минуту), дальность стрельбы и наносимый урон. В этом случае ты можешь быстро организовать скрипт свойств (для каждого вида оружия), который содержит 3 переменные и соответствующие значения этих переменных, которые будет использовать система скриптинга игры:
rof = 120 range = 200 damage = 50
Какие именно параметры будут представлены в скрипте свойств, зависит от самой игры. Но сам принцип понятен. Например, rof может означать число выстрелов в минуту, а range - дистанцию выстрела в метрах, футах, ярдах или в любых других единицах измерения, используемых в игре. Также нам необходимо позаботиться о типе данных, в которых будут сохраняться переменные. В вышеприведённом примере у нас есть 3 значения, которые, очевидно, могут быть представлены как целочисленный тип данных, например int или long или даже unsigned char (подразумевая, что значения никогда не выйдут за пределы присвоенного им типа данных). Но что произойдёт, если мы захотим сохранить букву, слово или целое предложение? Или, к примеру, значение с плавающей точкой (floating point value) или булево (Boolean) значение (true или false)? Как насчёт комплексных типов данных, как например информация о цвете или 3D-координаты? Это не проблема, так как мы обязательно учтём все эти случаи при создании нашей системы скриптов.
Вообще, нам не стоит беспокоится о снижении производительности при использовании системы скриптов в реальном времени, так как наша система скриптов будет редко использоваться в реальном времени. Подумай, в каких местах скрипт свойств будет использоваться наиболее часто. Скорее всего это будут участки кода, где определяются свойства игровых объектов. Все они, как правило, загружаются единовременно (один раз) при запуске приложения. После этого система располагает этими данными, загруженными в оперативную память, и ей не нужно повторно считывать их из скрипта свойств. Таким образом, в свете того, что скрипт свойств будет целиком считываться в ОЗУ (= оперативное запоминающее устройство) в момент запуска, нам редко придётся обращаться к нему повторно в реальном времени, во время выполнения критически важного кода .
Система скриптов
Мы уже разобрались в том, что собой представляет скрипт, и даже определили, какой именно вид скриптов будем использовать (скрипт свойств). Но, до настоящего момента, упустили одну важную деталь, не определив, как именно будут обрабатываться скрипты. Мы можем просто открыть текстовый редактор, выставить некоторые значения, сохранить файл и ожидать, что наша игра чудесным образом будет знать, как его считывать. Нам необходимо разработать пригодную к повторному применению (reusable), автоматизированную систему, которую сможем вызывать для чтения любого скрипта (их может быть несколько) и передачи нужных значений в нужное время (см. Рис. 1). Видно, что наша система скриптов будет "венчать" систему менеджмента ресурсов. А всё потому, что скрипты - это те же самые ресурсы (более узко специализированные). В Главе 1.1 мы определяли понятие "ресурс" как нечто (вообще, это обычный файл), которое хранится на жёстком диске компьютера, имеет имя файла (filename) и путь (filepath). Ресурс может быть загружен и сохранён во временной памяти в период запуска игрового приложения. В будущем ты также увидишь, что наша система скриптов состоит из 2 компонентов: скриптов и переменных. Хотя, фактически система напрямую манипулирует только скриптами. Скрипты же, в свою очередь, манипулируют своими внутренними переменными. Поэтому для продолжения работы нам необходимо чётко выделить 2 этих компонента (или объекта) системы скриптов. Продолжив рассуждения и применив объектно-ориентированный подход, мы сразу предположим, что эти объекты будут двумя отдельными классами, которые нужно будет создать. Напомним, что наша система менеджмента ресурсов уже на месте и мы обеспечим код для загрузки и обработки скриптов, они будут прекрасно работать в качестве общих ресурсов, избавляя нас от необходимости создавать специализированный класс для управления (менеджмента) скриптами. Мы разберём эту тему более детально чуть позднее, сразу после того, как совершим небольшой экскурс по двум важнейшим темам объектно-ориентированного программирования, рассмотрев инкапсуляцию и автоматизацию.
Если присмотреться к рабочему процессу (Рис. 1), то можно увидеть, что сначала скрипт загружается системой скриптов, которая сама является экземпляром (инстансом) менеджера ресурсов, построенного с использованием шаблонов (templates). Класс скрипта будет вызван для загрузки скрипта и всех его внутренних переменных, которые, в свою очередь, станут переменными класса переменных. Сразу после загрузки скрипта, игра может запросить значение любой из его переменных. С того момента, как скрипт был загружен, его значения доступны напрямую из временной памяти. Это даёт многократный прирост в скорости, по сравнению со считыванием данных с физического носителя через систему стандартного файлового ввода/вывода, которая, как известно, является "бутылочным горлышком" во время выполнении критически важного кода.
Мы также упомянули идею о том, чтобы сохранять в переменных различные типы значений (различных типов данных и даже комплексные типы данных). Для достижения этой цели мы присвоим каждой переменной специальный идентификатор типа (type identifier), который позволит идентифицировать тип данных, хранимых в той или иной переменной, чтобы таким образом точно знать как её загружать и обрабатывать. Очевидно, что необходимо обеспечить реализацию для каждого типа данных, но эта задача предельно проста.
Раз мы собираемся запускать скрипты через систему менеджмента ресурсов, нам не нужно беспокоиться, что происходит с памятью. Даже если ты загрузишь сотню скриптов, со множественными повторными загрузками (например, один и тот же скрипт будет загружен дважды), и затем упустишь их из виду, забыв закрыть половину из них, тебе не о чем волноваться. Менеджер ресурсов необычайно "снисходителен" и эффективно исправит все эти "промахи". Но для этого, повторимся, ты должен загружать все скрипты только через менеджер ресурсов. Если ты загружаешь их сам, независимо от менеджера ресурсов, то должен самостоятельно вовремя уничтожить их. Здесь мы немного забегаем вперёд. Ведь мы пока не реализовали систему скриптов. Сперва посмотрим на то, как наши скрипты будут составлены (composed).
Строение скрипта (script composition)
Кратко рассмотрим внутреннее строение (структуру) простейшего скрипта, который наша система сможет считывать. Текст скрипта вводится в обычный текстовый файл, который затем сохраняется и считывается системой скриптов движка. Точнее, система считывает переменные с присвоенными им значениями. Программер может затем получить доступ к этим переменным в коде, во время работы игрового приложения, и запросить их значения. Кроме этого, мы хотим иметь возможность оставлять комментарии (нет, не те, что в соцсетях) в наших скриптах, обеспечивая таким образом дополнительное описание переменных (при необходимости). Для достижения этой цели мы применим блоки переменных (variable blocks). Блок переменных - это обычная группа переменных, которую мы ограничим (выделим) как-нибудь, чтобы всё, что находится за её пределами игнорировалось системой. Мы определим границы блока переменных с помощью двух выражений: #begin и #end :Этот текст находится за пределами блока переменных и поэтому будет игнорироваться. #begin Этот текст находится внутри блока переменных и поэтому будет считан системой. #end
Это позволит нам размещать несколько блоков внутри одного скрипта, что сделает длинные скрипты более читабельными. Переменные при этом будут логически сгруппированы в блоки и снабжены необходимыми комментариями. Единственное ограничение - блок не может размещаться внутри другого блока (быть вложенным, ветвящимся и т.д.) и каждый блок должен открываться выражением #begin и закрываться выражением #end. Как в этом примере:
Первый блок. #begin Какие-то данные внутри этого блока. #end Второй блок. #begin Какие-то данные внутри этого блока. #end
Другой важный аспект - это переменные, которые будут размещаться внутри блоков. Каждая переменная описывается строго определённым образом (форматом), где сначала идёт имя (name) переменной, затем её тип (type) и присвоенное значение (value):
Первый блок. #begin name type value #end
Этой информации должно быть достаточно для начала работы. Позднее, когда мы дойдём до применения системы скриптов, мы разберём другие важные подробности. А сейчас перейдём к практической части.
Однажды реализованный и внедрёный в движок класс Script будет автоматически выполнять интерпретацию и загрузку данных из скриптов, не требуя вмешательства со стороны программера.
Сейчас в Проекте Engine всего 9 файлов: Engine.h, Engine.cpp, LinkedList.h, ResourceManagement.h, Geometry.h, State.h, State.cpp, Input.h и Input.cpp, которые мы создали в предыдущих главах.
Добавляем Scripting.h (Проект Engine)
Заголовочный файл Scripting.h будет содержать объявление всего двух классов: Variable (для работы с переменными различных типов) и Script (для работы с внешними файлами скриптов).ОК, приступаем.
- Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
- В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
- Во всплывающем меню Добавить->Создать элемент... (Add->New Item...)
- В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "Scripting.h".
Добавленный файл сразу откроется в правой части MSVC++2010.
- В только что созданном и открытом файле Scripting.h набираем следующий код:
//----------------------------------------------------------------------------- // File: Scripting.h // A simple scripting system. The scripts are nothing more than a collection of // variables stored in a text file. // Простая система скриптов. Скрипт - это обычный текстовый файл // со списком переменных. // // Programming a Multiplayer First Person Shooter in DirectX // Copyright (c) 2004 Vaughan Young //----------------------------------------------------------------------------- #ifndef SCRIPTING_H #define SCRIPTING_H //----------------------------------------------------------------------------- // Variable Type Enumeration // Перечисление поддерживаемых типов переменных //----------------------------------------------------------------------------- enum{ VARIABLE_BOOL, VARIABLE_COLOUR, VARIABLE_FLOAT, VARIABLE_NUMBER, VARIABLE_STRING, VARIABLE_VECTOR, VARIABLE_UNKNOWN }; //----------------------------------------------------------------------------- // Variable Class //----------------------------------------------------------------------------- class Variable { public: Variable( char *name, FILE *file ); Variable( char *name, char type, void *value ); virtual ~Variable(); char GetType(); char *GetName(); void *GetData(); private: char m_type; // Type of data stored in the variable. char *m_name; // Name of the variable. void *m_data; // Data stored in the variable. }; //----------------------------------------------------------------------------- // Script Class //----------------------------------------------------------------------------- class Script : public Resource< Script > { public: Script( char *name, char *path = "./" ); virtual ~Script(); void AddVariable( char *name, char type, void *value ); void SetVariable( char *name, void *value ); void SaveScript( char *filename = NULL ); bool *GetBoolData( char *variable ); D3DCOLORVALUE *GetColourData( char *variable ); float *GetFloatData( char *variable ); long *GetNumberData( char *variable ); char *GetStringData( char *variable ); D3DXVECTOR3 *GetVectorData( char *variable ); void *GetUnknownData( char *variable ); private: LinkedList< Variable > *m_variables; // Linked list of variables in the script. // Связный список переменных в скрипте. }; #endif
- Сохрани Решение (Файл->Сохранить все).
Исследуем Scripting.h
В начале листинга видим перечисление (enumeration):... //----------------------------------------------------------------------------- // Variable Type Enumeration // Перечисление поддерживаемых типов переменных //----------------------------------------------------------------------------- enum{ VARIABLE_BOOL, VARIABLE_COLOUR, VARIABLE_FLOAT, VARIABLE_NUMBER, VARIABLE_STRING, VARIABLE_VECTOR, VARIABLE_UNKNOWN }; ...
Выражение enum - это всего лишь простой способ перечислить набор констант, выстроенных последовательно, одна за другой. В С++ суть перечисления заключается в том, что каждому его элементу обязательно присваивается значение, соответствующее его порядковому номеру в списке (начиная с 0). Если значение начинается не с нуля, (как в этом примере:)
enum{CAT=47, DOG, BIRD, FISH};
...значение каждого следующего элемента будет увеличиваться на единицу. Например, элемент DOG будет иметь значение 48, элементу BIRD автоматически будет присвоено значение 49 и так далее. Если ни одному элементу не присвоено никакое значение, перечисление (enumeration) автоматически сделает это, присвоив первому элементу значение 0, второму 1, третьему 2 и так далее. В нашем случае элемент VARIABLE_UNKNOWN стоит седьмым по порядку. Поэтому ему будет присвоено значение 6.
... //----------------------------------------------------------------------------- // Variable Class //----------------------------------------------------------------------------- class Variable { public: Variable( char *name, FILE *file ); Variable( char *name, char type, void *value ); virtual ~Variable(); char GetType(); char *GetName(); void *GetData(); private: char m_type; // Type of data stored in the variable. char *m_name; // Name of the variable. void *m_data; // Data stored in the variable. }; ...
Здесь, в принципе, ничего сверхъестественного, за исключением наличия двух конструкторов (с разным количеством параметров) и странного указателя FILE *file в первом конструкторе. *file является указателем на экземпляр структуры, которая содержит поток файловых данных (input/output stream; подробнее по этой теме читаем здесь: https://purecodecpp.com/archives/2724а1) для открытого файла. Файл должен быть текстовым документом с расширением *.txt , в котором хранятся переменные скрипта с присвоенными им значениями. Очевидно, что данный (первый) конструктор вызывается в том случае, когда ты создаёшь (вернее, извлекаешь) скриптовые переменные из текстового файла скрипта. Параметр char *name представляет собой имя создаваемой переменной.
Второй конструктор применяется, когда файл скрипта создаётся "с нуля" (другими словами, когда он не существует). Два экстра-параметра используются в этом конструкторе для указания типа переменной и соответствующего ей значения. Очень важно, чтобы значение переменной имело правильный тип. Это означает, что если переменная, например, имеет тип string (строка), то в этом конструкторе необходимо передать параметр string. Позднее ты увидишь множество примеров его применения.
После деструктора видим 3 функции + 3 соответствующих переменных члена класса, которые хранят тип (*m_type), имя (*m_name) и данные (*m_data) переменной. Данные функции применяются для доступа к любой из этих переменных при необходимости. Например, когда необходимо узнать тип определённой переменной, достаточно вызвать функцию GetType(), которая затем вернёт один из типов, указанных в перечислении, рассмотренном ранее. Скорее всего самой часто используемой функцией будет GetData(), которая возвращает указатель void, содержащий адрес, по которому хранятся данные переменной. Указатель затем должен быть транслирован в один из трёх используемых указателей, что производится на основании типа запрошенной переменной. Весь процесс подробно описан в Scripting.cpp, созданием которого мы займёмся прямо сейчас.
Добавляем Scripting.cpp (Проект Engine)
В файле исходного кода Scripting.cpp будут размещаться реализации функций, объявленных в Scripting.h.- В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
- Во всплывающем меню Добавить->Создать элемент...
- В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи "Scripting.cpp".
- Жмём "Добавить".
- В только что созданном и открытом файле Scripting.cpp набираем следующий код:
//----------------------------------------------------------------------------- // File: Scripting.cpp // Scripting.h implementation. // Реализация функций, объявленных в Scripting.h . // Refer to the Scripting.h interface for more details. // // Programming a Multiplayer First Person Shooter in DirectX // Copyright (c) 2004 Vaughan Young //----------------------------------------------------------------------------- #include "Engine.h" //----------------------------------------------------------------------------- // The variable class constructor. //----------------------------------------------------------------------------- Variable::Variable( char *name, FILE *file ) { // Store the name of the variable // Сохраняем имя переменной m_name = new char[strlen( name ) + 1]; strcpy( m_name, name ); // Ensure the file pointer is valid. // Проверяем корректность указателя файла if( file == NULL ) return; // Read the variable's type // Читаем тип переменной char buffer[MAX_PATH]; fscanf( file, "%s", buffer ); if( strcmp( buffer, "bool" ) == 0 ) { // The variable is a boolean // Тип перемнной - BOOL m_type = VARIABLE_BOOL; // Read and set the bool for the variable. // Читаем и устанавливаем переменную типа BOOL bool value; fscanf( file, "%s", buffer ); if( strcmp( buffer, "true" ) == 0 ) value = true; else value = false; m_data = new bool; memcpy( m_data, &value, sizeof( bool ) ); } else if( strcmp( buffer, "colour" ) == 0 ) { // The variable is a colour. // Тип переменной - цветовое значение. m_type = VARIABLE_COLOUR; // Read and set the colour for the variable. // Читаем и устанавливаем переменную типа COLOUR D3DCOLORVALUE colour; fscanf( file, "%s", buffer ); colour.r = (float)atof( buffer ); fscanf( file, "%s", buffer ); colour.g = (float)atof( buffer ); fscanf( file, "%s", buffer ); colour.b = (float)atof( buffer ); fscanf( file, "%s", buffer ); colour.a = (float)atof( buffer ); m_data = new D3DCOLORVALUE; memcpy( m_data, &colour, sizeof( D3DCOLORVALUE ) ); } else if( strcmp( buffer, "float" ) == 0 ) { // The variable is a float. // Тип переменной - число с плавающей точкой. m_type = VARIABLE_FLOAT; // Read and set the float for the variable. // Читаем и устанавливаем переменную типа FLOAT float value; fscanf( file, "%s", buffer ); value = (float)atof( buffer ); m_data = new float; memcpy( m_data, &value, sizeof( float ) ); } else if( strcmp( buffer, "number" ) == 0 ) { // The variable is a number. // Тип переменной - число. m_type = VARIABLE_NUMBER; // Read and set the number for the variable. // Читаем и устанавливаем числовую переменную. long value; fscanf( file, "%s", buffer ); value = atol( buffer ); m_data = new long; memcpy( m_data, &value, sizeof( long ) ); } else if( strcmp( buffer, "string" ) == 0 ) { // The variable is a string. // Тип переменной - строка. m_type = VARIABLE_STRING; // Find the opening inverted commas. // Ищем символы обратного слэша (\) bool commasFound = false; ZeroMemory( buffer, MAX_PATH * sizeof( char ) ); fscanf( file, "%c", buffer ); while( true ) { if( strcmp( buffer, "\"" ) == 0 ) { commasFound = true; break; } if( strcmp( buffer, " " ) != 0 ) { fpos_t pos; fgetpos( file, &pos ); fsetpos( file, &--pos ); break; } fscanf( file, "%c", buffer ); } // Read and set the string for the variable. // Читаем и устанавливаем строковую переменную. char completeString[MAX_PATH]; ZeroMemory( completeString, MAX_PATH * sizeof( char ) ); bool addSpacing = false; do { fscanf( file, "%s", buffer ); if( strcmp( &buffer[strlen( buffer ) - 1], "\"" ) == 0 ) { buffer[strlen( buffer ) - 1] = 0; commasFound = false; } if( addSpacing == false ) addSpacing = true; else strcat( completeString, " " ); strcat( completeString, buffer ); } while( commasFound == true ); m_data = new char[strlen( completeString ) + 1]; strcpy( (char*)m_data, completeString ); } else if( strcmp( buffer, "vector" ) == 0 ) { // The variable is a vector. // Тип переменной - вектор. m_type = VARIABLE_VECTOR; // Read and set the vector for the variable. // Читаем и устанавливаем переменную типа vector. D3DXVECTOR3 vector; fscanf( file, "%s", buffer ); vector.x = (float)atof( buffer ); fscanf( file, "%s", buffer ); vector.y = (float)atof( buffer ); fscanf( file, "%s", buffer ); vector.z = (float)atof( buffer ); m_data = new D3DXVECTOR3; memcpy( m_data, &vector, sizeof( D3DXVECTOR3 ) ); } else { // The variable has an unknown type. // Тип переменной неизвестен. m_type = VARIABLE_UNKNOWN; // Read and set the data (same as a string) for the variable. // Читаем и устанавливаем переменную неизвестного типа // (аналогично строковой переменной). fscanf( file, "%s", buffer ); m_data = new char[strlen( buffer ) + 1]; strcpy( (char*)m_data, buffer ); } } //----------------------------------------------------------------------------- // The variable class constructor. //----------------------------------------------------------------------------- Variable::Variable( char *name, char type, void *value ) { // Store the name of the variable. // Сохраняем имя переменной m_name = new char[strlen( name ) + 1]; strcpy( m_name, name ); // Store the type of the variable. // Сохраняем тип переменной. m_type = type; // Set the variable's data based on its type. // Сохраняем данные переменной, основываясь на её типе. switch( m_type ) { case VARIABLE_BOOL: m_data = new bool; memcpy( m_data, (bool*)value, sizeof( bool ) ); return; case VARIABLE_COLOUR: m_data = new D3DCOLORVALUE; memcpy( m_data, (D3DCOLORVALUE*)value, sizeof( D3DCOLORVALUE ) ); return; case VARIABLE_FLOAT: m_data = new float; memcpy( m_data, (float*)value, sizeof( float ) ); return; case VARIABLE_NUMBER: m_data = new long; memcpy( m_data, (long*)value, sizeof( long ) ); return; case VARIABLE_STRING: m_data = new char[strlen( (char*)value ) + 1]; strcpy( (char*)m_data, (char*)value ); return; case VARIABLE_VECTOR: m_data = new D3DXVECTOR3; memcpy( m_data, (D3DXVECTOR3*)value, sizeof( D3DXVECTOR3 ) ); return; default: m_data = new char[strlen( (char*)value ) + 1]; strcpy( (char*)m_data, (char*)value ); return; } } //----------------------------------------------------------------------------- // The variable class destructor. //----------------------------------------------------------------------------- Variable::~Variable() { SAFE_DELETE_ARRAY( m_name ); SAFE_DELETE( m_data ); } //----------------------------------------------------------------------------- // Returns the type of the variable. // Возвращает тип переменной. //----------------------------------------------------------------------------- char Variable::GetType() { return m_type; } //----------------------------------------------------------------------------- // Returns the name of the variable. // Возвращает имя переменной. //----------------------------------------------------------------------------- char *Variable::GetName() { return m_name; } //----------------------------------------------------------------------------- // Returns the data in the variable. // Возвращает данные переменной. //----------------------------------------------------------------------------- void *Variable::GetData() { switch( m_type ) { case VARIABLE_BOOL: return (bool*)m_data; case VARIABLE_COLOUR: return (D3DCOLORVALUE*)m_data; case VARIABLE_FLOAT: return (float*)m_data; case VARIABLE_NUMBER: return (long*)m_data; case VARIABLE_STRING: return (char*)m_data; case VARIABLE_VECTOR: return (D3DXVECTOR3*)m_data; default: return m_data; } } //----------------------------------------------------------------------------- // The script class constructor. //----------------------------------------------------------------------------- Script::Script( char *name, char *path ) : Resource< Script >( name, path ) { // Create the linked list that will store all of the script's variables. // Создаём связный список, хранящий все переменные скрипта. m_variables = new LinkedList< Variable >; // Open the script file using the filename. // Открываем файл скрипта по имени его файла. FILE *file = NULL; if( ( file = fopen( GetFilename(), "r" ) ) == NULL ) return; // Continue reading from the file until the eof is reached. // Продолжаем чтение файла до тех пор, пока не достигнем // спецметки eof (end of file). bool read = false; char buffer[MAX_PATH]; fscanf( file, "%s", buffer ); while( feof( file ) == 0 ) { // Check if the file position indicator is between a #begin and #end // statement. If so then read the data into the variable linked list. // Проверяем, индикатор положения между тегами #begin и #end. // Если да, то считываем данные в связный список переменных. if( read == true ) { // Stop reading data if an #end statement has been reached. // Останавливаем чтение данных при достижении тега #end. if( strcmp( buffer, "#end" ) == 0 ) read = false; else m_variables->Add( new Variable( buffer, file ) ); } else if( strcmp( buffer, "#begin" ) == 0 ) read = true; // Read the next string. // Читаем следующую строку. fscanf( file, "%s", buffer ); } // Close the script file. // Закрываем файл скрипта. fclose( file ); } //----------------------------------------------------------------------------- // The script class destructor. //----------------------------------------------------------------------------- Script::~Script() { SAFE_DELETE( m_variables ); } //----------------------------------------------------------------------------- // Adds a new variable to the script. // Добавляет новую переменную в скрипт. //----------------------------------------------------------------------------- void Script::AddVariable( char *name, char type, void *value ) { m_variables->Add( new Variable( name, type, value ) ); } //----------------------------------------------------------------------------- // Sets the value of an existing variable in the script. // Устанавливает значение существующей переменной скрипта. //----------------------------------------------------------------------------- void Script::SetVariable( char *name, void *value ) { // Find the variable. // Находим переменную. Variable *variable = NULL; m_variables->Iterate( true ); while( m_variables->Iterate() != NULL ) { if( strcmp( m_variables->GetCurrent()->GetName(), name ) == 0 ) { variable = m_variables->GetCurrent(); break; } } // Ensure the variable was found. // Проверяем, найдена ли переменная. if( variable == NULL ) return; // Get the variable's type. // Получаем тип переменной. char type = variable->GetType(); // Destroy the variable. // Уничтожаем переменную. m_variables->Remove( &variable ); // Add the variable back in with the new value. // Добавляем в скрипт переменную с новым значением. AddVariable( name, type, value ); } //----------------------------------------------------------------------------- // Saves the script to file. // Сохраняет файл скрипта. //----------------------------------------------------------------------------- void Script::SaveScript( char *filename ) { FILE *file = NULL; char output[MAX_PATH]; // Open the given filename if available, otherwise the internal filename. // Открываем данное имя файла (если есть). // В противном случае назначаем своё имя. if( filename != NULL ) { if( ( file = fopen( filename, "w" ) ) == NULL ) return; } else { if( ( file = fopen( GetFilename(), "w" ) ) == NULL ) return; } // Write the #begin statement to the file. // Записываем в файл тег #begin. fputs( "#begin\n", file ); // Write each variable to the file. // Записываем каждую переменную связного списка в файл. m_variables->Iterate( true ); while( m_variables->Iterate() != NULL ) { switch( m_variables->GetCurrent()->GetType() ) { case VARIABLE_BOOL: if( *((bool*)m_variables->GetCurrent()->GetData()) == true ) sprintf( output, "%s bool true", m_variables->GetCurrent()->GetName() ); else sprintf( output, "%s bool false", m_variables->GetCurrent()->GetName() ); fputs( output, file ); fputs( "\n", file ); continue; case VARIABLE_COLOUR: sprintf( output, "%s colour %f %f %f %f", m_variables->GetCurrent()->GetName(), ( (D3DCOLORVALUE*)m_variables->GetCurrent()->GetData() )->r, ( (D3DCOLORVALUE*)m_variables->GetCurrent()->GetData() )->g, ( (D3DCOLORVALUE*)m_variables->GetCurrent()->GetData() )->b, ( (D3DCOLORVALUE*)m_variables->GetCurrent()->GetData() )->a ); fputs( output, file ); fputs( "\n", file ); continue; case VARIABLE_FLOAT: sprintf( output, "%s float %f", m_variables->GetCurrent()->GetName(), *(float*)m_variables->GetCurrent()->GetData() ); fputs( output, file ); fputs( "\n", file ); continue; case VARIABLE_NUMBER: sprintf( output, "%s number %d", m_variables->GetCurrent()->GetName(), *(long*)m_variables->GetCurrent()->GetData() ); fputs( output, file ); fputs( "\n", file ); continue; case VARIABLE_STRING: sprintf( output, "%s string \"%s\"", m_variables->GetCurrent()->GetName(), (char*)m_variables->GetCurrent()->GetData() ); fputs( output, file ); fputs( "\n", file ); continue; case VARIABLE_VECTOR: sprintf( output, "%s vector %f %f %f", m_variables->GetCurrent()->GetName(), ( (D3DXVECTOR3*)m_variables->GetCurrent()->GetData() )->x, ( (D3DXVECTOR3*)m_variables->GetCurrent()->GetData() )->y, ( (D3DXVECTOR3*)m_variables->GetCurrent()->GetData() )->z ); fputs( output, file ); fputs( "\n", file ); continue; default: sprintf( output, "%s unknown %s", m_variables->GetCurrent()->GetName(), (char*)m_variables->GetCurrent()->GetData() ); fputs( output, file ); fputs( "\n", file ); continue; } } // Write the #end statement to the file. // Записываем в файл тег #end. fputs( "#end", file ); // Close the script file. // Закрываем файл скрипта. fclose( file ); } //----------------------------------------------------------------------------- // Returns boolean data from the named variable. // Возвращает данные типа BOOL по имени переменной. //----------------------------------------------------------------------------- bool *Script::GetBoolData( char *variable ) { m_variables->Iterate( true ); while( m_variables->Iterate() != NULL ) if( strcmp( m_variables->GetCurrent()->GetName(), variable ) == 0 ) return (bool*)m_variables->GetCurrent()->GetData(); return NULL; } //----------------------------------------------------------------------------- // Returns colour data from the named variable. // Возвращает данные типа COLOUR по имени переменной. //----------------------------------------------------------------------------- D3DCOLORVALUE *Script::GetColourData( char *variable ) { m_variables->Iterate( true ); while( m_variables->Iterate() != NULL ) if( strcmp( m_variables->GetCurrent()->GetName(), variable ) == 0 ) return (D3DCOLORVALUE*)m_variables->GetCurrent()->GetData(); return NULL; } //----------------------------------------------------------------------------- // Returns float data from the named variable. // Возвращает данные типа FLOAT по имени переменной. //----------------------------------------------------------------------------- float *Script::GetFloatData( char *variable ) { m_variables->Iterate( true ); while( m_variables->Iterate() != NULL ) if( strcmp( m_variables->GetCurrent()->GetName(), variable ) == 0 ) return (float*)m_variables->GetCurrent()->GetData(); return NULL; } //----------------------------------------------------------------------------- // Returns number data from the named variable. // Возвращает данные числового типа по имени переменной. //----------------------------------------------------------------------------- long *Script::GetNumberData( char *variable ) { m_variables->Iterate( true ); while( m_variables->Iterate() != NULL ) if( strcmp( m_variables->GetCurrent()->GetName(), variable ) == 0 ) return (long*)m_variables->GetCurrent()->GetData(); return NULL; } //----------------------------------------------------------------------------- // Returns string data from the named variable. // Возвращает данные строкового типа по имени переменной. //----------------------------------------------------------------------------- char *Script::GetStringData( char *variable ) { m_variables->Iterate( true ); while( m_variables->Iterate() != NULL ) if( strcmp( m_variables->GetCurrent()->GetName(), variable ) == 0 ) return (char*)m_variables->GetCurrent()->GetData(); return NULL; } //----------------------------------------------------------------------------- // Returns vector data from the named variable. // Возвращает данные типа Vector по имени переменной. //----------------------------------------------------------------------------- D3DXVECTOR3 *Script::GetVectorData( char *variable ) { m_variables->Iterate( true ); while( m_variables->Iterate() != NULL ) if( strcmp( m_variables->GetCurrent()->GetName(), variable ) == 0 ) return (D3DXVECTOR3*)m_variables->GetCurrent()->GetData(); return NULL; } //----------------------------------------------------------------------------- // Returns unknown data from the named variable. // Возвращает данные неизвестного типа по имени переменной. //----------------------------------------------------------------------------- void *Script::GetUnknownData( char *variable ) { m_variables->Iterate( true ); while( m_variables->Iterate() != NULL ) if( strcmp( m_variables->GetCurrent()->GetName(), variable ) == 0 ) return m_variables->GetCurrent()->GetData(); return NULL; }
- Сохрани Решение (Файл->Сохранить все).
Исследуем Scripting.cpp
Реализация класса Variable
Реализация класса Variable размещена в самом начале Scripting.cpp, так как его будет использовать класс Script, размещённый чуть ниже. Согласись, будет неправильно писать реализацию класса Script, у которого не готова ни одна переменная, с которой он будет работать. Напомним, что у нас 2 конструктора, которые создают экземпляры класса Variable с разным набором параметров. Наиболее интересным является первый конструктор, размещённый в самом начале Scripting.cpp .Пользуясь комментариями, обязательно изучи его. Этот материал особенно пригодится при добавлении своих собственных типов данных (да, никто не отменял расширяемость движка). Для этого тебе сначала необходимо добавить новый тип в перечисление в самом начале Scripting.h, а затем добавить код загрузки в оба конструктора. В конце останется лишь добавить возвращаемый код для своего нового типа данных, используемый функцией GetType().
Инкапсуляция и автоматизация (теоретическое отступление)
Что происходит? Где класс Script? По подзаголовку, ты, наверное, догадался, что мы не будем сейчас переходить к классу Script. Вместо этого, мы совершим небольшой теоретический экскурс в объектно-ориентированное программирование. Но остаётся вопрос "Зачем? Ведь, здесь не совсем подходящее место для этого." Ведь до этого мы говорили о скриптинге, а не об объектно-ориентированном программировании. Проблема в том, что если бы мы разместили этот материал в самом начале курса, его будет трудно понять, не имея перед глазами реального кода в качестве примеров. Очень удобно, что класс Script, который мы обязательно рассмотрим, полностью соответствует данной теме. Поэтому имеет смысл рассмотреть эти теории именно сейчас, когда у нас перед глазами реальные примеры с исходным кодом по теме. Данная тема во многом перекликается с исходным кодом системы скриптов и релевантна ей.
Инкапсуляция и автоматизация - два очень важных аспекта программирования, которые сделают твою жизнь намного проще. Они идут "рука за руку" с такими чудесными словами, как абстракция и полиморфизм. Все эти термины являются именами различных форм техник программирования, которые специально разработаны для упрощения исходного кода, что, в свою очередь, облегчает его обслуживание и последующее применение в других проектах. Вспомни все эти странные слова, о которых мы говорили в Главе 1.1: пригодность к обслуживанию (maintability), к повторному использованию (reusability) и удобство применения (usability). Сейчас эти слова уже начинают что-то значить.
Рассмотрим ряд определений, которые перекликаются с нашей ситуацией. Икапсуляция означает, что ты комбинируешь функционал для создания компонента более высокого уровня (это и называют абстракцией), который выполняет ту же самую задачу, независимо от других блоков кода (т.е. каждый компонент является своего рода "капсулой", довольно сильно изолированной от других компонентов). Другими словами, компонент самодостаточен, он не полагается на внешнюю помощь для выполнения своих обязанностей. Также внутри компонента размещаются данные, которые он использует. Это очень важный момент, так как создаваемые классы базируются на данных, которыми они манипулируют. Хотя, это и не является основной причиной того, почему классы в объектно-ориентированном программировании работают так как положено. Суть заключается в том, что ты комбинируешь функционал определённого объекта в класс, который может работать абсолютно независимо, получая ввод и выдавая тот вывод, для которого он и был разработан. Здесь мы напрямую выходим к теме автоматизации, где ты создаёшь объекты, которые при комбинировании могут производить результат высокого уровня через серию низкоуровневых операций ввода/вывода, а также процессов. Итак, ты уже видишь преимущества автоматизации, когда помещаешь в очередь (в стек) несколько объектов один над другим, у каждого из которых своя строго определённая роль. В комбинации они работают вместе, для одного общего дела, что само по себе является другим примером инкапсуляции, только на более высоком уровне. Рис. 2 наглядно иллюстрирует весь этот жаргон.
Для лучшего понимания того, как работают вместе две этих концепции, рассмотрим пример. В качестве примера снова возьмём автомобиль и представим, что нам необходимо смоделировать его с применением объектно-ориентированного подхода. Самое очевидное - отделить (выделить) двигатель и определить двигатель и оставшуюся часть автомобиля как 2 отдельных объекта, которые работают вместе. Далее мы можем выделить другие части машины, как например сиденья и колёса, и тоже определить их в качестве отдельных объектов. Конечно, можно пойти дальше и выделить множество других деталей автомобиля, вплоть до болтов и гаек. Но делать это совсем необязательно. Так как же узнать, когда надо остановиться? Всё просто. Надо просто взглянуть на "данные", которые эти объекты обрабатывают (процессят), и затем разделить авто на отдельные логические группы, которые манипулируют одними и теми же общими данными.
Так, если мы рассмотрим двигатель автомобиля, то увидим, что он использует топливо, воздух, масло, воду, педаль газа и тормоза в качестве "вводных данных" для производства выходного продукта - энергии для вращения приводных валов (или карданного вала, если это "Жигули-классика";-). Важный момент здесь это то, что двигатель, в принципе, самодостаточен и ему не нужна никакая другая деталь автомобиля для манипулирования этим типом данных. Поэтому мы можем сделать вывод о том, что двигатель является отдельным компонентом, который может быть инкапсулирован. Это означает, что вместе с ним также инкапсулируются и "данные" (топливо, воздух и т.д.), с которыми он работает. При желании, по такому же принципу можно выделить другие компоненты автомобиля, идентифицируя каждый агрегат по уникальному типу "данных", с которыми он работает. Перед выделением объекта обязательно убедись, что соответствующие данные не используются другими объектами. И наоборот, если компонент не использует схожие данные с другим компонентом, то и объединять их не стоит. Это тот самый момент теории, который нужно запомнить, чтобы обезопасить себя от "детских" ошибок, как например комбинирование амортизаторов с двигателем или тормозов с трансмиссией лишь на том основании, что они тоже используются для приведение авто в движение. Вывод всему вышесказанному такой: всегда инкапсулируй объекты, основываясь на данных, с которыми они работают, а не на их функционале (см. Рис.3).
Автоматизация является фундаментальным понятием при разработке программного обеспечения, особенно приложений с интенсивной логикой, какими являются компьютерные игры. Рассмотрим эту концепцию на всё том же автомобиле и посмотрим, как автоматизация может здорово упростить процесс приведения его в движение. Первый шаг - применить акселерацию, используя органы управления авто (педаль газа), которые дают "ввод" двигателю, давая команду увеличить обороты, что, в свою очередь, создаст "вывод" энергии. Эта энергия проходит через трансмиссию и передаётся приводным валам, которые затем передают эту энергию колёсам. Таким образом вывод двигателя становится вводом для трансмиссии, которая даёт свой вывод, который, в свою очередь, становится вводом для колёс. А сейчас, если мы рассмотрим автомобиль в качестве законченной системы (т.е. мы инкапсулируем все компоненты автомобиля в единый класс более высокого уровня), мы можем с помощью всего одного ввода (нажатие педали газа) привести в движение весь автомобиль. И происходит это именно за счёт автоматизации, суть которой заключается в том, что каждый компонент выполняет свою роль для одной общей цели - получения конечного результата. Так какой же из компонентов автомобиля производит итоговый вывод, приводящий его в движение? Все. Каждый из них, будучи скомбинированным друг с другом, производит общий вывод. Другими словами, мы имеем набор самодостаточных объектов, которые получают ввод, обрабатывают его и дают соответствующий вывод, который, в свою очередь, становится вводом для другого объекта этой взаимосвязанной цепи. Этот процесс продолжается без перерыва, а самое главное - без внешнего вмешательства. Нам не нужно внедрять какие-либо компоненты для помощи из вне, так как весь процесс автоматизирован (см. Рис.4).
Но самое удивительное здесь другое. Так как каждый субобъект автомобиля инкапсулирован, то в этом случае все они могут быть модифицированы или заменены без ущерба для других компонентов. Например, ты можешь заменить колёса и ожидать при этом точно такой же производительности от движка. Ты также можешь модифицировать двигатель и ожидать, что трансмиссия будет работать в том же режиме, что и раньше. Единственное ограничение - это интерфейсы между компонентами. К примеру, ты не можешь заменить двигатель, если его стыковочный узел с трансмиссией отличается от прежнего. Главное правило остаётся следующим. Если ввод (полученный от другого компонента) удовлетворяет данному компоненту и производит корректный вывод, то внутри него ты можешь делать всё, что захочешь.
Лишь в случае крайней необходимости ты можешь изменить интерфейс. В любом случае делать это крайне не рекомендуется. Для автомобиля это, может, и не представляет особой проблемы. А при разработке программного обеспечения очень даже представляет. Причина этого заключается в том, что когда ты создаёшь новую версию соответствующего компонента и изменяешь интерфейс, то в этом случае ты потенциально "разрушаешь" другие компоненты прежних версий, которые пытаются использовать новый интерфейс. Вообще, считается хорошей идеей и признаком хорошего тона продолжать поддержку старых интерфейсов. В программировании это называют обеспечением обратной совместимости (backwards compatibility). Недаром DirectX активно использует это свойство. И игра, (правильно) спрограммированная под DirectX 8.0, без труда запустится в ОС с установленным DirectX 11 или более поздними версиями.
Объявление класса Script (Проект Engine)
Вернёмся к нашей главной теме. Сейчас ты увидишь, как всё вышеизложенное мы применим в деле. До этого мы бегло просмотрели класс Variable, получив достаточно информации для перехода к классу Script. Ты уже знаешь, что класс Script использует класс Variable для своих нужд. Рассмотрим объявление класса Script в Scripting.h:... //----------------------------------------------------------------------------- // Script Class //----------------------------------------------------------------------------- class Script : public Resource< Script > { public: Script( char *name, char *path = "./" ); virtual ~Script(); void AddVariable( char *name, char type, void *value ); void SetVariable( char *name, void *value ); void SaveScript( char *filename = NULL ); bool *GetBoolData( char *variable ); D3DCOLORVALUE *GetColourData( char *variable ); float *GetFloatData( char *variable ); long *GetNumberData( char *variable ); char *GetStringData( char *variable ); D3DXVECTOR3 *GetVectorData( char *variable ); void *GetUnknownData( char *variable ); private: LinkedList< Variable > *m_variables; // Linked list of variables in the script. // Связный список переменных в скрипте. }; ...
Как видишь, класс Script просто наследуется от класса-родителя Resource. Так как скрипт обладает всеми свойствами расширенного ресурса, у нас нет причин, чтобы не включить поддержку скриптов в нашу систему менеджмента ресурсов. Напомним, что наш класс Resource на самом деле является шаблоном и представляет собой "заготовку" для создания классов нежели базовый класс. Это означает, что при создании класса Resource (в соответствии с шаблоном) мы обязательно должны указать используемый тип ресурса. Это делается для того, чтобы компилятор знал, каким образом строить данный класс. В нашем случае при указании шаблонного класса, в угловых скобках мы указываем тип Script.
Класс Script содержит в себе целую подборку функций для работы с переменными, которые хранятся в связном списке с именем m_variables. Напомним, что реализация класса LinkedList тоже является шаблоным класом, и значит при создании класса на его основе мы должны указать в угловых скобках тип (или вид хранимых элементов). В нашем случае в самом конце объявления класса Script мы создаём связный список переменных и в угловых скобках в качестве типа хранимых элементов укажем ранее созданный класс Variable. Таким образом мы создадим класс LinkedList с типом Variable, где в упорядоченном виде будут храниться переменные каждого скрипта. Если ты ещё не догадался, именно здесь мы и применяем инкапсуляцию и автоматизацию, рассмотренные выше. Посмотрим как эти две концепции работают на реальном примере, а не на абстрактном автомобиле. Автоматизация является фундаментальным понятием при проектировании программного обеспечения, особенно когда дело касается приложений с интенсивной логикой, какими являются игры. Представь, если вдруг мы попытаемся применить наш класс Script, который самостоятельно обрабатывал все переменные скриптов, которые хранятся в созданном вручную связном списке, который также управляется классом Script. При таком подходе класс Script очень скоро станет очень запутанным, плохо управляемым и чреватым ошибками, что противорречит нашей цели о пригодности к обслуживанию (maintability) нашего движка. Вместо этого мы создали серию инкапсулированных классов, каждый из которых выполняет одну определённую функцию, причём без посторонней помощи. При таком подходе мы можем безопасно строить классы, как например наш Script класс, построенный "поверх" системы менеджмента ресурсов и технологии связных списков, в котором всё чётко и ясно прописано и который будет работать именно так, как задумано. И даже если что-то в работе этой связки пойдёт не так, процесс отладки (debugging) и устранения ошибок при таком подходе выполняется намного проще. Ведь вместо того, чтобы просто сказать, что где-то внутри класса Script есть проблема, мы можем сузить область поиска, выяснив в каком именно подклассе мог произойти сбой. Это означает, что мы имеем дело с более простым кодом, даже несмотря на тот факт, что скомбинированные друг с другом классы производят больший, более сложный вывод.
Напомним ещё раз, что всегда очень важно разделять компоненты и классы, основываясь на данных, а не на функционале. Каждый класс инкапсулируется именно вокруг тех данных, которые он обрабатывает, а не вокруг функционала, который он предоставляет. Если взглянуть на наш класс Script, то легко заметить, что он использует инкапсулированный класс Variable для управления каждой переменной внутри класса, вместо того, чтобы пытаться манипулировать всеми переменными самостоятельно, с помощью своих внутренних функций. Так зачем же мы разделили систему скриптов на два отдельных класса (Script и Variable)? Класс Variable имеет дело исключительно с данными переменных, тогда как класс Script эти данные мало интересуют. Класс Script вообще не интересует, что находится внутри той или иной переменной или какого она типа, или даже какое у неё имя. Если классу Script потребуются эти сведения, он просто запросит их у соответствующего экземпляра класса Variable. Более того, у нас есть классы Resource и LinkedList, которые заботятся лишь о хранимых данных, которыми здесь являются имя файла скрипта и количество переменных соответственно.
И ещё раз, если скрипту потребуются какие-либо подробные сведения о той или иной переменной, он может просто запросить переменную из связного списка и затем запросить у данной переменной всю необходимую информацию. На Рис.5 показан проект системы скриптов (scripting system) с применением инкапсуляции и автоматизации.
Вся эта схема является системой скриптов и позволяет применять её как инкапсулированный компонент примерно таким же образом, как мы это делали в примере с гипотетическим автомобилем. Мы можем посылать команды в класс Script или запрашивать информацию у него и ожидать, что он будет работать определённым образом и выдавать определённый результат, даже несмотря на то, что далеко не всю работу он проделывает самостоятельно. Все другие компоненты, которые использует класс Script, работают совместно (благодаря автоматизации) выдавая один общий результат (см. Рис.6).
Разработка кода с использованием данных концепций значительно приближает нас к достижению всех трёх целей дизайн-проект нашего движка, изложенных в Главе 1.1. Во-первых, исходный код в этом случае проще обслуживать (maintability), так как он состоит из нескольких классов, специфичных для каждого вида данных. Это позволяет намного проще находить проблематичные участки кода. Во-вторых, мы можем быстрее достичь цели повторного использования кода (reusability). Вместо того, чтобы разрабатывать компоненты, которые полагаются один на другой (так как они работают с одними и теми же данными), мы создали инкапсулированные классы, которые не зависят от типа данных и которые можно легко изменять и заменять при необходимости. Например, мы можем полностью заменить реализацию классов Variable, Resource или LinkedList не изменив ни единой строки кода в классе Script (до тех пор, пока не изменены интерфейсы). А всё потому, что они сгруппированы именно вокруг данных, с которыми они работают, а не на основе функционала, который они предоставляют, будучи скомбинированными. Если бы мы "слепили" весь этот функционал вместе, то в этом случае было бы затруднительно изолировать, заменять или исправлять в любом из компонентов. В третьих, ты должно быть заметил, что степень удобства использования (usability) также значительно увеличилась. В основном из-за того, что множество хитроспелетений внутрисистемных операций могут быть спрятаны, позволяя программеру использовать систему скриптов как единый модуль. Программеру необходимо лишь изучить данные для ввода и структуру результата, который он получит при выводе. При этом он может пропустить всё, что находится посередине (внутри системы скриптов).
Реализация класса Script (Проект Engine)
Вернёмся к нашей теме, продолжив обсуждать класс Script. Обратившись к его реализации, размещённой в Scripting.cpp, мы более подробно рассмотрим функции, которые он содержит. Начнём с конструктора:... //----------------------------------------------------------------------------- // The script class constructor. //----------------------------------------------------------------------------- Script::Script( char *name, char *path ) : Resource< Script >( name, path ) { ...
Это обычное дело при загрузке любого физического ресурса, так как эти два параметра необходимы для системы менеджмента ресурсов. Как можем видеть, они тут же передаются конструктору класса Resource, так как класс Script наследуется от класса Resource. Запомни, что именно это наследование позволяет управлять скриптами системе менеджмента ресурсов, что в свою очередь является ключевым фактором при контроле используемой памяти, так как предотвращает загрузку нескольких копий одного и того же скрипта. Это также позволяет быть уверенным в том, что при закрытии приложения все ранее загруженные в память скрипты будут удалены из неё, предотвратив таким образом т.н. "утечки памяти" (memory leaks).
Вслед за этим, создаём экземпляр класса LinkedList. Таким образом мы создаём связный список, в котором будут храниться все переменные нашего скрипта. Так как класс LinkedList является шаблоном, то при создании его экземпляра мы обязательно должны указать тип хранимых элементов. В нашем случае указываем класс Variable. Следующий шаг -предпринять попытку открыть дескриптор текстового файла скрипта, который указан в параметрах конструктора:
... // Create the linked list that will store all of the script's variables. // Создаём связный список, хранящий все переменные скрипта. m_variables = new LinkedList< Variable >; // Open the script file using the filename. // Открываем файл скрипта по имени его файла. FILE *file = NULL; if( ( file = fopen( GetFilename(), "r" ) ) == NULL ) return; ...
FILE *file - это структура, которая будет хранить детали потока (stream) открытого файла.
fopen - стандартная функция файлового ввода/вывода (описана в stdio.h), которая открывает файл. Вот её прототип:
FILE *fopen( const char *filename, const char *mode );
Здесь первым параметром является полное имя файла (включает в себя имя файла вместе с расширением и полный путь к нему).
Второй параметр - режим, в котором открывается файл. Он может принимать следующие значения:
ЗНАЧЕНИЕ | ОПИСАНИЕ |
---|---|
r | Открывает файл для чтения. Файл должен существовать. |
w | Открывает файл для записи. Если файл уже существует, его содержимое перезаписывается. |
a | Открывает файл для добавления в конец файла (appending). Если файл не существует, он будет автоматически создан. |
r+ | Открывает файл для чтения и записи. Файл должен существовать. |
w+ | Открывает файл для чтения и записи. Если файл уже существует, его содержимое перезаписывается. |
a+ | Открывает файл для чтения и добавления в конец файла (appending). Если файл не существует, он будет автоматически создан. |
В нашем случае в качестве первого параметра применяется функция GetFileName, указанная в классе Resource и возвращающая полное имя файла (имя файла + его расширение + полный путь к нему) скрипта. Мы открываем файл, применив во втором параметре r, что позволит нам считать данные из файла, не изменяя их. Выполнение функции fopen обрамлено условным оператором if, который, в случае возникновения ошибки или когда файл скрипта попросту не существует, досрочно завершает работу fopen и возвращает NULL. Эта проверка нужна здесь для того, чтобы предотвратить выполнение дальнейших операций над неправильно составленным или вовсе несуществующим скриптом.
Двигаемся дальше по исходному коду класса Script (Scripting.cpp) и рассмотрим метод чтения переменных из скрипта:
... // Continue reading from the file until the eof is reached. // Продолжаем чтение файла до тех пор, пока не достигнем // спецметки eof (end of file). bool read = false; char buffer[MAX_PATH]; fscanf( file, "%s", buffer ); while( feof( file ) == 0 ) { // Check if the file position indicator is between a #begin and #end // statement. If so then read the data into the variable linked list. // Проверяем, индикатор положения между тегами #begin и #end. // Если да, то считываем данные в связный список переменных. if( read == true ) { // Stop reading data if an #end statement has been reached. // Останавливаем чтение данных при достижении тега #end. if( strcmp( buffer, "#end" ) == 0 ) read = false; else m_variables->Add( new Variable( buffer, file ) ); } else if( strcmp( buffer, "#begin" ) == 0 ) read = true; // Read the next string. // Читаем следующую строку. fscanf( file, "%s", buffer ); } ...
Сперва мы объявляем переменную read типа bool, с помощью которой будем определять, какие участки скрипта считывать, а какие нет. Не забываем, что помимо блоков переменных наш скрипт может содержать, например, комментарии, которые, очевидно, считывать не нужно.
Далее мы создаём переменную buffer типа char. По сути это массив символов, который мы будем использовать для чтения слов (в нашем случае это строки) скрипта, по одной за раз.
Далее используем функцию fscanf, которая позволяет считывать форматированные данные из потока. Вот её прототип:
int fscanf(FILE *stream, const char *format, ...);
Здесь первый параметр - указатель на поток открытого файла. Второй - формат данных, которые функция будет искать в потоке. Существует довольно много различных форматов. Вот лишь некоторые из них:
ЗНАЧЕНИЕ | ОПИСАНИЕ |
---|---|
%с | Однобайтовый символ (single byte character). |
%d | Десятичное числовое значение. |
%f | Числовое значение с плавающей точкой (floating point value). |
%s | Строка, оканчивающаяся там, где встречается первый символ пробела. |
Нас интересует чтение по одной строке за раз, поэтому мы используем формат %s.
В последнем необязательном параметре мы передаём указатель на наш массив buffer, который по завершении работы функции fscanf пополнится новой строкой, найденной в потоке.
Далее мы входим в цикл while, который последовательно проверяет поток file, используя условие feof, который возвращает ненулевое значение, когда достигнут конец файла (end of file - eof). В этом случае выполнение цикла прерывается, так как мы достигли конца файла скрипта и считывать больше нечего.
Внутри цикла видим несколько операторов if else. Здесь проверяем, собираемся ли мы считывать переменную из скрипта. Если да, то проверяем буфер (массив buffer) на окончание строк с блоком переменных. Для этого применяется функция strcmp (от англ. "string compare" - сравнение строк), которая проверяет, не содержит ли наш буфер тег #end. Данная функция просто сравнивает две строки и возвращает 0, если они одинаковы, собственно что нам и требуется. Если мы достигаем конца блока переменных, нам нужно просигнализировать, что мы больше не ищем переменные. По крайней мере, пока не достигнем следующего тега #begin, сигнализирующего о начале нового блока переменных. В остальных случаях, до тех пор, пока не будет обнаружен тег #end, то в этом случае очевидно, что мы нашли новую переменную.
Когда переменная найдена, мы используем функцию Add из класса LinkedList для добавления новой переменной в конец связного списка m_variables. Мы передаём указатель на буфер (buffer) и файл (file) в конструктор новой переменной, чтобы таким образом класс Variable мог вызвать сам себя с переданными в него параметрами. По завершении работы функции Add, индикатор позиционирования (проще говоря, курсор) потока file будет перемещён в конец данной строки вида "переменная-значение", готовясь начать чтение новой строки. А всё потому, что конструктор класса Variable считывает соответствующие символы из потока file по порядку, чтобы затем извлечь из неё тройку "имя + тип переменной + значение".
В том случае, когда мы не собираемся считывать переменные, это означает, что мы находимся за пределами блока переменных (т.е. не между тегами #begin ... #end). Поэтому нам необходимо проверить, не вошли ли мы в него. Мы делаем это снова применив функцию strcmp, которая в этот раз ищет тег #begin. При обнаружении тега #begin, сигнализируем о готовности войти в блок переменных и начать чтение, установив переменную read в true.
Последний шаг внутри общего цикла while - это дать команду на считывание следующей строки из потока file:
... // Read the next string. // Читаем следующую строку. fscanf( file, "%s", buffer ); } // Close the script file. // Закрываем файл скрипта. fclose( file ); } ...
Цикл затем возвращается в начало и вся процедура повторяется снова. Выполнение цикла будет продолжаться до тех пор, пока не будет достигнут специальный маркер eof (end of file). В этом случае цикл прерывается. Как только цикл прервался, скрипт считается полностью загруженным в память и мы, наконец, вызываем функцию fclose для закрытия потока file.
В Scripting.h при объявлении класса Script были указаны ещё 3 функции: AddVariable, SetVariable и SaveScript. Они применяются для создания скриптов, что называется "на лету", то есть во время выполнения приложения, что мы будем делать очень редко. Их реализации можно найти в Scripting.cpp, сразу после деструктора класса Script. Функция AddVariable добавляет новую переменную в скрипт, с обязательным указанием её соответствующего значения. Функция SetVariable изменяет значение существующей переменной в скрипте. Функция SaveScript записывает скрипт из памяти в указанный текстовый файл.
Эти функции служат в основном для удобства и позднее ты можешь применять их в своём коде при разработке собственных приложений.
Оставшиеся функции применяются для получения значения любой из переменных внутри скрипта. Так как все они очень похожи и отличаются лишь возвращаемым результатом, мы рассмотрим лишь одну из них - GetNumberData. Её мы будем применять особенно часто. Вот её реализация:
... //----------------------------------------------------------------------------- // Returns number data from the named variable. // Возвращает данные числового типа по имени переменной. //----------------------------------------------------------------------------- long *Script::GetNumberData( char *variable ) { m_variables->Iterate( true ); while( m_variables->Iterate() != NULL ) if( strcmp( m_variables->GetCurrent()->GetName(), variable ) == 0 ) return (long*)m_variables->GetCurrent()->GetData(); return NULL; } ...
Функция принимает 1 параметр - имя переменной, значение которой необходимо узнать. Затем функция итерирует (т.е., грубо говоря, "просматривает") связный список переменных скрипта с применением функции strcmp для поиска переменной с заданными именем. Когда переменная найдена, внутренняя функция класса LinkedList GetData запрашивает данные этой переменной и возвращает указатель на эти данные, которые затем интерпретируются в значение типа long. Если переменная не найдена, возвращается NULL.
Интегрируем систему скриптов в движок
Принцип тот же, что и при интеграции других систем.Изменения в Scripting.cpp (Проект Engine)
- Добавь инструкцию #include "Engine.h" в самом начале файла Scripting.cpp (проверь её наличие).
Изменения в Engine.h (Проект Engine)
- Добавь инструкцию #include "Scripting.h" в файл Engine.h, сразу после инструкции #include "Geometry.h":
... //----------------------------- // Engine Includes //----------------------------- #include "LinkedList.h" #include "ResourceManagement.h" #include "Geometry.h" #include "Scripting.h" #include "Input.h" #include "State.h" ...
До тех пор, пока ты не планируешь самостоятельно загружать, считывать и уничтожать скрипты, ты всегда должен использовать для этого разработанную ранее систему менеджмента ресурсов (resource management system). Для этого:
- Добавь новый менеджер ресурсов под названием m_scriptManager в объявлении класса Engine, в секции private класса Engine, ДО объявления объекта системы ввода (Input):
- Добавь функцию GetScriptManager в объявлении класса Engine, в секции public класса Engine, ДО функции доступа к системе ввода (Input):
После внесённых изменений класс Engine будет выглядеть так:
... //----------------------------------------------------------------------------- // Engine Class //----------------------------------------------------------------------------- class Engine { public: Engine( EngineSetup *setup = NULL ); virtual ~Engine(); void Run(); HWND GetWindow(); void SetDeactiveFlag( bool deactive ); void AddState( State *state, bool change = true ); void RemoveState ( State *state ); void ChangeState( unsigned long id ); State *GetCurrentState(); ResourceManager< Script > *GetScriptManager(); Input *GetInput(); private: bool m_loaded; // Indicates if the engine is loading. // Флаг показывает, загружен ли движок HWND m_window; // Main window handle. // Дескриптор главного окна приложения bool m_deactive; // Indicates if the application is active or not. // Флаг активности приложения EngineSetup *m_setup; // Copy of the engine setup structure. // Копия структуры EngineSetup LinkedList< State > *m_states; // Связный список (Linked list) стейтов. State *m_currentState; // Указатель на текущий стейт. bool m_stateChanged; // Флаг показывает, изменён ли стейт в текущем кадре. ResourceManager< Script > *m_scriptManager; // Менеджер скриптов. Input *m_input; }; ...
Функция GetScriptManager позволяет получить доступ к менеджеру скриптов из других частей Проекта Engine. Напоминаем, что класс Resource является шаблоном, поэтому при создании его экземпляра обязательно указываем в угловых скобках тип ресурса. В нашем случае это Script.
Изменения в Engine.cpp (Проект Engine)
- Добавь следующую строку в конструктор класса Engine...
m_scriptManager = new ResourceManager< Script >; cразу после строки m_currentState = NULL:
...cразу после строки m_currentState = NULL:
... // Create the window and retrieve a handle to it. // Note: Later the window will be created using a windowed/fullscreen flag. // Создаём окно. // Позднее окно будет создаваться с учётом флага оконного/полноэкранного режимов. m_window = CreateWindow( "WindowClass", m_setup->name, WS_OVERLAPPED, 0, 0, 800, 600, NULL, NULL, m_setup->instance, NULL ); m_states = new LinkedList< State >; m_currentState = NULL; m_scriptManager = new ResourceManager< Script >; // Создаём экземпляр класса Input. m_input = new Input( m_window ); ...
- Добавь следующую строку в деструктор класса Engine:
SAFE_DELETE( m_scriptManager );
... //----------------------------------------------------------------------------- // The engine class destructor. //----------------------------------------------------------------------------- Engine::~Engine() { // Ensure the engine is loaded. // Проверяем, загружен ли движок. if( m_loaded == true ) { // Everything will be destroyed here (such as the DirectX components). // Здесь всё уничтожаем. if( m_currentState != NULL ) { // Уничтожаем связные списки со стейтами. m_currentState->Close(); SAFE_DELETE( m_states ); // Уничтожаем объект Input. SAFE_DELETE( m_input ); // Уничтожаем менеджер скриптов. SAFE_DELETE( m_scriptManager ); } } ...
- Добавь реализацию функции GetScriptManager в самом конце Engine.cpp:
... //--------------------------------------------------- // Возвращает указатель на текущий менеджер скриптов. //--------------------------------------------------- ResourceManager< Script > *Engine::GetScriptManager() { return m_scriptManager; } ...
- Сохрани Решение (Файл->Сохранить все).
Тестовая перекомпиляция Engine.lib
Итак, система скриптов полностью интегрирована в движок.Для проверки работосопособности исходного кода, добавленного в этой Главе, перекомпилируем исходный код Проекта Engine:
- В Обозревателе решений щёлкаем правой кнопкой мыши по значку Проекта Engine. Во всплывающем меню выбираем "Перестроить" (применяется в случае, когда код уже был успешно скомпилирован ранее).
Обрати внимание
Напомним о том, что следует отличать ошибки (errors) от предупреждений (warnings). Компиляция может завершится успешно даже при выводе сотен всевозможных предупреждений. Чаще всего они связаны с использованием устаревших (deprecated) либо небезопасных (unsafe), по мнению Microsoft, функций. За основу учебного курса была взята книга Young V. "Programming Multiplayer FPS in DirectX", вышедшая в свет в 2005 году, где все примеры написаны под классический C++ в IDE MS Visual C++ 6.0 аж 1998 года выпуска. С тех пор очень многое изменилось. Microsoft развивала C++, Windows API и свои IDE семимильными шагами. В результате многие функции, успешно применявшиеся тогда, сегодня признаны Microsoft устаревшими и не рекомендованными к использованию. К предупреждениям можно прислушиваться, а можно и нет. Для простоты изложения мы будем их игнорить. А чтобы они не выводились совсем, пропиши строку _CRT_SECURE_NO_WARNINGS в свойствах Проекта Engine (Свойства -> Свойства конфигурации -> С/С++ -> Препроцессор -> Определения препроцессора).
Полученная в результате компиляции двоичная библиотека Engine.lib перезаписывается по тому же пути (там же её будет искать тестовое приложение из Проекта Test).
Применение системы скриптов
В данной Главе мы не будем создавать/модифицировать тестовое приложение (Проект Test). Но обязательно сделаем это чуть позднее. А сейчас просто распишем основные моменты.Если при создании реализации системы скриптов ты не встретил особых трудностей, то её использование покажется тебе до смешного просто простым делом. Но сперва рассмотрим различные типы данных, которые можно применять к нашим переменным. В Таблице 1 приводятся типы, поддерживаемые нашей системой скриптов + даётся их краткое описание.
Таблица 1. Типы данных, поддерживаемые системой скриптов
ТИП | ОПИСАНИЕ |
---|---|
bool | Булева переменная может принимать всего 2 значения: true/false (истина/ложь). Значение чувствительно к регистру. Любые значения, не совпадающие со значением true, трактуются как false. |
colour | Значение цвета в формате RGBA (Red, Green, Blue, Alpha). Каждый из четырёх компонентов является числом с плавающей точкой (тип float), изменяемый от 0.0 до 1.0. Каждый компонент должен быть отделён от другого хотя бы одним пробелом. |
float | Число с плавающей точкой. |
number | Целое число. |
string | Строка символов, состоящая из букв и/или цифр. Если строка состоит из нескольких слов (например, отделённых друг от друга пробелами), то в этом случае он должна быть заключена в кавычки (например: "string goes here"). |
unknown | Специальный тип, означающий данные неизвестного типа. Когда тип переменной не определён (например, при создании) то ей присваивается этот тип. В системе данный тип хранится как массив символов. Программер может получить доступ к переменной во время выполнения приложения и в результате она вернёт массив символов. В любом случае, интерпретация данных в один из приемлемых форматов - задача программера. Кроме того, тип unknown можно эффективно использовать для хранения переменных самых разных типов (включая различные пользовательские форматы данных). |
vector | Вектор в 3D-пространстве с координатами (x, y, z), каждый компонент которого является числом с плавающей точкой (тип float) и должен быть отделён от другого хотя бы одним пробелом. |
Вот пример простейшего скрипта, в котором представлены переменные всех поддерживаемых типов:
Это комментарий, так как он расположен за пределами блока переменных. #begin ballColour colour 1.0 0.0 0.65 1.0 my_var float 27.3978 text string Testing? someData number 1234 MyStory string "Давным давно... " myStory string "... В далёкой-далёкой галактике" made_up code jdf93mf093j userVar unknown 0 0 0JHM-93 5TKY-4 7 3HUR position vector 20.5 79.938 -4.0 WORKING bool true #end
В данном скрипте можно отметить несколько интересных моментов. Во-первых, переменные MyStory и myStory уникальны, так как отличаются регистром символа в имени. Да, имена переменных чувствительны к регистру и ты должен всегда давать переменным уникальные имена (т.е. в одном скрипте не должно быть переменных с одинаковыми именами). Во-вторых, переменная made_up будет автоматически приведена к типу unknown, так как её тип (code) не может быть распознан системой.
Скрипт готов. Следующий шаг - его загрузка. Загрузить скрипт можно двумя способами:
Через менеджер ресурсов (рекомендуется)
Менеджер скриптов на базе менеджера ресурсов позаботится обо всём этом автоматически в том случае, если ты будешь загружать скрипты через него. Вот так может выглядеть функция загрузки скрипта через менеджер скриптов, размещённая в коде приложения:... Script *myScript = m_scriptManager->Add( "script.txt" ); ...
Данная инструкция предпринимает попытку загрузить скриптовый файл script.txt, который расположен в той же директории, что и стартовый исполняемый файл игрового приложения. В случае удачной загрузки скрипта, переменная myScript будет содержать указатель на загруженный скрипт. Для удаления скрипта достаточно выполнить команду:
... m_scriptManager->Remove( *myScript ); ...
Своим способом (без использования менеджера скриптов)
Напомним, что при загрузке скриптов своим способом, программер ответственен за их своевременное уничтожение, а также за недопущение ситуаций, когда в памяти оказываются загружены несколько экземпляров одного и того же скрипта. В противном случае это приведёт к неэффективному расходу памяти.Команды самостоятельной загрузки и удаления скрипта (без использования менеджера скриптов) выглядит так:
... Script *myScript = new Script( "script.txt" ); SAFE_DELETE( myScript ); ...
Доступ к переменным скрипта
В обоих случаях получить доступ к переменным скрипта можно с помощью следующих команд:... myScript->GetFloatData( "my_var" ); myScript->GetStringData( "MyStory" ); myScript->GetVectorData( "position" ); ...
Как видишь, ничего сложного. Самое главное, что необходимо запомнить, это, когда ты загружаешь скрипт самостоятельно, не забудь его своевременно удалить. В остальных случаях пользуйся менеджером скриптов.
Исходные коды
...Решения GameProject01 (для MS Visual C++ 2010), с которым работали в данной Главе, забираем здесь.Итоги
Наша система скриптов полностью работоспособна и даёт возможность самостоятельно создавать любые игровые скрипты. Мы даже уделили немного времени на изучение двух базовых концепций объектно-ориентированного программирования (ООП) - инкапсуляции и автоматизации.Уже в следующей Главе мы увидим систему скриптов в действии, когда будем создавать энумерацию (от англ. "enumeration" - перечисление) устройств Direct3D. Там мы будем через систему скриптов сохранять и загружать настройки видеоадаптера, для того чтобы пользователю не приходилось устанавливать их при каждом последующем запуске игры.
Источники
1. Young V. Programming a Multiplayer FPS in DirectX 9.0. - Charles River Media, 2005
ДАЛЕЕ ==> Кодим 3D FPS DX9. 1.10 Добавляем рендеринг Ч.1
Последние изменения страницы Среда 13 / Июль, 2022 14:23:34 MSK
Последние комментарии wiki