第 5 章 瓢虫快跑
游戏是移动应用中最令人兴奋的部分,无论是玩游戏,还是做游戏。最近红极一时“愤怒的小鸟”,根据开发者Rovio公司称,第一年下载量达50万次,同时每天运行的人时数超过一百万小时。(甚至有人说要把它拍成故事片!)我们可无法保证电影的成功,但可以让您用App Inventor创建自己的游戏“瓢虫快跑”,里面的瓢虫要吃蚜虫,同时要避免被青蛙吃掉。
应用描述
如图5-1所示的“瓢虫快跑”应用,用户可以:
通过倾斜设备来控制瓢虫移动;
查看屏幕上的能量指示条,能量会随时间减少,并引起瓢虫的饥饿;
让瓢虫追逐并吃掉蚜虫来获得能量,抵御饥饿;
帮助瓢虫躲避青蛙,因为青蛙吃瓢虫。
图 5-1 瓢虫快跑游戏手机截屏
学习要点
在开始探索本章之前,我们假设你已经完成了第3章MoleMash的学习,并熟悉了过程创建、随机数生成、Ifelse块以及ImageSprite、Canvas、Sound和Clock组件。
本章在复习MoleMash以及前几章内容的基础上,主要介绍以下内容:
使用多个ImageSprite组件,并检测它们之间的碰撞;
使用OrientationSensor(方向传感器)组件检测设备的倾斜,并用它来控制ImageSprite;
改变ImageSprite的显示图片;
在Canvas组件上画线;
用Clock组件控制多个事件;
用变量来记录数值(瓢虫的能量水平);
创建和使用带参数的过程;
使用and块。
设计组件
在应用中,使用一个Canvas组件作为三个ImageSprite组件的活动场地,三个ImageSprite组件分别代表瓢虫、蚜虫和青蛙,此外,还要为青蛙配一个声音组件。OrientationSensor(方向传感器)通过测量设备的倾斜来移动瓢虫,Clock组件用来改变蚜虫的运动方向。另有一个显示瓢虫能量水平的Canvas组件;一个重新启动按钮,当瓢虫饿死或被吃掉时,用来重新启动游戏。表5-1提供了本应用中使用的全部组件列表。
表5-1 瓢虫快跑游戏中的所有组件
组件类型 | 面板中分组 | 命名 | 作用 |
---|---|---|---|
Canvas | Drawing and Amination | FieldCanvas | 运动场地 |
ImageSprite | Drawing and Amination | Ladybug | 用户控制的角色 |
OrientationSensor | Sensor | OrientationSensor1 | 测试手机的倾斜,控制瓢虫移动 |
Clock | User Interface | Clock1 | 决定何时改变Imagesprite的方向 |
ImageSprite | Drawing and Amination | Aphid | 蚜虫:瓢虫的捕食对象 |
ImageSprite | Drawing and Amination | Frog | 青蛙:瓢虫的捕食者 |
Canvas | Drawing and Amination | EnergyCanvas | 显示瓢虫的能量水平 |
Button | User Interface | RestartButton | 重启游戏 |
Sound | Media | Sound1 | 青蛙吃瓢虫时发出的声音 |
准备开始
下载瓢虫、蚜虫、死瓢虫及青蛙的图像,此外还有青蛙的声音文件。
登陆App Inventor网站,建一个名为“LadybugChase”新项目,屏幕标题设置为“瓢虫快跑”。打开块编辑器并连接到测试设备,将下载的图片及声音文件上载(Upload file)到媒体面板。
如果使用设备而不是模拟器,你需要禁用“屏幕自动旋转”功能,否则当设备旋转时,会改变设备的显示方向。在大多数设备上,可以点击设置->显示,然后取消选中的“屏幕自动旋转”复选框即可。
活动的瓢虫
在这个“第一人称”的游戏中,瓢虫代表玩家,玩家通过倾斜手机来控制瓢虫的运动。与MoleMash不同,这里玩家被带入游戏,而不是在设备以外用手触碰。
添加组件
在前几章,我们一次性地创建了所有的组件,但这不是开发人员的习惯做法。相反,通常每次只创建一部分组件,编写相应的程序,并进行测试,然后在进入到下一部分。在本节中,我们先来创建瓢虫并控制它的运动。
在组件设计器中创建一个Canvas,命名为FieldCanvas,并设置其宽度为“Fill parent”,高度为300像素;
在FieldCanvas上放置一个ImageSprite,重命为Ladybug,并设置其Picture属性为活的瓢虫图片。不必在意它的x、y属性,这取决于ImageSprite被放在画布上的位置。
也许你已经注意到,ImageSprites还有Interval、Heading以及speed属性,而这些都是在本程序中要用到的:
Interval属性:在本游戏中可以设置为10(毫秒),来设定ImageSprite自身的移动频率(而不是像MoleMash中那样,运动被MoveTo过程所控制);
Heading属性:指示ImageSprite将要移动的方向。例如:0表示向右,90表示向上,180表示向左,等等。现在就让它取默认值——向右,我们将在块编辑器中改变它;
Speed属性:指定ImageSprite在每个时间间隔内移动的距离(单位为像素)。我们将在块编辑器中设置Speed属性。
瓢虫的运动由OrientationSensor通过检测设备的倾斜程度来进行控制;Clock组件用来每隔10毫秒(每秒100次)检测一次设备的方向,并相应地改变瓢虫的Heading(方向)属性 。我们将在块编辑器中做如下设置:
1. 添加OrientationSensor组件,它将出现在“不可见组件”区域;
2. 添加Clock组件,它也将出现在“不可见组件”区域,并设置其TimerInterval属性为10毫秒。对照图5-2检查添加的组件。
图 5-2 在组件设计器中为动画瓢虫设置用户界面
添加行为
切换到块编辑器,创建名为UpdateLadybug的过程(procedure)及Clock1.Timer块,如图5-3所示。尝试不使用抽屉,直接输入块的名字(如“when Clock1.Timer”)来生成块。(请注意,对数字100的乘法操作使用的是星号(*),但图中看不到。)虽然可以单击右键选择添加注释,但这不是必须的。
图 5-3 每隔10毫秒改变一次瓢虫的方向及速度
在UpdateLadybug过程里用到了两个OrientationSensor最有用的属性:
Angle(角度):表示设备倾斜的方向;
Magnitude(幅度):表示设备的倾斜程度,范围从0 (不倾斜)至1(最大倾斜)。
Magnitude乘以100是告诉瓢虫,在每个时间间隔(TimerInterval)内,在某个特定的方向,移动的距离在0到100像素之间。时间间隔为之前在组件设计器中设定的10毫秒。
虽然在连接设备上可以测试瓢虫的移动,但与打包下载到设备上的运行效果相比,瓢虫的速度要么太慢,要么太快。对于安装运行的应用,如果太慢,可以增加速度;相反,则减小速度。
显示能量水平
在第二个Canvas组件上用一个红色线条来显示瓢虫的能量水平。线条高度为1个像素,宽度为瓢虫的能量值,取值范围从200(健康)到0(死)。
添加组件
在组件设计器中,在FieldCanvas下方创建一个新的Canvas组件,命名为EnergyCanvas;设置Width属性为“Fill parent”,Height属性为1个像素。
创建变量:Energy
在块编辑器中,创建一个初始值为200的变量来记录瓢虫的能量水平。(还记得吧,在第2章PaintPot中,第一次使用变量dotSize)以下是具体步骤:
1. 在块编辑器中,拖出一个initialize global name to块,将name改为energy;
2. 如果energy块的右侧插槽内有其他块,删掉它:选中并按Delete键或直接拖到垃圾桶;
3. 创建一个数组块200(直接输入数字200或拖动Math抽屉中的0块),然后插入initialize global energy to块,如图5-4所示。
图 5-4 将变量energy初始化为200
图5-5中显示了当鼠标悬浮在初始化变量块的“energy”文本上时,呼出了全局变量energy的“get”及“set”块;
图 5-5 从初始化变量块中获得set及get块
画出能量条
我们要在变量energy与红色线条之间建立通信,使线条长度(像素)与能量值相等。为此创建如下两个类似的组块:
1. 在EnergyCanvas上从(0, 0)点到(energy, 0)点画一条红线,以显示当前的能量水平;
2. 在EnergyCanvas上从(0, 0)点到(EnergyCanvas.Width, 0)点画一条白线,在画新能量水平线之前,清除当前的能量水平线。(记得前面设置EnergyCanvas.Width为“Fill parent”。)
然而,最好能创建一个过程,能用任何颜色在EnergyCanvas上画任意长度的线。为此,需要定义两个参数:length(长度)和color(颜色),当程序被调用时,我们只需要指定参数值,就像在MoleMash一章中调用random integer内置过程一样。下面是创建DrawEnergyLine过程的步骤,如图5-6所示。
1. 进入Procedures抽屉,拖出一个to procedure块;
2. 点击过程名(可能是“procedure” ),改为“DrawEnergyLine”;
3. 点击过程块左上角的蓝色方块,呼出两个块:input及input x;
4. 将input x块插入到input块内,将x修改为color;
5. 重复步骤4:插入第二块input x并命名为“length”;
6. 按照图5-6所示,为该过程添加的其余的块:将鼠标悬停在to DrawEnergyLine块的参数color及length文本上,获得get color及get length块;或者从Variables抽屉中直接拖出get块,插入到to DrawEnergyLine内部的块中,点击下拉菜单选择color或length。
图 5-5a 为DrawEnergyLine过程添加输入(参数)
图 5-6 定义过程DrawEnergyLine
现在,你已经掌握了创建过程的窍门,让我们再写一个DisplayEnergy的过程,两次调用DrawEnergyLine过程:第一次用来擦除旧线(覆盖整个EnergyCanvas的白线),第二次用来显示新的能量线,如图5-7所示。
图 5-7 定义过程DisplayEnergy
DisplayEnergy过程由以下四行命令组成:
1. 设定画笔颜色为白色;
2. 画一条贯穿EnergyCanvas的横线(1个像素高);
3. 设定画笔颜色为红色;
4. 画一条长度等于energy值的线。
提示:将若干行代码规整到一个过程中,通过调用这个过程来取代逐行地执行这些代码,这个过程被称作重构,这种强大的技术使得程序更易于维护,也更可靠。在这种情况下,如果我们想改变能量线的高度或位置,我们只需对DrawEnergyLine过程做一次修改,而不必分两次来完成这一修改。
饥饿而死
不同于前几章的应用,本游戏设定了结束环节:如果瓢虫吃不到足够的蚜虫,或者被青蛙吃掉,则游戏结束。此时我们希望瓢虫不再移动(设置Ladybug.Enabled为false),并将活瓢虫图片换成死瓢虫(将Ladybug.Picture设置为已上传的图片文件名)。GameOver过程的创建如图5-8所示。
图 5-8 定义GameOver过程
再按图5-9所示向UpdateLadybug(由Clock.Timer每10毫秒调用一次)添加红框内的代码:
减少瓢虫的能量(energy = energy - 1);
显示新的能量水平(call DisplayEnergy);
如果energy值为0则游戏结束。
测试:你可以在设备上测试这段代码,并验证能量水平随时间的减少,并最终导致瓢虫死亡。重启应用可以点击“Reset Connection->AI Companion”。
图 5-9 UpdateLadybug过程的第二个版本
添加蚜虫
下面来添加蚜虫,即让蚜虫在FieldCanvas上浮动。如果瓢虫撞上蚜虫(视同“吃”掉它),则瓢虫的能量水平升高,而蚜虫消失,且稍后会再次出现。(在用户看来,这完全是另一只蚜虫,但实际上是同一个ImageSprite组件。)
添加一个ImageSprite
添加蚜虫首先要回到组件设计器,创建另一个ImageSprite,要确保它不落在瓢虫上,命名为Aphid,其属性设置如下:
1. Picture属性:设置为已上传的蚜虫图像文件;
2. Interval属性:设置为10,即:像瓢虫一样,每10毫秒移动一次;
3. Speed属性:设置为2,因此蚜虫移动不会太快,以便让瓢虫能抓住它。
不必在意它的x、y属性(只要不是在瓢虫上)或title属性,这些可以在块编辑器中设置。
控制蚜虫
实验发现,蚜虫每隔50毫秒(Clock1跳动5次)改变一次方向的效果最好。可以通过创建第二个Clock组件,并设定其TimerInterval属性为50毫秒来实现这一效果。但是,我们希望能够尝试不同的技术:使用random fraction(随机分数)块,每次调用,它都将返回一个≥0但<1的随机数。创建UpdateAphid过程,并用Clock1.Timer来调用它,如图5-10所示。
图 5-10 添加UpdateAphid过程
块的作用
定时器每次跳动(每秒100次)都将调用UpdateLadybug及UpdateAphid过程。UpdateAphid过程首先生成一个介于0到1之间的随机数,例如0.15,如果该数<0.20(在20%的时间里),蚜虫将改变方向,改变的角度为0到360之间的随机数;如果该数≥0.20(在其余80%的时间里),蚜虫方向保持不变。
瓢虫吃掉蚜虫
下一步,当他们碰撞时,让瓢虫“吃掉”蚜虫。幸运的是,App Inventor提供了ImageSprite组件之间的碰撞检测。问题是:当瓢虫与蚜虫碰撞时,会发生哪些事情?在继续阅读之前,请你停下来想想这个问题。
为了处理瓢虫与蚜虫的碰撞,创建EatAphid过程,其具体步骤如下:
瓢虫的能量水平上升50,来模拟享受美食;
让蚜虫消失(设置其Visible属性为false);
让蚜虫停止移动(设置其Enabled属性为false);
让蚜虫移动到屏幕上任意位置(这与MoleMash中移动地鼠遵循了相同的编码方式)。
请对照图5-11检查您的块。如果你还能想到发生其他事情,比如音效,可以自行添加。
图 5-11 创建EatAphid过程
块的作用
每次调用EatAphid,变量energy增加50,缓解了瓢虫的饥饿。然后,蚜虫的Visible及Enabled属性都被设置为false,看上去像是消失了。最后,产生随机的x、y坐标,并调用Aphid.MoveTo,这样,蚜虫会在一个新位置再次出现(否则,它一出现便会被立即吃掉)。
瓢虫与蚜虫之间的碰撞检测
图5-12显示了在瓢虫与蚜虫之间做碰撞检测的代码。
图 5-12 检测并处理瓢虫与蚜虫之间的碰撞
块的作用
当瓢虫与另一个ImageSprite碰撞时,将调用Ladybug.CollidedWith,参数“other”指向任何与瓢虫发生相撞的ImageSprite。此时,只有蚜虫可以碰撞,但稍后会有青蛙加入进来。我们采用防御性编程方式,即在调用EatAphid之前,要确认碰撞的对象就是蚜虫;此外还要确认蚜虫可见,否则,蚜虫在被吃掉之后而重新出现之前,还会与瓢虫再次碰撞。如果缺少这项确认,隐形的蚜虫会被再次吃掉,并引起能量水平的再次增加,这会让用户感到费解。
提示:防御性编程是一种避免错误的编程方式,当程序被修改时,仍然可以正常工作。在图5-12中,对other=Aphid的检查并不是绝对必要的,因为此时瓢虫可碰撞的唯一对象就是蚜虫,但检查可以防止后续程序的错误:当添加另一个ImageSprite(青蛙)时,如果忘记了修改Ladybug.CollidedWith,程序就会出错。通常来说,程序员修复bug的时间要多余写新代码的时间,所以多花一点时间尝试防御型编程是非常值得的。
蚜虫的回归
最终蚜虫要重新出现,按图5-13所示修改UpdateAphid:仅当蚜虫可见时,令其改变方向(改变一个不可见的蚜虫岂不是浪费时间。);若蚜虫不可见(如刚刚被吃掉),将有1/20(5%)的机会重新出现,或者说会被再吃掉。
图 5-13 修改UpdateAphid使隐形蚜虫起死回生
块的功能
UpdateAphid变得有些复杂,让我们仔细推敲一下:
如果蚜虫可见(这应该是常态,除非刚刚被吃掉),UpdateAphid的行为没有变化,即有20%的几率改变方向;
如果蚜虫不可见(刚被吃掉),则执行“else”部分。首先生成一个随机分数,如果它<0.05(在5%的时间里),蚜虫再次变得可见并且可用,即,有资格被再次吃掉。
因为Clock1.Timer每隔10毫秒调用一次UpdateAphid,而当蚜虫隐形后,只有1/20(5%)的机会恢复可见,因此蚜虫平均重现的时间是200毫秒(1/5秒)。
添加重新启动按钮
在测试蚜虫被吃的新功能时,你可能已经注意到,游戏的确需要一个重新启动按钮。(在创建应用过程中,将应用分解成小的功能模块,完成一块就测试一块,这种开发模式大有裨益。在测试过程中,经常会发现一些被忽略了事情,一边做一边测一边改,比起整个应用完成之后再回来做修改,要容易得多。)在组件设计器中,将Button组件添加在EnergyCanvas下方,改名为“ResetButton”,并设置其Text属性为“重新启动”。
在块编辑器中,创建当RestartButton被点击时的代码,如图5-14所示:
1. 能量水平设置回200;
2. 重新使蚜虫可见并且可用;
3. 重新使瓢虫可用,并将其图片改为活的瓢虫(除非你想要僵尸瓢虫!)。
图 5-14 按下“重新启动”按钮让游戏重新开始
添加青蛙
到目前为止,让瓢虫活着并不难,因此我们需要一个捕食者。就是说我们要添加一个奔向瓢虫的青蛙,如果发生碰撞,瓢虫被吃掉了,游戏结束。
让青蛙追捕瓢虫
首先回到组件设计器,在FieldCanvas上添加第三个ImageSprite组件Frog,设置其Picture属性为相应的图片,interval属性为10,Speed为1,让它的移动速度慢于其他生物。
图5-15显示了Clock1.Timer中调用了新创建的过程。
图 5-15 让青蛙向着瓢虫移动
块的功能
现在你应该可以熟练地使用随机分数来控制事件的发生几率了,这里,青蛙有10%的机会直接直奔向瓢虫。这里用到了三角函数,但别害怕,你不必明白其中的道理!App Inventor提供了大量的数学函数,也包括三角函数。本例中使用的ATAN2(反正切)块,返回值是一个角度,该角度由一组给定的x、y值相对应。(如果你熟悉三角函数,会发现求解ATAN2时所用的y值与你所期望的y值符号正好相反,即y的减法顺序是反的,这是因为在Android的画布上,向下是y坐标的增加方向,这与标准的x-y坐标系正相反。)
让青蛙吃掉瓢虫
现在需要修改碰撞代码,如果瓢虫与青蛙碰撞,能量水平以及能量线都将变为0,且游戏结束,如图5-16所示。
图 5-16 让青蛙吃掉瓢虫
块的功能
在第一个if(瓢虫与蚜虫的碰撞检测)块的基础上,添加了第二个if块,来检测瓢虫与青蛙的碰撞。如果瓢虫和青蛙碰撞,将发生三件事情:
1. 变量energy降为0,瓢虫失去生命力;
2. DisplayEnergy被调用,以清除此前的能量线(并绘制新的空白线);
3. 调用前面写过的GameOver过程,以便让瓢虫停止移动,并将图片改为死瓢虫。
瓢虫回归
RestartButton.Click已经用程序将死瓢虫图片替换成了活的图片。现在,需要添加代码将瓢虫移动到任意位置。(想想看,在新游戏开始时,如果没有移动瓢虫,会发生什么?瓢虫与青蛙的位置关系如何?)图5-17显示了游戏重新启动时用来移动瓢虫的块。
图 5-17 ResetButton.Click的最终版本
块的功能
两个版本的RestartButton.Click之间,唯一的差别就是Ladybug.MoveTo块及其参数。调用了两次内置的随机整数函数,分别用于生成合法的x、y坐标。虽然没有任何设置来防止瓢虫与蚜虫、瓢虫与青蛙的位置上的重叠,但几率起到了决定性作用。
测试:重新启动游戏,并确信瓢虫出现在一个新的任意位置。
添加音效
测试游戏时,你可能注意到:当蚜虫或瓢虫被吃掉时,缺少良好的反馈。要添加音效及触觉反馈,请执行以下操作:
1. 在组件设计器中添加一个Sound组件。设置其Source属性为已上传的声音文件;
2. 进入块编辑器,进行如下操作:
在EatAphid过程中添加Sound1.Vibrate块,参数为100毫秒,以便在蚜虫被吃掉时,设备产生振动;
在Ladybug.CollidedWith中调Sound1.Play,位置在调用GameOver之前,以便当青蛙吃掉瓢虫时发出叫声。
改进
下面这些想法目的是改进游戏,或者让游戏更个性化:
目前,当游戏结束时,青蛙和蚜虫还在移动,这与其Enabled属性有关:在GameOver中将其设置为false,并在RestartButton.Click中重新设置为true;
设置并显示一个分数,来表示瓢虫的存活时间。你可以用Label来显示一个数值,该数值在Clock1.Timer内不断递增;
将EnergyCanvas的Height属性增加为2,以便使能量条更加明显,并在DrawEnergyLine内画两条线,一个在另一个之上。(使用一个过程,而不是复制代码先擦除再重绘能量线,这样做的另一个好处是:如果你需要修改线的粗细、颜色或位置时,只需要修改一处的代码。)
添加背景图和更多音效来渲染气氛,比如用真声或预警声来提示瓢虫能量水平的降低;
让游戏随时间推移而变得越来越难,如增加青蛙的速度,或降低Interval属性值;
从技术上来说,被青蛙吃掉的瓢虫应该消失。改变游戏规则:如果瓢虫被吃,则隐形;如果是饿死,则显示死瓢虫图;
将瓢虫、蚜虫和青蛙的图片换成更适合你个人口味的图片,如霍比特人、半兽人以及邪恶的巫师;或者是反叛星际战斗机、能源舱及帝国战机。
小结
已经有两个游戏被你收入囊中(假设你学习了MoleMash),现在你该知道如何创建自己的游戏了,这是许多新程序员或有志者的目标!具体来说,您学习了:
可以创建多个ImageSprite组件(瓢虫,蚜虫和青蛙),并在它们之间做碰撞检测;
用OrientationSensor可以检测设备的倾斜,而测得的值可用于控制sprite(或你能想到的任何其他对象)的移动;
一个Clock组件可以控制多个发生频率相同(改变瓢虫和青蛙的方向),或通过使用random fraction块来控制频率不同的事件。例如,如果你想在一个周期中,有大约1/4(25%)的时间里会发生某事件,只要将它放在if块中,并设定条件为random fraction的结果<0.25即可;
一个应用中可以使用多个Canvas组件,我们的例子中有两个,一个用于游戏场地,另一个用于变量的图形化显示(而不是用Label显示);
用户自定义过程可以使用参数(如在DrawEnergyLine 中使用的“color”及“length”)来控制其行为,这极大地扩展了过程抽象的能力。
另一个游戏中常用的组件是Ball,与ImageSprite唯一不同的是,它的外观是一个被填充的圆形,而不是一张任意的图片。
资源下载
英汉对照
orientation: 方向
field: 场地
ladybug: 瓢虫
aphid: 蚜虫
frog: 青蛙
energy: 能量
restart: 重新开始
chase: 追逐,奔跑
upload: 上载,上传
file: 文件
interval: 间隔
heading: 前进方向
speed: 速度
timer: 计时器
updat: 更新
angle: 角度
magnitude: 幅度
delete: 删除
procedure: 过程
input: 输入
display: 显示
enable: 使有效
reset: 重置
visible: 可见的
eat: 吃
collide: 碰撞
with: 与...
else: 否则
fraction: 分数