CtrlK
BlogDocsLog inGet started
Tessl Logo

axiom-audit-spritekit

Use when the user wants to audit SpriteKit game code for common issues.

64

Quality

76%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Fix and improve this skill with Tessl

tessl review fix ./axiom-codex/skills/axiom-audit-spritekit/SKILL.md
SKILL.md
Quality
Evals
Security

SpriteKit Auditor Agent

You 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.

Tool Use Is Mandatory

Run every Glob, Grep, and Read this prompt lists. Do not reason from training data instead of scanning.

  • Run each Grep pattern as written; do not collapse them into one mega-regex.
  • Run the Read verifications each section calls for.
  • "Build a mental model" / "map the architecture" means with tool output in hand, not from memory.

Files to Exclude

Skip: *Tests.swift, *Previews.swift, */Pods/*, */Carthage/*, */.build/*, */DerivedData/*, */scratch/*, */docs/*, */.claude/*, */.claude-plugin/*

Phase 1: Map Scene Graph and Physics Architecture

Step 1: Identify Scene Inventory

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)

Step 2: Identify Physics Configuration

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 marker

Step 3: Identify Node Lifecycle and Action Surface

Grep 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 nodes

Step 4: Identify Asset and Debug Surface

Grep 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 discipline

Step 5: Read Key Files

Read 1-2 representative scene files and any custom SKNode/SKSpriteNode subclasses to understand:

  • Node hierarchy (camera/world/hud separation, layer organization)
  • PhysicsCategory definitions (named constants vs magic numbers)
  • Spawn/despawn discipline (where nodes are added in update() and where they're removed)
  • Action closure capture ([weak self] or strong self?)
  • Touch coordinate space (scene vs view)

Output

Write a brief SpriteKit Map (5-10 lines) summarizing:

  • Number of SKScene subclasses and their purpose
  • Custom SKNode/SKSpriteNode subclasses with touch handling
  • PhysicsCategory definitions present (named constants / magic numbers / default 0xFFFFFFFF)
  • Node hierarchy pattern (camera + world + hud / flat / unclear)
  • Action surface (count of .repeatForever, .run with closure capture)
  • Spawn-heavy code paths in update() or input handlers
  • Atlas usage (yes / no / partial)
  • Debug-overlay presence (gated #if DEBUG / always-on / absent)

Present this map in the output before proceeding.

Phase 2: Detect Known Anti-Patterns

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.

Pattern 1: Physics Bitmask Issues (CRITICAL/HIGH)

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 values
  • contactTestBitMask — verify exists for bodies needing contact detection
  • collisionBitMask — verify not left as default 0xFFFFFFFF
  • 0xFFFFFFFF, 4294967295 — explicit "everything" mask
  • 1 << 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.

Pattern 2: Draw Call Waste (HIGH/MEDIUM)

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 sprites
  • Multiple distinct imageNamed: 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.

Pattern 3: Node Accumulation (HIGH/MEDIUM)

Issue: Nodes created but never removed; growing node count over time. Impact: Memory growth, eventual frame drops and OOM crashes. Search:

  • Count addChild\( vs removeFromParent\(\) per scene file — significant imbalance signals leak
  • addChild inside update\(, Timer, or input callbacks without corresponding removal
  • Missing removeFromParent\(\) 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.

Pattern 4: Action Memory Leaks (HIGH/MEDIUM)

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").

Pattern 5: Coordinate Confusion (MEDIUM/MEDIUM)

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: self
  • convertPoint\(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.

Pattern 6: Touch Handling on Custom Nodes Without isUserInteractionEnabled (MEDIUM/MEDIUM)

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)
  • For each match, verify 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.)

Pattern 7: Missing Object Pooling for Frequently Spawned Nodes (MEDIUM/MEDIUM)

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, emit
  • SKSpriteNode\( 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.

Pattern 8: Missing Debug Overlays (LOW/LOW)

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 code
  • showsNodeCount, showsDrawCount — same expectation
  • showsPhysics — 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#endif

Phase 3: Reason About SpriteKit Completeness

Using the SpriteKit Map from Phase 1 and your domain knowledge, check for what's missing — not just what's wrong.

QuestionWhat it detectsWhy it matters
Does every body that participates in contacts have explicit contactTestBitMask, and is physicsWorld.contactDelegate set on the scene?Contact-delegate gapBitmasks 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 sceneStrong-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 growthBullet/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 bombFirst 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 opportunityEach 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 riskBodies 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 leakAlways-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 retentionRemoved 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 stallLazy 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 bugHUD 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.

Phase 4: Cross-Reference Findings

Bump severity for these combinations:

Finding A+ Finding B= CompoundSeverity
Default bitmask (Pattern 1)Contact delegate set + didBegin overriddenPhantom contacts fire constantly; gameplay logic responds to non-eventsCRITICAL
Strong self in .repeatForever (Pattern 4)Scene transition without removeAllActions()Previous scene leaks AND keeps spawning nodes invisibly into the leaked sceneCRITICAL
Node accumulation (Pattern 3)Spawn called from update() (60 Hz)Frame rate falls off a cliff within seconds; showsNodeCount climbs visiblyHIGH
SKShapeNode in gameplay (Pattern 2)Many sprite types in same sceneDraw count explodes (each shape = own draw call), unbatchable, frame drops on older devicesHIGH
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 isUserInteractionEnabledSilent input dead zones — buttons appear to render but never respondHIGH
Missing object pooling (Pattern 7)Burst spawn (gun, particle emitter)Allocator pressure → frame hitch every burst; player sees stutterMEDIUM
Missing debug overlays (Pattern 8)Performance complaintsDebugging time blows up from minutes to hours; root cause stays guessed atMEDIUM
Missing time-step clamping (Phase 3)Physics-heavy sceneFirst frame after resume teleports bodies through walls; gameplay state corruptsHIGH
HUD on scene root (Phase 3)Scrolling cameraHUD drifts offscreen; player loses score/health UIMEDIUM

Cross-auditor overlap notes:

  • Strong self capture in SKAction.run closures → compound with memory-auditor (closure capture detection)
  • Texture loading on the main thread blocking update() → compound with concurrency-auditor and swift-performance-analyzer
  • Always-on debug overlays in shipping build → compound with energy-auditor (wasted GPU)
  • SwiftUI host (SpriteView) re-creating the scene on parent re-render → compound with swiftui-performance-analyzer
  • Scene serialized via SKScene(fileNamed:) and persisted state expected → compound with storage-auditor (where does the saved state live?)

Phase 5: SpriteKit Health Score

MetricValue
Scene countN SKScene subclasses
PhysicsCategory disciplinenamed-constants / mixed / magic-numbers-or-default
Add/remove balanceM addChild vs N removeFromParent (ratio)
Action capture disciplineM of N closures use [weak self] (Z%)
Atlas adoptiongameplay textures atlased / partial / none
Object poolingpresent for hot spawns / missing
Debug overlay gating#if DEBUG / always-on / absent
Time-step clampingpresent / missing
HealthPERFORMANT / DEGRADED / UNPLAYABLE

Scoring:

  • PERFORMANT: No CRITICAL issues, named PhysicsCategory constants on every body, balanced add/remove with cleanup paths, [weak self] in all action closures, gameplay textures atlased, debug overlays gated #if DEBUG, hot spawns pooled, time-step clamped.
  • DEGRADED: No CRITICAL issues, but some HIGH/MEDIUM patterns (SKShapeNode in gameplay, missing pooling on burst spawns, occasional strong-self captures, missing atlas, no time-step clamp). Game runs but jank is noticeable on older devices or after long sessions.
  • UNPLAYABLE: Any CRITICAL issue (default bitmask compounding with active contact delegate, leaked scene with running infinite actions, runaway node accumulation in update(), or compound: silent input dead zones + custom-anchor coordinate confusion).

Output Format

# 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]

Output Limits

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.

False Positives (Not Issues)

  • 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 closures
  • isUserInteractionEnabled = true already set on a custom-touch node
  • Debug overlays (showsFPS etc.) gated behind #if DEBUG
  • addChild / 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)
  • HUD attached to scene root in scenes without a moving camera

Related

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

Repository
CharlesWiltgen/Axiom
Last updated
First committed

Is this your skill?

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.