第十二章 计分及受控移动改进

我们此前编写的代码,都是以1、2号组块为对象,本章还将延续这样的做法。这是一种常用的编写软件思路,即,用少量简单的对象实现程序的核心功能,解决程序中遇到的各种难题,找到解决问题的最优方案,并将其扩展到更多的对象上。这样做的好处是可以降低问题的难度,排除一些次要因素的干扰,集中精力解决难题。一旦我们在个别对象上取得了解决问题的经验与方法,就可以轻松地将其复制到更多对象上。

第一节 计分

计分从表面看是个算数问题,但其实它包含了游戏设计者对游戏的趣味性与难度的理解。在俄罗斯方块游戏中,每次消除填满的行时,都会增加游戏者的得分;为了奖励一次性消除多行,还要设计得分的梯度,即,每多消除一行,都要有额外的奖励,具体得分设计如下:

  • 一次性消除一行得10分;
  • 一次性消除2行,第一行得10分,第二行得20分;
  • 一次性消除3行,第一行得10分,第二行得20分,第三行得30分;
  • 一次性消除4行,第一行得10分,第二行得20分,第三行得30分,第四行的40分。 如果用公式来表示一次性消除4行的得分,就是:

    得分 = 1×10 + 2×10 + 3×10 + 4×10

你能想到如何用代码来实现这个计算吗? 没错,就是用循环语句。我们定义一个带参数并且有返回值的过程,如图12-1所示。输入一次性消除的行数,返回本次消除行的得分。

图12-1 计算消除行的得分

为了全程累计游戏得分,我们声明一个全局变量“总分”,并在每次消除行时,向“总分”中增加本次得分。在计时事件处理程序中做累计得分的操作,如图12-2所示。

图12-2 计算累计得分

我们利用屏幕的标题来显示总分,经过在手机上的测试,得出的结果是,当一次性消除4行时,累计得分为0。哪里出了问题呢?回头看代码:累计得分的操作在“消除行”后面,此时,“色块列表”中的已经填满的行数已经为0,因此无从计算分数,修改代码顺序,将累计得分操作放在“消除行”之前,如图12-3所示,再测试,一次填满4行时,得分为100,测试通过,继续测试填满一样,分数累计结果也正确。

图12-3 调整代码顺序

我们还可以对代码做进一步优化,修改了原有的局部变量(由组块覆盖的行改为填满行列表),并添加了新的局部变量(填满行数)设置一个局部变量填满行列表长度,来减少对列表长度计算的次数,(将原来的两次计算减少为一次计算),如图12-4所示。这样的修改有两三点好处,第一,提高程序的执行效率,第二,提高程序的坚固程度,第三,提高了代码的可读性。第二点尤其重要,此前我们已经提到过,这里再次强调:我们经常会犯这样的错误,假设程序中存在若干处相同的代码,当我们要修改这些代码时,难免会漏掉其中的一部分,这使得整个程序变得脆弱,甚至会漏洞百出,不堪一击。这是我们非常不愿意看到的局面。

图12-4 优化之后的代码

最后不要忘了一件重要的事情,就是在游戏重新开始时,重新将总分设为0,如图12-5所示。

图12-5 重新开始游戏时,设总分为零

第二节 受控移动的代码改进

如果你随着本开发笔记的进度一边学习、一边实践的话,在测试过程中你会发现一些不合逻辑的现象,有些块已经触底,但后来的块在左右移动或快速下落时,竟然可以“穿墙而过”,覆盖掉这些块。这是因为此前对按钮点击事件的处理,只考虑到底部及左右边界对移动的限制,而没有把已经触底的组块考虑在内。现在我们就来解决这些问题,先来看左移动事件。如图12-6所示。

图12-6 原来的左移程序

一、左移程序

还记得我们对触块的判断吗?对组块中的每个方块,侦测其“下一行”的色块列表值,如果不为零,即被视为触块,在这里,我们用同样的判断方法,只是将“下一行”变为左侧的列。如图12-7所示。这里有两个地方容易出错。①当组块刚刚创建(出现在屏幕顶端)时,由于2号组块的行数需要-3,这会导致索引值<0的错误,因此我们在从-3到0的循环中,加入了“当前行-3>0”的条件,来避免发生这种错误;②在组块触底的瞬间,当前行=17,这会导致索引值>192,因此我们在程序的一开始就设定条件“基准行≤16”,来排出这样的错误。

图12-7 改进后的左移程序

经过测试,程序运行正常。

二、右移程序

用同样的方法改造右移程序,如图12-8所示。

图12-8 改进后的右移程序

三、快落程序

在测试中会发现点击快落按钮,会导致下落中的组块沉入已触底的组块中,但最多只会沉入一行,如图12-9的右图所示,这是因为对触块的判断会阻止快落组块的继续下落。解决这一问题的关键,是在快落事件中,对组块的下方的行进行判断,看其是否已经接触到了已经触底的组块。代码修改的结果如图12-9所示,注意我们对触底或触块的判断,在求颜色码时,使用的行数是(基准行+2),这是因为如果只考虑(基准行+1)行,那么在下一个计时周期中,组块的基准行还将+1,这样,无法避免后来的组块沉入已触底组块。

图12-9 快落组块沉入已触底组块以及改进后的快落程序

四、旋转程序

相对于前面的几个程序来说,对旋转程序的修改将更为复杂,我们需要为组块设定一个中心点(或者说转轴),并根据旋转过程中组块覆盖的区域,设定旋转禁区。如图12-10所示。在组块旋转过程中,既不能碰到边界,也不能碰到已经触底的组块。此外,组块在触块的瞬间也不允许再做旋转操作,否则将出现图12-11中的情形。

图12-10 1、2号组块的旋转禁区
图12-11 触块时旋转组块

对于1、2号组块而言,这里所说的旋转,并非一般意义上的连续的顺时针或逆时针旋转,而是在两种状态之间切换,切换的路径就构成了旋转禁区,如图12-10所示,其中1号组块的旋转禁区包含8个方块,它们的坐标是:

(基准行-2, 基准列-2)    (基准行-2, 基准列-1)    (基准行-2, 基准列)
(基准行-1, 基准列-2)    (基准行-1, 基准列-1)    (基准行-1, 基准列)
(基准行+1, 基准列)    (基准行+1, 基准列+1)

如果去掉坐标中的基准行、基准列,这8个坐标可以简化为:

(-2,-2) (-2,-1) (-2,0) (-1,-2) (-1,-1) (-1,0)    (1,0)  (1,1)

2号组块的旋转禁区同样包含8个方块,它们的坐标是:

(基准行-3, 基准列-2)    (基准行-3, 基准列-1)
(基准行-2, 基准列-2)    (基准行-2, 基准列-1)
(基准行-1, 基准列-2)    (基准行-1, 基准列-1)    (基准行-1, 基准列+1)    
(基准行, 基准列+1)

同样可以简化为下列坐标:

(-3,-2) (-3,-1) (-2,-2) (-2,-1) (-1,-2) (-1,-1) (-1,1) (0,1)

对于1号组块而言,虽然它自上而下下落时,上方的区域很难有已经触底的块,但为了安全起见,我们还是保留完整的禁区坐标。特别需要说明一点,这里的坐标顺序不是(x,y),而是(行,列),或者说是(y,x)。

我们为这些旋转禁区建立一个列表,并在旋转程序中访问这个列表中的相关列表项,从而判断旋转动作是否可以执行。这是一个三层列表,第一层19个元素,对应于19种组块;第二层与旋转禁区中方块的个数有关,对1、2号组块而言,均包含8个元素;第三层是坐标值,包含两个元素:行、列。目前列表的第一层只有两个元素,如图12-12所示。这里我们忽略了对基准行、基准列的引用,在程序中将引用组块的当前行及当前列,来确定具体的禁区坐标。

图12-12 只包含1、2号组块的旋转禁区列表

下面我们开始动手改写旋转程序。

首先定义一个有返回值的触犯禁区过程:根据组块编号及当前行、当前列信息,对现有组块在旋转时是否会触犯禁区作出判断,如图12-13所示。这段程序利用了旋转禁区列表,如上所述,这是一个三层列表,通过组块编号来确定被访问的第一层列表项的索引值,得到临时变量禁区坐标列表;通过对禁区坐标列表求列表长度,得到临时变量列表长度,并执行循环语句“针对从1到‘列表长度’且增量为1的每个‘坐标索引值’,执行...”,求每一组坐标所对应的颜色码。只要其中有一组坐标对应的颜色码≠0,则“触犯禁区=真”,因此将无法实施旋转操作。这里遗留一个问题:为什么临时变量禁区坐标列表与列表长度不能在一个临时变量块中同时设置声明呢?如图12-13右下角的那个块。读者可以自己试试看,同时声明设置会有什么问题,我们会在第14章中解释这个问题。

图12-13 定义有返回值的触犯禁区过程

下一步改写旋转程序,结果如图12-14所示。首先判断左右及底部边界是否会成为旋转的障碍,如果旋转不会触及边界,再判断旋转是否会触及禁区。如果组块在当前位置的旋转既不会触及边界,也不会触及禁区,则执行旋转操作。

图12-14 改写之后旋转程序

在用手机进行测试时,出现了运行故障,如图12-15所示,故障的原因是“访问了索引值<0的列表项”,这样的错误在我们的开发过程中已经不是第一次出现了,还记得吗?回想一下以前出现这类错误的原因,初步判断与刚刚创建的新组块有关,因为只有当行的数值很小时,才会导致索引值为负数,例如,如果当前行=12,2号组块禁区中的第一项(基准行-3, 基准列-2)为(-21, -1),因此索引值为(-21-1)12+(7-2)=-3119;如果当前行=23,则最小索引值为(23-3-1)12+(7-2)=-197,这正是故障提示中给出的错误索引值;只有当2号组块从顶部边界完全露出时,及当前行=4时,最小索引值为(4-3-1)*12+(7-2)=5,程序才能正常运行。

图12-15A 测试过程中出现运行故障——编程视图(从列表中选取了索引值为-197的列表项,最小合法值为1)
图12-15B 测试过程中出现运行故障——测试手机

程序中多次出现索引值<0的错误,这提示我们,在与索引值相关的代码中,添加“索引值>0”的执行条件,来避免此类问题的再次出现。

图12-15中的错误来自于触犯禁区过程,由于触犯禁区调用的求颜色码,而求颜色码调用了求索引值,于是,当试图旋转刚刚生成的2号组块时,发生了索引值<0的错误,因此我们需要改造求颜色码过程,如图12-16所示。其中,当索引值>0时,返回正确的颜色码,当索引值≤0时,返回0,这里的0与背景灰色方块的颜色码相同,表示画布顶部边界不会对旋转构成障碍。

图12-16 经过改造进的求颜色码过程

对修改过的代码进行测试,程序运行正常。

通过对按钮点击事件处理程序的改写,我们的程序现在趋于完善了,接下来的任务是,逐一引入另外的17种组块,编写它们的绘制与擦除程序,并在相关的过程及事件处理程序中,为原来只有两种选择的条件语句,添加更多的可选择分支。这是一份需要细心和耐心的任务,让我们再接再厉,争取最后的胜利!