原生JS实现Flappy Bird小游戏(本地化存储)


源码地址
展示地址

1.准备

该游戏主要涉及的知识:

  • JavaScript基础
  • ES6基础(模板字符串,箭头函数,模块化)
  • H5 Web Storage知识
2.难点
(1)、创建柱子

由于两根柱子之间的距离是150,柱子的最小长度为50,页面的大小为600,(600-150)/2 = 225, 因此柱子的长度范围为50-275.通过Math.random函数来实现随机生成长度不同的柱子。
createEle()函数用来创建元素,在创建的同时可以给元素添加相应的样式。
创建完元素以后使用appendChild将元素插入页面。并且将柱子的信息保存下来,方便在后面实现柱子的移动的时候使用。

/**
   * 创建柱子
   * @param {*设置柱子的left值} x 
   */
  createPipe: function(x){
    var upHeight = 50 + Math.floor(Math.random() * 175);
    var downHeight = 600 - 150 -upHeight;
    // 创建上柱子
    var oUpPipe = createEle('div', ['pipe', 'pipe-up'],{
      height: upHeight + 'px',
      left: x + 'px'
    });
    // 创建下柱子
    var oDownPipe = createEle('div', ['pipe', 'pipe-bottom'],{
      height: downHeight + 'px',
      left: x + 'px'
    });
    // 将柱子插入页面
    this.el.appendChild(oUpPipe);
    this.el.appendChild(oDownPipe);
    // 保存柱子的信息
    this.pipeArr.push({
      up: oUpPipe,
      down: oDownPipe,
      y: [upHeight, upHeight+150]
    })
  },
/**
 * 创建元素
 * @param {*} eleName 
 * @param {*} classArr 
 * @param {*} styleObj 
 */
export function createEle(eleName, classArr, styleObj){
  let dom = document.createElement(eleName);
  for(let i = 0; i < classArr.length; i++){
    dom.classList.add(classArr[i]);
  }
  for(let key in styleObj){
    dom.style[key] = styleObj[key];
  }
  return dom;
}
(2)、柱子移动

柱子移动:我们通过在创建柱子的时候保存的柱子的信息,通过对应信息得到柱子的偏移量然后通过改变left值来实现柱子的移动。
由于一开始我们只设置了7对柱子,因此在移动过程中要实现柱子的循环,当X<52的时候,表示柱子已经看不到了,这时候,我们拿到最后一个柱子的偏移值,加上300(两个柱子之间的距离是300)得到当前柱子的新的位置。
每轮循环的时候为了使创建的柱子的高度不是重复的,在这里我们调用getPipeHeight重新设置柱子的高度

 /**
   * 柱子移动,通过在创建柱子的时候保存柱子的信息,然后改变柱子的left值实现柱子的移动
   */
  pipeMove: function(){
    for(let i = 0; i < this.pipeLength; i++){
      let oUpPipe = this.pipeArr[i].up;
      let oDownPipe = this.pipeArr[i].down;
      let x = oUpPipe.offsetLeft - this.skyStep;
      // 因为柱子的width是52
      if(x < -52){
        let lastPipeLeft = this.pipeArr[this.pipeLastIndex].up.offsetLeft;
        oUpPipe.style.left = lastPipeLeft + 300 + 'px';
        oDownPipe.style.left = lastPipeLeft + 300 + 'px';
        // 由于是一个循环,每次拿到最后一个柱子的时候,pipeLastIndex应该改变
        this.pipeLastIndex ++;
        this.pipeLastIndex = this.pipeLastIndex % this.pipeLength;
        // 为了使每一轮循环的柱子的高度是不同的,我们在这里调用getPipeHeight重新设置柱子的高度
        var newPipeHeight = this.getPipeHeight();
        // console.log(result);
        oUpPipe.style.height = newPipeHeight.up + 'px';
        oDownPipe.style.height = newPipeHeight.down + 'px';
        continue;
      }
      oUpPipe.style.left = x + 'px';
      oDownPipe.style.left = x + 'px';
    }
  },
 /**
   * 获取柱子的高度
   */
  getPipeHeight: function(){
    var upHieght = 50+ Math.floor(Math.random() * 175);
    var downHeight = 600 - 150 - upHieght;
    return {
      up: upHieght,
      down: downHeight
    }
  },
(3)、碰撞检测

边界碰撞检测
当小鸟的top值大于maxTop或者小于minTop时游戏结束

 /**
   * 边界碰撞检测
   */
  judgeBoundary: function(){
    if(this.birdTop > this.maxTop || this.birdTop < this.minTop){
      this.failGame();  //游戏结束
    }
  },

柱子碰撞检测
难点在于如果获取是哪一根柱子
因为柱子是和分数相关联的,因此我们可以通过分数来得到柱子的信息。
由于柱子是由7个循环来的,因此分数需要this.score % this.pipeLength
柱子和小鸟相遇时的图示:
小鸟距离屏幕左边的距离是80px,自身有15px的margin-left,因此在pipeX <= 95px的时候小鸟和柱子相遇
在这里插入图片描述
小鸟离开柱子的图示:
pipeX = 80 - 52 - 15 =13,因此pipe>=13的时候小鸟刚好离开柱子。
判断完小鸟和柱子左右碰撞的情况还需要判断上下碰撞的情况(birdY <pipeY[0] || birdY >= pipeY[1])
在这里插入图片描述

  /**
   * 柱子的碰撞检测
   */
  judgePipe: function(){
    let index = this.score % this.pipeLength;
    let pipeX = this.pipeArr[index].up.offsetLeft;
    let pipeY = this.pipeArr[index].y;
    let birdY = this.birdTop;
    if((pipeX <= 95 && pipeX >= 13) && (birdY < pipeY[0] || birdY >= pipeY[1])){
      this.failGame();
    }
  },
(4)、本地存储

util.js
添加数据
当添加的数据不是数组形式的时候,需要通过JSON.stringify(value)将数据转换成数组形式的字符串。

/**
 * 添加数据
 */
export function setLocal (key, value){
  if(typeof value === 'object' && value !== null){
    value = JSON.stringify(value);
  }
  localStorage.setItem(key,value);
}

获取数据
由于想获取出来的数据时数组或者字符串形式的因此取数据的时候需要使用JSON.parse(value)将数组字符串或者对象字符串转换成正常的数组或者对象。

/**
 * 取数据
 * @param {*} key 
 */
export function getLocal (key){
  var value = localStorage.getItem(key);
  if(value === null) {
    return value;
  }
  if(value[0] === '[' || value[0] === '{'){
    return JSON.parse(value);
  }
  return value;
}

我们使用scoreArr来保存成绩信息

/**
   * 从本地获取分数
   */
  getScore: function(){
    var scoreArr = getLocal('score');  //键值不存在的时候为null,我们这里要求的是数组
    return scoreArr ? scoreArr : [];
  },
/**
   * 设置分数
   */
  setScore: function(){
    this.scoreArr.push({
      score: this.score,
      // time
      time: this.getDate(),
    })
    // 排序
    this.scoreArr.sort((a,b)=>b.score - a.score)
    // 设置本地数据
    setLocal('score',this.scoreArr);
  },
(5)、成绩展示

成绩展示过程中使用模板字符串实现元素的添加

  /**
   * 成绩展示
   */
  renderRankList: function(){
    var template = '';
    for(var i = 0; i < 8; i++){
      var degreeClass = '';
      switch (i) {
        case 0: 
          degreeClass = 'first';
          break;
        case 1:
          degreeClass = 'second';
          break;
        case 2:
          degreeClass = 'third';
          break;
      }
      template += `
        <li class="rank-item">
            <span class="rank-degree ${degreeClass}">${i+1}</span>
            <span class="rank-score">${this.scoreArr[i].score}</span>
            <span class="time">${this.scoreArr[i].time}</span>
        </li>
      `
    }
    this.oRankList.innerHTML = template;
  }
3.源代码

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="./assets/index.css">
  <link rel="stylesheet" href="./assets/reset.css">
  <script src="./js/index1.js" type="module"></script>
</head>
<body>
  <div id="game">
    <div class="bird"></div>
    <div class="start start-blue">点击开始</div>
    <div class="score">0</div>
    <div class="mask"></div>
    <div class="end">
      <div class="over">Game Over</div>
      <div class="result">Your Reasults:</div>
      <div class="final-score">0</div>
      <ul class="rank-list">
      </ul>
      <div class="restart">重新开始</div>
    </div>
  </div>
</body>
</html>

index.js

import {createEle, setLocal, getLocal,formatNum} from './util1.js'
var bird = {
  skyPosition: 0, // 天空的初始位置
  skyStep: 2, //天空移动的速度
  birdTop: 220, // 小鸟的高度
  boundColor: 'blue',
  startFlag: false,  // 用来标识是否开始
  birdStepY: 0,  //小鸟下落的步长 
  minTop: 0,  // 边界最小值
  maxTop: 570, //边界最大值
  pipeLength: 7,
  pipeArr: [],  // 通过将柱子的数据存放起来,在柱子移动的时候可以直接取值操作
  score: 0,
  pipeLastIndex: 6,  //用来实现柱子循环
  /**
   * 初始化函数
   */
  init: function(){ 
    this.initData();
    this.animate();
    this.handle();
    if(sessionStorage.getItem('play')){
      this.start();
    }
  },
  /**
   * 获取页面元素
   */
  initData: function(){
    this.el = document.getElementById('game');
    this.oBird = this.el.getElementsByClassName('bird')[0];
    this.oStart = this.el.getElementsByClassName('start')[0];
    this.oScore = this.el.getElementsByClassName('score')[0];
    this.oMask = this.el.getElementsByClassName('mask')[0];
    this.oEnd = this.el.getElementsByClassName('end')[0];
    this.oFinalScore = this.oEnd.getElementsByClassName('final-score')[0];
    this.oRankList = this.el.getElementsByClassName('rank-list')[0];
    this.oRestart = this.el.getElementsByClassName('restart')[0];
    this.scoreArr = this.getScore();
    // console.log(this.scoreArr);
  },
  /**
   * 动画执行函数
   * 由于如果在每个函数中都添加定时器的话,会导致开启过多的定时器
   * 因此在执行函数中统一开启定时器
   */
  animate: function(){
    let count = 0;
    this.timer = setInterval(()=>{
      this.skyMove();
      count ++;
      // 如果游戏开始
      if(this.startFlag){
        this.birdDrop();
        this.pipeMove();
      }
      if(!this.startFlag){
        if(count % 10 === 0){
          this.birdJump();
          this.startBound();
          this.birdFly(count);
        }
      }
    },30)
  },
  /** 1
   * 小鸟上下移动
   */
  birdJump: function(){
    // setInterval(()=>{
      this.birdTop = this.birdTop === 220 ? 260 : 220;
      this.oBird.style.top = this.birdTop + 'px';
    // },300);
  },
  birdDrop: function(){
    this.birdStepY++;
    this.birdTop += this.birdStepY;
    this.oBird.style.top = this.birdTop + 'px';
    // 碰撞检测
    this.judgeKnock();
    this.addScore();
  },
  /**
   * 小鸟飞
   */
  birdFly: function(count){
    this.oBird.style.backgroundPositionX = count % 3 *30 + 'px';
  },
  /** 2
   * 天空移动
   */
  skyMove: function(){
    // setInterval(()=>{
      this.skyPosition -= this.skyStep;
      this.el.style.backgroundPositionX = this.skyPosition + 'px';
    // },30)
  },
  /**
   * 柱子移动,通过在创建柱子的时候保存柱子的信息,然后改变柱子的left值实现柱子的移动
   */
  pipeMove: function(){
    for(let i = 0; i < this.pipeLength; i++){
      let oUpPipe = this.pipeArr[i].up;
      let oDownPipe = this.pipeArr[i].down;
      let x = oUpPipe.offsetLeft - this.skyStep;
      // 因为柱子的width是52
      if(x < -52){
        let lastPipeLeft = this.pipeArr[this.pipeLastIndex].up.offsetLeft;
        oUpPipe.style.left = lastPipeLeft + 300 + 'px';
        oDownPipe.style.left = lastPipeLeft + 300 + 'px';
        // 由于是一个循环,每次拿到最后一个柱子的时候,pipeLastIndex应该改变
        this.pipeLastIndex ++;
        this.pipeLastIndex = this.pipeLastIndex % this.pipeLength;
        // 为了使每一轮循环的额柱子的高度是不同的,我们在这里调用getPipeHeight重新设置柱子的高度
        var newPipeHeight = this.getPipeHeight();
        // console.log(result);
        oUpPipe.style.height = newPipeHeight.up + 'px';
        oDownPipe.style.height = newPipeHeight.down + 'px';
        continue;
      }
      oUpPipe.style.left = x + 'px';
      oDownPipe.style.left = x + 'px';
    }
  },
  /**
   * 获取柱子的高度
   */
  getPipeHeight: function(){
    var upHieght = 50+ Math.floor(Math.random() * 175);
    var downHeight = 600 - 150 - upHieght;
    return {
      up: upHieght,
      down: downHeight
    }
  },
  /** 3
   * 文字放大缩小
   */
  startBound: function(){
    // setInterval(()=>{
      //保存开始的颜色
      let prevColor = this.boundColor;
      this.boundColor = prevColor === 'blue' ? 'white' : 'blue';
      this.oStart.classList.remove('start-' + prevColor);
      this.oStart.classList.add('start-' + this.boundColor);
    // },300)
  },
  /**
   * 碰撞检测
   */
  judgeKnock: function(){
    this.judgeBoundary();  //边界碰撞检测
    this.judgePipe();  //柱子碰撞检测
  },
  /**
   * 边界碰撞检测
   */
  judgeBoundary: function(){
    if(this.birdTop > this.maxTop || this.birdTop < this.minTop){
      this.failGame();  //游戏结束
    }
  },
  /**
   * 柱子的碰撞检测
   */
  judgePipe: function(){
    let index = this.score % this.pipeLength;
    let pipeX = this.pipeArr[index].up.offsetLeft;
    let pipeY = this.pipeArr[index].y;
    let birdY = this.birdTop;
    if((pipeX <= 95 && pipeX >= 13) && (birdY < pipeY[0] || birdY >= pipeY[1])){
      this.failGame();
    }
  },
  /**
   * 加分,通过小鸟和柱子的距离是否小于13来判断小鸟是否通过这个柱子
   */
  addScore: function(){
    let index = this.score % this.pipeLength;
    let pipeX = this.pipeArr[index].up.offsetLeft;
    if(pipeX < 13){
      this.score++;
      this.oScore.innerText = this.score;
    }
  },
  /**
   * 所有的点击函数的统一执行函数
   */
  handle: function(){
    this.handleStart();
    this.handleClick();
    this.handleRestart();
  },
  /** 4
   * 点击开始
   */
  handleStart: function(){
    this.oStart.onclick = ()=>{
      this.start();
    }
  },
  /**
   * 开始
   */
  start: function(){
      this.oStart.style.display = 'none';
      this.oScore.style.display = 'block';
      // 天空移动速度加快
      this.skyStep = 5;
      // 小鸟的初始位置变化
      this.oBird.style.left = '80px';
      // 取消小鸟的过渡动画
      this.oBird.style.transition = 'none';
      this.startFlag = true;
      // 创建柱子
      for(let i = 0; i < this.pipeLength; i++){
        this.createPipe(300 * (i+1));
      }
  },
  /**
   * 点击小鸟运动
   */
  handleClick: function(){
    this.el.onclick = (e)=>{
      if(!e.target.classList.contains('start')){
        this.birdStepY = -10;
      }
    }
  },
  /**
   * 点击重新开始
   */
  handleRestart: function(){
    this.oRestart.onclick = ()=>{
      // 通过play实现点击重新开始
      sessionStorage.setItem('play', true);
      window.location.reload(); //重新加载页面
    }
  },
  /**
   * 创建柱子
   * @param {*设置柱子的left值} x 
   */
  createPipe: function(x){
    var upHeight = 50 + Math.floor(Math.random() * 175);
    var downHeight = 600 - 150 -upHeight;
    // 创建上柱子
    var oUpPipe = createEle('div', ['pipe', 'pipe-up'],{
      height: upHeight + 'px',
      left: x + 'px'
    });
    // 创建下柱子
    var oDownPipe = createEle('div', ['pipe', 'pipe-bottom'],{
      height: downHeight + 'px',
      left: x + 'px'
    });
    // 将柱子插入页面
    this.el.appendChild(oUpPipe);
    this.el.appendChild(oDownPipe);
    // 保存柱子的信息
    this.pipeArr.push({
      up: oUpPipe,
      down: oDownPipe,
      y: [upHeight, upHeight+150]
    })
  },
  /**
   * 游戏结束
   */
  failGame: function(){
    // 清除动画
    clearInterval(this.timer);
    this.setScore();
    this.oMask.style.display = 'block';
    this.oEnd.style.display = 'block';
    this.oBird.style.display = 'none';
    this.oScore.style.display = 'none';
    this.oFinalScore.innerText = this.score;
    // 成绩展示
    this.renderRankList();
  },
  /**
   * 从本地获取分数
   */
  getScore: function(){
    var scoreArr = getLocal('score');  //键值不存在的时候为null,我们这里要求的是数组
    return scoreArr ? scoreArr : [];
  },
  /**
   * 设置分数
   */
  setScore: function(){
    this.scoreArr.push({
      score: this.score,
      // time
      time: this.getDate(),
    })
    // 排序
    this.scoreArr.sort((a,b)=>b.score - a.score)
    // 设置本地数据
    setLocal('score',this.scoreArr);
  },
  /**
   * 获取时间
   */
  getDate: function(){
    let d = new Date();
    let year = formatNum(d.getFullYear());
    let month = formatNum(d.getMonth()+1);
    let day = formatNum(d.getDate());
    let hour = formatNum(d.getHours());
    let minute = formatNum(d.getMinutes());
    let second = formatNum(d.getSeconds());
    let time = `${year}.${month}.${day} ${hour}:${minute}:${second}`
    return time;
  },
  /**
   * 成绩展示
   */
  renderRankList: function(){
    var template = '';
    for(var i = 0; i < 8; i++){
      var degreeClass = '';
      switch (i) {
        case 0: 
          degreeClass = 'first';
          break;
        case 1:
          degreeClass = 'second';
          break;
        case 2:
          degreeClass = 'third';
          break;
      }
      template += `
        <li class="rank-item">
            <span class="rank-degree ${degreeClass}">${i+1}</span>
            <span class="rank-score">${this.scoreArr[i].score}</span>
            <span class="time">${this.scoreArr[i].time}</span>
        </li>
      `
    }
    this.oRankList.innerHTML = template;
  }
}
bird.init();

util.js

/**
 * 创建元素
 * @param {*} eleName 
 * @param {*} classArr 
 * @param {*} styleObj 
 */
export function createEle(eleName, classArr, styleObj){
  let dom = document.createElement(eleName);
  for(let i = 0; i < classArr.length; i++){
    dom.classList.add(classArr[i]);
  }
  for(let key in styleObj){
    dom.style[key] = styleObj[key];
  }
  return dom;
}

/**
 * 存数据
 */
export function setLocal (key, value){
  if(typeof value === 'object' && value !== null){
    value = JSON.stringify(value);
  }
  localStorage.setItem(key,value);
}
/**
 * 取数据
 * @param {*} key 
 */
export function getLocal (key){
  var value = localStorage.getItem(key);
  if(value === null) {
    return value;
  }
  if(value[0] === '[' || value[0] === '{'){
    return JSON.parse(value);
  }
  return value;
}
export function formatNum(num){
  if(num < 10){
    num = '0' + num;
  }
  return num;
}

index.css

#game {
  position: relative;
  width: 100%;
  height: 600px;
  background-image: url("../images/sky.png");
}
.bird{
  position: absolute;
  left: 50%;
  top: 235px;
  margin-left: -15px;
  width: 30px;
  height: 30px;
  background-image: url('../images/birds.png');
  transition: top 0.3s linear;
  /* background: red; */
}
.start{
  position: absolute;
  top: 295px;
  left: 50%;
  margin-left: -100px;
  width: 200px;
  height: 60px;
  font-weight: bolder;
  line-height: 60px;
  text-align: center;
  transition: all 0.3s linear;
  cursor: pointer;
  /* background-color: red; */
}
.start.start-white{
  color: #fff;
  font-size: 24px;
}
.start.start-blue{
  color: #09f;
  font-size: 36px;
}
.score{
  position: absolute;
  left: 50%;
  /* 水平居中 */
  transform: translateX(-50%);
  font-size: 24px;
  color: #fff;
  font-weight: bolder;
  display: none;
}
.mask{
  /* 用定位让宽高和父元素一样 */
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: black;
  opacity: 0.7;
  display: none;
  z-index: 2;
}
.end{
  position: absolute;
  left: 50%;
  top: 75px;
  transform: translateX(-50%);
  z-index: 10;
  text-align: center;
  color: red;
  display: none;
  z-index: 3;
}
.end .over{
  font-size: 35px;
  font-weight: bolder;
  color: red;
}
.end .result,
.end .final-score{
  margin-top: 20px;
  color: #ff0;
  font-size: 20px;
  font-weight: bold;
}
.end .rank-list{
  margin-top: 20px;
  color: #09f;
  /* 文本左对齐 */
  text-align: left;
}
.end .rank-item{
  height: 30px;
  line-height: 30px;
  margin-bottom: 10px;
  padding: 0 10px;
  font-size: 13px;
  /* background-color: red; */
}
.end .rank-degree{
  display: inline-block;
  width: 14px;
  height: 14px;
  line-height: 14px;
  text-align: center;
  color: #fff;
  background-color: #8eb9f5;
  font-size: 12px;
  margin-right: 10px;
}
.end .rank-degree.first{
  background-color: #f54545;
}
.end .rank-degree.second{
  background-color: #ff8547;
}
.end .rank-degree.third{
  background-color: #ffac38;
}
.end .rank-score{
  display: inline-block;
  width: 30px;
}
.end .restart{
  color: #09f;
  font-size: 18px;
  cursor: pointer;
  font-weight: bolder;
}
.pipe{
  position: absolute;
  /* left: 300px; */
  width: 52px;
  height: 150px;
  z-index: 1;
}
.pipe.pipe-up{
  top:0;
  background-image: url('../images/pipe2.png');
  background-position-y: bottom;
}
.pipe.pipe.pipe-bottom{
  bottom: 0;
  background-image: url('../images/pipe1.png');
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值