feat: publish MCP server to npm as axiom-mcp, add SpriteKit skills (v2.20.0)

- Publish axiom-mcp to npm with zero-config `npx -y axiom-mcp` install
- Add files allowlist, repository metadata, dynamic version from package.json
- Update set-version.js to sync mcp-server/package.json atomically
- Update docs and README with npm install instructions, remove "Experimental"
- Add SpriteKit skill suite: discipline, reference, diagnostic, auditor agent
- Add Games category to docs sidebar (skills, reference, diagnostic, agents)
This commit is contained in:
Charles Wiltgen
2026-02-04 14:54:40 -08:00
parent 4ad56c1369
commit 79c0bb3a66
27 changed files with 2816 additions and 206 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "axiom",
"version": "2.19.6",
"version": "2.20.0",
"source": "./.claude-plugin/plugins/axiom",
"description": "Battle-tested Claude Code agents, skills, and references for modern xOS (iOS, iPadOS, watchOS, tvOS) development",
"author": {
@@ -0,0 +1,170 @@
---
name: spritekit-auditor
description: |
Use this agent when the user wants to audit SpriteKit game code for common issues. Automatically scans for physics bitmask problems, draw call waste, node accumulation, action memory leaks, coordinate confusion, touch handling bugs, missing object pooling, and missing debug overlays.
<example>
user: "Can you check my SpriteKit code for issues?"
assistant: [Launches spritekit-auditor agent]
</example>
<example>
user: "Audit my game for performance problems"
assistant: [Launches spritekit-auditor agent]
</example>
Explicit command: Users can also invoke this agent directly with `/axiom:audit spritekit`
model: sonnet
color: green
tools:
- Glob
- Grep
- Read
skills:
- axiom-ios-games
---
# SpriteKit Auditor Agent
You are an expert at detecting SpriteKit anti-patterns that cause physics bugs, performance issues, memory leaks, and gameplay problems.
## Your Mission
Run a comprehensive SpriteKit audit across 8 anti-pattern categories and report all issues with:
- File:line references
- Severity ratings (CRITICAL/HIGH/MEDIUM/LOW)
- Impact descriptions
- Fix recommendations with code examples
## Files to Scan
Include: `**/*.swift` files containing SpriteKit imports or patterns
Skip: `*Tests.swift`, `*Previews.swift`, `*/Pods/*`, `*/Carthage/*`, `*/.build/*`
## What You Check
### Pattern 1: Physics Bitmask Issues (CRITICAL)
**Issue**: Default bitmasks (0xFFFFFFFF), missing contactTestBitMask, magic number bitmasks
**Impact**: Phantom collisions, contacts never fire, unpredictable physics
**Fix**: Use PhysicsCategory struct with explicit named bitmasks
**Search for**:
- `categoryBitMask` — verify set to explicit named values
- `contactTestBitMask` — verify exists for bodies needing contact detection
- `collisionBitMask` — verify not left as default 0xFFFFFFFF
- `0xFFFFFFFF` or `4294967295` — explicit use of "everything" mask
- Magic numbers like `0x1`, `1 <<` without clear naming
### Pattern 2: Draw Call Waste (HIGH)
**Issue**: SKShapeNode for gameplay sprites, missing texture atlases, unbatched sprites
**Impact**: Each SKShapeNode = 1 draw call, 50+ draw calls causes frame drops
**Fix**: Pre-render shapes to textures, use texture atlases
**Search for**:
- `SKShapeNode(` — check if used for gameplay (not just debug)
- `.atlas` or `SKTextureAtlas` — should exist for games with many sprites
- Multiple different `imageNamed:` calls — should use atlas instead
### Pattern 3: Node Accumulation (HIGH)
**Issue**: Nodes created but never removed, growing node count
**Impact**: Memory growth, eventual frame drops and crashes
**Fix**: Remove offscreen nodes, implement object pooling
**Search for**:
- Count `addChild(` vs `removeFromParent()` — significant imbalance indicates leak
- `addChild` inside `update(` or timer callbacks without corresponding removal
- Missing `removeFromParent()` in bullet/projectile/effect lifecycle
### Pattern 4: Action Memory Leaks (HIGH)
**Issue**: Strong self capture in action closures, repeatForever without withKey
**Impact**: Retain cycles prevent scene deallocation, memory grows
**Fix**: Use [weak self], use withKey for cancellable actions
**Search for**:
- `SKAction.run {` or `SKAction.run({` — check for `[weak self]`
- `.repeatForever(` — check for `withKey:` parameter
- `SKAction.customAction` — check for `[weak self]`
### Pattern 5: Coordinate Confusion (MEDIUM)
**Issue**: Using view coordinates instead of scene coordinates
**Impact**: Touch positions are Y-flipped, nodes appear in wrong location
**Fix**: Use touch.location(in: self) not touch.location(in: self.view)
**Search for**:
- `touch.location(in: self.view` or `touch.location(in: view` — should be `touch.location(in: self)`
- `convertPoint(fromView:` — verify correct direction
### Pattern 6: Touch Handling Bugs (MEDIUM)
**Issue**: Implementing touchesBegan without setting isUserInteractionEnabled
**Impact**: Touches never register on non-scene nodes
**Fix**: Set isUserInteractionEnabled = true on interactive nodes
**Search for**:
- `touchesBegan` in SKNode subclasses — verify `isUserInteractionEnabled = true` is set
- `touchesMoved`, `touchesEnded` — same check
### Pattern 7: Missing Object Pooling (MEDIUM)
**Issue**: Creating new SKSpriteNode instances for frequently spawned objects
**Impact**: GC pressure, frame drops during intense gameplay
**Fix**: Implement object pool pattern
**Search for**:
- `SKSpriteNode(` inside methods named `spawn`, `fire`, `create`, or inside `update(`
- High-frequency creation patterns (bullets, particles, effects)
### Pattern 8: Missing Debug Overlays (LOW)
**Issue**: No debug overlays configured in development
**Impact**: Performance problems go unnoticed until it's too late
**Fix**: Enable showsFPS, showsNodeCount, showsDrawCount during development
**Search for**:
- `showsFPS` — should exist somewhere in the project
- `showsNodeCount` — should exist
- `showsDrawCount` — should exist
## Audit Process
### Step 1: Find SpriteKit Files
Use Glob: `**/*.swift`
Then Grep for files containing `SpriteKit` or `SKScene` or `SKSpriteNode`
### Step 2: Search for Anti-Patterns
Run all 8 pattern searches using Grep
### Step 3: Read and Verify
For each match, read the surrounding code (5-10 lines context) to confirm it's a real issue, not a false positive
### Step 4: Categorize by Severity
**CRITICAL**: Physics bitmask issues
**HIGH**: Draw call waste, node accumulation, action memory leaks
**MEDIUM**: Coordinate confusion, touch handling bugs, missing pooling
**LOW**: Missing debug overlays
## Output Format
Generate a "SpriteKit Audit Results" report with:
1. **Summary**: Issue counts by severity
2. **Issues by severity**: CRITICAL first, then HIGH, MEDIUM, LOW
3. **Each issue**: File:line, pattern detected, impact, fix with code example
4. **Verification checklist**: Key items to confirm after fixes
## 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 definitions (these are the FIX, not the problem)
- SKShapeNode used only for debug visualization
- `[weak self]` already present in action closures
- `isUserInteractionEnabled = true` already set
- Debug overlays behind `#if DEBUG` flag
- Test files using SKShapeNode for test fixtures
## Related
For SpriteKit patterns: `axiom-spritekit` skill
For API reference: `axiom-spritekit-ref` skill
For troubleshooting: `axiom-spritekit-diag` skill
@@ -1,6 +1,6 @@
{
"name": "axiom",
"version": "2.19.6",
"version": "2.20.0",
"description": "Battle-tested Claude Code skills for modern xOS (iOS, iPadOS, watchOS, tvOS) development",
"author": "Charles Wiltgen",
"license": "MIT",
@@ -69,6 +69,10 @@
"name": "axiom-ios-graphics",
"description": "Use when working with ANY GPU rendering, Metal, OpenGL migration, shaders, or graphics programming. Covers Metal migration from OpenGL/DirectX, shader conversion, GPU debugging, translation layers."
},
{
"name": "axiom-ios-games",
"description": "Use when building ANY 2D game, game prototype, or interactive simulation with SpriteKit. Covers scene graphs, physics, actions, game loops, rendering performance, SwiftUI integration."
},
{
"name": "axiom-apple-docs",
"description": "Use when ANY question involves Apple framework APIs, Swift compiler errors, or Xcode-bundled documentation. Covers Liquid Glass, Swift 6.2 concurrency, Foundation Models, SwiftData, StoreKit, 32 Swift compiler diagnostics."
+2 -1
View File
@@ -3,13 +3,14 @@ description: Ask a question about iOS/Swift development - routes to the right Ax
argument: question (optional) - Your iOS development question
---
You are an iOS development assistant with access to 14 specialized Axiom skills and 0 autonomous agents.
You are an iOS development assistant with access to 15 specialized Axiom skills and 0 autonomous agents.
## Skills Reference
### Build & Environment
- **axiom-ios-build** — Use when ANY iOS build fails, test crashes, Xcode misbehaves, or environment issue occurs before debugging code.
- **axiom-ios-games** — Use when building ANY 2D game, game prototype, or interactive simulation with SpriteKit.
- **axiom-ios-ui** — Use when building, fixing, or improving ANY iOS UI including SwiftUI, UIKit, layout, navigation, animations, design guidelines.
### UI & Design
@@ -1,6 +1,6 @@
---
description: Smart audit selector - analyzes your project and suggests relevant audits
argument: area (optional) - Which audit to run: memory, concurrency, accessibility, energy, swiftui-performance, swiftui-architecture, swiftui-nav, swift-performance, core-data, networking, codable, icloud, storage, liquid-glass, textkit, testing, build
argument: area (optional) - Which audit to run: memory, concurrency, accessibility, energy, swiftui-performance, swiftui-architecture, swiftui-nav, swift-performance, core-data, networking, codable, icloud, storage, liquid-glass, textkit, testing, build, spritekit
disable-model-invocation: true
---
@@ -32,6 +32,7 @@ If no area specified → analyze project and suggest relevant audits
| textkit | textkit-auditor | TextKit issues, text rendering problems |
| testing | testing-auditor | Flaky tests, slow tests, Swift Testing migration, test quality |
| build | build-optimizer | Build time optimization opportunities |
| spritekit | spritekit-auditor | Physics bitmask issues, draw call waste, node accumulation, action leaks |
## Direct Dispatch
@@ -119,6 +120,7 @@ If no area argument:
- Find Timer.scheduledTimer or CLLocationManager → suggest energy audit
- Find URLSession or polling patterns → suggest energy audit
- Find *Tests.swift files → suggest testing audit
- Find SpriteKit imports (import SpriteKit, SKScene, SKSpriteNode) → suggest spritekit audit
2. Present findings and ask: "Based on your project, I suggest these audits: [list]. Which would you like to run?"
@@ -1,4 +1,4 @@
2.19.6
129
30
2.20.0
133
31
10
@@ -0,0 +1,143 @@
---
name: axiom-ios-games
description: Use when building ANY 2D game, game prototype, or interactive simulation with SpriteKit. Covers scene graphs, physics, actions, game loops, rendering performance, SwiftUI integration.
license: MIT
---
# iOS Games Router
**You MUST use this skill for ANY game development, SpriteKit, SceneKit, or interactive simulation work.**
## When to Use
Use this router when:
- Building a new SpriteKit game or prototype
- Implementing physics (collisions, contacts, forces, joints)
- Setting up game architecture (scenes, layers, cameras)
- Debugging SpriteKit issues (contacts not firing, tunneling, frame drops)
- Optimizing game performance (draw calls, node counts, batching)
- Managing game loop, delta time, or pause handling
- Implementing touch/input handling in a game context
- Integrating SpriteKit with SwiftUI or Metal
- Working with particle effects or texture atlases
- Looking up SpriteKit API details
## Routing Logic
### SpriteKit
**Architecture, patterns, and best practices**`/skill axiom-spritekit`
- Scene graph model, coordinate systems, anchor points
- Physics engine: bitmask discipline, contact detection, body types
- Actions system: sequencing, grouping, named actions, timing
- Input handling: touches, coordinate conversion
- Performance: draw calls, batching, object pooling, SKShapeNode trap
- Game loop: frame cycle, delta time, pause handling
- Scene transitions and data passing
- SwiftUI integration (SpriteView, UIViewRepresentable)
- Metal integration (SKRenderer)
- Anti-patterns and code review checklist
- Pressure scenarios with push-back templates
**API reference and lookup**`/skill axiom-spritekit-ref`
- All 16 node types with properties and performance notes
- SKPhysicsBody creation methods and properties
- Complete SKAction catalog (movement, rotation, scaling, fading, composition, physics)
- Texture and atlas management
- SKConstraint types and SKRange
- SKView configuration and scale modes
- SKEmitterNode properties and presets
- SKRenderer setup and SKShader syntax
**Troubleshooting and diagnostics**`/skill axiom-spritekit-diag`
- Physics contacts not firing (6-branch decision tree)
- Objects tunneling through walls (5-branch)
- Poor frame rate (4 top branches, 12 leaves)
- Touches not registering (6-branch)
- Memory spikes and crashes (5-branch)
- Coordinate confusion (5-branch)
- Scene transition crashes (5-branch)
### SceneKit (Future)
SceneKit skills are planned but not yet available. For 3D game development, use Apple's SceneKit documentation directly.
## Decision Tree
1. Building/designing a SpriteKit game? → axiom-spritekit
2. How to use a specific SpriteKit API? → axiom-spritekit-ref
3. Something broken or performing badly? → axiom-spritekit-diag
4. Physics contacts not working? → axiom-spritekit-diag (Symptom 1)
5. Frame rate dropping? → axiom-spritekit-diag (Symptom 3)
6. Coordinate/position confusion? → axiom-spritekit-diag (Symptom 6)
7. Need the complete action list? → axiom-spritekit-ref (Part 3)
8. Physics body setup reference? → axiom-spritekit-ref (Part 2)
## Anti-Rationalization
| Thought | Reality |
|---------|---------|
| "SpriteKit is simple, I don't need a skill" | Physics bitmasks default to 0xFFFFFFFF and cause phantom collisions. The bitmask checklist catches this in 2 min. |
| "I'll just use SKShapeNode, it's quick" | Each SKShapeNode is a separate draw call. 50 of them = 50 draw calls. axiom-spritekit has the pre-render-to-texture pattern. |
| "I can figure out the coordinate system" | SpriteKit uses bottom-left origin (opposite of UIKit). Anchor points add another layer. axiom-spritekit-diag Symptom 6 resolves in 5 min. |
| "Physics is straightforward" | Three different bitmask properties, modification rules inside callbacks, and tunneling edge cases. axiom-spritekit Section 3 covers all gotchas. |
| "The performance is fine on my device" | Performance varies dramatically across devices. axiom-spritekit Section 6 has the debug overlay checklist. |
## Critical Patterns
**axiom-spritekit**:
- PhysicsCategory struct with explicit bitmasks (default `0xFFFFFFFF` causes phantom collisions)
- Camera node pattern for viewport + HUD separation
- SKShapeNode pre-render-to-texture conversion
- `[weak self]` in all `SKAction.run` closures
- Delta time with spiral-of-death clamping
**axiom-spritekit-ref**:
- Complete node type table (16 types with batching behavior)
- Physics body creation methods (circle cheapest, texture most expensive)
- Full action catalog with composition patterns
- SKView debug overlays and scale mode matrix
**axiom-spritekit-diag**:
- 5-step bitmask checklist (2 min vs 30-120 min guessing)
- Debug overlays as mandatory first diagnostic step
- Tunneling prevention flowchart
- Memory growth diagnosis via `showsNodeCount` trending
## Example Invocations
User: "I'm building a SpriteKit game"
→ Invoke: `/skill axiom-spritekit`
User: "My physics contacts aren't firing"
→ Invoke: `/skill axiom-spritekit-diag`
User: "How do I create a physics body from a texture?"
→ Invoke: `/skill axiom-spritekit-ref`
User: "Frame rate is dropping in my game"
→ Invoke: `/skill axiom-spritekit-diag`
User: "How do I set up SpriteKit with SwiftUI?"
→ Invoke: `/skill axiom-spritekit`
User: "What action types are available?"
→ Invoke: `/skill axiom-spritekit-ref`
User: "Objects pass through walls"
→ Invoke: `/skill axiom-spritekit-diag`
User: "How do I organize my SpriteKit scene?"
→ Invoke: `/skill axiom-spritekit`
User: "My game uses too many draw calls"
→ Invoke: `/skill axiom-spritekit`
User: "How do physics bitmasks work?"
→ Invoke: `/skill axiom-spritekit`
User: "What particle emitter settings should I use for fire?"
→ Invoke: `/skill axiom-spritekit-ref`
User: "Memory keeps growing during gameplay"
→ Invoke: `/skill axiom-spritekit-diag`
@@ -0,0 +1,383 @@
---
name: axiom-spritekit-diag
description: Use when physics contacts don't fire, objects tunnel through walls, frame rate drops, touches don't register, memory spikes, coordinate confusion, or scene transition crashes
license: MIT
metadata:
version: "1.0.0"
---
# 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
```swift
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
```swift
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
```swift
#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.
1. **Leaving default bitmasks**`collisionBitMask` defaults to `0xFFFFFFFF` (collides with everything). Always set all three masks explicitly.
2. **Forgetting `contactTestBitMask`** — Defaults to `0x00000000`. Contacts never fire without setting this.
3. **Forgetting `physicsWorld.contactDelegate = self`** — Fixes ~30% of contact issues on its own.
4. **Using SKShapeNode for gameplay** — Each instance = 1 draw call. Pre-render to texture with `view.texture(from:)`.
5. **SKAction.move on physics bodies** — Actions override physics, causing jitter and missed collisions. Use forces/impulses.
6. **Strong self in action closures**`SKAction.run { self.foo() }` in `repeatForever` creates retain cycles. Use `[weak self]`.
7. **Not removing offscreen nodes** — Node count climbs silently, degrading performance.
8. **Missing `isUserInteractionEnabled = true`** — Default is `false` on 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
@@ -0,0 +1,614 @@
---
name: axiom-spritekit-ref
description: SpriteKit API reference — all node types, physics body creation, action catalog, texture atlases, constraints, scene setup, particles, SKRenderer
license: MIT
compatibility: [iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+]
metadata:
version: "1.0.0"
---
# SpriteKit API Reference
Complete API reference for SpriteKit organized by category.
## When to Use This Reference
Use this reference when:
- Looking up specific SpriteKit API signatures or properties
- Checking which node types are available and their performance characteristics
- Finding the right physics body creation method
- Browsing the complete action catalog
- Configuring SKView, scale modes, or transitions
- Setting up particle emitter properties
- Working with SKRenderer or SKShader
## Part 1: Node Hierarchy
### All Node Types
| Node | Purpose | Batches? | Performance Notes |
|------|---------|----------|-------------------|
| `SKNode` | Container, grouping | N/A | Zero rendering cost |
| `SKSpriteNode` | Textured sprites | Yes (same atlas) | Primary gameplay node |
| `SKShapeNode` | Vector paths | **No** | 1 draw call each — avoid in gameplay |
| `SKLabelNode` | Text rendering | No | 1 draw call each |
| `SKEmitterNode` | Particle systems | N/A | GPU-bound, limit birth rate |
| `SKCameraNode` | Viewport control | N/A | Attach HUD as children |
| `SKEffectNode` | Core Image filters | No | Expensive — cache with `shouldRasterize` |
| `SKCropNode` | Masking | No | Mask + content = 2+ draw calls |
| `SKTileMapNode` | Tile-based maps | Yes (same tileset) | Efficient for large maps |
| `SKVideoNode` | Video playback | No | Uses AVPlayer |
| `SK3DNode` | SceneKit content | No | Renders SceneKit scene |
| `SKReferenceNode` | Reusable .sks files | N/A | Loads archive at runtime |
| `SKLightNode` | Per-pixel lighting | N/A | Limits: 8 lights per scene |
| `SKFieldNode` | Physics fields | N/A | Gravity, electric, magnetic, etc. |
| `SKAudioNode` | Positional audio | N/A | Uses AVAudioEngine |
| `SKTransformNode` | 3D rotation wrapper | N/A | xRotation, yRotation for perspective |
### SKSpriteNode Properties
```swift
// Creation
SKSpriteNode(imageNamed: "player") // From asset catalog
SKSpriteNode(texture: texture) // From SKTexture
SKSpriteNode(texture: texture, size: size) // Custom size
SKSpriteNode(color: .red, size: CGSize(width: 50, height: 50)) // Solid color
// Key properties
sprite.anchorPoint = CGPoint(x: 0.5, y: 0) // Bottom-center
sprite.colorBlendFactor = 0.5 // Tint strength (0-1)
sprite.color = .red // Tint color
sprite.normalTexture = normalMap // For lighting
sprite.lightingBitMask = 0x1 // Which lights affect this
sprite.shadowCastBitMask = 0x1 // Which lights cast shadows
sprite.shader = customShader // Per-pixel effects
```
### SKLabelNode Properties
```swift
let label = SKLabelNode(text: "Score: 0")
label.fontName = "AvenirNext-Bold"
label.fontSize = 24
label.fontColor = .white
label.horizontalAlignmentMode = .left
label.verticalAlignmentMode = .top
label.numberOfLines = 0 // Multi-line (iOS 11+)
label.preferredMaxLayoutWidth = 200
label.lineBreakMode = .byWordWrapping
```
---
## Part 2: Physics API
### SKPhysicsBody Creation
```swift
// Volume bodies (have mass, respond to forces)
SKPhysicsBody(circleOfRadius: 20) // Cheapest
SKPhysicsBody(rectangleOf: CGSize(width: 40, height: 60))
SKPhysicsBody(polygonFrom: path) // Convex only
SKPhysicsBody(texture: texture, size: size) // Pixel-perfect (expensive)
SKPhysicsBody(texture: texture, alphaThreshold: 0.5, size: size)
SKPhysicsBody(bodies: [body1, body2]) // Compound
// Edge bodies (massless boundaries)
SKPhysicsBody(edgeLoopFrom: rect) // Rectangle boundary
SKPhysicsBody(edgeLoopFrom: path) // Path boundary
SKPhysicsBody(edgeFrom: pointA, to: pointB) // Single edge
SKPhysicsBody(edgeChainFrom: path) // Open path
```
### Physics Body Properties
```swift
// Identity
body.categoryBitMask = 0x1 // What this body IS
body.collisionBitMask = 0x2 // What it bounces off
body.contactTestBitMask = 0x4 // What triggers didBegin/didEnd
// Physical characteristics
body.mass = 1.0 // kg
body.density = 1.0 // kg/m^2 (auto-calculates mass)
body.friction = 0.2 // 0.0 (ice) to 1.0 (rubber)
body.restitution = 0.3 // 0.0 (no bounce) to 1.0 (perfect bounce)
body.linearDamping = 0.1 // Air resistance (0 = none)
body.angularDamping = 0.1 // Rotational damping
// Behavior
body.isDynamic = true // Responds to forces
body.affectedByGravity = true // Subject to world gravity
body.allowsRotation = true // Can rotate from physics
body.pinned = false // Pinned to parent position
body.usesPreciseCollisionDetection = false // For fast objects
// Motion (read/write)
body.velocity = CGVector(dx: 100, dy: 0)
body.angularVelocity = 0.0
// Force application
body.applyForce(CGVector(dx: 0, dy: 100)) // Continuous
body.applyImpulse(CGVector(dx: 0, dy: 50)) // Instant
body.applyTorque(0.5) // Continuous rotation
body.applyAngularImpulse(1.0) // Instant rotation
body.applyForce(CGVector(dx: 10, dy: 0), at: point) // Force at point
```
### SKPhysicsWorld
```swift
scene.physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
scene.physicsWorld.speed = 1.0 // 0 = paused, 2 = double speed
scene.physicsWorld.contactDelegate = self
// Ray casting
let body = scene.physicsWorld.body(at: point)
let bodyInRect = scene.physicsWorld.body(in: rect)
scene.physicsWorld.enumerateBodies(alongRayStart: start, end: end) { body, point, normal, stop in
// Process each body the ray intersects
}
```
### Physics Joints
```swift
// Pin joint (pivot)
let pin = SKPhysicsJointPin.joint(
withBodyA: bodyA, bodyB: bodyB,
anchor: anchorPoint
)
// Fixed joint (rigid connection)
let fixed = SKPhysicsJointFixed.joint(
withBodyA: bodyA, bodyB: bodyB,
anchor: anchorPoint
)
// Spring joint
let spring = SKPhysicsJointSpring.joint(
withBodyA: bodyA, bodyB: bodyB,
anchorA: pointA, anchorB: pointB
)
spring.frequency = 1.0 // Oscillations per second
spring.damping = 0.5 // 0 = no damping
// Sliding joint (linear constraint)
let slide = SKPhysicsJointSliding.joint(
withBodyA: bodyA, bodyB: bodyB,
anchor: point, axis: CGVector(dx: 1, dy: 0)
)
// Limit joint (distance constraint)
let limit = SKPhysicsJointLimit.joint(
withBodyA: bodyA, bodyB: bodyB,
anchorA: pointA, anchorB: pointB
)
// Add joint to world
scene.physicsWorld.add(joint)
// Remove: scene.physicsWorld.remove(joint)
```
### Physics Fields
```swift
// Gravity (directional)
let gravity = SKFieldNode.linearGravityField(withVector: vector_float3(0, -9.8, 0))
// Radial gravity (toward/away from point)
let radial = SKFieldNode.radialGravityField()
radial.strength = 5.0
// Electric field (charge-dependent)
let electric = SKFieldNode.electricField()
// Noise field (turbulence)
let noise = SKFieldNode.noiseField(withSmoothness: 0.5, animationSpeed: 1.0)
// Vortex
let vortex = SKFieldNode.vortexField()
// Drag
let drag = SKFieldNode.dragField()
// All fields share:
field.region = SKRegion(radius: 100) // Area of effect
field.strength = 1.0 // Intensity
field.falloff = 0.0 // Distance falloff
field.minimumRadius = 10 // Inner dead zone
field.isEnabled = true
field.categoryBitMask = 0xFFFFFFFF // Which bodies affected
```
---
## Part 3: Action Catalog
### Movement
```swift
SKAction.move(to: point, duration: 1.0)
SKAction.move(by: CGVector(dx: 100, dy: 0), duration: 0.5)
SKAction.moveTo(x: 200, duration: 1.0)
SKAction.moveTo(y: 300, duration: 1.0)
SKAction.moveBy(x: 50, y: 0, duration: 0.5)
SKAction.follow(path, asOffset: true, orientToPath: true, duration: 2.0)
```
### Rotation
```swift
SKAction.rotate(byAngle: .pi, duration: 1.0) // Relative
SKAction.rotate(toAngle: .pi / 2, duration: 0.5) // Absolute
SKAction.rotate(toAngle: angle, duration: 0.5, shortestUnitArc: true)
```
### Scaling
```swift
SKAction.scale(to: 2.0, duration: 0.5)
SKAction.scale(by: 1.5, duration: 0.3)
SKAction.scaleX(to: 2.0, y: 1.0, duration: 0.5)
SKAction.resize(toWidth: 100, height: 50, duration: 0.5)
```
### Fading
```swift
SKAction.fadeIn(withDuration: 0.5)
SKAction.fadeOut(withDuration: 0.5)
SKAction.fadeAlpha(to: 0.5, duration: 0.3)
SKAction.fadeAlpha(by: -0.2, duration: 0.3)
```
### Composition
```swift
SKAction.sequence([action1, action2, action3]) // Sequential
SKAction.group([action1, action2]) // Parallel
SKAction.repeat(action, count: 5) // Finite repeat
SKAction.repeatForever(action) // Infinite
action.reversed() // Reverse
SKAction.wait(forDuration: 1.0) // Delay
SKAction.wait(forDuration: 1.0, withRange: 0.5) // Random delay
```
### Texture & Color
```swift
SKAction.setTexture(texture)
SKAction.setTexture(texture, resize: true)
SKAction.animate(with: [tex1, tex2, tex3], timePerFrame: 0.1)
SKAction.animate(with: textures, timePerFrame: 0.1, resize: false, restore: true)
SKAction.colorize(with: .red, colorBlendFactor: 1.0, duration: 0.5)
SKAction.colorize(withColorBlendFactor: 0, duration: 0.5)
```
### Sound
```swift
SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false)
```
### Node Tree
```swift
SKAction.removeFromParent()
SKAction.run(block)
SKAction.run(block, queue: .main)
SKAction.customAction(withDuration: 1.0) { node, elapsed in
// Custom per-frame logic
}
```
### Physics
```swift
SKAction.applyForce(CGVector(dx: 0, dy: 100), duration: 0.5)
SKAction.applyImpulse(CGVector(dx: 50, dy: 0), duration: 1.0/60.0) // ~1 frame
SKAction.applyTorque(0.5, duration: 1.0)
SKAction.changeCharge(to: 1.0, duration: 0.5)
SKAction.changeMass(to: 2.0, duration: 0.5)
```
### Timing Modes
```swift
action.timingMode = .linear // Constant speed
action.timingMode = .easeIn // Slow fast
action.timingMode = .easeOut // Fast slow
action.timingMode = .easeInEaseOut // Slow fast slow
action.speed = 2.0 // 2x speed
```
---
## Part 4: Textures and Atlases
### SKTexture
```swift
// From image
let tex = SKTexture(imageNamed: "player")
// From atlas
let atlas = SKTextureAtlas(named: "Characters")
let tex = atlas.textureNamed("player_run_1")
// Subrectangle (for manual sprite sheets)
let sub = SKTexture(rect: CGRect(x: 0, y: 0, width: 0.25, height: 0.5), in: sheetTexture)
// From CGImage
let tex = SKTexture(cgImage: cgImage)
// Filtering
tex.filteringMode = .nearest // Pixel art (no smoothing)
tex.filteringMode = .linear // Smooth scaling (default)
// Preload
SKTexture.preload([tex1, tex2]) { /* Ready */ }
```
### SKTextureAtlas
```swift
// Create in Xcode: Assets.xcassets New Sprite Atlas
// Or .atlas folder in project bundle
let atlas = SKTextureAtlas(named: "Characters")
let textureNames = atlas.textureNames // All texture names in atlas
// Preload entire atlas
atlas.preload { /* Atlas ready */ }
// Preload multiple atlases
SKTextureAtlas.preloadTextureAtlases([atlas1, atlas2]) { /* All ready */ }
// Animation from atlas
let frames = (1...8).map { atlas.textureNamed("run_\($0)") }
let animate = SKAction.animate(with: frames, timePerFrame: 0.1)
```
---
## Part 5: Constraints
```swift
// Orient toward another node
let orient = SKConstraint.orient(to: targetNode, offset: SKRange(constantValue: 0))
// Orient toward a point
let orient = SKConstraint.orient(to: point, offset: SKRange(constantValue: 0))
// Position constraint (keep X in range)
let xRange = SKConstraint.positionX(SKRange(lowerLimit: 0, upperLimit: 400))
// Position constraint (keep Y in range)
let yRange = SKConstraint.positionY(SKRange(lowerLimit: 50, upperLimit: 750))
// Distance constraint (stay within range of node)
let dist = SKConstraint.distance(SKRange(lowerLimit: 50, upperLimit: 200), to: targetNode)
// Rotation constraint
let rot = SKConstraint.zRotation(SKRange(lowerLimit: -.pi/4, upperLimit: .pi/4))
// Apply constraints (processed in order)
node.constraints = [orient, xRange, yRange]
// Toggle
node.constraints?.first?.isEnabled = false
```
### SKRange
```swift
SKRange(constantValue: 100) // Exactly 100
SKRange(lowerLimit: 50, upperLimit: 200) // 50...200
SKRange(lowerLimit: 0) // >= 0
SKRange(upperLimit: 500) // <= 500
SKRange(value: 100, variance: 20) // 80...120
```
---
## Part 6: Scene Setup
### SKView Configuration
```swift
let skView = SKView(frame: view.bounds)
// Debug overlays
skView.showsFPS = true
skView.showsNodeCount = true
skView.showsDrawCount = true
skView.showsPhysics = true
skView.showsFields = true
skView.showsQuadCount = true
// Performance
skView.ignoresSiblingOrder = true // Enables batching optimizations
skView.shouldCullNonVisibleNodes = true // Auto-hide offscreen (manual is faster)
skView.isAsynchronous = true // Default: renders asynchronously
skView.allowsTransparency = false // Opaque is faster
// Frame rate
skView.preferredFramesPerSecond = 60 // Or 120 for ProMotion
// Present scene
skView.presentScene(scene)
skView.presentScene(scene, transition: .fade(withDuration: 0.5))
```
### Scale Mode Matrix
| Mode | Aspect Ratio | Content | Best For |
|------|-------------|---------|----------|
| `.aspectFill` | Preserved | Fills view, crops edges | Most games |
| `.aspectFit` | Preserved | Fits in view, letterboxes | Exact layout needed |
| `.resizeFill` | Distorted | Stretches to fill | Almost never |
| `.fill` | Varies | Scene resizes to match view | Adaptive scenes |
### SKTransition Types
```swift
SKTransition.fade(withDuration: 0.5)
SKTransition.fade(with: .black, duration: 0.5)
SKTransition.crossFade(withDuration: 0.5)
SKTransition.flipHorizontal(withDuration: 0.5)
SKTransition.flipVertical(withDuration: 0.5)
SKTransition.reveal(with: .left, duration: 0.5)
SKTransition.moveIn(with: .right, duration: 0.5)
SKTransition.push(with: .up, duration: 0.5)
SKTransition.doorway(withDuration: 0.5)
SKTransition.doorsOpenHorizontal(withDuration: 0.5)
SKTransition.doorsOpenVertical(withDuration: 0.5)
SKTransition.doorsCloseHorizontal(withDuration: 0.5)
SKTransition.doorsCloseVertical(withDuration: 0.5)
// Custom with CIFilter:
SKTransition(ciFilter: filter, duration: 0.5)
```
---
## Part 7: Particles
### SKEmitterNode Key Properties
```swift
let emitter = SKEmitterNode(fileNamed: "Spark")!
// Emission control
emitter.particleBirthRate = 100 // Particles per second
emitter.numParticlesToEmit = 0 // 0 = infinite
emitter.particleLifetime = 2.0 // Seconds
emitter.particleLifetimeRange = 0.5 // ± random
// Position
emitter.particlePosition = .zero
emitter.particlePositionRange = CGVector(dx: 10, dy: 10)
// Movement
emitter.emissionAngle = .pi / 2 // Direction (radians)
emitter.emissionAngleRange = .pi / 4 // Spread
emitter.particleSpeed = 100 // Points per second
emitter.particleSpeedRange = 50 // ± random
emitter.xAcceleration = 0
emitter.yAcceleration = -100 // Gravity-like
// Appearance
emitter.particleTexture = SKTexture(imageNamed: "spark")
emitter.particleSize = CGSize(width: 8, height: 8)
emitter.particleColor = .white
emitter.particleColorAlphaSpeed = -0.5 // Fade out
emitter.particleBlendMode = .add // Additive for fire/glow
emitter.particleAlpha = 1.0
emitter.particleAlphaSpeed = -0.5
// Scale
emitter.particleScale = 1.0
emitter.particleScaleRange = 0.5
emitter.particleScaleSpeed = -0.3 // Shrink over time
// Rotation
emitter.particleRotation = 0
emitter.particleRotationSpeed = 2.0
// Target node (for trails)
emitter.targetNode = scene // Particles stay in world space
// Render order
emitter.particleRenderOrder = .dontCare // .oldestFirst, .oldestLast, .dontCare
// Physics field interaction
emitter.fieldBitMask = 0x1
```
### Common Particle Presets
| Effect | Key Settings |
|--------|-------------|
| Fire | `blendMode: .add`, fast `alphaSpeed`, orange→red color, upward speed |
| Smoke | `blendMode: .alpha`, slow speed, gray color, scale up over time |
| Sparks | `blendMode: .add`, high speed + range, short lifetime, small size |
| Rain | Downward `emissionAngle`, narrow range, long lifetime, thin texture |
| Snow | Slow downward speed, wide position range, slight x acceleration |
| Trail | Set `targetNode` to scene, narrow emission angle, medium lifetime |
| Explosion | High birth rate, short `numParticlesToEmit`, high speed range |
---
## Part 8: SKRenderer and Shaders
### SKRenderer (Metal Integration)
```swift
import MetalKit
let device = MTLCreateSystemDefaultDevice()!
let renderer = SKRenderer(device: device)
renderer.scene = gameScene
renderer.ignoresSiblingOrder = true
// In Metal render loop:
func draw(in view: MTKView) {
guard let commandBuffer = commandQueue.makeCommandBuffer(),
let rpd = view.currentRenderPassDescriptor else { return }
renderer.update(atTime: CACurrentMediaTime())
renderer.render(
withViewport: CGRect(origin: .zero, size: view.drawableSize),
commandBuffer: commandBuffer,
renderPassDescriptor: rpd
)
commandBuffer.present(view.currentDrawable!)
commandBuffer.commit()
}
```
### SKShader (Custom GLSL ES Effects)
```swift
// Fragment shader for per-pixel effects
let shader = SKShader(source: """
void main() {
vec4 color = texture2D(u_texture, v_tex_coord);
// Desaturate
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(vec3(gray), color.a) * v_color_mix.a;
}
""")
sprite.shader = shader
// With uniforms
let shader = SKShader(source: """
void main() {
vec4 color = texture2D(u_texture, v_tex_coord);
color.rgb *= u_intensity;
gl_FragColor = color;
}
""")
shader.uniforms = [
SKUniform(name: "u_intensity", float: 0.8)
]
// Built-in uniforms:
// u_texture sprite texture
// u_time elapsed time
// u_path_length shape node path length
// v_tex_coord texture coordinate
// v_color_mix color/alpha mix
// SKAttribute for per-node values
```
## Resources
**WWDC**: 2014-608, 2016-610, 2017-609
**Docs**: /spritekit/skspritenode, /spritekit/skphysicsbody, /spritekit/skaction, /spritekit/skemitternode, /spritekit/skrenderer
**Skills**: axiom-spritekit, axiom-spritekit-diag
@@ -0,0 +1,962 @@
---
name: axiom-spritekit
description: Use when building SpriteKit games, implementing physics, actions, scene management, or debugging game performance. Covers scene graph, physics engine, actions system, game loop, rendering optimization.
license: MIT
metadata:
version: "1.0.0"
---
# SpriteKit Game Development Guide
**Purpose**: Build reliable SpriteKit games by mastering the scene graph, physics engine, action system, and rendering pipeline
**iOS Version**: iOS 13+ (SwiftUI integration), iOS 11+ (SKRenderer)
**Xcode**: Xcode 15+
## When to Use This Skill
Use this skill when:
- Building a new SpriteKit game or interactive simulation
- Implementing physics (collisions, contacts, forces, joints)
- Setting up game architecture (scenes, layers, cameras)
- Optimizing frame rate or reducing draw calls
- Implementing touch/input handling in a game
- Managing scene transitions and data passing
- Integrating SpriteKit with SwiftUI or Metal
- Debugging physics contacts that don't fire
- Fixing coordinate system confusion
Do NOT use this skill for:
- SceneKit 3D rendering (future skill)
- GameplayKit entity-component systems (future skill)
- Metal shader programming (`axiom-metal-migration-ref`)
- General SwiftUI layout (`axiom-swiftui-layout`)
---
## 1. Mental Model
### Coordinate System
SpriteKit uses a **bottom-left origin** with Y pointing up. This differs from UIKit (top-left, Y down).
```
SpriteKit: UIKit:
┌─────────┐ ┌─────────┐
│ +Y │ │ (0,0) │
│ ↑ │ │ ↓ │
│ │ │ │ +Y │
│(0,0)──→+X│ │ │ │
└─────────┘ └─────────┘
```
**Anchor Points** define which point on a sprite maps to its `position`. Default is `(0.5, 0.5)` (center).
```swift
// Common anchor point trap:
// Anchor (0, 0) = bottom-left of sprite is at position
// Anchor (0.5, 0.5) = center of sprite is at position (DEFAULT)
// Anchor (0.5, 0) = bottom-center (useful for characters standing on ground)
sprite.anchorPoint = CGPoint(x: 0.5, y: 0)
```
**Scene anchor point** maps the view's frame to scene coordinates:
- `(0, 0)` — scene origin at bottom-left of view (default)
- `(0.5, 0.5)` — scene origin at center of view
### Node Tree
Everything in SpriteKit is an `SKNode` in a tree hierarchy. Parent transforms propagate to children.
```
SKScene
├── SKCameraNode (viewport control)
├── SKNode "world" (game content layer)
│ ├── SKSpriteNode "player"
│ ├── SKSpriteNode "enemy"
│ └── SKNode "platforms"
│ ├── SKSpriteNode "platform1"
│ └── SKSpriteNode "platform2"
└── SKNode "hud" (UI layer, attached to camera)
├── SKLabelNode "score"
└── SKSpriteNode "healthBar"
```
### Z-Ordering
`zPosition` controls draw order. Higher values render on top. Nodes at the same `zPosition` render in child array order (unless `ignoresSiblingOrder` is `true`).
```swift
// Establish clear z-order layers
enum ZLayer {
static let background: CGFloat = -100
static let platforms: CGFloat = 0
static let items: CGFloat = 10
static let player: CGFloat = 20
static let effects: CGFloat = 30
static let hud: CGFloat = 100
}
```
---
## 2. Scene Architecture
### Scale Mode Decision
| Mode | Behavior | Use When |
|------|----------|----------|
| `.aspectFill` | Fills view, crops edges | Full-bleed games (most games) |
| `.aspectFit` | Fits in view, letterboxes | Puzzle games needing exact layout |
| `.resizeFill` | Stretches to fill | Almost never — distorts |
| `.fill` | Matches view size exactly | Scene adapts to any ratio |
```swift
class GameScene: SKScene {
override func sceneDidLoad() {
scaleMode = .aspectFill
// Design for a reference size, let aspectFill crop edges
}
}
```
### Camera Node Pattern
Always use `SKCameraNode` for viewport control. Attach HUD elements to the camera so they don't scroll.
```swift
let camera = SKCameraNode()
camera.name = "mainCamera"
addChild(camera)
self.camera = camera
// HUD follows camera automatically
let scoreLabel = SKLabelNode(text: "Score: 0")
scoreLabel.position = CGPoint(x: 0, y: size.height / 2 - 50)
camera.addChild(scoreLabel)
// Move camera to follow player
let follow = SKConstraint.distance(SKRange(constantValue: 0), to: playerNode)
camera.constraints = [follow]
```
### Layer Organization
```swift
// Create layer nodes for organization
let worldNode = SKNode()
worldNode.name = "world"
addChild(worldNode)
let hudNode = SKNode()
hudNode.name = "hud"
camera?.addChild(hudNode)
// All gameplay objects go in worldNode
worldNode.addChild(playerSprite)
worldNode.addChild(enemySprite)
// All UI goes in hudNode (moves with camera)
hudNode.addChild(scoreLabel)
```
### Scene Transitions
```swift
// Preload next scene for smooth transitions
guard let nextScene = LevelScene(fileNamed: "Level2") else { return }
nextScene.scaleMode = .aspectFill
let transition = SKTransition.fade(withDuration: 0.5)
view?.presentScene(nextScene, transition: transition)
```
**Data passing between scenes**: Use a shared game state object, not node properties.
```swift
class GameState {
static let shared = GameState()
var score = 0
var currentLevel = 1
var playerHealth = 100
}
// In scene transition:
let nextScene = LevelScene(size: size)
// GameState.shared is already accessible
view?.presentScene(nextScene, transition: .fade(withDuration: 0.5))
```
**Note**: A singleton works for simple games. For larger projects with testing needs, consider passing a `GameState` instance through scene initializers to avoid hidden global state.
**Cleanup in `willMove(from:)`**:
```swift
override func willMove(from view: SKView) {
removeAllActions()
removeAllChildren()
physicsWorld.contactDelegate = nil
}
```
---
## 3. Physics Engine
### Bitmask Discipline
**This is the #1 source of SpriteKit bugs.** Physics bitmasks use a 32-bit system where each bit represents a category.
```swift
struct PhysicsCategory {
static let none: UInt32 = 0
static let player: UInt32 = 0b0001 // 1
static let enemy: UInt32 = 0b0010 // 2
static let ground: UInt32 = 0b0100 // 4
static let projectile: UInt32 = 0b1000 // 8
static let powerUp: UInt32 = 0b10000 // 16
}
```
**Three bitmask properties** (all default to `0xFFFFFFFF` — everything):
| Property | Purpose | Default |
|----------|---------|---------|
| `categoryBitMask` | What this body IS | `0xFFFFFFFF` |
| `collisionBitMask` | What it BOUNCES off | `0xFFFFFFFF` |
| `contactTestBitMask` | What TRIGGERS delegate | `0x00000000` |
**The default `collisionBitMask` of `0xFFFFFFFF` means everything collides with everything.** This is the most common source of unexpected physics behavior.
```swift
// CORRECT: Explicit bitmask setup
player.physicsBody?.categoryBitMask = PhysicsCategory.player
player.physicsBody?.collisionBitMask = PhysicsCategory.ground | PhysicsCategory.enemy
player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy | PhysicsCategory.powerUp
enemy.physicsBody?.categoryBitMask = PhysicsCategory.enemy
enemy.physicsBody?.collisionBitMask = PhysicsCategory.ground | PhysicsCategory.player
enemy.physicsBody?.contactTestBitMask = PhysicsCategory.player | PhysicsCategory.projectile
```
### Bitmask Checklist
For every physics body, verify:
1. `categoryBitMask` set to exactly one category
2. `collisionBitMask` set to only categories it should bounce off (NOT `0xFFFFFFFF`)
3. `contactTestBitMask` set to categories that should trigger delegate callbacks
4. Delegate is assigned: `physicsWorld.contactDelegate = self`
### Contact Detection
```swift
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
}
func didBegin(_ contact: SKPhysicsContact) {
// Sort bodies so bodyA has the lower category
let (first, second): (SKPhysicsBody, SKPhysicsBody)
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
(first, second) = (contact.bodyA, contact.bodyB)
} else {
(first, second) = (contact.bodyB, contact.bodyA)
}
// Now dispatch based on categories
if first.categoryBitMask == PhysicsCategory.player &&
second.categoryBitMask == PhysicsCategory.enemy {
guard let playerNode = first.node, let enemyNode = second.node else { return }
playerHitEnemy(player: playerNode, enemy: enemyNode)
}
}
}
```
**Modification rule**: You cannot modify the physics world inside `didBegin`/`didEnd`. Set flags and apply changes in `update(_:)`.
```swift
var enemiesToRemove: [SKNode] = []
func didBegin(_ contact: SKPhysicsContact) {
// Flag for removal don't remove here
if let enemy = contact.bodyB.node {
enemiesToRemove.append(enemy)
}
}
override func update(_ currentTime: TimeInterval) {
for enemy in enemiesToRemove {
enemy.removeFromParent()
}
enemiesToRemove.removeAll()
}
```
### Body Types
| Type | Created With | Responds to Forces | Use For |
|------|-------------|-------------------|---------|
| Dynamic volume | `init(circleOfRadius:)`, `init(rectangleOf:)`, `init(texture:size:)` | Yes | Players, enemies, projectiles |
| Static volume | Dynamic body + `isDynamic = false` | No (but collides) | Platforms, walls |
| Edge | `init(edgeLoopFrom:)`, `init(edgeFrom:to:)` | No (boundary only) | Screen boundaries, terrain |
```swift
// Screen boundary using edge loop
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
// Texture-based body for irregular shapes
guard let texture = enemy.texture else { return }
enemy.physicsBody = SKPhysicsBody(texture: texture, size: enemy.size)
// Circle for performance (cheapest collision detection)
bullet.physicsBody = SKPhysicsBody(circleOfRadius: 5)
```
### Tunneling Prevention
Fast-moving objects can pass through thin walls. Fix:
```swift
// Enable precise collision detection for fast objects
bullet.physicsBody?.usesPreciseCollisionDetection = true
// Make walls thick enough (at least as wide as fastest object moves per frame)
// At 60fps, an object at velocity 600pt/s moves 10pt/frame
```
### Forces vs Impulses
```swift
// Force: continuous (applied per frame, accumulates)
body.applyForce(CGVector(dx: 0, dy: 100))
// Impulse: instant velocity change (one-time, like a jump)
body.applyImpulse(CGVector(dx: 0, dy: 50))
// Torque: continuous rotation
body.applyTorque(0.5)
// Angular impulse: instant rotation change
body.applyAngularImpulse(1.0)
```
---
## 4. Actions System
### Core Patterns
```swift
// Movement
let move = SKAction.move(to: CGPoint(x: 200, y: 300), duration: 1.0)
let moveBy = SKAction.moveBy(x: 100, y: 0, duration: 0.5)
// Rotation
let rotate = SKAction.rotate(byAngle: .pi * 2, duration: 1.0)
// Scale
let scale = SKAction.scale(to: 2.0, duration: 0.3)
// Fade
let fadeOut = SKAction.fadeOut(withDuration: 0.5)
let fadeIn = SKAction.fadeIn(withDuration: 0.5)
```
### Sequencing and Grouping
```swift
// Sequence: one after another
let moveAndFade = SKAction.sequence([
SKAction.move(to: target, duration: 1.0),
SKAction.fadeOut(withDuration: 0.3),
SKAction.removeFromParent()
])
// Group: all at once
let spinAndGrow = SKAction.group([
SKAction.rotate(byAngle: .pi * 2, duration: 1.0),
SKAction.scale(to: 2.0, duration: 1.0)
])
// Repeat
let pulse = SKAction.repeatForever(SKAction.sequence([
SKAction.scale(to: 1.2, duration: 0.3),
SKAction.scale(to: 1.0, duration: 0.3)
]))
```
### Named Actions (Critical for Management)
```swift
// Use named actions so you can cancel/replace them
node.run(pulse, withKey: "pulse")
// Later, stop the pulse:
node.removeAction(forKey: "pulse")
// Check if running:
if node.action(forKey: "pulse") != nil {
// Still pulsing
}
```
### Custom Actions with Weak Self
```swift
// WRONG: Retain cycle risk
node.run(SKAction.run {
self.score += 1 // Strong capture of self
})
// CORRECT: Weak capture
node.run(SKAction.run { [weak self] in
self?.score += 1
})
// For repeating actions, always use weak self
let spawn = SKAction.repeatForever(SKAction.sequence([
SKAction.run { [weak self] in self?.spawnEnemy() },
SKAction.wait(forDuration: 2.0)
]))
scene.run(spawn, withKey: "enemySpawner")
```
### Timing Modes
```swift
action.timingMode = .linear // Constant speed (default)
action.timingMode = .easeIn // Accelerate from rest
action.timingMode = .easeOut // Decelerate to rest
action.timingMode = .easeInEaseOut // Smooth start and end
```
### Actions vs Physics
**Never use actions to move physics-controlled nodes.** Actions override the physics simulation, causing jittering and missed collisions.
```swift
// WRONG: Action fights physics
playerNode.run(SKAction.moveTo(x: 200, duration: 0.5))
// CORRECT: Use forces/impulses for physics bodies
playerNode.physicsBody?.applyImpulse(CGVector(dx: 50, dy: 0))
// CORRECT: Use actions for non-physics nodes (UI, effects, decorations)
hudLabel.run(SKAction.scale(to: 1.5, duration: 0.2))
```
---
## 5. Input Handling
### Touch Handling
```swift
// CRITICAL: isUserInteractionEnabled must be true on the responding node
// SKScene has it true by default; other nodes default to false
class Player: SKSpriteNode {
init() {
super.init(texture: SKTexture(imageNamed: "player"), color: .clear, size: CGSize(width: 50, height: 50))
isUserInteractionEnabled = true // Required!
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Handle touch on this specific node
}
}
```
### Coordinate Space Conversion
```swift
// Touch location in SCENE coordinates (most common)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let locationInScene = touch.location(in: self)
// Touch location in a SPECIFIC NODE's coordinates
let locationInWorld = touch.location(in: worldNode)
// Hit test: what node was touched?
let touchedNodes = nodes(at: locationInScene)
}
```
**Common mistake**: Using `touch.location(in: self.view)` returns UIKit coordinates (Y-flipped). Always use `touch.location(in: self)` for scene coordinates.
### Game Controller Support
```swift
import GameController
func setupControllers() {
NotificationCenter.default.addObserver(
self, selector: #selector(controllerConnected),
name: .GCControllerDidConnect, object: nil
)
// Check already-connected controllers
for controller in GCController.controllers() {
configureController(controller)
}
}
```
---
## 6. Performance
### Performance Priorities
For detailed performance diagnosis, see `axiom-spritekit-diag` Symptom 3. Key priorities:
1. **Node count** — Remove offscreen nodes, use object pooling
2. **Draw calls** — Use texture atlases, replace SKShapeNode with pre-rendered textures
3. **Physics cost** — Prefer simple body shapes, limit `usesPreciseCollisionDetection`
4. **Particles** — Limit birth rate, set finite emission counts
### Debug Overlays (Always Enable During Development)
```swift
if let view = self.view as? SKView {
view.showsFPS = true
view.showsNodeCount = true
view.showsDrawCount = true
view.showsPhysics = true // Shows physics body outlines
// Performance: render order optimization
view.ignoresSiblingOrder = true
}
```
### Texture Atlas Batching
Sprites using textures from the same atlas render in a single draw call.
```swift
// Create atlas in Xcode: Assets New Sprite Atlas
// Or use .atlas folder in project
let atlas = SKTextureAtlas(named: "Characters")
let texture = atlas.textureNamed("player_idle")
let sprite = SKSpriteNode(texture: texture)
// Preload atlas to avoid frame drops
SKTextureAtlas.preloadTextureAtlases([atlas]) {
// Atlas ready present scene
}
```
### SKShapeNode Trap
**SKShapeNode generates one draw call per instance.** It cannot be batched. Use it for prototyping and debug visualization only.
```swift
// WRONG: 100 SKShapeNodes = 100 draw calls
for _ in 0..<100 {
let dot = SKShapeNode(circleOfRadius: 5)
addChild(dot)
}
// CORRECT: Pre-render to texture, use SKSpriteNode
let shape = SKShapeNode(circleOfRadius: 5)
shape.fillColor = .red
guard let texture = view?.texture(from: shape) else { return }
for _ in 0..<100 {
let dot = SKSpriteNode(texture: texture)
addChild(dot)
}
```
### Object Pooling
For frequently spawned/destroyed objects (bullets, particles, enemies):
```swift
class BulletPool {
private var available: [SKSpriteNode] = []
private let texture: SKTexture
init(texture: SKTexture, initialSize: Int = 20) {
self.texture = texture
for _ in 0..<initialSize {
available.append(createBullet())
}
}
private func createBullet() -> SKSpriteNode {
let bullet = SKSpriteNode(texture: texture)
bullet.physicsBody = SKPhysicsBody(circleOfRadius: 3)
bullet.physicsBody?.categoryBitMask = PhysicsCategory.projectile
bullet.physicsBody?.collisionBitMask = PhysicsCategory.none
bullet.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
return bullet
}
func spawn() -> SKSpriteNode {
if available.isEmpty {
available.append(createBullet())
}
let bullet = available.removeLast()
bullet.isHidden = false
bullet.physicsBody?.isDynamic = true
return bullet
}
func recycle(_ bullet: SKSpriteNode) {
bullet.removeAllActions()
bullet.removeFromParent()
bullet.physicsBody?.isDynamic = false
bullet.physicsBody?.velocity = .zero
bullet.isHidden = true
available.append(bullet)
}
}
```
### Offscreen Node Removal
```swift
// Manual removal is faster than shouldCullNonVisibleNodes
override func update(_ currentTime: TimeInterval) {
enumerateChildNodes(withName: "bullet") { node, _ in
if !self.frame.intersects(node.frame) {
self.bulletPool.recycle(node as! SKSpriteNode)
}
}
}
```
---
## 7. Game Loop
### Frame Cycle (8 Phases)
```
1. update(_:) ← Your game logic here
2. didEvaluateActions() ← Actions completed
3. [Physics simulation] ← SpriteKit runs physics
4. didSimulatePhysics() ← Physics done, adjust results
5. [Constraint evaluation] ← SKConstraints applied
6. didApplyConstraints() ← Constraints done
7. didFinishUpdate() ← Last chance before render
8. [Rendering] ← Frame drawn
```
### Delta Time
```swift
private var lastUpdateTime: TimeInterval = 0
override func update(_ currentTime: TimeInterval) {
let dt: TimeInterval
if lastUpdateTime == 0 {
dt = 0
} else {
dt = currentTime - lastUpdateTime
}
lastUpdateTime = currentTime
// Clamp delta time to prevent spiral of death
// (when app returns from background, dt can be huge)
let clampedDt = min(dt, 1.0 / 30.0)
updatePlayer(deltaTime: clampedDt)
updateEnemies(deltaTime: clampedDt)
}
```
### Pause Handling
```swift
// Pause the scene (stops actions, physics, update loop)
scene.isPaused = true
// Pause specific subtree only
worldNode.isPaused = true // Game paused but HUD still animates
// Handle app backgrounding
NotificationCenter.default.addObserver(
self, selector: #selector(pauseGame),
name: UIApplication.willResignActiveNotification, object: nil
)
```
---
## 8. Particle Effects
### Emitter Best Practices
```swift
// Load from .sks file (designed in Xcode Particle Editor)
guard let emitter = SKEmitterNode(fileNamed: "Explosion") else { return }
emitter.position = explosionPoint
addChild(emitter)
// CRITICAL: Auto-remove after emission completes
let duration = TimeInterval(emitter.numParticlesToEmit) / TimeInterval(emitter.particleBirthRate)
+ TimeInterval(emitter.particleLifetime + emitter.particleLifetimeRange / 2)
emitter.run(SKAction.sequence([
SKAction.wait(forDuration: duration),
SKAction.removeFromParent()
]))
```
### Target Node for Trails
Without `targetNode`, particles move with the emitter. For trails (like rocket exhaust), set `targetNode` to the scene:
```swift
let trail = SKEmitterNode(fileNamed: "RocketTrail")!
trail.targetNode = scene // Particles stay where emitted
rocketNode.addChild(trail)
```
### Infinite Emitter Cleanup
```swift
// WRONG: Infinite emitter never cleaned up
let fire = SKEmitterNode(fileNamed: "Fire")!
fire.numParticlesToEmit = 0 // 0 = infinite
addChild(fire)
// Memory leak particles accumulate forever
// CORRECT: Set emission limit or remove when done
fire.numParticlesToEmit = 200 // Stops after 200 particles
// Or manually stop and remove:
fire.particleBirthRate = 0 // Stop new particles
fire.run(SKAction.sequence([
SKAction.wait(forDuration: TimeInterval(fire.particleLifetime)),
SKAction.removeFromParent()
]))
```
---
## 9. SwiftUI Integration
### SpriteView (Recommended, iOS 14+)
The simplest way to embed SpriteKit in SwiftUI. Use this unless you need custom SKView configuration.
```swift
import SpriteKit
import SwiftUI
struct GameView: View {
var body: some View {
SpriteView(scene: {
let scene = GameScene(size: CGSize(width: 390, height: 844))
scene.scaleMode = .aspectFill
return scene
}(), debugOptions: [.showsFPS, .showsNodeCount])
.ignoresSafeArea()
}
}
```
### UIViewRepresentable (Advanced)
Use when you need full control over SKView configuration (custom frame rate, transparency, or multiple scenes).
```swift
import SwiftUI
import SpriteKit
struct SpriteKitView: UIViewRepresentable {
let scene: SKScene
func makeUIView(context: Context) -> SKView {
let view = SKView()
view.showsFPS = true
view.showsNodeCount = true
view.ignoresSiblingOrder = true
return view
}
func updateUIView(_ view: SKView, context: Context) {
if view.scene == nil {
view.presentScene(scene)
}
}
}
```
### SKRenderer for Metal Hybrid
Use `SKRenderer` when SpriteKit is one layer in a Metal pipeline:
```swift
let renderer = SKRenderer(device: metalDevice)
renderer.scene = gameScene
// In your Metal render loop:
renderer.update(atTime: currentTime)
renderer.render(
withViewport: viewport,
commandBuffer: commandBuffer,
renderPassDescriptor: renderPassDescriptor
)
```
---
## 10. Anti-Patterns
### Anti-Pattern 1: Default Bitmasks
**Time cost**: 30-120 minutes debugging phantom collisions
```swift
// WRONG: Default collisionBitMask is 0xFFFFFFFF
let body = SKPhysicsBody(circleOfRadius: 10)
node.physicsBody = body
// Collides with EVERYTHING even things it shouldn't
// CORRECT: Always set all three masks explicitly
body.categoryBitMask = PhysicsCategory.player
body.collisionBitMask = PhysicsCategory.ground
body.contactTestBitMask = PhysicsCategory.enemy
```
### Anti-Pattern 2: Missing contactTestBitMask
**Time cost**: 30-60 minutes wondering why didBegin never fires
```swift
// WRONG: contactTestBitMask defaults to 0 no contacts ever fire
player.physicsBody?.categoryBitMask = PhysicsCategory.player
// Forgot contactTestBitMask!
// CORRECT: Both bodies need compatible masks
player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
enemy.physicsBody?.categoryBitMask = PhysicsCategory.enemy
```
### Anti-Pattern 3: Actions on Physics Bodies
**Time cost**: 1-3 hours of jittering and missed collisions
```swift
// WRONG: SKAction.move overrides physics position each frame
playerNode.run(SKAction.moveTo(x: 200, duration: 1.0))
// Physics body position is set by action, ignoring forces/collisions
// CORRECT: Use physics for physics-controlled nodes
playerNode.physicsBody?.applyForce(CGVector(dx: 100, dy: 0))
```
### Anti-Pattern 4: SKShapeNode for Gameplay
**Time cost**: Hours diagnosing frame drops
Each SKShapeNode is a separate draw call that cannot be batched. 50 shape nodes = 50 draw calls. See the pre-render-to-texture pattern in Section 6 (SKShapeNode Trap) for the fix.
### Anti-Pattern 5: Strong Self in Action Closures
**Time cost**: Memory leaks, eventual crash
```swift
// WRONG: Strong capture in repeating action
node.run(SKAction.repeatForever(SKAction.sequence([
SKAction.run { self.spawnEnemy() },
SKAction.wait(forDuration: 2.0)
])))
// CORRECT: Weak capture
node.run(SKAction.repeatForever(SKAction.sequence([
SKAction.run { [weak self] in self?.spawnEnemy() },
SKAction.wait(forDuration: 2.0)
])))
```
---
## 11. Code Review Checklist
### Physics
- [ ] Every physics body has explicit `categoryBitMask` (not default)
- [ ] Every physics body has explicit `collisionBitMask` (not `0xFFFFFFFF`)
- [ ] Bodies needing contact detection have `contactTestBitMask` set
- [ ] `physicsWorld.contactDelegate` is assigned
- [ ] No world modifications inside `didBegin`/`didEnd` callbacks
- [ ] Fast objects use `usesPreciseCollisionDetection`
### Actions
- [ ] No `SKAction.move`/`rotate` on physics-controlled nodes
- [ ] Repeating actions use `withKey:` for cancellation
- [ ] `SKAction.run` closures use `[weak self]`
- [ ] One-shot emitters are removed after emission
### Performance
- [ ] Debug overlays enabled during development
- [ ] `ignoresSiblingOrder = true` on SKView
- [ ] No SKShapeNode in gameplay sprites (use pre-rendered textures)
- [ ] Texture atlases used for related sprites
- [ ] Offscreen nodes removed manually
### Scene Management
- [ ] `willMove(from:)` cleans up actions, children, delegates
- [ ] Scene data passed via shared state, not node properties
- [ ] Camera used for viewport control
---
## 12. Pressure Scenarios
### Scenario 1: "Physics Contacts Don't Work — Ship Tonight"
**Pressure**: Deadline pressure to skip systematic debugging
**Wrong approach**: Randomly changing bitmask values, adding `0xFFFFFFFF` everywhere, or disabling physics
**Correct approach** (2-5 minutes):
1. Enable `showsPhysics` — verify bodies exist and overlap
2. Print all three bitmasks for both bodies
3. Verify `contactTestBitMask` on body A includes category of body B (or vice versa)
4. Verify `physicsWorld.contactDelegate` is set
5. Verify you're not modifying the world inside the callback
**Push-back template**: "Let me run the 5-step bitmask checklist. It takes 2 minutes and catches 90% of contact issues. Random changes will make it worse."
### Scenario 2: "Frame Rate Is Fine on My Device"
**Pressure**: Authority says "it runs at 60fps for me, ship it"
**Wrong approach**: Shipping without profiling on minimum-spec device
**Correct approach**:
1. Enable `showsFPS`, `showsNodeCount`, `showsDrawCount`
2. Test on oldest supported device
3. If >200 nodes or >30 draw calls, investigate
4. Check for SKShapeNode in gameplay
5. Verify offscreen nodes are being removed
**Push-back template**: "Performance varies by device. Let me check node count and draw calls — takes 30 seconds with debug overlays. If counts are low, we're safe to ship."
### Scenario 3: "Just Use SKShapeNode, It's Faster to Code"
**Pressure**: Sunk cost — already built with SKShapeNode, don't want to redo
**Wrong approach**: Shipping with 100+ SKShapeNodes causing frame drops
**Correct approach**:
1. Check `showsDrawCount` — each SKShapeNode adds a draw call
2. If >20 shape nodes in gameplay, pre-render to textures
3. Use `view.texture(from:)` to convert once, reuse as SKSpriteNode
4. Keep SKShapeNode only for debug visualization
**Push-back template**: "Each SKShapeNode is a separate draw call. Converting to pre-rendered textures is a 15-minute refactor that can double frame rate. SKSpriteNode from atlas = 1 draw call for all of them."
## Resources
**WWDC**: 2014-608, 2016-610, 2017-609, 2013-502
**Docs**: /spritekit, /spritekit/skscene, /spritekit/skphysicsbody, /spritekit/maximizing-node-drawing-performance
**Skills**: axiom-spritekit-ref, axiom-spritekit-diag
+3
View File
@@ -0,0 +1,3 @@
# Use bd merge for beads JSONL files
.beads/issues.jsonl merge=beads
+40
View File
@@ -0,0 +1,40 @@
# Agent Instructions
This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started.
## Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --status in_progress # Claim work
bd close <id> # Complete work
bd sync # Sync with git
```
## Landing the Plane (Session Completion)
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
**MANDATORY WORKFLOW:**
1. **File issues for remaining work** - Create issues for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **PUSH TO REMOTE** - This is MANDATORY:
```bash
git pull --rebase
bd sync
git push
git status # MUST show "up to date with origin"
```
5. **Clean up** - Clear stashes, prune remote branches
6. **Verify** - All changes committed AND pushed
7. **Hand off** - Provide context for next session
**CRITICAL RULES:**
- Work is NOT complete until `git push` succeeds
- NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
+23 -3
View File
@@ -43,7 +43,7 @@ export default withMermaid(defineConfig({
items: [
{ text: 'Overview', link: '/guide/' },
{ text: 'Quick Start', link: '/guide/quick-start' },
{ text: 'MCP Server (Experimental)', link: '/guide/mcp-install' },
{ text: 'MCP Server', link: '/guide/mcp-install' },
{ text: 'Example Workflows', link: '/guide/workflows' }
]
}
@@ -104,6 +104,12 @@ export default withMermaid(defineConfig({
{ text: 'simulator-tester', link: '/agents/simulator-tester' },
{ text: 'testing-auditor', link: '/agents/testing-auditor' }
]
},
{
text: 'Games',
items: [
{ text: 'spritekit-auditor', link: '/agents/spritekit-auditor' }
]
}
],
'/hooks/': [
@@ -193,6 +199,13 @@ export default withMermaid(defineConfig({
{ text: 'Swift Testing', link: '/skills/testing/swift-testing' },
{ text: 'UI Testing', link: '/skills/ui-design/ui-testing' }
]
},
{
text: 'Games',
items: [
{ text: 'Overview', link: '/skills/games/' },
{ text: 'SpriteKit', link: '/skills/games/spritekit' }
]
}
],
'/reference/': [
@@ -243,6 +256,12 @@ export default withMermaid(defineConfig({
{ text: 'Privacy UX Patterns', link: '/reference/privacy-ux' },
{ text: 'StoreKit 2 (In-App Purchases)', link: '/reference/storekit-ref' }
]
},
{
text: 'Games',
items: [
{ text: 'SpriteKit API', link: '/reference/spritekit-ref' }
]
}
],
'/diagnostic/': [
@@ -264,7 +283,8 @@ export default withMermaid(defineConfig({
{ text: 'Storage Diagnostics', link: '/diagnostic/storage-diag' },
{ text: 'SwiftData Migration Diagnostics', link: '/diagnostic/swiftdata-migration-diag' },
{ text: 'SwiftUI Debugging Diagnostics', link: '/diagnostic/swiftui-debugging-diag' },
{ text: 'SwiftUI Navigation Diagnostics', link: '/diagnostic/swiftui-nav-diag' }
{ text: 'SwiftUI Navigation Diagnostics', link: '/diagnostic/swiftui-nav-diag' },
{ text: 'SpriteKit Diagnostics', link: '/diagnostic/spritekit-diag' }
]
}
]
@@ -276,7 +296,7 @@ export default withMermaid(defineConfig({
footer: {
message: 'Released under the MIT License',
copyright: 'Copyright © 2026 Charles Wiltgen • v2.19.6'
copyright: 'Copyright © 2026 Charles Wiltgen • v2.20.0'
}
}
}))
+4 -4
View File
@@ -1,7 +1,7 @@
{
"disciplineSkills": 79,
"referenceSkills": 35,
"diagnosticSkills": 15,
"disciplineSkills": 81,
"referenceSkills": 36,
"diagnosticSkills": 16,
"commands": 10,
"agents": 30
"agents": 31
}
+63
View File
@@ -0,0 +1,63 @@
# spritekit-auditor
Scans SpriteKit game code for the 8 most common anti-patterns that cause physics bugs, performance issues, and memory leaks.
## How to Use This Agent
**Natural language (automatic triggering):**
- "Can you check my SpriteKit code for issues?"
- "Audit my game for performance problems"
- "Scan my SpriteKit project for anti-patterns"
- "Check my physics bitmask setup"
**Explicit command:**
```bash
/axiom:audit spritekit
```
## What It Checks
### Critical
- **Physics bitmask issues** — Default `0xFFFFFFFF` masks, missing `contactTestBitMask`, magic number bitmasks without named constants
### High Priority
- **Draw call waste**`SKShapeNode` used for gameplay sprites (1 draw call each, unbatchable), missing texture atlases
- **Node accumulation** — Nodes created but never removed, `addChild` without matching `removeFromParent`
- **Action memory leaks** — Strong `self` capture in `SKAction.run` closures, `repeatForever` without `withKey:`
### Medium Priority
- **Coordinate confusion**`touch.location(in: self.view)` instead of `touch.location(in: self)`
- **Touch handling bugs**`touchesBegan` implemented without `isUserInteractionEnabled = true`
- **Missing object pooling**`SKSpriteNode` creation inside `update()` or spawn functions
### Low Priority
- **Missing debug overlays** — No `showsFPS`, `showsNodeCount`, or `showsDrawCount` configured
## Example Output
```markdown
## SpriteKit Audit Results
### Summary
- **CRITICAL Issues**: 2 (Physics bitmask problems)
- **HIGH Issues**: 3 (Draw call waste, action leaks)
- **MEDIUM Issues**: 1 (Touch handling)
### CRITICAL: Default Bitmask
**File**: `GameScene.swift:45`
**Issue**: collisionBitMask not set (defaults to 0xFFFFFFFF)
**Impact**: Body collides with everything, causing phantom collisions
**Fix**: Set explicit collisionBitMask using PhysicsCategory struct
```
## Model & Tools
- **Model**: sonnet (needs code understanding for pattern analysis)
- **Tools**: Glob, Grep, Read
- **Color**: green
## Related
- [SpriteKit](/skills/games/spritekit) — Architecture patterns and anti-patterns
- [SpriteKit API Reference](/reference/spritekit-ref) — Complete API tables
- [SpriteKit Diagnostics](/diagnostic/spritekit-diag) — Decision trees for common symptoms
+63
View File
@@ -0,0 +1,63 @@
---
name: spritekit-diag
description: SpriteKit diagnostics — physics contacts, tunneling, frame drops, touch bugs, memory, coordinates, transitions
---
# SpriteKit Diagnostics
Systematic SpriteKit troubleshooting with decision trees and time-cost annotations. Covers the 7 most common SpriteKit symptoms that waste developer time.
## Symptoms This Diagnoses
Use when you're experiencing:
- `didBegin(_:)` never called (physics contacts not firing)
- Objects passing through walls (tunneling)
- Frame rate below 60fps (performance drops)
- `touchesBegan` not called on nodes
- Memory growing during gameplay
- Sprites appearing in wrong positions (coordinate confusion)
- Crashes during or after scene transitions
## Example Prompts
- "My physics contacts aren't firing, didBegin never gets called"
- "Bullets pass through walls in my game"
- "SpriteKit frame rate is dropping"
- "touchesBegan doesn't work on my sprite node"
- "Memory keeps growing during my game"
- "My sprite positions are Y-flipped"
- "App crashes when transitioning between scenes"
## Diagnostic Workflow
**Mandatory first step**: Enable debug overlays (`showsFPS`, `showsNodeCount`, `showsDrawCount`, `showsPhysics`). Most SpriteKit bugs become visually obvious with overlays enabled.
### Decision Trees
| Symptom | Branches | Time Saved |
|---------|----------|------------|
| Physics contacts not firing | 6 branches | 30-120 min → 2-5 min |
| Objects tunneling through walls | 5 branches | 20-60 min → 5 min |
| Poor frame rate | 4 top, 12 leaves | 2-4 hrs → 15-30 min |
| Touches not registering | 6 branches | 15-45 min → 2 min |
| Memory spikes/crashes | 5 branches | 1-3 hrs → 15 min |
| Coordinate confusion | 5 branches | 20-60 min → 5 min |
| Scene transition crashes | 5 branches | 30-90 min → 5 min |
### Quick Reference
| 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 |
## Related
- [SpriteKit](/skills/games/spritekit) — Architecture patterns, anti-patterns, and code review checklist
- [SpriteKit API Reference](/reference/spritekit-ref) — Complete API tables for all SpriteKit classes
- [spritekit-auditor](/agents/spritekit-auditor) — Automated scanning for SpriteKit anti-patterns
+25 -75
View File
@@ -1,42 +1,24 @@
# MCP Server (Experimental)
# MCP Server
Axiom includes an MCP (Model Context Protocol) server that brings its iOS development skills to any MCP-compatible AI coding tool — VS Code with GitHub Copilot, Claude Desktop, Cursor, Gemini CLI, and more.
::: warning Experimental
The MCP server is functional but pre-npm-publish. Installation currently requires cloning the repository and building from source. An `npm install` workflow is planned for a future release.
:::
## What You Get
The MCP server exposes Axiom's full catalog through the MCP protocol:
- **129 skills** as MCP Resources (on-demand loading)
- **30 agents** as MCP Tools (autonomous scanning and fixing)
- **133 skills** as MCP Resources (on-demand loading)
- **31 agents** as MCP Tools (autonomous scanning and fixing)
- **10 commands** as MCP Prompts (structured workflows)
## Prerequisites
- **Node.js 18+** — check with `node --version`
- **pnpm** (or npm) — for installing dependencies
- **Clone the Axiom repository:**
```bash
git clone https://github.com/CharlesWiltgen/Axiom.git
cd Axiom/mcp-server
```
- **Build the MCP server:**
```bash
pnpm install
pnpm build
```
This compiles the TypeScript source and produces `dist/index.js`, the server entry point.
That's it. No cloning, no building.
## Installation by Tool
Each tool needs a configuration snippet that tells it how to launch the Axiom MCP server. Replace `/path/to/Axiom` with your actual clone path.
Each tool needs a configuration snippet that tells it how to launch the Axiom MCP server.
### VS Code + GitHub Copilot
@@ -46,12 +28,8 @@ Add to your VS Code `settings.json`:
{
"github.copilot.chat.mcp.servers": {
"axiom": {
"command": "node",
"args": ["/path/to/Axiom/mcp-server/dist/index.js"],
"env": {
"AXIOM_MCP_MODE": "development",
"AXIOM_DEV_PATH": "/path/to/Axiom/.claude-plugin/plugins/axiom"
}
"command": "npx",
"args": ["-y", "axiom-mcp"]
}
}
}
@@ -65,12 +43,8 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
{
"mcpServers": {
"axiom": {
"command": "node",
"args": ["/path/to/Axiom/mcp-server/dist/index.js"],
"env": {
"AXIOM_MCP_MODE": "development",
"AXIOM_DEV_PATH": "/path/to/Axiom/.claude-plugin/plugins/axiom"
}
"command": "npx",
"args": ["-y", "axiom-mcp"]
}
}
}
@@ -84,12 +58,8 @@ Add to `.cursor/mcp.json` in your workspace:
{
"mcpServers": {
"axiom": {
"command": "node",
"args": ["/path/to/Axiom/mcp-server/dist/index.js"],
"env": {
"AXIOM_MCP_MODE": "development",
"AXIOM_DEV_PATH": "/path/to/Axiom/.claude-plugin/plugins/axiom"
}
"command": "npx",
"args": ["-y", "axiom-mcp"]
}
}
}
@@ -102,12 +72,8 @@ Add to `~/.gemini/config.toml`:
```toml
[[mcp_servers]]
name = "axiom"
command = "node"
args = ["/path/to/Axiom/mcp-server/dist/index.js"]
[mcp_servers.env]
AXIOM_MCP_MODE = "development"
AXIOM_DEV_PATH = "/path/to/Axiom/.claude-plugin/plugins/axiom"
command = "npx"
args = ["-y", "axiom-mcp"]
```
## Configuration
@@ -122,34 +88,32 @@ AXIOM_DEV_PATH = "/path/to/Axiom/.claude-plugin/plugins/axiom"
### Development Mode (Live Skills)
Reads skills directly from the Claude Code plugin directory. Changes to skill files are reflected immediately — no rebuild needed. This is the recommended mode when you've cloned the repo.
For Axiom contributors who want live-reloading skills during development:
```bash
AXIOM_MCP_MODE=development \
AXIOM_DEV_PATH=/path/to/Axiom/.claude-plugin/plugins/axiom \
node dist/index.js
node /path/to/Axiom/mcp-server/dist/index.js
```
Changes to skill files are reflected immediately — no rebuild needed.
### Production Mode (Bundled)
Reads from a pre-compiled snapshot (`dist/bundle.json`). Self-contained with no file system access after initialization. Build the bundle first:
The default when installed via npm. Reads from a pre-compiled snapshot with no file system access after initialization.
```bash
pnpm build:bundle
node dist/index.js
npx axiom-mcp
```
## Verify It Works
### Quick Test
Start the server manually to confirm it launches without errors:
Run the server directly to confirm it launches without errors:
```bash
cd /path/to/Axiom/mcp-server
AXIOM_MCP_MODE=development \
AXIOM_DEV_PATH=../.claude-plugin/plugins/axiom \
node dist/index.js
npx axiom-mcp
```
The server should start and wait for stdin input (MCP uses stdio transport). Press `Ctrl+C` to stop.
@@ -159,7 +123,7 @@ The server should start and wait for stdin input (MCP uses stdio transport). Pre
For interactive testing, use the official MCP Inspector:
```bash
npx @modelcontextprotocol/inspector node dist/index.js
npx @modelcontextprotocol/inspector npx axiom-mcp
```
This opens a web UI where you can browse resources, test prompts, and invoke tools.
@@ -181,37 +145,23 @@ It should list Axiom's available skills via the MCP resources protocol.
node --version
```
**Verify the build completed** — `dist/index.js` should exist:
```bash
ls /path/to/Axiom/mcp-server/dist/index.js
```
**Check environment variables** — in development mode, `AXIOM_DEV_PATH` must point to a valid plugin directory:
```bash
ls /path/to/Axiom/.claude-plugin/plugins/axiom/skills
```
### Skills Not Appearing
**Enable debug logging** to see what the server loads:
```bash
AXIOM_LOG_LEVEL=debug \
AXIOM_MCP_MODE=development \
AXIOM_DEV_PATH=../.claude-plugin/plugins/axiom \
node dist/index.js 2>&1 | grep -i skill
AXIOM_LOG_LEVEL=debug npx axiom-mcp 2>&1 | grep -i skill
```
### Client Can't Connect
MCP uses stdin/stdout for communication. Common issues:
- **Wrong path** in your tool's config — double-check the absolute path to `dist/index.js`
- **Missing build** — run `pnpm build` if `dist/index.js` doesn't exist
- **Wrong config** — ensure `command` is `"npx"` and `args` is `["-y", "axiom-mcp"]`
- **Other stdout writers** — make sure nothing else writes to stdout; logs go to stderr only
Test the command from your config manually:
```bash
node /path/to/Axiom/mcp-server/dist/index.js
npx axiom-mcp
# Should start without errors, waiting for stdin
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

+62
View File
@@ -0,0 +1,62 @@
---
name: spritekit-ref
description: SpriteKit API reference — node types, physics bodies, actions, textures, constraints, particles, SKRenderer
---
# SpriteKit API Reference
Complete API reference for SpriteKit organized by category. Covers all 16 node types, physics body creation and properties, the full action catalog, texture atlases, constraints, scene setup, particle emitters, and SKRenderer for Metal integration.
## When to Use This Reference
Use this reference when:
- Looking up specific SpriteKit API signatures or properties
- Checking physics body creation methods
- Finding the right SKAction for an animation
- Configuring SKEmitterNode particle properties
- Setting up SKView with debug overlays
- Looking up SKConstraint types
- Configuring SKShader for custom effects
## Example Prompts
- "How do I create a physics body from a texture?"
- "What SKAction types are available for movement?"
- "What properties does SKEmitterNode have?"
- "How do I set up SKRenderer for Metal?"
- "What are the SKView debug overlay options?"
- "What particle settings create a fire effect?"
## What's Covered
### Part 1: Node Hierarchy
All 16 node types with purpose, batchability, and performance notes. Key properties for SKSpriteNode (anchor points, color blend, lighting, shaders) and SKLabelNode (font, alignment, multiline).
### Part 2: Physics API
Body creation methods (circle, rectangle, polygon, texture, edge, compound). All physics body properties (mass, friction, restitution, damping). Force/impulse methods. SKPhysicsWorld configuration. All 5 joint types (pin, fixed, spring, sliding, limit). Physics field types (gravity, radial, electric, noise, vortex, drag).
### Part 3: Action Catalog
All action types organized by category: movement, rotation, scaling, fading, composition, texture/color, sound, node tree, physics. Timing modes and speed control.
### Part 4: Textures and Atlases
SKTexture creation (imageNamed, atlas, subrectangle, CGImage). Filtering modes (nearest for pixel art, linear for smooth). Atlas preloading. Animation from atlas frames.
### Part 5: Constraints
SKConstraint types: orient, position, distance, rotation. SKRange creation patterns. Constraint ordering and toggling.
### Part 6: Scene Setup
SKView configuration with all debug overlays. Scale mode matrix. All SKTransition types.
### Part 7: Particles
SKEmitterNode key properties organized by category (emission, position, movement, appearance, scale, rotation). Common particle preset settings for fire, smoke, sparks, rain, snow, trails, explosions.
### Part 8: SKRenderer and Shaders
SKRenderer Metal integration pattern. SKShader GLSL-like syntax with uniforms and built-in variables.
## Documentation Scope
This page documents the `axiom-spritekit-ref` skill. For architecture patterns and best practices, use the discipline skill. For troubleshooting, use the diagnostic skill.
- For game development patterns, see [SpriteKit](/skills/games/spritekit)
- For troubleshooting, see [SpriteKit Diagnostics](/diagnostic/spritekit-diag)
- For automated scanning, use [spritekit-auditor](/agents/spritekit-auditor)
+32
View File
@@ -0,0 +1,32 @@
# Games
Skills for building games on Apple platforms using SpriteKit, with future support for SceneKit and GameplayKit.
## Available Skills
### SpriteKit
Complete guide to building 2D games with SpriteKit. Covers the scene graph model, physics engine (bitmask discipline, contact detection, body types), action system, game loop, performance optimization, and SwiftUI/Metal integration.
- [SpriteKit](/skills/games/spritekit) — Architecture, patterns, anti-patterns, and code review checklist
## Available Agents
- [spritekit-auditor](/agents/spritekit-auditor) — Scans SpriteKit code for physics bitmask issues, draw call waste, node accumulation, and action memory leaks
## Available References
- [SpriteKit API](/reference/spritekit-ref) — All 16 node types, physics body creation, complete action catalog, texture atlases, constraints, particles, SKRenderer
## Available Diagnostics
- [SpriteKit Diagnostics](/diagnostic/spritekit-diag) — Decision trees for contacts not firing, tunneling, frame drops, touch bugs, memory spikes, coordinate confusion, transition crashes
## Example Prompts
- "I'm building a SpriteKit game"
- "My physics contacts aren't firing"
- "Frame rate is dropping in my game"
- "How do I set up SpriteKit with SwiftUI?"
- "Objects pass through walls in my game"
- "Audit my SpriteKit code for issues"
+85
View File
@@ -0,0 +1,85 @@
---
name: spritekit
description: SpriteKit game development — scene graph, physics, actions, performance, SwiftUI integration
---
# SpriteKit
Complete guide to building reliable SpriteKit games. Covers the scene graph model, physics engine, action system, game loop, rendering optimization, and integration with SwiftUI and Metal.
## When to Use
Use this skill when:
- Building a new SpriteKit game or interactive simulation
- Implementing physics (collisions, contacts, forces, joints)
- Setting up game architecture (scenes, layers, cameras)
- Optimizing frame rate or reducing draw calls
- Implementing touch/input handling in a game
- Managing scene transitions and data passing
- Integrating SpriteKit with SwiftUI or Metal
- Debugging physics contacts that don't fire
## Example Prompts
- "I'm building a SpriteKit platformer, how should I structure the scenes?"
- "My physics contacts aren't firing — what's wrong?"
- "How do I organize layers with a camera node?"
- "What's the correct way to handle touch in SpriteKit?"
- "My frame rate is dropping, how do I optimize?"
- "How do I integrate SpriteKit with SwiftUI?"
- "Objects are passing through walls in my game"
## What This Skill Provides
### Scene Graph Model
- Bottom-left origin coordinate system (opposite of UIKit)
- Anchor point mechanics for sprites and scenes
- Node tree hierarchy with z-ordering layers
- Camera node pattern for viewport control and HUD
### Physics Engine
- Bitmask discipline (the #1 source of SpriteKit bugs)
- PhysicsCategory struct pattern for named bitmasks
- Contact detection with delegate pattern
- Body types: dynamic volume, static volume, edge
- Tunneling prevention with precise collision detection
- Forces vs impulses for movement
### Actions System
- Sequencing, grouping, and repeating actions
- Named actions for cancellation and management
- Timing modes (linear, easeIn, easeOut, easeInEaseOut)
- Critical rule: never use actions on physics-controlled nodes
### Performance Optimization
- Debug overlays (showsFPS, showsNodeCount, showsDrawCount)
- Texture atlas batching for reduced draw calls
- SKShapeNode trap (1 draw call per instance, unbatchable)
- Object pooling for frequently spawned objects
- Offscreen node removal
### Game Loop
- 8-phase frame cycle understanding
- Delta time with spiral-of-death clamping
- Pause handling
### Anti-Patterns
- Default bitmasks (0xFFFFFFFF)
- Missing contactTestBitMask
- Actions fighting physics
- SKShapeNode for gameplay sprites
- Strong self capture in action closures
### Code Review Checklist
- 14-item verification covering physics, actions, performance, and scene management
### Pressure Scenarios
- Physics contacts deadline debugging
- Frame rate denial
- SKShapeNode sunk cost
## Related
- [SpriteKit API Reference](/reference/spritekit-ref) — Complete API tables for all node types, physics, actions, textures, and particles
- [SpriteKit Diagnostics](/diagnostic/spritekit-diag) — Decision trees for 7 common SpriteKit symptoms
- [spritekit-auditor](/agents/spritekit-auditor) — Automated scanning for SpriteKit anti-patterns
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Charles Wiltgen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+69 -113
View File
@@ -4,14 +4,31 @@ Model Context Protocol (MCP) server for Axiom's iOS development skills, agents,
## Features
- **129 Skills** - iOS development expertise as MCP Resources (on-demand loading)
- **10 Commands** - Structured prompts as MCP Prompts
- **30 Agents** - Autonomous tools as MCP Tools
- **Dual Distribution** - Works standalone or bundled with Claude Code plugin
- **Hybrid Runtime** - Development mode (live files) or production mode (bundled)
- **133 Skills** iOS development expertise as MCP Resources (on-demand loading)
- **10 Commands** Structured prompts as MCP Prompts
- **31 Agents** Autonomous tools as MCP Tools
- **Dual Distribution** Works standalone or bundled with Claude Code plugin
- **Hybrid Runtime** Development mode (live files) or production mode (bundled)
## Installation
### Quick Start (npm)
No clone or build step needed. Add to your tool's MCP configuration:
```json
{
"mcpServers": {
"axiom": {
"command": "npx",
"args": ["-y", "axiom-mcp"]
}
}
}
```
This downloads and runs the server in production mode with all skills bundled.
### For Claude Code Users (Bundled)
The MCP server starts automatically when you install the Axiom plugin:
@@ -20,22 +37,7 @@ The MCP server starts automatically when you install the Axiom plugin:
claude-code plugin add axiom@axiom-marketplace
```
No additional configuration needed! The plugin's `.mcp.json` launches the server in development mode.
### For Other Tools (Standalone)
Install via pnpm (or npm):
```bash
# Install dependencies
pnpm install
# Build the server
pnpm build
# Run in development mode
AXIOM_MCP_MODE=development AXIOM_DEV_PATH=/path/to/axiom/plugin node dist/index.js
```
No additional configuration needed — the plugin's `.mcp.json` launches the server in development mode.
## Usage
@@ -47,31 +49,13 @@ Add to your VS Code `settings.json`:
{
"github.copilot.chat.mcp.servers": {
"axiom": {
"command": "node",
"args": ["/Users/YourName/Projects/Axiom/mcp-server/dist/index.js"],
"env": {
"AXIOM_MCP_MODE": "development",
"AXIOM_DEV_PATH": "/Users/YourName/Projects/Axiom/.claude-plugin/plugins/axiom"
}
"command": "npx",
"args": ["-y", "axiom-mcp"]
}
}
}
```
Then in GitHub Copilot Chat:
```
User: What iOS debugging skills do you have?
Copilot: [calls resources/list]
I have access to Axiom's iOS development skills:
**Debugging & Troubleshooting**
- xcode-debugging: Environment-first diagnostics
- memory-debugging: Leak diagnosis (5 patterns)
...
```
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
@@ -80,12 +64,8 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
{
"mcpServers": {
"axiom": {
"command": "node",
"args": ["/Users/YourName/Projects/Axiom/mcp-server/dist/index.js"],
"env": {
"AXIOM_MCP_MODE": "development",
"AXIOM_DEV_PATH": "/Users/YourName/Projects/Axiom/.claude-plugin/plugins/axiom"
}
"command": "npx",
"args": ["-y", "axiom-mcp"]
}
}
}
@@ -93,18 +73,14 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
### Cursor
Add to Cursor's MCP settings (`.cursor/mcp.json` in your workspace):
Add to `.cursor/mcp.json` in your workspace:
```json
{
"mcpServers": {
"axiom": {
"command": "node",
"args": ["/Users/YourName/Projects/Axiom/mcp-server/dist/index.js"],
"env": {
"AXIOM_MCP_MODE": "development",
"AXIOM_DEV_PATH": "/Users/YourName/Projects/Axiom/.claude-plugin/plugins/axiom"
}
"command": "npx",
"args": ["-y", "axiom-mcp"]
}
}
}
@@ -117,12 +93,8 @@ Configure MCP server in `~/.gemini/config.toml`:
```toml
[[mcp_servers]]
name = "axiom"
command = "node"
args = ["/Users/YourName/Projects/Axiom/mcp-server/dist/index.js"]
[mcp_servers.env]
AXIOM_MCP_MODE = "development"
AXIOM_DEV_PATH = "/Users/YourName/Projects/Axiom/.claude-plugin/plugins/axiom"
command = "npx"
args = ["-y", "axiom-mcp"]
```
## Configuration
@@ -132,7 +104,7 @@ AXIOM_DEV_PATH = "/Users/YourName/Projects/Axiom/.claude-plugin/plugins/axiom"
| Variable | Values | Default | Description |
|----------|--------|---------|-------------|
| `AXIOM_MCP_MODE` | `development`, `production` | `production` | Runtime mode |
| `AXIOM_DEV_PATH` | File path | `~/Projects/Axiom/.claude-plugin/plugins/axiom` | Plugin directory for dev mode |
| `AXIOM_DEV_PATH` | File path | | Plugin directory for dev mode |
| `AXIOM_LOG_LEVEL` | `debug`, `info`, `warn`, `error` | `info` | Logging verbosity |
### Modes
@@ -151,15 +123,14 @@ AXIOM_MCP_MODE=development AXIOM_DEV_PATH=/path/to/plugin node dist/index.js
#### Production Mode (Bundled Skills)
```bash
# Default mode - no environment variables needed
node dist/index.js
# Default mode no environment variables needed
npx axiom-mcp
```
- Reads pre-bundled snapshot from `dist/bundle.json`
- Bundle contains all 129 skills, 10 commands, 30 agents
- Bundle contains all 133 skills, 10 commands, 31 agents
- No file system access after initialization
- Self-contained, distributable via npm
- Bundle generated via `pnpm build:bundle`
- Self-contained, distributed via npm
## MCP Resources
@@ -235,11 +206,15 @@ mcp-server/
│ │ └── handler.ts # Prompts protocol
│ ├── tools/
│ │ └── handler.ts # Tools protocol
│ ├── catalog/
│ │ └── index.ts # Skill catalog + search
│ ├── search/
│ │ └── index.ts # BM25 search engine
│ └── scripts/
│ └── bundle.ts # Bundle generator
└── dist/ # Compiled output
├── index.js # Server entry point
├── bundle.json # Production bundle (1.15 MB)
├── bundle.json # Production bundle
└── ...
```
@@ -247,19 +222,19 @@ mcp-server/
```bash
# Install dependencies
pnpm install
npm install
# Build once
pnpm build
npm run build
# Build with production bundle
pnpm build:bundle
npm run build:bundle
# Watch mode (rebuild on changes)
pnpm dev
npm run dev
# Run server
pnpm start
npm start
```
The `build:bundle` command:
@@ -316,7 +291,7 @@ echo '{"jsonrpc":"2.0","id":1,"method":"resources/list"}' | node dist/index.js
Install the official MCP Inspector:
```bash
npx @modelcontextprotocol/inspector node dist/index.js
npx @modelcontextprotocol/inspector npx axiom-mcp
```
Opens a web UI for testing MCP protocol interactions.
@@ -333,7 +308,7 @@ claude-code plugin reload axiom
## Troubleshooting
### Server won't start
### Server Won't Start
**Check Node version:**
```bash
@@ -347,41 +322,29 @@ echo $AXIOM_MCP_MODE
echo $AXIOM_DEV_PATH
```
**Verify plugin path exists:**
**Verify plugin path exists (dev mode):**
```bash
ls $AXIOM_DEV_PATH/skills
# Should show .md files
# Should show skill directories
```
### Skills not appearing
### Skills Not Appearing
**Check log output (stderr):**
```bash
AXIOM_LOG_LEVEL=debug node dist/index.js 2>&1 | grep -i skill
AXIOM_LOG_LEVEL=debug npx axiom-mcp 2>&1 | grep -i skill
```
**Verify frontmatter parsing:**
### MCP Client Can't Connect
MCP uses stdin/stdout for communication. Common issues:
- **Wrong command** in your tool's config — use `npx` with args `["-y", "axiom-mcp"]`
- **Other stdout writers** — make sure nothing else writes to stdout; logs go to stderr only
Test the command from your config manually:
```bash
# Test parser directly
node -e "
const matter = require('gray-matter');
const fs = require('fs');
const content = fs.readFileSync('$AXIOM_DEV_PATH/skills/axiom-xcode-debugging/SKILL.md', 'utf-8');
console.log(matter(content).data);
"
```
### MCP client can't connect
**Check stdio transport:**
- MCP uses stdin/stdout for communication
- Make sure nothing else writes to stdout
- Logs must go to stderr only
**Verify command in client config:**
```bash
# Test command manually
node /full/path/to/dist/index.js
npx axiom-mcp
# Should start without errors, waiting for stdin
```
@@ -403,23 +366,16 @@ node /full/path/to/dist/index.js
- Tools protocol (agents)
- Complete MCP feature coverage
### Phase 4: Production Bundle ✅ (Current)
### Phase 4: Production Bundle ✅
- Pre-compiled skill snapshot
- Production mode loader
- Bundle generator script
- Dual-mode Loader interface
### Phase 5: Full Coverage (Next)
- All 129 skills with MCP annotations
- All 10 commands with argument schemas
- All 30 agents with input schemas
- Multi-client testing
### Phase 6: Distribution
- npm publish (@axiom-dev/mcp)
- MCP Registry listing
- Documentation site integration
- Release automation
### Phase 5: npm Distribution ✅
- Published as `axiom-mcp` on npm
- Zero-config install via `npx axiom-mcp`
- Multi-client configuration guides
## Architecture
@@ -432,7 +388,7 @@ User installs plugin → .mcp.json → MCP server (dev mode) → Live skills
**Standalone (Other Tools)**
```
User configures MCP → Server (prod mode) → Bundled skills
npx axiom-mcp → Server (prod mode) → Bundled skills
```
**Key Insight:** Same codebase, different entry points. Development mode for rapid iteration, production mode for distribution.
@@ -450,4 +406,4 @@ See the main Axiom repository for contribution guidelines.
## License
MIT License - See LICENSE file in main repository
MIT License See [LICENSE](LICENSE)
+17 -3
View File
@@ -1,12 +1,17 @@
{
"name": "@axiom-dev/mcp",
"version": "0.1.0",
"name": "axiom-mcp",
"version": "2.20.0",
"description": "MCP server for Axiom development skills, agents, and commands",
"type": "module",
"main": "dist/index.js",
"bin": {
"axiom-mcp": "dist/index.js"
"axiom-mcp": "./dist/index.js"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsc",
"build:bundle": "tsc && node dist/scripts/bundle.js",
@@ -25,6 +30,15 @@
],
"author": "Charles Wiltgen",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/CharlesWiltgen/Axiom.git",
"directory": "mcp-server"
},
"homepage": "https://charleswiltgen.github.io/Axiom/guide/mcp-install",
"bugs": {
"url": "https://github.com/CharlesWiltgen/Axiom/issues"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"gray-matter": "^4.0.3"
+2
View File
@@ -17,6 +17,7 @@ const ROUTER_CATEGORIES: Record<string, string> = {
'axiom-ios-ml': 'Machine Learning',
'axiom-ios-vision': 'Computer Vision',
'axiom-ios-graphics': 'Graphics & Metal',
'axiom-ios-games': 'Games',
'axiom-ios-testing': 'Testing',
};
@@ -124,6 +125,7 @@ function inferCategoryFromName(name: string): string {
if (name.includes('vision') || name.includes('photo') || name.includes('camera')) return 'Computer Vision';
if (name.includes('foundation-model') || name.includes('intelligence')) return 'Apple Intelligence';
if (name.includes('metal') || name.includes('graphics')) return 'Graphics & Metal';
if (name.includes('spritekit') || name.includes('scenekit') || name.includes('game')) return 'Games';
if (name.includes('debug')) return 'Build & Environment';
if (name.includes('triage') || name.includes('app-store-connect')) return 'Build & Environment';
if (name.includes('intent') || name.includes('shortcut') || name.includes('widget') || name.includes('extension') || name.includes('haptic') || name.includes('storekit') || name.includes('iap') || name.includes('now-playing') || name.includes('localization') || name.includes('spotlight') || name.includes('privacy') || name.includes('deep-link') || name.includes('app-store') || name.includes('background-process')) return 'System Integration';
+4 -1
View File
@@ -20,10 +20,13 @@ import { PromptsHandler } from './prompts/handler.js';
import { DynamicToolsHandler } from './tools/handler.js';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { readFileSync } from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
/**
* Main entry point for Axiom MCP Server
*/
@@ -58,7 +61,7 @@ async function main() {
const server = new Server(
{
name: 'axiom-mcp',
version: '0.1.0',
version: pkg.version,
},
{
capabilities: {
+17
View File
@@ -325,6 +325,23 @@ try {
label: '.claude-plugin/plugins/axiom/hooks/metadata.txt'
});
// 5. Prepare mcp-server/package.json update
const mcpPackagePath = path.join(root, 'mcp-server/package.json');
if (fs.existsSync(mcpPackagePath)) {
let mcpPackage;
try {
mcpPackage = JSON.parse(fs.readFileSync(mcpPackagePath, 'utf8'));
} catch (err) {
throw new Error(`Failed to parse mcp-server/package.json: ${err.message}`);
}
mcpPackage.version = version;
updates.push({
path: mcpPackagePath,
content: JSON.stringify(mcpPackage, null, 2) + '\n',
label: 'mcp-server/package.json'
});
}
// Write all files atomically (write to temp, then rename)
const tempFiles = [];
try {