原生JS实现——flappy bird 像素小鸟 项目总结

项目展示

原生JS实现——flappy bird 像素小鸟 项目总结_第1张图片

原生JS实现——flappy bird 像素小鸟 项目总结_第2张图片

原生JS实现——flappy bird 像素小鸟 项目总结_第3张图片

项目准备

原生JS实现——flappy bird 像素小鸟 项目总结_第4张图片

images中所用到的图片

原生JS实现——flappy bird 像素小鸟 项目总结_第5张图片

原生JS实现——flappy bird 像素小鸟 项目总结_第6张图片
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

index.html

用一个 div 来 包裹游戏内容区域

<div id="game">
	
	<div class="bird">div>

	
	<div class="start start-white">开始游戏div>

	
	<div class="score">0div>

	
	<div class="mask">div>

	
	<div class="end">
		<div class="over">Game Overdiv>
		<div class="results">Your Resultsdiv>
		<div class="final-score">0div>
		
		<ul class="rank-list">	
		ul>
		<div class="restart">重新开始div>
	div>
div>

CSS

首先,我们需要有一个样式重置的CSS文件,我们这里直接引入百度的reset.css

重置样式

/* http://meyerweb.com/eric/tools/css/reset/ 
   v2.0 | 20110126
   License: none (public domain)
*/

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed, 
figure, figcaption, footer, header, hgroup, 
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
     
	margin: 0;
	padding: 0;
	border: 0;
	font-size: 100%;
	font: inherit;
	vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, 
footer, header, hgroup, menu, nav, section {
     
	display: block;
}
body {
     
	line-height: 1;
}
ol, ul {
     
	list-style: none;
}
blockquote, q {
     
	quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
     
	content: '';
	content: none;
}
table {
     
	border-collapse: collapse;
	border-spacing: 0;
}

之后我们在 index.html中引入即可

首先,对于我们的游戏区域的div,我们给一个样式

游戏区域样式

#game {
     
    width: 100%;
    height: 600px;
    background: url(../images/sky.png);
    /* 更改background-position-x 可以使背景图片移动 */
    background-position-x: 0px;
    position: relative;
    overflow: hidden;
}

我们在进行游戏的时候,背景图片是会移动的,我们使用 background-position-x 的更改,进行背景图片的移动

小鸟的样式

.bird {
     
    position: absolute;
    width: 30px;
    height: 30px;
    background: url(../images/birds.png);
    left: 50%;
    margin-left: -15px;
    top: 235px;
    /* 过渡效果,top改变触发过度效果, linear表示匀速 */
    transition: top .3s linear;
}

这里的背景图片之所以用birds 而不是 用 bird 是因为,我们小鸟运动的时候,翅膀是会扇动的,如果用bird,我们没法做到扇动的效果,因此,我们用birds来做背景图片,其中含有三个小鸟运动状态的图片,我们只需要更改 background-position-x 的值即可。同时,transition 属性,表示,更改了top 的值,会有一个缓冲的效果。因为刚开始的界面,我们会进行小鸟的上下跳动的动画

start 开始游戏的样式

.start {
     
    width: 200px;
    height: 60px;
    position: absolute;
    left: 50%;
    margin-left: -100px;
    top: 295px;
    line-height: 60px;
    text-align: center;
    font-weight: bolder;
    transition: all .3s linear;
    cursor: pointer;
}

.start.start-white {
     
    color: #fff;
    font-size: 24px;
}

.start.start-blue {
     
    color: #09f;
    font-size: 36px;
}

开始页面的 “开始游戏” 的字样 也会 进行缩放跳动,并且字体颜色也会改变的动画,我们也给一个transition 属性。

score 顶部游戏分数区域样式

.score {
     
    position: absolute;
    text-align: center;
    left: 50%;
    /* transform: translateX(-50%)可以在不知道宽的情况下,达到水平居中 */
    transform: translateX(-50%);
    font-size: 20px;
    font-weight: bolder;
    color: #fff;
    display: none;
    z-index: 1;
}

使用transform来 进行未知宽高的情况下 的 居中操作

优秀结束后的结算蒙层样式

.mask {
     
    position: absolute;
    /* 下面的效果能达到背景平铺父级 */
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background-color: rgba(0, 0, 0, .7);
    display: none;
    z-index: 2;
}

游戏结算后的分数结算&&排行榜列表样式

.end {
     
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    top: 70px;
    display: none;
    text-align: center;
    z-index: 3;
}

.end .over {
     
    color: red;
    font-weight: bolder;
    font-weight: 36px;
}

.end .results,
.end .final-score {
     
    margin-top: 20px;
    font-size: 20px;
    font-weight: bold;
    color: #ff0;
}

/* 排行榜列表 */

.end .rank-list {
     
    color: #09f;
    margin-top: 20px;
}

.end .rank-list .rank-item {
     
    height: 30px;
    line-height: 30px;
    margin-bottom: 10px;
    font-size: 14px;
}

.end .rank-list .rank-item .rank-degree {
     
    display: inline-block;
    width: 14px;
    height: 14px;
    line-height: 14px;
    font-size: 12px;
    color: #fff;
    background-color: #8eb9f5;
    margin-right: 10px;
}

.end .rank-list .rank-degree.first {
     
    background-color: #f54545;
}

.end .rank-list .rank-degree.second {
     
    background-color: #ff8547;
}

.end .rank-list .rank-degree.third {
     
    background-color: #ffac38;
}

.end .rank-score {
     
    width: 30px;
    display: inline-block;
}

.end .restart {
     
    color: #09f;
    font-size: 18px;
    font-weight: bolder;
    cursor: pointer;

}

柱子障碍物样式

.pipe {
     
    position: absolute;
    /* 将固定不变的样式在CSS内写 */
    width: 52px;
    /* background-color: red; */
}

.pipe.pipe-up {
     
    top: 0;
    background-image: url(../images/pipe2.png);
    background-position-y: bottom;
}

.pipe.pipe-down {
     
    background-image: url(../images/pipe1.png);
    bottom: 0;
}

JS逻辑代码

index.js

JS代码注释清楚,就不做多加解释了

// 用对象收编变量的思想

var bird = {
     
    // 添加这些属性到对象中,减少对dom的请求
    // 例如要对 sky背景的background-position-x修改,不必每次都对dom元素操作取值
    skyPosition: 0,
    skyStep: 2, // 控制天空背景移动的速度 2为初始未开始的速度
    birdTop: 235, // bird 元素初始的高度位置
    // birdX: 0, // birdx 雪碧图的初始位置 用于后期扇动翅膀 
    stratColor: 'white',
    startFlag: false, // 判断游戏是否开始,初始未开始时是 false
    birdSetpY: 0, // 小鸟 加速 降落事件中的初始步长
    minTop: 0, // 小鸟活动范围的边界(上界)
    maxTop: 570, // 小鸟活动范围的边界(下界) 600 - 30 
    timer: null,
    pipeLength: 7, // 想生成的柱子的个数
    pipeArr: [],
    pipeLastIndex: 6, // 最后一根柱子的索引
    score: 0,
    scoreArr: [],

    init: function () {
     
        this.initData();
        this.animate();
        this.handleStart();
        this.handleClick();
        this.handleReStart();

        if (getSession('play')) {
     
            this.start();
        }
    },

    // 初始化函数
    initData: function () {
     
        // 使用this 使得bird内的成员均可以使用下面的属性
        this.el = document.getElementById('game');
        this.oBird = this.el.getElementsByClassName('bird')[0];
        this.oStart = this.el.getElementsByClassName('start')[0];
        this.oScore = this.el.getElementsByClassName('score')[0];
        this.oMask = this.el.getElementsByClassName('mask')[0];
        this.oEnd = this.el.getElementsByClassName('end')[0];
        this.oFinalScore = this.oEnd.getElementsByClassName('final-score')[0];
        this.oRankList = this.el.getElementsByClassName('rank-list')[0];
        this.oReStart = this.el.getElementsByClassName('restart')[0];

        this.scoreArr = this.getScore();
        // console.log(this.scoreArr);
    },

    // 获取Local Storage 中的数据
    getScore: function () {
     
        var scoreArr = getLocal('score');
        // 防止获取的是 null 无法进行push
        return scoreArr ? scoreArr : [];

    },

    // 运动函数
    animate: function () {
     
        // 减少整个JS文件中定时器的使用次数,将skyMove birdJump等事件放入animate 函数的定时器中一同运动
        var self = this;
        var count = 0;
        this.timer = setInterval(function () {
     
            self.skyMove();
            // 游戏开始
            if (self.startFlag) {
     
                self.bridDrop();
                self.pipeMove();
            }
            if (++count % 10 === 0) {
     
                // 游戏未开始
                if (!self.startFlag) {
     
                    self.startBound();
                    self.birdJump();
                }
                self.birdFly(count);
            }
        }, 30);
    },

    // 天空背景运动函数
    skyMove: function () {
     
        // setInterval 内的回调函数 中的this 指向 指向的是window  因此 要更改this指向
        // 或者使用ES6 的箭头函数
        // var self = this;
        // setInterval(function () {
     
        //     // 考虑长远点,当我们的天空背景如果运动的速度更改,应该如果操作
        //     // self.skyPosition -= 2;
        //     // 使用变量进行改变
        //     self.skyPosition -= self.skyStep;
        //     self.el.style.backgroundPositionX = self.skyPosition + 'px';
        // }, 30)

        // 上述的注释是未来减少 定时器 的使用 因此 skyMove 只写事件处理
        this.skyPosition -= this.skyStep;
        this.el.style.backgroundPositionX = this.skyPosition + 'px';
    },

    // 开始之前小鸟的跳动(上下跳动)
    birdJump: function () {
     
        this.birdTop = this.birdTop === 220 ? 260 : 220;
        this.oBird.style.top = this.birdTop + 'px';
    },

    // 小鸟扇动翅膀 小鸟fly
    birdFly: function (count) {
     
        // this.birdX -= 30;
        // 使用计数器,count,使得其能与上述表达式达到一样效果,减少变量的使用
        this.oBird.style.backgroundPositionX = count % 3 * -30 + 'px';
    },

    // 开始游戏之后,不点击鼠标事件,小鸟自由落下(加速降落)
    bridDrop: function () {
     
        // this.oBird.style.top = 
        this.birdTop += ++this.birdSetpY;
        this.oBird.style.top = this.birdTop + 'px';
        this.judgeKnoke();
        this.addScore();
    },

    // 分数累加
    addScore: function () {
     
        var index = this.score % this.pipeLength;
        var pipeX = this.pipeArr[index].up.offsetLeft;
        if (pipeX < 13) {
      // 13 柱子正好越过小鸟的距离
            // this.score++;
            this.oScore.innerText = ++this.score;
        }
    },

    // 判断是否撞天 or 地 or 柱子
    judgeKnoke: function () {
     
        // 判断是否撞到边界
        this.judgeBoundary();
        // 判断是否撞到柱子
        this.judgePipe();
    },

    // 判断 撞到边界的函数
    judgeBoundary: function () {
     
        if (this.birdTop <= this.minTop || this.birdTop >= this.maxTop) {
     
            this.failGame();
        }
    },

    // 判断 是否撞到柱子
    judgePipe: function () {
     
        // 柱子数组下标索引
        var index = this.score % this.pipeLength;
        var pipeX = this.pipeArr[index].up.offsetLeft;
        var pipeY = this.pipeArr[index].y; // []
        var birdY = this.birdTop;

        if ((pipeX <= 95 && pipeX >= 13) && (birdY <= pipeY[0] || birdY >= pipeY[1])) {
     
            this.failGame();
        }
    },

    // 生成柱子
    createPipe: function (x) {
     
        // 上下柱子之间的距离相等 150 px
        // 所以上下柱子的长度最合理的范围是 (600 - 150)/2 = 225
        // 控制范围在50~175

        // var upHeight = 50 + Math.floor(Math.random() * 175);
        // var downHeight = 450 - upHeight;
        // var oDiv = document.createElement('div');
        // oDiv.classList.add('pipe');
        // oDiv.classList.add('pipe-up');
        // oDiv.style.height = upHeight + 'px';
        // oDiv.style.left = x + 'px';
        // this.el.appendChild(oDiv);

        // var oDiv1 = document.createElement('div');
        // oDiv1.classList.add('pipe');
        // oDiv1.classList.add('pipe-down');
        // oDiv1.style.height = downHeight + 'px';
        // oDiv1.style.left = x + 'px'
        // this.el.appendChild(oDiv1);

        // 上述代码冗余太多,耦合度太高,需要进行封装一下

        var upHeight = 50 + Math.floor(Math.random() * 175);
        var downHeight = 450 - upHeight;
        // createEle函数存放在函数工具库 utils.js 中
        var oUpPipe = createEle('div', ['pipe', 'pipe-up'], {
     
            left: x + 'px',
            height: upHeight + 'px',
        });

        var oDownPipe = createEle('div', ['pipe', 'pipe-down'], {
     
            height: downHeight + 'px',
            left: x + 'px'
        });

        this.el.appendChild(oUpPipe);
        this.el.appendChild(oDownPipe);

        // 将柱子放在pipe函数中,方便后序的读取
        this.pipeArr.push({
     
            up: oUpPipe,
            down: oDownPipe,
            y: [upHeight, upHeight + 150 - 30] // 小鸟过柱子上下运动的安全距离
        })

    },

    // 柱子移动函数
    pipeMove: function () {
     
        for (var i = 0; i < this.pipeLength; i++) {
     
            var oUpPipe = this.pipeArr[i].up;
            var oDownPipe = this.pipeArr[i].down;
            // 使得 柱子和背景的运动速度一样
            var x = oUpPipe.offsetLeft - this.skyStep;


            if (x < -52) {
     
                // clearInterval(this.timer);
                var lastPipeLeft = this.pipeArr[this.pipeLastIndex].up.offsetLeft;
                oUpPipe.style.left = lastPipeLeft + 300 + 'px';
                oDownPipe.style.left = lastPipeLeft + 300 + 'px';
                // 改变最后一个柱子的索引值,将柱子可以连续移动
                this.pipeLastIndex = i;
                continue;
            }
            oUpPipe.style.left = x + 'px';
            oDownPipe.style.left = x + 'px';
        }
    },

    // '开始游戏' 这四个字 放大缩小
    startBound: function () {
     
        // 将之前的颜色保存到变量中
        var prevColor = this.stratColor;
        this.stratColor = this.stratColor === 'blue' ? 'white' : 'blue';
        this.oStart.classList.remove('start-' + prevColor);
        this.oStart.classList.add('start-' + this.stratColor);
    },

    // 监听 Start 的事件函数
    handleStart: function () {
     
        this.oStart.onclick = this.start.bind(this);
    },
    start: function () {
     
        var self = this;
        self.oScore.style.display = 'block';
        self.oStart.style.display = 'none';
        self.skyStep = 5;
        // 点击游戏开始,游戏上锁
        self.startFlag = true;

        self.oBird.style.left = '80px';
        // 让小鸟元素 更改top值时候的 过渡效果取消
        self.oBird.style.transition = 'none';

        for (var i = 1; i <= self.pipeLength; i++) {
     
            self.createPipe(300 * i);
        }
    },
    // 监听父元素 被点击的事件 控制小鸟往上飞
    handleClick: function () {
     
        var self = this;
        this.el.onclick = function (e) {
     
            var dom = e.target;
            // 事件委托,当事件源对象为 start‘开始游戏’时候,小鸟不往上飞10px
            var isStart = dom.classList.contains('start');
            if (!isStart) {
     
                self.birdSetpY = -10;
            }
        }
    },

    // 重新开始点击事件
    handleReStart: function () {
     
        this.oReStart.onclick = function () {
     
            // 不能用local storage 来判断是否玩过游戏
            // 如果玩过游戏,点重新开始,直接进入游戏
            // 如果没玩过游戏,进入开始页面,需要点击开始游戏
            setSession('play', true);
            window.location.reload();
        }
    },

    // 游戏结束函数
    failGame: function () {
     
        // console.log('end');
        clearInterval(this.timer);
        this.setScore();
        this.oMask.style.display = 'block';
        this.oEnd.style.display = 'block';
        this.oScore.style.display = 'none';
        this.oBird.style.display = 'none';
        this.oFinalScore.innerText = this.score;
        this.renderRankList();
    },

    // 结束时候设置分数,并将分数保存到Local Storage
    setScore: function () {
     
        this.scoreArr.push({
     
            score: this.score,
            time: this.getDate(),
        })
        this.scoreArr.sort(function (a, b) {
     
            return b.score - a.score;
        })
        var scoreLength = this.scoreArr.length;
        this.scoreArr.length = scoreLength > 8 ? 8 : scoreLength;
        setLocal('score', this.scoreArr);
    },

    // 获取时间信息
    getDate: function () {
     
        var d = new Date();
        var year = d.getFullYear();
        var month = formatNum(d.getMonth() + 1);
        var day = formatNum(d.getDate());
        var hour = formatNum(d.getHours());
        var minute = formatNum(d.getMinutes());
        var second = formatNum(d.getSeconds());
        return `${
       year}.${
       month}.${
       day} ${
       hour}:${
       minute}:${
       second}`;
    },


    // 设置排行榜列表
    renderRankList: function () {
     
        var template = '';
        for (var i = 0; i < this.scoreArr.length; i++) {
     
            var degreeClass = '';
            switch (i) {
     
                case 0:
                    degreeClass = 'first';
                    break;
                case 1:
                    degreeClass = 'second';
                    break;
                case 2:
                    degreeClass = 'third';
                    break;
                default:
                    break;
            }
            template += `
            
  • ${ degreeClass}">${ i + 1} ${ this.scoreArr[i].score} ${ this.scoreArr[i].time}
  • `
    ; } this.oRankList.innerHTML = template; }, } bird.init();

    utils.js

    // 存放工具函数  已经封装好的函数
    
    function createEle(eleName, classArr, styleObj) {
         
        var dom = document.createElement(eleName);
        // 类名赋值
        for (var i = 0; i < classArr.length; i++) {
         
            dom.classList.add(classArr[i]);
        }
        // 属性赋值
        for (var key in styleObj) {
         
            dom.style[key] = styleObj[key];
        }
        return dom;
    }
    
    
    function setLocal(key, value) {
         
        if (typeof value === 'object' && value !== null) {
         
            value = JSON.stringify(value);
        }
        localStorage.setItem(key, value);
    }
    
    function getLocal(key) {
         
        var value = localStorage.getItem(key);
        if (value === null) {
         
            return null;
        }
        if (value[0] === '[' || value[0] === '{') {
         
            return JSON.parse(value);
        }
        return value;
    }
    
    function setSession(name, value) {
         
        sessionStorage.setItem(name, value);
    };
    
    function getSession(name) {
         
        return sessionStorage.getItem(name);
    }
    
    //  将个位数转为双位,如 8 变为 08
    //  @param {Number|String} number - 要转换的数字
    
    function formatNum(number) {
         
        if (number < 10) {
         
            return '0' + number;
        }
        return number;
    }
    

    end

    github 链接

    语雀链接

    你可能感兴趣的:(前端,js,游戏,css,html,面试)