Flappy Bird项目代码

学了一段时间的Canvas 有必要实战一下,这是一套视频课程中的项目,拿来练练手,毕竟编程只看不写等于不会。就好像学汉字,总共常用的也就那几千个,每个字也都认识,学霸们能够写出大作,学渣们却连一篇像样的作文都写不出来。所以,最主要的是多写,多练,借鉴别人的代码结构,如何巧妙使用,如何构造,自己能写出来,能理解,能运用才算自己。长点心,没有什么学不会。共勉,以上都是说给自己的。 .~_~.

先看下什么是Flappy Bird:Flappy Bird百度百科 , 好像还有个floppy bird ,大概意思就是无精打采的小鸟。先玩两局感受下。

先实现背景的飞翔
背景的效果


> 1.仅绘制天空

<body>
    <!-- width="500" height="500" -->
    <canvas id="cvs"> </canvas>
    <script src="./loadImage.js">
    </script>
    <script>
    var cvs = document.querySelector('#cvs');
    var ctx = cvs.getContext('2d');

    function Sky(ctx, img, step) {
        this.ctx = ctx;
        this.img = img;
        this.width = this.img.width;
        this.height = this.img.height;

        // 这里是绑定在类上的属性,那么由该类创建的实例共享该属性
        Sky.objNum++;

        // 该案例总共创建了两个实例,第一个放在画布上,第二个在第一个的右侧,宽高相等
        this.x = this.width * (Sky.objNum - 1);

        this.y = 0;
        // 移动的速度,每隔100ms移动20
        this.step = step || 20;
    }

    Sky.prototype = {
        constructor: Sky,
        draw: function() {
            this.ctx.drawImage(this.img, this.x, this.y);
        },
        update:function(){
            // 两个sky背景每隔一定时间100ms,同时往左移动,当第一个移出画布时(this.x <= -this.width),第一个的x坐标往右移两个宽度(注意,这里创建了两个sky实例)
            this.x -= this.step;
            if (this.x <= -this.width) {
                this.x += this.width *Sky.objNum;
            }
        }
    }

    Sky.objNum = 0;

    </script>

    <script>
    loadImage({
        bird: './img/bird.png',
        land: './img/land.png',
        pipeDown: './img/pipeDown.png',
        pipeUp: './img/pipeUp.png',
        sky: './img/sky.png'
    }, function(imgObj) {
        // console.log(imgObj.bird.src);
        // console.log(imgObj.sky.src);

        // 画布的大小和背景图片的大小一样大
        cvs.width = imgObj.sky.width;
        cvs.height = imgObj.sky.height;

        var sky1 = new Sky(ctx, imgObj.sky);
        var sky2 = new Sky(ctx, imgObj.sky);

        setInterval(function() {
            sky1.draw();
            sky1.update();

            sky2.draw();
            sky2.update();
        }, 100);
    })
    </script>
</body>

2.天空中加入bird

这只鸟儿自动下落,点击画布,往上升一点。算法比较简单。

    <script>
    function Bird(ctx, img, widthFrame, heightFrame, x, y) {
        this.ctx = ctx;
        this.img = img;

        // 给的素材图片每一行有几个小图
        this.widthFrame = widthFrame;

        // 给的素材图片每一列有几个小图
        this.heightFrame = heightFrame;
        // 本次给的素材小鸟只有一行三张图片,我们是切换这三张图来动态显示小鸟,这个属性表示当前切换的第几个图
        this.currentFrame = 0;

        this.width = this.img.width / this.widthFrame;
        this.height = this.img.height / this.heightFrame;

        this.x = x;
        this.y = y;

        // 每次移动的步长
        this.moveDistance = 2;
        // 步长每次加0.5
        this.speedPlus = 0.5;
        // 点击画布,步长变为负值,表示向上移动
        this._bind();

    }
    Bird.prototype = {
        constructor: Bird,
        draw: function() {
            this.ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
            // this.ctx.drawImage(this.img, this.width * this.currentFrame, 0, this.width, this.height, this.x , this.y, this.width, this.height);
        },
        update: function() {
            this.currentFrame = ++this.currentFrame >= this.widthFrame ? 0 : this.currentFrame;

            // this.x += this.speedPlus;
            // 让小鸟不断下落
            // setInterval中,每隔一定时间(100ms)向下移动
            this.y += this.moveDistance;
            // 给速度一个增量,加速度为speedPlus
            this.moveDistance += this.speedPlus;

        },
        _bind: function() {
            var self = this;
            this.ctx.canvas.addEventListener('click', function() {
                // 点击一下屏幕,小鸟的 每次移动距离变为-4 表示向上移动,但是由于(this.moveDistance += this.speedPlus;)这个值会由 -4 慢慢变大 ,小鸟先上升后下降
                self.moveDistance = -4;
            });
        }
    }
    </script>
    <script>
    loadImage({
        bird: './img/bird.png',
        land: './img/land.png',
        pipeDown: './img/pipeDown.png',
        pipeUp: './img/pipeUp.png',
        sky: './img/sky.png'
    }, function(imgObj) {
        // console.log(imgObj.bird.src);
        // console.log(imgObj.sky.src);

        // 画布的大小和背景图片的大小一样大
        cvs.width = imgObj.sky.width;
        cvs.height = imgObj.sky.height;

        var sky1 = new Sky(ctx, imgObj.sky);
        var sky2 = new Sky(ctx, imgObj.sky);

        var bird = new Bird(ctx, imgObj.bird, 3, 1, 10, 10);

        setInterval(function() {
            sky1.draw();
            sky1.update();

            sky2.draw();
            sky2.update();

            bird.draw();
            bird.update();

        }, 100);
    })
    </script>

3.天空背景+鸟+大地

效果如下,这里可以点击屏幕使小鸟上天,但是录制这个gif的时候,屏幕点击不了,所以就没有理会小鸟,只看下面的大地的左右移动,跟天空类似。

这里写图片描述

    <script>
    function Bird(ctx, img, widthFrame, heightFrame, x, y) {
        this.ctx = ctx;
        this.img = img;

        // 给的素材图片每一行有几个小图
        this.widthFrame = widthFrame;

        // 给的素材图片每一列有几个小图
        this.heightFrame = heightFrame;
        // 本次给的素材小鸟只有一行三张图片,我们是切换这三张图来动态显示小鸟,这个属性表示当前切换的第几个图
        this.currentFrame = 0;

        this.width = this.img.width / this.widthFrame;
        this.height = this.img.height / this.heightFrame;

        this.x = x;
        this.y = y;

        // 每次移动的步长
        this.moveDistance = 2;
        // 步长每次加0.5
        this.speedPlus = 0.5;
        // 点击画布,步长变为负值,表示向上移动
        this._bind();

    }
    Bird.prototype = {
        constructor: Bird,
        draw: function() {
            this.ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
            // this.ctx.drawImage(this.img, this.width * this.currentFrame, 0, this.width, this.height, this.x , this.y, this.width, this.height);
        },
        update: function() {
            this.currentFrame = ++this.currentFrame >= this.widthFrame ? 0 : this.currentFrame;

            // this.x += this.speedPlus;
            // 让小鸟不断下落
            // setInterval中,每隔一定时间(100ms)向下移动
            this.y += this.moveDistance;
            // 给速度一个增量,加速度为speedPlus
            this.moveDistance += this.speedPlus;

        },
        _bind: function() {
            var self = this;
            this.ctx.canvas.addEventListener('click', function() {
                // 点击一下屏幕,小鸟的 每次移动距离变为-4 表示向上移动,但是由于(this.moveDistance += this.speedPlus;)这个值会由 -4 慢慢变大 ,小鸟先上升后下降
                self.moveDistance = -4;
            });
        }
    }
    </script>
    <script>
    // 这里用混入继承来实现扩展原型
    function extend(o1, o2) {
        for (var key in o2) {
            if (o2.hasOwnProperty(key)) {
                o1[key] = o2[key];
            }
        }
    }

    function Land(ctx, img, speed) {
        this.ctx = ctx;
        this.img = img;
        this.x = Land.objNum * this.img.width;
        // 大地在下方,大地的顶部在画布的高度-大地图片的高度处
        this.y = this.ctx.canvas.height - this.img.height;
        this.speed = speed || 10;
        Land.objNum++;
    }

    extend(Land.prototype, {
        draw: function() {
            this.ctx.drawImage(this.img, this.x,this.y);
        },
        update: function() {
            this.x -= this.speed;
            if (this.x < -this.img.width) {
                // 道理同天空背景的移动,只不过这里用了四个大地,而Sky我们实例化了两个
                this.x += Land.objNum * this.img.width;
            }
        }
    });
    Land.objNum = 0;
    </script>
    <script>
    loadImage({
        bird: './img/bird.png',
        land: './img/land.png',
        pipeDown: './img/pipeDown.png',
        pipeUp: './img/pipeUp.png',
        sky: './img/sky.png'
    }, function(imgObj) {
        // console.log(imgObj.bird.src);
        // console.log(imgObj.sky.src);

        // 画布的大小和背景图片的大小一样大
        cvs.width = imgObj.sky.width;
        cvs.height = imgObj.sky.height;

        var sky1 = new Sky(ctx, imgObj.sky);
        var sky2 = new Sky(ctx, imgObj.sky);

// 实例化四个大地,三个不够,可以画个图就知道了
        var land = new Land(ctx, imgObj.land, 10);
        var land1 = new Land(ctx, imgObj.land, 10);
        var land2 = new Land(ctx, imgObj.land, 10);
        var land3 = new Land(ctx, imgObj.land, 10);

        var bird = new Bird(ctx, imgObj.bird, 3, 1, 10, 10);



        setInterval(function() {
            sky1.draw();
            sky1.update();

            sky2.draw();
            sky2.update();

            land.draw();
            land.update();
            land1.draw();
            land1.update();
            land2.draw();
            land2.update();
            land3.draw();
            land3.update();



            bird.draw();
            bird.update();

        }, 100);
    })
    </script>

4.Sky + Bird + Land +Pipe

   <script>
        /*
        * 管道的特点:
        * 1、成对出现,所以x轴可以共享,但是y轴不共享
        * 2、上下管道之间的路径固定,可以由用户指定sapce
        * 3、管道的高度是随机生成的,随机生成上管道的高度,下管道就可以计算了,管道有最低高度minHeight
        * 4、当管道走出画布,从右边出来时,高度需要重新随机生成
        function Pipe( ctx, imgDown, imgUp, space, landHeight, speed ) {

            this.ctx = ctx;
            this.imgDown = imgDown;       //口朝下的管道,在画布的上面
            this.imgUp = imgUp;           //口朝上的管道,在画布的下面
            this.space = space;           //上下管道的间距
            this.landHeight = landHeight; //大地的高度
            this.speed = speed;

            // 管道最小高度
            this.minHeight = 100; //管道高度不能为0,所以管道有最低高度

            // 管道默认的宽高
            this.width = this.imgDown.width;
            this.height = this.imgDown.height;

            Pipe.len++;
            // 这个数值300,防止小鸟一出场就挂了
            this.x = 300 + this.width * 3 * ( Pipe.len - 1 );
            this.y = 0;

            // 初始化管道的坐标,自调用函数
            this._init();
        }

        // 管道实例的数量
        Pipe.len = 0;

        // 扩展原型方法
        extend( Pipe.prototype, {

            // 初始化管道的坐标
            _init: function() {

                // 单个管道的最大高度
                var maxHeight = this.ctx.canvas.height - this.landHeight - this.space - this.minHeight;

                // 随机生成上管道的高度在 50 到 maxHeight 之间
                var randomHeight = Math.random() * maxHeight;
                randomHeight = randomHeight < this.minHeight? this.minHeight : randomHeight;

                // (这是一个负值)上面管道的y轴坐标 = 随机生成的高度(上面的管道显示的部分) - 管道默认的高度 (上面管道显示+隐藏的部分)
                this.downY = randomHeight - this.height;

                // 下面管道的y轴坐标 = 随机生成的高度(上面的管道显示的部分) + 上下管道的间隔
                this.upY = randomHeight + this.space;
            },

            // 绘制管道
            draw: function() {
                this.ctx.drawImage( this.imgDown, this.x, this.downY );
                this.ctx.drawImage( this.imgUp, this.x, this.upY );
                this._drawPath();
            },

            // 根据管道的宽高和坐标绘制对应的路径
            _drawPath: function() {
            //绘制管道外侧的矩形,为下面小鸟撞击做铺垫。判断小鸟有没有飞入这个矩形框中
                this.ctx.rect( this.x, this.downY, this.width, this.height );
                this.ctx.rect( this.x, this.upY, this.width, this.height );
                this.ctx.stroke();
            },

            // 更新下一帧的数据
            update: function() {
                this.x -= this.speed;
                // 管道走出画布,向右拼接,同时重新生成高度
                if ( this.x <= -this.width ) {
                    this._init();
                    this.x += this.width * 3 * Pipe.len;
                }
            }
        } );
    </script>


            // 让背景动起来
            var timer = setInterval(function() {

                /*
                * 每次绘制新的游戏画面时,
                * 先判断小鸟有没有碰撞,
                * 如果碰撞暂停定时器。
                * */
                var birdCoreX = bird.x + bird.width / 2;
                var birdCoreY = bird.y + bird.height / 2;

                // 如果小鸟撞向管道,或者上天,或者入地,那么游戏结束
                if ( ctx.isPointInPath( birdCoreX, birdCoreY )
                        || birdCoreY < 0
                        || birdCoreY > (ctx.canvas.height - imgObj.land.height) )
                {
                    clearInterval( timer ); //停止游戏
                    ctx.fillStyle = 'rgba( 100, 100, 100, 0.8 )';
                    ctx.fillRect( 0, 0, ctx.canvas.width, ctx.canvas.height );
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';

                    ctx.fillStyle = 'red';
                    ctx.font = '900 40px 微软雅黑';
                    ctx.fillText( 'GAME OVER!!!', ctx.canvas.width / 2, ctx.canvas.height / 2 );
                    return;
                }

                // 先清除上一次绘制的6个管道路径,
                // 然后再按照新的位置绘制新路径
                ctx.beginPath();

                pipe.draw();
                pipe.update();

                pipe1.draw();
                pipe1.update();

                pipe2.draw();
                pipe2.update();

                pipe3.draw();
                pipe3.update();

                pipe4.draw();
                pipe4.update();

                pipe5.draw();
                pipe5.update();
            }, 50);

5.ctx.save()和ctx.restore是什么意思呢?

这里写图片描述

我们先看下上图的代码:
<head>
    <script language="javascript">
    function draw() {
        var ctx = document.getElementById('myCanvas').getContext("2d");
        // 注意:所有的移动都是基于这一上下文。
        ctx.translate(80, 80);

        for (var i = 1; i < 10; i++) {

            ctx.save();

            // 每次移动之前的原点
            ctx.fillRect(0, 0, 2, 2);
            ctx.translate(60 * i, 0);
            // 每次移动之后的原点,移动后如果用restore的话,可以使原点归位
            ctx.fillRect(0, 0, 4, 4);


            ctx.fillStyle = "rgb(" + (30 * i) + "," + (255 - 30 * i) + ",255)";
            ctx.beginPath();
            ctx.arc(0, 0, 30, 0, Math.PI, true);

            ctx.closePath();
            ctx.fill();

            ctx.restore();
        }
    }

    window.onload = function() {
        draw();
    }
    </script>
当你屏蔽掉ctx.save()和ctx.restore()之后,会发现根本不是想要的样子,至于理解的话,[引用别人写出来:](http://bbs.csdn.net/topics/390866099)

❑ save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。

❑ restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。

save和restore要配对使用(restore可以比save少,但不能多),如果restore调用次数比save多,会引发Error。

下面分别列出有无save——restore的情况:
**有save——restore**
i=1
执行save保存起点(0,0),图中红色小点,
执行ctx.translate(),起点变为(60*i,0)即(60,0),第一个黑点,
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(90,0)
执行restroe起点回到(0,0),图中红色小点。
i=2
执行save保存起点(0,0)
执行ctx.translate(),起点变为(60*i,0)即(120,0),第二个黑点,
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(150,0)
执行restroe起点回到(0,0)
i=3
执行save保存起点(0,0)
执行ctx.translate(),起点变为(60*i,0)即(180,0)
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(210,0)
执行restroe起点回到(0,0)
看到坐标点的变化了吗?你画的半圆直径是60,所以第一个半圆的起止点在(60,0)和(120,0)第二个半圆的起止点在(120,0)和(180,0),第三个半圆的起止点(180,0)和(240,0)所以这些半圆都是相切的,就像下面那个图一样。


**无save——restore**
i=1
执行ctx.translate(),起点变为(60*i,0)即(60,0)
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(90,0)
i=2
执行ctx.translate(),起点变为(90+60*i,0)即(210,0)
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(240,0)
i=3
执行ctx.translate(),起点变为(240+60*i,0)即(420,0)
执行drawTop():起点偏移到(x + radius*cos( startAngle), y + radius*sin( startAngle))即(450,0)
对比坐标,第一个半圆起止点是(60,0)和(120,0),第二个半圆起止点是(210,0)和(270,0),第三个半圆的起止点是(420,0)和(480,0)。看到坐标了吗?第一个半圆的止点与第二个半圆的起点差了90,第二个半圆的止点与第三个半圆的起点差了150,所以你画出的图像就错位了,就像你的上图一样

这里写图片描述

这里写图片描述

这就很容易理解了。相当于,笔触每次被挪动后,放回原位,下次别人来找的时候,在原来的地方能找到,而不是去另外的地方找。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值