Главная » Разработка для Windows Phone 7 » Перемещение вдоль полилинии Windows Phone 7

0

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

Следующий проект под именем CarOnPolylineCourse (Объезд по сложной траектории) включает класс PolylineInterpolator (Средство интерполяции полилинии), который делает это возможным. Но прежде чем перейти к классу PolylineInterpolator, давайте сначала рассмотрим класс Game1. Он включает такие поля:

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

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

const float SPEED = 0.25f;                     // циклов в секунду

GraphicsDeviceManager graphics;

SpriteBatch spriteBatch;

Texture2D car;

Vector2 carCenter;

PolylineInterpolator polylineInterpolator = new PolylineInterpolator(); Vector2 position; float rotation;

}

Как видим, скорость здесь представлена в циклах, и создается экземпляр этого загадочного класса PolylineInterpolator. Метод LoadContent практически аналогичен тому, каким он был в предыдущем проекте, за исключением того, что в данном случае точки добавляются не в массив turnPoints, а в свойство Vertices (Вершины) класса PolylineInterpolator:

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

protected override void LoadContent() {

spriteBatch = new SpriteBatch(GraphicsDevice);

car = this.Content.Load<Texture2D>("Car");

carCenter = new Vector2(car.Width / 2, car.Height / 2);

float margin = car.Width;

Viewport viewport = this.GraphicsDevice.Viewport;

polylineInterpolator.Vertices.Add(

new Vector2(car.Width, car.Width)); polylineInterpolator.Vertices.Add(

new Vector2(viewport.Width – car.Width, car.Width)); polylineInterpolator.Vertices.Add(

new Vector2(car.Width, viewport.Height – car.Width)); polylineInterpolator.Vertices.Add(

new Vector2(viewport.Width – car.Width, viewport.Height -

car.Width));

polylineInterpolator.Vertices.Add(

new Vector2(car.Width, car.Width));

}

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

Как и в приложениях предыдущей главы, в которых использовались параметрические уравнения, метод Update прост до слез:

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

protected override void Update(GameTime gameTime) {

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

float t = (SPEED * (float)gameTime.TotalGameTime.TotalSeconds) % 1; float angle;

position = polylineInterpolator.GetValue(t, false, out angle); rotation = angle + MathHelper.PiOver2;

base.Update(gameTime);

}

Как обычно, значения t лежат в диапазоне от 0 до 1, где 0 указывает на начало траектории в верхнем левом углу экрана. По мере движения автомобиля по траектории и приближения к исходной позиции снова, значение t стремится к 1. Это t передается непосредственно в метод GetValue класса PolylineInterpolator, который возвращает значение Vector2 где-то на полилинии.

Как дополнительный приз последний аргумент метода GetValue позволяет получить значение angle, которое определяет касательную к полилинии в данной точке. Этот угол отсчитывается по часовой стрелке от положительного направления оси X. Например, когда автомобиль движется из верхнего левого в верхний правый угол, angle равен 0. Когда автомобиль находится на пути из верхнего правого в нижний левый угол, значение угла лежит в диапазоне от п/2 и п в зависимости от соотношения размеров экрана. Автомобиль на рисунке расположен вертикально вверх, поэтому в ходе перемещения его придется поворачивать на дополнительные п/2 радиан.

Метод Draw никак не изменился:

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

protected override void Draw(GameTime gameTime) {

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

spriteBatch.Draw(car, position, null, Color.White, rotation,

carCenter, 1, SpriteEffects.None, 0); spriteBatch.End();

base.Draw(gameTime);

}

На данном снимке экрана автомобиль направляется в нижний левый угол:

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

Проект XNA: CarOnPolylineCourse Файл: PolylineInterpolator.cs (полностью) using System;

using System.Collections.Generic; using Microsoft.Xna.Framework;

namespace CarOnPolylineCourse {

public class PolylineInterpolator {

public PolylineInterpolator() {

Vertices = new List<Vector2>();

}

public List<Vector2> Vertices { protected set; get; }

public float TotalLength() {

float totalLength = 0;

// Обратите внимание, объезд траектории начинается с индекса 1

for (int i = 1; i < Vertices.Count; i++) {

totalLength += (Vertices[i] – Vertices[i – 1]).Length();

}

return totalLength;

}

public Vector2 GetValue(float t, bool smooth, out float angle) {

if (Vertices.Count == 0)

return GetValue(Vector2.Zero, Vector2.Zero, t, smooth, out angle);

else if (Vertices.Count == 1)

return GetValue(Vertices[0], Vertices[0], t, smooth, out angle);

if (Vertices.Count == 2)

return GetValue(Vertices[0], Vertices[1], t, smooth, out angle);

// Вычисляем общую протяженность маршрута float totalLength = TotalLength(); float accumLength = 0;

// Обратите внимание, объезд траектории начинается с индекса 1

for (int i = 1; i < Vertices.Count; i++) {

float prevLength = accumLength;

accumLength += (Vertices[i] – Vertices[i – 1]).Length();

if (t >= prevLength / totalLength && t <= accumLength / totalLength) {

float tPrev = prevLength / totalLength; float tThis = accumLength / totalLength; float tNew = (t – tPrev) / (tThis – tPrev);

return GetValue(Vertices[i – 1], Vertices[i], tNew, smooth, out angle);

}

}

return GetValue(Vector2.Zero, Vector2.Zero, t, smooth, out angle);

}

Vector2 GetValue(Vector2 vertex1, Vector2 vertex2, float t,

bool smooth, out float angle)

angle = (float)Math.Atan2(vertex2.Y – vertex1.Y, vertex2.X – vertex1.X);

return smooth ? Vector2.SmoothStep(vertex1, vertex2, t) : Vector2.Lerp(vertex1, vertex2, t);

}

}

}

Всего одно свойство Vertices позволяет определить коллекцию объектов Vector2, которая описывает полилинию. Если требуется, чтобы полилиния заканчивалась в той же точке, в которой начинается, необходимо явно продублировать эту точку в коллекции. Всю работу выполняет метод GetValue. Сначала этот метод определяет общую длину полилинии. Затем проходит по всем вершинам и суммирует длины отрезков между ними, выявляя пару точек, для которых эта суммарная длина выходит за рамки диапазона допустимых значений t. Тогда эти точки передаются в закрытый метод GetValue для выполнения линейной интерполяции с помощью Vector2.Lerp и для вычисления угла между двумя касательными, используя второго лучшего друга разработчика графических приложений метод Math.Atan2.

Но постойте, в методе GetValue есть еще аргумент Boolean, который заставляет этот метод использовать Vector2.SmoothStep, а не Vector2.Lerp. Чтобы попробовать этот альтернативный вариант, замените данный вызов метода Update класса Game1:

position = polylineInterpolator.GetValue(t, false, out angle);

следующим:

position = polylineInterpolator.GetValue(t, true, out angle);

Более «сглаженную» интерполяцию обеспечит кубическое уравнение. Также оно обусловит замедление автомобиля при приближении к вершинам и ускорение после их прохождения. Такое изменение скорости весьма приятно, но движение по-прежнему будет неровным и далеким от реалистичного.

Мне не нравится неэффективность класса PolylineInterpolator. GetValue приходится несколько раз вызывать метод Length класса Vector2, а это, безусловно, включает вычисление квадратного корня. Было бы хорошо, если бы класс сохранял общую длину и суммарную длину для каждой вершины, обеспечивая возможность просто повторно использовать эти данные при последующих вызовах GetValue. Как обсуждалось, класс не может сделать этого, потому что не располагает сведениями о том, когда значения Vector2 добавляются или удаляются из коллекции Vertices. Одно из решений – сделать эту коллекцию закрытой и разрешить передавать в конструктор класса только коллекцию точек. Другой подход – заменить класс List классом ObservableCollection, что обеспечит уведомление о добавлении или удалении объектов.

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

По теме:

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