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

第3课 绘制游戏标题

所谓“万丈高楼平地起”,本节课从绘制一个文本开始,最终实现将游戏标题“挡板小游戏”展示在屏幕的正中。

在Canvas API中,fillText方法可以直接绘制文本,并且支持设置字体(font-family)、字号、颜色、文本样式(font-style)等,可想而知,实现标题绘制应该不难。

安装与配置Visual Studio Code

这一章及第3章都在编写JS代码,暂时不需要使用微信开发者工具。Visual Studio Code(以下简称VSCode)是编写JS最适合的工具,该开发工具可以从微软官网下载、安装:https://code.visualstudio.com/。

为了方便在浏览器中预览HTML5页面效果,可以在VSCode中安装一个Live Server插件。安装的方法是,在VSCode左侧的Tab导航栏中选择“扩展”页面,在搜索框中输入Live Server并检索,找到对应的插件并按提示安装,如图2-2所示。

图2-2 搜索与安装VSCode插件

安装以后在Live Server的任何页面单击右键,都可以从弹出的菜单中看到Open with Live Server项,如图2-3所示。

在HTML5页面上选择这个菜单,就可以快速在浏览器中查看该页面的效果。Live Server插件可以自动刷新,在修改了页面源码后,回到浏览器,HTML页面会自动更新。

编程初学者极易犯的一类错误是中英文标点不分。代码中只能使用英文标点,如果中文单引号、双引号混进代码中都将引发错误。为了方便区别中英文标点,需要对VSCode的编辑器字体进行额外设置,在菜单中依次选择“首选项”→“设置”命令,并在弹出的界面中搜索Font Family,然后输入以下内容:

完成后保存,即完成了VSCode编辑区文本字体的设置。宋体可以清晰地显示中英文标点,前面3款英文字体可以清晰、美观地展示“0O和1lI”这些不易区别的字符,它们组合在一起是VSCode中文编程首选组合。

此外,为避免每次修改文件后都需要手动保存文件,可以在设置页搜索Auto Save,并将选项设置为afterDelay。注意,Tab Size是Ta b键缩进量,一般设置为2。

学习使用HTML标记,开始绘制游戏标题

工具准备好了,接下来在屏幕正中绘制文字。

图2-3 查看Live Server右键菜单

在HTML页面中显示一个文本很容易,很多标签诸如div、p、span等都可以做到。但在游戏中绘制就必须用到画布,在画布的绘制上下文对象(RenderingContext)中有一个fillText方法,它可以用于绘制文本,语法如下:

fillText方法最多有4个参数,具体如下:

□text规定在画布上输出的文本;

□x为开始绘制文本的x坐标位置;

□y为开始绘制文本的y坐标位置;

□maxWidth可选,指允许的最大文本宽度,以像素计。

注意:在语法描述中,方括号代表参数可选,fillText方法的第4个参数maxWidth可以传递,也可以不传递。可选参数一般都有默认行为,当fillText方法传递的是maxWidth参数时,代表文本绘制的宽度受到限制,反之不受限制。在描述时,一般使用中括号括住可选参数。

接下来尝试使用fillText实现绘制需求。

首先,需要一个画布。在项目的目录下创建一个simple.html文件,然后在该文件内输入如下代码:

该文件只有4行代码。第1行是HTML注释,第2行至第4行是一个canvas元素,用于在页面上显示一张画布。

simple.html页面结构并不完整,一般HTML页面都具备<html>、<body>等结构标签,如代码清单2-1所示,index.html页面是一个页面结构较为完整的示例:

代码清单2-1 默认的HTML5页面模板

上面代码做了什么呢?我们一起来看一下。

□第17行至第19行,用<canvas>标签在页面中创建了一个Canvas对象,它的id属性为canvas。HTML中的每个标签组件都可以有一个id,id相当于组件的身份,JS代码依赖id找到并操作对应的组件。

□第8行至第14行代码,是通过<style>标签嵌入的自定义样式,主要是为了让画布有一个黑色边框,便于在页面中查看效果。

□HTML是富文本标记语言,这门语言的主要作用是标记和构建页面结构。每个HTML标记都是成对出现的,它们都具有相同的格式:<tag>...</tag>,其中tag可以是任何已定义的标签名称,例如html、body、head、canvas等。每对标签又可分为前后两部分:开始标签和结束标签,像<html>、<body>、<canvas>这些都是开始标签,而</html>、</body>、</canvas>则是结束标签。HTML标记允许嵌套,开始标签与结束标签之间可以嵌入其他HTML内容,例如第17行至第19行的<canvas>标签内嵌于<body>标签中;而第16行至第20行的<body>标签则内嵌于<html>标签中。

□在HTML标记代码中,一个很重要的概念是属性,例如第17行的id是<canvas>标签的属性,第3行lang是<html>标签的属性。如果将HTML组件看作对象,那么属性就是描述对象的特征,不同属性代表不同含义。例如id代表身份标识,在同一个HTML页面中,id一般是不重复的,而lang代表页面的语言。相同名称的属性在不同HTML标记中的含义是相同的。

□第9行至第13行是HTML中内嵌的CSS代码。CSS是一种样式描述语言,作用就是“装饰”HTML组件。CSS语法分为两部分:花括号外面是选择器,代表对谁应用样式描述,例如第9行中canvas是一个元素选择器,代表对所有canvas元素应用样式;花括号里面是样式描述代码,每组样式都是成对出现的,冒号(:)前面是样式名,后面是样式值,例如margin代表外边距,30px表示30个像素,是margin的样式值。

画布有了,我们开始绘制。在Canvas对象上使用不了<div>、<p>等这些HTML文本标签,也没有办法将这些标签包含在<canvas>标签内部。在HTML5中操作画布,和第1章在小游戏中操作画布一样,首先要取得画布的渲染上下文对象(RenderingContext),然后在这个渲染上下文对象上调用相应的方法进行操作。以下是我们获取2D渲染上下文对象,并在该对象上调用fillText方法的示例代码:

上面代码做了什么呢?我们一起看一下。

□在<body>标签下创建了一个<script>标签,这个标签允许我们在页面中嵌入JS代码。在HTML中,所有UI组件都必须放在<body>标签内,<script>标签可以放在<head>标签内,也可以放在<body>标签内,有的开发者还将其放在了<html>标签外(不提倡)。从第7行开始,我们将<script>标签放在<body>和<canvas>标签后面,方便操作。

□第9行、第10行创建了两个页面常量canvas与context。canvas是画布对象,context是画布的2D渲染上下文对象,后者的类型是CanvasRenderingContext2D。

□第11行调用2D渲染上下文对象的fillText方法,在屏幕坐标(10,30)处绘制文本。这个坐标位置是相对于画布的位置,并不是相对于HTML页面的位置,但因为画布是放在(0,0)处的,所以效果是等同的。

□第8行至第11行都是内嵌的JS代码。JS代码与HTML标签代码不同,不需要尖括号作为关键字开始与结束的标志。在HTML代码中,如果有嵌套,并且有忽略空白字符的要求,必须用特殊字符将结构标签与内容显式隔开;在JS代码中,空格与换行符是天然的分隔标记,这基本也是所有类C语言的分隔风格。

□第9行,当我们调用document对象的getElementById方法时,只需要在对象后面加一个点(.),然后加上方法就可以了。在JS中,点(.)是访问对象成员(包括属性和方法)的符号,被称为点访问符。

□第9行、第10行使用了等号(=),在JS代码中,等号代表赋值。等号既可以作为赋值符号,也可以充当分隔符号,例如第10行代码可以写成:const context=canvas.getContext("2d"),我们将等号两边的空格去掉也是没问题的,但const后面的空格不能去掉,因为这个空格是分隔字符,没有字符替代它的职责。

使用Live Server插件的Open with Live Server菜单即可查看页面的运行效果,如图2-4所示。

图2-4 标题默认绘制效果

方便编写与查看效果,且实时预览,这是我们采用VSCode+Live Server编写HTML5页面代码的原因。

思考与练习2-1:fillText方法第4个参数maxWidth用于设置最大文本宽度,尝试添加此参数并设置为20,查看其运行效果。

拓展:如何使用const关键字

下面这行代码使用const关键字声明了一个常量canvas:

const与let是ES6新增的两个重要关键字。let声明的变量只在let所在的代码区域内有效。const声明的常量只有在声明时可以赋值,声明之后值就不能改变了。

在编程中有一个不成文的“吝啬原则”:如果成员可以不被访问,就不要允许它被外部访问;如果只允许“读”就可以,就不要允许它可以被“写”。尽量减少变量或常量的访问权限,可以有效降低软件开发的潜在风险,特别是在多人协作开发的大型复杂项目中,谁都不希望自己的代码因别人的无意修改而引发Bug。

const声明的常量有两种,以下是第一种:

其中,PANEL_HEIGHT是全局常量,命名方式是每个单词的字母全部大写并以下画线间隔。

第二种便是前面提到的canvas:

canvas是作为一种不可更改的“变量”而存在的,命名方式与一般变量的命名方式相同,采用小驼峰命名法。在函数内,所有我们确定在声明后不会修改的临时变量,都可以改用const关键字声明为常量。

注意:有些教程或图书将const声明的常量称为变量,指的是一种不可修改或只能在声明时赋值一次的变量。本书采用MDN(Mozilla Developer Network)的描述,即使用let、var声明的是变量,使用const声明的是常量,它们都可以称为标识符。

拓展:如何给代码添加注释

在下面这段实战代码中,第1行为注释:

这是单行注释,单行注释以两个斜杠(//)开头,放在代码行上方。如果注释内容不多,也可以把注释放在行尾,如下所示:

还有一种多行注释,以/*开始,以*/结尾,示例如下:

这种注释一般放在文件首部。

删除注释虽然不会影响程序运行,但会影响阅读。

如何改变字体、字号和颜色

现在可以使用fillText绘制文本了,但是在本课前面的运行截图中,文本太小,颜色也很单调,下面尝试改变标题的文本大小和颜色。

在Canvas API中,可以使用fillStyle属性设置填充颜色,使用font属性指定文本的字体和字号。font属性的调用语法如下:

font属性接收一个字符串,字符串的内容从前向后依次包含各文本样式的值,并以空格间隔。常用的文本样式如下。

□font-style:规定字体样式。可能的值有normal、italic、oblique。

□font-weight:规定字体的粗细。可能的值有normal、bold、bolder、lighter,或者100、200、300、400、500、600、700、800、900。

□font-size/line-height:规定字号或行高,以像素计。

□font-family:规定字体系列。

下面用fillStyle、font这两个属性给游戏标题修改颜色及改变大小,具体如代码清单2-2所示。

代码清单2-2 修改标题文本的颜色及字号

在上面的代码中,有以下几处改动。

□省略掉的HTML代码没有变化,与本课上一步练习的内容相同。

□第7行代码注释掉了,注释掉的代码与注释效果是等同的,不会执行。

□第10行至第12行是新增的代码,其中第10行设置了font属性的font-family和font-size样式,第11行通过fillStyle属性设置填充样式为用"#ff0000"(十六进制)表示的红色。

□第5行、第6行通过点访问符调用了对象的方法(分别是getElementById、getContext)。第10行至第12行通过点访问符访问了对象成员的另一种形式:属性。点访问符既可以访问对象的属性,又可以调用对象的方法,区别在于名称后面有没有小括号,有括号是方法,反之是属性。当调用方法时,小括号内还可以填写调用方法所需的参数,参数有时有,有时无,依据实际需要而定。当点访问符访问属性时有两种情况:一种是读,另一种是写。像第10行至第11行,属性访问都位于赋值符号(=)的左边,代表这些属性要被设置为新值,所以这里表示写;如果位于赋值符号右边,及其他没有赋值符号的情况,则表示读。

□第12行通过点访问符调用了fillText方法,并传递了3个参数,实参的次序是不能更改的,要与方法中定义的形参顺序一致。每个参数之间用逗号(,)分隔。

运行效果如图2-5所示。

颜色变了,字体与字号均没有变化,这是为什么呢?

在font属性中设置文本样式有先后次序,顺序不可以颠倒,必须按照font-style、font-weight、font-size、font-family这个顺序设置。如果前面的文本样式不需要,则可以略去不写。在上述代码中:

图2-5 标题颜色、大小的默认修改效果

代码清单2-2是将font-family放在了font-size样式的前面,其实它应该放在font-size样式的后面。我们再次修改代码:

这次可以了,运行效果如图2-6所示。

可以看到,字体、颜色、文本大小均有改变。

图2-6 第二次修改标题字号的效果

拓展:在font-family中要使用中文字体的英文名称

看一下以下这行实战中出现的代码:

其中,STHeiti是黑体,此处为什么不用中文直接表示呢?

渲染上下文对象的font属性中的font-family与CSS中使用的font-family,它们的值都必须是英文,这与我们平常在Word软件里看到的字体名称,例如宋体、黑体是不一样的。直接把中文名称写进font属性里将不会有效果;如果这样做,font-family的值将被替换为宿主环境的默认字体。在谷歌浏览器中,这个默认字体一般为“宋体-简”,英文为Songti SC。

当我们想使用中文字体时,必须知道中文字体的英文名称,下表是常用中文字体名称的英文对照表。

从该表中可以看出,有的英文名是中文名称的拼音,有的不是。张鑫旭在其博客上总结了一份较为详细的中英文对照表:https://www.zhangxinxu.com/study/201703/font-family-chinese-english.html,大家可以参考。

思考与练习2-2:尝试使用font属性,设置绘制文本的字体为微软雅黑,字号为30px。

如何给文本添加文本样式

我们已经成功地改变了文本的大小、颜色和字体,那么常见的字体样式,例如斜体、粗体是不是也能实现?

答案是肯定的,我们可以通过font属性中的font-style、font-weight分别实现斜体、粗体效果。

font-style有3个选项:normal、italic、oblique。italic表示斜体,oblique也表示斜体,两者有什么不同呢?使用oblique,即使字体没有斜体字符库也能实现斜体效果,italic则不然,但如果字体本身有斜体字符库,则优先采用italic效果。

font-weight的选项共有9个级别(即100~900)以及4个预定义名称(即normal、bold、bolder和lighter)。这是两套字重表述体系,一般情况下400与normal相当,700与bold相当,900与bolder相当,300与lighter相当。但是normal、bold这样的文字表述属于相对表述,使用这些值时还要考虑父元素的值,它们的值实际上是不确定的,在网页中有可能一个元素的lighter比另一个元素的bolder还要粗重。所以,一般建议使用100~900这样的数字表述。

现在我们使用italic的font-style和600(加粗)的font-weight,修改绘制文本的代码,具体如下:

运行效果如图2-7所示。

如果感觉字重不够,还可以改为800:

运行效果与图2-7是一样的。

我们分别将font-weight的值修改成600和800,为什么两个值的效果是一样的呢?

这是因为字体对字重的实现并不是完全的。图2-8所示为一个字体对字重的实现效果,它只实现了400、700和900这3个字重。多数字体都仅实现了部分字重,如果代码中指定的字重不存在,比它小的字重将会被选用,这就造成有时候800与600的字重效果是一样的。

图2-7 设置标题的粗体、斜体效果

图2-8 某字体的字重实现情况

思考与练习2-3:修改本节代码,使用oblique尝试另一种斜体效果。

如何在绘制文本中使用渐变色

在设置了文本的字体、大小和颜色后,又设置了字体样式—斜体和粗体,但是颜色只是单色,有些单调。可以使用彩色进行绘制,实现如图2-9所示这样的渐变效果吗?

渲染上下文对象的fillStyle属性,中文意思是绘制样式,并不是绘制颜色,这表明它除了可以为颜色赋值之外,还可以为其他对象赋值。在Canvas API中,可以使用createLinearGradient创建线性渐变填充对象(CanvasGradient),这个对象也可以作为fillStyle的合法值。

渲染上下文对象的fillStyle属性可以接收的值有3类。

□颜色(Color),指示绘图填充色的CSS颜色值,默认值是#000000。

图2-9 标题颜色的渐变效果

□渐变填充对象(CanvasGradient),用于填充绘图的渐变颜色对象,有线性渐变(LinearGradient)或放射性渐变(RadialGradient)。

□填充材质对象(CanvasPattern),用于填充绘图的图像材质对象。fillStyle的调用语法如下:

ctx.fillStyle=color|gradient|pattern

注意:fillStyle是一个实例属性,是在对象实例上调用的成员;它并不是类的静态属性,不能在类上像静态方法一样调用它。在上述语法表述中,ctx是一个实例变量,它是一个RenderingContext对象的实例(下同)。符号竖杠(|)代表这3个值是并列的,可选其一,这是语法描述中常用的一种符号。

得益于JS是弱类型动态语言,所以fillStyle属性既可以设置为字符串,又可以设置为对象。在上述语法表述中:第1个选项是一个字符串,默认值是#000000,所以本课前面默认看到的文本是黑色的;第2个选项是线性填充对象CanvasGradient,可以实现从一个方向到另一个方向的颜色渐变,该对象使用RenderingContext#createLinearGradient方法创建;第3个选项是材质填充对象Pattern,使用图片作为填充介质,使用ctx.createPattern方法创建。

接下来在示例中看一看,如何使用CanvasGradient对象实现文本的彩色绘制:

上面代码做了什么呢?

□第8行通过createLinearGradient方法创建了一个颜色渐变对象,常量名称为grd。因为接下来我们不需要改变这个常量,所以使用const关键字声明它。第9行、第10行是调用它的方法,并不改变它本身。

□createLinearGradient方法的调用语法为ctx.createLinearGradient(x0,y0,x1,y1)。其中,(x0,y0)和(x1,y1)分别是渐变起始点、结束点的坐标。如果想横向渐变,那就保持y坐标不变;如果想纵向渐变,那就保持x坐标不变。也可以是任意角度的渐变,这取决于两点坐标的斜度。我们在代码中编写的是从左向右的横向渐变。

□addColorStop方法的调用语法为:ctx.addColorStop(stop,color),其中stop是介于0.0与1.0之间的值,表示渐变开始与结束之间的位置,color是任意的CSS颜色值。第9行、第10行调用两次addColorStop方法,添加了两个渐变颜色点:第一个是红色,第二个是白色。渐变方向是自左向右,这个方向并不是一定的,是由第8行在调用createLinearGradient时传入的坐标点决定的。

□第8行使用渲染上下文对象(RenderingContext)的measureText方法,这个方法可以返回一个文本在当前绘制环境中的理论尺寸对象,其中包括宽度。为什么要测量呢?因为即使是同一段文本,在不同的绘制样式设置下,所占的宽度等信息也是不同的。

运行效果如图2-10所示。

图2-10 文本的渐变填充效果

颜色渐变点是可以根据需要任意设置的,如果我们想实现一个由红到白、再到黄的渐变效果,可以这样修改代码:

上面代码做了什么呢?

□第6行代码被注释掉,在第7行将颜色渐变点的stop属性修改为.5,.5与0.5是等同的。

□第8行代码添加了一个颜色渐变点,颜色渐变点的位置在最右边。

上述代码的运行效果如图2-11所示。

图2-11 文本的双色渐变填充效果

思考与练习2-4:仔细观察本节的颜色渐变效果,感觉右边黄色占据了一多半,正常情况下应该是黄色、红色区域一样多,这是为什么呢?

如何让文本居中绘制

现在我们已经实现了文本的彩色渐变绘制,回到我们最初的需求:将文本绘制在画布的正中位置,这个需求怎么实现呢?

CSS有一个text-align样式,一般设置其值为center,可以将文本居中。但前面我们已经了解过了,Canvas作为画布,是不能在其内使用HTML标记的。如果想让文本左右居中,则需要知道画布宽度和文本宽度,然后通过计算确定文本的绘制位置来实现。

画布宽度通过canvas.width获取,文本宽度的测量方法在本课前面使用过。以下是修改后的示例代码:

上面代码做了什么呢?

□第7行通过measureText计算出了文本的宽度,因为这个数值接下来至少要在两个地方(第9行和第11行)用到,所以将它声明为一个常量,便于复用。这行代码,包括下面第8行代码必须放在第6行代码下面。

□第8行通过measureText计算了一个大写字母M的宽度,那么算它干什么呢?因为M字符的宽度近似等于它的高度,这里是将这个宽度值作为游戏标题“挡板小游戏”的高度值使用的。measureText方法返回的尺寸信息中并不包含高度信息,文本的高度信息的测量是一个较为复杂的问题,涉及许多内容,但大多数字体的字符M的宽度值近似等于其高度值,所以在这里我们可以这样使用。虽然常量txtHeight只在第17行用到一次,但为了代码整齐,我们将它与第7行的txtWidth放在了一起。

□第9行通过txtWidth和canvas.width计算出了文本在x轴方向上的起始位置xpos。

□第11行,渐变填充对象的起始坐标x也设置为xpos,结束坐标x是xpos加上文本宽度txtWidth。

□第16行使用fillText绘制文本时,起始坐标x值是xpos,起始坐标y值是画布高度减去文本高度的一半,计算方法与xpos是相同的。

□第9行、第16行中的斜杠(/)代表四则运算中的除;第11行的加号(+)代表四则运算中的加;第9行中的减号(-)代表四则运算中的减;四则运算中的乘在JS中是用星号(*)表示的,在这里没有用到。

□在JS中,运算符是有优先级之分的,除法运算符的优先级高于减法运算符,所以第9行必须将canvas.width-txtWidth用小括号括住,否则txtWidth会先与后面的2进行除法运算。

图2-12 左右居中绘制的游戏标题

运行效果如图2-12所示。

由图2-12可知,文本左右居中了,但上下并未居中,这是为什么呢?是我们的代码有问题吗?

原因与文本绘制基线有关系,渲染上下文对象有一个属性叫作textBaseLine,它有6个有效值:top、hanging、middle、alphabetic、ideographic、bottom。

这6个值分别对应字符在上下垂直方向上的6个位置,如图2-13所示。

图2-13 文本的基线位置

如果将textBaseLine设置为top,那么fillText方法设置的起始坐标y值就在上方的top线上,文本是偏下绘制的;如果设置为middle,y值就在中间的middle线上,文本是居中绘制的;如果设置为bottom,y值就在底部的bottom上,文本是偏上绘制的。

现在,我们计算的起始绘制坐标y值(在fillText参数中使用),是画布高度减去文本高度的一半,只需要设置文本绘制基线为top,便可以实现上下居中绘制的效果,修改后的代码如下:

运行效果如图2-14所示。

从文本呈现效果来看,不仅左右居中,上下也是居中的。

如果使用middle作为textBaseline属性的值,起始绘制坐标y值只需填写canvas.height/2即可。textBaseline的默认值是alphabetic,因此在修改代码前,文本位置是偏上的。

图2-14 上下左右居中的绘制效果

思考与练习2-5:目前颜色渐变的方向是从左至右,依次是红、白、黄,尝试实现颜色从上向下平均渐变,次序仍然是红、白、黄。

拓展:为什么要在代码中使用常量

以下是在本课上一节的示例中出现的代码:

其中,textWidth、txtHeight与xpos均是常量。这里使用常量有两个好处:

□使程序代码解耦,在一个地方修改常量就可以变更多处。例如,xpos代表起始绘制坐标x值,如果我们要改变文本的绘制位置,只需要修改xpos这个常量就可以了。

□常量不可更改,初始化之后,可以放心大胆使用。txtWidth表示文本宽度,在示例中有两处用到,我们不必担心这两个地方取到的值不一样。

变量可以更改,常量不可以更改。在能使用常量的地方就不要使用变量,这样就不用担心常量多了会影响程序性能。一般情况下,引用类型的全局常量多了才会影响性能,局部常量尤其是值类型的常量并不会影响性能。

本课小结

本课源码参见:disc/第2章/2.1。

这节课我们主要学习了如何绘制文本,以及如何控制文本的颜色、大小、斜体、粗细等样式。

下节课我们开始绘制挡板,在这个小游戏中挡板共有两个,它们可以反弹小球,是这个项目中最能体现游戏交互性的功能之一。