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

Игра Breakout

Ладно, в этой главе мы много говорили о вспомогательных классах и, наконец, пришло время использовать их в работе. Сейчас я пропущу этап разработки концепции и просто скажу, что Breakout — это вариация Pong для одного игрока, играющего против стены из блоков. Игру Breakout создали Нолан Башнелл и Стив Возняк и выпустили ее в Atari в 1976 году. В первоначальном варианте это была черно-белая игра, подобно Pong, но чтобы сделать ее более захватывающей на экране были размещены прозрачные полосы для раскрашивания блоков (рис. 3.13).


Рис. 3.13

Рис. 3.13


Мы пойдем проторенным путем, повторно используя некоторые компоненты игры Pong, а также вспомогательные классы, о которых узнали в этой главе. Breakout более сложная игра, чем Pong; в ней может быть много уровней и большое количество улучшений. Например, Arkanoid, клон Breakout, как и многие игры, выпущенные в 1980 – 1990 годах, использует ту же самую базовую идею игры и добавляет оружие, улучшенные графические эффекты и множество уровней с различным расположением блоков.

Как видно на рис. 3.14, класс BreakoutGame структурирован таким же образом, как класс PongGame из предыдущей главы. Отсутствует обработка спрайтов, поскольку теперь она выполняется с помощью класса SpriteHelper. ряд других внутренних методов и вызовов также заменены на обращение к вспомогательным классам. Например, StartLevel случайным образом генерирует новый уровень на основании его номера, и для генерации случайных значений в нем используется класс RandomHelper.


Рис. 3.14

Рис. 3.14


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

Взгляните на рис. 3.15 для быстрого обзора игры Breakout, разработкой которой мы будем заниматься на нескольких последующих страницах. Она интереснее и лучше приспособлена к повторной игре, чем Pong, которая становится интересной только если играют два человека. Игра Breakout использует ту же фоновую текстуру и два звуковых файла из проекта Pong, но также добавляет новую текстуру (BreakoutGame.png) для ракетки, мяча и блоков, и новые звуки для завершения уровня (BreakoutVictory.wav) и для уничтожения блока (BreakoutBlockKill.wav).


Рис. 3.15

Рис. 3.15


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

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

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

Уровни Breakout

Поскольку вы используете много идей из Pong, пропустим похожий или полностью идентичный код. Сосредоточимся сейчас на новых переменных:

/// <summary>
/// Сколько строк и столбцов блоков отображать:
/// </summary>
const int NumOfColumns = 14,
          NumOfRows    = 12;

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

/// <summary>
/// Уровень где мы и текущий счет.
/// </summary>
int level = 0, score = -1;

/// <summary>
/// Все блоки текущего игрового поля. Если все они очищены,
/// мы переходим на следующий уровень.
/// </summary>
bool[,] blocks = new bool[NumOfColumns, NumOfRows];

/// <summary>
/// Местоположение для каждого имеющегося блока,
/// инициализируется в Initialize().
/// </summary>
Vector2[,] blockPositions = new Vector2[NumOfColumns, NumOfRows];

/// <summary>
/// Ограничивающие прямоугольники для каждого блока,
/// также вычисляются заранее и в каждом кадре проверяется
/// не столкнулся ли мяч с каким-нибудь блоком.
/// </summary>
BoundingBox[,] blockBoxes = new BoundingBox[NumOfColumns, NumOfRows];

Сперва вы определяете максимально возможное количество столбцов и строк блоков; на первом уровне будут заполнены не все линии, а только 10% блоков. Местоположение ракетки также чуть проще, чем в Pong, поскольку здесь у нас только один игрок. Затем вы сохраняете текущий уровень и счет — это новый код. В Pong у каждого игрока есть три мяча и игра заканчивается, когда все они потеряны. В Breakout игрок начинает с уровня 1 и продвигается вперед, пока не потеряет мяч. Здесь нет таблицы рекордов или игрового шрифта, поэтому номер уровня и счет отображаются в заголовке окна.

Затем определяются все блоки; наиболее важный массив — это blocks, который сообщает вам, какие блоки используются в данный момент. Массив block инициализируется перед началом каждого уровня, в то время как blockPositions и blockBoxes инициализируются только один раз в конструкторе игры; blockPositions используется для определения позиции центра каждого блока, необходимой при визуализации, а blockBoxes определяет ограничивающие прямоугольники блоков для обнаружения столкновений. Важно отметить, что ни один из этих списков не использует экранные координаты. Все значения находятся в диапазоне от 0 до 1: 0 — это лево или верх, а 1 — это право или низ. Этот способ делает игру независимой от разрешения и упрощает как визуализацию, так и обнаружение столкновений.

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

void StartLevel()
{
  // Случайные, постерпенно усложняющиеся уровни
  for (int y = 0; y < NumOfRows; y++)
    for (int x = 0; x < NumOfColumns; x++)
      blocks[x, y] =
        RandomHelper.GetRandomInt(10) < level + 1;
  // Используем нижние блоки только на высоких уровнях
  if (level < 6)
    for (int x = 0; x < NumOfColumns; x++)
      blocks[x, NumOfRows - 1] = false;
  if (level < 4)
    for (int x = 0; x < NumOfColumns; x++)
      blocks[x, NumOfRows - 2] = false;
  if (level < 2)
    for (int x = 0; x < NumOfColumns; x++)
      blocks[x, NumOfRows - 3] = false;

  // Остановка игры
  ballSpeedVector = Vector2.Zero;

  // Ждем, пока игрок не нажмет пробел или А
  // для начала уровня.
  pressSpaceToStart = true;

  // Обновление заголовка
  Window.Title =
    "XnaBreakout - Level " + (level+1) +
    " - Score " + Math.Max(0, score);
} // StartLevel

В цикле for вы просто заполняете массив блоков новыми значениями в зависимости от уровня. Для первого уровня значение level равно 0 и будут заполнены только 10% блоков. RandomHelper.GetRandomInt(10) возвращает значение от 0 до 9, которое будет меньше 1 только в 10% случаев. На уровне 2 будет заполнено 20% и так до уровня 10 и последующих, где будет заполнено 100%. В игре нет ограничений; можете играть столько, сколько хотите.

Затем, для начальных уровней очищаются три нижние линии блоков, чтобы сделать эти уровни проще. Начиная с уровня 3 очищаются только две линии, начиная с уровня 5 — одна линия, а когда вы достигнете уровня 7 будут заполняться все линии.

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

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

Игровой цикл

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

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

// Игра еще не запущена? Помещаем мяч на ракетку.
if (pressSpaceToStart)
{
  ballPosition = new Vector2(paddlePosition, 0.95f - 0.035f);
  
  // Обрабатываем нажатие пробела
  if (keyboard.IsKeyDown(Keys.Space) ||
      gamePad.Buttons.A == ButtonState.Pressed)
  {
    StartNewBall();
  } // if
} // if
else
{
  // Обнаружение столкновений
  CheckBallCollisions(moveFactorPerSecond);
  
  // Обновляем местоположение мяча и отражаем от границ
  ballPosition += ballSpeedVector *
       moveFactorPerSecond * BallSpeedMultiplicator;
    
  // Мяч потерян?
  if (ballPosition.Y > 0.985f)
  {
    // Воспроизводим звук
    soundBank.PlayCue("PongBallLost");
    // Игра окончена, возвращаемся на уровень 0
    level = 0;
    StartLevel();
    // Отображаем сообщение
    lostGame = true;
  } // if
  
  // Проверяем. Все ли блоки уничтожены и уровень завершен
  bool allBlocksKilled = true;
  for (int y = 0; y < NumOfRows; y++)
    for (int x = 0; x < NumOfColumns; x++)
      if (blocks[x, y])
      {
        allBlocksKilled = false;
          break;
      } // for for if
      
  // Мы выиграли, переходим на следующий уровень
  if (allBlocksKilled == true)
  {
    // Воспроизводим звук
    soundBank.PlayCue("BreakoutVictory");
    lostGame = false;
    level++;
    StartLevel();
  } // if
} // else

Сперва вы проверяете, запущен ли мяч. Если нет, позиция мяча обновляется и он помещается в центр ракетки игрока. Затем ожидается нажатие пробела или A и мяч запускается (просто формируется случайный ballSpeedVector и мяч отражается от ракетки по направлению к стене блоков).

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

И, наконец, вы проверяете не были ли уничтожены все блоки и завершен уровень. Если все блоки уничтожены вы воспроизводите победный звук и начинаете новый уровень. Игрок видит сообщение «You Won!» на экране (см. метод Draw) и должен нажать пробел для начала нового уровня.

Рисование Breakout

Благодаря классу SpriteHelper, метод Draw в игре Breakout получился коротким и простым:

protected override void Draw(GameTime gameTime)
{
  // Визуализируем фон
  background.Render();
  SpriteHelper.DrawSprites(width, height);

  // Визуализируем всю игровую графику
  paddle.RenderCentered(paddlePosition, 0.95f);
  ball.RenderCentered(ballPosition);
  // Визуализируем все блоки
  for (int y = 0; y < NumOfRows; y++)
    for (int x = 0; x < NumOfColumns; x++)
      if (blocks[x, y])
        block.RenderCentered(blockPositions[x, y]);

  if (pressSpaceToStart &&
      score >= 0)
  {
    if (lostGame)
      youLost.RenderCentered(0.5f, 0.65f, 2);
    else
      youWon.RenderCentered(0.5f, 0.65f, 2);
  } // if

  // Рисуем все спрайты на экране
  SpriteHelper.DrawSprites(width, height);

  base.Draw(gameTime);
} // Draw(gameTime)

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

Затем вы рисуете ракетку и мяч, что очень легко сделать благодаря вспомогательному методу RenderCentered из класса SpriteHelper, который работает так (три перегрузки предназначены только для большего удобства использования этого метода):

public void RenderCentered(float x, float y, float scale)
{
  Render(new Rectangle(
    (int)(x * 1024 - scale * gfxRect.Width/2),
    (int)(y * 768 - scale * gfxRect.Height/2),
    (int)(scale * gfxRect.Width),
    (int)(scale * gfxRect.Height)));
} // RenderCentered(x, y)

public void RenderCentered(float x, float y)
{
  RenderCentered(x, y, 1);
} // RenderCentered(x, y)

public void RenderCentered(Vector2 pos)
{
  RenderCentered(pos.X, pos.Y);
} // RenderCentered(pos)

RenderCentered получает Vector2 или значения с плавающей точкой x и y, и преобразует местоположение в диапазоне от 0 до 1 (формат используемый в игре) в экранные координаты для разрешения 1024 × 768. Метод Draw из SpriteHelper затем масштабирует все из формата 1024 × 768 в соответствии с реальным текущим разрешением экрана. Это может звучать сложно, но на самом деле легко для использования.

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

// Инициализация всех блоков, установка местоположения
// и ограничивающих прямоугольников.
for (int y = 0; y < NumOfRows; y++)
  for (int x = 0; x < NumOfColumns; x++)
  {
    blockPositions[x, y] = new Vector2(
          0.05f + 0.9f * x / (float)(NumOfColumns - 1),
          0.066f + 0.5f * y / (float)(NumOfRows - 1));
    Vector3 pos = new Vector3(blockPositions[x, y], 0);
    Vector3 blockSize = new Vector3(
          GameBlockRect.X/1024.0f, GameBlockRect.Y/768, 0);
    blockBoxes[x, y] = new BoundingBox(
          pos - blockSize/2, pos + blockSize/2);
} // for for

Ограничивающие прямоугольники blockBoxes используются для проверки столкновений, которую мы обсудим через секунду. В вычислении местоположения также нет ничего сложного; координата x меняется от 0.05 до 0.95 с количеством приращений, равным количеству столбцов (14, если я правильно помню). Можете попробовать изменить значение константы NumOfColumns на 20 и на поле будет больше блоков.

И, наконец, в тех случаях когда игрок завершает уровень или проигрывает игру, на экране визуализируется небольшое сообщение об этом с коэффициентом масштабирования 2. Затем вы просто вызываете метод Draw класса SpriteHelper для визуализации всех игровых элементов на экране. Посмотрите тестовые модули игры, чтобы увидеть как разрабатывались визуализация блоков, ракеток и игровых сообщений. Я снова начинал с тестовых модулей, а затем писал реализацию.

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

Проверка столкновений для игры Breakout немного сложнее, чем простая проверка ракеток и границ экрана в Pong. Более сложной частью является корректное отражение от блоков, в которые попадает мяч. Чтобы просмотреть полный код, обратитесь к исходным кодам к этой главе.

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


Рис. 3.16

Рис. 3.16


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

// Мяч попал по какому-нибудь блоку?
for (int y = 0; y < NumOfRows; y++)
  for (int x = 0; x < NumOfColumns; x++)
    if (blocks[x, y])
    {
      // Проверка столкновения
      if (ballBox.Intersects(blockBoxes[x, y]))
      {
        // Убиваем блок
        blocks[x, y] = false;

        // Увеличиваем счет
        score++;

        // Обновляем заголовок
        Window.Title =
              "XnaBreakout - Level " + (level + 1) +
              " - Score " + score;

        // Воспроизводим звук
        soundBank.PlayCue("BreakoutBlockKill");

        // Отражаем мяч обратно
        ballSpeedVector = -ballSpeedVector;

        // Прерываемся, обрабатываем за раз только 1 блок
        break;
      } // if
    } // for for if

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

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