用原生JS写一个飞机大战小游戏

01.gif

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情 >>

前言

之前写的小游戏都是面向过程的编程思想写的 , 但是JS毕竟是一个面向对象的语言 , 于是我想用面向对象的方法写一个小游戏。es6的class语法我不是很熟练,正好借此机会熟悉一下class的语法,锻炼一下自己的编程思维。文中可能会有一些语法错误的地方,勿喷。

敌机的种类我写了两种,他们的样式和攻击方式不同。在class语法里面添加一个新的子类还是很简便的,后面我也可以在里面添加更多的敌机类型。我还写了一个升级的功能,可以改变主机的攻击方式,以及敌机的生成速度,游戏难度和等级成正比。

1.绘制游戏区域

游戏区域的元素只有背景,飞机,子弹和提示信息的弹出层。背景图可以通过CSS动画的衔接,达到一直向前滚动的效果。下面附上CSS和HTML代码

 <style>
    .box {
      position: relative;
      margin: 20px auto;
      width: 500px;
      height: 700px;
      overflow: hidden;
    }

    #MyGameBG_1 {
      margin-top: -750px;
    }

    #MyGameBG_2 {
      margin-top: -10px;
    }

    .MyGameBG img {
      width: 100%;
      float: left;
      position: relative;
      animation: move 7s linear infinite forwards;
    }

    @keyframes move {
      from {
        transform: translateY(0)
      }

      to {
        transform: translateY(100%)
      }
    }

    .ele {
      position: absolute;
      width: 60px;
      height: 60px;
    }

    .eleB1 {
      background: url(./333.png) no-repeat;
      background-size: 100%;
    }

    .eleB2 {
      background: url(./444.png);
      background-size: 90%;
      background-position: center;
    }

    .eleC {
      background: url(./222.png) no-repeat;
      background-size: 100%;
    }

    .fire {
      position: absolute;
      width: 10px;
      height: 20px;
      border-radius: 50%;
    }

    .fire1 {
      background-color: red;
    }

    .fire2 {
      background-color: rgb(229, 255, 0);
    }

    .del {
      background: url(./pngsucai_4322_0a8932.png) no-repeat;
      background-size: 100%;
    }

    .defen {
      font-size: 30px;
      position: absolute;
      top: 40%;
      right: 10%;
    }

    .dialog {
      position: absolute;
      top: 275px;
      left: 150px;
      width: 200px;
      height: 150px;
      background-color: rgba(255, 0, 0, 0.623);
      line-height: 150px;
      text-align: center;
      font-size: 20px;
      font-weight: 700;
      color: green;
      display: none;
    }

    h3 {
      position: absolute;
      top: 700px;
      left: 500px;
      color: blue;
    }

    .exl {
      position: absolute;
      background-color: red;
      height: 20px;
      width: 500px;
      top: 720px;
      left: 550px;
    }

    .ex {
      height: 20px;
      background-color: green;
    }
  </style>
  
  // HTML代码
  <body>
  <div class="box">
    <div class="MyGameBG">
      <img src="./背景02.jpg" alt="" id="MyGameBG_1">
      <img src="./背景02.jpg" alt="" id="MyGameBG_2">
    </div>
    <div class="dialog">
    </div>
  </div>
  <h3>Lv1</h3>
  <div class="exl">
    <div class="ex"></div>
  </div>
  <div class="defen">
    键盘上下左右控制
    <br>
    Lv1: 普通子弹
    <br>
    Lv2: 子弹变为两颗
    <br>
    Lv3: 攻击速率提升
    <br>
    Lv4: 子弹变为三颗
    <br>
    <br>
    得分
    <br>
    <span>0</span>
  </div>
  <script src="./飞机大战.js"></script>
</body>
复制代码
背景02.jpg 222.png 333.png 444.png pngsucai_4322_0a8932.png

1.生成飞机实例

将飞机的公共样式和方法提取出来,写在父类A的属性里。然后声明两个子类B和C,分别为继承飞机公共样式的敌机和主机。B类里面会写入敌机的公共样式和方法,根据敌机的种类不同,B类下面会有对应的敌机子类B1和B2。主机因为只有一个,所以一个主机C类就够用了。

生成飞机实例的类写好之后,就可以声明一个定时器来自动生成敌机实例了。

  // 飞机的公共属性
  class A {
    constructor() {
      this.ele = document.createElement('div')
      this.box = document.querySelector('.box')
      this._initA() // 添加公共样式
      this.fen = 0 // 初始得分
    }
    // 公共样式
    _initA() {
      this.ele.classList.add('ele')
      this.box.appendChild(this.ele)
    }
  }
  // B类敌机,敌机的公共属性
  class B extends A {
    constructor() {
      const that = super() // 获取父类的this
      this.ele = that.ele
      airB.push(this) // 将B实例存入数组中
      this._initB() // 初始化B的样式
    }
    // B类自定义样式
    _initB() {
      const x = Math.random() * 450
      this.ele.style.left = `${x}px` // 随机位置
    }
  }
   // B1类的敌机
   class B1 extends B {
    constructor() {
      const that = super() // 获取父类的this
      this.ele = that.ele
      this._initB1()
    }
    _initB1() {
      this.ele.classList.add('eleB1')
    }
   }
  // B2类的敌机
  class B2 extends B {
    constructor() {
      const that = super() // 获取父类的this
      this.ele = that.ele
      this._initB2()
    }
    _initB2() {
      this.ele.classList.add('eleB2')
    }
   }
  // C类飞机,主机
  class C extends A {
    constructor() {
      const that = super() // 获取父类的this
      this.ele = that.ele
      this._initC()
    }
    // 自定义C类的样式
    _initC() {
      this.ele.style.left = `200px`
      this.ele.style.top = `600px`
      this.ele.classList.add('eleC')
    }
  }
  let airB = [] // 集合B类飞机的所有实例
  let b1 = new B1()// 实例化B类飞机
  let c = new C()// 实例化C类飞机
  // 自动生成B实例
  let timeB
  function getB(date) {
    timeB = setInterval(function () {
      let x = Math.random() * 1000
      x >= 400 ? new B1() : new B2()
    }, date)
  }
  getB(2500) // 初始生成敌机的速率   
  let level = 1  // 等级
复制代码

02.gif

2.飞机的移动方法

飞机的移动方法写到父类A里,因为敌机和主机都会调用这个方法进行移动。移动的动画我用的requestAnimationFrame这个方法写的,用定时器来写也可以。

敌机的移动是自动且随机方向的,可以用定时器自动调用移动的方法,用随机数来随机方向。主机的移动是由键盘控制的,在键盘事件里面调用实例原型里的方法即可。

移动的方法我写了4个函数,代码就显得有点多。封装成一个函数也可以,根据传入的参数来判断往哪边移动即可。

 // 飞机的公共属性
  class A {
    constructor() {
      ......
    }
    .....
    //右移动
    rafRight(num = 0) {
      let i = num
      requestAnimationFrame(() => {
        //获取元素的坐标值,要把字符串里的数字提取出来
        let moveX = parseFloat(this.ele.style.left) || 0
        if (moveX >= 440) return
        moveX += 5
        this.ele.style.left = moveX + 'px'
        i++
        if (i >= 10) return
        return this.rafRight(i)
      })
    }
    //左移动
    rafLeft(num = 0) {
      let i = num
      requestAnimationFrame(() => {
        let moveX = parseFloat(this.ele.style.left) || 0
        if (moveX <= 0) return
        moveX -= 5
        this.ele.style.left = moveX + 'px'
        i++
        if (i >= 10) return
        return this.rafLeft(i)
      })
    }
    //上移动
    rafTop(num = 0) {
      let i = num
      requestAnimationFrame(() => {
        let moveY = parseFloat(this.ele.style.top) || 0
        if (moveY <= 400) return
        moveY -= 5
        this.ele.style.top = moveY + 'px'
        i++
        if (i >= 10) return
        return this.rafTop(i)
      })
    }
    //下移动
    rafDown(num = 0) {
      let i = num
      requestAnimationFrame(() => {
        let moveY = parseFloat(this.ele.style.top) || 0
        if (moveY >= 640) return
        moveY += 5
        this.ele.style.top = moveY + 'px'
        i++
        if (i >= 10) return
        return this.rafDown(i)
      })
    }
  }
    // B类敌机,敌机的公共属性
  class B extends A {
    constructor() {
      .....
      this.moveTime = this._move()  // 自动移动
    }
    .....
     // 定时调用父类的移动函数
     _move() {
      return setInterval(() => {
        let x = Math.random() * 1000
        if (x <= 150) {
          super.rafTop()
        } else if (x > 150 && x <= 300) {
          super.rafDown()
        } else if (x > 300 && x <= 650) {
          super.rafRight()
        } else {
          super.rafLeft()
        }
      }, 1050)
    }
  }
  // 键盘事件,控制C实例的移动
  let life = true
  document.addEventListener('keydown', function (e) {
    if (life) {
      if (e.key === 'ArrowUp') {
        c.rafTop()
      } else if (e.key === 'ArrowRight') {
        c.rafRight()
      } else if (e.key === 'ArrowLeft') {
        c.rafLeft()
      } else if (e.key === 'ArrowDown') {
        c.rafDown()
      }
    }
  })
复制代码

3.生成子弹,以及子弹的移动

子弹我本来想用对象的方式来写,写一个子弹的类,然后在飞机的类里面调用生成子弹的实例。但是这种方式飞机和子弹的实例之间就没什么联系了,后面写飞机被击中的判断时不好下手了。于是我把生成子弹的函数作为公共方法写到了A这个飞机的父类里,子类飞机通过调用父类的方法来生成子弹元素。

两种敌机的子弹攻击方式不同,在对应的类里面也各自的样式即可。

  // 飞机的公共属性
  class A {
    constructor() {
      .....
    }
   .....
    // 生成子弹元素的方法
    _fire() {
      let fires = document.createElement('div') // 创建子弹元素
      this.box.appendChild(fires)
      // 添加样式
      fires.classList.add('fire')
      let moveX = parseFloat(this.ele.style.left) || 0
      let moveY = parseFloat(this.ele.style.top) || 0
      fires.style.left = moveX + 25 + 'px'
      fires.style.top = moveY + 'px'
      return fires //返回这个子弹元素
    }
    //子弹移动
    _fireMove(i, item, air, n = 0) { // i控制子弹方向,item为子弹元素,air为飞机实例
      requestAnimationFrame(() => {
        //获取元素的坐标值,要把字符串里的数字提取出来
        let moveY = parseFloat(item.style.top) || 0
        moveY += i * 5
        item.style.top = moveY + 'px'
        n++ // 计数,减少计算量
        if (moveY <= -10 || moveY >= 710) return item.style.display = 'none'
        if (n >= 50) {
          // 判断air参数是B实例还是C实例
          let flag
          if (Array.isArray(air)) {
            for (let i = 0; i < air.length; i++) {
              flag = this._attack(air[i], item, i) // 击毁判定函数
              if (flag) break // 达成条件就退出循环,减少计算量
            }
          } else {
            const flag = this._attack(air, item) // 击毁判定函数
          }
          if (flag) return // 子弹击毁飞机后,终止移动函数
        }
        return this._fireMove(i, item, air, n)
      })
    }
     // 击毁判定
     _attack(p, f, i) { // p参数为飞机实例,f参数为子弹,i为B实例元素的下标
  
    }
  }
  .....
   // B1类的敌机
   class B1 extends B {
    constructor() {
      .....
      this.fire = that.fire
      this.timer = this.openFire() // 自动攻击
    }
    .....
    // B1类飞机子弹样式
    _fireB1() {
      let fb = super._fire()
      fb.classList.add('fire1')
      let moveY = parseFloat(this.ele.style.top) || 0
      fb.style.top = moveY + 40 + 'px'
      super._fireMove(1, fb, c)
    }
    // 自动攻击的方法
    openFire() {
      return setInterval(() => {
        this._fireB1()
      }, 1000)
    }
   }
   // B2类的敌机
  class B2 extends B {
    constructor() {
    .....
      this.fire = that.fire
      this.timer = this.openFire() // 自动攻击
    }
    .....
    // B2类飞机子弹样式
    _fireB2() {
      let fb1 = super._fire()
      let fb2 = super._fire()
      fb1.classList.add('fire1')
      fb2.classList.add('fire1')
      let moveX = parseFloat(this.ele.style.left) || 0
      let moveY = parseFloat(this.ele.style.top) || 0
      fb1.style.left = moveX + 10 + 'px'
      fb2.style.left = moveX + 40 + 'px'
      fb1.style.top = moveY + 40 + 'px'
      fb2.style.top = moveY + 40 + 'px'
      super._fireMove(1, fb1, c)
      super._fireMove(1, fb2, c)
    }
    // 自动攻击的方法
    openFire() {
      return setInterval(() => {
        this._fireB2()
      }, 1500)
    }
   }
   .....
  // C类飞机,主机
  class C extends A {
    constructor() {
    .....
      this.openFire() // 自动攻击
    }
    .....
    // C类子弹样式
      // lv1子弹
      _fire1() {
        let fb = super._fire()
        fb.classList.add('fire2')
        super._fireMove(-1, fb, airB)
      }
      .....
      // 自动射击的方法,根据等级的不同,射击的速率不同
      openFire() {
        return setInterval(() => {
          if (level == 1) {
            this._fire1()
          } else if (level == 2) {
            this._fire2()
          }
        }, 800)
      }
  }
复制代码

04.gif

4.等级系统

写这个功能主要是为了增加游戏的趣味性。根据游戏的时长来获取经验值,经验值满了之后就会升级,在不同的等级下,主机的子弹会产生变化,并且敌机的生成速率也会变快。经验值的增长与等级成反比,等级越高,经验值涨的越慢。

    // C类飞机,主机
    class C extends A {
      constructor() {
        .....
      }
        .....
      // C类子弹样式
      // lv1子弹
      _fire1() {
        let fb = super._fire()
        fb.classList.add('fire2')
        super._fireMove(-1, fb, airB)
      }
      // lv2子弹
      _fire2() {
        let fb1 = super._fire()
        let fb2 = super._fire()
        let moveX = parseFloat(this.ele.style.left) || 0
        let moveY = parseFloat(this.ele.style.top) || 0
        fb1.style.left = moveX + 10 + 'px'
        fb1.style.top = moveY + 10 + 'px'
        fb2.style.left = moveX + 40 + 'px'
        fb2.style.top = moveY + 10 + 'px'
        fb1.classList.add('fire2')
        fb2.classList.add('fire2')
        super._fireMove(-1, fb1, airB)
        super._fireMove(-1, fb2, airB)
      }
      // LV3子弹样式与LV2相同
      // lv4子弹
      _fire4() {
        let fb1 = super._fire()
        let fb2 = super._fire()
        let fb3 = super._fire()
        let moveX = parseFloat(this.ele.style.left) || 0
        let moveY = parseFloat(this.ele.style.top) || 0
        fb1.style.left = moveX + 'px'
        fb1.style.top = moveY + 10 + 'px'
        fb2.style.left = moveX + 25 + 'px'
        fb2.style.top = moveY + 10 + 'px'
        fb3.style.left = moveX + 50 + 'px'
        fb3.style.top = moveY + 10 + 'px'
        fb1.classList.add('fire2')
        fb2.classList.add('fire2')
        fb3.classList.add('fire2')
        super._fireMove(-1, fb1, airB)
        super._fireMove(-1, fb2, airB)
        super._fireMove(-1, fb3, airB)
      }
      // 调用开火方法,等级不同,速度也不同
      openFire() {
        return setInterval(() => {
          if (level == 1) {
            this._fire1()
          } else if (level == 2) {
            this._fire2()
          }
        }, 800)
      }
      openFire2() {
        return setInterval(() => {
          if (level == 3) {
            this._fire2()
          } else if (level >= 4) {
            this._fire4()
          }
        }, 500)
      }
    }
// 下面这些代码都写在全局里
    let level = 1  // 等级
    let wid = 0 // 经验条
    // 经验值增长
    setInterval(() => {
      // 控制经验增长速率,与等级成反比
      wid += 3 / (level + 1)
      ex.style.width = wid + 'px'
      if (wid >= 500) { // wid大于500就升级
        level++
        levelUp()
        if (level == 3) { // 大于3级时,开启C类的另一个自动开火函数
          clearInterval(c.openFire())
          c.openFire2()
        }
        document.querySelector('h3').innerHTML = 'Lv' + level
        wid = 0
        clearInterval(timeB)
        // 根据不同等级,改变B实例的生成速率,与等级成正比
        getB((2500 - level * 400))
      }
    }, 50)
    // 升级时显示提示弹层
    function levelUp() {
      document.querySelector('.dialog').style.display = 'block'
      document.querySelector('.dialog').innerHTML = '升级了!'
      setTimeout(() => {
        document.querySelector('.dialog').style.display = 'none'
      }, 1000)
    }
复制代码

05.gif

5.子弹击中飞机的判断,以及飞机的击毁

因为子弹击中的判断和飞机的击毁所有的飞机实例都会用到,所以把这两个方法写到A这个父类里,子类飞机在发射子弹的时候,就会调用这个方法来判断子弹是否击中飞机,如果击中了,就把这个飞机实例给击毁,使用父类中击毁的方法。如果被击中的是本机C实例,那么就游戏结束。

  // 飞机的公共属性
  class A {
    constructor() {
     ......
    }
    .......
      // 击毁判定
      _attack(p, f, i) { // p参数为飞机实例,f参数为子弹,i为B实例元素的下标
        let fx = parseFloat(f.style.left) + 5 || 0
        let fy = parseFloat(f.style.top) + 10 || 0
        let px = parseFloat(p.ele.style.left) + 30 || 0
        let py = parseFloat(p.ele.style.top) + 30 || 0
        // 判断坐标是否重合
        if (Math.abs(fx - px) < 30 && Math.abs(fy - py) < 50) { 
          this._delete(p, f, i)
          return true
        }
      }
      // 击毁飞机的方法
      _delete(p, f, i) {
        p.ele.classList.add('del') // 添加飞机爆炸的样式
        if (p === c) { // 主机被击中,游戏结束
          life = false // 控制键盘事件的变量
          //清除所有定时器
          let qc = setInterval(function () { }, 1)
          for (let i = 0; i < qc; i++) {
            clearInterval(i)
          }
          document.querySelector('.dialog').style.display = 'block'
          document.querySelector('.dialog').innerHTML = '游戏结束!!'
          return
        } else { // 敌机被击中
          clearInterval(p.timer)
          clearInterval(p.moveTime)
          airB.splice(i, 1)
          f.remove()
          this.fen++ // 得分加1,并显示分数
          document.querySelector('span').innerHTML = this.fen
          setTimeout(() => {
            p.ele.remove()
            p = null  // 清除实例
          }, 200)
        }
      }
  }
复制代码

01.gif

01.jpg

总结

以前写代码都是把步骤封装成函数,用到哪一步就调用这个函数。用对象的方式来写这个游戏,根据类的继承,可以很方便的生成各种不同类型的飞机实例,给每一种飞机类型的添加独特的子弹攻击方式也很方便。给每一个关卡写一个BOSS飞机出来也不是难事。

我对继承的理解还是比较浅,第一次使用class来写小游戏,代码不多,300多行,里面也有很多不完善的地方。主要是自己想通过写小游戏来练一练思维和语法,有些的不好的地方欢迎指正,不喜勿喷。

03.jpg

04.jpg

猜你喜欢

转载自juejin.im/post/7127553483594530846