canvas制作FlappyBird

Posted on 2017-12-02 20:09  DoubleStone  阅读(560)  评论(0)    收藏  举报

FlappyBird的风靡,是不是让同学们也有了一个自己制作这个游戏的冲动呢?下来我就把整个游戏的制作流程做一个介绍。如有不足之处还请各位大佬海涵。

首先准备好素材,以下一张图片是我从网上找到的素材图。实际制作中可以根据自己的想法替换成别的更搞笑的素材。

 

简要流程:

 

简要代码:

 1 window.onload = function () { //页面加载完成
 2     imageLoaded();
 3 }
 4 
 5 function imageLoaded () {
 6     var image = new Image();
 7     image.src = 'flappyBird.png';
 8     image.onload = function () { //图片加载完成
 9     var canvas = document.getElementById('canvas');
10         var cxt = canvas.getContext('2d');
11         data.image = image;
12         drawImage(cxt); //canvas绘图
13         [通过某事件进入GetReady]
14     }
15 }
16 
17 function getReady () {
18     [单击事件进入gamePlaying]
19 }
20 
21 function gamePlaying () {
22     [失败则进入fail]
23 }
24 
25 function fail () {
26     [通过某事件进入GetReady]
27 }
28 
29 function drawImage (cxt) {
30     [绘制代码]
31 }

我们再定义一个全局变量data,游戏中所有的数据都从这里获取或修改,当然在做好游戏后玩的过程中可以通过F12修改其中数值达到外挂的效果~

 1 var data = {
 2   image: null, //将图片元素存入,在调用canvas绘制函数时用
 3   score: 0, //当前得分
 4   bestScore: 0, //历史得分,更早获得过的最高分,通过cookie实现
 5   system: {
 6     dataRefreshRate: 20, //数据刷新率,游戏是每一个周期改变数据达到运行的目的,用一个统一的值防止混乱
 7     screenRefreshRate: 20, //屏幕刷新率,做第一版的时候没考虑这个问题,结果导致配置低的机子以每20毫秒刷新一次的话(即50hz)会特别卡,所以这版将这个分离出来
 8     start: false, //游戏是否开始
 9     fail: false, //游戏失败与否
10   },
11   element: { //游戏中的所有元素,其中的show为是否显示,draw为元素对应的绘制函数
12     bird: { //鸟,其实整个游戏可以看作小鸟水平方向静止不动,障碍物向左运动
13       show: true,
14       color: 0, //0黄色,1蓝色,2红色
15       attitude: 0, //飞行姿态,0~2
16       speedY: 0, //下落速度
17       left: 120, //距左边界距离
18       top: 315, //距上边界距离
19       gravity: 0, //重力数值
20       animate: false, //是否运动
21       draw: drawBird
22     },
23     background: { //背景
24       show: true,
25       type: 0, //0白天,1黑夜
26       draw: drawBackground
27     },
28     bottomStripe: { //底部滚动条
29       show: true,
30       move: true, //是否移动
31       deviation: 0, //偏移量
32       draw: drawBottomStripe
33     },
34     obstacle: { //柱子,即障碍物
35       show: false,
36       previousAdopt: 56, //距离上个障碍物的距离
37       adopt: 140, //可通过上下柱之间高度
38       width: 72, //宽度
39       body: [], //用于存储障碍物的队列,重要
40       draw: drawObstacle
41     },
42     title: { //标题
43       show: true,
44       type: 0, //0 flappyBird, 1 Get Ready, 2 Game Over
45       top: 0, //距顶端距离
46       draw: drawTitle
47     },
48     tip: { //点击提示
49       show: false,
50       draw: drawTip
51     },
52     startButton: { //开始按钮
53       show: false,
54       draw: drawStartButton
55     },
56     score: { //分数
57       show: false,
58       value: 0, //分数值
59       draw: drawScore
60     },
61     rankings: { //分数榜单
62       show: false,
63       top: 600, //距顶端距离
64       draw: drawRankings
65     },
66     mask: { //黑幕,渐变效果使用
67       show: false,
68       alpha: 0, //透明度
69       draw: drawMask
70     }
71   },
72   TIME: {} //setInterval与setTimeout的存储位置
73 }
74     

例如data.element.startButton.draw值为drawStartButton,对应函数为:

function drawStartButton (cxt) {
    cxt.drawImage(data.image, 708, 236, 104, 58, 128, 400, 144, 81);
}

现在开始按游戏流程制作:当页面加载完成后,进行一系列数据初始化,在window.onload中添加执行函数resetData();

function resetData () {
  data.score = 0; //分数重置为0
  data.element.bird.color = parseInt(Math.random() * 1000) % 3; //鸟的颜色随机3选1
  data.element.background.type = parseInt(Math.random() * 1000) % 2; //背景随机2选1
  data.element.bird.top = 315; //初始化鸟的高度
  data.element.bird.speedY = 0; //初始化鸟的垂直速度
  data.element.bird.gravity = 0; //初始化重力
  data.element.obstacle.previousAdopt = 56; //距离下个阻挡物的距离
  data.element.obstacle.body = []; //清空存储阻挡物的队列
}

然后编辑image.onload中的drawImage(cxt):

for循环将每个show值为true的组件依次绘制,一定要注意绘制顺序,最底层的要先绘制,最顶层的最后绘制,之所以不用for i in arr的原因也是怕其影响顺序,注意其周期是data.system.screenRefreshRate,也是唯一使用此数据的地方。若手机运行吃力,不妨将此周期延长。

function drawImage (cxt) {
  var drawOrder = ['background', 'obstacle', 'bottomStripe', 'title', 'tip', 'startButton', 'score', 'rankings', 'bird', 'mask'];
  data.TIME.drawImage = setInterval(function () {
    for(var i = 0, len = drawOrder.length; i < len; i++){
      if (data.element[drawOrder[i]].show === true) {
        data.element[drawOrder[i]].draw(cxt);
      }
    }
  }, data.system.screenRefreshRate);
}

我们再写一个简单函数,传入名称与布尔值,改变数据的show值,以方便其显示或隐藏

function showElement (name, show) {
  data.element[name].show = show ? true : false;
}

再回到image.onload中,图片载入完成后可将标题、开始按钮、底层运动条纹显示出来,还有小鸟,并使小鸟做出飞翔动作。

在图片素材中可以看到小鸟动作总共3帧,依次绘制实现动画效果,函数传入参数可以控制小鸟停止动作与否。

function birdAnimate (animate) {
  if (animate === true) {
    data.TIME.birdAnimate = setInterval(function () {
      data.element.bird.attitude = (data.element.bird.attitude + 1) % 3;
    }, data.system.dataRefreshRate * 9);
  } else {
    clearInterval(data.TIME.birdAnimate);
  }
}

绘制小鸟的参数,将素材中的关键点提前写死在函数中,而且随着垂直速度的上升或下降进行旋转,具体实现参考canvas的旋转部分,这里不赘述。

function drawBird (cxt) {
  var birdPosition = [
    [
      [6, 982],
      [62, 982],
      [118, 982]
    ],
    [
      [174, 982],
      [230, 658],
      [230, 710]
    ],
    [
      [230, 762],
      [230, 814],
      [230, 866]
    ]
  ];

  cxt.save();
  cxt.translate(data.element.bird.left, data.element.bird.top);
  cxt.rotate(Math.atan(data.element.bird.speedY / 10));
  cxt.drawImage(data.image,
    birdPosition[data.element.bird.color][data.element.bird.attitude][0],
    birdPosition[data.element.bird.color][data.element.bird.attitude][1],
    34, 24, -24, -17, 48, 34);
  cxt.restore();
}

再给canvas添加鼠标单击事件,当鼠标单击它且单击的位置位于按钮内时,进入getReady状态,而且当后面游戏失败后,重新开始时也是进入getReady状态。

function getReady () {
  data.element.title.type = 1; //标题变为Get Ready
  resetData(); //重置数据
  showElement('score', true); //显示分数
  showElement('tip', true); //显示点击提示
  showElement('startButton', false); //隐藏开始按钮
  dataUpdata(true);
  if (data.system.start === false) { //当游戏未开始时执行,相当于一局游戏中只执行一次
    data.element.bird.gravity = 0.4; //设置重力
    showElement('title', false); //隐藏标题
    showElement('tip', false); //隐藏点击提示
    showElement('obstacle', true); //显示障碍物
    createObstacle(); //构建障碍物
  }
  data.system.start  = true;
  data.element.bird.speedY = -7; //每次单击时,将小鸟垂直速度强制为-7,以达到单击时小鸟跳动效果
}

 其中的dataUpdate函数:

function dataUpdata (update) { //传递update也起到开启关闭作用
  if (update === true) { //每次数据变化:鸟的速度为速度加上重力加速度,而距离顶端高度为高度再加上速度
    data.TIME.dataUpdate = setInterval(function () {
      data.element.bird.speedY = data.element.bird.speedY + data.element.bird.gravity;
      data.element.bird.top = data.element.bird.top + data.element.bird.speedY;
    }, data.system.dataRefreshRate);
  } else {
    clearInterval(data.TIME.dataUpdate);
  }  
}

这是一个比较关键的部分,构建障碍物:

function createObstacle () {
  data.TIME.obstacle = setInterval(function () {
    data.element.obstacle.previousAdopt = data.element.obstacle.previousAdopt + 1; //每一个周期障碍物的距离加1,当达到一定值后归零,创建新障碍物
    for (var i = 0, len = data.element.obstacle.body.length; i < len; i++) { //所有已存在的障碍物向左移动,数值可微调以达到自然效果
      data.element.obstacle.body[i][0] -= 0.54;
    }
    if (data.element.obstacle.previousAdopt > 448) {
      data.element.obstacle.previousAdopt = 0;
      var obstacleTop = parseInt(Math.random() * 260) + 100, //随机生成缺口高度
          obstacleBottom = obstacleTop + data.element.obstacle.adopt;
      data.element.obstacle.body.push([400, obstacleTop, obstacleBottom]); // data.element.obstacle.body中推入一个数组:[其距离左端的距离, 上端距, 下端距]
    }
    for (var i = 0; i < data.element.obstacle.body.length; i++) {
      if (data.element.obstacle.body[0][0] < -72) {
        data.element.obstacle.body.shift(); //当最左边的障碍物超出视距后,清除。
      }
    }
  }, data.dataRefreshRate);
}

当data.element.obstacle.body长度大于等于1时则进行绘制,具体方法与小鸟绘制方法相似。

 

每个数据周期须进行判定小鸟是否碰撞,我们将它放在data.TIME.dataUpdate的setInterval里面:

if (!collisionJudge()) {
  fail();
}

其中小鸟可近似看成矩形,满足其存活的条件为:1.不落到地面上,2.不碰到障碍物上。

不落到地面上即data.element.bird.top小于某固定值即可,而不碰到障碍物上即障碍物运动到小鸟的位置时,要保证小鸟的高度在障碍物中间缝隙之间,每次只需要判断当前位置的障碍物是否与小鸟相撞,返回布尔值即可。

 

function fail其实就比较简单了,其中具体内容为清除之前的一系列Interval,小鸟动作静止,显示分数、排行榜等,及再次回到getReady的按钮,此时完成一个闭环,游戏初步完成。

以下是自己完成度较高的代码,不足之处还望各位大佬指出,共同学习进步。

https://github.com/Fifthwolf/canvas-FlappyBird

博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3