第七章 组块的旋转

第一节 对旋转的定义

在游戏设计中,对旋转的定义涉及到以下三个关键点:

  1. 旋转方向的规定(顺时针或逆时针),这涉及到旋转之后要呈现的新组块的类型;
  2. 新组块基准行的偏移量,这影响游戏用户的体验,即旋转动作是否平滑,无跳跃感;
  3. 新组块基准列的偏移量,同样涉及到用户的体验,即旋转动作是否有左右的漂移感。

对与条形组块来说,旋转方向对旋转后的组块类型没有影响,因为条形组块的变化只有水平和垂直两种。对于水平条形组块来说,我们假设定旋转方向为顺时针,并假设在旋转之后,新组块(垂直条形阻块)的基准列值不变(基准列的偏移量为零);而基准行的值加1(基准行的偏移量为+1)。同样,对于垂直组块,我们假设旋转的方向为逆时针,旋转之后,基准行的值减1(基准行的偏移量为-1),基准列的值保持不变(基准列的偏移量为零)。如图7-1所示。

图7-1 对组块旋转的描述

一旦由水平组块变为垂直组块,此前编写的某些事件处理程序需要做相应的改变:

  1. 计时事件处理程序:
    • “调用绘制红色水平组块”替换为“调用绘制红色垂直组块”;
    • “调用擦除红色水平组块”替换为“调用擦除红色垂直组块”;
    • “擦除”的条件改为“基准行>4”;
  2. 左移按钮点击事件处理程序:左移的条件改为“基准列>1”;
  3. 右移按钮点击事件处理程序:右移的条件改为“基准列<12”;

另外两个事件处理程序(屏幕初始化及快落按钮点击事件)的代码暂时不需要修改。在下一个旋转事件发生之前,程序将一直针对垂直组块进行操作。

此时我们对程序的设计有了新的认识:在某一时刻,我们只能操作一种组块,无论是绘制、擦除、左右移动或旋转,都与这个组块的类型有关。因此,某些事件处理程序需要随时了解当前的组块类型,以便对发生的事件做出正确的处理。这要求我们必须声明一个全局变量,来记录当前时刻的组块类型;每当发生旋转事件,组块类型将发生改变,这个全局变量也会随之而改变,以此保证程序的正确运行。在图7-2中,显示了游戏中的所有可能出现的组块,共7种类型,而每种组块还有1至4个不等的变形(旋转之后的形状)。

图7-2 游戏中的所有组块类型

我们需要为所有的可能形态(形状和方向)设置一个编号,如表7-1。表中的第一行是组块可能处于的19种形态,第二行是与组块形态对应的组块编号。

表7-1 游戏中所有形态的组块及其编号

在此后的各个章节中,我们将引入全局变量组块编号,来处理不同类型组块的广顺南大街南口相关程序。

第二节 旋转事件处理程序

在编写旋转事件处理程序(以下简称旋转程序)之前,我们来整理一下此前编写的代码,如图7-3所示,这些代码包括:

  1. 四个全局变量:
    • 基准行、基准列:用来记录当前时刻组块的位置,用于组块的绘制;
    • 前一刻基准行、前一刻基准列:用来记录前一时刻组块位置,用于擦除操作;
  2. 在应用启动时执行的两个事件处理程序:
    • 屏幕初始化事件:绘制灰色方阵,作为整个游戏的背景;
    • 计时事件:生成1号组块,并让其自由下落;
  3. 四个全局变量:
    • 基准行、基准列:用来记录当前时刻组块的位置,用于组块的绘制;
    • 前一刻基准行、前一刻基准列:用来记录前一时刻组块位置,用于擦除操作;
  4. 两个通用的自定义过程:
    • 画块:用于在指定的行、列绘制任意颜色的方块;
    • 擦除:用于在指定行、列绘制浅灰色方块;
  5. 三个按钮点击事件处理程序:
    • 左移事件:1号组块在满足条件时向左移动;
    • 右移事件:1号组块在满足条件时向右移动;
    • 快落事件:1号组块在满足条件时,每个计时间组块多下落1行;
  6. 两个1号组块的专用过程:
    • 绘制红色水平组块:根据基准行、基准列的值绘制1号组块;
    • 擦除红色水平组块:根据前一刻基准行、前一刻基准列的值擦除1号组块;
  7. 两个2号组块的专用过程:
    • 绘制红色垂直组块:根据基准行、基准列的值绘制2号组块;
    • 擦除红色垂直组块:根据前一刻基准行、前一刻基准列的值擦除2号组块。
  8. 三个按钮点击事件处理程序:
    • 左移事件:1号组块在满足条件时向左移动;
    • 右移事件:1号组块在满足条件时向右移动;
    • 快落事件:1号组块在满足条件时,每个计时间组块多下落1行;
图7-3 此前编写的代码

图7-3中显示的是折叠的代码块,折叠的方法将在本章第三节中讲解。

在上述代码中,序号为12b、46a、46b及46c四个事件处理程序,都是针对1号组块而编写的。当发生旋转事件时,组块类型发生变化,因此与组块类型相关的代码必须加以修改。我们先来编写旋转事件处理程序,然后再对上述程序进行逐一修改。

首先我们要声明一个全局变量“组块编号”,来保存当前时刻正在屏幕上自由下落的组块类型,所有的事件处理程序都将依据这个变量来确定具体执行哪一段程序。

设“组块编号”初始值为1,即,程序启动时让屏幕上出现1号组块。当旋转事件发生时,首先判断当前时刻的“组块编号”值,并根据当前值来修改“组块编号”值,并对旋转之后新组块的基准行进行修正,如图7-4所示。

图7-4 旋转按钮点击事件处理程序

在旋转事件处理程序中,我们为条件语句添加了一个“否则”的分支:当满足“组块编号=1”的条件时,执行“则”之后的代码;当不满足条件时,执行“否则”之后的代码。

以上完成了对组块编号及基准行的修改,即完成了旋转事件处理程序的编写,但要实现真正的旋转,还要完成对四个事件处理程序(12b、46a、46b及46c)的修改。

第三节 修改与组块编号相关的程序

一、修改计时程序

如图7-5所示,为计时程序增加一个条件语句,来判断当前时刻的组块编号,并依据组块编号来决定程序的走向。

图7-5 为计时事件处理程序添加条件判断语句

我们把原来1号组块的代码完整地复制一份,添加到“否则”分支中,然后逐句进行修改,修改的结果如图7-6中的右图所示。

图7-6 复制1号组块的代码,稍加修改就可实现对2号组块的操作

我们还可以进一步简化代码,将相同部分的代码提取到条件语句之外,将多余的代码丢到垃圾桶中。如图7-7所示。

图7-7 简化后的计时事件处理程序

我们来测试一下修改过的代码,测试的结果如图7-8所示,当1号组块旋转为2号组块时,1号组块并没有被完整擦除;同样,当2号组块旋转为1号组块时,屏幕上依然残留着一部分2号组块。这不是我们想要的结果,我们需要对代码进行检查,找出问题的所在。可以断定的是,问题与组块的擦除操作有关。

图7-8 对左侧代码的测试
图7-9 分析错误原因:时刻2程序不知道时刻1遗留的是1号组块

如图7-9所示,时刻1发生了旋转事件,组块编号从1变为2,而屏幕上遗留的是1号组块;时刻2,由于组块编号等于2,因此将执行计时事件中的“否则”分支,即,先擦除2号组块,再绘制新的2号组块。这样做的结果,就是屏幕上的待擦除的1号组块仅仅被擦掉了与2号组块重叠的那个方块,而其余三个方块就留在了屏幕上。同样的事情也发生在由2号组块旋转为1号组块的过程中。也就是说,当前时刻,程序并不知道前一时刻的组块编号,因此擦除操作才出现错误。解决的办法是声明一个全局变量前一刻组块编号,并在擦除操作前进行判断,根据前一刻组块编号,来决定调用哪一个擦除过程。修改后的代码如图7-10所示。

图7-10 为擦除命令添加限定条件

在计时事件处理程序中,每次擦除及绘制组块的操作完成之后,设“前一刻组块编号=组块编号”,在不发生旋转事件的情况下,程序执行“则”分支中的代码(修改前的代码);当发生旋转事件时,由于“前一刻组块编号=组件编号”的条件不满足,因此程序执行“否则”分支中的代码。经过再次测试,程序的运行正常,屏幕上不再有残留的组块。

二、修改左右移动程序

如图7-11所示,为两个事件处理程序添加条件判断语句,依据当前时刻的组块编号,来决定左右移动的前提条件。

图7-11 修改左右移动程序

三、修改快落程序 如图7-12所示,由于两种组块的基准行都在组块的最下方,它们执行快落操作的限定条件是相同的,因此也就无需修改快落事件处理程序。

图7-12不必修改快落事件处理程序

四、对程序设计的一点思考

不必修改快落程序,从中透射出一个程序设计的技巧:游戏中每一种组块的基准行、基准列的位置都是人为设置的,由于我们事先约定所有类型组块的基准行都位于组块的底部,因此,快落程序执行的条件(基准行<15)与组块编号无关,因此当组块编号变化时,无需修改快落程序,这大大地减少了代码的修改量。

这种设置的好处还不止于此,在计时程序中,最后一段代码用于判断组块是否触底,由于所有组块的基准行都在组块底部,因此,所有组块的触底条件时一致的:基准行>16,这样,这部分代码也不必因组块编号的变化而改变,同样减少了代码的修改量。

通过人为设定某些规则,从而减少代码的编写量,这也是程序设计的一部分,这部分工作应该在编码前完成,而且良好的设计可以起到事半功倍的效果,不仅可以减少代码量,也有助于构造出坚固的程序(不易出错的程序)。

你可以思考一下,是否可以将组块的基准列设置为组块最左侧或最右侧的列,从而减少因组块编号的变化而带来的代码修改量呢?这里我们尝试将1号组块中最右边的方块设置为基准列,并与现在基准列设置进行比较,看看哪种策略代码的修改量更小。如图7-13所示。

图7-13 假设1号组块的基准列位于最右侧,要修改左移程序及旋转程序适应组块编号的改变

改变设置的影响其实还不止于此,为了保证1、2号组块在屏幕上的初始位置位于画布的中央,还需要修改组块基准列的初始值,如图7-14所示。这样使得不同组块基准列的初始值不统一,而且当我们今后将代码扩展到19种组块时,这样的设置会带来更多的麻烦。

图7-14 改变1号组块基准列的位置,将导致不同组块基准列初始值的不统一

通过以上对比,可以粗略地得出结论:如果1号组块的基准列选在最右侧方块上,并不能明显地减少代码的修改量,反而会带来全局变量(基准列)初始值的改动,因此,这样的选择并没有优势。衡量的结果是,我们依然选择1号组块的右数第2个方块作为基准列的位置。

在软件设计过程中,经常会遇到类似的多重选择,有时我们不必急于判断某一个方案的优劣,因为对某个方案的评价可能要贯穿于整个软件的开发过程。通常我们会有一种感觉,觉得哪种方案更好,那么就坚持做下去,所谓“条条大路通罗马”,或“殊途同归”,说的就是不同的路径最终都会到达同一个终点。能够到达终点是最重要的,因此不必拘泥于暂时的优劣与得失。即便在软件开发完成后,也可以回过头来审视设计的优劣,并加以修正。

第四节 逐步了解开发工具

一、代码块的折叠、展开与排列

当我们编写一个规模较大的程序时,编程视图的操作区内会有很多代码块,将它们折叠起来,可以节省显示空间,并有助于对代码的管理和查阅。

可以对一个代码块进行整体的折叠与展开,如图7-15所示,用鼠标右键点击最外层代码块,将弹出快捷菜单,选择菜单中的第三项“折叠代码块”,即可将其折叠成一行。同样,对于已经折叠起来的代码块点击右键,选择菜单第二项“展开代码块”,即可将一行代码展开成原来的样子。

图7-15 代码块的整体折叠与展开

也可以对局部的代码进行折叠与展开,如图7-16所示,这样做的好处是,我们可以忽略代码中的细节,而从整体上对代码有更好的把握。

图7-16 局部代码块的折叠与展开

此外,还可以对所有代码进行一次性的折叠与展开,对所有代码块进行统一的排列:横向排列、纵向排列或按类别排列。如图7-17所示。方法是在操作区的空白处点击鼠标右键,就会弹出图中的菜单。当程序中代码块过多时,这些功能有助于我们对代码的管理和查阅。

图7-17 操作所有代码块

二、输入项的两种显示方式:外挂与内嵌

1、输入项:在App Inventor中,有许多带有空缺的块,如图7-18中的循环语句、加法块及创建列表块等,在使用这些块时,要用数字、文字或变量等来填充这些空缺。这些空缺就是“输入项”。

图7-18 带有空缺的块,这些空缺被称为“输入项”

2、外挂与内嵌:如图7-19所示,输入项可以有两种显示方式,内嵌式与外挂式。其中内嵌式将输入项沿水平方向排列,并嵌入在代码块的内部;外挂式则让输入项在垂直方向排列,并呈现为若干个开放的插槽。这两种排列方式只是为了显示的方便,比如有些内嵌式的块有多个输入项,在屏幕上显得过宽,不利于代码的查看及维护,这是就可以将其修改为外挂式,将输入项纵向排列起来。

图7-19 输入项的两种显示方式:内嵌式与外挂式

3、外挂与内嵌的转换:如图7-20,是我们程序中的代码,循环语句的三个内嵌式的输入项呈水平排列,用鼠标右键点击该块,将弹出快捷菜单,选择第三项“外挂输入项”即可将显示方式切换成外挂式,反之亦然。

图7-20 输入项显示方式的转换:内嵌式↔外挂式

三、禁用与删除代码块

在图7-15、7-16、7-20中都显示了右键菜单,其中都有“禁用代码块”“删除代码块”与“禁用代码块”“删除代码块”的条目,前者可以将选中的代码块删除,后者会让选中的代码块变成灰白色,并处于禁用状态,即,当程序运行到禁用代码块时,将跳过这段代码,继续执行此后的代码。禁用功能可以帮助我们调试程序,比如,当我们在调试程序过程中遇到了错误,但不确定错误是由哪一行代码产生的,我们就可以对相关代码进行禁用,用排除法找出错误代码。

小结

本章实现了条形组块的旋转,从内容上涵盖了从游戏的设计到实现的全部过程,对于我们理解软件开发的过程具有非常重要的意义。软件开发是一个创造性的过程,程序员也是创造者。本章具体内容罗列如下:

  1. 对旋转的定义:这个定义会直接影响后面程序的编写,如果定义合理,将显著减少程序的复杂度;
  2. 旋转事件处理程序:与左右移动事件的处理程序一样,在这里只修改相关的全局变量,而不参与组块的绘制与擦除;
  3. 旋转的实现:通过对全局变量(组块编号、前一刻组块编号)的引用,实现了对计时事件处理程序、左右移动事件成立程序的改造;
  4. 代码块的常规操作:学会使用App Inventor中对代码块的操作,便于我们对代码的管理及查阅,提高工作效率。