Главная » Разработка для Windows Phone 7 » Play и Replay

0

Метод Replay находится в классе Game1.Helper.cs:

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

void Replay() {

for (int i = 0; i < 4; i++) holds[i] = null;

foreach (List<CardInfo> final in finals) final.Clear();

foreach (List<CardInfo> pile in piles) pile.Clear();

ShuffleDeck(deck);

// Распределение карт по стопкам

for (int card = 0; card < 52; card++) {

piles[card % 8].Add(deck[card]);

}

CalculateDisplayMatrix();

Данный метод очищает массив holds и коллекции finals и piles, перемешивает колоду карт случайным образом и распределяет их в восемь коллекций в piles. Метод завершается вызовом CalculateDisplayMatrix (Вычислить матрицу отображения). Это не единственный случай вызова данного метода. Он также вызывается из метода OnActivated при восстановлении приложения после захоронения. И после этого при любом перемещении карты из или добавлении в одну из коллекций матрица отображения пересчитывается, просто на всякий случай.

Эта матрица отвечает за определение высоты игрового поля, если его необходимо увеличить для просмотра всех карт в области piles. Приложение обрабатывает этот аспект не очень красиво. Все игровое поле просто немного сжимается, включая все карты и даже кнопку «повторить игру»:

Это решение мне не очень нравится, тем не менее рассмотрим метод CalculateDisplayMatrix, который все это делает:

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

void CalculateDisplayMatrix() {

// Высота окна просмотра равна 480, соответственно

// предпочтительным настройкам заднего буфера

int viewportHeight = this.GraphicsDevice.Viewport.Height;

// Определяем общую требуемую высоту и выполняем масштабирование по вертикали int maxCardsInPiles = 0;

foreach (List<CardInfo> pile in piles)

maxCardsInPiles = Math.Max(maxCardsInPiles, pile.Count);

int requiredHeight = 2 * yMargin + yGap + 2 * hCard +

yOverlay * (maxCardsInPiles – 1);

// Задаем матрицу сжатия по оси Y, чтобы обеспечить отображение на экране всех

карт

if (requiredHeight > viewportHeight)

displayMatrix = Matrix.CreateScale(1, (float)viewportHeight / requiredHeight, 1); else

displayMatrix = Matrix.Identity;

// Находим обратную матрицу для тестирования inverseMatrix = Matrix.Invert(displayMatrix);

}

Объект displayMatrix используется в вызове Begin класса SpriteBatch, так что применяется ко всем объектам одним махом. Хотя это несколько не соответствует моему привычному порядку изложения, но мы уже готовы рассмотреть метод Draw класса Game1.

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

protected override void Draw(GameTime gameTime) {

spriteBatch.Begin(SpriteSortMode.Immediate, null, null, null, null, null,

displayMatrix); spriteBatch.Draw(surface, Vector2.Zero, Color.White);

// Отрисовываем holds

for (int hold = 0; hold < 4; hold++) {

CardInfo cardInfo = holds[hold];

if (cardInfo != null) {

Rectangle source = GetCardTextureSource(cardInfo);

Vector2 destination = new Vector2(cardSpots[hold].X, cardSpots[hold].Y); spriteBatch.Draw(cards, destination, source, Color.White);

}

}

// Отрисовываем piles

for (int pile = 0; pile < 8; pile++) {

Rectangle cardSpot = cardSpots[pile + 8];

for (int card = 0; card < piles[pile].Count; card++) {

CardInfo cardInfo = piles[pile][card];

Rectangle source = GetCardTextureSource(cardInfo);

Vector2 destination = new Vector2(cardSpot.X, cardSpot.Y + card *

yOverlay);

spriteBatch.Draw(cards, destination, source, Color.White);

}

}

// Отрисовываем finals, включая все предыдущие карты (для автоперемещения)

for (int pass = 0; pass < 2; pass++) {

for (int final = 0; final < 4; final++) {

for (int card = 0; card < finals[final].Count; card++) {

CardInfo cardInfo = finals[final][card];

if (pass == 0 && cardInfo.AutoMoveInterpolation == 0 || pass == 1 && cardInfo.AutoMoveInterpolation != 0)

{

Rectangle source = GetCardTextureSource(cardInfo); Vector2 destination =

new Vector2(cardSpots[final + 4].X,

cardSpots[final + 4].Y) + cardInfo.AutoMoveInterpolation *

cardInfo.AutoMoveOffset;

spriteBatch.Draw(cards, destination, source, Color.White);

}

}

}

}

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

if (touchedCard != null) {

Rectangle source = GetCardTextureSource(touchedCard); spriteBatch.Draw(cards, touchedCardPosition, source, Color.White);

}

spriteBatch.End();

base.Draw(gameTime);

После вызова метода Begin объекта SpriteBatch и вывода растрового изображения surface для игрового поля метод готов к отрисовке карт. Этот процесс начинается с самого простого: четырех возможных карт массива holds. Небольшой метод GetCardTextureSource возвращает Rectangle для позиционирования карты в рамках растрового изображения карт, и массив cardSpot обеспечивает координаты для размещения каждой карты.

Следующая часть несколько сложнее. В зоне piles для отображения перекрывающихся карт к координатам cardSpot должны применяться смещения. По-настоящему проблематичной является зона finals. Сложности здесь связаны с реализацией возможности автоматического перемещения. Как мы увидим, карта, отвечающая условиям для автоматического перемещения, удаляется из массива holds или коллекции piles, в которой она находится, и помещается в коллекцию finals. Но это перемещение карты из предыдущего местоположения в новое должно быть анимировано. Для этого в CardInfo предусмотрены свойства AutoMoveOffset (Смещение для автоперемещения) и AutoMoveInterpolation (Интерполяция для автоперемещения).

Метод Draw должен отрисовывать все четыре коллекции finals последовательно слева направо и затем в рамках каждой коллекции все карты, начиная с самой первой карты (которой всегда является туз) до самой последней, которая располагается в самом верху стопки. Я обнаружил, что это не всегда получается, и анимированная карта иногда на мгновение как будто задвигается в одну из стопок finals. Именно поэтому цикл для отображения коллекций finals включает два прохода: один для неанимированных карт и другой для любой анимированной в ходе автоматического перемещения карты. (Данное приложение одновременно выполняет анимацию только одной карты, а вот в предыдущей версии обеспечивалась анимация нескольких карт.)

Draw завершается отрисовкой карты, которую пользователь, возможно, перемещает в настоящий момент.

Метод Update практически полностью посвящен реализации анимации для автоматического перемещения и обработки касания. Его большая часть с вложенными циклами foreach обеспечивает перемещение карт, которые помечены для автоматического перемещения и, следовательно, уже перенесены в коллекции finals.

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

protected override void Update(GameTime gameTime) {

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

// Обрабатываем автоматическое перемещение карты и, возможно, // инициируем следующее автоматическое перемещение bool checkForNextAutoMove = false;

foreach (List<CardInfo> final in finals)

foreach (CardInfo cardInfo in final) {

if (cardInfo.AutoMoveTime > TimeSpan.Zero) {

cardInfo.AutoMoveTime -= gameTime.ElapsedGameTime;

if (cardInfo.AutoMoveTime <= TimeSpan.Zero) {

cardInfo.AutoMoveTime = TimeSpan.Zero; checkForNextAutoMove = true;

cardInfo.AutoMoveInterpolation = (float)cardInfo.AutoMoveTime.Ticks

/

AutoMoveDuration.Ticks;

}

}

if (checkForNextAutoMove && !AnalyzeForAutoMove() && HasWon()) {

congratsComponent.Enabled = true;

}

}

На самом деле выбор карт для автоматического перемещения выполняется в конце этого кода посредством вызова метода AnalyzeforAutoMove (Анализ возможности автоперемещения), который описан в файле Game1.Helpers.cs. (AnalyzeforAutoMove также вызывается позже в перегруженном Update после перемещения карты вручную.) Этот метод перебирает все элементы holds и piles и вызывается метод CheckForAutoMove (Проверка возможности автоперемещения) для каждой верхней карты. Если CheckForAutoMove возвращает true, значит, этот метод уже переместил карту в соответствующую коллекцию finals, и ее необходимо убрать с того места, где она находится на экране. Для фактического перемещения в Update инициализируются три свойства CardInfo, показанные выше:

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

bool AnalyzeForAutoMove() {

for (int hold = 0; hold < 4; hold++) {

CardInfo cardInfo = holds[hold];

if (cardInfo != null && CheckForAutoMove(cardInfo)) {

holds[hold] = null;

cardInfo.AutoMoveOffset += new Vector2(cardSpots[hold].X, cardSpots[hold].Y);

cardInfo.AutoMoveInterpolation = 1; cardInfo.AutoMoveTime = AutoMoveDuration; return true;

}

}

for (int pile = 0; pile < 8; pile++) {

CardInfo cardInfo = TopCard(piles[pile]);

if (cardInfo != null && CheckForAutoMove(cardInfo)) {

piles[pile].Remove(cardInfo);

cardInfo.AutoMoveOffset += new Vector2(cardSpots[pile + 8].X,

cardSpots[pile + 8].Y + piles[pile].Count *

yOverlay);

cardInfo.AutoMoveInterpolation = 1; cardInfo.AutoMoveTime = AutoMoveDuration; return true;

}

}

return false;

Код реализации логики выбора карт, соответствующих условиям автоматического перемещения (если таковые имеются), оказывается самой длинной частью приложения. Сложность в том, что карта, которая еще может использоваться в стратегии игры, не должна перемещаться в коллекцию finals. Например, четверка червей не должна быть перенесена в коллекцию finals, если где-то в коллекции piles или holds еще имеется тройка пик или тройка треф.

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

bool CheckForAutoMove(CardInfo cardInfo) {

if (cardInfo.Rank == 0)            // т.е. туз

{

for (int final = 0; final < 4; final++)

if (finals[final].Count == 0) {

finals[final].Add(cardInfo);

cardInfo.AutoMoveOffset = -new Vector2(cardSpots[final + 4].X,

cardSpots[final + 4].Y);

return true;

}

}

else if (cardInfo.Rank == 1) // т.е. двойка {

for (int final = 0; final < 4; final++) {

CardInfo topCardInfo = TopCard(finals[final]);

if (topCardInfo != null &&

topCardInfo.Suit == cardInfo.Suit && topCardInfo.Rank == 0)

{

finals[final].Add(cardInfo);

cardInfo.AutoMoveOffset = -new Vector2(cardSpots[final + 4].X,

cardSpots[final + 4].Y);

return true;

}

}

}

else {

int slot = -1; int count = 0;

for (int final = 0; final < 4; final++) {

CardInfo topCardInfo = TopCard(finals[final]);

if (topCardInfo != null) {

if (topCardInfo.Suit == cardInfo.Suit && topCardInfo.Rank == cardInfo.Rank – 1)

{

slot = final;

}

else if (topCardInfo.Suit < 2 != cardInfo.Suit < 2 && topCardInfo.Rank >= cardInfo.Rank – 1)

{

count++;

}

}

}

if (slot >= 0 && count == 2) {

cardInfo.AutoMoveOffset = -new Vector2(cardSpots[slot + 4].X,

cardSpots[slot + 4].Y);

finals[slot].Add(cardInfo); return true;

return false;

Ранее в перегруженном Update после анимации автоматически перемещаемых карт выполнялась проверка того, не касается ли пользователь карты, пытаясь «выбрать» ее. Выбор конкретной карты может быть допустимым или нет. Если карта уже перемещена и пользователь пытается «положить» эту карту, ее выбор также может быть недопустимым. Допустимость определяется в ходе вызовов TryPickUpCard (Попытаться выбрать карту) и TryPutDownCard (Попытаться положить карту). Обратите внимание, что точка касания настраивается с помощью inverseMatrix соответственно фактическому местоположению карты.

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

protected override void Update(GameTime gameTime) {

while (TouchPanel.IsGestureAvailable) {

GestureSample gesture = TouchPanel.ReadGesture();

// Настраиваем местоположение и его изменение для сжатого изображения Vector2 position = Vector2.Transform(gesture.Position, inverseMatrix); Vector2 delta = position – Vector2.Transform(gesture.Position – gesture.Delta,

inverseMatrix);

switch (gesture.GestureType) {

case GestureType.Tap:

// Проверяем, нажата ли кнопка Replay

if ((gesture.Position – centerReplay).Length() < radiusReplay) {

congratsComponent.Enabled = false; Replay();

}

break;

case GestureType.FreeDrag:

// Продолжаем перемещать выбранную карту

if (touchedCard != null) {

touchedCardPosition += delta;

}

// Делаем попытку выбрать карту

else if (firstDragInGesture) {

TryPickUpCard(position);

}

firstDragInGesture = false; break;

case GestureType.DragComplete:

if (touchedCard != null && TryPutDownCard(touchedCard)) {

CalculateDisplayMatrix();

if (!AnalyzeForAutoMove() && HasWon()) {

congratsComponent.Enabled = true;

firstDragInGesture = true; touchedCard = null; break;

base.Update(gameTime);

Оба метода, TryPickUpCard и TryPutDownCard, реализованы в файле Game1.Helpers.cs и на самом деле определяют правила игры.

TryPickUpCard – более простой из этих двух методов. Он лишь принимает координаты касания и должен определить, на какую карту это касание приходится. Выбрана может быть только карта одной из коллекций holds или верхняя карта одной из коллекций piles. В противном случае метод даже не пытается определить возможность каких-либо манипуляций с картой:

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

bool TryPickUpCard(Vector2 position) {

for (int hold = 0; hold < 4; hold++) {

if (holds[hold] != null && IsWithinRectangle(position, cardSpots[hold])) {

Point pt = cardSpots[hold].Location;

touchedCard = holds[hold]; touchedCardOrigin = holds; touchedCardOriginIndex = hold;

touchedCardPosition = new Vector2(pt.X, pt.Y); holds[hold] = null; return true;

}

}

for (int pile = 0; pile < 8; pile++) {

if (piles[pile].Count > 0) {

Rectangle pileSpot = cardSpots[pile + 8]; pileSpot.Offset(0, yOverlay * (piles[pile].Count – 1));

if (IsWithinRectangle(position, pileSpot)) {

Point pt = pileSpot.Location;

int pileIndex = piles[pile].Count – 1;

touchedCard = piles[pile][pileIndex]; touchedCardOrigin = piles; touchedCardOriginIndex = pile;

touchedCardPosition = new Vector2(pt.X, pt.Y);

piles[pile].RemoveAt(pileIndex);

return true;

}

}

}

return false;

Как только карта выбрана, метод TryPickUpCard уже задает поля, касающиеся этой выбранной карты, которые затем используются методом Update для последующего ее перемещения по экрану.

Метод TryPutDownCard позволяет размещать карты в коллекциях piles или finals либо в массиве holds, обеспечивая при этом выполнение правил игры. Если карта не может быть помещена в данную стопку, она просто восстанавливается в исходном местоположении без всякой анимации:

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

bool TryPutDownCard(CardInfo touchedCard) {

Vector2 cardCenter = new Vector2(touchedCardPosition.X + wCard / 2,

touchedCardPosition.Y + hCard / 2);

for (int cardSpot = 0; cardSpot < 16; cardSpot++) {

Rectangle rect = cardSpots[cardSpot];

// Значительно расширяем прямоугольник местоположения карт для стопок if (cardSpot >= 8)

rect.Inflate(0, hSurface – rect.Bottom);

if (IsWithinRectangle(cardCenter, rect)) {

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

if (cardSpot < 4) {

int hold = cardSpot;

if (holds[hold] == null) {

holds[hold] = touchedCard; return true;

}

}

else if (cardSpot < 8) {

int final = cardSpot – 4;

if (TopCard(finals[final]) == null) {

if (touchedCard.Rank == 0) // т.е. туз {

finals[final].Add(touchedCard); return true;

}

}

else if (touchedCard.Suit == TopCard(finals[final]).Suit &&

touchedCard.Rank == TopCard(finals[final]).Rank + 1)

{

finals[final].Add(touchedCard); return true;

}

}

else {

int pile = cardSpot – 8;

if (piles[pile].Count == 0) {

piles[pile].Add(touchedCard); return true;

else {

CardInfo topCard = TopCard(piles[pile]);

if (touchedCard.Suit < 2 != topCard.Suit < 2 && touchedCard.Rank == topCard.Rank – 1)

{

piles[pile].Add(touchedCard); return true;

}

}

}

// Карта располагалась в заданном прямоугольнике, // но ее размещение там было недопустимым break;

}

}

// Восстанавливаем карту в ее исходном местоположении

if (touchedCardOrigin is CardInfo[]) {

(touchedCardOrigin as CardInfo[])[touchedCardOriginIndex] = touchedCard;

}

else {

((touchedCardOrigin as

List<CardInfo>[])[touchedCardOriginIndex]).Add(touchedCard); }

return false;

}

Но вся эта работа прекращается, когда следующий метод, который просто проверяет, не является ли верхняя карта коллекции finals королем, возвращает значение true:

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

bool HasWon() {

bool hasWon = true;

foreach (List<CardInfo> cardInfos in finals)

hasWon &= cardInfos.Count > 0 && TopCard(cardInfos).Rank == 12;

return hasWon;

}

Метод Update использует это для активации компонента CongratulationsComponent, полный код которого представлен в следующем листинге:

Проект XNA: PhreeCell Файл: CongratulationsComponent.cs

using System;

using Microsoft.Xna.Framework;

using Microsoft.Xna.Framework.Graphics;

namespace PhreeCell {

public class CongratulationsComponent : DrawableGameComponent {

const float SCALE_SPEED = 0.5f;                                  // полразмера в секунду

const float ROTATE_SPEED = 3 * MathHelper.TwoPi; // 3 оборота в секунду

SpriteBatch spriteBatch; SpriteFont pericles108;

string congratulationsText = "You Won!";

float textScale;

float textAngle;

Vector2 textPosition;

Vector2 textOrigin;

public CongratulationsComponent(Game game) : base(game) {

}

protected override void LoadContent() {

spriteBatch = new SpriteBatch(this.GraphicsDevice); pericles108 = this.Game.Content.Load<SpriteFont>("Pericles108"); textOrigin = pericles108.MeasureString(congratulationsText) / 2; Viewport viewport = this.GraphicsDevice.Viewport;

textPosition = new Vector2(Math.Max(viewport.Width, viewport.Height) /

2,

Math.Min(viewport.Width, viewport.Height) /

2);

base.LoadContent();

}

protected override void OnEnabledChanged(object sender, EventArgs args) {

Visible = Enabled;

if (Enabled) {

textScale = 0; textAngle = 0;

}

}

public override void Update(GameTime gameTime) {

if (textScale < 1) {

textScale +=

SCALE_SPEED * (float)gameTime.ElapsedGameTime.TotalSeconds; textAngle +=

ROTATE_SPEED * (float)gameTime.ElapsedGameTime.TotalSeconds;

}

else if (textAngle 1= 0) {

textScale = 1; textAngle = 0;

}

base.Update(gameTime);

}

public override void Draw(GameTime gameTime) {

spriteBatch.Begin();

spriteBatch.DrawString(pericles108, congratulationsText, textPosition,

Color.White, textAngle, textOrigin, textScale, SpriteEffects.None, 0);

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

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

А теперь можно нажать красную кнопку и начать игру заново. Удачи!

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

По теме:

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