第1章 水果配对

这是一款挑战瞬间记忆能力的游戏:先后翻开两张牌,如果图案相同,则牌保持翻开状态;如果图案不同,则两张牌瞬间重新合上。

1.1  游戏描述

游戏的用户界面如图1-1 所示,功能描述如下。

(1) 时间因素:限制游戏时长(如60 秒),如果在规定时间内完成游戏,则剩余时间转化为奖励得分。

(2) 空间因素:用户界面上有16 张卡片,排成4×4 的方阵;卡片的背面图案为安卓机器人,正面图案为8 种水果,可以两两配对。

(3) 游戏操作(翻牌):玩家先翻开一张卡片,再翻开另一张卡片,如果两张卡片的正面图案相同,则两张卡片保持翻开状态;如果两张卡片的正面图案不同,两张卡片将闪现片刻,然后迅速反转回去,显示背面图案。

(4) 计分规则:每翻开一对卡片得10 分;如果在规定时间内翻开所有卡片,满分为80 分;剩余游戏时间(秒数)×10 作为奖励得分,与翻牌得分一同计入 总分;如果在规定时间内没有翻开所有卡片,则不计分。

图像说明文字

(5) 历史记录:首次游戏得分被保存在手机中,在每次游戏完成时,将本次得分与历史记录进行比较,并保存高的得分;玩家可以清除游戏成绩的历史记录。

(6) 退出游戏:玩家在完成一轮游戏后,可以选择退出游戏。

上面一段文字,既是游戏开发任务的起点,也是终点。在软件工程中,类似这样的“游戏描述”被称为“需求文档”,它从用户角度描述了软件的功能。开发人员依据这个文档,将整个开发任务分解为一个个子任务,并逐个加以实现。在开发任务完成之后,客户会依据这个文档对项目进行验收,这就是开发任务的终点。

游戏描述与记叙文的写作有相似之处:记叙文中包含了时间、地点、人物、事件四大关键要素,而游戏描述中通常也会包含时间、空间、角色、事件等基本要素,也要描述角色(组件)在特定的时间、空间内的行为(所发生的事件)。

此外,游戏描述又与说明文相似,要求文字简练准确,内容具有条理性、客观性和完整性,不强调修辞方法的使用,等等。一篇好的游戏描述为我们后续的应用程序开发提供了一份完整的框架及任务清单,我们的每一个开发步骤都会依据这份文档,因此千万不可掉以轻心。

有这样一种说法:需求文档中隐含了程序中的变量和过程,其中的名词有可能成为程序中的全局变量,而动词或动宾词组有可能成为程序中的过程。具体来说,在游戏描述的第一条中,游戏时长、剩余时间及奖励得分都有可能成为程序中的全局变量;在第三条中,翻牌、闪现、反转等操作,有可能成为程序中的过程。如果名词、动词能够与变量、过程一一对应,那么编程的难度会大大降低;但实际上,游戏描述使用的是人类的自然语言,而自然语言存在很大的不确定性,同样的一个游戏,不同的人可能使用不同的方法来描述它。因此,这种说法可以借鉴,但不能作为绝对的依据,将复杂的问题简单化。

1.2  界面设计

打开App Inventor 设计视图,完成用户界面的设计。

1.2.1  界面布局

屏幕被划分为两个部分:屏幕顶部使用了水平布局组件,内部放置了显示分数的标签和显示游戏剩余时间的数字滑动条;屏幕中央使用了4×4 表格布局组件,共16 个单元格,用于放置16 个按钮,如图1-2 所示。组件的命名及属性设置见表1-1。

图像说明文字

1.2.2  组件属性设置

详见表1-1。

图像说明文字

1.2.3  上传资源文件

游戏中用到了10 张图片,其中用于显示卡片正面图案的水果图片8 张、卡片的背面图片1 张, 用于产品发布的图标图案1 张(菠萝的卡通画,ananas.jpg)。上传结果如图1-2 的右下角所示,图片的外观及规格见表1-2。

图像说明文字

1.3  编写程序——屏幕初始化

如果把编写软件比喻为烹制一道菜肴,那么用户界面上的元素就相当于制作这道菜肴的全部食材。当食材备齐之后,就可以考虑进入烹制阶段了。就软件而言,当用户界面设计完成之后,就可以开始编写代码了。

我们很自然地会问,从哪里开始呢?无论是初学者,还是有经验的程序员,都无法回避这个问题。通常的做法是,按照游戏的时间顺序来编写程序。但是对于初学者来说,也可以从最简单的功能做起,例如,先设置按钮的背面图案,然后处理这个按钮,当点击它时,让按钮显示正面图案; 然后考虑第二个按钮,当点击第二个按钮时,可能会有两种情况(两个按钮的正面图案相同或者不同),再分别处理这两种可能的情况。这里我们采用通常的做法,首先来编写屏幕初始化程序,在这段程序中,我们要完成3 项任务:

(1) 设所有按钮的显示文本为空;

(2) 设所有按钮的图片属性为安卓机器人(back.png);

(3) 将8 对(16 张)不同的图案分配给16 个按钮,作为它们的正面图案。

提示 上述功能的实现依赖于两项关键技术——列表及随机数。这里假设读者已经了解App Inventor 中关于列表及随机数的知识。如果读者还没有学习过相关的技术,推荐访问https://book1.17coding. net/,阅读《App Inventor 编程实例及指南》中的“总统测验”及“瓢虫快跑”两章,或访问https:// web.17coding.net/reference,阅读参考手册中的相关条目。

1.3.1  创建按钮列表

首先我们引入一个新的概念——组件对象。我们可以在编程视图中,随意点击一个项目中的组件,打开该组件的代码块抽屉。你会发现,在代码块的最后一行,总有一个与该组件同名的代码块,这个代码块代表了这个组件本身,我们称之为“组件对象”;对于按钮来说,就是按钮对象。如图1-3 所示,椭圆形线条圈出的就是表格布局对象。你可以把组件对象看作一类特殊的数据(比如由键值对组成的列表),里面包含了该组件的所有属性值。

图像说明文字

为了能够在程序运行过程中,读取或改写任意一个按钮的属性,我们需要利用“按钮对象”。将所有按钮对象放置到一个列表变量中,这样就可以依据列表项的索引值,随时找到任何一个按钮,并读取或改写它的属性值。

首先声明一个全局变量“按钮列表”,并编写一个“创建按钮列表”过程,在该过程中完成列表项的设置,然后在屏幕初始化程序中调用该过程,如图1-4 所示。这个列表的神奇之处,稍后你就能有所体会。

图像说明文字

这里要问一个问题:为什么我们要在屏幕初始化程序中来设置按钮列表,而不是在声明按钮列表时,直接利用按钮对象设置变量的初始值呢?这种情况如图1-5 所示。

图像说明文字

原因是这样的:在屏幕初始化时,程序首先要创建项目中的所有组件和全局变量;但是由于组件和变量的生成顺序无法确定,在声明全局变量(按钮列表)时,无法确认组件(按钮)是否已经创建完成,因此App Inventor 不允许使用组件对象对全局变量进行初始化。图1-5 中带叹号的三角形代表“警告”,“警告”意味着程序中存在严重错误。

1.3.2  让按钮显示背面图案

我们可以在设计视图中将每个按钮的图片属性设置为back.png,这样当游戏被打开时,16 个按钮会默认显示背面图案(安卓机器人)。但试想一下,当第一轮游戏结束,准备开始下一轮游戏时, 如何将16 个按钮上的正面图案全部恢复为背面图案呢?也就是说,如何在程序运行过程中设置每个按钮的图片属性呢?当然,你可以逐个设置,不过这需要16 行代码,那么有没有更为简便的方法呢? App Inventor 提供了一组“任意组件”代码,可以用来动态地读取或改写任何一个组件的属性值,如图1-6 所示。

在编程视图的代码块面板中,将内置块和Screen1 折叠起来,就可以看到最后一组“任意组件” 类代码块,项目中添加的所有组件类型都会在这里出现。点击其中的“任意按钮”项,将打开与按钮类组件相关的代码块抽屉,其中有两种颜色的块,浅灰色块用于读取某个按钮组件的某种属性值(如图片属性所对应的文件名),深灰色块用于设置某个按钮组件的某种属性值。

图像说明文字

“组件对象列表+ 循环+ 任意组件”是解决上述问题的钥匙!创建两个过程“清空按钮文字” 及“初始化背面图案”,利用循环语句逐个设置按钮的显示文本及图片属性,并在屏幕初始化程序中调用这些过程,代码如图1-7 所示,测试结果如图1-8 所示。

图像说明文字

这里需要提醒一下,屏幕初始化后,按钮的排列顺序如图1-9 所示。

图像说明文字

1.3.3  创建图片列表

声明一个全局变量“图片列表”,用来保存所有正面图案的图片文件名,如图1-10 所示。

图像说明文字

此处,我们在声明全局变量“图片列表”的同时,创建了该列表,与之前“按钮列表”的创建相比较,我们可以更加深入地理解普通数据与“组件对象”类数据之间的区别。

1.3.4  为按钮指定正面图案

首先需要说明一下,这个步骤并不是游戏开发过程中必需的,这里只是为了让读者了解如何设置按钮的正面图案;因此,这里显示的图片是按照固定顺序排列的。我们设置按钮1 和按钮9 具有相同的正面图案;同样,按钮2 和按钮10 具有相同的正面图案,以此类推。与设置背面图案相同的是,这里也要使用“组件对象列表+ 循环+ 任意组件”这把钥匙;不同的是,图片属性的值来自于另一个列表变量“图片列表”。设置正面图案的代码如图1-11 所示,其测试结果如图1-12 所示。

图像说明文字

为了让屏幕初始化程序看起来简洁,提高代码的可读性,我们创建一个“初始化正面图案”过程,并在屏幕初始化程序中调用该过程,如图1-13 所示。尽管这个过程不是游戏程序中必需的,但我们还是自始至终地保持一种良好的开发习惯——将一段具有特定功能的代码封装为过程,以使程序从整体上变得简洁,且易于阅读。

图像说明文字

1.3.5  随机显示正面图案

在图1-12 中,卡片的图案排列是有规律的,如果卡片一直是这样排列,那么游戏将毫无乐趣可言。游戏的乐趣在于其多变性,就像我们玩扑克牌游戏,每次手中拿到的牌都是不一样的,这种不可预知的变化才使得游戏充满乐趣和挑战。几乎所有的编程语言都有生成随机数的功能,App Inventor 也不例外。我们来看看如何利用App Inventor 的列表及随机数功能来实现类似洗牌的操作。

洗牌原理叙述如下。

(1) 需要两个列表,A 和B;开始时,列表A 按顺序放置了8 对(16 个) 图案,列表B 为空。

(2) 从A 中随机选出一个列表项X,添加到列表B 中,并从A 中删除列表项X。

(3) 从A 中剩余的所有列表项中随机选出一个列表项Y 添加到B 中,再从A 中删除Y。

(4) 重复第三步直到列表A 为空,此时列表B 中随机排列了8 对(16 个) 图案。

(5) 分别将这16 个图案设置为按钮1 ~按钮16 的图片属性。

根据上述原理,我们首先来设计列表A。列表A 所有的列表项最终要被删除掉,成为空列表, 因此不必使用全局变量来保存它。我们创建一个“随机显示图案”过程,在该过程中用局部变量“图案列表”来充当列表A,并用双倍的图片列表来填充图案列表(列表A)。接下来考虑列表B。声明一个全局变量“随机图案列表”来充当列表B,并设置其初始值为空列表,如图1-14 所示,代码的测试结果如图1-15 所示。

图像说明文字

对照上述的洗牌原理,我们可以理解图1-14 中每一行代码的作用。也许你会问,为什么要设置一个随机列表,它似乎与图案的显示无关。如果只是让16 个按钮随机显示16 个图案,那么列表B (随机图案列表)的确是多余的,你可以试试看,即使删除过程中与随机图案列表相关的代码,也不会影响图案的随机显示。但是不要忘记,这个过程只是为了向读者展示如何为按钮随机分配正面图案,真正的游戏中并不会在游戏一开始就向玩家展示所有正面图案。随机图案列表的作用要到后面的程序中才能体现出来。

图像说明文字

好了,到此为止,我们已经实现了用16 个按钮随机显示16 个图案的功能,不过在游戏开始时,我们只需要所有按钮显示背面图案。将图1-14 中的代码稍做修改,得到的新代码如图1-16 所示。注意,在游戏中不需要一次性地随机显示正面图案,因此“随机显示图案”的过程名称显得有些不够贴切,我们将过程名改为“随机分配图案”。

图像说明文字

上述代码有两点需要强调。第一,为了保持屏幕初始化程序的简洁,我们定义了“清空按钮文字”及“初始化背面图案”过程,并在屏幕初始化程序中调用这两个过程。第二,虽然删除了“随机分配图案”过程中设置按钮图片属性的代码,但要记住,“按钮列表”中的列表项与“随机图案列表”中的列表项存着一一对应的关系,在后来翻开卡片显示图案以及判断两个卡片图案是否相同时,这是唯一的线索:根据按钮在按钮列表中的索引值来求得按钮的正面图案。表1-3 描述了图1-15 中按钮列表与随机图案列表之间列表项的对应关系。

图像说明文字

我们现在已经实现了16 个按钮的随机图案设置,并在程序开始运行时,只显示背面图案,下面将针对每个按钮设计它们被点击后的行为。

1.4  编写程序——处理按钮点击事件

为了便于描述卡片被翻开的过程,这里引入了流程图(见图1-17),它可以清晰完整地描述一张卡片被点击时所处的状态,以及针对不同状态所采用的处理方法。

1.4.1  流程图

图像说明文字

图1-17 中的流程有3 种可能的路径:如果点击按钮翻开的的是第一张卡片,则执行路径①,记住第一张卡片;如果点击按钮翻开的是第二张卡片,则记住第二张卡片,并判断两张卡片图案的异同,如果相同,则执行路径②,否则,执行路径③;无论是执行路径②还是路径③,最后都要忘记两张卡片。

注意流程图中的3 个矩形框:记住第一张卡片、记住第二张卡片、忘记两张卡片,这是编写程序的关键。所谓记住或忘记,就是要用全局变量来记录已经翻开的卡片。这里我们声明两个全局变量“翻牌1”及“翻牌2”,来保存正在翻开等待判断的两个按钮对象。在应用初始化时,设置它们的值为0①,当第一张牌被翻开时,设

翻牌1 = 第一个被点击的按钮对象

当第二张牌被翻开时,设

翻牌2 = 第二个被点击的按钮对象

并以这两个变量为依据,判断按钮图案的异同。

1.4.2  判断两个按钮图案的异同

我们先以按钮1 及按钮2 为例来编写代码,如图1-18 所示。

图像说明文字

① 在一般的编程语言中,会保留一个空值(null),用来表示那些已经声明但尚未赋值的变量的状态,但App Inventor 中没有这样的空值,因此这里用0 来代替。

当按钮1 或按钮2 被点击时,事件处理程序的执行过程如下。

(1) 根据按钮对象在按钮列表中的位置(索引值),从随机图案列表中获取按钮的正面图案,并显示该图案。

(2) 设置被点击按钮的启用属性值为假。(考虑一下为什么,如果不这样,当再次点击该按钮时,会发生什么事情?)

(3) 判断它是不是第一张被翻开的卡片:如果是,将翻牌1 设置为该按钮对象;否则,将翻牌2 设置为该按钮对象,并判断已经翻开的两个按钮的正面图案是否相同。这里我们暂时不做进一步的处理,而是利用屏幕的标题属性来显示测试结果:如果按钮1 与按钮2 的图案相同,则屏幕的标题显示“图案相同”,否则显示“图案不同”。

(4) 如果已经翻开两张卡片,无论它们的正面图案是否相同,都必须重新将翻牌1 及翻牌2 的值设置为0。

测试结果如图1-19 所示。

图像说明文字

1.4.3  处理两个按钮图案相同的情况

按照图1-17 的设计,当图案相同时,记住已经翻开的卡片对数。凡是需要记住的内容,都需要一个全局变量来保存它,已翻开卡片的对数一方面用于计算游戏得分,另一方面用于判断是否所有卡片都已经被翻开(即对数等于8 时)。我们将这个变量命名为“翻牌对数”。

当两张卡片的正面图案相同时,有3 件事情需要完成。

(1) 为全局变量“翻牌对数”的值加1。

(2) 计算并显示游戏得分。

(3) 判断“翻牌对数”是否= 8,并依据判断结果选择执行两条路径之中的一条:

a. 当翻牌对数= 8 时,显示“游戏结束”;

b. 当翻牌对数< 8 时,显示“图案相同”。

假设每翻开一对卡片得10 分,因此游戏得分= 翻牌对数×10,我们用标签“得分”来显示游戏得分,具体代码如图1-20 所示。

图像说明文字

1.4.4  处理两个按钮图案不同的情况

当两张被翻开的卡片图案不同时,将它们重新扣上,即显示背面图案。为了让已经翻开的图片能够显示一定的时间,这里需要用到计时器组件,一旦判断出两个卡片图案不同,就启动计时器。经过一个计时间隔的时长后,计时器发生计时事件,在计时事件的处理程序中,将两张卡片同时扣上。我们用闪现计时器来实现这一功能。这里闪现计时器的计时间隔为500 毫秒,如果需要加大游戏的难度,可以将计时间隔设置得更短。

我们在“图案不同”的分支里添加一个语句“启动闪现计时器”,并编写闪现计时器的计时事件处理程序,如图1-21 所示。

图像说明文字

在闪现计时器的计时事件中,我们设置两个按钮的启用属性为真,图片属性为背面图案,并将计时器1 的启用属性设置为假,即让计时器1 停止计时。经过测试,程序运行正常。

1.4.5  代码的复用——改进按钮点击事件处理程序

到目前为止,我们已经能够处理两个按钮的点击事件。我们需要将按钮1 点击事件处理程序中的代码复制到其他14 个按钮的点击事件处理程序中。这听起来很可怕,试想,如果开发过程中需要修改其中的部分代码(这种事情经常会发生),那么我们要完成15 倍的工作量,同时也增加了程序出错的风险。即便我们能够一丝不苟地完成这些代码,但又如何编写闪现计时器的计时事件处理程序呢?因此,需要寻找一个更为简洁的代码编写方法。让我们先来观察一下已有的两个按钮的点击事件处理程序,找出其中不同的部分, 如图1-22 所示。

经过观察,我们发现这两段程序中共有7 处不同,其中4 处与按钮本身有关,另外3 处与按钮在按钮列表中的索引值有关,这个索引值也是按钮正面图案在随机图案列表中的索引值,用来求得按钮的正面图案。能否创建一个通用的过程,来处理不同按钮的点击事件,就取决于能否合理地设置过程的参数,并在调用该过程时为参数指定确切的值。能够想到的参数就是按钮本身,即按钮对象,而索引值可以通过按钮在按钮列表中的位置获得。很不错的分析,让我们来试试看。

图像说明文字

创建一个带参数的过程,过程名为“处理点击事件”,参数名为“按钮”,将按钮1 的代码拖拽到新建的过程中,然后对代码进行改造,如图1-23 所示。

图像说明文字

(1) 添加一个局部变量“索引值”,它的值为被点击的按钮在按钮列表中的位置。前面我们讲过,按钮列表与随机图案列表中的列表项是一一对应的,因此按钮在按钮列表中的位置也是它的正面图案在随机图案列表中的位置,以此来设置被点击按钮的正面图案。

(2) 使用“任意组件”类代码,取代原来的前两行代码——设置按钮的图片属性为正面图案, 设置按钮的启用属性为假。

(3) 如果被点击的按钮是第一张卡片,则设翻牌1 为该按钮。

(4) 如果被点击的按钮是第二张卡片,根据索引值求得第二张卡片的正面图案;此时需要获取第一张卡片的正面图案,为此添加局部变量“翻牌1 索引值”,其值为翻牌1 在按钮列表中的位置,并根据该索引值,求出第一张卡片的正面图案。

(5) 当翻牌1 与翻牌2 的图案相同时,翻牌对数加1,得分更新,并设翻牌1、翻牌2 的值为0, 稍后你会看到为什么要调整这两行代码的位置。

(6) 当翻牌1 与翻牌2 的图案不同时,启用闪现计时器。

(7) 无论图案是否相同,设翻牌1 及翻牌2 为0。

(8) 在按钮1 及按钮2 的点击事件处理程序中调用该过程,并为参数指定具体按钮。

经过测试,程序运行正常。接下来为其余14 个按钮编写点击事件处理程序,很简单——调用“处理点击事件”过程,并将参数设置为触发事件的按钮,结果如图1-24 所示。

图像说明文字

在编写图1-24 的其他14 个事件处理程序时,我们是将按钮1 的程序复制粘贴14 次,并逐一修改事件主体(按钮)及所调用过程的参数(按钮对象)。这样可以免去逐个点击按钮创建程序的重复操作。操作方法如图1-25 所示。

图像说明文字

1.4.6  代码的规整

与一般的编程语言相比,使用App Inventor 开发应用会遇到一种特殊的困难:当程序中的代码过多时,屏幕就显得拥挤和混乱。因此,代码的折叠与摆放也是一件需要考虑的事情。我的习惯是,将代码折叠之后,按类别及顺序排列整齐,这样做一方面可以节省屏幕空间,另一方面也便于代码的查看和修改。如图1-26 所示,将全局变量、自定义过程及事件处理程序分类码放整齐;左上角的两个过程是项目中无用的过程,暂时也放在这里。

图像说明文字

1.4.7  改造闪现计时器的计时事件处理程序

与按钮点击事件相关联的还有闪现计时器的计时事件。在图1-21 中,我们直接改写了按钮1 及按钮2 的图像及启用属性,现在需要将这段程序加以修改,以适用于所有的按钮。

还记得全局变量翻牌1 和翻牌2 中保存的是什么吗?是的,保存的正是已经被翻开的两个按钮。我们正好可以利用这两个变量,对计时程序中的前四行代码进行改写,如图1-27 所示。

图像说明文字

1.4.8  测试

上述代码需要经过测试才能进入下一步开发,测试结果如图1-28 所示,测试过程记录如下。

(1) 程序启动之后,16 个按钮显示背面图案。✓

(2) 点击按钮1,按钮1 显示正面图案。✓

(3) 点击按钮2,按钮2 显示正面图案,屏幕标题显示“图案不同”。✓

(4) 之后两个正面图案并没有闪现之后变为背面图案。✘

(5) 在开发环境的编程视图中,弹出错误提示,如图1-28 所示。✘

图像说明文字

问题出在哪里呢?图1-28 中右下角的一行字是错误信息的含义,其中提到了“按钮”和“数字”,在程序试图设置按钮的属性时,本应提供按钮类型的参数,却提供了数字类型的参数。这让我们很容易想到对翻牌1 及翻牌2 的设置,它们的值要么是0,要么是某个按钮对象。

问题有可能出在全局变量翻牌1 和翻牌2 的设置上。我们来分析一下程序的执行顺序,如图1-29 所示。

图像说明文字

当翻开两张卡片时:

翻牌1 = 按钮1

翻牌2 = 按钮2

由于图案不相同,闪现计时器被启动,从这一时刻(0 毫秒)起开始计时,500 毫秒之后开始执行计时程序。而此时的按钮点击程序并没有停止,在屏幕标题显示“图案不同”之后,立即执行最后两条命令——设翻牌1 及翻牌2 的值为0。

由于CPU 时钟的数量级是GHz(每秒钟10 亿次运算),整个按钮点击程序的执行时间也不会超过1 毫秒。因此,当计时程序开始运行时,翻牌1 和翻牌2 的值已经被设为0,这就是错误的原因,如图1-28 所示。

为了解决这个问题,我们调整程序的流程,如图1-30 所示。与新流程对应的代码如图1-31 所示。经过测试,程序运行正常,运行结果如图1-32 所示。

图像说明文字

图像说明文字

程序中的错误,程序员称之为bug。要问程序员是怎样炼成的,就是在找bug 的过程中炼成的。因此,不要害怕程序出错,这是人与机器交流的好机会,由此你才能更多地了解计算机,了解程序的运行机制。

程序开发到这里,游戏已经具备了基本的功能,但是这样的游戏显然是毫无乐趣的,任何人最终都能将所有卡片翻开,而且无论如何也只能得到80 分,因此我们要增加游戏的难度,并让那些记忆力超强的玩家能得到更高的分数。我们的方法是限制游戏时间,并用剩余时间来奖励那些高手。

1.5  编写程序——控制游戏时长

我们用计时器组件来控制游戏时长,用数字滑动条组件来显示游戏的剩余时间,组件属性的具体设置参见之前的表1-1。

1.5.1  控制游戏时长

我们用游戏计时器来实现控制游戏时长的功能。游戏计时器的计时间隔为1 秒(即1000 毫秒),即每隔1 秒会触发一次计时事件。如果希望游戏时长为60 秒,那么当计时次数达到60 次时, 游戏结束。为了便于计算成绩,我们利用剩余时间来判断游戏是否结束。声明一个全局变量“剩余时间”,设其初始值为60,在每次计时事件中让它的值减1。当“剩余时间”等于0 时,游戏结束, 游戏计时器停止计时。具体代码如图1-33 所示。

图像说明文字

1.5.2  显示剩余时间

通过设置数字滑动条组件的滑块位置,可以表示游戏的剩余时间。需要说明一点,滑动条的宽度属性只代表它的几何尺寸,而滑块的位置属性仅仅与最大值、最小值以及当前值有关,与滑动条的宽度无关。例如,如果滑动条宽度为120 像素,则每过1 秒,滑块向左移动2 像素,是滑动条宽度的1/60;如果滑动条为180 像素,则每过1 秒,滑块向左移动3 像素,也是宽度的1/60。因此只要在游戏计时器的计时事件中,让滑块位置等于剩余时间即可,代码如图1-34 所示。

图像说明文字

如果此时我们测试程序,滑块不会有任何变化,因为游戏计时器还没有启动。我们需要在屏幕初始化程序中,设置游戏计时器的启用属性为真,如图1-35 所示。

图像说明文字

测试发现,当所有卡片都被翻开,屏幕标题显示“游戏结束”时,滑块仍然在滑动,我们需要在合适的位置添加代码,让游戏计时器停止计时。我们在屏幕初始化程序中启动计时器,还需要在适当的时间让它停止计时。有两种情况需要停止计时:(1) 当剩余时间= 0 时;(2) 当翻牌对数= 8 时。前者我们已经做到了(如图1-33 所示),现在需要对后者进行处理。在“处理点击事件”过程中,当翻牌对数= 8 时,让游戏计时器停止计时,具体代码如图1-36 所示。

图像说明文字

1.5.3  将剩余时间计入总成绩

为了鼓励玩家在更短的时间内翻开所有卡片,我们将剩余时间的10 倍作为奖励,添加到游戏的最后得分中。这样,每次的游戏得分将有所不同,增加了游戏的趣味性。代码如图1-37 所示。

图像说明文字

1.6  编写程序——设计游戏结尾

到目前为止,我们只是用屏幕的标题来显示游戏结束的状态。需要为游戏设计一个正式的结尾,并实现一些重要的功能。这些功能包括:

(1) 显示游戏得分;

(2) 显示历史最高得分;

(3) 清除历史记录;

(4) 返回游戏;

(5) 退出游戏。

上述功能的实现主要依赖于对话框组件及本地数据库组件。我们需要创建一个名为“游戏结束”的过程,并在适当的位置调用该过程。

1.6.1  显示游戏得分

有两种情况会导致游戏结束:(1) 剩余时间= 0;(2) 翻牌对数= 8。这两种情况需要分别加以考虑,其中关键条件是剩余时间是否> 0。如果剩余时间> 0,则计算总分,否则将没有成绩。

对话框组件提供了很多内置过程,在调用这些过程时,屏幕上会弹出一个对话框:有些对话框只显示简单的信息,信息停留片刻后,就会慢慢隐去;有些则可以显示多项信息,并提供若干个按钮供用户选择。在用户选择了某个按钮之后,将触发“选择完成”事件,开发者可以从该事件携带的消息中获得用户的选择,并针对不同选择执行不同的程序分支。在“游戏结束”过程中,我们先使用一个简单的只带一个按钮的内置过程,如图1-38 所示。

图像说明文字

然后在两处分别调用“游戏结束”过程,如图1-39 及图1-40 所示。

图像说明文字

测试结果如图1-41 所示。

图像说明文字

1.6.2  保存游戏得分

针对剩余时间> 0 的情况,我们用一张流程图来理清解决问题的思路,如图1-42 所示。

图像说明文字

App Inventor 支持将应用中的数据保存到手机里。通过调用本地数据库组件的内置过程,可以保存、提取或清除数据,具体方法可参见 https://web.17coding.net/reference 与本地数据库(TinyDB) 相关的条目。由于要显示历史记录,并允许玩家清除记录和退出游戏,我们选用对话框组件最复杂的内置过程,该内置过程可显示标题及消息,并提供3 个按钮供用户选择。我们用标题来显示历史记录,用消息来显示本次得分,3 个按钮分别实现“清除记录”“退出游戏”及“返回游戏”的功能。按照流程图的思路,我们将对游戏结束过程进行改造,修改后的代码如图1-43 所示。

图像说明文字

经测试,游戏运行正常,测试结果见图1-44。

图像说明文字

1.6.3  处理对话框的按钮选择

在对话框组件的“完成选择”事件里,携带了用户的“选择结果”,它等于对话框中按钮上的文本,我们将根据这一信息来决定程序的走向。事件处理程序如图1-45 所示。这里我们暂时用屏幕的标题栏来显示程序的执行结果,稍后我们将编写一个“游戏初始化”过程,来处理“返回”操作。

图像说明文字

提示 退出程序功能在测试阶段无法实现。当游戏开发完成,编译成APK 文件并安装到手机上时, 该功能才能生效。

1.6.4  创建游戏初始化过程

如图1-46 所示,“游戏初始化”过程将实现以下功能。

(1) 生成新的随机图案列表。

(2) 让所有卡片显示背面图案。

(3) 让全局变量翻牌对数= 0。

(4) 让全局变量剩余时间 = 60。

(5) 让滑块回到起始点。

(6) 得分显示为0。

(7) 启动游戏计时器,开始新一轮游戏。

图像说明文字

最后一项任务是将对话框“完成选择”事件中的临时测试语句替换为“游戏初始化”过程,如图1-47 所示。

图像说明文字

在3 项选择中,第一项选择“退出游戏”将退出程序,而其他选择将开始新一轮的游戏。

1.7  程序的测试与修正

程序的编写与测试是相生相伴的,但开发过程中的测试是为了验证局部程序的正确性,这并不能排除程序中的全部错误;因此,当开发工作接近尾声时,还要对程序进行综合测试,并对错误加以修正。

1.7.1  选取列表项错误

1. 测试过程描述

在开发工具中连接手机AI 伴侣,对程序进行测试。当翻开全部卡片后,游戏弹出对话框,再选择返回按钮时,开发工具的编程视图中会出现错误提示,如图1-48 所示。同时,测试手机上显示上一轮游戏结束时的画面,如图1-49 所示。

图像说明文字

2. 问题分析

从错误提示上看,错误与列表操作有关。我们来查看一下,在对话框组件中,当选择了“重新开始”按钮之后,都发生了哪些事情——在对话框1 的“完成选择”事件中,调用了游戏初始化过程,我们来查看该过程。如图1-46 所示,该过程调用了两个过程(随机分配图案和初始化背面图案),并执行了5 条指令(设置全局变量值2 条,以及设置组件属性3 条),其中两个过程都涉及列表操作,那么问题在哪个过程里呢?我们发现,当开发环境提示错误信息时,测试手机上仍然显示上一轮游戏结束时的画面(显示已经翻开的水果图案);也就是说,初始化背面图案的过程没有起作用。我们尝试调换两个过程的顺序,让初始化背面图案过程优先执行,而随机分配图案过程随后执行。如图1-50 所示,测试结果发现所有卡片都显示了背面图案。这说明初始化背面图案过程中没有错误;由此看来,问题就出在随机分配图案过程中。

图像说明文字

在“随机分配图案”过程中,被操作的列表有3 个:(1) 全局变量“图片列表”;(2) 全局变量“随机图案列表”;(3) 局部变量“图案列表”。我们来分析每一步操作之后列表的变化。如图1-51 所示。

图像说明文字

与简单变量不同的是,列表变量在内存中保存了两类信息:(1) 每个列表项的存放地址;(2) 每个列表项的值。当访问列表项时,首先获得的是列表项的地址,然后再根据地址获取列表项的值。

当执行“设列表A 为列表B”这样的指令时,我们并没有开辟另一块内存空间来单独存放列表A, 而是把列表B 存放数据的地址“引见”给列表A,两个列表拥有同一套列表项的地址及列表项的值(或者说是一个列表拥有两个名字)。因此,当我们删除其中任何一个列表中的元素时,另一个列表中对应的元素也就不存在了。

在图1-51 中,当我们从局部变量图案列表中逐项删除其中的元素时,全局变量图片列表中的列表项也会被同时删除,可以通过实验来证明这一点。我们在用户界面中添加一个标签组件,命名为“图片列表”,并在随机分配图案过程中监控图片列表的内容。

先把显示图片列表的代码放在随机分配图案过程的第一行,如图1-52 所示。重新启动程序,测试结果如图1-54 中的左图所示。

图像说明文字

再将显示图片列表的代码放在过程的末尾,如图1-53 所示,其测试结果如图1-54 中的右图所示。

图像说明文字

图1-54 的结果证明了我们的结论:临时变量“图案列表”和全局变量“图片列表”指向的是同一组数据。如果你有兴趣,可以将显示列表内容的代码放在不同的位置,并观察列表项的变化,相信你会有收获的。

3. 程序的修正

找到问题的原因就等于解决了问题的一大半,下面我们来修补程序,完成这个重新开始游戏的功能。

解决方法一:创建一个“图片初始化”过程,在每次重新开始游戏时,调用该过程,如图1-55 所示。

图像说明文字

解决方法二:使用列表复制功能,如图1-56 所示,与图1-53 对比,多了一个“复制列表”的代码块。复制的意思就是另外生成一个一模一样的列表,新列表与原来的列表不再使用同一个存储空间。这样,对新列表的任何操作不会再影响到原有列表。

图像说明文字

如果采用第二种方法,则在游戏初始化过程中,将不必调用图片初始化过程。我们采用第二种方法。

继续进行测试。第一轮游戏运行正常,当开始第二轮游戏时,开发环境中不再出现错误提示, 却发现点击按钮时没有任何反应。

1.7.2  重新开始游戏时点击按钮无响应

1. 问题分析

这也许是最容易解决的一个问题:按钮对于点击行为没有响应。这说明按钮处于禁用状态(启用属性值为假)。回想一下我们的程序,每翻开一对卡片,都会设置按钮的启用属性值为假。在一轮游戏结束,并开始下一轮游戏时,执行了游戏初始化过程,该过程并没有更改按钮的启用属性值,按钮实际上仍然处于禁用状态,因此点击按钮才会没有反应。

2. 修改程序

在游戏初始化过程里,添加针对按钮列表的循环语句,将每个按钮的启用属性值设置为真。修改后的代码如图1-57 所示。

图像说明文字

经过测试,程序运行正常。继续测试发现,在第二轮乃至此后的每一轮游戏中,图案的排列顺序都与第一轮完全相同。

1.7.3  重新开始游戏时图案排列不变

1. 问题分析

图案随机排列的功能由“随机分配图案”过程负责,因此我们来检查这个过程。为了查看程序的执行效果,我们添加了一个标签,用于显示“随机图案列表”的内容,并在随机分配图案过程里设置它的显示文本属性,如图1-58 所示。测试结果如图1-59 所示,随机图案列表的列表项多出一倍。问题的原因在于,每次调用“随机分配图案”过程时,都会在原有列表的末尾添加16 个列表项,因此每一轮游戏都会显示前面的16 个图案,而新生成的16 个图案永远都不可能被显示。

图像说明文字

2. 程序修正

在随机分配图案过程里添加一行代码,在每次调用该过程时,先清空原有列表,如图1-60 所示。

图像说明文字

经过测试,问题得到解决,继续测试。当我们快速点击按钮时,开发工具中会出现这样的错误提示,如图1-61 所示(这个错误在1.4.8 节出现过,见图1-28);同时,快速点击按钮有时会让一张卡片单独翻开,接下来的操作好像与它不再有任何关系,最终也无法让它再配成对,然后是闯关失败。

图像说明文字

1.7.4  快速点击按钮时系统提示错误

1. 问题分析

问题的出现一定与闪现计时器的延迟有关。从闪现计时器开始计时,到第一次计时事件发生,之间有500 毫秒的时间,此时全局变量翻牌1 与翻牌2 都不等于0。如果这期间玩家点击了第三个按钮, 那么翻牌2 将等于第三个按钮,而第二个按钮将失去翻牌2 的“身份”,像一个孤儿一样,不能被再次点击(启用属性值为假),也没有机会被重新设置其背面图案及启用属性。这就是问题出现的原因。

2. 程序修正

为了防止发生这样的问题,我们采用一个极端的方法:在两张不同的卡片被翻开后,让所有的按钮都处于未启用状态,直到两张不同的卡片扣过去之后,再启用那些没有被翻开的按钮。这项功能需要对按钮的翻开状态进行判断。对“处理点击事件”过程进行修改,改过的代码如图1-62 及图1-63 所示。

图像说明文字

图像说明文字

现在,无论你以多快的速度点击按钮,程序都不会再出错了。

测试环节告一段落,不过随着更多的人开始使用这个软件,还有可能发现新的bug。

1.8  代码整理

在一个游戏开发完成之后,整理代码是一个非常好的自我提升机会,它可以让开发者站在全局的高度审视开发过程,将宝贵的开发经验真正收入囊中。图1-64 中列出了应用中的全部代码(略去了用于测试的部分),其中包括7 个全局变量、7 个自定义过程以及20 个事件处理程序,在App Inventor 的编程视图中折叠了所有代码之后,可见的就只有这3 类代码,其他代码都被封装在这3 类代码中。需要提醒读者的是,不要忽视代码的排列,建议按照从左向右的顺序,依次摆放变量、过程及事件处理程序,养成习惯之后,会让自己的开发工作变得井井有条。

图像说明文字

这里再推荐一种要素关系图,图中包含了项目中的各类要素:组件(属性)、变量、过程及事件处理程序,同时给出了各个要素之间的调用或设置关系,其中的黑色箭头表示对过程的调用,深灰色箭头表示对变量的改写,而浅灰色箭头表示对组件属性的设置。它不仅可以帮助我们从整体的角度去认识程序,还能够对程序的优化提供思路,如图1-65 所示。

图像说明文字

首先,要素关系图可以帮助我们查找程序中的错误。图中箭头指向的要素是被调用(过程)或被改写(变量或组件属性)的要素,这样做的好处之一是,我们可以从中看到某个变量的变化原因。例如全局变量“剩余时间”,有两个深灰色箭头指向该变量,它们分别来自“游戏初始化”过程及游戏计时程序,其中前者将其设置为最大值(60 毫秒),而后者对其执行减1 的运算。这样, 当程序的某个环节出现错误时,很容易逆着箭头的方向找到问题的所在。

此外,这个图也可以帮助开发者做代码的优化。例如,在“屏幕初始化”程序中有四行代码, 其中除了调用“创建按钮列表”与“清空按钮文字”过程之外,其余代码都包含在“游戏初始化” 过程之中,可以在屏幕初始化程序中直接调用“游戏初始化”过程,这样既优化了程序的结构,也提高了代码的复用性。

注意图1-65 右下角有一个空闲的全局变量“图片列表”,没有任何箭头指向它。这很容易理解, 在程序运行过程中,它的值只是被读取,而不曾被改写。在一般的编程语言中,有一种语言要素被称为常量,与变量不同的是,它的值在程序运行过程中保持不变,像图片列表这样的数据就可以保存在常量中。

我们可以用“优雅”这个词来形容一组好的程序,好程序其实没有特定的标准,以下几点是笔者个人的经验,与大家共享。

(1) 关注代码的可读性:可读性的关键在于组件、变量及过程的命名。好的命名让代码读起来像一篇文章,易于理解。像本游戏中对计时器的命名,在笔者自己开发这个程序时,用的名称是计时器1 和计时器2,这就不是一种好的命名,在开发到收尾阶段时,连我自己都会发懵。因此在撰写本书时,将计时器1 命名为“闪现计时器”,将计时器2 命名为“游戏计时器”。

(2) 关注程序的结构:从图1-65 中我们可以直观地体会到什么是结构。像这样在事件处理程序中直接改写变量值或组件属性的做法,当程序足够庞大时,会给代码的维护带来很大的麻烦。就App Inventor 开发的程序而言,比较好的做法是,让事件处理程序调用某个过程,让过程来改写变量或属性的值。

(3) 谨慎对待写操作:对组件属性和变量的值有两种操作——读和写。这两种操作中,写操作是不安全的。如果一组程序中有多处代码对同一个变量进行写操作,那么这个变量就像一颗潜伏的炸弹,随时有引爆的危险。好的办法是,减少写操作入口,必要时可以绘制变量的状态图,标出所有的写操作,以便调试或优化程序。

我们对现有程序做如下两项改进。

(1) 改造屏幕初始化事件处理程序:只调用创建按钮列表、清空按钮文字及游戏初始化3 个过程。

(2) 去除重复调用:在要素关系图中,游戏计时器组件汇聚了3 个箭头,应该减为两个箭头, 因为对计时器的设置只有两种可能,即启用计时或终止计时。启用计时在游戏初始化过程中执行,终止计时在游戏计时(事件处理)程序以及游戏结束过程中执行,而游戏计时程序又调用了游戏结束过程,这相当于终止计时被执行了两次。因此可以删除前者对终止计时的设置,这样指向游戏计时的箭头就剩下两个了。

我们重新绘制改进之后的要素关系图,如图1-66 所示。

图像说明文字

目录

  • 推荐序一
  • 推荐序二
  • 前言
  • 第1章 水果配对
  • 第2章 计算器
  • 第3章 九格拼图
  • 第4章 天气预报——基础版
  • 第5章 天气预报——图片版
  • 第6章 打地鼠
  • 第7章 幼儿加法启蒙
  • 第8章 简易家庭账本——登录
  • 第9章 简易家庭账本——导航菜单与收入记录
  • 第10章 简易家庭账本——系统设置
  • 第11章 简易家庭账本——支出记录
  • 第12章 简易家庭账本——收支查询
  • 第13章 简易家庭账本——年度收支汇总
  • 第14章 简易家庭账本——分类汇总及其他
  • 第15章 数学实验室(一):鸡兔同笼
  • 第16章 数学实验室(二):素数问题
  • 第17章 数学实验室(三):公约数与公倍数
  • 第18章 数学实验室(四):绘制函数曲线
  • 第19章 寻找加油站
  • 第20章 贪吃蛇
  • 第21章 因式分解之十字相乘
  • 后记
  • 附录A 开发工具、测试方案与学习资源
  • 附录B 不同的App Inventor汉化版本