微信小游戏初体验

本文旨在通过分析官方给出的一个飞机大战小游戏的源代码来说明如何进行小游戏的开发。

1.前言

前天一个 跳一跳 小游戏刷遍了朋友圈,也代表了微信小程序拥有了搭载游戏的功能(早该往这方面发展了,这才是应该有的形态嘛)。作为一个前端er,我的大刀早已经饥渴难耐了,赶紧去下一波最新的微信官方开发工具,体验一波小游戏要如何开发。

我们欣喜地看到可以直接点击小游戏体验一下,而且官方也有一个示例源代码,是一个简易版的飞机大战的源码,直接点开模拟器就可以看效果。

2.源码分析

(还是原汁原味的打飞机游戏呀!)通过阅读这个源代码我们便可以知道如何进行小游戏的开发了。废话少说直接进入主题,先来分析一波源码的整体结构。

路径内容
audio音频文件目录
images图片文件目录
js主要源代码目录
game.js游戏主入口
game.json游戏的配置文件

下面是官方示例中的js文件具体的作用

./js
├── base                                   // 定义游戏开发基础类
│   ├── animatoin.js                       // 帧动画的简易实现
│   ├── pool.js                            // 对象池的简易实现
│   └── sprite.js                          // 游戏基本元素精灵类
├── libs
│   ├── symbol.js                          // ES6 Symbol简易兼容
│   └── weapp-adapter.js                   // 小游戏适配器
├── npc
│   └── enemy.js                           // 敌机类
├── player
│   ├── bullet.js                          // 子弹类
│   └── index.js                           // 玩家类
├── runtime
│   ├── background.js                      // 背景类
│   ├── gameinfo.js                        // 用于展示分数和结算界面
│   └── music.js                           // 全局音效管理器
├── databus.js                             // 管控游戏状态
└── main.js                                // 游戏入口主函数

官方文档中提到, game.jsgame.json 是小游戏必须要有的两个文件

下面我会分析我认为主要的文件与结构,不会对每一行代码进行解析,大家有兴趣可以自行阅读官方的源码。每个文件后会跟随我认为重要的几个小点。

game.js

import './js/libs/weapp-adapter'
import './js/libs/symbol'

import Main from './js/main'

new Main()
  1. 小程序启动会调用 game.js ,在其中导入了小游戏官方提供的适配器,用于注入canvas以及模拟DOM以及BOM(后续会具体说明这个文件),可以在 https://mp.weixin.qq.com/debu… 下载源代码,修改适合自己的版本并通过webpack打包自用。当然目前已经足够我们使用。
  2. 导入symbol的polyfill,主要用于模拟ES6类的私有变量。
  3. 导入Main类并实例化Main,于是顺藤摸瓜我们将目光移至Main.js

Main.js

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() {
    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()

    window.requestAnimationFrame(
      this.loop.bind(this),
      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 = area.startX
        && x = area.startY
        && y  {
              item.drawToCanvas(ctx)
            })

    this.player.drawToCanvas(ctx)

    databus.animations.forEach((ani) => {
      if ( ani.isPlaying ) {
        ani.aniRender(ctx)
      }
    })

    this.gameinfo.renderGameScore(ctx, databus.score)
  }

  // 游戏逻辑更新主函数
  update() {
    this.bg.update()

    databus.bullets
           .concat(databus.enemys)
           .forEach((item) => {
              item.update()
            })

    this.enemyGenerate()

    this.collisionDetection()
  }

  // 实现游戏帧循环
  loop() {
    databus.frame++

    this.update()
    this.render()

    if ( databus.frame % 20 === 0 ) {
      this.player.shoot()
      this.music.playShoot()
    }

    // 游戏结束停止帧循环
    if ( databus.gameOver ) {
      this.gameinfo.renderGameOver(ctx, databus.score)

      this.touchHandler = this.touchEventHandler.bind(this)
      canvas.addEventListener('touchstart', this.touchHandler)

      return
    }

    window.requestAnimationFrame(
      this.loop.bind(this),
      canvas
    )
  }
}
  1. 导入了创建游戏需要的我放飞机,敌方飞机,背景,游戏信息,音乐,游戏全局数据类,并获取了canvas的上下文(看到这是不是有一个疑惑,canvas到底是从哪里定义?先带着这个问题最后再说),创建了一个全局数据实例(后面会提到)。
  2. 创建Main的实例自然会调用构造方法,在构造方法中调用restart函数,进行了游戏的初始化并进行循环刷帧( requestAnimationFrame 看起来是不是很亲切)。
  3. loop函数中我们可以看到主要调用了update, render方法,并设置了player发射子弹的时间,对游戏是否结束进行判断,最后接着刷帧。
  4. update方法会调用各个场景内对象的update方法来更新他们的位置以及其他信息。
  5. render方法会调用各个场景内对象的render方法来将他们绘制到canvas中。

Main内结构清晰,主要理解整个流程就是调用 requestAnimationFrame 来不停地刷帧更新位置信息推动所有对象运动,每个对象在每一帧都有新的位置,连起来就是动画了。分清位置的更新与对象的绘制是关键。

databus.js

import Pool from './base/pool'

let instance

/**
 * 全局状态管理器
 */
export default class DataBus {
  constructor() {
    if ( instance )
      return instance

    instance = this

    this.pool = new Pool()

    this.reset()
  }

  reset() {
    this.frame      = 0
    this.score      = 0
    this.bullets    = []
    this.enemys     = []
    this.animations = []
    this.gameOver   = false
  }

  /**
   * 回收敌人,进入对象池
   * 此后不进入帧循环
   */
  removeEnemey(enemy) {
    let temp = this.enemys.shift()

    temp.visible = false

    this.pool.recover('enemy', enemy)
  }

  /**
   * 回收子弹,进入对象池
   * 此后不进入帧循环
   */
  removeBullets(bullet) {
    let temp = this.bullets.shift()

    temp.visible = false

    this.pool.recover('bullet', bullet)
  }
}
  1. 我们可以看出,databus是一个单例对象,不论在其他代码中new多少次,都是返回的同一个实例,符合我们的期望。
  2. reset定义了所需要的数据源并初始化
  3. 通过一个对象池的概念,控制当前页面对象的数量,避免使用js原有的垃圾处理机制,而是通过对象池来复用已经创建的对象,算是一个性能优化。
  4. frame属性主要是用来刷帧的时候用来控制子弹的发射与敌机的出现时间。

sprite.js

/**
 * 游戏基础的精灵类
 */
export default class Sprite {
  constructor(imgSrc = '', width=  0, height = 0, x = 0, y = 0) {
    this.img     = new Image()
    this.img.src = imgSrc

    this.width  = width
    this.height = height

    this.x = x
    this.y = y

    this.visible = true
  }

  /**
   * 将精灵图绘制在canvas上
   */
  drawToCanvas(ctx) {
    if ( !this.visible )
      return

    ctx.drawImage(
      this.img,
      this.x,
      this.y,
      this.width,
      this.height
    )
  }

  /**
   * 简单的碰撞检测定义:
   * 另一个精灵的中心点处于本精灵所在的矩形内即可
   * @param{Sprite} sp: Sptite的实例
   */
  isCollideWith(sp) {
    let spX = sp.x + sp.width / 2
    let spY = sp.y + sp.height / 2

    if ( !this.visible || !sp.visible )
      return false

    return !!(   spX >= this.x
              && spX = this.y
              && spY <= this.y + this.height  )
  }
}
  1. 作为所有场景对象的基类,定义了所有精灵对象基本有的信息(位置,图片,是否可见)
  2. 定义了两种能力,检测碰撞与将自己绘制在canvas上

可以看出画图主要是用的canvas里的drawImage方法,也是我们自行开发小游戏以后会用到的方法。包括background,player等类都会继承自精灵类,并且会添加自己的update方法来暴露更新自己位置信息的接口。enermy还会包装一层爆炸动画的封装,思路大同小异,就不在多赘述了。

3.结论

  1. 我们发现小游戏的开发与我们使用canvas进行h5小游戏的开发并没有什么太大的区别,无论从绘图的api还是事件的api都十分相似,还可以用window对象,这主要归功于官方提供的 webapp-adapter.js ,该js会注入window对象并提供相应的canvas全局变量,也是文章中提到为什么在main.js里找不到canvas变量在哪里定义的原因了。所以我们可以开开心心地使用canvas来开发小游戏了!!!
  2. 官方还说了一句,可以不引入 webapp-adapter.js 来开发小游戏,( https://mp.weixin.qq.com/debu… )这是小游戏的api文档(当时找了很久)适配器的源码写得也很清晰,可以一读来了解一些,其中也有很多官方写的TODO的事情,还并不十分完善,如果想要快速移植已有的h5游戏代码使用适配器是很有效的。如果想直接开发小游戏根据api文档直接来开发也是很有效的方法,毕竟引入一层适配器还是会有一定的开销。

tips: 读一读适配器源码也有利于了解如何开发小程序(例如事件绑定之类的操作)

4.结语

小程序终于可以来做小游戏了,感觉还是休闲类的游戏会占主导地位,前端大大可以迎接新的战场啦哈哈哈~~~(接下来会去掉适配器用原生api改写官方demo)

SegmentFault责编内容来自:SegmentFault (源链) | 更多关于

阅读提示:酷辣虫无法对本内容的真实性提供任何保证,请自行验证并承担相关的风险与后果!
本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 移动开发 » 微信小游戏初体验

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录