Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
7
.claude/skills/axiom-spritekit-ref/.openskills.json
Normal file
7
.claude/skills/axiom-spritekit-ref/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-spritekit-ref",
|
||||
"installedAt": "2026-04-12T08:06:39.535Z"
|
||||
}
|
||||
711
.claude/skills/axiom-spritekit-ref/SKILL.md
Normal file
711
.claude/skills/axiom-spritekit-ref/SKILL.md
Normal file
@@ -0,0 +1,711 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
|
||||
## Part 7: SwiftUI Integration
|
||||
|
||||
### SpriteView
|
||||
|
||||
```swift
|
||||
import SpriteKit
|
||||
import SwiftUI
|
||||
|
||||
// Basic embedding
|
||||
struct GameView: View {
|
||||
var body: some View {
|
||||
SpriteView(scene: makeScene())
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
||||
func makeScene() -> SKScene {
|
||||
let scene = GameScene(size: CGSize(width: 1024, height: 768))
|
||||
scene.scaleMode = .aspectFill
|
||||
return scene
|
||||
}
|
||||
}
|
||||
|
||||
// With options
|
||||
SpriteView(
|
||||
scene: scene,
|
||||
transition: .fade(withDuration: 0.5), // Scene transition
|
||||
isPaused: false, // Pause control
|
||||
preferredFramesPerSecond: 60, // Frame rate
|
||||
options: [.ignoresSiblingOrder, .shouldCullNonVisibleNodes],
|
||||
debugOptions: [.showsFPS, .showsNodeCount] // Debug overlays
|
||||
)
|
||||
```
|
||||
|
||||
### SpriteView Options
|
||||
|
||||
| Option | Purpose |
|
||||
|--------|---------|
|
||||
| `.ignoresSiblingOrder` | Enable draw order batching optimization |
|
||||
| `.shouldCullNonVisibleNodes` | Auto-hide offscreen nodes |
|
||||
| `.allowsTransparency` | Allow transparent background (slower) |
|
||||
|
||||
### Debug Options
|
||||
|
||||
| Option | Shows |
|
||||
|--------|-------|
|
||||
| `.showsFPS` | Frames per second |
|
||||
| `.showsNodeCount` | Total visible nodes |
|
||||
| `.showsDrawCount` | Draw calls per frame |
|
||||
| `.showsPhysics` | Physics body outlines |
|
||||
| `.showsFields` | Physics field regions |
|
||||
| `.showsQuadCount` | Quad subdivisions |
|
||||
|
||||
### Communicating Between SwiftUI and SpriteKit
|
||||
|
||||
```swift
|
||||
// Observable model shared between SwiftUI and scene
|
||||
@Observable
|
||||
class GameState {
|
||||
var score = 0
|
||||
var isPaused = false
|
||||
var lives = 3
|
||||
}
|
||||
|
||||
// Scene reads/writes the shared model
|
||||
class GameScene: SKScene {
|
||||
var gameState: GameState?
|
||||
|
||||
override func update(_ currentTime: TimeInterval) {
|
||||
guard let state = gameState, !state.isPaused else { return }
|
||||
// Game logic updates state.score, state.lives, etc.
|
||||
}
|
||||
}
|
||||
|
||||
// SwiftUI view owns the model
|
||||
struct GameContainerView: View {
|
||||
@State private var gameState = GameState()
|
||||
@State private var scene: GameScene = {
|
||||
let s = GameScene(size: CGSize(width: 1024, height: 768))
|
||||
s.scaleMode = .aspectFill
|
||||
return s
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Score: \(gameState.score)")
|
||||
SpriteView(scene: scene, isPaused: gameState.isPaused)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
.onAppear { scene.gameState = gameState }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key pattern**: Use `@Observable` model as bridge. Scene mutates it; SwiftUI observes changes. Avoid recreating scenes in view body — use `@State` to persist the scene instance.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
3
.claude/skills/axiom-spritekit-ref/agents/openai.yaml
Normal file
3
.claude/skills/axiom-spritekit-ref/agents/openai.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "SpriteKit Reference"
|
||||
short_description: "SpriteKit API reference"
|
||||
Reference in New Issue
Block a user