使用 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
- Nodes and Sprites
- Actions and Animation
- Physics
- Touch Handling
- Camera
- Particle Effects
- SwiftUI Integration
- Common Mistakes
- Review Checklist
- References
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
每一帧的渲染遵循以下顺序:
update(_:)—— 游戏逻辑处理- 执行 Action 动作
didEvaluateActions()—— Action 执行完毕后的逻辑- 模拟物理效果
didSimulatePhysics()—— 物理模拟完成后的调整- 应用约束(Constraints)
didApplyConstraints()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。可以在场景中重写 touchesBegan、touchesMoved 和 touchesEnded 方法。使用 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 -->




