微信小游戏开发:前端篇
上QQ阅读APP看书,第一时间看更新

第2课 微信小游戏是如何运行的

上节课我们安装了微信开发者工具,创建了第一个小游戏项目,并且可以在本地和手机上预览了示例项目。那么这个小游戏示例项目是怎么运行的呢?这节课我们看一下小游戏的内部运行机制。

JS是官方指定的小游戏开发语言,微信内置了一个JS VM,这是一个与浏览器、Node.js类似的宿主环境。不同于浏览器,JS VM没有BOM API和DOM API,只有微信提供的wx API。这些wx API是运行小游戏的关键,本课将在项目中介绍如何用wx API来完成创建画布、绘制图形、显示图片,以及如何响应用户交互等基础功能。通过对这些基础功能的学习,我们可以了解微信小游戏是如何运行的。

创建画布

在小游戏中,画布是使用wx.createCanvas接口创建的。第一个被创建的是上屏画布,尺寸默认与设备当前的屏幕尺寸相同。我们先看看如何创建并使用上屏画布。

将第1课创建的源码复制到disc/第1章/1.2/1.2.1目录下,接下来关于Canvas API的实验将基于这份源码修改。调用wx.createCanvas接口,创建一个Canvas对象,代码如下:

上面代码已将原来的第5行、第6行代码注释掉,并在第8行创建了画布。

在小游戏运行期间,首次调用wx.createCanvas接口创建的画布就是上屏画布,在这个画布上绘制的内容都将显示在屏幕上。第2次、第3次及后面第N次创建的画布则是离屏画布,离屏画布上绘制的内容默认不会显示在屏幕上。

在这个示例项目中,因为使用了官方提供的Adapter(适配器类库),即js/libs/weapp-adapter.js(第2行),并且在weapp-adapter.js内部已经调用了一次wx.createCanvas,把创建的画布作为全局变量暴露了出来,此时我们在第7行再次创建的画布将会是一个离屏画布,又因为它与适配器中的全局变量canvas重名,所以将上屏画布覆盖了。

离屏画布不能显示,但属性仍然是可以获取的。下面第10行的代码尝试在调试窗口打印新画布的宽和高。

这里打印出来的画布尺寸是414×736,其大小与模拟器选择的机型有关。笔者选择的机型是iPhone 6 Plus,屏幕大小也是414×736,如图1-18所示。

从测试结果看,模拟器的机型尺寸与打印出来的画布尺寸一样,无论离屏画布,还是上屏画布,创建后的尺寸均与屏幕尺寸相同。

图1-18 模拟器的机型尺寸

思考与练习1-1:console.assert是浏览器环境下的断言API,在小游戏开发中仍然可以使用。请尝试使用console.assert断言屏幕尺寸,如果与预期不符,则抛出“屏幕尺寸断言有误”的异常。

拓展:如何给变量命名

在下面这行实战代码中:

canvas是一个自定义的变量,let是声明变量的关键字。除了let,另一个声明变量的关键字是var。JS的变量名称在声明时必须满足以下规则。

□名称第一个字符只能使用字母或者下画线。

□名称只能由英文字母、数字、下画线组成。

□不能使用JS关键词、保留字。

□不能使用与宿主环境重名的名称。

像1canvas、$canvas、123、function、class这些变量名都是非法的,不能使用,而Canvas、GameGlobal、wx这些名字又与小游戏平台使用的全局名称重名,也不能使用。

在使用JS变量时,需要注意以下几点。

□变量名区分大小写,例如变量mychar与myChar,它们是两个不同的变量。这一点与HTML不同,HTML是不区分大小写的。

□JS具有动态性,变量可以“不问而取”,一个变量从未声明过,也可以直接使用,但一般在开发中要避免这种行为。

JS变量在命名时除了要遵守语法约束外,建议也遵循以下命名规范。

□使用小驼峰命名法,首字母小写,后面每个单词的第一个字母大写,例如myFirst-Canvas。

□尽量使用有具体含义的英语单词,只使用常见的英文单词缩写,一般不使用单字母作为变量名称。

注意:关于JS的编码规范,“番外篇”中有更多讲解。

现在,我们已经在小游戏中创建了画布,那么如何在画布上进行内容绘制,并显示在屏幕上呢?

如何绘制矩形

接下来我们尝试在画布上绘制一个简单的矩形。

对于画布,我们可以使用canvas.getContext("2d")获取2D渲染上下文对象Rendering-Context,继而用RenderingContext对象的fillRect方法绘制几何矩形。我们使用2D渲染上下文对象绘制矩形,矩形尺寸为100×100,颜色为红色,完成后的代码如下所示。

上述的代码是什么意思呢?

□第4行,通过canvas.getContext方法,以2d为参数得到一个2D上下文绘制对象(RenderingContext)。另一个可以选择的参数是webgl,可返回3D上下文绘制对象。

□第5行,通过上下文绘制对象的fillStyle属性设置了填充颜色。在画布上改变绘制样式及调用绘制方法,这些都是在绘制上下文对象上进行的。

□第6行,调用上下文绘制对象上的绘制方法fillRect,在画布上绘制一个矩形。

小游戏支持2D和WebGL 1.0中几乎所有的属性和方法。绘制几何图形、图像等二维对象时使用2d参数创建上下文绘制对象即可。如果要绘制3D模型,需要使用webgl。

在微信开发者工具的工具栏上单击“编译”按钮,即可进行测试。但操作后并没有测试结果,模拟器上是一片黑色,并没有呈现出一个红色矩形,这是为什么呢?

原因是当前的文件变量canvas是一个离屏画布,这一点在本课前面提到过。对此,有以下两个解决方法。

解决方法一,将引用适配器的代码注释掉(第1行),代码如下:

解决方法二,保留适配器引入,将新创建的canvas更名为otherCanvas(见以下代码中的第5行),并使用2D绘制上下文对象的drawImage API将离屏画布翻绘到主屏上(见以下代码中的第14行):

绘制效果如图1-19所示。

无论离屏画布还是上屏画布,其尺寸默认均与设备屏幕相同,无须设置画布的宽和高。如果设置了,相当于清空画布的内容并重置渲染上下文对象。

图1-19 红色矩形绘制效果图

思考与练习1-2:下面这行代码用于在屏幕左上角的(0,0)点处绘制一个100×100的矩形:

如何将上述代码改为在屏幕里居中绘制呢?

拓展:如何理解小游戏的全局变量及作用域

对于下面这两行实战代码:

otherCanvas与context是两个变量,这两个变量在game.js文件中的任何位置均可访问,它们是当前文件作用域下的文件变量。

在小游戏中,包括文件作用域在内,共有6种作用域,具体如下。

□区块作用域:在花括号{}内定义的作用域,相关变量称为区块变量。

□函数/方法作用域:在一个函数之内皆可访问的作用域,相关变量称为函数变量。

□类作用域:在一个类中使用,只要使用this修饰符皆可访问的作用域,相关变量称为类变量。

□文件作用域:当前文件之内皆可访问的作用域,相关变量称为文件变量,例如上面的otherCanvas与context。

□全局作用域:在当前项目内的任何文件内皆可访问的作用域,相关变量称为全局变量。

□开放数据域:只能在指定目录下的JS文件中才能访问的作用域。开放数据域是微信小游戏/小程序独有的,是一个封闭、独立的JS作用域。

其中,前5个作用域的可访问范围是按从小到大依次排列的,这是HTML5与小游戏都具有的作用域。最后一个开放数据域是小游戏/小程序专有的,微信为避免微信好友关系链数据被不法商家滥用,专门创造了一个开放数据域的概念,微信好友关系链数据只能在这个开放数据域下拉取和展示,无法传出及向第三方数据库转存。

在浏览器的宿主环境中,如果想声明一个全局变量,可以在全局对象window上定义,例如:

在上述代码中,canvas与context皆是全局变量,即只要是在同一个HTML5页面中,即使是不同的JS文件,也都可以访问。

小游戏没有全局对象window,微信创造了一个名为GameGlobal的全局对象。若要将前面定义的变量变为全局变量,可以这样做:

在上述代码中,canvas与context皆为全局变量,在小游戏项目中的任何JS文件里均可访问这些变量。

如何清空画布

在绘制完矩形以后,接下来了解一下如何清空画布。

前面我们提到过,重新设置画布的宽和高可以清空画布,此外调用RenderingContext.clearRect方法也可以达到同样目的。

先看一下第一个方法。通过设置width和height属性可以改变Canvas对象的宽和高,同时这会导致Canvas内容清空和渲染上下文对象重置,代码如下:

运行项目,红色矩形不再呈现。即使重设的画布尺寸与原来相同,也会发生重置。

接下来看一下第二种方法,即正规的画布清空方法clearRect,它是绘制上下文对象的方法,示例如下:

上述代码运行效果同方法一。

如何绘制网络图片

现在我们了解了如何绘制几何图形,但是游戏中需要绘制几何图形的情况很少,很多时候需要绘制网络图片。接下来我们看一下如何将一张网络图片绘制到画布上。

我们可以使用接口wx.createImage创建图像对象,并用这个图像对象加载网络图片,然后使用RenderingContext的drawImage方法将图像转绘到画布上,示例如下:

在该示例中:

□第4行通过wx.createImage接口创建了一个image对象,第5行通过设置image的src属性指定了一个网络图片地址;

□第6行调用mainContext.drawImage方法转绘图片,其中mainContext是上屏画布的上下文绘制对象。

但是,当我们单击微信开发者工具的“编译”按钮进行预览时,屏幕上什么也没显示。这是为什么呢?

注意:在正常情况下,上述示例代码是什么也不会显示的,但如果读者先运行过后文的示例,再回头来查看这个示例,图片是有可能绘制出来的。这是因为网络图片已经被缓存至本地了。此时只需要依次单击工具栏区的“清缓存”→“全部清空”选项,缓存即会失效。

原因是图片加载需要时间,image对象并不能直接绘制到画布上。在设置src属性后,需要等待图像异步加载完成,然后才可以绘制到画布上。我们可以通过给image对象设置一个onload回调监听,捕捉图像完成加载的时机,然后在回调函数中完成绘制,代码如下:

图1-20 网络图片绘制效果

这次图像就可以显示了,运行效果如图1-20所示。

注意:一般在小程序/小游戏中,对普通HTTPS请求(wx.request)、上传文件(wx.uploadFile)、下载文件(wx.downloadFile)和WebSocket通信(wx.connectSocket)都有域名检验,对于未在后台配置的域名则不允许访问,但使用图像组件(Image)加载网络图片不受域名校验限制。

通过Image对象的src属性可以加载网络图片,也可以加载本地图片,例如将src属性设置为images/hero.png也是可以的。虽然目前我们并未在本地项目详情面板中开启“不校验合法域名”,但小游戏中的Image对象加载网络素材不受域名校验影响,所以图片仍然可以正常显示。使用网络图片在一定程度上可以减轻代码包大小受限的压力。

思考与练习1-3:如何不使用onload回调,实现网络图片的绘制呢?

Image对象在完成加载之前,其width、height属性一直为0。假如我们设置了一个定时器,每200ms执行一次,让它不断检查Image对象的width属性,width属性值大于0代表图像加载已经完成了,然后立即进行绘制,这样的方案可行吗?示例代码如下:

在实际测试中,这种方式可以实现想要的效果。如果网速没问题,平均不到1s就能看到图像。

但如果不使用定时器,而使用for循环呢?让程序原地循环100000次,等待图片加载完成,这样是不是也能实现相同的效果呢?示例代码如下:

想一想,上述代码的输出是什么?

图片可以绘制了,接下来考虑如何实现动画。所谓动画,就是不停地擦除与重绘。使用全局函数requestAnimationFrame可以执行帧重绘代码,接下来就来看看如何通过帧重绘实现动画。

如何在小游戏中实现动画

我们知道,所谓的动画就是静态图片的快速叠加和切换。那么如何在绘制图片的基础上实现动画呢?

在HTML5开发中,一般通过定时器和requestAnimationFrame方法实现动画效果。小游戏提供了与HTML5同名的相关函数,具体如下。

□setInterval:设置间隔定时器。

□setTimeout:设置延时定时器。

□requestAnimationFrame:开启帧重绘函数。

□clearInterval:清除间隔定时器。

□clearTimeout:清除延时定时器。

□cancelAnimationFrame:取消帧重绘。

其中,前3个函数是创建定时器和动画的,后3个是清除定时器和停止动画的,它们是一一对应的关系。使用requestAnimationFrame创建动画,在效率上优于setInterval和setTimeout,此外,在小游戏中使用wx.setPreferredFramesPerSecond接口可以修改帧频。

既然小游戏中的动画就是重复的擦除与绘制,那么接下来的实验将使用requestAnimationFrame实现动画,示例如下:

在上述的代码中:

□第10行,变量imagePositionY记录了图片的y坐标,它在第15行每执行一次递增一个数字,并传递给drawImage方法,动画便是这样实现的。

□第17行中的requestAnimationFrame使moveDownImage在每帧重复执行,在回调函数moveDownImage中,每次执行都是先清屏(第13行),擦去已经绘制的旧图片,再将图片绘制在画布上(第15行)。

□第10行的变量imagePositionY代表图片的y坐标,第15行的imagePositionY++使图像的y坐标在每1帧中加1,运行时看起来图片就像在向下移动了。

运行效果如图1-21所示。

在小游戏中,屏幕左上角的(0,0)是原点坐标,X轴的正方向向右,Y轴的正方向向下,所以当imagePositionY变量增加时,图片是向下移动的。图1-22是手机屏幕的坐标示意图。

图1-21 帧重绘动画效果

图1-22 手机屏幕坐标系示意图

思考与练习1-4:在本课前面实现动画的代码中,在图像未加载完成的情况下就开始执行moveDownImage命令了,这是不合理的。如何优化它?

如何实现人机交互

继完成网络图片的绘制之后,现在动画也实现了,接下来要考虑实现人机交互了。

一个游戏是不可能没有人机互动的,下面看一下如何在小游戏中接收并响应用户输入。

小游戏使用wx.onTouchMove API监听触摸移动事件,并通过Touch对象的screenX、screenY属性获知触摸点坐标的信息。那么我们通过监听触摸事件,并在回调事件中擦拭与重绘画布,是不是就可以实现界面交互呢?

下面尝试实现不让图片自动向下移动,而是当手指触摸屏幕时随着手指移动。

我们先看一下触摸事件,小游戏参照HTML5 DOM中TouchEvent的生命周期,提供了以下监听触摸事件的API。

□wx.onTouchStart:监听触摸开始。

□wx.onTouchMove:监听触摸移动。

□wx.onTouchEnd:监听触摸结束。

□wx.onTouchCancel:监听触摸取消。

在下面的示例中,我们通过监听屏幕上的手指触摸事件,改变图片位置并进行重绘:

上面的代码发生了什么呢?

□小游戏的触摸事件是多指触控,e.touches返回的是一个Touch对象数组,第11行我们只取一个Touch就够用了;

□每个Touch对象都有clientX、clientY属性,代表触摸点的本地坐标,使用该坐标重绘图片,便实现了图片跟随手指移动的效果。

运行效果如图1-1所示,在模拟器中使用鼠标即可控制图片的移动。

注意:在2018年12月及以前查看微信官方文档关于Touch对象的说明,会发现只有screenX、screenY属性,并无clientX、clientY属性。不过在笔者完稿时,clientX、clientY属性已经存在了。小游戏的触摸对象是依照HTML5设计的,基本上HTML5有的,小游戏也有,有些属性虽然文档中没有,但并不代表一定不能使用。

思考与练习1-5:上面实现的图片拖动效果稍显生硬,松开手指,第二次再拖动时图片还会发生跳跃。有一种拖动效果是延迟跟随,可通过不断设置新目标位置并减速移向目标来实现,读者可尝试实践。

拓展:如何理解局部变量

在函数内声明的变量就是局部变量,局部变量的作用域局限于函数之内,一般在函数退出后,函数的作用域也就销毁了,函数内的局部变量自然也就不能访问了。下面来看一个示例:

在上述代码中:

□第4行中的touch是一个局部变量,第3行中的e也是一个局部变量。

□第6行中的image是在函数外定义的,它是文件变量,此处访问image变量时会先查找最近的函数作用域。如果没找到,则会上升到上一层的文件作用域中查找。如果还没有找到,则会继续到全局作用域中查找。要是全局作用域中也找不到,则程序将会报出一个空对象运行时异常。

拓展:了解微信小游戏的API风格

wx.createImage是小游戏创建Image对象的接口,几乎所有小游戏/小程序接口都是以wx开头的。小游戏/小程序的API设计是有规律可循的,它主要有以下5个方面的特点。

1.同步接口以Sync结尾

一般wx API的同步方法与异步方法是成对出现的,两者的差别在于前者多一个Sync后缀,例如本地存储接口getStorageSync是同步接口,getStorage则是异步接口。

注意:虽然获取系统信息的接口wx.getSystemInfoSync和wx.getSystemInfo在名称上有差异,但由于历史原因二者都是同步接口,这种情况并不多见。大部分小游戏/小程序接口都是以wx开头的,自从有了云开发,开始有了以cloud开头的平台接口。

2.异步调用都有3个相同的回调参数

3个回调参数分别是success、fail和complete。

对API的异步调用,除了参数之外,都有success、fail、complete这3个回调参数。

□success:设置接口调用成功的回调函数。

□fail:设置接口调用失败的回调函数。

□complete:设置接口调用结束的回调函数,无论调用成功与否都会执行。

以下是示例代码:

wx.authorize是小游戏/小程序主动授权的接口,接口参数是object类型。

3.使用onX的形式添加事件监听

小游戏中的监听事件普遍使用wx.onX(callback)这种形式添加,X代表首字母大写的具体事件英文单词,示例如下:

个别事件监听则是使用onX=function(){...}这种形式添加的,示例如下:

4.兼容HTML5开发习惯

小游戏的API有全局API,例如requestAnimationFrame,也有wx.login这样的wx API。前者一般是为了与旧的HTML5编写习惯契合,后者则是微信新定义的接口,它们以wx或以cloud、openapi.*等前缀开头。

5.接口成对出现

许多API都是成对出现的,例如requestAnimationFrame与cancelAnimationFrame对应,它们都是全局API;Canvas对象的onTouchStart与offTouchStart对应;wx.onX与wx.offX对应,这些都是最为常见的格式。

本课小结

本课源码参见:disc/第1章/1.2。

这节课主要讲解了在小游戏项目中图片是如何绘制出来的,动画是如何产生的,人机交互是如何完成的。第1课创建的小飞机示例项目中有一个爆炸效果,现在读者能猜想出那个爆炸效果是怎么实现的吗?

本章内容已结束。通过对本章的学习,相信读者已经对如何创建和调试小游戏项目以及小游戏项目是如何运行的有了初步了解。这一章只是让读者对相关概念有一个整体上的理解。

从下一章开始,我们将进入实战环节,从最简单的3行代码编写入手,一步步开发一个近2万行代码的小游戏项目。