netlib.narod.ru | < Назад | Оглавление | Далее > |
Хватит обсуждать вспомогательные классы и игровые компоненты. Пришло время написать еще одну замечательную игру. Благодаря множеству классов, доступных в небольшом игровом движке, теперь проще писать текст на экране, рисовать спрайты, обрабатывать ввод и воспроизводить звуки.
Перед тем, как погрузиться в детали логики игры Тетрис, было бы полезно подумать о размещении всех игровых элементов, также как мы делали в предыдущих играх. Вместо того, чтобы рисовать на экране все игровые элементы, вы лишь отображаете фоновые прямоугольники, показывающие где что будет выводиться. В качестве фона вы опять используете изображение космоса (клянусь, это в последний раз). Фоновые прямоугольники — это новая текстура, существующая в двух видах (рис. 4.7). Это сделано для того, чтобы разделить игровые компоненты и улучшить визуальное оформление на экране. Вы можете просто использовать один и тот же прямоугольник для обоих частей игры, но поскольку их размеры значительно отличаются, будет плохо выглядеть либо фоновый прямоугольник основного поля, либо меньший по размерам фоновый прямоугольник дополнительных игровых компонентов, которому также нужна графика фонового прямоугольника, но меньших размеров.
Рис. 4.7 |
Для визуализации эих прямоугольников на экране вы снова используете класс SpriteHelper и проверяете все с помощью следующего тестового модуля:
public static void TestBackgroundBoxes() { TestGame.Start("TestBackgroundBoxes", delegate { // Визуализация фона TestGame.game.background.Render(); // Рисование фоновых прямоугольников для всех компонентов TestGame.game.backgroundBigBox.Render(new Rectangle( (512 - 200) - 15, 40 - 12, 400 + 23, (768 - 40) + 16)); TestGame.game.backgroundSmallBox.Render(new Rectangle( (512 - 480) - 15, 40 - 10, 290 - 30, 300)); TestGame.game.backgroundSmallBox.Render(new Rectangle( (512 + 240) - 15, 40 - 10, 290 - 30, 190)); }); } // TestBackgroundBoxes()
Результатом работы тестового модуля является экран, показанный на рис. 4.8.
Рис. 4.8 |
Вы можете спросить, почему правый прямоугольник меньше и откуда я взял все эти значения. Я просто начал с некоторых произвольных значений и менял их, пока они не стали подходить к окончательному варианту игры. Сначала в тестовом модуле рисуется фон, поскольку вы не можете вызвать метод Draw класса TetrisGame, находясь в тестовом модуле (иначе этот тестовый модуль перестанет работать позже, когда игра будет полностью реализована).
Затем рисуются три прямоугольника. Верхний левый прямоугольник используется для отображения следующего блока. Центральный прямоугольник показывает сетку игры Тетрис. И, наконец, верхний правый прямоугольник показывает табло со счетом. Вы видели тестовый модуль для него ранее.
Пришло время заполнить эти прямоугольники содержимым. Начнем с главного компонента: TetrisGrid. Этот класс отвечает за отображение всей сетки игры Тетрис. Он обрабатывает ввод, перемещает падающий блок и показывает все имеющиеся данные. Вы уже видели, какие методы используются в классе TetrisGrid, когда обсуждали игровые компоненты. Перед визуализацией сетки вы должны проверить первые константы, определенные в классе TetrisGrid:
#region Константы public const int GridWidth = 12; public const int GridHeight = 20; ...
Есть пара более интересных констант, но сейчас вам нужны только размеры сетки. Итак, у вас есть поле игры Тетрис из 12 столбцов и 20 строк. С помощью текстуры Block.png, которая является простым квадратным блоком, теперь вы легко нарисуете полную сетку в методе Draw:
// Вычисляем размеры блока и т.д. int blockWidth = gridRect.Width / GridWidth; int blockHeight = gridRect.Height / GridHeight; for (int x = 0; x < GridWidth; x++) for (int y = 0; y < GridHeight; y++) { game.BlockSprite.Render(new Rectangle( gridRect.X + x * blockWidth, gridRect.Y + y * blockHeight, blockWidth-1, blockHeight-1), new Color(60, 60, 60, 128)); // Пустой цвет } // for for
Переменная GridRect передается из главного класса в метод Draw как параметр и задает размер области, в которой вы хотите нарисовать сетку. Это тот же самый прямоугольник, который вы использовали для фона, чуть уменьшенный для точного соответствия. Первая вещь, которую вы должны сделать здесь, — вычислить ширину и высоту каждого рисуемого блока. Затем вы проходите по всему массиву и рисуете каждый блок с помощью метода SpriteHelper.Render, используя полупрозрачный темный цвет для отображения пустой фоновой решетки. На рис. 4.9 показано, как это выглядит. Поскольку вы используете игровые компоненты, весь этот код не помещается в тестовый модуль. Тестовый модуль просто рисует фоновый прямоугольник, а затем вызывает метод TetrisGrid.Draw для отображения результата (см. тестовый модуль TestEmptyGrid).
Рис. 4.9 |
Перед тем, как визуализировать что-нибудь полезное на нашей новой сетке, подумаем о типах блоков, которые будут в нашей игре. В стандартной игре Тетрис есть семь типов блоков; все они состоят из четырех маленьких квадратов, соединенных между собою (рис. 4.10). Самый любимый тип блока, это, конечно, линия (или палка), поскольку она может уничтожить до четырех линий, принося вам максимальное количество очков.
Рис. 4.10 |
Эти типы блоков должны быть определены в классе TetrisGrid. Один из способов сделать это — воспользоваться перечислением, содержащим все возможные типы блоков. Это перечисление должно также содержать и тип для пустого блока, позволяющий вам использовать данную структуру данных также и для всей сетки, поскольку каждый блок сетки может либо содержать часть блока предопределенного типа, либо быть пустым. Взгляните на оставшиеся константы класса TetrisGrid:
/// <summary> /// Типы блоков для каждого нового падающего блока /// </summary> public enum BlockTypes { Empty, Block, Triangle, Line, RightT, LeftT, RightShape, LeftShape, } // enum BlockTypes /// <summary> /// Количество типов блоков, которые мы можем использовать /// для каждого блока сетки /// </summary> public static readonly int NumOfBlockTypes = EnumHelper.GetSize(typeof(BlockTypes)); /// <summary> /// Цвета блоков для каждого типа блока /// </summary> public static readonly Color[] BlockColor = new Color[] { new Color( 60, 60, 60, 128 ), // Empty, цвет не используется new Color( 50, 50, 255, 255 ), // Line, синий new Color( 160, 160, 160, 255 ), // Block, серый new Color( 255, 50, 50, 255 ), // RightT, красный new Color( 255, 255, 50, 255 ), // LeftT, желтый new Color( 50, 255, 255, 255 ), // RightShape, зеленовато-голубой new Color( 255, 50, 255, 255 ), // LeftShape, пурпурный new Color( 50, 255, 50, 255 ), // Triangle, зеленый }; // Color[] BlockColor /// <summary> /// Неповернутые фигуры /// </summary> public static readonly int[][,] BlockTypeShapesNormal = new int[][,] { // Empty new int[,] { { 0 } }, // Line new int[,] { { 0, 1, 0 }, { 0, 1, 0 }, { 0, 1, 0 }, { 0, 1, 0 } }, // Block new int[,] { { 1, 1 }, { 1, 1 } }, // RightT new int[,] { { 1, 1 }, { 1, 0 }, { 1, 0 } }, // LeftT new int[,] { { 1, 1 }, { 0, 1 }, { 0, 1 } }, // RightShape new int[,] { { 0, 1, 1 }, { 1, 1, 0 } }, // LeftShape new int[,] { { 1, 1, 0 }, { 0, 1, 1 } }, // Triangle new int[,] { { 0, 1, 0 }, { 1, 1, 1 }, { 0, 0, 0 } }, }; // BlockTypeShapesNormal
BlockType — это перечисление, о котором мы говорим; оно содержит все возможные типы блоков и также используется для случайной генерации нового блока в игровом компоненте NextBlock. Первоначально все поле сетки заполнено пустыми блоками. Сетка определена так:
/// <summary> /// Реальная сетка, содержащая все блоки, включая текущий падающий блок. /// </summary> BlockTypes[,] grid = new BlockTypes[GridWidth, GridHeight];
Между прочим, numOfBlockTypes показывает вам полнофункциональность класса перечисления. Вы можете легко определить, сколько элементов в перечислении BlockTypes.
Затем определяется цвет для каждого типа блоков. Эти цвета используются для предварительного просмотра в NextBlock и для визуализации всей сетки. У каждого квадрата сетки есть тип блока и вы можете легко использовать BlockColors для преобразования члена перечисления в целое число, которое можно использовать в методе Draw:
BlockColor[(int)grid[x,y]]
И, наконец, определяются формы блоков, что выглядит чуть более сложно, особенно если учесть, что вы должны позволить этим блокам вращаться. Это выполняется с помощью BlockTypeShapes, большого массива, хранящего все возможные типы блоков во всех возможных позициях, вычисляемого в конструкторе TetrisGrid.
Чтобы добавить новый блок к сетке игры Тетрис, вы просто добавляете в сетку каждую из частей блока, что делает метод AddRandomBlock. Вы сохраняете отдельный список с именем floatingGrid, запоминающий, какие части сетки должны быть перемещены вниз (смотри следующий раздел «Гравитация», вы просто позволяете всему падать) при каждом вызове Update:
// Случайный тип и поворот блока currentBlockType = (int)nextBlock.SetNewRandomBlock(); currentBlockRot = RandomHelper.GetRandomInt(4); // Получаем предварительно вычисленную форму int[,] shape = BlockTypeShapes[currentBlockType,currentBlockRot]; int xPos = GridWidth/2-shape.GetLength(0)/2; // Центрируем блок в самой верхней позиции сетки currentBlockPos = new Point(xPos, 0); // Добавляем новый блок for (int x = 0; x < shape.GetLength(0); x++) for (int y = 0; y < shape.GetLength(1); y++) if ( shape[x,y] > 0 ) { // Проверяем, перекрыл ли он что-нибудь if (grid[x + xPos, y] != BlockTypes.Empty) { // В этом случае игра закончена! gameOver = true; Sound.Play(Sound.Sounds.Lose); } // if else { grid[x + xPos, y] = (BlockTypes)currentBlockType; floatingGrid[x + xPos, y] = true; } // else } // for for if
Сперва вы определяете, какой тип блока будет добавлен. Чтобы помочь вам в этом есть вспомогательный метод в классе NextBlock, который случайным образом выбирает тип следующего блока и возвращает тип того блока, который в данный момент отображается в окне NextBlock. Вращение также задается случайным образом; скажите «Привет!» классу RandomHelper.
С этими данными вы можете получить предварительно вычисленную фигуру и поместить ее в центр верхней части вашей сетки. Два цикла перебирают все квадраты фигуры. Они добавляют каждый фрагмент фигуры, проверяя, не перекрывает ли он имеющиеся в сетке данные. Если это случилось, игра окончена и вы услышите звук проигрыша. Происходит это, когда нагромождение блоков достигает вершины сетки и вы не можете добавить новый блок.
Теперь в вашей сетке есть новый блок, но скучно просто видеть его висящим вверху; он должен падать вниз.
Для проверки влияния гравитации на текущий блок используется тестовый модуль TestFallingBlockAndLineKill. Активный блок обновляется каждый раз, когда вы вызываете метод Update класса TetrisGrid, что происходит не слишком часто. На первом уровне метод Update вызывается только каждые 1000 мс (каждую секунду). Здесь вы проверяете может ли текущий блок быть перемещен вниз:
// Пытаемся переместить падающий блок вниз if (MoveBlock(MoveTypes.Down) == false || movingDownWasBlocked) { // Не удалось? Фиксируем падающий блок, // он больше не перемещается! for (int x = 0; x < GridWidth; x++) for (int y=0; y < GridHeight; y++) floatingGrid[x,y] = false; Sound.Play(Sound.Sounds.BlockFalldown); } // if movingDownWasBlocked = false;
Большая часть логики игры Тетрис сосредоточена во вспомогательном методе MoveBlock, который проверяет, возможно ли перемещение в заданном направлении. Если блок вообще не может быть перемещен, он фиксируется и вы очищаете массив floatingGrid и воспроизводите звук падения блока на землю.
После очистки массива floatingGrid у вас нет активного блока, который перемещается вниз и используется показанный ниже код, чтобы проверить, были ли уничтожены какие-нибудь линии.
// Проверяем, есть ли блок, который может перемещаться // если нет, добавляем новый случайный блок наверх! bool canMove = false; for (int x = 0; x < GridWidth; x++) for (int y = 0; y < GridHeight; y++) if ( floatingGrid[x,y] ) canMove = true; if (canMove == false) { int linesKilled = 0; // Проверяем, получили ли заполненную линию for (int y = 0; y < GridHeight; y++) { bool fullLine = true; for (int x = 0; x < GridWidth; x++ ) if ( grid[x,y] == BlockTypes.Empty ) { fullLine = false; break; } // for if // Получили заполненную линию? if (fullLine) { // Перемещаем все вниз for (int yDown = y-1; yDown > 0; yDown--) for (int x = 0; x < GridWidth; x++) grid[x,yDown+1] = grid[x,yDown]; // Очищаем верхнюю линию for (int x = 0; x < GridWidth; x++) grid[0,x] = BlockTypes.Empty; // Добавляем 10 очков и увеличиваем счетчик линий score += 10; lines++; linesKilled++; Sound.Play(Sound.Sounds.LineKill); } // if } // for // Если уничтожили 2 и более линий, увеличиваем счет if (linesKilled >= 2) score += 5; if (linesKilled >= 3) score += 10; if (linesKilled >= 4) score += 25; // Добавляем новый блок вверху AddRandomBlock(); } // if
Сначала здесь проверяется, есть ли активный перемещаемый блок. Если нет, вы попадаете в блок if, проверяющий, есть ли полностью заполненные линии, которые можно уничтожить. Чтобы определить, заполнена ли линия полностью, вы предполагаете, что заполнена, а затем смотрите, есть ли в этой линии пустой блок. В результате вы узнаете, что линия не заполнена и продолжаете проверять следующую линию. Если же линия полностью заполнена, вы удаляете ее, копируя все находящиеся выше нее линии вниз. Это место, где мог бы произойти небольшой взрыв. В любом случае игрок получает 10 очков за убитую линию, и вы слышите звук удаления линии.
Если игрок смог уничтожить сразу несколько линий, он награждается дополнительными очками. И, в конце, метод AddRandomBlock, который вы видели раньше, используется для создания нового блока наверху.
Обработка пользовательского ввода сама по себе больше не является серьезной задачей, благодаря вспомогательному классу Input. Вы можете легко проверить были ли нажаты или продолжают удерживаться клавиши управления курсором или кнопки игрового пульта. Клавиши Escape и Back обрабатываются в классе BaseGame и позволяют вам выйти из игры. Помимо этого для игры Тетрис вам потребуется только четыре клавиши. Для перемещения вправо и влево используются соответствующие клавиши управления курсором. Клавиша перемещения курсора вверх используется для вращения текущего блока, а клавиша перемещения курсора вниз, также как пробел или клавиша А, применяется для ускоренного перемещения падающего блока вниз.
Подобно тому, как при обработке гравитации проверяется, может ли блок перемещаться вниз, аналогичная проверка выполняется здесь, чтобы увидеть, можно ли переместить блок влево или вправо. Реальное перемещение блока происходит только при успешном прохождении проверки; этот код выполняется в методе Update класса TetrisGame, потому что вам надо проверять пользовательский ввод в каждом кадре, а не только когда происходит обновление в TetrisGrid, выполняющееся каждые 1000 мс, как вы узнали раньше. Сперва этот код располагался в методе Update класса TetrisGrid, но для улучшения игры был перемещен и слегка усовершенствован, чтобы позволять быстро перемещать блок влево или вправо, несколько раз нажимая клавишу управления курсором.
Ну что же, вы многое узнали о поддерживающем коде и почти готовы запустить игру Тетрис в первый раз. Но вы должны взглянуть на вспомогательный метод MoveBlock, потому что он является наиболее сложной и важной частью вашей игры Тетрис. Другой важный метод — это RotateBlock, который работает аналогичным образом и проверяет, может ли блок быть повернут. Его вы можете исследовать самостоятельно в исходном коде игры Тетрис. Пожалуйста, используйте тестовые модули в классе TertisGame, чтобы увидеть, как работают эти методы:
#region Перемещение блока public enum MoveTypes { Left, Right, Down, } // enum MoveTypes /// <summary> /// Помните, если перемещение вниз заблокировано, /// это увеличивает скорость игры, поскольку мы /// вынуждены переходить к следующему блоку! /// </summary> public bool movingDownWasBlocked = false; /// <summary> /// Перемещение текущего падающего блока влево, вправо или вниз. /// Если что-либо блокирует направление, перемещение невозможно /// и ничего не меняется! /// </summary> /// <returns> /// Возвращаем true если перемещение прошло успешно, /// и false в ином случае /// </returns> public bool MoveBlock(MoveTypes moveType) { // Очищаем старую позицию for (int x = 0; x < GridWidth; x++) for (int y = 0; y < GridHeight; y++) if ( floatingGrid[x,y] ) grid[x,y] = BlockTypes.Empty; // Перемещаем блок в новую позицию bool anythingBlocking = false; Point[] newPos = new Point[4]; int newPosNum = 0; if ( moveType == MoveTypes.Left ) { for (int x = 0; x < GridWidth; x++) for (int y = 0; y < GridHeight; y++) if ( floatingGrid[x,y] ) { if (x-1 < 0 || grid[x-1,y] != BlockTypes.Empty) anythingBlocking = true; else if ( newPosNum < 4 ) { newPos[newPosNum] = new Point(x-1, y); newPosNum++; } // else if } // for for if } // if (left) else if ( moveType == MoveTypes.Right ) { for (int x = 0; x < GridWidth; x++) for (int y = 0; y < GridHeight; y++) if ( floatingGrid[x,y] ) { if (x+1 >= GridWidth || grid[x+1,y] != BlockTypes.Empty) anythingBlocking = true; else if ( newPosNum < 4 ) { newPos[newPosNum] = new Point(x+1, y); newPosNum++; } // else if } // for for if } // if (right) else if ( moveType == MoveTypes.Down ) { for (int x = 0; x < GridWidth; x++) for (int y = 0; y < GridHeight; y++) if ( floatingGrid[x,y] ) { if (y+1 >= GridHeight || grid[x,y+1] != BlockTypes.Empty) anythingBlocking = true; else if ( newPosNum < 4 ) { newPos[newPosNum] = new Point(x, y+1); newPosNum++; } // else if } // for for if if ( anythingBlocking == true ) movingDownWasBlocked = true; } // if (down) // Если что-нибудь заблокировано, // восстанавливаем старое состояние if (anythingBlocking || // Или мы не получили все 4 новых позиции? newPosNum != 4 ) { for (int x = 0; x < GridWidth; x++) for (int y = 0; y < GridHeight; y++) if ( floatingGrid[x,y] ) grid[x,y] = (BlockTypes)currentBlockType; return false; } // if else { if (moveType == MoveTypes.Left) currentBlockPos = new Point(currentBlockPos.X-1, currentBlockPos.Y); else if (moveType == MoveTypes.Right) currentBlockPos = new Point(currentBlockPos.X+1, currentBlockPos.Y); else if (moveType == MoveTypes.Down) currentBlockPos = new Point(currentBlockPos.X, currentBlockPos.Y+1); // Иначе мы можем переместиться в новую позицию, // давайте сделаем это! for (int x = 0; x < GridWidth; x++) for (int y = 0; y < GridHeight; y++) floatingGrid[x,y] = false; for (int i = 0; i < 4; i++) { grid[newPos[i].X,newPos[i].Y] = (BlockTypes)currentBlockType; floatingGrid[newPos[i].X,newPos[i].Y] = true; } // for Sound.Play(Sound.Sounds.BlockMove); return true; } // else } // MoveBlock(moveType) #endregion
Здесь вы можете выполнить три типа перемещения: Left (влево), Right (вправо) и Down (вниз). Каждое из этих перемещений обрабатывается отдельным кодовым блоком, глядящим есть ли что-нибудь слева, справа или снизу и возможно ли требуемое перемещение. Перед тем, как перейти к деталям этого метода, следует упомянуть два момента. Во-первых, здесь есть вспомогательная переменная movingDownWasBlocked, определенная перед методом. Причина наличия данной переменной в том, чтобы ускорить процесс проверки, если текущий блок достиг уровня земли и сохранить этот факт на уровне класса, чтобы метод Update мог использовать его позже (что может произойти несколько кадров спустя) и сделать обновление в коде проверки гравитации, виденном вами ранее, намного быстрее, чем в случае, когда пользователь не хочет уронить блок вниз прямо здесь. Это очень важная часть игры, поскольку если каждый блок будет немедленно фиксироваться по достижении низа, игра станет слишком трудной и все удовольствие пропадет, когда она ускорится и сетка станет более заполненной.
Затем используется еще один трюк, упрощающий процесс проверки — временное удаление текущего блока из сетки. Благодаря этому вы легко можете проверить, допустимо ли новое местоположение, поскольку ваше текущее местоположение не будет ничего блокировать. Код также использует несколько вспомогательных переменных для хранения новой позиции, и код немного упрощен, чтобы учитывать только четыре части блока. Если вы измените типы блоков и количество частей в блоках, вы должны также поменять этот метод.
После всех этих установок вы проверяете, допустима ли новая виртуальная позиция блока в трех кодовых блоках. Обычно она допустима и вы заканчиваете с четыьмя новыми значениями в массиве newPosNum. Если у вас окажется меньше четырех значений, вы знаете, что что-то вам помешало и переменная anythingBlocking становится равной true. В этом случае восстанавливается старое местоположение блока и как сетка, так и массив floatingGrid остаются без изменений.
Но если попытка перемещения была успешной, местоположение блока обновляется и вы очищаете floatingGrid, а затем снова добавляете блок в новой позиции, добавляя его как в grid, так и в floatingGrid. Пользователь также слышит очень тихий звук перемещения блока, и вы заканчиваете метод.
Со всем этим новым кодом в классе TetrisGrid вы теперь можете проверить тестовые модули в классе TetrisGame. Помимо тестов, которые вы уже видели ранее, есть еще два важных тестовых модуля для игровой логики:
TestRotatingBlock, который проверяет метод RotateBlock класса TetrisGrid.
TestFallingBlockAndKillLine, который используется для тестирования гравитации и пользовательского ввода, которые мы только что изучили.
Очевидно, что вы будете часто возвращаться к старым тестовым модулям, чтобы обновить их в соответствии с последними изменениями, потребовавшимися для вашей игры. Например, тестовый модуль TestBackgroundBoxes, который вы видели ранее, очень прост, но структура и местоположение фоновых прямоугольников достаточно часто меняются по мере реализации и тестирования игровых компонентов, и вам приходится делать соответствующие обновления для отражения этих изменений. Одним из примеров может быть информационное табло, которое окружено фоновым прямоугольником, но прежде чем вы узнаете насколько большим должно быть табло, вы должны узнать какое будет его содержимое и сколько для него потребуется места. После написания метода TestScoreboard становится очевидно, например, что для табло потребуется фоновый прямоугольник немного меньшего размера, чем для компонента NextBlock.
Другой частью тестирования игры является постоянная проверка ошибок и улучшение игрового кода. Предыдущие игры были очень просты и вы могли сделать только незначительные усовершенствования после первого запуска, но игра Тетрис более сложна и вы можете потратить много часов исправляя и улучшая ее.
И последнее, вы можете проверить, будет ли игра запускаться на Xbox 360 — просто выберите в свойствах проекта платформу Xbox 360 и попробуйте скомпилировать и запустить его на консоли. Все необходимые для этого шаги были исследованы в главе 1, где также есть полезный раздел о решении проблем, в том случае если что-то не работает на Xbox 360. Если вы пишете новый код, то должны время от времени проверять, что он компилируется и для Xbox 360. Не следует писать какой-либо код, вызывающий неуправляемые сборки и использовать некоторые классы и методы .NET 2.0 Framework, отсутствующие на Xbox 360.
netlib.narod.ru | < Назад | Оглавление | Далее > |