第八章 随机生成组块

在前几章中,我们实现了对1、2号组块的控制:自由下落、左右移动、快速下落以及旋转,这些是游戏中最基本的操作,也是程序中最简单的部分。从本章开始,我们的程序将逐渐变得复杂起来,需要你全神贯注、一丝不苟地对待程序中的那些微小的变化和对细节的解释。

在开始新内容之前,我们来回顾一下已经存在的代码,如图8-1所示,我们声明了6个全局变量(橙色块),编写了6个事件处理程序(黄色块)以及6个自定义过程(紫色块),其中有两个通用过程(画块、擦除),还有四个针对不同组块的专用过程。

图8-1 现有程序中的全部代码

这里需要强调的是,我们的程序是有结构的。让我来打个比方,把程序与自然界中的生物体,比如人体做一个类比,那么全局变量相当于构成人体的基本元素,如碳氢氧氮等,通用过程相当于人体中的细胞(如上皮细胞、神经细胞等),专用过程相当于人体中的各种组织(如肌肉组织、上皮组织等),事件处理程序相当于人体的各种器官(如感觉器官、消化器官等),而整个程序就是一个完整的人体。这个比方虽然显得有些牵强,但我希望它能够帮助我们理解程序中各种要素之间的关系。在这些要素中,全局变量是程序中最基础的部分,在程序运行的全过程中,它们的值处于动态的变化之中,随时都有其他过程或程序来读取或改写它们;通用过程具有最基本的、最简单的功能,它们是构成程序的最坚实的砖块,当我们欣赏一个完工的作品时,往往忽视了它们的存在,如画块及擦除过程;专用过程通过调用通用过程来实现更为具体的、高级的功能,它们更贴近于我们将要实现的目标,如绘制红色水平组块、擦除红色水平组块等;事件处理程序通过调用专用过程来完成某项特定的任务,如控制组块的旋转及移动等,它们是距离用户最近的程序;而整个程序就像一个有机的整体,为用户提供了各项完整的功能,在与用户的交互过程中,实现了产品的价值。

从变量、通用过程、专用过程到事件处理程序,这是一种由简单到复杂、由低级到高级的构造过程,使程序呈现出一种结构。有结构的生物体是高级的,同样,有结构的程序也是高级的。程序的规模和复杂度越高,对结构的要求也越高。一个良好的结构是建造复杂软件的必要条件,因此我们在编程学习过程中,要关注并理解程序的结构,并在开发实践中有意识地使程序结构化。

本章将要对此前的程序进行两项改造:

  1. 在程序启动时,不再预设显示1号组块,而是让1、2号组块随机出现;
  2. 某个组块触底后,不再停止计时,而是随机生成新的组块,并从屏幕顶端开始下落。

第一节 用随机数生成组块

一、改造屏幕初始化程序

在游戏开始启动时,最先被触发的是“屏幕初始化事件”,在该事件处理程序中,我们绘制了灰色方阵。我们对程序的改造就从这里开始,共有三处需要改动:

  1. 去掉“画布1”的线宽设置:我们已经在设计视图中将线宽设为24,在整个程序运行过程中,线宽不变,始终为24;
  2. 将绘制灰色方阵的代码规整为一个独立的过程“绘制背景”:稍后我们将添加一个“重新开始”按钮,那时会用到这个独立的过程;
  3. 为组块编号赋值:自动生成一个随机整数,作为组块编号的初始值。
图8-2 对屏幕初始化事件处理程序的改造设想

前两个目标很容易实现,如图8-2所示,我们来考虑第3个目标。这里需要用到“取随机整数”的数学块。该块的默认输入项为1和100,由于目前只有两种组块,因此将100改为2,即“取1到2之间的随机整数”。这个块的运算结果可能是1,也可能是2,几率各占50%。改造的结果如图8-3所示。

图8-3 改造之后的屏幕初始化事件处理程序

二、添加重新开始功能

为了测试修改的结果,我们需要在用户界面上添加一个“重新开始”的按钮。切换到设计视图,拖一个按钮到水平布局1的中间位置,将显示文本及按钮名称改为“重新开始”,宽度设为“充满”,然后切换到编程视图,将屏幕初始化程序中的代码完整地复制到重新开始按钮点击事件处理程序(以下简称为重新开始程序)中,此外,设置全局变量的初始值(基准行、基准列、前一刻基准行、前一刻基准列、组块编号、前一刻组块编号等),并重新启动计时器(设置计时器的启用计时属性为真),如图8-4所示。你可以试想一下,如果不设置这些全局变量的初始值会出现什么情况?这些变量在初始化之前的值是多少?不设置计时器的启用属性又会出现什么情况?

图8-4 添加“重新开始”按钮,并编写事件处理程序
图8-5 左侧代码的测试结果

我们来做一下测试,看程序的运行效果如何。如果测试中恰好生成2号组块,当它刚刚露出一个方块时,点击左移或右移按钮,看看会出现什么结果。图8-5是我测试点击右移按钮的结果,直到2号组块完整地露出来后,右移操作才正常,即,当基准行<4时,2号组块的左移及右移程序是错误的。我们需要回过头来重新检查写过的代码,来修正这个错误。

对于画布上残留的组块痕迹,我们应该很自然地想到这是擦除操作的问题,而只有计时程序调用过擦除过程,因此我们应该在计时程序中查找错误。如图8-6所示,我们展开代码块,果然看到“擦除红色垂直组块”的调用条件是“基准行>4”,即,当2号组块完整地出现在屏幕上时,擦除组块操作才起作用。我们将条件语句去掉,再试,结果正常。

图8-6 代码中的遗留问题

接下来的问题是,对于1号组块,擦除操作也是有条件的,条件是“基准行>1”,那么会不会也在左右移动时出错呢?经过测试,果然当1号组块在第一行时,做左右移动操作,也会在屏幕上留下一个红色方块,这是条件语句惹的祸。

回想当初添加这个条件语句的情景,是考虑到当组块自由下落时,如果1号组块位于第一行(基准行=1)、2号组块小于等于第四行(基准行≤4)时,没有必要进行擦除组块的操作,也就是说,只有当1号组块的基准行≥2,而2号组块的基准行>4时,擦除操作才有效果。当时的程序还没有涉及到左右移动的操作。

上述情况会经常发生在软件开发的过程当中,尤其是对于那些设计不够充分的软件。不过,即便是“过度设计”的软件,在开发过程中依然会出现类似的问题。这里之所以给读者呈现出一个不够完美的开发过程,是想告诉初学者,真实的开发过程就是如此,不要因为过程中出现的错误而气馁。

下面我们来改写代码,如图8-7所示,将两个条件语句丢到垃圾桶中,代码变得更加简单。再进行测试,结果一切正常。

图8-7 去掉擦除组块的限定条件

第二节 改变组块的触底行为

虽然我们可以让程序“重新开始”,但是当组块触底时,由于计时器停止计时,因此程序也就停止运行了。我们希望当一个组块触底时,它就停在那里,此时随机生成另一个组块,让它出现在屏幕的顶端,并开始自由下落。因此我们需要对计时事件处理程序进行改造,即,对图8-7中的最后一组代码进行修改。

先给出具体的思路,当基准行>16时,我们要做如下操作:

  1. 随机生成一个组块编号;
  2. 设置新组块基准行的值为1(此前的值=17),基准列的值为7;
  3. 设置新组块的前一刻基准行值为0(此前的值=16)、前一刻基准列的值为7;
  4. 用步骤1中生成的组块编号在步骤2中设定的位置(基准行、基准列)绘制相应的组块。

在以上步骤中,如果忽略掉步骤3,程序的运行暂时不会出错,但会为后续的开发埋下祸根,即,程序会将已经触底的方块擦除掉(因为前一刻基准行=16)。因此这里将它们设置成(0,7),以防后患。

比较省力的办法是改造重新开始程序,如图8-8所示,我们创建一个新过程创建新组块,将重新开始程序中中间部分的代码移动到创建新组块中,并在重新开始程序中调用刚创建的创建新组块过程。

图8-8 定义一个过程——“创建新组块”,并在重新开始程序中调用它

此外,我们在屏幕初始化程序中,将生成随机数的语句替换成创建新组块,如图8-9所示。虽然我们在程序开始运行时,已经为相关的全局变量设置了初始值,但利用创建新组块这样的过程,将全局变量的赋值操作封装在一个过程里面,会让代码变得简洁而容易阅读。此外,创建新组块是我们在后续开发中要频繁执行的操作,因此将其定义为一个独立的过程,可以提高代码的复用性,为程序的整体结构提供有效的支持。

图8-9 最终“屏幕初始化”就只剩下两行代码

下一步将计时事件处理程序中的最后一行代码(让计时器停止运行)替换成创建新组块,然后开始测试我们的新程序。如图8-10及图8-11所示。

当一个组块触底后,计时器不再停止计时,而是在屏幕的顶端出现另一个组块,并开始自由下落;我们分别点击旋转、左移、右移按钮,来测试各部分程序是否存在错误,于是形成了图8-12中的画面。

到目前为止,我们的程序已经有了一些游戏的味道,随机生成组块、组块自由下落、组块在按钮的控制之下实现了左右移动、旋转及快落等等。这里需要特别强调的是“随机生成组块”,其中的“随机”方法,是几乎所有游戏软件中都会使用到的方法,比如棋牌类游戏中的发牌环节、对对碰游戏中图案的随机排列等等,利用生成随机数的数学工具,可以让游戏变换出很多的花样,这正是游戏吸引人的地方。

下面继续回到我们正在开发的游戏——俄罗斯方块,下一步该做什么呢?很显然,我们不能满足于组块触底后以这样的方式排列在屏幕上,我们要让它们一层一层地堆叠在一起,并且,当它们堆满一行时,清除掉这行块,让上面的块整体落下来,并增加得分。我们需要一步一步来,首先让这些块可以堆叠起来。堆叠起来的程序涉及到对列表变量的操作,在下一节中我们将介绍这种非常重要的数据类型,并在下一章实现组块的堆叠。

图8-10 计时器不再停止计时
图8-11 左侧代码的测试结果

第三节 列表及列表变量

一、三个重要概念:列表项、索引值、列表长度

列表是一种包含一个或多个元素的数据类型,这些元素可以是数字、字符、颜色、逻辑值,也可以是另一个列表,其中的每一个元素叫做一个列表项;所有列表元素在列表中进行有序的排列,某个元素在列表中的位置叫做索引值;列表中包含的元素总数叫做列表的长度。在书面上,通常用括号加逗号的方式来表示一个列表,如:

  • 数字列表:(4, 2, 9, 14, 1),列表长度为5,索引值是3的列表项为9,列表项1的索引值为5;
  • 文字列表:(香蕉, 苹果, 菠萝,梨),列表长度为4,索引值是1的列表项为‘香蕉’,列表项‘菠萝’的索引值为3;
  • 逻辑值列表:(真, 假, 假, 真, 真),列表长度为5,索引值是5的列表项为‘真’,列表项‘真’的索引值分别为1、4、5;
  • 列表的列表:((255,0,0), (0,255,0), (0,0,255), (255,0,255),(200,200,200)),列表长度为5,每个列表项都是一个列表,其中包含三个列表项,外层列表索引值为2的列表项为(0,255,0),(200,200,200)的索引值为5;
  • 混合列表:(12, 25, 红色, 真, (200,200,200)),列表长度为5,索引值为5的列表项为(200,200,200),列表项‘红色’的索引值为3,列表项12的索引值为1,列表项25的索引值为2,等等。

二、列表变量的声明及列表的创建

列表变量的声明:在App Inventor的编程视图中,像声明普通变量一样,可以声明一个列表变量,如图8-12所示。

图8-12 声明一个列表变量

列表的创建——有两种方式创建列表:

1) 静态创建:利用代码块“列表”直接创建一个列表,如图8-13所示。“列表”块是一个可扩展的代码块,理论上讲,可以添加无限数量的列表项,如图中的②;将需要的值填入“列表”块右侧的插槽中,即可完成创建列表的操作。在图8-13中,我们创建了一个有8个列表项的列表变量,每个列表项都是一种颜色;创建列表块的插槽也称为“输入项”,前面我们讲过,输入项可以是外挂的方式(如③),也可以是内嵌的方式式(如④)。

图8-13 静态创建列表变量

2) 动态创建:首先声明一个变量,并使用“空列表”块为变量赋值,如图8-14中的①所示;然后在程序的运行过程中,使用“向列表{}添加项{}”块,动态地向列表的末尾追加一个或多个列表项,如图8-14中的②所示,其中的列表项可能是另一个变量,它的值是在程序运行过程中动态生成的;也可以使用“在列表{}的第{}项处插入”块,向列表的指定位置插入一个列表项(插入一项后,原有列表中插入项之后的列表项索引值自动+1),如图8-14中的③所示。在下一章中,我们将会使用这种方法向列表中添加列表项。

图8-14动态创建列表变量

三、列表的操作

假设颜色列表如图8-15, 可以对列表进行以下操作:

图8-15 将对此列表进行操作

(1) 求列表长度,返回值为数字:

(2) 判断列表是否为空,返回值为‘真’或‘假’:

(3) 判断列表中是否包含某项,返回值为‘真’或‘假’:

(4) 随机选取列表项,返回某个列表项的值:

(5) 已知索引值,查询列表项的值:

(6) 已知列表项的值,求该列表项的索引值:

(7) 已知索引值,改写列表项的值:

(8) 删除列表项:被删除的列表项之后的所有列表项索引值自动-1。

以上操作为列表的常用操作。

小结

  1. 理解程序的结构;
  2. 随机生成组块;
  3. 将一组代码封装为一个独立的过程,供其他程序调用;
  4. 一个组块触底后,随机生成新的组块,并自屏幕顶端开始自由下落;
  5. 列表变量及列表的常规操作。