Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

📢 hello everyone, I am Xiao Cheng, this article will take you to make a snake game

📢 Thank you very much for reading

📢 may your life be bright and lovely

preface

Recently in the study, again met the snake case, just learned JavaScript before there was met, while this time there is a little time, followed to do it, this article will take you hand in hand to achieve a snake small game, the difficulty is not very big, hee hee

Here are some lessons to be learned from this case:

Object-oriented programming, This points to the problem, Simple configuration of WebPack,

First, achieve the effect preview

The following functions need to be implemented:

  1. The page layout
  2. Randomly generated food
  3. Score Statistics (Quantity of food eaten)
  4. Level up (speed up)
  5. The snake growth
  6. Event monitoring
  7. Hit the body detection
  8. Bump wall detection
  9. The end of the judgment

Two, code implementation

1. Page layout

Make a simple layout, mainly using less and Flex layout combination

A few interesting points

In layout, the global variable BG-color is used to define the global color, which adds more extensibility to the code

@bg-color: #b7d4a8;
Copy the code

The border-box model in CSS3 is used globally to avoid the influence of border and margin on the original size of the box

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
Copy the code

When drawing the snake, you need to set the length of the snake by adding a div tag to the container. Therefore, you need to set the style of the div tag in the container separately during layout

// index.html
<!-- 蛇 -->
<div id="snake">
    <!--Snake-body.-->
    <div></div>
</div>

// index.less
#snake {
  & > div {
    width: 10px;
    height: 10px;
    background-color: black;
    // Set the spacing
    border: 1px solid @bg-color;
    // Enable positioning
    position: absolute; }}Copy the code

For the food style, use Flex with a little rotation

#food {
  position: absolute;
  width: 10px;
  height: 10px;
  left: 40px;
  top: 100px;
  display: flex;
  flex-flow: row wrap;
  justify-content: space-between;
  align-content: space-between;
  & > div {
    width: 4px; height: 4px; background-color: black; transform: rotate(45deg); }}Copy the code

Rotate each div at an Angle to make it look nice

Note here: since our snake body and food both need to move, we need to set them to the absolute positioning mode and pay attention to the relative positioning of the parent box

2. Randomly generate food

Let’s go through what attributes or methods do we need for food

  1. Each food has to have a place that we passX 和 YAttribute to locate
  2. We also need a way to randomly generate food locations
// Define the Food class Food
class Food {
    // Define the food element
    element: HTMLElement;
    constructor() {
        // Get the food element from the page to Element
        this.element = document.getElementById("food")!
    }
    // Get the X-axis coordinates of the food
    get X() {
        return this.element.offsetLeft
    }
    get Y() {
        return this.element.offsetTop
    }
    // A method to modify the location of food
    change() {
        // One cell is 10
        let top = Math.round(Math.random() * 29) * 10
        let left = Math.round(Math.random() * 29) * 10
        this.element.style.left = left + 'px'
        this.element.style.top = top + 'px'}}Copy the code

Here we create a Food class that defines the location of the Food

We first declare an Element property named HTMLElement. Constructor needs to get our food element and assign it to the Element property

Here because of ts syntax check mechanism is more strict, we need to obtain a node at the end of the add! To trust element fetching here

So the TS is anticipating that we can’t get this node and it’s going to make an error, just get used to it, add it!

In the method to get food coordinates, we use the getter function to get values, so we can get X and Y values just like we would with a normal variable

Because every time the food is eaten, we need to generate a new food. In fact, we just change the location of the food, which is always the same food node. Here, random is used to generate a random number of 0-29, and then 10 times, so that the location can be selected as a random multiple of 10. At the same time within the map

For example, we use 29 pure numbers, which is not conducive to changing the map. When the map changes, we need to modify the source code to improve the code, which is not good, we can use a variable to save oh

3. Score statistics

After writing the Food class, let’s write a simple ScorePanel class that sets the score and rating at the bottom

  1. We need to have a score record, a grade record, and a way to modify them
  2. To improve scalability, we need two variables to control the maximum level of limitation and how many points of escalation are achieved
class ScorePanel {
    // Record scores and grades
    score = 0;
    level = 1;
    // Grade and grade elements
    scoreEle: HTMLElement
    levelEle: HTMLElement
    // Set the limit level of a variable
    maxLevel: number
    // Set a variable to indicate how many timeshare upgrades
    upScore: number
    constructor(maxLevel: number = 10, upScore: 10) {
        this.scoreEle = document.getElementById("score")!
        this.levelEle = document.getElementById("level")!
        this.maxLevel = maxLevel
        this.upScore = upScore
    }
    // set a bonus method
    addScore() {
        this.scoreEle.innerHTML = ++this.score + ' ';
        (this.score % this.upScore === 0) && this.levelUp()
    }
    // The way to improve the level
    levelUp() {
        this.level < this.maxLevel && (this.levelEle.innerHTML = ++this.level + ' ')}}Copy the code

We created a ScorePanel class

In this class, we pre-set a lot of variables, in TS we need to set their use type

Here we set up the method of extra points

addScore() {
        this.scoreEle.innerHTML = ++this.score + ' ';
        (this.score % this.upScore === 0) && this.levelUp()
}
Copy the code

When we call this function, we can increase the score, and then we need to evaluate the current score. When the score reaches the level score we set, we call the levelUp method in the class to raise the current level

4. Snake growth

After defining the basic peripheral functions, it’s time to officially attack the snake

Let’s start by creating a snake class that sets the snake’s own properties, such as location and length

First we need to set up some variables to store our node

/ / the snake
head: HTMLElement
// Snake body
bodies: HTMLCollection
// Get the snake container
element: HTMLElement
constructor() {
    this.element = document.getElementById("snake")!
    this.head = document.querySelector("#snake > div") as HTMLElement
    this.bodies = this.element.getElementsByTagName("div")}Copy the code

In TS, we try to set our variables up so that we don’t misuse them and cause errors

Let’s define getter and setter methods to get the position of the snake head and set the position of the snake head

Why the snakehead?

We need to drive the movement of the body by the direction of the head, because each piece of the body follows the previous one

// Get the snake coordinates
get X() {
    return this.head.offsetLeft
}
get Y() {
    return this.head.offsetTop
}
Copy the code

(There are a lot of judgments in the set. It’s too long to write.)

After setting up the set and get methods, we need to write a method that makes the snake grow, which is nothing more than adding an extra div element to the snake node

// Snake body method
addBody() {
    // Add a div to element
    this.element.insertAdjacentHTML("beforeend"."<div></div>")}Copy the code

Small science

The insertAdjacentHTML() method parses the specified text into an Element and inserts the resulting node into the specified location in the DOM tree. It does not reparse the element it is using, so it does not destroy existing elements within the element. This avoids the extra serialization step and makes it faster than using innerHTML directly.

The specified locations are as follows

  • 'beforebegin': precedes the element itself.
  • 'afterbegin': before inserting the first child node inside the element.
  • 'beforeend': After the last child node is inserted inside the element.
  • 'afterend': comes after the element itself.

5. Control the snake’s movement

Now our snake is able to add a body, but we didn’t add a way to control the snake’s movement, so there’s no way to show this effect

Let’s move on to how do we make the snake move?

We use the direction key of the keyboard to control the movement direction of the snake. As mentioned above, the movement of the whole snake is driven by the head of the snake, so we first control the movement of the head of the snake

First we need to create a GameControl class that acts as a controller for the game and controls the movement of the snake

First of all, we need to have a keyboard response event to obtain the user’s keyboard events. At the same time, we need to judge whether the keys are the four keys that can control the snake’s movement

So we can write two functions: keydownHandle, the keyboard event response function, run, the main controller, and say what key did the user press to perform the corresponding change

We can wrap these two functions in an init function and start them together as initializers

init() {
    // Bind keyboard events
    document.addEventListener("keydown".this.keydownHandle.bind(this))
    this.run()
}
Copy the code

In this function, we can separate the event callback into a function because we need to use the TS checking mechanism, but since the callback object here is document, we need to manually change the reference to this

We handle keyboard events in keydownHandle, using a direaction variable to record the current key

// Store the snake's movement direction
direction: string = ' '

// Keyboard response function
keydownHandle(event: KeyboardEvent) {
    // Check whether it is valid
    this.direction = event.key                    
}
Copy the code

Determine which direction the snake is moving based on direction

// Create a snake movement method
run() {
    let X = this.snake.X
    let Y = this.snake.Y
    // Change the value according to the keystroke direction
    switch (this.direction) {
        // Decrease top up
        case "ArrowUp":
            Y -= 10
            break
        // Increment down top
        case "ArrowDown":
            Y += 10
            break
        // The left is reduced
        case "ArrowLeft":
            X -= 10
            break
        // Add to right left
        case "ArrowRight":
            X += 10
            break}}Copy the code

After we change the X and Y values, we need to reassign them to the corresponding values in Snake. Since we set the setter function, we can directly assign the values

this.snake.X = X;
this.snake.Y = Y;
Copy the code

We through to the four directions judging key switch, we allow us to control the snake’s movement, but now it is not enough to achieve the effect of moving, we need to implement after pressing a key direction, is constantly moving in one direction, so we can start a timer in the run, enables it to recursive calls to the run

// recursive call
this.isLive && setTimeout(this.run.bind(this), 300 - (this.scorePanel.level - 1) * 30)
Copy the code

Since our snake has a death mechanism, we need to pre-judge the following, there is also the issue of this pointing, we need to manually adjust pointing to the current class

By the time we get to this point, our snake head is already moving

6. Check your food intake

Now that the head of the snake is able to move, we can touch the food and anywhere, we need to check whether the food is eaten, what happens when the food is eaten, and what functions are implemented

// Check to see if food is available
checkEat(X: number, Y: number) {
    if (X === this.food.X && Y === this.food.Y) {
        // Food position changed
        this.food.change()
        / / points
        this.scorePanel.addScore()
        / / snake plus one
        this.snake.addBody()
    }
}
Copy the code

In the function to check whether the food is eaten, we need two parameters, namely the position of the snake’s head, to judge whether it overlaps with the food. If it overlaps, the position of the food will be changed, the score will be scored, and the body will be added by one

7. Control snake body movement

Now our snake is able to eat food, but what we find is that after eating food, its body doesn’t go with it, it’s positioned in the upper left corner, so we have to deal with the movement of the snake

Since it involves snake’s own features, we’ll go back to the Snake class

// Add a snake body movement method
moveBody() {
    // The position is where the previous snake block was
    for (let i = this.bodies.length - 1; i > 0; i--) {
        let X = (this.bodies[i - 1] as HTMLElement).offsetLeft;
        let Y = (this.bodies[i - 1] as HTMLElement).offsetTop;
        (this.bodies[i] as HTMLElement).style.left = X + 'px';
        (this.bodies[i] as HTMLElement).style.top = Y + 'px'; }}Copy the code

We go through the loop, starting with the last snake block, and changing its position to the position of the previous snake block

So can move one by one, don’t understand can think about oh ~

In this code, we encountered a lot of type assertion problems, because the TS checking mechanism does not determine whether there is an offset class method in the array element, so we are given an error message

8. Wall detection

We need to end the game when our snake’s head hits the wall, so we need to add a little judgment, and since the snake can only go in one direction, we need to optimize the following code so that instead of calling set X and set Y every time, we can just return when the new value is the same as the old value

set Y(value) {
    // If the new value is the same as the old value, the new value is returned without modification
    if(this.Y === value){
        return;
    }
    if (value < 0 || value > 290) {
        throw new Error('Snake hits the wall')}// Move the body
    this.moveBody();
    this.head.style.top = value + 'px';
}
Copy the code

When we hit the wall, we throw an error, and then we can use a try in GameControl… Catch to catch the error and give instructions

try {
	this.snake.X = X;
	this.snake.Y = Y;
} catch (e: any) {
    alert(e.message + 'GAME OVER')
    // isLive set to false
    this.isLive = false
}
Copy the code

And end the snake’s life

9. U-turn detection

Since our snake can’t turn around, we need to determine that the following users want to turn around and handle this event

We continue to add code to the function that sets the value

First of all, when there is only one body, we do not need to consider. Therefore, we need to judge whether there is a second snake body and the most critical point is whether the position of the snake body is equal to the value we are going to walk

What does that mean?

When the snake moves, the position of the second part of the snake body should be the position of the first part, and the position of the snake head should be the position of value. When the snake head is reversed, its value will become the position of the second part of the snake body

And just to make sense of it, the circle is where the snake head is going to be, and the square on the right is the snake head

So we add this code, and when the u-turn condition is met, we keep it going

set Y(value) {
    // Is there a second body
    if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
        // If you turn around, you should keep going
        if (value > this.Y) {
            value = this.Y - 10
        } else {
            value = this.Y + 10}}}Copy the code

10. Crash detection

When a snake eats itself, it needs to end the game, so we need to detect whether it’s eating itself

We need to traverse all the following positions of the snake body and compare them with the position of the snake head. If there is the same position as the snake head, it means that the snake head ate the snake body

checkHeadBody() {
    // Get all the bodies and check if they overlap
    for (let i = 1; i < this.bodies.length; i++) {
        let bd = this.bodies[i] as HTMLElement
        if (this.X === bd.offsetLeft && this.Y === bd.offsetTop) {
            throw new Error('I bumped into myself.')}}}Copy the code

Since we need multiple type assertions here, separate assertions are extracted

Third, summary

The framework of the whole Snake game is so much, when writing this article, there may be some code length is too long, there is a little reduction of the code, may affect the reading or understanding, please forgive me

From this example, I have a simple understanding of TypeScript, but there is still a lot of knowledge that has not been covered. I feel that this example is not good enough and I need to practice it again. In general, Typescript has many limitations compared to javascipt. These limitations allow potential unknown bugs to be exposed, which helps maintain code and reduces the need for developers to find bugs later

I still have a lot to learn about typescript, so keep working on it, and I welcome your comments and suggestions, and let’s grow together.

Thank you very much for reading, welcome to put forward your opinion, if you have any questions, please point out, thank you! 🎈