Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.10 Добавляем рендеринг
Программируем 3D-шутер от первого лица (FPS) (Win32, C++, DirectX9)

1.10 Добавляем рендеринг


Мда, собственно, для чего всё и затевалось. И хотя здесь мы ничего рендерить пока не будем (а будем всё готовить и настраивать), глава обязательна для прочтения. Так как именно здесь простой фреймворк превращается в игровой движок.
На тему программирования под DirectX написана не одна сотня книг (многие из которых на русском языке). Настоятельно рекомендуется прочитать хотя бы пару из них. Не факт, что всё усвоишь, но основные моменты всё равно запомнишь.

Рендеринг средствами Direct3D

Direct3D - один из основных компонентов DirectX. До 8 версии за рендеринг(external link) отвечали целых два компонента:

  • Direct3D отвечал за рендеринг трёхмерной графики;
  • DirectDraw занимался всей 2D-графикой.

Сейчас оба этих компонента объединены в один - DirectGraphics. Несмотря на это, многие до сих пор называют его Direct3D. В данном курсе мы тоже будем называть его Direct3D. Кроме того, многие интерфейсы всё ещё поддерживают прежнее название, поэтому нам будет намного проще общаться друг с другом. Полное описание Direct3D может растянуться не на одну книгу. Поэтому мы кратко рассмотрим несколько общих тем, которые нам понадобятся. За более подробной информацией смело обращайся к документации DirectX SDK (обычно вся на нагл. языке).
Direct3D - это интерфейс программирования приложений (Application Programming Interface - API), который используется в основном для вывода графики на экран. Обычно это происходит путём рендеринга 3D-геометрии с использованием видеокарты компьютера (в терминологии игрокодинга - display adapter - адаптер дисплея). Что делает Direct3D по-настоящему полезным здесь, так это используемая им технология Слой (уровень) аппаратной абстракции(external link) (от англ. "Hardware Abstraction Layer" - "HAL"; проще говоря, унифицированный уровень обращения к железу). Direct3D-драйвер реализует HAL интерфейс, который взаимодействует непосредственно с оборудованием и позволяет приложениям использовать возможности 3D-ускорителя с макcимальным быстродействием. HAL:

  • имеет низкоуровневый доступ к 3D-чипу и реализует 3D-функции на аппаратном или программно-аппаратном уровне;
  • представляет собой специфичный для данного оборудования (в нашем случае это видеокарта) интерфейс, предоставляемый производителем видеокарты.
Рис.1 Взаимосвязь между приложением, Direct3D и драйвером дисплея
Рис.1 Взаимосвязь между приложением, Direct3D и драйвером дисплея
Рис.2 Различия между 2-мя типами Картезианской системы координат
Рис.2 Различия между 2-мя типами Картезианской системы координат
Рис.4 Нормали вершин используются для расчёта уровня освещённости граней
Рис.4 Нормали вершин используются для расчёта уровня освещённости граней
Рис.5 Пример затенения по Гуро
Рис.5 Пример затенения по Гуро

Каждый HAL отличается друг от друга, так как разработан специально для одной конкретной модели видеокарты (далее - адаптер дисплея) и предоставляет доступ толко к тому функционалу, который она поддерживает. Direct3D, в свою очередь, "общается" напрямую с HAL и посредством приложений, которые используют эту связку. В результате получаем приложения, которые в полной мере используют возможности видеокарты, выдавая очень быстрый и качественный 3D-рендеринг в реальном времени, при этом не сильно напрягая процессор ПК (см. Рис. 1).

Исходя из названия, Direct3D производит рендеринг 3D-объектов. Все эти объекты имеют свою позицию в 3D-пространстве. Для определения местоположения любого 3D-объекта используют набор координат - x, y, z. Такая система координат называется Картезианской и подразделяется на 2 типа:

  • направление оси z определяется по правилу правой руки (праворучная, right-handed);
  • направление оси z определяется по правилу левой руки (леворучная, left-handed).

Direct3D для представления объектов в 3D-пространстве использует леворучную Картезианскую систему координат.
В обоих видах данной коорд. системы координаты x и y вычисляются точно также, как это делается в "плоской" двухмерной системе координат (положительные координаты по оси x откладываются в правую сторону от нулевой координаты, по оси y - вверх). Когда эта двухмерная система переходит в 3D, добавляется третье измерение, которое измеряется по оси z. В леворучной координатной системе ось z направлена в сторону, противоположную наблюдателю, а в праворучной - направлена на него (См. Рис.2).
Для рендеринга объектов в этом 3D-пространстве Direct3D использует примитивы. В Главе 1.5 мы рассматривали различную 3D-геометрию, включая вершины, рёбра и грани. Примитивы представляют собой всего лишь трёхмерные геометрические формы, состоящие из точек в пространстве, называемые вершинами. Вершины соединены друг с другом рёбрами и образуют грани. Простейшим примитивом является вершина, размещённая в 3D-пространстве. В нашем Проекте наиболее часто используемым примитивом будет треугольник (он же полигон, он же грань). Грань (в Direct3D) состоит из 3-х вершин, которые соединены друг с другом и образуют поверхность треугольной формы. Сложные меши (от англ. polygon mesh - "полигональная сетка(external link)") создаются путём объединения нескольких граней. В Direct3D абсолютно все трехмерные объекты, начиная от куба и заканчивая моделями автомобиля и человека, состоят из простых треугольных граней. Когда к этим граням применяют текстуру, Direct3D может рендерить их как единый объект.
Если помнишь, в Главе 1.5 мы отмечали, что каждая грань имеет свою так называемую "нормаль". Нормаль грани:

  • представляет собой вектор, проведённый перпендикулярно плоскости грани в направлении, противоположном её фронтальной стороне;
  • широко применяются в Direct3D для определения, видна ли данная грань или нет, а также для расчёта освещённости объектов;
  • указывает, какая из сторон грани является фронтальной (т.е. наружной и, соответственно, видимой).

Если нормаль грани направлена от вьюера (он же вьюпорт - виртуальная камера, которая "показывает" 3D-мир) (т.е. в сторону, противоположную ему), то в этом случае Direct3D определяет такую грань как невидимую и потому просто не рендерит её. Эта простейшая технология оптимизации выводимой 3D-графики называется отсечением обратной стороны (backface culling(external link)) (см. Рис.3).

Рис.3 Нормали граней используются в Direct3D для отсечения невидимых граней
Рис.3 Нормали граней используются в Direct3D для отсечения невидимых граней


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

Свои нормали есть даже у вершин. Их так и называют нормали вершин. Они используются при расчёте уровня освещённости граней. В самом простом случае Direct3D вычисляет количество света, которое падает на поверхность (состоящую из граней), взяв за основу угол между вектором направления источника света и нормалью вершины (см. Рис. 4).

Direct3D также поддерживает применение различных видов затенения (shading) граней. На Рис.4 представлены грани, которые освещены с применением т.н. "плоского" затенения (flat shading). Это означает, что каждая грань затенена одним цветом и в одной заранее рассчитанной степенью интенсивности.
Другой интересный метод затенения - тонирование по Гуро (Gouraud shading(external link)), где сначала вычисляются цвет и интенсивность закрашивания каждой вершины, а затем эти значения интерполируются через поверхность грани, что значительно улучшает качество выводимой картинки (см. Рис. 5). Данный метод тонирования мы будем использовать по умолчанию во всех последующих Проектах данного курса.

Другим интересным свойством Direct3D является возможность текстурирования примитивов. Здесь на передний план вновь выходят нормали граней, так как Direct3D применяет текстуры только к фронтальным (=видимым) поверхностям граней. Для корректного размещения текстуры на поверхности грани (и даже на поверхности нескольких граней) Direct3D использует координаты текстуры(external link) (texture coordinates). Каждая вершина текстурируемой грани должна иметь координаты текстуры. Текстуры делятся на так называемые текселы(external link) (от англ. texels, texture element, элемент текстуры, пиксель текстуры), которые представляют собой индивидуальные значения цвета текстуры. Текселы образуют сетку текстуры, где каждой колонке соответствует определённая координата по оси u, а строке - координата по оси v. Таким образом каждый тексел текстуры имеет т.н. UV-координаты. В 3DS Max есть даже такой модификатор UVW map, применяемый для наложения текстур на 3D-объекты. Координаты текстуры указывают, от какой вершины грани она будет брать начало. Другими словами, ты указываешь UV-координаты, а Direct3D наносит текстуру на вершину, начиная с этих UV-координат (см. Рис. 6).

Рис.6 Наложение текстуры на грань с использованием UV текстурных координат
Рис.6 Наложение текстуры на грань с использованием UV текстурных координат


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

Устройство Direct3D (Direct3D device)

Виртуальное устройство (device) - это "сердце" Direct3D. Если не считать самого объекта Direct3D, Direct3D-устройство является одним из первых объектов, которые ты создашь, и одно из последних, которое ты уничтожишь. Каждый объект Direct3D-устройства создаётся специально для запуска под управлением физического адаптера дисплея (видеокарты) того компьютера, на котором он был создан (проинициализирован). Причиной этого является то, что все адаптеры дисплея, их драйверы и HAL, созданные для них, отличаются своими возможностями. Помимо этого, при создании объекта Direct3D-устройства пользователь может изменять множество параметров. Это означает, что создаваемое в данный момент Direct3D-устройство крайне специфично именно для данного экземпляра игрового приложения. Когда (например, при завершении работы приложения) Direct3D-устройство уничтожается и затем создаётся заново, оно может заметно отличаться от предыдущего (например в графических настройках, которые выберет пользователь).
Устройство является родителем всех графических объектов сцены. У вашего компьютера может быть любое количество Direct3D-устройств (например в случае присутствия в ПК дискретной видеокарты и встроенного в мат. плату видеочипа).

Direct3D поддерживает создание двух видов устройств:

  • Устройство HAL (HAL device с аппаратной обработкой графики средствами видеоадаптера; наиболее предпочтительный вариант).

Устройство HAL всегда крайне специфично (совпадает по поддерживаемым технологиям) с текущим физическим адаптером дисплея, который, в свою очередь, поддерживает только те возможности, которые предусмотрены его производителем. Поэтому если на коробке от видеокарты написано, что поддерживается Pixel Shader 2.0, при запуске приложения с третьей версией шейдеров оно выдаст ошибку, и кина не будет. Преимуществом устройства HAL является аппаратная обработка всей графики, что большинство видеоадаптеров проделывают очень быстро, выдают безумно красивую 3D-прорисовку и при этом не нагружают процессор.

  • Устройство программной эмуляции (Reference device).

Поддерживает абсолютно все функции, заложенные Direct3D, но их обработка целиком "ложится на плечи" центрального процессора, который зачастую производит её намного медленнее. Как результат - очень низкий показатель FPS (кол-во кадров всекунду) и относительно скудная 3D-картинка. Применяется лишь в том случае, когда адаптер дисплея не поддерживает необходимые функции рендеринга (например, видеокарта без встроенного 3D-ускорителя).

Закрыть
noteПримечание

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


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

  • глубина цвета;
  • разрешение экрана;
  • частота обновления экрана.

Помимо этого, Direct3D-устройство поддерживает множество других продвинутых настроек для всего на свете, начиная от стенсил-буферов(external link) и заканчивая антиалиасингом (antialiacing(external link), сглаживание). Но, так как разные HAL имеют разные возможности (в зависимости от физического адаптера дисплея), наборы изменяемых параметров у них также будут отличаться. Валидные (=корректные) комбинации этих параметров называют режимами дисплея (display modes). По сути объект устройства Direct3D состоит из объекта адаптера дисплея, на котором оно запускается, и режима дисплея (проще говоря, настроек), выбранным для него.
В связи с тем, что на базе одного адаптера дисплея можно создать бесчисленное множество комбинаций Direct3D-устройств, часто бывает необходимо произвести т.н. энумерацию (от англ. enumeration - перечисление) адаптера дисплея. Это означает, что мы запросим у адаптера дисплея сведения обо всех поддерживаемых комбинациях параметров, с которыми будет создано устройство Direct3D.

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

Рис. 7 Сейчас Проект нашего движка выглядит так
Рис. 7 Сейчас Проект нашего движка выглядит так

Сейчас в Проекте Engine нашего движка всего 11 файлов: Engine.h, Engine.cpp, LinkedList.h, ResourceManagement.h, Geometry.h, State.h, State.cpp, Input.h, Input.cpp, Scripting.h и Scripting.cpp, которые мы создали в предыдущих главах (см. Рис. 7). Чуть ниже есть второй Проект Test. Его пока не трогаем!

Создаём DeviceEnumeration.h (Проект Engine)

В заголовочном файле DeviceEnumeration.h внутри класса DeviceEnumeration представлены объявления функций для настройки режима адаптера дисплея и выбора других его настроек.
ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "DeviceEnumeration.h".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле DeviceEnumeration.h набираем следующий код:

DeviceEnumeration.h (Проект Engine)
//-----------------------------------------------------------------------------
// File: DeviceEnumeration.h
// Содержит класс перечисления устройств Direct3D.
//
// Original sourcecode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------

#ifndef DEVICE_ENUMERATION_H
#define DEVICE_ENUMERATION_H

//-----------------------------------------------------------------------------
// Структура DisplayMode (режим дисплея)
//-----------------------------------------------------------------------------
struct DisplayMode
{
	D3DDISPLAYMODE mode; // Режим дисплея Direct3D.
	char bpp[6]; // Глубина цветопередачи (т.е. уровень цветности),
				//выраженная в виде строки символов для дисплея.
};

//-----------------------------------------------------------------------------
// Класс перечисления устройств (Device Enumeration)
//-----------------------------------------------------------------------------
class DeviceEnumeration
{
public:
	INT_PTR Enumerate( IDirect3D9 *d3d );

	INT_PTR SettingsDialogProc( HWND dialog, UINT uiMsg, WPARAM wParam, LPARAM lParam );

	D3DDISPLAYMODE *GetSelectedDisplayMode();
	bool IsWindowed();
	bool IsVSynced();

private:
	void ComboBoxAdd( HWND dialog, int id, void *data, char *desc );
	void ComboBoxSelect( HWND dialog, int id, int index );
	void ComboBoxSelect( HWND dialog, int id, void *data );
	void *ComboBoxSelected( HWND dialog, int id );
	bool ComboBoxSomethingSelected( HWND dialog, int id );
	int ComboBoxCount( HWND dialog, int id );
	bool ComboBoxContainsText( HWND dialog, int id, char *text );

private:
	Script *m_settingsScript; // Скрипт, в котором хранятся настройки дисплея.

	D3DADAPTER_IDENTIFIER9 m_adapter; // Идентификатор адаптера Direct3D.
	LinkedList< DisplayMode > *m_displayModes; // Связный список (Linked list) перечисляемых режимов дисплея.
	D3DDISPLAYMODE m_selectedDisplayMode; // Режим дисплея, выбранный пользователем.
	bool m_windowed; // Флаг, указывающий, должно ли приложение запускаться в оконном режиме.
	bool m_vsync; // Флаг, указывающий, включен ли v-sync (вертикальная синхронизация) или нет.
};

//-----------------------------------------------------------------------------
// Внешние указатели
//-----------------------------------------------------------------------------
extern DeviceEnumeration *g_deviceEnumeration;

#endif

  • Сохрани Решение (Файл->Сохранить все).

Структура DisplayMode (режим дисплея)

В самом начале DeviceEnumeration.h представлена маленькая структура DisplayMode, которая хранит подробные настройки одного режима дисплея, который используется для создания объекта устройства Direct3D.

Фрагмент DeviceEnumeration.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Структура DisplayMode (режим дисплея)
//-----------------------------------------------------------------------------
struct DisplayMode
{
	D3DDISPLAYMODE mode; // Режим дисплея Direct3D.
	char bpp[6]; // Глубина цветопередачи (т.е. уровень цветности),
				//выраженная в виде строки символов для дисплея.
};
...

Если не заметил, эта структура является т.н. "обёрткой" (от англ. wrapper) вокруг стандартной Direct3D-структуры D3DDISPLAYMODE. Причиной такого подхода является тот факт, что Direct3D не работает с индивидуально созданными цветовыми моделями. Вместо этого он использует заранее предопределённые т.н. форматы дисплея (display formats), которые определяют, каким образом сохраняются данные о цвете каждого пиксела, отображаемого на экране. В Таблице 1 представлены наиболее распространённые форматы дисплея, используемые в Direct3D.

Таблица 1. Наиболее распространённые форматы дисплея, используемые в Direct3D

Формат дисплея Описание
D3DFMT_X1R5G5B5 16-битный цвет. По 5 бит на каждый компонент палитры RGB (от англ. Red, Green, Blue - Красный, Зелёный, Синий) + 1 бит зарезервирован.
D3DFMT_A1R5G5B5 16-битный цвет. По 5 бит на каждый компонент палитры RGB (от англ. Red, Green, Blue - Красный, Зелёный, Синий) + 1 бит является альфа-каналом, указывающий на то, какой цвет будет являться прозрачным (например, для создания трафаретных спрайтов).
D3DFMT_R5G6B5 16-битный цвет. По 5 бит на красный и синий компоненты палитры RGB (от англ. Red, Green, Blue - Красный, Зелёный, Синий) + 6 бит на зелёный компонент.
D3DFMT_X8R8G8B8 32-битный цвет. По 8 бит на каждый компонент палитры RGB (от англ. Red, Green, Blue - Красный, Зелёный, Синий) + 8 бит зарезервированы.
D3DFMT_A8R8G8B8 32-битный цвет. По 8 бит на каждый компонент палитры RGB (от англ. Red, Green, Blue - Красный, Зелёный, Синий) + 8 бит на альфа-канал, указывающий на то, какой цвет (или цвета) будет являться прозрачным (например, для создания трафаретных спрайтов).
D3DFMT_A2R10G10B10 32-битный цвет. По 10 бит на каждый компонент палитры RGB (от англ. Red, Green, Blue - Красный, Зелёный, Синий) + 2 бита на альфа-канал.

Данный список форматов дисплея далеко не полный. Но их будет вполне достаточно для нашей дальнейшей работы. Полный список всегда можно найти в документации к DirecX SDK.
С точки зрения программиста с готовыми форматами дисплея работать намного удобнее (это одно из преимуществ DirectX). В то же время, они могут слегка запутать неподготовленного пользователя, увидевшего подобный список в окне настроек адаптера дисплея. Когда в последний раз ты заходил в опции игры и перед тобой появлялся список форматов дисплея? Так вот, чтобы сделать жизнь игрока чуточку проще, мы дополнительно введём небольшую текстовую строку с (упрощённым) описанием каждого предлагаемого формата дисплея. При таком подходе формат D3DFMT_A1R5G5B5 будет представлен как "16 bpp" (16 bits per pixel - 16 бит на 1 пиксел), а D3DFMT_A8R8G8B8 - как "32 bpp".

Прототип (описание) структуры D3DDISPLAYMODE выглядит так:

Прототип (описание) структуры D3DDISPLAYMODE
typedef struct _D3DDISPLAYMODE {
UINT Width;		// Ширина экрана, в пикселях.
UINT Height;		// Высота экрана, в пикселях.
UINT RefreshRate;	// Частота экрана. Если это значение равно 0, то принимается адаптер с частотой по умолчанию.
D3DFORMAT Format;	// Структура D3DFORMAT. Описывает формат поверхности отображения на экран (см. Таблицу 1).
} D3DDISPLAYMODE;

Ширина и высота экрана должны совпадать с реальными значениями ширины и высоты окна приложения. Частота экрана (RefreshRate) указывает на то, как часто содержимое экрана (или окна приложения) должно обновляться (перерисовываться) и обычно совпадает с частотой обновления экрана монитора (60-85Гц).
Теперь, когда у нас есть структура, где можно хранить форматы дисплея (после того, как они будут перечислены при опросе адартера дисплея), рассмотрим класс DeviceEnumeration.

Класс DeviceEnumeration

Сейчас ты кое-что уже знаешь об объекте устройства Direct3D. Он используется Direct3D для управления всем рендерингом. Существуют 2 типа (объектов) устройств, которые создаёт Direct3D: устройство HAL и устройство программной эмуляции (reference device). Нас интересует только устройство HAL, так как устройство программной эмуляции работает слишком медленно и не подходит для создания интерактивных 3D-приложений (хотя такие игры, как Unreal и Half-Life, увидевшие свет в 1998 г., очень неплохо смотрелись даже в программной обработке). Устройство HAL целиком "заточено" под аппаратные возможности видеокарты (её принято называть "адаптером дисплея") твоего компьютера. Другими словами, HAL (данной видеокарты) специально разработан для поддержки возможностей определённого адаптера дисплея. По этой причине нам необходимо создать объект устройства, который будет обращаться только к этому HAL. Это означает, что объект устройства Direct3D, созданный на базе одного адаптера дисплея, будет отличаться от аналогичного объекта устройства Direct3D, созданного на базе другого адаптера дисплея. Вдобавок ко всему, физический адаптер дисплея поддерживает далеко не все комбинации режимов дисплея. Например одни адаптеры не поддерживают разрешение 1920х1080, а другие могут не поддерживать частоту обновления больше 100 Гц. Раз объект устройства может быть создан со столь многими, сильно отличающимися друг от друга параметрами, довольно сложно создать одну общую комбинацию режимов устройства и ожидать, что она будет запускаться на большинстве компьютеров с различными видеокартами "на борту". Особенно это касается сетевых игр, оснащённых мультиплеером.

Как видишь, у нас есть причины предложить конечному пользователю способ управления настройками видеоадаптера, для успешного запуска игры и корректного рендеринга на каждой, отдельно взятой, конфигурации компьютера. В самом простом случае, предложим игроку выбрать разрешение экрана. Но мы не будем на этом останавливаться. Ты можешь создать настолько проработанное окно настроек игры, насколько пожелаешь, позволив игроку регулировать каждый аспект создания устройства Direct3D (многие игроки очень ценят "гибкое" меню настроек графики в играх). Хорошим примером таких диалоговых окон являются примеры DirectX SDK (в нашем случае расположены по адресу c:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Samples\C++\Direct3D\Bin; твой путь к каталогу с примерами может отличаться от приведённого). Запусти первый попавшийся пример и нажми F2, вызвав диалоговое окно с установками Direct3D. Там очень много всевозможных опций. В нашем движке мы разработаем похожее диалоговое окно, но немного упрощённое, с меньшим количеством настроек. В нём пользователь сможет выбрать наиболее часто применяемые параметры. Например, выставить/убрать флаг запуска игры в оконном режиме (= без развёртывания на полный экран). В случае выбора пользователем полноэкранного режима (флаг "оконный режим" убран), мы предложим выбрать несколько других параметров, как например вертикальная синхронизация (v-sync), глубину цветопередачи (color depth), разрешение экрана (resolution) и частоту обновления (refresh rate).

Для управления процессом перечисления устройств Direct3D мы применим специальный класс DeviceEnumeration, реализация которого выглядит очень громоздко и путано. Но как и в остальных случаях, когда начнёшь "играть" с ним, изменяя различные параметры и видя произведённый эффект, ты сразу увидишь, как всё это работает.

Закрыть
noteКстати,..

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

Прежде чем ты бросишься что-либо изменять, рассмотрим объявление класса DeviceEnumeration (DeviceEnumeration.h).

Фрагмент DeviceEnumeration.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Класс перечисления устройств (Device Enumeration)
//-----------------------------------------------------------------------------
class DeviceEnumeration
{
public:
	INT_PTR Enumerate( IDirect3D9 *d3d );

	INT_PTR SettingsDialogProc( HWND dialog, UINT uiMsg, WPARAM wParam, LPARAM lParam );

	D3DDISPLAYMODE *GetSelectedDisplayMode();
	bool IsWindowed();
	bool IsVSynced();

private:
	void ComboBoxAdd( HWND dialog, int id, void *data, char *desc );
	void ComboBoxSelect( HWND dialog, int id, int index );
	void ComboBoxSelect( HWND dialog, int id, void *data );
	void *ComboBoxSelected( HWND dialog, int id );
	bool ComboBoxSomethingSelected( HWND dialog, int id );
	int ComboBoxCount( HWND dialog, int id );
	bool ComboBoxContainsText( HWND dialog, int id, char *text );

private:
	Script *m_settingsScript; // Скрипт, в котором хранятся настройки дисплея.

	D3DADAPTER_IDENTIFIER9 m_adapter; // Идентификатор адаптера Direct3D.
	LinkedList< DisplayMode > *m_displayModes; // Связный список (Linked list) перечисляемых режимов дисплея.
	D3DDISPLAYMODE m_selectedDisplayMode; // Режим дисплея, выбранный пользователем.
	bool m_windowed; // Флаг, указывающий, должно ли приложение запускаться в оконном режиме.
	bool m_vsync; // Флаг, указывающий, включен ли v-sync (вертикальная синхронизация) или нет.
};
...

Выглядит громоздко. Поэтому будем разбирать его по частям.
Во-первых, у данного класса отсутствует конструктор и деструктор. А всё потому, что данный класс является вспомогательным (т.н. "utility class"), который не требует выделения памяти для своей работы. Это означает, что данный класс вызывается единожды при загрузке движка, во время создания объекта устройства Direct3D. Как только оно будет создано, данный класс более не будет применяться во время работы приложения (вплоть до следующего запуска игры). Весь функционал данного класса может быть экспонирован в виде нескольких public-функций, вызывая которые движок выделяет и освобождает память при необходимости.
Особый интерес для нас представляют функции Enumerate и SettingsDialogProc, которые мы разберём чуть ниже.

После выполнения перечисления адаптеров дисплея пользователь выберет в диалоговом окне желаемые настройки. Первое, что необходмо определить - это в каком режиме будет запускаться приложение: в оконном или полноэкранном. В зависимости от выбора данной опции дальнейшие настройки будут существенно различаться. Такая проверка осуществляется путём вызова функции IsWindowed, которая возвращает TRUE в случае, когда пользователь выбрал оконный режим запуска приложения. Запуск приложения в окне сильно упрощает дальнейшую работу по конфигурации игры, т.к. в этом случае мы располагаем практически всей необходимой информацией (откуда она берётся - мы расскажем чуть позднее). Функция IsWindowed возвращает FALSE в случае, когда пользователь выбрал полноэкранный режим запуска игры (fullscreen).

Как только мы определили, какой режим выбрал пользователь (оконный или полноэкранный), в дело вступает функция GetSelectedDisplayMode, которая возвращает указатель на структуру D3DDISPLAYMODE, содержащую подробные настройки конфигурации режима экрана, выбранные пользователем. В ней содержатся сведения о выбранном разрешении (т.е. ширина и высота экрана), частоте обновления и формате дисплея (=глубина цветопередачи), с которым будет создаваться объект устройства Direct3D.
Дополнительно мы можем использовать функцию IsVSynced, которая возвращает TRUE, когда пользователь выбрал опцию v-sync (вертикальная синхронизация). В случае выбора данной опции окно приложения синхронизируется с частотой обновления монитора. Это означает, что видеокарта будет ожидать, когда монитор окажется готов к обновлению картинки, не переходя к рендерингу следующего кадра. На практике это даёт более плавную анимацию и снижает визуальные артефакты (например, размазывание при быстром движении объектов на экране). В то же время, это означает, что обновление рендеринга будет привязано к частоте обновления монитора, которая в большинстве современных ЖК-мониторов не превышает 60 Гц.

Закрыть
noteПримечание

В Direct3D функция v-sync представлена в виде т.н. интервала показа (presentation interval). По умолчанию интервал показа (вывода на экран) всегда равен единице. На деле это означает, что видеокарта обновляет (сменяет) кадры при каждом обновлении экрана монитора. Это как раз и есть тот случай, когда v-sync включен, так как частота смены кадров приложения равна частоте обновления монитора.

Класс DeviceEnumeration содержит в себе несколько закрытых (private) функций для управления т.н. комбо-боксами (combo box). Комбо-бокс представляет собой стандартный элемент управления Windows, который выглядит как текстовая строка (textbox) с небольшой стрелкой с правой стороны. При нажатии на стрелку раскрывается список доступных опций. Мы будем использовать комбо-боксы в диалоговом окне для того, чтобы дать возможность пользователю выбирать из списка валидных видеорежимов, составленного на основе опроса его адаптера дисплея. Эти функции используются для различных манипуляций с комбо-боксами. Их реализация - тема для отдельной книги по Windows-программированию. Но (по возможности) мы их тоже обсудим. Тебе также никогда не придётся обращаться к этим функциям напрямую (собственно, поэтому они и объявлены как private; т.е. они не могут вызываться из других внешних классов).

В последней секции private класса DeviceEnumeration представлено несколько переменных членов, в которых мы попытаемся разобраться.
m_settingsScript является указателем на скрипт. Вот здесь-то мы и применили нашу новую систему скриптов. Раз уж мы не создавали тестового приложения для проверки работы системы скриптов, здесь ты всё-таки увидишь её в действии, когда закончим реализацию системы перечисления адаптеров дисплея. Переменная m_settingsScript будет хранить настройки экрана, выбранные пользователем. После того, как перечисление адаптеров дисплея будет закончено, движок запишет текущие настройки в скрипт на жёстком диске, для того, чтобы их можно было загрузить из него при запуске игры в следующий раз. Движок считает эти данные из скрипта и использует их для своей дальнейшей настройки. Это предотвратит ситуацию, когда пользователь должен снова и снова вводить желаемые настройки всякий раз при запуске игры.
m_adapter представляет собой инициализированную структуру D3DADAPTER_IDENTIFIER9. Она содержит всю информацию о дисплее адаптера, установленного в компьютере игрока. Вот её прототип (определение):

Прототип (определение) структуры D3DADAPTER_IDENTIFIER9
typedef struct _D3DADAPTER_IDENTIFIER9 {
// Название и описание драйвера (только для представления в меню выбора).
char Driver[MAX_DEVICE_IDENTIFIER_STRING];
char Description[MAX_DEVICE_IDENTIFIER_STRING];

char DeviceName[32];  // Имя адаптера дисплея.

// Идентифицирует версию компонентов 32-битного драйвера.
// Используй это для приложений Win32.
LARGE_INTEGER DriverVersion;

// Идентифицирует версию компонентов 16-битного драйвера.
// Не применяется в приложениях Win32.
DWORD DriverVersionLowPart;
DWORD DriverVersionHighPart;

DWORD VendorId;  // Идентифицирует производителя чипсета адаптера дисплея
DWORD DeviceId;  // Идентифицирует тип чипсета адаптера дисплея
DWORD SubSysId;  // Идентифицирует печатную плату адаптера дисплея
DWORD Revision;  // Идентифицирует версию ревизии чипсета адаптера дисплея
GUID DeviceIdentifier;  // Уникальный идентификатор адаптера дисплея
DWORD WHQLLevel;  // Версия WHQL-валидации адаптера дисплея
} D3DADAPTER_IDENTIFIER9;

Эта структура довольно большая и нам не требуется получать столько информации из неё. Мы будем её использовать лишь для отображения названия и версии драйвера адаптера дисплея в компьютере пользователя. Эта информация вовсе не обязательна для успешного запуска игры, но мы всё равно предоставим её, так как сделать это совсем не сложно.
Последние 4 переменные используются для хранения информации о том, как именно будет создаваться объект устройства.
m_displayModes - это связный список (Linked list) режимов дисплея, в котором будут храниться все валидные комбинации режимов экрана, полученные сразу после перечисления адаптеров дисплея. Как только пользователь выбрал желаемый режим дисплея, он тут же сохраняется в переменной m_selectedDisplayMode, благодаря чему движок может быстро получить доступ к ней и использовать эти данные для создания объекта устройства.
Последние 2 флага (являются переменными типа BOOL) m_windowed и m_vsync указывают, выбрал ли пользователь запуск приложения в оконном режиме и включил ли v-sync. Как мы ранее упоминали, доступ к этим настройкам осуществляется через соответствующие функции IsWindowed и IsVSynced.

Создаём DeviceEnumeration.cpp (Проект Engine)

В файле исходного кода DeviceEnumeration.cpp будут размещаться реализации функций, объявленных в Scripting.h.
ОК, приступаем.

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи "DeviceEnumeration.cpp".
  • Жмём "Добавить".

Добавленный файл сразу откроется в правой части MSVC++2010.

  • В только что созданном и открытом файле DeviceEnumeration.cpp набираем следующий код:

DeviceEnumeration.cpp (Проект Engine)
//-----------------------------------------------------------------------------
// File: DeviceEnumeration.cpp
// Реализация классов и функций, объявленных в DeviceEnumeration.h .
// Перечисление адаптеров дисплея, создание диалогового окна
// и заполнение структуры с данными
// о графических настройках, выбранных пользователем.
//
// Original SourceCode:
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#include "Engine.h"

//-----------------------------------------------------------------------------
// Globals
//-----------------------------------------------------------------------------
DeviceEnumeration *g_deviceEnumeration = NULL;

//-----------------------------------------------------------------------------
// Функция обратного вызова (т.н. "директор" или "направитель") для обработчика
// сообщений диалогового окна графических настроек.
//-----------------------------------------------------------------------------
BOOL CALLBACK SettingsDialogProcDirector( HWND hDlg, UINT uiMsg, WPARAM wParam, LPARAM lParam )
{
	return g_deviceEnumeration->SettingsDialogProc( hDlg, uiMsg, wParam, lParam );
}

//-----------------------------------------------------------------------------
// Перечисляет доступные устройства Direct3D на адаптере дисплея по умолчанию
// (Первичный графический драйвер).
//-----------------------------------------------------------------------------
INT_PTR DeviceEnumeration::Enumerate( IDirect3D9 *d3d )
{
	// Создаём связный список видеорежимов.
	m_displayModes = new LinkedList< DisplayMode >;

	// Загружаем (или создаём заново) скрипт с установками экрана.
	m_settingsScript = new Script( "DisplaySettings.txt" );

	// Получаем детальную информацию об адаптере по умолчанию.
	d3d->GetAdapterIdentifier( D3DADAPTER_DEFAULT, 0, &m_adapter );

	// Строим список допустимых форматов пиксела.
	D3DFORMAT allowedFormats[6];
	allowedFormats[0] = D3DFMT_X1R5G5B5;
	allowedFormats[1] = D3DFMT_A1R5G5B5;
	allowedFormats[2] = D3DFMT_R5G6B5;
	allowedFormats[3] = D3DFMT_X8R8G8B8;
	allowedFormats[4] = D3DFMT_A8R8G8B8;
	allowedFormats[5] = D3DFMT_A2R10G10B10;

	// Проходим через список допустимых форматов пиксела.
	for( char af = 0; af < 6; af++ )
	{
		// Получаем количество видеорежимов и проходим через них.
		unsigned long totalAdapterModes = d3d->GetAdapterModeCount( D3DADAPTER_DEFAULT, allowedFormats[af] );
		for( unsigned long m = 0; m < totalAdapterModes; m++ )
		{
			// Получаем подробности о данном видеорежиме.
			D3DDISPLAYMODE mode;
			d3d->EnumAdapterModes( D3DADAPTER_DEFAULT, allowedFormats[af], m, &mode );

			// Отклоняем видеорежимы с низким разрешением.
			if( mode.Height < 480 )
				continue;

			// Создаём новый видеорежим (режим дисплея).
			DisplayMode *displayMode = new DisplayMode;
			memcpy( &displayMode->mode, &mode, sizeof( D3DDISPLAYMODE ) );
			if( af < 3 )
				strcpy( displayMode->bpp, "16 bpp" );
			else
				strcpy( displayMode->bpp, "32 bpp" );

			// Добавляем этот видеорежим в связный список.
			m_displayModes->Add( displayMode );
		}
	}

	return DialogBox( NULL, MAKEINTRESOURCE( IDD_GRAPHICS_SETTINGS ), NULL, SettingsDialogProcDirector );
}

//-----------------------------------------------------------------------------
// Обрабатываем оконные сообщения диалогового окна графических настроек.
//-----------------------------------------------------------------------------
INT_PTR DeviceEnumeration::SettingsDialogProc( HWND dialog, UINT msg, WPARAM wparam, LPARAM lparam )
{
	switch( msg )
	{
		case WM_INITDIALOG:
		{
			// Отображаем информацию об адаптере и версию его драйвера.
			char version[16];
			sprintf( version, "%d", LOWORD( m_adapter.DriverVersion.LowPart ) );
			Edit_SetText( GetDlgItem( dialog, IDC_DISPLAY_ADAPTER ), m_adapter.Description );
			Edit_SetText( GetDlgItem( dialog, IDC_DRIVER_VERSION ), version );

			// Проверяем, есть ли в скрипте графических настроек что-либо из этого.
			if( m_settingsScript->GetBoolData( "windowed" ) == NULL )
			{
				// Скрипт графических настроек пуст, поэтому по умолчанию запуск в оконном режиме.
				CheckDlgButton( dialog, IDC_WINDOWED, m_windowed = true );
			}
			else
			{
				// Загружаем стейт оконного режима.
				CheckDlgButton( dialog, IDC_WINDOWED, m_windowed = *m_settingsScript->GetBoolData( "windowed" ) );
				CheckDlgButton( dialog, IDC_FULLSCREEN, !m_windowed );

				// Проверяем тот случай, когда выбран полноэкранный режим (fullscreen).
				if( m_windowed == false )
				{
					// Включаем (делаем доступными) все элементы управления, относящиеся к полноэкранному режиму.
					EnableWindow( GetDlgItem( dialog, IDC_VSYNC ), true );
					EnableWindow( GetDlgItem( dialog, IDC_COLOUR_DEPTH ), true );
					EnableWindow( GetDlgItem( dialog, IDC_RESOLUTION ), true );
					EnableWindow( GetDlgItem( dialog, IDC_REFRESH_RATE ), true );

					// Загружаем состояние флага vsync.
					CheckDlgButton( dialog, IDC_VSYNC, m_vsync = *m_settingsScript->GetBoolData( "vsync" ) );

					// Заполняем комбо-бокс форматов дисплея.
					ComboBox_ResetContent( GetDlgItem( dialog, IDC_COLOUR_DEPTH ) );
					m_displayModes->Iterate( true );
					while( m_displayModes->Iterate() )
						if( !ComboBoxContainsText( dialog, IDC_COLOUR_DEPTH, m_displayModes->GetCurrent()->bpp ) )
							ComboBoxAdd( dialog, IDC_COLOUR_DEPTH, (void*)m_displayModes->GetCurrent()->mode.Format, m_displayModes->GetCurrent()->bpp );
					ComboBoxSelect( dialog, IDC_COLOUR_DEPTH, *m_settingsScript->GetNumberData( "bpp" ) );

					char text[16];

					// Заполняем комбо-бокс доступных разрешений экрана (resolutions).
					ComboBox_ResetContent( GetDlgItem( dialog, IDC_RESOLUTION ) );
					m_displayModes->Iterate( true );
					while( m_displayModes->Iterate() )
					{
						if( m_displayModes->GetCurrent()->mode.Format == (D3DFORMAT)PtrToUlong( ComboBoxSelected( dialog, IDC_COLOUR_DEPTH ) ) )
						{
							sprintf( text, "%d x %d", m_displayModes->GetCurrent()->mode.Width, m_displayModes->GetCurrent()->mode.Height );
							if (!ComboBoxContainsText( dialog, IDC_RESOLUTION, text ) )
								ComboBoxAdd( dialog, IDC_RESOLUTION, (void*)MAKELONG( m_displayModes->GetCurrent()->mode.Width, m_displayModes->GetCurrent()->mode.Height ), text );
						}
					}
					ComboBoxSelect( dialog, IDC_RESOLUTION, *m_settingsScript->GetNumberData( "resolution" ) );

					// Заполняем комбо-бокс доступных значений частоты обновления экрана (refresh rates).
					ComboBox_ResetContent( GetDlgItem( dialog, IDC_REFRESH_RATE ) );
					m_displayModes->Iterate( true );
					while( m_displayModes->Iterate() )
					{
						if( (DWORD)MAKELONG( m_displayModes->GetCurrent()->mode.Width, m_displayModes->GetCurrent()->mode.Height ) == (DWORD)PtrToUlong( ComboBoxSelected( dialog, IDC_RESOLUTION ) ) )
						{
							sprintf( text, "%d Hz", m_displayModes->GetCurrent()->mode.RefreshRate );
							if (!ComboBoxContainsText( dialog, IDC_REFRESH_RATE, text ) )
								ComboBoxAdd( dialog, IDC_REFRESH_RATE, (void*)m_displayModes->GetCurrent()->mode.RefreshRate, text );
						}
					}
					ComboBoxSelect( dialog, IDC_REFRESH_RATE, *m_settingsScript->GetNumberData( "refresh" ) );
				}
			}

			return true;
		}

		case WM_COMMAND:
		{
			switch( LOWORD(wparam) )
			{
				case IDOK:
				{
					// Сохраняем детальные настройки выбранного режима дисплея.
					m_selectedDisplayMode.Width = LOWORD( PtrToUlong( ComboBoxSelected( dialog, IDC_RESOLUTION ) ) );
					m_selectedDisplayMode.Height = HIWORD( PtrToUlong( ComboBoxSelected( dialog, IDC_RESOLUTION ) ) );
					m_selectedDisplayMode.RefreshRate = PtrToUlong( ComboBoxSelected( dialog, IDC_REFRESH_RATE ) );
					m_selectedDisplayMode.Format = (D3DFORMAT)PtrToUlong( ComboBoxSelected( dialog, IDC_COLOUR_DEPTH ) );
					m_windowed = IsDlgButtonChecked( dialog, IDC_WINDOWED ) ? true : false;
					m_vsync = IsDlgButtonChecked( dialog, IDC_VSYNC ) ? true : false;

					// Уничтожаем связный список режимов дисплея. (Да, он больше не нужен.)
					SAFE_DELETE( m_displayModes );

					// Получаем выбранное значение из каждого комбо-бокса.
					long bpp = ComboBox_GetCurSel( GetDlgItem( dialog, IDC_COLOUR_DEPTH ) );
					long resolution = ComboBox_GetCurSel( GetDlgItem( dialog, IDC_RESOLUTION ) );
					long refresh = ComboBox_GetCurSel( GetDlgItem( dialog, IDC_REFRESH_RATE ) );

					// Проверяем, есть ли в скрипте графических настроек какие-либо из этих параметров.
					if( m_settingsScript->GetBoolData( "windowed" ) == NULL )
					{
						// Добавляем все настройки в скрипт.
						m_settingsScript->AddVariable( "windowed", VARIABLE_BOOL, &m_windowed );
						m_settingsScript->AddVariable( "vsync", VARIABLE_BOOL, &m_vsync );
						m_settingsScript->AddVariable( "bpp", VARIABLE_NUMBER, &bpp );
						m_settingsScript->AddVariable( "resolution", VARIABLE_NUMBER, &resolution );
						m_settingsScript->AddVariable( "refresh", VARIABLE_NUMBER, &refresh );
					}
					else
					{
						// Устанавливаем новые параметры + их значения.
						m_settingsScript->SetVariable( "windowed", &m_windowed );
						m_settingsScript->SetVariable( "vsync", &m_vsync );
						m_settingsScript->SetVariable( "bpp", &bpp );
						m_settingsScript->SetVariable( "resolution", &resolution );
						m_settingsScript->SetVariable( "refresh", &refresh );
					}

					// Сохраняем все настройки в файл скрипта графических настроек.
					m_settingsScript->SaveScript();

					// Уничтожаем скрипт графических настроек.
					SAFE_DELETE( m_settingsScript );

					// Закрываем диалоговое окно графических настроек.
					EndDialog( dialog, IDOK );

					return true;
				}

				case IDCANCEL:
				{
					// Уничтожаем список видеорежимов.
					SAFE_DELETE( m_displayModes );

					// Уничтожаем скрипт графических настроек.
					SAFE_DELETE( m_settingsScript );

					EndDialog( dialog, IDCANCEL );

					return true;
				}

				case IDC_COLOUR_DEPTH:
				{
					if( CBN_SELCHANGE == HIWORD(wparam) )
					{
						char res[16];
						DWORD selectedRes = (DWORD)PtrToUlong( ComboBoxSelected( dialog, IDC_RESOLUTION ) );

						// Обновляем комбо-бокс выбора разрешения экрана (resolution).
						ComboBox_ResetContent( GetDlgItem( dialog, IDC_RESOLUTION ) );
						m_displayModes->Iterate( true );
						while( m_displayModes->Iterate() )
						{
							if( m_displayModes->GetCurrent()->mode.Format == (D3DFORMAT)PtrToUlong( ComboBoxSelected( dialog, IDC_COLOUR_DEPTH ) ) )
							{
								sprintf( res, "%d x %d", m_displayModes->GetCurrent()->mode.Width, m_displayModes->GetCurrent()->mode.Height );
								if( !ComboBoxContainsText( dialog, IDC_RESOLUTION, res ) )
								{
									ComboBoxAdd( dialog, IDC_RESOLUTION, (void*)MAKELONG( m_displayModes->GetCurrent()->mode.Width, m_displayModes->GetCurrent()->mode.Height ), res );
									if( selectedRes == (DWORD)MAKELONG( m_displayModes->GetCurrent()->mode.Width, m_displayModes->GetCurrent()->mode.Height ) )
										ComboBoxSelect( dialog, IDC_RESOLUTION, (void*)selectedRes );
								}
							}
						}
						if( ComboBoxSelected( dialog, IDC_RESOLUTION ) == NULL )
							ComboBoxSelect( dialog, IDC_RESOLUTION, 0 );
					}

					return true;
				}

				case IDC_RESOLUTION:
				{
					if( CBN_SELCHANGE == HIWORD(wparam) )
					{
						char refresh[16];
						DWORD selectedRefresh = (DWORD)PtrToUlong( ComboBoxSelected( dialog, IDC_REFRESH_RATE ) );

						// Обновляем комбо-бокс выбора значения частоты обновления экрана (refresh rate).
						ComboBox_ResetContent( GetDlgItem( dialog, IDC_REFRESH_RATE ) );
						m_displayModes->Iterate( true );
						while( m_displayModes->Iterate() )
						{
							if( (DWORD)MAKELONG( m_displayModes->GetCurrent()->mode.Width, m_displayModes->GetCurrent()->mode.Height ) == (DWORD)PtrToUlong( ComboBoxSelected( dialog, IDC_RESOLUTION ) ) )
							{
								sprintf( refresh, "%d Hz", m_displayModes->GetCurrent()->mode.RefreshRate );
								if( !ComboBoxContainsText( dialog, IDC_REFRESH_RATE, refresh ) )
								{
									ComboBoxAdd( dialog, IDC_REFRESH_RATE, (void*)m_displayModes->GetCurrent()->mode.RefreshRate, refresh );
									if( selectedRefresh == m_displayModes->GetCurrent()->mode.RefreshRate )
										ComboBoxSelect( dialog, IDC_REFRESH_RATE, (void*)selectedRefresh );
								}
							}
						}
						if( ComboBoxSelected( dialog, IDC_REFRESH_RATE ) == NULL )
							ComboBoxSelect( dialog, IDC_REFRESH_RATE, 0 );
					}

					return true;
				}

				case IDC_WINDOWED:
				case IDC_FULLSCREEN:
				{
					// Проверяем, изменил ли пользователь значение флага оконного/полноэкранного режима.
					if( IsDlgButtonChecked( dialog, IDC_WINDOWED ) )
					{
						// Очистить и сделать недоступными все элементы управления, относящиеся к полноэкранному режиму.
						ComboBox_ResetContent( GetDlgItem( dialog, IDC_COLOUR_DEPTH ) );
						ComboBox_ResetContent( GetDlgItem( dialog, IDC_RESOLUTION ) );
						ComboBox_ResetContent( GetDlgItem( dialog, IDC_REFRESH_RATE ) );
						CheckDlgButton( dialog, IDC_VSYNC, false );
						EnableWindow( GetDlgItem( dialog, IDC_VSYNC ), false );
						EnableWindow( GetDlgItem( dialog, IDC_COLOUR_DEPTH ), false );
						EnableWindow( GetDlgItem( dialog, IDC_RESOLUTION ), false );
						EnableWindow( GetDlgItem( dialog, IDC_REFRESH_RATE ), false );
					}
					else
					{
						// Активировать (сделать доступными) все элементы управления, относящиеся к полноэкранному режиму.
						EnableWindow( GetDlgItem( dialog, IDC_VSYNC ), true );
						EnableWindow( GetDlgItem( dialog, IDC_COLOUR_DEPTH ), true );
						EnableWindow( GetDlgItem( dialog, IDC_RESOLUTION ), true );
						EnableWindow( GetDlgItem( dialog, IDC_REFRESH_RATE ), true );

						// Заполняем комбо-бокс с валидными форматами дисплея (экрана).
						ComboBox_ResetContent( GetDlgItem( dialog, IDC_COLOUR_DEPTH ) );
						m_displayModes->Iterate( true );
						while( m_displayModes->Iterate() )
						{
							if( !ComboBoxContainsText( dialog, IDC_COLOUR_DEPTH, m_displayModes->GetCurrent()->bpp ) )
								ComboBoxAdd( dialog, IDC_COLOUR_DEPTH, (void*)m_displayModes->GetCurrent()->mode.Format, m_displayModes->GetCurrent()->bpp );
						}
						ComboBoxSelect( dialog, IDC_COLOUR_DEPTH, 0 );
					}

					return true;
				}
			}
		}
	}

	return false;
}

//-----------------------------------------------------------------------------
// Возвращает выбранный режим дисплея (видеорежим).
//-----------------------------------------------------------------------------
D3DDISPLAYMODE *DeviceEnumeration::GetSelectedDisplayMode()
{
	return &m_selectedDisplayMode;
}

//-----------------------------------------------------------------------------
// Получает данные о том, какой режим выбран: оконный или полноэкранный.
//-----------------------------------------------------------------------------
bool DeviceEnumeration::IsWindowed()
{
	return m_windowed;
}

//-----------------------------------------------------------------------------
// Получает данные о том, выбран ли режим v-sync.
//-----------------------------------------------------------------------------
bool DeviceEnumeration::IsVSynced()
{
	return m_vsync;
}

//-----------------------------------------------------------------------------
// Добавляет новую запись в комбо-бокс.
//-----------------------------------------------------------------------------
void DeviceEnumeration::ComboBoxAdd( HWND dialog, int id, void *data, char *desc )
{
	HWND control = GetDlgItem( dialog, id );
	int i = ComboBox_AddString( control, desc );
	ComboBox_SetItemData( control, i, data );
}

//-----------------------------------------------------------------------------
// Выбирает запись (строку) в комбо-боксе по её индексу.
//-----------------------------------------------------------------------------
void DeviceEnumeration::ComboBoxSelect( HWND dialog, int id, int index )
{
	HWND control = GetDlgItem( dialog, id );
	ComboBox_SetCurSel( control, index );
	PostMessage( dialog, WM_COMMAND, MAKEWPARAM( id, CBN_SELCHANGE ), (LPARAM)control );
}

//-----------------------------------------------------------------------------
// Выбирает запись (строку) в комбо-боксе по содержащимся в ней данным.
//-----------------------------------------------------------------------------
void DeviceEnumeration::ComboBoxSelect( HWND dialog, int id, void *data )
{
	HWND control = GetDlgItem( dialog, id );
	for( int i = 0; i < ComboBoxCount( dialog, id ); i++ )
	{
		if( (void*)ComboBox_GetItemData( control, i ) == data )
		{
			ComboBox_SetCurSel( control, i );
			PostMessage( dialog, WM_COMMAND, MAKEWPARAM( id, CBN_SELCHANGE ), (LPARAM)control );
			return;
		}
	}
}

//-----------------------------------------------------------------------------
// Возвращает данные выбранной записи в комбо-боксе.
//-----------------------------------------------------------------------------
void *DeviceEnumeration::ComboBoxSelected( HWND dialog, int id )
{
	HWND control = GetDlgItem( dialog, id );
	int index = ComboBox_GetCurSel( control );
	if( index < 0 )
		return NULL;
	return (void*)ComboBox_GetItemData( control, index );
}

//-----------------------------------------------------------------------------
// Проверяет, корректна ли запись, выбранная в комбо-боксе.
//-----------------------------------------------------------------------------
bool DeviceEnumeration::ComboBoxSomethingSelected( HWND dialog, int id )
{
	HWND control = GetDlgItem( dialog, id );
	int index = ComboBox_GetCurSel( control );
	return ( index >= 0 );
}

//-----------------------------------------------------------------------------
// Возвращает количество записей (строк) в комбо-боксе.
//-----------------------------------------------------------------------------
int DeviceEnumeration::ComboBoxCount( HWND dialog, int id )
{
	HWND control = GetDlgItem( dialog, id );
	return ComboBox_GetCount( control );
}

//-----------------------------------------------------------------------------
// Проверяет, содержат ли записи (строки) в комбо-боксе заданный текст.
//-----------------------------------------------------------------------------
bool DeviceEnumeration::ComboBoxContainsText( HWND dialog, int id, char *text )
{
	char item[MAX_PATH];
	HWND control = GetDlgItem( dialog, id );
	for( int i = 0; i < ComboBoxCount( dialog, id ); i++ )
	{
		ComboBox_GetLBText( control, i, item );
		if( lstrcmp( item, text ) == 0 )
			return true;
	}
	return false;
}

  • Сохрани Решение (Файл->Сохранить все).

Реализация функции Enumerate

Функция Enumerate применяется для начала процесса энумерации (перечисления) адаптеров дисплея.

Фрагмент DeviceEnumeration.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Перечисляет доступные устройства Direct3D на адаптере дисплея по умолчанию
// (Первичный графический драйвер).
//-----------------------------------------------------------------------------
INT_PTR DeviceEnumeration::Enumerate( IDirect3D9 *d3d )
{
	// Создаём связный список видеорежимов.
	m_displayModes = new LinkedList< DisplayMode >;

	// Загружаем (или создаём заново) скрипт с установками экрана.
	m_settingsScript = new Script( "DisplaySettings.txt" );

	// Получаем детальную информацию об адаптере по умолчанию.
	d3d->GetAdapterIdentifier( D3DADAPTER_DEFAULT, 0, &m_adapter );

	// Строим список допустимых форматов пиксела.
	D3DFORMAT allowedFormats[6];
	allowedFormats[0] = D3DFMT_X1R5G5B5;
	allowedFormats[1] = D3DFMT_A1R5G5B5;
	allowedFormats[2] = D3DFMT_R5G6B5;
	allowedFormats[3] = D3DFMT_X8R8G8B8;
	allowedFormats[4] = D3DFMT_A8R8G8B8;
	allowedFormats[5] = D3DFMT_A2R10G10B10;

	// Проходим через список допустимых форматов пиксела.
	for( char af = 0; af < 6; af++ )
	{
		// Получаем количество видеорежимов и проходим через них.
		unsigned long totalAdapterModes = d3d->GetAdapterModeCount( D3DADAPTER_DEFAULT, allowedFormats[af] );
		for( unsigned long m = 0; m < totalAdapterModes; m++ )
		{
			// Получаем подробности о данном видеорежиме.
			D3DDISPLAYMODE mode;
			d3d->EnumAdapterModes( D3DADAPTER_DEFAULT, allowedFormats[af], m, &mode );

			// Отклоняем видеорежимы с низким разрешением.
			if( mode.Height < 480 )
				continue;

			// Создаём новый видеорежим (режим дисплея).
			DisplayMode *displayMode = new DisplayMode;
			memcpy( &displayMode->mode, &mode, sizeof( D3DDISPLAYMODE ) );
			if( af < 3 )
				strcpy( displayMode->bpp, "16 bpp" );
			else
				strcpy( displayMode->bpp, "32 bpp" );

			// Добавляем этот видеорежим в связный список.
			m_displayModes->Add( displayMode );
		}
	}

	return DialogBox( NULL, MAKEINTRESOURCE( IDD_GRAPHICS_SETTINGS ), NULL, SettingsDialogProcDirector );
}
...

В качестве параметра она принимает указатель на объект Direct3D, который используется для получения доступа к информации об адаптере дисплея, а также для целей их энумерации. И первым делом мы подготавливаем к энумерации несколько переменных членов.
Первым создаётся связный список (Linked list) адаптеров дисплея. После этого создаём новый скрипт DisplaySettings.txt и назначаем указатель на него, который хранится в переменной m_settingsScript.

Фрагмент DeviceEnumeration.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Перечисляет доступные устройства Direct3D на адаптере дисплея по умолчанию
// (Первичный графический драйвер).
//-----------------------------------------------------------------------------
INT_PTR DeviceEnumeration::Enumerate( IDirect3D9 *d3d )
{
	// Создаём связный список видеорежимов.
	m_displayModes = new LinkedList< DisplayMode >;

	// Загружаем (или создаём заново) скрипт с установками экрана.
	m_settingsScript = new Script( "DisplaySettings.txt" );
...

В следующей строке мы получаем доступ к информации об адаптере дисплея путём вызова функции (метода) GetAdapterIdentifier, которая экспонирована объектом Direct3D.:

Фрагмент DeviceEnumeration.cpp (Проект Engine)
...
	// Получаем детальную информацию об адаптере по умолчанию.
	d3d->GetAdapterIdentifier( D3DADAPTER_DEFAULT, 0, &m_adapter );
...

Вот описание её параметров:
Таблца 2. Параметры функции GetAdapterIdentifier

Параметр Описание
UINT Adapter Параметр ввода. В нашем случае принимает значение D3DADAPTER_DEFAULT для получения сведений об основном (первичном) адаптере дисплея.
DWORD Flags Параметр ввода, флаг. В нашем случае принимает значение 0. Также может принимать значение D3DENUM_WHQL_LEVEL, что позволяет приложению выйти в Интернет для загрузки новых сертификатов Microsoft Windows Hardware Quality Labs (WHQL).
D3DADAPTER_IDENTIFIER9 *pIdentifier Параметр вывода (возвращаемое значение). В нашем случае принимает значение m_adapter и является указателем на структуру D3DADAPTER_IDENTIFIER9, которую мы будем заполнять сведениями об адаптере.


Чуть ниже видим список форматов дисплея (формат цветности пиксела), для которых мы будем искать доступные видеорежимы:

Фрагмент DeviceEnumeration.cpp (Проект Engine)
...
	// Строим список допустимых форматов пиксела.
	D3DFORMAT allowedFormats[6];
	allowedFormats[0] = D3DFMT_X1R5G5B5;
	allowedFormats[1] = D3DFMT_A1R5G5B5;
	allowedFormats[2] = D3DFMT_R5G6B5;
	allowedFormats[3] = D3DFMT_X8R8G8B8;
	allowedFormats[4] = D3DFMT_A8R8G8B8;
	allowedFormats[5] = D3DFMT_A2R10G10B10;
...

Здесь нас интересуют только 16-битные и 32-битные форматы пиксела, причём только те, что используются в игрокодинге более или менее часто. При энумерации адаптера дисплея, функция может вернуть множество других форматов дисплея, поддерживаемых данной видеокартой. Проблема в том, что мы не собираемся использовать все из них, так как многие форматы нам не подходят (или устарели). Например, существуют ряд 8-битных форматов дисплея, которые поддерживают все адаптеры без исключения. Но мы не будем их использовать, так как они позволяют одновременно выводить на экран не более 256 цветов, что явно недостаточно для получения качественной картинки. Хотя поэкспериментировать с ними, конечно, никто не запрещает.
В нашем случае мы выбрали несколько форматов дисплея, поддержку которых мы хотим включить в движок, и внесли их все в массив allowedFormats. При проведении энумерации движок будет выдавать видеорежимы, которые используют только указанные нами форматы дисплея (формат цветности пиксела), что и происходит чуть ниже.
Далее видим два цикла for (один находится внутри другого), где поочерёдно производится энумерация адаптера дисплея с учётом каждого из форматов дисплея, указанных в массиве allowedFormats:

Фрагмент DeviceEnumeration.cpp (Проект Engine)
...
	// Проходим через список допустимых форматов пиксела.
	for( char af = 0; af < 6; af++ )
	{
		// Получаем количество видеорежимов и проходим через них.
		unsigned long totalAdapterModes = d3d->GetAdapterModeCount( D3DADAPTER_DEFAULT, allowedFormats[af] );
		for( unsigned long m = 0; m < totalAdapterModes; m++ )
		{
			// Получаем подробности о данном видеорежиме.
			D3DDISPLAYMODE mode;
			d3d->EnumAdapterModes( D3DADAPTER_DEFAULT, allowedFormats[af], m, &mode );

			// Отклоняем видеорежимы с низким разрешением.
			if( mode.Height < 480 )
				continue;

			// Создаём новый видеорежим (режим дисплея).
			DisplayMode *displayMode = new DisplayMode;
			memcpy( &displayMode->mode, &mode, sizeof( D3DDISPLAYMODE ) );
			if( af < 3 )
				strcpy( displayMode->bpp, "16 bpp" );
			else
				strcpy( displayMode->bpp, "32 bpp" );

			// Добавляем этот видеорежим в связный список.
			m_displayModes->Add( displayMode );
		}
	}
...

Как только мы вошли в новую итерацию первого (внешнего) цикла for, немедленно запрашиваем (у Direct3D) общее количество режимов дисплея, которые поддерживает видеоадаптер для данного формата дисплея (поочерёдно выбираемого из массива allowedFormats). Мы получаем эту информацию от функции GetAdapterModeCount, экспонируемую объектом Direct3D. В параметрах мы продолжаем указывать D3DADAPTER_DEFAULT для опроса только первичного адаптера дисплея (т.е. который по умолчанию установлен в данной ОС). Мы также передаём в функцию формат дисплея (= формат цветности пиксела), для которого хотим узнать количество доступных видеорежимов.

Сразу после этого мы входим во второй цикл for, расположенный внутри первого. В этот раз мы "просматриваем" (итерируем через цикл) каждый из видеорежимов, отобранных функцией GetAdapterModeCount. Внутри цикла мы поочерёдно запрашиваем каждый видеорежим у Direct3D и затем помещаем их во временную структуру mode (типа D3DDISPLAYMODE). К каждому видеорежиму применяем функцию EnumAdapterModes для получения доступа к детальной информации о них. Здесь тоже указываем параметр D3DADAPTER_DEFAULT для опроса только первичного адаптера дисплея (т.е. который по умолчанию установлен в данной ОС). В параметрах мы также передаём формат дисплея и индекс видеорежима для которого запрашиваем детальную информацию.

Дальше мы проводим небольшую проверку, "отсекая" видеорежимы с чересчур низким разрешением экрана. Все видеорежимы с высотой экрана менее 480 точек не подходят для наших целей. Таким образом мы указываем минимальное поддерживаемое разрешение в 640х480 пикселов. Если у тестируемого формата высота экрана меньше (а большинство видеокарт поддерживают такие форматы в 16-битном цвете), то он пропускается путём указания служебного слова continue, которое немедленно прерывает выполнение цикла и даёт команду на начало его следующей итерации. Если видеорежим прошёл тест на размер экрана по вертикали, он может рассматриваться как валидный (корректный).
Сразу после этого создаём новую структуру DisplayMode и копируем в неё данные об итерируемом видеорежиме при помощи функции memcpy.

Следующий шаг - создание небольшого текстового описания, для наглядного представления форматов дисплея пользователю ("16 bpp" и "32 bpp"). Текстовое описание хранится в массиве bpp (тип char), расположенном внутри структуры DisplayMode (см. DeviceEnumeration.h).

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

По завершении обоих циклов энумерация адаптера дисплея будет полностью завершена и функция Enumerate вызывает заранее подготовленное диалоговое окно IDD_GRAPHICS_SETTINGS (создадим чуть позднее):

Фрагмент DeviceEnumeration.cpp (Проект Engine)
...
	return DialogBox( NULL, MAKEINTRESOURCE( IDD_GRAPHICS_SETTINGS ), NULL, SettingsDialogProcDirector );
...

К этому времени связный список m_displayModes будет полностью заполнен значениями всех корректных режимов адаптера дисплея, поддерживаемые форматами дисплея, указанными в массиве allowedFormats. Теперь мы готовы представить этот список пользователю, чтобы он выбрал нужный режим из выпадающего списка (combo box) специального диалогового окна, которое мы создадим через пару мгновений.
Функция DialogBox создаёт и показывает на экране указанное диалоговое окно. Вот её прототип:

Прототип функции DialogBox
INT_PTR DialogBox
{
  HINSTANCE hInstance,  // Дескриптор исполняемого модуля приложения.
  LPCTSTR  lpTemplate,  // Шаблон загружаемого диалогового окна.
  HWND hWndParent,  // Дескриптор окна приложения.
  DLGPROC lpDialogFunc  // Функция обратного вызова (call-back function) процедуры выборки сообщений диалогового окна.
}

В нашем случае первый параметр устанавливаем в NULL.
Второй параметр - идентификатор, который указывает функции, какой именно ресурс диалога должен быть загружен (о ресурсах речь пойдёт чуть ниже). Здесь мы применяем макрос MAKEINTRESOURCE для конвертации имени диалогового окна (которое имеет числовое значение типа integer) в значение типа resource, которое совместимо с данной функцией.
Третий параметр устанавливаем в NULL.
Четвёртый параметр указывает на функцию, которая будет выполнять обратный вызов процедуры диалогового окна (тоно также, как мы это делали при создании главного окна игрового приложения).

Создание диалогового окна графических настроек (Graphic Settings)

При использовании различных Windows-приложений ты часто видишь окна этих приложений с размещёнными на них элементами управления. Вот лишь некоторые из них:

  • Кнопка (Button)
  • Строка ввода текста (text box или edit box)
  • Комбо-бокс (combo box; текстовая строка с выпадающим меню)
  • Древовидные элементы (tree controls).

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

  • Модальное диалоговое окно создаётся на основе шаблона, хранящегося в ресурсах или созданного в памяти.
  • Для создания диалогового окна используются функция DialogBox или DialogBoxParam. Во время выполнения функция не возвращает управление до тех пор, пока не будет вызвана функция EndDialog.
  • Функция обработки сообщений диалогового окна очень похожа на функцию обработки сообщений обычного окна. Отличия очень незначительные. Если обработку сообщений берёт на себя функция, то она возвращает TRUE, в противном случае возвращается FALSE. От ОС MS Windows в диалоговое окно приходит сообщение WM_INITDIALOG вместо WM_CREATE.
  • В исходном коде диалогового окна часто отсутствует цикл обработки сообщений. На самом деле он тоже имеется, но его создаёт операционная система. Она же берёт на себя обработку и перенаправление сообщений.
  • Важным моментом работы с модальными диалоговыми окнами является обработка сообщения WM_CLOSE при вызове функции EndDialog, которая удаляет из памяти модальное диалоговое окно.


Типичным примером модального диалогового окна является окно, вызываемое функцией Windows API MessageBox. Здесь ОС берёт на себя не только обработку сообщений, но и создание шаблона окна, а также организацию функций сообщений окна.

MS Visual C++ позволяет добавлять в Проекты диалоговые окна, которые затем активируются во время выполнения приложения (как в нашем примере, когда мы вызывали функцию DialogBox). При создании диалогового окна (вообще, это один из видов ресурсов) необходимо указать его имя, по которому оно будет вызываться из главного окна приложения. Взглянув на вызов функции DialogBox, размещённой в конце функции Enumerate, видим, что здесь даётся команда на загрузку диалогового окна IDD_GRAPHICS_SETTINGS, которое мы применим в нашем движке для предоставления возможности пользователю указывать графические настройки.
При создании новых диалогов ты можешь изменять их на своё усмотрение и размещать на них любые элементы управления. Всякий раз при добавлении элемента в диалоговое окно, ему (элементу) необходимо присваивать уникальное имя, которое будет использоваться для обращения к нему (например для получения данных или его деактивации).

Диалоговые окна (как и другие ресурсы) хранятся в специальных файлах с расширением .rc (ещё их называют "шаблонами ресурсов"), используемых MS Visual C++. При открытии шаблона ресурса в текстовом редакторе, легко убедиться, что он представляет собой обычный текстовый файл, содержащий массу информации о различных ресурсах, сохранённых в нём. Если шаблон ресурсов содержит диалоговое окно (иногда их несколько), то, помимо информации о самом диалоге, в нём также можно найти подробные сведения о каждом элементе управления, принадлежащем соответствующему диалоговому окну (например имя элемента, размеры, положение на форме и т.д.).
После добавления в Проект нового ресурса (обычно его добавляют в соответствующий фильтр "Ресурсы"), при его первом сохранении MS Visual C++ запросит указать имя шаблона ресурса. После сохранения MS Visual C++ создаёт 2 файла:

  • Шаблон ресурсов (.rc)
  • Заголовочный файл с именем resource.h, который является своеобразным интерфейсом со ссылками на содержимое шаблона ресурсов.

Оба файла должны быть включены в Проект (или в Проекты одного Решения).

Закрыть
noteПримечание

В нашем случае шаблон ресурсов (.rc) будет размещён в Проекте Test, а его заголовок resource.h - в Проекте Engine. Это связано с тем, что файл шаблона ресурсов должен быть доступен исполняемому файлу приложения (Проект Test). Проект Engine не создаёт исполняемого файла (т.к. создаёт библиотеку DLL), поэтому ему не нужен шаблон ресурсов. Заголовочный файл resource.h должен быть доступен любому Проекту, которому необходимо использовать ресурсы. Так как наш движок во время компиляции будет получать доступ к диалоговому окну графических установок и его элементам управления, заголовок resource.h должен быть включён в Проект Engine. Но, несмотря на это, "физически" эти два файла должны располагаться в одном каталоге (в нашем случае это будет каталог Проекта Engine), т.к. resource.h ссылается на элементы шаблона ресурсов, расположенного в том же каталоге. В противном случае придётся править пути в resource.h


Начиная с MS Visual C++ 2005 эта интегрированная среда разработки (ИСР) поставляется в двух (иногда в трёх и более) вариантах: платном и бесплатном. В платном варианте IDE предоставляет широкий функционал и широчайшие возможности. В бесплатном варианте - напротив, базовый функционал сильно урезан. Именно с таким бесплатным вариантом мы и работали всё это время, о чём говорит слово "Express" в его названии. Так вот, в нашей MS Visual C++ 2010 Express отсутствует редактор ресурсов, при помощи которого нам необходимо создать модальное диалоговое окно графических настроек игры.

Закрыть
noteПримечание

Точнее, редактор ресурсов отсутствует для приложений Win32 (с т.н. "неуправляемым" исходным кодом). Как вариант, можно задействовать конструктор диалоговых окон из библиотеки CLR(external link), присутствующей в любой ОС с установленным .NET Framework. Но в этом случае наша игра просто откажется стартовать без установленного в системе .NET Framework 4.0, что приведёт к необходимости добавить его установщик в дистрибутив с игрой. Это не соответствует нашим целям спрограммировать игру на классическом C++. .NET-программирование это тема отдельного курса, в которую мы пока вдаваться не будем.

Рис. 8 Программа ResEdit. Указываем пути вложений.
Рис. 8 Программа ResEdit. Указываем пути вложений.
ъ
Рис. 9 Программа ResEdit. Главное окно с заготовкой диалогового окна.
Рис. 9 Программа ResEdit. Главное окно с заготовкой диалогового окна.


На помощь придёт бесплатная программа ResEdit (забираем, например, отсюда: http://soft.oszone.net/program/11258/ResEdit/(external link)), которая позволяет:

  • Cоздавать и редактировать диалоговые окна с "интуитивно понятным" перетаскиванием элементов управления на форму;
  • Редактировать исполняемые PE-файлы (EXE, DLL), извлекая из них структуру вложенных ресурсов;
  • Создавать шаблоны ресурсов (.rc), компилированные модули ресурсов (.res) и даже исполняемые файлы (.exe).

ResEdit воспроизводит язык описания ресурсов и сохраняет свои проекты в файлы шаблонов ресурсов, полностью совместимые с MS Visual C++. Помимо этого, в том же каталоге автоматически создаётся заголовочный файл resource.h, необходимый для включения соответствующего шаблона ресурсов в Проекты MS Visual C++. Нам останется лишь поместить оба готовых файла в каталог Проекта Engine и добавить их в Проекты нашего Решения GameProject01, что можно проделать даже в бесплатной MS Visual C++ 2010 Express.

Ок, начнём проектировать диалоговое окно графических настроек адаптера.

  • После установки, стартуй ResEdit. Ярлык найдёшь в меню Пуск.
  • В появившемся мастере выбираем "Developer mode" (Режим разработки; мы будем создавать проект с нуля). Жмём "Next".
  • На след. странице отмечаем радиокнопкой пункт "Resource Script (*.rc)" и в поле ввода "Name" вводим имя Resource. При необходимости меняем каталог проекта (каталог по умолчанию C:\Users\<Имя пользователя>\ResEdit Projects\ - не лучший вариант). Жмём "Finish".
  • В появившемся сообщении говорится, что сперва необходимо указать пути к каталогам с заголовочными файлами. Жмём "Yes".
  • В появившемся окне настроек (Preferences) сразу откроется раздел Include Paths (Пути заголовочных файлов Include). Жмём на кнопку с многоточием и в появившемся окне "Обзор папок" указываем путь к установленному Windows SDK. В нашем случае путь оказался такой: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Include\ (см. Рис. 8). При необходимости бесплатно загружаем Windows SDK с официального сайта Майкрософт, например по этой ссылке: https://www.microsoft.com/en-US/download/details.aspx?id=3138(external link). Жмём ОК.

Проект создан, но в нём пока нет ресурсов. Создадим диалоговое окно.

  • В панели Resources главного окна программы щёлкаем правой кнопкой мыши. Во вспылвающем контекстном меню выбираем "Add Resource...->Dialog".

В центральной части появится форма диалогового окна с парой кнопок, слева - обозреватель ресурсов (Resources) и инспектор свойств (Properties), справа - панель инструментов (Toolbox) с элементами управления, пригодными для перетаскивания на форму (см. Рис. 9). Примерный вид итогового диалогового окна показа на Рис.10.

Рис. 10 Диалоговое окно графических настроек
Рис. 10 Диалоговое окно графических настроек

Дальше всё предельно просто:

  • Отмечаем одним кликом мыши необходимый элемент из панели инструментов (Toolbox) и "наносим" его на форму диалогового окна в центральной части окна программы ResEdit (установив курсор над формой и зажав левую кнопку мыши, перемещаем его слева направо, сверху вниз).
  • Размещаем и масштабируем каждый из элементов управления на форме примерно так, как показано на Рис.10.
  • С помощью инспектора свойств (Properties) устанавливаем нужные свойства каждого элемента управления, следуя инструкциям в Таблице 3.
  • Периодически сохраняем проект (File->Save).

Расположение элементов управления на форме может быть любым (строго выравнивать их по линейке необязательно). Главное - чтобы пользователь мог без труда разобраться, что к чему. Для упрощения задачи на Рис.10 рядом с каждым элементом управления нанесён красно-белый кружок-маркер с цифрой. Каждая цифра соответствует номеру строки Таблицы 3, представленной ниже.

  • Сверяй номера маркеров с соответствующими строками в Таблице 3.
  • Редактируй свойства ресурсов и элементов управления в инспекторе свойств (Properties) программы ResEdit в соответствии с подсказками, представленными в Таблице 3.

Таблица 3. Свойства элементов управления диалогового окна графических настроек

№ маркера Тип ресурса или элемента упр. в окне Toolbox Свойства (в окне Properties)
1 (РЕСУРС!) Dialog, Диалоговое окно (форма). ID=IDD_GRAPHICS_SETTINGS; (Для смены ID формы в окне Resources щёлкни правой кнопкой мыши по значку IDD_DIALOG и в контекстном меню выбери Rename. В появившемся окне в строке "Ordinal Identfier" введи новое имя Ресурса.) Caption="Graphic Settings (Графические настройки)"; Width=290; Height=185.
2 Group Box Caption="Adapter Details (Сведения об адаптере)"; XPosition=7; YPosition=7; Width=276; Height=48.
3 Static Text Caption="Display Adapter:"; XPosition=16; YPosition=20; Width=98; Height=8.
4 Static Text Caption="Driver Version:"; XPosition=16; YPosition=38; Width=98; Height=8.
5 Edit Control ID=IDC_DISPLAY_ADAPTER; XPosition=115; YPosition=17; Width=159; Height=12; Read Only=True.
6 Edit Control ID=IDC_DRIVER_VERSION; XPosition=115; YPosition=34; Width=159; Height=12; Read Only=True.
7 Group Box Caption="Display Settings"; XPosition=7; YPosition=57; Width=276; Height=100.
8 Radio Button Caption="Windowed"; ID=IDC_WINDOWED; Group=True; TabStop=True; Auto=True; XPosition=16; YPosition=73; Width=98; Height=8.
9 Radio Button Caption="Fullscreen"; ID=IDC_FULLSCREEN; Auto=True; XPosition=16; YPosition=89; Width=98; Height=8.
10 CheckBox Caption="V-Sync"; ID=IDC_VSYNC; Disabled=True; TabStop=True; Auto=True; XPosition=115; YPosition=89; Width=60; Height=8.
11 Static Text Caption="Colour Depth:"; XPosition=16; YPosition=106; Width=98; Height=8.
12 ComboBox ID=IDC_COLOUR_DEPTH; Disabled=True; Group=True; TabStop=True; Vertical ScrollBar=True; Type=Drop List; XPosition=115; YPosition=103; Width=159; Height=204.
13 Static Text Caption="Resolution:"; XPosition=16; YPosition=124; Width=98; Height=8.
14 ComboBox ID=IDC_RESOLUTION; Disabled=True; Group=True; TabStop=True; Vertical ScrollBar=True; Type=Drop List; XPosition=115; YPosition=121; Width=159; Height=204.
15 Static Text Caption="Refresh Rate:"; XPosition=16; YPosition=140; Width=98; Height=8.
16 ComboBox ID=IDC_REFRESH_RATE; Disabled=True; Group=True; TabStop=True; Vertical ScrollBar=True; Type=Drop List; XPosition=115; YPosition=137; Width=159; Height=204.
17 Button ID=IDOK; Caption="OK"; TabStop=True; Default Button = True; XPosition=65; YPosition=164; Width=50; Height=14.
18 Button ID=IDCANCEL; Caption="Cancel"; TabStop=True; XPosition=175; YPosition=164; Width=50; Height=14.

Свойства элементов управления, не указанные в Таблице 3, оставляем как есть. У всех элементов типа Static Text идентификатор ID автоматически устанавливается в IDC_STATIC. (Проверь!)

  • Сохрани Проект диалогового окна.
  • Открой каталог проекта Resource программы ResEdit с помощью Проводника Windows.

Путь к нему выводится на тайтлбаре (titlebar; Это полоса над окном программы, где, обычно, пишется название программы, а в правом углу расположены значки - свернуть/развернуть/закрыть.) главного окна программы ResEdit. В нашем случае он такой: C:\Users\<Имя пользователя>\ResEdit Projects\Resource.

Закрыть
noteПримечание

При просмотре каталогов через Проводник русскоязычной Windows 7, каталог "Users" отображается под псевдонимом "Пользователи".

В нём видим 2 файла:

  • Resource.rc
  • resource.h

Первым делом переименуй resource.h в Resource.h, сменив регистр первой буквы имени. Так в окне MSVC++ мы сразу будем видеть, что заголовок Resource.h относится к шаблону ресурсов Resource.rc . Напомним, что оба файла можно открыть в любом текстовом редакторе (например, Блокнот Windows) и просмотреть/отредактировать их содержимое. Вот их листинги:

Resource.h (Проект Engine)
#ifndef IDC_STATIC
#define IDC_STATIC (-1)
#endif

#define IDD_GRAPHICS_SETTINGS                   100
#define IDC_DISPLAY_ADAPTER                     40000
#define IDC_DRIVER_VERSION                      40001
#define IDC_WINDOWED                            40002
#define IDC_VSYNC                               40003
#define IDC_COLOUR_DEPTH                        40004
#define IDC_REFRESH_RATE                        40005
#define IDC_RESOLUTION                          40006
#define IDC_FULLSCREEN                          40007

Resource.h - заголовочный файл, в котором мы просто определяем (назначаем) уникальные ID-идентификаторы ресурсов, которые будем использовать.

Resource.rc (Проект Test)
// Generated by ResEdit 1.6.6
// Copyright (C) 2006-2015
// http://www.resedit.net

#include <windows.h>
#include <commctrl.h>
#include <richedit.h>
#include "resource.h"

//
// Dialog resources
//
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDD_GRAPHICS_SETTINGS DIALOG 0, 0, 290, 185
STYLE DS_3DLOOK | DS_CENTER | DS_MODALFRAME | DS_SHELLFONT | WS_CAPTION | WS_VISIBLE | WS_POPUP | WS_SYSMENU
CAPTION "Graphic Settings (Графические настройки)"
FONT 8, "Ms Shell Dlg"
{
    GROUPBOX        "Display Settings", 0, 7, 57, 276, 100, 0, WS_EX_LEFT
    AUTORADIOBUTTON "Windowed", IDC_WINDOWED, 16, 73, 98, 8, WS_GROUP | WS_TABSTOP, WS_EX_LEFT
    AUTORADIOBUTTON "Fullscreen", IDC_FULLSCREEN, 16, 89, 98, 8, 0, WS_EX_LEFT
    LTEXT           "Colour Depth:", 0, 16, 106, 98, 8, SS_LEFT, WS_EX_LEFT
    COMBOBOX        IDC_COLOUR_DEPTH, 115, 103, 159, 204, WS_GROUP | WS_TABSTOP | WS_VSCROLL | WS_DISABLED | CBS_DROPDOWNLIST | CBS_HASSTRINGS, WS_EX_LEFT
    LTEXT           "Resolution:", 0, 16, 124, 36, 9, SS_LEFT, WS_EX_LEFT
    COMBOBOX        IDC_RESOLUTION, 115, 121, 159, 204, WS_GROUP | WS_TABSTOP | WS_VSCROLL | WS_DISABLED | CBS_DROPDOWNLIST | CBS_HASSTRINGS, WS_EX_LEFT
    LTEXT           "Refresh Rate:", 0, 16, 140, 98, 8, SS_LEFT, WS_EX_LEFT
    COMBOBOX        IDC_REFRESH_RATE, 115, 137, 159, 204, WS_GROUP | WS_TABSTOP | WS_VSCROLL | WS_DISABLED | CBS_DROPDOWNLIST | CBS_HASSTRINGS, WS_EX_LEFT
    AUTOCHECKBOX    "V-Sync", IDC_VSYNC, 115, 89, 60, 8, WS_DISABLED, WS_EX_LEFT
    LTEXT           "Display Adapter:", 0, 16, 20, 98, 8, SS_LEFT, WS_EX_LEFT
    LTEXT           "Driver Version:", 0, 16, 38, 98, 8, SS_LEFT, WS_EX_LEFT
    EDITTEXT        IDC_DISPLAY_ADAPTER, 115, 17, 159, 12, ES_AUTOHSCROLL | ES_READONLY, WS_EX_LEFT
    EDITTEXT        IDC_DRIVER_VERSION, 115, 34, 159, 12, ES_AUTOHSCROLL | ES_READONLY, WS_EX_LEFT
    PUSHBUTTON      "Cancel", IDCANCEL, 175, 164, 50, 14, 0, WS_EX_LEFT
    DEFPUSHBUTTON   "OK", IDOK, 65, 164, 50, 14, 0, WS_EX_LEFT
    GROUPBOX        "Adapter Details (Сведения об адаптере)", 0, 7, 7, 276, 48, 0, WS_EX_LEFT
}

Resource.rc - т.н. файл (шаблон) ресурсов. Здесь мы определяем расположение диалоговых окон, элементов управления и других ресурсов, а также их размеры и множество других параметров. Обрати внимание, что здесь активно применяются константы только тех ресурсов, которые определённые в Resource.h (Проект Engine).

Закрыть
noteНевероятно, но факт!

В конце листинга Resource.h обязательно оставь пустую строку, перенеся каретку ввода на следующую строку. В противном случае при компиляции MS VC++ выдаст ошибку! Природа подобных "Фэн-шуй наклонностей" никому неизвестна.

Закрыть
noteОбрати внимание

Если захочешь поэксперементировать с "ручным" редактированием файлов Resourse.rc и Resource.h, помни, что наш исходный код оперирует элементами управления и их уникальными именами, представленными в них (в частности, он почти всегда обращается к свойству ID элемента). При неосторожном редактировании шаблона ресурса всё приложение может рухнуть и неожиданно завершить свою работу при попытке кода приложения обратиться к элементу управления, который ты изменил или удалил.

Что-либо редактировать в них сейчас не нужно. Добавим оба файла в соответствующие Проекты нашего Решения GameProject01.

  • Скопируй файлы Resource.rc и Resource.h в каталог Проекта Engine.

(В нашем случае он расположен здесь: C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\GameProject01\GameProject01 .)

  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Существующий элемент...
  • В появившемся окне выбери файл Resource.h.
  • Жмём "Добавить".

Добавленный файл Resource.h появится в списке заголовочных файлов Проекта Engine.

  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы ресурсов" Проекта Test.
  • Во всплывающем меню Добавить->Существующий элемент...
  • В появившемся окне выбери файл Resource.rc.

Напомним, что он находится в соседнем каталоге Проекта Engine, расположенном по следующему пути: C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\GameProject01\GameProject01 .

  • Жмём "Добавить".

Добавленный файл Resource.h появится в списке файлов ресурсов Проекта Test.

  • Сохрани Решение (Файл->Сохранить все).

Открой Resource.h в окне редактирования MS Visual C++ 2010, дважды щёлкнув по нему. В нём содержатся несколько выражений #define, которые определяют уникальные имена каждого диалога (у нас он всего один) и элемента управления для быстрого доступа к ним во время выполнения приложения.
Ещё раз напомним, что файл шаблона ресурсов (.rc) должен быть доступен исполняемому файлу приложения (т.е. находится с ним в одном Проекте). В данный момент у нас 2 Проекта: Engine и Test. Проект Engine не является исполняемым файлом (т.к. это библиотека .lib), поэтому ему не нужен файл шаблона ресурсов.
Проект Test - напротив, является исполняемым файлом и имено в него необходимо вносить файл шаблона ресурсов (.rc).
Заголовочный файл Resource.h должен быть доступен любому Проекту, который будет использовать ресурсы, описанные в нём, поэтому он включён в Проект Engine. Для подключения добавленного заголовка Resource.h:

  • Укажи соответствующую директиву #include "Resource.h" в самом начале файла Engine.h:
Фрагмент файла Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "Resource.h"
#include "LinkedList.h"
#include "ResourceManagement.h"
#include "Geometry.h"
#include "Scripting.h"
#include "Input.h"
#include "State.h"
...

Что касается шаблона ресурсов Resource.h, то тут всё ещё проще. Он не требует указания каких либо ссылок на него в исходном коде. Достаточно просто включить его в Проект с исполняемым файлом, что мы только что проделали.

Закрыть
noteОбрати внимание

"Физически" файлы Resource.h и Rersource.rc должны располагаться в одном каталоге на жёстком диске (в нашем случае это будет каталог Проекта Engine), т.к. resource.h ссылается на элементы шаблона ресурсов, расположенного в том же каталоге. В противном случае придётся править пути в resource.h

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

Использование диалогового окна графических настроек

Дела в гору! У нас есть готовое диалоговое окно графических настроек, интегрированное в наш движок и у нас почти готова процедура перечисления режимов графического адаптера. Осталось ещё два шага до того, как мы увидим систему энумерации графического адаптера в деле.

В самом конце функции Enumerate мы вызвали функцию DialogBox для отображения диалогового окна графических настроек адаптера. Поэтому далее необходимо получить и нужным обработать полученную от пользователя информацию (которую он указал в диалоговом окне), чтобы настроить приложение на работу соответствующим образом.
Сразу после этого мы интегрируем всю систему энумерации (опроса) адаптера дисплея в движок.
Но перед этим давай немного разберёмся, как диалоговое окно получает информацию от пользователя.
Нам уже известно, что диалог представляет собой одну из разновидностей окна Windows, и, как ты помнишь, окна Windows получают сообщения. Диалоговые окна - не исключение и тоже получают сообщения (собственно, для этого диалоги и нужны). Всякий раз, когда пользователь взаимодействует с каким-либо элементом диалогового окна (ставит галочки, выбирает строку в комбо-боксе, вводит данные в строку редактирования и т.д.), генерируется т.н. событие (event) и в это самое диалоговое окно немедленно посылается сообщение, информирующее о наступлении определённого события. Некоторые события относятся к самому диалоговому окну (активация, сворачивание, закрытие диалога пользователем и др.). Для обработки всех этих событий, происходящих в реальном времени в момент выполнения диалога, диалоговому окну необходимо назначить процедуру обратного вызова (call-back procedure). Точно так же мы делали в Главе 1.2 при создании главного окна приложения. Более того, принцип работы процедуры обратного вызова диалога практически ничем не отличается от процедуры обратного вызова обычного окна Windows. Поэтому мы не будем подробно разбирать её код, а вместо этого рассмотрим несколько наиболее важных событий, обработку которых производит функция обратного вызова диалогового окна графических настроек.
Называется она SettingsDialogProc. Она назначается конкретному диалоговому окну во время вызова функции DialogBox. В то же время, при вызове DialogBox мы указываем в качестве параметра совсем другую функцию обратного вызова SettingsDialogProcDirector:

Фрагмент файла Engine.cpp (Проект Engine)
...
	return DialogBox( NULL, MAKEINTRESOURCE( IDD_GRAPHICS_SETTINGS ), NULL, SettingsDialogProcDirector );
...

Исходя из названия, SettingsDialogProcDirector является своего рода директором (в смысле "направителем", "указателем"), который, в свою очередь, вызывает функцию обратного вызова SettingsDialogProc из класса DeviceEnumeration, применяя глобальный указатель g_deviceEnumeration. Это связано с тем, что мы не можем использовать функцию SettingsDialogProc в качестве параметра напрямую, так как она относится к классу который сначала должен быть инстанциирован (на его базе должен быть создан экземпляр).

Оказавшись внутри функции SettingsDialogProc, мы используем выражение switch...case для определения и последующей обработки поступившего сообщения.

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

Фрагмент файла Engine.cpp (Проект Engine)
...
		case WM_INITDIALOG:
		{
			// Отображаем информацию об адаптере и версию его драйвера.
			char version[16];
			sprintf( version, "%d", LOWORD( m_adapter.DriverVersion.LowPart ) );
			Edit_SetText( GetDlgItem( dialog, IDC_DISPLAY_ADAPTER ), m_adapter.Description );
			Edit_SetText( GetDlgItem( dialog, IDC_DRIVER_VERSION ), version );

			// Проверяем, есть ли в скрипте графических настроек что-либо из этого.
			if( m_settingsScript->GetBoolData( "windowed" ) == NULL )
			{
				// Скрипт графических настроек пуст, поэтому по умолчанию запуск в оконном режиме.
				CheckDlgButton( dialog, IDC_WINDOWED, m_windowed = true );
			}
			else
			{
				// Загружаем стейт оконного режима.
				CheckDlgButton( dialog, IDC_WINDOWED, m_windowed = *m_settingsScript->GetBoolData( "windowed" ) );
				CheckDlgButton( dialog, IDC_FULLSCREEN, !m_windowed );

				// Проверяем тот случай, когда выбран полноэкранный режим (fullscreen).
				if( m_windowed == false )
				{
					// Включаем (делаем доступными) все элементы управления, относящиеся к полноэкранному режиму.
					EnableWindow( GetDlgItem( dialog, IDC_VSYNC ), true );
					EnableWindow( GetDlgItem( dialog, IDC_COLOUR_DEPTH), true );
					EnableWindow( GetDlgItem( dialog, IDC_RESOLUTION ), true );
					EnableWindow( GetDlgItem( dialog, IDC_REFRESH_RATE ), true );

					// Загружаем состояние флага vsync.
					CheckDlgButton( dialog, IDC_VSYNC, m_vsync = *m_settingsScript->GetBoolData( "vsync" ) );

					// Заполняем комбо-бокс форматов дисплея.
					ComboBox_ResetContent( GetDlgItem( dialog, IDC_COLOUR_DEPTH ) );
					m_displayModes->Iterate( true );
					while( m_displayModes->Iterate() )
						if( !ComboBoxContainsText( dialog, IDC_COLOUR_DEPTH, m_displayModes->GetCurrent()->bpp ) )
							ComboBoxAdd( dialog, IDC_COLOUR_DEPTH, (void*)m_displayModes->GetCurrent()->mode.Format, m_displayModes->GetCurrent()->bpp );
					ComboBoxSelect( dialog, IDC_COLOUR_DEPTH, *m_settingsScript->GetNumberData( "bpp" ) );

					char text[16];

					// Заполняем комбо-бокс доступных разрешений экрана (resolutions).
					ComboBox_ResetContent( GetDlgItem( dialog, IDC_RESOLUTION ) );
					m_displayModes->Iterate( true );
					while( m_displayModes->Iterate() )
					{
						if( m_displayModes->GetCurrent()->mode.Format == (D3DFORMAT)PtrToUlong( ComboBoxSelected( dialog, IDC_COLOUR_DEPTH ) ) )
						{
							sprintf( text, "%d x %d", m_displayModes->GetCurrent()->mode.Width, m_displayModes->GetCurrent()->mode.Height );
							if (!ComboBoxContainsText( dialog, IDC_RESOLUTION, text ) )
								ComboBoxAdd( dialog, IDC_RESOLUTION, (void*)MAKELONG( m_displayModes->GetCurrent()->mode.Width, m_displayModes->GetCurrent()->mode.Height ), text );
						}
					}
					ComboBoxSelect( dialog, IDC_RESOLUTION, *m_settingsScript->GetNumberData( "resolution" ) );

					// Заполняем комбо-бокс доступных значений частоты обновления экрана (refresh rates).
					ComboBox_ResetContent( GetDlgItem( dialog, IDC_REFRESH_RATE ) );
					m_displayModes->Iterate( true );
					while( m_displayModes->Iterate() )
					{
						if( (DWORD)MAKELONG( m_displayModes->GetCurrent()->mode.Width, m_displayModes->GetCurrent()->mode.Height ) == (DWORD)PtrToUlong( ComboBoxSelected( dialog, IDC_RESOLUTION ) ) )
						{
							sprintf( text, "%d Hz", m_displayModes->GetCurrent()->mode.RefreshRate );
							if (!ComboBoxContainsText( dialog, IDC_REFRESH_RATE, text ) )
								ComboBoxAdd( dialog, IDC_REFRESH_RATE, (void*)m_displayModes->GetCurrent()->mode.RefreshRate, text );
						}
					}
					ComboBoxSelect( dialog, IDC_REFRESH_RATE, *m_settingsScript->GetNumberData( "refresh" ) );
				}
			}

			return true;
		}
...

Да, здоровенный case. Windows-программирование вообще штука непростая. Данный код сильно "заточен" под наше диалоговое окно и вовсю использует его константы. Большая часть кода просто манипулирует диалоговым окном и его элементами управления. Сфокусируемся на том, как всё это влияет на весь процесс энумерации дисплея адаптера. Подробные комментарии помогут разобраться в коде. Кратко "пробежимся" по тем этапам, которые происходят в нём.
Первым делом показываем пользователю имя адаптера дисплея и версию его драйвера. Для этого применяем структуру m_adapter D3DADAPTER_IDENTIFIER, которую уже заполнили в функции Enumerate.
Второй шаг - проверить, существует ли (создан ли ранее) скрипт настроек. При первом запуске приложения скрипт настроек отсутствует, поэтому мы выставляем в диалоге оконный режим запуска приложения. При наличии валидного (= корректно составленного) скрипта настроек, мы считываем настройки из него и выставляем в соответствии с ними все элементы управления диалога. Если скрипт настроек указывает на то, что приложение должно запускаться в оконном режиме, то в этом случае количество необходимых настроек сведено к минимуму, т.к. мы можем игнорировать элементы управления для выбранного видеорежима. При наличии в настройках опции полноэкранного режима, диалог активирует элементы управления настроек режима отображения (частота обновления, глубина цветности, разрешение) и автоматически выставляет их в соответствии с установками скрипта настроек.
При активировании флажка полноэкранного режима, первым делом активируются все его элементы управления (они неактивны по умолчанию, т.к. по умолчанию выбран оконный режим запуска приложения).
Далее проверяем состояние флага v-sync. Если в скрипте настроек напротив него стоит TRUE, то автоматически отмечаем данный элемент управления галочкой. В противном случае оставляем неотмеченным.
Финальный этап - заполнить комбо-боксы для каждой из трёх настроек режима дисплея:

  • формат дисплея (color depth);
  • разрешение экрана (resolution);
  • частота обновления экрана (refresh rate).

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

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

Элемент управления Описание
IDOK Когда пользователь щёлкает по кнопке OK, нам необходимо сохранить выбранные настройки и записать их в скрипт. После этого закрываем диалоговое окно, чтобы движок смог приступить к созданию объекта устройства.
IDCANCEL Когда пользователь щёлкает по кнопке Cancel (Отмена), мы закрываем диалоговое окно и возвращаем код неудачи (failure code), тем самым давая движку понять, что пользователь вышел из приложения.
IDС_COLOUR_DEPTH Данный комбо-бокс позволяет пользователю выбрать желаемую глубину цветности (color depth), причём только для полноэкранного режима. Будучи выбранным, данный элемент управления автоматически обновляет соседний комбо-бокс выбора разрешения экрана (resolution), заполняя его только теми опциями, которые соответстуют выбранной до этго глубине цветности.
IDС_RESOLUTION Данный комбо-бокс позволяет пользователю выбрать желаемое разрешение (resolution), причём только для полноэкранного режима. Будучи выбранным, данный элемент управления автоматически обновляет соседний комбо-бокс выбора частоты обновления экрана (refresh rate), заполняя его только теми опциями, которые соответстуют выбранному до этого разрешению экрана.
IDС_WINDOWED, IDC_FULLSCREEN Эти два взаимоисключающие элементы управления позволяют пользователю выбрать запуск приложения в оконном или полноэкранном режиме. Когда выбран оконный режим, все элементы управления настроек видеорежима становятся неактивны. При выборе полноэкранного режима все элементы управления настроек видеорежима активны и доступны для выбора. При этом комбо-бокс глубины цветопередачи автоматически заполняется значениями поддерживаемых форматов дисплея.

Обработчик сообщения WM_COMMAND также представлен в виде оператора case, внутри которого также расположено несколько операторов switch...case, проверяющие наступление какого-либо события из Таблицы 4.
Рассмотрим, что происходит в исходном коде, когда пользователь щёлкает по кнопке OK:

Фрагмент файла Engine.cpp (Проект Engine)
...
		case WM_COMMAND:
		{
			switch( LOWORD(wparam) )
			{
				case IDOK:
				{
					// Сохраняем детальные настройки выбранного режима дисплея.
					m_selectedDisplayMode.Width = LOWORD( PtrToUlong( ComboBoxSelected( dialog, IDC_RESOLUTION ) ) );
					m_selectedDisplayMode.Height = HIWORD( PtrToUlong( ComboBoxSelected( dialog, IDC_RESOLUTION ) ) );
					m_selectedDisplayMode.RefreshRate = PtrToUlong( ComboBoxSelected( dialog, IDC_REFRESH_RATE ) );
					m_selectedDisplayMode.Format = (D3DFORMAT)PtrToUlong( ComboBoxSelected( dialog, IDC_COLOUR_DEPTH ) );
					m_windowed = IsDlgButtonChecked( dialog, IDC_WINDOWED ) ? true : false;
					m_vsync = IsDlgButtonChecked( dialog, IDC_VSYNC ) ? true : false;

					// Уничтожаем связный список режимов дисплея. (Да, он больше не нужен.)
					SAFE_DELETE( m_displayModes );

					// Получаем выбранное значение из каждого комбо-бокса.
					long bpp = ComboBox_GetCurSel( GetDlgItem( dialog, IDC_COLOUR_DEPTH ) );
					long resolution = ComboBox_GetCurSel( GetDlgItem( dialog, IDC_RESOLUTION ) );
					long refresh = ComboBox_GetCurSel( GetDlgItem( dialog, IDC_REFRESH_RATE ) );

					// Проверяем, есть ли в скрипте графических настроек какие-либо из этих параметров.
					if( m_settingsScript->GetBoolData( "windowed" ) == NULL )
					{
						// Добавляем все настройки в скрипт.
						m_settingsScript->AddVariable( "windowed", VARIABLE_BOOL, &m_windowed );
						m_settingsScript->AddVariable( "vsync", VARIABLE_BOOL, &m_vsync );
						m_settingsScript->AddVariable( "bpp", VARIABLE_NUMBER, &bpp );
						m_settingsScript->AddVariable( "resolution", VARIABLE_NUMBER, &resolution );
						m_settingsScript->AddVariable( "refresh", VARIABLE_NUMBER, &refresh );
					}
					else
					{
						// Устанавливаем новые параметры + их значения.
						m_settingsScript->SetVariable( "windowed", &m_windowed );
						m_settingsScript->SetVariable( "vsync", &m_vsync );
						m_settingsScript->SetVariable( "bpp", &bpp );
						m_settingsScript->SetVariable( "resolution", &resolution );
						m_settingsScript->SetVariable( "refresh", &refresh );
					}

					// Сохраняем все настройки в файл скрипта графических настроек.
					m_settingsScript->SaveScript();

					// Уничтожаем скрипт графических настроек.
					SAFE_DELETE( m_settingsScript );

					// Закрываем диалоговое окно графических настроек.
					EndDialog( dialog, IDOK );

					return true;
				}
...

Сперва выбранный режим адаптера сохраняется в структуре m_selectedDisplayMode DisplayMode. После этого уничтожаем связный список режимов дисплея, так как он больше не нужен.
Далее сохраняем индекс выбранного пункта в каждом из трёх комбо-боксов. Это необходимо для последующей записи выбранных значений в скрипт настроек, чтобы при следующем запуске приложения все настройки автоматически считывались из него, устанавливая соответствующие элементы управления диалога в нужном положении. Если скрипт настроек пуст (обычно так бывает при первом запуске приложения), мы добавляем текущие настройки в него. Если скрипт настроек не пуст, то просто изменяем в скрипте имеющиеся переменные. В итоге мы даём команду на сохранение скрипта (все опреации до этого проводились в оперативной памяти) в файл на жёстком диске, после чего удаляем скрипт из оперативной памяти. Одна из последних строк кода вызывает функцию EndDialog, которая закрывает диалоговое окно и возвращает значение (в нашем случае это IDOK), по которому позднее можно определить, какую именно кнопку нажал пользователь: OK или Cancel (Отмена).
Сообщения от других элементо управления обрабатываются точно также. Подробные комментарии помогут разобраться.

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

Интеграция системы энумерации адаптера в движок

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

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

Изменения в Engine.h (Проект Engine)

Принцип тот же, что и при интеграции других систем.

  • Добавь инструкцию #include "Engine.h" в самом начале файла DeviceEnumeration.cpp (проверь её наличие).

  • Добавь инструкцию #include "DeviceEnumeration.h" в файл Engine.h, сразу после инструкции #include "Scripting.h":
Фрагмент файла Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Includes
//-----------------------------------------------------------------------------
#include "Resource.h"
#include "LinkedList.h"
#include "ResourceManagement.h"
#include "Geometry.h"
#include "Scripting.h"
#include "DeviceEnumeration.h"
#include "Input.h"
#include "State.h"
...


Следующие переменные - scale (масштаб) и totalBackBuffers (число бэкбуферов объекта адаптера), в общем-то, никак не относятся к системе энумерации адаптера. Теме не менее, они также очень важны для дальнейшей настройки движка:

  • Добавь следующие строки в секции инициализации переменных структуры EngineSetup, сразу перед строкой void (*StateSetup)();:

float scale;
unsigned char totalBackBuffers;

  • Добавь следующие строки в конструкторе структуры EngineSetup, сразу перед строкой StateSetup = NULL; (т.к. функция StateSetup должна инициализироваться в последнюю очередь):

scale = 1.0f;
totalBackBuffers = 1;

Фрагмент файла Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Setup Structure
//-----------------------------------------------------------------------------
struct EngineSetup
{
	HINSTANCE instance; // Дескриптор экземпляра приложения.
	char *name; // Название приложения.
	float scale; // Масштаб (scale) в метрах/футах.
	unsigned char totalBackBuffers; // Число используемых бэкбуферов.
	void (*StateSetup)(); // Функция подготовки стейта.

	//-------------------------------------------------------------------------
	// The engine setup structure constructor.
	//-------------------------------------------------------------------------
	EngineSetup()
	{
		instance = NULL;
		name = "Application";
		scale = 1.0f;
		totalBackBuffers = 1;
		StateSetup = NULL;
	}
};
...

Здесь добавляются две новые записи - float scale и unsigned char totalBackBuffers, которым затем, в конструкторе структуры EngineSetup, присваиваются значения по умолчанию - 1.0 и 1 соответственно.
Чтобы разобраться, для чего они нужны, снова "погрузимся" в теорию 3D-графики.

Scale
Когда работаешь с 3D-графикой, всё вокруг представлено в условных единицах измерения, называемых юнитами (units). Юнит является абстрактной единицей измерения, которая может быть привязана к единицам измерения из реального мира (например, метры и футы). К примеру, у нас есть в 3D-пространстве отрезок длиной 4 юнита. В реальном мире это ничего не значит, так как никто не в курсе, какую длину имеет 1 юнит. Программные пакеты для работы с 3D-графикой в большинстве своём имеют возможность приравнивать (выставлять в настройках) этот условный юнит к реальным мерам длины (мм, см, м, км, дюйм, фут, миля и т.д.). Данное свойство называется масштабирование (scale) или установка масштаба. При создании игрового движка также необходимо установить масштаб единиц измерения, с которым он будет работать. Это также связано с тем, что ряду компонентов (в том числе из DirectX) необходимо знать, какой длины 1 юнит. Одним из таких компонентов является DirectSound. Он использует масштаб при работе с т.н. 3D-звуком.
3D-звук может быть размещён в любом месте 3D-пространства. Суть технологии заключается в том, что такой звук может быть услышан (или не услышан) в зависимости от расстояния до воображаемого 3D-микрофона (забегая вперёд, заметим, что это будет объект игрока со своими виртуальными "ушами"), который также размещён в том же 3D-пространстве. Для того, чтобы DirectSound мог рассчитать, насколько громко должен звучать звук, он должен вычислить расстояние между источником звука и воображаемым микрофоном. К слову, технология не нова и ты видел её блестящую реализацию ещё в первом Half-life, впервые увидевшем свет ещё в апреле 1998 г.
DirectSound работает с расчётом на то, сколько метров помещается в одном юните. Раз уж именно такой принцип работы заложен в DirectSound по умолчанию (вообще, это один из основных компонентов, больше остальных использующий в своей работе масштаб), мы также вводим переменную масштаба и присваиваем ей определённое значение по тому же самому принципу. Это означает, что свойство scale в структуре EngineSetup представляет собой количество метров в одном юните. В нашем случае ему присвоено значение 1.0 (1 юнит = 1 метр). Это соответствует установкам DirectSound по умолчанию. Ты можешь изменять это значение в исходном коде, чтобы твоё 3D-окружение могло иметь любой масштаб по твоему желанию. К примеру, тебе понадобилось сделать так, чтобы 1 юнит был равен 2 метрам (для эффективного уменьшения размера всего сразу наполовину). Для этого достаточно установить: scale=0.5f . И наоборот, для увеличения всего 3D-окружения в 2 раза достаточно установить: scale=2.0f (при таком значении 1 юнит = 0,5 метра). Наконец, ты можешь приравнять юнит к любым другим реальным единицам измерения. Для этого достаточно вычислить процентное соотношение выбранной единицы измерения (например, фут) к метру (то есть сколько футов в одном метре). Это отношение и будет новым масштабом. В нашем случае мы знаем, что в 1 метре 100 сантиметров, а 1 фут равен прибл. 30,48 см. Всё что нужно сделать - это просто разделить 30,48 на 100 и в результате получим значение масштаба равное 0,3048. Если указать это число в качестве значения свойства scale, то в этом случае твоё 3D-окружение будет оперировать футами в качестве единицы измерения.

Рис. 11 Рендеринг в бэкбуфер невидим для пользователя до тех пор, пока не произойдёт флип
Рис. 11 Рендеринг в бэкбуфер невидим для пользователя до тех пор, пока не произойдёт флип


totalBackBuffers
Для вывода изображения на экран Direct3D использует так называемые бэкбуферы (от англ. "back buffer" - задний буфер, закадровый буфер) - специальные области в памяти, где готовится очередой кадр изображения. Обычно есть два буфера:

  • передний (front buffer)
  • задний (back buffer; иногда их бывает несколько).

Передний буфер - это кадр который ты видишь на экране в определенный момент времени. Всё выглядит так, как будто изображение напрямую выводится на экран. Однако графический конвейер DirectX рендерит изображение не сразу на экран, а в задний буфер (back buffer) - внеэкранную текстуру, поверхность для рисования, размещаемую в памяти компьютера (её размеры всегда равны размеру экрана монитора или разрешению экрана), на которой происходят все операции по прорисовке кадра. (Та же технология применяется и в OpenGL.) Именно поэтому бэкбуфер часто назвают "целью рендеринга по умолчанию" графического конвейера (default render target; в последних версиях DirectX render target может указывать не на бэкбуфер). Когда все команды рендеринга данного кадра в бэкбуфере закончились, вызывается специальная команда Flip (от англ. "перевернуть", быстро поменять местами; в последних версиях DirectX для этого используют функцию Present), которая просто копирует содержимое заднего буфера в передний. И всё то, что рисовали в задний буфер, моментально оказывается в переднем буфере, то есть на экране (см. Рис. 11). Содержимое экрана - наоборот, помещается в бэкбуфер. (Впрочем, далеко не всегда.) Таким образом DirectX формирует изображение не напрямую на экран, а "рисует" в специальную текстуру (расположенную в бэкбуфере, в памяти), а затем эта текстура в полностью готовом виде выводится на экран. И так со скоростью 30-60 раз в секунду! Это сделано для того чтобы движущаяся картинка на экране всегда оставалась плавной, чтобы зритель (игрок) не видел как на экране строится сцена. Ввиду такой колоссальной скорости в большинстве видеокарт работа с бэкбуферами изображения реализована на аппаратном уровне, что, безусловно, увеличивает быстродействие и качество выводимой картинки.

Закрыть
noteОбрати внимание

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

Рис. 12 Использование бэкбуферов для вывода сцены на экран
Рис. 12 Использование бэкбуферов для вывода сцены на экран

Так вот, чаще всего полноэкранные приложения используют 2 бэкбуфера (т.н. двойная буферизация) - 1 передний и 1 задний буфер. Оконное приложение (даже игра под Direct3D!) имеет в своём распоряжении всего 1 бэкбуфер (т.е. изображение рендерится напрямую в область видимости). При двойной буферизации в то время как Direc3D рендерит текущий кадр в один из бэкбуферов (с точки зрения Direct3D они вообще не подразделяются на передний и задний), другой бэкбуфер (который содержит последний кадр, "отрисованный" на нём) показывается на экране. Как только Direct3D закончит рендеринг во внеэкранный бэкбуфер, он быстро меняет бэкбуферы местами, выводя на экран только что отрендеренный кадр (см. Рис. 12). Этот процесс продолжается на протяжении всей жизни приложения (по крайней мере, пока в рендеринге участвует Direct3D). Если приложение работает на скорости 40 fps, то это означает, что Direct3D рендерит, показывает и меняет местами бэкбуферы 40 раз в секунду, что даёт на экране чёткое и плавное изображение в движении.
Существует несколько различных способов использования бэкбуферов для вывода сцены на экран, но мы не будем сейчас подробно рассматривать этот процесс. В данный момент нас интересует лишь способ изменения количества используемых бэк-буферов. Когда ты создаёшь оконное приложение, Direct3D игнорирует любые указанные значения данного параметра, так как в оконном режиме он может использовать всего один бэкбуфер. И напротив, при выборе полноэкранного режима ты можешь использовать столько бэкбуферов, сколько пожелаешь. Наиболее часто указывают число бэк-буферов равное:

  • 2 (т.н. "двойная буферизация", "double buffering");
  • или 3 (т.н. "тройная буферизация", "triple buffering").

Чем больше бэкбуферов ты используешь, тем (теоретически) более сглаженными выглядят переходы между кадрами, что даёт более плавную анимацию. В то же время, в большинстве случаев, когда число бэкбуферов больше двух, разница в качестве анимации часто просто неуловима для человеческого глаза.
Указание слишком большого числа бэкбуферов влечёт за собой повышенный расход памяти. Рассмотрим пример. При запуске приложения в полноэкранном режиме в разрешении 1280х1024 и глубиной цветности 32 бита, то в этом случае 1 бэк-буфер займёт 1280 х 1024 х 32 = 41 943 040 бит памяти. Так как в одном байте 8 бит, получившийся результат соответствует 5 242 880 байт, или 5,2 мегабайта. При использовании 3-х таких бэкбуферов потребуется почти 16 Мб памяти видеокарты (бэкбуферы хранятся именно там). А ведь ещё нам потребуется загрузить в видеопамять текстуры, полигональные сетки (меши), данные вершин и многое другое. Таким образом, как видишь, если система располагает достаточным объёмом видеопамяти, ты можешь без труда использовать 3 и более бэкбуферов. Как вариант, можно снизить разрешение и/или глубину цветности, что уменьшит количество требуемой памяти.

Закрыть
noteОбрати внимание

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

  • Добавь следующие строки в секцию public класса Engine, сразу после объявления функции SetDeactiveFlag:

float GetScale();
IDirect3DDevice9 *GetDevice();
D3DDISPLAYMODE *GetDisplayMode();
ID3DXSprite *GetSprite();

Здесь мы объявляем 4 дополнительных служебных функции, которые нам понадобятся в дальнейшей работе. Вот их описание:

Функция Описание
float GetScale() Возвращает текущее значение масштаба, которым оперирует движок. Если не изменяли, то обычно возвращается значение, указанное до этого в структуре EngineSetup.
IDirect3DDevice9 *GetDevice() Возвращает указатель на объект устройства Direct3D, который вскоре будет создан.
D3DDISPLAYMODE *GetDisplayMode() Возвращает указатель на структуру D3DDISPLAYMODE, содержащую подробное описание текущих настроек экрана. Это те самые настройки, которые пользователь указал в меню графических настроек при энумерации адаптера дисплея.
ID3DXSprite *GetSprite() Возвращает указатель на интерфейс ID3DXSprite, который мы создадим сразу после того, как будет создан объект устройства. Спрайт - это обычное 2D-изображение, которое может быть отрисовано на экране (в частности в 3D-сцене) в качестве обычной картинки. Так как Direct3D работает с объектами в 3D-пространстве, интерфейс ID3DXSprite (который теперь расположен в библиотеке D3DX) разработан для упрощения всего процесса вывода спрайтов на экран.

Уже через несколько абзацев мы добавим в Engine.cpp их реализации.

  • Добавь следующие строки в секцию private класса Engine, сразу после строки EngineSetup *m_setup;:

IDirect3DDevice9 *m_device;
D3DDISPLAYMODE m_displayMode;
ID3DXSprite *m_sprite;
unsigned char m_currentBackBuffer;

Фрагмент файла Engine.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Engine Class
//-----------------------------------------------------------------------------
class Engine
{
public:
	Engine( EngineSetup *setup = NULL );
	virtual ~Engine();

	void Run();

	HWND GetWindow();
	void SetDeactiveFlag( bool deactive );

	float GetScale();
	IDirect3DDevice9 *GetDevice();
	D3DDISPLAYMODE *GetDisplayMode();
	ID3DXSprite *GetSprite();

	void AddState( State *state, bool change = true );
	void RemoveState( State *state );
	void ChangeState( unsigned long id );
	State *GetCurrentState();

	ResourceManager< Script > *GetScriptManager();

	Input *GetInput();

private:
	bool m_loaded; // Флаг показывает, загружен ли движок или нет.
	HWND m_window; // Дескриптор главного окна.
	bool m_deactive; // Флаг показывает, активно приложение или нет.

	EngineSetup *m_setup; // Экземпляр (инстанс) структуры EngineSetup.
	IDirect3DDevice9 *m_device; // Интерфейс устройства Direct3D.
	D3DDISPLAYMODE m_displayMode; // Режим дисплея текущего устройства Direct3D.
	ID3DXSprite *m_sprite; // Интерфейс спрайта для рендеринга спрайтов (двухмерных текстурированных примитивов).
	unsigned char m_currentBackBuffer; // Следит за тем, какой бэкбуфер находится спереди своп-цепочки.

	LinkedList< State > *m_states; // Связный список (Linked list) стейтов.
	State *m_currentState; // Указатель на текущий стейт.
	bool m_stateChanged; // Флаг показывает, изменён ли стейт в текущем кадре.

	ResourceManager< Script > *m_scriptManager; // Менеджер скриптов.

	Input *m_input; // Input object.
};
...


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

Переменная Описание
IDirect3DDevice9 *m_device Сохраняет указатель на наш объект устройства, который вскоре будет создан.
D3DDISPLAYMODE m_displayMode Структура, которая сохраняет выбранный режим дисплея, с которым и будет работать видеокарта.
ID3DXSprite *m_sprite Является инстансом интерфейса ID3DXSprite, который создаётся сразу после создания объекта устройства.
unsigned char m_currentBackBuffer Переменная m_currentBackBuffer используется для отслеживания того, какой из буферов стоит впереди всей цепочки в каждый момент времени. Другими словами, всякий раз, когда цепочка буферов сдвигается, переменная m_currentBackBuffer увеличивается на единицу до тех пор, пока её значение не превысит общее число бэкбуферов. В этом случае m_CurrentBackBuffers сбрасывается на значение 1, что является началом цепочки. И весь процесс повторяется вновь.

Рис. 13 Swap chain (цепочка перелистывыния) бэкбуферов в деле
Рис. 13 Swap chain (цепочка перелистывыния) бэкбуферов в деле

Бэкбуферы выводятся на экран в порядке зацикленной очереди (весь процесс называется swap chain - от англ. "быстро сменяемая цепочка, очередь"). Когда ты командуешь Direct3D вывести очередной кадр на экран, вся цепочка бэкбуферов смещается на одну позицию (просиходит т.н. flip или flipping - от англ. "щелчок, схлопывание"). Таким образом первый бэкбуфер становится последним, а второй бэкбуфер становится первым (см. Рис.13).
Переменная m_currentBackBuffer используется для отслеживания, какой из буферов занял место экранного буфера в данный момент времени. Другими словами, каждый раз, когда своп-цепочка смещается (происходит флип), значение переменной m_currentBackBuffer увеличивается на единицу до тех пор, пока не превысит значение общего количества бэкбуферов (та самая переменная m_totalBackbuffers, которую мы указываем в окне графических настроек). Как только это произойдёт, переменная m_currentBackBuffer вновь принимает значение 1.
О необходимости этой переменной мы крато говорили. Но всё сразу станет понятно, как только мы начнём делать игру во второй части данного курса. Наша будущая игра в качестве главного меню будет использовать диалоговое окно (похожее на окно графических настроек). Оба этих диалога не рендерятся Direct3D; вместо этого они прорисовываются средствами GDI (Graphical Device Interface), который является частью Windows. Проблема в том, что GDI может выводить изображение только в первый буфер. Это означает, что если своп-цепочка смещается по кругу и фронтальным становится любой буфер, отличный от первого, диалог просто не будет виден на экране. Именно для избежания подобной ситуации нам необходимо отслеживать, какой из буферов своп-цепочки является в данный момент фронтальным. В этом случае мы просто тупо проматываем всю своп-цепочку до тех пор, пока на месте фронтального буфера не окажется первый буфер.

Изменения в Engine.cpp (Проект Engine)

  • Добавь следующие строки в конструктор класса Engine, сразу после вызова функции CoInitializeEx:

IDirect3D9 *d3d = Direct3DCreate9( D3D_SDK_VERSION );
g_deviceEnumeration = new DeviceEnumeration;
if( g_deviceEnumeration->Enumerate( d3d ) != IDOK )
{
SAFE_RELEASE( d3d );
return;
}
После внесённых изменений конструктор класса Engine выглядит так:

Фрагмент файла Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Конструктор класса Engine.
//-----------------------------------------------------------------------------
Engine::Engine( EngineSetup *setup )
{
	// Указываем, что движок ещё не загружен.
	m_loaded = false;

	// Если никакой структуры EngineSetup не передаётся, обязательно создадим её.
	// Иначе, копируем имя передаваемой структуры.
	m_setup = new EngineSetup;
	if( setup != NULL )
		memcpy( m_setup, setup, sizeof( EngineSetup ) );

	// Сохраняем указатель движка в глобальной переменной для более простого и быстрого доступа.
	g_engine = this;

	// Подготавливаем оконный класс и регистрируем его.
	WNDCLASSEX wcex;
	wcex.cbSize        = sizeof( WNDCLASSEX );
	wcex.style         = CS_CLASSDC;
	wcex.lpfnWndProc   = WindowProc;
	wcex.cbClsExtra    = 0;
	wcex.cbWndExtra    = 0;
	wcex.hInstance     = m_setup->instance;
	wcex.hIcon         = LoadIcon( NULL, IDI_APPLICATION );
	wcex.hCursor       = LoadCursor( NULL, IDC_ARROW );
	wcex.hbrBackground = NULL;
	wcex.lpszMenuName  = NULL;
	wcex.lpszClassName = "WindowClass";
	wcex.hIconSm       = LoadIcon( NULL, IDI_APPLICATION );
	RegisterClassEx( &wcex );

	// Инициализируем COM, используя многопоточное взаимодействие.
	CoInitializeEx( NULL, COINIT_MULTITHREADED );

	// Инициализируем интерфейс Direct3D.
	IDirect3D9 *d3d = Direct3DCreate9( D3D_SDK_VERSION );

	// Энумерируем (опрашиваем) конфигурации устройства Direct3D на адаптере по умолчанию (первичный видеодрайвер).
	g_deviceEnumeration = new DeviceEnumeration;
	if( g_deviceEnumeration->Enumerate( d3d ) != IDOK )
	{
		SAFE_RELEASE( d3d );
		return;
	}
...

Строка
IDirect3D9 *d3d = Direct3DCreate9( D3D_SDK_VERSION );
создаёт объект устройства Direct3D (который необходим для энумерации адаптера дисплея) с использованием функции Direct3DCreate9. Функция принимает всего один параметр, который всегда должен иметь значение D3D_SDK_VERSION. Он является идентификатором, который DirectX использует для того, чтобы убедиться, что приложение будет построено с использованием корректных DirectX-заголовков (они могут изменяться от версии к версии).
В следующих строках создаётся экземпляр класса DeviceEnumeration. Объявляется он со своей собственной глобальной переменной g_deviceEnumeration, которую мы должны использовать в качестве имени экземпляра. Это делается для того, чтобы процедура обратного вызова диалогового окна также получила доступ к этому экземпляру. Если помнишь, процедура обратного вызова не может быть вызвана из самого класса диалогового окна, поэтому мы получаем доступ к ней именно через этот глобальный указатель (глобальным он объявлен в самом начале DeviceEnumeration.cpp), чтобы получить к ней доступ из любого участка кода.
Как только экземпляр класса DeviceEnumeration был создан, вызываем функцию Enumerate, передавая ей в качестве параметра указатель на наш только что созданный объект Direct3D. Вызов функции Enumerate происходит в условии оператора if, который проверяет возвращаемое значение. Если функция Enumerate возвращает любое значение, кроме IDOK, значит пользователь не нажал кнопку OK диалогового окна (например при нажатии кнопки Cancel функция возвращает IDCANCEL). В этом случае мы не хотим продолжать создание объекта устройства и дальнейшую загрузку движка. Поэтому мы уничтожаем объект устройства Direct3D и выходим из конструктора класса Engine. Это пприведёт к тому, что флаг m_loaded, расположенный в самом конце конструктора класса Engine, не будет установлен в TRUE, т.к. оператор return будет последней строкой в нём. Поэтому когда приложение вызовет функцию Run, флаг m_loaded покажет, что движок не был загружен и она немедленно прекратит своё выполнение, вызвав деструктор класса Engine и таким образом осуществив выход из приложения.
В противном случае (когда функция Enumerate всё-таки возвратит IDOK) мы можем продолжить создавать объект устройства. И происходит это, главным образом, при создании главного окна программы. Код функции CreateWindow мы изменим следующим образом:

  • Измени функцию CreateWindowEx в конструкторе класса Engine следующим образом:
Фрагмент файла Engine.cpp (Проект Engine)
...
	// Создаём окно и возвращаем его дескриптор.
	m_window = CreateWindowEx( WS_EX_TOPMOST, "WindowClass", m_setup->name, g_deviceEnumeration->IsWindowed() ? WS_OVERLAPPED : WS_POPUP, 0, 0, 800, 600, NULL, NULL, m_setup->instance, NULL );
...




До этого четвёртый параметр функции CreateWindowEx (стиль окна) был установлен в WS_OVERLAPPED (обычное окно с тайтлбаром и бордером). Однако при запуске приложения в полноэкранном режиме данный параметр должен принимать значение WS_POPUP (окно-заставка, открывающееся на весь экран). Pop-up-окно не имеет элементов, присущих обычным окнам. Для pop-up-окна они вообще не нужны, так как всё время остаются невидимыми и не используются.
Мы знаем, что одной из опций диалогового окна графических настроек является возможность выбора запуска приложения в оконном или полноэкранном режимах. К моменту создания окна мы уже запросили настройки от пользователя. Поэтому в третьем параметре функции CreateWindow мы запрашиваем у класса DeviceEnumeration (через его глобальный указатель g_deviceEnumeration) состояние флага IsWindowed для того, чтобы решить какой стиль окна применить в том или ином случае. Мы делаем это путём вызова одноимённой функции IsWindowed и применения условного (тернарного) оператора (conditional operator) в виде символа вопросительного знака (?). Данный оператор подставляет на место третьего параметра значение стиля окна, соответствующий тому или иному значению, возвращаемому функцией IsWindowed. Условный оператор "?" ближе всего по смыслу выражению if...else. он проверяет выражение (стоящее слева от символа знака вопроса) на значение TRUE/FALSE (истина/ложь). Если выражение соответствует значению TRUE, то на место третьего параметра подставляется значение, стоящее слева от символа знака двоеточия ":" (в нашем случае - WS_OVERLAPPED). В противном случае - подставляется значение справа от символа знака двоеточия ":" (в нашем случае - WS_POPUP).

  • Добавь реализации функций GetScale, GetDevice, GetDisplayMode, GetSprite, объявленных ранее, в Engine.cpp, сразу после реализации функции SetDeactiveFlag:

Фрагмент файла Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Возвращает значения масштаба, с которым движок работает в данный момент.
//-----------------------------------------------------------------------------
float Engine::GetScale()
{
	return m_setup->scale;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на объект устройства Direct3D.
//-----------------------------------------------------------------------------
IDirect3DDevice9 *Engine::GetDevice()
{
	return m_device;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на режим дисплея текущего объекта устройства Direct3D.
//-----------------------------------------------------------------------------
D3DDISPLAYMODE *Engine::GetDisplayMode()
{
	return &m_displayMode;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на интерфейс спрайта.
//-----------------------------------------------------------------------------
ID3DXSprite *Engine::GetSprite()
{
	return m_sprite;
}
...


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

Фрагмент файла Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// Возвращает дескриптор текущего окна.
//-----------------------------------------------------------------------------
HWND Engine::GetWindow()
{
	return m_window;
}

//-----------------------------------------------------------------------------
// Устанавливает флаг неактивности.
//-----------------------------------------------------------------------------
void Engine::SetDeactiveFlag( bool deactive )
{
	m_deactive = deactive;
}

//-----------------------------------------------------------------------------
// Возвращает значения масштаба, с которым движок работает в данный момент.
//-----------------------------------------------------------------------------
float Engine::GetScale()
{
	return m_setup->scale;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на объект устройства Direct3D.
//-----------------------------------------------------------------------------
IDirect3DDevice9 *Engine::GetDevice()
{
	return m_device;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на режим дисплея текущего объекта устройства Direct3D.
//-----------------------------------------------------------------------------
D3DDISPLAYMODE *Engine::GetDisplayMode()
{
	return &m_displayMode;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на интерфейс спрайта.
//-----------------------------------------------------------------------------
ID3DXSprite *Engine::GetSprite()
{
	return m_sprite;
}

//-----------------------------------------------------------------------------
// Добавляет стейт в движок.
//-----------------------------------------------------------------------------
void Engine::AddState( State *state, bool change )
{
	m_states->Add( state );

	if( change == false )
		return;

	if( m_currentState != NULL )
		m_currentState->Close();

	m_currentState = m_states->GetLast();
	m_currentState->Load();
}

//-----------------------------------------------------------------------------
// Удаляет стейт из движка.
//-----------------------------------------------------------------------------
void Engine::RemoveState( State *state )
{
	m_states->Remove( &state );
}

//-----------------------------------------------------------------------------
// Сменить текущий стейт на стейт с указанным ID.
//-----------------------------------------------------------------------------
void Engine::ChangeState( unsigned long id )
{
	// Итерируем через список стейтов и находим новый стейт, на который надо сменить.
	m_states->Iterate( true );
	while( m_states->Iterate() != NULL )
	{
		if( m_states->GetCurrent()->GetID() == id )
		{
			// Закрываем предыдущий стейт.
			if( m_currentState != NULL )
				m_currentState->Close();

			// Устанавливаем идущий следом стейт в качестве текущего и загружаем его.
			m_currentState = m_states->GetCurrent();
			m_currentState->Load();

			// Сменяем бэк-буферы до тех пор, пока первый бэк-буфер не станет фронтальным
			while( m_currentBackBuffer != 0 )
			{
				m_device->Present( NULL, NULL, NULL, NULL );

				if( ++m_currentBackBuffer == m_setup->totalBackBuffers + 1 )
					m_currentBackBuffer = 0;
			}

			// Указываем флаг, что стейт был изменён в данном кадре.
			m_stateChanged = true;

			break;
		}
	}
}

//-----------------------------------------------------------------------------
// Возвращает указатель на текущий стейт.
//-----------------------------------------------------------------------------
State *Engine::GetCurrentState()
{
	return m_currentState;
}

//-----------------------------------------------------------------------------
// Возвращает указатель на объект input.
//-----------------------------------------------------------------------------
Input *Engine::GetInput()
{
	return m_input;
}

===
Продолжение см. здесь: Программируем 3D FPS. 1.10 Добавляем рендеринг продолж


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Программируем 3D-шутер от первого лица (FPS) (Win32, Cpp, DirectX9)  »  Часть 1. Создание движка  »  1.10 Добавляем рендеринг

Contributors to this page: slymentat .
Последнее изменение страницы Среда 30 / Ноябрь, 2016 20:40:48 MSK автор slymentat.

Последние комментарии

No records to display

Хостинг