Native JS realizes the super detailed analysis of 10000 words of FlappyBird game. Come and play it yourself

catalogue โ€‹

1. Adaptive equipment๐Ÿพ

2. Background scrolling๐Ÿ’

3. Creation and movement of pipes๐ŸŒธ

4. Bird operation๐ŸŒท

5. Collision detection๐Ÿ€

6. Touch screen events๐ŸŒน

7. Make start and end panels๐ŸŒป

8. Score statistics๐ŸŒบ

Let's take a look at the effect we want to do next: ๐Ÿ™‹๐Ÿ™‹๐Ÿ™‹

Students who need source code and materials have links at the end of the article.  

1. Adaptive equipment ๐Ÿ’จ

The background under the PC terminal is 320px*568px (the size of the game background picture), and the window under the mobile terminal is full

Create a new public JS file, which puts some of our public methods. Let's first define an isPhone method to judge whether it is a mobile device

function isPhone() {
    var arr = ["iPhone","iPad","Android"]
    var is = false;
    for(var i = 0;i<arr.length;i++) {
        if(navigator.userAgent.indexOf(arr[i])!=-1) {
            is = true;
        }
    }
    return is;
}

In the isPhone method, we define an array arr to store the device name of the mobile terminal. The UserAgent is the user ID in the HTTP request. Generally, it sends a string that can represent the client type. The indexOf() method can return the position where a specified string value appears for the first time in the string. If the string value to be retrieved does not appear, the method returns - 1

We default to the PC side. If indexOf does not return - 1, it means that it matches the elements in the array and represents a mobile device, then our isPhone method returns true.

This method of judging the mobile terminal can be saved, and we can also use it for many projects in the future

Because we stipulate that the background picture in the mobile terminal should occupy the whole screen, we need an if statement to judge. if isPhone returns true, it means that we need to modify the width and height of the background picture in the mobile terminal:

sw and sh are global variables defined outside. By default, sw = 320 and sh = 568. Because we will use sw and sh later, we need to re assign them if the device is a mobile terminal.

if (isPhone()) {
        var bg = document.querySelector('.contain');
        sw = document.documentElement.clientWidth + 'px';
        sh = document.documentElement.clientHeight + 'px';
        bg.style.width = sw;
        bg.style.height = sh;
}

document.documentElement.clientWidth ๏ผŒ is the screen width of the current device. Pay attention to adding symbols

We can simulate whether different devices on the mobile terminal occupy the full screen under the chrome browser. We need to refresh the page every time we change the device:

โ€‹

In this way, we have completed the effect of adapting the device. We have succeeded in occupying the full screen under the mobile terminal. Now let's start making our flappybird game!

2. Background scrolling ๐Ÿ’จ

In the following code, bg is the outermost box containing the background picture we obtained. The background picture is tiled on the x-axis, so we only need a timer to continuously call the background moving function. In the background moving function, we move the position of the background by 5 pixels to the left every time we call the background

var timer = setInterval(function(){
        bgMove();
},30)
function bgMove() {
      var bg = document.querySelector('.contain');
      bgDis -= 5;
      bg.style.backgroundPosition = `${bgDis}px 0`;
}

In the game we made, whether it is the background movement, the pipeline movement to be done later, the movement of birds, and the finally encapsulated functions need to be called in this timer, so as to have the same effect as the animation we see.  

3. Creation and movement of pipes ๐Ÿ’จ

Before moving the pipeline, we need to create the pipeline first. Because we want to make the generated pipeline inconsistent in height, we need to write a random number function first. We're in public JS:

function rand(min, max) {
    return Math.round(Math.random() * (max-min) + min);
}

Let's sort out the idea of creating a pipeline first:

  1. First write the style of the pipeline. The code of html and css is at the end of the article. When you look at the analysis, you should first take a look at the html structure and css style.  
  2. Specify the upper and lower pipe spacing of 120px, and realize the random height of the upper pipe through the defined rand random function. The height of the background picture minus the interval minus the height of the upper pipe is the height of the lower pipe. Here, the height of the lower pipe should not be random.
  3. Add the code that generates the pipeline to the ul through insertAdjacentHTML

Because the pipeline is constantly generated, we need to call the pipeline movement function pipeMove() in the timer:

var timer = setInterval(function(){
        bgMove();
        pipeMove();
},30)
We define this pipeMove method outside and complete the creation and movement of pipes in pipeMove
function pipeMove() {
       //1. Create a pipe
       createPipe();
       //2. Pipeline movement
}

Let's improve the pipeline creation function createPipe according to the idea written at the beginning:

function createPipe() {
        var pipeheight = rand(100,300);
        var ul = document.querySelector('ul');
        var str = `<li class="top" style="height:${pipeheight+'px'};left:${sw+'px'}"><div></div></li><li class="bottom" style="height:${sh-pipeheight-120+'px'};left:${sw+'px'}"><div></div></li>`;
        ul.insertAdjacentHTML('beforeend',str);
}

Run the code to see if the pipeline has been created:

Obviously, there are too many pipes. Because the timer calls the pipe creation function every 30 milliseconds, there are a lot of pipes generated. We define some global variables to limit:

var space = 100;  //Create interval for pipe
var count = 0;  //Pipe count

Modify the createPipe function to execute the following code for creating pipes only when the count reaches the interval of creating pipes 100. Otherwise, it will not be executed, which limits the number of generated pipes

function createPipe() {
       count ++;
       if (count != space) {
            return ;
       }
       count = 0;
       var pipeheight = rand(100,300);
       var ul = document.querySelector('ul');
       var str = `<li class="top" able="0" style="height:${pipeheight+'px'};left:${sw+'px'}"><div></div></li><li class="bottom" style="height:${sh-pipeheight-120+'px'};left:${sw+'px'}"><div></div></li>`;
       ul.insertAdjacentHTML('beforeend',str);
}

Now the pipeline can be generated continuously on the right side of the background, so that the creation of the pipeline is completed. Next, continue to improve the pipeline movement in the pipeMove method:

function pipeMove() {
            //1. Create a pipe
            createPipe();
            //2. Pipeline movement
            var li = document.querySelectorAll('li');
            li.forEach(function(value,index,arr){
                arr[index].style.left = arr[index].offsetLeft-2+'px';
            })
        }

We first get all the pipes created, and then move the pipe to the left by two pixels for each call through the foreach loop, and the pipe can move successfully.

Note: directly through obj style. Left and obj style. Top can obtain the location, but it has limitations. This method can only obtain the attribute values of left and top of the style in the line, but not the left and top attribute values of the style tag and the external reference of link. So we use offsetleft to get

Then we add a boundary to the pipe and delete this element in ul when it goes beyond the background:

li.forEach(function(value,index,arr){
        arr[index].style.left = arr[index].offsetLeft-5+'px';
        if(arr[index].offsetLeft<=-62) {
             ul.removeChild(arr[index]);
        }
})

Let's run the code to see the effect:

ย โ€‹

In this way, the creation and movement of the pipeline are basically completed. Let's start the operation of the bird.

4. Bird operation ๐Ÿ’จ

The next step is to set up the "bird style" and "css".

Similarly, we need to call the middle note function birdMov in the timer.

var timer = setInterval(function(){
             bgMove();
             pipeMove();
             birdMove();
        },30)

Because the bird will fall down at the beginning of the game, first write a movement function of the bird:

function birdMove() {
       var bird = document.querySelector('#bird');
       bird.style.top = bird.offsetTop +5 + 'px';
}

In this way, the bird will fall at a constant speed, but our bird in the game is not at a constant speed, so we also need to define a global variable speed and initialize its value to 0 to control the speed of the bird.

Because when we click the screen in the game, the bird will fly up, and the speed of flying up is different from that of flying down, so we declare an isDown variable globally to judge whether the bird flies down. isDown = true by default, because if the bird does not operate, it must fly down.

function birdMove() {
            if(isDown) {
                speed += 0.4;
                speed = speed > 8 ? 8 : speed;
            }
            else{
                speed += 0.7;
            }
            var bird = document.querySelector('.bird');
            bird.style.top = bird.offsetTop +speed + 'px';
}

If you don't click the screen, the speed of the bird will increase by 0.4 every 30 milliseconds, and then use a ternary expression. If the speed reaches 8, the limit speed of the bird will not increase. Finally, we change the original fixed value of 5 into speed to realize the dynamic change of the speed of the bird

Next, we want to realize that the bird can move upward when clicking any part of the background, so we need to give a click event to the background image:

var contain = document.querySelector('.contain');
        contain.addEventListener('click',function() {
        isDown = false;
        speed = -8;
})

When we click on the screen, the bird will fly up, so isDown is set to false, and then immediately give an upward displacement distance of 8

Let's run the code to see the effect:

โ€‹

Does it feel a little bit, but the bird's head will lift up when clicking the screen, so you have to change the background picture of the bird when clicking the screen

We create two classes. One is birddown, which is the picture of the bird with its head down, and the other is birdup, which is the picture of the bird with its head up.

.birddown {
      background: url(./img/down_bird0.png);
}
.bird_up {
      background: url(./img/up_bird0.png);
}

Then add the default style class birddown to bird, so that when we click the screen, we can change the class of bird to birdup:

contain.addEventListener('click',function() {
       isDown = false;
       speed = -8;
       var bird = document.querySelector('#bird');
       bird.className = 'birdup';
})

In this way, when we click on the screen, the bird will change from head down to head up, but if we don't click on the screen, the bird should return to the default down style, because if we don't click on the screen, the bird will fly down. How do we think about this?

When does the bird fly down? That is, when the speed is 0, the speed of the bird is - 8 every time we click the screen, but we have been calling birdmove. Every time the speed is increased by 0.7, the upward speed will always be smaller and smaller, and then when it is greater than 0, the bird will fly down.

In this way, we complete the code in birdmove, click the screen, the bird will fly up, and when it falls, it will fly down:

function birdMove() {
            var bird = document.querySelector('#bird');
            if(isDown) {
                speed += 0.4;
                speed = speed > 8 ? 8 : speed;
            }
            else{
                speed += 0.7;
                if(speed>=0) {
                    speed = 0;
                    isDown = true;
                    bird.className = 'birddown';
                }
            }
            var bird = document.querySelector('#bird');
            bird.style.top = bird.offsetTop +speed + 'px';
        }

Let's see the effect:

โ€‹

In this way, we have basically finished the action of the bird. Now we need to make the game gameover when the bird touches the top and bottom.

Put the following code to judge the boundary in birdmove. In this way, judge whether it exceeds the boundary every time. If it exceeds the boundary, directly gameover and clear the timer, and then execute the again function to restart the game.

if(bird.offsetTop<0||bird.offsetTop>sh-30) {
            alert('gameover');
            clearInterval(timer);
            again();
            return;
}

Create this again function to restart the game:

function again() {
        bgDis = 0;//bg moving distance
        count = 0;  //Pipe count
        isDown = true;//Determine whether to fly down
        speed = 0;//Control the speed of the bird
        var ul = document.querySelector('ul');
        ul.innerHTML = '';//Empty the pipe
        var bird = document.querySelector('#bird');
        bird.style.top = 100+'px';
        start()
}

We need to reinitialize some variables in the again function. Here, some variables such as pipe spacing or background width and height do not need to be reinitialized. And do not take var, because if you take var, it is a local variable, but what we want to change here is a global variable.

Then we need to clear all the pipes in the picture, that is, the content in ul. Then restore the height of the bird to the first 20 pixels from the top.

Finally, a start function is called, which is encapsulated by the start base note.

function start() {
      timer = setInterval(function(){
           bgMove();
           pipeMove();
           birdMove();
      },30)
}

Because we cleared the timer at the end of the game, we have to call the timer again when we restart.

Note: the timer variable timer should not add var, because the function with var added after encapsulation is not a global variable

In this way, when our bird touches the top or bottom, the gameover dialog box will pop up, click OK, and then restart the game

Then we modify the original birddown class and birdup class to animation

@keyframes birddown {
            from {
                background-image: url(img/down_bird0.png);
            }
            to {
                background-image: url(img/down_bird1.png);
            }
        }
        @keyframes birdup {
            from {
                background-image: url(img/up_bird0.png);
            }
            to {
                background-image: url(img/up_bird1.png);
            }
        }

From bird0 to bird1, there is a change in the bird's wings. In this way, with animation, the bird is like flying its wings.  

.birddown {
       animation: birddown 0.05s linear infinite;
}
.birdup {
       animation: birdup 0.05s linear infinite;
}

5. Collision detection ๐Ÿ’จ

It's not difficult for us to judge whether the bird hit the top or bottom, but how to judge whether the bird collided with the pipe?

Now let's go back to public JS file, write the collision detection function isCrash, which is also highly reusable.

function isCrash(a,b) {
    var l1 = a.offsetLeft;
    var t1 = a.offsetTop;
    var r1 = l1 + a.offsetWidth;
    var b1 = t1 + a.offsetHeight;

    var l2 = b.offsetLeft;
    var t2 = b.offsetTop;
    var r2 = l2 + b.offsetWidth;
    var b2 = t2 + b.offsetHeight;
    if (r2<l1 || b2<t1 || r1<l2 || b1<t2) {
        // No collision
        return false;
    } else {
        // collision
        return true;
    }
}

As long as one condition is not satisfied in the if statement, it means that there will be no collision. This is easy to understand. Here, let's analyze why R2 < l1 means that there will be no collision? If a represents the pipe and b represents the bird, then l1 is the distance from the pipe to the left background, and l2 represents the distance from the bird to the left of the background. Then R2 < l1 means the width of the bird itself and the distance from the bird to the left of the background is smaller than that from the pipe to the left of the background, so they will not touch, so the same is true in other directions.

Then we call the check() function in the start function:

function start() {
            timer = setInterval(function(){
                 bgMove();
                 pipeMove();
                 birdMove();
                 check();
            },30)
        }

The check() function calls isCrash to see if all pipes collide with birds. If so, it's gameover

function check() {
            var bird = document.querySelector('#bird');
            var li = document.querySelectorAll('li');
            li.forEach(function(value,index,arr){
                if(isCrash(arr[index],bird)){
                    alert('gameover');
                    clearInterval(timer);
                    again();
                    return;
                }
            })
        }

Let's take a look at the effect and see if collision detection has been realized:

โ€‹

So all our collision detection has been completed

6. Touch screen events ๐Ÿ’จ

Because the bird only adds click events, we have to add touch screen events if it is under the mobile terminal:

contain.touchstart = function(e) {
        e.preventDefault();
        isDown = false;
        speed = -8;
        var bird = document.querySelector('#bird');
        bird.className = 'birdup';
}

We can copy the code in the click event to the touch screen event, because when we double-click the screen on the mobile terminal, the screen will enlarge, so we need to prevent the default event

It is worth noting that when we first define the style of the pipeline, the left value of the pipeline is 320px, which is the background width under the pc end, but the screen width under the mobile end is different, so we need to delete the default 320px and define the left value of the pipeline as sw in the function generated by the pipeline, because the value of sw under the mobile end is the width of the screen.

7. Make start and end panels ๐Ÿ’จ

Now let's start making the start panel, first write the style of the start panel, and then do this up and down effect. Here we also need to use the animation effect of css

โ€‹

Define two animations, give a move animation to the box on the start panel, so as to achieve the effect of moving up and down, and then add bird animation to the bird's box, so that the bird can flutter its wings

@keyframes bird {
       from {
           background-image: url(img/bird0.png);
       }
       to {
           background-image: url(img/bird1.png);
       }
}
@keyframes move {
       from {
           transform: translateY(-2rem);
       }
       to {
           transform: translateY(2rem);
       }
}

Now we need to give a click event to the ok button on the start panel. When we click the start button, the start panel will not be displayed, then the bird will be displayed, and then the start function will be called to start the game

The following is the ok click event of the start panel. Because there are two btn buttons, one is the start panel and the other is the end panel, btn[0] is the start panel button

var btn = document.querySelectorAll('.but');
btn[0].addEventListener('click',function() {
        var start1 = document.querySelector('#start');
        var bird = document.querySelector('#bird');
        start1.style.display = 'none';
        bird.style.display = 'block';
        start();
})

The end panel doesn't have any animation effect, so after we write the css style, we can add a click event to the ok button. When we click the ok key of the end panel, the end panel will be hidden and the start panel will be displayed. The most important thing is to initialize all global variables. We have written a similar method again before, so we take the original again method directly, Put other statements in and call again directly.

btn[1].addEventListener('click',function() {
        again();
})
function again() {
            bgDis = 0;//bg moving distance
            count = 0;  //Pipe count
            isDown = true;//Determine whether to fly down
            speed = 0;//Control the speed of the bird
            var ul = document.querySelector('ul');
            ul.innerHTML = '';//Empty the pipe
            var bird = document.querySelector('#bird');
            bird.style.top = 100+'px';
            var start1 = document.querySelector('#start');
            var bird = document.querySelector('#bird');
            var end = document.querySelector('#end');
            start1.style.display = 'block';
            bird.style.display = 'none';
            end.style.display = 'none';
        }

When the bird touches the pipe or touches the top and bottom, first pop up the gameover dialog box, and then call the again function to restart the game. But now we have the end panel. What we want to achieve is to replace the original alert with the current end panel. Then we encapsulate a gameover function. In this method, we display the end panel, and at the end of the game, we have to display the statistical score on the end panel (this part will count the score in the next section)

if(bird.offsetTop<0||bird.offsetTop>sh-30) {
        clearInterval(timer);
        gameOver();
        return;
}
function gameOver() {
        var end = document.querySelector('#end');
        end.style.display = 'block';
}

Let's take a look at the implementation effect:
โ€‹

In this way, the production of the start panel and the end panel is completed. Isn't it very silky

8. Score statistics ๐Ÿ’จ

The score should be added dynamically, because the bird will score only when it crosses the pipe, and the left value of the bird is 20px, so only the left value of the pipe plus the width of the pipe is smaller than the left value of the bird, then it means that the bird has crossed the pipe.

We define a global variable score and add one whenever a bird crosses a pipe. Let's make a simple style and focus on whether this effect can be achieved

โ€‹

The score here is the pink box in the figure above. The code is written in the pipe movement function. When the left value of the pipe plus the width of the pipe is smaller than the left value of the bird, we let score + +, and then assign the score to the box to display it:

if(arr[index].offsetLeft+arr[index].offsetWidth<20) {
            score++;
            var scorex = document.querySelector('#score');
            scorex.innerHTML = score;
}

Let's look at the effect:

ย โ€‹

Every time you fly over a pipe, the score is increased by 20. Why? Shouldn't you add one every time?

Because we put the code in the pipeline moving function, and the pipeline moving function is called every 30 milliseconds, as long as the bird flies over the pipeline, it will execute score + + until the pipeline is destroyed. Therefore, we need to add certain restrictions. For example, we can add a user-defined attribute to the pipeline and set its value to 0. If the bird flies over the pipeline, let the attribute value be 1, so that there will be no emptying just now

We set the user-defined attribute able to 0 for the upper pipeline. We don't need to set this attribute for the lower pipeline, otherwise the score will be increased by 2 for each pipeline

if(arr[index].offsetLeft+arr[index].offsetWidth<20) {
         if(arr[index].getAttribute("able") == 0) {
               score++;
               var scorex = document.querySelector('#score');
               scorex.innerHTML = score;
               arr[index].setAttribute("able",'1');
         }
}

After modification, every time the bird passes through the pipeline, the score will be increased by one. Now all we have to do is replace the number with the corresponding picture

We declare a setScore function:

function setScore() {
        var arr = (score + "").split("");
        var str = "";
        for (var i=0; i<arr.length; i++) {
            str += `<img src="img/${arr[i]}.png">`;
        }
        var scorex = document.querySelector('#score');
        scorex.innerHTML = str;
}

Many students here may not understand the first line of the setScore function. Let's look at this Code:

var score = 110;
console.log(typeof score); //number
arr = (score+'');
console.log(typeof arr);//string

A quotation mark after a numeric variable is a string type, so we can use the string method split to turn our scores into an array. For example, if you get 12 points, arr[0] is 1 and arr[1] is 2. The length of the array is 2, because our number is a picture, and the picture name is 1 png,2. png,3. PNG and so on. So now what is stored in str string is

< img SRC = "img / ${arr [0]}. PNG" > < img SRC = "img / ${arr [1]}. PNG" >, so that the picture corresponding to the score can be displayed.

Note: add score = 0 to the again function; Setscore() to initialize the score

Because we need to save the record with the highest score locally, we need to use local storage and add the corresponding functions to the gameOver method:

function gameOver() {
        var end = document.querySelector('#end');
        end.style.display = 'block';
        var socrer = document.querySelector('.score');
        socrer.innerHTML = score;
        if (localStorage.best/1 < score) {
            localStorage.best = score;
        }
        var best = document.querySelector('.best');
        best.innerHTML = localStorage.best;
}

Note: localstorage Best / 1 is because of localstorage Best is a string type and needs / 1 to be converted to numeric type

Here is our final result:

โ€‹

See the students here, please point a praise, thank you!   ๐Ÿ™    ๐Ÿ™    ๐Ÿ™  

Source address:

flappybird game source code and materials

Tags: Front-end Javascript html5

Posted by thorpe on Mon, 18 Apr 2022 19:02:36 +0930