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

0

Для описания движения по кривым траекториям использовать параметрические уравнения не очень удобно, поэтому сам XNA предлагает обобщенное решение с использованием классов Curve (Кривая) и CurveKey (Ключ кривой), описанных в пространстве имен Microsoft.Xna.Framework.

Класс Curve включает свойство Keys (Ключи) типа CurveKeyCollection (Коллекция ключей кривой). Это коллекция объектов CurveKey. Каждый объект CurveKey позволяет задавать числовую пару (Position, Value). И свойство Position, и свойство Value типа float. Затем местоположение передается в свойство Curve метода Evaluate (Вычислить), и он возвращает интерполированное значение.

Но здесь все довольно запутанно, потом что (как указывается в документации) свойство Position класса CurveKey – это практически всегда время, а свойство Value очень часто обозначает местоположение или, чтобы быть более точным, одна из координат. Чтобы использовать Curve для интерполяции между точками в двухмерном пространстве, потребуется два экземпляра Curve: один для координаты X, и второй для Y. Эти экземпляры Curve во многом описываются параметрическими уравнениями.

На этих двух окружностях отрисуем точки через каждые 45 градусов:

Предположим, требуется провести автомобиль по траектории, напоминающей знак бесконечности. Знак бесконечности будет реализован как две соприкасающихся окружности. (Методика, которую я собираюсь представить, позволит впоследствии разъединить эти окружности, если потребуется.)

 

Если радиус каждой окружности составляет 1 единицу, вся фигура занимает 4 единицы в ширину и 2 единицы в высоту. Координаты X этих точек (двигаясь слева направо) соответствуют значениям 0, 0.293, 1, 0.707, 2, 2.293, 3, 3.707 и 4; и координаты Y (двигаясь сверху вниз) соответствуют значениям 0, 0.293, 1, 1.707 и 2. Значение 0,707 – это просто синус и косинус угла 45 градусов, и 0,293 – значение, получаемое, если вычесть предыдущее значение из единицы.

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

0,  0.293, 1, 1.707, 2, 2.293, 3, 3.707, 4, 3.707, 3, 2.293, 2, 1.707, 1, 0.293, 0

Если при обходе знака бесконечности используется параметр t, меняющийся в диапазоне от 0 до 1, первое значение будет соответствовать t = 0, и последнее (которое аналогично) соответствует t = 1. Для каждого последующего значения t увеличивается на 1/16 или 0,0625. Получаем ряд значений Y:

1,  0.293, 0, 0.293, 1, 1.707, 2, 1.707, 1, 0.293, 0, 0.293, 1, 1.707, 2, 1.707, 1

Теперь мы готовы к написанию кода. Определим поля проекта CarOnlnfinityCourse (Автомобиль на бесконечной траектории):

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

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

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

GraphicsDeviceManager graphics;

SpriteBatch spriteBatch;

Viewport viewport;

Texture2D car;

Vector2 carCenter;

Curve xCurve = new Curve();

Curve yCurve = new Curve();

Vector2 position;

float rotation;

}

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

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

protected override void Initialize() {

float[] xValues = { 0, 0.293f, 1,        1.707f,  2, 2.293f, 3, 3.707f,

4, 3.707f, 3,                            2.293f,  2, 1.707f, 1, 0.293f };

float[] yValues = { 1, 0.293f, 0,        0.293f,  1, 1.707f, 2, 1.707f,

1, 0.293f, 0,                            0.293f,  1, 1.707f, 2, 1.707f };

for (int i = -1; i < 18; i++) {

int index = (i + 16) % 16;

float t = 0.0625f * i;

xCurve.Keys.Add(new CurveKey(t, xValues[index])); yCurve.Keys.Add(new CurveKey(t, yValues[index]));

xCurve.ComputeTangents(CurveTangent.Smooth); yCurve.ComputeTangents(CurveTangent.Smooth); base.Initialize();

}

Массивы xValues и yValues содержат только 16 значений; они не включают последнюю точку, которая дублирует первую. Может показаться довольно странным, но цикл for начинается с индекса -1 и до 17, но модуль 16 гарантирует, что массивы индексируются от 0 до 15. В итоге коллекции Keys классов xCurve и yCurve получают координаты, ассоциированные со

значениями t -0.0625, 0, 0.0625, 0.0125…… 0.875, 0.9375, 1 и 1.0625. Очевидно, что здесь на две

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

Эти дополнительные точки используются для вызовов метода ComputeTangents (Вычислить тангенс) после цикла for. Класс Curve выполняет интерполяцию кубическим сплайном Эрмита, которую также называют cspline. Возьмем две точки: pt1 и pt2. cspline выполняет интерполяцию между этими двумя точками на основании не только pt1 и pt2, но также используя касательные к кривой в этих точках pt1 и pt2. Эти касательные для объекта Curve можно задать как часть объектов CurveKeys, или их можно вычислять в объекте Curve, исходя из соприкасающихся точек. Такой подход я реализовал посредством двух вызовов ComputeTangents. Благодаря аргументу CurveTangent.Smooth метод ComputeTangents использует не только две соприкасающиеся точки, но и точки на противоположной стороне. На самом деле, это простое взвешенное среднее, но это вариант лучше, чем остальные имеющиеся альтернативы.

Классы Curve и CurveKey предлагают и другие варианты, но предпринятый мною подход обеспечивает наилучшие результаты с минимальными усилиями. Разве не в этом суть программирования?

Метод LoadContent должен загрузить изображение автомобиля и получить координаты его центра:

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

protected override void LoadContent() {

spriteBatch = new SpriteBatch(GraphicsDevice);

viewport = this.GraphicsDevice.Viewport;

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

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

}

Теперь займемся Update. Этот метод вычисляет t на основании TotalGameTime. Класс Curve определяет метод Evaluate, который может принимать это значение t напрямую; так приложение получает интерполированные координаты X и Y. Но все данные двух объектов Curve определяются, исходя из максимального значения координаты X = 4 и Y = 2. Поэтому Update вызывает небольшой метод GetValue, который масштабирует значения соответственно размеру экрана.

Проект XNA: CarOnInfinityCourse Файл: 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 x = GetValue(t, true); float y = GetValue(t, false); position = new Vector2(x, y);

rotation = MathHelper.PiOver2 + (float)

Math.Atan2(GetValue(t + 0.001f, false) – GetValue(t – 0.001f, false), GetValue(t + 0.001f, true) – GetValue(t – 0.001f, true));

base.Update(gameTime);

}

float GetValue(float t, bool isX) {

if (isX)

return xCurve.Evaluate(t) * (viewport.Width – 2 * car.Width) / 4 + car.Width;

return yCurve.Evaluate(t) * (viewport.Height – 2 * car.Width) / 2 + car.Width;

}

После вычисления поля position мы приходим к небольшой проблеме, потому что классу Curve не хватает очень важного метода: метода, который бы обеспечивал вычисление касательной сплайна. Класс Curve использует касательные для вычисления сплайна, но после того как сплайн рассчитан, класс не обеспечивает доступа к касательным самого сплайна!

Это назначение других четырех вызовов GetValue. Для приближенного вычисления производной к t прибавляются и вычитаются небольшие значения. Таким образом Math.Atan2 вычисляет угол rotation.

Как обычно, стандартный Draw:

Проект XNA: CarOnInfinityCourse Файл: 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);

}

Если предполагается вычислять касательные, используемые для обсчета сплайна, с помощью класса Curve (как я сделал в данном приложении), классу необходимо предоставить достаточное количество точек. То есть не только сверх диапазона точек, которые подлежат интерполяции, но достаточно для того, чтобы обеспечить более или менее точные касательные. Изначально я попробовал определить траекторию в виде знака бесконечности, задавая точки через каждые 90 градусов, и это совершенно не сработало.

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

По теме:

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