Добрый вечер. Сегодня мы продолжим обсуждать наш движок для игры (да-да, до игры ещё очень далеко) и рассмотрим такие моменты как логирование и механизм конфигурирования движка. Для обоих функциональностей у нас будет по отдельному классу (файлы о которых пойдет речь в данном уроке можно найти в разделе проекта Overlord Engine вместе со свежей версией движка), и если с ведением логов все более менее понятно, то конфигурирование требует более подробного рассказа. Можно начинать?

    Наверняка вы уже слыхали нечто подобное в каком-либо чате или форуме, но если нет, то этот термин переводится примерно следующим образом - «движок, управляемый данными». Смысл термина заключается в том, что весь игровой процесс описывается и настраивается с помощью конфигурационных (здесь имеются ввиду не только *ini файлы, но и файлы с описанием игровых локаций, скрипты и т. д.) файлов. Т. е. движок спроектирован таким образом, что вся игровая механика вынесена за пределы exe'шника игры. Во время запуска главного исполняемого файла игры, осуществляется чтение настроек, хранящихся в конфигурационных файлах и по ним строятся локации, расставляются враги, препятствия, проигрываются всевозможные визуальные и звуковые эффекты. Именно благодаря такому подходу в создании игр стало возможным появление такого явления как mod-мэйкерство, когда группа энтузиастов берет игру, и, изменив некторые настройки, меняет геймплэй или добавляет в игру врагов, квесты, NPC и т.д.

    Что же мы поимеем, решив создать data driven engine? К положительным моментам можно отнести следующее:
1. Положительную реакцию коммьюнити – фанаты будут рады получить дополнительные примочки к своей любимой игре в виде всевозможных модов. Возможно какой-нибудь мод сможет потягаться в интересности с оригинальной игрой (как это было в случае с CS'ом).
2. Облегчение жизни программистам – теперь для внесения изменений в игру не требуется их непосредственное участие.
3. Ускорение времени компиляции (для маленьких игр это не столь существенно, однако для игр чьи движки состоят из миллионов строк кода это время может быть весьма существенным), т.к. компилироваться будет только движок а не вся игра целиком.

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

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

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

class				CXMLTag{
protected:

	friend			CXMLFile;

	// список вложенных тэгов
	std::vector< CXMLTag* >		Tags;
	// список параметров текущего тэга
	std::vector< std::string >	Params;
	// список значений текущего тэга
	std::vector< std::string >	Values;

	int	ProcessParams( CXMLTag *tag , std::vector< std::string > &Lexemmas , int &i );
	bool	IsClosingTag( CXMLTag *tag , std::vector< std::string > &Lexemmas , int &i  );
	void	LoadXML( std::vector< std::string > &Lexemmas );
	void	LoadXML( CXMLTag *tag , std::vector< std::string > &Lexemmas , int &i );
	void	SaveXML( FILE * f_stream , char * tab_space );
	void	PrintXML( char * );
public:
	CXMLTag( void ){
	}

	// имя текущего тэга
	std::string			Name;

	// добавление вложенного тэга tag_name с одним параметром  tag_attrib_name
	// который имеет значение  tag_attrib_value
	void	AddTag( char *tag_name, char *tag_attrib_name, char *tag_attrib_value);

	// добавление тэга с двумя параметрами
	void	AddTag( char *tag_name , char *tag_attrib_name1 , char *tag_attrib_value1 , 
			 char *tag_attrib_name2 , char *tag_attrib_value2 );

	// возвращает true если у тэга есть параметр  param_name
	// в обратном случае возвращает false
	bool	CheckParam( char *param_name);

	// возвращает true если существует вложенный тэг  tag_name
	// иначе возвращает false
	bool	CheckTag( char *tag_name );

	// возвращает true если существует вложенный тэг  tag_name с параметром param_name
	// иначе возвращает false
	bool	CheckTagParam( char * tag_name , char * param_name );

	// возвращает true если существует вложенный тэг  tag_name с параметром param_name
	// и значением param_value
	// иначе возвращает false
	bool	CheckTagParamValue( char * tag_name , char * param_name , char * param_value);

	// возвращает true если существует вложенный тэг с курсором 
	// tag_cursor с параметром param_name
	// и значением param_value
	// иначе возвращает false
	bool	CheckTagParamValue( int tag_cursor , char * param_name , char * param_value);

	// возвращает значение параметра текущего тэга
	std::string	&GetParam( char * );
	
	// возвращает значение параметра вложенного тэга
	std::string	&GetParam( char * , char * );

	// возвращает параметр текущего тэга в виде вещественно числа
	double	GetParam_double( char * );
	
	// возвращает параметр вложенного тэга в виде вещественного числа
	double	GetParam_double( char * , char * );

	// возвращает количество вложенных тэгов
	size_t	NumOfChildTags( void ){
		return( Tags.size() );
	}

	// пробегаем по вложенным тэгам
	CXMLTag	&operator[]( char * tag_name );
	CXMLTag	&operator[]( size_t tag_cursor );

	// очистка объекта CXMLTag
	void	Release( void );
};

// класс с которым программист непосредственно работает
// в классе реализованы методы для оперирования с файлами – загрузка, 
// сохранение, вывод на экран
class		CXMLFile:public CXMLTag{
public:
		CXMLFile( void ){}
		CXMLFile( char * file_path ){LoadXML( file_path );}

	void	LoadXML( char * );
	void	PrintXML( void ){ CXMLTag::PrintXML( "   " ); }
	void	SaveXML( char * file_name );
};

    Класс для ведения логов, существенно проще, поэтому привожу его целиком с минимальными комментариями:

class			CLogStream{
public:
	CLogStream( void ){}
	CLogStream( char * ProjectName , char * ProjectVersion , 
					 char * EngineVersion );
	CLogStream			&operator<<( char * str );
	CLogStream			&operator<<( std::string & str );
	CLogStream			&operator<<( float f );
};

// при создании объекта старый лог очищается и заводится новый в котором 
// пишется версия проекта, движка и название проекта
// далее для вывода в лог можно использовать либо созданный объект 
// либо анонимный экземпляр класса
CLogStream::CLogStream( char * ProjectName , char * ProjectVersion , char * EngineVersion ){

	__time64_t		ltime;

	FILE		*LogFile;
	LogFile = fopen( "c:\\system.log" , "wt" );

	fprintf( LogFile , "===========================================================================\n\n" );
	fprintf( LogFile , "New logging session started at :    " );
	fprintf( LogFile , "%s", _ctime64( <ime ) );
	fprintf( LogFile , "Project name                   :    %s\n" , ProjectName );
	fprintf( LogFile , "Project version                :    %s\n" , ProjectVersion );
	fprintf( LogFile , "Enginen version                :    %s\n" , EngineVersion );
	fprintf( LogFile , "\n===========================================================================\n" );

	fclose( LogFile );
}

// вывод в файл C-строки
CLogStream			&CLogStream::operator<<( char * str ){
	FILE		*LogFile;
	LogFile = fopen( "c:\\system.log" , "at" );
	fprintf( LogFile , "%s" , str );
	fclose( LogFile );
	return( *this );
}

// вывод в файл STL строки
CLogStream			&CLogStream::operator<<( std::string & str ){
	FILE		*LogFile;
	LogFile = fopen( "c:\\system.log" , "at" );
	fprintf( LogFile , "%s" , str.c_str() );
	fclose( LogFile );
	return( *this );
}

// вывод в файл вещественного числа
CLogStream			&CLogStream::operator<<( float f ){
	FILE		*LogFile;
	LogFile = fopen( "c:\\system.log" , "at" );
	fprintf( LogFile , "%f" , f );
	fclose( LogFile );
	return( *this );
}

    Вот собственно и все на сегодня.

    Ахой! Для начала расскажу чем мы будем заниматься в данной рубрике. Нет, ну из названия конечно понятно, что мы опять будет делать игры. Это так, но есть одно «НО». Оно всего одно, зато весьма существенное: здесь вы не увидите пространных рассуждений о создании КИ, здесь мы будем заниматься конкретной игрой, сначала произведём декомпозицию основной задачи «Создать игру» на более мелкие и более простые части и будем в каждую в своём уроке реализовывать.

    Для начала определимся с игрой – это будет не тетрис и не пинбол, как многие могли подумать (я надеюсь каждый из вас уже пачку таких игр написал) это слишком просто. Конечно можно и Lines навернуть до такой степени что технологии HL2 померкнут, но это уже извращение. Поэтому делать мы будем РПГ. Не надо смеяться ибо это будет предельно упрощенная РПГ, в которой моделька главного героя под управлением игрока будет бегать по подземелью и крошить врагов, плюс ближе к концу добавим поддержку мультиплеера (выпустим официальный аддон с мультиплеером как это сейчас принято )) ), что бы можно было подключиться к серверу найти братьев по оружию и устроить замес в на несколько персон. Вот собственно программа минимум. Приступим?

    Хе-хе-хе. Вижу вы ответили утвердительно на поставленный вопрос ). Однако не спешите составлять список фич, которые будет поддерживать игра, какие шейдеры 10.0+ версии будут в игре. Сейчас не это главное. Писать код мы сейчас тоже не будем. А просто поговорим об объектной модели нашей КИ. Для этого как раз самое время, поскольку даже реализация такого простого проекта, который описан выше, может превратиться в ад для программиста. Итак...

    В одной книжке, которая недавно была мною прочитана была такая фраза:

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

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

    Пойдем прямо по пунктам - «загружает модели, текстуры, музыку и шейдеры». От того как хорошо вы создадите систему загрузки ресурсов и их представления в движке, будет зависеть очень многое: прозрачности кода, легкость с которой вы сможете добавлять новые фичи, скорость кодирования в конце концов. Поэтому я решил остановиться на такой концепции – все ресурсы делятся на несколько групп: аудио/видео ресурсы, текстуры, шейдеры, модели, элементы управления, игровые объекты. Загрузкой каждого такого ресурса занимается отдельный менеджер. По сути менеджер является совокупностью массивов загруженных данных и кода для доступа и оперирования этими данными. Подобная организация позволит сделать движок более модульным, и при правильном проектировании интерфейса менеджеров изменение кода для оперирования ресурсами не повлияет на остальную часть программы (конечно если это не какая-то радикальная доработка). Обязательным элементом каждого менеджера будет массив загруженных ресурсов. Доступ к конкретному ресурсу будь то текстура или шейдер будет осуществляться по курсору этого элемента в массиве. То есть вместо кода

struct CGameObject{
	LPDIRECT3DDEVICE9	*texture;
};
У нас будет
struct CGameObject{
	int 			texture_id;
};

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

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

    Следующая идея, которой мы будем придерживаться, заключается в том что абсолютно все настройки будут грузиться из файлов. То есть у нас будут xml-ные файлы, в которых будут прописаны настройки рендера, списки используемых текстур, моделей, шейдеров etc. (это позволит нам создать некоторое подобие data driven engine – движка управляемого данными).

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