| 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 | < Назад | Оглавление | Далее > |