Работа с пакетом D3DFrame

         

Функция CUnitAnimation vLoadTextures()


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

void CUnitAnimation::vLoadTextures(void) { // Загрузка анимаций int i, j; int iLocalCount = 0; char szBitmapFileName[128]; // Выделение памяти для текстур m_Textures = new CTexture[ (m_iNumStillFrames * (UNITMANAGER_MAXOWNERS + 1)) + (m_iNumMoveFrames * (UNITMANAGER_MAXOWNERS + 1)) + (m_iNumAttackFrames * (UNITMANAGER_MAXOWNERS + 1))+ (m_iNumDieFrames * (UNITMANAGER_MAXOWNERS + 1))]; // Графика для ожидания (покоя) m_iStartStillFrames = 0; for(i = 0; i < m_iNumStillFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j); // Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; } // Графика для перемещения m_iStartMoveFrames = m_iTotalTextures; for(i = 0; i < m_iNumMoveFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j); // Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; } // Графика для атаки m_iStartAttackFrames = m_iTotalTextures; for(i = 0; i < m_iNumAttackFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j); // Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; } // Графика для гибели m_iStartDieFrames = m_iTotalTextures; for(i = 0; i < m_iNumDieFrames; i++) { for(j = 0; j < UNITMANAGER_MAXOWNERS+1; j++) { sprintf(szBitmapFileName, "UnitData\\%s%d_%d.tga", m_szBitmapPrefix, iLocalCount, j); // Задаем устройство визуализации m_Textures[m_iTotalTextures].vSetRenderDevice(m_pd3dDevice); // Загружаем текстуру m_Textures[m_iTotalTextures].vLoad(szBitmapFileName); // Увеличиваем общее количество текстур m_iTotalTextures++; } iLocalCount++; } }

Пожалуйста, не бейте меня! Я понимаю, что это большой фрагмент кода, но к счастью в нем много повторяющихся фрагментов. В работе кода можно выделить два основных этапа. На первом этапе осуществляется выделение памяти для объектов текстур. Здесь вычисляется количество текстур, необходимых для добавления всех кадров анимации. На втором этапе для каждой анимационной последовательности выполняется цикл в котором загружаются необходимые для нее текстуры.



Функция CUnitAnimation vReset()


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

void CUnitAnimation::vReset(void) { memset(m_szName, 0x00, 64); memset(m_szBitmapPrefix, 0x00, 64); // Освобождаем память текстур if(m_iTotalTextures) { delete [] m_Textures; m_Textures = NULL; m_iTotalTextures = 0; } m_iNumStillFrames = 0; m_iNumMoveFrames = 0; m_iNumAttackFrames = 0; m_iNumDieFrames = 0; m_iType = 0; m_iStartStillFrames = 0; m_iStartMoveFrames = 0; m_iStartAttackFrames = 0; m_iStartDieFrames = 0; }

Как видно в коде, чтобы определить наличие текстур я проверяю значение переменной m_iTotalTextures. Если какие-либо текстуры загружены, я удаляю массив m_Textures и устанавливаю количество загруженных текстур равным 0. Просто, не так ли?



Функция CUnitAnimation vSetRenderDevice()


Поскольку DirectX для загрузки текстуры необходимо устройство визуализации, я добавил функцию установки устройства визуализации, которая инициализирует указатель на устройство. В единственном параметре этой функции передается указатель LPDIRECT3DDEVICE9, который сохраняется в члене данных m_pd3dDevice. Позднее он будет использован для загрузки данных текстуры.

Вот как выглядит код этой функции:

void CUnitAnimation::vSetRenderDevice(LPDIRECT3DDEVICE9 pd3d) { m_pd3dDevice = pd3d; }

Функция CUnitManager iAddUnit()

Когда вы хотите ввести в игру новое подразделение, следует вызвать функцию добавления подразделения. Она находит неактивное подразделение и инициализирует его данные для использования в игре. Вот как выглядит код этой функции:

int CUnitManager::iAddUnit(char *szName, int iOwner) { int i; int iFoundID = -1; // Ищем соответствующий тип for(i = 0; i < m_iTotalUnitBaseObjs; i++) { if(!stricmp(szName, m_UnitBaseObjs[i].m_szName)) { iFoundID = i; break; } } // Возвращаемся, если базовый тип не найден if(iFoundID == -1) { return(-1); } // Ищем свободный блок данных подразделения for(i = 0; i < m_iTotalUnitObjs; i++) { // Проверяем, является ли блок неактивным if(!m_UnitObjs[i].m_bActive) { // Активируем подразделение m_UnitObjs[i].m_bActive= 1; // Устанавливаем его внутренние типы m_UnitObjs[i].vSetBaseValues( m_UnitBaseObjs[iFoundID].m_Defense, m_UnitBaseObjs[iFoundID].m_Offense1, m_UnitBaseObjs[iFoundID].m_Offense2, m_UnitBaseObjs[iFoundID].m_Offense3, m_UnitBaseObjs[iFoundID].m_Movement, m_UnitBaseObjs[iFoundID].m_Animation); // Устанавливаем тип подразделения m_UnitObjs[i].m_iType = iFoundID; // Устанавливаем владельца подразделения m_UnitObjs[i].m_iOwner = iOwner; // Увеличиваем количество подразделений у владельца m_iOwnerTotal[iOwner]++; return(i); } } return(-1); }

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

ПРИМЕЧАНИЕ Всегда выполняйте поиск имени базового типа подразделения. Если вы не будете искать имя, соответствующее переданному из вызывающего кода, у вас может использоваться несуществующий тип подразделения!

Следующая часть кода в цикле перебирает список всех подразделений игрока, ища неактивную запись. Данный этап необходим потому что активные подразделения уже присутствуют в игре и мы не можем использовать здесь относящиеся к ним записи. После обнаружения неактивного подразделения, оно делается активным и выполняется установка его базовых типов. В конце задается тип подразделения и его владелец. Я также отслеживаю сколько подразделений находится у каждого владельца, чтобы не превысить установленный лимит, если он существует.

Еще раз проясню ситуацию: массив m_UnitObjs хранит данные подразделений, которые изменяются во время игры, а массив m_UnitBaseObjs хранит шаблоны подразделений, которые никогда не меняются. Объекты m_UnitObjs меняют свои данные состояния, а объекты m_UnitBaseObjs — нет. Взаимосвязь между базовыми типами и динамическими объектами показана на Рисунок 8.27.



Функция CUnitManager iLoadBaseTypes()


Вы узнали, качие элементы класса хранят информацию базовых типов, но как загружаются данные? Здесь вступает в игру функция iLoadBaseTypes(). Она получает пять параметров, каждый из которых является именем файла, содержащего импортируемые данные. Отдельные файлы требуются для данных защиты, данных атаки, данных передвижения, данных анимации и данных подразделений. Функция загрузки базовых типов получает имена пяти файлов и импортирует данные из них в диспетчер подразделений. На Рисунок 8.23 показана взаимосвязь между классом диспетчера подразделений и импортируемыми файлами.





Функция CUnitManager ptrGetDefenseType()


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

CUnitDefense* CUnitManager::ptrGetDefenseType(char *szName) { int i; CUnitDefense *ptrUnitDefense = NULL; for(i = 0; i < m_iTotalDefObjs; i++) { if(!stricmp(szName, m_DefenseObjs[i].m_szName)) { ptrUnitDefense = &m_DefenseObjs[i]; return(ptrUnitDefense); } } return(ptrUnitDefense); }

Код представляет собой простой цикл, перебирающий все загруженные типы защиты. Название каждого типа защиты сравнивается с переданной функции строкой. Если строки совпадают, возвращается указатель на тип защиты. Это позволяет вызывающему коду использовать данные типа защиты без создания копии данных, благодаря чему уменьшается объем занимаемой памяти.

Функции, подобные рассмотренной выше, используются для получения указателей на типы атаки, передвижения и анимации. Я не буду приводить их здесь, поскольку они практически идентичны уже рассмотренному коду и вы можете увидеть их в файле UnitTemplateClasses.cpp.

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

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



Функция ID3DXFont DrawText()


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

INT DrawText( LPCSTR pString, INT Count, LPRECT pRect, DWORD Format, D3DCOLOR Color );

Первый параметр, pString, исключительно прост, ведь в нем передается отображаемый текст. В рассматриваемом примере я передаю в этом параметре имя игрока.

Второй параметр, Count, содержит количество отображаемых символов. Я передаю в этом параметре –1, чтобы DirectX мог сам вычислить, сколько символов отображать. Если вы будете поступать так же, убедитесь, что ваша строка завершается нулевым символом!

Третий параметр, pRect, представляет собой описание прямоугольной области, сообщающее DirectX где именно следует отображать текст. В рассматриваемом примере я создаю область визуализации внутри изображения текстового поля ввода и сохраняю ее параметры в переменной rectText.

В четвертом параметре, Format, передаются флаги форматирования, сообщающие системе как выполнять визуализацию текста. В рассматриваемой программе я использую флаг DT_LEFT, указывающий системе, что выводимый текст должен выравниваться по левому краю. Существует множество других флагов. Их описание вы найдете в документации DirectX SDK.

Пятый параметр, Color, определяет цвет, используемый при визуализации. В этом параметре я использую макрос D3DCOLOR_RGBA(), позволяющий просто указать значения RGBA для шрифта.



Функция IDirectInputDevice8 SetProperty()


Реализация буферизованного ввода достаточно проста — достаточно установить свойство устройства клавиатуры. Это осуществляется с помощью функции установки свойств. Вот как выглядит ее прототип:

HRESULT SetProperty( REFGUID rguidProp, LPCDIPROPHEADER pdiph );

Первый параметр, rguidProp, является GUID того свойства устройства, которое вы хотите установить. Чтобы установить размер буфера устройства используйте значение DIPROP_BUFFERSIZE.

Второй параметр, pdiph, является структурой данных, содержащей информацию о создаваемом буфере. Тип этой структуры данных — DIPROPDWORD. В коде я заполняю эту структуру данных нулями и устанавливаю параметр, определяющий размер создаваемого буфера клавиатуры. Количество сохраняемых в буфере событий клавиатуры задает следующая строка кода:

dipdw.dwData = KEYBOARD_BUFFERSIZE;

Поле dwData определяет максимальное количество сохраняемых в буфере событий клавиатуры. В рассматриваемом примере я использую значение 10. Вы можете поиграться с этой константой, чтобы подобрать более подходящее для вашей игры значение.



Функция iInitDirectInput()


Функция iInitDirectInput() — это мое собственное творение и я использую ее для создания главного объекта DirectInput. Код, используемый мной для создания упомянутого объекта должен выглядеть для вас очень знакомым, поскольку я уже описывал его в предыдущем разделе главы. Здесь я привожу полный код функции:

int iInitDirectInput(void) { HRESULT hReturn; // Не пытаться создать Direct Input, если он уже создан if(!pDI) { // Создаем объект DInput if(FAILED(hReturn = DirectInput8Create( g_hInstance, DIRECTINPUT_VERSION, IID_IDirectInput8, (VOID**)&pDI, NULL))) { return(INPUTERROR_NODI); } } else { return(INPUTERROR_DI_EXISTS); } return(INPUTERROR_SUCCESS); }

В приведенном выше коде я сперва проверяю, существует ли объект DirectInput. Если да, мне не надо создавать еще один объект. В этом случае функция возвращает код ошибки, говорящий вызывающей программе, что объект уже создан.

В следующем блоке кода выполняется вызов функции DirectInput8Create() для создания объекта DirectInput. Как только он будет успешно выполнен, моя функция возвращает WinMain() код успешного завершения. В результате этих действий глобальный указатель pDI будет содержать ссылку на созданный при вызове функции объект DirectInput.



Функция iInitKeyboard()


Теперь, когда у нас есть действующий объект ввода в форме глобального указателя pDI, можно создать интерфейс объекта клавиатуры. Здесь выходит на сцену моя функция iInitKeyboard(). В ней я создаю устройство клавиатуры, устанавливаю буфер клавиатуры, задаю режим доступа, захватываю клавиатуру и получаю раскладку клавиатуры. Вот как выглядит код функции:

int iInitKeyboard(HWND hWnd) { HRESULT hReturn = 0; DIPROPDWORD dipdw; // Не пытайтесь создать клавиатуру дважды if(pKeyboard) { return(INPUTERROR_KEYBOARDEXISTS); } // Выход, если не найден интерфейс DirectInput else if (!pDI) { return(INPUTERROR_NODI); } // Получаем интерфейс устройства системной клавиатуры if(FAILED(hReturn = pDI->CreateDevice( GUID_SysKeyboard, &pKeyboard, NULL))) { return(INPUTERROR_NOKEYBOARD); } // Создаем буфер для хранения данных клавиатуры ZeroMemory(&dipdw, sizeof(DIPROPDWORD)); dipdw.diph.dwSize = sizeof(DIPROPDWORD); dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER); dipdw.diph.dwObj = 0; dipdw.diph.dwHow = DIPH_DEVICE; dipdw.dwData = KEYBOARD_BUFFERSIZE; // Устанавливаем размер буфера if(FAILED(hReturn = pKeyboard->SetProperty( DIPROP_BUFFERSIZE, &dipdw.diph))) { return(INPUTERROR_NOKEYBOARD); } // Устанавливаем формат данных клавиатуры if(FAILED(hReturn = pKeyboard->SetDataFormat( &c_dfDIKeyboard))) { return(INPUTERROR_NOKEYBOARD); } // Устанавливаем уровень кооперации для монопольного доступа if(FAILED(hReturn = pKeyboard->SetCooperativeLevel( hWnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND ))) { return(INPUTERROR_NOKEYBOARD); } // Захватываем устройство клавиатуры pKeyboard->Acquire(); // Получаем раскладку клавиатуры g_Layout = GetKeyboardLayout(0); return(INPUTERROR_SUCCESS); }

Гм-м — многовато кода для простой инициализации клавиатуры, не так ли? В действительности все не так уж и плохо, если учесть чего мы с помощью этого кода достигнем.

Первая часть кода проверяет не проинициализирован ли уже указатель pKeyboard. Если да, объект клавиатуры уже создан ранее и функция возвращает код ошибки, извещающий нас об этом. В следующей проверке мы убеждаемся, что существует объект ввода pDI. Если инициализация DirectInput не выполнена, нет смысла пытаться создать объект клавиатуры!

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



Функция инициализации пути


На Рисунок 12.11 изображен ход выполнения кода поиска пути.



Функция iReadKeyboard()


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

if(!pKeyboard || !pDI) { return(INPUTERROR_NOKEYBOARD); }

Этот маленький фрагмент кода проверяет существуют ли объекты клавиатуры и DirectInput. Если какого-нибудь из них нет, функция возвращает код ошибки. Пришло время следующего фрагмента:

hr = pKeyboard->GetDeviceData( sizeof(DIDEVICEOBJECTDATA), didKeyboardBuffer, &dwItems, 0);

Вызов функции получения данных от устройства возвращает любые данные, находящиеся в буфере устройства ввода. В данном случае возвращается буфер клавиатуры. Переменная dwItems будет содержать количество возвращенных элементов, а сами они будут помещены в буфер didKeyboardBuffer. Переменная hr сохраняет код завершения, возвращаемый функцией получения данных от устройства. Логика проверки кода завершения выглядит следующим образом:

// Клавиатуа может быть потеряна, захватить устройство снова if(FAILED(hr)) { pKeyboard->Acquire(); return(INPUTERROR_SUCCESS); }

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

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

// Если есть данные, обработаем их if (dwItems) { // Обработка данных for(dwCurBuffer = 0; dwCurBuffer < dwItems; dwCurBuffer++) { // Преобразование скан-кода в код ASCII byteASCII = Scan2Ascii( didKeyboardBuffer[dwCurBuffer].dwOfs); // Указываем, что клавиша нажата if(didKeyboardBuffer[dwCurBuffer].dwData & 0x80) { ascKeys[byteASCII][dwCurBuffer] = 1; diks[didKeyboardBuffer[dwCurBuffer].dwOfs] [dwCurBuffer] = 1; } // Указываем, что клавиша отпущена else { ascKeys[byteASCII][dwCurBuffer] = 0; diks[didKeyboardBuffer[dwCurBuffer].dwOfs] [dwCurBuffer] = 0; } } }

Код проверяет были ли возвращены какие-нибудь данные функцией получения данных от устройства. Если да, код в цикле перебирает возвращенные элементы в буфере и сохраняет результаты в глобальных массивах diks и ascKeys.



Функция LoadMap()


Функция загрузки карты работает во многом так же, как и функция сохранения карты. Тем не менее, в ней есть ряд ключевых отличий. Во-первых, функция GetSaveFileName() заменена на функцию GetOpenFileName(). Я не знаю, почему есть две различных функции, выполняющих одинаковые действия, но кто я такой, чтобы подвергать сомнению установленный порядок вещей? Так или иначе, но функция получает имя открываемого файла и заносит его в указанную строку. Убедившись, что указанный файл существует, код открывает его, и загружает содержимое в глобальный массив карты. После завершения работы воспроизводится звуковой сигнал, оповещающий об успешной загрузке файла.

Если вы этого еще не сделали, запустите программу D3D_MapEditorPlus и щелкните по кнопке Load. Загрузите файл с именем TileMap.dat и вы увидите рисунок, выполненный моей рукой из песка.

Это все основные сведения о загрузке и сохранении блочных карт. С демонстрацией!



Функция SaveMap()

Чтобы сохранить карту вам необходимо сохранить все содержимое массива блоков g_iTileMap. Поскольку массив представляет собой одну непрерывную область памяти, операция достаточно проста. Фактически, вы должны создать файл для сохранения данных, записать в этот файл содержимое массива блочной карты, после чего закрыть файл. Для большего удобства я добавил функциональность, предоставляющую пользователю диалоговое окно выбора файла. Оно позволяет выбрать имя файла данных с помощью простого и интуитивно понятного интерфейса. Вот как выглядит код, который я использую для сохранения карты:

void vSaveMap(void) { FILE *fp; int iRet; OPENFILENAME fileStruct; char szFileName[512]; char szFilter[32]; char szExtension[32]; // Очищаем буфер для получения имени файла memset(szFileName, 0x00, 512); // Создаем фильтр имен файлов memset(szFilter, 0x00, 32); strcpy(szFilter, "*.dat"); // Указываем расширение имени файла memset(szExtension, 0x00, 32); strcpy(szExtension, "dat"); // Создаем структуру диалога выбора файла memset(&fileStruct, 0x00, sizeof(OPENFILENAME)); // Инициализируем структуру fileStruct.hInstance = g_hInstance; fileStruct.hwndOwner = g_hWnd; fileStruct.lpstrDefExt = szExtension; fileStruct.lpstrFileTitle = szFileName; fileStruct.lpstrFilter = szFilter; fileStruct.nMaxFileTitle = 512; fileStruct.lStructSize = sizeof(OPENFILENAME); // Получаем имя файла iRet = GetSaveFileName(&fileStruct); // Выходим в случае ошибки if(!iRet) { return; } // Открываем файл fp = fopen(szFileName, "wb"); // Возвращаемся, если не можем открыть файл if(fp == NULL) { return; } // Сохраняем буфер блочной карты fwrite(g_iTileMap, 10000, sizeof(int), fp); // Закрываем файл fclose(fp); // Воспроизводим звук, сообщающий о завершении действия PlaySound("bleep.wav",NULL,SND_FILENAME|SND_ASYNC); }

В первой части кода выполняется инициализация структуры данных OPENFILENAME, необходимой для функции GetSaveFileName(). Функция GetSaveFileName() является частью Microsoft Visual C++ SDK и предоставляет все необходимое для создания диалогового окна сохранения файла.

СОВЕТ Предоставляемое Microsoft диалоговое окно делает получение имени файла очень простой задачей. Я рекомендую вам применять его в своих программах.

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

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

ПРИМЕЧАНИЕ Если вы введете имя файла в поле Save As и не выберете расширение, файл будет сохранен без расширения. Убедитесь, что вы сохраняете карты в файлы с расширением .dat, если не хотите столкнуться с проблемами во время их загрузки.

Загрузите редактор карт и поиграйте с ним, редактируя и сохраняя карты. Это предоставит вам материал для экспериментов с загрузкой карт.



Функция смены слоя


Для поддержки переключения слоев я также добавил в редактор новую функцию. Вот ее прототип:

void vChangeLayer(int iLayer);

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



Функция vChangeLayer()


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

void vChangeLayer(int iLayer) { // Уничтожение кнопок слоев DestroyWindow(hBUTTON_LAYER1); DestroyWindow(hBUTTON_LAYER2); DestroyWindow(hBUTTON_LAYER3); DestroyWindow(hBUTTON_LAYER4); // Установка кнопок в состояние по умолчанию hBUTTON_LAYER1 = CreateWindow( "BUTTON", "1", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 3, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER1, g_hInstance, NULL); hBUTTON_LAYER2 = CreateWindow( "BUTTON", "2", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 25, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER2, g_hInstance, NULL); hBUTTON_LAYER3 = CreateWindow( "BUTTON", "3", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 48, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER3, g_hInstance, NULL); hBUTTON_LAYER4 = CreateWindow( "BUTTON", "4", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 71, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER4, g_hInstance, NULL); // Активация требуемой кнопки if(iLayer == 1) { DestroyWindow(hBUTTON_LAYER1); hBUTTON_LAYER1 = CreateWindow( "BUTTON", "1", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 3, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER1, g_hInstance, NULL); } else if(iLayer == 2) { DestroyWindow(hBUTTON_LAYER2); hBUTTON_LAYER2 = CreateWindow( "BUTTON", "2", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 25, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER2, g_hInstance, NULL); } else if(iLayer == 3) { DestroyWindow(hBUTTON_LAYER3); hBUTTON_LAYER3 = CreateWindow( "BUTTON", "3", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 48, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER3, g_hInstance, NULL); } else if(iLayer == 4) { DestroyWindow(hBUTTON_LAYER4); hBUTTON_LAYER4 = CreateWindow( "BUTTON", "4", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 71, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER4, g_hInstance, NULL); } // Установка текущего слоя g_iCurLayer = (iLayer - 1); PlaySound("button.wav", NULL, SND_FILENAME|SND_ASYNC); }

Возьмем к примеру кнопку слоя с номером 2. Когда вы щелкаете по ней, выполняется вызов функции и значение ее параметра iLayer равно 2. Функция уничтожает кнопки слоев, а затем создает снова без черной рамки вокруг. Затем функция проверяет, на какой слой указывает параметр iLayer. Она доходит до второй проверки и вновь уничтожает кнопку второго слоя. Затем кнопка создается вновь, но уже с черной рамкой вокруг, показывающей, что данный слой активен. В самом конце кода функции переменной g_iCurLayer также присваивается значение, соответствующее активному слою.



Функция vCreateMinimap()


Функция создания мини-карты вызывается приложением только один раз во время инициализации. Функция создает окно для мини-карты и размещает его в нижнем левом углу главного окна редактора. При вычислении координат этого окна я учитываю местоположение и размер главного окна редактора и размер мини-карты. Вот код, который создает окно:

// Создание окна мини-карты hWndMinimap = CreateWindowEx( WS_EX_LEFT|WS_EX_TOPMOST|WS_EX_TOOLWINDOW, "Minimap", "Minimap", WS_BORDER | WS_VISIBLE | WS_MINIMIZEBOX, rcWindow.left + 10, rcWindow.bottom + g_iYOffset - 140, 100, 100, hwnd, NULL, hinst, NULL);

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

Теперь, когда у вас есть окно мини-карты, необходим код, который будет отображать саму мини-карту. Этим занимается функция vRenderMinimap().



Функция vDrawUnit()

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

void CD3DFramework::vDrawUnit( float fXPos, float fYPos, float fXSize, float fYSize, float fRot, CUnitAnimation *animObj, int iTexture, int iOwner) { D3DXMATRIX matWorld; D3DXMATRIX matRotation; D3DXMATRIX matTranslation; D3DXMATRIX matScale; // Установка значений по умолчанию для // местоположения, масштабирования и вращения D3DXMatrixIdentity(&matTranslation); // Масштабирование блока D3DXMatrixScaling(&matScale, fXSize, fYSize, 1.0f); D3DXMatrixMultiply(&matTranslation,&matTranslation,&matScale); // Вращение блока D3DXMatrixRotationZ(&matRotation, (float)DegToRad(-fRot)); D3DXMatrixMultiply(&matWorld, &matTranslation, &matRotation); // Перемещение блока matWorld._41 = fXPos - 0.5f; // X-Pos matWorld._42 = fYPos + 0.5f; // Y-Pos // Установка матрицы m_pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld); // Используем буфер вершин блока m_pd3dDevice->SetStreamSource( 0, m_pVBUnit, 0, sizeof(TILEVERTEX)); // Используем фрмат вершин блока m_pd3dDevice->SetFVF(D3DFVF_TILEVERTEX); // Задаем используемую текстуру m_pd3dDevice->SetTexture( 0, animObj->m_Textures[iTexture].m_pTexture); // Отображаем квадрат m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); // Задаем используемую текстуру m_pd3dDevice->SetTexture( 0, animObj->m_Textures[iTexture + iOwner + 1].m_pTexture); // Отображаем квадрат m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); // Разыменовываем текстуру m_pd3dDevice->SetTexture(0, NULL); }

Первое отличие данной функции от ранее рассмотренной vDrawTile() — добавление параметра вращения. Он позволяет вам развернуть двухмерное изображение на любой необходимый угол. Само вращение реализуется перемножением матриц поворота и преобразования. Матрица поворота создается вспомогательной функцией DirectX D3DXMatrixRotationZ().

СОВЕТ В DirectX угол поворота всегда вычисляется в радианах. Для преобразования угловых величин в радианы я использую макрос DegToRad(). Вам в ваших программах также следует использовать аналогичную функцию или вращение графики и трехмерных объектов будет выполняться неправильно.

Следующее отличие рассматриваемой функции заключается в том, что я использую буфер вершин m_pVBUnit вместо буфера m_pVBTile. Это сделано для того, чтобы базовая точка располагалась в центре, о чем я говорил ранее.

Самое большое отличие данной функции — добавление параметра CUnitAnimation. Он сообщает функции откуда она должна брать текстуры. Указатель на класс анимации необходим потому, что именно в нем хранятся используемые для визуализации текстуры.

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



Функция vGenerateMap()


Структура этой программы редактирования карт практически полностью повторяет структуру предыдущего примера. Главные отличия сконцентрированы в функции генерации карты. Ее цель — генерировать случайно расположенные участки суши, базируясь на передаваемом в качестве параметра номере используемого алгоритма. В рассматриваемом примере я реализовал только один тип алгоритма генерации карты, но ничто не мешает вам добавить и другие! Вот как выглядит код функции:

void vGenerateMap(int iType) { int iRandDirection; int iSeedPos[32]; int i, j; int iNumSeeds = 32; int iNumUpdates = 800; // -- ТИП 0 -- Случайные семена if(iType == 0) { // Очиска карты vInitMap(); // Создание случайно расположенных начальных семян for(i = 0; i < iNumSeeds; i++) { // Установка начальной позиции семени iSeedPos[i] = rand() % (g_iMapHeight * g_iMapWidth); // Помещаем в позицию семени блок с изображением травы g_iTileMap[iSeedPos[i]] = 17; } // Перемещение семени for(i = 0; i < iNumUpdates; i++) { for(j = 0; j < iNumSeeds; j++) { iRandDirection = rand()%4; // перемещаем семя вверх if(iRandDirection == 0) { iSeedPos[j] -= g_iMapWidth; } // Перемещаем семя вправо else if(iRandDirection == 1) { iSeedPos[j]++; } // Перемещаем семя вниз else if(iRandDirection == 2) { iSeedPos[j] += g_iMapWidth; } // Перемещаем семя влево else if(iRandDirection == 3) { iSeedPos[j]--; } // Если семя вышло за пределы карты, // помещаем его в случайную позицию if(iSeedPos[j] < 0 || iSeedPos[j] >= (g_iMapHeight * g_iMapWidth)) { iSeedPos[j] = rand() % (g_iMapHeight * g_iMapWidth); } // Помещаем в позицию семени блок с изображением травы g_iTileMap[iSeedPos[j]] = 17; } } } // Отображение мини-карты vRenderMinimap(); // Воспроизведение звука, сообщающего о завершении операции PlaySound("bleep.wav",NULL,SND_FILENAME|SND_ASYNC); }

Код может выглядеть устрашающе, так что сперва взгляните на Рисунок 10.14, где показан ход выполнения функции.



Функция vRender()


Сейчас у вас есть буфер вершин для подразделения и функция, помогающая при визуализации. Белым пятном остается место, где создается изображение каждого кадра. Встречайте старого знакомого — функцию vRender().

Функция визуализации в программе D3DFrame_UnitTemplate работает во многом так же, как и одноименная функция из программы D3DFrame_2DTiles. В первой части выполняется визуализация карты посредством цикла, в котором перебираются и отображаются отдельные блоки карты. На этом подобие заканчивается.



Функция vRenderMinimap()

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

void vRenderMinimap(void) { RECT rectSrc; RECT rectDest; int iX; int iY; int iCurTile; int iBufferPos; // Очистить вторичный буфер, заполнив его синим цветом g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0); // Начинаем отображение сцены g_pd3dDevice->BeginScene(); // Визуализация мини-карты // Сверху вниз for(iY = 0; iY < g_iMapHeight; iY++) { // Справа налево for(iX = 0; iX < g_iMapWidth; iX++) { // Вычисление смещения в буфере iBufferPos = iX + (iY * g_iMapWidth); // Получаем требуемый блок iCurTile = g_iTileMap[iBufferPos]; // Отображаем блок vDrawInterfaceObject((iX), (iY), (float)1, (float)1, iCurTile); } } // Завершаем сцену g_pd3dDevice->EndScene(); // Исходный прямоугольник rectSrc.top = 0; rectSrc.bottom = g_iMapHeight; rectSrc.left = 0; rectSrc.right = g_iMapWidth; // Прямоугольник места назначения rectDest.top = 0; rectDest.bottom = g_iMapHeight; rectDest.left = 0; rectDest.right = g_iMapWidth; // Представляем результат g_pd3dDevice->Present(&rectSrc, &rectDest, hWndMinimap, NULL); }

Я отметил наиболее интересные части функции полужирным курсивом. Обратите внимание, что при выводе я устанавливаю размер каждого блока равным 1 х 1 пикселу. Благодаря этому при отображении мини-карты происходит масштабирование каждого блока до размеров единственной точки. Важно заметить, что при этом точка представляет общий цвет блока, поскольку это масштабированное представление, а не замещающая текстура. Что такое замещающая текстура? Один из методов рисования мини-карты заключается в том, что каждому типу блоков назначается представляющий его цвет. Например, вода может изображаться квадратами синего цвета, земля — квадратами зеленого цветаа постройки — черными квадратами. В этом случае не надо выполнять масштабирование блока, а достаточно заменить блок на точку с цветом, соттветствующим функциональному назначению данного блока в игре. Лично я предпочитаю метод масштабирования, поскольку он позволяет получить более точное представление карты и обходится без дополнительного кода для замещения текстур.

СОВЕТ Если вы хотите получить мини-карту более крупного масштаба, надо только изменить размер окна мини-карты и размер блоков в цикле визуализации. Побробуйте сделать это!

Генерация случайной карты


Функция vInitMap() отвечает за создание случайной карты. Взгляните как выглядит код, выполняющий эти действия:

void vInitMap(void) { int i; // Заполнение карты случайными блоками for(i = 0; i < g_iMapWidth * g_iMapHeight; i++) { g_iTileMap[i] = rand()%3; } }

Возможно вы думаете «Что мне может дать случайное заполнение карты?». Гораздо больше, чем вы могли предположить. Хотя вы будуте редактировать почти каждый блок на игровом поле, случайный набор блоков является хорошей отправной точкой, благодаря которой карта будет выглядеть естественно. Скорее всего, вы не захотите вручную размещать каждый камень, куст или ягоду на нескольких десятках карт. Сначала это может быть забавно, но очень быстро вы устанете.

В коде видно, как я просматриваю весь буфер карты и присваиваю каждому блоку случайное значение в диапазоне от 0 до 2. В результате карта будет похожа на мешанину. Но есть несколько вещей, которые вы можете реализовать здесь. Например, вы можете задать параметры распределения случайных блоков. Пусть 10 процентов карты беспорядочно заполняются камнями, а 60 процентов — водой. Тпкие игры, как SimCity 4 и Civilization используют подобный метод при создании собственных карт. Позднее я подробнее рассмотрю автоматическую генерацию карт, так что пока прекратим вешать лапшу на уши.



Глобальные переменные для просмотра карты





На Рисунок 10.4 видно как ширина и высота карты задают ее общий размер. Также видно как размер окна просмотра задается в блоках. Кроме того, можно обратить внимание как координаты области просмотра задают ее положение на карте.



Глобальные переменные карты


В заголовочном файле main.h проекта находится несколько исключительно важных типов данных, используемых для просмотра карты. Вот их краткий список:

int g_iTileSize = 32; int g_iTilesWide = 20; int g_iTilesHigh = 15; int g_iMapWidth = 100; int g_iMapHeight = 100; int g_iXPos = 0; int g_iYPos = 0; int g_iTileMap[10000];

Первая переменная, g_iTileSize, сообщает программе просмотра карты сколько точек в ширину и в высоту занимают используемые блоки. Я присваиваю ей значение 32, следовательно ширина и высота моих блоков будут равны 32 точкам.

Вторая переменная, g_iTilesWide, сообшает программе сколько блоков должно помещаться в окне просмотра по горизонтали. Поскольку ширина используемого мной окна равна 640 точкам, а ширина блока равна 32 точкам, я присваиваю этой переменной значение 20, чтобы карта занимала все окно.

Третья переменная, g_iTilesHigh, работает точно так же, как g_iTilesWide, за исключением того, что задает количество блоков в окне по вертикали. Высота области просмотра равна 480 точкам, так что 15 блоков замечательно заполнят ее.

Четвертая переменная, g_iMapWidth, сообщает программе сколько блоков в карте по оси X. Поскольку программа просмотра может прокручивать карту, последняя может быть больше, чем область просмотра. Я задаю здесь значение 100, чего должно быть вполне достаточно для демонстрации прокрутки.

Пятая переменная, g_iMapHeight, работает точно так же как и предыдущее поле, за исключением того, что задает количество блоков в карте по оси Y. Ей я также присваиваю значение 100, чтобы карта была квадратной.

Шестая переменная, g_iXPos, сообщает программе просмотра в каком месте по оси X расположена область просмотра. Поскольку карта по размерам больше чем окно просмотра, программа должна отслеживать положение окна просмотра на карте. Это число не может быть отрицательным, поскольку отрицательные координаты располагаются за пределами карты.

Седьмая переменная, g_iYPos, задает вторую координату местоположения окна просмотра на карте.

Восьмая переменная, g_iTileMap, представляет собой массив целых чисел, описывающий блочную карту. В нем хранятся номера всех блоков, отображаемых на карте. Поскольку ширина и высота нашей карты равны 100 блокам, я создаю массив размером 10 000 элементов.

Назначение этих переменных и их значения представлены на Рисунок 10.4.



В заголовочном файле проекта main.h появилось несколько новых членов данных, необходмых для редактирования. Вот они в порядке их появления:

int g_iCurTile = 0; int g_iCurTileSet = 0; int g_iMaxTileSet = 3; int g_iTotalTiles = 18;

Первая переменная, g_iCurTile, сообщает редактору какой именно блок выбран пользователем для рисования в данный момент. Когда пользователь редактирует карту, на нее будет помещаться именно этот блок.

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

Далее идет переменная g_iMaxTileSet. Она сообщает системе сколько страниц может быть в наборе блоков. Фактически вы можете указать здесь сколь угодно большое число. Я использую его лишь для того, чтобы уберечь пользователя от погони за горизонтом.

Последний элемент, g_iTotalTiles, сообщает программе сколько блоков загружено в память. Это очень важная информация, поскольку она позволяет предотвратить выбор пользователем отсутствующих блоков, что может привести к краху программы. Я загружаю 18 блоков, но вы можете увеличить количество загружаемых блоков в соответствии с вашими потребностями, увеличив значение переменной.



Характеристики частиц


Теперь, когда вы познакомились с примерами частиц, пришло время узнать их основные характеристики. Для разработки игр вам необходима система частиц, которая будет обрабатывать используемые в игре частицы. Я предпочитаю использовать простой класс, но вы можете решить, что вашему проекту требуется диспетчер или иное, более сложное, решение. Основными характеристиками частиц являются:

Изображение Движение Анимация

Ход исполнения программы D3D_MapEditorPlusGold





На Рисунок 10.12 видно, как программа инициализирует DirectInput, клавиатуру, DirectGraphics, объекты интерфейса, блочную карту, панель инструментов и, наконец, окно мини-карты. Поскольку очень удобно, когда мини-карта размещается в собственном перемещаемом окне, я создаю отдельное окно специально для этой цели. Это делает функция vCreateMinimap().



Ход выполнения функции bFindPath()




Код начинается с помещения стартового узла в закрытый список. Затем он ищет все открытые узлы, расположенные вокруг текущего узла (им является стартовый узел). После того, как найдены все открытые узлы, код поверяет есть ли среди них цель. Если да, то путь инвертируется и функция возвращает код успешного завершения. Если цель отсутствует в открытом списке, код ищет открытый узел с наименьшей стоимостью и добавляет его в закрытый список. Процесс повторяется и будет завершен если найден путь до цели, либо если в открытом списке не осталось узлов, либо если превышено максимальное количество узлов пути.

Функция поиска пути не самая сложная и не самая большая, но именно она координирует усилия по поиску пути. Остальные функции выполняют лишь вспомогательные роли и вы должны приспособить их к вашей программе.

ПРИМЕЧАНИЕ Код поиска пути не оптимизирован. Не используйте его в своих проектах без предварительной оптимизации.

Ход выполнения функции проверки входных данных





На Рисунок 10.6 показан ход выполнения функции проверки входных данных. В первой части кода я проверяю буфер клавиатуры, чтобы убедиться, что пользователь нажимал на какие-нибудь клавиши. Если да, код проверяет какие именно клавиши нажаты. Если нажата клавиша Esc, программа завершает работу. Если нажата какая-нибудь клавиша управления курсором в коде соответствующим образом меняются значения координат области просмотра g_iXPos и g_iYPos. После того, как выполнена проверка нажатия клавиш управления курсором, код выполняет проверку, чтобы убедиться, что координаты находятся в допустимом диапазоне. Благодаря этому в окне просмотра не отображаются области, лежащие за границами карты.



Ход выполнения функции vGenerateMap()





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

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

Есть еще пара вещей, отсутствующих на иллюстрации. Во-первых, перед началом создания ландшафта я вызываю функцию vInitMap() для очистки карты. Это требуется для того, чтобы перед созданием случайно расположенных блоков с изображением травы массив карты был приведен в исходное состояние. Во-вторых, на иллюстрации отсутствует проверка, гарантирующая, что семя не будет блуждать за пределами карты. Если семя выходит за границы карты, оно помещается в случайную позицию внутри карты и продолжает формировать изображение суши в новом месте. И последняя отсутствующая вещь — вызов функции отображения миникарты, показывающей обновленный ландшафт.



Ход выполнения функции визуализации





На Рисунок 8.31 видно, что функция визуализации вызывает перед началом визуализации функцию обновления данных подразделений. Это важный шаг, так как кадры анимации подразделений должны быть обновлены до того, как начнется их визуализация. Технически вы можете выполнять обновление и позже, но главное, чтобы оно обязательно где-нибудь выполнялось!



Ход выполнения кода поиска пути в main cpp





Обратите внимание, как функция vInitPathing() использует при вычислении пути объект класса CPathFinder. Кроме того, на рисунке изображена функция iGetMapCost(), которая вычисляет базовую стоимость для данного узла карты. Вот ее код:

int iGetMapCost(int iX, int iY) { // Узел непроходим, если находится вне горизонтальных границ карты if(iX < 0 || iX >= g_iTilesWide) return(-1); // Узел непроходим, если находится вне вертикальных границ карты if(iY < 0 || iY >= g_iTilesHigh) return(-1); // Узел непроходим, если номер блока карты отличается от 0 if(g_iTileMap[iX + (iY * g_iMapWidth)][1] != 0) { return(-1); } // Для всех остальных случаев возвращаем стоимость блока else { return(g_iTileMap[iX + (iY * g_iMapWidth)][1]); } }

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

Как я говорил раньше, функция vInitPathing() использует функцию получения стоимости узла карты при обращении к объекту поиска пути. Вот код функции инициализации пути:

void vInitPathing(void) { bool bRet; int iTempX; int iTempY; int iDir; // Начальная и конечная позиции на карте int iNodeStartX; int iNodeStartY; int iNodeEndX; int iNodeEndY; // Таймеры DWORD dwStartTime; DWORD dwTotalTime; // Объект класса пути CPathFinder pathMyPath; // Очистить карту со стрелками // Она используется в дальнейшем для отображения пути for(int i = 0; i < g_iMapWidth * g_iMapHeight; i++) { g_iArrowMap[i] = -1; } // Ищем на карте исходный пункт for(int y = 0; y < g_iMapHeight; y++) { for(int x = 0; x < g_iMapWidth; x++) { if(g_iTileMap[x + (y * g_iMapWidth)][0] == 19) { g_iRabbitXPos = x; g_iRabbitYPos = y; // Сохраняем исходное состояние iNodeStartX = g_iRabbitXPos; iNodeStartY = g_iRabbitYPos; break; } } } // Ищем на карте конечный пункт for(y = 0; y < g_iMapHeight; y++) { for(int x = 0; x < g_iMapWidth; x++) { if(g_iTileMap[x + (y * g_iMapWidth)][0] == 20) { iNodeEndX = x; iNodeEndY = y; break; } } } // Обновляем отображаемое сообщение sprintf(g_szPathStatus, "CALCULATING PATH"); vRender(); // Задаем функцию получения стоимости pathMyPath.vSetCostFunction(iGetMapCost); // Запуск таймера dwStartTime = timeGetTime(); // Задаем начальную и конечную позиции pathMyPath.vSetStartState(iNodeStartX, iNodeStartY, iNodeEndX, iNodeEndY); // Ищем путь - максимальная длина 300 узлов bRet = pathMyPath.bFindPath(300); // Остановка таймера dwTotalTime = timeGetTime() - dwStartTime; // Выход в случае сбоя if(!bRet) { // Обновляем отображаемое сообщение sprintf(g_szPathStatus, "FAILED, OPEN = %d, CLOSED = %d, TIME = %ld", pathMyPath.m_iActiveOpenNodes, pathMyPath.m_iActiveClosedNodes, dwTotalTime); return; } else { // Обновляем отображаемое сообщение sprintf(g_szPathStatus, "COMPLETE, OPEN = %d, CLOSED = %d, TIME = %ld", pathMyPath.m_iActiveOpenNodes, pathMyPath.m_iActiveClosedNodes, dwTotalTime); } // Теперь следуем по пути CPathNode *GoalNode = pathMyPath.m_CompletePath->m_Path[0]; int iTotalNodes = 0; // Устанавливаем временную позицию, // чтобы определить направление стрелки iTempX = GoalNode->m_iX; iTempY = GoalNode->m_iY; // Старт из позиции 1, а не 0 iTotalNodes++; GoalNode = pathMyPath.m_CompletePath->m_Path[iTotalNodes]; // Перебираем в цикле составляющие путь узлы // Для каждого шага рисуем стрелку while(iTotalNodes < pathMyPath.m_CompletePath->m_iNumNodes) { // Определяем направление стрелки iDir = vFindDirection(iTempX, iTempY, GoalNode->m_iX, GoalNode->m_iY); // Сохраняем стрелку в карте стрелок g_iArrowMap[GoalNode->m_iX + (GoalNode->m_iY * g_iMapWidth)] = iDir; // Визуализируем сцену vRender(); // Устанавливаем временную позицию, // чтобы определить направление стрелки iTempX = GoalNode->m_iX; iTempY = GoalNode->m_iY; // Увеличиваем счетчик узлов iTotalNodes++; // Получаем следующий узел GoalNode = pathMyPath.m_CompletePath->m_Path[iTotalNodes]; }; }

Гм-м — придется просмотреть весьма много кода. Я знаю, что код выглядит сложно, но большая его часть предназначена для отображения стрелок, представляющих найденный путь. В первой части кода определяется где на карте кролик находится сначала и где он должен оказаться в конце. Поскольку начало и конец пути представлены на карте специальными блоками, код просто ищет их и сохраняет координаты их местоположения.

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

Все самое интересное происходит когда программа вызывает принадлежащую объекту поиска пути функцию bFindPath(). Именно она выполняет работу по поиску наиболее эффективного пути на карте от начального до конечного пункта. Если путь найден, функция возвращает 1; если путь найти не удалось, функция возвращает 0.

Чтобы отобразить найденный путь на экране программа перебирает в цикле все входящие в путь узлы карты, начиная с первого, пока не доберется до цели. Проходя по пути она отображает стрелки, чтобы показать путь по которому кролик добирается от одного узла к другому. Направление вычисляется на основании данных о местоположении предыдущего узла пути относительно текущего. Здесь вступает в игру функция vFindDirection(). Она очень простая, поскольку лишь вычисляет какую именно стрелку необходимо отображать.



Ход выполнения процедуры отображения текста





На рис 9.11 видно как функция визуализации проверяет номер текущего экрана, чтобы выяснить что именно должно быть отображено. Если активен экран номер четыре, она переходит к визуализации текстового поля ввода. При этом сначала функция рисует основные элементы интерфейса. Затем она выводит изображение текстового поля ввода. Это простая текстура, которую я создал в Photoshop. После этого программа проверяет активен ли текстовый ввод. Если да, то проверяется значение таймера мерцания курсора. Если таймер завершил отсчет заданного временного промежутка, код проверяет скрыт курсор или нет и меняет его состояние на противоположное. Вернувшись к визуализации код отображает курсор, если он не скрыт. И последняя вешь, которую делает код, — отображение введенного имени игрока. Обратите внимание, что код отображает имя игрока независимо от активности текстового ввода. Даже если игрок не вводит текст, введенное имя игрока должно быть видно ему.



Ход выполнения программы


Ход выполнения программы очень похож на работу остальных примеров в этой книге. Сперва выполняется инициализация составляющих программу систем. После ее завершения программа начинает обработку сообщений и вводимых пользователем данных. Это продолжается до тех пор, пока пользователь не завершит работу приложения. Все это и кое-что еще показано на Рисунок 10.5.



Ход выполнения программы D3D_Particles





На Рисунок 13.4 видно, как функция WinMain() выполняет инициализацию системы, последовательно обращаясь к функциям InitD3D(), vInitInterfaceObjects() и vInitParticles(). С первыми двумя функциями мы уже встречались в предыдущих примерах, а вот функция инициазизации частиц новая и появляется в этом примере впервые. Ее цель — создание частиц для сцены и установка их атрибутов для анимации.



Ход выполнения программы просмотра карт





На Рисунок 10.5 появилась только одна новая функция — vInitMap().



Ход выполнения программы


Ход выполнения программы редактирования карты практически не отличается от работы программы просмотра карты. Также как и в программе просмотра карт, здесь сперва инициализируются клавиатура, система визуализации, текстуры и карта. Нововведением является инициализация панели инструментов. Панель инструментов содержит область выбора блоков, позволяющую указать тот блок, который будет использоваться для редактирования. После того, как все компоненты инициализированы, программа ожидает ввода данных пользователем и отображает карту. Ход выполнения программы показан на Рисунок 10.8.



Импорт данных из пяти различных





На Рисунок 8.23 показано как диспетчер подразделений загружает информацию в базовые типы из пяти различных файлов данных. Имена этих файлов BaseType_Defense.csv, BaseType_Offense.csv, BaseType_Movement.csv, BaseType_Unit.csv и BaseType_Animation.csv. Расширение имени файла .csv обозначает, что это файлы в формате с разделенными запятыми значениями. Такие файлы содержат значения, разделенные запятыми. Это общепринятый формат, поддерживаемый электронными таблицами, поскольку он позволяет сохранять данные в простом для импортирования формате. Лично я для ввода и редактирования информации о подразделениях использую программу работы с электронными таблицами Excel. Вот пример данных для базовых типов защиты:

Medium Heli Armor, 20, 2, 2, 30, 30, 0

Heavy Heli Armor, 30, 2, 2, 50, 100, 0

Light Heli Armor, 10, 2, 2, 20, 70, 0

Числа не имеют особого смысла, пока вы не увидите соответствующие им названия столбцов. В приведенном выше примере первый столбец содержит название типа защиты. Последующие столбцы содержат коэффициент защиты от пуль, коэффициент защиты от ракет, коэффициент защиты от лазера, коэффициент защиты от ручной схватки, максимальное количество очков повреждений и скорость восстановления.

Как видно из приведенных чисел, тяжелая броня обеспечивает лучшую защиту от пуль и рукопашной схватки, чем средняя или легкая. Это становится еще более очевидным, если загрузить данные в программу работы с электронными таблицами. Взгляните на Рисунок 8.24, чтобы увидеть как типы защиты выглядят в Excel.



Инициализация частиц


Код инициализации частиц выглядит следующим образом:

void vInitParticles(void) { // Инициализация каждой частицы for(int i = 0; i < TOTAL_PARTICLES; i++) { // Установка последовательности анимации текстур g_partExplosion[i].vSetTextures( rand() % 3, // Тип анимации 0, // Начальная текстура 5, // Конечная текстура 10); // Пауза между сменой текстур // Установка начального местоположения g_partExplosion[i].vSetPos( 0.0f + (rand() % g_iWindowWidth), // X 0.0f + (rand() % g_iWindowHeight), // Y 0.0f); // Z // Установка начальной скорости g_partExplosion[i].vSetSpeed( -1.0f + rand() % 2, // X -8.0f + rand() % 4, // Y 0.0f); // Z // Установка гравитационного воздействия g_partExplosion[i].vSetGravity( 0.0f, // X 0.1f, // Y 0.0f); // Z // Установка длительности жизни частицы g_partExplosion[i].vSetLife(200); } }

Функция в цикле перебирает все частицы, количество которых задается определенной в заголовочном файле константой TOTAL_PARTICLES. Для каждой частицы задается случайное местоположение и скорость. Затем задается гравитационное воздействие и, в самом конце, продолжительность жизни частицы устанавливается равной 200. Это сообщает системе, что частица будет существовать в течение 200 игровых тактов.

При случайном размещении я помещаю частицы где-нибудь в пределах экрана. При генерации случайной скорости используется небольшой диапазон значений для скорости перемещения по горизонтали и больший диапазон — для перемещения по вертикали. В результате создается впечатление, что частицы летят по экрану снизу вверх.

Тип анимации выбирается случайным образом, а диапазон используемых при анимации текстур для всех частиц будет от 0 до 5. Кроме того, я задаю паузу между сменой текстур, равной 10 для того, чтобы кадры анимации не сменяли друг друга слишком быстро.



Инициализация DirectInput


Откройте файл main.cpp и найдите код функции WinMain(). В ней вы найдете обычный код создания объектов Windows, за которым следует код инициализации DirectInput и устройства клавиатуры, выглядящий так:

// Инициализация DirectInput iResult = iInitDirectInput(); if(iResult != INPUTERROR_SUCCESS) { MessageBox(hWnd, "DirectInput Error", "Unable to initialize Direct Input.", MB_ICONERROR); vCleanup(); exit(1); } // Инициализация клавиатуры DI iResult = iInitKeyboard(hWnd); if(iResult != INPUTERROR_SUCCESS) { MessageBox(hWnd, "DirectInput Error", "Unable to initialize Keyboard.", MB_ICONERROR); vCleanup(); exit(1); }

В приведенном выше коде вызываются две функции: iInitDirectInput() и iInitKeyboard(). Вызов первой из них инициализирует главный объект DirectInput, а вызов второй создает устройство клавиатуры. Увидеть ход выполнения программы можно на Рисунок 9.3.



Рабочей лошадкой DirectInput является интерфейс IDirectInput8. Это COM-объект, отвечающий за настройку среды ввода. После того, как вы создали объект DirectInput можно создавать устройства для объекта. Как же можно создать этот объект? С помощью следующего кода:

if(FAILED(hReturn = DirectInput8Create( g_hInstance, DIRECTINPUT_VERSION, IID_IDirectInput8, (VOID**)&pDI, NULL))) { return(INPUTERROR_NODI); } Из приведенного кода видно, что объект DirectInput создает функция DirectInput8Create(). Вот как выглядит прототип этой функции:

HRESULT WINAPI DirectInput8Create( HINSTANCE hinst, DWORD dwVersion, REFIID riidltf, LPVOID *ppvOut, LPUNKNOWN punkOuter ); В первом параметре, hinst, должен передаваться дескриптор текущего экземпляра вызывающего приложения. В приведенном выше фрагменте кода я передаю глобальный указатель экземпляра с именем g_hInstance. Он содержит экземпляр приложения и инициализируется в главной функции окна.

Следующий параметр, dwVersion, содержит номер версии DirectInput, которую вы намереваетесь использовать. В приведенном выше примере я использую глобальную константу с именем DIRECTINPUT_VERSION. Ее значение равно 0x0800, и это значит, что мы намереваемся использовать DirectInput версии 8.

ПРИМЕЧАНИЕ Хотя эта книга посвящена использованию DirectX 9, компонент DirectInput не изменялся с версии 8. Третий параметр, riidltf, передает уникальный идентификатор интерфейса. Для DirectX 8 и 9 вы должны использовать идентификатор IID_IDirectInput8.

Четвертый параметр, ppvOut, содержит адрес указателя в котором будет сохранена ссылка на объект DirectInput. Для этого параметра я использую глобальный указатель с именем pDI. Тип указателя pDI — LPDIRECTINPUT8.

Последний параметр, punkOuter, используется для указания на интерфейс IUnknown COM-объекта. Я всегда передаю в этом параметре значение NULL, и вы можете поступать так же.

Если работа функции завершена успешно, она возвращает значение DI_OK.


Объект DirectInput создает устройства в виде объектов интерфейса IDirectInputDevice8. Интерфейс IDirectInputDevice8 выполняет большую часть работы по поддержке конкретного устройства. Чтобы создать интерфейс устройства вы должны вызвать метод CreateDevice() главного объекта DirectInput. Вот как выглядит его прототип:

HRESULT CreateDevice( REFGUID rguid, LPDIRECTINPUTDEVICE *lplpDirectInputDevice, LPUNKNOWN pUnkOuter ); В первом параметре, rguid, передается GUID создаваемого устройства. Для этой цели каждый тип устройств в DirectX имеет свой собственный GUID. Если вы хотите создать интерфейс клавиатуры, передайте в этом параметре идентификатор GUID_SysKeyboard. Чтобы создать интерфейс мыши, передайте идентификатор GUID_SysMouse.

Второй параметр, lplpDirectInputDevice, представляет собой указатель на указатель на создаваемое новое устройство. В своих примерах я передаю указатель с именем pKeyboard типа LPDIRECTINPUTDEVICE8.

Последний параметр применяется для COM, и большинство людей просто передают здесь NULL.

В случае успешного завершения функция возвращает значение DI_OK.

Интерфейс шрифта


Возможно, рассматривая код визуализации вы заметили, что для отображения текста на экране я использую объект с именем pD3DXFont. Это экземпляр предоставляемого DirectX интерфейса ID3DXFont. Данный интерфейс очень полезен, так как выполняет все необходимое для отображения шрифтов в Direct3D. Вам надо лишь указать дескриптор шрифта и выводимый текст. Это действительно просто! Если вы взглянете на функцию инициализации объектов интерфейсов, то увидите следующий код:

// Шрифт текста hFont = CreateFont(16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, PROOF_QUALITY, 0, "fixedsys"); D3DXCreateFont(g_pd3dDevice, hFont, &pD3DXFont);

В первой строке вызывается системная функция CreateFont(). Она является частью системы GDI Windows и создаеет дескриптор шрифта, получая в качестве параметров имя шрифта, его размер и ряд других атрибутов. Подробные сведения об этой функции вы найдете в справочнике MSDN.

После того, как вы получили дескриптор шрифта, остается только вызвать функцию D3DXCreateFont(). Эта функция получает указатель на устройство Direct3D, дескриптор шрифта и адрес указателя на объект ID3DXFont. В результате выполнения функции в указатель на объект ID3DXFont помещается ссылка на созданный интерфейс шрифта, который будет использоваться в дальнейшем при визуализации.

В рассматриваемом примере я создаю моноширинный шрифт. Я люблю моноширинные шрифты, поскольку при их использовании очень просто вычислить длину создаваемой строки. В пропорциональных шрифтах ширина символов неодинакова. Это создает кучу проблем, особенно когда требуется отобразить курсор в конце текста!

Вернемся к коду визуализации. Для отображения только что созданного шрифта я обращаюсь к функции интерфейса шрифта DrawText().



Использование альфаканала


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

// Включение прозрачности m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE); m_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); m_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

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



Использование шаблонов для генерации случайного ландшафта





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



Мы уже достигли конца очередной


Мы уже достигли конца очередной главы? Это произошло так быстро! Вот несколько секретов, которые мы узнали в этой главе:
Вот четыре блока, которые являются фунндаментом разработки подразделений: атака, защита, передвижение и анимация. Базовые типы подразделений помогают сократить объем используемой памяти. Базовые типы подразделений помогают организовывать подразделения в группы. Диспетчер подразделений упрощает контроль за подразделениями. Использование текстовых файлов для хранения данных подразделений поможет сделать вашу игру более гибкой. Использование текстовых файлов для хранения данных подразделений поможет сделать вашу игру более гибкой. исходный текст перевода взят с сайта netlib.narod.ru
и собран специально для
http://www.natahaus.ru/

В этой главе я показал вам как реализовать в ваших играх ввод данных с клавиатуры. Есть множество доступных методов ввода, и я надеюсь, что вы возьмете показанное вам в этой главе за основу более гибких и функциональных систем. Перед тем, как продолжить чтение, обратите внимание на следующие моменты:
DirectInput предоставляет все необходимое для работы с клавиатурой в вашей игре. Он также предоставляет возможность работать с другими типами устройств ввода, необходимыми для игры. Коды DIK — это внутренние коды клавиатуры, назначенные DirectInput. С помощью DirectInput можно получить коды ASCII, но для этого вы должны сами преобразовывать коды DIK. Буферизованный ввод необходим. Никогда не реализуйте методы ввода без буферизации. Интерфейс ID3DXFont предоставляет вам мощные средства для отображения текста. предоставляет вам мощные средства для отображения текста. исходный текст перевода взят с сайта netlib.narod.ru
и собран специально для
http://www.natahaus.ru/


Эта глава познакомила вас с основами редактирования, просмотра и генерации карт, а также с методами отображения мини-карты. Вспомните следующие ключевые моменты:
Редактор карт является одной из наиболее важных частей вашего проекта. Если с вашим редактором карт сложно работать, создание карт для игры будет занимать очень много времени и игроки не захотят создавать свои собственные карты. Кнопки интерфейса редактора карт хорошо размещать на панели инструментов. Сделайте панель инструментов перемещаемой, чтобы она не занимала ценное пространство области редактирования. Мини-карта выглядит более реалистично, если при ее отображении вы масштабируете исходные блоки, а не используете цветовое представление блоков. Учтите, что это влияет на скорость, поскольку для масштабирования требуются дополнительные ресурсы системы визуализации. Существует множество методов алгоритмической генерации карт. Попробуйте сперва продемонстрированный мной простейший метод, а затем усовершенствуйте его, пока не получите мощный генератор случайных ландшафтов. Слои предоставляют вам большую свободу творчества при создании карт. Благодаря им вы сможете использовать перекрытия, полупрозрачные блоки и многие другие эффекты.


Я предоставил вам краткий обзор частиц с точки зрения их использования в разработке игр. Взяв его за основу вы можете создавать собственные реализации систем частиц. Есть сотни вещей, которые можно сделать при помощи частиц, и ограничивает вас в этом только собственное воображение. Если вы создадите замечательный пример использования частиц, сообщите мне об этом по электронной почте и я размещу его на своем сайте. Вот несколько моментов, заслуживающих вашего внимания:
Частицы — это мелкие фрагметы крупных объектов. Хотя частицы небольшие, вы можете использовать для их анимации любые графические изображения. Анимация текстур улучшит внешний вид ваших частиц и обеспечит дополнительную гибкость проекту. Для управления частицами можно использовать класс системы частиц. собран специально для http://www.natahaus.ru/

Итоги и оптимизация


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

Выполняя поиск среди открытых узлов, начинайте его с тех узлов, которые расположены ближе к цели. Во многих случаях это поможет сократить количество узлов, которые требуется просмотреть при поиске пути. Не помещайте другие подразделения на карту препятствий. Если вы поступите так, перемещения подразделений могут вызвать проблемы. Одно из возможных решений состоит в том, чтобы при поиске пути не учитывать другие подразделения и в случае столкновения с другим подразделением заново рассчитывать путь. Если подразделения не блокируют друг друга, это не требуется. Используйте диспетчер путей для управления фиксированным пулом путей. Если вы используете пул путей, это поможет вам сократить нагрузку на процессор, вызванную необходимостью динамического формирования каждого пути. Используйте многоуровневый поиск пути. Для этого применяйте крупномасштабную карту препятствий, каждый узел которой соответствует нескольким блокам обычной карты, что позволит маневрировать среди больших ландшафтных препятствий. Как только подразделение достигнет более сложной области, переключитесь на карту препятствий более мелкого масштаба с большим количеством узлов. Многоуровневый поиск пути может значительно ускорить работу кода. Никогда не вычисляйте перемещение всех подразделений сразу. Создайте очередь путей и вычисляйте на каждом такте игры пути для небольшого числа подразделений. Если в вашей игре разворот подразделений занимает какое-то время, добавьте к вычислениям общей стоимости каждого узла карты стоимость смены направления движения. Это позволит выбирать для подразделений наиболее эффективный путь с наименьшим количеством поворотов. Кроме того, вы можете сделать ряд других подобных дополнений к стоимости узла карты. Например, вы можете увеличить стоимость узлов, расположенных рядом со вражескими позициями, чтобы предотвратить случайное вступление ваших подразделений в бой! Алгоритм А* работает на картах различных типов, в том числе и на тех, где форма блоков отличается от квадратной. Испытайте его на картах с шестиугольными блоками или даже на маршрутных картах. собран специально для http://www.natahaus.ru/

Изменение частиц с течением времени





На Рисунок 13.2 изображена ракета, след за которой образуют частицы дыма. Интересно то, что изображающие дым частицы со временем меняют свой цвет от темно-серого до светло-серого. Вместо того, чтобы для каждой позиции дымового следа использовать собственную частицу, вы используете одни и те же частицы, но добавляете анимацию, изменяющую их цвет от темно-серого до светло-серого. Это базовый принцип анимации частиц. Существуют и другие варианты. Вы можете анимировать размер частиц или используемую текстуру. Границы задает только ваше воображение.



Изменение процедур сохранения и загрузки

Функции vSaveMap() и vLoadMap() модифицированы для включения в каждую карту информации о дополнительных слоях. Поскольку в примере поддерживается четыре слоя, будет сохраняться и записываться в четыре раза больше данных. Необходимые для этого изменения кода минимальны. Приведенная ниже строка показывает изменения, необходимые для функции vLoadMap():

fread(g_iTileMap, 40000, sizeof(int), fp);

Обратите внимание, что функция fread() считывает 40 000 целых чисел, а не 10 000, как раньше. Аналогичные изменения вносятся и в функцию vSaveMap():

fwrite(g_iTileMap, 40000, sizeof(int), fp);

В функции сохранения карты количество записываемых чисел также изменено с 10 000 на 40 000. Это единственное изменение, которое необходимо сделать в функции записи.

ВНИМАНИЕ! Не пвтайтесь загружать карты, созданные одной версией редактора в другую версию. Это может привести к краху программы.

Изменения в функции vCheckMouse()


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

g_iTileMap[iTileX+g_iXPos+ ((iTileY + g_iYPos) * g_iMapWidth)][g_iCurLayer] = g_iCurTile;

В коде видно, что теперь номер блока помещается в многомерный массив g_iTileMap. Поскольку карта теперь состоит из нескольких слоев, для того, чтобы определить, в какой именно слой должен быть помещен блок, я использую переменную g_iCurLayer.

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