像素鸟html与js源码(4节课勉强做完)

简介: 像素鸟html与js源码(4节课勉强做完)

《flappy bird》是一款由来自越南的独立游戏开发者Dong Nguyen所开发的作品,游戏于2013年5月24日上线,并在2014年2月突然暴红。2014年2月,《Flappy Bird》被开发者本人从苹果及谷歌应用商店(Google Play)撤下。2014年8月份正式回归App Store,正式加入Flappy迷们期待已久的多人对战模式。游戏中玩家必须控制一只小鸟,跨越由各种不同长度水管所组成的障碍。



 

初始化数据

建议所有的js拆开成多个文件,方便理解,不然500多行代码会疯掉的,上下拉动多次心态会变动的。

初始化函数init

初始化的函数,将所有数据进行初始化,主要是画布以及图片的初始化。

function init() {
        ctx = document.getElementById("canvas").getContext("2d");
        flysound = document.getElementById("flysound");
        scoresound = document.getElementById("scoresound");
        hitsound = document.getElementById("hitsound");
        deadsound = document.getElementById("deadsound");
        swooshingsound = document.getElementById("swooshingsound");
        ctx.lineWidth = 2;
        ctx.font = "bold 40px HirakakuProN-W6"; //绘制字体还原
        ctx.fillStyle = "#FFFFFF";
        upbackground = new Image();
        upbackground.src = "images/background.png";
        bottombackground = new Image();
        bottombackground.src = "images/ground.png";
        bottomstate = 1;
        birdimage = new Image();
        birdimage.src = "images/bird.png";
        birdstate = 1;
        tipimage = new Image();
        tipimage.src = "images/space_tip.png";
        boardimage = new Image();
        boardimage.src = "images/scoreboard.png";
        boardx = (backgroundwidth - boardwidth) / 2;
        pipeupimage = new Image();
        pipeupimage.src = "images/pipeup.png";
        pipedownimage = new Image();
        pipedownimage.src = "images/pipedown.png";
        times = Math.ceil(boxwidth / groundwidth) + 1;
        initPipe();
        canvas = document.getElementById("canvas");
        canvas.addEventListener("mousedown", mouseDown, false);
        window.addEventListener("keydown", keyDown, false);
        //window.addEventListener("keydown",getkeyAndMove,false);
        setInterval(run, 1000 / fps);
      }

随机管道数据

随机范围需要控制好,出现在容器外是没有任何意义的。

function initPipe() {
        for (i = 0; i < 200; i++)
          pipeheight[i] = Math.ceil(Math.random() * 216) + 56; //高度范围从56~272
        for (i = 0; i < 3; i++) {
          pipeoncanvas[i][0] = boxwidth + i * pipeinterval;
          pipeoncanvas[i][1] = pipeheight[pipenumber];
          pipenumber++;
        }
      }

游戏的主要逻辑及绘制

这里能看到有很多的函数,这里只是进行一定的控制。

function run() {
        //游戏未开始
        if (gamestate == 0) {
          drawBeginScene(); //绘制开始场景
          drawBird(); //绘制鸟
          drawTip(); //绘制提示
        }
        //游戏进行中
        if (gamestate == 1) {
          birdvy = birdvy + gravity;
          drawScene(); //绘制场景
          drawBird(); //绘制鸟
          drawScore(); //绘制分数
          checkBird(); //检测鸟是否与物体发生碰撞
        }
        //游戏结束
        if (gamestate == 2) {
          if (birdy + birdheight < backgroundheight)
            //如果鸟没有落地
            birdvy = birdvy + gravity;
          else {
            birdvy = 0;
            birdy = backgroundheight - birdheight;
          }
          drawEndScene(); //绘制结束场景
          drawBird(); //绘制鸟
          drawScoreBoard(); //绘制分数板
          //ctx.fillRect(boardx+14,boardy+boardheight-40,75,40); // 测试重新开始按钮的位置
        }
      }

所有内容的绘制

绘制是比较麻烦的,而且需要绘制的内容比较多,很多人会喜欢直接使用图片的方式来操作。 绘制过程其实可以让你的各方面能力成长的更快一些,但是绘制过程中很多细节会出现思想卡壳的情况,别担心,用笔头画一画就很容易明白了。

绘制过程中可以拆开绘制,并且放置在多个js文件中,不着急整体拼接,否则几百行代码操作复杂度较高。

//绘制提示
      function drawTip() {
        ctx.drawImage(
          tipimage,
          birdx - 57,
          birdy + birdheight + 10,
          tipwidth,
          tipheight
        );
      }
      //绘制分数板
      function drawScoreBoard() {
        //绘制分数板
        ctx.drawImage(boardimage, boardx, boardy, boardwidth, boardheight);
        //绘制当前的得分
        ctx.fillText(score, boardx + 140, boardheight / 2 + boardy - 8); //132
        //绘制最高分
        ctx.fillText(highscore, boardx + 140, boardheight / 2 + boardy + 44); //184
      }
      //绘制开始场景(不包括管道)
      function drawBeginScene() {
        //清理画布上上一桢的画面
        ctx.clearRect(boxx, boxy, boxwidth, boxheight);
        //绘制上方静态背景
        ctx.drawImage(upbackground, 0, 0, backgroundwidth, backgroundheight);
        //绘制下方的动态背景
        drawmovingscene();
        //绘制边框线
        ctx.strokeRect(boxx + 1, boxy + 1, boxwidth - 2, boxheight - 2);
      }
      //绘制场景
      function drawScene() {
        ctx.clearRect(boxx, boxy, boxwidth, boxheight); //清理画布上上一桢的画面
        ctx.drawImage(upbackground, 0, 0, backgroundwidth, backgroundheight); //绘制上方静态背景
        drawmovingscene(); //绘制下方的动态背景
        drawAllPipe(); //绘制管道
        ctx.strokeRect(boxx + 1, boxy + 1, boxwidth - 2, boxheight - 2); //绘制边框线
      }
      //绘制结束场景(不包括管道)
      function drawEndScene() {
        ctx.clearRect(boxx, boxy, boxwidth, boxheight); //清理画布上上一桢的画面
        ctx.drawImage(upbackground, 0, 0, backgroundwidth, backgroundheight); //绘制上方静态背景
        //绘制下方的静态背景,根据bottomstate来判断如何绘制静态地面
        switch (bottomstate) {
          case 1:
            for (i = 0; i < times; i++)
              ctx.drawImage(
                bottombackground,
                groundwidth * (i - 0.75),
                backgroundheight,
                groundwidth,
                groundheight
              );
            break;
          case 2:
            for (i = 0; i < times; i++)
              ctx.drawImage(
                bottombackground,
                groundwidth * i,
                backgroundheight,
                groundwidth,
                groundheight
              );
            break;
          case 3:
            for (i = 0; i < times; i++)
              ctx.drawImage(
                bottombackground,
                groundwidth * (i - 0.25),
                backgroundheight,
                groundwidth,
                groundheight
              );
            break;
          case 4:
            for (i = 0; i < times; i++)
              ctx.drawImage(
                bottombackground,
                groundwidth * (i - 0.5),
                backgroundheight,
                groundwidth,
                groundheight
              );
        }
        //绘制当前的柱子
        for (i = 0; i < 3; i++) {
          drawPipe(pipeoncanvas[i][0], pipeoncanvas[i][1]);
        }
        ctx.strokeRect(boxx + 1, boxy + 1, boxwidth - 2, boxheight - 2); //绘制边框线
      }
      //绘制下方的动态背景
      function drawmovingscene() {
        if (bottomstate == 1) {
          for (i = 0; i < times; i++)
            ctx.drawImage(
              bottombackground,
              groundwidth * i,
              backgroundheight,
              groundwidth,
              groundheight
            );
          bottomstate = 2;
        } else if (bottomstate == 2) {
          for (i = 0; i < times; i++)
            ctx.drawImage(
              bottombackground,
              groundwidth * (i - 0.25),
              backgroundheight,
              groundwidth,
              groundheight
            );
          bottomstate = 3;
        } else if (bottomstate == 3) {
          for (i = 0; i < times; i++)
            ctx.drawImage(
              bottombackground,
              groundwidth * (i - 0.5),
              backgroundheight,
              groundwidth,
              groundheight
            );
          bottomstate = 4;
        } else if (bottomstate == 4) {
          for (i = 0; i < times; i++)
            ctx.drawImage(
              bottombackground,
              groundwidth * (i - 0.75),
              backgroundheight,
              groundwidth,
              groundheight
            );
          bottomstate = 1;
        }
      }
      //使用给定的高度和位置绘制上下两根管道
      function drawPipe(location, height) {
        //绘制下方的管道
        ctx.drawImage(
          pipeupimage,
          0,
          0,
          pipewidth * 2,
          height * 2,
          location,
          boxheight - (height + groundheight),
          pipewidth,
          height
        );
        //绘制上方的管道
        ctx.drawImage(
          pipedownimage,
          0,
          793 - (backgroundheight - height - blankwidth) * 2,
          pipewidth * 2,
          (backgroundheight - height - blankwidth) * 2,
          location,
          0,
          pipewidth,
          backgroundheight - height - blankwidth
        );
      }
      //绘制需要显示的管道
      function drawAllPipe() {
        for (i = 0; i < 3; i++) {
          pipeoncanvas[i][0] = pipeoncanvas[i][0] - movespeed;
        }
        if (pipeoncanvas[0][0] <= -pipewidth) {
          pipeoncanvas[0][0] = pipeoncanvas[1][0];
          pipeoncanvas[0][1] = pipeoncanvas[1][1];
          pipeoncanvas[1][0] = pipeoncanvas[2][0];
          pipeoncanvas[1][1] = pipeoncanvas[2][1];
          pipeoncanvas[2][0] = pipeoncanvas[2][0] + pipeinterval;
          pipeoncanvas[2][1] = pipeheight[pipenumber];
          pipenumber++;
        }
        for (i = 0; i < 3; i++) {
          drawPipe(pipeoncanvas[i][0], pipeoncanvas[i][1]);
        }
      }
      function drawBird() {
        birdy = birdy + birdvy;
        if (gamestate == 0) {
          drawMovingBird();
        }
        //根据鸟的y轴速度来判断鸟的朝向,只在游戏进行阶段生效
        else if (gamestate == 1) {
          ctx.save();
          if (birdvy <= 8) {
            ctx.translate(birdx + birdwidth / 2, birdy + birdheight / 2);
            ctx.rotate(-Math.PI / 6);
            ctx.translate(-birdx - birdwidth / 2, -birdy - birdheight / 2);
          }
          if (birdvy > 8 && birdvy <= 12) {
            ctx.translate(birdx + birdwidth / 2, birdy + birdheight / 2);
            ctx.rotate(Math.PI / 6);
            ctx.translate(-birdx - birdwidth / 2, -birdy - birdheight / 2);
          }
          if (birdvy > 12 && birdvy <= 16) {
            ctx.translate(birdx + birdwidth / 2, birdy + birdheight / 2);
            ctx.rotate(Math.PI / 3);
            ctx.translate(-birdx - birdwidth / 2, -birdy - birdheight / 2);
          }
          if (birdvy > 16) {
            ctx.translate(birdx + birdwidth / 2, birdy + birdheight / 2);
            ctx.rotate(Math.PI / 2);
            ctx.translate(-birdx - birdwidth / 2, -birdy - birdheight / 2);
          }
          drawMovingBird();
          ctx.restore();
        }
        //游戏结束后鸟头向下并停止活动
        else if (gamestate == 2) {
          ctx.save();
          ctx.translate(birdx + birdwidth / 2, birdy + birdheight / 2);
          ctx.rotate(Math.PI / 2);
          ctx.translate(-birdx - birdwidth / 2, -birdy - birdheight / 2);
          ctx.drawImage(
            birdimage,
            0,
            0,
            92,
            64,
            birdx,
            birdy,
            birdwidth,
            birdheight
          );
          ctx.restore();
        }
      }
      //绘制扇动翅膀的鸟
      function drawMovingBird() {
        if (birdstate == 1 || birdstate == 2 || birdstate == 3) {
          ctx.drawImage(
            birdimage,
            0,
            0,
            92,
            64,
            birdx,
            birdy,
            birdwidth,
            birdheight
          );
          birdstate++;
        } else if (birdstate == 4 || birdstate == 5 || birdstate == 6) {
          ctx.drawImage(
            birdimage,
            92,
            0,
            92,
            64,
            birdx,
            birdy,
            birdwidth,
            birdheight
          );
          birdstate++;
        } else if (birdstate == 7 || birdstate == 8 || birdstate == 9) {
          ctx.drawImage(
            birdimage,
            184,
            0,
            92,
            64,
            birdx,
            birdy,
            birdwidth,
            birdheight
          );
          birdstate++;
          if (birdstate == 9) birdstate = 1;
        }
      }
      function drawScore() {
        ctx.fillText(score, boxwidth / 2 - 2, 120);
      }

鸟的碰撞判断

判断游戏是否还能进行,无论是管道产生碰撞,或者接触到了容器底部,也就是碰撞地面都属于挂掉了。加上声音控制就有一定的效果了。

//检查鸟是否与管道产生碰撞(不可能与第三组管道重合),以及鸟是否碰撞地面
      function checkBird() {
        //通过了一根管道加一分
        if (
          (birdx > pipeoncanvas[0][0] &&
            birdx < pipeoncanvas[0][0] + movespeed) ||
          (birdx > pipeoncanvas[1][0] && birdx < pipeoncanvas[1][0] + movespeed)
        ) {
          playSound(scoresound, "sounds/point.mp3");
          score++;
        }
        //先判断第一组管道
        //如果鸟在x轴上与第一组管道重合
        if (
          birdx + birdwidth > pipeoncanvas[0][0] &&
          birdx + birdwidth < pipeoncanvas[0][0] + pipewidth + birdwidth
        ) {
          //如果鸟在y轴上与第一组管道上部或下部重合
          if (
            birdy < backgroundheight - pipeoncanvas[0][1] - blankwidth ||
            birdy + birdheight > backgroundheight - pipeoncanvas[0][1]
          ) {
            hitPipe();
          }
        }
        //判断第二组管道
        //如果鸟在x轴上与第二组管道重合
        //这里我原本使用else if出现了问题,但第一版中却没有问题,对比代码后发现原因是上方第一个if后没有加大括号,
        //这里的else无法区分对应哪一个if,加上大括号后问题解决,建议将if后的内容都加上大括号,养成良好的变成习惯
        else if (
          birdx + birdwidth > pipeoncanvas[1][0] &&
          birdx + birdwidth < pipeoncanvas[1][0] + pipewidth + birdwidth
        ) {
          //如果鸟在y轴上与第二组管道上部或下部重合
          if (
            birdy < backgroundheight - pipeoncanvas[1][1] - blankwidth ||
            birdy + birdheight > backgroundheight - pipeoncanvas[1][1]
          ) {
            hitPipe();
          }
        }
        //判断是否碰撞地面
        else if (birdy + birdheight > backgroundheight) {
          hitPipe();
        }
      }
      //撞击到管道或地面后的一些操作
      function hitPipe() {
        ctx.font = "bold 40px HirakakuProN-W6";
        //ctx.font="bold 35px HarlemNights";
        ctx.fillStyle = "#000000";
        playSound(hitsound, "sounds/hit.mp3");
        playSound(deadsound, "sounds/die.mp3");
        updateScore();
        gamestate = 2; //游戏结束
      }

成绩与键盘鼠标事件

成绩这里很好理解,就是1分1分的增加,键盘鼠标事件比较麻烦,需要计算x与y轴,音乐播放情况等等,代码中注释给的比较全面,不太容易理解的地方可以通过debug来挨个看看执行过程。

//刷新最好成绩
      function updateScore() {
        if (score > highscore) highscore = score;
      }
      //处理键盘事件
      function keyDown() {
        if (gamestate == 0) {
          playSound(swooshingsound, "sounds/swooshing.mp3");
          birdvy = -jumpvelocity;
          gamestate = 1;
        } else if (gamestate == 1) {
          playSound(flysound, "sounds/wing.mp3");
          birdvy = -jumpvelocity;
        }
      }
      //处理鼠标点击事件,相比键盘多了位置判断
      function mouseDown(ev) {
        var mx; //存储鼠标横坐标
        var my; //存储鼠标纵坐标
        if (ev.layerX || ev.layerX == 0) {
          // Firefox
          mx = ev.layerX;
          my = ev.layerY;
        } else if (ev.offsetX || ev.offsetX == 0) {
          // Opera
          mx = ev.offsetX;
          my = ev.offsetY;
        }
        if (gamestate == 0) {
          playSound(swooshingsound, "sounds/swooshing.mp3");
          birdvy = -jumpvelocity;
          gamestate = 1;
        } else if (gamestate == 1) {
          playSound(flysound, "sounds/wing.mp3");
          birdvy = -jumpvelocity;
        }
        //游戏结束后判断是否点击了重新开始
        else if (gamestate == 2) {
          //ctx.fillRect(boardx+14,boardy+boardheight-40,75,40);
          //鼠标是否在重新开始按钮上
          if (
            mx > boardx + 14 &&
            mx < boardx + 89 &&
            my > boardy + boardheight - 40 &&
            my < boardy + boardheight
          ) {
            playSound(swooshingsound, "sounds/swooshing.mp3");
            restart();
          }
        }
      }
      function restart() {
        gamestate = 0; //回到未开始状态
        //ctx.font="bold 40px HarlemNights";  //绘制字体还原
        ctx.font = "bold 40px HirakakuProN-W6"; //绘制字体还原
        ctx.fillStyle = "#FFFFFF";
        score = 0; //当前分数清零
        pipenumber = 0; //读取的管道数清零
        initPipe(); //重新初始化水管高度
        birdx = 192 - birdwidth; //鸟的位置和速度回到初始值
        birdy = 224 - birdheight;
        birdvy = 0;
      }
      function playSound(sound, src) {
        if (src != "" && typeof src != undefined) {
          sound.src = src;
        }
      }

初始化body

初始化的过程中我们主要针对各种音频进行初始化,路径在js中设置即可,留下一个canvas做所有内容的呈现容器即可。

<body onLoad="init();">
  <audio id="flysound" playcount="1" autoplay="true" src=""></audio>
  <audio id="scoresound" playcount="1" autoplay="true" src=""></audio>
  <audio id="hitsound" playcount="1" autoplay="true" src=""></audio>
  <audio id="deadsound" playcount="1" autoplay="true" src=""></audio>
  <audio id="swooshingsound" playcount="1" autoplay="true" src=""></audio>
  <canvas id="canvas" width="384" height="512" style="margin-top: 8px"></canvas>
</body>

总结

这个代码是来自于GitHub的,搞下来后跟着编写了两遍也就会的差不多了,其实创新的难度还是很大的,开源的东西多学习学习才会有更多的思路来创造自己的开源内容,而且这个案例的内容还是非常不错的,建议喜欢前端的技术人们初学的时候可以捉摸捉摸。

图片资源绘制效果,背景可以自己更换,鸟有3种飞行状态,地面就一种,水管主要是上下两个部分。

相关文章
|
19天前
|
JavaScript 前端开发 容器
AJAX载入外部JS文件到页面并让其执行的方法(附源码)
AJAX载入外部JS文件到页面并让其执行的方法(附源码)
18 0
|
2天前
|
数据安全/隐私保护
樱花飘落官网引导页HTML源码
直接修改主文件即可,无加密无授权,测试非常好看,有樱花特效
6 1
|
2天前
超帅的主页命令滚动HTML源码
超帅的主页命令滚动HTML源码,超级好看的!作为网站跳转页还是挺不错的! 仅KB大小,喜欢的老铁可以下载收藏!
6 1
超帅的主页命令滚动HTML源码
|
3天前
|
JavaScript 前端开发 BI
原生html—摆脱ps、excel 在线绘制财务表格加水印(html绘制表格js加水印)
原生html—摆脱ps、excel 在线绘制财务表格加水印(html绘制表格js加水印)
7 1
|
3天前
在线拼接图片工具HTML源码
在线将多张图片拼接成一张图片,多图合一并导出下载。 无需本地安装软件。 下载时,使用日期时间作为文件名, 规避图片文件名相同造成的覆盖问题;也能省去一部覆盖确认操作 多语言支持
5 0
在线拼接图片工具HTML源码
|
8天前
黑色个人主页HTML源码
黑色个人主页HTML源码,源码由HTML+CSS+JS组成,记事本打开源码文件可以进行内容文字之类的修改,双击html文件可以本地运行效果,也可以上传到服务器里面
28 6
黑色个人主页HTML源码
|
10天前
漂亮的七彩引导页导航HTML源码
漂亮的七彩引导页导航HTML源码,源码由HTML+CSS+JS组成,记事本打开源码文件可以进行内容文字之类的修改,双击html文件可以本地运行效果,也可以上传到服务器里面,重定向这个界面
6 0
漂亮的七彩引导页导航HTML源码
|
11天前
|
JavaScript 前端开发
js怎么删除html元素
js怎么删除html元素
23 10
|
11天前
|
移动开发 HTML5
HTML5漫画风格个人介绍源码
HTML5漫画风格个人介绍源码,源码由HTML+CSS+JS组成,记事本打开源码文件可以进行内容文字之类的修改,双击html文件可以本地运行效果,也可以上传到服务器里面,重定向这个界面
13 0
HTML5漫画风格个人介绍源码
|
13天前
|
开发框架 前端开发 JavaScript
采用C#.Net +JavaScript 开发的云LIS系统源码 二级医院应用案例有演示
技术架构:Asp.NET CORE 3.1 MVC + SQLserver + Redis等 开发语言:C# 6.0、JavaScript 前端框架:JQuery、EasyUI、Bootstrap 后端框架:MVC、SQLSugar等 数 据 库:SQLserver 2012