spritekit

spritekit

热门

使用 SpriteKit 构建 2D 游戏与动画。适用于使用 SKScene 和 SKView 创建游戏场景、通过 SKSpriteNode 添加精灵图、使用 SKAction 序列制作动画、利用 SKPhysicsBody 与碰撞检测模拟物理效果、使用 SKEmitterNode 创建粒子特效、构建瓦片地图、使用 SKCameraNode,或在 SwiftUI 中使用 SpriteView 集成 SpriteKit 场景等任务。

961Star
48Fork
更新于 2026/7/31
SKILL.md
只读
名称
spritekit
描述

使用 SpriteKit 构建 2D 游戏与动画。适用于使用 SKScene 和 SKView 创建游戏场景、通过 SKSpriteNode 添加精灵图、使用 SKAction 序列制作动画、利用 SKPhysicsBody 与碰撞检测模拟物理效果、使用 SKEmitterNode 创建粒子特效、构建瓦片地图、使用 SKCameraNode,或在 SwiftUI 中使用 SpriteView 集成 SpriteKit 场景等任务。

SpriteKit

使用 SpriteKit 和 Swift 6.3 为 iOS 26+ 开发 2D 游戏与交互式动画。涵盖场景生命周期、节点层级结构、Action 动作、物理引擎、粒子效果、相机控制、触摸手势处理以及 SwiftUI 集成。

Contents

Scene Setup

SpriteKit 通过 SKView 来渲染内容,SKView 用于展示 SKScene——SKScene 是整个节点树的根节点,框架每一帧都会对其进行更新与渲染。

Creating a Scene

继承 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) {
        // 每帧渲染前调用一次
    }
}

Presenting a Scene (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 会留黑边(letterbox);.fill 会拉伸图像,可能导致变形。

Frame Cycle

每一帧的渲染遵循以下顺序:

  1. update(_:) —— 游戏逻辑处理
  2. 执行 Action 动作
  3. didEvaluateActions() —— Action 执行完毕后的逻辑
  4. 模拟物理效果
  5. didSimulatePhysics() —— 物理模拟完成后的调整
  6. 应用约束(Constraints)
  7. didApplyConstraints()
  8. didFinishUpdate() —— 渲染前的最终调整

仅在需要处理逻辑的生命周期回调中进行重写。

Nodes and Sprites

使用 SKNode(无视觉外观)作为无形的容器或布局组。子节点会自动继承父节点的位置、缩放、旋转、透明度(alpha)和播放速度(speed)。SKSpriteNode 是最主要的视觉节点。

Common Node Types

Class Purpose
SKSpriteNode 纹理图像或纯色块
SKLabelNode 文本渲染
SKShapeNode 矢量路径(每个 Draw Call 开销较大)
SKEmitterNode 粒子效果
SKCameraNode 视口(Camera)控制
SKTileMapNode 基于网格的瓦片地图
SKAudioNode 定位音频(Positional Audio)
SKCropNode / SKEffectNode 遮罩 / CIFilter 滤镜特效
SK3DNode 嵌套 SceneKit 3D 内容

Creating Sprites

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

Drawing Order

SKView 上设置 ignoresSiblingOrder = true 可以提升性能;SpriteKit 会根据 zPosition 来决定绘制顺序。如果不设置该属性,节点将按照节点树的渲染顺序绘制。

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

Naming and Searching

为节点设置 name 属性,方便直接查找节点而无需保存成员变量。可以使用 childNode(withName:)enumerateChildNodes(withName:using:) 或下标访问。匹配模式:// 查找整个节点树,* 匹配任意字符,.. 表示父节点。

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

Actions and Animation

SKAction 对象定义了节点随着时间的推移所发生的变化。Action 是不可变且可复用的。通过 node.run(_:) 来运行 Action。

Basic Actions

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

// 顺序执行:一个接一个地运行
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)
    ])
)

Texture Animation

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)来控制缓动曲线。可以为 Action 指定 key 属性,方便后续控制或取消:

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

player.run(pulse, withKey: "pulse")
player.removeAction(forKey: "pulse") // 稍后停止运行

Physics

SpriteKit 内置了 2D 物理引擎。场景中的 physicsWorld 负责管理重力和碰撞检测。

Adding 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 mask)控制物理碰撞与碰撞回调:

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 碰撞事件回调。

Contact Detection

实现 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() 中设置标志位或添加事件,然后在 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 控制单个物理体是否受重力影响。

Touch Handling

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。这样该节点就会直接接收触摸事件,而不是传给场景。

Camera

SKCameraNode 用于控制场景的可视区域。将其添加为子节点,并赋值给 scene.camera

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

Following a Character

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

Camera Zoom and HUD

缩放相机节点可以实现镜头缩放效果:setScale(0.5) 放大 2 倍,setScale(2.0) 缩小 2 倍。添加为相机子节点的节点会保持固定在屏幕上(即 HUD UI 元素):

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 用于生成粒子效果。可以在 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 Integration

SpriteView 用于在 SwiftUI 中嵌入 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

传入 options: [.allowsTransparency] 可支持透明背景,.shouldCullNonVisibleNodes 可自动剔除屏幕外不可见的节点,.ignoresSiblingOrder 可使用基于 zPosition 的绘制顺序。开发调优时可使用 debugOptions: [.showsFPS, .showsNodeCount]

Communicating Between SwiftUI and the Scene

使用共享的 @Observable 对象在 SwiftUI 与场景间传递数据。在 @State 中存储 scene 实例,避免视图重新渲染时重新创建场景:

@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

// DON'T: 每次 body 重新求值时都会重新创建场景
var body: some View {
    SpriteView(scene: GameScene(size: CGSize(width: 390, height: 844)))
}

// DO: 仅创建一次并重复使用
@State private var scene = GameScene(siz

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