axiom-spritekit-diag
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-spritekit-diag
Agent 安装分布
Skill 文档
SpriteKit Diagnostics
Systematic diagnosis for common SpriteKit issues with time-cost annotations.
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
Mandatory First Step: Enable Debug Overlays
Time cost: 10 seconds setup vs hours of blind debugging
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.
Symptom 1: Physics Contacts Not Firing
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(_:)
Quick Diagnostic Print
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.
Symptom 2: Objects Tunneling Through Walls
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
Symptom 3: Poor Frame Rate
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
Quick Performance Audit
#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
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
Symptom 5: Memory Spikes and Crashes
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
Symptom 6: Coordinate Confusion
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
Symptom 7: Scene Transition Crashes
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 }
Common Mistakes
These mistakes cause the majority of SpriteKit issues. Check these first before diving into symptom trees.
- Leaving default bitmasks â
collisionBitMaskdefaults to0xFFFFFFFF(collides with everything). Always set all three masks explicitly. - Forgetting
contactTestBitMaskâ Defaults to0x00000000. Contacts never fire without setting this. - Forgetting
physicsWorld.contactDelegate = selfâ Fixes ~30% of contact issues on its own. - 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 â
SKAction.run { self.foo() }inrepeatForevercreates retain cycles. Use[weak self]. - Not removing offscreen nodes â Node count climbs silently, degrading performance.
- Missing
isUserInteractionEnabled = trueâ Default isfalseon all non-scene nodes.
Diagnostic Quick Reference Card
| Symptom | First Check | Most Likely Cause |
|---|---|---|
| Contacts don’t fire | contactDelegate set? |
Missing contactTestBitMask |
| Tunneling | Object speed vs wall thickness | Missing 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 positions | Y-axis direction | Using view coordinates instead of scene |
| Transition crash | willMove(from:) cleanup? |
Strong references to old scene |
Resources
WWDC: 2014-608, 2016-610, 2017-609
Docs: /spritekit/skphysicsbody, /spritekit/maximizing-node-drawing-performance
Skills: axiom-spritekit, axiom-spritekit-ref