# Spritekit Game

> Use for 2D games: arcade, puzzle, sports, ping pong, platformer, shooter, 2D racing. SpriteKit + SpriteView architecture, scene hierarchy, physics, game loop, audio, particles, game feel.

- Skill: `abdullah4ai/spritekit-game` (Agent Skill)
- Install (CLI): `npx skillmds@latest add abdullah4ai/spritekit-game`
- Raw SKILL.md: https://api.skillmd.com/api/skills/abdullah4ai/spritekit-game/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: abdullah4ai (https://skillmd.com/u/abdullah4ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/abdullah4ai/spritekit-game

---

# SpriteKit 2D Game Development

**This skill is for 2D games only.** For 3D games (racing, bowling, board games, marble maze, tower defense, 3D sports), load the `scenekit-game` skill instead.

**ARCHITECTURE OVERRIDE**: 2D games use SpriteKit (SKScene + SpriteView), NOT plain SwiftUI views with timers or Canvas. The standard @main App → RootView → MainView pattern still applies, but MainView hosts a SpriteView instead of standard SwiftUI content. AppTheme rules apply to SwiftUI parts (menus, overlays) but NOT to SpriteKit scene internals where you use SKColor, SKLabelNode fonts, etc. directly.

Also load the `game-assets` skill for downloading sprites and textures via `nw_download_asset`.

## SpriteView — SwiftUI Bridge

```swift
import SpriteKit
import SwiftUI

struct GameView: View {
    @State var gameState = GameState()

    var body: some View {
        ZStack {
            SpriteView(scene: makeScene(), isPaused: gameState.isPaused)
                .ignoresSafeArea()

            // SwiftUI overlays for HUD, menus (use AppTheme here)
            if gameState.isPaused {
                PauseMenuView(gameState: gameState)
            }
        }
    }

    private func makeScene() -> GameScene {
        let scene = GameScene(size: UIScreen.main.bounds.size)
        scene.scaleMode = .resizeFill
        scene.gameState = gameState
        return scene
    }
}
```

Rules:
- Use `.scaleMode = .resizeFill` so the scene adapts to actual view size
- Pass data between SwiftUI and SpriteKit via `@Observable` game state objects
- SwiftUI overlays (menus, HUD, pause) sit in a ZStack above SpriteView
- Use `SpriteView(scene:, isPaused:)` to control pause from SwiftUI
- SwiftUI parts use AppTheme. SpriteKit scene internals use SKColor/SKLabelNode directly.

## Scene Architecture

Layer-based node hierarchy — create in `didMove(to:)`:

```
SKScene (GameScene)
├── backgroundLayer (SKNode) — z: -100
├── gameplayLayer (SKNode)  — z: 0
└── hudLayer (SKNode)       — z: 100
```

Rules:
- Set `zPosition` on layers, not individual sprites
- Camera: attach `SKCameraNode` to gameplayLayer, set `scene.camera`
- Scene transitions: `view?.presentScene(newScene, transition: .push(with: .up, duration: 0.3))`
- Clean up in `willMove(from:)` — remove actions, nil out references

## Physics

```swift
struct PhysicsCategory {
    static let none:    UInt32 = 0
    static let player:  UInt32 = 0x1 << 0
    static let enemy:   UInt32 = 0x1 << 1
    static let ball:    UInt32 = 0x1 << 2
    static let wall:    UInt32 = 0x1 << 3
}
```

Rules:
- Set `categoryBitMask`, `contactTestBitMask`, `collisionBitMask` on every physics body
- Implement `SKPhysicsContactDelegate` — use Double Dispatch (delegate to nodes, not massive if-else)
- Use rectangle/circle bodies for performance — avoid texture-based bodies
- Enable `usesPreciseCollisionDetection = true` only for fast objects (balls, bullets)
- Set `physicsWorld.gravity = .zero` for top-down games, `CGVector(dx: 0, dy: -9.8)` for platformers

## Game Loop

```swift
private var lastUpdateTime: TimeInterval = 0

override func update(_ currentTime: TimeInterval) {
    let dt = lastUpdateTime == 0 ? 0 : currentTime - lastUpdateTime
    lastUpdateTime = currentTime

    updateGameLogic(deltaTime: dt)
}
```

Rules:
- Always calculate delta time — never assume fixed frame rate
- `update()` → game logic, `didEvaluateActions()` → post-action, `didSimulatePhysics()` → position corrections
- Use `@MainActor @objc` for CADisplayLink targets in Swift 6

## Game State with @Observable

```swift
@Observable
class GameState {
    var score: Int = 0
    var lives: Int = 3
    var isPaused: Bool = false
    var phase: GamePhase = .menu

    enum GamePhase {
        case menu, playing, paused, gameOver
    }
}
```

Share between SwiftUI and SKScene — SKScene updates properties, SwiftUI reacts automatically.

## Audio — Procedural Sound Effects

Generate sounds programmatically — no audio files needed:

```swift
import AVFoundation

func generateTone(frequency: Float, duration: Float) -> AVAudioPlayer? {
    let sampleRate: Float = 44100
    let samples = Int(sampleRate * duration)
    var data = Data()

    // WAV header
    let dataSize = samples * 2
    let fileSize = 36 + dataSize
    data.append(contentsOf: "RIFF".utf8)
    data.append(contentsOf: withUnsafeBytes(of: Int32(fileSize).littleEndian) { Array($0) })
    data.append(contentsOf: "WAVEfmt ".utf8)
    data.append(contentsOf: withUnsafeBytes(of: Int32(16).littleEndian) { Array($0) })
    data.append(contentsOf: withUnsafeBytes(of: Int16(1).littleEndian) { Array($0) })  // PCM
    data.append(contentsOf: withUnsafeBytes(of: Int16(1).littleEndian) { Array($0) })  // mono
    data.append(contentsOf: withUnsafeBytes(of: Int32(44100).littleEndian) { Array($0) })
    data.append(contentsOf: withUnsafeBytes(of: Int32(88200).littleEndian) { Array($0) })
    data.append(contentsOf: withUnsafeBytes(of: Int16(2).littleEndian) { Array($0) })
    data.append(contentsOf: withUnsafeBytes(of: Int16(16).littleEndian) { Array($0) })
    data.append(contentsOf: "data".utf8)
    data.append(contentsOf: withUnsafeBytes(of: Int32(dataSize).littleEndian) { Array($0) })

    for i in 0..<samples {
        let t = Float(i) / sampleRate
        let envelope = max(0, 1.0 - t / duration)
        let sample = Int16(sin(2.0 * .pi * frequency * t) * Float(Int16.max) * envelope * 0.5)
        data.append(contentsOf: withUnsafeBytes(of: sample.littleEndian) { Array($0) })
    }

    return try? AVAudioPlayer(data: data)
}
```

Common game sounds:
- Hit/bounce: 440-880 Hz, 0.05-0.1s duration
- Score: 660 Hz then 880 Hz sequence, 0.1s each
- Game over: 220 Hz descending, 0.3s duration
- Serve/launch: 330 Hz rising, 0.15s duration

## Particle Effects

Create particles in code (no .sks files needed):

```swift
func createHitParticles(at position: CGPoint) -> SKEmitterNode {
    let emitter = SKEmitterNode()
    emitter.particleBirthRate = 200
    emitter.numParticlesToEmit = 20
    emitter.particleLifetime = 0.3
    emitter.particleSpeed = 150
    emitter.particleSpeedRange = 50
    emitter.emissionAngleRange = .pi * 2
    emitter.particleScale = 0.1
    emitter.particleScaleRange = 0.05
    emitter.particleAlphaSpeed = -3.0
    emitter.particleColor = .white
    emitter.position = position
    // Auto-remove after emission completes
    emitter.run(.sequence([.wait(forDuration: 0.5), .removeFromParent()]))
    return emitter
}
```

## Game Feel — Juice Effects

Screen shake on impact:
```swift
func screenShake(intensity: CGFloat = 8, duration: TimeInterval = 0.15) {
    let shake = SKAction.sequence([
        SKAction.moveBy(x: intensity, y: intensity, duration: duration / 4),
        SKAction.moveBy(x: -intensity * 2, y: -intensity, duration: duration / 4),
        SKAction.moveBy(x: intensity, y: -intensity, duration: duration / 4),
        SKAction.moveBy(x: 0, y: intensity, duration: duration / 4),
    ])
    camera?.run(shake)
}
```

Hit pause (freeze frame) — 50-100ms pause on impact:
```swift
func hitPause(duration: TimeInterval = 0.05) {
    isPaused = true
    DispatchQueue.main.asyncAfter(deadline: .now() + duration) { self.isPaused = false }
}
```

Haptic feedback:
```swift
let impact = UIImpactFeedbackGenerator(style: .medium)
impact.impactOccurred()
```

## Performance Rules

- **SKSpriteNode** for all game objects — SKShapeNode is slow (drops FPS significantly)
- Convert shapes to textures: `let texture = view.texture(from: shapeNode)`
- Preload textures before gameplay to avoid frame drops
- Use texture atlases for sprite sheets (group by scene/level, not one giant atlas)
- Verify scene deallocation: override `deinit` during development
- Static bodies for walls/platforms, dynamic only for moving objects
- Disable physics on off-screen nodes

## Genre-Specific Patterns

**Sports/Pong**: `gravity = .zero`, rectangle paddles, ball with `restitution = 1.0`, AI tracks ball with delay
**Platformer**: `gravity = CGVector(dx: 0, dy: -9.8)`, `applyImpulse()` for jumps, ground detection via contact
**Endless Runner**: Parallax scrolling with duplicate background sprites leap-frogging, SKCameraNode follows player
**Puzzle/Match-3**: Grid data structure, nested loop match detection, SKAction chains for animations
**Shooter**: Contact (not collision) tests for bullets, spawn timers for enemy waves, projectile pooling

## Game Assets

For visual assets (sprites, backgrounds, textures), load the `game-assets` skill to use the `nw_download_asset` tool. For sounds, generate programmatically as shown above.

