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

         

Диалоговое окно создания нового проекта в Visual C++





Диалоговое окно, которое вы видите, используется для указания типа создаваемого проекта. Как видно Visual C++ используется для создания всех типов программ, включая элементы управления ActiveX, COM-объекты, надстройки DevStudio и конечно же приложения Windows. Нас интересует тип приложений, который называется Win32 Application. Этот тип используется при создании программ для Windows. Выделите в списке пункт Win32 Application чтобы выбрать этот тип.

Перед тем, как двинуться дальше, вы должны сообщить Visual C++ имя вашего проекта и где он будет храниться. Эта информация вводится в текстовые поля Project name и Location. Для нашего примера в качестве имени проекта укажите CreateWindow.

СОВЕТ Я храню весь исходный код в папке C:\Source\. Рекомендую вам создать аналогичную структуру каталогов, где будут храниться исходные тексты ваших программ. Лучше хранить весь код в единственном месте, чем разбрасывать его по всему жесткому диску.

После того, как вы введете требуемую информацию, щелкните по кнопке OK. Вам будет предложено диалоговое окно, показанное на Рисунок 2.4.




Диалоговое окно выбора типа Windowsприложения





Диалоговое окно, представленное на Рисунок 2.4, предлагает три варианта:

An empty project — пустой проект; A simple Win32 application — простое приложение Win32; A typical "Hello World!" application — типичное приложение «Hello World».

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

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

Третий вариант, типичное приложение «Hello World!», делает больше, чем два предыдущих. Он создает законченую программу для Windows, которая выводит текст «Hello World!». Я не использую этот вариант слишком часто, поскольку создаваемый код на мой вкус выглядит слишком загроможденным.

Выберите первый переключатель и щелкните по кнопке Finish, чтобы открыть диалоговое окно, изображенное на Рисунок 2.5.




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


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

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

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



DirectShow


Первая вещь на которой следует остановиться — имя проекта. В отличие от первых двух проектов из этой главы, имя данного проекта начинается с префикса DShow. Я сделал это потому, что данная программа использует DirectShow а не DirectMusic. DirectShow представляет собой отдельный интерфейс DirectX предназначенный для работы с потоковой аудиовизуальной информацией в Windows. Он может применяться для воспроизведения различных форматов, в том числе AVI, MPEG, MP3 и даже WAV. Как видите, вы можете воспроизводить не только звук, но и видео, а также комбинировать оба этих способа. Это действительно замечательная возможность, открывающая дорогу к воспроизведению видеофрагментов в начале вашей игры и между уровнями.



Добавление деталей к блокам


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



Добавление исходных файлов в проект


Раскройте меню Project и выберите пункт Project | Add To Project | New. В результате на экран будет выведено диалоговое окно, изображенное на Рисунок 2.7.




Добыча ресурсов




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

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

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



Два подразделения с различными типами защиты




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

СОВЕТ Вы можете использовать данный прием и в других областях, например, создавая различные способы передвижения. Для танков может использоваться способ передвижения с названием «Гусеницы». Для грузовиков способ передвижения может быть назван «Колеса». Различные способы передвижения определяют значения таких атрибутов, как максимальная скорость, время ускорения, радиус поворота и т.д.

Два типа подразделений используют один тип атаки





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



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





На Рисунок 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, чтобы увидеть его в действии.




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


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

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



Двухмерная сетка для размещения блоков





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

int x,y; // отображение сверху вниз for(y = 0; y < 10; y++) { // Отображение слева направо for(x = 0; x < 10; x++) { // Ваша функция для отображения блока vDisplayTile(x, y); } }

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



Empire


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

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



Фабрика в игре Utopia





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

Школы увеличивают благосостояние населения. Побочным эффектом увеличения благосостояния является увеличение производительности фабрик. Образованное население — производительное население.

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

Жилые дома необходимы, чтобы вашему населению было где жить. Один квадрат жилых домов предоставляет кров для 500 человек.

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



Файл MouseZoneClass cpp


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



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

Файл main.cpp не слишком сложен, поскольку он по большей части следует каркасу приложения DirectX. Первая представляющая для нас интерес функция — это конструктор класса. Вот его код:

CD3DFramework::CD3DFramework() { m_strWindowTitle = _T("2D Tile Example"); m_pStatsFont = NULL; m_shWindowWidth = 480; m_shWindowHeight = 480; m_shTileMapWidth = 10; m_shTileMapHeight = 10; }

Как видно из приведенного фрагмента кода, в конструкторе класса задается размер блочной карты. Я установил высоту и ширину карты равной 10 блокам, чтобы при выводе карта занимала все окно целиком. Ширина и высота блока равны 48 пикселам, поэтому размер окна устанавливается равным 480 на 480 точек. Поскольку 10 * 48 = 480, данный размер как раз обеспечивает точное совпадение окна и выводимой карты.

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

HRESULT CD3DFramework::OneTimeSceneInit() { m_pStatsFont = new CD3DFont(_T("Arial"), 8, NULL); if(m_pStatsFont == NULL) return E_FAIL; // Заполнение карты блоками с изображением травы memset(m_iTileMap, 0, (m_shTileMapWidth * m_shTileMapHeight) * sizeof(int)); // заполнение второй половины блоками с изображением песка for(int i = 0; i < 50; i++) { m_iTileMap[i+50] = 3; } // Случайное размещение камней на траве // Инициализация генератора случайных чисел srand(timeGetTime()); for(i = 0; i < 50; i++) { // Размещение камней на траве, если случайное число = 5 if(rand()%10 == 5) m_iTileMap[i] = 1; } // Размещение переходных блоков между травой и песком for(i = 50; i < 60; i++) { m_iTileMap[i] = 2; } return S_OK; }

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

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

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

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

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

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

sprintf(szFileName, "grass00.bmp"); if(FAILED(D3DXCreateTextureFromFile(m_pd3dDevice, szFileName, &m_pTexture[0]))) { return S_OK; } sprintf(szFileName, "grass_rocks.bmp"); if(FAILED(D3DXCreateTextureFromFile(m_pd3dDevice, szFileName, &m_pTexture[1]))) { return S_OK; } sprintf(szFileName, "grass_edging_bottom.bmp"); if(FAILED(D3DXCreateTextureFromFile(m_pd3dDevice, szFileName, &m_pTexture[2]))) { return S_OK; } sprintf(szFileName, "beach.bmp"); if(FAILED(D3DXCreateTextureFromFile(m_pd3dDevice, szFileName, &m_pTexture[3]))) { return S_OK; }

Код инициализации текстур использует вспомогательную функцию DirectX с именем D3DXCreateTextureFromFile(). Это действительно крутая функция, ведь она содержит весь код, необходимый для загрузки изображений таких форматов, как BMP, TGA и JPEG. Чтобы использовать ее необходимо включить в проект библиотеку d3dx9.lib и заголовочный файл d3dx9tex.h. Прототип функции выглядит следующим образом:

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

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

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

Последний параметр, ppTexture, является указателем на текстуру. Если вы вернетесь назад, к моему описанию заголовочного файла, то вспомните, что для хранения указателей на текстуры я использую массив m_pTexture. В этом параметре я также указываю индекс в массиве текстур. Например, для текстуры с номером 1 я указываю в параметре m_pTexture[0], для текстуры с номером 2 использую m_pTexture[1], и так далее.

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

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

HRESULT CD3DFramework::Render() { D3DXMATRIX matTranslation; D3DXMATRIX matRotation; D3DXMATRIX matRotation2; D3DXMATRIX matScale; int iX; int iY; int iCurTile; float fTileX, fTileY; // Очистка порта просмотра m_pd3dDevice->Clear(0L, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(120,120,120), 1.0f, 0L); // Начало создания сцены if(SUCCEEDED(m_pd3dDevice->BeginScene())) { // Вертикаль for(iY = 0; iY < m_shTileMapHeight; iY++) { // Горизонталь for(iX = 0; iX < m_shTileMapWidth; iX++) { // Вычисление номера отображаемого блока iCurTile = m_iTileMap[iX + (iY * m_shTileMapWidth)]; // Вычисление экранных координат fTileX = -240.0f + (iX * 48.0f); fTileY = 192.0f - (iY * 48.0f); // Отображение блока vDrawTile(fTileX, fTileY, 48.0f, 48.0f, iCurTile); } } // Отображение частоты кадров m_pStatsFont->DrawText(2, 0, D3DCOLOR_ARGB(255,255,255,0), m_strFrameStats); // Отображение сведений о видеокарте m_pStatsFont->DrawText(2, 20, D3DCOLOR_ARGB(255,255,255,0), m_strDeviceStats); // Завершение создания сцены m_pd3dDevice->EndScene(); } return S_OK; }

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

Далее расположен вызов функции BeginScene(). Она запускает механизм трехмерной визуализации. Вы должны вызвать эту функцию перед выполнением операций трехмерной графики.

ВНИМАНИЕ Вы должны начинать визуализацию с вызова функции BeginScene() и завершать ее вызовом функции EndScene(). Если вы не сделаете это, ваша программа аварийно завершится.

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

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

Следующие инструкции вычисляют, в каком месте экрана должен размещаться отображаемый блок. Поскольку в программе используется трехмерная графика, для задания смещения блока применяются значения с плавающей точкой. Размеры создаваемого программой окна составляют 480 точек в ширину и в высоту. Учитывая эту особенность, для того, чтобы блоки отображались вплотную к границе окна, они должны смещаться на 240 единиц влево. Аналогичным образом для того чтобы блоки отображались вплотную к верхней границе окна, они должны смещаться на 240.0 – 48.0, или 192.0 единицы вверх от начала координат.

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

vDrawTile( float fXPos, float fYPos, float fXSize, float fYSize, int iTexture)

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

Следующий параметр, fYPos, аналогичен первому, за исключением того, что задает местоположение в трехмерной системе координат вдоль оси Y.

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

Последний параметр является индексом в массиве текстур m_pTexture. Он задает текстуру для визуализации.

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

Смотрите, это было не так уж и плохо, правда? Поток выполнения программы показан на Рисунок 5.40.



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


Загрузите файл main.cpp. Первая вещь, которая представляет для нас интерес — глобальная переменная с именем g_iNumTextures. Она содержит количество загружаемых в память текстур блоков. Я создал ее, чтобы упростить добавление блоков в программе. В настоящее время значение этой переменной равно 9. Если вы будете экспериментировать с программой и добавите свои собственные блоки, убедитесь, что соответствующим образом изменено и значение переменной.

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

m_shWindowWidth = 640; m_shWindowHeight = 320; m_shTileMapWidth = 10; m_shTileMapHeight = 10;

На этот раз я создаю окно, ширина которого равна 640 точкам, а высота — 320 точкам. Я поступаю так по той причине, что визуализация изометрических блоков слегка отличается от визуализации двухмерных квадратных блоков, и для нее требуется большая экранная область.

Следующий блок переменных задает размеры визуализируемой блочной карты. Если вы решите увеличить размер блочной карты, не забудьте добавить элементы к массиву m_iTileMap.

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

// Инициализация генератора случайных чисел srand(timeGetTime()); for(int i = 0; i < 100; i++) { // Заполнение базового слоя блоками с изображением травы песка и брусчатки if(rand()%10 == 3) m_iTileMap[i][0] = 2; else if(rand()%10 == 4) m_iTileMap[i][0] = 3; else m_iTileMap[i][0] = 4; // Заполнение слоя деталей деревьями и колоннами if(rand()%10 == 5) m_iTileMap[i][1] = 6; else if(rand()%10 == 4) m_iTileMap[i][1] = 5; else if(rand()%10 == 3) m_iTileMap[i][1] = 8; }

Сперва вызов функции srand() инициализирует генератор случайных чисел, используя в качестве начального значения текущее системное время. Чтобы получить текущее значение времени, я вызываю функцию timeGetTime(). Она находится в библиотеке winmm.lib и требует, чтобы в программу был включен заголовочный файл mmsystem.h. Инициализация генератора случайных чисел позволяет получать различные результаты при каждом запуске программы.

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

Обратите внимание, что устанавливая блоки основного слоя я обращаюсь к первому слою массива карты, используя ссылки вида m_iTileMap[xxTileToChangexx] [0]. Располагая блоки с деталями, я обращаюсь ко второму слою, и ссылки выглядят так: m_iTileMap[xxTileToChangexx] [1]. Если вы желаете, то можете добавить еще измерения; только не забудьте проверить, что вы заполняете эти слои осмысленными значениями.

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

Прииигооотооовииитьсяяя к вииизуууааалииизааацииииии!!!!! (Мои извинения Майклу Бафферу). Сейчас действительно настало время перейти к отображению блоков, так что не будем задерживаться...

// Вертикаль for(iY = 0; iY < m_shTileMapHeight; iY++) { // Горизонталь for(iX = 0; iX < m_shTileMapWidth; iX++) { //--------------------------------------------- // ВИЗУАЛИЗАЦИЯ БАЗОВОГО СЛОЯ //--------------------------------------------- // Вычисление номера отображаемого блока iCurTile = m_iTileMap[iX + (iY * m_shTileMapWidth)][0]; // Вычисление экранных координат fTileX = -32.0f + (iX*32.0f) - (iY*32.0f); fTileY = 128.0f - ((iY*16.0f) + (iX*16.0f)); // Отображение блока vDrawTile(fTileX, fTileY, 64.0f, 32.0f, iCurTile); //--------------------------------------------- // ВИЗУАЛИЗАЦИЯ СЛОЯ С ДЕТАЛЯМИ //--------------------------------------------- // Вычисление номера отображаемого блока iCurTile = m_iTileMap[iX + (iY * m_shTileMapWidth)][1]; if(iCurTile != 0) { // Вычисление экранных координат fTileX = -32.0f + (iX*32.0f) - (iY*32.0f); fTileY = 128.0f - ((iY*16.0f) + (iX*16.0f)); if(iCurTile == 5) vDrawTile(fTileX, fTileY, 64.0f, 125.0f, iCurTile); else if(iCurTile == 6) vDrawTile(fTileX, fTileY, 67.0f, 109.0f, iCurTile); else if(iCurTile == 8) vDrawTile(fTileX, fTileY, 64.0f, 64.0f, iCurTile); } } }

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

Начальный фрагмент кода визуализации занимается отображением базового слоя. Он выводит на экран блоки, изображающие траву, брусчатку и песок. Возможно вы заметитли переменную iCurTile. Я использую ее, чтобы вычислить, какой блок должен быть отображен. Работает этот код точно также, как и в предыдущей программе, за исключением добавленной ссылки на дополнительное измерение массива карты. В данном случае я ссылаюсь на первое измерение буфера блоков, или в терминах кода — [0].

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

X-Pos = ЭкранноеСмещение + (X * ШиринаБлока) - (Y * (ВысотаБлока / 2))

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

Y-Pos = ЭкранноеСмещение - ((Y * (ШиринаБлока / 2)) + (X * (ШиринаБлока / 2)))

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

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

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



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


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

HRESULT CD3DFramework::OneTimeSceneInit() { int i; m_pStatsFont = new CD3DFont(_T("Arial"), 8, NULL); if(m_pStatsFont == NULL) return E_FAIL; // Выделение памяти для блоков for(i = 0; i < g_iNumTiles; i++) { m_pObject[i] = new CD3DMesh(); } // Заполнение карты блоками с кодом 0 memset(m_iTileMap, 0, (m_shTileMapWidth * m_shTileMapHeight) * sizeof(int)); // Случайное размещение скал на траве // Инициализация генератора случайных чисел srand(timeGetTime()); for(i = 0; i < 100; i++) { if(rand() % 5 == 3) m_iTileMap[i] = 1; else m_iTileMap[i] = 0; } return S_OK; }

Новые действия начинаются в первом цикле for. В нем выделяется память для трехмерных объектов (блоков). Для этой цели используется оператор new. Смотрите, разве это не просто?

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



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


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



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


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



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


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



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


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

// Инициализация Direct Sound bRet = bInitializeSoundSystem(hWnd); if(bRet == 0) { MessageBox(hWnd, "Initialization Failure", "Failed to initialize Direct Sound", MB_ICONEXCLAMATION | MB_OK); // Сбой в программе, выход exit(1); }

Я вызываю функцию bInitializeSoundSystem() сразу после того, как создано окно программы. Функция получает один параметр — дескриптор окна. Если функция возвращает 0, значит ее выполнение закончилось неудачно. В этом случае я вывожу на экран окно с сообщением об ошибке и завершаю работу программы после того, как пользователь щелкнет по кнопке OK. В противном случае все работает как предполагалось и выполнение кода продолжается.



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


Основной код программы располагается в файле main.cpp. Загрузите его сейчас и следуйте дальше. Найдите в коде функцию WinMain() и обратите внимание на следующий фрагмент:

// Воспроизведение музыки bRet = bPlayTitleMusic(); if(bRet == 0) { MessageBox(hWnd, "Initialization Failure", "Failed to initialize DirectShow", MB_ICONEXCLAMATION | MB_OK); // Сбой в программе, выход exit(1); }

В этом блоке кода вызывается функция bPlayTitleMusic(), являющаяся локальной для моей программы. Она отвечает за инициализацию DirectShow и воспроизведение файла MP3 из каталога с звуковыми файлами DirectX SDK. Давайте перейдем к этой функции.



Файлы ресурсов

Если вы умеете программировать, но являетесь новичком в программировании для Windows, вам может быть непонятно назначение папки Resource Files.

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

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

Я не хочу потеряться среди связанных с ресурсами тем, так что давайте отправимся дальше. В вашем проекте пока нет файлов, поэтому в папках ничего не перечислено. Давайте исправим это, добавив в проект файл с исходным кодом на С++.



Фаза идей


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

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

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

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



Фаза определения требований


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

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

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

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

Поддержка от 2 до 16 игроков. Игра может быть сохранена и заново загружена. Необязательный выделенный сервер. Администратор игры может отключать и блокировать игроков.

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

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



Фаза производства


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

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

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

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


#bn {display:block;} #bt {display:block;}


Фаза разработки


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

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

Контроль исходного кода. Управление метками. Отслеживание ошибок. Тестирование отдельных частей.

Фаза технической документации


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

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



Фаза тестирования


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

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

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

Несколько простых этапов процесса тестирования показаны на Рисунок 4.3.



Форт в игре Utopia





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


Яндекс.Маркет


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


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

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

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

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

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

стую строку.

Функция bInitializeSoundSystem()


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



Функция bPlayTitleMusic()


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

bool bPlayTitleMusic(void) { HRESULT hr; // Инициализация COM CoInitialize(NULL); // Создание графа CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&g_pGraph); // Запрос интерфейсов объекта g_pGraph->QueryInterface( IID_IMediaControl, (void **)&g_pMediaControl); g_pGraph->QueryInterface( IID_IMediaEvent, (void **)&g_pEvent); g_pGraph->QueryInterface( IID_IMediaSeeking, (void **)&g_pSeeking); // Загрузка песни (вставьте имя своего файла) hr = g_pGraph->RenderFile( L"c:\\dxsdk\\samples\\media\\track3.mp3", NULL); if(hr != S_OK) { return(0); } // Установка темпа воспроизведения g_pSeeking->SetRate(1); // Воспроизведение музыки g_pMediaControl->Run(); // Установка флага воспроизведения g_bBackgroundMusicActive = 1; return(1); }

Функция CreateWindowEx()


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

HWND CreateWindowEx( DWORD dwExStyle, LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam );

Первый параметр имеет тип DWORD и называется dwExStyle. Он похож на определяющий стиль член структуры WNDCLASSEX, но задает дополнительные стили окна. Список доступных дополнительных стилей приведен в таблице 2.6.

Таблица 2.6. Дополнительные стили окна

Стиль Описание
WS_EX_ACCEPTFILES Окно может получать файлы с использованием механизма перетаскивания.
WS_EX_APPWINDOW Заставляет окно перемешаться на верхний уровень панели задач, когда оно видимо.
WS_EX_CLIENTEDGE Окно обрамлено рамкой, чтобы клиентская область выглядела углубленной.
WS_EX_CONTROLPARENT Позволяет переключаться между дочерними окнами с помощью клавиши TAB.
WS_EX_DLGMODALFRAME Окно будет обрамлено двойной рамкой.
WS_EX_LEFT Окно выровнено по левой границе. Это значение по умолчанию.
WS_EX_LEFTSCROLLBAR Для некоторых языков полосу прокрутки следует располагать слева от текста. В таких случаях и применяется этот стиль.
WS_EX_LTRREADING Отображаемый в окне текст читается слева направо. Это значение по умолчанию.
WS_EX_NOPARENTNOTIFY Подавляет сообщение WM_PARENTNOTIFY отправляемое дочерним окном родительскому при создании или уничтожении. Применяется для дочерних окон.
WS_EX_OVERLAPPEDWINDOW Комбинация флагов WS_EX_CLIENTEDGE и WS_EX_WINDOWEDGE.
WS_EX_PALETTEWINDOW Комбинация флагов WS_EX_WINDOWEDGE, WS_EX_TOOLWINDOW и WS_EX_TOPMOST.
WS_EX_RIGHT Содержимое окна выравнивается по правой границе. Это необходимо для некоторых языков.
WS_EX_RIGHTSCROLLBAR Полоса прокрутки располагается справа от клиентской части окна. Это значение по умолчанию.
WS_EX_RTLREADING В некоторых языках текст читают справа налево. Для таких языков использование данного стиля позволит системе отображать символы в окне справа налево.
WS_EX_STATICEDGE Создает окно, предназначенное для элементов, которые не будут получать входные данные от пользователя.
WS_EX_TOOLWINDOW Создает окно, выглядящее как панель инструментов.
WS_EX_TOPMOST Окно будет оставаться самым верхним на рабочем столе, независимо от того, активно оно или нет.
WS_EX_WINDOWEDGE У окна будет рамка с рельефной гранью.

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

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

Третий параметр — это еще одна завершающаяся нулевым символом строка. Вместо того, чтобы определять имя класса, она задает текст, который будет выведен в заголовке окна. Вы можете назвать свою программу как пожелаете, а я свою назвал «Create Window Example».

Четвертый параметр, dwStyle, позволяет задать различные комбинации стилей для вашего окна. Доступные стили перечислены в таблице 2.7.

Таблица 2.7. Cтили окна

Стиль Описание
WS_BORDER Окно обрамлено тонкой рамкой.
WS_CAPTION У окна есть заголовок.
WS_CHILD Окно является дочерним. Этот стиль нельзя использовать вместе со стилем WS_POPUP.
WS_CHILDWINDOW То же самое, что и WS_CHILD.
WS_CLIPCHILDREN Предотвращает рисование в тех областях окна, которые заняты дочерними окнами.
WS_CLIPSIBLINGS Используется только для дочерних окон. Если дочернее окно, для которого был установлен этот стиль перекрывается другими дочерними окнами, то при обновлении содержимого окна будет перерисовываться только та часть, которая не закрыта другими окнами.
WS_CLIPSIBLINGS Окно заблокировано и не может принимать данные от пользователя.
WS_DLGFRAME Стиль для диалоговых окон.
WS_GROUP Указывает, что данное окно является первым в группе окон. Используется для упорядочивания окон. Первое окно, у которого установлен стиль WS_GROUP начинает группу, после чего все последующие окна будут добавляться в ту же группу, пока снова не встретится окно с установленным стилем WS_GROUP (оно начнет следующую группу).
WS_HSCROLL Окно с горизонтальной полосой прокрутки.
WS_ICONIC После создания окно отображается в свернутом виде.
WS_MAXIMIZE После создания окно отображается развернутым на весь экран.
WS_MAXIMIZEBOX В заголовке окна есть кнопка для развертывания на весь экран.
WS_MINIMIZE То же самое, что и стиль WS_ICONIC.
WS_MINIMIZEBOX В заголовке окна есть кнопка для свертывания окна.
WS_OVERLAPPED Перекрывающееся окно с заголовком и рамкой.
WS_OVERLAPPEDWINDOW Окно с комбинацией стилей WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX и WS_MAXIMIZEBOX.
WS_POPUP Стиль для всплывающих окон.
WS_POPUPWINDOW Всплывающее окно с комбинацией стилей WS_BORDER, WS_POPUP и WS_SYSMENU.
WS_SIZEBOX Окно может изменять размер.
WS_SYSMENU У окна есть системное меню, вызываемое при щелчке правой кнопкой мыши по заголовку. Чтобы этот стиль работал правильно, он должен использоваться совместно со стилем WS_CAPTION.
WS_TABSTOP Окно может стать активным при нажатии на клавишу TAB.
WS_THICKFRAME То же самое, что и стиль WS_SIZEBOX.
WS_TILED То же самое, что и стиль WS_OVERLAPPED.
WS_VISIBLE После создания окно будет видимым.
WS_VSCROLL Окно с вертикальной полосой прокрутки.

В программе CreateWindow в этом параметре я указываю стиль WS_OVERLAPPEDWINDOW. Это придает окну вид, похожий на большинство других приложений Windows.

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

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




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



Функция 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, где изображена вся рассмотренная к данному моменту структура меню.




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

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 байта памяти для хранения имени зоны. Это максимальная длина имени зоны. (Я выбрал это значение абсолютно произвольно; если вам нужны более джлинные имена зон, вы вольны изменить его.) После выделения памяти для имени зоны, я заполняю ее нулевыми значениями.



Функция обработки сообщений


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

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

Мой пример проверяет сообщения WM_CREATE, WM_DESTROY и WM_PAINT. Дополнительная работа производится только для сообщения WM_DESTROY.

Когда приложение получает сообщение WM_DESTROY, оно вызывает функцию PostQuitMessage(), которая завершает работу программы Windows. Это происходит, когда вы завершаете работу, щелкнув по кнопке закрытия окна.

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

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



Функция RegisterClassEx()


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

if(RegisterClassEx(&wndclass) == 0) { // Сбой программы, выход exit(1); }

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

Функция очень проста. Вот ее прототип:

ATOM RegisterClassEx( CONST WNDCLASSEX *lpwcx );

Первый и единственный необходимый для нее параметр является указателем на структуру данных WNDCLASSEX. Здесь все просто, — в нашем примере достаточно только передать функции значение &wndclass.

Не следует слишком волноваться из-за того, что функция возвращает значение типа ATOM. Важно только проверить равно возвращаемое функцией значение NULL или нет. Если возвращаемое функцией RegisterClassEx() значение не равно нулю, ее выполнение завершилось успешно.

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



Функция ShowWindow()


Верите или нет, но факт создания окна не приводит к его отображению. (Эти парни из Microsoft иногда такие странные!) Чтобы окно было действительно выведено на экран вы должны вызвать функцию ShowWindow(). К счастью, она достаточно прямолинейна. Вот ее прототип:

BOOL ShowWindow( HWND hWnd, int nCmdShow );

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

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

Таблица 2.8. Значения для функции ShowWindow()

Значение Описание
SW_HIDE Окно скрыто и затем окно активируется.
SW_MAXIMIZE Окно развернуто на весь экран.
SW_MINIMIZE Окно свернуто и затем окно активируется.
SW_RESTORE Восстанавливает окно из свернутого или развернутого состояния. Это полезно, если требуется вернуть окну его первоначальные размеры и местоположение.
SW_SHOW Активирует окно и отображает его.
SW_SHOWMAXIMIZED Окно активируется и отображается развернутым на весь экран.
SW_SHOWMINIMIZED Окно активируется и отображается свернутым.
SW_SHOWNA Окно отображается в его текущем состоянии. Не оказывает никакого влияния если окно в данный момент активно.
SW_SHOWNORMAL Отображает окно в его нормальном состоянии. Это значение используется, когда функция ShowWindow() вызывается в первый раз.

Хотя есть много значений, которые вы можете указать во втором параметре, простейший способ заключается в передаче значения целочисленной переменной iCmdShow, которая является одним из параметров функции WinMain(). Именно так я и поступаю в коде примера.

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



Функция SoundSystem hrInitSoundSystem()

Первая действительно важная функция — это функция инициализации. Вот ее код:

HRESULT SoundSystem::hrInitSoundSystem(void) { HRESULT hResult; IDirectMusicAudioPath8 *path; // Инициализация COM CoInitialize(NULL); // Создание загрузчика if(FAILED(hResult = CoCreateInstance( CLSID_DirectMusicLoader, NULL, CLSCTX_INPROC, IID_IDirectMusicLoader8, (void**)&m_pLoader))) { return(SOUNDERROR_MUSICLOADER); } // Создание исполнителя if(FAILED(hResult = CoCreateInstance( CLSID_DirectMusicPerformance, NULL, CLSCTX_INPROC, IID_IDirectMusicPerformance8, (void**)&m_pPerformance))) { return(SOUNDERROR_MUSICPERFORMANCE); } // Инициализация аудиосистемы if(FAILED(hResult = m_pPerformance->InitAudio( NULL, NULL, m_hWnd, DMUS_APATH_DYNAMIC_STEREO, 4, DMUS_AUDIOF_ALL, NULL ))) { return(SOUNDERROR_INITAUDIO); } // Получение пути по умолчанию if(FAILED(m_pPerformance->GetDefaultAudioPath(&path))) return(SOUNDERROR_PATH); // Установка уровня громкости if(FAILED(path->SetVolume(0, 0))) return(SOUNDERROR_VOLUME); return(S_OK); }

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

ПРИМЕЧАНИЕ Вы не обязаны вызывать здесь функцию установки уровня громкости, но я предпочитаю при инициализации аудиосистемы задать и уровень громкости по умолчанию. Кроме того, вы можете удалить код инициализации COM, если планируете инициализировать COM где-то в другом месте вашей программы.

Функция SoundSystem hrLoadSound()


Следующая функция в списке загружает звуковые данные. Вот как выглядит ее код:

HRESULT SoundSystem::hrLoadSound(char *szname,GameSound *gs) { WCHAR szWideFileName[512]; // Проверяем инициализирована ли аудиосистема if(!m_pLoader) return(SOUNDERROR_MUSICLOADER); if(!m_pPerformance) return(SOUNDERROR_MUSICPERFORMANCE); // Очищаем звуковые данные, если они существуют if(gs->m_pSound) { gs->m_pSound->Unload(m_pPerformance); gs->m_pSound->Release(); gs->m_pSound = NULL; } // Копируем имя файла DXUtil_ConvertGenericStringToWideCch( szWideFileName, szname, 512); // Загружаем звуковые данные из файла if (FAILED(m_pLoader->LoadObjectFromFile ( CLSID_DirectMusicSegment, IID_IDirectMusicSegment8, szWideFileName, (LPVOID*) &gs->m_pSound ))) { return(SOUNDERROR_LOAD); } // Устанавливаем указатель на исполнителя в объекте звукового фрагмента gs->m_pPerformance = m_pPerformance; // Загружаем данные if (FAILED (gs->m_pSound->Download(m_pPerformance))) { return(SOUNDERROR_DOWNLOAD); } return(S_OK); }

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

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

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

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

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

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



Функция SoundSystem hrPlaySound()


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

HRESULT SoundSystem::hrPlaySound(GameSound *gs) { // Проверяем наличие объекта исполнителя if(!m_pPerformance) return(SOUNDERROR_MUSICPERFORMANCE); // Проверяем наличие звукового сегмента if(!gs->m_pSound) return(SOUNDERROR_NOSEGMENT); // Воспроизводим звуковой сегмент if(FAILED (m_pPerformance->PlaySegmentEx( gs->m_pSound, NULL, NULL, DMUS_SEGF_DEFAULT | DMUS_SEGF_SECONDARY, 0, NULL, NULL, NULL ))) return(SOUNDERROR_PLAYFAIL); return(S_OK); }

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

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



Функция vCheckInput()


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

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

Функция vCheckMusicStatus()


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

void vCheckMusicStatus(void) { long evCode; // Проверка кода события g_pEvent->WaitForCompletion(0, &evCode); // Если музыка закончилась, запустить ее заново if(evCode == EC_COMPLETE) { // Устанавливаем начальную позицию в 0 LONGLONG lStartPos = 0; // Останавливаем музыку g_pMediaControl->Stop(); // Устанавливаем позиции g_pSeeking->SetPositions( &lStartPos, AM_SEEKING_AbsolutePositioning, NULL, AM_SEEKING_NoPositioning); // Запускаем музыку g_pMediaControl->Run(); } }

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



Функция vInitInterfaceObjects()


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



Функция vPlaySound()


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

void vPlaySound(void) { // Воспроизведение звукового сегмента g_pPerformance->PlaySegmentEx( g_pSound, NULL, NULL, DMUS_SEGF_DEFAULT | DMUS_SEGF_SECONDARY, 0, NULL, NULL, NULL ); }

Функция vPlaySound() очень проста. Фактически она состоит из единственного вызова функции IDirectMusicPerformance8::PlaySegmentEx().



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