Загрузка...
 
Печать
Кодим 3D FPS DX9

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


Содержание



Intro

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

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

Direct3D - один из основных компонентов DirectX. До появления 8 версии за рендеринг отвечали целых два компонента:
  • 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-ускорителя с максимальным быстродействием.
HAL:
  • имеет низкоуровневый доступ к 3D-чипу и реализует 3D-функции на аппаратном или программно-аппаратном уровне;
  • представляет собой специфичный для данного оборудования (в нашем случае это видеокарта) интерфейс, предоставляемый производителем видеокарты.

Image
Рис.1 Взаимосвязь между приложением, Direct3D и драйвером дисплея

Image
Рис.2 Различия между 2-мя типами Картезианской системы координат

Image
Рис.3

Image
Рис.4

Image
Рис.5 Пример затенения по Гуро


Каждый HAL отличается друг от друга, так как разработан специально для одной конкретной модели видеокарты (далее - адаптер дисплея) и предоставляет доступ только к тому функционалу, который она поддерживает. Direct3D, в свою очередь, "общается" напрямую с HAL и посредством приложений, которые используют эту связку. В результате получаем приложения, которые в полной мере используют возможности видеокарты, выдавая очень быстрый и качественный 3D-рендеринг в реальном времени, при этом не сильно напрягая процессор ПК (см. Рис.1).
Исходя из названия, Direct3D производит рендеринг 3D-объектов. Все эти объекты имеют свою позицию в 3D-пространстве. Для определения местоположения любого 3D-объекта используют набор координат - х, у, z. Такая система координат называется Картезианской и подразделяется на 2 типа:
  • направление оси z определяется по правилу правой руки (праворучная, right-handed);
  • направление оси z определяется по правилу левой руки (леворучная, left-handed).
Direct 3D для представления объектов в 3D-nространстве использует леворучную Картезианскую систему координат.
В обоих видах данной коорд. системы координаты х и у вычисляются точно также, как это делается в "плоской" двухмерной системе координат (положительные координаты по оси х откладываются в правую сторону от нулевой координаты, по оси у - вверх). Когда эта двухмерная система переходит в 3D, добавляется третье измерение, которое измеряется по оси z. В леворучной координатной системе ось z направлена в сторону, противоположную наблюдателю, а в праворучной - направлена на него (См. Рис.2).
Для рендеринга объектов в этом 3D-пространстве Direct3D использует примитивы. В Главе 1.5 мы рассматривали различную 3D-геометрию, включая вершины, рёбра и грани. Примитивы представляют собой всего лишь трёхмерные геометрические формы, состоящие из точек в пространстве, называемые вершинами. Вершины соединены друг с другом рёбрами и образуют грани. Простейшим примитивом является вершина, размещённая в 3D-пространстве. В нашем Проекте наиболее часто используемым примитивом будет треугольник (он же полигон, он же грань). Грань (в Direct3D) состоит из 3-х вершин, которые соединены друг с другом и образуют поверхность треугольной формы. Сложные меши (от англ. polygon mesh - "полигональная сетка") создаются путём объединения нескольких граней. В Direct3D абсолютно все трехмерные объекты, начиная от куба и заканчивая моделями автомобиля и человека, состоят из простых треугольных граней (См. Рис.3; подробнее по данной теме читай статью Базовые понятия 3D-графики). Когда к этим граням применяют текстуру, Direct3D может рендерить их как единый объект.
Если помнишь, в Главе 1.5 мы отмечали, что каждая грань имеет свою так называемую "нормаль". Нормаль грани:
  • представляет собой вектор, проведённый перпендикулярно плоскости грани в направлении, противоположном её фронтальной стороне;
  • широко применяются в Direct3D для определения, видна ли данная грань или нет, а также для расчёта освещённости объектов;
  • указывает, какая из сторон грани является фронтальной (т.е. наружной и, соответственно, видимой).
Если нормаль грани направлена от вьюера (он же вьюпорт или виртуальная камера, которая "показывает" 3D-мир) (т.е. в сторону, противоположную ему), то в этом случае Direct3D определяет такую грань как невидимую и потому просто не рендерит её. Эта простейшая технология оптимизации выводимой 3D-графики называется отсечением обратной стороны(external link) (backface culling) (см. Рис.6).
Image
Рис.6 Нормали граней используются в Direct3D для отсечения невидимых граней

Нормаль грани вычисляется исходя из того, в каком порядке указаны вершины, составляющие грань. Так как Direct3D использует леворучную Картезианскую систему координат, то для корректного вычисления нормали координаты грани следует указывать по часовой стрелке.
Свои нормали есть даже у вершин. Их так и называют нормали вершин. Они используются при расчёте уровня освещённости граней. В самом простом случае Direct3D вычисляет количество света, которое падает на поверхность (состоящую из граней), взяв за основу угол между вектором направления источника света и нормалью вершины (см. Рис.5).
Direct3D также поддерживает применение различных видов затенения (shading) граней. На Рис.4 представлены грани, которые освещены с применением т.н. "плоского" затенения (flat shading). Это означает, что каждая грань затенена одним цветом и в одной заранее рассчитанной степенью интенсивности.
Другой интересный метод затенения - тонирование по Гуро (Gouraud shading), где сначала вычисляются цвет и интенсивность закрашивания каждой вершины, а затем эти значения интерполируются через поверхность грани, что значительно улучшает качество выводимой картинки (см. Рис. 5). Данный метод тонирования мы будем использовать по умолчанию во всех последующих Проектах данного курса.
Другим интересным свойством Direct3D является возможность текстурирования примитивов. Здесь на передний план вновь выходят нормали граней, так как Direct3D применяет текстуры только к фронтальным ( = видимым) поверхностям граней. Для корректного размещения текстуры на поверхности грани (и даже на поверхности нескольких граней) Direct3D использует координаты текстуры (texture coordinates). Каждая вершина текстурируемой грани должна иметь координаты текстуры. Текстуры делятся на так называемые текселы (от англ. texels, texture element, элемент текстуры, пиксель текстуры), которые представляют собой индивидуальные значения цвета текстуры. Текселы образуют сетку текстуры, где каждой колонке соответствует определённая координата по оси и, а строке - координата по оси v. Таким образом каждый тексел текстуры имеет т.н. UV-координаты. В 3DS Мах есть даже такой модификатор UVW-mар(external link), применяемый для наложения текстур на 3D-объекты. Координаты текстуры указывают, от какой вершины грани она будет брать начало. Другими словами, ты указываешь UV-координаты, a Direct3D наносит текстуру на вершину, начиная с этих UV-координат (см. Рис.7).
Image
Рис.7 Наложение текстуры на грань с использованием 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 является аппаратная обработка всей графики, что большинство видеоадаптеров проделывают очень быстро, выдают безумно красивую ЗО-прорисовку и при этом не нагружают процессор.
  • Устройство программной эмуляции HEL (Hardware Emulation Layer, Reference device).
Поддерживает абсолютно все функции, заложенные Direct3D, но их обработка целиком "ложится на плечи" центрального процессора, который зачастую производит её намного медленнее. Как результат - очень низкий показатель FPS (кол-во кадров всекунду) и относительно скудная 3D-картинка. Применяется лишь в том случае, когда адаптер дисплея не поддерживает необходимые функции рендеринга (например, видеокарта без встроенного 3D-ускорителя).
Закрыть
noteСовет

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

При создании нового устройства Direct3D ты можешь устанавливать различные параметры, которые контролируют его дальнейшее поведение. Основными параметрами являются:
  • глубина цвета;
  • разрешение экрана;
  • частота обновления экрана.
Помимо этого, Direct3D-устройство поддерживает множество других продвинутых настроек для всего на свете, начиная от стенсил-буферов(external link) и заканчивая антиалиасингом (antialiacing, сглаживание). Но, так как разные HAL имеют разные возможности (в зависимости от физического адаптера дисплея), наборы изменяемых параметров у них также будут отличаться. Валидные (=корректные) комбинации этих параметров называют режимами дисплея (display modes). По сути объект устройства Direct3D состоит из объекта адаптера дисплея, на котором оно запускается, и режима дисплея (проще говоря, настроек), выбранным для него.
В связи с тем, что на базе одного адаптера дисплея можно создать бесчисленное множество комбинаций Direct3D-устройств, часто бывает необходимо произвести т.н. энумерацию (от англ. enumeration - перечисление) адаптера дисплея. Это означает, что мы запросим у адаптера дисплея сведения обо всех поддерживаемых комбинациях параметров, с которыми будет создано устройство Direct3D.
Создадим класс, который производит энумерацию адаптера дисплея (первичное устройство), а затем показывает диалоговое окно, в котором пользователь может произвести необходимые настройки, на основе которых будет создан объект устройства Direct3D.
Сейчас в Проекте Engine нашего движка всего 11 файлов: Engine.h, Engine.срр, LinkedList.h, ResourceManagement.h, Geometry.h, State.h, State.cpp, Input.h, Input.cpp, Scripting.h и Scripting.срр, которые мы создали в предыдущих главах. Чуть ниже есть второй Проект Test. Его пока не трогаем!

Добавляем DeviceEnumeration.h (Проект Engine)

В заголовочном файле DeviceEnumeration.h внутри класса DeviceEnumeration представлены объявления функций для настройки режима адаптера дисплея и выбора других его настроек.
ОК, приступаем.
  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент... (Add->New Item...)
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "DeviceEnumeration.h".
Image
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле DeviceEnumeration.h набираем следующий код:
DeviceEnumeration.h
//-----------------------------------------------------------------------------
// File: DeviceEnumeration.h
// Used for enumerating Direct3D devices. A dialog is provided to allow the
// user to select a supported device configuration.
// Объявления классов и функций, энумерирующих устройства Direct3D.
// Есть даже ресурс диалоговое окно, выводящее окно графических настроек.
//
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#ifndef DEVICE_ENUMERATION_H
#define DEVICE_ENUMERATION_H

//-----------------------------------------------------------------------------
// Display Mode Structure
//-----------------------------------------------------------------------------
struct DisplayMode
{
	D3DDISPLAYMODE mode; // Direct3D display mode.
	char bpp[6]; // Colour depth expressed as a character string for display.
				// Режим цветности, представленный как строка символов
				// (для наглядного представления игроку в меню настроек).
};

//-----------------------------------------------------------------------------
// Device Enumeration Class
//-----------------------------------------------------------------------------
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; // Script which stores the device configuration.
			// Скрипт, куда сохраним настройки устройства.

	D3DADAPTER_IDENTIFIER9 m_adapter; // Direct3D adapter identifier.
				// Direct3D-идентификатор адаптера
	LinkedList< DisplayMode > *m_displayModes; // Linked list of enumerated display modes.
				// Связный список энумерированных видеорежимов.
	D3DDISPLAYMODE m_selectedDisplayMode; // User selected display mode.
				// Видеорежим, выбранный пользователем.
	bool m_windowed; // Indicates if the application should run in windowed mode.
			// Флаг того, будет ли приложение запущено в оконном режиме.
	bool m_vsync; // Inidicates if v-sync should be enabled.
			// Флаг показывает, включена ли вертикальная синхронизация кадров.
};

//-----------------------------------------------------------------------------
// Externals
//-----------------------------------------------------------------------------
extern DeviceEnumeration *g_deviceEnumeration;

#endif

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

Исследуем DeviceEnumeration.h (Проект Engine)


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

В самом начале DeviceEnumeration.h представлена маленькая структура DisplayMode, которая хранит подробные настройки одного режима дисплея, который используется для создания объекта устройства Direct3D. Если не заметил, эта структура является т.н. "обёрткой" (от англ. wrapper) вокруг стандартной Direct3D-структуры D3DDISPLAYM0DE. Причиной такого подхода является тот факт, что 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 bрр" (16 bits per pixel - 16 бит на 1 пиксел), a D3DFMT_A8R8G8B8 - как "32 bрр".
Вообще прототип (описание) структуры D3DDISPLAYMODE выглядит так:
Структура D3DDISPLAYMODE
typedef struct _D3DDISPLAYMODE
{
	UINT Width;		// Ширина экрана, в пикселях.
	HINT 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, созданного на базе другого адаптера дисплея. Вдобавок ко всему, физический адаптер дисплея поддерживает далеко не все комбинации режимов дисплея. Например одни адаптеры не поддерживают разрешение 1920x1080, а другие могут не поддерживать частоту обновления больше 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, реализация которого (в файле DeviceEnumeration.cpp, который мы создадим уже через пару абзацев) выглядит очень громоздко и путано. Но как и в остальных случаях, когда начнёшь "играть" с ним, изменяя различные параметры и видя произведённый эффект, ты сразу увидишь, как всё это работает. Это один из лучших способов научиться программировать. Когда видишь код, который не понимаешь, просто начни изменять его, доверившись своей интуиции. Затем ты перепишешь его по своему усмотрению и изменишь/удалишь вещи, которые тебе не нравятся. Проделывая эти простые действия, ты вскоре обнаружишь, что стал мастером в данной теме.
Прежде чем ты бросишься что-либо изменять, рассмотрим объявление класса DeviceEnumeration (в файле DeviceEnumeration.h):
Фрагмент DeviceEnumeration.h (Проект Engine)
...
//-----------------------------------------------------------------------------
// Device Enumeration Class
//-----------------------------------------------------------------------------
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; // Script which stores the device configuration.
			// Скрипт, куда сохраним настройки устройства.

	D3DADAPTER_IDENTIFIER9 m_adapter; // Direct3D adapter identifier.
				// Direct3D-идентификатор адаптера
	LinkedList< DisplayMode > *m_displayModes; // Linked list of enumerated display modes.
				// Связный список энумерированных видеорежимов.
	D3DDISPLAYMODE m_selectedDisplayMode; // User selected display mode.
				// Видеорежим, выбранный пользователем.
	bool m_windowed; // Indicates if the application should run in windowed mode.
			// Флаг того, будет ли приложение запущено в оконном режиме.
	bool m_vsync; // Inidicates if v-sync should be enabled.
			// Флаг показывает, включена ли вертикальная синхронизация кадров.
};
...

Выглядит громоздко. Поэтому будем разбирать его по частям.
Во-первых, у данного класса отсутствует конструктор и деструктор. А всё потому, что данный класс является вспомогательным (т.н. "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.
#ifdef _WIN32
  LARGE_INTEGER DriverVersion;
  // Идентифицирует версию компонентов 16-битного драйвера.
  // Не применяется в приложениях Win32.
#else
  DWORD         DriverVersionLowPart;
  DWORD         DriverVersionHighPart;
#endif

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

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

Добавляем DeviceEnumeration.срр (Проект Engine)

В файле исходного кода DeviceEnumeration.срр будут размещаться реализации функций, объявленных в DeviceEnumeration.h.
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Файлы исходного кода" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент...
  • В появившемся окне выбери "Файл С++ (.cpp)" и в поле "Имя" введи "DeviceEnumeration.срр".
  • Жмём "Добавить".
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле DeviceEnumeration.срр набираем следующий код:
DeviceEnumeration.срр (Проект Engine)
//-----------------------------------------------------------------------------
// File: DeviceEnumeration.cpp
// DeviceEnumeration.h implementation.
// Refer to the DeviceEnumeration.h interface for more details.
// Реализации функций, объявленных в DeviceEnumeration.h .
//
// Programming a Multiplayer First Person Shooter in DirectX
// Copyright (c) 2004 Vaughan Young
//-----------------------------------------------------------------------------
#include "Engine.h"

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

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

//-----------------------------------------------------------------------------
// Enumerates available Direct3D devices on the default adapter.
// Энумерирует доступные устройства Direct3D на видеоадаптере по умолчанию.
//-----------------------------------------------------------------------------
INT_PTR DeviceEnumeration::Enumerate( IDirect3D9 *d3d )
{
	// Create the display modes linked list.
	// Создаём связный список видеорежимов.
	m_displayModes = new LinkedList< DisplayMode >;

	// Load the settings script.
	// Загружаем скрипт.
	m_settingsScript = new Script( "DisplaySettings.txt" );

	// Get the details of the default adapter.
	// Получаем инфу от адаптера по умолчанию.
	d3d->GetAdapterIdentifier( D3DADAPTER_DEFAULT, 0, &m_adapter );

	// Build a list of the allowable pixel formats.
	// Строим список допустимых (для нас) форматов цветности.
	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;

	// Go through the list of allowable pixel formats.
	// Проходим через связный список допустимых форматов цветности.
	for( char af = 0; af < 6; af++ )
	{
		// Get the number of adapter modes and go through them.
		// Получаем кол-во доступных видеорежимов и входим в каждый из них.
		unsigned long totalAdapterModes = d3d->GetAdapterModeCount( D3DADAPTER_DEFAULT, allowedFormats[af] );
		for( unsigned long m = 0; m < totalAdapterModes; m++ )
		{
			// Get the display mode details.
			// Получаем допинфу по видеорежиму.
			D3DDISPLAYMODE mode;
			d3d->EnumAdapterModes( D3DADAPTER_DEFAULT, allowedFormats[af], m, &mode );

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

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

			// Add this display mode to the list.
			// Добавляем видеорежим в связный список.
			m_displayModes->Add( displayMode );
		}
	}

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

//-----------------------------------------------------------------------------
// Handles window messages for the graphics settings dialog.
// Обрабатываем оконные сообщения диалогового окна графических настроек.
//-----------------------------------------------------------------------------
INT_PTR DeviceEnumeration::SettingsDialogProc( HWND dialog, UINT msg, WPARAM wparam, LPARAM lparam )
{
	switch( msg )
	{
		case WM_INITDIALOG:
		{
			// Display the adapter details and its driver version.
			// Показываем инфу по видеоадаптеру и текущую версию его драйвера.
			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 );

			// Check if the settings script has anything in it.
			// Проверяем, есть ли в скрипте данные по следующим настройкам...
			if( m_settingsScript->GetBoolData( "windowed" ) == NULL )
			{
				// The settings script is empty, so default to windowed mode.
				// Скрипт настроек пуст. Поэтому по умолчанию стартуем игру в
				// оконном режиме.
				CheckDlgButton( dialog, IDC_WINDOWED, m_windowed = true );
			}
			else
			{
				// Load the window mode state.
				// Загружаем стейт оконного режима.
				CheckDlgButton( dialog, IDC_WINDOWED, m_windowed = *m_settingsScript->GetBoolData( "windowed" ) );
				CheckDlgButton( dialog, IDC_FULLSCREEN, !m_windowed );

				// Check if running in fullscreen mode.
				// Проверяем случай, когда выбран полноэкранный режим.
				if( m_windowed == false )
				{
					// Enable all the fullscreen controls.
					// Включаем все штуки полноэкранного режима.
					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 );

					// Load the vsync state.
					// Включаем vsync.
					CheckDlgButton( dialog, IDC_VSYNC, m_vsync = *m_settingsScript->GetBoolData( "vsync" ) );

					// Fill in the display formats combo box.
					// Заполняем комбо-бокс диалогового окна доступными форматами цветности.
					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];

					// Fill in the resolutions combo box.
					// Заполняем комбо-бокс диалогового окна доступными разрешениями.
					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" ) );

					// Fill in the refresh rates combo box.
					// Заполняем комбо-бокс диалогового окна доступными значениями частоты обновления.
					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:
				{
					// Store the details of the selected display mode.
					// Сохраняем настройки для выбранного видеорежима.
					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;

					// Destroy the display modes list.
					// Уничтожаем связный список видеорежимов.
					SAFE_DELETE( m_displayModes );

					// Get the selected index from each combo box.
					// Получаем значения, выбранные в каждом комбо-боксе.
					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 ) );

					// Check if the settings script has anything in it.
					// Проверяем, есть ли в скрипте настроек все нужные данные.
					if( m_settingsScript->GetBoolData( "windowed" ) == NULL )
					{
						// Add all the settings to the script.
						// Добавляем все настройки в скрипт настроек.
						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
					{
						// Set all the settings.
						// Если в скрипте уже есть данные, просто выставляем у каждого параметра
						// новое значение.
						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 );
					}

					// Save all the settings out to the settings script.
					// Сохраняем все настройки в скрипте настроек.
					m_settingsScript->SaveScript();

					// Destroy the settings script.
					// Уничтожаем объект скрипта настроек.
					SAFE_DELETE( m_settingsScript );

					// Close the dialog.
					// Закрываем диалоговое окно.
					EndDialog( dialog, IDOK );

					return true;
				}

				case IDCANCEL:
				{
					// Destroy the display modes list.
					// Уничтожаем связный список видеорежимов.
					SAFE_DELETE( m_displayModes );

					// Destroy the settings script.
					// Уничтожаем объект скрипта настроек.
					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 ) );

						// Update the resolution combo box.
						// Обновляем в комбо-боксе список доступных разрешений экрана.
						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 ) );

						// Update the refresh rate combo box.
						// Обновляем комбо-бокс выбора частоты обновления экрана.
						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:
				{
					// Check if the user has change to windowed or fullscreen mode.
					// Проверяем, какой режим игры выбрал игрок: оконный или полноэкранный.
					if( IsDlgButtonChecked( dialog, IDC_WINDOWED ) )
					{
						// Clear and disable all the fullscreen controls.
						// Очищаем и выключаем все фишки полноэкранного режима.
						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
					{
						// Enable all the fullscreen controls.
						// Включаем все фишки полноэкранного режима.
						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 );

						// Fill in the display formats combo box.
						// Заполняем комбо-бокс доступными форматами цветности.
						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;
}

//-----------------------------------------------------------------------------
// Returns the selected display mode.
// Возвращает выбранный видеорежим.
//-----------------------------------------------------------------------------
D3DDISPLAYMODE *DeviceEnumeration::GetSelectedDisplayMode()
{
	return &m_selectedDisplayMode;
}

//-----------------------------------------------------------------------------
// Indicates if the display is windowed or fulscreen.
// Показывает, в каком режиме запущена игра: в оконном или полноэкранном.
//-----------------------------------------------------------------------------
bool DeviceEnumeration::IsWindowed()
{
	return m_windowed;
}

//-----------------------------------------------------------------------------
// Indicates if the display is v-synced.
// Показывает, включен ли v-sync.
//-----------------------------------------------------------------------------
bool DeviceEnumeration::IsVSynced()
{
	return m_vsync;
}

//-----------------------------------------------------------------------------
// Adds an entry to the combo box.
// Добавляет запись в комбо-бокс диалогового окна.
//-----------------------------------------------------------------------------
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 );
}

//-----------------------------------------------------------------------------
// Selects an entry in the combo box by index.
// Выбирает запись в комбо-боксе по индексу.
//-----------------------------------------------------------------------------
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 );
}

//-----------------------------------------------------------------------------
// Selects an entry in the combo box by data.
// Выбирает запись в комбо-боксе по данным.
//-----------------------------------------------------------------------------
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;
		}
	}
}

//-----------------------------------------------------------------------------
// Returns the data for the selected entry in the combo box.
// Возвращает данные выбранной в комбо-боксе записи.
//-----------------------------------------------------------------------------
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 );
}

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

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

//-----------------------------------------------------------------------------
// Checks if the combo box contains the given text.
// Проверяет, сожержит ли комбо-бокс искомый текст.
//-----------------------------------------------------------------------------
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;
}

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

Исследуем код DeviceEnumeration.cpp


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

Функция Enumerate применяется для процесса энумерации (перечисления) адаптеров дисплея:
Фрагмент DeviceEnumeration.cpp
...
//-----------------------------------------------------------------------------
// Enumerates available Direct3D devices on the default adapter.
// Энумерирует доступные устройства Direct3D на видеоадаптере по умолчанию.
//-----------------------------------------------------------------------------
INT_PTR DeviceEnumeration::Enumerate( IDirect3D9 *d3d )
{
	// Create the display modes linked list.
	// Создаём связный список видеорежимов.
	m_displayModes = new LinkedList< DisplayMode >;

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

	// Get the details of the default adapter.
	// Получаем инфу от адаптера по умолчанию.
	d3d->GetAdapterIdentifier( D3DADAPTER_DEFAULT, 0, &m_adapter );

	// Build a list of the allowable pixel formats.
	// Строим список допустимых (для нас) форматов цветности.
	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;

	// Go through the list of allowable pixel formats.
	// Проходим через связный список допустимых форматов цветности.
	for( char af = 0; af < 6; af++ )
	{
		// Get the number of adapter modes and go through them.
		// Получаем кол-во доступных видеорежимов и входим в каждый из них.
		unsigned long totalAdapterModes = d3d->GetAdapterModeCount( D3DADAPTER_DEFAULT, allowedFormats[af] );
		for( unsigned long m = 0; m < totalAdapterModes; m++ )
		{
			// Get the display mode details.
			// Получаем допинфу по видеорежиму.
			D3DDISPLAYMODE mode;
			d3d->EnumAdapterModes( D3DADAPTER_DEFAULT, allowedFormats[af], m, &mode );

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

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

			// Add this display mode to the list.
			// Добавляем видеорежим в связный список.
			m_displayModes->Add( displayMode );
		}
	}

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

В качестве параметра она принимает указатель на объект Direct3D, который используется для получения доступа к информации об адаптере дисплея, а также для целей их энумерации. И первым делом мы подготавливаем к энумерации несколько переменных членов.
Первым создаётся связный список (Linked list) адаптеров дисплея. После этого создаём новый скрипт DisplaySettings.txt и назначаем указатель на него, который хранится в переменной m_settingsScript.
Фрагмент DeviceEnumeration.cpp
...
//-----------------------------------------------------------------------------
// Enumerates available Direct3D devices on the default adapter.
// Энумерирует доступные устройства Direct3D на видеоадаптере по умолчанию.
//-----------------------------------------------------------------------------
INT_PTR DeviceEnumeration::Enumerate( IDirect3D9 *d3d )
{
	// Create the display modes linked list.
	// Создаём связный список видеорежимов.
	m_displayModes = new LinkedList< DisplayMode >;

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

В следующей строке мы получаем доступ к информации об адаптере дисплея путём вызова функции (метода) GetAdapterIdentifier, которая экспонирована объектом Direct3D:
Фрагмент DeviceEnumeration.cpp
...
	// Get the details of the default adapter.
	// Получаем инфу от адаптера по умолчанию.
	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 *pldentifier Параметр вывода (возвращаемое значение). В нашем случае принимает значение m_adapter и является указателем на структуру D3DADAPTER_IDENTIFIER9, которую мы будем заполнять сведениями об адаптере.

Чуть ниже видим список форматов дисплея (формат цветности пиксела), для которых мы будем искать доступные видеорежимы:
Фрагмент DeviceEnumeration.cpp
...
	// Build a list of the allowable pixel formats.
	// Строим список допустимых (для нас) форматов цветности.
	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
...
	// Go through the list of allowable pixel formats.
	// Проходим через связный список допустимых форматов цветности.
	for( char af = 0; af < 6; af++ )
	{
		// Get the number of adapter modes and go through them.
		// Получаем кол-во доступных видеорежимов и входим в каждый из них.
		unsigned long totalAdapterModes = d3d->GetAdapterModeCount( D3DADAPTER_DEFAULT, allowedFormats[af] );
		for( unsigned long m = 0; m < totalAdapterModes; m++ )
		{
			// Get the display mode details.
			// Получаем допинфу по видеорежиму.
			D3DDISPLAYMODE mode;
			d3d->EnumAdapterModes( D3DADAPTER_DEFAULT, allowedFormats[af], m, &mode );

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

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

			// Add this display mode to the list.
			// Добавляем видеорежим в связный список.
			m_displayModes->Add( displayMode );
		}
	}
...

Как только мы вошли в новую итерацию первого (внешнего) цикла for, немедленно запрашиваем (у Direct3D) общее количество режимов дисплея, которые поддерживает видеоадаптер для данного формата дисплея (поочерёдно выбираемого из массива allowedFormats). Мы получаем эту информацию от функции GetAdapterModeCount, экспонируемую объектом Direct3D. В параметрах мы продолжаем указывать D3DADAPTER_DEFAULT для опроса только первичного адаптера дисплея (т.е. который по умолчанию установлен в данной ОС). Мы также передаём в функцию формат дисплея (= формат цветности пиксела), для которого хотим узнать количество доступных видеорежимов.
Сразу после этого мы входим во второй цикл for, расположенный внутри первого. В этот раз мы "просматриваем" (итерируем через цикл) каждый из видеорежимов, отобранных функцией GetAdapterModeCount. Внутри цикла мы поочерёдно запрашиваем каждый видеорежим у Direct3D и затем помещаем их во временную структуру mode (типа D3DDISPLAYMODE). К каждому видеорежиму применяем функцию EnumAdapterModes для получения доступа к детальной информации о них. Здесь тоже указываем параметр D3DADAPTER_DEFAULT для опроса только первичного адаптера дисплея (т.е. который по умолчанию установлен в данной ОС). В параметрах мы также передаём формат дисплея и индекс видеорежима для которого запрашиваем детальную информацию.
Дальше мы проводим небольшую проверку, "отсекая" видеорежимы с чересчур низким разрешением экрана. Все видеорежимы с высотой экрана менее 480 точек не подходят для наших целей. Таким образом мы указываем минимальное поддерживаемое разрешение в 640x480 пикселов. Если у тестируемого формата высота экрана меньше (а большинство видеокарт поддерживают такие форматы в 16-битном цвете), то он пропускается путём указания служебного слова continue, которое немедленно прерывает выполнение цикла и даёт команду на начало его следующей итерации. Если видеорежим прошёл тест на размер экрана по вертикали, он может рассматриваться как валидный (корректный).
Сразу после этого создаём новую структуру DisplayMode и копируем в неё данные об итерируемом видеорежиме при помощи функции memcpy.
Следующий шаг - создание небольшого текстового описания, для наглядного представления форматов дисплея пользователю ("16 bрр" и "32 bрр"). Текстовое описание хранится в массиве bрр (тип char), расположенном внутри структуры DisplayMode (см. DeviceEnumeration.h).
В конце добавляем протестированный видеорежим в связный список видеорежимов.
После этого возвращаемся в начало цикла для проверки следующего видеорежима. Как только будут проверены все видеорежимы для данного формата, управление передаётся первому (внешнему) циклу for для проверки следующего по списку формата дисплея.
По завершении обоих циклов энумерация адаптера дисплея будет полностью завершена и функция Enumerate вызывает заранее подготовленное диалоговое окно IDD_GRAPHICS_SETTINGS (создадим чуть позднее):
Фрагмент DeviceEnumeration.cpp
...
	return DialogBox( NULL, MAKEINTRESOURCE( IDD_GRAPHICS_SETTINGS ), NULL, SettingsDialogProcDirector );
}
...

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

Функция DialogBox(external link)

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

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

Создание диалогового окна графических настроек (Graphic Settings). Программа Resource Editor by Anders Melander

Статья по теме: Работа с двоичными ресурсами в MSVCpp2010 Express.
При использовании различных 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 С++ позволяет добавлять в Проекты диалоговые окна, которые затем активируются во время выполнения приложения (как в нашем примере, когда мы вызывали функцию DialogBox). При создании диалогового окна (вообще, это один из видов ресурсов) необходимо указать его имя, по которому оно будет вызываться из главного окна приложения. Взглянув на вызов функции DialogBox, размещённой в конце функции Enumerate, видим, что здесь даётся команда на загрузку диалогового окна IDD_GRAPHICS_SETTINGS, которое мы применим в нашем движке для предоставления возможности пользователю указывать графические настройки.
При создании новых диалогов ты можешь изменять их на своё усмотрение и размещать на них любые элементы управления. Всякий раз при добавлении элемента в диалоговое окно, ему (элементу) необходимо присваивать уникальное имя (ID-идентификатор), которое будет использоваться для обращения к нему (например для получения данных или его деактивации).
Диалоговые окна (как и другие ресурсы) до компиляции хранятся в специальных файлах с расширением .гс (ещё их называют "шаблонами ресурсов"), используемых MS Visual С++. При открытии шаблона ресурса в текстовом редакторе, легко убедиться, что он представляет собой обычный текстовый файл, содержащий массу информации о различных ресурсах, сохранённых в нём. Если шаблон ресурсов содержит диалоговое окно (иногда их несколько), то, помимо информации о самом диалоге, в нём также можно найти подробные сведения о каждом элементе управления, принадлежащем соответствующему диалоговому окну (например имя элемента, размеры, положение на форме и т.д.).
В платных версиях MSVC++ после добавления в Проект нового ресурса (обычно его добавляют в соответствующий фильтр "Ресурсы"), при его первом сохранении MS Visual С++ запросит указать имя шаблона ресурса. При этом IDE автоматически создаёт 2 файла:
  • Шаблон ресурсов (.гс)
  • Заголовочный файл с именем resource.h, который является своеобразным интерфейсом со ссылками на содержимое шаблона ресурсов.
Оба файла должны быть включены в Проект (или в Проекты одного Решения).
Закрыть
noteПримечание

В нашем случае шаблон ресурсов (.гс) будет размещён в Проекте Test, а его заголовок resource.h - в Проекте Engine. Это связано с тем, что файл шаблона ресурсов должен быть доступен исполняемому файлу приложения (Проект Test). Проект Engine не создаёт исполняемого файла (т.к. создаёт библиотеку DLL), поэтому ему не нужен шаблон ресурсов. Заголовочный файл resource.h должен быть доступен любому Проекту, которому необходимо использовать ресурсы. Так как наш движок во время компиляции будет получать доступ к диалоговому окну графических установок и его элементам управления, заголовок resource.h должен быть включён в Проект Engine. Но, несмотря на это, "физически" эти два файла должны располагаться в одном каталоге (в нашем случае это будет каталог Проекта Engine: C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\GameProject01\GameProject01), т.к. resource.h ссылается на элементы шаблона ресурсов, расположенного в том же каталоге. В противном случае придётся править пути в resource.h

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

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


Image
Рис.6 Программа Resource Editor by AM


Просто добавить ресурсы в Проект GameProject01 не получится. Сперва необходимо их описать в специальном скрипте ресурсов, представляющем собой обычный текстовый файл с расширением .rc . Напомним, в бесплатной MSVC++2010 Express редактор ресурсов вырезан напрочь. Поэтому для создания скрипта ресурсов воспользуемся сторонним приложением Resource Editor by Anders Melander (далее - Resource Editor by AM). Далее в этой статье будем работать только в ней. В разделе Софт нашего сайта ты найдёшь и другие редакторы двоичных ресурсов. Все они имеют во многом схожий интерфейс и поддерживают сохранение макета в файл шаблона ресусов (.rc).
Самораспаковывающийся архив (2,8 Мб) предложит распаковать Portable-версию программы в тот же каталог.
При необходимости загугли и скачай Windows SDK. Хотя, по идее, он ставится вместе с MS VC++ 2010.
  • Перейди в каталог с программой и запусти ResourceEditor.exe .
Программа на русском языке автоматически создаст рабочий Проект (см. Рис.6).
Быстро пробежимся по её интерфейсу:
ПАНЕЛЬ ИНТЕРФЕЙСА ОПИСАНИЕ
Свойства (Properties) Похож на редактор свойств в IDE от Borland и Microsoft. Активируется сразу после создания ресурса. В двухколоночной таблице в формате Свойство - Значение. Редактируем свойства активного (=выбранного в данный момент или только что созданного) ресурса/элемента управления. У разных ресурсов разные свойства. Но есть и общие: ID, Name, Language и т.д.
Ресурсы (Resources) Древовидный список ресурсов (например, открытого .exe-файла). Ресурсы группируются по типу. При клике ЛКМ по ресурсу в панели Свойства отображаются его свойства, доступные для редактирования. Ресурсы могут быть добавлены, удалены, экспортированы, сохранены.
Варианты (Variants) Показывает различные варианты ресурса. Любой ресурс может иметь только 1 ID, но несколько вариантов на разных языках, с разной глубиной цветности или размерами.
Preview Предварительный просмотр ресурса (например, курсора). Любопытно, что Resource Editor by AM умеет отображать в этой панели видеоролики и HTML-документы (если есть).
Edit Редактор ресурсов. Здесь мышью перемещаем элементы диалогового окна или просто перерисовываем курсоры/значки. Ресурсы неизвестного типа отображаются здесь в виде HEX-кода или текста. Чтобы не уходить от темы (создание скрипта ресурсов) сам встроенный редактор значков (иконок) здесь не рассматриваем. Там всё просто. При желании разберёшься сам.

Напомним, для описания двух значков нам требуются два файла:
  • Resource01.rc
  • resource.h
Первый сгенерим с помощью Resource Editor by AM, второй создадим в текстовом редакторе по образцу (об этом ниже). В resource.h прописываются т.н. ID (идентификационные номера) ресурсов.

Создаём ресурс диалоговое окно

  • В Resource Editor by AM На панели Ресурсы жмём "Добавить ресурс" -> Dialog.
Пустая форма появится в панели Edit. В ней же, справа, видим список элементов управления, доступных для нанесения на форму, сгруппированных в свитки (Standart, Common Controls, Undocumented).
  • В панели Свойства выставь Width = 290, Height = 185.

Image
Рис.7 Вид будущего окна графических настроек


Примерный вид итогового диалогового окна показан на Рис.7.
  • Отмечаем одним кликом мыши необходимый элемент (панель Edit) и "наносим" его на форму диалогового окна (установив курсор над формой и зажав левую кнопку мыши, перемещаем его слева направо, сверху вниз).
  • Размещаем и масштабируем каждый из элементов управления на форме примерно так, как показано на Рис.7.
  • С помощью инспектора свойств (Properties) устанавливаем нужные свойства каждого элемента управления, следуя инструкциям в Таблице 3.
  • Сохрани скрипт ресурса в файл Resource01.rc в каталог с Проектом Engine.
В нашем случае (Win7 x64): C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\GameProject01\GameProject01 .
В главном меню: Файл -> Сохранить как... Укажи имя Resource01. Во всплывающем списке выбери расширение .rc и жми Сохранить.
  • Открой сохранённый файл Resource01.rc в любом текстовом редакторе. Ознакомься с его содержимым.
В скрипте видим текстовое описание формы диалогового окна.
Расположение элементов управления на форме может быть любым (строго выравнивать их по линейке необязательно). Главное - чтобы пользователь мог без труда разобраться, что к чему. Для упрощения задачи на Рис.7 рядом с каждым элементом управления нанесён красно-белый кружок-маркер с цифрой. Каждая цифра соответствует номеру строки Таблицы 3, представленной ниже.
  • Сверяй номера маркеров с соответствующими строками в Таблице 3.
  • Наноси на форму элементы управления и редактируй их свойства в панели Свойства программы Resource Editor by AM в соответствии с подсказками, представленными в Таблице 3.
  • Наблюдай за изменениями в панели Preview.
Таблица 3. Свойства элементов управления диалогового окна графических настроек
№ МАРКЕРА ТИП РЕСУРСА/ЭЛ-ТА УПРАВЛЕНИЯ ОПИСАНИЕ Строка в resource.h
1 (РЕСУРС!) Dialog, Диалоговое окно (форма). ID=101; Text="Graphic Settings (Графические настройки)"; Width = 290; Height= 185. #define IDD_GRAPHICS_SETTINGS 101
2 Group BOX Text="Adapter Details (Сведения об адаптере)"; Left = 7; Top = 7; Width=276; Height=48.
3 Static Text Text="Display Adapter:"; Left=16; Top=20; Width=98; Height=8.
4 Static Text Text="Driver Version:"; Left = 16; Top=38; Width=98; Height=8.
5 Edit Control ID=40001; Left=115; Top=17; Width=159; Height=12; Control Style -> Style: es_ReadOnly=True. #define IDC_DISPLAY_ADAPTER 40001
6 Edit Control ID=40002; Left=115; Top=34; Width=159; Height=12; Control Style -> Style: es_ReadOnly=True. #define IDC_DRIVER_VERSION 40002
7 Group Box Text="Display Settings"; Left=7; Top=57; Width=276; Height=100.
8 Radio Button Text="Windowed"; ID=40003; WindowStyle->WS_Group=True; TabStop=True; Left=16; Top=73; Width=98; Height=8. #define IDC_WINDOWED 40003
9 Radio Button Text="Fullscreen"; ID=40004; Left=16; Top=89; Width=98; Height=8. #define IDC_FULLSCREEN 40004
10 CheckBox Text="V-Sync"; ID=40005; WindowStyle->WS_Disabled=True; TabStop=True; Left=115; Top=89; Width=60; Height=8. #define IDC_VSYNC 40005
11 Static Text Text="Colour Depth:"; Left=16; Top=106; Width=98; Height=8.
12 ComboBox ID=40006; WindowStyle->WS_Disabled=True; WS_Group=True; TabStop=True; VScroll=True; Control Style -> Kind=Dropdown; Left=115; Top=103; Width = 159; Height=204. #define IDC_COLOUR_DEPTH 40006
13 Static Text Text="Resolution:"; Left = 16; Top=124; Width=98; Height=8.
14 ComboBox ID=40007; WindowStyle->WS_Disabled=True; WS_Group=True; TabStop=True; VScroll=True; Control Style -> Kind=Dropdown; Left=115; Top=121; Width = 159; Height=204. #define IDC_RESOLUTION 40007
15 Static Text Text="Refresh Rate:"; Left=16; Top=140; Width=98; Height=8.
16 ComboBox ID=40008; WindowStyle->WS_Disabled=True; WS_Group=True; TabStop=True; VScroll=True; Control Style -> Kind=Dropdown; Left=115; Top=137; Width = 159; Height=204. #define IDC_REFRESH_RATE 40008
17 Button Identifier=IDOK; Text="OK"; TabStop=True; Control Style -> Kind=DefPushButton; Left=65; Top=164; Width=50; Height=14.
18 Button Identifier=IDCANCEL; Text="Cancel"; WindowStyle->WS_TabStop=True; Left=175; Top=164; Width=50; Height=14.

Свойства элементов управления, не указанные в Таблице 3, оставляем как есть. Значениями ID могут быть любые целые числа, которые должны соответствовать элементам управления, определённым (defined) в resource.h. Значениями, начинающимися с цифры 4, по стандартам Майкрософт обозначаются элементы управления диалогового окна. А ID=101 - это идентификатор самого диалогового окна.
  • Сохрани шаблон ресурсов (Ctrl+S).
  • Закрой окно Resource Editor by AM.
  • Открой Resource01.rc в любом текстовом редакторе. Просмотри его содержимое.
Закрыть
noteОбрати внимание

При создании Resource01.rc программа Resource Editor by AM автоматически определила каждый элемент управления тегом #define. Нам это не подходит, т.к. MSVC++ хочет видеть эти определения только в отдельном файле resource.h . Поэтому...

  • Найди в листинге Resource01.rc следующий блок кода:
Фрагмент Resource01.rc
...
//////////////////////////////////////////////////////////////////////////////
//
// Symbol definitions
//

#define IDC_DISPLAY_ADAPTER             0
#define IDC_DRIVER_VERSION              0
#define IDC_WINDOWED                    0
#define IDC_FULLSCREEN                  0
#define IDC_VSYNC                       0
#define IDC_COLOUR_DEPTH                0
#define IDC_RESOLUTION                  0
#define IDC_REFRESH_RATE                0
...

  • Удали его, сохранив изменения в Resource01.rc.

Добавляем resource.h (Проект Engine)

В заголовочном файле resource.h перечисляются ID-идентификаторы ресурсов, описанных в скрипте ресурсов (Resource01.rc).
  • Стартуй MSVC++ 2010 и открывай Решение GameProject01 (если не сделал это раньше).
  • В "Обозревателе решений" главного окна MSVC++2010 щёлкни правой кнопкой мыши по папке (в терминологии Майкрософт это не папки, а фильтры!) "Заголовочные файлы" Проекта Engine.
  • Во всплывающем меню Добавить->Создать элемент... (Add->New Item...)
  • В появившемся окне выбери "Заголовочный файл (.h)" и в поле "Имя" введи "resource.h" (Да, именно с маленькой буквы! Так требует MSVC++.).
Image
Добавленный файл сразу откроется в правой части MSVC++2010.
  • В только что созданном и открытом файле resource.h набираем следующий код:
resource.h
#ifndef IDC_STATIC
#define IDC_STATIC (-1)
#endif

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

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

  • Отступив пустую строку вниз, поочерёдно добавь строки кода из четвёртого столбца Таблицы 3.
После внесённых изменений resource.h будет выглядеть так:
resource.h (Проект Engine)
#ifndef IDC_STATIC
#define IDC_STATIC (-1)
#endif

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

  • Сохрани Решение (Файл->Сохранить все).
В принципе всё. Скрипт ресурсов Resource01.rc и сопутствующий заголовок resource.h готовы. Осталось добавить Resource01.rc в Проект Test и кое-что прописать в коде.

Добавляем файл Resource01.rc в Проект Test

  • Стартуй MSVC++2010 Express, если не сделал этого раньше. Открой Решение GameProject01, с которым работаем всё это время.
  • В Обозревателе решений щёлкни правой кнопкой мыши по "папке" (Напомним, что в терминологии Microsoft это не папки, а фильтры!) "Файлы ресурсов" Проекта Test.
  • Во всплывающем контекстном меню выбери Добавить->Существующий элемент.
  • В появившемся окне выбора файла выбери файл скрипта ресурсов Resource01.rc (C:\Users\<Имя пользователя>\Documents\Visual Studio 2010\Projects\GameProject01\GameProject01\Resource01.rc) и нажми "Добавить".

Изменения в Resource01.rc. Прописываем #include resource.h

  • Перейди в каталог Проекта Test и открой Resource01.rc в любом текстовом редакторе.
  • Найди следующий фрагмент:
Фрагмент Resource01.rc
...
#define APSTUDIO_READONLY_SYMBOLS
#define APSTUDIO_HIDDEN_SYMBOLS
#include "windows.h"
...

  • Строкой ниже добавь директиву
#include "resource.h"

  • Сохрани изменения в Resource01.rc .
Вот полный листинг Resource01.rc после всех внесённых изменений:
Resource01.rc
//////////////////////////////////////////////////////////////////////////////
//
// Resource Script generated by Anders Melander's Resource Editor
//
//////////////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 1 resource.
//

//
//////////////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//

#define APSTUDIO_READONLY_SYMBOLS
#define APSTUDIO_HIDDEN_SYMBOLS
#include "windows.h"
#include "resource.h"

#undef APSTUDIO_HIDDEN_SYMBOLS

#undef APSTUDIO_READONLY_SYMBOLS
//
//////////////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////////
//
// Additional include files
//



//////////////////////////////////////////////////////////////////////////////
//
// Default Language
//

LANGUAGE LANG_NEUTRAL,SUBLANG_NEUTRAL // Language neutral

//////////////////////////////////////////////////////////////////////////////
//
// DIALOGEX : Dialog
//

101 DIALOGEX 0, 0, 291, 185
  LANGUAGE LANG_NEUTRAL,SUBLANG_NEUTRAL // Language neutral
  CAPTION L"Graphic Settings (\x0413\x0440\x0430\x0444\x0438\x0447\x0435\x0441\x043A\x0438\x0435 \x043D\x0430\x0441\x0442\x0440\x043E\x0439\x043A\x0438)"
  EXSTYLE 0 // 0x00000000L
  STYLE DS_SETFONT | DS_MODALFRAME | WS_SYSMENU | WS_DLGFRAME | WS_BORDER | WS_VISIBLE | WS_POPUP // 0x90C800C0L
  FONT 8, "MS Shell Dlg", 0, 0, 1
BEGIN
  CONTROL L"Adapter Details (\x0421\x0432\x0435\x0434\x0435\x043D\x0438\x044F \x043E\x0431 \x0430\x0434\x0430\x043F\x0442\x0435\x0440\x0435)", 0, "button", BS_GROUPBOX | WS_VISIBLE | WS_CHILD, 7, 7, 276, 48
  CONTROL "Display Adapter:", 0, "static", SS_LEFT | WS_VISIBLE | WS_CHILD, 16, 20, 98, 8
  CONTROL "Driver Version:", 0, "static", SS_LEFT | WS_VISIBLE | WS_CHILD, 16, 38, 98, 8
  CONTROL "", 40001, "edit", ES_LEFT | ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP | WS_VISIBLE | WS_CHILD, 115, 17, 159, 12, WS_EX_CLIENTEDGE
  CONTROL "", 40002, "edit", ES_LEFT | ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP | WS_VISIBLE | WS_CHILD, 115, 34, 159, 12, WS_EX_CLIENTEDGE
  CONTROL "Display Settings", 0, "button", BS_GROUPBOX | WS_VISIBLE | WS_CHILD, 7, 57, 276, 100
  CONTROL "Windowed", 40003, "button", BS_AUTORADIOBUTTON | WS_TABSTOP | WS_GROUP | WS_VISIBLE | WS_CHILD, 16, 73, 98, 8
  CONTROL "Fullscreen", 40004, "button", BS_AUTORADIOBUTTON | WS_TABSTOP | WS_VISIBLE | WS_CHILD, 16, 89, 98, 8
  CONTROL "V-Sync", 40005, "button", BS_AUTOCHECKBOX | WS_TABSTOP | WS_DISABLED | WS_VISIBLE | WS_CHILD, 115, 89, 60, 8
  CONTROL "Colour Depth:", 0, "static", SS_LEFT | WS_VISIBLE | WS_CHILD, 16, 106, 98, 8
  CONTROL "", 40006, "combobox", CBS_DROPDOWN | WS_TABSTOP | WS_GROUP | WS_VSCROLL | WS_DISABLED | WS_VISIBLE | WS_CHILD, 115, 103, 159, 204, WS_EX_CLIENTEDGE
  CONTROL "Resolution:", 0, "static", SS_LEFT | WS_VISIBLE | WS_CHILD, 16, 124, 98, 8
  CONTROL "", 40007, "combobox", CBS_DROPDOWN | WS_TABSTOP | WS_GROUP | WS_VSCROLL | WS_DISABLED | WS_VISIBLE | WS_CHILD, 115, 121, 159, 204, WS_EX_CLIENTEDGE
  CONTROL "Refresh Rate:", 0, "static", SS_LEFT | WS_VISIBLE | WS_CHILD, 16, 140, 98, 8
  CONTROL "", 40008, "combobox", CBS_DROPDOWN | WS_TABSTOP | WS_GROUP | WS_VSCROLL | WS_DISABLED | WS_VISIBLE | WS_CHILD, 115, 137, 159, 204, WS_EX_CLIENTEDGE
  CONTROL "OK", 1, "button", BS_DEFPUSHBUTTON | BS_LEFT | BS_RIGHT | BS_TOP | BS_BOTTOM | WS_TABSTOP | WS_VISIBLE | WS_CHILD, 65, 164, 50, 14
  CONTROL "Cancel", 2, "button", BS_PUSHBUTTON | BS_LEFT | BS_RIGHT | BS_TOP | BS_BOTTOM | WS_TABSTOP | WS_VISIBLE | WS_CHILD, 175, 164, 50, 14
END


#ifdef APSTUDIO_INVOKED

//////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE
BEGIN
  "\0"
END

2 TEXTINCLUDE
BEGIN
  "#define APSTUDIO_HIDDEN_SYMBOLS\r\n",
  "#include ""windows.h""\r\n",
  "#undef APSTUDIO_HIDDEN_SYMBOLS\r\n"
END

3 TEXTINCLUDE
BEGIN
  "\0"
END

#endif    // APSTUDIO_INVOKED


//////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//

#ifndef APSTUDIO_INVOKED

#endif    // not APSTUDIO_INVOKED
//
//////////////////////////////////////////////////////////////////////////////


Изменения в Engine.h (Проект Engine). Прописываем #include resource.h

  • В начале листинга Engine.h найди список подключаемых заголовочных файлов (include-строки):
Фрагмент Engine.h (Проект Engine)
...
//--------------------
// System Includes
//--------------------
#include <stdio.h>
#include <tchar.h>
#include <windowsx.h>
...

  • Строкой ниже добавь строку:
#include "resource.h"

  • Сохрани Решение (Файл->Сохранить всё).
Теперь ресурс диалоговое окно доступен в приложении Test. Но по-настоящему мы его затестим только в Главе 1.11.
Resource01.гс - т.н. файл (шаблон) ресурсов. Здесь мы определяем расположение диалоговых окон, элементов управления и других ресурсов, а также их размеры и множество других параметров. Обрати внимание, что здесь активно применяются константы только тех ресурсов, которые определены в resource.h (Проект Engine).
Закрыть
noteОбрати внимание

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

Ещё раз напомним, что файл шаблона ресурсов (.rс) должен быть доступен исполняемому файлу приложения (т.е. находиться с ним в одном Проекте). В данный момент у нас 2 Проекта: Engine и Test. Проект Engine не является исполняемым файлом (т.к. это библиотека .lib), поэтому ему не нужен файл шаблона ресурсов.
Проект Test - напротив, является исполняемым файлом и имено в него необходимо вносить файл шаблона ресурсов (.гс). Заголовочный файл resource.h должен быть доступен любому Проекту, который будет использовать ресурсы, описанные в нём, поэтому он включён в Проект Engine. Что касается шаблона ресурсов Resource01.rc, то тут всё ещё проще. Он не требует указания каких либо ссылок на него в исходном коде. Достаточно просто включить его в Проект с исполняемым файлом, что мы только что проделали.
Если запутался и не понял, что и куда вставлять, в конце Главы 1.11 будет ссылка на архив с Решением, включающим данные ресурсы.

Применение диалогового окна графических настроек

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

Исходя из названия, SettingsDialogProcDirector является своего рода директором (в смысле "направителем", "указателем"), который, в свою очередь, вызывает функцию обратного вызова SettingsDialogProc из класса DeviceEnumeration, применяя глобальный указатель g_deviceEnumeration. Это связано с тем, что мы не можем использовать функцию SettingsDialogProc в качестве параметра напрямую, так как она относится к классу который сначала должен быть инстанциирован (на его базе должен быть создан экземпляр).
Оказавшись внутри функции SettingsDialogProc, мы используем выражение switch...case для определения и последующей обработки поступившего сообщения.
Первое сообщение, которое необходимо обработать, это WM_INITDIALOG, которое поступает сразу после загрузки диалогового окна. Следующий код используется для подготовки диалога и загрузки первоначальных настроек элементов управления:
Фрагмент DeviceEnumeration.cpp
...
//-----------------------------------------------------------------------------
// Handles window messages for the graphics settings dialog.
// Обрабатываем оконные сообщения диалогового окна графических настроек.
//-----------------------------------------------------------------------------
INT_PTR DeviceEnumeration::SettingsDialogProc( HWND dialog, UINT msg, WPARAM wparam, LPARAM lparam )
{
	switch( msg )
	{
		case WM_INITDIALOG:
		{
			// Display the adapter details and its driver version.
			// Показываем инфу по видеоадаптеру и текущую версию его драйвера.
			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 );

			// Check if the settings script has anything in it.
			// Проверяем, есть ли в скрипте данные по следующим настройкам...
			if( m_settingsScript->GetBoolData( "windowed" ) == NULL )
			{
				// The settings script is empty, so default to windowed mode.
				// Скрипт настроек пуст. Поэтому по умолчанию стартуем игру в
				// оконном режиме.
				CheckDlgButton( dialog, IDC_WINDOWED, m_windowed = true );
			}
			else
			{
				// Load the window mode state.
				// Загружаем стейт оконного режима.
				CheckDlgButton( dialog, IDC_WINDOWED, m_windowed = *m_settingsScript->GetBoolData( "windowed" ) );
				CheckDlgButton( dialog, IDC_FULLSCREEN, !m_windowed );

				// Check if running in fullscreen mode.
				// Проверяем случай, когда выбран полноэкранный режим.
				if( m_windowed == false )
				{
					// Enable all the fullscreen controls.
					// Включаем все штуки полноэкранного режима.
					EnableWindow( GetDlgItem( dialog, IDC_VSYNC ), true );
					EnableWindow( GetDlgItem( dialog, IDC_DISPLAY_FORMAT ), true );
					EnableWindow( GetDlgItem( dialog, IDC_RESOLUTION ), true );
					EnableWindow( GetDlgItem( dialog, IDC_REFRESH_RATE ), true );

					// Load the vsync state.
					// Включаем vsync.
					CheckDlgButton( dialog, IDC_VSYNC, m_vsync = *m_settingsScript->GetBoolData( "vsync" ) );

					// Fill in the display formats combo box.
					// Заполняем комбо-бокс диалогового окна доступными форматами цветности.
					ComboBox_ResetContent( GetDlgItem( dialog, IDC_DISPLAY_FORMAT ) );
					m_displayModes->Iterate( true );
					while( m_displayModes->Iterate() )
						if( !ComboBoxContainsText( dialog, IDC_DISPLAY_FORMAT, m_displayModes->GetCurrent()->bpp ) )
							ComboBoxAdd( dialog, IDC_DISPLAY_FORMAT, (void*)m_displayModes->GetCurrent()->mode.Format,
							m_displayModes->GetCurrent()->bpp );
					ComboBoxSelect( dialog, IDC_DISPLAY_FORMAT, *m_settingsScript->GetNumberData( "bpp" ) );

					char text[16];

					// Fill in the resolutions combo box.
					// Заполняем комбо-бокс диалогового окна доступными разрешениями.
					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" ) );

					// Fill in the refresh rates combo box.
					// Заполняем комбо-бокс диалогового окна доступными значениями частоты обновления.
					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 Когда пользователь щёлкает по кнопке ОК, нам необходимо сохранить выбранные настройки и записать их в скрипт. После этого закрываем диалоговое окно, чтобы движок смог приступить к созданию объекта устройства.
IDCANCEL Когда пользователь щёлкает по кнопке Cancel (Отмена), мы закрываем диалоговое окно и возвращаем код неудачи (failure code), тем самым давая движку понять, что пользователь вышел из приложения.
IDC_COLOUR_DEPTH Данный комбо-бокс позволяет пользователю выбрать желаемую глубину цветности (color depth), причём только для полноэкранного режима. Будучи выбранным, данный элемент управления автоматически обновляет соседний комбо-бокс выбора разрешения экрана (resolution), заполняя его только теми опциями, которые соответствуют выбранной до этго глубине цветности.
IDC_RESOLUTION Данный комбо-бокс позволяет пользователю выбрать желаемое разрешение (resolution), причём только для полноэкранного режима. Будучи выбранным, данный элемент управления автоматически обновляет соседний комбо-бокс выбора частоты обновления экрана (refresh rate), заполняя его только теми опциями, которые соответствуют выбранному до этого разрешению экрана.
IDC_WINDOWED, IDC_FULLSCREEN Эти два взаимоисключающие элементы управления позволяют пользователю выбрать запуск приложения в оконном или полноэкранном режиме. Когда выбран оконный режим, все элементы управления настроек видеорежима становятся неактивны. При выборе полноэкранного режима все элементы управления настроек видеорежима активны и доступны для выбора. При этом комбо-бокс глубины цветопередачи автоматически заполняется значениями поддерживаемых форматов дисплея.

Обработчик сообщения WM_COMMAND также представлен в виде оператора case, внутри которого также расположено несколько операторов switch...case, проверяющие наступление какого-либо события из Таблицы 4. Рассмотрим, что происходит в исходном коде, когда пользователь щёлкает по кнопке ОК:
Фрагмент DeviceEnumeration.cpp
...
		case WM_COMMAND:
		{
			switch( LOWORD(wparam) )
			{
				case IDOK:
				{
					// Store the details of the selected display mode.
					// Сохраняем настройки для выбранного видеорежима.
					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_DISPLAY_FORMAT ) );
					m_windowed = IsDlgButtonChecked( dialog, IDC_WINDOWED ) ? true : false;
					m_vsync = IsDlgButtonChecked( dialog, IDC_VSYNC ) ? true : false;

					// Destroy the display modes list.
					// Уничтожаем связный список видеорежимов.
					SAFE_DELETE( m_displayModes );

					// Get the selected index from each combo box.
					// Получаем значения, выбранные в каждом комбо-боксе.
					long bpp = ComboBox_GetCurSel( GetDlgItem( dialog, IDC_DISPLAY_FORMAT ) );
					long resolution = ComboBox_GetCurSel( GetDlgItem( dialog, IDC_RESOLUTION ) );
					long refresh = ComboBox_GetCurSel( GetDlgItem( dialog, IDC_REFRESH_RATE ) );

					// Check if the settings script has anything in it.
					// Проверяем, есть ли в скрипте настроек все нужные данные.
					if( m_settingsScript->GetBoolData( "windowed" ) == NULL )
					{
						// Add all the settings to the script.
						// Добавляем все настройки в скрипт настроек.
						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
					{
						// Set all the settings.
						// Если в скрипте уже есть данные, просто выставляем у каждого параметра
						// новое значение.
						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 );
					}

					// Save all the settings out to the settings script.
					// Сохраняем все настройки в скрипте настроек.
					m_settingsScript->SaveScript();

					// Destroy the settings script.
					// Уничтожаем объект скрипта настроек.
					SAFE_DELETE( m_settingsScript );

					// Close the dialog.
					// Закрываем диалоговое окно.
					EndDialog( dialog, IDOK );

					return true;
				}
...

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

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

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

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

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

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

  • Добавь инструкцию #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.Of;
totalBackBuffers = 1;


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

	//-------------------------------------------------------------------------
	// 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 соответственно. Чтобы разобраться, для чего они нужны, снова "погрузимся" в теорию ЗD-графики.

Понятие масштаба (scale) в 3D-графике

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

Число бэкбуферов. Переменная totalBackBuffers


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

Image
Рис.9 Применение бэкбуферов для вывода сцены на экран

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


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

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

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

Любая внеэкранная (background) поверхность, создаваемая как часть составной (complex) поверхности, называется имплицитной (implicit surface). Есть ряд действий, которые запрещено с ними проделывать. Например отсоединять (detach) или удалять от основной (primary) поверхности. Составные поверхности легко создавать средствами Direct3D (в DirectX 7 и более ранних версиях - DirectDraw), т.к. данный компонент автоматически создаёт требуемое число бэкбуферов и соединяет их с основной поверхностб. вывода (primary surface).2

Указание слишком большого числа бэкбуферов влечёт за собой повышенный расход памяти. Рассмотрим пример. При запуске приложения в полноэкранном режиме в разрешении 1280x1024 и глубиной цветности 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-изображение, которое может быть отрисовано на экране (в частности в ЗО-сцене) в качестве обычной картинки. Так как Direct3D работает с объектами в 3D-пространстве, интерфейс ID3DXSprite (который теперь расположен в библиотеке D3DX) разработан для упрощения всего процесса вывода спрайтов на экран.

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

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

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


После внесённых изменений класс Engine будет выглядеть так:
Фрагмент 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; // Indicates if the engine is loading.
					// Флаг показывает, загружен ли движок
	HWND m_window; // Main window handle.
					// Дескриптор главного окна приложения
	bool m_deactive; // Indicates if the application is active or not.
					// Флаг активности приложения

	EngineSetup *m_setup; // Copy of the engine setup structure.
					// Копия структуры EngineSetup
	IDirect3DDevice9 *m_device;
	D3DDISPLAYMODE m_displayMode;
	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;
};
...

Здесь к функциям мы также добавили в секцию private 4 новых переменных члена, некоторые из которых используются описанными выше функциями. Вот их описание:
ПЕРЕМЕННЫЙ ЧЛЕН ОПИСАНИЕ
IDirect3DDevice9 *m_device Сохраняет указатель на наш объект устройства, который вскоре будет создан.
D3DDISPLAYMODE m_displayMode Структура, которая сохраняет выбранный режим дисплея, с которым и будет работать видеокарта.
ID3DXSprite *m_sprite Является инстансом интерфейса ID3DXSprite, который создаётся сразу после создания объекта устройства.
unsigned char m_currentBackBuffer Переменная m_currentBackBuffer используется для отслеживания того, какой из буферов стоит впереди всей цепочки в каждый момент времени. Другими словами, всякий раз, когда цепочка буферов сдвигается, переменная m_currentBackBuffer увеличивается на единицу до тех пор, пока её значение не превысит общее число бэкбуферов. В этом случае m_CurrentBackBuffers сбрасывается на значение 1, что является началом цепочки. И весь процесс повторяется вновь. (См. Рис.10)

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

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


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

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

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


После внесённых изменений реализация конструктора класса Engine выглядит так:
Фрагмент Engine.cpp (Проект Engine)
...
//-----------------------------------------------------------------------------
// The engine class constructor.
//-----------------------------------------------------------------------------
Engine::Engine( EngineSetup *setup )
{
	// Indicate that the engine is not yet loaded.
	// Показываем, что движок пока не загружен.
	m_loaded = false;

	// If no setup structure was passed in, then create a default one.
	// Otehrwise, make a copy of the passed in structure.
	// Если структура EngineSetup не передана, то создаём одну.
	// Если есть, то делаем её копию.
	m_setup = new EngineSetup;
	if( setup != NULL )
		memcpy( m_setup, setup, sizeof( EngineSetup ) );

	// Store a pointer to the engine in a global variable for easy access.
	// Сохраняем указатель на экземпляр движка в глобальную переменную.
	// Для более простого доступа к ней из любой части движка
	g_engine = this;

	// Prepare and register the window class.
	// Заполняем переменные члены оконного класса.
	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 );

	// Initialise the COM using multithreaded concurrency.
	// Инициализируем 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.срр), чтобы получить к ней доступ из любого участка кода.
Как только экземпляр класса DeviceEnumeration был создан, вызываем функцию Enumerate, передавая ей в качестве параметра указатель на наш только что созданный объект Direct3D. Вызов функции Enumerate происходит в условии оператора if, который проверяет возвращаемое значение. Если функция Enumerate возвращает любое значение, кроме IDOK, значит пользователь не нажал кнопку ОК диалогового окна (например при нажатии кнопки Cancel функция возвращает IDCANCEL). В этом случае мы не хотим продолжать создание объекта устройства и дальнейшую загрузку движка. Поэтому мы уничтожаем объект устройства Direct3D и выходим из конструктора класса Engine. Это пприведёт к тому, что флаг mloaded, расположенный в самом конце конструктора класса Engine, не будет установлен в TRUE, т.к. оператор return будет последней строкой в нём. Поэтому, когда приложение вызовет функцию Run, флаг mloaded покажет, что движок не был загружен и она немедленно прекратит своё выполнение, вызвав деструктор класса Engine и таким образом осуществив выход из приложения.
В противном случае (когда функция Enumerate всё-таки возвратит IDOK) мы можем продолжить создавать объект устройства. И происходит это при создании главного окна программы. Код функции CreateWindow мы изменим следующим образом:
  • Замени вызов функции CreateWindowEx в реализации конструктора класса Engine на следующие строки:
Фрагмент Engine.cpp (Проект Engine)
...
	// Создаём окно и возвращаем его дескриптор.
	m_window = CreateWindow( "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 (окно-заставка, открывающееся на весь экран). Рор-ир-окно не имеет элементов, присущих обычным окнам. Для рор-ир-окна они вообще не нужны, так как всё время остаются невидимыми и не используются. Мы знаем, что одной из опций диалогового окна графических настроек является возможность выбора запуска приложения в оконном или полноэкранном режимах. К моменту создания окна мы уже запросили настройки от пользователя. Поэтому в третьем параметре функции CreateWindow мы запрашиваем у класса DeviceEnumeration (через его глобальный указатель g_deviceEnumeration) состояние флага IsWindowed для того, чтобы решить какой стиль окна применить в том или ином случае. Мы делаем это путём вызова одноимённой функции IsWindowed и применения условного (тернарного) оператора (conditional operator) в виде символа вопросительного знака (?). Данный оператор подставляет на место третьего параметра значение стиля окна, соответствующий тому или иному значению, возвращаемому функцией IsWindowed. Условный оператор "?" ближе всего по смыслу выражению if...else, он проверяет выражение (стоящее слева от символа знака вопроса) на значение TRUE/FALSE (истина/ложь). Если выражение соответствует значению TRUE, то на место третьего параметра подставляется значение, стоящее слева от символа знака двоеточия ":" (в нашем случае - WS_OVERLAPPED). В противном случае - подставляется значение справа от символа знака двоеточия ":" (в нашем случае - WS_POPUP).

  • Добавь реализации функций GetScale, GetDevice, GetDisplayMode, GetSprite, объявленных ранее, в конец Engine.срр:
Фрагмент 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;
}

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

Источники


1. Young V. Programming a Multiplayer FPS in DirectX 9.0. - Charles River Media, 2005
2. Calvert C. Delphi 2 Unleashed. Second ed. - SAMS Publishing, 1996


ДАЛЕЕ ==> Кодим 3D FPS DX9. 1.10 Добавляем рендеринг Ч.2

Последние изменения страницы Среда 27 / Июль, 2022 00:49:14 MSK

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

No records to display

Search Wiki Page

Точное совпадение

Категории

|--> C#
|--> C++