axiom-spritekit-diag

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SpriteKit Diagnostics

SpriteKit 诊断指南

Systematic diagnosis for common SpriteKit issues with time-cost annotations.
针对SpriteKit常见问题的系统化诊断指南,附时间成本说明。

When to Use This Diagnostic Skill

何时使用该诊断技能

Use this skill when:
  • Physics contacts never fire (didBegin not called)
  • Objects pass through walls (tunneling)
  • Frame rate drops below 60fps
  • Touches don't register on nodes
  • Memory grows continuously during gameplay
  • Positions and coordinates seem wrong
  • App crashes during scene transitions
出现以下情况时使用本技能:
  • 物理碰撞从未触发(didBegin 未被调用)
  • 物体穿墙(穿透问题)
  • 帧率低于60fps
  • 节点未响应触摸
  • 游戏过程中内存持续增长
  • 位置和坐标显示异常
  • 场景切换时应用崩溃

Mandatory First Step: Enable Debug Overlays

强制第一步:启用调试叠加层

Time cost: 10 seconds setup vs hours of blind debugging
swift
if let view = self.view as? SKView {
    view.showsFPS = true
    view.showsNodeCount = true
    view.showsDrawCount = true
    view.showsPhysics = true
}
If
showsPhysics
doesn't show expected physics body outlines, your physics bodies aren't configured correctly. Stop and fix bodies before debugging contacts.
For SpriteKit architecture patterns and best practices, see
axiom-spritekit
. For API reference, see
axiom-spritekit-ref
.

时间成本:10秒设置 vs 数小时盲目调试
swift
if let view = self.view as? SKView {
    view.showsFPS = true
    view.showsNodeCount = true
    view.showsDrawCount = true
    view.showsPhysics = true
}
如果
showsPhysics
未显示预期的物理体轮廓,说明你的物理体配置有误。在调试碰撞前先修复物理体配置。
关于SpriteKit架构模式和最佳实践,请查看
axiom-spritekit
。如需API参考,请查看
axiom-spritekit-ref

Symptom 1: Physics Contacts Not Firing

症状1:物理碰撞未触发

Time saved: 30-120 min → 2-5 min
didBegin(_:) never called
├─ Is physicsWorld.contactDelegate set?
│   └─ NO → Set in didMove(to:):
│        physicsWorld.contactDelegate = self
│        ✓ This alone fixes ~30% of contact issues
├─ Does the class conform to SKPhysicsContactDelegate?
│   └─ NO → Add conformance:
│        class GameScene: SKScene, SKPhysicsContactDelegate
├─ Does body A have contactTestBitMask that includes body B's category?
│   ├─ Print: "A contact: \(bodyA.contactTestBitMask), B cat: \(bodyB.categoryBitMask)"
│   ├─ Result should be: (A.contactTestBitMask & B.categoryBitMask) != 0
│   └─ FIX: Set contactTestBitMask to include the other body's category
│        player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
├─ Is categoryBitMask set (not default 0xFFFFFFFF)?
│   ├─ Default category means everything matches — but in unexpected ways
│   └─ FIX: Always set explicit categoryBitMask for each body type
├─ Do the bodies actually overlap? (Check showsPhysics)
│   ├─ Bodies too small or offset from sprite → Fix physics body size
│   └─ Bodies never reach each other → Check collisionBitMask isn't blocking
└─ Are you modifying the world inside didBegin?
    ├─ Removing nodes inside didBegin can cause missed callbacks
    └─ FIX: Flag nodes for removal, process in update(_:)
节省时间:30-120分钟 → 2-5分钟
didBegin(_:) 从未被调用
├─ 是否设置了physicsWorld.contactDelegate?
│   └─ 否 → 在didMove(to:)中设置:
│        physicsWorld.contactDelegate = self
│        ✓ 仅此一项即可解决约30%的碰撞问题
├─ 类是否遵循SKPhysicsContactDelegate协议?
│   └─ 否 → 添加协议遵循:
│        class GameScene: SKScene, SKPhysicsContactDelegate
├─ 物体A的contactTestBitMask是否包含物体B的类别?
│   ├─ 打印:"A contact: \(bodyA.contactTestBitMask), B cat: \(bodyB.categoryBitMask)"
│   ├─ 预期结果:(A.contactTestBitMask & B.categoryBitMask) != 0
│   └─ 修复:设置contactTestBitMask以包含另一物体的类别
│        player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
├─ 是否设置了categoryBitMask(而非默认的0xFFFFFFFF)?
│   ├─ 默认类别意味着会匹配所有物体,但可能出现意外情况
│   └─ 修复:始终为每个物体类型显式设置categoryBitMask
├─ 物体是否真的重叠?(检查showsPhysics)
│   ├─ 物体过小或与精灵偏移 → 修复物理体尺寸
│   └─ 物体从未接触 → 检查collisionBitMask是否阻止了碰撞
└─ 是否在didBegin内修改物理世界?
    ├─ 在didBegin内移除节点可能导致回调丢失
│   └─ 修复:标记待移除节点,在update(_:)中处理

Quick Diagnostic Print

快速诊断打印

swift
func didBegin(_ contact: SKPhysicsContact) {
    print("CONTACT: \(contact.bodyA.node?.name ?? "nil") (\(contact.bodyA.categoryBitMask)) <-> \(contact.bodyB.node?.name ?? "nil") (\(contact.bodyB.categoryBitMask))")
}
If this never prints, the issue is delegate/bitmask setup. If it prints but with wrong bodies, the issue is bitmask values.

swift
func didBegin(_ contact: SKPhysicsContact) {
    print("CONTACT: \(contact.bodyA.node?.name ?? "nil") (\(contact.bodyA.categoryBitMask)) <-> \(contact.bodyB.node?.name ?? "nil") (\(contact.bodyB.categoryBitMask))")
}
如果从未打印,问题出在代理/位掩码设置。如果打印但物体不符,问题在位掩码值。

Symptom 2: Objects Tunneling Through Walls

症状2:物体穿墙

Time saved: 20-60 min → 5 min
Fast objects pass through thin walls
├─ Is the object moving faster than wall thickness per frame?
│   ├─ At 60fps: max safe speed = wall_thickness × 60 pt/s
│   ├─ A 10pt wall is safe up to ~600 pt/s
│   └─ FIX: usesPreciseCollisionDetection = true on the fast object
├─ Is usesPreciseCollisionDetection enabled?
│   ├─ Only needed on the MOVING object (not the wall)
│   └─ FIX: fastObject.physicsBody?.usesPreciseCollisionDetection = true
├─ Is the wall an edge body?
│   ├─ Edge bodies have zero area — tunneling is easier
│   └─ FIX: Use volume body for walls (rectangleOf:) with isDynamic = false
├─ Is the wall thick enough?
│   └─ FIX: Make walls at least 10pt thick for objects up to 600pt/s
└─ Are collision bitmasks correct?
    ├─ Wall's categoryBitMask must be in object's collisionBitMask
    └─ FIX: Verify with print: object.collisionBitMask & wall.categoryBitMask != 0

节省时间:20-60分钟 → 5分钟
快速移动的物体穿过薄墙
├─ 物体移动速度是否超过每帧墙的厚度?
│   ├─ 60fps下:最大安全速度 = 墙厚度 × 60 pt/s
│   ├─ 10pt厚的墙对速度不超过600pt/s的物体是安全的
│   └─ 修复:为快速移动的物体启用usesPreciseCollisionDetection = true
├─ 是否启用了usesPreciseCollisionDetection?
│   ├─ 仅需在**移动的物体**上启用(无需在墙上启用)
│   └─ 修复:fastObject.physicsBody?.usesPreciseCollisionDetection = true
├─ 墙是否是边缘物理体?
│   ├─ 边缘物理体面积为零 → 更容易出现穿透
│   └─ 修复:使用体积物理体作为墙(rectangleOf:)并设置isDynamic = false
├─ 墙的厚度是否足够?
│   └─ 修复:对于速度达600pt/s的物体,墙厚度至少设为10pt
└─ 碰撞位掩码是否正确?
    ├─ 墙的categoryBitMask必须在物体的collisionBitMask中
│   └─ 修复:通过打印验证:object.collisionBitMask & wall.categoryBitMask != 0

Symptom 3: Poor Frame Rate

症状3:帧率过低

Time saved: 2-4 hours → 15-30 min
FPS below 60 (or 120 on ProMotion)
├─ Check showsNodeCount
│   ├─ >1000 nodes → Offscreen nodes not removed
│   │   ├─ Are you removing nodes that leave the screen?
│   │   ├─ FIX: In update(), remove nodes outside visible area
│   │   └─ FIX: Use object pooling for frequently spawned objects
│   │
│   ├─ 200-1000 nodes → Likely manageable, check draw count
│   └─ <200 nodes → Nodes aren't the problem, check below
├─ Check showsDrawCount
│   ├─ >50 draw calls → Batching problem
│   │   ├─ Using SKShapeNode for gameplay? → Replace with pre-rendered textures
│   │   ├─ Sprites from different images? → Use texture atlas
│   │   ├─ Sprites at different zPositions? → Consolidate layers
│   │   └─ ignoresSiblingOrder = false? → Set to true
│   │
│   ├─ 10-50 draw calls → Acceptable for most games
│   └─ <10 draw calls → Drawing isn't the problem
├─ Physics expensive?
│   ├─ Many texture-based physics bodies → Use circles/rectangles
│   ├─ usesPreciseCollisionDetection on too many bodies → Use only on fast objects
│   ├─ Many contact callbacks firing → Reduce contactTestBitMask scope
│   └─ Complex polygon bodies → Simplify to fewer vertices
├─ Particle overload?
│   ├─ Multiple emitters active → Reduce particleBirthRate
│   ├─ High particleLifetime → Reduce (fewer active particles)
│   ├─ numParticlesToEmit = 0 (infinite) without cleanup → Add limits
│   └─ FIX: Profile with Instruments → Time Profiler
├─ SKEffectNode without shouldRasterize?
│   ├─ CIFilter re-renders every frame
│   └─ FIX: effectNode.shouldRasterize = true (if content is static)
└─ Complex update() logic?
    ├─ O(n²) collision checking? → Use physics engine instead
    ├─ String-based enumerateChildNodes every frame? → Cache references
    └─ Heavy computation in update? → Spread across frames or background
节省时间:2-4小时 → 15-30分钟
帧率低于60(ProMotion设备低于120)
├─ 检查showsNodeCount
│   ├─ 节点数>1000 → 未移除屏幕外节点
│   │   ├─ 是否移除了离开屏幕的节点?
│   │   ├─ 修复:在update()中移除可见区域外的节点
│   │   └─ 修复:对频繁生成的物体使用对象池
│   │
│   ├─ 节点数200-1000 → 通常可控,检查绘制次数
│   └─ 节点数<200 → 问题不在节点,检查以下项
├─ 检查showsDrawCount
│   ├─ 绘制次数>50 → 批处理问题
│   │   ├─ 是否在游戏中使用SKShapeNode? → 替换为预渲染纹理
│   │   ├─ 精灵来自不同图片? → 使用纹理图集
│   │   ├─ 精灵处于不同zPositions? → 合并图层
│   │   └─ ignoresSiblingOrder = false? → 设置为true
│   │
│   ├─ 绘制次数10-50 → 对大多数游戏来说可接受
│   └─ 绘制次数<10 → 问题不在绘制
├─ 物理计算开销过大?
│   ├─ 大量基于纹理的物理体 → 使用圆形/矩形物理体
│   ├─ 过多物体启用了usesPreciseCollisionDetection → 仅在快速移动物体上使用
│   ├─ 大量碰撞回调触发 → 缩小contactTestBitMask范围
│   └─ 复杂多边形物理体 → 简化为更少顶点
├─ 粒子效果过载?
│   ├─ 多个发射器活跃 → 降低particleBirthRate
│   ├─ particleLifetime过高 → 降低该值(减少活跃粒子数)
│   ├─ numParticlesToEmit = 0(无限发射)未清理 → 添加限制
│   └─ 修复:使用Instruments的Time Profiler进行性能分析
├─ SKEffectNode未设置shouldRasterize?
│   ├─ CIFilter会在每一帧重新渲染
│   └─ 修复:如果内容静态,设置effectNode.shouldRasterize = true
└─ update()逻辑过于复杂?
    ├─ O(n²)碰撞检测? → 改用物理引擎
│   ├─ 每帧基于字符串枚举子节点? → 缓存引用
│   └─ update()中计算量过大? → 分散到多帧或后台处理

Quick Performance Audit

快速性能审计

swift
#if DEBUG
private var frameCount = 0
#endif

override func update(_ currentTime: TimeInterval) {
    #if DEBUG
    frameCount += 1
    if frameCount % 60 == 0 {
        print("Nodes: \(children.count)")
    }
    #endif
}

swift
#if DEBUG
private var frameCount = 0
#endif

override func update(_ currentTime: TimeInterval) {
    #if DEBUG
    frameCount += 1
    if frameCount % 60 == 0 {
        print("Nodes: \(children.count)")
    }
    #endif
}

Symptom 4: Touches Not Registering

症状4:触摸无响应

Time saved: 15-45 min → 2 min
touchesBegan not called on a node
├─ Is isUserInteractionEnabled = true on the node?
│   ├─ SKScene: true by default
│   ├─ All other SKNode subclasses: FALSE by default
│   └─ FIX: node.isUserInteractionEnabled = true
├─ Is the node hidden or alpha = 0?
│   ├─ Hidden nodes don't receive touches
│   └─ FIX: Check node.isHidden and node.alpha
├─ Is another node on top intercepting touches?
│   ├─ Higher zPosition nodes with isUserInteractionEnabled get first chance
│   └─ DEBUG: Print nodes(at: touchLocation) to see what's there
├─ Is the touch in the correct coordinate space?
│   ├─ Using touch.location(in: self.view)? → WRONG for SpriteKit
│   └─ FIX: Use touch.location(in: self) for scene coordinates
│        Or touch.location(in: targetNode) for node-local coordinates
├─ Is the physics body blocking touch pass-through?
│   └─ Physics bodies don't affect touch handling — not the issue
└─ Is the node's frame correct?
    ├─ SKNode (container) has zero frame — can't be hit-tested by area
    ├─ SKSpriteNode frame matches texture size × scale
    └─ FIX: Use contains(point) or nodes(at:) for manual hit testing

节省时间:15-45分钟 → 2分钟
节点未响应touchesBegan
├─ 节点的isUserInteractionEnabled是否为true?
│   ├─ SKScene:默认true
│   ├─ 其他所有SKNode子类:默认FALSE
│   └─ 修复:node.isUserInteractionEnabled = true
├─ 节点是否隐藏或alpha = 0?
│   ├─ 隐藏节点无法接收触摸
│   └─ 修复:检查node.isHidden和node.alpha
├─ 是否有上层节点拦截触摸?
│   ├─ zPosition更高且isUserInteractionEnabled为true的节点会优先接收触摸
│   └─ 调试:打印nodes(at: touchLocation)查看触摸位置的节点
├─ 触摸是否使用了正确的坐标空间?
│   ├─ 是否使用touch.location(in: self.view)? → 对SpriteKit来说是错误的
│   └─ 修复:使用touch.location(in: self)获取场景坐标
│        或使用touch.location(in: targetNode)获取节点本地坐标
├─ 物理体是否阻止触摸穿透?
│   └─ 物理体不影响触摸处理 → 不是此问题的原因
└─ 节点的frame是否正确?
│   ├─ SKNode(容器)frame为零 → 无法通过区域点击测试
│   ├─ SKSpriteNode的frame匹配纹理尺寸×缩放比例
│   └─ 修复:使用contains(point)或nodes(at:)进行手动点击测试

Symptom 5: Memory Spikes and Crashes

症状5:内存飙升与崩溃

Time saved: 1-3 hours → 15 min
Memory grows during gameplay
├─ Nodes accumulating? (Check showsNodeCount over time)
│   ├─ Count increasing? → Nodes created but not removed
│   │   ├─ Missing removeFromParent() for expired objects
│   │   ├─ FIX: Add cleanup in update() or use SKAction.removeFromParent()
│   │   └─ FIX: Implement object pooling for frequently spawned items
│   │
│   └─ Count stable? → Memory issue elsewhere
├─ Infinite particle emitters?
│   ├─ numParticlesToEmit = 0 creates particles forever
│   ├─ Each emitter accumulates particles up to birthRate × lifetime
│   └─ FIX: Set finite numParticlesToEmit or manually stop and remove
├─ Texture caching?
│   ├─ SKTexture(imageNamed:) caches — repeated calls don't leak
│   ├─ SKTexture(cgImage:) from camera/dynamic sources → Not cached
│   └─ FIX: Reuse texture references for dynamic textures
├─ Strong reference cycles in actions?
│   ├─ SKAction.run { self.doSomething() } captures self strongly
│   ├─ In repeatForever, this prevents scene deallocation
│   └─ FIX: SKAction.run { [weak self] in self?.doSomething() }
├─ Scene not deallocating?
│   ├─ Add deinit { print("Scene deallocated") }
│   ├─ If never prints → retain cycle
│   ├─ Common: strong delegate, closure capture, NotificationCenter observer
│   └─ FIX: Clean up in willMove(from:):
│        removeAllActions()
│        removeAllChildren()
│        physicsWorld.contactDelegate = nil
└─ Instruments → Allocations
    ├─ Filter by "SK" to see SpriteKit objects
    ├─ Mark generation before/after scene transition
    └─ Persistent growth = leak

节省时间:1-3小时 → 15分钟
游戏过程中内存持续增长
├─ 节点不断累积?(随时间检查showsNodeCount)
│   ├─ 节点数增加? → 创建节点后未移除
│   │   ├─ 过期物体缺少removeFromParent()
│   │   ├─ 修复:在update()中添加清理逻辑或使用SKAction.removeFromParent()
│   │   └─ 修复:对频繁生成的物体使用对象池
│   │
│   └─ 节点数稳定? → 内存问题出在其他地方
├─ 无限粒子发射器?
│   ├─ numParticlesToEmit = 0会无限生成粒子
│   ├─ 每个发射器累积的粒子数可达birthRate × lifetime
│   └─ 修复:设置有限的numParticlesToEmit或手动停止并移除发射器
├─ 纹理缓存?
│   ├─ SKTexture(imageNamed:)会缓存 → 重复调用不会泄漏
│   ├─ 从相机/动态源创建的SKTexture(cgImage:) → 不会被缓存
│   └─ 修复:复用动态纹理的引用
├─ 动作中存在强引用循环?
│   ├─ SKAction.run { self.doSomething() }会强引用self
│   ├─ 在repeatForever中,这会阻止场景被释放
│   └─ 修复:SKAction.run { [weak self] in self?.doSomething() }
├─ 场景未被释放?
│   ├─ 添加deinit { print("Scene deallocated") }
│   ├─ 如果从未打印 → 存在引用循环
│   ├─ 常见原因:强引用代理、闭包捕获、NotificationCenter观察者
│   └─ 修复:在willMove(from:)中清理:
│        removeAllActions()
│        removeAllChildren()
│        physicsWorld.contactDelegate = nil
└─ 使用Instruments → Allocations工具
    ├─ 按"SK"过滤查看SpriteKit对象
    ├─ 在场景切换前后标记生成点
    └─ 持续增长 = 内存泄漏

Symptom 6: Coordinate Confusion

症状6:坐标混乱

Time saved: 20-60 min → 5 min
Positions seem wrong or flipped
├─ Y-axis confusion?
│   ├─ SpriteKit: origin at BOTTOM-LEFT, Y goes UP
│   ├─ UIKit: origin at TOP-LEFT, Y goes DOWN
│   └─ FIX: Use scene coordinate methods, not view coordinates
│        touch.location(in: self)  ← CORRECT (scene space)
│        touch.location(in: view)  ← WRONG (UIKit space, Y flipped)
├─ Anchor point confusion?
│   ├─ Scene anchor (0,0) = bottom-left of view is scene origin
│   ├─ Scene anchor (0.5,0.5) = center of view is scene origin
│   ├─ Sprite anchor (0.5,0.5) = center of sprite is at position (default)
│   ├─ Sprite anchor (0,0) = bottom-left of sprite is at position
│   └─ FIX: Print anchorPoint values and draw expected position
├─ Parent coordinate space?
│   ├─ node.position is relative to PARENT, not scene
│   ├─ Child at (0,0) of parent at (100,100) is at scene (100,100)
│   └─ FIX: Use convert(_:to:) and convert(_:from:) for cross-node coordinates
│        let scenePos = node.convert(localPoint, to: scene)
│        let localPos = node.convert(scenePoint, from: scene)
├─ Camera offset?
│   ├─ Camera position offsets the visible area
│   ├─ HUD attached to camera stays in place
│   └─ FIX: For world coordinates, account for camera position
│        scene.convertPoint(fromView: viewPoint)
└─ Scale mode cropping?
    ├─ aspectFill crops edges — content at edges may be offscreen
    └─ FIX: Keep important content in the "safe area" center

节省时间:20-60分钟 → 5分钟
位置显示异常或翻转
├─ Y轴混淆?
│   ├─ SpriteKit:原点在左下角,Y轴向上
│   ├─ UIKit:原点在左上角,Y轴向下
│   └─ 修复:使用场景坐标方法,而非视图坐标
│        touch.location(in: self)  ← 正确(场景空间)
│        touch.location(in: view)  ← 错误(UIKit空间,Y轴翻转)
├─ 锚点混淆?
│   ├─ 场景锚点(0,0) = 视图左下角为场景原点
│   ├─ 场景锚点(0.5,0.5) = 视图中心为场景原点
│   ├─ 精灵锚点(0.5,0.5) = 精灵中心为位置点(默认)
│   ├─ 精灵锚点(0,0) = 精灵左下角为位置点
│   └─ 修复:打印anchorPoint值并绘制预期位置
├─ 父节点坐标空间?
│   ├─ node.position是相对于父节点的坐标,而非场景
│   ├─ 父节点在(100,100)时,子节点(0,0)的场景坐标为(100,100)
│   └─ 修复:使用convert(_:to:)和convert(_:from:)进行跨节点坐标转换
│        let scenePos = node.convert(localPoint, to: scene)
│        let localPos = node.convert(scenePoint, from: scene)
├─ 相机偏移?
│   ├─ 相机位置会偏移可见区域
│   ├─ 附加到相机的HUD会保持固定位置
│   └─ 修复:如需世界坐标,需考虑相机位置
│        scene.convertPoint(fromView: viewPoint)
└─ 缩放模式裁剪?
    ├─ aspectFill会裁剪边缘 → 边缘内容可能超出屏幕
│   └─ 修复:将重要内容放在屏幕中心的"安全区域"

Symptom 7: Scene Transition Crashes

症状7:场景切换崩溃

Time saved: 30-90 min → 5 min
Crash during or after scene transition
├─ EXC_BAD_ACCESS after transition?
│   ├─ Old scene deallocated while something still references it
│   ├─ Common: Timer, NotificationCenter, delegate still referencing old scene
│   └─ FIX: Clean up in willMove(from:):
│        removeAllActions()
│        removeAllChildren()
│        physicsWorld.contactDelegate = nil
│        // Remove any NotificationCenter observers
├─ Crash in didMove(to:) of new scene?
│   ├─ Accessing view before it's available
│   ├─ Force-unwrapping optional that's nil during init
│   └─ FIX: Use guard let view = self.view in didMove(to:)
├─ Memory spike during transition?
│   ├─ Both scenes exist simultaneously during transition animation
│   ├─ For large scenes, this doubles memory usage
│   └─ FIX: Preload textures, reduce scene size, or use .fade transition
│        (fade briefly shows neither scene, reducing peak memory)
├─ Nodes from old scene appearing in new scene?
│   ├─ node.move(toParent:) during transition
│   └─ FIX: Don't move nodes between scenes — recreate in new scene
└─ didMove(to:) called twice?
    ├─ Presenting scene multiple times (button double-tap)
    └─ FIX: Disable transition trigger after first tap
         guard view?.scene !== nextScene else { return }

节省时间:30-90分钟 → 5分钟
场景切换期间或之后崩溃
├─ 切换后出现EXC_BAD_ACCESS?
│   ├─ 旧场景已被释放但仍有对象引用它
│   ├─ 常见原因:Timer、NotificationCenter、代理仍引用旧场景
│   └─ 修复:在willMove(from:)中清理:
│        removeAllActions()
│        removeAllChildren()
│        physicsWorld.contactDelegate = nil
│        // 移除所有NotificationCenter观察者
├─ 新场景的didMove(to:)中崩溃?
│   ├─ 在视图可用前访问视图
│   ├─ 初始化时强制解包nil可选值
│   └─ 修复:在didMove(to:)中使用guard let view = self.view
├─ 切换期间内存飙升?
│   ├─ 切换动画期间两个场景会同时存在
│   ├─ 对于大型场景,这会使内存使用量翻倍
│   └─ 修复:预加载纹理、缩小场景尺寸或使用.fade过渡
│        (fade过渡会短暂隐藏两个场景,降低峰值内存)
├─ 旧场景的节点出现在新场景中?
│   ├─ 切换期间调用了node.move(toParent:)
│   └─ 修复:不要在场景间移动节点 → 在新场景中重新创建
└─ didMove(to:)被调用两次?
│   ├─ 多次触发场景切换(如按钮双击)
│   └─ 修复:首次触发后禁用切换触发器
         guard view?.scene !== nextScene else { return }

Common Mistakes

常见错误

These mistakes cause the majority of SpriteKit issues. Check these first before diving into symptom trees.
  1. Leaving default bitmasks
    collisionBitMask
    defaults to
    0xFFFFFFFF
    (collides with everything). Always set all three masks explicitly.
  2. Forgetting
    contactTestBitMask
    — Defaults to
    0x00000000
    . Contacts never fire without setting this.
  3. Forgetting
    physicsWorld.contactDelegate = self
    — Fixes ~30% of contact issues on its own.
  4. Using SKShapeNode for gameplay — Each instance = 1 draw call. Pre-render to texture with
    view.texture(from:)
    .
  5. SKAction.move on physics bodies — Actions override physics, causing jitter and missed collisions. Use forces/impulses.
  6. Strong self in action closures
    SKAction.run { self.foo() }
    in
    repeatForever
    creates retain cycles. Use
    [weak self]
    .
  7. Not removing offscreen nodes — Node count climbs silently, degrading performance.
  8. Missing
    isUserInteractionEnabled = true
    — Default is
    false
    on all non-scene nodes.

这些错误导致了大多数SpriteKit问题。在深入症状排查前先检查这些项。
  1. 保留默认位掩码
    collisionBitMask
    默认值为
    0xFFFFFFFF
    (与所有物体碰撞)。始终显式设置三个位掩码。
  2. 忘记设置contactTestBitMask — 默认值为
    0x00000000
    。不设置此项碰撞永远不会触发。
  3. 忘记设置physicsWorld.contactDelegate = self — 仅此一项即可解决约30%的碰撞问题。
  4. 在游戏中使用SKShapeNode — 每个实例对应1次绘制调用。使用
    view.texture(from:)
    预渲染为纹理。
  5. 对物理体使用SKAction.move — 动作会覆盖物理引擎,导致抖动和碰撞丢失。使用力/冲量替代。
  6. 动作闭包中强引用self
    repeatForever
    中的
    SKAction.run { self.foo() }
    会创建引用循环。使用
    [weak self]
  7. 未移除屏幕外节点 — 节点数会悄然增加,导致性能下降。
  8. 未设置isUserInteractionEnabled = true — 所有非场景节点默认值为
    false

Diagnostic Quick Reference Card

诊断速查表

SymptomFirst CheckMost Likely Cause
Contacts don't fire
contactDelegate
set?
Missing
contactTestBitMask
TunnelingObject speed vs wall thicknessMissing
usesPreciseCollisionDetection
Low FPS
showsDrawCount
SKShapeNode in gameplay or missing atlas
Touches broken
isUserInteractionEnabled
?
Default is
false
on non-scene nodes
Memory growth
showsNodeCount
increasing?
Nodes created but never removed
Wrong positionsY-axis directionUsing view coordinates instead of scene
Transition crash
willMove(from:)
cleanup?
Strong references to old scene
症状首要检查项最可能原因
碰撞未触发是否设置了contactDelegate?缺少contactTestBitMask
物体穿墙物体速度与墙厚度的关系未启用usesPreciseCollisionDetection
帧率低showsDrawCount游戏中使用SKShapeNode或未使用纹理图集
触摸无响应isUserInteractionEnabled?非场景节点默认值为false
内存增长showsNodeCount是否持续增加?创建节点后从未移除
位置异常Y轴方向使用视图坐标而非场景坐标
切换崩溃willMove(from:)是否做了清理?对旧场景存在强引用

Resources

资源

WWDC: 2014-608, 2016-610, 2017-609
Docs: /spritekit/skphysicsbody, /spritekit/maximizing-node-drawing-performance
Skills: axiom-spritekit, axiom-spritekit-ref
WWDC:2014-608, 2016-610, 2017-609
文档:/spritekit/skphysicsbody, /spritekit/maximizing-node-drawing-performance
技能:axiom-spritekit, axiom-spritekit-ref