Введение в XNA

Усложненные технологии визуализации

Разбить на страницы
Показывать лекцию целиком

Любому разработчику периодически приходится решать множество типовых задач визуализации:

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

    3.1. Вывод на элементы управления .NET средствами XNA Framework

    До сих пор все наши приложения осуществляли монопольный вывод графической информации на поверхность формы. Такой подход не позволяет размещать на форме элементы управления .NET, что значительно ограничивает свободу разработчика. Конечно, можно попробовать использовать различные вспомогательные диалоговые окна и плавающие панели инструментов, но это не очень красивое решение проблемы, хотя и вполне работоспособноеПодобный подход применяется, к примеру, в редакторе сценариев (Activity Editor) игры Microsoft Train Simulator . Гораздо интереснее было бы научиться выводить информацию не на саму форму, а на элементы управления, размещенные на ней (к примеру, на элемент управления Panel ). Это позволило бы нам легко ограничить область вывода XNA Framework определенной областью формы, а освободившееся пространство использовать для размещения различных элементов управления Windows Forms.

    Вывод на поверхность элемента управления практически не отличается от вывода на поверхность формы, ведь в Windows Forms форма является частным случаем элементом управления. Более, форму можно поместить на другую форму или компонент в качестве элемента управленияЭтот прием используется в редакторе форм Visual Studio . Это возможно благодаря тому, что любая форма или элемент управления напрямую или косвенно наследуется от общего предка - класса Control. Как следствие, любой элемент управления Windows Forms обладает свойствами Handle, Width, Height, методами SetWidth, Show, Hide, обработчиками событий Paint, Click, Resize и так далееПолный перечень свойств и методов класса Control можно найти в MSDN .

    Примечание

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

    Но вот незадача, некоторые члены класса Control объявлены как protected. Это не создает никаких проблем при создании новой формы путем наследования от класса Form, так как в этом случае вы автоматически получаете полный доступ к защищенным (protected) членам класса. Однако при использовании готовых компонентов, помещаемых на форму средствами визуального редактора форм Visual Studio, все намного серьезнее. Например, вы не сможете получить доступ к такому важному методу как SetStyle, чтобы установить стиль ControlStyles.Opaque. А ведь без этого невозможно запретить самовольную перерисовку элемента управления, приводящую к мерцанию изображения в XNA -приложениях (раздел 1.2.3).

    Наиболее красивое решение этой проблемы - создание на базе класса Control собственного элемента управления, изменяющего статус метода SetStyle с protected на public. Для этого запустите Visual Studio и создайте новый проект библиотеки классов XnaPanel (File | New Project… | Class Library, в текстовом поле Name введите XnaPane и нажмите OK). Установите флажок Create directory for solution, так как в решении будет еще один проект, предназначенный для тестирования созданного компонента. Подключите к проекту сборку System.Windows. Forms и введите код из листинга 3.1.

    using System;
    using System.Collections.Generic; using System.Text; 
    using System.Windows. Forms;
    namespace GSP.XNA 
    {
    выделялся среди других элементов формы
    // Объявляем класс (элемент управления) XnaPanel, 
    наследуемый непосредственно public class
    XnaPanel : Control 
    { 
    // Конструктор элемента управления public XnaPanel() 
    { 
    // Изменяем цвет элемента управления, чтобы он
    1x1 пикселей
    BackColor = Color.CornflowerBlue; 
    // Размер элемента управления не должен быть меньше MinimumSize = new
    Size(1, 1);
     }
    SetStyle метода Control как public SetStyle(ControlStyles flag, 
    bool value)
    //
    //
    Переопределяем метод
    public new void
    { 
    Вызываем оригинальный метод класса Control base.SetStyle(flag, value);
    }
    }
    }

    Важно

    Для предотвращения неконтролируемого уменьшения размера компонента xnaPanel до размеров меньше одного пикселя, его свойству MinimumSize необходимо присвоить значение Size(1, 1) . Если этого не сделать, площадь области визуализации теоретически может достигнуть нуля, что приведет к генерации исключения при сбросе устройства

    После компиляции проекта Visual Studi o самостоятельно создаст в окне Components и добавит в нее полученный элемент управления (рисунок 3.1).

    Toolbox группу XnaPanel
    (рис 3.1) Панель Toolbox с компонентом XnaPanel

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

    Чтобы добавить к решению еще один проект щелкните правой кнопкой мыши на названии решения в окне Solution Explorer и выберите в контекстом меню пункт Add | New Project… (рисунок 3.3). В появившемся диалоговом окне выберите элемент Windows Application, введите в поле Name название приложения (например, Test ) и нажмите кнопку Ok. Как всегда, подключите к проекту необходимые сборки XNA Framework и добавьте необходимые директивы using.

    (рис 3.3) Внешний вид тестового приложения, использующего компонент XnaPanel (рис 3.2) Добавление к решению (Solution) нового проекта

    Поместите на форму компонент SplitContainer, расширьте его на всю форму путем присвоения параметру Dock значения Fill и зафиксируйте размер правой панели, присвоив свойству FixedPanel значение Panel2. В правой панели компонента SplitContainer разместите группу ( Group ) "Параметры " и тоже расширьте ее на всю панель при помощи свойства Dock. В группе создайте три метки ( Label ) расположенные друг под другом: Цвет вершины №1, Цвет вершины №2, Цвет вершины №3. Поместите напротив этих меток три панели ( Panel ) и присвойте им имена vertex1Panel, vertex2Panel и vertex3Panel. Чтобы создать черные рамки вокруг панелей, установите свойство BorderStyle в значение FixedSingle. Поместите в левой панели компонента SplitContainer наш компонент XnaPanel, назовите его xnaPanel и расширьте его на всю свободную область формы, присвоив свойству Dock значение Fill. В заключение, поместите на форму невизуальный компонент ColorDialog.

    Следующий этап - "оживление " формы путем создания обработчиков событий. Для начала мы добавим в форму несколько вспомогательных полей и обработчики событий Load/ и FormClosed (листинг 3.2).

    // Различные вспомогательные поля GraphicsDevice device = null; 
    PresentationParameters presentParams; Effect effect = null; 
    VertexDeclaration decl = null; VertexPositionColor[] vertices = null;
    bool closing = false;
    new XnaGraphics.Color(vertex2Panel.BackColor.R, vertex2Panel.BackColor.G, 
    vertex2Panel.BackColor.B));
    vertices[2] = new VertexPositionColor(new Vector3(-0.4f, -0.4f, 0.0f), 
     new XnaGraphics.Color(vertex3Panel.BackColor.R, 
    vertex3Panel.BackColor.G, 
    vertex3Panel.BackColor.B));
    // Рисуем треугольник
    effect.Begin();
    foreach(EffectPass pass in effect.CurrentTechnique.Passes)
    {
    pass.Begin();
    device.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0,
    vertices.Length / 3);
    pass.End(); 
    } 
    effect.End();
    // Переключаем экранные буферы
    device.Present();
    } 
    // Обрабатываем ситуацию внезапной потери устройства
    catch (DeviceNotResetException)
    {
    Invalidate() ;
    }
    catch (DeviceLostException)
    {
    } 
    }
    private void xnaPanelResize(object sender, EventArgs e)
    {
    // Если форма не минимизирована
    if (WindowState != FormWindowState.Minimized)
    { 
    // Обновляем размер формы
    presentParams.BackBufferWidth = xnaPanel.ClientSize. Width;
    presentParams.BackBufferHeight = xnaPanel.ClientSize.Height; 
    // Сбрасываем устройство
    device.Reset(presentParams);
    } 
    }

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

    private void vertex1Panel_Click(object sender, EventArgs e) 
    { 
    // Преобразовываем параметр sender к Panel
    Panel panel = (Panel)sender; 
    // Выбираем в диалоговом окне текущий цвет вершины
    colorDialog1.Color = panel.BackColor; 
    // Показываем стандартный диалог выбора цвета
    if (colorDialog1.ShowDialog() == DialogResult.OK)
    { 
    // Если пользователь выбрал цвет, изменяем цвет панели на выбраный
    panel.BackColor = colorDialog1.Color; 
    // Обновляем содержимое нашего компонента xnaPanel
    panelDX.Invalidate();
    } 
    }

    Как видно, обработчик получает информацию о нажатой панели из параметра sender, что позволяет без изменений использовать этот обработчик для панелей vertex2Panel и vertex3Panel: просто выделите эти панели и назначьте в качестве обработчика события Click метод vertex1Panel_Click.

    Остается лишь откомпилировать программу и посмотреть результат. Готовое приложение можно найти в ../1/example.zip в каталоге Examples\Ch03\Ex01.

    Примечание

    В Visual Studio выбор активного проектаПроект, который запускается при нажатии клавиши F5 осуществляется при помощи контекстного меню: щелкните на названии проекта в окне Solution Explorer и выберите в появившемся контекстном меню пункт Set as StartUp Project (рисунок 3.4).

    (рис 3.4) Выбор активного проекта с использованием контекстного меню

    Практическое упражнение №3.1.

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

    (рис 3.5) Иллюстрация к практическому упражнению №3.1

    Для ограничения области визуализации XNA Framework воспользуйтесь компонентом XnaPanel из примера Ch03\Ex02. Существует два способа добавления компонента XnaPanel в окно Toolbox:

  • одключить к решению проект XnaPanel из примера Ch03\Ex01. Для этого во вкладке Solution Explorer щелкнете правой кнопкой мыши на узле с названием решения ( Solution ) и выберете в контекстном меню пункт Add | Existing Project… . Укажите в открывшемся окне файл проекта компонента PanelDX (Examples\Ch03\Ex01 - XnaPanel\XnaPanel\XnaPanel.csproj) и нажмите Ok. После компиляции подключенного проекта (Ctrl + Shift + B) компонент Xna Panel автоматически появится в окне Toolbox.
  • Добавить в окно ToolBox компонент из сборки примера Ex01. Для этого щелкните правой кнопкой мыши на поверхности окна ToolBox и выберите в контекстном меню пункт Choose Items…. Откроется диалоговое окно Choose Toolbox Items…. Щелкните на кнопку Browse… и укажите сборку с компонентом PanelDX (Examples\Ch03\Ex01 - XnaPanel\XnaPanel\bin\Release\XnaPanel.dll) . Наконец, нажмите кнопку Ok, после чего на панели Toolbox появится компонент XnaPanel.
  • Какой из этих двух вариантов выбрать – решать вам, но лично мне больше симпатизирует первый подход.

    3.2. Полноэкранный режим

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

  • В полноэкранном режиме пользователь полностью погружается в виртуальный мир, не отвлекаясь на различные элементы интерфейса Windows вроде панели задач ( Taskbar ).
  • Контроль всего пространства экрана позволяет переключать экранный и задний ( back ) буферы без необходимости копирования информации между буферами: при показе итогового изображения XNA Framework просто делает задний буфер экранным, а экранный задним.
  • При запуске приложения на компьютере с недостаточно мощной видеокартой пользователь может повысить производительность путем уменьшения разрешения экрана и глубины цвета.
  • К недостаткам полноэкранного режима можно отнести несколько усложнение кода приложения и невозможность использования элементов управления Windows Forms, что затрудняет применение полноэкранного режима в различных утилитах.

    Переход полноэкранный режим осуществляется путем создания графического устройства ( GraphicsDevice ) с соответствующим образом настроенной структурой PresentationParameters. Легко догадаться, что свойству IsFullScreen необходимо присвоить значение true:

    presentParams = new PresentationParameters();
    presentParams.IsFullScreen = true;

    Кроме того, приложение должно задать параметры используемого видеорежима, а именно:

  • Разрешение экрана.
  • Формат пикселей экрана (количество бит на пиксель и т.п.).
  • Частоту вертикальной развертки.
  • РазрешениеКоличество пикселей по высоте и ширине экрана и вспомогательных ( back ) буферов задается свойствами BackBufferWidth и BackBufferHeight структуры PresentationParameters. Например:

    // Используем видеорежим с разрешением 640 x 480 
    presentParams.BackBufferWidth = 640; 
    presentParams.BackBufferHeight = 480;

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

    public SurfaceFormat BackBufferFormat { get; set; }

    где

  • SurfaceFormat - перечислимый тип, используемый для задания формата пикселей (таблица 3.1).
  • В качестве формата экранного буфера автоматически используется формат, наиболее близкий к формату заднего буфера. Например, если вы укажете для заднего буфера формат SurfaceFormat.Color, в качестве формата экранного буфера будет выбран формат SurfaceFormat.Bgr32. Обычно это не существенно, ведь альфа-канал все равно не оказывает никакого влияния на отображаемом на экране изображении; тем не менее, в некоторых немногочисленных ситуациях этот нюанс все же приходится учитывать.

    Характеристика форматов пикселей, соответствующих значениям перечислимого типа SurfaceFormat
    Значение Может использоваться в качестве формата экранного буфера Размер в битах
    Весь пиксель Красный канал Зеленый канал Синий канал Альфа канал
    Unknown ? ? ? ? ?
    Rgba1010102 X 32 10 10 10 2
    Color 32 8 8 8 8
    Bgr32 X 32 8 8 8 -
    Bgr565 X 16 5 6 5 -
    Bgra5551 16 5 5 5 1

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

    Однако применение формата SurfaceFormat.Unknown для полноэкранного графического устройства приводит к генерации исключения System.InvalidOperationException. Так что при создании устройства, использующего полноэкранный режим, приложение обязано явно указывать формат пикселей заднего буфера. В нашем первом приложении мы, не мудрствуя лукаво, будем использовать формат SurfaceFormat.Bgr32, поддерживаемый всеми видеокартами, совместимыми с XNA Framework: presentParams.BackBufferFormat = SurfaceFormat.Bgr32 ;

    Примечание

    Должно быть, вы заметили, что пиксели формата SurfaceFormat.Bgr32 имея размер 32 бита, содержат всего 24 бита полезной информации (для красного, синего и зеленого каналов отведено по 8 бит). Такая избыточность обусловлена тем, что современные процессоры умеют работать только с типами данных разрядностью 8, 16, 32 и 64 бит. Соответственно, 24-х битные типы данных пришлось бы эмулировать посредством 8, 16 и 32-х разрядных типов, что неминуемо увеличило количество операций, требуемых для считывания и записи значения пикселя и, соответственно, ощутимо снизило производительность. Например, для записи 24-х битного значения необходимо загрузить регистр 32 бита, модифицировать 24 бита, и записать полученное 32-х битное значение обратно в память.

    И, наконец, последней характеристикой любого графического режима является частота обновления экрана. Для современных электронно-лучевых трубок нормой является частота обновления порядка 85 герц и выше, так как при более низких частотах становится заметным мерцание монитора. Мониторы LCD менее критичны к низкой частоте обновления экрана, однако редкий современный LCD -монитор поддерживает частоту смены кадров более 60 Гц. Частота обновления экрана задается свойством FullScreenRefreshRateInHz структуры PresentationParameters:

    public int FullScreenRefreshRateInHz { get; set; }

    В оконном режиме этому свойству присевается нулевое значение, ведь окно все равно может обновляться с частотой, отличной от частоты обновления, используемой рабочим столом. В полноэкранном режиме нулевое значение параметра FullScreenRefreshRateInHz, означает, что приложение отдает выбор частоты обновления экрана на откуп Windows и драйверам видеокарты/монитора. Так как при указании частоты обновления неподдерживаемой текущей видеоподсистемой может быть сгенерировано исключение System. InvalidOperationException, мы пока не будем пытаться самостоятельно выбирать частоту обновления экрана.

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

    presentParams = new PresentationParameters();
    presentParams.BackBufferCount = 1;
    presentParams.SwapEffect = SwapEffect.Discard;
    // Используем полноэкранный режим 640Ч480, 
    32 бита на пиксель, частота обновления выбирается
    // автоматически
    presentParams.IsFullScreen = true;
    presentParams.BackBufferWidth = 640;
    presentParams.BackBufferHeight = 480;
    presentParams.BackBufferFormat = SurfaceFormat.Bgr32;
    presentParams.FullScreenRefreshRateInHz = 0;
    GraphicsDeviceCapabilities caps =
    GraphicsAdapter.DefaultAdapter.GetCapabilities(DeviceType.Hardware);
    CreateOptions options = CreateOptions.SingleThreaded; if
    (caps.DeviceCapabilities.SupportsHardwareTransformAndLight)
    options |= CreateOptions.HardwareVertexProcessing;
    else
    options |= CreateOptions.SoftwareVertexProcessing;
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, 
    DeviceType.Hardware, this.Handle, options, presentParams);

    Так же необходимо подправить код обработчика событий Resize. Дело в том, что даже в полноэкранном режиме форма продолжает жить своей жизнью и в ряде случаях (при переключении в другое приложение путем нажатия клавиш alt + tab ) Windows может самостоятельно изменять размер формы, что, разумеется, приводит к генерации события Resize. Но так как в полноэкранном режиме размер формы некоим образом не связан с разрешением экрана, приложение не должно изменять размер заднего ( back ) буфера:

    private void MainForm_Resize(object sender, EventArgs e)
    {
    // Если приложение находится в полноэкранном режиме, 
    оно не должно отслеживать изменение
    // размера формы
    if ((!presentParams.IsFullScreen)  
    (WindowState != FormWindowState.Minimized))
    {
    presentParams.BackBufferWidth = ClientSize.Width;
    presentParams.BackBufferHeight = ClientSize.Height;
    device.Reset(presentParams); 
    } 
    }

    Визуализация сцены осуществляется аналогично оконному режиму:

    private void MainFormPaint(object sender, PaintEventArgs e) 
    {
    if (closing)
    return;
    try 
    {
    if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.Lost)
    throw new DeviceLostException();
    if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.NotReset)
    device.Reset(presentParams);
    // Задаем область визуализации размеров во весь экран
    device.Viewport = Helper.FullScreenViewport presentParams); 
    // Очищаем весь экран
    device.Clear(XnaGraphics.Color.CornflowerBlue);
    // Используем квадратную область визуализации 
    (во избежание геометрических искажений 
    // изображения)
    device.Viewport = Helper.SquareViewport(presentParams);
    // Рисуем CD-диск (см. раздел 2.6.3)
    }
    catch (DeviceNotResetException)
    {
    Invalidate() ;
    }
    catch (DeviceLostException)
    {
    }
    }

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

    // Вычисление квадратной области визуализации
    public static Viewport SquareViewport(PresentationParameters 
    presentParams)
    {
    return SquareViewport(new System.Drawing.Size(presentParams.
    BackBufferWidth,
    presentParams.BackBufferHeight)); }
    // Вычисление области визуализации размеров во весь экран
    public static Viewport FullScreenViewport(PresentationParameters 
    presentParams)
    {
    return FullScreenViewport(new System.Drawing.Size(presentParams.
    BackBufferWidth,
    presentParams.BackBufferHeight));
    
    }

    Готовое приложение, визуализирующее изображение CD-диска в полноэкранном режиме (рисунок 3.6), находится в каталоге Examples\Ch03\Ex03.

    (рис 3.6) CD-диск, визуализированный полноэкранном режиме (640x480x32bpp)

    3.2.1. Выбор оптимального видеорежима

    В примере Ch03\Ex03 (листинг 3.4) мы неявно предполагаем, что абсолютно все современные и будущие видеоподсистемы будут поддерживать видеорежим 640Ч480Ч32 bppBPP – Bit Per Pixel (Бит на пиксель) . Если это вдруг окажется не так, приложение аварийно завершит работу с исключением System.InvalidOperationException. Поэтому было бы логичным на всякий случай предусмотреть поведение приложение при отсутствии поддержки требуемого видеорежима. Например, при неудачной попытке создания полноэкранного графического устройства приложение могло бы попытаться создать графическое устройство, осуществляющее визуализацию в окне:

    presentParams = new PresentationParameters();
    presentParams.BackBufferCount = 1;
    presentParams.SwapEffect = SwapEffect.Discard;
    presentParams.IsFullScreen = true;
    // Используем полноэкранный режим 640Ч480, 32 бита на пиксель, 60 герц
    presentParams.BackBufferWidth = 640;
    presentParams.BackBufferHeight = 480;
    presentParams.BackBufferFormat = SurfaceFormat.Bgr32;
    presentParams.FullScreenRefreshRateInHz = 60;
    try {
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, 
    DeviceType.Hardware, this.Handle,
    options, presentParams);
     }
    catch(InvalidOperationException) 
    {
    // Если попытка использования полноэкранного режима 
    потерпела фиаско, используем 
    // визуализацию в окне
    presentParams.IsFullScreen = false;
    presentParams.BackBufferWidth = 0;
    presentParams.BackBufferHeight = 0;
    presentParams.BackBufferFormat = SurfaceFormat.Unknown;
    presentParams.FullScreenRefreshRateInHz = 0;
    
    // После выполнения этой команды интерфейс формы будет 
    заблокирован: форму нельзя 
    // будет перемещать по экрану, изменять ее размер, 
    минимизировать и т.п.
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, 
    DeviceType.Hardware,
    this.Handle, options, presentParams); 
    }

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

    И так, при создании полноэкранного графического устройства необходимо подобрать видеорежим, гарантированно поддерживаемый данной видеокартой и монитором. А почему бы нам вместо гадания на кофейной гуще не использовать текущий видеорежим рабочего стола, по определению поддерживаемый видеоподсистемой компьютера? Ведь, как известно, в качестве видеорежима рабочего стола обычно используется видеорежим, наиболее оптимальный для текущего монитора компьютера. Это особенно актуально для LCD -мониторов, оптимизированных для работы каком-то одном разрешении экрана (как правило, 1024x768 или 1280x1024). Информация о текущем видеорежиме доступна посредством свойства CurrentDisplayMode класса GraphicsAdapter:

    public DisplayMode CurrentDisplayMode { get; }

    Вся информация о видеорежиме хранится в структуре DisplayMode:

    public struct DisplayMode
    {
    // Количество пикселей по горизонтали
    public int Width { get; } 
    // Количество пикселей по вертикали
    public int Height { get; } 
    // Формат пикселей
    public SurfaceFormat Format { get; } 
    // Частота обновления экрана
    public int RefreshRate { get; }
    }

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

    // Получаем описание текущего видеорежима
    DisplayMode displayMode = GraphicsAdapter.DefaultAdapter.
    CurrentDisplayMode;
    presentParams = new PresentationParameters();
    presentParams.IsFullScreen = true;
    presentParams.BackBufferCount = 1;
    presentParams.SwapEffect = SwapEffect.Discard;
    // Использует такие же параметры визуализации, 
    как у текущего видеорежима
    presentParams.BackBufferWidth = displayMode.Width;
    presentParams.BackBufferHeight = displayMode.Height;
    presentParams.BackBufferFormat = displayMode.Format;
    presentParams.FullScreenRefreshRateInHz = displayMode.RefreshRate;
    GraphicsDeviceCapabilities caps =
    GraphicsAdapter.DefaultAdapter.GetCapabilities(DeviceType.Hardware);
    CreateOptions options = CreateOptions.SingleThreaded;
    if (caps.DeviceCapabilities.SupportsHardwareTransformAndLight)
    options |= CreateOptions.HardwareVertexProcessing; else
    options |= CreateOptions.SoftwareVertexProcessing;
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, 
    DeviceType.Hardware, 
    this.Handle, options, presentParams);

    3.2.2. Получение списка доступных видеорежимов

    Хотя использование видеорежима рабочего стола дает вполне нормальные результаты, в ряде случаев подобный подход оказывается неприемлемым. В основном это касается дешевых видеокарт, демонстрирующих довольно низкую производительность в современных трехмерных приложениях, в результате чего приемлемая частота смены кадров достигается лишь в самых низких разрешениях вроде 640Ч480 или 800Ч600. Поэтому было логичным предоставить пользователю возможность самостоятельно выбирать параметры видеорежима в зависимости от своих потребностей.

    Наше следующее приложение будет отображать на экране перечень всех видеорежимов, поддерживаемых видеокартой и затем переключаться в видеорежим, выбранный пользователем, и визуализировать изображение диска (рисунок 3.7). Эту функциональность достаточно легко реализовать, так как разработчики XNA Framework снабдили класс GraphicsAdapter коллекцией SupportedDisplayModes, содержащей набор структур DisplayMode с информацией обо всех видеорежимах, поддерживаемых указанной связкой видеокарта – монитор.

    (рис 3.7) Диалоговое окно выбора видеорежима

    Итак, создайте новое приложение Windows Forms и добавьте в него новую форму. Присвойте свойствам формы значения согласно таблице 3.2. Поместите на форму компонент ListBox и назовите его displayModeListBox. В заключение, поместите на форму кнопку Ok и присвойте ее свойству DialogResult значение OK.

    Свойства формы выбора видеорежима
    Свойство Значение
    Name DisplayModeForm
    Text Выберите видеорежим
    FormBorderStyle FixedDialog
    MaximizeBox False
    MinimizeBox False

    Теперь реализуем логику работы формы, а именно: конструктор формы и свойство SelectedDisplayMode, возвращающее информацию о выбранном видеорежиме (листинг 3.9).

    public partial class DisplayModeForm : Form 
    {
    // Конструктор формы. В качестве параметра 
    принимает объект графического адаптера, для 
    // которого необходимо выбрать графический режим. Public
    DisplayModeForm(GraphicsAdapter adapter) 
    {
    InitializeComponent(); 
    // Перебираем все графические режимы, поддерживаемые 
    графическим адаптером и добавляем их на
    // панель displayModeListBox
    foreach (DisplayMode diplayMode in adapter.SupportedDisplayModes)
    displayModeListBox.Items.Add(diplayMode);
    // Выбираем самый первый элемент списка видеорежимов
    displayModeListBox.SelectedIndex = 0; 
    }
    // Возвращает информацию о графическом режиме, выбранном пользователем
    public DisplayMode SelectedDisplayMode 
    {
    get 
    {
    return (DisplayMode)displayModeListBox.SelectedItem; 
    } 
      } 
    }

    И наконец, в обработчик события Load главной формы необходимо вставить код взаимодействия с диалоговым окном выбора видеорежима:

    private void MainFormLoad(object sender, EventArgs e) 
    {
    SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
    MinimumSize = SizeFromClientSize(new Size(1, 1));
    presentParams = new PresentationParameters();
    presentParams.IsFullScreen = true;
    presentParams.BackBufferCount = 1;
    presentParams.SwapEffect = SwapEffect.Discard;
    // Создаем диалоговое окно выбора видеорежима
    using (DisplayModeForm displayModeForm = new
    DisplayModeForm(GraphicsAdapter.DefaultAdapter))
    { 
    // Отображаем диалоговое окно
    displayModeForm.ShowDialog();
    // Получаем видеорежим, выбранный пользователем
    DisplayMode mode = displayModeForm.SelectedDisplayMode;
    // Задаем требуемый видеорежим
    presentParams.BackBufferWidth = mode.Width;
    presentParams.BackBufferHeight = mode.Height;
    presentParams.BackBufferFormat = mode.Format;
    presentParams.FullScreenRefreshRateInHz = mode.RefreshRate; 
    }
    // Создаем графическое устройство
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, 
    DeviceType.Hardware, 
    4> this.Handle, options, presentParams);
    }

    Оставшийся код приложения не содержит чего-либо интересного, поэтому мы не будем на нем останавливаться. Готовое приложение находится в каталоге Examples\Ch03\Ex05.

    Запустите полученное приложение и попробуйте поэкспериментировать с различными разрешениями экрана. Очень скоро вы заметите одну важную особенность: в некоторых видеорежимах диск вместо круглой формы растягивается/сплющивается в эллипсовидную фигуру. После внимательного изучения соотношения сторон разрешений экрана мы прейдем к выводу, что диск корректно отображается лишь при условии совпадения отношения сторон монитора с отношением соответствующих размерностей разрешения экрана. Например, отношение сторон моего монитора NEC MultiSync FE991SB равно 4:3 = 1.33, поэтому в разрешениях 320Ч240, 640Ч480, 800Ч600, 1024Ч768, 1280Ч960, 1600Ч1200, 1792Ч1344 изображение отображается без искажений. В разрешении 1280Ч1024 (соотношение сторон 5:4=1.25) круг едва заметно сплющивается вдоль оси Y, а вот в разрешении 1360Ч768 (соотношение сторон 16:9=1.77) сплющивание круга вдоль оси X становится более чем заметным. С другой стороны, на широкоэкранных мониторах с соотношением сторон 16:9 именно разрешения 1088Ч612, 1280Ч768, 1360Ч768 и 1600Ч900 (т.е. с соотношением сторон 16:9) будут давать изображение без искажений.

    (рис 3.8) Искажение формы круглого диска, визуализированного в разрешении 1360Ч768, при отображение на мониторе с соотношение сторон 4:3

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

    Примечание

    Обратите внимание на отсутствие в списке видеорежимов с форматами пикселей с поддержкой альфа канала. Дело в том, что коллекция SupportedDisplayModes содержит только видеорежимы экранного буфера, а экранный буфер не поддерживает альфа-канал по причине банальной ненужности (см. таблицу 3.1). Так что отсутствие поддержки альфа-канала экранным буфером вовсе не означает отсутствие поддержки форматов с альфа-каналом задним буфером. Более того, если современная видеокарта поддерживает формат SurfaceFormat.Bgr32, то она с вероятность 99.9% поддерживает и формат SurfaceFormat.Color. Впрочем, эти нюансы для нас пока не актуальны, так как наши текущие приложения все равно не используют альфа-канал.

    3.2.3. Диалоговое окно выбора видеорежима

    Диалоговое окно выбора видеорежима из примера Ch03\Ex05 хотя и выполняет свою задачу, однако имеет при этом ряд существенных недостатков, затрудняющих его применение в реальных приложениях:

  • При каждом запуске приложения приходится заново выбирать видеорежим, что довольно быстро начинает раздражать. Поэтому было бы логичным реализовать сохранение параметров выбранного видеорежима в конфигурационный файл.
  • Все параметры видеорежимов "свалены в одну кучу ", отчего очень быстро начинает "рябить в глазах ". Коммерческие приложения обычно разбивают информацию о видеорежимах на три списка: в первом списке пользователь выбирает разрешение экрана, во втором – глубину цвета (формат пикселей), а в третьем – частоту обновления экрана.
  • Для пользователя актуальны только те видеорежимы, соотношение сторон которых совпадает с соотношением сторон монитора. С другой стороны, для оценки в уме соотношения сторон видеорежима вроде 1792Ч1344 требуются незаурядные математические способности, поэтому было бы логичным предусмотреть типовые фильтры 4:3, 5:4 и 16:9, отбрасывающие видеорежимы с соотношением сторон отличным заданногоВ принципе, можно было бы запрашивать у пользователя соотношение сторон монитора, после чего масштабировать изображение исходя из параметров разрешения экрана и соотношения сторон монитора (такой подход применяется, к примеру, в игре Half-Life 2) .
  • Не помешало бы на всякий случай предоставить пользователю возможность выбора визуализации в оконном режиме, который иногда оказывается весьма полезным (например, при одновременной работе с несколькими приложениями).
  • Ну что ж, настало время разработать новую версию диалогового окна, свободную от этих недостатков (рисунок 3.9). Запустите Visual Studio 2005 и создайте приложение Windows Application. Мы начнем с создания класса, инкапсулирующего работу с файлами конфигурации приложения. Наш файл конфигурации будет содержать 7 полей:

    Поля файла конфигурации
    Поле Тип данных Описание Значение по умолчанию
    Width int размер заднего буфера вдоль оси X 0
    Height int размер заднего буфера вдоль оси Y 0
    Format SurfaceFormat формат пикселей заднего буфера SurfaceFormat.Unknown
    RefreshRate int частота обновления экрана 0
    FullScreen bool работает ли приложение в полноэкранном режиме ( false – нет, true – да) true
    Init bool равен true, если информация о конфигурации приложения была загружена из файла false
    ShowSettingForm bool надо ли отображать при запуске приложения диалоговое окно выбора видеорежима true

    Так как наш файл конфигурации не будет представлять собой нечего экстравагантного, все операции по созданию класса вполне можно выполнить в дизайнере Visual Studio. Для этого в окне Solution Explorer раскройте узел Properties, и сделайте двойной щелчок левой кнопкой мыши на узле Setting.settings, после чего слева откроется дизайнер файла конфигурации приложенияДизайнер файла конфигурации приложения доступен также посредством вкладки Settings окна Properties . Добавьте в настройки приложения новые поля согласно таблице 3.3 и рисунок 3.10.

    (рис 3.10) Новая версия диалогового окна выбора видеорежима (рис 3.9) Настройка параметров конфигурации приложения

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

    Свойства формы диалогового окна и элементов управления
    Класс Свойство Значение
    Form (диалоговое окно) Name SettingsForm
    Text Параметры
    FormatBorderStyle FixedDialog
    MinimizeBox false
    MaximizeBox false
    CheckBox Name inWindowCheckBox
    Text Визуализировать в окне
    GroupBox Text Видеорежим
    Label Text Соотношение сторон
    ComboBox (фильтр отображаемых видеорежимов по соотношению сторон) Name aspectRatioComboBox
    DropDownStyle DropDownList
    Label Text Разрешение
    ComboBox (список разрешений экрана) Name resolutionComboBox
    DropDownStyle DropDownList
    Label Text Глубина цвета:
    ComboBox (список форматов пикселей) Name colorDepthComboBox
    DropDownStyle DropDownList
    Label Text Частота обновления
    ComboBox (список частот обновления экрана) Name refreshRateComboBox
    DropDownStyle DropDownList
    Button Name okButton
    Text Ok
    Button Text Cancel
    DialogResult Cancel

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

    // Инкапсулирует элемент списка соотношений сторон
    public struct AspectItem 
    {
    // Коэффициент отношения сторон
    public float aspect;
    public AspectItem(float aspect) 
    {
    this.aspect = aspect; 
    }
    // Проверяет указанный элемент списка разрешений 
    (ResolutionItem) на соответствие текущему 
    // соотношению сторон. В случае равенства метод 
    возвращает true, иначе - false public bool
    Compare(float aspect) 
    {
    // Нулевое соотношение сторон является зарезервированным 
    значением ( "любое соотношение
    // сторон "), поэтому возвращаем true if 
    (this.aspect == 0) return true;
    // Сравниваем коэффициенты отношения сторон, 
    закладывая небольшой  "запас " в
    один процент. 
    // Дело в том, что некоторые режимы имеют не совсем 
     "академически " правильное
    соотношение 
    // сторон. Например, соотношение сторон разрешения 
    1360 x 768 равно 85/48=1.771, в 
    // как 16/9=1.778.
    if (Math.Abs(this.aspect - aspect) < this.aspect * 0.01f)
    return true;
    else
    return false;
    }
    // Возвращает текущее соотношение сторон в удобочитаемом виде
    public override string ToString() 
    {
    if (aspect == 0)
    return "Любое";
    if (Compare(4.0f / 3.0f))
    return "4 : 3";
    if (Compare(5.0f / 4.0f))
    return "5 : 4";
    if (Compare(16.0f / 9.0f)) return "16 : 9";
    return aspect.ToString(); 
    }
     }
    // Инкапсулирует элемент списка разрешений экрана
    public struct ResolutionItem : IComparable<ResolutionItem>
    {
    // Разрешение экрана
    public int width;
    public int height;
    public ResolutionItem(int width, int height) 
    {
    this.width = width;
    this.height = height;
     }
    // Отношение количества пикселей вдоль осей X и Y для 
    текущего разрешения экрана
    public float Aspect 
    {
    get
     {
    return (float)width / (float)height; 
    } 
    }
    // Возвращает в удобочитаемом виде текст элемента 
    списка public override string ToString() 
    {
    return width.ToString() + " x " + height.ToString(); 
    }
    // Реализация интерфейса IComparable<ResolutionItem>, 
    используемого при сортировке элементов 
    // списка
    public int CompareTo(ResolutionItem other) 
    { 
    // Сначала сравнивает ширина разрешений if (width > other.width) 
    return 1;
    if (width < other.width)
    return -1;
    // Если ширина одинаковая, сравнивается высота разрешений. 
    В результате разрешения сначала 
    // сортируются по ширине, а затем по высоте. if (height > 
    other.height) return 1;
    if (height < other.height) return -1;
    return 0;
    } 
    }
    // Инкапсулирует элемент списка форматов пикселей
    public struct ColorDepthItem : IComparable<ColorDepthItem>
    {
    // Формат пикселей
    public SurfaceFormat value;
    public ColorDepthItem(SurfaceFormat value) 
    {
    this.value = value; 
    }
    // Возвращает строку с описанием формата пикселя
    public override string ToString() 
    {
    string str;
    switch (value) 
    {
    case SurfaceFormat.Bgr32:
    str = "32 бита ({0})";
    break;
    case SurfaceFormat.Bgr565:
    str = "16 бит ({0})";
    break; case
    SurfaceFormat.Rgba1010102:
    str = "32 бит ({0})";
    break; case
     SurfaceFormat.Bgr555:
    str = "16 бит {0}";
    break; default:
    str = "{0}";
    break; 
    }
    return string.Format(str, value);
     }
    // Реализация интерфейса IComparable<ColorDepthItem>, 
    используемого при сортировке элементов 
    // списка
    public int CompareTo(ColorDepthItem other) 
    {
    if (value > other.value)
     return -1;
    if (value < other.value)
     return 1;
    return 0; 
    }
    }
    // Инкапсулирует элемент списка частот обновлений экрана public
     struct RefreshRateItem : IComparable<RefreshRateItem> 
    {
    // Частота обновления экрана
    public int value;
    public RefreshRateItem(int value) 
    {
    this.value = value; 
    }
    // Возвращает строку с частотой обновления экрана
     public override string ToString() 
    {
    return value.ToString() + " Гц"; 
    }
    // Реализация интерфейса IComparable< RefreshRateItem>, 
    используемого при сортировке 
    // элементов списка
    public int CompareTo(RefreshRateItem other) 
    {
    if (value > other.value)
    return 1;
    if (value < other.value)
     return -1;
    return 0; 
    }
    }

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

    public partial class SettingsForm : Form
    {
    // Набор ассоциативных сортированных массив с 
    информаций о поддерживаемых видеорежимах
    SortedDictionary<ResolutionItem, SortedDictionary<ColorDepthItem, 
     SortedDictionary<RefreshRateItem, bool>>> modeCollection;
    // Текущие настройки приложения. Класс Properties.Settings 
    автоматически генерируется 
    // дизайнером настроек приложения (рисунок 3.10). 
    Properties.Settings settings;
    // Конструктор формы
    internal SettingsForm(Properties.Settings settings, 
    GraphicsAdapter adapter) 
    {
    InitializeComponent();
    // Сохраняем ссылку на настройки приложения
     this.settings = settings;
    // Если файл с конфигурацией приложения не был найден,
     устанавливаем в качестве параметров по 
    // умолчанию настройки рабочего стола if (!settings.Init) 
    {
    settings.Width = adapter.CurrentDisplayMode.Width;
     settings.Height = adapter.CurrentDisplayMode.Height;
     settings.Format = adapter.CurrentDisplayMode.Format;
     settings.RefreshRate = adapter.CurrentDisplayMode.RefreshRate;
     settings.FullScreen = true;
    }
    modeCollection = new SortedDictionary<ResolutionItem, 
    4> SortedDictionary<ColorDepthItem, SortedDictionary<RefreshRateItem,
    bool>>>();
    // Перебираем все видеорежимы, поддерживаемые видеокартой и
     заполняем ассоциативный массив 
    // modeCollection
    foreach (DisplayMode mode in adapter.SupportedDisplayModes)
    {
    ResolutionItem res = new ResolutionItem(mode.Width, mode.Height);
    if (!modeCollection.ContainsKey(res))
    modeCollection.Add(res, new SortedDictionary<ColorDepthItem,
     SortedDictionary<RefreshRateItem, bool>>());
    ColorDepthItem depth = new ColorDepthItem(mode.Format); if
     (!modeCollection[res].ContainsKey(depth))
    modeCollection[res].Add(depth, new SortedDictionary<RefreshRateItem,
     bool>());
    RefreshRateItem refresh = new RefreshRateItem(mode.RefreshRate); if
     (!modeCollection[res][depth].ContainsKey(refresh))
     modeCollection[res][depth].Add(refresh, true); }
    // Задаем состояние флага  "визуализировать в окне "
    inWindowCheckBox.Checked = !settings.FullScreen;
    // Добавляем в список  "соотношение сторон " типовые фильтры
    видеорежимов aspectRatioComboBox.Items.Add(new AspectItem());
     aspectRatioComboBox.Items.Add(new AspectItem(4.0f / 3.0f));
    aspectRatioComboBox.Items.Add(new AspectItem(5.0f / 4.0f));
     aspectRatioComboBox.Items.Add(new AspectItem(16.0f / 9.0f));
    // Выбираем самый первый элемент списка ( "Любое ") 
    aspectRatioComboBox.SelectedIndex = 0; }
    }

    Как видно, информация о поддерживаемых видеорежимах хранится в многомерном ассоциативном массиве, упрощающем поиск информации о требуемых видеорежимах. Например, для проверки существования видеорежима с разрешением 1024x768x32bpp:@85Hz приложение должно проверить существование элемента modeCollection[ "1024x768 "][ SurfaceFormat.Brg32][85] . Последний оператор конструктора выбирает нулевой элемент списка, генерируя событие SelectedIndexChanged, обработчик которого приведен в листинге 3.13.

    // Обновляет список поддерживаемых разрешений экрана с 
    учетом нового фильтра соотношения
    // сторон
    private void aspectRatioComboBoxSelectedIndexChanged(object 
    sender, EventArgs e)
    {
    // Получаем текущий элемент списка  "соотношение сторон "
    AspectItem aspect = (AspectItem)aspectRatioComboBox.SelectedItem;
    // Текущее разрешение экрана
    ResolutionItem currentResolution; 
    // Используем в качестве текущего разрешения экрана элемент
     из списка  "Разрешение ". Если же 
    // элемент в списке не выбран, используем разрешение
     экрана из текущих настроек приложения if 
     (resolutionComboBox.SelectedIndex != -1)
    currentResolution = (ResolutionItem)resolutionComboBox.
    SelectedItem; else
    currentResolution = new ResolutionItem(settings.Width,
     settings.Height);
    // Очищаем список разрешений экрана
    resolutionComboBox.Items.Clear(); 
    // Перебираем все разрешения экрана
    foreach (ResolutionItem res in modeCollection.Keys) 
    // Если разрешение экрана удовлетворяет заданному соотношению сторон
    if (aspect.Compare(res.Aspect)) 
    // Добавляем его в список разрешений
    resolutionComboBox.Items.Add(res);
    // Если было найдено хотя бы одно разрешение экрана, 
    соответствующее заданному соотношению 
    // сторон
    if (resolutionComboBox.Items.Count != 0) 
    { 
    // В списке разрешений экрана пытаемся выбрать текущее разрешение
    resolutionComboBox.SelectedItem = currentResolution; 
    // Если это не удалось, выбираем самый первый элемент списка if
     (resolutionComboBox.SelectedIndex == -1)
     resolutionComboBox.SelectedIndex = 0; }
    // Вызываем обработчик события изменения состояния 
    переключателя  "Визуализировать в окне ",
    // который при необходимости активирует/деактивирует 
    определенные элементы формы вроде кнопки
    // Ok
    inWindowCheckBox_CheckedChanged(inWindowCheckBox, null); 
    }
    // Обработчик события CheckedChange флага  "Визуализировать
     в окне ",
    private void inWindowCheckBox_CheckedChanged(object sender, EventArgs e)
    {
    // Если флаг включен
    if (inWindowCheckBox.Checked) 
    { 
    // Деактивируем все списки, связанные с параметрами 
    полноэкранного режима
     aspectRatioComboBox.Enabled = false; resolutionComboBox.Enabled =
     false; colorDepthComboBox.Enabled = false; refreshRateComboBox.Enabled
     = false; okButton.Enabled = true; 
    }
    else 
    { 
    // Активируем список  "Соотношение сторон "
    aspectRatioComboBox.Enabled = true; 
    // Если список доступных разрешений экрана не пустой if
    (resolutionComboBox.Items.Count > 0) 
    { 
    // Активируем оставшиеся три списка и кнопку Ok 
    resolutionComboBox.Enabled = true;
    colorDepthComboBox.Enabled = true;
    refreshRateComboBox.Enabled = true;
    okButton.Enabled = true; 
    }
    else 
    { 
    // В противном случае блокируем эти элементы управления
     resolutionComboBox.Enabled = false;
     colorDepthComboBox.Enabled = false;
     refreshRateComboBox.Enabled = false; okButton.Enabled =
     false; 
    } 
    } 
    }

    Так как выбранное разрешение оказывает влияние на доступные форматы пикселей списка "Глубина цвета ", необходимо определить обработчик события SelectedIndexChanged списка разрешений (листинг 3.14).

    private void resolutionComboBoxSelectedIndexChanged(object sender, EventArgs e)
    {
    // Получаем выбранный элемент списка разрешений экрана
    ResolutionItem res = (ResolutionItem) resolutionComboBox.SelectedItem; 
    // Текущая глубина цвета
    ColorDepthItem currentColorDepth; 
    // Используем в качестве текущей глубины цвета 
    элемент из списка  "Глубина цвета ".
    Если же 
    // элемент в списке не выбран, используем глубину 
    цвета из текущих настроек приложения
    if (colorDepthComboBox.SelectedIndex != -1)
    currentColorDepth = (ColorDepthItem)colorDepthComboBox.SelectedItem;
    else
    currentColorDepth = new ColorDepthItem(settings.Format);
    // Очищаем список  "Глубина цвета "
    colorDepthComboBox.Items.Clear(); 
    // Перебираем все форматы пикселей выбранного 
    разрешения экрана и добавляем их в список 
    //  "Глубина цвета "
    foreach (ColorDepthItem colorDepth in modeCollection[res].Keys)
     colorDepthComboBox.Items.Add(colorDepth);
    // Пытаемся выбрать глубину цвета, как у текущих настроек приложения
    colorDepthComboBox.SelectedItem = currentColorDepth; 
    // В случае неудачи выбираем первый элемент списка
    if (colorDepthComboBox.SelectedIndex == -1)
     colorDepthComboBox.SelectedIndex = 0; 
    }

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

    private void colorDepthComboBox_SelectedIndex
    Changed(object sender, EventArgs e)
    {
    // Получаем выбранное разрешение и глубину цвета (формат пикселей)
    ResolutionItem res = (ResolutionItem)resolutionComboBox.SelectedItem;
    ColorDepthItem colorDepth = (ColorDepthItem)
    colorDepthComboBox.SelectedItem;
    // Текущая частота обновления экрана
    RefreshRateItem currentRefreshRate; 
    // Используем в качестве текущей частоты обновления 
    экрана элемент из списка. Если же элемент 
    // в списке не выбран, используем частоту обновления 
    экрана из текущих настроек приложения if
     (refreshRateComboBox.SelectedIndex != -1)
    currentRefreshRate = (RefreshRateItem)refreshRate
    ComboBox.SelectedItem; else
    currentRefreshRate = new RefreshRateItem(settings.RefreshRate);
    // Очищаем список  "Частота обновления "
    refreshRateComboBox.Items.Clear(); 
    // Перебираем частоты обновления экрана, поддерживаемые 
    выбранным разрешением с указанной
    // глубиной цвета
    foreach (RefreshRateItem refreshRate in modeCollection[res]
    [colorDepth].Keys)
     refreshRateComboBox.Items.Add(refreshRate);
    // Выбираем в списке  "Частота обновления " 
    текущую частоту обновления экрана
    refreshRateComboBox.SelectedItem = currentRefreshRate; 
    // Если такого элемента нет, выбираем самый первый элемент списка
    if (refreshRateComboBox.SelectedIndex == -1)
     refreshRateComboBox.SelectedIndex = 0; 
    }

    Завершая создание диалогового окна, мы должны реализовать обработчик нажатия кнопки Ok:

    private void okButton_Click(object sender, EventArgs e)
    {
    // Обновляем настройки приложения на основе текущего 
    состояния элементов управления
    // формы окна
    settings.FullScreen = !inWindowCheckBox.Checked;
    if (!inWindowCheckBox.Checked)
    {
    settings.Width = ((ResolutionItem)resolutionComboBox.SelectedItem).width; 
    settings.Height = ((ResolutionItem)resolutionComboBox.SelectedItem).height;
    settings.Format = ((ColorDepthItem)colorDepthComboBox.SelectedItem).value;
    settings.RefreshRate = ((RefreshRateItem)refreshRate
    ComboBox.SelectedItem).value; 
    }
    // Настройки были инициализированы. При следующем показа
     диалогового окна все элементы 
    // управления будут инициализированы согласно значениями 
    сохраненных настроек
    settings.Init = true; 
    // Пользователь настроил приложение. Показывать диалоговое 
    окно больше нет необходимости.
    settings.ShowSettingForm = false;
    DialogResult = DialogResult.OK; 
    }

    Интеграция диалогового окна в приложение

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

  • Вполне логично, что диалоговое окно должно отображаться, если не был найден файл конфигурации. Данную ситуацию можно распознать по свойству Init класса Settings равному true.
  • Если приложение потерпело неудачу при создании графического устройства, то при следующем запуске приложения не помешало бы отобразить диалоговое окно с настройками приложения – вполне вероятно, что изменение видеорежима поможет избежать проблемы. Как такое поведение реализовать на практике? При возникновении проблем приложение должно присвоить свойству ShowSettingForm конфигурации приложения значение true и завершить работу. А при следующем запуске приложение обнаружит, что свойство ShowSettingForm равно true, и отобразит диалоговое окно с настройками приложения.
  • Наконец, нужен механизм, позволяющий пользователю легко исправить настройки приложения по собственному желанию. Для этой цели можно встроить в обработчик события Load главной формы приложения распознавание ключа /config в параметрах командной строки приложения. Таким образом, инсталлятор приложения наряду с ярлыком запуска приложения может создать дополнительный ярлык "Настройка приложения ", вызывающий это же приложение с параметром /config.
  • Код, реализующий всю вышеперечисленную функциональность, будет иметь достаточно большой размер, поэтому его логично будет инкапсулировать в отдельный метод, чтобы не захламлять обработчик события Load (листинг 3.17).

    using System.Diagnostics;
    void InitGraphivsDevice() 
    {
    SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
    MinimumSize = SizeFromClientSize(new Size(1, 1));
    // Загружаем настройки приложения из файла
     settings = new Properties.Settings();
    // Определяем, содержит ли командная строка параметр  "/config
     " string[] args = Environment.GetCommandLineArgs(); bool
     configParam = false;
    if ((args.Length == 2)  (args[1].ToUpper() == "/CONFIG"))
     configParam = true;
    // Если выполняется одно из трех вышеописанных условий
    if ((configParam) || (settings.ShowSettingForm) || (!settings.Init)) 
    { 
    // Создаем диалоговое окно
    using (SettingsForm settingsForm = new SettingsForm(settings, 
    4> GraphicsAdapter.DefaultAdapter))
    { 
    // Отображаем диалоговое
     окно
    settingsForm.ShowDialog();	
    // Если командная строка содержит  "/config
     " if (configParam) 
    { 
    // Завершаем работу приложения closing =true;
    Application.Idle += new EventHandler(ApplicationIdle); return; 
    } 
     } 
    }
    presentParams = new PresentationParameters();
     presentParams.BackBufferCount = 1;
     presentParams.SwapEffect = SwapEffect.Discard;
    // Настраиваем параметры визуализации согласно настройкам приложения
     presentParams.IsFullScreen = settings.FullScreen;
    if (settings.FullScreen) 
    {
    presentParams.BackBufferWidth = settings.Width;
    presentParams.BackBufferHeight = settings.Height;
    presentParams.BackBufferFormat = settings.Format;
    presentParams.FullScreenRefreshRateInHz = settings.RefreshRate; 
    }
    else 
    {
    presentParams.BackBufferWidth = 0;
    presentParams.BackBufferHeight = 0;
    presentParams.BackBufferFormat = SurfaceFormat.Unknown;
    presentParams.FullScreenRefreshRateInHz = 0; 
    }
    // Проверяем наличие аппаратных вершинных процессоров
    GraphicsDeviceCapabilities caps = GraphicsAdapter.
    DefaultAdapter.GetCapabilities( 
    DeviceType.Hardware);
    CreateOptions options = CreateOptions.SingleThreaded;
    if (caps.DeviceCapabilities.SupportsHardwareTransformAndLight)
    options |= CreateOptions.HardwareVertexProcessing; else
    options |= CreateOptions.SoftwareVertexProcessing;
    try 
    { 
    // Создаем графическое устройство
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
     DeviceType.Hardware,
     this.Handle,
    options, presentParams); 
    }
    catch (InvalidOperationException) 
    { 
    // Если при создании графического устройства возникли проблемы
     closing = true;
    Application.Idle += new EventHandler(ApplicationIdle); 
    // Отображаем диалоговое окно с предложением изменить параметры 
    визуализации
    if (MessageBox.Show("Ошибка при создании графического устройства.
     Изменить  " + 
    "параметры визуализации (разрешение экрана и т.п.)?", "Ошибка",
     MessageBoxButtons.YesNo, 
    MessageBoxIcon.Error) == DialogResult.Yes)
    { 
    // При положительном ответе устанавливаем параметр ShowSettingForm 
    в значение true, 
    // указывающий на необходимость показать окно с настройками 
    приложения при следующем запуске
    settings.ShowSettingForm = true; 
    // Сохраняем настройки приложения в файл
    settings.Save(); 
    // Перезапускаем приложение
    Process.Start(Application.ExecutablePath); 
    }
    return; 
    } 
    }
    private void MainFormLoad(object sender, EventArgs e)
    {
    // Загружаем настройки из файла и создаем графическое устройство
    InitGraphivsDevice(); 
    // Если приложение завершает работу, выходим из обработчика 
    события Load
    if (closing)
     return;
    // Остальные действие (создание декларации вершины, заполнение 
    массива вершин, загрузка
    // эффекта и т.п.
    ...
    }

    Остальные фрагменты приложения не содержат чего-либо заслуживающего внимания, и вы сможете легко их реализовать самостоятельно. Готовое приложение находится в example.zip в каталоге Examples\Ch03\Ex03.

    Местоположение файла настроек приложения

    .NET Framework 2.0 сохраняет пользовательские настройки в XML -файле user.config, который располагается по достаточно запутанному пути:

    <Profile Directory>\<Company Name>\<App 
    Name><Evidence Type><Evidence
    Hash>\<Version>\user.config

    где

  • Profile Directory - каталог локального профиля приложения, обычно имеющий название вроде c:\Documents and Settings\<Имя Пользователя>\Local Settings\Application Data
  • Company Name - строка, формируемая на основе названия компании, заданного атрибутом AssemblyCompany в файле Properties\AssemblyInfo.cs.
  • App Name - строка, формируемая на основе названия приложения, заданного атрибутом AssemblyProduct в файле Properties\AssemblyInfo.cs.
  • Evidence Type, Evidence Hash - вычисляются на основе информации о домене приложения.
  • Version - строка, формируемая на основе версии приложения, заданной атрибутом AssemblyVersion в файле Properties\AssemblyInfo.cs.
  • Например, на моем компьютере пример Сh03\Ex06 хранит информацию о конфигурации приложения в файле C:\Documents and Settings\Administrator\Local Settings\Application Data\GSPInc\Ex06-FullscreenDialog.Url31gvfqvzegievbxah0w1qmgu2s2siyo3\1.0.0.0\user.config. Сам файл имеет простую структуру и легко может быть проанализирован любым продвинутым пользователем:

    <?xml version="1.0" encoding="utf-8"?> <configuration>
    <userSettings>
    <GSP.XNA.Book.Ch03.Ex06.Properties.Settings>
    <setting name="Width" serializeAs="String">
    <value>1280</value>
    </setting>
    <setting name="Height"
    serializeAs="String">
    <value>960</value>
    </setting> 
    <setting name="Format"
    erializeAs="String">
    <value>Bgr32</value>
    </setting> 
    <setting name="RefreshRate"
    serializeAs="String">
    <value>85</value> </setting>
    <setting name="FullScreen"
    serializeAs="String">
    <value>True</value>
    </setting>
    <setting name="Init"
    serializeAs="String">
    <value>True</value> </setting>
    <setting name="ShowSettingForm"
    serializeAs="String">
    <value>False</value> </setting>
     </GSP.XNA.Book.Ch03.Ex06.Properties.Settings>
    </userSettings> </configuration>

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

    3.3. Анимация

    До сих пор мы визуализировали исключительно статичные изображение, в то время как XNA Framework в первую очередь предназначен для визуализация динамичных сцен с движущимися объектами. Как создать анимированную сцену? Обратимся к мультипликации и кинематографу, в которых эффект движения создается путем вывода на экран последовательности слегка различающихся изображений (кадров). Экспериментально было установлено, что для создания эффекта плавного движения частота смены кадров должна быть порядка 25-ти FPSFPS – Frames Per Second (кадры в секунду) .

    И так, для создания анимации мы должны отображать визуализировать различные фазы движения изображения с частотой 25 кадров в секунду. Наше первое приложение будет визуализировать шарик (точнее диск), летающий по форме и отскакивающий от ее стенок (рисунок 3.11). Анимация будет моделироваться посредством таймера (компонент Timer ), тикающего с интервалом 40 миллисекунд (то есть 25 раз в секунду). После каждого тика таймера мы будем прибавлять к координатам шарика значение вектора скорости и перерисовывать сцену. При пролете шарика сквозь стенку он будет отскакивать обратно, при этом вектор скорости будет изменяться на противоположный. Для придания движениям шарика некоторой неопределенности, модуль вектора скорости будет изменяться на незначительную случайную величину. Основные фрагменты приложения приведены в листинге 3.18. Исходный код примера находится в example.zip в каталоге Examples\Ch03\Ex07.

    (рис 3.18)  Прыгающий шарик (рис 3.11) public partial class MainForm : Form 
    {
    // Файл эффекта, используемый для визуализации изображения const
    string effectFileName = "Data\\ColorFill.fx";
    // Минимальная скорость диска ( "шарика
    ") const float diskMinSpeed = 0.5f;
    
    // Максимальная скорость диска
    const float diskMaxSpeed = 1.3f; 
    // Количество сегментов в диске
    const int diskSlices = 32; 
    // Радиус диска
    const float diskRadius = 0.1f; 
    // Цвет центра диска
    readonly static XnaGraphics.Color diskInnerColor = 
    XnaGraphics.Color.White; 
    // Цвет края диска
    readonly static XnaGraphics.Color diskOuterColor = 
    XnaGraphics.Color.Green;
    // Толщина стенки вдоль границы экрана
    const float borderSize = 0.1f; 
    // Цвет внутренней границы стенки
    readonly static XnaGraphics.Color borderInnerColor = 
    XnaGraphics.Color.DarkBlue; 
    // Цвет внешней границы стенки
    readonly static XnaGraphics.Color borderOuterColor = 
    XnaGraphics.Color.CornflowerBlue;
    GraphicsDevice device = null;
    PresentationParameters presentParams; Effect
    effect = null; VertexDeclaration decl =
     null;
    // Массив вершин стены вдоль края формы
    VertexPositionColor[] borderVerts = null; 
    // Массив вершин диска с центром в начале системы координат
    VertexPositionColor[] baseDiskVerts = null; 
    // Массив вершин диска, перемещаемого по поверхности экрана
    VertexPositionColor[] diskVerts = null;
    FillMode fillMode = FillMode.Solid;
    // Скорость диска вдоль оси X
    float speedX; 
    // Скорость диска вдоль оси Y
    float speedY; 
    // Координата X центра диска
    float posX = 0; 
    // Координата Y центра диска
    float posY = 0;
    // Генератор случайных чисел
    Random rnd = new Random();
    // Конфигурация приложения (разрешение экрана и т.п.)
     Properties.Settings settings;
    bool closing = false;
    // Вычисляет случайную скорость диска, лежащую в 
    диапазоне diskMinSpeed .. diskMaxSpeed float
     RndSpeed() 
    {
    return diskMinSpeed + (float)rnd.NextDouble() * 
    (diskMaxSpeed - diskMinSpeed); 
    }
    // Обработчик события Load главной формы
    private void MainForm_Load(object sender, EventArgs e)
    { 
    // Чтение файла конфигурации и создание графического устройства
     InitGraphivsDevice();
    if (closing) return;
    // Создание декларации вершины
    decl = new VertexDeclaration(device, 
    VertexPositionColor.VertexElements);
    // Создание и заполнение массива вершин,
     визуализируемого с использованием списка 
    // треугольников (PrimitiveType.TriangleStrip)
    borderVerts = new VertexPositionColor[10];
    borderVerts[0] = new VertexPositionColor(new 
    Vector3(-1.0f, -1.0f, 0.0f),
     borderOuterColor);
    borderVerts[1] = new VertexPositionColor(new 
    Vector3(-1.0f + borderSize, -1.0f + borderSize, 0.0f),
    borderInnerColor);
    borderVerts[2] = new VertexPositionColor(new 
    Vector3(-1.0f, 1.0f, 0.0f),
     borderOuterColor);
    borderVerts[3] = new VertexPositionColor(new 
    Vector3(-1.0f + borderSize, 1.0f -borderSize, 0.0f),
    borderInnerColor);
    borderVerts[4] = new VertexPositionColor(new 
    Vector3(1.0f, 1.0f, 0.0f),
     borderOuterColor);
    borderVerts[5] = new VertexPositionColor(new 
    Vector3(1.0f - borderSize, 1.0f -borderSize, 0.0f),
    borderInnerColor);
    borderVerts[6] = new VertexPositionColor(new 
    Vector3(1.0f, -1.0f, 0.0f),
     borderOuterColor);
    borderVerts[7] = new VertexPositionColor(new 
    Vector3(1.0f - borderSize, -1.0f + borderSize, 0.0f),
    borderInnerColor);
    borderVerts[8] = new VertexPositionColor(new 
    Vector3(-1.0f, -1.0f, 0.0f),
     borderOuterColor);
    borderVerts[9] = new VertexPositionColor(new
     Vector3(-1.0f + borderSize, -1.0f + borderSize, 0.0f),
    borderInnerColor);
    // Создание диска с центром в начале координат. 
    Диск визуализируется с использованием веера 
    // треугольников (PrimitiveType.TriangleFan)
    baseDiskVerts = new VertexPositionColor[diskSlices + 2];
    baseDiskVerts[0] = new VertexPositionColor(new 
    Vector3(0.0f, 0.0f, 0.0f),
     XnaGraphics.Color.White);
    for (int i = 0; i <= diskSlices; i++) {
    float angle = (float)i / (float)diskSlices * 2.0f * (float)Math.PI;
    float x = diskRadius * (float)Math.Sin(angle);
    float y = diskRadius * (float)Math.Cos(angle);
    baseDiskVerts[i + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f),
     diskOuterColor); };
    // Создаем массив вершин диска путем клонирования. 
    Таким образом, при старте приложения диск 
    // расположен в начале системы координат.
    diskVerts = (VertexPositionColor[]) baseDiskVerts.Clone();
    // Задаем начальную скорость диска вдоль осей X и Y speedX
     = RndSpeed(); speedY = RndSpeed();
     }
    // Обработчик события Paint главной формы
    private void MainForm_Paint(object sender, PaintEventArgs e) 
    { 
    ... 
    // Визуализируем сцену
    effect.Begin();
    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
    {
    pass.Begin();
    device.DrawUserPrimitives(PrimitiveType.TriangleStrip, borderVerts, 0,
     borderVerts.Length - 2);
    device.DrawUserPrimitives(PrimitiveType.TriangleFan, diskVerts, 0,
     diskVerts.Length - 2);
    pass.End(); 
    } effect.End();
    device.Present(); 
    ...
    }
    // Обработчик события Tick таймера, свойству Interval
     которого присвоено значение 40 private
     void timer_Tick(object sender, EventArgs e) 
    { 
    // Изменяем координаты центра диска на расстояние, 
    которое он должен пройти за время между 
    // двумя тиками таймера
    posX += speedX * (float)timer.Interval * 0.001f; posY +=
     speedY * (float)timer.Interval * 0.001f;
    // Если диск столкнулся с правым краем границы
    if (posX >= 1 - diskRadius - borderSize) 
    { 
    // Диск не должен перелетать за границу
    posX = 1 - diskRadius - borderSize; 
    // Изменяем направление движения диска на противоположное
     и вычисляем новую скорость. speedX
     = -Math.Sign(speedX) * RndSpeed(); 
    }
    // Если диск столкнулся с верхним краем границы
    if (posY >= 1 - diskRadius - borderSize) 
    {
    posY = 1 - diskRadius - borderSize;
    speedY = -Math.Sign(speedY) * RndSpeed(); 
    }
    // Если диск столкнулся с левым краем границы
    if (posX <= -1 + diskRadius + borderSize) 
    
    posX = -1 + diskRadius + borderSize;
    speedX = -Math.Sign(speedX) * RndSpeed(); 
    }
    // Если диск столкнулся с нижним краем границы
    if (posY <= -1 + diskRadius + borderSize)
     {
    posY = -1 + diskRadius + borderSize;
    speedY = -Math.Sign(speedY) * RndSpeed(); 
    }
    // Вычисляем новые координаты диска на основе  
    "эталонного " диска с центром 
    в начале 
    // координат.
    for (int i = 0; i < baseDiskVerts.Length; i++) 
    {
    diskVerts[i].Position.X = baseDiskVerts[i].Position.X + posX;
     diskVerts[i].Position.Y = baseDiskVerts[i].Position.Y + posY; 
    }
    // Перерисовываем изображение Invalidate(); 
     } 
    }

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

    Запустите приложение на выполнение. Первое что бросится в глаза – это движение окружности рывками. Но ведь этого не может быть! Фильмы то при частоте 25 fps идут очень плавно. В чем же принципиальная разница между фильмом и нашим приложением? Все очень просто. В кинематографе кадры снимается с некоторой экспозициейВремя, в течение которого объектив аппарата остается открытым при фото- и киносъемке . В результате каждый кадр фильма содержит усредненное изображение за период экспозиции, что проявляется в смазывании быстродвижущихся объектов. Таким образом, 25 FPS в кинофильме и 25 FPS в нашем приложении – это немного разные FPS. В частности, в компьютерных играх при частоте кадров 25-30 FPS ощущаются некие "подергивания " в динамичных сценах – объекты движутся как бы рывками. Эту проблему решают методом грубой силы, то есть простым увеличением частоты кадров – практический опыт показыва ет, что в большинстве случаев вполне достаточно частоты кадров 60 FPS.

    Дополнительная информация

    Для визуализации быстродвижущихся объектов вроде спортивного автомобиля, пролетающего мимо наблюдателя со скоростью 300 км/ч, даже частота кадров 60 FPS оказывается недостаточной. С другой стороны, при попытке наращивания FPS мы упираемся в возможности аппаратуры – максимальная частота вертикальной развертки современных мониторов (то есть частота смены кадров) редко превышает 60-100 Hz. Поэтому для повышения реалистичности движений применяют технологии Motion BlurЭффект размывания изображения быстро движущегося объекта 55, которые различными способами пытаются имитировать экспозицию при "киносъемке " кадров изображения.

    Ну что ж, попробуем увеличить частоту смены кадров. Как известно, компонент таймер может тикать до 64-х раз в секунду, т.е. минимальный интервал между двумя тиками равен 0.015 миллисекунд. Присвойте свойству Interval таймера значение 15 и снова запустите приложение. Шарик будет двигаться ощутимо плавнее, однако скорость не будет постоянной: периодически шарик будет ни с того то замедляться, то ускоряться.

    Почему это происходит? Для ответа на этот вопрос необходимо внимательно прочитать описания таймера Windows в MSDN. Хотя таймер и может тикать с интервалом 15 миллисекунд, точность каждого тика составляет 55 миллисекунд. Таким образом, по мере уменьшения интервала между тиками, точность таймера катастрофически падает, что приводит к неравномерности движения объектов. Но есть еще один немаловажный фактор – события от таймера имеют самый низкий приоритет среди всех сообщений. Например, если пользователь в это время нажимает клавишу клавиатуры, а в очереди сообщений находится необработанное сообщение WM_TIMER, то сообщения WM_KEYDOWN, WM_CHAR и WM_KEYUP будут вставлены в очередь перед сообщением WM_TIMER. По сути, сообщение WM_TIMER обрабатывается только при условии пустой очереди событий потока, а так как в очереди может находиться не более одного сообщения wmtimer, таймер может "терять " тики. Чтобы убедиться в этом, попробуйте ухватиться указателем мыши за край формы и поизменять ее размер, что тут же приведет к потере сообщений и, соответственно, замедлению шарика.

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

    3.3.1. Использование события Idle

    Итак, использование обычного компонента Timer не увенчалось успехом из-за ряда ограничений системного таймера Windows. Кроме того, использование таймера для визуализации сцены обладает еще одним фундаментальным ограничением. Дело в том, что, задавая определенную частоту визуализации кадров, мы делаем неявное предположение, что абсолютно любой компьютер может визуализировать сцену с требуемой частотой кадров. Если это вдруг окажется не так, приложение будет работать в "замедленном " режиме. Учитывая, что производительность видеоподсистем различных компьютеров может отличаться в десятки раз, возникновение подобной проблемы более чем вероятно.

    Что же делать? Раз визуализация сцены с фиксированной частотой кадров чревата возникновением множества проблем, надо просто визуализировать кадры с максимально возможной частотой, для чего достаточно поместить в обработчик события Idle вызов метода Invalidate. Для измерения временных интервалов между вызовами обработчика события Idle можно воспользоваться свойством System.Enviroment.TickCount, возвращающим количество миллисекунд, прошедших с момента загрузки операционной системы. Основные фрагменты нового варианта кода приведены в листинге 3.19.

    // Пример Examples\Ch03\Ex08
    public partial class MainForm : Form
    {
    // Значение свойства System.Enviroment.TickCount 
    во время последнего вызова обработчика
    // события Idle.
    int lastTick;
    private void MainForm_Load(object sender, EventArgs e)
    { ...
    // Запоминаем текущее значение свойства System.Enviroment.TickCount
     lastTick = Environment.TickCount;
    Application.Idle += new EventHandler(Application_Idle);
     }
    void Application_Idle(object sender, EventArgs e) 
    { 
    // Если приложение завершает работу, закрываем 
    главную форму. if (closing) 
    {
    Close(); return; }
    int currentTick = Environment.TickCount; 
    // Вычисляем время (в секундах), прошедшее между 
    двумя вызовами обработчика Idle float
     delta = (float)(currentTick - lastTick) * 0.001f;
    // Изменяем положение диска
    posX += speedX * delta; posY += speedY * delta;
    // Обрабатываем столкновение с стеной вдоль границы экрана 
    ...
    // Запоминаем текущее время с момента загрузки операционной системы
    lastTick = currentTick; 
    // Перерисовываем экран
    Invalidate(); 
    } 
    ... 
    }

    Шарик примера Ch03\Ex08 движется ощутимо плавнее, однако небольшие, еле заметные рывки все же остались. Для определения причины рывков вставьте в обработчик события Idle команду Trace.WriteLine(delta), запустите приложение, вернитесь в Visual Studio и посмотрите содержимое окна Output. Скорее всего, его содержимое будет выглядеть примерно следующим образом:

    delta	=	0,078
    delta	=	0
    delta	=	0
    delta	=	0,016
    delta	=	0,015
    delta	=	0
    delta	=	0,016
    delta	=	0,015
    delta	=	0,016
    delta	=	0
    delta	=	0,016

    Обратите внимание на множество нулевых значений параметра delta, соответствующих полной остановке шарика на месте, которая воспринимается пользователем как небольшое подергивание. Почему между некоторыми вызовами события Idle проходит 15-16 миллисекунд, а между другими – меньше одной миллисекунды. Подобный разброс производительности не выглядит правдоподобным, значит дело в чем-то ином. Для выяснения причин этой аномалии придется обратиться к MSDN. Метод Environment.TickCount использует функцию Win32 GeTickCount, точность которой порядка 10 миллисекунд. Соответственно, при попытке измерить временной интервал близкий к 10 миллисекундам метод Environment.TickCount может вернуть два одинаковых значения, что мы и наблюдаем.

    3.3.2. Использование высокоточного таймера Stopwatch

    Так как точность измерения времени посредством свойства Environment.TickCount зачастую оказывается недостаточной, Microsoft по многочисленным просьбам трудящихся добавил в .NET Framework 2.0 класс System.Diagnostics.Stopwatch, предназначенный для высокоточного измерения временных интервалов. Данный класс весьма прост в использовании. Сначала приложение должно создать экземпляр класса Stopwatch и запустить таймер методом Start. Затем по мере необходимости приложение посредством свойства ElapsedMilliseconds получает количество миллисекунд, прошедшее с момента запуска таймера. Когда же все измерения времени выполнены, приложение останавливает таймер командой Stop.

    Для оценки точности таймера Stopwatch можно воспользоваться статическим свойством Frequency, возвращающее количество тиков высокоточного таймера, используемого классом Stopwatch для измерения временных интервалов, за 1 секунду. На моем компьютере свойство Frequency равно 1.870.000.0000, что соответствует фантастической точности $$1/frac18700000000 = 5?10^{-10}сек$$.

    Так как метод ElapsedMilliseconds по определению не может изменять интервалы меньше 1 миллисекунды, в классе Stopwatch имеется один метод ElapsedTicks, возвращающий количество тиков таймера. Для перевода тиков в секунды значение свойства ElapsedTicks надо поделить на Stopwatch.Frequency. При этом следует всегда помнить о том, что при измерении сверхкоротких интервалов появляются иные погрешности, вызванные переключением задач, кэш промахами, длинным конвейером процессора, накладными затратами на вызовы функций и т.п., в результате чего предельная точность измерений таймера Stopwatch редко достижима на практике.

    Код приложения, переписанный с использованием таймера Stopwatch приведен в листинге 3.20.

    // Пример Examples\Ch03\Ex09
    public partial class MainForm : Form
    {
    // Высокоточный таймер
    Stopwatch stopwatch; 
    // Время, прошедшее с момента запуска таймера при
     последнем вызове обработчика события
     // Idle
    long lastTime;
    private void MainForm_Load(object sender, EventArgs e) 
    { 
    ... 
    // Запускаем таймер
    stopwatch = new Stopwatch();
    stopwatch.Start();
    lastTime = 0;
    // Выводим в окно Output точность таймера
    Trace.WriteLine("Accuracy = " + 1.0 / Stopwatch.Frequency + " sec");
     Application.Idle += new EventHandler(Application_Idle); 
    }
    void Application_Idle(object sender, EventArgs e) 
    { 
    // Если приложение завершает работу, закрываем главную форму. If
     (closing) 
    {
    Close(); return; }
    // Получаем количество миллисекунд, прошедших с 
    момента запуска таймера
    double currentTime = (double)stopwatch.ElapsedTicks / 
    (double)Stopwatch.Frequency;
    // Вычисляем время (в секундах), прошедшее между двумя 
    вызовами обработчика события Idle
    ` float delta = (float)(currentTick - lastTick);
    // Изменяем положение диска
    posX += speedX * delta; posy
     += speedY * delta;
    // Обрабатываем столкновение с краями экрана 
    ...
    // Запоминаем текущее показание таймера
    lastTime = currentTime; 
    // Перерисовываем экран 
    Invalidate(); 
    } 
    ... 
    }

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

    Accuracy = 5,3475935828877E-10 sec
    delta =	0,035
    delta =	0,006
    delta =	0,003
    delta =	0,002
    delta =	0,011
    delta =	0,012
    delta =	0,012
    delta =	0,012
    delta =	0,011
    delta =	0,013
    delta =	0,011

    Примечание

    Обратите внимание на ощутимую задержку при визуализации первого кадра, обусловленную накладными расходами JIT -компилятора при компиляции метода Paint.

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

    Accuracy = 5,3475935828877E-10 sec
    delta =	0,035
    delta =	0,022
    delta =	0,013
    delta =	0,032
    delta =	0,015
    delta =	0,032
    delta =	0,014
    delta =	0,032
    delta =	0,016
    delta =	0,032
    delta =	0,014
    delta =	0,016
    delta =	0,019

    Все дело в вертикальной синхронизации, которая будет рассмотрена в следующем разделе.

    3.3.3 Управление вертикальной синхронизацией

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

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

    Для борьбы с этим недоразумением XNA Framework предоставляет программисту возможность управления синхронизацией переключения заднего и экранного буферов с моментом окончания формирования электронным лучом изображения на экране монитора. Эта функциональность доступна посредством свойства PresentationInterval класса PresentationParameters:

    public PresentInterval PresentationInterval { get; set; }

    В подавляющем большинстве случаев свойству PresentationInterval присваивают одно из следующих трех значений перечислимого типа PresentInterval:

  • PresentInterval.Default - значение по умолчанию, аналогичное PresentInterval.One.
  • PresentInterval.One - буферы переключаются только по окончанию обновления электронным лучом изображения на экране монитора.
  • PresentInterval.Immediate - кадровые буферы переключаются так быстро, насколько это возможно.
  • Хотя значения PresentInterval.Default и PresentInterval.One формально являются братьями-близнецами, между ними есть одна очень тонкая разница. При использовании PresentInterval.Default синхронизации с обратным ходом луча осуществляется посредством обычного таймера Windows, который, как вы помните, обладает очень низкой точностью. Из-за особенностей организации оконной подсистемы Windows низкая точность таймера приводит к частым задержкам при переключении буферов и, соответственно, "рывкам " при анимации. В полноэкранном режиме последствия, как правило, не столь серьезны.

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

    Итак, однозначно правильного режима смены кадров не существует, поэтому было бы разумным предоставить этот выбор пользователю. Для этой цели я поместил на форму диалогового окна Параметры дополнительный флажок Вертикальная синхронизация ( vsynchCheckBox ), установка которого соответствует использованию режима PresentInterval.One, а снятие - PresentInterval.Immediate (рисунок 3.12). Для сохранения значения режима вертикальной синхронизации в настройки приложения было добавлено дополнительное поле PresentationInterval типа Mcrosoft.Xna.Framework.Graphics.PresentInterval, а сам код приложения подвергся косметической доработке (листинг 3.21). Готовое приложение находится в example.zip в каталоге Examples\Ch03\Ex10.

    (рис 3.21)  Диалоговое окно Параметры с новым флажком Вертикальная синхронизация (рис 3.12) // Класс главной формы приложения public
     partial class MainForm : Form 
    {
    void InitGraphivsDevice() 
    { 
    ...
    presentParams = new PresentationParameters();
    presentParams.BackBufferCount = 1;
    presentParams.SwapEffect = SwapEffect.Discard;
    presentParams.IsFullScreen = settings.FullScreen;
    // Устанавливаем режим вертикальной синхронизации 
    согласно настройкам приложения
    presentParams.PresentationInterval = settings.PresentationInterval; 
    ...
    }
     ...
     }
    // Класс диалогового окна  "Параметры "
    public partial class SettingsForm : Form
    {
    // Конструктор диалогового окна
    internal SettingsForm(Properties.Settings settings, 
    GraphicsAdapter adapter) 
    { 
    ...
    // Устанавливает значение флажка  "Вертикальная 
    синхронизация " согласно настройкам
     приложения if (settings.PresentationInterval == PresentInterval.One)
    vsynchCheckBox.Checked = true;
     else
    vsynchCheckBox.Checked = false; 
    }
    // Обработчик нажатия кнопки Ok
    private void okButton_Click(object sender, EventArgs e) 
    { 
    ...
    // Сохраняем в настройках приложения информацию о
     режиме кадровой синхронизации if
     (vsynchCheckBox.Checked)
    settings.PresentationInterval = PresentInterval.One;
     else
    settings.PresentationInterval = PresentInterval.Immediate; 
    ...
    } 
    }

    Запустите приложение и попробуйте поэкспериментировать с настройками кадровой синхронизации, наблюдая за окном Output. При включенной вертикальной синхронизации интервал между визуализацией кадров будет примерно равен константе 1.0/{частота обновления экрана текущего видеорежима}. При отключении вертикальной синхронизации частота интервал смены кадров уменьшится до величины сопоставимой с 1 мс, что соответствует частоте порядка 1000 fps. При этом и в том и в другом случае движения шарика будут плавными.

    3.3.4. Замена циклов foreach на for

    При визуализации диска проходы ( Passes ) эффекта перебираются посредством цикла foreach.

    Достоинствами цикла foreach является простота кода и защита от потенциальных ошибок вроде использования неправильного индекса при обращении к элементу коллекции. Обратной стороной является неявное использование циклом foreach специального типа, реализующего интерфейс Enumerator. Например, код

    List<int> numbers = new List<int>(3);
    ...
    int s = 0;
    foreach (int v in numbers) s += v;

    неявно заменяется компилятором следующим аналогом:

    List<int> numbers = new List<int>(3);
    ...
    int s = 0;
    // Создается объект enumerator
    IEnumerator enumerator = numbers.GetEnumerator(); 
    // Перебирает элементы коллекции while
     (enumerator.MoveNext())
    // Получает доступ к текущему элементу коллекции s
     += (int)enumerator.Current;

    На первый взгляд, данный код является неэффективным. Но в действительности все не так уж и плохо. Во-первых, метод GetEnumerator возвращает структуру:

    public class List<T> : IList<T>, 
    ICollection<T>, IEnumerable<T>, IList,
    ICollection,
    IEnumerable
    {
    public Enumerator<T> 
    GetEnumerator() {
    return new Enumerator<T>((List<T>)
     this); }
    ...
    public struct Enumerator : IEnumerator<T>, IDisposable,
     IEnumerator 
    {
    private List<T> list;
    private int index;
    private int version;
    private T current;
    internal Enumerator(List<T> list);
    public void Dispose();
    public bool MoveNext();
    public T Current { get; }
    object IEnumerator.Current { get; }
    void IEnumerator.Reset(); }
    }

    Соответственно, вызов метода GetEnumerator не приводит к выделению памяти в управляемой куче и учащению вызовов сборщика мусора. Во-вторых, компилятор C# вызывает метод MoveNext и свойство Current структуры Enumerator напрямую без приведения к ссылке на интерфейс IEnumerator, так что боксирование ( boxing ) структуры Enumerator не происходит. И, наконец, компилятор может встроить ( inline ) код метода MoveNext и свойства Current непосредственно в код цикла, избежав накладных расходов на вызовы методов. Так что при использовании массивов или коллекций наподобие List разница в производительности циклов foreach и for будет исключающее мала, и в подавляющем большинстве случаев на нее можно не обращать внимания.

    Однако при использовании других коллекций не все так просто. Например, у некоторых объектов метод GetEnumerator возвращает объект, размещаемых в управляемой куче. Подобный подход имеет два недостатка:

  • Частый вызов цикла foreach для перебора элементов такой коллекции приведет к созданию множества объектов enumerator, и, соответственно, частому вызову сборщика мусора, что привет к падению производительности.
  • Если метод MoveNext является виртуальным, то компилятор не сможет встроить его непосредственно в код цикла, что тоже негативно скажется на производительности.
  • Таким образом, в критичных к производительности приложениях при переборе элементов коллекций, возвращающих объект enumerator, имеет смысл избегать циклов foreach.

    Но давайте вернемся к коллекции Passes класса Effect. Данная коллекция реализуется классом EffectPassCollection, объявленным следующим образом:

    public sealed class EffectPassCollection :
     IEnumerable<EffectPass> 
    {
    // Список для хранения информации о проходах
     private List<EffectPass> pPass;
    // Возвращает объект (полученный путем боксирования 
    структуры), реализующий интерфейс 
    // IEnumerator<EffectPass>
    public IEnumerator<EffectPass> GetEnumerator()
    {
    return (IEnumerator<EffectPass>) this.pPass.GetEnumerator();
    } 
    }

    Давайте внимательно рассмотрим этот код. Класс EffectPassCollection в действительности хранит информацию о проходах в коллекции List. Соответственно метод this.pPass. GetEnumerator возвращает ссылку на структуру Enumerator класса List, определение которой было приведено в начале раздела.

    И все бы было просто замечательно, если бы не один маленький нюанс - структура IEnumerator приводится к интерфейсу IEnumerator<EffectPass>, что приводит к боксированию и выделению памяти в управляемой куче. Чтобы оценить влияние этой особенности на производительность приложения, мы встроим в обработчик события Paint код, перебирающий элементы коллекции 10.000.000 раз посредством циклов for и foreach с измерением времени их выполнения (листинг 3.22).

    // Пример Examples\Ch03\Ex11 #define TEST
    #if TEST
    bool testing = true;
    #endif
    private void MainForm_Paint(object sender, PaintEventArgs e)
    {
    // Измерение выполняется только при условии определения 
    идентификатора TEST
    #if TEST
    // Тест выполняется только один раз
    if (testing)
    { 
    // Число итераций
    const int n = 10000000;
     int sum;
    // Выводим количество сборок мусора, выполненных на момент 
    начала эксперимента
     Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
    Stopwatch timer = new Stopwatch(); 
    // Запускаем таймер
    timer.Start();
    // Перебираем элементы коллекции passes и суммируем коды первой буквы
     sum = 0;
    EffectPassCollection passes = effect.CurrentTechnique.Passes; 
    // Выполняем n итераций
    for (int i = 0; i < n; i++) 
    // Перебираем элементы коллекции passes посредством 
    for и суммируем коды первой буквы for
     (int j = 0; j < passes.Count; j++) sum += (int)passes[j].Name[0];
    // Вычисляем суммарное время, затраченное на интеграции
    double time1 = (double)timer.ElapsedTicks / (double)Stopwatch.Frequency;
    // Отображаем отчет
    Trace.WriteLine(" 	 for 	 ");
    Trace.WriteLine("Sum : " + sum.ToString());
    Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
    Trace.WriteLine("Time1 : " + time1);
    Trace.WriteLine("");
    // Перезапускаем таймер
     timer.Reset(); timer.Start();
    sum = 0; 
    // Снова выполняем итерации, но уже посредством цикла foreach for
     (int i = 0; i < n; i++)
    foreach (EffectPass pass in effect.CurrentTechnique.Passes) sum 
    += (int)pass.Name[0];
    // Вычисляем суммарное время, потраченное на итерации
    double time2 = (double)timer.ElapsedTicks / 
    (double)Stopwatch.Frequency;
    // Отображаем отчет
    Trace.WriteLine(" 	 foreach 	 ");
    Trace.WriteLine("Sum : " + sum.ToString());
    Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
    Trace.WriteLine("Time2 : " + time2); 
    // Определяем разницу в 
    производительности
    Trace.WriteLine("Time2 / Time1 = " + time2 / time1);
    Trace.WriteLine("");
    testing = false; }
    #endif 
    }

    Запустив модифицированное приложение на своем компьютере с процессором Intel Pentium-4 2.8C я получил следующие результаты:

    Accuracy = 3,5730747380043E-10 sec
    Gen 0 collection count: 3
    	 for 	
    Sum : 1120000000
    Gen 0 collection count: 3
    Time1 : 0,554915015846586
    	 foreach 	
    Sum : 1120000000
    Gen 0 collection count: 915
    Time2 : 1,84470303389776
    Time2 / Time1 = 3,32429828211344

    Как видно, в процессе работы цикла foreach сборщик мусора был вызван 915 разРезультаты, полученные на других компьютерах, могут заметно отличаться. Так на моем втором домашнем компьютере с процессором Intel Core2 Due E6300 при выполнении циклов foreach сборщик мусора был вызван 229 раз. В целом же, частота вызовов сборщика мусора в первую очередь зависит от размера кэша второго уровня CPU. Например, размер кэша процессора Intel Core2 Due E6300 (2MB) в четыре раза больше, чем у Pentium-4 2.8C (512KB). Соответственно, сборщик мусора на Intel Core2 Due E6300 вызывается в 4 раза реже по сравнению с Pentium-4 2.8C (915/229 = 3.996)., а разница в производительности между циклами for и foreach достигла трехкратной величины. Таким образом, перебор элементов коллекции foreach в методе Paint, вызываемом около сотни раз в секунду, является не самой лучшей идеей. Особенно если учесть, что реальные приложения зачастую содержат десятки эффектов, а внезапная сборка мусора при визуализации кадра может привести заметному провалу производительности.

    Примечание

    На самом деле Microsoft здорово поработала над эффективностью сборщика мусора, и сейчас сборка мусора в поколении 0 обычно занимает порядка 1 мс. В частности, пренебрегая накладными затратами цикла foreach, можно прикинуть, что в нашем эксперименте каждая сборка мусора выполнялась в среднем не более чем за (1.84 - 0.55) / 915 = 0.0014 секунд. Тем не менее, рано или поздно частые сборки мусора могут спровоцировать сбор мусора в старших поколениях, который будет воспринят пользователем как внезапное "подтормаживание " приложения.

    3.3.5. Устранение зависимости движений диска от производительности компьютера

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

    В принципе для ликвидации данного недостатка можно было бы найти аналитическое выражение зависимости координат центра диска от времени. Однако такое решение будет пригодно лишь для самых простых случаев. Поэтому мы реализуем более универсальный подход: моделирование движений диска с некоторым фиксированным шагом, например, 0.005 секунды. Реализация данной технологии потребует лишь косметической правки обработчика события Idle (листинг 3.23).

    // Дискретный шаг времени (в секундах), с которым выполняются расчеты
    const float timeStep = 0.005f;
    // Максимальный временной интервал между двумя
     вызовами обработчика события Idle (в секундах)
    const float maxDelta = 5.0f;
    void Application_Idle(object sender, EventArgs e)
    {
    ...
    // Определяем текущее время
    double currentTime = (double)stopwatch.ElapsedTicks /
     (double)Stopwatch.Frequency;
    // Если время между двумя вызовами обработчика Idle 
    превышает maxDelta, корректируем lastTime 
    // во избежание слишком длинной работы обработчика события 
    Idle if (currentTime - lastTime > maxDelta) 
    lastTime = currentTime - maxDelta;
    
    // Моделируем движения шарика дискретным шагом времени timeStep 
    while (lastTime + timeStep < currentTime) 
    {
    posX += speedX * timeStep; 
    posY += speedY * timeStep;
    lastTime += timeStep; }
    Invalidate(); 
    }

    В самых запущенных случаях задержка между визуализацией кадров может достигать минуты и даже больше. Так как моделирование игровой логики интервала времни в несколько минут может занять достаточно существенно время, в приложении используется простой прием - игровая логика рассчитывается не более чем за 5 секунд игрового времени (константа maxDelta ). Для пользователя подобный трюк является незаметным, ведь он все равно не сможет спрогнозировать поведение приложения на значительный временной интервал.

    Примечание

    В интерактивных приложениях вроде автосимуляторов при возникновении длительной задержки между кадрами имеет смысл приостановить игровой процесс, ведь в течение задержки в несколько секунд автомобиль игрока наверняка слетит с трассы или столкнется с препятствием. Эффекта автоматической приостановки игрового процесса можно достичь путем присвоения константе maxDelta небольшого значения порядка 0.2 - 0.5 секунд.

    3.4. Визуализация полупрозрачных примитивов

    В компьютерной графике довольно часто приходится моделировать различные полупрозрачные объекты вроде стекла, воды, тумана и т.п. Полупрозрачность - это характеристика объекта, показывающая, какую часть света пропускает среда (объект) без изменения направления его распространения. Так полностью прозрачный объект пропускает через себя весь свет, в результате чего не оказывает никакого влияния на находящиеся позади него объекты (иными словами, он является невидимкой). Непрозрачный объект напротив не пропускает через себя свет, и соответственно полностью перекрывает находящиеся позади него объекты. К слову, все наши объекты до сих пор являлись полностью не прозрачными.

    Коэффициент поглощения ? определенной среды есть количественная мера, позволяющая оценить, какая доля светового, падающего на поверхность раздела сред, проходит дальше, а какая поглощается. Например, если стекло имеет коэффициент поглощения 0.2, то результирующий цвет будет представлять собой объединение 20% цвета стекла и 80% цвета объектов позади стекла. То есть наблюдатель будет видеть как сам объект, так и предметы позади объекта.

    В общем случае итоговый цвет полупрозрачного объекта может быть определен по формуле:

    $$C=c_s-\alpha+c_d-(1-\alpha) $$

    где

    $$c$$ - результирующий цвет

    $$c_s$$ - цвет полупрозрачного объекта

    $$c_d$$ - цвет фона позади объекта

    $$\alpha$$ - коэффициент поглощения, равный 0 для полностью прозрачных объектов, и 1 для непрозрачных.

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

    3.4.1. Смешивание цветов

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

    (рис 3.13) Принцип работы блоков ROP

    Примечание

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

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

    $$\qquard d’=op(b-c,c-d) $$

    где

    $$\qquard d'$$ - новый цвет пикселя кадрового буфера $$[\qquard d'_r,\qquard d'_g,\qquard d'_b,\qquard d'_a] $$

    $$op$$ - операция (см. таблица 3.5)

    $$s$$ - цвет пикселя $$[s_r, s_g, s_b, s_a ] $$

    $$d$$ - текущий цвет пикселя кадрового буфера $$[d_r,d_g,d_b,d_a]$$

    $$b, с$$ - векторные коэффициенты $$[b_r,b_g,b_b,b_a]$$ и $$[c_r,c_g,c_b,c_a] $$

    Примечание

    Наряду с операциями смешения блоки ROP могут выполнять множество других полезных простых операций над пикселями вроде отсечения невидимых областей объектов при визуализации трехмерных цен, реализовывать полноэкранное сглаживание ( FSAA - Full Scene Anti Aliasing ) и т.д.

    Управление смешением пикселей осуществляется посредством свойств свойства RenderState экземпляра класса GraphicsDevice. Активация режима смешения осуществляется путем присвоения значения true свойству AlphaBlendEnable.

    public bool AlphaBlendEnable { get; set; }

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

    public BlendFunction BlendFunction { get; set; }
    Значения перечислимого типа BlendFunction
    Значение Описание
    Add (значение по умолчанию) Покомпонентно складывает два аргумента
    Subtract Покомпонентно вычитает из первого аргумента (цвет пикселя) второй (цвет кадрового буфера).
    ReverseSubtract Покомпонентно вычитает из второго аргумента (цвет кадрового буфера) первый (цвет пикселя).
    Max Покомпонентно сравнивает оба аргумента и возвращает компоненты с наибольшим значением
    Min Покомпонентно сравнивает оба аргумента и возвращает компоненты с наименьшим значением

    Каждый из аргументов операции предварительно умножается на коэффициент, определяемый посредством перечислимого типа Blen (таблица 3.6). При этом коэффициент $$b$$ задается свойством RenderState.SourceBlend, а коэффициент c – свойством RenderState.DestinationBlend:

    public Blend SourceBlend { get; set; } public 
    Blend DestinationBlend { get; set; }
    Некоторые значения перечислимого типа Blend
    Значение Описание
    BlendFactor В качестве коэффициента используется вектор, присвоенный свойству RenderState.BlendFactor
    InverseBlendFactor Коэффициент получается путем вычитания из единичного вектора (1, 1, 1, 1) значения свойства RenderState.BlendFactor.
    Zero Коэффициент равен вектору (0, 0, 0, 0).
    One Коэффициент равен вектору (1, 1, 1, 1).
    SourceColor В качестве коэффициента используются цвета примитива $$[s_r , s_g ,s_b, s_a ] $$
    InverseSourceColor В качестве коэффициента используется вектор $$[1-s_r,1-s_g,1-s_b,1-s_a] $$
    SourceAlpha В качестве коэффициента используется вектор альфа каналов примитива $$[s_a ,s_a ,s_a ,s_a ] $$.
    InverseSourceAlpha В качестве коэффициента используется вектор $$[1-s_a,1-s_a,1-s_a,1-s_a] $$.
    DestinationColor В качестве коэффициента используется цвет пикселя кадрового буфера $$[d_r ,d_g ,d_b,d_a ] /$$.
    InverseDestinationColor В качестве коэффициента используется вектор [1-d_r,1-d_g,1-d_b,1-d_a].
    DestinationAlpha В качестве коэффициента используется вектор альфа-каналов пикселя кадрового буфера $$[d_a,d_a,d_a,d_a ] $$.
    InverseDestinationAlpha В качестве коэффициента используется вектор $$[1-d_a,1-d_a ,1-d_a,1-d_a] $$.
    SourceAlphaSaturation Вектор коэффициентов равен [f,f,f,1], где $$f = min(s_a,d_a) $$.

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

    $$\qquard d’_r=(1-s_r)*s_r-s_a*d_r\\ \qquard d’_g=(1-s_g)*s_g-s_a*d_g\\ \qquard d’_b=(1-s_b)*s_b-s_a*d_b $$

    Значение альфа канала нас не интересует, так как он все равно не учитывается при выводе изображения на экран. Вдобавок, часто используемые форматы пикселей SurfaceFormat.Bgr565 и SurfaceFormat.Bgr32 не содержат альфа канала.

    Для начала давайте определим, какой операцией связаны между собой цвет примитива и цвет кадрового буфера. Во всех трех выражениях из произведения с множителями $$s_r ,s_g ,s_b$$ вычитается произведение, содержащее множители $$d_r ,d_g ,d_b,$$ следовательно для реализации этой формулы необходимо использовать операцию вычитания BlendFunction.Subtract.

    Перейдем к коэффициентам. Сопоставив коэффициенты перед $$s_r ,s_g ,s_b$$ с таблицей 3.6 мы придем к выводу, что они соответствуют значению Blend.InverseSourceColor. Все коэффициенты перед $$d_r ,d_g ,d_b$$ равны $$s_a,$$ следовательно они могут быть заданы с использованием константы Blend.SourceAlpha.

    Таким образом, программирование блоков ROP для смешения цветов по формуле 3.3 реализуется посредством следующих четырех строчек кода:

    device.RenderState.AlphaBlendEnable = true;
    device.RenderState.BlendFunction = BlendFunction.Subtract; 
    device.RenderState.SourceBlend = Blend.InverseSourceColor
     device.RenderState.DestinationBlend = Blend.SourceAlpha;

    Дополнительная информация

    Хотя по умолчанию XNA Framework применяет для смешения всех цветовых компонентов общие выражения, он так же позволяет использовать раздельные выражения для смешивания RGB и Alpha компонентов цвета. Эта функциональность активируется посредством присвоения свойству RenderState.SeparateAlphaBlendEnabled значения true, после чего вышеописанные свойства RenderState.BlendFunction, RenderState.SourceBlend и RenderState.DestinationBlend будут влиять исключительно на R, G и B составляющие цвета. Режим смешения альфа-компоненты цвета управляется аналогичной тройкой свойств: RenderState.AlphaBlendOperation, RenderState.AlphaSourceBlend, RenderState.AlphaDestinationBlend. Так как раздельное смешения каналов применяется достаточно редко, мы пока не будет акцентировать на нем внимание.

    3.4.2. Использование смешивания цветов для реализации эффекта полупрозрачности

    Для реализации визуализации полупрозрачных примитивов, мы должны переложить выражение 3.1 на причудливый язык программирования блоков ROP из XNA Framework. Для простоты мы положим, что весь примитив имеет одну и ту же прозрачность, что позволит нам использовать для задания коэффициента непрозрачности параметр RenderState.BlendFactor. Преимуществом такого подхода является возможность изменения прозрачности всех вершин примитива посредством коррекции одного единственного параметра. Само выражение 3.1 содержит операцию сложения и два коэффициента непрозрачности, так что оно легко реализуется средствами XNA Framework:

    // Задаем коэффициент непрозрачности (0 – 
    абсолютно прозрачен, 255 – полностью непрозрачен).
    // При присвоении свойству RenderState.BlendFactor 
    он автоматически приводится к диапазону
    // 0..1 путем деления на 255.
    const byte opacity = 50;
    // Включаем режим смешивания цветов
    device.RenderState.AlphaBlendEnable = true;
    // Используем операцию сложения
    device.RenderState.BlendFunction = BlendFunction.Add;
    // Коэффициент смешения всех трех компонентов цвета 
    равен opacity (точнее opacity / 255)
    device.RenderState.BlendFactor = new XnaGraphics.Color
    (opacity, opacity, opacity, 0);
    // Коэффициент, на который умножается цвет примитива,
     равен opacity / 255
    device.RenderState.SourceBlend = Blend.BlendFactor;
    // Коэффициент, на который умножается цвет кадрового 
    буфера, равен 1 - opacity / 255
    device.RenderState.DestinationBlend = Blend.InverseBlendFactor;

    Для демонстрации использования эффекта полупрозрачности мы строим в решение практического упражнения 3.1 ( Examples\Ch03\Ex02 ) возможность управления прозрачностью круга (рисунок 3.14). Для этого необходимо добавить в группу Параметры ползунок ( TrackBar ) и две метки, свойства которых перечислены в таблице 3.7.

    (рис 3.14) Приложение, визуализирующее полупрозрачный круг
    Свойства новых элементов управления группы Параметры
    Класс Свойство Значение
    TrackBar Name opacityTrackBar
    Minimum 0
    Maximum 255
    TickFrequency 0
    Label Name opacityLabel
    Label Text Непрозрачность

    Так же мне необходимо реализовать обработчик события Scrol ползунка и немного подправить обработчик события Paint (листинг 3.24). Готовый проект приложения находится в example.zip в каталоге Examples\Ch03\Ex13.

    private void opacityTrackBarScroll(object sender, EventArgs e)
    {
    // Отображаем текущий коэффициент непрозрачности
    opacityLabel.Text = ((float)opacityTrackBar.Value / 255.0f).
    ToString("0.00");
    // Перерисовывает компонент xnaPanel
    xnaPanel.Invalidate();
     }
    private void xnaPanel_Paint(object sender, PaintEventArgs e)
    {
    ...
    // Рисуем шахматную доску (код визуализации шахматной доски 
    взят из пример Ch01\Ex03
    // (см. раздел 1.2).
    ...
    device.RenderState.CullMode = CullMode.None;
     device.RenderState.PointSize = 3;
    device.RenderState.AlphaBlendEnable = true;
    device.RenderState.BlendFunction = BlendFunction.Add; 
    // Значение параметра BlendFactor вычисляется на основе
     текущего положения ползунка прокрутки
     device.RenderState.BlendFactor = new XnaGraphics.
     Color((byte)opacityTrackBar.Value,
    (byte)opacityTrackBar.Value, (byte)opacityTrackBar.Value, 0);
     device.RenderState.SourceBlend = Blend.BlendFactor; 
    device.RenderState.DestinationBlend = Blend.InverseBlendFactor;
    // Визуализируем круг
    ...
    }

    3.4.3. Анимация построения фигуры Лиссажу

    В следующем примере мы создадим приложение, анимирующее построение фигуры Лиссажу, заданной выражением

    $$x = \sin(2-a) y =\cos(3-a) $$

    где

  • x, y - координаты текущей точки
  • а - угол, пробегающий с определенным шагом значения от 0 до 360 градусов (0…2·?)
  • Вначале все пиксели фигуры будут прозрачными. Затем мы начнем постепенно увеличивать непрозрачность вершин фигуры, при этом непрозрачность всех вершин будет расти неравномерно: первыми непрозрачными станут вершины, соответствующие углу а равному 0, а последними - а равному 360 градусов. Соответственно, фигура Лиссажу будет как бы рисоваться как бы невидимым пером (рисунок 3.15).

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

    device. RenderState.AlphaBlendEnable = true;
    device.RenderState.BlendFunction = BlendFunction.Add;
    device.RenderState.SourceBlend = Blend.SourceAlpha;
    device.RenderState.DestinationBlend = Blend.InverseSourceAlpha;

    Чтобы облегчить задачу мы воспользуемся в качестве отправной точки решением практического упражнения 2.5. Все что от нас требуется - переписать обработчик события Idle и немного подправить обработчики событий Load и Paint формы (листинг 3.25).

    (рис 3.25)  Построение фигуры Лиссажу (рис 3.15) // Пример Examples\Ch03\Ex14
    public partial class MainForm : Form
    {
    // Количество сегментов в кривой
    const int QuadStrips = 200; 
    // Количество треугольников в кривой
    * 2;
    const int TriStrips = QuadStrips 
    ... 
    // Массив вершин фигуры Лиссажу
    VertexPositionColor[] vertices = null; 
    // Таймер
    Stopwatch stopwatch = null; 
    ...
    private void MainForm_Load(object sender, EventArgse)
    {
    //
    Создаем массив вершин объекта
    vertices = new VertexPositionColor[TriStrips + 2];
    // Перебираем все вершины фигуры Лиссажу
    for (int i = 0; i <= QuadStrips; i++) 
    { 
    // Вычисляем текущий угол
    float angle = 2.0f * (float)Math.PI * (float)i 
    // Рассчитываем координаты текущей точки фигуры 
    Лиссажу float cos = (float)Math.Cos(angle);
     float x = 0.85f * (float)Math.Sin(2 * angle);
    / (float)QuadStrips;
    float y = 0.85f * (float)Math.Cos(3 * angle); 
    // Вычисляем цвет точки
    byte green = (byte)(Math.Pow(0.5f + cos * 0.5f, 0.3f) * 255.0f); byte
     red = (byte)(Math.Pow(0.5f - cos * 0.5f, 0.3f) * 255.0f);
    /// Рассчитываем вектор, перпендикулярный графику фигуры Лиссажу длиной 
    0.015 float sx = -2.55f * (float)Math.Sin(3 * angle); float sy =-
    1.7f *(float)Math.Cos(2 * angle); float length =
     (float)Math.Sqrt(sx * sx + sy * sy); float nx = sx / length *
     0.015f; float ny = sy / length * 0.015f;
    // Заносим в массив информацию о двух вершинах 
    фигуры Лиссажу, смещенных на 0.015 в
    // направление, перпендикулярном фигуре. Таким 
    образом, кривая фигуры Лиссажу будет иметь
    // ширину 0.03 единицы.
    vertices[i * 2] = new VertexPositionColor(new 
    Vector3(x - nx, y - ny, 0.0f),
    new XnaGraphics.Color(red, green, 0)); vertices
    [i * 2 + 1] = new VertexPositionColor(new
     Vector3(x + nx, y + ny, 0.0f), new XnaGraphics.
     Color(red, green, 0)); 
    }; 
    ... 
    // Запускаем таймер
    stopwatch = new Stopwatch();
     stopwatch.Start();
    // Задаем обработчик события Idle, выполняющий 
    коррекцию прозрачности вершин
    Application.Idle+=new EventHandler(Application_Idle); 
    // Рассчитываем текущие коэффициенты непрозрачности вершин
    Application_Idle(this, null); 
    }
    // Рассчитывает текущие коэффициенты непрозрачности 
    вершин и перерисовывает изображение void
     Application_Idle(object sender, EventArgs e) 
    {
    if (closing) 
    Close();
    // Получаем текущее время
    float currentTime = (float)stopwatch.ElapsedTicks / 
    (float)Stopwatch.Frequency;
    // Перебираем все вершины фигуры Лиссажу
    for (int i = 0; i <= QuadStrips; i++) 
    { 
    // Вычисляем коэффициент непрозрачности для текущей пары вершин
    byte opacity = (byte)Math.Max(Math.Min((currentTime - 15.0f * (float)i / (float)QuadStrips) *
     255.0f, 255f), 0.0f); 
    // Изменяем коэффициент непрозрачности вершин. К 
    сожалению, структура Color не позволяет 
    // изменять отдельные компоненты цвета, поэтому 
    приходится создавать новую структуру и 
    // указывать значения всех цветовых компонентов.
    vertices[i * 2].Color = new XnaGraphics.Color(vertices[i * 2].Color.R,
     vertices[i * 2].Color.G, vertices[i * 2].Color.B, opacity);
    vertices[i * 2 + 1].Color = new XnaGraphics.Color
    (vertices[i * 2 + 1].Color.R,
     vertices[i * 2 + 1].Color.G, vertices[i * 2 + 1].Color.B, opacity); };
    // Если фигура Лиссажу полностью визуализирована 
    (она визуализируется ровно 16 секунд) if
     (currentTime > 16.0f) {
    // Прекращаем анимацию, дабы не загружать центральный
     процессор и видеокарту бесполезной 
    // работой.
    Application.Idle -= new EventHandler(Application_Idle); 
    // Выключаем таймер
    stopwatch.Stop(); 
    }
    // Перерисовываем форму 
    Invalidate(); 
    }
    private void MainForm_Paint(object sender, PaintEventArgs e) 
    { 
    ...
    device.RenderState.CullMode = CullMode.None;
    device.RenderState.FillMode = fillMode; 
    // Задаем режим смешения пикселей
    device.RenderState.AlphaBlendEnable = true;
    device.RenderState.BlendFunction = BlendFunction.Add;
    device.RenderState.SourceBlend = Blend.SourceAlpha;
    device.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
    device.VertexDeclaration = decl;
    // Визуализируем фигуру Лиссажу effect.Begin();
    foreach (EffectPass pass in effect.CurrentTechnique.Passes) 
    {
    pass.Begin();
    device.DrawUserPrimitives(PrimitiveType.TriangleStrip, vertices, 0, 
    vertices.Length - 2);
    pass.End(); 
    } effect.End();
    device.Present();
    }
    }

    Заключение

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

    Страницы:

    Любому разработчику периодически приходится решать множество типовых задач визуализации:

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

    3.1. Вывод на элементы управления .NET средствами XNA Framework

    До сих пор все наши приложения осуществляли монопольный вывод графической информации на поверхность формы. Такой подход не позволяет размещать на форме элементы управления .NET, что значительно ограничивает свободу разработчика. Конечно, можно попробовать использовать различные вспомогательные диалоговые окна и плавающие панели инструментов, но это не очень красивое решение проблемы, хотя и вполне работоспособноеПодобный подход применяется, к примеру, в редакторе сценариев (Activity Editor) игры Microsoft Train Simulator . Гораздо интереснее было бы научиться выводить информацию не на саму форму, а на элементы управления, размещенные на ней (к примеру, на элемент управления Panel ). Это позволило бы нам легко ограничить область вывода XNA Framework определенной областью формы, а освободившееся пространство использовать для размещения различных элементов управления Windows Forms.

    Вывод на поверхность элемента управления практически не отличается от вывода на поверхность формы, ведь в Windows Forms форма является частным случаем элементом управления. Более, форму можно поместить на другую форму или компонент в качестве элемента управленияЭтот прием используется в редакторе форм Visual Studio . Это возможно благодаря тому, что любая форма или элемент управления напрямую или косвенно наследуется от общего предка - класса Control. Как следствие, любой элемент управления Windows Forms обладает свойствами Handle, Width, Height, методами SetWidth, Show, Hide, обработчиками событий Paint, Click, Resize и так далееПолный перечень свойств и методов класса Control можно найти в MSDN .

    Примечание

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

    Но вот незадача, некоторые члены класса Control объявлены как protected. Это не создает никаких проблем при создании новой формы путем наследования от класса Form, так как в этом случае вы автоматически получаете полный доступ к защищенным (protected) членам класса. Однако при использовании готовых компонентов, помещаемых на форму средствами визуального редактора форм Visual Studio, все намного серьезнее. Например, вы не сможете получить доступ к такому важному методу как SetStyle, чтобы установить стиль ControlStyles.Opaque. А ведь без этого невозможно запретить самовольную перерисовку элемента управления, приводящую к мерцанию изображения в XNA -приложениях (раздел 1.2.3).

    Наиболее красивое решение этой проблемы - создание на базе класса Control собственного элемента управления, изменяющего статус метода SetStyle с protected на public. Для этого запустите Visual Studio и создайте новый проект библиотеки классов XnaPanel (File | New Project… | Class Library, в текстовом поле Name введите XnaPane и нажмите OK). Установите флажок Create directory for solution, так как в решении будет еще один проект, предназначенный для тестирования созданного компонента. Подключите к проекту сборку System.Windows. Forms и введите код из листинга 3.1.

    using System;
    using System.Collections.Generic; using System.Text; 
    using System.Windows. Forms;
    namespace GSP.XNA 
    {
    выделялся среди других элементов формы
    // Объявляем класс (элемент управления) XnaPanel, 
    наследуемый непосредственно public class
    XnaPanel : Control 
    { 
    // Конструктор элемента управления public XnaPanel() 
    { 
    // Изменяем цвет элемента управления, чтобы он
    1x1 пикселей
    BackColor = Color.CornflowerBlue; 
    // Размер элемента управления не должен быть меньше MinimumSize = new
    Size(1, 1);
     }
    SetStyle метода Control как public SetStyle(ControlStyles flag, 
    bool value)
    //
    //
    Переопределяем метод
    public new void
    { 
    Вызываем оригинальный метод класса Control base.SetStyle(flag, value);
    }
    }
    }

    Важно

    Для предотвращения неконтролируемого уменьшения размера компонента xnaPanel до размеров меньше одного пикселя, его свойству MinimumSize необходимо присвоить значение Size(1, 1) . Если этого не сделать, площадь области визуализации теоретически может достигнуть нуля, что приведет к генерации исключения при сбросе устройства

    После компиляции проекта Visual Studi o самостоятельно создаст в окне Components и добавит в нее полученный элемент управления (рисунок 3.1).

    Toolbox группу XnaPanel
    (рис 3.1) Панель Toolbox с компонентом XnaPanel

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

    Чтобы добавить к решению еще один проект щелкните правой кнопкой мыши на названии решения в окне Solution Explorer и выберите в контекстом меню пункт Add | New Project… (рисунок 3.3). В появившемся диалоговом окне выберите элемент Windows Application, введите в поле Name название приложения (например, Test ) и нажмите кнопку Ok. Как всегда, подключите к проекту необходимые сборки XNA Framework и добавьте необходимые директивы using.

    (рис 3.3) Внешний вид тестового приложения, использующего компонент XnaPanel (рис 3.2) Добавление к решению (Solution) нового проекта

    Поместите на форму компонент SplitContainer, расширьте его на всю форму путем присвоения параметру Dock значения Fill и зафиксируйте размер правой панели, присвоив свойству FixedPanel значение Panel2. В правой панели компонента SplitContainer разместите группу ( Group ) "Параметры " и тоже расширьте ее на всю панель при помощи свойства Dock. В группе создайте три метки ( Label ) расположенные друг под другом: Цвет вершины №1, Цвет вершины №2, Цвет вершины №3. Поместите напротив этих меток три панели ( Panel ) и присвойте им имена vertex1Panel, vertex2Panel и vertex3Panel. Чтобы создать черные рамки вокруг панелей, установите свойство BorderStyle в значение FixedSingle. Поместите в левой панели компонента SplitContainer наш компонент XnaPanel, назовите его xnaPanel и расширьте его на всю свободную область формы, присвоив свойству Dock значение Fill. В заключение, поместите на форму невизуальный компонент ColorDialog.

    Следующий этап - "оживление " формы путем создания обработчиков событий. Для начала мы добавим в форму несколько вспомогательных полей и обработчики событий Load/ и FormClosed (листинг 3.2).

    // Различные вспомогательные поля GraphicsDevice device = null; 
    PresentationParameters presentParams; Effect effect = null; 
    VertexDeclaration decl = null; VertexPositionColor[] vertices = null;
    bool closing = false;
    new XnaGraphics.Color(vertex2Panel.BackColor.R, vertex2Panel.BackColor.G, 
    vertex2Panel.BackColor.B));
    vertices[2] = new VertexPositionColor(new Vector3(-0.4f, -0.4f, 0.0f), 
     new XnaGraphics.Color(vertex3Panel.BackColor.R, 
    vertex3Panel.BackColor.G, 
    vertex3Panel.BackColor.B));
    // Рисуем треугольник
    effect.Begin();
    foreach(EffectPass pass in effect.CurrentTechnique.Passes)
    {
    pass.Begin();
    device.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0,
    vertices.Length / 3);
    pass.End(); 
    } 
    effect.End();
    // Переключаем экранные буферы
    device.Present();
    } 
    // Обрабатываем ситуацию внезапной потери устройства
    catch (DeviceNotResetException)
    {
    Invalidate() ;
    }
    catch (DeviceLostException)
    {
    } 
    }
    private void xnaPanelResize(object sender, EventArgs e)
    {
    // Если форма не минимизирована
    if (WindowState != FormWindowState.Minimized)
    { 
    // Обновляем размер формы
    presentParams.BackBufferWidth = xnaPanel.ClientSize. Width;
    presentParams.BackBufferHeight = xnaPanel.ClientSize.Height; 
    // Сбрасываем устройство
    device.Reset(presentParams);
    } 
    }

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

    private void vertex1Panel_Click(object sender, EventArgs e) 
    { 
    // Преобразовываем параметр sender к Panel
    Panel panel = (Panel)sender; 
    // Выбираем в диалоговом окне текущий цвет вершины
    colorDialog1.Color = panel.BackColor; 
    // Показываем стандартный диалог выбора цвета
    if (colorDialog1.ShowDialog() == DialogResult.OK)
    { 
    // Если пользователь выбрал цвет, изменяем цвет панели на выбраный
    panel.BackColor = colorDialog1.Color; 
    // Обновляем содержимое нашего компонента xnaPanel
    panelDX.Invalidate();
    } 
    }

    Как видно, обработчик получает информацию о нажатой панели из параметра sender, что позволяет без изменений использовать этот обработчик для панелей vertex2Panel и vertex3Panel: просто выделите эти панели и назначьте в качестве обработчика события Click метод vertex1Panel_Click.

    Остается лишь откомпилировать программу и посмотреть результат. Готовое приложение можно найти в ../1/example.zip в каталоге Examples\Ch03\Ex01.

    Примечание

    В Visual Studio выбор активного проектаПроект, который запускается при нажатии клавиши F5 осуществляется при помощи контекстного меню: щелкните на названии проекта в окне Solution Explorer и выберите в появившемся контекстном меню пункт Set as StartUp Project (рисунок 3.4).

    (рис 3.4) Выбор активного проекта с использованием контекстного меню

    Практическое упражнение №3.1.

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

    (рис 3.5) Иллюстрация к практическому упражнению №3.1

    Для ограничения области визуализации XNA Framework воспользуйтесь компонентом XnaPanel из примера Ch03\Ex02. Существует два способа добавления компонента XnaPanel в окно Toolbox:

  • одключить к решению проект XnaPanel из примера Ch03\Ex01. Для этого во вкладке Solution Explorer щелкнете правой кнопкой мыши на узле с названием решения ( Solution ) и выберете в контекстном меню пункт Add | Existing Project… . Укажите в открывшемся окне файл проекта компонента PanelDX (Examples\Ch03\Ex01 - XnaPanel\XnaPanel\XnaPanel.csproj) и нажмите Ok. После компиляции подключенного проекта (Ctrl + Shift + B) компонент Xna Panel автоматически появится в окне Toolbox.
  • Добавить в окно ToolBox компонент из сборки примера Ex01. Для этого щелкните правой кнопкой мыши на поверхности окна ToolBox и выберите в контекстном меню пункт Choose Items…. Откроется диалоговое окно Choose Toolbox Items…. Щелкните на кнопку Browse… и укажите сборку с компонентом PanelDX (Examples\Ch03\Ex01 - XnaPanel\XnaPanel\bin\Release\XnaPanel.dll) . Наконец, нажмите кнопку Ok, после чего на панели Toolbox появится компонент XnaPanel.
  • Какой из этих двух вариантов выбрать – решать вам, но лично мне больше симпатизирует первый подход.

    3.2. Полноэкранный режим

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

  • В полноэкранном режиме пользователь полностью погружается в виртуальный мир, не отвлекаясь на различные элементы интерфейса Windows вроде панели задач ( Taskbar ).
  • Контроль всего пространства экрана позволяет переключать экранный и задний ( back ) буферы без необходимости копирования информации между буферами: при показе итогового изображения XNA Framework просто делает задний буфер экранным, а экранный задним.
  • При запуске приложения на компьютере с недостаточно мощной видеокартой пользователь может повысить производительность путем уменьшения разрешения экрана и глубины цвета.
  • К недостаткам полноэкранного режима можно отнести несколько усложнение кода приложения и невозможность использования элементов управления Windows Forms, что затрудняет применение полноэкранного режима в различных утилитах.

    Переход полноэкранный режим осуществляется путем создания графического устройства ( GraphicsDevice ) с соответствующим образом настроенной структурой PresentationParameters. Легко догадаться, что свойству IsFullScreen необходимо присвоить значение true:

    presentParams = new PresentationParameters();
    presentParams.IsFullScreen = true;

    Кроме того, приложение должно задать параметры используемого видеорежима, а именно:

  • Разрешение экрана.
  • Формат пикселей экрана (количество бит на пиксель и т.п.).
  • Частоту вертикальной развертки.
  • РазрешениеКоличество пикселей по высоте и ширине экрана и вспомогательных ( back ) буферов задается свойствами BackBufferWidth и BackBufferHeight структуры PresentationParameters. Например:

    // Используем видеорежим с разрешением 640 x 480 
    presentParams.BackBufferWidth = 640; 
    presentParams.BackBufferHeight = 480;

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

    public SurfaceFormat BackBufferFormat { get; set; }

    где

  • SurfaceFormat - перечислимый тип, используемый для задания формата пикселей (таблица 3.1).
  • В качестве формата экранного буфера автоматически используется формат, наиболее близкий к формату заднего буфера. Например, если вы укажете для заднего буфера формат SurfaceFormat.Color, в качестве формата экранного буфера будет выбран формат SurfaceFormat.Bgr32. Обычно это не существенно, ведь альфа-канал все равно не оказывает никакого влияния на отображаемом на экране изображении; тем не менее, в некоторых немногочисленных ситуациях этот нюанс все же приходится учитывать.

    Характеристика форматов пикселей, соответствующих значениям перечислимого типа SurfaceFormat
    Значение Может использоваться в качестве формата экранного буфера Размер в битах
    Весь пиксель Красный канал Зеленый канал Синий канал Альфа канал
    Unknown ? ? ? ? ?
    Rgba1010102 X 32 10 10 10 2
    Color 32 8 8 8 8
    Bgr32 X 32 8 8 8 -
    Bgr565 X 16 5 6 5 -
    Bgra5551 16 5 5 5 1

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

    Однако применение формата SurfaceFormat.Unknown для полноэкранного графического устройства приводит к генерации исключения System.InvalidOperationException. Так что при создании устройства, использующего полноэкранный режим, приложение обязано явно указывать формат пикселей заднего буфера. В нашем первом приложении мы, не мудрствуя лукаво, будем использовать формат SurfaceFormat.Bgr32, поддерживаемый всеми видеокартами, совместимыми с XNA Framework: presentParams.BackBufferFormat = SurfaceFormat.Bgr32 ;

    Примечание

    Должно быть, вы заметили, что пиксели формата SurfaceFormat.Bgr32 имея размер 32 бита, содержат всего 24 бита полезной информации (для красного, синего и зеленого каналов отведено по 8 бит). Такая избыточность обусловлена тем, что современные процессоры умеют работать только с типами данных разрядностью 8, 16, 32 и 64 бит. Соответственно, 24-х битные типы данных пришлось бы эмулировать посредством 8, 16 и 32-х разрядных типов, что неминуемо увеличило количество операций, требуемых для считывания и записи значения пикселя и, соответственно, ощутимо снизило производительность. Например, для записи 24-х битного значения необходимо загрузить регистр 32 бита, модифицировать 24 бита, и записать полученное 32-х битное значение обратно в память.

    И, наконец, последней характеристикой любого графического режима является частота обновления экрана. Для современных электронно-лучевых трубок нормой является частота обновления порядка 85 герц и выше, так как при более низких частотах становится заметным мерцание монитора. Мониторы LCD менее критичны к низкой частоте обновления экрана, однако редкий современный LCD -монитор поддерживает частоту смены кадров более 60 Гц. Частота обновления экрана задается свойством FullScreenRefreshRateInHz структуры PresentationParameters:

    public int FullScreenRefreshRateInHz { get; set; }

    В оконном режиме этому свойству присевается нулевое значение, ведь окно все равно может обновляться с частотой, отличной от частоты обновления, используемой рабочим столом. В полноэкранном режиме нулевое значение параметра FullScreenRefreshRateInHz, означает, что приложение отдает выбор частоты обновления экрана на откуп Windows и драйверам видеокарты/монитора. Так как при указании частоты обновления неподдерживаемой текущей видеоподсистемой может быть сгенерировано исключение System. InvalidOperationException, мы пока не будем пытаться самостоятельно выбирать частоту обновления экрана.

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

    presentParams = new PresentationParameters();
    presentParams.BackBufferCount = 1;
    presentParams.SwapEffect = SwapEffect.Discard;
    // Используем полноэкранный режим 640Ч480, 
    32 бита на пиксель, частота обновления выбирается
    // автоматически
    presentParams.IsFullScreen = true;
    presentParams.BackBufferWidth = 640;
    presentParams.BackBufferHeight = 480;
    presentParams.BackBufferFormat = SurfaceFormat.Bgr32;
    presentParams.FullScreenRefreshRateInHz = 0;
    GraphicsDeviceCapabilities caps =
    GraphicsAdapter.DefaultAdapter.GetCapabilities(DeviceType.Hardware);
    CreateOptions options = CreateOptions.SingleThreaded; if
    (caps.DeviceCapabilities.SupportsHardwareTransformAndLight)
    options |= CreateOptions.HardwareVertexProcessing;
    else
    options |= CreateOptions.SoftwareVertexProcessing;
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, 
    DeviceType.Hardware, this.Handle, options, presentParams);

    Так же необходимо подправить код обработчика событий Resize. Дело в том, что даже в полноэкранном режиме форма продолжает жить своей жизнью и в ряде случаях (при переключении в другое приложение путем нажатия клавиш alt + tab ) Windows может самостоятельно изменять размер формы, что, разумеется, приводит к генерации события Resize. Но так как в полноэкранном режиме размер формы некоим образом не связан с разрешением экрана, приложение не должно изменять размер заднего ( back ) буфера:

    private void MainForm_Resize(object sender, EventArgs e)
    {
    // Если приложение находится в полноэкранном режиме, 
    оно не должно отслеживать изменение
    // размера формы
    if ((!presentParams.IsFullScreen)  
    (WindowState != FormWindowState.Minimized))
    {
    presentParams.BackBufferWidth = ClientSize.Width;
    presentParams.BackBufferHeight = ClientSize.Height;
    device.Reset(presentParams); 
    } 
    }

    Визуализация сцены осуществляется аналогично оконному режиму:

    private void MainFormPaint(object sender, PaintEventArgs e) 
    {
    if (closing)
    return;
    try 
    {
    if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.Lost)
    throw new DeviceLostException();
    if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.NotReset)
    device.Reset(presentParams);
    // Задаем область визуализации размеров во весь экран
    device.Viewport = Helper.FullScreenViewport presentParams); 
    // Очищаем весь экран
    device.Clear(XnaGraphics.Color.CornflowerBlue);
    // Используем квадратную область визуализации 
    (во избежание геометрических искажений 
    // изображения)
    device.Viewport = Helper.SquareViewport(presentParams);
    // Рисуем CD-диск (см. раздел 2.6.3)
    }
    catch (DeviceNotResetException)
    {
    Invalidate() ;
    }
    catch (DeviceLostException)
    {
    }
    }

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

    // Вычисление квадратной области визуализации
    public static Viewport SquareViewport(PresentationParameters 
    presentParams)
    {
    return SquareViewport(new System.Drawing.Size(presentParams.
    BackBufferWidth,
    presentParams.BackBufferHeight)); }
    // Вычисление области визуализации размеров во весь экран
    public static Viewport FullScreenViewport(PresentationParameters 
    presentParams)
    {
    return FullScreenViewport(new System.Drawing.Size(presentParams.
    BackBufferWidth,
    presentParams.BackBufferHeight));
    
    }

    Готовое приложение, визуализирующее изображение CD-диска в полноэкранном режиме (рисунок 3.6), находится в каталоге Examples\Ch03\Ex03.

    (рис 3.6) CD-диск, визуализированный полноэкранном режиме (640x480x32bpp)

    3.2.1. Выбор оптимального видеорежима

    В примере Ch03\Ex03 (листинг 3.4) мы неявно предполагаем, что абсолютно все современные и будущие видеоподсистемы будут поддерживать видеорежим 640Ч480Ч32 bppBPP – Bit Per Pixel (Бит на пиксель) . Если это вдруг окажется не так, приложение аварийно завершит работу с исключением System.InvalidOperationException. Поэтому было бы логичным на всякий случай предусмотреть поведение приложение при отсутствии поддержки требуемого видеорежима. Например, при неудачной попытке создания полноэкранного графического устройства приложение могло бы попытаться создать графическое устройство, осуществляющее визуализацию в окне:

    presentParams = new PresentationParameters();
    presentParams.BackBufferCount = 1;
    presentParams.SwapEffect = SwapEffect.Discard;
    presentParams.IsFullScreen = true;
    // Используем полноэкранный режим 640Ч480, 32 бита на пиксель, 60 герц
    presentParams.BackBufferWidth = 640;
    presentParams.BackBufferHeight = 480;
    presentParams.BackBufferFormat = SurfaceFormat.Bgr32;
    presentParams.FullScreenRefreshRateInHz = 60;
    try {
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, 
    DeviceType.Hardware, this.Handle,
    options, presentParams);
     }
    catch(InvalidOperationException) 
    {
    // Если попытка использования полноэкранного режима 
    потерпела фиаско, используем 
    // визуализацию в окне
    presentParams.IsFullScreen = false;
    presentParams.BackBufferWidth = 0;
    presentParams.BackBufferHeight = 0;
    presentParams.BackBufferFormat = SurfaceFormat.Unknown;
    presentParams.FullScreenRefreshRateInHz = 0;
    
    // После выполнения этой команды интерфейс формы будет 
    заблокирован: форму нельзя 
    // будет перемещать по экрану, изменять ее размер, 
    минимизировать и т.п.
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, 
    DeviceType.Hardware,
    this.Handle, options, presentParams); 
    }

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

    И так, при создании полноэкранного графического устройства необходимо подобрать видеорежим, гарантированно поддерживаемый данной видеокартой и монитором. А почему бы нам вместо гадания на кофейной гуще не использовать текущий видеорежим рабочего стола, по определению поддерживаемый видеоподсистемой компьютера? Ведь, как известно, в качестве видеорежима рабочего стола обычно используется видеорежим, наиболее оптимальный для текущего монитора компьютера. Это особенно актуально для LCD -мониторов, оптимизированных для работы каком-то одном разрешении экрана (как правило, 1024x768 или 1280x1024). Информация о текущем видеорежиме доступна посредством свойства CurrentDisplayMode класса GraphicsAdapter:

    public DisplayMode CurrentDisplayMode { get; }

    Вся информация о видеорежиме хранится в структуре DisplayMode:

    public struct DisplayMode
    {
    // Количество пикселей по горизонтали
    public int Width { get; } 
    // Количество пикселей по вертикали
    public int Height { get; } 
    // Формат пикселей
    public SurfaceFormat Format { get; } 
    // Частота обновления экрана
    public int RefreshRate { get; }
    }

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

    // Получаем описание текущего видеорежима
    DisplayMode displayMode = GraphicsAdapter.DefaultAdapter.
    CurrentDisplayMode;
    presentParams = new PresentationParameters();
    presentParams.IsFullScreen = true;
    presentParams.BackBufferCount = 1;
    presentParams.SwapEffect = SwapEffect.Discard;
    // Использует такие же параметры визуализации, 
    как у текущего видеорежима
    presentParams.BackBufferWidth = displayMode.Width;
    presentParams.BackBufferHeight = displayMode.Height;
    presentParams.BackBufferFormat = displayMode.Format;
    presentParams.FullScreenRefreshRateInHz = displayMode.RefreshRate;
    GraphicsDeviceCapabilities caps =
    GraphicsAdapter.DefaultAdapter.GetCapabilities(DeviceType.Hardware);
    CreateOptions options = CreateOptions.SingleThreaded;
    if (caps.DeviceCapabilities.SupportsHardwareTransformAndLight)
    options |= CreateOptions.HardwareVertexProcessing; else
    options |= CreateOptions.SoftwareVertexProcessing;
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, 
    DeviceType.Hardware, 
    this.Handle, options, presentParams);

    3.2.2. Получение списка доступных видеорежимов

    Хотя использование видеорежима рабочего стола дает вполне нормальные результаты, в ряде случаев подобный подход оказывается неприемлемым. В основном это касается дешевых видеокарт, демонстрирующих довольно низкую производительность в современных трехмерных приложениях, в результате чего приемлемая частота смены кадров достигается лишь в самых низких разрешениях вроде 640Ч480 или 800Ч600. Поэтому было логичным предоставить пользователю возможность самостоятельно выбирать параметры видеорежима в зависимости от своих потребностей.

    Наше следующее приложение будет отображать на экране перечень всех видеорежимов, поддерживаемых видеокартой и затем переключаться в видеорежим, выбранный пользователем, и визуализировать изображение диска (рисунок 3.7). Эту функциональность достаточно легко реализовать, так как разработчики XNA Framework снабдили класс GraphicsAdapter коллекцией SupportedDisplayModes, содержащей набор структур DisplayMode с информацией обо всех видеорежимах, поддерживаемых указанной связкой видеокарта – монитор.

    (рис 3.7) Диалоговое окно выбора видеорежима

    Итак, создайте новое приложение Windows Forms и добавьте в него новую форму. Присвойте свойствам формы значения согласно таблице 3.2. Поместите на форму компонент ListBox и назовите его displayModeListBox. В заключение, поместите на форму кнопку Ok и присвойте ее свойству DialogResult значение OK.

    Свойства формы выбора видеорежима
    Свойство Значение
    Name DisplayModeForm
    Text Выберите видеорежим
    FormBorderStyle FixedDialog
    MaximizeBox False
    MinimizeBox False

    Теперь реализуем логику работы формы, а именно: конструктор формы и свойство SelectedDisplayMode, возвращающее информацию о выбранном видеорежиме (листинг 3.9).

    public partial class DisplayModeForm : Form 
    {
    // Конструктор формы. В качестве параметра 
    принимает объект графического адаптера, для 
    // которого необходимо выбрать графический режим. Public
    DisplayModeForm(GraphicsAdapter adapter) 
    {
    InitializeComponent(); 
    // Перебираем все графические режимы, поддерживаемые 
    графическим адаптером и добавляем их на
    // панель displayModeListBox
    foreach (DisplayMode diplayMode in adapter.SupportedDisplayModes)
    displayModeListBox.Items.Add(diplayMode);
    // Выбираем самый первый элемент списка видеорежимов
    displayModeListBox.SelectedIndex = 0; 
    }
    // Возвращает информацию о графическом режиме, выбранном пользователем
    public DisplayMode SelectedDisplayMode 
    {
    get 
    {
    return (DisplayMode)displayModeListBox.SelectedItem; 
    } 
      } 
    }

    И наконец, в обработчик события Load главной формы необходимо вставить код взаимодействия с диалоговым окном выбора видеорежима:

    private void MainFormLoad(object sender, EventArgs e) 
    {
    SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
    MinimumSize = SizeFromClientSize(new Size(1, 1));
    presentParams = new PresentationParameters();
    presentParams.IsFullScreen = true;
    presentParams.BackBufferCount = 1;
    presentParams.SwapEffect = SwapEffect.Discard;
    // Создаем диалоговое окно выбора видеорежима
    using (DisplayModeForm displayModeForm = new
    DisplayModeForm(GraphicsAdapter.DefaultAdapter))
    { 
    // Отображаем диалоговое окно
    displayModeForm.ShowDialog();
    // Получаем видеорежим, выбранный пользователем
    DisplayMode mode = displayModeForm.SelectedDisplayMode;
    // Задаем требуемый видеорежим
    presentParams.BackBufferWidth = mode.Width;
    presentParams.BackBufferHeight = mode.Height;
    presentParams.BackBufferFormat = mode.Format;
    presentParams.FullScreenRefreshRateInHz = mode.RefreshRate; 
    }
    // Создаем графическое устройство
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, 
    DeviceType.Hardware, 
    4> this.Handle, options, presentParams);
    }

    Оставшийся код приложения не содержит чего-либо интересного, поэтому мы не будем на нем останавливаться. Готовое приложение находится в каталоге Examples\Ch03\Ex05.

    Запустите полученное приложение и попробуйте поэкспериментировать с различными разрешениями экрана. Очень скоро вы заметите одну важную особенность: в некоторых видеорежимах диск вместо круглой формы растягивается/сплющивается в эллипсовидную фигуру. После внимательного изучения соотношения сторон разрешений экрана мы прейдем к выводу, что диск корректно отображается лишь при условии совпадения отношения сторон монитора с отношением соответствующих размерностей разрешения экрана. Например, отношение сторон моего монитора NEC MultiSync FE991SB равно 4:3 = 1.33, поэтому в разрешениях 320Ч240, 640Ч480, 800Ч600, 1024Ч768, 1280Ч960, 1600Ч1200, 1792Ч1344 изображение отображается без искажений. В разрешении 1280Ч1024 (соотношение сторон 5:4=1.25) круг едва заметно сплющивается вдоль оси Y, а вот в разрешении 1360Ч768 (соотношение сторон 16:9=1.77) сплющивание круга вдоль оси X становится более чем заметным. С другой стороны, на широкоэкранных мониторах с соотношением сторон 16:9 именно разрешения 1088Ч612, 1280Ч768, 1360Ч768 и 1600Ч900 (т.е. с соотношением сторон 16:9) будут давать изображение без искажений.

    (рис 3.8) Искажение формы круглого диска, визуализированного в разрешении 1360Ч768, при отображение на мониторе с соотношение сторон 4:3

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

    Примечание

    Обратите внимание на отсутствие в списке видеорежимов с форматами пикселей с поддержкой альфа канала. Дело в том, что коллекция SupportedDisplayModes содержит только видеорежимы экранного буфера, а экранный буфер не поддерживает альфа-канал по причине банальной ненужности (см. таблицу 3.1). Так что отсутствие поддержки альфа-канала экранным буфером вовсе не означает отсутствие поддержки форматов с альфа-каналом задним буфером. Более того, если современная видеокарта поддерживает формат SurfaceFormat.Bgr32, то она с вероятность 99.9% поддерживает и формат SurfaceFormat.Color. Впрочем, эти нюансы для нас пока не актуальны, так как наши текущие приложения все равно не используют альфа-канал.

    3.2.3. Диалоговое окно выбора видеорежима

    Диалоговое окно выбора видеорежима из примера Ch03\Ex05 хотя и выполняет свою задачу, однако имеет при этом ряд существенных недостатков, затрудняющих его применение в реальных приложениях:

  • При каждом запуске приложения приходится заново выбирать видеорежим, что довольно быстро начинает раздражать. Поэтому было бы логичным реализовать сохранение параметров выбранного видеорежима в конфигурационный файл.
  • Все параметры видеорежимов "свалены в одну кучу ", отчего очень быстро начинает "рябить в глазах ". Коммерческие приложения обычно разбивают информацию о видеорежимах на три списка: в первом списке пользователь выбирает разрешение экрана, во втором – глубину цвета (формат пикселей), а в третьем – частоту обновления экрана.
  • Для пользователя актуальны только те видеорежимы, соотношение сторон которых совпадает с соотношением сторон монитора. С другой стороны, для оценки в уме соотношения сторон видеорежима вроде 1792Ч1344 требуются незаурядные математические способности, поэтому было бы логичным предусмотреть типовые фильтры 4:3, 5:4 и 16:9, отбрасывающие видеорежимы с соотношением сторон отличным заданногоВ принципе, можно было бы запрашивать у пользователя соотношение сторон монитора, после чего масштабировать изображение исходя из параметров разрешения экрана и соотношения сторон монитора (такой подход применяется, к примеру, в игре Half-Life 2) .
  • Не помешало бы на всякий случай предоставить пользователю возможность выбора визуализации в оконном режиме, который иногда оказывается весьма полезным (например, при одновременной работе с несколькими приложениями).
  • Ну что ж, настало время разработать новую версию диалогового окна, свободную от этих недостатков (рисунок 3.9). Запустите Visual Studio 2005 и создайте приложение Windows Application. Мы начнем с создания класса, инкапсулирующего работу с файлами конфигурации приложения. Наш файл конфигурации будет содержать 7 полей:

    Поля файла конфигурации
    Поле Тип данных Описание Значение по умолчанию
    Width int размер заднего буфера вдоль оси X 0
    Height int размер заднего буфера вдоль оси Y 0
    Format SurfaceFormat формат пикселей заднего буфера SurfaceFormat.Unknown
    RefreshRate int частота обновления экрана 0
    FullScreen bool работает ли приложение в полноэкранном режиме ( false – нет, true – да) true
    Init bool равен true, если информация о конфигурации приложения была загружена из файла false
    ShowSettingForm bool надо ли отображать при запуске приложения диалоговое окно выбора видеорежима true

    Так как наш файл конфигурации не будет представлять собой нечего экстравагантного, все операции по созданию класса вполне можно выполнить в дизайнере Visual Studio. Для этого в окне Solution Explorer раскройте узел Properties, и сделайте двойной щелчок левой кнопкой мыши на узле Setting.settings, после чего слева откроется дизайнер файла конфигурации приложенияДизайнер файла конфигурации приложения доступен также посредством вкладки Settings окна Properties . Добавьте в настройки приложения новые поля согласно таблице 3.3 и рисунок 3.10.

    (рис 3.10) Новая версия диалогового окна выбора видеорежима (рис 3.9) Настройка параметров конфигурации приложения

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

    Свойства формы диалогового окна и элементов управления
    Класс Свойство Значение
    Form (диалоговое окно) Name SettingsForm
    Text Параметры
    FormatBorderStyle FixedDialog
    MinimizeBox false
    MaximizeBox false
    CheckBox Name inWindowCheckBox
    Text Визуализировать в окне
    GroupBox Text Видеорежим
    Label Text Соотношение сторон
    ComboBox (фильтр отображаемых видеорежимов по соотношению сторон) Name aspectRatioComboBox
    DropDownStyle DropDownList
    Label Text Разрешение
    ComboBox (список разрешений экрана) Name resolutionComboBox
    DropDownStyle DropDownList
    Label Text Глубина цвета:
    ComboBox (список форматов пикселей) Name colorDepthComboBox
    DropDownStyle DropDownList
    Label Text Частота обновления
    ComboBox (список частот обновления экрана) Name refreshRateComboBox
    DropDownStyle DropDownList
    Button Name okButton
    Text Ok
    Button Text Cancel
    DialogResult Cancel

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

    // Инкапсулирует элемент списка соотношений сторон
    public struct AspectItem 
    {
    // Коэффициент отношения сторон
    public float aspect;
    public AspectItem(float aspect) 
    {
    this.aspect = aspect; 
    }
    // Проверяет указанный элемент списка разрешений 
    (ResolutionItem) на соответствие текущему 
    // соотношению сторон. В случае равенства метод 
    возвращает true, иначе - false public bool
    Compare(float aspect) 
    {
    // Нулевое соотношение сторон является зарезервированным 
    значением ( "любое соотношение
    // сторон "), поэтому возвращаем true if 
    (this.aspect == 0) return true;
    // Сравниваем коэффициенты отношения сторон, 
    закладывая небольшой  "запас " в
    один процент. 
    // Дело в том, что некоторые режимы имеют не совсем 
     "академически " правильное
    соотношение 
    // сторон. Например, соотношение сторон разрешения 
    1360 x 768 равно 85/48=1.771, в 
    // как 16/9=1.778.
    if (Math.Abs(this.aspect - aspect) < this.aspect * 0.01f)
    return true;
    else
    return false;
    }
    // Возвращает текущее соотношение сторон в удобочитаемом виде
    public override string ToString() 
    {
    if (aspect == 0)
    return "Любое";
    if (Compare(4.0f / 3.0f))
    return "4 : 3";
    if (Compare(5.0f / 4.0f))
    return "5 : 4";
    if (Compare(16.0f / 9.0f)) return "16 : 9";
    return aspect.ToString(); 
    }
     }
    // Инкапсулирует элемент списка разрешений экрана
    public struct ResolutionItem : IComparable<ResolutionItem>
    {
    // Разрешение экрана
    public int width;
    public int height;
    public ResolutionItem(int width, int height) 
    {
    this.width = width;
    this.height = height;
     }
    // Отношение количества пикселей вдоль осей X и Y для 
    текущего разрешения экрана
    public float Aspect 
    {
    get
     {
    return (float)width / (float)height; 
    } 
    }
    // Возвращает в удобочитаемом виде текст элемента 
    списка public override string ToString() 
    {
    return width.ToString() + " x " + height.ToString(); 
    }
    // Реализация интерфейса IComparable<ResolutionItem>, 
    используемого при сортировке элементов 
    // списка
    public int CompareTo(ResolutionItem other) 
    { 
    // Сначала сравнивает ширина разрешений if (width > other.width) 
    return 1;
    if (width < other.width)
    return -1;
    // Если ширина одинаковая, сравнивается высота разрешений. 
    В результате разрешения сначала 
    // сортируются по ширине, а затем по высоте. if (height > 
    other.height) return 1;
    if (height < other.height) return -1;
    return 0;
    } 
    }
    // Инкапсулирует элемент списка форматов пикселей
    public struct ColorDepthItem : IComparable<ColorDepthItem>
    {
    // Формат пикселей
    public SurfaceFormat value;
    public ColorDepthItem(SurfaceFormat value) 
    {
    this.value = value; 
    }
    // Возвращает строку с описанием формата пикселя
    public override string ToString() 
    {
    string str;
    switch (value) 
    {
    case SurfaceFormat.Bgr32:
    str = "32 бита ({0})";
    break;
    case SurfaceFormat.Bgr565:
    str = "16 бит ({0})";
    break; case
    SurfaceFormat.Rgba1010102:
    str = "32 бит ({0})";
    break; case
     SurfaceFormat.Bgr555:
    str = "16 бит {0}";
    break; default:
    str = "{0}";
    break; 
    }
    return string.Format(str, value);
     }
    // Реализация интерфейса IComparable<ColorDepthItem>, 
    используемого при сортировке элементов 
    // списка
    public int CompareTo(ColorDepthItem other) 
    {
    if (value > other.value)
     return -1;
    if (value < other.value)
     return 1;
    return 0; 
    }
    }
    // Инкапсулирует элемент списка частот обновлений экрана public
     struct RefreshRateItem : IComparable<RefreshRateItem> 
    {
    // Частота обновления экрана
    public int value;
    public RefreshRateItem(int value) 
    {
    this.value = value; 
    }
    // Возвращает строку с частотой обновления экрана
     public override string ToString() 
    {
    return value.ToString() + " Гц"; 
    }
    // Реализация интерфейса IComparable< RefreshRateItem>, 
    используемого при сортировке 
    // элементов списка
    public int CompareTo(RefreshRateItem other) 
    {
    if (value > other.value)
    return 1;
    if (value < other.value)
     return -1;
    return 0; 
    }
    }

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

    public partial class SettingsForm : Form
    {
    // Набор ассоциативных сортированных массив с 
    информаций о поддерживаемых видеорежимах
    SortedDictionary<ResolutionItem, SortedDictionary<ColorDepthItem, 
     SortedDictionary<RefreshRateItem, bool>>> modeCollection;
    // Текущие настройки приложения. Класс Properties.Settings 
    автоматически генерируется 
    // дизайнером настроек приложения (рисунок 3.10). 
    Properties.Settings settings;
    // Конструктор формы
    internal SettingsForm(Properties.Settings settings, 
    GraphicsAdapter adapter) 
    {
    InitializeComponent();
    // Сохраняем ссылку на настройки приложения
     this.settings = settings;
    // Если файл с конфигурацией приложения не был найден,
     устанавливаем в качестве параметров по 
    // умолчанию настройки рабочего стола if (!settings.Init) 
    {
    settings.Width = adapter.CurrentDisplayMode.Width;
     settings.Height = adapter.CurrentDisplayMode.Height;
     settings.Format = adapter.CurrentDisplayMode.Format;
     settings.RefreshRate = adapter.CurrentDisplayMode.RefreshRate;
     settings.FullScreen = true;
    }
    modeCollection = new SortedDictionary<ResolutionItem, 
    4> SortedDictionary<ColorDepthItem, SortedDictionary<RefreshRateItem,
    bool>>>();
    // Перебираем все видеорежимы, поддерживаемые видеокартой и
     заполняем ассоциативный массив 
    // modeCollection
    foreach (DisplayMode mode in adapter.SupportedDisplayModes)
    {
    ResolutionItem res = new ResolutionItem(mode.Width, mode.Height);
    if (!modeCollection.ContainsKey(res))
    modeCollection.Add(res, new SortedDictionary<ColorDepthItem,
     SortedDictionary<RefreshRateItem, bool>>());
    ColorDepthItem depth = new ColorDepthItem(mode.Format); if
     (!modeCollection[res].ContainsKey(depth))
    modeCollection[res].Add(depth, new SortedDictionary<RefreshRateItem,
     bool>());
    RefreshRateItem refresh = new RefreshRateItem(mode.RefreshRate); if
     (!modeCollection[res][depth].ContainsKey(refresh))
     modeCollection[res][depth].Add(refresh, true); }
    // Задаем состояние флага  "визуализировать в окне "
    inWindowCheckBox.Checked = !settings.FullScreen;
    // Добавляем в список  "соотношение сторон " типовые фильтры
    видеорежимов aspectRatioComboBox.Items.Add(new AspectItem());
     aspectRatioComboBox.Items.Add(new AspectItem(4.0f / 3.0f));
    aspectRatioComboBox.Items.Add(new AspectItem(5.0f / 4.0f));
     aspectRatioComboBox.Items.Add(new AspectItem(16.0f / 9.0f));
    // Выбираем самый первый элемент списка ( "Любое ") 
    aspectRatioComboBox.SelectedIndex = 0; }
    }

    Как видно, информация о поддерживаемых видеорежимах хранится в многомерном ассоциативном массиве, упрощающем поиск информации о требуемых видеорежимах. Например, для проверки существования видеорежима с разрешением 1024x768x32bpp:@85Hz приложение должно проверить существование элемента modeCollection[ "1024x768 "][ SurfaceFormat.Brg32][85] . Последний оператор конструктора выбирает нулевой элемент списка, генерируя событие SelectedIndexChanged, обработчик которого приведен в листинге 3.13.

    // Обновляет список поддерживаемых разрешений экрана с 
    учетом нового фильтра соотношения
    // сторон
    private void aspectRatioComboBoxSelectedIndexChanged(object 
    sender, EventArgs e)
    {
    // Получаем текущий элемент списка  "соотношение сторон "
    AspectItem aspect = (AspectItem)aspectRatioComboBox.SelectedItem;
    // Текущее разрешение экрана
    ResolutionItem currentResolution; 
    // Используем в качестве текущего разрешения экрана элемент
     из списка  "Разрешение ". Если же 
    // элемент в списке не выбран, используем разрешение
     экрана из текущих настроек приложения if 
     (resolutionComboBox.SelectedIndex != -1)
    currentResolution = (ResolutionItem)resolutionComboBox.
    SelectedItem; else
    currentResolution = new ResolutionItem(settings.Width,
     settings.Height);
    // Очищаем список разрешений экрана
    resolutionComboBox.Items.Clear(); 
    // Перебираем все разрешения экрана
    foreach (ResolutionItem res in modeCollection.Keys) 
    // Если разрешение экрана удовлетворяет заданному соотношению сторон
    if (aspect.Compare(res.Aspect)) 
    // Добавляем его в список разрешений
    resolutionComboBox.Items.Add(res);
    // Если было найдено хотя бы одно разрешение экрана, 
    соответствующее заданному соотношению 
    // сторон
    if (resolutionComboBox.Items.Count != 0) 
    { 
    // В списке разрешений экрана пытаемся выбрать текущее разрешение
    resolutionComboBox.SelectedItem = currentResolution; 
    // Если это не удалось, выбираем самый первый элемент списка if
     (resolutionComboBox.SelectedIndex == -1)
     resolutionComboBox.SelectedIndex = 0; }
    // Вызываем обработчик события изменения состояния 
    переключателя  "Визуализировать в окне ",
    // который при необходимости активирует/деактивирует 
    определенные элементы формы вроде кнопки
    // Ok
    inWindowCheckBox_CheckedChanged(inWindowCheckBox, null); 
    }
    // Обработчик события CheckedChange флага  "Визуализировать
     в окне ",
    private void inWindowCheckBox_CheckedChanged(object sender, EventArgs e)
    {
    // Если флаг включен
    if (inWindowCheckBox.Checked) 
    { 
    // Деактивируем все списки, связанные с параметрами 
    полноэкранного режима
     aspectRatioComboBox.Enabled = false; resolutionComboBox.Enabled =
     false; colorDepthComboBox.Enabled = false; refreshRateComboBox.Enabled
     = false; okButton.Enabled = true; 
    }
    else 
    { 
    // Активируем список  "Соотношение сторон "
    aspectRatioComboBox.Enabled = true; 
    // Если список доступных разрешений экрана не пустой if
    (resolutionComboBox.Items.Count > 0) 
    { 
    // Активируем оставшиеся три списка и кнопку Ok 
    resolutionComboBox.Enabled = true;
    colorDepthComboBox.Enabled = true;
    refreshRateComboBox.Enabled = true;
    okButton.Enabled = true; 
    }
    else 
    { 
    // В противном случае блокируем эти элементы управления
     resolutionComboBox.Enabled = false;
     colorDepthComboBox.Enabled = false;
     refreshRateComboBox.Enabled = false; okButton.Enabled =
     false; 
    } 
    } 
    }

    Так как выбранное разрешение оказывает влияние на доступные форматы пикселей списка "Глубина цвета ", необходимо определить обработчик события SelectedIndexChanged списка разрешений (листинг 3.14).

    private void resolutionComboBoxSelectedIndexChanged(object sender, EventArgs e)
    {
    // Получаем выбранный элемент списка разрешений экрана
    ResolutionItem res = (ResolutionItem) resolutionComboBox.SelectedItem; 
    // Текущая глубина цвета
    ColorDepthItem currentColorDepth; 
    // Используем в качестве текущей глубины цвета 
    элемент из списка  "Глубина цвета ".
    Если же 
    // элемент в списке не выбран, используем глубину 
    цвета из текущих настроек приложения
    if (colorDepthComboBox.SelectedIndex != -1)
    currentColorDepth = (ColorDepthItem)colorDepthComboBox.SelectedItem;
    else
    currentColorDepth = new ColorDepthItem(settings.Format);
    // Очищаем список  "Глубина цвета "
    colorDepthComboBox.Items.Clear(); 
    // Перебираем все форматы пикселей выбранного 
    разрешения экрана и добавляем их в список 
    //  "Глубина цвета "
    foreach (ColorDepthItem colorDepth in modeCollection[res].Keys)
     colorDepthComboBox.Items.Add(colorDepth);
    // Пытаемся выбрать глубину цвета, как у текущих настроек приложения
    colorDepthComboBox.SelectedItem = currentColorDepth; 
    // В случае неудачи выбираем первый элемент списка
    if (colorDepthComboBox.SelectedIndex == -1)
     colorDepthComboBox.SelectedIndex = 0; 
    }

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

    private void colorDepthComboBox_SelectedIndex
    Changed(object sender, EventArgs e)
    {
    // Получаем выбранное разрешение и глубину цвета (формат пикселей)
    ResolutionItem res = (ResolutionItem)resolutionComboBox.SelectedItem;
    ColorDepthItem colorDepth = (ColorDepthItem)
    colorDepthComboBox.SelectedItem;
    // Текущая частота обновления экрана
    RefreshRateItem currentRefreshRate; 
    // Используем в качестве текущей частоты обновления 
    экрана элемент из списка. Если же элемент 
    // в списке не выбран, используем частоту обновления 
    экрана из текущих настроек приложения if
     (refreshRateComboBox.SelectedIndex != -1)
    currentRefreshRate = (RefreshRateItem)refreshRate
    ComboBox.SelectedItem; else
    currentRefreshRate = new RefreshRateItem(settings.RefreshRate);
    // Очищаем список  "Частота обновления "
    refreshRateComboBox.Items.Clear(); 
    // Перебираем частоты обновления экрана, поддерживаемые 
    выбранным разрешением с указанной
    // глубиной цвета
    foreach (RefreshRateItem refreshRate in modeCollection[res]
    [colorDepth].Keys)
     refreshRateComboBox.Items.Add(refreshRate);
    // Выбираем в списке  "Частота обновления " 
    текущую частоту обновления экрана
    refreshRateComboBox.SelectedItem = currentRefreshRate; 
    // Если такого элемента нет, выбираем самый первый элемент списка
    if (refreshRateComboBox.SelectedIndex == -1)
     refreshRateComboBox.SelectedIndex = 0; 
    }

    Завершая создание диалогового окна, мы должны реализовать обработчик нажатия кнопки Ok:

    private void okButton_Click(object sender, EventArgs e)
    {
    // Обновляем настройки приложения на основе текущего 
    состояния элементов управления
    // формы окна
    settings.FullScreen = !inWindowCheckBox.Checked;
    if (!inWindowCheckBox.Checked)
    {
    settings.Width = ((ResolutionItem)resolutionComboBox.SelectedItem).width; 
    settings.Height = ((ResolutionItem)resolutionComboBox.SelectedItem).height;
    settings.Format = ((ColorDepthItem)colorDepthComboBox.SelectedItem).value;
    settings.RefreshRate = ((RefreshRateItem)refreshRate
    ComboBox.SelectedItem).value; 
    }
    // Настройки были инициализированы. При следующем показа
     диалогового окна все элементы 
    // управления будут инициализированы согласно значениями 
    сохраненных настроек
    settings.Init = true; 
    // Пользователь настроил приложение. Показывать диалоговое 
    окно больше нет необходимости.
    settings.ShowSettingForm = false;
    DialogResult = DialogResult.OK; 
    }

    Интеграция диалогового окна в приложение

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

  • Вполне логично, что диалоговое окно должно отображаться, если не был найден файл конфигурации. Данную ситуацию можно распознать по свойству Init класса Settings равному true.
  • Если приложение потерпело неудачу при создании графического устройства, то при следующем запуске приложения не помешало бы отобразить диалоговое окно с настройками приложения – вполне вероятно, что изменение видеорежима поможет избежать проблемы. Как такое поведение реализовать на практике? При возникновении проблем приложение должно присвоить свойству ShowSettingForm конфигурации приложения значение true и завершить работу. А при следующем запуске приложение обнаружит, что свойство ShowSettingForm равно true, и отобразит диалоговое окно с настройками приложения.
  • Наконец, нужен механизм, позволяющий пользователю легко исправить настройки приложения по собственному желанию. Для этой цели можно встроить в обработчик события Load главной формы приложения распознавание ключа /config в параметрах командной строки приложения. Таким образом, инсталлятор приложения наряду с ярлыком запуска приложения может создать дополнительный ярлык "Настройка приложения ", вызывающий это же приложение с параметром /config.
  • Код, реализующий всю вышеперечисленную функциональность, будет иметь достаточно большой размер, поэтому его логично будет инкапсулировать в отдельный метод, чтобы не захламлять обработчик события Load (листинг 3.17).

    using System.Diagnostics;
    void InitGraphivsDevice() 
    {
    SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
    MinimumSize = SizeFromClientSize(new Size(1, 1));
    // Загружаем настройки приложения из файла
     settings = new Properties.Settings();
    // Определяем, содержит ли командная строка параметр  "/config
     " string[] args = Environment.GetCommandLineArgs(); bool
     configParam = false;
    if ((args.Length == 2)  (args[1].ToUpper() == "/CONFIG"))
     configParam = true;
    // Если выполняется одно из трех вышеописанных условий
    if ((configParam) || (settings.ShowSettingForm) || (!settings.Init)) 
    { 
    // Создаем диалоговое окно
    using (SettingsForm settingsForm = new SettingsForm(settings, 
    4> GraphicsAdapter.DefaultAdapter))
    { 
    // Отображаем диалоговое
     окно
    settingsForm.ShowDialog();	
    // Если командная строка содержит  "/config
     " if (configParam) 
    { 
    // Завершаем работу приложения closing =true;
    Application.Idle += new EventHandler(ApplicationIdle); return; 
    } 
     } 
    }
    presentParams = new PresentationParameters();
     presentParams.BackBufferCount = 1;
     presentParams.SwapEffect = SwapEffect.Discard;
    // Настраиваем параметры визуализации согласно настройкам приложения
     presentParams.IsFullScreen = settings.FullScreen;
    if (settings.FullScreen) 
    {
    presentParams.BackBufferWidth = settings.Width;
    presentParams.BackBufferHeight = settings.Height;
    presentParams.BackBufferFormat = settings.Format;
    presentParams.FullScreenRefreshRateInHz = settings.RefreshRate; 
    }
    else 
    {
    presentParams.BackBufferWidth = 0;
    presentParams.BackBufferHeight = 0;
    presentParams.BackBufferFormat = SurfaceFormat.Unknown;
    presentParams.FullScreenRefreshRateInHz = 0; 
    }
    // Проверяем наличие аппаратных вершинных процессоров
    GraphicsDeviceCapabilities caps = GraphicsAdapter.
    DefaultAdapter.GetCapabilities( 
    DeviceType.Hardware);
    CreateOptions options = CreateOptions.SingleThreaded;
    if (caps.DeviceCapabilities.SupportsHardwareTransformAndLight)
    options |= CreateOptions.HardwareVertexProcessing; else
    options |= CreateOptions.SoftwareVertexProcessing;
    try 
    { 
    // Создаем графическое устройство
    device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
     DeviceType.Hardware,
     this.Handle,
    options, presentParams); 
    }
    catch (InvalidOperationException) 
    { 
    // Если при создании графического устройства возникли проблемы
     closing = true;
    Application.Idle += new EventHandler(ApplicationIdle); 
    // Отображаем диалоговое окно с предложением изменить параметры 
    визуализации
    if (MessageBox.Show("Ошибка при создании графического устройства.
     Изменить  " + 
    "параметры визуализации (разрешение экрана и т.п.)?", "Ошибка",
     MessageBoxButtons.YesNo, 
    MessageBoxIcon.Error) == DialogResult.Yes)
    { 
    // При положительном ответе устанавливаем параметр ShowSettingForm 
    в значение true, 
    // указывающий на необходимость показать окно с настройками 
    приложения при следующем запуске
    settings.ShowSettingForm = true; 
    // Сохраняем настройки приложения в файл
    settings.Save(); 
    // Перезапускаем приложение
    Process.Start(Application.ExecutablePath); 
    }
    return; 
    } 
    }
    private void MainFormLoad(object sender, EventArgs e)
    {
    // Загружаем настройки из файла и создаем графическое устройство
    InitGraphivsDevice(); 
    // Если приложение завершает работу, выходим из обработчика 
    события Load
    if (closing)
     return;
    // Остальные действие (создание декларации вершины, заполнение 
    массива вершин, загрузка
    // эффекта и т.п.
    ...
    }

    Остальные фрагменты приложения не содержат чего-либо заслуживающего внимания, и вы сможете легко их реализовать самостоятельно. Готовое приложение находится в example.zip в каталоге Examples\Ch03\Ex03.

    Местоположение файла настроек приложения

    .NET Framework 2.0 сохраняет пользовательские настройки в XML -файле user.config, который располагается по достаточно запутанному пути:

    <Profile Directory>\<Company Name>\<App 
    Name><Evidence Type><Evidence
    Hash>\<Version>\user.config

    где

  • Profile Directory - каталог локального профиля приложения, обычно имеющий название вроде c:\Documents and Settings\<Имя Пользователя>\Local Settings\Application Data
  • Company Name - строка, формируемая на основе названия компании, заданного атрибутом AssemblyCompany в файле Properties\AssemblyInfo.cs.
  • App Name - строка, формируемая на основе названия приложения, заданного атрибутом AssemblyProduct в файле Properties\AssemblyInfo.cs.
  • Evidence Type, Evidence Hash - вычисляются на основе информации о домене приложения.
  • Version - строка, формируемая на основе версии приложения, заданной атрибутом AssemblyVersion в файле Properties\AssemblyInfo.cs.
  • Например, на моем компьютере пример Сh03\Ex06 хранит информацию о конфигурации приложения в файле C:\Documents and Settings\Administrator\Local Settings\Application Data\GSPInc\Ex06-FullscreenDialog.Url31gvfqvzegievbxah0w1qmgu2s2siyo3\1.0.0.0\user.config. Сам файл имеет простую структуру и легко может быть проанализирован любым продвинутым пользователем:

    <?xml version="1.0" encoding="utf-8"?> <configuration>
    <userSettings>
    <GSP.XNA.Book.Ch03.Ex06.Properties.Settings>
    <setting name="Width" serializeAs="String">
    <value>1280</value>
    </setting>
    <setting name="Height"
    serializeAs="String">
    <value>960</value>
    </setting> 
    <setting name="Format"
    erializeAs="String">
    <value>Bgr32</value>
    </setting> 
    <setting name="RefreshRate"
    serializeAs="String">
    <value>85</value> </setting>
    <setting name="FullScreen"
    serializeAs="String">
    <value>True</value>
    </setting>
    <setting name="Init"
    serializeAs="String">
    <value>True</value> </setting>
    <setting name="ShowSettingForm"
    serializeAs="String">
    <value>False</value> </setting>
     </GSP.XNA.Book.Ch03.Ex06.Properties.Settings>
    </userSettings> </configuration>

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

    3.3. Анимация

    До сих пор мы визуализировали исключительно статичные изображение, в то время как XNA Framework в первую очередь предназначен для визуализация динамичных сцен с движущимися объектами. Как создать анимированную сцену? Обратимся к мультипликации и кинематографу, в которых эффект движения создается путем вывода на экран последовательности слегка различающихся изображений (кадров). Экспериментально было установлено, что для создания эффекта плавного движения частота смены кадров должна быть порядка 25-ти FPSFPS – Frames Per Second (кадры в секунду) .

    И так, для создания анимации мы должны отображать визуализировать различные фазы движения изображения с частотой 25 кадров в секунду. Наше первое приложение будет визуализировать шарик (точнее диск), летающий по форме и отскакивающий от ее стенок (рисунок 3.11). Анимация будет моделироваться посредством таймера (компонент Timer ), тикающего с интервалом 40 миллисекунд (то есть 25 раз в секунду). После каждого тика таймера мы будем прибавлять к координатам шарика значение вектора скорости и перерисовывать сцену. При пролете шарика сквозь стенку он будет отскакивать обратно, при этом вектор скорости будет изменяться на противоположный. Для придания движениям шарика некоторой неопределенности, модуль вектора скорости будет изменяться на незначительную случайную величину. Основные фрагменты приложения приведены в листинге 3.18. Исходный код примера находится в example.zip в каталоге Examples\Ch03\Ex07.

    (рис 3.18)  Прыгающий шарик (рис 3.11) public partial class MainForm : Form 
    {
    // Файл эффекта, используемый для визуализации изображения const
    string effectFileName = "Data\\ColorFill.fx";
    // Минимальная скорость диска ( "шарика
    ") const float diskMinSpeed = 0.5f;
    
    // Максимальная скорость диска
    const float diskMaxSpeed = 1.3f; 
    // Количество сегментов в диске
    const int diskSlices = 32; 
    // Радиус диска
    const float diskRadius = 0.1f; 
    // Цвет центра диска
    readonly static XnaGraphics.Color diskInnerColor = 
    XnaGraphics.Color.White; 
    // Цвет края диска
    readonly static XnaGraphics.Color diskOuterColor = 
    XnaGraphics.Color.Green;
    // Толщина стенки вдоль границы экрана
    const float borderSize = 0.1f; 
    // Цвет внутренней границы стенки
    readonly static XnaGraphics.Color borderInnerColor = 
    XnaGraphics.Color.DarkBlue; 
    // Цвет внешней границы стенки
    readonly static XnaGraphics.Color borderOuterColor = 
    XnaGraphics.Color.CornflowerBlue;
    GraphicsDevice device = null;
    PresentationParameters presentParams; Effect
    effect = null; VertexDeclaration decl =
     null;
    // Массив вершин стены вдоль края формы
    VertexPositionColor[] borderVerts = null; 
    // Массив вершин диска с центром в начале системы координат
    VertexPositionColor[] baseDiskVerts = null; 
    // Массив вершин диска, перемещаемого по поверхности экрана
    VertexPositionColor[] diskVerts = null;
    FillMode fillMode = FillMode.Solid;
    // Скорость диска вдоль оси X
    float speedX; 
    // Скорость диска вдоль оси Y
    float speedY; 
    // Координата X центра диска
    float posX = 0; 
    // Координата Y центра диска
    float posY = 0;
    // Генератор случайных чисел
    Random rnd = new Random();
    // Конфигурация приложения (разрешение экрана и т.п.)
     Properties.Settings settings;
    bool closing = false;
    // Вычисляет случайную скорость диска, лежащую в 
    диапазоне diskMinSpeed .. diskMaxSpeed float
     RndSpeed() 
    {
    return diskMinSpeed + (float)rnd.NextDouble() * 
    (diskMaxSpeed - diskMinSpeed); 
    }
    // Обработчик события Load главной формы
    private void MainForm_Load(object sender, EventArgs e)
    { 
    // Чтение файла конфигурации и создание графического устройства
     InitGraphivsDevice();
    if (closing) return;
    // Создание декларации вершины
    decl = new VertexDeclaration(device, 
    VertexPositionColor.VertexElements);
    // Создание и заполнение массива вершин,
     визуализируемого с использованием списка 
    // треугольников (PrimitiveType.TriangleStrip)
    borderVerts = new VertexPositionColor[10];
    borderVerts[0] = new VertexPositionColor(new 
    Vector3(-1.0f, -1.0f, 0.0f),
     borderOuterColor);
    borderVerts[1] = new VertexPositionColor(new 
    Vector3(-1.0f + borderSize, -1.0f + borderSize, 0.0f),
    borderInnerColor);
    borderVerts[2] = new VertexPositionColor(new 
    Vector3(-1.0f, 1.0f, 0.0f),
     borderOuterColor);
    borderVerts[3] = new VertexPositionColor(new 
    Vector3(-1.0f + borderSize, 1.0f -borderSize, 0.0f),
    borderInnerColor);
    borderVerts[4] = new VertexPositionColor(new 
    Vector3(1.0f, 1.0f, 0.0f),
     borderOuterColor);
    borderVerts[5] = new VertexPositionColor(new 
    Vector3(1.0f - borderSize, 1.0f -borderSize, 0.0f),
    borderInnerColor);
    borderVerts[6] = new VertexPositionColor(new 
    Vector3(1.0f, -1.0f, 0.0f),
     borderOuterColor);
    borderVerts[7] = new VertexPositionColor(new 
    Vector3(1.0f - borderSize, -1.0f + borderSize, 0.0f),
    borderInnerColor);
    borderVerts[8] = new VertexPositionColor(new 
    Vector3(-1.0f, -1.0f, 0.0f),
     borderOuterColor);
    borderVerts[9] = new VertexPositionColor(new
     Vector3(-1.0f + borderSize, -1.0f + borderSize, 0.0f),
    borderInnerColor);
    // Создание диска с центром в начале координат. 
    Диск визуализируется с использованием веера 
    // треугольников (PrimitiveType.TriangleFan)
    baseDiskVerts = new VertexPositionColor[diskSlices + 2];
    baseDiskVerts[0] = new VertexPositionColor(new 
    Vector3(0.0f, 0.0f, 0.0f),
     XnaGraphics.Color.White);
    for (int i = 0; i <= diskSlices; i++) {
    float angle = (float)i / (float)diskSlices * 2.0f * (float)Math.PI;
    float x = diskRadius * (float)Math.Sin(angle);
    float y = diskRadius * (float)Math.Cos(angle);
    baseDiskVerts[i + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f),
     diskOuterColor); };
    // Создаем массив вершин диска путем клонирования. 
    Таким образом, при старте приложения диск 
    // расположен в начале системы координат.
    diskVerts = (VertexPositionColor[]) baseDiskVerts.Clone();
    // Задаем начальную скорость диска вдоль осей X и Y speedX
     = RndSpeed(); speedY = RndSpeed();
     }
    // Обработчик события Paint главной формы
    private void MainForm_Paint(object sender, PaintEventArgs e) 
    { 
    ... 
    // Визуализируем сцену
    effect.Begin();
    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
    {
    pass.Begin();
    device.DrawUserPrimitives(PrimitiveType.TriangleStrip, borderVerts, 0,
     borderVerts.Length - 2);
    device.DrawUserPrimitives(PrimitiveType.TriangleFan, diskVerts, 0,
     diskVerts.Length - 2);
    pass.End(); 
    } effect.End();
    device.Present(); 
    ...
    }
    // Обработчик события Tick таймера, свойству Interval
     которого присвоено значение 40 private
     void timer_Tick(object sender, EventArgs e) 
    { 
    // Изменяем координаты центра диска на расстояние, 
    которое он должен пройти за время между 
    // двумя тиками таймера
    posX += speedX * (float)timer.Interval * 0.001f; posY +=
     speedY * (float)timer.Interval * 0.001f;
    // Если диск столкнулся с правым краем границы
    if (posX >= 1 - diskRadius - borderSize) 
    { 
    // Диск не должен перелетать за границу
    posX = 1 - diskRadius - borderSize; 
    // Изменяем направление движения диска на противоположное
     и вычисляем новую скорость. speedX
     = -Math.Sign(speedX) * RndSpeed(); 
    }
    // Если диск столкнулся с верхним краем границы
    if (posY >= 1 - diskRadius - borderSize) 
    {
    posY = 1 - diskRadius - borderSize;
    speedY = -Math.Sign(speedY) * RndSpeed(); 
    }
    // Если диск столкнулся с левым краем границы
    if (posX <= -1 + diskRadius + borderSize) 
    
    posX = -1 + diskRadius + borderSize;
    speedX = -Math.Sign(speedX) * RndSpeed(); 
    }
    // Если диск столкнулся с нижним краем границы
    if (posY <= -1 + diskRadius + borderSize)
     {
    posY = -1 + diskRadius + borderSize;
    speedY = -Math.Sign(speedY) * RndSpeed(); 
    }
    // Вычисляем новые координаты диска на основе  
    "эталонного " диска с центром 
    в начале 
    // координат.
    for (int i = 0; i < baseDiskVerts.Length; i++) 
    {
    diskVerts[i].Position.X = baseDiskVerts[i].Position.X + posX;
     diskVerts[i].Position.Y = baseDiskVerts[i].Position.Y + posY; 
    }
    // Перерисовываем изображение Invalidate(); 
     } 
    }

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

    Запустите приложение на выполнение. Первое что бросится в глаза – это движение окружности рывками. Но ведь этого не может быть! Фильмы то при частоте 25 fps идут очень плавно. В чем же принципиальная разница между фильмом и нашим приложением? Все очень просто. В кинематографе кадры снимается с некоторой экспозициейВремя, в течение которого объектив аппарата остается открытым при фото- и киносъемке . В результате каждый кадр фильма содержит усредненное изображение за период экспозиции, что проявляется в смазывании быстродвижущихся объектов. Таким образом, 25 FPS в кинофильме и 25 FPS в нашем приложении – это немного разные FPS. В частности, в компьютерных играх при частоте кадров 25-30 FPS ощущаются некие "подергивания " в динамичных сценах – объекты движутся как бы рывками. Эту проблему решают методом грубой силы, то есть простым увеличением частоты кадров – практический опыт показыва ет, что в большинстве случаев вполне достаточно частоты кадров 60 FPS.

    Дополнительная информация

    Для визуализации быстродвижущихся объектов вроде спортивного автомобиля, пролетающего мимо наблюдателя со скоростью 300 км/ч, даже частота кадров 60 FPS оказывается недостаточной. С другой стороны, при попытке наращивания FPS мы упираемся в возможности аппаратуры – максимальная частота вертикальной развертки современных мониторов (то есть частота смены кадров) редко превышает 60-100 Hz. Поэтому для повышения реалистичности движений применяют технологии Motion BlurЭффект размывания изображения быстро движущегося объекта 55, которые различными способами пытаются имитировать экспозицию при "киносъемке " кадров изображения.

    Ну что ж, попробуем увеличить частоту смены кадров. Как известно, компонент таймер может тикать до 64-х раз в секунду, т.е. минимальный интервал между двумя тиками равен 0.015 миллисекунд. Присвойте свойству Interval таймера значение 15 и снова запустите приложение. Шарик будет двигаться ощутимо плавнее, однако скорость не будет постоянной: периодически шарик будет ни с того то замедляться, то ускоряться.

    Почему это происходит? Для ответа на этот вопрос необходимо внимательно прочитать описания таймера Windows в MSDN. Хотя таймер и может тикать с интервалом 15 миллисекунд, точность каждого тика составляет 55 миллисекунд. Таким образом, по мере уменьшения интервала между тиками, точность таймера катастрофически падает, что приводит к неравномерности движения объектов. Но есть еще один немаловажный фактор – события от таймера имеют самый низкий приоритет среди всех сообщений. Например, если пользователь в это время нажимает клавишу клавиатуры, а в очереди сообщений находится необработанное сообщение WM_TIMER, то сообщения WM_KEYDOWN, WM_CHAR и WM_KEYUP будут вставлены в очередь перед сообщением WM_TIMER. По сути, сообщение WM_TIMER обрабатывается только при условии пустой очереди событий потока, а так как в очереди может находиться не более одного сообщения wmtimer, таймер может "терять " тики. Чтобы убедиться в этом, попробуйте ухватиться указателем мыши за край формы и поизменять ее размер, что тут же приведет к потере сообщений и, соответственно, замедлению шарика.

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

    3.3.1. Использование события Idle

    Итак, использование обычного компонента Timer не увенчалось успехом из-за ряда ограничений системного таймера Windows. Кроме того, использование таймера для визуализации сцены обладает еще одним фундаментальным ограничением. Дело в том, что, задавая определенную частоту визуализации кадров, мы делаем неявное предположение, что абсолютно любой компьютер может визуализировать сцену с требуемой частотой кадров. Если это вдруг окажется не так, приложение будет работать в "замедленном " режиме. Учитывая, что производительность видеоподсистем различных компьютеров может отличаться в десятки раз, возникновение подобной проблемы более чем вероятно.

    Что же делать? Раз визуализация сцены с фиксированной частотой кадров чревата возникновением множества проблем, надо просто визуализировать кадры с максимально возможной частотой, для чего достаточно поместить в обработчик события Idle вызов метода Invalidate. Для измерения временных интервалов между вызовами обработчика события Idle можно воспользоваться свойством System.Enviroment.TickCount, возвращающим количество миллисекунд, прошедших с момента загрузки операционной системы. Основные фрагменты нового варианта кода приведены в листинге 3.19.

    // Пример Examples\Ch03\Ex08
    public partial class MainForm : Form
    {
    // Значение свойства System.Enviroment.TickCount 
    во время последнего вызова обработчика
    // события Idle.
    int lastTick;
    private void MainForm_Load(object sender, EventArgs e)
    { ...
    // Запоминаем текущее значение свойства System.Enviroment.TickCount
     lastTick = Environment.TickCount;
    Application.Idle += new EventHandler(Application_Idle);
     }
    void Application_Idle(object sender, EventArgs e) 
    { 
    // Если приложение завершает работу, закрываем 
    главную форму. if (closing) 
    {
    Close(); return; }
    int currentTick = Environment.TickCount; 
    // Вычисляем время (в секундах), прошедшее между 
    двумя вызовами обработчика Idle float
     delta = (float)(currentTick - lastTick) * 0.001f;
    // Изменяем положение диска
    posX += speedX * delta; posY += speedY * delta;
    // Обрабатываем столкновение с стеной вдоль границы экрана 
    ...
    // Запоминаем текущее время с момента загрузки операционной системы
    lastTick = currentTick; 
    // Перерисовываем экран
    Invalidate(); 
    } 
    ... 
    }

    Шарик примера Ch03\Ex08 движется ощутимо плавнее, однако небольшие, еле заметные рывки все же остались. Для определения причины рывков вставьте в обработчик события Idle команду Trace.WriteLine(delta), запустите приложение, вернитесь в Visual Studio и посмотрите содержимое окна Output. Скорее всего, его содержимое будет выглядеть примерно следующим образом:

    delta	=	0,078
    delta	=	0
    delta	=	0
    delta	=	0,016
    delta	=	0,015
    delta	=	0
    delta	=	0,016
    delta	=	0,015
    delta	=	0,016
    delta	=	0
    delta	=	0,016

    Обратите внимание на множество нулевых значений параметра delta, соответствующих полной остановке шарика на месте, которая воспринимается пользователем как небольшое подергивание. Почему между некоторыми вызовами события Idle проходит 15-16 миллисекунд, а между другими – меньше одной миллисекунды. Подобный разброс производительности не выглядит правдоподобным, значит дело в чем-то ином. Для выяснения причин этой аномалии придется обратиться к MSDN. Метод Environment.TickCount использует функцию Win32 GeTickCount, точность которой порядка 10 миллисекунд. Соответственно, при попытке измерить временной интервал близкий к 10 миллисекундам метод Environment.TickCount может вернуть два одинаковых значения, что мы и наблюдаем.

    3.3.2. Использование высокоточного таймера Stopwatch

    Так как точность измерения времени посредством свойства Environment.TickCount зачастую оказывается недостаточной, Microsoft по многочисленным просьбам трудящихся добавил в .NET Framework 2.0 класс System.Diagnostics.Stopwatch, предназначенный для высокоточного измерения временных интервалов. Данный класс весьма прост в использовании. Сначала приложение должно создать экземпляр класса Stopwatch и запустить таймер методом Start. Затем по мере необходимости приложение посредством свойства ElapsedMilliseconds получает количество миллисекунд, прошедшее с момента запуска таймера. Когда же все измерения времени выполнены, приложение останавливает таймер командой Stop.

    Для оценки точности таймера Stopwatch можно воспользоваться статическим свойством Frequency, возвращающее количество тиков высокоточного таймера, используемого классом Stopwatch для измерения временных интервалов, за 1 секунду. На моем компьютере свойство Frequency равно 1.870.000.0000, что соответствует фантастической точности $$1/frac18700000000 = 5?10^{-10}сек$$.

    Так как метод ElapsedMilliseconds по определению не может изменять интервалы меньше 1 миллисекунды, в классе Stopwatch имеется один метод ElapsedTicks, возвращающий количество тиков таймера. Для перевода тиков в секунды значение свойства ElapsedTicks надо поделить на Stopwatch.Frequency. При этом следует всегда помнить о том, что при измерении сверхкоротких интервалов появляются иные погрешности, вызванные переключением задач, кэш промахами, длинным конвейером процессора, накладными затратами на вызовы функций и т.п., в результате чего предельная точность измерений таймера Stopwatch редко достижима на практике.

    Код приложения, переписанный с использованием таймера Stopwatch приведен в листинге 3.20.

    // Пример Examples\Ch03\Ex09
    public partial class MainForm : Form
    {
    // Высокоточный таймер
    Stopwatch stopwatch; 
    // Время, прошедшее с момента запуска таймера при
     последнем вызове обработчика события
     // Idle
    long lastTime;
    private void MainForm_Load(object sender, EventArgs e) 
    { 
    ... 
    // Запускаем таймер
    stopwatch = new Stopwatch();
    stopwatch.Start();
    lastTime = 0;
    // Выводим в окно Output точность таймера
    Trace.WriteLine("Accuracy = " + 1.0 / Stopwatch.Frequency + " sec");
     Application.Idle += new EventHandler(Application_Idle); 
    }
    void Application_Idle(object sender, EventArgs e) 
    { 
    // Если приложение завершает работу, закрываем главную форму. If
     (closing) 
    {
    Close(); return; }
    // Получаем количество миллисекунд, прошедших с 
    момента запуска таймера
    double currentTime = (double)stopwatch.ElapsedTicks / 
    (double)Stopwatch.Frequency;
    // Вычисляем время (в секундах), прошедшее между двумя 
    вызовами обработчика события Idle
    ` float delta = (float)(currentTick - lastTick);
    // Изменяем положение диска
    posX += speedX * delta; posy
     += speedY * delta;
    // Обрабатываем столкновение с краями экрана 
    ...
    // Запоминаем текущее показание таймера
    lastTime = currentTime; 
    // Перерисовываем экран 
    Invalidate(); 
    } 
    ... 
    }

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

    Accuracy = 5,3475935828877E-10 sec
    delta =	0,035
    delta =	0,006
    delta =	0,003
    delta =	0,002
    delta =	0,011
    delta =	0,012
    delta =	0,012
    delta =	0,012
    delta =	0,011
    delta =	0,013
    delta =	0,011

    Примечание

    Обратите внимание на ощутимую задержку при визуализации первого кадра, обусловленную накладными расходами JIT -компилятора при компиляции метода Paint.

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

    Accuracy = 5,3475935828877E-10 sec
    delta =	0,035
    delta =	0,022
    delta =	0,013
    delta =	0,032
    delta =	0,015
    delta =	0,032
    delta =	0,014
    delta =	0,032
    delta =	0,016
    delta =	0,032
    delta =	0,014
    delta =	0,016
    delta =	0,019

    Все дело в вертикальной синхронизации, которая будет рассмотрена в следующем разделе.

    3.3.3 Управление вертикальной синхронизацией

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

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

    Для борьбы с этим недоразумением XNA Framework предоставляет программисту возможность управления синхронизацией переключения заднего и экранного буферов с моментом окончания формирования электронным лучом изображения на экране монитора. Эта функциональность доступна посредством свойства PresentationInterval класса PresentationParameters:

    public PresentInterval PresentationInterval { get; set; }

    В подавляющем большинстве случаев свойству PresentationInterval присваивают одно из следующих трех значений перечислимого типа PresentInterval:

  • PresentInterval.Default - значение по умолчанию, аналогичное PresentInterval.One.
  • PresentInterval.One - буферы переключаются только по окончанию обновления электронным лучом изображения на экране монитора.
  • PresentInterval.Immediate - кадровые буферы переключаются так быстро, насколько это возможно.
  • Хотя значения PresentInterval.Default и PresentInterval.One формально являются братьями-близнецами, между ними есть одна очень тонкая разница. При использовании PresentInterval.Default синхронизации с обратным ходом луча осуществляется посредством обычного таймера Windows, который, как вы помните, обладает очень низкой точностью. Из-за особенностей организации оконной подсистемы Windows низкая точность таймера приводит к частым задержкам при переключении буферов и, соответственно, "рывкам " при анимации. В полноэкранном режиме последствия, как правило, не столь серьезны.

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

    Итак, однозначно правильного режима смены кадров не существует, поэтому было бы разумным предоставить этот выбор пользователю. Для этой цели я поместил на форму диалогового окна Параметры дополнительный флажок Вертикальная синхронизация ( vsynchCheckBox ), установка которого соответствует использованию режима PresentInterval.One, а снятие - PresentInterval.Immediate (рисунок 3.12). Для сохранения значения режима вертикальной синхронизации в настройки приложения было добавлено дополнительное поле PresentationInterval типа Mcrosoft.Xna.Framework.Graphics.PresentInterval, а сам код приложения подвергся косметической доработке (листинг 3.21). Готовое приложение находится в example.zip в каталоге Examples\Ch03\Ex10.

    (рис 3.21)  Диалоговое окно Параметры с новым флажком Вертикальная синхронизация (рис 3.12) // Класс главной формы приложения public
     partial class MainForm : Form 
    {
    void InitGraphivsDevice() 
    { 
    ...
    presentParams = new PresentationParameters();
    presentParams.BackBufferCount = 1;
    presentParams.SwapEffect = SwapEffect.Discard;
    presentParams.IsFullScreen = settings.FullScreen;
    // Устанавливаем режим вертикальной синхронизации 
    согласно настройкам приложения
    presentParams.PresentationInterval = settings.PresentationInterval; 
    ...
    }
     ...
     }
    // Класс диалогового окна  "Параметры "
    public partial class SettingsForm : Form
    {
    // Конструктор диалогового окна
    internal SettingsForm(Properties.Settings settings, 
    GraphicsAdapter adapter) 
    { 
    ...
    // Устанавливает значение флажка  "Вертикальная 
    синхронизация " согласно настройкам
     приложения if (settings.PresentationInterval == PresentInterval.One)
    vsynchCheckBox.Checked = true;
     else
    vsynchCheckBox.Checked = false; 
    }
    // Обработчик нажатия кнопки Ok
    private void okButton_Click(object sender, EventArgs e) 
    { 
    ...
    // Сохраняем в настройках приложения информацию о
     режиме кадровой синхронизации if
     (vsynchCheckBox.Checked)
    settings.PresentationInterval = PresentInterval.One;
     else
    settings.PresentationInterval = PresentInterval.Immediate; 
    ...
    } 
    }

    Запустите приложение и попробуйте поэкспериментировать с настройками кадровой синхронизации, наблюдая за окном Output. При включенной вертикальной синхронизации интервал между визуализацией кадров будет примерно равен константе 1.0/{частота обновления экрана текущего видеорежима}. При отключении вертикальной синхронизации частота интервал смены кадров уменьшится до величины сопоставимой с 1 мс, что соответствует частоте порядка 1000 fps. При этом и в том и в другом случае движения шарика будут плавными.

    3.3.4. Замена циклов foreach на for

    При визуализации диска проходы ( Passes ) эффекта перебираются посредством цикла foreach.

    Достоинствами цикла foreach является простота кода и защита от потенциальных ошибок вроде использования неправильного индекса при обращении к элементу коллекции. Обратной стороной является неявное использование циклом foreach специального типа, реализующего интерфейс Enumerator. Например, код

    List<int> numbers = new List<int>(3);
    ...
    int s = 0;
    foreach (int v in numbers) s += v;

    неявно заменяется компилятором следующим аналогом:

    List<int> numbers = new List<int>(3);
    ...
    int s = 0;
    // Создается объект enumerator
    IEnumerator enumerator = numbers.GetEnumerator(); 
    // Перебирает элементы коллекции while
     (enumerator.MoveNext())
    // Получает доступ к текущему элементу коллекции s
     += (int)enumerator.Current;

    На первый взгляд, данный код является неэффективным. Но в действительности все не так уж и плохо. Во-первых, метод GetEnumerator возвращает структуру:

    public class List<T> : IList<T>, 
    ICollection<T>, IEnumerable<T>, IList,
    ICollection,
    IEnumerable
    {
    public Enumerator<T> 
    GetEnumerator() {
    return new Enumerator<T>((List<T>)
     this); }
    ...
    public struct Enumerator : IEnumerator<T>, IDisposable,
     IEnumerator 
    {
    private List<T> list;
    private int index;
    private int version;
    private T current;
    internal Enumerator(List<T> list);
    public void Dispose();
    public bool MoveNext();
    public T Current { get; }
    object IEnumerator.Current { get; }
    void IEnumerator.Reset(); }
    }

    Соответственно, вызов метода GetEnumerator не приводит к выделению памяти в управляемой куче и учащению вызовов сборщика мусора. Во-вторых, компилятор C# вызывает метод MoveNext и свойство Current структуры Enumerator напрямую без приведения к ссылке на интерфейс IEnumerator, так что боксирование ( boxing ) структуры Enumerator не происходит. И, наконец, компилятор может встроить ( inline ) код метода MoveNext и свойства Current непосредственно в код цикла, избежав накладных расходов на вызовы методов. Так что при использовании массивов или коллекций наподобие List разница в производительности циклов foreach и for будет исключающее мала, и в подавляющем большинстве случаев на нее можно не обращать внимания.

    Однако при использовании других коллекций не все так просто. Например, у некоторых объектов метод GetEnumerator возвращает объект, размещаемых в управляемой куче. Подобный подход имеет два недостатка:

  • Частый вызов цикла foreach для перебора элементов такой коллекции приведет к созданию множества объектов enumerator, и, соответственно, частому вызову сборщика мусора, что привет к падению производительности.
  • Если метод MoveNext является виртуальным, то компилятор не сможет встроить его непосредственно в код цикла, что тоже негативно скажется на производительности.
  • Таким образом, в критичных к производительности приложениях при переборе элементов коллекций, возвращающих объект enumerator, имеет смысл избегать циклов foreach.

    Но давайте вернемся к коллекции Passes класса Effect. Данная коллекция реализуется классом EffectPassCollection, объявленным следующим образом:

    public sealed class EffectPassCollection :
     IEnumerable<EffectPass> 
    {
    // Список для хранения информации о проходах
     private List<EffectPass> pPass;
    // Возвращает объект (полученный путем боксирования 
    структуры), реализующий интерфейс 
    // IEnumerator<EffectPass>
    public IEnumerator<EffectPass> GetEnumerator()
    {
    return (IEnumerator<EffectPass>) this.pPass.GetEnumerator();
    } 
    }

    Давайте внимательно рассмотрим этот код. Класс EffectPassCollection в действительности хранит информацию о проходах в коллекции List. Соответственно метод this.pPass. GetEnumerator возвращает ссылку на структуру Enumerator класса List, определение которой было приведено в начале раздела.

    И все бы было просто замечательно, если бы не один маленький нюанс - структура IEnumerator приводится к интерфейсу IEnumerator<EffectPass>, что приводит к боксированию и выделению памяти в управляемой куче. Чтобы оценить влияние этой особенности на производительность приложения, мы встроим в обработчик события Paint код, перебирающий элементы коллекции 10.000.000 раз посредством циклов for и foreach с измерением времени их выполнения (листинг 3.22).

    // Пример Examples\Ch03\Ex11 #define TEST
    #if TEST
    bool testing = true;
    #endif
    private void MainForm_Paint(object sender, PaintEventArgs e)
    {
    // Измерение выполняется только при условии определения 
    идентификатора TEST
    #if TEST
    // Тест выполняется только один раз
    if (testing)
    { 
    // Число итераций
    const int n = 10000000;
     int sum;
    // Выводим количество сборок мусора, выполненных на момент 
    начала эксперимента
     Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
    Stopwatch timer = new Stopwatch(); 
    // Запускаем таймер
    timer.Start();
    // Перебираем элементы коллекции passes и суммируем коды первой буквы
     sum = 0;
    EffectPassCollection passes = effect.CurrentTechnique.Passes; 
    // Выполняем n итераций
    for (int i = 0; i < n; i++) 
    // Перебираем элементы коллекции passes посредством 
    for и суммируем коды первой буквы for
     (int j = 0; j < passes.Count; j++) sum += (int)passes[j].Name[0];
    // Вычисляем суммарное время, затраченное на интеграции
    double time1 = (double)timer.ElapsedTicks / (double)Stopwatch.Frequency;
    // Отображаем отчет
    Trace.WriteLine(" 	 for 	 ");
    Trace.WriteLine("Sum : " + sum.ToString());
    Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
    Trace.WriteLine("Time1 : " + time1);
    Trace.WriteLine("");
    // Перезапускаем таймер
     timer.Reset(); timer.Start();
    sum = 0; 
    // Снова выполняем итерации, но уже посредством цикла foreach for
     (int i = 0; i < n; i++)
    foreach (EffectPass pass in effect.CurrentTechnique.Passes) sum 
    += (int)pass.Name[0];
    // Вычисляем суммарное время, потраченное на итерации
    double time2 = (double)timer.ElapsedTicks / 
    (double)Stopwatch.Frequency;
    // Отображаем отчет
    Trace.WriteLine(" 	 foreach 	 ");
    Trace.WriteLine("Sum : " + sum.ToString());
    Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
    Trace.WriteLine("Time2 : " + time2); 
    // Определяем разницу в 
    производительности
    Trace.WriteLine("Time2 / Time1 = " + time2 / time1);
    Trace.WriteLine("");
    testing = false; }
    #endif 
    }

    Запустив модифицированное приложение на своем компьютере с процессором Intel Pentium-4 2.8C я получил следующие результаты:

    Accuracy = 3,5730747380043E-10 sec
    Gen 0 collection count: 3
    	 for 	
    Sum : 1120000000
    Gen 0 collection count: 3
    Time1 : 0,554915015846586
    	 foreach 	
    Sum : 1120000000
    Gen 0 collection count: 915
    Time2 : 1,84470303389776
    Time2 / Time1 = 3,32429828211344

    Как видно, в процессе работы цикла foreach сборщик мусора был вызван 915 разРезультаты, полученные на других компьютерах, могут заметно отличаться. Так на моем втором домашнем компьютере с процессором Intel Core2 Due E6300 при выполнении циклов foreach сборщик мусора был вызван 229 раз. В целом же, частота вызовов сборщика мусора в первую очередь зависит от размера кэша второго уровня CPU. Например, размер кэша процессора Intel Core2 Due E6300 (2MB) в четыре раза больше, чем у Pentium-4 2.8C (512KB). Соответственно, сборщик мусора на Intel Core2 Due E6300 вызывается в 4 раза реже по сравнению с Pentium-4 2.8C (915/229 = 3.996)., а разница в производительности между циклами for и foreach достигла трехкратной величины. Таким образом, перебор элементов коллекции foreach в методе Paint, вызываемом около сотни раз в секунду, является не самой лучшей идеей. Особенно если учесть, что реальные приложения зачастую содержат десятки эффектов, а внезапная сборка мусора при визуализации кадра может привести заметному провалу производительности.

    Примечание

    На самом деле Microsoft здорово поработала над эффективностью сборщика мусора, и сейчас сборка мусора в поколении 0 обычно занимает порядка 1 мс. В частности, пренебрегая накладными затратами цикла foreach, можно прикинуть, что в нашем эксперименте каждая сборка мусора выполнялась в среднем не более чем за (1.84 - 0.55) / 915 = 0.0014 секунд. Тем не менее, рано или поздно частые сборки мусора могут спровоцировать сбор мусора в старших поколениях, который будет воспринят пользователем как внезапное "подтормаживание " приложения.

    3.3.5. Устранение зависимости движений диска от производительности компьютера

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

    В принципе для ликвидации данного недостатка можно было бы найти аналитическое выражение зависимости координат центра диска от времени. Однако такое решение будет пригодно лишь для самых простых случаев. Поэтому мы реализуем более универсальный подход: моделирование движений диска с некоторым фиксированным шагом, например, 0.005 секунды. Реализация данной технологии потребует лишь косметической правки обработчика события Idle (листинг 3.23).

    // Дискретный шаг времени (в секундах), с которым выполняются расчеты
    const float timeStep = 0.005f;
    // Максимальный временной интервал между двумя
     вызовами обработчика события Idle (в секундах)
    const float maxDelta = 5.0f;
    void Application_Idle(object sender, EventArgs e)
    {
    ...
    // Определяем текущее время
    double currentTime = (double)stopwatch.ElapsedTicks /
     (double)Stopwatch.Frequency;
    // Если время между двумя вызовами обработчика Idle 
    превышает maxDelta, корректируем lastTime 
    // во избежание слишком длинной работы обработчика события 
    Idle if (currentTime - lastTime > maxDelta) 
    lastTime = currentTime - maxDelta;
    
    // Моделируем движения шарика дискретным шагом времени timeStep 
    while (lastTime + timeStep < currentTime) 
    {
    posX += speedX * timeStep; 
    posY += speedY * timeStep;
    lastTime += timeStep; }
    Invalidate(); 
    }

    В самых запущенных случаях задержка между визуализацией кадров может достигать минуты и даже больше. Так как моделирование игровой логики интервала времни в несколько минут может занять достаточно существенно время, в приложении используется простой прием - игровая логика рассчитывается не более чем за 5 секунд игрового времени (константа maxDelta ). Для пользователя подобный трюк является незаметным, ведь он все равно не сможет спрогнозировать поведение приложения на значительный временной интервал.

    Примечание

    В интерактивных приложениях вроде автосимуляторов при возникновении длительной задержки между кадрами имеет смысл приостановить игровой процесс, ведь в течение задержки в несколько секунд автомобиль игрока наверняка слетит с трассы или столкнется с препятствием. Эффекта автоматической приостановки игрового процесса можно достичь путем присвоения константе maxDelta небольшого значения порядка 0.2 - 0.5 секунд.

    3.4. Визуализация полупрозрачных примитивов

    В компьютерной графике довольно часто приходится моделировать различные полупрозрачные объекты вроде стекла, воды, тумана и т.п. Полупрозрачность - это характеристика объекта, показывающая, какую часть света пропускает среда (объект) без изменения направления его распространения. Так полностью прозрачный объект пропускает через себя весь свет, в результате чего не оказывает никакого влияния на находящиеся позади него объекты (иными словами, он является невидимкой). Непрозрачный объект напротив не пропускает через себя свет, и соответственно полностью перекрывает находящиеся позади него объекты. К слову, все наши объекты до сих пор являлись полностью не прозрачными.

    Коэффициент поглощения ? определенной среды есть количественная мера, позволяющая оценить, какая доля светового, падающего на поверхность раздела сред, проходит дальше, а какая поглощается. Например, если стекло имеет коэффициент поглощения 0.2, то результирующий цвет будет представлять собой объединение 20% цвета стекла и 80% цвета объектов позади стекла. То есть наблюдатель будет видеть как сам объект, так и предметы позади объекта.

    В общем случае итоговый цвет полупрозрачного объекта может быть определен по формуле:

    $$C=c_s-\alpha+c_d-(1-\alpha) $$

    где

    $$c$$ - результирующий цвет

    $$c_s$$ - цвет полупрозрачного объекта

    $$c_d$$ - цвет фона позади объекта

    $$\alpha$$ - коэффициент поглощения, равный 0 для полностью прозрачных объектов, и 1 для непрозрачных.

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

    3.4.1. Смешивание цветов

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

    (рис 3.13) Принцип работы блоков ROP

    Примечание

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

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

    $$\qquard d’=op(b-c,c-d) $$

    где

    $$\qquard d'$$ - новый цвет пикселя кадрового буфера $$[\qquard d'_r,\qquard d'_g,\qquard d'_b,\qquard d'_a] $$

    $$op$$ - операция (см. таблица 3.5)

    $$s$$ - цвет пикселя $$[s_r, s_g, s_b, s_a ] $$

    $$d$$ - текущий цвет пикселя кадрового буфера $$[d_r,d_g,d_b,d_a]$$

    $$b, с$$ - векторные коэффициенты $$[b_r,b_g,b_b,b_a]$$ и $$[c_r,c_g,c_b,c_a] $$

    Примечание

    Наряду с операциями смешения блоки ROP могут выполнять множество других полезных простых операций над пикселями вроде отсечения невидимых областей объектов при визуализации трехмерных цен, реализовывать полноэкранное сглаживание ( FSAA - Full Scene Anti Aliasing ) и т.д.

    Управление смешением пикселей осуществляется посредством свойств свойства RenderState экземпляра класса GraphicsDevice. Активация режима смешения осуществляется путем присвоения значения true свойству AlphaBlendEnable.

    public bool AlphaBlendEnable { get; set; }

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

    public BlendFunction BlendFunction { get; set; }
    Значения перечислимого типа BlendFunction
    Значение Описание
    Add (значение по умолчанию) Покомпонентно складывает два аргумента
    Subtract Покомпонентно вычитает из первого аргумента (цвет пикселя) второй (цвет кадрового буфера).
    ReverseSubtract Покомпонентно вычитает из второго аргумента (цвет кадрового буфера) первый (цвет пикселя).
    Max Покомпонентно сравнивает оба аргумента и возвращает компоненты с наибольшим значением
    Min Покомпонентно сравнивает оба аргумента и возвращает компоненты с наименьшим значением

    Каждый из аргументов операции предварительно умножается на коэффициент, определяемый посредством перечислимого типа Blen (таблица 3.6). При этом коэффициент $$b$$ задается свойством RenderState.SourceBlend, а коэффициент c – свойством RenderState.DestinationBlend:

    public Blend SourceBlend { get; set; } public 
    Blend DestinationBlend { get; set; }
    Некоторые значения перечислимого типа Blend
    Значение Описание
    BlendFactor В качестве коэффициента используется вектор, присвоенный свойству RenderState.BlendFactor
    InverseBlendFactor Коэффициент получается путем вычитания из единичного вектора (1, 1, 1, 1) значения свойства RenderState.BlendFactor.
    Zero Коэффициент равен вектору (0, 0, 0, 0).
    One Коэффициент равен вектору (1, 1, 1, 1).
    SourceColor В качестве коэффициента используются цвета примитива $$[s_r , s_g ,s_b, s_a ] $$
    InverseSourceColor В качестве коэффициента используется вектор $$[1-s_r,1-s_g,1-s_b,1-s_a] $$
    SourceAlpha В качестве коэффициента используется вектор альфа каналов примитива $$[s_a ,s_a ,s_a ,s_a ] $$.
    InverseSourceAlpha В качестве коэффициента используется вектор $$[1-s_a,1-s_a,1-s_a,1-s_a] $$.
    DestinationColor В качестве коэффициента используется цвет пикселя кадрового буфера $$[d_r ,d_g ,d_b,d_a ] /$$.
    InverseDestinationColor В качестве коэффициента используется вектор [1-d_r,1-d_g,1-d_b,1-d_a].
    DestinationAlpha В качестве коэффициента используется вектор альфа-каналов пикселя кадрового буфера $$[d_a,d_a,d_a,d_a ] $$.
    InverseDestinationAlpha В качестве коэффициента используется вектор $$[1-d_a,1-d_a ,1-d_a,1-d_a] $$.
    SourceAlphaSaturation Вектор коэффициентов равен [f,f,f,1], где $$f = min(s_a,d_a) $$.

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

    $$\qquard d’_r=(1-s_r)*s_r-s_a*d_r\\ \qquard d’_g=(1-s_g)*s_g-s_a*d_g\\ \qquard d’_b=(1-s_b)*s_b-s_a*d_b $$

    Значение альфа канала нас не интересует, так как он все равно не учитывается при выводе изображения на экран. Вдобавок, часто используемые форматы пикселей SurfaceFormat.Bgr565 и SurfaceFormat.Bgr32 не содержат альфа канала.

    Для начала давайте определим, какой операцией связаны между собой цвет примитива и цвет кадрового буфера. Во всех трех выражениях из произведения с множителями $$s_r ,s_g ,s_b$$ вычитается произведение, содержащее множители $$d_r ,d_g ,d_b,$$ следовательно для реализации этой формулы необходимо использовать операцию вычитания BlendFunction.Subtract.

    Перейдем к коэффициентам. Сопоставив коэффициенты перед $$s_r ,s_g ,s_b$$ с таблицей 3.6 мы придем к выводу, что они соответствуют значению Blend.InverseSourceColor. Все коэффициенты перед $$d_r ,d_g ,d_b$$ равны $$s_a,$$ следовательно они могут быть заданы с использованием константы Blend.SourceAlpha.

    Таким образом, программирование блоков ROP для смешения цветов по формуле 3.3 реализуется посредством следующих четырех строчек кода:

    device.RenderState.AlphaBlendEnable = true;
    device.RenderState.BlendFunction = BlendFunction.Subtract; 
    device.RenderState.SourceBlend = Blend.InverseSourceColor
     device.RenderState.DestinationBlend = Blend.SourceAlpha;

    Дополнительная информация

    Хотя по умолчанию XNA Framework применяет для смешения всех цветовых компонентов общие выражения, он так же позволяет использовать раздельные выражения для смешивания RGB и Alpha компонентов цвета. Эта функциональность активируется посредством присвоения свойству RenderState.SeparateAlphaBlendEnabled значения true, после чего вышеописанные свойства RenderState.BlendFunction, RenderState.SourceBlend и RenderState.DestinationBlend будут влиять исключительно на R, G и B составляющие цвета. Режим смешения альфа-компоненты цвета управляется аналогичной тройкой свойств: RenderState.AlphaBlendOperation, RenderState.AlphaSourceBlend, RenderState.AlphaDestinationBlend. Так как раздельное смешения каналов применяется достаточно редко, мы пока не будет акцентировать на нем внимание.

    3.4.2. Использование смешивания цветов для реализации эффекта полупрозрачности

    Для реализации визуализации полупрозрачных примитивов, мы должны переложить выражение 3.1 на причудливый язык программирования блоков ROP из XNA Framework. Для простоты мы положим, что весь примитив имеет одну и ту же прозрачность, что позволит нам использовать для задания коэффициента непрозрачности параметр RenderState.BlendFactor. Преимуществом такого подхода является возможность изменения прозрачности всех вершин примитива посредством коррекции одного единственного параметра. Само выражение 3.1 содержит операцию сложения и два коэффициента непрозрачности, так что оно легко реализуется средствами XNA Framework:

    // Задаем коэффициент непрозрачности (0 – 
    абсолютно прозрачен, 255 – полностью непрозрачен).
    // При присвоении свойству RenderState.BlendFactor 
    он автоматически приводится к диапазону
    // 0..1 путем деления на 255.
    const byte opacity = 50;
    // Включаем режим смешивания цветов
    device.RenderState.AlphaBlendEnable = true;
    // Используем операцию сложения
    device.RenderState.BlendFunction = BlendFunction.Add;
    // Коэффициент смешения всех трех компонентов цвета 
    равен opacity (точнее opacity / 255)
    device.RenderState.BlendFactor = new XnaGraphics.Color
    (opacity, opacity, opacity, 0);
    // Коэффициент, на который умножается цвет примитива,
     равен opacity / 255
    device.RenderState.SourceBlend = Blend.BlendFactor;
    // Коэффициент, на который умножается цвет кадрового 
    буфера, равен 1 - opacity / 255
    device.RenderState.DestinationBlend = Blend.InverseBlendFactor;

    Для демонстрации использования эффекта полупрозрачности мы строим в решение практического упражнения 3.1 ( Examples\Ch03\Ex02 ) возможность управления прозрачностью круга (рисунок 3.14). Для этого необходимо добавить в группу Параметры ползунок ( TrackBar ) и две метки, свойства которых перечислены в таблице 3.7.

    (рис 3.14) Приложение, визуализирующее полупрозрачный круг
    Свойства новых элементов управления группы Параметры
    Класс Свойство Значение
    TrackBar Name opacityTrackBar
    Minimum 0
    Maximum 255
    TickFrequency 0
    Label Name opacityLabel
    Label Text Непрозрачность

    Так же мне необходимо реализовать обработчик события Scrol ползунка и немного подправить обработчик события Paint (листинг 3.24). Готовый проект приложения находится в example.zip в каталоге Examples\Ch03\Ex13.

    private void opacityTrackBarScroll(object sender, EventArgs e)
    {
    // Отображаем текущий коэффициент непрозрачности
    opacityLabel.Text = ((float)opacityTrackBar.Value / 255.0f).
    ToString("0.00");
    // Перерисовывает компонент xnaPanel
    xnaPanel.Invalidate();
     }
    private void xnaPanel_Paint(object sender, PaintEventArgs e)
    {
    ...
    // Рисуем шахматную доску (код визуализации шахматной доски 
    взят из пример Ch01\Ex03
    // (см. раздел 1.2).
    ...
    device.RenderState.CullMode = CullMode.None;
     device.RenderState.PointSize = 3;
    device.RenderState.AlphaBlendEnable = true;
    device.RenderState.BlendFunction = BlendFunction.Add; 
    // Значение параметра BlendFactor вычисляется на основе
     текущего положения ползунка прокрутки
     device.RenderState.BlendFactor = new XnaGraphics.
     Color((byte)opacityTrackBar.Value,
    (byte)opacityTrackBar.Value, (byte)opacityTrackBar.Value, 0);
     device.RenderState.SourceBlend = Blend.BlendFactor; 
    device.RenderState.DestinationBlend = Blend.InverseBlendFactor;
    // Визуализируем круг
    ...
    }

    3.4.3. Анимация построения фигуры Лиссажу

    В следующем примере мы создадим приложение, анимирующее построение фигуры Лиссажу, заданной выражением

    $$x = \sin(2-a) y =\cos(3-a) $$

    где

  • x, y - координаты текущей точки
  • а - угол, пробегающий с определенным шагом значения от 0 до 360 градусов (0…2·?)
  • Вначале все пиксели фигуры будут прозрачными. Затем мы начнем постепенно увеличивать непрозрачность вершин фигуры, при этом непрозрачность всех вершин будет расти неравномерно: первыми непрозрачными станут вершины, соответствующие углу а равному 0, а последними - а равному 360 градусов. Соответственно, фигура Лиссажу будет как бы рисоваться как бы невидимым пером (рисунок 3.15).

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

    device. RenderState.AlphaBlendEnable = true;
    device.RenderState.BlendFunction = BlendFunction.Add;
    device.RenderState.SourceBlend = Blend.SourceAlpha;
    device.RenderState.DestinationBlend = Blend.InverseSourceAlpha;

    Чтобы облегчить задачу мы воспользуемся в качестве отправной точки решением практического упражнения 2.5. Все что от нас требуется - переписать обработчик события Idle и немного подправить обработчики событий Load и Paint формы (листинг 3.25).

    (рис 3.25)  Построение фигуры Лиссажу (рис 3.15) // Пример Examples\Ch03\Ex14
    public partial class MainForm : Form
    {
    // Количество сегментов в кривой
    const int QuadStrips = 200; 
    // Количество треугольников в кривой
    * 2;
    const int TriStrips = QuadStrips 
    ... 
    // Массив вершин фигуры Лиссажу
    VertexPositionColor[] vertices = null; 
    // Таймер
    Stopwatch stopwatch = null; 
    ...
    private void MainForm_Load(object sender, EventArgse)
    {
    //
    Создаем массив вершин объекта
    vertices = new VertexPositionColor[TriStrips + 2];
    // Перебираем все вершины фигуры Лиссажу
    for (int i = 0; i <= QuadStrips; i++) 
    { 
    // Вычисляем текущий угол
    float angle = 2.0f * (float)Math.PI * (float)i 
    // Рассчитываем координаты текущей точки фигуры 
    Лиссажу float cos = (float)Math.Cos(angle);
     float x = 0.85f * (float)Math.Sin(2 * angle);
    / (float)QuadStrips;
    float y = 0.85f * (float)Math.Cos(3 * angle); 
    // Вычисляем цвет точки
    byte green = (byte)(Math.Pow(0.5f + cos * 0.5f, 0.3f) * 255.0f); byte
     red = (byte)(Math.Pow(0.5f - cos * 0.5f, 0.3f) * 255.0f);
    /// Рассчитываем вектор, перпендикулярный графику фигуры Лиссажу длиной 
    0.015 float sx = -2.55f * (float)Math.Sin(3 * angle); float sy =-
    1.7f *(float)Math.Cos(2 * angle); float length =
     (float)Math.Sqrt(sx * sx + sy * sy); float nx = sx / length *
     0.015f; float ny = sy / length * 0.015f;
    // Заносим в массив информацию о двух вершинах 
    фигуры Лиссажу, смещенных на 0.015 в
    // направление, перпендикулярном фигуре. Таким 
    образом, кривая фигуры Лиссажу будет иметь
    // ширину 0.03 единицы.
    vertices[i * 2] = new VertexPositionColor(new 
    Vector3(x - nx, y - ny, 0.0f),
    new XnaGraphics.Color(red, green, 0)); vertices
    [i * 2 + 1] = new VertexPositionColor(new
     Vector3(x + nx, y + ny, 0.0f), new XnaGraphics.
     Color(red, green, 0)); 
    }; 
    ... 
    // Запускаем таймер
    stopwatch = new Stopwatch();
     stopwatch.Start();
    // Задаем обработчик события Idle, выполняющий 
    коррекцию прозрачности вершин
    Application.Idle+=new EventHandler(Application_Idle); 
    // Рассчитываем текущие коэффициенты непрозрачности вершин
    Application_Idle(this, null); 
    }
    // Рассчитывает текущие коэффициенты непрозрачности 
    вершин и перерисовывает изображение void
     Application_Idle(object sender, EventArgs e) 
    {
    if (closing) 
    Close();
    // Получаем текущее время
    float currentTime = (float)stopwatch.ElapsedTicks / 
    (float)Stopwatch.Frequency;
    // Перебираем все вершины фигуры Лиссажу
    for (int i = 0; i <= QuadStrips; i++) 
    { 
    // Вычисляем коэффициент непрозрачности для текущей пары вершин
    byte opacity = (byte)Math.Max(Math.Min((currentTime - 15.0f * (float)i / (float)QuadStrips) *
     255.0f, 255f), 0.0f); 
    // Изменяем коэффициент непрозрачности вершин. К 
    сожалению, структура Color не позволяет 
    // изменять отдельные компоненты цвета, поэтому 
    приходится создавать новую структуру и 
    // указывать значения всех цветовых компонентов.
    vertices[i * 2].Color = new XnaGraphics.Color(vertices[i * 2].Color.R,
     vertices[i * 2].Color.G, vertices[i * 2].Color.B, opacity);
    vertices[i * 2 + 1].Color = new XnaGraphics.Color
    (vertices[i * 2 + 1].Color.R,
     vertices[i * 2 + 1].Color.G, vertices[i * 2 + 1].Color.B, opacity); };
    // Если фигура Лиссажу полностью визуализирована 
    (она визуализируется ровно 16 секунд) if
     (currentTime > 16.0f) {
    // Прекращаем анимацию, дабы не загружать центральный
     процессор и видеокарту бесполезной 
    // работой.
    Application.Idle -= new EventHandler(Application_Idle); 
    // Выключаем таймер
    stopwatch.Stop(); 
    }
    // Перерисовываем форму 
    Invalidate(); 
    }
    private void MainForm_Paint(object sender, PaintEventArgs e) 
    { 
    ...
    device.RenderState.CullMode = CullMode.None;
    device.RenderState.FillMode = fillMode; 
    // Задаем режим смешения пикселей
    device.RenderState.AlphaBlendEnable = true;
    device.RenderState.BlendFunction = BlendFunction.Add;
    device.RenderState.SourceBlend = Blend.SourceAlpha;
    device.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
    device.VertexDeclaration = decl;
    // Визуализируем фигуру Лиссажу effect.Begin();
    foreach (EffectPass pass in effect.CurrentTechnique.Passes) 
    {
    pass.Begin();
    device.DrawUserPrimitives(PrimitiveType.TriangleStrip, vertices, 0, 
    vertices.Length - 2);
    pass.End(); 
    } effect.End();
    device.Present();
    }
    }

    Заключение

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

    Вернуться к учебному плану