Використовуємо 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 :
- 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 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-додатків без зайвої роботи