Программирование стратегических игр с DirectX 9.0

         

Активация ввода текста


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

// Экран начала новой игры else if(iMenu == 4) { MZones.vFreeZones(); MZones.vInitialize(1); MZones.iAddZone("EXIT_BUTTON", 587, 0, 53, 24, 0); // // Установка поля ввода текста // // Установка позиции курсора g_shTextInputXPos = 200; g_shTextInputYPos = 196; // Очистка текста memset(g_szTextInputBuffer, 0x00, 64); // Установка позиции ввода данных g_shTextInputPosition = 0; // Установка активного поля данных g_iTextInputFieldID = GAMEINPUT_NAME; // Установка флага активности поля ввода g_bTextInputActive = 1; // Установка таймера мерцания курсора g_dwTextInputTimer = 0; // Установка состояния мерцания курсора g_bTextInputCursorFlash = 0; // Установка максимальной длинны текста: 20 символов g_shTextMaxSize = 20; }

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



Таблица 9.2. Глобальные переменные, управляющие вводом текста

Переменная Описание
g_shTextInputXPos Координата X текстового поля ввода.
g_shTextInputYPos Координата Y текстового поля ввода.
g_szTextInputBuffer Хранит содержимое текстового поля.
g_shTextInputPosition Активная позиция в текстовом поле.
g_iTextInputFieldID Следит, какое текстовое поле активно.
g_bTextInputActive Сообщает системе, что текстовый ввод включен.
g_dwTextInputTimer Таймер для анимации курсора в активном текстовом поле.
g_bTextInputCursorFlash Определяет включен курсор или выключен во время мерцания.
g_shTextMaxSize Максимальное количество символов в буфере.
<
В приведенном выше коде я устанавливаю координаты текстового поля таким образом, чтобы они указывали на верхний левый угол первого символа в поле с именем игрока. Благодаря этому система визуализации будет знать где отображать текст с именем, когда он будет введен. Кроме того, эти значения сообщают системе визуализации где отображать курсор.

Затем устанавливается позиция ввода текста. Я присваиваю этой переменной 0, чтобы игрок вводил текст с начала буфера имени.

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

Потом я присваиваю полю g_bTextInputActive значение 1. Оно сообщает программе, что текстовое поле активно и ожидает ввод. Это важно знать, так как программа должна добавлять текст в поле и отображать его.

После того, как текстовое поле активизировано, я присваиваю 0 переменной g_dwTextInputTimer. Данный таймер отвечает за анимацию курсора. Следующая переменная, g_bTextInputCursorFlash, определяет включен курсор или выключен. Когда таймер курсора заканчивает отсчет она меняет свое состояние.

Последнее, что требуется сделать для инициализации текстового ввода — задать максимальное количество символов в имени игрока. Я делаю это присваивая переменной g_shTextMaxSize значение 20.


Буферизованный ввод с клавиатуры


Следующая часть функции может показаться вам странной, поскольку пока я еще не объяснил ее назначение. Дело в том, что для клавиатуры имеется два способа получения входных данных: непосредственный и буферизованный. Непосредственный ввод позволяет получить состояние клавиш на момент опроса. Если пользователь нажал клавишу хотя бы на 1/100 секунды раньше, это событие будет пропущено, поскольку оно не произошло именно в тот момент, когда выполнялась проверка. В игре это представляет серьезную проблему, поскольку циклы визуализации и обработки данных отнимают много времени, что может привести к частой потере вводимых данных. Данный момент проиллюстрирован на рис.9.4.


Рис. 9.4. Непосредственное чтение данных клавиатуры

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

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


Рис. 9.5. Буферизованный ввод с клавиатуры

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



Чтение данных клавиатуры


Вернемся к функции WinMain() и рассмотрим следующий фрагмент кода:

while(msg.message != WM_QUIT) { if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } else { // Чтение из буфера клавиатуры iResult = iReadKeyboard(); // Проверяем, сколько нажатий на клавиши возвращено if(iResult) { // Цикл обработки полученных данных for(i = 0; i < iResult; i++) { // Выход из программы, если нажата клавиша ESC if(diks[DIK_ESCAPE][i]) { PostQuitMessage(0); } else if (ascKeys[13][i]) { PostQuitMessage(0); } } } } }

Представленный код является стандартным циклом обработки сообщений Windows. Его ключевой особенностью является вызов функции iReadKeyboard(). Обращение к ней происходит каждый раз, когда в очереди нет системных сообщений для обработки. Функция возвращает количество зафиксированных изменений состояний клавиш и сохраняет их в глобальных массивах diks и ascKeys. Если функция возвратила какие-нибудь данные, программа в цикле перебирает полученные изменения состояний клавиш и проверяет не была ли нажата клавиша Esc. Если клавиша была нажата, выполнение программы завершается.



Функция 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 для шрифта.

netlib.narod.ru< Назад | Оглавление | Далее >



Функция 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() для создания устройства клавиатуры. Ранее я уже описывал эту функцию, так что код должен выглядеть для вас очень знакомо.



Функция 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.



Инициализация 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.


Рис. 9.3. Ход выполнения программы DInput_Simple



Рабочей лошадкой 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.

netlib.narod.ru< Назад | Оглавление | Далее >

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


Возможно, рассматривая код визуализации вы заметили, что для отображения текста на экране я использую объект с именем 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().



Навигация по меню


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

if(g_iCurrentScreen == 0) { // Переход к главному меню if(!stricmp(szZoneHit, "TITLE_SCREEN")) { // Делаем главное меню активным g_iCurrentScreen = 1; // Устанавливаем активные зоны vSetupMouseZones(1); } // Переход к экрану завершения игры else if(!stricmp(szZoneHit, "EXIT_BUTTON")) { // Делаем экран завершения текущим g_iCurrentScreen = 2; // Устанавливаем активные зоны vSetupMouseZones(2); } }

Приведенный код выполняется когда активен титульный экран. Если пользователь выбирает активную зону TITLE_SCREEN, программа делает активным экран главного меню и устанавливает активные зоны для него. Если выбрана кнопка Exit, код активирует экран выхода из игры и устанавливает активные зоны для него. Такие же действия выполняются в коде для каждого доступного пункта меню. Ход выполнения функции проверки ввода показан на рис. 9.9.


Рис. 9.9. Структура функции проверки ввода

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



Обработка текстового ввода


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

// ВВОД С КЛАВИАТУРЫ // Чтение из буфера клавиатуры int iResult = iReadKeyboard(); // Проверяем, сколько нажатий на клавиши возвращено if(iResult) { // Цикл перебора полученных данных for(int i = 0; i < iResult; i++) { // Выход из программы, если нажата клавиша ESC if(diks[DIK_ESCAPE][i]) { PostQuitMessage(0); } // ЛОГИКА РАБОТЫ ТЕКСТОВОГО ВВОДА if(g_bTextInputActive) { // Не сохранять текст, если для него нет места в буфере if(g_shTextInputPosition < g_shTextMaxSize) { // Сохраняем отпущенные клавиши for(int j = 32; j < 123; j++) { // Проверяем, что введен допустимый символ if((j > 96) || (j == 32) || (j > 47 && j < 58)) { // Проверяем, что клавиша отпущена if(ascKeys[j][i]) { if(g_bShift) { g_szTextInputBuffer[ g_shTextInputPosition] = toupper(j); } else { g_szTextInputBuffer[g_shTextInputPosition] = j; } g_shTextInputPosition++; } } } } // Проверяем не нажата ли клавиша удаления символа if(diks[DIK_BACK][i]) { // Проверяем введен ли какой-нибудь текст if(g_shTextInputPosition) { // Удаляем последний символ g_szTextInputBuffer[g_shTextInputPosition - 1] = '\0'; // Сдвигаем курсор назад g_shTextInputPosition--; } } // Проверяем не нажата ли клавиша ENTER if(diks[DIK_RETURN][i]) { // Завершаем ввод имени g_bTextInputActive = 0; // АКТИВАЦИЯ НОВОЙ ИГРЫ if(g_iTextInputFieldID == GAMEINPUT_NAME) { // Делаем текущим основной игровой экран g_iCurrentScreen = 5; // Устанавливаем активные зоны vSetupMouseZones(5); } break; } } } }

Это достаточно большой фрагмент кода, так что взгляните на рис. 9.10.


Рис. 9.10. Блок-схема ввода данных с клавиатуры


На рис. 9. 10 показана логика, необходимая для получения данных от клавиатуры и их помещения в текстовое поле. Начнем сверху: программа вызывает функцию чтения с клавиатуры, чтобы проверить есть ли какие-либо ожидающие обработки данные. Если есть, система в цикле перебирает полученные данные и выполняет ряд проверок. Сперва проверяется не была ли нажата клавиша Esc. Если да, программа помещает в очередь сообщение о выходе и завершает работу. Если нет, работа продолжается и выполняется проверка активности текстового поля. Если текстовое поле активно, система проверяет осталось ли в текстовом поле свободное место для ввода очередного символа. Если свободное место обнаружено, программа в цикле перебирает все клавиши клавиатуры и проверяет состояние каждой из них. Если проверяемая на данной итерации цикла клавиша является алфавитно-цифровой или пробелом, программа проверяет, была ли данная клавиша отпущена. Если клавиша была отпущена, проверяется нажата ли клавиша Shift. Если да, программа помещает в буфер имени игрока символ данной клавиши в верхнем регистре. Если клавиша Shift не нажата, в буфер помещается полученный по умолчанию символ. Данный процесс повторяется, пока не будут обработаны все состояния клавиш, находящиеся в буфере DirectInput.

Кроме того, на рис. 9.10 изображены проверки нажатия клавиш Backspace и Enter. Если игрок нажимает клавишу Backspace, программа удаляет последний символ в буфере имени игрока и передвигает курсор на одну позицию назад. Если нажата клавиша Enter, программа переходик к экрану новой игры и деактивирует текстовый ввод.


Обзор DirectInput


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


Рис. 9.1. Объекты DirectInput

На рис. 9.1 изображен главный объект DirectInput с двумя объектами устройств. Левый объект является мышью, а правый — клавиатурой. Под мышью изображен экземпляр объекта устройства, представляющий кнопки мыши. Под клавиатурой находится экземпляр объекта устройства, представляющий клавиши клавиатуры.



Определение состояния DIK


Массив didKeyboardBuffer хранит данные возвращенные DirectInput. Чтобы сделать их читаемыми, необходимо проверить значение каждого элемента массива. Если результат поразрядной логической операции И над возвращенным значением и константой 0x80 не равен нулю, значит клавиша была нажата; в ином случае клавиша была отпущена. Я знаю, это выглядит причудливо, но именно так работает DirectInput!



Отображение введенного текста


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

// Отображение экрана новой игры vDrawInterfaceObject(0, 0, 256.0f, 256.0f, 0); vDrawInterfaceObject(256, 0, 256.0f, 256.0f, 1); vDrawInterfaceObject(512, 0, 256.0f, 256.0f, 2); vDrawInterfaceObject(0, 256, 256.0f, 256.0f, 3); vDrawInterfaceObject(256, 256, 256.0f, 256.0f, 4); vDrawInterfaceObject(512, 256, 256.0f, 256.0f, 5); // Поле ввода vDrawInterfaceObject(192, 64, 256.0f, 256.0f, 14); // Отображаем курсор, если ввод активен if(g_bTextInputActive) { // Обновление состояния мерцания курсора if(timeGetTime() > g_dwTextInputTimer) { if(g_bTextInputCursorFlash) { g_bTextInputCursorFlash = 0; g_dwTextInputTimer = timeGetTime() + 250; } else { g_bTextInputCursorFlash = 1; g_dwTextInputTimer = timeGetTime() + 250; } } // Рисуем курсор, если он не скрыт if(g_bTextInputCursorFlash) { vDrawInterfaceObject(g_shTextInputXPos + g_shTextInputPosition * 8, g_shTextInputYPos, 4.0f, 16.0f, 15); } } // Отображение текста // Создаем прямоугольник для текста RECT rectText = { g_shTextInputXPos, g_shTextInputYPos, g_shTextInputXPos + (g_shTextMaxSize * 8), g_shTextInputYPos + 20 }; // Выводим текст pD3DXFont->DrawText(g_szTextInputBuffer, -1, &rectText, DT_LEFT, D3DCOLOR_RGBA(255, 255, 255, 255));

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


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

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



Преобразование кода DIK в код ASCII


Для преобразования кодов DIK в коды ASCII я написал следующую функцию:

BYTE Scan2Ascii(DWORD scancode) { UINT vk;

// Преобразование скан-кода в код ASCII vk = MapVirtualKeyEx(scancode, 1, g_Layout); // Возвращаем код ASCII return(vk); }

Функция получает код клавиши DirectInput и вызывает функцию MapVirtualKeyEx() для преобразования его в ASCII. Для работы функции отображения кодов необходимы данные о раскладке клавиатуры, которые мы получили на этапе инициализации.

netlib.narod.ru< Назад | Оглавление | Далее >



Проект DInput_Simple


В сопроводительные файлы книги включен проект DInput_Simple. Он строит небольшое приложение, создающее объект клавиатуры и читающее поступающие от него данные. Окно программы показано на рис.9.2.


Рис. 9.2. Окно программы DInput_Simple

На рис. 9.2 изображено обычное окно с текстом, сообщающим, что для выхода из программы следует нажать на клавишу Esc. Вместо того, чтобы использовать для перехвата нажатия на клавишу Esc сообщения Windows, мы воспользуемся DirectInput и устройством клавиатуры. Теперь загрузите проект и следуйте за мной далее.

Проект содержит два файла: main.cpp и main.h. В файле main.cpp находится код реализации функций, а в заголовочном файле main.h сосредоточена вся заголовочная информация. Проекту необходимы две библиотеки: dxguid.lib и dinput8.lib. Библиотека dxguid.lib содержит уникальные GUID для устройств DirectInput. В библиотеке dinput8.lib находятся сами функции DirectInput.



Раскладка клавиатуры

В рассматриваемом примере я покажу вам как считывать коды клавиш DirectInput и ASCII-коды клавиш. Чтобы получить возможность преобразования кодов DIK в коды ASCII вы должны вызвать функцию GetKeyboardLayout(). Она получает раскладку подключенной к системе клавиатуры для дальнейшего использования.

ПРИМЕЧАНИЕ

Функция GetKeyboardLayout() не является необходимой для работы кода DirectInput. Я применяю ее только для преобразования кодов DIK в коды ASCII.

Этапы, необходимые для инициализации клавиатуры, показаны на рис.9.6.


Рис. 9.6. Этапы инициализации клавиатуры



Установка формата данных клавиатуры


Затем вы должны задать формат данных клавиатуры. Это простая формальность, для соблюдения которой достаточно вызвать функцию IDirectInputDevice8::SetDataFormat(). Функция получает один параметр, задающий формат данных устройства. Для клавиатуры используйте значение c_dfDIKeyboard. Если же вам необходимо задать формат данных для мыши, воспользуйтесь значением c_dfDIMouse.



Установка уровня кооперации


Поскольку DirectX предоставляет прямой доступ к аппаратуре, очень важен уровень кооперации устройства. Он определяет как программа может использовать данный ресурс совместно с другими приложениями. Если вы установите монопольный режим, больше никто не сможет воспользоваться данным ресурсом. Если вы установите совместный режим, то доступ к клавиатуре смогут получить все желающие. Уверен, вы можете вспомнить игры, которые не делят клавиатуру ни с кем. Мне на ум приходит EverQuest. Поскольку создатели игры не хотели, чтобы сторонние разработчики писали приложения для их игры, они заблокировали использование клавиатуры вне их программы. Это не слишком хорошо и может вызвать настоящие проблемы, если вы переключитесь из игры на другое приложение, чтобы проверить почту или сделать что-нибудь еще.

Для установки уровня кооперации применяется функция IDirectInputDevice8::SetCooperativeLevel(). Вот ее прототип:

HRESULT SetCooperativeLevel( HWND hwnd, DWORD dwFlags );

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

Второй параметр, dwFlags, задает уровень кооперации устройства. Доступные уровни перечислены в таблице9.1.

Таблица 9.1. Уровни кооперации устройств

Значение Описание
DISCL_BACKGROUND Доступ к клавиатуре будет предоставлен даже если окно свернуто.
DISCL_EXCLUSIVE Предоставляется монопольный доступ к клавиатуре, для всех остальных клавиатура недоступна.
DISCL_FOREGROUND Доступ к данным клавиатуры предоставляется только когда окно активно.
DISCL_NONEXCLUSIVE Устройство используется совместно с другими программами.
DISCL_NOWINKEY Блокирует клавишу Windows.

Для рассматриваемого примера я устанавливаю флаги уровня кооперации DISCL_NONEXCLUSIVE и DISCL_FOREGROUND. Благодаря этому программа использует клавиатуру совместно с другими приложениями, а сама может читать данные клавиатуры только когда ее окно активно.



Ввод с клавиатуры


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



Ввод текста в игре


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

Для начала взгляните на рис.9.7, где изображен пример ввода текста в игре.


Рис. 9.7. Пример ввода текста в игре

На рис. 9.7 вы видите уже ставший знакомым интерфейс игры Battle Armor с полем ввода текста в центре экрана. В это поле вводится имя игрока. Обратите внимание, что я ввел в это поле строку «Lost Logic» и курсор находится в конце введенного текста. Все графические элементы должны быть вручную обработаны в вашей игре, так что читайте дальше, чтобы выяснить как это происходит.

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


Рис. 9.8. Ход выполнения программы D3D_InputBox



Захват клавиатуры


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



Алгоритмы генерации карт


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


Рис. 10.13. Окно программы D3D_MapEditorGeneration

Обратите внимание на уже ставшие привычными элементы рисунка. Здесь есть панель инструментов, область редактирования и мини-карта. Новым элементом является появившаяся на панели инструментов кнопка Generate. Она очищает всю карту, заполняя ее водой, и создает случайно расположенные участки суши. Кроме того, вы можете заметить, что я увеличил размер мини-карты. Я сделал это для того, чтобы вы более ясно увидели эффект генерации случайной карты. Есть и еще одно изменение, которое нельзя заметить на рис. 10.13. Если вы запустите программу D3D_MapEditorGeneration, то увидите красный прямоугольник на мини-карте. Он показывает, какой именно фрагмент большой карты отображается в окне просмотра и помогает понять что именно вы сейчас редактируете.

Теперь загрузите проект D3D_MapEditorGeneration и следуйте за мной, а я буду рассказывать вам о коде программы.



Функция LoadMap()


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

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

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

netlib.narod.ru< Назад | Оглавление | Далее >



Функция 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().



Функция 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, где показан ход выполнения функции.


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


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

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

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


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

СОВЕТ

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

netlib.narod.ru< Назад | Оглавление | Далее >



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


Функция 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 используют подобный метод при создании собственных карт. Позднее я подробнее рассмотрю автоматическую генерацию карт, так что пока прекратим вешать лапшу на уши.



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


В заголовочном файле 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.


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

На рис. 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 блоков, но вы можете увеличить количество загружаемых блоков в соответствии с вашими потребностями, увеличив значение переменной.



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


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


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

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


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


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



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

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

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

Обратите внимание, что функция fread() считывает 40000 целых чисел, а не 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.

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

netlib.narod.ru< Назад | Оглавление | Далее >



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


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

hBUTTON_LAYER1 = CreateWindow( "BUTTON", "1", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 3, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER1, hinst, NULL); hBUTTON_LAYER2 = CreateWindow( "BUTTON", "2", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 25, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER2, hinst, NULL); hBUTTON_LAYER3 = CreateWindow( "BUTTON", "3", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 48, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER3, hinst, NULL); hBUTTON_LAYER4 = CreateWindow( "BUTTON", "4", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 71, 275, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_LAYER4, hinst, NULL);

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



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


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

// Слои for(iLayer = 0; iLayer < 4; iLayer++) {

// Вычисляем смещение в буфере iBufferPos = iX+g_iXPos+((iY+g_iYPos)*g_iMapWidth); // Получаем требуемый блок iCurTile = g_iTileMap[iBufferPos][iLayer]; // Отображаем блок if(iCurTile != 0 || iLayer == 0) {

vDrawInterfaceObject((iX * g_iTileSize), (iY * g_iTileSize), (float)g_iTileSize, (float)g_iTileSize, iCurTile); } }

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

На рис. 10.17 показано совмещение слоев в действии. Там изображены четыре слоя с блоками. Первый слой заполнен блоками с номером 1. Большая часть второго слоя заполнена блоками с номером 0, но кроме этого там есть несколько блоков с номером 2. Большая часть третьего слоя также заполнена блоками с номером 0, но на нем есть и несколько блоков с номером 3. Аналогичным образом устроен и четвертый слой. Когда слои совмещаются вместе, блок с номером 0 работает как цветовой ключ для размещения второго, третьего и четвертого слоев поверх первого. Результат виден в нижней части иллюстрации. Рассмотрев изображенные в левой части рисунка отдельные блоки вы поймете, как он получен.


Рис. 10.17. Объединение слоев карты



Изменения в заголовочном файле


Программа D3D_MapEditorLayers базируется на предыдущих версиях редактора карт, так что большая его часть должна выглядеть для вас знакомо. Первое принципиальное отличие находится в заголовочном файле 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][4];

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



Компоненты редактора карт


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

Область редактирования.

Область выбора блоков.

Мини-карта.

Область вывода информации.



Методы генерации карт


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


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

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

netlib.narod.ru< Назад | Оглавление | Далее >



Мини-карта


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



Многомерный массив


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

Следующее изменение— добавление переменной g_iCurLayer. Она отслеживает с каким именно слоем карты ведется работа. Это очень важно знать, когда вы размещаете новый блок в окне редактирования. Программа должна знать куда его поместить!



Многослойные карты


Вы помните многослойные блоки, о которых рассказывалось в главе5? Если нет, вам лучше сейчас вернуться назад и повторить изложенный там материал. Слои позволяют вам отображать несколько блоков один поверх другого. Например, вы можете вывести блок с изображением травы, а затем добавить поверх него блок с изображением деревьев. Вы можете даже добавить поверх блока с изображением деревьев блок с изображением огня, чтобы показать лесной пожар. Открывающиеся возможности безграничны. В связи с этим возникает вопрос: как реализовать редактирование нескольких слоев в редакторе карт? Подумайте об этом, поскольку я собираюсь показать вам подобную возможность! Взгляните на рис. 10.16, где изображен редактор карт с поддержкой слоев.


Рис. 10.16. Окно программы D3D_MapEditorLayers

На рис. 10.16 показано окно программы D3D_MapEditorLayers. На панели инструментов появились четыре новые кнопки, отмеченные цифрами от 1 до 4. Они позволяют установить активный слой, который будет редактироваться. Например, чтобы редактировать базовый слой, щелкните по кнопке 1. После того, как вы выбрали первый слой, все щелчки по области редактирования будут менять текстуры, выводящиеся в первом слое блоков. Кнопки, отвечающие за другие слои работают аналогично. В изображенном на иллюстрации окне редактирования вы видите маленький песчанный остров с травой в центре. Изображение песка находится в слое 1, а изображение травы — в слое 2. Благодаря этому достигается плавный переход от травы к песку без необходимости вводить специальные переходные блоки с изображением травы и песка. Хватит обсуждать иллюстрацию. Загрузите программу D3D_MapEditorLayers и следуйте вперед.



Навигация по карте


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

void vCheckInput(void) { // Чтение из буфера клавиатуры int iResult = iReadKeyboard(); // Проверяем, сколько нажатий на клавиши возвращено if(iResult) { // Перебираем в цикле полученные данные for(int i = 0; i < iResult; i++) { // Выход из программы, если нажата клавиша ESC if(diks[DIK_ESCAPE][i]) { PostQuitMessage(0); } // Вверх if(diks[DIK_UP][i]) { g_iYPos--; } // Вниз if(diks[DIK_DOWN][i]) { g_iYPos++; } // Влево if(diks[DIK_LEFT][i]) { g_iXPos--; } // Вправо if(diks[DIK_RIGHT][i]) { g_iXPos++; } // Проверяем, не вышли ли за границы if(g_iYPos < 0) g_iYPos = 0; else if (g_iYPos >= (g_iMapHeight - g_iTilesHigh)) g_iYPos = (g_iMapHeight - g_iTilesHigh); if(g_iXPos < 0) g_iXPos = 0; else if (g_iXPos >= (g_iMapWidth - g_iTilesWide)) g_iXPos = (g_iMapWidth - g_iTilesWide); } } }

Иллюстрации всегда хорошо дополняют слова, так что взгляните на рис. 10.6, показывающий работу кода.


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

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



Область редактирования


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

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


Рис. 10.2. Редактор уровней Warcraft III с включенной сеткой

На рисунке показана область редактирования редактора уровней игры Warcraft III с включенной сеткой редактирования. Как видите, сетка ускоряет выравнивание плиток.



Область выбора блоков


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



Область вывода информации


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

netlib.narod.ru< Назад | Оглавление | Далее >



Основы редактирования карт

Первый вопрос, который вы должны задать: «Что такое редактор карт?». Редактор карт помогает вам собирать вместе графические блоки карты в формате, пригодном для использования в вашей игре. Он очень похож на программу для рисования, где в качестве холста выступает карта, а в качестве кистей— графические блоки.

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

И, наконец, последний вопрос: «На что похож редактор карт?». Если вы когда-нибудь использовали редактор уровней в игре, то уже знаете ответ на этот вопрос. Если нет, взгляните на рис. 10.1.


Рис. 10.1. Редактор уровней игры Warcraft III

На рис. 10.1 представлено окно редактора уровней игры Warcraft III, поставляемого вместе с игрой и являющегося очень мощным инструментом. Он позволяет вам редактировать карты, поставляемые с игрой, или создавать свои собственные с нуля. На рисунке вы можете видеть мини-карту, представляющую высокоуровневый взгляд на карту и крупный план редактируемой в данный момент области, занимающий большую часть окна редактора. Вокруг области редактирования есть различные панели инструментов, позволяющие выбирать текстуры и производимые с картой действия.

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

ПРИМЕЧАНИЕ

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

Отображение блоков на панели инструментов


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

void vRenderTileSet(void) { RECT rectDest; RECT rectSrc; int iX; int iY; int iTile;

// Включаем рассеянное освещение g_pd3dDevice->SetRenderState(D3DRS_AMBIENT, 0x00606060);

// Очищаем вторичный буфер и z-буфер g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0); // Начинаем визуализацию g_pd3dDevice->BeginScene();

// Задаем состояние альфа-смешивания // Это необходимо для реализации прозрачности/полупрозрачности g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

// Отображение активных блоков for(iY = 0; iY < 7; iY++) { for(iX = 0; iX < 3; iX++) { // Вычисляем отображаемый блок iTile = (g_iCurTileSet * 21) + (iX + (iY * 3)); // Отображаем, если это существующий блок if(iTile < g_iTotalTiles) { vDrawInterfaceObject( iX * g_iTileSize, iY * g_iTileSize, (float)g_iTileSize, (float)g_iTileSize, iTile); } // Рисуем рамку поверх текущего блока if(iTile == g_iCurTile) { vDrawInterfaceObject( iX * g_iTileSize, iY * g_iTileSize, (float)g_iTileSize, (float)g_iTileSize, 18); } } }

// Отображаем текущий блок vDrawInterfaceObject( 32, 32 * 7, (float)g_iTileSize, (float)g_iTileSize, g_iCurTile);

// Завершаем визуализацию g_pd3dDevice->EndScene();

// Исходный прямоугольник rectSrc.top = 0; rectSrc.bottom = g_iTileSize * 8; rectSrc.left = 0; rectSrc.right = g_iTileSize * 3;

// Целевой прямоугольник rectDest.top = 2; rectDest.bottom = (g_iTileSize * 8) + 2; rectDest.left = 0; rectDest.right = (g_iTileSize * 3);


g_pd3dDevice->Present(&rectSrc, &rectDest, hWndToolBar, NULL); }

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

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

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



Рис. 10.9. Структура панели инструментов

На рис. 10.9 видно как отображение блоков начинается в левом верхнем углу и продолжается сверху вниз. На иллюстрации текущим блоком является блок с номером 10 и поэтому именно вокруг него нарисован красный квадрат. Тот же самый блок скопирован и в нижней части области просмотра блоков, что так же указывает какой именно блок выбран. Еще ниже на панели инструментов выводятся кнопки для навигации по набору блоков.

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


Отображение мини-карты


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


Рис. 10.11. Окно программы D3D_MapEditorPlusGold

Правильно, — это старый редактор карт, но теперь — золотое издание! (Я знаю, что название программы становится все более причуддливым, но по крайней мере я не называю ее MapEditor 2700+ или как-нибудь еще в этом роде!)

На рис. 10.11 вы видите редактор карт, но теперь в нижнем левом углу есть небольшое окно в котором отображается мини-карта. В действительности это большая карта мира, на которой каждый блок представляется одной точкой. Это позволяет мне отобразить полную карту мира, размером 100 x 100 блоков в окне размером 100 x 100 точек. Загрузите проект D3D_MapEditorPlusGold и я покажу вам изменения, необходимые для реализации отображения мини-карты.

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


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

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



Переменные для новых кнопок


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

const int ID_BUTTON_LAYER1 = 40006; const int ID_BUTTON_LAYER2 = 40007; const int ID_BUTTON_LAYER3 = 40008; const int ID_BUTTON_LAYER4 = 40009; HWND hBUTTON_LAYER1 = NULL; HWND hBUTTON_LAYER2 = NULL; HWND hBUTTON_LAYER3 = NULL; HWND hBUTTON_LAYER4 = NULL;

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



Программирование панели инструментов


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

void vCreateToolbar(HWND hwnd, HINSTANCE hinst) { WNDCLASSEX wcToolBar; // Инициализация и регистрация класса окна панели инструментов wcToolBar.cbSize = sizeof(wcToolBar); wcToolBar.style = CS_HREDRAW | CS_VREDRAW; wcToolBar.lpfnWndProc = fnMessageProcessor; wcToolBar.cbClsExtra = 0; wcToolBar.cbWndExtra = 0; wcToolBar.hInstance = hinst; wcToolBar.hIcon = LoadIcon(NULL, IDI_APPLICATION); wcToolBar.hCursor = LoadCursor(NULL, IDC_ARROW); wcToolBar.hbrBackground= (HBRUSH) GetStockObject (COLOR_BACKGROUND); wcToolBar.lpszMenuName = NULL; wcToolBar.lpszClassName= "ToolBar"; wcToolBar.hIconSm = LoadIcon(NULL, IDI_APPLICATION); RegisterClassEx(&wcToolBar); // Создание окна панели инструментов hWndToolBar = CreateWindowEx( WS_EX_LEFT|WS_EX_TOPMOST|WS_EX_TOOLWINDOW, "ToolBar", "ToolBar", WS_BORDER | WS_VISIBLE | WS_CAPTION | WS_MINIMIZEBOX, g_iWindowWidth - 100, g_iYOffset, 100, g_iWindowHeight - 20, hwnd, NULL, hinst, NULL);

// Кнопка выбора предыдущего блока hBUTTON_PREVTILE = CreateWindow( "BUTTON", "<", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 10, 405, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_PREVTILE, hinst, NULL);

// Кнопка выбора следующего блока hBUTTON_NEXTTILE = CreateWindow( "BUTTON", ">", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 65, 405, 20, 20, hWndToolBar, (HMENU)ID_BUTTON_NEXTTILE, hinst, NULL);

// Активация области редактирования SetActiveWindow(g_hWnd);

// Отображение набора блоков на панели инструментов vRenderTileSet(); }

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

Функция CreateWindowEx() выполняет фактическое создание панели инструментов. Она создает окно с именем ToolBar без кнопок закрытия и свертывания. Это гарантирует, что пользователь случайно не закроет область выбора блоков. Кроме того, в функции создания окна я задаю местоположение панели инструментов таким образом, чтобы она находилась внутри границ главного окна.

После того, как окно панели инструментов создано, я создаю пару кнопок для навигации по набору блоков. Эти кнопки называются hBUTTON_PREVTILE и hBUTTON_NEXTTILE. Когда их нажимают, программа переходит к предыдущей или следующей странице странице набора блоков.



Просмотр карты


Хватит уже теории! Как насчет какого-нибудь кода, который покажет вам как создать свой собственный редактор карт? Загрузите проект D3D_MapViewer и следуйте вперед.

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

Запустите программу просмотра карт, и вы увидите окно, изображенное на рис.10.3.


Рис. 10.3. Окно программы просмотра карты

На рис. 10.3 вы видите выглядящий знакомым набор блоков, отображенный в окне несколько большего размера. Отличие этой блочной карты от примеров из главы 5 заключается в том, что вы можете передвигать карту с помощью клавиш управления курсором. Стрелки вверх и вниз вызывают перемещение вдоль оси Y, а стрелки влево и вправо — вдоль оси X. Запустите программу и проверьте это самостоятельно.

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



Редактирование карты


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


Рис. 10.7. Окно программы D3D_MapEditorLite

На рис. 10.7 изображено окно программы D3D_MapEditorLite. Оно выглядит очень похоже на окно программы просмотра карт, о которой вы уже читали. Главным отличием данной программы является добавление области выбора блоков и возможность редактировать карту в реальном времени.

Теперь загрузите проект D3D_MapEditorLite и следуйте дальше вместе со мной.


// Вычисляем координаты блока iTileX = iMouseX / g_iTileSize; iTileY = iMouseY / g_iTileSize; // Вычисляем, какой блок выбран g_iCurTile = (g_iCurTileSet * 21) + (iTileX + (iTileY * 3)) - 1; // Проверяем, что выбран существующий блок if(g_iCurTile < 0|| g_iCurTile >= g_iTotalTiles) { g_iCurTile = 0; } vRenderTileSet(); } // Проверяем, находится ли указатель мыши в окне редактирования else { GetWindowRect(g_hWnd, &rcWindow);

if(iMouseX > rcWindow.left && iMouseX < rcWindow.right && iMouseY > rcWindow.top && iMouseY < rcWindow.bottom) { // Преобразуем координаты указателя мыши // в локальные координаты окна редактирования iMouseX -= rcWindow.left + g_iXOffset; iMouseY -= rcWindow.top + g_iYOffset;

// Вычисляем координаты блока iTileX = iMouseX / g_iTileSize; iTileY = iMouseY / g_iTileSize;

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

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

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

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

netlib.narod.ru< Назад | Оглавление | Далее >

Сохранение и загрузка карты


Вау! Мы уже узнали как редактировать карту, так что не осталось никаких препятствий на пути к вершинам картографии. Редактирование карт— великолепная возможность, но она бессмысленна, если вы не можете сохранить результаты своей работы. Здесь на сцену выходит функция сохранения карты. В этом разделе я покажу вам как добавить к программе D3D_MapEditorLite полнофункциональные кнопки сохранения и загрузки. Загрузите новый улучшенный проект D3D_MapEditorPlus и пойдемте дальше. Сперва взгляните на рис. 10.10, где изображено окно этой программы.


Рис. 10.10. Окно программы D3D_MapEditorPlus

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



Визуализация блоков


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

// Сверху вниз for(iY = 0; iY < g_iTilesHigh; iY++) { // Справа налево for(iX = 0; iX < g_iTilesWide; iX++) { // Вычисляем смещение в буфере iBufferPos = iX + g_iXPos + ((iY + g_iYPos) * g_iMapWidth); // Получаем требуемый блок iCurTile = g_iTileMap[iBufferPos]; // отображаем блок vDrawInterfaceObject((iX * g_iTileSize), (iY * g_iTileSize), (float)g_iTileSize, (float)g_iTileSize, iCurTile); } }

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

netlib.narod.ru< Назад | Оглавление | Далее >



Загрузка изображений блоков


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

for(int i = 0; i < 3; i++) { // Установка имени sprintf(szTileName, "tile%d.bmp", i); // Загрузка if(FAILED(D3DXCreateTextureFromFile( g_pd3dDevice, szTileName, &g_pTexture[i]))) { return; } }

Загрузка блоков довольно проста. Я просто запускаю цикл, перебирающий доступные блоки и загружающий файлы с именами от tile0.bmp до tile2.bmp. Поскольку загружаются только три блока, процесс выполняется очень быстро. Для последующего использования загруженные блоки сохраняются в массиве g_pTexture.