若是说学编程就是学逻辑的话,那锻炼逻辑能力的最好方法就莫过于写游戏了。最近看了一位大神的fly bird小游戏,感受颇有帮助。因而为了寻求进一步的提升,我花了两天时间本身写了一个canvas版本的。虽然看起来原理都差很少,可是实现方法截然不同,若是有兴趣的话能够你们本身下载下来玩一玩,大概效果就像下面这样:
怎么样?是否是感受难度巨大?...多是由于我比较菜吧。相信高手仍是大有人在的,随便过个几十关也是不在话下。可是若是有和我同样10关都过不了小菜鸡的话,根本不用丧气对吧?咱是程序员是否是?游戏不会玩,做弊还不会吗?咳咳,下面就是做弊的方法:javascript
<style> *{ margin: 0; padding: 0; } html,body { height: 100%; width: 100%; overflow: hidden; } #canvas{ display: block; margin: 50px auto; } </style>
<canvas id="canvas" width="343" height="480"></canvas>
很简单,就是这样。css
// 图片集合 var imgs = { //建立图片 bg: new Image(), grass: new Image(), title: new Image(), bird0: new Image(), bird1: new Image(), up_bird0: new Image(), up_bird1: new Image(), down_bird0: new Image(), down_bird1: new Image(), startBtn: new Image(), up_pipe: new Image(), up_mod: new Image(), down_pipe: new Image(), down_mod: new Image(), scroe0:new Image(), scroe1:new Image(), scroe2:new Image(), scroe3:new Image(), scroe4:new Image(), scroe5:new Image(), scroe6:new Image(), scroe7:new Image(), scroe8:new Image(), scroe9:new Image(), //加载图片 loadImg: function (fn) { this.bg.src = './img/bg.jpg'; this.grass.src = './img/banner.jpg'; this.title.src = './img/head.jpg'; this.bird0.src = './img/bird0.png'; this.bird1.src = './img/bird1.png'; this.up_bird0.src = './img/up_bird0.png'; this.up_bird1.src = './img/up_bird1.png'; this.down_bird0.src = './img/down_bird0.png'; this.down_bird1.src = './img/down_bird1.png'; this.startBtn.src = './img/start.jpg'; this.up_pipe.src = './img/up_pipe.png'; this.up_mod.src = './img/up_mod.png'; this.down_pipe.src = './img/down_pipe.png'; this.down_mod.src = './img/down_mod.png'; this.scroe0.src = './img/0.jpg'; this.scroe1.src = './img/1.jpg'; this.scroe2.src = './img/2.jpg'; this.scroe3.src = './img/3.jpg'; this.scroe4.src = './img/4.jpg'; this.scroe5.src = './img/5.jpg'; this.scroe6.src = './img/6.jpg'; this.scroe7.src = './img/7.jpg'; this.scroe8.src = './img/8.jpg'; this.scroe9.src = './img/9.jpg'; var that = this; //添加定时器,判断图片是否加载完成 var timer = setInterval(function() { if (that.bg.complete&&that.grass.complete &&that.title.complete&&that.startBtn.complete &&that.bird0.complete&&that.bird1.complete &&that.up_bird0.complete&&that.up_bird1.complete &&that.down_bird0.complete&&that.down_bird1.complete &&that.up_pipe.complete&&that.up_mod.complete &&that.down_mod.complete&&that.down_pipe.complete &&that.scroe0.complete&&that.scroe1.complete &&that.scroe2.complete&&that.scroe3.complete &&that.scroe4.complete&&that.scroe5.complete &&that.scroe6.complete&&that.scroe7.complete &&that.scroe8.complete&&that.scroe9.complete) { //删除定时器 clearInterval(timer); //图片所有加载完成后,运行此函数 fn(); } }, 50) } }
...抱歉有点长,可是怕破坏代码的结构,就所有拷下来了,上面的朋友快点下来吧,都是重复的没啥好看的。我来给你们解释一下,首先这是一个对象字面量,建立的时候新建了若干个图片对象,而后它有一个函数loadImg,只要一执行,就会给全部的图片添加路径,而后添加一个定时器每一段时间经过查询全部图片的complete属性判断图片是否所有加载完成。若是是,就删除这个定时器,并执行一段回调函数,仍是很好理解的吧:),不过我感受这种方法可能有点蠢,不知道各位高人有没有更好的方法?html
你们都知道,其实canvas就是画图,若是要用canvas实现动画效果的话,就只能一遍一遍的擦了画、画了擦了。java
先把几个固定不动的部分的绘制方法和清空画布的方法写在函数里git
//绘制背景 function drawBg() { ctx.drawImage(imgs.bg,0,0); } //绘制开始按钮 function drawStartBtn() { ctx.drawImage(imgs.startBtn,130,300); } //清空画布 function clean() { ctx.clearRect(0,0,canvas.width,canvas.height); }
把会动的部分也加上程序员
var v = 0;//草坪滚动的增量 //绘制草坪 function drawGrass() { //每次运行横坐标向左移 ctx.drawImage(imgs.grass,3*v--,423); ctx.drawImage(imgs.grass,337+3*v--,423); if(3*v < -343){ v=0; } }
这样每次运行一次,草坪就会向左移一点了github
var shake = true;//标题的抖动状态 //标题的抖动效果 function titleShake() { if (shake) { ctx.drawImage(imgs.title,53,97); ctx.drawImage(imgs.bird1,250,137); }else{ ctx.drawImage(imgs.title,53,103); ctx.drawImage(imgs.bird0,250,143); } }
这样经过改变shake的值,就可使标题的抖动了。
机智的各位应该已经发现了,上面两个函数须要重复调用,才能产生动画的效果,因此这就是我接下来要讲的。编程
var startTimer;//开始界面定时器 var startTime = 0;//定时器运行的次数 function startLayer() { startTimer = setInterval(function () { clean(); drawBg(); drawStartBtn(); drawGrass(); titleShake(); //定时器每运行7次改变标题位置 if(startTime == 7){ shake = !shake; startTime = 0; } //运行次数+1 startTime++; //window.requestAnimationFrame(startLayer) }, 24); }
你们也能够理解为这就是开始界面,由于开始界面就是经过定时器一次次运行上面的函数所实现的。然而上面定义的startTimer和startTime又有什么用呢,固然不是画蛇添足,首先,把这个定时器赋给一个变量,是为了在开始游戏的时候把这个界面关掉,也就是把这个定时器取消,日后看你们就明白了:)其次,startTime是为了记录定时器运行的次数,由于这个定时器刷新的实现极快,只有短短的24毫秒,若是标题以这个速度抖动的话,你们的眼睛必定受不了了吧,因此我设法让他慢下来,每运行7次抖动一次,固然你们能够设置九、十、11使它的频率更加缓慢(你们还能够尝试使用requestAnimation-
-Frame,那样性能更佳,可是控制频率略显麻烦。这里使用setInterval更容易理解)固然这个做弊没有半毛钱关系,不过下面就是重头戏了。canvas
var bird = { bird: [imgs.bird0,imgs.bird1],//正常状态,图片 up_bird: [imgs.up_bird0,imgs.up_bird1],//向上飞状态 down_bird: [imgs.down_bird0,imgs.down_bird1],//向下掉状态 posX: 100,//横坐标 posY: 200,//纵坐标Y speed: 0,//速度 index: 0,//翅膀挥动,切换图片的标 alive: true,//存活状态 //绘制小鸟 draw: function (bird) { ctx.drawImage(bird,this.posX,this.posY); }, //飞行中 fly: function () { //纵坐标随速度改变 this.posY+=this.speed; //加速度为1 this.speed++; //若是坠地,死亡 if(this.posY >= 395){ this.speed = 0; this.draw(this.bird[this.index]); this.dead(); } //若是撞顶,弹回来 if(this.posY <= 0){ this.speed = 6; } //若是速度为正,则向下,反之,则向上,不然水平 if(this.speed>0){ this.draw(this.down_bird[this.index]); }else if(this.speed<0){ this.draw(this.up_bird[this.index]); }else{ this.draw(this.bird[this.index]); } //确保坠落速度不会太快 if(bird.speed > 6){ bird.speed = 6; } }, //煽动翅膀,切换图片 wingWave: function () { this.index++; if(this.index > 1){ this.index = 0; } }, //死亡 dead: function() { this.alive = false; } }
...固然这只是主角的代码,一个对象字面量。可是它能够操控主角的全部行为(虽然也没有几个行为...),首先就是画出主角draw(),经过传进不一样的图片绘制出主角不一样状况下的英姿...而后是wingWave(),经过改变index,切换上面定义的图片数组中的图片,也就是挥翅膀。再而后就是飞行fly(),在飞行过程当中主角会碰到各类各样的事故,像是飞的过高撞到天花板啊,或是飞的过低,摔了个狗啃屎。再干脆点一头撞死在了钢管上,可是这个函数并不在这里,由于小鸟撞死在钢管上究竟是小鸟的行为,仍是钢管的行为呢,我还没想明白,因此干脆放在了全局中。数组
//判断是否碰撞 function isHit(oPipe){ if(bird.posX+bird.bird[0].width>oPipe.posX&&bird.posX<oPipe.posX+oPipe.down_pipe.width){ if(bird.posY<oPipe.up_posY||bird.posY+30>oPipe.down_posY){ bird.dead(); } } }
就像这样,经过判断小鸟和钢管的位置判断小鸟是否是撞在钢管上了。反正结果仍是撞死bird.dead()。看到这里相信不用我说,你们也明白了吧,只要将这段代码注释掉,咱们的小鸟不就练成的绝世铁头功,钢管都捅穿给你看。或者稍稍增大一点小鸟会被碰撞到的体积,那就是凌波微步、轻功管上飘了呀。说了半天,还没告诉你们这个水管又是哪里来的。
//水管类 class Pipe { constructor(up_pipe,up_mod,down_pipe,down_mod) { //构造函数 this.up_pipe = up_pipe;//上水管头部 this.up_mod = up_mod;//上水管中间部分 this.down_pipe = down_pipe; this.down_mod = down_mod; this.up_height = Math.floor(Math.random()*60);//随机生成上管体高度 this.down_height = (60 - this.up_height)*3;//保证全部上下水管距离相同 this.posX = 300;//横坐标 this.up_posY = this.up_height*3+this.up_pipe.height;//上水管纵坐标 this.down_posY = 362-this.down_height;//下水管纵坐标 this.hadSkipped = false;//是否被越过 this.hadSkippedChange = false;//去重 } //绘制水管 drawPipe() { ctx.drawImage(this.up_pipe,this.posX,this.up_height*3); ctx.drawImage(this.down_pipe,this.posX,362-this.down_height); } //绘制管体 drawMods() { for(var i=0;i<this.up_height;i++){ ctx.drawImage(this.up_mod,this.posX,i*3) } for(var j=0;j<this.down_height;j++){ ctx.drawImage(this.down_mod,this.posX,362-this.down_height+this.down_pipe.height+j); } } //水管移动 move() { this.posX -= 6; this.drawMods(); this.drawPipe(); } }
又是一段冗长的代码,你们不要急躁,我来给你们详细解释,水管分为两部分,一部分是固定的管口,还有一部分是为了控制钢管长度的管体,在上面的图片也能够看到,每一关的管道是分为上下两个的——up_pipe和down_pipe,也就是说咱们看到的钢管是由数个相同的管体加管口构成的,这里管体的数量是随机的,这样就可使管道拥有随机的长度了。而后为了保证上下两个钢管的中间距离固定,下管道的高度就是总高度减去上管道的高度,嗯,这里须要理一理,你们也能够直接去看个人代码。有了上面的理论,接下来就简单了,绘制管口drawPipe(),注意给管体预留出位置来,再绘制管体drawMods(),用一个for循环依次绘制出数个管体叠加在一块儿的样子。水管移动move(),就是改变水管的横坐标了。这里能够经过改变上下水管高度的总值,来增长上下水管之间的距离,是否是游戏难度一下就降了不少?再有就是判断水管是否被小鸟跨越的hadskiped属性,往下看
//判断是否越过水管 function isSkipped(oPipe) { if(bird.posX>oPipe.posX+oPipe.down_pipe.width){ //水管已经被越过 oPipe.hadSkipped = true; //确保水管只被越过一次 if(!oPipe.hadSkippedChange&&oPipe.hadSkipped){ //分数+1 scroll++; oPipe.hadSkippedChange = true; } } }
我是经过判断水管的位置是否已经位于小鸟的后面来判断,小鸟是否越过了水管的,若是越过了就+1分,至于没越过就是经过前面讲过到的isHit()判断了,由于不是同一时间段发生的事情因此不能放在一块儿。
var scroll = 0;//当前得分 var scrollImg = [imgs.scroe0,imgs.scroe1,imgs.scroe2, imgs.scroe3,imgs.scroe4,imgs.scroe5, imgs.scroe6,imgs.scroe7,imgs.scroe8, imgs.scroe9];//存储数字图片 //绘制当前得分 function drawScore() { //每绘制一位数,向右移23,绘制下一位数 for(var i=0;i<scroll.toString().length;i++){ ctx.drawImage(scrollImg[parseInt(scroll.toString().substr(i,1))],147+i*23,40) } }
首先,把全部分数有关的图片放到这里scrollImg来,方便使用。而后判断数字的位数,也就是个十百千万。循环并截取每一个位数,再经过相应的图片绘制出来,而且每绘制一个位数的图片位置向右移23,这样数字就不会叠在一块儿了。这里有一种最没意思的做弊方法,就是手动调整分数,但这只是一个数字,游戏的乐趣果真仍是在于过程,下面...
//游戏界面 function gameLayer() { gameTimer = setInterval(function () { clean(); drawBg(); drawGrass(); if(gameTime%5 == 0){ if(gameTime == 30){ createPipes(); gameTime = 0; } bird.wingWave(); } gameTime++; for(var i = 0;i< pipes.length;i++){ pipes[i].move(); isHit(pipes[i]); isSkipped(pipes[i]); } drawScore(); bird.fly(); //若是小鸟死了 if(!bird.alive){ gameOver();//游戏结束 reset();//数据重置 } }, 24); }
...看到这里,估计已经有人在骂我了,讲了半天游戏还没开始...好吧,大家看,其实游戏的界面也不过是一个定时器,将前面讲到的函数和代码,无脑的、重复的执行着。而后这里必定要注意画图的顺序,否则后画的部分会把前面覆盖掉,其次这里的gameTimer和gameTime也和开始界面中startTimer、startTime起到相似的做用,每过一段较长的时间生成一个水管,也就是经过水管类实例化一个水管对象,具体的方法被我封装进一个createPipes函数里了。
var pipes = [];//用于存放水管 function createPipes() { var pipe = new Pipe(imgs.up_pipe,imgs.up_mod,imgs.down_pipe,imgs.down_mod); //添加进pipes中,若是已经有三个水管,则依次替换 if(pipes.length<3){ pipes.push(pipe); }else{ pipes[index] = pipe; index++; if(index >= 3){ index = 0; } } }
由于实现的方法没有想象中那么简单,首先咱们要创造一个水管的数组,它的做用就是为了控制水管的数量,否则咱们的定时器就会一遍一遍的创造出无数的水管,可是前面的水管早就离咱们远去,因此我就用数组把水管装起来,控制只有一个屏幕的水管,也就是三个。若是建立了超过三个水管,就会把最前面一个替换掉,由于它已经超出了咱们的视野。
光有动画也不行,只能看不能玩有个皮用啊。因此咱们固然要添加响应事件了。
//键盘点击事件 function kd(e) { if (e.keyCode === 32) { bird.speed = -10; } } //触屏事件 function ts() { bird.speed = -10; } //start按钮点击事件 function startBtn_click(e) { //判断点击位置 if(e.clientX>canvas.offsetLeft+canvas.width/2-imgs.startBtn.width/2 &&e.clientX<canvas.offsetLeft+canvas.width/2+imgs.startBtn.width/2 &&e.clientY<canvas.offsetTop+300+imgs.startBtn.height &&e.clientY>canvas.offsetTop+300){ clean(); //清除开始界面定时器 clearInterval(startTimer); gameLayer(); //添加响应事件 window.addEventListener('keydown',kd,false) window.addEventListener('touchstart',ts,false) //删除start按钮响应事件 canvas.removeEventListener('click',startBtn_click,false); } } canvas.addEventListener('click', startBtn_click , false);
这就是全部的响应事件了,经过按空格键和点击屏幕均可以改变小鸟的速度,只要把这个速度调整到一个比较舒服的程度,游戏难度就会大大下降。其次,由于canvas是一个总体,因此咱们没有办法直接监听里面图片按钮的响应事件,只能退而求其次,判断点击的位置是否在按钮的位置上了,就上面那段有点长的if判断语句。
假如咱们的主角真的一个不当心如咱们所料的撞死在了钢管上(往上翻,就在游戏开始那里),那就表示gameOver();
//游戏结束 function gameOver(){ //清除定时器 clearInterval(gameTimer); //清除窗口响应事件 window.removeEventListener('keydown',kd,false); window.removeEventListener('touchstart',ts,false); //绘制GAME OVER ctx.font = "50px blod"; ctx.fontWeight = '1000' ctx.fillStyle = "white"; ctx.fillText("GAME OVER", 20, 200); drawStartBtn(); }
整个世界都平静了下来,定时器关掉,响应事件移除掉,而后绘上大大的、惨白的GAME OVER,下面附带一个游戏开始时就出现的start按钮。不是有一句话说的是,结束不过是新的开始吗,你又能够再来一局了。......好吧,这个就是我为了偷懒随便搞搞的。不过这还没完,数据还得重置一下,否则怎么从新开始。
//重置数据 function reset(){ bird.posY = 200; bird.speed = 0; bird.alive = true; pipes = []; scroll = 0; canvas.addEventListener('click', startBtn_click , false); }
最后再给这个start按钮添加上点击事件,大功告成!这就是我调整难度以后的样子:
啧啧啧,这种闲庭信步的感受......
果真游戏仍是有点难度才有意思......
吁...一篇又臭又长、废话又多的文章终于写完了,若是你们以为有帮助,或者对这篇文章有兴趣的话,就赏个赞。若是以为个人程序有问题,或者有别的想说的,均可以在评论里告诉我,我会看的。
个人项目地址:https://github.com/tzc123/can...
参考项目地址:http://www.jianshu.com/p/45d9...