Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету. |
Усложненные технологии визуализации
Интеграция диалогового окна в приложение
И так, у нас есть класс для работы с настройками приложения и диалоговое окно для визуального управления этими настройками. Теперь самое время интегрировать эту функциональность в наше приложение. Для начала мы должны определиться со стратегией поведения приложения, а именно, в каких случаях оно должно отображать диалоговое окно:
- Вполне логично, что диалоговое окно должно отображаться, если не был найден файл конфигурации. Данную ситуацию можно распознать по свойству 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; // Остальные действие (создание декларации вершины, заполнение массива вершин, загрузка // эффекта и т.п. ... }Листинг 3.17.
Остальные фрагменты приложения не содержат чего-либо заслуживающего внимания, и вы сможете легко их реализовать самостоятельно. Готовое приложение находится в 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-ти FPS10FPS – Frames Per Second (кадры в секунду) .
И так, для создания анимации мы должны отображать визуализировать различные фазы движения изображения с частотой 25 кадров в секунду. Наше первое приложение будет визуализировать шарик (точнее диск), летающий по форме и отскакивающий от ее стенок (рисунок 3.11). Анимация будет моделироваться посредством таймера (компонент Timer ), тикающего с интервалом 40 миллисекунд (то есть 25 раз в секунду). После каждого тика таймера мы будем прибавлять к координатам шарика значение вектора скорости и перерисовывать сцену. При пролете шарика сквозь стенку он будет отскакивать обратно, при этом вектор скорости будет изменяться на противоположный. Для придания движениям шарика некоторой неопределенности, модуль вектора скорости будет изменяться на незначительную случайную величину. Основные фрагменты приложения приведены в листинге 3.18. Исходный код примера находится в example.zip в каталоге Examples\Ch03\Ex07.
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(); } }Листинг 3.18.
Расчет координат вершин диска, использующий тригонометрические функции, является весьма ресурсоемкой операцией, поэтому в приложении используется небольшая хитрость. Вместо многократного расчета вершин диска приложение рассчитывает только координаты "эталонного " диска с центром в начале системы координат, которые заносятся в массив baseDiskVerts. После этого для получения вершин заданной окружности необходимо сместить все вершины "эталонной " окружности на расстояние заданной окружности от центра.
Запустите приложение на выполнение. Первое что бросится в глаза – это движение окружности рывками. Но ведь этого не может быть! Фильмы то при частоте 25 fps идут очень плавно. В чем же принципиальная разница между фильмом и нашим приложением? Все очень просто. В кинематографе кадры снимается с некоторой экспозицией11Время, в течение которого объектив аппарата остается открытым при фото- и киносъемке . В результате каждый кадр фильма содержит усредненное изображение за период экспозиции, что проявляется в смазывании быстродвижущихся объектов. Таким образом, 25 FPS в кинофильме и 25 FPS в нашем приложении – это немного разные FPS. В частности, в компьютерных играх при частоте кадров 25-30 FPS ощущаются некие "подергивания " в динамичных сценах – объекты движутся как бы рывками. Эту проблему решают методом грубой силы, то есть простым увеличением частоты кадров – практический опыт показыва ет, что в большинстве случаев вполне достаточно частоты кадров 60 FPS.
Дополнительная информация
Для визуализации быстродвижущихся объектов вроде спортивного автомобиля, пролетающего мимо наблюдателя со скоростью 300 км/ч, даже частота кадров 60 FPS оказывается недостаточной. С другой стороны, при попытке наращивания FPS мы упираемся в возможности аппаратуры – максимальная частота вертикальной развертки современных мониторов (то есть частота смены кадров) редко превышает 60-100 Hz. Поэтому для повышения реалистичности движений применяют технологии Motion Blur12Эффект размывания изображения быстро движущегося объекта 55, которые различными способами пытаются имитировать экспозицию при "киносъемке " кадров изображения.
Ну что ж, попробуем увеличить частоту смены кадров. Как известно, компонент таймер может тикать до 64-х раз в секунду, т.е. минимальный интервал между двумя тиками равен 0.015 миллисекунд. Присвойте свойству Interval таймера значение 15 и снова запустите приложение. Шарик будет двигаться ощутимо плавнее, однако скорость не будет постоянной: периодически шарик будет ни с того то замедляться, то ускоряться.
Почему это происходит? Для ответа на этот вопрос необходимо внимательно прочитать описания таймера Windows в MSDN. Хотя таймер и может тикать с интервалом 15 миллисекунд, точность каждого тика составляет 55 миллисекунд. Таким образом, по мере уменьшения интервала между тиками, точность таймера катастрофически падает, что приводит к неравномерности движения объектов. Но есть еще один немаловажный фактор – события от таймера имеют самый низкий приоритет среди всех сообщений. Например, если пользователь в это время нажимает клавишу клавиатуры, а в очереди сообщений находится необработанное сообщение WM_TIMER, то сообщения WM_KEYDOWN, WM_CHAR и WM_KEYUP будут вставлены в очередь перед сообщением WM_TIMER. По сути, сообщение WM_TIMER обрабатывается только при условии пустой очереди событий потока, а так как в очереди может находиться не более одного сообщения wmtimer, таймер может "терять " тики. Чтобы убедиться в этом, попробуйте ухватиться указателем мыши за край формы и поизменять ее размер, что тут же приведет к потере сообщений и, соответственно, замедлению шарика.
Эти недостатки компонента Timer затрудняют его применение в реальных игровых приложениях. Впрочем, данный компонент предназначен для реализации задач, не критичных к точности срабатывания таймера, вроде автоматического сохранения документа через заданные интервалы времени или обновления текущего времени на форме.