Quick Tip: How to Make a Game Loop in JavaScript

716 阅读4分钟
原文链接: www.sitepoint.com

This article was peer reviewed by Andrew Ray and Sebastian Seitz. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

The “game loop” is a name given to a technique used to render animations and games with changing state over time. At its heart is a function that runs as many times as possible, taking user input, updating the state for the elapsed time, and then drawing the frame.

In this short article you’ll learn how this fundamental technique works and you’ll be able to start making your own browser based games and animations.

Here’s what game loop in JavaScript looks like:

function update(progress) {
  // Update the state of the world for the elapsed time since last render
}

function draw() {
  // Draw the state of the world
}

function loop(timestamp) {
  var progress = timestamp - lastRender

  update(progress)
  draw()

  lastRender = timestamp
  window.requestAnimationFrame(loop)
}
var lastRender = 0
window.requestAnimationFrame(loop)

The requestAnimationFrame method requests that the browser call a specified function as soon as it can before the next repaint occurs. It’s an API specifically for rendering animations but you can also use setTimeout with a short timeout for a similar result. requestAnimationFrame is passed a timestamp of when the callback started firing, it contains the number of milliseconds since the window loaded and is equal to performance.now().

The progress value, or time between renders is crucial for creating smooth animations. By using it to adjust the x and y positions in our update function, we ensure our animations move at a consistent speed.

Updating the Position

Our first animation will be super simple. A red square that moves to the right until it reaches the edge of the canvas and loops back around to the start.

We’ll need to store the square’s position and increment the x position in our update function. When we hit a boundary we can subtract the canvas width to loop back around.

var width = 800
var height = 200

var state = {
  x: width / 2,
  y: height / 2
}

function update(progress) {
  state.x += progress

  if (state.x > width) {
    state.x -= width
  }
}

Drawing the New Frame

This example uses the element for rendering the graphics but the game loop can be used with other outputs like HTML or SVG documents too.

The draw function simply renders the current state of the world. On each frame we’ll clear the canvas and then draw a 10px red square with its center at the position stored in our state object.

var canvas = document.getElementById("canvas")
var width = canvas.width
var height = canvas.height
var ctx = canvas.getContext("2d")
ctx.fillStyle = "red"

function draw() {
  ctx.clearRect(0, 0, width, height)

  ctx.fillRect(state.x - 5, state.y - 5, 10, 10)
}

And we have movement!

See the Pen Game Loop in JavaScript: Basic Movement by SitePoint (@SitePoint) on CodePen.

Note: In the demo you might notice that the size of the canvas has been set in both the CSS and via width and height attributes on the HTML element. The CSS styles set the actual size of the canvas element that will be drawn to the page, the HTML attributes set the size of the coordinate system or ‘grid’ that the canvas API will use. See this Stack Overflow question for more information.

Next we’ll get keyboard input to control the position of our object, state.pressedKeys will keep track of which keys are pressed.

var state = {
  x: (width / 2),
  y: (height / 2),
  pressedKeys: {
    left: false,
    right: false,
    up: false,
    down: false
  }
}

Let’s listen to all keydown and keyup events and update state.pressedKeys accordingly. The keys I’ll be using are D for right, A for left, W for up and S for down. You can find a list of key codes here.

var keyMap = {
  68: 'right',
  65: 'left',
  87: 'up',
  83: 'down'
}
function keydown(event) {
  var key = keyMap[event.keyCode]
  state.pressedKeys[key] = true
}
function keyup(event) {
  var key = keyMap[event.keyCode]
  state.pressedKeys[key] = false
}

window.addEventListener("keydown", keydown, false)
window.addEventListener("keyup", keyup, false)

Then we just need to update the x and y values based on the pressed keys and ensure that we keep our object within the boundaries.

function update(progress) {
  if (state.pressedKeys.left) {
    state.x -= progress
  }
  if (state.pressedKeys.right) {
    state.x += progress
  }
  if (state.pressedKeys.up) {
    state.y -= progress
  }
  if (state.pressedKeys.down) {
    state.y += progress
  }

  // Flip position at boundaries
  if (state.x > width) {
    state.x -= width
  }
  else if (state.x < 0) {
    state.x += width
  }
  if (state.y > height) {
    state.y -= height
  }
  else if (state.y < 0) {
    state.y += height
  }
}

And we have user input!

See the Pen Game Loop in Javascript: Dealing with User Input by SitePoint (@SitePoint) on CodePen.

Asteroids

Now that we have the fundamentals under our belt we can do something more interesting.

It’s not that much more complex to make a ship like was seen in the classic game Asteroids.

Our state needs to store an additional vector(an x,y pair) for movement as well as a rotation for the ships direction.

var state = {
  position: {
    x: (width / 2),
    y: (height / 2)
  },
  movement: {
    x: 0,
    y: 0
  },
  rotation: 0,
  pressedKeys: {
    left: false,
    right: false,
    up: false,
    down: false
  }
}

Our update function needs to update three things:

  • rotation based on the left/right pressed keys
  • movement based on the up/down keys and rotation
  • position based on the movement vector and the boundaries of the canvas
function update(progress) {
  // Make a smaller time value that's easier to work with
  var p = progress / 16

  updateRotation(p)
  updateMovement(p)
  updatePosition(p)
}

function updateRotation(p) {
  if (state.pressedKeys.left) {
    state.rotation -= p * 5
  }
  else if (state.pressedKeys.right) {
    state.rotation += p * 5
  }
}

function updateMovement(p) {
  // Behold! Mathematics for mapping a rotation to it's x, y components
  var accelerationVector = {
    x: p * .3 * Math.cos((state.rotation-90) * (Math.PI/180)),
    y: p * .3 * Math.sin((state.rotation-90) * (Math.PI/180))
  }

  if (state.pressedKeys.up) {
    state.movement.x += accelerationVector.x
    state.movement.y += accelerationVector.y
  }
  else if (state.pressedKeys.down) {
    state.movement.x -= accelerationVector.x
    state.movement.y -= accelerationVector.y
  }

  // Limit movement speed
  if (state.movement.x > 40) {
    state.movement.x = 40
  }
  else if (state.movement.x < -40) {
    state.movement.x = -40
  }
  if (state.movement.y > 40) {
    state.movement.y = 40
  }
  else if (state.movement.y < -40) {
    state.movement.y = -40
  }
}

function updatePosition(p) {
  state.position.x += state.movement.x
  state.position.y += state.movement.y

  // Detect boundaries
  if (state.position.x > width) {
    state.position.x -= width
  }
  else if (state.position.x < 0) {
    state.position.x += width
  }
  if (state.position.y > height) {
    state.position.y -= height
  }
  else if (state.position.y < 0) {
    state.position.y += height
  }
}

The draw function translates and rotates the canvas origin before drawing the arrow shape.

function draw() {
  ctx.clearRect(0, 0, width, height)

  ctx.save()
  ctx.translate(state.position.x, state.position.y)
  ctx.rotate((Math.PI/180) * state.rotation)

  ctx.strokeStyle = 'white'
  ctx.lineWidth = 2
  ctx.beginPath ()
  ctx.moveTo(0, 0)
  ctx.lineTo(10, 10)
  ctx.lineTo(0, -20)
  ctx.lineTo(-10, 10)
  ctx.lineTo(0, 0)
  ctx.closePath()
  ctx.stroke()
  ctx.restore()
}

That’s all the code that we need to re-create a ship like in Asteroids. The keys for this demo are the same as in the previous one (D for right, A for left, W for up and S for down).

See the Pen Game Loop in JavaScript: Recreating Asteroids by SitePoint (@SitePoint) on CodePen.

I’ll leave it to you to add the asteroids, bullets and collision detection 😉

Level Up

If you have found this article interesting you will enjoy watching Mary Rose Cook live-code Space Invaders from scratch for a more complex example, it’s a few years old now but is an excellent intro to building games in the browser. Enjoy!