- 2019-05-27
- 红警ol小游戏开发小结
在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 token = jwt.sign({ exp, iss: [代码][代码]'redalert-td'[代码][代码], uid: ctx.body.id, openid: ctx.body.openid }, config.secret)[代码].跳过一部分登录、注册逻辑 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差不多。
2019-05-16 - CSS 火焰?不在话下
正文从下面开始。 今天的小技巧是使用纯 CSS 生成火焰,逼真一点的火焰。 嗯,长什么样子?在 CodePen 上输入关键字 [代码]CSS Fire[代码],能找到这样的: [图片] 或者这样的: [图片] 我们希望,仅仅使用 CSS ,效果能再更进一步吗?能不能是这样子: [图片] 如何实现 嗯,我们需要使用 [代码]filter[代码] + [代码]mix-blend-mode[代码] 的组合来完成。 很多 CSS 华而不实的效果都是 [代码]filter[代码] + [代码]mix-blend-mode[代码],很有意思,但是业务中根本用不上,当然多了解了解总没坏处。 如上图,整个蜡烛的骨架, 除去火焰的部分很简单,掠过不讲。主要来看看火焰这一块如何生成,并且如何赋予动画效果。 Step 1: filter blur && filter contrast 模糊滤镜叠加对比度滤镜产生的融合效果。 单独将两个滤镜拿出来,它们的作用分别是: [代码]filter: blur()[代码]: 给图像设置高斯模糊效果。 [代码]filter: contrast()[代码]: 调整图像的对比度。 但是,当他们“合体”的时候,产生了奇妙的融合现象。 先来看一个简单的例子: [图片] 仔细看两圆相交的过程,在边与边接触的时候,会产生一种边界融合的效果,通过对比度滤镜把高斯模糊的模糊边缘给干掉,利用高斯模糊实现融合效果。 利用上述 [代码]filter blur & filter contrast[代码],我们要先生成一个类似火焰形状的三角形。(略去过程) 这里类似火焰形状的三角形的具体实现过程,在这篇文章有详细的讲解:你所不知道的 CSS 滤镜技巧与细节 [图片] 父元素添加 [代码]filter: blur(5px) contrast(20)[代码],会变成这样: [图片] Step 2: 火焰粒子动画 看着已经有点样子了,接下来是火焰动画,我们先去掉父元素的 [代码]filter: blur(5px) contrast(20)[代码] ,然后继续 。 这里也是利用了 [代码]filter[代码] 的融合效果,我们在上述火焰中,利用 SASS 随机均匀分布大量大小不一的圆形棕色 div ,隐匿在火焰三角内部,大概是这样: [图片] 接下来,我们再利用 SASS,给中间每个小圆赋予一个从下往上逐渐消失的动画,并且均匀赋予不同的 [代码]animation-delay[代码],看起来会是这样: [图片] OK,最重要的一步,我们再把父元素的 [代码]filter: blur(5px) contrast(20)[代码] 打开,神奇的火焰效果就出来了: [图片] Step 3: mix-blend-mode 润色 当然,上述效果已经很不错了。经过各种尝试,调整参数,最后我发现加上 [代码]mix-blend-mode: screen[代码] 混合模式,效果更好,得到头图上面的最终效果如下: [图片] 完整源码在我的 CodePen 上:CodePen Demo – CSS Fire 另外一些效果 当然,掌握了这种方法后,这种生成火焰的技巧也可以迁移到其他效果去。下图是我鼓捣到另外一个小 Demo,当 hover 到元素的时候,产生火焰效果: [图片] CodePen Demo – Hover Fire 嗯,这些其实都是对滤镜及混合模式的一些搭配运用。按照惯例,肯定有人会留言喷了,整这些花里胡哨的有什么用,性能又不好,业务中敢上不把你的腿给打骨折。 [图片] 于我而言,虚心接受各种批评质疑及各种不同的观点,当然我是觉得搞技术一方面是实用,另一方面是兴趣使然,自娱自乐。希望喷子绕道~ 回到正题,了解了这种黏糊糊湿答答的技巧后,还可以折腾出其他很多有意思的效果,当然可能需要更多的去尝试,如下面使用一个标签实现的滴水效果: [图片] CodePen Demo – 单标签实现滴水效果 值得注意的细节点 动画虽然美好,但是具体使用的过程中,仍然有一些需要注意的地方: CSS 滤镜可以给同个元素同时定义多个,例如 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] ,但是滤镜的先后顺序不同产生的效果也是不一样的; 也就是说,使用 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] 和 [代码]filter: brightness(1.5) contrast(150%) blur(5px)[代码] 处理同一张图片,得到的效果是不一样的,原因在于滤镜的色值处理算法对图片处理的先后顺序。 滤镜动画需要大量的计算,不断的重绘页面,属于非常消耗性能的动画,使用时要注意使用场景。记得开启硬件加速及合理使用分层技术; [代码]blur()[代码] 混合 [代码]contrast()[代码] 滤镜效果,设置不同的颜色会产生不同的效果,这个颜色叠加的具体算法暂时没有找到很具体的规则细则,使用时比较好的方法是多尝试不同颜色,观察取最好的效果; 细心的读者会发现上述效果都是基于黑色底色进行的,动手尝试将底色改为白色,效果会大打折扣。 最后 本文只是简单的介绍了整个思路过程,许多 CSS 代码细节,调试过程没有展现出来。主要几个 CSS 属性默认大家已经掌握了大概,阅读后可以自行去了解补充更多细节: [代码]filter[代码] [代码]mix-blend-mode[代码] 更多精彩 CSS 技术文章汇总在我的 Github – iCSS ,持续更新,欢迎点个 star 订阅收藏。 好了,本文到此结束,希望对你有帮助 😃 如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。 最后,新开通的公众号求关注,形式希望是更短的篇幅,质量更高一些的技巧类文章,包括但不局限于 CSS: [图片]
2019-04-26 - 小程序怎么打开手电筒
有api可以打开手电筒吗
2019-01-31