3月初开始接触微信小游戏,从注册、集成环境开发到上线流程,它都和微信小程序很相似,只有审核阶段比小程序繁琐。从技术角度讲,小游戏比小程序更纯粹,因为几乎所有和UI渲染相关的接口都是围绕Canvas技术,这也是我欣赏它的地方。知道就要做到,所以我决定把“趣玩周易”这个基于Canvas的网页端作品迁移到(或者说重新开发)微信小游戏中,在学习过程中,我会一边熟悉微信小游戏的API和研发流程,一边寻找“趣玩周易”的游戏模式。
注册与登录
注册微信小游戏账号,和注册微信小程序很相似。一开始我甚至觉得是微信研发团队为了图省事,就连两个开发平台的文档都照搬照抄。当我接触微信小游戏开发一段时间后,发现它们之间还是有细微差别的。首先,微信小游戏的确是小程序的衍生产品,大部分能力也很相似。其次,它们在项目代码组织结构上、API使用上、审核上就有明显的不同,本文下方会涉及到这些不同。
我想会有人过去和我一样,总是把微信公众号、微信公众平台的概念搞混,随着接触公众号、小程序和小游戏的不断深入,我已经明白它们之间的关系。在小程序和小游戏还没有出现之前,微信公众平台里只有微信公众号这一个平台,所以那个时候也无需刻意区分概念。随着小程序和小游戏的诞生,微信公众平台包括了微信公众号、微信小程序和微信小游戏,所以就不能再把微信公众号叫做微信公众平台了,因为它们是从属关系。这就好像URI和URL的关系一样,URI包括了URL和URN两个规范,但在URN还没有普及的情况下,URI在URL也无需刻意区分。现在不管是小游戏、小程序,还是微信公众号,它们的 注册和登录入口 都相同。而且现在的登录体验优化了——微信扫码登录,在手机端选择要登录的账号类型和账号。
在注册账号时,要看清楚账号类型,小程序账号和小游戏账号是不一样的。对于这篇文章来说,你应该注册小游戏账号。
集成开发工具
微信小程序项目、微信小游戏项目和微信公众号网页项目,它们使用同一个 微信开发者工具 开发。这个集成开发工具都能开发什么项目呢?小程序项目和公众号网页项目,小程序项目又包括了小程序、小游戏和代码片段三种,如下图1⃣️所示。以后就按照官方对它们的定义来称呼。下图2⃣️所示,从这里创建新的小程序项目,或者打开已经创建的项目。下图3⃣️用来管理已创建的小程序项目,删除操作只是删掉了记录,并不会把项目的目录扔进垃圾箱或直接移除。
书本里定义的那些概念之所以要被人为的规定好,只是为了让人们达成对某些事物的共识,通过语言的方式方便沟通,而不至于对同一个事物出现理解的争议。但“名可名非常名”,所以那些被人为的用公认的名称所定义的事物,重要的不是它叫什么,重要的是它是什么。这就好像我们所要达到的目标都是同一个,那你又怎么能确定你实现目标的方法就一定比别人的要好呢?每个人的思考方式,他能利用的资源,限制他的条件都是不一样的,所以每个人选择的“道”自然也不尽相同。
上面这番话,意义深远。微信开发者工具就好比是微信公众平台为大部分开发者推荐的正统“概念”,但用不用它以及怎么用它在你,还有很多其他可选项,比如Egret、Cocos这些第三方游戏开发引擎也都有自己的集成开发工具,你也可以用其他代码编辑器。“工欲善其事必先利其器”,合适你的“器”才能称为利器,孙悟空的金箍棒多神力,猪八戒却未必舞得好金箍棒,因为钉耙才是他的利器呀,唐僧的“利器”是他那颗一心向佛的心,既然师徒四人的终极目标是取真经,又何必规定谁用什么“器”,未免形式主义!
创建小游戏项目
从微信开发者工具的启动界面,找到小程序项目下的小游戏,从这里创建第一个小游戏项目。你既可以“新建项目”,也可以从已有项目(其他人的开源小项目,或Egret、Cocos创建的项目)导入。如果选择了创建新项目,就要填写好项目名称、项目存放位置和你的小游戏账号的AppID,开发模式选择是小游戏,不使用云开发服务。如果你想导入已有项目,我已经把 趣玩周易 这个小作品的源代码开源到GitHub仓库上,随着对围绕“趣玩周易”的游戏模式的可行性研究,我会不断更新这个项目,也欢迎对国学或周易感兴趣的志同道合的朋友提供有趣的游戏创意。
导入“趣玩周易”小作品后,你可能会看到类似的界面,之所以说类似,是因为我会随着对这个小作品开发的深入,它的目录结构、代码组织方式、UI都会大不一样,这些变动都会提交到 趣玩周易的GitHub仓库 中,不出意外的话,切换到第一次提交的代码就能看到和下图一摸一样的界面。
在小游戏项目中,只有两个必要的文件,一个是 game.js,它是游戏的全部逻辑所在,另一个文件是 game.json,它是小游戏全局配置,还有一个会自动生成的配置文件 project.config.json,和小程序的代码编译构建相关,里边存放着小程序的AppID。
渲染一太极图
我在 趣玩周易六十四卦 这篇文章中,简要介绍了如何用Photoshop绘制太极图,并简单普及了关于八卦和周易64卦的概念。下面结合微信小游戏特有的渲染API,将太极图渲染在画布上。上面提到了小游戏的逻辑代码都写在 game.js 文件中。首先创建一个图片实例,为它设置图片路径,并监听图片加载成功的事件。
const img = wx.createImage()
img.src = 'img/tj.png'
img.onload = () => {
// 太极图片加载成功
}
不像我们在BOM(浏览器对象模型)中用 new Image()
创建一个图像实例,也不像我们在DOM(文档对象模型)中用 document.createElement('image')
创建一个图像元素实例,我们要想在小游戏中实例化一个图像,就需要用 wx.createImage()
接口创建。不必排斥或诧异为何在微信小游戏里要这样创建图像实例,你只要去接受它,适应它就可以了,这种思维对学习新东西很有帮助。在这个API的背后一定做了必要的优化,只要能帮助我们完成加载图像的目标,至于用什么API都是次要的,不必纠结那些细枝末节和与“虚名”相关的东西。
这不,又出现一个新的 API,这个API用来创建canvas画布实例。在DOM中我们使用 document.createElement('canvas')
创建canvas画布实例,小游戏里怎么创建canvas画布实例呢?
const canvas = wx.createCanvas()
const ctx = canvas.getContext('2d')
const offScreenCanvas = wx.createCanvas()
虽然创建canvas画布实例的API和在DOM中的不同,但和DOM中创建的canvas画布实例基本无差别,该有的属性和方法它都有,比如获取代表“画笔”的绘图上下文依然用 canvas.getContext('2d')
方法,获取和设置画布尺寸依然用 canvas.width
属性。首次调用 wx.createCanvas()
创建的是显示在屏幕上的画布,之后调用创建的都是离屏画布,离屏画布的作用是预先渲染好不常变化且每次重新渲染会影响性能的画面,这也是最值得优化小游戏性能的地方。
太极图加载好了,画布实例和绘图上下文对象也创建完毕了,接下来就把太极图渲染到屏幕上。把上面介绍的新API都用进来:
// 创建画布实例,首次调用这个方法创建的画布会显示在屏幕上,也只有一个画布能显示在画布上
const canvas = wx.createCanvas()
// 获取画布实例的绘图上下文对象,就像拿到了专属于这个画板的画笔
const ctx = canvas.getContext('2d')
// 创建图像实例
const img = wx.createImage()
// 为图像实例设置图像资源相对路径,并自动开始加载图像
img.src = 'img/tj.png'
// 图像加载完毕后会执行下面的回调函数
img.onload = function() {
// 让渲染在屏幕上的太极图的尺寸(宽和高相等)为屏幕宽度的0.3倍
const imgSize = canvas.width * .3
// 适时地保存上下文状态是个好的最佳实践,比如当转换坐标系又不希望影响接下来的渲染时。
ctx.save()
// 转换坐标系,平移原点到屏幕正中间
ctx.translate(canvas.width * .5, canvas.height * .5)
// 转换坐标系,顺时针旋转90度
ctx.rotate(90 * Math.PI / 180)
// 绘制图像
ctx.drawImage(this, -imgSize / 2, -imgSize / 2, imgSize, imgSize);
ctx.restore()
}
动起来的太极图
动画就是由一系列的静态图像按固定速度连续切换所产生的视觉暂留现象,每一张静态图称作一帧。只要能描述物体的各个状态的变化过程,就能模拟出这个物体的动画,所以要想模拟太极图的旋转动画,就要知道在一段时间内太极图在每一个时刻的旋转角度,每一时刻之间的间隙越短,或者说帧速越快,那么动画就越流畅。
假设太极图的初始旋转角度为0度,下一时刻状态变为1度,再下一刻变为2度,3度,4度,5度,6度,7度 … … 这样就把太极图的旋转动画描述出来了,每一刻的旋转角度在上一刻的基础上加1度,用代码表示出来就是这样:
// 总是在上一刻的旋转角度值的基础上加1
angle = angle + 1
// 简写如下
angle += 1
如何让动画连续起来呢,可以用 setTimeout()
方法,它的作用是在指定时间后执行传入的函数:
let angle = 0;
(function change() {
console.log(angle)
angle += 1
setTimeout(change, 16)
})()
每16毫秒,控制台会输出最新的angle值。同样的功能,还可用 setInterval()
方法实现:
let angle = 0
setInterval(()=>{
console.log(angle)
angle += 1
}, 16)
上方的实现,同样每16毫秒,控制台输出新的angle值。还有一个方法 requestAnimationFrame
最常用,它会在系统准备好渲染下一帧画面时调用传入的回调函数,动画更流畅,性能相对更优:
let angle = 0;
(function render(){
requestAnimationFrame(render)
console.log(angle)
angle += 1
})()
用最后一个方法让太极图随着时间的推移动起来。
const canvas = wx.createCanvas()
const ctx = canvas.getContext('2d')
const img = wx.createImage()
img.src = 'img/tj.png'
img.onload = render
const imgSize = canvas.width * .3
let angle = 0
function render() {
requestAnimationFrame(render)
ctx.clearRect(0, 0, canvas.width, canvas.height)
angle += 1
ctx.save()
ctx.translate(canvas.width * .5, canvas.height * .5)
ctx.rotate(angle * Math.PI / 180)
ctx.drawImage(img, -imgSize / 2, -imgSize / 2, imgSize, imgSize);
ctx.restore()
}