axiom-spritekit-diag by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-spritekit-diag针对常见 SpriteKit 问题的系统性诊断,附带耗时标注。
在以下情况时使用此技能:
didBegin 未被调用)耗时:10 秒设置 vs 数小时的盲目调试
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。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
节省时间: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(_:) 中处理
func didBegin(_ contact: SKPhysicsContact) {
print("CONTACT: \(contact.bodyA.node?.name ?? "nil") (\(contact.bodyA.categoryBitMask)) <-> \(contact.bodyB.node?.name ?? "nil") (\(contact.bodyB.categoryBitMask))")
}
如果这从未打印,问题在于委托/位掩码设置。如果打印了但物体不对,问题在于位掩码值。
节省时间:20-60 分钟 → 5 分钟
快速物体穿过薄墙
│
├─ 物体移动速度是否超过每帧墙的厚度?
│ ├─ 在 60fps 下:最大安全速度 = 墙厚度 × 60 pt/s
│ ├─ 10pt 厚的墙对于约 600 pt/s 的速度是安全的
│ └─ 修复:在快速物体上设置 usesPreciseCollisionDetection = true
│
├─ usesPreciseCollisionDetection 启用了吗?
│ ├─ 仅需在移动的物体上设置(而不是墙上)
│ └─ 修复:fastObject.physicsBody?.usesPreciseCollisionDetection = true
│
├─ 墙是边缘体吗?
│ ├─ 边缘体面积为零——更容易发生穿隧
│ └─ 修复:使用体积体作为墙(rectangleOf:)并设置 isDynamic = false
│
├─ 墙足够厚吗?
│ └─ 修复:对于速度高达 600pt/s 的物体,墙至少 10pt 厚
│
└─ 碰撞位掩码正确吗?
├─ 墙的 categoryBitMask 必须在物体的 collisionBitMask 中
└─ 修复:用打印验证:object.collisionBitMask & wall.categoryBitMask != 0
节省时间:2-4 小时 → 15-30 分钟
FPS 低于 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²) 的碰撞检查? → 改用物理引擎
├─ 每帧使用基于字符串的 enumerateChildNodes? → 缓存引用
└─ update 中有繁重的计算? → 分散到多帧或在后台进行
#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
}
节省时间:15-45 分钟 → 2 分钟
节点上的 touchesBegan 未被调用
│
├─ 节点上的 isUserInteractionEnabled = true 吗?
│ ├─ SKScene:默认为 true
│ ├─ 所有其他 SKNode 子类:默认为 FALSE
│ └─ 修复:node.isUserInteractionEnabled = true
│
├─ 节点是否隐藏或 alpha = 0?
│ ├─ 隐藏的节点不接收触摸
│ └─ 修复:检查 node.isHidden 和 node.alpha
│
├─ 是否有另一个节点在上面拦截触摸?
│ ├─ 具有 isUserInteractionEnabled 的更高 zPosition 节点优先获得机会
│ └─ 调试:打印 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:) 进行手动命中测试
节省时间: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 对象
├─ 在场景转换前后标记生成
└─ 持续增长 = 泄漏
节省时间: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) = 精灵的中心在 position 处(默认)
│ ├─ 精灵锚点 (0,0) = 精灵的左下角在 position 处
│ └─ 修复:打印 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 会裁剪边缘——边缘的内容可能在屏幕外
└─ 修复:将重要内容保持在“安全区域”中心
节省时间:30-90 分钟 → 5 分钟
场景转换期间或之后崩溃
│
├─ 转换后出现 EXC_BAD_ACCESS?
│ ├─ 旧场景已释放,但仍有东西引用它
│ ├─ 常见原因:Timer、NotificationCenter、委托仍在引用旧场景
│ └─ 修复:在 willMove(from:) 中清理:
│ removeAllActions()
│ removeAllChildren()
│ physicsWorld.contactDelegate = nil
│ // 移除任何 NotificationCenter 观察者
│
├─ 在新场景的 didMove(to:) 中崩溃?
│ ├─ 在视图可用之前访问它
│ ├─ 强制解包在 init 期间为 nil 的可选值
│ └─ 修复:在 didMove(to:) 中使用 guard let view = self.view
│
├─ 转换期间内存激增?
│ ├─ 两个场景在转换动画期间同时存在
│ ├─ 对于大型场景,这会加倍内存使用量
│ └─ 修复:预加载纹理,减少场景大小,或使用 .fade 转换
│ (淡入淡出短暂地不显示任何场景,减少峰值内存)
│
├─ 旧场景的节点出现在新场景中?
│ ├─ 转换期间使用 node.move(toParent:)
│ └─ 修复:不要在场景之间移动节点——在新场景中重新创建
│
└─ didMove(to:) 被调用了两次?
├─ 多次呈现场景(按钮双击)
└─ 修复:在第一次点击后禁用转换触发器
guard view?.scene !== nextScene else { return }
这些错误导致了大多数 SpriteKit 问题。在深入研究症状树之前,请先检查这些。
collisionBitMask 默认为 0xFFFFFFFF(与所有物体碰撞)。始终明确设置所有三个掩码。contactTestBitMask — 默认为 0x00000000。不设置此值,接触永远不会触发。physicsWorld.contactDelegate = self — 仅此一项即可修复约 30% 的接触问题。view.texture(from:) 预渲染到纹理。SKAction.run { self.foo() } 在 repeatForever 中会创建保留循环。使用 [weak self]。isUserInteractionEnabled = true — 在所有非场景节点上默认为 false。| 症状 | 首先检查 | 最可能的原因 |
|---|---|---|
| 接触未触发 | contactDelegate 设置了吗? | 缺少 contactTestBitMask |
| 穿隧效应 | 物体速度 vs 墙厚度 | 缺少 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
每周安装次数
70
代码仓库
GitHub 星标数
601
首次出现
2026年2月5日
安全审计
安装于
opencode66
gemini-cli63
codex62
github-copilot61
kimi-cli60
amp59
Systematic diagnosis for common SpriteKit issues with time-cost annotations.
Use this skill when:
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.
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(_:)
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.
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
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
#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
}
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
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
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
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 }
These mistakes cause the majority of SpriteKit issues. Check these first before diving into symptom trees.
collisionBitMask defaults to 0xFFFFFFFF (collides with everything). Always set all three masks explicitly.contactTestBitMask — Defaults to 0x00000000. Contacts never fire without setting this.physicsWorld.contactDelegate = self — Fixes ~30% of contact issues on its own.view.texture(from:).SKAction.run { self.foo() } in creates retain cycles. Use .| 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 |
WWDC : 2014-608, 2016-610, 2017-609
Docs : /spritekit/skphysicsbody, /spritekit/maximizing-node-drawing-performance
Skills : axiom-spritekit, axiom-spritekit-ref
Weekly Installs
70
Repository
GitHub Stars
601
First Seen
Feb 5, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode66
gemini-cli63
codex62
github-copilot61
kimi-cli60
amp59
repeatForever[weak self]isUserInteractionEnabled = true — Default is false on all 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 |