preface
I still remember when I was just in college, in the first programming class, the teacher said, "don't let me catch you playing games, otherwise you will write what you play.". I didn't know anything and didn't dare to play.
If I can go back to that class now, I can play with my greedy snake unscrupulously. If she catches me, she will throw the source code address directly to her. Isn't it fast.
To get back to business, this article will take you to implement a web version of Snake game. The technology stack selects the current popular Vite + Vue3 + Ts.
👉👉 Online trial 👉👉 Source address
It is recommended to read this article in combination with the source code for better results~
Game screenshot
[external chain picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-wqpvnq7j-1635420801146)( https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f6f9d0ceb196419e91b327b4b7d3beda ~tplv-k3u1fbpfcp-watermark.image?)]
directory structure
├── src ├── assets // Storing static resources ├── components // vue component │ ├── Cell.vue // Every little square │ ├── Controller.vue // Game controller │ ├── KeyBoard.vue // Mobile terminal soft keyboard │ └── Map.vue // Map components ├── game // Game core logic │ ├── GameControl.ts // Controller class │ ├── Food.ts // Food category │ ├── Snake.ts // Snakes │ ├── hit.ts // Collision logic │ ├── render.ts // Render view logic │ ├── map.ts // Map related logic │ └── index.ts // Mainstream process ├── types // TS type ├── utils // Tool function ├── main.ts // Main entry file └── App.vue // vue root component ......
Implementation process
Note: the implementation process only intercepts the key code for explanation. It is recommended to read the source code for easier understanding.
Game screen rendering
/src/game/map.ts
// Get screen size const clientWidth = document.documentElement.clientWidth - 20; const clientHeight = document.documentElement.clientHeight - 40; // Number of rows export const gameRow = clientWidth > 700 ? Math.floor(clientHeight / 54) : Math.floor(clientHeight / 34); // Number of columns export const gameCol = clientWidth > 700 ? Math.floor(clientWidth / 54) : Math.floor(clientWidth / 34); // Initialize the map. Now all location type s are 0 export function initMap(map: Map) { for (let i = 0; i < gameRow; i++) { const arr: Array<number> = []; for (let j = 0; j < gameCol; j++) { arr.push(0); } map.push(arr); } return map; }
How to calculate the number of grids?
Here, I judge a device by obtaining the width and height of the device screen. The grid with a large screen is a little larger (50px) and the grid with a small screen is a little smaller (30px). Here I have subtracted a little from the width and height in order to make the picture have a sense of area and look more beautiful.
Then, divide the width and height of the screen by the size of each grid to obtain the number of rows and columns of the map. Because each grid directly has a 2px margin, it is 54 and 34.
How to generate maps?
Then, we render the map through a two-dimensional array according to the number of rows and columns calculated in the previous step. The element value of the two-dimensional array determines the color of each small grid. Because it is initialization, we first set all to 0 by default, and then pass the value of the element to the sub component Cell.vue.
/src/components/Map.vue
<template> <div class="game-box"> <!-- that 's ok --> <div class="row" v-for='row in gameRow' :key='row'> <!-- column --> <div class="col" v-for='col in gameCol' :key='col'> <!-- Small lattice --> <Cell :type='map[row-1][col-1]'></Cell> </div> </div> </div> </template>
How to distinguish elements?
/src/components/Cell.vue
<template> <div class='cell-box' :class='classes'> </div> </template> <script lang='ts' setup> import { computed, defineProps } from 'vue'; const props = defineProps(['type']); // Color of small grid const classes = computed(() => { return { head: props.type === 2, body: props.type === 1, food: props.type === -1, }; }); </script>
Think about what elements will appear on the whole game map, snake head (2), snake body (1) and food (- 1). Therefore, we assign different class es according to different element values, so that different elements can display different styles on the map.
Design of controller class
/src/game/GameControl.ts
export class GameControl { // snake snake: Snake; // food private _food: Food; // Map private _map: Map; // Game status private _isLive: IsLive; constructor(map: Map, isLive: IsLive) { this._map = map; this.snake = new Snake(); this._food = new Food(); this._isLive = isLive; } // Start the game start() { // Bind keyboard key press event document.addEventListener('keydown', this.keydownHandler.bind(this)); // Add to frame cycle list addTicker(this.handlerTicker.bind(this)); // Mark game status as start this._isLive.value = 2; } // Create a keyboard press response function keydownHandler(event: KeyboardEvent) { this.snake.direction = event.key; } // Render map private _timeInterval = 200; // Move snake private _isMove = intervalTimer(this._timeInterval); // Define frame loop function handlerTicker(n: number) { if (this._isMove(n)) { try { this.snake.move(this.snake.direction, this._food); } catch (error: any) { // Mark game status as end this._isLive.value = 3; // Stop cycle stopTicker(); } } render(this._map, this.snake, this._food); } // Restart the game replay() { reset(this._map); this.snake.direction = 'Right'; this.snake = new Snake(); this._food = new Food(); this._isLive.value = 2; addTicker(this.handlerTicker.bind(this)); } }
Start the game
When we start the game, we have to do three things: first bind the keyboard event, then add a frame loop to make the game move, and finally set the game state to in the game.
How do I add / stop a frame loop?
If you don't know about frame cycle, please refer to my following article.
👉👉 A magical front-end animation API requestAnimationFrame
/src/utils/ticker.ts
let startTime = Date.now(); type Ticker = Function; let tickers: Array<Ticker> = []; const handleFrame = () => { tickers.forEach((ticker) => { ticker(Date.now() - startTime); }); startTime = Date.now(); requestAnimationFrame(handleFrame); }; requestAnimationFrame(handleFrame); //Add frame loop export function addTicker(ticker: Ticker) { tickers.push(ticker); } //Stop frame cycle export function stopTicker() { tickers = []; } // Time accumulator export function intervalTimer(interval: number) { let t = 0; return (n: number) => { t += n; if (t >= interval) { t = 0; return true; } return false; }; }
Restart the game
When we restart the game, we also have to do three things: reset the map, add a frame loop, and set the game state to in the game.
Snake design
/src/game/Snake.ts
export class Snake { bodies: SnakeBodies; head: SnakeHead; // Create an attribute to store the moving direction of the snake (that is, the direction of the key) direction: string; constructor() { this.direction = 'Right'; this.head = { x: 1, y: 0, status: 2, }; this.bodies = [ { x: 0, y: 0, status: 1, }, ]; } // Define a method to check whether the snake eats food checkEat(food: Food) { if (this.head.x === food.x && this.head.y === food.y) { // Score increase // this.scorePanel.addScore(); // The position of the food should be reset food.change(this); // The snake needs to add a section this.bodies.unshift({ x: food.x, y: food.y, status: 1, }); } } // Control Snake Movement move(food: Food) { // Determine whether the game is over if (hitFence(this.head, this.direction) || hitSelf(this.head, this.bodies)) { throw new Error('game over'); } const headX = this.head.x; const headY = this.head.y; const bodyX = this.bodies[this.bodies.length - 1].x; const bodyY = this.bodies[this.bodies.length - 1].y; switch (this.direction) { case 'ArrowUp': case 'Up': // To move up, you need to check whether the key is in the opposite direction if (headY - 1 === bodyY && headX === bodyX) { moveDown(this.head, this.bodies); this.direction = 'Down'; return; } moveUp(this.head, this.bodies); break; case 'ArrowDown': case 'Down': // To move down, you need to check whether the key is in the opposite direction if (headY + 1 === bodyY && headX === bodyX) { moveUp(this.head, this.bodies); this.direction = 'Up'; return; } moveDown(this.head, this.bodies); break; case 'ArrowLeft': case 'Left': // To move to the left, you need to check whether the key is in the opposite direction if (headY === bodyY && headX - 1 === bodyX) { moveRight(this.head, this.bodies); this.direction = 'Right'; return; } moveLeft(this.head, this.bodies); break; case 'ArrowRight': case 'Right': // To move to the right, you need to check whether the key is in the opposite direction if (headY === bodyY && headX + 1 === bodyX) { moveLeft(this.head, this.bodies); this.direction = 'Left'; return; } moveRight(this.head, this.bodies); break; default: break; } // Check whether the snake eats food this.checkEat(food); } // The moving end modifies the moving direction changeDirection(direction: string) { if (direction === 'Left' && this.direction !== 'Left' && this.direction !== 'Right') { this.direction = 'Left'; return; } if (direction === 'Right' && this.direction !== 'Left' && this.direction !== 'Right') { this.direction = 'Right'; return; } if (direction === 'Up' && this.direction !== 'Up' && this.direction !== 'Down') { this.direction = 'Up'; return; } if (direction === 'Down' && this.direction !== 'Up' && this.direction !== 'Down') { this.direction = 'Down'; return; } } }
How do snakes move?
This place has bothered me for the longest time, but as long as I figure it out, it's not very difficult. We need to modify the coordinates of the snake head according to the direction, then we put the coordinates of the snake head into the last element of the array of snake bodies, and then delete the first element of the array of snake bodies. Because the snake moves from the next section to the previous section, it looks like the snake is moving in the view.
/src/game/Snake.ts
// Move up function moveUp(head: SnakeHead, bodies: SnakeBodies) { head.y--; bodies.push({ x: head.x, y: head.y + 1, status: 1, }); bodies.shift(); } // Move down function moveDown(head: SnakeHead, bodies: SnakeBodies) { head.y++; bodies.push({ x: head.x, y: head.y - 1, status: 1, }); bodies.shift(); } // Move right function moveRight(head: SnakeHead, bodies: SnakeBodies) { head.x++; bodies.push({ x: head.x - 1, y: head.y, status: 1, }); bodies.shift(); } // Move left function moveLeft(head: SnakeHead, bodies: SnakeBodies) { head.x--; bodies.push({ x: head.x + 1, y: head.y, status: 1, }); bodies.shift(); }
Then we will render the location information of the new snake to the view.
/src/game/render.ts
// Every time you render, you need to reset the map, and then render the new data export function render(map: Map, snake: Snake, food: Food) { // Reset map reset(map); // Render snake head _renderSnakeHead(map, snake.head); // Render snake body _renderSnakeBody(map, snake.bodies); // Render food _renderFood(map, food); } // Reset map resets all elements of the 2D array to 0 export function reset(map: Map) { for (let i = 0; i < map.length; i++) { for (let j = 0; j < map[0].length; j++) { if (map[i][j] !== 0) { map[i][j] = 0; } } } } // Render snake body - 1 food 1 snake body 2 snake head function _renderSnakeBody(map: Map, bodies: SnakeBodies) { for (let i = 0; i < bodies.length; i++) { const row = bodies[i].y; const col = bodies[i].x; map[row][col] = 1; } } // Render snake head - 1 food 1 snake body 2 snake head function _renderSnakeHead(map: Map, head: SnakeHead) { const row = head.y; const col = head.x; map[row][col] = 2; } // Render food - 1 food 1 snake body 2 snake head function _renderFood(map: Map, food: Food) { const row = food.y; const col = food.x; map[row][col] = -1; }
How to detect whether snakes eat food?
This is very simple. Just judge whether the coordinates of the snake head are the same as the snake body. At the same time, we push the position of the current snake head into the array of snake bodies, but do not delete the element of snake tail. It looks like a snake has added a section on the view.
How to detect snake collision?
There are two situations at the end of the game, one is to encounter the boundary, the other is to encounter yourself. The judgment when encountering the boundary is whether the coordinates of the snake head exceed the number of rows and columns. When you encounter yourself, you judge whether the coordinates of the snake head coincide with a section of the snake's body.
/src/game/hit.ts
// Does the snake head touch the boundary export function hitFence(head: SnakeHead, direction: string) { // 1. Get the position of snake head // 2. Is it beyond the scope of the game to detect snakeheads let isHitFence = false; switch (direction) { case 'ArrowUp': case 'Up': // Move up isHitFence = head.y - 1 < 0; break; case 'ArrowDown': case 'Down': // Move down because head.y starts from 0 and gameRow starts from 1, so gameRow should be - 1 isHitFence = head.y + 1 > gameRow - 1; break; case 'ArrowLeft': case 'Left': // Move left isHitFence = head.x - 1 < 0; break; case 'ArrowRight': case 'Right': // Move right isHitFence = head.x + 1 > gameCol - 1; break; default: break; } return isHitFence; } // Does the snake head touch itself export function hitSelf(head: SnakeHead, bodies: SnakeBodies) { // 1. Obtain the coordinates of snake head const x = head.x; const y = head.y; // 2. Get the body const snakeBodies = bodies; // 3. Check whether the snake head has hit itself, that is, whether the next movement of the snake head will repeat the elements of the body array const isHitSelf = snakeBodies.some((body) => { return body.x === x && body.y === y; }); return isHitSelf; }
How to change the direction of the snake?
This is also very simple. Just modify the corresponding direction value, but pay attention to judge that the snake can't turn back.
Food design
How to randomly generate food?
Generate a random coordinate by generating a random number. When the new coordinate coincides with the snake, call itself to generate it again.
/src/game/Food.ts
export class Food { // Coordinates of food x: number; y: number; status = -1; constructor() { this.x = randomIntegerInRange(0, gameCol - 1); this.y = randomIntegerInRange(0, gameRow - 1); } // Modify the location of food change(snake: Snake) { // Generate a random location const newX = randomIntegerInRange(0, gameCol - 1); const newY = randomIntegerInRange(0, gameRow - 1); // 1. Obtain the coordinates of snake head const x = snake.head.x; const y = snake.head.y; // 2. Get the body const bodies = snake.bodies; // 3. The food shall not coincide with the head and body const isRepeatBody = bodies.some((body) => { return body.x === newX && body.y === newY; }); const isRepeatHead = newX === x && newY === y; // Re randomization if conditions are not met if (isRepeatBody || isRepeatHead) { this.change(snake); } else { this.x = newX; this.y = newY; } } }
epilogue
If you want to practice, you can follow my ideas. If you don't understand, you can ask me in the comment area. I will reply as soon as I see it.
👉👉 Online trial 👉👉 Source address
Don't forget to praise and star~