| name | game-feel |
| description | Make iOS apps and games FEEL responsive, satisfying, alive — haptics, particle/spring animation, sound feedback, screen shake, celebrations, and SpriteKit (iOS 26 / Swift 6). Use for: "game feel", "juice it up", "make it feel good / punchy", "add haptics", Core Haptics / CHHapticEngine / AHAP, sensoryFeedback, "add confetti", "celebrate achievement", "particle effect", "screen shake", "hit pause", "build a 2D game", SpriteKit / SKScene / SKAction / SpriteView, SwiftUI Metal shader effects (colorEffect / distortionEffect / layerEffect), ripple / shimmer, audio-reactive visualizer, error shake, GCController, Game Center / GameKit, "my app feels flat". HIG layout → apple-hig; general SwiftUI screens → swiftui-ui. |
Game Feel
The difference between an app that works and an app that feels alive is game
feel — the moment-to-moment tactile, visual, and audible response to every
interaction. A button that bounces, taps that you can feel, a milestone that
erupts in confetti, an enemy that flashes and the screen that kicks back when you
hit it. This skill is the craft of "juice": adding feedback that makes
interactions satisfying without making them slower or less accessible.
It covers three feedback channels that fire together — haptics (what you
feel), animation/juice (what you see), audio (what you hear) — plus the
SpriteKit 2D engine and reward systems (confetti, badges, celebrations) that
package them into game-like experiences. It applies to any iOS app, not just
games: a fitness app celebrating a streak, a finance app confirming a transfer, a
to-do app checking off a task all benefit from deliberate feel.
Phase 0 — Context & capability check (do this FIRST)
Before generating any feel code, establish the ground truth:
- Deployment target.
sensoryFeedback, @Observable, .symbolEffect,
spring presets need iOS 17+. MPNowPlayingSession, modern Photos picker need
iOS 16+. SpriteKit + SpriteView work iOS 13+. State the floor you target.
- Swift version. This skill targets Swift 6 strict concurrency / iOS 26. If
the project is older, note which patterns degrade gracefully.
- Platform & hardware. Haptics exist only on iPhone (Taptic Engine) — not
iPad, not Mac, not Simulator. Always gate with
CHHapticEngine.capabilitiesForHardware().supportsHaptics. macOS gets visual
juice + audio but no haptics.
- Accessibility is non-negotiable. Every animated celebration MUST honor
Reduce Motion (
@Environment(\.accessibilityReduceMotion) /
UIAccessibility.isReduceMotionEnabled) with an equally rewarding non-animated
fallback. Some users enable Reduce Motion for sensory reasons — also skip
sounds in that path.
- Existing code. Search before adding — don't duplicate a confetti view,
haptic manager, or game scene that already exists:
Glob: **/*Confetti*.swift, **/*Haptic*.swift, **/*Celebration*.swift, **/*Scene*.swift, **/*Emitter*.swift
Grep: "CAEmitterLayer", "CHHapticEngine", "UIImpactFeedbackGenerator", "SKScene", "sensoryFeedback"
If one exists, advise on improving it rather than generating a parallel system.
Triage mode — when the ask is "make this app feel better"
If the request is broad ("make it feel good / add polish") rather than a
specific moment, do not start coding. Inspect first, present a numbered
candidate table (# · Area · Opportunity · Suggested feel · Likely APIs · Effort), and let the user pick rows — unless they asked for direct
implementation. Read references/feel-judgment.md
before a broad pass.
The three pillars of feel
Feel comes from causality (feedback at the exact instant of the cause),
harmony (the three channels agreeing — a heavy impact = heavy haptic + low
thud + big shake), and restraint (feel reserved for moments that matter; if
everything is juicy, nothing is). Fire all three channels from the same code path
so they stay in sync.
Restraint is context-specific. Some contexts forbid delight —
finance/medical/legal, reading-heavy screens, many-times-per-session actions.
Shake the container, not the window; propose opt-out toggles. Full exclusion +
good-fit lists in references/feel-judgment.md.
INTERACTION (tap, hit, unlock, success)
│ fire all three at the SAME moment — never delayed
├──▶ HAPTIC what you feel (UIFeedbackGenerator / Core Haptics)
├──▶ VISUAL what you see (spring anim, particles, flash, shake)
└──▶ AUDIO what you hear (sound effect, AVAudioEngine, SKAction)
Pillar 1 — Haptics (what you feel)
The fastest, cheapest win in all of game feel. Three tiers, pick the lightest
that fits — all code + the full engine lifecycle in
references/haptics.md:
- Tier 1 —
UIFeedbackGenerator (UIKit, the 90% case). Pre-canned,
system-tuned, one line: UIImpactFeedbackGenerator (.light….rigid, call
prepare() ~0.1 s before), UISelectionFeedbackGenerator for detents,
UINotificationFeedbackGenerator for .success/.warning/.error outcomes.
- Tier 2 — SwiftUI
.sensoryFeedback (iOS 17+). Declarative, no generator
lifecycle: .sensoryFeedback(.success, trigger: didSave).
- Tier 3 — Core Haptics (
CHHapticEngine). Custom patterns + audio sync
(charging meter, textured rumble) — transient vs continuous events, dynamic
parameters, AHAP files, and the stoppedHandler/resetHandler recovery
handlers you MUST set.
Haptic design rules (WWDC "Practice audio haptic design"; expanded in
references/haptics.md): causality — fire at the
instant of the visual change, NEVER asyncAfter a haptic; harmony — match
weight to meaning (heavy object → .heavy/.rigid, success → .success);
utility — reserve for significant moments; gate on supportsHaptics,
respect Low Power Mode and the system "System Haptics" setting.
Pillar 2 — Visual juice (what you see)
Motion that exaggerates cause and effect. The toolbox, lightest → heaviest:
Spring animations — the single most important juice primitive. Overshoot +
settle reads as "alive". Use .spring(response:dampingFraction:): lower
dampingFraction = bouncier (0.5–0.6 for playful, 0.8 for subtle).
Symbol effects (iOS 17+) — instant juice on SF Symbols, zero layout work:
.symbolEffect(.bounce, value:) (.wiggle on iOS 18).
Squash & stretch on press — .scaleEffect(isPressed ? 0.94 : 1.0) + a
spring. Code for all three primitives (and flash & color pop) is in
references/view-layer-juice.md ("Juice
primitives").
Particles — confetti for celebrations (SwiftUI CAEmitterLayer,
references/celebrations.md) or SKEmitterNode for
in-scene effects (explosions, sparks, trails — see SpriteKit below).
Screen shake & hitstop — the heavy-impact pair: a brief randomized offset on
impact, and freezing the action 1–3 frames so a big hit lands harder. SpriteKit
game-loop versions in references/spritekit.md; the
SwiftUI/UIKit container-shake path (incl. the "wrong-password" shake,
constants, shake↔haptic pairing) in
references/view-layer-juice.md.
GPU fragment-shader effects (iOS 17+) — per-pixel feel nothing else can do:
tap ripple, shimmer sweep, RGB-split glitch, organic blobs, via
colorEffect/distortionEffect/layerEffect:
references/gpu-shaders.md.
Rhythm — primary instant, secondary staggered. The primary hit (squash +
haptic) is instant per causality, but a second visual layer staggered
~80–180 ms after reads as more alive — stagger visuals only, the haptic
never leaves the interaction frame
(references/view-layer-juice.md).
The Reduce Motion rule applies to ALL of the above: when on, replace springs
with .easeIn, skip particles/shake, show a static reveal. Never lose the
information (the badge still appears, the success still confirms) — only drop
the motion.
Pillar 3 — Audio feedback (what you hear)
Sound closes the feedback loop. Three options by weight (code in
references/haptics.md "Playing feedback sounds"):
AudioServicesPlaySystemSound (lightest, honors the silent switch),
SKAction.playSoundFileNamed (in-scene one-shot), AVAudioEngine (pitch
shift, mixing, layering).
Rules: keep effect files short (CAF/WAV, < a few seconds), respect the mute
switch for non-essential SFX, don't play sounds when Reduce Motion is on, and
sync the sound to the same trigger frame as the haptic and visual.
Reacting to audio (visuals driven by live sound — visualizer, beat-synced
game, voice-level ring): AVAudioEngine tap → vDSP FFT → mel bins + onset/BPM,
smoothed with the frame-rate-independent k = 1 - exp(-dt/tau) EMA. Pipeline, tau
table, and raw-vs-smoothed rule in
references/audio-reactive.md.
SpriteKit — the 2D game engine for feel
For anything genuinely game-like (a 2D game, an interactive animated scene, a
physics toy), SpriteKit is the native, battery-efficient choice. It renders a
tree of nodes (SKScene → SKSpriteNode/SKLabelNode/SKEmitterNode…) and
gives you actions, physics, particles, and a camera — exactly the primitives juice
is built from. Current for iOS 26 / Swift 6.3.
Scene setup rules (skeleton code in
references/spritekit.md): set
physicsWorld.contactDelegate in didMove(to:) — never in update() or after
a delay; use delta time in update() for frame-rate-independent movement.
Embed in SwiftUI with SpriteView — but store the scene in @State so a
re-render doesn't recreate it (the #1 SwiftUI+SpriteKit bug).
The juice primitives SpriteKit gives you — actions (SKAction.sequence/
.group/.repeatForever), physics (applyImpulse for snappy jumps), particles
(SKEmitterNode), camera (SKCameraNode for shake + follow), shaders
(SKShader), lighting. The full API walkthrough + game-loop patterns (screen
shake / hitstop / delta-time / object pooling), tile maps, atlases, transitions,
joints, and fields: references/spritekit.md.
Before shipping a SpriteKit scene, run the SpriteKit auditor — a structured
detector for the bugs that silently kill feel and frame rate (phantom collisions,
contacts that never fire, leaked scenes, node accumulation, draw-call waste,
time-step bombs): references/spritekit-auditor.md.
Game input & social layers. A real game also needs controller input
(references/game-controllers.md) and a social
layer (references/game-center-gamekit.md —
Game Center + GameKit multiplayer, with the auth-first/silent-failure gotcha).
Reward systems — confetti, badges, celebrations
The packaged payoff: when a user hits a milestone (10 workouts, a 7-day streak,
a level-up), erupt in a coordinated celebration — CAEmitterLayer confetti, a
spring badge reveal fired with .sensoryFeedback(.success), an @Observable
milestone tracker that celebrates only once per milestone (persisted), and
a Reduce-Motion fade-in fallback. The complete ready-to-drop implementation
(confetti for iOS and macOS, overlay, badge views + collection grid, haptic
manager, persistent tracker, the one-line .celebrateOnMilestone modifier) is
in references/celebrations.md.
Confetti performance gotcha: cap birthRate at 50–80 and use 3–5 distinct
particle shapes — 200+ particles drop frames on older chips. Stop emission after
the burst; never leave the emitter running.
Decision tree — what do I reach for?
I want to…
│
├── review a whole app for feel ("make it feel better")
│ → references/feel-judgment.md (triage table + when-NOT-to-add-feel guardrails)
│
├── add feel to a single interaction (tap / toggle / success / error)
│ → Haptic: .sensoryFeedback (iOS 17+) or UIFeedbackGenerator
│ → Visual: spring scaleEffect, or .symbolEffect on an SF Symbol
│ → Audio: AudioServicesPlaySystemSound (optional)
│
├── shake on error / reject (form field, card), NO SpriteKit → view-layer-juice.md
├── a GPU per-pixel effect (ripple, shimmer, glitch, blob) → gpu-shaders.md
├── visuals reacting to live audio (visualizer, beat-sync) → audio-reactive.md
│
├── an achievement / milestone / level-up
│ → references/celebrations.md (confetti + badge + overlay + haptic)
│ (report to Game Center too → references/game-center-gamekit.md)
│
├── a custom tactile pattern (charge-up, texture, audio-synced rumble)
│ → Core Haptics → references/haptics.md
│
├── a 2D game or interactive animated scene
│ → SpriteKit → references/spritekit.md (actions, physics, shake, hitstop,
│ emitters) · controllers → game-controllers.md · leaderboards/multiplayer
│ → game-center-gamekit.md · before shipping → spritekit-auditor.md
│
└── lock-screen / Now-Playing media controls, photo picker
→ broader "media & audio" surface; see references/haptics.md "Audio session"
notes, otherwise a media-specific skill
Anti-patterns (the things that kill feel)
The full 17-row anti-pattern/why/fix table is in
references/feel-judgment.md. Headlines: never
delay a haptic (asyncAfter breaks causality); don't haptic every
scroll/keystroke; always gate on supportsHaptics and Reduce Motion; cap
confetti birthRate at 50–80; fire all three channels from one trigger; store
SpriteView scenes in @State; named PhysicsCategory bitmasks; remove
spawned nodes; [weak self] in .repeatForever; honor the mute switch;
celebrate milestones once (persisted); no drama in high-trust flows; one haptic
per shake (not per oscillation); thumbstick spawns where the thumb lands;
authenticate Game Center before matchmaking.
Self-audit checklist
Before calling a feel implementation done, run the full checklist in
references/feel-judgment.md ("Self-audit
checklist") — it gates haptics (interaction-frame timing, supportsHaptics,
prepare(), recovery handlers), visuals (springs, squash, capped confetti),
audio (short SFX, mute switch, Reduce Motion), accessibility (no information
lost when motion is off, Low Power Mode), and SpriteKit (@State scene,
bitmasks, delta time, node cleanup, atlases, the auditor pass).
References
| File | Covers |
|---|
| haptics.md | Core Haptics engine lifecycle, transient vs continuous events, dynamic parameters & curves, AHAP, audio-synced patterns, HapticManager, capability/diagnostics, audio-session notes, feedback-sound options |
| spritekit.md | Scene lifecycle, nodes, actions, physics, touch, camera, particles, SwiftUI integration, mistakes; tile maps, atlases, transitions, game-loop / delta-time / screen-shake / hitstop / object pooling, audio, lighting, shaders, constraints, joints, fields, performance |
| spritekit-auditor.md | Structured bug detector: bitmask issues, contact-delegate gaps, leaked scenes, node accumulation, action leaks, coordinate confusion, draw-call waste, time-step bombs, health score, false-positive list |
| celebrations.md | CAEmitterLayer confetti (iOS + macOS), celebration overlay, badge views + collection grid, @Observable persistent milestone tracker, HapticManager, .celebrateOnMilestone — all Reduce-Motion aware |
| view-layer-juice.md | Non-SpriteKit juice: container shake (phaseAnimator/keyframeAnimator/CAKeyframeAnimation), wrong-password field shake, duration/amplitude/decay constants, shake↔haptic pairing, primary-instant/secondary-staggered rhythm, juice primitives |
| gpu-shaders.md | 3-modifier decision table, [[stitchable]]/SwiftUI_Metal.h skeleton + Swift→MSL arg mapping, WWDC24 tap-ripple, Inferno recipes (shimmer/water/RGB-split/swirl, MIT), MSL perf rules, shader-math toolkit (smooth-min, centered FBM, two-layer displacement, dual-time drift) |
| audio-reactive.md | AVAudioEngine tap → vDSP FFT → mel bins + onset/BPM → AudioFrame; frame-rate-independent k = 1 - exp(-dt/tau) EMA, tau table, raw-vs-smoothed accent rule |
| game-controllers.md | Menu button→UI map, thumbstick-follows-touch, press-under-thumb, brand-correct glyphs, GCController discovery + ContentUnavailableView prompt, keyboard-for-games, visionOS spatial controllers, require-or-not platform table |
| game-center-gamekit.md | Auth-first (silent failure), access point, achievement/leaderboard/challenge specs (1465×767 crop, recent-score-not-best), real-time vs turn-based multiplayer (reliable/unreliable + Codable envelope, expectedPlayerCount == 0, saveCurrentTurn), sandbox separation, trap + terminology tables |
| feel-judgment.md | Triage workflow (audit checklist, candidate table, strong/weak heuristics, effort bands), "when NOT to add feel" guardrails (high-trust/reading-heavy/repeated, container-not-window, opt-out toggles), Motion.animate Reduce-Motion helper, anti-pattern table, self-audit checklist |