mcpField
A
axiom-spritekit-diag

axiom-spritekit-diag

github.com/charleswiltgen開発1.2k149 回インストール今日更新
リポジトリ

axiom-spritekit-diag について

SpriteKitのよくある問題を調べる開発者向けの診断スキルです。症状別の確認手順で、物理挙動、パフォーマンス、タッチ、メモリ、座標、シーン遷移の原因を探ります。

主な機能

  1. 01症状別の診断チェックリスト
  2. 02物理接触と衝突の確認
  3. 03フレームレートとメモリの調査
  4. 04タッチと座標の診断
  5. 05シーン遷移時のクラッシュ確認

活用シーン

  1. 01物理接触イベントが起きない原因の調査
  2. 02オブジェクトが壁を通り抜ける問題の調査
  3. 03フレームレート低下やメモリ増加の追跡
  4. 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 の問題の大半を引き起こします。症状別の診断に取りかかる前に、まず確認してください。

  1. デフォルトのビットマスクをそのままにする — collisionBitMask のデフォルト値は 0xFFFFFFFF(すべてと衝突)です。3 つのマスクすべてを必ず明示的に設定してください。
  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 です。

診断クイックリファレンスカード

症状最初に確認すること最も可能性の高い原因
コンタクトが発生しない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

コメント