第十一章 代码整理及任务展望

开发笔记写到第10章,游戏的编程任务已经过半,而且最艰难的部分已经跨越过去,现在是我们喘口气,休息一下,回顾和展望的时候了。

你可能觉得我们的游戏还只是个开始,只有一种形状的组块,而且也没有成绩的计算,程序中还有很多匪夷所思的毛病,怎么能说是“已经过半”了呢?

我所说的过半,是就游戏开发的技术难度而言。到目前为止,技术上的难点已经解决,如组块的堆叠、满行的判断及消除等等,剩下的任务虽然还有很多,但只是一些需要细心与耐心的工作,因此,我们选择在这个时候停下来,无论是对于我这个编写笔记的人,还是对您这位阅读笔记的人,都是一个不错的、而且也是必要的安排。

在这一章中,我们要回顾一下此前的开发过程,小结一下重要的技术要点,同时盘点一下程序中的问题,并列出下一步的任务清单,以便我们能够有条不紊地完成下一步的任务。是的,如果把我们的开发任务比喻成一次登山活动,那么现在是我们登顶前的一次休整,让我们放松心情,积攒能量,蓄势待发!

代码整理是我们进行开发过程回顾的主要手段,我们的回顾过程将围绕图11-1中的内容展开。

图11-1 前10章的全部代码

像以往一样,我们把代码进行了分类,其中过程类代码又分为有返回值类、无返回值画面刷新类及无返回值变量更新类。我们在开始编写一个软件时,通常将达成目标视为首要任务,我们更关心那些可能存在的技术障碍,并把主要精力放在解决这些难题上,因此,不会特别在意程序的结构以及代码的组织。俗话说,小车不倒只管推,向前推进是压倒一切的事务。俄罗斯方块的开发过程也不例外,以上的这些代码仔细推敲起来,还有很多可以改进之处。重新审视这些代码,对它们进行适当地剪裁,将有利于提高程序的整体质量,改善程序的运行效率,提高程序运行的稳定性,同时对于提高我们的能力也大有裨益。对于俄罗斯方块的开发来说,还有另外一层意义:我们的游戏中共有19种组块,而现在我们只实现了2种组块,我们需要为后来的那些组块做一些必要的准备工作。

第一节 全局变量

我们要问自己两个问题:①为什么要使用全局变量?②目前程序中的全局变量是否非用不可?或者说有些全局变量是否可以用局部变量来代替?

对初学编程的人来说,这两个问题可能显得唐突,好像事情本就该如此,就像每天都会有日出日落一样,我们很少会问为何如此。然而,正是这样的问题,把我们从编程的启蒙状态中唤醒,并引导我们走上一条理性的技术之路,这是一个成长的过程。

首先回答第一个问题:为什么要使用全局变量。我们先来打个比方,打电话是我们日常的通信手段,每个人的手机或固定电话都会有一个电话号码,私人之间的交往就靠拨打这些号码来实现。但有些特殊的电话号码,如110、120、119等,它们被称作“特殊服务号码”,全国通用,7×24小时全天候人工值守,拨打时无需加拨区号,每个人都知道它们的用途,需要时可随时拨打;它们只有三位数字,可以节省拨号时间,以最快速度打通电话。

这些“特殊服务号码”的特点也正是全局变量的特点,下面我们用专业语言来加以阐述,全局变量的特点可以归结为以下四点:

  1. 全局可见:在整个程序中,任何过程和事件处理程序都可以访问它(空间上的全覆盖);
  2. 全程可见:在程序运行的任何阶段,都可以访问它(时间上的全覆盖),不同的计时周期内可见;
  3. 可读可写:任何一段程序都可以随时读取它的值,并修改它的值(操作方法全覆盖),这一点与特服号码不同,特服号码只能拨打,不能修改;
  4. 把持资源:这是前三个特点的原因,在程序运行过程中,全局变量保存在内存中,始终占有一段固定的空间,不释放,直到退出程序。

像110、119这样的特殊号码属于稀缺资源,这种稀缺性不是指号码本身(三位数的数字从100到999共有900个),而是指它对使用者记忆的占用。试想,如果将110报警电话拆分为10个号码,不同类型的案件(抢劫、盗窃、走私、贩毒等等)拨打不同的报警电话,这样我们还能准确快速地报警吗?因此,真正稀缺的资源是存储空间,当一个软件中有太多的全局变量时,会过多地占用内存空间,而且潜在的危险是,很多过程和事件处理程序都在对全局变量进行读写,这给程序的稳定性带来挑战。因此,当我们需要使用一个变量来保存数据时,要衡量是否必须使用全局变量,如果不要求变量的全局、全程可见,就不必使用全局变量。

与全局变量对应的是局部变量,我们在前面的程序中使用了许多局部变量。局部变量只在有限的范围内有效,如图11-2所示,在过程或事件处理程序中的局部变量只在声明局部变量的橙色代码块内部有效,当橙色代码块执行完毕后,局部变量将不复存在,变量也无法再被访问,此时,用于存储该变量的内存空间已经被释放。在图11-3中,我们将“设置屏幕标题为‘触底’”一行代码从声明局部变量的橙色方块中拖出,放到橙色方块之外,此时,“触底”块的左侧出现一个红色三角形的警告标志,表明这个代码是错误代码。

图11-2A 过程中的局部变量(有返回值)
图11-2B 事件处理程序中的局部变量(无返回值)
图11-3 局部变量的有效范围

对第一个问题的解答,实际上是我们处理第二个问题的依据。我们来看看程序中现有的全局变量是否都必须是“全局”的。如图11-4所示,你来判断一下,有没有可以裁剪掉的全局变量。

判断其是否必须为全局变量,具体说只有两条:①变量是否被多个过程或事件处理程序所读写;②变量值随时间的变化是否具有累计性。满足其中的任何一条,都必须当做全局变量来处理。图11-4中的9个变量中,基准行、基准列、组块编号、前一刻组块编号这四个变量除了被计时事件调用之外,还被几个按钮点击事件所调用,来改变组块的状态,显然它们应该具有全局的特性。前一刻基准行、前一刻基准列、色块列表、重绘起始行这四个变量在每个计时周期中的值都有可能变化,而且这个变化具有累积性,即依赖于前一个或几个周期的值,因此它们也必须是全局的。只有最后一个“重绘终止行”,它的值具有随机性,不具备时间的累积性,也仅被“重绘画布”一个过程所引用,因此它可以处理为局部变量。

图11-4 程序中的全局变量

你也许会问,“重绘终止行”不仅出现在“重绘画布”中,还出现在计时事件处理程序中,你不是说被多个过程或事件处理程序所读写,就应该是全局变量吗?这个问题要这样来看:“重绘画布”过程在整个程序中,只被计时事件处理程序调用,并没有出现在其他的事件处理程序中,“重绘画布”的存在,是为了解决程序的结构化问题,并不是为了提高代码的复用性,我们完全可以不用创建这个过程,而把过程中的代码直接写在计时事件处理程序中,这是第一个理由;除此之外,如果你仔细分析代码,“重绘终止行”的读写操作都发生在同一个计时周期内,并没有跨越计时周期,因此它不具备时间上的累积性。从这两点来看,“重绘终止行”就应该被处理为局部变量。

我们对代码稍作处理,如图11-5所示,为“重绘画布”过程添加一个输入参数“重绘终止行”,将循环变量的终止值有原来的“global 重绘终止行”改为参数“重绘终止行”;在计时事件处理程序中,让“重绘画布”直接引用“global前一刻基准行”,来代替原来的“设global重绘终止行为global前一刻基准行”;最后,删除全局变量“重绘终止行”。

图11-5 将重绘终止行降级为局部变量

如果从简洁的角度来看,重绘画布过程甚至不需要重绘终止行这个参数,在重绘画布过程中,直接将循环变量的终止值设为前一刻基准行,其运行结果与图11-5中的代码完全相同,那么为什么还要设置重绘终止行这个参数呢?这是为了提高程序的可读性,便于理解程序的意图。

通过对代码的检讨,我们发现原来的代码有些“愚蠢”的地方,明明是可以简单地引用已有的全局变量,却还要另外设置一个全局变量,真是多此一举。不过无论是对于初学者,还是那些有经验的老程序员,这样的失误都在所难免,因此,阶段性地回顾代码,是一件十分有必要的事情。

第二节 另类的全局变量——组件

为什么说组件是全局变量呢?我们来分析一下,首先,组件是否全局可见?是的。其次,组件是否全程可见?是的。最后,组件的某些属性是否可被改写?是的。好了,有这三条,结论是显而易见的,组件也是全局变量。

组件是程序中的重要资源,它们不仅用于向用户显示某些信息,同时也可以存储很多信息,这些信息保存在组件的属性里,如,对于按钮组件来说,它具有12项可改写的属性,包括背景颜色、是否启用、文字的样式(粗体、斜体、字号、颜色)、宽度、高度、图像、是否显示交互效果、显示文本、是否显示等等,其中的每个属性的值都可以在程序运行过程中进行修改,它们是一些显式的、或者说可见的全局变量。

在这一节里,我们顺便将原有的重新开始按钮改造为一个多功能按钮,按钮上的文字将在“暂停”、“继续”及“重新开始”之间切换,来满足程序在不同阶段对按钮功能的需求。如图11-6所示,屏幕底部中间的那个暂停按钮,就是我们接下来要改造的按钮。

图11-6 默认为“暂停”按钮

首先明确按钮的三种状态:

  1. 当计时器处于计时启用状态时,按钮显示文字“暂停”,此时如果点击按钮,将停止计时,并将按钮文字修改为“继续”;
  2. 当计时器处于停止状态时,显示“继续”,此时,如果点击按钮,则计时器重新开始计时,按钮文字修改为“暂停”;
  3. 当游戏结束时,程序会让计时器停止计时,将按钮文字修改为“重新开始”,此时如果点击按钮,将重新开始游戏。

我们仍然沿用这个按钮原来的名字(重新开始),下面我们编写代码来实现上述功能。通过读取按钮上的文字,来判断程序当前的运行状态,从而决定执行不同的操作。如图11-7所示。首先,让按钮的默认显示文字为“暂停”,然后修改计时事件处理程序,当游戏结束时,设置按钮的显示文本为“重新开始”,如左图;再来编写按钮点击事件处理程序,如右图。程序经过测试,实现了预期效果。

图11-7 为重新开始按钮编写程序

关于全局变量的问题,我们就讨论到这里,下一节讨论与过程有关的问题。

第三节 过程的另一种分类

上一节我们提到重绘画布过程的存在是为了实现程序的结构化,而非代码的复用性,这句话是什么意思呢?我们先来绘制一张图,看看这些过程之间的关系。

在现有的程序中有三类要素:全局变量、过程及事件处理程序,我们要用这些要素来创造一种结构,并透过结构看清这些要素存在的意义,图11-8就是最后的成果。

图中最显眼的莫过于计时事件处理程序,众多的线条以它为起点向外辐射;那些紫色的过程方块,分层部署在它的右侧;全局变量夹在过程与事件处理程序之间,另外几个事件处理程序与少数过程及全局变量保持着简单的联系。这就是我们程序的全貌。不过我们本节的重点不是描绘整个程序,而是那些紫色的过程。

图11-8 程序结构图

观察这些紫色的块,找出它们之间的差异,这是第一步。一个显著的不同是,有些块汇聚了不止一个箭头,而有些块只有一个箭头指向它,我们把前者称为“复用块”,而把后者成为“结构块”。这个世界上原本没有“复用块”、“结构块”这样的名称,我这样称呼它们,是为了用它们来说明一些道理。

所谓复用块,是能够被多个其他程序调用,或被多次调用的块,图中有七个被多个程序调用的块:画块、擦除、求索引值、求颜色码、创建新组块、初始化色块列表及绘制背景,其中被调用最广泛的是求索引值,被四个其他过程所调用; 五个在一处被多次调用的块:球填满的行、绘制、擦除水平及垂直组块(图中黑色粗线指向的块)。被调用次数最多的莫过于画块与擦除,每一次组块下落,它们都要被调用四次(因为组块有四个方块)。这些块将某种功能封装在内部,对外提供的仅仅是一个过程的名称,当其他部分的程序需要使用这种功能时,直呼其名就可以了。它们的存在提高了代码的复用性。

结构块与复用块不同的是,它们的存在不是为了提高代码的复用性,而是为了满足一种结构性的需求。图中有八个这样的块:组块下落、组块变量更新、已经触块、消除行、求触底组块覆盖的行、求重绘起始行、重绘画布以及更新色块列表,我们注意到它们都是由计时程序直接调用的块。还记得组块下落、组块变量更新这两个过程的来历吗?回顾一下第10章的图10-7、10-8及10-9,我们直接把原来计时程序中的代码拖到两个新定义的过程里,反过来再在计时程序中调用它们。这样做的目的并不是为了让其他程序也可以调用它们,或者说提高代码的复用性,而仅仅是为了让计时程序显得不那么冗长而已。其他几个结构块的作用也是如此,我们完全可以将这些过程里的代码直接写在计时程序中。

结构块虽然不能提高代码的复用性,但对于一个复杂程序来说,它们的存在却是非常必要的:

  • 有助于为程序搭建起稳定的结构:为后续开发打基础,为功能的扩展提供可能性;
  • 提高了程序的可读性:可以在宏观尺度上把握程序的脉络,理解程序的结构;
  • 提高了代码的可维护性:当程序需要修改时,可以将修改的范围限制在过程内部,而不牵连到程序的其他部分,即,将错误的风险控制在有限范围内。

第四节 关于绘制背景过程

还记得在第四章中我们绘制了16×12的灰色方阵,这个方阵在游戏过程中充当背景,从这个方阵的绘制过程中,我们理解了什么是双层循环,进而,在第九章,我们利用这个双层循环,对色块列表进行了初始化,并分化出绘制背景及初始化色块列表过程,有了色块列表,才有了此后的求填满的行、消除行、乃至重绘画布等等的一系列至关重要的程序。不过我们此时并不是为这个灰色方阵唱赞歌,而是要让绘制背景过程光荣隐退。

从图11-8的程序结构图中,我们看到有两个事件处理程序调用了绘制背景过程:屏幕初始化程序及重新开始程序。绘制背景过程实际上是执行192次画布的画线操作,为了执行画线操作,要进行192×4次起点及终点坐标的计算,因此,在游戏结束后,当我们点击重新开始按钮时,会发现屏幕并没有立即更新,而是停顿片刻,才开始进入新一轮游戏。

在上一章我们遇到了最终填满行的组块不能下落到位的问题,问题的原因是屏幕渲染(绘制画布)类程序的运行效率远低于计算类程序,这与重新开始游戏时出现的片刻停顿,属于同一类问题,因此我们为绘制背景的操作找到了一个替代方案——利用静态图片来充当画布的背景。

在Photoshop软件中制作一个方格图片并不困难,如图11-9中的右图所示,要求图片的宽300像素,高400像素,每个灰色方块为24×24像素,方块之间有1像素的间隙。图片绘制成功后,保存为png格式,并上传到我们的游戏项目中。如图11-9中的左图所示。

图11-9 为画布设置背景图片

现在我们可以将绘制背景过程从屏幕初始化及重新开始程序中删除了,经过测试,在重新开始游戏的环节,程序的运行显得流畅了。

第五节 后续任务

后续任务包括四个部分:

  1. 修正1、2号组块的受控移动(左右移动、快落及旋转)代码:此前的程序只考虑到左右及底部边界对移动的限制,并未考虑已经触底的组块对移动的限制;
  2. 增加计分功能:需要声明一个分数的全局变量,并利用屏幕的标题属性显示分数;
  3. 编写除1、2号组块之外的17种组块的“绘制”及“擦除”程序;
  4. 将现有针对1、2号组块编写的程序推广到19种组块:凡是含有“if 组块编号=1 则...”这样语句的代码,都需要进行重新编写。

其中第三项任务共需编写34个过程,工作量大,但并不复杂;相比之下,第四项任务工作量大,而且复杂,下面我们罗列出与组块类型相关的代码,以便对工作量有一个大致的了解。

  1. 需要扩展条件分支语句的过程(修改与组块编号有关的条件判断语句):

    • 组块下落
    • 求触底组块覆盖的行
    • 已经触块
    • 求重绘起始行
    • 更新色块列表
  2. 需要扩展数值范围的过程:

    • 创建新组块:将随机数范围从1~2扩展到1~19
    • 重绘画布:将绘制彩色方块的红色改为色块列表中记录的实际颜色
  3. 事件处理程序(修改与组块编号有关的条件判断语句):

    • 左移按钮的点击事件处理程序
    • 右移按钮的点击事件处理程序
    • 快落按钮的点击事件处理程序
    • 旋转按钮的点击事件处理程序
    • 计时器1的计时事件处理程序

程序开发至今,最复杂的一段程序莫过于计时事件处理程序,为了确保这段程序的简洁与稳定,我们不允许在这段程序中做组块类型的判断。但我们发现,在组块已经触块后进行触顶判断时,有几行代码与组块编号有关,如图11-10所示。

图11-10 计时程序中的组块类型判断

这里我们对其进行改进。方法是创建一个有返回值的过程,取名为已经触顶,将触顶判断封装起来。然后在主程序中调用已经触顶过程。如图11-11所示。

图11-11 用已经触顶过程封装与组块类型相关的代码

经过修改,需要调整与组块类型相关的程序列表,添加一个已经触顶过程,去掉计时程序:

  1. 需要修改条件判断语句,扩展分支语句的过程:

    • 组块下落
    • 求触底组块覆盖的行
    • 已经触块
    • 已经触顶
    • 求重绘起始行
    • 更新色块列表
  2. 需要扩展数值范围的过程:

    • 创建新组块:将随机数范围从1~2扩展到1~19
    • 重绘画布:根据色块列表中的颜色码重绘画布
  3. 事件处理程序:

    • 左移按钮的点击事件处理程序
    • 右移按钮的点击事件处理程序
    • 快落按钮的点击事件处理程序
    • 旋转按钮的点击事件处理程序

这些就是我们接下来几章要完成的任务。