spritekit
Original:🇺🇸 English
Translated
Build 2D games and animations using SpriteKit. Use when creating game scenes with SKScene and SKView, adding sprites with SKSpriteNode, animating with SKAction sequences, simulating physics with SKPhysicsBody and contact detection, creating particle effects with SKEmitterNode, building tile maps, using SKCameraNode, or integrating SpriteKit scenes in SwiftUI with SpriteView.
10installs
Added on
NPX Install
npx skill4agent add dpearson2699/swift-ios-skills spritekitTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →SpriteKit
Build 2D games and interactive animations for iOS 26+ using SpriteKit and
Swift 6.3. Covers scene lifecycle, node hierarchy, actions, physics, particles,
camera, touch handling, and SwiftUI integration.
Contents
- Scene Setup
- Nodes and Sprites
- Actions and Animation
- Physics
- Touch Handling
- Camera
- Particle Effects
- SwiftUI Integration
- Common Mistakes
- Review Checklist
- References
Scene Setup
SpriteKit renders content through , which presents an -- the
root node of a tree that the framework animates and renders each frame.
SKViewSKSceneCreating a Scene
Subclass and override lifecycle methods. The coordinate system
origin is at the bottom-left by default.
SKSceneswift
import SpriteKit
final class GameScene: SKScene {
override func didMove(to view: SKView) {
backgroundColor = .darkGray
physicsWorld.contactDelegate = self
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
setupNodes()
}
override func update(_ currentTime: TimeInterval) {
// Called once per frame before actions are evaluated.
}
}Presenting a Scene (UIKit)
swift
guard let skView = view as? SKView else { return }
skView.ignoresSiblingOrder = true
let scene = GameScene(size: skView.bounds.size)
scene.scaleMode = .resizeFill
skView.presentScene(scene)Scale Modes
Use when the scene should adapt to view size changes (rotation,
multitasking). Use for fixed-design game scenes.
letterboxes; stretches and may distort.
.resizeFill.aspectFill.aspectFit.fillFrame Cycle
Each frame follows this order:
- -- game logic
update(_:) - Evaluate actions
- -- post-action logic
didEvaluateActions() - Simulate physics
- -- post-physics adjustments
didSimulatePhysics() - Apply constraints
didApplyConstraints()- -- final adjustments before rendering
didFinishUpdate()
Override only the callbacks where work is needed.
Nodes and Sprites
Use (without a visual) as an invisible container or layout group.
Child nodes inherit parent position, scale, rotation, alpha, and speed.
is the primary visual node.
SKNodeSKSpriteNodeCommon Node Types
| Class | Purpose |
|---|---|
| Textured image or solid color |
| Text rendering |
| Vector paths (expensive per draw call) |
| Particle effects |
| Viewport control |
| Grid-based tiles |
| Positional audio |
| Masking / CIFilter |
| Embedded SceneKit content |
Creating Sprites
swift
let player = SKSpriteNode(imageNamed: "hero")
player.position = CGPoint(x: frame.midX, y: frame.midY)
player.name = "player"
addChild(player)Drawing Order
Set on for better performance; SpriteKit
then uses to determine order. Without it, nodes draw in tree order.
ignoresSiblingOrder = trueSKViewzPositionswift
background.zPosition = -1
player.zPosition = 0
foregroundUI.zPosition = 10Naming and Searching
Assign to find nodes without instance variables. Use ,
, or . Patterns: searches
the entire tree, matches any characters, refers to the parent.
namechildNode(withName:)enumerateChildNodes(withName:using:)subscript//*..swift
player.name = "player"
if let found = childNode(withName: "player") as? SKSpriteNode { /* ... */ }Actions and Animation
SKActionnode.run(_:)Basic Actions
swift
let moveUp = SKAction.moveBy(x: 0, y: 100, duration: 0.5)
let grow = SKAction.scale(to: 1.5, duration: 0.3)
let spin = SKAction.rotate(byAngle: .pi * 2, duration: 1.0)
let fadeOut = SKAction.fadeOut(withDuration: 0.3)
let remove = SKAction.removeFromParent()Combining Actions
swift
// Sequential: run one after another
let dropAndRemove = SKAction.sequence([
SKAction.moveBy(x: 0, y: -500, duration: 1.0),
SKAction.removeFromParent()
])
// Parallel: run simultaneously
let scaleAndFade = SKAction.group([
SKAction.scale(to: 0.0, duration: 0.3),
SKAction.fadeOut(withDuration: 0.3)
])
// Repeat
let pulse = SKAction.repeatForever(
SKAction.sequence([
SKAction.scale(to: 1.2, duration: 0.5),
SKAction.scale(to: 1.0, duration: 0.5)
])
)Texture Animation
swift
let walkFrames = (1...8).map { SKTexture(imageNamed: "walk_\($0)") }
let walkAction = SKAction.animate(with: walkFrames, timePerFrame: 0.1)
player.run(SKAction.repeatForever(walkAction))Control the speed curve with (, , ,
). Assign keys to actions for later access:
timingMode.linear.easeIn.easeOut.easeInEaseOutswift
let easeIn = SKAction.moveTo(x: 300, duration: 1.0)
easeIn.timingMode = .easeInEaseOut
player.run(pulse, withKey: "pulse")
player.removeAction(forKey: "pulse") // stop laterPhysics
SpriteKit provides a built-in 2D physics engine. The scene's
manages gravity and collision detection.
physicsWorldAdding Physics Bodies
swift
// Circle body
player.physicsBody = SKPhysicsBody(circleOfRadius: player.size.width / 2)
player.physicsBody?.restitution = 0.3
// Static rectangle
ground.physicsBody = SKPhysicsBody(rectangleOf: ground.size)
ground.physicsBody?.isDynamic = false
// Texture-based body for irregular shapes
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)Category and Contact Masks
Use bit masks to control collisions and contact callbacks:
swift
struct PhysicsCategory {
static let player: UInt32 = 0b0001
static let enemy: UInt32 = 0b0010
static let ground: UInt32 = 0b0100
}
player.physicsBody?.categoryBitMask = PhysicsCategory.player
player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
player.physicsBody?.collisionBitMask = PhysicsCategory.groundcategoryBitMaskcollisionBitMaskcontactTestBitMaskdidBegindidEndContact Detection
Implement and set
in :
SKPhysicsContactDelegatephysicsWorld.contactDelegate = selfdidMove(to:)swift
extension GameScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
if mask == PhysicsCategory.player | PhysicsCategory.enemy {
handlePlayerHit(contact)
}
}
}Forces and Impulses
swift
player.physicsBody?.applyForce(CGVector(dx: 0, dy: 50)) // continuous
player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 200)) // instant
player.physicsBody?.applyAngularImpulse(0.5) // spinUse for jumps and projectile launches. Configure gravity with
and per-body with
.
.applyImpulsephysicsWorld.gravity = CGVector(dx: 0, dy: -9.8)affectedByGravityTouch Handling
SKSceneUIRespondertouchesBegantouchesMovedtouchesEndednodes(at:)swift
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
let tappedNodes = nodes(at: location)
if tappedNodes.contains(where: { $0.name == "playButton" }) {
startGame()
}
}For node-level touch handling, subclass the node and set
. That node then receives touches directly
instead of the scene.
isUserInteractionEnabled = trueCamera
SKCameraNodescene.cameraswift
let cameraNode = SKCameraNode()
addChild(cameraNode)
camera = cameraNode
cameraNode.position = CGPoint(x: frame.midX, y: frame.midY)Following a Character
Update the camera position in or use constraints:
didSimulatePhysics()swift
override func didSimulatePhysics() {
cameraNode.position = player.position
}
// Constrain camera to world bounds
let xRange = SKRange(lowerLimit: frame.midX, upperLimit: worldWidth - frame.midX)
let yRange = SKRange(lowerLimit: frame.midY, upperLimit: worldHeight - frame.midY)
cameraNode.constraints = [SKConstraint.positionX(xRange, y: yRange)]Camera Zoom and HUD
Scale the camera node inversely: zooms in 2x,
zooms out 2x. Nodes added as children of the camera stay fixed on screen
(HUD elements):
setScale(0.5)setScale(2.0)swift
let scoreLabel = SKLabelNode(text: "Score: 0")
scoreLabel.position = CGPoint(x: 0, y: frame.height / 2 - 40)
scoreLabel.fontName = "AvenirNext-Bold"
scoreLabel.fontSize = 24
cameraNode.addChild(scoreLabel)Particle Effects
SKEmitterNode.sksswift
// Load from file
guard let emitter = SKEmitterNode(fileNamed: "Fire") else { return }
emitter.position = CGPoint(x: frame.midX, y: 100)
addChild(emitter)One-Shot Emitters
Set for finite effects and remove after completion:
numParticlesToEmitswift
func spawnExplosion(at position: CGPoint) {
guard let explosion = SKEmitterNode(fileNamed: "Explosion") else { return }
explosion.position = position
explosion.numParticlesToEmit = 100
addChild(explosion)
let wait = SKAction.wait(forDuration: TimeInterval(explosion.particleLifetime))
explosion.run(SKAction.sequence([wait, .removeFromParent()]))
}Set to the scene so particles stay in world space when the
emitter moves: .
targetNodeemitter.targetNode = selfSwiftUI Integration
SpriteViewswift
import SwiftUI
import SpriteKit
struct GameView: View {
@State private var scene: GameScene = {
let s = GameScene()
s.size = CGSize(width: 390, height: 844)
s.scaleMode = .resizeFill
return s
}()
var body: some View {
SpriteView(scene: scene)
.ignoresSafeArea()
}
}SpriteView Options
Pass for transparent backgrounds,
for offscreen culling, or
for -based draw order. Use
during development.
options: [.allowsTransparency].shouldCullNonVisibleNodes.ignoresSiblingOrderzPositiondebugOptions: [.showsFPS, .showsNodeCount]Communicating Between SwiftUI and the Scene
Pass data through a shared object. Store the scene in
to avoid re-creation on view re-renders:
@Observable@Stateswift
@Observable final class GameState {
var score = 0
var isPaused = false
}
struct GameContainerView: View {
@State private var gameState = GameState()
@State private var scene = GameScene()
var body: some View {
SpriteView(scene: scene, isPaused: gameState.isPaused)
.onAppear { scene.gameState = gameState }
}
}Common Mistakes
Creating a new scene on every SwiftUI re-render
swift
// DON'T: Scene is recreated on every body evaluation
var body: some View {
SpriteView(scene: GameScene(size: CGSize(width: 390, height: 844)))
}
// DO: Create once and reuse
@State private var scene = GameScene(size: CGSize(width: 390, height: 844))
var body: some View {
SpriteView(scene: scene)
}Adding a child node that already has a parent
A node can only have one parent. Remove from the current parent first or
create a separate instance. Adding a node that already has a parent crashes.
Forgetting to set contactTestBitMask
swift
// DON'T: Bodies collide but didBegin is never called
player.physicsBody?.categoryBitMask = PhysicsCategory.player
enemy.physicsBody?.categoryBitMask = PhysicsCategory.enemy
// DO: Set contactTestBitMask to receive contact callbacks
player.physicsBody?.contactTestBitMask = PhysicsCategory.enemyUsing SKShapeNode for performance-critical rendering
SKShapeNodeSKSpriteNodeNot removing nodes that leave the screen
swift
// DON'T
enemy.run(SKAction.moveBy(x: -800, y: 0, duration: 3.0))
addChild(enemy)
// DO: Remove after leaving the visible area
enemy.run(SKAction.sequence([
SKAction.moveBy(x: -800, y: 0, duration: 3.0),
SKAction.removeFromParent()
]))
addChild(enemy)Setting physicsWorld.contactDelegate too late
Set in , not in
or after a delay.
physicsWorld.contactDelegate = selfdidMove(to:)update(_:)Review Checklist
- Scene subclass overrides for setup, not
didMove(to:)init - chosen appropriately for the game's design
scaleMode - set to
ignoresSiblingOrderontruefor performanceSKView - used consistently when
zPositionis enabledignoresSiblingOrder - Physics set in
contactDelegatedidMove(to:) - Category, collision, and contact bit masks configured correctly
- set for any pair needing
contactTestBitMask/didBegincallbacksdidEnd - Static bodies use
isDynamic = false - avoided in performance-critical paths;
SKShapeNodepreferredSKSpriteNode - Actions that move nodes offscreen include in sequence
.removeFromParent() - One-shot emitters remove themselves after particle lifetime expires
- Emitter set when particles should stay in world space
targetNode - Scene stored in when used with
@Statein SwiftUISpriteView - Texture atlases used for related sprites to reduce draw calls
- uses delta time for frame-rate-independent movement
update(_:) - Nodes removed from parent before being re-added elsewhere
References
- See references/spritekit-patterns.md for tile maps, texture atlases, shaders, scene transitions, game loop patterns, audio, and SceneKit embedding.
- SpriteKit documentation
- SKScene
- SKSpriteNode
- SKAction
- SKPhysicsBody
- SKEmitterNode
- SKCameraNode
- SpriteView
- SKTileMapNode