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

项目展示

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

项目准备

在这里插入图片描述

images中所用到的图片

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

index.html

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

<div id="game">
	<!-- 小鸟div -->
	<div class="bird"></div>

	<!-- 开始游戏按钮 -->
	<div class="start start-white">开始游戏</div>

	<!-- 最上层分数 -->
	<div class="score">0</div>

	<!-- mask 蒙层 -->
	<div class="mask"></div>

	<!-- 游戏结束box -->
	<div class="end">
		<div class="over">Game Over</div>
		<div class="results">Your Results</div>
		<div class="final-score">0</div>
		<!-- 排行榜列表 -->
		<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 += `
            <li class="rank-item">
                <span class = "rank-degree ${degreeClass}">${i + 1}</span>
                <span class = "rank-score">${this.scoreArr[i].score}</span> 
                <span class = "rank-time">${this.scoreArr[i].time}</span> 
            </li>
            `;
        }
        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 链接

语雀链接

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Flappy Bird中,小鸟的动画通常包括以下几个状态: 1. 静止状态:小鸟呆在原地,翅膀不动; 2. 上升状态:小鸟向上飞行,翅膀快速振动; 3. 下降状态:小鸟下落,翅膀慢慢扇动。 要实现小鸟的动画,可以使用Python中的Pygame库。具体步骤如下: 1. 加载小鸟的图片资源,并定义小鸟的初始位置; 2. 定义小鸟的状态变量,如上升、下降等; 3. 在游戏主循环中,根据小鸟的状态变量,切换小鸟的不同动画状态,并更新小鸟的位置; 4. 在屏幕上绘制小鸟的当前状态。 下面是一个简单的示例代码: ```python import pygame # 初始化Pygame pygame.init() # 加载小鸟图片资源 bird_imgs = [ pygame.image.load("bird1.png"), pygame.image.load("bird2.png"), pygame.image.load("bird3.png"), ] bird_rect = bird_imgs[0].get_rect() bird_rect.center = (100, 300) # 设置窗口大小 win_size = (288, 512) screen = pygame.display.set_mode(win_size) # 定义小鸟状态变量 is_up = False bird_img_index = 0 bird_y = bird_rect.centery # 游戏主循环 while True: # 处理事件 for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() elif event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: is_up = True # 更新小鸟状态 if is_up: bird_img_index = 1 bird_y -= 5 if bird_y < 0: bird_y = 0 is_up = False else: bird_img_index = 2 bird_y += 3 if bird_y > 512: bird_y = 512 is_up = True # 绘制小鸟 screen.blit(bird_imgs[bird_img_index], bird_rect.move(0, bird_y - bird_rect.centery)) # 更新屏幕 pygame.display.flip() ``` 在这个示例代码中,我们加载了三张小鸟的图片资源,分别对应小鸟的三种状态。在游戏主循环中,根据小鸟的状态变量,切换小鸟的不同动画状态,并更新小鸟的位置。最后,在屏幕上绘制小鸟的当前状态。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值