mirror of
https://github.com/CharlesWiltgen/Axiom.git
synced 2026-09-20 19:58:20 +08:00
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:
@@ -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."
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
# Use bd merge for beads JSONL files
|
||||
.beads/issues.jsonl merge=beads
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"disciplineSkills": 79,
|
||||
"referenceSkills": 35,
|
||||
"diagnosticSkills": 15,
|
||||
"disciplineSkills": 81,
|
||||
"referenceSkills": 36,
|
||||
"diagnosticSkills": 16,
|
||||
"commands": 10,
|
||||
"agents": 30
|
||||
"agents": 31
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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 |
@@ -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)
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user