Greenfoot 创意指南(一)
本书旨在帮助你通过实践学习的方法快速学习如何编程游戏和其他交互式应用。与其他文本不同,它们从详细描述语言或开发平台的所有方面开始,我们将只涵盖完成任务所需的精确内容。随着你在书中的进步,你的编程技能和能力将随着你学习动画、碰撞检测、人工智能和游戏设计等主题而增长。基于项目的学习方法是一种经过验证的方法,并在基础教育、中等教育和高等教育中变得突出。它增强了学习过程并提高了知识保留。本书所涉及的主题紧
原文:
zh.annas-archive.org/md5/a463fa25d7de14ca238342316a0d8de4
译者:飞龙
前言
本书旨在帮助你通过实践学习的方法快速学习如何编程游戏和其他交互式应用。与其他文本不同,它们从详细描述语言或开发平台的所有方面开始,我们将只涵盖完成任务所需的精确内容。随着你在书中的进步,你的编程技能和能力将随着你学习动画、碰撞检测、人工智能和游戏设计等主题而增长。基于项目的学习方法是一种经过验证的方法,并在基础教育、中等教育和高等教育中变得突出。它增强了学习过程并提高了知识保留。
本书所涉及的主题紧密跟随我在游戏设计课程中讲解的内容。通过多年的教学经验,我发现基于项目的学习方法可以迅速让学生成功地进行编程,并创造出有趣的游戏和应用。我希望你们也能对在短时间内能完成多少工作感到惊讶。
我们将用 Java 编写我们的游戏。Java 是世界上最受欢迎和最强大的编程语言之一,在金融行业、游戏公司和研究机构中得到广泛应用。我们将使用 Greenfoot(www.greenfoot.org)进行编程——一个交互式 Java 开发环境。这个环境允许新手和经验丰富的程序员快速创建视觉上吸引人的应用。它提供了一个安全的环境进行实验,并允许你在各种平台上分享你的工作。
为了最大限度地利用本书,你应该:
-
在阅读本书的同时打开 Greenfoot 进行编码
-
在完成一个章节后尝试你自己的代码
-
了解一些章节中没有涵盖的细节将在接下来的章节中解决
-
为你的成就感到自豪,并与朋友、家人和 Greenfoot 社区分享。
学习不是一个被动的活动。深入研究每一章,进行实验,加入你自己的独特风格,然后编写一些真正属于你自己的代码。我迫不及待地想看看你能做什么。
本书涵盖的内容
第一章,让我们直接进入…,带你完成一个创建简单游戏的完整教程,包括介绍屏幕、游戏结束屏幕、得分、鼠标输入和声音。这个教程的目的是向你介绍 Greenfoot 基础知识、Java 基础和良好的编程实践。
第二章,动画,讨论了如何在 Greenfoot 中执行动画。动画需要适当的及时图像交换以及在屏幕上的真实运动。阅读给定主题并看到示例后,你将应用学到的动画技术到你在第一章,*让我们直接进入…*中创建的游戏中。
第三章,碰撞检测,讨论了为什么碰撞检测对于大多数模拟和游戏来说是必要的。您将学习如何使用 Greenfoot 的内置碰撞检测机制,然后学习更精确的碰撞检测方法。您将使用基于边界的和隐藏精灵的碰撞检测方法来创建僵尸入侵模拟。
第四章,投射物,讨论了在创意 Greenfoot 中,演员的运动通常可以最好地描述为被发射。足球、子弹、激光、光束、棒球和烟花都是这类对象的例子。您将学习如何实现这种类型的推进运动。您还将通过实现一个综合平台游戏来学习如果存在重力,它如何影响它。
第五章,交互式应用程序设计和理论,讨论了在 Greenfoot 中创建引人入胜和沉浸式的体验,这比将编程效果集合到一个应用程序中要复杂得多。在本章中,您将学习如何通过理解用户选择与结果之间的关系、对用户进行条件化以及将适当复杂度纳入您的作品中来吸引用户。您将看到一个经过验证的迭代开发过程,它有助于您将理论付诸实践。
第六章,滚动和映射世界,讨论了如何创建比单个屏幕能容纳的还要广阔的世界。在本章的开始,您将编写一个滚动探险游戏,到本章结束时,您将把它扩展成一个大型映射游戏。
第七章,人工智能,讨论了尽管人工智能是一个深奥且复杂的话题,但您仍然可以学习一些简单的技术,以在您的世界中创造出具有智能和自主性的演员的错觉。首先,您将学习如何有效地使用随机行为。接下来,您将实现简单的启发式算法来模拟智能行为。最后,您将学习 A*搜索算法,以允许游戏演员在屏幕上的两个位置之间移动时智能地绕过障碍物。
第八章,用户界面,讨论了如何将界面添加到您的 Greenfoot 场景中。在本章中,您将学习如何通过按钮、标签、菜单和抬头显示与用户进行通信。
第九章, Greenfoot 中的游戏手柄,讨论了游戏手柄设备的功能,然后教你如何设置 Greenfoot 以与之协同工作。然后,你将为我们创建的游戏添加游戏手柄支持,该游戏在第一章, 让我们直接进入…,和第二章, 动画中介绍。
第十章, 接下来要探索什么…,为你提供了一个反思在这本书的学习过程中所学技能的机会。然后,我将继续建议你应该尝试的项目,以便继续你的编程和交互式应用程序作者的旅程。
你需要这本书什么
对于这本书,你需要从www.greenfoot.org/door
下载 Greenfoot 并将其安装到你的电脑上。Greenfoot 是免费的,可在 Windows、Mac 和 Linux 上运行。Greenfoot 网站提供了易于遵循的安装说明。安装后,你应该完成在www.greenfoot.org/doc
上找到的六个简单教程。这些教程可以在不到两小时内完成,并将为你提供从这本书中获得最大价值所需的所有知识。
这本书适合谁阅读
如果你准备好探索创意编程的世界,那么你会欣赏这本书中描述的方法、技巧和流程。本书适合所有水平的 Java 程序员(从新手到专家),它系统地引导你了解构建引人入胜的交互式应用程序的关键主题。你将学习如何通过指导编程练习来构建游戏、模拟和动画。
术语
在这本书中,你会找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“正如你所见,我们对Enemy
类进行了一些非常简单的修改。”
代码块设置如下:
private void increaseLevel() {
int score = scoreBoard.getValue();
if( score > nextLevel ) {
enemySpawnRate += 2;
enemySpeed++;
nextLevel += 100;
}
}
当我们希望引起你对代码块中特定部分的注意时,相关的行或项目将以粗体显示:
public void act() {
if( Greenfoot.mouseClicked(this) ) {
AvoiderWorld world = new AvoiderWorld(pad);
Greenfoot.setWorld(world);
}
}
新术语和重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“在新类弹出窗口中点击确定按钮,然后在主场景窗口中点击编译按钮。”
注意
警告或重要提示将以这样的框显示。
小贴士
小贴士和技巧如下所示。
读者反馈
我们欢迎读者的反馈。请告诉我们你对这本书的看法——你喜欢什么或可能不喜欢什么。读者的反馈对我们开发你真正能从中获得最大价值的标题非常重要。
要发送一般反馈,只需将电子邮件发送到<feedback@packtpub.com>
,并在邮件主题中提及书名。
如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 图书的骄傲拥有者,我们有许多事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您在www.packtpub.com
的账户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support
并注册,以便将文件直接通过电子邮件发送给您。
下载本书的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从:www.packtpub.com/sites/default/files/downloads/B00626_ColorImages.pdf
下载此文件。
勘误
尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata
,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过从www.packtpub.com/support
选择您的标题来查看任何现有勘误。
盗版
网络上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上遇到我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过发送链接到疑似盗版材料至<copyright@packtpub.com>
与我们联系。
我们感谢您在保护我们的作者和我们提供有价值内容的能力方面的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过<questions@packtpub.com>
联系我们,我们将尽力解决。
第一章。让我们直接进入…
*“无论你走得有多慢,只要你不停止。” | ||
---|---|---|
–孔子 |
在本章中,你将构建一个简单的游戏,玩家通过鼠标控制角色,尝试躲避迎面而来的敌人。随着游戏的进行,敌人变得越来越难以躲避。这个游戏包含了创建交互式 Greenfoot 应用程序所需的基本元素。具体来说,在本章中,你将学习如何:
-
创建介绍和游戏结束屏幕
-
显示用户得分
-
使用鼠标控制角色的移动
-
播放背景音乐
-
动态生成敌人并在适当的时候移除它们
-
创建游戏关卡
在本章中,我们将学习基本的编程概念,并熟悉 Greenfoot 开发环境。随着你的学习,思考所提出的概念以及你如何在你的项目中使用它们。如果你是 Java 的新手,或者有一段时间没有编写 Java 程序了,请确保花时间查阅可能让你感到困惑的内容。Java 是一种成熟的编程语言,有无数的在线资源可以查阅。同样,本书假设对 Greenfoot 有最低限度的了解。在需要时,请务必查看www.greenfoot.org上的简单教程和文档。尝试代码并尝试新事物——你会很高兴你这么做的。换句话说,遵循本章第一行引用的孔子的建议。
本书中的许多章节都是独立的;然而,大多数章节都依赖于本章。本章提供了创建我们将继续使用并在后续章节中参考的 Greenfoot 应用程序的框架。
Avoider 游戏教程
这个教程主要基于 Michael James Williams 的AS3 Avoider 游戏教程。在那个教程中,你将构建一个游戏,游戏会从屏幕顶部生成笑脸敌人。玩家的目标是避开这些敌人。你避开它们的时间越长,你的得分就越高。我们将使用 Greenfoot 构建相同的游戏,而不是 Flash 和 ActionScript。与 Michael James Williams 的教程一样,我们将从小处着手,逐渐添加功能。我们将经常暂停以考虑最佳实践和良好的编程实践。享受这些学习机会!
我们将首先构建 Avoider 游戏的基本组件,包括初始场景、游戏环境、敌人和英雄。然后,我们将添加额外的功能,例如得分、介绍和游戏结束屏幕以及关卡的概念。
如前言所述,我们假设你已经下载了 Greenfoot 并已安装。如果你还没有,请现在就做。前往www.greenfoot.org获取下载和安装 Greenfoot 的简单说明。当你在那里时,确保你至少熟悉www.greenfoot.org/doc
上提供的所有教程。
基本游戏元素
所有游戏都有一个游戏发生的环境,其中对象进行交互。在 Greenfoot 中,环境由World
类表示,而在环境中交互的对象由Actor
类表示。在本章的这一部分,我们将创建一个世界,向世界添加敌人,并添加一个将由玩家控制的英雄。
创建场景
启动 Greenfoot,通过点击 Greenfoot 菜单栏中的场景然后点击新建…来创建一个新的场景。你会看到图 1中显示的窗口。将文件名输入为AvoiderGame
,然后点击创建按钮。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00238.jpeg
图 1:这是 Greenfoot 的新场景窗口
创建我们的世界
接下来,我们需要为我们的游戏创建一个世界。我们通过在场景窗口中右键单击(或在 Mac 上按 ctrl 键单击)世界类,并在出现的弹出菜单中选择**新建子类…**来完成此操作(参见图 2)。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00239.jpeg
图 2:这是关于在世界类上右键单击以创建子类
在新建类弹出窗口中,将类命名为AvoiderWorld
,选择背景图像类别,然后选择space1.jpg
库图像作为新类的图像。完成这些操作后,弹出窗口应类似于图 3。
小贴士
一旦你将一个图像与新的World
类或Actor
类关联,该图像将被复制到 Greenfoot 项目的images
目录中。我们将在后面的章节中依赖这一点。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00240.jpeg
图 3:这显示了新建类弹出窗口
在新建类弹出窗口中点击确定按钮,然后在主场景窗口中点击编译按钮。现在你应该有一个看起来像图 4中所示的场景。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00241.jpeg
图 4:这显示了编译了 AvoiderWorld 类的 AvoiderGame 场景
现在我们有了自己的世界,名为AvoiderWorld
,我们将很快在其中添加演员。
小贴士
在本章的后面部分,我们将向我们的游戏添加两个World
类的子类——一个用于我们的介绍屏幕,另一个用于我们的游戏结束屏幕。那些说明将被简略。如果你需要关于子类化World
类的详细说明,请务必参考本节。
创建我们的英雄
让我们创建玩家在玩游戏时将控制的角色。Greenfoot 使这变得非常简单。我们将遵循之前创建World
类时使用的相同步骤。首先,在场景窗口中右键单击Actor
类(见图 5),然后选择**新建子类…**菜单项。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00242.jpeg
图 5:此图显示了在 Actor 类上右键单击以继承它
在新类弹出窗口中,将类命名为Avatar
,并选择symbols->skull.png
作为新的类图像。在主场景窗口中,点击编译按钮。
现在,要创建一个敌人,您只需执行与英雄相同的步骤,只是选择symbols->Smiley1.png
作为图像,并将类名选择为Enemy
。同样,完成此操作后,再次点击编译按钮。
现在,您应该有一个看起来像图 6所示的场景。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00243.jpeg
图 6:此图显示了创建世界并添加两个演员后的 Avoider Game 场景
我们刚才做了什么?
Greenfoot 将场景视为包含Actor
的World
。World
的主要职责是从屏幕上添加和删除每个 Actor
,并定期调用每个Actor
的act()
方法。每个Actor
的职责是实现它们的act()
方法来描述它们的行为。Greenfoot 为您提供了实现通用World
和Actor
行为的代码。(您之前已经右键点击过这些实现。)作为一名游戏程序员,您必须为World
和Actor
编写特定的行为代码。您通过继承提供的World
和Actor
类来创建新类,并在其中编写代码。您已经完成了继承,现在是时候添加代码了。
小贴士
查看www.greenfoot.org/files/javadoc/
以了解更多关于World
和Actor
类的信息。
Oracle 在docs.oracle.com/javase/tutorial/java/concepts/
提供了关于面向对象编程概念的优秀概述。如果您认真学习 Java 并编写好的 Greenfoot 场景,您应该阅读这些材料。
添加我们的英雄
最后,我们需要将我们的英雄添加到游戏中。为此,右键单击Avatar
类,从弹出菜单中选择new Avatar()
,将鼠标指针旁边出现的头骨图片拖到屏幕中央,然后点击鼠标左键。现在,在任何黑色背景上右键单击(不要在头骨上右键单击)并选择弹出菜单中的保存世界。
执行此操作将永久将我们的英雄添加到游戏中。如果您在 Greenfoot 的场景窗口中点击重置按钮,您应该仍然看到您放置在屏幕中间的头骨。
使用鼠标作为游戏控制器
让我们在Avatar
类中添加一些代码,这样我们就可以使用鼠标来控制它的移动。双击Avatar
以打开代码编辑器(你也可以右键单击类并选择打开编辑器)。
你将看到一个代码编辑窗口出现,其外观如图 7 所示。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00244.jpeg
图 7:这是我们 Avatar 类的代码
你可以看到我们之前讨论过的act()
方法。因为里面没有代码,所以当我们运行场景时,Avatar
不会移动或显示任何其他行为。我们希望的是让Avatar
跟随鼠标。如果有一个我们可以使用的followMouse()
方法会怎么样?让我们假装有! 在act()
方法中,输入followMouse();
。你的act()
方法应该看起来像图 8。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00245.jpeg
图 8:显示了添加了 followMouse()函数的 act()方法
为了好玩,让我们编译一下看看会发生什么。你认为会发生什么?点击编译按钮来找出答案。你看到了像图 9 中显示的那样的事情吗?
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00246.jpeg
图 9:这是关于在 Greenfoot 中查看编译错误
如果你查看图 9 的底部,你会看到 Greenfoot 为我们提供了一个有用的错误信息,甚至突出显示了有问题的代码。正如我们所知,我们假装方法followMouse()
存在。当然,它不存在。然而,我们很快就会编写它。在整个手册的编写过程中(以及任何 Java 编码过程中),你都会犯错误。有时,你会犯一个“打字错误”,有时,你会使用一个不存在的符号(就像我们之前做的那样)。你还会犯其他一些常见的错误。
注意
帮助!我刚刚犯了一个编程错误!
不要慌张!你可以做很多事情来解决这个问题。我会在这里列出一些。首先,你使用的编码过程可以大大帮助你调试代码(查找错误)。你应该遵循的过程被称为增量开发。只需遵循以下步骤:
-
编写几行代码。(真的!!不要编写更多代码!)
-
保存并编译。
-
运行并测试你的代码。(真的!!试试看!)
-
重复。
现在,如果你遇到错误,它一定是由于你刚刚编写的最后 2-5 行代码造成的。你知道确切的位置在哪里。将此与编写 30 行代码然后测试它们进行比较。你将会有累积的难以找到的错误。以下是一些其他调试技巧:
-
非常仔细地阅读你得到的错误信息。虽然它们可能很晦涩,但它们确实会指向错误的位置(有时甚至给出行号)。
-
有时,你会得到多个、长篇的错误信息。不用担心。只需从顶部开始阅读并处理第一个。通常,通过修复第一个,许多其他问题也会得到解决。
-
如果你实在找不到,让其他人帮你阅读代码。别人能多快地发现你的错误真是令人惊讶。
-
打印一些信息。你可以使用
System.out.println()
来打印变量,并检查你正在查看的代码是否实际在运行。 -
学习如何使用调试器。这是一个非常有用的工具,但超出了本书的范围。了解调试器是什么,并使用它。*Greenfoot 有一个内置的调试器,你可以使用它*。
在极其罕见的情况下,如果 Greenfoot 程序中存在错误,请按照www.greenfoot.org/support
中找到的说明进行报告。
创建followMouse
函数
好的,让我们回到我们的英雄。我们上一次离开我们的英雄(Avatar 类)时,有一个错误,因为实际上没有followMouse()
方法。让我们来修复它。在Avatar
类的act()
方法之后添加以下代码中的方法:
private void followMouse() {
MouseInfo mi = Greenfoot.getMouseInfo();
if( mi != null ) {
setLocation(mi.getX(), mi.getY());
}
}
我们现在有了followMouse()
方法的实现。保存文件,编译 Greenfoot 场景,并尝试运行代码。头骨的图片应该会跟随你的鼠标。如果出了问题,仔细查看调试窗口(如图 9 所示)以查看 Java 给你提供的关于错误的线索。你是不是打错了什么?验证一下你的Avatar
类中的代码是否与图 10 中的代码完全一致。*遵循之前提供的调试提示*。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00247.jpeg
图 10:此图显示了完成followMouse()
方法的 Avatar 类
嘿,等等!我是怎么想出followMouse()
方法的代码的?我是带着这些信息出生的吗?不,我实际上只是查阅了 Greenfoot 文档(www.greenfoot.org/files/javadoc/
),并看到有一个名为MouseInfo
的类。我点击了它,并阅读了它所有的方法。
小贴士
现在去阅读 Greenfoot 文档。实际上它相当简短。只有七个类,每个类大约有 20 个或更少的方法。
代码分解
让我们分解这段代码。首先,我们通过Greenfoot.getMouseInfo()
获取一个表示鼠标数据的对象。然后,我们使用该对象通过getX()
和getY()
获取鼠标的位置,然后使用setLocation(x,y)
设置我们的英雄的x和y位置。我是怎么知道要使用setLocation()
的?再次,它是在Actor
类的 Greenfoot 文档中。这是 Greenfoot 为所有演员提供的一个方法。最后,我们必须包含if(mi != null)
部分,因为如果你不小心将鼠标移动到 Greenfoot 窗口之外,将没有鼠标信息,尝试访问它将导致错误(查看图 10中的代码注释,第 22 行)。
由于followMouse()
方法在act()
方法中被调用,我们的英雄(Avatar 类)将持续移动到鼠标的位置。
小贴士
当在 Greenfoot 中输入方法时,你可以按Ctrl + 空格键,Greenfoot 将显示一个可能尝试编写的潜在方法的列表。从列表中选择一个方法,Greenfoot 将为你自动完成该方法,包括方法参数的占位符。
添加敌人
我们将分两步向我们的游戏添加敌人。首先,我们需要编写Enemy
类的代码,然后我们将向我们的世界AvoiderWorld
添加代码来创建一支永无止境的敌人军队。这两个步骤都非常简单。
敌人代码
双击Enemy
类并更改其act()
方法,使其看起来像以下代码片段:
public void act() {
setLocation(getX(), getY() + 1);
}
记得在Avatar
类中使用过setLocation()
吗?我们在这里再次使用它,每次调用act()
方法时将敌人向下移动一个像素。在 Greenfoot 中,屏幕的左上角是坐标(0,0)。x坐标随着向右移动而增加,y坐标随着向下移动而增加。这就是为什么我们将敌人的x位置设置为当前的x坐标值(我们不会向左或向右移动)以及其y位置设置为当前的y坐标加一(我们向下移动一个像素。)
保存你的Enemy
类,然后编译你的场景。运行场景,右键单击Enemy
类,并在弹出菜单中选择new Enemy()
。将这个敌人添加到屏幕上,并观察它向下移动。
创建一支军队
现在我们已经完成了Enemy
类的编写,我们可以用它来创建一支军队。为此,我们将向AvoiderWorld
类的act()
方法中添加代码。通过双击AvoiderWorld
或右键单击它并在弹出菜单中选择打开编辑器来打开AvoiderWorld
的编辑器。如果你查看AvoiderWorld
的代码,你会注意到 Greenfoot 不会自动为你创建act()
方法。没问题,我们只需添加它。在AvoiderWorld
中放入以下代码:
public void act() {
// Randomly add enemies to the world
if( Greenfoot.getRandomNumber(1000) < 20 ) {
Enemy e = new Enemy();
addObject(e, Greenfoot.getRandomNumber(getWidth()-20)+10, -30);
}
}
act()
方法首先检查一个在 0 到 1000 之间(包括 0 但不包括 1000)随机生成的数字是否小于 20。从长远来看,这段代码将在act()
方法被调用的 2%的时间内运行。这足够了吗?嗯,act()
方法通常每秒调用 50 次(范围从 1 到 100,取决于速度滑块的位置),所以 2%的 50 次是 1。因此,平均每秒将创建一个敌人。这对于我们游戏的起始级别来说感觉是合适的。
在if
语句内部,我们创建一个敌人并将其放置在世界的特定位置,使用addObject()
方法。addObject()
方法接受三个参数:要添加的对象、对象的x坐标和对象的y坐标。y坐标是恒定的,选择它使得新创建的敌人从屏幕顶部开始,并随着它缓慢向下移动而出现。x坐标更复杂。它是动态生成的,以便敌人可以出现在屏幕上的任何有效的x坐标。以下是我们正在讨论的代码:
Greenfoot.getRandomNumber( (getWidth() – 20) + 10, -30);
图 11展示了生成的x坐标值的范围。在这个图中,矩形代表给定代码的x坐标可能值的集合。在 Greenfoot 中,为屏幕坐标生成值范围的这种方法是常见的。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00248.jpeg
图 11:这是代码生成的 x 坐标值的范围
编译并运行场景;你应该会看到一个连续的敌人洪流沿着屏幕向下移动。
无界世界
运行场景后,你会注意到敌人最终会堆积在屏幕底部。在 Greenfoot 中,你可以创建有界(演员不允许穿过屏幕边界)和无界(演员允许退出屏幕)的世界。默认情况下,Greenfoot 创建的是有界世界。然而,将世界改为无界非常容易。双击AvoiderWorld
以打开代码编辑器。找到以下代码行:
super(600, 400, 1);
将前面的代码更改为以下代码行:
super(600, 400, 1, false);
查看 Greenfoot 文档中的World
类,我们会注意到有两个构造函数(有关这些构造函数的详细信息,请参阅www.greenfoot.org/files/javadoc/greenfoot/World.html
):一个接受三个参数,另一个接受四个。具有四个参数的构造函数与接受三个参数的构造函数具有相同的参数,再加上一个额外的boolean
参数,表示世界是有界还是无界。我们的代码更改添加了第四个布尔参数并将其设置为false
(世界中没有边界。)
现在,编译并运行场景。敌人会按照要求从屏幕底部掉落。
所有这些敌人都去哪里了?我们将在下一节中处理这个问题。
内存管理
在 Greenfoot 应用程序中,你会创建成百上千的演员。当我们完成一个演员,比如当它被杀死或离开屏幕时,我们希望移除该对象,并且它不再消耗任何系统资源。Java 通过一个称为垃圾回收的方法来管理内存资源。使用此方法,Java 试图自动确定你是否不再需要演员,如果你不需要,它将删除该演员并释放与其相关的所有资源。在 Greenfoot 中,你可以通过使用removeObject()
方法从World
中移除演员来让 Java 知道你已经完成了演员。这就是我们在成功避开它并且它已经离开屏幕后想要对Enemy
演员做的事情。
在敌人离开屏幕后,移除Enemy
的最方便的地方是在Enemy
类本身中。将以下代码作为Enemy
类中act()
方法内的最后一行代码添加:
checkRemove();
现在,我们需要添加checkRemove()
方法。将此方法的定义放在act()
方法下方。以下是定义:
private void checkRemove() {
World w = getWorld();
if( getY() > w.getHeight() + 30 ) {
w.removeObject(this);
}
}
你的Enemy
类的代码应该看起来像图 12中所示的那样。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00249.jpeg
图 12:这显示了如何添加代码以移除如果敌人从屏幕底部移出
现在,编译并运行场景。敌人像以前一样从屏幕底部落下,但你可以放心,它们很快就会从世界中移除,垃圾回收也会进行。
你的作业
学习不是被动的,你真的需要参与这个过程。在继续本章的下一部分之前,你应该:
-
确保你的 Avoider Game 版本可以正常工作,在 Greenfoot 的主应用程序菜单中点击场景,然后选择**另存为…**来创建 Avoider Game 的实验副本。让我们把这个实验副本命名为
AvoiderGameIExperimentation
。 -
在你的实验副本上玩玩。改变敌人的出生率。改变敌人下降的速度。
-
将
turn(5);
添加到Enemy
类的act()
方法中。编译并运行。发生了什么?尝试用不同的值代替5
作为turn()
的输入参数。
如果事情变得太疯狂,删除你的实验副本,并从我们的原始 Avoider Game 创建一个新的副本来玩耍。没有造成伤害,也没有任何不当行为。
小贴士
在整本书中,采取这种通过实验代码的方法。在玩耍的过程中会发生很多学习。思考如何更改代码的行为本身就会给你的大脑提供一种新的处理和理解它的方式。在受控环境中犯错误将更好地为你准备以后处理错误。你将开始熟悉 Greenfoot 的错误信息。
接下来…
到目前为止做得很好!我们已经建立了游戏的基础,接下来我们将添加一些东西,比如介绍屏幕、游戏结束屏幕和分数,使它看起来和感觉更像一个游戏。
使其成为一个游戏
在本节中,我们将添加一个游戏结束屏幕、一个介绍屏幕和一些背景音乐。但在我们做所有这些之前,我们需要知道我们的英雄何时接触到敌人。这将是我们结束游戏的提示。确定两个角色何时接触的行为称为碰撞检测。碰撞检测用于判断子弹是否击中敌人,玩家在跳跃后是否落在平台上,或者判断一片落叶是否落在地面上。我们将在下一节讨论这个重要话题,并在接下来的章节中花费大量时间讨论。
检测碰撞
Greenfoot 提供了几个Actor
方法,您可以使用它们来确定您是否接触到了另一个Actor
。这些方法没有特定的顺序,包括:getIntersectingObjects()
、getNeighbors()
、getObjectsAtOffset()
、getObjectsInRange()
、getOneIntersectingObject()
和getOneObjectAtOffset()
。它们都提供了确定碰撞的不同方法。对于我们的游戏,我们将使用getOneIntersectingObject()
。此方法的原型如下:
protected Actor getOneIntersectingObject(java.lang.Class cls)
此方法接受一个参数,即您想要检查碰撞的对象类别。此方法将碰撞定义为边界框;边界框是能够包围图形中所有像素的最小矩形。此方法既高效又快速,但不是最精确的。在图 12中,我们可以看到一幅头骨的图片和一幅笑脸的图片。尽管这两幅图片的像素没有重叠,但我们可以看到它们的边界框是重叠的;因此,getOneIntersectingObject()
会报告这两个角色正在接触。在第三章碰撞检测中,我们将探讨更高级的碰撞检测方法。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00250.jpeg
图 13:此图显示了两个角色的边界框
带着这些新信息,我们将向我们的Avatar
类添加碰撞检测。如果我们的英雄接触到敌人之一,我们将将其从游戏中移除。(在本章的后面部分,我们将在移除我们的英雄后显示游戏结束屏幕。)双击Avatar
类以打开其编辑窗口。将其act()
方法更改为以下内容:
public void act() {
followMouse();
checkForCollisions();
}
然后,在act()
方法下添加此checkForCollisions()
方法的定义:
private void checkForCollisions() {
Actor enemy = getOneIntersectingObject(Enemy.class);
if( enemy != null ) {
getWorld().removeObject(this);
Greenfoot.stop();
}
}
Avatar
类应该看起来像图 14 中所示的那样。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00251.jpeg
图 14:添加了碰撞检测的Avatar
类。
让我们仔细检查checkForCollisions()
方法中正在发生的事情。我们首先调用getOneIntersectionObject()
并将它的返回值保存在变量enemy
中。如果这个对象没有接触到任何敌人,这个变量将是null
,在这种情况下,if
语句中的表达式将评估为false
,我们不会执行其内部的语句。否则,我们接触到了一个类型为Enemy
的对象,并将执行if
语句的内容。
if
语句中只有两行代码。在第一行中,我们使用getWorld()
方法,该方法在Actor
类中实现,来获取我们所在世界的World
实例的引用。我们不是将引用保存在一个变量中,而是立即调用World
的removeObject()
方法,将关键字this
作为参数传递以移除我们的英雄。最后,我们使用Greenfoot
实用类中的stop()
方法暂停我们的游戏。
现在,编译并运行这个场景。敌人应该从屏幕顶部流下来并在底部退出。你应该能够通过移动鼠标来控制英雄,它是Avatar
类的一个实例。如果我们的英雄接触到任何一个敌人,游戏应该停止。
添加游戏结束屏幕
首先,你需要使用你喜欢的图形设计/绘图程序,比如 GIMP、CorelDRAW、Inkscape、Greenfoot 内置的图形编辑器,甚至是 Windows Paint,绘制整个游戏结束屏幕。我使用 Adobe Illustrator 创建了图 15中显示的屏幕。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00252.jpeg
图 15:我的 AvoiderGame 游戏结束屏幕;尝试设计你自己的。
无论你使用什么来绘制你的图像,确保你可以以PNG
或JPG
格式保存它。其大小应该是 600 x 400(与你的世界大小相同)。将此图像保存在你的AvoiderGame
场景的images
文件夹中。
使用与创建AvoiderWorld
相同的步骤(避免者游戏教程部分),创建另一个世界;命名为AvoiderGameOverWorld
,并将你之前创建的图像与之关联。在你的场景的世界类区域,你应该看到图 16中所示的内容。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00253.jpeg
图 16:添加 AvoiderGameOverWorld 后的世界类区域
场景切换
现在,我们想要显示游戏结束屏幕,如果我们的英雄接触到敌人。为此,我们需要执行以下三个步骤:
-
检测我们与敌人发生碰撞,然后通过调用一个方法通知我们的世界,
AvoiderWorld
,游戏已经结束。 -
在我们的
AvoiderWorld
类中,我们需要实现Avatar
将用来信号世界末日结束的游戏结束方法。 -
在我们的游戏结束方法中,将世界设置为
AvoiderGameOverWorld
,而不是AvoiderWorld
。
让我们从第一步开始。之前,在本节的检测碰撞子节中,你编写了代码,如果英雄接触到敌人,就会从游戏中移除英雄。这段代码包含在checkForCollisions()
方法中。为了实现第一步,我们需要将该方法更改为以下方法:
private void checkForCollisions() {
Actor enemy = getOneIntersectingObject(Enemy.class);
if( enemy != null ) {
AvoiderWorld world = (AvoiderWorld) getWorld();
world.endGame();
}
}
唯一的区别是if
语句内的代码。我希望你能理解我们现在要求世界结束游戏,而不是移除英雄对象。可能让人困惑的部分是将AvoiderWorld
替换为World
以及添加(AvoiderWorld)
部分。问题是,我们将在AvoiderWorld
中实现endGame()
,而不是World
。因此,我们需要一种方法来指定getWorld()
的返回值将被视为AvoiderWorld
,而不仅仅是普通的World
。用 Java 术语来说,这被称为类型转换。
现在,让我们看看第二步和第三步。这是你需要添加到AvoiderWorld
中的代码。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00254.jpeg
图 17:这显示了添加到 AvoiderWorld 中的 endGame()方法
我们已经更改并添加了最小量的代码,但如果你仔细跟随着,你应该能够保存、编译并运行代码。看到我们的英雄接触到敌人时出现的游戏结束界面吗?(如果没有,请回过头来重新追踪你的步骤。你可能输入了错误的内容。)
注意
三个 P:计划,计划,再计划
编程是复杂的事情。当你有一个问题要解决时,你不想只是坐下来,对着电脑开始乱敲,直到你敲出一个解决方案。你想要坐下来,用笔和 ePad(在我那个时代是笔和纸)来规划。我在编写显示游戏结束界面的三个步骤时给你提供了一个小的例子。帮助你设计解决方案的最佳方法之一是自顶向下的设计(也称为分而治之)。
在自顶向下的设计中,你从非常高的层面开始思考问题的解决方案,然后反复将这个解决方案分解成子解决方案,直到子解决方案变得小且易于管理。
添加“再玩一次”按钮
游戏结束界面很棒,但我们不想整天都盯着它看。好吧,那么让我们设置一下,点击游戏结束界面就可以重新开始游戏。AvoiderGameOverWorld
需要持续检查鼠标是否被点击,然后将世界状态重置为AvoiderWorld
,这样我们就可以再次玩游戏了。查看 Greenfoot 文档,我们可以看到mouseClicked()
函数。让我们在AvoiderGameOverWorld
的act()
方法中使用这个方法,以及更改世界状态的代码。将以下代码添加到AvoiderGameOverWorld
中:
public void act() {
// Restart the game if the user clicks the mouse anywhere
if( Greenfoot.mouseClicked(this) ) {
AvoiderWorld world = new AvoiderWorld();
Greenfoot.setWorld(world);
}
}
这段代码应该对你来说非常熟悉。if
语句内的代码几乎与我们添加到AvoiderWorld
类中的endGame()
方法中的代码相同,只是这次我们创建并切换到AvoiderWorld
。
新的部分是检查用户是否点击了屏幕上的任何位置。如果用户刚刚点击了其参数中提供的对象,Greenfoot.mouseClicked()
方法返回 true。我们提供了 this
变量,它代表 AvoiderGameOverWorld
实例的整体。
编译并运行。做得好!我们的游戏进展得很顺利!
添加一个介绍屏幕
添加一个介绍屏幕非常简单,我们只需要执行我们在创建游戏结束屏幕时所做的许多相同步骤。首先,我们需要使用你想要的任何图形编辑器程序创建一个介绍屏幕图像。我创建的图像显示在图 18中。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00255.jpeg
图 18:我们游戏的介绍屏幕图像。
确保图像是 PNG 或 JPG 格式,并且像素大小为 600 x 400。将此图像保存在你的 AvoiderGame
场景的 images
文件夹中。
创建一个新的世界(通过继承 World
),命名为 AvoiderGameIntroScreen
,并将其与刚刚创建的图像关联起来。当你完成这个步骤后,你的场景的世界类区域应该看起来像图 19中所示的截图。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00256.jpeg
图 19:这些都是你在 AvoiderGame 中创建的所有世界。
设置初始屏幕
我们显然希望我们的新介绍屏幕在玩家第一次开始游戏时首先显示。要选择 AvoiderGameIntroScreen
世界作为我们的起始 World
,我们需要在世界类区域中右键单击它,并在出现的弹出窗口中选择 new AvoiderGameIntroScreen()
菜单选项(见图 20)。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00257.jpeg
图 20:这是关于选择我们的起始世界
让我们确保一切连接正确。编译并运行你的 Greenfoot 应用程序。你应该从你刚刚创建的介绍屏幕开始,但无法做太多其他事情。我们现在将解决这个问题。
添加“播放”按钮
我们将重复我们在从游戏结束屏幕实现游戏重启时所做的完全相同的步骤。
将以下代码添加到 AvoiderGameIntroScreen
:
public void act() {
// Start the game if the user clicks the mouse anywhere
if( Greenfoot.mouseClicked(this) ) {
AvoiderWorld world = new AvoiderWorld();
Greenfoot.setWorld(world);
}
}
这段代码应该对你来说非常熟悉。这正是我们添加到 AvoiderGameOverWorld
类中的代码。
编译并运行。享受乐趣。看看你能坚持多久!
到目前为止一切顺利,但确实缺少一些关键的游戏元素。
添加背景音乐
在本教程的这一部分,你需要在网上搜索一首你希望在游戏中播放的歌曲(.mp3
)。
注意
获取音乐
每次你在游戏中添加资源(音乐或图形)时,确保你这样做是合法的。互联网上有许多网站提供免费使用提供的音乐或图片。永远不要使用专有音乐,并且始终引用你获取资源的来源。我从 newgrounds.com 获取了添加到游戏中的音乐,并在我的代码中为作者提供了信用。
我们只希望在开始玩游戏时播放音乐,而不是在介绍或游戏结束屏幕上播放。因此,我们在显示 AvoiderWorld
时开始播放音乐,在显示 AvoiderGameOverWorld
之前关闭它。我们只想播放一次音乐,所以不想在 act()
方法中添加播放音乐的代码——想象一下那样做的噪音!我们需要的是一个在对象创建时只被调用一次的方法。这正是类的构造函数所提供的。(如果你需要回顾类和对象是什么,请参阅 What have we just done? 部分的资料框)
注意
构造函数是什么?
在 Java 编程(以及其他面向对象的语言)中,我们在类中编写代码。一个类描述了我们想要在程序中创建的对象的方法和属性。你可以把类看作是构建对象的蓝图。例如,我们的 Enemy
类描述了出现在我们的 Avoider 游戏中的每个敌人对象的行为和属性。每个 类 都有一个 构造函数,它执行每个创建的对象所需的全部初始化。你可以很容易地识别类的构造函数。构造函数的名称与它们所在的类完全相同,并且没有返回类型。作为一个快速测试,找到我们的 AvoiderWorld
类中的构造函数。找到了吗?
我们每次创建新对象时都会调用构造函数。在 Greenfoot 中,右键单击 Enemy
类,你会看到顶部的菜单选项是 new Enemy()
。Enemy()
部分是构造函数。new
关键字创建新对象,而 Enemy()
初始化该新对象。明白了吗?
以下是一些你应该阅读的好资源,以了解更多关于构造函数函数的信息:
docs.oracle.com/javase/tutorial/java/javaOO/constructors.html
java.about.com/od/workingwithobjects/a/constructor.htm
编写音乐代码
现在我们知道了代码应该放在哪里(大家说 constructor
),我们需要知道要写什么代码。Greenfoot 提供了一个用于播放和管理音乐的类,称为 GreenfootSound
。这个类使得播放音乐变得非常简单。在我向你展示要放入构造函数中的代码之前,你应该查看 GreenfootSound
的文档,看看你是否能弄清楚要写什么。
小贴士
不,真的!去阅读文档!自己尝试去做真的会对你有帮助。
这里是你需要添加到 AvoiderWorld
构造函数中的代码。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00258.jpeg
图 21:这是 AvoiderWorld 的构造函数
分析音乐代码
让我们看看AvoiderWorld
构造函数中的每一行代码。首先,你有调用超类构造函数的调用,正如之前所述,这是为了正确初始化你的游戏世界。接下来,我们有这一行:
bkgMusic = new GreenfootSound("sounds/UFO_T-Balt.mp3");
这创建了一个新的GreenfootSound
对象,并将对它的引用保存在bkgMusic
变量中。你需要更改前面的代码,而不是使用sounds/UFO_T-Balt.mp3
,你需要使用一个字符串来给出你下载以播放的音乐文件名(你需要将音乐保存在你的 Greenfoot 项目文件夹中的sounds
文件夹中)。我们还需要声明在构造函数中使用的bkgMusic
变量。为此,你需要在类的顶部添加一个变量声明,如图 22 所示。通过在类的顶部声明变量,它将可以访问你的类中的所有方法。这将在我们添加停止播放音乐的代码时变得很重要。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00259.jpeg
图 22:这显示了 AvoiderWorld 类中 bkgMusic 变量的声明
我们接下来要讨论的代码行是这一行:
bkgMusic.playLoop();
这行代码开始播放音乐,并在结束时重新开始播放。如果我们只做了bkgMusic.play()
,那么这首歌只会播放一次。
构造函数中的最后一行是非常重要的一行,它是 Greenfoot 自动添加的。记得在本书的添加我们的英雄部分,我指导你将Avatar
类(我们的英雄)的实例放置在屏幕中央,右键点击,并选择保存世界菜单选项吗?当你这样做时,Greenfoot 创建了这个prepare()
方法。如果你查看这个方法的内容,你会看到其中包含了创建Avatar
对象并将其添加到屏幕的代码。然后,它在构造函数中添加了对prepare()
的调用。如果你再次选择保存世界菜单选项,这个prepare()
方法将会更新。
好的,保存、编译并运行。它工作了吗?如果没有,回去找到错误。
停止音乐
如果你运行了你的代码,你在游戏中会有音乐,但是当你死亡并进入游戏结束屏幕时,音乐并没有关闭。我们必须在显示AvoiderGameOverWorld
之前显式地关闭音乐。这很简单!我们只需要在之前添加到AvoiderWorld
中的endGame()
方法的开头添加以下代码行:
bkgMusic.stop();
现在,保存、编译并运行。它应该按照计划工作。
注意
私有、受保护和公共
Java 关键字private
、protected
和public
修改了 Java 中变量、方法或类的访问方式。良好的编程实践规定,你应该将所有类的实例变量设置为private
,并且只通过方法访问该变量。对于方法,你希望只在你自己的private
类中访问它们;否则,将其设置为public
。关键字protected
用于使方法对类的子类可用,但对外部类不可用。有关更多信息,请参阅以下链接:
你的任务
在继续之前执行以下操作:
-
一旦显示游戏结束屏幕,播放音乐。你打算播放欢快的音乐来提振玩家的精神,还是播放悲伤和忧郁的音乐来真正打击他们?确保在切换到
AvoiderWorld
之前将其关闭。 -
我们敌人的动作相当平淡。你能让它变得更有趣吗?一些想法是让敌人角色具有可变速度,左右漂移,或从顶部或底部进入。你将想出什么?
在尝试这些挑战之前,请记住创建AvoiderGame
的备份副本。
接下来…
几乎完成了!我们已经构建了游戏的基础,接下来将添加一些内容使其更具挑战性。
提高可玩性
在本章的最后部分,我们将添加代码来提高游戏的可玩性。首先,我们将添加一个分数。接下来,我们需要随着时间的推移增加游戏的挑战性。随着玩家在游戏中的进步,我们希望提高挑战性;我们将添加一个等级系统来实现这一点。
游戏评分
我们的游戏正在发展;然而,我们需要一种方法来判断我们在游戏中的表现如何。有许多方法可以判断游戏表现,例如,完成的关卡、时间、进度等——但最常见的方法是为玩家分配分数。我们将在游戏中添加一个评分系统,奖励玩家避免的敌人数量。
添加 Counter 类
在游戏中计数并在屏幕上显示计数是如此常见,以至于 Greenfoot 为你提供了一个Counter类。要访问此类,你需要将其导入到你的场景中。为此,在 Greenfoot 的主菜单中选择编辑,然后选择导入类…子菜单选项。你将看到一个窗口,就像图 23 中显示的那样。确保在左侧选中Counter框,然后点击导入按钮。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00260.jpeg
图 23:这是 Greenfoot 的导入类窗口
这将把Counter
类添加到你的演员类列表中,以便在我们的游戏中使用,如图 24 所示。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00261.jpeg
图 24:你的场景窗口中的 Actor 类部分现在包括 Counter 类
我们希望分数能立即在游戏中显示。在 Greenfoot 网站上的教程 4 (www.greenfoot.org/doc/tut-4
) 中,你被介绍了“拯救世界”,以便自动将Actor
放置在你的世界中。我将描述如何手动将Actor
放置在你的世界中;具体来说,你将向你的AvoiderWorld
世界添加一个Counter
类的实例。
我们讨论了 Greenfoot 已经在你的AvoiderWorld()
构造函数中添加了对prepare()
方法的调用。在AvoiderWorld
类中找到这个方法的定义。将其更改为以下代码:
private void prepare() {
Avatar avatar = new Avatar();
addObject(avatar, 287, 232);
scoreBoard = new Counter("Score: ");
addObject(scoreBoard, 70, 20);
}
这个方法的前两行已经存在。最后两行在游戏屏幕上放置了一个分数显示。scoreBoard = new Counter("Score: ");
这段代码创建了一个带有标签Score:
的新Counter
对象,并将其引用存储在scoreBoard
变量中(我们尚未声明这个变量,但很快就会声明。)下一行代码将我们的Counter
添加到游戏屏幕的左上角。
最后,我们需要在类的顶部声明scoreBoard
变量。在构造函数上方添加private Counter scoreBoard;
,如图图 25所示。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00262.jpeg
图 25:在 AvoiderWorld 类中声明 scoreBoard 变量。
编译、运行并测试你的场景。
随着时间的推移提高分数
我们需要做最后一件事。我们需要在scoreBoard
变量上调用setValue()
来随时间增加我们的分数。一个我们可以这样做的地方是在AvoiderWorld
中创建敌人时。思考一下,对于每个创建的敌人,你将得到一些分数,因为你最终需要避开它。以下是你在AvoiderWorld
中的act()
方法应该如何更改:
public void act() {
// Randomly add enemies to the world
if( Greenfoot.getRandomNumber(1000) < 20) {
Enemy e = new Enemy();
addObject( e, Greenfoot.getRandomNumber(getWidth()-20)+10, -30);
// Give us some points for facing yet another enemy
scoreBoard.setValue(scoreBoard.getValue() + 1);
}
}
我所做的唯一改变是添加了关于分数的注释,并在scoreBoard
上添加了对setValue()
的调用。这段代码使用getValue()
获取当前分数,将其加 1,然后使用setValue()
设置新值。Counter
类的典型用法也在Counter
类的顶部注释中提供。查看它!
编译你的AvoiderGame
场景并尝试运行。你是否得到了增加的分数?
添加等级
到目前为止,我们的游戏并不具有很大的挑战性。我们可以做的一件事是,让游戏随着时间的推移变得更加具有挑战性。为此,我们将在 Avoider 游戏中加入等级的概念。我们将通过定期增加敌人生成的速率和敌人移动的速度来增加游戏的挑战性。
增加生成速率和敌人速度
在AvoiderWorld
中添加两个变量,enemySpawnRate
和enemySpeed
,并给它们设置初始值;我们将使用这两个变量来增加难度。你的AvoiderWorld
类的顶部应该看起来像图 26。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00263.jpeg
图 26:这显示了 AvoiderWorld 中的变量
根据得分增加难度
接下来,我们需要添加一个方法,根据玩家的得分增加游戏的难度。为此,我们需要将以下方法添加到AvoiderWorld
中:
private void increaseLevel() {
int score = scoreBoard.getValue();
if( score > nextLevel ) {
enemySpawnRate += 2;
enemySpeed++;
nextLevel += 100;
}
}
在increaseLevel()
中,我们引入了一个新的变量nextLevel
,我们需要在AvoiderWorld
类的顶部添加其声明。以下是您需要添加到enemySpawnRate
和enemySpeed
变量声明旁边的声明:
private int nextLevel = 100;
从increaseLevel()
函数中的代码可以看出,随着玩家得分的增加,我们同时增加了enemySpawnRate
和enemySpeed
。我们需要做的最后一件事是在AvoiderWorld
的act()
方法中使用enemySpawnRate
和enemySpeed
变量来创建敌人,并从AvoiderWorld
的act()
方法中调用increaseLevel()
。以下是新的act()
方法:
public void act() {
// Randomly add enemies to the world
if( Greenfoot.getRandomNumber(1000) < enemySpawnRate) {
Enemy e = new Enemy();
e.setSpeed(enemySpeed);
addObject( e, Greenfoot.getRandomNumber(getWidth()-20)+10, -30);
// Give us some points for facing yet another enemy
scoreBoard.setValue(scoreBoard.getValue() + 1);
}
increaseLevel();
}
实现敌人速度增加
我现在很想大声喊出编译并运行!,但还有一个细节。在act()
方法中,我们使用e.setSpeed(enemySpeed);
这一行来改变敌人的速度;然而,我们从未在Enemy
类中实现过该方法。此外,我们还需要对Enemy
类进行一些修改,以便使用新设置的速度。
图 27给出了Enemy
类的完整代码。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00264.jpeg
图 27:这显示了完成的 Enemy 类
如您所见,我们对Enemy
类进行了一些非常简单的修改。我们添加了setSpeed()
方法,该方法简单地接受一个整数参数,并使用该值来设置在类顶部声明的speed
变量。在act()
方法中,我们使用speed
变量的值在setLocation()
调用中;我们不断地将speed
添加到当前的y坐标。
编译并运行,享受你的新游戏!
你的任务
由于这是 Avoider 游戏的结束说明。我将给你一些挑战性任务。祝你好运!尝试实现以下内容:
-
一旦玩家的得分超过 600,除了我们现在拥有的敌人外,还需要添加一个新敌人。新敌人应该在外观上与我们的现有敌人非常不同。如果你觉得可以,让新敌人的移动方式也与现有敌人不同。
-
定期生成一个提供英雄特殊能力的道具。例如,这个道具可以使英雄暂时无敌,允许英雄杀死三个敌人,或者缩小英雄的大小,使其更容易躲避敌人。
-
在游戏结束屏幕上显示玩家的最终得分。
这些挑战肯定需要一些时间,你不应该感到必须尝试它们。我只是想给那些真正感兴趣的人提供一个继续在 Avoider 游戏上工作的方法。你不需要完成这些挑战就可以进入下一章。
接下来…
恭喜!你做到了!祝你好玩。玩你的新游戏。
摘要
本章展示了如何制作一个有趣且引人入胜的游戏。我们包含了鼠标控制、一个英雄角色、敌人、得分以及介绍和游戏结束屏幕。
由于本书假设您在 Greenfoot 中已有一些工作经验,因此本章也起到了刷新您对如何在 Greenfoot 中编程的记忆的作用。
在接下来的章节中,我们将探讨 Greenfoot 中的高级编程概念,这些概念将使您能够创建有趣、创新且引人入胜的应用程序。这些章节将假设您已经掌握了本章中的内容。
第二章:动画
*“没有欲望的学习会损害记忆,它所吸收的什么也保留不住。” | ||
---|---|---|
–莱昂纳多·达·芬奇 |
在 Greenfoot 场景中通过处理键盘或鼠标事件并适当地使用setLocation()
方法来移动角色相对简单。然而,我们可以做得更好。通过进一步动画化我们的角色,我们可以赋予它们生命。我们可以给我们的玩家/用户一个充满活力、生机勃勃的世界的错觉。
从本质上讲,编程动画是一种幻觉艺术。通过在适当的时候添加微小的动作或图像变化,我们诱使用户相信我们的创作不仅仅是屏幕上的静态像素。在本章中,你将学习以下用于动画 Greenfoot 角色的技术:
-
图像交换和移动
-
定时和同步
-
缓动
Greenfoot 是一个创建交互性和吸引人的应用程序的绝佳平台,您可以在互联网上共享或用作桌面应用程序。正是您创建这些类型应用程序的愿望使您来到这里,根据达芬奇的看法,正是这种愿望将帮助您无限期地保留这本书中的信息。
重温避免者游戏
在本章中,我们将继续完善我们在第一章“让我们直接进入…”中创建的避免者游戏。如果您跳过了那一章,或者只是想从一份全新的副本开始,您可以从 Packt Publishing 网站上的本书产品页面下载这个游戏的代码:www.packtpub.com/support
。我在本章中略过的大部分概念很可能在前一章中已经详细讨论过;如有需要,请务必参考那一章。现在,打开 Greenfoot 中的AvoiderGame
场景并继续阅读。
图像交换和移动
图像交换是古老的动画技术。也许在小时候,你在纸垫的角落画了一个棒状人物,并在每一页上稍作改变。当你快速翻阅页面时,你的棒状人物就活了起来。图 2展示了我尝试的这种动画。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00265.jpeg
图 1:这展示了传统的棒状人物动画
在 Greenfoot 中,我们将通过快速切换图像来动画化角色,以达到图 1中显示的纸动画相同的效果。我们将学习如何使用 Greenfoot 的setImage()
方法来实现这一点。
使用 setImage()
当我们通过从Actor
类或我们的Actor
子类之一派生新Actor
时,Greenfoot 会提示我们输入新类的名称并为其选择一个图像。Greenfoot 还允许我们在场景运行时动态设置Actor
对象的图像,使用 Greenfoot 的Actor
类提供的setImage()
方法。以下是从 Greenfoot 文档中摘录的内容:
public void setImage(java.lang.String filename)
throws java.lang.IllegalArgumentException
Set an image for this actor from an image file. The file may be in jpeg, gif or png format. The file should be located in the project directory.
Parameters:
filename - The name of the image file.
如你所见,setImage()
允许我们通过指定任何JPEG
、GIF
或PNG
文件的路径来设置演员的图像。默认情况下,Greenfoot 会在你的 Greenfoot 项目中包含的images
文件夹中查找。你应该将你将在场景中使用的所有图像放置在这个文件夹中。
让我们使用这种方法来为 Avoider 游戏中的敌人添加动画效果。
让敌人不那么开心
Avoider 游戏中的敌人太开心了。让我们让它们变得悲伤和失望,因为它们意识到我们的英雄将避开它们。
查找资源
我们需要做的第一件事是找到一组合适的笑脸图像,我们可以将其切换到我们的场景中的Enemy
演员。通常,你需要使用 Greenfoot 内置的图像编辑器或像 GIMP 或 Adobe Illustrator 这样的工具来创建自己的图像资源,或者你可以从互联网上下载图像;有很多免费图像可供选择。幸运的是,Greenfoot 的默认安装已经包含了我们需要的所有图像。在 OSX 上,图像位于以下文件夹中:
/Applications/Greenfoot 2.3.0/Greenfoot.app/Contents /Resources/Java/greenfoot/imagelib/symbols
在 Windows 上,图像位于以下文件夹中:
C:/Program Files/Greenfoot/lib/greenfoot/imagelib/symbols
为了方便起见,我已经将所有笑脸图像放在了这本书的文件存储库中,可以在 Packt Publishing 网站上找到,网址为www.packtpub.com/sites/default/files/downloads/0383OS_ColoredImages.pdf
。
你需要将文件smiley1.png
、smiley3.png
、smiley4.png
和smiley5.png
放入你的AvoiderGame
目录下的images
文件夹中。完成此操作后,你的图像文件夹应包含图 2中显示的文件。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00266.jpeg
图 2:这是你的 AvoiderGame 项目中的图像文件夹内容。
现在我们已经有了可用的图像,我们可以开始编码了。
小贴士
注意,一旦你将演员的图像设置为 Greenfoot 在创建时提供的图像,例如图 2中的skull.png
,Greenfoot 会自动将图像放置在你的images
文件夹中。因此,你不必从磁盘上的位置复制笑脸图像,你可以创建一个新的演员,然后依次将这个演员的图像设置为每个笑脸。然后,你可以简单地删除这个新演员。你会发现你的图像文件夹看起来就像图 2中显示的那样。
根据演员位置调用 setImage()
在 Greenfoot 主场景窗口的演员类部分双击Enemy
演员以开始编辑Enemy
代码。我们练习良好的功能分解,并在Enemy
的act()
方法中简单地添加对changeDispositon()
的调用;我们很快就会编写这个方法。现在你的act()
方法应该看起来像这样:
public void act() {
setLocation(getX(), getY() + speed);
changeDisposition();
checkRemove();
}
现在,我们将实现changeDisposition()
方法。在这个方法中,我们想要改变敌人的状态,因为他们逐渐意识到他们不会得到英雄。让我们假设我们的敌人直到达到屏幕中间都保持乐观。之后,我们将逐渐让他们陷入绝望。
在changeDisposition()
方法的实现中,我们将使用一个实例变量来跟踪我们需要显示的下一张图片。您需要在速度实例变量的声明下方添加这个变量声明和初始化(在类顶部任何方法之外):
private int timeToChange = 1;
在此基础上,我们现在可以查看changeDisposition()
的实现。以下是我们的代码:
private void changeDisposition() {
int ypos = getY();
int worldHeight = getWorld().getHeight();
int marker1 = (int) (worldHeight * 0.5);
int marker2 = (int) (worldHeight * 0.75);
int marker3 = (int) (worldHeight * 0.90);
if( timeToChange == 1 && ypos > marker1) {
setImage("smiley4.png");
timeToChange++;
}
else if( timeToChange == 2 && ypos > marker2) {
setImage("smiley3.png");
timeToChange++;
}
else if( timeToChange == 3 && ypos > marker3) {
setImage("smiley5.png");
timeToChange++;
}
}
这段代码背后的逻辑很简单。我们想要在敌人下落的过程中选择特定的位置来更改图片。一个复杂的问题是敌人的速度可以通过setSpeed()
方法来改变。我们在AvoiderWorld
类中使用这个方法来增加敌人的速度,以增加游戏的难度。因此,我们不能简单地使用像if( ypos == 300)
这样的代码来更改敌人的图片,因为演员可能永远不会有一个精确的y位置为300
。例如,如果敌人的速度是 7,那么它下落时的y位置如下:7, 14, 21, …, 294, 301, 308,等等。
如我们所见,敌人永远不会有一个精确的y位置为 300。你可能接下来想要尝试像if( ypos > 300 )
这样的代码;然而,这并不是最优的,因为这会导致图片在超过 300 的每个 y 位置上持续被设置。因此,我们应该采用changeDisposition()
中展示的方法,并使用timeToChange
来控制一次性的、顺序的图片更改。
现在我们已经理解了changeDisposition()
背后的逻辑,让我们逐行分析。我们首先创建变量来保存我们想要更改敌人图片的位置。这些位置基于场景的高度;marker1
位于高度的 50%,marker2
位于高度的 75%,而marker3
位于敌人从屏幕底部退出之前的一个稍微靠前的位置。if
语句在更改演员图片之前测试两个条件。它检查是否使用timeToChange
来更改特定图片,以及演员是否已经通过了一个给定的y位置。
小贴士
在之前的代码中,有一些行将十进制数字(类型为double
)转换为整数(类型为int
),例如这一行:
int marker1 = (int) (worldHeight * 0.5)
关于将一个变量转换为另一个变量(也称为类型转换)的更多信息,请参阅以下链接:
docs.oracle.com/javase/specs/jls/se7/html/jls-5.html
编译你的 Greenfoot 场景并玩游戏。看看你是否能获得超过 250 分的分数!完全坦白:在写下最后一句话后,我连续玩了四次游戏,得到了以下分数:52,33,28,254。哇!254!
注意
功能分解
功能分解与自顶向下的设计密切相关,这是一个通过将问题重新定义为更小、更简单的子问题来反复定义问题的过程。当你为程序中的特定动作或功能编写代码时,尝试思考你可以编写的更小的方法,你可以将它们组合起来解决更大的问题。
通常,你希望编写少于 40 行代码的方法,并且只实现一个定义良好的任务。实际上,如果可能的话,我更喜欢做得更小。你会发现,如果你遵循这个实践,代码编写、调试和修改都会更容易。在这本书中,我使用了功能分解。你会发现,书中所有的 act()
方法主要包含对其他方法的调用序列。
使用 setLocation()
setImage()
方法是 Greenfoot 中用于动画角色的最有用的方法;然而,以某些方式移动角色也可以产生有趣的效果。我们已经使用 setLocation()
来移动敌人和我们的英雄;现在让我们用它来动画背景星系,使其看起来像我们在穿越太空。
创建星系
我们将提供各种大小、以不同速度在背景中移动的星星,以产生高速穿越太空的效果。创建星系非常简单,我们已经有非常相似的代码。想象一下,如果我们的敌人有一个小光点的图像,而不是笑脸,而我们有很多这样的敌人。哇!你就有了一个星系。
一张白纸
如果我们要创建自己的动态星系,那么我们就不再需要与 AvoiderWorld
关联的当前背景图像。然而,如果我们把这个类改为没有与之关联的图像,那么我们将会得到一个白色背景——这不是外太空的一个很好的表现。
解决方案是创建一个新的纯黑色、600 x 400 像素的图像,然后将其作为 AvoiderWorld
类的背景图像。启动你最喜欢的图像编辑器或使用 Greenfoot 内置的编辑器,创建一个大黑矩形,将其保存为 PNG 文件到你的 Avoider 项目的 images
文件夹中,然后将 AvoiderWorld
设置为使用这个新图像作为背景。
星类
对于我们的星星,我们将做一些不同的事情。我们不会将星星的图像设置为包含图形的文件,而是将动态绘制图像。由于光点并不复杂,这将很容易做到。
要创建我们的星星演员,在Actor 类部分右键单击Actor
类,并选择新建子类…。在弹出的新建类窗口中,将新类名称输入为Star
,并将新类图像选择为无图像。
小贴士
记住,我们在第一章中讲解了如何创建新的演员,让我们直接进入…。
打开一个新的代码编辑器窗口,为你的新Star
类添加以下构造函数:
public Star() {
GreenfootImage img = new GreenfootImage(10,10);
img.setColor(Color.white);
img.fillOval(0,0,10,10);
setImage(img);
}
这个构造函数动态创建了一个用于Star
类图像的图像。首先,我们创建了一个宽度为10
像素、高度为10
像素的新图像。接下来,我们设置用于在这个图像中绘制任何内容的颜色。我们通过在类文件顶部添加以下import
语句来获取对Color
类的访问权限(有关更多信息,请参阅下面的信息框):
import java.awt.Color;
在设置颜色后,我们使用fillOval()
方法绘制一个椭圆形。fillOval()
的第一个两个参数指定了我们正在绘制的形状的左上角相对于我们图像左上角的偏移量。图 3显示了这种映射。fillOval()
的下一个两个参数指定了包含我们的椭圆形的边界框的宽度和高度。由于我们的宽度和高度相同,fillOval()
将绘制一个圆。最后,我们将演员的图像设置为刚刚创建的新图像。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00267.jpeg
图 3:这显示了使用 fillOval()的第一个两个参数值为 8 和 5 的效果
注意
处理颜色
在Star()
构造函数中,我们进行了一个涉及颜色的操作。计算机(以及基本上任何带有屏幕的东西)上有几种不同的方式来表示颜色,我们将使用 RGBA 颜色模型。如果你对此好奇,你可以在en.wikipedia.org/wiki/RGBA_color_space
上了解更多关于它的信息。
幸运的是,我们不需要了解太多关于理论的知识。Java 提供了一个名为Color
的类,它为我们管理了大部分的复杂性。要将这个Color
类引入到你的代码中,你需要在文件顶部添加一个import
语句。这个import
语句是import java.awt.Color;
。如果你没有将这个添加到上面的代码中,你会得到编译错误。
要了解更多关于这个Color
类的信息,请查看官方文档docs.oracle.com/javase/7/docs/api/java/awt/Color.html
。
我们接下来要为Star
类添加的是act()
方法。我们只需要慢慢将这个演员向下移动到屏幕底部,然后一旦它从屏幕底部移出就将其移除。我们使用setLocation()
来完成前者,使用checkRemove()
方法来完成后者。以下是act()
和checkRemove()
方法完成的代码:
public void act() {
setLocation(getX(), getY()+1);
checkRemove();
}
private void checkRemove() {
World w = getWorld();
if( getY() > w.getHeight() + 30 ) {
w.removeObject(this);
}
}
checkRemove()
方法与在Enemy
类中使用并解释过的代码完全相同,请参阅第一章,“让我们直接进入…”。实际上,Star
类和Enemy
类之间有很多相似之处,以至于我认为我们应该提前将Enemy
类中的setSpeed()
方法添加到Star
类中,因为在我们的移动星星场实现中,我们很可能需要它。将此方法添加到Star
类中:
public void setSpeed( int s) {
speed = s;
}
正如我们在Enemy
类中所做的那样,我们需要在类的顶部添加实例变量speed
。以下是变量声明的代码:
int speed = 1;
我们应该在act()
方法中再进行一次修改,现在使用speed
变量来移动Star
对象。将act()
方法中的setLocation()
代码修改为如下:
setLocation(getX(), getY() + speed);
Star
类的完整代码在图 4中展示。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00268.jpeg
图 4:这显示了完成的Star
类实现
这将是编译场景并确保你没有拼写错误的好时机。我们还没有在我们的游戏中添加星星,所以你不会注意到游戏中的任何区别。添加星星是我们接下来要做的。
创建移动场
我们将在AvoiderWorld
类中生成我们的星星。打开这个类的编辑器窗口,并在act()
方法中添加一行代码来调用我们尚未编写的generateStars()
方法,但很快就会编写。你的act()
方法现在应该看起来像这样:
public void act() {
generateEnemies();
generateStars();
increaseLevel();
}
generateStars()
方法以类似于generateEnemies()
创建新敌人的方式创建新的星星。以下是generateStars()
的代码:
private void generateStars() {
if( Greenfoot.getRandomNumber(1000) < 350) {
Star s = new Star();
addObject( s, Greenfoot.getRandomNumber(getWidth()-20)+10, -1);
}
}
if
语句决定了我们是否想在此时创建一个星星。有 35%的概率我们会创建一个星星,这最终会创建一个相当密集的星星场。在if
语句内部,我们创建一个新的Star
对象并将其添加到World
中。添加此代码并编译运行游戏,看看你的想法。你喜欢星星吗?它们还可以,但看起来有点像在下雨的高尔夫球。我们可以做得更好。
使用视差
视差是近处的物体似乎相对于远处的物体在观看角度上处于不同位置的效果。例如,如果你曾经从汽车窗户向外看,并观察树木移动,你会注意到靠近你的树木似乎比背景中的树木移动得更快。我们可以利用这种现象给我们的星星场带来深度感。
让我们将generateStars()
方法修改为创建两种类型的星星。一些会靠近,一些会远离。靠近的星星会移动得更快,亮度也会比远离的星星高,但我们将会生成更多远离的星星。如果你将我们的屏幕想象成一个通向太空的窗口,我们将看到更远处的物体,而不是近处的物体。因此,我们需要更多的星星。图 5展示了这一点。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00269.jpeg
图 5:这表明,通过窗户看去,对于更远处的物体,你的视野更宽。
最后,我们希望添加一些随机的变化到星星中,这样生成的星星场就不会看起来太均匀。这是我们的视差增强的 generateStars()
方法:
private void generateStars() {
if( Greenfoot.getRandomNumber(1000) < 350) {
Star s = new Star();
GreenfootImage image = s.getImage();
if( Greenfoot.getRandomNumber(1000) < 300) {
// this is a close bright star
s.setSpeed(3);
image.setTransparency(
Greenfoot.getRandomNumber(25) + 225);
image.scale(4,4);
} else {
// this is a further dim star
s.setSpeed(2);
image.setTransparency(
Greenfoot.getRandomNumber(50) + 100);
image.scale(2,2);
}
s.setImage(image);
addObject( s, Greenfoot.getRandomNumber(
getWidth()-20)+10, -1);
}
}
我们添加了访问当前星星图像、更改图像并将其设置为新的星星图像的功能。内部的 if-else
语句处理了附近和远处的星星的变化。有 30%的几率星星会是近处的。附近的星星速度更快(setSpeed()
)、亮度更高(setTransparency()
)和更大(scale()
)。
setTransparency()
方法接受一个整数参数,用于指定图像的透明度。对于完全不透明的物体,你应输入值 255
;对于完全透明的物体,输入 0
。我们使远处的星星更透明,这样更多的黑色背景就会透过来,使其不那么明亮。GreenfootImages
上的 scale()
方法用于改变图像的大小,以便它适合由该方法的前两个参数定义的边界框。正如我们在代码中所看到的,附近的星星被缩放到一个 4 x 4 像素的图像中,而远处的星星被缩放到一个 2 x 2 像素的图像中。
我们离完成星星场已经非常接近了。编译并运行场景,看看你现在对这个场景的看法。
星空看起来很棒,但仍然有两个问题。首先,当游戏开始时,背景是完全黑色的,然后星星开始下落。为了真正保持你在太空中的错觉,我们需要游戏从星星场开始。其次,星星正在生成在敌人、我们的英雄和得分计数器上方;这真的破坏了它们远处的错觉。让我们来修复这个问题。
解决星星在屏幕上其他角色前面的问题只需要一行代码。这是你需要添加到 AvoiderWorld
构造函数中的代码行:
setPaintOrder(Avatar.class, Enemy.class, Counter.class);
setPaintOrder()
方法定义在 World
类中,AvoiderWorld
是其子类。这个方法允许你设置屏幕上显示的类的顺序。因此,我们首先列出 Avatar
类(它将在所有东西的顶部),然后是 Enemy
类,最后是 Counter
类。按照这种顺序,例如,我们的敌人将显示在得分上方。任何未列出的类都将绘制在所有已列出的类之后;因此,我们的星星将位于屏幕上所有角色的后面。
如果我们对 generateStars()
方法进行小的修改,绘制初始的星星场就很容易了。目前,我们的星星由于这一行而硬编码为从 -1
的 y 坐标开始:
addObject( s, Greenfoot.getRandomNumber(getWidth()-20)+10, -1);
如果我们将 generateStars()
修改为接受一个整数参数,该参数指定绘制星星的 y 值,那么我们可以使用这个方法来创建初始的星星场。看 generateStars()
的第一行:
private void generateStars() {
改成这样:
private void generateStars(int yLoc) {
取方法中的最后一行:
addObject( s, Greenfoot.getRandomNumber(getWidth()-20)+10, -1);
改成这个:
addObject( s, Greenfoot.getRandomNumber(getWidth()-20)+10, yLoc);
这两个行更改使我们能够为我们的星星指定任何起始y值。由于这个更改,我们需要在act()
方法中将generateStars()
的调用更改为以下代码行:
generateStars(-1);
如果你编译并运行场景,你应该看到的唯一区别是星星现在真正在背景中。我们仍然需要添加一个简单的方法定义和调用来绘制初始星星场。方法定义如下:
private void generateInitialStarField() {
for( int i=0; i<getHeight(); i++ ) {
generateStars(i);
}
}
如果我们游戏的高度是四百,那么这种方法会调用generateStars()
四百次。每次,它都会提供一个不同的y值来绘制星星。我们将通过在构造函数中添加这一行来用星星填满屏幕:
generateInitialStarField();
我们对AvoiderWorld
类的定义进行了很多更改,这使得你可能在错误的地方放置了代码的可能性越来越大。以下是你可以用来检查你的代码的AvoiderWorld
类的完整列表:
import greenfoot.*;
public class AvoiderWorld extends World {
private GreenfootSound bkgMusic;
private Counter scoreBoard;
private int enemySpawnRate = 20;
private int enemySpeed = 1;
private int nextLevel = 100;
public AvoiderWorld() {
super(600, 400, 1, false);
bkgMusic = new GreenfootSound("sounds/UFO_T-Balt.mp3")
// Music Credit:
// http://www.newgrounds.com/audio/listen/504436 by T-balt
bkgMusic.playLoop();
setPaintOrder(Avatar.class, Enemy.class, Counter.class);
prepare();
generateInitialStarField();
}
public void act() {
generateEnemies();
generateStars(-1);
increaseLevel();
}
private void generateEnemies() {
if( Greenfoot.getRandomNumber(1000) < enemySpawnRate) {
Enemy e = new Enemy();
e.setSpeed(enemySpeed);
addObject( e, Greenfoot.getRandomNumber(
getWidth()-20)+10, -30);
scoreBoard.setValue(scoreBoard.getValue() + 1);
}
}
private void generateStars(int yLoc) {
if( Greenfoot.getRandomNumber(1000) < 350) {
Star s = new Star();
GreenfootImage image = s.getImage();
if( Greenfoot.getRandomNumber(1000) < 300) {
// this is a close bright star
s.setSpeed(3);
image.setTransparency(
Greenfoot.getRandomNumber(25) + 225);
image.scale(4,4);
} else {
// this is a further dim star
s.setSpeed(2);
image.setTransparency(
Greenfoot.getRandomNumber(50) + 100);
image.scale(2,2);
}
s.setImage(image);
addObject( s, Greenfoot.getRandomNumber(
getWidth()-20)+10, yLoc);
}
}
private void increaseLevel() {
int score = scoreBoard.getValue();
if( score > nextLevel ) {
enemySpawnRate += 2;
enemySpeed++;
nextLevel += 100;
}
}
public void endGame() {
bkgMusic.stop();
AvoiderGameOverWorld go = new AvoiderGameOverWorld();
Greenfoot.setWorld(go);
}
private void prepare() {
Avatar avatar = new Avatar();
addObject(avatar, 287, 232);
scoreBoard = new Counter("Score: ");
addObject(scoreBoard, 70, 20);
}
private void generateInitialStarField() {
int i = 0;
for( i=0; i<getHeight(); i++ ) {
generateStars(i);
}
}
}
编译并运行你的游戏。这已经很不错了。你的游戏应该看起来像图 6A中显示的截图。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00270.jpeg
图 6A:这显示了到目前为止的游戏
使用 GreenfootImage
等一下。我是怎么知道 Greenfoot 的GreenfootImage
类以及它包含的setColor()
和fillOval()
方法的?答案是简单的,因为我阅读了文档。我了解到 Greenfoot 提供了GreenfootImage
类来帮助处理和操作图像。一般来说,Greenfoot 提供了一套有用的类来帮助程序员创建交互式应用程序。我们在第一章中学习了World
类和Actor
类,让我们直接进入…。图 6B显示了 Greenfoot 提供的所有类。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00271.jpeg
图 6B:这显示了 Greenfoot 提供的类,以帮助你编写应用程序。此截图直接来自 Greenfoot 的帮助文档。
你可以通过访问 Greenfoot 的网站来访问 Greenfoot 的文档,正如我在第一章中建议的那样,让我们直接进入…。如果你不在网上,你可以通过在 Greenfoot 的主菜单中选择帮助菜单选项,然后从下拉菜单中选择Greenfoot 类文档来访问文档。这将使用默认的网页浏览器打开 Greenfoot 的类文档。
小贴士
Greenfoot 的类文档非常简短和简洁。你应该花 20-30 分钟阅读 Greenfoot 提供的每个类以及这些类中包含的每个方法。这将是一个非常值得的时间投资。
时间和同步
在 Greenfoot 中创建逼真的动画时,时间安排非常重要。我们经常需要让演员对事件做出临时的动画反应。我们需要一种方式来允许(或阻止)某些事物持续一定的时间。使用 Greenfoot 提供的SimpleTimer
类(你可以像在第一章中导入Counter
类一样将其导入你的场景中),你可以等待特定的时间;然而,等待特定的时间很少是正确的选择。
为什么呢?好吧,Greenfoot 为玩家/用户提供了一种通过位于 Greenfoot 主场景窗口底部的速度滑块来加快或减慢场景的能力。如果你在代码中等待了 2 秒钟,然后玩家加快了游戏速度,那么相对于其他所有事物的速度,2 秒钟的等待时间在游戏中会持续更长;如果用户减慢了场景,则会产生相反的效果。我们希望使用一种在 Greenfoot 中“等待”的方法,该方法与游戏速度成比例。
我们将探讨在 Greenfoot 中计时事件的三种不同方法:延迟变量、随机动作和触发事件。
延迟变量
延迟变量与计时器的概念非常相似。然而,我们不会计算秒数(或毫秒数),而是计算调用act()
方法的次数。这将与速度滑块精确成比例,因为该滑块控制act()
方法调用之间的时间。接下来,我们将查看使用延迟变量的示例。
伤害头像
我们的游戏有点苛刻。如果你触碰到敌人一次,你就会死亡。让我们改变游戏,这样你每次被击中都会受到伤害,而杀死我们的英雄需要四次打击。我们需要做的第一件事是创建一个实例变量,该变量将跟踪我们英雄的健康状况。将此实例变量添加到Avatar
类的顶部,任何方法之外:
private int health = 3;
每当我们的英雄接触到敌人时,我们将从这个变量中减去一个。当这个变量为0
时,我们将结束游戏。
当我们的英雄被敌人击中时,我们希望向玩家提供视觉反馈。我们可以通过在游戏顶部添加健康条或生命指示器来实现这一点;然而,让我们只是让我们的英雄看起来受伤。为此,我们需要创建skull.png
图像的副本,该图像用于表示Avatar
类的实例,并增强它们以看起来受损。你可以使用图像编辑器,如 GIMP、Adobe Illustrator 或其他编辑器来做出这些更改。图 7显示了受损的skull.png
图像的版本。确保你将你的头骨图像命名为与我完全相同的方式。第一个图像skull.png
已经在图像文件夹中;其他三个需要命名为skull1.png
、skull2.png
和skull3.png
。为什么以这种方式命名如此重要,很快就会变得明显。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00272.jpeg
图 7:这是我的四个skull.png
副本,显示了增加的伤害。它们分别命名为 skull.png、skull1.png、skull2.png 和 skull3.png。
目前,我们的Avatar
类中的act()
方法如下所示:
public void act() {
followMouse();
checkForCollisions();
}
我们将修改checkForCollisions()
函数的实现,以处理我们的英雄拥有生命并看起来受损的情况。目前的代码片段如下所示:
private void checkForCollisions() {
Actor enemy = getOneIntersectingObject(Enemy.class);
if( enemy != null ) {
getWorld().removeObject(this);
Greenfoot.stop();
}
}
我们需要将其更改为:
private void checkForCollisions() {
Actor enemy = getOneIntersectingObject(Enemy.class);
if( hitDelay == 0 && enemy != null ) {
if( health == 0 ) {
AvoiderWorld world = (AvoiderWorld) getWorld();
world.endGame();
}
else {
health--;
setImage("skull" + ++nextImage + ".png"););
hitDelay = 50;
}
}
if( hitDelay > 0 ) hitDelay--;
}
如我们所见,我们添加了相当多的代码。第一个if
语句检查在受到敌人伤害之前需要满足的两个条件:首先,自上次我们受到敌人伤害以来已经过去了足够的时间,其次,我们现在正在接触Enemy
类的一个实例。当英雄接触到敌人并受到伤害时,我们希望给我们的英雄一段短暂的不可伤害时间,以便移动,而不会在每次调用act()
方法时继续受到伤害。如果我们不这样做,英雄会在你眨眼之前受到四次打击。我们使用hitDelay
整型变量来计算等待时间。如果我们已经受到打击,我们将hitDelay
设置为50
,如内层if-else
语句的else
部分所示。函数中的最后一个if
语句继续递减hitDelay
。当hitDelay
减到0
时,我们可以被敌人击中,并且不再递减hitDelay
。
注意
Java 增量与递减运算符
在最后一段代码中,我们大量使用了 Java 的增量(++
)和递减(--
)运算符。它们简单地分别从它们应用的变量中加一或减一。然而,在使用它们时有一些微妙之处需要你注意。看看以下代码:
int x = 0, y=0, z=0;
y = ++x;
z = x++;
注意,增量运算符可以应用于变量之前(前缀)或之后(后缀)。在这段代码完成后,x
是2
,y
是1
,z
是1
。你可能惊讶z
是1
而不是2
。原因是后缀增量运算符将在变量递增之前返回变量的值。有关更多信息,请参阅以下链接:
docs.oracle.com/javase/tutorial/java/nutsandbolts/op1.html
在内层if-else
语句中,我们知道我们已经受到敌人的打击。我们检查我们的health
是否为0
;如果是,我们就死了,游戏就像以前一样结束。如果我们还有health
,我们就减少我们的health
,更改我们的图像,并设置hitDelay
。
我们将图像更改为下一个更损坏的图像的方式是基于我们之前如何命名文件。我们通过将skull
字符串与一个整数连接,然后再与.png
字符串连接来构建文件名。这种方法为我们提供了一种简短且易于程序化的更改图像的方法。另一种选择是使用switch
语句,根据health
的值调用带有不同文件名的setImage()
。在我们的新版本checkForCollisions()
中,我们使用了两个新的实例变量;我们仍然需要声明和初始化这些变量。在添加本节开头添加的health
变量下方添加这些行:
private int hitDelay = 0;
private int nextImage = 0;
现在,编译你的场景并验证你的英雄需要受到四次攻击才能死亡。
小贴士
hitDelay
变量是延迟变量的一个好例子。在本书的其余部分,我们将使用延迟变量来计时各种活动。在继续之前,请确保你理解我们如何使用hitDelay
。
随机动作
随机动作是模拟简单智能或自然现象的最有效方法之一。它以不可预测的方式重复动作,并为游戏增添了悬念和挑战。我们已经在随机生成英雄需要躲避的敌人流。我们现在将使用它们来改进我们的星系动画。
闪烁
星星已经看起来很棒,并为游戏提供了真正的运动感。我们将通过让它们像真正的星星一样闪烁来增强它们。为此,我们使用setTransparency()
方法使星星完全透明,并使用延迟变量等待一段时间后再将星星再次变得不透明。我们将使用 Greenfoot 的随机数生成器来确保星星的闪烁不频繁。首先,我们在Star
类的act()
方法中添加一个方法调用checkTwinkle()
:
public void act() {
setLocation(getX(), getY()+speed);
checkRemove();
checkTwinkle();
}
我们需要在speed
变量声明下添加以下延迟变量以及用于存储对象顶部当前透明度的变量:
int twinkleTime = 0;
int currentTransparency = 0;
以下是对checkTwinkle()
的实现:
private void checkTwinkle() {
GreenfootImage img = getImage();
if( twinkleTime > 0 ) {
if( twinkleTime == 1) {
img.setTransparency(currentTransparency);
}
twinkleTime--;
} else {
if( Greenfoot.getRandomNumber(10000) < 10) {
twinkleTime = 10;
currentTransparency = img.getTransparency();
img.setTransparency(0);
}
}
}
让我们看看外部if-else
语句的else
部分。以很小的随机概率,我们将twinkleTime
(我们的延迟变量)设置为10
,保存星星当前透明度以便稍后恢复,然后将透明度设置为0
。
初始if-else
语句的if
部分,如果twinkleTime
大于0
,则递减twinkleTime
,当twinkleTime
等于1
时恢复我们星星的透明度。因为twinkleTime
只设置为10
,所以星星将只在极短的时间内不可见。这种短暂的闪烁给人一种星星闪烁的错觉。
编译并运行场景,看看你是否能捕捉到星星的闪烁。如果你在验证这一点上有困难,请改变闪烁发生的频率并再次尝试。
触发事件
当某个事件发生时触发演员的变化是另一种动画方式。例如,你可能有一个敌人演员,只有当你进入一定范围内时,它才会追逐你。你也可能有一个演员对键盘事件或位置做出响应。
在本节中,我们将给我们的英雄添加眼睛。显然,我们的英雄非常关心附近的敌人,肯定想密切关注他们。
小贴士
给演员添加动画眼睛是赋予该演员个性的一种极好的方式。眼睛非常富有表情,可以轻松地表达兴奋、悲伤或恐惧。不要犹豫,添加动画眼睛。
添加眼睛
这可能看起来有点奇怪,但我们将创建一个单独的 Eye
角色演员。我们这样做有几个原因。首先,要让眼睛四处看需要相当多的代码。我们可以将这段代码封装在 Eye
类中,并使我们的 Avatar
类更加简洁。其次,将眼睛作为独立的实体意味着我们可以将它们添加到未来的演员中,即使我们改变了 Avatar
类的图像,它们仍然可以正常工作。
另一种选择是为我们想要看的每个方向创建一个带有眼睛的头骨图像。我们为英雄创建不同图像以显示不同等级的伤害的事实将进一步复杂化问题。因此,我们将创建一个单独的 Eye
角色演员。
创建一个新的 Actor
子类,命名为 Eye
。不要将图像与这个 Actor
类关联。我们将动态绘制一个眼睛的图像,并在需要朝不同方向看时适当地重新绘制它。以下是 Eye
类的实现:
import greenfoot.*;
import java.awt.Color;
import java.util.List;
public class Eye extends Actor {
public Eye() {
drawEye(2,2);
}
public void act() {
lookAtEnemies();
}
public void lookAtEnemies() {
List<Enemy> eList = getObjectsInRange(120, Enemy.class);
if( !eList.isEmpty() ) {
Enemy e = eList.get(0);
if( e.getX() < getX() ) {
if( e.getY() < getY() ) {
drawEye(1,1);
} else {
drawEye(1,3);
}
} else {
if( e.getY() < getY() ) {
drawEye(3,1);
} else {
drawEye(3,3);
}
}
}
}
private void drawEye(int dx, int dy) {
GreenfootImage img = new GreenfootImage(10,10);
img.setColor(Color.white);
img.fillOval(0,0,10,10);
img.setColor(Color.black);
img.fillOval(dx,dy,6,6);
setImage(img);
}
}
这个类的主要有两个方法:drawEye()
方法和 lookAtEnemies()
方法。drawEye()
图像使用与我们在 Star
类中绘制星星图像相同的方法来绘制眼睛。对于眼睛,我们只需要绘制一个额外的黑色圆圈作为瞳孔。drawEye()
方法接受两个整数参数,提供瞳孔在眼睛中的位置。fillOval()
的偏移部分在 图 3 中进行了演示。总结来说,第一个 fillOval()
命令绘制了眼睛较大的白色部分,第二个 fillOval()
命令在给定的偏移量处绘制了小的黑色瞳孔,以模拟朝某个方向注视。
lookAtEnemies()
方法会在眼睛给定距离内找到所有敌人,并使用 drawEye()
方法注视它找到的第一个敌人。通过使用 if
语句比较敌人的 x 和 y 位置与自己的位置,眼睛将敌人分类为四个象限之一:左上,左下,右上和右下。利用这些信息,drawEye()
方法分别使用整数参数 (1,1)
,(1,3)
,(3,1)
和 (3,3)
被调用。图 8 展示了敌人所在的象限与 drawEye()
调用之间的相关性。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00273.jpeg
图 8:这显示了敌人位置与调用 drawEye()
的映射
在 lookAtEnemies()
中,我们使用了一种新的碰撞检测方法,称为 getObjectsInRange()
。此方法与 getOneIntersectingObject()
有两种不同之处。首先,它不是使用调用 Actor
类的边界框来确定是否发生碰撞,而是在调用 Actor
类周围绘制一个半径由 getObjectsInRange()
的第一个参数定义的圆。此方法返回该圆中找到的所有敌人,而不仅仅是单个敌人。敌人以 Java List
数组的形式返回。在 Eye
类的顶部,我们需要包含 import java.util.List;
代码以使用 List
数据类型。我们一次只能盯着一个敌人,所以我们选择使用 get()
方法并传递整数值 0
来访问列表中的第一个敌人。以下是 Greenfoot 关于 getObjectsInRange()
的文档:
protected java.util.List getObjectsInRange(int radius, java.lang.Class cls)
上一行代码返回围绕此对象半径为 radius
的所有对象。一个对象如果在范围内,意味着其中心与该对象中心的距离小于或等于 radius
。
getObjectsInRange()
方法的参数描述如下:
-
radius
:这是圆的半径(以单元格为单位) -
cls
:这是要查找的对象的类(传递null
将查找所有对象)
给我们的英雄赋予视力
现在我们有一个名为 Eye
的 Actor
类,我们只需要对 Avatar
类进行一些修改,以便为我们的英雄添加眼睛。我们需要创建两个眼睛,将它们放在我们的英雄身上,然后我们需要确保每次我们的英雄移动时眼睛都保持在原位。我们首先向 Avatar
类添加实例变量:
private Eye leftEye;
private Eye rightEye;
然后我们通过添加此方法在头骨图像上创建并放置这些眼睛:
protected void addedToWorld(World w) {
leftEye = new Eye();
rightEye = new Eye();
w.addObject(leftEye, getX()-10, getY()-8);
w.addObject(rightEye, getX()+10, getY()-8);
}
初始时,你可能认为我们可以在 Avatar
类的构造方法中创建眼睛并添加它们。通常,这会是运行一次的代码的理想位置。问题是,在我们可以将眼睛添加到世界中之前,Avatar
类的实例需要存在于一个世界中。如果我们查看 AvoiderWorld
中的代码,添加我们的英雄,我们会看到这个:
Avatar avatar = new Avatar();
addObject(avatar, 287, 232);
我们英雄的创建是一个两步的过程。首先,创建Avatar
类的一个实例(第一行),然后我们将这个实例添加到世界中(第二行)。注意,构造函数在对象放置到世界中之前运行,所以我们不能通过getWorld()
方法访问我们所在的世界实例。Greenfoot 的开发者意识到一些角色将需要访问它们所在的世界以完成初始化,因此他们在Actor
类中添加了addedToWorld()
方法。当初始化需要访问世界时,Actor
类会重写此方法,并且每当一个角色被添加到世界中时,Greenfoot 都会调用它。我们在Avatar
类中使用此方法来将眼睛放置在我们的英雄身上。
我们现在已经创建了眼睛并将它们添加到了我们的英雄身上。现在,我们只需要确保眼睛在英雄移动时始终伴随着它。为此,我们在Avatar
类的followMouse()
函数中添加以下几行代码:
leftEye.setLocation(getX()-10, getY()-8);
rightEye.setLocation(getX()+10, getY()-8);
以下代码添加在以下代码行之后:
setLocation(mi.getX(), mi.getY());
为什么setLocation()
调用中的 10s 和 8s 会对应leftEye
和rightEye
?这些是正确放置眼睛在英雄眼窝中的值。我是通过试错法确定这些值的。图 9展示了详细信息。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00274.jpeg
图 9:展示了眼睛位置是如何确定的
现在是时候享受乐趣了。编译并运行你的游戏,享受你的劳动成果。你的游戏应该看起来像图 10中所示的截图。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00275.jpeg
图 10:我们的游戏有动画敌人、移动的背景星系(带有闪烁)以及当被击中时视觉上会变化的英雄
缓动
在本章的最后一个大节中,我们将探讨使用缓动方程以有趣的方式移动我们的角色。缓动函数使用缓动方程来计算作为时间函数的位置。几乎你见过的每一个网页、移动设备或电影中的动画,在某个时间点都使用了缓动。我们将在游戏中添加三个新的角色,它们根据三种不同的缓动函数移动:线性、指数和正弦。
加速和减速
加速是添加新挑战和平衡玩家技能的绝佳方式。加速为玩家提供速度、力量、健康或其他与游戏相关的技能的短暂提升。它们通常随机出现,可能不在最方便的位置,因此需要玩家快速做出实时决策,权衡移动到加速器与它的有益效果之间的风险。
同样,我们可以创建随机出现的游戏对象,这些对象会负面影响玩家的表现。我称这些为减弱效果。它们也要求玩家做出快速、实时的决策,但现在他们需要在避开它们和保持当前轨迹并承受负面影响之间做出选择。
我们将在游戏中添加两个新的角色作为减弱效果,以及一个新角色作为增强效果。所有这三个角色都将使用缓动进行移动。我们首先介绍一个新的Actor
类,它将包含所有关于缓动和作为增强或减弱效果的公共代码。我们的增强和减弱效果将从这个类继承。使用继承和多态来编写简洁、灵活和可维护的代码是良好的面向对象编程实践。
基类
为我们的增强效果创建一个经过深思熟虑的基类将提供轻松创建新增强效果和增强现有效果的途径。在我们讨论新类的代码之前,我们需要将一个新的 Greenfoot 提供的类导入到我们的项目中,就像我们在第一章中导入Counter
类一样,让我们直接进入…。我们将导入的类是SmoothMover
。我们需要这个类,因为它更准确地跟踪Actor
的位置。以下是其文档的摘录:
public abstract class SmoothMover extends greenfoot.Actor
A variation of an actor that maintains a precise location (using doubles for the co-ordinates instead of ints). This allows small precise movements (e.g. movements of 1 pixel or less) that do not lose precision.
要导入这个类,请点击 Greenfoot 主菜单中的编辑,然后在出现的下拉菜单中点击导入类…。在随后出现的导入类窗口中,在左侧选择SmoothMover
,然后点击导入按钮。
现在我们已经在项目中有了SmoothMover
,我们可以创建PowerItems
类。右键点击SmoothMover
并选择新建子类…。您不需要为此类关联图像,因此在场景图像部分选择无图像。
让我们来看看PowerItems
(我们为增强和减弱效果而创建的新基类)的实现:
import greenfoot.*;
public abstract class PowerItems extends SmoothMover
{
protected double targetX, targetY, expireTime;
protected double origX, origY;
protected double duration;
protected int counter;
public PowerItems( int tX, int tY, int eT ) {
targetX = tX;
targetY = tY;
expireTime = eT;
counter = 0;
duration = expireTime;
}
protected void addedToWorld(World w) {
origX = getX();
origY = getY();
}
public void act() {
easing();
checkHitAvatar();
checkExpire();
}
protected abstract double curveX(double f);
protected abstract double curveY(double f);
protected abstract void checkHitAvatar();
protected void easing() {
double fX = ++counter/duration;
double fY = counter/duration;
fX = curveX(fX);
fY = curveY(fY);
setLocation((targetX * fX) + (origX * (1-fX)),
(targetY * fY) + (origY * (1-fY)));
}
private void checkExpire() {
if( expireTime-- < 0 ) {
World w = getWorld();
if( w != null ) w.removeObject(this);
}
}
}
我们首先需要讨论这个类的所有实例变量。共有七个。其中两个用于跟踪起始坐标(origX
和origY
),另外两个用于跟踪结束坐标(targetX
和targetY
)。实例变量expireTime
指定这个演员在移除自己之前应该执行多少次act()
方法的调用。换句话说,它指定了演员的生命周期。duration
实例变量简单地保存expireTime
的初始值。expireTime
变量会不断递减,直到达到 0,但我们需要知道其原始值用于缓动方程。counter
变量记录这个演员移动了多少次。图 11展示了这些变量的图形表示。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00276.jpeg
图 11:此图以图形方式展示了 PowerItems 中实例变量的含义
实例变量在构造函数中初始化,除了origX
和origY
,它们在addedToWorld()
方法中初始化(该方法在本章前面已经讨论过),这样我们就可以将它们设置为 actor 当前的x和y位置。
由于我们明智地使用了功能分解,act()
方法很容易理解。首先,它通过调用easing()
来移动 actor。接下来,调用checkHitAvatar()
来查看它是否与我们的英雄发生了碰撞。这个方法是abstract
的,这意味着它的实现留给这个类的子类。这样做是因为每个子类都希望在它们发生碰撞时对我们的英雄应用其独特的效果。最后,它检查act()
方法是否被调用expireTime
次。如果是这样,PowerItem
已经达到了其期望的生命周期,是时候移除它了。我们将在下一节讨论easing()
、checkHitAvatar()
和checkExpire()
的具体实现。
easing()
方法实际上是这个类的关键方法。它包含了一个缓动方程的通用形式,足够灵活,允许我们定义许多不同类型的有趣运动。该方法将 actor 移动到起点和终点之间的一定比例的位置。它首先计算在当前时间点,我们需要在x方向上从原始值到目标值之间移动的距离的百分比,以及y方向上的类似计算,并将这些值分别保存在局部变量fX
和fY
中。接下来,我们使用curveX()
和curveY()
函数来操纵这些百分比,然后我们使用这些百分比在调用setLocation()
时。与checkHitAvatar()
一样,curveX()
和curveY()
也是abstract
的,因为它们的细节取决于从PowerItems
派生的类。我们将在下一节讨论abstract
方法checkHitAvatar()
、curveX()
和curveY()
,并提供一个详细的示例。
在此之前,让我们快速看一下PowerItems
的act()
方法中的最后一个方法。最后一个方法checkExpire()
,当expireTime
达到 0 时,简单地移除 actor。
注意
抽象类
抽象类是共享几个相关类之间代码和实例变量的有效方式。在抽象类中,你可以实现尽可能多的代码,而不需要包含在子类(子类)中的特定知识。对我们来说,PowerItems
类是一个抽象类,它包含了我们所有增强和减弱的通用代码。有关抽象类的更多信息,请访问docs.oracle.com/javase/tutorial/java/IandI/abstract.html
。
线性缓动
我们将要添加到游戏中的第一个减权是如果被触摸会暂时使我们的英雄昏迷。遵循我们游戏的主题,其中好事(笑脸)是坏事,我们将我们的新减权设计成一个蛋糕。要创建我们的新Actor
,在 Greenfoot 主场景窗口的Actor 类部分右键点击PowerItems
,并从出现的菜单中选择新建子类…。将类命名为Cupcake
,并选择位于食物类别的松饼(对我来说它看起来像蛋糕!)图片。
在编辑器窗口中打开Cupcake
类,使其看起来像这样:
import greenfoot.*;
public class Cupcake extends PowerItems
{
public Cupcake( int tX, int tY, int eT) {
super(tX, tY, eT);
}
protected double curveX(double f) {
return f;
}
protected double curveY(double f) {
return f;
}
protected void checkHitAvatar() {
Avatar a = (Avatar) getOneIntersectingObject(Avatar.class);
if( a != null ) {
a.stun();
getWorld().removeObject(this);
}
}
}
因为我们从PowerItems
的代码中继承,Cupcake
相当简短和简洁。这个类的构造函数仅仅将其参数传递给PowerItems
中的构造函数。由于PowerItems
是一个抽象类,我们需要在这里实现PowerItems
中的抽象方法(curveX()
、curveY()
和checkHitAvatar()
)。
Cupcake
类将成为我们线性缓动的例子。它将从起始位置以恒定的线性步骤移动到结束位置。因为它线性,我们的curveX()
和curveY()
方法非常简单。它们根本不改变输入参数。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00277.jpeg
图 12:这是一个展示 Cupcake 类实例如何在屏幕上线性移动的例子
让我们看看图 12中展示的例子。在这个例子中,Cupcake
被调用到目标位置**(150, 100**)并且设置了过期时间4
,并被添加到位置**(10,10**)的世界中。位置**(a**)显示了对象的初始值。位置**(b**)、(c)、(d)和**(e**)分别显示了在act()
方法调用一次、两次、三次和四次后与对象关联的值。正如我们所见,这个演员沿着直线移动。为了更好地理解线性缓动,让我们讨论一下为什么在位置**(b**)的值是这样的。在初始化(在位置**(a**)显示)之后,act()
方法中的函数(从PowerItems
继承而来)被调用。easing()
方法将counter
设置为 1,然后将fX
和fY
设置为 0.25,如以下代码所示:
double fX = ++counter/duration; // counter is incremented to 1
double fy= counter/duration; // counter remains 1
Cupcake
中的curveX()
和curveY()
方法不改变fX
和fY
。对于给定的值,setLocation()
的第一个参数的第一个参数值为 45 ((150 * 0.25) + (10 * 0.75)),第二个参数值为 32.5 ((1000.25) + (10 * 0.75))*。
在easing()
之后,act()
方法中接下来调用的方法是checkHitAvatar()
。这个方法简单地调用Avatar
(我们的英雄)实例上的stun()
方法,如果与之碰撞。stun()
方法将在所有加权和减权讨论之后展示。此时,我们将展示对Avatar
类所需的所有更改。
指数缓动
现在我们已经讨论了大多数关于能力提升和能力降低的理论,我们可以快速讨论剩下的。接下来我们要添加的是一种能力提升。它将从英雄承受的部分伤害中恢复。考虑到我们游戏的主题,这个有益的演员看起来必须很糟糕。我们将它做成一块石头。
要创建我们的新Actor
类,在 Greenfoot 主场景窗口的演员类部分右键点击PowerItems
,然后从出现的菜单中选择新建子类…。将类命名为Rock
,并选择位于自然类别的rock.png
图片。
在编辑器窗口中打开Rock
类,并将其更改为如下所示:
import greenfoot.*;
public class Rock extends PowerItems
{
public Rock( int tX, int tY, int eT ) {
super(tX, tY, eT);
}
protected double curveX(double f) {
return f;
}
protected double curveY(double f) {
return f * f * f;
}
protected void checkHitAvatar() {
Avatar a = (Avatar) getOneIntersectingObject(Avatar.class);
if( a != null ) {
a.addHealth();
getWorld().removeObject(this);
}
}
}
Cupcake
类和Rock
类之间的两个主要区别是curveY()
的实现以及checkHitAvatar()
调用addHealth()
而不是stun()
。我们将在稍后描述addHealth()
,如前所述。curveY()
的变化给这个演员一个曲线方向,通过立方它所给的值。这种效果在图 13的示例中得到了展示。比较每个位置的y位置的变化。y值呈指数增长。首先,它只移动 1.4 像素(从位置**(a**)到位置**(b**)),最后,大约跳过 52 像素(从位置**(d**)到位置**(e**))。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00278.jpeg
图 13:这是一个示例,展示了 Rock 类的实例如何在屏幕的 y 方向上以指数方式移动
正弦曲线
我们即将添加的最后一种能力是Clover
。它将暂时减慢我们的英雄速度,并使用正弦曲线。要创建这个类,在 Greenfoot 主场景窗口的演员类部分右键点击PowerItems
,然后从出现的菜单中选择新建子类…。将类命名为Clover
,并选择位于自然类别的shamrock
图片。在编辑器窗口中打开它,并将其更改为如下所示:
import greenfoot.*;
import java.lang.Math;
public class Clover extends PowerItems
{
public Clover(int tX, int tY, int eT) {
super(tX, tY, eT);
}
protected double curveX(double f) {
return f;
}
protected double curveY(double f) {
return Math.sin(4*f);
}
protected void checkHitAvatar() {
Avatar a = (Avatar)
getOneIntersectingObject(Avatar.class);
if( a != null ) {
a.lagControls();
getWorld().removeObject(this);
}
}
}
与Rock
类一样,Clover
类在其curveY()
方法中执行一些独特的事情。它在类顶部导入 Java 的数学库,并在curveY()
的实现中使用Math.sin()
。这使得y运动像正弦波一样振荡。
在Clover
中,checkHitAvatar()
调用与Avatar
类实例碰撞的lagControls()
,而不是stun()
或addHealth()
。在下一节中,我们将实现Avatar
类中的stun()
、addHealth()
和lagControls()
。
对 Avatar 类的更改
为了适应我们新能力物品的效果,Avatar
类需要实现一些方法并更改一些现有方法。这些方法是stun()
、addHealth()
和lagControls()
。
小贴士
在继续本章之前,这里有一个额外的挑战。尝试自己实现这些方法。仔细思考每一个,并在纸上草拟它们。尝试这个的最坏情况是你会学到很多。
stun()
和lagControls()
的实现涉及添加延迟变量并使用它们来影响移动。在Avatar
类中,所有移动都在followMouse()
方法中处理。为了使我们的英雄昏迷,我们只需要暂时禁用followMouse()
方法一小段时间。以下是修改此方法的步骤:
private void followMouse() {
MouseInfo mi = Greenfoot.getMouseInfo();
if( stunDelay < 0 ) {
if( mi != null ) {
setLocation(mi.getX(), mi.getY());
leftEye.setLocation(getX()-10, getY()-8);
rightEye.setLocation(getX()+10, getY()-8);
}
} else {
stunDelay--;
}
}
我们还需要在类的顶部定义stunDelay
实例变量:
private int stunDelay = -1;
这遵循了我们在本章开头添加的实例变量hitDelay
的使用模式。它是我们的延迟变量示例。现在,我们实现stun()
:
public void stun() {
stunDelay = 50;
}
每次调用stun()
时,followMouse()
方法将无法工作 50 个周期(act()
方法的调用次数)。
实现lagControls()
的方式类似,除了我们需要暂时改变移动,而不是阻止它。再次,我们需要更改followMouse()
方法:
private void followMouse() {
MouseInfo mi = Greenfoot.getMouseInfo();
if( stunDelay < 0 ) {
if( mi != null ) {
if( lagDelay > 0 ) {
int stepX = (mi.getX() - getX())/40;
int stepY = (mi.getY() - getY())/40;
setLocation(stepX + getX(), stepY + getY());
--lagDelay;
} else {
setLocation(mi.getX(), mi.getY());
}
leftEye.setLocation(getX()-10, getY()-8);
rightEye.setLocation(getX()+10, getY()-8);
}
} else {
stunDelay--;
}
}
让我们先添加实例变量lagDelay
,然后讨论它在followMouse()
中的使用。在类的顶部stunDelay
下面添加此行:
private int lagDelay = -1;
当lagDelay
的值大于 0 时,它将实现延迟控制。在上面的方法内部if-else
语句中,通过只将我们的英雄移动到鼠标位置的四分之一处来实现延迟。这使得我们的英雄缓慢地向鼠标位置爬行。延迟变量lagDelay
递减,直到小于 0。它是如何变成大于 0 的?它是在Clover
类调用的lagControls()
方法中设置的。以下是该方法的代码:
public void lagControls() {
lagDelay = 150;
}
现在我们需要实现addHealth()
方法。以下是代码:
public void addHealth() {
if( health < 3 ) {
health++;
if( --nextImage == 0 ) {
setImage("skull.png");
} else {
setImage("skull" + nextImage + ".png");
}
}
}
此方法简单地撤销我们在击中敌人时发生的伤害。如果我们已经处于满血状态,则此方法不执行任何操作;否则,它增加health
实例变量,减少nextImage
,以便与我们要显示的图像保持同步,并将Avatar
的图像设置为之前的、损坏较少的图像。非常酷!
我们对Avatar
类进行了重大修改。以下是它的完整代码:
import greenfoot.*;
public class Avatar extends Actor {
private int health = 3;
private int hitDelay = 0;
private int stunDelay = -1;
private int lagDelay = -1;
private int nextImage = 0;
private Eye leftEye;
private Eye rightEye;
protected void addedToWorld(World w) {
leftEye = new Eye();
rightEye = new Eye();
w.addObject(leftEye, getX()-10, getY()-8);
w.addObject(rightEye, getX()+10, getY()-8);
}
public void act() {
followMouse();
checkForCollisions();
}
public void addHealth() {
if( health < 3 ) {
health++;
if( --nextImage == 0 ) {
setImage("skull.png");
} else {
setImage("skull" + nextImage + ".png");
}
}
}
public void lagControls() {
lagDelay = 150;
}
public void stun() {
stunDelay = 50;
}
private void checkForCollisions() {
Actor enemy = getOneIntersectingObject(Enemy.class);
if( hitDelay == 0 && enemy != null ) {
if( health == 0 ) {
AvoiderWorld world = (AvoiderWorld) getWorld();
world.endGame();
}
else {
health--;
setImage("skull" + ++nextImage + ".png");
hitDelay = 50;
}
}
if( hitDelay > 0 ) hitDelay--;
}
private void followMouse() {
MouseInfo mi = Greenfoot.getMouseInfo();
if( stunDelay < 0 ) {
if( mi != null ) {
if( lagDelay > 0 ) {
int stepX = (mi.getX() - getX())/40;
int stepY = (mi.getY() - getY())/40;
setLocation(stepX + getX(), stepY + getY());
--lagDelay;
} else {
setLocation(mi.getX(), mi.getY());
}
leftEye.setLocation(getX()-10, getY()-8);
rightEye.setLocation(getX()+10, getY()-8);
}
} else {
stunDelay--;
}
}
}
我们离尝试所有这些功能已经很近了。我们只需要在AvoiderWorld
类中随机创建并添加能量提升和降低项。
AvoiderWorld
类的更改
我们需要在AvoiderWorld
类的顶部创建三个新的实例变量,以指定我们用于生成我们的新能量物品的概率。在nextLevel
的声明和初始化下面添加这些代码行:
private int cupcakeFrequency = 10;
private int cloverFrequency = 10;
private int rockFrequency = 1;
初始时,这些物品的创建不会非常频繁,但我们将通过在increaseLevel()
函数中增加它们来改变这一点。以下是代码:
private void increaseLevel() {
int score = scoreBoard.getValue();
if( score > nextLevel ) {
enemySpawnRate += 3;
enemySpeed++;
cupcakeFrequency += 3;
cloverFrequency += 3;
rockFrequency += 2;
nextLevel += 50;
}
}
在act()
方法中,我们调用一个生成敌人的函数和另一个生成星星的函数。遵循这个模式,在act()
方法中添加此行:
generatePowerItems();
因为所有的能量物品类都继承自PowerItems
,我们可以使用多态来编写一些相当简洁的代码。以下是generatePowerItems()
的实现:
private void generatePowerItems() {
generatePowerItem(0, cupcakeFrequency); // new Cupcake
generatePowerItem(1, cloverFrequency); // new Clover
generatePowerItem(2, rockFrequency); // new Health
}
很好,我们可以使用一个方法来创建我们新的力量物品——generatePowerItem()
。此方法接受一个整数,描述我们想要创建的力量物品类型,以及生成这些特定物品的频率。以下是实现:
private void generatePowerItem(int type, int freq) {
if( Greenfoot.getRandomNumber(1000) < freq ) {
int targetX = Greenfoot.getRandomNumber(
getWidth() -80) + 40;
int targetY = Greenfoot.getRandomNumber(
getHeight()/2) + 20;
Actor a = createPowerItem(type, targetX, targetY, 100);
if( Greenfoot.getRandomNumber(100) < 50) {
addObject(a, getWidth() + 20,
Greenfoot.getRandomNumber(getHeight()/2) + 30);
} else {
addObject(a, -20,
Greenfoot.getRandomNumber(getHeight()/2) + 30);
}
}
}
此方法看起来很像我们其他生成演员的方法。它将在给定的随机速率下生成一个物品,并将这些物品放置在屏幕的左侧或右侧,向屏幕内部随机生成的位置移动。局部变量targetX
将是屏幕上的任何有效x坐标,除了屏幕左右两侧的40
像素宽的边界。我们只想确保它移动足够长,以便可以看到,并且对游戏有影响。变量targetY
有稍微严格的约束。我们只想在屏幕上半部分生成y值,加上初始的20
像素,以防止演员移动得太靠近屏幕顶部。内部的if-else
语句只是简单地选择将对象放置在屏幕的左侧或右侧作为其初始位置。
与我们生成其他演员的方式相比,这里真正的区别在于对createPowerItem()
的调用。由于我们使用此方法来生成三种任意一种力量物品,我们不能硬编码创建特定物品的过程,例如,new Cupcake();
。我们使用createPowerItem()
来创建与generatePowerItems()
的类型参数匹配的正确对象。以下是createPowerItem()
的实现:
private Actor createPowerItem(int type, int targetX, int targetY, int expireTime) {
switch(type) {
case 0: return new Cupcake(targetX, targetY,
expireTime);
case 1: return new Clover(targetX, targetY,
expireTime);
case 2: return new Rock(targetX, targetY,
expireTime);
}
return null;
}
此方法根据类型创建一个新的Cupcake
、Clover
或Rock
力量物品。
我们真的为这个游戏添加了很多内容,现在是时候编译并测试它了。通常情况下,你不会在没有测试代码的小部分的情况下添加这么多代码。例如,我们本可以完全实现Rock
力量提升并测试它,然后再添加其他力量物品。出于教学目的,我们继续这样做是有意义的。我希望你在编译代码时不会遇到太多的错误。通过有系统地检查你的代码与本章中的代码,并密切注意编译错误信息,你应该能够快速消除任何错误。
小贴士
如果你需要刷新一下 Java switch 语句的工作方式,请参考以下链接:
docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html
编译、调试和玩。这个游戏变得越来越好。查看我的图 14截图。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00279.jpeg
图 14:这是包含力量提升、力量降低和各种闪亮元素的完整避免者游戏
避免者游戏
我们的避免者游戏变得越来越完整,玩起来更有趣。在第五章《交互式应用设计理论》中,我们将探讨游戏设计理论,了解如何构建有趣且引人入胜的游戏。那时,我们将重新审视我们的游戏并提高其可玩性。
你的作业
当一个Avatar
对象被击中时,它会在短时间内对再次被击中免疫。不幸的是,我们没有为玩家提供任何视觉反馈来指示这一事件的发生或何时结束。你的任务是让英雄在不能被击中时眨眼。查看Star
类以获取如何使对象眨眼的提示。
摘要
在本章中,我们涵盖了大量的内容。你学习了几个重要的动画角色技术,包括图像交换、延迟变量、视差和缓动。我们的敌人、我们的英雄和背景都更加生动。你应该在创建游戏、模拟、动画镜头或教育应用时使用本章的所有技术。
第三章。碰撞检测
*“像明天就要死去一样生活。像永远都要活着一样学习。” | ||
---|---|---|
–圣雄甘地 |
通常,在 Greenfoot 中,你需要确定两个或多个对象是否接触。这被称为碰撞检测,对于大多数模拟和游戏都是必要的。检测算法范围从简单的边界框方法到非常复杂的像素颜色分析。Greenfoot 提供了一系列简单的方法来实现碰撞检测;在第一章,“让我们直接进入…”,和第二章,“动画”中,你已经接触到了其中的一些。在本章中,你将学习如何使用 Greenfoot 的其他内置碰撞检测机制,然后学习更精确的方法来使用它们进行碰撞检测。虽然像素完美的碰撞检测超出了本书的范围,但基于边界的和隐藏精灵的碰撞检测方法对于大多数 Greenfoot 应用来说已经足够了。本章将涵盖以下主题:
-
Greenfoot 内置方法
-
基于边界的检测方法
-
隐藏精灵方法
我们将暂时放下 Avoider Game 的开发,使用一个简单的僵尸入侵模拟来展示我们的碰撞检测方法。僵尸似乎很适合这一章。从他的引言中判断,我认为甘地希望你像僵尸一样学习。
ZombieInvasion 交互式模拟
在第一章,“让我们直接进入…”和第二章,“动画”中,我们一步一步地构建 Avoider Game,并在每个章节结束时得到可玩的游戏版本。在僵尸模拟中,我们将看到一群僵尸突破墙壁,向另一边的家园前进。用户可以通过在模拟中放置爆炸来与模拟互动,这将摧毁两种类型的僵尸和墙壁。对于我们的僵尸模拟,我将在一开始就提供大部分代码,我们将集中精力实现碰撞检测。所有提供的代码都使用了我们在前两章中介绍的概念和技术,应该看起来非常熟悉。我们在这里将只提供一个代码的概述讨论。图 1提供了我们场景的图片。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00280.jpeg
图 1:这是 ZombieInvasion 的屏幕截图
让我们创建一个新的场景,称为ZombieInvasion
,然后逐步添加并讨论World
子类和Actor
子类。或者,你也可以在www.packtpub.com/support
下载ZombieInvasion的初始版本。
在 ZombieInvasionWorld 中动态创建演员
这个类有两个主要职责:将世界中的所有演员放置好,并在鼠标点击时创建爆炸。大部分情况下,用户只能观察场景,并且只能通过创建爆炸与之互动。ZombieInvasionWorld
类相当简单,因为我们正在创建一个交互式模拟而不是游戏。以下是完成此任务的代码:
import greenfoot.*;
public class ZombieInvasionWorld extends World {
private static final int DELAY = 200;
int bombDelayCounter = 0; // Controls the rate of bombs
public ZombieInvasionWorld() {
super(600, 400, 1);
prepare();
}
public void act() {
if( bombDelayCounter > 0 ) bombDelayCounter--;
if( Greenfoot.mouseClicked(null) && (bombDelayCounter == 0) ) {
MouseInfo mi = Greenfoot.getMouseInfo();
Boom pow = new Boom();
addObject(pow, mi.getX(), mi.getY());
bombDelayCounter = DELAY;
}
}
private void prepare() {
int i,j;
for( i=0; i<5; i++) {
Wall w = new Wall();
addObject(w, 270, w.getImage().getHeight() * i);
}
for( i=0; i<2; i++) {
for( j=0; j<8; j++) {
House h = new House();
addObject(h, 400 + i*60, (12 +h.getImage().getHeight()) * j);
}
}
for( i=0; i<2; i++) {
for( j=0; j<8; j++) {
Zombie1 z = new Zombie1();
addObject(z, 80 + i*60, 15 + (2 +z.getImage().getHeight()) * j);
}
}
for( i=0; i<2; i++) {
for( j=0; j<7; j++) {
Zombie2 z = new Zombie2();
addObject(z, 50 + i*60, 30 + (3 +z.getImage().getHeight()) * j);
}
}
}
}
当你在场景屏幕上右键单击并从弹出菜单中选择 保存世界 时,Greenfoot 将自动为你创建 prepare()
方法,并供应适当的代码以添加屏幕上的每个 Actor
。这创建了你的场景的初始状态(用户首次运行你的场景时看到的那个)。在 ZombieInvasionWorld
中,我们手动实现 prepare()
方法,并且可以比 Greenfoot 以更紧凑的方式实现。我们使用循环来添加我们的演员。通过这种方法,我们添加了 Wall
、House
、Zombie1
和 Zombie2
。我们将在本章后面实现这些类。
act()
方法负责监听鼠标点击事件。如果鼠标被点击,我们将在鼠标的当前位置添加一个 Boom
对象。Boom 是我们创建的用于显示爆炸的演员,我们希望它正好放置在鼠标点击的位置。我们使用延迟变量 boomDelayCounter
来防止用户快速创建过多的爆炸。记住,我们在上一章(第二章,动画)中详细解释了延迟变量。如果你想让用户能够快速创建爆炸,那么只需简单地移除延迟变量。
创建障碍物
我们将为我们的僵尸群创建两个障碍:房屋和墙壁。在模拟中,House
对象没有任何功能。它只是为僵尸演员提供一个障碍:
import greenfoot.*;
public class House extends Actor {
}
House
类的代码非常简单。它的唯一目的是将房屋图像(buildings/house-8.png
)添加到 Actor
中。它没有其他功能。
墙壁比房屋更复杂。随着僵尸敲打墙壁,墙壁会慢慢破碎。Wall
类的大多数代码都实现了这种动画,如下面的代码所示:
import greenfoot.*;
import java.util.List;
public class Wall extends Actor {
int wallStrength = 2000;
int wallStage = 0;
public void act() {
crumble();
}
private void crumble() {
// We will implement this in the next section…
}
}
Wall
类的破碎动画实现与我们在上一章(第二章,动画)中看到的 Avatar
类受到伤害的实现非常相似。有趣的代码都包含在 crumble()
方法中,该方法从 act()
方法中反复调用。图 1 展示了墙壁在不同程度的衰变状态。我们将在 检测与多个对象的碰撞 部分详细实现并解释 crumble()
方法。
创建我们的主要演员框架
Zombie
类包含了描述我们模拟中僵尸行为的所有代码。僵尸不断地笨拙地向前移动,试图到达房子里的人类。他们会敲打并最终摧毁任何挡道的墙壁,如下面的代码所示:
import greenfoot.*;
import java.util.*;
public class Zombie extends Actor {
int counter, stationaryX, amplitude;
protected void addedToWorld(World w) {
stationaryX = getX();
amplitude = Greenfoot.getRandomNumber(6) + 2;
}
public void act() {
shake();
if( canMarch() ) {
stationaryX = stationaryX + 2;
}
}
public void shake() {
counter++;
setLocation((int)(stationaryX + amplitude*Math.sin(counter/2)), getY());
}
private boolean canMarch() {
// We will implement this in the next section…
return false; // Temporary return value
}
}
这个类中的两个重要方法是shake()
和canMarch()
。shake()
方法实现了僵尸的来回笨拙移动。它调用setLocation()
并保持y
坐标不变。它将x
坐标改为正弦运动(来回)。它来回移动的距离由amplitude
变量定义。这种运动也被用于第二章中描述的一种电源关闭,动画,并在图 2中显示。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00281.jpeg
图 2:这是使用正弦波在僵尸对象中产生来回运动的插图。我们从一个标准的正弦波(a)开始,将其旋转 90 度(b),并减少在 y 方向上的移动量,直到达到期望的效果(在 y 方向上不移动)。呼叫(c)和(d)显示了减少 y 方向移动的效果。
我们将在检测与多个对象的碰撞部分中完全实现并解释canMarch()
。canMarch()
方法检查周围的演员(房子、墙壁或其他僵尸),以查看是否有任何阻碍僵尸向前移动。作为一个临时措施,我们在canMarch()
的末尾插入以下行:
return false;
这允许我们编译和测试代码。通过始终返回false
,Zombie
对象将永远不会向前移动。这是一个简单的占位符,我们将在本章后面实现真正的响应。
我们有两个Zombie
类的子类:Zombie1
和Zombie2
:
public class Zombie1 extends Zombie {
}
public class Zombie2 extends Zombie {
}
这使得我们能够拥有两种不同外观的僵尸,但只需编写一次僵尸行为的代码。我选择了一个蓝色(people/ppl1.png
)僵尸和一个黄色橙色(people/ppl3.png
)僵尸。如果你有任何艺术技巧,你可能想创建自己的PNG
图像来使用。否则,你可以继续使用 Greenfoot 提供的图像,就像我这样做。
创建爆炸
这里是我们在ZombieInvasionWorld
类描述中之前讨论过的Boom
类的实现。Boom
类将立即绘制一个爆炸,这将清除爆炸范围内的所有内容,然后短暂停留,之后消失。我们使用以下代码创建爆炸:
import greenfoot.*;
import java.awt.Color;
import java.util.List;
public class Boom extends Actor {
private static final int BOOMLIFE = 50;
private static final int BOOMRADIUS = 50;
int boomCounter = BOOMLIFE;
public Boom() {
GreenfootImage me = new GreenfootImage
(BOOMRADIUS*2,BOOMRADIUS*2);
me.setColor(Color.RED);
me.setTransparency(125);
me.fillOval(0 , 0, BOOMRADIUS * 2, BOOMRADIUS*2);
setImage(me);
}
public void act() {
if( boomCounter == BOOMLIFE)
destroyEverything(BOOMRADIUS);
if( boomCounter-- == 0 ) {
World w = getWorld();
w.removeObject(this);
}
}
private void destroyEverything(int x) {
// We will implement this in the next section…
}
}
让我们讨论构造函数(Boom()
)和act()
方法。Boom()
方法使用GreenfootImage
的绘图方法手动创建一个图像。我们就是这样使用这些绘图方法在上一章中展示的AvoiderGame
中绘制星星和眼睛,我们在上一章中介绍了它,第一章, 让我们直接进入…,和第二章, 动画。构造函数通过使用setImage()
将这个新图像设置为演员的图像来结束。
act()
方法使用了延迟变量的有趣用法。不是等待一定的时间(以act()
方法的调用次数来衡量)后才允许事件发生,而是使用boomCounter
延迟变量来控制这个Boom
对象存活的时间。经过短暂的延迟后,对象将从场景中移除。
我们将在后面的部分讨论destroyEverything()
方法的实现。
测试一下
你现在应该有一个几乎完整的僵尸入侵模拟。让我们编译我们的场景,确保在添加代码时消除任何引入的错别字或错误。这个场景不会做很多事情。僵尸会来回移动,但不会取得任何进展。你可以在运行中的场景的任何地方点击,看到Boom
爆炸;然而,它现在还不会摧毁任何东西。
让我们使用 Greenfoot 的碰撞检测方法使这个场景更有趣。
内置的碰撞检测方法
我们将遍历 Greenfoot 提供的所有碰撞检测方法。首先,我们将回顾一些方法并讨论它们的预期用途。然后,我们将基于更高级的碰撞检测方法(基于边界的和隐藏精灵)讨论剩余的方法。我们已经在 Avoider Game 的实现中使用了几个碰撞检测方法。我们在这里将简要描述这些特定方法。最后,我们不会讨论getNeighbors()
和intersects()
,因为这些方法仅适用于包含使用大于一个单元格大小创建的世界 Greenfoot 场景。
注意
单元格大小和 Greenfoot 世界
到目前为止,我们只创建了设置了World
构造函数的cellSize
参数为1
的世界(AvoiderWorld
和ZombieInvasionWorld
)。以下是从 Greenfoot 关于World
类的文档中摘录的内容:
public World(int worldWidth, int worldHeight, int cellSize)
Construct a new world. The size of the world (in number of cells) and the size of each cell (in pixels) must be specified.
Parameters:
worldWidth - The width of the world (in cells).
worldHeight - The height of the world (in cells).
cellSize - Size of a cell in pixels.
Greenfoot 网站上提供的简单教程主要使用大单元格大小。这使得游戏移动、轨迹和碰撞检测非常简单。另一方面,我们希望创建更灵活的游戏,允许平滑的运动和更逼真的动画。因此,我们将我们的游戏单元格定义为 1 x 1 像素(一个像素),相应地,我们将不会讨论针对具有大单元格大小的世界的方法,例如getNeighbors()
和intersects()
。
在我们讨论的过程中,请记住,我们有时会向我们的 ZombieInvasion
场景添加代码。
检测单个对象的碰撞
getOneIntersectingObject()
方法非常适合简单的碰撞检测,通常用于检查子弹或其他类型的敌人是否击中了游戏的主要主角,以便减去健康值、减去生命值或结束游戏。这是我们使用并在 第一章 中解释的方法,即 Let’s Dive Right in…,来构建 Avoider Game 的第一个工作版本。我们在此处不再讨论它,只会在下一节中提及它,作为说明 isTouching()
和 removeTouching()
的使用方法。
isTouching() 和 removeTouching()
以下是一个使用 getOneIntersectingObject()
的常见模式:
private void checkForCollisions() {
Actor enemy = getOneIntersectingObject(Enemy.class);
if( enemy != null ) { // If not empty, we hit an Enemy
AvoiderWorld world = (AvoiderWorld) getWorld();
world.removeObject(this);
}
}
我们在 Avoider Game 中多次使用了这个基本模式。isTouching()
和 removeTouching()
方法提供了一种更紧凑的方式来实现前面的模式。以下是一个使用 isTouching()
和 removeTouching()
而不是 getOneIntersectingObject()
的等效函数:
private void checkForCollisions() {
if( isTouching(Enemy.class) ) {
removeTouching(Enemy.class);
}
}
如果你只是要移除与对象相交的对象,那么你只需要 isTouching()
和 removeTouching()
方法。然而,如果你想要对相交的对象执行某些操作,这需要调用对象的类方法,那么你需要将相交的对象存储在命名变量中,这需要使用 getOneIntersectingObject()
方法。
小贴士
通常,始终使用 getOneIntersectingObject()
而不是 isTouching()
和 removeTouching()
。它更灵活,并且提供的代码更容易在未来扩展。
检测多个对象的碰撞
碰撞检测方法 getIntersectingObjects()
返回一个列表,包含所有被调用演员接触到的给定类别的演员。当需要针对接触特定演员的每个对象采取行动,或者需要根据接触该演员的对象数量来改变演员的状态时,需要使用此方法。当使用 getOneIntersectingObject()
时,你只关心至少被一个指定类型的对象接触。例如,在游戏 PacMan 中,每次你接触到幽灵时都会失去一条生命。无论你撞到的是一个、两个还是三个,最终结果都会相同——你会失去一条生命。然而,在我们的僵尸模拟中,Wall
演员根据当前敲打它的僵尸数量受到伤害。这是 getIntersectingObjects()
的完美应用。
在上面提供的 Wall
代码中,我们省略了 crumble()
方法的实现。以下是该代码:
private void crumble() {
List<Zombie> army = getIntersectingObjects(Zombie.class);
wallStrength = wallStrength - army.size();
if( wallStrength < 0 ) {
wallStage++;
if( wallStage > 4 ) {
World w = getWorld();
w.removeObject(this);
}
else {
changeImage();
wallStrength = 2000;
}
}
}
private void changeImage() {
setImage("brick"+wallStage+".png");
}
让我们快速回顾一下之前看到的内容。在第二章 动画 的 伤害角色 部分,我们每次角色被敌人触摸时都会改变角色的图像,使其看起来受损。我们在这里使用相同的动画技术来使其看起来像墙壁正在受损。然而,在这段代码中,我们给墙壁赋予了一个由 wallStrength
变量定义的耐久性属性。wallStrength
的值决定了墙壁在明显看起来更加破碎和裂缝之前可以承受多少次僵尸的撞击。
wallStrength
变量实际上是我们在上一章 第二章. 动画 中讨论的延迟变量的一个例子。这个变量不是延迟一定的时间,而是延迟一定数量的僵尸撞击。当 wallStrength
小于 0 时,我们会使用 changeImage()
方法更改图像,除非这是我们第四次破碎,这将导致我们完全移除墙壁。图 3 展示了我为这个动画创建并使用的墙壁图像。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00282.jpeg
图 3:这些是用于动画墙壁破碎的四个图像
现在,让我们讨论碰撞检测方法 getIntersectingObjects()
。当被调用时,此方法将返回所有与调用对象相交的给定类的对象。您可以通过将类作为此方法的参数提供来指定您感兴趣的类。在我们的代码中,我提供了参数 Zombie.class
,因此该方法只会返回所有接触墙壁的僵尸。由于继承,我们将得到所有 Zombie1
对象和所有 Zombie2
对象,它们都与对象相交。您可以使用在 List
接口中定义的方法访问、操作或遍历返回的对象。对于我们来说,我们只想计算我们碰撞了多少个僵尸。我们通过在从 getIntersectingObjects()
返回的 List
对象上调用 size()
方法来获取这个数字。
注意
Java 接口和 List
碰撞检测方法 getIntersectingObjects()
第一次让我们了解了 List
接口。在 Java 中,接口用于定义两个或多个类将共有的方法集。当 Java 类实现接口时,该类承诺它实现了该接口中定义的所有方法。因此,由 getIntersectingObjects()
返回的 Actor
对象集合可以存储在数组、链表、队列、树或其他任何数据结构中。无论用于存储这些对象的数据结构是什么,我们知道我们可以通过 List
接口中定义的方法访问这些对象,例如 get()
或 size()
。
更多信息,请参阅以下链接:docs.oracle.com/javase/tutorial/java/IandI/createinterface.html
。
在我们的ZombieInvasion
模拟中,我们需要再次使用getIntersectingObjects()
。在我们查看Zombie
类的代码时,我们留下了canMarch()
方法的实现未完成。现在让我们使用getIntersectingObjects()
来实现该方法。以下是代码:
private boolean canMarch() {
List<Actor> things = getIntersectingObjects(Actor.class);
for( int i = 0; i < things.size(); i++ ) {
if( things.get(i).getX() > getX() + 20 ) {
return false;
}
}
return true;
}
此方法检查是否有任何演员阻碍了该对象向前移动。它通过首先获取所有接触该对象的Actor
类的对象,然后检查每个对象是否位于该对象的前面来完成此操作。我们不关心Actor
是否在顶部、底部或后面接触调用对象,因为这些演员不会阻止该对象向前移动。canMarch()
中的这一行代码为我们提供了所有相交演员的列表:
List<Actor> things = getIntersectingObjects(Actor.class);
然后,我们使用for
循环遍历演员列表。要访问列表中的项目,您使用get()
方法。get()
方法有一个形式参数,指定了列表中您想要的对象的索引。对于列表中的每个演员,我们检查其x坐标是否在我们前面。如果是,我们返回false
(我们不能移动);否则,我们返回true
(我们可以移动)。
我们已经将crumble()
方法的实现添加到了Wall
类中(别忘了还要添加changeImage()
),并将canMarch()
方法的实现添加到了Zombie
类中。让我们编译我们的场景并观察发生了什么。我们的模拟几乎完成了。唯一缺少的是Boom
类中destroyEverything()
方法的实现。我们将在下一节中查看该实现。
检测范围内的多个对象
我们需要实现的最后一个方法来完成我们的模拟是destroyEverything()
。在这个方法中,我们将使用 Greenfoot 碰撞检测方法getObjectsInRange()
。此方法接受两个参数。我们在所有其他碰撞检测方法中都已经看到了第二个参数,它指定了我们正在测试碰撞的演员的类。第一个参数提供了一个围绕演员绘制的圆的半径,该半径定义了搜索碰撞的位置。图 4显示了radius
参数与搜索区域之间的关系。与getIntersectingObjects()
不同,getObjectsInRange()
返回一个列表,其中包含调用对象指定的范围内的演员。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00283.jpeg
图 4:这显示了getObjectsInRange()
方法中半径参数的作用
现在我们已经了解了getObjectsInRange()
方法,让我们看看destroyEverything()
方法的实现:
private void destroyEverything(int x) {
List<Actor> objs = getObjectsInRange(x, Actor.class);
World w = getWorld();
w.removeObjects(objs);
}
这种方法简短而强大。它调用getObjectsInRange()
,带有半径x
,这是在调用destroyEverything()
时传递给它的值,以及Actor.class
,在 Greenfoot 术语中意味着一切。所有在半径定义的圆内的对象都将由getObjectsInRange()
返回并存储在objs
变量中。现在,我们可以遍历objs
中包含的所有对象,并逐个删除它们。幸运的是,Greenfoot 提供了一个可以在一次调用中删除一组对象的函数。以下是它在 Greenfoot 文档中的定义:
public void removeObjects(java.util.Collection objects)
Remove a list of objects from the world.
Parameters:
objects - A list of Actors to remove.
是时候测试一下了
模拟完成。编译并运行它,确保一切按预期工作。记住,你可以点击任何地方来炸毁建筑、墙壁和僵尸。重置场景并移动事物。添加墙壁和僵尸,看看会发生什么。做得不错!
基于边界的碰撞检测方法
基于边界的碰撞检测涉及从Actor
开始向外逐步搜索,直到检测到碰撞,或者确定没有障碍物为止。该方法找到与之碰撞的项目的边缘(或边界)。这种方法在物体需要相互弹跳,或者一个物体落在另一个物体上并需要在该物体上停留一段时间时特别有用,例如,当用户控制的Actor
跳到平台上时。我们将在本章介绍这种碰撞检测方法,并在接下来的章节中使用它。
检测偏移量下的单物体碰撞
Greenfoot 的碰撞检测方法的偏移量版本非常适合基于边界的碰撞检测。它们允许我们在调用Actor
的中心的某个距离或偏移量处检查碰撞。为了演示这个方法的使用,我们将修改Zombie
类中canMarch()
方法的实现。以下是我们的修改版本:
private boolean canMarch() {
int i=0;
while(i<=step) {
int front = getImage().getWidth()/2;
Actor a = getOneObjectAtOffset(i+front, 0, Actor.class);
if( a != null ) {
return false;
}
i++;
}
return true;
}
通常,当一个演员移动时,它将通过一定数量的像素改变其位置。在Zombie
类中,如果僵尸可以移动,它们将移动多远被存储在step
变量中。我们需要通过在Zombie
类的顶部插入以下代码行来声明和初始化这个实例变量,如下所示:
private int step = 4;
使用step
变量来存储演员的移动长度是一种常见的做法。在上面的canMarch()
实现中,我们检查僵尸前方直到包括完整一步的每个像素。这由while
循环处理。我们将变量i
从0
增加到step
,每次在位置i + front
处检查碰撞。由于一个物体的原始位置是其中心,我们将front
设置为表示该演员的图像宽度的一半。图 5说明了这个搜索过程。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00284.jpeg
图 5:使用基于边界的检测,一个对象逐像素搜索碰撞。它从其前端开始,然后从前端+0 开始搜索对象,一直到前端+步长。
如果在我们的while
循环中的任何时间检测到碰撞,我们返回false
,表示演员不能向前移动;否则,我们返回true
。测试这个新的canMarch()
版本。
在偏移量处检测多对象碰撞
碰撞检测方法getObjectsAtOffset()
与getOneObjectAtOffset()
非常相似。正如其名所示,它只是返回给定偏移量处所有碰撞的演员。为了演示其用法,我们将像对getOneObjectAtOffset()
所做的那样重新实现canMarch()
。为了利用获取碰撞演员列表的优势,我们将在canMarch()
中添加一些额外的功能。对于每个阻挡僵尸前进运动的演员,我们将稍微推挤他们。
这是canMarch()
的实现:
private boolean canMarch() {
int front = getImage().getWidth()/2;
int i = 1;
while(i<=step) {
List<Actor> a = getObjectsAtOffset(front+i,0,Actor.class);
if( a.size() > 0 ) {
for(int j=0;j<a.size()&&a.get(j) instanceof Zombie;j++){
int toss = Greenfoot.getRandomNumber(100)<50 ? 1 : -1;
Zombie z = (Zombie) a.get(j);
z.setLocation(z.getX(),z.getY()+toss);
}
return false;
}
i++;
}
return true;
}
在这个版本中,我们使用while
循环和step
变量,与之前canMarch()
的getOneObjectAtOffset()
版本所做的方式几乎相同。在while
循环内部,我们添加了新的“推挤”功能。当我们检测到列表中至少有一个Actor
时,我们使用for
循环遍历列表,轻微地推动我们与之碰撞的每个演员。在for
循环中,我们首先使用instanceof
运算符检查Actor
类是否是Zombie
类。如果不是,我们跳过它。我们不希望有推挤Wall
或House
的能力。对于每个我们与之碰撞的僵尸,我们以相等的概率将toss
变量设置为1
或-1
。然后我们使用setLocation()
移动那个僵尸。这种效果很有趣,给人一种僵尸试图推挤和冲到前面的错觉。编译并运行带有canMarch()
更改的场景,看看结果如何。图 6展示了僵尸如何在前面的更改下聚集在一起。
注意
instanceof
运算符
Java 的instanceof
运算符检查左侧参数是否是从右侧指定的类(或其任何子类)创建的对象。如果是,它将返回true
;否则返回false
。如果左侧对象实现了右侧指定的接口,它也将返回true
。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00285.jpeg
图 6:这是僵尸推挤和冲向房屋中的人类的一个视图
隐藏精灵碰撞检测方法
getOneObjectAtOffets()
和 getObjectsAtOffset()
方法的缺点之一是它们只检查单个像素的粒度。如果一个感兴趣的对象位于提供给这些方法的偏移量上方或下方一个像素,那么将不会检测到碰撞。实际上,在这个实现中,如果你允许模拟运行直到僵尸到达房屋,你会注意到一些僵尸可以穿过房屋。这是因为像素检查在房屋之间失败。处理这种不足的一种方法是用隐藏精灵碰撞检测。图 7展示了这种方法。
https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/crt-greenfoot/img/image00286.jpeg
图 7:这显示了使用隐藏精灵来检查碰撞。
在隐藏精灵方法中,你使用另一个Actor
类来测试碰撞。图 7显示了一个Zombie
对象使用一个较小的、辅助的Actor
类来确定是否与花朵发生了碰撞。虽然隐藏精灵显示为半透明的红色矩形,但在实际应用中,我们会设置透明度(使用setTransparency()
)为0
,使其不可见。隐藏精灵方法非常灵活,因为你可以为你的隐藏精灵创建任何形状或大小,并且它没有像前两种碰撞检测方法那样只关注单个像素的问题。接下来,我们再次修改Zombie
类中的canMarch()
方法,这次使用隐藏精灵碰撞检测。
我们需要做的第一件事是创建一个新的Actor
,它将作为隐藏精灵使用。因为我们打算为僵尸使用这个隐藏精灵,所以让我们称它为ZombieHitBox
。现在创建这个Actor
的子类,并且不要将它与任何图像关联。我们将在构造函数中绘制图像。以下是ZombieHitBox
的实现:
import greenfoot.*;
import java.awt.Color;
import java.util.*;
public class ZombieHitBox extends Actor {
GreenfootImage body;
int offsetX;
int offsetY;
Actor host;
public ZombieHitBox(Actor a, int w, int h, int dx, int dy, boolean visible) {
host = a;
offsetX = dx;
offsetY = dy;
body = new GreenfootImage(w, h);
if( visible ) {
body.setColor(Color.red);
// Transparency values range from 0 (invisible)
// to 255 (opaque)
body.setTransparency(100);
body.fill();
}
setImage(body);
}
public void act() {
if( host.getWorld() != null ) {
setLocation(host.getX()+offsetX, host.getY()+offsetY);
} else {
getWorld().removeObject(this);
}
}
public List getHitBoxIntersections() {
return getIntersectingObjects(Actor.class);
}
}
ZombieHitBox
的构造函数接受六个参数。它之所以需要这么多参数,是因为我们需要提供它附加到的Actor
类(a
参数),定义要绘制的矩形的尺寸(w
和h
参数),提供矩形相对于提供的Actor
的偏移量(dx
和dy
参数),并检查隐藏精灵是否可见(visible
参数)。在构造函数中,我们使用GreenfootImage()
、setColor()
、setTransparency()
、fill()
和setImage()
来绘制隐藏精灵。我们之前在第二章 动画中讨论了这些方法。
我们使用act()
方法来确保这个隐藏精灵与它附加的Actor
类(我们将称之为host
精灵)一起移动。为此,我们只需调用setLocation()
,提供host
精灵当前的x和y位置,并根据构造函数中提供的偏移值进行微调。然而,在这样做之前,我们检查host
是否已被删除。如果已被删除,我们就删除碰撞框,因为它只与host
有关。这处理了爆炸摧毁host
但并未完全达到碰撞框的情况。
最后,我们提供一个公共方法,host
精灵将使用它来获取所有与隐藏精灵发生碰撞的精灵。我们把这个方法命名为getHitBoxIntersections()
。
接下来,我们需要增强Zombie
类以使用这个新的隐藏精灵。我们需要对这个隐藏精灵有一个引用,因此我们需要在Zombie
类的声明下添加一个新的属性。在step
变量的声明下插入此行代码:
private ZombieHitBox zbh;
接下来,我们需要增强addedToWorld()
方法来创建并将ZombieHitBox
连接到Zombie
。以下是该方法的实现:
protected void addedToWorld(World w) {
stationaryX = getX();
amplitude = Greenfoot.getRandomNumber(6) + 2;
zbh = new ZombieHitBox(this, 10, 25, 10, 5, true);
getWorld().addObject(zbh, getX(), getY());
}
我们为我们的隐藏精灵创建一个 10 x 25 的矩形,并最初使其可见,这样我们就可以在我们的场景中测试它。一旦你对隐藏精灵的位置和大小满意,你应该将ZombieHitBox
的visible
参数从true
更改为false
。
现在我们已经创建、初始化并放置了ZombieHitBox
,我们可以对canMarch()
进行修改,以展示隐藏精灵方法的使用:
private boolean canMarch() {
if( zbh.getWorld() != null ) {
List<Actor> things = zbh.getHitBoxIntersections();
if( things.size() > 1 ) {
int infront = 0;
for(int i=0; i < things.size(); i++ ) {
Actor a = things.get(i);
if( a == this || a instanceof ZombieHitBox)
continue;
if( a instanceof Zombie) {
int toss =
Greenfoot.getRandomNumber(100)<50 ? 1:-1;
infront += (a.getX() > getX()) ? 1 : 0;
if( a.getX() >= getX() )
a.setLocation(a.getX(),a.getY()+toss);
} else {
return false;
}
}
if( infront > 0 ) {
return false;
} else {
return true;
}
}
return true;
} else {
getWorld().removeObject(this);
}
return false;
}
与之前的canMarch()
实现不同,我们首先需要询问隐藏精灵获取与这个僵尸碰撞的演员列表。一旦我们得到这个列表,我们检查它的大小是否大于一个。它需要大于一个的原因是ZombieHitBox
将包括它所附着的僵尸。如果我们没有与其他僵尸或演员发生碰撞,我们返回true
。如果我们与多个演员发生碰撞,那么我们将遍历它们所有,并根据Actor
的类型做出一些决定。如果Actor
是这个僵尸或ZombieHitBox
的实例,我们跳过它并且不采取任何行动。下一个检查是Actor
是否是Zombie
类的实例。如果不是,那么它是一些其他对象,比如House
或Wall
,我们返回false
,这样我们就不会向前移动。如果是Zombie
类的实例,我们检查它是否在这个僵尸的前面。如果是,我们稍微推它一下(就像我们在之前的canMarch()
实现中做的那样)并增加infront
变量。遍历演员列表结束后,我们检查infront
变量。如果有僵尸在这个僵尸的前面,我们返回false
以防止它向前移动。否则,我们返回true
。最外层的if
语句简单地检查与这个对象关联的击中框(zbh
)是否已经被Boom
对象之前销毁。如果是,那么我们需要移除这个对象。
编译并运行这个场景版本。你应该观察到僵尸们很好地聚集在一起,互相推搡,但它们无法越过房屋。使用隐藏精灵的碰撞检测方法比其他方法复杂一些,但提供了很好的精度。
挑战
好的,我们在僵尸模拟中实现了多种形式的碰撞检测。你更喜欢哪种碰撞检测方法用于这个模拟?
作为挑战,创建一个Actor
球,它偶尔从左侧滚动过来,并将僵尸推开。如果球击中Wall
,让它对它造成 1,000 点伤害。你将使用哪种形式的碰撞检测来检测球与僵尸以及球与墙壁之间的碰撞?
概述
碰撞检测是任何游戏、模拟或交互式应用的关键组成部分。Greenfoot 提供了检测碰撞的内置方法。在本章中,我们详细解释了这些方法,并展示了如何使用它们进行更高级的碰撞检测。具体来说,我们讨论了基于边界的和隐藏精灵技术。向前推进,我们将经常使用碰撞检测,并选择适合我们示例的方法。在下一章中,我们将探讨投射物,并将有充足的机会将本章学到的知识付诸实践。
更多推荐
所有评论(0)