第 3 章 打地鼠

最后更新于:2022-04-01 02:53:27

> **作者介绍** > > **Ellen Spertus** > > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d514d99d1.png) > > 本书的共同作者之一,美国加州奥克兰市米尔斯大学的计算机科学教授,同时也是谷歌公司的资深科学家。她先后在MIT获得了计算机科学与工程学士学位、电子工程与计算机科学硕士及博士学位,并利用暑假的空闲时间为微软公司工作。她曾撰文探讨技术及社会问题,而且经常将两者相结合。1993年纽约时报曾以《改变计算机领域面貌的女性》为题介绍Spertus,并在后续的文章中称其为“最性感的活着的极客”。2009年Spertus加入谷歌的App Inventor for Android团队,并参与撰写了本书的部分章节。 本章将创建一个“打地鼠”的游戏,游戏灵感来自一款经典的街机游戏Whac-A-Mole,其中的小动物会突然从洞中冒出,玩家则用木槌击打它们,击中得分。“打地鼠”的创作者是一名App Inventor团队的成员,与其说她是为了测试sprite组件的功能(她做到了),不如说是她自己喜欢玩游戏。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d51627b45.png) ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d516a19c7.png) **图 3-1 打地鼠游戏的用户界面** 当Ellen Spertus加入Google公司的App Inventor团队时,她希望App Inventor也可以用于游戏的开发,因此她自告奋勇地承担起sprites的实现任务。sprite原本用来表示神话中的角色,如仙女、妖精等,到20世纪70年代开始出现在计算机界,用来代表那些能够在电脑屏幕上移动的图像(在电子游戏中)。Ellen第一次使用sprite是在20世纪80年代早期,她曾经参加电脑训练营并使用TI 99/4 编程。她在sprites以及“打地鼠”游戏上所做的努力,受到了双重怀旧情绪的驱使——计算机以及游戏——她童年时代的最爱。 可以查看Android版“打地鼠”游戏的[视频教程](http://www.appinventor.org/AndroidMash-steps)。【此教程由Wolber教授基于上一个版本的App Inventor录制的,但同样可以有助于理解开发过程。】 ## **学习目标** 如图3-1所示的“打地鼠”应用将实现以下功能: * 一只地鼠随机出现在屏幕上,每秒钟移动一次; * 如果手指触碰到地鼠,则让设备震动,显示的命中数加1,地鼠随机移动到一个新位置; * 如果手指直接触摸到屏幕但没点击中地鼠,则显示失败数加1; * 点击“重新开始”按钮,游戏重新开始,命中和失败的计数归零。 ## **学习内容** 本章内容覆盖了以下的组件及概念: * ImageSprite组件:具有触感的可移动图像; * Canvas组件:容纳ImageSprite的平台; * Clock组件:用来计时,让sprite随即移动; * Sound组件:击中地鼠时产生震动; * Button组件:开始新游戏; * Procedures:用来实现一系列的指令,可以重复调用,如移动地鼠; * 产生随机数; * 使用加法块(+)及减法块(-)。 ## **准备开始** 登陆App Inventor网站,开始新项目“MoleMash ”,将屏幕标题(title)设为“打地鼠”,并连接到测试设备。 下载地鼠图片mole.png。下载方法:控制键+单击(Mac)或单击右键(Windows)并选择“图片另存为”或类似选项。下载成功后,在设计器组件列表下方的Media部分,单击“Upload file…”,找到刚下载的文件mole.png并上传到App Inventor中。 ## **设计组件** 创建“打地鼠”游戏需要以下组件: * Canvas组件:用来限定游戏中地鼠的活动区域; * ImageSprite组件:用来显示地鼠图片,随机移动,并具有触感; * Sound组件:当地鼠被触摸到时,发出震动; * Label组件:用来显示“命中: ”、“失败: ”以及命中、失败的次数; * HorizontalArrangements组件:用来放置Label组件,使组件的布局合理; * Button组件:用来将命中及失败次数归零(重新开始游戏); * Clock组件:使地鼠每秒钟随机移动一次。 表3-1显示了应用中用到的全部组件。 **表3-1 “打地鼠”应用中的全部组件列表** | 组件类型| 组件种类 | 命名 | 作用| | --- | --- | --- | --- | | Canvas | Drawing and Animation | Canvas1|ImageSprite的容 | | ImageSprite | Drawing and Animation | Mole| 用户点击的目标 | |Button | User Interface | ResetButton| 重新设置得分 | | Clock| User Interface | Clock1 | 控制地鼠的移动频率 | | Sound | Media | Sound1 | 当地鼠被击中时震动| | Label| User Interface| HitsLabel | 显示文字“击中: ” | | Label | User Interface| HitsCountLabel | 显示击中次数 | | HorizontalArrangement | Layout | HorizontalArrangement1 |放置HitsLabel及HitsCountLabel | | Label | User Interface | MissesLabel | 显示文字“失败: ” | | Label | User Interface | MissesCountLabel | 显示失败次数 | | HorizontalArrangement | Layout | HorizontalArrangement2 |放置MissesLabel及MissesCountLabel | ### **设置活动组件** 本节将设置游戏中所需的活动组件,下节再来设置显示分数的组件。 1\. 找到Palette->Drawing and Animation->Canvas组件,拖入预览窗口,采用其默认名称Canvas1,设置Width属性为“Fill parent”,即与屏幕等宽,设置Height属性为300像素; 2\. 找到Palette->Drawing and Animation->ImageSprite,将ImageSprite组件拖入到Canvas1中的任何位置,在组件列表底部单击rename,改名为“Mole”,设置其Picture属性为之前上传的mole.png; 3\. 找到Palette->User Interface->Button,拖动Button组件放在Canvas1下面,改名为“ResetButton”,并设置其Text属性为“重新开始”; 4\. 找到Palette->User Interface->Clock,拖入Clock组件,它将落在预览窗口下方的“非可是组件”区域; 5\. 找到Palette->Media->Sound,拖入Sound组件,它也将落在“非可视组件”区域。 现在组件设计器看起来应该如图3-2(地鼠的位置有可能不同)。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d51700656.png) **图 3-2 组件设计器视图中的所有“活动”组件** ### **布置Label组件** 现在设置显示用户得分的组件,即,显示命中与失败次数的组件。 1\. 找到Palette->Layout->HorizontalArrangement,拖动组件放在“重新启动”按钮的下方,保留HorizontalArrangement1的默认名称; 2\. 从Palette->User Interface中拖动两个Label组件到HorizontalArrangement1中; * 将左侧Label改名为HitsLabel,设置其Text属性为“命中: ”(确保冒号后有一个空格); * 将右侧Label改名为HitsCountLabel,设置其Text属性为“0”; 3\. 拖入第二个HorizontalArrangement,将其放在HorizontalArrangement1下面; 4\. 将两个Label拖放在HorizontalArrangement2中; * 左侧Label改名为MissesLabel,设置其Text属性为“失败: ”(确保冒号后有一个空格); * 右侧Label改名为MissesCountLabel,设置其Text属性为“0”。 你的屏幕看起来如图3-3。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d5176e07b.png) **图 3-3 组件设计器视图中“打地鼠”应用的所有组件** ## **为组件添加行为** 组件已经创建完成,下面切换到块编辑器来实现程序的行为。设置的目标:①让地鼠每秒钟在Canvas1上随机移动一次;②用户拍打这只随机移动的地鼠,应用显示用户命中或失败的次数(注:建议用手指而不是木槌拍打!);按下“重新启动”按钮命中及失败次数归零。 ### **移动地鼠** 在迄今为止完成的应用中,曾经调用过内置过程 ,如HelloPurr中的Sound1.Vibrate(震动)。假如App Inventor中有一个内置过程,可以将ImageSprite移动到屏幕上的某个随机位置,那岂不是很好?可惜没有,不过我们可以自己来创建过程!就像内置过程一样,自己创建的过程会显示在Procedures抽屉中,需要时可以随时调用它。 具体来说,创建一个名为MoveMole的过程,让地鼠在屏幕上移动到某个随机位置。游戏开始时调用一次MoveMole过程,当用户成功地点击到地鼠后,每秒钟执行一次该过程。 ### **创建MoveMole过程** 要理解地鼠如何移动,需要了解Android的图形定位机制。Canvas(以及Screen)可以看作是由x(水平)坐标和y(垂直)坐标织成的网格,其左上角的(x,y)坐标为(0,0)。 x坐标向右为增大, y坐标向下为增大,如图3-4所示。一个ImageSprite的x、y属性表示它左上角的位置,因此当地鼠位于屏幕左上角时,他的x和y值都是0。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d517da780.png) **图 3-4 屏幕上Mole的位置——坐标、高度和宽度信息,x坐标及宽度以蓝色表示,y坐标和高度以橙色表示** 为了将地鼠的移动限制在屏幕之内,要确定x和y的最大值,这要用到地鼠Mole和画布Canvas1的Width(宽度)及Height(高度)属性。(地鼠的Width和Height属性值与上传的图片的大小相同,而在创建Canvas1时,你设置的高度是300像素,宽度为“Fill parent”,即等于它的“父”容器——屏幕的宽度。)如果地鼠图片的宽度是36像素,画布宽度是200像素,那么Mole的x坐标最低可以为0(靠近屏幕左侧边缘),而最大为164(200 - 36,或Canvas1.Width - Mole.Width),这样才能保证Mole不超出屏幕的右侧边缘。同样,Mole顶部的y坐标范围可从0到Canvas1.Height - Mole.Height。 图3-5显示了创建的MoveMole过程,图中标有详细注释(可以有选择地添加到过程中)。 为了随机地放置Mole,x坐标要在0到Canvas1.Width - Mole.Width的范围内选择,同样,y坐标要在0到Canvas1.Height - Mole.Height的范围内。使用Math抽屉里的内置过程random integer生成一个随机整数,将“from”参数从改默的1改为0,同样修改“to”参数,如图3-5所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d5184db9a.png) **图 3-5A MoveMole过程,用于将Mole放在一个随机的位置上** 按如下步骤创建过程: 1\. 找到Procedures:单击块编辑器中的Procedures抽屉; 2\. 得到to procedure:在Procedures抽屉中点击to procedure块(不带result的to procedure); 3\. 设置过程名称:单击块中的文字“procedure”并输入“MoveMole”; 4\. 移动Mole:单击Mole抽屉,将call Mole.MoveTo块拖到procedure块中“do”的右侧;注意:我们还需要提供x和y的坐标; 5\. 设定Mole的x坐标:如前所述,x坐标范围在0与Canvas1.Width - Mole.Width之间: * 点击Math抽屉; * 拖出random integer from块,将左侧插头(突起)插入call Mole.MoveTo块的“x”插槽; * 点选from之后的数字1并输入0; * 丢弃数字100:点击该块,再按键盘上的Del或Delete键,或直接拖入垃圾箱; * 点击Math抽屉,将一个减法块(-)拖入to插槽; * 点击Canvas1抽屉,向下滚动直到看见Canvas1.BackgroundColor ,将其拖入到减法块“-”的左侧,然后从BackgroundColor所在的下拉菜单中选择Width选项; * 同样,点击Mole抽屉并拖入Mole.Enabled块,然后从Enabled块所在的下拉菜单中选择Width选项,并将它插入到“-”右侧的插槽中; 6\. 按类似步骤设定y坐标,应该是一个从0到Canvas1.Height - Mole.Height的随机整数; 7\. 对图3-5A(行内输入)或3-5B(外展输入)检查操作结果。 8\. random integer from to块的“external inputs”(外展输入)方式:右键点击random块,选择列表第三项external inputs;如果想恢复行内输入,右键点击random块,选择inline inputs。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d518c57d8.png) **图 3-5B MoveMole过程,用于将Mole放在一个随机的位置上** ### **在应用启动时调用MoveMole过程** 已经完成了MoveMole过程,现在该调用它了。对于程序员来说,最熟悉的事情就是在应用启动的同时执行某些指令,块Screen1.Initialize就是专为这个目的而设计的: 1\. 点击Screen1抽屉,并拖出Screen1.Initialize块; 2\. 单击Procedures抽屉,你会看到一个call MoveMole块(这很有趣:你自己创建了一个新块,不是吗?!)。把它拖入Screen1.Initialize,如图3-6所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d537532f5.png) **图 3-6 在应用启动时调用MoveMole过程** ### **每秒钟调用一次MoveMole过程** 要让地鼠每一秒移动一次,需要用到Clock组件。设置Clock1的TimerInterval属性为其默认值1000(毫秒),即1秒,我们称每秒一次的计时为计时器的心跳。这意味着,在Clock1.Timer块中,无论设定什么动作,它都会随着计时器的心跳,每秒钟执行一次。以下是具体设置: 1\. 单击Clock1抽屉,并拖出Clock1.Timer; 2\. 单击Procedures抽屉,将call MoveMole块拖到Clock1.Timer块中,如图3-7所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d537b8298.png) **图 3-7 计时器开始计时后,每次心跳(每秒)都会调用一次MoveMole过程** 如果你觉得心跳得太快或太慢,可以在组件设计器中改变Clock1的TimerInterval属性,来增加或减小地鼠的移动频率。 ### **记录成绩** 刚才我们创建了两个Label:初始值为0的HitsCountsLabel和MissesCountsLabel,希望以此来记录用户的成绩:当用户命中Mole一次,或失败一次(直接拍打到屏幕)时,对应Label中的数字增加,为此要用到Canvas1.Touched块,它表示Canvas被触摸到,并记录了触摸点的x和y坐标(我们不必关心),以及是否碰到了sprite(这是我们关心的)。图3-8显示了即将创建的代码。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d5380f285.png) **图 3-8 触碰到Canvas1时,让命中(HitsCountLabel)或失败(MissesCountLabel)次数递增** 图3-8可以理解为:当触碰到canvas时,检查sprite是否也被碰到。应用中只有一个sprite,即Mole,如果碰到Mole,则HitsCountLabel.Text中的数字+1,否则,MissesCountLabel.Text中的数字+1(如果没碰到sprite,则touchedSprite的值为false )。 下面介绍如何创建这些块: 1\. 点击Canvas1抽屉,并拖出Canvas1.Touched; 2\. 单击Control抽屉,拖出Ifelse块(先拖入if块,然后为其添加else块:点击if左边的蓝色方块,在弹出框中将else块拖入if块),并放入Canvas1.Touched块中; 3\. 从Variables抽屉中拖出get块,放入ifelse的if插槽内,选择下拉菜单中的touchedSprite选项;或者将鼠标悬停在when Canvas.Touched块的参数touchedSprite上,从中获取get touchedSprite块; 4\. 按照我们的设想,如果if检测成功(即Mole被触摸到),则HitsCountLabel.Text递增: * 从HitsCountLabel抽屉里拖出set HitsCountLabel.Text to块并放入“then”的右边; * 点击Math抽屉,拖出一个加号(+),将其放在“to”插槽中; * 点击HitsCountLabel抽屉,拖动HitsCountLabel.Text块到“+”的左边; * 点击Math抽屉,并拖动一个“0”块到“+”的右边,将0改为1 ; 5\. 在ifelse块的“else”部分,对MissesCountLabel块重复步骤4。 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d5388a346.png) 测试:测试你的新代码:在设备上触摸Canvas,命中或错过地鼠,看看分数有什么变化。 ### **过程抽象** 计算机科学的重要手段之一,就是命名然后调用一组指令(如MoveMole),这种能力被称为过程抽象。之所以叫做“抽象”,是因为过程的调用者(在实际项目中,很有可能不是过程的开发者)只需要知道过程的功能(如移动地鼠),而不需要知道过程的实现方法(生成两个随机整数)。如果没有过程抽象,不可能实现那些大型程序,因为它们的代码量太大,对个人来说是力所不及的,这一点与现实世界中的劳动分工相类似。例如,不同的工程师设计出汽车的不同部件,没有人了解所有的细节,而司机只需要了解接口(例如,踩下制动踏板把车停下来),而无需了解如何实现这些接口。 与复制和粘贴代码相比,过程抽象的优势在于: * 由于过程的代码独立于其它部分的程序,因此更易于对过程的测试; * 如果代码中有错误,只需要对局部进行修改; * 如果需要改变过程的实现 (或功能),如确保地鼠不连续出现在同一个位置,只需要修改一处的代码; * 可以将过程汇集到一个程序库中,以便在不同的程序中使用。(遗憾的是App Inventor暂时不支持这项功能。) * 将大块代码拆分成代码片段,有助于对应用做深入剖析,并加以实现(“分而治之”)。 * 给过程一个有意义的命名,将有助于提高代码的可读性,更易被别人(或一个月后的自己)读懂; 在后面的章节中,还将学到过程更加强大的功能:添加参数,提供返回值,以及调用过程本身。有关内容请参见第21章。 ### **重置分数** 朋友看到你玩MoleMash,他可能也想试试身手,所以最好能让成绩归零。根据前面学过的内容,不经提示你也有能力把它做出来。阅读之前动脑筋试试看。 我们要在ResetButton.Click块中设置HitsCountLabel.Text和MissesCountLabel.Text的值为0。如图3-9所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d54302cd8.png) **图 3-9 按下Reset按钮让命中次数(HitsCountLabel)和失败次数(MissesCountLabel)归零** 此处提供一个技巧,来快速建立ResetButton.Click的事件处理程序:在工作区直接输入0并回车,将生成数字块0,等同于从Math抽屉中拖出。(这种输入方式对其他块也同样有效。) > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d5388a346.png) 测试:开始游戏,尝试多次命中及错过地鼠,然后按下“重新启动”按钮。 ### **添加触摸地鼠行为** 我们希望在触摸到地鼠时,设备能够振动,这要用到Sound1.Vibrate块。如图3-10所示。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d54dcab8a.png) **图 3-10 碰到地鼠时让设备短暂振动(100毫秒)** > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d5388a346.png) 测试:当你在设备上实际触摸到地鼠时,看看振动的效果如何。如果你觉得振动时间过长或过短,可以修改Sound1.Vibrate块的毫秒数。 ## **完整的MoleMash应用** 图3-11中描述了完整的MoleMash应用中所有的块。 ![{%}](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-31_55e3d558a8294.png) **图 3-11 完整的MoleMash应用** ## **改进** 对MoleMash应用还可以做如下补充: * 添加按钮,让用户可以控制地鼠的移动速度; * 添加一个Label,随时显示地鼠出现(或移动)的次数; * 添加另一个ImageSprite,如一朵花的图片:用户不许碰到它,如果碰到将会受到惩罚,减少得分或结束游戏; * 用ContactPicker组件从用户手机的电话簿中选择图片,来替代地鼠图片。 ## **小结** 本章介绍了一些非常有用的技巧,适用于普通应用,更适用于游戏: * Canvas组件使用了直角坐标系,其中x表示水平方向(从左边的0到右边的Canvas.Width -1),y表示垂直方向(从顶部的0到底部的Canvas.Height -1)。从Canvas的高度和宽度中减去某个ImageSprite的高度和宽度,这个范围可以确保sprite在画布上完整地显示; * 利用Canvas和ImageSprite组件的Touched方法 来实现对设备触感的应用; * 创建实时交互应用:不仅可以对用户的操作做出实时响应,也可以对设备内部的计时器做出响应。具体地说,Clock.Interval属性用于设定计时器的心跳频率,这个频率也可以用于控制ImageSprite (或其它)组件的移动; * Label可用于显示得分,根据玩家的操作结果,得分会相应升高(或下降); * 通过Sound.Vibrate方法对用户的触摸事件进行反馈,让设备震动一定的毫秒数; * 不仅可以调用内置过程,也可以通过给一组块设定名称(MoveMole),来创建自己的过程,这些过程也可以像内置过程一样被调用,这就是所谓的过程抽象,在计算机科学中这是一个非常重要的思想,可以实现代码的复用 ,也使得创建复杂应用成为可能; * 利用Math抽屉中的random integer(随机整数)块,可以产生不可预知行为,让游戏每次开始时都有所不同。 在第5章(瓢虫快跑)中,将了解更多的游戏制作技巧,包括移动中的ImageSprite组件之间的碰撞检测。 ### **术语解释** procedure:译为过程、步骤、程序,在早期面向过程的编程语言中,也被称作子程序。它是一段由单行或多行语句组成的代码片段,被包装成一个有名称的单元,相对独立,用于完成某种特定功能。当主程序中需要实现该功能时,就会引用它的名称来实现对它的调用。本书中译为“过程”。 false:中文译为“假”,与true(真)相反,是布尔(Boolean)类型变量的值。 Boolean:译为“布尔数学体系的”,是一个形容词,是为了纪念数学家布尔的伟大贡献。与这个词相关的短语还有boolean algebra(布尔代数)、boolean operation(布尔运算)、boolean value(布尔值)等等。其中计算机软件中使用Boolean表示一种变量类型:布尔型变量,这种变量只有两个值:true(真)与false(假)。 ### **背景知识** George Boole:中文译为“乔治·布尔”,是19世纪一位自学成才的爱尔兰数学家,他创立了布尔代数(boolean algebra)。布尔代数用于解决集合运算及逻辑运算问题,是当代计算机硬件设计的理论基础。在软件领域中,逻辑运算(and,or,not)是程序编写过程中的关键环节,在App Inventor中具体体现为“if”块,它根据逻辑运算的结果(true、false)来决定下一步程序的走向。此外,在“while”块中也用到了逻辑运算。 TI 99/4:由美国德州仪器生产的一款早期的家用电脑,发布于1979年11月,价值$1,150,CPU为TI TMS9900,主频3MHz,16K内存,26K只读存储器,192x256、16色、13英寸监视器,内置TI BASIC语言。(引自[http://oldcomputers.net](http://oldcomputers.net/),顺便说一句,这是一个很有趣的网站!) ### **中英文对照** button:按钮 clock:时钟 control:控制 count:计数 false:假 fill:充满 hit:击打 命中 if:如果 integer:整数 interface:界面 interval:间隔 label:标签 mash:捣碎 media:媒体 miss:错过 mole:鼹鼠 parent:双亲 procedure:过程 random:随机 sound:声音 sprite:精灵 text:文字 true:真 user:用户 vibrate:震动 ### **资源下载** [mole.png](http://www.17coding.net/images/right/3/mole.png)
';