×

Используем SpriteKit для создания анимации в Swift

[От редакции: после публикации статьи оказалось, что в ней есть заимствования из статьи «Using SpriteKit to create animations in Swift». Благодарим Dim Walker за внимательность]

Сейчас я работаю в GameDev-компании, которая использует игровой движок SpriteKit в разработке своих проектов. В этой статье я хочу продемонстрировать, как даже в UIkit-проектах можно легко применить его для быстрого создания анимаций.

SpriteKit — это нативный игровой движок от Apple, представленный впервые в iOS 7 и Mac OS 10.9. Обычно он используется для создания 2D-игр. Но это не мешает ему быть хорошим инструментом при создании анимаций, 2D-текстур и не только. Например, на WWDC 2017 Apple раскрыла, что задействовала SpriteKit в UI для Memory Debugger в Xcode.

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

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

SpriteKit очень удобен для создания несложных анимационных сцен, таких как полноэкранная анимация загрузки, иллюстрация в Onboarding- и Tutorial-экранах или в других элементах пользовательского интерфейса. Для наглядности попробуем создать анимацию загрузки, в которой будут использоваться 4 emoji: Tangerine, Lemon, Peach, Mango.

Настройка сцены

Все содержимое в SpriteKit-сцене представлено объектом SKScene. Затем ее наполнение определяется с помощью так называемых нод (node), позволяющих создавать иерархию наподобие применяемой в UIViews или CALayers. Сцена является root-нодой в иерархии.

Для наполнения сцены используются определенные сабклассы SKNode:

  • SKSpriteNode подойдет для создания контента на базе спрайтов;
  • SKLabelNode — для работы с текстовым контентом;
  • SKLightNode — для управления светом на сцене;
  • SKEmitterNode — для создания эффекта частиц, то есть дыма, огня, искр и других;
  • SKShapeNode — для создания простых графических элементов из математических точек, линий и кривых;
  • SKReferenceNode, SKAudioNode, SKCameraNode, SKVideoNode... Полный список можно посмотреть в документации.

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

Благодаря этим приемам мы сможем оживить нашу картинку и привести ее элементы в движение.

Итак, приступим!

Попробуем начать с создания SKScene в качестве базовой основы для будущей анимации. Для этого придадим элементу форму квадрата белого цвета, исходя из размера контроллера:

func makeScene() -> SKScene {
        
        let minimumDimension = min(view.frame.width, view.frame.height)
        let size = CGSize(width: minimumDimension, height: minimumDimension)
        let scene = SKScene(size: size)
        scene.backgroundColor = .white
        return scene
 }

Мы презентуем SKScene через SKView (который является сабклассом от UIView), добавив определенные манипуляции по изменению размера и центрированию. Теперь можно сделать present сцены:

import UIKit
import SpriteKit
 
final class ViewController: UIViewController {
 
  private let animationView = SKView()
  
  override func viewDidLoad() {
    super.viewDidLoad()
 
    view.addSubview(animationView)
    let scene = makeScene()
    animationView.frame.size = scene.size
    animationView.presentScene(scene)
  }
  
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
 
    animationView.center.x = view.bounds.midX
    animationView.center.y = view.bounds.midY
  }
}

Добавление нод

Имея сцену для рендеринга, можно начать добавлять контент.

Так как в текущем примере будут использоваться самые простые emoji-смайлики в качестве текстур, удобнее всего будет применить SKLabelNode (аналог UILabel в UIkit).

Итак, приступим к созданию extension для SKLabelNode, позволяющего рендерить emoji:

extension SKLabelNode {
  
  func renderEmoji(_ emoji: Character) {
    fontSize = 50
    text = String(emoji)
    
    verticalAlignmentMode = .center
    horizontalAlignmentMode = .center
  }
}

Далее, напишем еще один из методов, чтобы добавить все смайлики на сцену.

func addEmoji(to scene: SKScene) {
    let allEmoji: [Character] = ["🍊", "🍋", "🍑", "🥭"]
    let distance = floor(scene.size.width / CGFloat(allEmoji.count))
    
    for (index, emoji) in allEmoji.enumerated() {
      let node = SKLabelNode()
      node.renderEmoji(emoji)
      node.position.y = floor(scene.size.height / 2)
      node.position.x = distance * (CGFloat(index) + 0.5)
      scene.addChild(node)
    }
 }

Пробуем оживить анимацию

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

func animateNodes(_ nodes: [SKNode]) {
    for (index, node) in nodes.enumerated() {
      // Создаем Delay для каждой ноды в зависимости от индекса
      let delayAction = SKAction.wait(forDuration: TimeInterval(index) * 0.2)
 
      // Анимация увеличения, а затем уменьшения
      let scaleUpAction = SKAction.scale(to: 1.5, duration: 0.3)
      let scaleDownAction = SKAction.scale(to: 1, duration: 0.3)
 
      // Ожидание в 2 секунды, прежде чем повторить Action
      let waitAction = SKAction.wait(forDuration: 2)
 
      // Формируем Sequence (последовательность) для SKAction
      let scaleActionSequence = SKAction.sequence([scaleUpAction,
                             			scaleDownAction,
                            			waitAction])
 
      // Создаем Action для повторения нашей последовательности
      let repeatAction = SKAction.repeatForever(scaleActionSequence)
 
      // Комбинируем 2 SKAction: Delay и Repeat
      let actionSequence = SKAction.sequence([delayAction, repeatAction])
 
      // Запускаем итоговый SKAction
      node.run(actionSequence)
    }
 }

И хотя приведенный выше код является рабочим, он достаточно сложен для понимания, если убрать из него комментарии. Однако у нас есть прекрасная возможность легко исправить этот недостаток, используя dot notation syntax в Swift. Это поможет существенно сократить размер кода, убрав временные let-присваивания:

func animateNodes(_ nodes: [SKNode]) {
    for (index, node) in nodes.enumerated() {
      node.run(.sequence([
        .wait(forDuration: TimeInterval(index) * 0.2),
        .repeatForever(.sequence([
          .scale(to: 1.5, duration: 0.3),
          .scale(to: 1, duration: 0.3),
          .wait(forDuration: 2)
          ]))
        ]))
    }
 }

Теперь можно запустить нашу анимацию, добавив вызов написанных выше методов в makeScene().

func makeScene() -> SKScene {
    let minimumDimension = min(view.frame.width, view.frame.height)
    let size = CGSize(width: minimumDimension, height: minimumDimension)
    let scene = SKScene(size: size)
    scene.backgroundColor = .white
    addEmoji(to: scene)
    animateNodes(scene.children)
    return scene
  }

Результат:

Добавим оригинальности

Получив очень наглядный и легко читаемый анимационный код, мы можем слегка развлечься и добавить немного движений. Например, заставить выполнять повороты на 360° с одновременным изменением размеров. Для этого нам просто потребуется объединить два действия по изменению размера и поворотам в одно целое:

func animateNodes(_ nodes: [SKNode]) {
    for (index, node) in nodes.enumerated() {
      node.run(.sequence([
        .wait(forDuration: TimeInterval(index) * 0.2),
        .repeatForever(.sequence([
          .group([
            .sequence([
              .scale(to: 1.5, duration: 0.3),
              .scale(to: 1, duration: 0.3)
              ]),
            .rotate(byAngle: .pi * 2, duration: 0.6)
            ]),
          .wait(forDuration: 2)
          ]))
        ]))
    }
  }

В результате получается:

Анимация текстур

Под конец покажу, как можно с помощью этих инструментов создавать полноценную живую анимацию. Для этого использую набор последовательных картинок. Каждый может взять видео-анимацию и разбить ее покадрово. Логика примерно такая же, как и при создании гифок. В моем случае анимация будет разбита на 28 кадров (изображений). Каждый кадр имеет название «warrior_walk_00XX», где XX — это число от 1 до 28. Все изображения помещаются в Assets проекта. Затем нужно собрать вместе этот массив текстур:

func animationFrames(forImageNamePrefix baseImageName: String,
                         frameCount count: Int) -> [SKTexture] {
 
        var array = [SKTexture]()
        for index in 1...count {
            let imageName = String(format: "%@%04d.png", baseImageName, index)
            let texture = SKTexture(imageNamed: imageName)
            array.append(texture)
        }
        return array
    }

Теперь нужно написать анимацию движения персонажа и его направление. И добавить вызов этой функции во viewDidLoad.

func createSceneContents(for scene: SKScene) {
        let defaultNumberOfWalkFrames: Int = 28
        let characterFramesOverOneSecond: TimeInterval = 1.0 / TimeInterval(defaultNumberOfWalkFrames)
        let walkFrames = animationFrames(forImageNamePrefix: "warrior_walk_",
                                         frameCount: defaultNumberOfWalkFrames)
 
        let sprite = SKSpriteNode(texture: walkFrames.first)
        sprite.position = CGPoint(x: animationView.frame.midX,
                                  y: animationView.frame.midY + 60)
        scene.addChild(sprite)
.       // Анимация текстур
        let animateFramesAction: SKAction = .animate(with: walkFrames,
                                                   timePerFrame: characterFramesOverOneSecond,
                                                   resize: true,
                                                   restore: false)
        // Анимация поворота персонажа на 90 градусов
        let rotate: SKAction = .rotate(byAngle: .pi / 2, duration: 0.3)
        let newPosition: CGFloat = 100
        let moveDuration: TimeInterval = 1.0
        sprite.run(.repeatForever(
            .sequence(
                [.group([ // Движение вверх
                    animateFramesAction,
                    .moveBy(x: 0.0, y: newPosition, duration: moveDuration)]),
                 rotate,
                 .group([ // Движение влево
                    animateFramesAction,
                    .moveBy(x: -newPosition, y: 0.0, duration: moveDuration)]),
                 rotate,
                 .group([ // Движение вниз
                    animateFramesAction,
                    .moveBy(x: 0.0, y: -newPosition, duration: moveDuration)]),
                 rotate,
                 .group([// Движение вправо
                    animateFramesAction,
                    .moveBy(x: newPosition, y: 0.0, duration: moveDuration)]),
                 rotate])
            ))
    }

В результате получается очень живой персонаж.

Анимация без кода

Теперь я покажу как можно создавать и добавлять анимацию практически без написания кода. Для этого лучше создать новый проект. В нем я заранее указываю, что экран будет использоваться только в Landscape-положении, создаю файл-сцены, подготавливаю SKView в Storyboard-файле и добавляю текстуры для SKScene в Assets папку проекта.

Весь код во ViewController будет состоять только из этих строчек:

let completeScene: CompletionScene! = SKScene(fileNamed: "CompletionScene") as? CompletionScene
 
    override func viewDidLoad() {
        super.viewDidLoad()
        
        completeScene.scaleMode = .aspectFill
        if let view = view as? SKView {
            view.presentScene(completeScene)
        }
    }

Затем переходим к SKS файлу сцены, где и будут происходить дальнейшие действия. Scene Editor немного отличается от стандартного Interface Builder в Storyboard-файлах. В нем все так же слева можно видеть Scene Graph — панель с иерархией объектов, которые размещаем на сцену, а справа классический Attribute Inspector со своим набором правил. Внизу уже расположен Action Editor, в котором будут редактироваться все SKAction.

Единственное, что может показаться не сразу привычным, это система координат. Здесь позиция x:0, y:0 начинается из центра.

В редакторе я набросал вполне обычный layout сцены завершения игры. В моем случае я расположил 18 объектов SKSpriteNode и 1 SKLabelNode.

После этого можно начинать добавлять в Action Editor нужные SKAction для анимации. Здесь они работают практически одинаково, как и если писать их кодом. В текущем примере я буду использовать: MoveTo, Rotate, FadeIn. Добавляется это все банальным перетаскиванием Action на нужную ноду, а затем в Attribute Inspector уже можно задавать более точные параметры для действия. Желательно следить за тем, что задано в параметрах Duration и Start Time.

Также стоит обратить внимание, что у SKSpriteNode присутствует свойство Anchor Point, которое, по сути, является точкой внутри ноды, от которой будут осуществляться все действия. По умолчанию значение AnchorPoint: 0.5, 0.5. Apple обычно так иллюстрирует, как работает эта точка:

Если же я буду применять Rotate к прожекторам со стандартным AnchorPoint, то они будут вращаться от своего центра а не от верхней точки, что будет смотреться немного нелепо. Именно поэтому для прожекторов и некоторых текстур со зрителями я изменил AnchorPoint, чтобы точка вращения ноды отличалась.

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

Таким образом, с применением минимального количества кода, можно быстро создавать анимации, которые вы можете в реальном времени просматривать и модифицировать. По итогу, достаточно будет только в Storyboard-файле, поверх SKView, наложить нужные кнопки действий.

Добавляем изюминку

В конце хочу еще показать, как можно быстро добавлять частицы для таких анимаций. Для этого нужно создать SpriteKit Particle File в Xcode, где в качестве particle template достаточно указать Bokeh. После этого в довольно простом для понимания Particle Emitter Editor можно настроить огромное количество параметров как будут выглядеть частицы. Останавливаться на каждом из параметров я не буду, но для текущего примера я настроил Lifetime, Angle Range, Emitter Birthrate, Speed и Color Ramp. Очень удобно, что все проводимые изменения можно мгновенно видеть в SKS файле.

После всех настроек я просто добавляю SKReferenceNode на сцену и указываю в Attribute Inspector, в параметре Reference, созданный SKS файл для частиц. В результате мы получаем:

При таком простом подходе к реализации есть пара недостатков:

  • стоит учитывать, что файл сцены — это не XML, а самый обычный бинарный файл. И если два разных разработчика внесли там изменения, а потом попробуют сделать merge своих изменений, то конфликт и потеря данных будет неизбежной;
  • иногда Scene Editor может крэшить весь Xcode, и изменение, которое вы сделали, в итоге откатится немного назад.

Подводя итоги

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

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

Итоговую реализацию первого и второго примера можно посмотреть в репозитории..

При написании статьи использовался материал со Swift by Sundell и Apple.

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному2
LinkedIn

Схожі статті




6 коментарів

Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.

Спасибо за техническую статью на DOU.
Не понимаю недовольства остальных. Все оформлено хорошо, источники указаны. Личные примеры есть!

Был бы рад увидеть подобное про SceneKit.

Да уж. Swift by Sundell здесь не просто «использовался», а «статья» фактически слизана оттуда.

Вот первоисточник от 2017 года: swiftbysundell.com/...​eate-animations-in-swift

Сравните код там и код здесь. Насколько я знаю, обычно редакция DOU следит за тем, чтобы контент был оригинальным, но в этом случае вышел промах.

Диванные детективы никогда не спят, интриги скандалы и расследования круглосуточно :)

Сам материал писался о том как делать различные анимации и сфере применения SpriteKit. Но так как в итоге получился большой лонг-рид на несколько десятков страниц, то весь материал был разбит на части. Это одна из частей. В качестве введения в основы был выбран пример от swiftbysundell, потому что он легко и просто иллюстрирует применение SKAction. Частично пример был собран в один проект и добавлена еще анимация текстур. Собственно источник указан был в самой статье и репозитории. Было бы куда интереснее разоблачение если бы источник не был указан.
Статья в итоге уже немного расширена материалом из следующей части, что бы не казалось будто полностью что-то передрали, хотя просто был использован хороший наглядный пример.

Планка-то падает. Следующая статья: как отобразить статический текст при помощи SwiftUI.

В последнее время я начал подозревать, что Ray Wenderlich купил долю в DOU :D

Типичный Ray Wenderlich: нет времени объяснять, просто добавь этот код во view controller.

Підписатись на коментарі