SpriteKit is Apple's 2D game framework. Learn to create engaging 2D games with physics, animations, and interactivity.
Basic Scene Setup
Create Game Scene
swift
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
backgroundColor = SKColor.black
// Setup game
setupPlayer()
setupEnemies()
setupUI()
}
override func update(_ currentTime: TimeInterval) {
// Game logic updates
}
}SpriteKit View
swift
struct GameView: UIViewRepresentable {
func makeUIView(context: Context) -> SKView {
let skView = SKView()
skView.ignoresSiblingOrder = true
skView.showsFPS = true
skView.showsNodeCount = true
let scene = GameScene(size: skView.bounds.size)
scene.scaleMode = .resizeFill
skView.presentScene(scene)
return skView
}
func updateUIView(_ uiView: SKView, context: Context) {}
}Sprites and Textures
Create Sprites
swift
class GameScene: SKScene {
func setupPlayer() {
let player = SKSpriteNode(imageNamed: "player")
player.position = CGPoint(x: size.width / 2, y: size.height / 2)
player.name = "player"
player.zPosition = 10
addChild(player)
}
func createAnimatedSprite() {
let textureAtlas = SKTextureAtlas(named: "player_animations")
let textures = [
textureAtlas.textureNamed("frame1"),
textureAtlas.textureNamed("frame2"),
textureAtlas.textureNamed("frame3")
]
let player = SKSpriteNode(texture: textures[0])
player.position = CGPoint(x: 200, y: 200)
let animation = SKAction.animate(with: textures, timePerFrame: 0.1)
let repeatAnimation = SKAction.repeatForever(animation)
player.run(repeatAnimation)
addChild(player)
}
}Physics
Physics Body
swift
class PhysicsScene: SKScene {
func setupPhysics() {
physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
// Add physics bodies
addPlayerPhysics()
addWalls()
}
func addPlayerPhysics() {
if let player = childNode(withName: "player") as? SKSpriteNode {
player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
player.physicsBody?.categoryBitMask = PhysicsCategory.player
player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy | PhysicsCategory.ground
player.physicsBody?.collisionBitMask = PhysicsCategory.all
player.physicsBody?.affectedByGravity = true
player.physicsBody?.allowsRotation = false
}
}
func addWalls() {
let ground = SKNode()
ground.position = CGPoint(x: size.width / 2, y: 0)
ground.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: size.width, height: 1))
ground.physicsBody?.categoryBitMask = PhysicsCategory.ground
ground.physicsBody?.isDynamic = false
addChild(ground)
}
}
struct PhysicsCategory {
static let player: UInt32 = 0x1 << 0
static let enemy: UInt32 = 0x1 << 1
static let ground: UInt32 = 0x1 << 2
static let projectile: UInt32 = 0x1 << 3
static let all: UInt32 = 0xFFFFFFFF
}Contact Detection
swift
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
}
func didBegin(_ contact: SKPhysicsContact) {
let collision: UInt32 = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
if collision == PhysicsCategory.player | PhysicsCategory.enemy {
handlePlayerEnemyCollision(contact: contact)
}
if collision == PhysicsCategory.projectile | PhysicsCategory.enemy {
handleProjectileEnemyCollision(contact: contact)
}
}
func handlePlayerEnemyCollision(contact: SKPhysicsContact) {
// Player hit enemy
print("Player hit enemy!")
}
func handleProjectileEnemyCollision(contact: SKPhysicsContact) {
// Projectile hit enemy
if let enemy = contact.bodyA.node?.name == "enemy" ? contact.bodyA.node : contact.bodyB.node {
enemy.removeFromParent()
}
}
}Actions and Animations
SpriteKit Actions
swift
class AnimationScene: SKScene {
func createActions() {
let sprite = SKSpriteNode(color: .red, size: CGSize(width: 50, height: 50))
sprite.position = CGPoint(x: 100, y: 100)
addChild(sprite)
// Move action
let moveAction = SKAction.move(to: CGPoint(x: 300, y: 300), duration: 2.0)
// Rotate action
let rotateAction = SKAction.rotate(byAngle: .pi * 2, duration: 1.0)
// Scale action
let scaleUp = SKAction.scale(to: 1.5, duration: 0.5)
let scaleDown = SKAction.scale(to: 1.0, duration: 0.5)
let scaleSequence = SKAction.sequence([scaleUp, scaleDown])
// Fade action
let fadeOut = SKAction.fadeOut(withDuration: 0.5)
let fadeIn = SKAction.fadeIn(withDuration: 0.5)
let fadeSequence = SKAction.sequence([fadeOut, fadeIn])
// Group actions
let moveAndRotate = SKAction.group([moveAction, rotateAction])
// Sequence actions
let fullSequence = SKAction.sequence([moveAndRotate, scaleSequence, fadeSequence])
sprite.run(fullSequence)
}
func spawnEnemies() {
let spawn = SKAction.run { [weak self] in
self?.spawnEnemy()
}
let wait = SKAction.wait(forDuration: 1.0)
let spawnSequence = SKAction.sequence([spawn, wait])
let repeatSpawn = SKAction.repeatForever(spawnSequence)
run(repeatSpawn)
}
func spawnEnemy() {
let enemy = SKSpriteNode(imageNamed: "enemy")
enemy.position = CGPoint(x: size.width / 2, y: size.height - 50)
let moveDown = SKAction.moveTo(y: -50, duration: 3.0)
let remove = SKAction.removeFromParent()
let sequence = SKAction.sequence([moveDown, remove])
enemy.run(sequence)
addChild(enemy)
}
}Input Handling
Touch Input
swift
class GameScene: SKScene {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
let touchedNode = atPoint(location)
if touchedNode.name == "button" {
handleButtonTap()
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
if let player = childNode(withName: "player") {
player.position = location
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// Handle touch end
}
func handleButtonTap() {
// Button action
print("Button tapped!")
}
}Game State Management
Game States
swift
enum GameState {
case menu
case playing
case paused
case gameOver
}
class GameManager: ObservableObject {
@Published var gameState: GameState = .menu
@Published var score: Int = 0
@Published var lives: Int = 3
func startGame() {
gameState = .playing
score = 0
lives = 3
}
func pauseGame() {
gameState = .paused
}
func resumeGame() {
gameState = .playing
}
func gameOver() {
gameState = .gameOver
}
func addScore(_ points: Int) {
score += points
}
func loseLife() {
lives -= 1
if lives <= 0 {
gameOver()
}
}
}Particle Systems
Create Effects
swift
class EffectsScene: SKScene {
func createExplosion(at position: CGPoint) {
let particles = SKEmitterNode()
particles.position = position
particles.particleTexture = SKTexture(imageNamed: "spark")
particles.numParticlesToEmit = 50
particles.particleLifetime = 0.5
particles.particleScale = 0.1
particles.particleScaleSpeed = -0.05
particles.particleAlpha = 1.0
particles.particleAlphaSpeed = -2.0
particles.emissionAngle = 0
particles.emissionAngleRange = .pi * 2
particles.particleSpeed = 100
particles.particleSpeedRange = 50
addChild(particles)
// Remove after animation
let wait = SKAction.wait(forDuration: 1.0)
let remove = SKAction.removeFromParent()
particles.run(SKAction.sequence([wait, remove]))
}
func createTrail(for node: SKNode) {
let trail = SKNode()
node.addChild(trail)
let emitNode = SKEmitterNode()
emitNode.position = CGPoint(x: -node.frame.size.width / 2, y: 0)
trail.addChild(emitNode)
// Configure trail
emitNode.numParticlesToEmit = 0
emitNode.particleTexture = SKTexture(imageNamed: "trail")
emitNode.particleLifetime = 0.3
emitNode.particleScale = 0.2
}
}Audio
Sound Effects
swift
class AudioManager {
private var audioEngine: AVAudioEngine?
func playSound(filename: String) {
guard let url = Bundle.main.url(forResource: filename, withExtension: "wav") else {
return
}
var soundID: SystemSoundID = 0
AudioServicesCreateSystemSoundID(url as CFURL, &soundID)
AudioServicesPlaySystemSound(soundID)
}
func playMusic(filename: String) {
guard let url = Bundle.main.url(forResource: filename, withExtension: "mp3") else {
return
}
do {
audioEngine = AVAudioEngine()
let player = AVAudioPlayerNode()
audioEngine?.attach(player)
let file = try AVAudioFile(forReading: url)
let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: UInt32(file.length))
try file.read(into: buffer)
player.scheduleBuffer(buffer, at: nil, options: .loops)
audioEngine?.start()
} catch {
print("Error playing music: \(error)")
}
}
}Best Practices
- 1Performance: Monitor frame rate
- 2Memory: Manage texture memory
- 3Physics: Use simple physics when possible
- 4Optimization: Use sprite atlases
- 5Testing: Test on different devices
- 6Polish: Add visual feedback
- 7Audio: Balance sound levels
- 8Fun: Focus on gameplay
SpriteKit makes 2D game development fun and accessible!