Категория: Gamedev
Просмотров: 3111
    Добрый вечер. Сегодня мы продолжим обсуждать наш движок для игры (да-да, до игры ещё очень далеко) и рассмотрим такие моменты как логирование и механизм конфигурирования движка. Для обоих функциональностей у нас будет по отдельному классу (файлы о которых пойдет речь в данном уроке можно найти в разделе проекта 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 );
}

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