原生JS实现FlappyBird游戏 超详细解析

举报
脖子右拧 发表于 2022/04/21 17:30:47 2022/04/21
【摘要】 ​ 目录​1.适配设备🐾2.背景滚动💐3.管道的创建与移动🌸4.小鸟操作🌷5.碰撞检测🍀6.触屏事件🌹7.制作开始与结束面板🌻8.得分统计🌺我们先来看看接下来我们要做的效果:🙋🙋🙋​有需要源码和素材的同学,在文章末尾有链接。 1.适配设备💨PC端下背景320px*568px(游戏背景图片大小),移动端下占满窗口新建一个public.js文件,这个文件放一些我们公共的方...

 目录

1.适配设备🐾

2.背景滚动💐

3.管道的创建与移动🌸

4.小鸟操作🌷

5.碰撞检测🍀

6.触屏事件🌹

7.制作开始与结束面板🌻

8.得分统计🌺


我们先来看看接下来我们要做的效果:🙋🙋🙋

有需要源码和素材的同学,在文章末尾有链接。 

1.适配设备💨

PC端下背景320px*568px(游戏背景图片大小),移动端下占满窗口

新建一个public.js文件,这个文件放一些我们公共的方法,下面我们先定义一个isPhone方法来判断是否是移动端设备

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;
}

在isPhone方法里我们定义了一个数组arr用来存储移动端的设备名,UserAgent是HTTP请求中的用户标识,一般发送一个能够代表客户端类型的字符串,indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置,如果要检索的字符串值没有出现,则该方法返回 -1

我们默认是PC端,如果indexOf不返回-1说明与数组中的元素匹配,代表是移动端设备,那么我们的isPhone方法就返回true。

这个判断移动端的方法大家可以保存下来,以后很多的项目我们也用的到

因为我们规定移动端下背景图片要占满屏幕,所以需要一个if语句进行判断,如果isPhone返回的是true,说明当前在移动端,我们需要修改背景图片的宽高:

sw和sh是在在外面定义的全局变量,默认情况下sw=320,sh=568,因为在后面我们还会用到sw,sh,所以如果设备是移动端的话,需要对它们进行重新赋值。

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 就是当前设备的屏幕宽度,注意加符号

我们可以在chrome浏览器下模拟移动端不同设备下是否占满全屏,每次换完设备时要刷新页面: 

这样的话我们适配设备的效果就完成了,成功做到了可以在移动端下占满全屏,下面就开始制作我们的flappybird游戏吧!

2.背景滚动💨

在下面的代码中bg是我们获取的最外层装背景图片的盒子,背景图片是在x轴平铺的,所以我们只需要一个定时器不断调用背景移动函数就行,背景移动函数里我们每次调用背景的位置就向左移动5像素

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

在我们制作的这个游戏中,不论是背景移动还是待会要做的管道的移动,小鸟的移动,最后封装的函数都需要在这个定时器里调用,这样才会有我们看到的那种动画一样的效果。 

3.管道的创建与移动💨

在实现管道的移动之前我们先需要创建管道,因为我们要让生成的管道高度不一致,所以需要先写一个随机数函数,我们就在public.js里完成:

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

我们先整理一下创建管道的思路:

  1. 先写管道的样式,html与css部分的代码在文章最后都有,大家在看解析的时候要先看一眼html结构和css样式。 
  2. 规定上下管道间隔120px,通过定义好的rand随机函数实现上管道高度随机,背景图片高度减去间隔减去上管道高度就是下管道高度,这里下管道高度不要给随机。
  3. 通过insertAdjacentHTML将生成管道的代码添加到ul里

 因为管道也是不断生成的,我们需要在timer定时器里调用管道移动函数pipeMove():

var timer = setInterval(function(){
        bgMove();
        pipeMove();
},30)
我们在外面定义这个pipeMove方法,在pipeMove里完成管道的创建与移动
function pipeMove() {
       //1.创建管道
       createPipe();
       //2.管道移动
}

下面先来根据在开头写的思路来完善管道创建函数createPipe: 

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);
}

运行代码看一看管道有没有被创建出来:

很明显管道数量太多了啊,因为定时器每隔三十毫秒就会调用管道创建函数,所以管道生成的就非常多,我们定义一些全局变量进行限制:

var space = 100;  //创建管道的间隔
var count = 0;  //管道的计数

修改一下createPipe函数,当计数达到创建管道的间隔100时才执行下面创建管道的代码,否则不执行,这样就对生成管道的数量进行了限制

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);
}

现在管道可以在背景的右面不断的生成,这样管道的创建就全部完成了,下面在pipeMove方法里继续完善管道移动的部分:

function pipeMove() {
            //1.创建管道
            createPipe();
            //2.管道移动
            var li = document.querySelectorAll('li');
            li.forEach(function(value,index,arr){
                arr[index].style.left = arr[index].offsetLeft-2+'px';
            })
        }

我们先获取创建的所有管道,然后通过foreach循环每次调用都让管道左移两像素,管道就能成功移动起来了。

注意:直接通过obj.style.left和obj.style.top可以获取位置,但是有局限性,这种获取的方法只能获取到行内样式的left和top的属性值,不能获取到style标签和link 外部引用的left和top属性值。所以这里用offsetleft获取

然后我们再给管道加一个边界,让他超出背景时就在ul里删除这个元素:

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

我们运行代码看一看效果:


 

这样管道的创建与运动就基本上完成了,下面我们开始小鸟的操作。

4.小鸟操作💨

首先我们先把小鸟的html结构和css样式搭建好,下一步就是让小鸟“动起来”。

同样我们需要在定时器中调用小鸟移动函数birdMov

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

因为游戏开始的时候小鸟要向下掉落所以先写一个小鸟的移动函数:

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

这样的话,小鸟就实现了一直匀速下落的效果,但是游戏中我们的小鸟并不是匀速的所以我们还需要定义一个全局变量speed,初始化它的值为0,来控制小鸟的速度。

因为我们在游戏中单击屏幕时小鸟会向上飞而且向上飞的速度和向下的速度不相同,所以我们在全局声明一个isDown变量,来判断小鸟是否向下飞。默认isDown = true,因为小鸟不操作的话一定是向下飞的。

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';
}

如果不点击屏幕每隔三十毫秒小鸟的速度就增加0.4,然后用一个三元表达式,如果速度达到8那么就是小鸟的极限速度就不再增加了,最后我们把原来的5这个固定值换成speed就实现了小鸟速度的动态变化

接下来我们要实现的是在单击背景的任一处时小鸟能够向上移动,所以我们需要给背景图片一个点击事件:

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

当我们点击屏幕时,小鸟要向上飞,所以isDown被赋为false,然后立刻给一个向上的位移距离为8

我们运行一下代码看看效果: 

是不是已经有那么点感觉了,但是小鸟点击屏幕的时候头会向上抬,所以还得在点击屏幕的时候改小鸟的背景图片

我们创建两个类,一个birddown类里面是小鸟头向下的图片,一个birdup类里面是小鸟头向上的。

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

然后给bird添加默认样式类birddown,这样当我们点击屏幕时,就修改bird的类为birdup:

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

这样的话我们在点击屏幕的时候小鸟就从头向下变成头向上了,但是如果不点击屏幕的时候小鸟还是应该回到默认向下的样式,因为不点击屏幕小鸟就会向下飞,那我们想想这个怎么实现呢?

那小鸟什么时候向下飞呢,就是speed为0的时候,我们每次点击屏幕的时候小鸟的速度都是-8,但是我们一直在调用birdmove,每次speed都加0.7,这样向上的速度总会越来越小然后当大于0 的时候小鸟就向下飞。

这样我们就在birdmove里补全代码实现点击屏幕小鸟就抬头向上飞,下降就低头向下飞:

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';
        }

我们看一下效果: 

这样我们小鸟的动作就基本写完了,现在需要当小鸟触顶和触底的时候应该让游戏gameover。

把下面判断边界的代码放在birdmove里,这样每次先判断一下是否超出边界,如果超出的话就直接gameover并清除定时器然后执行again函数重新开始游戏。

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

再外面创建这个again函数来实现重新开始游戏:

function again() {
        bgDis = 0;//bg的移动距离
        count = 0;  //管道的计数
        isDown = true;//判断是否向下飞
        speed = 0;//控制小鸟的速度
        var ul = document.querySelector('ul');
        ul.innerHTML = '';//清空管道
        var bird = document.querySelector('#bird');
        bird.style.top = 100+'px';
        start()
}

我们在again函数里要重新初始化一些变量,这里有些变量如管道间隔或者背景宽高是不需要再次初始化的。并且不用带var,因为如果带var了就是局部变量,但这里我们要改变的是全局变量。

然后我们需要清空所有画面中的管道,也就是ul里的内容。然后把小鸟的高度恢复到最开始的距离顶端20像素的位置。

最后调用了一个start函数,这个start函数就是把最开始的计时器给封装了:

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

因为在游戏结束的时候我们清空了计时器,所以重新开始的时候我们得再次调用这个计时器。

注意:计时器变量timer不应该加var,因为封装后加了var的在函数里就不是全局变量了

这样当我们的小鸟在触顶或者触底的时候就会弹出gameover对话框点击确定然后就重新开始游戏

然后我们把原来写的birddown类和birdup类修改为动画

@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);
            }
        }

从bird0到bird1就是小鸟的翅膀有个变化,这样加上动画后小鸟就像在飞动翅膀。 

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

5.碰撞检测💨

如何判断触顶或者触底对我们来说并不难,但是如何判断小鸟和管道相撞呢?

下面我们回到public.js文件里写一下这个碰撞检测函数isCrash,这个函数同样是复用性很高的。

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) {
        // 不碰撞
        return false;
    } else {
        // 碰撞
        return true;
    }
}

在if语句里只要有一个条件不满足就说明不会碰撞,这个很好理解,这里我们就分析一下为什么r2<l1就说明不会碰撞呢?如果这里a代表管道,b代表小鸟,那么l1就是管道到左侧背景的距离,l2代表小鸟到背景左侧的距离,那么r2<l1的意思就是小鸟本身的宽度再加上小鸟到背景左侧的距离比管道到背景左侧的距离还小,这样二者肯定不会碰上,所以其他方向同理。

然后我们在开始函数里再调用一下check()函数:

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

check()函数通过调用isCrash来看所有的管道和小鸟有没有碰撞,如果有的话就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;
                }
            })
        }

我们来看一看效果,看看碰撞检测有没有实现:

这样我们所有的碰撞检测都完成了

6.触屏事件💨

因为小鸟只添加了点击事件,所以如果移动端下的话我们还得添加触屏事件:

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

我们把点击事件里的代码复制到触屏事件里就可以啦,因为在移动端下我们双击屏幕时屏幕会放大,所以我们要阻止默认事件

值得注意的是我们在最开始定义管道的样式时管道的left值是320px,是我们在pc端下的背景宽度,但是在移动端下屏幕宽度不一,所以我们要把默认的320px删掉,在管道生成的函数里定义管道的left值为sw,因为我们在移动端下sw的值就是屏幕的宽度。

7.制作开始与结束面板💨



下面我们先开始制作开始面板,先写完开始面板的样式,然后要做的就是这种上下动的效果,这里还需要我们再用到css的动画效果

定义两个动画,给开始面板的盒子一个move动画这样就实现了上下动的效果,再给小鸟的盒子添加bird动画,这样小鸟就可以扑哧翅膀了

@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);
       }
}

现在我们需要给开始面板的ok按钮一个点击事件,当我们点击开始按钮时要不显示开始面板,然后再显示小鸟,再调用start函数来开始游戏

下面是开始面板的ok点击事件,因为btn按钮有两个一个是开始面板的一个是结束面板的所以btn[0]就是开始面板的按钮

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();
})

结束面板没有什么动画效果,所以我们写完css样式后,就给ok按钮添加点击事件就行,当点击结束面板的ok键时,结束面板隐藏起来,开始面板显示出来,而且最重要的是还要初始化所有全局变量,而我们之前写过一个类似的方法again,所以我们把原来的again方法直接拿过来,把其他的语句放进去直接调用again就行。

btn[1].addEventListener('click',function() {
        again();
})
function again() {
            bgDis = 0;//bg的移动距离
            count = 0;  //管道的计数
            isDown = true;//判断是否向下飞
            speed = 0;//控制小鸟的速度
            var ul = document.querySelector('ul');
            ul.innerHTML = '';//清空管道
            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';
        }

当小鸟碰到管道或者触顶触底时,先弹出gameover对话框再调用again函数来重新开始游戏。但是现在我们有了结束面板,我们想实现的是把原来的alert换成现在的结束面板,那么我们再封装一个gameover函数,在这个方法里面我们显示结束面板并且游戏结束的时候我们还得统计得分显示在结束面板上(这个部分在下一节统计得分)



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

我们看一下实现效果:

这样开始面板和结束面板的制作就都完成了。是不是非常丝滑呀

8.得分统计💨

得分我们应该是动态添加的,因为只有当小鸟越过管道的时候才会得分,而小鸟的left值是20px,所以只需要管道的left值加上管道的宽度比小鸟的left值小,那么就代表小鸟越过了管道。

我们定义一个全局变量score,每当小鸟越过一个管道score就加一,我们先简单制作一下样式重点看看这个效果能不能实现

这里的scorex就是上图的粉色框,代码写在管道移动函数里当管道的left值加上管道的宽度比小鸟的left值小时我们让score++,然后把得分赋给盒子让他显示出来:

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

我们看看实现的效果:

 

每次飞过一个管道得分却加了二十,这是为什么呢,不应该每次都加一么?

因为我们把代码放在了管道移动函数里,而管道移动函数每三十毫秒就会被调用一次,所以小鸟只要飞过了管道,就会一直执行score++直到管道被销毁。所以我们需要加一定的限制条件,比如我们可以给管道都加一个自定义属性,设置它的值为0,如果小鸟飞过管道后就让这个属性值为1,这样就不会出现刚才的清空了

我们给上管道设置自定义属性able为0,我们不需要给下管道也设置这个属性,要不然每经过一个管道score就会加2了

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');
         }
}

修改之后,每次小鸟经过管道分数就会加一,现在我们要做的就是把数字换成对应的图片就行

我们声明一个setScore函数:

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;
}

这里很多同学可能对setScore函数里的第一行不太理解,我们看这段代码:

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

数值型变量后面加一个引号就是字符串型,这样我们就能用字符串方法split把我们的得分变成数组。比如得了12分的话,arr[0]就是1,arr[1]就是2。数组长度就是2,因为我们的数字是一张张图片,图片名就是1.png,2.png,3.png以此类推。所以现在str字符串里存储的就是

<img src="img/${arr[0]}.png">  <img src="img/${arr[1]}.png">,这样对应分数的图片就能显示出来。

注意:在again函数里要加上score=0;setScore()来初始化得分

因为我们需要把得分最高的记录保存到本地,所以需要用到本地存储,把相应的功能加在gameOver方法中:

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;
}

注意:这里localStorage.best/1是因为localStorage.best是字符串类型,需要/1来转化为数值型

下面就是我们的最终成果啦: 

看到这里的同学麻烦点个赞谢谢啦! 🙏  🙏  🙏 

源码地址:

flappybird游戏源码及素材


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。