![](https://habrastorage.org/webt/go/ti/3z/goti3zbe-douszd51y8huhfg3ki.jpeg)
Всем привет! В преддверии запуска курса «iOS-разработчик. Базовый курс» мы организовали очередной открытый урок. Этот вебинар рассчитан на людей, которые имеют опыт разработки на любых языках и платформах, однако желают ещё изучить язык Swift и освоить разработку под iOS. На уроке мы подробно разобрали синтаксис и ключевые конструкции языка Swift, познакомились с основными инструментами разработки.
Участники вебинара узнали:
- что собой представляет язык Swift, каковы его особенности;
- как среда разработки XCode помогает в процессе работы;
- как создать простейшую игру под iOS.
Вебинар провёл Алексей Соболевский, iOS-разработчик в Яндексе.
Делаем Snake своими руками
Для работы мы использовали интегрированную среду разработки Xcode. Это удобная, бесплатная и функциональная среда, созданная компанией Apple.
В самом начале создали новый проект и выбрали базовый набор файлов «Game»:
![](https://habrastorage.org/webt/vs/lu/hg/vsluhgmqy_6rxlvmyqttv0filci.jpeg)
Не мудрствуя лукаво, назвали проект «Snake». Все настройки оставили по умолчанию, убедившись лишь в том, что в строке Game Technology стоит SpriteKit.
Подробности создания проекта.
После выполнения вышеперечисленных действий в левой части окна отобразится список файлов, автоматически созданных для нашего проекта. Одним из наиболее важных файлов является AppDelegate.swift, который помогает системе связываться с нашим кодом, когда возникают какие-нибудь значимые события для приложения (запуск, пуш, переход по ссылке и т. п.). Код этого файла:
//
// AppDelegate.swift
// SnakeGame
//
// Created by Alexey Sobolevsky on 15/09/2019.
// Copyright 2019 Alexey Sobolevsky. All rights reserved.
//
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
}
Не менее важными файлами являются GameScene.swift и GameViewController.swift. Класс GameScene создаёт сцену, а GameViewController отвечает за один экран приложения, который мы видим (один экран — один GameViewController). Конечно, это правило поддерживается не всегда, но в целом оно работает. Так как наше приложение довольно простое, у нас будет всего один GameViewController. С него и начнём.
Пишем GameViewController
Код по умолчанию мы удалим. У вью-контроллера есть несколько методов, которые срабатывают в зависимости от состояния экрана. Например,
viewDidLoad()
срабатывает, когда все элементы экрана уже загрузились, и экран вот-вот отобразится на смартфоне. Так как у нас игра, мы должны в наш вью-контроллер поместить игровую сцену (именно здесь будет бегать змейка и будут происходить все остальные события игры). Создаём сцену:
let scene = GameScene(size: view.bounds.size)
let — константа и ключевое слово. В языке Swift используется также и ключевое слово var, необходимое для определения переменной. Используя var, мы можем изменять значение переменных многократно во время работы программы. Используя let, мы не можем изменить значение переменных после инициализации.
Теперь нам надо убедиться, что вью, в которое мы поместим созданную сцену, соответствует нужному типу. Для этого используем конструкцию guard — это то же самое, что
if
, только наоборот (if not):guard let skView = view as? SKView else {
return
}
Убедившись, что элемент экрана соответствует нужному типу, добавляем к нему нашу сцену:
skView.presentScene(scene)
Также нужно, чтобы показывалось количество кадров в секунду (FPS):
skView.showsFPS = true
Потом отобразим количество элементов всех типов на сцене:
skView.showsNodeCount = true
И сделаем так, чтобы элементы отображались на экране вне зависимости от их порядка в иерархии элементов:
skView.ignoresSiblingOrder = true
А ещё не забываем о том, что наша сцена должна растягиваться на всю ширину экрана:
scene.scaleMode = .resizeFill
Вот итоговый код файла GameViewController.swift:
import UIKit
import SpriteKit
import GameplayKit
class GameViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
setup()
}
private func setup() {
guard let skView = view as? SKView else {
return
}
let scene = GameScene(size: view.bounds.size)
skView.showsFPS = true
skView.showsNodeCount = true
skView.ignoresSiblingOrder = true
scene.scaleMode = .resizeFill
skView.presentScene(scene)
}
}
Подробности создания файла GameViewController.swift.
Итак, мы создали сцену, но она пустая, поэтому если запустим эмулятор сейчас, увидим лишь чёрный экран.
Пишем GameScene
Как и в прошлый раз, большую часть кода удаляем, а потом выполняем необходимые настройки сцены. Здесь также есть свои методы. Например, так как мы добавили нашу сцену во ViewController, нам нужен метод
didMove()
:override func didMove(to view: SKView) {
setup(in: view)
}
Далее, когда запускается игра, на каждый кадр происходит вызов метода
Update()
:override func update(_ currentTime: TimeInterval) {
snake?.move()
}
И ещё нам понадобится несколько обработчиков нажатия на экран:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touchedNode = findTouchedNode(with: touches) else {
return
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touchedNode = findTouchedNode(with: touches) else {
return
}
Как известно, Swift славится наличием синтаксического сахара. Синтаксический сахар — это такие технические моменты, которые упрощают жизнь разработчику, ускоряют написание кода. Всё это очень помогает при настройке сцены, которой мы сейчас и займёмся. В первую очередь, зададим цвет:
backgroundColor = SKColor.white
Так как змейка работает в плоскости, физика нам не нужна, и её можно отключить, чтобы змейка не падала вниз из-за гравитации. Также нам не нужно, чтобы игра вращалась и т. п.:
physicsWorld.gravity = .zero
physicsWorld.contactDelegate = self
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsBody?.allowsRotation = false
physicsBody?.categoryBitMask = CollisionCategories.edgeBody
physicsBody?.collisionBitMask = CollisionCategories.snake | CollisionCategories.snakeHead
view.showsPhysics = true
Теперь создадим кнопки:
let counterClockwiseButton = ControlsFactory.makeButton(at: CGPoint(x: scene.frame.minX + 30, y: scene.frame.minY + 50),
name: .counterClockwiseButtonName)
addChild(counterClockwiseButton)
let clockwiseButton = ControlsFactory.makeButton(at: CGPoint(x: scene.frame.maxX - 90, y: scene.frame.minY + 50),
name: .clockwiseButtonName)
addChild(clockwiseButton)
Когда вы написали какой-то участок кода, следует подумать о том, можно ли код улучшить или отрефакторить так, чтобы его можно было в дальнейшем переиспользовать. Смотрите, у нас на экране по сути две кнопки, для создания которых используется один и тот же код. А значит, этот код можно вынести в отдельную функцию. Для этого создадим новый класс и, соответственно, файл ControlsFactory.swift со следующим кодом:
import SpriteKit
final class ControlsFactory {
static func makeButton(at position: CGPoint, name: String) -> SKShapeNode {
let button = SKShapeNode()
button.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 45, height: 45)).cgPath
button.position = position
button.fillColor = .gray
button.strokeColor = UIColor.lightGray.withAlphaComponent(0.7)
button.lineWidth = 10
button.name = name
return button
}
}
Чтобы нарисовать рандомное яблоко, которое будет «кушать» наша змейка, создаём класс Apple и файл Apple.swift:
import SpriteKit
final class Apple: SKShapeNode {
let diameter: CGFloat = 10
convenience init(at point: CGPoint) {
self.init()
path = UIBezierPath(ovalIn: CGRect(x: -diameter/2, y: -diameter/2, width: diameter, height: diameter)).cgPath
fillColor = .red
strokeColor = UIColor.red.withAlphaComponent(0.7)
lineWidth = 5
position = point
physicsBody = SKPhysicsBody(circleOfRadius: diameter / 2, center: .zero)
physicsBody?.categoryBitMask = CollisionCategories.apple
}
}
И описываем наше яблоко функцией
createApple()
в GameScene.swift:private func createApple() {
let padding: UInt32 = 15
let randX = CGFloat(arc4random_uniform(UInt32(gameFrameRect.maxX) - padding) + padding)
let randY = CGFloat(arc4random_uniform(UInt32(gameFrameRect.maxY) - padding) + padding)
let apple = Apple(at: CGPoint(x: randX, y: randY).relative(to: gameFrameRect))
gameFrameView.addChild(apple)
}
Что же, пришла очередь и для змеи. Она будет состоять из двух частей: тела (SnakeBodyPart.swift) и головы (SnakeHead.swift).
Код SnakeBodyPart.swift:
import SpriteKit
class SnakeBodyPart: SKShapeNode {
init(at point: CGPoint, diameter: CGFloat = 10.0) {
super.init()
path = UIBezierPath(ovalIn: CGRect(x: -diameter/2, y: -diameter/2, width: diameter, height: diameter)).cgPath
fillColor = .green
strokeColor = UIColor.green.withAlphaComponent(0.7)
lineWidth = 5
position = point
physicsBody = SKPhysicsBody(circleOfRadius: diameter - 4, center: .zero)
physicsBody?.isDynamic = true
physicsBody?.categoryBitMask = CollisionCategories.snake
physicsBody?.contactTestBitMask = CollisionCategories.edgeBody | CollisionCategories.apple
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Код SnakeHead.swift:
import SpriteKit
final class SnakeHead: SnakeBodyPart {
init(at point: CGPoint) {
super.init(at: point, diameter: 20)
physicsBody?.categoryBitMask = CollisionCategories.snakeHead
physicsBody?.contactTestBitMask = CollisionCategories.edgeBody | CollisionCategories.apple
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Однако не будем вас утомлять описанием каждой строчки, т. к. подробности создания файла GameScene.swift и других классов хорошо отображены в видео. Предлагаем лишь посмотреть итоговый код GameScene.swift:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var gameFrameRect: CGRect = .zero
var gameFrameView: SKShapeNode!
var startButton: SKLabelNode!
var stopButton: SKLabelNode!
var snake: Snake?
override func didMove(to view: SKView) {
setup(in: view)
}
override func update(_ currentTime: TimeInterval) {
snake?.move()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touchedNode = findTouchedNode(with: touches) else {
return
}
if let shapeNode = touchedNode as? SKShapeNode,
touchedNode.name == .counterClockwiseButtonName || touchedNode.name == .clockwiseButtonName {
shapeNode.fillColor = .green
if touchedNode.name == .counterClockwiseButtonName {
snake?.moveCounterClockwise()
} else if touchedNode.name == .clockwiseButtonName {
snake?.moveClockwise()
}
} else if touchedNode.name == .startButtonName {
start()
} else if touchedNode.name == .stopButtonName {
stop()
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touchedNode = findTouchedNode(with: touches) else {
return
}
if let shapeNode = touchedNode as? SKShapeNode,
touchedNode.name == .counterClockwiseButtonName || touchedNode.name == .clockwiseButtonName {
shapeNode.fillColor = .gray
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touchedNode = findTouchedNode(with: touches) else {
return
}
if let shapeNode = touchedNode as? SKShapeNode,
touchedNode.name == .counterClockwiseButtonName || touchedNode.name == .clockwiseButtonName {
shapeNode.fillColor = .gray
}
}
// MARK: -
private func start() {
guard let scene = scene else { return }
snake = Snake(at: CGPoint(x: scene.frame.midX, y: scene.frame.midY))
gameFrameView.addChild(snake!)
createApple()
startButton.isHidden = true
stopButton.isHidden = false
}
private func stop() {
snake = nil
gameFrameView.removeAllChildren()
startButton.isHidden = false
stopButton.isHidden = true
}
private func setup(in view: SKView) {
backgroundColor = SKColor.white
physicsWorld.gravity = .zero
physicsWorld.contactDelegate = self
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsBody?.allowsRotation = false
physicsBody?.categoryBitMask = CollisionCategories.edgeBody
physicsBody?.collisionBitMask = CollisionCategories.snake | CollisionCategories.snakeHead
view.showsPhysics = true
let margin: CGFloat = 20
let gameFrame = frame.inset(by: view.safeAreaInsets)
gameFrameRect = CGRect(x: margin, y: margin + view.safeAreaInsets.top + 55,
width: gameFrame.width - margin * 2, height: gameFrame.height - margin * 2 - 55)
drawGameFrame()
guard let scene = view.scene else {
return
}
let counterClockwiseButton = ControlsFactory.makeButton(at: CGPoint(x: scene.frame.minX + 30, y: scene.frame.minY + 50),
name: .counterClockwiseButtonName)
addChild(counterClockwiseButton)
let clockwiseButton = ControlsFactory.makeButton(at: CGPoint(x: scene.frame.maxX - 90, y: scene.frame.minY + 50),
name: .clockwiseButtonName)
addChild(clockwiseButton)
startButton = SKLabelNode(text: "S T A R T")
startButton.position = CGPoint(x: scene.frame.midX, y: 55)
startButton.fontSize = 40
startButton.fontColor = .green
startButton.name = .startButtonName
addChild(startButton)
stopButton = SKLabelNode(text: "S T O P")
stopButton.position = CGPoint(x: scene.frame.midX, y: 55)
stopButton.fontSize = 40
stopButton.fontColor = .red
stopButton.name = .stopButtonName
stopButton.isHidden = true
addChild(stopButton)
}
final func drawGameFrame() {
gameFrameView = SKShapeNode(rect: gameFrameRect)
gameFrameView.fillColor = .lightGray
gameFrameView.lineWidth = 2
gameFrameView.strokeColor = .green
addChild(gameFrameView)
}
private func findTouchedNode(with touches: Set<UITouch>) -> SKNode? {
return touches.map { [unowned self] touch in touch.location(in: self) }
.map { atPoint($0) }
.first
}
private func createApple() {
let padding: UInt32 = 15
let randX = CGFloat(arc4random_uniform(UInt32(gameFrameRect.maxX) - padding) + padding)
let randY = CGFloat(arc4random_uniform(UInt32(gameFrameRect.maxY) - padding) + padding)
let apple = Apple(at: CGPoint(x: randX, y: randY).relative(to: gameFrameRect))
gameFrameView.addChild(apple)
}
}
// MARK: - SKPhysicsContactDelegate
extension GameScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
var contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
contactMask ^= CollisionCategories.snakeHead
switch contactMask {
case CollisionCategories.apple:
let apple = contact.bodyA.node is Apple ? contact.bodyA.node : contact.bodyB.node
snake?.addBodyPart()
apple?.removeFromParent()
createApple()
case CollisionCategories.edgeBody:
stop()
break
default:
break
}
}
}
private extension String {
static let counterClockwiseButtonName = "counterClockwiseButton"
static let clockwiseButtonName = "clockwiseButton"
static let startButtonName = "startButton"
static let stopButtonName = "stopButton"
}
Результатом работы стала простейшая игра Snake:
![](https://habrastorage.org/webt/x7/i7/wt/x7i7wtj2zk-1y-xb-hkj4z-prg0.jpeg)
На написание игры у нас ушло около полутора часов. Если хотите получить навыки программирования на Swift, повторите все этапы самостоятельно. Кстати здесь вы получите полный доступ ко всем файлам кода, которые использовались в данном проекте.