Adsense

воскресенье, 26 ноября 2017 г.

Cristmas Craft. Шаг 2

Во-первых, я решил отказаться от идеи «Час 1..2..3..». Буду называть это «Шаг». Так правильнее и проще.

Раз уж игра определяется как рилтайм стратегия (менеджер) с видом сверху-вниз, то это предполагает наличие карты, на которой будет происходить всё действо. Поэтому, первым делом понадобиться написать простенький редактор карты. Но ДО того, как это делать, необходимо определиться с форматом данных проекта.



Я решил поступить таким образом.

Карта локации - это преинициализация игровых данных. Т.е. эти данные будут загружены один раз при начале новой игры. А уже при других сессиях игры (после прохождения первой миссии и сохранения достижения, а впоследствии при загрузке игры) данные будут сохраняться/загружаться из файла профиля игрока. Поэтому формат файла должен быть гибок и легко расширяем.

Для этой цели я решил использовать старый добрый подход - использовать чанки (CHUNK). Этот термин я впервые встретил при разборе анимационного формата FLIC (.fli, .flc, .flx). Тогда я подумал, что это присуще лишь данному формату. Но уже при знакомстве с .3ds я понял, что это довольно распространённый подход. И достаточно удобный, если, конечно, его таковым сделать.

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

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

Сам формат файла можно представить следующим образом:

4 байта - сигнатура CCD1 - Cristmas Craft Data v1

Далее идут чанки:

Код чанка - 1 байт
Указатель на предыдущий чанк - 4 байта
Указатель на следующий чанк - 4 байта
Далее данные, соответствующие типу данных чанка.

Для начала разработки нам нужны всего 2 типа чанков:

0 - конец файла
1 - карта

0 - просто обозначает, что далее чанков больше нет. Об этом также свидетельствует указатель на следующий блок, который равен нулевому указателю.

1 - это карта уровня (БЕЗ каких-либо дополнительных объектов и слоёв - они будут являться отдельными чанками).

Сама карта уровня (данные) выглядят следующим образом:

Название карты -  байт. Этого достаточно для её идентификации среди множества карт записанных в одном файле.
Ширина карты - 1 байт.
Высота карты - 1 байт.
Данные карты объёмом равным ширина карты * высота карты.

Пора от слов переходить к делу.

Первым делом я создал файл gamedata.h в котором объявил несколько структур. Но, для того, чтобы данные корректно читались и записывались пачками на различных ОС и их версиях, необходимо позаботиться о том, чтобы структуры упаковывались везде одинаково. Я использовал выравнивание на 1 байт. Не круто для 64-битных систем, да и для 32-битных тоже, зато компактно:

#pragma pack( push, 1 )

Первой я объявил структуру заголовка файла данных. Она получилась очень простой:

struct GD_FILEDESC {
    unsigned long    signature;

};

И структуру чанка:

struct GD_CHUNK
{
    unsigned char    chunk;
    unsigned long    prevchunk;
    unsigned long    nextchunk;
};


chunk - это идентификатор чанка, а prevchunk и nextchunk - указатели на предыдущий и следующий чанки в файле.

Само собой нам нужна структура карты:

struct GD_MAP
{
char name[8];
unsigned char width;
unsigned char height;
unsigned char *data;
};

Здесь:
name - имя карты;
width и height - ширина и высота соответственно;
а *data - указатель на данные карты в памяти (НЕ в файле!).

После того, как нужные структуры объявлены, необходимо вернуть компилятору возможность выравнивать данные так, как ему лучше:

#pragma pack(pop)

Ну и, для того чтобы было проще работать с идентификаторами чанков, я объявил нумерованный список с их именами:

enum GD_CHUNKNAMES {
GD_CHUNK_END = 0,
GD_CHUNK_MAP
};

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

Подключив заголовочный файл и библиотеку stdio я объявил идентификатор рабочего файла:

FILE *gd_file = NULL;

и рабочего чанка:

GD_CHUNK gdchunk;

Первую функцию, которую я начал писать, стала функция создания нового файла.

int gd_openwrite( const char *filename )

В начале функции я проверяю, не открыт ли уже какой-то файл. Если это так, то необходимо прервать функцию и вернуть код ошибки:

if( gd_file != NULL )
return GD_FILEOPENED;

Если файл не открыт, то можно попробовать создать его:

if( NULL == ( gd_file = fopen( filename, "wb" ) ) )
return GD_NOFILE;

Если создать файл получилось, то можно записать заголовок файла. Подготовим структуру заголовка и запишем идентификатор файла:

GD_FILEDESC gddesc;
gddesc.signature = 0x31444343;
fwrite( &gddesc.signature, 4, 1,gd_file );

Число в поле signature это "CCD1" по принципу lower-to-higher.

Осталось только внести предварительные данные в рабочий чанк, и на этом функция готова:

gdchunk.chunk = 0;
gdchunk.prevchunk = 0;
gdchunk.nextchunk = 4;


return GD_OK;

Если есть функция создания файла, то должна быть и функция его закрытия:

int gd_closefile()

До того, как что-то закрыть, надо быть уверенным, что это "что-то" было открыто:

if( gd_file == NULL )
return GD_FILEISNOTOPEN;

При закрытии файла нужно также сбросить рабочий чанк:

fclose( gd_file );
gd_file = NULL;

gdchunk.chunk = 0;
gdchunk.prevchunk = 0;
gdchunk.nextchunk = 0;

return GD_OK;

Предполагая работу с чанками я подготовил функцию записи заголовка чанка:

int gd_writechunkheader()
{
fwrite( &gdchunk, sizeof( GD_CHUNK ), 1, gd_file );
return GD_OK;
}

А теперь можно заняться самими типами чанков:

int gd_writechunk( unsigned char chunk, unsigned char *data )

Здесь тоже проверяем, а открыт ли файл:

if( gd_file == NULL )
return GD_FILEISNOTOPEN;

Номер нового чанка нужно запомнить:

gdchunk.chunk = chunk;

Перебор чанков ведём через переключатель:

switch( chunk )

Первый чанк у нас нулевой - "конец данных":

case GD_CHUNK_END:

Cледующего чанка не будет:

gdchunk.nextchunk = 0;

Записываем чанк и выходим:

return gd_writechunkheader();
break;

Теперь чанк карты:

case GD_CHUNK_MAP:

Запомним указатель на текущий чанк, чтобы в следующем чанке указать на него, как на предыдущий:

unsigned long prevchunk = gdchunk.nextchunk;

Из переданного в функцию указателя получим структуру карты:

GD_MAP *map = (GD_MAP*)data;

Подсчитаем размеры блока данных:

unsigned long datasize = map->width * map->height;

Получим указатель на следующий блок:

gdchunk.nextchunk = prevchunk + datasize + 10 + sizeof( GD_CHUNK );

Здесь сложены размеры данных с размером заголовка чанка плюс 10 байт - это 8 символов на имя карты и 2 байта на размеры карты.

Сохраним заголовок чанка, имя и размеры карты:

int rslt = gd_writechunkheader();
if( rslt != 0 )
return rslt;

fwrite( data, 10, 1, gd_file );

Теперь записываем саму карту:

fwrite( map->data, datasize, 1, gd_file );

Запомним указатель на предыдущий (только что сохранённый) чанк:

gdchunk.prevchunk = prevchunk;

return GD_OK;
break;

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

return GD_WRONGCHUNK;

Что ж, основа функций положена. Теперь осталось проверить как всё это работает.

Создадим тестовое консольное приложение, которое сгенерирует прообраз карты и сохранит его в файл. Создадим файл _gdtest.cpp, в котором объявим структуру карты:

GD_MAP map;

Точку входа в приложение необходимо рассчитать не только для Линукс, но и для Винды:

#ifdef __linux__
int main( int argc, char *argv[] )
#elif _WIN32
#include <tchar.h>
int _tmain(int argc, _TCHAR* argv[])
#endif

Для генерируемой карты зададим имя и размеры:

sprintf( map.name, "test" );
map.width = 128;
map.height = 128;

Выделим место в памяти необходимое для данных и очистим его:

map.data = new unsigned char[ map.width * map.height ];

for( int y = 0; y < map.height; y++ )
for( int x = 0; x < map.width; x++ )

map.data[y*map.width+x] = 0;

Теперь запишем какие-то данные в определенное место карты (предполагаем деревья):

for( int y = map.height/4; y < map.height-map.height/4; y++ )
for( int x = map.width/2; x < map.width; x++ )

map.data[y*map.width+x] = 1;

Теперь создадим файл, запишем карту, нулевой чанк и закроем файл:

gd_openwrite( "test.ccd" );
gd_writechunk( GD_CHUNK_MAP, (unsigned char*)&map );
gd_writechunk( GD_CHUNK_END, 0 );
gd_closefile();

Освобождаем память:


delete map.data;

Отлично. Осталось только всё это собрать и протестировать.

Пишем простой Makefile:

CC=g++

# Project > Use: make
cristmas: cclink 

cclink: gamedata.o 
$(CC) -o cristmascraft gamedata.o 

# GD TEST > Use: make gdtest
gdtest: gdlink 

_gdtest.o: _gdtest.cpp 
$(CC) -c _gdtest.cpp 

gamedata.o: gamedata.cpp 
$(CC) -c gamedata.cpp 

gdlink: _gdtest.o gamedata.o 
$(CC) -o gdtest _gdtest.o gamedata.o

Здесь я сразу подготовил сборку проекта по умолчанию, и добавил сборку теста, которую можно выполнить по команде make gdtest.

Собрав тестовое приложение и запустив мы получим на выходе файл test.ccd - это и есть наша карта.

Для Windows я в проекте отключил инкрементную сборку, т.к. у меня MSVC++ 2010, там какой-то косяк имеется. Мелкомягкие его не то, чтобы не отрицают, а напротив, чуть ли не гордятся ))) Аж мануал по нему у себя запостили.

На этом пока всё.

Ссылка на исходники: http://catcut.net/oNyc

Комментариев нет:

Отправить комментарий