Главная » Разработка для Windows Phone 7 » Графическое представление Windows Phone 7

0

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

Такой график строится приложением AccelerometerGraph (Диаграмма акселерометра). На рисунке показано его типовое представление:

Назначение данного приложения – продемонстрировать пользователю реальные показания акселерометра, поэтому в нем не выполняется никакого сглаживания. Красная линия соответствует значениям X, зеленая – Y, и синяя – Z. (Мнемоническая схема RGB == XYZ.) При портретном режиме отображения график перемещается вверх по экрану по мере того, как снизу добавляются новые значения. Каждый пиксел в вертикальном направлении соответствует одному «тику» обновлению экрана, т.е. 1/30 секунды. Более толстые горизонтальные линии представляют секунды. Более тонкие горизонтальные линии соответствуют 1/5 секунды (6 пикселов). Вертикальная линия в центре соответствует нулевому значению составляющей ускорения. Две другие более толстые вертикальные линии соответствуют значениям 1 (справа) и -1 (слева). Левый край экрана представляет значение -2, и правый край – значение 2. Как продемонстрировало предыдущее приложение, этого должно быть достаточно для соответствующего отображения всего диапазона возможных значений.

В ходе выполнения приложения старые значения как будто «ползут» вверх по экрану. Инстинктивно кажется, что в коде это должно быть реализовано следующим образом: создается объект Texture2D размером с экран и при каждом вызове Update все его пикселы просто сдвигаются на ширину Texture2D, так что верхняя строка исчезает, и новая строка может быть добавлена снизу. Но при таком подходе 30 раз в секунду приходится перемещать слишком большое количество пикселов.

Имеет больше смысла вставлять новые данные в строку, номер которой определяется переменной, получающей приращение при каждом вызове Update. (Назовем ее строкой вставки.) После этого в методе Draw можно дважды отрисовывать разделенный на две части Texture2D. Первая часть отображается вверху экрана и начинается со строки, следующей за строкой вставки, и продолжается до конца Texture2D. Вторая часть отрисовывается под первой; она начинается вверху Texture2D и заканчивается строкой вставки.

Поскольку чтобы обеспечить возможность отображения новых данных, старые данные должны быть удалены из строки вставки, неподвижные линии графика имеет смысл обрабатывать отдельно. Эта часть повторяется в вертикальном направлении через каждые 30 пикселов, поэтому она может быть реализована как небольшое растровое изображение, отображаемое многократно. В полях AccelerometerGraph для backgroundTexture задается

высота 30 пикселов, и высота graphTexture (Текстура графика) (в которой отображаются красная, зеленая и синяя линии) соответствует размеру экрана.

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

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

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

int displayWidth, displayHeight;

Texture2D backgroundTexture;

Texture2D graphTexture;

uint[] pixels;

int totalTicks;

int oldInsertRow;

Vector3 oldAcceleration;

Vector3 accelerometerVector;

object accelerometerVectorLock = new object();

public Game1() {

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

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

graphics.SupportedOrientations = DisplayOrientation.Portrait; graphics.PreferredBackBufferWidth = 480; graphics.PreferredBackBufferHeight = 768;

}

}

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

Как обычно, метод Initialize запускает акселерометр: Проект XNA: AccelerometerGraph Файл: Game1.cs (фрагмент)

protected override void Initialize() {

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

try {

accelerometer.Start();

}

catch {

}

base.Initialize();

}

void OnAccelerometerReadingChanged(object sender, AccelerometerReadingEventArgs

args) {

lock (accelerometerVectorLock) {

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

(float)args.Z); }

Метод LoadContent преимущественно посвящен созданию и инициализации backgroundTexture, который включает горизонтальные и вертикальные линии. Хотя код здесь довольно обобщенный, но высота backgroundTexture будет вычисляться равной 30 пикселам, и горизонтальные линии будут отрисовываться через каждые 6 пикселов.

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

protected override void LoadContent() {

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

displayWidth = this.GraphicsDevice.Viewport.Width; displayHeight = this.GraphicsDevice.Viewport.Height;

// Создаем текстуру фона и инициализируем ее

int ticksPerSecond = 1000 / this.TargetElapsedTime.Milliseconds; int ticksPerFifth = ticksPerSecond / 5;

backgroundTexture = new Texture2D(this.GraphicsDevice, displayWidth, ticksPerSecond);

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

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

for (int x = 0; x < backgroundTexture.Width; x++) {

Color clr = Color.Black;

if (y == 0 || x == backgroundTexture.Width / 2 || x == backgroundTexture.Width / 4 || x == 3 * backgroundTexture.Width / 4)

{

clr = new Color(12 8, 128, 128);

}

else if (y % ticksPerFifth == 0 ||

((x – backgroundTexture.Width / 2) %

(backgroundTexture.Width / 16) == 0))

{

clr = new Color(64, 64, 64);

}

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

}

backgroundTexture.SetData<uint>(pixels); // Создаем текстуру графика

graphTexture = new Texture2D(this.GraphicsDevice, displayWidth, displayHeight); pixels = new uint[graphTexture.Width * graphTexture.Height];

// Инициализируем

oldInsertRow = graphTexture.Height – 2;

}

В конце LoadContent создается большой graphTexture, размер которого соответствует размеру экрана, и воссоздается поле массива pixels специально для этого растрового изображения.

LoadContent завершается заданием предпоследней строки Texture2D в качестве значения oldInsertRow (Предыдущая строка вставки). Как будет показано далее, при первом

вычислении строки вставки в методе Update в качестве значения insertRow задается последняя строка растрового изображения.

Каждый вызов Update обеспечивает отрисовку трех прямых линий на graphTexture: красной, зеленой и синей. Если принять, что каждая линия отрисовывается из точки (x1, y1) в точку (x2, y2), тогда y2 должна равняться y1 + 1 (если не произойдет ничего такого, что приведет к потере тиков в Update). Значения X каждой цветной линии вычисляются на основании составляющих X, Y и Z старого и нового векторов ускорения.

Проблема в том, что y1 может располагаться внизу объекта Texture2D, а y2 – вверху. В следующем коде я нашел, что проще работать с тремя значениями Y: oldInsertRow – это то, что выше я называл yi, а вот и newInsertRow (Новая строка вставки), и insertRow (Строка вставки) представляютy2. Разница в том, что insertRow всегда находится в Texture2D (то есть меньше высоты растрового изображения), а вот newInsertRow может выходить за рамки допустимого диапазона значений. Преимущество newInsertRow в том, что его значение всегда гарантированно больше значения oldInsertRow. Это несколько упрощает алгоритмы отрисовки линий, поскольку в данном случае не приходится иметь дело с линией, проходящей снизу вверх через все растровое изображение.

Основная задача Update – вызов DrawLines с передачей в него oldInsertRow, newInsertRow и старого и нового векторов ускорения:

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

protected override void Update(GameTime gameTime) {

// Обеспечиваем возможность выхода из игры

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

Vector3 acceleration;

lock (accelerometerVectorLock) {

acceleration = accelerometerVector;

}

totalTicks = (int)Math.Round(gameTime.TotalGameTime.TotalSeconds /

this.TargetElapsedTime.TotalSeconds); int insertRow = (totalTicks + graphTexture.Height – 1) % graphTexture.Height;

// newInsertRow гарантированно всегда больше, чем oldInsertRow, // но может быть больше высоты graphTexture!

int newInsertRow = insertRow < oldInsertRow ? insertRow + graphTexture.Height :

insertRow;

// Сначала обнуляем pixels

for (int y = oldInsertRow + 1; y <= newInsertRow; y++) for (int x = 0; x < graphTexture.Width; x++)

pixels[(y % graphTexture.Height) * graphTexture.Width + x] = 0;

// Отрисовываем три линии на основании старого и нового значений ускорения DrawLines(graphTexture, pixels, oldInsertRow, newInsertRow, oldAcceleration, acceleration);

this.GraphicsDevice.Textures[0] = null;

if (newInsertRow >= graphTexture.Height) {

graphTexture.SetData<uint>(pixels);

}

else {

Rectangle rect = new Rectangle(0, oldInsertRow,

graphTexture.Width, newInsertRow – oldInsertRow

graphTexture.SetData<uint>(0, rect,

pixels, rect.Y * rect.Width, rect.Height *

rect.Width); }

oldInsertRow = insertRow; oldAcceleration = acceleration;

base.Update(gameTime);

}

В конце перегруженного Update выполняется обновление graphTexture из массива pixels. Если newInsertRow не выходит за границы растрового изображения, только две строки требуют обновления. В противном случае используется более простая форма вызова SetData, решающая проблему «в лоб».

Фактическая отрисовка линий обеспечивается парой методов. Метод DrawLines просто разбивает вектор ускорения на три составляющих и выполняет три вызова метода DrawLine, в ходе которых вычисляются значения X на основании этих составляющих:

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

void DrawLines(Texture2D texture, uint[] pixels, int oldRow, int newRow, Vector3 oldAcc, Vector3 newAcc)

{

DrawLine(texture, pixels, oldRow, newRow, oldAcc.X, newAcc.X, Color.Red); DrawLine(texture, pixels, oldRow, newRow, oldAcc.Y, newAcc.Y, Color.Green); DrawLine(texture, pixels, oldRow, newRow, oldAcc.Z, newAcc.Z, Color.Blue);

}

void DrawLine(Texture2D texture, uint[] pixels, int oldRow, int newRow, float oldAcc, float newAcc, Color clr)

{

DrawLine(texture, pixels,

texture.Width / 2 + (int)(oldAcc * texture.Width / 4), oldRow, texture.Width / 2 + (int)(newAcc * texture.Width / 4), newRow, clr);

}

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

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

void DrawLine(Texture2D texture, uint[] pixels,

int x1, int y1, int x2, int y2, Color clr)

{

if (x1 == x2 && y1 == y2) {

return;

}

else if (Math.Abs(y2 – y1) > Math.Abs(x2 – x1)) {

int sign = Math.Sign(y2 – y1);

for (int y = y1; y != y2; y += sign)

float t = (float)(y – y1) / (y2 – y1); int x = (int)(x1 + t * (x2 – x1) + 0.5f); SetPixel(texture, pixels, x, y, clr);

}

}

else {

int sign = Math.Sign(x2 – x1);

for (int x = x1; x != x2; x += sign) {

float t = (float)(x – x1) / (x2 – x1); int y = (int)(y1 + t * (y2 – y1) + 0.5f); SetPixel(texture, pixels, x, y, clr);

}

}

}

// Обращаем внимание на корректировку Y и применение побитового ИЛИ!

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

pixels[(y % texture.Height) * texture.Width + x] |= clr.PackedValue;

}

Метод SetPixel (Задать значение пиксела) корректирует координаты Y, которые могут выходить за границы растрового изображения. Также обратите внимание на использование операции ИЛИ. Если синяя и красная линии частично перекрываются, например, тогда перекрывающаяся часть линии будет отрисована пурпурным цветом.

Наконец, метод Draw отрисовывает оба объекта Texture2D, применяя аналогичную логику, которая обеспечивает многократную отрисовку текстуры до полного заполнения ею экрана:

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

protected override void Draw(GameTime gameTime) {

spriteBatch.Begin(); // Отрисовываем текстуру фона

int displayRow = -totalTicks % backgroundTexture.Height;

while (displayRow < displayHeight) {

spriteBatch.Draw(backgroundTexture, new Vector2(0, displayRow), Color.White);

displayRow += backgroundTexture.Height;

}

// Отрисовываем текстуру графика

displayRow = -totalTicks % graphTexture.Height;

while (displayRow < displayHeight) {

spriteBatch.Draw(graphTexture, new Vector2(0, displayRow), Color.White); displayRow += graphTexture.Height;

}

spriteBatch.End(); base.Draw(gameTime);

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

По теме:

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