Використовуємо SpriteKit для створення анімації в Swift

Зараз я працюю в 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 :

Додатково будуть використовуватися різні 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 syntax notation в 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])
))
}

В результаті виходить дуже живий персонаж.

Підводячи підсумки

Як можна переконатися на прикладі, застосувавши SpriteKit для анімації, нам вдалося написати дуже зрозумілий і наочний код, за яким легко експериментувати, змінюючи напрямку руху і масштабуючи елементи картинки. Безсумнівно, не можна назвати його універсальним засобом для створення анімації, оскільки у нас немає можливості застосувати такий спосіб для будь-яких видів анімації UIViews, але це може виявитися простим інструментом для окремо взятої картинки.

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

Підсумковий код проекту можна подивитися в репозиторії .

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

Опубліковано: 16/10/19 @ 10:00
Розділ Різне

Рекомендуємо:

Секретні техніки опрацювання вимог. Частина 1
Поради для початківця Java розробника. Підготовка до співбесіди — частина 2
C++ дайджест #20: CppCon 2019, Open Sourcing STL від MSVC
Шпаргалка з кібербезпеки для розробників
LocaleBro — локалізація Android - і iOS-додатків без зайвої роботи