第十四章 对19种组块的列表数据编程

在上一章中我们熟悉了相对坐标的概念,这一章我们将全面应用这一概念,为19种组块建立数据模型——列表,并通过对列表的操作,来实现游戏中的大部分功能。

第一节 组块的绘制

一、组块绘制的数据

表14-1 19种组块的绘制数据——相对坐标及颜色码

上表中列出了所有类型组块的绘制坐标以及颜色码,组块上的标记是基准行、基准列的位置,所有坐标值都是以这一点为原点而设置的,第一个值(x)为行,第二个至(y)值为列。颜色码对应于颜色列表中每一种颜色的索引值。如图14-1,我们声明一个全局变量颜色列表,将赤橙红绿青蓝紫灰这8种颜色设置为列表元素,它们的索引值分别为1~8,注意这里的灰色,索引值为8。

图14-1 声明全局变量颜色列表

二、初始化全局变量——组块坐标列表

有了以上数据,我们就可以创建组块坐标列表,如图14-2所示,列表的第1层包含19个列表项,代表19种类型的组块;第2层各包含5个列表项,其中前四个列表项代表组块中的四个方块(直到这时,我才意识到所有的组块都包含4个方块!),第五个列表项代表这种组块的颜色码;第2层的前四个列表项包含下一层(第3层)列表,包含两个列表项,代表相对坐标的行、列值。

无论是上面的表格,还是图14-2中的列表,乍看起来的确有些令人望而生畏,但实际上这些工作需要的只是细心和耐心,程序员的工作就是无法避开一些看似琐碎的事情。

图14-2 所有类型组块的绘制坐标列表

三、编写绘制及擦除过程

有了组块坐标列表,我们就可以为19种组块创建一个统一的绘制及擦除过程,如图14-3所示。这里用到了上一章讲到的多层列表访问技术,代码中结合了两种访问方式(上一章提到的逐层访问及直接访问)。

图14-3 对19种组块通用的绘制及擦除过程

想想我们此前的计划——为19种组块分别建立绘制及擦除过程,而现在,因为有了数据列表,大大减少了代码量,这同样意味着提升了代码的坚固性,避免了错误的发生。这就是技术的力量!

第二节 修改相关过程

为了让我们的工作显得井井有条,我们将任务列表复制到这里。(其实是对自己没信心,怕漏掉了某项任务。)

  1. 需要修改条件判断语句,扩展分支语句的过程:
    • a. 组块下落
    • b. 求触底组块覆盖的行
    • c. 已经触块
    • d. 已经触顶
    • e. 求重绘起始行
    • f. 更新色块列表
  2. 需要扩展数值范围的过程:
    • a. 创建新组块:将随机数范围从1~2扩展到1~19
    • b. 重绘画布:将色块列表中的颜色码对应为颜色列表中的具体颜色
  3. 事件处理程序:
    • a. 左移按钮的点击事件处理程序
    • b. 右移按钮的点击事件处理程序
    • c. 快落按钮的点击事件处理程序
    • d. 旋转按钮的点击事件处理程序

一、创建新组块

我们从最容易修改的过程开始,如图14-4所示,只需要将随机数的上限值改为19即可。

图14-4 修改后的创建新组块过程

二、组块下落

如图14-5,这个过程的修改也非常简单,简单得都没什么可说的了。

图14-5 修改组块下落过程

三、求触底组块覆盖的行

1、整理数据

如表14-2,每种组块所覆盖的行不尽相同,但可以归结为四类,在表中我们用不同的背景颜色加以区分。其实无非是覆盖的行数不同(1、2、3、4),稍候你会看到这也将基准行统一设置为最下面一行所带来的好处。

表14-2 19种组块所覆盖的行

2、建立数据列表——覆盖三行组块列表

如图14-6所示,之所以这样设置,是因为覆盖三行的组块种类少于覆盖两行的组块种类。等一会儿你将明白为什么这么设置。

图14-6 初始化列表——覆盖三行组块列表

3、修改过程——求触底组块覆盖的行

这是一个有返回值的过程,返回值是一个列表,其中包含了触底组块覆盖的行(行号)。如图14-7所示,由于覆盖1行、4行的组块各有一个,因此单独对1、2号组块进行判断;注意第三个“如果”——覆盖三行组块列表中是否含有组块编号列表项,用来判断当前组块是否覆盖了三行,如果是,返回一个包含三个列表项的覆盖行号列表,如果不是,则返回包含两个行号的列表。

图14-7 修改过程——求触底组块覆盖的行

四、已经触块

1、数据整理

表14-3 19种组块底部方块坐标

组块在下落时,有触底及触块两种可能,触底具有统一的判断标准,即基准行>16,但触块的判断没有统一标准,与组块底部的形状有关。表14-3给出了19种组块底部方块的相对坐标值,我们将依据这些数据来判断组块是否触块。

2、建立数据列表

如图14-8所示,我们初始化一个全局变量——触块坐标列表,这同样是一个三层列表:第一层包含19个列表项,代表19种类型的组块;第2层分别包含1~4个列表项,是组块中可能在下方触块的方块坐标;第3层各包含两个列表项,是具体的坐标值(行,列)。

图14-8 触块坐标列表

3、修改过程——已经触块

这是一个有返回值的过程,如果已经触块,则返回值为真,否则为假。过程的核心代码是求颜色码,针对组块底部的方块,判断其下方的方块颜色码是否为0,只要有一个方块的颜色码不为0,则触块为真。

通过访问全局变量触块坐标列表中与组块编号相对应的列表项——局部变量触块坐标列表,来求得可能触块的底部方块列表,并通过对局部变量触块坐标列表的遍历,来求得具体的坐标值,并据此求得颜色码。具体代码如图14-9所示。

图14-9 修改过的已经触块过程

我们在第12章 遗留了一个问题,为什么两个临时变量触块坐标列表及列表长度不能在同一个代码块中进行设置声明,如图14-9中右下角的块。我们来做一个试验,如图14-10所示,我们在同一个临时变量块中同时声明两个变量:列表及列表长度,并同时为列表及列表长度初始化,让列表长度等于刚刚创建的列表(包含两个列表项)的长度,结果是:当我们将“取列表”块放在求列表长度块内时,“取列表”块显示警告,说明这种代码的用法是错误的。为什么会这样?主要是因为这两个临时变量被同时声明,也被同时初始化,它们之间没有先后顺序,也就是说,在初始化列表长度时,列表也正在做初始化,因此它们之间不能存在依赖关系。

图14-10 同时声明的临时变量之间不能有依赖关系

五、已经触顶

1、数据整理

表14-4 19种组块触顶时基准行的取值范围

对触顶的定义是:组块顶行位于灰色方阵的第一行,根据组块顶行的位置以及组块的行数,可以推算出基准行的值,如表14-4所示。

2、建立数据列表

在表14-4中,我们将组块覆盖的行的数据也列在表中,与触顶基准行的取值范围作对照,发现这两类数据有相同的归类方式,因此,我们之前建立的“覆盖三行组块列表”同样可以用于对触顶的判断。

3、修改过程——已经触顶

这是一个有返回值的过程,当组块已经触块时,会调用该过程,来判断触块的组块是否同时触顶。如果触顶,则返回值为真,否则,返回值为假。如图14-11所示,以1号组块为例,返回值为“前一刻基准行=1”,这是一个由数学等号连接起来的式子。在App Inventor中,有两个等号,如图14-11的右上角所示,这两个等号作用完全相同,用于判断等号两边的值是否相等,如果相等,则返回真,否则返回假。其他几个数学比较算符(大于、小于等)也会返回逻辑值(真、假)。

图14-11 修改后的已经触顶过程

六、求重绘起始行

这是一个有返回值的过程,返回一个行号——画布上包含彩色方块的最小的行,当触底或触块的组块填满某些行时,需要从色块列表中删除这些行,并重绘画布。为了减少不必要的重绘工作量,对于那些位于画布上部的没有彩色方块的行,不执行重绘指令。这个重绘起始行是重绘画布过程的参数,表明重绘操作从这一行开始。至于重绘终止行,被设为前一刻基准行。我们此前专门讨论过这个话题(见第十一章第一节)。

1、数据整理

表14-5 19种组块顶行方块的相对y轴坐标——相对行号

求重绘起始行,也就是求组块的顶行,根据组块在垂直方向上覆盖的行数,可以求出顶行相对于基准行的相对行号,如表14-5所示。

2、建立数据列表

在表14-5中,我们依然让两行数据对照出现,发现两类数据具有相同的归类方式,因此我们仍然可以使用数据“覆盖三行组块列表”,来求重绘起始行。

3、修改过程——求重绘起始行

如图14-12所示,过程中的临时变量组块顶行是已经触底或触块组块顶行方块的绝对行坐标值,每当有触底或触块的情况发生,就要计算一次组块顶行,并与此前记录的重绘起始行值进行比较,并保留较小的值,作为新的重绘起始行值。

图14-12 修改后的求重绘起始行过程

七、更新色块列表

1、数据整理

当一个组块触底或触块后,需要更新组块列表,将列表中有关列表项的颜色码由0更改为触底或触块组块的颜色码,这就需要知道触底组块中所有方块的坐标,而这恰恰是组块坐标列表的内容。

2、建立列表变量

参见组块坐标列表,我们可以用组块编号作为索引值,访问该列表中对应的列表项,从而获得触底或触块组块的坐标列表。

3、修改过程——更新色块列表

这是一个无返回值的过程,如图14-13所示,过程中使用临时变量相对坐标列表来保存从全局变量组块坐标列表中选出的列表项,并对相对坐标列表进行遍历,逐一修改他们的颜色码。注意,这里的颜色码来自于相对坐标列表的第5项。组块坐标列表中不仅包含了19种组块的相对坐标值,也包含了组块的颜色码。

图14-13 修改后的更新色块列表过程

以上我们完成了七个过程的修改,下面开始对事件处理程序的修改。

八、重绘画布

在重绘画布过程中,为画块过程的颜色参数提供更多种可能的颜色值,代码如图14-14所示。

图14-14 改造之后的重绘画布过程

以上完成了对8个过程的修改,下面开始对事件处理程序进行修改。

九、左移程序

1、数据整理

表14-6 19种组块向左移动的边界及禁区

向左移动能否实现,取决于两个条件,即,组块的左边界:①是否触边;②是否触块,因此我们的数据整理也分为两类。第一行数据表示组块左边界不触边的条件,以1号组块为例,当基准列>3时,不触边。第二行数据是组块左边界的各个方块相对于基准行列的相对坐标值,1号组块的左边界只有一个方块,其相对于基准行列的相对坐标为(0,-2)。

2、建立数据列表

在整理19种组块左移触边的条件时,我们发现这些数据可以归为3类:>1、>2及>3,其中>3的组块有两种,可以直接使用条件语句进行判断,除此之外的两种中,>1的组块个数较少,可以将其保存在一个列表中,通过列表查询进行判断,其余的组块可以用条件语句的“否则”分支来处理。我们为列表取名“左移列表_大于1”,里面保存了五个组块编号,它们触边的条件是“基准列≤1”(与可移动的条件正好相反),如图14-15所示。

图14-15 左移列表_大于1

我们还需要建立一个左移禁区边界坐标列表,来保存第二行的数据。这是一个三层列表,第1层包含19个列表项,代表19种类型的组块;第2层列表包含1~4个数量不等的列表项,代表组块左边界若干个方块的相对坐标值对;第3层列表包含2个列表项,代表相对坐标的行、列值。如图14-16所示。

图14-16 左移禁区的相对坐标列表

3、修改左移程序

图14-17 修改之后的左移程序

与此前修改的几个过程相比,左移程序的修改略显复杂。如图14-17所示,主流程分为两个阶段:判断阶段与执行阶段。在判断阶段,在基准行≤16的大前提下,判断又划分为两个阶段:首先判断是否触边(用到了刚刚建立的左移列表_大于1),然后,在组块不触边的前提下,再判断组块的左边界是否触块。以组块编号为索引值取得临时变量禁区坐标——组块左边界方块的坐标列表;求得列表的长度,并遍历禁区坐标的列表项,将每一组相对坐标换算为绝对坐标,求其左侧一列(绝对列-1)方块的颜色码。如果颜色码≠0,则意味着组块的左边界触碰到已经触底的块,将无法继续向左移动。

左移程序的执行阶段很简单:当组块既不触边也不触块时,让新的基准列值=原基准列值-1。

十、右移按钮的点击事件处理程序

1、数据整理

表14-7 19种组块向右移动的边界及禁区

与向左移动类似,组块能否实现向右移动,取决于两个条件,即,组块的右边界:①是否触边;②是否触块。表14-7中给出了右移的不触边条件及组块右侧边界各个方块的相对坐标值,下面我们把他们转换为列表数据。

2、建立列表数据

与左移边界数据不同的是,右移边界数据只有三类,其中<10的只有一个——17号组块,因此可以用条件判断的“如果,则”来处理;剩下两种类型的数据中,<12的组块数较少,因此将这些组块的编号保存到右移列表_小于12中,如图14-18所示,通过对列表的查询进行判断,其余<11的组块放在条件语句的“否则”分支中处理。

图14-18 右移列表_小于12

同样,我们需要为第二行数据建立一个三层的列表,来保存这些右侧边界禁区的坐标,如图14-19所示。

图14-19 右移禁区边界坐标列表

3、修改右移程序

右移程序与左移程序的思路完全相同,只是边界和禁区一个在左边,另一个在右边,因此对边界的判断一个用“≤”算符,另一个用“≥”算符;与列有关的操作(禁区判断及更新基准列)一个用减法,另一个用加法。具体代码如图14-20所示。

图14-20 右移程序

十一、快落按钮的点击事件处理程序

快落程序主要功能是:在条件允许时,为当前行的值+1,而核心的问题是+1的“条件”:当组块下边界距离底部边界或已经触底的块之间至少有一个空行时,才能允许快落。这与此前对组块触底和触块的判断有相似之处,只是触底和触块时,组块的下部边界与画布的底边或已触底块之间没有空行,因此,我们可以从已经触块过程中获得解决问题的思路。

1、数据整理

首先,鉴于所有类型组块的基准行都位于组块的底部,因此,所有组块快落的条件之一是基准行<15,即,相对于快落操作来说,当基准行≥15时,即被视为触底,将不能执行快落操作。其次,对于触块条件,我们可以利用触块坐标列表中的数据来进行判断,我们甚至可以对已经触块过程稍加改造,就可以成为快落触块过程,你想想看,我们需要做怎样的改造呢?

2、将已经触块过程改造为快落触块过程

其实很简单,我们为已经触块过程的求颜色码环节添加循环语句,将触块测试由原来的只测试下一行变为测试下两行。这样做还有一个潜在的好处,当你觉得快落操作的下落速度不够快时,你可以让每次点击按钮快落2行,这时只需要将循环的终止值变为“行+2”就可以了。具体代码如图14-21所示。

图14-21 将已经触块过程改造为快落触块过程

3、修改快落程序

有了快落触块过程,快落程序的修改变得非常简单,如图14-22所示。

图14-22 修改之后的快落程序

十二、旋转按钮的点击事件处理程序

这是最后一个需要修改的程序,也是最繁琐的一项任务,让我们静下心来,排除杂念,来完成这最艰巨的任务。

1、数据整理

我们在第11章中讨论过1、2号组块的旋转程序,通过设定旋转方向和旋转之后的位置,可以圈定旋转的禁区,并用相对坐标的方式来表示这些禁区,如图14-23所示。现在我们需要对除3号组块之外的其余16种组块进行旋转禁区的圈定。

图14-23 对1、2号组块旋转禁区的圈定

首先我们来分析黄色T形组块,从4号组块顺时针旋转为5号组块开始。

图14-24 对4~7号组块旋转禁区的圈定

在图14-24的左下角,黄色区域为4号组块,其基准行列的标记为实心红星;红线描边的是5号组块,其基准行列的标记为空心红星;图中黑色线条覆盖的区域为旋转禁区。如果禁区中有已经触底的块,或者这些区域处于画布边界之外,则不能进行旋转操作。我们用相对坐标值(相对于旋转前的组块)来表示这些禁区内的方块,这正是表14-8中第1行数据的内容,它们是:(-1,-1)、(-1,1)、(1,0)及(1,1)。

从图中很容易看到旋转前后组块的两个变化:①组块编号的变化:+1,这正是表14-8中第2行数据的内容;②基准行列位置的变化,基准行+1,基准列不变,我们用一对相对坐标值(相对于旋转之前的组块)来表示这种变化(1,0),这正是表14-8中第3行数据的内容。

用同样的方法分析其他几种组块,如图14-25所示,我们就得出了表14-8中的所有数据,这些数据是修改程序的基础。希望读者能够仔细核对每一种组块的数据,来体会这些数据的含义。

图14-25 对8~19号组块旋转禁区的圈定

表14-8 19种组块与旋转相关的数据

2、建立数据列表

首先建立旋转禁区坐标列表:在第11章创建的全局变量旋转禁区的基础上,为第一层列表添加17个列表项。注意,第3个列表项为空列表,因为3号组块不参与旋转操作。空列表起到占位的作用,否则无法实现组块编号与列表索引值之间的对应关系。如图14-26所示。

图14-26 19种组块的旋转禁区坐标列表

表14-8中第二行数据可以划分为三类:+1、-1、及-3,表示旋转之后组块编号的变化。大部分组块旋转之后组块编号+1,有少部分组块旋转之后组块编号-1或-3。我们用列表来保存-1和-3的组块编号,它们分别是2、9、11号组块和7、5、19号组块,列表名称为组块编号_1_3(由于变量的命名中不能使用减号,姑且用下划线姑且理解为来代替减号:-1、-3),如图14-27所示。通过对列表的检索,可以判断组块旋转后编号是否-1或-3,而对于那些既不-1也不-3的组块(一定是+1的组块),则放在条件语句的“否则”分支中加以处理。

图14-27 用列表保存旋转之后编号-1或-3的组块编号

最后根据表格中的第三行数据,创建旋转坐标改变列表,如图14-28所示,这是一个两层列表,第1层包含19个列表项,代表19种组块;第2层各包含2个列表项(组块3除外),代表某个组块经过旋转后,新组块的基准行、列位置相对于原组块基准行列位置的相对坐标值。

图14-28 旋转之后,新组块的基准行列相对于原组块基准行列的相对坐标值

3、修改触犯禁区过程

在第12章,我们为1、2号组块创建了旋转禁区列表,并创建了触犯禁区过程,并在旋转程序中调用该过程。在旋转过程中,对不同组块分别进行两项判断:首先判断旋转是否会导致组块移动到画布边界之外,如1号组块,判断其基准行是否>15;在确定旋转不会导致出界的前提下,再判断旋转禁区内是否有已经触底的块;如果既不出界、也不触犯禁区,则执行旋转操作。

现在我们要对触犯禁区过程加以改造,希望在该过程中实现以上两种判断,从而减少旋转程序中的代码量。

如图14-29所示,在改进后的过程中,利用组块编号从旋转禁区列表中获得当前组块的禁区坐标列表及列表长度,通过对禁区坐标列表的遍历,获得禁区内每个方块的相对行列值,经过运算将相对行列之转变为绝对行列值,并依此来做两项判断:①判断禁区方块是否出界,即行的值是否在1~16之间,列的值是否在1~12之间;②如果不出界,再判断禁区方块是否为已触底的块(颜色码≠0);如果组块的旋转既不出界,也不触块,则过程返回值为假,否则为真。

图14-29 修改之后的触犯禁区过程

4、修改旋转程序

如图14-30所示,旋转程序分为两个阶段:①判断旋转的组块是否触犯禁区(包括出界和触块);②如果不触犯禁区,则当组块编号≠3时,执行旋转操作。

旋转操作也分为两个部分:①设定旋转后新组块基准行、基准列的值;②设定旋转后的组块编号。这两项操作分别依赖于我们此前创建的数据列表:旋转坐标改变及组块编号_1_3。

在旋转程序中,我们还有可以改进的余地,比如程序最外层的两个条件语句是否可以调换顺序,也就是说,是否可以先判断组块编号≠3,再判断是否触犯禁区?这样的调换不会影响程序的执行结果,但会影响程序的执行效率。想想看,哪种顺序更合理?

图14-30 修改后的旋转程序

这里还有一个潜在的问题,可能发生在下列组块旋转时:1、4、8、10、15、19。这些组块在旋转之后基准行+1,而在触犯禁区过程中设定的触底条件是“行≤16”,因此,当这些组块的基准行=16时,旋转之后,基准行就会变为17,组块就会越出画布的下边界。这样的情况不是一定会发生,这与旋转的时机有关,如果旋转导致的基准行+1发生在计时程序的触底判断之后、组块下落之前,就会导致旋转后的组块出界。

同样,对于那些旋转之后基准行-1的块(2、7、9、11、14、18),则会发生相反的问题:当旋转导致的基准行-1发生在计时程序的触块判断之后、组块下落之前时,如果旋转前的组块已经触块,则旋转后的组块虽然还可以下落,也将被视为已经触块,致使组块悬在空中。

在对程序进行极限测试时,这些现象都出现过。所谓极限测试,就是飞快地、不停地点击旋转按钮。对于一个正常的游戏者而言,这样的行为是不多见的,因此错误出现的几率也很小。这算是程序中的bug,如果想排除前面的问题(旋转后基准行+1),可以将触底条件修正为“行≤15”,但这也会造成一些本来可以执行的旋转操作无法执行。对于基准行-1导致的bug,要排除很困难,至少现在我还没想出办法来。

作为一个实验性的游戏程序,至此我们已经完成了程序核心代码的编写,游戏已经具备了主要功能:控制组块移动、判断是否满行、消除满行并落下之上的色块、分数累计等等。但是如果想把游戏当做一个完备的产品发布,还需要做一些辅助功能,来提高程序的趣味性,在下一章,我们将为游戏添加以下功能:

  • 利用Android设备的触屏功能,用划屏手势来控制组块的移动;
  • 保存最高成绩记录,并在游戏结束时,提示玩家;
  • 为游戏添加退出功能;
  • 预报下一个出现的块;
  • 添加组块直落功能——长按快落按钮时,组块立刻一落到底。