# Chapter 10: Game feel and juice (/guides/handmade-web-games/techniques-for-game-feel-and-juice)





## Juice? [#juice]

**Juice** is the feedback that makes a game feel alive and satisfying to play. It's often a variety of techniques, like visual cues, sound effects, and haptic feedback. Done well, juice makes your game more immersive, interesting, and intuitive. Remember to not overdo it!

<PongJuiceCompareDemo />

In this chapter, we'll introduce particle effects, screen shake, and bullet time—all parts of our juiced pong demo above.

## Visual feedback [#visual-feedback]

### Anticipation [#anticipation]

(todo)

### Squash and stretch on impact [#squash-and-stretch-on-impact]

Squash & stretch is the first of Disney's [12 principles of animation](https://en.wikipedia.org/wiki/Twelve_basic_principles_of_animation) <small className="opacity-75">\[Wikipedia]</small>, and it's an effective way to breathe some life into otherwise lifeless objects. Despite our "character" here being nothing but a blue square, a very simple squash & stretch effect gives it a physical presence and liveliness. We'll expand on the technique introduced in [Chapter 4](/guides/handmade-web-games/animation#ch-0-7-squash-and-stretch) below.

<SquashStretchDemo />

<details>
  <summary>
    <strong>Code for squash and stretch</strong>
  </summary>

  To apply squash and stretch we'll extend the player state with scale factors:

  ```ts twoslash
  const state = {
    player: {
      x: 200, // center of player X coord
      vx: 0, // x velocity
      scaleX: 1, // [!code ++]
      scaleY: 1, // [!code ++]
    },
    walls: [22, 354],
  };
  ```

  On impact, set the scale from the impact speed, then ease back toward `1` each tick with the exponential decay function from [Chapter 4](/guides/handmade-web-games/animation#exponential-smoothing):

  ````ts twoslash
  const state = {
    player: {
      x: 188,
      vx: 0,
      scaleX: 1,
      scaleY: 1,
    },
    walls: [22, 354],
  };

  type State = typeof state;

  /**
   * ```ts
   * function expDecay(current, target, t) {
   *   return current * Math.exp(-t) + target * (1 - Math.exp(-t));
   * }
   * ```
   */
  declare function expDecay(current: number, target: number, t: number): number;

  /**
   * ```ts
   * function squashFromSpeed(speed) {
   *   const maximumSquash = 0.5;
   *   const impactSpeedScale = 280;
   *   return maximumSquash * (1 - Math.exp(-speed / impactSpeedScale));
   * }
   * ```
   */
  declare function squashFromSpeed(speed: number): number;

  /**
   * ```ts
   * function calculateCollision(state) {
   *   const playerHalfWidth = 18;
   *   const minImpactSpeed = 25;
   *   const { player, walls } = state;
   *   const [leftWall, rightWall] = walls;
   *
   *   if (player.x - playerHalfWidth < leftWall) {
   *     const impactSpeed = -player.vx;
   *     player.x = leftWall + playerHalfWidth;
   *     player.vx = 0;
   *     return impactSpeed >= minImpactSpeed ? impactSpeed : 0;
   *   }
   *
   *   if (player.x + playerHalfWidth > rightWall) {
   *     const impactSpeed = player.vx;
   *     player.x = rightWall - playerHalfWidth;
   *     player.vx = 0;
   *     return impactSpeed >= minImpactSpeed ? impactSpeed : 0;
   *   }
   *
   *   return 0;
   * }
   * ```
   */
  declare function calculateCollision(state: State): number;
  // ---cut---
  function update(dt: number) {
    const { player } = state;
    const impactSpeed = calculateCollision(state);

    if (impactSpeed > 0) {
      const squashAmount = squashFromSpeed(impactSpeed);
      player.scaleX = 1 - squashAmount; // [!code ++]
      player.scaleY = 1 + squashAmount; // [!code ++]
    }

    const decayRate = 8;
    player.scaleX = expDecay(player.scaleX, 1, dt * decayRate); // [!code ++]
    player.scaleY = expDecay(player.scaleY, 1, dt * decayRate); // [!code ++]
  }
  ````

  Finally, draw using the scaled size and ensure that the player stays flush against each wall while squashed.

  ```ts twoslash
  const playerSize = 36;
  const groundY = 132;

  const state = {
    player: {
      x: 188,
      vx: 0,
      scaleX: 1,
      scaleY: 1,
    },
    walls: [22, 354] as [number, number],
  };

  type State = typeof state;

  declare function drawBackground(ctx: CanvasRenderingContext2D): void;
  declare function drawWalls(ctx: CanvasRenderingContext2D, walls: [number, number]): void;
  // ---cut---
  function draw(ctx: CanvasRenderingContext2D) {
    drawBackground(ctx);
    drawWalls(ctx, state.walls);
    drawPlayer(ctx, state);
  }

  function drawPlayer(ctx: CanvasRenderingContext2D, state: State) {
    const { player, walls } = state;
    const [leftWall, rightWall] = walls;
    const drawW = playerSize * player.scaleX;
    const drawH = playerSize * player.scaleY;

    // compute whether the player is pressed against a wall
    const half = playerSize / 2;
    const isOnLeft = player.x === leftWall + half;
    const isOnRight = player.x === rightWall - half;

    // offset the draw so that the player hugs the wall
    let offsetX = 0;
    if (isOnLeft) offsetX = (drawW - playerSize) / 2;
    if (isOnRight) offsetX = (playerSize - drawW) / 2;

    const playerLeftX = player.x - drawW / 2 + offsetX;
    ctx.fillRect(playerLeftX, groundY - drawH, drawW, drawH + 1);
  }
  ```
</details>

### Screen shake [#screen-shake]

In [Chapter 6](/guides/handmade-web-games/cameras-and-viewports), we showed that adding screen shake is as simple as adding a few transforms when rendering your game. There are two main techniques for screen shake.

One is to bump the camera in a single direction, then ease back to its true position.

<ScreenBumpDemo />

<details>
  <summary>
    <strong>Code for camera bump</strong>
  </summary>

  To do this we'll extend our camera state to include `x`/`y` offsets for the camera bump:

  ```ts twoslash
  const state = {
    camera: {
      x: 0,
      y: 0,
      zoom: 1,
      bump: { x: 0, y: 0 }, // [!code ++]
    },
  };
  ```

  Then bumping the camera is as simple as incrementing those offsets:

  ```ts twoslash
  const state = {
    camera: {
      x: 0,
      y: 0,
      zoom: 1,
      bump: { x: 0, y: 0 },
    },
  };
  // ---cut---
  function bumpCamera(impulseX: number, impulseY: number) {
    state.camera.bump.x += impulseX;
    state.camera.bump.y += impulseY;
  }
  ```

  Each tick, we will ease the camera back to its starting position using the exponential decay function from [Chapter 4](/guides/handmade-web-games/animation#exponential-smoothing).

  ```ts twoslash
  function expDecay(current: number, target: number, t: number) {
    return current * Math.exp(-t) + target * (1 - Math.exp(-t));
  }

  const state = {
    camera: {
      x: 0,
      y: 0,
      zoom: 1,
      bump: { x: 0, y: 0 },
    },
  };
  // ---cut---
  function update(dt: number) {
    const decayRate = 8; // (you can tune this)
    state.camera.bump.x = expDecay(state.camera.bump.x, 0, dt * decayRate);
    state.camera.bump.y = expDecay(state.camera.bump.y, 0, dt * decayRate);
    //                             ^ from               ^ to    ^ how fast
  }
  ```

  Finally, actually apply the offset when handling the camera position translation:

  ```ts twoslash
  function inCamera(
    ctx: CanvasRenderingContext2D,
    bounds: { width: number; height: number },
    camera: {
      x: number;
      y: number;
      zoom: number;
      bump: { x: number; y: number };
    },
    draw: () => void,
  ) {
    ctx.save();
    ctx.translate(bounds.width / 2, bounds.height / 2);
    ctx.scale(camera.zoom, camera.zoom);
    ctx.translate(
      -(camera.x + camera.bump.x), // [!code ++]
      -(camera.y + camera.bump.y), // [!code ++]
    );
    draw();
    ctx.restore();
  }
  ```

  That's it for the first type of screen shake!
</details>

That first type of screen shake works well for directional impacts, but lacks *heaviness*. We can add more weight to it. The other type we'll look at rumbles the entire view over a period of time. For things like explosions where directionality matters less than the overall impact, it's a great way to add juice. (The two approaches can of course be combined for a directional bump with some aftershock rumbling.)

<ScreenShakeDemo />

<details>
  <summary>
    <strong>Code for rumble shake</strong>
  </summary>

  Like before, we'll start by defining the state for this screen shake:

  ```ts twoslash
  const state = {
    time: 0, // [!code ++]
    camera: {
      x: 0,
      y: 0,
      zoom: 1,
      // [!code ++]
      shake: {
        factor: 0, // [!code ++]
      }, // [!code ++]
    },
  };
  ```

  Then increment the `state.camera.shake.factor` whenever you want to trigger screen shake, and decay each tick like before.

  ```ts twoslash
  function expDecay(current: number, target: number, t: number) {
    return current * Math.exp(-t) + target * (1 - Math.exp(-t));
  }

  const state = {
    time: 0,
    camera: {
      x: 0,
      y: 0,
      zoom: 1,
      shake: {
        factor: 0,
      },
    },
  };
  // ---cut---
  function shakeCamera(strength: number) {
    state.camera.shake.factor += strength;
  }

  function update(dt: number) {
    const decayRate = 8;
    state.time += dt;
    state.camera.shake.factor = expDecay(state.camera.shake.factor, 0, dt * decayRate);
  }
  ```

  `state.time` will be the clock we use as the input for the sine and cosine waves that create the shake effect—just like you saw in [Chapter 5](/guides/handmade-web-games/math-for-games#sine-waves).

  <Callout type="info">
    The `52` and `56` below are arbitrary frequency multipliers that tune how fast the view jitters on
    each axis. The numbers need to be close enough that the motion isn't noticeably faster in one
    direction, but not so close that they start forming circles.
  </Callout>

  ```ts twoslash
  function inCamera(
    ctx: CanvasRenderingContext2D,
    bounds: { width: number; height: number },
    camera: {
      x: number;
      y: number;
      zoom: number;
      shake: { factor: number };
    },
    time: number,
    draw: () => void,
  ) {
    const shakeX = Math.sin(time * 52) * camera.shake.factor; // [!code ++]
    const shakeY = Math.cos(time * 56) * camera.shake.factor; // [!code ++]

    ctx.save();
    ctx.translate(bounds.width / 2, bounds.height / 2);
    ctx.scale(camera.zoom, camera.zoom);
    ctx.translate(
      -(camera.x + shakeX), // [!code ++]
      -(camera.y + shakeY), // [!code ++]
    );
    draw();
    ctx.restore();
  }
  ```
</details>

### Freeze on impact [#freeze-on-impact]

(todo: example code adding simulated pause time to the accumulator)

### Particles [#particles]

Particle effects are another great way to add juice to your games. Impact sparks, smoke, tire dust, muzzle flashes, blood splats, water spray, jump clouds, explosions, fire, vehicle exhaust, and ambient weather like rain or snowfall can all be implemented with particle emitters. So can fire and smoke emissions from a rocket:

<Ld59RocketDemo />

<Callout type="info">
  This is the code that got me excited about particle effects. It's adapted from [The
  Transmitter](https://transmitter.onsclom.net/), a submission to the [Ludum Dare
  59](https://ldjam.com/events/ludum-dare/59/games/overall/compo) game jam by
  [@onsclom](https://www.onsclom.net/), who helped tremendously with this guide.
</Callout>

The basic technique is to start with some state similar to our bouncing ball from [Chapter 4](/guides/handmade-web-games/animation#a-bouncing-ball).

```ts
const particle = {
  x: 0,
  y: 0,
  vx: 0,
  vy: 0,
  age: 0,
  life: 0,
  // plus any other properties you want...
  color: "orange",
  fromSize: 10,
  toSize: 0,
  fromOpacity: 1,
  toOpacity: 0,
};
```

The main new concepts here are `age` and `life`.

* `age` ticks up with `dt` once per update
* `life` is how long the particle lasts
* when `age > life` we skip updating and rendering
* any `from*` and `to*` values are `lerp`ed using `t = age / life`

The state from this particle provides settings for size and opacity changes over the particle's life, but you can add anything you want to it. Maybe you want to be able to transition the colors, add rotation speed, use a non-linear easing curve—when you define your own particle system, you decide exactly what goes into it.

Using this state, let's spawn some particles.

<Breakout>
  <Scrollycoding preview="canvas2d">
    <slot>
      <>      </>
    </slot>

    <slot path="steps">
      <>
        We'll start by emitting just one particle on an interval from the center of the canvas. This code should look familiar if you followed [Chapter 4's bouncing ball demo](/guides/handmade-web-games/animation#a-bouncing-ball).

        You'll notice we're using the variable frame-rate `update` function rather than `fixedUpdate` since particles tend to be used for visual flare rather than for game logic that requires fixed time steps. This means we can avoid updating the particles' positions more frequently than they're rendered.
      </>
    </slot>

    <slot path="steps">
      Next we'll add a few properties to allow us to transition the size and opacity of the particle over the course of its `life`. Here we're animating the size and opacity, but you can update any properties you want or even derive them without adding state.
    </slot>

    <slot path="steps">
      <>
        Because they're small and ubiquitous, it's easy to end up with thousands of particles on screen. Assuming updates run at 120fps, that could involve hundreds of thousands of object allocations and dynamic array resizes per second. That can be a problem when the browser decides it is time to run the [garbage collector](https://en.wikipedia.org/wiki/Garbage_collection_\(computer_science\)) <small className="opacity-75">\[Wikipedia]</small>. If it takes too long to collect and free unused objects, you may drop a frame or two periodically.

        We'll mitigate this issue by creating our particles up front. The idea is to preallocate one big array for all the particles you could ever need, and generate the objects in it just once. This approach, called a ring buffer or [circular buffer](https://en.wikipedia.org/wiki/Circular_buffer) <small className="opacity-75">\[Wikipedia]</small>, is an optimization. So far in this guide we've been fairly oblivious to performance concerns, but when you might be spawning thousands of particles 120 times per second, particle emitter code is likely to be the hottest loop in your game code. The ring buffer reduces potential dropped frames that can happen during garbage collection by preventing said "garbage" from being created in the first place. Operations like `.filter` or `.map` and building new objects all allocate new memory that needs to be cleaned up by the GC. By making a fixed-size array with all the particle objects in it up front we eliminate that work for the GC entirely.

        <details>
          <summary>
            <strong>
              Is this premature optimization?
            </strong>
          </summary>

          Our [benchmark](https://particle-benchmark.spud.gg/) aims to answer that question.

          In my browser, the benchmark shows that a ring buffer ensures relatively stable `update` times whereas a `push` + `filter` approach occasionally slows down during GC—at least, for very high particle counts. Most small games don't need the number of particles required to really see the difference here, though. That being said, the more memory your game uses, the slower garbage collection gets. So what might be negligible GC time when you're starting out your project could result in many dropped frames as your code grows in complexity later on. Tracing sources of slow GC is arguably way harder than using a ring buffer, so I'd say that this is a worthwhile optimization to include.

          I will note two minor inconveniences with the ring buffer approach, though: (1) you need to estimate a reasonable upper bound on the total particles up-front, and (2) you need to clear out stale properties when you initialize a new particle.

          If you'd like to learn more about how V8's Orinoco garbage collector works, [Playing with Garbage](https://www.youtube.com/watch?v=easvMCCBFkQ) <small className="opacity-75">\[YouTube]</small> by SimonDev provides a great rundown.
        </details>
      </>
    </slot>

    <slot path="steps">
      The `"lighter"` blend mode is often used for fire and sparks since it makes overlapping particles brighter. We'll increase the number of particles on screen to highlight the effect.
    </slot>
  </Scrollycoding>
</Breakout>

## Game feel [#game-feel]

(todo)

* input buffering
* coyote time

## Haptic feedback [#haptic-feedback]

If you've added controller support, then you can deliver touch feedback through controller haptics. (And if you haven't yet added controller support, you should! It's fun and easy—see [Chapter 8](/guides/handmade-web-games/controllers-and-haptics).) Haptics provide a low-effort way to increase the immersiveness of your game through tactile feedback.

## Audio feedback [#audio-feedback]

Some basic tips when providing audio feedback in games:

* Randomize the pitch or speed of repetitive sounds, or loop through several audio samples. (Or both!)
* If you have overlapping sounds, run them through a compressor so the total volume doesn't grow too high.
* Play sounds for state changes and collisions, especially when triggered by the player. Even hover effects deserve subtle audio cues.

<UpNext>
  In [Chapter 11 →](/guides/handmade-web-games/offline-mode) we'll look at making your web games
  installable so that they can be played offline.
</UpNext>

<SeeAlso>
  * [Celeste & Forgiveness](https://www.maddymakesgames.com/articles/celeste_and_forgiveness/index.html) by Maddy Thorson
</SeeAlso>

[https://garden.bradwoods.io/notes/design/juice](https://garden.bradwoods.io/notes/design/juice)

[https://www.youtube.com/watch?v=Fy0aCDmgnxg](https://www.youtube.com/watch?v=Fy0aCDmgnxg)

[https://www.youtube.com/watch?v=AJdEqssNZ-U](https://www.youtube.com/watch?v=AJdEqssNZ-U)

in some other chapter—tile maps: [https://developer.mozilla.org/en-US/docs/Games/Techniques/Tilemaps](https://developer.mozilla.org/en-US/docs/Games/Techniques/Tilemaps)
