Use when the user wants to audit SpriteKit game code for common issues.
64
76%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./axiom-codex/skills/axiom-audit-spritekit/SKILL.mdYou are an expert at detecting SpriteKit issues — both known anti-patterns AND missing/incomplete patterns that cause physics bugs, frame drops, memory leaks, scene-transition crashes, and unplayable gameplay.
Run every Glob, Grep, and Read this prompt lists. Do not reason from training data instead of scanning.
Skip: *Tests.swift, *Previews.swift, */Pods/*, */Carthage/*, */.build/*, */DerivedData/*, */scratch/*, */docs/*, */.claude/*, */.claude-plugin/*
Glob: **/*.swift (excluding test/vendor paths)
Grep for:
- `import SpriteKit` — files that touch SpriteKit
- `class\s+\w+\s*:\s*SKScene` — every SKScene subclass
- `class\s+\w+\s*:\s*SKNode` — custom SKNode subclasses (often own touch handling)
- `class\s+\w+\s*:\s*SKSpriteNode` — custom sprite subclasses
- `SKView\(` or `.modelContainer\(SKView` or `SpriteView\(` — host integration (UIKit/SwiftUI)Grep for:
- `physicsBody\s*=` — physics body construction sites
- `physicsWorld` — global physics setup (gravity, contactDelegate, speed)
- `categoryBitMask`, `contactTestBitMask`, `collisionBitMask` — bitmask configuration
- `SKPhysicsContactDelegate`, `didBegin`, `didEnd` — contact delegate adoption
- `struct\s+PhysicsCategory`, `enum\s+PhysicsCategory` — named bitmask constants
- `usesPreciseCollisionDetection` — high-velocity body markerGrep for:
- `addChild\(`, `removeFromParent\(`, `removeAllChildren\(` — node lifecycle balance
- `SKAction\.run`, `SKAction\.customAction` — closure-capturing actions
- `\.repeatForever\(`, `\.repeat\(` — long-lived actions (need withKey)
- `run\(.*withKey:` — keyed actions (cancellable)
- `update\(_:`, `didEvaluateActions`, `didSimulatePhysics`, `didFinishUpdate` — game-loop hooks
- `func touchesBegan`, `func touchesMoved`, `func touchesEnded` — input surface
- `isUserInteractionEnabled` — input enable on non-scene nodesGrep for:
- `SKTextureAtlas\(`, `\.atlas` — atlas usage
- `SKShapeNode\(` — shape nodes (gameplay or debug?)
- `imageNamed:` or `SKTexture\(imageNamed:` — texture loading sites
- `showsFPS`, `showsNodeCount`, `showsDrawCount`, `showsPhysics`, `showsFields` — debug overlays
- `#if DEBUG` paired with debug-overlay flags — gating disciplineRead 1-2 representative scene files and any custom SKNode/SKSpriteNode subclasses to understand:
update() and where they're removed)[weak self] or strong self?)Write a brief SpriteKit Map (5-10 lines) summarizing:
.repeatForever, .run with closure capture)update() or input handlersPresent this map in the output before proceeding.
Run all 8 detection patterns. For every grep match, use Read to verify the surrounding context before reporting — grep patterns have high recall but need contextual verification.
Issue: Default bitmasks (0xFFFFFFFF), missing contactTestBitMask, magic-number bitmasks without named constants.
Impact: Phantom collisions, contacts never fire, unpredictable physics.
Search:
categoryBitMask — verify set to explicit named valuescontactTestBitMask — verify exists for bodies needing contact detectioncollisionBitMask — verify not left as default 0xFFFFFFFF0xFFFFFFFF, 4294967295 — explicit "everything" mask1 << outside a PhysicsCategory definition — magic-number bitmasks
Verify: Read matching files; check for a PhysicsCategory struct/enum that names each bitmask.
Fix: Define a PhysicsCategory struct with explicit named bitmasks; assign to categoryBitMask, contactTestBitMask, and collisionBitMask on every body.Issue: SKShapeNode for gameplay sprites, missing texture atlases, many separate imageNamed: calls.
Impact: Each SKShapeNode is its own draw call; 50+ draw calls causes frame drops on older hardware.
Search:
SKShapeNode\( — check whether used for gameplay (not just debug)SKTextureAtlas, \.atlas — should exist for games with many spritesimageNamed: calls in the same scene — should use atlas
Verify: Read matching files; SKShapeNode in gameplay = problem, SKShapeNode behind #if DEBUG = fine.
Fix: Pre-render shapes to textures via SKView.texture(from:); collect related sprites into a SKTextureAtlas.Issue: Nodes created but never removed; growing node count over time. Impact: Memory growth, eventual frame drops and OOM crashes. Search:
addChild\( vs removeFromParent\(\) per scene file — significant imbalance signals leakaddChild inside update\(, Timer, or input callbacks without corresponding removalremoveFromParent\(\) in bullet/projectile/effect lifecycle (fire, spawn, emit)
Verify: Read the spawn site and search for the corresponding cleanup (offscreen check, TTL action, contact handler removal).
Fix: Remove offscreen nodes via intersects(scene.frame) check, time-out actions ending in .removeFromParent(), or implement object pooling.Issue: Strong self capture in action closures; .repeatForever without withKey:.
Impact: Retain cycles prevent scene deallocation; previous scene's actions keep running invisibly after transition.
Search:
SKAction\.run\s*\{ or SKAction\.run\( — check for [weak self]\.repeatForever\( — check for withKey: parameter on run(_:withKey:)SKAction\.customAction — check for [weak self]
Verify: Read matching files; confirm closure body actually references self. Closures that don't reference self don't need [weak self].
Fix: SKAction.run { [weak self] in self?.doThing() }; for cancellable infinite actions, node.run(action, withKey: "spawnLoop") so it can be node.removeAction(forKey: "spawnLoop").Issue: Using view coordinates instead of scene coordinates in touch handlers. Impact: Touch positions are Y-flipped relative to expectations; nodes appear to react in the wrong location. Search:
touch\.location\(in:\s*self\.view, touch\.location\(in:\s*view — should be in: selfconvertPoint\(fromView: — verify direction is correct (view → scene, not scene → view by accident)
Verify: Read matching files; in SKScene.touchesBegan, the correct call is touch.location(in: self).
Fix: let location = touch.location(in: self) inside an SKScene's touch handler.Issue: Implementing touchesBegan on a custom SKNode/SKSpriteNode without setting isUserInteractionEnabled = true.
Impact: Touches never reach the node; the override is silently dead code.
Search:
touchesBegan, touchesMoved, touchesEnded, touchesCancelled overrides in classes inheriting from SKNode/SKSpriteNode (not SKScene)isUserInteractionEnabled = true is set in init or didMove
Verify: Read the class init; flag if no isUserInteractionEnabled assignment is present.
Fix: Set self.isUserInteractionEnabled = true in the node's init before any touch override matters. (SKScene defaults to true.)Issue: Creating new SKSpriteNode instances in tight gameplay loops (bullets, particles, enemies).
Impact: GC and ARC pressure, allocator fragmentation, frame drops during intense action.
Search:
SKSpriteNode\( inside methods named spawn, fire, shoot, create, emitSKSpriteNode\( inside update\(, Timer, or SKAction.run bodies
Verify: Read matching files; estimate spawn frequency from surrounding code.
Fix: Pre-allocate a pool of nodes, deactivate (isHidden, removeFromParent) on despawn, reactivate (addChild, reset position) on respawn.Issue: No debug overlays configured in development builds. Impact: Performance and physics problems go unnoticed; debugging takes 30-120 min instead of 2-5 min. Search:
showsFPS — should appear at least once in DEBUG-gated codeshowsNodeCount, showsDrawCount — same expectationshowsPhysics — required for physics-body diagnosis
Verify: Read matching files; confirm overlay flags are gated behind #if DEBUG (always-on in production also flagged).
Fix: In view setup: #if DEBUG\nview.showsFPS = true\nview.showsNodeCount = true\nview.showsDrawCount = true\nview.showsPhysics = true\n#endifUsing the SpriteKit Map from Phase 1 and your domain knowledge, check for what's missing — not just what's wrong.
| Question | What it detects | Why it matters |
|---|---|---|
Does every body that participates in contacts have explicit contactTestBitMask, and is physicsWorld.contactDelegate set on the scene? | Contact-delegate gap | Bitmasks set but delegate missing → contacts silently never fire; the bug looks like physics, not wiring |
For every scene transition, are timers invalidated, child nodes removed, and removeAllActions() called on the outgoing scene's persistent nodes? | Leaked previous scene | Strong-ref or running-action survivors keep the old scene alive driving invisible work and memory growth |
Are nodes spawned in update() or on a recurring SKAction removed when they leave the play area (offscreen check or TTL action)? | Unbounded growth | Bullet/particle/effect leaks; showsNodeCount rises forever until OOM |
Does the game loop clamp delta time on update(_:) to prevent the spiral-of-death after backgrounding or breakpoints? | Time-step bomb | First frame after resume gets a multi-second delta → physics teleports through walls, simulation explodes |
Are gameplay sprites loaded from a single SKTextureAtlas (vs many independent imageNamed: calls)? | Atlas opportunity | Each non-atlas texture forces its own draw call; atlas batching reduces draw count by an order of magnitude |
For every fast-moving body, is usesPreciseCollisionDetection = true set on the moving body (not the static wall)? | Tunneling risk | Bodies travelling > wall_thickness × frame_rate per second pass through walls without precise CCD |
Are debug overlays (showsFPS, showsNodeCount, showsDrawCount, showsPhysics) gated behind #if DEBUG so they don't ship to production? | Production debug leak | Always-on overlays cost frames and confuse users; production builds should be clean |
Do custom SKNode subclasses with their own state release that state on removeFromParent() (cancel observers, stop running actions, drop strong refs)? | Detached-node retention | Removed nodes still consume memory and may resurrect later via timer callbacks |
Are textures preloaded asynchronously (SKTextureAtlas.preload) before the scene presents, rather than loaded lazily on first display? | First-frame stall | Lazy loading on first scene display causes a 1-2 second hitch as the GPU pages textures in |
| Does the scene have a clear layer structure (camera + world + hud) with HUD attached to the camera, not the scene root? | HUD-scrolls-with-world bug | HUD added directly to scene scrolls when camera moves, causing labels to drift offscreen |
Require evidence from the Phase 1 map — don't speculate without reading the code.
Bump severity for these combinations:
| Finding A | + Finding B | = Compound | Severity |
|---|---|---|---|
| Default bitmask (Pattern 1) | Contact delegate set + didBegin overridden | Phantom contacts fire constantly; gameplay logic responds to non-events | CRITICAL |
Strong self in .repeatForever (Pattern 4) | Scene transition without removeAllActions() | Previous scene leaks AND keeps spawning nodes invisibly into the leaked scene | CRITICAL |
| Node accumulation (Pattern 3) | Spawn called from update() (60 Hz) | Frame rate falls off a cliff within seconds; showsNodeCount climbs visibly | HIGH |
| SKShapeNode in gameplay (Pattern 2) | Many sprite types in same scene | Draw count explodes (each shape = own draw call), unbatchable, frame drops on older devices | HIGH |
| Coordinate confusion (Pattern 5) | Custom anchor points (non-default anchorPoint) | Touch lands but on the wrong sprite; debugging takes hours because both layers feel "almost right" | HIGH |
Custom SKNode touchesBegan (Pattern 6) | Missing isUserInteractionEnabled | Silent input dead zones — buttons appear to render but never respond | HIGH |
| Missing object pooling (Pattern 7) | Burst spawn (gun, particle emitter) | Allocator pressure → frame hitch every burst; player sees stutter | MEDIUM |
| Missing debug overlays (Pattern 8) | Performance complaints | Debugging time blows up from minutes to hours; root cause stays guessed at | MEDIUM |
| Missing time-step clamping (Phase 3) | Physics-heavy scene | First frame after resume teleports bodies through walls; gameplay state corrupts | HIGH |
| HUD on scene root (Phase 3) | Scrolling camera | HUD drifts offscreen; player loses score/health UI | MEDIUM |
Cross-auditor overlap notes:
self capture in SKAction.run closures → compound with memory-auditor (closure capture detection)update() → compound with concurrency-auditor and swift-performance-analyzerenergy-auditor (wasted GPU)SpriteView) re-creating the scene on parent re-render → compound with swiftui-performance-analyzerSKScene(fileNamed:) and persisted state expected → compound with storage-auditor (where does the saved state live?)| Metric | Value |
|---|---|
| Scene count | N SKScene subclasses |
| PhysicsCategory discipline | named-constants / mixed / magic-numbers-or-default |
| Add/remove balance | M addChild vs N removeFromParent (ratio) |
| Action capture discipline | M of N closures use [weak self] (Z%) |
| Atlas adoption | gameplay textures atlased / partial / none |
| Object pooling | present for hot spawns / missing |
| Debug overlay gating | #if DEBUG / always-on / absent |
| Time-step clamping | present / missing |
| Health | PERFORMANT / DEGRADED / UNPLAYABLE |
Scoring:
[weak self] in all action closures, gameplay textures atlased, debug overlays gated #if DEBUG, hot spawns pooled, time-step clamped.update(), or compound: silent input dead zones + custom-anchor coordinate confusion).# SpriteKit Audit Results
## SpriteKit Map
[5-10 line summary from Phase 1]
## Summary
- CRITICAL: [N] issues
- HIGH: [N] issues
- MEDIUM: [N] issues
- LOW: [N] issues
- Phase 2 (pattern detection): [N] issues
- Phase 3 (completeness reasoning): [N] issues
- Phase 4 (compound findings): [N] issues
## SpriteKit Health Score
[Phase 5 table]
## Issues by Severity
### [SEVERITY/CONFIDENCE] [Pattern Name]: [Description]
**File**: path/to/file.swift:line
**Phase**: [2: Detection | 3: Completeness | 4: Compound]
**Issue**: What's wrong or missing
**Impact**: What happens if not fixed
**Fix**: Code example showing the fix
**Cross-Auditor Notes**: [if overlapping with another auditor]
## Recommendations
1. [Immediate actions — CRITICAL fixes (bitmask discipline, leaked-scene cleanup, runaway spawns)]
2. [Short-term — HIGH fixes (atlas migration, pooling, time-step clamping)]
3. [Long-term — completeness gaps from Phase 3 (texture preload, layer structure, async asset loading)]
4. [Test plan — `showsNodeCount` over 5 minutes, `showsDrawCount` per scene, scene-transition memory check, fast-body tunneling test]If >50 issues in one category: Show top 10, provide total count, list top 3 files. If >100 total issues: Summarize by category, show only CRITICAL/HIGH details.
PhysicsCategory struct/enum definitions themselves (these are the FIX, not the problem)SKShapeNode used only behind #if DEBUG for visualization[weak self] already present in action closuresisUserInteractionEnabled = true already set on a custom-touch nodeshowsFPS etc.) gated behind #if DEBUGaddChild / removeFromParent count imbalance where the missing removals are TTL actions ending in .removeFromParent()imageNamed: for one-off background textures (atlas overhead exceeds the benefit for a single texture)update(_:) without time-step clamping in turn-based or non-physics games (clamping is irrelevant)For SpriteKit architecture and patterns: axiom-games (skills/spritekit.md)
For SpriteKit API reference: axiom-games (skills/spritekit-ref.md)
For SpriteKit diagnostics (contacts not firing, tunneling, frame drops): axiom-games (skills/spritekit-diag.md)
For action closure capture leaks: memory-auditor agent
For main-thread asset loading: concurrency-auditor agent
For SwiftUI host (SpriteView) re-creation churn: swiftui-performance-analyzer agent
ea3be7c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.