Главная » Разработка для Windows Phone 7 » Улучшенная визуализация «пузырька» Windows Phone 7

0

Приложение AccelerometerVisualization (Визуализация акселерометра) – небольшой шаг по улучшению приложения XnaAccelerometer из главы 5. Приложение XnaAccelerometer просто показывало плавающий «пузырек» без какой либо шкалы или числовых значений. В данном приложении добавляется шкала (в виде концентрических кругов) и некоторая текстовая информация:

Приложение AccelerometerVisualization также реализует, вероятно, самый базовый тип сглаживания: фильтр нижних частот, который усредняет текущее значение с предыдущим сглаженным значением. Необработанные показания акселерометра отображаются в строке «Raw» (Необработанный), тогда как сглаженное значение представлено в строке «Avg» («average» – среднее). Отображаются также минимальное и максимальное значения. Они вычисляются с помощью методов Vector3.Min и Vector3.Max, которые находят минимальное и максимальное значения составляющих X, Y и Z по отдельности. Красный «пузырек» масштабируется соответственно модулю вектора и меняет цвет на зеленый, если составляющая Z вектора имеет положительное значение.

Рассмотрим поля приложения:

Проект XNA: AccelerometerVisualization Файл: Game1.cs (фрагмент, демонстрирующий поля)

public class Game1 : Microsoft.Xna.Framework.Game {

const int BALL RADIUS = 8;

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

Viewport viewport; SpriteFont segoe14;

StringBuilder stringBuilder = new StringBuilder();

int unitRadius; Vector2 screenCenter; Texture2D backgroundTexture; Vector2 backgroundTextureCenter; Texture2D ballTexture; Vector2 ballTextureCenter; Vector2 ballPosition;

float ballScale; bool isZNegative;

Vector3 accelerometerVector;

object accerlerometerVectorLock = new object();

Vector3 oldAcceleration;

Vector3 minAcceleration = Vector3.One;

Vector3 maxAcceleration = -Vector3.One;

}

Хотя приложение отображает что-то наподобие «пузырька», который перемещается в направлении, противоположном направлению силы тяжести и вектора ускорения, в приложении этот объект называют «ball» (шар). Поле oldAcceleration (Предыдущее значение ускорения) используется для сглаживания значений. При каждом обновлении экрана значение oldAcceleration соответствует предыдущему сглаженному («Avg») значению.

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

Чтобы упростить эту запутанную схему, в конструкторе класса свойству SupportedOrientations объекта GraphicsDeviceManager (Диспетчер графических устройств) (на который ссылается поле graphics) можно задать значение DisplayOrientation.LandscapeLeft. В моем приложении этого не сделано, и оно поддерживает ориентацию LandscapeRight. Однако в конструкторе задается размер заднего буфера для обеспечения места под строку состояния телефона:

Проект XNA: AccelerometerVisualization Файл: Game1.cs (фрагмент)

public Game1() {

graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content";

// Частота кадров по умолчанию для устройств Windows Phone – 30 кадров/с TargetElapsedTime = TimeSpan.FromTicks(333333);

// Альбомная ориентация, но оставляем место под строку состояния graphics.PreferredBackBufferWidth = 728; graphics.PreferredBackBufferHeight = 480;

}

В перегруженном Initialize создается объект Accelerometer, задается обработчик событий изменения показаний и выполняется запуск акселерометра:

Проект XNA: AccelerometerVisualization Файл: Game1.cs (фрагмент)

protected override void Initialize() {

Accelerometer accelerometer = new Accelerometer(); accelerometer.ReadingChanged += OnAccelerometerReadingChanged;

try

accelerometer.Start();

}

catch {

}

base.Initialize();

}

Обработчик событий ReadingChanged (Показания изменились) вызывается асинхронно, поэтому правильным поведением будет просто сохранять значение в коде, защитив его блокировкой lock:

Проект XNA: AccelerometerVisualization Файл: Game1.cs (фрагмент)

void OnAccelerometerReadingChanged(object sender, AccelerometerReadingEventArgs

args) {

lock (accerlerometerVectorLock) {

accelerometerVector = new Vector3((float)args.X, (float)args.Y,

(float)args.Z); }

}

Если немного поэкспериментировать с обработчиком ReadingChanged, выясняется, что он вызывается почти 50 раз в секунду. Это даже чаще, чем обновляется экран, поэтому сглаживание может быть целесообразным реализовать именно в этом обработчике. (Я так поступаю в некоторых приложениях данной главы, которые будут рассматриваться далее.)

Перегруженный метод LoadContent в данном приложении преимущественно отвечает за подготовку двух текстур: большого поля backgroundTexture (Текстура фона), который покрывает всю поверхность концентрическими кругами, и ballTexture (Текстура шара), который перемещается по полю. Код, отрисовывающий линии и окружности на backgroundTexture, довольно специализированный, включает простые циклы и пару методов для задания цветов пикселов.

Проект XNA: AccelerometerVisualization Файл: Game1.cs (фрагмент)

protected override void LoadContent() {

// Создаем новый SpriteBatch, который может использоваться для отрисовки текстур spriteBatch = new SpriteBatch(this.GraphicsDevice);

// Получаем данные экрана и шрифта viewport = this.GraphicsDevice.Viewport;

screenCenter = new Vector2(viewport.Width / 2, viewport.Height / 2); segoe14 = this.Content.Load<SpriteFont>("Segoe14");

// Эквивалент единичного вектора в пикселах unitRadius = (viewport.Height – BALL_RADIUS) / 2;

// Создаем и отрисовываем текстуру фона backgroundTexture =

new Texture2D(this.GraphicsDevice, viewport.Height, viewport.Height); backgroundTextureCenter =

new Vector2(viewport.Height / 2, viewport.Height / 2);

Color[] pixels = new Color[backgroundTexture.Width * backgroundTexture.Height];

// Отрисовываем горизонтальную линию for (int x = 0; x < backgroundTexture.Width; x++) SetPixel(backgroundTexture, pixels,

x, backgroundTexture.Height / 2, Color.White);

// Отрисовываем вертикальную линию

for (int y = 0; y < backgroundTexture.Height; y++) SetPixel(backgroundTexture, pixels,

backgroundTexture.Width / 2, y, Color.White);

// Отрисовываем окружности

DrawCenteredCircle(backgroundTexture, pixels, unitRadius, Color.White); DrawCenteredCircle(backgroundTexture, pixels, 3 * unitRadius / 4, Color.Gray); DrawCenteredCircle(backgroundTexture, pixels, unitRadius / 2, Color.White); DrawCenteredCircle(backgroundTexture, pixels, unitRadius / 4, Color.Gray); DrawCenteredCircle(backgroundTexture, pixels, BALL_RADIUS, Color.White);

// Задаем значения пикселов текстуры фона backgroundTexture.SetData<Color>(pixels);

// Создаем и отрисовываем текстуру пузырька ballTexture = new Texture2D(this.GraphicsDevice,

2 * BALL RADIUS, 2 * BALL RADIUS); ballTextureCenter = new Vector2(BALL_RADIUS, BALL_RADIUS); pixels = new Color[ballTexture.Width * ballTexture.Height]; DrawFilledCenteredCircle(ballTexture, pixels, BALL RADIUS); ballTexture.SetData<Color>(pixels);

}

void DrawCenteredCircle(Texture2D texture, Color[] pixels, int radius, Color clr) {

Point center = new Point(texture.Width / 2, texture.Height / 2); int halfPoint = (int)(0.707 * radius + 0.5);

for (int y = -halfPoint; y <= halfPoint; y++) {

int x1 = (int)Math.Round(Math.Sqrt(radius * radius – Math.Pow(y, 2))); int x2 = -x1;

SetPixel(texture, pixels, x1 + center.X, y + center.Y, clr); SetPixel(texture, pixels, x2 + center.X, y + center.Y, clr);

// Поскольку они симметричны, просто подставляем координаты // для симметричной части

SetPixel(texture, pixels, y + center.X, x1 + center.Y, clr); SetPixel(texture, pixels, y + center.X, x2 + center.Y, clr);

}

}

void DrawFilledCenteredCircle(Texture2D texture, Color[] pixels, int radius) {

Point center = new Point(texture.Width / 2, texture.Height / 2);

for (int y = -radius; y < radius; y++) {

int x1 = (int)Math.Round(Math.Sqrt(radius * radius – Math.Pow(y, 2)));

for (int x = -x1; x < x1; x++)

SetPixel(texture, pixels, x + center.X, y + center.Y, Color.White);

}

}

void SetPixel(Texture2D texture, Color[] pixels, int x, int y, Color clr) {

pixels[y * texture.Width + x] = clr;

Именно эта логика подсказала мне о необходимости явного задания ширины заднего буфера равной 728. Если использовать для ширины значение по умолчанию, 800 пикселов, фактическое представление будет сжиматься на 10%, обеспечивая место для строки состояния. Толщина отрисовываемых линий и окружностей составляет всего лишь 1 пиксел, и для них не реализовано никакого механизма сглаживания, поэтому в случае сжатия экрана их четкость частично будет утрачена.

В перегруженном Update происходит несколько любопытных вещей. Этот метод в основном отвечает за получение вектора ускорения и отображение его в графической и числовой форме. Метод сохраняет необработанное значение в поле newAcceleration (Новое ускорение), а сглаженное значение – в поле avgAcceleration (Среднее ускорение):

Проект XNA: AccelerometerVisualization Файл: Game1.cs (фрагмент)

protected override void Update(GameTime gameTime) {

if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit();

Vector3 newAcceleration = Vector3.Zero;

lock (accerlerometerVectorLock) {

newAcceleration = accelerometerVector;

}

maxAcceleration = Vector3.Max(maxAcceleration, newAcceleration); minAcceleration = Vector3.Min(minAcceleration, newAcceleration);

// Сглаживание с использованием фильтра нижних частот

Vector3 avgAcceleration = 0.5f * oldAcceleration + 0.5f * newAcceleration; stringBuilder.Remove(0, stringBuilder.Length);

stringBuilder.AppendFormat("Raw: ({0:F2}, {1:F2}, {2:F2}) = {3:F2}\n",

newAcceleration.X, newAcceleration.Y, newAcceleration.Z, newAcceleration.Length()); stringBuilder.AppendFormat("Avg: ({0:F2}, {1:F2}, {2:F2}) = {3:F2}\n",

avgAcceleration.X, avgAcceleration.Y, avgAcceleration.Z, avgAcceleration.Length()); stringBuilder.AppendFormat("Min: ({0:F2}, {1:F2}, {2:F2}) = {3:F2}\n",

minAcceleration.X, minAcceleration.Y, minAcceleration.Z, minAcceleration.Length()); stringBuilder.AppendFormat("Max: ({0:F2}, {1:F2}, {2:F2}) = {3:F2}",

maxAcceleration.X, maxAcceleration.Y, maxAcceleration.Z, maxAcceleration.Length());

ballScale = avgAcceleration.Length();

int sign = this.Window.CurrentOrientation ==

DisplayOrientation.LandscapeLeft ? 1 : -1;

ballPosition =

new Vector2(screenCenter.X + sign * unitRadius * avgAcceleration.Y / ballScale,

screenCenter.Y + sign * unitRadius * avgAcceleration.X /

ballScale);

isZNegative = avgAcceleration.Z < 0;

oldAcceleration = avgAcceleration;

base.Update(gameTime);

}

Поле accelerometerVector сохраняется обработчиком события ReadingChanged во втором потоке выполнения с помощью блокировки lock, поэтому для выполнения доступа к нему из

основного потока выполнения приложения требуется другая блокировка lock, использующая тот же объект:

lock (accerlerometerVectorLock) {

newAcceleration = accelerometerVector;

}

После этого на основании необработанного значения и значения поля oldAcceleration вычисляется сглаженное значение:

Vector3 avgAcceleration = 0.5f * oldAcceleration + 0.5f * newAcceleration;

Это значение avgAcceleration используется методом Update для целей отображения и замещает значение поля oldAcceleration:

oldAcceleration = avgAcceleration;

Такой тип сглаживания называют фильтром нижних частот. Он обеспечивает сглаживание высокочастотных отклонений путем усреднения текущего значения с предыдущими. Если v0 – это текущее необработанное значение вектора (newAcceleration), и v-1 – предыдущее необработанное значение, а v-2 – это необработанное значение, предшествующее v-1, сглаженное значение вычисляется по формуле:

Теперь получаем, что

Рост координаты Y акселерометра ? уменьшение координаты X экрана

Рост координаты X акселерометра ? уменьшение координаты Y экрана

В перегруженном Update сначала определяется значение sign (знак), которое соответствует 1 для режима LandscapeLeft и -1 для LandscapeRight:

int sign = this.Window.CurrentOrientation == DisplayOrientation.LandscapeLeft ? 1 : – 1;

Если оба компонента X и Y сглаженного вектора ускорения равны 0, «пузырек» должен размещаться в точке (screenCenter.X, screenCenter.Y). Довольно просто. Смещения этого центра должны вычисляться на основании значения sign и расстояния от центра до внешнего радиуса:

ballPosition =

new Vector2(screenCenter.X + sign * unitRadius * avgAcceleration.Y, screenCenter.Y + sign * unitRadius * avgAcceleration.X);

Но я не был удовлетворен этим вычислением. Небольшие неточности в показаниях акселерометра – и «пузырек» полностью «вылетал» за внешний круг и границы экрана. Я

принял решение компенсировать это делением на длину сглаженного вектора. Этот прием уже использовался для масштабирования «пузырька»:

ballScale = avgAcceleration.Length();

Поэтому я включил его в вычисление местоположения «пузырька»:

ballPosition =

new Vector2(screenCenter.X + sign * unitRadius * avgAcceleration.Y / ballScale, screenCenter.Y + sign * unitRadius * avgAcceleration.X / ballScale);

Перегруженный Draw отрисовывает фон, «пузырек» и четыре строки текста:

Проект XNA: AccelerometerVisualization Файл: Game1.cs (фрагмент)

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.Navy); spriteBatch.Begin();

spriteBatch.Draw(backgroundTexture, screenCenter, null, Color.White, 0,

backgroundTextureCenter, 1, SpriteEffects.None, 0); spriteBatch.Draw(ballTexture, ballPosition, null,

isZNegative ? Color.Red : Color.Lime, 0, ballTextureCenter, ballScale, SpriteEffects.None, 0); spriteBatch.DrawString(segoe14, stringBuilder, Vector2.Zero, Color.White); spriteBatch.End();

base.Draw(gameTime);

}

Когда вы трясете телефон, заметить перемещения пузырька сложно, но всегда видны изменения минимального и максимального значений. Если аппаратные средства используемого вами телефона аналогичны моему, составляющие X, Y и Z необработанного вектора ускорения никогда не выйдут за рамки диапазона от -2 до 2, более того различие между максимальным и минимальным значением не превысит 3,46. Похоже, это ограничение аппаратных средств.

Источник: Чарльз Петзольд, Программируем Windows Phone 7, Microsoft Press, © 2011.

По теме:

  • Комментарии