axiom-spritekit
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSpriteKit Game Development Guide
SpriteKit游戏开发指南
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+
用途:通过掌握场景图、物理引擎、动作系统和渲染管线,构建可靠的SpriteKit游戏
iOS版本要求:iOS 13+(支持SwiftUI集成),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
在以下场景使用本技能:
- 开发新的SpriteKit游戏或交互式模拟程序
- 实现物理效果(碰撞、接触、力、关节)
- 搭建游戏架构(场景、图层、相机)
- 优化帧率或减少绘制调用
- 在游戏中实现触摸/输入处理
- 管理场景过渡与数据传递
- 集成SpriteKit与SwiftUI或Metal
- 调试未触发的物理接触事件
- 解决坐标系混淆问题
请勿在以下场景使用本技能:
- SceneKit 3D渲染()
axiom-scenekit - GameplayKit实体组件系统
- Metal着色器编程()
axiom-metal-migration-ref - 通用SwiftUI布局()
axiom-swiftui-layout
1. Mental Model
1. 核心概念模型
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 . Default is (center).
position(0.5, 0.5)swift
// Common anchor point trap:
// Anchor (0, 0) = bottom-left of sprite is at position
// Anchor (0.5, 0.5) = center of sprite is at position (DEFAULT)
// Anchor (0.5, 0) = bottom-center (useful for characters standing on ground)
sprite.anchorPoint = CGPoint(x: 0.5, y: 0)Scene anchor point maps the view's frame to scene coordinates:
- — scene origin at bottom-left of view (default)
(0, 0) - — scene origin at center of view
(0.5, 0.5)
SpriteKit使用左下角为原点的坐标系,Y轴向上。这与UIKit(左上角为原点,Y轴向下)不同。
SpriteKit: UIKit:
┌─────────┐ ┌─────────┐
│ +Y │ │ (0,0) │
│ ↑ │ │ ↓ │
│ │ │ │ +Y │
│(0,0)──→+X│ │ │ │
└─────────┘ └─────────┘锚点定义了精灵上的哪个点对应其属性。默认值为(中心位置)。
position(0.5, 0.5)swift
// 常见锚点陷阱:
// 锚点(0, 0) = 精灵的左下角对应position
// 锚点(0.5, 0.5) = 精灵的中心对应position(默认值)
// 锚点(0.5, 0) = 精灵的底部中心对应position(适用于站在地面上的角色)
sprite.anchorPoint = CGPoint(x: 0.5, y: 0)场景锚点将视图框架映射到场景坐标系:
- — 场景原点位于视图左下角(默认值)
(0, 0) - — 场景原点位于视图中心
(0.5, 0.5)
Node Tree
节点树
Everything in SpriteKit is an in a tree hierarchy. Parent transforms propagate to children.
SKNodeSKScene
├── 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"SpriteKit中的所有元素都是树层级结构的一部分。父节点的变换会传递给子节点。
SKNodeSKScene
├── SKCameraNode(视口控制)
├── SKNode "world"(游戏内容图层)
│ ├── SKSpriteNode "player"
│ ├── SKSpriteNode "enemy"
│ └── SKNode "platforms"
│ ├── SKSpriteNode "platform1"
│ └── SKSpriteNode "platform2"
└── SKNode "hud"(UI图层,绑定到相机)
├── SKLabelNode "score"
└── SKSpriteNode "healthBar"Z-Ordering
Z轴排序
zPositionzPositionignoresSiblingOrdertrueswift
// Establish clear z-order layers
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
}zPositionzPositionignoresSiblingOrdertrueswift
// 定义清晰的Z轴层级
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
2. 场景架构
Scale Mode Decision
缩放模式选择
| Mode | Behavior | Use When |
|---|---|---|
| Fills view, crops edges | Full-bleed games (most games) |
| Fits in view, letterboxes | Puzzle games needing exact layout |
| Stretches to fill | Almost never — distorts |
| Matches view size exactly | Scene adapts to any ratio |
swift
class GameScene: SKScene {
override func sceneDidLoad() {
scaleMode = .aspectFill
// Design for a reference size, let aspectFill crop edges
}
}| 模式 | 行为 | 适用场景 |
|---|---|---|
| 填充视图,裁剪边缘 | 全屏沉浸式游戏(大多数游戏) |
| 适配视图,保留黑边 | 需要精确布局的益智类游戏 |
| 拉伸填充视图 | 几乎不推荐——会导致画面变形 |
| 完全匹配视图尺寸 | 场景需适配任意比例的情况 |
swift
class GameScene: SKScene {
override func sceneDidLoad() {
scaleMode = .aspectFill
// 基于参考尺寸设计,让aspectFill自动裁剪边缘
}
}Camera Node Pattern
相机节点模式
Always use for viewport control. Attach HUD elements to the camera so they don't scroll.
SKCameraNodeswift
let camera = SKCameraNode()
camera.name = "mainCamera"
addChild(camera)
self.camera = camera
// HUD follows camera automatically
let scoreLabel = SKLabelNode(text: "Score: 0")
scoreLabel.position = CGPoint(x: 0, y: size.height / 2 - 50)
camera.addChild(scoreLabel)
// Move camera to follow player
let follow = SKConstraint.distance(SKRange(constantValue: 0), to: playerNode)
camera.constraints = [follow]始终使用进行视口控制。将HUD元素绑定到相机,使其不会随场景滚动。
SKCameraNodeswift
let camera = SKCameraNode()
camera.name = "mainCamera"
addChild(camera)
self.camera = camera
// HUD会自动跟随相机
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
图层组织
swift
// Create layer nodes for organization
let worldNode = SKNode()
worldNode.name = "world"
addChild(worldNode)
let hudNode = SKNode()
hudNode.name = "hud"
camera?.addChild(hudNode)
// All gameplay objects go in worldNode
worldNode.addChild(playerSprite)
worldNode.addChild(enemySprite)
// All UI goes in hudNode (moves with camera)
hudNode.addChild(scoreLabel)swift
// 创建用于组织的图层节点
let worldNode = SKNode()
worldNode.name = "world"
addChild(worldNode)
let hudNode = SKNode()
hudNode.name = "hud"
camera?.addChild(hudNode)
// 所有游戏玩法对象都添加到worldNode
worldNode.addChild(playerSprite)
worldNode.addChild(enemySprite)
// 所有UI元素都添加到hudNode(随相机移动)
hudNode.addChild(scoreLabel)Scene Transitions
场景过渡
swift
// Preload next scene for smooth 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.
swift
class GameState {
static let shared = GameState()
var score = 0
var currentLevel = 1
var playerHealth = 100
}
// In scene transition:
let nextScene = LevelScene(size: size)
// GameState.shared is already accessible
view?.presentScene(nextScene, transition: .fade(withDuration: 0.5))Note: A singleton works for simple games. For larger projects with testing needs, consider passing a instance through scene initializers to avoid hidden global state.
GameStateCleanup in :
willMove(from:)swift
override func willMove(from view: SKView) {
removeAllActions()
removeAllChildren()
physicsWorld.contactDelegate = nil
}swift
// 预加载下一个场景以实现平滑过渡
guard let nextScene = LevelScene(fileNamed: "Level2") else { return }
nextScene.scaleMode = .aspectFill
let transition = SKTransition.fade(withDuration: 0.5)
view?.presentScene(nextScene, transition: transition)场景间数据传递:使用共享游戏状态对象,而非节点属性。
swift
class GameState {
static let shared = GameState()
var score = 0
var currentLevel = 1
var playerHealth = 100
}
// 场景过渡时:
let nextScene = LevelScene(size: size)
// GameState.shared可直接访问
view?.presentScene(nextScene, transition: .fade(withDuration: 0.5))注意:单例模式适用于简单游戏。对于需要测试的大型项目,建议通过场景初始化器传递实例,避免隐藏的全局状态。
GameState在中清理资源:
willMove(from:)swift
override func willMove(from view: SKView) {
removeAllActions()
removeAllChildren()
physicsWorld.contactDelegate = nil
}3. Physics Engine
3. 物理引擎
Bitmask Discipline
位掩码规范
This is the #1 source of SpriteKit bugs. Physics bitmasks use a 32-bit system where each bit represents a category.
swift
struct PhysicsCategory {
static let none: UInt32 = 0
static let player: UInt32 = 0b0001 // 1
static let enemy: UInt32 = 0b0010 // 2
static let ground: UInt32 = 0b0100 // 4
static let projectile: UInt32 = 0b1000 // 8
static let powerUp: UInt32 = 0b10000 // 16
}Three bitmask properties (all default to — everything):
0xFFFFFFFF| Property | Purpose | Default |
|---|---|---|
| What this body IS | |
| What it BOUNCES off | |
| What TRIGGERS delegate | |
The default of means everything collides with everything. This is the most common source of unexpected physics behavior.
collisionBitMask0xFFFFFFFFswift
// CORRECT: Explicit bitmask setup
player.physicsBody?.categoryBitMask = PhysicsCategory.player
player.physicsBody?.collisionBitMask = PhysicsCategory.ground | PhysicsCategory.enemy
player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy | PhysicsCategory.powerUp
enemy.physicsBody?.categoryBitMask = PhysicsCategory.enemy
enemy.physicsBody?.collisionBitMask = PhysicsCategory.ground | PhysicsCategory.player
enemy.physicsBody?.contactTestBitMask = PhysicsCategory.player | PhysicsCategory.projectile这是SpriteKit最常见的bug来源。物理位掩码使用32位系统,每个位代表一个类别。
swift
struct PhysicsCategory {
static let none: UInt32 = 0
static let player: UInt32 = 0b0001 // 1
static let enemy: UInt32 = 0b0010 // 2
static let ground: UInt32 = 0b0100 // 4
static let projectile: UInt32 = 0b1000 // 8
static let powerUp: UInt32 = 0b10000 // 16
}三个位掩码属性(默认值均为——与所有对象交互):
0xFFFFFFFF| 属性 | 用途 | 默认值 |
|---|---|---|
| 表示当前物体所属的类别 | |
| 表示当前物体会与哪些类别发生碰撞反弹 | |
| 表示当前物体与哪些类别接触时会触发代理回调 | |
默认的值意味着所有物体都会相互碰撞,这是导致意外物理行为最常见的原因。
collisionBitMask0xFFFFFFFFswift
// 正确做法:显式设置位掩码
player.physicsBody?.categoryBitMask = PhysicsCategory.player
player.physicsBody?.collisionBitMask = PhysicsCategory.ground | PhysicsCategory.enemy
player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy | PhysicsCategory.powerUp
enemy.physicsBody?.categoryBitMask = PhysicsCategory.enemy
enemy.physicsBody?.collisionBitMask = PhysicsCategory.ground | PhysicsCategory.player
enemy.physicsBody?.contactTestBitMask = PhysicsCategory.player | PhysicsCategory.projectileBitmask Checklist
位掩码检查清单
For every physics body, verify:
- set to exactly one category
categoryBitMask - set to only categories it should bounce off (NOT
collisionBitMask)0xFFFFFFFF - set to categories that should trigger delegate callbacks
contactTestBitMask - Delegate is assigned:
physicsWorld.contactDelegate = self
对于每个物理体,请验证:
- 仅设置为一个类别
categoryBitMask - 仅设置为需要反弹的类别(不要设为
collisionBitMask)0xFFFFFFFF - 设置为需要触发代理回调的类别
contactTestBitMask - 已分配代理:
physicsWorld.contactDelegate = self
Contact Detection
接触检测
swift
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
}
func didBegin(_ contact: SKPhysicsContact) {
// Sort bodies so bodyA has the lower category
let (first, second): (SKPhysicsBody, SKPhysicsBody)
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
(first, second) = (contact.bodyA, contact.bodyB)
} else {
(first, second) = (contact.bodyB, contact.bodyA)
}
// Now dispatch based on categories
if first.categoryBitMask == PhysicsCategory.player &&
second.categoryBitMask == PhysicsCategory.enemy {
guard let playerNode = first.node, let enemyNode = second.node else { return }
playerHitEnemy(player: playerNode, enemy: enemyNode)
}
}
}Modification rule: You cannot modify the physics world inside /. Set flags and apply changes in .
didBegindidEndupdate(_:)swift
var enemiesToRemove: [SKNode] = []
func didBegin(_ contact: SKPhysicsContact) {
// Flag for removal — don't remove here
if let enemy = contact.bodyB.node {
enemiesToRemove.append(enemy)
}
}
override func update(_ currentTime: TimeInterval) {
for enemy in enemiesToRemove {
enemy.removeFromParent()
}
enemiesToRemove.removeAll()
}swift
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
}
func didBegin(_ contact: SKPhysicsContact) {
// 排序物体,让bodyA的类别值更小
let (first, second): (SKPhysicsBody, SKPhysicsBody)
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
(first, second) = (contact.bodyA, contact.bodyB)
} else {
(first, second) = (contact.bodyB, contact.bodyA)
}
// 根据类别分发处理逻辑
if first.categoryBitMask == PhysicsCategory.player &&
second.categoryBitMask == PhysicsCategory.enemy {
guard let playerNode = first.node, let enemyNode = second.node else { return }
playerHitEnemy(player: playerNode, enemy: enemyNode)
}
}
}修改规则:不能在/回调中修改物理世界。请设置标记并在中应用更改。
didBegindidEndupdate(_:)swift
var enemiesToRemove: [SKNode] = []
func didBegin(_ contact: SKPhysicsContact) {
// 标记需要移除的对象——不要在此处直接移除
if let enemy = contact.bodyB.node {
enemiesToRemove.append(enemy)
}
}
override func update(_ currentTime: TimeInterval) {
for enemy in enemiesToRemove {
enemy.removeFromParent()
}
enemiesToRemove.removeAll()
}Body Types
物理体类型
| Type | Created With | Responds to Forces | Use For |
|---|---|---|---|
| Dynamic volume | | Yes | Players, enemies, projectiles |
| Static volume | Dynamic body + | No (but collides) | Platforms, walls |
| Edge | | No (boundary only) | Screen boundaries, terrain |
swift
// Screen boundary using edge loop
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
// Texture-based body for irregular shapes
guard let texture = enemy.texture else { return }
enemy.physicsBody = SKPhysicsBody(texture: texture, size: enemy.size)
// Circle for performance (cheapest collision detection)
bullet.physicsBody = SKPhysicsBody(circleOfRadius: 5)| 类型 | 创建方式 | 是否响应力 | 适用场景 |
|---|---|---|---|
| 动态体积 | | 是 | 玩家、敌人、投射物 |
| 静态体积 | 动态体 + | 否(但会发生碰撞) | 平台、墙壁 |
| 边缘 | | 否(仅作为边界) | 屏幕边界、地形 |
swift
// 使用边缘环创建屏幕边界
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
// 基于纹理创建不规则形状的物理体
guard let texture = enemy.texture else { return }
enemy.physicsBody = SKPhysicsBody(texture: texture, size: enemy.size)
// 使用圆形以提升性能(碰撞检测成本最低)
bullet.physicsBody = SKPhysicsBody(circleOfRadius: 5)Tunneling Prevention
穿模问题解决
Fast-moving objects can pass through thin walls. Fix:
swift
// Enable precise collision detection for fast objects
bullet.physicsBody?.usesPreciseCollisionDetection = true
// Make walls thick enough (at least as wide as fastest object moves per frame)
// At 60fps, an object at velocity 600pt/s moves 10pt/frame快速移动的物体可能会穿过薄墙。解决方法:
swift
// 为快速移动的物体启用精确碰撞检测
bullet.physicsBody?.usesPreciseCollisionDetection = true
// 让墙壁足够厚(至少等于最快物体每帧移动的距离)
// 在60fps下,速度为600pt/s的物体每帧移动10ptForces vs Impulses
力与冲量
swift
// Force: continuous (applied per frame, accumulates)
body.applyForce(CGVector(dx: 0, dy: 100))
// Impulse: instant velocity change (one-time, like a jump)
body.applyImpulse(CGVector(dx: 0, dy: 50))
// Torque: continuous rotation
body.applyTorque(0.5)
// Angular impulse: instant rotation change
body.applyAngularImpulse(1.0)swift
// 力:持续作用(每帧应用,会累积)
body.applyForce(CGVector(dx: 0, dy: 100))
// 冲量:瞬间改变速度(一次性作用,比如跳跃)
body.applyImpulse(CGVector(dx: 0, dy: 50))
// 扭矩:持续旋转
body.applyTorque(0.5)
// 角冲量:瞬间改变旋转状态
body.applyAngularImpulse(1.0)4. Actions System
4. 动作系统
Core Patterns
核心模式
swift
// Movement
let move = SKAction.move(to: CGPoint(x: 200, y: 300), duration: 1.0)
let moveBy = SKAction.moveBy(x: 100, y: 0, duration: 0.5)
// Rotation
let rotate = SKAction.rotate(byAngle: .pi * 2, duration: 1.0)
// Scale
let scale = SKAction.scale(to: 2.0, duration: 0.3)
// Fade
let fadeOut = SKAction.fadeOut(withDuration: 0.5)
let fadeIn = SKAction.fadeIn(withDuration: 0.5)swift
// 移动
let move = SKAction.move(to: CGPoint(x: 200, y: 300), duration: 1.0)
let moveBy = SKAction.moveBy(x: 100, y: 0, duration: 0.5)
// 旋转
let rotate = SKAction.rotate(byAngle: .pi * 2, duration: 1.0)
// 缩放
let scale = SKAction.scale(to: 2.0, duration: 0.3)
// 淡入淡出
let fadeOut = SKAction.fadeOut(withDuration: 0.5)
let fadeIn = SKAction.fadeIn(withDuration: 0.5)Sequencing and Grouping
序列与组
swift
// Sequence: one after another
let moveAndFade = SKAction.sequence([
SKAction.move(to: target, duration: 1.0),
SKAction.fadeOut(withDuration: 0.3),
SKAction.removeFromParent()
])
// Group: all at once
let spinAndGrow = SKAction.group([
SKAction.rotate(byAngle: .pi * 2, duration: 1.0),
SKAction.scale(to: 2.0, duration: 1.0)
])
// Repeat
let pulse = SKAction.repeatForever(SKAction.sequence([
SKAction.scale(to: 1.2, duration: 0.3),
SKAction.scale(to: 1.0, duration: 0.3)
]))swift
// 序列:按顺序执行
let moveAndFade = SKAction.sequence([
SKAction.move(to: target, duration: 1.0),
SKAction.fadeOut(withDuration: 0.3),
SKAction.removeFromParent()
])
// 组:同时执行
let spinAndGrow = SKAction.group([
SKAction.rotate(byAngle: .pi * 2, duration: 1.0),
SKAction.scale(to: 2.0, duration: 1.0)
])
// 重复
let pulse = SKAction.repeatForever(SKAction.sequence([
SKAction.scale(to: 1.2, duration: 0.3),
SKAction.scale(to: 1.0, duration: 0.3)
]))Named Actions (Critical for Management)
命名动作(管理的关键)
swift
// Use named actions so you can cancel/replace them
node.run(pulse, withKey: "pulse")
// Later, stop the pulse:
node.removeAction(forKey: "pulse")
// Check if running:
if node.action(forKey: "pulse") != nil {
// Still pulsing
}swift
// 使用命名动作以便取消/替换
node.run(pulse, withKey: "pulse")
// 之后停止脉冲动作:
node.removeAction(forKey: "pulse")
// 检查动作是否在运行:
if node.action(forKey: "pulse") != nil {
// 仍在执行脉冲
}Custom Actions with Weak Self
使用弱引用的自定义动作
swift
// WRONG: Retain cycle risk
node.run(SKAction.run {
self.score += 1 // Strong capture of self
})
// CORRECT: Weak capture
node.run(SKAction.run { [weak self] in
self?.score += 1
})
// For repeating actions, always use weak self
let spawn = SKAction.repeatForever(SKAction.sequence([
SKAction.run { [weak self] in self?.spawnEnemy() },
SKAction.wait(forDuration: 2.0)
]))
scene.run(spawn, withKey: "enemySpawner")swift
// 错误做法:存在循环引用风险
node.run(SKAction.run {
self.score += 1 // 强引用self
})
// 正确做法:使用弱引用
node.run(SKAction.run { [weak self] in
self?.score += 1
})
// 对于重复动作,务必使用弱引用
let spawn = SKAction.repeatForever(SKAction.sequence([
SKAction.run { [weak self] in self?.spawnEnemy() },
SKAction.wait(forDuration: 2.0)
]))
scene.run(spawn, withKey: "enemySpawner")Timing Modes
时间模式
swift
action.timingMode = .linear // Constant speed (default)
action.timingMode = .easeIn // Accelerate from rest
action.timingMode = .easeOut // Decelerate to rest
action.timingMode = .easeInEaseOut // Smooth start and endswift
action.timingMode = .linear // 匀速(默认值)
action.timingMode = .easeIn // 从静止开始加速
action.timingMode = .easeOut // 减速到静止
action.timingMode = .easeInEaseOut // 平滑的开始和结束Actions vs Physics
动作与物理的配合
Never use actions to move physics-controlled nodes. Actions override the physics simulation, causing jittering and missed collisions.
swift
// WRONG: Action fights physics
playerNode.run(SKAction.moveTo(x: 200, duration: 0.5))
// CORRECT: Use forces/impulses for physics bodies
playerNode.physicsBody?.applyImpulse(CGVector(dx: 50, dy: 0))
// CORRECT: Use actions for non-physics nodes (UI, effects, decorations)
hudLabel.run(SKAction.scale(to: 1.5, duration: 0.2))绝不要使用动作来移动受物理控制的节点。动作会覆盖物理模拟,导致抖动和碰撞丢失。
swift
// 错误做法:动作与物理模拟冲突
playerNode.run(SKAction.moveTo(x: 200, duration: 0.5))
// 正确做法:对受物理控制的节点使用力/冲量
playerNode.physicsBody?.applyImpulse(CGVector(dx: 50, dy: 0))
// 正确做法:对非物理节点(UI、特效、装饰)使用动作
hudLabel.run(SKAction.scale(to: 1.5, duration: 0.2))5. Input Handling
5. 输入处理
Touch Handling
触摸处理
swift
// CRITICAL: isUserInteractionEnabled must be true on the responding node
// SKScene has it true by default; other nodes default to false
class Player: SKSpriteNode {
init() {
super.init(texture: SKTexture(imageNamed: "player"), color: .clear, size: CGSize(width: 50, height: 50))
isUserInteractionEnabled = true // Required!
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Handle touch on this specific node
}
}swift
// 关键:响应节点的isUserInteractionEnabled必须设为true
// SKScene默认值为true;其他节点默认值为false
class Player: SKSpriteNode {
init() {
super.init(texture: SKTexture(imageNamed: "player"), color: .clear, size: CGSize(width: 50, height: 50))
isUserInteractionEnabled = true // 必须设置!
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// 处理针对此特定节点的触摸
}
}Coordinate Space Conversion
坐标系转换
swift
// Touch location in SCENE coordinates (most common)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let locationInScene = touch.location(in: self)
// Touch location in a SPECIFIC NODE's coordinates
let locationInWorld = touch.location(in: worldNode)
// Hit test: what node was touched?
let touchedNodes = nodes(at: locationInScene)
}Common mistake: Using returns UIKit coordinates (Y-flipped). Always use for scene coordinates.
touch.location(in: self.view)touch.location(in: self)swift
// 触摸在场景坐标系中的位置(最常用)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let locationInScene = touch.location(in: self)
// 触摸在特定节点坐标系中的位置
let locationInWorld = touch.location(in: worldNode)
// 点击测试:触摸到了哪个节点?
let touchedNodes = nodes(at: locationInScene)
}常见错误:使用返回的是UIKit坐标系(Y轴翻转)。始终使用获取场景坐标系。
touch.location(in: self.view)touch.location(in: self)Game Controller Support
游戏控制器支持
swift
import GameController
func setupControllers() {
NotificationCenter.default.addObserver(
self, selector: #selector(controllerConnected),
name: .GCControllerDidConnect, object: nil
)
// Check already-connected controllers
for controller in GCController.controllers() {
configureController(controller)
}
}swift
import GameController
func setupControllers() {
NotificationCenter.default.addObserver(
self, selector: #selector(controllerConnected),
name: .GCControllerDidConnect, object: nil
)
// 检查已连接的控制器
for controller in GCController.controllers() {
configureController(controller)
}
}6. Performance
6. 性能优化
Performance Priorities
性能优先级
For detailed performance diagnosis, see Symptom 3. Key priorities:
axiom-spritekit-diag- Node count — Remove offscreen nodes, use object pooling
- Draw calls — Use texture atlases, replace SKShapeNode with pre-rendered textures
- Physics cost — Prefer simple body shapes, limit
usesPreciseCollisionDetection - Particles — Limit birth rate, set finite emission counts
如需详细的性能诊断,请参考中的症状3。关键优先级:
axiom-spritekit-diag- 节点数量 — 移除屏幕外的节点,使用对象池
- 绘制调用 — 使用纹理图集,将SKShapeNode替换为预渲染纹理
- 物理成本 — 优先选择简单的物理体形状,限制的使用
usesPreciseCollisionDetection - 粒子效果 — 限制生成速率,设置有限的发射数量
Debug Overlays (Always Enable During Development)
调试叠加层(开发期间始终启用)
swift
if let view = self.view as? SKView {
view.showsFPS = true
view.showsNodeCount = true
view.showsDrawCount = true
view.showsPhysics = true // Shows physics body outlines
// Performance: render order optimization
view.ignoresSiblingOrder = true
}swift
if let view = self.view as? SKView {
view.showsFPS = true
view.showsNodeCount = true
view.showsDrawCount = true
view.showsPhysics = true // 显示物理体轮廓
// 性能优化:渲染顺序优化
view.ignoresSiblingOrder = true
}Texture Atlas Batching
纹理图集批处理
Sprites using textures from the same atlas render in a single draw call.
swift
// Create atlas in Xcode: Assets → New Sprite Atlas
// Or use .atlas folder in project
let atlas = SKTextureAtlas(named: "Characters")
let texture = atlas.textureNamed("player_idle")
let sprite = SKSpriteNode(texture: texture)
// Preload atlas to avoid frame drops
SKTextureAtlas.preloadTextureAtlases([atlas]) {
// Atlas ready — present scene
}使用同一图集纹理的精灵会在一次绘制调用中完成渲染。
swift
// 在Xcode中创建图集:Assets → New Sprite Atlas
// 或在项目中使用.atlas文件夹
let atlas = SKTextureAtlas(named: "Characters")
let texture = atlas.textureNamed("player_idle")
let sprite = SKSpriteNode(texture: texture)
// 预加载图集以避免掉帧
SKTextureAtlas.preloadTextureAtlases([atlas]) {
// 图集已准备就绪——显示场景
}SKShapeNode Trap
SKShapeNode陷阱
SKShapeNode generates one draw call per instance. It cannot be batched. Use it for prototyping and debug visualization only.
swift
// WRONG: 100 SKShapeNodes = 100 draw calls
for _ in 0..<100 {
let dot = SKShapeNode(circleOfRadius: 5)
addChild(dot)
}
// CORRECT: Pre-render to texture, use SKSpriteNode
let shape = SKShapeNode(circleOfRadius: 5)
shape.fillColor = .red
guard let texture = view?.texture(from: shape) else { return }
for _ in 0..<100 {
let dot = SKSpriteNode(texture: texture)
addChild(dot)
}每个SKShapeNode实例会生成一次绘制调用,无法进行批处理。仅在原型设计和调试可视化时使用。
swift
// 错误做法:100个SKShapeNode = 100次绘制调用
for _ in 0..<100 {
let dot = SKShapeNode(circleOfRadius: 5)
addChild(dot)
}
// 正确做法:预渲染为纹理,使用SKSpriteNode
let shape = SKShapeNode(circleOfRadius: 5)
shape.fillColor = .red
guard let texture = view?.texture(from: shape) else { return }
for _ in 0..<100 {
let dot = SKSpriteNode(texture: texture)
addChild(dot)
}Object Pooling
对象池
For frequently spawned/destroyed objects (bullets, particles, enemies):
swift
class BulletPool {
private var available: [SKSpriteNode] = []
private let texture: SKTexture
init(texture: SKTexture, initialSize: Int = 20) {
self.texture = texture
for _ in 0..<initialSize {
available.append(createBullet())
}
}
private func createBullet() -> SKSpriteNode {
let bullet = SKSpriteNode(texture: texture)
bullet.physicsBody = SKPhysicsBody(circleOfRadius: 3)
bullet.physicsBody?.categoryBitMask = PhysicsCategory.projectile
bullet.physicsBody?.collisionBitMask = PhysicsCategory.none
bullet.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
return bullet
}
func spawn() -> SKSpriteNode {
if available.isEmpty {
available.append(createBullet())
}
let bullet = available.removeLast()
bullet.isHidden = false
bullet.physicsBody?.isDynamic = true
return bullet
}
func recycle(_ bullet: SKSpriteNode) {
bullet.removeAllActions()
bullet.removeFromParent()
bullet.physicsBody?.isDynamic = false
bullet.physicsBody?.velocity = .zero
bullet.isHidden = true
available.append(bullet)
}
}对于频繁生成/销毁的对象(子弹、粒子、敌人):
swift
class BulletPool {
private var available: [SKSpriteNode] = []
private let texture: SKTexture
init(texture: SKTexture, initialSize: Int = 20) {
self.texture = texture
for _ in 0..<initialSize {
available.append(createBullet())
}
}
private func createBullet() -> SKSpriteNode {
let bullet = SKSpriteNode(texture: texture)
bullet.physicsBody = SKPhysicsBody(circleOfRadius: 3)
bullet.physicsBody?.categoryBitMask = PhysicsCategory.projectile
bullet.physicsBody?.collisionBitMask = PhysicsCategory.none
bullet.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
return bullet
}
func spawn() -> SKSpriteNode {
if available.isEmpty {
available.append(createBullet())
}
let bullet = available.removeLast()
bullet.isHidden = false
bullet.physicsBody?.isDynamic = true
return bullet
}
func recycle(_ bullet: SKSpriteNode) {
bullet.removeAllActions()
bullet.removeFromParent()
bullet.physicsBody?.isDynamic = false
bullet.physicsBody?.velocity = .zero
bullet.isHidden = true
available.append(bullet)
}
}Offscreen Node Removal
屏幕外节点移除
swift
// Manual removal is faster than shouldCullNonVisibleNodes
override func update(_ currentTime: TimeInterval) {
enumerateChildNodes(withName: "bullet") { node, _ in
if !self.frame.intersects(node.frame) {
self.bulletPool.recycle(node as! SKSpriteNode)
}
}
}swift
// 手动移除比shouldCullNonVisibleNodes更快
override func update(_ currentTime: TimeInterval) {
enumerateChildNodes(withName: "bullet") { node, _ in
if !self.frame.intersects(node.frame) {
self.bulletPool.recycle(node as! SKSpriteNode)
}
}
}7. Game Loop
7. 游戏循环
Frame Cycle (8 Phases)
帧周期(8个阶段)
1. update(_:) ← Your game logic here
2. didEvaluateActions() ← Actions completed
3. [Physics simulation] ← SpriteKit runs physics
4. didSimulatePhysics() ← Physics done, adjust results
5. [Constraint evaluation] ← SKConstraints applied
6. didApplyConstraints() ← Constraints done
7. didFinishUpdate() ← Last chance before render
8. [Rendering] ← Frame drawn1. update(_:) ← 在此处编写游戏逻辑
2. didEvaluateActions() ← 动作执行完成
3. [物理模拟] ← SpriteKit运行物理模拟
4. didSimulatePhysics() ← 物理模拟完成,可调整结果
5. [约束评估] ← 应用SKConstraints
6. didApplyConstraints() ← 约束应用完成
7. didFinishUpdate() ← 渲染前的最后机会
8. [渲染] ← 绘制帧Delta Time
增量时间
swift
private var lastUpdateTime: TimeInterval = 0
override func update(_ currentTime: TimeInterval) {
let dt: TimeInterval
if lastUpdateTime == 0 {
dt = 0
} else {
dt = currentTime - lastUpdateTime
}
lastUpdateTime = currentTime
// Clamp delta time to prevent spiral of death
// (when app returns from background, dt can be huge)
let clampedDt = min(dt, 1.0 / 30.0)
updatePlayer(deltaTime: clampedDt)
updateEnemies(deltaTime: clampedDt)
}swift
private var lastUpdateTime: TimeInterval = 0
override func update(_ currentTime: TimeInterval) {
let dt: TimeInterval
if lastUpdateTime == 0 {
dt = 0
} else {
dt = currentTime - lastUpdateTime
}
lastUpdateTime = currentTime
// 限制增量时间以避免性能崩溃
// (应用从后台返回时,dt可能会非常大)
let clampedDt = min(dt, 1.0 / 30.0)
updatePlayer(deltaTime: clampedDt)
updateEnemies(deltaTime: clampedDt)
}Pause Handling
暂停处理
swift
// Pause the scene (stops actions, physics, update loop)
scene.isPaused = true
// Pause specific subtree only
worldNode.isPaused = true // Game paused but HUD still animates
// Handle app backgrounding
NotificationCenter.default.addObserver(
self, selector: #selector(pauseGame),
name: UIApplication.willResignActiveNotification, object: nil
)swift
// 暂停场景(停止动作、物理模拟、更新循环)
scene.isPaused = true
// 仅暂停特定子树
worldNode.isPaused = true // 游戏暂停但HUD仍可动画
// 处理应用进入后台
NotificationCenter.default.addObserver(
self, selector: #selector(pauseGame),
name: UIApplication.willResignActiveNotification, object: nil
)8. Particle Effects
8. 粒子效果
Emitter Best Practices
发射器最佳实践
swift
// Load from .sks file (designed in Xcode Particle Editor)
guard let emitter = SKEmitterNode(fileNamed: "Explosion") else { return }
emitter.position = explosionPoint
addChild(emitter)
// CRITICAL: Auto-remove after emission completes
let duration = TimeInterval(emitter.numParticlesToEmit) / TimeInterval(emitter.particleBirthRate)
+ TimeInterval(emitter.particleLifetime + emitter.particleLifetimeRange / 2)
emitter.run(SKAction.sequence([
SKAction.wait(forDuration: duration),
SKAction.removeFromParent()
]))swift
// 从.sks文件加载(在Xcode粒子编辑器中设计)
guard let emitter = SKEmitterNode(fileNamed: "Explosion") else { return }
emitter.position = explosionPoint
addChild(emitter)
// 关键:发射完成后自动移除
let duration = TimeInterval(emitter.numParticlesToEmit) / TimeInterval(emitter.particleBirthRate)
+ TimeInterval(emitter.particleLifetime + emitter.particleLifetimeRange / 2)
emitter.run(SKAction.sequence([
SKAction.wait(forDuration: duration),
SKAction.removeFromParent()
]))Target Node for Trails
轨迹的目标节点
Without , particles move with the emitter. For trails (like rocket exhaust), set to the scene:
targetNodetargetNodeswift
let trail = SKEmitterNode(fileNamed: "RocketTrail")!
trail.targetNode = scene // Particles stay where emitted
rocketNode.addChild(trail)如果不设置,粒子会随发射器移动。对于轨迹效果(如火箭尾焰),请将设置为场景:
targetNodetargetNodeswift
let trail = SKEmitterNode(fileNamed: "RocketTrail")!
trail.targetNode = scene // 粒子会停留在发射位置
rocketNode.addChild(trail)Infinite Emitter Cleanup
无限发射器的清理
swift
// WRONG: Infinite emitter never cleaned up
let fire = SKEmitterNode(fileNamed: "Fire")!
fire.numParticlesToEmit = 0 // 0 = infinite
addChild(fire)
// Memory leak — particles accumulate forever
// CORRECT: Set emission limit or remove when done
fire.numParticlesToEmit = 200 // Stops after 200 particles
// Or manually stop and remove:
fire.particleBirthRate = 0 // Stop new particles
fire.run(SKAction.sequence([
SKAction.wait(forDuration: TimeInterval(fire.particleLifetime)),
SKAction.removeFromParent()
]))swift
// 错误做法:无限发射器永远不会被清理
let fire = SKEmitterNode(fileNamed: "Fire")!
fire.numParticlesToEmit = 0 // 0表示无限
addChild(fire)
// 内存泄漏——粒子会不断累积
// 正确做法:设置发射限制或在使用完毕后移除
fire.numParticlesToEmit = 200 // 发射200个粒子后停止
// 或手动停止并移除:
fire.particleBirthRate = 0 // 停止生成新粒子
fire.run(SKAction.sequence([
SKAction.wait(forDuration: TimeInterval(fire.particleLifetime)),
SKAction.removeFromParent()
]))9. SwiftUI Integration
9. SwiftUI集成
SpriteView (Recommended, iOS 14+)
SpriteView(推荐,iOS 14+)
The simplest way to embed SpriteKit in SwiftUI. Use this unless you need custom SKView configuration.
swift
import SpriteKit
import SwiftUI
struct GameView: View {
var body: some View {
SpriteView(scene: {
let scene = GameScene(size: CGSize(width: 390, height: 844))
scene.scaleMode = .aspectFill
return scene
}(), debugOptions: [.showsFPS, .showsNodeCount])
.ignoresSafeArea()
}
}将SpriteKit嵌入SwiftUI的最简单方式。除非需要自定义SKView配置,否则请使用此方式。
swift
import SpriteKit
import SwiftUI
struct GameView: View {
var body: some View {
SpriteView(scene: {
let scene = GameScene(size: CGSize(width: 390, height: 844))
scene.scaleMode = .aspectFill
return scene
}(), debugOptions: [.showsFPS, .showsNodeCount])
.ignoresSafeArea()
}
}UIViewRepresentable (Advanced)
UIViewRepresentable(进阶)
Use when you need full control over SKView configuration (custom frame rate, transparency, or multiple scenes).
swift
import SwiftUI
import SpriteKit
struct SpriteKitView: UIViewRepresentable {
let scene: SKScene
func makeUIView(context: Context) -> SKView {
let view = SKView()
view.showsFPS = true
view.showsNodeCount = true
view.ignoresSiblingOrder = true
return view
}
func updateUIView(_ view: SKView, context: Context) {
if view.scene == nil {
view.presentScene(scene)
}
}
}当需要完全控制SKView配置(自定义帧率、透明度或多场景)时使用。
swift
import SwiftUI
import SpriteKit
struct SpriteKitView: UIViewRepresentable {
let scene: SKScene
func makeUIView(context: Context) -> SKView {
let view = SKView()
view.showsFPS = true
view.showsNodeCount = true
view.ignoresSiblingOrder = true
return view
}
func updateUIView(_ view: SKView, context: Context) {
if view.scene == nil {
view.presentScene(scene)
}
}
}SKRenderer for Metal Hybrid
SKRenderer与Metal混合
Use when SpriteKit is one layer in a Metal pipeline:
SKRendererswift
let renderer = SKRenderer(device: metalDevice)
renderer.scene = gameScene
// In your Metal render loop:
renderer.update(atTime: currentTime)
renderer.render(
withViewport: viewport,
commandBuffer: commandBuffer,
renderPassDescriptor: renderPassDescriptor
)当SpriteKit是Metal管线中的一个图层时,使用:
SKRendererswift
let renderer = SKRenderer(device: metalDevice)
renderer.scene = gameScene
// 在Metal渲染循环中:
renderer.update(atTime: currentTime)
renderer.render(
withViewport: viewport,
commandBuffer: commandBuffer,
renderPassDescriptor: renderPassDescriptor
)10. Anti-Patterns
10. 反模式
Anti-Pattern 1: Default Bitmasks
反模式1:默认位掩码
Time cost: 30-120 minutes debugging phantom collisions
swift
// WRONG: Default collisionBitMask is 0xFFFFFFFF
let body = SKPhysicsBody(circleOfRadius: 10)
node.physicsBody = body
// Collides with EVERYTHING — even things it shouldn't
// CORRECT: Always set all three masks explicitly
body.categoryBitMask = PhysicsCategory.player
body.collisionBitMask = PhysicsCategory.ground
body.contactTestBitMask = PhysicsCategory.enemy时间成本:30-120分钟调试莫名的碰撞问题
swift
// 错误做法:默认collisionBitMask为0xFFFFFFFF
let body = SKPhysicsBody(circleOfRadius: 10)
node.physicsBody = body
// 会与所有物体碰撞——包括不应该碰撞的对象
// 正确做法:始终显式设置三个位掩码
body.categoryBitMask = PhysicsCategory.player
body.collisionBitMask = PhysicsCategory.ground
body.contactTestBitMask = PhysicsCategory.enemyAnti-Pattern 2: Missing contactTestBitMask
反模式2:缺少contactTestBitMask
Time cost: 30-60 minutes wondering why didBegin never fires
swift
// WRONG: contactTestBitMask defaults to 0 — no contacts ever fire
player.physicsBody?.categoryBitMask = PhysicsCategory.player
// Forgot contactTestBitMask!
// CORRECT: Both bodies need compatible masks
player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
enemy.physicsBody?.categoryBitMask = PhysicsCategory.enemy时间成本:30-60分钟疑惑为什么didBegin从未触发
swift
// 错误做法:contactTestBitMask默认值为0——永远不会触发接触事件
player.physicsBody?.categoryBitMask = PhysicsCategory.player
// 忘记设置contactTestBitMask!
// 正确做法:两个物体都需要设置兼容的掩码
player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
enemy.physicsBody?.categoryBitMask = PhysicsCategory.enemyAnti-Pattern 3: Actions on Physics Bodies
反模式3:对物理体使用动作
Time cost: 1-3 hours of jittering and missed collisions
swift
// WRONG: SKAction.move overrides physics position each frame
playerNode.run(SKAction.moveTo(x: 200, duration: 1.0))
// Physics body position is set by action, ignoring forces/collisions
// CORRECT: Use physics for physics-controlled nodes
playerNode.physicsBody?.applyForce(CGVector(dx: 100, dy: 0))时间成本:1-3小时调试抖动和碰撞丢失问题
swift
// 错误做法:SKAction.move会每帧覆盖物理位置
playerNode.run(SKAction.moveTo(x: 200, duration: 1.0))
// 物理体位置由动作设置,忽略力和碰撞
// 正确做法:对受物理控制的节点使用物理方法
playerNode.physicsBody?.applyForce(CGVector(dx: 100, dy: 0))Anti-Pattern 4: SKShapeNode for Gameplay
反模式4:在游戏玩法中使用SKShapeNode
Time cost: Hours diagnosing frame drops
Each SKShapeNode is a separate draw call that cannot be batched. 50 shape nodes = 50 draw calls. See the pre-render-to-texture pattern in Section 6 (SKShapeNode Trap) for the fix.
时间成本:数小时诊断掉帧问题
每个SKShapeNode都是独立的绘制调用,无法批处理。50个形状节点=50次绘制调用。请参考第6节中的预渲染为纹理模式解决。
Anti-Pattern 5: Strong Self in Action Closures
反模式5:在动作闭包中强引用self
Time cost: Memory leaks, eventual crash
swift
// WRONG: Strong capture in repeating action
node.run(SKAction.repeatForever(SKAction.sequence([
SKAction.run { self.spawnEnemy() },
SKAction.wait(forDuration: 2.0)
])))
// CORRECT: Weak capture
node.run(SKAction.repeatForever(SKAction.sequence([
SKAction.run { [weak self] in self?.spawnEnemy() },
SKAction.wait(forDuration: 2.0)
])))时间成本:内存泄漏,最终导致崩溃
swift
// 错误做法:重复动作中强引用self
node.run(SKAction.repeatForever(SKAction.sequence([
SKAction.run { self.spawnEnemy() },
SKAction.wait(forDuration: 2.0)
])))
// 正确做法:使用弱引用
node.run(SKAction.repeatForever(SKAction.sequence([
SKAction.run { [weak self] in self?.spawnEnemy() },
SKAction.wait(forDuration: 2.0)
])))11. Code Review Checklist
11. 代码审查清单
Physics
物理部分
- Every physics body has explicit (not default)
categoryBitMask - Every physics body has explicit (not
collisionBitMask)0xFFFFFFFF - Bodies needing contact detection have set
contactTestBitMask - is assigned
physicsWorld.contactDelegate - No world modifications inside /
didBegincallbacksdidEnd - Fast objects use
usesPreciseCollisionDetection
- 每个物理体都有显式的(非默认值)
categoryBitMask - 每个物理体都有显式的(非
collisionBitMask)0xFFFFFFFF - 需要接触检测的物体已设置
contactTestBitMask - 已设置
physicsWorld.contactDelegate - 未在/
didBegin回调中修改物理世界didEnd - 快速移动的物体使用了
usesPreciseCollisionDetection
Actions
动作部分
- No /
SKAction.moveon physics-controlled nodesrotate - Repeating actions use for cancellation
withKey: - closures use
SKAction.run[weak self] - One-shot emitters are removed after emission
- 未对受物理控制的节点使用/
SKAction.moverotate - 重复动作使用以便取消
withKey: - 闭包使用
SKAction.run[weak self] - 一次性发射器在发射完成后会被移除
Performance
性能部分
- Debug overlays enabled during development
- on SKView
ignoresSiblingOrder = true - No SKShapeNode in gameplay sprites (use pre-rendered textures)
- Texture atlases used for related sprites
- Offscreen nodes removed manually
- 开发期间已启用调试叠加层
- SKView已设置
ignoresSiblingOrder = true - 游戏玩法精灵中未使用SKShapeNode(使用预渲染纹理)
- 相关精灵使用了纹理图集
- 手动移除了屏幕外的节点
Scene Management
场景管理部分
- cleans up actions, children, delegates
willMove(from:) - Scene data passed via shared state, not node properties
- Camera used for viewport control
- 清理了动作、子节点和代理
willMove(from:) - 场景数据通过共享状态传递,而非节点属性
- 使用相机进行视口控制
12. Pressure Scenarios
12. 压力场景
Scenario 1: "Physics Contacts Don't Work — Ship Tonight"
场景1:"物理接触不工作——今晚必须上线"
Pressure: Deadline pressure to skip systematic debugging
Wrong approach: Randomly changing bitmask values, adding everywhere, or disabling physics
0xFFFFFFFFCorrect approach (2-5 minutes):
- Enable — verify bodies exist and overlap
showsPhysics - Print all three bitmasks for both bodies
- Verify on body A includes category of body B (or vice versa)
contactTestBitMask - Verify is set
physicsWorld.contactDelegate - Verify you're not modifying the world inside the callback
Push-back template: "Let me run the 5-step bitmask checklist. It takes 2 minutes and catches 90% of contact issues. Random changes will make it worse."
压力:截止日期压力导致跳过系统性调试
错误做法:随机修改位掩码值,到处设置,或禁用物理
0xFFFFFFFF正确做法(2-5分钟):
- 启用——验证物理体存在且重叠
showsPhysics - 打印两个物体的三个位掩码
- 验证物体A的包含物体B的类别(反之亦然)
contactTestBitMask - 验证已设置
physicsWorld.contactDelegate - 验证未在回调中修改物理世界
反驳模板:"让我运行5步位掩码检查清单,只需要2分钟,能解决90%的接触问题。随机修改会让问题更糟。"
Scenario 2: "Frame Rate Is Fine on My Device"
场景2:"我的设备上帧率没问题"
Pressure: Authority says "it runs at 60fps for me, ship it"
Wrong approach: Shipping without profiling on minimum-spec device
Correct approach:
- Enable ,
showsFPS,showsNodeCountshowsDrawCount - Test on oldest supported device
- If >200 nodes or >30 draw calls, investigate
- Check for SKShapeNode in gameplay
- Verify offscreen nodes are being removed
Push-back template: "Performance varies by device. Let me check node count and draw calls — takes 30 seconds with debug overlays. If counts are low, we're safe to ship."
压力:权威人士说"我这里能跑60fps,直接上线"
错误做法:未在最低配置设备上进行性能分析就上线
正确做法:
- 启用、
showsFPS、showsNodeCountshowsDrawCount - 在最旧的支持设备上测试
- 如果节点数>200或绘制调用>30,进行调查
- 检查游戏玩法中是否使用了SKShapeNode
- 验证屏幕外节点已被手动移除
反驳模板:"性能因设备而异。让我检查节点数和绘制调用,用调试叠加层只需要30秒。如果数值较低,我们就可以安全上线。"
Scenario 3: "Just Use SKShapeNode, It's Faster to Code"
场景3:"直接用SKShapeNode就行,编码更快"
Pressure: Sunk cost — already built with SKShapeNode, don't want to redo
Wrong approach: Shipping with 100+ SKShapeNodes causing frame drops
Correct approach:
- Check — each SKShapeNode adds a draw call
showsDrawCount - If >20 shape nodes in gameplay, pre-render to textures
- Use to convert once, reuse as SKSpriteNode
view.texture(from:) - Keep SKShapeNode only for debug visualization
Push-back template: "Each SKShapeNode is a separate draw call. Converting to pre-rendered textures is a 15-minute refactor that can double frame rate. SKSpriteNode from atlas = 1 draw call for all of them."
压力:沉没成本——已经用SKShapeNode构建完成,不想重做
错误做法:上线包含100+SKShapeNode的版本,导致掉帧
正确做法:
- 查看——每个SKShapeNode都会增加一次绘制调用
showsDrawCount - 如果游戏玩法中有超过20个形状节点,预渲染为纹理
- 使用一次性转换,然后作为SKSpriteNode复用
view.texture(from:) - 仅在调试可视化时保留SKShapeNode
反驳模板:"每个SKShapeNode都是一次独立的绘制调用。转换为预渲染纹理只需要15分钟重构,能让帧率翻倍。来自同一图集的SKSpriteNode只需要一次绘制调用就能完成所有渲染。"
Resources
资源
WWDC: 2014-608, 2016-610, 2017-609, 2013-502
Docs: /spritekit, /spritekit/skscene, /spritekit/skphysicsbody, /spritekit/maximizing-node-drawing-performance
Skills: axiom-spritekit-ref, axiom-spritekit-diag
WWDC:2014-608, 2016-610, 2017-609, 2013-502
文档:/spritekit, /spritekit/skscene, /spritekit/skphysicsbody, /spritekit/maximizing-node-drawing-performance
相关技能:axiom-spritekit-ref, axiom-spritekit-diag