Розробка мобільної гри на SpriteKit (iOS)
SpriteKit — нативний 2D-фреймворк Apple, вбудований в iOS SDK з версії 7. Не потребує сторонніх залежностей, хорошо інтегрується з GameplayKit для логіки ІІ противників, показує стабільні 60 fps на iPhone SE другого покоління при розумному навантаженні. Для 2D-ігор з помірною складністю — розумний вибір, особливо якщо команда вже пише на Swift та не хоче тягти в проект Unity або Godot.
Архітектура гри: сцени, ноди та фізика
Все в SpriteKit — це дерево SKNode. SKScene — кореневий контейнер, SKSpriteNode — об'єкт, що відображається, SKEmitterNode — система частинок, SKLabelNode — текст. Типова помилка в перших проектах — створювати сцени як «моноліт», мішаючи логіку руху, відображення, звук та UI в одному файлі. При 200 рядках це вже нечитаємо.
Робочою структура через компонентний підхід з GKComponent з GameplayKit:
class EnemyNode: SKSpriteNode {
var movementComponent: MovementComponent?
var healthComponent: HealthComponent?
}
class MovementComponent: GKComponent {
override func update(deltaTime seconds: TimeInterval) {
guard let node = entity?.component(ofType: GKSKNodeComponent.self)?.node else { return }
node.position.y -= CGFloat(150 * seconds)
}
}
Це дозволяє тестувати MovementComponent ізольовано та переиспользувати між типами ворогів.
Фізичний движок SpriteKit заснований на Box2D. SKPhysicsBody трьох видів: circleOfRadius, rectangleOf(size:) та bodyWithTexture(_:alphaThreshold:size:) — останній генерує полігональний коллайдер по пікселям текстури. На практиці bodyWithTexture з alphaThreshold: 0.5 зручний, але дорогий: на складних текстурах генерація тіла займає ощутимий час. Кешуємо та переиспользуємо:
extension SKPhysicsBody {
private static var cache: [String: SKPhysicsBody] = [:]
static func cached(texture: SKTexture, size: CGSize, key: String) -> SKPhysicsBody {
if let cached = cache[key] {
return cached.copy() as! SKPhysicsBody
}
let body = SKPhysicsBody(texture: texture, alphaThreshold: 0.5, size: size)
cache[key] = body
return body.copy() as! SKPhysicsBody
}
}
Коллізії встановлюються через categoryBitMask та contactTestBitMask. Типова проблема — пропуск коллізій при високій швидкості («туннелювання»). Рішення: usesPreciseCollisionDetection = true для швидких тіл, але це дорого по CPU. Альтернатива — SKPhysicsWorld.enumerateBodies(alongRayStart:end:using:) для ручних ray cast перевірок в update(_:).
Атлас текстур та продуктивність
Draw call — головний враг продуктивності в SpriteKit. Кожна унікальна текстура — потенційно окремий draw call. SKTextureAtlas групує спрайти в один атлас:
let atlas = SKTextureAtlas(named: "Enemies")
let texture = atlas.textureNamed("enemy_run_01")
Xcode компілює атлас автоматично з папки .spriteatlas. Правило: все, що рисується одночасно — в один атлас. Перевірити кількість draw calls можна в Xcode через View → Debug → Statistics прямо в симуляторі при запущеній грі.
При SKSpriteNode розміром 64×64 та текстурою 512×512 Metal виконує downscale на GPU кожен кадр. Текстури мають бути максимально близькі до розміру відображення. Xcode Instruments → Metal System Trace покаже, якщо GPU перевантажений ненужним масштабуванням.
Анімація через SKAction.animate(with:timePerFrame:):
let frames = (1...8).map { atlas.textureNamed("run_\(String(format: "%02d", $0))") }
let animation = SKAction.animate(with: frames, timePerFrame: 1.0/12.0, resize: false, restore: false)
let loop = SKAction.repeatForever(animation)
character.run(loop, withKey: "running")
withKey: дозволяє зупинити або замінити анімацію пізніше через removeAction(forKey:).
Звук: AVAudioEngine замість SKAction.playSoundFileNamed
SKAction.playSoundFileNamed(_:waitForCompletion:) — зручно для прототипу, але не годиться для продакшену: немає контролю громкості, немає можливості паузи, файл декодується при кожному вызові. Для ігор використовуємо AVAudioEngine з AVAudioPlayerNode:
class AudioManager {
private let engine = AVAudioEngine()
private var playerNodes: [String: AVAudioPlayerNode] = [:]
private var audioFiles: [String: AVAudioFile] = [:]
func preloadSound(named name: String) throws {
let url = Bundle.main.url(forResource: name, withExtension: "wav")!
audioFiles[name] = try AVAudioFile(forReading: url)
}
func playSound(named name: String) {
guard let file = audioFiles[name] else { return }
let node = AVAudioPlayerNode()
engine.attach(node)
engine.connect(node, to: engine.mainMixerNode, format: file.processingFormat)
node.scheduleFile(file, at: nil)
node.play()
}
}
Передзавантажуємо звуки у фоні при старті сцени, не блокуючи main thread.
GameplayKit: поведінка ворогів без винаходу велосипеда
GKStateMachine чудово підходить для AI-станів ворога:
class EnemyIdleState: GKState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
stateClass == EnemyChaseState.self || stateClass == EnemyAttackState.self
}
}
GKAgent2D з GKGoal дозволяє реалізувати pursuit, flee, flocking без ручного програмування векторної математики. Для процедурної генерації рівнів — GKNoise та GKPerlinNoiseSource.
Типові проблеми в продакшені
Просадка FPS при появленні багатьох ворогів — частіше за все SKPhysicsBody у кожного з точними коллайдерами. Рішення: упростити коллайдери до circleOfRadius або rectangleOf, точну физику столкновення робити лише для гравця.
Утічки пам'яті при зміні сцен — SKScene не освобождается, якщо залишилися невідмінені SKAction з сильними посиланнями на об'єкти. Завжди вызивувати removeAllActions() в willMove(from:).
Текстури не виконуються — SKTextureAtlas тримається в пам'яті, поки хоча б один SKSpriteNode використовує його текстуру. При зміні рівня явно замінюємо текстури нодів на SKTexture() перед видаленням сцени, потім вызивувати removeFromParent().
Етапи роботи
Аудит ТЗ: жанр, кількість рівнів, монетизація (IAP, реклама), цільові пристрої, iOS-мінімум.
Прототип: core gameplay loop за першу тиждень — саме в цей момент зрозуміло, варто йти далі з SpriteKit або потрібен Unity.
Розробка: сцени, ігрова механіка, фізика, AI, звук, UI (окремана SKScene поверх ігрової або UIKit-оверлей через SKView).
Інтеграція Game Center: таблиці рекордів, досягнення.
Тестування на реальних пристроях: iPhone SE 2gen (слабий GPU), iPad Pro (великий екран, інший aspect ratio).
Публікація: App Store Connect, вікова оцінка, метадані.
Орієнтири по термінах
| Складність гри | Термін |
|---|---|
| Проста казуалка (1-3 механіки, 5-10 рівнів) | 2–4 тижні |
| Середній проект (10+ рівнів, AI враги, IAP) | 1,5–2 місяці |
| Повнофункціональна гра з контентом | 2–3 місяці |
Терміни сильно залежать від обсягу контенту (графіка, звук) — якщо ассеты готові, розробка швидша. Якщо потрібно створювати з нуля — додаємо час на дизайн.







