本文旨在经过分析官方给出的一个飞机大战小游戏的源代码来讲明如何进行小游戏的开发。
前天一个跳一跳
小游戏刷遍了朋友圈,也表明了微信小程序拥有了搭载游戏的功能(早该往这方面发展了,这才是应该有的形态嘛)。做为一个前端er,个人大刀早已经饥渴难耐了,赶忙去下一波最新的微信官方开发工具,体验一波小游戏要如何开发。javascript
咱们欣喜地看到能够直接点击小游戏体验一下,并且官方也有一个示例源代码,是一个简易版的飞机大战的源码,直接点开模拟器就能够看效果。html
(仍是原汁原味的打飞机游戏呀!)经过阅读这个源代码咱们即可以知道如何进行小游戏的开发了。废话少说直接进入主题,先来分析一波源码的总体结构。前端
路径 | 内容 |
---|---|
audio | 音频文件目录 |
images | 图片文件目录 |
js | 主要源代码目录 |
game.js | 游戏主入口 |
game.json | 游戏的配置文件 |
下面是官方示例中的js文件具体的做用java
./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.js
和game.json
是小游戏必需要有的两个文件
下面我会分析我认为主要的文件与结构,不会对每一行代码进行解析,你们有兴趣能够自行阅读官方的源码。每一个文件后会跟随我认为重要的几个小点。webpack
import './js/libs/weapp-adapter' import './js/libs/symbol' import Main from './js/main' new Main()
game.js
,在其中导入了小游戏官方提供的适配器,用于注入canvas以及模拟DOM以及BOM(后续会具体说明这个文件),能够在https://mp.weixin.qq.com/debu... 下载源代码,修改适合本身的版本并经过webpack打包自用。固然目前已经足够咱们使用。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 < 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) } // 游戏逻辑更新主函数 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 ) } }
requestAnimationFrame
看起来是否是很亲切)。
Main内结构清晰,主要理解整个流程就是调用
requestAnimationFrame
来不停地刷帧更新位置信息推进全部对象运动,每一个对象在每一帧都有新的位置,连起来就是动画了。分清位置的更新与对象的绘制是关键。
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) } }
/** * 游戏基础的精灵类 */ 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.x + this.width && spY >= this.y && spY <= this.y + this.height ) } }
能够看出画图主要是用的canvas里的drawImage方法,也是咱们自行开发小游戏之后会用到的方法。包括background,player等类都会继承自精灵类,而且会添加本身的update方法来暴露更新本身位置信息的接口。enermy还会包装一层爆炸动画的封装,思路大同小异,就不在多赘述了。
webapp-adapter.js
,该js会注入window对象并提供相应的canvas全局变量,也是文章中提到为何在main.js里找不到canvas变量在哪里定义的缘由了。因此咱们能够开开心心地使用canvas来开发小游戏了!!!webapp-adapter.js
来开发小游戏,(https://mp.weixin.qq.com/debu...)这是小游戏的api文档(当时找了好久)适配器的源码写得也很清晰,能够一读来了解一些,其中也有不少官方写的TODO的事情,还并不十分完善,若是想要快速移植已有的h5游戏代码使用适配器是颇有效的。若是想直接开发小游戏根据api文档直接来开发也是颇有效的方法,毕竟引入一层适配器仍是会有必定的开销。tips: 读一读适配器源码也有利于了解如何开发小程序(例如事件绑定之类的操做)web
小程序终于能够来作小游戏了,感受仍是休闲类的游戏会占主导地位,前端大大能够迎接新的战场啦哈哈哈~~~(接下来会去掉适配器用原生api改写官方demo)json
12.30更新canvas
经过以前的源码分析,咱们只能找到使用适配器版本的官方Demo,而找不到一个无适配器版本的官方Demo,因而本身动手丰衣足食,将官方Demo的适配器移除,下面介绍须要进行哪些改动。小程序
首先对适配器的源码简单阅读后能够发现,适配器作的事情就是模拟了window对象,而后将window对象按devtool和小程序运行的实际环境暴露给全局对象,供咱们来使用(devtool里就是window,实际环境中则是GameGlobal)。那么相应咱们就该把全部引用到window的地方都进行修改,由于实际运行环境中并无这个全局对象。下面我主要说明在源代码中使用到window的地方。微信小程序
libs/symbol.js
,改成直接使用原生支持的symbol来模拟私有变量,其余文件只需删除对该文件的引入便可。window.innerHeight
与window.innerWidth
改成使用 const { screenWidth, screenHeight, devicePixelRatio } = wx.getSystemInfoSync()
来获取屏幕宽高与dpr,并在相应地方进行替换。音频文件处理
主要是runtime/music.js
里与小游戏api的转化,主要是将 new Audio()
转化为wx.createInnerAudioContext()
方法获取实例和currentTime
在原生是一个只读属性,要改成seek
方法
let instance export default class Music { constructor() { if ( instance ) return instance instance = this // this.bgmAudio = new Audio() this.bgmAudio = wx.createInnerAudioContext() this.bgmAudio.loop = true this.bgmAudio.src = 'audio/bgm.mp3' // this.shootAudio = new Audio() this.bgmAudio = wx.createInnerAudioContext() this.shootAudio.src = 'audio/bullet.mp3' // this.boomAudio = new Audio() this.bgmAudio = wx.createInnerAudioContext() this.boomAudio.src = 'audio/boom.mp3' this.playBgm() } playBgm() { this.bgmAudio.play() } playShoot() { // this.shootAudio.currentTime = 0 this.boomAudio.seek(0) this.shootAudio.play() } playExplosion() { // this.boomAudio.currentTime = 0 this.boomAudio.seek(0) this.boomAudio.play() } }
图片文件的处理
new Image()
替换为wx.createImage()
获取实例便可canvas对象处理
由于须要全局暴露,因此咱们把canvas归于到Databus全局管理中去,使用wx.createCanvas()
获取全局canvas对象
export default class DataBus { constructor() { if ( instance ) return instance instance = this this.pool = new Pool() this.canvas = wx.createCanvas() this.reset() } }
事件机制
canvas
对象没有addEventListener
之类的方法,同理BOM和DOM对象都没有,因此须要用微信的api来处理事件,demo里则是换为wx.onTouchStart()
wx.onTouchMove()
wx.onTouchEnd()
替换先有的方法。(注意main.js里也有须要替换的,原理同样,不赘述了)
// player/index.js initEvent() { wx.onTouchStart(((e) => { let x = e.touches[0].clientX let y = e.touches[0].clientY // if (this.checkIsFingerOnAir(x, y)) { this.touched = true this.setAirPosAcrossFingerPosZ(x, y) } }).bind(this)) wx.onTouchMove(((e) => { let x = e.touches[0].clientX let y = e.touches[0].clientY if (this.touched) this.setAirPosAcrossFingerPosZ(x, y) }).bind(this)) wx.onTouchEnd(((e) => { this.touched = false }).bind(this)) }
requestAnimationFrame
方法
window
就能够了,全局对象里已经支持,setInterval
同样至此咱们已经完成了移除适配器,能够在一个极简的条件下开发咱们的小游戏了!!