build a snake game using canvas and requestAnimationFrame
Tags: javascript, programming
Published: 2022-01-23 15:13:53

this project is inspired by [dan's streaming](https://www.youtube.com/watch?v=dFRiNUp-FT0&t=5613s),but implement my way.

![snake.gif](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/886f0a556b4d4ddda96aad77c72d6283~tplv-k3u1fbpfcp-watermark.image?)

[live-demo](https://cyhfe.github.io/snake/)

[github](https://github.com/cyhfe/snake)

[blog](https://chenyuhao.vercel.app/)


## data structure and variables

```js
const canvas = document.getElementById("canvas")
const ctx = canvas.getContext("2d")

const width = 400
const height = 400
const cellLength = 20

let foodPosition

let initSnake = [
  [0, 0],
  [1, 0],
  [2, 0],
]

let snake = [...initSnake]

let direction = "right"

let canChangeDirection = true
```

## canvas 

```js
//  background
function drawBackground() {
  ctx.strokeStyle = "#bfbfbf"
  for (let i = 0; i <= height / cellLength; i++) {
    ctx.beginPath()
    ctx.moveTo(0, cellLength * i)
    ctx.lineTo(width, cellLength * i)
    ctx.stroke()
  }

  for (let i = 0; i <= width / cellLength; i++) {
    ctx.beginPath()
    ctx.moveTo(cellLength * i, 0)
    ctx.lineTo(cellLength * i, height)
    ctx.stroke()
  }
}

// snake
function drawSnake() {
  let step = 100 / (snake.length - 1)
  for (let i = 0; i < snake.length; i++) {
    // gradient color
    const percent = Math.min(100 - step * i, 90)
    ctx.fillStyle = `hsl(0,0%,${percent}%)`

    ctx.fillRect(
      snake[i][0] * cellLength,
      snake[i][1] * cellLength,
      cellLength,
      cellLength
    )
  }
}

// draw food

// random food position
function generateRandomFood() {
  // if no place to generate
  if (snake.length > width * height) {
    return alert("you win")
  }
  const randomX = Math.floor(Math.random() * (width / cellLength))
  const randomY = Math.floor(Math.random() * (height / cellLength))
  // if the position comflict with snake, then re-generate
  for (let i = 0; i < snake.length; i++) {
    if (snake[i][0] === randomX && snake[i][1] === randomY) {
      return generateRandomFood()
    }
  }
  foodPosition = [randomX, randomY]
}

// draw
function drawFood() {
  ctx.fillStyle = "#ff7875"
  ctx.fillRect(
    foodPosition[0] * cellLength,
    foodPosition[1] * cellLength,
    cellLength,
    cellLength
  )
}
```

## snake movement

```js

function snakeMove() {
  let next
  let last = snake[snake.length - 1]
  // set new snake head by direction
  switch (direction) {
    case "up": {
      next = [last[0], last[1] - 1]
      break
    }
    case "down": {
      next = [last[0], last[1] + 1]
      break
    }
    case "left": {
      next = [last[0] - 1, last[1]]
      break
    }
    case "right": {
      next = [last[0] + 1, last[1]]
      break
    }
  }

  // boundary collision
  const boundary =
    next[0] < 0 ||
    next[0] >= width / cellLength ||
    next[1] < 0 ||
    next[1] >= height / cellLength

  // self collision
  const selfCollision = snake.some(([x, y]) => next[0] === x && next[1] === y)

  // if collision, restart
  if (boundary || selfCollision) {
    return restart()
  }

  snake.push(next)

  // if next movement is food, push head, do not shift
  if (next[0] === foodPosition[0] && next[1] === foodPosition[1]) {
    generateRandomFood()
    return
  }
  snake.shift()

  canChangeDirection = true
}
```

## event listener

```js
document.addEventListener("keydown", (e) => {
  switch (e.key) {
    case "ArrowUp":
      if (direction === "down" || !canChangeDirection) return
      direction = "up"
      canChangeDirection = false
      break
    case "ArrowDown":
      if (direction === "up" || !canChangeDirection) return
      direction = "down"
      canChangeDirection = false
      break
    case "ArrowLeft":
      if (direction === "right" || !canChangeDirection) return
      direction = "left"
      canChangeDirection = false
      break
    case "ArrowRight":
      if (direction === "left" || !canChangeDirection) return
      direction = "right"
      canChangeDirection = false
      break
  }
})
```

## requestAnimationFrame for animate

```js
// its too fast for this game by default, make it slow down
function animate() {
  let count = 0
  function loop() {
    if (++count > 5) {
      draw()
      count = 0
    }
    requestAnimationFrame(loop)
  }
  requestAnimationFrame(loop)
}
```

## fix Bug

because requestAnimationFrame is async, as if snake's direction is right, i can change it to top,and then left before snake movement.

 so i add `canChangeDirection`, direction can change only after snake moved

```js
// event callback
case "ArrowUp":
  if (direction === "down" |!canChangeDirection) return
  direction = "up"
  canChangeDirection = false
  break
```