netlib.narod.ru< Назад | Оглавление | Далее >

Давайте напишем Pong

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

После совмещения элементов меню вы добавите ракетки, перемещаемые вверх и вниз с помощью возможностей ввода, рассмотренных в главе 1. Мяч перемещается с помощью нескольких простых переменных, и каждый раз, когда ракетка отбивает мяч, воспроизводится звук PongBallHit.wav. Если мяч улетает за левую или правую стороны экрана, воспроизводится звук PongBallLost.wav и игрок теряет одну жизнь.

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

В следующих частях главы вы проверите всю игру целиком на Xbox 360 и подумаете об улучшениях, которые можно внести в игру.

Спрайты

Как вы видели в главе 1, класс SpriteBatch используется для визуализации ваших текстур непосредственно на экране. Поскольку у нас пока нет никаких вспомогательных классов, мы будем визуализировать всю графику тем же самым способом. За дополнительной информацией о вспомогательных классах, которые сделают вашу повседневную жизнь программиста легче, обратитесь к главе 3. Для игры Pong вы будете использовать два слоя: Space Background, который загружается из текстуры PongBackground.dds, и меню и игровые текстуры, отображающие элементы меню и игровые компоненты (ракетки и мяч).

Для загрузки всех текстур используются приведенные ниже строки. Прежде всего, вам надо объявить используемые текстуры:

Texture2D backgroundTexture, menuTexture, gameTexture;

Затем загружаем все, что нам надо, в методе Initialize:

// Загрузка всего содержимого
backgroundTexture = content.Load<Texture2D>("PongBackground");
menuTexture       = content.Load<Texture2D>("PongMenu");
gameTexture       = content.Load<Texture2D>("PongGame");

И, наконец, визуализируем фон с помощью метода класса SpriteBatch, о котором узнали в главе 1:

// Рисуем текстуру фона в отдельном проходе, иначе она может смешиваться
// с другими спрайтами из-за работы механизма упорядочивания
spriteBatch.Begin();
spriteBatch.Draw(backgroundTexture,
                 new Rectangle(0, 0, width, height),
                 Color.LightGray);
spriteBatch.End();

Значение LightGray для цвета означает, что вы слегка затемняете фон, чтобы элементы переднего плана (текст меню и игровые элементы) были более контрастными. Как видите, здесь просто одна строка для визуализации спрайта на экране, а визуализация частей текстуры спрайтов будет более сложной. Взглянем на прямоугольники, используемые для игры:

static readonly Rectangle
  XnaPongLogoRect      = new Rectangle(  0,   0, 512, 110),
  MenuSingleplayerRect = new Rectangle(  0, 110, 512,  38),
  MenuMultiplayerRect  = new Rectangle(  0, 148, 512,  38),
  MenuExitRect         = new Rectangle(  0, 185, 512,  38),
  GameLifesRect        = new Rectangle(  0, 222, 100,  34),
  GameRedWonRect       = new Rectangle(151, 222, 155,  34),
  GameBlueWonRect      = new Rectangle(338, 222, 165,  34),
  GameRedPaddleRect    = new Rectangle( 23,   0,  22,  92),
  GameBluePaddleRect   = new Rectangle(  0,   0,  22,  92),
  GameBallRect         = new Rectangle(  1,  94,  33,  33),
  GameSmallBallRect    = new Rectangle( 37, 108,  19,  19);

Здесь довольно много прямоугольников, но все равно проще использовать константы, чем, например, импортировать XML-данные. Статические доступные только для чтения экземпляры используются здесь вместо констант потому что константам не могут быть назначены структуры, а статические доступные только для чтения экземпляры ведут себя точно так же, как константы. Вы можете спросить, как получены эти значения и как убедиться, что они правильные.

Тестирование модулей в игре

Здесь вступает в игру тестирование модулей. Тестирование модулей в программировании игр в основном означает разбиение задачи на небольшие, легко управляемые проблемы. Даже для очень простой игры, написание тестовых модулей — хорошая идея. Тестовые модули замечательно подходят для выравнивания текстур на экране, проверки звуковых эффектов и добавления проверки столкновений. Сперва я планировал написать эту главу и игру Pong без использования тестирования модулей, но начав программировать игру я не мог остановиться, и прежде чем опомнился у меня уже было написано шесть тестовых модулей, и я был вынужден и дальше идти по этому пути, поскольку не было смысла удалять эти очень полезные тесты.

Например, для проверки графики меню вы используете следующий тестовый модуль:

public static void TestMenuSprites()
{
  StartTest(
    delegate
    {
      testGame.RenderSprite(testGame.menuTexture,
                512-XnaPongLogoRect.Width/2, 150,
                XnaPongLogoRect);
      testGame.RenderSprite(testGame.menuTexture,
                512-MenuSingleplayerRect.Width/2, 300,
                MenuSingleplayerRect);
      testGame.RenderSprite(testGame.menuTexture,
                512-MenuMultiplayerRect.Width/2, 350,
                MenuMultiplayerRect, Color.Orange);
      testGame.RenderSprite(testGame.menuTexture,
                512-MenuExitRect.Width/2, 400,
                MenuExitRect);
   });
} // TestMenuSprites()

Пожалуйста, обратите внимание: это не финальный способ, которым вы будете использовать в этой книге тестирование модулей. Здесь показана только самая базовая идея о выполнении тестов. Делегат содержит код, который вы выполняете в каждом кадре в методе Draw.

Вы можете спросить себя: что это за StartTest? Что насчет testGame или метода RenderSprite? Откуда они взялись? Хорошо, это одно из главных отличий между старым способом программирования игр и использованием гибкого подхода с тестированием модулей. Все эти методы еще не существуют. Подобно тому, как планировалась игра, вы планируете и тесты модулей, записывая как вы хотите что-либо проверить; в данном случае отображается логотип игры и три элемента меню (Singleplayer, Multiplayer и Exit).

После написания тестового модуля и исправления всех синтаксических ошибок вы можете немедленно начать тестирование, скомпилировав ваш код — просто нажмите F5 и увидите набор сообщений об ошибках. Эти ошибки должны быть исправлены одна за другой и тогда тестовый модуль может быть запущен. Статические тесты модулей нечасто используют функцию Assert, но вполне можно добавить код, генерирующий исключения, если какие-то значения оказываются не теми, которые ожидались. Для вашего тестового модуля вы выполняете проверку глядя на результат на экране и модифицируя метод RenderSprite, пока все не будет работать так, как вы хотели.

В следующей главе мы более подробно поговорим о тестировании модулей. Для игры Pong вы просто выполняете наследование от класса PongGame и добавляете простой делегат для исполнения произвольного кода в вашем тесте модуля:

delegate void TestDelegate();
class TestPongGame : PongGame
{
  TestDelegate testLoop;
  public TestPongGame(TestDelegate setTestLoop)
  {
    testLoop = setTestLoop;
} // TestPongGame(setTestLoop)

  protected override void Draw(GameTime gameTime)
  {
    base.Draw(gameTime);
    testLoop();
  } // Draw(gameTime)
} // class TestPongGame

Теперь можно написать очень простой метод StartTest для создания экземпляра TestPongGame и последующего вызова Run для исполнения метода Draw с вашим собственным кодом testLoop:

static TestPongGame testGame;

static void StartTest(TestDelegate testLoop)
{
  using (testGame = new TestPongGame(testLoop))
  {
    testGame.Run();
  } // using
} // StartTest(testLoop)

Статический экземпляр testGame используется для упрощения написания тестов модулей, но это может путать, если вы используете его где-нибудь еще, поскольку он становится действительным только после вызова StartTest. В следующих главах вы увидите лучший способ.

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

public void RenderSprite(Texture2D texture, int x, int y,
  Rectangle sourceRect, Color color)
{
  //TODO
} // RenderSprite(texture, rect, sourceRect)

public void RenderSprite(Texture2D texture, int x, int y,
  Rectangle sourceRect)
{
  //TODO
} // RenderSprite(texture, rect, sourceRect

После добавления этих двух методов вы можете выполнить метод TestMenuSprites. Как это сделать? С TestDriven.NET вы можете просто щелкнуть правой кнопкой мыши и выбрать команду Start Test, но XNA Game Studio Express не поддерживает подключаемые модули, и вам надо использовать другой способ тестирования модулей, изменяя метод Main в файле Program.cs:

static void Main(string[] args)
{
  //PongGame.StartGame();
  PongGame.TestMenuSprites();
} // Main(args)

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

public static void StartGame()
{
  using (PongGame game = new PongGame())
  {
    game.Run();
  } // using
} // StartGame()

Если вы сейчас нажмете F5, то вместо обычного игрового кода будет выполняться тест модуля. Поскольку RenderSprite пока не содержит никакого кода, вы увидите только фоновое изображение, рисуемое методом Draw класса PongGame. Теперь добавим код для работы меню. Вы уже знаете как визуализировать спрайты, но вызывать функции начала и завершения класса SpriteBatch для каждого вызова RenderSprite очень неэффективно. Создайте простой список спрайтов, которые надо визуализировать в отдельном кадре и добавляйте к нему элементы при каждом вызове RenderSprite. Затем, в конце формирования кадра вы просто нарисуете все спрайты:

class SpriteToRender
{
  public Texture2D  texture;
  public Rectangle  rect;
  public Rectangle? sourceRect;
  public Color      color;

  public SpriteToRender(Texture2D setTexture, Rectangle setRect,
                        Rectangle? setSourceRect, Color setColor)
  {
    texture    = setTexture;
    rect       = setRect;
    sourceRect = setSourceRect;
    color      = setColor;
  } // SpriteToRender(setTexture, setRect, setColor)
} // SpriteToRender

List<SpriteToRender> sprites = new List<SpriteToRender>();

Кстати, вы добавляете весь этот код, включая тесты модулей в ваш класс PongGame. Обычно желательно в дальнейшем повторно использовать код и усовершенствовать игру, поэтому лучше разделить все на несколько классов. Чтобы упростить материал и поскольку мы не будем позже использовать части этого кода, сейчас мы просто пытаемся написать все необходимое как можно быстрее. Хотя ясно, что это не самый чистый и изящный способ кодирования, это обычно самый быстрый и эффективный способ запуска тестов модулей. Позднее вы сможете выполнить рефакторинг кода, чтобы сделать его более элегантным и лучше приспособленным к повторному использованию. Благодаря тестовому модулю у вас будет надежный инструмент, позволяющий удостовериться, что после нескольких изменений структуры кода он продолжает правильно функционировать.

Возможно, вы заметили, что в предыдущем коде для аргумента sourceRect вместо обычного Rectangle используется Rectangle?. Запись Rectangle? означает необязательный параметр, который может принимать значение null, что позволяет создавать перегрузки RenderSprite, которые не используют sourceRect для визуализации всей текстуры.

public void RenderSprite(Texture2D texture, Rectangle rect,
                         Rectangle? sourceRect, Color color)
{
  sprites.Add(new SpriteToRender(texture, rect, sourceRect, color));
} // RenderSprite(texture, rect, sourceRect, color)

Все это достаточно прямолинейно. Метод DrawSprites, который вызывается в конце метода Draw тоже не слишком сложен:

public void DrawSprites()
{
  // Если спрайтов в кадре нет, визуализация не нужна
  if (sprites.Count == 0)
    return;

  // Начало визуализации спрайтов
  spriteBatch.Begin(SpriteBlendMode.AlphaBlend,
                    SpriteSortMode.BackToFront,
                    SaveStateMode.None);

  // Визуализируем все спрайты
  foreach (SpriteToRender sprite in sprites)
    spriteBatch.Draw(sprite.texture,
                     // Масштабируем согласно разрешению
                     new Rectangle(
                           sprite.rect.X * width / 1024,
                           sprite.rect.Y * height / 768,
                           sprite.rect.Width * width / 1024,
                           sprite.rect.Height * height / 768),
                     sprite.sourceRect, sprite.color);

  // Мы закончили, отображаем все на экране
  // при помощи метода End
  spriteBatch.End();

  // Удаляем список запомненных спрайтов
  sprites.Clear();
} // DrawSprites()

Хотя для данной игры это и не важно, по крайней мере на платформе Windows где в качестве разрешения по умолчанию вы будете использовать 1024 × 768, надо будет масштабировать все спрайты из разрешения 1024 × 768 до текущего разрешения. Пожалуйста обратите внимание, что все текстуры для этой игры и всех последующих игр в этой книге обычно хранятся в разрешении 1024 × 768. Код в DrawSprites гарантирует, что все спрайты будут корректно масштабироваться согласно установленному в данный момент разрешению. Например, на Xbox 360 возможны несколько разрешений и желательно заставить игру работать в этих разрешениях, которые вы не можете знать заранее. По этой причине игры для Xbox 360 должны быть независимыми от разрешения и поддерживать формат HDTV 1080p (1920 × 1080) если это возможно.

Сначала DrawSprites проверяет, есть ли спрайты для визуализации и, если нет, завершает работу. Затем визуализируются все спрайты с установленным по умолчанию режимом альфа-смешивания, отсортированные от задних к передним и без сохранения режима; это означает, что если вы меняете режим визуализации XNA, он не будет восстановлен при вызове End. Обычно всегда предпочтительнее использовать SaveStateMode.None, поскольку это быстрее, а через тесты модулей вы удостоверяетесь, что все работает и не делает изменений, способных нарушить визуализацию после этого метода.

Вы можете подумать «Все это только чтобы визуализировать графику главного меню?», но если вы сейчас нажмете клавишу F5, то увидите экран, показанный на рис. 2.5. Поскольку вы реализовали базовый код вашей игры, весь код визуализации спрайтов и почти все, что нужно для ваших тестовых модулей, вы уже выполнили почти половину работы. Сейчас вам осталось только добавить игровую графику, управление, немного кода для обработки столкновений мяча — и работа завершена.


Рис. 2.5

Рис. 2.5


Добавляем мяч и ракетки

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

public static void TestGameSprites()
{
  StartTest(
    delegate
    {
      // Показываем жизни
      testGame.ShowLives();
      // Мяч в центре
      testGame.RenderBall();
      // Визуализируем обе ракетки
      testGame.RenderPaddles();
    });
} // TestGameSprites()

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

Чтобы удостовериться, что вы поняли как действуют эти тесты модулей, добавьте его и нажмите F5 после того, как добавите его вызов в Main и закомментируете старый тест:

//PongGame.StartGame();
//PongGame.TestMenuSprites();
PongGame.TestGameSprites();

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

public void ShowLives()
{
  // Жизни левого игрока
  RenderSprite(menuTexture, 2, 2, GameLivesRect);
  for (int num = 0; num < leftPlayerLives; num++)
    RenderSprite(gameTexture, 2+GameLivesRect.Width+
                 GameSmallBallRect.Width*num-2, 9,
                 GameSmallBallRect);
  // Жизни правого игрока
  int rightX = 1024-GameLivesRect.Width-GameSmallBallRect.Width*3-4;
  RenderSprite(menuTexture, rightX, 2, GameLivesRect);
  for (int num = 0; num < rightPlayerLives; num++)
    RenderSprite(gameTexture, rightX+GameLivesRect.Width+
                 GameSmallBallRect.Width*num-2, 9,
                 GameSmallBallRect);
} // ShowLives()

ShowLives просто отображает для каждого игрока текст «Lives:» и добавляет изображения мяча из игровой текстуры, чье количество соответствует количеству имеющихся у игрока жизней. RenderBall еще проще:

public void RenderBall()
{
  RenderSprite(gameTexture,
               (int)((0.05f+0.9f*ballPosition.X)*1024) - 
                 GameBallRect.Width/2,
               (int)((0.02f+0.96f*ballPosition.Y)*768) - 
                 GameBallRect.Height/2,
               GameBallRect);
} // RenderBall()

И, наконец, у вас есть функция RenderPaddles для отображения левой и правой ракеток в их текущей позиции:

public void RenderPaddles()
{
  RenderSprite(gameTexture,
               (int)(0.05f*1024)-GameRedPaddleRect.Width/2,
               (int)((0.06f+0.88f*leftPaddlePosition)*768) -
                 GameRedPaddleRect.Height/2,
               GameRedPaddleRect);
  RenderSprite(gameTexture,
               (int)(0.95f*1024)-GameBluePaddleRect.Width/2,
               (int)((0.06f+0.88f*rightPaddlePosition)*768) -
                 GameBluePaddleRect.Height/2,
               GameBluePaddleRect);
} // RenderPaddle(leftPaddle)

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

/// <summary>
/// Текущая позиция ракетки, 0 - верх, 1 - низ.
/// </summary>
float leftPaddlePosition  = 0.5f,
      rightPaddlePosition = 0.5f;

/// <summary>
/// Текущая позиция мяча, также от 0 до 1,
/// 0 - лево и верх, 1 - право и низ.
/// </summary>
Vector2 ballPosition = new Vector2(0.5f, 0.5f);

/// <summary>
/// Вектор скорости мяча, случайно генерируется
/// для каждого нового мяча. Обнуляется, если
/// мы в меню или игра закончена.
/// </summary>
Vector2 ballSpeedVector = new Vector2(0, 0);

Немного поясню, почему в методах визуализации мы работаем с числами с плавающей точкой. Это способ не иметь дела с экранными координатами, многочисленными разрешениями и проверкой границ экрана. И для мяча и для ракеток значения изменяются от 0 до 1. Для координаты X 0 означает левую границу, а 1 означает правую границу экрана. То же самое и для координаты Y и ракеток; 0 — это наивысшая позиция, а 1 — это низ экрана.Вы также используете вектор скорости для обновления позиции мяча в каждом кадре, о чем мы поговорим чуть позже.

Ракетки просто визуализируются на экране; вы помещаете левую ракетку (красную) у левой стороны и добавляете 5%, чтобы она была вся видна и за ней образовалась небольшая область, при попадании мяча в которую у игрока теряется жизнь. То же самое справедливо и для правой ракетки (синей), которая помещается на 95% (это 0.95f) ширины экрана. Взгляните на изображение, которое вы увидите сейчас после нажатия F5 (рис. 2.6.).


Рис. 2.6

Рис. 2.6


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

Обработка ввода игрока

Как вы уже видели в главе 1, получать входные данные от клавиатуры и игрового пульта в XNA очень просто. Написание дополнительного тестового модуля для этого будет излишним; вы знаете как все это работает и хотите здесь проверить только управление ракетками. Вам не требуется писать новый тестовый модуль, можно использовать тест TestGameSprites, возможно, переименовав его в TestSingleplayerGame. Содержимое тестового модуля остается тем же самым; вы просто изменяете обработку ввода и обновляете позиции ракеток в методе Update класса PongGame:

// Получаем текущие состояния клавиатуры и пульта
gamePad  = GamePad.GetState(PlayerIndex.One);
gamePad2 = GamePad.GetState(PlayerIndex.Two);
keyboard = Keyboard.GetState();

gamePadUp = gamePad.DPad.Up == ButtonState.Pressed ||
            gamePad.ThumbSticks.Left.Y > 0.5f;
gamePadDown = gamePad.DPad.Down == ButtonState.Pressed ||
              gamePad.ThumbSticks.Left.Y < -0.5f;
gamePad2Up = gamePad2.DPad.Up == ButtonState.Pressed ||
             gamePad2.ThumbSticks.Left.Y > 0.5f;
gamePad2Down = gamePad2.DPad.Down == ButtonState.Pressed ||
               gamePad2.ThumbSticks.Left.Y < -0.5f;

// Перемещаемся на половину высоты экрана за секунду
float moveFactorPerSecond = 0.5f *
    (float)gameTime.ElapsedRealTime.TotalMilliseconds / 1000.0f;

// Перемещение вверх или вниз, если нажаты клавиши курсора
// или кнопки пульта
if (gamePadUp || keyboard.IsKeyDown(Keys.Up))
  rightPaddlePosition -= moveFactorPerSecond;
if (gamePadDown || keyboard.IsKeyDown(Keys.Down))
  rightPaddlePosition += moveFactorPerSecond;

// Второй игрок может управляться человеком или компьютером
if (multiplayer)
{
  // Перемешение вверх или вниз, если нажаты
  // клавиши управления или кнопки пульта
  if (gamePad2Up || keyboard.IsKeyDown(Keys.W))
    leftPaddlePosition -= moveFactorPerSecond;
  if (gamePad2Down || keyboard.IsKeyDown(Keys.S))
    leftPaddlePosition += moveFactorPerSecond;
} // if
else
{
  // Позволяем компьютеру следовать за позицией мяча
  float computerChange = ComputerPaddleSpeed * moveFactorPerSecond;
  if (leftPaddlePosition > ballPosition.Y + computerChange)
    leftPaddlePosition -= computerChange;
  else if (leftPaddlePosition < ballPosition.Y - computerChange)
    leftPaddlePosition += computerChange;
} // else

// Проверяем, что позиции ракеток находятся между 0 и 1
if (leftPaddlePosition < 0)
  leftPaddlePosition = 0;
if (leftPaddlePosition > 1)
  leftPaddlePosition = 1;
if (rightPaddlePosition < 0)
  rightPaddlePosition = 0;
if (rightPaddlePosition > 1)
  rightPaddlePosition = 1;

Здесь вы можете заметить несколько переменных (multiplayer, gamePad, gamePad2, keyboard и ComputerPaddleSpeed), но в этом разделе мы сосредоточимся на коде, меняющем местоположение ракеток. Чтобы гарантировать, что мяч и ракетки будут всегда передвигаться с одной и той же скоростью, не зависящей от частоты кадров, вычисляется значение moveFactorPerSecond. Значение moveFactorPerSecond будет 1 для 1 fps (frame per second, кадров в секунду), 0.1 для 10 fps, 0.01 для 100 fps и т.д.

Затем вы меняете местоположение правой ракетки, если нажаты клавиши управления курсором или кнопки игрового пульта. Левая ракетка управляется вторым игроком через второй игровой пульт, если он есть, или клавишами W и S. Если значение multiplayer не равно true, значит мы находимся в однопользовательском режиме и второй ракеткой управляет компьютер; в этом случае она просто следует за мячом, ограниченная значением ComputerPaddleSpeed, равным 0.5. Сначала мяч перемещается достаточно медленно, но его скорость увеличивается при каждом столкновении, а, кроме того, вы можете ударить мяч боковой стороной ракетки, что также увеличивает его скорость. Компьютер перестает успевать следить за мячом и вы можете выиграть.

Чтобы новый метод Update заработал, добавьте новые переменные и константы:

/// <summary>
/// Множитель скорости мяча, определяет какую часть экрана
/// мяч может преодолеть за секунду.
/// </summary>
const float BallSpeedMultiplicator = 0.5f;

/// <summary>
/// Скорость ракетки компьютера. Если мяч перемещается вверх
/// или вниз быстрее, чем ракетка, компьютер не успевает
/// отбить его и вы выигрываете.
/// </summary>
const float ComputerPaddleSpeed = 0.5f;

/// <summary>
/// Режимы игры
/// </summary>
enum GameMode
{
  Menu,
  Game,
  GameOver,
} // enum GameMode

GamePadState gamePad, gamePad2;
KeyboardState keyboard;

bool gamePadUp    = false,
     gamePadDown  = false,
     gamePad2Up   = false,
     gamePad2Down = false;

/// <summary>
/// Играем ли мы в многопользовательскую игру? Если значение false,
/// левой ракеткой управляет компьютер. 
/// </summary>
bool multiplayer = false;

/// <summary>
/// Текущий режим игры. Очень простой ход игры.
/// </summary>
GameMode gameMode = GameMode.Menu;

/// <summary>
/// Выбранный в данный момент элемент меню.
/// </summary>
int currentMenuItem = 0;

Для текущего теста вам нужны только те переменные, которые я упоминал ранее. Однако, взгляните на остальные переменные, которые нужны для игры. BallSpeedMultiplicator определяет как быстро перемещается мяч и, следовательно, насколько скоростной будет игра. Режимы игры используются для обработки всех трех состояний, в которых может находиться игра. Они определяют запущена ли игра только что, находитесь ли вы в меню или уже в самой игре. Когда вы в игре и игрок теряет последнюю жизнь, режим меняется на состояние завершения игры и отображается победитель.

Пока вы не нуждаетесь в этом коде, но это последняя часть обработки входных данных, так что посмотрите на работу с меню:

// Показываем экран в зависимости от текущего режима
if (gameMode == GameMode.Menu)
{
  // Показываем меню
  RenderSprite(menuTexture,
      512-XnaPongLogoRect.Width/2, 150, XnaPongLogoRect);
  RenderSprite(menuTexture,
      512-MenuSingleplayerRect.Width/2, 300, MenuSingleplayerRect,
      currentMenuItem == 0 ? Color.Orange : Color.White);
  RenderSprite(menuTexture,
      512-MenuMultiplayerRect.Width/2, 350, MenuMultiplayerRect,
      currentMenuItem == 1 ? Color.Orange : Color.White);
  RenderSprite(menuTexture,
  512-MenuExitRect.Width/2, 400, MenuExitRect,
  currentMenuItem == 2 ? Color.Orange : Color.White);

  if ((keyboard.IsKeyDown(Keys.Down) ||
       gamePadDown) && remDownPressed == false)
  {
    currentMenuItem = (currentMenuItem + 1)%3;
  } // else if
  else if ((keyboard.IsKeyDown(Keys.Up) ||
    gamePadUp) && remUpPressed == false)
  {
    currentMenuItem = (currentMenuItem + 2) % 3;
  } // else if
  else if ((keyboard.IsKeyDown(Keys.Space) ||
            keyboard.IsKeyDown(Keys.LeftControl) ||
            keyboard.IsKeyDown(Keys.RightControl) ||
            keyboard.IsKeyDown(Keys.Enter) ||
            gamePad.Buttons.A == ButtonState.Pressed ||
            gamePad.Buttons.Start == ButtonState.Pressed ||
            // Back или Escape - выход из игры
            keyboard.IsKeyDown(Keys.Escape) ||
            gamePad.Buttons.Back == ButtonState.Pressed) &&
            remSpaceOrStartPressed == false &&
            remEscOrBackPressed == false)
  {
    // выход из приложения
    if (currentMenuItem == 2 ||
        keyboard.IsKeyDown(Keys.Escape) ||
        gamePad.Buttons.Back == ButtonState.Pressed)
    {
      this.Exit();
    } // if
    else
    {
      // Запуск игры

.. обработка игры и т.д. ...

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

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

Обнаружение столкновений

Чтобы мяч сместился из центра, ваш тестовый модуль TestSingleplayerGame вызывает приведенный ниже метод. Мяч начинает перемещаться в одном из четырех случайно выбираемых направлений:

/// <summary>
/// Запускаем новый мяч в начале каждой игры или после потери мяча.
/// </summary>
public void StartNewBall()
{
  ballPosition = new Vector2(0.5f, 0.5f);
  Random rnd = new Random((int)DateTime.Now.Ticks);
  int direction = rnd.Next(4);
  ballSpeedVector =
      direction == 0 ? new Vector2( 1,  0.8f) :
      direction == 1 ? new Vector2( 1, -0.8f) :
      direction == 2 ? new Vector2(-1,  0.8f) :
      new Vector2(-1, -0.8f);
} // StartNewBall()

В методе Update позиция мяча обновляется на основании значения ballSpeedVector:

// Обновление позиции мяча
ballPosition += ballSpeedVector *
    moveFactorPerSecond * BallSpeedMultiplicator;

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


Рис. 2.7

Рис. 2.7


Для проверки столкновений можно продолжать использовать TestSingleplayerGame, но гораздо проще сконструировать несколько новых тестов, каждый для проверки отдельного типа столкновения. И снова тестовые модули замечательно подходят для такого типа проблем. У вас есть ясная идея о том, что надо сделать, но вы пока еще не знаете как это будет сделано. Просто напишите тестовый модуль, а затем работайте над реализацией:

public static void TestBallCollisions()
{
  StartTest(
    delegate
    {
      // Гарантируем, что мы в однопользовательском режиме игры
      testGame.gameMode = GameMode.Game;
      testGame.multiplayer = false;
      testGame.Window.Title =
        "Xna Pong - Press 1-5 to start collision tests";
      // Запуск конкретной сцены столкновения в зависимости
      // от пользовательского ввода.
      if (testGame.keyboard.IsKeyDown(Keys.D1))
      {
        // Первый тест, простое столкновение с границей экрана
        testGame.ballPosition = new Vector2(0.6f, 0.9f);
        testGame.ballSpeedVector = new Vector2(1, 1);
      } // if
      else if (testGame.keyboard.IsKeyDown(Keys.D2))
      {
        // Второй тест, прямое столкновение с правой ракеткой
        testGame.ballPosition = new Vector2(0.9f, 0.6f);
        testGame.ballSpeedVector = new Vector2(1, 1);
        testGame.rightPaddlePosition = 0.7f;
      } // if
      else if (testGame.keyboard.IsKeyDown(Keys.D3))
      {
        // Третий тест, прямое столкновение с левой ракеткой 
        testGame.ballPosition = new Vector2(0.1f, 0.4f);
        testGame.ballSpeedVector = new Vector2(-1, -0.5f);
        testGame.leftPaddlePosition = 0.35f;
      } // if
      else if (testGame.keyboard.IsKeyDown(Keys.D4))
      {
        // Дополнительный тест для проверки столкновения
        // с краем правой ракетки
        testGame.ballPosition = new Vector2(0.9f, 0.4f);
        testGame.ballSpeedVector = new Vector2(1, -0.5f);
        testGame.rightPaddlePosition = 0.29f;
      } // if
      else if (testGame.keyboard.IsKeyDown(Keys.D5))
      {
        // Дополнительный тест для проверки столкновения
        // с краем правой ракетки
        testGame.ballPosition = new Vector2(0.9f, 0.4f);
        testGame.ballSpeedVector = new Vector2(1, -0.5f);
        testGame.rightPaddlePosition = 0.42f;
      } // if
      // Показываем жизни
      testGame.ShowLives();
      // Мяч в центр
      testGame.RenderBall();
      // Визуализируем обе ракетки
      testGame.RenderPaddles();
    });
} // TestBallCollisions ()

Нажимая клавиши 1 – 5 вы устанавливаете одну из тестовых сцен столкновений. Например, если нажать 1, мяч помещается в точку (0.6, 0.9), находящуюся вблизи середины нижней границы экрана. Скорость мяча устанавливается равной (1, 1), чтобы гарантировать, что он будет перемещаться к границе экрана, от которой мяч должен отразиться, как описано в концепции. Если нажать клавиши 4 или 5 будут запущены дополнительные тесты для ракеток, проверяющие отражение мяча от краев правой ракетки, что требует немного большего объема точной настройки, чем другие более простые проверки столкновений. Обнаружение столкновений выполняется в методе Update класса PongGame.

Теперь можно начать тестирование. Очевидно, если запустить тест сейчас, он не будет работать, поскольку вы пока еще не реализовали никаких проверок столкновений.

Проверка столкновений с верхним и нижним краями экрана является простейшей; весь приведенный ниже код добавляется в метод Update непосредственно перед обновлением местоположения мяча для следующего кадра:

// Проверка для верхней и нижней границ экрана
if (ballPosition.Y < 0 || ballPosition.Y > 1)
{
  ballSpeedVector.Y = -ballSpeedVector.Y;

  // Возвращаем мяч в пространство экрана
  if (ballPosition.Y < 0)
    ballPosition.Y = 0;
  if (ballPosition.Y > 1)
    ballPosition.Y = 1;
} // if

Самая важная часть здесь — это инвертирование составляющей Y вектора скорости мяча. Иногда может оказаться, что в следующем кадре moveFactorPerSecond будет меньше, чем в текущем. Тогда местоположение мяча останется за границами экрана, и вы будете в каждом кадре инвертировать составляющую Y скорости мяча. Чтобы избежать такой ситуации, вы добавляете код, гарантирующий, что местоположение мяча будет всегда находиться внутри экранной области. Та же самая подстройка выполняется и для ракеток. Если вы хотите проверить обнаружение столкновений с верхней и нижней границами экрана, можете запустить тест сейчас, нажав F5.

Обнаружение столкновений с ракетками немного сложнее. При обнаружении столкновений с ракеткой конструируются ограничивающие параллелепипеды, используемые при проверке пересечений, и представляемые в XNA классом BoundingBox. Структура BoundingBox использует структуры Vector3 и работает в трехмерном пространстве, но вы можете игнорировать значение Z, всегда присваивая ему 0, и все прекрасно подойдет для работы в двухмерном пространстве игры Pong:

// Обнаружение столкновений с ракеткой

// Создание ограничивающих параллелепипедов для использования
// во вспомогательном методе обнаружения пересечений.
Vector2 ballSize = new Vector2(GameBallRect.Width / 1024.0f,
                               GameBallRect.Height / 768.0f);
BoundingBox ballBox = new BoundingBox(
      new Vector3(ballPosition.X - ballSize.X / 2,
                  ballPosition.Y - ballSize.Y / 2, 0),
      new Vector3(ballPosition.X + ballSize.X / 2,
                  ballPosition.Y + ballSize.Y / 2, 0));
Vector2 paddleSize = new Vector2(GameRedPaddleRect.Width / 1024.0f,
                                 GameRedPaddleRect.Height / 768.0f);
BoundingBox leftPaddleBox = new BoundingBox(
      new Vector3(-paddleSize.X/2,
                  leftPaddlePosition-paddleSize.Y/2, 0),
      new Vector3(+paddleSize.X/2,
                  leftPaddlePosition+paddleSize.Y/2, 0));
BoundingBox rightPaddleBox = new BoundingBox(
      new Vector3(1-paddleSize.X/2,
                  rightPaddlePosition-paddleSize.Y/2, 0),
      new Vector3(1+paddleSize.X/2,
                  rightPaddlePosition+paddleSize.Y/2, 0));

// Мяч столкнулся с левой ракеткой?
if (ballBox.Intersects(leftPaddleBox))
{
  // Отражение от ракетки
  ballSpeedVector.X = -ballSpeedVector.X;

  // Небольшое увеличение скорости
  ballSpeedVector *= 1.05f;

  // Не столкнулись ли мы с краем ракетки?
  if (ballBox.Intersects(new BoundingBox(
      new Vector3(leftPaddleBox.Min.X - 0.01f,
                  leftPaddleBox.Min.Y - 0.01f, 0),
      new Vector3(leftPaddleBox.Min.X + 0.01f,
                  leftPaddleBox.Min.Y + 0.01f, 0))))
    // Отражение под более сложным для другого игрока углом
    ballSpeedVector.Y = -2;
  else if (ballBox.Intersects(new BoundingBox(
      new Vector3(leftPaddleBox.Min.X - 0.01f,
                  leftPaddleBox.Max.Y - 0.01f, 0),
      new Vector3(leftPaddleBox.Min.X + 0.01f,
                  leftPaddleBox.Max.Y + 0.01f, 0))))
    // Отражение под более сложным для другого игрока углом
    ballSpeedVector.Y = +2;

  // Перемещение от ракетки
  ballPosition.X += moveFactorPerSecond * BallSpeedMultiplicator;
} // if

Код создания ограничивающих параллелепипедов очень похож на код визуализации в RenderBall и RenderPaddles. Код обнаружения столкновения с краями слегка усложняет все, но это лишь только быстрый и грязный способ увеличить скорость мяча, если вы попали по нему краем ракетки. Эти несколько строк делают игру более интересной.

Практически такой же код используется и для правой ракетки; вы только заменяете все переменные левой ракетки на переменные правой ракетки и меняете знаки в коде перемещения от ракетки.

Заключительная вещь, которую надо сделать, чтобы ваша игра обрабатывала все возможные ситуации — последняя проверка столкновений для обнаружения ситуации, когда мяч оказывается позади ракетки и игрок теряет жизнь. Код для этого очень прост. Вы можете непосредственно обрабатывать ситуацию, когда игрок теряет все жизни и игра заканчивается. Отображение текста Red Won или Blue Won выполняется в методе Draw. Если пользователь нажимает пробел или Esc, он возвращается в главное меню и игра начинается заново.

// Мяч потерян?
if (ballPosition.X < -0.065f)
{
  // Воспроизводим звук
  soundBank.PlayCue("PongBallLost");

  // Уменьшаем количество жизней
  leftPlayerLives--;

  // Запускаем новый мяч
  StartNewBall();
} // if
else if (ballPosition.X > 1.065f)
{
  // Воспроизводим звук
  soundBank.PlayCue("PongBallLost");

  // Уменьшаем количество жизней
  rightPlayerLives--;

  // Запускаем новый мяч
  StartNewBall();
} // if

// Если у какого-нибудь игрока не осталось жизней,
// другой игрок выиграл!
if (gameMode == GameMode.Game &&
    (leftPlayerLives == 0 ||
     rightPlayerLives == 0))
{
  gameMode = GameMode.GameOver;
  StopBall();
} // if

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


Рис. 2.8

Рис. 2.8


Добавление звука

Обычно для добавления звука к вашей игре вы собирали несколько WAV-файлов, переносили их в ваш проект, а затем воспроизводили. В XNA загрузка WAV-файлов не поддерживается по той причине, что Windows и Xbox 360 используют различные форматы для звуков и музыки. Чтобы преодолеть эту проблему Microsoft изобрела инструментальные средства XACT, которые уже некоторое время доступны в DirectX SDK и Xbox 360 SDK. ХАСТ это сокращение для «Кросс-платформенные утилиты создания аудиосопровождения Microsoft» (Microsoft Cross-Platform Audio Creation Tool). XNA также использует эти инструментальные средства и Microsoft решила сделать их единственным способом для воспроизведения звуковых файлов.

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

Чтобы добавить эти файлы к вашей игре, вам необходимо создать новый проект XACT. Чтобы запустить XACT щелкните по кнопке Start (Пуск) и выберите пункт меню All Programs (Все программы) | Microsoft XNA Game Studio Express | Tools.

В новом проекте XACT добаьте волновой банк, выбрав пункт Create Wave Bank в меню Wave Banks, а затем добавьте звуковой банк, выбрав команду Create Sound Bank в меню Sound Banks. Теперь перетащите два файла .wav в новый волновой банк XACT (рис. 2.9).


Рис. 2.9

Рис. 2.9


Перетащите два новых элемента волновых банков в звуковой банк, а затем перетащите их в реплики. Если вы запутались или столкнулись с проблемами, обратитесь за дополнительной информацией к главе 9.

Вот основные элементы, используемые в XACT для звуков:

На рис. 2.10 показано, как ваш проект XACT должен выглядеть теперь. Вы устанавливаете громкость для PongBallHit равной –8, а для PongBallLost — равной –4; значение по умолчанию, –12, создает слишком тихий звук, а немного меньшая громкость у звука удара кажется более подходящей для игры. У всех остальных параметров значения по умолчанию не меняются, так что вам достаточно сохранить проект как PongSound.xap. Затем добавьте этот файл в ваш проект в XNA Studio и он будет использовать конвейер содержимого XNA для ватоматической компиляции и построения всех файлов, необходимых как для Windows, так и для Xbox 360. Убедитесь, что два файла .wav находятся в том же самом каталоге, что и файл PongSound.xap, иначе конвейер содержимого может столкнуться с проблемами при поиске этих файлов и не сможет построить ваш проект XACT.


Рис. 2.10

Рис. 2.10


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

public static void TestSounds()
{
  StartTest(
    delegate
    {
      if (testGame.keyboard.IsKeyDown(Keys.Space))
        testGame.soundBank.PlayCue("PongBallHit");
      if (testGame.keyboard.IsKeyDown(Keys.LeftControl))
        testGame.soundBank.PlayCue("PongBallLost");
    });
} // TestSounds()

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

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

// Мяч потерян?
if (ballPosition.X < -0.065f)
{
  // Воспроизводим звук
  soundBank.PlayCue("PongBallLost");

  // Уменьшаем количество жизней
  leftPlayerLives--;

  // Запускаем новый мяч
  StartNewBall();
} // if
else if (ballPosition.X > 1.065f)
{
  // Воспроизводим звук
  soundBank.PlayCue("PongBallLost");

  // Уменьшаем количество жизней
  rightPlayerLives--;

  // Запускаем новый мяч
  StartNewBall();
} // if

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


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz