原生JS实现飞机大战游戏 超详细解析 快来做一个自己玩吧

Posted YinJie…

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原生JS实现飞机大战游戏 超详细解析 快来做一个自己玩吧相关的知识,希望对你有一定的参考价值。

目录

1. 案例分析💨

2. 适配设备 💨

3. 背景滚动💨

4. hero操作💨

5. 敌机的创建与运动💨

6. 子弹的创建与运动💨

7. 碰撞检测💨

8. 统计得分💨

9. 设置开始与结束界面💨

10. 设置带本地存储功能的排行榜💨

 

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

 

 

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

下面是另外两个原生JS的游戏和项目,大家可以选择阅读,都有详细解析:

原生JS实现FlappyBird游戏:原生JS实现FlappyBird游戏 超详细解析 快来做一个自己玩吧

原生JS实现本地存储记账本:我用JS做了一个记账本 [数据可本地存储] 附万字详解

1. 案例分析💨

我们先思考一下这个游戏都由哪几个部分或者说哪些功能组成呢?

  • 开始前 :一个开始游戏面板 
  • 游戏中:背景滚动  hero的操作  敌机的创建与运动  子弹的创建与运动  碰撞检测
  • 游戏结束:一个排行榜面板

 

2. 适配设备 💨

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

function isPhone() 
    var arr = ["iPhone","iPad","android"];
    var is = false;
    for (var i=0; i<arr.length; i++) 
        if (navigator.userAgent.includes(arr[i])) 
            is = true;
            break;
        
    
    return is;

在isPhone方法里我们定义了一个数组arr用来存储移动端的设备名,UserAgent是HTTP请求中的用户标识,一般发送一个能够代表客户端类型的字符串,includes 方法判断数组是否包含指定的值,包含返回 true,不包含返回 false。

我们默认是PC端,如果includes返回true则代表与数组中的元素匹配,代表是移动端设备,那么我们的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浏览器下模拟移动端不同设备下是否占满全屏,每次换完设备时要刷新页面: 

 

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

 

3. 背景滚动💨

游戏背景是最外层盒子 container 的背景图片,背景图片是在y轴上平铺的,所以我们通过定时器改变背景图片的y轴位置就能达到持续滚动的效果。

我们在实现各个子模块功能的时候都写到单独的文件里,下面我们创建一个背景滚动文件 bg.js ,把相关功能的实现写在这里:

// 背景滚动
var dContainer = document.getElementById("container");
var dis = 0;  //bg滚动的量
var speed = 5;  //滚动的速度
function bgMove() 
    dis += 5;
    dis = dis>sh ? 0 : dis;
    dContainer.style.backgroundPosition = `0 $dispx`;

我们在 index.html 中定义一个定时器,然后每隔30毫秒调用 bgMove 这个方法:

function start() 
    timer = setInterval(function() 
        // 2.1 背景滚动
        bgMove();
        , 30)

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

我们看一下背景移动的效果:

这样背景的滚动就完成了,下面我们开始进行 hero 的操作。

 

4. hero操作💨

我们新建一个控制 hero 移动的 js 文件:hero.js

然后分为三步

1 .获取装hero飞机的盒子

2. 添加键盘事件,判断按下的状态

3. 封装移动函数

这里需要着重强调的就是第二步,我们先看一下代码:(这里37.38.39.40是阿斯克码分别代表左上右下键)

var isLeft = false;
var isTop = false;
var isRight = false;
var isBottom = false;
//键盘按下事件
window.onkeydown = function(e) 
    if (e.keyCode === 37) 
        isLeft = true;
     else if (e.keyCode === 38) 
        isTop = true;
     else if (e.keyCode === 39) 
        isRight = true;
     else if (e.keyCode === 40) 
        isBottom = true;
      

//键盘抬起事件
window.onkeyup = function(e) 
    if (e.keyCode === 37) 
        isLeft = false;
     else if (e.keyCode === 38) 
        isTop = false;
     else if (e.keyCode === 39) 
        isRight = false;
     else if (e.keyCode === 40) 
        isBottom = false;
      

这里每当按下键盘或者键盘抬起的时候,我们都会判断相应的状态,如果没有这一步,我们实现不了飞机向左上飞或者向右上飞,只能要么竖着上下飞,要么横着左右飞。

这里我们再看看hero移动的函数就能更好的理解了:

var dHero = document.getElementById("hero");
function heroMove() 
    var left = dHero.offsetLeft;
    var top = dHero.offsetTop;
    if (isLeft) 
        left -= 8;
        left = left<-33 ? -33 : left;
    
    if (isTop) 
        top -= 8;
        top = top<0? 0 : top;
    
    if (isRight) 
        left += 8;
        left = left>sw-33 ? sw-33 : left;
    
    if (isBottom) 
        top += 8;
        top = top>sh-82 ? sh-82 : top
    
    dHero.style.left = left + 'px';
    dHero.style.top = top + 'px';

当我们按下左移键时,isLeft等于true,当我们按下上移键时,isTop等于true,所以在移动函数heroMove里,前两个if都会被执行,这样就实现了向左上方飞的效果。

把 hero 的操作函数添加到定时器中:

function start() 
        timer = setInterval(function() 
            // 2.1 背景滚动
            bgMove();
            // 2.2 hero的操作  pc键盘
            heroMove();
        , 30)
    

下面我们来看一下效果:

 这样我们就实现了通过上下左右键对 hero 的操作

 

5. 敌机的创建与运动💨

在实现敌机的创建之前,因为我们要让生成的敌机实现随机分布,所以需要先写一个随机数函数,我们就在public.js里完成:

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

创建一个 enemy.js文件编写敌机的创建与运动,首先我们写一个创建敌机的函数:

var dEnemy = document.getElementById("enemy");
function createEnemy() 
    var d = document.createElement("div");
    d.className = "enemy";
    d.style.left = rand(0,sw-38) + 'px';
    d.speed = rand(3,8);
    dEnemy.appendChild(d);

这里我们首先获取 enemy 元素,enemy盒子是作为装载生成敌机的父盒子,类 enemy 就是给创建的 div 盒子增加了敌机的背景,因为最外层的背景盒子我们给了他一个相对定位,然后把装载敌机的盒子一个绝对定位。这样才能让敌机在背景上移动,在类 enemy 里我们定义所有生成的敌机的 top 值都是负的,让敌机从背景外向内移动。然后把创建的div盒子作为 dEnemy 的孩子添加进去。rand函数是创建的一个返回随机数的函数。第三行语句是为了让敌机生成在背景的水平方向的任意位置上,然后让生成的敌机速度也是随机的,减去38是因为我们创建的敌机的宽度是38。

接下来我们看一下敌机的运动函数:

// 敌机的创建于运动
var dEnemy = document.getElementById("enemy");//通过概率来限制敌机的创建与游戏难度
var diff = 200; //难度系数
//敌机运动
function enemyMove() 
    // 1. 敌机的创建
    if (rand(0,diff) <= 10) 
        createEnemy()
    
    // 2. 敌机的运动
    var es = dEnemy.children;
    for (var i=0; i<es.length; i++) 
        var e = es[i];
        if (e.offsetTop > sh) 
            // 飞出了屏幕,需要删掉
            dEnemy.removeChild(e);
            i --;  //防止漏掉元素
            continue;
        
        e.style.top = e.offsetTop + e.speed + 'px';
    

在敌机创建部分我们用了一个if语句,因为我们在通过定时器调用这个函数时,大概每秒钟会调用三十次,那样的话每次调用都创建一个敌机,敌机的数量就太多了。rand(0,200) <= 10意思就是是原来二十分之一的概率,这样生成的敌机数量正好。

还有一个值得注意的点是,当敌机飞出屏幕时,我们需要把敌机这个元素删点,那为什么要i--呢?

比如我们的敌机数组有四个元素,现在判断的是第二个元素,也就是i等于1,当我们移除掉这个元素后,原来的第三个元素就到了我们移除的第二个元素的位置上来。但是因为for循环还会进行一个i++的操作,这样i就等于2了,就是数组的第三个元素。但这其实是第四个元素,因为我们已经把第二个元素删掉了,所以就漏掉了第三个元素,就需要进行一个i--操作来防止漏掉元素。

把 enemyMove 方法添加到主页定时器中:

function start() 
        timer = setInterval(function() 
            // 2.1 背景滚动
            bgMove();
            // 2.2 hero的操作  pc键盘
            heroMove();
            // 2.3 敌机的创建与运动
            enemyMove();
        , 30)
    

我们看一下效果:

 

6. 子弹的创建与运动💨

我们创建一个 bullet.js 文件,子弹的创建和上一节中敌机的创建是很相似的:

function createBullet() 
    var dHero = document.getElementById("hero");
    var d = document.createElement("div");
    d.className = "bullet";
    d.style.left = dHero.offsetLeft + 33 - 3 + 'px';
    d.style.top = dHero.offsetTop + 'px';
    dBullet.appendChild(d);

只不过子弹的定位是跟 hero 相关的,所以子弹的 top,left值需要用到 hero 的位置,' 33 -3 '那里前面介绍过33是指 hero 飞机宽度的一半,而3就是子弹宽度的一半,这样就能保证子弹是从飞机头的那个位置发射出来的。 

接下来我们再完成子弹的运动函数:

//子弹运动及创建
var dBullet = document.getElementById("bullet");
// 使用间隔
var space = 7;
var count = 0; //计数
//子弹运动
function bulletMove() 
    count ++;
    // 1. 子弹的创建
    if (count === space) 
        createBullet();
        count = 0;
    
    // 2. 子弹的运动
    var bs = dBullet.children;
    for (var i=0; i<bs.length; i++) 
        var top = bs[i].offsetTop;
        if (top <= -14) 
            dBullet.removeChild(bs[i]);
            i-- ;
            continue;
        
        bs[i].style.top = top - 9 + 'px';
    

在子弹的移动函数中我们调用子弹的创建函数,通过 space 和 count 两个变量来控制子弹的生成频率,要不然子弹每隔30毫秒就生成一个就太快了。然后我们让子弹在超出边界后就自动销毁。

我们把这个方法和之前一样加到主页的定时器中:

    function start() 
        timer = setInterval(function() 
            // 2.1 背景滚动
            bgMove();
            // 2.2 hero的操作  pc键盘
            heroMove();
            // 2.3 敌机的创建与运动
            enemyMove();
            // 2.4 子弹的创建与运动
            bulletMove();
        , 30)
    

启动项目,看一下子弹的效果:

这样我们子弹的创建与运动就完成了,下一步就该判断子弹命中敌机后,销毁敌机的操作了。

 

7. 碰撞检测💨

我们在这一节要实现子弹与敌机相碰时,子弹和敌机都会销毁,如果 hero 和敌机相撞那就游戏结束了。首先我们创建一个 check.js 文件,在这里定义上述功能。

下面先理解一下判断是否碰撞的函数:

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 就说明不会碰撞呢? l1 代表飞机到左侧背景的距离, l2 代表敌机到背景左侧的距离,那么 r2 < l1 的意思就是敌机本身的宽度再加上敌机到背景左侧的距离比飞机到背景左侧的距离还小,这样二者肯定不会碰上,所以其他方向同理。 

定义 check 函数判断敌机与hero,敌机与子弹是否碰撞:

function check() 
    // 1. hero与敌机
    // 2. 子弹与敌机
    var es = dEnemy.children;
    var bs = dBullet.children;
    for(var i=0; i<es.length; i++) 
        var e = es[i];
        // 英雄与敌机
        if (isCrash(dHero, e)) 
            // gameover
            alert('ganmeover');
            clearInterval(timer);
        
        // 子弹与敌机
        for (var j=0; j<bs.length; j++) 
            var b = bs[j];
            if (isCrash(e,b)) 
                // 1. 子弹消失
                dBullet.removeChild(b);
                // 2. 敌机消失
                dEnemy.removeChild(e);
                i --;
                break;
            
        
    

在 check 方法中我们调用 isCrash 方法校验英雄与敌机,子弹与敌机是否碰撞,如果英雄与敌机碰撞,我们就清除主页定时器,并执行 gameover 的弹窗。然后通过两个 for 循环,先遍历所有敌机,再对每一个子弹遍历,判断是否子弹和敌机碰撞,如果二者碰撞那就通过 removeChild 把移除元素。

将 check 方法加入定时器中:

function start() 
        timer = setInterval(function() 
            // 2.1 背景滚动
            bgMove();
            // 2.2 hero的操作  pc键盘
            heroMove();
            // 2.3 敌机的创建与运动
            enemyMove();
            // 2.4 子弹的创建与运动
            bulletMove();
            // 2.5 碰撞检测
            check();
        , 30)

运行项目,看一下效果能否都实现:

在子弹在和敌机碰撞时,就达到了消灭敌机的效果,并且 hero 在与敌机相撞时也会弹窗提示游戏结束,这样我们游戏的主体部分就完成了,剩下的就是一个让人头疼的带本地存储功能的计分和排行榜功能了。

 

8. 统计得分💨

我们设置当子弹击毁敌机的时候得分就加一,得分会在游戏界面的左上角显示出来,这一节我们主要实现得分的这个功能,显示与样式这里先不关注。

因为在子弹和敌机碰撞的时候得分才会加一,所以这个功能应该添加在上一节的 check 方法之中

先在 check.js 中获取元素,定义得分变量 score:

var score = 0; //得分
var pScore = document.getElementById("score");

这里 pScore 获取的就是游戏界面左上角装载得分的盒子

然后是得分的逻辑实现:

for (var j=0; j<bs.length; j++) 
            var b = bs[j];
            if (isCrash(e,b)) 
                // 1. 子弹消失
                dBullet.removeChild(b);
                // 2. 敌机消失
                dEnemy.removeChild(e);
                // 3. 加分
                score ++;
                pScore.innerHTML = "得分:" + score;
                // 4. 处理数据
                i --;
                break;
            
        

现在当子弹命中敌机的时候,左上角的得分就会相应的加一。

 

9. 设置开始与结束界面💨

在游戏开始的时候应该先设置一个开始界面,然后可以输入昵称,这样方便后续结束游戏的时候设置排行榜。

下面是我们定义的开始界面,样式和 html 结构这里就不展示了,我们主要关注功能的实现:

单击开始按钮的时候就会隐藏开始界面,然后调用 start 函数,star函数封装了定时器 timer :

    startBut.onclick = function() 
        if (iptNick.value === "") 
            alert("昵称不能为空");
            return ;
        
        dStart.style.display = 'none';
        start();
    

开始界面设置完后,我们就实现结束界面,先看一下结束界面的效果:


在结束界面需要我们把最终得分还有排行榜输出出来,这里我们先不关系排行榜如何设置,先实现游戏结束的功能,当点击再来一次的时候,结束面板就会隐藏,弹出开始面板,因为我们知道结束面板的弹出和 hero 与敌机相撞这个事件是绑定的,所以我们可以把这些功能放在一个 gameover 函数中,当触发事件就调用这个函数。

在 index.html 中我们定义一个 gameover 函数:

//游戏结束
function gameover() 
        //停止计时
        clearInterval(timer);
        //修改本次得分 
        pShowScore.innerHTML = score;
        // 设置排行榜
        setPHB();
        // 显示结束面板
        dEnd.style.display = "block";
    

如果游戏结束的话一定要先清除定时器 timer ,否则游戏还会继续进行,然后把最终得分展示在结束面板,然后设置排行榜,这里先定义一个 setPHB 方法,下一节我们再完善里面的功能,最后再显示结束面板,这样 gameover 函数就完成了。

当敌机与hero相撞时,调用gameover函数:

// 英雄与敌机
if (isCrash(dHero, e)) 
        // gameover
        gameover();

下面我们实现单击再来一次重新开始游戏的效果

首先肯定是点击它的时候让结束面板隐藏,显示开始面板,我们定义一个 again 方法:

    function again() 
        dEnd.style.display = "none";
        dStart.style.display = "block";
    

但是这就完事了么?很明显没有,因为当你每次重新开始游戏的时候都应该让 hero 在起始的中间位置,我们再定义一个 setHeroPosition 方法:

var dHero = document.getElementById("hero");

//重新定位hero的位置
function setHeroPosition() 
    dHero.style.left = (sw-66)/2 + 'px';
    dHero.style.top = sh - 82 + 'px';

这个方法我们把它定义在 hero.js 文件中。

那现在重新开始游戏能正常实现了么?也没有,因为我们还得恢复所有数据:

    againBut.onclick = function() 
        again();
        //数据还原
        dis = 0;
        count = 0;
        dBullet.innerHTML = "";
        score = 0;
        pScore.innerHTML = "得分:0";
        dEnemy.innerHTML = "";
        setHeroPosition();
    

在 index.html 中定义这个点击事件,先调用前面定义过的 again 方法,然后把所有我们计数用的变量初始化,再把画面中的所有子弹和敌机删除,最后调用 setHeroPosition 方法实现 hero 归位。

至此我们开始界面与结束界面的全部功能就都实现了。

 

10. 设置带本地存储功能的排行榜💨

我们先想一下这个排行榜应该怎么做,正常就是数据以对象存储在数组里,然后遍历显示在结束面板上。但是这样的话,如果我们刷新页面,所有的数据就被销毁了,那我们这个排行榜也就没有意义了,所以这里要通过 localStorage 本地存储实现。

在 gameover 函数中我们声明了一个 setPHB 方法,现在我们用 localStorage 来实现这个方法:

 function setPHB() 
        if (!localStorage.phb) 
            localStorage.phb = "[]";
        
        var arr = JSON.parse(localStorage.phb);
        var isExit = -1; //昵称是否存在  -1表示不存在 
        for (var i=0; i<arr.length; i++) 
            if (arr[i].nick === iptNick.value) 
                // 存在
                isExit = i;
                break;
            
        
        if (isExit != -1) 
            // 更新数据
            arr[isExit].score = score;
         else 
            //将新数据放入数组
            arr.push(
                nick: iptNick.value,
                score: score
            );
        
        // 排序
        arr = arr.sort(function(a, b) 
            return b.score - a.score;
        )
        //设置ul的内容
        setUl(arr);

        //将新数据存入到本地
        localStorage.phb = JSON.stringify(arr);
    

如果有小伙伴看不太懂,那听我先分析分析:

最开始那个 if 语句是什么意思?

它的意思就是因为localstorage本身就是存在的,所以我们只需要判断localStorage.phb存不存在,不存在就把他赋值为一个空数组。因为我们第一次游戏的话浏览器中肯定没有localStorage.phb,所以我们把它设置为空数组就行。

为什么这里的空数组还有带上引号呢? 

因为本地存储只能存储字符串,可以将对象JSON.stringify()编码后存储,或者通过JSON.parse()解析后获取数据

JSON.parse和JSON.stringify都是啥意思?

形象点说,就是JSON.parse方法可以把带字符串的玩意去掉字符串符号,比如原来是 " abc ",经过JSON.parse方法就能变成 abc。JSON.stringify()就是把这个过程反了过来。

通过 JSON.parse(localStorage.phb) 我们把本地存储的数据解码后拿出来,赋给变量 arr 。然后我们定义了一个变量 isExit 。如果排行榜上小张得了十分,下一次小张继续游戏得了十五分的话,那就得在排行榜上更新小张的得分记录,所以这里 isExit 就是干这个的。通过一个 for 循环判段 arr 数组里是否有开始界面输入的昵称相同的,有的话就通过索引更新数据,没有的话就把新的数据放进数组中。然后通过 sort 方法把数组排序,排行榜我们只取前三名。定义一个 setUI 方法把前三名记录在结束面板的排行榜上显示出来,最后再把数组 arr 通过 JSON.stringify() 给编码再存储回去。

下面我们完善 setUI 的代码,实现排行榜的显示效果:

    function setUl(arr) 
        ul.innerHTML = "";
        for (var i=0; i<arr.length; i++) 
            if (i > 2) 
                break;
            
            var li = document.createElement("li");
            li.innerHTML = `
                    <span>$i+1.</span>
                    <span>$arr[i].nick</span>
                    <span>$arr[i].score</span>
            `;
            ul.appendChild(li);
        
    

执行 setUI 的时候,我们先把排行榜清空重新排列,当 i>2 的时候说明是三名往后,就不用执行了直接 break 。排行榜的每条记录通过 innerHTML 插入排名,昵称和得分。

这样我们的飞机大战就全部完工啦!

 

源码地址:

https://gitee.com/jie_shao1112/aircraft-warhttps://gitee.com/jie_shao1112/aircraft-war

 

原生JS实现FlappyBird游戏 超详细解析 快来做一个自己玩吧

目录

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 = `$bgDispx 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游戏源码及素材

以上是关于原生JS实现飞机大战游戏 超详细解析 快来做一个自己玩吧的主要内容,如果未能解决你的问题,请参考以下文章

纯原生JS使用ES6语法实现飞机大战

纯原生JS使用ES6语法实现飞机大战

基于Java的飞机大战的课程设计与实现

前端案例:飞机大战( js+dom 操作,代码完整)

JAVA 实现《飞机大战-III》游戏

[知了堂学习笔记]_纯JS制作《飞机大战》游戏_第1讲(实现思路与游戏界面的实现)