1.序言

前天一个跳一跳小游戏刷遍了朋友圈,也代表了微信小程序拥有了搭载游戏的功能(早该往这方面发展了,这才是该当有的形态嘛 ...

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

微信跳跳php微信小游戏跳一跳初体验 Python

2.源码剖析

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

./js下面是官方示例中的js文件详细的浸染

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

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

game.js

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打包自用。
当然目前已经足够我们利用。

导入symbol的polyfill,紧张用于仿照ES6类的私有变量。

导入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 = thisdatabus.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 = falsedatabus.score += 1break}}}) for ( let i = 0, il = databus.enemys.length; i < il;i++ ) { let enemy = databus.enemys[i] if ( this.player.isCollideWith(enemy) ) {databus.gameOver = truebreak}}} //游戏结束后的触摸事宜处理逻辑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)}}

导入了创建游戏须要的我放飞机,敌方飞机,背景,游戏信息,音乐,游戏全局数据类,并获取了canvas的高下文(看到这是不是有一个迷惑,canvas到底是从哪里定义?先带着这个问题末了再说),创建了一个全局数据实例(后面会提到)。

创建Main的实例自然会调用布局方法,在布局方法中调用restart函数,进行了游戏的初始化并进行循环刷帧( requestAnimationFrame 看起来是不是很亲切)。

loop函数中我们可以看到紧张调用了update, render方法,并设置了player发射子弹的韶光,对游戏是否结束进行判断,末了接着刷帧。

update方法会调用各个场景内工具的update方法来更新他们的位置以及其他信息。

render方法会调用各个场景内工具的render方法来将他们绘制到canvas中。

Main内构造清晰,紧张理解全体流程便是调用 requestAnimationFrame 来一直地刷帧更新位置信息推动所有工具运动,每个工具在每一帧都有新的位置,连起来便是动画了。
分清位置的更新与工具的绘制是关键

databus.js

import Pool from './base/pool'let instance/ 全局状态管理器/export default class DataBus { constructor() { if ( instance ) return instanceinstance = thisthis.pool = new Pool() this.reset()}reset() { this.frame = 0this.score = 0this.bullets = [] this.enemys = [] this.animations = [] this.gameOver = false} / 回收仇敌,进入工具池 此后不进入帧循环 /removeEnemey(enemy) {let temp = this.enemys.shift()temp.visible = falsethis.pool.recover('enemy', enemy)} / 回收子弹,进入工具池 此后不进入帧循环 /removeBullets(bullet) {let temp = this.bullets.shift()temp.visible = falsethis.pool.recover('bullet', bullet)}}

我们可以看出,databus是一个单例工具,不论在其他代码中new多少次,都是返回的同一个实例,符合我们的期望。

reset定义了所须要的数据源并初始化

通过一个工具池的观点,掌握当前页面对象的数量,避免利用js原有的垃圾处理机制,而是通过工具池来复用已经创建的工具,算是一个性能优化。

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 ) returnctx.drawImage( this.img, this.x, this.y, this.width, this.height)} / 大略的碰撞检测定义: 另一个精灵的中央点处于本精灵所在的矩形内即可 @param{Sprite} sp: Sptite的实例 /isCollideWith(sp) {let spX = sp.x + sp.width / 2let spY = sp.y + sp.height / 2if ( !this.visible || !sp.visible ) return falsereturn !!( spX >= this.x&& spX <= this.x + this.width&& spY >= this.y&& spY <= this.y + this.height )}}

作为所有场景工具的基类,定义了所有精灵工具基本有的信息(位置,图片,是否可见)

定义了两种能力,检测碰撞与将自己绘制在canvas上

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

3.结论

我们创造小游戏的开拓与我们利用canvas进行h5小游戏的开拓并没有什么太大的差异,无论从绘图的api还是事宜的api都十分相似,还可以用window工具,这紧张归功于官方供应的 webapp-adapter.js ,该js会注入window工具并供应相应的canvas全局变量,也是文章中提到为什么在main.js里找不到canvas变量在哪里定义的缘故原由了。
以是我们可以开愉快心地利用canvas来开拓小游戏了!


官方还说了一句,可以不引入 webapp-adapter.js 来开拓小游戏,( https://mp.weixin.qq.com/debu... )这是小游戏的api文档(当时找了良久)适配器的源码写得也很清晰,可以一读来理解一些,个中也有很多官方写的TODO的事情,还并不十分完善,如果想要快速移植已有的h5游戏代码利用适配器是很有效的。
如果想直接开拓小游戏根据api文档直接来开拓也是很有效的方法,毕竟引入一层适配器还是会有一定的开销。

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

4.结语

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

原文来源:http://www.wxapp-union.com/portal.php?mod=view&aid=3501

如有侵权,请联系删除!