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

举报
红目香薰 发表于 2023/02/05 13:19:46 2023/02/05
【摘要】 像素鸟html与js源码(4节课勉强做完)

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

编辑

目录

初始化数据

初始化函数init

随机管道数据

游戏的主要逻辑及绘制

所有内容的绘制

鸟的碰撞判断

成绩与键盘鼠标事件

初始化body

总结




初始化数据

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

      var boxx = 0;
      var boxy = 0;
      var boxwidth = 384;
      var boxheight = 512;
      var backgroundwidth = 384;
      var backgroundheight = 448;
      var groundwidth = 18.5;
      var groundheight = 64;

      var birdwidth = 46;
      var birdheight = 32;
      var birdx = 192 - birdwidth;
      var birdy = 224 - birdheight;
      var birdvy = 0; //鸟初始的y轴速度
      var birdimage;
      var gravity = 1; //重力加速度
      var jumpvelocity = 11; //跳跃时获得的向上速度
      var birdstate;

      var upbackground;
      var bottombackground;
      var bottomstate;
      var pipeupimage;
      var pipedownimage;
      var pipewidth = 69; //管道的宽度
      var blankwidth = 126; //上下管道之间的间隔
      var pipeinterval = pipewidth + 120; //两个管道之间的间隔
      var pipenumber = 0; //当前已经读取管道高度的个数
      var fps = 30; //游戏的帧数,推荐在30~60之间
      var gamestate = 0; //游戏状态:0--未开始,1--已开始,2--已结束
      var times; //地板图片的条数  Math.ceil(boxwidth/groundwidth)+1;
      var highscore = 0; //得到过的最高分
      var score = 0; //目前得到的分数
      var movespeed = groundwidth / 4; //场景向左移动的速度,为底部场景的宽度的1/4

      var tipimage; //开始的提示图片
      var tipwidth = 168;
      var tipheight = 136;

      var boardimage; //分数板的图片
      var boardx;
      var boardy = 140;
      var boardwidth = 282;
      var boardheight = 245;

      var canvas;
      var ctx;
      var i;
      var pipeheight = [];
      //各种音效
      var flysound; //飞翔的声音
      var scoresound; //得分的声音
      var hitsound; //撞到管道的声音
      var deadsound; //死亡的声音
      var swooshingsound; //切换界面时的声音

      var pipeoncanvas = [
        //要显示在Canvas上的管道的location和height
        [0, 0],
        [0, 0],
        [0, 0],
      ];

初始化函数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种飞行状态,地面就一种,水管主要是上下两个部分。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。