“下落”应该怎么理解?“下落”可以理解为向下运动。 那么“向下运动”又应该怎么理解?就是将绘制方块的位置向下挪动。 通过这种置换问题的方法,我们已经有了大体的概念。 也就是说,最初在某个地方绘制方块,然后在另一个地方绘制方块的话,方块就运动了。如果两个位置相距太远,我们会觉得突然出现了一个新的方块,而不会感觉到它在运动;反之,如果两个位置比较近,就会产生它在运动的感觉。 实验要小 现在已经知道该做什么了,但有一件事需要注意。 在进行新的尝试时,最好写出只包含所尝试的内容的代码。不要加入多余的要素,而致使程序过长。 比如,两侧墙壁和底部是不动的。而我们现在想要尝试运动,所以不需要不动的要素,可以暂且把它们去掉,等程序写成后再添加回来。 当然,也会有人认为“这样做太麻烦”。在现在的程序上添加移动方块的代码,也是一种做法。看着自己的程序越来越强大,自然是件令人开心的事。 然而,一般第一次都不会成功,或者说肯定不会成功。“开始无法按照预期运行,然后一遍遍地修改再试”——这种循环是不可避免的。在这种情况下,过长的文件只会给我们的修正工作带来困扰,所以在这之后的一段时间内,我们就不会绘制两侧墙壁和底部了①。 ① 本来应该让你自己体会的。让你先采用在原有程序中添加新要素这种做法,等到陷入困境后,你就会明白“在原有程序上试验是不行的”这一道理。然而,大部分人在遇到问题后并不能意识到是因为文件太长以致找不到问题所在。许多人在不知道导致问题的原因的情况下,就产生了厌恶的情绪,因此我就在这里提前指明了。顺便说一句,许多专业程序员也没意识到这一问题。 还要指出的是,方块也不需要组合为板块一起下落。板块的形状各异,但和“下落”都没有关系。无论有几个方块,“下落”动作都不会变化。只要掌握了让1 个方块下落的方法,改为4 个应该也是轻而易举的事。 更进一步想的话,其实都不需要绘制方块,因为方块的下落和圆形、三角形的下落没什么区别,所以用点就可以了。只要能写出让点运动的程序,随后将点改为方块即可。形状不是问题所在。 如此一来,我们的目标就很简单了——编写“使1 个点下落的程序”就行,由此学习如何编写会动的程序。至于改为方块或者板块,都是以后的任务。 不得不提的是,建议你创建一个新的文件进行编写。如果一直使用同一个文件,就看不到修改前的内容了。 6.2.1 试着让点下落 首先确定最初的位置。确定好最初的位置后在那里绘制出1 个点就行了。因为将它放在哪里都行,所以就放在最方便的左上角吧。 存储区[60000] → 999999 左上角会出现1 个点,程序宣告结束(“第6 章”文件夹中的“00_1个点”)。 下一个位置 接下来要在下一个位置上画点了。即使不是向下运动,而是向右运动也没有关系。因为两者的编写难度没有什么太大的区别,所以直接采用“向下运动”。向下运动1 行,存储区编号加100。 存储区[60000] → 999999 存储区[60100] → 999999 现在绘制了2 个点(文件“01_2 个点”)。运行一下试一试。 很遗憾,看不出运动的痕迹,只能看到1 个纵向的小长方形。总之就是只显示出了2 个点。如果仔细观察一下的话,就会发现刚开始的一瞬间,画面中只有1 个点,第2 个点是随后出现的,所以也不能说没有动。然而准确地说,这个“动”只是“线伸长了”。 那么该怎么办呢?仔细想来,起点上的人出发之后,就不在起点上了。既然已经移动,那么就必须将其从原来的位置上消去才行。将点消除 现在就来将原来位置上的点消掉吧。 怎么样才能消掉呢? 希望你回想一下给像素涂色的过程。因为与像素相连的存储区保存的数字代表着颜色,所以将其改为其他数字后颜色就会发生变化。 因为999999 代表白色,而0 代表原本的黑色。所以不要想着如何将点消除,只需考虑“重新涂为黑色”即可。刻度盘这一概念非常重要,希望你能时常回想起来。 图 由此,我们加入将点重新涂为黑色的代码,如下所示(文件“02_点运动1 步”)。 存储区[60000] → 999999 存储区[60000] → 0 存储区[60100] → 999999 因为程序一瞬间就会运行完毕,所以请多运行几遍。由于速度太快,因此看不出点的运动,但勉强可以看到点一闪而过。 更远一些 1 步实在太短,现在让点多移动几步。 存储区[60000] → 999999 存储区[60000] → 0 存储区[60100] → 999999 存储区[60100] → 0 存储区[60200] → 999999 存储区[60200] → 0 存储区[60300] → 999999 像上面这样写下去(文件“03_ 点运动3 步”),点就可以走得更远。但这实在不是什么明智的方法。要移动99 回的话,就需要写下200 行,这真是太傻了。 那么该怎么办呢? 我想你已经知道了吧——用循环就行。 使用循环进行移动 现在编写使点运动到画面最下方的程序。因为画面纵向共100 个像素,所以循环100 次应该就可以了。 要涂100 次色,可以像下面这样做。 存储区[0] → 0 只要 存储区[0] < 100 存储区[60000 + (存储区[0] × 100)] → 999999 存储区[0] → 存储区[0] + 1 如此便单纯地涂了100 个点(文件“04_100 个点”)。 循环的时候,将任意选出的存储区作为“循环过的次数”来使用。最初设为0,然后每次循环时增加1,于是就可以通过这个数字生成每次都会变化的数字。在60000 上加上0 号存储区的100 倍,每次循环时就会下移1 像素。 然而,目前只不过画出了一条线而已。 通过循环实现在消除的同时进行移动 现在添加将点消除的内容。只要将绘制出的点再涂成黑色即可。 存储区[0] → 0 只要 存储区[0] < 100 存储区[60000 + (存储区[0] × 100)] → 999999 存储区[60000 + (存储区[0] × 100)] → 0 存储区[0] → 存储区[0] + 1 如此一来,涂出颜色的像素又会被迅速消除。运行一下试一试(文件“05_ 点运动到下方”)。我们终于完成了让点运动到画面一端的任务。 6.2.2 让方块下落 截止到现在,我们已经能够移动点了。通过使用循环,每次可以在不同的地方画点。当然,还需要把绘制过的点消去才行。 现在我们将目前的方法套用到方块上。 与画点时相同,先在左上角绘制1 个方块,然后将它移动1 步,最后通过循环,让它下落到底部。按这个顺序进行即可。不过,既然我们已经有了画点的程序,那么就没有理由不使用一下。因为将点替换为方块即可,所以只需略加改造就好了。再看一遍程序。 0 存储区[0] → 0 1 只要 存储区[0] < 100 2 存储区[60000 + (存储区[0] × 100)] → 999999 3 存储区[60000 + (存储区[0] × 100)] → 0 4 存储区[0] → 存储区[0] + 1 在行2 中画点,在行3 中消点。绘制点的程序仅需1 行,但绘制方块就不止1 行了。如果想用1 行搞定,就需要用到局部程序。 现在,我们来编写绘制方块的程序与消除方块的程序。首先,我们假设已经拥有相关的局部程序,在此基础上写下代码。 0 存储区[0] → 0 1 只要 存储区[0] < 100 2 绘制方块() 3 消除方块() 4 存储区[0] → 存储区[0] + 1 “绘制方块”和“消除方块”是局部程序的名称。如果单纯取名为“方块”的话,会分不清到底是绘制还是消除,因此将名字取得长一些。 编写局部程序 接下来编写局部程序。这里同样可以参考以前的代码,在前一章中做出的程序是这样的。 方块() 是 存储区[1] → 0 只要 存储区[1] < 4 存储区[0] → 0 只要 存储区[0] < 4 存储区[60000 + (存储区[3] × 500) + (存储区[4] × 5) + 存储区[0] + (存储区[1] × 100)] → 999999 存储区[0] → 存储区[0] + 1 存储区[1] → 存储区[1] + 1 要利用这个程序在指定位置绘制方块的话,该怎么做呢?因为在3号存储区中保存了以方格为单位的纵向编号,而在4 号存储区中保存了以方格为单位的横向编号,所以只要引用这个局部程序即可。例如: 存储区[3] → 1 存储区[4] → 2 方块() #----以下为局部程序 方块() 是 存储区[1] → 0 只要 存储区[1] < 4 存储区[0] → 0 只要 存储区[0] < 4 存储区[60000 + (存储区[3] × 500) + (存储区[4] × 5) + 存储区[0] + (存储区[1] × 100)] → 999999 存储区[0] → 存储区[0] + 1 存储区[1] → 存储区[1] + 1 如此一来就可以绘制出1 个方块(文件“06_ 局部程序方块”),这个方块的左上角位于画面中的“1 的2”处(从左上角开始向下1 格,向右2格)。如果不敢确信的话,建议你实际编写出来运行一下。可以看到左上角位于“1 的2”处的方块被绘制了出来。 图 改造局部程序 现在对上面的程序加以改造,生成“绘制方块”和“消除方块”的程序。首先,“绘制方块”的内容与“方块”这个局部程序的内容一致,所以只需更改名称即可。至于“消除方块”,将颜色改为黑色即可。 图 就是上面这样。“绘制方块”和“消除方块”的区别在于所涂的颜色不同,也就是行15 与行25 中“→”右侧的数字不同。此外,在引用局部程序前,需要让3 号存储区保存以方格为单位的纵向编号,而在4号存储区中保存以方格为单位的横向编号。这就是行2 与行3 的内容。因为需要让方块下落,所以将代表纵向的3 号存储区设置为每次循环都加1 的0 号存储区。横向一直是0,也就是一直位于最左端。 因为在“消除方块”中,要在相同位置绘制黑色方块,所以可以省略掉修改3 号和4 号的步骤。 那么,赶快试试吧(文件“07_ 方块应该下落”)。遗憾的是,运行结果有问题。 果然无法运行 我是在知道有问题的情况下故意给出了有错误的代码。你是否注意到了问题所在?如果没有注意到的话,就从现在开始一起来修正吧。 大多数编程书中都不会给出有错误的代码,一般都会给出最终可以正确运行的程序。然而,仅仅观察完成品,有时无法弄明白该如何制作,因为最重要的在于了解过程。 一口气就将代码写下来,不犯任何错误是不可能的,妄图极力避免也没有用①。既然如此,不如先不考虑错误,直接写下来,随后再进行订正,这样反而更快。 ① 当然,熟练之后就不会犯很简单的错误。如果是我的话,上面的错误就会在思考阶段被解决,而不会写出来。达到这种程度后一次就可以写好。 先写再修正,再写再修正……这一实践过程本该由你自己亲身体验,而不是在书中讲述。但因为很多人根本不知道该怎么办,所以在书中介绍修正错误的顺序还是有必要的。虽然会浪费一点纸张,但我还是决定以后也采用同样的方式。 6.2.3 修正错误 想要修正错误,从现在的运行结果去推测原因会比较快。 运行以后可以感觉到第1 个方块的运动是没有问题的。画面左上角会先出现1 个方块,然后消失。 但是随后的运行结果就不正常了。方块会出现在很远之外,随后消失,然后又在同一个地方出现。就这样一直循环,而不会上下移动。 图 此外,运行一段时间后你就可以发现,程序是不会结束的。循环本应该是100 次,但是现在就算已经循环了100 次,运行也不会结束。因为大概每隔一秒钟就会重新绘制一次方块,所以本来两分钟左右就可以结束的。这可能也是一条有用的线索。 怎样的错误导致了现在的结果 从现在开始,你需要一边观察程序,一边思考究竟是怎样的错误导致了现在的结果。 图 目前手头上的信息是“只会向下移动1 次”“方块出现在距离很远的地方”“无论多久都不会结束”这3 项。该从哪个开始思考呢?因为现在看不出到底哪一条比较重要,所以就按照顺序来思考吧。 首先,“只会向下移动1 次”代表着“方块的出现位置不发生变化”。在局部程序“绘制方块”中,由3 号存储区决定方块的位置。如行2 中所示,3 号就是0 号。因此,会不会是因为每次绘制方块时0 号存储区的数字都没有变化呢? 接下来分析“方块出现在距离很远的地方”这一现象。“绘制方块”中会引用保存着纵向位置的3 号存储区,如刚才所见,3 号存储区的值就是0 号存储区的值。于是疑问又一次聚集在0 号上。“出现在距离很远的地方”这一现象说明数值并没有逐一增加,而是一下增加了很多。 最后是“无论多久都不会结束”这条信息。这代表着循环一直没有结束。也就是说,循环条件行中的条件一直是成立的。循环条件行位于行1,其条件是“0 号比100 小”。果然还是0 号①。 现在所有的矛头都指向了0 号存储区。如果0 号一直保持比100 小的某个数字不变,那么方块就不会下移,循环也不会结束。“出现在远处”想必也是出于同样的原因。 ① 只看循环条件行就能发现0 号存在问题,因此先考虑“无论多久都不会结束”是一个好主意。但是想要得到“无论多久都不会结束”这一信息,需要花费一些时间。相反,“不再向下移动”这一信息可以很快获得。 针对可疑的地方在脑中运行程序 现如今我们有必要调查一下“0 号到底是什么情况”。因为只要集中注意力调查操纵0 号的部分即可,而不必纠结于其他存储区,所以不算复杂。如果没有之前的那些推测,就不知道该将重点放在哪里,最终势必会影响到阅读代码的效率。 那么,就让我们一边阅读程序一边在脑中再现吧。首先,行0 中0号存储区被设为0,当然满足行1 中的循环条件,于是进入行2。在行2和行3 中,0 号的数值没有变化。随后前往“绘制方块”。 在行10 中,1 号被设为0,在行11 中进入循环,前往行12。这里属于“常见类型”,可以看出代码会循环4 次。不需要可丁可卯地去读。在行12 中,在0 号存入0。 图 找到了!这就是问题所在。 出现问题的原因 假设0 号变成了10,那么在引用“绘制方块”之后会发生什么呢?它在行12 中又变回了0。无论0 号是5 还是100,到了行12 都会被设置为0。这就是问题的原因所在。不管是什么数字都会被还原为0。 这是因为我们在局部程序的内部和外部使用了同一个存储区。在编写局部程序外部的代码时,实在难以设想到0 号在局部程序的内部被重新设置。 回想一下,在我们编写绘制方块的二重循环时,曾出现过类似的问题。本来需要2 个存储区,但却只准备了1 个,这就是出现问题的原因。看来不得不重视“使用了在其他地方用过的存储区”这一错误。 修正 那么,该怎么修正呢? 修改局部程序内部,抑或是外部的编号即可。也就是说,0 号只能用于一个地方。 按照现在的情况,比起修改局部程序的内部,修改外部要方便一些。因为出现的次数越少,改起来自然越方便。因此,现在将局部程序外的0 号修改为尚未使用的2 号。其结果如下所示(文件“08_ 方块下落0”)。 图 灰底部分就是修正过的行。如此一来,方块终于可以一点一点地下落了。 然而,运行结束后就会出现“试图操纵的内容超出了存储区的范围”这样的错误提示,我们还要继续修正。这就是我们的下一个课题。 此外,现在可以看到组成方块的点逐个出现和消失,所以不能完全认为“方块在运动”。这一问题会在将来得到解决,请你先暂时忽略。出现这种结果的原因在于运行速度过慢。以后我会介绍让计算机处理得更快的方法。 这个错误的根本原因在哪里 现在让我们思考一下究竟是什么原因导致了现在的错误。虽说将0号存储区同时用于局部程序的内部和外部是原因所在,但尚有更深层次的问题存在。 先写对局部程序进行引用的部分,然后再写局部程序时,因为只要记得进行引用的部分使用了哪些编号,然后在编写局部程序时避开这些编号即可,所以并非难事。 反之,先写局部程序,然后编写进行引用的部分时也是同样的道理。记住编写局部程序时使用过的编号,然后在进行引用的部分避开它们即可。 然而这一次并非以上任何一种形式。在编写完进行引用的部分后,直接将以前的局部程序搬了过来。“之前已经编写了绘制方块的程序呀!直接搬过来就行了。”——如此便导致了后来的情况。至于局部程序中究竟使用了哪些存储区,其实早就忘记了。而这时要查出局部程序中使用的存储区编号,则相当麻烦。 “这不算什么吧?很明显不能使用0 号呀!”你也许会这么想,因为我也是这么认为的。但是,随着程序变得越来越大,要用到的存储区可能会多达上百个,甚至上千个。比如“946 号存储区用过了吗?”针对这一个问题,我们可能就需要调查数百数千行代码。单单是想一下就够头痛了。 我会在后面介绍此问题的解决方法,你现在只要知道它的存在即可。另外,只要稍微花些功夫,这个问题还是可以被基本解决的。建议你自己思考一下①。 ① 比如定下“局部程序内部只使用100 号以上的存储区”这样的规则。然而,当你需要在局部程序中引用局部程序时,问题就出现了,所以这并不算是完美的解决方法。完美的解决方法虽然存在,但是异常繁琐,想必你是想不到的。其关键词是“栈”,涉及的内容远远超出了本书的范围。 6.2.4 修正问题 如前所述,本程序尚存在问题。当方块到达下方后,会弹出“试图操纵的内容超出了存储区的范围(编号:70000)。程序异常终止。”这样的提示。如“程序异常终止。有错误出现。”所述,这个程序中存在错误,需要更正。 你是否已经注意到了问题所在呢? 提示信息的意思是“尝试修改70000 号,但是它不在存储区的范围内”。也就是说,程序打算修改70000 号。因为与像素相连的存储区到69999 号为止,所以修改70000 号是存在问题的。那么到底是在哪里尝试修改70000 了呢?又为什么呢? 因为答案很简单,我就直接说出来吧。 答案就是:不应该循环100 次。因为垂直方向上只有20 个格子,所以最多只能循环20 次。如果循环次数多于20 次,就会超出画面范围。其结果就是存储区编号超过69999。思考一下将20 存入2 号时要涂色的编号即可。最终就是在60000 上加上2 号存储区的500 倍,结果就变成了70000。 因此,正确的写法应该是下面这样(文件“09_ 方块下落1”)。 存储区[2] → 0 只要 存储区[2] < 20 存储区[3] → 存储区[2]#纵 {以下相同} 如此一来,我们终于可以看到“程序运行完毕”的提示了。 需要2 个局部程序吗 现在重新审视一下我们的程序。 图 我在前面已经说过多次,程序不应该写得太长。现在的这2 个局部程序的内容几乎一模一样,很明显是不应该的。当然,为了让程序先运行起来,这种做法并非不可取。由于缩减程序很费功夫,因此有时候为了了解运行状况,需要尽快让程序运行起来,那时可能就需要对代码进行复制。 然而,这种不负责任的代码,还是需要尽快修改好才是。“绘制方块”和“消除方块”这2 个程序只有颜色这一点差别,怎么想也不应该将它们分开。更进一步地想,今后除了白色方块,还需要绘制红色方块,难道说到时候还需要把“绘制方块”这个局部程序拆分为“绘制白色方块”和“绘制红色方块”?实在是难以想象。 可以绘制出任意颜色的局部程序 现在就把“绘制方块”改造为可以绘制出任意颜色的方块的局部程序吧。 该怎么办呢?回想一下将程序改造为“可以在指定位置绘制方块”时的情境,也就是“拉面店的浇头”这个话题(5.3.1 节)。去吃拉面时,提前准备好浇头的餐券即可。也就是说,利用存储区就行了。 因此,让我们实际尝试一下。使用存储区后,程序变成下面这样(文件“10_ 方块下落2”)。 图 在5 号存储区中提前保存好颜色,然后在局部程序中使用。在行4和行6 中将5 号设置为指定的颜色,然后在行17 中使用,就好像是“根据餐券配上浇头”一般。 不觉得难懂吗 这个程序怎么样呢? 程序变短这一点值得肯定。然而在我看来,程序变得难懂了。 刚才的程序中标有“绘制方块”和“消除方块”这样的文字,哪里绘制哪里消除都一目了然。 然而,现在的程序就不能一眼看出来。如果不知道5 号存储区中保存着颜色信息这一“方块”的用法,就难以理解这个程序。所以在999999 和0 的旁边添加了注释。而且,涂成白色(999999)时方块出现,涂成黑色(0)时方块消失这部分也不是很清晰。因为背景色是黑色,所以涂黑后图形会消失。但如果事先不知道这一点,就无法知道它是“消除”的代码。因此,不得不在程序中加入“# 绘制”“# 消除”这样的注释。不得不添加注释,并不是件好事。 虽说如此,将近乎相同的代码写上两遍同样很不明智。到底该怎么办呢? 一个办法就是使用“方块”写出“绘制方块”和“消除方块”的程序(文件“11_ 方块下落3”)。 图 和最初一样,分别作出了“绘制方块”和“消除方块”这2 个局部程序。但其内容是让5 号存储区保存999999 或者0,然后引用“方块”。 当然,只要有“方块”,我们就可以绘制出任何颜色的方块,这样一来这个程序中并没有什么新东西。不过,虽然绘制方块的程序本身只有1 个,但却能够在程序中使用“绘制方块”和“消除方块”这样易懂的文字。而且在主程序进行引用时,不需要修改5 号存储区,显得更为方便。 然而,实现它的代价就是程序变长了,也更费事了。至于是否需要将重点放在简明易懂上,则因人而异。我个人更讨厌偏长的程序,所以随后会使用之前那个只有“方块”的程序,你也可以根据自己的判断决定。