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

0

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

Сам шар создается в статическом методе Texture2DExtensions.CreateBall (Создать шар), который описан в библиотеке Petzold.Phone.Xna:

Проект XNA: Petzold.Phone.Xna Файл: Texture2DExtensions.cs (фрагмент)

public static Texture2D CreateBall(GraphicsDevice graphicsDevice, int radius) {

Texture2D ball = new Texture2D(graphicsDevice, 2 * radius, 2 * radius); Color[] pixels = new Color[ball.Width * ball.Height]; int radiusSquared = radius * radius;

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

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

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

pixels[(int)(ball.Width * (y + radius) + x + radius)] = Color.White;

}

ball.SetData<Color>(pixels); return ball;

}

Но это простая часть. Сложность состоит в описании физики перемещения шарика по наклонной плоскости. Начнем с рассмотрения взаимосвязей между вектором акселерометра и углами, описывающими наклонение телефона.

Теперь приподнимем телефон слева:

Предположим, телефон просто лежит на плоской поверхности, такой как стол. Представим это следующей схемой:

Плоскость телефона образует с поверхностью стола угол а. Можно ли из вектора акселерометра вычислить значение а?

Когда телефон просто лежит на столе, вектор ускорения равен (0, 0, -1). Если наклонить телефон, как показано на рисунке, вектор ускорения, возможно, становится равным (0.34, 0, – 0.94). (На первый взгляд можно и не заметить, что квадраты этих чисел в сумме дают 1, но это так.[27])

Вектор ускорения всегда направлен к центру земли, т.е. под прямым углом к горизонтальной поверхности:

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

Вычисление ускорения катящегося шара несколько запутанно (для примера, ознакомьтесь с работой A. P. French, Newtonian Mechanics[28], W. W. Norton, 1971, страницы 652-653), но без учета трения все сильно упрощается:

ускорение =  * g * sіn (а)

где g – это ускорение свободного падения, составляющее 9,8 м/с2. Уже такого количества деталей более чем достаточно для реализации катящегося шара в простом приложении на Windows Phone 7. На самом деле нам достаточно знать, что ускорение пропорционально синусу а. И это чрезвычайно неожиданно, поскольку означает, что ускорение катящегося поперек поверхности телефона шара (при портретной ориентации) пропорционально составляющей Х вектора ускорения! Для шара, катящегося вдоль по поверхности, ускорение – это двухмерный вектор, который может быть найден прямо из составляющих X и Y вектора акселерометра.

Приложение TiltAndRoll (Наклон и качение) моделирует перекатывание шара по поверхности экрана на основании наклонения телефона. Когда шар касается одного из краев экрана, он не отскакивает, а, напротив, теряет всю скорость в направлении, перпендикулярном краю. Мяч продолжает катиться вдоль края, если такое поведение соответствует наклонению телефона.

В приложении TiltAndRoll двухмерный вектор ускорения вычисляется из трехмерного вектора акселерометра и умножается на константу GRAVITY (гравитация), единицами измерения которой являются пикселы в секунду в квадрате. Теоретически вычислить значение GRAVITY можно, умножив 9,8 м/с2 на 39,37 дюймов в метре, и затем на 264 пиксела в дюйме, и еще на 2/3. В результате получим значение около 68000, но на практике оно обусловливает слишком быстрое ускорение шара. Я выбрал значение несколько поменьше, что создало эффект перемещения шара в вязкой жидкости:

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

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

const float GRAVITY = 1000;            // пикселов в секунду в квадрате

const int BALL RADIUS = 16; const int BALL_SCALE = 16;

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

Viewport viewport;

Texture2D ball;

Vector2 ballCenter;

Vector2 ballPosition;

Vector2 ballVelocity = Vector2.Zero;

Vector3 oldAcceleration, acceleration;

object accelerationLock = 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;

}

}

Конечно, если хотите, вы можете увеличить значение GRAVITY.

Чтобы упростить вычисления, конструктор разрешает только портретную ориентацию. Составляющие X и Y вектора акселерометра будут соответствовать координатам экрана, только оси Y акселерометра и экрана являются противоположно направленными.

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

Проект XNA: TiltAndRoll Файл: 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 (accelerationLock) {

acceleration = 0.5f * oldAcceleration +

0.5f * new Vector3((float)args.X, (float)args.Y,

(float)args.Z);

oldAcceleration = acceleration;

Как можно заметить в коде выше, радиус и масштаб шара заданы константами BALL_RADIUS и BALL_SCALE. Поскольку метод Texture2DExtensions.CreateBall не делает попытки реализации сглаживания, более гладкое изображение можно получить, сделав шар больше отображаемого размера и заставив XNA выполнять некоторое сглаживание при формировании его визуального представления. При создании радиус шара определяется как произведение BALL_RADIUS и BALL_SCALE, но впоследствии при отображении к нему применяется коэффициент масштабирования 1 / BALL_SCALE.

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

protected override void LoadContent() {

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

viewport = this.GraphicsDevice.Viewport;

ball = Texture2DExtensions.CreateBall(this.GraphicsDevice,

BALL_RADIUS * BALL_SCALE);

ballCenter = new Vector2(ball.Width / 2, ball.Height / 2); ballPosition = new Vector2(viewport.Width / 2, viewport.Height / 2);

}

ballPosition (Местоположение шара), инициализированный в LoadContent – это точка, хранящаяся как объект Vector2. Скорость также хранится как объект Vector2, но это действительный вектор, выраженный в пикселах в секунду. Скорость будет меняться только при соударении шара с краями экрана или при наклонении телефона, во всех остальных случаях скорость будет оставаться неизменной благодаря эффекту инерции. Все эти вычисления выполняются в перегруженном Update:

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

protected override void Update(GameTime gameTime) {

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

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

// Вычисляем новую скорость и координаты Vector2 acceleration2D = Vector2.Zero;

lock (accelerationLock) {

acceleration2D = new Vector2(acceleration.X, -acceleration.Y);

}

float elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds; ballVelocity += GRAVITY * acceleration2D * elapsedSeconds; ballPosition += ballVelocity * elapsedSeconds;

// Проводим проверку на соударение с краем

if (ballPosition.X – BALL RADIUS < 0) {

ballPosition.X = BALL_RADIUS; ballVelocity.X = 0;

}

if (ballPosition.X + BALL_RADIUS > viewport.Width) {

ballPosition.X = viewport.Width – BALL_RADIUS; ballVelocity.X = 0;

}

if (ballPosition.Y – BALL_RADIUS < 0)

ballPosition.Y = BALL RADIUS; ballVelocity.Y = 0;

}

if (ballPosition.Y + BALL RADIUS > viewport.Height) {

ballPosition.Y = viewport.Height – BALL_RADIUS; ballVelocity.Y = 0;

}

base.Update(gameTime);

}

Два самых важных вычисления здесь:

ballVelocity += GRAVITY * acceleration2D * elapsedSeconds; ballPosition += ballVelocity * elapsedSeconds;

Вектор acceleration2D – это просто вектор показаний акселерометра без составляющей Z и с инвертированной координатой Y. Вектор скорости зависит от вектора ускорения и истекшего времени в секундах. Местоположение шара определяется результирующим вектором скорости и истекшим временем в секундах.

В этом прелесть векторов, что нам не нужно знать, наклонен ли телефон в одном направлении со скоростью (и, следовательно, обусловливает увеличение этой скорости), или в противоположную строну (в случае чего скорость уменьшается), или под каким-то другим углом, не имеющим никакого влияния на вектор скорости.

Выражения if, завершающие обработку в Update, обеспечивают проверку выхода шара за край экрана. В этом случае шар возвращается в окно просмотра, и соответствующая составляющая скорости обнуляется. Здесь не используются выражения if и else, поскольку шар может одновременно выйти за два края, и этот вариант также должен обрабатываться. В такой ситуации шар останавливается в углу экрана.

Метод Draw просто отрисовывает шар с применением коэффициента масштабирования: Проект XNA: TiltAndRoll Файл: Game1.cs (фрагмент)

protected override void Draw(GameTime gameTime) {

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

spriteBatch.Draw(ball, ballPosition, null, Color.Pink, 0,

ballCenter, 1f / BALL SCALE, SpriteEffects.None, 0); spriteBatch.End();

base.Draw(gameTime);

}

Приложение TiltAndBounce (Наклон и отскок) очень похоже на TiltAndRoll, только в нем шар отскакивает от краев. Это означает, что когда шар касается краев экрана, одна из составляющих его скорости меняет знак на противоположный. Например, если при соударении с правым или левым краем экрана скорость шара была (x, y), когда он отскакивает, его скорость становится (-x, y). Но это не соответствует законам физики. Шар должен терять часть скорости при соударении. Чтобы реализовать это, в поля приложения TiltAndBounce включен коэффициент затухания 2/3:

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

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

const float GRAVITY = 1000;            // пикселы в секунду в квадрате

const float BOUNCE = 2f / 3; // доля скорости const int BALL RADIUS = 16; const int BALL_SCALE = 16;

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

Viewport viewport;

Texture2D ball;

Vector2 ballCenter;

Vector2 ballPosition;

Vector2 ballVelocity = Vector2.Zero;

Vector3 oldAcceleration, acceleration;

object accelerationLock = new object();

}

Шар, имеющий до соударения с правым или левым краем экрана скорость (x, y), после соударения перемещается со скоростью (-BOUNCE-x, y).

Конструктор аналогичен приложению TiltAndRoll, как и перегруженный Initialize, и методы ReadingChanged, LoadContent и Draw. Единственным отличием в Update является реализация отскока шара в логике соударения с краем:

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

protected override void Update(GameTime gameTime) {

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

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

// Вычисляем новую скорость и координаты Vector2 acceleration2D = Vector2.Zero;

lock (accelerationLock) {

acceleration2D = new Vector2(acceleration.X, -acceleration.Y);

}

float elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds; ballVelocity += GRAVITY * acceleration2D * elapsedSeconds; ballPosition += ballVelocity * elapsedSeconds;

// Проверяем возможность отскока от края bool needAnotherLoop = false;

do {

needAnotherLoop = false;

if (ballPosition.X – BALL_RADIUS < 0) {

ballPosition.X = -ballPosition.X + 2 * BALL RADIUS; ballVelocity.X *= -BOUNCE; needAnotherLoop = true;

}

else if (ballPosition.X + BALL_RADIUS > viewport.Width) {

ballPosition.X = -ballPosition.X – 2 * (BALL RADIUS – viewport.Width); ballVelocity.X *= -BOUNCE; needAnotherLoop = true;

else if (ballPosition.Y – BALL_RADIUS < 0) {

ballPosition.Y = -ballPosition.Y + 2 * BALL_RADIUS; ballVelocity.Y *= -BOUNCE; needAnotherLoop = true;

}

else if (ballPosition.Y + BALL_RADIUS > viewport.Height) {

ballPosition.Y = -ballPosition.Y – 2 * (BALL_RADIUS – viewport.Height); ballVelocity.Y *= -BOUNCE; needAnotherLoop = true;

}

}

while (needAnotherLoop); base.Update(gameTime);

}

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

Можно даже расширить эту логику обработки отскока до простой игры. Приложение EdgeSlam (Удар по краям) очень похоже на TiltAndBounce. В этой игре она из сторон экрана выделяется белой линией. Цель – ударить шаром в этот край экрана. Как только шар ударяет выделенную сторону, произвольным образом выделяется одна из других сторон экрана. За каждое соударение с соответствующей стороной игрок получает 1 балл, за соударение с неправильной стороной с игрока снимается 5 штрафных очка. Счет отображается в центре экрана.

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

Кроме всех тех же констант, которые мы видели ранее, поля включают еще две новые для подсчета очков:

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

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

const      float GRAVITY = 1000;       // пикселы в секунду в квадрате

const      float BOUNCE = 2f / 3; // доля скорости

const      int BALL_RADIUS = 16;

const      int BALL_SCALE = 16;

const      int HIT = 1;

const      int PENALTY = -5;

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

Viewport viewport; Vector2 screenCenter;

SpriteFont segoe96; int score;

StringBuilder scoreText = new StringBuilder(); Vector2 scoreCenter;

Texture2D tinyTexture; int highlightedSide; Random rand = new Random();

Texture2D ball;

Vector2 ballCenter;

Vector2 ballPosition;

Vector2 ballVelocity = Vector2.Zero;

Vector3 oldAcceleration, acceleration;

object accelerationLock = 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;

}

}

SpriteFont используется для отображения счета большими цифрами в центре экрана. Объект tinyTexture обеспечивает выделение одной из сторон, которая выбирается случайным образом и обозначается значением highlightedSide (Выделенная сторона).

Перегруженный метод Initialize и метод акселерометра ReadingChanged аналогичны виденным ранее. LoadContent создает tinyTexture и загружает шрифт, а также создает шар:

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

protected override void LoadContent() {

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

viewport = this.GraphicsDevice.Viewport;

screenCenter = new Vector2(viewport.Width / 2, viewport.Height / 2);

ball = Texture2DExtensions.CreateBall(this.GraphicsDevice,

BALL_RADIUS * BALL_SCALE);

ballCenter = new Vector2(ball.Width / 2, ball.Height / 2); ballPosition = screenCenter;

tinyTexture = new Texture2D(this.GraphicsDevice, 1, 1); tinyTexture.SetData<Color>(new Color[] { Color.White });

segoe96 = this.Content.Load<SpriteFont>("Segoe9 6");

}

Метод Update начинается так же, как и в приложении TiltAndBounce, но большой цикл do несколько сложнее в данном случае. При соударении шара с одним из краев требуется скорректировать счет в зависимости от того, в какую из сторон попал шар: выделенную или нет:

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

protected override void Update(GameTime gameTime) {

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

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

// Вычисляем новую скорость и координаты Vector2 acceleration2D = Vector2.Zero;

lock (accelerationLock) {

acceleration2D = new Vector2(acceleration.X, -acceleration.Y);

}

float elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds; ballVelocity += GRAVITY * acceleration2D * elapsedSeconds; ballPosition += ballVelocity * elapsedSeconds;

// Проверяем возможность отскока от края bool needAnotherLoop = false; bool needAnotherSide = false;

do {

needAnotherLoop = false;

if (ballPosition.X – BALL_RADIUS < 0) {

score += highlightedSide == 0 ? HIT : PENALTY; ballPosition.X = -ballPosition.X + 2 * BALL_RADIUS; ballVelocity.X *= -BOUNCE; needAnotherLoop = true;

}

else if (ballPosition.X + BALL_RADIUS > viewport.Width) {

score += highlightedSide == 2 ? HIT : PENALTY;

ballPosition.X = -ballPosition.X – 2 * (BALL_RADIUS – viewport.Width); ballVelocity.X *= -BOUNCE; needAnotherLoop = true;

}

else if (ballPosition.Y – BALL RADIUS < 0) {

score += highlightedSide == 1 ? HIT : PENALTY; ballPosition.Y = -ballPosition.Y + 2 * BALL RADIUS; ballVelocity.Y *= -BOUNCE; needAnotherLoop = true;

}

else if (ballPosition.Y + BALL_RADIUS > viewport.Height) {

score += highlightedSide == 3 ? HIT : PENALTY;

ballPosition.Y = -ballPosition.Y – 2 * (BALL_RADIUS – viewport.Height); ballVelocity.Y *= -BOUNCE; needAnotherLoop = true;

}

needAnotherSide |= needAnotherLoop;

}

while (needAnotherLoop);

if (needAnotherSide) {

scoreText.Remove(0, scoreText.Length); scoreText.Append(score);

scoreCenter = segoe96.MeasureString(scoreText) / 2; highlightedSide = rand.Next(4);

}

base.Update(gameTime);

Если в ходе обработки отскока переменная needAnotherSide (Необходима другая сторона) принимает значение true, перегруженный Update завершается обновлением объекта StringBuilder под именем scoreText (Счет) и выбором новой стороны-мишени случайным образом. Поле scoreText остается незаданным, пока игрок не заработал каких-то баллов. Сначала я задавал этому полю значение нуль, но в начале игры шар расположен в центре экрана, и шар внутри нуля выглядел очень странно!

Перегруженный метод Draw, исходя из highlightedSide, определяет, где должен размещаться tinyTexture, и также отвечает за отображение счета:

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

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.Navy);

spriteBatch.Begin();

Rectangle rect = new Rectangle();

switch (highlightedSide) {

case 0: rect = new Rectangle(0, 0, 3, viewport.Height); break;

case 1: rect = new Rectangle(0, 0, viewport.Width, 3); break;

case 2: rect = new Rectangle(viewport.Width – 3, 0, 3, viewport.Height);

break;

case 3: rect = new Rectangle(3, viewport.Height – 3, viewport.Width, 3);

break; }

spriteBatch.Draw(tinyTexture, rect, Color.White);

spriteBatch.DrawString(segoe9 6, scoreText, screenCenter,

Color.LightBlue, 0,

scoreCenter, 1, SpriteEffects.None, 0);

spriteBatch.Draw(ball, ballPosition, null, Color.Pink, 0,

ballCenter, 1f / BALL_SCALE, SpriteEffects.None, 0); spriteBatch.End();

base.Draw(gameTime);

}

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

По теме:

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