SpriteKit Game Development Guide
Purpose: Build reliable SpriteKit games by mastering the scene graph, physics engine, action system, and rendering pipeline
iOS Version: iOS 13+ (SwiftUI integration), iOS 11+ (SKRenderer)
Xcode: Xcode 15+
When to Use This Skill
Use this skill when:
- Building a new SpriteKit game or interactive simulation
- Implementing physics (collisions, contacts, forces, joints)
- Setting up game architecture (scenes, layers, cameras)
- Optimizing frame rate or reducing draw calls
- Implementing touch/input handling in a game
- Managing scene transitions and data passing
- Integrating SpriteKit with SwiftUI or Metal
- Debugging physics contacts that don't fire
- Fixing coordinate system confusion
Do NOT use this skill for:
- SceneKit 3D rendering (
axiom-scenekit)
- GameplayKit entity-component systems
- Metal shader programming (
axiom-metal-migration-ref)
- General SwiftUI layout (
axiom-swiftui-layout)
1. Mental Model
Coordinate System
SpriteKit uses a bottom-left origin with Y pointing up. This differs from UIKit (top-left, Y down).
SpriteKit: UIKit:
βββββββββββ βββββββββββ
β +Y β β (0,0) β
β β β β β β
β β β β +Y β
β(0,0)βββ+Xβ β β β
βββββββββββ βββββββββββ
Anchor Points define which point on a sprite maps to its position. Default is (0.5, 0.5) (center).
sprite.anchorPoint = CGPoint(x: 0.5, y: 0)
Scene anchor point maps the view's frame to scene coordinates:
(0, 0) β scene origin at bottom-left of view (default)
(0.5, 0.5) β scene origin at center of view
Node Tree
Everything in SpriteKit is an SKNode in a tree hierarchy. Parent transforms propagate to children.
SKScene
βββ SKCameraNode (viewport control)
βββ SKNode "world" (game content layer)
β βββ SKSpriteNode "player"
β βββ SKSpriteNode "enemy"
β βββ SKNode "platforms"
β βββ SKSpriteNode "platform1"
β βββ SKSpriteNode "platform2"
βββ SKNode "hud" (UI layer, attached to camera)
βββ SKLabelNode "score"
βββ SKSpriteNode "healthBar"
Z-Ordering
zPosition controls draw order. Higher values render on top. Nodes at the same zPosition render in child array order (unless ignoresSiblingOrder is true).
enum ZLayer {
static let background: CGFloat = -100
static let platforms: CGFloat = 0
static let items: CGFloat = 10
static let player: CGFloat = 20
static let effects: CGFloat = 30
static let hud: CGFloat = 100
}
2. Scene Architecture
Scale Mode Decision
| Mode |
Behavior |
Use When |
.aspectFill |
Fills view, crops edges |
Full-bleed games (most games) |
.aspectFit |
Fits in view, letterboxes |
Puzzle games needing exact layout |
.resizeFill |
Stretches to fill |
Almost never β distorts |
.fill |
Matches view size exactly |
Scene adapts to any ratio |
class GameScene: SKScene {
override func sceneDidLoad() {
scaleMode = .aspectFill
}
}
Camera Node Pattern
Always use SKCameraNode for viewport control. Attach HUD elements to the camera so they don't scroll.
let camera = SKCameraNode()
camera.name = "mainCamera"
addChild(camera)
self.camera = camera
let scoreLabel = SKLabelNode(text: "Score: 0")
scoreLabel.position = CGPoint(x: 0, y: size.height / 2 - 50)
camera.addChild(scoreLabel)
let follow = SKConstraint.distance(SKRange(constantValue: 0), to: playerNode)
camera.constraints = [follow]
Layer Organization
let worldNode = SKNode()
worldNode.name = "world"
addChild(worldNode)
let hudNode = SKNode()
hudNode.name = "hud"
camera?.addChild(hudNode)
worldNode.addChild(playerSprite)
worldNode.addChild(enemySprite)
hudNode.addChild(scoreLabel)
Scene Transitions
guard let nextScene = LevelScene(fileNamed: "Level2") else { return }
nextScene.scaleMode = .aspectFill
let transition = SKTransition.fade(withDuration: 0.5)
view?.presentScene(nextScene, transition: transition)
Data passing between scenes: Use a shared game state object, not node properties.
class GameState {
static let shared = GameState()
var score = 0
var currentLevel = 1
var playerHealth = 100
}
let nextScene = LevelScene(size: size)
view?.presentScene(nextScene, transition: .fade(withDuration: 0.5))
Note: A singleton works for simple games. For larger projects with testing needs, consider passing a GameState instance through scene initializers to avoid hidden global state.
Cleanup in willMove(from:):
override func willMove(from view: SKView) {
removeAllActions()
removeAllChildren()
physicsWorld.contactDelegate = nil
}
3. Physics Engine
Bitmask Discipline
This is the #1 source of SpriteKit bugs. Physics bitmasks use a 32-bit system where each bit represents a category.
struct PhysicsCategory {
static let none: UInt32 = 0
static let player: UInt32 = 0b0001
static let enemy: UInt32 = 0b0010
static let ground: UInt32 = 0b0100
static let projectile: UInt32 = 0b1000
static let powerUp: UInt32 = 0b10000
}
Three bitmask properties (all default to 0xFFFFFFFF β everything):
| Property |
Purpose |
Default |
categoryBitMask |
What this body IS |
0xFFFFFFFF |
collisionBitMask |
What it BOUNCES off |
0xFFFFFFFF |
contactTestBitMask |
What TRIGGERS delegate |
0x00000000 |
The default collisionBitMask of 0xFFFFFFFF means everything collides with everything. This is the most common source of unexpected physics behavior.
player.physicsBody?.categoryBitMask = PhysicsCategory.player
player.physicsBody?.collisionBitMask = PhysicsCategory.ground | PhysicsCategory.enemy
player.physicsBody?.contactTestBitMask =