axiom-spritekit-diag
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSpriteKit 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 doesn't show expected physics body outlines, your physics bodies aren't configured correctly. Stop and fix bodies before debugging contacts.
showsPhysicsFor SpriteKit architecture patterns and best practices, see . For API reference, see .
axiom-spritekitaxiom-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架构模式和最佳实践,请查看。如需API参考,请查看。
axiom-spritekitaxiom-spritekit-refSymptom 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 != 0Symptom 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.
- Leaving default bitmasks — defaults to
collisionBitMask(collides with everything). Always set all three masks explicitly.0xFFFFFFFF - Forgetting — Defaults to
contactTestBitMask. Contacts never fire without setting this.0x00000000 - Forgetting — Fixes ~30% of contact issues on its own.
physicsWorld.contactDelegate = self - Using SKShapeNode for gameplay — Each instance = 1 draw call. Pre-render to texture with .
view.texture(from:) - SKAction.move on physics bodies — Actions override physics, causing jitter and missed collisions. Use forces/impulses.
- Strong self in action closures — in
SKAction.run { self.foo() }creates retain cycles. UserepeatForever.[weak self] - Not removing offscreen nodes — Node count climbs silently, degrading performance.
- Missing — Default is
isUserInteractionEnabled = trueon all non-scene nodes.false
这些错误导致了大多数SpriteKit问题。在深入症状排查前先检查这些项。
- 保留默认位掩码 — 默认值为
collisionBitMask(与所有物体碰撞)。始终显式设置三个位掩码。0xFFFFFFFF - 忘记设置contactTestBitMask — 默认值为。不设置此项碰撞永远不会触发。
0x00000000 - 忘记设置physicsWorld.contactDelegate = self — 仅此一项即可解决约30%的碰撞问题。
- 在游戏中使用SKShapeNode — 每个实例对应1次绘制调用。使用预渲染为纹理。
view.texture(from:) - 对物理体使用SKAction.move — 动作会覆盖物理引擎,导致抖动和碰撞丢失。使用力/冲量替代。
- 动作闭包中强引用self — 中的
repeatForever会创建引用循环。使用SKAction.run { self.foo() }。[weak self] - 未移除屏幕外节点 — 节点数会悄然增加,导致性能下降。
- 未设置isUserInteractionEnabled = true — 所有非场景节点默认值为。
false
Diagnostic Quick Reference Card
诊断速查表
| Symptom | First Check | Most Likely Cause |
|---|---|---|
| Contacts don't fire | | Missing |
| Tunneling | Object speed vs wall thickness | Missing |
| Low FPS | | SKShapeNode in gameplay or missing atlas |
| Touches broken | | Default is |
| Memory growth | | Nodes created but never removed |
| Wrong positions | Y-axis direction | Using view coordinates instead of scene |
| Transition crash | | 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