像素鸟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种飞行状态,地面就一种,水管主要是上下两个部分。

相关文章
|
2天前
|
定位技术
时尚的联系我们表单HTML模板(源码)
一款时尚的联系我们表单Html模板,带地图和所在位置,输入基本信息和信息发送,看起来很漂亮的联系我们页面。
11 1
时尚的联系我们表单HTML模板(源码)
|
5天前
|
前端开发 JavaScript
Canvas三维变化背景动画HTML源码
Canvas三维变化背景动画HTML源码
19 5
|
6天前
|
JSON 移动开发 数据格式
html5+css3+js移动端带歌词音乐播放器代码
音乐播放器特效是一款html5+css3+js制作的手机移动端音乐播放器代码,带歌词显示。包括支持单曲循环,歌词显示,歌曲搜索,音量控制,列表循环等功能。利用json获取音乐歌单和歌词,基于html5 audio属性手机音乐播放器代码。
42 6
|
5天前
轻量级消息弹框HTML源码
轻量级消息弹框插件特效源码是一段不错的消息弹框代码,支持设置通知类型、动画、字体、位置等等属性,可定制程度较高,进行适当扩展等,欢迎对此段代码感兴趣的朋友参考使用。
9 0
轻量级消息弹框HTML源码
|
20天前
|
XML 前端开发 JavaScript
前端开发进阶:从HTML到React.js
【10月更文挑战第9天】前端开发进阶:从HTML到React.js
|
27天前
|
JavaScript 前端开发
电话号码正则表达式 代码 javascript+html,JS正则表达式判断11位手机号码
电话号码正则表达式 代码 javascript+html,JS正则表达式判断11位手机号码
69 1
|
21天前
|
JavaScript 前端开发
JavaScript 与 HTML 的结合
JavaScript 与 HTML 的结合
14 0
|
27天前
|
机器学习/深度学习 JSON JavaScript
LangChain-21 Text Splitters 内容切分器 支持多种格式 HTML JSON md Code(JS/Py/TS/etc) 进行切分并输出 方便将数据进行结构化后检索
LangChain-21 Text Splitters 内容切分器 支持多种格式 HTML JSON md Code(JS/Py/TS/etc) 进行切分并输出 方便将数据进行结构化后检索
22 0
|
2月前
|
C++ Windows
HTML+JavaScript构建C++类代码一键转换MASM32代码平台
HTML+JavaScript构建C++类代码一键转换MASM32代码平台
|
2月前
|
C++
HTML+JavaScript构建一个将C/C++定义的ANSI字符串转换为MASM32定义的DWUniCode字符串的工具
HTML+JavaScript构建一个将C/C++定义的ANSI字符串转换为MASM32定义的DWUniCode字符串的工具