第三章 九格拼图

第一节 游戏描述

  1. 时间因素:对游戏耗时进行记录,以秒为单位,游戏时长将成为最终得分的负面因素;
  2. 空间因素:用户界面分为上中下三部分,上部显示完整的图片、游戏耗时、移动次数及“重新开始”按钮,中部为一个3×3的格子,完整的图片被平均分割成9个小图(在程序中被称为碎片),其中的8个小图随机排列在9个格子中(去掉了右下角的小图),留有一个空格,供玩家移动小图,并最终拼出原始图片;下部为消息提示区,用来显示程序运行过程中的提示信息;
  3. 游戏操作:用户触摸空格周围的小图(程序中被称为“空格的邻居”),小图将自动移动到空格中,原来的位置变为空格;每移动一次小图,累计一次移动次数,移动次数将成为最终得分的负面因素;
  4. 记分规则:有两个因素会影响到玩家的最终得分——游戏耗时及移动次数,计算方法为: 得分 =(10000-移动次数)/游戏耗时(取计算结果的整数部分)
  5. 历史记录:在互联网上保存游戏的最高纪录(包括游戏耗时、移动次数及最终得分),每次游戏结束时,显示历史记录,如果本次得分高于历史记录,则保存本次得分;
  6. 重新开始游戏:玩家在成功完成一次拼图后,将弹出对话框,选择对话框中的“返回”按钮将开始新一轮的游戏;在游戏过程中,玩家可以点击“重新开始”按钮,放弃当前游戏,开始新一轮游戏;
  7. 退出游戏:拼图成功后,玩家可以选择对话框中的退出按钮,退出游戏。
图3-1 九格拼图游戏的用户界面

第二节 界面设计

一、界面布局

屏幕的顶部为水平布局组件,分左右两部分,左侧为小尺寸的完整图片,右侧为垂直布局组件,其中包含了两个水平布局组件和一个重新开始按钮,水平布局组件中分别放置标签来显示游戏耗时及移动次数;屏幕中部为一个正方形画布,其中放置了8个图片精灵,通过坐标来决定精灵的位置;屏幕底部有三个标签,用来显示程序运行过程中的相关信息,如列表变量的值、网络数据库的返回信息、开发者信息等等。如图3-2所示,组件清单见表3-1。(由于小图素材的尺寸为111×111像素,而图片精灵的宽、高属性尚未设置,因此图片之间有重叠。)

图3-2 设计游戏的用户界面

表3-1 组件清单

二、组件属性设置

表3-2 组件属性设置(按屏幕自上而下顺序)

三、上传资源文件

资源文件共10张图片(其实只需要9个),其中0.png(330×350)为原始图,1~9.png为局部小图(第9张图可以省去),大小为110×110,如图3-3所示。

图3-3 图片素材

由于画布宽度为300像素,因此小图的尺寸需要在程序中统一进行设定,宽、高均设置为99像素,这样小图之间有1个像素的空白。上传后的资源文件如图3-3中的素材清单。

第三节 难点分析

一、程序的主流程

游戏之初,8个碎片(小图)被随机放置在9个空格中,并保留一个空格;玩家通过不断触碰空格周围的碎片来实现碎片的移动(碎片与空格交换位置),并最终使得所有碎片按照正确的顺序排列。程序的主流程如图3-4所示。

图3-4 程序的主流程

二、术语解释

在接下来的问题分析及程序编写过程中,会频繁地提到几个词语,这里先行做一下解释。

(1)位置编号:画布被等分为3行、3列,形成了9个格子,这9个格子按照从左向右、自上而下的顺序,被赋予从1至9的编号,如表3-5所示,其中的阿拉伯数字为位置编号。在后续的讨论中,这组编号是固定不变的因素,将作为其他可变要素的参照。

图3-5 位置编号是固定不变的

(2)碎片代号:在第二节中,我们对每个精灵组件进行了重命名,它们名字的末尾是从1~8的数字,这组数字既是每个碎片的代号,也是精灵组件所显示的文件名(不包含.PNG的部分)。在图3-5中,中文数字(壹、贰、叁…)表示的是碎片代号。

(3)空格的邻居:指的与空格相邻的四个位置:正上方、正下方、左侧及右侧,如图3-5中,碎片叁(位置1)、碎片贰(位置5)及碎片壹(位置7)是空格的邻居。一个碎片仅当它是空格的邻居时,才能被移动。

(4)碎片排列顺序:如果将8个碎片随机摆放在9个格子中,它们的排列顺序将以位置编号为基准,其中包含了空格的代号9。例如,图3-5中的排列顺序,可以表示为(3 8 5 9 2 7 1 6 4),如表3-3及图3-6所示。

表3-3 碎片的排列顺序(随机)

图3-6 碎片的排列顺序(随机)

针对图3-5中的排列数序,当玩家对碎片进行了一番挪动后,碎片的代号与与位置编号相匹配时,说明拼图成功,如图3-7所示,此时碎片的排列顺序可以表示为(1 2 3 4 5 6 7 8 9),如表3-4所示。

图3-7 拼图成功时碎片的排列顺序

表3-4 碎片的排列顺序(拼图成功)

三、难点分析

在实现这个游戏的过程中,有三个关键点:

  1. 碎片的随机摆放;
  2. 碎片的移动;
  3. 对拼图成功的判断。

与这三个关键点密切相关的是碎片的排列顺序。(1)是对碎片排列顺序的初始化,(2)是对排列顺序的改变,(3)是判断碎片排列顺序是否与位置编号相匹配。下面分别讨论解决这三个问题的思路

(1)碎片的随机排列

随机排列是游戏中带有普遍性的问题,比如扑克牌游戏中的洗牌,又如本书第一章《水果配对》中的水果图案的随机分配,等等。解决这类问题需要两个列表:顺序列表和随机列表。初始状态下,顺序列表中放置了即将被打乱顺序的所有元素,它们按照某种固定的顺序排列,而随机列表为空。利用循环语句,对顺序列表进行遍历,每循环一次,从顺序列表中随机选择一项,追加到随机列表中,并从顺序列表中删除该选中项,然后进入下一次循环。这个技术的关键在于每次循环生成的随机数,随机数的范围从1到顺序列表的长度N,注意这个N是变化的,因为每次循环都要从顺序列表中删除一项。程序的流程如图3-8所示。

图3-8 让碎片随机排列的操作流程
图3-9 移动碎片的流程

(2)碎片的移动

所谓移动有两重含义:(1)数据的更新——碎片的排列顺序产生变化(空格与碎片的位置对调);(2)用户界面的更新——将碎片移动到空格的位置,碎片原来的位置成为空格。这项操作并不困难。这一步的关键在于判断玩家触碰的碎片是不是空格的邻居。在程序的运行过程中,随时跟踪空格的邻居,它会随每一步移动而变化。我们用列表来保存那些成为空格邻居的碎片,以便随时判断某个被触碰的碎片是否在邻居列表中。程序的流程如图3-9所示。

(3)判断拼图是否成功

在每次移动碎片之后,判断碎片的排列顺序是否与位置编号相匹配。通过循环语句对碎片顺序列表进行遍历,逐个对比碎片代号与循环变量是否相等,如果全部相等,则拼图成功。

以上分析我们没有动用一行代码,但是已经将程序的脉络理清了。作为一个初学者,很不容易做到这一点,因为在开始编写一个软件之前,你通常不知道会遭遇那些困难。不过,我们学习编程的过程也是不断遭遇难题,然后解决难题的过程。随着经历的难题越来越多,你可以学会在动手之前,预计可能存在的技术上的障碍,并优先着手铲除这些障碍,之后再进入到常规的开发过程。

第四节 编写程序——初始化

一、初始化全局变量

在整个程序中,我们声明了4个全局变量,如图3-10所示:碎片列表、坐标列表、空格的邻居(列表)及碎片排列顺序(列表),其中的前两个列表在整个程序的运行过程中,列表的长度、列表项的值及列表项的排列顺序均保持不变,我们可以把它们理解为常量。后两个列表会随着程序的运行发生改变,其中空格的邻居列表的长度及列表项的值都会发生改变,而碎片排列顺序列表的长度及列表项的值不变,但列表项的排列顺序会发生改变。

图3-10 初始化全局变量

(1)碎片列表

在上一章《计算器》中我们引入了组件对象的概念,本章这个概念将派上大用场。首先我们声明一个全局变量碎片列表,并将其初始化为空列表(如图3-10),然后创建一个过程——初始化碎片列表,在这个过程里,我们生成了一个包含8个碎片的列表,稍后将在屏幕初始化事件中调用该过程。如图3-11所示。

图3-11 初始化碎片列表

(2)坐标列表

精灵组件放置在画布上,可以通过设定它们的(x,y)坐标属性,来精确地设置它们的位置。这里使用循环语句对坐标值进行设置,如图3-12。由于这些坐标值本身在整个程序运行过程中保持不变,也可以用直接创建列表的方式来初始化坐标值,这两种方式是等价的。不过程序员可能更喜欢采用第一种方式。试想,此刻我们设置坐标的前提是画布的宽高为300,小图的宽高为99,如果我们想改变画布或小图的尺寸,那么第二种方式写成的代码修改起来将更为麻烦,而第一种方式仅需修改乘数100即可。

图3-12 初始化坐标列表

(3)碎片排列顺序

如果你理解了图3-8中的流程,很容易理解下面的过程——随机排列碎片:图3-8中的顺序列表OList与过程中的固定位置列表相对应,图3-8中的随机列表RList与碎片排列顺序列表相对应。虽然固定位置列表中有9个列表项,但循环语句只执行了8次循环,这是因为8次循环之后,固定位置列表中只剩下一个列表项,没有必要再求随机数,将这个唯一的列表项直接追加到碎片排列顺序列表中即可。如图3-14所示。

图3-13 初始化碎片排列顺序列表

(4)求空格的邻居 这是一个有返回值的过程,根据空格的位置求出它的邻居的列表。这是过程看起来代码很多,但其中的逻辑并不复杂,如图3-15所示,首先求出空格在碎片排列顺序列表中的位置,如果空格不靠画布的左边界,则将空格左侧的碎片列为它的邻居;如果空格不靠画布的右边界,则将空格右侧的碎片列为它的邻居,以此类推。一个空格最多只能有4个邻居(当空格的位置=5时),最少可以有2个邻居(当空格在四个角上时)。

图3-14 求空格的邻居

二、初始化组件属性

(1)初始化碎片属性:图片、宽、高

这些属性在程序运行过程中保持不变,因此将其归到一个过程中设置。如图3-16所示。

图3-15 设置图像精灵的属性:图片、宽度及高度

(2)设置碎片的(x,y)坐标

在程序运行过程中,碎片的坐标会因碎片的移动而改变,这里定义了一个过程——放置碎片,依据前面已经初始化的碎片排列顺序列表,对碎片的位置进行初始设置,如图3-17所示。

图3-16 初始化碎片的位置——(x,y)坐标

三、屏幕初始化事件处理程序

在屏幕初始化事件中,调用上述过程,完成游戏中数据及用户界面的初始化,如图3-18所示。

图3-17 屏幕初始化时间处理程序

注意图中几个过程的调用顺序,想想看,为什么要依照这样的顺序执行?

四、跟踪程序的执行过程

我们在画布的下方添加了一个水平布局组件,其中放置了两个标签组件,分别命名为碎片顺序及邻居,用来跟踪程序运行过程中两个变动列表的值,以便于我们更好地理解程序的运行过程,并为纠错提供方便。界面设计如图3-19所示。

图3-18 添加两个标签,用来显示列表变量的值

然后编写一个过程——查看关键数据,就只有两行代码,如图3-19所示。可以在程序中需要的位置调用这个过程,跟踪列表值的变化。首先在屏幕初始化事件中调用该过程,如图3-20所示。

图3-19 跟踪列表变量的变化

测试结果如图3-21所示。注意两个画布下方两个标签的显示值,左侧标签显示了碎片排列顺序,右侧列表显示了空格的邻居。

图3-20 屏幕初始化之后的用户界面

第五节 编写程序——移动碎片

画布上有8个精灵组件,利用精灵组件的触摸事件来触发碎片的移动。我们首先以图3-20中的碎片1为例,按照流程图3-9中的设计,来实现碎片1的移动,然后再将与碎片1相关的代码,移植到其他的碎片上。

首先判断碎片1是否在空格的邻居列表中,如果判断结果为真,则执行空格与碎片1的换位操作:(1)更新碎片顺序列表,(2)设置碎片1的新位置;换位完成后,再更新空格的邻居列表,并查看两个列表的变动情况。代码如图3-22所示。

图3-21 实现碎片1与空格的位置交换

下面我们将碎片1的代码推而广之。首先创建一个通用的过程——移动碎片,将上述代码移动到新建的过程里。为了使过程对所有碎片通用,为过程添加一个参数——碎片代号,将原有碎片1的代号替换为这个参数。此外,将设置碎片1的X、Y坐标的代码替换为任意组件类代码——设某图像精灵组件的X、Y坐标,修改后的代码如图3-22所示。

图3-22 移动碎片的通用过程--对所有碎片有效

为了提高代码的可读性,我们新建一个过程——碎片与空格换位,将移动碎片过程里碎片与空格交换位置的代码封装在这个新建的过程里,如图3-23所示。

图3-23 创建碎片与空格换位过程

然后在移动碎片过程里调用新建过程,如图3-24所示。

图3-24 在移动碎片过程里调用碎片与空格换位过程

下面编写所有碎片的触摸事件处理程序。在处理程序中调用移动碎片过程,并为过程提供参数——碎片代号,代码如图3-25所示.

图3-25 编写所有碎片的触摸事件处理程序

至此,我们的游戏已经具备了最基本的功能:当玩家触摸某个空格相邻的碎片时,碎片会与空格调换位置。从程序跟踪的结果上看,程序的运行结果是正确的。测试结果如图3-26所示。

图3-26 跟踪测试移动碎片程序

第六节 编写程序——判断拼图是否成功

每次移动碎片完成之后,要做一个判断——碎片的排列顺序是否与位置编码相匹配,如果匹配,则拼图成功,游戏结束,否则,继续等待下一次移动。通过观察游戏运行过程中碎片顺序列表的变化,你会发现当拼图成功时,碎片顺序列表是这样的(1 2 3 4 5 6 7 8 9),如图3-26所示,对这个排列的验证只需要一个循环语句就可以完成,代码如图3-27所示。我们创建了一个有返回值的过程:拼图成功,先假设局部变量成功为真,遍历碎片排列顺序列表,一旦有某个碎片的代号与循环变量不相等,也就是与位置编码不相等,则成功为假,过程的返回值为假;如果所有碎片的代号均与循环变量相等,则返回值为真。

图3-27 有返回值的过程——判断拼图是否成功

在移动碎片过程里调用拼图成功过程,如图3-28所示。

图3-28 每次移动碎片完成后,判断拼图是否成功

第七节 编写程序——计算游戏得分

一、统计游戏耗时

利用计时器的计时事件来累计游戏时长,计时器每隔1秒钟触发一次计时事件,因此用户界面上的耗时标签每隔1秒钟刷新一次数据。代码如图3-29所示。这里我们没有单独设置一个全局变量来累计游戏耗时,而是利用耗时标签的显示文本来保存这个累计值。值得一提的是,组件的属性同样具有保存值的功能,充分利这一点,可以减少全局变量的使用,减少对设备内存的占用。

图3-29 累计并显示游戏耗时

二、统计碎片移动次数

同累计游戏耗时一样,我们利用移动次数标签的显示文本来保存移动次数的累计结果,代码如图3-30所示。

图3-30 累计并显示碎片的移动次数

三、计算游戏得分

当游戏结束时,根据游戏耗时及移动次数来计算游戏得分,并在对话框中显示出来。注意,这是设计时器的启用属性为假,令计时器停止计时。代码如图3-31所示。

图3-31 计算并显示游戏得分

第八节 编写程序——游戏结束

一、提取历史记录

网络微数据库组件(TinyWebDB)用于在互联网上保存信息,可以实现数据在网络上的共享。这里将游戏的成绩以列表的方式保存到网络微数据库中,每次拼图成功时,提取并显示历史记录,如果历史记录不存在,则显示历史记录为空,并将本次成绩保存到数据库中。

网络数据库的访问是一种异步通信,当一个请求发出去后,并不能立即收到请求结果(本地的微数据库组件TinyDB就可以立即收到结果),而且何时能够收到结果也不确定,要看整个访问链路(手机-WIFI-网络服务器)的通信状况。不过为了让我们的应用能够具有分享功能,我们还是可以忽略这个不足1秒钟或1秒钟左右的延迟。当应用收到网络返回的数据时,会触发网络微数据库组件的获得数值事件,事件中携带了两条消息:(1)请求数据使用的标签;(2)请求的数据本身。当拼图成功时,我们让网络微数据库发出请求数据指令,并利用获得数值事件来处理游戏结束后的相关操作,代码如图3-32所示。首先我们在移动碎片过程里添加代码,当拼图成功时,用标签PINTU_SCORE向数据库发出数据请求,并将原来与得分相关的代码放在网络微数据库组件的获得数值事件中。

图3-32 向网络微数据库请求数据

我们设保存及提取数据的标签为PINTU_SCORE,第一次从数据库提取数据时,数据库为空,这时将返回空值;当保存过一次成绩之后,数据库的返回值是一个包含三个列表项的列表,其列表项分别为得分、游戏耗时及移动次数。针对不同的返回值,我们需要进行判断,并进行不同的处理,代码如图3-33所示。

图3-33游戏结束时,向网络微数据库请求历史记录,并根据返回结果显示相应内容

二 更新历史记录

在两种情况下需要保存得分:(1)如果历史记录为空;(2)历史记录不为空,但本次得分大于历史得分。仍然以PINTU_SCORE为标签,保存成绩列表。更新历史记录的代码应该写在获得数值事件处理程序中,为了使程序具有可读性,我们创建一个过程,来实现保存成绩的功能。如图3-34所示。

图3-34 保存成绩过程

然后在获得数值事件处理程序中调用该过程,如图3-35所示。

图3-35 调用保存成绩过程

当成绩保存成功时,网络微数据库组件将触发完成数值存储事件,我们在这个事件处理程序中,让消息标签显示“成绩保存成功”,来确认程序的执行结果。在一个商用的程序中,通常会用一个标签,或弹出一个对话框,来提示操作结果,这里我们只是让读者了解网络微数据库组件的功能。代码如图3-36所示。

图3-36 用屏幕标题显示程序的执行结果

网络微数据库组件的两项操作(获取或保存数据)都与互联网有关,当网络中的某个环节发生问题时,会导致操作失败,这时将触发另一个事件——发生Web服务故障事件,该事件携带了一条消息,来说明故障的具体原因,我们用消息标签来显示它。如图3-37所示。

图3-37 当网络发生故障时,触发web服务故障事件

三、处理对话框的完成选择事件

如图3-31所示,当游戏结束时,我们为玩家提供了两个选择:退出或返回游戏,当玩家选择退出时,游戏将关闭;当玩家选择返回时,将重新开始新一轮游戏。我们先来实现重新开始游戏的功能。

首先创建一个游戏初始化过程。我们可以查看一下屏幕初始化程序,其中包含两部分代码:(1)对不变要素的初始化,包括初始化碎片列表、初始化坐标列表及初始化碎片属性;(2)对变动要素的初始化,包括初始化随机排列碎片、放置碎片及求空格的邻居。游戏初始化过程将包含第(2)部分代码;除此之外,还要启动计时器,并将耗时及移动次数标签的显示文本设为0,将消息标签的显示文本设置为空,代码如图3-38所示。这里仍然调用了查看关键数据过程,以便于我们的跟踪调试,在正式发布应用时,需要将这个调用去除掉。

图3-38 定义游戏初始化过程

下面来编写对话框完成选择时间处理程序,代码如图3-39所示。

图3-39 对话框完成选择事件处理程序

退出选项无法用AI伴侣进行测试,只有将程序编译并安装到Android设备上,退出功能才能生效。

四、添加重新开始按钮

在测试过程中,发现有一种情况似乎拼图是永远不可能成功的,就是只剩下最后两个顺序错误的碎片时(也可能是我还没有找到成功的方法)。这时,需要放弃这一轮游戏,重新开始新一轮游戏。添加一个重新开始按钮,当按钮被点击时,调用游戏初始化过程,重新开始游戏。代码如图3-40所示。

图3-40 游戏玩不开时重新开始

第九节 代码整理

一、 代码的罗列

图3-41中列出了程序中的全部代码,包括4个全局变量、12个自定义过程以及15个事件处理程序。全局变量均为列表型变量,其中前两个为固定列表(常量),后两个为变动列表,程序中几乎所有的过程及事件处理程序都围绕着这两个变动列表而展开。除此之外,两个标签——耗时及移动次数,除了用于显示数据,也同时充当了全局变量角色。

图3-41 程序中的全部代码

在12个过程里,过程1、2用于初始化固定列表,过程3用于初始化精灵组件的不变属性,过程4用于初始化变动列表,过程5用于初始化精灵组件位置,过程6通过调用过程7~9来实现精灵组件的移动,过程10实现数据的存储,过程11用于跟踪程序的运行过程,是调试程序的工具,不是必须的,过程12用于初始化变动因素(列表及组件属性),以便开始新一轮的游戏。

整个应用中有15个事件处理程序,虽然程序的数量较多,但每个程序并不复杂,稍后可以从要素关系图中窥见它们的全貌。

二、要素关系图

图3-42 程序的要素关系图

从图3-42中可以看出,最核心的过程是游戏初始化及移动碎片,在所有组件中碎片N 的状态被改写的最为频繁。从图中我们还发现,屏幕初始化程序与游戏初始化过程调用了某些共同的过程,我们可以改造一下程序,让屏幕初始化程序调用游戏初始化过程,这样可以简化要素之间的关系,提高代码的可读性与复用性,如图3-43所示。

图3-43 改进屏幕初始化程序

改进后的要素关系图如图3-44所示。

图3-44 改进后的要素关系图

本章内容到此结束,不过我本人对这个游戏还存有一丝疑惑,在测试过程中,发现有些情况下,拼图无法成功,而有些时候成功又非常容易,也就是说,随机生成的游戏初始状态对能否拼图成功,或成功的难易程度是有影响的,这影响了游戏的公平性,也使得游戏的得分缺乏说服力。