Статьи

Создайте игру-головоломку тетромино, используя Swift — заключительные шаги

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

вращение

Подобно методу перемещения, мы подготовили поведение поворота с помощью метода rotate класса Tetromino :

 func rotate(rotation: Rotation = .Counterclockwise) { switch rotation { case .Counterclockwise: let bitmapSet = bitmaps[shape]! if rotationalState + 1 == bitmapSet.count { rotationalState = 0 } else { ++rotationalState } case .Clockwise: let bitmapSet = bitmaps[shape]! if (rotationalState == 0) { rotationalState = bitmapSet.count - 1 } else { --rotationalState } } } 

Теперь нам нужно вызвать этот метод из класса GameScene . Давайте реализуем это сейчас.

  1. Добавьте новый метод с именем rotateTetromino после метода moveTetrominoTo .

     func moveTetrominoTo(direction: Direction) {...} func rotateTetromino() { activeTetromino.rotate() if collidedWith(.None) { activeTetromino.rotate(rotation: .Clockwise) } else { refresh() } } 

    Мы немедленно поворачиваем активное тетромино и проверяем на наличие столкновений в его стационарном положении. Если столкновение обнаружено, мы немедленно поворачиваем кирпич в обратном направлении к его предыдущему состоянию. Если столкновения нет, мы обновляем игровую сцену.

  2. Как и боковое движение, нам нужно больше обрабатывать события касания, чтобы выполнить вращение наших кирпичей. Простой способ сделать это — отслеживать прикосновения на самой игровой доске. Давайте сделаем это сейчас.

Добавьте третье условие в обработчик событий touchesBegan чтобы проверить наличие прикосновений внутри игрового поля, а затем выполнить вращение.

 if location.x < gameBoardFrame.origin.x { moveTetrominoTo(.Left) } else if location.x > gameBoardFrame.origin.x + gameBoardFrame.width { moveTetrominoTo(.Right) } else if CGRectContainsPoint(gameBoardFrame, location) { rotateTetromino() } 

Сохраните и запустите ( ⌘ + R ) нашу игру. Теперь игра выглядит и ощущается как настоящая. Если вы попытались сыграть в игру в ее текущем состоянии и попытаться очистить линии , вы будете разочарованы, обнаружив, что это одно ключевое правило по-прежнему отсутствует. Не волнуйтесь, если вы не знакомы с этим правилом. Я кратко расскажу об этом в следующем разделе, а затем рассмотрим его реализацию.

Очистка Линий

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

  1. Добавьте новый метод с именем clearLines после метода clearLines .

     func collidedWith(direction: Direction) -> Bool {...} func clearLines() { var linesToClear = [Int]() for row in 0..<gameBitmapDynamic.count - 1 { var isLine = true for col in 0..<gameBitmapDynamic[0].count { if gameBitmapDynamic[row][col] == 0 { isLine = false } } if isLine { linesToClear.append(row) } } if linesToClear.count > 0 { for line in linesToClear { gameBitmapDynamic.removeAtIndex(line) gameBitmapDynamic.insert([8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8], atIndex: 1) } } } 

Мы перебираем все строки в игровом растровом изображении (кроме пола), который содержит активное tetromino, и проверяем нулевые значения. Нулевое значение означает, что в этом конкретном месте нет блока. Если есть хотя бы одно нулевое значение, мы можем смело предположить, что эта строка не имеет полной строки. Мы пропускаем этот ряд и переходим к следующему, пока не найдем полную строку.

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

  1. Давайте moveTetrominoTo этот новый метод в нашем методе moveTetrominoTo в разделе, где мы обрабатываем посадку:

     if direction == .Down { clearLines() gameBitmapStatic.removeAll(keepCapacity: true) gameBitmapStatic = gameBitmapDynamic ... } 

Сохраните и запустите ( ⌘ + R ) игру. Вы можете временно отрегулировать скорость сброса, уменьшив значение переменной defaultSpeed если вы не хотите слишком долго ждать, пока кирпичи приземлятся. Или мы можем просто добавить функцию мгновенного удаления. Это наша цель в следующем.

Instadrop

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

  1. Создайте новый метод с именем, как вы уже догадались, instaDrop . Вы можете добавить его после нашего метода clearLines .

     func clearLines() {...} func instaDrop() { while collidedWith(.Down) == false { activeTetromino.moveTo(.Down) updateGameBitmap() } clearLines() gameBitmapStatic.removeAll(keepCapacity: true) gameBitmapStatic = gameBitmapDynamic activeTetromino = Tetromino() centerActiveTetromino() refresh() lastUpdate = NSDate() } 

Мы постоянно перемещаем активное тетромино вниз, пока на пути нет препятствий. Мы используем метод collidedWith для проверки столкновений. Мы также обновляем растровое изображение игры, чтобы отразить новые позиции активного тетромино.

После цикла мы выполняем те же процедуры посадки, что и в moveTetrominoTo после moveTetrominoTo кирпича. Для удобства мы можем поместить это в отдельный метод, чтобы уменьшить дублирование кода. Давайте сделаем это сейчас. Создайте функцию с именем didLand и обновите ее тело, используя дублированный фрагмент кода.

 func instaDrop() {...} func didLand() { clearLines() gameBitmapStatic.removeAll(keepCapacity: true) gameBitmapStatic = gameBitmapDynamic activeTetromino = Tetromino() centerActiveTetromino() refresh() lastUpdate = NSDate() } 

Теперь обновите instaDrop и moveTetrominoTo, чтобы повторно использовать эту общую функцию.

 func moveTetrominoTo(direction: Direction) { if collidedWith(direction) == false { ... } else { if direction == .Down { didLand() return } } refresh() } ... func instaDrop() { while collidedWith(.Down) == false {...} didLand() } 
  1. Давайте реализуем другой обработчик сенсорных событий для этого поведения. Прикосновение к нижней части игрового поля было бы очевидным выбором, но игрок должен быть осторожен, чтобы случайно не прикоснуться к нему.

Добавьте четвертое условие в обработчик событий touchesBegan чтобы проверить наличие касаний под игровым полем, а затем выполните сброс.

 if location.x < gameBoardFrame.origin.x { moveTetrominoTo(.Left) } else if location.x > gameBoardFrame.origin.x + gameBoardFrame.width { moveTetrominoTo(.Right) } else if CGRectContainsPoint(gameBoardFrame, location) { rotateTetromino() } else if location.y < gameBoardFrame.origin.y { instaDrop() } 

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

Следующий кирпич

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

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

     ... class GameScene: SKScene { let gameBoard = SKSpriteNode() let nextTetrominoDisplay = SKSpriteNode() var activeTetromino = Tetromino() var nextTetromino = Tetromino() ... } 
  2. Создайте новый метод с именем showNextTetromino в GameScene.swift .

     func showNextTetromino() { nextTetrominoDisplay.removeAllChildren() for row in 0..<nextTetromino.bitmap.count { for col in 0..<nextTetromino.bitmap[row].count { if nextTetromino.bitmap[row][col] > 0 { let bit = nextTetromino.bitmap[row][col] let square = SKSpriteNode(color: colors[bit], size: CGSize(width: blockSize, height: blockSize)) square.anchorPoint = CGPoint(x: 0, y: 1.0) square.position = CGPoint(x: col * Int(blockSize) + col, y: -row * Int(blockSize) + -row) nextTetrominoDisplay.addChild(square) } } } let nextTetrominoDisplayFrame = nextTetrominoDisplay.calculateAccumulatedFrame() let gameBoardFrame = gameBoard.calculateAccumulatedFrame() nextTetrominoDisplay.position = CGPoint(x: gameBoardFrame.origin.x + gameBoardFrame.width - nextTetrominoDisplayFrame.width, y: -30) if nextTetrominoDisplay.parent == nil { self.addChild(nextTetrominoDisplay) } } 

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

  3. Давайте добавим пару новых строк в метод didMoveToView .

     override func didMoveToView(view: SKView) { /* Setup your scene here */ ... nextTetrominoDisplay.anchorPoint = CGPoint(x: 0, y: 1.0) showNextTetromino() } 

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

  1. Мы также модифицируем метод didLand для обработки этого обновления.

     func didLand() { clearLines() gameBitmapStatic.removeAll(keepCapacity: true) gameBitmapStatic = gameBitmapDynamic activeTetromino = nextTetromino centerActiveTetromino() nextTetromino = Tetromino() showNextTetromino() refresh() lastUpdate = NSDate() } 

Вместо того, чтобы создавать новый объект Tetromino для установки в качестве активного, мы назначаем следующее tetromino в качестве текущего активного. Наконец, мы создаем новое tetromino для добавления в очередь, затем вызываем showNextTetromino для его отображения.

Сохраните и запустите ( ⌘ + R ) игру. Получайте удовольствие от стратегии и планирования вашего следующего шага. Там еще пара элементов отсутствует. Какой смысл играть в игру, не уходя никуда. Нам нужно чувство прогресса. Это то, что мы собираемся реализовать дальше.

Подсчет очков и повышение уровня

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

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

     class GameScene: SKScene { ... let scoreLabel = SKLabelNode() let levelLabel = SKLabelNode() var score = 0 var level = 1 var nextLevel = 3000 override func didMoveToView(view: SKView) {...} } 

Мы начинаем с 0 баллов и 1 уровня. Чтобы повысить уровень, нам нужно превысить определенный балл. Например, чтобы достичь уровня 2, нам нужно преодолеть 3000 очков.

  1. Затем мы создаем новый метод с именем updateScoreWith который принимает количество баллов в качестве параметра для добавления к текущему результату.

     func updateScoreWith(points: Int = 1) { if scoreLabel.parent == nil &amp;&amp; levelLabel.parent == nil { let gameBoardFrame = gameBoard.calculateAccumulatedFrame() scoreLabel.text = &quot;Score: \(score)&quot; scoreLabel.fontSize = 20.0 scoreLabel.fontColor = SKColor.whiteColor() scoreLabel.horizontalAlignmentMode = .Left scoreLabel.position = CGPoint(x: gameBoardFrame.origin.x, y: -scoreLabel.frame.height - 50) self.addChild(scoreLabel) levelLabel.text = &quot;Level: \(level)&quot; levelLabel.fontSize = 20.0 levelLabel.fontColor = SKColor.whiteColor() levelLabel.horizontalAlignmentMode = .Left levelLabel.position = CGPoint(x: scoreLabel.frame.origin.x, y: -levelLabel.frame.height - scoreLabel.frame.height - 50 - 10) self.addChild(levelLabel) } score += points * level * level scoreLabel.text = &quot;Score: \(score)&quot; if score > nextLevel { levelLabel.text = &quot;Level: \(++level)&quot; nextLevel = Int(2.5 * Double(nextLevel)) if dropTime - 150 <= 0 { // Maximum speed dropTime = 100 } else { dropTime -= 150 } } } 

Мы готовим метки узлов, которые будут отображать наш текущий счет и уровень. Настройка размера шрифта, цвета, выравнивания и положения. Далее мы обновляем счет, используя текущий уровень в качестве множителя. Чем выше уровень, тем выше оценка. Затем мы проверяем, превысили ли мы целевой балл. Если бы это было так, мы увеличиваем уровень на 1, а скорость — на 150 миллисекунд. Мы также увеличиваем целевой показатель для следующего уровня на 150%. Мы также обновляем отображаемую оценку в нашей сцене. Мы установили ограничение скорости, но реально есть очень маленький шанс, что кто-то достигнет этого.

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

     override func didMoveToView(view: SKView) { ... updateScoreWith(points: 0) } 

    Далее мы вызываем его в методе moveTetrominoTo для добавления автоматических точек при падении.

     func moveTetrominoTo(direction: Direction) { if collidedWith(direction) == false { activeTetromino.moveTo(direction) if direction == .Down { updateScoreWith() lastUpdate = NSDate() } } else { ... } ... } 

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

 func clearLines() { ... if linesToClear.count > 0 { ... var multiplier = linesToClear.count == 4 ? 10 : 1 updateScoreWith(points: linesToClear.count * linesToClear.count * linesToClear.count) } } 

Наконец, мы набрали в два раза больше, чем обычно, выполняя мгновенное падение

 func instaDrop() { while collidedWith(.Down) == false { updateScoreWith(points: 2) activeTetromino.moveTo(.Down) updateGameBitmap() } didLand() } 

Это оно! Сохраните и запустите ( ⌘ + R ) игру и попытайтесь побить мой рекорд в 406 858 баллов. На этом этапе вы можете настроить размер и цвет фигур, игрового поля и других элементов. Поиграйте с правилами подсчета очков, уровнями цели, сенсорным управлением и другой механикой. Сделайте эту игру по-настоящему своей.

Рекорд

Вывод

Сама игра теперь практически завершена, за исключением нескольких пропущенных наворотов. Я позволю вам позаботиться о добавлении звуковых эффектов, пользовательских значков приложений, графики экрана запуска и обработке состояния «игра закончена» в качестве упражнения. Тем не менее, обратите внимание, что вам не рекомендуется продавать это как свою собственную игру из-за риска столкновения с нарушением авторских прав. А пока, похлопайте себя по спине, чтобы он оставался до конца.

Мы рассмотрели только некоторые из наиболее важных функций Swift, включая вывод типов, константы, перечисления и дополнительные параметры. Но ваше обучение не останавливается здесь. Продолжайте читать книгу Apple Swift Programming Language, чтобы получить более глубокое понимание и понимание языка.

Спасибо и всего наилучшего вашей карьере в разработке приложений или игр!