用一个 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文件,我们这里直接引入百度的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 {
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 {
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代码注释清楚,就不做多加解释了
// 用对象收编变量的思想
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();
// 存放工具函数 已经封装好的函数
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;
}
github 链接
语雀链接