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

         

Активные зоны и графика


Взгляните на начало схемы. Обратите внимание, что она начинается со стартовой заставки игры. В большинстве игр отображение заставки можно прервать, щелкнув мышью по кнопке на экране или нажав клавишу на клавиатуре. В схеме я описал кнопку на экране, которая позволяет игроку перейти от заставки к главному меню. Рядом с описанием кнопки я поместил индикатор «mzone», сообщающий что это будет активная зона для мыши. Активной зоной называется область экрана, по которой пользователь может щелкнуть мышью, чтобы были выполнены какие-то действия. Графика стартового экрана игры присутствует в схеме как отдельный элемент. Рядом с ним я поместил индикатор «bmp», указывающий, что это графический элемент.

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

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

Исследуя схему дальше, мы встречаем интерфейс загрузки игры. Здесь я описал графический элемент, содержащий список сохраненных ранее игр. Я также перечислил текстовое поле для ввода имени файла, кнопку загрузки и кнопку возврата к главному меню. Есть еще несколько активных зон, которые я не упомянул в схеме. Можете добавить их? Ответ будет дан чуть позже.

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


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

Вот что можно сказать о схеме интерфейса игры «Крестики-нолики». Она весьма обширна, несмотря на то, что «Крестики-нолики» — это очень простая игра. А теперь попробуйте вообразить как будет выглядеть схема интерфейса для игры из серии Command & Conquer. Ах, вы интересуетесь, что я пропустил в интерфейсе загрузки игры? Я пропустил графические элементы и активные зоны, которые позволят игроку прокручивать список записанных игр. Что делать, если список всех сохраненных игр не помещается на одном экране? Итак, вам потребуется полоса прокрутки и реагирующие на щелчки мыши области для кнопок. Вы могли подумать и о многих других элементах, которые я также не учел. Теперь напишите вашу собственную схему интерфейса для игры «Тетрис», и посмотрите, что получилось. Выможете быть удивлены сложностью этой схемы.


Архитектура проекта D3D_MouseZoneHighlights


Проект содержит четыре уникальных файла: main.cpp, main.h, MouseZoneClass.cpp и MouseZoneClass.h. Данный пример программы является усовершенствованной версией проекта D3D_MouseZones в которой сделано не так уж и много изменений.

Для программы необходимы следующие библиотеки: d3d9.lib, dxguid.lib, d3dx9dt.lib, d3dxof.lib, comctl32.lib и winmm.lib.



Архитектура проекта D3D_MouseZones


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

Программе необходимы следующие библиотеки: d3d9.lib, dxguid.lib, d3dx9dt.lib, d3dxof.lib, comctl32.lib и winmm.lib.



Архитектура проекта D3D_TitleScreen




Проект содержит два уникальных файла: main.cpp и main.h. Остальные, включенные в проект файлы, — d3dfont.cpp, d3dutil.cpp и dxutil.cpp, — являются частью Microsoft DirectX 9.0 SDK. Вы заметили, что список короче чем раньше? Это из-за того, что данный пример не использует предоставляемый Microsoft каркас приложения DirectX. В данном проекте мы воспользуемся лишь несколькими вспомогательными файлами для выполнения действий не имеющих отношения к каркасу приложения.

Программе необходимы библиотеки d3d9.lib, dxguid.lib, d3dx9dt.lib, d3dxof.lib, comctl32.lib и winmm.lib.



Блокировка буфера вершин


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

HRESULT Lock( UINT OffsetToLock, UINT SizeToLock, VOID **ppbData, DWORD Flags );

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

Второй параметр, SizeToLock, указывает количество байт, блокируемых в буфере вершин. Если вы хотите заблокировать весь буфер, укажите значение 0.

Третий параметр, ppbData, является указателем на блокируемый буфер вершин. Ничего себе имечко у него!

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

Таблица 6.9. Значения D3DLOCK

Значение Описание
D3DLOCK_DISCARD Приложение переписывает значения пользуясь только операциями записи. Этот метод применяется когда используются динамические данные, такие как динамические текстуры, и буферы вершин
D3DLOCK_NO_DIRTY_UPDATE Оберегает систему от изменения грязных областей данных. Флаг используется достаточно редко
D3DLOCK_NOSYSLOCK Предотвращает остановку действий системы, таких как перемещение курсора мыши, на время блокировки. Рекомендуется использовать, когда выполняется длительная по времени блокировка.
D3DLOCK_READONLY Буфер доступен только для чтения
D3DLOCK_NOOVERWRITE Система немедленно возвращается из функции блокировки, поскольку приложение обязуется не перезаписывать содержимое буфера. Этот вариант работает быстрее, чем блокировка только для чтения

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



Данные местоположения


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

Точка 1— (0,0,0)

Точка 2 — (0,10,0)

Точка 3 — (10,0,0)

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


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

Как видно из рис. 6.9, местоположение каждой из вершин определяет геометрию образуемого в трехмерном пространстве объекта. Если изменить местоположение, то при визуализации изменится и геометрия объекта. Понятно, что для трехмерной визуализации это очень важно. Для хранения данных о местоположении я использую тип данных D3DXVECTOR3, поскольку он содержит элементы для хранения координат по осям X, Y и Z.



Данные нормали


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


Рис. 6.10. Нормаль треугольной грани

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



Данные текстуры


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

Direct3D представляет информацию о координатах текстуры в виде декартовых координат на плоскости. Другими словами координаты текстуры содержат два числа— одно для оси X и одно для оси Y. Главное отличие координат текстуры заключается в том, что диапазон допустимых значений для них ограничен. Они могут принимать значения в диапазоне от 0.0 до 1.0. Это объясняется тем, что указываются относительные координаты, а реальные будут зависеть от размера изображения текстуры. Возьмем текстуру размером 16 на 16 точек. Координаты текстуры (0.5, 0.5) будут соответствовать центру текстуры с реальными координатами точки (8, 8). В случае текстуры размером 32 на 32 точки те же самые координаты текстуры будут соответствовать центру текстуры с реальными координатами (16, 16). Эту концепцию иллюстрирует рис. 6.11.


Рис. 6.11. Координаты текстуры указываются относительно ее размера

На рис. 6.11 представлены две сетки. Сетка слева изображает текстуру размером 16 на 16 точек. Сетка справа изображает текстуру размером 32 на 32 точки. Под каждой сеткой изображена шкала координат текстуры с диапазоном значений от 0.0 до 1.0. Поскольку шкала связана с размером текстуры, размеры шкал совпадают с размерами сеток. Шкала справа в два раза больше, чем шкала слева, но диапазон значений на ней все тот же — от 0.0 до 1.0. Я понимаю, что говорю очевидные вещи, но они важны для понимания координат текстур.

Корме того, для каждой сетки я показываю диапазон координат точек. Меньшая сетка начинается с точки (0, 0) и заканчивается точкой (15, 15). Большая текстура начинается с точки (0, 0) и простирается до точки (31, 31). Здесь нет ничего особенного; числа просто обозначают координаты точек.

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



Детализация схемы интерфейса


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

Взаимосвязи.

Звук.

Состояние.



Динамическое отображение меню


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

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

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



Двухмерная графика в Direct3D


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

Взгляните на рис. 6.8 и пристегните ремни — настало время писать код.


Рис. 6.8. Окно программы TitleScreen

На рис. 6.8 показано окно, полученное в результате работы программы D3D_TitleScreen. Эта программа отображает стартовый экран воображаемой игры Battle Armor, который выглядит так, будто нарисован с использованием двухмерной графики. (Возможно однажды у меня появится время, чтобы действительно закончить работу над игрой Battle Armor, но до тех пор будем просто притворяться, что это реальная игра.) Итак, начнем! А сейчас загрузите проект в Visual C++, чтобы двигаться дальше.



Файл MouseZoneClass.cpp


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



Файл программы Main.cpp


Следующий уникальный файл, main.cpp, содержит основную функциональность программы. Он следует стандартной структуре программы, которую я описал в главе2.



Файл программы Main.cpp


Следующий уникальный файл проекта называется main.cpp. Он содержит обычный код Windows и некоторый объем нового кода для обработки ввода от мыши и обнаружения активных зон.



Файл программы Main.cpp


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



Функции класса MouseZoneClass


Первые две объявленные функции— это конструктор и деструктор класса. В их прототипах нет ничего необычного; это просто обычная рутина программирования.

Следом за ними идет функция vInitialize(). Она получает единственный параметр, определяющий для скольких активных зон будет выделена память.

Функция vFreeZones() очищает всю память, выделенную для хранения данных активных зон и сбрасывает внутренние переменные.

Функция iAddZone() активирует новую зону в массиве m_HotSpots. Если свободная зона доступна, функция возвратит ее номер. Если же свободных зон больше нет, функция возвращает –1.

Функция iRemoveZone() делает указанную зону неактивной.

Функция bCheckZones() получает набор координат и состояние кнопок мыши, после чего определяет активирована ли какая-нибудь зона. Функция возвращает имя активной зоны. Если никакая зона не активирована, функция возвращает NULL.



Функция InitD3D()


Функция InitD3D() занимается трудной задачей создания среды визуализации Direct3D. Используемый в этом примере код выглядит очень простым, если его сравнивать с полноценной процедуой инициализации Direct3D. Например, я не потрудился перечислить доступные видеоадаптеры и устройства. Код просто настраивает среду выполнения и надеется, что все будет хорошо. Если у вас инициализация экрана не выполняется, можно попробовать изменить код. Это должно сработать, поскольку основная часть кода инициализации взята из DirectX SDK. Хватит предостережений, давайте взглянем на код функции:

HRESULT InitD3D(HWND hWnd) { D3DPRESENT_PARAMETERS d3dpp; D3DXMATRIX matproj, matview; D3DDISPLAYMODE d3ddm;

// Создание объекта D3D if(NULL == (g_pD3D = Direct3DCreate9(D3D_SDK_VERSION))) return E_FAIL;

// Получение видеорежима, используемого рабочим столом, чтобы мы могли // установить такой же формат для вторичного буфера if(FAILED(g_pD3D->GetAdapterDisplayMode( D3DADAPTER_DEFAULT, &d3ddm))) return E_FAIL;

// Создание вторичного буфера и установка формата ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.Windowed = TRUE; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.BackBufferFormat = d3ddm.Format; d3dpp.EnableAutoDepthStencil = FALSE;

// Создание D3DDevice if(FAILED(g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, DS3DDEVTYPE_HAL, hWnd, 3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &g_pd3dDevice))) { return E_FAIL; }

// Установка двухмерного представления и состояния визуализации D3DXMatrixIdentity(&matview); g_pd3dDevice->SetTransform(D3DTS_VIEW, &matview);

// Установка ортогональной проекции, т.е двухмерная графика в трехменом пространстве D3DXMatrixOrthoLH(&matproj, (float)g_iWindowWidth, (float)g_iWindowHeight, 0, 1); // Задание матрицы проецирования g_pd3dDevice->SetTransform(D3DTS_PROJECTION, &matproj); // Выключение отбраковки g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); // Выключение освещения g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE); // Выключение Z-буфера g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, FALSE); g_pd3dDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);

return S_OK; }

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


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

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



Функция MouseZoneClass::bCheckZones()


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

bool MouseZoneClass::bCheckZones( short shX, short shY, char *szZoneHit, bool bLeftDown, bool bRightDown)

В первых двух параметрах передаются скорректированные координаты указателя мыши по осям X и Y соответственно. Мы передадим координаты, которые вычислили чуть раньше.

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

В последних двух параметрах функции передается состояние кнопок мыши. Если кнопка нажата, передается 1, а если отпущена— 0. Я передаю здесь переменные g_bLeftButton и g_bRightButton.

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

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

0 Титульный экран
1 Главное меню
2 Экран завершения игры
3 Выход из программы
7 Меню Options

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


Рис. 6.26. Титульный экран, экран завершения, главное меню и меню параметров


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

bool MouseZoneClass::bCheckZones(short shX, short shY, char *szZoneHit, bool bLeftDown, bool bRightDown) { int i; for(i = (m_iMaxZones-1); i >= 0; i--) { // Проверим активна ли зона if(m_HotSpots[i].m_bActive == 1) { // Соответствует ли состояние кнопок требуемому? if((bLeftDown && m_HotSpots[i].m_shClickType == 0) || (bRightDown && m_HotSpots[i].m_shClickType == 1) || ((bRightDown || bLeftDown) && m_HotSpots[i].m_shClickType == 2) || ((!bRightDown && !bLeftDown) && m_HotSpots[i].m_shClickType == 3)) { // Проверка координат по горизонтали if(m_HotSpots[i].m_shZoneXPos <= shX { // Проверка координат по вертикали if(m_HotSpots[i].m_shZoneYPos <= shY) { // Попали ли в зону заданной ширины? if((m_HotSpots[i].m_shZoneXPos + m_HotSpots[i].m_shZoneWidth) >= shX) { // Попали ли в зону указанной высоты? if((m_HotSpots[i].m_shZoneYPos + m_HotSpots[i] .m_shZoneHeight) >= shY) { // Устанавливаем указатель на имя зоны strcpy(szZoneHit, m_HotSpots[i].m_szZoneName); // Возвращаем 1 (попадание) return(1); } } } } } } } // Возвращаем 0 (нет попадания) return(0); }

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

Сперва в цикле выполняется проверка включена ли зона. Если зона включена, мы переходим к проверке соответствия текущего состояния кнопок мыши типу щелчка, активирующего зону.

Если зона активируется щелчком левой кнопкой мыши, код проверяет равен ли тип щелчка 0 и равно ли 1 значение переменной bLeftDown.

Если зона активируется щелчком правой кнопкой мыши, код проверяет равен ли тип щелчка 1 и равно ли 1 значение переменной bRightDown.

Если зона может быть активирована щелчком любой кнопки мыши, код проверяет равен ли тип щелчка 2 и равно ли 1 значение переменной bLeftDown или bRightDown.

Если зона активируется, когда на нее наведен указатель мыши, а ни одна из кнопок не нажата, код проверяет равен ли тип щелчка 3 и равно ли 0 значение переменных bLeftDown и bRightDown.

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

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



Функция MouseZoneClass::iAddZone()


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

int MouseZoneClass::iAddZone( char *szZoneName, short shX, short shY, short shWidth, short shHeight, short shClickType)

Первый параметр называется szZoneName и в нем передается имя создаваемой активной зоны. Класс активной зоны использует это имя, чтобы сообщить вам какая из зон была активирована, поэтому оно очень важно. Для титульного экрана используются два имени. Кнопке выхода из игры я присваиваю имя EXIT_BUTTON, а остальной части экрана— имя TITLE_SCREEN.

Следующие два параметра, shX и shY, задают местоположение левого верхнего угла активной зоны. Активные зоны являются прямоугольными, так что данные о местоположении одного из углов необходимы. Все координаты указываются в пространстве рабочей области окна, так что вам не надо беспокоиться об их вычислении. Если вы взглянете на код, то увидите, что зона с именем TITLE_SCREEN начинается в левом верхнем углу экрана с координатами (0, 0).

Следующий параметр называется shWidth. Он задает ширину активной зоны. Ширина окна игры в рассматриваемом примере равна 640 точкам, поэтому я задаю ширину охватывающей весь экран зоны равной 640.

Следующий параметр, shHeight, задает высоту зоны.

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

Таблица 6.10. Типы щелчков мышью в классе MouseZoneClass

Значение Описание
0 Нажатие левой кнопки мыши.
1 Нажатие правой кнопки мыши.
2 Нажатие любой кнопки мыши.
3 Нажатие кнопок мыши не требуется.

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


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

int MouseZoneClass::iAddZone(char *szZoneName, short shX, short shY, short shWidth, short shHeight, short shClickType) { int i;

for(i = 0; i < m_iMaxZones; i++) { // Ищем неиспользуемую зону if(m_HotSpots[i].m_bActive == 0) { m_HotSpots[i].m_shZoneXPos = shX; m_HotSpots[i].m_shZoneYPos = shY; m_HotSpots[i].m_shZoneWidth = shWidth; m_HotSpots[i].m_shZoneHeight = shHeight; m_HotSpots[i].m_shClickType = shClickType; // Активируем горячую точку m_HotSpots[i].m_bActive = 1; // Сохраняем имя strcpy(m_HotSpots[i].m_szZoneName, szZoneName); return(i); } } // Нет свободных зон, возвращаем -1 (ошибка) return(-1); }

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



Функция MouseZoneClass::iRemoveZone()


Данная функция отключает активную зону. Вот ее код:

int MouseZoneClass::iRemoveZone(char *szZoneName) { int i;

for(i = 0; i < m_iMaxZones; i++) { // Проверим, активна ли зона if(m_HotSpots[i].m_bActive == 1) { // Проверка соответствия имени зоны if(!stricmp(m_HotSpots[i].m_szZoneName, szZoneName)) { // Деактивация m_HotSpots[i].m_bActive = 0; return(1); } } } return(0); }

Функция перебирает в цикле все зоны, для которых выделена память и сравнивает имя каждой из них с переданным параметром. Если имя найдено, зона деактивируется путем установки переменной m_bActive в 0. Чтобы сообщить об успещном завершении функция возвращает 1.

Если зона с указанным именем не найдена, функция сообщает об ошибке, возвращая 0.



Функция MouseZoneClass::MouseZoneClass()


Первой из функций является конструктор класса. Его код выглядит следующим образом:

MouseZoneClass::MouseZoneClass(void) { // Количество зон равно нулю m_iMaxZones = 0; }

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


Следом идет деструктор класса, код которого выглядит так:

MouseZoneClass::~MouseZoneClass(void) { // Очистка выделенных зон vFreeZones(); }

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



Функция MouseZoneClass::vFreeZones()


Как видите, функция проверяет переданный ей номер меню и действует соответствующим образом. WinMain() при вызове функции передает ей номер меню 0. В коде, относящемся к меню с номером 0, первым расположен вызов функции MouseZoneClass::vFreeZones(). Эта функция удаляет все существующие на данный момент активные зоны и выполняет инициализацию класса активных зон. Это необходимо, так как к титульному экрану можно перейти из других меню.


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

void MouseZoneClass::vFreeZones(void) { int i;

if(m_iMaxZones) { // Освобождение имен for(i = 0; i < m_iMaxZones; i++) { delete [] m_HotSpots[i].m_szZoneName; } // Освобождение горячих точек delete [] m_HotSpots; m_iMaxZones = 0; } }

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



Функция MouseZoneClass::vInitialize()

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

ВНИМАНИЕ

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

Затем начинается код действительного создания активных зон. Активные зоны, используемые в титульном экране, создаются путем вызова функции MouseZoneClass::iAddZone(). Титульный экран содержит две активных зоны: одну для кнопки выхода из игры и одну для всего остального экрана. Кнопка выхода из игры направляет пользователя к финальному экрану программы, а вторая активная зона переносит игрока к главному меню.


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

void MouseZoneClass::vInitialize(int iMaxZones) { int i; // Очистка существующих зон vFreeZones(); // Сохранение максимального количества зон m_iMaxZones = iMaxZones; // Выделение памяти для указанного количества зон m_HotSpots = new stHotSpot[m_iMaxZones]; // Очистка данных зоны for(i = 0; i < m_iMaxZones; i++) { m_HotSpots[i].m_shZoneXPos = 0; m_HotSpots[i].m_shZoneYPos = 0; m_HotSpots[i].m_shZoneWidth = 0; m_HotSpots[i].m_shZoneHeight = 0; m_HotSpots[i].m_shClickType = 0; m_HotSpots[i].m_bActive = 0; m_HotSpots[i].m_szZoneName = new char[64]; memset(m_HotSpots[i].m_szZoneName, 0x00, 64); } }

Сначала выполняется вызов функции vFreeZones(), которая освобождает любую выделенную ранее память.

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

Теперь выделяется память для хранения массива структур данных горячих точек. Для каждой активной зоны создается одна структура данных.

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



Функция vCheckInput()


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

if(timeGetTime() > dwInputTimer) { // Проверка входных данных vCheckInput(); dwInputTimer = timeGetTime()+50; }



Функция vDrawInterfaceObject()


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

void vDrawInterfaceObject( int iXPos, int iYPos, float fXSize, float fYSize, int iTexture );

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

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

Последний параметр, iTexture, является индексом в глобальном массиве текстур. Он определяет какая исменно текстура будет отображена.

Теперь взгляните на код этого бриллианта из мира функций:

void vDrawInterfaceObject(int iXPos, int iYPos, float fXSize, float fYSize, int iTexture) { D3DXMATRIX matWorld, matRotation; D3DXMATRIX matTranslation, matScale; float fXPos, fYPos;

// Установка начальных значений местоположения, // масштабирования и вращения D3DXMatrixIdentity(&matTranslation); // Масштабирование спрайта D3DXMatrixScaling(&matScale, fXSize, fYSize, 1.0f); D3DXMatrixMultiply(&matTranslation, &matTranslation, &matScale); // Поворот спрайта D3DXMatrixRotationZ(&matRotation, 0.0f); D3DXMatrixMultiply(&matWorld, &matTranslation, &matRotation); // Вычисление местоположения на экране fXPos = (float)(-(g_iWindowWidth / 2) + iXPos); fYPos = (float)(-(g_iWindowHeight / 2) - iYPos + fYSize - g_iYOffset); // Перемещение спрайта matWorld._41 = fXPos; // X matWorld._42 = fYPos; // Y // Установка матрицы g_pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld); g_pd3dDevice->SetTexture(0, g_pTexture[iTexture]); g_pd3dDevice->SetStreamSource(0, g_pVBInterface, 0, sizeof(CUSTOMVERTEX)); g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX); g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); // Разыменовывание текстуры g_pd3dDevice->SetTexture(0, NULL); }


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

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

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

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



Рис. 6.20. Отображение текстуры в трехмерном пространстве на экране

На рис. 6.20, точка с координатами (0,0) соответствует центру экрана. Это коренным образом отличается от традиционной двухмерной визуализации. В традиционной двухмерной среде изображенной на рис 6.20 точке в центре экрана соответствуют координаты (400,300). Так как вы имеете дело с трехмерным миром, вам необходимо компенсировать это различие. Для этого и пригодился код о котором я только что говорил. Он преобразует трехмерную геометрию для отображения в двухмерном пространстве экрана.



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

Теперь матрица содержит данные о масштабировании, вращении и перемещении, необходимые для визуализации. Матрицу следует активировать, для чего необходимо вызвать функцию IDirect3DDevice9::SetTransform(). Я вызываю эту функцию, которая меняет матрицу D3DTS_WORLD на матрицу, созданную ранее для отображения растра. В результате активируется новая матрица.

Затем я активирую требуемую текстуру. Для этой цели применяется вызов функции IDirect3DDevice9::SetTexture(), которой в качестве параметра передается указатель на ту текстуру, которую мы хотим отобразить.

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

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

И последней — но от этого она не становится менее важной — я вызываю функцию IDirect3DDevice9::DrawPrimitive(). Она является самым сердцем визуализации и выполняет всю работу, необходимую для отображения трехмерных объектов. Поскольку для изображения квадрата использовалась полоса треугольников, в первом параметре функции я указываю тип данных D3DPT_TRIANGLESTRIP.

И, наконец, я разыменовываю текстуру, присваивая активной текстуре значение NULL.

Закончив разбирать функцию рисования элементов интерфейса, вернемся назад к функции vRender() и посмотрим, каким образом в ней реализовано рисование интерфейса. Ниже я еще раз привел отвечающий за это фрагмент кода:

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, 6);



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



Рис. 6.21. Расположение текстур на титульном экране

На рис. 6.21 изображен экран обрамленный тонкой линией. Кроме того там изображены шесть текстур, расположенных в виде сетки. Эти шесть текстур полностью заполняют экран. Поскольку ширина и высота каждой текстуры равна 256 точкам, их общий размер превышает размеры экрана ширина которого равна 640 точкам, а высота — 480 точкам. Такие размеры текстур необходимы для того, чтобы они соответствовали требованиям оборудования, которое поддерживает только текстуры с размером, равным степени двойки. Большинство современных видеокарт могут работать только с текстурами размером 2 x 2, 4 x 4, 8 x 8, 16 x 16, 32 x 32, 64 x 64, 128 x 128, 256 x 256 и т.д. точек, поэтому и требуется данное действие. Лучший способ учесть эти требования — создать экран в требуемом разрешении, а затем разбить его на блоки размером 256 x 256 точек. Конечно, в результате увеличится объем использукмой памяти, но это можно проигнорировать.

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

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

Функция vInitInterfaceObjects()


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


Рис. 6.18. Полоса треугольников, используемая для двухмерной визуализации

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



Функция vRender()


Вы готовы к визуализации? Думаю, да. Следуйте за мной к функции vRender(). Вот как выглядит ее код:

void vRender(void) { // Очистка вторичного буфера g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0); // Начало создания сцены g_pd3dDevice->BeginScene();

// Рисование титульного экрана 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, 6);

// Конец создания сцены g_pd3dDevice->EndScene();

// Вывод содержимого вторичного буфера на экран g_pd3dDevice->Present(NULL, NULL, NULL, NULL); }

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



Функция vSetupMouseZones()


Функция vSetupMouseZones() содержит следующий код:

void vSetupMouseZones(int iMenu) { // Титульный экран if(iMenu == 0) { MZones.vFreeZones(); MZones.vInitialize(2); MZones.iAddZone("TITLE_SCREEN", 0, 0, 640, 480, 2); MZones.iAddZone("EXIT_BUTTON", 587, 0, 53, 24, 0); } // Главное меню else if(iMenu == 1) { MZones.vFreeZones(); MZones.vInitialize(5); MZones.iAddZone("EXIT_BUTTON", 587, 0, 53, 24, 0); MZones.iAddZone("MAINMENU_NEWGAME", 192, 64, 256, 64, 0); MZones.iAddZone("MAINMENU_LOADGAME", 192, 128, 256, 64, 0); MZones.iAddZone("MAINMENU_SAVEGAME", 192, 192, 256, 64, 0); MZones.iAddZone("MAINMENU_OPTIONS", 192, 256, 256, 64, 0); } // Экран выхода из игры else if(iMenu == 2) { MZones.vFreeZones(); MZones.vInitialize(1); MZones.iAddZone("TITLE_SCREEN", 0, 0, 640, 480, 2); } // Меню параметров else if(iMenu == 7) { MZones.vFreeZones(); MZones.vInitialize(5); MZones.iAddZone("EXIT_BUTTON", 587, 0, 53, 24, 0); MZones.iAddZone("OPTIONS_AUDIO", 192, 64, 256, 64, 0); MZones.iAddZone("OPTIONS_VIDEO", 192, 128, 256, 64, 0); MZones.iAddZone("OPTIONS_DIFF", 192, 192, 256, 64, 0); MZones.iAddZone("OPTIONS_BACK", 192, 256, 256, 64, 0); } }

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



Функция WinMain()


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

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HWND hWnd; MSG msg; WNDCLASSEX wndclass; RECT rcWindowClient; // Инициализация класса окна wndclass.cbSize = sizeof(wndclass); wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = fnMessageProcessor; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = "Title Demo"; wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION); // Регистрация класса окна if(RegisterClassEx(&wndclass) == NULL) { // Выход из программы при сбое exit(1); } // Создание окна hWnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW, "Title Demo", "D3D_TitleScreen", WS_OVERLAPPEDWINDOW, 0, 0, g_iWindowWidth, g_iWindowHeight, NULL, NULL, hInstance, NULL); // Отображение окна ShowWindow(hWnd, iCmdShow); // Получение размеров клиентской области GetClientRect(hWnd, &rcWindowClient); // Вычисление смещения визуализации на основе размеров клиентской области g_iXOffset = (g_iWindowWidth - (rcWindowClient.right - rcWindowClient.left)); g_iYOffset = (g_iWindowHeight - (rcWindowClient.bottom - rcWindowClient.top)); // Изменение размеров окна, чтобы они соответствовали желаемому разрешению SetWindowPos(hWnd, NULL, 0, 0, g_iWindowWidth + g_iXOffset, // Ширина g_iWindowHeight + g_iYOffset, // Высота NULL); // Очистка структуры сообщения ZeroMemory(&msg, sizeof(msg)); // Инициализация Direct3D if(SUCCEEDED(InitD3D(hWnd))) { // Инициализация виртуального буфера для отображения четырехугольников vInitInterfaceObjects(); // Вход в цикл сообщений while(msg.message != WM_QUIT) { if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } else { // Визуализация сцены vRender(); } } } // Освобождение ресурсов и выход из приложения vCleanup(); UnregisterClass("Title Demo", wndclass.hInstance); return 0; }

Чтобы отделить код, относящийся к Direct3D от «стандартного» кода программы для Windows, взгляните на рис. 6.12.


Рис. 6.12. Структура вызовов других функций в функции WinMain()

На рис. 6.12 показан список функций, которые я использую в WinMain(). Они приведены в порядке их исполнения. Обратите внимание, что большинство вызовов не относятся к Direct3D. Это объясняется тем, что функциия WinMain() содержит в основном код инициализации, а не код выполнения или визуализации.

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


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

// Инициализация Direct3D if(SUCCEEDED(InitD3D(hWnd))) { // Инициализация виртуального буфера для отображения четырехугольников vInitInterfaceObjects(); // Инициализация активных зон vSetupMouseZones(0);

// Начало цикла обработки сообщений while(msg.message != WM_QUIT) { if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } else { // Если все нормально, обработать щелчок мыши if(timeGetTime() > dwInputTimer) {

// Проверка входных данных

vCheckInput();

dwInputTimer = timeGetTime() + 50;

}

// Проверка необходимости выхода из программы if(g_iCurrentScreen == 3) {

break;

}

// Визуализация сцены vRender(); } } }

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


Рис. 6.23. Первый экран программы D3D_MouseZones

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



Глобальные данные активных зон


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

#define STRICT #include <windows.h> #include <commctrl.h> #include <commdlg.h> #include <math.h> #include <tchar.h> #include <stdio.h> #include <D3DX9.h> #include "DXUtil.h" #include "D3DUtil.h" #include "MouseZoneClass.h"

// Глобальные переменные LPDIRECT3D9 g_pD3D = NULL; LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; LPDIRECT3DVERTEXBUFFER9 g_pVBInterface = NULL; // Глобальный массив для хранения графики интерфейса LPDIRECT3DTEXTURE9 g_pTexture[32]; // Смещения видимой области окна int g_iXOffset = 0; int g_iYOffset = 0; // Размеры окна int g_iWindowWidth = 640; int g_iWindowHeight = 480; // Структура данных нашего настраиваемого формата вершин struct CUSTOMVERTEX { D3DXVECTOR3 position; // Местоположение D3DXVECTOR3 vecNorm; // Нормаль FLOAT tu, tv; // Координаты текстуры FLOAT tu2, tv2; // Координаты текстуры }; // Описание структуры настраиваемого формата вершинwh #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_TEX2)

void vDrawInterfaceObject(int iXPos, int iYPos, float fXSize, float fYSize, int iTexture); void vInitInterfaceObjects(void); LRESULT WINAPI fnMessageProcessor(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); HRESULT InitD3D(HWND hWnd); void vRender(void); void vCleanup(void); void vCheckInput(void);

void vSetupMouseZones(int iMenu);

// Глобальный класс активных зон MouseZoneClass MZones;

// Идентификатор текущего меню int g_iCurrentScreen = 0;

// Переменные состояния кнопок мыши bool g_bLeftButton = 0;

bool g_bRightButton = 0;


// Глобальный дескриптор окна игры HWND g_hWnd;

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

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

Следующий фрагмент нового кода создает глобальный объект MouseZoneClass. Он называется MZones и используется для всех активных зон, присутствующих в игре.

Затем в коде расположилась целочисленная переменная с именем g_iCurrentScreen. Она отслеживает в каком меню пользователь находится в данный момент. Это необходимо для того, чтобы программа знала какие команды меню ей обрабатывать.

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

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


Горячие точки, или как я научился любить щелчки мыши


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

Как обнаружить активную зону.

Как осуществлять навигацию по меню.

Как обрабатывать события кнопок мыши.

Как подсвечивать элементы интерфейса над которыми находится указатель мыши.



Как определить подсвечиваемую активную зону


Вот как выглядит код функции vCheckInput():

void vCheckInput(void) { bool bRet; char szZoneHit[64]; POINT Point; RECT rcWindowRect; int iMouseX; int iMouseY;

// Проверка смещения окна GetWindowRect(g_hWnd, &rcWindowRect);

// Обновить местоположение мыши GetCursorPos(&Point);

// Вычисление реальных координат мыши iMouseX = Point.x - g_iXOffset - rcWindowRect.left; iMouseY = Point.y - g_iYOffset - rcWindowRect.top;

// Проверка попадания bRet = MZones.bCheckZones((short)iMouseX, (short)iMouseY, szZoneHit, g_bLeftButton, g_bRightButton); if(bRet) { // ЛОГИКА ТИТУЛЬНОГО ЭКРАНА 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); } } // ЛОГИКА ГЛАВНОГО МЕНЮ else if(g_iCurrentScreen == 1) { // Выключаем подсветку всех зон

g_bMainMenu_NewGame_Highlight = 0;

g_bMainMenu_LoadGame_Highlight = 0;

g_bMainMenu_SaveGame_Highlight = 0;

g_bMainMenu_Options_Highlight = 0;

// Переход к экрану завершения игры if(!stricmp(szZoneHit, "EXIT_BUTTON")) { // Делаем экран завершения текущим g_iCurrentScreen = 2; // Устанавливаем активные зоны vSetupMouseZones(2); } else if(!stricmp(szZoneHit, "MAINMENU_NEWGAME")) { // Добавьте сюда логику начала новой игры } else if(!stricmp(szZoneHit, "MAINMENU_LOADGAME")) { // Добавьте сюда логику для загрузки игры } else if(!stricmp(szZoneHit, "MAINMENU_SAVEGAME")) { // Добавьте сюда логику для записи игры } else if(!stricmp(szZoneHit, "MAINMENU_OPTIONS")) { // Деламем меню параметров текущим g_iCurrentScreen = 7; // Устанавливаем активные зоны vSetupMouseZones(7); } // Проверим находится ли указатель над активной зоной

else if(!stricmp(szZoneHit, "MAINMENU_NEWGAME_H"))


{

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

g_bMainMenu_NewGame_Highlight = 1;

}

else if(!stricmp(szZoneHit, "MAINMENU_LOADGAME_H"))

{

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

g_bMainMenu_LoadGame_Highlight = 1;

}

else if(!stricmp(szZoneHit, "MAINMENU_SAVEGAME_H"))

{

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

g_bMainMenu_SaveGame_Highlight = 1;

}

else if(!stricmp(szZoneHit, "MAINMENU_OPTIONS_H"))

{

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

g_bMainMenu_Options_Highlight = 1;

}

} // ЛОГИКА ЭКРАНА ЗАВЕРШЕНИЯ else if(g_iCurrentScreen == 2) { // Выходим из программы, если пользователь нажал кнопку мыши if(!stricmp(szZoneHit, "EXIT_SCREEN")) { // Флаг, сообщающий WinMain() о завершении программы g_iCurrentScreen = 3; } } // ЛОГИКА МЕНЮ ПАРАМЕТРОВ else if(g_iCurrentScreen == 7) { // Переходим к завершающему экрану if(!stricmp(szZoneHit, "EXIT_BUTTON")) { // Делаем завершающий экран текущим g_iCurrentScreen = 2; // Устанавливаем активные зоны vSetupMouseZones(2); } // Возврат к главному меню else if(!stricmp(szZoneHit, "OPTIONS_BACK")) { // Делаем главное меню текущим g_iCurrentScreen = 1; // Устанавливаем активные зоны vSetupMouseZones(1); } } } }

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

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


Как отобразить подсветку активной зоны


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

else if(g_iCurrentScreen == 1) { // Отображение главного меню 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); // Отображение подсвеченных зон, если они есть

// либо обычного меню без подсветки

if(g_bMainMenu_NewGame_Highlight) {

vDrawInterfaceObject(192, 64, 256.0f, 256.0f, 10);

}

else if(g_bMainMenu_LoadGame_Highlight) {

vDrawInterfaceObject(192, 64, 256.0f, 256.0f, 11);

}

else if(g_bMainMenu_SaveGame_Highlight) {

vDrawInterfaceObject(192, 64, 256.0f, 256.0f, 12);

}

else if(g_bMainMenu_Options_Highlight) {

vDrawInterfaceObject(192, 64, 256.0f, 256.0f, 13);

}

else {

// Меню без подсветки

vDrawInterfaceObject(192, 64, 256.0f, 256.0f, 7);

}

}

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


Рис. 6.29. Изображения меню для реализации подсветки

На рис. 6.29 представлены пять изображений меню. На первом представлено меню в котором не подсвечен ни один пункт. Следующие четыре изображения представляют меню с различными состояниями подсветки. Каждое состояние характеризуется собственным подсвеченным пунктом меню. Пусть вас не смущает термин «подсветка». При желании вы можете полностью изменить изображение пункта меню.

Я отображаю на экране первое изображение с рис. 6.29, если ни одна из подсвечиваемых зон не активна. Я отображаю второе изображение, если подсвечивается пункт меню New Game, третье изображение если подсвечивается пункт меню Load Game и четвертое изображение если подсвечивается пункт меню Save Game. Пятое изображение показывается когда должен быть подсвечен пункт меню Options.

Это совсем не трудно, не так ли?

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



Как создать подсвечиваемую активную зону


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

MZones.iAddZone("MAINMENU_NEWGAME_H", 192, 64, 256, 64, 3); MZones.iAddZone("MAINMENU_LOADGAME_H", 192, 128, 256, 64, 3); MZones.iAddZone("MAINMENU_SAVEGAME_H", 192, 192, 256, 64, 3); MZones.iAddZone("MAINMENU_OPTIONS_H", 192, 256, 256, 64, 3);

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



Ключевые типы данных Direct3D


В первых строках кода объявлены несколько ключевых переменных DirectX, необходимых для работы программы. Вот как выглядит этот фрагмент:

LPDIRECT3D9 g_pD3D = NULL; LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; LPDIRECT3DVERTEXBUFFER9 g_pVBInterface = NULL; LPDIRECT3DTEXTURE9 g_pTexture[32];

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

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

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

И, наконец, в коде объявлен массив текстур с именем g_pTexture[]. В нем хранится 32 элемента типа LPDIRECT3DTEXTURE9. Это всего лишь место хранения изображений, необходимых примеру. Благодаря вспомогательным библиотекам DirectX использовать текстуры очень удобно. Кроме того, они очень важны, так что уделите им достаточно внимания. Может быть вам интересно, почему я создал массив для 32 текстур. Для этого нет никаких причин. В программе используется только семь элементов массива, но всегда лучше предохраняться, чем извиняться!

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

int g_iXOffset = 0; int g_iYOffset = 0; int g_iWindowWidth = 640; int g_iWindowHeight = 480;

Сначала перечислены две целочисленных переменных, g_iXOffset и g_iYOffset, которые применяются для хранения смещения клиентской области окна относительно координат окна. Я объясню их назначение позже, при обсуждении файла main.cpp. Сейчас просто примите их такими, как они есть, так же как смерть и налоги.

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



Настройка параметров отображения


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


Рис. 6.16. Двойная буферизация в действии

На рис. 6.16 изображены два буфера — первичный (FB) и вторичный (BB). Оба буфера содержат графические данные. В первичном буфере находится изображение, которое в данный момент отображается на экране пользователя, а вторичный буфер хранит изображение, которое будет показано следующим. Когда наступает время показа следующего изображения, вторичный бувер меняется местами с первичным, либо содержимое вторичного буфера копируется в первичный буфер. В результате на экране появляется новое изображение.

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

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

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

HRESULT GetAdapterDisplayMode( UINT Adapter, D3DDISPLAYMODE *pMode );


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

Второй параметр, pMode, вызывает больше вопросов, поскольку является указателем на структуру данных D3DDISPLAYMODE. После завершения работы функции эта структура данных будет содержать информацию о текущем видеорежиме. На повестку дня выностися вопрос: «Как выглядит структура данных D3DDISPLAYMODE?» А вот и ответ на него:

typedef struct _D3DDISPLAYMODE { UINT Width; UINT Height; UINT RefreshRate; D3DFORMAT Format; } D3DDISPLAYMODE;

Первый член структуры, Width, хранит ширину экрана в пикселях.

Второй член структуры, Height, хранит высоту экрана.

Третий член структуры, RefreshRate, содержит частоту кадров текущего видеорежима. Если его значение равно 0, значит установлено значение частоты по умолчанию.

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

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

typedef struct _D3DPRESENT_PARAMETERS_ { UINT BackBufferWidth; UINT BackBufferHeight; D3DFORMAT BackBufferFormat; UINT BackBufferCount; D3DMULTISAMPLE_TYPE MultiSampleType; DWORD MultiSampleQuality; D3DSWAPEFFECT SwapEffect; HWND hDeviceWindow; BOOL Windowed; BOOL EnableAutoDepthStencil; D3DFORMAT AutoDepthStencilFormat; DWORD Flags; UINT FullScreen_RefreshRateInHz; UINT PresentationInterval; } D3DPRESENT_PARAMETERS;



Первые два элемента структуры, BackBufferWidth и BackBufferHeight, достаточно просты; они просто хранят размеры буфера.

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

Затем мы задаем тип множественной выборки, хранящийся в переменной MultiSampleType. Эта переменная имеет тип D3DMULTISAMPLE_TYPE. Ух ты — еще одно перечисление! Давайте обратимся к таблице 6.1, чтобы разобраться в элементах этого нового перечисления и их назначении.

Таблица 6.1. Типы множественной выборки

Значение Описание
D3DMULTISAMPLE_NONE Множественная выборка отсутствует.
D3DMULTISAMPLE_NONMASKABLE Разрешает использование значений уровня качества.
D3DMULTISAMPLE_2_SAMPLES Доступно две выборки.
D3DMULTISAMPLE_3_SAMPLES Доступно три выборки.
D3DMULTISAMPLE_4_SAMPLES Доступно четыре выборки.
D3DMULTISAMPLE_5_SAMPLES Доступно пять выборок.
D3DMULTISAMPLE_6_SAMPLES Доступно шесть выборок.
D3DMULTISAMPLE_7_SAMPLES Доступно семь выборок.
D3DMULTISAMPLE_8_SAMPLES Доступно восемь выборок.
D3DMULTISAMPLE_9_SAMPLES Доступно девять выборок.
D3DMULTISAMPLE_10_SAMPLES Доступно десять выборок.
D3DMULTISAMPLE_11_SAMPLES Доступно одиннадцать выборок.
D3DMULTISAMPLE_12_SAMPLES Доступно двенадцать выборок.
D3DMULTISAMPLE_13_SAMPLES Доступно тринадцать выборок.
D3DMULTISAMPLE_14_SAMPLES Доступно четырнадцать выборок.
D3DMULTISAMPLE_15_SAMPLES Доступно пятнадцать выборок.
D3DMULTISAMPLE_16_SAMPLES Доступно шестнадцать выборок.
D3DMULTISAMPLE_FORCE_DWORD Не используется.
Множественная выборка необходима для визуализации с включенным сглаживанием. Не знаете что такое сглаживание? Взгляните на рис. 6.17, чтобы увидеть его в действии.

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

ПРИМЕЧАНИЕ



Запомните, что множественная выборка корректно работает только в том случае, если значение параметра, определяющего режим работы цепочки переключения экранных буферов равно D3DSWAPEFFECT_DISCARD. Вернемся к представленным параметрам. Следующий параметр в списке называется MultiSampleQuality и имеет тип DWORD. Как вы, возможно, догадались, этот параметр устанавливает качество множественной выборки. Его значение может находиться в диапазоне от нуля до максимально возможного уровня, возвращаемого функцией IDirect3D9::CheckDeviceMultiSampleType(), минус один.

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

Таблица 6.2. Режимы переключения экранных буферов

Значение Описание
D3DSWAPEFFECT_DISCARD Разрешает драйверу видеокарты самому выбрать наиболее эффективный метод переключения. Этот метод обычно быстрее, чем другие. Единственная проблема этого метода заключается в том, что он негарантирует сохранность вторичного буфера и его содержимого. Так что при использовании этого метода вы не можете полагаться на неизменность содержимого вторичного буфера. Вы должны использовать этот метод, если значение параметра, определяющего тип множественной выборки, отличается от D3DMULTISAMPLE_NONE.
D3DSWAPEFFECT_FLIP Использует циклическую схему экранных буферов. Буферы перемещаются по кругу в замкнутой кольцевой очереди. Метод позволяет добиться гладкой визуализации, если значение параметра, определяющего интервал переключения не равно D3DPRESENT_INTERVAL_IMMEDIATE.
D3DSWAPEFFECT_COPY Этот метод может использоваться только в том случае, если у вас есть один вторичный буфер. Кроме того, этот метод гарантирует неизменность изображения во вторичном буфере. Недостатком метода является то, что в оконном режиме он может вызвать нарущения целостности графики. Это вызвано тем, что изображение выводится во время обратного хода кадровой развертки монитора. Не используйте этот метод, если ваша программа работает в оконном режиме и вам необходима гладкая графика.
D3DSWAPEFFECT_FORCE_DWORD Не используется
<


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

Следующий член данных, Windowed, указывает будет ли использоваться оконный режим, или программа будет работать в полноэкранном режиме. Установите значение TRUE для оконного режима и FALSE — для полноэкранной визуализации.

Затем расположен флаг EnableAutoDepthStencil. Он сообщает системе визуализации будет ли она управлять буфером глубины или нет. Если вы присвоите данному параметру значение TRUE, Direct3D будет управлять буфером глубины за вашу программу.

Следующий член структуры, названный AutoDepthStencilFormat, устанавливает используемый формат буфера глубины. Он используется только в том случае, если значение параметра EnableAutoDepthStencil равно TRUE. Если вы используете автоматическую установку буфера глубины, убедитесь, что в этом параметре вы указали корректный формат.

Следующий член данных в списке носит неопределенное имя Flags. Вы не слишком любите такие прямолинейные имена как это? Значения для этого параметра перечислены в таблице 6.3.

Таблица 6.3. Флаги режима отображения

Значение Описание
D3DPRESENTFLAG_LOCKABLE_BACKBUFFER Разрешает приложению блокировать вторичный буфер.
D3DPRESENTFLAG_DISCARD_DEPTHSTENCIL Разрешает системе освобождать z-буфер после каждого отображения данных буфера. Это может увеличить быстродействие, если драйвер видеокарты поддерживает данную возможность.
D3DPRESENTFLAG_DEVICECLIP Область визуализации в оконном режиме будет обрезаться. Работает только в Windows 2000 и Windows XP.
D3DPRESENTFLAG_FORCEGDIBLT Для копирования изображений будет использоваться GDI. Работает только в Windows 2000 и Windows XP.
D3DPRESENTFLAG_VIDEO Сообщает драйверу видеокарты, что вторичный буфер содержит видеоданные.
Следующий член данных называется FullScreen_RefreshRateInHz. Его назначение достаточно очевидно: он задает частоту с которой видеокарта будет обновлять изображение на экране монитора. Для визуализации в оконном режиме здесь следует указать значение D3DPRESENT_RATE_DEFAULT; в этом случае будет установлена частота кадров по умолчанию данной видеокарты.



Ах, вот и последний элемент параметров отображения! Он называется PresentationInterval. Эта переменная устанавливает максимальную частоту, с которой будет отображаться вторичный буфер. Оконные приложения требуют, чтобы значение этого параметра было D3DPRESENT_INTERVAL_IMMEDIATE. Для полноэкранной визуализации можно использовать значение D3DPRESENT_INTERVAL_DEFAULT или одно из значений, перечисленных в таблице 6.4.

Таблица 6.4. Интервалы отображения

Значение Описание
D3DPRESENT_INTERVAL_DEFAULT Система сама выбирает частоту смены кадров. В оконном режиме используется частота по умолчанию.
D3DPRESENT_INTERVAL_ONE Перед отображением графики система ждет один период кадровой развертки.
D3DPRESENT_INTERVAL_TWO Перед отображением графики система ждет два периода кадровой развертки.
D3DPRESENT_INTERVAL_THREE Перед отображением графики система ждет три периода кадровой развертки.
D3DPRESENT_INTERVAL_FOUR Перед отображением графики система ждет четыре периода кадровой развертки.
D3DPRESENT_INTERVAL_IMMEDIATE Система не ожидает завершения кадровой развертки перед выводом изображения. Этот метод может вызвать несогласованность графики.

Настройка среды визуализации


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

D3DXMATRIX *D3DXMatrixOrthoLH( D3DXMATRIX *pOut, FLOAT w, FLOAT h, FLOAT zn, FLOAT zf );

Первый параметр, pOut, хранит указатель на объект D3DXMATRIX, используемый для хранения полученного результата.

Второй параметр, w, задает ширину области просмотра.

Третий параметр, h, задает высоту области просмотра.

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

Пятый параметр, zf, задает максимальное значение по оси Z. Объекты, расположенные дальше от камеры не отображаются.

Взгляните на фрагмент моего кода, который выполняет эту операцию:

D3DXMatrixOrthoLH(&matproj, (float)g_iWindowWidth, (float)g_iWindowHeight, 0, 1); g_pd3dDevice->SetTransform(D3DTS_PROJECTION, &matproj);

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

Проекционная матрица является основой и готова к использованию. Последним шагом в настройке проекции для двухмерной визуализации является вызов функции SetTransform() с параметром D3DTS_PROJECTION. Он активирует проекционную матрицу и открывает путь к двухмерной визуализации.

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


Следующей настройкой состояния визуализации является отключение аппаратного освещения для сцены. Этот шаг не является необходимым; я добавил его только чтобы упростить данный пример. Для выключения освещения переменной состояния визуализации с именем D3DRS_LIGHTING присваивается значение FALSE.

Затем я отключаю Z-буфер, присваивая переменной состояния визуализации D3DRS_ZENABLE значение FALSE. Я выключаю z-буферизацию для того, чтобы никакие фрагменты моей двухмерной картинки не были удалены во время визуализации.

Последним шагом настройки состояний визуализации является отключение записи в z-буфер. Для этого переменной состояния визуализации D3DRS_ZWRITEENABLE присваивается значение FALSE. Я делаю это чтобы предотвратить изменение z-буфера операциями двухмерной визуализации. Это очень важно, когда в одной сцене совместно используются двухмерные и трехмерные элементы. Вы же не хотите, чтобы двухмерные элементы интерфейса изменяли буфер глубины, используемый вашими трехмерными объектами.

На этом настройка среды визуализации закончена. Пришло время функции vInitInterfaceObjects().


Обнаружение активных зон


Если вы забыли, что такое активная зона, пожалуйста вернитесь к началу этой главы и еще раз прочитайте раздел «Активные зоны и графика». Первая из программ, которые мы сейчас рассмотрим, называется D3D_MouseZones, и демонстрирует простую навигацию по меню. Она содержит несколько работающих меню, по которым может перемещаться пользователь. Экран программы с главным меню изображен на рис.6.22.


Рис. 6.22. Окно программы D3D_MouseZones

На рис. 6.22 показан экран с главным окном программы. В данном примере работают не все кнопки меню, а только Options и Exit. Программа устанавливает активные зоны и реагирует на щелчки пользователя по кнопкам меню. Теперь загрузите проект, чтобы перейти к изучению кода. Имя проекта — D3D_MouseZones.



Обнаружение сообщений кнопок мыши


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

switch(msg) { case WM_LBUTTONDOWN: g_bLeftButton = 1; break; case WM_LBUTTONUP: g_bLeftButton = 0; break; case WM_RBUTTONDOWN: g_bRightButton = 1; break; case WM_RBUTTONUP: g_bRightButton = 0; break; case WM_DESTROY: PostQuitMessage(0); return 0; default: break; }

Первые четыре инструкции case проверяют системные сообщения мыши. Первое из них, WM_LBUTTONDOWN, позволяет узнать что нажата левая кнопка мыши. Следующее, WM_LBUTTONUP, сообщает вам, что левая кнопка мыши была отпущена. То же самое справедливо и для правой кнопки мыши, только используются сообщения WM_RBUTTONDOWN и WM_RBUTTONUP.

Простейший способ хранить состояние мыши — воспользоваться глобальными переменными. Для хранения состояния двух кнопок мыши я использую переменные g_bRightButton и g_bLeftButton. Что может быть проще?



Определение формата вершины


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

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



Определение требований к интерфейсу


Первый аспект разработки интерфейса относится к выяснению того, что требуется от интерфейса. Вы должны спросить себя «Что должен делать интерфейс?». Это ключевой вопрос, потому что не ответив на него вы не сможете начать разработку. Конечно, вы можете начать программирование, оставив размышления об интерфейсе на потом, но я настоятельно не рекомендую так поступать. Итак, вопрос в том, как вам определить требования к интерфейсу?

Моя первая рекомендация— начните с чистого блокнота. Я использую блокноты в которых листы скреплены проволочной спиралью, поскольку они легко складываются и в сложенном виде их удобно использовать в качестве справочника во время написания программы. Вооружившись новым блокнотом выпишите в виде схемы различные интерфейсы, необходимые вашей игре. Схема для простой игры «Крестики-нолики» может выглядеть так:

Начальный экран

Заставка игры (bmp)

Кнопка пропуска заставки (mzone)

Главное меню

Кнопка начала новой игры (mzone)

Кнопка загрузки игры (mzone)

Кнопка записи игры (mzone)

Кнопка выхода из игры (mzone)

Интерфейс игры

Игровое поле (bmp)

Клетки игрового поля (mzones)

Изображение хода игрока (bmp)

Горячая точка для выхода из игры (mzone)

Загрузка игры

Список записанных игр (bmp)

Поле для ввода имени записаной игры (mzone)

Кнопка загрузки (mzone)

Кнопка возврата к главному меню (mzone)

Запись игры

Список записанных игр (bmp)

Поле для ввода имени записываемой игры (mzone)

Кнопка записи (mzone)

Кнопка возврата к главному меню (mzone)

Кнопка возврата к игре (mzone)

Завершение игры

Графика для завершения игры (bmp)

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



Подсветка пунктов меню


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

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



Прототипы функций


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

void vDrawInterfaceObject(int iXPos, int iYPos, float fXSize, float fYSize, int iTexture); void vInitInterfaceObjects(void); LRESULT WINAPI fnMessageProcessor(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); HRESULT InitD3D(HWND hWnd); void vRender(void); void vCleanup(void);

Я объясню назначение каждой из перечисленных функций в следующих разделах этой главы.



Раскадровка интерфейса


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


Рис. 6.7. Раскадровка интерфейса игры Battle Armor

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

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

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

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



Щелчки мышью и взаимодействие с интерфейсом


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

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

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



Сложность навигации по меню


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

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



Согласованность интерфейса


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

Так как же добиться согласованности интерфейса? Один способ — минимизировать изменения графического оформления при переключении меню. Позже в этой главе вы увидите несколько программ, демонстрирующих навигацию по меню. Добравшись до них, попробуйте обратить внимание на то, как зафиксированы меню, чтобы не допустить значительных изменений графического оформления. Сведя изменения графики к минимуму, вы предоставите игроку графический и психологический якорь, привязывающий его к интерфейсу. Работа с соответствующими этому правилу интерфейсами большинству пользователей кажется более простой. И вашей игре следование этому правилу будет только на пользу.

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

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



Состояния интерфейса


Ради примера, представьте, что игрок выбрал в главном меню пункт «Схватка». Согласно схеме, в результате этого действия игрок переходит к меню с номером 3. Кадр с этим меню изображен на рис.6.4.


Рис. 6.4. Схема кадра меню режима "схватка"

Схема, соответствующая рис. 6.4, выглядит так:

3. Меню схватки

A. Одиночная игра

Графика

Активная зона

(7. Меню однопользовательской схватки)

Звук

(H. Щелчок по кнопке)

B. Многопользовательская игра

Графика

Активная зона

(8. Меню многопользовательской схватки)

Звук

(H. Щелчок по кнопке)

C. Возврат к основному меню

Графика

Активная зона

(2. Основное меню)

Звук

(H. Щелчок по кнопке)

D. Нижняя полоса

Графика

E. Верхняя полоса

Графика

F. Кнопка выхода

Графика

Активная зона

(0. Рабочий стол)

G. Музыкальное сопровождение

Звуковой файл

H. Щелчок по кнопке

Звуковой файл

Как видно из приведенной схемы, в этом интерфейсе нет никаких новых элементов. Так как игрок просто продолжает навигацию по системе меню, никаких специальных элементов не требуется. Для примера предположим, что игрок выбрал многопользовательский режим игры и перешел к интерфейсу с номером 8. Следуйте за мной к описанию интерфейса, изображенного на рис. 6.5.


Рис. 6.5. Схема кадра меню многопользовательской схватки

Схема кадра, изображенная на рис. 6.5 не слишком отличается от рассмотренных ранее интерфейсов. В нем присутствуют несколько кнопок, которые просто отправляют игрока к другим меню. Предположим, игрок выбрал в меню пункт «Игровой сервер». На рис. 6.6 показан интерфейс игрового сервера.


Рис. 6.6. Схема кадра с меню игрового сервера

Интерфейс на рис. 6.6 более сложен, чем те, которые мы изучали до сих пор. Вот схема этого маленького шедевра:

9. Экран сервера многопользовательской схватки

A. Флажок готовности

Графика

(unchecked_box.bmp)

Активная зона

Графика

(checked_box.bmp)

Звук


(O. Щелчок по кнопке)

B. Имя игрока

Текстовое поле

Ширина: 16 символов

Переменная состояния

m_szPlayerName[]

С. Цвета игрока

Графика

Переменная состояния

m_iPlayerColor[]

(color0.bmp – color9.bmp)

Активная зона

Звук

(O. Щелчок по кнопке)

D. Команда игрока

Графика

Переменная состояния

m_iPlayerTeam[]

(team0.bmp – team5.bmp)

Активная зона

Звук

(O. Щелчок по кнопке)

E. IP-адрес игрока

Текстовое поле

Ширина: 16 символов

Переменная состояния

m_szPlayerIP[]

F. Верхняя полоса

Графика

(top_bar.bmp)

G. Окно переговоров

Текстовое поле

Ширина: 24 символа

Переменная состояния

m_szChatBuffer[]

Н. Поле ввода реплик

Текстовое поле ввода

Ширина: 24 символа

Переменная состояния

m_szChatSendBuffer[]

I. Кнопка выхода

Графика

(exitbutton.bmp)

Активная зона

(0. Рабочий стол)

J. Карта игры

Графика

Переменная состояния

m_iGameMapID

(gamemap_0.bmp – gamemap9.bmp)

К. Кнопка выбора карты

Графика

(choosmapbutton.bmp)

Активная зона

(10. Меню выбора карты)

Звук

(O. Щелчок по кнопке)

L. Кнопка начала игры

Графика

(startbutton.bmp)

Активная зона

(11. Игровой экран многопользовательской схватки)

Звук

(O. Щелчок по кнопке)

M. Музыкальное сопровождение

Звуковой файл

N. Нижняя полоса

Графика

(bottom_bar.bmp)

O. Щелчок по кнопке

Звуковой файл

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

Далее идет элемент «Имя игрока». В его своиствах указан новый тип элемента — текстовое поле. Это указывает, что элемент содержит динамический текст, создаваемый из системного шрифта. Атрибуты элемента сообщают, что текстовое поле может содержать не более 16 символов. Это важная отметка, сообщающая художнику, чтобы он оставил на экране достаточно места для вывода переменной с именем игрока. Кроме того, у элемента «Имя игрока» есть новое свойство, называемое «Переменная состояния». Оно указывает, что внешний вид элемента зависит от внутренней переменной. В нашем случае переменная — это содержащий имя игрока массив символов с именем m_szPlayerName[]. Эту информацию полезно ввести в схему для того, чтобы вы могли отслеживать какие переменные требуются для работы вашего интерфейса.

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

Перейдем ниже, к элементу с меткой «H», чтобы познакомиться с новым типом элемента — текстовым полем ввода. Этот тип указывает, что игрок может вводить текст в данное поле. Переменная состояния, указанная в свойствах элемента, будет хранить введенный текст.

Типы остальных элементов мы уже рассмотрели ранее, поэтому здесь мы их пропустим.


Создание буфера вершин


Чтобы создать объект для визуализации необходимо сначала создать буфер вершин, который будет хранить данные, описывающие геометрию объекта. Эту задачу выполняет функция IDirect3DDevice9::CreateVertexBuffer(). Она достаточно прямолинейна, поскольку лишь создает буфер вершин для геометрических операций DirectX. Вот как выглядит прототип этой функции:

HRESULT CreateVertexBuffer( UINT Length, DWORD Usage, DWORD FVF, D3DPOOL Pool, IDirect3DVertexBuffer9 **ppVertexBuffer, HANDLE* pHandle );

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

Второй параметр, Usage, содержит одну или несколько констант типа D3DUSAGE. Доступные значения перечислены в таблице 6.7.

Таблица 6.7. Значения D3DUSAGE

Значение Описание
D3DUSAGE_DYNAMIC Буфер вершин требует динамического использования памяти. Это позволяет драйверу видеокарты управлять буфером для оптимизации скорости визуализации. Если данный флаг отсутствует, буден создан статический буфер. Совместно с этим флагом нельзя использовать флаг параметров пула D3DPOOL_MANAGED
D3DUSAGE_AUTOGENMIPMAP Для буфера автоматически генерируются mip-текстуры. Этот метод требует больше памяти, но благодаря его использованию достигается более высокое качество визуализации
D3DUSAGE_DEPTHSTENCIL Буфер является буфером глубины или буфером трафарета. Совместно с этим флагом можно использовать только флаг параметров пула D3DPOOL_DEFAULT
D3DUSAGE_RENDERTARGET Буфер является целевым буфером визуализации. Совместно с этим флагом должен использоваться флаг параметров пула D3DPOOL_DEFAULT

В рассматриваемой программе я присваиваю параметру значение NULL. В результате будет создан статический буфер вершин.

Третий параметр, FVF, содержит комбинацию флагов D3DFVF. Он сообщает системе, какая информация будет содержаться в данных каждой вершины. Вы можете предпочесть не использовать формат FVF для буфера вершин. В этом случае присвойте данному параметру значение NULL. За дополнительной информацией о флагах D3DFVF обращайтесь к документации DirectX SDK.


Четвертый параметр, Pool, сообщает системе, какой класс памяти следует использовать. Так как данные вершин занимают память, они должны ее где-то получить. Значения D3DPOOL указывают доступные возможности. Все значения перечислены в таблице 6.8.

Таблица 6.8. Значения D3DPOOL

Значение Описание
D3DPOOL_DEFAULT Разрешает системе выделять память в наиболее подходящем пуле данных. Данный метод требует освобождения выделенных ресурсов перед сбросом устройства Direct3D
D3DPOOL_MANAGED Разрешает системе в случае необходимости копировать ресурсы из системной памяти в память устройства. Это позволяет выполнять сброс устройства без предварительного принудительного освобождения управляемой памяти
D3DPOOL_SYSTEMMEM Требует, чтобы система хранила ресурсы вне памяти устройства. Это не самый эффективный способ для тех систем, где есть аппаратные ускорители трехмерной графики. Созданные ресурсы не требуется освобождать перед сбросом устройства
D3DPOOL_SCRATCH Ресурсы данного типа недоступны для устройства Direct3D. Тем не менее их можно создавать, блокировать и копировать
D3DPOOL_FORCE_DWORD Не используется
В программе для настройки пула памяти я использую значение D3DPOOL_DEFAULT. В данной ситуации это простейший способ действий.

Пятый параметр, ppVertexBuffer, должен содержать адрес указателя IDirect3DVertexBuffer9. После завершения функции он будет указывать на созданный буфер вершин. В коде я использую глобальный буфер вершин с именем g_pVBInterface.

Последний параметр, pHandle, в данное время не используется, так что не стоит о нем беспокоиться. Разве это не здорово?


Создание объекта Direct3D

Функция Direct3DCreate9() является самой важной в Direct3D, поскольку без нее ничего не сможет произойти. Это самая первая функция, которую вы вызываете в процессе настройки среды визуализации. Главной задачей функции является создание объекта IDirect3D9. Вот как выглядит ее прототип:

IDirect3D9 *Direct3DCreate9( UINT SDKVersion );

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

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

ПРИМЕЧАНИЕ

Если вы добавите к вашей системе новые видеокарты или мониторы после вызова функции Direct3DCreate9(), объект IDirect3D9 не будет знать об этих устройствах. Чтобы новые устройства были перечислены в списке, необходимо заново создать объект.

Создание устройства трехмерной визуализации


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

HRESULT CreateDevice( UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags, D3DPRESENT_PARAMETERS *pPresentationParameters, IDirect3DDevice9 **ppReturnedDeviceInterface );

Первый параметр, Adapter, содержит порядковый номер используемой видеокарты. Для большинства систем с одним монитором можно использовать значение D3DADAPTER_DEFAULT. Этот параметр работает точно так же как и первый параметр функции GetAdapterDisplayMode(), который я описал ранее.

Второй параметр, DeviceType, является перечислением, задающим тип устройства. Доступные типы перечислены в таблице6.5. В данной программе я использую значение D3DDEVTYPE_HAL. Из-за этого программа может не работать с видеокартами, не поддерживающими аппаратное ускорение. В этом случае для того, чтобы программа запустилась попробуйте изменить значение этого параметра на D3DDEVTYPE_REF.

Таблица 6.5. Типы устройств

Значение Константа Описание
1 D3DDEVTYPE_HAL Визуализация выполняется аппаратурой видеокарты. Этот метод позволяет использовать преимущества доступных методов аппаратного ускорения.
2 D3DDEVTYPE_REF Direct3D всю визуализацию выполняет программно. Это плохой выбор, если видеокарта поддерживает аппаратное ускорение.
3 D3DDEVTYPE_SW Используется программное устройство визуализации, эмулирующее аппаратуру.

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



Структура данных настраиваемого формата вершин (FVF)


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

struct CUSTOMVERTEX { D3DXVECTOR3 position; // Местоположение D3DXVECTOR3 vecNorm; // Нормаль FLOAT tu, tv; // Координаты текстуры FLOAT tu2, tv2; // Координаты текстуры }; #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX2)

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



Структура данных stHotSpot


Первый заслуживающий внимания элемент заголовочного файла класса— структура данных stHotSpot. Вот как выглядит ее код:

struct stHotSpot { short m_shZoneXPos; short m_shZoneYPos; short m_shZoneWidth; short m_shZoneHeight; bool m_bActive; short m_shClickType; char *m_szZoneName; };

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


Рис. 6.27. Взаимосвязь между структурой данных и активной зоной

Из рис. 6.27 видно, что переменные m_shZoneXPos и m_shZoneYPos задают координаты верхнего левого угла зоны. Член данных m_shZoneWidth определяет ширину зоны, а m_shZoneHeight — ее высоту.

Член данных m_bActive предназначен для внутреннего использования классом активных зон и определяет используется ли зона в данный момент. Он используется, когда при создании новой зоны происходит поиск свободного места для ее данных.

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

Переменная m_szZoneName используется для хранения имени активной зоны.



Удобство и простота интерфейса


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

Сколько щелчков мышью должен сделать пользователь для выполнения обычных задач?

Сколько уровней в вашей иерархии меню?

Согласованы ли интерфейсы во всей игре?



Управление щелчками мыши


Внутри этого небольшого фрагмента кода находится вызов функции vCheckInput(). Возможно, вы недоумеваете для чего здесь нужен вызов функции timeGetTime(). Дело в том, что современные компьютеры настолько быстрые, что единственное нажатие на кнопку мыши длится десятки итераций цикла обработки сообщений. В результате одно нажатие на кнопку мыши активирует пункт меню десятки раз. Это приведет к проблемам, поскольку ваш интерфейс окажется слишком чувствительным к сделанным пользователем щелчкам мышью. Чтобы побороть проблему, я установил таймер, который разрешает обработку сообытия мыши не чаще чем раз в 50 милисекунд. Код проверяет текущее время и смотрит прошло ли 50 милисекунд с момента последнего вызова функции vCheckInput(). Если прошло достаточно времени, функция вызывается снова и таймер сбрасывается. Если время еще не истекло, выполнение кода продолжается, но никакой проверки введенных данных не происходит. Величина 50 милисекунд выбрана мной произвольным образом и вы можете изменить ее в соответствии с вашими вкусами. Если вам непонятно, какой эффект она оказывает, установите значение 0 и запустите пример (попробуйте щелкнуть в меню по кнопке Options).

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

void vCheckInput(void) { bool bRet; char szZoneHit[64]; POINT Point; RECT rcWindowRect; int iMouseX; int iMouseY;

// Проверка смещения окна GetWindowRect(g_hWnd, &rcWindowRect); // Обновление позиции указателя мыши GetCursorPos(&Point); // Вычисление реальных координат указателя мыши iMouseX = Point.x - g_iXOffset - rcWindowRect.left; iMouseY = Point.y - g_iYOffset - rcWindowRect.top; // Проверка попадания в активную зону bRet = MZones.bCheckZones( (short)iMouseX, (short)iMouseY, szZoneHit, g_bLeftButton, g_bRightButton); if(bRet) { // ЛОГИКА ДЛЯ ТИТУЛЬНОГО ЭКРАНА 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); } } // ЛОГИКА ГЛАВНОГО МЕНЮ else if(g_iCurrentScreen == 1) { // Переход к экрану завершения игры if(!stricmp(szZoneHit, "EXIT_BUTTON")) { // Делаем завершающий экран текущим g_iCurrentScreen = 2; // Устанавливаем активные зоны vSetupMouseZones(2); } else if(!stricmp(szZoneHit, "MAINMENU_NEWGAME")) { // Добавьте сюда код для начала новой игры } else if(!stricmp(szZoneHit, "MAINMENU_LOADGAME")) { // Добавьте сюда код для загрузки игры } else if(!stricmp(szZoneHit, "MAINMENU_SAVEGAME")) { // Добавьте сюда код для сохранения игры } else if(!stricmp(szZoneHit, "MAINMENU_OPTIONS")) { // Делаем меню параметров текущим g_iCurrentScreen = 7; // Устанавливаем активные зоны vSetupMouseZones(7); } } // ЛОГИКА ЭКРАНА ЗАВЕРШЕНИЯ ИГРЫ else if(g_iCurrentScreen == 2) { // Выходим из программы, если пользователь // нажмет любую кнопку мыши if(!stricmp(szZoneHit, "TITLE_SCREEN")) { // Сообщаем WinMain() о завершении программы g_iCurrentScreen = 3; } } // ЛОГИКА МЕНЮ ПАРАМЕТРОВ else if(g_iCurrentScreen == 7) { // Переход к экрану завершения игры if(!stricmp(szZoneHit, "EXIT_BUTTON")) { // Делаем экран завершения игры текущим g_iCurrentScreen = 2; // Устанавливаем активные зоны vSetupMouseZones(2); } // Возврат к главному меню else if(!stricmp(szZoneHit, "OPTIONS_BACK")) { // Делаем главное меню текущим g_iCurrentScreen = 1; // Устанавливаем активные зоны vSetupMouseZones(1); } } } }



Вычисление местоположения указателя мыши

Следующим этапом работы кода является вычисление текущего местоположения указателя мыши. Это делается с помощью вызова предоставляемой Windows функции GetCursorPos(). Эта функция проверяет текущее местоположение указателя мыши и сохраняет полученный результат в структуре POINT. Структура POINT содержит координаты указателя мыши по осям X и Y.

ПРИМЕЧАНИЕ

Функция GetCursorPos() не является частью DirectX SDK. Это внутренняя функция Windows. Для работы с мышью я предпочитаю применять стандартные функцииI Windows, поскольку они достаточно быстрые и значительно проще в использовании, чем вызовы DirectX.

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



Вычисление смещения клиентской области


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


Рис. 6.13. Окно размером 640 x 480

На рис. 6.13 показано созданное мной окно шириной 640 точек и высотой 480 точек. Обратите внимание, что высота заголовка равна 24 точкам. В результате высота видимой области визуализации будет составлять только 456 точек, что представляет проблему как для художника, так и для программиста. Поскольку все ваши вычисления для визуализации графического интерфейса пользователя базируются на размерах экрана, проблема должна быть решена. Функция GetClientRect() — ваш выход.

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

Новый размер окна по X =
(Желаемый размер по X) + ((Желаемый размер по X) - (Размер клиентской области по X))

Новый размер окна по Y =
(Желаемый размер по Y) + ((Желаемый размер по Y) - (Размер клиентской области по Y))

Для примера, изображенного на рис. 6.13 формулы работают следующим образом:

Новый размер окна по X = 640 + (640 - 640)

Новый размер окна по Y = 480 + (480 - 456)

Ширина окна остается равной 640 точкам, а высота становится равной 504 точкам. Теперь размеры области визуализации в окне достаточны для корректного отображения изображений размером 640 на 480 точек. На рис. 6.14 показано новое окно и его размеры.


Рис. 6.14. Измененное окно размером 640 x 504

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

Перемещайтесь по коду вниз, пока не увидите вызов функции InitD3D(). Найдя его, перейдите к коду, реализующему функцию.



Вычисление смещения клиентской области окна на рабочем столе


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


Рис. 6.24. Различия между клиентской областью и пространством рабочего стола

На рис. 6.24 показано окно игры на рабочем столе системы. Ширина клиентской области окна равна 640 точкам, а высота — 480 точкам. Ширина рабочего стола сотавляет 1024 точки, а его высота — 768 точек. В клиентской области окна есть активная зона, и ее координаты в клиентском пространстве — (340, 10). Очень важно понимать, что класс активных зон хранит координаты в клиентском простанстве, а не в пространстве рабочего стола. Теперь представьте себе, что произойдет, если вы будете искать активную зону с координатами (340, 10), а окно будет передвинуто. В этом вам поможет рис. 6.25.


Рис. 6.25. Перемещение окна на рабочем столе

На рис. 6.25 окно было перемещено. Поэтому его клиентская область сместилась на 10 точек вправо и на 10 точек вниз. Щелчки мышью по активной зоне теперь регистрируются со смещением на 10 точек по обеим осям. Это вызвано тем фактом, что система передает программе данные о положении указателя мыши в пространстве рабочего стола. Проверка активной зоны использует координаты (340, 10) и не беспокоится о смещении клиентской области окна. Это вызывает проблему, поскольку активаня зона на рис. 6.25 в действительности расположена по координатам (350, 20). Для решения этой проблемы мы вычисляем в каком месте рабочего стола расположено окно и вычитаем его координаты из координат указателя мыши в момент щелчка. В результате мы получаем координаты в клиентской области не зависящие от местоположения окна. На рис. 6.25 корректировка значений снова вернет нас в точку (340, 10), поскольку вычисления будут выглядеть следующим образом: (350–10, 20–10).



Выход из программы


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

if(g_iCurrentScreen == 3) { break; }

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



Взаимосвязь шаблонов интерфейса


Хотя «Крестики-нолики»— это интригующая и сложная игра, она не отвечает требованиям, предъявляемым к настоящему примеру; поэтому я начну создавать полностью новую схему интерфейса.


Рис. 6.1. Стартовый экран игры Battle Armor

На рис. 6.1 показан стартовый экран моей игры Battle Armor. Он не слишком сложен, но здесь присутствуют несколько элементов, образующих интерфейс. На рис. 6.2 приведена схема кадра со стартовым экраном.


Рис. 6.2. Схема кадра стартового экрана игры Battle Armor

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

1. Стартовый экран

A. Заставка

Графика

Активная зона

(2. Основное меню)

B. Индикатор загрузки

Текст

C. Нижняя полоса

Графика

D. Верхняя полоса

Графика

E. Кнопка выхода

Графика

Активная зона

(0. Рабочий стол)

F. Музыкальное сопровождение

Звуковой файл

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

Самый первый элемент в схеме помечен номером 1 и представляет собой кадр интерфейса. Он называется «Стартовый экран», поскольку это название точно отражает его назначение. Так как это элемент самого верхнего уровня, он расположен вплотную к левому краю страницы. Остальные кадры интерфейса должны быть перечислены таким же образом.

Далее располагается элемент с меткой «A» и названием «Заставка». Ниже перечислены различные свойства этого элемента. Первое из перечисленых свойств — «Графика». Оно сообщает вам, что данный элемент содержит графическое изображение. В данном случае изображением является картинка с названием игры. Следующее свойство — «Активная зона». Это значит, что данный элемент также является горячей точкой и выполняет какие-то действия, когда пользователь щелкает по нему мышью. Чтобы узнать, какие действия будут выполнены, взгляните на следующую строчку. Там расположена запись «(2. Основное меню)». Она означает, что при щелчке мышью по данному элементу пользователь переходит ко второму кадру интерфейса, который называется «Основное меню». Знаете, что? Вы только что создали взаимосвязь между стартовым экраном и основным меню! Полагаю это было для вас неожиданно, но здесь действительно нет ничего сложного.


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

Следующие два элемента с метками «C» и «D» являются графическими элементами, образующими верхнюю и нижнюю полосы интерфейса. В них нет ничего особенного.

Элемент с меткой «E» более интересен, так как содержит и графику и активную зону. Возможно, вам интересно, где расположен элемент «(0. Рабочий стол)». Это специальная метка, означающая рабочий стол Windows. Другими словами, при щелчке пользователя по данному элементу программа завершает свою работу и игрок возвращается к рабочему столу системы. Итак, вы сделали это — определили еще одну важную взаимосвязь интерфейса.

Последний элемент с меткой «F» относится к музыкальному оформлению. Он сообщает, что во время показа стартового экрана будет воспроизводиться фоновая музыка. Музыка не связана с какими-либо другими элементами, поэтому никаких уточнений не приводится. Если хотите, можете указать имя файла WAV или MP3, который будет воспроизводиться.


Заголовочный файл Main.h


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

Объявляю глобальные переменные Direct3D.

Описываю пользовательскую структуру формата вершин.

Объявляю различные глобальные переменные проекта.

Определяю прототипы функций.



Заголовочный файл Main.h


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

Устанавливаю глобальные переменные Direct3D.

Устанавливаю структуру настраиваемого формата вершин.

Устанавливаю различные глобальные переменные.

Определяю прототипы функций.

Устанавливаю глобальные данные активных зон.

Устанавливаю глобальные данные кнопок мыши.



Заголовочный файл Main.h


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


Рис. 6.28. Главное меню с подсветкой кнопки Options

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

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

// Переменные состояния подсветки bool g_bMainMenu_NewGame_Highlight = 0; bool g_bMainMenu_LoadGame_Highlight = 0; bool g_bMainMenu_SaveGame_Highlight = 0; bool g_bMainMenu_Options_Highlight = 0;

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



Заголовочный файл MouseZoneClass.h


Итак, вы увидели класс активных зон в действии, и теперь настало время разобраться как он работает. Класс активных зон состоит из двух файлов: MouseZoneClass.h и MouseZoneClass.cpp. Откройте заголовочный файл MouseZoneClass.h и следуйте дальше.



Загрузка текстур


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

HRESULT D3DXCreateTextureFromFile( LPDIRECT3DDEVICE9 pDevice, LPCSTR pSrcFile, LPDIRECT3DTEXTURE9 *ppTexture );

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

Второй параметр, pSrcFile, содержит имя загружаемого файла. Функция может загружать файлы различных типов, включая JPEG, TGA, BMP и PCX. В коде программы я передаю в этом параметре имена различных файлов с текстурами. Если у вас есть желание поэкспериментировать с различными типами графических файлов, вы можете изменить приведенные имена.

Третий параметр, ppTexture, является адресом указателя на объект IDirect3DTexture9, который будет хранить загруженную текстуру. Как видите в рассматриваемом примере я передаю в этом параметре глобальный массив текстур с именем g_pTexture.

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



Закрытые члены данных класса MouseZoneClass


Далее в заголовочном файле расположено объявление класса MouseZoneClass. Ниже приведен его код:

class MouseZoneClass { private: int m_iMaxZones; stHotSpot *m_HotSpots;

public: MouseZoneClass(void); ~MouseZoneClass(void); void vInitialize(int iMaxZones); void vFreeZones(void); int iAddZone(char *szZoneName, short shX, short shY, short shWidth, short shHeight, short shClickType); int iRemoveZone(char *szZoneName); bool bCheckZones(short shX, short shY, char *szZoneHit, bool bLeftDown, bool bRightDown); };

Есть только две закрытые переменные класса — m_iMaxZones и m_HotSpots. Переменная m_iMaxZones хранит количество активных зон, для которых выделена память. Это очень важные сведения, поскольку количество используемых зон может изменяться. Переменная m_HotSpots является указателем на массив структур данных stHotSpot, представляющих реально существующие активные зоны.



Заполнение буфера вершин данными


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

Взгляните на следующий фрагмент кода в котором содержатся используемые программой данные вершин:

pVertices[0].position = D3DXVECTOR3(0.0f, 0.0f, 0.0f); pVertices[0].tu = 0.0f; pVertices[0].tv = 1.0f; pVertices[0].tu2 = 0.0f; pVertices[0].tv2 = 1.0f; pVertices[0].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[1].position = D3DXVECTOR3(0.0f, 1.0f, 0.0f); pVertices[1].tu = 0.0f; pVertices[1].tv = 0.0f; pVertices[1].tu2 = 0.0f; pVertices[1].tv2 = 0.0f; pVertices[1].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[2].position = D3DXVECTOR3(1.0f, 0.0f, 0.0f); pVertices[2].tu = 1.0f; pVertices[2].tv = 1.0f; pVertices[2].tu2 = 1.0f; pVertices[2].tv2 = 1.0f; pVertices[2].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f); pVertices[3].position = D3DXVECTOR3(1.0f, 1.0f, 0.0f); pVertices[3].tu = 1.0f; pVertices[3].tv = 0.0f; pVertices[3].tu2 = 1.0f; pVertices[3].tv2 = 0.0f; pVertices[3].vecNorm = D3DXVECTOR3(0.0f,0.0f,1.0f);

Я знаю, что необученному взгляду эти данные почти ничего не говорят. Черт побери, даже для обученного взгляда они похожи на мигрень. Вы еще не забыли рис. 6.18? Его обновленная версия приведена на рис. 6.19.


Рис. 6.19. Геометрия, используемая для двухмерной визуализации в трехмерном пространстве

На рис. 6.19 представлены четыре пронумерованных вершины. Первой вершине присвоен номер 0, а последней — номер 3. Номера на рис. 6.19 соответствуют позициям в массиве вершин из приведенного выше кода. Находящиеся рядом с каждой из вершин координаты показывают вам где в трехмерном пространстве расположена каждая вершина.

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


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

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

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


Звуковое оформление интерфейса


Помните активную зону стартового экрана, щелчок по которой переводит игрока к основному меню? Схема кадра с этим меню показана на рис. 6.3.


Рис. 6.3. Схема кадра с основным меню

На рис. 6.3 показан кадр с основным меню. В нем нет ничего сложного — всего лишь несколько графических изображений и активных зон. Но подождите! Взгляните на изображение динамика рядом с элементом «A». Да ведь это напоминает звук! Перед тем, как подробнее разобраться с этим вопросом, взгляните на схему меню:

2. Основное меню

A. Схватка

Графика

Активная зона

(3. Меню схватки)

Звук

(I. Щелчок по кнопке)

B. Кампания

Графика

Активная зона

(4. Меню кампании)

Звук

(I. Щелчок по кнопке)

C. Загрузка игры

Графика

Активная зона

(5. Меню загрузки игры)

Звук

(I. Щелчок по кнопке)

D. Настройка

Графика

Активная зона

(6. Меню настройки)

Звук

(I. Щелчок по кнопке)

E. Нижняя полоса

Графика

F. Верхняя полоса

Графика

G. Кнопка выхода

Графика

Активная зона

(0. Рабочий стол)

H. Музыкальное сопровождение

Звуковой файл

I. Щелчок по кнопке

Звуковой файл

Ничего себе, какой сложной вещью оказалось так просто выглядящее меню. Первым в списке элементов стоит пункт меню «Схватка». Так как этот элемент является пунктом меню, с ним связаны графическиое изображение и активная зона. Главным отличием этого элемента является добавленный к нему элемент «Звук». Этот новый элемент сообщает вам, что при щелчке по активной зоне, связанной с пунктом меню «Схватка» должен воспроизводиться звук. Элемент, расположенный под пунктом «Звук», сообщает, какой именно звук должен быть воспроизведен. В данном случае будет воспроизведен звук, который перечислен в списке элементов под меткой «I». Элемент с меткой «I» называется «Щелчок по кнопке» и в нем указан тип воспроизводимого звука. Если желаете, вы можете указать здесь имя реального файла WAV или MP3.