zoukankan      html  css  js  c++  java
  • 微信小游戏 demo 飞机大战 代码分析 (一)(game.js, main.js)

    微信小游戏 demo 飞机大战 代码分析(一)(main.js)

    微信小游戏 demo 飞机大战 代码分析(二)(databus.js)

    微信小游戏 demo 飞机大战 代码分析(三)(spirit.js, animation.js)

    微信小游戏 demo 飞机大战 代码分析(四)(enemy.js, bullet.js, index.js)

    本博客将使用逐行代码分析的方式讲解该demo,本文适用于对其他高级语言熟悉,对js还未深入了解的同学,博主会尽可能将所有遇到的不明白的部分标注清楚,若有不正确或不清楚的地方,欢迎在评论中指正

    本文的代码均由微信小游戏自动生成的demo飞机大战中获取

    文件目录

    game.js

    • 首先让我们来看一下作为入口的game.js,可以看到在这里只进行了main类的初始化,因此下一步我们应该查看一下main类中的函数

    • 代码

      import Player     from './player/index'
      import Enemy      from './npc/enemy'
      import BackGround from './runtime/background'
      import GameInfo   from './runtime/gameinfo'
      import Music      from './runtime/music'
      import DataBus    from './databus'
      
      let ctx   = canvas.getContext('2d')
      let databus = new DataBus()
      
      /**
       * 游戏主函数
       */
      export default class Main {
        constructor() {
          // 维护当前requestAnimationFrame的id
          this.aniId    = 0
          //重新生成新的界面
          this.restart()
        }
      
        //界面生成函数
        restart() {
          databus.reset()
      
          canvas.removeEventListener(
            'touchstart',
            this.touchHandler
          )
      
          this.bg       = new BackGround(ctx)
          this.player   = new Player(ctx)
          this.gameinfo = new GameInfo()
          this.music    = new Music()
      
          this.bindLoop     = this.loop.bind(this)
          this.hasEventBind = false
      
          // 清除上一局的动画
          window.cancelAnimationFrame(this.aniId);
      
          this.aniId = window.requestAnimationFrame(
            this.bindLoop,
            canvas
          )
        }
      
        /**
         * 随着帧数变化的敌机生成逻辑
         * 帧数取模定义成生成的频率
         */
        enemyGenerate() {
          if ( databus.frame % 30 === 0 ) {
            let enemy = databus.pool.getItemByClass('enemy', Enemy)
            enemy.init(6)
            databus.enemys.push(enemy)
          }
        }
      
        // 全局碰撞检测
        collisionDetection() {
          let that = this
      
          databus.bullets.forEach((bullet) => {
            for ( let i = 0, il = databus.enemys.length; i < il;i++ ) {
              let enemy = databus.enemys[i]
      
              if ( !enemy.isPlaying && enemy.isCollideWith(bullet) ) {
                enemy.playAnimation()
                that.music.playExplosion()
      
                bullet.visible = false
                databus.score  += 1
      
                break
              }
            }
          })
      
          for ( let i = 0, il = databus.enemys.length; i < il;i++ ) {
            let enemy = databus.enemys[i]
      
            if ( this.player.isCollideWith(enemy) ) {
              databus.gameOver = true
      
              break
            }
          }
        }
      
        // 游戏结束后的触摸事件处理逻辑
        touchEventHandler(e) {
           e.preventDefault()
      
          let x = e.touches[0].clientX
          let y = e.touches[0].clientY
      
          let area = this.gameinfo.btnArea
      
          if (   x >= area.startX
              && x <= area.endX
              && y >= area.startY
              && y <= area.endY  )
            this.restart()
        }
      
        /**
         * canvas重绘函数
         * 每一帧重新绘制所有的需要展示的元素
         */
        render() {
          ctx.clearRect(0, 0, canvas.width, canvas.height)
      
          this.bg.render(ctx)
      
          databus.bullets
                .concat(databus.enemys)
                .forEach((item) => {
                    item.drawToCanvas(ctx)
                  })
      
          this.player.drawToCanvas(ctx)
      
          databus.animations.forEach((ani) => {
            if ( ani.isPlaying ) {
              ani.aniRender(ctx)
            }
          })
      
          this.gameinfo.renderGameScore(ctx, databus.score)
      
          // 游戏结束停止帧循环
          if ( databus.gameOver ) {
            this.gameinfo.renderGameOver(ctx, databus.score)
      
            if ( !this.hasEventBind ) {
              this.hasEventBind = true
              this.touchHandler = this.touchEventHandler.bind(this)
              canvas.addEventListener('touchstart', this.touchHandler)
            }
          }
        }
      
        // 游戏逻辑更新主函数
        update() {
          if ( databus.gameOver )
            return;
      
          this.bg.update()
      
          databus.bullets
                 .concat(databus.enemys)
                 .forEach((item) => {
                    item.update()
                  })
      
          this.enemyGenerate()
      
          this.collisionDetection()
      
          if ( databus.frame % 20 === 0 ) {
            this.player.shoot()
            this.music.playShoot()
          }
        }
      
        // 实现游戏帧循环
        loop() {
          databus.frame++
      
          this.update()
          this.render()
      
          this.aniId = window.requestAnimationFrame(
            this.bindLoop,
            canvas
          )
        }
      }
      
      

    一点基础知识

    • 帧:游戏中的帧和动画中的帧,视频中的帧概念类似,即游戏过程中物体和动画效果变化的一个周期。
    • 精灵:是游戏中的一个基本概念,指的是在游戏中的一个基本物体或动画或贴图,如NPC或者敌人,在本例中有子弹,敌机和玩家
    • 回调函数:在特定事件发生后,由事件方进行调用的函数
    • 画布:顾名思义就是使用了画东西的地方,其实就是用于渲染相关内容的位置

    main.js

    main 即为游戏的主函数,我们来逐个分析一下其内容

    • export default 为 ES6,即js的一个版本中的语言,在js中,任何类或对象使用export既可以在其他文件中通过import进行调用使用,使用 import {类或对象名} from 文件路径,但若使用export default则可以省略 { }, 但一份文件中仅仅可以存在一个export default

    初始化生成对象

    1. 在main函数前其调用生成了一个2d画布,名称为ctx

    2. 生成了一个数据总线对象databus,数据总线的内容将在下次博客中解释

    main 类

    contructor()

    contructor 用于创建main 对象,其中调用了restart函数,因此我们跳转到restart函数中进行查看

    restart()

    该函数用于重新生成一个界面

    • 首先重置数据总线对象的内容

    • 监听触碰事件

    • 初始化背景对象,玩家对象,游戏信息对象和音乐对象

      this.bg       = new BackGround(ctx)
      this.player   = new Player(ctx)
      this.gameinfo = new GameInfo()
      this.music    = new Music()
      
    • 绑定事件循环,初始化状态,并开始运行

      this.bindLoop     = this.loop.bind(this)
          this.hasEventBind = false
      
          // 清除上一局的动画
          window.cancelAnimationFrame(this.aniId);
      
          this.aniId = window.requestAnimationFrame(
            this.bindLoop,
            canvas
          )
      
    • js语法中,可以将某个对象的方法单独拿出来作为一个方法使用,但是在使用过程中,避免不了出现未知该函数所指向的对象的情况

      • 例如在该代码中,若写作this.bindLoop = this.loop 那么该函数所属的类就丢失了,那么该函数一些执行也就无法进行
      • 为了避免这样的情况,js使用bind函数,将所需的类绑定到该函数上,这样就有效地解决了这个问题
    • window.requestAnimationFrame()

      • 该函数使用了两个参数,第一个是回调函数,第二个是画布
      • 画布的功能即用来工作的区域
      • 而回调函数的作用是在浏览器在该帧渲染完毕之后,调用的函数,根据博主的资料查询,回调函数执行次数通常是每秒60次,但在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。
      • 在该例子中,restart中的该函数仅仅是使用初始化的main对象更新loop函数,并将其作为刷新内容
      • 但由于main对象中的逻辑会产生变更,因此在之后的loop函数也对其进行了请求,并绑定了参数。使用新缠身过的main对象和新产生的canvas在浏览器中进行渲染

    enemyGenerate()

    该函数用于生成敌人飞机

    • 在databus中有一个frame参数,相当于每次刷新(更新)的计数器,
    • 使用该函数时,若刷新次数为30的整数倍时,就会申请一个新的敌机对象并初始化,其中init的参数为该敌机的速度,生成后加入databus对象的存储数组中

    collisionDetection()

    全局碰撞检测

    • 首先对于每个子弹,判断子弹是否与敌机相撞,若相撞则隐藏敌机和子弹
      • 该处需要解释一下的是,将子弹和敌机隐藏的是直接代表子弹和敌机已经销毁
      • 但此处并未在逻辑中将对象销毁,而是在绘图中判断其visible是否为true,若为true则才会画入画布中
      • 而统一更新回收入pool
    • 对每一架敌机,判断是否与用户相撞,若相撞,则在databus中设置游戏结束

    touchEventHandler(e)

    游戏结束后判断是否重新开始的函数

    • 获取触摸的坐标
    • 在gameinfo中获取重新开始上下左右xy坐标
    • 比对触摸位置是否在按钮内部,若在则调用restart函数重新启动函数

    render()

    渲染函数,用于渲染场景,用于每次修改内容后重新渲染场景内容(每一帧调用)

    • 清除画布的所有内容
    • 调用背景类的渲染函数,在ctx上渲染出一个背景
    • concat函数为js函数,用于连接连个数组
    • 连接databus中的bullets和enemys数组,并且将这个合成数组中的每一项画到画布上,画到画布上的操作是以利用函数drawToCanvas,而该函数实现于Spirite类中,
    • spirit即精灵,是游戏设计中的一个概念,相当于游戏中一个最基本的物体或者一个概念,该demo中的spirit实现方式将在后续博客中写上
    • 将player画到画布上,同样的,player也继承于Spirit类
    • 将所有动画类的未播放的内容进行播放,在该demo中,Animation类继承Spirit,而所有物体均继承于Animation类,因此都具有该能力,不过由于所有物体都均仅有一帧图像,因此无需进行播放,
    • 在databus类中有一个专门存放动画的数组,任何继承于Animation类的对象都会在初始化构造时被放入该数组当中
    • 调用gameinfo的函数更新图像左上角的分数内容
    • 判断,若游戏结束
      • 若未绑定事件,将touchHandler事件添加绑定,
      • 将事件加入监听中
      • (该段代码博主并未非常理解,欢迎在评论中指正或指导)

    update()

    游戏逻辑更新主函数

    • 若游戏已经结束,不执行该代码,直接放回结束
    • 更新背景参数
    • 对所有bullets和enemys对象进行更新
    • 调用enemyGenerate() 生成敌人(根据前面描述,需要判断是否满足刚好经过30帧)
    • 进行全局碰撞检测,并进行处理
    • 判断是否经过20帧,每经过20帧,调用player生成一个新的bullet(子弹),并且调用射击音乐

    loop()

    实现游戏帧循环

    • 每次循环将帧计数器加一
    • 更新逻辑
    • 渲染逻辑更新后的场景
    • 使用window.requestAnimationFrame进行调用,为下一帧界面渲染做准备
  • 相关阅读:
    Android常用URI收藏
    2017 ZSTU寒假排位赛 #3
    HDU 3689 Infinite monkey theorem ——(自动机+DP)
    CodeForces 755D PolandBall and Polygon ——(xjbg)
    2017 ZSTU寒假排位赛 #2
    HDU 3264 Open-air shopping malls ——(二分+圆交)
    HDU 1255 覆盖的面积 ——(线段树+扫描线)
    HDU 3265 Posters ——(线段树+扫描线)
    2017 ZSTU寒假排位赛 #1
    UVA 11853 Paintball ——(dfs+圆交判定)
  • 原文地址:https://www.cnblogs.com/Phoenix-blog/p/10948630.html
Copyright © 2011-2022 走看看