spritekit

spritekit

熱門

使用 SpriteKit 開發 2D 遊戲與動畫。適用於使用 SKScene 與 SKView 建立遊戲場景、透過 SKSpriteNode 新增精靈(sprite)、利用 SKAction 序列製作動畫、使用 SKPhysicsBody 與碰撞檢測模擬物理效果、透過 SKEmitterNode 建立粒子特效、構建瓦片地圖(tile map)、使用 SKCameraNode,以及在 SwiftUI 中透過 SpriteView 整合 SpriteKit 場景。

961星標
48分支
更新於 2026/7/31
SKILL.md
唯讀
名稱
spritekit
描述

使用 SpriteKit 開發 2D 遊戲與動畫。適用於使用 SKScene 與 SKView 建立遊戲場景、透過 SKSpriteNode 新增精靈(sprite)、利用 SKAction 序列製作動畫、使用 SKPhysicsBody 與碰撞檢測模擬物理效果、透過 SKEmitterNode 建立粒子特效、構建瓦片地圖(tile map)、使用 SKCameraNode,以及在 SwiftUI 中透過 SpriteView 整合 SpriteKit 場景。

SpriteKit

使用 SpriteKit 與 Swift 6.3 為 iOS 26+ 開發 2D 遊戲和互動式動畫。涵蓋場景生命週期、節點階層、動作、物理引擎、粒子特效、攝影機、觸控處理以及 SwiftUI 整合。

目錄

場景設定

SpriteKit 透過 SKView 渲染內容,而 SKView 負責呈現 SKScene —— SKScene 是樹狀結構的根節點,框架在每一幀(frame)都會對此樹狀結構進行動畫處理與繪製。

建立場景

繼承 SKScene 並覆寫生命週期方法。預設的座標系原點位於左下角。

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) {
        // 在評估動作前,每一幀都會呼叫一次此方法。
    }
}

呈現場景 (UIKit)

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)

當場景需要適應視圖尺寸變化(如旋轉、多工處理)時,請使用 .resizeFill。對於固定設計尺寸的遊戲場景,請使用 .aspectFill.aspectFit 會加上黑邊(letterboxing);.fill 則會拉伸圖像且可能造成變形。

畫格週期 (Frame Cycle)

每一幀都遵循以下執行順序:

  1. update(_:) —— 遊戲邏輯
  2. 評估動作 (Evaluate actions)
  3. didEvaluateActions() —— 動作評估完畢後的邏輯
  4. 模擬物理效果
  5. didSimulatePhysics() —— 物理模擬完成後的調整
  6. 套用約束 (Apply constraints)
  7. didApplyConstraints()
  8. didFinishUpdate() —— 繪製前的最終調整

僅覆寫有實作需求的回呼函式(callbacks)。

節點與精靈

使用 SKNode(無視覺呈現)作為隱形的容器或佈局分組。子節點會繼承父節點的位置、縮放、旋轉、透明度(alpha)與速度。SKSpriteNode 則是主要的視覺節點。

常見節點類型

類別 用途
SKSpriteNode 紋理圖像(Textured image)或純色區塊
SKLabelNode 文字繪製
SKShapeNode 向量路徑(每次 Draw Call 開銷較大)
SKEmitterNode 粒子特效
SKCameraNode 視埠(Viewport)控制
SKTileMapNode 基於網格的瓦片(Tile)地圖
SKAudioNode 位置音訊(Positional audio)
SKCropNode / SKEffectNode 遮罩 / CIFilter 特效
SK3DNode 嵌入 SceneKit 內容

建立精靈

let player = SKSpriteNode(imageNamed: "hero")
player.position = CGPoint(x: frame.midX, y: frame.midY)
player.name = "player"
addChild(player)

繪製順序

SKView 上設定 ignoresSiblingOrder = true 以獲得更佳的效能;屆時 SpriteKit 會使用 zPosition 來決定繪製順序。若未開啟此設定,節點將依照樹狀結構順序繪製。

background.zPosition = -1
player.zPosition = 0
foregroundUI.zPosition = 10

命名與搜尋

為節點指派 name 即可在不使用實體變數的情況下搜尋節點。可以使用 childNode(withName:)enumerateChildNodes(withName:using:) 或下標語法(subscript)。搜尋模式說明:// 代表搜尋整棵節點樹,* 可比對任意字元,.. 代表存取父節點。

player.name = "player"
if let found = childNode(withName: "player") as? SKSpriteNode { /* ... */ }

動作與動畫

SKAction 物件定義了隨時間套用到節點上的變更。動作具有不可變性(immutable)且可重複使用。透過 node.run(_:) 執行動作。

基本動作

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()

組合動作

// 順序執行:一個接一個執行
let dropAndRemove = SKAction.sequence([
    SKAction.moveBy(x: 0, y: -500, duration: 1.0),
    SKAction.removeFromParent()
])

// 並行執行:同時執行
let scaleAndFade = SKAction.group([
    SKAction.scale(to: 0.0, duration: 0.3),
    SKAction.fadeOut(withDuration: 0.3)
])

// 循環重複
let pulse = SKAction.repeatForever(
    SKAction.sequence([
        SKAction.scale(to: 1.2, duration: 0.5),
        SKAction.scale(to: 1.0, duration: 0.5)
    ])
)

紋理動畫

let walkFrames = (1...8).map { SKTexture(imageNamed: "walk_\($0)") }
let walkAction = SKAction.animate(with: walkFrames, timePerFrame: 0.1)
player.run(SKAction.repeatForever(walkAction))

使用 timingMode.linear.easeIn.easeOut.easeInEaseOut)來控制速度曲線。為動作指定 Key 以便日後存取或管理:

let easeIn = SKAction.moveTo(x: 300, duration: 1.0)
easeIn.timingMode = .easeInEaseOut

player.run(pulse, withKey: "pulse")
player.removeAction(forKey: "pulse") // 稍後停止此動作

物理引擎

SpriteKit 內建 2D 物理引擎。場景的 physicsWorld 負責管理重力與碰撞檢測。

新增物理剛體 (Physics Bodies)

// 圓形剛體
player.physicsBody = SKPhysicsBody(circleOfRadius: player.size.width / 2)
player.physicsBody?.restitution = 0.3

// 靜態矩形剛體
ground.physicsBody = SKPhysicsBody(rectangleOf: ground.size)
ground.physicsBody?.isDynamic = false

// 用於不規則形狀的紋理剛體
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)

分類與接觸遮罩 (Category and Contact Masks)

使用位元遮罩(bit masks)來控制碰撞與接觸回呼:

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.ground

categoryBitMask 用於標示該剛體類別。collisionBitMask 控制物理碰撞反應(如彈開)。contactTestBitMask 則觸發 didBegin/didEnd 事件。

接觸檢測

實作 SKPhysicsContactDelegate 並在 didMove(to:) 中設定 physicsWorld.contactDelegate = self

extension GameScene: SKPhysicsContactDelegate {
    func didBegin(_ contact: SKPhysicsContact) {
        let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
        if mask == PhysicsCategory.player | PhysicsCategory.enemy {
            queuePlayerHit()
        }
    }
}

接觸回呼會在物理模擬期間執行。建議在 queuePlayerHit() 中設定標記(flag)或新增事件,隨後在 update(_:) 中再對節點、剛體或世界狀態進行變更。

施力與衝量 (Forces and Impulses)

player.physicsBody?.applyForce(CGVector(dx: 0, dy: 50))      // 持續施力
player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 200))   // 瞬時衝量
player.physicsBody?.applyAngularImpulse(0.5)                  // 角衝量(自轉)

跳躍與發射子彈請使用 .applyImpulse。透過 physicsWorld.gravity = CGVector(dx: 0, dy: -9.8) 設定全域重力,個別剛體則可透過 affectedByGravity 控制是否受重力影響。

觸控處理

SKScene 繼承自 UIResponder。可在場景中覆寫 touchesBegantouchesMovedtouchesEnded。使用 nodes(at:) 進行點擊測試(hit-test)。

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()
    }
}

若要在節點層級處理觸控,請建立該節點的子類別,並設定 isUserInteractionEnabled = true。該節點就會直接接收觸控事件,而不會交由場景處理。

攝影機

SKCameraNode 控制場景的可見區域。將其新增為子節點並指派給 scene.camera

let cameraNode = SKCameraNode()
addChild(cameraNode)
camera = cameraNode
cameraNode.position = CGPoint(x: frame.midX, y: frame.midY)

跟隨角色

didSimulatePhysics() 中更新攝影機位置,或使用約束(constraints):

override func didSimulatePhysics() {
    cameraNode.position = player.position
}

// 將攝影機限制在世界邊界內
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)]

攝影機縮放與 HUD

對攝影機節點進行反向縮放:setScale(0.5) 會放大 2 倍,setScale(2.0) 會縮小 2 倍。作為攝影機子節點新增的節點會固定在螢幕上(例如 HUD 介面元素):

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)

粒子特效

SKEmitterNode 用於產生粒子特效。可在 Xcode 的 SpriteKit Particle File 編輯器(.sks)中設計發射器,或直接以程式碼配置。

// 從檔案載入
guard let emitter = SKEmitterNode(fileNamed: "Fire") else { return }
emitter.position = CGPoint(x: frame.midX, y: 100)
addChild(emitter)

單次觸發的粒子發射器 (One-Shot Emitters)

針對有限次數的特效設定 numParticlesToEmit,並在發射結束後移除:

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()]))
}

targetNode 設定為場景(emitter.targetNode = self),如此一來當發射器移動時,粒子仍會維持在世界座標空間中。

SwiftUI 整合

在 SwiftUI 中透過 SpriteView 嵌入 SpriteKit 場景。

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: [.allowsTransparency] 可支援透明背景,.shouldCullNonVisibleNodes 可進行畫面上未顯示節點的剔除(offscreen culling),.ignoresSiblingOrder 則可依據 zPosition 決定繪製順序。開發期間可使用 debugOptions: [.showsFPS, .showsNodeCount]

SwiftUI 與場景之間的溝通

透過共享的 @Observable 物件傳遞資料。將場景存放在 @State 中,可避免 View 重新繪製(re-render)時重複建立場景:

@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 }
    }
}

常見錯誤

每次 SwiftUI 重新繪製時都建立新場景

// 錯誤做法:每次 body 評估時都會重新建立場景
var body: some View {
    SpriteView(scene: GameScene(size: CGSize(width: 390, height: 844)))
}

// 正確做法:建立一次並重複使用
@State private var scene = GameScene(siz

<!-- truncated for translation batch; full body continues in source -->