Создание загрузчика XML.

Содержание статьи:

Введение.
предварительные работы.
Класс cXMLtag.

Введение.

к началу статьи
Доброго времени суток, любезные господа. Чему посвящена статья я думаю ясно из названия, поэтому остановимся на том, зачем нам все таки это нужно. Практически во всех приличных движках (тех что в узких гейм девелоперских кругах предваряются заклинанием «data driven») весь графический контент прописан в огромных ресурсных файлах, в которых зачастую хранятся инструкции как эти данные обрабатывать. Для хранения всего этого добра можно изобрести какой-нибудь свой формат, но после того как работы по его созданию будут закончены и пройдут первые несколько дней эйфории от переполняющей Вас гордости, окажется что в Вашем божественном творении никто ничего понять не может и перед Вами встанет выбор – делать какую-нибудь удобную тулзу или садиться и обучать всех страждущих всем премудростям формата. Что касается первого варианта, то мне его воплощать не особо хочется. Реализовать второй вариант я категорически неспособен, т.к. уже были неприятные инциденты (помнится, когда младшая сестра просила меня объяснить ей какие-то вещи из высшей арифметики, все мои разъяснения начинались с дикого крика - «Ты что, дура?!»). Сможете ли вы справиться с описанным раскладом мне неведомо, поэтому осмелюсь предложить вам альтернативу – XML, который имеет ряд преимуществ – 1.он широко распространен, 2.для работы с ним наверняка есть какие-нибудь бесплатные утилиты, 3.данные будут организованы в виде дерава, 4.он похож на HTML, а его то уж знают даже дети младшего школьного возраста, соответственно при необходимости объяснить новому сотруднику (члену команды – нужное подчеркнуть) как у вас в игре организованы данные, Вам достаточно изречь - «Это XML», после чего послать человека читать мануал со списком поддерживаемых Вашим движком тэгов. На мой взгляд, плюсов более чем достаточно, посему давайте начнем.

Предварительные работы.

к началу статьи
Для начала рассмотрим пример простенького XML'я:

<texture w="64" h="64">
	<filters/>
	<filters w="10">
		<recreate_with_alpha alpha="0xffffffff" />
	</filters>
</texture>
Как видно заставить программу понимать сие, будет непросто. Поэтому для начала озаботимся созданием нескольких функций, которые приведут содержимое файла к виду удобному для обработки. Итак функция номер раз:

void	ReadProtoText( char *script_file , cArray<cString1024> &PTA ){
	FILE			*f_stream = fopen( script_file , "rt" );
	if( !f_stream ){
		MessageBox( 0 , "Не найден XML файл." , «Ошибка» , 0 );
		exit( 0 );
	}

	cString1024				PT;

	for( ; fscanf( f_stream , "%s" , PT.GetS() ) != EOF ; ){
		ProtoText.AddEnd( PT );
	}
}
Данная функция просто читает текстовый файл с нашими данными, и записывает их в массив PTA. Для большей ясности скажу что здесь используются самодельные классы массива и строки, первый я уже рассматривал в одном из уроков по C++, а с последним бегло ознакомимся сейчас:

template<int n>class		cStaticString{
	char				s[ n ];
public:
	cStaticString( void ){s[ 0 ]='\0';}
	cStaticString( char *str ){
		if( str )	strcpy( s , str );
		else 	s[ 0 ] = '\0';
	}
	bool	operator==( cStaticString<n> &R ){return(!strcmp( s , R.s ));}
	bool	operator==( char * str ){return(!strcmp( s , str ));}
	bool	operator!=( cStaticString<n> &R ){return( strcmp( s , R.s ));}
	bool	operator!=( char * str ){return(strcmp( s , str ));}
	char	*GetS( void ){return( s );}
	char	&operator[]( int i ){return( s[ i ] );}
	void	AddEnd( char ch ){
		int	i( strlen( s ) );
		s[ i ] = ch;
		s[ i + 1 ] = '\0';
	}
	int	Length( void ){return( strlen( s ) );}
	void	operator=( char *s2 ){strcpy( s , s2 );}
	void	Reset( void ){s[ 0 ]='\0';}
	void	operator=( cStaticString<n> &R ){strcpy( s , R.s );}
};

#define			cString32			cStaticString<32>
#define			cString1024			cStaticString<1024>
Все просто.

Содержимое файла прочитано, но оно все еще не готово для обработки, поэтому получившийся массив строк PTA обработаем ещё одной функцией:


void				DispatchProtoText( cArray<cString1024> &PTA , 
								cArray<cString32> &LA ){
	cString32		Lexemma;
	cString32		str;
	for( int i( 0 ) ; i < PTA.Cursor ; i++ ){
		for( int j( 0 ) ; j < strlen( PTA[ i ].GetS() ) ; j++ ){
			if( ( ( PTA[ i ][ j ] >= 'a' && PTA[ i ][ j ] <= 'z' ) 
				|| ( PTA[ i ][ j ] >= 'A' && PTA[ i ][ j ] <= 'Z' ) 
				|| ( PTA[ i ][ j ] >= '0' && PTA[ i ][ j ] <= '9' )
				|| ( PTA[ i ][ j ] == '_' ) ) ){
				str.AddEnd( PTA[ i ][ j ] );
			}
			else{
				if( str.Length() ){
					Lexemma = str;
					LA.AddEnd( Lexemma );
					str.Reset();
				}
				if( ( PTA[ i ][ j ] >= '!' && PTA[ i ][ j ] <= '/' ) ||
					( PTA[ i ][ j ] >= ':' && PTA[ i ][ j ] <= '@' ) || 
					( PTA[ i ][ j ] >= '[' && PTA[ i ][ j ] <= '_' ) ||
					( PTA[ i ][ j ] >= '{' && PTA[ i ][ j ] <= '}' ) ){
					Lexemma[ 0 ] = PTA[ i ][ j ];
					Lexemma[ 1 ] = '\0';
					LA.AddEnd( Lexemma );
				}
			}
		}
		if( str.Length() ){
			Lexemma = str;
			LA.AddEnd( Lexemma );
			str.Reset();
		}
	}
}
Данная функция формирует массив из идентификаторов (набор букв, цифр и «_», начинающийся с буквы), констант и прочих символов (знаки препинания, скобки, математические знаки, при этом всякая псевдографика остается за бортом). Вот теперь можно и XML разбирать. Для этого нам понадобится ...

Класс cXMLtag.

к началу статьи

class				cXMLtag{
	class					cXMLtagPTR{
		cXMLtag				*ptr;
	public:
		cXMLtag				*operator->( void ){return( ptr );}
		cXMLtagPTR( void ){ptr=NULL;}
		cXMLtagPTR( cXMLtag *p ){ptr=p;}
		void				operator=( cXMLtag *p ){ptr=p;}
		void				CreateTag( void ){ptr = new cXMLtag;}
		cXMLtag				*GetPTR( void ){return( ptr );}
	};
	cString32		Name;
	cArray<cXMLtagPTR>	Tags;
	cArray<cString32>	Params;
	cArray<cString32>	Values;
	int		ProcessParams( cXMLtag *tag , cArray<cString32> &Lexemmas , int &i );
	bool		IsClosingTag( cXMLtag *tag , cArray<cString32> &Lexemmas , int &i  );
public:
	cXMLtag( void ){
		Tags.Cursor = 0;
		Params.Cursor = 0;
		Values.Cursor = 0;
	}
	void			LoadXML( cArray<cString32> &Lexemmas , int &i );
	void			PrintXML( char * );
};
Начнем по-порядку. CXMLtagPTR – это класс указателя на тэг, каждый тэг имеет массив Tags таких указателей (про древовидную структуру XML'я помните?), так же в тэге хранится список параметров - Params и их значений – Values, ну в поле Name хранится название тэга. Как Вы наверное догадались загрузка осуществляется с помощью функции void LoadXML( cArray<cString32> &Lexemmas , int &i ):

void				cXMLtag::LoadXML( cArray<cString32> &Lexemmas , int &i ){
	if( !strcmp( Lexemmas[ i ].GetS() , "<" ) ){
		if( i + 1 < Lexemmas.Cursor ){
			if( IsIdent( Lexemmas[ i + 1 ] ) ){
				Name = Lexemmas[ i + 1 ];
				i += 2;
				if( !ProcessParams( this , Lexemmas , i ) ){
					goto end;
				}
			}
			else{
				MessageBox( 0 , "Недопустимая структура XML 
					cXMLtag::LoadXML( cXMLtag *tag , 
					cArray<cString32> &Lexemmas , int &i )" , 
					"Ошибка" , 0 );
				exit( 0 );
			}
		}
	}
	else{
		MessageBox( 0 , "Недопустимая структура XML 
			cXMLtag::LoadXML( cXMLtag *tag , cArray<cString32> &Lexemmas , 
			int &i )" , "Ошибка" , 0 );
		exit( 0 );
	}
	
	if( IsClosingTag( this , Lexemmas , i ) ){
		goto end;
	}

process_inner_tags:;

	for( ; i < Lexemmas.Cursor ; ){
		Tags.AddEnd( NULL );
		Tags[ Tags.Cursor - 1 ].CreateTag();
		Tags[ Tags.Cursor - 1 ]->LoadXML( Lexemmas , i );
		if( IsClosingTag( this , Lexemmas , i ) ){
			goto end;
		}
	}

end:;
}
В первых трёх if'ах мы проверяем, что перед нами начало ОТКРЫВАЮЩЕГО тэга (т.е. имеем «<» и идентификатор). Является ли строка идентификатором мы выясняем с помощью функции IsIdent. После чего обрабатываем параметры тэга (функция ProcessParams – возвращает 1, если текщему тэгу должен быть закрывающий тэг, и возвращает 0 противном случае; здесь и далее под «текущим» тэгом подразумевается тэг на который указывает this). Если закрывающего тэга нет, то функция завершает свою работу, если есть то сначала проверяем «следующий тег закрывающий для текущего?», если да то функция завершает работу, если нет, то обрабатываем вложенные тэги. После обработки каждого вложенного тэга проверяем, является ли следующий тэг закрывающим текущего тэга. Если да то функция завершает работу, если нет, то грузим новый тэг. Основная функция описана, теперь можно коснуться некоторых утилитарных функций:

int	cXMLtag::ProcessParams( cXMLtag *tag , cArray<cString32> &Lexemmas , int &i ){
	for( ; i < Lexemmas.Cursor ;  ){
		if( i + 4 < Lexemmas.Cursor ){
			if( IsIdent( Lexemmas[ i ] ) 
				&& !strcmp( Lexemmas[ i + 1 ].GetS() , "=" ) 
				&& !strcmp( Lexemmas[ i + 2 ].GetS() , "\"" ) 
				&& !strcmp( Lexemmas[ i + 4 ].GetS() , "\"" ) ){
				tag->Params.AddEnd( Lexemmas[ i ] );
				tag->Values.AddEnd( Lexemmas[ i + 3 ] );
				i += 5;
				goto end;
			}
		}
		if( i + 1 < Lexemmas.Cursor ){
			if( !strcmp( Lexemmas[ i ].GetS() , "/" ) 
				&& !strcmp( Lexemmas[ i + 1 ].GetS() , ">" ) ){
				i += 2;
				return( 0 );
			}
		}
		if( !strcmp( Lexemmas[ i ].GetS() , ">" ) ){
			i += 1;
			return( 1 );
		}
		MessageBox( 0 , "Недопустимая структура XML 
			cXMLtag::ProcessParams( cXMLtag *tag , cArray<cString32> &Lexemmas , 
			int &i )" , "Ошибка" , 0 );
		exit( 0 );
end:;
	}
	return( 2 );
}
Здесь в первых вложенных if'ах смотрим, что бы к каждый параметр был идентификатором, чтобы к нему «прилагался» знак присваивания и значение, заключенное в кавычки. Заметьте, в этой реализации парсера мы грузим только строки и целые числа, впрочем доработать функцию, так чтобы можно было загружать и вещественные числа не составит труда, но это Вы уж как-нибудь сами. ОК? Если параметр не удалось прочитать, смотрим а параметр ли это? Может мы уже наткнулись на конец тэга, если это действительно конец и ещё параметров не предвидется, то смотрим нужен ли закрывающий тег (и возвращаем 1), если же не нужен, то возвращаем 0. Ну и на закуску вообще элементарщина (вывод обработанного XML на экран), здесь кроме работы со строками ничего нет:

void					cXMLtag::PrintXML( char * tab_space ){
	char			str[ 1024 ];
	char			new_tab_space[ 2048 ];
	strcpy( str , tab_space );
	strcat( str , "<" );
	strcat( str , Name.GetS() );
	strcat( str , " ");
	for( int i( 0 ) ; i < Params.Cursor ; i++ ){
		strcat( str , Params[ i ].GetS() );
		strcat( str ,  " " );
		strcat( str ,  "=" );
		strcat( str ,  " " );
		strcat( str ,  "\"" );
		strcat( str , Values[ i ].GetS() );
		strcat( str ,  "\"" );
	}
	strcat( str , ">" );

	cout<<str<<endl;

	strcpy( new_tab_space , tab_space );
	strcat( new_tab_space , tab_space );

	for( int i( 0 ) ; i < Tags.Cursor ; i++ ){
		Tags[ i ]->PrintXML( new_tab_space );
	}

	strcpy( str , tab_space );
	strcat( str , "<" );
	strcat( str , "/" );
	strcat( str , Name.GetS() );
	strcat( str , ">" );
	cout<<str<<endl;
}
Напоследок скажу, что созданная схема имеет одно несущественное ограничение, которое Вам придется иметь ввиду – все тэги файла должны быть вложены в какой-либо корневой тэг. Так например следующий пример обработается некорректно

<texture w="64" h="64">
	<filters/>
	<filters w="10">
		<recreate_with_alpha alpha="0xffffffff" />
	</filters>
</texture>
<texture w="64" h="64">
	<filters/>
	<filters w="10">
		<recreate_with_alpha alpha="0xffffffff" />
	</filters>
</texture>
А со следующим проблем не возникнет

<root>
	<texture w="64" h="64">
		<filters/>
		<filters w="10">
			<recreate_with_alpha alpha="0xffffffff" />
		</filters>
	</texture>
	<texture w="64" h="64">
		<filters/>
		<filters w="10">
			<recreate_with_alpha alpha="0xffffffff" />
		</filters>
	</texture>
</root>
т.е. Вам придется либо после вызова ReadProtoText, надо будет добавлять в конец обобщающие тэги, либо держать это ограничение в голове, создавая ресурсные файлы для своей игры. Данный класс ещё сыроват, т.к. нужно создать удобный интерфейс для работы с информацией, которую он хранит, но это уже дело вкуса и личных предпочтений, поэтому я считаю свою миссию выполненной и незамедлительно удаляюсь.

PS: связь как всегда по почте dodonov_a_a (__AT) inbox.ru

к началу статьи


Исходные коды.
© 2004-2005 Savardge.ru
Hosted by uCoz