Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.9 Добавляем поддержку скриптов (scripting)
Программируем 3D-шутер от первого лица (FPS) (Win32, C++, DirectX9)

1.9 Добавляем поддержку скриптов (scripting)


Преимущества скриптов

Мы начнём с определения, что же собой представляет скрипт. К этой главе ты уже хорошо знаком со стандартным кодом языка 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 переменные и соответствующие значения этих переменных, которые будет использовать система скриптинга игры:

sample_config.ini
rof = 120
range = 200
damage = 50

Какие именно параметры будут представлены в скрипте свойств, зависит от самой игры. Но сам принцип понятен. Например, rof может означать число выстрелов в минуту, а range - дистанцию выстрела в метрах, футах, ярдах или в любых других единицах измерения, используемых в игре. Также нам необходимо позаботиться о типе данных, в которых будут сохраняться переменные. В вышеприведённом примере у нас есть 3 значения, которые, очевидно, могут быть представлены как целочисленный тип данных, например int или long или даже unsigned char (подразумевая, что значения никогда не выйдут за пределы присвоенного им типа данных). Но что произойдёт, если мы захотим сохранить букву, слово или целое предложение? Или, к примеру, значение с плавающей точкой (floating point value) или булево (Boolean) значение (true или false)? Как насчёт комплексных типов данных, как например информация о цвете или 3D-координаты? Это не проблема, так как мы обязательно учтём все эти случаи при создании нашей системы скриптов.
Вообще, нам не стоит беспокоится о снижении производительности при использовании системы скриптов в реальном времени, так как наша система скриптов будет редко использоваться в реальном времени. Подумай, в каких местах скрипт свойств будет использоваться наиболее часто. Скорее всего это будут участки кода, где определяются свойства игровых объектов. Все они, как правило, загружаются единовременно (один раз) при запуске приложения. После этого система располагает этими данными, загруженными в оперативную память, и ей не нужно повторно считывать их из скрипта свойств. Таким образом, в свете того, что скрипт свойств будет целиком считываться в ОЗУ (= оперативное запоминающее устройство) в момент запуска, нам редко придётся обращаться к нему повторно в реальном времени, во время выполнения критически важного кода.

Рис. 1 Схема нашей системы скриптов
Рис. 1 Схема нашей системы скриптов

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

Мы уже разобрались в том, что собой представляет скрипт, и даже определили, какой именно вид скриптов будем использовать (скрипт свойств). Но, до настоящего момента, упустили одну важную деталь, не определив, как именно будут обрабатываться скрипты. Мы можем просто открыть текстовый редактор, выставить некоторые значения, сохранить файл и ожидать, что наша игра чудесным образом будет знать, как его считывать. Нам необходимо разработать пригодную к повторному применению (reusable), автоматизированную систему, которую сможем вызывать для чтения любого скрипта (их может быть несколько) и передачи нужных значений в нужное время (см. Рис. 1).
На Рис.1 видно, что наша система скриптов будет "венчать" систему менеджмента ресурсов. А всё потому, что скрипты - это те же самые ресурсы (более узко специализированные). В Главе 1.1 мы определяли понятие "ресурс" как нечто (вообще, это обычный файл), которое хранится на жёстком диске компьютера, имеет имя файла (filename) и путь (filepath). Ресурс может быть загружен и сохранён во временной памяти в период запуска игрового приложения. В будущем ты также увидишь, что наша система скриптов состоит из 2 компонентов: скриптов и переменных. Хотя, фактически система напрямую манипулирует только скриптами. Скрипты же, в свою очередь, манипулируют своими внутренними переменными. Поэтому для продолжения работы нам необходимо чётко выделить 2 этих компонента (или объекта) системы скриптов. Продолжив рассуждения и применив объектно-ориентированный подход, мы сразу предположим, что эти объекты будут двумя отдельными классами, которые нужно будет создать. Напомним, что наша система менеджмента ресурсов уже на месте и полностью интегрирована в движок в Главе 1.4(external link). Таким образом, если мы обеспечим код для загрузки и обработки скриптов, они будут прекрасно работать в качестве общих ресурсов, избавляя нас от необходимости создавать специализированный класс для управления (менеджмента) скриптами. Мы разберём эту тему более детально чуть позднее, сразу после того, как совершим небольшой экскурс по двум важнейшим темам объектно-ориентированного программирования, рассмотрев инкапсуляцию и автоматизацию.
Если присмотреться к рабочему процессу (Рис. 1), то можно увидеть, что сначала скрипт загружается системой скриптов, которая сама является экземпляром (инстансом) менеджера ресурсов, построенного с использованием шаблонов (templates). Класс скрипта будет вызван для загрузки скрипта и всех его внутренних переменных, которые, в свою очередь, станут переменными класса переменных. Сразу после загрузки скрипта, игра может запросить занчение любой из его переменных. С того момента, как скрипт был загружен, его значения доступны напрямую из временной памяти. Это даёт многократный прирост в скорости, по сравнению со считыванием данных с физического носителя через систему стандартного файлового ввода/вывода, которая, как известно, является "бутылочным горлышком" во время выполнении критически важного кода.
Мы также упомянули идею о том, чтобы сохранять в переменных различные типы значений (различных типов данных и даже комплексные типы данных). Для достижения этой цели мы присвоим каждой переменной специальный идентификатор типа (type identifier), который позволит идентифицировать тип данных, хранимых в той или иной переменной, чтобы таким образом точно знать как её загружать и обрабатывать. Очевидно, что необходимо обеспечить реализацию для каждого типа данных, но эта задача предельно проста.
Раз мы собираемся запускать скрипты через систему менеджмента ресурсов, нам не нужно беспокоиться, что происходит с памятью. Даже если ты загрузишь сотню скриптов, со множественными повторными загрузками (например, один и тот же скрипт будет загружен дважды), и затем упустишь их из виду, забыв закрыть половину из них, тебе не о чем волноваться. Менеджер ресурсов необычайно "снисходителен" и эффективно исправит все эти "промахи". Но для этого, повторимся, ты должен загружать все скрипты только через менеджер ресурсов. Если ты загружаешь их сам, независимо от менеджера ресурсов, то должен самостоятельно вовремя уничтожить их. Здесь мы немного забегаем вперёд. Ведь мы пока не реализовали систему скриптов. Сперва посмотрим на то, как наши скрипты будут составлены (composed).

Строение скрипта (script composition)

Кратко рассмотрим внутреннее строение (структуру) простейшего скрипта, который наша система сможет считывать. Текст скрипта вводится в обычный текстовый файл, который затем сохраняется и считывается системой скриптов движка. Точнее, система считывает переменные с присвоенными им значениями. Программер может затем получить доступ к этим переменным в коде, во время работы игрового приложения, и запросить их значения. Кроме этого, мы хотим иметь возможность оставлять комментарии (нет, не те, что в соцсетях) в наших скриптах, обеспечивая таким образом дополнительное описание переменных (при необходимости). Для достижения этой цели мы применим блоки переменных (variable blocks). Блок переменных - это обычная группа переменных, которую мы ограничим (выделим) как-нибудь, чтобы всё, что находится за её пределами игнорировалось системой. Мы определим границы блока переменных с помощью двух выражений: #begin и #end :

sample_config.ini
Этот текст находится за пределами блока переменных и поэтому будет игнорироваться.
#begin
Этот текст находится внутри блока переменных и поэтому будет считан системой.
#end

Это позволит нам размещать несколько блоков внутри одного скрипта, что сделает длинные скрипты более читабельными. Переменные при этом будут логически сгруппированы в блоки и снабжены необходимыми комментариями. Единственное ограничение - блок не может размещаться внутри другого блока (быть вложенным, ветвящимся и т.д.) и каждый блок должен открываться выражением #begin и закрываться выражением #end. Как в этом примере:

sample_config.ini
Первый блок.
#begin
Какие-то данные внутри этого блока.
#end

Второй блок.
#begin
Какие-то данные внутри этого блока.
#end

Другой важный аспект - это переменные, которые будут размещаться внутри блоков. Каждая переменная описывается строго определённым образом (форматом), где сначала идёт имя (name) переменной, затем её тип (type) и присвоенное значение (value):

sample_config.ini
Первый блок.
#begin
name type value
#end


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

===

Рис. 2 Сейчас Проект нашего движка выглядит так
Рис. 2 Сейчас Проект нашего движка выглядит так

Однажды реализованный и внедрёный в движок класс Script будет автоматически выполнять интерпретацию и загрузку данных из скриптов, не требуя вмешательства со стороны программера.
Сейчас в Проекте Engine всего 9 файлов: Engine.h, Engine.cpp, LinkedList.h, ResourceManagement.h, Geometry.h, State.h, State.cpp, Input.h и Input.cpp, которые мы создали в предыдущих главах (см. Рис. 2).

Создаём Scripting.h (Проект Engine)

Заголовочный файл Scripting.h будет содержать всего 2 класса: Variable (для работы с переменными различных типов) и Script (для работы с внешними файлами скриптов).
ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "Scripting.h".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле Scripting.h набираем следующий код:

Scripting.h (Проект Engine)
//-----------------------------------------------------------------------------
// File: Scripting.h
// Простенькая система скриптов. Скрипты представляют собой набор различных перемнных,
// сохранённых в текстовом файле.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef SCRIPTING_H
#define SCRIPTING_H

//-----------------------------------------------------------------------------
// Перечисление (Enumeration) типов переменных 
//-----------------------------------------------------------------------------
enum{ VARIABLE_BOOL, VARIABLE_COLOUR, VARIABLE_FLOAT, VARIABLE_NUMBER, VARIABLE_STRING, VARIABLE_VECTOR, VARIABLE_UNKNOWN };

//-----------------------------------------------------------------------------
// Класс Variable
//-----------------------------------------------------------------------------
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; // Тип данных, хранимых в перемнной.
	char *m_name; // Имя переменной.
	void *m_data; // Данные, хранящиеся в переменной.
};

//-----------------------------------------------------------------------------
// Класс Script
//-----------------------------------------------------------------------------
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) переменных в скрипте.
};

#endif

  • Сохрани Решение (Файл->Сохранить все).

Исследуем Scripting.h

В начале листинга видими перечисление (enumeration):

Фрагмент Scripting.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Перечисление (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:

Фрагмент Scripting.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Класс Variable
//-----------------------------------------------------------------------------
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; // Тип данных, хранимых в перемнной.
	char *m_name; // Имя переменной.
	void *m_data; // Данные, хранящиеся в переменной.
};
...

Здесь, в принципе, ничего сверъестественного, за исключением наличия двух конструкторов (с разным количеством параметров) и странного указателя FILE *file в первом конструкторе. *file является указателем на экземпляр структуры, которая содержит поток данных для открытого файла. Файл должен быть текстовым документом с расширением *.txt , в котором хранятся переменные скрипта с присвоенными им значениями. Очевидно, что данный (первый) конструктор вызывается в том случае, когда ты создаёшь (вернее, извлекаешь) скриптовые переменные из текстового файла скрипта. Параметр char *name представляет собой имя создаваемой переменной.
Второй конструктор применяется, когда файл скрипта создаётся "с нуля" (другими словами, когда он не существует). 2 экстра-параметра используюися в этом конструкторе для указания типа переменной и соответствующего ей значения. Очень важно, чтобы значение переменной имело правильный тип. Это означает, что если переменная, например, имеет тип string (строка), то в этом конструкторе необходимо передать параметр string. Позднее ты увидишь множество примеров его применения.
После деструктора видим 3 функции + 3 соответстующих переменных члена класса, которые хранят тип (*m_type), имя (*m_name) и данные (*m_data) переменной. Данные функции применяются для доступа к любой из этих переменных при необходимости. Например, когда необходимо узнать тип определённой переменной, достаточно вызвать функцию GetType(), которая затем вернёт один из типов, указанных в перечислении, рассмотренном ранее. Скорее всего самой часто используемой функцией будет GetData(), которая возвращает указатель void, содержащий адрес, по которому хранятся данные переменной. Указатель затем должен быть транслирован в один из трёх используемых указателей, что производится на основании типа запрошенной переменной. Весь процесс подробно описан в Scripting.cpp, созданием которого мы займёмся прямо сейчас.

Создаём Scripting.cpp (Проект Engine)

В файле исходного кода Scripting.cpp будут размещаться реализации функций, объявленных в Scripting.h.
ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи "Scripting.cpp".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле Scripting.cpp набираем следующий код:

Scripting.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// File: Scripting.cpp
// Реализация классов и функций, объявленных в Scripting.h .
// Обратись к интерфейсу Scripting.h за подробностями.
//
// Original SourceCode:
// 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 )
{
	// Сохраняем имя переменной
	m_name = new char[strlen( name ) + 1];
	strcpy( m_name, name );

	// Проверяем, что указатель на файл существует и верен.
	if( file == NULL )
		return;

	// Читаем тип переменной.
	char buffer[MAX_PATH];
	fscanf( file, "%s", buffer );
	if( strcmp( buffer, "bool" ) == 0 )
	{
		// Переменная булева типа (BOOL).
		m_type = 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 )
	{
		// Переменная является цветом.
		m_type = 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 )
	{
		// Переменная типа float (с плавающей точкой).
		m_type = VARIABLE_FLOAT;

		// Читаем и устанавливаем переменную типа 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 )
	{
		// Переменная является числом.
		m_type = VARIABLE_NUMBER;

		// Читаем и устанавливаем числовую (целочисленную) перемнную.
		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 )
	{
		// Переменная является строкой (тип string).
		m_type = VARIABLE_STRING;

		// Ищем открывающие обратные слэши (начало блока переменных).
		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 );
		}

		// Читаем и устанавливаем переменную типа string (строка).
		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 )
	{
		// Переменная является вектором (тип vector).
		m_type = VARIABLE_VECTOR;

		// Читаем и устанавливаем переменную типа 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
	{
		// Переменная неизвестного типа (unknown).
		m_type = VARIABLE_UNKNOWN;

		// Читаем и устанавливаем данные (также как для типа string) для переменной.
		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 )
{
	// Сохраняем имя переменной.
	m_name = new char[strlen( name ) + 1];
	strcpy( m_name, name );

	// Сохраняем тип переменной.
	m_type = 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 );
}

//-----------------------------------------------------------------------------
// Возвращает тип переменной.
//-----------------------------------------------------------------------------
char Variable::GetType()
{
	return m_type;
}

//-----------------------------------------------------------------------------
// Возвращает имя переменной.
//-----------------------------------------------------------------------------
char *Variable::GetName()
{
	return m_name;
}

//-----------------------------------------------------------------------------
// Возвращает данные, хранящиеся в переменной.
//-----------------------------------------------------------------------------
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 )
{
	// Создаём связный список (linked list) в котором будут храниться все переменные скрипта.
	m_variables = new LinkedList< Variable >;

	// Открываем скрипт, указав имя его файла (filename).
	FILE *file = NULL;
	if( ( file = fopen( GetFilename(), "r" ) ) == NULL )
		return;

	// Продолжаем чтение файла, пока не достигнем eof (end of file = конец файла).
	bool read = false;
	char buffer[MAX_PATH];
	fscanf( file, "%s", buffer );
	while( feof( file ) == 0 )
	{
		// Проверяем, находится ли индикатор позиции файла между тегами #begin и #end.
		// Если да, то читаем данные в связный список переменных.
		if( read == true )
		{
			// Останавливаем чтение данных, если встретили тег #end.
			if( strcmp( buffer, "#end" ) == 0 )
				read = false;
			else
				m_variables->Add( new Variable( buffer, file ) );
		}
		else if( strcmp( buffer, "#begin" ) == 0 )
			read = true;

		// Читаем следующую строку.
		fscanf( file, "%s", buffer );
	}

	// Закрываем файл скрипта.
	fclose( file );
}

//-----------------------------------------------------------------------------
// The script class destructor.
//-----------------------------------------------------------------------------
Script::~Script()
{
	SAFE_DELETE( m_variables );
}

//-----------------------------------------------------------------------------
// Добавляет новую переменную в скрипт.
//-----------------------------------------------------------------------------
void Script::AddVariable( char *name, char type, void *value )
{
	m_variables->Add( new Variable( name, type, value ) );
}

//-----------------------------------------------------------------------------
// Устанавливает значение существующей переменной в скрипте.
//-----------------------------------------------------------------------------
void Script::SetVariable( char *name, void *value )
{
	// Ищем переменную.
	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;
		}
	}

	// Проверяем, если переменная не была найдена.
	if( variable == NULL )
		return;

	// Получаем тип переменной.
	char type = variable->GetType();

	// Уничтожаем переменную.
	m_variables->Remove( &variable );

	// Добавляем переменную на прежнее место с новым значением.
	AddVariable( name, type, value );
}

//-----------------------------------------------------------------------------
// Сохраняет скрипт в файл.
//-----------------------------------------------------------------------------
void Script::SaveScript( char *filename )
{
	FILE *file = NULL;
	char output[MAX_PATH];

	// Открываем указанное имя файла, если таковое имеется. В противном случае используем внутреннее имя файла.
	if( filename != NULL )
	{
		if( ( file = fopen( filename, "w" ) ) == NULL )
			return;
	}
	else
	{
		if( ( file = fopen( GetFilename(), "w" ) ) == NULL )
			return;
	}

	// Пишем тег #begin в файл.
	fputs( "#begin\n", 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;
		}
	}

	// Пишем тег #end в файл.
	fputs( "#end", file );

	// Закрываем файл скрипта.
	fclose( file );
}

//-----------------------------------------------------------------------------
// Возвращает данные булева типа из указанной переменной.
//-----------------------------------------------------------------------------
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;
}

//-----------------------------------------------------------------------------
// Возвращает данные типа цвет (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;
}

//-----------------------------------------------------------------------------
// Возвращает данные типа 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;
}

//-----------------------------------------------------------------------------
// Возвращает данные целочисленного типа (number) из указанной переменной.
//-----------------------------------------------------------------------------
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;
}

//-----------------------------------------------------------------------------
// Возвращает строку (тип string) из указанной переменной.
//-----------------------------------------------------------------------------
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;
}

//-----------------------------------------------------------------------------
// Возвращает данные о цвете (тип colour) из указанной переменной.
//-----------------------------------------------------------------------------
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;
}

//-----------------------------------------------------------------------------
// Возвращает данные неизвестного типа (unknown) из указанной переменной.
//-----------------------------------------------------------------------------
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;
}

  • Сохрани Решение (Файл->Сохранить все).

Реализация класса Variable

Реализация класса Variable размещена в самом начале Scripting.cpp, так как его будет использовать класс Script, размещённый чуть ниже. Согласись, будет неправильно писать реализацию класса Script, у которого не готова ни одна переменная, с которой он будет работать. Напомним, что у нас 2 конструктора, которые создают экземпляры класса Variable с разным набором параметров. Наиболее интересным является первый конструктор, размещённый в самом начале Scripting.cpp :

Фрагмент Scripting.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// The variable class constructor.
//-----------------------------------------------------------------------------
Variable::Variable( char *name, FILE *file )
{
	// Сохраняем имя переменной
	m_name = new char[strlen( name ) + 1];
	strcpy( m_name, name );

	// Проверяем, что указатель на файл существует и верен.
	if( file == NULL )
		return;

	// Читаем тип переменной.
	char buffer[MAX_PATH];
	fscanf( file, "%s", buffer );
	if( strcmp( buffer, "bool" ) == 0 )
	{
		// Переменная булева типа (BOOL).
		m_type = 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 )
	{
		// Переменная является цветом.
		m_type = 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 )
	{
		// Переменная типа float (с плавающей точкой).
		m_type = VARIABLE_FLOAT;

		// Читаем и устанавливаем переменную типа 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 )
	{
		// Переменная является числом.
		m_type = VARIABLE_NUMBER;

		// Читаем и устанавливаем числовую (целочисленную) перемнную.
		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 )
	{
		// Переменная является строкой (тип string).
		m_type = VARIABLE_STRING;

		// Ищем открывающие обратные слэши (начало блока переменных).
		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 );
		}

		// Читаем и устанавливаем переменную типа string (строка).
		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 )
	{
		// Переменная является вектором (тип vector).
		m_type = VARIABLE_VECTOR;

		// Читаем и устанавливаем переменную типа 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
	{
		// Переменная неизвестного типа (unknown).
		m_type = VARIABLE_UNKNOWN;

		// Читаем и устанавливаем данные (также как для типа string) для переменной.
		fscanf( file, "%s", buffer );
		m_data = new char[strlen( buffer ) + 1];
		strcpy( (char*)m_data, buffer );
	}
}
...


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

Инкапсуляция и автоматизация (теоретическое отступление)

Что происходит? Где класс Script? По подзаголовку, ты, наверное, догадался, что мы не будем сейчас переходить к классу Script. Вместо этого, мы совершим небольшой теоретический экскурс в объектно-ориентированное программирование. Но остаётся вопрос "Зачем? Ведь, здесь не совсем подходящее место для этого." Ведь до этого мы говорили о скриптинге, а не об объектно-ориентированном программировании. Проблема в том, что если бы мы разместили этот материал в самом начале курса, его будет трудно понять, не имея перед глазами реального кода в качестве примеров. Очень удобно, что класс Script, который мы обязательно рассмотрим, полностью соответствует данной теме. Поэтому имеет смысл рассмотреть эти теории именно сейчас, когда у нас перед глазами реальные примеры с исходным кодом по теме. Данная тема во многом перекликается с исходным кодом системы скриптов и релевантна ей.

Рис. 3 Инкапсуляция и автоматизация в деле
Рис. 3 Инкапсуляция и автоматизация в деле

Инкапсуляция и автоматизация - 2 очень важных аспекта программирования, которые сделают твою жизнь намного проще. Они идут "рука за руку" с такими чудесными словами, как абстракция и полиморфизм. Все эти термины являются именами различных форм техник программирования, которые специально разработаны для упрощения исходного кода, что, в свою очередь, облегчает его обслуживание и последующее применение в других проектах. Вспомни все эти странные слова, о которых мы говорили в Главе 1.1: пригодность к обслуживанию (maintability), к повторному использованию (reusability) и удобство применения (usability). Сейчас эти слова уже начинают что-то значить.
Рассмотрим ряд определений, которые перекликаются с нашей ситуацией. Икапсуляция означает, что ты комбинируешь функционал для создания компонента более высокого уровня (это и называют абстракцией), который выполняет ту же самую задачу, независимо от других блоков кода (т.е. каждый компонент является своего рода "капсулой", довольно сильно изолированной от других компонентов). Другими словами, компонент самодостаточен, он не полагается на внешнюю помощь для выполнения своих обязанностей. Также внутри компонента размещаются данные, которые он использует. Это очень важный момент, так как создаваемые классы базируются на данных, которыми они манипулируют. Хотя, это и неявляется основной причиной того, почему классы в объектно-ориентированном программировании работают так как положено. Суть заключается в том, что ты комбинируешь функционал определённого объекта в класс, может работать абсолютно независимо, получая ввод и выдавая тот вывод, для которого он и был разработан. Здесь мы напрямую выходим к теме автоматизации, где ты создаёшь объекты, которые при комбинировании могут производить результат высокого уровня через серию низкоуровневых операций ввода/вывода, а также процессов. Итак, ты уже видишь преимущества автоматизации, когда помещаешь в очередь (в стек) несколько объектов один над другим, у каждого из которых своя строго определённая роль. В комбинации они работают вместе, для одного общего дела, что само по себе является другим примером инкапсуляции, только на более высоком уровне. Рис. 3 наглядно иллюстрирует весь этот жаргон.

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

Рис. 4 Разбиваем автомобиль на классы
Рис. 4 Разбиваем автомобиль на классы
Рис. 5 Инкапс. объект движется благодаря автоматизации
Рис. 5 Инкапс. объект движется благодаря автоматизации

Так если мы рассмотрим двигатель автомобиля, то увидим, что он использует топливо, воздух, масло, воду, педаль газа и тормоза в качестве "вводных данных" для производства выходного продукта - энергии для вращения приводных валов (или карданного вала, если это "Жигули-классика";-). Важный момент здесь это то, что двигатель, в принципе, самодостаточен и ему не нужна никакая другая деталь автомобиля для манипулирования этим типом данных. Поэтому мы можем сделать вывод о том, что двигатель является отдельным компонентом, который может быть инкапсулирован. Это означает, что вместе с ним также инкапсулируются и "данные" (топливо, воздух и т.д.), с которыми он работает. При желании, по такому же принципу можно выделить другие компоненты автомобиля, идентифицируя каждый агрегат по уникальному типу "данных", с которыми он работает. Перед выделением объекта обязательно убедись, что соответствующие данные не используются другими объектами. И наоборот, если компонент не использует схожие данные с другим компонентом, то и объединять их не стоит. Это тот самый момент теории, который нужно запомнить, чтобы обезопасить себя от "детских" ошибок, как например комбинирование амортизаторов с двигателем или тормозов с трансмиссией лишь на том основании, что они тоже используются для приведение авто в движение. Вывод всему вышесказанному такой: всегда инкапсулируй объекты, основываясь на данных, с которыми они работают, а не на их функционале (см. Рис. 4).

Автоматизация является фундаментальным понятием при разработке программного обеспечения, особенно приложений с интенсивной логикой, какими являются компьютерные игры. Рассмотрим эту концепцию на всё том же автомобиле и посмотрим, как автоматизация может здорово упростить процесс приведения его в движение. Первый шаг - применить акселерацию, используя органы управления авто (педаль газа), которые дают "ввод" двигателю, давая команду увеличить обороты, что, в свою очередь, создаст "вывод" энергии. Эта энергия проходит через трансмиссию и передаётся приводным валам, которые затем передают эту энергию колёсам. Таким образом вывод двигателя становится вводом для трансмиссии, которая даёт свой вывод, который, в свою очередь, становится вводом для колёс. А сейчас, если мы рассмотрим автомобиль в качестве законченной системы (т.е. мы инкапсулируем все компоненты автомобиля в единый класс более высокого уровня), мы можем с помощью всего одного ввода (нажатие педали газа) привести в движение весь автомобиль. И происходит это именно за счёт автоматизации, суть которой заключается в том, что каждый компонент выполняет свою роль для одной общей цели - получения конечного результата. Так какой же из компонентов автомобиля производит итоговый вывод, приводящий его в движение? Все. Каждый из них, будучи скомбинированным друг с другом, производит общий вывод. Другими словами, мы имеем набор самодостаточных объектов, которые получают ввод, обрабатывают его и дают соответствующий вывод, который, в свою очередь, становится вводом для другого объекта этой взаимосвязанной цепи. Этот процесс продолжается без перерыва, а самое главное - без внешнего вмешательства. Нам не нужно внедрять какие-либо компоненты для помощи из вне, так как весь процесс автоматизирован (см. Рис.5).
Но самое удивительное здесь другое. Так как каждый субобъект автомобиля инкапсулирован, то в этом случае все они могут быть модифицированы или заменены без ущерба для других компонентов. Например, ты можешь заменить колёса и ожидать при этом точно такой же производительности от движка. Ты также можешь модифицировать двигатель и ожидать, что транмиссия будет работать в том же режиме, что и раньше. Единственное ограничение - это интерфейсы между компонентами. К примеру, ты не можешь заменить двигатель, если его стыковочный узел с трансмиссией отличается от прежнего. Главное правило остаётся следующим. Если ввод (полученный от другого компонента) удовлетворяет данному компоненту и производит корректный вывод, то нутри него ты можешь делать всё, что захочешь.
Лишь в случае крайней необходимости ты можешь изменить интерфейс. В любом случае делать это крайне не рекомендуется. Для автомобиля это, может, и не представляет особой проблемы. А при разработке программного обеспечения очень даже представляет. Причина этого заключается в том, что когда ты создаёшь новую версию соответствующего компонента и изменяешь интерфейс, то в этом случае ты потенциально разрушаешь другие комопненты прежних версий, которые пытаются использовать новый интерфейс. Вообще, считается хорошей идеей и признаком хорошего тона продолжать поддержку старых интерфейсов. В программировании это называют обеспечением обратной совместимости (backwards compatibility). Недаром DirectX активно использует это свойство. И игра, (правильно) спрограммированная под DirectX 8.0, без труда запустится в ОС с установленным DirectX 11 или более поздними версиями.

Объявление класса Script (Проект Engine)

Вернёмся к нашей главной теме. Сейчас ты увидишь, как всё вышеизложенное мы применим в деле. До этого мы бегло просмотрели класс Variable, получив достаточно информации для перехода к классу Script. Ты уже знаешь, что класс Script использует класс Variable для своих нужд. Рассмотрим объявление класса Script в Scripting.h:

Scripting.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Класс Script
//-----------------------------------------------------------------------------
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) переменных в скрипте.
};
...

Как видишь, класс Script просто наследуется от класса-родителя Resource. Так как скрипт обладает всеми свойствами расширенного ресурса, у нас нет причин, чтобы не включить поддержку скриптов в нашу систему менеджмента ресурсов. Напомним, что наш класс Resource на самом деле является шаблоном и представляет собой "заготовку" для создания классов нежели базовый класс. Это означает, что при создании класса Resource (в соответствии с шаблоном) мы бязательно должны указать используемый тип ресурса. Это делается для того, чтобы компилятор знал, каким образом строить данный класс. В нашем случае при указании шаблонного класса, в угловых скобках мы указываем тип Script.

Scripting.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Класс Script
//-----------------------------------------------------------------------------
class Script : public Resource< Script >
...

Класс Script содержит в себе целую подборку функций для работы с переменными, которые хранятся в связном списке с именем m_variables. Напомним, что реализация класса LinkedList тоже является шаблоным класом, и значит при создании класса на его основе мы должны указать в угловых скобках тип (или вид хранимых элементов). В нашем случае в самом конце объявления класса Script мы создаём связный список переменных и в угловых скобках в качестве типа хранимых элементов укажем ранее созданный класс Variable:

Scripting.h (Проект Engine)
...
private:
	LinkedList< Variable > *m_variables; // Связный список (Linked list) переменных в скрипте.
...
Рис. 6 Схема системы скриптов с исп. инкапс. и автоматизации
Рис. 6 Схема системы скриптов с исп. инкапс. и автоматизации
Рис. 7 Система скриптов производит вывод
Рис. 7 Система скриптов производит вывод

Таким образом мы создадим класс LinkesList с типом Variable, где в упорядоченном виде будут храниться переменные каждого скрипта. Если ты ещё не догадался, именно здесь мы и применяем инкапсуляцию и автоматизацию, рассмотренные выше. Посмотрим как эти две концепции работают на реальном примере, а не на абстрактном автомобиле.
Автоматизация является фундаментальным понятием при проектировании программного обеспечения, особенно когда дело касается приложений с интенсивной логикой, какими являются игры. Представь, если вдруг мы попытаемся применить наш класс Script, который самостоятельнообрабатывал все переменные скриптв, которые хранятся в созданном вручную связном списке, который также управляется классом Script. При таком подходе класс Script очень скоро станет очень запутанным, плохо управляемым и чреватым ошибками, что противорречит нашей цели о пригодности к обслуживанию (maintability) нашего движка. Вместо этого мы создали серию инкапсулированных классов, каждый из которых выполняет одну определённую функцию, причём без посторонней помощи. При таком подходе мы можем безопасно строить классы, как например наш Script класс, построенный "поверх" системы менеджмента ресурсов и технологии связных списков, в котором всё чётко и ясно прописано и который будет работать именно так, как задумано. И даже если что-то в работе этой связки пойдёт не так, процесс отладки (debugging) и устранения ошибок при таком подходе выполняется намного проще. Ведь вместо того, чтобы просто сказать, что где-то внутри класса Script есть проблема, мы можем сузить область поиска, выяснив в каком именно подклассе мог произойти сбой. Это означает, что мы имеем дело с более простым кодом, даже несмотря на тот факт, что скомбинированные друг с другом классы производят больший, более сложный вывод.

Напомним ещё раз, что всегда очень важно разделять компоненты и классы, основываясь на данных, а не на функционале. Каждый класс инкапсулируется именно вокруг тех данных, которые он обрабатывает, а не вокруг функционала, который он предоставляет. Если взглянуть на наш класс Script, то легко заметить, что он использует инкапсулированный класс Variable для управления каждой переменной внутри класса, вместо того, чтобы пытаться манипулировать всеми переменными самостоятельно, с помощью своих внутренних функций. Так зачем же мы разделили систему скриптов на два отдельных класса (Script и Variable)? Класс Variable имеет дело исключительно с данными переменных, тогда как класс Script эти данные мало интересуют. Класс Script вообще не интересует, что находится внутри той или иной переменной или какого она типа, или даже какое у неё имя. Если классу Script потребуются эти сведения, он просто запросит их у соответствующего экземпляра класса Variable. Более того, у нас есть классы Resource и LinkedList, которые заботятся лишь о хранимых данных, которыми здесь являются имя файла скрипта и количество переменных соответственно.
И ещё раз, если скрипту потребуются какие-либо подробные сведения о той или иной переменной, он может просто запросить переменную из связного списка и затем запросить у данной переменной всю необходимую информацию. На Рис. 6 показан проект системы скриптов (scripting system) с применением инкапсуляции и автоматизации.
Вся эта схема является системой скриптов и позволяет применять её как инкапсулированный компонент примерно таким же образом, как мы это делали в примере с гипотетическим автомобилем. Мы можем посылать команды в класс Script или запрашивать информацию у него и ожидать, что он будет работать определённым образом и выдавать определённый результат, даже несмотря на то, что далеко не всю работу он проделывает самостоятельно. Все другие компоненты, котрые использует класс Script, работают совместно (благодаря автоматизации) выдавая один общий результат (см. Рис. 7).

Разработка кода с использованием данных концепций значительно приближает нас к достижению всех трёх целей дизайн-проект нашего движка, изложенных в Главе 1.1. Во-первых, исходный код в этом случае проще обслуживать (maintability), так как он состоит из нескольких классов, специфичных для каждого вида данных. Это позволяет намного проще находить проблематичные участки кода. Во-вторых, мы можем быстрее достичь цели повторного использования кода (reusability). Вместо того, чтобы разрабатывать компоненты, которые полагаются один на другой (так как они работают с одними и теми же данными), мы создали инкапсулированные классы, которые не зависят от типа данных и которые можно легко изменять и заменять при необходимости. Например, мы можем полностью заменить реализацию классов Variable, Resource или LinkedList не изменив ни единой строки кода в классе Script (до тех пор, пока не изменены интерфейсы). А всё потому, что они сгруппированы именно вокруг данных, с которыми они работают, а не на основе функционала, который они предоставляют, будучи скомбинированными. Если бы мы "слепили" весь этот функционал вместе, то в этом случае было бы затруднительно изолировать, заменять или исправлять в любом из компонентов. В третьих, ты должно быть заметил, что степень удобства использования (usability) также значительно увеличилась. В основном из-за того, что множество хитроспелетений внутрисистемных операций могут быть спрятаны, позволяя программеру использовать систему скриптов как единый модуль. Программеру необходимо лишь изучить данные для ввода и структуру результата, который он получит при выводе. При этом он может пропустить всё, что находится посередине (внутри системы скриптов).

Реализация класса Script (Проект Engine)

Вернёмся к нашей теме, продолжив обсуждать класс Script. Обратившись к его реализации, размещённой в Scripting.cpp, мы более подробно рассмотрим функции, которые он содержит. Начнём с конструктора.

Фрагмент файла Scripting.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// The script class constructor.
//-----------------------------------------------------------------------------
Script::Script( char *name, char *path ) : Resource< Script >( name, path )
{
	// Создаём связный список (linked list) в котором будут храниться все переменные скрипта.
	m_variables = new LinkedList< Variable >;

	// Открываем скрипт, указав имя его файла (filename).
	FILE *file = NULL;
	if( ( file = fopen( GetFilename(), "r" ) ) == NULL )
		return;

	// Продолжаем чтение файла, пока не достигнем eof (end of file = конец файла).
	bool read = false;
	char buffer[MAX_PATH];
	fscanf( file, "%s", buffer );
	while( feof( file ) == 0 )
	{
		// Проверяем, находится ли индикатор позиции файла между тегами #begin и #end.
		// Если да, то читаем данные в связный список переменных.
		if( read == true )
		{
			// Останавливаем чтение данных, если встретили тег #end.
			if( strcmp( buffer, "#end" ) == 0 )
				read = false;
			else
				m_variables->Add( new Variable( buffer, file ) );
		}
		else if( strcmp( buffer, "#begin" ) == 0 )
			read = true;

		// Читаем следующую строку.
		fscanf( file, "%s", buffer );
	}

	// Закрываем файл скрипта.
	fclose( file );
}
...

Конструктор стартует с принятия имени и пути загрузки файла скрипта:

Фрагмент файла Scripting.cpp (Проект Engine)
...
Script::Script( char *name, char *path ) : Resource< Script >( name, path )
...

Это обычное дело при загрузке любого физического ресурса, так как эти два параметра необходимы для системы менеджмента ресурсов. Как можем видеть, они тут же передаются конструктору класса Resource, так как класс Script наследуется от класса Resource. Запомни, что именно это наследование позволяет управлять скриптами системе менеджмента ресурсов, что в свою очередь является ключевым фактором при контроле используемой памяти, так как предотвращает загрузку нескольких копий одного и того же скрипта. Это также позволяет быть увереным в том, что при закрытии приложения все ранее загруженные в память скрипты будут удалены из неё, предотвратив таким образом т.н. "утечки памяти" (memory leaks).

Вслед за этим, создаём экземпляр класса LinkedList. Таким образом мы создаём связный список, в котором будут храниться все переменные нашего скрипта. Так как класс LinkedList является шаблоном, то при создании его экземпляра мы обязательно должны указать тип хранимых элементов. В нашем случае указываем класс Variable. Следующий шаг - предпринять попытку открыть дескриптор текстового файла скрипта, который указан в параметрах конструктора:

Фрагмент файла Scripting.cpp (Проект Engine)
...
Script::Script( char *name, char *path ) : Resource< Script >( name, path )
{
	// Создаём связный список (linked list) в котором будут храниться все переменные скрипта.
	m_variables = new LinkedList< Variable >;

	// Открываем скрипт, указав имя его файла (filename).
	FILE *file = NULL;
	if( ( file = fopen( GetFilename(), "r" ) ) == NULL )
		return;
...

FILE *file - это структура, которая будет хранить детали потока (stream) открытого файла.
fopen - стандартная функция файлового ввода/вывода (описана в stdio.h), которая открывает файл. Вот её прототип:

Прототип функции fopen
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) и рассмотрим метод чтения переменных из скрипта:

Фрагмент файла Scripting.cpp (Проект Engine)
...
	// Продолжаем чтение файла, пока не достигнем eof (end of file = конец файла).
	bool read = false;
	char buffer[MAX_PATH];
	fscanf( file, "%s", buffer );
	while( feof( file ) == 0 )
	{
...

Сперва мы объявляем переменную read типа bool, с помощью которой будем определять, какие участки скрипта считывать, а какие нет. Не забываем, что помимо блоков переменных наш скрипт может содержать, например, комментарии, которые, очевидно, считывать не нужно.
Далее мы создаём переменную buffer типа char. По сути это массив символов, который мы будем использовать для чтения слов (в нашем случае это строки) скрипта, по одной за раз.
Далее используем функцию fscanf, которая позволяет считывать форматированные данные из потока. Вот её прототип:

Прототип функции fscanf
int fscanf( FILE *stream, const char *format [, argument ]... );

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

Формат Описание
Однобайтовый символ (single byte character).
%d Десятичное числовое значение.
%f Числовое значение с плавающей точкой (floating point value).
%s Строка, оканчивающаяся там, где встречается первый символ пробела.

Нас интересует чтение по одной строке за раз, поэтому мы используем формат %s.
В последнем необязательном параметре мы передаём указатель на наш массив buffer, который по завершении работы функции fscanf пополнится новой строкой, найденной в потоке.
Далее мы входим в цикл while, который последовательно проверяет поток file, используя условие feof, который возвращает ненулевое значение, когда достигнут конец файла (end of file - eof). В этом случае выполнение цикла прерывается, так как мы достигли конца файла скрипта и считывать больше нечего.

Внутри цикла видим несколько операторов if else:

Фрагмент файла Scripting.cpp (Проект Engine)
...
		// Проверяем, находится ли индикатор позиции файла между тегами #begin и #end.
		// Если да, то читаем данные в связный список переменных.
		if( read == true )
		{
			// Останавливаем чтение данных, если встретили тег #end.
			if( strcmp( buffer, "#end" ) == 0 )
				read = false;
			else
				m_variables->Add( new Variable( buffer, file ) );
		}
...

Здесь проверяем, собираемся ли мы считывать переменную из скрипта. Если да, то проверяем буфер (массив buffer) на окончание строк с блоком переменных. Для этого применяется функция strcmp (от англ. "string compare" - сравнение строк), которая проверяет, не содержит ли наш буфер тег #end. Данная функция просто сравнивает две строки и возвращает 0, если они одинаковы, собственно что нам и требуется. Если мы достигаем конца блока переменных, нам нужно просигнализировать, что мы больше не ищем переменные. По крайней мере, пока не достигнем следующего тега #begin, сигнализирующего о начале нового блока переменных. В остальных случаях, до тех пор, пока не будет обнаружен тег #end, то в этом случае очевидно, что мы нашли новую переменную.

Когда переменная найдена, мы используем функцию Add из класса LinkedList для добавления новой переменной в конец связного списка m_variables. Мы передаём указатель на буфер (buffer) и файл (file) в конструктор новой переменной, чтобы таким образом класс Variable мог вызвать сам себя с переданными в него параметрами. По завершении работы функции Add, индикатор позиционирования (проще говоря, курсор) потока file будет перемещён в конец данной строки вида "переменная-значение", готовясь начать чтение новой строки. А всё потому, что конструктор класса Variable считывает соответствующие символы из потока file по порядку, чтобы затем извлечь из неё тройку "имя + тип переменной + значение".

В том случае, когда мы не собираемся считывать переменные, это означает, что мы находимся за пределами блока переменных (т.е. не между тегами #begin ... #end). Поэтому нам необходимо проверить, не вошли ли мы в него. Мы делаем это снова применив функцию strcmp, которая в этот раз ищет тег #begin:

Фрагмент файла Scripting.cpp (Проект Engine)
...
		else if( strcmp( buffer, "#begin" ) == 0 )
			read = true;
...

При обнаружении тега #begin, сигнализируем о готовности войти в блок переменных и начать чтение, установив переменную read в true.

Последний шаг внутри общего цикла while - это дать команду на считывание следующей строки из потока file:

Фрагмент файла Scripting.cpp (Проект Engine)
...
		// Читаем следующую строку.
		fscanf( file, "%s", buffer );
	}

	// Закрываем файл скрипта.
	fclose( file );
}
...

Цикл затем возвращается в начало и вся процедура повторяется снова. Выполнение цикла будет продолжаться до тех пор, пока не будет достигнут специальный маркер eof (end of file). В этом случае цикл прерывается. Как только цикл прервался, скрипт считается полностью загруженным в память и мы, наконец, вызываем функцию fclose для закрытия потока file.

В Scripting.h при объявлении класса Script были указаны ещё 3 функции: AddVariable, SetVariable и SaveScript. Они применяются для создания скриптов, что называется "на лету", то есть во время выполнения приложения, что мы будем делать очень редко. Их реализации можно найти в Scripting.cpp, сразу после деструктора класса Script.
Функция AddVariable добавляет новую переменную в скрипт, с обязательным указанием её соответствующего значения.
Функция SetVariable изменяет значение существующей переменной в скрипте.
Функция SaveScript записывает скрипт из памяти в указанный текстовый файл.
Эти функции служат в основном для удобства и позднее ты можешь применять их в своём коде при разработке собственных приложений.

Оставшиеся функции применяются для получения значения любой из переменных внутри скрипта. Так как все они очень похожи и отличаются лишь возвращаемым результатом, мы рассмотрим лишь одну из них - GetNumberData. Её мы будем применять особенно часто.

Фрагмент файла Scripting.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Возвращает данные целочисленного типа (number) из указанной переменной.
//-----------------------------------------------------------------------------
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. В данный момент от тебя не требуется полного понимания того, как всё это работает. По крайней мере, пока не соберёшься изменять компоненты системы или добавлять новые. Но необходимо знать, как применять систему скриптов в реальном коде. Об этом и пойдёт речь далее.

Интегрируем систему скриптов в движок

Принцип тот же, что и при интеграции системы пользовательского ввода.

Изменения в Scripting.cpp (Проект Engine)

  • Добавь инструкцию #include "Engine.h" в самом начале файла Scripting.cpp (проверь её наличие).

Изменения в Engine.h (Проект Engine)

  • Добавь инструкцию #include "Scripting.h" в файл Engine.h, сразу после инструкции #include "Geometry.h":
Фрагмент Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// 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:

ResourceManager< Script > *m_scriptManager;

  • Добавь функцию GetScriptManager в объявлении класса Engine, в секции public:

ResourceManager< Script > *GetScriptManager();

Фрагмент Engine.h (Проект 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; // Флаг показывает, загружен ли движок или нет.
	HWND m_window; // Дескриптор главного окна.
	bool m_deactive; // Флаг показывает, активно приложение или нет.

	EngineSetup *m_setup; // Копия (инстанс) структуры EngineSetup.

	LinkedList< State > *m_states; // Связный список (Linked list) стейтов.
	State *m_currentState; // Указатель на текущий стейт.
	bool m_stateChanged; // Флаг показывает, изменён ли стейт в текущем кадре.

	ResourceManager< Script > *m_scriptManager; // Менеджер скриптов.

	Input *m_input; // Input object.
};
...

Функция GetScriptManager позволяет получить доступ к менеджеру скриптов из других частей Проекта Engine. Напоминаем, что класс Resource является шаблоном, поэтому при создании его экземпляра обязательно указываем в угловых скобках тип ресурса. В нашем случае это Script.

Изменения в Engine.cpp (Проект Engine)

  • Добавь следующую строку в конструктор класса Engine:

m_scriptManager = new ResourceManager< Script >;
cразу после строки m_currentState = NULL:

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Конструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::Engine( EngineSetup *setup )
{
	// Указываем, что движок ещё не загружен.
	m_loaded = false;

	// Если никакой структуры EngineSetup не передаётся, обязательно создадим её.
	// Иначе, копируем имя передаваемой структуры.
	m_setup = new EngineSetup;
	if( setup != NULL )
		memcpy( m_setup, setup, sizeof( EngineSetup ) );

	// Сохраняем указатель движка в глобальной переменной для более простого и быстрого доступа.
	g_engine = this;

	// Подготавливаем оконный класс и регистрируем его.
	WNDCLASSEX wcex;
	wcex.cbSize        = sizeof( WNDCLASSEX );
	wcex.style         = CS_CLASSDC;
	wcex.lpfnWndProc   = WindowProc;
	wcex.cbClsExtra    = 0;
	wcex.cbWndExtra    = 0;
	wcex.hInstance     = m_setup->instance;
	wcex.hIcon         = LoadIcon( NULL, IDI_APPLICATION );
	wcex.hCursor       = LoadCursor( NULL, IDC_ARROW );
	wcex.hbrBackground = NULL;
	wcex.lpszMenuName  = NULL;
	wcex.lpszClassName = "WindowClass";
	wcex.hIconSm       = LoadIcon( NULL, IDI_APPLICATION );
	RegisterClassEx( &wcex );

	// Инициализируем COM, используя многопоточное взаимодействие.
	CoInitializeEx( NULL, COINIT_MULTITHREADED );

	// Создаём окно и возвращаем его дескриптор.
// Note: Позднее окно будет создаваться с флагом windowed/fullscreen (оконный/полноэкранный режимы).
	m_window = CreateWindowEx( WS_EX_TOPMOST, "WindowClass", (LPCWSTR)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 );

	// Создаём генератор случайных чисел на основе текущего времени.
	srand( timeGetTime() );

	// Позволяет приложению произвести настройку всех необходимых стейтов.
	if( m_setup->StateSetup != NULL )
		m_setup->StateSetup();

	// Движок полностью загружен и готов к работе.
	m_loaded = true;
}
...

  • Добавь следующую строку в деструктор класса Engine:

SAFE_DELETE( m_scriptManager );

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Деструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::~Engine()
{
	// Проверяем, что движок загружен.
	if( m_loaded == true )
	{
		// Всё, что здесь, будет уничтожаться (например, более неиспользуемые DirectX компоненты).

		// Уничтожаем связные списки со стейтами.
		if( m_currentState != NULL )
			m_currentState->Close();
		SAFE_DELETE( m_states );

		// Уничтожаем ранее созданные объекты.
		SAFE_DELETE( m_input );
		SAFE_DELETE( m_scriptManager );
	}

	// Деинициализация COM.
	CoUninitialize();

	// Unregister the window class.
	UnregisterClass( "WindowClass", m_setup->instance );

	// Уничтожение структуры engine setup.
	SAFE_DELETE( m_setup );
}
...

  • Добавь реализацию функции GetScriptManager:

ResourceManager< Script > *Engine::GetScriptManager()
{
return m_scriptManager;
}
сразу после реализации функции State *Engine::GetCurrentState() в самом конце Engine.cpp:

Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Возвращает указатель на текущий стейт.
//-----------------------------------------------------------------------------
State *Engine::GetCurrentState()
{
	return m_currentState;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на текущий менеджер скриптов.
//-----------------------------------------------------------------------------
ResourceManager< Script > *Engine::GetScriptManager()
{
	return m_scriptManager;
}
...

  • Сохрани Решение (Файл->Сохранить все).


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

Тестовая перекомпиляция Engine.lib

Итак, система скриптов полностью интегрирована в движок.
Для проверки работосопособности исходного кода, добавленного в этой Главе, перекомпилируем исходный код Проекта Engine:

  • В Обозревателе решений щёлкаем правой кнопкой мыши по значку Проекта Engine. Во всплывающем меню выбираем "Перестроить" (применяется в случае, когда код уже был успешно скомпилирован ранее).

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

Закрыть
noteОбрати внимание...

Напомним о том, что следует отличать ошибки (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  000JHM-935TKY-473HUR
position  vector  20.5 79.938 -4.0
WORKING  bool  true
#end

В данном скрипте можно отметить несколько интересных моментов. Во-первых, переменные MyStory и myStory уникальны, так как отличаются регистром символа в имени. Да, имена переменных чувствительны к регистру и ты должен всегда давать переменным уникальные имена (т.е. в одном скрипте не должно быть переменных с одинаковыми именами).
Во-вторых, переменная made_up будет автоматически приведена к типу unknown, так как её тип (code) не может быть распознан системой.

Скрипт готов. Следующий шаг - его загрузка. Загрузить скрипт можно двумя способами:

Через менеджер ресурсов (рекомендуется)

Менеджер скриптов на базе менеджера ресурсов позаботится обо всём этом автоматически в том случае, если ты будешь загружать скрипты через него. Вот так может выглядеть функция загрузки скрипта через менеджер скриптов, размещённая в коде приложения:

Main.cpp (Какой-то игровой Проект)
...
Script *myScript = m_scriptManager->Add( "script.txt" );
...

Данная инструкция предпринимает попытку загрузить скриптовый файл script.txt, который расположен в той же директории, что и стартовый исполняемый файл игрового приложения. В случае удачной загрузки скрипта, переменная myScript будет содержать указатель на загруженный скрипт.
Для удаления скрипта достаточно выполнить команду:

Main.cpp (Какой-то игровой Проект)
...
m_scriptManager->Remove( *myScript );
...

Своим способом (без использования менеджера скриптов)

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

Main.cpp (Какой-то игровой Проект)
...
Script *myScript = new Script( "script.txt" );

SAFE_DELETE( myScript );
...


Доступ к переменным скрипта

В обоих случаях получить доступ к переменным скрипта можно с помощью следующих команд:

Main.cpp (Какой-то игровой Проект)
...
myScript->GetFloatData( "my_var" );
myScript->GetStringData( "MyStory" );
myScript->GetVectorData( "position" );
...


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

Итоги

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

Исходные коды Решения GameProject01 (для MS Visual C++ 2010), с которым работали в данной Главе, забираем здесь(external link).

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


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.9 Добавляем поддержку скриптов (scripting)

Contributors to this page: slymentat .
Последнее изменение страницы Среда 11 / Октябрь, 2017 10:52:09 MSK автор slymentat.

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

No records to display