JavaScript操作canvas制作前端H5小游戏——Flappy Bird

游戏查看

源码和素材下载

博主学习前端一年多一点,还是个新手,不过勇于尝试,才能不断进步,如果代码质量不好,欢迎提意见,下面开始讲解,首先贴张游戏界面图:

这里写图片描述

游戏使用canvas画图制作,分析游戏确定有这几个元素:

  1. 天空背景不动
  2. 小鸟上下移动,左右不动
  3. 地板和水管向左移动(造成小鸟向前移动的错觉)

canvas画图是覆盖画图,所以画图顺序很重要,它的api我就不多说了,只有用到以下内容:

<!-- html代码 -->
<canvas id="canvas">您的浏览器不支持canvas</canvas>
/* js相关 */
var canvas = document.getElementById('canvas'),   //获取canvas节点
    ctx = canvas.getContext('2d');                //获取画布上下文画图环境
//画图(只举了一种,还有另一种传参方式)
ctx.drawImage(img, imgx, imgy, imgw, imgh, canx, cany, canw, canh);
//参数的含义依次为:图片资源、图片的x坐标、y坐标、宽度、高度、画布中x坐标、y坐标、宽度、高度
//因为我把所有的图片都合成一张,所以需要用截取图像的传参方式

下面简单说说整个游戏的运行代码结构:

var img = new Image();           //加载图像
img.src = './img.png';             
img.onload = start;              //图像加载完成就运行start函数,所以start是入口

function start(){
    //检查是否碰撞到地板,水管
    check();
    if(是否游戏结束){
        //游戏结束的操作然后退出
        return;
    }
    //画背景
    ...
    if(isStarted){            //isStarted为是否开始游戏的变量,是全局的,默认为false
        //开始游戏就画小鸟,水管
    }else{
        //否则就画准备开始的图像
    }
    //画分数(默认为0,准备阶段画的是0...
    //画地板
    ...
    //设置定时器,保证动画在游戏中不断进行
    timer = requestAnimationFrame(start); //和setTimeout(start, 16)效果差不多
}

document.ontouchstart = document.onmousedown = function(e){
    //点击屏幕时小鸟进行跳跃等处理
}

整体结构就是这样,然后我们一部分一部分完成就可以了。

第一步:获取设备的屏幕大小,兼容各种屏幕设备

var viewSize = (function(){

    var pageWidth = window.innerWidth,
        pageHeight = window.innerHeight;

    if (typeof pageWidth != 'number') {
        pageHeight = document.documentElement.clientHeight;
        pageWidth = document.documentElement.clientWidth;
    };

    if(pageWidth >= pageHeight){
        pageWidth = pageHeight * 360 / 640;
    }
    pageWidth = pageWidth >  414 ? 414 : pageWidth;
    pageHeight = pageHeight > 736 ? 736 : pageHeight;

    return {
         pageWidth,
        height: pageHeight
    };

})();
//然后就设置画布宽高
canvas.width = viewSize.width;
canvas.height = viewSize.height;
//定义原图像与游戏界面的像素比
var k = viewSize.height / 600    //我找的背景图高度为600px,所以比例就是屏幕高除以600

第二步:完成游戏进行中的部分(没有gameover检查,isStarted为true时)

1)画背景(没有难点,主要是图像大小的计算要想清楚)

//清除
ctx.clearRect(0,0,viewSize.width,viewSize.height);
//画背景
ctx.drawImage(img, 0, 0, 800, 600, 0, 0, Math.ceil(k * 800), viewSize.height);

2)画小鸟:我在全局定义了一个小鸟类,如下:

function Bird(){
    //小鸟拍翅膀有三种状态,所以画图相关大多用一个数组来表示
    this.imgX = [170, 222, 275];                           //在原图中x的坐标
    this.imgY = [750, 750, 750];                           //在原图中y的坐标
    this.imgW = [34, 34, 34];                              //在原图中宽度
    this.imgH = [24, 24, 24];                              //在原图中高度
    var canX = Math.ceil(110 / 450 * viewSize.width);      //在画布中x的坐标
    this.canX = [canX, canX, canX];
    var canY = Math.ceil(380 / 800 * viewSize.height);     //在画布中y的初始坐标 
    this.canY = [canY, canY, canY];                        
    var canW = Math.ceil(34 * k);                          //在画布中的宽度  
    this.canW = [canW, canW, canW];                 
    var canH = Math.ceil(24 * k);                          //在画布中的高度
    this.canH = [canH, canH, canH];
    //下面三个变量是用来协助记住是在三个状态中的哪个状态,后面一看就知道了
    this.index = 0;                                        
    this.count = 0;
    this.step = 1;
    //表示小鸟飞行的时间,后面知道用途
    this.t = 0;
    //记住初始y坐标,也是后面一看就知道了
    this.y = [canY, canY, canY];
}

定义类的好处就是可以不用设置那么多的全局变量,你可以直接定义小鸟为一个对象,接着定义小鸟画图方法:

Bird.prototype.draw = function(){
    var index = this.index;
    //翅膀拍动, this.count就是用来控制拍动的频率,记住定时器1秒运行16帧,频率很快的
    this.count++;
    if(this.count == 6){
        this.index += this.step;   
        this.count = 0;
    }
    //this.index的变化过程为0、1、2、1、0、1、2、1...所以需要this.index +1和-1变化
    if((this.index == 2 && this.step == 1) || (this.index == 0 && this.step) == -1){
        this.step = - this.step;
    } 
    //计算垂直位移,使用公式 y = a * t * (t - c),这里就知道了this.t是代表着小鸟起跳后到现在的时间
    //我使用了抛物线的函数方程,你也可以自己选择,代码下面我会给出函数坐标图就很清除了
    var c = 0.7 * 60;
    var minY = - 85 * viewSize.height / 800;
    var a = -minY * 4 / (c * c);
    var dy = a * this.t * (this.t - c);  //dy是小鸟的位移
    //下面是小鸟飞到顶部的情况,我的处理是,使再点击失效,要小鸟飞下来才能继续点击
    if(this.y[0] + dy < 0){
        canClick = false;
    }else{
        canClick = true;
    }
    //然后小鸟在画布的y坐标就等于原先的y坐标加上位移
    for(var i = 0; i < 3; i++){
        this.canY[i] = this.y[i] + Math.ceil(dy);
    }
    this.t++;
    ctx.drawImage(img, this.imgX[index], this.imgY[index], this.imgW[index], 
                       this.imgH[index], this.canX[index], this.canY[index], 
                       this.canW[index], this.canH[index]);

};

给出小鸟计算方程的坐标图

这里写图片描述

因为canvas的y正方向是向下的,所以跳跃应该位移是先负后正,自由落体又是抛物线,接下来就是数学知识了,图中可以看出:

  • 如果this.t > c,dy > 0,所以可以得出,当this.t = c小鸟到最高点,选c的大小就可以控制上升和下落的速度

  • 在this.t = c/2时,dy达到了最小值,所以,控制Ymin可以确定小鸟的垂直移动最大距离。

要画小鸟就可以:

var bird = new Bird();
bird.draw();

3)画水管:

游戏画面中最多出现两组水管,当第一组水管到中间时,第二组开始出现,当第一组水管从游戏界面的左边出去了,第二组水管刚刚到达中间,而最右边又开始有水管进来,以此类推,不断重复。

这里写图片描述

先解决一组水管的画法,仍然先定义水管类,分为上水管和下水管:

//基类,属性的含义同小鸟类
function Pie(){
    this.imgY = 751;              
    this.imgW = 52;              
    this.imgH = 420;
    this.canX = viewSize.width;               //默认在画布的最右边
    this.canW = Math.ceil(80 / 450 * viewSize.width);
    this.canH = Math.ceil(this.canW * 420 / 52);
}
//其中top我们随机生成,代表的是同一组水管中,上水管的左下角在画布中的y坐标
//上水管类
function UpPie(top){  
    Pie.call(this);                       //继承相同的属性
    this.imgX = 70;                       //上水管在原图中的x坐标  
    this.canY = top - this.canH;          //上水管在画布中的y坐标计算
    this.draw = drawPie;                   
};
//下水管类
function DownPie(top){
    Pie.call(this);
    this.imgX = 0;
    this.canY = top + Math.ceil(150 / 800 * viewSize.height);  //上水管和下水管的距离固定,大小可调
    this.draw = drawPie;
}
function drawPie(){
    var speed = 2 * k;
    this.canX -= speed;  //每画一次就向左边走
    ctx.drawImage(img, this.imgX, this.imgY, this.imgW, this.imgH, 
                       this.canX, this.canY, this.canW, this.canH);
}

然后开始画水管:

//用一个数组存在画面中的水管
var Pies = [];
//创建水管函数,首先随机生成top,然后分别实例化上、下水管然后存进Pies里
function createPie(){
    var minTop = Math.ceil(90 /800 * viewSize.height),
        maxTop = Math.ceil(390 /800 * viewSize.height),
        top = minTop + Math.ceil(Math.random() * (maxTop - minTop));
    Pies.push(new UpPie(top));
    Pies.push(new DownPie(top));
};
//画水管时,首先判断
//第一组水管出左边屏幕,移除水管
if(Pies[0].canX <= -Pies[0].canW && Pies.length == 4){
    Pies[0] = null;
    Pies[1] = null;
    Pies.shift();
    Pies.shift();
    canCount = true;
}
//第一组水管到达中间时创建水管
if(Pies[0].canX <= 0.5 * (viewSize.width - Pies[0].canW) && Pies.length == 2){
    createPie();
}
//然后就可以画水管
for(var i = 0, len = Pies.length; i < len; i++){
    Pies[i].draw();
}

4)画分数,比较简单,主要是需要计算居中:

/**
 * 分数类
 */
function Score(){
    this.imgX = 900;
    this.imgY = 400;
    this.imgW = 36;
    this.imgH = 54;
    this.canW = Math.ceil(36 * k);
    this.canH = Math.ceil(54 * k);
    this.canY = Math.ceil(50 / 800 * viewSize.height);
    this.canX = Math.ceil(viewSize.width / 2 - this.canW / 2);
    this.score = 0;
}
Score.prototype.draw = function(){
    var aScore = ('' + this.score).split('');
    var len = aScore.length;
    //计算一下居中
    this.canX = 0.5 * (viewSize.width - (this.canW + 10) * len + 10);
    for(var i = 0; i < len; i++){
        var num = parseInt(aScore[i]);
        if(num < 5){
            var imgX = this.imgX + num * 40;
            var imgY = 400;
        }else{
            var imgX = this.imgX + (num - 5) * 40;
            var imgY = 460;
        }
        var canX = this.canX + i * (this.canW + 2);
        ctx.drawImage(img, imgX, imgY, this.imgW, this.imgH, canX, this.canY, this.canW, this.canH);
    }
};

然后画就简单了

var score = new Score();

score.draw();

5)画地板,主要是需要让它向左移动

//地板类
function Ground(){
    this.imgX = 0;
    this.imgY = 600;
    this.imgH = 112;
    this.imgW = 600;
    this.canH = Math.ceil(112 * k);
    this.canW = Math.ceil(k * 800);
    this.canX = 0;
    this.canY = viewSize.height - this.canH;
}
Ground.prototype.draw = function(){
    if(this.imgX > 24) this.imgX = 0;   //因为无限滚动,所以需要无痕接上
    ctx.drawImage(img, this.imgX, this.imgY, this.imgW, this.imgH, 
                       this.canX, this.canY, this.canW, this.canH);
    this.imgX += 2;
};

画的时候实例就可以了:

var ground = new Ground();

ground.draw();

到这里你就可以看到水管和地板可以向后走了,小鸟也能飞起来了,只是会不断下落,所以我们要设置点击弹跳。

第三步:点击处理

//touchstart是手机端,mousedown是PC端
document.ontouchstart = document.onmousedown = function(e){
    //游戏如果结束点击无效
    if(gameover) return;
    if(isStarted){
        //游戏如果开始了,那么久开始
        //刚才在小鸟飞出顶部我做了点击屏蔽,
        if(canClick){
            //当我们点击的时候,我们应该恢复初始状态,初始状态就是this.t=0, bird.y[i]储存了初始高度
            for(var i = 0; i < 3; i++){
                bird.y[i] = bird.canY[i];
            }
            bird.t = 0;
        }else{
            return;
        }
    }else{
        //游戏没有开始说明在准备,所以开始
        isStarted = true;
    }

    //在ios客户端,touch事件之后还会触发click事件,阻止默认事件就可以屏蔽了
    var e = e || window.event;
    if(e.preventDefault){
        e.preventDefault();
    }else{
        e.returnValue = false;
    }
};

现在你已经可以使小鸟跳跃了,胜利就在前方。

第四步:check函数

检测小鸟和地板是否碰撞最为简单:

//地板碰撞,小鸟的y坐标 + 小鸟的高度 >= 地板的y坐标,表示撞了地板 
if(bird.canY[0] + bird.canH[0] >= ground.canY){
    gameover = true;
    return;
}

检测小鸟和水管是否碰撞,可以化成两个矩形是否重合,重合的情况比较复杂,我们可以看不重合的情况:只有4种,如图:

(1)这里写图片描述 (2) 这里写图片描述

(3)这里写图片描述 (4)这里写图片描述

只要符合上面一种情况就不重合,其余情况就是重合,所以:

//检测两个矩形是否重合,可以反着看,先找出矩形不重合的情况,
function isOverLay(r1, r2){
    var flag = false;
    if(r1.top > r2.bottom || r1.bottom < r2.top || r1.right < re2.left || r1.left > r2.right){
        flag = true;
    }
    //反之就是重合
    return !flag;
}
//水管碰撞
var birdRect = {
    top: bird.canY[0],
    bottom: bird.canY[0] + bird.canH[0],
    left: bird.canX[0],
    right: bird.canX[0] + bird.canW[0]
};
for(var i = 0, len = Pies.length; i < len; i++){
    var t = Pies[i];
    var pieRect = {
        top: t.canY,
        bottom: t.canY + t.canH,
        left: t.canX,
        right: t.canX + t.canW
    };
    if(isOverLay(birdRect,pieRect)){
        gameover = true;
        return;
    }
}

还需要检查是否得分

if(Math.floor(bird.canX[0]) > Math.floor(Pies[0].canX + Pies[0].canW) && canCount){
    //小鸟的左边出了第一组水管的右边就得分,得分以后,第一组水管还没出屏幕左边时不能计算得分
    canCount = false;
    score.score++;
};

所以check函数为:

function check(){
    function isOverLay(r1, r2){
        var flag = false;
        if(r1.top > r2.bottom || r1.bottom < r2.top || r1.right < re2.left || r1.left > r2.right){
            flag = true;
        }
        //反之就是重合
        return !flag;
    }
    //地板碰撞
    if(bird.canY[0] + bird.canH[0] >= ground.canY){
        console.log(viewSize)
        console.log(bird.canY[0],bird.canH[0],ground.canY)
        gameover = true;
        return;
    }
    //水管碰撞
    var birdRect = {
        top: bird.canY[0],
        bottom: bird.canY[0] + bird.canH[0],
        left: bird.canX[0],
        right: bird.canX[0] + bird.canW[0]
    };
    for(var i = 0, len = Pies.length; i < len; i++){
        var t = Pies[i];
        var pieRect = {
            top: t.canY,
            bottom: t.canY + t.canH,
            left: t.canX,
            right: t.canX + t.canW
        };
        if(isOverLay(birdRect,pieRect)){
            gameover = true;
            return;
        }
    }
    //是否得分
    if(Math.floor(bird.canX[0]) > Math.floor(Pies[0].canX + Pies[0].canW) && canCount){
        canCount = false;
        score.score++;
    };
}

现在游戏已经可以玩了,就是还差gameover处理,和重新开始处理了

第五步:gameover处理:

//画gameover字样
ctx.drawImage(img, 170, 990, 300, 90, Math.ceil(viewSize.width * 0.5 - k * 277 * 0.5), 
              Math.ceil(200 / 800 * viewSize.height), 277 * k, 75 * k);
//画重新开始点击按钮           
ctx.drawImage(img, 550, 1005, 160, 90, Math.ceil(viewSize.width * 0.5 - k * 160 * 0.5), 
              Math.ceil(400 / 800 * viewSize.height), 160 * k, 90 * k)
//因为有重新点击开始,所以在html中有个隐藏的div用来点击重新开始,现在让它出现
startBtn.style.display = 'block';
startBtn.style.width = 160 * k + 'px';
startBtn.style.height = 90 * k + 'px';
startBtn.style.left = Math.ceil(viewSize.width * 0.5 - k * 160 * 0.5) + 'px';
startBtn.style.top = Math.ceil(400 / 800 * viewSize.height) + 'px';
//消除定时器       
cancelAnimationFrame(timer);  //如果用setTimeout就是:cleatTimeout(timer)
//回收资源
ground = null;
bird = null;
score = null;
for(var i = 0, len = Pies.length; i < len; i++){
    Pies[i] = null;
}
Pies = [];

第六步:重新开始游戏处理

startBtn.ontouchstart = startBtn.onmousedown = function(e){
    //初始化参数
    canClick = true;
    gameover = false;
    canCount = true;
    isStarted = false;
    startBtn.style.display = 'none';
    ground = new Ground();
    bird = new Bird();
    score = new Score();
    Pies = [];
    createPie();
    //开定时器
    timer = requestAnimationFrame(start); //或者timer = setTimeout(start, 16);
    //阻止冒泡到document
    var e = e || window.event;
    if(e.stopPropagation){
        e.stopPropagation();
    }else{
        e.cancelBubble = false;
    }

}

到此结束,贴上全部代码,有耐心看完的估计没有几个,哈哈哈

html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Flappy Bird</title>
    <meta name="viewport" content="width=device-width"/>
    <style>
       body,html{
           padding:0;
           margin:0;
           height:100%;
           width:100%;
           backgroung:#f1f1f1;
           cursor:pointer;
           overflow: hidden;
        }
        canvas{
            position:relative;
            z-index:998;
        }
        #restart{
            position:absolute;
            top:0;left:0;
            z-index:999;
            display:none;
        }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    <div id="restart"></div>
    <script src="index.js"></script>
</body>
</html>

js代码

var viewSize = (function(){

    var pageWidth = window.innerWidth,
        pageHeight = window.innerHeight;

    if (typeof pageWidth != 'number') {
        if (document.compatMode == 'CSS1Compat') {
            pageHeight = document.documentElement.clientHeight;
            pageWidth = document.documentElement.clientWidth;
        } else {
            pageHeight = document.body.clientHeight;
            pageWidth = document.body.clientWidth;
        }
    };
    if(pageWidth >= pageHeight){
        pageWidth = pageHeight * 360 / 640;
    }
    pageWidth = pageWidth >  414 ? 414 : pageWidth;
    pageHeight = pageHeight > 736 ? 736 : pageHeight;

    return {
         pageWidth,
        height: pageHeight
    };

})();

(function(){
    var lastTime = 0;
    var prefixes = 'webkit moz ms o'.split(' '); //各浏览器前缀

    var requestAnimationFrame = window.requestAnimationFrame;
    var cancelAnimationFrame = window.cancelAnimationFrame;

    var prefix;
//通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式
    for( var i = 0; i < prefixes.length; i++ ) {
        if ( requestAnimationFrame && cancelAnimationFrame ) {
            break;
        }
        prefix = prefixes[i];
        requestAnimationFrame = requestAnimationFrame || window[ prefix + 'RequestAnimationFrame' ];
        cancelAnimationFrame  = cancelAnimationFrame  || window[ prefix + 'CancelAnimationFrame' ] || window[ prefix + 'CancelRequestAnimationFrame' ];
    }

//如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到setTimeout
    if ( !requestAnimationFrame || !cancelAnimationFrame ) {
        requestAnimationFrame = function( callback, element ) {
            var currTime = new Date().getTime();
            //为了使setTimteout的尽可能的接近每秒60帧的效果
            var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) );
            var id = window.setTimeout( function() {
                callback( currTime + timeToCall );
            }, timeToCall );
            lastTime = currTime + timeToCall;
            return id;
        };

        cancelAnimationFrame = function( id ) {
            window.clearTimeout( id );
        };
    }

//得到兼容各浏览器的API
    window.requestAnimationFrame = requestAnimationFrame;
    window.cancelAnimationFrame = cancelAnimationFrame;
})()

var canvas = document.getElementById('canvas'),
    ctx = canvas.getContext('2d'),
    img = new Image(),
    k= viewSize.height / 600,
    canClick,
    gameover,
    canCount,
    isStarted,
    timer,
    ground,
    bird,
    score,
    Pies,
    startBtn = document.getElementById('restart');
//导入图像
img.onload = start;
img.src = './img.png';
//设置画布宽高
canvas.width = viewSize.width;
canvas.height = viewSize.height;
init();
function init(){
    canClick = true;
    gameover = false;
    canCount = true;
    isStarted = false;
    startBtn.style.display = 'none';
    ground = new Ground();
    bird = new Bird();
    score = new Score();
    Pies = [];
    createPie();
}
function destroy(){
    ground = null;
    bird = null;
    score = null;
    for(var i = 0, len = Pies.length; i < len; i++){
        Pies[i] = null;
    }
    Pies = [];
}
/**
 * 开始游戏
 */
function start(){
    check();
    if(gameover){
        console.log(1)
        ctx.drawImage(img, 170, 990, 300, 90, Math.ceil(viewSize.width * 0.5 - k * 277 * 0.5), Math.ceil(200 / 800 * viewSize.height), 277 * k, 75 * k)
        ctx.drawImage(img, 550, 1005, 160, 90, Math.ceil(viewSize.width * 0.5 - k * 160 * 0.5), Math.ceil(400 / 800 * viewSize.height), 160 * k, 90 * k)
        startBtn.style.width = 160 * k + 'px';
        startBtn.style.height = 90 * k + 'px';
        startBtn.style.left = Math.ceil(viewSize.width * 0.5 - k * 160 * 0.5) + 'px';
        startBtn.style.top = Math.ceil(400 / 800 * viewSize.height) + 'px';
        startBtn.style.display = 'block';
        cancelAnimationFrame(timer);
        destroy();
    }else{
        //清除
        ctx.clearRect(0,0,viewSize.width,viewSize.height);
        //画背景
        ctx.drawImage(img, 0, 0, 800, 600, 0, 0, Math.ceil(k * 800), viewSize.height);
        if(isStarted){
            //第一组水管出左边屏幕,移除水管
            if(Pies[0].canX <= -Pies[0].canW && Pies.length == 4){
                Pies[0] = null;
                Pies[1] = null;
                Pies.shift();
                Pies.shift();
                canCount = true;
            }
            //画小鸟
            bird.draw();
            //创建水管
            if(Pies[0].canX <= 0.5 * (viewSize.width - Pies[0].canW) && Pies.length == 2){
                createPie();
            }
            //画水管
            for(var i = 0, len = Pies.length; i < len; i++){
                Pies[i].draw();
            }

        }else{
            //画ready
            ctx.drawImage(img, 170, 900, 300, 90, Math.ceil(viewSize.width * 0.5 - k * 277 * 0.5), Math.ceil(200 / 800 * viewSize.height), 277 * k, 75 * k)
            ctx.drawImage(img, 170, 1150, 230, 150, Math.ceil(viewSize.width * 0.5 - k * 200 * 0.5), Math.ceil(400 / 800 * viewSize.height), 200 * k, 150 * k)
        }
        //画分数
        score.draw();
        //画地板
        ground.draw();
        //设置定时器
        timer = requestAnimationFrame(start);

    }

};
/**
 * 检查是否碰撞、得分
 */
function check(){
    function isOverLay(rect1, rect2){
        var flag = false;
        if(rect1.top > rect2.bottom || rect1.bottom < rect2.top || rect1.right < rect2.left || rect1.left > rect2.right) flag = true;
        return !flag;
    }
    //地板碰撞
    if(bird.canY[0] + bird.canH[0] >= ground.canY){
        console.log(viewSize)
        console.log(bird.canY[0],bird.canH[0],ground.canY)
        gameover = true;
        return;
    }
    //水管碰撞
    var birdRect = {
        top: bird.canY[0],
        bottom: bird.canY[0] + bird.canH[0],
        left: bird.canX[0],
        right: bird.canX[0] + bird.canW[0]
    };
    for(var i = 0, len = Pies.length; i < len; i++){
        var t = Pies[i];
        var pieRect = {
            top: t.canY,
            bottom: t.canY + t.canH,
            left: t.canX,
            right: t.canX + t.canW
        };
        if(isOverLay(birdRect,pieRect)){
            gameover = true;
            return;
        }
    }
    //是否得分
    if(Math.floor(bird.canX[0]) > Math.floor(Pies[0].canX + Pies[0].canW) && canCount){
        canCount = false;
        score.score++;
    };
}
/**
 * 点击
 */
document.ontouchstart = document.onmousedown = function(e){
    if(gameover) return;
    if(isStarted){
        if(canClick){
            for(var i = 0; i < 3; i++){
                bird.y[i] = bird.canY[i];
            }
            bird.t = 0;
        }else{
            return;
        }
    }else{
        isStarted = true;
    }
    var e = e || window.event;
    if(e.preventDefault){
        e.preventDefault();
    }else{
        e.returnValue = false;
    }
};

startBtn.ontouchstart = startBtn.onmousedown = function(e){
    var e = e || window.event;
    if(e.stopPropagation){
        e.stopPropagation();
    }else{
        e.cancelBubble = false;
    }
    init();
    timer = requestAnimationFrame(start);
}
/**
 * 分数类
 */
function Score(){
    this.imgX = 900;
    this.imgY = 400;
    this.imgW = 36;
    this.imgH = 54;
    this.canW = Math.ceil(36 * k);
    this.canH = Math.ceil(54 * k);
    this.canY = Math.ceil(50 / 800 * viewSize.height);
    this.canX = Math.ceil(viewSize.width / 2 - this.canW / 2);
    this.score = 0;
}
Score.prototype.draw = function(){
    var aScore = ('' + this.score).split('');
    var len = aScore.length;
    this.canX = 0.5 * (viewSize.width - (this.canW + 10) * len + 10);
    for(var i = 0; i < len; i++){
        var num = parseInt(aScore[i]);
        if(num < 5){
            var imgX = this.imgX + num * 40;
            var imgY = 400;
        }else{
            var imgX = this.imgX + (num - 5) * 40;
            var imgY = 460;
        }
        var canX = this.canX + i * (this.canW + 2);
        ctx.drawImage(img, imgX, imgY, this.imgW, this.imgH, canX, this.canY, this.canW, this.canH);
    }
};
/**
 * 小鸟类
 */
function Bird(){
    this.imgX = [170, 222, 275];
    this.imgY = [750, 750, 750];
    this.imgW = [34, 34, 34];
    this.imgH = [24, 24, 24];
    this.index = 2;
    this.count = 0;
    this.step = 1;
    var canX = Math.ceil(110 / 450 * viewSize.width);
    this.canX = [canX, canX, canX];
    var canY = Math.ceil(380 / 800 * viewSize.height);
    this.canY = [canY, canY, canY];
    var canW = Math.ceil(34 * k);
    this.canW = [canW, canW, canW];
    var canH = Math.ceil(24 * k);
    this.canH = [canH, canH, canH];
    this.t = 0;
    this.y = [canY, canY, canY];
}
Bird.prototype.draw = function(){
    var index = this.index;
    //翅膀拍动
    this.count++;
    if(this.count == 6){
        this.index += this.step;
        this.count = 0;
    }
    if((this.index == 2 && this.step == 1) || this.index == 0 && this.step == -1) this.step = - this.step;
    //计算垂直位移,使用公式 y = a * t * (t - c)
    var c = 0.7 * 60;
    var minY = - 85 * viewSize.height / 800;
    var a = -minY * 4 / (c * c);
    var dy = a * this.t * (this.t - c);

    if(this.y[0] + dy < 0){
        canClick = false;
    }else{
        canClick = true;
    }
    for(var i = 0; i < 3; i++){
        this.canY[i] = this.y[i] + Math.ceil(dy);
    }
    this.t++;
    ctx.drawImage(img, this.imgX[index], this.imgY[index], this.imgW[index], this.imgH[index], this.canX[index], this.canY[index], this.canW[index], this.canH[index])

};
/**
 * 水管基类
 */
function Pie(){
    this.imgY = 751;
    this.imgW = 52;
    this.imgH = 420;
    this.canX = viewSize.width;
    this.canW = Math.ceil(80 / 450 * viewSize.width);
    this.canH = Math.ceil(this.canW * 420 / 52);
}
/**
 * 上水管类
 */
function UpPie(top){
    Pie.call(this);
    this.imgX = 70;
    this.canY = top - this.canH;
    this.draw = drawPie;
};
UpPie.prototype = new Pie();
/**
 * 下水管类
 */
function DownPie(top){
    Pie.call(this);
    this.imgX = 0;
    this.canY = top + Math.ceil(150 / 800 * viewSize.height);
    this.draw = drawPie;
}
DownPie.prototype = new Pie();

function drawPie(){
    var speed = 2 * k;
    this.canX -= speed;
    ctx.drawImage(img, this.imgX, this.imgY, this.imgW, this.imgH, this.canX, this.canY, this.canW, this.canH);
}

/**
 * 创建水管
 */
function createPie(){
    var minTop = Math.ceil(90 /800 * viewSize.height),
        maxTop = Math.ceil(390 /800 * viewSize.height),
        top = minTop + Math.ceil(Math.random() * (maxTop - minTop));
    Pies.push(new UpPie(top));
    Pies.push(new DownPie(top));
};
/**
 * 地板类
 */
function Ground(){
    this.imgX = 0;
    this.imgY = 600;
    this.imgH = 112;
    this.imgW = 600;
    this.canH = Math.ceil(112 * k);
    this.canW = Math.ceil(k * 800);
    this.canX = 0;
    this.canY = viewSize.height - this.canH;
}
Ground.prototype.draw = function(){
    if(this.imgX > 24) this.imgX = 0;
    ctx.drawImage(img, this.imgX, this.imgY, this.imgW, this.imgH, this.canX, this.canY, this.canW, this.canH);
    this.imgX += 2;
};
原文地址:https://www.cnblogs.com/kang-xjtu/p/5251457.html