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

       

Функция WinMain()


Следуя далее мы столкнемся с функцией WinMain(). Как я говорил ранее, она является неотъемлимой частью всех программ Windows, а также является первой функцией, вызываемой во время выполнения программы.

На Рисунок 2.1 под функцией WinMain() располагается блок условия с текстом «Событие для обработки?». Он изображает цикл обработки сообщений, являющийся стандартной частью большинства приложений для Windows. Обычно после того как вы инициализировали программу Windows, чтобы она отображала окно и необходимые кнопки, начинается ожидание событий. В большинстве случаев в коде программы присутствует тот или иной вид цикла, проверяющего наличие событий в очереди. На рисунке это изображено пунктирной линией, ведущей к очереди событий. Цикл обработки событий проверяет очередь, пока не обнаружит наличие в ней какой-либо информации.

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




Первой представляющей интерес функцией является 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.




Функция 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.19 представлены четыре пронумерованных вершины. Первой вершине присвоен номер 0, а последней — номер 3. Номера на Рисунок 6.19 соответствуют позициям в массиве вершин из приведенного выше кода. Находящиеся рядом с каждой из вершин координаты показывают вам где в трехмерном пространстве расположена каждая вершина.

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

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

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

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



Гидропонная фабрика





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



Главное меню с подсветкой кнопки 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, механизм визуализации знает, что для кнопки следует включить подсветку. Весьма просто, не так ли?



Главные вехи


В Empire Earth вы начинаете игру в доисторическом веке. Чтобы выйти из доисторического века вы должны собрать заданное количество пищи. Это объясняет почему важно собирать пищу — она необходима вам для развития цивилизации. Фактически, в игре каждый временной период назван эпохой. Когда ваша цивилизация готова войти в следующую эпоху, вы тратите требуемые ресурсы и ждете некоторое время, пока не произойдут изменения. Самое интересное в эпохах то, что каждый раз когда вы переходите к новой эпохе у вас появляются новые объекты, доступные для строительства. Вы не только можете строить лучшие здания, но и можете тренировать лучшие войска. Это подводит нас к следующему ключевому пункту: главным вехам игры. Как следует из заголовка, такое глобальное событие в игре, как переход к новой эпохе, рассматривается как главная веха. На Рисунок 3.3 показаны эпохи, доступные в Empire Earth.



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


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

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

Граф фильтров


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

Чтение данных из файла. Декодирование потоковых данных. Захват видео. Передача данных системной аппаратуре.

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



граф фильтров читает данные из





Как видно на Рисунок 7.5, граф фильтров читает данные из файла MP3, декодирует их, а затем отправляет аудиоаппаратуре для воспроизведения. Рабочей лошадкой индустрии фильтров в DirectShow является интерфейс IGraphBuilder. В таблице 7.6 перечислены входящие в этот интерфейс функции.

Таблица 7.6. Методы интерфейса IGraphBuilder
Метод Описание
Abort Сообщает графу о необходимости прекратить текущую операцию.
AddSourceFilter Добавляет фильтр источника.
Connect Соединяет два контакта.
Render Добавляет фильтр к выходному контакту.
RenderFile Загружает файл для воспроизведения. Я использую этот метод в своем примере для загрузки MP3-файла.
SetLogFile Устанавливает обработчик для файла журналирования выходной информации.
ShouldOperationContinue Сообщает должна ли продолжаться операция. Это очень странная функция, которую вам никогда не придется вызывать.


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





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



Храм NOD ©2002 Electronic Arts All Rights Reserved





После запуска ядерных ракет храм должен быть перезаряжен. Это сохраняет баланс игры, поскольку игрок NOD не может просто сидеть на месте и запускать ракету за ракетой. Ядерная ракета особенно разрушительна в точке взрыва. Большинство строений в игре не могут противостоять такому нападению и разрушаются. На Рисунок 1.10 показаны опустошения, производимые подобным нападением.



Хранение блоков в двухмерном массиве


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

// Установка размеров карты #define TilesWide 10 #define TilesHigh 10 // Объявление массива с картой int iTileMap[TilesWide][TilesHigh]; // Заполнение всей карты блоком с номером 0 memset(&iTileMap, 0, (TilesWide * TilesHigh) * sizeof(int));

Как видите, в приведенном фрагменте кода объявлен двухмерный массив целых чисел. Поскольку и ширина и высота карты составляют 10 блоков, вся карта содержит 100 блоков. Координаты верхнего левого блока равны 0, 0, а координаты нижнего правого блока — 99, 99. Такая карта изображена на Рисунок 5.31.



Хранение многослойных блоков


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

// Установка размеров карты #define TilesWide 10 #define TilesHigh 10 #define TileLayers 3 // Объявление массива для хранения карты int iTileMap[TilesWide][TilesHigh][TileLayers]; // Заполнение всей карты блоком с номером 0 memset(&iTileMap, 0, (TilesWide * TilesHigh * TileLayers) * sizeof(int));

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

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

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

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



Игровая кампания


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

Причина, по которой я говорю об этих играх, как о заранее подготовленных заключается в том, что все игры кампании созданы для вас разработчиками. В отличие от многопользовательских игр или схваток, в режиме кампании есть установленные опасности и награды. Например в игре Emperor: Battle for Dune, выпущенной Westwood, требуется, чтобы вы доказали свою ценность для клана, выполняя простые задачи, такие как управление в сражении небольшим подразделением боевых единиц. По мере развития кампании вам поручают выполнение все более трудных задач. Кроме того, по мере продвижения игры задачи также становятся более длительными и более сложными.



Игровое поле


На Рисунок 1.1 показано представление игрового поля игры Utopia. Мир разделен на два отдельных острова. На игровом поле существуют несколько видов элементов: блоки, представляющие землю, здания, океан, корабли, рыбу и погоду.




Чем была бы игра без игрового поля? Тем же самым, чем будет приборная панель автомобиля без ветрового стекла! Игровое поле используется чтобы показать игру в действии. На нем отображается территория, здания и подразделения. Прочитав это вы можете подумать «Ха! Я так и знал!». Но прежде чем делать поспешные выводы, подумайте обо всех играх прошлых лет, которые обходились без игрового поля. Текстовые приключенческие игры, MUD и игры через BBS в основном обходились без игрового поля. Я думаю, должен вызывать интерес тот факт, что многие игры прошлого вообще обходились без графического вывода. Помните об этом разрабатывая свои игры. Роскошная графика и спецэффекты не являются необходимыми для того чтобы игра была интересной и захватывающей.

Индикаторы ресурсов


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



Инициализация аудиосистемы


В отличие от загрузчика, исполнитель требует, чтобы после успешного создания интерфейса была выполнена его инициализация. Эта задача осуществляется функцией IDirectMusicPerformance8::InitAudio(), которая инициализирует исполнителя и устанавливает для него аудио-путь. Вот как выглядит прототип функции:

HRESULT InitAudio( IDirectMusic** ppDirectMusic, IDirectSound** ppDirectSound, HWND hWnd, DWORD dwDefaultPathType, DWORD dwPChannelCount, DWORD dwFlags, DMUS_AUDIOPARAMS *pParams );

Первый параметр, ppDirectMusic, позволяет функции возвратить созданный интерфейс DirectMusic. Если значение этого параметра равно NULL, то интерфейс DirectMusic создается и используется внутри объекта исполнителя. Я предпочитаю использовать NULL, поскольку это упрощает код.

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

Третий параметр, hWnd, предназначен для передачи дескриптора того окна для которого создается интерфейс DirectSound. Если значение параметра равно NULL, используется фоновое окно. Я предпочитаю использовать здесь NULL.

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

Таблица 7.4. Типы аудио-пути

Значение Описание
DMUS_APATH_DYNAMIC_3D Трехмерный звук
DMUS_APATH_DYNAMIC_MONO Монофонический звук
DMUS_APATH_DYNAMIC_STEREO Стереофонический звук
DMUS_APATH_SHARED_STEREOPLUSREVERB Стереофонический звук с эхом

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

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

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

Таблица 7.5. Флаги функциональных возможностей исполнителя

Значение Описание
DMUS_AUDIOF_3D Трехмерные буферы
DMUS_AUDIOF_ALL Все возможности
DMUS_AUDIOF_BUFFERS Множественные буферы
DMUS_AUDIOF_DMOS Дополнительные DMO (DirectX Media Object)
DMUS_AUDIOF_EAX Эффекты EAX
DMUS_AUDIOF_ENVIRON Моделирование среды
DMUS_AUDIOF_STREAMING Изменяющиеся формы волн

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

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

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

Вот как выглядит код, описываемый в приведенном выше тексте:

// Инициализация аудиосистемы if(FAILED(hResult = g_pPerformance->InitAudio( NULL, NULL, hWnd, DMUS_APATH_DYNAMIC_STEREO, 4, DMUS_AUDIOF_ALL, NULL ))) { return(0); } // Получение пути по умолчанию if(FAILED(g_pPerformance->GetDefaultAudioPath(&dmAudioPath))) return(0);

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


Первая вещь, которую делает функция, — инициализация COM. Это необходимый этап, поскольку DirectShow использует COM-интерфейсы.

Следующий шаг к небесам DirectShow — создание объекта графа. Я выполняю это с помощью вызова функции CoCreateInstance(). Интерфейс IGraphBuilder использует CLSID CLSID_FilterGraph и идентификатор интерфейса IID_IGraphBuilder. Указатель на интерфейс сохраняется в глобальной переменной с именем g_pGraph.

Теперь, когда у вас есть интерфейс построителя графов, вы можете создать другие интерфейсы, отправив запрос построителю графов. Это делается с помощью функции IGraphBuilder::QueryInterface(). Мы создаем три интерфейса: IID_IMediaControl, IID_IMediaEvent и IID_IMediaSeeking. После создания указатель на каждый из интерфейсов сохраняется в глобальных переменных, о которых я упоминал ранее.

К данному моменту вы инициализировали COM, создали объект графа и необходимые для программы вспомогательные интерфейсы. Это основные действия инициализации, необходимые для воспроизведения файлов MP3. Осталось только загрузить файл с музыкой, установить темп воспроизведения и начать воспроизведение. Перед тем, как идти дальше, взгляните на Рисунок 7.6.



Инициализация класса звуковой системы


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

bool bInitializeSoundSystem(void) { HRESULT hr; // Выделение памяти для звуковых фрагментов g_sndButton = new GameSound; g_sndButtonOver = new GameSound; // Инициализация звуковой системы g_SoundSys.hrInitSoundSystem(); // Загрузка звуковых фрагментов hr = g_SoundSys.hrLoadSound("button.wav", g_sndButton); if(hr == SOUNDERROR_LOAD) { return(0); } hr = g_SoundSys.hrLoadSound( "button_over.wav", g_sndButtonOver); if(hr == SOUNDERROR_LOAD) { return(0); } // Успешное завершение return(1); }

В программе выполняется загрузка двух звуковых файлов: button_over.wav и button.wav. Поскольку у нас два звуковых файла, нам необходимо два объекта GameSound. Они присутствуют в виде переменных g_sndButton и g_sndButtonOver. Каждая из них является объектом GameSound и объявлена в заголовочном файле main.h. Первым действием является выделение памяти для двух объектов звуковых фрагментов. Я делаю это с помощью оператора new.

Объекты звуковых фрагментов не слишком полезны без звуковой системы, поэтому я объявляю в заголовочном файле main.h указатель на объект звуковой системы с именем g_SoundSys. Чтобы этим объектом можно было воспользоваться, его следует инициализировать, для чего я вызываю метод hrInitSoundSystem(). Данный вызов инициализирует DirectSound и подготавливает систему к воспроизведению звука.

Затем я дважды вызываю метод класса звуковой системы hrLoadSound(). В параметрах этих вызовов я передаю два объекта звуковых фрагментов, чтобы они были заполнены данными из указанных файлов WAV. Вы можете спокойно заменить загружаемые здесь WAV-файлы на свои собственные.

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

Итак, мы разобрали этапы успешной инициализации звуковой системы.



Интерфейс


В C&C есть несколько элементов, перенесенных из более ранней игры Westwood, Dune. На Рисунок 1.7 можно заметить общие принципы организации интерфейса.



Интерфейс Command & Conquer ©2002 Electronic Arts All Rights Reserved





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



Интерфейс с видимым фрагментом 16 x 16 блоков





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

int(Ширина экрана в точках % Количество блоков в ширину)

int(Высота экрана в точках % Количество блоков в высоту)

Предположив, что минимальное разрешение экрана равно 800 x 600, мы можем вычислить, что максимально возможный размер блоков на Рисунок 5.6 равен (800 / 16) = 50 точек в ширину и (600 / 16) = 37 точек в высоту. Если вы придерживаетесь метода, в котором размеры блоков равны степеням двойки, то максимальный размер блоков ограничен 32 точками в ширину и 32 точками в высоту.

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



В интерфейсе, представленном на Рисунок





В интерфейсе, представленном на Рисунок 2.2, нет ничего особенного. Основные элементы управления сосредоточены в верхнем меню: File, Edit, View, Insert и т.д. Все, что вы делаете в Visual C++, должно быть связано с проектом. Проект — это верхний уровень иерархии в среде разработки.


Исполнитель DirectMusic


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

Вся работа исполнителя поддерживается единственным интерфейсом с именем IDirectMusicPerformance8. Он является рабочей лошадкой DirectMusic; поэтому в нем присутствуют десятки функций. Перечисление функций приведено в таблице 7.2.

Таблица 7.2. Методы интерфейса IDirectMusicPerformance8

Метод Описание
AddNotificationType Добавляет тип уведомления.
AddPort Назначает исполнителю порт.
AdjustTime Смещает время исполнителя вперед или назад.
AllocPMsg Выделяет память для сообщения.
AssignPChannel Назначает канал исполнителя.
AssignPChannelBlock Назначает блок из 16 каналов.
ClonePMsg Копирует сообщение исполнителя.
CloseDown Закрывает объект исполнителя.
CreateAudioPath Создает аудио-путь (audio path).
CreateStandardAudioPath Создает аудио-путь со стандартными параметрами.
DownloadInstrument Загружает инструмент DLS.
FreePMsg Освобождает занятую сообщением память.
GetBumperLength Возвращает время между помещением сообщения в буфер и началом его обработки.
GetDefaultAudioPath Возвращает аудио-путь по умолчанию.
GetGlobalParam Возвращает глобальные значения исполнителя.
GetGraph Возвращает инструментальный граф (toolgraph).
GetLatencyTime Возвращает время, необходимое исполнителю на обработку звука и вывод его на динамики.
GetNotificationPMsg Возвращает сообщение уведомления.
GetParam Возвращает параметры дорожки.
GetParamEx Возвращает параметры дорожки. Поддерживает саморегулируемые сегменты.
GetPrepareTime Возвращает латентность дорожки.
GetQueueTime Возвращает время, когда сообщения могут быть вытеснены.
GetResolvedTime Преобразует время в предел.
GetSegmentState Возвращает состояние текущего сегмента.
GetTime Возвращает время исполнителя.
InitAudio Инициализирует исполнителя.
Invalidate Вытесняет все сообщения.
IsPlaying Проверяет, воспроизводится ли текущий сегмент.
MIDIToMusic Преобразует значение ноты MIDI в значение DirectMusic.
MusicToMIDI Преобразует значение DirectMusic в значение MIDI.
MusicToReferenceTime Преобразует MUSIC_TIME в REFERENCE_TIME.
PChannelInfo Возвращает информацию о канале.
PlaySegment Воспроизводит сегмент.
PlaySegmentEx Воспроизводит сегмент с дополнительными параметрами.
ReferenceToMusicTime Преобразует REFERENCE_TIME в MUSIC_TIME.
RemoveNotificationType Удаляет тип уведомления.
RemovePort Удаляет порт.
RhythmToTime Преобразует время ритма во время музыки.
SendPMsg Отправляет сообщение.
SetBumperLength Устанавливает интервал между помещением сообщения в буфер и его обработкой.
SetDefaultAudioPath Устанавливает аудио-путь по умолчанию. Установленный путь становится активным.
SetGlobalParam Устанавливает глобальные значения.
SetGraph Устанавливает инструментальный граф.
SetNotificationHandle Устанавливает обработчик события.
SetParam Устанавливает данные дорожки.
SetPrepareTime Устанавливает время между отправкой сообщения и воспроизведением звука.
Stop Останавливает воспроизведение сегмента.
StopEx Останавливает сегмент или аудио-путь.
TimeToRhythm Преобразует время музыки во время ритма.

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



Использование блоков для динамического содержимого


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

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



Использование блоков для экономии памяти


Давайте для примера возьмем карту игры Warcraft III, ширина которой равна 100 блокам, и высота также равна 100 блокам. Подобная сетка карты изображена на Рисунок 5.3.



Использование яркости блока для эффекта тумана войны





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



Использование класса звуковой системы в меню


Вы помните программу с меню игры Battle Armor, которую я описывал в 6 главе? Пришло время добавить к ней звуки и музыку. Откройте находящийся на компакт-диске проект с именем D3D_MenuSounds и следуйте за мной. Вместо того, чтобы вываливать на вас новые курганы кода, я просто проведу обзор внесенных изменений с высоты птичьего полета.

В файл Main.h я добавил директиву включения заголовочного файла класса звуковой системы. Кроме того, я создал глобальный объект класса звуковой системы и несколько объектов звуковых фрагментов. Все это показано на Рисунок 7.13.



Использование свойства проходимости при поиске пути





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



Исследуем блоки расположенные вокруг нового





На Рисунок 5.26 вы видите, что я перебрал все блоки, расположенные по соседству с вновь добавляемым, и пометил те, которые также содержат изображение дорог. В результате были помечены два соседних блока — один на севере и один на востоке.

Соседние блоки помечены, что дальше? Теперь воспользуемся справочной таблицей, чтобы определить, какой блок должен использоваться в данной ситуации. Как создается справочная таблица? В этом нет ничего сложного: двигаясь по часовой стрелке вы назначаете каждому блоку номер, являющийся последовательно увеличивающейся степенью двойки. В этом методе первому блоку назначается номер 1, второму — 2, третьему — 4, четвертому — 8. Звучит знакомо? Надеюсь, что да, учитывая что практически все, что вы делаете на компьютере основано на этом методе! Рисунок 5.27 показывает, какие значения присвоены соседним блокам.



История


Предпосылка для игры Command & Conquer заключается в том, что вы контролируете команду Глобальной Оборонной Инициативы Объединенных Наций, или GDI. GDI воюет со злым Братством NOD, лидера которого зовут Кейн. Вы можете видеть его фотографию на Рисунок 1.6. Главная идея состоит в том, что обе группировки находятся на планете, где сражаются за тибериум. Тибериум — это минерал, который делает возможным существование окружающего мира. Он используется в игре для постройки любого военного подразделения или здания. Без него вы проиграете. Раз тибериум это минерал, его необходимо собирать. Поэтому общая стратегия в игре выглядит так: собрать столько тибериума, сколько возможно, построить армию и уничтожить другого игрока (или игроков).



В конце каждой главы будет


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

Издатели


Если вы думаете, что ваша игра достаточно хороша для издателя, можете попробовать предложить ее некоторым сетевым издательствам. Одна из таких компаний — Garage Games, которая может издать вашу игру за определенный процент с продаж. Дополнительную информацию можно получить на сайте www.garagegames.com.

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



Она позволяет изменить формат окна,





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

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


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





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

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

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



Яркость


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



Экран радара


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

Большинство современных стратегий реального времени используют тот или иной вид радарного экрана. Игры, подобные Age of Empires от Ensemble Studios также используют экран радара для показа ресурсов, людей, территории и других, интересных игроку элементов. Игра Age of Empires идет в средневековом окружении, так что в ней не может быть настоящего радара, однако показывается та же самая информация. Возможно, какой-то фермер создал твердотопливный ракетный двигатель из свиного навоза, чтобы запустить спутник-шпион. Выбирайте объяснение сами.



Элементы сюжета


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



Эпохи в игре Empire Earth





На Рисунок 3.3 видно, что игра поддерживает тринадцать эпох. Каждая из них дает дополнительные преимущества игроку и увеличивает игровые возможности. Хотя вехи делают игру увлекательнее, постарайтесь ограничить их количество. Сведя количество вех к минимуму, вы не будете рисковать запутать игрока. Предполагаю, что игроки — не сборище безмозглых дураков. (По крайней мере, я надеюсь на это — так как сам я тоже игрок!) Но вы все же должны быть бдительными при создании игры, чтобы в нее было просто начать играть.



Этапы инициализации DirectMusic





На Рисунок 7.2 видно, как вы инициализировали COM, создали загрузчик, создали исполнителя, инициализировали аудиосистему, получили аудио-путь по умолчанию, установили уровень громкости, загрузили файл и, наконец, загрузили сегмент WAV. Этот набор шагов будет повторяться каждый раз, когда вы будете использовать в своих программах DirectMusic, так что запомните его (или по крайней мере, заложите эту страницу).



Этапы инициализации DirectShow





На Рисунок 7.6 показана взаимосвязь между интерфейсом управления аудиовизуальным потоком, интерфейсом событий аудиовизуального потока, интерфейсом позиционирования аудиовизуального потока и интерфейсом графа фильтров. Также видна связь между функциями воспроизведения и интерфейсом управления аудиовизуальным потоком и между функцией установки темпа и интерфейсом позиционирования аудиовизуального потока. Позднее вы увидите как в эту большую картину вписывается интерфейс событий аудиовизуального потока.



Как добавить трехмерные деревья


Что может вызвать трудности при добавлении деревьев к игре? Множество самых разных вещей. Во-первых, у деревьев есть ветви и листва, сквозь которую вы можете видеть. Во-вторых, деревья обычно колеблются под дуновениями ветра. В-третьих они должны по крайней мере казаться объемными. Графика Doom и Doom II больше не считается превосходной. Дни использования двухмерных спрайтов вместо трехмерных моделей остались далеко в прошлом. Или нет?

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

Взгляните на небольшую пальмовую рощу, изображенную на Рисунок 5.29.



Как инициализировать COM


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

CoInitialize(NULL);

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



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


Вот как выглядит код функции 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"za)) { // Добавьте сюда логику для записи игры } 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.



Как регулировать громкость


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

HRESULT SetVolume( long lVolume, DWORD dwDuration );

Первый параметр, lVolume, устанавливает желаемый уровень громкости в сотнях децибел. Допустимы значения от –9600 до 0. Значение 0 соответствует максимальной громкости.

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

Вот как выглядит используемый в примере код:

// Установка громкости if(FAILED(dmAudioPath->SetVolume(0,0))) return(0);

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


В функции 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 в конце, чтобы их было легче идентифицировать в коде.



Как создать проект


Лучший способ обучения — практика, так что давайте создадим новый проект, для чего щелкнем по меню File и выберем пункт New. Вам будет предоставлено множество вариантов, изображенных на Рисунок 2.3.




Как создавать блоки?


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



Как воспроизвести файл MIDI


Вы когда-нибудь хотели воспроизводить в вашей игре MIDI-файлы? Если да, то вы попали по правильному адресу. К счастью, воспроизводить MIDI-файлы очень просто. Фактически, если вы читали предыдущий раздел, то уже знаете все, что необходимо! Абсолютно верно — воспроизведение MIDI-файлов осуществляется точно также, как воспроизведение WAV-файлов. Я пошел дальше и создал включенный в сопроводительные файлы проект с именем DMusic_PlayMIDI. Загрузите его, скомпилируйте и выполните. Вы увидите окно, изображенное на Рисунок 7.3.



в вашей игре было реализовано


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

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

В сопроводительные файлы к этой книге включен проект с названием DShow_PlayMP3. На Рисунок 7.4 показано окно, выводимое этой программой.


Как воспроизвести файл WAV


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

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



Как вычислить местоположение в массиве


Получение местоположение блока в массиве начинается с координаты X. Рассмотрим пример нахождения в массиве блока с координатами 5,5. На Рисунок 5.10 от блока в верхнем левом углу мы перемещаемся на пять блоков вправо. В результате мы окажемся в позиции, отмеченной на рисунке буквой A.

Теперь к текущей позиции вы должны прибавить координату Y блока, умноженную на ширину карты. Ширина карты равна десяти блокам, поэтому мы прибавляем 10 * 5 (координата Y) к текущей позиции. Следуйте по стрелке, размещенной справа от позиции, помеченной буквой A, и вы увидите, что она заканчивается в искомой позиции массива, отмеченной буквой B.

Итак, следуя Рисунок 5.10, мы получаем следующую формулу:

X (A) + (Y * Ширина карты) = Позиция в массиве (B)

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