Wasd and Qe

How to 3D

Chapter 7: Camera and Terrain

Wasd and Qe

Many games let the player move the camera with W, A, S, D, or the cursor keys. Some let the player yaw the camera with other keys, like Q and E. To achieve this in our renderers, we need a key listener that looks like this:

function onKeyDown(event: KeyboardEvent) {
  const key = event.key;
  // TODO: respond to key
  render();
}
function onKeyDown(event: KeyboardEvent) {
  const key = event.key;
  // TODO: respond to key
  render();
}

This callback must be registered with the browser like this:

function initialize() {
  // ...
  window.addEventListener('keydown', onKeyDown);
}
function initialize() {
  // ...
  window.addEventListener('keydown', onKeyDown);
}

Inside the callback, we consider which key was pressed and call an appropriate method of the camera class.

This callback has been added to the first-person grid-walker below. Try it out. Before pressing the keys, click on the canvas to give it keyboard focus.

There are no Easter eggs hidden in this renderer. You'll find nothing but a grid in all directions. As you look around, you might notice that the movement is choppy. The severity of the choppiness depends on your operating system's key repeat rate. If the key repeat rate is fast, then new keydown events will be generated more quickly and the movement will be smoother.

We do not want to force our users to change the system-wide repeat rate in order to get smooth rendering. Smoothness can be achieved in other ways. For example, we could switch to continuous rendering using the requestAnimationFrame function that we've used previously:

let then: DOMHighResTimeStamp | null = null;

function animate(now: DOMHighResTimeStamp) {
  const elapsed = then ? now - then : 0;
  // update animated state
  render();
  requestAnimationFrame(animate);
  then = now;
}
let then: DOMHighResTimeStamp | null = null;

function animate(now: DOMHighResTimeStamp) {
  const elapsed = then ? now - then : 0;
  // update animated state
  render();
  requestAnimationFrame(animate);
  then = now;
}

The animate method must update any state that changes over time. For smooth camera motion, we update the camera based on which keys are currently held down. The browser doesn't offer a way to query the state of a key, so we'll have to track their state manually through event listeners. These TypeScript functions track the key state in three global variables:

// These will be -1, 0, or 1.
let horizontal = 0;
let vertical = 0;
let turn = 0;

function onKeyDown(event: KeyboardEvent) {
  const key = event.key;
  if (key === 'w' || key === 'ArrowUp') {
    vertical = 1;
  } else if (key === 's' || key === 'ArrowDown') {
    vertical = -1;
  } else if (key === 'a' || key === 'ArrowLeft') {
    horizontal = -1;
  } else if (key === 'd' || key === 'ArrowRight') {
    horizontal = 1;
  } else if (key === 'q') {
    turn = 1;
  } else if (key === 'e') {
    turn = -1;
  }
  render();
}

function onKeyUp(event: KeyboardEvent) {
  const key = event.key;
  if (key === 'w' || key === 's' || key === 'ArrowUp') {
    vertical = 0;
  } else if (key === 'a' || key === 'd' || key === 'ArrowLeft') {
    horizontal = 0;
  } else if (key === 'q' || key === 'e') {
    turn = 0;
  }
}

// TODO: add event listeners
// These will be -1, 0, or 1.
let horizontal = 0;
let vertical = 0;
let turn = 0;

function onKeyDown(event: KeyboardEvent) {
  const key = event.key;
  if (key === 'w' || key === 'ArrowUp') {
    vertical = 1;
  } else if (key === 's' || key === 'ArrowDown') {
    vertical = -1;
  } else if (key === 'a' || key === 'ArrowLeft') {
    horizontal = -1;
  } else if (key === 'd' || key === 'ArrowRight') {
    horizontal = 1;
  } else if (key === 'q') {
    turn = 1;
  } else if (key === 'e') {
    turn = -1;
  }
  render();
}

function onKeyUp(event: KeyboardEvent) {
  const key = event.key;
  if (key === 'w' || key === 's' || key === 'ArrowUp') {
    vertical = 0;
  } else if (key === 'a' || key === 'd' || key === 'ArrowLeft') {
    horizontal = 0;
  } else if (key === 'q' || key === 'e') {
    turn = 0;
  }
}

// TODO: add event listeners

We inspect the three key-state variables in the animate callback and update the camera accordingly. To ensure the changes aren't dependent on the speed of the computer and browser, we temper the amount of movement and rotation by how much time has actually passed since the last frame.

Try moving and turning the camera in this renderer that uses continuous rendering to smooth out the camera changes:

← Rotate AroundLooking with the Mouse →