第九章 组块的堆叠

在第四章中,我们绘制了一个灰色的方阵作为整个游戏的背景,其实这段程序的功能还不止于此,它还可以用于创建一项很重要的数据——色块列表:一个包含了192(12×16)个列表项的列表,其中每个列表项的值都代表一种颜色。我们本章即将实现的组块堆叠功能,就要依赖于这个列表来完成。

图9-1灰色方阵列表

如图9-1所示,我们即将处理的列表可以书写成这样的格式: (灰,灰,...,灰,红,灰,......灰),列表项按照先逐列递增、再逐行递增的顺序排列,即,从第1行、第1列开始,然后是第1行、第2列,当第一行排列完成后,再从第2行、第1列继续向后排。例如图中第3行、第10列的红色方块,它在列表中的位置是2×12+10=34,即在列表中的索引值为34。我们用公式表示某行、某列方块在列表中的索引值:

索引值 =(行数-1) × 12 + 列数                

我们可以用数字来代替颜色,如用0来表示灰色,1来表示红色,2表示橙色,等等,这样图9-1对应的列表就可以表示为(0,0,...,1,0,......,0),其中列表项“1”的索引值为34。

第一节 创建色块列表

在上一章的第三节我们介绍了列表变量的声明及列表的创建,现在我们把这些知识运用到程序中。如图9-2所示,在上一章我们创建了绘制背景过程,用来绘制灰色方阵,我们就用这个过程来创建色块列表。

首先声明全局变量色块列表,设初始值为空列表。在绘制背景过程的内层循环中,在“画线”语句块之后,为色块列表添加列表项。循环共执行192次,因此列表的长度为192。

图9-2 为色块列表添加列表项

我们可以想象一下整个色块列表的内容:(0,0,......,0),共192项。在程序刚刚开始运行时,即,在屏幕初始化时,我们为色块列表添加了列表项,而这个列表将在程序的运行过程中被不断地修改,直到游戏结束。

从现在开始,我们对程序的思考将沿着两条线索推进:一条线索是对画布的更新,包括背景的绘制、组块的下落、消除填满行后的画布重绘等等;另一条线索是数据(变量)的更新,即对全局变量的修改(除了现有的基准行、基准列、组块编号、色块列表等全局变量外,还包括未来的游戏得分)。换句话说,接下来我们创建的过程,要么用于画布的更新,要么用于数据的更新,我们不会在一个过程里,同是对画布和数据进行操作。这是游戏编程的惯例,也是必须遵循的原则,在后续的开发中,你将体会到这一原则带来的好处。

为了将画面处理与变量更新分开处理,我们对“绘制背景”过程进行切割,将其中对列表操作的部分独立出来,单独创建一个初始化色块列表过程,同时,将绘制背景过程恢复成原来的样子,如图9-3所示。

图9-3 将对列表变量的操作从绘制背景过程中分离出来

然后对重新开始及屏幕初始化程序作出相应的修改,如图9-4所示。

图9-4 修改重新开始程序及屏幕初始化程序

我们可以回顾一下创建新组块过程,如图9-5所示,其中的所有代码变量都是对数据的处理(全局变量的初始化),没有任何对画布的操作,这也符合画布与数据操作分离的原则。

图9-5 创建新组块过程中没有对画布的操作

第二节 修改色块列表

当第一个组块触底时,组块覆盖了某些灰色方块,覆盖的区域与三个变量有关:组块编号、基准行及基准列。我们以1号组块为例,当基准行=16时,组块触底,假设此时基准列=7,则1号组块覆盖的区域为第16行的第5~8列,这里我们创建一个过程,来计算它们在列表中的索引值,如图9-6所示。

图9-6 根据行、列值计算方块在色块列表中的索引值

通过调用求索引值过程,求得被触底组块覆盖区域的方块在色块列表中的索引值,然后对列表项进行修改,将列表项的值从0(对应于灰色)修改为1(对应于红色)。如图9-7所示。

图9-7 修改列表项的值

基于对具体组块及其所在的行、列值,我们实现了对特定列表项的修改,下面我们沿着这个思路,将代码通用化,即,对任意类型的组块、任意的基准行、基准列,来修改对应的列表项。

对1号组块而言,其所在行的值是唯一的,等于其基准行,而所在列的值分别为:

(基准列-2)、(基准列-1)、基准列、(基准列+1)

对2号组块而言,其所在列的值是唯一的,等于其基准列,所在行的值分别为:

(基准行-3)、(基准行-2)、(基准行-1)、基准行

根据以上分析,我们创建一个“更新色块列表”过程,来实现对列表项的修改。思路是这样的:已知组块编号及其所在的行、列值,通过调用“求索引值”求得组块中各个方块所在位置在色块列表中对应的索引值,并依据索引值对列表项的值做出修改。具体代码如图9-8所示。

图9-8 用循环语句实现对色块列表中列表项的修改

这里我们使用了循环语句,通过对循环变量的设置,可以遍历到组块中的每个方块,对其求索引值,并最终修改列表项的值。

在我们创建的过程里,使用了参数终点行、终点列,它们与基准行、基准列之间的关系,在后面会涉及到。如果这时对程序进行测试,我们看不到任何的变化,因为还没有任何程序来调用新创建的过程。下面我们来研究列表项的修改给整个程序进程带来的影响。

第三节 组块停止下落的条件

此前我们说当组块触底时,即,基准行=16时,组块将停止下落,但从现在起,我们要修正组块停止下落的条件:当组块落到画布的最后一行,或落到已经触底的组块上时,将停止下落,我们称前者为触底,后者为触块。

按照这样的描述,我们会想到,当一个组块下落时,应该随时询问两件事情:

  1. 组块的基准行是否大于16;
  2. 组块中每个方块的正下方是否堆叠了已触底的组块。

第一个问题已经在前几章中得到解决,这里主要来解决第二个问题。

我们把第二个问题做一个翻译,用列表的语言来描述它。以1号组块为例,假设这一时刻1号组块的基准行为m,基准列为n,那么下一时刻基准行为m+1,基准列仍然为n,由此我们可以确定下一时刻这四个方块的位置:所在的行为m+1,所在的列分别为n-2、n-1、n以及n+1,我们的目标是求出这四个方块在色块列表中对应列表项的索引值,并依据索引值判断列表项的值(颜色码)是否为0。如果全部为0,则可以继续下落,否则,哪怕只有一个颜色码不为0,则停止下落。

第一步,我们先来定义一个求颜色码的过程,如图9-9所示。在已知行、列值的情况下,通过调用求索引值过程,来求指定行、列方块的颜色码。

图9-9 定义求颜色码过程

你可能已经注意到,求颜色码、求索引值这两个过程与此前定义的过程有所不同,如图9-10所示,此前我们定义的过程是功能型的,如“擦除红色水平组块”,它可以执行某些操作,或改变某些全局变量的值,这种类型的过程叫做无返回值过程;而求索引值及求颜色码这两个过程是计算型的,它会得出一个结果,这个结果被称为返回值,这样的过程被称为有返回值过程。

图9-10 两种不同类型的过程:有返回值与无返回值

调用这两类过程的方法也不相同,有返回值过程只能充当其他代码块的输入项(插头在左侧),而无返回值的过程可以作为一个独立的语句块来使用(凹槽在左上角),如图9-10所示。

有了求颜色码过程,我们可以获得画布上任意行列方块的颜色码。对于正在下落的组块来说,为了判断其是否触底,我们需要求它下一行方块的颜色码。我们先针对1号组块,来判断它下一时刻即将落入的位置是否都是灰色方块。如图9-11所示,我们对1号组块中的四个方块做循环,求每个方块正下方位置的颜色码,如果求颜色码的4个返回值都为0,则可以继续下落,否则,只要有一个返回值≠0,则停止下落。

图9-11 判断正在下落的1号组块是否可以继续下落

停止下落之后要做两件事:①更新色块列表,②生成新的组块,代码如图9-12所示。这里添加了一个“设 列序号为 100”的语句,目的是为了终止循环:在对四个方块逐个进行判断时,一旦某个方块的颜色码不为零,就没有必要再对后面的方块进行判断,因此设循环变量“列序号=100”,100远远大于循环变量的终止值1,因此循环被终止运行。(其实循环并没有终止,详见本章第六节:测试循环语句的中断。)

图9-12 对组块触块的判断

相比之下,对2号组块的判断要简单一些,因为2号组块只有一列,只要对基准行的下一行“求颜色码”即可,如图9-13所示。

图9-13 判断正在下落的2号组块是否可以继续下落

需要注意的是,无论是哪个类型的组块,在求颜色码及更新色块列表过程中使用的都是前一刻基准行这个参数,这是因为在组块下落之后,在求颜色码之前,对全局变量进行了更新,其中“设 基准行 为 基准行+1”语句是为下一次计时事件而设置的,在做触底判断的时刻,组块所在的行等于前一刻基准行,如图9-14所示。

图9-14 在组块下落之后,为下一次计时事件更新了全局变量

我们已经实现了对组块触块条件的判断,现在需要与触底条件——“基准行>16”进行整合,综合考虑可能会出现的各种情况。

仍然以1号组块为例,分为两种情况:

  1. 当基准行>16时,组块已经自然下落到第16行,并执行了“设 基准行 为 基准行+1”语句,使得基准行=17。由于“快落”事件处理程序中设置的快落条件是“基准行<15”,如图9-15所示,因此排除了其他可能的情况。在这种情况下,我们希望组块就停留在当前位置,然后更新色块列表,并创建新组块。
    图9-15 为快落事件设置了执行条件
  2. 当基准行≤16时,此时要依据组块的实际位置来判断是否允许组块继续下落。如果可以继续下落,则直接进入下一次计时事件;如果不能,则更新色块列表,并创建新组块,再进入下一次计时事件。

以上两种情况不可能同时存在,是一种“非此即彼”的关系,因此在程序上要分开处理;举例来说,当组块落到第16行时,如果去判断第17行的色块列表项时,程序就会出错。

同样,对于2号组块,也存在这两种情况,也必须分别加以处理。图9-16中显示了程序的修改结果。为了截图的方便,很多代码采用了内嵌输入项的显示方式。

图9-16 停止下落的情况分为两种:触底及触块

下面进入测试环节,一切进展顺利,但当组块越垒越高,到达画布顶端时,测试手机和浏览器的编程视图中反复弹出错误提示,如图9-17所示。这两者提示信息基本相同,其中的数字略有变化。我们要学会查看错误信息,并从中找出错误的原因。错误提示为:试图替换列表(...)中的第-5 (及-17/-29)项,而最小的有效值为1。

图9-17A 开发工具的编程视图中弹出的错误提示
图9-17B 手机的提示

我们根据以上提示,判断错误与替换列表项有关,程序中涉及此项操作的只有更新色块列表过程,在图9-17B的最右侧的图中,屏幕的标题显示“2”,这是我们为确定出错组块而添加的一个测试语句的结果,它意味着2号组块导致了程序出错,如图9-18所示。当2号组块出现在画布顶端时,程序已经完成了组块的绘制,并完成了全局变量的更新,因此,此时基准行=2,基准列=7,前一刻基准行=1,前一刻基准列=7。由于组块“得知”无法再下落,因此需要更新色块列表,在调用“更新色块列表”过程时,使用的参数是2号组的前一刻基准行及前一刻基准列,即,参数终点行=1,终点列=7,如图9-18所示;更新色块列表过程在执行循环语句时,调用求索引值过程,首次循环时,求索引值的参数为行=-2(=1-3),列=7,因此索引值为(-2-1)×12+7 = -29,同理,第二次循环时,索引值为(-1-1)×12+7 = -17,第三次为(0-1)×12+7 = -5,这正是手机错误提示中显示的数字。

图9-18 在调用更新色块列表时,求索引值过程的返回值出现负值

问题的出现是必然的,这也正是我们下一步将要处理的问题:组块触顶。

第四节 组块触顶判断

对于正在下落的组块,不必关心它是否触顶,只有在组块以第二种方式停止下落,即触块时,才需要判断它是否同时触顶。如果没有触顶,则更新色块列表,并创建新组块;如果触顶,则游戏结束。

我们先将编程环境切换到设计视图,为游戏添加一个“对话框”组件。对话框组件属于非可视组件,但仍被列在组件的“用户界面”分组中,位列第8。将“对话框”组件拖出预览窗口中,它将自动落入预览窗口下方的非可视组件陈列区。保留其属性值的默认设置。如图9-19所示。当已经触底的组块同时触顶时,我们用“对话框”组件来显示“游戏结束”的提示。

图9-19 为游戏添加对话框组件

在组块已经触块的情况下,判断触顶的条件只有一条:组块最上方的方块是否处在第一行。对1号组块而言,条件为“前一刻基准行=1”;对2号组块而言,条件为“前一刻当前行≤4”。最终的程序及测试结果如图9-20所示。当满足触顶条件时,要做两件事:①让计时器停止计时;②弹出对话框,通知用户游戏结束。

图9-20A 对1号组块的触顶判断
图9-20B 对2号组块的触顶判断
图9-20C 触顶判断的执行效果

本章解决了俄罗斯方块游戏中的两大难点之一——组块下落过程中对触块情况的判断,另一个难点是如何在组块填满一行或多行后,消除这些组块,并让上方的组块整体落下来。我们将在下一章里挑战第二个难点。在此之前,我们结合前面学过的内容,来学习一种编程中的常用工具——程序流程图。

第五节 编程基础——流程图

一、常用的图形符号

图9-21所示的是几种常用的流程图符号:

  • 处理框:描述某个处理过程;
  • 判断框:用于表述一个判断条件;
  • 起止框:一段程序的开始及结束;
  • 流程线:表示程序的流转方向,对于无箭头的线,默认的方向是自上而下或自左向右;
  • 连接点:将较大的流程图分割成若干部分,各部分之间用连接点连接;
  • 标注文字:配合判断框使用,标明判断的结果。
图9-21 流程图的常用基本符号

图中的符号并不是全部的流程图符号,感兴趣的读者可以找到此类的书籍做扩展阅读。

二、流程图的结构

从程序的流转顺序上划分,流程图有三种常用结构,如图9-22所示:

  • 顺序流程:自上而下、从左向右绘制,用于描述按顺序执行的一系列处理过程;
  • 选择结构:依据不同的判断结果(真或假),程序会转向不同的分支;
  • 循环结构:当条件满足时,重复执行一段程序;
图9-22 流程图的三种结构

从流程图对细节的描述上划分,可以有多种层次:

  • 主流程图:用于描述程序的主干流程,忽略细节;
  • 局部流程图:描述某一部分程序的运行流程,适当地描述细节。

三、在不同尺度上描述程序的流程

我们可以把流程图想象为月球的表面,当我们远距离观看它时,它只有一个大致的轮廓;当我们乘着飞船越来越靠近它时,看到的是越来越多的细节。有时我们需要轮廓图,即主流程图,以便看清楚程序的主线,这在最初设计程序时是非常必要的,它就像我们人体的脊柱一样,是整个程序的支撑。随着程序开发的不断深入,开始涉及到一些非常具体的逻辑问题,这时就需要用细节图,即局部流程图来缕清思路,避免出现逻辑上的疏漏。

在图9-23A是“计时事件处理程序”的主流程图,简要地描绘了处理程序的几个关键步骤,让我们清楚地知道,在计时事件发生时,都执行了哪些操作。

图9-23A 计时程序的主流程图
图9-23B 左图中环节①的局部流程图

在图9-23B是主流程图中环节①,是针对画面更新,即组块下落操作所绘制的局部流程图,流程图中的每一个步骤都能与代码块一一对应,这就是对流程细节的描述。图9-23C是计时程序中与组块1相关的代码图,我们可以将流程图与代码进行对照,看如何用代码来具体实现流程图中设定的操作。

图23C 计时程序中与1号组块相关的代码

随着俄罗斯方块游戏开发的深入,将面临越来越复杂的逻辑问题,有些时候,我们需要借助流程图来描述这些复杂的问题,以避免可能产生的带来的疏漏。
第六节 测试循环语句的终止

在第三节中,我们试图人为地增大循环变量,使循环变量超出其取值范围,来实现跳出循环语句的目的。在常规编程语言中,这种方法是有效的,但在App Inventor中,这样的方法并没有奏效。为了证明这一点,我们来创建一个新项目,证明以上的结论,如图9-24所示。

图9-24 在App Inventor中无法终止循环语:测试项目的用户界面
图9-24 在App Inventor中无法终止循环语句:测试程序
图9-24 在App Inventor中无法终止循环语句:测试结果

在这个项目中,设定了一个题目,求1~100之间整数的和,大家都知道这个结果是5050。在项目中添加了一个复选框,如果复选框被选中,当循环变量数=10时,让循环变量数=1000,以此令循环终止。然而在程序执行过程中,无论复选框是否被选中,结果都是5050,显然,人为设置循环变量的操作并没有起到预期的效果——循环并未终止。