netlib.narod.ru | < Назад | Оглавление | Далее > |
Ладно, в этой главе мы много говорили о вспомогательных классах и, наконец, пришло время использовать их в работе. Сейчас я пропущу этап разработки концепции и просто скажу, что Breakout — это вариация Pong для одного игрока, играющего против стены из блоков. Игру Breakout создали Нолан Башнелл и Стив Возняк и выпустили ее в Atari в 1976 году. В первоначальном варианте это была черно-белая игра, подобно Pong, но чтобы сделать ее более захватывающей на экране были размещены прозрачные полосы для раскрашивания блоков (рис. 3.13).
Рис. 3.13 |
Мы пойдем проторенным путем, повторно используя некоторые компоненты игры Pong, а также вспомогательные классы, о которых узнали в этой главе. Breakout более сложная игра, чем Pong; в ней может быть много уровней и большое количество улучшений. Например, Arkanoid, клон Breakout, как и многие игры, выпущенные в 1980 – 1990 годах, использует ту же самую базовую идею игры и добавляет оружие, улучшенные графические эффекты и множество уровней с различным расположением блоков.
Как видно на рис. 3.14, класс BreakoutGame структурирован таким же образом, как класс PongGame из предыдущей главы. Отсутствует обработка спрайтов, поскольку теперь она выполняется с помощью класса SpriteHelper. ряд других внутренних методов и вызовов также заменены на обращение к вспомогательным классам. Например, StartLevel случайным образом генерирует новый уровень на основании его номера, и для генерации случайных значений в нем используется класс RandomHelper.
Рис. 3.14 |
Обратите внимание на множество тестовых методов в классе. В следующей главе ситуация будет улучшена, путем введения классов BaseGame и TestGame, которые делают управление классом игры и в особенности тестирование модулей более легким и более организованным.
Взгляните на рис. 3.15 для быстрого обзора игры Breakout, разработкой которой мы будем заниматься на нескольких последующих страницах. Она интереснее и лучше приспособлена к повторной игре, чем Pong, которая становится интересной только если играют два человека. Игра Breakout использует ту же фоновую текстуру и два звуковых файла из проекта Pong, но также добавляет новую текстуру (BreakoutGame.png) для ракетки, мяча и блоков, и новые звуки для завершения уровня (BreakoutVictory.wav) и для уничтожения блока (BreakoutBlockKill.wav).
Рис. 3.15 |
Прежде чем начать копировать код из предыдущего проекта, использовать ваши новые вспомогательные классы и рисовать новые игровые элементы, вы должны подумать об игре и том, какие проблемы могут возникнуть. Уверен, вы сможете пойти вперед и реализовать игру, но будет достаточно сложно, например, проверить столкновения, являющиеся самой трудной частью этой игры. Тестовые модули выручат вас и позволят, как минимум, проверить все основные части вашей игры, также помогая организовать ваш код и вынуждая программировать только то, что реально необходимо. Как всегда начнем с наиболее очевидных частей игры и тестовых модулей для них, добавляя затем дополнительные тесты модулей, пока все не будет сделано, и в заключение объединим все вместе и протестируем финальную игру.
Вот краткий обзор тестов модулей игры Breakout; за дополнительной информацией обращайтесь к исходным кодам к этой главе. У вас еще нет класса TestGame, так что продолжим использовать тот же вид тестирования модулей, что и в прошлой главе. Лучший способ выполнения статических тестов модулей будет показан в следующей главе. У вас только три тестовых модуля, но они много используются и меняются по ходу разработки игры.
TestSounds — Быстрый тест для проверки всех новых звуков в вашем проекте. Нажимайте пробел, Alt, Ctrl или Shift для воспроизведения звуков. Я также добавил небольшую паузу после воспроизведения, чтобы отдельные звуки было проще различать. Этот тест используется для проверки нового проекта XACT, который я создал для этой игры.
TestGameSprites — Этот тест первоначально использовался для проверки класса SpriteHelper, но затем весь код был перемещен в метод Draw игры. Тест также использовался для инициализации всех блоков в игре; код был перемещен в конструктор, который будет показан далее. Данный тест показывает, что не обязательно в конце иметь сложные тесты, поскольку к этому моменту в нем только четыре строчки кода, но важной частью является упрощение жизни во время кодирования игры. Копируйте полезные части тестовых модулей в ваш код так часто, как считаете нужным. Статические тестовые модули не должны оставаться неприкосновенными, подобно динамическим тестам вспомогательных классов, потому что вы используете их только для построения и тестирования вашей игры. Когда игра работает, вам больше не нужны статические тестовые модули, кроме как для тестирования более поздних частей игры.
TestBallCollisions — Точно так же, как в предыдущей главе, проверка столкновений мяча — это самый полезный тестовый модуль. Здесь вы проверяете, происходит ли столкновение с границами экрана и ракеткой так, как ожидалось. Чтобы все заработало потребуется несколько незначительных изменений. Теперь вам потребуется более сложный код обнаружения столкновений с блоками, который мы подробно обсудим чуть позже. Если хотите, можете подумать о других способах обнаружения столкновений и усовершенствовать игру. Например, имеет смысл поместить мяч в ловушку позади стены блоков и посмотреть, будут ли правильно разрушены все блоки.
Поскольку вы используете много идей из 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) и должен нажать пробел для начала нового уровня.
Благодаря классу 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 |
Давайте поближе взглянем на основу кода обнаружения столкновений с блоками. Обнаружение столкновений с границами экрана и ракеткой выполняется также, как в игре 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 | < Назад | Оглавление | Далее > |