关于 axiom-spritekit-diag
面向 SpriteKit 开发者的故障诊断技能,以症状为线索排查物理、性能、触控、内存、坐标和场景切换问题,并列出检查步骤与常见原因。
主要功能
- 01按症状组织的诊断清单
- 02物理接触与碰撞检查
- 03帧率和内存问题排查
- 04触控与坐标诊断
- 05场景切换崩溃检查
适用场景
- 01排查物理接触回调未触发
- 02调查物体穿过墙壁的问题
- 03追踪帧率下降或内存增长
- 04调试输入、位置或场景切换故障
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-spritekit-diag安装后在 Claude Code 中输入/axiom-spritekit-diag确认已加载。
详细介绍
SpriteKit 诊断
针对常见 SpriteKit 问题的系统化诊断,并标注所需时间。
何时使用此诊断技能
在以下情况下使用此技能:
- 物理接触事件始终不触发(未调用 didBegin)
- 物体穿过墙壁(隧穿)
- 帧率降至 60fps 以下
- 节点无法响应触摸
- 游戏过程中内存持续增长
- 位置和坐标似乎不正确
- 场景切换期间应用崩溃
强制执行的第一步:启用调试叠加层
耗时:设置需 10 秒,避免数小时盲目调试
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。
症状 1:物理接触事件不触发
节省时间:30–120 分钟 → 2–5 分钟
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(_:)
快速诊断打印
func didBegin(_ contact: SKPhysicsContact) {
print("CONTACT: \(contact.bodyA.node?.name ?? "nil") (\(contact.bodyA.categoryBitMask)) <-> \(contact.bodyB.node?.name ?? "nil") (\(contact.bodyB.categoryBitMask))")
}
如果始终没有打印内容,问题出在委托或位掩码设置上。如果有打印内容,但物体不正确,问题出在位掩码值上。
症状 2:物体穿墙
节省时间:20–60 分钟 → 5 分钟
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
症状 3:帧率低
节省时间:2–4 小时 → 15–30 分钟
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
快速性能检查
#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
}
症状 4:触摸事件未注册
节省时间:15–45 分钟 → 2 分钟
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
症状 5:内存激增并崩溃
节省时间:1–3 小时 → 15 分钟
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
症状 6:坐标混淆
节省时间:20–60 分钟 → 5 分钟
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
症状 7:场景切换时崩溃
节省时间:30–90 分钟 → 5 分钟
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 }
常见错误
以下错误是大多数 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。
诊断速查表
| 症状 | 首要检查项 | 最可能原因 |
|---|---|---|
| 接触事件不触发 | 是否设置了 contactDelegate? | 缺少 contactTestBitMask |
| 隧穿 | 物体速度与墙壁厚度 | 未设置 usesPreciseCollisionDetection |
| FPS 低 | showsDrawCount | 游戏中使用 SKShapeNode 或缺少纹理图集 |
| 触摸失效 | 是否启用了 isUserInteractionEnabled? | 非场景节点的默认值是 false |
| 内存增长 | showsNodeCount 是否持续增加? | 创建节点后未将其移除 |
| 位置错误 | Y 轴方向 | 使用了视图坐标,而非场景坐标 |
| 切换场景时崩溃 | 是否在 willMove(from:) 中执行清理? | 对旧场景存在强引用 |
资源
WWDC:2014-608、2016-610、2017-609
文档:/spritekit/skphysicsbody、/spritekit/maximizing-node-drawing-performance
技能:axiom-spritekit、axiom-spritekit-ref
评论
支持 Markdown