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

         

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

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






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




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




Главная часть кода расположена в файле программы 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. Загрузите его сейчас и следуйте дальше. Найдите в коде функцию 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. Давайте перейдем к этой функции.



Содержание раздела