I am participating in the nuggets Community game creative submission Contest. For details, please see: Game Creative Submission Contest
preface
This article will achieve a web version of snake small game, technology stack selection of the current popular Vite + Vue3 + Ts.
👉👉 online demo 👉👉 source code address
It is recommended to read this article with the source code, the effect is better oh ~
screenshots
The directory structure
├ ─ ─ the SRC ├ ─ ─ assets// Store static resources├ ─ ─ the components/ / the vue components│ ├ ─ ─ Cell. Vue// Each little square│ ├ ─ ─ Controller. Vue// Game controller│ ├ ─ ─ the rid_device_info_keyboard. Vue// Mobile soft keyboard│ └ ─ ─Map.vue // Map component├ ─ ─ game// Core game logic│ ├ ─ ─ GameControl. Ts// Controller class│ ├ ─ ─ Food. Ts/ / class food│ ├ ─ ─ Snake. Ts/ / snakes│ ├ ─ ─ hit. Ts// Collision logic│ ├ ─ ─ render. Ts// Render the view logic│ ├ ─ ─ map. Ts// Map related logic│ └ ─ ─ index. Ts/ / main process├ ─ ─ types/ / TS type├ ─ ─ utils// Utility functions├ ─ ─ the main ts// Main entry file└ ─ ─ App. Vue// Vue root component.Copy the code
The implementation process
Note: the implementation process only intercept the key code to explain, it is recommended to read the source code, easier to understand.
Game rendering
/src/game/map.ts
// Get the screen size
const clientWidth = document.documentElement.clientWidth - 20;
const clientHeight = document.documentElement.clientHeight - 40;
/ / the number of rows
export const gameRow = clientWidth > 700 ? Math.floor(clientHeight / 54) : Math.floor(clientHeight / 34);
/ / the number of columns
export const gameCol = clientWidth > 700 ? Math.floor(clientWidth / 54) : Math.floor(clientWidth / 34);
// Initialize the map now that all positions are of type 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;
}
Copy the code
How do I count the number of cells?
Here, I judge a device by obtaining the width and height of the device’s screen. The larger screen is larger (50px) and the smaller screen is smaller (30px). I subtracted a little bit of width and height here to give the picture a sense of area and make it look nicer.
Then divide the width and height of the screen by the size of each grid to get the number of rows and columns of the map. Since each grid has a 2px margin, it is 54 and 34.
How do you create maps?
Then, according to the number of rows and columns calculated in the previous step, we render the map through a two-dimensional array. The element value of the two-dimensional array determines the color of each cell. Since this is initialization, we default to all zeros and pass the element’s value to the child component cell.vue.
/src/components/Map.vue
<template>
<div class="game-box">
<! Line -- -- -- >
<div class="row"
v-for='row in gameRow'
:key='row'>
<! - column -- -- >
<div class="col"
v-for='col in gameCol'
:key='col'>
<! -- Small box -->
<Cell :type='map[row-1][col-1]'></Cell>
</div>
</div>
</div>
</template>
Copy the code
How do you 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']);
// The color of the small box
const classes = computed(() = > {
return {
head: props.type === 2.body: props.type === 1.food: props.type === -1}; });</script>
Copy the code
Think about what elements will appear on the entire game map, snake head (2), snake body (1) and food (-1). So we can assign different classes to different elements based on their values, so that different elements can display different styles on the map.
Controller class design
/src/game/GameControl.ts
export class GameControl {
/ / the snake
snake: Snake;
/ / food
private _food: Food;
/ / map
private _map: Map;
// Game state
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 keypress events
document.addEventListener('keydown'.this.keydownHandler.bind(this));
// Add to the frame loop list
addTicker(this.handlerTicker.bind(this));
// mark the game state as start
this._isLive.value = 2;
}
// Create a keypress response function
keydownHandler(event: KeyboardEvent) {
this.snake.direction = event.key;
}
/ / render the map
private _timeInterval = 200;
// Whether to move the snake
private _isMove = intervalTimer(this._timeInterval);
// Define the frame loop function
handlerTicker(n: number) {
if (this._isMove(n)) {
try {
this.snake.move(this.snake.direction, this._food);
} catch (error: any) {
// Mark the game state as over
this._isLive.value = 3;
// Stop the loop
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)); }}Copy the code
Start the game
To start the game we need to do three things, first bind keyboard events, then add a frame loop to get the game moving, and finally put the game state into play.
How do I add/stop frame loops?
Do not understand the frame loop can refer to my following article.
👉👉 a fantastic 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 a frame loop
export function addTicker(ticker: Ticker) {
tickers.push(ticker);
}
// Stop the frame loop
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;
};
}
Copy the code
Restart the game
To restart the game, we also need to do three things: reset the map, add a frame loop, and put the game state into the game.
Snake design
/src/game/Snake.ts
export class Snake {
bodies: SnakeBodies;
head: SnakeHead;
// Create a property to store the direction of the snake's movement (i.e. 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 has eaten the food
checkEat(food: Food) {
if (this.head.x === food.x && this.head.y === food.y) {
// The score increases
// this.scorePanel.addScore();
// The food position should be reset
food.change(this);
// Add a section for the snake
this.bodies.unshift({
x: food.x,
y: food.y,
status: 1}); }}// Control snake movement
move(food: Food) {
// Determine if 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':
// Moving up requires checking if 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':
// Moving down requires checking if 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, 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, 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 if the snake has eaten
this.checkEat(food);
}
// Change the movement direction on the mobile terminal
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; }}}Copy the code
How does the snake move?
This is the place that has bothered me the longest, but it’s not so hard once I figure it out. 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 snake body array, and then delete the first element of the snake body array. Because when the snake moves, it’s always when the next snake moves to the same spot as the previous snake, so it looks like the snake is moving.
/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 to the 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(); }Copy the code
Then we will render the new snake’s position information to the view.
/src/game/render.ts
// Each render requires the map to be reset before new data is rendered
export function render(map: Map, snake: Snake, food: Food) {
/ / reset the map
reset(map);
// Render the snake head
_renderSnakeHead(map, snake.head);
// Render the snake
_renderSnakeBody(map, snake.bodies);
// Render the food
_renderFood(map, food);
}
// Reset map resets all elements of the two-dimensional 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;
}
Copy the code
How do you detect whether a snake has eaten food?
This is very simple, as long as determine whether the coordinates of the snake head and snake body are the same. When we do the same, we push the current snake head into the array of the snake body, but don’t delete the tail element, and it looks like the snake has added a section to the view.
How do you detect snake collisions?
The end of the game has two situations, one is to encounter the boundary, one is to encounter themselves. The determination of the boundary is whether the coordinate of the snake’s head exceeds the number of rows and columns. The judgment of encountering oneself is whether the coordinates of the snake’s head coincide with some part of the snake’s body.
/src/game/hit.ts
// Whether the snake head touches the boundary
export function hitFence(head: SnakeHead, direction: string) {
// 1. Obtain the position of the snake head
// 2. Check whether the snake head is outside the scope of the game
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 at 0 and gameRow starts at 1, so gameRow needs -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 to the right
isHitFence = head.x + 1 > gameCol - 1;
break;
default:
break;
}
return isHitFence;
}
// Whether the snake head touches itself
export function hitSelf(head: SnakeHead, bodies: SnakeBodies) {
// 1. Obtain the coordinates of the snake head
const x = head.x;
const y = head.y;
// 2. Get the body
const snakeBodies = bodies;
// 3. Check if the snake's head has hit itself, i.e., if the next move of the snake's head repeats the elements of the body array
const isHitSelf = snakeBodies.some((body) = > {
return body.x === x && body.y === y;
});
return isHitSelf;
}
Copy the code
How to change the direction of the snake’s movement?
This is also easy to change the direction of the corresponding value, but note that the snake can not turn back.
Food design
How do you randomly generate food?
Generate a random coordinate by generating random number. When the new coordinate coincides with the snake, call itself to generate again.
/src/game/Food.ts
export class Food {
// Food coordinates
x: number;
y: number;
status = -1;
constructor() {
this.x = randomIntegerInRange(0, gameCol - 1);
this.y = randomIntegerInRange(0, gameRow - 1);
}
// Change the location of food
change(snake: Snake) {
// Generate a random position
const newX = randomIntegerInRange(0, gameCol - 1);
const newY = randomIntegerInRange(0, gameRow - 1);
// 1. Obtain the coordinates of the snake head
const x = snake.head.x;
const y = snake.head.y;
// 2. Get the body
const bodies = snake.bodies;
// 3. Food should not overlap with the head or body
const isRepeatBody = bodies.some((body) = > {
return body.x === newX && body.y === newY;
});
const isRepeatHead = newX === x && newY === y;
// re-randomize if the condition is not met
if (isRepeatBody || isRepeatHead) {
this.change(snake);
} else {
this.x = newX;
this.y = newY; }}}Copy the code
conclusion
👉👉 online demo 👉👉 source code address
Don’t forget to hit “like” and “star”