使用 jQuery 和 100 行 JavaScript 构建 Flappy Bird 游戏






4.97/5 (62投票s)
使用 jQuery 和 100 行 JavaScript 构建 Flappy Bird 游戏
引言
使用 jQuery 和 100 行 JavaScript 构建 Flappy Bird 游戏
背景
Flappy Bird 成为一大新闻:开发者一天收入 50,000 美元。我下载了一个游戏玩了一下——它看起来确实很简单很愚蠢。我把它放在一边。然后我的朋友们又提起同一个话题,开玩笑地说:“你可能只需要几天就能做好。一天 50,000 美元——这可是一笔不错的生意。”我想我应该做一个,但我那时很忙。后来又发生了其他事情,终于当 Apple 开始拒绝 Flappy Bird 的克隆版本,因为实在太多了,我想——“我必须抓住这个机会。只要时机合适,再加一点运气——那每天 50,000 美元就能是我的了。”
我在这里展示的版本,我花了整整 2 个小时才做好,而且只用了不到 100 行 Javascript。
对于那些不知道这个游戏的人——请从你们的山洞里出来。你点击小鸟,给它一个初始向上的速度。然后它开始下落(受到重力作用)。你需要让它保持在空中,并避开它可能撞到的障碍物。为了简化目标,小鸟只上下移动,水平移动的感知是通过滚动背景实现的。就是这样——做好它,每天 50,000 美元就是你的了。
资产
游戏只需要 5 张图片:小鸟、背景草地、背景天空、障碍物和提示点击开始的图片。
正如你所见,为了省去一些麻烦,我通过使用动画 GIF 文件来避免帧动画。这样浏览器可以更有效地使用它。此外,这也是阻止我将其发布到 Windows Phone 的原因——因为那里的浏览器控件不支持动画 GIF 文件。
基础 HTML 也非常简单
<div id='board' style='position:absolute; left:50px; top:50px; width:478px;
height:300px; overflow:hidden;'>
<div id='score' style='position:absolute; left:400px; top:0px; height:25px;
z-index:5; color:red; font-weight:900'></div>
<img class="c" id='bird' src="b2.gif" style="z-index:5"/>
<img id='instr' src='instr.png' class='c' style="left:205px; top:75px;
z-index:100" />
<div class="bg" id='bGrnd' style="top:-20px; height:320px;
background-image:url(bg1.png) "/>
<div class="bg" id='fGrnd' style="top:235px; height:85px; z-index:4;
background-image:url(bg2.png) "/>
</div>
我还包含了来自 https://ajax.googleapis.ac.cn/ajax/libs/jquery/1.11.1/jquery.js 的最新 jQuery全局变量和初始化
游戏使用以下全局变量
bird | 用于保存我们小鸟的 jQuery 对象 |
board | 用于保存游戏板——容器对象的 jQuery 对象 |
dimPipe | 障碍物的尺寸 |
cPos | 小鸟的当前位置(只有 Y 坐标可以改变) |
重力 | 可配置的重力常数——小鸟下落的速度 |
iniSpeed | 可配置的初始速度 |
curSpeed | 小鸟当前的垂直速度 |
score | 当前得分 |
noClr | 已清除的障碍物数量 |
tmStep | 用于定位小鸟和发射障碍物的定时器步长 |
状态 | 游戏状态:0 - 未开始;1 - 进行中;2 - 游戏结束 |
$(document).ready(function() {
bird = $('#bird');
var evt = (typeof(bird[0].ontouchend) == "function")
? "touchstart" : "mousedown";
board = $('#board').bind(evt, onTap);
start();
});
function start() {
state = noClr = score = 0; // not started
cPos = { x: 80, y:100, h:40, w:50 };
bird.css({left:cPos.x, top:cPos.y, width:cPos.w, height:cPos.h, rotate:0});
$('.obs').remove();
$('#instr').show();
}
正如你在 $.ready 中看到的,我们初始化了 bird 和 board 的全局变量,向 board 绑定了点击事件处理程序,并调用了 start() 函数。关于点击:在 Android 设备上,mouseDown 事件比实际的点击发生得晚一些,所以在上面的代码中,我们检查元素是否有 onTouchEnd 事件,并将其用作触摸支持的指示。在 start() 函数中,我重置了所有变量,移除了游戏板上还存在的任何障碍物,并显示了提示点击/触摸的说明图片。
点击/触摸处理
所以游戏已经准备好了。缺失的部分是点击游戏板时会发生什么。此时游戏会启动主定时器(BirdStep)(如果需要),并设置小鸟的初始向上速度。
function onTap() {
if (state > 1) return;
if (state == 0) {
state = 1;
$('#instr').hide();
tmStep = window.setInterval(BirdStep, 30);
}
curSpeed = iniSpeed;
}
需要考虑的是,程序使用 3 种状态:- 0 - 未运行
- 1 - 游戏进行模式
- 2 - 游戏结束模式 - 不接受任何输入。
然而,主要逻辑是在 BirdStep 定时器函数中完成的。
function BirdStep() {
// update bird position
curSpeed += gravity;
cPos.y = Math.max(cPos.y + curSpeed, 0);
var mh = board.height()-cPos.h, m = -12, lo = 0, actPipe = $('.obs');
bird.css({top: cPos.y});
// check if we hit the floor or other obstacles
if (cPos.y > mh)
return gameOver();
for (var i = actPipe.length-1; i >= 0; i--) {
var s = actPipe[i].style, x = parseInt(s.left), y = parseInt(s.top);
lo = Math.max(lo, x);
if (x+dimPipe.width +m < cPos.x || x > cPos.x+cPos.w+m) continue;
if (y+dimPipe.height+m < cPos.y || y > cPos.y+cPos.h+m) continue;
return gameOver();
}
// check if can launch more obstacles
if (actPipe.length > 3 || lo > 300 || Math.random() >= 0.05 * (1+noClr))
return;
var og = cPos.h * 2;
var oh = og + Math.floor(Math.random() * (mh-og+1));
var obs = $("<img /><img />").addClass('c obs').css({left:480, zIndex:3}).css(dimPipe).attr('src', 'vine.png')
.appendTo(board).animate({left:-50}, 3000, 'linear', function() {
$('#score').text(' Score: ' + (score += 1 + Math.floor(++noClr/10)));
this.remove();
});
obs[0].style.top = oh + 'px';
obs[1].style.top = (oh - og - dimPipe.height) + "px";
}
正如你所见,这个函数试图完成 3 项主要任务:更新小鸟位置
每次 BirdStep 定时器执行时,当前小鸟的速度都会因重力而增加,并加到当前小鸟的 Y 坐标上。同时,我在这里检查以确保小鸟不会飞过顶部(负 Y)。命中检测
我们在这里测试小鸟是否掉得太低(Y 超过游戏板高度),或者是否撞到任何障碍物——循环检查小鸟的位置(存储在cPos
中,并减去一些容差 - m = 12px)是否与任何障碍物——类名为 .obs 的任何对象——相交。如果相交,那么游戏就输了——我们可以直接退出。发射新障碍物
首先我们检查是否可以发射新障碍物- 屏幕上已有的障碍物少于 4 个
- 最后一个障碍物移动了一段距离
- 添加一些随机因素
创建完成后,它们会被动画处理,移出屏幕的左边缘(left = -50px),此时分数会增加,障碍物会被移除。为了实现动画,我们使用了简单明了的 jQuery 线性动画。
锦上添花:视差滚动效果
这基本上就是游戏了。但到目前为止,它看起来太普通了。为了增加一些亮点,让我们添加视差滚动效果。实际上,我们添加了 2 个视差层——天空和草地。我们还需要增加深度感知——在这个实现中——天空的移动速度会比草地慢——这应该足够了。要创建视差层,我将创建一个非常宽的 div
元素(16,000px),设置 background-repeat: repeat-x;
,并将所需的图片作为背景。浏览器会将图片水平复制。我需要做的就是添加动画——使用非常方便的 jQuery animate 设置 div
的 left 位置。
function Parallax(elm, tmo) {
elm.css('left', 0).animate({left:-15360}, {
duration:tmo*1000, easing:'linear',
complete : function() { Parallax(elm, tmo); }
});
}
function onTap() {
if (state == 0) {
...
Parallax($('#bGrnd'), 240);
Parallax($('#fGrnd'), 80);
...
}
}
正如你所见,代码出奇地简单:div 的 left 位置被设置为 0px,然后线性动画到 -15,360px(小于 16,000 的所有背景图片宽度的最大公约数——这样我就不必为函数添加额外的参数),之后整个过程会重复。提供的参数是动画的时间——前景(草地)应该滚动 80 秒,背景(天空)——240 秒——慢 3 倍。锦上添花:旋转
除了视差,让小鸟旋转一下会更好——向上飞的时候倾斜,下落的时候向下倾斜。游戏结束后,还可以展示小鸟翻滚。为了实现这一点,我创建了一个简单的 jQuery css hook。请参考 jQuery 文档关于 CSS Hooks 的详细信息。
$.cssNumber.rotate = true;
$.cssHooks.rotate = {
set : function(el, v) {
if (typeof v === 'string')
v = (v.indexOf("rad") != -1) ? parseInt(v) * 180 / Math.PI : parseInt(v);
v = (~~v);
if (v == ($.data(el, 'rotate') || 0)) return;
el.style["MozTransform"] = el.style["MozTransform"] = el.style["-webkit-transform"]
= el.style["transform"] = " rotate(" + (v % 360) + "deg)";
$.data(el, 'rotate', v);
},
get : function(el, computed) {
return $.data(el, 'rotate') || 0;
}
};
正如你在这里看到的,我们将当前旋转值存储在 $.data("rotate")
中,并设置元素的浏览器特定 CSS 属性来设置当前旋转。为了使用新获得的功能,让我们修改
BirdStep
函数,将小鸟的旋转角度设为速度的 5 倍。如果小鸟向上飞且速度为负,小鸟就向上倾斜;如果小鸟在下落且速度为正,小鸟就向下倾斜。除此之外,我们希望将倾斜度限制在 -20 到 90 度之间——这完全是任意的。function BirdStep() { ... var ang = curSpeed * 5; bird.css({top: cPos.y, rotate:(ang < -20) ? -20 : (ang > 90) ? 90 : ang}); ... }我们还可以引入一个漂亮的动画,当小鸟死亡时——它会掉到地上并旋转 540 度,持续一秒钟,然后等待另外半秒。
function gameOver() { state = 2; $(":animated").stop(); if (tmStep) tmStep = window.clearInterval(tmStep); bird.animate({ top:board.height()-cPos.h, rotate:540}, 1000) .animate({ top:board.height()-cPos.h}, 500, function() { $('#score').text(' Score: ' + score); start(); }); }除此之外,正如你所看到的,我们将游戏状态设置为“die”模式,这样在展示动画时我们就不会处理任何点击,停止所有动画(考虑视差滚动和移动障碍物),停止小鸟定时器。之后,我们可以播放“死亡序列”,一旦完成,就回到开始屏幕。
关注点
正如我一开始提到的,这是我花了 2 个小时制作的第一个版本。添加了 PhoneGap,并将其部署到 Galaxy 手机上,结果完全令人沮丧:虽然在最慢的笔记本电脑上运行良好,但在最好的智能手机上却完全卡顿,所以我不得不花费接下来的 2 天时间来尝试提高性能——缓存 jQuery 障碍物对象,处理定时器执行时间与预期不符的问题,等等。即使有了所有这些改进,根据你的智能手机型号,性能可能仍然有点令人失望——请亲自到以下地址查看:
至于每天 50,000 美元——并没有真正实现——在过去 3 个月里,我从那个游戏里赚了 20 美元,或者说我投入的时间每小时大约 1 美元。我再次将我的成功归因于绝佳的时机和一点点运气。幸好我已经有了 Google 开发者账户(一次性 25 美元)、Apple 开发者账户(每年 99 美元)、Microsoft Store(每年 99 美元)和 Web Publishing(每年 49 美元)——否则这可能是一次非常昂贵的经历。但既然我已经付了这些钱——这是我以前没有的 20 美元——杯子已经半满了!