在10月份的时候,我们接手了一款红警ol的小游戏。其玩法是玩家选择红警ol游戏中的经典攻击武器,并且拖动武器到规定的地图部分,来抵御来自幽灵的一波波进攻,最后在小游戏结束时生成游戏过程中的武器摆放阵型图来让玩家主动转发,配合小游戏专有的关系链数据来提前预热红警ol的游戏。
技术选型:
依附于微信的生态,小游戏一诞生就已经得到了众多国内知名的游戏开发引擎的适配,而一些国外的游戏开发框架在社区内也有针对小游戏版本的提供。红警ol小游戏选择的游戏引擎开发库是Phaser.js,一款相对小众但是在开源社区内又是比较受欢迎的游戏框架。
为什么选择Phaser.js?
Phaser.js给一个刚刚接触游戏开发的新手的感觉就是,这个完全是为新手而开发的游戏框架。官网上有近700多个开发示例,而且针对不同的游戏内置对象都有相当丰富的示例介绍和可以在线调试运行环境,通过示例的源码,可以很快速的进行上手开发。
另外Phaser.js是一个专注于2D领域的游戏引擎,其提供的功能模块非常丰富,可以很好的满足我们开发一款2D小游戏的需求。这里要提一下的是,Phaser.js提供了四种不同的物理引擎(Arcade、P2、Box2d和Najia)来帮我开发一些带物理效果的游戏,四种物理引擎在提供的功能的丰富度和性能上表现都不同,开发者可以根据自己游戏所要体现的物理效果丰富度和表现力的不同来搭配不同的游戏物理引擎。
(图片来自网络)
游戏开发经验
1)、在Phaser中,我们通过State来管理不同的游戏场景,通常一个游戏可以分为游戏加载场景、游戏开始场景、游戏进行中场景和游戏结束场景,而在小游戏中,我们还会多个游戏排行榜场景,主要承载一些游戏排行榜的内容和一些分享的功能。场景区分之后就是用不同的State来管理这些场景的加载和显示了。每个State都有初始化(init)、预加载(preload)、准备就绪(create)、更新周期(update)、渲染完毕(render) 五个生命周期函数,管理一个场景也就是在这些生命周期函数内执行相应的逻辑。
2)、小游戏资源打包
在web开发中,我们通常为了提升性能而将多张小的图片合成一张雪碧图,这样可以节省大量网络请求时间。在小游戏的开发上,Phaser.js支持加载由多张图片生成的纹理图集,我们可以通过TexturePacker这个工具来帮助我们生成纹理图集,具体的使用过程这里就不阐述,网上教程一大把。
当把所有的纹理资源都打包成一张张纹理图集之后,可以进一步通过图片压缩工具去压缩图片,来达到图片加载性能的最大化。通常,我们将同一场景的图片打包成一个纹理图集,或者将一系列的动画序列帧打包成一个纹理图集。
3)、小游戏资源的版本管理
小游戏本身对游戏的包体体积是有大小限制的,势必我们不能将所有游戏图集资源放在游戏的包体内一块打包上传,通常的做法是将非首屏的所有游戏资源放置在一个文件内,然后将该文件夹放在CDN的服务器上,在游戏的开始场景中,通过loading场景去加载CDN上的资源。
由于部门所使用的CDN系统是需要人为手动线上点击发布,所以当新版本的小游戏在本地资源修改后,小游戏新版本发布之后的更新时间和新的资源文件同步时间无法做到绝对的一致,这样就会存在问题。新的资源更新上线了,但是用户的微信小游戏还没更新,那么引用到的资源就会错乱,或者用户的微信小游戏更新了,但是资源还没更新,同样也会产生资源引用错误。所以解决的办法是:
1、每次版本更新之前,对所有资源生成一个文件MD5值的列表保存在一个配置文件内
2、修改资源文件的名称为文件名加文件MD5值的方式
3、将修改好的资源文件增量发布到线上
4、将配置文件放置在本地,通过配置文件来加载远程资源
优化
1)首屏资源加载优化
在部分的安卓低端机型下发现,小游戏长时间处在loading,小游戏只有真正绘制了首帧之后,才会隐藏 loading 页。为了减少用户看到loading或黑屏的概率,我们把小游戏首屏的资源放在包体内一块打包上传,这样小游戏下载完成后,首屏也会很快的出现。
2)使用对象池
红警小游戏的玩法是有大波的敌人来进攻基地,当敌人被消灭后,我们并不是直接的销毁它,而是将原来的这个游戏的角色Obejct放置在一个对象池内,当下次再创建这个游戏角色时,就可以直接在对象池获取即可,这样可以省去游戏中频繁创建游戏角色对象带来的性能开销。
后端接口开发
开发koa+myql
由于之前用过express但对koa不熟,所以稍微花了一点时间去上手,但实际上koa的上手十分简单,框架使用简洁,基本是开箱即用。
封装async sql方法
node8提供了对async原生的支持,稍微封装了sql函数,便可以以同步的写法来执行sql。
const mysql = require( 'mysql' ) const config = require( './config' ) const pool = mysql.createPool(config.mysql) // sql辅助函数 const sql = (query, params = []) => new Promise((resolve, reject) => { pool.query(query, params, (err, results, fields) => { if (err) return reject(err) resolve(results) }) }) process.on( 'exit' , function () { console.log( 'end' ) pool.end() }) // 调用方式写起来跟同步一样舒服 res = await sql( "select * from table where id = ?" , [id]) |
接口数据校验
为了防止玩家随意篡改分数,跟前端约定一套简单的签名机制,校验失败返回错误
let jsonStr = JSON.stringify(body) let [time, random, sign] = header.sign.split( '|' ) md5Result = '哈希值' //只是举例,线上跟这里的例子有出入 if (md5Result != sign) { ctx.status = 403 ctx.body = { success: false , error: 'invite sign for score' , } return } |
使用JWT登录态维护
后端使用了JWT来维护登录态,具体流程为
客户端调用wx.login获取code,并提交到后端接口。
后端请求微信的接口获取用户openid,第一次登录则插入新纪录到mysql,并生成唯一用户id。
把uid,openid等用户标识用JWT签名,发还客户端作为校验凭证。
之后的每次请求都会带上该凭证,在koa的中间件中处理登录态。
登录操作
//获取openid const res = await request({ url: 'https://api.weixin.qq.com/sns/jscode2session' , qs: { appid: config.appId, secret: config.appSecret, js_code: body.code, grant_type: 'authorization_code' }, json: true , }) //...跳过一部分登录、注册逻辑 const exp = Math.floor(Date.now() / 1000) + tokenAge //生成登录凭证
const exp = Math.floor(Date.now() / 1000) + tokenAge//生成登录凭证const token = jwt.sign({ exp, iss: 'redalert-td', uid: ctx.body.id, openid: ctx.body.openid }, config.secret) |
在中间件处理登录态
const getToken = (ctx) => { if (!ctx.header || !ctx.header.authorization) { return null } const authorization = ctx.request.header.authorization const m = authorization.match(/^Bearer\s(.+)/) if (!m) return null return m[1] } module.exports = async (ctx, next) => { try { const token = getToken(ctx) if (!token) throw new Error( 'need login' ) const decoded = jwt.verify(token, config.secret) if (decoded.uid !== ctx.request.body.id || !decoded.openid) { ctx.status = 403 ctx.body = { success: false , error: 'forbidden' } return } //省略一部分逻辑 } catch (e) { log.error(e) ctx.status = 401 ctx.body = { success: false , error: 'need login' } return } await next() } |
轮训玩家邀请状态
小游戏后期加入了功能:当局邀请多个朋友进入游戏给玩家提供buff,需要邀请后游戏中的玩家能立刻感知邀请成功,当局由于开发时间较为紧迫,前后端联调时间不多,原有的websocket方案改为轮训实现,轮训时间为5s,其中当局的邀请信息不需要入库,存在redis里设置过期时间即可。
服务部署
整个小游戏的后端资源分为静态资源和接口两部分,部署在腾讯云上,其中静态资源等存放在腾讯云的cos上,mysql是腾讯云的cdb,接口部分则直接用node8+koa+pm2部署的http服务,并使用了docker部署,镜像使用的是keymetrics/pm2
。
FROM keymetrics /pm2 :8-jessie WORKDIR /usr/src/app COPY package*.json ./ RUN npm install --only=production COPY . . EXPOSE 8080 CMD [ "pm2-runtime" , "start" , "process.prod.yml" ] |
其中https直接使用了腾讯云的负载均衡,非常方便(腾讯云真没给我广告费),不需要在服务器上配置任何https相关服务或在node里实现https,直接上传证书即可,用法和效果都跟stgw差不多。