build a snake game using canvas and requestAnimationFrame
p4nghu
Posted on January 23, 2022
this project is inspired by dan's streaming,but implement my way.
data structure and variables
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
// 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
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
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
// 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
// event callback
case "ArrowUp":
if (direction === "down" |!canChangeDirection) return
direction = "up"
canChangeDirection = false
break
💖 💪 🙅 🚩
p4nghu
Posted on January 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
tailwindcss Integrating Tailwind CSS with Other Frontend Frameworks for Admin Panels
November 30, 2024
webdev JavaScript Higher-Order Functions Made Easy: Learn with a Real-Life Example! 💡
November 30, 2024