Files

111 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Game Feel & Juice (screenshake, hitstop, tweening/easing, particles, squash & stretch, camera, SFX layering)
The canon: Steve Swink's *Game Feel*, Vlambeer/"Juice it or lose it" (Martin Jonasson & Petri Purho), and Jan Willem Nijman's "The art of screenshake" (see Sources). "Juice" = generous, layered, non-gameplay-affecting feedback that makes every input feel physical and satisfying. Focus: what an AI builder should reflexively add so games feel **alive instead of static**, without hurting readability or perf.
---
## 1. The core principle
**Every meaningful action should produce disproportionate, multi-sensory feedback.** A single "hit" should simultaneously trigger several of: sound, particles, screenshake, hitstop, flash, knockback, squash/stretch, and a number/pop. The gameplay math can stay simple — the *feel* comes from the feedback layer stacked on top. The classic demo takes the same identical Breakout and makes it feel amazing purely by adding these layers.
**Rule:** Separate "simulation" from "presentation." Feedback (shake, tween, particles, flash) must **never change gameplay outcomes** — it's cosmetic. This keeps physics deterministic (important for netcode/replays) while letting you pile on juice freely.
---
## 2. Screenshake (the highest impact-per-effort effect)
- Offset the **camera** (or a camera container), not the world objects. Apply a random offset each frame from a **trauma value** that **decays over time**.
- **Use trauma², not linear** (Squirrel Eiserloh's "Juicing Your Cameras"): store `trauma` in [0,1], set `shake = trauma * trauma` (or `trauma³`). Add trauma on events (small hit +0.3, explosion +0.8), decay `trauma -= decay * dt` each frame. Squaring makes small hits subtle and big hits explosive.
- Offset with **noise, not pure random**, for smoother shake: `offsetX = maxOffset * shake * (noise(t) )`; also shake **rotation** slightly (`maxAngle * shake * noise`) — rotational shake reads as more violent.
- **Directional shake** for directional hits (kick the camera opposite the impact) reads better than omnidirectional for melee/recoil.
- **Cap it & scale it down for accessibility.** Excessive shake causes motion sickness — expose a "screen shake" slider/toggle. Never shake so hard UI/targets become unreadable.
- Scale offsets by `dt`-independent amount but decay by `dt` so it's frame-rate independent.
Nijman's rules also include: **more bullets, bigger bullets, muzzle flash, impact effects, permanent decals, camera lerp/lookahead, camera kick, sleep/hitstop, gun delay, knockback, screen shake, and even a little randomness in everything.**
---
## 3. Hitstop / hitstun / "sleep" (freeze frames on impact)
- On a big hit, **freeze the game (or just the two involved entities) for a few frames** (~30120ms), then resume. This sells impact enormously — the brain reads the pause as force.
- Implement as a global **time scale** or a short "freeze frames" counter: while frozen, skip the gameplay update (or set `timeScale=0`) but **keep rendering** and keep the flash/particles visible. Common: freeze ~26 frames for normal hits, longer for finishers.
- Keep hitstop **short**; too long feels laggy/unresponsive. Often paired with a brief hit **flash** (tint the sprite white for 12 frames) and knockback that starts *after* the freeze.
- Distinguish **hitstop** (brief freeze for feel) from **hitstun** (gameplay state where a hit character can't act) — the latter is real gameplay, the former is pure juice.
---
## 4. Tweening & easing (motion that isn't linear)
- **Almost nothing should move linearly.** Real, satisfying motion accelerates/decelerates. Use **easing functions** for UI transitions, pickups flying to the HUD, menus, popups, camera moves, damage numbers.
- Common curves (see easings.net for exact formulas/graphs):
- **easeOutQuad/Cubic** — arrive gently (great for things settling into place).
- **easeInQuad/Cubic** — start slow (things launching).
- **easeOutBack** — slight overshoot then settle (pop-in for UI/pickups — very juicy).
- **easeOutElastic / easeOutBounce** — springy/bouncy (buttons, coin pickups). Use sparingly.
- **easeInOutQuad** — smooth both ends (camera, generic).
- **Use a tween library** rather than hand-rolling: **GSAP** (best-in-class, huge easing set, timelines), **@tweenjs/tween.js** (lightweight, common in Three.js), or the engine's built-in (Phaser `this.tweens.add`). Tweens compose (chains, delays, yoyo, repeat) and auto-handle dt.
- **Kill/clean up tweens** when the target is destroyed or the scene shuts down (leaked tweens mutating dead objects = crashes/bugs). Phaser: kill on `SHUTDOWN`; GSAP: `.kill()`.
- A **spring/damped-lerp** (`x += (target - x) * (1 - exp(-k*dt))`) is a great procedural alternative for continuous following (cameras, cursors, UI) — always frame-rate correct if you use the `exp` form rather than `x += (target-x)*0.1` (the naive lerp is frame-rate dependent — see §6).
---
## 5. Particles & squash-and-stretch
- **Particles everywhere:** dust on landing/footsteps, sparks on hit, debris on death, trails on projectiles, confetti on win, ambient motes. They cost little and add enormous life. Use the engine's particle system (Phaser `add.particles`, Three.js points/instanced sprites) and **pool** them (no per-emit allocation — see `threejs-foundational.md`).
- **Squash & stretch** (Disney's foundational animation principle): deform on acceleration to convey weight/speed while preserving apparent volume.
- Jump: stretch vertically (taller/thinner) on takeoff, squash (shorter/wider) on landing, then spring back via `easeOutBack`/elastic.
- Preserve volume: if you scale `y` by `s`, scale `x` by ~`1/s`, so it doesn't look like it's growing.
- Drive it from velocity (stretch along the movement direction) or trigger on discrete events (land, hit, shoot recoil).
- **Anticipation + follow-through:** wind up before a big action (tiny reverse/squash) and overshoot after — even a couple frames sells it.
- **Pop numbers & flashes:** floating damage numbers that rise + fade + scale-pop, combo counters, "+10" pickups. Cheap, huge readability + satisfaction win.
---
## 6. Camera feel (lerp, lookahead, deadzone, punch)
- **Never hard-snap the camera to the player.** Smoothly follow (`lerp`/spring) toward the target so motion is fluid.
- **Frame-rate-correct smoothing:** `pos += (target - pos) * (1 - Math.exp(-k * dt))`. The naive `pos += (target - pos) * 0.1` is **frame-rate dependent** (faster on 144Hz than 60Hz) — a common bug; use the exp form or a fixed-timestep camera update.
- **Lookahead:** offset the camera slightly in the direction of movement / where the player is aiming, so the player sees more of what's ahead. Ease the offset in/out.
- **Deadzone / soft zone:** don't move the camera until the player leaves a central box — prevents nauseating micro-jitter from small movements.
- **Camera punch/kick** on impacts (a quick zoom-in or positional kick that eases back) complements screenshake.
- Clamp the camera to level bounds; round the final camera position to whole pixels for pixel-art games to avoid shimmer.
---
## 7. SFX layering & audio feel
- **Layer sounds** for richness: a single "shoot" = a low thump + a mid body + a high click/transient. Impacts = hit + debris + a tail.
- **Pitch-randomize** repeated sounds (±515%) so rapid fire / footsteps don't sound robotic (`playbackRate` / Howler `rate`). Slightly randomize volume too.
- Sound is feedback: **every** action should have audio, and it should be tight (low latency) and satisfy on first frame of the action (see the audio skill for Web Audio/Howler, mobile unlock, buses).
- Duck music briefly under big events; add a subtle low-frequency "boom" on explosions.
---
## 8. Don't over-juice (readability & perf guardrails)
- **Juice must not obscure gameplay.** If shake/flash/particles hide the player, enemies, or projectiles, dial back. Readability > spectacle.
- **Accessibility:** provide toggles/sliders for screen shake, flashing, and reduced motion (respect `prefers-reduced-motion`). Heavy flashing risks photosensitivity — avoid rapid full-screen strobing.
- **Perf:** pool particles/tweens, cap concurrent particles, don't allocate in the hit path. Juice is cheap but "thousands of unpooled particles per explosion" is not.
- Keep hitstop short and prediction-safe in multiplayer (apply juice on the client only; never let it alter the authoritative sim).
---
## Defaults to apply
- **Auto-juice defaults:** whenever the builder generates a hit/pickup/death/land/win event, reflexively attach the stack: **sound + particles + short hitstop + white flash + screenshake(trauma²) + easeOutBack pop / squash-stretch + floating number.** Same simple gameplay, dramatically better feel — this is the single biggest perceived-quality lever.
- **Ship a tiny reusable "juice" toolkit** in generated games: a `trauma`-based shake (squared, noise-driven, decaying), a `hitstop(frames)` helper (timeScale/freeze-count), a `flash(sprite)` helper, easing/tween helpers (or GSAP/tween.js), and frame-rate-correct camera follow with lerp + lookahead + deadzone.
- **Always use the `exp`-based lerp** for cameras/followers so smoothing is frame-rate independent — bake this in to avoid the classic 144Hz-vs-60Hz bug.
- **Separate presentation from simulation** so juice never affects gameplay/netcode. Provide screenshake/flash/reduced-motion toggles by default for accessibility.
---
## Sources
- Martin Jonasson & Petri Purho — "Juice it or lose it" (GDC talk): https://www.youtube.com/watch?v=Fy0aCDmgnxg ; companion "Game feel" write-up on grapefrukt.
- Jan Willem Nijman (Vlambeer) — "The art of screenshake": https://www.youtube.com/watch?v=AJdEqssNZ-U
- Steve Swink — *Game Feel: A Game Designer's Guide to Virtual Sensation* (book), gamefeelbook.com.
- Squirrel Eiserloh — "Juicing Your Cameras With Math" (GDC, trauma² screenshake): https://www.youtube.com/watch?v=tu-Qe66AvtY
- easings.net — easing function reference with formulas & graphs: https://easings.net/
- Robert Penner's easing equations (origin of the standard easings): http://robertpenner.com/easing/
- GSAP docs (tweening/easing/timelines): https://gsap.com/docs/v3/ ; tween.js: https://github.com/tweenjs/tween.js
- Disney's 12 principles of animation (squash & stretch, anticipation, follow-through): https://en.wikipedia.org/wiki/Twelve_basic_principles_of_animation
- MDN — `prefers-reduced-motion`: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
- "The Juice Factor" / GDC Vault talks on feedback & game feel.