Создай и играй: код для игры

Статья посвящена реализации классической игры “Змейка”. В данном руководстве мы воссоздадим эту игру, используя SwiftUI.

Игра “Змейка” в SwiftUI

Игра 

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

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

Компоненты

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

var timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()

Затем создаем представление, которое отображает каждый сегмент змейки и запоминает ее положение:

struct SnakeBody: View {
@State var snakeHead:CGPoint
var body: some View {
Rectangle()
.fill(Color.green)
.frame(width: 24, height: 24)
.position(snakeHead)
}
}

В главном цикле игры необходимо отрисовать срез сегментов змейки по мере ее продвижения. Сама змейка увеличивается в размере только в том случае, если игроку удалось “поглотить” отдельные квадраты.

@State var snakeParts:[AnyView] = [Rectangle()
.fill(Color.green)
.frame(width: 24, height: 24)
.position(x: screenWidth / 2, y: screenHeight / 2)
.eraseToAnyView()]

Обратите внимание, что код, определяющий первую запись в данном массиве, идентичен структуре SnakeBody. Но я не мог воспользоваться этой структурой, поскольку она не была инициализирована до того, как компилятор посчитал нужным выполнить здесь инициализацию переменной @State.

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

ZStack {
ForEach((snakeStart..<snakeParts.count), id: \.self) {
snakeParts[$0]
}
}.id(snakeCount)

Напоследок потребуется еще набор элементов управления, позволяющих оповещать змейку о необходимости сменить направление движения. Стоит отметить немного необычный код в этой части. Речь идет о longPressGesture с минимальным временем, равным 0, иначе говоря  —  касание. Код выглядит так: 

Text("Up")
.font(Fonts.avenirNextCondensedBold(size: 24))
.onLongPressGesture(minimumDuration: 0) {
snakeDirection = .up
}.padding()

Об этом ранее не упоминалось, но из примера кода видно, что я также создал перечисление с 4-мя направлениями для удобства чтения. 

Процесс игры 

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

.onReceive(timer, perform: { _ in
snakeCount = snakeParts.count - 1
print("snakeCount ",snakeCount)
switch snakeDirection {
case .up:
snakeHead.y -= 12
case .down:
snakeHead.y += 12
case .right:
snakeHead.x += 12
case .left:
snakeHead.x -= 12
}

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

let newPart = SnakeBody(snakeHead: snakeHead)
snakeParts.append(newPart
.eraseToAnyView())
snakeStart += 1

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

if snakeHead == snakeFood {
snakeStart -= 1
foodView = 0.0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
let rndX = Int.random(in: -10...10) * 12 + Int(screenWidth) / 2
let rndY = Int.random(in: -20...20) * 12 + Int(screenHeight) / 2
snakeFood = CGPoint(x: rndX, y: rndY)
foodView = 1.0
})
if snakeSpeed > 0.1 {
snakeSpeed -= 0.025
timer = Timer.publish(every: snakeSpeed, on: .main, in: .common).autoconnect()
}
}

Обратите внимание на странные магические формулы для значений rndX и rndY. Их использование обусловлено необходимостью убедиться, что каждый кусочек еды размещается в положении X/Y, сопоставимом с движением головы змейки. 

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

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

Рекомендации по доработке игры 

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

  • Змейка исчезает с экрана. Для этого потребуется внести в код проверку границ. Это первое условие окончания игры. 
  • Змейка сталкивается с частью своего тела без последствий. Это второе условие окончания игры, которое следует включить в код.  
  • По окончании игры выводится итоговый результат и/или сообщение для мониторинга успехов игрока. 

Обнаруженная ошибка 

Хотя это небольшое приложение отлично функционировало на iPhone 14 и 12,9-дюймовом симуляторе iPad Pro, на iPhone 8 оно работало со сбоями. Причина заключалась в чрезмерной точности инструкции if, отслеживающей захват еды змейкой. Отсюда вытекает финальное задание для читателей статьи  —  добавить менее взыскательную функцию, которая будет проверять степень совпадения координат. 

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Mark Lucking: Code a Snake Game With Button Controls Using SwiftUI

Предыдущая статьяКак использовать JavaScript для сокращения HTML-кода
Следующая статьяMeteor вместо Next.js: создаем NFT-маркетплейс