Знакомимся с языком Swift на примере игры Snake



    Всем привет! В преддверии запуска курса «iOS-разработчик. Базовый курс» мы организовали очередной открытый урок. Этот вебинар рассчитан на людей, которые имеют опыт разработки на любых языках и платформах, однако желают ещё изучить язык Swift и освоить разработку под iOS. На уроке мы подробно разобрали синтаксис и ключевые конструкции языка Swift, познакомились с основными инструментами разработки.



    Участники вебинара узнали:

    • что собой представляет язык Swift, каковы его особенности;
    • как среда разработки XCode помогает в процессе работы;
    • как создать простейшую игру под iOS.

    Вебинар провёл Алексей Соболевский, iOS-разработчик в Яндексе.

    Делаем Snake своими руками


    Для работы мы использовали интегрированную среду разработки Xcode. Это удобная, бесплатная и функциональная среда, созданная компанией Apple.

    В самом начале создали новый проект и выбрали базовый набор файлов «Game»:



    Не мудрствуя лукаво, назвали проект «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:



    На написание игры у нас ушло около полутора часов. Если хотите получить навыки программирования на Swift, повторите все этапы самостоятельно. Кстати здесь вы получите полный доступ ко всем файлам кода, которые использовались в данном проекте.
    • +14
    • 5,3k
    • 1
    OTUS. Онлайн-образование
    562,42
    Цифровые навыки от ведущих экспертов
    Поделиться публикацией

    Похожие публикации

    Комментарии 1

      0
      Как то вы сложно преподнесли информацию, сначала вы пишете, что «Код по умолчанию мы удалим» а потом пишете про метод viewDidLoad() который мы уже адалили, а про viewWillLayoutSubviews() ни слова.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое