Java8-游戏开发入门手册-全-

Java8 游戏开发入门手册(全)

协议:CC BY-NC-SA 4.0

一、设置 Java 8 游戏开发环境

欢迎阅读《Java 8 游戏开发入门》一书!让我们从创建本书使用的坚实的开发软件基础开始。这个基础的核心将是 Java SDK(软件开发工具包)8,也称为 JDK (Java 开发工具包)8。我还将为您设置 NetBeans IDE 8.0(集成开发环境),这将使编写 Java 8 游戏变得容易得多。之后,我将向您介绍最新的开源新媒体内容创作软件包,用于数字插图(Inkscape)、数字成像(GIMP [GNU 图像处理器])、数字视频(EditShare Lightworks)、数字音频(Audacity)以及 3D 建模和动画(Blender)。在本章的最后,我还会推荐一些其他的专业级软件包,你应该考虑把它们添加到你将在本章的课程中创建的专业游戏开发工作站中。

为了从所有这些免费的专业级软件中获得最佳效果,您将需要一个现代化的 64 位工作站,至少具有 4GB 的系统内存(6GB 或 8GB 更好)和一个多核处理器(中央处理器[CPU]),如 AMD FX-6300(六核)、AMD FX-8350(八核)或英特尔 i7(四核)。诸如此类的工作站已经成为商品,可以在沃尔玛或 Pricewatch.com 以实惠的价格购买。

在本章中,您要做的第一件事是确保您已经删除了任何过时的 Java 版本,如 Java 7 或 Java 6,或者任何过时的 NetBeans 版本,如 NetBeans 7 或 NetBeans 6。这包括从您的工作站卸载(移除或完全删除)这些较旧的开发软件版本。

您将使用 Windows 程序管理实用程序程序和功能来完成此操作,这些程序和功能可以在 Windows 操作系统管理实用程序的 Windows 操作系统(OS)控制面板套件中找到。在 Linux 和 Mac 平台上也有类似的工具,如果你碰巧使用了这些不常用的操作系统。因为大多数开发人员使用 Windows 7、8 或 9,所以对于本书中的示例,您将使用 Windows 64 位平台。

接下来,我将向您展示在互联网上的确切位置可以获得这些软件包,所以请准备好启动您的快速互联网连接,以便您可以下载近 1g 的全新游戏内容制作软件!下载所有这些软件的最新版本后,您将安装编程和内容开发包,并对它们进行配置以用于本书。

执行这些软件安装的顺序很重要,因为 Java JDK 8 和 Java 8 运行时环境(JRE)构成了 NetBeans IDE 8.0 的基础。这是因为 NetBeans IDE 8.0 最初是使用 Java 编程语言编写的,所以您会看到使用这种语言的软件是多么的专业。因此,Java 8 软件将是您安装的第一个软件。

安装 Java 8 后,您将安装 NetBeans 8.0,以便在 Java 编程语言之上拥有一个图形用户界面(GUI),这将使 Java 软件开发工作过程更容易。安装这两个主要的软件开发工具后,您将获得大量新的媒体内容创建软件包,可以与 Java 8 和 NetBeans 8.0 结合使用来创建 2D 和 3D 游戏。

为 Java 8 游戏开发准备工作站

假设你已经有一个专业级的工作站用于新媒体内容开发和游戏开发,你需要删除所有过时的 JDK 和 ide,并确保你有最新的 V8(不是饮料,傻瓜!)Java 和 NetBeans 软件已安装在您的系统上,随时可以使用。如果你是新手,没有适合游戏的工作站,去沃尔玛或 Pricewatch.com,购买一台经济实惠的多核(使用 4 核、6 核或 8 核)64 位计算机,运行 Windows 8.1(或 9.0,如果有),至少有 4GB、6GB 或 8GB 的 DDR3 (1333 或 1600 内存访问速度)系统内存和 750GB,甚至 1TB 的硬盘驱动器。

删除旧软件的方法是通过 Windows 控制面板及其一组实用程序图标,其中一个是程序和功能图标(Windows 7 和 8),如图 1-1 所示。请注意,在 Windows 的早期版本中,该实用程序图标的标签可能会有所不同,可能是“添加或删除程序”之类的标签。

A978-1-4842-0415-3_1_Fig1_HTML.jpg

图 1-1。

Use the Programs and Features utility icon to uninstall or change programs on your computer workstation

单击“程序和功能”链接,或者在以前版本的 Windows 中双击图标,以启动该实用程序。然后,向下滚动查看您的工作站上是否安装了任何旧版本的 Java 开发工具(Java 5、Java 6 或 Java 7)。请注意,如果您有一个全新的工作站,您应该会发现您的系统上没有预装的 Java 或 NetBeans 版本。如果您确实找到了它们,请归还系统,因为它以前可能已经被使用过了!

如图 1-2 所示,在我的 Windows 7 HTML5 开发工作站上,我安装了一个旧版本的 Java,Java 7(2013 年 11 月 29 日),占用了 344MB 的空间。要删除某个软件,请通过单击选择它(它将变为浅蓝色),然后单击图顶部显示的卸载按钮。我留下了屏幕截图中显示的工具提示,“卸载该程序”,这样您就可以看到,如果您将鼠标悬停在程序和功能实用程序中的任何内容上,它会告诉您该功能的用途。

单击卸载按钮后,该实用程序将删除旧版本的 Java。如果您想要保留旧的 Java 项目文件,请确保备份您的 Java 项目文件文件夹(如果您还没有这样做的话)。确保定期备份工作站的硬盘驱动器,以免丢失任何工作。

A978-1-4842-0415-3_1_Fig2_HTML.jpg

图 1-2。

Select any version of Java older than the current version (Java 8), and click the Uninstall button at the top

还要确保卸载所有版本的 Java 在我的例子中,有 64 位 Java 7 update 45 和 Java SDK 7u45,用于运行或执行 ide,如 NetBeans(或 Eclipse),它们是用 Java 编程语言编写的。

接下来,您需要确定您的工作站上是否有任何旧版本的 NetBeans IDE。在我的例子中,如图 1-3 所示,我的 64 位 Windows 7 工作站上确实安装了 NetBeans 7 IDE。我选择了要删除的,然后单击了卸载/更改按钮,如左边所示,这将弹出一个自定义卸载摘要对话框,如右边所示。

制造商(在本例中是 NetBeans 开发团队)可以为他们的产品创建自定义的卸载摘要对话框,以便在卸载过程中使用,正如您在此处看到的。此对话框允许您选择是否要卸载 GlassFish Server 4 和 NetBeans UserDir 配置文件夹。因为您正在安装 NetBeans 和 GlassFish 的新版本,所以请选中这两个复选框,然后单击“卸载”按钮。

A978-1-4842-0415-3_1_Fig3_HTML.jpg

图 1-3。

Find and select any version of NetBeans that is older than version 8.0; also, uninstall old GlassFish versions

下载 Java JDK 8 和 NetBeans 8.0

既然已经从您的工作站上删除了 Java 和 NetBeans 的过时版本,那么您将需要访问 Internet,分别访问 Oracle 和 NetBeans 网站,以获得最新的开发 SDK 和 ide。在写这本书的时候,我将向你展示如何使用谷歌的搜索引擎(我使用这种方法是为了防止下载链接或 URL 发生变化)以及演示当前的直接下载 URL。

让我们先来看看 Java 8,因为它是你阅读本书时将要做的所有事情的基础。谷歌搜索 Java JDK 8 会给你甲骨文 Java 下载页面的搜索结果,该页面位于甲骨文技术网部分,如图 1-4 截图顶部所示。此页面的 URL 当前为 www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 。值得注意的是,这个网址可能会在未来的任何时候改变,你可以随时使用谷歌搜索找到最新的。在下载 64 位 Windows 7/8 的 170MB SDK 安装程序文件之前,您需要单击 Java 8 下载表左上角显示的接受许可协议选项旁边的单选按钮。一旦您接受许可协议,这 11 个特定于操作系统的链接将被激活使用。

A978-1-4842-0415-3_1_Fig4_HTML.jpg

图 1-4。

Google the term “Java JDK 8,” open the JDK 8 Downloads page, and select Accept License Agreement

确保下载的 Java JDK 8 软件符合您的操作系统和位级别(x86 表示 32 位级别的操作系统)。大多数现代工作站使用 64 位 Linux、Mac、Solaris (Oracle)、Windows 7 或 Windows 8 操作系统。这将用操作系统名称后的 x64 描述来指定。

要找出操作系统的位级别,在 Windows 7 上,打开“开始”菜单,右键单击计算机条目,然后选择上下文菜单底部的“属性”选项。在 Windows 8 上,单击开始(如果您处于 Windows 7 桌面模式,则是桌面左下角的窗口窗格图标),然后单击左下角的向下箭头图标,再单击电脑设置紫色齿轮图标,最后单击屏幕左下角的电脑信息条目。在这两个用例中,都应该有一个文本条目,说明系统类型和 32 位操作系统或 64 位操作系统。

现在您已经下载了 Java JDK 8 安装程序,接下来您需要做的是下载 NetBeans IDE 8.0。在谷歌上搜索 NetBeans 8.0,如图 1-5 顶部所示,点击下载搜索结果选项,将会进入 NetBeans IDE 8.0.1 下载页面(目前为 https://netbeans.org/downloads )。如果您想像我一样在浏览器中保持两个选项卡都打开,那么右键单击下载链接,并选择在新选项卡中打开链接选项。

A978-1-4842-0415-3_1_Fig5_HTML.jpg

图 1-5。

Google the term “NetBeans 8.0,” open the NetBeans IDE 8.0.1 Download page, and download all versions

进入 NetBeans IDE 8.0 下载页面后,从页面右上角的下拉菜单中选择您正在使用的语言和平台(OS)。我选了英语和 Windows。现在,您可以单击页面底部三个下载按钮中的一个来下载支持 JavaFX 8 新媒体(因此将支持游戏开发)编程语言(应用编程接口[API])的 NetBeans IDE 8.0。当我详细讲述 Java 编程语言时,你会在第三章中了解到更多关于 API 的知识。

如果你只打算开发面向个人的 Java SE(标准版)和 JavaFX 应用(游戏),那么点击第一个按钮。如果你要开发 Java EE(企业版)和 JavaFX 应用(游戏)用于企业(商务),那么点击第二个按钮。如果您打算同时开发 JavaFX 和 HTML5 应用(游戏),这也是我为自己的业务所做的,那么您可以单击第五个下载按钮,下载 NetBeans IDE 8.0 的“全部”版本。该版本将允许您使用 NetBeans 支持的所有编程语言进行开发!

因为 NetBeans IDE 是免费的,并且您的工作站硬盘驱动器可以处理大量数据,所以我建议您安装这个 204MB All 版本的 IDE,以防您需要 NetBeans IDE 8.0 作为软件开发人员能够为您提供的任何其他功能(Java EE、Java ME、PHP、HTML5、Groovy、GlassFish、Tomcat、C++)。如果您要安装客户端或 Java SE IDE 版本,这需要额外的 120MB 磁盘空间,但是如果您要安装服务器端或 Java EE IDE 版本,这需要不到 20MB 的额外磁盘空间。

单击下载按钮后,软件下载将开始。完成后,您就可以安装 Java 8 和 NetBeans IDE 8.0 了。最后,为了完成全面的 Java 8 游戏开发工作站的设置,您将获得一些辅助的新媒体内容工具。当你通读这本书时,你将能够使用工作站来创建可交付的 epic Java 8 游戏!越来越令人兴奋了!

安装 Java 8 软件开发环境

NetBeans IDE 8.0 需要安装 Java 才能运行,因此您需要先安装 JDK 和 JRE。因为你想用最新的、功能最丰富的 Java 版本开发游戏,所以你要安装 2014 年发布的 Java 8。安装最新版本的软件可以确保您拥有最新的功能和最少的错误。确保经常检查您是否使用了所有软件包的最新版本;毕竟,这些都是开源的,可以免费下载、升级和使用!

第一步是找到您将安装程序文件下载到系统中的位置。默认值应设置为 Windows 中的下载文件夹。我把我的下载到一个C:/Clients/Java8文件夹,如图 1-6 所示。

A978-1-4842-0415-3_1_Fig6_HTML.jpg

图 1-6。

Find the JDK 8 install file on your hard disk drive, right-click it, and select Run as administrator

该文件将使用格式 jdk-version-platform-bitlevel 命名,因此要找到最新的版本(在本例中,它是jdk-8u25-windows-x64)。右键单击它,并选择 Run as administrator 选项,这样安装程序就拥有了创建文件夹、向其中传输文件等所需的所有操作系统“权限”。

启动安装程序后,您会看到欢迎对话框,如图 1-7 (左)所示。点击下一步按钮进入选择要安装的功能对话框,如图 1-7 (右图)所示,并接受默认值。

A978-1-4842-0415-3_1_Fig7_HTML.jpg

图 1-7。

Click Next in the Welcome dialog to advance to the Select Features to Install dialog, and then click the Next button

如你所见,安装程序会将 180MB 的软件安装到你工作站上的C:\ProgramFiles\Java\jdk1.8.0_25文件夹中。单击下一步按钮开始安装过程,安装过程将提取安装文件,然后使用动画进度条将它们复制到您的系统上,如图 1-8 (左图)所示。

A978-1-4842-0415-3_1_Fig8_HTML.jpg

图 1-8。

Java 8 installation will extract and copy install files (left) and then suggest the installation directory (right)

在你的系统上安装了 Java SDK 之后,你会看到 JRE 安装对话框,如图 1-8 (右图)所示。请确保您接受此 JRE 的默认安装位置;它应该安装在\Java\jre8文件夹中。最好允许 Oracle (Java SDK)将软件放在行业标准的文件夹位置,因为您将使用的使用此 JRE 的其他软件包(如 NetBeans IDE 8.0)将首先在那里查找它。单击“下一步”按钮安装 JRE。

A978-1-4842-0415-3_1_Fig9_HTML.jpg

图 1-9。

During installation a progress bar shows you what is installing (left) and then gives you a completed dialog (right)

安装过程中会显示一个进度条,如图 1-9 (左图)所示。完成后会显示安装成功对话框,如图 1-9 (右图)所示。如果您想要访问教程、API 文档、开发人员指南、版本发布说明等等,您可以单击“后续步骤”按钮。

安装 NetBeans IDE 8.0

现在,你已经准备好安装 NetBeans 了,所以找到你的netbeans-8.0-windows文件(见图 1-6 )。右键单击它,并选择“以管理员身份运行”选项来启动安装程序。一旦启动,你会看到如图 1-10 所示的对话框,它给你一个定制按钮,你可以用它来定制安装。

A978-1-4842-0415-3_1_Fig10_HTML.jpg

图 1-10。

The Welcome to the NetBeans IDE 8.0 Installer dialog

单击下一步按钮开始默认(完全)安装,您将看到 NetBeans IDE 8.0 许可协议对话框,如图 1-11 (左图)所示。选中“我接受许可协议中的条款”复选框,并单击“下一步”按钮前进到“JUnit 许可协议”对话框,如图 1-11 (右图)所示。

A978-1-4842-0415-3_1_Fig11_HTML.jpg

图 1-11。

Accept the terms of the license agreement, click the Next button (left), and then do the same for JUnit (right)

在 JUnit 许可协议对话框中,如图 1-11 (右图)所示,单击我接受许可协议声明中的条款旁边的单选按钮,然后单击下一步按钮继续安装。如图 1-12 所示,接下来的两个安装程序对话框将允许您指定 NetBeans 8.0 和 GlassFish 4.0 在系统上的安装位置。我建议也接受这两个对话框中的默认安装位置。您会注意到,NetBeans 安装程序也在默认位置找到了您的 Java 安装。

A978-1-4842-0415-3_1_Fig12_HTML.jpg

图 1-12。

Accept the default installation directory suggestions for NetBeans IDE (left) and GlassFish 4.0 (right)

一旦您接受了这些默认的安装位置,并单击 Next 按钮在这些对话框中前进,您将得到一个摘要对话框,如图 1-13 (左)所示。此对话框包含一个“安装”按钮,该按钮将触发您在前面五个 NetBeans IDE 8.0 安装对话框中设置的安装。

A978-1-4842-0415-3_1_Fig13_HTML.jpg

图 1-13。

Select the Check for Updates check box, and click the Install button (left) and the Finish button (right)

在安装过程中,您会看到安装对话框及其进度条,如图 1-14 所示,它会告诉您安装已完成的确切百分比,以及当前正在您的工作站上提取和安装哪些 IDE 文件。

当安装过程完成时,您将看到设置完成对话框,如图 1-13 (右图)所示。现在,您可以在您的工作站上开发 Java 8 和 JavaFX 应用(游戏)了。

A978-1-4842-0415-3_1_Fig14_HTML.jpg

图 1-14。

The Installation progress dialog, showing the percentage of install complete

接下来,让我们下载五个最流行的免费开源新媒体内容开发软件包,这样您就拥有了 Java 8 游戏开发业务所需的所有工具!

之后,您将会看到我在工作站上使用的其他一些令人印象深刻的开源软件。这样,如果您愿意,甚至在完成本章之前,您就可以组装出终极软件开发工作站,仅用硬件(和操作系统)的成本就可以创建一个非常有价值的内容制作工作站!

安装新媒体内容制作软件

JavaFX 支持许多新媒体元素或资产的“类型”, Java FX 是 Java 8(和 Java 7)中的新媒体引擎,也是 Java 8 游戏开发的基础。新媒体的主要类型包括数字插图、数字图像、数字音频、数字视频和 3D,您将在本章的剩余部分安装领先的开源软件。

下载并安装 Inkscape

因为 JavaFX 支持数字插图软件包(如 Adobe Illustrator 和 FreeHand)中常用的 2D(或矢量)技术,所以您将下载并安装流行的开源数字插图软件包 Inkscape。

Inkscape 适用于 Linux、Windows 和 Mac 操作系统,就像你在本章中安装的所有软件包一样,所以你可以使用任何你喜欢的平台来开发游戏!

在网上找 Inkscape 软件包,去 Google 搜索,输入 Inkscape,如图 1-15 ,左上方。单击下载链接(或右键单击,并在单独的选项卡中打开),然后单击代表您正在使用的操作系统的图标。企鹅代表 Linux(最左边的图标),窗口代表 Windows(中间的图标),风格化的苹果代表 Mac(最右边的图标)。

A978-1-4842-0415-3_1_Fig15_HTML.jpg

图 1-15。

Google the term “InkScape,” go to the Inkscape Download page, and click the icon that matches your OS

如果您想使用 64 位 Windows 版本的 Inkscape,请向下滚动,查看这三个图标下面的文本链接,以访问特定的操作系统下载。下载完软件后,右键单击它并以管理员身份运行,然后在您的工作站上安装软件。如果您有以前版本的 Inkscape,安装程序会将其升级到最新版本。您不需要使用本章前面使用的程序和功能实用程序来卸载 SDK 和 ide,它们不会像新媒体制作软件包那样升级以前的版本。

安装软件后,在任务栏上创建一个快速启动图标,这样只需单击鼠标即可启动 Inkscape。接下来,您将安装一个流行的数字图像软件包 GIMP,它允许您以 JavaFX 支持的 JPEG、PNG 或 GIF 数字图像文件格式为您的游戏创建“光栅”或基于像素(位图)的艺术作品。光栅图像不同于矢量,或形状,插图,所以你需要 GIMP。

下载和安装 GIMP

JavaFX 还支持使用光栅图像技术的 2D 图像,该技术将图像表示为像素阵列,常用于数字图像合成软件包,如 Adobe Photoshop 和 Corel Painter。在本节中,您将下载并安装流行的开源数字图像编辑和合成软件包 GIMP。该软件适用于 Linux、Windows、Solaris、FreeBSD 和 Mac 操作系统。

在网上找 GIMP 软件,去 Google 搜索,输入 GIMP,如图 1-16 所示。

A978-1-4842-0415-3_1_Fig16_HTML.jpg

图 1-16。

Google the term “GIMP,” go to the GIMP Downloads page, and click the Download GIMP link

单击下载链接(或右键单击,并在单独的选项卡中打开它),然后单击下载 GIMP 2.8.14(或代表您正在使用的操作系统的最新版本)。下载页面将自动检测您正在使用的操作系统,并为您提供正确的操作系统版本;对我来说,是 Windows。下载并安装最新版本的 GIMP,然后为您的工作站任务栏创建一个快速启动图标,就像您为 Inkscape 所做的那样。接下来,您将安装一个强大的数字音频编辑和音频效果软件包,名为 Audacity。

下载并安装 Audacity

JavaFX 支持使用数字音频技术的数字音频序列。数字音频通过采集数字音频样本来表示模拟音频。数字音频内容通常使用数字音频合成和序列器软件包创建,如 Propellerhead Reason 和 Cakewalk Sonar。在本节中,您将下载并安装流行的开源数字音频编辑和优化软件包 Audacity。Audacity 可用于 Linux、Windows 和 Mac 操作系统,因此您可以使用任何您喜欢的操作系统平台来为基于 Java 8 和 JavaFX 的游戏创建和优化数字音频。

要在网上找到 Audacity 软件包,使用谷歌搜索引擎,输入 Audacity,如图 1-17 左上角所示。单击下载链接(或者右键单击,并在单独的选项卡中打开),然后单击 Audacity for Windows(或者代表您正在使用的操作系统的版本)。

A978-1-4842-0415-3_1_Fig17_HTML.jpg

图 1-17。

Google the term “Audacity,” go to the Audacity Download page, and click a link matching your OS

下载并安装 Audacity 的最新版本(目前是 2.0.6),然后为您的工作站任务栏创建一个快速启动图标,就像您为 Inkscape 和 GIMP 所做的那样。接下来,您将安装一个强大的数字视频编辑和特效软件包,名为 EditShare Lightworks。

下载并安装 EditShare Lightworks

JavaFX 还支持数字视频,它使用基于光栅像素的运动视频技术。光栅将视频表示为一系列帧,每个帧包含一个基于像素阵列的数字图像。数字视频资产通常是使用数字视频编辑和特效软件包创建的,如 Adobe After Effects 和 Sony Vegas。在本节中,您将下载并安装名为 Lightworks 的开源数字视频编辑软件。

EditShare 的 Lightworks 曾经是一个付费软件包,直到它被开源。您必须在 Lightworks 网站上注册才能下载和使用该软件。该软件包适用于 Linux、Windows 和 Mac 操作系统。要在网上找到 Lightworks,进入谷歌搜索,输入 Lightworks,如图 1-18 所示,在左上方。单击下载链接(或右键单击,并在单独的选项卡中打开),然后单击相应的下载按钮和代表您正在使用的操作系统的选项卡。下载页面将自动检测您正在使用的操作系统,并选择正确的操作系统选项卡;在我的情况下,Windows。

A978-1-4842-0415-3_1_Fig18_HTML.jpg

图 1-18。

Google the term “Lightworks,” go to the Lightworks Downloads page, and click the tab that matches your OS

如果您还没有注册,请在 Lightworks 网站上注册。一旦获得批准,您就可以下载并安装最新版本的 Lightworks。安装软件,并为您的任务栏创建一个快速启动图标,就像您对其他软件所做的那样。接下来,您将安装一个名为 Blender 的 3D 建模和动画包。

下载和安装 Blender

JavaFX 最近开始支持在 JavaFX 环境之外创建的 3D 新媒体资产,这意味着您将能够使用第三方软件包创建 3D 模型、纹理和动画,例如 Autodesk 3D Studio Max 或 Maya 和 NewTek Lightwave 3D。在本节中,您将下载并安装流行的开源三维建模和动画软件包 Blender。Blender 适用于 Linux、Windows 和 Mac 操作系统,因此您可以使用任何您喜欢的操作系统平台来创建和优化 3D 模型、3D 纹理映射和 3D 动画,以便在 Java 8 和 JavaFX 游戏中使用。

要在网上找到 Blender 软件,使用谷歌搜索引擎,输入 Blender,如图 1-19 所示。点击正确的下载链接下载并安装 Blender,然后创建快速启动图标。

A978-1-4842-0415-3_1_Fig19_HTML.jpg

图 1-19。

Google the term “Blender,” go to the Blender Download page, and click the tab for your OS

其他感兴趣的开放源码软件包

我在我的新媒体内容制作业务中使用了许多其他的专业级开源软件包,我想我应该让你知道一下,以防你没有听说过它们。这将为您到目前为止构建的新媒体制作工作站增加更多功能和多样性。值得注意的是,您已经为自己节省了数千美元,否则在进行所有这些大量下载和安装的过程中,您将花费在类似的付费内容制作软件包上。我想你可能会说我的座右铭是,“第一次就做对,并确保一直做下去”,所以让我告诉你一些其他免费的,甚至一些更实惠的新媒体内容制作软件包,我已经安装在我自己的内容制作工作站上。

除了过去售价高达六位数的 EditShare Lightworks 软件包之外,开源软件的最佳价值之一是一个商业生产力软件套件,Oracle 在收购 Sun Microsystems 后将其开源。甲骨文将其 OpenOffice 软件套件转移到流行的 Apache 开源项目中。OpenOffice 4.1 是一个完整的办公效率软件套件,包含六个成熟的商务效率软件包!因为您的内容制作代理实际上是一个成熟的企业,您可能应该了解这个软件,因为它是一个非常可靠的开源软件产品。可以在 www.openoffice.org 找到;这个流行的商业软件包已经被像你这样精明的专业人士下载了超过一亿次,所以正如他们所说的,这不是一个玩笑!

Audacity 数字音频编辑软件的一个很好的补充是 Rosegarden MIDI sequencing 和 music composition and scoring 软件,它可以用于音乐创作和打印音乐出版的结果乐谱。玫瑰园,目前在 14.02 版本,正在从 Linux 移植到 Windows,可以通过谷歌搜索或在 www.rosegardenmusic.com 找到。

另一个令人印象深刻的音频、MIDI 和声音设计软件包是 Qtractor 如果您运行的是 Linux 操作系统,请务必通过谷歌搜索或前往 https://Qtractor.SourceForge.net 下载并安装这个专业级数字音频合成软件包。

对于 3D 角色建模和动画,有机会一定要去看看 DAZ Studio 的 3D 软件包( www.daz3d.com )。DAZ Studio Pro 目前的版本是 4.6,没错,是免费的!你必须登录并注册,就像你在 EditShare Lightworks 上做的那样,但这只是小小的代价!这个网站上还有一个免费的 3D 建模软件包,名为 Hexagon 2.5,还有一个不到 20 美元的流行地形生成软件包,名为 Bryce 7.1 Pro。DAZ Studio 网站上最贵的软件是 Carrara (150 美元)和 Carrara Pro (285 美元)。DAZ 工作室的大部分收入来自销售各种类型的角色模型,所以看看吧,因为它是 3D 内容(虚拟)世界中不可忽视的力量!

另一个令人印象深刻(基本版免费)的世界级软件包是英国 Planetside Software 公司的 Terragen 3.2。你可以从 https://planetside.co.uk 下载基础版,也可以加入它的论坛。我已经在我的几本 Android 应用开发书中使用过这个软件,所以我知道它对多媒体应用和游戏很有效。它也被专业电影制作人使用,因为它的质量水平是原始的。

Caligari TrueSpace 7.61 也是非常优秀的免费 3D 建模和动画软件。节目,也就是“自由而活着!”根据 Caligari 的网站( https://Caligari.us ),你仍然可以从那里下载它,当它最初由 Caligari 公司的创始人罗曼·奥曼迪(后来被微软收购)开发时,它曾花费近 1000 美元。一个专业级的 3D 建模和动画软件包,这个程序在全盛时期拥有数百万用户。这是一个非常酷的软件,有一个有趣的用户界面,所以一定要抓住它!

另一个你应该看看的 3D 渲染软件是 POV-Ray(视觉光线跟踪器的持久性),它可以与任何 3D 建模和动画软件包配合使用,使用高级光线跟踪渲染算法来生成令人印象深刻的 3D 场景。POV-Ray 网站上的最新版本(www.povray.org)是 3.7,与 64 位和多核(多线程)兼容,可以免费下载!

Bishop3D 是一个很酷的 3D 建模软件包,专门设计用于 POV-Ray。该软件可用于创建自定义 3D 对象,然后可将其导入 POV-Ray(然后导入 JavaFX)以在您的游戏中使用。最新的版本是 Windows 7 版的 1.0.5.2,下载量为 8MB。该软件可以在 www.bishop3d.com 找到,目前可以免费下载!

另一个值得研究的免费 3D 建模软件是 Wings 3D。这个软件可以用来创建自定义的 3D 对象,然后可以导入到 JavaFX 中,在您的游戏中使用。最新版本是 64 位、16MB 的下载版本,是 1.5.3,于 2014 年 4 月发布,适用于 Windows 7、Mac OS X 和 Ubuntu Linux。该软件可以在 www.wings3d.com 找到,目前可以免费下载!

对于 UI 设计原型,Evolus 的免费软件包 Pencil 2.0.6 允许您在用 Java、Android 或 HTML5 创建 UI 设计之前,轻松地进行原型设计。该软件位于 http://pencil.evolus.vn ,可用于 Linux、Windows 和 Mac OSs。

接下来,您将看到我是如何在任务栏上组织一些基本的操作系统实用程序和开源软件的。

在任务栏区域组织快速启动图标

对于某些操作系统实用程序,如计算器、文本编辑器(记事本)和文件管理器(资源管理器),我会在任务栏上创建快速启动图标,因为这些实用程序在编程和新媒体内容开发工作流程中经常使用。我还将各种新媒体开发、编程和办公效率应用作为快速启动图标。图 1-20 显示了一打这样的程序,包括你刚刚安装的所有程序,按照你安装的顺序,还有一些其他的程序,比如 OpenOffice 4.1,DAZ Studio Pro 4.6 和 Bryce 7.1 Pro。

A978-1-4842-0415-3_1_Fig20_HTML.jpg

图 1-20。

Make taskbar Quick Launch icons for key system utilities, NetBeans 8.0, and new media production software

有几种方法可以创建这些快速启动图标:您可以将程序从“开始”菜单拖放到任务栏上,或者右键单击桌面上或资源管理器文件管理器中的图标,然后从上下文菜单中选择“将该程序固定到任务栏”。一旦图标出现在任务栏上,你可以简单地通过向左或向右拖动来改变它们的位置。

恭喜您,您已经建立了一个高度优化的新媒体 Java 8 游戏开发工作站,它将允许您创建您或您的客户可以想象的任何新媒体 Java 8 游戏!

摘要

在第一章中,我确保您拥有开发出色的 Java 8 游戏所需的一切,包括最新版本的 Java 8、JavaFX 和 NetBeans 8.0 以及所有最新的开源新媒体软件。

您首先下载并安装了最新的 Java JDK 8 和 NetBeans IDE 8.0 软件。然后,你对大量专业开源新媒体工具做了同样的事情。

在下一章中,我将向您展示如何使用 NetBeans 8.0 创建一个 Java 8 项目。

二、设置 Java 8 IDE:NetBeans 8.0 简介

让我们从第二章的开始,考虑 NetBeans IDE 8.0,因为它是您将用来创建 Java 8 游戏的主要软件。尽管 Java JDK 8 是 Java 8 游戏和 NetBeans 8.0 的基础,但您将从了解 NetBeans 开始您的旅程,因为它是“前端”,是您查看 Java 游戏项目的窗口。

NetBeans 8.0 是 Java JDK 8 的官方集成开发环境,因此,它将是你在本书中使用的。这并不是说您不能使用另一个 IDE,例如 Eclipse 或 IntelliJ,它们分别是 Android 4.x (32 位)和 Android 5.x (64 位)的官方 IDE,但是我更喜欢使用 NetBeans 8.0 来开发我的新媒体应用以及 Java 8、JavaFX 8、HTML5、CSS3(级联样式表 3)和 JavaScript 软件开发标记和编程范例的游戏开发。

这不仅是因为 NetBeans 8.0 集成了 JavaFX Scene Builder,您将在本书的第五章中了解到这一点,还因为它也是一个 HTML5 IDE,我使用 Java 8、JavaFX 8、Android 4.x 或 Android 5.x 以及 HTML5 为我的客户创建我设计的所有东西。我这样做是为了让内容可以在封闭的或专有的操作系统和平台上工作。正如你在第一章中观察到的,我更喜欢开源软件和平台。

首先,您将了解 NetBeans 8.0 的新增功能。该版本 NetBeans 与 Java 8 同时发布,版本号同步并非巧合。您将会发现为什么您希望使用 NetBeans 8.0,而不是较旧的 NetBeans 版本,例如 NetBeans 7.4 或更早的版本。

接下来,您将研究 NetBeans IDE 8.0 的各种属性,这些属性使它成为 Java 8 游戏开发的宝贵工具。您将无法获得本章中所有特性的实际操作经验,但是您将在本书的过程中探索它能为您做的所有很酷的事情(您将需要将一个高级代码库放在适当的位置,以真正锻炼一些特性)。

最后,您将学习如何使用 NetBeans 8.0 创建您的 Java 8 和 JavaFX 项目,以便在阅读本书的过程中创建您将要开发的 Java 8 游戏。

NetBeans 8.0 的主要属性:智能 IDE

假设您已经为新媒体内容和游戏开发准备好了专业级工作站,您需要删除所有过时的 JDK 和 ide,并确保您的系统上安装了最新的 V8 Java 和 NetBeans 软件,并准备就绪。如果你是新手,没有适合游戏的工作站,去沃尔玛或 PriceWatch.com,购买一台经济实惠的多核(使用 4 核、6 核或 8 核)64 位计算机,运行 Windows 8.1(或 9.0,如果有),至少有 4GB、6GB 或 8GB 的 DDR3 (1333 或 1600 内存访问速度)系统内存和 750GB,甚至 1TB 的硬盘驱动器。

NetBeans 8.0 很聪明:将您的代码编辑放入 Hyperdrive

虽然 IDE 确实像一个文字处理器,只是适合于编写代码文本而不是创建业务文档,但是像 NetBeans 这样的编程集成开发环境为您的编程工作过程所做的工作比文字处理器为您的文档创作工作过程所做的工作多得多。

例如,您的文字处理器不会就您为业务编写的内容提出实时建议,而 NetBeans IDE 会在您编写代码时查看您正在编写的内容,并帮助您编写代码语句和结构。

NetBeans 要做的事情之一是为您完成代码行,并对代码语句应用颜色以突出不同类型的构造(类、方法、变量、常量、引用等)(有关更多详细信息,请参见第三章)。NetBeans 还将应用代码缩进的行业标准,以使您的代码更易于阅读(对于您自己和游戏应用开发团队的其他成员来说)。

此外,NetBeans 将提供匹配的代码结构括号、冒号和分号,以便您在创建复杂、深度嵌套或异常密集的编程结构时不会迷路。从 Java 8 game 初学者到 Java 8 game developer,您将创建这样的结构,我将指出您遇到的密集、复杂或深度嵌套的 Java 8 代码。

NetBeans 还可以提供引导代码,例如您将在本章稍后创建的 JavaFX 游戏应用引导代码(请参见“创建您的 Java 8 项目:InvinciBagel”一节),以及代码模板(您可以填写和自定义)、编码提示和技巧以及代码重构工具。随着您的 Java 代码变得越来越复杂,它也成为代码重构的更好候选,这可以使代码更容易理解,更容易升级,并且更高效。NetBeans 还可以自动重构您的代码。

如果你想知道,代码重构是改变现有计算机代码的结构,使其更有效或可伸缩,而不改变其外部行为,即它完成的任务。例如,您可以使用 Java 8,通过实现 Lambda 表达式,获得 Java 6 或 Java 7 代码并使其更高效。

此外,NetBeans 提供了各种类型的弹出帮助器对话框,其中包含方法、常量、资产引用(请参见第三章),甚至是关于如何构造代码语句的建议,例如,何时使用强大的新 Java 8 Lambda 表达式功能来使您的代码更加简化并与多线程兼容可能是合适的。

NetBeans 8.0 是可扩展的:用多种语言进行代码编辑

您的文字处理器不能做的另一件事是允许您向它添加功能,而 NetBeans 可以使用其插件体系结构做到这一点。描述这种类型的体系结构的术语是可扩展的,这意味着如果需要,它可以扩展以包括附加的特性。因此,例如,如果您想扩展 NetBeans 8.0 以允许您使用 Python 编程,您可以这样做。NetBeans 8.0 也可以以这种方式支持较旧的语言,如 COBOL 和 BASIC,尽管目前大多数流行的消费电子设备都使用 Java、XML、JavaScript 和 HTML5,但我真的不知道为什么有人愿意花时间这样做。不过,我在谷歌上搜索了一下,有人在 NetBeans 8.0 中用 Python 和 COBOL 编写代码,所以有现实证明 IDE 确实是可扩展的。

可能是因为它的可扩展性,NetBeans IDE 8.0 支持许多流行的编程语言,包括客户端的 C、C++、Java SE、JavaScript、XML、HTML5 和 CSS,以及服务器端的 PHP、Groovy、Java EE 和 JavaServer Pages (JSP)。客户端软件在终端用户持有或使用的设备上运行(在 iTV 的情况下);服务器端软件在服务器上远程运行,当软件在服务器上运行时,它通过互联网或类似的网络与最终用户对话。客户端软件效率更高,因为它位于运行它的硬件设备的本地,因此更具可伸缩性:随着越来越多的人在任何给定的时间点使用该软件,不需要服务器经历过载。

NetBeans 8.0 是高效的:有组织的项目管理工具

一个好的编程 IDE 需要能够管理可以变得非常大的项目,包括包含在项目文件夹层次结构中的数百个文件夹中的超过一百万行代码,以及数千个文件和新媒体资产。因此,项目管理功能在任何主流 IDE 中都必须非常健壮。NetBeans 8.0 包含大量的项目管理功能,允许您以多种不同的方式查看 Java 8 游戏开发项目及其相应的文件以及它们之间的相互关系。

有四种主要的项目管理视图或“窗格”,可以用来查看项目中不同类型的相互关系。(我称它们为窗格,因为整个 IDE 都在我称之为窗口的地方)。我跳到前面(跳到这一章的末尾,一旦你的 Java 8 游戏项目已经创建好了)并创建了图 2-1 中的截图。这个屏幕截图显示了在这个新项目中打开的四个项目管理窗格,以便您可以准确地看到它们将向您显示什么。

A978-1-4842-0415-3_2_Fig1_HTML.jpg

图 2-1。

Project management panes, at the left of the IDE, include Projects, Files, Services, and Navigator

屏幕左侧的项目窗格显示了构成您的(游戏)项目的 Java 源代码包和库。下一个窗格是文件窗格,它包含硬盘上的项目文件夹和文件层次结构。服务窗格包含数据库、服务器、存储库和构建主机,如果它们在项目中使用的话(这些主要是服务器端技术,以及开发团队使用的技术,所以我不打算详细讨论这些)。

项目窗格应该一直保持打开状态(如图 2-7 到 2-21 所示)。项目窗格为您提供了 Java 8 游戏项目中所有项目源代码和资源(内容)的主要访问点。“文件”窗格不仅显示项目文件夹和文件层次结构,还显示每个文件中的数据和 FXML 标记(JavaFX)或 Java 8 代码层次结构。

导航窗格(底部)显示了 Java 代码结构中存在的关系。在这种情况下,它们是InvinciBagel类、.start()方法和.main()方法(更多信息,参见第三章)。

NetBeans 8.0 是用户界面设计友好的:UI 设计工具

NetBeans 8.0 还为大量平台设计了 GUI 拖放设计工具,包括 Java SE、Java EE、Java ME、JavaFX 和 Java Swing 以及 C、C++、PHP、HTML5 和 CSS3。NetBeans 提供了可视编辑器,可以为您编写应用的 UI 代码,因此您所要做的就是让屏幕上的可视内容看起来像您希望它在游戏应用中的样子。由于游戏使用 JavaFX 新媒体(游戏)引擎,您将在本书的第五章中了解 JavaFX Scene Builder,这是一种基于 FXML 的高级可视化设计编辑器。

JavaFX 拥有 Prism 游戏引擎和 3D(使用 OpenGL ES[OpenGL for Embedded Systems])支持,因此我将重点介绍 JavaFX 场景图和 JavaFX APIs。这里的假设是,你可能想要构建最先进的 Java 8 游戏,利用 JavaFX 引擎(现在是 Java 8 的一部分,还有 Lambda 表达式)将是实现这一目标的方法。开发游戏的最快方法是利用 Java 8 和 JavaFX 环境慷慨提供的高级代码和编程结构,用于创建包含强大新媒体元素的尖端应用(在这里是游戏)。

NetBeans 8.0 对错误不友好:用调试器来消除错误

所有的计算机编程语言都有一个假设,即“bug”或不完全符合您要求的代码对您的编程项目的负面影响,会随着它保持未修复状态的时间越长而越大,因此 bug 必须在它们“出生”时就被消灭。NetBeans bug 查找代码分析工具,集成的 NetBeans 调试器,以及与第三方 FindBugs 项目的集成,正如您现在从经验(Audacity)中了解到的,可以在 SourceForge 网站( http://findbugs.sourceforge.net )(如果您想要独立版本),所有这些都补充了我前面讨论的实时“随键入”代码纠正和效率工具(请参见“NetBeans 8.0 是智能的:将您的代码编辑放入 Hyperdrive”一节)。

您的 Java 代码不会很复杂,直到本书的稍后部分,所以一旦您的知识库稍微高级一点,我将介绍当您需要使用这些工具时它们是如何工作的。

NetBeans 8.0 是一个速度狂:用分析器优化代码

NetBeans 还有一个叫做 Profiler 的东西,它会在 Java 8 代码运行时查看代码,然后告诉您它使用内存和 CPU 周期的效率。这允许您优化代码,使其更有效地使用关键系统资源,这对 Java 8 游戏开发非常重要,因为这将影响在功能不太强大的系统上(例如,在单核和双核 CPU 上)玩游戏的流畅度。

这个分析器是一个动态的软件分析工具,因为它在 Java 代码运行时查看您的代码,而 FindBugs 代码分析工具是一个静态的软件分析工具,因为它只是在编辑器中查看您的代码,而您的代码并没有在系统内存中编译和运行。NetBeans 调试器允许您在代码运行时逐句通过代码,因此可以将该工具视为从静态(编辑)到动态(执行)代码分析模式的混合体。

在创建了 Java 8 (JavaFX)游戏引擎的基础之后(在以下几节中),您将运行 Profiler 来查看它在 NetBeans IDE 8.0 中是如何工作的。我将在前面尽可能多地介绍 NetBeans 的关键特性,以便您能够熟悉这个软件。

创建您的 Java 8 游戏项目:InvinciBagel

让我们言归正传,为你的游戏创造基础。我将演示如何创建一个原创游戏,以便您可以看到开发一个尚不存在的游戏所涉及的过程,这与大多数游戏编程书籍相反,它们复制了市场上已经存在的游戏。我得到了我的客户 Ira Harrison-Rubin 的许可,他是 BagelToons 系列的漫画家/作家/幽默作家,让读者在这本书的过程中看到他的 InvinciBagel 卡通游戏的创作过程。

单击任务栏上的快速启动图标(或双击桌面上的图标)启动 NetBeans 8.0,您将看到 NetBeans 启动屏幕,如图 2-2 所示。此屏幕包含一个进度条(红色),将告诉您如何配置 NetBeans IDE 以供使用。这包括将 IDE 的各种组件加载到您的计算机系统内存中,以便在开发过程中可以流畅地实时使用它们。

A978-1-4842-0415-3_2_Fig2_HTML.jpg

图 2-2。

Launch NetBeans 8.0, using the Quick Launch icon

将 NetBeans IDE 8.0 加载到系统内存后,屏幕上将显示 NetBeans 8.0 起始页,如图 2-3 所示。单击“起始页”选项卡右侧的“x”关闭此页面。

这将显示我称之为处女 IDE,没有项目活动。现在就享受吧,因为你很快就会在这个 IDE 中填满你的项目组件的窗格(你可以在图 2-4 中看到这个空 IDE 的一部分,它包含菜单和快捷图标,除此之外没什么)。

如果您想知道,每次启动 NetBeans IDE 时都会显示起始页,如果您想稍后打开起始页选项卡,也许是为了浏览媒体库部分(演示)和教程,您可以这样做!要随时打开起始页,可以使用 NetBeans IDE 8.0 的“帮助”菜单和“起始页”子菜单。为了将来参考,我通常这样标注菜单顺序:帮助➤开始菜单。

在 NetBeans IDE 8.0 中,您要做的第一件事就是创建一个新的 InvinciBagel 游戏项目!为此,您将使用 NetBeans 8.0 新项目系列对话框。这是我之前提到的那些有用的 Java 编程特性之一(参见“NetBeans IDE 8.0 是智能的:将您的编辑放入 Hyperdrive”一节),它使用正确的 JavaFX 库、.main().start()方法以及导入语句创建一个引导项目(有关更多详细信息,请参见第三章)。

A978-1-4842-0415-3_2_Fig3_HTML.jpg

图 2-3。

Close the Start Page tab, at the top left of the screen, by clicking the “x” at the right of the tab to reveal NetBeans IDE 8.0

点击 DE 左上角的文件菜单,如图 2-4 (左图),然后选择新建项目(第一个菜单项)。注意,在这个选项的右边,有一个键盘快捷键(Ctrl+Shift+N),以防你想记住它。

A978-1-4842-0415-3_2_Fig4_HTML.jpg

图 2-4。

Showing virgin NetBeans 8.0 IDE (left) and a JavaFX New Project dialog (right)

如果您想要使用此键盘快捷方式来调出新的项目系列对话框,请按住键盘上的 CTRL 和 Shift 键(同时按住),并在按住它们的同时按下 N 键。这将做同样的事情使用文件➤新项目菜单序列。

系列中的第一个是选择项目对话框,如图 2-4 (右图)所示。因为您将在游戏中使用强大的 JavaFX 新媒体引擎,所以从“类别”窗格的编程语言类别列表中选择 JavaFX,并且因为游戏是一种应用,所以从“项目”窗格中选择 JavaFX 应用。

请记住,Oracle 使 JavaFX 成为 Java 7 和 Java 8 的一部分,因此 JavaFX 游戏也是 Java 游戏,而在 Java 7 之前(在 Java 6 中),JavaFX 是它自己独立的编程语言!JavaFX 引擎必须重新编码为 Java (7 和 8) API(一组库),才能成为 Java 编程语言的无缝部分。JavaFX API 取代了 AWT (Abstract Windowing Toolkit)和 Swing,虽然这些旧的 UI 设计库仍然可以在 Java 项目中使用,但它们通常只由遗留(旧的)Java 代码使用,以便这些项目可以在 Java 7 和 8 中编译和运行。在本章的稍后部分,您将编译并运行您在这里创建的新项目。

请注意,在其他窗格下面有一个描述窗格,它会告诉您您的选择会给你带来什么。在这种情况下,这将是一个启用了 JavaFX 特性的新 Java 应用;这里,“enabled”表示 JavaFX API 库将包含(并启动)在 Java 应用项目的类和方法中,您将很快在代码中看到这一点(有关代码含义的更多信息,请参见第三章)。

点击下一个按钮,进入系列中的下一个对话框,即查找特征对话框,如图 2-5 所示。该对话框在“激活 JavaFX 2”时显示进度条,这相当于在项目代码基础结构中安装 JavaFX API 库。你会发现有时候 JavaFX 8 还是被称为 JavaFX 2 (2.3 是人们开始使用 JavaFX 8 这个名字之前 JavaFX 的最新版本,可能是为了和 Java 8 同步)。我也看到了关于 JavaFX 3 的讨论,它现在被称为 JavaFX 8,因为 JavaFX 现在是 Java 8 的一部分,所以在本书中我将简称它为 JavaFX。

A978-1-4842-0415-3_2_Fig5_HTML.jpg

图 2-5。

Step 2: Finding Feature dialog, showing the progress bar for the process of activating JavaFX

一旦 Finding Feature 对话框为你的游戏项目激活了 JavaFX,你将得到 Name and Location 对话框,如图 2-6 所示。将项目命名为 InvinciBagel,保留默认的项目位置、项目文件夹、JavaFX 平台,并按照 NetBeans 8.0 的配置方式创建应用类设置。

A978-1-4842-0415-3_2_Fig6_HTML.jpg

图 2-6。

Name the project InvinciBagel, and leave the other settings as they are

让 NetBeans 8.0 为您做事通常是个好主意。如您所见,NetBeans 在您的用户文件夹和我的文档子文件夹中为项目位置数据字段创建了逻辑C:\Users\user\My Documents\NetBeansProjects文件夹。

对于您的项目文件夹数据字段,NetBeans 同样会在 NetBeansProjects 文件夹下创建一个名为 InvinciBagel 的子文件夹,就像您自己所做的一样。

对于 JavaFX 平台下拉菜单,NetBeans 8.0 默认为最新的 JDK 8,也称为 JDK 1.8,并具有最新的 JavaFX 8(应该是 JavaFX 3.0)。

因为您不会创建共享库的多个应用,所以请不要选中“使用专用文件夹存储库”复选框。最后,选择 Create Application Class,它将被命名为 invincibagel,并将位于 InvinciBagel 包中;因此,完整的路径和类名如下:invincibagel.InvinciBagel(遵循packagename.ClassName Java 命名范例和风格)。

(您将在第三章中了解更多关于包、类和方法的信息,但您最终将在这里接触到其中的一些信息,因为 NetBeans 8.0 将编写一些引导 Java 代码,这些代码将为您的 InvinciBagel Java 8 游戏提供基础。我将查看图 2-7 中所示的 Java 代码的一些基本组件,但在本章中我将主要关注 NetBeans IDE 8.0,并在第三章中关注 Java 8 编程语言。)

如图所示,NetBeans 编写了 package 语句、七个 JavaFX import 语句、类声明以及.start().main()方法。NetBeans 8.0 将 Java 关键编程语句单词涂成蓝色,注释涂成灰色。数据值为橙色,输入/输出为绿色。

A978-1-4842-0415-3_2_Fig7_HTML.jpg

图 2-7。

Examine the bootstrap JavaFX code that NetBeans created for you, based on the New JavaFX Application dialog

在运行此引导代码之前,为了确保 NetBeans 8.0 为您编写的代码能够正常工作,您需要将其编译为可执行格式,并在系统内存中运行。

在 NetBeans 8.0 中编译您的 Java 8 游戏项目

在向您展示如何在运行(测试)Java 8 代码之前编译它的过程中,我在这里展示了“漫长的道路”,这样您就可以了解编译/运行 Java 8 代码测试过程的每一步。点击运行菜单,然后选择编译文件(第十一个菜单项)来编译你的 Java 代码,或者使用 F9 键盘快捷键,如选择右侧所示,如图 2-8 所示。现在您的项目已经准备好运行了!

A978-1-4842-0415-3_2_Fig8_HTML.jpg

图 2-8。

Click the Run menu, at the top of the IDE, and then select Compile File, or press the F9 function key

图 2-9 展示了编译进度条,在编译过程中它会出现在 IDE 的底部。

A978-1-4842-0415-3_2_Fig9_HTML.jpg

图 2-9。

The Compile progress bar is shown at the bottom of the screen, along with expand and collapse icon functionality

这里还需要注意的是,当您使用“文件”“保存”菜单序列(或 CTRL-S 键盘快捷键)时,NetBeans 将编译项目代码,因此,如果您在创建引导代码后立即使用 NetBeans IDE 的“保存”功能,您就不必执行我刚才向您展示的编译过程,因为该过程是在每次保存游戏项目时“自动地”(而不是手动地)完成的。

图中还显示,在编译进度条的正上方,突出显示了一段代码,这段代码在图 2-7 中是可见的,但我已经使用代码编辑器窗格左侧的减号图标将其折叠。您可以在代码编辑器窗格的中间看到三个未折叠的减号图标(在 InvinciBagel 类下),在代码编辑器窗格的顶部看到三个折叠的图标,分别代表两个注释和 import 语句代码块。减号图标会变成加号图标,以便可以展开折叠的代码视图。现在,您已经了解了如何在 NetBeans 中编译项目,以及如何折叠和展开项目代码的逻辑块(组件)视图,现在是运行代码的时候了。

在 NetBeans 8.0 中运行 Java 8 游戏项目

现在您已经创建并编译了您的 bootstrap Java 8/JavaFX 游戏项目,是时候运行或执行 bootstrap 代码,看看它做了什么。您可以通过使用运行➤运行项目菜单序列来完成此操作(参见图 2-8 ,或者您可以使用 IDE 顶部的快捷图标(类似于视频传输播放按钮),如图 2-10 所示。

A978-1-4842-0415-3_2_Fig10_HTML.jpg

图 2-10。

Click the Run Project shortcut icon (green play button), at the top middle of the IDE (tool tip pop-up shown)

一旦你运行了编译好的 Java 代码,一个窗口将会打开,你的软件在其中运行,在屏幕的右边,如图 2-11 所示。目前,该程序使用流行的“Hello World!”示例应用。

A978-1-4842-0415-3_2_Fig11_HTML.jpg

图 2-11。

Drag the separator bar upward to reveal the Compile Output area of the IDE (running the application seen at right)

单击代码编辑器窗格和底部输出选项卡之间的分隔线,按住鼠标按钮,向上拖动分隔线,显示输出选项卡的内容,如图 2-11 所示。

“输出”选项卡将包含 NetBeans 中不同类型的输出,例如 Ant 的编译操作输出、运行操作输出(如图所示)、探查器操作输出(您将在下一节中探讨),甚至是应用本身的输出。

您可能已经注意到,在图 2-10 中,这个 bootstrap Java 8/JavaFX 应用的代码在第 25 行包含了一个(System.out.println("Hello World!"); Java 语句,所以如果您想将当前正在运行的应用打印到输出窗格(有时在编程界称为输出控制台),请单击“Hello World!”中的“Hello World”按钮应用(运行在 IDE 之上)。

一旦你点击这个按钮,“你好,世界!”将出现在 Output 选项卡中,在红色文本下面,表示它正在执行 InvinciBagel.jar 文件。一个.jar (Java 归档)文件是 Java 应用的可分发格式。编译过程的一部分是创建这个文件,所以如果您的编译版本工作正常,您就可以准备好分发.jar文件,如果您的应用设计和编程已经完成的话!

一个.jar文件并不包含您实际的 Java 8 代码,而是一个压缩的、加密的“Java 字节流”版本的应用,JRE 可以执行和运行它(就像 NetBeans 8.0 现在正在做的那样)。附加在InvinciBagel.jar文件前面的路径告诉您编译后的.jar文件驻留在哪里,以及 NetBeans 当前从哪里访问它来运行它。在我的系统上,这个位置是 C:\ Users \ user \ Documents \ netbeans projects \ InvinciBagel \ dist \ run 1331700299 \ InvinciBagel . jar。

让我们看看其他一些输出选项卡文本,看看 NetBeans 做了什么来达到可以为这个项目运行.jar文件的程度。首先,编译器删除并重建 build-jar-properties 文件,该文件位于\NetBeansProjects\InvinciBagel\build文件夹中,基于游戏应用的独特属性。

接下来,Ant 创建一个\NetBeansProjects\InvinciBagel\dist\分发文件夹来保存项目.jar文件,然后检测 JavaFX 的使用情况,启动ant-javafx.jar来向 Ant 构建引擎添加 JavaFX 功能,这将创建.jar文件。最后,您将看到一条警告,要求将manifest.custom.codebase属性从星号值(这意味着“一切”)更改为特定值。我可能会在本书的后面进入应用开发的清单和权限领域,在您更深入一点之后。然后启动 JavaFX,并构建.jar文件。

Ant 是创建您的.jar文件的构建引擎或构建工具。其他构建引擎,如 Maven 和 Gradle,也可以在 NetBeans 中使用,因为正如您现在所知道的,NetBeans 是可扩展的!

Ant 也在 Eclipse IDE 中使用,并且是一个已经存在很长时间的 Apache 开源项目。要了解更多关于 Ant 构建系统及其功能的信息,请访问 Ant 网站( http://ant.apache.org )

接下来,您将探索 NetBeans 8.0 中的分析功能,它可以在运行时分析您的代码,并让您知道您的 Java 8 代码运行的效率如何。这对于游戏来说很重要,尤其是街机游戏或者任何在用户屏幕上实时移动精灵的游戏。你将在本书第六章中学习游戏概念和设计。

在 NetBeans 8.0 中分析您的 Java 8 游戏项目

要启动 Java 8 代码分析实用程序,使用 IDE 顶部的 Profile 菜单,选择 Profile Project (InvinciBagel)(第一个菜单项),如图 2-12 所示,或者使用 Profile Project 快捷图标,该图标在图 2-13 中给出的折叠屏幕视图中可见(您可以通过代码编辑器窗格中的 Java 代码行编号看出我折叠了该屏幕截图,该窗格仅包含第 1 行和第 38 行,即该范围中的第一个和最后一个数字;我用 Photoshop 去掉了第 2-37 行。

A978-1-4842-0415-3_2_Fig12_HTML.jpg

图 2-12。

Click the NetBeans IDE 8.0 Profile menu, and select the Profile Project (InvinciBagel) menu option

正如您在屏幕顶部的 Profile 菜单和 Profile Project 图标工具提示中所看到的,Profile Project 工具的键盘快捷键是 ALT+F2(按住键盘上的 ALT 键,同时按下键盘左上角的 F2 功能键)。

A978-1-4842-0415-3_2_Fig13_HTML.jpg

图 2-13。

The shortcut icon for the Profile Project utility, with tool tip (screen collapsed)

剖析 Java 8 游戏应用的 CPU 使用情况

使用 Profile Project 菜单项或快捷图标会打开 Profile InvinciBagel(你的游戏项目名称)对话框,如图 2-14 所示。让我们单击对话框左侧的中央 CPU 按钮,这将使对话框处于分析性能(选择特征)模式。稍后您将看到内存使用情况分析(参见“分析 Java 8 游戏应用内存使用情况”一节)。Monitor (button)选项支持实时线程监控,可以在编写 Java 代码时使用。

A978-1-4842-0415-3_2_Fig14_HTML.jpg

图 2-14。

Set the filter, using the drop-down menu in the Profile Project dialog, and select the Advanced (instrumented) output setting

在此对话框中,您可以选择快速配置文件或高级配置文件,它们具有可视化显示性能的图形工具。正如您所看到的,这是从 Instrumentation Filter 下拉菜单中选择的选项以及 Profile only project classes 选项。保持选中“使用已定义的分析点”,以使 NetBeans 8.0 尽可能完成最多的分析工作。还要注意对话框底部的顶置量规(指示器),指示 50%的值。

第一次运行 NetBeans 分析工具时,它需要校准您的工作站,因为每个工作站都有不同的特征,例如内存量和 CPU 内核或处理器的数量。

图 2-15 显示校准信息对话框,该对话框提示在校准过程中只有 NetBeans 在您的工作站上运行,并告诉您将来如何再次校准(如果您更改系统硬件配置),使用配置文件➤高级命令➤管理校准数据菜单序列。

还有一个警告,说你应该禁用动态 CPU 频率切换(这通常被称为超频),这是最近常见的功能。

因为我想测试较慢的 CPU 速度,所以我没有这么做,因为这涉及到进入工作站主板上的系统 BIOS(基本输入/输出系统),不是初学者可以随便玩的东西。

最终,测试游戏应用的最彻底的方法是在各种不同的操作系统和硬件配置上进行,但我想向您展示这种评测功能,因为这是获得应用性能良好基线的一种很好的方法,您可以在改进代码时对其进行改进(然后反复运行评测器,将结果与原始基线测量结果进行比较)。

A978-1-4842-0415-3_2_Fig15_HTML.jpg

图 2-15。

The first time you profile, a calibration is performed

单击“确定”按钮后,NetBeans IDE 8.0 将根据您的系统硬件特征来校准其分析工具,这在快速的现代多核工作站上应该不会花费很长时间。

如果您运行的是 Windows 操作系统(如这里所见,在 64 位 Windows 7 版本中),您可能会得到一个 Windows 防火墙已经阻止了此程序的某些功能的 Windows 安全警告对话框。您希望拥有 NetBeans 8.0 的所有功能,因此接下来让我们看看如何在 Windows 中允许访问 Java SE 8 平台。

通过 Windows 防火墙解除对 Java 8 平台二进制文件的封锁

如果出现可怕的阻止功能网络对话框,如图 2-16 所示,选择允许 Java Platform SE 二进制文件在专用网络(如我的家庭或工作网络)上通信复选框,然后单击允许访问按钮,这将允许 Java 8 platform SE 二进制文件通过 Windows 防火墙进行通信。

A978-1-4842-0415-3_2_Fig16_HTML.jpg

图 2-16。

Allow Java features to be used by clicking Allow access

在您允许访问Java 8 platform SE binary之后,NetBeans 8.0 性能分析工具可以(并将)运行,并将生成基本的性能分析遥测结果。在接下来的几节中,您将更深入地了解这些,这几节讨论了如何分析性能分析结果,以及它们揭示了应用如何使用内存和 CPU 资源。

分析 NetBeans IDE 8.0 游戏项目 CPU 分析工具结果

NetBeans Profiler 主要查看内存使用情况和用于执行代码的 CPU 时间。使用的内存越少,CPU 时间越快(相当于执行代码所需的 CPU 处理周期越少),应用的优化就越好。分析器还关注与代码(软件)相关的东西,比如方法调用和线程状态,这些你将在本书的课程中学到。

运行 NetBeans 8.0 Profiler 后,您会看到 IDE 左侧的“项目”、“文件”和“服务”选项卡中添加了一个 Profiler 选项卡,如图 2-17 所示。您已经在本章的前面检查了这三个选项卡(请参见“NetBeans 8.0 是高效的:有组织的项目管理工具”一节),所以现在让我们来研究一下 Profiler 选项卡。

“Profiler”选项卡的顶部是“Controls”部分,带有“Stop(termin ate)Profiled Application”、“Reset Collected Profiling Results Buffer”、“Garbage Collection”、“Modify Profiling Session”和“VM Telemetry Overview”图标。

下面是 Status 部分,显示您选择的分析类型(在本例中是 CPU)、配置(分析性能)和状态(正在运行)。

A978-1-4842-0415-3_2_Fig17_HTML.jpg

图 2-17。

Profile’s Basic Telemetry section, at the left of the IDE, under the Profile tab, shows methods, threads, and total and used memory

“性能分析结果”部分包含打开“代码编辑器”部分中关于性能分析数据结果(报告)的选项卡的图标,而“视图”部分对虚拟内存(VM)遥测、线程和线程锁争用做同样的事情。在下一节中,当您分析内存使用情况时(您当前正在分析 CPU 使用情况),您将会看到其中的一些。

在代码分析会话期间,您可以在“保存的快照”部分保存不同时间点的快照。基本遥测部分显示关于分析会话的统计信息,包括方法数量、过滤器设置、运行的线程和内存使用情况。

单击性能分析结果部分的实时结果图标,打开一个实时性能分析结果选项卡,如图 2-18 所示,在顶部标有 CPU 时间(2:12:09 pm)。

正如您所看到的,您可以打开您的代码层次结构,包括.main()方法、.start()方法和.handle()方法,并查看它们所用总 CPU 时间的百分比以及实际使用的 CPU 时间(以毫秒为单位),这是 Java 8 和 JavaFX 的 Java 编程中使用的时间值,甚至是 HTML5、JavaScript 和 Android 应用开发中使用的时间值。

最后,正如您在图底部的输出窗格中看到的,还有文本输出,就像这个输出窗格用于显示编译、运行和执行的代码一样,也显示了分析器正在做什么。在“你好,世界!”通过单击应用的“Hello World”按钮,可以看到 Profiler 代理正在初始化、缓存类等等。在 NetBeans 的这个区域中有大量的选项卡和选项,我无法在这一基本的 NetBeans 概述章节中一一介绍,所以您可以随意使用您在屏幕上看到的内容!

A978-1-4842-0415-3_2_Fig18_HTML.jpg

图 2-18。

NetBeans Profiler output, shown in the cpu tab, at the top right, and the Output tab, at the bottom right

剖析 Java 8 游戏应用的内存使用情况

接下来让我们看看内存分析。点击 Profile 项目图标,打开分析内存对话框,如图 2-19 所示。正如您所看到的,如果您为分配选择记录堆栈跟踪,分析器将使用更多的系统开销。

A978-1-4842-0415-3_2_Fig19_HTML.jpg

图 2-19。

Select the Memory section of the Profile InvinciBagel dialog and select Record stack trace for allocation

一旦内存分析器运行,使用如图 2-20 (顶部)所示的窗口➤分析➤虚拟机遥测概述菜单序列,打开虚拟机遥测概述选项卡(底部)。该选项卡显示分配的内存和使用的内存。您可以将鼠标悬停在可视栏上,以获得任何时间点的准确读数。用编程术语来说,将鼠标悬停在某处将会在代码中使用“鼠标悬停”来访问

A978-1-4842-0415-3_2_Fig20_HTML.jpg

图 2-20。

Use the Window ➤ Profiling menu sequence to access the visual profiling tabs

查看窗口➤分析菜单序列中的一些其他可视报告选项卡。图 2-21 中显示的是线程选项卡,显示所有 11 个线程(参见屏幕左侧的基本遥测窗格),包括每个线程正在做什么(线程正在运行什么代码),以及虚拟机遥测选项卡,显示虚拟内存随时间的使用情况。

A978-1-4842-0415-3_2_Fig21_HTML.jpg

图 2-21。

Use the Window ➤ Profiling menu sequence to access the Threads and VM Telemetry tabs

随着时间的推移,您将学会如何使用 NetBeans Profiler,首先是通过实验,然后随着您对 Profiler 功能的熟悉,在您自己的项目变得越来越复杂时使用它,并观察您的代码库在线程、CPU 使用以及内存分配和使用方面的情况。NetBeans Profiler 是一个强大而有用的工具,它将作为 Java 8 游戏开发的代码开发基础。我将它包含在本章中是为了给你一个坚实的概述,因为这个知识库将帮助你利用这个软件,最大限度地发挥它的潜力和能力。

显然,这是一个高级的 IDE 和软件开发工具,不可能在一个简短的章节(也许是一本书;但是,这不是 NetBeans 8.0 游戏开发的标题),所以您将在本书的几乎所有章节中更多地了解 NetBeans 8.0 可以为您做什么,因为 NetBeans 8.0 和 Java 8(以及 JavaFX 8)不可避免地交织在一起。

摘要

在第二章中,您学习了 NetBeans IDE 8.0,它将作为 Java 8 游戏开发工作流程的基础和主要工具。这个 IDE 是编写、编译、运行、测试和调试 Java 8(和 JavaFX 8)代码的地方,也是使用 NetBeansProject 文件夹及其子文件夹存储和引用新媒体(图像、音频、视频、3D、字体、形状等)资源的地方。

您首先了解了 NetBeans 8.0 及其高级特性,这些特性使其成为 Java 8 的官方 IDE,并帮助程序员第一次快速、高效地开发代码(即,使代码没有错误)。在这个概述之后,您创建了您的 Java 8 游戏项目,使用我为一个主要客户开发的真实世界游戏项目作为模型。

您浏览了新的 Java 应用系列对话框,并为您的游戏创建了 JavaFX 框架,这将允许您使用新的媒体资源,如图像、音频、视频和 3D。然后,您探索了如何使用 NetBeans 8.0 编译和运行应用。您还学习了 Output 选项卡,以及如何将它用于编译器输出、运行时输出和概要分析输出,这是您接下来要考虑的。

您研究了 NetBeans 8.0 中的 CPU 分析和内存分析;学习了如何设置和启动分析项目工具;并研究了 NetBeans Profiler 可以根据您的 Java 8 游戏项目为您创建的一些输出、统计和可视化报告。

在下一章中,我将介绍 Java 8 编程语言的概述,以确保您了解 Java 8 的工作原理;一个 Java 入门章节,如果你愿意的话。

三、Java 8 入门:Java 8 概念和原则的介绍

让我们以您在前一章中获得的关于 NetBeans IDE 8.0 的知识为基础,探索 Java 8 编程语言背后的基本概念和原理。Java JDK 8 将是您的 Java 8 游戏以及 NetBeans IDE 8.0 的基础,因此花时间学习本章是很重要的,这是一本 Java 8“初级读本”,它为您提供了这种国际流行的计算机(和设备)编程语言的概述。

当然,在阅读本书的过程中,您会学到更多高级的概念,比如 Lambda 表达式,以及其他 Java 8 组件,比如最近的 JavaFX 多媒体引擎,所以请注意,本章将涵盖最基础的 Java 编程语言概念、技术和原则,涵盖目前在计算机、ITV 和手持设备上广泛使用的三个主要 Java SE 版本。

这些被数十亿用户使用的 Java 版本包括 Java 6,用于 32 位 Android 4.x 操作系统和应用;Java 7,用于 64 位 Android 5.x 操作系统和应用;以及 Java 8,它在许多流行的操作系统中使用,如微软 Windows、苹果 OS X、Oracle Solaris 和大量流行的 Linux“发行版”或发行版(定制的 Linux 操作系统版本,如 SUSE、Ubuntu、Mint、Mandrake、Fedora 和 Debian)。

您将从最简单的概念(Java 的最高级别)开始,并逐步学习更难的概念(Java 编程结构的核心)。您将从学习 Java 语法或行话开始,包括什么是 Java 关键字,Java 如何界定其编程结构,以及如何注释您的代码。首先检查这一点将使您在阅读 Java 代码方面有一个良好的开端,因为能够从关于代码的注释(通常由 Java 代码的作者使用注释编写)中辨别 Java 代码是很重要的。

然后,您将考虑 API 的顶级概念,包是什么,以及如何导入和使用 Java 包提供的预先存在的代码。这些 Java 包是 Java 8 API 的一部分,值得注意的是,您可以创建自己的定制 Java 包,包含您的游戏或应用。

之后,您将考虑这些 Java 包中的构造,它们被称为 Java 类。Java 类是 Java 编程的基础,可用于构建您的应用(在这种情况下,您的 Java 8 游戏)。您将了解这些类包含的方法、变量和常量,以及什么是超类和子类,什么是嵌套类和内部类,以及如何利用它们。

最后,您将发现什么是 Java 对象,并了解它们如何构成面向对象编程(OOP)的基础。您还将了解什么是构造函数方法,以及它如何创建 Java 对象,方法是使用一种特殊的方法,称为构造函数方法,它与包含它的类同名。让我们开始吧——我们有很多内容要谈!

Java 的语法:注释和代码分隔符

关于语法,也就是 Java 如何用它的编程语言写东西,有几件事你需要马上考虑。这些主要的语法规则允许 Java 编译器理解你是如何构建你的 Java 代码的。Java 编译是 Java 编程过程的一部分,在这个过程中,JDK 编译器(程序)将 Java 代码转换成由 Java 运行时引擎(JRE)执行或运行的字节码。这个 JRE,在本例中是 JRE 8,安装在最终用户的计算机系统上。Java 编译器需要知道您的 Java 代码块在哪里开始和结束,您的各个 Java 编程语句或指令在这些 Java 代码块中在哪里开始和结束,您的代码的哪些部分是 Java 编程逻辑,哪些部分是对您自己的注释,或者对您的游戏项目编程团队的其他成员的注释(注释)。

让我们从注释开始,因为这个主题是最容易掌握的,而且你已经在你的 InvinciBagel 游戏 bootstrap Java 代码中看到了注释,在第二章中。有两种方法可以将注释添加到 Java 代码中:单行注释,也称为“行内”注释,放置在一行 Java 代码逻辑之后;多行注释,或“块”注释,放置在一行 Java 代码或一个 Java 代码块(Java 代码结构)之前(或之后)。

单行注释通常用于添加关于那一行 Java 逻辑(我喜欢称之为 Java 编程“语句”)正在做什么的注释,也就是说,那一行 Java 代码在整个代码结构中要完成什么。Java 中的单行注释以双正斜杠序列开始。例如,如果你想在你在第二章中创建的 InvinciBagel 引导代码中注释一个 import 语句,你可以在代码行后添加两个正斜杠。这是你的 Java 代码被注释后的样子(参见图 3-1 ,右下方):

import javafx.stage.Stage // This line of code imports the Stage class from JavaFX.stage package

接下来,让我们看看多行注释,它们显示在图 3-1 的顶部,在包语句的上方(你将在下一节中学习)。如您所见,这些 Java 块注释的处理方式不同,使用星号旁边的单个正斜杠开始注释,以及与此相反,在单个正斜杠旁边的星号结束多行注释。

A978-1-4842-0415-3_3_Fig1_HTML.jpg

图 3-1。

Multiline comments (first five lines of code, at the top) and single-line comments (last three lines of code, at the bottom)

正如您在 NetBeans 8.0 的“InvinciBagel.java 代码编辑”选项卡中所看到的,正如我排列单行注释以使其看起来漂亮(酷)且有条理一样,块注释中的 Java 约定也是排列星号,一个作为开始注释分隔符,一个作为结束注释分隔符。

Definition

Java 编程中的“约定”是大多数(如果不是全部)Java 程序员实现 Java 构造的方式。在这种情况下,这就是 Java 代码块注释的样式。

还有第三种类型的注释,称为 Javadoc 注释,您不会在 Java 8 游戏开发中使用它,因为您的代码旨在用于创建游戏,而不是向公众发布。如果您打算编写一个 Java 游戏引擎,供其他人用来创建游戏,这时您应该使用 Javadoc 注释向您的 Java 8 游戏引擎添加文档。JDK 中的 javadoc.exe 工具可以使用 Javadoc 注释,根据您放入 Javadoc 注释中的文本内容,为包含 Javadoc 注释的 Java 类生成 HTML 文档。

Javadoc 注释类似于多行注释,但它使用两个星号来创建开始 Javadoc 注释分隔符,如下所示:

/** This is an example of a Java Documentation (Javadoc) type of Java code comment.

This is a type of comment which will automatically generate Java documentation!

*/

如果您想在 Java 语句或编程结构的中间插入一个注释(作为一名专业的 Java 程序员,您绝不能这样做),请使用多行注释格式,如下所示:

i``mport``/* This line of code imports the Stage class */

这不会产生任何错误,但是会让代码的读者感到困惑,所以不要用这种方式注释代码。但是,在 NetBeans 8.0 中,以下使用双正斜杠注释此代码的单行注释方式会生成编译器错误:

i``mport``// This line of code imports the Stage class

这里,编译器将只看到单词 import,因为单行注释位于行尾,而多行注释则使用块注释分隔符序列(星号和正斜杠)结束。因此,编译器将为第二个未正确注释的代码抛出一个错误,本质上是问“导入什么?”

正如 Java 编程语言使用双正斜杠和斜杠-星号对来分隔 Java 代码中的注释一样,其他一些关键字符也用于分隔 Java 编程语句和整个 Java 编程逻辑块(我经常称之为 Java 代码结构)。

分号在 Java(所有版本)中用来分隔或分开 Java 编程语句,比如图 3-1 中看到的 package 和 import 语句。Java 编译器寻找一个 Java 关键字,该关键字开始一个 Java 语句,然后将该关键字之后的所有内容作为 Java 代码语句的一部分,直到分号(这是告诉 Java 编译器“我已经完成了这个 Java 语句的编码工作”的方式)。例如,在你的 Java 应用的顶部声明 Java 包,你使用 Java 包关键字,你的包的名字,然后一个分号,如下所示(参见图 3-1 ):

package invincibagel;

Import 语句也使用分号分隔,如图所示。import 语句提供 import 关键字、要导入的包和类,最后是分号分隔符,如下面的 Java 编程语句所示:

import javafx.application.Application;

接下来,您应该看看花括号({。。。})分隔符,和多行注释分隔符一样,它有一个左花括号,用来分隔(也就是说,它显示了编译器)Java 语句集合的开始,还有一个右花括号,用来分隔 Java 编程语句集合的结束。花括号允许您在许多 Java 构造中使用多个 Java 编程语句,包括在 Java 类、方法、循环、条件语句、lambda 表达式和接口中,所有这些都将在本书的课程中学习。

如图 3-2 所示,用花括号分隔的 Java 代码块可以相互嵌套(包含),允许更复杂的 Java 代码结构。使用花括号的第一个(最外面的)代码块是 InvinciBagel 类,其他构造嵌套如下:start()方法。setOnAction()方法和 handle()方法。随着本章的深入,您将会看到所有这些代码都做了什么。我想让你现在想象一下(借助图 3.2 中的红色方块)花括号是如何允许你的方法(和类)定义它们自己的代码块(结构)的,每个代码块都是一个更大的 Java 结构的一部分,最大的 Java 结构是 InvinciBagel.java 类本身。请注意,每个左花括号都有一个匹配的右花括号。还要注意代码的缩进,最里面的 Java 代码结构向右缩进得最远。每个代码块额外缩进四个字符或空格。如您所见,该类没有缩进(0),start()方法在。setOnAction()方法在中是 8 个空格,handle()方法在中是 12 个空格。NetBeans 8.0 将为您缩进每一个 Java 代码结构!另请注意,NetBeans 8.0 在 ide 中绘制了非常精细的(灰色)缩进参考线,这样,如果您愿意,就可以直观地排列代码结构。

A978-1-4842-0415-3_3_Fig2_HTML.jpg

图 3-2。

Nested Java code blocks for the InvinciBagel class, start method, setOnAction method, and handle method

每个红色方块中的 Java 代码以花括号开始,以花括号结束。现在,您已经熟悉了各种 Java 8 代码注释方法,以及 Java 8 游戏编程语句需要如何分隔,无论是单独的还是作为 Java 代码块,接下来您将学习各种 Java 代码结构本身——它们是如何使用的,它们可以为您的应用和游戏做些什么,以及使用哪些重要的 Java 关键字来实现它们。

Java APIs:使用包按功能组织

在一个编程平台的最高层,比如谷歌的 32 位 Android 4,用的是 Java SE 6;64 位 Android 5,使用 Java SE 7;当前的 Oracle Java SE 平台(最近发布为 Java SE 8)有一个包含类、接口、方法和常量的包集合,它们共同构成了 API。应用(在本例中是游戏)开发人员可以使用这些 Java 代码集合(在本例中是 Java 8 API)来创建跨许多操作系统、平台和消费电子设备的专业级软件,这些设备包括计算机、笔记本电脑、上网本、笔记本、平板电脑、iTV 电视机、游戏控制台、智能手表和智能手机。

要安装给定版本的 API 级别,您需要安装 SDK(软件开发工具包)。Java SDK 有一个特殊的名字,JDK (Java 开发工具包)。熟悉 Android(Linux 之上的 Java SE)操作系统开发的人都知道,每次添加一些新特性时,都会发布不同的 API 级别。这是因为这些新的硬件功能需要支持,而不是因为谷歌的高管们觉得每隔几个月就要发布一个新的 SDK。Android 有 24 个不同的 API 级别,而 Java SE 只有 8 个,并且目前只有 3 个 Java 的 API 级别(Java 6、Java 7、Java 8)在使用。

Java SE 6 配合 Eclipse ADT(Android Developer Tools)IDE 使用,为 32 位 Android(1.5 到 4.5 版本)开发;Java SE 7 配合 IntelliJ IDEA 使用,为 64 位 Android(5.0 及以上版本)开发;Java 8 与 NetBeans IDE 配合使用,用于在 Windows、Mac OS X、Linux 和 Oracle Solaris 操作系统上开发 JavaFX 和 Java 8。我有三个不同的工作站,分别针对这些 Java API 平台和 IDE 软件包进行了优化,这样我就可以同时为 Android 4 (Java 6)、Android 5 (Java 7)和 JavaFX (Java 8)开发应用。幸运的是,你只需要花几百块钱就可以在 PriceWatch.com 上获得一台强大的 Windows 8.1 六核或八核 64 位 AMD 工作站!

除了 API 级别(您安装并正在使用的 SDK),Java 编程语言中最高级别的构造是包。Java 包使用 package 关键字在 Java 代码的顶部声明应用的包。这必须是声明的第一行代码,而不是注释(见图 3-1;参见第二章。您在第二章中使用的 NetBeans 中的新项目系列对话框将为您创建您的包,并根据您希望在应用中执行的操作导入您需要使用的其他包。在本例中,这些是 JavaFX 包,因此您可以使用 JavaFX 新媒体引擎。

正如您可能已经从名称中确定的那样,Java 包收集了所有的 Java 编程结构。这些包括与您的应用相关的类、接口和方法,因此 InvinciBagel 包将包含您的所有代码,以及您为使用代码而导入的代码,以创建、编译和运行 invinciBagel 游戏。

当然,Java 包对于组织和包含您自己的所有应用代码很有用,但是对于组织和包含 SDK(API)的 Java 代码更有用,您将使用这些代码以及您自己的 Java 编程逻辑来创建您的 Java 8 应用。通过使用 Java import 关键字,您可以使用作为您的目标 API 的一部分的任何类,该关键字与您想要使用的包和类一起构成了一个 import 语句。

import 语句以 import 关键字开始,后面是完全限定的类名,即包名、任何子包名和作为完整命名引用路径的类名(类的完整专有名称)。分号终止 import 语句。正如您已经在图 3-1 中看到的,用于从 javafx.event 包中导入 JavaFX EventHandler 类的 import 语句应该如下所示:

import javafx.event.EventHandler;

import 语句告诉 Java 编译器,您将使用被引用的类中的方法(或常量),使用 import 关键字,以及该类存储在哪个包中。如果您在自己的 Java 类中使用一个类、方法或接口,如 InvinciBagel 类(见图 3-2 ),并且您还没有使用 import 语句声明要使用的类,Java 编译器将抛出一个错误,直到您在类的顶部添加所需的 import 语句(在 Java 包声明语句之后,在 Java 类声明语句之前)。

Note

可以使用完全限定的类名来代替 Java import 关键字,也就是说,在 Java 代码中,类名的前面加上包名。惯例规定使用 import 语句;然而,如果你想打破标准 Java 编程惯例,图 3-2 中的第 20 行可以写成javafx.scene.control.Button btn = new javafx.scene.control.Button();

Java 类:构建的逻辑 Java 结构

包级别下的下一个逻辑 Java 编程构造是 Java 类级别,正如您在 import 语句中看到的,它引用包含类的包和类本身。正如包组织所有相关的类一样,类也组织所有相关的方法、变量和常量,有时还组织其他嵌套类。

因此,Java 类用于在功能组织的下一个逻辑级别组织您的 Java 代码,因此您的类将包含为您的应用添加功能的 Java 代码构造。这些可能包括方法、变量、常量、嵌套类或内部类。

Java 类也可以用来创建 Java 对象。Java 对象是使用 Java 类构造的,并且与 Java 类和该类的构造方法同名。

正如你在图 3-2 中看到的,你使用一个 Java class 关键字和你的类名来声明你的类。您还可以在声明的前面加上 Java 修饰符关键字,您将在本章的后面学习这些关键字(参见“Java 修饰符关键字:访问控制及其他”一节)。Java 修饰符关键字总是放在 Java class 关键字之前,使用以下格式:

<modifier keywords>``class

Java 类的一个强大特性是,它们可以用来模块化您的 Java 游戏代码,这样您的核心游戏应用特性就可以成为一个高级类的一部分,该高级类可以被子类化以创建该类的更专业的版本。在 Java 类层次术语中,一旦一个类被子类化,它就成为一个超类。一个类总是使用一个 Java extends 关键字来子类化一个超类。如果一个类没有以这种方式扩展给定的超类,那么它会自动扩展 Java master class:Java . lang . object。

使用 Java extends 关键字告诉编译器,您希望将超类的能力和功能添加(扩展)到您的类中,一旦使用该 extends 关键字,该类就成为子类。子类扩展了超类提供的核心功能。要扩展您的类定义以包含超类,您可以使用以下格式添加(或扩展,没有双关的意思)现有的类声明:

<modifier keywords>``class``<your custom classname>``extends

当你用你的类扩展一个超类时,它现在是那个超类的一个子类,你可以在你的子类中使用超类的所有特性(嵌套类、内部类、方法、变量、常量),而不需要把它们都显式地写(编码)在你的类的主体中,那样会是多余的(和混乱的)。

Note

如果您正在扩展的超类(或者,如果您喜欢,子类化)中的任何数据字段或方法已经使用 private access control 关键字声明,则这些变量(或常量)和方法将被保留,仅供该超类使用(或者在该超类中使用),因此您的子类将无法访问它们。同样的规则也适用于嵌套类和内部类;这些类结构不能使用任何在包含它们的 Java 构造中声明为私有的代码(或者在它们之上的代码)。

您的类的主体在花括号内编码(见图 3-2 ,最外面的红框),花括号跟在您的类(在本例中,还有 javafx.application.Application 超类)声明之后。这就是为什么您首先学习了 Java 语法,并且您正在使用类声明和包含类定义(变量、常量、方法、构造函数、嵌套类)结构的 Java 语法来构建它。

如图所示,InvinciBagel 类从 JavaFX 包中扩展了一个应用超类。因此,当前超类到子类层次结构的继承图(我将在整本书中使用该工具,向您展示在整个 Java 和 JavaFX API 模式中事物的来源)将如下所示:

> java.lang.Object

> javafx.application.Application

> invincibagel. InvinciBagel

通过扩展 javafx.application 包及其应用类,您将为 InvinciBagel 类提供托管(或运行)javafx 应用所需的一切。JavaFX 应用类“构造”一个应用对象,以便它可以使用系统内存;打电话给安。init()方法,初始化任何可能需要初始化的东西;并调用一个. start()方法(见图 3-2 ,第二个最外面的红框),该方法将最终启动 InvinciBagel Java 8 游戏应用所需的东西放置到位。

当最终用户使用完 InvinciBagel 游戏应用时,application 类使用 Application()构造函数方法创建的 Application 对象将调用它的。stop()方法并从系统内存中删除应用,从而释放内存空间供最终用户使用。您将很快学习 Java 8 方法、构造函数和对象,因为您正在从高级包和类构造向较低级的方法和对象构造前进,因此您正在从高级概述向较低级移动。您可能想知道 Java 类是否可以相互嵌套,也就是说,Java 类是否包含其他 Java 类。答案是肯定的,他们当然可以(也确实可以)!接下来让我们看看 Java 嵌套类的概念。

嵌套类:存在于其他类中的 Java 类

Java 中的嵌套类是定义在另一个 Java 类内部的类。嵌套类是它所嵌套的类的一部分,这种嵌套意味着这两个类打算以某种方式一起使用。嵌套类有两种类型:静态嵌套类,通常简称为嵌套类;非静态嵌套类,通常称为内部类。

静态嵌套类(我将称之为嵌套类)用于创建与包含它们的类一起使用的实用程序,有时仅用于保存与该类一起使用的常量。开发 Android 应用的人非常熟悉嵌套类,因为它们在 Android API 中非常常用,用于保存实用程序方法或 Android 常量,这些方法或常量用于定义屏幕密度设置、动画运动插值曲线类型、对齐常量和用户界面元素缩放设置等内容。如果你正在寻找关于静态概念的理解,它可以被认为是固定的,或者不能被改变。照片是静态图像,而视频不是静态的。在本书中,我们会经常看到这个概念。

嵌套类使用 Java 中通常所说的点符号来引用嵌套类“脱离”其主类或父类,即包含类。比如大师级。NestedClass 将是引用格式,用于通过主类(包含类)名称引用嵌套类,这里使用泛型类类型名称。如果您创建了 InvinciBagel SplashScreen 嵌套类来绘制 Java 游戏的闪屏,那么它将在 Java 代码中被引用为 InvinciBagel。SplashScreen,使用 Java 8 点符号语法。

例如,让我们看一下 JavaFX 应用类,它包含一个参数嵌套类。这个嵌套类封装或包含您可以为 JavaFX 应用设置的参数。因此,这个应用。参数嵌套类将是与应用类相同的 javafx.application 包的一部分,并且如果使用 import 语句,将被引用为 Java FX . Application . Application . parameters。

类似地,构造函数方法可以写成 Application。参数(),因为构造函数方法必须与包含它的类具有完全相同的命名模式。除非您正在为其他开发人员编写代码,这是嵌套类最常用的时候(如 JavaFX 应用类或许多嵌套的实用程序或常量提供者类,您将在 Android OS 中找到),否则您更有可能使用非静态嵌套类(通常称为 Java 内部类)。

嵌套类可以通过使用 Java static 关键字来声明。Java 关键字有时也被称为 Java 修饰符。因此,如果你要做一个InvinciBagel.SplashScreen嵌套类,InvinciBagel类和它的SplashScreen嵌套类声明(Java 8 编程结构)看起来会像这样:

public class InvinciBagel extends Application {

static class SplashScreen {

// The Java code that creates and displays your splashscreen is in here

}

}

需要注意的是,如果您使用import javafx.application.Application.Parameters来导入一个嵌套类,那么您可以在您的类中引用该嵌套类,只使用参数类名,而不是完整的类名路径,该路径显示了您的类的代码如何通过Application.Parameter (ClassName.NestedClassName)点标记语法引用穿过父类到达它的嵌套类。

正如您将在本书中多次看到的,Java 方法也可以使用点符号来访问。因此,不使用类名。如果您使用 import 语句来导入这个嵌套类,那么只需使用NestedClassName.MethodName。这是因为 Java import 语句已经被用来建立这个嵌套类的完整引用路径,通过它的包含类,所以您不需要提供这个完整路径引用,编译器就能知道您引用的是什么代码构造!

接下来,让我们看看非静态嵌套类,它们通常被称为 Java 内部类。

内部类:不同类型的非静态嵌套类

Java 内部类也是嵌套类,但是它们不是在 class 关键字和类名之前使用 static 关键字修饰符声明的,这就是它们被称为非静态嵌套类的原因。因此,在另一个不使用 static (keyword)修饰符的类中的任何类声明在 Java 中都被称为内部类。Java 中有三种类型的内部类:成员类、本地类和匿名类。在本节中,您将发现这些内部类之间的区别,以及它们是如何实现的。

像嵌套类一样,成员类是在包含(父)类的主体中定义的。您可以在包含类的体中的任何地方声明成员类。如果您想访问属于包含类的数据字段(变量或常量)和方法,而不必提供数据字段或方法(ClassName.DataField or ClassName.Method)的路径(通过点符号),您可以声明一个成员类。成员类可以被认为是不使用 Java static modifier 关键字的嵌套类。

尽管嵌套类是通过其包含类(或顶级类)引用的,使用静态嵌套类的点标记路径,但成员类由于不是静态的,所以是特定于实例的,这意味着通过该类创建的对象(实例)可以彼此不同(对象是类的唯一实例),而静态(固定)嵌套类将只有一个版本,不会改变。例如,私有内部类只能由包含它的父类使用。编码为私有类的内部类看起来像这样:

public class InvinciBagel extends Application {

private class SplashScreen {

// The Java code that creates and displays your splashscreen is in here

}

}

因为这个类被声明为私有的,所以它是供您自己的应用使用的(特别是包含类的使用)。因此,这不是供其他类、应用或开发人员使用的实用程序或常量类。您也可以在不使用 private access 修饰符关键字的情况下声明您的内部类,类似于下面的 Java 编程结构:

public class InvinciBagel extends Application {

class SplashScreen {

// The Java code that creates and displays your splashscreen is in here

}

}

这种级别的访问控制称为包或包私有,是应用于任何类、接口、方法或数据字段的默认访问控制级别,这些类、接口、方法或数据字段在声明时不使用任何其他 Java 访问控制修饰符关键字(public、protected、private)。这种类型的内部类不仅可以被顶级类或包含类访问,还可以被包含该类的包中的任何其他类成员访问。这是因为包含类被声明为 public,内部类被声明为 package private。如果希望内部类在包外可用,可以使用下面的 Java 代码结构将其声明为 public:

public class InvinciBagel extends Application {

public class SplashScreen {

// The Java code that creates and displays your splashscreen is in here

}

}

还可以声明一个内部类 protected,这意味着它只能被父类的任何子类访问。如果在不是类的低级 Java 编程结构中声明一个类,比如方法或迭代控制结构(通常称为循环),从技术上讲,它被称为局部类。局部类只在代码块内部可见;因此,它不允许(或者说使用)类修饰符,比如 static、public、protected 或 private。局部类的用法类似于局部变量,只是它是一个复杂的 Java 编码结构,而不是一个简单的局部使用的数据字段值。

最后,有一种内部类叫做匿名类。匿名类是没有给定类名的局部类。您可能会比本地类更频繁地遇到匿名类。这是因为程序员经常不命名他们的本地类(使它们成为匿名类);本地类包含的逻辑只在本地用于它们的声明,所以这些类实际上不需要名字——它们只是在 Java 代码块内部被引用。

Java 方法:核心 Java 函数代码构造

在类内部,通常有方法和这些方法使用的数据字段(变量或常量)。因为我们是从外部到内部,或者从顶层结构到底层结构,所以接下来我将介绍方法。在其他编程语言中,方法有时被称为函数。图 3-2 提供了一个。start()方法,展示了该方法如何保存创建基本“Hello World!”应用。该方法中的编程逻辑使用 Java 编程语句来创建 Stage 对象和 Scene 对象,在 StackPane 对象中的屏幕上放置一个按钮,并定义事件-处理逻辑,以便当单击按钮时,引导 Java 代码编写“Hello World!”文本到 NetBeans IDE 输出区域。

方法声明以访问修饰符关键字开始,可以是 public、protected、private 或 package private(这是通过根本不使用任何访问控制修饰符来指定的)。如图所示,已经使用公共访问控制修饰符声明了. start()方法。

在这个访问控制修饰符之后,您需要声明该方法的返回类型。这是该方法在被调用后将返回的数据类型。因为。start()方法执行设置操作,但不返回特定类型的值,它使用 void 返回类型,这表示该方法执行任务,但不向调用实体返回任何结果数据。在这种情况下,调用实体是 JavaFX 应用类,因为。start()方法是关键方法之一(其他方法是。停止()和。init()方法)来控制 JavaFX 应用的生命周期阶段。

接下来,您将提供方法名,按照惯例(编程规则),它应该以小写字母(或单词,最好是动词)开头,任何后续的(内部)单词(名词或形容词)都以大写字母开头。例如,显示闪屏的方法将被命名为。showSplashScreen()或。displaySplashScreen()并且因为它执行某些操作但不返回值,所以将使用以下代码进行声明:

public void displaySplashScreen() { Java code to display splashscreen goes in here }

如果您需要传递参数,这些参数是命名的数据值,必须在方法体(花括号内的部分)中进行操作,这些参数放在方法名的括号内。在图 3-2 中。start()方法为您的引导“HelloWorld!”JavaFX 应用使用以下 Java 方法声明语法接收一个名为 primaryStage 的 Stage 对象:

public void start(Stage primaryStage) { bootstrap Java code to start Application goes in here }

您可以使用数据类型和参数名称对来提供任意数量的参数,每对之间用逗号分隔。方法也可以没有参数,在这种情况下,参数括号是空的,左括号和右括号紧挨着,例如。start()和。停止()。

定义方法的编程逻辑将包含在方法体中,如前所述,方法体位于定义方法开始和结束的花括号内。方法中的 Java 编程逻辑可以包括变量声明、程序逻辑语句和迭代控制结构(循环),您将利用所有这些来创建您的 Java 游戏。

在继续之前,让我们关注另一个适用于方法的 Java 概念,即重载 Java 方法。重载一个 Java 方法意味着使用相同的方法名,但是不同的参数列表配置。这意味着,如果您定义了多个同名的方法,Java 可以通过查看传递给被调用方法的参数,然后通过匹配参数列表数据类型和名称以及它们出现的顺序,使用该参数列表来辨别要使用的方法(同名的),从而确定要使用哪个(重载的)方法。当然,为了让这个 Java 方法重载特性正常工作,您的参数列表配置必须都是惟一的。

你将在本书的课程中学习如何使用和如何编写 Java 方法,从第四章开始,所以我在这里不打算花太多时间在它们上面,除了定义它们是什么以及它们在 Java 类中如何声明和使用的基本规则。

然而,我将要详细介绍的一种特殊的方法是构造函数方法。这是一种可用于创建对象的方法。Java 对象是 OOP 的基础,所以接下来您将看一看构造函数方法,因为在学习 Java 对象本身之前这样做是很重要的,您将在本章的后面学习 Java 对象(参见“Java 对象:虚拟现实,使用 Java 构造”一节)。

创建 Java 对象:调用类的构造函数方法

一个 Java 类可以包含一个与该类同名的构造函数方法,该方法可用于使用该类创建 Java 对象。构造函数方法使用它的 Java 类像蓝图一样在内存中创建该类的一个实例,这样就创建了一个 Java 对象。构造函数方法总是返回一个 Java 对象,因此不使用其他方法通常使用的任何 Java 返回类型(void、String 等)。使用 Java new 关键字调用构造函数方法,因为您正在创建一个新对象。

你可以在图 3-2 (ll)所示的引导 JavaFX 代码中看到这样的例子。20、28 和 30),其中通过使用以下对象声明、命名和创建 Java 代码结构,分别创建新按钮、StackPane 和 Scene 对象:

<Java class name> <your object instance name> =``new

以这种方式声明 Java 对象的原因是因为 Java 对象是 Java 类的一个实例,在一个以分号结束的 Java 语句中使用类名、正在构造的对象名、Java new 关键字和类的构造函数方法名(以及参数,如果有的话)。

例如,让我们看一下在当前 Java 代码的第 20 行创建的按钮对象。这里,通过 equals 操作符左侧的 Java 语句部分,您告诉 Java 语言编译器,您想要创建一个名为 btn 的按钮对象,使用 JavaFX 按钮类作为对象蓝图。这将声明 Button 类(对象类型)并给它一个惟一的名称。

因此,创建对象的第一部分称为对象声明。创建 Java 对象的第二部分称为对象实例化,从 equals 操作符的右边可以看出,这部分对象创建过程包括一个构造函数方法和 Java new 关键字。

要实例化一个 Java 对象,需要调用 Java new 关键字,并调用对象构造器方法。因为这发生在 equals 操作符的右边,所以对象实例化的结果放在声明的对象中,该对象在 Java 语句的左边。正如你将在本章稍后看到的,当我讨论操作符时(参见“Java 操作符:在应用中操作数据”一节),这就是 equals 操作符的作用,它是一个有用的操作符。

这就完成了声明(类名)、命名(对象名)、创建(使用 new 关键字)、配置(使用构造函数方法)和加载(使用 equals 操作符)您自己的定制 Java 对象的过程。

值得注意的是,这个过程的声明和实例化部分也可以使用单独的 Java 代码行进行编码。例如,按钮对象实例化(见图 3-2 ,l. 20)可以编码如下:

Button btn;

btn =``new

这一点很重要,因为以这种方式编写对象创建代码允许您在类的顶部声明一个对象,其中类中使用或访问这些对象的每个方法都可以看到该对象。在 Java 中,除非使用修饰符声明,否则对象或数据字段只在声明它的 Java 编程结构(类或方法)中可见。

如果你在你的类中声明一个对象,因此在类中包含的所有方法之外,那么你的类中的所有方法都可以访问(使用)那个对象。类似地,在方法中声明的任何内容都是该方法的局部内容,并且仅对该方法的其他成员可见(方法范围分隔符中的 Java 语句)。如果您想实现这个单独的对象声明(在类中,在方法之外)和对象实例化(在。start()方法)在您当前的 InvinciBagel 类中,InvinciBagel 类的前几行 Java 代码将更改为如下所示的 Java 编程逻辑:

public class InvinciBagel extends Application {

Button btn;

@Override

public void start(Stage primaryStage) {

btn = new Button();

btn.setText("Say 'Hello World'");

// The other programming statements continue underneath here

}

}

当对象声明和实例化被分开时,它们可以根据可见性的需要放在方法内部(或外部)。在前面的代码中,InvinciBagel 类的其他方法可以调用。setText()方法调用显示 Java 编译器没有抛出错误。图 3-2 中按钮对象的声明方式,只有。start()方法可以看到对象,因此只有。start()方法可以使用这个 btn.setText()方法调用。

创建构造函数方法:对对象的结构进行编码

构造函数方法更像是一种在系统内存中创建对象的方法,而其他方法(或函数,如果使用不同的编程语言)通常用于执行某种类型的计算或处理。构造函数方法用于在内存中创建 Java 对象,而不是执行其他一些编程功能,这一点可以通过使用 Java new 关键字来证明,该关键字在内存中创建新对象。因此,构造函数方法将定义对象的结构,并允许调用实体使用构造函数方法的参数列表用自定义数据值填充对象结构。

在本节中,您将创建两个示例构造函数方法,以了解如何实现这一点以及构造函数方法通常包含的内容。假设您正在为您的游戏创建 InvinciBagel 对象,那么您使用以下 Java 代码结构声明一个公共 InvinciBagel()构造函数方法:

public InvinciBagel() {

int lifeIndex = 1000;  // Defines units of lifespan

int hitsIndex = 0;    //  Defines units of damage ("hits" on the object)

String directionFacing = "E";        // Direction that the object is facing

Boolean currentlyMoving = false;  //  Flag showing if the object is in motion

}

当使用InvinciBagel mortimer = new InvinciBagel(); Java 方法调用时,这个构造函数方法创建一个名为 mortimer 的 InvinciBagel 对象,它有 1000 个生命单位,没有命中,面向东方,当前没有移动。

接下来,让我们探讨一下重载构造函数方法的概念,您在前面已经学过了(参见“Java 方法:Java 核心函数代码构造”一节),并创建另一个构造函数方法,该方法具有允许您在创建 InvinciBagel 对象时定义其 lifeIndex 和 directionFacing 变量的参数。此构造函数方法如下所示:

public InvinciBagel(``int lifespan, String direction

int lifeIndex;

int hitsIndex;

String directionFacing = null;

Boolean currentlyMoving = false;

lifeIndex = lifespan;

directionFacing = direction;

}

在这个版本中,顶部的 lifeIndex 和 hitsIndex 变量被初始化为 0,这是一个整数的默认值,因此您不必在代码中使用 lifeIndex = 0 或 hitsIndex = 0。Java 编程语言支持方法重载,所以如果您使用一个InvinciBagel bert = new InvinciBagel(900, "W");方法调用来实例化 InvinciBagel 对象,将会使用正确的构造函数方法来创建该对象。名为 bert 的 InvinciBagel 对象的生命指数为 900 个生命单位,没有生命命中,面向西方,目前不会移动。

您可以拥有任意多的(重载的)构造函数方法,只要它们都是 100%唯一的。这意味着重载的构造函数必须有不同的参数列表配置,包括参数列表长度(参数的数量)和参数列表类型(数据类型的顺序)。如您所见,参数列表(长度、数据类型、顺序)允许 Java 编译器区分重载方法。

Java 变量和常量:数据字段中的值

下一层(从 API、包、类、方法,到 Java 类和方法中操作的实际数据值)是数据字段。数据值保存在一个叫做变量的东西里;如果数据是固定的,或者是永久的,它就叫做常量。常量是一种特殊类型的变量(我将在下一节中介绍),因为正确声明一个常量比声明一个 Java 变量要复杂(高级)一些。

在 Java 行话中,在类的顶部声明的变量被称为成员变量、字段或数据字段,尽管所有的变量和常量都可以被认为是数据字段。在方法中声明的变量或在类或方法中声明的其他低级 Java 编程结构称为局部变量,因为它只能在花括号分隔的编程结构中局部看到。最后,在方法声明或方法调用的参数列表区域中声明的变量被称为参数,这并不奇怪。

变量是一个数据字段,它保存了 Java 对象或软件的一个属性,这个属性会随着时间的推移而改变。可以想象,这对于游戏编程来说尤其重要。最简单的变量声明形式可以通过使用 Java 数据类型关键字,以及您希望在 Java 程序逻辑中使用的变量名称来实现。在上一节中,使用构造函数方法,您声明了一个名为 hitsIndex 的整数变量来保存 InvinciBagel 对象在游戏过程中受到的伤害或命中。您使用以下 Java 变量声明编程语句定义了变量数据类型,并将其命名为:

int hitsIndex; // This could also be coded as: int hitsIndex = 0; (the default Integer is Zero)

正如您在该部分中看到的,您可以使用等号运算符将变量初始化为初始值,以及与声明的数据类型相匹配的数据值:例如:

String facingDirection = "E";

这个 Java 语句声明了一个 String 数据类型变量,并将其命名为 facingDirection,位于 equals 运算符的左侧,然后将声明的变量设置为值“E”,这表示方向是东或右。这类似于对象的声明和实例化,只是 Java new 关键字和构造函数方法被数据值本身所取代,因为现在声明的是变量(数据字段),而不是创建的对象。你将在本章后面了解不同的数据类型(我已经介绍过整数、字符串和对象)(参见“Java 数据类型:在应用中定义数据”一节)。

您还可以在变量声明中使用 Java 修饰符关键字,这一点我将在下一节中介绍,届时我将向您展示如何声明一个不可变的变量,也称为常量,它在内存中是固定的或锁定的,并且不能更改。

既然我已经差不多完成了从最大的 Java 结构到最小的(数据字段)结构的讨论,我将开始讨论适用于 Java 所有层次(类、方法、变量)的主题。这些概念通常会随着您阅读 Java 8 入门章节的结束而变得更加复杂。

在内存中固定数据值:在 Java 中定义数据常量

如果您已经熟悉计算机编程,您会知道通常需要数据字段总是包含相同的数据值,并且在应用运行周期期间不会改变。这些被称为常量,它们是使用特殊的 Java 修饰符关键字定义或声明的,这些关键字用于固定内存中的内容,使它们不能被更改。还有一些 Java 修饰符关键字将限制(或取消限制)对象实例,或者对 Java 类或包内部或外部的某些类的访问(您将在下一节中详细研究)。

要声明固定的 Java 变量,必须使用 Java final 修饰符关键字。“最终”和你父母说某事是最终的意思是一样的:它固定在一个地方,是一个事实,永远不会改变。因此,创建常数的第一步是添加这个 final 关键字,将其放在声明中的数据类型关键字之前。

在声明 Java 常量(以及其他编程语言中的常量)时,一个约定是使用大写字符,每个单词之间带有下划线,这表示代码中的常量。

如果你想为你的游戏创建屏幕宽度和屏幕高度常量,你可以这样做:

final int SCREEN_HEIGHT_PIXELS = 480;

final int SCREEN_WIDTH_PIXELS  = 640;

如果希望类的构造函数方法创建的所有对象都能够看到并使用该常量,可以添加 Java static modifier 关键字,将其放在 final modifier 关键字的前面,如下所示:

static final int SCREEN_HEIGHT_PIXELS = 480;

static final int SCREEN_WIDTH_PIXELS = 640;

如果您只希望您的类以及由此类创建的对象能够看到这些常量,您可以通过在 static modifier 关键字之前放置 Java private modifier 关键字来声明这些常量,使用以下代码:

private static final int SCREEN_HEIGHT_PIXELS = 480;

private static final int SCREEN_WIDTH_PIXELS = 640;

如果您希望任何 Java 类,甚至那些在您的包之外的类(即任何其他人的 Java 类),能够看到这些常量,您可以通过在 static modifier 关键字之前放置 Java public modifier 关键字来声明这些常量,使用下面的 Java 代码:

public static final int SCREEN_HEIGHT_PIXELS = 480;

public static final int SCREEN_WIDTH_PIXELS = 640;

正如您所看到的,为您的类声明一个常量比声明一个简单的变量涉及到更详细的 Java 语句!接下来,您将更深入地了解 Java 修饰符关键字,因为它们允许您控制诸如对类、方法和变量的访问,以及锁定它们以防被修改,以及类似的相当复杂的高级 Java 代码控制概念。

Java 修饰符关键字:访问控制等等

Java 修饰符关键字是保留的 Java 关键字,用于修改主要类型的 Java 编程结构中的代码的访问、可见性或持久性(在应用执行期间内存中存在的时间)。修饰符关键字是第一个在 Java 代码结构之外声明的关键字,因为该结构的 Java 逻辑(至少对于类和方法来说)包含在花括号分隔符内,该分隔符位于类关键字和类名之后,或者方法名和参数列表之后。修饰符关键字可以用于 Java 类、方法、数据字段(变量和常量)和接口。

如您在图 3-2 底部所见,对于。main()方法,由 NetBeans 为您的 InvinciBagel 类定义创建,它使用 public 修饰符,您可以使用多个 Java 修饰符关键字。那个。main()方法首先使用 public 修饰符关键字,这是一个访问控制修饰符关键字,然后使用 static 修饰符关键字,这是一个非访问控制修饰符关键字。

访问控制修饰符:公共、受保护、私有、包私有

让我们先介绍访问控制修饰符,因为它们是首先声明的,在非访问修饰符关键字或返回类型关键字之前,并且因为它们在概念上更容易理解。有四种访问控制修饰符级别适用于任何 Java 代码结构。如果没有声明 access control 修饰符关键字,那么默认的访问控制级别 package private 将应用于该代码结构,这使得它对于 Java 包(在本例中是 invincibagel)中的任何 Java 编程结构都是可见的,因此也是可用的。

其他三个访问控制修饰符级别有自己的访问控制修饰符关键字,包括 public、private 和 protected。这些都是根据它们所做的事情来命名的,所以您可能很清楚如何应用它们来公开共享您的代码或防止它被公开使用,但是让我们在这里详细地讨论每一个,只是为了确保安全,因为访问(安全性)是当今的一个重要问题,无论是在您的代码内部还是在外部世界。我先从最少的访问控制开始!

Java 的 Public 修饰符:允许公众访问 Java 程序结构

Java 公共访问修饰符关键字可以被类、方法、构造函数、数据字段(变量和常量)和接口使用。如果你声明一个公共的东西,它就可以被公众访问!这意味着它可以被世界上任何其他类、任何其他包导入和使用。本质上,您的代码可以在任何使用 Java 编程语言创建的软件中使用。正如您将在 Java 或 JavaFX 编程平台(API)中使用的类中看到的,public 关键字最常用于开源编程 Java 平台或用来创建定制应用(如游戏)的包中。

需要注意的是,如果您试图访问和使用的公共类存在于您自己的包之外的包中(在您的例子中,是 invincibagel),那么 Java 编程约定是使用 Java import 关键字创建一个允许使用该公共类的 import 语句。这就是为什么当您阅读完本书时,您的 InvinciBagel.java 类的顶部将会有几十个 import 语句,因为您将会利用代码库中已存在的 Java 和 JavaFX 类,这些类已经使用 public access control 修饰符关键字进行了编码、测试、精炼和公开,因此您可以随心所欲地使用它们来创建 Java 8 游戏!

由于 Java 中的类继承概念,公共类中的所有公共方法和公共变量都将被该类的子类继承(一旦被子类化,就成为超类)。图 3-2 在InvinciBagel类关键字前面提供了一个公共访问控制修饰符关键字的例子。

Java 的 Protected 修饰符:变量和方法允许子类访问

Java protected access modifier 关键字可由数据字段(变量和常量)和方法(包括构造函数方法)使用,但不能由类或接口使用。protected 关键字允许超类中的变量、方法和构造函数只被其他包(如 invincibagel 包)中该超类的子类访问,或者被包含这些受保护成员的类(Java 构造)的同一个包中的任何类访问。

这个访问修饰符关键字本质上保护了一个类中的方法和变量,该类通过被其他开发人员子类化(扩展)而成为(希望被用作)一个超类。除非您拥有包含这些受保护的 Java 构造的包(事实上您没有),否则您必须扩展超类并从该超类创建您自己的子类,以便能够使用受保护的方法。

您可能想知道,为什么要这样做,以这种方式保护 Java 代码结构?当你在设计一个大的项目时,比如 Android OS API,你通常会希望不要直接在那个类之外或之内使用更高级的方法和变量,而是在一个定义更好的子类结构中使用。

您可以通过保护这些方法和变量构造不被直接使用来实现这种直接的使用预防,这样它们就只成为其他类中更详细的实现的蓝图,而不能被直接使用。本质上,保护一个方法或变量只是把它变成一个蓝图或定义。

Java 的私有修饰符:变量、方法和类只能进行本地访问

Java private access control 修饰符关键字可由数据字段(变量或常量)和方法(包括构造函数方法)使用,但不能由类或接口使用。private 修饰符可以由嵌套类使用;但是,它不能被外部类或主(最顶层)类使用。private access control 关键字允许类中的变量、方法和构造函数只能在该类内部被访问。private access control 关键字允许 Java 实现一个称为封装的概念,在这个概念中,一个类(以及使用该类创建的对象)可以封装自己,可以说对 Java 世界之外隐藏了它的“内部”。封装的 OOP 概念可以在大型项目中使用,以允许团队创建(更重要的是,调试)他们自己的类和对象。这样,其他任何人的 Java 代码都无法破解存在于这些类内部的代码,因为它们的方法、变量、常量和构造函数都是私有的!

access modifier 关键字本质上是将类中的方法或变量私有化,这样它们只能在该类中本地使用,或者由该类的构造函数方法创建的对象使用。除非您拥有包含这些私有 Java 构造的类,否则您不能访问或使用这些方法或数据字段。这是 Java 中最严格的访问控制级别。如果从类内部访问私有变量的公共方法称为公共方法,则可以在类外部访问声明为私有的变量。get()方法调用被声明为公共的,因此提供了通过该公共方法访问私有变量或常量中的数据的路径(或门道)。

Java 的包私有修饰符:包中的变量、方法和类

如果没有声明 Java 访问控制修饰符关键字,那么默认的访问控制级别(也称为包私有访问控制级别)将应用于该 Java 构造(类、方法、数据字段或接口)。这意味着这些 Java 构造对于包含它们的 Java 包中的任何其他 Java 类都是可见的,或者是可用的。这种包私有级别的访问控制最容易与您的方法、构造函数、常数和变量一起使用,因为它只需通过不显式声明访问控制修饰符关键字来应用。

您将在自己的 Java 应用(游戏)编程中经常使用这个默认的访问控制级别,因为通常您是在自己的包中创建自己的应用,供用户在编译后的可执行状态下使用。但是,如果您正在开发游戏引擎供其他游戏开发者使用,您将会使用更多我在本节中讨论的访问控制修饰符关键字来控制其他人如何使用您的代码。

非访问控制修饰符:final、static、abstract、volatile、synchronized

不专门为 Java 构造提供访问控制特性的 Java 修饰符关键字被称为非访问控制修饰符关键字。这些包括经常使用的 final、static 和 abstract 修饰符关键字,以及不经常使用的 synchronized 和 volatile 修饰符关键字,它们用于更高级的线程控制,我不会在这个初级编程标题中讨论它们,除非描述它们的含义和作用,以防您在 Java 世界旅行中遇到它们。

我将按照复杂性的顺序来介绍这些概念,也就是说,从初学者最容易理解的概念到面向对象编程初学者最难理解的概念。面向对象编程就像冲浪,因为它看起来很难,直到你练习多次,然后突然你就明白了!

Java 的最后一个修饰符:不可修改的变量、方法和类

您已经研究了 final 修饰符关键字,因为它与 static 关键字一起用于声明常数。最终数据字段变量只能初始化(设置)一次。final 引用变量是一种特殊类型的 Java 变量,它包含对内存中对象的引用,不能被更改(重新分配)来引用不同的对象;然而,保存在(最终)被引用对象中的数据是可以更改的,因为只有对对象本身的引用才是最终引用变量,最终引用变量实际上是用 Java final 关键字锁定的。

Java 方法也可以使用 final 修饰符关键字来锁定。当一个 Java 方法成为 final 时,如果包含该方法的 Java 类被子类化,那么这个 final 方法不能在子类的主体中被覆盖或修改。这基本上锁定了方法代码结构内部的内容。例如,如果您想要。InvinciBagel 类的 start()方法(如果它曾经被子类化过的话)总是做与 InvinciBagel 超类相同的事情(准备一个 JavaFX 登台环境),您使用下面的代码:

public class InvinciBagel extends Application {

Button btn;

@Override

public``final

btn = new Button();

// The other method programming statements continue here

}

}

这可以防止任何子类(public class InvinciBagelReturns 扩展 InvinciBagel)改变 InvinciBagel 游戏引擎(JavaFX)的初始设置,这就是。start()方法确实适合你的游戏应用(见第四章)。使用 final modifier 关键字声明的类不能被扩展或子类化,从而锁定该类以备将来使用。

Java 的静态修饰符:存在于类中的变量或方法(不在对象中)

正如您已经看到的,static 关键字可以与 final 关键字结合使用来创建一个常量。static 关键字用于创建独立存在的 Java 构造(方法或变量),或者在使用定义静态变量或静态方法的类创建的任何对象实例之外。一个类中的静态变量将强制该类的所有实例共享该变量中的数据,就从该类创建的对象而言,几乎就像它是一个全局变量一样。类似地,静态方法也将存在于该类的实例化对象之外,并将由所有这些对象共享。静态方法不会引用自身外部的变量,例如实例化对象的变量。

一般来说,静态方法有自己的内部(局部或静态)变量和常数,还会使用方法参数列表接收变量,然后根据这些参数和自己的内部(静态局部)常数(如果需要)提供处理和计算。因为 static 是一个应用于类实例的概念,因此比任何类本身的级别都低,所以不能使用 static 修饰符关键字来声明类。

Java 的抽象修饰符:要扩展和实现的类和方法

Java abstract modifier 关键字更多的是保护您的实际代码,而不是运行时放在内存中的代码(对象实例和变量等等)。abstract 关键字允许您指定如何将代码用作超类,也就是说,一旦代码被扩展,它如何在子类中实现。因此,它仅适用于类和方法,不适用于数据字段(变量和常量)。

使用 abstract modifier 关键字声明的类不能被实例化,它旨在用作超类(蓝图)来创建(扩展)其他类。因为 final 类不能扩展,所以不能在类级别同时使用 final 和 abstract 修饰符关键字。如果一个类包含任何使用 abstract modifier 关键字声明的方法,则该类本身必须声明为抽象类。然而,抽象类不必包含任何抽象方法。

使用 abstract modifier 关键字声明的方法是声明用于子类但没有当前实现的方法。这意味着它的方法体中没有 Java 代码,如你所知,在 Java 中是用花括号描述的。任何扩展抽象类的子类都必须实现所有这些抽象方法,除非该子类也被声明为抽象,在这种情况下,抽象方法被传递到下一个子类级别。

Java 的 Volatile 修饰符:对数据字段的高级多线程控制

Java volatile modifier 关键字在开发多线程应用时使用,在基本的游戏开发中不打算这样做,因为您希望对游戏进行足够好的优化,以便它只使用一个线程。volatile 修饰符告诉运行应用的 Java 虚拟机(JVM)将声明为 volatile 的数据字段(变量或常量)的私有(线程的)副本与系统内存中该变量的主副本合并。

这类似于 static modifier 关键字,区别在于静态变量(数据字段)由多个对象实例共享,而可变数据字段(变量或常量)由多个线程共享。

Java 的 Synchronized 修饰符:对方法的高级多线程控制

Java synchronized 修饰符关键字也在开发多线程应用时使用,在这里您不会为基本的游戏开发这样做。synchronized 修饰符告诉运行应用的 JVM,声明为 synchronized 的方法一次只能被一个线程访问。这个概念类似于同步数据库访问的概念,它可以防止记录访问冲突。synchronized 修饰符关键字同样可以防止访问您的方法(在系统内存中)的线程之间的冲突,方法是将访问序列化为一次一个,这样就不会出现多个线程并行(同时)访问内存中的一个方法的情况。

既然您已经学习了主要的 Java 构造(类、方法和字段)和基本的修饰符关键字(public、private、protected、static、final、abstract 等等),现在让我们进入花括号,了解用于创建 Java 编程逻辑的工具,这些逻辑将最终定义您的游戏应用的游戏玩法。

Java 数据类型:在应用中定义数据类型

因为您已经了解了一些 Java 数据类型中遇到的变量和常量,所以接下来让我们来研究这些变量和常量,因为这对于您当前从简单到复杂的主题的进展来说并不太难!

Java 中有两种主要的数据类型分类:原始数据类型,如果您使用过不同的编程语言,这是您最熟悉的数据类型;引用(对象)数据类型,如果您使用过另一种 OOP 语言,如 Lisp、Python、Objective-C、C++或 C# (C Sharp),您就会知道这种数据类型。

原始数据类型:字符、数字和布尔值(标志)

Java 编程语言中有八种原始数据类型,如表 3-1 所示。您将在本书中使用它们来创建您的 InvinciBagel 游戏,所以我现在不打算详细介绍它们中的每一个,只是说 Java 布尔数据变量用于标志或开关(开/关),char 用于 Unicode 字符或创建字符串对象(char 数组),其余的用于保存不同大小和分辨率的数值。整数值保存整数,而浮点值保存分数(小数点值)。对于变量的使用范围,使用正确的数值数据类型是很重要的,因为,正如你在表 3-1 的二进制大小一栏中看到的,大数值数据类型比小数值数据类型多使用八倍的内存。

表 3-1。

Java Primitive Data Types, Along with Their Default Values, Size in Memory, Definition, and Numeric Range

数据类型 默认 二进制大小 定义 范围
布尔 错误的 1 位 真值或假值 0 到 1(假或真)
\u0000 16 位 Unicode 字符 \u0000 到\ uffff
字节 Zero 8 位 有符号整数值 –128 到 127(总共 256 个值)
短的 Zero 16 位 有符号整数值 –-32768 到 32767(总共 65,536 个值)
(同 Internationalorganizations)国际组织 Zero 32 位 有符号整数值 –2147483648 转 2147483647
长的 Zero 64 位 有符号整数值 –9223372036854775808 转 922337203685
漂浮物 Zero 32 位 IEEE 754 浮点值 1.4E-45 至 3.4028235E+38
两倍 Zero 64 位 IEEE 754 浮点值 4.9E-324 至 1.7976931348623157E+308

引用数据类型:对象和数组

(OOP 语言也有引用数据类型,它在内存中提供对另一个包含更复杂数据结构的结构的引用,比如对象或数组。这些更复杂的数据结构是使用代码创建的;在 Java 中,这是一个类。有各种类型的 Java 数组类可以创建数据数组(如简单的数据库)以及 Java 类中的构造函数方法,它可以在内存中创建对象结构,包含 Java 代码(方法)和数据(字段)。

Java 操作符:操纵应用中的数据

在本节中,您将了解 Java 编程语言中一些最常用的运算符,尤其是那些对游戏编程最有用的运算符。这些包括算术运算符,用于数学表达式;关系运算符,用于确定数据值之间的关系(等于、不等于、大于、小于等);逻辑运算符,用于布尔逻辑;赋值运算符,它执行算术运算,并在一次紧凑运算中将值赋给另一个变量(运算符);以及条件运算符,也称为三元运算符,它根据真或假(布尔)计算的结果为变量赋值。

还有概念上更高级的按位运算符,用于在二进制数据(零和一)级别执行运算,其逻辑超出了本书的初学者范围,并且在 Java 游戏编程中的使用不像这些其他更主流类型的运算符那样常见,在本书的过程中,您将使用其中的每一种运算符来完成游戏逻辑中的各种编程目标。

Java 算术运算符

Java 算术运算符是编程中最常用的,尤其是在街机类型的游戏中,游戏中的东西在屏幕上以离散的像素数量移动。从小学到大学,你已经在数学课上学过,使用这些基本的算术运算符可以创建许多更复杂的方程。

表 3-2 中显示的算术运算符中,您可能不太熟悉的是模运算符,它将在除法运算完成后返回余数(剩余的部分),以及递增和递减运算符,它们分别从一个值中加 1 或减 1。这些用于实现您的计数器逻辑。计数器(使用递增和递减操作符)最初用于循环,(我将在下一节介绍);然而,这些递增和递减运算符对游戏编程也非常有用(点数、寿命损失、游戏棋子移动和类似的进展)。

表 3-2。

Java Arithmetic Operators, Their Operation Type, and a Description of the Arithmetic Operation

操作员 操作 描述
加号+ 添加 将运算符两边的操作数相加
减号– 减法 从左操作数中减去右操作数
相乘* 增加 将运算符两边的操作数相乘
划分/ 分开 用左操作数除以右操作数
模数% 剩余物 将左操作数除以右操作数,返回余数
增量++ 添加 1 将操作数的值增加 1
减量- 减去 1 将操作数的值减 1

要实现算术运算符,请将希望接收算术运算结果的数据字段(变量)放在等于赋值运算符的左侧,将希望执行算术运算的变量放在等号的右侧。以下是添加 x 和 y 变量并将结果赋给 z 变量的示例:

Z = X``+

如果你想从 x 中减去 y,你用减号而不是加号;如果你想把 x 和 y 值相乘,你用星号而不是加号;如果你想用 x 除以 y,你要用正斜杠而不是加号。这些操作如下所示:

Z = X``-

Z = X``*

Z = X``/

你会经常用到这些算术运算符,所以在你读完这本书之前,你会得到一些很好的练习。接下来让我们更仔细地看看关系运算符,因为有时您会想要比较值而不是计算它们。

Java 关系运算符

在某些情况下,Java 关系运算符用于在两个变量之间或者变量和常量之间进行逻辑比较。这些应该是你在学校熟悉的,包括等于、不等于、大于、小于、大于或等于和小于或等于。在 Java 中,equal to 在被比较的数据字段之间使用两个等号,并在等号前使用一个感叹号来表示“不等于”表 3-3 显示了关系运算符,以及每个运算符的示例和描述。

表 3-3。

Java Relational Operators, an Example in Which A = 25 and B = 50, and a Description of the Relational Operation

操作员 例子 描述
== 不正确 两个操作数的比较:如果它们相等,那么条件等于真
!= (答!= B)为真 两个操作数的比较:如果它们不相等,则条件等于真
> 不正确 两个操作数的比较:如果左操作数大于右操作数,则等于真
< (A < B)为真 两个操作数的比较:如果左操作数小于右操作数,则等于真
>= (A >= B)不正确 比较两个操作数:如果左操作数大于或等于右操作数等于真
<= (A <= B)为真 比较两个操作数:如果左操作数小于或等于右操作数,则等于真

大于符号是向右的箭头,小于符号是向左的箭头。它们用在等号前,分别创建大于或等于和小于或等于关系运算符,如表 3-3 底部所示。

这些关系运算符返回布尔值 true 或 false,因此在 Java 的控制(循环)结构中使用得相当多,在游戏编程逻辑中也用于控制游戏将采取的路径(结果)。例如,假设你想确定游戏屏幕的左边缘在哪里,这样当不可战胜的怪物向左移动时,他就不会离开屏幕。使用这种关系比较:

boolean changeDirection = false ; // Create boolean variable changeDirection, initialize

changeDirection =``(invinciBagelX <= 0)``; //  boolean changeDirection is``TRUE

请注意,我使用了< =小于或等于(是的,Java 也支持负数),因此,如果 invincibegel 越过了屏幕的左侧(x=0),changeDirection 布尔标志将被设置为 true 值,sprite 移动编程逻辑可以通过改变移动方向(因此 invincibegel 从墙上弹开)或完全停止移动(因此 invincibegel 粘在墙上)来处理这种情况。

在本书中,你将会接触到大量的关系操作符,因为它们在创建游戏逻辑时非常有用,所以我们很快就会从中获得很多乐趣。接下来让我们看看逻辑运算符,这样我们就可以处理布尔集合,分组比较事物,这对游戏也很重要。

Java 逻辑运算符:

Java 逻辑运算符类似于布尔运算(并集、交集等。),并允许您确定两个布尔变量是否具有相同的值(和),或者其中一个布尔变量是否与另一个不同(或)。还有一个 NOT 运算符,它反转任何比较的布尔操作数的值。表 3-4 显示了 Java 的三个逻辑操作符,以及每个操作符的例子和描述。

表 3-4。

Java Logical Operators, an Example in Which A = True and B = False, and a Description of Logical Operation

操作员 例子 描述
&& (A && B)是假的 当两个操作数的值相同时,逻辑 AND 运算符等同于 true。
|| (A || B)是真的 当两个操作数的值相同时,逻辑 OR 运算符等同于 true。
!(A && B)是真的 逻辑 NOT 运算符会反转应用它的运算符(或集合)的逻辑状态。

让我们使用逻辑运算符来增强上一节中的游戏逻辑示例,包括 InvinciBagel 在屏幕上移动的方向。现有的 facingDirection 字符串变量将控制 InvinciBagel 面对的方向(如果在运动,还会向内移动)。现在,您可以使用以下逻辑运算符来确定 InvinciBagel 是否面向左侧(W 或 West);如果 travelingWest 布尔变量为 true 如果屏幕左侧的 hit(或 passed)布尔变量 hitLeftSideScrn 也等于 true。为此,修改后的代码将包括另外两个布尔变量声明和初始化,如下所示:

boolean changeDirection = false ; // Create boolean variable changeDirection, initialize

boolean hitLeftSideScrn = false ; // Create boolean variable hitLeftSideScrn, initialize

boolean travelingWest = false ;   // Create boolean variable travelingWest, initialize

hitLeftSideScrn =``(invinciBagelX <= 0)``; //  boolean hitLeftSideScrn is``TRUE

travelingWest =``(facingDirection == "W")``// boolean travelingWest is``TRUE``if facingDirection="``W

changeDirection = (hitLeftSideScrn && travelingWest) // Change Direction, if both equate to TRUE

为了查明 InvinciBagel 是否面向西方(或者如果也在移动,则正在行进),您创建了另一个 travelingWest 布尔变量,并将其初始化(将其设置为 equal)为 false(因为您的初始 facingDirection 设置为 East)。然后,创建一个名为 hitLeftSideScrn 的布尔变量,将其设置为(invinciBagelX <= 0)关系运算符语句。

最后,用travelingWest = (facingDirection == "W")逻辑创建一个关系操作符语句,然后准备将 changeDirection 布尔变量用于新的逻辑操作符。该逻辑运算符将使用changeDirection = (hitLeftSideScrn && travelingWest)逻辑运算编程语句,确保 hitLeftSideScrn 和 travelingWest 布尔变量都设置为真。

现在,您已经开始练习声明和初始化变量,并使用关系和逻辑操作符来确定主要游戏棋子的方向和位置(在街机游戏中称为精灵;关于游戏设计术语的更多信息,请参见第六章。接下来,让我们看看赋值操作符。

Java 赋值运算符

Java 赋值运算符将赋值运算符右侧的逻辑构造中的值赋给赋值运算符左侧的变量。最常见的赋值运算符也是 Java 编程语言中最常用的运算符,即等号运算符。等号运算符可以以任何算术运算符开头,以创建也执行算术运算的赋值运算符,如表 3-5 所示。当变量本身成为等式的一部分时,这允许创建更“密集”的编程语句。因此,不需要写 C = C + A,你可以简单地使用 C+=A 并获得相同的最终结果。在你的游戏逻辑设计中,你会经常用到这个赋值操作符快捷键。

表 3-5。

Java Assignment Operators, What Each Assignment Is Equal to in Code, and a Description of the Operator

操作员 例子 描述
= C=A+B 基本赋值运算符:将右侧操作数的值赋给左侧操作数
+= C+=A 等于 C=C+A 加法赋值运算符:将右操作数与左操作数相加;将结果放入左操作数
-= 等于 C=C-A 子赋值运算符:从左操作数中减去右操作数;将结果放入左操作数
*= C=A 等于 C=CA MULT 赋值运算符:将右操作数和左操作数相乘;将结果放入左操作数
/= C/=A 等于 C=C/A DIV 赋值运算符:将左操作数除以右操作数;将结果放入左操作数
%= C%=A 等于 C=C%A 模赋值运算符:用左操作数除以右操作数;将余数放入左侧操作数

最后,你要看看条件操作符,它也允许你编写强大的游戏逻辑。

Java 条件运算符

Java 语言也有一个条件运算符,它可以计算一个条件,并根据该条件的解析,只使用一个紧凑的编程结构为您进行变量赋值。条件运算符的通用 Java 编程语句总是使用以下基本格式:

Variable = (evaluated expression) ? Set this value if TRUE : Set this value if FALSE ;

所以,在等号的左边,你有一个变量,它会根据等号右边的内容而改变(被设置)。这与你到目前为止所学的一致。

在等号的右边,有一个计算表达式,例如,“x 等于 3”,后面是一个问号,然后是两个用冒号分隔的数值,最后是一个分号来结束条件运算符语句。如果要在 x 等于 3 时将变量 y 的值设置为 25,而在 x 不等于 3 时将变量 y 的值设置为 10,则可以使用以下 Java 编程逻辑编写条件运算符编程语句:

y``=``(x``==``3)``?``25``:``10``;

接下来,您将看到 Java 逻辑控制结构,它利用了您刚刚学习的操作符。

Java 条件控制:决策还是循环

正如您刚才看到的,许多 Java 操作符可以有相当复杂的结构,并使用很少的 Java 编程逻辑字符提供很大的处理能力。Java 还有几个更复杂的条件控制结构,一旦你通过编写 Java 逻辑控制结构为那些决策或任务重复设置了条件,它就可以自动为你做出决策或执行重复的任务。

在本节中,您将首先探索决策控制结构,例如 Java switch-case 结构和 if-else 结构。然后,您将了解 Java 的循环控制结构,包括 for、while 和 do-while。

决策控制结构:开关盒和 If-Else

一些最强大的 Java 逻辑控制结构允许您定义在应用运行时希望程序逻辑为您做出的决策。一个这样的结构提供了一个逐案的“平面”决策矩阵;另一个有级联(如果这个,做这个;如果没有,就这样做;如果没有,就这样做;依此类推)一种结构类型,它按照您希望的检查顺序对事物进行评估。

让我们从 Java switch 语句开始,它使用 Java switch 关键字和这个决策树顶部的一个表达式,然后使用 Java case 关键字为这个表达式求值的每个结果提供 Java 语句块。如果表达式求值没有调用(使用)switch 语句结构(花括号)中的任何一种情况,您还可以提供一个 Java default 关键字和 Java 语句代码块来完成您想要做的事情。

case 语句中使用的变量可以是四种 Java 数据类型之一:char(字符)、byte、short 或 int(整数)。您通常希望在每个 case 语句代码块的末尾添加一个 Java break 关键字,至少在这样的用例中是这样,在这种用例中,需要在值之间进行切换,并且对于 switch 语句的每次调用,只有一个是可行的(或允许的)。默认语句“如果其中任何一个不匹配”是开关内的最后一个语句,不需要这个 break 关键字。

如果您没有在每个 case 逻辑块中提供 Java break 关键字,那么在通过 switch 语句的同一次传递中,可以对多个 case 语句进行求值。这将在表达式求值树从顶部(第一个 case 代码块)到底部(最后一个 case 代码块或默认关键字代码块)的过程中完成。因此,如果您有一个布尔“标志”的集合,如 hasValue、isAlive、isFixed 等,这些都可以通过使用一个根本不使用任何 break 语句的 switch-case 语句结构在一次“传递”中处理。

这样做的意义在于,您可以根据 case 语句的求值顺序,以及是否将 break 关键字放在任何给定 case 语句代码块的末尾,创建一些相当复杂的决策树。

切换情况决策树编程构造的一般格式如下所示:

switch (expression) {

case value1 :

programming statement one;

programming statement two;

break;

case value2 :

programming statement one;

programming statement two;

break;

default :

programming statement one;

programming statement two;

}

假设你想在你的游戏中决定当不可战胜的怪物被击中时(被击中,被打成肉泥,被击中,等等)调用哪个不可战胜的怪物死亡动画。死亡动画例程(方法)将被调用,基于无敌手被击中时的活动状态,例如飞行(F)、跳跃(J)、奔跑(R)或空闲(I)。假设这些状态保存在一个名为 ibState 的数据字段中,类型为 char,它保存一个字符。一旦命中发生,使用这些游戏块状态指示器来调用正确方法的开关情况代码结构将是:

switch (ibState) {            // Evaluate ibState char and execute case code blocks accordingly

case 'F' :

deathLogicFlying();  // Java method controlling death sequence if InvinciBagel flying

break ;

case 'J' :

deathLogicJumping(); // Java method controlling death sequence if InvinciBagel jumping

break ;

case 'R' :

deathLogicRunning(); // Java method controlling death sequence if InvinciBagel running

break ;

default :

deathLogicIdle();    // Java method controlling death sequence if InvinciBagel is idle

这个 switch-case 逻辑构造在 switch()语句的求值部分中对 ibState char 变量求值(注意,这是一个 Java 方法),然后为每个游戏角色状态(飞行、跳跃、奔跑)提供一个 case 逻辑块,并为空闲状态提供一个默认逻辑块(这是设置它的一种逻辑方式)。

因为一个游戏棋子不能同时空闲、奔跑、飞行和跳跃,所以需要使用 break 关键字来使这个决策树的每个分支都是唯一的(唯一的)。

switch-case 决策结构通常被认为比 if-else 决策结构更有效、更快,if-else 决策结构只能使用 if 关键字进行简单评估,如下所示:

if``(expression =``true

programming statement one;

programming statement two;

}

您还可以添加一个 else 关键字,使这个决策结构评估在布尔变量(true 或 false 条件)评估为 false 而不是 true 时需要执行的语句,这使这个结构更强大(也更有用)。这种通用编程结构将如下所示:

if``(expression =``true

programming statement one;

programming statement two;

}``else``{                         // Execute this code block if (expression =``false

programming statement one;

programming statement two;

}

此外,可以嵌套 if-else 结构,从而创建 if{}-{else if}-{else if}-else{}结构。如果这些结构嵌套得太深,那么您会希望切换到 switch-case 结构,相对于嵌套的 if-case 结构,if-else 嵌套得越深,这将变得越来越有效。例如,您之前为 InvinciBagel 游戏编写的 switch-case 语句,如果转换为嵌套的 if-else 决策结构,将类似于下面的 Java 编程结构:

if``(ibState = '``F

deathLogicFlying();

}``else if``(ibState = '``J

deathLogicJumping();

}``else if``(ibState = '``R

deathLogicRunning();

}``else

deathLogicIdle();

}

正如您所看到的,这个 if-else 决策树结构与您之前创建的 switch-case 非常相似,只是决策代码结构相互嵌套,而不是包含在一个平面结构中。根据经验,我将使用 if 和 if-else 进行一值和二值评估,使用 switch-case 进行三值和更高值的评估。我在关于 Android 的书中大量使用了这种开关盒结构。

接下来,让我们看看 Java 中广泛使用的其他类型的条件控制结构,即循环编程结构。这些函数允许您执行一个编程语句块预定义的次数(使用 for 循环)或直到实现一个目标(使用 while 或 do-while 循环)。

循环控制结构:While、Do-While 和 For

尽管决策树类型的控制结构被遍历了固定的次数(除非遇到 break [switch-case]或 resolved expression [if-else],否则遍历一次),但循环控制结构会随着时间的推移而不断执行,这对于 while 和 do-while 结构来说,有点危险,因为如果不小心使用编程逻辑,可能会产生无限循环!for 循环结构执行有限数量的循环(数量在 for 循环的定义中指定),您将在本节中看到。

让我们从有限循环开始,首先介绍 for 循环。Java for 循环使用以下通用格式:

for (initialization; boolean expression; update equation) {

programming statement one;

programming statement two;

}

如您所见,for 循环求值区域的三个部分在括号内,用分号分隔,因为每个部分都包含一个编程语句。第一个是变量声明和初始化,第二个是布尔表达式求值,第三个是显示如何在每次循环中递增循环的更新方程。

要在屏幕上沿 X 和 Y 方向移动 InvinciBagel 40 像素diagonally, for 循环如下:

for``(int x=0; x < 40; x = x + 1) {   // Note: an x = x + 1 statement could also be coded as

invinciBagelX++;  // Note: invinciBagelX++ could be coded invinciBagelX = invinciBagelX + 1;

invinciBagelY++;  // Note: invinciBagelY++ could be coded invinciBagelY = invinciBagelY + 1;

}

相反,while(或 do-while)类型的循环不会在有限数量的处理周期内执行,而是使用以下结构执行循环内的语句,直到满足条件:

while (boolean expression) {

programming statement one;

programming statement two;

}

使用 while 循环结构对 for 循环进行编码,将 InvinciBagel 移动 40 个像素,如下所示:

int x = 0;

while (x < 40) {

invinciBagelX++;

invinciBagelY++;

x++

}

do-while 循环和 while 循环之间的唯一区别是,对于后者,循环逻辑编程语句是在计算之前而不是之后执行的。因此,使用 do-while 循环编程结构,前面的示例将编写如下:

int x = 0;

do {

invinciBagelX++;

invinciBagelY++;

x++

}``while

正如您所看到的,Java 编程逻辑结构在花括号内,跟在 Java do 关键字后面,在右花括号后有 while 语句。注意,while 求值语句(以及整个构造)必须以分号结束。

如果要确保 while 循环结构中的编程逻辑至少执行一次,请使用 do-while,因为求值是在循环逻辑执行之后执行的。如果您想确保循环中的逻辑只在计算成功后(无论何时)才执行,这是更安全的编码方式,请使用 while 循环结构。

Java 对象:虚拟现实,使用 Java 结构

我把最好的 Java 对象留到了最后,因为它们可以用一种或另一种方式使用我在这一章中介绍的所有概念来构造,还因为它们是 OOP 语言(在这种情况下,是 Java 8)的基础。事实上,Java 8 编程语言中的一切都是基于 Java 的 Object 超类(我喜欢称之为 masterclass),它在 java.lang 包中,因此它的 import 语句将引用 java.lang.Object,Java Object 类的完整路径名。

Java 对象是用来“虚拟化”现实的,它允许你在日常生活中看到的周围的物体,或者,在你的游戏中,你根据你的想象创造的物体,被逼真地模拟。这是通过使用数据字段(变量和常量)和您在本章中学习的方法来完成的。这些 Java 编程结构将组成对象特征或属性(常量);状态(变量);和行为(方法)。Java 类构造组织每个对象定义(常量、变量和方法),并使用类的构造函数方法生成该对象的实例,该类通过各种 Java 关键字和构造来设计和定义对象。

创建 InvinciBagel 对象:属性、状态和行为

让我们以 InvinciBagel 对象为例,展示常量如何定义特征,变量如何定义状态,方法如何定义行为。我们将使用到目前为止你在本章中学到的 Java 编码结构来完成这项工作,包括你已经在某种程度上定义的常量、变量和方法。

让我们从特征开始,特征是关于一个物体的不会改变的东西,因此可以用常数来表示,这些常数是不会(不能)改变的变量。百吉饼的一个重要特征是类型(风味)。我们都有自己的最爱;我的有原味、鸡蛋、黑麦、洋葱和粗面包。另一个特点是百吉饼的大小;众所周知,有迷你百吉饼,正常大小的百吉饼,还有巨型百吉饼。

private static final String FLAVOR_OF_BAGEL = "Pumpernickel";

private static final String SIZE_OF_BAGEL = "Mini Bagel";

因此,常量用于定义对象的特征或属性。如果您正在定义一辆汽车、一艘船或一架飞机,颜色(油漆)、引擎(类型)和传输(类型)是属性(常量),因为它们通常不会改变,除非您是一名机械师或拥有一家车身修理厂!

关于将改变的对象的事情,例如它的位置、方向、它是如何行进的(飞行、驾驶、行走、奔跑)等等,被称为状态,并且使用变量来定义,这些变量可以基于现实生活中正在发生的事情实时地不断改变。这些变量将允许任何 Java 对象模仿或虚拟化它们在 Java 世界的虚拟现实中创建的真实世界的对象。当然,在游戏中尤其如此,这也是为什么本书的主题《Java 和游戏》特别相关和适用的原因。

InvinciBagel 的状态(变量)会比属性(常量)多,因为它是游戏的一部分,会特别积极地试图挽救它的洞和得分。您想要定义为变量的一些状态包括屏幕(x,y)位置、方向、行进方向、行进类型、击中次数和使用寿命。

public int invinciBagelX = 0;                    // X screen location of the InvinciBagel

public int invinciBagelY = 0;                   //  Y screen location of the InvinciBagel

public String bagelOrientation = "side";       //   Defines bagel orientation (front, side, top)

public int lifeIndex = 1000;                  //    Defines units of lifespan used

public int hitsIndex = 0;                    //     Defines units of damage (hits taken)

public String directionFacing = "E";        //      Direction that the object is facing

public String movementType = "idle"        //  Type of movement (idle, fly, run, jump)

public boolean currentlyMoving = false;   //   Flag showing if the object is in motion

随着您继续阅读本书并创建 invincibegel 游戏,您将添加属性、状态和行为,使 invincibegel 及其游戏环境和游戏玩法更加逼真、有趣和令人兴奋,就像您在现实生活中所做的一样。事实上,您正在使用 Java 对象和 Java 构造来建模一个真实的虚拟世界,在这个世界中,InvinciBagel 玩家可以战胜邪恶,并向美味的目标发射奶油芝士球。

让我们来看几个控制 InvinciBagel 行为的方法。在本书的过程中,您将创建复杂的方法来完成游戏目标,因此我将在这里向您介绍方法如何为对象提供行为,以演示如何创建反映真实世界对象功能的对象。

对于您的 InvinciBagel 游戏,主要行为将是相对于 x(宽度)和 y(高度)维度在屏幕上的 2D 移动,这将访问、使用和更新整数 invinciBagelX、invinciBagelY 和前面讨论的布尔 currentlyMoving 数据字段;InvinciBagel 角色的方向(正面、侧面、朝下等),这将访问、使用和更新 bagelOrientation 字符串字段;InvinciBagel 的预期寿命,它将访问、使用和更新 lifeIndex 变量;InvinciBagel 的运行状况,它将访问、使用和更新 hitsIndex 变量。InvinciBagel 行进的方向(东或西),这将访问、使用和更新 directionFacing 字符串变量。以及 InvinciBagel 正在使用的运动类型(飞、跳、跑、闲),这将访问、使用和更新 movementType 字符串变量。

下面是如何声明这些方法(行为)以及关于它们将要做什么的伪代码:

public void moveInvinciBagel(int x, int y) {

currentlyMoving = true;

invinciBagelX = x;

invinciBagelY = y;

}

public String getInvinciBagelOrientation() {

return bagelOrientation;

}

public void setInvinciBagelOrientation(String orientation) {

bagelOrientation = orientation;

}

public int getInvinciBagelLifeIndex() {

return lifeIndex;

}

public void setInvinciBagelLifeIndex(int lifespan) {

lifeIndex = lifespan;

}

public String getInvinciBagelDirection() {

return directionFacing;

}

public void setInvinciBagelDirection(String direction) {

directionFacing = direction;

}

public int getInvinciBagelHitsIndex() {

return hitsIndex;

}

public void setInvinciBagelHitsIndex(int damage) {

hitsIndex = damage;

}

public String getInvinciBagelMovementType() {

return movementType;

}

public void setInvinciBagelMovementType(String movement) {

movementType = movement;

}

惯例是创建。get()和。set()方法,就像这里所做的那样。这些允许您的 Java 代码轻松访问您的对象状态(变量)。现在,是时候将所有这些属性(常量)、状态(变量)和行为(方法)安装到对象的蓝图中了。如前所述,这是使用 Java 类编程结构完成的。

创建无敌蓝图:创建游戏片类

让我们将所有这些 InvinciBagel 虚拟化代码安装到一个游戏片类中,以创建一个用于游戏片对象的类和构造函数方法:

public class GamePiece

private static final String FLAVOR_OF_BAGEL = "Pumpernickel"; // Flavor (or type) of bagel

private static final String SIZE_OF_BAGEL = "Mini Bagel";    // Size (classification) of bagel

public int invinciBagelX = 0;                    // X screen location of the InvinciBagel

public int invinciBagelY = 0;                   //  Y screen location of the InvinciBagel

public String bagelOrientation = "side";       //  Define bagel orientation (front, side, top)

public int lifeIndex = 1000;                  //   Defines units of lifespan used

public int hitsIndex = 0;                    //    Defines units of damage (hits taken)

public String directionFacing = "E";        //  Direction that the bagel object is facing

public String movementType = "idle";       //   Type of movement (idle, fly, run, jump)

public boolean currentlyMoving = false; //    Flag showing if the object is in motion

public void moveInvinciBagel(int x, int y) {        // Movement Behavior

currentlyMoving = true;

invinciBagelX = x;

invinciBagelY = y;

}

public String getInvinciBagelOrientation() {        // Get Method for Orientation

return bagelOrientation;

}

public void setInvinciBagelOrientation(String orientation) {     // Set Method for Orientation

bagelOrientation = orientation;

}

public int getInvinciBagelLifeIndex() {             // Get Method for Lifespan

return lifeIndex;

}

public void setInvinciBagelLifeIndex(int lifespan) {             // Set Method for Lifespan

lifeIndex = lifespan;

}

public String getInvinciBagelDirection() {          // Get Method for Facing Direction

return directionFacing;

}

public void setInvinciBagelDirection(String direction) {         // Set Method for Direction

directionFacing = direction;

}

public int getInvinciBagelHitsIndex() {             // Get Method for Hits (damage)

return hitsIndex;

}

public void setInvinciBagelHitsIndex(int damage) {           // Set Method for Hits (damage)

hitsIndex = damage;

}

public String getInvinciBagelMovementType() {         // Get Method for Movement Type

return movementType;

}

public void setInvinciBagelMovementType(String movement) {   // Set Method for Movement Type

movementType = movement;

}

}

值得注意的是,这些常量、变量和方法用于演示类、方法和数据字段关键字如何让开发人员创建(虚拟化)他们的游戏组件。随着游戏的开发,这些可能会改变,因为游戏开发是一个改进的过程,在这个过程中,您会不断地改变和增强 Java 代码库,以添加特性和功能。

现在,你所要做的就是添加你的 GamePiece()构造函数方法,这将创建一个新的对象,该对象具有你希望默认游戏块包含的初始化变量设置。然后,您将创建第二个重载构造函数方法。第二个构造函数方法将允许参数传递到构造函数方法中,以便您可以为这些相同的变量(状态)提供自定义(非默认)设置。这样,如果调用 GamePiece(),就得到一个默认对象;如果你调用 GamePiece(这里有参数列表),你会得到一个自定义对象。

创建一个 GamePiece()构造函数:重载一个 GamePiece

最后,让我们创建构造函数方法(实际上是两个),它从 GamePiece 类获取状态(变量)并创建一个默认对象。您将使用此对象创建自定义重载构造函数方法。第一个构造函数方法将使用包私有访问控制方法,不使用 access modifier 关键字,这样 invincibagel 包中的任何代码都可以调用这个构造函数方法。然后,您将使用以下 Java 代码设置默认变量:

GamePiece() {

invinciBagelX = 0;

invinciBagelY = 0;

bagelOrientation = "side";

lifeIndex = 1000;

hitsIndex = 0;

directionFacing = "E";

movementType = "idle";

currentlyMoving = false;

}

重载的构造函数方法将在方法参数列表区域为那些逻辑上允许对象创建时发生变化的变量声明参数。唯一不允许变化的两个变量是 hitsIndex(一个新对象不会有任何伤害点,因此需要为 0)和 currentlyMoving(一个新对象出现时不会移动,即使只是一秒钟的一部分),您将初始化这两个变量,就像您对默认构造函数所做的那样。其他五个变量(状态)将使用通过参数列表传递的参数来设置,使用一个等于赋值操作符。这是使用以下代码完成的:

GamePiece(int``x``, int``y``, String``orientation``, int``lifespan``, String``direction``, String``movement``) {

invinciBagelX =``x

invinciBagelY =``y

bagelOrientation =``orientation

lifeIndex =``lifespan

hitsIndex = 0;

directionFacing =``direction

movementType =``movement

currentlyMoving = false;

}

我在参数列表中加粗了变量,以及它们在构造函数方法中的使用位置,以设置对象的状态(变量)。这些变量是在 GamePiece 类的顶部声明的,您已经用它来设计、定义和创建 GamePiece 对象。第二个构造函数方法可以说是重载了第一个构造函数方法,因为它使用了完全相同的方法调用(方法名),带有不同的参数列表(充满参数,而不是空的或没有参数)。这给了你一个默认的对象构造方法和一个自定义的对象构造方法,所以在你的游戏逻辑中,你可以创建一个默认的或者自定义的游戏角色。

摘要

在第三章中,您了解了 Java 8 编程语言中一些更重要的概念和结构。当然,我不可能在一章中涵盖 Java 的所有内容,所以在本书的过程中,我坚持使用你将用来创建游戏的概念、构造和关键字。大多数 Java 书籍都有 800 页或更多,所以如果你想真正深入了解 Java,我建议从基肖里·莎兰的《Java 8 基础》开始(Apress,2014)。

您首先从 Java 的高级角度出发,考虑它的语法,包括 Java 注释和分隔符、API 以及 Java API 包含的 Java 包。您还学习了 Java 类,包括嵌套类和内部类,因为 Java 包包含 Java 类。然后你进入 Java 的下一个层次,到方法,就像其他编程语言中的函数,以及一种特殊的 Java 方法,叫做构造函数方法。

接下来,您探索了 Java 如何表示数据,使用字段或数据字段,研究了不同的类型,如常量、固定数据字段、变量或可以改变其值的数据字段。之后,您仔细查看了 Java 修饰符关键字,包括 public、private 和 protected 访问控制关键字,以及 final、static、abstract、volatile 和 synchronized 非访问控制修饰符关键字。

在完成了基本的代码结构以及如何修改它们之后,您转到了主要的 Java 数据类型,比如 boolean、char、byte、int、float、short、long 和 double,然后探索了用于处理这些数据类型或者将这些数据类型过渡到您的编程逻辑的 Java 操作符。您学习了算术运算符,用于数值;逻辑运算符,用于布尔值;关系运算符,用于考虑数据值之间的关系;条件运算符,允许您建立任何条件变量赋值;和赋值运算符,它们允许您为变量赋值(或在变量之间赋值)。

然后,您查看了 Java 逻辑控制结构,包括决策控制结构(我喜欢称之为决策树)和循环或迭代逻辑控制结构。您了解了 Java switch-case 结构、if-else 结构、for 循环结构和 do-while 循环结构。最后,您研究了 Java 对象,发现了如何使用 Java 类、方法和构造函数方法定义对象属性、状态和行为。

在下一章中,我将概述 JavaFX 多媒体引擎及其类和功能,因为您将利用 JavaFX 向游戏添加媒体元素,如图像、视频和音频,并使用 JavaFX 对象构造(类)控制游戏,如 Stage、Scene 和 StackPane。

四、JavaFX 8 简介:探索 Java 8 多媒体引擎的功能

让我们通过学习构成 JavaFX 8 多媒体引擎的功能、组件和类,在前面章节第四章中学习 Java 编程语言的知识。这个令人惊叹的 JavaFX 8 API 是使用 JavaFX 包添加到 Java 8 中的,你在第二章和第三章中看到了这个包,它是与 Java 8 一起发布的。JavaFX 8 包对游戏编程很重要,因为它包含了你想用于游戏编程的高级形式的类,包括使用场景图组织游戏组件的类;用户界面布局和设计的类;2D 数字插图类(也称为矢量图形);以及用于数字成像(也称为光栅图形)的类;2D 动画;数字视频;数字音频;3D;网络引擎(WebKit);还有更多,所有这些我都将在本章中介绍,这样您就可以确切地知道您可以得到什么,现在这些 JavaFX 8 库已经被添加到 Java 8 编程语言中了。

深入研究这些细节的理由不仅是为了让您知道 JavaFX 8.0 可以为您的 Java 8 游戏开发做些什么,而且是为了让您对这个 JavaFX 多媒体引擎的各个组件是如何组合在一起的有一个总体的了解。您将了解 JavaFX Quantum Toolkit、Prism 渲染技术、WebKit web 引擎、Glass Windowing Toolkit、音频和视频媒体引擎以及场景图形 API。

在您真正开始在游戏中使用 JavaFX 之前,您需要这份 Java FX 工作方式的高级概述,因为它是一组相当复杂的 API(我喜欢称之为引擎)。这是因为它为在 Java 8 应用(在这里是游戏)中实现用户界面(UI)和用户体验(UX)带来了强大的力量。因此,在这些基础章节中,请耐心听我讲述如何掌握您的 IDE (NetBeans 8.0)、编程语言(Java 8)以及这种新的媒体引擎(JavaFX 8),它现在是 Java 8 编程平台的一部分,在国际上的影响力和受欢迎程度都在快速增长。

一旦您研究了 JavaFX 8.0 是如何在最高级别上组合在一起的(就像您在第三章中所做的那样),您将考虑一些您可能用来构建 Java 8 游戏的关键类,例如 Node 类以及 Stage、Scene、Group、StackPane、Animation、Layout、Shape、Geometry、Control、Media、Image、Camera、Effect、Canvas 和 Paint 类。您已经学习了 JavaFX 应用类(参见第二章和第三章),所以现在您将重点关注可用于构建复杂多媒体项目的类,如 Java 8 游戏。

最后,您将深入了解您在第二章中生成的引导 JavaFX 应用,以及 Java。main()方法和 JavaFX。start()方法使用 Stage()构造函数方法创建 primaryStage Stage 对象,并在其中使用 scene()构造函数方法创建名为 Scene 的场景对象。您将探索如何使用 Stage 类中的方法来设置场景和标题并显示舞台,以及如何创建和使用 StackPane 和 Button 类(对象),并向按钮添加 EventHandler。

JavaFX 概述:从场景图到操作系统

和上一章一样,在 Java 8 上,我从最高级别的 JavaFX 开始这个概述,使用场景图形 API 和可视化编辑工具,它们包含在一个名为 Scene Builder 的 JavaFX 应用中,我们不会使用它(Scene Builder 是用于应用 UI 设计而不是游戏设计的);我们将使用 GIMP 来代替。正如您在第一章中所观察到的那样(参见图 1-5),Scene Builder 集成到了 NetBeans 8.0 中(JavaFX 被列为专门支持在 NetBeans 中使用,主要是因为 Scene Builder 已经成为 NetBeans GUI 的一个组成部分)。

如图 4-1 所示,这些 JavaFX 应用构建工具存在于 JavaFX 8 API(Java FX 包的集合,如 javafx.scene 和 javafx.application)的“顶部”,这最终允许您构建(使用场景图)和 UI 设计(使用场景生成器)您的 Java FX 新媒体作品(在本例中,是 Java 8 游戏)。请注意,JavaFX 8.0 API 不仅连接(这里使用钢轴承来表示插头)到它上面的场景图形和场景生成器,还连接到它下面的 Java JDK 8 和 Quantum 工具包。正如您所看到的,Java JDK 8(和 API)然后将 JavaFX 新媒体引擎连接到 NetBeans、JVM 和 Java 当前支持的各种平台以及未来的平台,如 Android 4 (32 位 Android)、Android 5 (64 位 Android)和 iOS。

A978-1-4842-0415-3_4_Fig1_HTML.jpg

图 4-1。

How JavaFX 8 is stratified, from the Scene Graph at the top down through Java 8, NetBeans 8.0, JVMs, and OSs

Quantum Toolkit 连接到 JavaFX 8.0 API,将您将要学习的所有强大的新媒体引擎联系在一起。Quantum Toolkit 还处理所有这些的线程管理,因此您的游戏代码和游戏的新媒体(音频、视频、3D 等)可以在双核、四核、六核和八核 CPU 上使用单独的处理器,这些处理器在当今的计算机和消费电子设备中非常常见。

Glass Windowing Toolkit 控制 JavaFX 8.0 的窗口管理,并负责显示屏上的所有离散区域,如舞台和弹出窗口,包括对话框。Glass 还管理事件处理队列,将事件传递给 JavaFX 进行处理,并设置计时器。

如图所示,还有一个 WebKit 引擎和一个媒体(播放器)引擎,由 Quantum Toolkit 管理。WebKit 引擎呈现您的 HTML5 和 CSS3 web 内容,媒体播放器媒体播放引擎分别播放您的数字音频和数字视频资产。

Quantum Toolkit 下面最重要的新媒体引擎是 Prism(游戏)引擎,它使用 Java 2D 渲染 2D 内容,如果您的用户使用 Windows Vista、Windows 7 或 Windows 8.1 平台,则使用 OpenGL (Mac、Linux、嵌入式操作系统)或 DirectX 渲染 3D 内容。Windows XP 支持于 2014 年 4 月停止,因为现在大多数计算机和消费电子设备都支持 64 位(XP 仅为 32 位)。

Prism 桥接了主要操作系统平台和消费电子(嵌入式)设备上的强大 3D 游戏引擎(DirectX、OpenGL ),因此 JavaFX 8.0 可以将复杂的渲染任务处理从 NVIDIA (GeForce)、AMD (ATI 镭龙)和英特尔卸载到图形处理单元(GPU)硬件。这使得 JavaFX(以及 Java 8)游戏速度更快,并允许游戏使用更少的 CPU 处理能力来将游戏资产渲染到屏幕上。这反过来允许更多的 CPU 处理能力用于游戏逻辑,如人工智能和碰撞检测。在本书的第四章中,当你掌握了 JavaFX 引擎之后,你将会学到游戏设计的这些领域。

值得注意的是,游戏开发人员不需要了解 Quantum(线程)、Glass(窗口)或 Prism(渲染)引擎的内部工作原理,就可以利用它们惊人的强大功能。在整本书中,您将关注图的顶层(场景图和场景生成器)以及 JavaFX 和 Java 8 API 层。我还将介绍 NetBeans IDE 8.0 级别,您将在第二章中了解到这一级别,但您也将在本书的剩余部分进一步探索这一级别。

至于图中的较低层,NetBeans 8.0 将生成一个 Java 字节码文件,供每个操作系统平台的定制 JVM 读取。图底部所示的 JVM 可以通过下载 Java 8 JRE 安装在任何给定的操作系统平台上,你已经在第一章中遇到过,当你把它作为 Java JDK 8 的一部分安装时。

这个 JVM 层允许你的游戏作为一个应用安装在所有流行的操作系统平台和嵌入式设备上,这些设备也支持 JavaFX 8。此外,您可以将您的 Java 8 游戏生成为一个 Java applet,它可以嵌入到一个网站中,甚至还有一个部署模型,在该模型中,应用可以从网站拖到您的桌面上,在那里它被安装为一个完整的 Java 8 应用。

此外,已经有一种方法可以在 iOS 8 以及 Android 4.4 和 5.0 上运行 JavaFX 8 应用。如果你对这方面的最新信息感兴趣,只需谷歌“Android 上的 JavaFX”或“iOS 上的 Java FX”;你可以打赌,到 2015 年,Android 5.0 和 Chrome OS 设备将“本机”运行 JavaFX 应用,这意味着有一天(很快)你将能够使用 IntelliJ 直接将 Java(和 JavaFX 引擎)应用导出到 Android 5.0,或使用 NetBeans 8.0 导出到 Chrome OS。有了这个 Java 8 和 JavaFX 8.0 动态组合,您最终将能够“一次编码,随处运行”!Oracle 最近发布了 Java 8 SE Embedded、Java 8 ME 和 Java 8 ME Embedded 版本,都支持 JavaFX。

Note

JetBrains IntelliJ IDEA 现在是用于创建 64 位 Android 5.0 应用的官方 IDE。这个 IDE 在我的 Android Apps for Absolute 初学者,第三版(Apress,2014)中进行了讨论,其中包括使用 Eclipse IDE 和 Java 6 开发 32 位 Android 4.0 应用,以及使用 IntelliJ IDEA 和 Java 7 开发 64 位 Android 5.0 应用。

让我们从图的顶部开始,看一下 JavaFX 场景图和 javafx.scene 包,它在 JavaFX API 中实现了场景图(下一章将介绍 Scene Builder)。

JavaFX 场景包:16 个核心 Java 8 类

在我们的高级概述之后,我想做的第一件事是介绍最重要的 JavaFX 包之一,javafx.scene 包。在第二章和第三章中,你会发现不止一个 JavaFX 包。正如你在第三章(见图 3-1)中看到的,InvinciBagel 游戏应用使用了四个不同的 JavaFX 包。javafx.scene 包包含 16 个强大的 Java 8 类(记住,javafx 是在 Java 8 中重新编码的),包括 Camera、ParallelCamera 和 PerspectiveCamera、Cursor 和 ImageCursor、LightBase、PointLight 和 AmbientLight 类;场景图类(节点、父、组和子场景);以及一些实用程序类(见图 4-2 )。

A978-1-4842-0415-3_4_Fig2_HTML.jpg

图 4-2。

The javafx.scene package and its 16-core Scene Graph, Scene utility, Lighting, Camera, and Cursor classes

我对这 16 个 javafx.scene 包类进行了逻辑分组。Scene 类位于图的 Scene Graph 部分,因为使用 Scene 类创建的 Scene 对象包含 Scene Graph 对象,这些对象是使用这四个与 Scene Graph 相关的类(Node、Parent、Group、SubScene)及其子类创建的。我将在本章后面详细介绍场景图类(参见“JavaFX 场景图:组织场景,使用父节点”一节)

JavaFX 中的场景图架构类从最高级别开始,有一个节点超类及其父类,包括 Group 和 SubScene 类,它们是父类的子类。这些核心类用于创建 JavaFX 场景图形层次结构,以及组织和分组使用 JavaFX 包中的其他 JavaFX 类创建的对象。

有三个我称之为场景实用程序的类,它们允许你在任何时候对你的场景或它的任何场景图节点进行快照(像截图一样),如果你在一个场景中使用 3D 基本体的话,还可以打开和关闭场景初始化。javafx.scene 包中的另一半(八个)类用于场景照明、场景摄影机和场景的光标控制。我将在本章后面讨论这些类(参见“JavaFx 场景内容:灯光、摄像机、光标、动作!”),在您看了场景图类之后,这些类创建、分组、管理和操作 JavaFX 场景内容。因此,我将按照您最有可能使用 javafx.scene 包的顺序,从图的左侧到右侧,从最少到最多,介绍图中所示的 Java FX . scene 包类。

JavaFX 场景类:场景大小和颜色以及场景图形节点

javafx.scene 包中的两个主要类是 scene 类和 Node 类。我将在下一节介绍 Node 类及其父类、组类和子场景子类,因为这些类及其子类(如 InvinciBagel 类中使用的 StackPane 类)用于实现 JavaFX 中的场景图架构。此外,在某种意义上(在我的图中), Node 类及其子类可以被视为在 Scene 类之下,尽管 Node 类不是 Scene 类的子类。事实上,节点(场景图)类和子类,或者说使用这些类创建的对象,包含在场景对象本身中。

因此,您将首先考虑如何使用 Scene 类及其 Scene()构造函数方法为 JavaFX 应用创建场景对象。本节将强化你在第三章学到的关于重载构造方法的知识,因为创建一个场景对象需要几种不同的方法。

Scene 类用于创建一个 Scene 对象,使用 Scene()构造函数类,根据您选择使用六个(重载的)构造函数方法中的哪一个,该类使用一个到五个参数。这些方法包括以下构造函数方法,以及它们的六种不同的(因此也是重载的)参数列表数据字段配置:

Scene(``Parent

Scene(``Parent``root,``double``width,``double

Scene(``Parent``root,``double``width,``double``height,``boolean``depthBuffer)

Scene(``Parent``root,``double``width,``double``height,``boolean``depthBuffer,``SceneAntialiasing``aAlias)

Scene(``Parent``root,``double``width,``double``height,``Paint``fill)

Scene(``Parent``root,``Paint

当前在您的引导 Java 和 JavaFX 代码中使用的构造函数是第二个,调用如下:

Scene scene =``new

如果您想给场景添加黑色背景,您可以选择第五个重载的构造函数方法,使用一种颜色。Color 类中的 BLACK 常量(这是一个 Paint 对象,因为 Color 是 Paint 子类)作为填充数据(在本例中为填充颜色)。您可以通过使用以下 Scene()对象构造函数方法调用来实现这一点:

Scene scene =``new``Scene(root, 300, 250,``Color.BLACK

请注意,根对象是一个父子类,称为 StackPane 类,它是使用 StackPane()构造函数方法(在 Scene()构造函数方法调用上方两行)通过使用以下 Java 代码行创建的:

StackPane root =``new

正如您所看到的,任何类都可以在构造函数中使用,只要它是为该构造函数参数位置(数据)声明(必需)的对象(类)类型的子类。您可以在参数列表中使用 Color 和 StackPane 对象,因为它们分别来自 Paint 和 Parent 类。

如果您想知道,布尔 depthBuffer 参数用于 3D 场景组件。因为这些场景组件是 3D 的并且具有深度(除了 2D x 和 y 组件之外,还有一个 z 组件),所以如果要创建 3D 场景或组合 2D 和 3D 场景组件,则需要包含此参数,并将其设置为 true 值。最后,在第四个构造函数方法的参数列表中传递的 SceneAntialiasing 对象(和类)为 3D 场景组件提供实时平滑。

JavaFX 场景图:组织场景,使用父节点

场景图不是 JavaFX 独有的,在很多新媒体内容创作软件包中都可以看到,它是一种类似倒置的树的数据结构,根节点在顶部,分支节点和叶节点从根节点开始。我第一次看到场景图设计方法是在我使用 Realsoft Oy 的名为 Real 3D 的 Amiga 软件包进行 3D 建模的时候。从那时起,这种方法被许多 3D、数字视频和数字图像软件包复制,现在是 JavaFX 组织内容和场景的一部分。出于这个原因,你们中的许多人可能对这种设计范例很熟悉(也很舒服)。

JavaFX 场景图数据结构不仅允许您设计、组织和设计 JavaFX 场景及其内容,而且如果您正确设置了场景图,还可以将不透明度、状态、事件处理器、变换和特殊效果应用于场景图层次的整个逻辑分支。图 4-3 是基本的场景图树,顶部是根节点,下面是分支和叶子节点。

A978-1-4842-0415-3_4_Fig3_HTML.jpg

图 4-3。

JavaFX Scene Graph hierarchy, starting with the root node and progressing to branch and leaf nodes

根节点是最顶端的节点,这就是为什么它被称为根,即使它在顶部,而不是底部,就像植物世界中的根一样。根节点没有父节点,也就是说,在场景图形层次中,它上面没有任何东西。根节点本身是其下的分支节点和叶节点的父节点。

场景图树中第二个最强大(也是最复杂)的结构是分支节点,它使用 javafx.scene.Parent 类作为其超类,并且可以包含子类(这是合乎逻辑的,因为它扩展了一个名为 Parent 的类)。分支节点可以包含其他分支节点以及叶节点,因此它可以用于创建一些非常复杂和强大的场景图层次结构(或场景图架构)。

层次结构中的最后一级是叶节点。叶节点是分支的末端,因此不能有子节点。值得注意的是,叶节点可以直接脱离根节点,如图 4-3 所示。可以使用父类、组类或子场景类(见图 4-2 )或它们的任何子类来创建分支节点,例如 WebView、PopupControl、Region、Pane 或 StackPane 类。

叶节点的示例包括 JavaFX 类(作为对象),这些类可以使用参数(如形状、文本或 ImageView)进行配置,但它们本身是设计或内容组件,并且没有被设计为具有子级(子对象)。

因此,在 JavaFX 场景图层次结构中,叶节点将始终包含一个 JavaFX 类,该类没有从父类子类化(扩展),并且其本身也没有被专门设计为在其中或其下具有子元素(子对象)。

父类的四个子类都可以用作分支节点,包括 Group 类,用于对子(叶节点)对象进行分组,以便可以对它们应用不透明度、变换和效果;Region 类,用于将子(叶节点)对象分组以形成屏幕布局,这也可以使用 CSS 来设置样式;Control 类,可用于创建自定义用户界面元素(JavaFX 中称为控件);以及 WebView 类,用于包含 JavaFX WebEngine 类(该类将 HTML5 和 CSS3 内容呈现到 WebView 中)。

JavaFX 场景内容:灯光,摄像机,光标,动作!

接下来,我们来看看图 4-2 中列在中间一栏的八个类。它们提供了强大的多媒体工具来控制您的应用的光标,并为您的 2D 和 3D JavaFX 应用(在这种情况下,是游戏,但也可以是电子书、iTV 节目或任何其他需要 JavaFX 通过 Java 语言提供的强大新媒体功能的内容)提供定制的灯光特效和定制的相机功能。

图中更一般化的类(Cursor、LightBase、Camera)是父类,更专门化的类(ImageCursor、PointLight、ParallelCamera 等)列在每个类之后,是这些父类的子类。除了 LightBase 类,这似乎是显而易见的!

正如您可能已经猜到的(正确),JavaFX Cursor 类可用于控制在任何给定时间使用的应用光标图形(箭头、手形、闭合的手形、十字准线等)。ImageCursor 子类可用于定义和提供基于图像的自定义光标以及自定义光标图像中的 x 和 y 位置,该位置定义了光标点(也称为光标热点)的位置。

LightBase 类及其 PointLight 和 AmbientLight 子类可用于照亮场景。这些类主要用于 3D 场景,它们需要在游戏运行的任何平台上具有 3D 功能,这在当今并不是一个真正的问题,因为大多数主要的 CPU 制造商也制造(并包括)GPU。此外,值得注意的是,如果渲染游戏的平台上没有 3D 处理模拟,Prism 游戏引擎将使用 3D 处理模拟来模拟 3D 环境(GPU)。此外,如果你设置正确,你可以在你的 2D 游戏中使用光照类,或者在混合 2D 3D 游戏中使用光照。

Camera 类及其 ParallelCamera 和 PerspectiveCamera 子类可用于在 3D 和 2D(以及混合)游戏应用中拍摄或录制场景。其中两个相机类 camera 和 ParallelCamera 不要求在运行 JavaFX 应用(在本例中为游戏)的平台上具备 3D (GPU)功能。

Camera 类的子类提供了两种不同的专用类型的摄像机。ParallelCamera 类可用于渲染场景,无需任何深度透视校正,这在 3D 行业中称为正交投影。这意味着这个类非常适合用于 2D 场景(和 2D 游戏)。

PerspectiveCamera 类有一个更复杂的相机,用于 3D 场景,它将支持 3D 查看量。与 LightBase 类及其子类一样,PerspectiveCamera 类需要在运行应用(或游戏)的硬件平台上具有 3D 功能。

PerspectiveCamera 类有一个 fieldOfView 属性(state 或 variable),可以用来改变它的查看体积,就像一个真正的相机变焦镜头,当你从广角放大它的时候。fieldOfView 属性的默认设置是 30 度的锐角。如果你还记得你高中时的几何,你可以通过向下看相机的 y(垂直)轴来想象这个视野。如你所料,确实有。getFieldOfView()和。setFieldOfView(double)方法调用来控制此摄像机类属性。

接下来,让我们仔细看看场景实用程序类。之后,您将检查一些 javafx.scene 子包,例如 javafx.scene.text、javafx.scene.image、javafx.scene.shape 和 javafx.scene.layout。

JavaFX 场景工具:场景快照和抗锯齿

最后,您应该快速查看一下图 4-2 中右栏所示的三个实用程序类,因为它们可用于提高用户设备屏幕上的场景输出质量(使用抗锯齿),以及为您的用户(用于社交媒体共享)或您的游戏逻辑本身提供屏幕捕捉功能。

我们先来考察一下 SceneAntialiasing 类。抗锯齿是一个数字图像行业术语,指的是一种平滑两种颜色交汇处锯齿状边缘的算法,通常位于对角线上或图像合成的圆形区域中。图像合成是将两个独立的图像分层放置以形成一个图像。有时,这两个(或更多)图像层的图像组件之间的边缘需要平滑。平滑(抗锯齿)是必需的,以便最终的合成看起来是一个无缝的图像,这是艺术家或游戏设计师的意图。有趣的是,您已经使用 StackPane 类(窗格是层)在 InvinciBagel 应用中实现了 JavaFX“层引擎”。“层叠”图像合成方法在游戏和软件中很常见,如 Photoshop 和 GIMP。

SceneAntialiasing 类为 3D 场景提供抗锯齿处理(算法),以便可以在 2D 场景背景上合成 3D 场景,无论这是默认颜色。白色或任何其他颜色值;2D 图像(创建混合的 2D-3D 应用);或者其他任何事。scene aliasing 类允许您将静态 scene aliasing 数据字段设置为 DISABLED(关闭抗锯齿)或 BALANCED(打开抗锯齿)值。平衡选项提供了质量和性能的平衡,这意味着设备硬件的处理能力越强,处理的抗混叠质量就越高。

接下来,让我们探索 SnapshotParameters 类(object),它用于设置(包含)将由 SnapshotResult 类(object)使用的渲染属性参数。这些参数包括要使用的相机对象的类型(平行或透视);depthBuffer(用于 3D)是打开的(对于 3D 为真)还是关闭的(对于 2D 为假);一个画图对象,用于包含图像数据;一个转换对象,用于包含任何转换数据;和 Rectangle2D 对象,用于定义要渲染的视口区域(即快照尺寸)。

您将在本章中看到所有这些 javafx.scene 子包类和概念,并在本书的课程中使用它们中的许多。您将在 Java 8 游戏开发中使用的许多功能都可以在这些 JavaFX 8.0 子包中找到。

SnapshotResult 类(更重要的是,使用该类创建的对象)包含生成的快照图像数据、生成该数据的参数以及生成该数据的场景图中的源节点。因此,该类支持的三种方法应该是显而易见的。getImage()方法将获得快照图像。getSource()方法获取源节点信息,而。getSnapshotParameters()方法将获取 SnapshotParameters 对象内容。

场景子包:其他 13 个场景包

你可能在想,“咻!javafx.scene 包概述中包含了很多内容!,“而且确实核心的 javafx.scene 包里面有很多类,涵盖场景创建;场景图组织;以及场景实用工具,比如灯光、相机、光标、截图(或者我们应该称这些为 sceneshots?).在 javafx.scene 包中,在我所称的子包中,或者在 javafx.scene 包下面的包中,有更多的内容,使用另一个点和另一个包名(描述)来引用。事实上,还有 13 个 javafx.scene 包(见表 4-1 ),涵盖了绘图、绘画、图表、UI 设计、成像、特效、媒体(音频和视频)回放、输入输出、文本、形状(2D 几何)、变换和网页(用 HTML5、JavaScript 和 CSS3 创建的内容)呈现等内容。在本节中,您将探索这些场景包类。

表 4-1。

Thirteen Second-Level JavaFX Scene Subpackages, Their Primary Functions, and a Description of Classes

包名 功能 内容描述
javafx.scene .画布 图画 画布类(和画布对象);对于自定义绘图图面
javafx.scene.chart 制图 图表类:饼图、折线图、XYChart、条形图、面积图、气泡图
javafx .场景.控件 用户界面控件 UI 控件类:按钮、菜单、滑块、标签、滚动条、文本字段
javafx.scene.effect 特技 特殊效果类:发光,混合,开花,阴影,反射,运动模糊
javafx.scene.image 成像 数字图像类:图像、图像视图、可写图像视图、像素格式
javafx.scene.input 输入(事件) 与将用户输入获取到 JavaFX 应用相关的类
javafx.scene.layout UI 布局 UI 布局容器类:TilePane、GridPane、FlowPane、Border
javafx.scene.media 媒体播放机 媒体播放类:MediaPlayer、MediaView、Track、AudioTrack、AudioClip
javafx.scene.paint 绘画 绘画类:绘画、颜色、线性渐变、径向渐变、停止、材质等等
javafx.scene.shape 几何学 2D 和 3D 几何类:网格、形状、Shape3D、圆弧、圆、直线、路径等
javafx.scene.text 文本和字体 文本呈现和字体呈现类:字体、文本、文本流等等
javafx.scene.transform 转换 变换类:变换,缩放,旋转,剪切,平移,仿射
javafx.scene.web WebKit Web 支持类:WebView、WebEvent、WebEngine、HTMLEditor

让我们从类最少的包开始。该表按字母顺序列出了子包,但是第一个子包 javafx.scene.canvas 恰好只包含一个类,canvas 类,顾名思义,它用于创建一个 Canvas 对象,该对象用作画布,您可以用它来创建东西!列出的下一个子包是 Java FX . scene . chart;这本书有图表类,如饼图、折线图、XYChart、条形图、面积图和 BubbleChart,供业务应用使用,这是完全不同的一本书,所以我将不讨论图表。

下一个子包 javafx.scene.control 提供了所有的 UI 控件(Android 中的 widget)类,比如 Button、Menu、CheckBox、RadioButton、DatePicker、ColorPicker、ProgressBar、Slider、Label、Scrollbar、TextField 和其他 80 多个类。因为 javafx.scene.control 中大约有一百个类,所以我甚至不打算在这里介绍它;关于这个子包,可能可以写一整本书!如果您想复习这些类,只需在 Google 或 Oracle Java 网站上参考“javafx.scene.control ”,您就可以连续几天仔细阅读这些类能做什么。对于这个子包,“reference”是关键词,因为当您需要实现一个给定的 UI 元素时,您会希望单独引用这个包及其类。

下一个子包 javafx.scene.effect 提供了所有的特效类,差不多有 24 个。这些对于 Java 8 游戏开发非常有用,因此这是我将在本节中详细介绍的少数几个子包之一。

javafx.scene.image 子包用于在 javafx 中实现数字影像,它包含 image、ImageView、WritableImage、PixelFormat 和 WritablePixelFormat 类。ImageView 类是您通常用来保存数字图像资产的类,如果您想要进行更高级的(算法)基于像素的数字图像创建,则更高级的 PixelFormat 类允许您逐个像素地创建数字图像。

javafx.scene.input 子包包括用于从 javafx 应用的用户获取输入的类。这个输入使用事件处理能力进行处理,你将在本书的课程中详细研究,并且你已经在你的 JavaFX 应用中体验过了,在第三章(见图 3-2,ll。第 22 至 24 页)。

javafx.scene.layout 子包包含用于创建 UI 设计布局以及用于屏幕布局设计的类。这些布局类包括控制和管理背景的类;添加边框并设置其样式;并提供 UI 窗格管理,如 StackPane、TilePane、GridPane、FlowPane 和 AnchorPane。这些 UI 类为 JavaFX 中的 UI 控件提供了自动屏幕布局算法。Background 类(及其子类)提供了屏幕背景实用程序,Border 类(及其子类)提供了屏幕边框实用程序,可用于为 UI 屏幕增加图形设计的趣味。

javafx.scene.media 子包包含用于播放音频或视频媒体的类,包括 media、MediaPlayer 和 MediaView 类。媒体类(实际上是对象)引用并包含媒体(音频或视频)资产,MediaPlayer 播放该资产,MediaView(对于视频)显示该资产。这个子包还有一个 Track 超类和 AudioTrack、VideoTrack、SubtitleTrack 子类,以及 AudioClip、AudioEqualizer 和 EquilizerBand 类,它们提供高级音频(均衡器)控件和短格式音频剪辑,或非常适合在游戏中使用的音频片段。你将在本书的后面使用 AudioClip 类(见第一章 5)。

javafx.scene.paint 子包包含 Stop 类和 paint 超类及其 Color、ImagePattern、LinearGradient 和 RadialGradient 子类,以及 Material 超类及其 PhongMaterial 子类。熟悉 3D 内容制作的人会认识这种 Phong 着色器算法,它允许模拟不同的表面外观(塑料、橡胶等)。Material 和 PhongMaterial 类需要 3D 功能才能在播放硬件上成功运行,就像 SceneAntialiasing、PerspectiveCamera 和 LightBase 类(及其子类)一样。Paint 类创建您的 Paint 对象,Color 类为该对象着色(用一种颜色填充它),LinearGradient 和 RadialGradient 类用颜色渐变填充 Paint 对象,Stop 类允许您定义渐变颜色在渐变中的开始和停止位置。最后,还有一个 ImagePattern 类,它可以用可平铺的图像模式填充 Paint 对象(这对游戏非常有用)。

javafx.scene.shape 子包为 2D 几何图形(通常称为图形)和 3D 几何图形(通常称为网格)提供了类。Mesh 超类及其 TriangleMesh 子类处理 3D 几何,Shape3D 超类及其 Box、Sphere、Cylinder 和 MeshView 子类也是如此。形状超类有更多的子类(11);这些是 2D 几何元素,包括圆弧、圆、三次曲线、椭圆、直线、路径、多边形、折线、四次曲线、矩形和 SVGPath 类。PathElement 超类及其 ArcTo、ClosePath、CubicCurveTo、HLineTo、LineTo、MoveTo、QuadCurveTo 和 VLineTo 子类也提供了路径支持,即定义为开放形状的路径(我喜欢称之为样条曲线,因为我是 3D 建模师),它允许您绘制样条曲线来创建自己的自定义形状!

javafx.scene.text 子包包含将文本形状和字体渲染到场景中的类。这包括 Font 类和 Text 类,前者用于使用您可能想要使用的任何非 JavaFX 系统字体的字体,后者用于创建一个将使用该字体显示文本值的文本节点。还有一个专门的布局容器类,称为 TextFlow,用于流动文本,就像您在文字处理器上看到的那样。

javafx.scene.transform 子包提供了用于渲染 2D 和 3D 空间变换的类,例如 transform 超类的 Scale、Rotate、Shear、Translate 和 Affine (3D rotation)子类。这些可以应用于场景图中的任何节点对象。这允许您的场景图中的任何内容(文本、UI 控件、形状、网格、图像、媒体等等)以您喜欢的任何方式进行转换,这为 JavaFX 游戏开发人员提供了大量的创造力。如果你想知道,平移是整个物体的线性运动;剪切是 2D 平面上两个不同方向的线性运动,或者是 2D 平面的另一部分固定时的一个方向的运动。想象移动一个平面的顶部,而底部保持固定,这样正方形就变成了平行四边形,或者向不同方向移动同一平面(正方形)的顶部和底部。

javafx.scene.web 子包提供了用于将 web 资源渲染到场景中的类,使用了一组类,包括 WebView、WebEvent、WebEngine、WebHistory 和 HTMLEditor。正如您可能想象的那样,WebEngine(请参阅,其他人也称之为引擎)类在 JavaFX 中执行显示 HTML5 + CSS3 + JS 的处理,WebView 类创建用于在场景图中显示 WebEngine 输出的节点。WebHistory 类(最终是对象)保存所访问网页的会话历史(从 WebEngine 实例化到从内存中删除), WebEvent 将 JavaScript web 事件处理与 JavaFX 事件处理联系起来。

既然您已经查看了 javafx.scene 包及其相关子包中大量重要且有用的类(对象),那么让我们来看看 15 个顶级 javafx 包,以便更好地了解 JavaFX 为应用开发提供的关键功能(当然,重点是那些可用于游戏开发的功能)。

其他 JavaFX 包:15 个顶级包

有 15 个顶级包(javafx.packagename 是我认为的顶级包),其中一些包也有子包级别,就像您看到的 javafx.scene 包和子包一样。表 4-2 概述了这些包装并描述了其内容。

表 4-2。

JavaFX Top-Level Packages, Their Primary Functions, and a Description of Their Functional Classes

包名 功能 内容描述
javafx.animation 动画 时间轴,转场,动画计时器,插值器,关键帧,键值
javafx .应用 应用 应用(初始化、启动、停止方法)、预加载程序、参数、平台
javafx.beans JavaFX beans 定义最一般形式的可观察性的 Java 接口
javafx.collections 收集 定义最一般形式的可观察性的 Java 集合
javafx.concurrent 穿线 线程类:任务、服务、计划服务、工作状态事件
javafx.css 半铸钢ˌ钢性铸铁(Cast Semi-Steel) 与在 JavaFX 中实现 CSS 相关的类
javafx.embed 使...嵌入 嵌入废弃的 Java Swing 和 Java AWT GUI 范例
javafx.event 事件处理器 事件处理类:Event、ActionEvent、EventType、WeakEventHandler
javafx.fxml 断续器 断续器
javafx.geometry 3D 几何图形 3D 几何类
javafx.print 印刷 打印类别
javafx.scene 场景控制 与场景创建、组织、控制和实现相关的课程
javafx.stage 舞台创作 舞台创作课
javafx.util JavaFX 实用程序 JavaFX 实用程序类
netscape.javascript Java Script 语言 允许 Java 代码调用 JavaScript 方法并检查 JavaScript 属性

我已经讨论过其中的一些,比如 javafx.application 包(参见第二章和第三章)和 javafx.scene 包(参见“JavaFX Scene 包:十六个强大的 Java 8 类”一节)。这里有几个其他的 JavaFX 包您应该仔细看看,因为它们(和 javafx.scene 包一起)包含了您想要在 Java 8 游戏开发中使用的类(还有其他的,比如 javafx.print、javafx.fxml、javafx.beans 和 javafx.embed 包不太可能在您的 Java 游戏设计和开发工作过程中使用);它们是 javafx.animation、javafx.stage、javafx.geometry、javafx.concurrent 和 javafx.event,接下来让我们深入看看这些包为您的游戏开发目标提供了什么。

游戏的 JavaFX 动画:使用 javafx.animation 类

javafx.animation 包包含 animation 超类,该超类包含 Timeline 和 Transition 子类以及 AnimationTimer、Interpolator、KeyFrame 和 KeyValue 类。动画是 Java 8 游戏中一个重要的设计元素,多亏了 JavaFX,这些动画类已经为我们编码好了,所以你所要做的就是正确地使用它们来为你的游戏添加动画!

JavaFX Animation 类:动画对象的基础

Animation 类(实际上是对象)提供了 JavaFX 中动画的核心功能。Animation 类包含两个(重载的)Animation()构造函数方法;它们是 Animation()和 Animation(double target frame rate),它们将在内存中创建动画对象,该对象将控制动画及其播放特征和生命周期。

动画类包含。play()方法。playFrom(cuePoint)或。playFrom(持续时间)方法和. playFromStart()方法。这些方法用于开始播放动画对象。也有。pause()方法可以暂停动画播放,而. stop()方法可以停止动画播放。那个。jumpTo(持续时间)和。jumpTo(cuePoint)方法用于跳转到动画中预定义的位置。

您可以使用 rate 属性设置动画播放速度(也称为帧速率或每秒帧数[FPS])。cycleCount 属性(变量)允许您指定动画循环的次数,delay 属性允许您指定动画开始前的延迟时间。如果动画正在循环,此延迟属性将指定循环之间的延迟时间,这可以帮助您创建一些逼真的效果。

您可以通过将 cycleCount 属性或特性(变量)设置为不定,然后使用 autoReverse 属性(设置为 false)来指定无缝动画循环,也可以通过为 autoReverse 属性指定 true 值来使用 pong(来回)动画循环。如果不希望动画无限循环,也可以将 cycleCount 设置为一个数值(如果希望动画只播放一次,请使用 1)。

那个。setRate()方法设置动画播放速率属性。setDelay()方法设置 Delay 属性,而。setCycleCount()和。setCycleDuration()方法控制循环特性。也有类似的。get()方法来“获取”这些动画对象变量(特性、属性、参数或特征;但是您更喜欢查看这些数据字段是可以的)。

使用 ActionEvent 对象加载的 onFinished 属性,可以指定动画播放完成时要执行的动作。当动画到达每个循环的结尾时,动作将被执行,正如你可以想象的那样,一些非常强大的东西可以在具有这种特定功能的游戏中被触发。

还有只读变量(属性),您可以随时“轮询”这些变量,以找到每个动画对象的状态、当前时间、当前速率、循环更新和总持续时间。例如,可以使用 currentTime 属性查看动画播放周期中任何时间点的播放头(帧指针)位置。

JavaFX TimeLine 类:用于属性时间轴管理的动画子类

JavaFX Timeline 类是 JavaFX Animation 超类的子类,因此它的继承层次结构从 Java 8 master class Java . lang . object 开始,向下发展到 Timeline 类,如下所示:

> java.lang.Object

> javafx.animation.Animation

> javafx.animation. Timeline

Timeline 对象可用于定义一种特殊的动画对象,该对象由对象类型 WritableValue 的 JavaFX 值(属性)组成。因为所有的 JavaFX 属性都是这种类型,所以这个类可以用来制作 JavaFX 中任何东西的动画,这意味着它的使用只受您的想象力的限制。

如前所述,时间轴动画是使用通过 KeyFrame 类创建的 KeyFrame 对象定义的,key frame 类既创建又管理这些对象。根据时间变量(通过 KeyFrame.time 访问)和要设置动画的属性(使用 KeyFrame 对象的 values 变量(通过 KeyFrame.values 访问)定义),由时间轴对象处理 KeyFrame 对象。

请务必注意,您需要在开始运行时间轴对象之前设置关键帧对象,因为您不能在正在运行的时间轴对象中更改关键帧对象。这是因为一旦启动,它就会被放入系统内存中。如果您想以任何方式更改正在运行的时间轴对象中的关键帧对象,首先,停止时间轴对象;然后,对关键帧进行更改;最后,再次启动时间轴对象。这将使用新值将时间轴对象及其修订的关键帧对象重新加载到内存中。

Interpolator 类根据时间轴方向在时间轴对象中插入这些 KeyFrame.values。插值是根据开始值和结束值创建中间(或补间)帧的过程。如果您想知道方向是如何推断出来的,它保存在 Animation 超类的 rate 和只读 currentRate 属性中(它是 extended Timeline 子类的一部分)。

反转 rate 属性的值(即,使其为负)将反转(切换)回放方向;在读取 currentRate 属性时,同样的原则也适用(负值表示相反的方向)。最后,KeyValue 类(object)用于保存 KeyFrame 对象中的值。

JavaFX 过渡类:过渡效果应用的动画子类

JavaFX Transition 类是 JavaFX Animation 超类的子类,因此它的继承层次结构从 Java 8 master class Java . lang . object 开始,向下发展到 Transition 类,如下所示:

> java.lang.Object

> javafx.animation.Animation

> javafx.animation. Transition

transition 类是一个公共抽象类,因此,它只能用于(子类化或扩展)创建 Transition 子类。事实上,已经为您创建了十个这样的子类,用于创建您自己的过渡特效;这些是 SequentialTransition、FadeTransition、FillTransition、PathTransition、PauseTransition、RotateTransition、ScaleTransition、TranslateTransition、ParallelTransition 和 StrokeTransition 类。作为动画的子类,过渡类包含了动画的所有功能。

您可能最终会直接使用这十个自定义过渡类,因为它们提供了您可能想要使用的不同类型的过渡(渐变、填充、基于路径、基于笔画、旋转、缩放、移动等)。接下来我将继续讨论 AnimationTimer 类,因为在本书中我们将使用这个类作为游戏引擎。

JavaFX AnimationTimer 类:帧处理、纳秒和脉冲

JavaFX AnimationTimer 类不是 JavaFX Animation 超类的子类,因此它的继承层次结构从 Java 8 master class Java . lang . object 开始,如下所示:

> java.lang.Object

> javafx.animation. AnimationTimer

这意味着 AnimationTimer 类是专门为 JavaFX 提供 AnimationTimer 功能的临时代码,它与 Animation(或时间轴或过渡)类或子类没有任何关系。因此,如果您将该类与占用 javafx.animation 包的 Animation、Interpolator、KeyFrame 和 KeyValue 类组合在一起,那么该类的名称可能会有些误导,因为它与这些类没有任何关系!

像 Transition 类一样,AnimationTimer 类也被声明为公共抽象类。因为是抽象类,所以只能用来(子类化或者扩展)创建 AnimationTimer 子类。与 Transition 类不同,它没有为您创建的子类;你必须从头开始创建你自己的 AnimationTimer 子类,我们将在本书的后面创建我们的 GamePlayLoop.java 类。

AnimationTimer 类看似简单,因为它只有一个您必须重写或替换的方法,包含在公共抽象类中。handle()方法。此方法提供了您希望在 JavaFX 引擎的舞台和场景处理周期的每一帧上执行的编程逻辑,它被优化为以 60FPS 播放(这对游戏来说是完美的)。JavaFX 使用脉冲系统,该系统基于新的 Java 8 纳秒时间单位(以前版本的 Java 使用毫秒)。

JavaFX 脉冲同步:场景图形元素的异步处理

JavaFX 脉冲是一种同步(计时)事件,它同步为 JavaFX 应用(游戏)创建的任何给定场景图结构中包含的元素的状态。JavaFX 中的脉冲系统由 Glass Windowing Toolkit 管理。脉冲使用高分辨率(纳秒)定时器,从 Java 8 API 开始,使用 System.nanoTime()方法的 Java 程序员也可以使用这种定时器。

JavaFX 中的脉冲管理系统被限制为 60FPS。这是为了让所有 JavaFX 线程都有“处理空间”来做它们需要做的事情。根据您在应用逻辑中所做的事情,JavaFX 应用将自动生成多达三个线程。一个基本的业务应用可能只使用主要的 JavaFX 线程,但是一个 3D 游戏也会产生 Prism 渲染线程,如果该游戏使用音频或视频,或者两者都使用,它通常会产生一个媒体播放线程。

在您的游戏开发过程中,您将使用音频、2D、3D,可能还有视频,因此您的 JavaFX 游戏应用肯定会是多线程的!正如您将看到的,JavaFX 被设计为能够利用多线程和纳秒计时功能以及 3D 渲染硬件(Prism)支持来创建游戏。

每当场景图中发生变化时,比如 UI 控件定位、CSS 样式定义或动画播放,都会调度一个 脉冲事件,并最终触发它来同步场景图中元素的状态。JavaFX 游戏设计中的诀窍是优化脉冲事件,以便它们专注于游戏运行逻辑(动画、碰撞检测等);因此,您将最小化 脉冲引擎查看的其他更改(UI 控件位置、CSS 样式更改等)。您将通过使用场景图作为固定的设计系统来实现这一点,这意味着您将使用场景图来设计您的游戏结构,但不会使用动态编程逻辑实时操纵场景图上的节点,因为脉冲系统将执行更新。

JavaFX 脉冲系统允许开发人员异步处理事件,就像在纳秒级调度任务的批处理系统,而不是像旧大型机时代的批处理调度程序那样每天一次。接下来,让我们研究如何使用. handle()方法在脉冲中调度代码。

利用 JavaFX 脉冲引擎:扩展 AnimationTimer 类以生成脉冲事件

扩展 AnimationTimer 类是让 JavaFX 脉冲系统为它处理的每个脉冲处理代码的一个好方法。您的实时游戏编程逻辑将放在。handle(long now)方法,可以通过使用另外两个 AnimationTimer 方法随意启动和停止。开始()和。停止()。

那个。开始()和。stop()方法是从 AnimationTimer 超类调用的,尽管这两个方法也可以被覆盖;只是要确保最终在重写代码方法中调用 super.start()和 super.stop()。如果作为内部类添加到当前 JavaFX public void 中。start()方法结构,代码结构可能看起来如下(参见第三章,图 3-2):

public void start(Stage primaryStage) {

Button btn = new Button;

new AnimationTimer() {

@Override

public void``handle(long now)

// Program logic that gets processed on every 脉冲 that JavaFX processes

}

} .start();

}

上面的编程逻辑展示了 AnimationTimer 内部类是如何构造的,以及 Java 点链接是如何工作的。那个。对 AnimationTimer 超类的 start()方法调用被附加到新的 AnimationTimer(){。。。}代码构造,以便将整个 AnimationTimer 创建(使用 new)、声明(使用花括号)和执行(使用. start()方法调用)链接到 AnimationTimer 对象构造。

如果您想为游戏逻辑的核心部分创建一个更复杂的 AnimationTimer 子类,比如碰撞检测,那么让这个游戏逻辑成为它自己的自定义 AnimationTimer 子类将是一个更好的主意(Java 代码设计方法)。

如果您要创建多个 AnimationTimer 子类来进行脉冲事件控制的高速处理,这一点尤其正确。没错,您可以同时运行多个 AnimationTimer 子类(只是不要忘乎所以,使用太多的 animation timer)。您可以使用 extends 关键字实现这一点,使用以下类定义创建您自己的 AnimationTimer 类,称为 GamePlayLoop:

public class GamePlayLoop``extends

@Override

public void``handle(long now)

// Program logic that gets processed on every 脉冲 that JavaFX processes

}

@Override

public void``start()

super.start();

}

@Override

public void``stop()

super.stop();

}

}

接下来,让我们研究一下 JavaFX Stage 类(object),它被传递到您的 InvinciBagel 中。start()方法!

JavaFX 屏幕和窗口控件:使用 javafx.stage 类

javafx.stage 包中包含的类可以被视为 javafx 应用(在本例中是一个游戏)显示的顶级类。这个显示在最终游戏的顶部,因为它向应用的最终用户显示了游戏的场景。Stage 对象内部是场景对象,这些对象内部是场景图形节点对象,它们包含组成应用的元素。

相比之下,从操作系统的角度来看,这个包中的类可以被认为是相当低级的;它们是 Stage、Screen、Window、WindowEvent、PopupWindow、Popup、DirectoryChooser 和 FileChooser 类以及 FileChooser。ExtensionFilter 嵌套类。这些类可用于连接设备的显示硬件、操作系统软件的窗口管理、文件管理和目录(文件夹)管理功能。

要获得运行 JavaFX 应用的设备所使用的显示硬件的描述,您需要使用 Screen 类。该类支持多屏幕(通常称为第二屏幕)方案,使用。getScreens()方法,该方法可以访问 ObservableList 对象,该对象将包含一个包含所有当前可用屏幕的列表(数组)。使用访问主屏幕。getPrimary()方法调用。您可以通过使用. getDpi()方法调用来获取主屏幕硬件的物理分辨率。也有。getBounds()和。getVisualBounds()方法调用可用的分辨率。

JavaFX 最终用户可以使用 Window 超类及其 Stage 和 PopupWindow 子类与您的应用进行交互。正如你在第三章中看到的(见图 3-2),这是使用名为 primaryStage 的 Stage 对象完成的,它被传递到你的。start()方法,或者使用 PopupWindow(对话框、工具提示、上下文菜单、通知等)子类,如 Popup 或 PopupControl 对象。

您可以使用 Stage 类在 JavaFX 应用编程逻辑中创建辅助阶段。主 Stage 对象总是由 JavaFX 平台使用公共 void start(Stage primaryStage)方法调用来构造,正如您已经在 NetBeans 创建的 bootstrap JavaFX 应用的第二章和第三章中看到的那样。

所有 JavaFX Stage 对象都必须使用构造,并在主 JavaFX 应用线程中修改,这一点我在上一节中已经讨论过了。因为阶段等同于它所运行的操作系统平台上的窗口,所以某些属性或特性是只读的,因为它们需要在操作系统级别被控制;这些是布尔属性(变量):alwaysOnTop、全屏、图标化和最大化。

所有 Stage 对象都有一个 StageStyle 属性和一个 Modality 属性,可以使用常量进行设置。stageStyle 常量是 StageStyle。装饰,舞台风格。无装饰,舞台风格。TRANSPARENT 和 StageStyle.UTILITY。通道常量是通道。无,模态。应用模态和模态。窗口 _ 模态。在下一节中,我将向您展示如何使用 StageStyle 属性和透明常量来做一些真正令人印象深刻的事情,这将使您的 JavaFX 应用在市场中脱颖而出。

Popup 类可用于从头开始创建自定义弹出通知,甚至自定义游戏组件。或者,您可以使用 PopupControl 类及其 ContextMenu 和 Tooltip 子类来提供这些预定义的(编码的)JavaFX UI 控件。

DirectoryChooser 和 FileChooser 类支持将标准 OS 文件选择和目录导航对话框传递到 JavaFX 应用中。文件选择器。ExtensionFilter 嵌套类提供了一个实用程序,用于根据文件类型(文件扩展名)过滤将出现在 FileChooser 对话框中的文件。

接下来,让我们将您当前的 InvinciBagel Stage 对象提升到下一个级别,并使其成为一个无窗口(浮动)应用。这是 JavaFX 令人印象深刻的特性之一,是 Flash 或其他游戏引擎无法比拟的。

使用 JavaFX 主 Stage 对象:创建浮动的无窗口应用

让我们将 InvinciBagel 应用的主阶段设置为透明的,这样按钮 UI 控件就可以悬浮在操作系统桌面上。这是 JavaFX 可以做到的,但你并不经常看到,它允许你创建浮动在操作系统桌面之上的 3D 应用(对于 3D 虚拟对象,这被称为无窗口 ActiveX 控件)。

这是通过使用 StageStyle 实现的。透明常数,与。initStyle()方法,来自 Stage 类。如图 4-4 所示,我还使用了我在第三章中告诉过你的技术(这种技术不遵循正确的 Java 编码惯例,为你计划使用的类声明一个导入语句)。在代码的第 35 行,我通过在 primary stage . init style(stage style style)方法调用中使用完全限定的类名(package . subpackage . class . constant)、Java FX . stage . stage style . transparent 来引用常量。这是通过下面一行 Java 代码完成的:

primaryStage.initStyle(javafx.stage.StageStyle.``TRANSPARENT

A978-1-4842-0415-3_4_Fig4_HTML.jpg

图 4-4。

Call an .initStyle() method with the StageStyle.TRANSPARENT constant, off the primaryStage Stage object

正如您所看到的,我在 NetBeans IDE 8.0 的代码编辑区域中单击了 primaryStage Stage 对象,它会显示(跟踪)该对象在代码中的用法。Stage 对象是使用。setTitle(),。initStyle(),。setScene()和。show()方法调用。

我要离开了。在代码中调用 setTitle()方法,但是要记住,一旦你让这种无窗口的应用处理工作起来,标题栏就是窗口的“chrome”或 UI 元素的一部分,当这些元素(包括标题栏)消失时,标题的这种设置就没有意义了。

如果您一直在担心内存优化,那么在应用开发工作过程的这一点上,您应该删除。setTitle()方法调用,因为标题不会使用 StageStyle 显示。StageStyle 属性的透明常数。

接下来,使用运行图标(或运行菜单),运行应用。如图 4-5 所示,你试图实现的并没有成功:窗口的铬元素不见了,透明度值也不明显。

A978-1-4842-0415-3_4_Fig5_HTML.jpg

图 4-5。

Run the project to see if the Stage object is transparent; clearly, something is set to White

在处理管道中一定有其他东西还没有使用透明度值定义它的背景。透明度使用十六进制值#00000000 来定义,这表示所有 aarggbb(alpha 通道,红色,绿色,蓝色)颜色和不透明度值都已关闭。您需要开始将应用的 JavaFX 组件视为层(目前,它们是 stage-scene-stackPane-button)。随着这本书的进展,你将学习数字成像概念,如色深、alpha 通道、图层、混合,以及所有与处理 2D 平面中的像素相关的技术信息。

您应该尝试设置的下一个透明值是 JavaFX 场景图层次结构中从舞台向下的下一级,它包含场景图本身。如前所述,下一个最顶层的组件是场景对象,它也有一个背景颜色值参数或属性。

像 Stage 类(object)一样,Scene 类(object)也没有透明的样式常量,因此您必须使用不同的方法和常量,以不同的方式将 Scene 对象的背景设置为透明值。您应该知道的一件事是,JavaFX 中所有将自身写入屏幕的东西都会以某种方式支持透明性,以允许 JavaFX 应用中的多层合成。

如果您阅读场景类文档,您会注意到有一个方法。setFill(颜色值),它接受一个颜色(类或对象)值,所以让我们接下来试试。如图 4-6 所示,我调用了。setFill()关闭名为 Scene 的场景对象,使用一个scene.setFill(Color.TRANSPARENT);语句,这是 NetBeans 帮我构造的!

A978-1-4842-0415-3_4_Fig6_HTML.jpg

图 4-6。

Call the .setFill() method with the Color.TRANSPARENT constant, off the Scene object named scene

再次运行应用,查看透明度是否显示出来。如图 4-7 所示,并不是!

A978-1-4842-0415-3_4_Fig7_HTML.jpg

图 4-7。

Run the project to look at the transparent Stage object; something is still set to White

因为您使用 StackPane 对象来实现 InvinciBagel 应用中的层,所以这是您需要尝试设置透明度值的下一个级别。显然,JavaFX 使用了一种颜色。其所有对象的白色默认背景色值。如果我是 JavaFX 设计团队的一员,我会主张将它改为彩色。透明常数,但是,当然,这可能会混淆新用户,因为阿尔法通道和合成层是先进的概念。

javafx.scene.layout.StackPane 类是 javafx.scene.layout.Region 类的子类,该类具有用于设置背景(类或对象)值的. setBackground()方法。同样,透明值常量必须可用,因为您总是需要将背景值设置为透明的,尤其是对于 Java 8 游戏设计。

有趣的是,在 Java 编程中,事情并不总是像您希望的那样简单和一致,因为,为了获得完全相同的最终结果(为设计元素安装透明的背景色/图像板),到目前为止,您已经使用了三个不同的方法调用,传递了三个自定义对象类型:。initStyle(StageStyle 对象)。setFill(颜色对象),以及。setBackground(背景对象)。

这一次,您将调用。setBackground(背景值)方法,还有另一个背景类(对象)常量,为空。如图 4-8 所示,一旦使用以下 Java 语句调用名为 root 的 StackPane 对象的方法,NetBeans 将帮助您找到该常量:root.setBackground(Background。空);。NetBeans 提供了一个方法选择器下拉框,一旦您选择了一个方法,就会出现一个信息对话框,显示该方法的来源(超类)以及它的作用和深入的描述。在这种情况下,空(无)、零颜色填充或零图像集等同于透明。现在,您已经准备好通过使用 run project 工作流程来测试您的无窗口(透明)应用版本了。

A978-1-4842-0415-3_4_Fig8_HTML.jpg

图 4-8。

Call a .setBackground() method with a Background.EMPTY constant, off the StackPane object named root

正如你在图 4-9 中看到的,你现在已经实现了你的目标,在桌面上只有按钮对象是可见的。我将 NetBeans IDE 8.0 的顶部向下拉了一点,这样您就可以看到它的效果有多好,并且仍然可以看到您为实现这一最终结果而添加的三行 Java 代码。使用 2D、3D 和 alpha 通道,可以通过这种 StageStyle 创建一些非常酷的应用。透明功能,所以我想我应该在本书的早期向您展示它,并在 JavaFX 概述章节中获得一些 JavaFX 应用 Java 编码经验。

A978-1-4842-0415-3_4_Fig9_HTML.jpg

图 4-9。

Windowless JavaFX application seen at the top; completed Java and JavaFX code that achieves it seen in the IDE

既然您已经研究了 javafx.stage 包,接下来让我们研究 javafx.geometry 包!

JavaFX 边界和尺寸:使用 javafx.geometry 类

尽管术语“几何体”在技术上适用于 2D 和 3D 资源,但是它们包含在 javafx.scene.shape 包中,我在前面已经介绍过了(参见“场景子包:13 个其他场景包”一节)。javafx.geometry 包更像是一个实用程序包,包含从头开始构建 2D 和 3D 构造的基础类。因此,该包提供了 Bounds 超类及其 BoundingBox 子类等类,以及 Insets、Point2D、Point3D、Dimension2D 和 Rectangle2D 几何内容创建实用程序类。

javafx.geometry 包中的所有类,除了 BoundingBox 类,都是直接从 java.lang.Object master 类扩展而来的,这意味着它们都是从头开始开发(编码)的,用于提供点(也称为顶点)、矩形、尺寸、边界和 insets(内部边界)作为 javafx 应用开发的几何实用程序。

Point2D 和 Point3D 类(最终是对象)保存 2D 平面上 2D 点的 x,y 坐标或 3D 空间中 3D 点的 x,y,z 坐标。这些点对象将用于构建由点集合组成的更复杂的 2D 或 3D 结构,如 2D 路径或 3D 网格。Point2D 和 Point3D 构造函数方法调用没有重载,它们分别使用以下标准格式:

Point2D(double``X,``double)

Point3D(double X, double Y, double Z

Rectangle2D 类(object)可用于定义一个矩形 2D 区域,通常称为平面,正如您所想象的,它在图形编程中有许多用途。Rectangle2D 对象在指定矩形的左上角有一个起点,使用一个x和一个y坐标位置以及一个尺寸(宽乘高)。Rectangle2D 对象的构造函数方法具有以下标准格式,并且未被重载:

Rectangle2D(double``minX``, double``minY``, double``width``, double``height``)

此外,Dimension2D 类(object)仅指定宽度和高度尺寸,并不使用x, y位置在屏幕上放置尺寸(这会使其成为矩形)。该类的构造函数方法如下:

Dimension2D(double``width``, double``height

Insets 类(object)类似于 Dimension2D 类,因为它不提供插入的位置值,但根据顶部、底部、左侧和右侧的偏移距离提供矩形插入区域的偏移。Insets 方法实际上是重载的,因此可以使用以下代码指定等距插入或自定义插入:

Insets(double``topRightBottomLeft

Insets(double``top``, double``right``, double``bottom``, double``left``)

Bounds 类是一个公共抽象类,永远不会是一个对象,而是一个创建节点边界类的蓝图,比如它的 BoundingBox 子类。Bounds 超类也允许一个负值,用来表示一个边界区域是空的(可以认为它是空的,或者未使用的)。BoundingBox 类使用以下(重载)构造函数方法创建 2D(第一个构造函数)或 3D(第二个构造函数)BoundingBox 对象:

BoundingBox(double``minX``, double``minY``, double``width``, double``height``)

BoundingBox(double``minX``, double``minY``, double``minZ``, double``width``, double``height``, double``depth``)

接下来,让我们看看 JavaFX 中的事件和动作事件处理,因为这将为您的游戏添加交互性。

游戏的 JavaFX 输入控件:使用 javafx.event 类

因为游戏本质上是交互式的,所以接下来让我们看看 javafx.event 包;它为我们提供了 Eventsuperclass 及其 ActionEvent 子类,用于处理动作事件,例如 UI 元素的使用和动画关键帧的处理。因为您将在 JavaFX 游戏(应用)中使用 ActionEvent,所以我在这里描述它的类继承层次,因为这也将向您展示 Event 类的起源:

Java.lang.Object

> java.util.EventObject

> javafx.event.Event

> javafx.event. ActionEvent

您的 InvinciBagel 游戏应用已经在使用这个 ActionEvent 类(object ),带有 EventHandler 接口和它的。handle()方法,由您自己编写代码来告诉您的应用在事件发生(触发)后如何“处理”事件(ActionEvent)。那个。handle()方法捕获这个触发的事件,然后根据。handle()方法。

Java 接口是一个提供空方法的类,这些方法被声明供使用,但还不包含任何 Java 编程结构。未实现的方法在使用时必须由 Java 程序员来实现。这个 Java 接口只定义了要实现的方法(在本例中,是处理 ActionEvent 的方法,以便以某种方式处理事件)。需要注意的是,Java 接口定义了需要编码的方法,但并没有为您编写方法代码,因此它是一个路线图,说明了您必须做些什么来完成现有的编程结构(在本例中,是用于处理 ActionEvent 对象的 Java 编程结构,即触发的操作事件)。

现在,让我们看看 JavaFX 中的多线程,这是高级游戏的另一个重要概念,以此来结束对 JavaFX API 和包层次结构中与 2D 和 3D(游戏)相关的一切的探索。

游戏的 JavaFX 线程控制:javafx.concurrent 包

游戏需要后台或异步处理。这可以使用除 JavaFX 应用线程、Prism 渲染线程和媒体播放线程之外的其他线程来完成,这些线程都是根据您在场景图形中使用的类(对象)自动创建的。您的应用编程逻辑可以生成自己的工作线程进行处理,这样您就不会使主 JavaFX 应用线程过载。接下来让我们看一下 javafx.concurrent 包,因为它为我们提供了 Service 超类及其 ScheduledService 子类,用于创建 Worker 对象,还提供了 Task 类,用于一次性任务处理。

因为您将在 JavaFX 游戏(应用)中使用 Service 和 ScheduledService,所以我在这里演示 ScheduledService 类继承层次结构,因为这也将向您展示服务类的 java.lang.Object 起源:

Java.lang.Object

> javafx.concurrent.Service

> javafx.concurrent. ScheduledService

而任务(类)对象只使用一次,为了完成一个给定的任务,一个service正在进行,一个服务对象和一个调度服务(类)对象可以被重用,也就是说,它们随时准备执行它们的service。这更适合于玩游戏的处理,因为玩游戏会持续很长一段时间,这里的假设是,在玩游戏期间,随着时间的推移,也需要计算所涉及的游戏逻辑处理的类型,而不是像任务类(对象)那样,只计算一次。

Worker Java 构造实际上是一个接口,Task、Service 和 ScheduledService 类都是基于这个 Worker 接口为您创建的(这比EventHandler接口更好,您必须自己实现它!).

Worker 对象使用后台线程执行处理,可以是可重用的(如在 Service 类中),也可以是不可重用的(如在 Task 类中)。工作线程状态由工作线程控制。状态类(对象)并包含工作线程的生命周期阶段。这些应用于 javafx.concurrent 包中的三个主要类,因为它们实现了 Worker 接口及其相关的嵌套类。如前一章所述,嵌套类是通过点符号访问的,因此 State 类嵌套在Worker interface (class中。因为在你使用一个工作线程之前,理解它的状态是非常重要的,所以我将以表格的形式详细描述它们,这样你就一目了然了(见表 4-3 )。

表 4-3。

Worker Thread Life Cycle States, as Defined by the Worker.State Nested Class for Use with a Worker Interface

工人。状态常数 意义
准备好的 工作对象(线程)已经初始化(或重新初始化)并准备好使用。
预定的 工作对象(线程)已计划执行,但当前没有运行。
运转 工作对象(线程)当前正在运行,并且正在执行 Java 编程逻辑。
成功 工作对象(线程)已成功执行,有效结果在 value 属性中。
不成功的 由于某些意外情况,工作对象(线程)未能成功执行。
取消 已通过调用 Worker.cancel()方法取消了工作对象(线程)。

正如本 JavaFX 8 多媒体引擎概述章节中的其他内容一样,在应用这些 JavaFX 编程结构和概念时,您将在本书的过程中深入了解如何使用这些包、类、嵌套类、接口、方法、常量和变量的细节!

摘要

在第四章中,您仔细查看了 JavaFX 8 API 中一些更重要的包、概念、组件、类、构造函数、常量和变量(属性), Java FX 8 API 是一个令人印象深刻的 36 个 Java FX . package name . sub pagename 包的集合,我在表格中列出了这些包,并根据多媒体 2D 和 3D(以及混合 2D-3D)游戏开发的需要逐一进行了介绍。我说的“概述”就是概述!

当然,我不能在一章中讨论 JavaFX 中的每个功能类,所以我首先概述了 JavaFX 引擎,以及它如何与上面的 JavaFX Scene Builder 工具和 JavaFX Scene Graph API集成,以及如何与下面的 Java 8 API、NetBeans 8.0 和目标操作系统集成,这为 JavaFX 提供了跨众多流行平台和设备以及主流 web 浏览器的扩展操作系统支持。

我展示了 JavaFX 的高级技术视图,详细介绍了它的结构,包括 JavaFX 场景图、API、Quantum、Prism、Glass、WebKit 和媒体引擎。您了解了这些多线程、渲染、窗口、媒体和 web 引擎如何与 Java 8 API 和 Java JDK 8 以及 NetBeans 8.0 及其生成的 JVM 字节码进行交互,这些字节码由当前运行在十几种不同消费电子设备类型上的各种操作系统平台读取。

您还了解了 JavaFX 的核心概念,如JavaFX Scene Graph和 JavaFX 脉冲 events 系统,您将在阅读本书时利用它们来创建 Java 8 游戏,从下一章开始,我将解释如何在 NetBeans 中使用 JavaFX Scene Builder 可视化编辑工具。

然后,深入研究游戏设计的一些关键 JavaFX 包和子包,比如Application, Scene, Shape, Effect, Layout, Control, Media, Image, Stage, Animation, Geometry, Event, and Concurrent,以及它们的包类和子类,甚至在某些情况下,它们的接口、嵌套类和常量。

您甚至花了一点时间向 InvinciBagel 应用添加了一些代码,将它变成了一个无窗口应用,并学习了如何使用 alpha 通道和十六进制#00000000 或颜色使stage, scene, and stackPane背景板透明。透明,背景。空,和场景样式。透明常数。我不得不在本章中加入一些关于 NetBeans IDE 8.0、Java 8 编程语言和 JavaFX API 的内容!

在下一章中,您将探索 JavaFX Scene Builder,它使得您在本章中学习的场景图结构的构建变得容易。你也将开始构建你的游戏启动画面,因为我知道你渴望开始一个游戏基础设施,即使它只是一个启动画面!

五、游戏设计介绍:概念、多媒体和使用场景生成器

在本章中,您将通过学习在 JavaFX 中使用场景图范例的最佳方式,了解 JavaFX Scene Builder 工具和 FXML,以及为什么(或为什么不)在某些类型的 Java 游戏开发场景中使用这些工具,来建立您对 JavaFX 多媒体引擎的了解。您还将研究基本的游戏设计优化概念、游戏类型以及适用于 Java 平台的游戏引擎,包括物理引擎,如 JBox2D 和 Dyn4J,以及 3D 游戏引擎,如 LWJGL 和 JMonkey。最后,你将考虑新媒体的概念,你将需要了解整合数字图像,数字音频,数字视频和动画到你的游戏制作管道。我们还将看看一些免费的开源多媒体制作工具,它们是你在第一章中安装的,现在可以用来制作 Java 8 游戏。

首先,你将重温静态(固定)与动态(实时)的基本概念,这在第三章(常量与变量)和第四章(脉冲)中有所涉及,也是游戏优化的基本原则之一。这一点很重要,因为您会希望您的游戏能够在用于玩游戏的所有不同平台和设备上流畅运行,即使设备只使用单处理器(这在当今实际上很少见,大多数设备都采用双核(双处理器)或四核(四处理器)CPU)。

接下来,你将学习游戏设计的概念、技术和术语,包括精灵、碰撞检测、物理模拟、背景板、动画、图层、关卡、逻辑和人工智能。你还将研究可以设计的不同类型的游戏,以及它们之间的区别。

然后,您将探索多媒体资产在当今视觉(和听觉)令人印象深刻的游戏中所扮演的角色。您将了解数字成像、数字视频、动画以及数字音频的原理,因为您将在本书的课程中使用许多这些新媒体资产类型,并且需要这些基础知识才能使用它们。

最后,您将深入了解您在第二章中生成的引导 JavaFX 应用代码,以及 Java。main()方法和 JavaFX。start()方法使用 Stage()构造函数方法创建 primaryStage Stage 对象,并在其中使用 scene()构造函数方法创建一个名为 Scene 的场景对象。您将了解如何使用 Stage 类中的方法来设置场景、标题舞台和显示舞台,以及如何创建和使用 StackPane 和 Button 类(对象)以及如何向按钮添加 EventHandler。

高级概念:静态与动态

我想从一个高层次的概念开始,涉及到我将在本章中谈到的一切,从您可以创建的游戏类型,到游戏优化,到 JavaFX 场景生成器和 JavaFX 场景图。不管你是否意识到,在探索 Java 常量的概念时,你已经在第三章回顾了这个概念,Java 常量是固定的或静态的,不会改变,而 Java 变量是动态的,会实时改变。类似地,JavaFX 场景图中的 UI 设计可以是静态的(固定的和不可移动的)或动态的(动画的、可拖动的或可换肤的,这意味着您可以更改 UI 外观以适应您的个人喜好)。

这些概念在游戏设计和开发中非常重要的原因是,您设计用来运行或渲染游戏的游戏引擎必须不断检查其动态部分,以查看它们是否发生了变化并需要响应(更新分数、移动精灵位置、播放动画帧、更改游戏角色的状态、计算碰撞检测、计算物理,等等)。这种对每个帧更新的检查(以及随后的处理)(在 JavaFX 中称为脉冲;参见第四章),以确保你所有的变量、位置、状态、动画、碰撞、物理等都符合你的 Java 游戏引擎逻辑,真的可以加起来,而且,在某些时候,做所有这些工作的处理器可能会过载,这会降低它的速度!

这种增强游戏动态性的所有实时、逐帧检查过载的结果是,游戏运行的帧速率将会降低。没错,像数字视频和动画一样,Java 8 游戏也有帧率,但是 Java 8 游戏帧率是基于你的编程逻辑的效率。你游戏的帧率越低,游戏玩起来就越不流畅,至少对于动态的、实时的游戏来说是这样,比如街机游戏;一款游戏玩起来有多流畅,关系到玩家的用户体验有多流畅。

出于这个原因,静态与动态的概念对于游戏设计的每个方面都非常重要,并且使某些类型的游戏比其他类型的游戏更容易实现出色的用户体验。我将在本章后面讨论不同类型的游戏(参见“游戏类型:谜题、棋盘游戏、街机游戏、混合游戏”一节),但是,正如你可能想象的,棋盘游戏本质上更静态,街机游戏更动态。也就是说,有一些优化方法可以让游戏保持动态,也就是说,看起来好像有很多事情在进行,而从处理的角度来看,实际发生的事情是很容易管理的。这是游戏设计的众多技巧之一,说到底,这是关于优化的。

Android (Java)编程中最重要的静态与动态设计问题之一是使用 XML 的 UI 设计(静态设计)与使用 Java 的 UI 设计(动态设计)。Android 平台允许使用 XML 而不是 Java 来完成 UI 设计,这样非程序员(设计者)可以为应用进行前端设计。JavaFX 允许使用 FXML 做完全相同的事情。要做到这一点,你必须创建一个 FXML JavaFX 应用,正如你在第二章中看到的那样(参见图 2-4,右侧,第三个选项,“JavaFX FXML 应用”)。这样做会将 javafx.fxml 包和类添加到应用中,让您设计 UI,使用 fxml,然后让您的 Java 编程逻辑“膨胀”它们,以便设计由 JavaFX UI 对象组成。

值得注意的是,使用 FXML 会给应用开发和编译过程增加另一层,其中包含 FXML 标记及其翻译和处理。我将在本章后面演示如何做到这一点,以防您的设计团队希望使用 FXML 而不是 Java 进行 UI 设计工作流程(参见“JavaFX Scene Builder:使用 FXML 进行 UI 设计”一节)。我这样做是因为我想涵盖 JavaFX 中的所有设计选项,包括 FXML,以确保这本书完整地涵盖了使用 Java 8 和 JavaFX 8.0 可以做些什么。然而,归根结底,这是一个 Java 8 编程的题目,所以我在本书中的主要焦点将是使用 Java 8,而不是 FXML。

在任何情况下,我关于使用 XML(或 FXML)创建 UI 设计的观点是,这种方法可以被视为静态的,因为设计是预先使用 XML 创建的,并且在编译时使用 Java“膨胀”。Java 膨胀方法使用设计者提供的 FXML 结构来创建场景图,该场景图基于使用 FXML 创建的 UI 设计结构(层次结构)填充有 JavaFX UI(类)对象。我将在本章的后面向您概述这是如何工作的,这样您就可以了解这是如何工作的(参见“JavaFX Scene Builder:使用 FXML 进行 UI 设计”一节)。

游戏优化:平衡静态元素与动态元素

游戏优化归结为平衡不需要实时处理的静态元素和需要持续处理的动态元素。过多的动态处理,尤其是在不需要的时候,会让你的游戏变得不稳定。这就是为什么游戏编程是一种艺术形式:它需要平衡以及伟大的人物,故事线,创造力,幻觉,预期,准确性,最后,优化。

表 5-1 中列出了动态游戏中一些不同的游戏组件优化考虑事项。如您所见,游戏的许多方面都可以进行优化,使处理器的工作负载明显不那么“繁忙”如果这些主要的动态游戏处理区域中有一个因为处理器每帧的宝贵周期而“失控”,这将极大地影响游戏的用户体验。我将在本章的下一节介绍游戏术语(精灵、碰撞检测、物理模拟等等)。

表 5-1。

Aspects of Game Play That Can Be Optimized to Minimize System Memory and Processor Cycle Usage

游戏方面 基本优化原则
精灵位置(移动) 移动精灵尽可能多的像素,以实现屏幕上的平滑移动。
冲突检出 仅在必要时检查屏幕上对象之间的碰撞(非常接近)。
物理模拟 尽量减少场景中需要进行物理计算的对象数量。
精灵动画 最大限度地减少需要循环的帧数,以创建流畅的动画效果。
背景动画 最小化有动画的背景区域,使整个背景看起来有动画,但实际上没有。
游戏逻辑 尽可能高效地编程游戏逻辑(模拟或人工智能)。
记分板更新 仅在得分时更新记分牌,并将得分更新最小化至每秒最多一次。
用户界面设计 使用静态 UI 设计,以便脉冲事件不用于 UI 元素定位或 CSS3。

考虑到所有这些游戏编程领域,使得游戏编程成为一项极其棘手的工作!

值得注意的是,这些方面中的一些共同作用,为玩家创造了一个特定的幻觉。例如,精灵动画将创建角色奔跑、跳跃或飞行的幻觉,但如果不将该代码与精灵定位(运动)代码相结合,将无法实现幻觉的真实性。为了微调一个错觉,动画的速度(帧速率)和移动的距离(每帧像素)可能需要调整(我喜欢称之为微调)以获得最真实的结果。我们将在第一章 3 节中做这件事。

如果你可以移动游戏元素(主要玩家精灵,目标精灵,敌人精灵,背景)更多的像素和更少的次数,你将节省处理周期。花费处理时间的是移动的部分,而不是距离(移动了多少像素)。类似地,对于动画,实现令人信服的动画所需的帧越少,保存这些帧所需的内存就越少。请记住,您正在优化内存使用以及处理周期。检测碰撞是游戏编程逻辑的主要部分;重要的是不要检查游戏元素之间的冲突,这些元素不是“在玩”(在屏幕上),也不是活动的,并且彼此不靠近。

自然力(物理模拟)和游戏逻辑(如果没有很好地编码(优化),是处理器最密集的方面。这些都是我将在本书的后面部分涉及的主题,当你更高级的时候(见第十六章和第十七章)。

游戏设计概念:精灵,物理,碰撞

让我们来看看构建游戏需要了解的各种游戏设计组件,以及可以用来实现游戏的这些方面的 Java 8(或 JavaFX)包和类,我喜欢称之为游戏组件。这些可以包括游戏元素本身(通常称为精灵)以及处理引擎,您可以自己编写代码,或者导入预先存在的 Java 代码库,如物理模拟和碰撞检测。

精灵是游戏的基础,定义你的主角,用来伤害主角的投射物,以及发射这些投射物的敌人。精灵是 2D 图形元素,可以是静态的(固定的,单个图像)或动态的(动画,几个图像的无缝循环)。精灵将基于编程逻辑在屏幕上移动,这决定了游戏如何运行。精灵需要与背景图像和其他游戏元素以及其他精灵合成,因此用于创建精灵的图形需要支持透明背景。

在第四章中,我向你介绍了阿尔法通道和透明度的概念。你需要用你的精灵来达到同样的最终效果,来创造一个无缝的游戏视觉体验。游戏的下一个最重要的方面是碰撞检测,因为如果你的精灵只是在屏幕上从彼此身边飞过,并且当他们相互接触或“相交”时,从来没有做过任何酷的事情,那么你真的不会有一个很好的游戏!一旦你添加了一个碰撞检测引擎(由交叉逻辑处理例程组成),你的游戏就可以确定任何两个精灵何时接触(边缘)或彼此重叠。碰撞检测将调用(触发)其他逻辑处理例程,这些例程将确定当任何两个给定的精灵(如投射物和主要角色)相交时会发生什么。例如,当投射物与主角相交时,伤害点可能会增加,生命力指数可能会降低,或者死亡动画可能会开始。相比之下,如果一件宝物与主角相交(被主角捡到),则可能会产生能量或能力点,生命力指数可能会增加,或者可能会启动“我找到了”的庆祝动画。正如你所看到的,除了精灵(角色、投射物、宝物、敌人、障碍等等)本身,游戏的碰撞检测是游戏的基本设计元素之一,这就是为什么我在本书的早期就介绍了这个概念。

对你的游戏来说,下一个重要的概念是真实世界的物理模拟。像重力这样的东西的加入;摩擦;弹跳;拖;加速度;运动曲线,如 JavaFX 插值器类提供的;和高度精确的碰撞检测的基础上增加了额外的真实感。

最后,添加到游戏中的最专有的属性或逻辑结构(Java 代码)是自定义游戏逻辑,它使您的游戏在市场中真正独一无二。这个逻辑应该保存在它自己的 Java 类或方法中,与物理模拟和碰撞检测代码分开。毕竟,如果您学习了 OOP 概念并将它们应用到您的编程逻辑中,Java 8 会使您的代码模块化结构良好!

当你开始把所有这些游戏组件加在一起时,它们开始让游戏变得更可信,也更专业。一个伟大游戏的关键目标之一是暂停信念,这意味着你的玩家完全相信前提、角色、目标和游戏。这是任何内容制作人,无论是电影制作人、电视制作人、作家、歌曲作者、Java 8 游戏程序员还是应用开发人员,都追求的目标。如今,游戏的创收能力与任何其他内容分发类型一样,如果不是更多的话。

接下来,让我们看看可以创建的不同类型的游戏,以及这些游戏在应用精灵、碰撞检测、物理模拟和游戏逻辑等核心游戏组件方面有何不同。

游戏类型:谜题、棋盘游戏、街机游戏、混合游戏

就像我在这一章中谈到的其他东西一样,游戏本身可以通过使用静态和动态的分类方法来分类。静态游戏不受处理器限制,因为它们本质上倾向于基于回合而不是基于手眼协调,因此,在某种意义上,它们更容易流畅地工作;只有游戏规则的编程逻辑和吸引人的图形需要被放置和调试。对于开发新类型的游戏类型来说,也存在一个重要的机会,这种游戏类型以前所未见的创造性的新方式使用静态和动态游戏玩法的混合组合。我自己正在研究其中的一些!

因为这是一个 Java 8 编程的标题,所以我将从这个角度来处理所有事情,这恰好是将游戏分成不同类别(静态、动态、混合)的一个好方法,所以让我们先讨论静态(固定图形)、回合制游戏。这些游戏包括棋类游戏、益智游戏、知识游戏、记忆游戏和策略游戏,所有这些游戏的受欢迎程度和可销售性都不容小觑。

静态游戏的酷之处在于,它们可以像动态游戏一样有趣,而且处理开销明显更少,因为它们不必达到 60FPS 的实时处理目标,就可以流畅、专业地玩游戏。这是因为游戏的本质根本不是基于移动,而是基于做出正确的战略移动,但只有当轮到你这样做的时候。

在静态游戏中可能涉及一些形式的碰撞检测,关于哪个游戏棋子已经移动到游戏棋盘或游戏面上的给定位置;然而,没有碰撞检测使处理器过载的危险,因为游戏板的其余部分是静态的,除了在特定玩家的回合中被有策略地移动的一个棋子。

策略游戏的处理逻辑更多的是基于策略逻辑的编程,在给定正确的移动顺序的情况下,允许玩家获得最终的胜利,而动态游戏编程逻辑更多地关注游戏精灵之间发生的冲突。动态游戏的重点是分数,躲避弹丸,寻找宝藏,完成水平目标,杀死敌人。

具有大量相关规则的复杂策略游戏,如国际象棋,可能比动态游戏有更多的编程逻辑例程。然而,因为代码的执行对时间不敏感,所以无论平台和 CPU 有多强大,最终的游戏都将是流畅的。当然,游戏规则集逻辑必须完美无缺,这种类型的游戏才能真正做到专业,所以,最终,静态和动态游戏都很难编码,尽管原因不同。

动态游戏可以被称为动作游戏或街机游戏,并且包括显示屏上的大量运动。这些高度动态的游戏几乎总是涉及射击,例如在第一人称射击游戏(例如,Doom,Half-Life)以及第三人称射击游戏(生化危机,侠盗猎车手)类型中,或者偷东西,或者躲避东西。还有就是障碍赛导航范式,常见于平台游戏,比如大金刚,超级马里奥。

值得注意的是,任何类型的游戏都可以使用 2D 或 3D 图形和资源,甚至是 2D 和 3D 资源的组合来制作,正如我在第四章中指出的,这是 JavaFX 允许的。

有这么多流行的游戏类型,总是有机会通过使用静态(策略)游戏类型和动态(动作)游戏类型的混合方法来创建一种全新的游戏类型。

游戏设计资产:新媒体内容概念

让你的游戏变得高度专业并让买家满意的最强大的工具之一是你在第一章中下载并安装的多媒体制作软件。在我继续讲下去之前,我需要花一些时间向您提供关于 Java 8 中支持的四种主要新媒体资产类型的基础知识,使用 JavaFX 8 多媒体引擎:数字图像,用于精灵、背景图像和 2D 动画;向量形状,用于 2D 插图、碰撞检测、3D 对象、路径和曲线;数字音频,用于音效、旁白和背景音乐;数字视频,在游戏中用于动画背景循环(天空中飞翔的鸟,飘动的云,等等)曾经高度优化。如图 5-1 所示,这四个主要流派,或者区域,都是通过 JavaFX 场景图安装的,使用了我在第四章中描述的包。将使用的一些主要类是 ImageView、AudioClip、Media、MediaView、MediaPlayer、Line、Arc、Path、Circle、Rectangle、Box、Sphere、Cylinder、Shape3D、Mesh 和 MeshView。

A978-1-4842-0415-3_5_Fig1_HTML.jpg

图 5-1。

How new media assets are implemented, using Scene Graph through the JavaFX API in Java 8 via NetBeans

因为在 Java 8 游戏设计和编程管道中使用这些新媒体元素之前,您需要有一定的技术基础,所以我将从数字图像和矢量插图开始,介绍这四个新媒体领域的基本概念。

数字成像概念:分辨率、色深、Alpha、图层

JavaFX(因此 Java8)支持大量流行的数字图像文件(数据)格式,这给了游戏设计者极大的灵活性。其中一些已经存在很久了,例如 CompuServe 的图形交换格式(GIF)和联合图像专家组(JPEG)格式。有些 JavaFX 图形文件格式更现代,比如可移植网络图形(PNG 发音为“ping”),这是您将在游戏中使用的文件格式,因为它可以产生最高的质量水平并支持图像合成。Java 支持的所有这些主流数字图像文件格式在 HTML5 浏览器中也受支持,并且因为 Java 应用可以与 HTML 应用和网站一起使用,这确实是一个非常合乎逻辑的协同作用!

最古老的 CompuServe GIF 格式是无损数字图像文件格式。之所以称为无损压缩,是因为它不会丢弃图像数据来获得更好的压缩结果。GIF 压缩算法不像 PNG 格式的压缩算法那样精细(强大), GIF 只支持索引颜色,这是它获得压缩(较小文件大小)的方式。如果您的游戏图像资产已经以 GIF 格式创建,您将能够毫无问题地在 Java 8 游戏应用中使用它们(除了效率较低的图像压缩算法和没有合成功能)。

Java 8 (JavaFX)支持的最流行的数字图像文件格式是 JPEG,它使用“真彩色”色深,而不是索引色深,以及所谓的有损数字图像压缩,其中压缩算法“丢弃”图像数据,以便它可以实现更小的文件大小(图像数据将永远丢失,除非您聪明地保存您的原始图像!).

如果您在压缩后放大 JPEG 图像,您将看到一个变色区域(效果),这在原始图像中是不存在的。图像中的一个或多个退化区域通常被称为压缩伪影。这只会发生在有损图像压缩中,在 JPEG(和运动图像专家组[MPEG])压缩中很常见。

Tip

我建议您在 Java 8 游戏中使用 PNG 数字图像格式。这是一种专业的图像合成格式,你的游戏本质上是一个实时精灵合成引擎,所以你需要使用 PNG32 图像。

PNG 有两个真彩色文件版本:PNG24(不能用于图像合成)和 PNG32(带有用于定义透明度的 alpha 通道)。

我为你的游戏推荐 PNG,因为它有一个不错的图像压缩算法,并且是一种无损图像格式。这意味着 PNG 有很好的图像质量以及合理的数据压缩效率,这将使你的游戏发行文件更小。PNG32 格式的真正力量在于它能够使用透明度和抗锯齿(通过其 alpha 通道)与其他游戏图像合成。

数字图像分辨率和纵横比:定义图像大小和形状

您可能知道,数字图像是由 2D(二维)像素阵列组成的(“像素”代表图片[pix]元素[el])。图像的清晰度由其分辨率表示,分辨率是图像宽度(或 W,有时称为 x 轴)和高度(或 H,有时称为 y 轴)维度中的像素数量。图像的像素越多,分辨率越高。这类似于数码相机的工作原理,因为图像捕捉设备(称为相机 CCD)的像素越多,可以实现的图像质量就越高。

要计算图像像素的总数,请将宽度像素乘以高度像素。例如,一个宽视频图形阵列(VGA) 800 × 480 图像包含 384,000 个像素,正好是八分之三兆字节。这就是你如何找到你的图像的大小,包括使用的千字节(或兆字节)和显示屏上的高度和宽度。

使用图像纵横比来指定数字图像资产的形状。纵横比是数字图像的宽高比,定义了正方形(1:1 纵横比)或矩形(也称为宽屏)数字图像形状。具有 2:1(宽屏)宽高比的显示器,例如 2,160 × 1,080 分辨率,现在已经上市。

1:1 纵横比的显示器(或图像)总是完美的正方形,2:2 或 3:3 纵横比的图像也是如此。例如,你可能会在智能手表上看到这个长宽比。值得注意的是,定义图像或屏幕形状的是这两个宽度和高度(x 和 y)变量之间的比率,而不是实际的数字本身。

纵横比应该始终表示为冒号两边可以达到(减少)的最小数字对。如果你在高中时注意学习最小公分母,那么长宽比对你来说很容易计算。我通常通过继续将冒号的每一边一分为二来计算长宽比。比如你拿 SXGA 1280×1024 分辨率来说,1280×1024 的一半是 640 × 512,640 × 512 的一半是 320×256;320 × 256 的一半是 160 × 128,再一半是 80 × 64,再一半是 40 × 32,再一半是 20×16;20 × 16 的一半是 10 × 8,其中的一半给出了 SXGA 的 5 × 4 纵横比,这可以通过在两个数字之间使用冒号来表示,例如纵横比为 5:4。

数字图像色彩理论和色彩深度:定义精确的图像像素颜色

每个数字图像像素的颜色值可以由红、绿和蓝(RGB)三种不同颜色的量来定义,这三种颜色在每个像素中以不同的量存在。消费电子显示屏利用加色,其中每个 RGB 颜色通道的光波长相加在一起,产生 1680 万种不同的颜色值。加色用于液晶显示器(LCD)、发光二极管(LED)和有机发光二极管(有机发光二极管)显示器。它与印刷中使用的减色法相反。为了向您展示不同的结果,在减色模式下,将红色与绿色(油墨)混合将产生紫色,而在加色模式下,将红色与绿色(浅色)混合将产生鲜艳的黄色。加色可以提供比减色更宽的颜色范围。

为每个像素保存的每个红色、绿色和蓝色值有 256 个亮度级别。这允许您为每个红色、绿色和蓝色值设置 8 位值控制颜色亮度变化,从最小值 0 (#00 或关闭,全暗或黑色)到最大值 255 (#FF 或全开,最大颜色贡献)。用于表示数字图像像素颜色的位数被称为图像的色深。

数字成像行业中使用的常见色深包括 8 位、16 位、24 位和 32 位。我将在这里概述这些,以及它们的格式。最低色深存在于 8 位索引的彩色图像中。这些具有最多 256 个颜色值,并使用 GIF 和 PNG8 图像格式来保存这种索引颜色类型的数据。

中等色深图像具有 16 位色深,因此包含 65,536 种颜色(计算为 256 × 256)。它受 TARGA (TGA)和标记图像文件格式(TIFF)数字图像格式支持。如果您想在 Java 8 游戏中使用 GIF、JPEG 和 PNG 之外的数字图像格式,请导入第三方 ImageJ 库。

真彩色颜色深度图像将具有 24 位颜色深度,因此将包含超过 1600 万种颜色。这被计算为 256 × 256 × 256,产生 16,777,216 种颜色。支持 24 位颜色深度的文件格式包括 JPEG(或 JPG)、PNG、BMP、XCF、PSD、TGA、TIFF 和 WebP。JavaFX 支持其中的三种:JPG、PNG24 (24 位)和 PNG32 (32 位)。使用 24 位色深将为您提供最高的质量水平。这就是为什么我推荐你的 Java 游戏使用 PNG24 或 PNG32。接下来,让我们看看如何通过 alpha 通道表示图像像素透明度值,以及如何在 Java 8 游戏中使用这些值实时合成数字图像!

数字图像合成:在图层中使用 Alpha 通道和透明度

合成是将多层数字影像无缝融合在一起的过程。正如你所想象的,这对于游戏设计和开发来说是一个极其重要的概念。当您想要在显示器上创建一个看起来像是单个图像(或动画)的图像,而实际上它是两个或多个合成图像层的无缝集合时,合成非常有用。您想要设置图像或动画合成的一个主要原因是,通过将它们放在不同的层上,允许对这些图像中的各种元素进行编程控制。

要实现这一点,您需要一个 alpha 通道透明度值,该值可用于控制给定像素与其他层(在其上方和下方)上的另一个像素(在相同的 x,y 图像位置)的混合量的精度。

像其他 RGB 通道一样,alpha 通道有 256 个透明度级别。在 Java 编程中,alpha 通道由# AARRGGBB 数据值的十六进制表示中的前两个槽来表示(我将在下一节中详细介绍)。Alpha 通道 ARGB 数据值使用八个数据槽(32 位)而不是 24 位图像中使用的六个数据槽(#RRGGBB),24 位图像实际上是一个没有 alpha 通道数据的 32 位图像。

因此,24 位(PNG24)图像没有 alpha 通道,不会用于合成,除非它是合成层堆栈中的底部图像板。相比之下,PNG32 图像将用作 PNG24(背景板)或 PNG32(较低的 z 顺序合成层)之上的合成层,这将需要此 alpha 通道功能来显示(通过 alpha 通道透明度值)图像合成中的某些像素位置。

数字图像 alpha 通道和图像合成的概念是如何影响 Java 游戏设计的?主要的优点是能够将游戏画面以及画面中的精灵、投射物和背景图形元素分解成多个组件层。这样做的原因是为了能够将 Java 8 编程逻辑(或 JavaFX 类)应用于单独的图形图像元素,以控制游戏屏幕的某些部分,否则如果它是一个单独的图像,您将无法单独控制这些部分。

图像合成的另一部分,称为混合模式,也是专业图像合成功能的重要因素。JavaFX 混合模式通过使用 Blend 类和 javafx.scene.effect 子包中的 BlendMode 常量值来应用(参见第四章)。这个 JavaFX 混合效果包为 Java 游戏开发人员提供了许多与 Photoshop(和 GIMP)为数字图像制作人员提供的相同的图像合成模式。这将 Java 8(通过 JavaFX)变成了一个强大的图像合成引擎,就像 Photoshop 一样,混合算法可以在非常灵活的水平上进行控制,使用自定义的 Java 8 代码。JavaFX 混合模式常量包括 ADD、SCREEN、OVERLAY、DARKEN、LIGHT、MULTIPLY、DIFFERENCE、EXCLUSION、SRC_ATOP、SRC_OVER、SOFT_LIGHT、HARD_LIGHT、COLOR_BURN 和 COLOR_DODGE。

用 Java 8 游戏代码表示颜色和 Alpha:使用十六进制记数法

现在您已经知道了什么是色深和 alpha 通道,以及在任何给定的数字图像中使用四种不同的图像通道(alpha、红色、绿色和蓝色[ARGB])的组合来表示颜色和透明度,重要的是理解作为程序员,您应该如何在 Java 8 和 JavaFX 中表示这四种 ARGB 图像颜色和透明度通道值。

在 Java 编程语言中,颜色和 alpha 不仅用于 2D 数字图像(通常称为位图图像),还用于 2D 插图(通常称为矢量图像)。颜色和透明度值也经常在许多不同的颜色设置选项中使用。正如您已经看到的 Stage 对象和 Scene 对象,您可以为舞台、场景、布局容器(StackPane)或 UI 控件等设置背景色(或透明度值)。

在 Java 中(JavaFX 也是如此),不同级别的 ARGB 颜色强度值用十六进制表示。十六进制(或 hex)是基于原始的 16 进制计算机记数法。这在很久以前被用来表示 16 位数据值。与更常见的从 0 到 9 计数的 Base10 不同,Base16 记数法从 0 到 F 计数,其中 F 表示 15 的 Base10 值(0 到 15 产生 16 个数据值)。

Java 中的十六进制值总是以 0 和 x 开头,就像这样:0xFFFFFF。这个十六进制颜色值表示颜色。白色常数,不使用 alpha 通道。在这种 24 位十六进制表示中,六个槽中的每一个都代表一个 16 进制值,因此要获得每种 RGB 颜色所需的 256 个值需要两个槽,即 16 × 16 = 256。因此,要使用十六进制表示法表示 24 位图像,您需要在井号后有六个槽来保存六个十六进制数据值(每个数据对表示 256 个级别的值)。如果您乘以 16 × 16 × 16 × 16 × 16 × 16,您将获得 16,777,216 种颜色,这在使用 24 位真彩色图像数据时是可能的。

十六进制数据槽以下列格式表示 RGB 值:0xRRGGBB。对于 Java 常量颜色。白色,十六进制颜色数据值表示中的所有红色、绿色和蓝色通道都处于全亮度(最大颜色值)设置。如果你把所有这些颜色加在一起,你会得到白光。

黄色表示红色和绿色通道打开,蓝色通道关闭,因此颜色是十六进制表示。因此,黄色为 0xFFFF00,其中红色和绿色通道槽完全打开(FF 或 255 Base10 数据值),蓝色通道槽完全关闭(00 或 0 值)。

ARGB 值的八个十六进制数据槽将使用以下格式保存数据:0xAARRGGBB。因此,对于颜色。白色,十六进制颜色数据值表示中的所有阿尔法、红色、绿色和蓝色通道将处于它们的最大亮度(或不透明度),并且阿尔法通道将完全不透明,即不透明,如 FF 值所表示的。因此,颜色的十六进制值。白色常数将是 0xFFFFFFFF。

100%透明的 alpha 通道可以通过将 alpha 槽设置为 0 来表示,正如您在创建一个无窗口的 Java 8 应用时所观察到的那样(参见第四章)。因此,您可以使用 0x00000000 和 0x00FFFFFF 之间的任何值来表示透明图像像素值。重要的是要注意,如果 alpha 通道值等于完全透明,那么可以包含在其他六个(RGB)十六进制数据值槽中的 16,777,216 个颜色值将完全无关紧要,因为该像素是透明的,将被评估为不存在,因此不会合成在最终图像或动画合成图像中。

数字图像遮罩:使用 Alpha 通道创建游戏精灵

alpha 通道在游戏设计中的主要应用之一是遮蔽图像或动画(一系列图像)的区域,以便它可以在游戏图像合成场景中用作游戏精灵。蒙版是使用 alpha 通道透明度值从数字图像中剪切出主题的过程,以便主题可以放置在它自己的虚拟层上。这是使用数字成像软件包完成的,例如 GIMP。

数字图像-合成软件包,如 Photoshop 和 GIMP,包含用于遮罩和图像合成的工具。如果不进行有效的遮罩,就无法进行有效的图像合成,因此对于希望将图形元素(如图像精灵和精灵动画)集成到游戏设计中的游戏设计人员来说,这是一个需要掌握的重要领域。数字图像蒙版技术已经存在很长时间了!

遮罩可以自动为您完成,使用专业的蓝屏(或绿屏)背景,以及可以自动提取那些精确颜色值的计算机软件来创建遮罩,遮罩被转换为 alpha 通道(透明度)信息(数据)。也可以使用数字图像软件,通过算法选择工具之一,结合各种锐化和模糊算法,手动进行遮罩。

在本书的过程中,你会学到很多关于这个工作过程的知识,使用常见的开源软件包,比如 GIMP。掩蔽可能是一个复杂的工作过程。这一章的目的是让你接触到基础知识,这些知识是你阅读这本书的过程的基础。

遮罩过程中的一个关键考虑因素是在被遮罩的对象(主题)周围获得平滑、清晰的边缘。这是为了当你把一个被遮罩的物体(在这种情况下,是一个游戏精灵)放到新的背景图像上时,它看起来就像是在第一个地方被拍摄的一样。成功做到这一点的关键在于你的选择工作过程,这需要以适当的方式使用数字图像软件选择工具,如 GIMP 中的剪刀工具,或 Photoshop 中的魔棒工具。选择正确的工作流程至关重要!

例如,如果您想要遮罩的对象周围有统一颜色的区域(可能您是对着蓝色屏幕拍摄的),您将使用具有适当阈值设置的魔棒工具来选择除对象之外的所有内容。然后,反转选择,这将为您提供包含该对象的选择集。通常,正确的工作流程需要以相反的方式进行。其他选择工具包含复杂的算法,可以查看像素之间的颜色变化。这些对于边缘检测非常有用,您可以将其用于其他选择方法。

平滑数字图像合成:使用抗锯齿平滑图像边缘

抗锯齿是一种流行的数字图像合成技术,在这种技术中,数字图像中的两种相邻颜色沿着两种颜色区域的边界混合在一起。当图像缩小时,这会欺骗观众的眼睛看到更平滑(更少锯齿)的边缘,从而消除所谓的图像锯齿。抗锯齿通过使用平均颜色值(一个颜色范围,是两种颜色结合在一起的中间部分)提供了令人印象深刻的结果,沿着边缘只有几个需要平滑的彩色像素。

让我们看一个例子,看看我在说什么。图 5-2 显示了一个图层上看起来非常清晰的红色圆圈,覆盖了一个背景图层上的黄色填充颜色。我放大了红色圆圈的边缘,然后制作了另一个截图,放在缩小的圆圈的右边。此屏幕截图显示了一系列抗锯齿颜色值(黄色-橙色、橙色到橙色、红色-橙色),正好位于红色和黄色的边缘,即圆形与背景的交界处。

A978-1-4842-0415-3_5_Fig2_HTML.jpg

图 5-2。

A red circle composited on a yellow background (left) and a zoomed-in view (right) showing antialiasing

值得注意的是,JavaFX 引擎将使用 Java2D 软件渲染器或使用 Prism 引擎渲染的硬件(可以使用 OpenGL 或 DirectX ),针对所有背景颜色和背景图像对 2D 形状和 3D 对象进行抗锯齿处理。您仍将负责正确合成,即使用每个图像的 alpha 通道为多层图像提供抗锯齿。

数字图像优化:使用压缩、索引颜色和抖动

影响数字图像压缩的因素有很多,您可以使用一些基本的技术以较小的数据占用量获得较高质量的结果。这是优化数字图像的主要目标;为您的应用(在本例中为游戏)获得尽可能小的数据占用空间,同时获得最高质量的视觉效果。让我们从对数据占用空间影响最大的几个方面入手,研究一下对于任何给定的数字图像来说,这些方面是如何对数据占用空间优化产生影响的。有趣的是,它们的重要性顺序与我迄今为止提出的数字成像概念的顺序相似。

影响最终数字图像资产文件大小的最关键因素是我所称的数据足迹,即数字图像的像素数量或分辨率。这是合乎逻辑的,因为需要存储每个像素,以及包含在它们的三个(24 位)或四个(32 位)通道中的颜色和 alpha 值。在保持图像清晰的同时,分辨率越小,生成的文件也就越小。

对于 24 位 RBG 图像,原始(或未压缩)图像的大小计算为宽×高× 3,对于 32 位 ARGB 图像,为宽×高× 4。例如,未压缩的 24 位真彩色 VGA 图像将具有 640 × 480 × 3,相当于 921,600B 的原始(raw)未压缩数字图像数据。要确定此原始 VGA 图像中的千字节数,您需要进行如下划分:921,600 ÷ 1,024,这是一千字节中的字节数,在 truecolor VGA 图像中会有 900KB 的数据。

通过优化数字影像分辨率来优化原始(未压缩)图像大小非常重要。这是因为一旦图像从游戏应用文件解压缩到系统内存中,这就是它将要占用的内存量,因为图像将使用 24 位(RGB)或 32 位(ARGB)表示法逐个像素地存储在内存中。这也是我游戏开发用 PNG24 和 PNG32,而不是索引色(GIF 或 PNG8)的原因之一;如果操作系统要将颜色转换到 24 位色彩空间,那么出于质量原因,您应该使用 24 位色彩空间,并处理(接受)稍大的应用文件大小。

图像颜色深度是压缩图像的数据足迹的第二个最重要的因素,因为图像中的像素数乘以 1 (8 位)、2 (16 位)、3 (24 位)或 4 (32 位)颜色数据通道。这种小文件大小是 8 位索引彩色图像仍然被广泛使用的原因,尤其是 GIF 图像格式。

如果用于构成图像的颜色变化不太大,索引色图像可以模拟真彩色图像。索引彩色影像仅使用 8 位数据(256 种颜色)来定义图像像素颜色,使用多达 256 种最佳选择颜色的调色板,而不是 3 个 RGB 颜色通道或 4 个 ARGB 颜色通道,每个通道包含 256 种颜色级别。同样,需要注意的是,一旦您使用 GIF 或 PNG8 编解码器将 24 位图像压缩为 8 位图像,您就只能使用原始的 16,777,216 种颜色中的(最多)256 种。这就是为什么我提倡使用 PNG24 或 PNG32 图像,而不是 GIF 或 PNG1 (2 色)、PNG2 (4 色)、PNG4 (16 色)或 PNG8 (256 色)图像,JavaFX 也支持这些图像。

根据任何给定的 24 位源图像中使用的颜色数量,使用 256 种颜色来表示最初包含 16,777,216 种颜色的图像可能会导致一种称为条带的效果。这是由于(通过压缩)得到的 256 色(或更少)调色板中相邻颜色之间的转换不是渐变的,因此看起来不是平滑的颜色渐变。索引的彩色图像有一个视觉校正条带的选项,称为抖动。

抖动是一种算法过程,它沿着图像中任何相邻颜色之间的边缘制作点图案,以欺骗眼睛看到第三种颜色。抖动会给你一个最大的颜色感知数量(65,536;256 × 256),但只有当这 256 种颜色中的每一种都与其他 256 种颜色中的每一种相邻时,才会出现这种情况。尽管如此,您仍然可以看到创建额外颜色的潜力,并且您会对索引颜色格式在某些压缩场景中(对于某些图像)可以达到的效果感到惊讶。

让我们拍摄一张真彩色图像,如图 5-3 所示,并将其保存为 PNG5 索引彩色图像格式,向您展示这种抖动效果。需要注意的是,虽然 PNG5 在 Android 和 HTML5 中受支持,但在 JavaFX 中不受支持,所以如果您自己做这个练习,请选择 2 色、4 色、16 色或 256 色选项!该图展示了奥迪 3D 图像中驾驶员侧后挡泥板上的抖动效果,因为它包含灰色渐变。

A978-1-4842-0415-3_5_Fig3_HTML.jpg

图 5-3。

A truecolor PNG24 image created with Autodesk 3ds Max, which you are going to compress as PNG5

有趣的是,在 8 位索引彩色图像中,使用小于 256 色的最大值是允许的。这通常是为了进一步减少影像的数据足迹。例如,仅使用 32 种颜色就可以获得良好效果的图像实际上是一个 5 位图像,从技术上来说,它被称为 PNG5,尽管这种格式本身通常被称为用于索引颜色使用级别的 PNG8。

我已经设置了这张索引色 PNG 图像,如图 5-4 所示,使用 5 位颜色(32 色,或 PNG5)来清晰地说明这种抖动效果。正如您在图像预览区域中看到的那样,在图的左侧,抖动算法在相邻的颜色之间制作点图案,以创建额外的颜色。

另外,请注意,您可以设置使用抖动的百分比。我经常选择 0%或 100%的设置;但是,您可以在这两个极端值之间的任何地方微调抖动效果。您也可以在抖动算法之间进行选择,因为正如您可能已经猜到的那样,抖动效果是使用抖动算法创建的,抖动算法是索引文件格式(在本例中为 PNG8)压缩例程的一部分。

我使用扩散抖动,这给了不规则形状的梯度一个平滑的效果,就像在汽车挡泥板上看到的那样。您也可以使用更随机的噪波选项,或者不太随机的模式选项。扩散选项通常给出最好的结果,这就是为什么我在使用索引色时选择它(这并不常见)。

正如你所想象的,抖动给图像增加了更难压缩的数据模式。这是因为对于压缩算法来说,图像中的平滑区域(如梯度)比尖锐过渡(边缘)或随机像素模式(如相机 CCD 的抖动或“噪声”)更容易压缩。

因此,应用抖动选项总是会增加几个百分点的数据占用空间。请务必检查应用和不应用抖动(在“导出”对话框中选择)的结果文件大小,以查看它是否值得提供更好的视觉效果。请注意,索引彩色 PNG 图像也有一个透明度选项(复选框),但 PNG8 图像中使用的 alpha 通道只有 1 位(开/关),而不是 PNG32 中的 8 位。

A978-1-4842-0415-3_5_Fig4_HTML.jpg

图 5-4。

Setting dithering to the diffusion algorithm and 32 colors (5 bit), with 100 percent dithering for PNG5 output

您还可以通过添加 alpha 通道来定义合成的透明度,从而增加图像的数据足迹。这是因为通过添加一个 alpha 通道,你将在被压缩的图像中添加另一个 8 位颜色通道(或者透明通道)。如果您需要 alpha 通道来定义图像的透明度,以支持未来的合成要求,例如将图像用作游戏精灵,那么除了包含 alpha 通道数据之外,没有太多选择。

如果您的 alpha 通道包含全零(或使用全黑填充颜色),这将定义您的图像为完全透明,或者包含全 FF 值(或使用全白填充颜色),这将定义您的图像为完全不透明,您将基本上(实际上)定义一个不包含任何有用的 alpha 数据值的 alpha。因此,需要移除透明图像,并且需要将不透明图像定义为 PNG24 而不是 PNG32。

最后,大多数用于遮罩数字图像 RGB 层中的对象的 alpha 通道应该能够很好地压缩。这是因为 alpha 通道主要是白色(不透明)和黑色(透明)的区域,在这两种颜色之间的边缘有一些中灰度值来反走样蒙版(见图 5-2 )。这些灰色区域包含 alpha 通道中的抗锯齿值,并将在图像的 RGB 层中的对象与任何背景颜色或可能在它后面使用的背景图像之间提供视觉上平滑的边缘过渡。

其原因是,在 alpha 通道图像蒙版中,8 位透明度渐变(从白到黑)定义了透明度级别,这可以被认为是每像素混合(不透明度)强度。因此,蒙版(包含在 alpha 通道中)中每个对象的边缘上的中灰度值将基本上用于平均对象边缘和任何目标背景的颜色,无论它可能包含什么颜色(或图像)值。这为任何可能使用的目标背景(包括动画背景)提供了实时抗锯齿。

数字视频和动画:帧、速率、循环、方向

有趣的是,我刚刚谈到的所有关于数字图像的概念同样适用于数字视频和动画,因为这两种格式都使用数字图像作为其内容的基础。数字视频和动画通过使用帧将数字成像扩展到第四维(时间)。这两种格式由有序的帧序列组成,随时间快速显示。

术语“帧”来自电影工业,在电影工业中,即使在今天,电影帧也以每秒 24 帧(24FPS)的速度通过电影放映机,这产生了运动的幻觉。因为数字视频和动画都是由包含数字图像的帧的集合组成的,所以当涉及到内存数据占用优化工作过程(对于动画资产)以及数字视频文件大小数据占用优化工作过程时,以每秒帧数表示的帧速率的概念也是非常重要的。如前所述,在 JavaFX 中,动画的这个属性存储在动画对象速率变量中(见第四章)。

关于动画对象或数字视频资源中的帧的优化概念与关于图像中的像素(数字图像的分辨率)的优化概念非常相似:使用的越少越好!这是因为动画或视频中的帧数会使每一帧所使用的系统内存和文件大小数据占用空间都成倍增加。在数码视频中,不仅每帧(图像)的分辨率,帧速率(在“压缩设置”对话框中指定)也会影响文件大小。在本章的前面,您已经了解到,如果您将图像中的像素数乘以其颜色通道数,您将获得图像的原始数据足迹。对于动画和数字视频,您现在将再次将该数字乘以需要用于创建运动错觉的帧数。

因此,如果您的游戏有一个动画 VGA (RGB)背景板(请记住,每帧为 900KB ),它使用五帧来创建运动的幻觉,那么您正在使用 900KB × 5 或 4,500KB (4.5MB)的系统内存来保存该动画。当然,这对于一个背景来说占用了太多的内存,这也是为什么你将使用静态背景和精灵覆盖来在不到一兆字节的空间内达到完全相同的效果。数字视频的计算有点不同,因为它有数百或数千帧。对于数字视频,您可以将原始图像数据大小乘以数字视频设置为回放的每秒帧数(帧速率)(此帧速率值在压缩过程中指定),然后将结果乘以视频文件中包含的内容持续时间的总秒数。

继续 VGA 示例,您知道 24 位 VGA 图像有 900KB。这使得将这带到下一个级别的计算变得容易。数字视频传统上以 30FPS 运行,因此 1 秒钟的标准清晰度原始(未压缩)数字视频将是 30 个图像帧,每个图像帧为 900KB,总数据量为 27,000KB!您可以看到为什么 MPEG-4 H.264 AVC 等视频压缩文件格式非常重要,它可以压缩数字视频所产生的大量原始数据。JavaFX 媒体包使用最令人印象深刻的视频压缩编解码器之一(“codec”代表 code-decode),它在 HTML5 和 Android 中也受支持,即前面提到的 MPEG-4 H.264 AVC(高级视频编解码器)。这对于开发人员资产优化非常方便,因为一个数字视频资产可以跨 JavaFX、HTML5 和 Android 应用使用。万一你想在你的游戏背景中使用数字视频(我不推荐),接下来我将讲述数字视频压缩和优化的基础知识。

数字视频压缩概念:比特率、数据流、标清、高清、UHD

让我们从商业视频中使用的主要或标准分辨率开始。这些也是常见的设备屏幕分辨率,可能是因为如果屏幕像素分辨率与屏幕上全屏播放的视频像素分辨率匹配,将会出现零缩放,这可能会导致缩放伪像。在高清晰度出现之前,视频是标准清晰度(SD),使用 480 像素的垂直分辨率。VGA 是标清分辨率,720 × 480 可以称为宽标清分辨率。高清(HD)视频有两种分辨率,1,280 × 720,我称之为伪高清,1,920×1080,业界称之为真高清。这两种高清分辨率都具有 16:9 的宽高比,用于电视和独立电视、智能手机、平板电脑、电子书阅读器和游戏控制台。现在还有一种超高清(UHD)分辨率,具有 4,096 × 2,160 像素。

视频流是一个比分辨率更复杂的概念,因为它涉及到在大范围内播放视频数据,例如 Java 8 游戏应用和远程视频数据服务器之间的视频数据,远程视频数据服务器将保存您潜在的大量数字视频资产。此外,运行 Java 游戏应用的设备将与远程数据服务器实时通信,在视频播放时接收视频数据包(之所以称为流,是因为视频从视频服务器通过互联网流入硬件设备)。MPEG-4 H.264 AVC 格式编解码器(编码器-解码器对)支持视频流。

你需要理解的最后一个概念是比特率。比特率是视频压缩过程中使用的关键设置,因为比特率代表您的目标带宽,或每秒钟能够容纳一定数量的比特流的数据管道大小。比特率设置还应该考虑任何给定的支持 Java 的设备中存在的 CPU 处理能力,使您的数字视频的数据优化更具挑战性。幸运的是,如今大多数设备都配备了双核或四核 CPU!

一旦比特通过数据管道,它们也需要被处理并显示在设备屏幕上。因此,数字视频资产的比特率不仅要针对带宽进行优化,还要考虑到 CPU 处理能力的变化。一些单核 CPU 可能无法在不丢帧的情况下解码高分辨率、高比特率的数字视频资产,因此,如果您打算将较旧或较便宜的消费电子设备作为目标,请确保优化低比特率视频资产。

数字视频数据足迹优化:使用编解码器及其设置

如前所述,您的数字视频资产将被压缩,使用称为编解码器的软件工具。视频编解码器有两个“方面”:一方面编码视频数据流,另一方面解码视频数据流。视频解码器将是使用它的操作系统、平台(JavaFX)或浏览器的一部分。解码器主要针对速度进行优化,因为回放的平滑度是一个关键问题,而编码器则针对减少其生成的数字视频资产的数据占用量进行了优化。因此,编码过程可能需要很长时间,这取决于工作站包含多少个处理核心。大多数数字视频内容制作工作站应该支持八个处理器内核,比如我的 64 位 AMD 八核工作站。

编解码器(编码器端)类似于插件,因为它们可以安装到不同的数字视频编辑软件包中,使它们能够编码不同的数字视频资源文件格式。因为 Java 和 JavaFX 8 支持 MPEG-4 H.264 AVC 格式,所以您需要确保您使用的数字视频软件包之一支持使用这种数字视频文件格式对数字视频数据进行编码。不止一家软件制造商生产 MPEG-4 编码软件,因此在编码速度和文件大小方面,会有不同的 MPEG-4 AVC 编解码器产生不同的(更好或更差的)结果。如果你想制作专业的数字视频,我强烈推荐你使用的专业解决方案是 Sorenson Squeeze Pro。

还有一个名为 EditShare LightWorks 12 的开源解决方案,计划到 2014 年原生支持输出到 MPEG4 编解码器。优化(设置压缩设置)数字视频数据文件大小时,有许多变量会直接影响数字视频数据的占用空间。我将按照对视频文件大小的影响,从最重要到最不重要的顺序来讨论这些参数,以便您知道调整哪些参数来获得您想要的结果。

与数字图像压缩一样,视频每帧的分辨率或像素数是开始优化过程的最佳位置。如果您的用户使用 800 × 480 或 1,280 × 720 的智能手机、电子阅读器或平板电脑,那么您不需要使用真正的高清 1,920 × 1,080 分辨率来获得数字视频资产的良好视觉效果。有了超高密度(小点距)显示器,你可以将 1,280 的视频放大 33 %,看起来相当不错。这种情况的例外可能是高清或 UHD(通常称为 4K 独立电视)游戏的目标是独立电视;对于这些巨大的 55 至 75 英寸(屏幕)场景,您可能希望使用行业标准的真正高清 1,920 × 1,080 分辨率。

假设数字视频本身的实际秒数无法缩短,下一个优化级别将是每秒视频使用的帧数(或 FPS)。如前所述,这就是所谓的帧速率,与其设置视频标准 30FPS 帧速率,不如考虑使用电影标准 24FPS 帧速率,甚至多媒体标准 20FPS 帧速率。您甚至可以使用 15FPS 的帧速率,这是视频标准的一半,具体取决于内容中的移动量(和速度)。请注意,15FPS 的数据量是 30FPS 的一半(编码数据减少了 100%)。对于某些视频内容,这将与 30FPS 内容一样回放(看起来)。对此进行测试的唯一方法是在编码过程中尝试帧速率设置。

获得较小数据占用空间的下一个最佳设置是您为编解码器设置的比特率。比特率等于所应用的压缩量,因此设定了数字视频数据的质量水平。值得注意的是,您可以简单地使用 30FPS、1,920 分辨率的高清视频,并指定低比特率上限。如果您这样做,结果将不会像您尝试使用较低的帧速率和分辨率以及较高(质量)的比特率设置那样好看。对此没有固定的规则,因为每个数字视频资源都包含完全唯一的数据(从编解码器的角度来看)。

获得较小数据占用空间的第二个最有效的设置是编解码器用来对数字视频进行采样的关键帧的数量。视频编解码器通过查看每一帧,然后在接下来的几帧中仅对变化或偏移进行编码来应用压缩,从而编解码器算法不必对视频数据流中的每一帧进行编码。这就是为什么会说话的头部视频比每个像素都在每帧上移动的视频(如使用快速摄像机平移或快速视野[FOV]缩放的视频)编码更好。

关键帧是编解码器中的一项设置,它强制编解码器不时对您的视频数据资源进行新的采样。关键帧通常有一个自动设置,它允许编解码器决定采样多少关键帧,还有一个手动设置,它允许您指定关键帧采样的频率,通常是每秒特定次数或整个视频的持续时间(总帧数)。

大多数编解码器通常具有质量或清晰度设置(滑块),用于控制压缩前应用到视频帧的模糊量。如果您不熟悉这个技巧,对图像或视频应用轻微的模糊(这通常是不可取的)可以实现更好的压缩,因为图像中的尖锐过渡(边缘)比柔和过渡更难编码,需要更多的数据来再现。也就是说,我会将质量(或清晰度)滑块保持在 80%到 100%之间,并尝试使用我在这里讨论的其他变量之一来减少您的数据占用量,例如降低分辨率、帧速率或比特率。

最终,对于任何给定的数字视频数据资产,您都需要微调许多不同的变量,以实现最佳的数据占用空间优化。重要的是要记住,对于数字视频编解码器来说,每个视频资源看起来都是不同的(数学上)。由于这个原因,不可能有可以被开发来实现任何给定压缩结果的标准设置。也就是说,随着时间的推移,调整各种设置的经验最终会让您感受到,为了获得所需的最终结果,您必须根据不同的压缩参数来更改各种设置。

数字音频概念:振幅、频率、样本

你们这些音响发烧友知道,声音是通过在空气中发送声波脉冲而产生的。数字音频很复杂;这种复杂性的一部分来自于需要将使用扬声器纸盆创建的模拟音频技术与数字音频编解码器连接起来。模拟扬声器通过脉冲产生声波。我们的耳朵以完全相反的方式接收模拟音频,捕捉和接收那些空气脉冲或不同波长的振动,然后将它们转换回我们大脑可以处理的数据。这就是我们“听到”声波的方式;然后,我们的大脑将不同的音频声波频率解释为不同的音符或音调。

声波产生各种音调,这取决于声波的频率。宽的或不常见的(长的)波产生低音,而更频繁的(短的)波长产生高音。有趣的是,不同频率的光会产生不同的颜色,所以模拟声音(音频)和模拟光(颜色)有着密切的相关性。你很快就会看到,数字图像(和视频)之间还有许多其他相似之处,这些相似之处也将贯穿到你的数字新媒体内容制作中。

声波的音量将由其振幅或波的高度(或大小)决定。因此,如果你在 2D 观察,声波的频率等于声波在 x 轴上的间距,振幅等于声波在 y 轴上的高度。

声波可以独特地成形,允许它们“搭载”各种声音效果。一种“纯”或基线类型的声波称为正弦波(您在高中三角学中学过,使用正弦、余弦和正切数学函数)。熟悉音频合成的人都知道,其他类型的声波也用于声音设计,例如锯齿波,它看起来像锯子的边缘(因此得名),以及脉冲波,它仅使用直角进行整形,产生即时的开和关声音,转换为音频脉冲(或突发)。

甚至在声音设计中使用随机波形(如噪声)来获得尖锐的声音效果。通过使用最近获得的数据足迹优化知识,您可能已经确定,声波(以及一般的新媒体数据)中出现的“混乱”或噪声越多,编解码器就越难压缩。因此,由于数据中的混乱,更复杂的声波将导致更大的数字音频文件。

将模拟音频转换为数字音频数据:采样、精度、高清音频

将模拟音频(声波)转换为数字音频数据的过程称为采样。如果你在音乐行业工作,你可能听说过一种叫做采样器的键盘(甚至是架装式设备)。采样是将模拟音频波分割成片段的过程,以便您可以使用数码音频格式将波形存储为数码音频数据。这就把无限精确的模拟声波变成了离散的数字数据,也就是说,变成了 0 和 1。使用的 0 和 1 越多,无限精确(原始)模拟声波的再现就越精确。

采样的音频声波的每个数字段称为样本,因为它在精确的时间点对声波进行采样。您想要的采样精度(分辨率)将决定用于再现模拟声波的 0 和 1 的数量,因此采样精度由用于定义每个波切片高度的数据量决定。与数字成像一样,这种精度被称为分辨率,或者更准确地说(没有双关语),样本分辨率。采样分辨率通常用 8 位、12 位、16 位、24 位或 32 位分辨率来定义。游戏大多利用 8 位分辨率来实现爆炸等效果,清晰度在这些效果中并不重要;12 位分辨率,用于清晰的口语对话和更重要的音频元素;背景音乐可能是 16 位分辨率。

在数字成像和数字视频中,分辨率由像素数来量化,而在数字音频中,则由用于定义每个模拟音频样本的数据位数来量化。同样,与数字成像一样,像素越多,质量越好,而数字音频的样本分辨率越高,声音再现越好。因此,更高的采样分辨率,使用更多的数据来再现给定的声波样本,将产生更高质量的音频回放,代价是更大的数据足迹。这就是 16 位音频(通常称为 CD 音质音频)听起来比 8 位音频更好的原因。根据所涉及的音频,12 位音频可能是一个很好的折衷方案。

在数字音频领域,有一种新型的音频样本,在消费电子行业被称为高清音频。HD 数字音频广播电台使用 24 位样本分辨率,因此每个音频样本或声波片段包含 16,777,216 位样本分辨率。一些较新的硬件设备现在支持高清音频,如你看到的智能手机广告中的“高清音频”,这意味着它们有 24 位音频硬件。如今,笔记本电脑(包括 PC)以及游戏机和 ITV 也标配了 24 位音频播放硬件。

需要注意的是,对于 Java 8 游戏来说,HD 音频可能不是必需的,除非您的游戏是面向音乐的,并且使用了高质量的音乐,在这种情况下,您可以通过 WAVE 文件格式使用 HD 音频样本。

另一个考虑因素是数字音频采样频率(也称为采样速率),它衡量在 1 秒的采样时间帧内,以特定采样分辨率采样的数量。就数字图像编辑而言,采样频率类似于数字图像中包含的颜色数量。您可能很熟悉“CD 音质音频”这个术语,它被定义为使用 16 位采样分辨率和 44.1kHz 采样速率(采集 44,100 个样本,每个样本具有 16 位采样分辨率,即 65,536 位音频数据)。您可以通过将采样比特率乘以采样频率,再乘以音频片段中的秒数,来确定音频文件中的原始数据量。显然,这可能是一个巨大的数字!音频编解码器在优化数据方面非常出色,数据占用空间非常小,音质损失非常小。

因此,数字图像和数字视频中存在的完全相同的权衡也发生在数字音频中:包含的数据越多,结果质量越高,但总是以更大的数据足迹为代价。在视觉媒体中,数据覆盖区的大小是使用色深、像素来定义的,在数字视频和动画的情况下,还使用帧来定义。在听觉媒体中,它是通过采样分辨率结合采样率来定义的。数字音频行业目前最常见的采样率包括 8kHz、22kHz、32kHz、44.1kHz、48kHz、96KHz、192kHz,甚至 384kHz。

较低的采样率,如 8kHz、11kHz 和 22kHz,是您将在游戏中使用的采样率,因为经过精心优化,这些采样率可以产生高质量的音效和街机音乐。这些速率对于采样任何基于语音的数字音频也是最佳的,例如电影对白或电子书旁白轨道。较高的音频采样速率(如 44.1kHz)更适合音乐,需要高动态范围(高保真)的声音效果(如隆隆雷声)可以使用 48kHz。更高的采样率将允许音频再现展示电影院(THX)的声音质量,但这不是大多数游戏所要求的。

数字音频流:专属音频与流音频

与数字视频数据一样,数字音频数据既可以包含在应用分发文件(对于 Java,是. JAR 文件)中,也可以使用远程数据服务器进行流式传输。与数字视频类似,流式数字音频数据的优势在于它可以减少应用文件的数据占用量。缺点是可靠性。许多相同的概念同样适用于音频和视频。流式音频将节省数据空间,因为您不必在中包含所有繁重的新媒体数字音频数据。JAR 文件。所以,如果你计划编写一个自动点唱机应用,你可能想考虑流式传输你的数字音频数据;否则,请尝试优化您的数字音频数据,以便您可以将它们(受控)包含在中。JAR 文件。这样,当应用的用户需要时,数据总是可用的!

流式数字音频的缺点是,如果用户的连接(或音频数据服务器)中断,您的数字音频文件可能并不总是呈现给最终用户使用您的游戏应用播放和收听!数字音频数据的可靠性和可用性是流传输和捕获之间权衡的另一个关键因素。这同样适用于数字视频资产。

同样,与数字视频一样,数字音频流的一个主要概念是数字音频数据的比特率。正如您在上一节中了解到的,比特率是在压缩过程中定义的。需要支持较低比特率带宽的数字音频文件将对音频数据进行更多的压缩,这将导致较低的质量。这些将在更多的设备上更平滑地传输(回放),因为更少的比特可以更容易地快速传输和处理。

JavaFX 中的数字音频:支持的数字音频编解码器和数据格式

JavaFX 中的数字音频编解码器比数字视频编解码器多得多,因为只有一种视频编解码器,即 MPEG-4 H.264 AVC。Android 音频支持包括. MP3 (MPEG-3)文件,Windows WAVE(脉码调制[PCM]音频)。WAV 文件、. MP4(或. M4A) MPEG-4 AAC(高级音频编码)音频和 Apple 的 AIFF (PCM)文件格式。JavaFX(以及 Java 8)支持的最常见的格式是流行的. MP3 数字音频文件格式。由于音乐下载网站,如 Napster 或 Soundcloud,你们大多数人都熟悉 MP3 数字音频文件,我们大多数人收集这种格式的歌曲用于 MP3 播放器和基于 CD-ROM 或 DVD-ROM 的音乐收藏。MP3 数字音频文件格式很受欢迎,因为它具有相当好的压缩比和质量,并得到广泛支持。

在 Java 8 应用中,MP3 是一种可以接受的格式,只要使用最佳的编码工作流程,尽可能获得最高的质量水平。值得注意的是,像 JPEG(用于图像)一样,MP3 是一种有损音频文件格式,其中一些音频数据(以及质量)在压缩过程中被丢弃,并且无法恢复。

JavaFX 有两种无损音频压缩编解码器,AIFF 和 WAVE。你们中的许多人对这些都很熟悉,因为它们分别是苹果 Macintosh 和微软 Windows 操作系统使用的原始音频格式。这些文件使用 PCM 音频,这是无损的(在这种情况下,因为没有应用任何压缩!).“PCM”,如上所述,代表“脉冲编码调制”,指的是它所保存的数据格式。

PCM 音频通常用于 CD-ROM 内容以及电话应用。这是因为 PCM WAVE audio 是一种未压缩的数字音频格式,没有对数据流应用 CPU 密集型压缩算法。因此,解码(CPU 数据处理)对于电话设备或 CD 播放器来说不是问题。

因此,当您开始将数字音频资产压缩成各种文件格式时,您可以使用 PCM 作为基准文件格式。它允许您查看 PCM (WAVE)与 MP3 和 MP4 音频压缩结果之间的差异,以了解您的 JAR 文件获得了多少数据占用优化;更重要的是,你还可以看到你的采样分辨率和采样频率优化将如何影响游戏音频效果所使用的系统内存。即使您使用 MP3 或 MP4 格式,在音频资产可以与 AudioClip 类一起使用并在 Java 8 游戏中用作声音效果之前,仍然必须将其解压缩到内存中。

因为 WAVE 或 AIFF 文件不会有任何质量损失(因为也不需要解压缩),PCM 数据可以直接从 JAR 文件放入系统内存!这使得 PCM 音频非常适合持续时间短(0.1 到 1 秒)的游戏音效,并且可以高度优化,使用 8 位和 12 位采样分辨率以及 8kHz、22kHz 或 32kHz 采样频率。最终,对于任何给定的数字音频数据,要找出 JavaFX 支持的哪种音频格式具有最佳的数字音频压缩结果,唯一真正的方法是使用您知道受支持且高效的主要编解码器对您的数字音频进行编码。稍后我将概述这一工作过程,当您向游戏添加音频时,您将使用相同的源音频样本观察不同格式之间的相对数据足迹结果(参见第一章 5)。然后,您将听取音频播放质量,这样您就可以做出关于质量和文件大小之间最佳平衡的最终决定。这是开发 JavaFX 数字音频资产以在 Java 8 游戏中使用的工作流程。

JavaFX 还支持流行的 MPEG-4 AAC 编解码器。这些数字音频数据可以包含在 MPEG4 容器(. mp4、. m4a、. m4v)或文件扩展名中,并且可以使用任何操作系统回放。值得注意的是,JavaFX 不包含 MPEG-4 解码器,而是支持所谓的多媒体容器,这意味着 JavaFX 使用操作系统的 MPEG-4 解码器。

出于这个原因,并且因为在线听力研究已经得出结论,MP3 格式比 MP4 具有更好的质量(对于音乐),您将通过 Media 和 MediaPlayer 类使用 MP3 格式来获得更长形式的音频(游戏背景音乐循环)。您将通过 JavaFX 慷慨提供的 audio clip digital audio sequencing engine(class)使用 PCM WAVE audio 格式的短格式(1 秒或更短)音频(游戏音效,如枪声、铃声、叫喊声、咕哝声、笑声、欢呼声和其他此类数字音频资产)。

数字音频优化:从 CD 质量的音频开始,然后反向工作

优化您的数字音频资产以便在市场上最广泛的硬件设备上播放,将比优化您的数字视频或数字图像(以及动画)更容易。这是因为目标屏幕分辨率和显示器宽高比之间的差异比硬件设备之间的数字音频播放硬件支持类型之间的差异大得多(具有 24 位高清音频播放硬件兼容性的新硬件可能除外)。所有硬件都可以很好地播放数字音频资产,因此音频优化是一个“一个音频资产影响所有设备”的场景,而对于视觉(视频、图像、动画)部分,您可以看到大到 4,096 × 2,160 像素(4K iTV 电视机)和小到 320 × 320 像素(翻盖手机和智能手表)的显示屏。

重要的是要记住,用户的耳朵不能感知数字音频的质量差异,而用户的眼睛可以感知数字图像、2D 动画和数字视频的质量差异。一般来说,对于 Java 游戏音频的支持,在所有硬件设备上有三个主要的数字音频支持“最佳点”。

通过使用 8kHZ 或 22kHz 的采样速率以及 8 位或 12 位的采样分辨率,较低质量的音频(如短旁白轨道、人物感叹词和短时声音效果)可以获得非常高的质量。中等质量的音频,如长旁白轨道、持续时间较长的声音效果和循环背景(环境)音频,通过使用 22kHz 或 32kHz 采样速率以及 12 位或 16 位采样分辨率,可以达到非常高的质量水平。

高质量的音频资产,如音乐,应进行优化,以接近 CD 质量的音频,并将使用 32kHz 或 44.1kHz 的采样速率,以及 16 位数据采样分辨率。对于处于音频频谱超高端的高清质量音频,您可以使用 48kHz 采样速率和 24 位数字音频数据采样分辨率。还有一种未命名的“中间某处”高端音频规格,使用 48kHz 采样率,以及 16 位数据采样分辨率,这恰好是杜比 THX 过去在电影院使用的高端音频体验技术(过去)。

最终,它归结为数字音频数据足迹优化工作流程中出现的质量-文件大小平衡结果,这可能是惊人的。因此,在所有这些硬件设备上优化数字音频资产的初始工作流程是创建 44.1kHz 或 48kHz 的基准 16 位资产,然后使用 JavaFX 支持的不同格式对其进行优化(压缩)。一旦工作流程完成,您就可以看到哪些数字音频资产提供了最小的数据占用空间,以及最高质量的数字音频回放。之后,您可以将 44.1KHz 或 48kHz 数据降低到 32kHz 并保存下来,首先使用 16 位分辨率,然后使用 12 位分辨率。接下来,重新打开原始的 48kHz 数据,下采样至 22kHz 采样频率,并使用 16 位和 12 位分辨率导出该数据,依此类推。稍后,当您将数字音频资产添加到 Java 8 游戏中时,您将执行此工作流程,因此您将看到整个流程(参见第一章 5)。

接下来,我们来看看 JavaFX Scene Builder,以及它是如何使用 FXML 来让设计人员可视化地设计 JavaFX 应用的。在本书的课程中,我不会使用 Scene Builder 或 FXML(只是 Java 8 代码和 JavaFX 类),所以请注意!

JavaFX 场景生成器:使用 FXML 进行 UI 设计

JavaFX Scene Builder 是一个可视化设计工具,可生成 FXML (JavaFX 标记语言)UI 场景图结构,以定义 JavaFX 应用的前端设计。然后,可以在 Java 8 中“膨胀”这个 FXML UI 定义,以创建应用的 JavaFX 场景图、节点、组和 SubScene 对象,其中填充了定义 UI 设计的 javafx.scene.control 包类(对象)。Oracle 提供 Scene Builder 可视化开发工具和 FXML 的目的是允许非程序员(表面上是 UI 设计师)为他们的 Java 8 应用设计前端 UI,以便 Java 程序员可以专注于后端功能应用任务处理逻辑。

因为 FXML 和 Scene Builder 针对 UI 设计进行了优化(排列控件,如按钮、文本输入字段、单选按钮、复选框等),所以在本书的整个过程中,我不打算使用 Scene Builder 和 FXML。不过,我将在本章中介绍它,以便您知道如何在其他 JavaFX 应用中使用它。我的理由是,除了最初的游戏闪屏,它包含一些 UI 按钮对象,显示游戏说明,列出贡献者,跟踪高分,保存当前游戏状态,并开始玩游戏,UI 设计不会是本书的主要焦点。

要使用 FXML,在使用 Scene Builder 可视化 UI 设计工具后不久,您必须创建一种特殊的 FXML 应用,正如您在第二章(见图 2-4)中创建 JavaFX 游戏时所学。创建 FXML 应用会导入 javafx.fxml 包和类。这允许 Java 8 代码膨胀由 UI 设计者创建的 FXML 结构,以便程序员可以使用它们将 Java 逻辑附加到各种 UI 控件。Android 操作系统也是这样做的,使用基本的 XML,但是在 Android 中这种方法不是可选的;这是做事方式的一部分。在 JavaFX 8 中(如图 2-4 所示),它只是一个选项。如果你想进一步研究基于 XML 的 Android UI 设计,可以看看我的书 Pro Android UI (Apress,2014)。

为您编写 FXML UI 设计构造的 Scene Builder 可视化布局工具是一个所见即所得的拖放界面设计工具。设计者所要做的就是将任何 JavaFX UI 控件从包含 javafx.scene.control 包中每个控件类(对象)的 UI 控制面板拖放到编辑屏幕上(参见第四章)。这个场景生成器集成到 NetBeans 8.0 中,以便于访问和与 JavaFX 集成,以防程序员也需要使用它来快速为他们的客户端设计 UI 原型。对于不想在 NetBeans IDE 中工作的设计人员,还有一个独立版本的场景构建器工具,版本 2.0。

您可以在 FXML 编辑和预览之间实时切换,查看 UI 设计和布局的变化,而无需编译 Java 应用。您还可以将所有 CSS 样式实时应用于场景构建器工具和 FXML 结构,并查看这些代码更改的结果,同样,无需任何 Java 编译!此外,您可以使用第三方 JAR 文件或 FXML 定义将自定义 UI 控件添加到 UI 控制面板库中。

场景构建器工具包 API 是开源的。这使您可以自定义 Scene Builder 的 UI 面板和控件的集成,允许您将 Scene Builder 集成到其他 ide 中,如 Eclipse 或 IntelliJ IDEA。GUI 组件(控件)库中最近添加了一个富文本 TextFlow 容器,提供了富文本编辑功能。有了这些新功能,您可以构建多节点、富文本结构,将其他 UI 元素或新的媒体元素类型与 TextFlow 元素集成在一起。

对于 3D“发烧友”来说,Scene Builder 可视化设计编辑器和 FXML 也完全支持 3D。可以使用场景构建器工具加载甚至保存 3D 对象,并且可以使用检查器面板实时编辑(和查看)对象的所有属性。现在还不能使用 Scene Builder 从头开始创建 3D 对象,您也不能在此时分配或编辑复杂的网格或材质属性,但我相信这些功能会随着计划添加到 JavaFX 8 中的高级 3D OpenGL ES 功能一起出现。

接下来,让我们深入了解一下 FXML 标记语言。之后,您将检查一个实际的 FXML UI 定义结构,并且您将确切地看到当前 JavaFX 应用的 UI 设计是如何使用 FXML UI 定义构造的。正如您将看到的,FXML 使 UI 设计变得更加容易!

FXML 定义:XML UI 定义结构的剖析

FXML 结构基于 JavaFX 类(对象)及其属性,FXML 标记和参数结构(您可以轻松创建)允许您使用标记语言更轻松地“模拟”前端 UI。FXML 结构使您可以更容易地构建场景图层次,FXML 标记及其参数(您将在下一节中看到)与 JavaFX API 类 1:1 匹配。

一旦创建了 UI 设计,Java 程序员就可以使用 javafx.fxml 类和方法,根据 Java 对象将 UI 布局容器和 UI 控件排列扩展到 javafx 场景和场景图结构中。然后,可以在应用 Java 代码中使用 UI 设计。如前所述,FXML 对于设计包含大量按钮、表单、复选框等的复杂、静态(固定)UI 设计布局非常有用。

Hello World UI FXML 定义:使用 FXML 复制当前的 UI 设计

在 FXML 结构中定义的第一件事是 FXML 处理指令。每个处理指令都以小于号、问号序列() and ends with the reversal of that sequence (question mark, greater-than sign [?>)开始。第一个处理指令声明了 XML 语言的用法、版本(1.0)和要使用的文本字符集语言编码格式(在本例中,是 UTF-8[通用字符集转换格式,8 位])。因为它是 8 位的,所以在这个国际字符集中有 256 个字符,它被设计成跨越许多基于日耳曼字符的语言,即使用 A 到 Z 字母表(包括重音字符)的语言。

XML 语言和字符集声明之后是处理指令。它们导入 Java 语言、实用程序和 javafx.scene 包以及 javafx.scene.layout 和 javafx.scene.control 包,这些包用于设计 UI 布局和布局包含的 UI 控件。

例如,您在当前应用中使用的 StackPane UI 布局容器位于 javafx.scene.layout 包中,而 button UI 控件元素位于 javafx.scene.control 包中。因为 FXML UI 布局容器是这个结构中的父元素,所以它首先出现,或者在您将要创建的嵌套 FXML UI 定义结构之外。

中,您将嵌套 StackPane 类(object)的子类,使用<子类>标签(XML 标签使用<箭头括号>编码)。嵌套在这些<子>标签中的是 UI 控件元素(在本例中,是一个按钮控件,所以您将使用<按钮>标签)。请注意,在箭头括号内使用了类(对象)的专有名称来创建 FXML 标签,因此这是非常符合逻辑的,应该很容易学习并融入到您的 UI 设计工作过程中:

<?``xml``version="1.0" encoding="``UTF-8

<? import java.lang.* ?>

<? import java.util.* ?>

<? import javafx.scene.* ?>

<? import javafx.scene.layout.* ?>

<? import javafx.scene.control.* ?>

<``StackPane``id="root" prefHeight="250" prefWidth="300"

<children>

<``Button``id="btn" text="Say 'Hello World'" layoutX="125" layoutY="116"

</children>

</StackPane>

接下来,让我们看一下标签和参数语法,以便您总是知道如何构造 FXML UI 布局和控件定义文件。一个没有子元素的 UI 元素,比如之前的

<Button``id="btn" text="Say 'Hello World'" layoutX="125" layoutY="116"

请注意,配置标记的参数(等同于对象的属性(或创建对象的类中的变量))嵌套在标记本身中,并使用变量名和等于运算符,以及引号中指定的数据值,如前面的代码所示。

嵌套了对象的 FXML 标签将使用这个不同的<类名>开始标签。在这个标签内(后)列出的嵌套标签之后是一个< /ClassName >结束标签。这允许标记语法指定(成为)其内部的<子>标记的容器,正如您在这里的示例中看到的,其中开始和结束 FXML 标记根据它们的嵌套(内部)层次结构进行排序:

<StackPane``id="root" prefHeight="250" prefWidth="300"

<children>

<Button``id="btn" text="Say 'Hello World'" layoutX="125" layoutY="116"

</children>

</StackPane>

如您所见,可以将参数放在父标记的开始标记中,方法是将它们放在开始标记的< ClassName 部分和大于号之间。如果需要的话,这就是为任何参数配置父标记的方式,就像指定 StackPane 大小和名称(在 FXML 中称为 id)一样。

摘要

在第五章中,您仔细了解了一些更重要的游戏设计和新媒体概念,您将在 Java 8 游戏开发工作流程中使用这些概念,这样您就拥有了创建游戏所必需的基础知识。您还学习了 JavaFX Scene Builder 和 FXML,只是为了掌握这些概念,因为我将使用 Java 8 代码和 JavaFX 类来完成本书中的所有工作,以满足我的 Android 书籍读者的要求(“我们如何仅使用 Java 代码来完成这些工作?我们不想使用 XML 来创建我们的应用!”是我这几天不断听到的口头禅)。

首先,您研究了静态与动态的关键概念,以及这对游戏设计和游戏优化的重要性,因为如果游戏优化不是游戏设计、开发和优化过程中的一个持续考虑因素,过多的动态会使旧的单核甚至双核 CPU 过载。

接下来,您探索了游戏设计(和开发)的一些关键组件,如精灵、碰撞检测、物理模拟、背景动画、UI 设计、评分引擎和游戏逻辑。您看了一下这些如何应用于静态游戏,或没有连续运动的游戏,如策略游戏、棋盘游戏、谜题、知识游戏、记忆游戏和动态游戏,以及使用连续运动的游戏,如平台游戏、街机游戏、第一人称射击游戏、第三人称射击游戏、驾驶游戏等。

您对新媒体资产类型以及数字图像、动画、数字视频和数字音频的概念和术语进行了高水平的技术概述。你学习了像素;决议;以及纵横比如何定义图像、动画或视频的形状,以及颜色深度和 alpha 通道透明度,以及如何使用十六进制表示法定义这些内容。然后,您研究了时间的第四维度,了解了帧、帧速率和比特率,并查看了数字音频、采样频率和采样分辨率。最后,您学习了 JavaFX 场景图和 FXML 如何工作以及如何在您当前的游戏中使用它们。

在下一章中,您将研究 JavaFX 场景图并为 Java 8 游戏应用创建基础设施,包括闪屏(游戏的主屏幕和主 UI)。

六、游戏设计的基础:JavaFX 场景图和InvinciBagel游戏基础设施

在这一章中,你将开始从用户界面(UI)和用户体验的角度,以及从“引擎盖下”游戏引擎、精灵引擎、碰撞引擎和物理引擎的角度来设计你的 InvinciBagel 游戏的基础设施。你将记住优化,因为你必须通过这本书的其余部分工作,所以你不会得到一个如此广泛或复杂的场景图,脉冲系统不能有效地更新一切。这意味着保持主要的游戏 UI 屏幕(场景或子场景节点)最少(三个或四个);确保 3D 和媒体引擎(数字音频和数字视频)使用自己的线程;并检查驱动游戏的功能性“引擎”都是逻辑编码的,使用它们自己的类和适当的 Java 8 编程约定、结构、变量、常数和修饰符(见第三章)。

首先,您将了解游戏将为用户提供的顶级、正面 UI 屏幕设计,包括用户在启动应用时看到的 InvinciBagel“品牌”闪屏。该屏幕上有按钮控件,用于访问其他信息屏幕,您可能希望尽量减少这些信息屏幕的数量,因为它们可能是场景节点(主游戏屏幕)或 ImageView 节点(其他信息屏幕)。这些游戏支持屏幕将包含用户为了有效地玩游戏而需要知道的东西,例如游戏说明和高分屏幕。您还将包括一个法律免责声明屏幕(以使您的法律部门满意),其中还将包含为创建游戏引擎和游戏资产而工作的各种程序员和新媒体工匠的学分。

您将开发的 InvinciBagel 游戏设计基础的下一个级别是 InvinciBagel 游戏的底层或背面(游戏用户看不到),游戏引擎组件 Java 类设计方面。其中包括一个游戏引擎,它将使用一个 Java FX . animation . animation timer 类来控制对游戏界面屏幕的游戏更新;一个精灵引擎,它将使用 Java 列表数组和集合来管理游戏精灵;碰撞引擎,当两个精灵之间发生碰撞时,它将进行检测并做出响应;一个物理引擎,它将把力和类似的物理模拟应用到游戏中,以便精灵加速并逼真地对重力做出反应;和一个演员引擎,它将管理 InvinciBagel 游戏中每个演员的特征。

最后,您将修改现有的 InvinciBagel.java 应用子类,为游戏播放屏幕和其他三个功能信息屏幕实现一个新的闪屏和按钮,为 InvinciBagel 游戏应用提供这些顶级 UI 特性和基础 UI 屏幕基础结构。这将最终让你进入一些 Java 和 JavaFX 编程,因为你为游戏创造了基础。

游戏设计基础:主要功能屏幕

你要设计的第一件事就是你的游戏用户将与之交互的顶级或者最高级别的用户界面。这些都将通过包含在主要 InvinciBagel 船级社代码中的 InvinciBagel splash(品牌)屏幕进行访问。如前所述,此 Java 代码将扩展 javafx.application.Application 类,并将启动应用,显示其闪屏,以及查看指令、玩游戏、查看高分或查看游戏的法律免责声明和游戏创作者(程序员、艺术家、作家、作曲家、声音设计师等)的选项。在图 6-1 中可以看到一个显示游戏的高级图表,从顶部的功能 UI 屏幕开始,向下发展到操作系统级别。

A978-1-4842-0415-3_6_Fig1_HTML.jpg

图 6-1。

Primary game functional screens and how they are implemented through Java and JavaFX API, using a JVM

这将需要向 StackPane 布局容器父节点添加另外三个按钮节点,并为闪屏背景图像容器添加一个 ImageView 节点。必须首先将 ImageView 节点添加到 StackPane 中,使其成为 StackPane 中的第一个子节点(z-order = 0),因为这个 ImageView 包含我称之为闪屏 UI 设计的背景板。因为它在背景中,所以图像需要在按钮 UI 控件元素的后面,这些元素的 z 顺序值为 1 到 4。

这意味着您将在应用的场景图中使用六个节点对象(一个父节点和五个子节点)来创建 InvinciBagel 闪屏!说明和信用屏幕将使用另一个 ImageView 节点,因此您已经有了六个节点,高分屏幕可能会使用另外两个(ImageView 和 TableView)节点,因此在您考虑为游戏屏幕添加节点之前,您可能会在场景图中有八个以上的节点来创建游戏支持基础结构,当然,这是您希望游戏获得最佳性能的地方。

如果你考虑一下,这真的没有那么糟糕,因为这些屏幕都是静态的,不需要更新,也就是说,它们包含的(UI)元素是固定的,不需要使用 脉冲系统更新,所以你应该仍然有 JavaFX 脉冲引擎的 99%的能力来处理 InvinciBagel 游戏 GamePlayLoop 引擎。事实上,随着 Java 8 和 JavaFX 8 继续提高其平台 API 和类的效率,您实际上可能会有更多的处理能力用于游戏(精灵运动、碰撞、物理、动画等),因此将处于良好的状态。

GamePlayLoop 将使用 javafx.animation 包及其 AnimationTimer 类为您处理游戏代码。你总是需要知道你要求脉冲引擎处理多少场景图节点对象,因为,如果这个数字变得太大,它将开始影响游戏的性能。

Java 类结构设计:游戏引擎支持

接下来,让我们来看看 InvinciBagel 游戏的功能结构将如何被整合,也就是说,在你的 Java 8 游戏编程代码中,这就是本书的全部内容!正面 UI 屏幕的外观和底层编程逻辑的外观之间确实没有关联,因为大多数编程代码都将用于在游戏屏幕上创建游戏体验。游戏说明和法律和信用屏幕将只是图像(ImageView),并将在图像中嵌入文本(导致使用更少的场景图形节点)或在 ImageView 的顶部合成一个透明的 TextView。高分屏幕将需要一点编程逻辑,这将在游戏开发的最后完成,因为游戏逻辑必须被创建和播放,才能在第一时间产生高分(参见第十七章)!

图 6-2 显示了完成 InvinciBagel 游戏所需的主要功能区组件。该图显示了位于层次顶部的 InvinciBagel 应用子类,它创建了顶层和场景,以及包含在它下面(或里面)的场景图。

A978-1-4842-0415-3_6_Fig2_HTML.jpg

图 6-2。

Primary game functional classes and how they are implemented under the Scene and Scene Graph levels

在 InvinciBagel 场景对象(实际上是在 InvinciBagel 应用子类中创建的)下面,是功能类的更广泛的结构设计,您将需要在本书的剩余部分中编写代码。图中所示的引擎(类)将创建您的游戏功能,如游戏引擎(游戏循环)、逻辑引擎(游戏播放逻辑)、精灵引擎(演员管理)、演员引擎(演员属性)、得分引擎(游戏得分逻辑)、动画引擎(动画逻辑)、碰撞检测和物理模拟。您将必须创建所有这些 Java 类函数来完全实现 InvinciBagel 游戏的全面的 2D 游戏引擎。

我称之为 GamePlayLoop 类的游戏引擎是创建 AnimationTimer 对象的主要类,该对象调用连续处理游戏循环的脉冲事件。如您所知,这个循环将调用。handle()方法,该方法又包含方法调用,这些方法调用最终将访问您将创建的用于管理 actors (sprite 引擎)的其他类;在屏幕上移动它们(演员引擎);检测任何碰撞(碰撞引擎);检测到碰撞后应用游戏逻辑(逻辑引擎);并应用物理的力量来为游戏提供逼真的效果,如重力和加速度(物理引擎)。

从第七章开始,你将构建这些不同的引擎,它们将被用来创造游戏体验。我将根据每个引擎和它们需要做什么来对章节主题进行分层,这样从学习和编码的角度来看,一切都是有逻辑的。

JavaFX 场景图设计:最小化 UI 节点

最小化场景图的技巧是使用尽可能少的节点来实现一个完整的设计,如图 6-3 所示,这可以通过一个 StackPane 根节点、一个 VBox 分支(父)节点和七个叶(子)节点(一个 TableView、两个 ImageView 和四个按钮 UI 控件)来实现。当你接下来开始编码场景图时(最后!),您将仅使用 14 个对象,仅导入 12 个类,来使您在上一节中设计的 InvinciBagel 游戏的整个顶层成为现实。TableView 将覆盖 ImageView 组合,其中包含设计的信息屏幕层。这个 TableView 对象将在游戏设计的后期添加。ImageView 背板将包含 InvinciBagel 艺术品;ImageView 合成层将包含三个不同的透明图像,根据 ActionEvents(按钮控件的点击)无缝覆盖背板图像;VBox 父 UI 布局容器将包含四个按钮控件。您还将创建一个 Insets 对象来保存填充值,以微调按钮库对齐方式。

A978-1-4842-0415-3_6_Fig3_HTML.jpg

图 6-3。

Primary splash screen Scene Graph node hierarchy, the objects it contains, and the assets it references

因为按钮对象不能单独定位,所以我不得不使用 HBox 类以及 Insets 类和 Pos 类来包含和定位按钮控件。在这一章中,我将介绍你将用于这个高级设计的类,这样你就可以对你将要添加到 InvinciBagel 类中来创建这个顶级 UI 设计的每个类有一个大概的了解。

我为匹配四个不同按钮所需的四个不同屏幕优化场景图形使用的方法是使用一个 ImageView 作为背板来包含 InvinciBagel 闪屏插图,然后再使用一个 ImageView 来包含使用透明度(alpha 通道)的不同合成图像(覆盖)。这样,您可以仅使用两个 ImageView 场景图形节点对象来模拟四个不同的屏幕。

最后,TableView 场景图节点将包含高分表的表结构。这将通过分数引擎创建,你将在最后创建,在你完成整个游戏设计和编程。现在,你将离开高分和玩游戏按钮代码未实现。

场景图代码:优化你当前的 InvinciBagel 类

我知道您渴望在 InvinciBagel 类代码上工作,所以让我们清理、组织和优化现有的 Java 代码来实现这个顶级 UI 屏幕设计。首先,将对象声明和命名 Java 代码放在 InvinciBagel 类的顶部。这更有组织性,你的类中的所有方法将能够看到和引用这些对象,而不需要使用 Java 修饰关键字。正如你在图 6-4 中看到的,这些包括你现有的场景 scene 对象,根 StackPane 对象,和 btn 按钮对象(我把它重命名为 gameButton)。我添加了另外三个按钮对象,分别名为 helpButton、scoreButton 和 legalButton,它们都是使用一行 Java 代码声明和命名的,还添加了两个 ImageView 对象,分别名为 splashScreenbackplate 和 splashScreenTextArea。您还需要创建四个 Image 对象来保存数字图像资产,这些资产将显示在 ImageView 节点中;我已经用一个复合 Java 语句将它们命名为 splashScreen、instructionLayer、legalLayer 和 scoresLayer 并声明了它们。最后,声明并命名 buttonContainer VBox 对象和 buttonContainerPadding Insets 对象。只要您使用 Alt+Enter 快捷键,选择正确的 javafx 包和类路径,NetBeans 就会为您编写导入语句。进口显示在图的顶部。

A978-1-4842-0415-3_6_Fig4_HTML.jpg

图 6-4。

Declaring and naming the 14 objects that will make up your Scene Graph hierarchy at the top of the class

在本章中,您将详细了解所有这些 JavaFX 类,以便了解它们的用途以及它们能为您的 Java 应用做些什么。

场景图设计:简化现有的。start()方法

现在,您可以优化。start()方法,这样它只有一二十行代码。首先,将场景图节点创建 Java 例程模块化到它们自己的 createSplashScreenNodes()方法中,该方法将在。start()方法,如图 6-5 所示。在此方法中创建所有节点后,创建一个 addNodesToStackPane()方法将节点添加到 StackPane 根节点,然后让三行 primaryStage 代码配置和管理 Stage 对象,最后是 ActionEvent 处理代码例程,这些例程将按钮 UI 控件“连接”到单击按钮时要执行的 Java 代码。

A978-1-4842-0415-3_6_Fig5_HTML.jpg

图 6-5。

Organize the .start() method with the createSplashScreenNodes() and addNodesToStackPane() methods

如您所见,在复制了。对于每个按钮对象,当您折叠 EventHandler 例程时,您有九行代码:一行用于创建节点,一行用于向根添加节点,三行用于 Stage 对象设置,四行用于 UI 按钮事件处理。如果你考虑到你在游戏结构顶层增加的功能数量(游戏、指令、法律、积分、记分牌),这是非常紧凑的。

按照正确的顺序做事很重要,因为一些 Java 代码是基于其他 Java 代码的。因此,对象声明排在第一位;然后在。start()方法,创建(实例化)节点。一旦声明、命名和实例化(创建)了它们,就可以将它们添加到 StackPane 根节点,然后配置(使用。setTitle()方法)并将场景 scene 对象添加到 primaryStage Stage 对象中。setScene()方法。在你的对象进入系统内存之后,你才能够处理 ActionEvent 处理例程,这些例程被附加到你的四个按钮 UI 控件上。接下来,让我们确保将在 createSplashScreenNodes()方法中引用的数字图像资产位于正确的 NetBeans 文件夹中。

场景图形资源:在项目中安装 ImageView 的图像资源

要在 Java 代码中引用 JAR 文件中的数字图像资产,必须在文件名前插入一个正斜杠。但是,在引用文件之前,您必须将这些图像文件从图书存储库中复制到计算机/计算机名/用户/用户/my documents/netbeans projects/InvinciBagel/src 文件夹中,如图 6-6 的左侧(和顶部)所示。您还可以看到这些数字图像资产是如何合成的,因为 invincibagelsplash PNG24 的背景板上有一个位置可以覆盖其他三个 PNG32 图像。复合 ImageView 资产中看到的白色区域实际上是透明的!现在,你准备好了!

A978-1-4842-0415-3_6_Fig6_HTML.jpg

图 6-6。

Windows 7 Explorer file management utility, showing a PNG24 splash screen and three PNG32 overlays

JavaFX UI 类:HBox、Pos、Insets 和 ImageView

让我们从编码中休息一下,深入了解一些新的类,您将使用它们来完成您的顶级游戏应用 UI 设计。其中包括 Pos 类(定位);Insets 类(填充);HBox 类(UI 布局容器);图像类(数字图像容器);ImageView 类(数字图像显示);以及 TableView 类(表格数据显示),您将在这里学习该类,但在游戏完全完成后,您将在游戏开发的后续代码中实现该类。您将按照从最简单(Pos)到最复杂(TableView)的顺序检查这些,然后编写。createSplashScreenNodes()和。addNodesToStackPane()方法,它们使用这些新的类(对象)。

JavaFX Pos 类:通用屏幕位置常量

Pos 类是一个 Enum 类,代表“枚举”这个类包含一个常量列表,这些常量被转换成整数值以在代码中使用。常量值(在本例中是定位常量,如 TOP、CENTER 和 BASELINE)使程序员更容易在代码中使用这些值。

Pos 类的 Java 类扩展层次结构从 java.lang.Object masterclass 开始,逐步发展到 java.lang.Enum 类,最后以 javafx.geometry.Pos 类结束。如图 6-4 所示(l. 6),Pos 位于 JavaFX geometry 包中,并使用以下子类层次结构:

java.lang.Object

> java.lang.Enum<Pos>

> javafx.geometry. Pos

Pos 类有一组常量,用于提供通用的水平和垂直定位和对齐(见表 6-1 )。正如您将在下一节中看到的,您将不得不使用 Insets 类和对象来获得您想要的像素精确定位。您将使用 BOTTOM_LEFT 常量将按钮控件组定位在初始屏幕的左下角。

表 6-1。

Pos Class Enum Constants That Can Be Used for Positioning and Alignment in JavaFX

位置常数 定位结果(对象)
基线 _ 中心 在基线上,垂直地;在中心,水平地
基线 _ 左侧 在基线上,垂直地;在左边,水平地
基线 _ 右侧 在基线上,垂直地;在右边,水平地
底部中心 在底部,垂直地;在中心,水平地
左下角 在底部,垂直地;在左边,水平地
右下 在底部,垂直地;在右边,水平地
中心 在中心,垂直和水平
中央 _ 左侧 在中心,垂直地;在左边,水平地
中间 _ 右侧 在中心,垂直地;在右边,水平地
顶部 _ 中间 在顶部,垂直地;在中心,水平地
左上角 在顶部,垂直地;在左边,水平地
右上方 在顶部,垂直地;在右边,水平地

因为 Pos 类提供了一般化的定位,所以它应该与 Insets 类结合使用,以实现像素级的精确定位。接下来让我们看看 Insets 类,因为它也在 javafx.geometry 包中。

JavaFX Insets 类:为用户界面提供填充值

insets 类是一个公共类,它直接扩展了 java.lang.Object masterclass,这意味着 Insets 类是从头开始编写的,以提供矩形区域内的 Insets 或 offsets。想象一个相框,在里面放一块垫子,或者在相框外面和里面的图片之间放一个漂亮的边框。这就是 insets 类用两个构造函数方法做的事情:一个提供相等或均匀的 insets,另一个提供不相等或不均匀的 Insets。

您将使用提供不相等 insets 值的构造函数,如果您正在构建一幅图片,这将看起来非常不专业!Insets 类的 Java 类层次结构从 java.lang.Object 主类开始,并使用该类创建 javafx.geometry.Insets 类。如图 6-4 所示(l. 5),Insets 包含在 JavaFX geometry 包中,就像 Pos 类一样,并使用以下类层次结构:

java.lang.Object

> javafx.geometry. Insets

Insets 类提供了一组四个双偏移值,用于指定矩形的四个边(上、右、下、左),在构造函数方法中应该按照这个顺序指定。您将使用 Insets 类(对象)来微调按钮控件组的位置,您将使用 HBox 布局容器来创建该控件组。将这些 Insets 对象视为在另一个框内绘制一个框的方式,它显示了您希望矩形内的对象围绕其边缘“尊重”的间距。Insets 对象的简单构造函数将使用以下格式:

Insets(double``topRightBottomLeft

此构造函数对所有间距边使用单个值(topRightBottomLeft),重载的构造函数允许您分别指定每个值,如下所示:

Insets(double``top``, double``right``, double``bottom``, double``left``)

这些值需要按此顺序指定。记住这一点的简单方法是使用模拟时钟。钟的顶部有“12”,右边有“3”,底部有“6”,左边有“9”。所以,从正午开始(对于你们这些西方流派的爱好者来说),总是顺时针工作,就像指针绕着钟面移动一样,你将有一个很好的方法来记住如何在“不均匀值”构造方法中指定 Insets 值。您将很快使用 insets 类来定位按钮控件组,该控件组最初“卡”在初始屏幕设计的左下角,远离屏幕的左侧和底部,使用这四个 Insets 定位参数中的两个。

JavaFX HBox 类:在设计中使用布局容器

因为按钮对象不容易定位,所以我将把这四个按钮对象放在 javafx.scene.layout 包中的一个布局容器中,该包名为 HBox,代表水平框。这个公共类将事情安排在一行中,因为您希望按钮在闪屏的底部对齐,所以您使用四个按钮控件节点的父节点,这些节点将成为这个 HBox 分支节点的子节点(叶节点)。这将创建一组 UI 按钮,它们可以作为闪屏设计的一个单元一起定位(移动)。

HBox 类是一个公共类,它直接扩展 javafx.layout.Pane 超类,后者又扩展 javafx.layout.Region 超类。javafx.layout.Region 超类扩展了 javafx.scene.parent 超类,后者又扩展了 javafx.scene.Node 超类,后者扩展了 java.lang.Object 主类。如图 6-4 所示(l. 11),HBox 包含在 javafx.scene.layout 包中,就像 StackPane 类一样,它使用如下的类层次结构:

java.lang.Object

> javafx.scene.Node

> javafx.scene.Parent

> javafx.scene.layout.Region

> javafx.scene.layout.Pane

> javafx.scene.layout. HBox

如果 HBox 指定了边框或填充值,HBox 布局容器的内容将遵循该边框或填充规范。填充值是使用 Insets 类指定的,您将在这个微调的 UI 控件库应用中使用它。

您将使用 HBox 类(object ),以及 Pos 类常量和 Insets 类(object ),将 UI 按钮对象分组在一起,然后,微调它们作为按钮控件库的位置。因此,这个 HBox 布局容器将成为按钮 UI 控件(或叶节点)的父节点(或分支节点)。

可以把 HBox 对象想象成一种水平排列子对象的方式。这些可能是您的图像资产,它将使用基本的 HBox 构造函数(零间距),或者 UI 控件,如按钮,它们排列在一起但有间距,使用重载的构造函数之一。创建 HBox 对象的最简单的构造函数将使用下面的空构造函数方法调用格式:

HBox()

您将用于创建 HBox 对象的重载构造函数将使用一个间距值在 HBox 内的子按钮对象之间留出一些空间,使用以下构造函数方法调用格式:

HBox(double``spacing

还有另外两种重载构造函数方法调用格式。这将允许您在构造函数方法调用本身中指定子节点对象(在本例中为按钮对象),如下所示:

HBox(double``spacing``, Nodes...``children``) - or, with``zero spacing

HBox(Nodes...``children

你将会使用“长表格”。getChildren()。addAll()方法链,但是您也可以通过使用以下构造函数来声明 HBox 及其按钮节点对象:

HBox buttonContainer = new HBox(12, gameButton, helpButton, scoreButton, legalButton);

如果子对象被设置为可调整大小,HBox 布局容器将基于不同的屏幕大小、纵横比和物理分辨率来控制子元素的大小调整。如果 HBox 区域将容纳子对象的首选宽度,它们将被设置为该值。此外,fillHeight 属性(布尔变量)设置为 true 作为默认值,指定子对象是否应该填充(放大)HBox 高度值。

HBox 的对齐是由 Alignment 属性(属性或变量)控制的,该属性默认为 Pos 类(Pos)中的 TOP_LEFT 常量。TOP_LEFT)。如果 HBox 的大小超过了其指定的宽度,子对象将使用它们的首选宽度值,多余的空间不会被使用。值得注意的是,HBox 布局引擎将对托管子元素进行布局,而不考虑它们的可见性属性(属性或变量)设置。

现在,我已经讨论了 JavaFX geometry 和 layout 类,您将使用它们来创建 UI(一组按钮对象)设计,让我们来看看 javafx.scene.image 包中与数字图像相关的类,它将允许您实现数字图像合成管道,您将在 HBox UI 布局容器对象中保存的这四个 JavaFX 按钮 UI 控件元素对象的后面放置该管道。

JavaFX Image 类:在设计中引用数字图像

Image 类是一个公共类,它直接扩展了 java.lang.Object masterclass,这意味着 Image 类也是从头开始编写的,以提供图像加载(引用)和缩放(调整大小)。您可以锁定缩放的纵横比,并指定缩放算法(质量)。支持 java.net.URL 类支持的所有 URL。这意味着你可以从网上加载图片(www.servername.com/image.png);从 OS(文件:image . png);或者从 JAR 文件中,使用正斜杠(/image.png)。

Image 类的 Java 类层次结构从 java.lang.Object 主类开始,并使用该类创建 javafx.scene.image.Image 类。如图 6-4 所示(l. 9),Image 包含在 JavaFX image 包中,就像 ImageView 类一样,使用如下的类层次结构:

java.lang.Object

> javafx.scene.image. Image

Image 类提供了六种不同的(重载的)Image()构造函数方法。这些函数从简单的 URL 到一组指定 URL、宽度、高度、比例、平滑和预加载选项的参数值。当您使用所有构造函数方法中最复杂的方法编写 Image()构造函数时,您将很快看到,这些方法应该在构造函数方法中按此顺序指定,其格式如下:

Image(String``url``, double``requestedWidth``, double``requestedHeight``, boolean``preserveRatio,``boolean``smooth,``boolean``backgroundLoading``)

Image 对象的简单构造函数仅指定 URL,并使用以下格式:

Image(String``url

如果您要加载图像,并让构造函数方法将图像缩放到不同的宽度和高度(通常较小,以节省内存),同时使用最高质量的重新采样(平滑像素缩放)锁定(保留)纵横比,则该图像对象构造函数使用以下格式:

Image(String``url``, double``scaleWidth``, double``scaleHeight``, boolean``preserveAspect``, boolean``smooth``)

如果希望在后台(异步)加载图像,使用其“本机”或物理分辨率和本机纵横比,image()构造函数使用以下格式:

Image(String``url``, boolean``backgroundLoading

两个 Image()构造函数方法也使用 java.io.InputStream 类,该类向 Image()构造函数方法提供输入数据的实时流(类似于视频或音频流,只针对 java 应用进行了定制)。这两种图像对象构造器格式采用以下格式(简单和复杂):

Image(InputStream``is

Image(InputStream``is,``double``newWidth``, double``newHeight``, boolean``preserveAspect``, boolean``smooth``)

因此,Image 类(对象)用于准备数字图像资产以供使用,即从 URL 读取其数据;如果有必要,调整它们的大小(使用你喜欢的平滑和纵横比锁定);并异步加载它们,而应用中的其他事情正在进行。需要注意的是,图像类(或对象)并不显示图像资产:图像类只是加载它;如果需要,缩放它;并将它放在系统内存中供您的应用使用。

要显示图像对象,您需要使用第二个类(对象),称为 ImageView 类。ImageView 对象可以用作场景图和引用上的节点,然后将图像对象数据“绘制”到布局容器上,该容器保存 ImageView 节点(在这种情况下,是叶 ImageView 节点的 StackPane 场景图根和父节点)。我将在下一节介绍 ImageView 类。

从数字图像合成的角度来看,StackPane 类(对象)是图像合成引擎,也可以称为层管理器,每个 ImageView 对象代表层堆栈中的一个层。图像对象包含图像视图层中的数字图像数据,或者如果需要,包含多个图像视图中的数字图像数据,因为图像对象和图像视图对象是分离的,并且彼此独立存在。

JavaFX ImageView 类:在设计中显示数字图像

ImageView 类是一个公共类,它直接扩展 javafx.scene.Node 超类,后者是 java.lang.Object 的扩展(参见第四章)。因此,ImageView 对象是 JavaFX 场景图中的一种节点对象,用于使用 Image 对象中包含的数据绘制视图。该类具有允许图像重采样(调整大小)的方法,并且与 image 类一样,您可以锁定缩放的纵横比以及指定重采样算法(平滑质量)。

ImageView 类的 Java 类层次结构从 java.lang.Object 主类开始,并使用该类创建 javafx.scene.Node 类,然后使用该类创建 ImageView 节点子类。如图 6-4 所示(l. 10),和 Image 类一样,ImageView 包含在 JavaFX image 包中。ImageView 类使用以下 Java 类继承层次结构:

java.lang.Object

> javafx.scene.Node

> javafx.scene.image. ImageView

ImageView 类提供了三种不同的(重载的)ImageView()构造函数方法。这些范围从一个空的构造函数(这是您稍后将在代码中使用的一个构造函数);转换为一个将图像对象作为其参数的函数;转换为以 URL 字符串对象作为参数并自动创建图像对象的方法。要创建 ImageView 对象,简单(空)ImageView()构造函数方法使用以下格式:

ImageView()

您将使用这个构造函数方法,这样我就可以向您展示如何使用?setImage()方法调用将图像对象加载到 ImageView 对象中。如果您想避免使用。setImage()方法调用时,可以使用重载的构造函数方法,其格式如下:

ImageView(Image``image

因此,要“显式”设置一个 ImageView 并将其连接到 Image 对象,如下所示:

splashScreenBackplate =``new

splashScreenBackplate.setImage(``splashScreen

您可以使用重载的构造函数方法将其压缩成一行代码,结构如下:

splashScreenBackplate =``new``ImageView(``splashScreen

如果您想绕过创建和加载图像对象的过程,也有一个构造函数方法,它使用以下格式:

ImageView(String``url

要在后台(异步)加载图像,使用其本机(默认)分辨率和本机纵横比,image()构造函数使用以下格式:

splashScreen = new Image("/invincibagelsplash.png", 640, 400, true, false, true);

splashScreenBackplate = new ImageView();

splashScreenBackplate.setImage(``splashScreen

如果不想指定图像尺寸、背景图像加载和平滑缩放,或者不想锁定任何缩放的纵横比,可以将前面三行 Java 代码压缩到以下构造函数中:

splashScreenBackplate = new ImageView("/invincibagel.png");   // uses third constructor method

至少在开始时(出于学习目的),我会用长时间的方式来做这件事,我会一直使用 Image()构造函数方法显式地加载图像对象,这样您就可以指定所有不同的属性,并看到您在这个 Java 编程逻辑中使用的所有不同的图像资产。我想在这里向您展示快捷方式代码,因为一旦您开始使用 ImageViews 作为精灵,您将在本书的后面使用这种方法(参见第八章)。你可以对你的精灵使用这种快捷方式,因为你不需要缩放它们,也因为它们已经高度优化,不需要后台加载。

接下来,让我们快速看一下 TableView 类,它将保存高分表。虽然你不会在这里实现它,但我会介绍这个类,因为它是你在本章中创建和实现的顶级 UI 设计的一部分。

JavaFX TableView 类:在设计中显示数据表

TableView 类是直接扩展 javafx.scene.control.Control 超类的公共类,javafx.scene.layout.Region 是 javafx.scene.layout.Region 的扩展,javafx.scene.Parent 是 javafx.scene.Node 场景图超类的扩展(见第四章)。因此,TableView < S >对象是一种 UI 控件(表格)和 JavaFX 场景图中的节点对象,用于使用 S 对象构建表格,每个对象都包含要在表格中显示的数据。在本书的后面部分,你将使用这些 S 对象将数据写入一个 TableView < S >对象,在得分超过当前列表中的得分之后。

TableView 类的 Java 类层次结构从 java.lang.Object 主类开始,并使用该类创建 javafx.scene.Node 类,然后使用该类创建父类。这用于创建一个区域类,该区域类又创建一个控件类,该控件类用于创建 TableView 类。TableView 类具有以下 Java 类继承层次结构:

java.lang.Object

> javafx.scene.Node

> javafx.scene.Parent

> javafx.scene.layout.Region

> javafx.scene.control.Control

> javafx.scene.control. TableView<S>

TableView 类提供了两种不同的(重载的)TableView()构造函数方法,一种是空的构造函数,另一种是接受 ObservableList 对象的构造函数,该对象以表数据项作为参数。创建空 TableView 对象的简单(空)TableView()构造函数方法将使用以下格式:

TableView()

第二种构造函数类型使用 javafx.collections 包中的 ObservableList 类(object ),这是一种列表类型,它允许数据更改事件侦听器跟踪列表中发生的任何更改。此 TableView 对象构造函数方法调用使用以下格式:

TableView(ObservableList<S>``items

我认为现在已经有足够的类背景信息了,所以让我们开始为你的第一个类编写代码。createSplashScreenNodes()方法,该方法将实例化并设置场景图形的所有节点对象!

场景图形节点:。createSplashScreenNodes()

使用 createSplashScreenNodes()方法要做的第一件事是编写空方法结构的代码,并添加已经存在于引导代码中的节点对象创建代码,引导代码是 NetBeans 在第二章中为您生成的。这包括按钮节点的节点对象、StackPane 根节点和名为 Scene 的场景对象。您将在。start()方法,因为该对象是使用。start(Stage primaryStage)构造函数方法调用。Button 对象已经被重命名为 gameButton(原来是 btn),所以您有三行对象实例化代码和一行配置代码,如下所示:

root = new StackPane();

scene = new Scene(``root

gameButton = new Button();

gameButton.setText("PLAY GAME");

需要注意的是,因为在场景 scene 对象的构造函数方法调用中使用了根 StackPane 对象,所以这一行代码需要放在前面(在使用它之前,必须先创建您的根对象!).接下来您需要创建的是 HBox 布局容器对象,它将保存您的四个按钮 UI 控件。您还将为 HBox 设置对齐属性;添加一个 Insets 对象以包含填充值;然后使用这四行 Java 代码将这些填充添加到 4 HBox 对象中:

buttonContainer = new HBox(``12

buttonContainer.setAlignment(Pos.``BOTTOM_LEFT

buttonContainerPadding = new Insets(0, 0, 10, 16);

buttonContainer.setpadding(``buttonContainerPadding

接下来,让我们采用一种方便的程序员快捷方式,将两行 gameButton(实例化和配置)代码复制并粘贴到 HBox 代码下面(因为按钮在 HBox 内部,这只是为了视觉组织,而不是为了使代码工作),然后在单独的行上再复制并粘贴三次。这将允许您通过创建以下四个按钮 Java 代码,分别将游戏更改为帮助、得分和合法:

gameButton = new Button();

gameButton.setText("PLAY GAME");

helpButton = new Button();

helpButton.setText("INSTRUCTIONS");

scoreButton = new Button();

scoreButton.setText("HIGH SCORES");

legalButton = new Button();

legalButton.setText("LEGAL & CREDITS");

现在您已经创建了 HBox 按钮 UI 控件布局容器和按钮,您还需要再编写一行代码,使用。getChildren()。addAll()方法链,像这样:

buttonContainer.``getChildren``().``addAll

接下来,让我们添加您的图像合成节点对象(image 和 ImageView ),以便您可以为 InvinciBagel 闪屏添加插图,以及装饰您的说明、法律免责声明和制作人员名单的面板覆盖图,并为您的(最终)游戏高分表添加背景和屏幕标题。我使用两个 ImageView 对象来包含这两层;让我们首先设置最底部的背板图像层,方法是使用以下 Java 代码实例化 image 对象,然后实例化 ImageView 对象,并将它们连接在一起:

splashScreen = new Image("/invincibagelsplash.png", 640, 400, true, false, true);

splashScreenBackplate = new ImageView();

splashScreenBackplate.setImage(splashScreen);   // this Java statement connects the two objects

最后,让我们对图像合成板做同样的事情,也就是说,ImageView 将保存包含 alpha 通道(透明度)值的不同图像对象,这些图像对象将为 InvinciBagel 闪屏插图(由才华横溢的 2D 艺术家 Patrick Harrington 创建)创建面板图像的覆盖图:

instructionLayer = new Image("/invincibagelinstruct.png", 640, 400, true, false, true);

splashScreenTextArea = new ImageView();

splashScreenTextArea.setImage(instructionLayer); // this Java statement connects the two objects

如图 6-7 所示,你的场景图节点创建(见 InvinciBagel 类的顶部)和节点对象实例化和配置(见 createSplashScreenNodes()方法)已经就绪并且没有错误。您仍然需要为其他两个屏幕添加图像对象,但是这里有足够的代码能够使用 addNodesToStackPane()方法将这些节点对象添加到场景图中,然后测试代码以确保它能够工作。根据 NetBeans IDE,此代码没有错误!

A978-1-4842-0415-3_6_Fig7_HTML.jpg

图 6-7。

Coding the createSplashScreenNodes() method; instantiating and configuring the nodes in the Scene Graph

接下来,让我们在 addNodesToStackPane()方法中将节点对象添加到 StackPane 场景图形根对象中。

向场景图添加节点:。addStackPaneNodes()

最后,您必须创建一个方法,将您已经创建的节点对象添加到场景图形根,在本例中是一个 StackPane 对象。您将使用。getChildren()。add()方法链将子节点对象添加到父 StackPane 根场景图节点。这是通过三行简单的 Java 代码完成的,如下所示:

root.getChildren().add(splashScreenBackplate);

root.getChildren().add(splashScreenTextArea);

root.getChildren().add(buttonContainer);

正如你在图 6-8 中看到的,Java 代码是没有错误的,根对象在类的顶部看到了它的声明。单击代码中的根对象会创建这种突出显示,它通过代码跟踪对象的使用。这是一个非常酷的 NetBeans 8.0 技巧,当您想要跟踪代码中的对象时,应该使用它。

A978-1-4842-0415-3_6_Fig8_HTML.jpg

图 6-8。

Coding the addNodesToStackPane() method, using the .getChildren() method chained to the .add() method

这里要注意的重要事情是节点对象添加到 StackPane 根场景图形对象的顺序。这会影响图像合成的合成层顺序,以及这些数字图像元素之上的 UI 元素合成。添加到 StackPane 的第一个节点将位于层堆栈的底部;这需要是 splashScreenBackplate ImageView 节点对象,如图所示。

下一个要添加的节点将是 splashScreenTextArea ImageView 节点对象,因为带有面板覆盖的透明图像需要放在 Pat Harrington 的 InvinciBagel 闪屏 2D 作品的正上方。之后,您可以放置 UI 设计,在本例中,可以使用 buttonContainer HBox 节点对象一次性完成,该节点对象包含所有的按钮对象。请注意,您不必向这个 StackPane 根场景图形对象添加按钮,因为您已经使用了。getChildren()。addAll()方法链,用于将按钮 UI 控件添加到 HBox(父对象)节点分支对象下的场景图形层次中。现在,您可以开始测试了!

测试 InvinciBagel 应用:脉动场景图形

单击 NetBeans IDE 顶部的绿色播放箭头,然后运行项目。这将弹出如图 6-9 所示的窗口(我已经删除了在第四章中添加的 Java 代码,演示如何创建一个无窗口应用)。所以,你又有了 windows“chrome ”,至少现在是这样。正如您所看到的,您只使用了十几条 import 语句(外部类)、几十行 Java 代码和场景图根(StackPane)对象下的六个子节点,就获得了非常好的结果。如您所见,JavaFX 在将背板、合成图像覆盖和按钮库覆盖合成为一个无缝、专业的结果方面做得非常好!

A978-1-4842-0415-3_6_Fig9_HTML.jpg

图 6-9。

Run the InvinciBagel application, and make sure that the StackPane compositing class is working correctly

因为您只复制和粘贴了每个按钮的 EventHandler 例程,并且更改了按钮对象的名称,而没有更改这些例程中的代码,所以按钮对象仍然可以正常工作(将文本写入控制台)并且不会导致编译器错误。但是,他们不会做您希望他们做的事情,即更改图像覆盖,以便设计左侧的面板包含您希望它向用户显示的标题和文本。

这将通过调用。setImage()方法,该方法将根据用户单击的按钮 UI 控件,将 splashScreenTextArea ImageView 对象设置为 instructionLayer、scoresLayer 或 legalLayer Image 对象。在添加最后两个 Image 对象实例之前,您不能实现这个事件处理代码!

完成 InvinciBagel UI 屏幕设计:添加图像

让我们以 createSplashScreenNodes()方法结束,在该方法的末尾再添加两行,以添加两个图像对象,引用 invincibagelcreds.png 和 invincibagelscores.png 的 32 位 PNG32 数字图像资产。这是通过使用下面两行使用 new 关键字的 Java 对象实例化代码来实现的:

legalLayer = new Image( "/invincibagelcreds.png", 640, 400, true, false, true );

scoresLayer = new Image( "/invincibagelscores.png", 640, 400, true, false, true );

如图 6-10 所示,代码是没有错误的,因为你已经将四个 PNG 文件复制到你的项目的/src 文件夹中。您不需要其他代码行(ImageView 对象实例化,。setImage()),因为您将使用 splashScreenTextArea ImageView 对象来保存这最后两个图像对象。因此,您可以节省所使用的场景图节点对象,因为您使用单个 ImageView 场景图节点对象来基于按钮事件显示三个不同的图像对象(覆盖)。

A978-1-4842-0415-3_6_Fig10_HTML.jpg

图 6-10。

Adding legalLayer and scoresLayer Image object instantiations to add the other image composite plates

这意味着,您将对 splashScreenTextArea ImageView 对象进行的 splashScreenTextArea.setImage()方法调用将被放置在三个按钮对象的 ActionEvent EventHandler 编程构造中,这三个按钮对象在被单击时会触发图像合成覆盖。第四个按钮对象将启动游戏,所以现在,在按钮事件处理结构中只有一个 Java 注释,使它成为一个“空”逻辑结构。现在,让我们看看如何完成这些 EventHandler 构造的编码,这样您就可以完成这个 UI 设计,并继续创建前面提到的游戏引擎。

交互性:连接 InvinciBagel 按钮以供使用

在所有这些重复的按钮事件处理结构中,您需要用对。setImage()方法,以便您可以将图像合成板 ImageView 设置为 Image 对象,该对象包含您想要覆盖由 Pat Harrington 创建的 InvinciBagel 背板作品的数字图像资产。您已经在 createSplashScreenNodes()方法中编写了两次这个代码结构,所以如果您想要一个快捷方式,您可以将代码行直接复制到您刚刚编写的两个 Image 对象实例化之上。

那个。因此,setOnAction()事件处理 Java 代码结构如下所示:

helpButton .setOnAction(new EventHandler<ActionEvent>() {

@Override

public void handle(ActionEvent event) {

splashScreenTextArea.setImage(``instructionLayer

}

});

scoreButton .setOnAction(new EventHandler<ActionEvent>() {

@Override

public void handle(ActionEvent event) {

splashScreenTextArea.setImage(``scoresLayer

}

});

legalButton .setOnAction(new EventHandler<ActionEvent>() {

@Override

public void handle(ActionEvent event) {

splashScreenTextArea.setImage(``legalLayer

}

});

如图 6-11 所示,您的事件处理代码没有错误,您已经准备好再次运行和测试了!

A978-1-4842-0415-3_6_Fig11_HTML.jpg

图 6-11。

Modify the body of the .handle() method for each of four Button controls to complete the infrastructure

如您所见,您暂时将 gameButton.setOnAction()事件处理结构保留为空;在下一章中,您将创建主要的游戏界面和一个脉冲事件处理引擎(结构),该引擎将通过调用您将在本书过程中编写的各种功能引擎来运行该游戏。

您现在也将高分屏幕的底部保留为空白,以便您可以在场景图形根堆栈窗格中用 TableView 节点对象覆盖两个 ImageView 层。在开发完 Java 8 游戏之后,您将完成高分按钮 UI 元素的复合。

现在,是时候对游戏应用的顶层 UI 部分进行最终测试了,以确保所有的 UI 按钮元素(对象)都能正常工作,并按照您的设计(编码)完成它们的功能。之后,您将再次运行 NetBeans 8.0 Profiler,以确保您刚刚创建的场景图形层次结构确实为您将从现在开始创建的游戏引擎留出了 99%的可用 CPU 处理能力。

测试最终的 InvinciBagel UI 设计

再次单击 NetBeans IDE 8.0 顶部的绿色播放箭头,然后运行您的项目。这将调出如图 6-12 所示的窗口。正如您所看到的,当您单击“法律和学分”按钮 UI 元素时,该覆盖图与 InvinciBagel artwork 背板无缝合成,如图左侧所示;当您单击“高分”按钮 UI 元素(控件)时,高分表格背景将就位,如图右侧所示。如您所见,javafx.image 包中的类提供了关于合成的原始结果

A978-1-4842-0415-3_6_Fig12_HTML.jpg

图 6-12。

The other two Image objects shown composited, using the background plate and compositing ImageViews

接下来,让我们来看看你在本章中编写的场景图实现占用了多少 CPU 周期,因为你想确保 100%静态的顶级 UI 设计,以便游戏中使用的唯一动态元素是游戏引擎(和相关引擎)本身。因为脉冲分辨率引擎遍历场景图层次可能会变得“昂贵”,所以这里需要非常小心!

请记住,您的主要目标是创建一个顶级的 UI 设计,用于启动游戏播放屏幕和循环,同时还实现一个允许您的用户显示说明、法律免责声明和制作人员名单的 UI,并负责设置一个用于显示高分表的区域。同时,您的任务是节省 99%的处理能力供以后使用,通过 JavaFX 脉冲引擎处理游戏逻辑、精灵运动、精灵动画、碰撞检测、得分和物理。

剖析 InvinciBagel 场景图的脉冲效率

重要的是,游戏 UI 设计不要从 CPU 中取走任何处理能力,因为游戏引擎将需要所有的处理能力。如图 6-13 所示,您可以使用 Profile>Profile Project(InvinciBagel)菜单序列来运行 Profiler,并对当前(顶层 UI)应用的 CPU 统计数据进行截图。

A978-1-4842-0415-3_6_Fig13_HTML.jpg

图 6-13。

Profiling the Scene Graph UI design thus far to make sure that it does not use any perceptible CPU cycles

正如您在图的右侧的 Total Time 列中所看到的,createSplashScreenNodes()方法需要 279 毫秒,或者大约十分之三秒的时间来执行,并且您的场景图被创建。执行 addNodesToStackPane()方法大约需要 3 毫秒,即千分之三秒。

如果您查看线程分析输出并单击 UI 按钮元素,您将会看到线程上出现一个彩色点,显示了按钮单击的处理开销,正如您所看到的,每次单击不到十分之一秒(查看最右边的调用列,了解我测试了多少次按钮单击函数)。我突出显示了 threads 视图,在那里我单击了 High Scores,然后是 Legal 和 Credits 按钮 UI 元素(参见图 6-14 )。正如您在这个视图中看到的,当前的设计使用了最少的资源。

A978-1-4842-0415-3_6_Fig14_HTML.jpg

图 6-14。

Profiling the Scene Graph UI design thus far to make sure that it does not use any perceptible thread overhead

Java 8 及其 JavaFX 引擎衍生了近十几个线程,所以你的游戏应用已经是高度多线程的了,甚至不需要在这个时间点!Oracle 的团队正在努力使 JavaFX 成为首屈一指的游戏引擎,所以性能会越来越好,这对 Java 8 游戏开发者来说是个好消息!

摘要

在第六章中,您已经开始着手为您的游戏进行实际的顶级 UI 设计,概述底层游戏引擎组件设计,并找出最有效的场景图节点设计。然后,您回到 Java 8 游戏编程,并重新设计了您现有的引导 Java 8 代码,这些代码最初是由 NetBeans 8.0 创建的。

因为 NetBeans 生成的 Java 代码设计并不适合您的目的,所以您完全重写了它,使它更有条理。这是通过创建两个自定义 Java 方法来实现的。createSplashScreenNodes()和。addNodesToStackPane(),用于模块化场景图节点创建过程,以及将三个父(和叶)节点对象添加到场景图根(在本例中,是 StackPane 对象,它用于其多层 UI 对象合成功能)。

接下来,您从 javafx.geometry 包中了解了一些用于实现这些新方法的 JavaFX 类,包括 Pos 类和 Insets 类;javafx.scene.image 包中的 Image 和 ImageView 类;HBox 类,来自 javafx.scene.layout 包;以及 javafx.scene.control 包中的 TableView 类。你编写了新的。createSplashScreenNodes()方法,该方法使用 Insets 对象、Image 和 ImageView 对象以及四个 Button 对象来实例化和配置 HBox 对象。一旦实例化和配置了所有这些场景图节点,您就可以编写一个。addNodesToStackPane()方法将节点对象添加到 StackPane 根对象,以便它们可以由 Stage 对象显示,Stage 对象引用场景图形的根对象。接下来,您测试了您的顶级游戏应用 UI 设计。然后,添加最后几个图像对象,并添加 ActionEvent EventHandler 程序逻辑。最后,您对应用进行了概要分析,以确保它是高效的。

在下一章中,我将介绍 JavaFX 脉冲引擎和 AnimationTimer 类,以便您可以为 Java 8 游戏引擎创建基础结构,该引擎将实时处理您的游戏事件。

七、游戏循环的基础:JavaFX 脉冲系统和游戏处理架构

现在,您已经为您的用户创建了学习如何玩游戏、开始游戏、查看高分以及查看法律免责声明和 Ira H. Harrison Rubin 的 InvinciBagel 知识产权游戏制作致谢名单所需的顶级 UI 屏幕,让我们进入正题,为您的 InvinciBagel 游戏创建游戏播放计时循环。从用户体验的角度来看,这是最重要的,并且对于您将在本书剩余部分创建的不同游戏引擎的正常运行也是至关重要的,包括精灵引擎、碰撞检测引擎、动画引擎、评分引擎和物理引擎。你将永远记住游戏的流畅度;JavaFX 脉冲系统的高效、优化实现在游戏的这个阶段是至关重要的(没有双关语)。为此,我将在本章中详细介绍 javafx.animation 包,以及它的所有函数类之间的区别。

首先,您将探索 javafx.animation 包中的两个动画超类:Animation 和 AnimationTimer。之后,您将了解动画、时间轴和过渡,以及这些类及其任何子类(如 PathAnimation 和 TranslateAnimation)如何允许您访问 JavaFX 脉冲事件计时系统。现在,你需要使用脉冲,如果你想创建一个面向行动的街机类型的 Java 8 游戏!

您还将仔细查看整个 javafx.animation 包的整体结构,因为您需要在 Java 8 游戏循环中使用其中一个类。您将通过使用整个包的图表来完成这个任务,这样您就可以对它的所有类是如何相互关联的有一个总体的了解。然后,您将详细检查所有 JavaFX 动画类之间的类层次结构。除了 AnimationTimer、Interpolator、KeyFrame 和 KeyValue,所有这些 javafx.animation 包类都使用 JavaFX Animation 超类进行子类化(使用 Java extends 关键字)。

最后,您将把新的 GamePlayLoop 类添加到 invincibagel 包中,该包将作为 invincibagel 应用子类中的 GamePlayLoop 对象创建,实现定时循环。这个 GamePlayLoop 类将包含一个. handle()方法,以及一个. start()方法和一个. stop()方法,这将允许您在 GamePlayLoop 运行时控制 GamePlayLoop 计时事件,并确定它何时处于潜伏状态(停止或暂停)。

我将创建一个图表,显示这个 InvinciBagel 游戏的类和对象层次结构,这样您就可以开始想象您正在编写的这些类和您正在创建的对象是如何组合在一起的。这就好像使用 Java 8 和 JavaFX 编写游戏代码本身就是一个(益智)游戏!很酷的东西。

游戏循环处理:利用 JavaFX 脉冲

即使在开发团队中的 Oracle 员工中,一个主要问题是实现游戏计时循环引擎的哪种设计方法应该与 JavaFX 动画包(类套件)中包含的类一起使用。事实上,这正是本章的全部内容:使用 javafx.animation 包及其类,这些类利用了 JavaFX 脉冲事件计时引擎。在包的类层次结构的顶层,如图 7-1 所示,AnimationTimer 和 Animation 类提供了获取这些脉冲事件的主要方法,以便它们为您进行实时处理。在这一部分,你将看到它们的不同之处以及它们的设计用途,包括它们应该用于的游戏种类。除了插值器(运动曲线应用)、关键帧(关键帧定义)和 KeyValue(关键帧自定义)之外,javafx.animation 包中的所有类都可以用于控制脉冲事件。

A978-1-4842-0415-3_7_Fig1_HTML.jpg

图 7-1。

Javafx.animation package subclass hierarchy; top level classes all coded from scratch with java.lang.Object

有四种基本方法来实现(访问)JavaFX 脉冲事件计时系统,以创建游戏计时循环。这些不同的方法适用于不同类型的游戏,我之前已经讨论过了(见第五章)。这些游戏类型从需要使用 脉冲事件引擎来实现特殊效果(过渡子类)或自定义动画(时间轴类,结合关键帧类和可能的 KeyValue 类)的静态游戏(棋盘游戏、益智游戏)到需要以每秒 60 次的游戏播放刷新率对 脉冲事件系统进行必要的核心访问的高度动态游戏(AnimationTimer 类)。

最高级别(视觉上,最低级别,显示在图的左下方)是使用 javafx.animation 包中预先构建的 Transition 子类,如 PathTransition 类(或对象),用于游戏精灵或投射物的路径,或 TranslateTransition,用于翻译(移动)屏幕上的东西。所有这些过渡子类都是为你编码的;你所要做的就是使用它们,这就是为什么在这个特殊的讨论中,我把它标为最高的功能级别。这种高水平的预建功能带来了内存和处理价格;这是因为,正如你从 Java 继承中所了解到的(参见第三章), path transition 类包含了它所有的方法、变量和常量,以及在类层次结构中位于它之上的所有类的方法、变量和常量。

这意味着整个 PathTransition 类、Transition 超类、Animation 超类和 java.lang.Object masterclass 都包含在该类的内存占用中,并且还可能包含处理开销,这取决于如何使用该类实现对象。这是一个需要考虑的问题,因为你在 JavaFX 动画包中的位置越低,它就越昂贵,你对为你编写的代码的控制就越多,而不是你自己编写的定制代码。

编码自定义游戏循环的下一个最高级别的方法是子类化 javafx.animation.Transition 类,以创建您自己的自定义过渡子类。这一级和前一级都被认为是顶级方法,最适用于静态但有动画效果的游戏,或者动态性较差的游戏。

中级解决方案是使用 Timeline 类及其相关的 KeyFrame 和 KeyValue 类,它们非常适合于实现拖放工具(如 Flash)中基于时间轴的动画类型。您会发现,如果您在网上查看 JavaFX 游戏引擎讨论,这是一种流行的方法,因为许多动画都是通过创建单个关键帧对象,然后使用时间轴对象来处理脉冲事件来实现的。

使用时间轴对象方法允许您指定处理游戏循环的帧速率,例如 30FPS。这将适用于可以使用较低帧速率的动态性较低的游戏,因为它们不涉及大量的帧间游戏处理,如精灵移动、精灵动画、碰撞检测或物理计算。请务必注意,如果使用 Timeline 对象(类),您将在系统内存中为帧速率和至少一个关键帧对象引用(这些是 Timeline 类定义的一部分)定义变量,以及从 Animation 超类继承的属性(变量),如 status、duration、delay、cycleCount、cycleDuration、autoReverse、currentRate、currentTime 和 on finished(action event)object property。

如果您熟悉创建动画,您会看到时间轴,以及至少一个关键帧对象和存储在每个关键帧对象中的潜在的大量 KeyValue 对象,显然是为创建基于时间轴的动画而设计的(优化的)。虽然这是一个非常强大的功能,但它也意味着使用时间轴和关键帧对象进行游戏循环处理将会创建近十几个内存分配区域,这些区域甚至可能不会在您的游戏中使用,或者可能不会针对您的游戏设计实现进行优化设计(编码)。

幸运的是,还有另一个与 javafx.animation 包计时相关的类,它没有这种预构建类的开销,所以我称之为最底层的方法,在这种方法中,您必须在一个简单的。handle()函数,它在每次传递时访问 JavaFX 脉冲引擎。

低级的解决方案包括使用 AnimationTimer 类,这样命名是因为 Java (Swing)已经有了一个 Timer 类(javax.swing.Timer),Java 的实用程序类(java.util.Timer)也是如此,如果您是一个足够高级的程序员,也可以使用它来处理所有线程同步问题(和编码)。

因为这是一本初学者级别的书,所以您将坚持使用 Java 8 游戏引擎(JavaFX 8)循环您的游戏。JavaFX 在 javafx.animation 包中有自己的 Timer 类,称为 AnimationTimer,以免与 Swing GUI Toolkit 的 Timer 类混淆(由于遗留代码的原因,它仍然受支持)。许多新开发人员对这个类名的“动画”部分感到困惑;不要假设这个定时器类是用于动画的;它的核心是为了计时。就访问 javafx 脉冲计时系统而言,该类是 javafx.animation 包中最低级别的类,本质上仅用于访问脉冲计时系统。其他的都被剥离了。

因此,AnimationTimer 类是为您实现提供最少系统开销(使用的内存)的类。在全速 60FPS 时,它将具有最高的性能,假设。handle()方法得到了很好的优化。这是用于快速、高动态游戏的类,例如街机游戏或射击游戏。出于这个原因,这是您将在游戏中使用的类,因为您可以继续构建游戏引擎框架并添加功能,而不会耗尽电量。

在本书中,您将使用最底层的方法,以防您将 Java 8 游戏开发推向极限,并且正在创建一个高度动态的、充满动作的游戏。JavaFX AnimationTimer 超类非常适合这种类型的游戏应用,因为它处理它的。每个 JavaFX 脉冲事件的 handle()方法。脉冲事件目前被限制在 60FPS,这是专业动作游戏的标准帧速率(也称为刷新率)。您将从 AnimationTimer 超类中派生出 GamePlayLoop.java 类的子类。

有趣的是,大多数现代 iTV LCD、有机发光二极管和 LED 显示屏产品也以这种精确的刷新率(60Hz)更新,尽管较新的显示器将以两倍于此的速率(120Hz)更新。具有 240Hz 刷新率的显示器也即将问世,但因为这些 120Hz 和 240Hz 刷新率显示器使用 60Hz 的偶数倍(2 倍或 4 倍),所以 60FPS 是为当今的消费电子设备开发游戏的合理帧速率。接下来,让我们在你的游戏中实现 GamePlayLoop.java 类,它将子类化 AnimationTimer 来访问脉冲。

创建一个新的 Java 类:GamePlayLoop.java

让我们使用 javafx.animation 包中的 AnimationTimer 超类来创建一个自定义的 GamePlayLoop 类(以及最终的对象)和所需的。handle()方法来处理您的游戏进行计算。如图 7-2 所示,在 NetBeans 8.0 中,右键单击项目层次结构窗格中的 invincibagel 包文件夹即可完成此操作。这将向 NetBeans 显示在创建新的 Java 类后,您希望将它放在哪里。

A978-1-4842-0415-3_7_Fig2_HTML.jpg

图 7-2。

Right-click the invincibagel package folder, and use the New ➤ Java Class menu sequence

点击新建➤ Java 类,将打开新建 Java 类对话框,如图 7-3 所示。将该类命名为 GamePlayLoop,保留 NetBeans 设置的其他默认值,具体取决于您右键单击 invincibagel 包文件夹,然后单击 Finish。

A978-1-4842-0415-3_7_Fig3_HTML.jpg

图 7-3。

Name the new Java class GamePlayLoop, and let NetBeans set up the other fields

NetBeans 将为 GamePlayLoop.java 类创建一个引导基础结构,带有一个包和一个类声明,如图 7-4 所示。现在,添加一个 extends 关键字和 AnimationTimer。

A978-1-4842-0415-3_7_Fig4_HTML.jpg

图 7-4。

NetBeans creates a GamePlayLoop.java class and opens it in an editing tab in the IDE, for you to edit

鼠标悬停在错误上,按 Alt+Enter,选择添加导入,如图 7-5 所示。

A978-1-4842-0415-3_7_Fig5_HTML.jpg

图 7-5。

Subclass an AnimationTimer superclass with an extends keyword: press Alt+Enter, and select Add import

一旦 NetBeans 添加了import javafx.animation.AnimationTimer;编程语句,您就可以开始创建这个类了,它将为您利用 JavaFX 脉冲引擎,并包含您所有的核心游戏循环处理,或者对将执行各种类型处理的类和方法的调用,例如精灵移动、精灵动画、碰撞检测、物理模拟、游戏逻辑、音频处理、AI、记分牌更新等等。

创建 GamePlayLoop 类结构:实现你的。handle()方法

请注意,一旦 NetBeans 为您编写了 import 语句,GamePlayLoop 类名下方就会出现另一个红色波浪状错误高亮显示。将鼠标放在上面,查看与这个最新错误相关的错误消息。如图 7-6 所示。每个 AnimationTimer 子类所需的 handle()方法还没有在这个 GamePlayLoop.java 类中实现(也称为 overridden ),所以接下来必须这样做。也许你甚至可以让 NetBeans 帮你写代码;让我们来看看,看看!

A978-1-4842-0415-3_7_Fig6_HTML.jpg

图 7-6。

Once you extend and import AnimationTimer, NetBeans throws an error: class does not implement the .handle()

正如您在弹出的错误消息的左下方看到的,您可以使用 Alt+Enter 组合键来打开一个帮助器对话框,它将为您提供几个解决方案,其中一个将实际编写未实现的。handle()方法。选择实现所有抽象方法,如图 7-7 所示,以蓝色突出显示。双击此选项后,NetBeans 将为您编写此方法结构:

@Override

public void handle (long``now

throw new UnsupportedOperationException("Not supported yet.");

}

请注意,an @Override 关键字位于公共 void 句柄方法访问关键字、返回类型关键字和方法名称之前。这告诉 Java 编译器。handle()方法将替换(覆盖)AnimationTimer 的。handle()方法,这就是为什么该错误指示您必须重写抽象方法。手柄(长)。

你肯定不希望你的。handle()方法在游戏循环中每秒抛出 60 个 UnsupportedOperationException()错误;但是,您现在将把它留在这里,以便您可以看到它的作用,并了解更多关于 NetBeans 错误控制台的信息。

A978-1-4842-0415-3_7_Fig7_HTML.jpg

图 7-7。

Take a coding shortcut: press Alt+Enter to bring up a helper dialog, and select Implement all abstract methods

如图 7-8 所示,一旦选择了实现所有抽象方法选项,Java 代码就没有错误了,类的基本包-导入-类-方法结构也就就位了。现在,您应该能够使用该类创建一个 GamePlayLoop 对象,所以让我们换个方式,在 InvinciBagel Java 类中进行一些编程,在该类中,您创建一个 GamePlayLoop 对象,然后分析应用以查看它做了什么。

A978-1-4842-0415-3_7_Fig8_HTML.jpg

图 7-8。

NetBeans creates a public void handle(long now) bootstrap method with UnsupportedOperationException

创建 GamePlayLoop 对象:添加脉冲控制

接下来,您需要声明、命名和实例化一个名为 gamePlayLoop 的 GamePlayLoop 对象,使用您创建的新类,结合 Java new 关键字。点击 InvinciBagel.java 选项卡,如图 7-9 所示,在声明 gamePlayLoop 对象的 Insets 对象声明下添加一行代码,命名为 GamePlayLoop,如下所示:

GamePlayLoop``gamePlayLoop

A978-1-4842-0415-3_7_Fig9_HTML.jpg

图 7-9。

Click the InvinciBagel.java editing tab, and declare a GamePlayLoop object named gamePlayLoop at the top

正如您所看到的,代码是没有错误的,因为 NetBeans 已经找到了您的 GamePlayLoop 类,它包含了被覆盖的。handle()方法,其父 AnimationTimer 类有一个构造函数方法,可以使用 GamePlayLoop 类创建 AnimationTimer(类型)对象,扩展 AnimationTimer。

现在,您必须使用 Java new 关键字在内存中实例化或创建 GamePlayLoop 对象的实例。这在游戏第一次开始时完成一次,这意味着实例需要放入。start()方法。

您可以在创建所有其他场景图节点对象和 ActionEvent EventHandler 对象后,使用以下 Java 代码行来完成此操作(另请参见图 7-10 ):

gamePlayLoop =``new

这个代码放置的逻辑(在最后)是根据创建和配置来设置所有静态对象,然后在最后创建动态对象,该对象将处理脉冲相关的逻辑。

A978-1-4842-0415-3_7_Fig10_HTML.jpg

图 7-10。

At the end of the .start() method, instantiate the gamePlayLoop object by using the Java new keyword

分析 GamePlayLoop 对象:运行 NetBeans Profiler

让我们使用配置文件➤项目配置文件菜单序列来运行 NetBeans Profiler,以确定您是否可以在任何配置文件视图中看到您创建的 GamePlayLoop 对象。如图 7-11 所示,GamePlayLoop < init >调用用了不到 2 毫秒的时间在内存中设置 GamePlayLoop 对象供您使用,使用的开销很小。

A978-1-4842-0415-3_7_Fig11_HTML.jpg

图 7-11。

Use a Profile ➤ Profile Project menu sequence to start the Profiler and look at GamePlayLoop memory use

接下来,让我们通过向下滚动 Profiler 选项卡来研究 threads analysis 窗格,如图 7-11 的左上角所示。查找线程图标 NetBeans 会询问您是否要启动线程分析工具;一旦你同意,它将打开螺纹标签(见图 7-12 )。

A978-1-4842-0415-3_7_Fig12_HTML.jpg

图 7-12。

Click the Threads icon, seen at the left of the screen, and open the Threads tab; the same eleven threads are running

如果您想知道为什么在图 7-12 所示的线程对象中看不到任何“信号”,就像您在上一章中单击按钮对象时所做的那样,您的假设是正确的,您应该在该图的某个地方看到 JavaFX 脉冲 engine 计时事件,所有的线程栏都是纯色的,因此没有动作或脉冲事件触发。我将让您根据需要经常使用 NetBeans profiling 实用程序,以便熟悉它,因为许多开发人员都避免使用该工具,因为他们还不习惯使用它。

你没有看到任何事件的原因是仅仅创建游戏循环对象是不够的。它内部的 handle()方法来抓取脉冲事件。因为它是一个定时器对象(确切地说,是一个动画定时器),像任何定时器一样,它需要启动和停止。接下来让我们为游戏循环创建这些方法。

控制你的游戏循环:。开始( )和。停止( )

因为 AnimationTimer 超类具有。开始()和。stop()方法控制类(对象)何时(使用. start()方法调用)和何时(不使用. stop()方法调用)处理脉冲事件,您只需在方法代码中使用 Java super 关键字将这些函数“向上”传递给 AnimationTimer 超类。您将重写。使用 Java @Override 关键字启动()方法,然后使用以下方法编程结构将方法调用功能传递给 AnimationTimer 超类:

@Override

public void start() {

super .start();

}

的。stop()方法结构将被覆盖,方法功能以完全相同的方式传递给超类,使用下面的 Java 方法编程结构:

@Override

public void stop() {

super .stop();

}

如图 7-13 所示,GamePlayLoop 类代码是无错误的,现在您可以在 InvinciBagel 类中编写启动 GamePlayLoop AnimationTimer 对象的代码,这样您就可以在分析应用时看到 脉冲对象。

A978-1-4842-0415-3_7_Fig13_HTML.jpg

图 7-13。

Adding .start() and .stop() methods to the GamePlayLoop class and using the Java super keyword properly

你需要打这个电话。start()方法关闭名为 gamePlayLoop 的 GamePlayLoop 对象。您刚刚创建的 start()方法。点击 InvinciBagel.java 选项卡,如图 7-14 所示,在 GamePlayLoop 对象实例化下面添加一行代码,调用。start()方法关闭名为 gamePlayLoop 的 GamePlayLoop 对象,如下所示:

gamePlayLoop.``start

如您所见,方法调用已经就绪,Java 代码没有错误,因为 NetBeans 现在可以找到。GamePlayLoop 类中的 start()方法。接下来,让我们使用运行➤项目序列并测试一两个脉冲,以确定现在将会发生什么,GamePlayLoop AnimationTimer 子类已经使用。start()方法调用。看看在。handle()方法就可以了!

A978-1-4842-0415-3_7_Fig14_HTML.jpg

图 7-14。

Call a .start() method off the gamePlayLoop object to start GamePlayLoop AnimationTimer

如图 7-15 所示,您会得到与中的内容相关的重复错误。handle()方法。

A978-1-4842-0415-3_7_Fig15_HTML.jpg

图 7-15。

Click Run ➤ Project, and open the Output pane to see errors being generated in .handle()

显然,NetBeans 8.0 并不总是为它为我们编写的引导方法编写最佳代码,所以让我们删除代码的throw new UnsupportedOperationException("Not implemented yet.");行(参见图 7-13 )。在它的位置,你将插入一个 Java 注释,这会创建一个空方法,如图 7-16 所示。这将允许您的游戏应用运行。虽然游戏应用窗口启动时抛出了错误,但是场景图形的组件没有写入场景,只能看到默认的白色背景色。如果您在 NetBeans 中继续学习,您将会观察到这一点。

A978-1-4842-0415-3_7_Fig16_HTML.jpg

图 7-16。

Replace throw new UnsupportedOperationException(); with a comment, creating an empty method

现在,让我们再次使用“剖析➤剖析项目”( InvinciBagel)工作流程,看看 NetBeans 的“实时结果”和“线程”选项卡中是否出现了新内容。点击图 7-17 左侧所示的实时结果图标,并在选项卡中启动实时结果分析器。注意,GamePlayLoop 对象是使用< init >创建的,而 AnimationTimer 是使用 invincibagel 启动的。分析器输出中的 GamePlayLoop.start()条目。

如您所见,初始化每个事件队列只需要几分之一毫秒,包括 脉冲事件和所有四个 ActionEvent EventHandler 事件处理队列。这与我们的最大游戏优化方法一致,使用静态场景图节点,并且不在 GamePlayLoop 内做任何事情,这些事情会消耗更多的系统资源(内存和处理周期),而不是在创建充满动作的街机游戏时完成各种任务所绝对需要的资源。

现在您已经创建并启动了 GamePlayLoop 对象,让我们来看看线程档案器!

A978-1-4842-0415-3_7_Fig17_HTML.jpg

图 7-17。

Use a Profile ➤ Profile Project menu sequence to start the Profiler, and look at GamePlayLoop memory use

再次,向下滚动图 7-17 左上角所示的 Profiler 选项卡,找到图 7-18 左上角显示的 Threads 图标。NetBeans 将询问您是否要启动线程分析工具;一旦你同意,它将打开线程标签。如图 7-18 所示,脉冲引擎正在运行,显示线程 6 的几个脉冲事件。有趣的是,一旦 JavaFX 确定。handle()方法为空,脉冲事件系统不会继续处理这个空。handle()方法并使用不必要的脉冲事件,这表明 JavaFX 脉冲事件系统具有一定的智能。

A978-1-4842-0415-3_7_Fig18_HTML.jpg

图 7-18。

Click the Threads icon, seen at the left side of the screen, and open a Threads tab; AnimationTimer pulses can be observed on Thread-6

InvinciBagel 图:包、类和对象

接下来,让我们以图表的形式看看你当前的包、类和对象层次结构(见图 7-19 ),看看你在创建你的游戏引擎方面处于什么位置。在图的右侧是 InvinciBagel 类,它保存场景图,以及 Stage、Scene 和 StackPane 对象,这些对象保存并显示您的闪屏 UI 设计。图的左边是 gamePlayLoop 类,它将包含游戏处理逻辑调用,并在 InvinciBagel 类中声明和实例化为 GamePlayLoop 对象,但不是场景图形层次的一部分。很快,您将开始构建图表中显示的其他功能区域,以便您可以控制您的精灵,检测精灵之间的碰撞,并模拟真实世界的物理力,使游戏更加逼真。随着您阅读本书并创建您的 Java 8 游戏,您将会看到该图的补充。

A978-1-4842-0415-3_7_Fig19_HTML.jpg

图 7-19。

Current invincibagel package, class, and object hierarchy, after addition of the GamePlayLoop

接下来,在进入 GamePlayLoop AnimationTimer 类和对象之前,您将在当前空的中放置一些相对简单的 Java 代码。handle()方法。您将这样做,以确保脉冲引擎正在处理,并看看 60FPS 有多快!(我不得不承认,我的好奇心占了上风!).

测试 GamePlayLoop:制作 UI 容器动画

让我们围绕 InvinciBagel 闪屏逆时针移动一个现有的场景图节点,例如 HBox 布局容器父(分支)节点,它包含四个 UI 按钮控件元素。您将通过使用一个简单的 if-else Java 循环控制编程结构来读取(使用. get()方法)和控制(使用. set()方法)控制(在本例中)屏幕位置角的 Pos 常量。

首先,在 GamePlayLoop 类的顶部声明一个名为 location 的 Pos 对象。然后,单击突出显示的错误消息,按 Alt+Enter,并选择“导入 Pos 类”选项,以便 NetBeans 为您编写导入语句。接下来在。handle()方法,添加一个 if-else 条件语句,该语句计算这个名为 location 的 Pos 对象,并将其与表示显示屏四个角的四个 Pos 类常量进行比较,这四个角包括 BOTTOM_LEFT、BOTTOM_RIGHT、TOP_RIGHT 和 TOP_LEFT。您的 Java 代码应该类似于下面的 if-else 条件语句 Java 程序结构(参见图 7-20 ):

Pos``location

@Override

public void handle(long now) {

location``= InvinciBagel.buttonContainer.``getAlignment()

if (location == Pos.BOTTOM_LEFT) {

InvinciBagel.buttonContainer.setAlignment(Pos.BOTTOM_RIGHT);

}``else if

InvinciBagel.buttonContainer.setAlignment(Pos.TOP_RIGHT);

}``else if

InvinciBagel.buttonContainer.setAlignment(Pos.TOP_LEFT);

}``else if

InvinciBagel.buttonContainer.setAlignment(Pos.BOTTOM_LEFT);

}

}

如图所示,您的代码没有错误,您已经准备好使用“运行➤项目”工作流程并观看 60FPS 的焰火了!准备好享受炫目的速度吧!

A978-1-4842-0415-3_7_Fig20_HTML.jpg

图 7-20。

Create an if-else loop that moves the HBox UI counterclockwise around the four corners of a splash screen

接下来,让我们最后一次运行实时结果分析器和线程分析器,看看您的脉冲引擎是否正在启动!一旦你这样做了,你就会知道你已经成功地为你的游戏实现了你的 GamePlayLoop 计时引擎,然后你就可以把你的注意力转移到开发你的游戏精灵,碰撞检测,物理和逻辑上了!

剖析游戏循环:脉冲引擎

现在,让我们最后一次使用“剖析➤剖析项目”( invincibagel)工作流程,看看 NetBeans 的“实时结果”和“线程”选项卡中是否出现了新内容。点击图 7-21 左侧所示的实时结果图标,并在选项卡中启动实时结果分析器。注意,GamePlayLoop 对象是使用< init >创建的;使用 invincibagel 启动了一个动画定时器。分析器输出中的 GamePlayLoop.start()条目;有一个不可战胜的怪物。GamePlayLoop.handle(long)条目,这意味着您的游戏计时循环正在被处理。

正如您所看到的,调用列显示了有多少脉冲访问了。GamePlayLoop 中的 handle()方法。处理 3,532 个脉冲只需要 40.1 毫秒,因此使用新的 Java 8 计时分辨率,每个脉冲相当于 0.0114 毫秒,即 114 纳秒。因此,您当前用于测试脉冲的代码,或者至少是 JavaFX 脉冲引擎,是高效运行的。

A978-1-4842-0415-3_7_Fig21_HTML.jpg

图 7-21。

Run the Live Results Profiler

当然,您需要从。handle()方法,然后进入下一章,在下一章中,您将开始处理这个方法中的游戏资产和逻辑。

接下来,让我们最后一次向下滚动图 7-21 左上角显示的 Profiler 选项卡,点击图 7-22 左上角显示的 Threads 图标,打开 Threads 选项卡。正如您所看到的,脉冲引擎正在运行,可以看到在 Thread-6 和 JavaFX 应用线程中处理脉冲事件。

鉴于空虚。handle()方法处理来自 Thread-6 中的 GamePlayLoop 对象(见图 7-18 ),可以假设 Thread-6 中的脉冲事件来自 GamePlayLoop AnimationTimer 子类。这意味着 JavaFX 应用线程中显示的脉冲事件显示了。handle()方法正在访问 InvinciBagel 类中 stackPane 场景图形根中包含的 buttonContainer HBox 对象。

A978-1-4842-0415-3_7_Fig22_HTML.jpg

图 7-22。

Run the Threads Profiler

现在,您已经有了一个低开销、速度极快的游戏处理循环,您可以开始创建您的其他(精灵、碰撞、物理、得分、逻辑等等)游戏引擎了!一个搞定了,还有一大堆要做!

摘要

在第七章中,您编写了在本书的课程中将设计和编码的许多游戏引擎中的第一个,GamePlayLoop 游戏播放计时类和对象,它们允许您利用强大的 JavaFX 脉冲事件处理系统。

首先,您研究了 javafx.animation 包中的不同类,以及使用 animation、Transition、Timeline 和 AnimationTimer 类来利用 JavaFX 脉冲事件处理系统的不同方法。

之后,您学习了如何在 NetBeans 中创建新的 Java 类,然后扩展了 AnimationTimer 超类以创建 GamePlayLoop 子类,它将以 60FPS 的速度处理您的游戏逻辑。您看到了如何使用 NetBeans 来帮助您编写这个新子类的大部分代码,包括 package 和 class 语句、import 语句和 bootstrap。handle()方法。

接下来,您进入 InvinciBagel.java 类,使用您创建的新类声明并命名了一个新的 gamePlayLoop GamePlayLoop 对象。然后,您测试了代码并对其进行了概要分析,以查看 Threads Live Results 选项卡中是否出现了任何新条目。您还测试了。NetBeans 为您编码的 handle()方法,并将其更改为空方法,以消除由脉冲事件引擎引发的重复错误。接下来,您实现了。开始()和。stop()方法,使用 Java super 关键字,这样您就可以控制对 脉冲引擎的使用,如果您想要添加额外的 Java 语句,比如保存游戏状态,稍后,当 脉冲引擎启动和停止时。您再次测试并分析了应用,以观察您的进展。最后,您将一些测试代码放在。handle()方法,这样您就可以再次测试和分析应用,以确保脉冲事件引擎快速一致地处理您放在。handle()方法。

在下一章,你将会看到如何创建和实现抽象类,这些抽象类将会被用来创建你的游戏精灵。一旦我们有了这些,它将允许我们在后面的章节中实时地在你的新游戏循环引擎中的显示屏上显示它们,制作它们的动画,并处理它们的运动。

八、创建你的演员引擎:为你的游戏设计角色并定义他们的能力

既然我们已经在第七章的中创建了游戏计时循环,让我们在第八章的中进入一些临时代码,并创建公共抽象类框架,我们可以用它来创建我们将在无敌百吉游戏中使用的不同类型的精灵。这实质上等同于你的游戏的“角色引擎”,因为你将定义和设计你的游戏将包括的各种类型的游戏组件作为角色,这两个类将用于创建所有其他类,这些类将用于创建你的游戏中的对象(组件)。这些将包括诸如无敌面包圈本身(面包圈类)、他的对手(敌人类)、他在游戏中寻找的所有宝藏(宝藏类)、射向他的东西(抛射体类)、他在上面和周围导航的东西(道具类),所有这些都提供了无敌面包圈必须尝试和实现的游戏目标。

在本章中,我们将创建两个公共抽象类结构。第一个,Actorclass,将是另一个,Hero 子类的超类。这两个抽象类可以在书中用来创建我们的固定精灵,它们是不动的精灵(障碍和宝藏),使用 Actor 超类,和在屏幕上移动的精灵,使用 Actor 类的 Hero 子类。英雄类将为运动精灵(超级英雄,和他在多人游戏版本中的主要敌人)提供额外的方法和变量。这些将跟踪像碰撞和物理特性,以及演员动画(运动状态)。在屏幕上有大量的动作会让游戏玩起来更有趣,并且让我们让游戏对玩家来说更有挑战性。

预先创建 Actor 引擎将使您获得创建公共抽象类的经验。正如你在第三章中所回忆的,公共抽象类在 Java 中被用来创建其他类(和对象)结构,但并不直接用在实际的游戏编程逻辑中。这就是为什么我称创建这两个“蓝图”类为创建演员引擎,因为我们本质上定义了游戏的最低级别,在这一章中的“演员”。

随着本书中游戏设计的进展,我们将使用 Actor (fixed sprite)类创建 Treasure 子类,用于在游戏过程中 InvinciBagel 将获得的“固定”宝藏。我们还将使用这个 Actor 超类创建 Prop 类,用于 InvinciBagel 必须成功地向上、向上、向下、绕过或通过的游戏中的障碍。我们还将使用 Actor 超类的 Hero 子类创建在屏幕上移动的精灵,比如 Bagel 类。我们最终会创造一个敌人职业和抛射职业。

除了在本章中设计两个关键的公共抽象演员类,我们还将使用不到 10 个 PNG32 数字图像资源来定义我们的主要无敌角色的数字图像状态。我们将在本章中这样做,以便在我们想要在本书的下一章中使用这些类和精灵图像状态之前,我们将在下一章中查看事件处理,以便玩家可以控制 InvinciBagel 在屏幕上的位置以及什么状态(站立、奔跑、跳跃、飞跃、飞行、着陆、错过、蹲伏等)。)他用它来导航他世界中的障碍。

游戏角色设计:预先定义属性

任何流行游戏的基础都是角色——英雄和他的主要敌人——以及游戏的障碍、武器库(投射物)和宝藏。这些“角色”中的每一个都需要使用 Java 中的变量来定义属性,这些属性使用系统内存区域来实时跟踪每个角色在游戏过程中发生了什么。我将尝试在第一次就正确地做到这一点,就像您希望在第一次定义数据库时,定义一个数据库记录结构来保存将来需要的数据一样。这可能是你游戏开发的一个具有挑战性的阶段,因为你需要展望未来,确定你希望你的游戏和它的演员有什么样的功能,然后把这些放到你的演员的能力(变量和方法)前面。图 8-1 让你了解在本章的过程中我们将为游戏角色安装的 24 个属性中的一些,因为我们创建了一百多行代码来实现我们的游戏角色引擎。

A978-1-4842-0415-3_8_Fig1_HTML.jpg

图 8-1。

Design a public abstract Actor superclass and a public abstract Hero subclass to use to create sprite classes

如你所见,我试图得到一个平衡的变量数量;在这种情况下,在固定精灵演员类和运动精灵英雄类之间,每个大约有 12 个。正如你从第三章中所知道的,因为我们将要创建的 Hero 子类扩展了 Actor 超类,它实际上有 24 个属性或特征,因为它假设了所有的超类变量,除了有自己的。一个设计上的挑战是将尽可能多的这些属性放在 Actor 超类中,这样固定的精灵就有尽可能多的灵活性。一个很好的例子是,在第一轮设计中,我在 Hero 类中有枢轴点(pX 和 pY 变量),但后来我想了想“如果我想稍后旋转固定精灵(障碍和宝藏)以获得更高的设计效率会怎么样”,所以我将这些变量放在 Actor 超类中,将这种枢轴(旋转)功能赋予固定和运动精灵。

我在 Hero 类中“上移”到 Actor 超类的另一个变量是 List 属性。在这个设计过程中,我对自己说,“如果出于某种原因,我希望我的固定精灵有不止一个图像状态呢?”我还将 Actor 类从使用简单的 Rectangle Shape 对象升级为使用 SVGPath Shape 子类,这样我就可以使用比矩形更复杂的形状来定义碰撞几何体(这就是 spriteBounds 变量的含义),以支持游戏后期更复杂的高级障碍结构。

还要注意,我在 Actor 类中有 spriteFrame ImageView,它保存 sprite 图像资产,因为固定和运动 sprite 都使用图像,所以我可以将 ImageView 放入 Actor 超类中。我在 Actor 超类中使用 imageStates 列表,这样固定精灵就可以像运动精灵一样访问不同的“视觉状态”。正如您可能已经猜到的,List 是一个填充了 JavaFX Image 对象的 Java List 对象。Actor 类中的 iX 和 iY 变量是图像(或初始)位置 X 和 Y,它们在游戏级别布局上放置一个固定的 sprite,但当由 Hero 子类假定时,也将保持运动 sprite 的当前 sprite 位置。其他变量保存布尔状态(活/死等。)和寿命、损坏、偏移、碰撞或我们稍后需要的物理数据。

无敌精灵图像:视觉动作状态

除了设计用于实现游戏中的角色、宝藏和障碍的最佳演员引擎类之外,另一个要优化的重要内容是游戏的主要角色,以及角色根据玩家的角色移动而在动画的不同状态之间移动。从内存优化的角度来看,我们能够完成所有这些的图像帧越少越好。正如你在图 8-2 中看到的,我将只使用九种不同的精灵图像资源来提供所有的无敌角色运动状态;其中一些可以以多种方式使用:例如,通过使用 pX 和 pY 变量,这将允许我们围绕我们选择的任何枢轴点旋转这些 sprite 帧。这方面的一个例子是飞行状态的中心轴点放置,如图 8-2 中间所示,通过将该图像顺时针旋转 50 度(水平方向)到 100 度(倾斜向下飞,而不是向上飞),我们就可以起飞(向上飞)、飞行和着陆(向下飞)。

A978-1-4842-0415-3_8_Fig2_HTML.jpg

图 8-2。

The nine primary character motion sprites for the InvinciBagel character that will be used during the game

尽管我们在 sprite Actor 引擎抽象类中提供了偏移和枢轴点功能,但这并不意味着我们不应该确保我们的运动 sprite 图像状态彼此之间很好地同步。这使得我们不必经常使用这些旋转或偏移功能来获得良好的视觉效果。这就是我所说的子画面注册,包括不同的子画面状态相对于彼此的最佳定位。

在图 8-3 中可以看到一些将相互使用的子画面帧之间的子画面注册的例子。例如,开始运行 imageStates[1]精灵应该以与站立(或等待)imageStates[0]精灵相同的脚位置开始其运行周期,如图 8-3 左侧所示。此外,相对于开始运行 sprite 的 imageStates[1],运行的 imageStates[2] sprite 应该尽可能保持其主体部分不动。一个准备着陆的 imageStates[6] sprite 应该相对于着陆的 imageStates[7] sprite 真实地改变脚的位置。

A978-1-4842-0415-3_8_Fig3_HTML.jpg

图 8-3。

Sprite registration (alignment) to make sure the transition motion is smooth

相对于所有其他精灵,您想要做的是优化精灵注册,将所有数字图像精灵放入相同的正方形 1:1 纵横比分辨率图像格式,并将它们全部放在数字图像合成软件包(如 GIMP 或 Photoshop)的层中。然后使用移动工具和轻推(使用键盘上的箭头键移动单个像素)每个精灵到适当的位置,相对于您打开可见性(使用每个层左侧的眼睛图标打开/关闭)的两个层。结果如图 8-3 所示。

创建执行元超类:修复执行元属性

让我们开始编写我们的公共抽象演员类,这将是我们在本书中为游戏创建的所有精灵的基础。我不会重新讨论如何在 NetBeans 中创建新类(参见图 7-2 ),因为您已经在第七章中了解到了这一点,所以创建一个 Actor.java 类,使用公共抽象类 Actor 声明它,并将前五行代码放在类的顶部,声明一个名为 imageStates 的 List < Image >,创建一个新的 ArrayList < >对象,以及一个名为 spriteFrame 的 ImageView、一个名为 spriteBound 的 SVGPath 和双变量 iX 和 iY。对所有这些进行保护,这样任何子类都可以访问它们,如图 8-4 所示。对于与 List 类(对象)、ArrayList 类(对象)、Image 类(对象)、ImageView 类(对象)和 SVGPath 类(对象)所需的导入语句相关的红色错误下划线,您需要使用 Alt-Enter 工作流程。一旦 NetBeans 为您编写了这些代码,声明 List < Image > ArrayList、spriteFrame ImageView、SVGPath collision Shape 对象以及包含 sprite 的 X 和 Y 位置的 double 变量的十几行代码应该类似于下面的 Java 类结构:

package invincibagel;

import java.util.ArrayList;

import java.util.List;

import javafx.scene.image.Image;

import javafx.scene.image.ImageView;

import javafx.scene.shape.SVGPath;

public``abstract

protected List<Image>``imageStates

protected ImageView``spriteFrame

protected SVGPath``spriteBound

protected double``iX

protected double``iY

}

A978-1-4842-0415-3_8_Fig4_HTML.jpg

图 8-4。

Create a New Class in NetBeans, name it public abstract class Actor, and add in the primary Actor variables

这五个变量或属性持有任何 sprite 的“核心”属性;spriteFrame ImageView 和它保存的图像资产(一个到多个可见状态)的 List ArrayList(这定义了子画面的外观)、spriteBound 碰撞形状区域(定义了被认为与子画面相交的区域)以及子画面在显示屏上的 X,Y 位置。

这五个变量也将在稍后使用 Actor()构造函数方法和 Hero()构造函数方法进行配置。首先我们将创建 Actor()构造函数;之后,我们将添加所有其他变量,我们需要每个 Actor 子类都包含这些变量。

在我们为 Actor 类创建了所有其他变量(这些变量不是使用 Actor()构造函数方法设置的)之后,我们将初始化这些变量以在构造函数方法中保存它们的默认值,最后我们将让 NetBeans 创建。get()和。使用一个你会喜欢的自动编码函数为我们的变量设置()方法。

我们将编码并传递给这个 Actor()构造函数的参数将包括名为 SVGdata 的 String 对象,它将包含一个定义 SVGPath 冲突形状的文本字符串,以及 sprite X,Y 位置和一个逗号分隔的图像对象列表。SVGPath 类有一个. setContent()方法,可以读取或“解析”原始 SVG 数据字符串,因此我们将使用它将字符串 SVG data 变量转换为 SVGPath 碰撞形状对象。

我们将不会在本章或下一章中实现碰撞代码或 SVGPath Shape 对象,但我们需要将它们放在适当的位置,这样我们可以在后面的第十六章碰撞检测处理以及如何使用 GIMP 和 PhysEd (PhysicsEditor)软件包创建碰撞多边形数据中使用它们。

我们将创建的 Actor 构造函数方法将遵循以下构造函数方法格式:

public Actor(String``SVGdata``, double``xLocation``, double``yLocation``, Image...``spriteCels``)

稍后,如果我们需要创建更复杂的 Actor()构造函数方法,我们可以通过添加其他更高级的参数来“重载”该方法,例如 pivot point pX 和 pY,或者 isFlipH 或 isFlipV 布尔值,以允许我们水平或垂直镜像固定的 sprite 图像。您的 Java 代码将如下所示:

public Actor(String``SVGdata``, double``xLocation``, double``yLocation``, Image...``spriteCels``) {

spriteBound = new SVGPath();

spriteBound.setContent(``SVGdata

spriteFrame = new ImageView(``spriteCels[0]

imageStates.addAll(Arrays.asList(``spriteCels

iX =``xLocation

iY =``yLocation

}

请注意,使用 Java new 关键字调用的 ImageView 构造函数通过使用 spriteCels[0]注释,使用逗号分隔的列表传递您正在传递的 List ArrayList 数据的第一帧(Image)。如果您要创建一个允许您设置轴心点数据的重载方法,它可能如下所示:

public Actor(String``SVG``, double``xLoc``, double``yLoc``, double``xPivot``, double``yPivot``, Image...``Cels``){

spriteBound = new SVGPath();

spriteBound.setContent(``SVG

spriteFrame = new ImageView(``Cels[0]

imageStates.addAll(Arrays.asList(``Cels

iX =``xLoc

iY =``yLoc

pX =``xPivot

pY =``yPivot

}

如图 8-5 所示,您需要使用 Alt-Enter 工作流程,并让 NetBeans 为您的 Arrays 类编写导入语句。一旦你这样做了,你的代码就不会有错误。

A978-1-4842-0415-3_8_Fig5_HTML.jpg

图 8-5。

Create a constructor method to set up fixed Actor sprite subclasses with collision shape, Image list, location

接下来,让我们编写这个类的另一个关键方法,抽象方法。update()方法,然后我们可以添加我们将需要的 Actor 类的其余固定 sprite 属性。之后,我们可以初始化 Actor()构造函数方法中的附加变量。最后,我们将学习如何为 Actor 类创建“getter 和 setter”方法,然后继续使用这个新的自定义 Actor 超类来创建我们的另一个 Hero motion sprites 子类。

创建一个。update()方法:连接到 GamePlayLoop 引擎

对于任何 sprite 类来说,除了创建它的构造函数方法之外,最重要的方法是。update()方法。那个。update()方法将包含 Java 8 代码,告诉 sprite 在 GamePlayLoop 的每个脉冲上做什么。因为这个原因,这个。update()方法将用于将使用我们的 Actor 超类和 Hero 子类创建的 Actor sprite 子类“连接”到我们在第七章中创建的 GamePlayLoop 计时引擎中。

因为我们需要一个。update()方法作为游戏中每个 Actor 对象(actor sprite)的一部分,我们需要包含一个“空的”(目前)抽象。我们当前正在编写的 Actor 超类中的 update()方法。

正如您在第三章中了解到的,这个公共抽象方法在 Actor 超类中是空的,或者更准确地说,是未实现的,但是需要在任何 Actor 子类中实现(也就是说,需要被实现)(或者再次声明为抽象方法),包括我们稍后将要编码的 Hero 子类。

该方法被声明为 public abstract void,因为它不返回任何值(它只是在每个 JavaFX 脉冲事件上执行)并且不包含{...}花括号,因为它里面(还)没有任何代码体!声明公共抽象(空的或未实现的)方法的单行代码应该如下所示:

public``abstract``void``update

正如你在图 8-6 中看到的,这个方法实现起来非常简单,一旦你在你的 Actor()构造函数方法下添加了这个新方法,你的 Java 8 代码再次没有错误,你就可以准备添加更多的代码了。

A978-1-4842-0415-3_8_Fig6_HTML.jpg

图 8-6。

Add an Arrays import statement to support constructor method; add a public abstract .update() method

接下来,我们将为我们的固定精灵演员超类添加其余的属性(或变量),这需要我们提前考虑,在创建这个游戏期间,我们希望能够用我们的精灵完成什么。

向 Actor 类添加 Sprite 控件和定义变量

从编码的角度来看,这个过程的下一部分很简单,因为我们将在 Actor 类的顶部声明更多的变量。然而,从设计的角度来看,这更加困难,因为它要求我们尽可能地提前思考,并推测我们的精灵演员(固定精灵和运动精灵)需要什么样的可变数据,以便能够在游戏的构建和游戏过程中做我们想做的一切。

在 iX 和 iY 变量之后,我要声明的第一个附加变量是 pX 和 pY 枢轴点变量。我最初将它们放在 Hero 子类中,一旦我们完成了这个 Actor 超类的创建,接下来我们将创建它。我将这些“升级”到演员超类级别的原因是因为我想拥有旋转固定精灵(宝藏和障碍)以及运动精灵的灵活性。在关卡和场景设计方面,这给了我更多的权力和灵活性。这些支点 X 和 Y 变量将被声明为受保护的 double 数据变量,并使用下面两行 Java 代码来完成:

protected double``pX

protected double``pY

接下来,我们需要在 Actor 类(对象)定义中添加一些布尔“标志”。这些将指示关于所讨论的精灵对象的某些事情,例如它是活的(对于固定精灵这将总是假的)还是死的,或者它是固定的(对于固定精灵这将总是真的,对于不在运动中的运动精灵也是真的)还是移动的,或者奖励对象,指示它们的捕获(碰撞)的附加点(或寿命),或者有价值的,指示它们的获取(碰撞)的附加能力(或寿命)。最后,我定义了一个水平翻转和垂直翻转标志,与没有这些标志的情况相比,使用(固定的或运动的)精灵图像资源给了我四倍的灵活性。

由于 JavaFX 可以在 X 或 Y 轴上翻转或镜像图像,这意味着我可以使用 FlipV 反转精灵方向(向左或向右),或使用 FlipH 反转方向(向上或向下)。

这六个额外的布尔标志固定(演员)sprite 属性将通过使用受保护的布尔数据变量来声明,使用以下六行 Java 8 代码,如图 8-7 所示(没有错误,没有减少):

protected boolean``isAlive

protected boolean``isFixed

protected boolean``isBonus

protected boolean``hasValu

protected boolean``isFlipV

protected boolean``isFlipH

A978-1-4842-0415-3_8_Fig7_HTML.jpg

图 8-7。

Add the rest of the variables needed to support rotation (pivot point), and sprite definition states

接下来,我们将在 Actor()构造函数方法中初始化这些变量。如果您想使用参数列表将这些布尔标志的设置传递给 Actor()构造函数方法,请记住,您可以创建任意多的重载构造函数方法格式,只要每个格式的参数列表都是 100%唯一的。在本书的后面部分,我们可能会这样做,例如,如果我们需要一个构造函数方法来为布局设计的目的旋转我们的固定精灵,或者一个围绕给定的轴翻转它,例如,为了相同的确切目的,或者一个两者都做的方法,这将给我们一个九参数 Actor()构造函数方法调用。

在 Actor 构造函数方法中初始化 Sprite 控件和定义变量

现在,我们将把轴心点 pX 和 pY 初始化为 0(左上角原点),所有布尔标志的值都设为 false,只有 isFixed 变量除外,对于固定的 sprite,它的值总是设为 true。我们将在当前 Actor()构造函数方法中使用以下八行 Java 代码,并在该方法中处理使用方法参数配置 Actor 对象的前四行代码的下面使用这些代码:

pX = 0;

pY = 0;

isAlive = false;

isFixed = true;

isBonus = false;

hasValu = false;

isFlipV = false;

isFlipH = false;

我们也可以使用复合初始化语句来实现这一点。这将把代码减少到三行:

px = pY = 0;

isFixed = true;

isAlive = isBonus = hasValu = isFlipV = isFlipH = false;

正如你在图 8-8 中看到的,我们现在已经编写了近 30 行无错误的 Java 8 代码,我们准备创建剩下的。get()和。set()方法将组成公共抽象 Actor 超类。

A978-1-4842-0415-3_8_Fig8_HTML.jpg

图 8-8。

Add initialization values to the eight new fixed sprite pivot and state definition variables you just declared

Actor 类中的其余方法通常称为“getter”和“setter”方法,因为这些方法提供了对类内部数据变量的访问。使用 getter 和 setter 方法是正确的做法,因为这样做实现了 Java 封装的概念(和优势),它允许 Java 对象成为对象属性(可变数据值)和行为(方法)的自包含容器。

访问参与者变量:创建 Getter 和 Setter 方法

NetBeans 的一个真正强大的(并且节省时间的)功能是,它将编写您的所有。get()和。自动为每个对象和数据变量设置()方法。我们将在本书中尽可能使用这一便利的特性,因此您可以习惯于使用这一节省时间的特性来为您编写大量 Java 8 代码,加速您的 Java 8 游戏代码生产输出!您可以通过使用源菜单及其插入代码子菜单来访问该自动编码功能,如图 8-9 所示。可以看到,还有一个键盘快捷键(Alt-Insert);使用其中任何一个都会调出浮动生成菜单,该菜单在图 8-9 的底部中央以红色高亮显示。

A978-1-4842-0415-3_8_Fig9_HTML.jpg

图 8-9。

Use Source ➤ Insert Code menu (or Alt+Insert) to bring up a Generate Getter and Setter dialog and select all

点击在生成浮动菜单中间高亮显示的 Getter 和 Setter 选项,将出现一个生成 Getter 和 Setter 对话框,如图 8-9 右侧所示。确保层次结构是打开的,并且 Actor 旁边的复选框被选中,这将自动选择该类中的所有变量,在这种情况下,在图 8-9 的右侧也显示了十几个被选中的变量。

一旦所有这些都被选中,点击对话框底部的 Generate 按钮,生成 24。get()和。如果 NetBeans 8.0 没有提供这一便利的 IDE 功能,则必须手动键入 set()方法。

这些。get()和。由 NetBeans 8.0 源代码➤插入代码➤生成➤ Getters 和 Setters 菜单序列生成的 set()方法将为您提供以下 24 个 Java 方法代码构造,相当于我们在公共抽象 Actor 类中定义的 12 个变量中的每一个都有两个方法:

public List<Image>``getImageStates()

return imageStates;

}

public void``setImageStates(List<Image> imageStates)

this.imageStates = imageStates;

}

public ImageView``getSpriteFrame()

return spriteFrame;

}

public void``setSpriteFrame(ImageView spriteFrame)

this.spriteFrame = spriteFrame;

}

public SVGPath``getSpriteBound()

return spriteBound;

}

public void``setSpriteBound(SVGPath spriteBound)

this.spriteBound = spriteBound;

}

public double``getiX()

return iX;

}

public void``setiX(double iX)

this.iX = iX;

}

public double``getiY()

return iY;

}

public void``setiY(double iY)

this.iY = iY;

}

public double``getpX()

return pX;

}

public void``setpX(double pX)

this.pX = pX;

}

public double``getpY()

return pY;

}

public void``setpY(double pY)

this.pY = pY;

}

public boolean``isAlive()

return isAlive;

}

public void``setIsAlive(boolean isAlive)

this.isAlive = isAlive;

}

public boolean``isFixed()

return isFixed;

}

public void``setIsFixed(boolean isFixed)

this.isFixed = isFixed;

}

public boolean``isBonus()

return isBonus;

}

public void``setIsBonus(boolean isBonus)

this.isBonus = isBonus;

}

public boolean``hasValu()

return hasValu;

}

public void``setHasValu(boolean hasValu)

this.hasValu = hasValu;

}

public boolean``isFlipV()

return isFlipV;

}

public void``setIsFlipV(boolean isFlipV)

this.isFlipV = isFlipV;

}

public boolean``isFlipH()

return isFlipH;

}

public void``setIsFlipH(boo lean isFlipH)

this.isFlipH = isFlipH;

}

注意,除了。get()和。set()方法生成,对于布尔变量还有一个附加的。是()方法,它是代替。get()方法。因为我已经使用“is”前缀命名了布尔标志,所以我将删除第二个“Is ”,以便这些“double is”方法更具可读性。我还将对 hasValu 方法做同样的事情,这样在方法调用中查询布尔设置就更自然了,例如。hasValu()、isFlipV()、isBonus()、isFixed()或。例如 isFlipH()。为了可读性,我建议您对代码进行同样的编辑。

现在我们准备创建我们的 Hero 子类,它将向我们在 Actor 类中创建的 13 个属性添加另外 11 个属性,使总数达到 24 个。Hero 类中的这 11 个附加属性将用于可以在屏幕上移动的可移动精灵(我喜欢称之为运动精灵)。在我们游戏的单人版本中,我们的无敌英雄角色将是主要的英雄角色对象,在未来的多人版本中,这将包括无敌英雄角色对象和敌人英雄角色对象。

创建英雄超类:动作演员属性

接下来让我们创建我们的公共抽象英雄类!这个类将是我们在本书中为游戏创建动作精灵的基础。在 NetBeans 中创建您的 Hero.java 类,并将其声明为public abstract class Hero extends Actor。由于我们已经在 Actor 类中完成了许多“繁重的工作”,所以您不必创建 ImageView 来保存 sprite 图像资产,也不必创建 List < Image > ArrayList 对象,该对象加载了一个由 Image 对象填充的 List 对象,或者创建一个 SVGPath Shape 对象来保存碰撞形状 SVG polyline(或多边形)路径数据。

由于我们不必声明任何主属性,因为这些属性是从 Actor 超类继承的,所以我们要做的第一件事是创建一个 Hero()构造函数方法。这将包含字符串对象中的碰撞形状数据,sprite X,Y 位置,以及将加载到 List ArrayList 对象中的图像对象。在我们创建了一个基本的 Hero()构造函数方法之后,我们将完成计算你的运动精灵需要包含的其他属性(或变量),就像我们在设计 Actor 超类时所做的一样。

请记住,您已经有了使用 Actor()方法在 Actor 类中构造的 spriteBound SVGPath Shape 对象、imageStates List ArrayList 对象、SpriteFrames Image 对象以及 iX 和 iY 变量。为了能够编写我们的 Hero()构造函数方法,我们还需要这些。由于这些都已经就绪,由于 Hero 类声明中的 java extends 关键字,我们所要做的就是使用 super()构造函数方法调用,并将这些变量从 Hero()构造函数向上传递给 Actor()构造函数。这将使用 Java super 关键字自动将这些变量传递给 Hero 类供我们使用。

因此,我们已经具备了编写核心 Hero()构造函数方法所需的一切,现在让我们开始吧。Hero()构造函数将接受与 Actor()构造函数相同数量的复杂参数。这些包括碰撞形状数据,包含在名为 SVGdata 的字符串对象中,子画面的“初始位置”X 和 Y 位置,以及子画面的图像对象(cels 或 frames)的逗号分隔列表,我将其命名为 Image...斯普里特塞尔。这张照片...designation 需要在参数列表的末尾,因为它是“开放式的”,这意味着参数列表将传入一个或多个图像对象。您的代码将如下所示:

public void Hero(String``SVGdata``, double``xLocation``, double``yLocation``, Image...``spriteCels``) {

super(``SVGdata``,``xLocation``,``yLocation``,``spriteCels``);

}

通过使用 super()将核心构造函数传递到 Actor 超类 Actor()构造函数方法,您之前编写的代码(在 Actor()构造函数内部)将使用 Java new 关键字和 SVGPath Shape 子类创建 spriteBound SVGPath Shape 对象,并将使用 SVGPath 类。setContent()方法,以便加载 SVGPath Shape 对象以及要用于 sprite 图像状态的碰撞形状。设置了 iX 和 iY 的初始位置,imageStates 列表数组加载了从参数列表末尾传入的 sprite 图像对象。

值得注意的是,因为我们是这样设置的,所以 Hero 类可以访问 Actor 类所拥有的一切(十三个强大的属性)。实际上,反过来看可能更“突出”,演员(固定精灵)类拥有英雄(运动精灵)类的所有能力。这种能力应该用于关卡设计 wow factor,包括多图像状态(List Array)、自定义 SVGPath 碰撞形状功能、自定义枢轴点位置,以及围绕 X 轴(FlipV = true)或 Y 轴(FlipH = true)或两个轴(FlipH = FlipV = true)翻转(镜像)精灵图像的能力。将这些功能放入你的 Actor 引擎(Actor 和 Hero 抽象超类)只是第一步;在你的游戏设计和编程中出色地使用它们,随着时间的推移,你会继续构建和完善游戏,这是本章中奠定基础的最终目标。正如你在图 8-10 中看到的,我们的基本(核心)构造函数代码是没有错误的。

A978-1-4842-0415-3_8_Fig10_HTML.jpg

图 8-10。

创建一个公共抽象类 Hero extends Actor 并添加一个构造函数方法和一个 super()构造函数

添加更新和冲突方法:。更新()和。碰撞()

现在我们有了一个基本的构造函数方法,稍后我们会添加它,让我们添加所需的抽象。update()方法,以及. collide()方法,因为运动精灵正在移动,因此可能会与物体发生碰撞!首先让我们添加public abstract void .update();方法,因为它是我们的 Actor 超类所需要的。这样做实质上是向下传递(或者向上传递,如果您愿意的话)了实现需求。update()方法,从 Actor 超类到 Hero 子类,并继续到 Hero 的任何未来子类(这将使 Hero 成为一个超类,并更好地反映其名称)。未来的非抽象(函数)类将实现这一点。update()方法,该方法将用于完成游戏编程逻辑的所有繁重工作。正如你在图 8-11 中看到的,运动精灵(Hero 子类)也需要有一个碰撞检测方法,我称之为。collide(),因为这是一个更短的名字,至少现在,除了返回一个布尔值 false(这里没有冲突,老板!)布尔数据值。的 Java 代码。collide()方法结构将把一个 Actor 对象作为它的参数,因为这是你的 Hero 对象将与之碰撞的对象,应该如下所示:

public boolean``collide``(``Actor object

return``false

}

A978-1-4842-0415-3_8_Fig11_HTML.jpg

图 8-11。

Add the @Override public abstract void .update() and public boolean .collide(Actor object) methods

接下来,我们再给这个英雄类增加十一个变量。这些将保存适用于运动精灵的数据值,这些精灵必须处理与物体的碰撞,并遵守物理定律。我们还需要一些东西,比如寿命变量,以及一个保存累积伤害(点数)的变量,如果敌人互相射击,伤害就会累积。我们将添加受保护的变量,如 X 和 Y 速度,X 和 Y 偏移(用于微调物体相对于精灵的位置),碰撞形状旋转和缩放因子,最后是摩擦,重力和反弹因子。

向 Hero 类添加 Sprite 控件和定义变量

我们需要做的下一件事是确保我们需要保存运动精灵数据的所有变量都在 Hero 类的顶部定义,如图 8-12 所示。NetBeans 将使用这些信息为 Hero 类创建 getter 和 setter 方法。Java 代码应该是这样的:

protected double``vX

protected double``vY

protected double``lifeSpan

protected double``damage

protected double``offsetX

protected double``offsetY

protected double``boundScale

protected double``boundRot

protected double``friction

protected double``gravity

protected double``bounce

A978-1-4842-0415-3_8_Fig12_HTML.jpg

图 8-12。

Add eleven variables at the top of Hero class defining velocity, lifespan, damage, physics, collision

在我们添加所有 22 个 getter 和 setter 方法之前,总共是 11 个。get()和 11。set()方法,为了匹配我们新的 Hero 类变量,让我们回过头来完成我们的 Hero()构造函数方法,并初始化我们刚刚在 Hero 类顶部添加的这十一个变量。

在 Hero 构造函数中初始化 Sprite 控件和定义变量

让我们给我们的英雄演员对象(运动精灵)1000 个单位的寿命,并设置其他变量为零,你可以看到我已经使用复合初始化语句来节省八行代码。如图 8-13 所示,代码没有错误,Java 编程语句应该采用以下格式:

lifespan =``1000

vX = vY = damage = offsetX = offsetY =``0

boundScale = boundRot = friction = gravity = bounce =``0

A978-1-4842-0415-3_8_Fig13_HTML.jpg

图 8-13。

Add initialization for your eleven variables inside of your constructor method using compound statements

在我们生成 getter 和 setter 方法之前,让我们看看如何使用复合变量声明语句的组合,以及如果我们不显式地指定它们,Java 将为我们的变量设置哪些默认变量类型值,以减少编写整个 Hero 类所需的代码量,从 25 行代码(如果不使用复合变量初始化语句,则为 33 行代码)减少到 14 行代码。

如果不计算带一个花括号(三个)的代码行,我们说的是不到十几行 Java 语句,包括包、类和导入声明,来编码整个公共抽象类。这是相当令人印象深刻的,考虑到核心类给了我们多少运动精灵的力量和能力。当然,在我们添加了 22 个 getter 和 setter 方法(每个方法有 3 行代码)后,我们将有大约 80 行代码,没有空格。值得注意的是,NetBeans 将为我们编写超过 75%的此类代码!相当酷。

通过复合语句和缺省变量值优化 Hero 类

在让 NetBeans 为我们编写 getter 和 setter 方法之前,我将做两件主要的事情来减少 Hero 类的主要部分的代码量。第一种方法是对所有相似的数据类型使用复合声明,首先声明受保护的 double 和受保护的 float 修饰符和关键字,然后在它们后面列出所有变量,用逗号分隔,这在编程术语中称为“逗号分隔”。11 个 Hero 类变量声明的 Java 代码现在将如下所示:

protected double vX, vY, lifeSpan, damage, offsetX, offsetY;

protected float boundScale, boundRot, friction, gravity, bounce;

正如你在图 8-13 和 8-14 中看到的,我们为初始化做了相同类型的复合语句:

lifeSpan = 1000;

vX = vY = damage = offsetX = offsetY = 0;

boundScale = boundRot = friction = gravity = bounce = 0;

如果您碰巧正在 HDTV 显示屏上进行编辑,也可以只用两行代码来完成:

lifeSpan = 1000;

vX = vY = damage = offsetX = offsetY = boundScale = boundRot = friction = gravity = bounce = 0;

接下来,如果我们依靠 Java 编译器将变量初始化为零,因为如果没有指定初始化值,double 和 float 变量将被初始化为,我们可以将这两行代码减少为一行代码:

lifeSpan = 1000;

现在我们已经完成了 Hero()构造函数方法的“核心”,让 NetBeans 编写一些代码吧!

A978-1-4842-0415-3_8_Fig14_HTML.jpg

图 8-14。

Optimize your Java code by using compound declarations, and leveraging default initialization values

访问英雄变量:创建 Getter 和 Setter 方法

在你的。collide()方法,并将光标放在那里,这将向 NetBeans 显示您希望它放置将要生成的代码的位置。这在图 8-15 中由源菜单后面的浅蓝色阴影线显示。使用源>插入代码菜单序列或 Alt-Insert 击键组合,当生成浮动弹出菜单出现在这条蓝线下时(这显示了选中的代码行),选择 Getter 和 Setter 选项,在图 8-15 中高亮显示,并选择所有的英雄职业。确保选择了所有的英雄类变量,或者通过使用英雄类主选择复选框,或者通过使用每个变量的复选框 UI 元素,如图 8-15 的右侧所示。

A978-1-4842-0415-3_8_Fig15_HTML.jpg

图 8-15。

Use the Source ➤ Insert Code ➤ Generate ➤ Getter and Setter menu sequence and select all class variables

单击“生成 Getters 和 Setters”对话框底部的“生成”按钮后,您将看到 22 个新方法,它们都是 NetBeans 为您编写的全新方法。这些方法如下所示:

public double``getvX()

return vX;

}

public void``setvX(double vX)

this.vX = vX;

}

public double``getvY()

return vY;

}

public void``setvY(double vY)

this.vY = vY;

}

public double``getLifeSpan()

return lifeSpan;

}

public void``setLifeSpan(double lifeSpan)

this.lifeSpan = lifeSpan;

}

public double``getDamage()

return damage;

}

public void``setDamage(double damage)

this.damage = damage;

}

public double``getOffsetX()

return offsetX;

}

public void``setOffsetX(double offsetX)

this.offsetX = offsetX;

}

public double``getOffsetY()

return offsetY;

}

public void``setOffsetY(double offsetY)

this.offsetY = offsetY;

}

public float``getBoundScale()

return boundScale;

}

public void``setBoundScale(float boundScale)

this.boundScale = boundScale;

}

public float``getBoundRot()

return boundRot;

}

public void``setBoundRot(float boundRot)

this.boundRot = boundRot;

}

public float``getFriction()

return friction;

}

public void``setFriction(float friction)

this.friction = friction;

}

public float``getGravity()

return gravity;

}

public void``setGravity(float gravity)

this.gravity = gravity;

}

public float``getBounce()

return bounce;

}

public void``setBounce(float bounce)

this.bounce = bounce;

}

值得注意的是,使用 Hero 类创建的对象也可以访问我们之前为 Actor 类生成的 getter 和 setter 方法。如果你想知道 Java 关键字在所有这些中意味着什么。set()方法,它引用的是使用 Actor 或 Hero 类构造函数方法创建的当前对象。因此,如果您调用。iBagel Bagel 对象(我们将在第十章中创建)的 setBounce()方法,这个关键字指的是这个(iBagel) Bagel 对象实例。因此,如果我们想要设置 50%的反弹因子,我们将使用我们的新。setBounce() setter 方法:

iBagel.``setBounce``(``0.50

接下来让我们看看这些 sprite Actor 类是如何与我们在本书中编写的其他类相适应的。在那之后,我们将总结我们在这一章中学到的东西,我们可以进入这本书的未来章节,并使用这些类为我们的游戏创建精灵,就像我们学习如何在游戏中使用精灵一样。

更新游戏设计:演员或英雄如何融入

让我们更新一下我在第七章(图 7-19)中介绍的图表,以包括 Actor.java 和 Hero.java 类。正如你在图 8-16 中看到的,我不得不切换。更新()物理和。collide()图的冲突部分,因为 Actor 类只包括。update()方法,而 Hero 类包含了这两种方法。自从。collide()方法将在。update()方法,我也用 chrome 球体连接了图的这两个部分。

那个。GamePlayLoop 对象中的 handle()方法将调用这些。update()方法,所以这里也有联系。Actor 和 Hero 类与 InvinciBagel 类之间存在联系,因为使用这些抽象类创建的所有游戏 sprite 对象都将在该类的方法中声明和实例化。

我们在开发我们的游戏引擎框架方面取得了很大的进展,同时,我们也看到了 Java 8 编程语言的一些核心特性是如何为我们所用的。在下一章的事件处理中,我们将会看到强大的 Java 8 lambda expressions 新特性,所以关于 Java 8 前沿特性的更多知识将会在游戏中出现。希望你和我一样激动!

A978-1-4842-0415-3_8_Fig16_HTML.jpg

图 8-16。

The current invincibagel package class (object) hierarchy, now that we have added Actor and Hero classes

摘要

在第八章中,我们写了第二轮的游戏引擎,我们将在本书中设计和编码,演员(固定精灵)超类,和它的英雄(运动精灵)子类。一旦我们在第十章和后续章节开始创建游戏精灵,英雄职业也将成为超职业。从本质上讲,在这一章中,你学习了如何创建公共抽象类,这些类将在本书中用来定义我们的 sprite 对象。这相当于为我们游戏中的所有演员(精灵)做了所有繁重的工作(精灵设计和编码工作),使我们从现在开始为我们的游戏创建强大的固定和运动精灵变得更加容易。我们正在首先建立我们的知识库和我们的游戏引擎框架!

我们首先看一下这些演员和英雄类将如何设计,以及我们将使用它们创建什么类型的实际精灵类。我们查看了九个 sprite 图像资源,以及这些资源如何通过仅使用九个资源来覆盖广泛的运动,并查看了如何相对于彼此“注册”sprite“状态”。

接下来,我们设计并创建了我们的 Actor 超类,以处理固定的精灵,如 props 和 treasure,创建了基本的 List 、ImageView、SVGPath、iX 和 iY 变量以及一个构造函数方法,该方法使用这些来定义固定的精灵外观、位置和碰撞边界。然后,我们添加了一些额外的变量,我们将需要在未来的游戏设计方面,并了解如何让 NetBeans 写。get()和。set()方法。

接下来,我们设计并创建了我们的 Hero 子类,它扩展了 Actor 来处理运动精灵,例如不可战胜的妖怪自己和他的敌人,以及投射物和移动挑战。我们创建了基本的构造函数方法来设置 Actor 超类中的变量,这次是为了定义运动精灵图像、初始位置和碰撞边界。然后,我们添加了一些额外的变量,这将是我们在未来的游戏设计方面需要的,并再次看到了 NetBeans 将如何编写我们的。get()和。为我们设置()方法,看起来总是很有趣!

最后,我们看了一下更新的 invincibagel 包、类和对象结构图,看看在本书的前八章中我们已经取得了多大的进步。这越来越令人兴奋了!

在下一章中,我们将看看如何控制游戏精灵,我们将使用本章中创建的演员引擎来创建游戏精灵。接下来的第九章将涵盖 Java 8 和 JavaFX 事件处理,这将允许我们的游戏玩家使用事件处理来操纵(控制)这些演员精灵。

九、控制你的演员形象:实现 Java 事件处理器和使用 Lambda 表达式

既然我们已经在第八章中创建了公共抽象演员和英雄类,我称之为“演员引擎”,让我们回到第九章中的 InvinciBagel 主要应用类代码,并创建事件处理框架,我们可以用它来控制我们游戏的主要英雄,InvinciBagel 本人。实现玩家和游戏编程逻辑之间的接口的事件处理可以被看作是你的游戏的“交互引擎”,如果我们遵循我们到目前为止一直使用的引擎范式。有许多与游戏交互的方式,包括箭头键(也称为消费电子设备的 DPAD)、键盘、鼠标或轨迹球、游戏控制器、触摸屏,甚至高级硬件,如陀螺仪和加速度计。你将为你的游戏开发做出的选择之一将是玩家将如何与游戏交互,使用他们玩游戏的硬件设备,以及它支持的输入能力。

在这一章中,我们将对你的 InvinciBagel.java 类进行一些升级。首先是以 Java 常量的形式添加对游戏宽度和高度变量的支持。这些将允许我们改变游戏表面的宽度和高度,游戏表面是弹出的游戏窗口内部的区域,或者如果您的游戏玩家使用消费电子设备,则是整个屏幕。

我们要做的第二个升级是添加 Java 代码,这将为我们在接下来的几章中设计游戏创建一个空白的白色屏幕。我们将通过安装一种颜色来实现这一点。Scene()构造函数方法调用中的白色背景颜色(以及我们新的 width 和 height 变量),然后将 Java 代码安装到已经就位的按钮控件事件处理器结构中,以隐藏我们用于闪屏 UI 设计的图像合成的两个 ImageView“plates”。我们也可以在以后使用这两个 ImageView 节点对象来保存我们游戏的背景图像,一旦我们进入那个设计层次。请记住,保持场景图形中的节点数量最少是很重要的,因此我们将重用节点对象,而不是添加更多。

我们将添加的第三个升级是向我们的场景对象添加键盘事件处理例程,该例程将处理我们将在游戏中使用的箭头键支持,以跨越任何具有箭头键键盘或 DPAD 的硬件设备。这将处理场景顶层到 StackPane(场景图)层次的所有事件。这将把用户按下的箭头键值传递给我们的节点对象。这将最终允许运动控制代码在游戏中移动演员,这是我们将在下一章更详细讨论的内容。

除了升级我们的 InvinciBagel.java 代码,增加键盘事件处理,我们还将在本章学习 lambda 表达式,以确保我在本书中涵盖了 Java 8 中的所有新内容。这些 lambda 表达式有些高级,不适合在本初学者手册中介绍,但是由于它们是 Java 8 的一个主要新特性,并且提供了多线程支持和更紧凑的代码结构,所以我将在本章中介绍它们,部分原因是 NetBeans 8(不出所料)愿意为您编写它们!

游戏表面设计:增加分辨率的灵活性

我想对 InvinciBagel.java 代码做的第一件事是为游戏应用添加宽度和高度常量,该代码应该已经在 NetBeans 的选项卡中打开(如果没有打开,请使用右键单击并打开工作流程)。这样做的原因是,您可能希望为上网本或平板电脑(1024×600)、iTV 电视机或电子阅读器(1280×720)或 HDTV 应用(1920×1080)甚至新的 4K iTV 电视机(4096×2160)提供定制版本。有了高度和宽度变量,您就不必在以后更改 Scene()构造函数方法调用,并使用这些变量而不是整个代码中的“硬编码”数值来进行某些屏幕边界计算。正如你在图 9-1 的顶部所看到的,我已经用一行 Java 代码为这两个变量创建了一个常量声明,这就是我们在前一章所学的复合语句。这个声明的 Java 代码可以在图 9-1 中的类的顶部看到,应该如下所示:

static final double``WIDTH``= 640,``HEIGHT

A978-1-4842-0415-3_9_Fig1_HTML.jpg

图 9-1。

Add private static final double WIDTH and HEIGHT constants; install these, and Color.WHITE, in Scene()

接下来我们要做的是升级我们的 Stage()构造函数方法调用,以使用另一个重载的构造函数方法,该方法允许我们指定背景颜色值。让我们使用颜色类常量 WHITE,以及我们新的宽度和高度显示屏幕大小常量,并使用下面一行 Java 代码创建这个新的构造函数方法调用,这些代码也显示在图 9-1 的底部(充满错误):

scene = new Scene(root,``WIDTH``,``HEIGHT``,``Color.WHITE

正如您在图 9-1 中所看到的,在您的颜色类引用下将有一条红色波浪错误下划线,直到您使用 Alt-Enter 工作进程调出助手对话框(如图所示),并选择指定“为 javafx.scene.paint.Color 添加导入”的选项,以便让 NetBeans 为您编写 Java 导入语句。一旦你这样做了,你的代码就不会有错误,我们将准备写一些代码来放置背景颜色。

为此,我们需要隐藏保存全屏(splashScreenbackplate)和覆盖(splashScreenTextArea)图像资产的 ImageView 节点对象。我们将通过将 visible 属性(或者特性,或者参数,如果您愿意的话)设置为 false 值来实现这一点,这将允许我们设置的白色背景颜色显示出来。

完成 UI 设计:编写一个游戏按钮

下一件事,我们将需要做的是完成按钮控制事件处理代码,以便当我们单击游戏播放按钮对象时,白色背景显示出来,供我们开发我们的游戏。稍后,我们可以使用 ImageView 板来支持闪屏,为游戏提供背景图像合成,使游戏在视觉上更加有趣。我们在场景图中隐藏两个 ImageView 节点对象的方法是调用。setVisible()方法从。附加到 PLAY GAME UI 按钮对象的 handle()方法。这可以在图 9-2 的底部看到,可以使用下面两行 Java 代码在。handle()EventHandler方法结构:

splashScreenBackplate.setVisible(``false

splashScreenTextArea.setVisible(``false

A978-1-4842-0415-3_9_Fig2_HTML.jpg

图 9-2。

Use a .setVisible() method for the ImageView class to hide background image plates and reveal White color

正如您所看到的,在这个事件处理结构的 EventHandler 部分下还有一个黄色的警告高亮显示,它与 Java lambda 表达式相关。在我们完成实现所有按钮的最终 UI 设计代码之后,这些代码控制着玩家可以看到什么,以及显示给他们的图像,我将进入 lambda 表达式,我们也将在代码中消除这些警告消息。之后,我们将继续实现箭头键事件处理结构,以便用户可以在屏幕上导航 InvinciBagel。

首先,让我们测试我们放入之前空的 PLAY GAME 按钮事件处理结构中的代码。

测试游戏按钮:确保你的代码工作

使用运行➤项目(或 IDE 顶部看起来像视频播放传输按钮的运行图标)启动 invincibagel 游戏,然后单击窗口左下角的播放游戏按钮。正如您在图 9-3 中看到的,屏幕变成白色,因为两个 ImageView 图像板不再可见,并且您的白色背景颜色正在透过!你会注意到,如果你点击其他三个按钮控制,他们不再工作。实际上他们正在工作,但是他们不再可见,所以 UI 设计现在看起来是坏的!

A978-1-4842-0415-3_9_Fig3_HTML.jpg

图 9-3。

Run the Project, and test the PLAY GAME Button to reveal white background

所以现在我们有能力看到我们正在做什么进入我们的游戏设计章节,这是本书接下来的八章。我们现在要做的就是修复(或者说升级)其他三个按钮控件事件处理结构,以包含方法调用,从而确保 ImageView 节点对象可见,以便它们可以显示我们希望它们向玩家显示的图像内容。让我们接下来处理这个问题,因为我们正在为 InvinciBagel 游戏进行按钮 UI 设计,然后我们可以看看新的 Java 8 lambda 表达式。

升级其他 UI 按钮代码:使 ImageView 可见

让我们向三个现有的按钮事件处理结构中的每一个添加几行代码,以确保两个 ImageView 图像板都设置为在单击这些按钮控件对象时可见。我们之所以需要将这些代码放入其他三个 UI 按钮事件处理结构中,是因为我们不知道用户点击按钮的顺序,所以我们需要将这些语句放入每个按钮事件处理器中。如果当这些语句被触发时,ImageView 节点对象已经被设置为可见,那么它们将仅仅保持可见,因为它们之前已经是可见的!Java 方法调用将如下所示:

splashScreenBackplate.setVisible(``false

splashScreenTextArea.setVisible(``false

正如你在图 9-4 中突出显示的,我已经在其他三个按钮事件处理结构中安装了这两个语句(除了它们引用的图像对象之外,其他都一样)。这些语句可以放在每个现有方法调用之前或之后。handle()方法。

A978-1-4842-0415-3_9_Fig4_HTML.jpg

图 9-4。

Add in the .setVisible(true) method calls for the splashScreenBackplate and splashScreenTextArea objects

现在你会看到,当你使用你的运行➤项目的工作过程中,你所有的按钮用户界面控件将做他们应该做的事情,并会显示一个白色的游戏背景或信息屏幕背后有 InvinciBagel 闪屏艺术作品。现在我们准备学习 Java lambda 表达式。

Lambda 表达式:一个强大的 Java 8 新特性

Java 8 在 2014 年发布的主要新特性之一是 lambda 表达式。使用 lambda 表达式可以使您的代码更加紧凑,并允许您使用 Java 8 的新 lambda->“arrow”操作符将方法转换为简单的 Java 语句。lambda 表达式提供了一种 Java 代码快捷方式,通过使用 lambda 表达式来构造一个单一的方法接口。

Java 8 lambda 表达式具有与 Java 方法结构相同的特性,因为它需要传入一系列参数,还需要指定代码“主体”。lambda 表达式调用的代码可以是单个 java 语句,也可以是包含多个 Java 编程语句的代码块。将利用传递到 lambda 表达式中的参数来表达该语句。简单 lambda 表达式的基本 Java 8 语法应该编写如下:

(the parameter list)``->

您还可以通过使用 Java 中使用的花括号来创建一个复杂的 lambda 表达式,该花括号用于结合 lambda 表达式定义整个 Java 代码语句块。这将通过使用以下格式来完成:

(the parameter list) -> { statement one; statement two; statement three; statement n; }

需要注意的是,您不必使用 lambda 表达式来替换传统的 Java 方法!事实上,如果你希望你的代码与 Java 7 兼容,例如,如果你希望你的代码也能在使用 Java 7 的 Android 5.0 中工作,你就不必使用 lambda 表达式。然而,由于这是一个专门的 Java 8 游戏开发标题,由于 lambda 表达式是 Java 8 的主要新特性,由于 NetBeans 会将您的 Java 方法转换为 lambda 表达式,正如您将要看到的,我决定在本书中使用它们。

让我们仔细看看让 NetBeans 将 Java 方法转换成 lambda 表达式的工作过程。正如你在图 9-5 中看到的,你当前在你的代码中有波浪形的黄色警告高亮。

当你把鼠标放在这些上面时,它们会给你一个“这个匿名的内部类创建可以被转换成一个 lambda 表达式”的消息。这让你知道 NetBeans 8.0 可能愿意为你写一些 lambda 表达式代码,真的很俏皮。

A978-1-4842-0415-3_9_Fig5_HTML.jpg

图 9-5。

Mouse-over wavy yellow warning highlight, and reveal a “inner class can be turned into a lambda” pop-up

要找到答案,您需要利用您信任的 Alt-Enter 工作流程,正如您所看到的,有一个 lambda 表达式选项,它将让 NetBeans 将代码重写为 lambda 表达式。原始代码如下所示:

gameButton.setOnAction(new EventHandler<ActionEvent>() {

@Override public void handle(ActionEvent event) {

splashScreenBackplate.setVisible(false);

splashScreenTextArea.setVisible(false);

}

});

NetBeans 编码的 lambda 表达式要简洁得多,看起来像下面的 Java 8 代码:

gameButton.setOnAction((ActionEvent event) -> {

splashScreenBackplate.setVisible(false);

splashScreenTextArea.setVisible(false);

});

正如您在图 9-6 中看到的,NetBeans 为您编写的这个 lambda 表达式本身并不是没有警告,因为有一个“参数事件未使用”警告,所以我们接下来将删除该事件,以使 lambda 表达式更加简洁!在某个时间点,Oracle 将更新这段编写 lambda 表达式的代码,以便它查看您的方法代码块内部,发现没有引用事件对象,并将删除它,这样就不会再生成警告。在那之前,我们需要自己编辑 NetBeans 的代码。

A978-1-4842-0415-3_9_Fig6_HTML.jpg

图 9-6。

The lambda expression that NetBeans writes for you has a warning message “Parameter event is not used”

因为我们在这个 lambda 表达式的代码体中没有使用 event 变量,所以我们可以删除它,得到下面这个最终的 Java 8 lambda 表达式代码,它比原始代码简单得多:

gameButton.setOnAction((``ActionEvent

splashScreenBackplate.setVisible(false);

splashScreenTextArea.setVisible(false);

});

如您所见,lambda 表达式要求 Java 编译器为您创建 ActionEvent 对象,用它创建的 ActionEvent 对象替换新的 EventHandler ()构造函数方法调用。如果您想知道为什么 lambda 表达式被添加到 Java 8 中,以及它们如何使它变得更好,它们允许 Java 函数(方法),尤其是“一次”或内部方法,像语句一样编写。它们也有助于多线程。

在我们开始学习 Java 8 和 JavaFX 中的事件处理类之前,让我们先来看看我在写这一章时遇到的一个更新,以及出现的几个警告亮点,它们并不准确。

处理 NetBeans 意外更新和错误警告

当我“升级”我的 UI 按钮事件处理代码结构以使用 lambda 表达式时,如图 9-7 所示,我注意到了一些事情,我想在进入事件处理之前用几页纸来解决。首先,有一个错误的警告“未使用参数 ActionEvent”,这是不正确的,因为事件处理构造固有地使用 ActionEvent 对象,除此之外,为什么上面和下面的其他相同构造没有显示相同的警告?我运行了代码,一切运行良好,所以我忽略了 NetBeans 中的这一亮点。我还在 IDE 的右下角看到一条“发现 39 个更新”的消息,因此我单击了蓝色链接“单击此处更新您的 IDE”,并拍摄了几张屏幕截图,展示了我更新 IDE 的工作过程。我不确定 NetBeans 是从哪里得到 39 的,因为安装程序对话框中列出了数百个更新,如图 9-7 的右侧所示。正如您所看到的,对 JavaFX、Java 8 和相关包以及 NetBeans 8 支持的非 Java 包进行了大量更新。我点击了下一步按钮,并调用了下载和更新过程,这需要几分钟。

A978-1-4842-0415-3_9_Fig7_HTML.jpg

图 9-7。

Showing the incorrect lambda expression warning message and the 39 updates found notification message

正如您在图 9-8 的左侧所看到的,您必须阅读并接受所有相关的许可协议,这些许可协议是您下载并安装所有软件包升级所必需的,这些软件包升级是自您最初安装 Java 8 和 NetBeans(或自您上次更新 ide)以来升级到的。

选择“我接受所有许可协议中的条款”复选框,然后单击“更新”按钮,开始下载和安装过程。正如您在图 9-8 的右下方看到的,进度条会告诉您已经下载了多少,以及正在下载和安装到系统上的内容。

A978-1-4842-0415-3_9_Fig8_HTML.jpg

图 9-8。

Showing the License Agreement dialog (left) and the download and update progress bar (right) in NetBeans

让我们用本章的剩余部分来看看事件处理,以及 Java 和 JavaFX 中与事件相关的类,我们可以用它们来为我们的 Java 8 游戏开发工作提供不同类型的事件处理。

事件处理:增加游戏的互动性

有人可能会说,事件处理是游戏开发的基础和核心,因为如果没有一种与游戏逻辑和演员进行交互的方式,你就真的没有一款游戏。我将在本章的这一节讲述 Java 和 JavaFX 事件处理相关的类,然后我们将实现键盘事件处理,这样我们就可以支持使用箭头键在屏幕上导航我们的 InvinciBagel 角色。之后,我们将把兼容 Java 7 的代码转换成兼容 Java 8 的 lambda 表达式,然后我们将在本书的下一章讨论精灵在屏幕上的移动。在我们开始剖析 Java 和 JavaFX 类之前,我想说的第一件事是为游戏处理的不同类型的事件,从箭头键(智能手机上的 DPAD)到键盘(或 iTV 的遥控器),到鼠标(或智能手机上的轨迹球),到触摸屏(智能手机和平板电脑),到游戏控制器(游戏控制台和 iTV 电视机),到陀螺仪和加速度计(智能手机和平板电脑),再到运动控制器,如 Leap Motion 和 Razer Hydra Portal。

控制器的类型:我们应该处理什么类型的事件?

要考虑的一个关键问题是什么是支持游戏相关事件的最符合逻辑的方法,比如箭头键;鼠标点击;触摸屏事件;游戏控制器按钮(A、B、C 和 D);以及更先进的控制器,如 Android、Kindle、Tizen 和 iOS 消费电子设备上可用的陀螺仪和加速度计。这个决定是由游戏运行的硬件设备决定的;如果一个游戏需要在任何地方运行,那么最终将需要处理不同事件类型的代码,甚至是不同的事件处理编程方法。我们将看看 Java 8 目前支持哪些输入事件。

有趣的是,Java 8 和 JavaFX 应用已经在这些嵌入式平台上运行,我会把钱花在开放平台(Android、Tizen、Chrome、Ubuntu)和目前支持 Java 技术的专有平台(Windows、黑莓、三星 Bada、LGE WebOS、Firefox OS、Opera 等)的原生支持上。),在近期的某个时间点。Java 8 的未来是光明的,感谢 JavaFX 和硬件平台的支持!

Java 8 和 JavaFX 事件:javafx.event 和 java.util

如您所见,javafx.event 包的 EventHandler 公共接口扩展了 java.util 包的 EventListenerinterface,它是创建和处理事件对象的方式,可以使用匿名内部类(Java 7)结构,也可以使用 lambda 表达式(Java 8)。现在,您已经熟悉了如何编写这两种类型的事件处理结构,在本书中,我将继续使用 Java 7(匿名内部类)方法编写方法,然后我将使用 NetBeans 将它们转换为 Java 8 lambda 表达式,以便您可以创建与 Java 7 (Android)和 Java 8 (PC OS)游戏代码交付管道兼容的游戏。

到目前为止,您在本书中用于用户界面按钮控件事件处理的 Actionevent 类(和对象)是 Event 超类的子类,Event 超类是 java.util 包的 EventObject 超类的子类,Event object 超类是 java.lang.Object 主类的子类。整个类层次结构如下所示:

java.lang.Object

> java.util.EventObject

> javafx.event.Event

> javafx.event. ActionEvent

ActionEvent 类也与 EventHandler 公共接口一起位于 javafx.event 包中。从现在开始,我们将使用的所有其他事件相关类都包含在 javafx.scene.input 包中。我将在本章的剩余部分重点介绍 javafx.scene.input 包,因为您已经学习了如何在 Java 7 中使用 EventHandler {…}结构,在 Java 8 中使用(ActionEvent) -> {…}结构,所以是时候学习如何在我们的 Java 8 游戏开发工作流程中使用其他类型的事件,称为输入事件。

接下来让我们看看这个重要的 JavaFX 场景输入事件包,以及它提供给我们用于 Java 8 游戏开发的 25 个输入事件类。

JavaFX 输入事件类:javafx.scene.input 包

尽管 java.util 和 javafx.event 包包含核心 eventObject、Event 和 EventHandler 类来“处理”事件,但在确保事件得到处理(处理)的基础级别,还有另一个名为 javafx.scene.input 的 javafx 包,它包含我们有兴趣用来处理(处理)玩家对您可能创建的不同类型游戏的输入的类。我将称之为“输入事件”,因为它们不同于我们在本书中遇到的动作事件和脉冲(计时)事件。一旦我们在本章中讲述了输入事件,您将会熟悉许多您想要在自己的 Java 8 游戏开发中使用的不同类型的事件。在本章的后面,我们还将实现一个 KeyEvent 对象来处理游戏中箭头键盘(或 DPAD 和游戏控制器)的使用。

有趣的是,javafx.scene.input 包中支持的许多输入事件类型更适合消费电子(行业术语是“嵌入式”)设备,如智能手机或平板电脑,这告诉我 javafx 正被定位(设计)用于 Android 或 Chrome 等开源平台。JavaFX 有专门的事件,如 GestureEvent、SwipeEvent、TouchEvent 和 ZoomEvent,它们支持新的嵌入式设备市场中的特定功能。这些输入事件类支持高级触摸屏设备特征,例如手势、页面滑动、触摸屏输入处理和多点触摸显示,这些都是这些设备所需的特征,它们支持高级输入范例,例如双指(收缩或展开)触摸输入,例如放大和缩小屏幕上的内容。

我们将在本书中涵盖更多“通用”输入类型,这些类型在个人电脑(台式机、笔记本电脑、笔记本电脑、上网本和较新的“专业”平板电脑,如 Surface Pro 3)以及嵌入式设备(包括智能手机、平板电脑、电子阅读器、iTV 电视机、游戏控制台、家庭媒体中心、机顶盒等)上都受支持。这些设备还将处理这些更广泛的(在它们的实现中)按键事件和鼠标事件类型的输入事件,因为鼠标和按键事件总是被传统软件包支持。

有趣的是,触摸屏显示器将“处理”鼠标事件和触摸事件,这非常方便,可以确保您的游戏在尽可能多的不同平台上运行。我经常在我的 Android 书籍标题中使用这种使用鼠标事件处理的方法,以便用户可以使用触摸屏和 DPAD 中心(点击)按钮来生成鼠标点击事件,而不必专门使用触摸事件。对于触摸屏用户来说,尽可能使用鼠标(点击)事件的另一个优点是,如果您使用触摸事件,您将无法进行其他操作,也就是说,您的游戏应用只能在触摸屏设备上运行,而不能在具有某种类型鼠标硬件的设备(如 iTV 电视机、笔记本电脑、台式机、上网本等)上运行。

同样的原则也适用于按键事件,尤其是我们将在这个游戏中使用的箭头键,因为这些键可以在键盘和遥控器的箭头小键盘上、游戏控制器上以及大多数智能手机的 DPAD 上找到。我还将向您展示如何包含备用键映射,以便您的玩家可以决定他们更喜欢使用哪种输入法来玩您的 Java 8 游戏。接下来让我们看看 KeyCode 和 KeyEvent 类。

KeyCode 类:使用枚举常量来定义玩家在游戏中使用的键

由于我们将在游戏中使用箭头小键盘,并且可能使用 A-S-D-W 键,以及未来游戏控制器的 GAME_A、GAME_B、GAME_C 和 GAME_D 按钮,所以让我们先仔细看看 KeyCode 类。这个类是一个公共枚举类,保存枚举常量值。这个类是 KeyEvent 类获取 KeyCode 常量值的地方,KeyCode 常量值用于(处理)确定播放器在任何特定的键事件调用中使用了哪个键。KeyCode 类的 Java 8 和 JavaFX 类层次结构如下所示:

java.lang.Object

> java.lang.Enum<KeyCode>

> javafx.scene.input. KeyCode

keycode 类中包含的常量值使用大写字母,并以 KeyCode 支持的键命名。例如,A、S、W 和 d 键码是 A、S、W 和 d。箭头键盘键码是上、下、左和右,游戏控制器按钮键码是 GAME_A、GAME_B、GAME_C 和 GAME_D

在我们介绍了这些用于输入事件处理的基础包和类之后,我们将在 EventHandler 对象中实现 KeyCode 常量和 KeyEvent 对象。您很快就会看到,这与设置 ActionEvent 的处理方式非常相似,KeyEvents 可以使用 Java 7 内部类方法或 Java 8 lambda 表达式进行编码。

我们将以非常模块化的方式设置我们的 KeyEvent 对象处理,以便事件键码评估结构为每个键码映射设置布尔标志变量。事件处理的本质是它是一个实时引擎,就像脉冲引擎一样,所以这些布尔标志将提供玩家在任何给定纳秒内按下或释放了什么键的准确“视图”。然后,可以通过在我们的其他游戏引擎类中使用 Java 游戏编程逻辑来读取和操作这些布尔值,这些游戏引擎类将实时处理这些关键事件。

KeyEvent 类:使用 KeyEvent 对象来保存玩家正在使用的键码

接下来,让我们仔细看看 KeyEvent 类。该类被指定为 public final KeyEvent,它扩展了 InputEvent 超类,后者用于创建 javafx.scene.input 包中的所有输入事件子类。使用 EventHandler 类将 KeyEvent 类设置为 motion,并处理 KeyCode 类常量值。该类的层次结构从 java.lang.Object 主类开始,经过 java.util.EventObject 事件超类,到 javafx.event.Event 类,后者用于创建 KeyEvent 类扩展的 javafx.scene.input.InputEvent 类(子类)。有趣的是,我们在这里跨越了四个不同的包!

KeyEvent 类的 Java 8 和 JavaFX 类层次结构从 java.lang 包跳转到 java.util 包、javafx.event 包、javafx.scene.input 包。KeyEvent 类的层次结构如下所示:

java.lang.Object

> java.util.EventObject

> javafx.event.Event

> javafx.scene.input.InputEvent

> javafx.scene.input. KeyEvent

EventHandler 对象生成的 KeyEvent 对象表示发生了击键。KeyEvent 通常在场景图节点中生成,例如可编辑的文本 UI 控件,但是在我们的示例中,我们将场景图节点层次结构之上的事件处理直接附加到名为 Scene 的场景对象,希望避免将事件处理附加到场景图中的任何节点对象(在我们的示例中,当前是名为 root 的 StackPane 对象)而导致的任何场景图处理开销。

每当按下并按住、释放或键入(按下并立即释放)某个键时,就会生成一个 KeyEvent 对象。根据这个按键操作本身的性质,您的 KeyEvent 对象被传递到一个。onKeyPressed(),一个。onKeyTyped()或。onKeyReleased()方法,以便在嵌套的。handle()方法,它将保存游戏特有的编程逻辑。

游戏通常使用按键和按键释放事件,因为用户通常按住按键来移动游戏中的角色。另一方面,键入事件往往是“高级”事件,通常不依赖于操作系统平台或键盘布局。键入的按键事件(。onKeyTyped()方法调用)将在输入 Unicode 字符时生成,并用于为 UI 控件(如文本字段)获取字符输入,并用于业务应用(如日历和文字处理器)。

在一个简单的例子中,击键事件将通过使用一次击键及其立即释放来产生。此外,可以使用按键事件的组合来产生替换字符,例如,可以使用 SHIFT 按键和“A”按键类型(按下并立即释放)来产生大写字母 A。

生成键类型的 KeyEvent 对象通常不需要按键释放。需要注意的是,在某些极端情况下,直到释放按键时才会生成按键类型的事件;这方面的一个很好的例子是输入 ASCII 字符代码序列的过程,使用的是老式的 Alt 键和数字键盘输入方法,这种方法在“过去”用于 DOS,并保留到 Windows 操作系统中。

需要注意的是,对于不生成任何 Unicode 字符的键,不会生成键类型的 KeyEvent 对象。这将包括动作键或修饰键,尽管它们会生成按键和释放按键的 KeyEvent 对象,因此可以用于游戏!一般来说,这不是一个好的用户界面设计(或用户体验设计)方法,因为这些键是用来修改其他键行为的。

KeyEvent 类有一个字符变量(我倾向于称之为字符特征,但我不会这样做),对于键入键的事件,它总是包含一个有效的 Unicode 字符,对于按下键或释放键的事件,它总是包含 CHAR_UNDEFINED。字符输入仅针对键入的事件进行报告,因为按键和按键释放事件不一定与字符输入相关联。因此,字符变量保证只对键类型的事件有意义。在某种意义上,通过不使用键类型事件,我们节省了内存和 CPU 处理,因为不必处理这个 Unicode 字符变量。

对于按键和按键释放的 KeyEvent 对象,KeyEvent 类中的 code 变量将包含您的 KeyEvent 对象的 keycode,该 KeyCode 是使用您之前学习过的 KeyCode 类定义的。对于键入事件,这个代码变量总是包含常量 KeyCode.UNDEFINED。所以正如你所看到的,按下键和释放键的用法与键入不同,这就是我们在游戏事件处理中使用它们的原因。

按键和按键释放事件是低级别的,取决于平台或键盘布局。每当按下或释放给定的键时都会生成字符输入,这是“轮询”不生成字符输入的键的唯一方式。被按下或释放的键由 code 变量指示,该变量包含一个虚拟键码。

添加键盘事件处理:使用 KeyEvents

我认为这是足够的背景信息,让我们继续实现游戏的按键事件处理,所以在你的屏幕宽度和高度常量声明后添加一行代码,并使用一个复合声明语句声明四个名为 up、down、left 和 right 的布尔变量,如图 9-9 所示。由于任何布尔值的缺省值都是 false(这将表示一个键没有被按下,也就是说,一个键当前被释放),我们不必显式初始化这些变量。这是通过使用下面一行 Java 代码完成的,它也显示在图 9-9 的顶部,没有错误:

boolean up, down, left, right;

A978-1-4842-0415-3_9_Fig9_HTML.jpg

图 9-9。

Add a .setOnKeyPressed() function call off a scene object and create a new EventHandler object

正如你在图 9-9 的底部看到的,我用。setOnKeyPressed()方法调用名为 Scene 的场景对象,我已经在前面的代码行中实例化了该对象。在这个方法调用中,我创建了一个新的 EventHandler < KeyEvent >,就像我们为动作事件所做的一样。如您所见,在您导入 KeyEvent 类之前,这段代码附有一条错误消息,如下所示:

scene``.setOnKeyPressed

使用 Alt-Enter 工作流程选择导入 javafx.scene.input.KeyEvent 选项,如图 9-9 所示,以消除此错误消息。接下来,我们来看看。我们需要编写 handle()方法来处理 KeyEvent。

处理 KeyEvent:使用 Switch-Case 语句

KeyEvent 对象处理是实现 Java 高效 switch-case 语句的完美应用。我们可以为每种类型的 KeyCode 常量添加一个 case 语句,该常量包含在传递到。handle()方法。可以使用. getCode()方法从 KeyEvent 对象中提取键码。在 switch()评估区域内,对名为 Event 的 KeyEvent 对象调用此方法。在 switch{}主体内部,case 语句将自己与提取的 KeyCode 常量进行比较,如果匹配,就处理冒号后面的语句。休息;语句允许处理退出开关情况评估,这是一种优化。

该事件处理开关案例结构应通过使用以下 Java 编程结构来实现,该结构也在图 9-10 中突出显示:

scene``.setOnKeyPressed

@Override

public void handle(KeyEvent``event

switch (``event

case UP:    up    = true; break;

case DOWN:  down  = true; break;

case LEFT:  left  = true; break;

case RIGHT: right = true; break;

}

}

});

A978-1-4842-0415-3_9_Fig10_HTML.jpg

图 9-10。

Add a switch-case statement inside of the public void handle() method setting Boolean direction variables

现在我们有了基本的按键事件处理结构,稍后我们会添加到这个结构中,让 NetBeans 将 Java 7 代码转换成 Java 8 lambda 表达式!之后,我们可以通过使用块复制和粘贴操作来创建一个按键释放事件处理结构。将 setOnKeyPressed()改为。setOnKeyReleased(),并将真值转换为假值。编程快捷方式几乎和让 NetBeans 为我们编写代码一样酷!

转换 KeyEvent 处理结构:使用 Java 8 Lambda 表达式

接下来,让 NetBeans 将我们的 EventHandler 代码结构重新编码为 lambda 表达式,这将大大简化它,将它从三层嵌套的代码块减少到只有两层嵌套的代码块,并将 11 行代码减少到只有 8 行。这些 lambda 表达式非常适合编写紧凑的代码,并且它们是为多线程环境设计的,所以只要有可能,它们的使用就能带来更好的线程使用效果!得到的 Java 8 lambda 表达式代码结构应该如下图所示,如图 9-11 :

scene``.setOnKeyPressed``(KeyEvent``event

switch (``event

case UP:    up    = true; break; // UP, DOWN, LEFT, RIGHT constants from KeyCode class

case DOWN:  down  = true; break;

case LEFT:  left  = true; break;

case RIGHT: right = true; break;

}

});

A978-1-4842-0415-3_9_Fig11_HTML.jpg

图 9-11。

Convert the KeyEvent method to a lambda expression; notice that the event variable is used in the switch

接下来,让我们使用块复制和粘贴操作,并将。OnKeyPressed() KeyEvent 处理自身下面的结构,将其更改为。OnKeyReleased KeyEvent 处理结构,用 false 值代替 true。

创建按键事件处理结构

下一件事,我们需要做的是创建 OnKeyPressed 结构的极性相反,并创建 OnKeyReleased 结构。这将使用相同的代码结构,只是真值将变成假值,而。setOnKeyPressed()方法调用将改为. setOnKeyReleased()方法调用。最简单的方法是选择。setOnKeyPressed()结构,并将其复制粘贴到自身下面。如图 9-12 所示的 Java 代码应该是这样的 Java 结构:

scene``.setOnKeyReleased``(KeyEvent``event

switch (``event

case UP:    up    = false; break;

case DOWN:  down  = false; break;

case LEFT:  left  = false; break;

case RIGHT: right = false; break;

}

});

A978-1-4842-0415-3_9_Fig12_HTML.jpg

图 9-12。

Use a block copy and paste operation to create .setOnKeyReleased() code block, using .setOnKeyPressed()

使用 lambda 表达式通过“隐式”声明和使用类(如本章实例中的 EventHandler 类)所做的一件有趣的事情是,它减少了类代码顶部的 import 语句的数量。这是因为,如果在代码中没有专门使用某个类(写下了它的名称),则该类的 import 语句不必与其他 import 语句一起放在代码的顶部。

另外,请注意 NetBeans 左边的代码折叠加号和减号图标也不见了!这是因为 lambda 表达式是一个基本的 Java 代码语句,而不是一个构造,比如在转换为 lambda 表达式之前的方法或内部类。如果你看一下图 9-12 ,你的事件处理代码看起来非常干净,结构良好,然而,仅仅十几行代码,它实际上为你的游戏做了很多。

接下来,让我们来看看您的导入语句代码块(尤其是当您的导入代码块已折叠时),因为您已经让 NetBeans 8 为您创建了 lambda 表达式。看看有没有不需要的导入!

优化导入语句:删除 EventHandler 类导入语句

单击 NetBeans 左上方的+号图标,展开 import 语句部分,查看是否有一个未使用的 import Java FX . event . eventhandler 语句,该语句下面带有黄色波浪下划线警告突出显示。我有这个,如图 9-13 所示,当我将鼠标悬停在它上面时,我会得到“未使用的导入”警告消息。我使用 Alt-Enter work process 调出 solutions options helper 对话框,果然,有一个“Remove Import Statement”选项。因此,NetBeans 将在为您编写代码的同时为您解开代码!相当惊人的功能!

A978-1-4842-0415-3_9_Fig13_HTML.jpg

图 9-13。

Mouse-over the import EventHandler warning highlight, and display the pop-up “Unused Import” warning

接下来,让我们添加传统的 ASDW 游戏按键事件处理,让我们的用户可以选择使用这些按键或使用双手来玩游戏!这将向您展示如何在现有的事件处理代码中添加可选键映射支持,只需在高效的 switch-case 语句中使用几行代码。

添加备用按键事件映射:使用 A-S-D-W

现在我们已经有了这些 KeyEvent 处理结构,让我们来看看添加一个备用键映射到游戏中常用的 ASDW 键有多容易。这是通过为键盘上的 A、S、D 和 W 字符添加一些 case 语句,并将它们设置为我们已经设置好的上、下、左和右布尔等价物来实现的。举例来说,这将允许用户用左手使用 A 和 D 字符,用右手使用上下箭头来更容易地玩游戏。

稍后,如果您想在游戏中添加更多功能,使用您的游戏控制器及其对 KeyCode 类的 GAME_A (Jump)、GAME_B (Fly)、GAME_C (climb)和 GAME_D (crawl)常量的支持,您所要做的就是将这些新功能添加到您的游戏中,在屏幕顶部的上、下、左、右位置添加另外四个布尔变量(Jump、Fly、climb 和 crawl ),并添加另外四个 case 语句。

这四个 W(上)、S(下)、A(左)和 D(右)case 语句,一旦添加到 switch 语句中,将使您的 KeyEvent 对象及其事件处理 Java 代码结构只有十几行 Java 代码。你的新。修改后,setOnKeyPressed()事件处理结构将类似于以下代码块:

scene``.setOnKeyPressed``(KeyEvent``event

switch (``event

case UP:    up    = true; break;

case DOWN:  down  = true; break;

case LEFT:  left  = true; break;

case RIGHT: right = true; break;

case W:     up    = true; break;

case S:     down  = true; break;

case A:     left  = true; break;

case D:     right = true; break;

}

});

如你所见,现在用户可以使用任意一组按键,或者同时使用两组按键来控制游戏。既然你已经做了。setOnKeyPressed()事件处理结构对游戏玩家来说更灵活(也更强大),让我们对。setOnKeyReleased()事件处理结构,当用户释放键盘、遥控器或设备键盘和小键盘上的 A 或 left、W 或 up、S 或 down、D 或 right 键时,该结构将改为为 UP、DOWN、LEFT 和 RIGHT 布尔标志变量设置 false 值。

你的。在 switch 语句体的末尾添加这些 case 语句后,setOnKeyReleased()事件处理 Java 代码应该如下所示:

scene``.setOnKeyReleased``(KeyEvent``event

switch (``event

case UP:    up    = false; break;

case DOWN:  down  = false; break;

case LEFT:  left  = false; break;

case RIGHT: right = false; break;

case W:     up    = false; break;

case S:     down  = false; break;

case A:     left  = false; break;

case D:     right = false; break;

}

});

现在,您已经添加了另一组玩家移动控制键,供您的玩家用来控制游戏,您的代码没有错误,并且具有简单有效的结构,如图 9-14 所示。我们在名为 Scene 的场景对象的最顶端处理一次事件,在该事件处理“计算”中不涉及任何场景图形节点对象,并且仅使用几个字节的内存来保存八个布尔值(开/关)。

这与我们优化内存和 CPU 周期的目标是一致的,因此它们可用于我们游戏中更高级的部分,如游戏逻辑、碰撞检测或物理计算。

我们还添加了常数,允许我们以后缩放这个 640 乘 400 的游戏原型,以适应不同分辨率的显示屏,如伪高清(1280 乘 720)、真高清(1920 乘 1080)和 UHD (4096 乘 2160)。这些也可以用在游戏逻辑中,计算屏幕区域的大小,确定移动边界。

A978-1-4842-0415-3_9_Fig14_HTML.jpg

图 9-14。

Add the case statements for ASDW keys to give users two key options, or to allow two-handed game play

到目前为止,我们已经添加了我们的演员和支持演员引擎,以及我们的基本事件处理过程,以便我们可以开始确定这个 InvinciBagel 游戏英雄在下一章中如何在屏幕上移动。我们有。句柄()以及。更新()和。collide()方法来保存代码,这将激活一个角色,并最终激活敌人,无论是单人还是未来的多人版本。

接下来,让我们重温一下这个游戏设计的概览图,看看 InvinciBagel 包、InvinciBagel 类以及 GamePlayLoop 和演员和英雄类,它们为我们的游戏玩法处理和演员(以及投射物、宝藏、敌人和障碍或“道具”)创建提供了基础。

更新我们的游戏设计:增加事件处理

让我们更新我在第七章(图 7-19)和第八章(图 8-17)中介绍的图表,以包括 EventHandler 类的 ActionEvent 和 KeyEvent 处理。正如你在图 9-15 中看到的,我在图中添加了 EventHandler 事件处理类,以及处理我们的 UI 设计控件的 ActionEvent 对象和我们将要用来在屏幕上移动 InvinciBagel actor 的 KeyEvent。自从。setOnKeyPressed()和。setOnKeyReleased()方法是从名为 scene 的 scene 场景对象中调用的,ActionEvent 也包含在 Scene 对象下,我将它们放在了图中的 Scene 对象中。

由 KeyEvent switch-case 语句设置的布尔标志将在。update()方法,并将移动 InvinciBagel。那个。GamePlayLoop 对象中的 handle()方法将调用。update()方法,所以这里也有一个连接。我们仍然在稳步推进我们的游戏引擎框架,增加了事件处理!

A978-1-4842-0415-3_9_Fig15_HTML.jpg

图 9-15。

The current InvinciBagel package class (object) hierarchy, now that we have added Actor and Hero classes

摘要

在第九章中,我们在游戏场景对象的创建中添加了常量,这样我们就可以在未来的任何时候改变支持的显示分辨率,并添加颜色。使用其他重载 Scene()构造函数方法之一的白色背景色。我们这样做是为了完成我们的 UI 设计并实现 PLAY GAME UI 按钮控件,这样它将隐藏两个 ImageView 图像合成板,这两个图像合成板当前包含闪屏资产,以后可以包含游戏背景数字图像资产。

我们学习了 ImageView 类(和对象)的可见特征(或属性,或变量),以及如何使用。setVisible()方法调用使用 true 或 false 值切换给定 ImageView 图像板的可见性。因为我们在“玩游戏”按钮的 ActionEvent 处理结构中关闭了 ImageView 图像合成板的可见性,所以我们当然必须确保将其他三个按钮 UI 控件的 visible 属性设置回 true (on 或 visible ),以防您的游戏玩家稍后想要查看这些屏幕。

接下来,我们讲述了如何使用 NetBeans 将 Java 7 兼容的匿名内部类事件处理结构转换成 Java 8 lambda 表达式。我想在本书中介绍 Java 8 lambda 表达式,尽管它们是一个高级特性,因为它们是 Java 8 的主要新特性之一,这是 Java 8 编程的一个标题。

最后,我们开始向我们的 Java 8 游戏编程基础设施添加新功能,并了解了输入事件(input event)类和子类,以及事件处理器(event handler)类结构是如何设置的,以及它们如何跨越 java.lang、java.util、javafx.event 和 javafx.scene.input 包。我们看了一下 KeyCode 常量和 KeyEvent 类,然后使用。setOnKeyPressed()和。setOnKeyReleased()事件处理器结构,用于 Java 7 和 Java 8 的兼容性。

在下一章中,我们将看看如何使用我们在本章中创建的 KeyEvent 事件处理结构在屏幕上移动游戏精灵,以及如何确定屏幕的边界(边缘)、角色方向、移动速度以及相关的动画和移动注意事项。

十、指导演员阵容:创建选角指导引擎和创建Bagel演员类

既然我们已经在第八章第一节创建了公共的抽象演员和英雄类(演员引擎),在第九章第三节创建了一些基本的按键事件处理,现在是时候在第十章第五节把更多的游戏基础设施放在这里了。我们将创建另一个 Java 类来管理我们的演员阵容,名为 CastingDirector.java(演员阵容引擎)。我们将这样做,以便我们可以跟踪游戏屏幕上的演员对象,这些对象是使用我们的演员和英雄抽象类创建的。在游戏中的任何给定时间(或关卡),知道屏幕上(或舞台上,或布景上,如果你喜欢我们正在使用的电影制作行话)当前有什么游戏组件(演员)是很重要的。

在这一章中,我们还需要学习更多关于 List 和 ArrayList 类以及 Set 和 HashSet 类的知识,并使用它们。这些 Java“集合”类将管理 List 对象和 Set 对象,我们将使用它们来跟踪屏幕上游戏中涉及的当前 Actor 对象。我们将在本章的开始部分详细介绍这些 java.util 包类,所以准备好学习 java 数组对象,以及其他一些相当高级的 Java 编程概念,这对初学者来说可能是一个挑战。但是,在你的 Java 8 游戏标题开发工作过程中,它们会非常有用,所以我决定将它们包含在本书中。

我们还想为游戏创建我们的第一个演员,InvinciBagel 角色演员,因为我不想离我们在第八章写的代码太远而没有实现它(用它来创建一个演员)。我们将通过创建一个 Bagel.java 类来实现这一点,该类将使用 Java extends 关键字来继承一个 Hero.java 抽象类。这使得 Bagel.java 成为一个子类,而 Hero.java 成为一个超类。

一旦我们有了一个 Bagel.java 类,我们将使用 Java new 关键字和 Bagel()构造方法为 Bagel 类创建一个名为 iBagel 的 Bagel 对象。我们将用一些临时 SVG 数据加载 iBagel 对象,至少直到我们在第十六章讨论碰撞检测的中讨论如何创建复杂的 SVG 碰撞形状数据。我们还将传递一个 X 和 Y 坐标,将 iBagel 演员放在屏幕中间,最后是 9 字符运动精灵“cels”,这是我们在第八章中首次看到的。

我们将这样做,这样你就可以开始利用我们在公共抽象 Actor 和 Hero 类基础设施中安装的主要数据字段(变量、属性或特性),这些是我们在第八章中精心设计的。

我们还将在第十章中再次使用我们的 InvinciBagel.java 主应用类,并将在一个新的。createGameActor()方法,这样我们就可以将我们的主角绑定到 GamePlayLoop 类的。handle()方法。这将访问(调用)Bagel 类。update()方法,这样我们就可以开始控制游戏的主要英雄 InvinciBagel 本人的动作。

游戏设计:添加我们的 CastingDirector.java 类

我想做的第一件事是更新我们的 invincibagel 包和类结构图,向您展示我们将在本章中使用 Java ArrayList 和 HashSet 类(对象)开发的新 actor (sprite)管理类。正如你在图 10-1 中看到的,我将这个类命名为 CastingDirector。java,因为它就像任何娱乐项目的选角导演一样,为项目添加演员,并在场景结束时移除他们。这个类还将包含一个 Java 集合(Java ListArray 是有序集合,Java HashSet 是无序集合),我们将在本书后面开始实现冲突检测时用到它。随着你的游戏关卡和场景变得越来越复杂,你会很高兴有一个 CastingDirector 类来保持你的游戏角色有组织,并根据游戏编程逻辑的需要从游戏中添加和删除角色。准确跟踪场景中有多少演员(固定精灵)和英雄(运动精灵)对象是很重要的,以便在碰撞检测代码(算法)中只涉及尽可能少的演员。这是一个优化游戏编程逻辑的功能,因此游戏可以在所有平台上运行。

A978-1-4842-0415-3_10_Fig1_HTML.jpg

图 10-1。

Create a CastingDirector.java actor casting and tracking engine to keep track of Actor and Hero objects

在我们编写 CastingDirector.java 类之前,让我们花点时间了解一下 Java 集合、泛型,以及我们将用来创建这些参与者管理工具的 List、ListArray、Set 和 HashSet 类。

列表和数组列表:使用 java.util 列表管理

首先,让我们来看看公共类 ArrayList ,因为它是一个类,然后我们将 List 看作是一个接口,而不是一个 Java 类。如果你想知道代表什么,它代表元素,如果你看到代表键的,如果你看到代表类型的,如果你看到代表值的被一个你在数组列表中使用的元素(对象)代替。在我们的例子中,ArrayList 是 CastingDirector.java 类 ArrayList(和 Set)将引用 Actor 对象(Actor 超类的子类)。类的层次结构如下:

java.lang.Object

> java.util.AbstractCollection<E>

> java.util.AbstractList<E>

> java.util. ArrayList<E>

这个类是 Java Collections 框架的一个成员,正如您可能已经猜到的那样,作为一个列表和一个数组,两者都包含数据集合,就像数据结构(或数据存储)一样,只是格式更简单。ArrayList 类可以“实现”或支持以下 Java 接口:Serializable、Cloneable、Iterable 、Collection 、List 和 RandomAccess。我们将使用 List 或者,在我们的例子中,List Java 接口,我们将在 List 的下一节学习 Java 接口时看到它。

实际上,ArrayList 类(和对象)创建了 List 接口的可调整大小的数组实现。因此,ArrayList 对象实现了所有可选的列表操作,并允许所有类型的列表元素,包括 null。除了实现 List Java 接口,这个类还提供了方法调用,包括一个. removeAll(),。addAll()和。clear(),我们将在我们的类中使用它来操作列表内容和 ArrayList 对象的大小,该对象在内部用于存储所使用的演员列表(对于 List )或图像列表(对于 List )。

每个数组列表对象实例都有一个容量。容量是用来存储 List 实现中的元素(对象)的数组的大小:在我们的例子中,是 List 。容量将始终至少与列表大小一样大。随着元素(Actor 对象)被添加到 ArrayList 中,它的容量将自动增长,这使它非常适合我们的 CastingDirector 类,因为我们可以使游戏的级别越来越复杂,也就是说,它可以在 List 的 ArrayList 中利用更多的 Actor 对象。

值得注意的是,List 实现不是同步的(能够同时在多个线程上运行)。如果您需要让多个线程同时访问一个 ArrayList 实例,并且这些线程中至少有一个修改了您的 List < Actor >结构,那么它必须在外部同步(手动,使用您的代码)。当一个敌人被杀死,或者一个投射物被射出,或者一个宝藏被发现(被收集)的时候,我们会特别的调用 CastingDirector 职业,并且不会让它在一个脉冲上被持续的调用。

ArrayList 对象的结构修改是添加或移除一个或多个元素的操作;仅仅在 ArrayList 中设置元素(Actor)的值不会被认为是结构修改。

Java 接口:定义实现类的规则

在我们看列表 Java 接口之前,让我们看一下 Java 接口一般都做些什么,因为我们没有足够的篇幅来涵盖第三章中所有的 Java 编程语言。因此,我将介绍一些更高级的 Java 主题,因为我们需要在本书中学习它们。一个很好的例子是第九章的中的 lambda 表达式和第十章中的 Java 接口。使用 Java 接口的原因是为了确保其他将要使用你的代码的程序员正确地实现它;也就是说,包括代码正常工作所必需的一切。

本质上,一个接口指定的只是另一个开发人员实现您的类所需的一组相关方法。这些是用“空方法”代码体指定的。例如,如果您想让其他开发人员使用 Hero 类,您可以指定一个 Hero 接口。这将使用以下 Java 代码来完成:

public``interface

public void``update

public boolean``collide

}

如您所见,这与我们使用。Actor 超类中的 update()方法,因为没有像通常在方法中那样指定{code body}。因此,在某种意义上,Java 接口也以一种抽象的方式被用来定义“实现”Java 接口的类中需要包含什么。正如您可能已经猜到的,您将在类声明中使用 Java implements 关键字来实现 Java 接口。

因此,如果您定义了一个 Hero 接口,并希望在您的某个类中实现它,在这种情况下,Java 编译器会监视代码并确保您实现了必要的方法结构,代码的类定义行和类体中的方法将如下所示:

public class SuperHero``implements

protected boolean flagVariable1, flagVariable2;

public void``update

// Java statements to process on each update

}

public boolean``collide

// Java statements to process for collision detection

}

}

对于我们之前看到的 java.util ArrayList 类,技术类定义如下:

public``class``ArrayList<E>``extends``AbstractList<E>``implements

ArrayList 类也实现了 RandomAccess、Cloneable 和 Serializable,但是我们现在不会使用它们,所以我只是向您展示了 ArrayList 类定义中与我们将在本章中学习的内容相关的部分,而不是完整的public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable类定义,如果您在线查看 ArrayList < E >类的 Java 类文档,就会看到这一点。

需要注意的是。addAll(),。removeAll()和。我们将与 ArrayList 类一起使用的 clear()方法调用被实现,因为 List Java 接口要求它们被实现,所以这是类之间的联系,也是为什么我们将使用以下代码指定 ArrayList <>对象的声明:

private List<``Actor

您可能想知道为什么我们不需要在声明和实例化语句的两边显式指定 Actor 对象类型。在 Java 7 之前,您需要在这个语句的两边,ArrayList <>()构造函数方法调用的内部指定您的 Actor 对象类型。因此,如果您正在编写需要与 Java 5 和 Java 6 兼容的游戏代码,您可以使用下面一行 Java 代码编写这条语句:

private List<``Actor``> CURRENT_CAST = new ArrayList<``Actor

既然我们已经了解了什么是 Java 接口,那么我们就来详细了解一下 List public interface。

List 公共接口:Java 对象的列表集合

List 公共接口也是 Java 集合框架的成员。Java 公共接口列表扩展了集合公共接口,后者扩展了 Iterable 公共接口。因此,超级接口到子接口的层次结构看起来就像下面的列表 Java 接口层次结构:

Interface Iterable<T>

> Interface Collection<E>

> Interface List<E>

列表是一个有序的集合< E >,也可以被认为是一个对象序列。在我们的例子中,列表< Actor >将是 Actor 对象的有序序列。List 界面的用户可以精确控制每个元素(在我们的例子中是 Actor 对象)在列表中的插入位置。用户可以使用整数索引(即列表中的位置)访问元素,在列表名称后使用括号。您也可以在列表中搜索元素。

例如,在 Actor.java 类中,我们在类的顶部声明了下面一行代码:

protected List<Image> imageStates = new ArrayList<>();

为了访问第一个 Actor 类 imageState 图像对象列表 sprite,我们将使用以下 Java 语句:

imageStates.get(0);

不像 Set 对象,我们将在本章的下一节学习,你的 List 接口符合 ArrayList 对象通常允许重复元素。游戏应用的一个例子可能包括投射物(比如说子弹),如果你对游戏进行了编码,允许敌人向百吉饼射击。当然,出于优化的目的,我们会尽量减少游戏中的重复元素,但是如果我们需要在游戏场景中有重复的元素,那么在 List 实现中有这个功能是很好的。

List 接口提供了四种方法,通过在方法调用中使用整数索引来对列表元素(对象)进行位置(索引)访问。这些方法包括.get(int index)方法,从列表中获取一个对象;.remove(int index)方法,从列表中删除一个对象;.set(int index, E element)方法,它将替换列表中的一个对象;和一个.listIterator()方法,它从列表中返回一个 ListIterator 对象。ListIterator 对象允许您一次对多个列表元素执行操作(添加、移除、设置/替换),以防您想知道 ListIterator 的用途。

List 接口提供了这个特殊的迭代器实现,称为 ListIterator < E >,它是迭代器< E >超级接口的子接口,允许多个元素的插入和替换。除了迭代器< E >接口提供的正常操作之外,ListIterator < E >还提供双向列表访问。

那个。我们前面讨论过的 listIterator()方法是用来获取从列表中指定位置开始的 ListIterator 对象的。因此,使用 imageStates List ArrayList 对象,imageStates.listIterator()方法调用将产生 ListIteration 对象,该对象包含对整个 imageStates ArrayList 对象的迭代。这将为我们提供 imageStates(0),起始列表元素,以及 ArrayList 构造中该列表的剩余部分,它们将被引用为 imageStates(1),imageStates(2),最后一个将被引用为 imageStates(8)。Java 列表类使用 ()引用列表对象,而 Java 数组类使用[]方括号。Java 列表是“动态的”(幸运的是,我们已经讨论了静态和动态);也就是说,它是开放的,而 Java 数组是“静态的”,即固定的,这意味着它的长度需要在创建时定义。

实现 List 的对象使用零开始它们的编号模式,就像所有 Java 数组对象一样。这并不奇怪,因为大多数 Java 编程构造也将从零开始计数,而不是从一开始计数。从优化的角度来看,值得注意的是,迭代 List < E >中的元素通常比通过编号对其进行索引更可取(更优化),例如使用 for 循环,这可能就是为什么对这个 ListIterator 接口的支持是 List < E >接口规范的一部分,因此也是 ArrayList 类的一部分,该类别无选择,只能实现 List < E >接口规范,因为它使用 Java implements 关键字。

List 接口还提供了三种方法,允许使用指定的对象访问列表。这些包括.indexOf(Object object).contains(Object object).remove(Object object)。同样,从性能的角度来看,应该谨慎使用这些方法,因为必须将一个“输入”对象与列表中的每个对象进行比较,这将比简单地使用列表中对象的索引占用更多的内存和 CPU 周期。毕竟,这就是指数的用途!在许多实现中,这将执行高成本的“线性”对象搜索,从 ListElement[0]开始,遍历整个列表比较对象。如果你的对象在这个列表的“头”上,这一点也不昂贵。另一方面,如果您的对象位于包含大量对象元素的列表的末尾,那么使用这些“面向对象”的方法,您很可能会发现性能下降。好吧,所有的方法都是面向对象的,所以,让我们巧妙地把这些方法称为“对象参数化”吧!

List 接口还提供了两种方法来有效地读取或删除列表中任意点的多个列表元素。.removeRange(int fromIndex, int toIndex)删除列表元素的范围,.subList(int fromIndex, int toIndex)返回指定的 fromIndex 和 toIndex 之间的列表部分的视图。fromIndex 包含在返回的子列表中,但是 toIndex 不包含在内。

最后,List 接口提供了三个方法,使用一个方法操作整个列表。因为我们使用列表来管理当前场景中的所有 Actor 对象,所以我们将主要使用这些方法,这些方法在本章的 ArrayList 部分已经提到过,包括。addAll(),。removeAll()和。清除()。我们还将使用.add(E element)方法向 CURRENT_CAST 列表添加一个 Actor 对象。

最后,虽然技术上允许 List 将自身包含为一个元素,但这并不被视为“好”的编程实践,所以我不建议这样做。如果您打算这样做,您应该非常小心,因为 equals 和 hashCode 方法在这样的列表中将不再是“定义良好”的。

Set 和 HashSet:使用 java.util 无序集

Set 公共接口也是 Java 集合框架的成员。Java 公共接口集扩展了集合公共接口,后者扩展了 Iterable 公共接口。因此,集合的超级接口到子接口的层次结构与列表的相同,并且看起来像下面的接口层次结构:

Interface Iterable<T>

> Interface Collection<E>

> Interface Set<E>

集合是一个无序的集合< E >,也可以被认为是一个没有特定顺序的对象的随机集合。集合< E >可能不包含重复的元素,并且如果重复的元素被添加到集合< E >中,或者如果任何“可变的”(可以变成其他东西的元素)元素被改变成与集合< E >中已经存在的另一个元素重复的元素,那么集合>将抛出一个错误,称为“异常”。这个无重复规则也意味着一个集合< E >最多只能包含一个空元素。正如所有精通数学的人可能已经猜到的那样,这个 Set < E >界面是仿照你在学校里学过的数学集合设计的。

Set 接口在所有构造函数方法的“契约”(对所有非合法类型的要求)以及。add(),。等于()和。hashCode()相关的方法。

根据规则,对这些构造函数方法的附加规定是,所有构造函数必须创建一个包含零个重复元素的集合

如前所述,如果可变(可更改)对象被用作 Set 集合中的元素,您必须小心谨慎。如果一个对象的值以一种影响。equals()方法比较,而可变对象是集合中的一个元素。这种禁止的特殊情况是不允许集合包含自身作为一个元素,而列表可以。

java.util HashSet 类:使用无序的对象集

接下来,让我们介绍一下公共类 HashSet ,它也是 Java 集合框架的成员。这个类为 Set 接口规范提供一个 HashSet 对象容器,类似于 ArrayList 类为 List 接口创建一个 ArrayList 对象容器。HashSet 类可以“实现”或支持以下 Java 接口:Serializable、Cloneable、Iterable 、Collection 和 Set 。我们将在我们的 CastingDirector.java 类中使用 Set ,或者在我们的例子中,Set 接口。设置类的层次如下:

java.lang.Object

> java.util.AbstractCollection<E>

> java.util.AbstractSet<E>

> java.util. HashSet<E>

HashSet 类以哈希表的形式实现 Set 接口,这实际上是一个 HashMap 的实例。HashSet 不保证对象集合< E >的迭代顺序;特别是,它不能保证顺序会随时间保持不变。这个类允许使用一个空元素。

需要注意的是,Set 的实现是不同步的。如果多个线程同时访问您的 HashSet 对象,并且其中至少有一个线程修改了您的 Set < E >,那么它应该是外部同步的。这通常是通过同步一些自然封装集合< E >的对象来实现的,比如 HashSet。我们以一种非常基本的方式使用 HashSet:保存由于某种原因从游戏中移除的对象,比如发现了宝藏;敌人被消灭;或者类似的游戏设计场景。

HashSet 类(和对象)提供的一个优点是基本数据集操作(如。add(),。remove(),。包含()和。size()方法。对一个 HashSet 对象的迭代将需要一个与 Set 对象实例大小之和成比例的时间段,Set 对象实例大小由 Set中当前元素的数量和支持 HashMap 对象实例的“容量”决定。

创造你的铸造引擎:CastingDirector.java

现在您已经对 Java 接口、Java 集合框架及其由 ArrayList 和 HashSet 类实现的 List 和 Set 接口有了一些了解,我们可以继续创建我们的基本 CastingDirector 类。该类将保存一个列表对象,其中列出了当前场景中当前“正在播放”的演员对象,以及另一个列表对象,其中列出了应该对哪些演员对象进行碰撞检查。还将有一个 Set 对象来保存需要删除的 Actor 对象。右键单击 NetBeans“项目”窗格中的 invincibagel 包文件夹,选择“新建➤ Java 类”菜单序列,弹出“新建 Java 类”对话框,如图 10-2 所示。将新类命名为 CastingDirector,并将其他字段留在对话框中,这些字段由 NetBeans 自动设置。

A978-1-4842-0415-3_10_Fig2_HTML.jpg

图 10-2。

在 invincibagel 包中创建新的 Java 类;将它命名为 InvinciBagel 项目的 casting director

我们将首先创建列表数组列表对象,一个保存当前场景的演员,然后第二个列表保存要检查碰撞检测的对象。之后我们将创建 Set HashSet 对象,它将提供一个无序的 Set 对象,它将收集那些需要从场景中移除的 Actor 对象。让我们开始创建公共 CastingDirector 类的主体。

创建 ArrayList 对象:CURRENT_CAST 数据存储列表

我们需要添加到 CastingDirector 类的第一件事是一个私有 List ArrayList 对象,我将把它命名为 CURRENT_CAST,因为它包含当前在舞台上的 Actor 对象,也就是当前的演员。尽管就在其声明中使用 static 和 final 关键字而言,它在技术上不是一个常量,但它充当了各种数据库的角色(没有双关的意思),所以我使用 ALL_CAPS,以便它在代码中作为一个数据结构脱颖而出。我还将添加一个基本的。get()方法访问 ArrayList 结构,使用 Java return 关键字将对象返回给调用实体。用于声明和实例化 CURRENT_CAST ArrayList 对象以及。getCurrentCast()方法的结构应该看起来像下面的 Java 代码:

package invincibagel;

public class``CastingDirector

private List<Actor>``CURRENT_CAST

public List<Actor> getCurrentCast() {

return``CURRENT_CAST

}

}

正如您在图 10-3 中看到的,在声明和实例化 CURRENT_CAST 对象的代码行中,在您的列表接口引用和 ArrayList 引用下有红色波浪错误突出显示,因此您需要使用 Alt-Enter 工作进程,并让 NetBeans 8 在您的类顶部编写import java.util.List;语句。的。getCurrentCast()将是最容易编写的方法,因为它只是将整个 CURRENT _ CAST ArrayList对象返回给调用该方法的任何 Java 实体。接下来,我们将看看如何编写更复杂的 ArrayList 数据存储访问方法,这些方法将处理从这个 CURRENT _ CAST ArrayList对象添加、移除和重置(清除)Actor 对象。

A978-1-4842-0415-3_10_Fig3_HTML.jpg

图 10-3。

Inside the CastingDirector class, add a CURRENT_CAST List object, and a .getCurrentCast() method

我们要编码的第一个方法是。addCurrentCast()方法,它将把以逗号分隔的 Actor 对象列表传递给 List(和实现 List 的 ArrayList 类)接口。addAll()方法调用。正如您已经了解到的,在方法参数列表的末尾传递一个逗号分隔的列表,除非它是唯一的参数,就像在本例中一样。

来展示。addCurrentCast()方法,我们将传递多个 Actor 对象到方法体中,我们使用了 Actor...注释,我将把(不止一个)Actor 对象命名为变量 actors。的主体内部。addCurrentCast()方法,我们将调用。使用点标记法从 CURRENT_CAST 对象中删除 addAll()方法。

在里面。addAll()方法我们将嵌套另一个 Java 语句,该语句将使用。asList()方法调用了数组类引用并传递了 actors Actor...逗号分隔的列表。这都是使用下面的 Java 方法构造来完成的:

public void addCurrentCast(Actor... actors) {

CURRENT_CAST.``addAll``( Arrays.``asList

}

正如您在图 10-4 中所看到的,您将在 Arrays 类下看到一个红色波浪状的错误高亮显示,因此使用 Alt-Enter 工作进程并让 NetBeans 为您编写您的import java.util.Arrays;语句。现在,我们准备编写与 CURRENT_CAST 数据存储相关的另外两个方法,这两个方法将从 ListArrayList数据存储对象中删除 Actor 对象,还有一个方法将完全清除它(将其重置为未使用)。

A978-1-4842-0415-3_10_Fig4_HTML.jpg

图 10-4。

Add .addCurrentCast(), removeCurrentCast(), and resetCurrentCast methods to the CastingDirector class

我们要编写的第二个方法是。removeCurrentCast()方法,该方法还将把以逗号分隔的 Actor 对象列表传递给 List(和实现 List 的 ArrayList 类)接口。removeAll()方法调用。

来展示这个。removeCurrentCast()方法,我们将传递多个 Actor 对象到方法体中,我们再次使用 Actor...注释,我将再次把这个变量命名为 actors。的主体内部。removeCurrentCast()方法,我们将再次调用。removeAll()方法,并在。removeAll()方法我们将嵌套另一个 Java 语句,该语句将使用。asList()方法调用了数组类引用,再次传递了 Actor...方法中以逗号分隔的命名参与者列表。这是使用图 10-4 中的 Java 方法完成的:

public void``removeCurrentCast

CURRENT_CAST.``removeAll``( Arrays.``asList

}

现在您只需要编写一个简单的。resetCurrentCast()方法,该方法调用。clear()方法调用:

public void``resetCurrentCast

CURRENT_CAST.``clear

}

接下来,让我们看看目前为止 CastingDirector.java 代码中的另一个问题,然后我们可以继续。

NetBeans 优化建议:最终生成列表数据存储对象

正如您在图 10-5 中看到的,您的代码没有错误,但也不是没有警告,所以让我们看看 NetBeans 希望我们对与我们的 CURRENT_CAST List < Array >数据存储对象相关的代码做些什么。我用鼠标悬停工作流程,弹出淡黄色提示消息,通知我 CURRENT_CAST 数据字段(变量,是一个对象)可以标记为 final,使用 Java final 关键字。如果我们要这样做,那么 CURRENT_CAST 对象的新声明和实例化语句的基本 Java 8 语法将编写如下:

private``final

A978-1-4842-0415-3_10_Fig5_HTML.jpg

图 10-5。

Mouse-over yellow warning highlight under CURRENT_CAST, and use the Alt-Enter dialog to fix problem

对于这个 Java 修饰符关键字 final 在对象方面的使用,经常会有误解。的确,对于大多数 Java 变量(数字、布尔和字符串),当变量成为最终变量时,变量值本身是不能改变的。许多人认为,final 修饰符在与 Java 对象(变量声明)一起使用时,也会使对象本身成为“final”,因此是“不可变的”,或者在内存中是不可变的。

通常在 Java 中,final 关键字在与对象变量一起使用时指的是内存引用,而 immutable 关键字适用于这些对象本身,意味着它们不能被更改。因此,一个被声明为 final 的对象(引用)仍然可以包含一个可变的对象(可以被改变,正如我们在这里所希望的)。

事实上,final modifier 关键字对内存中的 Java 对象(比如我们的 CURRENT_CAST ArrayList 对象)所做的就是锁定它在内存中的位置,也就是说,完成它。因此,NetBeans 在这里建议的是一种优化,它将允许您的 CURRENT_CAST 数据存储对象始终保留在内存中的位置(在创建后)。

这并不意味着你的 List ArrayList 对象本身不能改变。你的游戏的 Java 代码可以扩展、收缩和清除(重置)一个列表数组列表对象,这个对象已经在任何时候被声明为最终的,基于你的游戏对。addCurrentCast(),。removeCurrentCast()和。resetCurrentCast()方法。

这里的优化理论是,JVM 越能“预先锁定”内存位置(在程序启动时,加载到内存中),它就能越好地优化内存,以及访问内存所需的 CPU 周期。如果你想一想,如果 CPU 不必在内存中“寻找”一个对象,那么它将能够更快地访问它。最终对象也可以在多线程环境中得到更好的使用。

但是,如果您不想使对象引用成为最终引用,可以选择在 NetBeans 中关闭此功能。这可以使用图 10-6 左侧的工具➤选项菜单序列来完成,以便进入图 10-6 右侧所示的选项对话框。正如您在该选项对话框的顶部所看到的,NetBeans 将其数百个首选项(也称为选项)组织到十个特定区域,甚至还有一个搜索过滤器,也显示在对话框的右上角,以防您不知道在哪里查找给定的选项。如果这些部分有太多的选项显示在对话屏幕上,将会有选项卡(提示选项卡在图 10-6 中显示为选中),您可以使用这些选项卡导航到您想要访问的区域。我们将进入提示部分,从下拉列表中选择 Java 语言,最后打开线程部分。

A978-1-4842-0415-3_10_Fig6_HTML.jpg

图 10-6。

Setting Editor Hints Preferences using the Tools ➤ Options menu and the Editor ➤ Hints ➤ Java ➤ Threading

现在我们已经讨论了最后一个对象变量问题,并向您展示了处理它的两种方法,让我们继续,并创建第二个名为 COLLIDE_CHECKLIST 的 ArrayList 对象来存储复杂的冲突数据。

另一个 ArrayList 对象:COLLIDE_CHECKLIST 数据存储列表

现在让我们创建第二个 List ArrayList 数据存储对象,并将其命名为 COLLIDE_CHECKLIST,因为它最终将在。collide()方法;如果你在游戏开发的后期实现复杂的多物体碰撞列表,就会发生这种情况。在本书中,我们不会达到需要实现这一点的高级水平,但我想向您展示如何将一个完整的 CastingDirector 类放在一起,以便当您在游戏开发中需要它时,您可以在游戏中添加更多高级功能。这个对象将保存 CURRENT_CAST ArrayList 的最新副本,并将有两个方法。那个。方法将返回 COLLIDE_CHECKLIST 对象,而。resetCollideCheckList()将通过使用。clear()方法调用,现在,我们将使用。addAll()方法加载 COLLIDE_CHECKLIST ArrayList 对象与 CURRENT_CAST ArrayList 对象的当前版本。稍后,我们可以使用这个列表来保存一个自定义的碰撞清单,该清单只将可能相互碰撞的对象分组到一个列表中。如图 10-7 所示,声明和实例化对象所需的 Java 代码应该如下:

private``final``List<Actor>``COLLIDE_CHECKLIST

A978-1-4842-0415-3_10_Fig7_HTML.jpg

图 10-7。

Add a COLLIDE_CHECKLIST List object, .getCollideCheckList(), and resetCollideCheckList() methods

a。getCollideCheckList()方法使用一个 return 关键字,来访问 COLLIDE_CHECKLIST,如下所示:

public List``getCollideCheckList

return``COLLIDE_CHECKLIST

}

a。resetCollideCheckList()方法使用. clear()方法清除 COLLIDE_CHECKLIST,然后使用。addAll()方法将当前 _CAST 对象的内容添加(插入)到 COLLIDE_CHECKLIST 对象中。

public void``resetCollideCheckList

COLLIDE_CHECKLIST .clear();

COLLIDE_CHECKLIST .addAll(CURRENT_CAST);

}

现在我们已经设置了 ArrayList 对象来保存角色成员和高级冲突列表数据集,让我们创建一个 HashSet 对象。这个 Set 对象将用于收集由于某种原因需要从游戏中移除的演员(场景和舞台)。

创建 HashSet 对象:REMOVED_ACTORS 数据存储集

现在让我们创建我们的第三个 Set HashSet 数据存储对象,让我们称它为 REMOVED_ACTORS,因为它将用于保存已经从当前阶段删除的 Actor 对象的集合。这个 Set < Actor >对象将保存所有的 Actor 对象,这些 Actor 对象由于某种原因需要从 CURRENT_CAST 列表中删除。REMOVED_ACTORS 数据存储(数据集)将有三个关联的方法。

那个。getRemovedActors()方法将返回 REMOVED_ACTORS 对象。addToRemovedActors()将是“核心”方法,它将随着游戏过程中发生的事情(寻找宝藏、杀死敌人等)向 REMOVED_ACTORS Set 对象添加 Actor 对象。)从舞台和场景中消除演员对象,而。将使用。removeAll()方法从 CURRENT_CAST ArrayList 对象中移除 Actors,然后使用。对 REMOVED_ACTORS HashSet 对象调用 clear()方法。如图 10-8 所示,需要使用 Java new 关键字声明和实例化 HashSet 对象的代码如下:

private``final``Set<Actor>``REMOVED_ACTORS``=``new

A978-1-4842-0415-3_10_Fig8_HTML.jpg

图 10-8。

Add a private final Set named REMOVED_ACTORS and use the Java new keyword to create a HashSet<>

这三种方法中最容易编写的是。getRemovedActors()方法,该方法简单地使用 return 关键字将整个 HashSet Set 对象传递给一个调用实体。这为 REMOVED_ACTORS 提供了对其他方法的访问,比如我们将在本节稍后编写的方法。Java 代码应该是这样的:

public Set``getRemovedActors

return``REMOVED_ACTORS

}

我们需要编码的下一个方法是最复杂的,也是最常用的,因为当你的角色发生变化时,你会使用这个方法:例如,一个被杀死的敌人,如无敌面包圈,一个用过的抛射物,如子弹,消耗的食物,如一团奶油奶酪,或发现的宝藏,如一个礼品盒。

那个。addToRemovedActors()方法使用 if-else 语句来确定是在参数列表中传递了多个 Actor 对象(构造的第一个或 if 部分),还是只需要移除一个 Actor 对象(构造的第二个或 else 部分)。if-else 语句的第一部分使用。length()方法来确定从一个 Actor 开始是否有多个 Actor 对象被传递到使用if(actors.length > 1)的方法调用参数列表中...参数允许一个以上的 Actor 对象提交给方法,如图 10-9 所示。

如果在 if{...}构造,请使用。addAll()方法将参数列表的内容添加到您的 REMOVED_ACTORS Set 对象中。这是通过使用。addAll()方法调用,它构造名为 actors 的 Actor[]数组,该数组与使用。用一组< E >对象类型调用 addAll()方法。第二个 else{...}部分通过使用 actors[0]注释(第一个 Actor 参数)和一个。使用以下代码调用 add()方法:

public void``addToRemovedActors

if (``actors.length > 1``) { REMOVED_ACTORS.addAll(``Arrays.asList((Actor[]) actors)

else {                   REMOVED_ACTORS.add(``actors[0]

请注意,由于我们已经转换了演员...参数(它们的目的是一个列表,但还不是一个)到一个数组中(因为编译器可以计算固定数量的项目),所以我们可以使用 actors[0]符号。

A978-1-4842-0415-3_10_Fig9_HTML.jpg

图 10-9。

Add .getRemovedActors(), .addToRemovedActors(), and .resetRemovedActors() method structures

既然我们有了向 REMOVED_ACTORS 集合 HashSet 添加一个或多个 Actor 对象的方法,那么让我们创建一个. resetRemovedActors()来清除 REMOVED_ACTORS 数据集。在我们清除 Set 对象之前,我们需要确保包含在其中的所有 Actor 对象都从 CURRENT_CAST Actor List 对象中删除,因为这就是它存在的目的,所以这个方法的第一部分将调用。removeAll()方法从 CURRENT_CAST ArrayList 对象中移除,并在该方法内部传递 REMOVED_ACTORS Set 对象。之后,我们可以使用。clear()方法调用 REMOVED_ACTORS 对象,将其重置为空,以便可以再次使用它来收集需要释放的 Actor 对象。如图 10-9 所示的 Java 代码应该是这样的:

public void``resetRemovedActors

CURRENT_CAST.``removeAll

REMOVED_ACTORS.``clear

}

接下来,我们将看看如何让 NetBeans 编写 CastingDirector()构造函数方法!

CastingDirector()构造函数:让 NetBeans 编写代码

有一种方法可以让 NetBeans 为您编写一个构造器方法,由于它有点“隐藏”,我将向您展示如何找到它!我将插入栏光标留在图 10-10 中,向您展示我点击了最后一个关键字和出现的黄色灯泡“tip”图标,以及当我将鼠标悬停在 tip 灯泡上时得到的淡黄色弹出工具提示消息。我得到的消息是“将初始化器移动到构造函数”,所以我按了建议的 Alt-Enter 组合键。果然有一个选项是 NetBeans 给我写这个构造器方法代码。

A978-1-4842-0415-3_10_Fig10_HTML.jpg

图 10-10。

Mouse-over the yellow light bulb icon in the line number area of the pane and reveal the constructor tip

对于您单击的第一个 final 关键字,按住 Alt-Enter 键,并让 NetBeans 为其编写 CastingDirector()构造函数方法,它将编写公共 CastingDirector(){...}结构,并添加第一个实例化语句。正如您在图 10-11 中所看到的,一旦您点击了类顶部的三个 final 关键字中的每一个,并使用相同的工作过程,您就可以让 NetBeans 为您编写整个构造器方法。NetBeans 生成的 Java 代码使用 Java this 关键字(以便 CastingDirector 对象可以引用自身)作为三个数据存储对象的开头,并使用 Java new 关键字创建 ArrayList < E >和 HashSet < E >的新实例,如下所示:

public CastingDirector() {

this``.CURRENT_CAST =``new

this``.COLLIDE_CHECKLIST =``new

this``.REMOVED_ACTORS =``new

}

A978-1-4842-0415-3_10_Fig11_HTML.jpg

图 10-11。

Use Alt-Enter, and have NetBeans write your CastingDirector() constructor method Java code for you

在我们结束这一章之前,我们可能应该为我们游戏的主角,不可战胜的恶魔,创建至少一个演员类(对象)。让我们使用 Hero 抽象类来创建一个 Bagel 类,以便稍后我们可以创建一个 iBagel 对象。我们将在下一章使用这些代码,在那里我们将学习如何在舞台上移动这个无敌的角色,同时也优化我们的 InvinciBagel 类的结构。

创造我们的主角:百吉饼英雄子类

让我们创建一个 Bagel.java 类,方法是在 ide 左侧的 NetBeans 项目窗格中右键单击 invincibagel 包文件夹,然后选择“新建➤ Java 类”菜单序列以打开“新建 Java 类”对话框,如图 10-12 所示。将该类命名为 Bagel,并接受其他默认项目、位置、包和创建的文件选项字段,方法是单击“完成”按钮,这将创建新的 Bagel.java 类,并在 NetBeans 的选项卡中打开它。

A978-1-4842-0415-3_10_Fig12_HTML.jpg

图 10-12。

Use the New Java Class dialog and create the Bagel.java class in the invincibagel package

你要做的第一件事是将 Java extends 关键字添加到 NetBeans 为你编写的public class Bagel {...}类声明语句的末尾,这样你的 Bagel 类继承了我们在第八章中创建的 Hero 类的所有功能(变量和方法)。这个当前为空的类的 Java 代码应该如下所示:

package invincibagel;

public class Bagel``extends

// an empty class structure

}

我们要写的第一件事是 Bagel()构造函数方法,因为我们想创建一个 Bagel 字符放在屏幕上,这样我们就可以开始处理运动代码,然后是碰撞代码。这段代码将接受 Hero 类 Hero()构造函数方法需要接收的相同参数,并使用 Java super 关键字(我喜欢称之为 super 构造函数)以 super()构造函数方法调用的形式将它们“向上”传递给 Hero 类 Hero()构造函数。这个 Bagel()构造函数方法的 Java 代码应该类似于下面的 Java 类和构造函数方法结构:

public class Bagel extends Hero {

public Bagel(String SVGdata, double xLocation, double yLocation, Image... spriteCels) {

super (SVGdata, xLocation, yLocation, spriteCels);

}

}

正如你在图 10-13 中看到的,在百吉饼类名下有一个红色波浪下划线高亮显示。如果你把鼠标放在这上面,你会看到“Bagel 不是抽象的,也没有覆盖抽象方法”。update() in Hero”,它告诉您要么需要将 Bagel 设为一个公共抽象类,我们不打算这样做,因为我们希望实际使用这个类来保存角色(对象)及其属性(变量)和功能(方法),所以消除此错误的另一个选项是添加您的@Override public void update() {...}方法结构,即使它现在是一个空方法。

A978-1-4842-0415-3_10_Fig13_HTML.jpg

图 10-13。

Code a public Bagel() constructor method that calls a super() constructor method (from Hero superclass)

实现(当前)空的代码。update()方法使用一个 Java @Override 关键字,一旦它就位,错误将会消失,代码将是无错误的,如图 10-14 所示。代码如下所示:

@Override

public void``update``() { //``empty method

A978-1-4842-0415-3_10_Fig14_HTML.jpg

图 10-14。

Add a public void .update() method to override the public abstract void update method in the Hero class

注意在图 10-14 的顶部,您必须添加import javafx.scene.image.Image;代码语句,以便能够使用图像...公共 Bagel()构造函数方法参数列表中的批注。

为了彻底起见,让我们重写一个公共布尔值。collide()方法,所以我们在 Bagel 类中有它。您可能想知道为什么 NetBeans 没有在图 10-14 中给我们一个错误。collide()方法添加到 Bagel 类中。正如你在图 10-15 中看到的,它显示了公共的抽象英雄类,我们没有创建。collide()方法是一个公共抽象方法,就像我们用。update()方法。这就是 NetBeans 8 没有生成任何错误突出显示的原因,因为我们不需要实现。所有 Hero 子类中的 collide()方法。

A978-1-4842-0415-3_10_Fig15_HTML.jpg

图 10-15。

The Hero abstract class has a public boolean collide() method but since it is not abstract it is not required

你可能想知道为什么我们没有?collide()到一个抽象方法中,需要注意的是,我们可以在未来的任何时间点这样做。原因是,我们可能希望在未来游戏开发的某个时候有(添加)不与场景(游戏)中任何东西冲突的运动精灵,也许是为了添加视觉细节元素,例如一只鸟飞过屏幕顶部。这是您自己的选择,所以如果您希望运动精灵总是与物体碰撞,您可以声明。collide()方法也是抽象的。

需要注意的重要一点是,我们仍然可以重写。collide()方法,这是我接下来要做的,只是为了向您展示,这仍然可以做到,而不必使用 Java 抽象关键字声明该方法,Bagel 类将使用被覆盖的。collide()方法而不是 Hero 超类中的“default”方法,后者返回一个 false 值(无冲突)。

这里需要注意的重要一点是,您可以将您的默认方法代码放入超类中,如果没有在任何给定的子类中被特别覆盖,它将成为您所有子类的默认方法编程逻辑。这允许你在一个地方(超类)实现所有子类的“默认”行为。

当然,您总是可以覆盖这个默认行为,并使用@Override 关键字和完全相同的方法声明格式来实现一个更具体的行为。这可以在靠近屏幕截图底部的图 10-16 中看到,如果您将它与图 10-15 的底部进行比较,您会看到它们的结构是相同的,除了在 Bagel 子类中使用的@Override 关键字。当我们讨论碰撞检测编程时,我们将用 Bagel 类自己定制的代码替换return false;行。collide()碰撞检测行为,随着时间的推移,随着我们向游戏中添加高级功能,这将变得相当复杂。现在我正在安装这个。collide()方法体(本质上是空的,因为它只返回 false ),所以您会看到一个完整的类。

A978-1-4842-0415-3_10_Fig16_HTML.jpg

图 10-16。

Override public boolean .collide() method body, for our use later on during a collision detection chapter

我们在这一章取得了很多好的进展,为你的游戏创造了一个选角导演和明星!

摘要

在第十章中,我们为游戏添加了两个关键类:CastingDirector.java 类和 Bagel.java 类。第一个执行演员管理和碰撞管理功能,第二个为游戏添加主要演员,这样我们就可以开始研究 InvinciBagel 如何在屏幕上移动。我们看了当前包和类结构的图表,以及新类如何适应我们在本书中实现的整体游戏引擎设计策略。

我们学习了什么是 Java 接口,以及 Java 接口如何允许我们控制其他开发人员对我们的类实现了什么。我们还学习了 Java 集合框架,它提供了数组、列表和集合等东西,用于为我们的 Java 8 和 JavaFX 应用(游戏)提供数据存储功能。

我们学习了 java.util 包及其 List 接口,以及 ArrayList 类,以及 ArrayList 类如何实现这个 List 接口。我们学习了元素、键、值和类型。我们了解到 List 和 ArrayList 对象有结构和顺序,而 Set 和 HashSet 对象没有特定的顺序,并且不能有重复的元素。

接下来,我们创建了您的 CastingDirector.java 类,来管理需要添加到游戏中和从游戏中移除的演员对象。这个类还将维护列表结构,该结构将用于冲突检测逻辑,我们将在本书第十六章的中添加。

最后,我们创建了第一个与演员相关的类,Bagel 类,它扩展了 Hero 超类,允许我们将主要的 InvinciBagel 演员对象角色放到游戏场景和舞台上。我们创建了 Bagel()构造函数方法,并使用@Override 关键字来覆盖。更新()和。collide()方法,这样我们就可以在本书的剩余部分中构建与这个角色相关的编程逻辑。

在下一章中,我们将看看如何使用我们在本章中创建的 KeyEvent 事件处理结构在屏幕上移动游戏精灵,以及如何确定屏幕的边界(边缘)、角色方向、移动速度以及相关的动画和移动注意事项。

十一、在 2D 移动你的演员形象:控制 X 和 Y 显示屏幕坐标

现在我们已经在第十章中创建了公共的 CastingDirector 类,我称之为“casting engine”,我们需要回到第十一章中的 InvinciBagel.javaprimary 应用类代码,并在。createGameActors()方法。我们还将使用 CastingDirector.java 类及其 CastingDirector()构造函数方法创建 castDirector 对象,这是我们在第十章中创建的。createCastingDirection()方法,它将管理我们的转换方向类相关的特性。

在我们将代码添加到将创建 iBagel Bagel 对象的 InvinciBagel.java 类中,并创建 castDirector CastingDirector 对象之后,我们将把代码重新组织成逻辑方法结构,用于 InvinciBagel 类中需要处理的主要任务区域。在我们这样做之后,我们将有八个逻辑方法区域。这些方法将作为功能区域的“指南”,当我们在本书的其余部分开发我们的游戏时,我们需要保持更新(添加语句)。例如,如果我们向游戏中添加一个 Actor,我们将通过在. createGameActors()方法中添加(实例化)Actor 对象来完成,然后将 Actor 对象添加到在新的中使用新的 CastingDirector()构造函数方法创建的 cast 对象中。createCastingDirection()方法。

除了。createGameActors()和。createCastingDirector()方法,我们的新方法将包括。loadImageAssets()方法。createSceneEventHandling()方法。createStartGameLoop()方法,而。addGameActorNodes()方法。因此,在这一章中,我们将为你的 InvinciBagel.java 类创建六个新方法,以显著地“增强”游戏核心类的顶层组织结构及其“顶层”。start()方法。只有一种方法可以在这个过程中存活下来,不需要任何修改;那将是。addNodesToStackPane()方法,您在第六章的中创建的方法(请参见图 6-8 以提醒您的记忆)。

在我们重组了我们的 InvinciBagel 代码基础设施之后,我们可以继续前进,开始创建程序逻辑,该程序逻辑将用于创建并在以后控制我们游戏的主要英雄,不可战胜的恶魔本人。这将涉及使用 Bagel()和 CastingDirector()构造函数方法,然后使用。add()方法调用和。addCurrentCast()方法分别调用。

在我们创建了 iBagel Actor 之后,我们将连接它的。将()方法更新到 GamePlayLoop。handle()方法,此时我们可以开始构建编程逻辑,在您的舞台上移动这个 InvinciBagel。此时,事情变得更加有趣,因为我们可以开始定义舞台的移动边界、精灵图像状态(九个不同的字符位置),以及这些与 X(左右)和 Y(上下)键用法的关系。例如,没有运动将会是站立的,左和右将会使用奔跑,向上将会使用跳跃,向下将会着陆,或者稍后在游戏设计中,当我们精炼代码时,特定的组合键可以使一个不可战胜的怪物飞行等等。

InvinciBagel.java 重新设计:增加逻辑方法

关于 InvinciBagel.java 代码,我想做的第一件事是使用半打新方法重新组织当前代码,这些新方法在逻辑上包含并显示了我们需要解决的不同领域,以便为我们的游戏添加新的参与者。这些包括管理事件处理、添加新的图像资产引用、创建新的游戏演员对象、将新的演员添加到场景图形、将新的演员对象添加到我们在第十章中创建的 CURRENT_CAST 列表,以及启动 GamePlayLoop animation timer 脉冲引擎。我们应该做的第一件事是把那些需要先做的 Java 语句放在。代码顶部的 start()方法本身。这些创建场景图形根,一个场景对象,并设置舞台对象:

primaryStage.setTitle(InvinciBagel);

root = new StackPane();

scene``= new Scene(``root

primaryStage.setScene(``scene

primaryStage.show();

正如你在图 11-1 中看到的,我们从被调用的方法中取出了根和场景对象实例。createSplashScreenNodes(),并将它们放在。start()方法。我这样做是因为它们是我们无敌游戏(职业)的基础。接下来,我们还将在现有代码中添加六个全新的方法结构。在这个过程中唯一不变的方法是您的。addNodesToStackPane()。你可以看到我是按照逻辑顺序调用这些方法的:添加事件、添加图像、添加演员、添加场景图和添加演员。

A978-1-4842-0415-3_11_Fig1_HTML.jpg

图 11-1。

Place basic configuration statements at top of the .start() method, and then the eight game method calls

在本章的第一部分中,我们将要用到的方法调用涉及到为场景对象创建事件处理器,我们将在设置好名为 Scene 的场景对象后立即执行。在添加到游戏的过程中,我们需要做的下一件事是加载图像对象资产(数字图像引用),这是您的。loadImageAssets()方法就可以了。一旦您的图像对象被声明和实例化,我们就可以使用。createGameActors()方法和构造器方法调用,我们在自定义 Actor 和 Hero 子类中创建了它们,比如我们在第十章中创建的 Bagel.java 类。一旦我们创建了演员,我们就可以使用。addGameActorNodes()以及使用。createCastingDirection()方法。最后,我们通过调用。createStartGameLoop()方法。那个。createSplashScreenNodes()和。addNodesToStackPane()位于末尾,因为现在闪屏内容制作工作已经完成,所以不会添加它们。我们将在本章中添加的方法调用代码如下所示:

createSceneEventHandling();

loadImageAssets();

createGameActors();

addGameActorNodes();

createCastingDirection();

createSplashScreenNodes();

addNodesToStackPane();

createStartGameLoop();

让我们言归正传,开始为 InvinciBagel.java 类实现这个代码重新设计过程。

场景事件处理方法:。createSceneEventHandling()

我们要做的第一件事是将游戏的事件处理移到它自己的方法中,我们称之为。createSceneEventHandling()。我创建一个方法来创建场景对象事件处理的原因是,如果以后你想在你的游戏中添加其他类型的输入事件,比如鼠标事件或拖动事件,你已经有了一个逻辑方法来保存这个事件相关的 Java 代码。

这个新的 Java 代码,可以在图 11-2 中看到,将涉及到把你的 scene.setOnKeyPressed()和 scene.setOnKeyReleased()方法处理结构,在第九章中创建,从你的。createSplashScreenNodes()方法,并将它们放入自己的方法结构中。稍后我们将重新定位所有 ActionEvent 处理器,它们在。start()方法,实际上属于。createSplashScreenNodes()方法,在该方法中,它们将与其他 splashscreen 对象组合在一起。这个新的事件处理代码结构应该类似于下面的 Java 代码:

private void``createSceneEventHandling()

scene.setOnKeyPressed((KeyEvent event) -> {

switch (event.getCode()) {

case UP:    up     = true; break;

case DOWN:  down   = true; break;

case LEFT:  left   = true; break;

case RIGHT: right  = true; break;

case W:     up     = true; break;

case S:     down   = true; break;

case A:     left   = true; break;

case D:     right  = true; break;

}

});

scene.setOnKeyReleased((KeyEvent event) -> {

switch (event.getCode()) {

case UP:    up     = false; break;

case DOWN:  down   = false; break;

case LEFT:  left   = false; break;

case RIGHT: right  = false; break;

case W:     up     = false; break;

case S:     down   = false; break;

case A:     left   = false; break;

case D:     right  = false; break;

}

});

}

A978-1-4842-0415-3_11_Fig2_HTML.jpg

图 11-2。

Create private void createSceneEventHandling() method for OnKeyReleased and OnKeyPressed event handling structures

既然游戏的事件处理已经就绪,在我们编写添加图像、演员和演员导演对象的其余方法结构之前,我们需要声明这些对象,以便在 InvinciBagel.java 类的顶部使用。让我们来完成这项工作,它设置了接下来需要编码的其余方法。

添加 InvinciBagel:声明图像、百吉饼和 CastingDirector

由于我们将在本章中开始在游戏屏幕上显示我们的英因西贝戈尔角色,并将我们在前面章节中编写的所有代码集合在一起,创建我们的游戏循环类(第七章)、演员和英雄类(第八章)、事件处理类(第九章)和 CastingDirector 类(第十章),我们需要在 InvinciBagel 类的顶部声明一些对象变量,然后才能在本章中实例化和使用这些对象。我们将使用静态关键字声明名为 iBagel 的 Bagel 对象,正如我们将调用 iBagel 对象一样。从 GamePlayLoop 对象的。handle()方法,这将使 iBagel 在这两个类之间“可见”。我们还将通过使用复合声明来声明九个 Image (sprite 状态)对象,iB0 到 iB8。最后,我们将声明一个 CastingDirector 对象,我们将其命名为 castDirector。我们需要在 InvinciBagel.java 类顶部添加的声明语句参见图 11-3 。它们包括位于 InvinciBagel.java 类顶部的以下 Java 变量声明语句:

static Bagel iBagel;

Image iB0, iB1, iB2, iB3, iB4, iB5, iB6, iB7, iB8;

CastingDirector castDirector;

A978-1-4842-0415-3_11_Fig3_HTML.jpg

图 11-3。

Add static Bagel iBagel, CastingDirector castDirector and Image object declaration named iB0 through iB8

既然我们已经声明了对象变量,我们将需要在。createGameActors()方法和 CastingDirection 引擎在 createCastingDirection()方法中,让我们继续创建我们的第一个新方法。loadImageAssets()方法,它将包含对 Image()构造函数的所有图像对象实例化调用。我们将把所有图像对象实例化到这个方法中。

演员图像资产加载方法:。loadImageAssets()

既然我们已经声明了在 InvinciBagel 类的顶部使用的九个图像对象,接下来我们需要做的就是将九个 PNG32 sprite 图像复制到 InvinciBagel NetBeans 项目的/src 文件夹中,这九个图像被命名为 sprite0.png 到 sprite8.png。这是使用适用于您的操作系统的文件管理实用程序来完成的;在我的 64 位 Windows 7 操作系统的情况下,它是 Windows Explorer 实用程序,如图 11-4 所示,图像资产被复制到C:/Users/user/MyDocuments/NetBeansProjects/InvinciBagel/src文件夹中。所有 PNG 图像资源都是 PNG32 (24 位 RGB 真彩色数据,带有 8 位 256 灰度级 alpha 通道),除了闪屏的背板是 PNG24,因为它不需要 alpha 通道,因为它是背景图像板。

A978-1-4842-0415-3_11_Fig4_HTML.jpg

图 11-4。

Copy the sprite0.png through sprite8.png files into your NetBeansProjects/InvinciBagel/src project folder

现在我们准备编写private void loadImageAssets(){...}方法。一旦创建了方法体(声明),您将需要从。createSplashScreenNodes()方法,以便游戏应用的所有图像对象加载都在一个中心位置完成。完成之后,您可以复制并粘贴 scoresLayer 映像实例化,并创建 iB0 到 iB8 映像实例化。确保将图像大小设置为 81 像素(X 和 Y ),并使用正确的文件名引用,如以下代码所示:

private void``loadImageAssets

splashScreen = new Image("/invincibagelsplash.png", 640, 400, true, false, true);

instructionLayer = new Image("/invincibagelinstruct.png", 640, 400, true, false, true);

legalLayer = new Image("/invincibagelcreds.png", 640, 400, true, false, true);

scoresLayer = new Image("/invincibagelscores.png", 640, 400, true, false, true);

iB0 = new Image("/sprite0.png", 81, 81, true, false, true);

iB1 = new Image("/sprite1.png", 81, 81, true, false, true);

iB2 = new Image("/sprite2.png", 81, 81, true, false, true);

iB3 = new Image("/sprite3.png", 81, 81, true, false, true);

iB4 = new Image("/sprite4.png", 81, 81, true, false, true);

iB5 = new Image("/sprite5.png", 81, 81, true, false, true);

iB6 = new Image("/sprite6.png", 81, 81, true, false, true);

iB7 = new Image("/sprite7.png", 81, 81, true, false, true);

iB8 = new Image("/sprite8.png", 81, 81, true, false, true);

}

正如你在图 11-5 中所看到的,你的代码是没有错误的,这意味着你已经将你的精灵资产复制到了正确的/src 文件夹中,现在你已经安装了十几个数字图像资产用于你的游戏。

A978-1-4842-0415-3_11_Fig5_HTML.jpg

图 11-5。

Create a private void loadImageAssets() method, add the iB0 through iB8 and splashScreen Image objects

现在你需要调用你在第十章中创建的 Bagel()构造函数方法的资产已经准备好了,我们可以继续创建一个保存游戏资产创建 Java 代码的方法。这相当于为我们最终创建的每个 Actor 子类调用构造函数方法,其中第一个是 Bagel 类,我们首先创建它,以便我们可以开始让我们的主要角色在屏幕上移动。

正在创建您的 InvinciBagel 百吉饼对象:。createGameActors()

加载图像资源后,游戏角色创建过程的下一步是调用游戏角色的构造函数方法。为了能够做到这一点,您必须首先子类化 Actor 超类(用于固定的游戏演员,可以称为“道具”)或 Hero 超类(用于运动游戏演员,例如英雄、他的敌人等等)。我将创建一个. createGameActors()方法来保存这些实例,因为即使最初在这个方法的主体中只有一行代码,最终,随着游戏变得越来越复杂,这个方法将作为我们已经安装的游戏角色资产的“路线图”。该方法将被声明为私有方法,因为 InvinciBagel 类将控制这些游戏角色的创建,并将具有 void 返回类型,因为该方法不向调用实体返回任何值。本例中是 start()方法)。在方法内部,我们将调用 Bagel()构造函数方法,使用一些“占位符”SVG 路径数据,以及一个 0,0 的初始 X,Y 屏幕位置,最后,九个 sprite cels 在构造函数方法调用的末尾使用逗号分隔的列表。方法体和对象实例化将使用以下三行 Java 代码:

private void``createGameActors()

iBagel =``new

}

正如你在图 11-6 中看到的,代码是没有错误的,你现在有了一个 iBagel Bagel 对象,你现在可以用它来开始开发 InvinciBagel sprite 在游戏进行阶段的移动,这通常是整个显示屏。在本章的稍后部分,我们将把这个 Bagel Actor 连接到 JavaFX 脉冲计时引擎。

A978-1-4842-0415-3_11_Fig6_HTML.jpg

图 11-6。

Add a private void createGameActors() method; add an iBagel object instantiation via Bagel() constructor

如果您想知道 SVGdata 字符串对象“M150 0 L75 200 L225 200 Z”是做什么的,它是以下画线指令(命令)的简写。M 是一个“绝对移动”命令,它告诉画线(或者在这种情况下是路径画)操作从位置 150,0 开始。L 是一个“画线”命令,告诉 SVG 数据在 150,0 到 75,200 之间画一条线。第二个 L 从 75,200 到 225,200 画一条线,给我们三角形的两条边。Z 是一个“关闭形状”命令,如果形状是打开的,就像我们当前的一样,它会画一条线来关闭形状。在这种情况下,这相当于从 225,200 到 150,0 画一条线,给我们三角形的三条边,封闭开放路径,给我们一个有效的碰撞检测边界。

我们将在稍后用一个更复杂的碰撞形状替换它,在第十六章的中,涵盖了碰撞检测多边形创建、SVG 数据和碰撞检测逻辑。我们实际的碰撞多边形将包含更多的数字,这使得我们的 Bagel()构造方法调用变得笨拙。正如你可能想象的那样,在游戏的那个点上(没有双关的意思),我可能会创建一个专门用于构建碰撞形状的工作进程。这个工作流程将向您展示如何使用 GIMP 生成 SVG 多边形数据,以便您可以将 SVG 数据放入它自己的 String 对象中,并在您的 Actor 对象构造函数中引用它。如果你想把碰撞数据的创建也变成它自己的方法,这就是看起来的样子,使用一个(理论上的)。createActorCollisionData()方法:

String``cBagel

private void``createActorCollisionData()

cBagel = "M150 0 L75 500 L225 200 Z";

}

private void createGameActors() {

iBagel = new Bagel(``cBagel

}

稍后,您还可以创建一个方法来加载图像精灵列表对象。这将把 ArrayList 作为参数传递,而不是逗号分隔的列表。注意,如果你这样做了,你还需要改变你的 Actor 抽象类构造函数来接受一个 ArrayList 对象,而不是一个图像...图像对象列表。

接下来,让我们看看如何将新创建的 iBagel 对象添加到游戏的场景图形对象中,该对象目前是一个名为 root 的 Stackpane 对象。

将 iBagel 添加到场景图:。addGameActorNodes()

JavaFX 应用开发人员经常忘记的一个步骤是将他们需要在场景中显示的对象(以及场景对象所附加的舞台)添加到场景图的根对象中。在我们的例子中,这是一个名为 root 的 StackPane 对象。当我们开始开发 Splashscreen 时,我们将需要使用在第六章中使用的相同的root.getChildren().add()方法调用链,来将我们的 iBagel 对象 ImageView(使用 iBagel.spriteFrame 引用)添加到场景图形根对象。我将在这个阶段添加一个方法,确保我们永远不会忘记这个重要的添加到场景图的步骤。我将通过把演员创作过程中的这个阶段变成它自己的方法来专门解决这个问题,我将称之为。addGameActorNodes()。这个方法体的创建,以及我们的第一个向场景图编程语句添加演员,将使用下面的 Java 代码来完成,这些代码也在图 11-7 中显示(突出显示):

private void``addGameActorNodes()

root``.getChildren().add(``iBagel.spriteFrame

}

A978-1-4842-0415-3_11_Fig7_HTML.jpg

图 11-7。

Create a private void addGameActorNodes() method; .add() iBagel.spriteFrame ImageView to root object

需要注意的是,我称之为。addGameActorNodes()方法,位于 InvinciBagel 类的顶部,在。start()方法,在我调用。addNodesToStackPane()方法。这样做有一个很好的理由,它可以追溯到你在第四章中所学的 JavaFX。请记住,您添加到 StackPane 图层管理对象的对象是使用 Z 索引(或 Z 顺序)显示的,这意味着它们“堆叠”在彼此的顶部。如果这些层中的任何一层没有阿尔法通道,我们在第五章中已经了解过,那么它们后面的任何东西都无法显示出来!出于这个原因,让我们的闪屏在玩家点击指令按钮控制对象的任何时间点覆盖我们的游戏的最简单的方法是最后添加这些资产。

通过拥有你的。addNodesToStackPane()方法。addGameActorNodes()方法,您将保证您的游戏资产将始终处于比您的 Splashscreen 资产更低的 Z 索引。这意味着 SplashScreenBackplate 和 splash screen textarea ImageView“plates”将始终位于 StackPane 的顶部 Z-index 层,因此,当它们显示(可见)时,它们将完全覆盖您的游戏。这是因为 SplashScreenBackplate ImageView 包含一个不透明的 PNG24 图像资源,其大小与您的场景(和舞台)对象相同。

当我们测试新的 InvinciBagel 游戏应用时,我们将看到这种方法顺序重组的结果,您将看到我们已经解决了游戏演员显示在闪屏顶部的问题。我们只是通过改变程序代码的执行顺序来实现这一点。这也应该向您指出,Java 编程代码的执行顺序几乎与 Java 编程逻辑本身一样重要!

创建和管理您的角色:。createCastingDirection()

现在是时候实现我们在第十章中创建的另一个类了,CastingDirector.java 类,以及它的 CastingDirector()构造函数方法。我们将在另一个新的自定义方法中这样做,我们将创建一个名为。createCastingDirection()。该方法将包含名为 castDirector 的 CastingDirector 对象的初始实例化,我们将使用 Java new 关键字和 CastingDirector()构造函数方法创建该对象,并使用。我们在第十章中创建的 addCurrentCast()方法。图 11-8 中显示的 Java 方法结构没有错误,应该如下所示:

private void``createCastingDirection()

castDirector =``new

castDirector``.addCurrentCast

}

A978-1-4842-0415-3_11_Fig8_HTML.jpg

图 11-8。

Create private void createCastingDirection() method with castDirector and .addCurrentCast() statements

现在我们已经将图像资产放置到位,创建了演员对象,将他添加到场景图中,创建了 CastingDirector 引擎,并将 iBagel 添加到角色中,我们已经准备好处理游戏计时引擎了。

创建并开始你的游戏循环:。创建 StartGameLoop

我要跳到。createSceneGraphNodes()方法,它仍然是我们最复杂的方法体,我将它保存到最后,并创建一个名为。createStartGameLoop()。在这个方法中,我们将创建并启动我们的 GamePlayLoop 对象,它是我们在第七章中使用 GamePlayLoop 类创建的。这个类扩展了 JavaFX AnimationTimer 超类,为我们的游戏提供对 JavaFX 脉冲计时引擎的访问。在里面。createStartGameLoop()方法我们将使用 Java new 关键字,使用 GamePlayLoop()构造函数方法为我们名为 gamePlayLoop 的游戏创建一个脉冲引擎。之后,我们将调用。这个 gamePlayLoop 对象的 start()方法来启动脉冲事件计时引擎。该调用通过使用以下四行 Java 编程逻辑来完成,在图 11-9 中也显示为无错误:

private void``createStartGameLoop()

gamePlayLoop =``new

gamePlayLoop``.start()

}

A978-1-4842-0415-3_11_Fig9_HTML.jpg

图 11-9。

Create a private void createStartGameLoop() method, and create and .start() the gamePlayLoop object

正如你在图 11-9 的底部所看到的,我已经折叠了其他的方法结构,并且它们在代码中的顺序与它们在。start()方法,用于组织目的。我最后启动一个游戏循环,因为我想确保在启动 JavaFX 脉冲引擎启动游戏之前,我已经完成了设置游戏环境所需的所有工作。如你所见,我使用了 Java 方法名和我的游戏代码设计,以提醒我每次添加新的游戏角色时需要做什么,现在我们的英雄已经就位,可能是游戏道具、投射物、敌人、宝藏等等。

更新 Splashscreen 场景图:. createsplash screen 节点()

现在是时候重组我们的。createSplashScreenNodes()方法体,然后我们将准备好把我们在 GamePlayLoop.java 类中创建的 JavaFX 脉冲引擎“连接”到我们使用 Actor.java、Hero.java 和 Bagel.java 类创建的 Actor 对象。我们已经从。createSplashScreenNodes()方法,并将它们放入。loadImageAssets()方法,它们更符合逻辑。我们需要做的另一件事是尝试简化我们的。start()方法将 ActionEvent 处理结构与它们各自的对象实例化和配置 Java 语句组合在一起。因此,举例来说,你的 gameButton 对象实例化、配置和事件处理都将保存在一个地方。我们将对 helpButton、scoreButton 和 legalButton 对象做同样的事情。我从。start()方法添加到。createSplashScreenNodes()方法在这里以粗体显示。新的。createSplashScreenNodes()方法体将包含以下三十几行 Java 代码:

private void``createSplashScreenNodes()

buttonContainer = new HBox(12);

buttonContainer.setAlignment(Pos.BOTTOM_LEFT);

buttonContainerPadding = new Insets(0, 0, 10, 16);

buttonContainer.setPadding(buttonContainerPadding);

gameButton = new Button();

gameButton.setText("PLAY GAME");

gameButton.setOnAction((ActionEvent) -> {

splashScreenBackplate.setVisible(false);

splashScreenTextArea.setVisible(false);

});

helpButton = new Button();

helpButton.setText("INSTRUCTIONS");

helpButton.setOnAction((ActionEvent) -> {

splashScreenBackplate.setVisible(true);

splashScreenTextArea.setVisible(true);

splashScreenTextArea.setImage(instructionLayer);

});

scoreButton = new Button();

scoreButton.setText("HIGH SCORES");

scoreButton.setOnAction((ActionEvent) -> {

splashScreenBackplate.setVisible(true);

splashScreenTextArea.setVisible(true);

splashScreenTextArea.setImage(scoresLayer);

});

legalButton = new Button();

legalButton.setText("LEGAL & CREDITS");

legalButton.setOnAction((ActionEvent) -> {

splashScreenBackplate.setVisible(true);

splashScreenTextArea.setVisible(true);

splashScreenTextArea.setImage(legalLayer);

});

buttonContainer.getChildren().addAll(gameButton, helpButton, scoreButton, legalButton);

splashScreenBackplate = new ImageView();

splashScreenBackplate.setImage(``splashScreen

splashScreenTextArea = new ImageView();

splashScreenTextArea.setImage(``instructionLayer

}

请注意,由于。loadImageAssets()方法在。createSplashScreenNodes()方法,我们仍然可以在方法体中保留引用加载的图像资产的最后四行代码。这是因为已经创建了 splashScreen 和 instructionLayer 图像对象,并在。loadImageAssets()方法。因为此方法是在。start()方法放在。createSplashScreenNodes()方法时,可以在该方法体中安全地使用这些对象。

正如你在图 11-10 中看到的,新方法是无错误的,所有的对象,包括名为 buttonContainer 的 HBox,名为 gameButton、helpButton、scoreButton 和 legalButton 的按钮,以及名为 splashScreenBackplate 和 splashScreenTextArea 的 ImageView,都在逻辑上组合在一起,并且现在组织得很好。

A978-1-4842-0415-3_11_Fig10_HTML.jpg

图 11-10。

Copy .setOnAction() event handlers from the .start() method into the .createSplashScreenNodes() method

因为我们不需要对你的。addNodesToStackPane()方法,我们已经完成了我们在这里需要做的代码重组,然后我们将这个游戏带到下一个复杂的级别!时不时地,你需要回去确保你的编程逻辑是最优结构的,这样当你构建更复杂的结构时,你就有了一个坚实的基础,就像你在构建一个真正的建筑结构一样。

为 iBagel 演员提供动力:使用游戏循环

接下来,让我们“连线”或连接这些游戏引擎,我们已经在本书前半部分的游戏代码基础设施中放置了这些引擎。我们需要做的第一件事是告诉 GamePlayLoop AnimationTimer 子类中的游戏引擎,我们希望它在每个脉冲上查看(更新)iBagel Bagel 对象。为此,我们需要在 GamePlayLoop.java 类中安装两行主要代码。第一个是对名为 iBagel 的静态 Bagel 对象的引用,我们在图 11-3 所示的代码中使用import static invincibagel.InvinciBagel.iBagel Java 语句声明了该对象。我们需要安装的第二行代码将位于。handle()方法,并将用于“连接”该。句柄()方法(脉冲引擎)到 iBagel 对象。update()方法。新的 GamePlayLoop 类导入语句和。handle()方法应该看起来像下面的 Java 代码,如图 11-11 所示,没有错误:

import javafx.animation.AnimationTimer;

import static invincibagel.InvinciBagel.iBagel;

public class GamePlayLoop extends AnimationTimer {

@Override

public void``handle``(long``now

iBagel.update();

}

}

A978-1-4842-0415-3_11_Fig11_HTML.jpg

图 11-11。

Add a Java statement inside of the GamePlayLoop .handle() method invoking an iBagel.update() method

Java 语句所做的是调用。在每个脉冲事件上更新名为 iBagel 的 Bagel 对象的()方法。你放进去的任何东西。update()方法每秒将执行 60 次。任何其他想要以 60 FPS 处理的 Actor 对象,只需添加一个类似的。对此的 update()调用。handle()方法。

移动 iBagel Actor 对象:编码您的。update()方法

现在我们准备开始开发代码,在屏幕上移动我们的 InvinciBagel Actor 对象。我们将在本书的剩余部分中完善这些 Java 代码,因为一切都围绕着这个主要的演员对象和他的动作。这包括他移动的位置(边界和碰撞),他移动的速度(速度和物理),以及他移动时的样子(在精灵图像单元或“状态”之间制作动画)。所有这些代码都源自 iBagel 对象的内部。update()方法,所以我们将通过添加一些基本代码来开始这个漫长的旅程,这些代码查看我们的 InvinciBagel.java 类中的布尔变量,并保存箭头键(或 ASDW 键)按下和释放状态,然后使用条件 If 语句处理这些状态。这个条件语句处理的结果将在屏幕上移动 InvinciBagel 字符(最初,稍后我们将添加更高级的编程逻辑)。我们最终会让这种运动和互动越来越“智能”。我们要做的第一件事是使用 import static 语句使 Boolean 变量 up、down、left 和 right 对 Bagel 类可见,正如我们在本章前面所做的那样,使 iBagel 对象对 GamePlayLoop 类可见。handle()方法。添加的四个导入静态语句如下所示:

package invincibagel;

import static``invincibagel.InvinciBagel.``down

import static``invincibagel.InvinciBagel.``left

import static``invincibagel.InvinciBagel.``right

import static``invincibagel.InvinciBagel.``up

import javafx.scene.image.Image;

public class Bagel extends Hero {...}

正如你在图 11-12 中看到的,这段代码没有错误或警告,我们准备好继续添加条件编程逻辑,看看这四个变量中哪些设置为真,哪些设置为假,哪些设置为释放。在这些条件语句中,我们将根据 vX 和 vY(演员移动速度)变量放置移动 iX 和 iY(演员位置)变量的代码。

A978-1-4842-0415-3_11_Fig12_HTML.jpg

图 11-12。

Add import static invincibagel.InvinciBagel references to static boolean down, left, right, and up variables

由于我们在 Bagel 对象内部编写代码(在我们的例子中,我们实例化并命名它为 iBagel),我们将有机会利用并理解 iX 和 iY 变量,这将在本章的下一节中进行,届时我们将开发访问和更改 iBagel Bagel 对象的 iX 和 iY 位置属性的代码语句,并添加访问和利用 iBagel Bagel 对象的 vX 和 vY velocity 属性的代码。

构建。update()方法:使用 If 语句确定 X 或 Y 移动

现在是时候在 Bagel 类中添加一些基本的 Java 编程逻辑了。update()方法,该方法将沿 X 轴或 Y 轴移动 iBagel 对象(如果按下了多个键,则沿两个轴移动)。因为我们的 iX 和 iY 变量保存了屏幕上的 Actor 位置,所以我们将在每个 if 语句中使用它们,并分别增加(或减少)每个轴的速度变量数量(如果我们处理 iX,则为 vX;如果我们处理 iY,则为 vY)。我们最初将 vX 和 vY 值设置为 1,这相当于一个相对缓慢的移动。如果 vX 和 vY 设置为 2,iBagel 将移动两倍的速度(它将在每个脉冲事件上移动两个像素,而不是一个像素)。

如果右边的布尔变量为真,我们希望你的 iBagel 对象沿着 X 轴正向移动,所以我们将使用一个if(right){iX+=vX}编程语句,使用我们在第三章中学到的+=运算符将 vX 速度值加到 iX 位置值上。类似地,如果左边的布尔变量为真,我们将使用一个if(left){iX-=vX}编程语句,它将使用-= Java 操作符从 iX 位置值中减去 vX 速度值。

当向上和向下(或 W 和 S)键被按下时,我们将沿着 Y 轴做本质上相同的事情。如果 down 布尔变量为 true,我们希望 iBagel 对象沿着 Y 轴正向移动。因此,我们将使用一个if(down){iY+=vY}编程语句,使用+=运算符将 vY 速度值加到 iY 位置值上。在 JavaFX 中,正的 X 值从 0,0 原点向右,而正的 Y 值从 0,0 向下。最后,为了向上移动 iBagel,我们将使用一个if(up){iY-=vY}编程语句,它将使用-=操作符从 iY 位置值中减去 vY 速度值。执行这四个条件 if 语句求值的基本 Java 代码,以及它们各自在 Bagel 类中的 X 或 Y sprite 移动计算。update()方法,如图 11-13 所示,到目前为止应该看起来像下面的方法体结构:

@Override

public void update() {

if(``right

if(``left

if(``down

if(``up

}

A978-1-4842-0415-3_11_Fig13_HTML.jpg

图 11-13。

Add four if statements to the .update() method, one for each right, left, down, and up boolean variable

接下来,让我们使用 ImageView 在屏幕上移动 iBagel。setTranslateX()和。setTranslateY()方法。

移动场景图 ImageView 节点:。setTranslateX()和。setTranslateY()

现在我们已经有了条件语句,它将根据玩家按下(或未按下)的箭头键(或 ASDW 键)来处理 invincibage 在屏幕上的位置,让我们添加 Java 编程语句,这些语句将从 InvinciBagel iX 和 iY 变量中获取数据,并将此 sprite 位置信息传递给 spriteFrame ImageView 节点对象,以实际让它在显示屏上重新定位节点。那个。setTranslateX()和。setTranslateY()方法是节点超类的转换方法的一部分。这些方法还包括将旋转和缩放节点对象的方法调用;在这种情况下,Actor spriteFrame ImageView 节点包含 List ArrayList 对象中的一个图像资产。

当我们称之为。setTranslate()方法,在 iBagel 对象的 spriteFrame ImageView 节点对象之外,我们引用安装在抽象 Actor 超类内部的 spriteFrame ImageView 对象。由于 Actor 超类用于创建 Hero 超类,而 Hero 超类用于创建 Bagel 类,因此可以通过使用 spriteFrame.setTranslateX)语句在 Bagel 类内部引用 spriteFrame ImageView 对象,如以下 Java 代码所示。update()方法,如图 11-14 所示:

public void update() {

if(right) { iX += vX }

if(left)  { iX -= vX }

if(down)  { iY += vY }

if(up)    { iY -= vY }

spriteFrame.``setTranslateX

spriteFrame.``setTranslateY

}

正如你在图 11-14 中看到的,代码是没有错误和警告的,我们已经准备好测试我们在本章中编写的代码,包括重组的 InvinciBagel.java 类和它的六个新方法,更新的 Bagel.java 类和它的。update()方法和更新后的 GamePlayLoop.java 类,以及它的。handle()方法。

A978-1-4842-0415-3_11_Fig14_HTML.jpg

图 11-14。

After the four if statements, add statements calling the .setTranslate() methods off of the spriteFrame

测试我们的新游戏设计:移动无敌

在这一章中,我们对我们的游戏应用做了重大的修改,特别是对 InvinciBagel.java 类的结构,增加了六个全新的方法,并完全移动了我们的事件处理代码。我们创建了一个 iBagel Bagel 对象,和一个 castDirector CastingDirector 对象,使用了我们在第十章中创建的类。我们使用 JavaFX 脉冲引擎连接了 GamePlayLoop 对象和一个 Actor 对象(一个 iBagel Bagel 对象)。GamePlayLoop.java 类中的 handle()方法和。Bagel.java 类中的 update()方法。现在是时候使用我们的“运行➤项目”工作流程,并确保我们在本章中放置的所有 Java 代码都做了它应该做的事情:也就是说,我们认为它应该做的事情。毕竟,这就是编程实践的全部内容:编写我们认为会做某事的代码,运行它以查看它是否能做,然后调试它以找出它为什么不能工作,如果事实上不能的话。一旦您单击 NetBeans IDE 顶部的“播放”按钮并调用“运行➤项目”过程,代码将会编译,图 11-15 中所示的 invincibagel 游戏窗口将会在您的桌面上打开。您应该注意的第一件事是 InvinciBagel sprite 不见了,因为我们先将它添加到了根 StackPane 对象中,而不是最后,因此闪屏和游戏用户界面设计仍按预期工作。

A978-1-4842-0415-3_11_Fig15_HTML.jpg

图 11-15。

Use Run ➤ Project to start the game and click the PLAY GAME Button

接下来,让我们通过单击 PLAY GAME 按钮控件对象来测试 ActionEvent 处理,然后测试 KeyEvent 处理。这将隐藏 splashScreenBackplate 和 splashScreenTextArea ImageView 对象,并显示使用颜色为名为 Scene 的场景对象设置的白色背景色。白色常数。

正如你所看到的,在图 11-16 的左半部分,情况确实如此,我们的 invincibegel 角色在屏幕上,我们准备测试我们在第九章中放置的 KeyEvent 处理,看看我们是否能让 invincibegel(iBagel Bagel 对象)角色在屏幕上移动。随着我们完成每一章,这开始变得越来越令人兴奋!

让我们先测试最坏的情况,看看 JavaFX 脉冲事件和关键事件处理基础设施到底有多强大。同时按向上键和向左箭头键,或 A 键和 W 键。正如你所看到的,InvinciBagel 角色在一个对角线向量上平稳地移动,向左上方。这样做的结果可以在图 11-16 的右半部分看到。尝试使用单独的键,以确保它们正常工作。

A978-1-4842-0415-3_11_Fig16_HTML.jpg

图 11-16。

Hold a left arrow (or A) and up arrow (or W) key down at the same time, and move the Actor diagonally

当你玩你现在启用运动的 InvinciBagel 精灵时,注意你可以将他移动到屏幕底部的 UI 按钮后面,如图 11-17 的左半部分所示。这是因为你有你的。addGameActorNodes()方法。调用 addNodesToStackPane()方法,该方法赋予游戏中的所有内容比用户界面设计中的所有内容更低的 Z 索引。还要注意,你可以将 InvinciBagel 移出屏幕(玩家看不到),我们将在第十二章中解决这个问题,届时你将添加到现有代码中,以建立边界并实现其他高级移动功能。最后,请注意,如果您使用左右箭头键(不是 ASDW 键),按钮控制焦点(蓝色轮廓)也会移动,这意味着我们还必须在未来的章节中通过“消费”我们的 KeyEvents 来解决这个问题如您所见,在我们结束之前,有许多非常酷的代码要写,还有关于 Java 8 和 JavaFX 8.0 的东西要学!

A978-1-4842-0415-3_11_Fig17_HTML.jpg

图 11-17。

Notice the InvinciBagel character is using a lower Z-index than the UI elements, and can move off-screen

在本章中,你又一次取得了巨大的进步,优化了你的主要 InvinciBagel.java 类,实现了你在本书中已经编写的所有类,将游戏循环引擎连接到你的主要游戏角色中,测试了你所有的 ActionEvent 和 KeyEvent 处理,并且做得非常好!我认为这是相当成功的一章,我们将继续在每一章中度过美好时光!

摘要

在第十一章中,我们重组了我们的主要 InvinciBagel.java 类,提取了五个关键的游戏创建 Java 语句,然后将其余的 Java 代码组织成八个逻辑方法(例程),其中六个是我们在本章中从头开始创建的。这六个新方法用于打包诸如添加图像资产、创建新的演员对象、将演员添加到场景图、将演员添加到演员阵容、创建和启动游戏引擎以及实现游戏关键事件处理例程之类的事情。我们添加了对象声明,这样我们就可以为游戏的主要角色创建一个新的 iBagel Bagel 对象,还创建了一个 castDirector CastingDirection 引擎,这样我们就可以在后面的章节中管理角色成员。

我们学习了导入静态语句,并了解了如何使用它们将 iBagel Bagel 对象连接到 GamePlayLoop.java 引擎。handle()方法。我们还使用这些导入静态语句来允许我们的 Bagel.java 类处理。update()方法。

接下来,我们讲述了如何使用条件 if 语句来确定游戏玩家正在使用哪些关键事件(保存在四个布尔变量中)。我们将这个逻辑放在了 Bagel 类中。update()方法,我们知道 GamePlayLoop 每秒钟快速执行 60 次。handle() JavaFX 脉冲引擎。

最后,我们测试了本章中添加的所有新方法和 Java 语句,看看基本的游戏精灵运动是否有效。我们观察了一些需要在未来章节中解决的问题,并彻底测试了现有的 KeyEvent 处理方法和抽象 actor 类的 iX、iY、vX 和 vY 属性,我们创建这些抽象 Actor 类作为所有游戏 Actor 资产的基础。

在下一章中,我们将仔细研究 JavaFX Node 类,并研究关于在屏幕上移动游戏精灵的高级概念,以及如何确定屏幕的边界(边缘)、角色方向、移动速度以及相关的动画和移动注意事项。

十二、在 2D 为演员形象设置边界:使用节点类LocalToParent属性

现在我们已经将你的 Java 代码组织成了 InvinciBagel.java 类中的逻辑方法,并连接了 GamePlayLoop。对百吉饼的 handle()方法。update()方法在第十章中,为了确保我们的 KeyEvent 处理器会在屏幕上移动我们的 InvinciBagel 角色,是时候为我们的游戏英雄建立一些边界了,可以说,这样他就不会离开游戏场。要做到这一点,我们需要比在第四章中更深入地研究 JavaFX 节点超类。我们将看看变换是如何执行的,更重要的是,它们如何相对于父节点起作用,父节点在场景图中位于它们的上方。对于我们的 Actor ImageView 节点,父节点将是场景图根 StackPane 节点。

在我们开始了解代码复杂性之前,比如绝对或相对转换,我们将在本章中讨论,以及诸如碰撞检测和物理模拟之类的事情,我们将在稍后的第十六章和第十七章中讨论,我们将需要在第十二章中回到我们的 InvinciBagel.java 主要应用类 Java 代码,以便我们可以做更多的事情来优化我们游戏的 Java 8 基础。在这本书的第一部分,我们已经把我们的游戏引擎放到了适当的位置,在我开始在我们到目前为止已经放好的位置上构建复杂的代码结构之前,我想确保一切都“符合标准”。我们要确保一切都“锁得紧紧的!”

出于这个原因,我将在本章的前几页去掉那些导入静态 Java 语句,尽管它们工作得很好,正如您所看到的,它们并不是 Java 编程中的“最佳实践”。在类之间有一种更复杂的交流方式,涉及到 Java“this”关键字,所以我将向您展示如何实现更多的私有变量(和更少的静态变量),然后我将教您如何使用 Java this 关键字表示的引用对象,在类之间发送对象数据变量。

对于一本初学者级别的书来说,这是一个有点高级的主题,但它将允许您编写更专业和“行业标准”的 Java 8 代码,因此值得付出额外的努力。有时候,正确的做事方法比基本的(简单的)编码方法更复杂和详细。这里的假设是你将制作一个商业上可行的游戏,所以你需要一个坚实的基础来构建越来越复杂的代码。

在我们完成了在 InvinciBagel 类中添加额外的代码改进之后,这将尽可能使用私有变量实现 Java“封装”,并且在需要向其他相关类提供对 InvinciBagel 对象的访问的地方添加 this 关键字——在本例中,现在是 GamePlayLoop 和 Bagel 类——我们将开始向我们的 Bagel 类中的 sprite 运动代码添加复杂性。update()方法。

我们将添加代码,告诉你的无敌角色场景和舞台的天花板和地板在哪里,以及屏幕的左右两边在哪里,这样他就不会从他平坦的 2D 世界中掉下来。我们还将组织 Bagel.java 类中的方法,以便。update()方法只调用更高级别的方法,这些方法以一种异常组织良好的方式包含所有 Java 编程逻辑。

InvinciBagel 私有化:去除静态修饰符

关于 InvinciBagel.java 类 Java 代码,以及 GamePlayLoop.java 类代码和 Bagel.java 类代码,我想做的第一件事是从这两个“worker”类的顶部删除这些导入静态语句,并在 Bagel()构造函数方法和 GamePlayLoop()构造函数方法中使用 Java this 关键字传递 InvinciBagel 类(上下文)对象。此过程的第一步将跨越本章的下几页,将公共静态布尔变量声明复合语句更改为不使用静态修饰符,而使用 private 关键字代替公共访问控制修饰符,如下所示:

private boolean up, down, left, right;

正如你在图 12-1 中看到的,这不会在代码中生成任何红色错误或黄色警告高亮;但是,它会产生灰色波浪下划线。这表示突出显示的代码当前没有被使用。由于 Java 中关于静态修饰符关键字的“约定”或一般规则是将它们与常量一起使用,例如我们在代码的第一行,因此,我将通过移除静态修饰符(首先),并使许多其他声明私有,来尽可能地“封装”这个 InvinciBagel.java 类中的代码。

A978-1-4842-0415-3_12_Fig1_HTML.jpg

图 12-1。

Change the public static access modifiers for the boolean KeyEvent variables to be private access control

我们接下来要放置的代码将消除这种灰色波浪突出显示,事实上,我们将让 NetBeans 使用源代码➤插入代码➤ Getters 和 Setters 工作进程为我们编写代码,我们在第八章中了解了这一点,当时我们创建了 Actor 和 Hero 超类。那个。是()和。我们接下来要生成的 set()方法是一种解决方案,它允许我们消除公共静态变量声明,该声明允许您的 InvinciBagel.java 外部的类“深入内部”(可以认为这是一个安全漏洞)来获取这四个布尔变量。将这些变量设为私有可以防止这种情况。所以我们需要把。是()和。使用更“正式”的方法调用,将()方法设置到强制外部类和方法“请求”该信息的位置。

这一次,我们将使用 NetBeans 生成 getter 和 Setters 对话框,如图 12-2 所示,来选择性地写入 getter(即。is()方法)和 setters(方法。set()方法),它将访问四个布尔变量。从技术上讲,现在我们只需要使用吸气剂。is()方法,所以你可以使用生成➤ Getter 菜单选项,显示在图 12-2 中间(用红线包围的)所选 Getter 和 Setter 选项上方的中间(弹出或浮动)生成菜单中。我更喜欢生成这两个方法“方向”,以防在软件开发过程的后期,由于一些与游戏逻辑开发相关的编程原因,我需要设置这些变量(在外部,在另一个类中)。

A978-1-4842-0415-3_12_Fig2_HTML.jpg

图 12-2。

Use the Source ➤ Insert Code ➤ Getter and Setter dialog to create methods for the four boolean variables

在“生成 Getters 和 Setters”对话框中选择四个布尔变量 down、left、right 和 up,如图 12-2 的最右侧所示,单击光标,使其位于类中最后一个花括号的前面(这将告诉 NetBeans 您希望它在当前类结构的末尾编写或放置此代码),然后单击该对话框底部的“生成”按钮,以生成八个新的布尔变量访问方法结构。

正如你在图 12-3 中看到的,在你的 InvinciBagel.java 类的底部有八个新方法。需要注意的是。set()方法都使用 Java this 关键字将传递给方法的布尔变量设置为 up、down、left 或 right(私有)变量。那个。例如,setUp()方法应该是这样的:

public void``setUp

this .up = up;

}

在这种情况下,this.up 引用 InvinciBagel 对象(InvinciBagel 类)内部的私有 up 变量。

正如您所看到的,这是一种新的(更复杂,或者至少在代码方面更复杂)方式,我们现在可以访问 up 变量,而不必使用静态修饰符关键字和 Bagel.java 类顶部的导入静态声明来跨类访问,稍后您将看到,我们不再需要使用它。

A978-1-4842-0415-3_12_Fig3_HTML.jpg

图 12-3。

Place the cursor at the bottom of the class so that the four .set() and .is() methods are the last ones listed

现在,我们通过将布尔变量声明为私有,并为 InvinciBagel 外部的类和对象放置 getter 和 setter 方法以请求该数据,使 InvinciBagel 类封装得更好一些(更私有,更少公开),我们将需要修改 Bagel 类构造函数方法以接收 InvinciBagel 对象的副本,以便调用类具有 InvinciBagel 类(以及对象)必须提供的“数字上下文”。这是通过使用 Bagel()参数列表中的一个附加参数 Java this 关键字来完成的。

将上下文从 InvinciBagel 传递到 Bagel:使用此关键字

关于如何消除静态导入语句,并以合法的方式在类(对象)之间进行访问,这个难题的最后一部分是使用 Bagel()构造函数方法将 InvinciBagel 类的当前配置传递给 Bagel 类(对象),该配置保存在上下文对象引用中,this 关键字实际上表示上下文对象引用。一旦 Bagel 类接收到关于 InvinciBagel 类(对象)如何设置、它包括什么以及它做什么的上下文信息(嘿,我没有无缘无故地把这个对象引用称为“上下文”对象引用),它将能够使用。isUp()方法来“查看”Boolean up 变量的值,而不需要在除常量之外的任何地方进行任何静态声明,这正是导入静态引用应该使用的。

要升级 Bagel 类,我们需要做的第一件事是设置一个变量来保存 invincibegel 上下文对象引用信息,并修改我们当前的 Bagel()构造函数方法,以便它可以接收 invincibegel 对象引用。我们需要在类的顶部添加一个protected InvinciBagel invinciBagel;语句,创建一个 invinciBagel 引用对象(该变量将在内存中保存对该对象的引用)来保存这些信息。我进行这种受保护访问的原因是,如果我们使用 Bagel 创建任何子类,它都可以访问这个上下文对象引用信息。这个对象声明将使用下面的 Java 语句,位于 Bagel.java 类的最顶层,如图 12-4 所示:

protected``InvinciBagel``invinciBagel

接下来,让我们将 InvinciBagel 上下文对象添加到 Bagel()构造函数的参数列表的前面,因为我们不能将它放在参数列表的末尾,因为我们使用参数列表的末尾来保存我们的图像...List(或 Array,在代码中的某个点上是这两者)规范。在构造函数方法本身的内部,您将设置 invinciBagel 引用对象(使用名称 iBagel 传递到构造函数方法中)等于 InvinciBagel 变量,您已经在 Bagel.java 类的顶部声明了该变量。这将通过使用以下修改的 Bagel()构造函数方法结构来完成,可以在图 12-4 的顶部看到突出显示:

public Bagel(InvinviBagel iBagel , String SVGdata, double xLocation, double yLocation,

Image... spriteCels)  {

super(SVGdata, xLocation, yLocation, spriteCels);

invinciBagel``=``iBagel

}

正如你在图 12-4 中看到的,我们的代码是没有错误的,我们已经准备好返回到我们的 InvinciBagel.java 类中,并将 Java this 关键字添加到 Bagel()构造函数方法调用中。这样做将把 InvinciBagel 类(对象)引用对象传递给 Bagel.java 类(对象),以便我们能够使用。是()和。set()方法,而不必指定任何导入语句。您还可以删除 Bagel.java 类顶部的四个导入静态语句。如图 12-4 所示,我已经删除了这些静态导入语句。

A978-1-4842-0415-3_12_Fig4_HTML.jpg

图 12-4。

Add an InvinciBagel object variable named invinciBagel, and add the object into the constructor method

现在让我们回到 InvinciBagel.java NetBeans 的编辑选项卡,并完成两个类的连接。

修改 iBagel 实例化:将 Java this 关键字添加到方法调用中

打开你的。使用 NetBeans 左侧的+扩展图标创建 GameActors()方法结构。在要传递给 Bagel()构造函数方法调用的参数列表的“头”或前面添加一个 this 关键字。您新修改的 Java 语句应该看起来像下面的代码,它也在图 12-5 中突出显示:

iBagel = new Bagel(``this

正如你在图 12-5 中看到的,你的 Java 代码是没有错误的,这意味着你现在已经将你的 InvinciBagel.java 类(或者由它创建的对象,然而你更喜欢看它)的上下文的副本传递到了 Bagel 类中(或者更准确地说,传递到了通过使用 Bagel()构造函数方法创建的对象中)。this 对象的数字上下文结构中包含的内容超出了初学者书籍的范围,但可以说 this 关键字将传递对对象的完整结构引用。这个引用包含了所有的上下文信息,这些信息需要给被传递的对象提供足够的信息,以便能够把所有的东西都放到“数字视角”(上下文)中,关于传递这个引用对象的类,在我们的例子中,这将是 InvinciBagel 类(object ),它把关于它自己的上下文信息传递给 Bagel 类(object)。这将包括你的对象结构(变量、常量、方法等。)以及与涉及系统内存使用和线程使用的更复杂的事情相关的状态信息。可以将使用 Java this 关键字将一个对象的上下文信息传递给另一个对象看作是将它们连接在一起,这样您的接收对象就可以通过使用 this object 引用来查看发送对象。

A978-1-4842-0415-3_12_Fig5_HTML.jpg

图 12-5。

Modify the iBagel instantiation to add a Java this keyword to the Bagel() constructor method parameters

现在,InvinciBagel 类(对象)的 this 引用(上下文对象)已经通过在 Bagel()构造函数方法中使用 Java this 关键字传递给了 Bagel 类(对象),我们现在已经在这两个类之间创建了一个更加符合行业标准的链接。我们现在可以开始换面包圈了。update()方法,以便它使用新的。is()方法调用从它现在拥有的 InvinciBagel 对象引用中获取四个不同的布尔变量值(状态)。我们需要这些数据来在屏幕上移动我们的无敌角色。

用你的新无敌组合。is()方法:更新你的面包圈。update()方法

消除使用导入静态引用和静态变量的下一步是使用。isUp()、isDown()、isLeft()和 isRight()方法调用。因为我们不再使用静态变量来跨类(对象)访问,所以我们需要替换当前在 Bagel 类的 if()语句中使用的这些实际的上、下、左、右静态变量。update()方法。这些将不再工作,因为它们现在被封装在 InvinciBagel 类中,并且是私有变量,所以我们将不得不使用。isUp()、isDown()、isLeft()和 isRight() "getter "方法,礼貌地敲开 InvinciBagel 的门,并询问这些值!

我们会打电话给我们的四个。is()方法“脱离”invinciBagel 引用对象(使用点符号),我们已经在 Bagel.java 类的顶部声明并命名了 InvinciBagel。这个变量(对象引用)包含 InvinciBagel 类上下文,我们使用 Java this 关键字将它从 InvinciBagel 类发送到 Bagel 类。这意味着,如果我们在代码中说 invinciBagel.isRight(),我们的 Bagel 类(对象)现在知道这意味着:使用“this”引用对象进入 invinciBagel InvinciBagel 对象(这里只是想显得可爱),这将向 Bagel 类(对象)显示如何、在哪里到达并执行public void .isRight() {...}方法结构,这将传递 invinciBagel 对象中封装的私有 boolean right 变量。这里包含的是 Java OOP“封装”概念的演示

你的新。update()方法体将使用相同的六行 Java 代码,修改后调用。is()方法位于现有条件 if 结构的 if(condition=true)计算部分的内部。新的 Java 代码如图 12-6 所示,应该如下所示:

public void``update()

if(invinciBagel.``isRight()

if(invinciBagel.``isLeft()

if(invinciBagel.``isDown()

if(invinciBagel.``isUp()

spriteFrame.setTranslateX(iX);

spriteFrame.setTranslateY(iY);

}

正如你在图 12-6 中看到的,代码是没有错误的,你现在有了一个。update()方法,该方法从 InvinciBagel.java 类中访问布尔变量,而无需使用任何导入静态语句。

A978-1-4842-0415-3_12_Fig6_HTML.jpg

图 12-6。

Insert the invinciBagel.is() method calls inside of the if statements, where the boolean variables used to be

你可能会想,既然这是在我的 Bagel.java 类中去掉导入静态语句的好方法,为什么我不用同样的方法在我的 InvinciBagel.java 类中去掉静态 Bagel iBagel 声明,以及在 GamePlayLoop.java 类中用来访问静态 iBagel Bagel 对象的导入静态语句呢?哇,这是一个奇妙的想法,伙计们,我只是希望我想到了它!事实上,让我们现在就做吧!

移除静态 iBagel 引用:修改 Handle()方法

正如你在图 12-7 中看到的,我们仍然有相当多的 InvinciBagel 变量是使用静态关键字声明的,事实上它们并不是常量。在本章结束之前,我们将消除这些,因此只有我们的宽度和高度常数使用静态修饰符关键字。因为我们要在 GamePlayLoop()构造函数方法中使用 Java this 关键字将 InvinciBagel 对象引用传递给 GamePlayLoop 类,这意味着我们可以从 InvinciBagel 类顶部的 Bagel iBagel 对象声明语句中删除 static 关键字。这可以使用下面的变量声明来完成,如图 12-7 所示(突出显示):

Bagel iBagel;

A978-1-4842-0415-3_12_Fig7_HTML.jpg

图 12-7。

Remove the Java static modifier keyword from in front of your Bagel iBagel object declaration statement

为了确保 InvinciBagel 和 GamePlayLoop 类(对象)可以相互通信,我们需要做的下一件事是使 GamePlayLoop()构造函数方法兼容(在 InvinciBagel 的参数列表中接受 InvinciBagel 的 this context 引用对象)InvinciBagel 类的 this 对象引用,我们需要将它发送到构造函数方法调用中的 GamePlayLoop 类。因为我们目前依赖 Java 编译器为我们创建 GamePlayLoop()构造函数方法,所以我们需要为自己创建一个!正如你在第三章中所学的,如果你没有为一个类显式地创建一个构造函数方法,那么将会为你创建一个。

增强 GamePlayLoop.java:创建 GamePlayLoop()构造函数方法

让我们在 GamePlayLoop.java 课堂上执行一个类似于我们在 Bagel.java 课堂上所做的工作流程。在类的顶部添加一个protected InvinciBagel invinciBagel;语句。接下来,创建一个公共的 GamePlayLoop()构造函数方法,在参数列表中有一个名为 iBagel 的 InvinciBagel 对象。在 GamePlayLoop()构造函数方法内部,将 iBagel InvinciBagel 对象引用设置为等于受保护的 InvinciBagel invinciBagel(reference)变量,以便我们可以在 GamePlayLoop 内部使用新的 InvinciBagel InvinciBagel 对象引用。handle()方法。这将允许我们调用。使用 invinciBagel InvinciBagel 引用对象更新 iBagel Bagel 对象的()方法。GamePlayLoop 类和构造函数方法结构,以及新的。在图 12-8 中,handle()方法体(包括修改后的 invinciBagel.iBagel.update()方法调用路径(对象引用结构))显示为无错误,应该类似于下面的 Java 代码:

public class``GamePlayLoop

protected``InvinciBagel``invinciBagel

public GamePlayLoop(InvinciBagel``iBagel

invinciBagel``=``iBagel

}

@Override

public void handle(long now) {

invinciBagel .iBagel.update();.

}

}

正如你在图 12-8 中看到的,我点击了 InvinciBagel 变量(invinciBagel 对象引用),因此它被高亮显示,你可以看到它在两种方法中的用法。声明在 GamePlayLoop 类中使用,GamePlayLoop()构造函数方法中的实例使用 InvinciBagel 类 this 关键字(使用 iBagel 参数)设置,变量引用在。handle()方法访问了 Bagel 类。使用 iBagel Bagel 对象和 invinciBagel InvinciBagel 引用对象的 update()方法。Java 很高级,但是很酷。

A978-1-4842-0415-3_12_Fig8_HTML.jpg

图 12-8。

Make the same change to GamePlayLoop by adding an invinciBagel InvinciBagel variable and constructor

现在,我们已经创建了自定义的 GamePlayLoop()构造函数方法,该方法用于接收名为 iBagel 的 invinciBagel 对象引用,然后将其赋给 InvinciBagel 变量,现在是时候返回到 InvinciBagel.java 代码(NetBeans 中的编辑选项卡)了。

这个(第二个)谜题的最后一块是移除静态的百吉饼 iBagel 声明是在 GamePlayLoop()构造函数方法调用中添加 Java this 关键字。在我们这样做之后,我们的 InvinciBagel、Bagel 和 GamePlayLoop 将相互连接,而不使用任何静态变量(除了宽度和高度常量)。

在 GamePlayLoop()构造函数中使用 this:GamePlayLoop(this)

打开你的。创建 StartGameLoop()方法结构,使用 NetBeans 左侧的+ expand 图标,如图 12-9 所示。在参数区域中添加 Java this 关键字,以便再次传递 InvinciBagel 对象引用,这一次是对 GamePlayLoop()构造函数方法的调用。这将为该类提供对 InvinciBagel 类及其上下文和结构的引用,就像您对 Bagel 类所做的那样。您新修改的 Java 方法体和构造器方法调用将看起来像下面的代码,它在图 12-9 中突出显示:

private void createStartGameLoop() {

gamePlayLoop = new GamePlayLoop(``this

gamePlayLoop.start();

}

A978-1-4842-0415-3_12_Fig9_HTML.jpg

图 12-9。

Add a Java this keyword inside of the GamePlayLoop() constructor method call to provide a context object

只要我们完全封装了 InvinciBagel.java 类,让我们也将 StackPane 根变量设为私有,并去掉静态修饰符。因为我已经将名为 root 的 StackPane 对象移回到。开始()菜单。createSplashScreenNodes()方法,没有理由使用 static modifier 关键字。我正试图移除所有的静态修饰符(不是常量)并尽可能“私有化”这个类。

删除剩余的静态变量:StackPane 和 HBox

现在,我将从 InvinciBagel.java 类顶部的变量声明行开始,从我们设为私有的布尔变量(它们是公共静态的)开始,看看这些变量中有哪些我可以设为私有的,而不是包保护的静态的(目前是 StackPane 和 HBox),或者有哪些是包保护的,可以设为私有的。私有变量将该变量的数据“封装”在类(对象)本身内部;在这种情况下,这将是我们目前正在完善的 InvinciBagel.java 代码。受包保护的变量将数据封装在包内;在这种情况下,这将是不可战胜的软件包。用于声明名为 root 的 StackPane 对象(场景图的根元素)为 InvinciBagel.java 类的私有成员(仅限)的新 Java 语句将使用以下代码完成,如图 12-10 所示。

private StackPane root;

A978-1-4842-0415-3_12_Fig10_HTML.jpg

图 12-10。

Change the declaration statement for the StackPane object named root from a static modifier to private

类顶部声明中的下一个静态变量是static HBox buttonContainer;,我也将使用下面的 Java 语句将这个变量声明改为私有变量:

private HBox buttonContainer;

让我们确保。createSplashScreenNodes()方法仍然可以“看到”或引用这个 buttonContainer HBox 对象,正如你在图 12-11 中看到的,它们可以。我还点击了 NetBeans 中的 HBox 对象,这样它就向我显示了整个代码中的对象引用(这是一个非常有用的功能,您应该使用它来可视化代码中不同 Java 8 编程语句之间的对象关系)。在 NetBeans 8 的代码中,通过使用黄色字段突出显示颜色来突出显示选定的对象。

A978-1-4842-0415-3_12_Fig11_HTML.jpg

图 12-11。

Change declaration statement for HBox object named buttonContainer from a static modifier to private

我最初声明这些字段是静态的,因为我在互联网上,在几个 Java 编码网站上读到过,这是一个“优化技巧”,它允许 JVM 留出固定的内存区域,并使代码运行得更优化。我已经决定在开发过程中,首先尽可能地封装我的游戏对象,然后在必要的时候再进行优化。

只要我们完全封装了 InvinciBagel.java 类,就让其他变量也成为私有变量。我在将每个变量(在 Bagel iBagel 声明之后)私有后测试了这个游戏,它运行得很好。当我将 Bagel iBagel 设为私有时,游戏挂在白色(背景色)屏幕上,所以我将 Bagel iBagel 声明包保留为受保护的(无访问控制修饰符关键字表示包受保护的访问)。

将剩余的变量私有:完成 InvinciBagel 类的封装

我将其他八行变量声明设为私有而非包保护的工作过程是,将 private Java access control 关键字放在(简单或复合)变量声明的前面,然后使用“运行➤项目”工作过程(或者单击 NetBeans 顶部的绿色 play 图标,这样更快)来测试代码,并使用包保护的访问控制来查看所有功能(包括按钮 UI、闪屏和字符移动)是否都正常工作。如果在软件开发的后期,由于某种原因,我们需要删除 private 修饰符或者用不同的修饰符关键字替换它,我们可以这样做,但是如果可能的话,最好从一开始就从一个完全对象封装的地方开始工作,然后在以后需要时打开对象。用于封装 InvinciBagel 类中所有数据字段的 Java 代码语句如下:

private Scene scene;

private Image splashScreen, instructionLayer, legalLayer, scoresLayer;

private Image iB0, iB1, iB2, iB3, iB4, iB5, iB6, iB7, iB8;

private ImageView splashScreenBackplate, splashScreenTextArea;

private Button gameButton, helpButton, scoreButton, legalButton,;

private Insets buttonContainerPadding;

private GamePlayLoop gamePlayLoop;

private CastingDirector castDirector;

正如你在图 12-12 中所看到的,到目前为止我们所做的所有类和修改的代码都是无错的,并且我们已经准备好构建更复杂的 Java 语句,在遵循行业标准 Java 8 编程实践的技术上正确的 Java 代码之上控制我们的主要角色精灵。

A978-1-4842-0415-3_12_Fig12_HTML.jpg

图 12-12。

Make all of the variable declarations after the iBagel Bagel object declaration use private access control

接下来,我们将回到我们的 Bagel.java 课堂。update()方法,并开始将代码提炼(和组织)成更符合逻辑方法,这样我们的。update()方法变得更像是一个“顶级”方法,它调用低级方法“Java 代码块”,实现诸如按键事件处理、字符移动、屏幕边界、sprite 图像状态等等。

这将允许我们把一个更复杂的“工作量”放在百吉饼角色的身上。通过调用逻辑方法来更新()方法。setXYLocation(),。moveInvinciBagel()和。setBoundaries(),以及本书后面的内容。setImageState(),。checkForCollision()和。例如 playAudioClip()。这样,你的。update()方法调用包含逻辑代码块的其他方法。这使您的 Java 代码组织良好,并使编程逻辑更容易可视化。

组织。update()方法:。moveInvinciBagel()

因为我们将向。在本书的剩余部分,我想放置一些“方法模块化”,这与我们在第十一章中为 InvinciBagel 类添加六个新的逻辑方法结构非常相似。因为我们将在。update()方法随着游戏变得越来越复杂,合乎逻辑的是。update()方法应该包含对其他方法的调用,这些方法逻辑地组织我们将需要在每一帧上执行的任务,例如确定按下(或未按下)的键,移动 InvinciBagel 角色,查看他是否离开了屏幕(设置边界),并最终控制他的视觉状态,检测碰撞,以及应用物理效果。我想做的第一件事是将 sprite 的移动“提取”到一个. moveInvinciBagel()方法中,该方法将执行任何需要使用moveInvinciBagel(iX, iY);方法调用来实现的翻译转换。这意味着我们必须创建private void moveInvinciBagel(double x, double y){...}方法结构并放置。setTranslate()方法调用,在。使用 update()方法。moveInvinciBagel()方法调用。对 Bagel.java 类执行这些更改的基本 Java 代码如图 12-13 所示,如下所示:

@Override

public void update() {

if(invinciBagel.isRight()) { iX += vX }

if(invinciBagel.isLeft())  { iX -= vX }

if(invinciBagel.isDown())  { iY += vY }

if(invinciBagel.isUp())    { iY -= vY }

moveInvinciBagel(iX, iY);

}

private void``moveInvinciBagel``(double``x``, double``y

spriteFrame.setTranslateX(``x

spriteFrame.setTranslateY(``y

}

A978-1-4842-0415-3_12_Fig13_HTML.jpg

图 12-13。

Create a .moveInvinciBagel() method for .setTranslate() method calls, and call if from .update() method

接下来,让我们使用 ImageView 在屏幕上移动 iBagel。setTranslateX()和。setTranslateY()方法。

的进一步模块化。update()方法:。setXYLocation()

您可能认为处理布尔变量的 KeyEvent 需要在。update()方法,但是由于它们只是被简单地求值,然后递增 Bagel 对象的 iX 和 iY 属性,因此可以将其放入自己的。setXYLocation()方法,这样我们在。update()方法。这将使进一步的精灵操作和游戏开发更有组织性,也将帮助我们看到在。更新()周期。我们要做的,也如图 12-14 所示,是创建一个. setXYLocation()方法,我们将首先在我们的。update()方法,然后将四个条件 if()语句放在这个新的private void setXYLocation(){...}方法结构中。我们 Bagel.java 类的新三方法结构。update()“命令链”将利用以下 Java 代码:

public void update() {

setXYLocation();

moveInvinciBagel(``iX``,``iY

}

private void``setXYLocation()

if(invinciBagel.isRight()) {``iX

if(invinciBagel.isLeft())  {``iX

if(invinciBagel.isDown())  {``iY

if(invinciBagel.isUp())    {``iY

}

private void moveInvinciBagel(double``x``, double``y

spriteFrame.setTranslateX(``x

spriteFrame.setTranslateY(``y

}

A978-1-4842-0415-3_12_Fig14_HTML.jpg

图 12-14。

Create a .setXYLocation() method, install four if() statements inside it, and call it from .update() method

接下来,我们需要编写一些代码来防止我们的无敌角色离开屏幕,以防我们的游戏玩家没有及时改变他的方向。稍后,当我们实现评分时,我们可以添加代码,为“出界”减分,但现在我们只是要停止移动,就好像在游戏区域的边缘(舞台和场景大小的边界)有一个无形的障碍。

设置屏幕边界:。setBoundaries()方法

对于 InvinciBagel 游戏来说,下一件最重要的事情是通过在。setXYLocation()方法,该方法计算箭头(或 ASDW)按键组合,并相应地递增 iX 和 iY Bagel 对象属性。moveInvinciBagel()方法,该方法实际执行移动。通过放置。setBoundaries()方法在调用 sprite 移动之前,我们可以在实际调用 move 函数(方法)之前确保 sprite 没有离开屏幕(如果他离开了屏幕,就把他移回到屏幕上)。编写这段代码的第一步是以像素为单位定义 sprite 的大小,这样我们就可以计算它以及我们的宽度和高度阶段大小常数,以确定边界变量值,我们将需要这些值来检查我们的 iX 和 iY sprite 在中的位置。setBoundaries()方法及其条件 if()语句结构。正如你在图 12-15 中看到的,我通过使用下面两行 Java 代码,在 Bagel.java 类的顶部定义了这些精灵像素大小常量声明:

protected static final double``SPRITE_PIXELS_X

protected static final double``SPRITE_PIXELS_Y

A978-1-4842-0415-3_12_Fig15_HTML.jpg

图 12-15。

Declare protected static final double SPRITE_PIXELS_X and SPRITE_PIXELS_Y constants at the top of class

接下来,我们需要使用 InvinciBagel 类中的宽度和高度常量以及我们刚刚在该类顶部定义的 SPRITE_PIXELS_X 和 SPRITE_PIXELS_Y 常量来计算四个屏幕边界值。您可能已经注意到,从我们的 0,0 初始 X,Y Bagel 对象位置坐标将 sprite 放在屏幕中央,JavaFX 使用 X 轴和 Y 轴居中的屏幕寻址范例。这意味着有四个象限,负值(反映正值)向左上方移动,正值向右下方移动。我们实际上可以稍后使用这个范例来快速确定角色在屏幕的哪个象限。因此,我们计算边界的方法是,取屏幕宽度的一半,减去子画面宽度的一半,以找到右边界(正)值,并简单地取左边界的负值。类似的计算适用于顶部和底部边界值限制,为此,我们将取屏幕高度的一半,减去子画面高度的一半,以找到底部(正)边界值,并简单地取其负值作为顶部边界限制值。这些计算的 Java 代码应该如下所示:

protected static final double``rightBoundary

protected static final double``leftBoundary

protected static final double``bottomBoundary

protected static final double``topBoundary

正如您在图 12-16 中所看到的,NetBeans 在 InvinciBagel 类中看到常量时遇到了问题。

A978-1-4842-0415-3_12_Fig16_HTML.jpg

图 12-16。

Hold a left arrow (or A) and up arrow (or W) key down at the same time, and move the Actor diagonally

将鼠标悬停在 NetBeans 中宽度常量下方红色波浪突出显示的错误上,并选择import static invincibagel.InvinciBagel.WIDTH;选项,以便 NetBeans 为您编写此导入语句。“正确地”利用导入静态(或者静态导入,如果您愿意的话)的行业标准方法是用于常量的导入和使用,因此我们在这里与 Java 编程标准过程保持高度一致。对高度常数引用下面红色突出显示的错误再次执行相同的工作过程,然后添加。setBoundaries()方法调用。setXYLocation()和。moveInvinciBagel()方法调用。update()方法。这可以通过在。update()方法,如图 12-17 所示:

setBoundaries();

正如你在图 12-17 中看到的,这将在方法调用下产生一个错误,直到我们对方法进行编码。

A978-1-4842-0415-3_12_Fig17_HTML.jpg

图 12-17。

Create rightBoundary, leftBoundary, bottomBoundary and topBoundary constants at the top of the class

在。setXYLocation()方法结构,以便您的方法与我们从您的。update()方法。接下来,您将放置四个条件 if()结构,每个屏幕边界一个,从 X 轴相关的左右屏幕边界开始,然后是 Y 轴相关的上下屏幕边界。第一个 if 语句需要查看 rightBoundary 值,并将当前的 iX 位置与该值进行比较。如果 iX 值大于或等于 rightBoundary 值限制,则需要将 iX 值设置为 rightBoundary 值。这将使无敌舰队锁定在边界的正确位置。相反的逻辑也适用于屏幕的左侧;如果 iX 值小于或等于 rightBoundary 值限制,那么您需要将 iX 值设置为等于 leftBoundary 值。

第三个 if 语句需要查看 bottomBoundary 值,并将当前 iY 位置与该值进行比较。如果 iY 值大于或等于 bottomBoundary 值限制,则您需要将 iY 值设置为 bottomBoundary 值。这将使你的无敌手锁定在屏幕边界的底部。这个逻辑的反过来也适用于屏幕顶部;如果 iY 值小于或等于 topBoundary 值限制,则需要将 iY 值设置为等于 topBoundary 值。控件的 Java 代码。包含四个 if()语句的 setBoundaries()方法如图 12-18 所示,应该如下所示:

private void``setBoundaries()

if (iX``>=

if (iX``<=

if (iY``>=

if (iY``<=

}

A978-1-4842-0415-3_12_Fig18_HTML.jpg

图 12-18。

Create a private void .setBoundaries() method and four if() statements to ascertain and set boundaries

接下来,让我们测试所有这些代码,看看它是否做了我们认为逻辑上应该做的事情!代码组织得很好,也很符合逻辑,所以我看不出它有任何问题,但是在 NetBeans 中测试它是确定的唯一真正的方法!让我们接下来做那件事。这越来越令人兴奋了!

测试 invincibagel 精灵边界:运行➤项目

现在是时候使用 NetBeans 运行➤项目工作流程并测试。setBoundaries()方法,该方法现在在。setXYLocation()方法,但在。moveInvinciBagel()方法。因此,现在的逻辑进展是检查按键并基于此设置 X 和 Y 位置,然后检查以确保您没有越过任何边界,然后定位精灵。

正如你在图 12-19 中所看到的,InvinciBagel 角色现在停在屏幕的所有四个边缘。在左侧和右侧,他在离屏幕边缘很近的地方停下来,因为精灵在 ImageView 区域的中心,但是一旦我们让他跑起来,我们将在下一章讲述如何制作角色运动的动画,这将看起来更靠近屏幕的边缘。我们总是可以选择在 Bagel.java 类的顶部调整我们的 leftBoundary 和 rightBoundary 变量算法,这允许我们在以后继续改进代码时“调整”边界限制值。

A978-1-4842-0415-3_12_Fig19_HTML.jpg

图 12-19。

Testing the InvinciBagel character movement; shown as stopping at the top and bottom boundary limits

现在,我们已经组织和封装了代码,使精灵运动正常工作,并设置了屏幕边缘的边界,我们可以开始考虑实现不同的精灵图像状态,以便当与按键运动结合时,我们可以开始创建一个更真实的 InvinciBagel 角色动作图!

摘要

在第十二章中,我们尽可能地私有化了我们的主 InvinciBagel.java 类,并删除了所有与常量(宽度和高度)不相关的静态修饰符关键字。首先,我们删除了公共的静态布尔变量,并使它们成为 InvinciBagel 类的私有变量,然后创建 getter 和 setter 方法,以允许 Bagel 类使用这些变量。is()方法调用。我们还必须使用 Java this 关键字将 InvinciBagel 对象引用传递给 Bagel()构造函数参数列表前面的 Bagel 对象。我们对静态 Bagel iBagel 对象声明做了同样的更改,删除了 static modifier 关键字,并使用 Java this 关键字传递 InvinciBagel 对象上下文,这次是在 GamePlayLoop()构造函数方法调用内部。要做到这一点,我们必须创建自己的自定义 GamePlayLoop()构造函数方法,而不是使用编译器(JVM)创建的方法,如果我们没有特别提供的话。

之后,我们删除了 StackPane 和 HBox 对象上的其他两个静态修饰符关键字,并将所有其他变量设为私有,至少现在是这样,以便为 InvinciBagel (primary)游戏类提供最高级别的封装。

接下来,我们重组了 Bagel.java 类中与。update()方法。我们创建了特定的方法来轮询按键值并设置对象的 iX 和 iY 属性,我们称之为。setXYLocation()方法,以及创建。moveInvinciBagel()方法来调用。setTranslate()方法。

最后,我们创建了新的。Bagel.java 类中的 setBoundaries()方法,该方法在。setXYLocation()方法,但在。moveInvinciBagel()方法,该方法确保我们的主要角色始终停留在屏幕上。

在下一章中,我们将看一下关于使用 List ArrayList 对象使游戏精灵在屏幕上移动的高级概念,以便我们在进入高级主题如数字音频、碰撞检测和物理模拟之前获得更真实的精灵动画。

十三、动画化您的演员形象状态:基于关键事件处理设置图像状态

现在我们已经将你的 Java 代码组织成 Bagel.java 类中的逻辑方法,并且确保我们所有的 Java 代码都符合第十一章和第十二章中的标准,是时候进入一些更复杂的代码结构了,当用户移动角色时,这些代码结构将在屏幕上激活我们的无敌角色。例如,如果角色正在正东或正西行进(仅使用左键或右键,沿直线行进),他应该在跑(在 imageStates(1)和 imageStates(2)列表<图像>元素之间交替)。如果向上键也被按下,他应该向左键或右键的方向跳跃,如果向下键被按下,他应该准备向左键或右键的方向降落。

我们还需要实现 Actor 类的 isFlipH 属性,以便角色根据他行进的方向面向正确的方向。我们将使用 JavaFX 功能来“翻转”或“镜像”任何围绕中心 Y 轴(isFlipH)或中心 X 轴(isFlipV)的图像,而不是使用另一个图像。一旦精灵动画状态与您在上一章中放置的运动代码相结合,您会惊讶于这个角色将变得多么逼真,并且我们仍然只使用了九个精灵状态图像(到目前为止,我们的新媒体资产使用的总数据量不到 84KB)。

在本章中,我们将只使用 Java 代码,并且只使用 Java FX animation timer(GamePlayLoop)超类来制作所有的角色动画。这样,我们通过仅使用 javafx.animation 包中使用最少内存开销的类来访问 脉冲事件计时引擎,从而优化了 JavaFX 脉冲引擎在游戏中的使用。AnimationTimer 类是最简单的类,没有类变量,只有一个要实现的. handle()方法,但它也是最强大的,因为它允许您编写自己的所有代码。

这种方法允许我们编写自定义代码,根据按下的键和移动来激活角色,而不是根据时间轴上的关键帧(及其键值)来触发预定义的时间轴对象。我在游戏引擎方面保持简单,并把所有的复杂性放到我们定制的游戏代码中。这将为我们以后省去很多麻烦,尝试“同步”基于关键帧和基于时间轴的线性动画,这将我们带入一个基于线性时间轴的范例,如 Flash 使用的。从 Java 编码的角度来看,这种 100%的 Java 8 编码方法当然更困难,但它给了我们更大的能力,以实现事件处理、屏幕移动、角色动画、物理和碰撞检测的无缝集成。设置大量预构建的 JavaFX Animation 子类可能最终会得到相同的结果,但代码会不那么优雅,并且可能更难以在其上构建游戏的未来版本。

我们所有的角色状态动画都将使用. setImageState()方法创建,该方法将从。update()方法,所以,我们将继续在我们的角色的运动和动画中进行组织。

无敌动画。setImageState()方法

在本章中,我们将创建一个(相当复杂的)方法,称为。setImageState(),它将根据在任何给定时刻按下的键来设置 InvinciBagel 角色的动画或运动状态。方法之前调用. setImageState()方法。中的 moveInvinciBagel()方法。update()方法将用于将角色的九个图像单元(帧)之一与角色的运动相结合。这将创造动画的幻觉,并且将在不使用任何动画时间线的情况下实现这一点。从游戏优化的角度来看,这意味着运行我们的 GamePlayLoop 的 JavaFX 引擎可以将其资源集中在单个动画(脉冲)引擎上。正如你在图 13-1 中看到的,我们需要添加一个.setImageState();方法调用,在。方法之前的 update()方法。moveInvinciBagel()方法调用并在。setBoundaries()方法调用。这样做之后,您必须创建一个空方法来消除错误突出显示。Java 代码如下所示:

private void``setImageState()

如图 13-1 所示,这个空的代码框架不会在代码中生成任何红色错误或黄色警告高亮。我们目前非常有条理,通过在这个 Bagel 类中仅使用四个方法调用,完成了所有的 KeyEvent 处理、边界检测、sprite 动画和 sprite 移动。update()方法。

A978-1-4842-0415-3_13_Fig1_HTML.jpg

图 13-1。

Create the private void setImageState( ) method; place a setImageState( ) method call in .update( ) method

我们要检查的第一件事是没有移动:也就是说,没有按下任何键,这样我们就可以正确地实现我们在上一章开发精灵移动算法时使用的“等待”InvinciBagel 状态。

InvinciBagel 等待状态:如果没有按键,则设置 imageState(0)

我们放置的第一个条件 if()语句将是默认或“无按键”状态,即 sprite zero,它显示不耐烦地等待移动和动画的 InvinciBagel。我们想要在圆括号内的 if 求值区域内寻找的是上、下、左、右变量的假值,它们都发生在同一时刻。到目前为止,我们一直在寻找一个真实的值,使用。isUp(),。isDown(),。isLeft()和。isRight()方法调用 invinciBagel 对象引用。在这种情况下,我们希望寻找一个错误的值。

要做到这一点,我们需要使用 Java 一元感叹号!操作员。这反转了布尔值,所以在我们的例子中,来自这些方法调用之一的 false 值将由一个!invinciBagel.isUp()构造来表示。为了查明是否有多个值同时为假,我们需要实现 Java 条件 AND 运算符,它使用两个连续的&字符,就像这个&&so;在这种情况下,我们将使用其中的三个& &条件 AND 运算符,来告诉 Java 编译器我们希望 Right 和 Left 以及 Down 和 Up 都为 false。所有这些逻辑都将进入 if()求值区域(在括号内)。在大括号内,如果满足 if()求值区域(如果 up、down、left 和 right 都为 false)的话,我们将使用。setImage()方法调用。在方法调用内部,我们将使用。对 imageStates List < Image >对象调用 get()方法,从 List < Image >对象中获取第一个图像引用 imageStates(0)。这是无敌的“等待”精灵细胞,显示他不耐烦地等待被移动(动画)。该构造的 Java 代码看起来像下面的 Java 编程结构(为了更容易阅读和学习,我对其进行了缩进):

if``(``!

! invinciBagel.isLeft()  &&

! invinciBagel.isDown()  &&

! invinciBagel.isUp()      ) {

spriteFrame.``setImage``(imageStates.``get(0)

如图 13-2 所示,第一个 if()语句表示“如果没有按下任何箭头键”,是无错误的。如果您使用“运行➤项目”工作流程,并测试此代码,您将获得与上一章相同的结果!为了查看这段代码是否有效,我们必须首先让 InvinciBagel 运行,这样当我们使用箭头键停止移动他时,我们会得到这种不耐烦的“等待”状态,这实际上使它更有效(有趣),当这种等待是在本章中我们将要实现的所有动画运动的上下文中时!

A978-1-4842-0415-3_13_Fig2_HTML.jpg

图 13-2。

Add a conditional if() statement that checks for no movement, and sets the wait sprite image state (zero)

接下来,让我们开始实现一些其他的角色图像单元,尝试让我们的角色动画化!

InvinciBagel 运行状态:如果按键设置图像状态(1 & 2)

正如你在第八章中看到的,我将尝试只用两个精灵元素实现角色的动画运行状态,imageState(1)和 imageState(2)。正如你将要看到的,无论是从图像资产的角度,还是从编码的角度,这都是你能得到的最优化的了。考虑到您不能使用单个图像状态来制作任何东西(如运行周期)的动画,这一点尤其正确。也就是说,我们将在这一章中通过使用单个 cel,设计良好的精灵状态,结合我们在第十二章中放置的精灵运动代码,来创建许多非常真实的动画。if(invinciBagel.isRight())if(invinciBagel.isLeft())语句结构最初会非常简单明了,但是随着我们在本章中增加细化功能,它们会变得更加复杂。我们将首先为这些奠定基础,然后添加 up 和 down 条件 if()语句,然后我们将细化左右键事件处理。在用于左右箭头键(以及 A 和 D 键移动)的 if()构造内部,我们将使用我们在第一个(不耐烦等待状态)if()构造中使用的相同的链式方法调用,只是这里我们将从 List < Image >对象调用 imageStates(1)或 imageStates(2) sprite cels,而不是使用 imageStates(0) sprite cel。如果按下右键或左键(true ),将 sprite 图像状态更改为状态 1 或 2 的 Java 代码应如下所示:

if(invinciBagel.``isRight

spriteFrame.``setImage``(imageStates.``get(1)

}

if(invinciBagel.``isLeft

spriteFrame.``setImage``(imageStates.``get(2)

}

如图 13-3 所示,Java 代码没有错误,我们已经准备好使用运行>项目工作流程测试这种初步运行模式。如果你快速连续按下左右箭头键,你会看到无敌跑!

A978-1-4842-0415-3_13_Fig3_HTML.jpg

图 13-3。

Add conditional if() statements that check for left/right movement, and sets the run sprite image states

因为这不是我们想让我们的游戏玩家让 InvinciBagel 跑起来的方式(因为它的本质是停止他在屏幕上的移动,因为它只是简单的跛脚),让我们快速地将你的上下键支持到位,这样我们就可以回来工作,在左右键上工作,这样我们就可以让运行周期工作了!

InvinciBagel 飞行状态:如果按键设置图像状态(3 & 4)

if(invinciBagel.isDown())if(invinciBagel.isUp())条件 if()结构与左右键结构相同,除了它们调用 imageStates(3)和 imageStates(4)列表元素,以允许 InvinciBagel 角色“进来着陆”(cel 3)和“起飞飞行”(cel 4)。随着我们在本章中添加更多的图像状态,并将这个 cel 动画代码与我们的运动和边界代码相结合,您将会越来越有兴趣测试本章的编码结果!如果您想走程序员的捷径,请复制并粘贴。isRight()和。isLeft()构造,只需更改。得到(1)和。开始做某事。得到(3)和。得到(4)。如图 13-4 所示,代码目前非常紧凑,组织良好;结构化;和逻辑;在仅仅六行 Java 代码中,我们已经实现了九个图像状态中的一半以上!当然,我们仍然需要添加细化代码,以实现方向改变的精灵镜像和运行周期定时细化。控件的 Java 代码。isUp()和。isDown()方法结构应该如下所示:

if(invinciBagel.isDown()) {

spriteFrame.``setImage``(imageStates.``get(3)

}

if(invinciBagel.isUp()) {

spriteFrame.``setImage``(imageStates.``get(4)

}

正如你在图 13-4 中看到的,我们的代码是没有错误的,我们准备给左右箭头键事件处理代码增加几层复杂性,因为这两个键定义了 InvinciBagel 行进的方向(东和西)。因此,这两个条件 if()语句结构尤其需要变得更加复杂,因为向东(向右)行进将使用原始(isFlipH = false)子画面,而向西(向左)行进将利用每个子画面的镜像版本(isFlipH = true),围绕中心 Y 轴“翻转”图像资产。

A978-1-4842-0415-3_13_Fig4_HTML.jpg

图 13-4。

Add conditional if() statements that check for up/down movement and sets jump/land sprite image state

镜像精灵:将你的图像资产从 9 增加到 36

现在让我们回到现有的。isLeft()和。isRight()条件求值语句,在本章的课程中,这些语句将变得相当“健壮”(复杂),让我们添加恶意镜像功能。JavaFX API 将其镜像功能“隐藏”在 ImageView 类的。setScaleX()方法调用。虽然我们不打算缩放我们的图像资产,因为这样做会导致原始 PNG32 图像资产中的伪像,但有一个鲜为人知的技巧,您可以将-1(负 100%缩放因子)值传递给. setScaleX()方法,以围绕 Y 轴翻转或镜像图像资产(或进入)。setScaleY()方法,绕 X 轴翻转或镜像)。显然,我们还需要在另一个条件 if()结构中“撤销”这一操作,方法是将 1(正 100%缩放因子)传递到同一个方法调用中,这(通常)没有太大意义,因为我们的图像比例已经是 100%(未缩放),但是考虑到-1 缩放翻转因子可能已经预先设置,这就是我们如何确保镜像被禁用,并且我们再次将原始 sprite 图像资产用于该特定状态。您新升级的实现 sprite 镜像的 Java 语句现在应该看起来像下面的代码,它也在图 13-5 中突出显示:

if(invinciBagel.``isRight

spriteFrame.``setImage``(imageStates.``get(1)

spriteFrame.``setScaleX

}

if(invinciBagel.``isLeft

spriteFrame.``setImage``(imageStates.``get(1)

spriteFrame.``setScaleX``(``-1

}

正如你在图 13-5 中看到的,你的 Java 代码仍然没有错误。有趣的是,我们在 spriteFrame ImageView 对象上调用 sprite mirroring 方法,而不是在这个 ImageView 内部的图像资产上。这相当重要,因为这意味着我们可以在。isRight()和。isLeft()来翻转 ImageView 中显示的任何精灵状态(图像)!这是高度优化的编程!

A978-1-4842-0415-3_13_Fig5_HTML.jpg

图 13-5。

Add a .setScaleX() method call to the .isRight() and .isLeft() evaluations to flip the sprite around the Y axis

现在,我们的 sprite 镜像代码已经就绪,我们需要处理运行周期的问题,这是通过使用我们的条件 if()处理交替完成 imageStates(1)和 imageStates(2)来实现的。

动画您的运行周期:创建一个嵌套的 If-Else 结构

本章中精灵动画的下一步是为我们的角色制作一个运行循环的动画,这是我们通常用 JavaFX 关键帧和时间轴类来做的,但是我们在这里只用十几行代码就完成了。因为我们已经使用了 AnimationTimer 类,所以这是最佳的方法,并且只需要使用一个布尔变量就可以实现。因为我们的运行周期有两个 cel,所以我们可以使用这个布尔变量并在 true 和 false 之间改变它的值。如果这个我们称之为 animator 的布尔值为 false,我们将在 imageStates(1)中显示 cel,这是我们开始奔跑的位置(脚着地)。如果 animator 为 true,我们将在 imageStates(2)中显示 cel,这是我们全力奔跑的位置(双脚全力运动)。在 Bagel.java 类的顶部创建布尔动画变量。NetBeans 给了我一个“变量未初始化”的警告,所以我显式地将它设置为默认的布尔值 false,因为我希望运行周期总是从一只脚离开地面开始。变量声明语句应该如下所示,显示在图 13-6 的最上方:

boolean``animator``=``false

因为我们希望运行周期总是从 imageStates(1)脚离开地面开始,所以我们将在“没有按下箭头键”代码语句中添加一行animator=false;代码。该语句现在将做两件不同的事情:设置 imageStates(0)等待 sprite 图像引用;并确保 animator 变量被初始化为假值,这确保运行周期从脚踏实地开始,就像在现实生活中一样。“没有按下箭头键”条件 if()结构的新 Java 代码应该如下所示:

if(``!``invinciBagel.isRight()

!``invinciBagel.isLeft()

!``invinciBagel.isDown()

! invinciBagel.isUp()       )  {

spriteFrame.setImage(imageStates.``get(0)

animator=false; }

控件的 Java 代码。isRight()和。isLeft()条件 if()结构现在将变得更加健壮,因为我们将不得不在确定右箭头键是否被按下的语句中嵌套另一个 if-else 条件语句。如果 right 为 true,它会将 ScaleX 属性设置为 1(非镜像),然后添加一个条件 If()语句来查看 animator 布尔变量的值是否为 false。如果 animator 为 false,我们使用便捷的方法链来获取 imageStates(1),并将该图像资产设置为 spriteFrame ImageView 将使用的 cel。之后,我们需要将 animator 变量设置为真值,以便稍后可以设置 imageStates(2)完整运行的精灵图像。如果 animator 为真,那么该结构的 else-if 部分将确认 animator 为真,如果为真,则再次使用spriteFrame.setImage(imageStates.get(2));方法链获取 imageStates(2)并将 animator 设置为假。该语句的新代码如图 13-6 所示,应该如下所示:

if(invinciBagel.``isRight()

spriteFrame.setScaleX(``1

if(``!animator

spriteFrame.setImage(imageStates.``get(1)

animator=true;

}   else if(``animator

spriteFrame.setImage(imageStates.``get(2)

animator=false;

}

}

值得注意的是,在这种情况下你可以去掉else if(animator),只使用一个没有 if(动画)部分的 else。然而,通过在 if-else-if-else 结构中嵌套更多的代码,我们将使右(左)按键结构变得更加复杂,所以为了可读性以及未来的代码开发目的,我将保持这种方式。如图 13-6 所示,代码是无错的。

A978-1-4842-0415-3_13_Fig6_HTML.jpg

图 13-6。

Nest an if-else logic structure alternating between sprite cels 1 and 2 using the boolean animator variable

现在,您可以将完全相同的代码结构实现到您的。isLeft()条件 if()结构。由于玩家将使用左键或右键(但不是两者一起使用,至少直到我们在游戏开发的后期开始添加那些隐藏的“复活节彩蛋”功能),我们可以在两个。isRight()和。isLeft()条件 if()构造,允许我们在这里做一点内存使用优化。如您所见,唯一的区别是 ScaleX 属性被设置为镜像 sprite 图像(使用-1 值),因此,the if(invinciBagel.isLeft())条件 if()结构的 Java 代码应该如下所示:

if(invinciBagel.``isLeft()

spriteFrame.setScaleX(``-1

if(``!animator

spriteFrame.``setImage``(imageStates.``get(1)

animator=true;

}   else if(``animator

spriteFrame.``setImage``(imageStates.``get(2)

animator=false;

}

}

正如你在图 13-7 中看到的,这段 Java 代码没有错误,你已经准备好使用你的 Run >项目工作流程,并测试 InvinciBagel 运行周期,这样你就可以看到你的超级英雄能跑多快(或者 JavaFX 中的 脉冲引擎能跑多快,使用你的 GamePlayLoop 类的 AnimationTimer 超类)。当你测试你的 Java 代码时,你会发现你的超级英雄角色跑得比人类可能的速度快得多(也比一个百吉饼跑得快得多);事实上,精灵动画细胞交替如此之快,它看起来像一个模糊的运行动画!

A978-1-4842-0415-3_13_Fig7_HTML.jpg

图 13-7。

Duplicate the nested if-else statement in .isLeft() structure, so InvinciBagel character runs both directions

接下来,我们需要添加一些 Java 代码来控制 InvinciBagel 精灵的运行速度。我们将使用两个整数变量来实现这一点:一个用作帧计数器,另一个保存运行速度值,稍后我们可以根据 vX(沿 X 轴的速度)变量来更改该值,以获得运行周期动画速度和精灵在屏幕上移动速度之间的真实匹配。

控制运行周期速度:设置动画节流程序逻辑

为了能够“节流”我们的运行周期 sprite 动画以实现不同的速度,我们需要引入一个名为 framecounter 的“计数器”变量,它将在我们将 false (sprite cel 1) animator 值更改为 true (sprite cel 2)之前计数到一定数量的帧。我们还将使用 runningspeed 变量,这样我们的动画速度就不会被硬编码,而是存在于一个变量中,我们可以在以后更改它。这允许我们对运行周期动画的速度(真实性)进行微调控制。在 Bagel.java 类的顶部声明这两个整数(int)变量,并将 framecounter 变量初始化为零,并将 runningspeed 变量的值设置为 6。既然都是假的(!animator)和 true (animator)二级 if()结构将使用这个“数到 6”变量,我们所做的数学运算将等于 6+6=12,分为 60FPS 脉冲计时循环,这意味着我们将未节流的动画减慢 500%(五倍,因为 60/12=5)。Bagel 类顶部的变量声明语句应该类似于下面的 Java 代码,也显示在图 13-8 的中间:

int``framecounter``=``0

int``runningspeed``=``6

正如你在图 13-8 中看到的,我点击了 framecounter 变量,所以它被高亮显示,你可以看到它在初始化语句中的使用,我们需要把它放入“没有按下箭头键”条件 if()结构中,就像我们处理 animator 变量一样。该 if()结构的代码如图 13-8 所示,如下所示:

if(``!``invinciBagel.isRight()

!``invinciBagel.isLeft()

!``invinciBagel.isDown()

! invinciBagel.isUp()       )  {

spriteFrame.setImage(imageStates.``get(0)

animator=false;

framecounter=0;

}

就像我们希望 animator 布尔变量在所有箭头键未被使用时被重置为 false 值一样,我们也希望 framecounter 整数变量在所有箭头键未被使用时(即,同时处于释放状态)被重置为零值。正如你所看到的,我们不仅在这个条件语句中设置了 sprite 的等待图像状态,我们还用它来重置我们的其他变量。

A978-1-4842-0415-3_13_Fig8_HTML.jpg

图 13-8。

Add int variables at top of the class for framecounter and runningspeed and set to zero in no movement if

现在我们准备好了。isRight()和。isLeft()条件 if 结构甚至更复杂,因为我们将嵌套我们的 Java 逻辑三嵌套条件 if 结构更深,以允许我们将 framecounter 和 runningspeed 整数变量合并到我们的条件 if()结构中。这将使我们的动画代码在将 animator false 值更改为 true 之前“等待”六个脉冲事件周期,然后在将其改回 false 之前再等待六个脉冲事件周期。

这是相当复杂的 Java 代码,至少对于一个初学 Java 8 的人来说是这样,但是游戏编程本来就是一项复杂的任务,所以让我们继续学习如何为我们的运行周期动画编写这种节流机制的代码。

我想在本书中向您介绍一些高级主题,而这一个(实现速度节流)是我们无法避免的,因为这种使用简单布尔交替图像状态逻辑结构的运行速度在我们的游戏中是不可行的,考虑到 JavaFX 脉冲事件计时引擎令人难以置信的速度及其“控制台游戏”60FPS 的帧速率,这使得我们的 InvinciBagel sprite 运行周期看起来不仅不现实,而且看起来很痛苦!

也就是说,至少这是我们的 Java 8 编码在本章中将要达到的复杂程度,所以请抓紧时间,在下一节中,当我们创建 16 行 Java 代码(嵌套的条件 if()结构)时,请享受这段旅程。这将是大量的工作,但由此产生的运行周期油门控制将是非常值得的努力!

编写运行周期节流器:三重嵌套 If-Else 结构

我们要对布尔 animator if()结构进行的修改是在animator=true;语句的“周围”放置一个if(framecounter >= runningspeed){...}结构,这样 animator 在六个脉冲事件循环发生之前不会变为真。如果 framecounter 等于(或由于某种原因大于)六,animator 变为 true,framecounter 重置为零,并使用 imageStates(2)。如果 framecounter 小于 6,语句的 else 部分使用framecounter+=1;语句将 framecounter 递增 1。在这个结构的两个部分中,我们将 framecounter 代码包装在两个if(animator)代码周围,如图 13-9 所示:

if(invinciBagel.``isRight()

spriteFrame.setScaleX(``1

if(``!animator

spriteFrame.setImage(imageStates.get(``1

if(``framecounter >= runningspeed

animator=``true

framecounter=0;

} else {``framecounter+=1;

}   else if(``animator

spriteFrame.setImage(imageStates.get(``2

if(``framecounter >= runningspeed

animator=``false

framecounter=0;

} else {``framecounter+=1;

}

}

A978-1-4842-0415-3_13_Fig9_HTML.jpg

图 13-9。

Add a third level of if-else nesting that prevents cels from alternating too quickly by using a framecounter

现在对条件 if()结构进行同样的修改。唯一的区别是 ScaleX 属性为-1(镜像精灵图像),如图 13-10 中的以下代码所示:

if(invinciBagel.``isLeft()

spriteFrame.setScaleX(``-1

if(``!animator

spriteFrame.``setImage``(imageStates.get(``1

if(``framecounter >= runningspeed

animator= true;

framecounter=0;

} else {``framecounter+=1;

}   else if(``animator

spriteFrame.``setImage``(imageStates.get(``2

if(``framecounter >= runningspeed

animator= false;

framecounter=0;

} else {``framecounter+=1;

}

}

A978-1-4842-0415-3_13_Fig10_HTML.jpg

图 13-10。

Duplicate the if-else structure from the isRight() structure into the isLeft() structure using framecounter

现在又到了使用运行➤项目工作流程和测试运行周期代码的时候了,现在代码展示了一个平滑、均匀、真实的运行周期。您可以使用值 7 或 8 将跑步速度微调为稍慢,或使用值 4 或 5 将跑步速度微调为稍快。当我们为跑步增加更快的 vX 值(比如 vX = 2)时,我们可以将跑步速度设置为 3 或 4 来匹配它,使游戏更加真实。

我在本章测试代码时注意到的另外一件事,我想在这里纠正的是用于着陆的 sprite cel 图像。我一直在使用 imageStates(3),它是“着陆”的图像,实际上应该更好地用于冲突情况。让我们保存这个 sprite cel 图像状态,以便稍后在碰撞检测代码开发阶段使用,以表示与曲面的碰撞(刚刚着陆或在碰撞图像上)。我想在按下向下箭头键时使用的图像实际上是 imageStates(6),即“准备着陆”图像。修改后的 Java 代码将如下所示,并在图 13-11 中突出显示:

if(invinciBagel.isDown()) {

spriteFrame.``setImage``(imageStates.``get(6)

}

让我们确保我们的 InvinciBagel 精灵动画的专业性随着本章的每一节都在提高,并使用运行➤项目工作流程和测试所有四个箭头键。一定要用左右键(左上、左下、右上和右下)来测试上下键,以真正了解到目前为止您所编写的 Java 代码的能力。我们还有很长的路要走,但已经取得了令人印象深刻的成绩!

A978-1-4842-0415-3_13_Fig11_HTML.jpg

图 13-11。

Change .isDown() sprite cel to imageStates(6) using the imageStates.get(6) method call to use correct cel

优化运行周期处理:关闭飞行和着陆状态的处理

我想做的下一件事在概念上也是先进的,但使用的代码要少得多。作为一个优化迷,我担心的是当玩家使用上下箭头键时,animator、framecounter 和 runningspeed 变量以及使用它们的编程逻辑可能会占用内存,所以我想在条件 if()结构的顶部放一个语句,将。setScaleX() sprite 镜像代码保持不变,但如果使用了 up 和 down 键,则会关闭其余的处理逻辑。排除运行周期逻辑的 Java 代码应该基于上下箭头键变量,它们都显示为 false,表明玩家只使用了左键或右键。这个排除逻辑如图 13-12 所示,看起来像下面的 Java 代码(以粗体显示):

if(invinciBagel.``isRight()

spriteFrame.setScaleX(``1

if(``!animator``&& (``!invinciBagel.isDown()``&&``!invinciBagel.isUp()

spriteFrame.setImage(imageStates.get(1));

if(framecounter >= runningspeed) {

animator=true;

framecounter=0;

} else { framecounter+=1; }

}   else if(animator) {

spriteFrame.setImage(imageStates.get(2));

if(framecounter >= runningspeed) {

animator=false;

framecounter=0;

} else { framecounter+=1; }

}

}

A978-1-4842-0415-3_13_Fig12_HTML.jpg

图 13-12。

Add if() statement logic to exclude processing of nested if-else hierarchy if down or up keys being pressed

我这样做的原因是因为当向上或向下键被按下时,跳跃(或飞行)或准备着陆精灵 cel 图像被显示。出于这个原因,优化指示我需要关闭所有“sprite cel 1 和 sprite cel 2 之间的交替”处理代码。我想这样做,以便当 imageStates(1)和 imageStates(2)甚至没有在 spriteFrame ImageView“容器”中使用(显示)时,这种持续的处理不会在内存和线程(CPU)中进行

为了实现这个优化目标,我在第二个嵌套 if()循环的外部添加了一个新的评估级别。这一层包含了使用 animator、framecounter 和 runningspeed 变量以及相关处理的所有 sprite cel 变化(1 到 2 以及相反)。这个新的更复杂的评估语句保证了如果满足新的条件,所有的运行周期处理逻辑都不会被执行。

这个新条件具体说的是,如果 animator 变量为假,并且 up 和 down 键都没有被使用(为假),则处理剩余的逻辑,在两个 sprite 图像状态之间切换(以及在每次切换之前等待一定数量的脉冲事件)。这意味着,如果按下向上或向下键中的任何一个(从而显示飞行或着陆精灵 cel 图像状态),则根本不会处理超过该点的任何编程逻辑,从而为我们将添加到游戏中的许多其他事情节省 CPU 处理周期,这些事情将需要将这种“节省”的处理开销用于其他与游戏相关的逻辑。

你可能想知道为什么我没有在这个编程结构的 else if(animator)部分添加同样的扩展条件。原因是循环的这一部分永远不会被执行,除非第一部分得到处理,我们用这个新语句排除了第一部分。这是因为在这个循环的第一部分,我们设置了animator=true;,如果按下了向上或向下键,这将永远不会发生(现在我们已经添加了扩展条件)。

真正酷的是,if(invinciBagel.isRight())if(invinciBagel.isLeft())条件 if()结构现在可以用于镜像所有 sprite cel 状态图像资产,当您的玩家使用左右键设置角色行进的方向时,并且只有当左右键(仅)用于使角色运行时,该 Java 代码处理的运行周期部分才会发生。

确保你在条件 if()结构的if(invinciBagel.isLeft())部分实现了相同的扩展条件,如下所示,也可以在图 13-13 中看到。

if(invinciBagel.``isLeft()

spriteFrame.setScaleX(``-1

if(``!animator``&& (``!invinciBagel.isDown()``&&``!invinciBagel.isUp()

spriteFrame.setImage(imageStates.get(1));

if(framecounter >= runningspeed) {

animator=true;

framecounter=0;

} else { framecounter+=1; }

}   else if(animator) {

spriteFrame.setImage(imageStates.get(2));

if(framecounter >= runningspeed) {

animator=false;

framecounter=0;

} else { framecounter+=1; }

}

}

A978-1-4842-0415-3_13_Fig13_HTML.jpg

图 13-13。

Add if() statement logic to exclude processing of nested if() hierarchy if down/up keys pressed to isRight()

接下来,让 ASDW 键成为他们自己的精灵控制键,而不是模仿箭头键。

添加事件处理:赋予 ASDW 键功能

因为我还需要实现几个 sprite 状态,所以我将升级事件处理器,使用双手游戏场景,使用 ASDW 箭头键或 ABCD(彩色)游戏控制器按钮,使用以下代码升级:

scene.``setOnKeyPressed

switch (event.getCode()) {

case UP:    up    = true; break;

case DOWN:  down  = true; break;

case LEFT:  left  = true; break;

case RIGHT: right = true; break;

case W:``wKey

case S:``sKey

case A:``aKey

case D:``dKey

}

});

scene.``setOnKeyReleased

switch (event.getCode()) {

case UP:    up    = true; break;

case DOWN:  down  = true; break;

case LEFT:  left  = true; break;

case RIGHT: right = true; break;

case W:``wKey

case S:``sKey

case A:``aKey

case D:``dKey

}

});

正如你在图 13-14 中看到的,代码是没有错误的,因为我在位于 InvinciBagel.java 类顶部的布尔上下左右复合声明语句中添加了 wKey、sKey、aKey 和 dKey 变量。

A978-1-4842-0415-3_13_Fig14_HTML.jpg

图 13-14。

Create event handling for the WSAD keys specifically, by using wKey, sKey, aKey, and dKey variables

接下来,我们需要使用源代码➤插入代码➤生成➤ Getters 和 Setters 的工作过程,让 NetBeans 编写八个。是()和。set()方法,这样我们就可以访问 Bagel.java 类中的变量。

创建 ASDW 键获取和设置方法:NetBeans 插入代码

将光标放在 InvinciBagel.java 类的右花括号前,如图 13-15 中的浅蓝色区域所示,使用源代码菜单选择插入代码选项。在生成浮动菜单中选择 Getter 和 Setter 选项以及 aKey、dKey、sKey 和 wKey 选项,如图 13-15 所示,这样 NetBeans 将生成 8 个。是()和。基于这个新的布尔声明语句的 set()方法也显示在该图的顶部:

private boolean up, down, left, right,``wKey``,``aKey``,``sKey``,``dKey``;

A978-1-4842-0415-3_13_Fig15_HTML.jpg

图 13-15。

Use the Source ➤ Insert Code ➤ Generate ➤ Getter and Setter sequence and select aKey, dKey, sKey, wKey

NetBeans 在类末尾生成的八种方法结构类似于下面的 Java 代码:

public boolean``iswKey()

return wKey;

}

public void``setwKey(boolean wKey)

this.wKey = wKey;

}

public boolean``isaKey()

return aKey;

}

public void``setaKey(boolean aKey)

this.aKey = aKey;

}

public boolean``issKey()

return sKey;

}

public void``setsKey(boolean sKey)

this.sKey = sKey;

}

public boolean``isdKey()

return dKey;

}

public void``setdKey(boolean dKey)

this.dKey = dKey;

}

正如您在图 13-16 中看到的,NetBeans 已经为我们添加到 InvinciBagel.java 类中的新 KeyEvent 处理变量创建了所有八个 getter 和 setter 方法。代码是没有错误的,我们准备回到 Bagel.java 类,并利用这些新的方法调用来实现跳转和回避。

A978-1-4842-0415-3_13_Fig16_HTML.jpg

图 13-16。

Hold a left arrow (or A) and up arrow (or W) key down at the same time, and move the Actor diagonally

添加跳跃和躲避动画:使用 W 和 S 键

让我们开始为使用另一组四个键的基础设施就位,以使我们的玩家在实现精灵(角色)动作(动画)时有更大的灵活性。使用键盘时,这些将使用 ASDW 键,使用游戏控制器时,这些将使用 JavaFX 键码类常量 GAME_A、GAME_B、GAME_C 和 GAME_D。在添加更多游戏玩法逻辑后,我将在游戏开发过程中稍后添加游戏控制器事件处理。基本跳转和回避(投射)子画面 cel 图像资产的实现将通过使用以下两个基本 Java 条件 if()结构来完成,这两个结构通过if(invinciBagel.iswKey())使用 W 键,通过if(invinciBagel.issKey())处理结构使用 S 键,如下所示,以及图 13-17 :

if(invinciBagel.``iswKey()

spriteFrame.``setImage``(imageStates.``get(5)

}

if(invinciBagel.``issKey()

spriteFrame.``setImage``(imageStates.``get(8)

}

A978-1-4842-0415-3_13_Fig17_HTML.jpg

图 13-17。

Add if() statements at the bottom of the method for .iswKey() and issKey for Jump and Evade animation

现在我们已经在游戏事件处理和角色动画处理中实现了 imageStates(0)、imageStates(1)、imageStates(2)、imageStates(4)、imageStates(5)、imageStates(6)和 imageStates(8)。其他精灵更适合用于碰撞检测,所以让我们把它们留到本书后面的章节。

最新详细信息:设置 isFlipH 属性

接下来,我想添加两行代码,我称之为“簿记”,因为我们已经在 Actor 超类中安装了 isFlipH 和 isFlipV 属性,所以如果我们镜像 X 或 Y 轴周围的精灵,我们需要确保正确设置这些变量,以反映 Actor 对象的更改(没有双关的意思),以便我们可以在游戏应用编程逻辑的其他领域中使用这些信息。我们将使用 Java this 关键字来引用我们在 Bagel.java 类中编码的 iBagel 对象,并调用。setIsFlipH()方法,如果 ScaleX 属性设置为 1,则使用 false 值;如果 ScaleX 属性设置为-1,则使用 true 值。需要注意的是,我们并不是绝对必须使用 Java 这个关键字;我使用它是为了意思清楚,所以你也可以简单地使用方法调用而不用点符号,就像这样:setIsFlipH(false);如果你喜欢的话。在图 13-18 中可以看到做这个简单簿记加法的 Java 代码,如下所示:

if(invinciBagel.``isRight()

spriteFrame.``setScaleX``(``1

this``.setIsFlipH(``false

} if(invinciBagel.``isLeft()

spriteFrame.``setScaleX``(``-1

this``.setIsFlipH(``true

}

A978-1-4842-0415-3_13_Fig18_HTML.jpg

图 13-18。

Be sure to set the isFlipH property for the Bagel object using .setIsFlipH() called off of the this (object)

接下来,让我们测试所有新的精灵动画代码,看看它是否做了我们认为逻辑上应该做的事情!代码很复杂,至少对于主要的左右箭头键字符移动来说是如此,但是它组织得很好,而且非常有逻辑性,从逻辑的角度来看,我看不出它有任何问题,但是使用 NetBeans 彻底测试它是找出答案的唯一方法!让我们接下来做那件事。要查看子画面图像状态,请参考第八章(图 8-2)。

测试 invincibagel 精灵动画状态:运行➤项目

现在是时候使用 NetBeans 运行➤项目工作流程并测试。setImageState()方法,该方法现在在。setXYLocation()和。setBoundaries()方法,但在。moveInvinciBagel()方法。因此,现在的逻辑进展是检查按键,基于此设置 X 和 Y 位置,检查以确保您没有越过任何边界,设置精灵动画(图像)状态,然后定位精灵。正如你在图 13-19 中看到的,当你使用左箭头键或右箭头键时,不可战胜的角色现在真实地在屏幕上运行。

A978-1-4842-0415-3_13_Fig19_HTML.jpg

图 13-19。

Testing the InvinciBagel character animation; showing here is the running animation using isFlipH mirror

如果您按下向上箭头键,同时(或之后)您按下向左或向右箭头键,结果如图 13-20 所示,InvinciBagel 将起飞并开始朝那个方向飞行,同样具有逼真的动画运动。

A978-1-4842-0415-3_13_Fig20_HTML.jpg

图 13-20。

Testing the InvinciBagel character animation; showing here is the leap up animation, using isFlipH mirror

如果你按下向下箭头键,同时(或甚至之后)你按下向左或向右箭头键,结果,如图 13-21 所示,是 InvinciBagel 将在准备着陆时下降,同样具有逼真的动画运动。

A978-1-4842-0415-3_13_Fig21_HTML.jpg

图 13-21。

Testing the InvinciBagel character animation; showing here is the landing animation, using isFlipH mirror

现在我们的箭头键正在调用主要的 run-leap-fly-land 游戏播放状态,让我们测试一下我们在本章末尾放置的这个新的 S 和 W 键逻辑,以便我们可以调用一些不太常用的(躲避子弹和跳过)sprite cel 状态,这将为我们在本章中放置的这个角色动画添加更多的多样性,只使用了大约 56 行代码(InvinciBagel.java 中有 26 行,Bagel.java 中有 36 行)。正如您在图 13-22 左侧看到的,当我们使用 S 键时,invincibegel 角色会侧转,这样子弹就会从他面前飞过;在屏幕截图的右侧,您可以看到当我们按下 W 键时,invincibegel 角色会越过物体!

A978-1-4842-0415-3_13_Fig22_HTML.jpg

图 13-22。

Testing InvinciBagel character animation; shown here is the evade (left) animation, and jumping (right)

在介绍动画的这一章中,你给这个游戏的主要无敌角色添加了相当多的“惊奇元素”。这将有助于使这款游戏成为各年龄段的流行游戏。

摘要

在第十三章中,我们使用高度优化的代码在 InvinciBagel 应用中实现了 sprite 动画。我们将七个基本的精灵形状与我们在第十一章中开发的按键移动和我们在第十二章中开发的边界检测结合起来,以创建一个真正活起来的完全动画化的无敌角色,并基于六个基本游戏控制键(上、下、左、右、W 和 S)的使用来实现这一点。

我们学习了如何使用 JavaFX ImageView 类 ScaleX 属性及其特殊用例设置来围绕 Y 轴翻转或镜像 ImageView“图像容器”内部的图像资产。这允许我们仅使用 9 个基本的精灵状态创建 36 个精灵图像状态,我们已经将它们作为图像资产导入到 List ArrayList 对象中。这是一种优化形式,因为它允许我们使用不到 84KB 的图像资产,而不是 336KB 的图像资产,用于我们的主要游戏超级英雄,不可战胜的角色。

接下来,我们学习了如何实现一个 Boolean animator 变量,我们使用它在两个不同的 sprite cel 运行状态之间制作动画,在我们的列表中是 imageStates(1)和 imageStates(2)。最终的运行周期对于专业使用来说太快了,所以接下来,我们添加了一个 framecounter 变量来减慢移动速度,并添加了一个 runningspeed 变量,允许我们对精灵的运行速度进行微调控制,我们将能够在稍后的游戏逻辑中利用这一点。

接下来,我们优化了我们的代码,这样,如果运行周期没有显示,也就是说,如果 InvinciBagel 正在飞行或着陆,就不会使用用于运行周期的变量和处理代码。我们还确保使用 this.setIsFlipH()方法调用在我们的 Actor 超类中(因此在我们的 Bagel 对象中)设置 isFlipH 属性。

接下来,我们在 InvinciBagel.java 类的事件处理代码中添加了四个新的游戏控制键,添加了四个新的私有布尔变量 aKey、sKey、dKey 和 wKey,并让 NetBeans 自动为它们创建 Getter 和 Setter 方法。在我们进行了增强之后,我们在 S 键上添加了精灵躲避动作图像,在 W 键上添加了精灵跳过动作图像,以使我们的游戏成为一个双手游戏,并为使用专业游戏控制器硬件做好准备。

最后,我们用其余的事件处理、演员和英雄类、CastingDirector 类、GamePlayLoop 类和 Bagel 类 sprite 移动和边界代码测试了这个新的 sprite 动画代码,这些代码是我们在前面六章(第七章到第十二章)中编写的。我们的精灵运动和动画无缝地配合在一起,当我们在游戏舞台上导航无敌的角色时,可以提供专业的效果。

在下一章中,我们将看看如何在游戏中添加固定的游戏精灵(道具演员)对象,这样当我们进入碰撞检测时,我们就有东西可以使用了,这样我们就可以开始真正使用我们最近的 CastingDirector 类了。

十四、设置游戏环境:使用Actor超类创建固定的精灵类

现在,我们已经在创建你的主要无敌角色方面取得了很大的进展,使用了 Bagel.java 类中的一些关键方法,以及演员和英雄超类,所有这些都与第七章到第十三章中的 GamePlayLoop 类和 CastingDirector 类一起放置到位,现在是时候添加固定的精灵对象了,我称之为“道具”,到场景和舞台上(屏幕上)。这些固定的物体对游戏来说几乎和主角本身一样重要,因为它们可以用来提供障碍、屏障、免受敌人攻击的保护以及主要游戏英雄角色要克服的各种挑战。我们还需要场景中的这些固定对象,以便在我们的碰撞检测程序逻辑中使用,并测试我们的 CastingDirector 类(object)。

如果你还记得,在第八章中,我们用固定精灵创建了演员超类,用运动精灵创建了英雄超类。我从运动精灵开始,因为主要的游戏角色是运动精灵,即使运动精灵更复杂,就其本质而言,玩起来也更有趣!现在我需要拿出一个章节,将 Actor 超类付诸实施,并创建一些与 Prop 相关的子类。我们将使用这些 Actor 子类在我们的场景(舞台)中填充平台、障碍物、障碍、桥梁、宝藏和类似的游戏固定位置对象,您将想要将这些对象添加到场景(舞台)中,以创建游戏世界并增强游戏体验(也称为用户体验)。

我们将使用 Actor 超类创建四个固定道具子类,这将使构建固定场景(当您创建了多个场景时,也称为关卡)变得容易。Prop.java 类将“按原样”使用您的固定 sprite 图像资产,而 PropH.java 类将使用 JavaFX spriteFrame.setScaleX(-1); Java 语句将 isFlipH 属性设置为 true,并围绕 Y 轴镜像图像资产。PropV.java 类将使用 JavaFX spriteFrame.setScaleY(-1); Java 语句将 isFlipV 属性设置为 true,并围绕 X 轴镜像图像资产。PropB.java 类(B 代表“两者”)将 isFlipV 属性和 isFlipH 属性都设置为 true,这将使用两个 JavaFX spriteFrame.setScale(-1); Java 语句镜像 X 和 Y 轴周围的图像资产。

一旦我们创建了这四个道具相关的演员子类,我们将使用它们来放置固定的对象到场景中,以创建这个游戏的第一级。这样,当我们进入碰撞检测章节时,真实游戏中的一切都将就位,我们将能够开始编码碰撞检测逻辑;然后,最终,一个自动攻击引擎;然后是游戏运行逻辑,它决定了如何实现评分引擎。

这一章对于创造一个更有特色的游戏是有价值的。任何游戏设计的主要部分,在这种情况下,它是 Ira H. Harrison Rubin 的 InvinciBagel 角色和游戏,是建立角色(无论是在单人游戏还是多人游戏中,英雄和他或她的敌人)参与的环境对游戏的成功和流行至关重要,因为这些固定元素是为玩家创造游戏挑战的主要部分。

创建 Prop.java 类:扩展 Actor.java

在 NetBeans 8 中打开 InvinciBagel 项目,右键单击包含您的。java 文件并选择新的➤ Java 类菜单序列。在如图 14-1 所示的“新建 Java 类”对话框中,将类命名为 Prop,并接受 NetBeans 建议的其他默认设置,然后单击“完成”按钮。

A978-1-4842-0415-3_14_Fig1_HTML.jpg

图 14-1。

Right-click on the project folder and select New ➤ Java Class; using a New Java Class dialog, name it Prop

一旦 NetBeans 创建了一个引导类,添加 Java extends 关键字和 Actor,如图 14-2 所示。

A978-1-4842-0415-3_14_Fig2_HTML.jpg

图 14-2。

Add a Java extends keyword and the Actor superclass. Mouse-over the error highlighting underneath Prop

使用建议的 Alt-Enter 工作流程,如图 14-3 所示,并选择“实现所有抽象方法”

A978-1-4842-0415-3_14_Fig3_HTML.jpg

图 14-3。

Use the Alt-Enter work process to pop up a helper dialog filled with suggestions regarding fixing the error

移除 throw new UnsupportedOperationException()并创建一个空的。update()方法。

A978-1-4842-0415-3_14_Fig4_HTML.jpg

图 14-4。

NetBeans generates abstract method for you; remove the throw new statement; create an empty method

Java 代码在类声明语句下仍然有一个红色波浪状的错误高亮,如图 14-5 所示。

A978-1-4842-0415-3_14_Fig5_HTML.jpg

图 14-5。

Overriding .update() method doesn’t remove the error, so mouse-over again, to reveal needed constructor

将鼠标放在上面,您会看到需要使用下面的 Java 代码编写一个 Prop()构造函数方法:

public``Prop

要删除图 14-6 中的错误,使用 Alt-Enter,选择为 javafx.scene.image.Image 添加导入。

A978-1-4842-0415-3_14_Fig6_HTML.jpg

图 14-6。

Add a public Prop() constructor, and mouse-over the error highlight, and select Add import for Image class

如果您仍然看到红色波浪错误突出显示,那么一旦您导入了 Image 类引用,这是因为您需要将 Prop()构造函数方法定义中的参数列表传递到 Actor 类。这是使用 Java super 关键字(有时称为 super()构造函数(方法))完成的,使用以下 Java 语句:

super (SVGdata, xLocation, yLocation, spriteCels);

因为这个 Prop 类使用了一个默认的,或者说未剪裁(未镜像)的 imageStates(0)图像,所以这是我们需要做的第一件事,来创建一个符合抽象 Actor 类的可用 Prop 类。另外,记住 Actor 类为我们初始化了所有的标志属性,这要感谢一个详细的设计过程。图 14-7 中可以看到这个类的 Java 代码,它现在包括了基本的超级构造函数,并且现在是无错误的:

A978-1-4842-0415-3_14_Fig7_HTML.jpg

图 14-7。

Add the super() constructor method call inside of the Prop() constructor, to get rid of the error highlight

我们需要做的下一件事是,使用传入构造函数方法的 xLocation 和 yLocation 值来定位固定的 sprite。setTranslateX()和。setTranslateY()方法。请记住,您在第十二章中使用了这些方法。moveInvinciBagel()方法。在这个 Prop()构造函数方法中,我们将再次使用这些方法,将这些固定的精灵放置在舞台上,在这里,您的构造函数方法参数指示对象构造函数在屏幕上定位它们。

重要的是要记住,由于我们在第八章中所做的工作,Actor 超类的 Actor()构造方法已经为我们执行了iX=xLocation;iY=yLocation; sprite iX 和 iY 属性设置。因此,我们在 Prop()构造函数方法中所要做的就是在构造函数方法内部调用spriteFrame.setTranslateX(xLocation);spriteFrame.setTranslateY(yLocation);,并且在我们的 super()构造函数方法调用之后。请注意,在代码中,在 super()构造函数方法调用中以及在。setTranslateX()和。setTranslateY()方法调用,以便在 Prop 对象实例化期间将固定精灵定位在舞台上,这样我们就不必在代码中的其他地方执行此操作。该类的 Java 代码和构造函数方法如下所示:

package invincibagel;

import javafxscene.image.Image;

public class``Prop``extends``Actor

public Prop(String SVGdata, double``xLocation``, double``yLocation

super (SVGdata, xLocation , yLocation

spriteFrame.setTranslateX(``xLocation

spriteFrame.setTranslateY(``yLocation

}

@Override

public void update() {

// empty method

}

}

正如你在图 14-8 中所看到的,这段 Java 代码是没有错误的,并且在场景(舞台)中定义和放置道具所需要的一切都已经就绪,这要感谢 Actor (fixed) sprite 类的良好设计。这包括 SVG 碰撞形状数据、X 和 Y 位置(场景中的位置)以及一个或多个图像资源。我们包含一个。一个固定的 sprite 类中的 update()方法允许我们拥有动画(不止一个图像单元)道具,如果我们想在我们的游戏设计过程中变得有趣,并增加游戏视觉设计的 wow 因素。

A978-1-4842-0415-3_14_Fig8_HTML.jpg

图 14-8。

Position a fixed sprite on the Stage in the constructor method using a .setTranslateX() and .setTranslateY()

接下来让我们创建 PropH、PropV 和 PropB(代表“两者”)类。这些将使用构造函数方法自动为我们围绕 X,Y 或 X 和 Y 轴镜像道具,因此对象将固有地镜像图像。

镜像属性类:在构造函数中设置 isFlip 属性

为了使我们构建场景的过程更容易,因为一切(碰撞、位置、动画)都是构造函数方法调用的一部分,道具之间的区别在于它们是如何镜像的(X 轴或 Y 轴,或者两个轴)。在这一节中,我们将在构造函数方法中创建为我们翻转精灵的 Prop 类的变体,这样我们所要做的就是创建 PropX 对象类型来获得我们想要的效果。创建第一个 PropH 类,如图 14-9 所示。

A978-1-4842-0415-3_14_Fig9_HTML.jpg

图 14-9。

Right-click on the project folder and select New ➤ Java Class; using a New Java Class dialog, name it PropH

将前面创建的基本 Prop 类的内容,包括 package、import 和 super()构造函数,复制到 PropH 类中,然后将 Prop 改为 PropH,如图 14-10 所示。

A978-1-4842-0415-3_14_Fig10_HTML.jpg

图 14-10。

Create a PropH class structure identical to the Prop class structure, except using PropH, instead of Prop

将 Java this 关键字添加到 super()构造函数下的新行中的 PropH()构造函数方法中,然后使用(键入)句点键打开方便的 NetBeans 方法选择器弹出窗口,如图 14-11 所示。

A978-1-4842-0415-3_14_Fig11_HTML.jpg

图 14-11。

Add a Java this keyword reference to PropH (object) inside of the constructor and open up method helper

双击。setIsFlipH 方法,在图 14-11 中突出显示,并在方法调用参数区插入一个真值。完成的this.setIsFlipH(true); Java 语句如图 14-12 所示。

A978-1-4842-0415-3_14_Fig12_HTML.jpg

图 14-12。

Call the .setIsFlipH() method, off of a Java this (PropH) object reference, and pass it the true setting value

值得注意的是,如果我们只在构造函数方法中使用this.setFlipH(true);方法调用,我们仍然需要观察 isFlipH 是这样设置的,并在代码中的其他地方调用您的.spriteFrame.setScaleX(-1);方法调用。让我们继续将这个方法调用放入您的构造函数方法中,这样我们所有的 PropH 对象都将自动使它们的图像资产围绕 Y 轴翻转。如果你想知道为什么?setScaleX(-1)方法镜像 Y 轴周围的图像资产,这也是我想知道的,但我所做的是将 isFlipH 与。setScaleX(-1)(因为 X 是 H 或水平轴)。也就是说,我会。setScaleY(-1)围绕垂直 Y 轴的镜像,对我来说更有意义。添加另一行代码,键入 spriteFrame 并按下句点键,然后选择。在弹出窗口中设置 ScaleX,如图 14-13 所示。

A978-1-4842-0415-3_14_Fig13_HTML.jpg

图 14-13。

Call a .setScaleX() method, off of the spriteFrame object variable reference, and pass it a -1 setting value

在弹出的方法选择器中双击 setScaleX(double value) void 选项,然后添加-1 值,完成方法调用。现在所有 PropH 对象将自动镜像图像资源 Y 轴!PropH 构造函数的最终代码。setTranslateX()和。setTranslateY()方法调用将在舞台上实际定位固定的 sprite 位置,如图 14-14 所示,类似于下面的 Java 代码:

import javafxscene.image.Image;

public class``PropH``extends``Actor

public PropH(String SVGdata, double``xLocation``, double``yLocation

super (SVGdata, xLocation , yLocation

this``.setIsFlipH(``true

spriteFrame.``setScaleX``(``-1

spriteFrame.setTranslateX(``xLocation

spriteFrame.setTranslateY(``yLocation

}

}

A978-1-4842-0415-3_14_Fig14_HTML.jpg

图 14-14。

A completed PropH() constructor method creates a fixed PropH object flipped along the horizontal X axis

接下来,执行与 PropH 类相同的工作过程,创建一个 PropV 类,将 isFlipV 属性设置为 true,并实现spriteFrame.setScaleY(-1);方法调用,如图 14-15 所示。

A978-1-4842-0415-3_14_Fig15_HTML.jpg

图 14-15。

A completed PropV() constructor method creates a fixed PropV object flipped along the vertical Y axis

接下来,执行与 PropV 类相同的工作过程,并创建 PropB 类。这个类将把 isFlipV 和 isFlipH 属性都设置为 true,并实现一个spriteFrame.setScaleX(-1);方法调用和一个spriteFrame.setScaleY(-1);方法调用。这最后一个 PropB 类的 Java 类结构将沿 X 和 Y 轴镜像固定道具图像,如图 14-16 所示,类似于以下代码:

public class``PropB``extends``Actor

public PropB(String SVGdata, double``xLocation``, double``yLocation

super (SVGdata, xLocation , yLocation,

this.``setIsFlipH``(``true

spriteFrame.``setScaleX``(``-1

this.``setIsFlipV``(``true

spriteFrame.``setScaleY``(``-1

spriteFrame.setTranslateX(``xLocation

spriteFrame.setTranslateY(``yLocation

}

}

正如你在图 14-16 中看到的,代码是没有错误的,你现在有了一个 PropB 类,它将为你的场景创建固定的对象,这些对象同时围绕 X 轴和 Y 轴翻转或镜像!有了这四个不同的固定道具类,我们就可以快速轻松地设计场景(以及最终的游戏关卡)元素,除了声明正确的道具类、引用正确的图像资源以及 X、Y 轨迹(位置)和碰撞多边形 SVG 数据之外,什么都不用做。

A978-1-4842-0415-3_14_Fig16_HTML.jpg

图 14-16。

A completed PropB() constructor method creates a fixed PropB object flipped along both the X and Y axis

现在我们准备使用这四个新的固定精灵类,并学习如何添加场景(舞台)元素。

使用道具类:创建固定场景对象

在我们开始编写 InvinciBagel.java 类的代码之前,您应该已经在 NetBeans 中打开了它自己的标签,并使用这些道具类将固定的精灵设计元素添加到我们的游戏中,我想向您展示如何去掉文件名旁边那些讨厌的小扳手图标。在创建了这四个新的道具相关的类之后,你的 IDE 屏幕上现在可能已经有几个了,所以让我们来学习如何让这些消失吧!如图 14-17 所示,如果右击扳手图标旁边的文件名,选择编译文件菜单选项,或者使用 F9 功能键,扳手就会消失。本质上,扳手表示您正在处理该文件,也就是说,您已经对该文件中的代码进行了更改,并且没有通过编译它来检查错误并保存它,从而使该代码永久化。

A978-1-4842-0415-3_14_Fig17_HTML.jpg

图 14-17。

If you want to get rid of the little wrench icon next to the file name, right-click the file, and Compile File

添加属性和图像声明:属性和图像对象

让我们首先声明我们需要创建的对象,以将固定精灵添加到我们游戏的场景和舞台对象中,这些对象最初将是一个名为 iPR0 的道具对象,它代表 InvinciBagel Prop zero,最终随着本章的进行,会有一个名为 iPH0 的 PropH 对象、一个名为 iPV0 的 PropV 对象和一个名为 iPB0 的 PropB 对象,这样您就有了使用所有这些类的经验,并且我们可以确保它们都按照我们设计的方式工作。我们还将在图像 iB0 到 iB8 声明的末尾添加一个 iP0 图像对象声明,然后再添加一个 iP1 图像对象声明。Java 语句如图 14-18 所示,如下所示:

Prop``iPR0

private Image iB0, iB1, iB2, iB3, iB4, iB5, iB6, iB7, iB8,``iP0

A978-1-4842-0415-3_14_Fig18_HTML.jpg

图 14-18。

Add Prop object declaration, name it iPR0 (invincibagel Prop zero), and add an iP0 to Image declaration

在我们可以实例化图像对象之前,我们需要将图像资产复制到/src 文件夹中,因此使用文件管理软件包(我使用的是 Windows Explorer)将 prop0.png 和 prop1.png png 8 图像文件与您已经安装在游戏项目中的其他十几个图像资产一起复制进来,如图 14-19 所示。

A978-1-4842-0415-3_14_Fig19_HTML.jpg

图 14-19。

Use your file management software to copy the prop0 and prop1 PNG8 files into the project /src folder

接下来,让我们实例化包含一个适当图像的图像对象,一个可平铺的砖块,这样我们就可以使用我们的四个 InvinciBagel.java 方法。loadImageAssets(),。createGameActors(),。addGameActorNodes(),最后是。createCastingDirection()将固定道具对象安装到游戏的场景对象和游戏的舞台对象中。

实例化图像对象:使用。loadImageAssets()方法

在 NetBeans 中打开“InvinciBagel.java”选项卡,并打开 loadImageAssets()方法。现在添加一个引用 prop0.png 文件及其 72x32 像素大小的 iP0 图像对象实例化语句。创建 iP0 图像对象的 Java 代码,如图 14-20 所示,应该类似于下面的 Java 语句:

iP0 = new Image(" /prop0.png ", 72 , 32 , true, false, true);

A978-1-4842-0415-3_14_Fig20_HTML.jpg

图 14-20。

Add the iP0 Image object instantiation using the prop0.png file name and the 72 by 32 pixel image size

现在我们准备添加我们的第一个道具对象到我们的游戏中来添加一个固定的精灵,这将允许我们使用在本章中放入 invincibagel 包代码库中的类来创建我们的游戏设计。

使用道具对象添加固定精灵:。addGameActors()

在你之后。loadImageAssets()方法在游戏设计过程中,我们设计了另外三个方法“容器”,用于向游戏中添加演员对象。这些是按照我们在这个过程中需要使用它们的顺序来调用的,所以让我们继续按照这个顺序来组织和使用它们。我们需要执行的第一个实例化创建了一个新的(使用 Java new 关键字)Prop 对象。现在,我们将再次使用我们的“虚拟”SVG 形状数据,以及屏幕 X 和 Y 位置的 0,0 中心,最后是 iP0 图像资源,我们刚刚在本章的前几节中声明并实例化了它。在图 14-21 中可以看到的 Java 实例化语句应该类似于下面的 Java 代码行:

iPR0 = new Prop("M150 0 L75 200 L225 200 Z", 0, 0, iP0

下一步是将 iPR0 属性对象的 ImageView 节点对象添加到场景图形根对象。这是使用. getChildren()完成的。add()方法链引用了 iPR0 属性对象内的 spriteFrame ImageView 对象,使用点标记法,使用以下 Java 语句,如图 14-21 所示:

root``.getChildren().add(``iPR0.spriteFrame

接下来,使用。addCurrentCast()方法将 iPR0 对象添加到 castDirector 对象,如图 14-21 所示:

castDirector``.addCurrentCast(``iPR0

A978-1-4842-0415-3_14_Fig21_HTML.jpg

图 14-21。

Instantiate an iPR0 Prop object, add it to the root Scene Graph, and add it to the CurrentCast List

接下来,使用运行➤项目工作流程测试代码。结果如图 14-22 左侧所示。

A978-1-4842-0415-3_14_Fig22_HTML.jpg

图 14-22。

Testing InvinciBagel 0,0 prop placement (left) and changing the z-index in .addGameActorNodes() (right)

正如您所看到的,InvinciBagel 字符在 Prop 对象的后面,这意味着我们需要改变。addGameActorNodes()方法,因为节点对象添加到场景图的顺序决定了它们的 z 索引或 z 顺序。如图 14-23 所示,我已经移动了。add()方法调用将 iPR0 添加到 iBagel 的 IP r0 之上,这样你就可以在图 14-22 的右侧看到,InvinciBagel 角色现在位于道具对象的顶部。当然,一旦我们增加了碰撞检测,这就不是问题了。让我们使用相同的工作流程,并在场景中添加一个 PropH 对象,这样我就可以向您展示如何翻转和镜像这个平铺图像,以便为您的 Java 8 游戏开发创建无缝的可平铺结构。

让我们使用相同的命名约定,将 PropH 对象命名为 iPH0。在图 14-23 中可以看到的 Java 实例化语句应该类似于下面的 Java 代码行:

iPH0 = new PropH ("M150 0 L75 200 L225 200 Z", 0, 0, iP0

添加固定 sprite 属性的下一步是将 iPH0 属性对象的 ImageView 节点对象添加到场景图形根对象中。这是通过使用。getChildren()。add()方法链,该方法链通过使用点标记来引用位于 iPH0 PropH 对象内部的 spriteFrame ImageView 对象。这是使用下面的 Java 语句完成的,该语句也显示在图 14-23 中:

root``.getChildren().add(``iPH0.spriteFrame

最后,我们将使用。addCurrentCast()方法,我们在第十章中创建的,使用下面一行 Java 代码,将这个 iPH0 对象添加到 castDirector CastingDirector 对象内部的 CURRENT _ CAST ListArrayList 对象,也显示在图 14-23 的底部:

castDirector``.addCurrentCast(``iPH0

A978-1-4842-0415-3_14_Fig23_HTML.jpg

图 14-23。

Instantiate an iPH0 PropH object, add it to the root Scene Graph, and add it to a CurrentCast List

正如你在图 14-23 中看到的,我也已经将道具对象的 0,0 坐标改为 0,148,并将道具对象的坐标改为 72,148。这将把 Y 轴镜像的 PropH 对象无缝地放置在 Prop 对象的右侧。如果您现在想查看无缝平铺效果,可以使用“运行➤项目”工作流程。如果您当前没有运行 NetBeans 8 IDE,并且您想要预测图 14-26 ,您现在可以看到这种平铺(镜像)效果。最后,我还将把 PropV 和 PropB 类(对象)集成到这个镶嵌中。

让我们使用完全相同的工作流程,并在场景中添加一个 PropV 对象,这样我们就可以看到如何围绕 X 轴翻转和镜像这个 tile 图像,从而为 Java 8 游戏开发创建更加复杂的无缝 tileable 构造。让我们使用相同的命名约定,并将这个新的 PropV 对象命名为 iPV0。您的新 Java 实例化语句,可以在图 14-24 中看到,应该看起来像下面这行 Java 代码:

iPV0 = new PropV ("M150 0 L75 200 L225 200 Z", 0, 0, iP0

添加固定 sprite 属性的下一个逻辑步骤是将该 iPV0 PropV 对象的 ImageView 节点对象添加到场景图形根 StackPane 对象中。这是通过使用。getChildren()。add()方法链,它引用 spriteFrame ImageView 对象,该对象位于 iPV0 PropV 对象内部,使用点标记法。这可以通过使用下面的 Java 编程语句来完成,该语句也显示在图 14-24 中:

root``.getChildren().add(``iPV0.spriteFrame

最后,我们将使用。addCurrentCast()方法,我们在第十章中创建的,使用下面一行 Java 代码,将这个 iPV0 对象添加到 castDirector CastingDirector 对象内部的 CURRENT _ CAST ListArrayList 对象,这也显示在图 14-24 的最底部:

castDirector``.addCurrentCast(``iPV0

A978-1-4842-0415-3_14_Fig24_HTML.jpg

图 14-24。

Instantiate an iPV0 PropV object, add it to the root Scene Graph, and add it to a CurrentCast List

最后,让我们将一个 PropB 对象添加到场景中,这样我就可以向您展示如何同时围绕 X 轴和 Y 轴翻转(镜像)可平铺图像。我们将遵循我们的命名约定,将 PropB 对象命名为 iPB0。在图 14-25 中可以看到的实例化语句应该类似于下面的 Java 代码:

iPB0 = new PropB ("M150 0 L75 200 L225 200 Z", 0, 0, iP0

接下来,让我们将 iPB0 PropB 对象的 ImageView 节点添加到场景图形根对象中。这是使用. getChildren()完成的。add()方法链。这引用了一个 spriteFrame ImageView 对象,它位于 iPB0 PropB 对象内部,使用点标记。这是使用下面的 Java 语句完成的,该语句也显示在图 14-25 中:

root``.getChildren().add(``iPB0.spriteFrame

最后,我们将使用。我们在第十章的中创建的 addCurrentCast()方法,用于将此 iPB0PropB 对象添加到 castDirector CastingDirector 对象内部的 CURRENT _ CAST ListArrayList对象,可以在图 14-25 的底部看到,该方法使用以下单行 Java 代码:

castDirector``.addCurrentCast(``iPB0

既然我们已经将本章前半部分创建的所有四个 Actor 子类投入使用,我们可以测试应用,看看不同的 X 轴和 Y 轴镜像对 prop0.png 砖块做了什么。

A978-1-4842-0415-3_14_Fig25_HTML.jpg

图 14-25。

Instantiate an iPB0 PropB object, add it to the root Scene Graph, and add it to a CurrentCast List

使用运行➤项目并测试游戏,看看每个砖块是如何被不同地镜像的,如图 14-26 所示。

A978-1-4842-0415-3_14_Fig26_HTML.jpg

图 14-26。

Run ➤ Project; Prop, PropH, and PropV shown at the left, and all four Prop subclasses shown at the right

接下来,我们来看看如何使用大型场景道具对象来合成舞台上的背景元素。

使用更大的场景道具:用 JavaFX 合成

我们创建的这四个道具演员子类的一个真正好处是,它们允许我们利用 PNG8(背景图像资产)和 PNG32(带有 alpha 通道的真彩色合成图像)图像资产在我们的游戏场景和舞台对象中进行数字图像合成。如果我们不通过实现碰撞检测来对运动精灵游戏角色使用固定道具,并且如果我们在后台保留这些固定道具,通过观察我们在。addGameActorNodes()方法就 Actor z-index 而言,我们可以使用我们为角色和障碍开发的相同合成引擎来优化游戏视觉元素。我们可能根本不需要在游戏中使用任何背景图片。至少,这允许我们添加更简单的背景图片,比如一个有云的基本天空,或者一个日落。这些压缩更好,由于他们的简单,可以使用 PNG8 图像与原始的结果。接下来让我们添加一个更大的固定 sprite 道具,它的宽度接近 500 像素,高度接近 100 像素。如图 14-27 所示,我们需要添加的第一件事是另一个道具对象,我们将命名为 iPR1,另一个图像对象,我们将命名为 iP1,使用以下代码:

Prop``iPR0,``iPR1

private``Image``iB0, iB1, iB2, iB3, iB4, iB5, iB6, iB7, iB8, iP0,``iP1

A978-1-4842-0415-3_14_Fig27_HTML.jpg

图 14-27。

Add an iPR1 Prop declaration (shown with all other PropH, PropV and PropB declarations) and iP1 Image

复制你的 iP0 镜像实例,创建一个 iP1 镜像,引用 prop1.png,如图 14-28 所示。

A978-1-4842-0415-3_14_Fig28_HTML.jpg

图 14-28。

Add the iP1 Image object instantiation using the prop1.png file name, and the 496 by 92 pixel image size

复制您的 iPR0 对象实例化和添加代码,创建一个 iPR1 对象,如图 14-29 所示。

A978-1-4842-0415-3_14_Fig29_HTML.jpg

图 14-29。

Instantiate an iPR1 Prop object, add it to the root Scene Graph, and add it to the CurrentCast List

为了在屏幕顶部放置一块苔藓,我在构造函数方法调用中使用了 0,-150 屏幕坐标。现在使用运行➤项目工作流程来看看结果,如图 14-30 左侧所示。

A978-1-4842-0415-3_14_Fig30_HTML.jpg

图 14-30。

Run the project; the Prop iPR1 is shown at the left, and iPV1 PropV mirrored object is shown at the right

你也可以“翻转”大型道具,创造一些非常酷的效果。创建一个 iPV1 声明和实例化,如图 14-31 所示,并创建一个 PropV 对象,它将沿着 X 轴镜像苔藓岩石。这可以在图 14-30 的右侧看到。在这一章中,我们已经在使用固定精灵道具类能力方面取得了很大的进步。

A978-1-4842-0415-3_14_Fig31_HTML.jpg

图 14-31。

Instantiate an iPV1 PropV object, add it to the root Scene Graph, and add it to a CurrentCast List

摘要

在第十四章中,我们创建了固定精灵“道具”类,允许我们设计游戏场景和固定对象,我们的运动精灵演员对象将与之交互。我们首先创建了 Prop 类,它扩展了我们在第八章中创建的 Actor 类。我们使用了。setTranslateX()和。setTranslateY()方法在构造函数方法中使用 xLocation 和 yLocation 参数来定位舞台上的 ImageView,类似于我们使用。moveInvinciBagel()类,仅在固定精灵的情况下,在构造函数方法内部,移动仅进行一次,以将道具定位在舞台上的场景中。

接下来,我们创建了更复杂的 PropH 和 PropV 类,它们除了在场景中定位固定的精灵之外,还围绕 Y 轴(PropH)和 X 轴(PropV)自动镜像它们。我们还创建了一个 PropB (B 代表 Both)类,它将自动镜像 X 轴和 Y 轴周围的固定 sprite 图像资产。

接下来,我们学习了如何通过在我们的 InvinciBagel.java 主游戏设计类中声明、实例化和添加(到 JavaFX 场景图以及 CastingDirector 对象)这些道具类来实现它们。我们学习了如何在屏幕上定位我们的固定精灵,以及如何创建无缝的镜像平铺效果,并测试了我们的新道具类,以确保它们可以用于设计游戏关卡。

最后,我们用更大的道具测试了这四个新的固定 sprite 类中的两个,如果我们不通过将它们添加到 CastingDirector 对象中来定义用于碰撞检测的固定 sprite 对象,可以想象它们可以用于场景背景合成。这将有助于减少游戏的数据足迹,最大限度地减少大型 PNG24 背景图像的使用,允许我们用小得多的 PNG8 背景图像替换这些数据繁重的图像。这些可以描绘场景“设置”,如雨天、多云的天空或生动的日落。

在下一章,我们将看看如何在游戏代码中添加一个数字音频音效引擎。这个音频引擎将使用 JavaFX AudioClip 类为游戏添加音效排序功能,以便我们在添加物理和碰撞检测等内容时,所有基本的新媒体元素(固定和运动图像以及音频反馈)都可以工作。通过这种方式,我们可以开始添加多媒体,在游戏体验中利用玩家的所有感官。

十五、实现游戏音频资源:使用 JavaFX AudioClip类音频序列引擎

既然我们已经使用抽象的 Actor(fixed sprite)和 Hero(motion sprite)超类创建了 motion sprite 和 fixed sprite 类,我们需要将代码放入播放游戏音频资产的位置。虽然一般不认为数字图像(视觉)资产重要,但数字音频(听觉)资产对游戏质量非常重要。事实上,您会惊讶于这些优秀的音频资产能为您的 Java 8 游戏产品增加多少感知价值。在本章中,我们将学习如何使用诸如 Audacity 2.0.6 这样的开源工具,为您的 Java 8 游戏开发优化和实现数字音频资产。

幸运的是,JavaFX AudioClip 引擎(实际上,它是 javafx.scene.media 包中的一个类)可以为我们的游戏开发带来很多动力。这是因为该类本质上被设计为音频排序引擎,能够控制音频资产性能的各个方面,以及使用 6 个八度音阶(三个向上和三个向下)的音高移位功能来创建新的音频资产。在本章开始时,我们将详细学习这个类,然后在我们的主要 InvinciBagel.java 类中实现它,并在新的 Bagel.java 类中使用它。playAudioClip()方法,我们将在该类的 primary 中对其进行编码和调用。update()方法。

在我们详细了解 JavaFX AudioClip 类之后,我们将开始使用流行的 Audacity 2.0.6 数字音频编辑(和音频效果)软件,我们在第一章中安装了该软件,当时我们安装了所有的开源游戏开发软件工具。我们将使用 Audacity 完成一个音频资产创建和优化过程。我们将使用我们在第五章中学到的概念,涵盖新媒体内容创作概念,并优化数字音频文件以实现 800%的数据空间节省。我们将这样做,以便我们的数字音频资产不使用超过 64KB 的系统内存;事实上,我们将获得六个 16 位数字音频资产,以使用不到 62KB 的数据。

一旦我们创建了六个音频资产,它们将与我们用来控制无敌角色的六个不同的键相匹配,我们将创建。InvinciBagel 类中的 loadAudioAssets()方法,并了解如何声明 AudioClip 和 URL 对象。在里面。loadAudioAssets()方法然后我们将一起使用这两个类(对象),以便为游戏创建我们的数字音频资产,并将它们安装到玩家的计算机系统(或消费电子设备)内存中。

一旦这六个 AudioClip 对象就位,我们将让 NetBeans 为 AudioClip 对象创建六个 Setter 方法,然后对它们进行“变形”。setiSound()方法添加到。我们需要的 playiSound()方法。完成之后,我们将进入 Bagel.java 类,并添加一个. playAudioClip()方法。在这个方法中,我们将调用。playiSound()方法,基于玩家按下的键。

在本章中,我们有很多内容要讲,所以让我们从深入了解 JavaFX AudioClip 类及其各种属性和方法开始,我们可以用它们来调用令人印象深刻的音频序列!解决了这个问题之后,我们就可以进入有趣的内容,开始使用 Audacity 2.0.6 和 NetBeans 8 进行创作了!

JavaFX AudioClip 类:一个数字音频序列器

public final AudioClip 类是 javafx.scene.media 包的一部分,本质上是一个数字音频样本回放和音频排序引擎,旨在使用简短的音频片段或“样本”来创建更复杂的音频演奏。这就是为什么这个类是用于 Java 8 游戏开发的完美的数字音频媒体播放类。从下面显示的 Java 继承层次结构可以看出,JavaFX AudioClip 类是 java.lang.Object master 类的直接子类。这意味着 AudioClip 类已经被“临时编码”,专门用于作为数字音频排序引擎。

java.lang.Object

> javafx.scene.media. AudioClip

您的 AudioClip 对象将分别引用内存中的一个数字音频样本。这些可以以几乎为零的延迟触发,这使得这个类成为用于 Java 8 游戏开发的完美类。AudioClip 对象的加载方式与媒体(长格式音频或视频)对象类似,使用 URL 对象,但行为方式有很大不同。例如,媒体对象不能自己播放;它需要一个 MediaPlayer 对象,如果包含数字视频,还需要 MediaView 对象。媒体对象将更适合于长格式的数字音频资产(如音乐),这些资产不能同时放入内存中,并且必须进行流式传输以获得最佳的内存利用率。MediaPlayer 只能将足够的解压缩数字音频数据“预滚动”到内存中,以便播放一小段时间,因此 MediaPlayer 方法对于较长的数字音频剪辑(尤其是经过压缩的数字音频剪辑)来说更加节省内存。

AudioClip 对象可以在实例化后立即使用,这一点您将在本章后面看到,对于 Java 8 游戏开发来说,这是一个重要的属性。AudioClip 对象回放行为可以说是“触发并忘记”,这意味着一旦您的 playiSound()方法调用之一被调用,您在数字音频样本上的唯一可操作控制就是调用 AudioClip。stop()方法。

有趣的是,您的 AudioClip 对象也可能被触发(播放)多次,audio clip 甚至可以被同时触发,这一点您将在本章稍后部分看到。要使用 Media 对象达到同样的效果,您必须为要并行播放的每个声音创建一个新的 MediaPlayer 对象。

AudioClip 对象如此通用(响应迅速)的原因是因为您的 AudioClip 对象存储在内存中。AudioClip 对象使用代表整个声音的原始、未压缩的数字音频样本。它以原始的、未压缩的状态存储在内存中,这就是为什么在本章的下一节我们将使用 WAVE 音频文件格式。这种音频格式应用零压缩,因此,优化的数字音频样本的最终文件大小也将代表这些样本中的每一个将利用的系统内存量。

然而,AudioClip 类真正令人印象深刻的是它通过其属性和三个重载属性赋予开发人员的能力。play()方法调用。使用 AudioClip 不定常数,您可以设置样本回放优先级,将音高(声音的频率)向上移动 8 倍(较高八度)或向下移动 1/8(较低八度),在空间频谱的任何位置移动声音,控制声音的左右平衡,控制声音的音量(或振幅),以及控制声音播放的次数,从一次到永远。

除了这些 AudioClip 属性的 getter 和 setter 方法之外,还有。播放()和。stop()方法以及三个(重载的)方法。play()方法:一个用于简单(默认)回放;您可以在其中指定音量;您可以在其中指定音量、平衡、频率(音高移位系数)、声相和样本优先级。

控制 AudioClip 对象的关键是调用三者之一。play()方法,具体取决于您希望如何控制样本回放。使用。play()进行简单的回放,就像我们在本章将要做的那样;或者使用。播放(双倍音量)以从 0.0(关)到 1.0(满)的相对音量播放您的 AudioClip 对象;或者使用。play(双音量、双平衡、双速率、双声相、双优先级)以相对音量(0.0 到 1.0)、相对平衡(-1.0 左、0.0 中、1.0 右)、音高移位率(0.125 到 8.0)、相对声相(-1.0 左、0.0 中、1.0 右)播放您的 AudioClip 对象;以及优先级整数,其指定哪些样本优先于其他样本播放,哪些样本由于资源和优先级低而可能不被播放。接下来让我们开始使用 Audacity 来优化我们的示例!

创建和优化数字音频:Audacity 2.0.6

发布 Audacity 的最新版本——在我写这篇文章的时候是 2.0.6,用你的麦克风录下你说“left”这个词的声音幸运的是,我的 NetBeans 8 和 Java 8 开发工作站也是我的 Skype 工作站,我在一个支架上有一个基本的 Logitech 可调麦克风,我可以用它来制作基本的音频文件,我们需要将音频文件放入我们在第章第 1 3 中放置的每个不同精灵动作的位置。在本章的这一节,我将介绍数字音频文件的“临时创建”和优化工作流程,对于我们在本章的编码部分需要使用的六个数字音频文件中的每一个,您都可以自己完成这项工作。如果您想简单地了解使用 JavaFX AudioClip 类实现数字音频资产的 Java 8 编码部分,您也可以使用本书软件资源库中包含的六个音频文件。我建议大家复习一下如何优化未压缩的音频,以便在系统内存中使用,因为我们将要获取 113KB 的原始音频数据,从中剔除 99KB 的数据,并将其再减少 88%,只有 14KB。

正如您在图 15-1 的左侧所看到的,我已经记录了口语单词“left”,并使用 Audacity 选择工具只选择了记录会话中包含音频数据的那部分,以较暗的灰色阴影显示。因为您可以在 Audacity 波形编辑区域看到数字音频波形表示,所以您可以看到您刚才记录的数据所在的录音部分。录音中不包含任何数字音频数据的部分看起来就像一条直线。

获取原始数字音频数据的最快方式,这是我们真正想要的。WAV 文件,并且是我们希望使用 Audacity 进行优化的唯一数据,是使用文件➤导出选择菜单序列。这将允许我们使用 WAVE PCM 数字音频格式直接将选定的音频数据写入 left.wav 文件。这样做之后,我们就可以开始一个新的 Audacity 编辑会话,只需打开该文件,开始数字音频内容优化过程。

A978-1-4842-0415-3_15_Fig1_HTML.jpg

图 15-1。

Launch Audacity, record your voice saying the word “left,” and then select the wave and Export Selection

在 Audacity 中使用导出选择菜单选项后,您将得到导出文件对话框,如图 15-2 所示。在对话框底部的文件名:字段中键入 left,然后单击保存按钮。

A978-1-4842-0415-3_15_Fig2_HTML.jpg

图 15-2。

In the Export File dialog, name the file “left” using the .WAV audio file type

现在,我们在 left.wav 音频文件中只剩下口语词,并已保存该文件,以便我们可以看到基线原始 32 位 44.1kHz 数字音频将为我们提供什么数据足迹(以及我们游戏的内存足迹,如果我们要按原样使用该文件),我们可以结束录制会话,因为它已经达到了目的。我们将通过使用文件➤关闭菜单序列来完成此操作,该菜单序列可以在图 15-3 的左侧看到。

接下来,我们将返回并使用文件➤打开菜单序列打开 left.wav 文件,该文件仅包含我们希望优化的数字音频数据段。这可以在图 15-3 (菜单序列)的中间看到,在屏幕截图的右边你可以看到一个选择一个或多个音频文件(文件打开)的对话框。请注意,left.wav 文件显示了它的原始数据量(113 KB)。

我们可以使用这个原始数据占用量作为基线,看看在我们即将开始的优化过程中,我们减少了多少次数据占用量(一旦完成,最终将减少 8.8 倍)。

A978-1-4842-0415-3_15_Fig3_HTML.jpg

图 15-3。

Close the current editing session, then Open the “left” file, noting its raw file size, for further optimization

当你点击打开按钮并在 Audacity 中第一次打开一个文件时,你会得到一个警告对话框,如图 15-4 所示。这将建议您可以制作原始文件的副本以在编辑会话中使用,而不是使用原始文件。这就是多媒体行业中所谓的“非破坏性”编辑,并且总是一个非常好的想法,因为它本质上为您提供了一个备份文件(原件),作为工作流程的一部分。

选择“编辑前制作文件副本(更安全)”单选按钮选项,并选中“不再警告,并始终使用我上面的选择”复选框,这将使 Audacity 2 成为一个非破坏性的非线性数字音频编辑软件包。单击“确定”按钮,我们就可以开始您的数字音频数据优化工作流程了。我们将优化我们的数字音频数据,但不压缩它,我将进入为什么这是下一步。

A978-1-4842-0415-3_15_Fig4_HTML.jpg

图 15-4。

Enable non-destructive audio editing in Audacity

优化与压缩:音频内存占用

你可能想知道为什么我使用未压缩的脉冲编码调制(PCM)波。wav)文件格式,而不是许多人用来存储数字音频音乐文件的行业标准的. MP3 文件格式。在我们开始优化过程之前,我将在前面介绍这样做的原因。在数字音频领域,数据占用优化实际上分为两个阶段。首先,优化采样分辨率(32 位原始录音、24 位 HD 音频、16 位 CD 质量音频和 8 位低质量音频)和采样频率(44.1kHz、22.05kHz、11.025kHz 是主要频率级别,仍然可以保持足够的数据以获得高质量的结果),然后应用压缩。压缩会影响文件大小;在这种情况下,它是你。Java 资源文件。

那么,为什么我们不把我们的文件压缩成 MP3 格式,使我们的。JAR 文件小几千字节?其主要原因是因为 MP3 是一种“有损”格式,它丢弃了音频样本的原始数据(和质量)。因为 JavaFX AudioClip 类将获取我们的数字音频资产并将其解压缩到内存中,所以如果我们使用 MP3,内存中包含的音频数据质量将比使用 WAV 格式时低。鉴于我们将在样本优化工作过程中获得至少 8 倍的数据占用空间减少(我们将在本章的下一节中学习),并且我们所有的数字音频资产都将被优化到 4KB 到 14K 之间的数据占用空间,相对于样本质量的降低,MP3 压缩不会给我们带来任何真正的 JAR 文件数据占用空间的减少,这将“消耗”我们的“成本”。游戏音频是短脉冲音效和音乐循环,所以我们可以使用 WAV 文件格式,仍然可以得到一个很好的结果,而不必使用任何压缩。另一个优点是,你在我们的文件管理软件中看到的 WAV 文件的数据大小也是样本将使用的内存量。

音频采样分辨率和频率:优化您的内存占用

内存数据占用减少过程的第一步是获取原始 32 位数据采样速率,并将其降低 100%,从 32 位浮点数据降至 16 位 PCM 数据,如屏幕截图左下方的图 15-5 所示。在 Audacity 样本编辑区域左侧的灰色信息面板中找到下拉箭头,我在这个截图中用红色圈出了它,因为如果您不习惯使用它,很难找到它。这将为您提供一个菜单,允许您设置数据显示(波形或频谱图)和设置样本数据格式,这是我们想要用来选择 16 位 PCM 选项而不是 32 位浮点选项的子菜单。不要用这个菜单设置你的采样率,因为它会降低你的声音(如果你以后想用这个作为特殊效果,你可以试试)。接下来,我们将了解设置采样速率的正确工作流程。

A978-1-4842-0415-3_15_Fig5_HTML.jpg

图 15-5。

Click the drop-down menu arrow at the left and select the Set Sample Format ➤ 16-bit PCM setting option

重要的是要注意,如果您在进行这种数据位级别的更改后保存您的文件,您的文件大小将不会改变!您可能已经注意到,“导出文件”对话框正在以 16 位 PCM WAVE 格式保存您的文件,因此它正在做与您在内存中所做的相同的调整,以调整磁盘上的文件大小。我在这里简单地介绍了这一步骤,以便您对整个过程有一个全面的了解,首先是降低采样数据速率,然后是降低采样频率速率,这是我们接下来要做的事情,最后是解决立体声与单声道采样问题,这将是我们在本章这一部分的最后一步。

每次应用工作流程时,这些“优化措施”都可以将文件大小(和内存占用)减少 100%或更多。事实上,当我们将采样频率从 44,100 降低到 11,025 时,我们将减少 200%的数据占用空间(从 44,100 降低到 22,050,然后从 22,050 降低到 11,025)。

设置音频采样频率:减少 200%的内存数据占用

在 Audacity 中为您的项目设置数字音频采样频率的正确方法是使用项目采样频率设置的下拉对话框。这可以在图 15-6 的左下角看到,在一个红色方框内突出显示。选择 11025 频率设置,这会将音频数据(将这些视为声波的垂直切片)采样速率从每秒 44,100 次降低到每秒 11,025 个数据切片,或者首先将采样的音频数据降低 4 倍,这是由于数据中的采样频率步长(在这种情况下,您应该将其视为使用的内存,而不是使用的文件空间)优化工作流程而导致的 200%的数据占用空间减少。

A978-1-4842-0415-3_15_Fig6_HTML.jpg

图 15-6。

Reduce audio sampling frequency by four times by reducing it from 44100 per second to 11025 per second

您可以在 44、100 和 8000 采样频率之间尝试这七种不同的设置,因为每种设置都有不同的质量水平,8000 的质量太低,不适用于声音样本,但它可能适合“脏”或“嘈杂”的声音,如爆炸。

如果你想听听这些不同的设置听起来像什么,当然,在你选择了每一个之后,点击图 15-6 左上角显示的音频传输按钮中的绿色播放(向右的三角形)。你会看到 32,000 频率听起来就像 44,100 频率,22,050 频率也是如此。16,000 或 11,025 的频率速率听起来并不“明亮”,但仍然可用,因此我使用 11,025 的速率,以获得甚至 4 倍的数据下采样。这是因为均匀的 2 倍(100%)或 4 倍(200%)缩减采样将始终提供最佳结果。这是因为所涉及的数学没有留下“部分”样本(或像素,因为同样的概念适用于成像)。

接下来,让我们使用文件➤导出工作流程。如果您想在菜单上看到这个,这个菜单顺序可以在图 15-1 中看到,并且显示在导出选择选项上方的文件菜单上。在图 15-7 所示的“导出文件”对话框中,您可以使用不同的文件名保存新版本的文件,这样,left.wav 文件中既有原始的未压缩数据,也有 left 立体声. wav 文件中的新的压缩(立体声)数据。将该文件命名为左立体声,点击保存按钮,将其保存为未压缩的 16 位 PCM WAV 文件,如图 15-7 左侧所示。

A978-1-4842-0415-3_15_Fig7_HTML.jpg

图 15-7。

Use the File ➤ Export dialog, name the file leftstereo, Save the file, then use File ➤ Open to check its file size

接下来我们要做的是使用图 15-3 中显示的相同工作流程,并使用文件➤打开菜单序列,打开选择一个或多个音频文件对话框,如图 15-7 右侧所示,这将允许我们将鼠标悬停在 leftstereo.wav 文件上,并看到其大小为 28.0 KB,比原始的 112 KB 源文件大小少四倍,正如我们所预期的那样!

因此,我们将这个表示单词“left”的音频文件的内存需求从 1/9 兆字节(112 KB)减少到 1/36 兆字节(28KB)。这意味着您可以拥有 36 个这样大小的音频资产,并且仍然只使用 1 兆字节的系统内存!当我创建其他五个音频资产时,这一个被证明是最大的,最小的(up 和 s,你可能已经猜到了)每个都不到 4KB!

我们数字音频优化工作流程的最后一步是将这些数据从立体声文件转换为单声道文件。我们这样做是因为我们的游戏音频资产不需要同一个口语词的两个副本。大部分游戏音频特效也是如此,比如激光爆破、爆炸;单声道音频在这些类型的音频音效情况下工作得很好。这一点尤其正确,因为 JavaFX AudioClip 类及其声相和平衡功能还允许我们使用单声道数字音频资源模拟立体声效果,如果我们愿意的话。

这还会将我们的数据占用空间再减少 100%,为我们提供 14KB 的音频文件。我们可以将 72 个这种大小的数字单声道音频资产放入 1 兆字节的系统内存中,因此使用单声道(单声道)音频资产而不是立体声数字音频资产是一件非常好的事情,这也是我们接下来要讨论的原因。

立体声与单声道音频:再减少 100%的内存占用

我们的数字音频优化工作流程的最后一个阶段是将我们的数字音频数据从使用立体声音频资产转换为单声道音频资产。我们将这样做,因为在这种情况下,我们不需要为我们的游戏音频资产复制两个相同的口语单词。对于大多数游戏相关的数字音频特效来说也是如此,比如激光爆破和爆炸。在这些类型的音频音效环境中,单声道音频的效果与立体声音频一样好。这一点尤其正确,因为 JavaFX AudioClip 类为开发人员提供了音频平移和平衡功能。这将允许开发者使用单声道音频资产来模拟立体声效果。Audacity 能够将立体声音频资产(两个声道,一个左声道和一个右声道)数据组合成一个听起来相同的单声道音频资产。

如果我们使用图 15-8 中的菜单序列所示的 Audacity Tracks ➤立体声音轨转单声道算法,将我们的两个立体声音轨合并成一个单声道音轨,它将会再减少 100%的数字音频数据占用空间,为我们提供一个 14KB 的音频文件。我们可以将 72 个这样大小的数字音频资产放入 1 兆字节的系统内存中。事实上,通过使用我在本章的这一节中展示的(未压缩的,不多不少的)数字音频数据占用优化,我已经成功地将所有六个数字音频资产放入了不到 62 KB 的内存占用中。

A978-1-4842-0415-3_15_Fig8_HTML.jpg

图 15-8。

Use the Tracks ➤ Stereo Track to Mono algorithm to combine the stereo samples into one Mono sample

在使用这种立体声到单声道的算法之前,我想让你做的是点击 Audacity 顶部的 Play transport 按钮,仔细听几次立体声音频资源。接下来,使用图 15-8 所示的菜单序列,调用立体声到单声道的合并算法。在您看到单个单声道音频资产后,您可以向前看,并在图 15-9 中看到,再次点击播放传输按钮,然后听音频样本,现在它是单声道的,看看您是否可以检测到任何差异。

使用专门的音效,这种差异甚至更难察觉(如果你能察觉的话)。Audacity 2.0.6 软件包是一个完全专业的数字音频编辑、增甜和音效程序,正如你在这里看到的,你可以使用该软件的正确工作流程来实现专业的游戏音频开发结果,这就是为什么我让你在第一章中下载并安装它。为了确保您拥有最强大的 Audacity 2.0.6 版本,请确保您下载了所有的 LADSPA、VST、Nyquist、LV2、LAME 和 FFMPEG 插件,然后将它们安装在 C:\ Program Files \ Audacity \ Plug-Ins 文件夹中,并重新启动 Audacity。

准备编码:导出资产并将其复制到项目中

让我们使用文件➤导出菜单序列导出最终的单声道文件并将其命名为 leftmono.wav,如图 15-9 所示。您可以使用相同的工作流程记录其他五个文件,或者使用我创建的资产,如果您愿意的话。

A978-1-4842-0415-3_15_Fig9_HTML.jpg

图 15-9。

Again use the File ➤ Export menu sequence and name the file leftmono and select the WAV 16-bit PCM

使用操作系统文件管理实用程序将六个音频资产复制到 InvinciBagel/src 文件夹中,如图 15-10 所示,以便我们在本章的剩余部分编写代码时参考。

A978-1-4842-0415-3_15_Fig10_HTML.jpg

图 15-10。

Use your file management software to copy the six .WAV files into the InvinciBagel/src project folder

现在我们准备好回到 Java 8 编码中。首先,我们将在 InvinciBagel.java 类中添加代码,以实现六个 AudioClip 对象,这些对象将引用我们的音频资产,然后,在 Bagel.java 类中,我们将在一个新的。playAudioClip()方法,我们将把它添加到我们的 Bagel 类的 primary。update()方法。

向 InvinciBagel.java 添加音频:使用 AudioClip

要实现 AudioClip 声音引擎,我们需要做的第一件事是声明六个私有 AudioClip 对象,在 InvinciBagel.java 类的顶部使用一个复合声明语句。我将用下面一行 Java 代码将这些声音命名为 0 到 5,如图 15-11 所示:

private``AudioClip

如图 15-11 所示,您必须使用 Alt-Enter 工作流程,并选择“为 javafx.scene.media.AudioClip 添加导入”选项,并让 NetBeans 8 为您编写 AudioClip 类导入语句。

A978-1-4842-0415-3_15_Fig11_HTML.jpg

图 15-11。

Add the private AudioClip compound declaration statement for your iSound0 through iSound5 objects

引用音频剪辑资源:使用 java.net.URL 类

与 JavaFX 中的图像对象(可以使用简单的正斜杠字符和文件名来引用)不同,数字音频资产不容易引用,需要使用 URL 类,它是 java.net(网络)包的一部分。URL 类用于创建 URL 对象,该对象提供统一资源定位符(URL)文件引用,它本质上是指向“数据资源”的“指针”,而“数据资源”通常是新的媒体资产,在我们的 Java 8 游戏开发中,它是/src 文件夹中的一个 WAVE 音频文件。

像 AudioClip 类一样,URL 类也是临时编写的,以提供 URL 对象,正如您从 Java 类层次结构中看到的那样,它看起来像下面这样:

java.lang.Object

> java.net. URL

我们实现六个 AudioClip 对象的第二步是使用复合声明语句在 InvinciBagel.java 类的顶部声明六个名为 iAudioFile0 到 iAudioFile5 的私有 URL 对象,这可以在图 15-12 中看到,看起来像下面的单行 Java 代码:

private``URL

如图 15-12 所示,您必须再次使用 Alt-Enter 工作流程,并选择“为 java.net.URL 添加导入”选项,并再次让 NetBeans 8 为您编写 URL 类导入语句。

A978-1-4842-0415-3_15_Fig12_HTML.jpg

图 15-12。

Add a private URL compound declaration statement for the iAudioFile0 through iAudioFile5 URL objects

现在我们准备编写加载 URL 对象的 Java 代码,然后使用这些代码实例化我们的 AudioClip 对象。为了在该类中使用自定义方法来维护我们的高级代码组织,让我们将 loadAudioAssets()方法添加到我们的。start()方法接下来,然后创建一个私有的 void loadAudioAssets()方法来保存我们的 AudioClip 相关代码,以防我们将来要在我们的游戏中添加六个以上的数字音频资产。

值得注意的是,由于 AudioClip 类在音高移动和 2D 空间(从左到右)音频移动方面的多功能性,您应该不需要像您想象的那样多的音频资源,因为您甚至可以将六个设计良好的音频资源变成数百种不同的游戏音效。

正在添加您的音频资产加载方法:。loadAudioAssets()

在你的 start()方法中创建一个方法调用,如图 15-13 所示,名为loadAudioAssets() ;,并在loadImageAssets();方法调用之前按逻辑方法顺序放置它。为了消除红色波浪错误突出显示,在 createSceneEventHandling()方法之后添加一个private void loadAudioAssets(){}空方法。通过这种方式,您的数字音频资源将在您的 KeyEvent 处理设置完成后,并且在您的数字图像资源被引用和加载到内存之前被引用和加载到内存中。

A978-1-4842-0415-3_15_Fig13_HTML.jpg

图 15-13。

Create the private void .loadAudioAssets() method to hold the AudioClip object instantiation statements

在你的内心。loadAudioAssets()方法体,您将有两个 Java 代码语句,因为加载数字音频资产比引用数字图像资产要复杂一些。首先,您将使用 getClass()加载您的第一个 iAudioFile0 URL 对象。getResource()方法链。这个方法链用您想要使用的数字音频样本资源加载 URL(统一资源定位器)对象,并将为您的类对象执行此操作(在本例中,这是 InvinciBagel 类,因为这是我们编写此代码的 Java 类)。

您正在寻找的获取 URL 的资源位于。getResource()方法调用,并使用与您用于数字图像资源的资源引用相同的格式,在本例中是“/leftmono.wav”文件引用,它由。getResource()方法转换为二进制 URL 数据格式。该 Java 代码如图 15-14 所示,看起来像下面的 Java 方法体:

private void``loadAudioAssets()

iAudioFile0 =``getClass().getResource

iSound0 = new AudioClip(iAudioFile0``.toString()

}

有趣的是,第二行代码使用了。toString()方法调用,它将这个 URL 对象转换回 String 对象,这是用音频资源 URL 加载 AudioClip()构造函数方法所必需的。你可能在想:为什么不用iSound0 = new AudioClip("/leftmono");?您可以尝试这样做,但是,您必须使用“file:/Users/Users/my documents/netbeans projects/InvinciBagel/src/left mono . wav”对目录进行“硬编码”

我使用了 URL 对象方法,以便您能够从 JAR 文件内部引用这个音频文件,而不是使用上面显示的方法,该方法需要硬盘驱动器上的“绝对”位置。因此,这个 getClass()。getResource()方法链正在将“相对”引用数据添加到此 URL 对象中。InvinciBagel 类需要这个相对引用数据,以便能够从 NetBeans 8 项目 InvinciBagel/src 文件夹内部以及 Java 8 游戏应用的 JAR 文件内部引用 WAV 音频资源文件。

A978-1-4842-0415-3_15_Fig14_HTML.jpg

图 15-14。

Instantiate and load the URL object, and then use it inside of the iSound0 AudioClip object instantiation

接下来,使用您信任的程序员的快捷方式,将这两行代码复制并粘贴五次,这是创建其他五个 AudioClip 对象的简单方法。将 iAudioFile0 和 iSound0 上的零分别更改为 iAudioFile1 至 iAudioFile5 和 iSound1 至 iSound5。然后将 WAV 音频文件名引用分别改为 rightmono.wav、upmono.wav、downmono.wav、wmono.wav、smono.wav。您完成的 loadAudioAssets()方法应该看起来像下面的 Java 方法体,如图 15-15 所示:

private void loadAudioAssets() {

iAudioFile0``= getClass().getResource("/``leftmono.wav

iSound0 = new AudioClip(``iAudioFile0

iAudioFile1``= getClass().getResource("/``rightmono.wav

iSound1 = new AudioClip(``iAudioFile1

iAudioFile2``= getClass().getResource("/``upmono.wav

iSound2 = new AudioClip(``iAudioFile2

iAudioFile3``= getClass().getResource("/``downmono.wav

iSound3 = new AudioClip(``iAudioFile3

iAudioFile4``= getClass().getResource("/``wmono.wav

iSound4 = new AudioClip(``iAudioFile4

iAudioFile5``= getClass().getResource("/``smono.wav

iSound5 = new AudioClip(``iAudioFile5

}

因为我们已经将 AudioClip 对象设为私有,所以我们需要在 InvinciBagel 类中创建方法,可以使用方法调用从我们的 Bagel.java 类(以及稍后开发的其他类)中调用这些方法。

A978-1-4842-0415-3_15_Fig15_HTML.jpg

图 15-15。

Create more AudioClip objects referencing the rightmono, upmono, downmono, wmono, and smono files

使用您的源代码➤插入代码➤生成➤ Setters 工作过程打开生成 Setters 对话框,如图 15-16 所示,并选择 iSound0 到 iSound5 对象,这样 NetBeans 就创建了 6 个。setiSound()方法。

A978-1-4842-0415-3_15_Fig16_HTML.jpg

图 15-16。

Use the Generate Setters dialog and create six .setiSound() methods

现在我们准备使用这六个新的。NetBeans 为我们编写的 setiSound()方法,我们并不需要它,因为我们的音频剪辑是使用。loadAudioAssets()方法,来创建六个。事实上,我们确实需要 playiSound()方法来回放这六个数字音频资产。让我们接下来做那件事。

提供对音频剪辑的访问。playiSound()方法

我们将要做的是我认为是另一个程序员的捷径,但我没有复制和粘贴,而是使用 NetBeans 源代码➤插入代码函数为我要更改的 iSound 对象创建 Setter 方法。setiSound()到。playiSound()方法,这样我就不必键入所有这六个方法体。如图 15-17 所示,NetBeans 为我们创建了六个完整的方法体,我们所要做的就是移除方法参数区域内的 AudioClip iSound 引用,将 setiSound()改为 playiSound(),最后将this.iSound0 = iSound0;语句改为this.iSound0.play();。我们将为这六个人中的每一个人做这件事。setiSound()方法体,这将允许我们快速创建六个。playiSound()方法体。

A978-1-4842-0415-3_15_Fig17_HTML.jpg

图 15-17。

Edit these six .setiSound() methods, created by NetBeans, at the bottom of the InvinciBagel.java class

编辑过程相对简单:选中 setiSound 的 set 部分,在 set 上键入“play”;选择参数区域的内部(AudioClip iSound#),并按 delete 或 backspace 键删除它;最后,在 this.iSound 和 the 之间的方法内部,选择 Java 语句的“= isound”部分;分号并键入。改为播放()。完整的 Java 方法体如图 15-18 所示,应该如下所示:

public void playiSound0() {

this.``iSound0

}

public void playiSound1() {

this.``iSound1

}

public void playiSound2() {

this.``iSound2

}

public void playiSound3() {

this.``iSound3

}

public void playiSound4() {

this.``iSound4

}

public void playiSound5() {

this.``iSound5

}

A978-1-4842-0415-3_15_Fig18_HTML.jpg

图 15-18。

Turn .setiSound() methods into .playiSound() methods by adding calls to the AudioClip .play() method

触发了。java 中的 playiSound()方法。playAudioClip()方法

现在我们已经声明了,universal resource 定位(引用)并实例化了我们的 AudioClip 对象,并创建了。playiSound()方法允许我们从 InvinciBagel.java 类的“外部”触发这些数字音频样本,我们可以进入 Bagel.java 类,并编写一些允许我们触发这些音频对象的代码,以查看 AudioClip 类的工作情况。用我们现有的代码做到这一点的最好方法是使用我们用来移动我们的运动精灵对象的事件处理器代码,也允许我们为我们当前为游戏设置的每个按键事件触发这些声音中的一个。这就是为什么我用它们将被触发的键来命名这些文件。我们需要在 Bagel.java 类中做的第一件事,类似于我们在 InvinciBagel 类中做的,是对。update()方法引用了空的私有 void playAudioClip()方法。这个方法调用和空的方法体如图 15-19 所示。

A978-1-4842-0415-3_15_Fig19_HTML.jpg

图 15-19。

Create an empty .playAudioClip() method in the Bagel.java class and add a call to it inside of .update()

在 playAudioClip()方法体中,我们需要创建条件 if()结构,类似于我们在第一章 2 中为 sprite 移动所做的。我们将通过 invinciBagel.playiSound()对象引用和条件 if()语句内的方法调用,将按键事件处理(左、右、上、下、w、s)与说出每个按键(AudioClip 对象 iSound0 到 iSound5)的音频文件相匹配,如图 15-20 所示,使用以下 Java 代码:

private void``playAudioClip()

if(invinciBagel.isLeft())  { invinciBagel.playiSound0(); }

if(invinciBagel.isRight()) { invinciBagel.playiSound1(); }

if(invinciBagel.isUp())    { invinciBagel.playiSound2(); }

if(invinciBagel.isDown())  { invinciBagel.playiSound3(); }

if(invinciBagel.iswKey())  { invinciBagel.playiSound4(); }

if(invinciBagel.issKey())  { invinciBagel.playiSound5(); }

}

A978-1-4842-0415-3_15_Fig20_HTML.jpg

图 15-20。

Add conditional if() statements to the .playAudioClip() method that call the correct .playiSound() method

现在,我们已经将数字音频添加到了我们的游戏引擎基础设施中,接下来我们所要做的就是将画外音替换为声音效果,我们将完成游戏开发中的数字音频部分。如果您使用运行➤项目工作流程并测试代码,您会发现使用 JavaFX 可以快速触发音频样本。

摘要

在第十五章中,我们将注意力从游戏在我们眼中的样子(视觉)转移到了它在我们耳中的声音(听觉),并花了一章来实现 AudioClip 类的代码,这样我们就可以触发数字音频音效。

首先,我们看一下 JavaFX AudioClip 类。我们了解了为什么它非常适合用于我们的音频游戏开发,包括短音乐循环(使用不定常数设置)或快速音效。

接下来,我们学习了如何使用 Audacity 2.0.6 优化数字音频资产。我们学习了优化数字音频的工作过程,以便只占用十几千字节的系统内存,以及如何优化音频,以至于我们甚至不必应用压缩,特别是因为 Java 8 支持的音频压缩编解码器是“有损”编解码器,一旦音频数据被解压缩到系统内存中,就会降低音频数据的质量。

最后,我们在 InvinciBagel.java 类中使用. loadAudioAssets()方法实现了 AudioClip 对象,然后创建了六个。playiSound()方法来允许外部类访问和播放这些数字音频资产。我们还在 Bagel.java 类中添加了一个. playAudioClip()方法,该方法根据按下的键触发音频样本。在下一章,我们将看看如何在我们的游戏代码中加入碰撞检测。

十六、碰撞检测:为游戏角色创建 SVG 多边形,并编写代码来检测碰撞

现在,我们已经为游戏音效和短循环音乐实现了数字音频,并且实现了创建运动精灵(角色)和固定精灵(道具)的数字图像相关类,我们现在将深入研究新媒体的另一个主要类型或领域:矢量。矢量用于 2D 插图软件(InkScape)以及 3D 建模和动画软件(Blender),并使用数学来定义用于创建 2D 或 3D 艺术品的形状。这使得 vectors 成为定义自定义碰撞形状的完美解决方案,它可以完美地包围我们的 sprite,因此我们不使用复杂的像素阵列来检测碰撞,而是使用更简单(内存和处理器效率更高)的碰撞多边形,它将完美地包围我们的 sprite。

幸运的是,javafx.scene.shape 包中的 JavaFX SVGPath 类允许我们使用自定义 SVG 路径(形状)数据来定义 sprite 碰撞边界。不仅如此,这个 SVGPath 类(object)也非常高效,因为它没有属性,只有几个方法和一个简单的 SVGPath()构造函数方法,正如你在第八章中已经看到的。这意味着使用 SVGPath 类(对象)相对来说是内存和处理器高效的。事实上,我们需要使用的唯一方法是。setContent()方法,我们在第八章中的 Actor 类构造函数方法中使用了它。因为我们将在游戏启动时做一次,SVG 路径碰撞数据将被加载到系统内存中,并将在我们的碰撞检测例程中使用,我们将在本章后面的部分中进行处理。

在本章中,我们将详细了解如何定义 SVG 路径数据。这是由万维网(W3)联盟(也称为 W3C)规定的,该组织定义了 HTML5。该规范在他们的 w3.org 网站上,位于 http://www.w3.org/TR/SVG/paths.html 网址,如果你想查看更多细节。

在我们详细了解了 SVG 或可缩放矢量图形(如果您想知道这代表什么)数据格式之后,我们将开始使用流行的 GIMP 2.8 数字图像编辑软件,您在第一章中安装了该软件,并学习如何创建碰撞多边形。我们还将了解如何使用 PhysEd (PhysicsEditor)碰撞多边形生成软件。这是来自一家名为 CodeAndWeb GmbH 的公司,该公司生产专业的、价格合理的游戏资产创建软件。

我们将使用 GIMP 2.8 完成碰撞多边形向量资源创建和优化过程,使用快速和脏工作过程,这允许 GIMP 100%为您创建碰撞多边形形状,以及一个更“复杂”的工作过程,其中您可以使用 GIMP 的路径工具手动创建自己的碰撞多边形。在我们学习了如何创建与 JavaFX SVGPath 类兼容的碰撞多边形之后,我们将在本章的剩余部分讨论 Java 8 游戏编程,创建碰撞检测引擎(代码),它将允许我们检测 InvinciBagel 角色何时与游戏环境中的任何其他演员对象(场景和舞台对象)接触。这是这本书开始变得更高级(有用)的地方。

SVG 数据格式:手工编码矢量形状

SVG 数据字符串中的数字(2D 空间中的 X,Y 数据点位置)数据可以使用十种不同的字母。每个版本都有大写(绝对参考)和小写(相对参考)版本。我们将使用绝对参考,因为我们需要这些数据点与 sprite 图像中的像素位置匹配,我们将“附加”或分组这些 SVG 路径数据字符串,以提供碰撞检测数据指南。正如您在表 16-1 中看到的,SVG 数据命令为您定义 Java 8 游戏开发的自定义曲线提供了很大的灵活性。您甚至可以将所有这些可扩展的矢量命令与您的 Java 8 代码相结合,以创建以前从未体验过的交互式矢量(数字插图)艺术作品,但由于这是一个游戏开发标题,我们将使用这些信息来开发高度优化的碰撞多边形,这些多边形仅使用十几个 X,Y 数据点(介于 12 和 15 之间)来定义一个相对详细的碰撞多边形,它将包含我们的精灵图像,并提供高度精确的(至少从游戏玩家的角度来看)碰撞结果。

表 16-1。

SVG data commands to use for creating SVG path data string (source: Worldwide Web Consortium w3.org)

SVG 命令名 标志 类型 参数 描述
动起来了 M 绝对的 x,Y 使用绝对坐标在 X,Y 处定义路径的起点
动起来了 m 亲戚 x,Y 使用相对坐标在 X,Y 处定义路径的起点
帕尔帕思 Z 绝对的 没有人 通过从最后一个坐标到第一个坐标画一条线来闭合 SVG 路径
帕尔帕思 z 亲戚 没有人 通过从最后一个坐标到第一个坐标画一条线来闭合 SVG 路径
利托 L 绝对的 x,Y 从当前点到下一个坐标绘制一条线
利托 l 亲戚 x,Y 从当前点到下一个坐标绘制一条线
水平直线 H 绝对的 X 从当前点到下一个坐标绘制一条水平线
水平直线 h 亲戚 X 从当前点到下一个坐标绘制一条水平线
垂直线条 V 绝对的 Y 从当前点到下一个坐标绘制一条垂直线
垂直线条 v 亲戚 Y 从当前点到下一个坐标绘制一条垂直线
弯曲的 C 绝对的 X,Y,X,Y,X 从当前点到下一点绘制一条三次贝塞尔曲线
弯曲的 c 亲戚 X,Y,X,Y,X 从当前点到下一点绘制一条三次贝塞尔曲线
短平滑曲线 S 绝对的 X,Y,X,Y 从当前点到下一点绘制一条三次贝塞尔曲线
短平滑曲线 s 亲戚 X,Y,X,Y 从当前点到下一点绘制一条三次贝塞尔曲线
二次贝塞尔曲线 Q 绝对的 X,Y,X,Y 绘制二次贝塞尔曲线(当前点到下一点)
二次贝塞尔曲线 q 亲戚 X,Y,X,Y 绘制二次贝塞尔曲线(当前点到下一点)
短二次贝塞尔曲线 T 绝对的 x,Y 绘制一个短的二次贝塞尔曲线(当前点到下一点)
短二次贝塞尔曲线 t 亲戚 x,Y 绘制一个短的二次贝塞尔曲线(当前点到下一点)
椭圆弧 A 绝对的 rX,rY,红色 从当前点到下一点绘制椭圆弧
椭圆弧 a 亲戚 rX,rY,红色 从当前点到下一点绘制椭圆弧

了解如何使用强大的 SVG 数据“路径绘制”命令的最佳方式是开始学习创建基于 SVG 数据的碰撞多边形路径的工作过程。我们将学习如何使用 GIMP 2.8.14 实现这一点,使用一种“快速而简单”的方法,让 GIMP 完成 100%的路径创建工作。之后,我们将学习另一种使用 GIMP 手工完成这项工作的方法。第二种方法为您提供了 100%的路径创建控制。在这个 SVG 主题的最后,我还将向您展示如何使用另一个专用的碰撞和物理开发工具 PhysEd,它来自一家创新的游戏开发软件工具公司,位于德国乌尔姆,名为 CodeAndWeb GmbH。

创建和优化碰撞数据:使用 GIMP

对我们来说幸运的是,流行的开源数字图像编辑软件包 GIMP(目前版本为 2.8.14)具有足够的路径功能,并且能够将其导出为 SVG 数据集,从而允许该软件用作成熟的碰撞多边形创建工具。GIMP 软件使用路径工具支持路径(显示为带有贝塞尔手柄的曲线数据点旁边的老式钢笔笔尖),您可以在图 16-1 中看到(工具箱图标第二行中左起第二个图标)。GIMP 支持路径的原因是,它通常不是一个矢量(基于路径)软件包,而是一个光栅(基于像素)软件包,因为创建路径然后将其“转换”到选择区域的能力对于数字图像合成技术人员来说非常有用。事实上,我们将使用 GIMP 的能力来做与此相反的事情:也就是说,将算法创建的选择集转换成路径数据,这将形成我们完全自动化的、“快速而肮脏的”SVG 路径数据创建过程的基础。我们首先来看看这个,因为它快速、简单、有效(但是“数据量很大”)。让我们从启动 GIMP 开始,并使用文件➤打开过程来打开您的项目/src 文件夹中的 sprite1.png png 32 数字图像资产。正如你在图 16-1 中看到的,我已经放大了图像。这允许我看到我们将要使用 GIMP 工具箱创建的碰撞数据路径(最初,这将是一个选择)。

A978-1-4842-0415-3_16_Fig1_HTML.jpg

图 16-1。

Launch GIMP, and use File ➤ Open to open the sprite1.png file

点击 GIMP 模糊选择工具(图标看起来像火花或魔杖),它显示为选中状态(按下,如“陷入”,而不是悲伤),如图 16-2 。单击图像中显示棋盘图案的任何区域,这在任何数字成像软件包以及许多其他类型的软件(例如 CodeAndWeb GmbH 的游戏资产创建软件包)中总是表示透明的。正如你所看到的,在图 16-2 中,你会在图像的透明区域周围得到一个动画的“爬行的蚂蚁”轮廓,因为透明区域刚刚被模糊选择工具算法选中,该算法选择连续颜色值的区域。

A978-1-4842-0415-3_16_Fig2_HTML.jpg

图 16-2。

Click in a transparent area using Fuzzy Select Tool to select Actor

在这种情况下,模糊选择工具选择透明度值。事实上,您应该注意到,您必须选择一个选项来选择图像中的透明区域,使用模糊选择工具选项对话框,该对话框可以在工具箱工具选项浮动调色板的底部看到。正如你所看到的,我已经指示 GIMP 通过选择“选择透明区域”选项,不仅要“查看”图像中的 RGB 板,还要查看 Alpha 板。

现在我们有了包含除了我们想要选择的 sprite 字符之外的所有内容的选择集,我们需要找出一种方法来获得与我们现在选择的内容相反的内容。无可否认,选择透明区域要比选择无敌手角色的不同颜色区域容易得多!幸运的是 GIMP 有一个算法可以完全反转选择,选择所有没有被选中的,取消选中所有被选中的。

在 GIMP 2.8 选择菜单下,找到反转选项,或者使用 Control+I 键盘快捷键,显示在反转选项旁边的菜单上,所有这些都可以在图 16-3 的左半部分看到。一旦你这样做了,你会注意到动画“行进的蚂蚁”不再围绕数字图像的正方形周长(范围)行进,它们只围绕 InvinciBagel 字符,这意味着选择已经被反转,我们想要选择的现在包含在选择集中。

下一步是将此光栅像素选择集(数组)转换为矢量(路径)数据表示。我们在 GIMP 中这样做的方法是使用选择➤路径菜单序列,如图 16-3 的右半部分所示。这将把 InvinciBagel 字符周围的选择转换为矢量路径数据,这就是我们想要剔除的。

A978-1-4842-0415-3_16_Fig3_HTML.jpg

图 16-3。

Invert selection using Select ➤ Invert, so only Actor is selected (left); use Select ➤ To Path to convert to path

一旦你将像素选择转换成矢量路径,你将得到如图 16-4 所示的结果。

A978-1-4842-0415-3_16_Fig4_HTML.jpg

图 16-4。

Right-click on Selection Path in Paths Palette, and Export Path

在另一个浮动调色板中,点击路径选项卡,如图 16-4 右侧所示。GIMP 有两个主要的浮动工具窗口;一个是 GIMP 工具箱,包含工具、选项、画笔、图案和渐变,另一个包含四个选项卡,代表您的数字图像的合成层、通道、选择路径,甚至还有一个撤销缓冲区,它为您提供了自启动以来在 GIMP 中所做的每个“移动”的“历史记录”。

选择名为“选择”的路径(名为“选择”的路径图层将变为蓝色)。接下来右键单击名为 Selection 的路径,在您可以对选择路径进行操作的菜单底部,您会看到一个导出路径菜单选项。这是另一个关键的 GIMP 算法,使我们能够创建和输出碰撞多边形的工作过程。

选择这个导出路径选项将为我们导出当前 InvinciBagel 字符选择路径数据,作为包含 SVG 数据的基于文本的(XML)文件,这是我们需要在 Bagel()构造函数方法调用中使用的。该数据将包含在第一个字符串 SVGdata 参数中,并将替换我们到目前为止一直用作占位符的“虚拟数据”。

一旦你调用导出路径菜单选项,你会看到导出路径到 SVG 对话框,如图 16-5 所示。如你所见,我选择了对话框底部的“导出活动路径”选项,因为我只想要一个碰撞多边形路径数据对象,我将该文件命名为 sprite1svgdata.svg,并将其保存在我的 C:\ Clients \ BagelToons \ InvinciBagelGame \ Shape _ Data 文件夹中。

A978-1-4842-0415-3_16_Fig5_HTML.jpg

图 16-5。

Select “Export the active path” option, in the Export Path to SVG dialog, and name the file sprite1svgdata

工作流程的下一步是在文本编辑器中打开我们在图 16-5 中导出的 sprite1svgdata.svg 文件:对于 Windows 用户,这将是记事本,对于 Macintosh 用户,这将是文本编辑,对于 Linux 用户,这可能是 vi 或 vim。

图 16-6 显示了 Windows 记事本的文件打开对话框,你可能会注意到,默认情况下,记事本会查找。txt(文本文件类型)文件扩展名,表示该文件中有文本数据。但是,在。svg 文件扩展名(类型),以 XML 数据的形式。我们需要使用对话框右下角的下拉菜单来告诉记事本查看所有可用的文件,并允许我们决定哪些文件包含文本数据,哪些不包含文本数据。

一旦你选择了“所有文件”选项,你会看到 sprite1svgdata 文件,你可以双击它打开文件(或者单击它选中它,然后点击打开按钮)。

A978-1-4842-0415-3_16_Fig6_HTML.jpg

图 16-6。

Use a text editor (like Notepad) and select “All Files” option and Open sprite1svgdata

如果您想在 Java 代码中使用这些数据,只需选择引号中 d =(数据等于)后面的部分,包括引号,您需要用引号来表示一个字符串,如图 16-7 中蓝色部分所示。

A978-1-4842-0415-3_16_Fig7_HTML.jpg

图 16-7。

Select the SVG data (including quote characters) for the SVG Path representation, and use it in your code

如图 16-7 所示,有 32 乘以 3 的数据对,接近 100 个数据点,这是一个很大的数据处理量,尤其是当两个都定义了 SVGPath 碰撞检测数据的对象发生碰撞时!

如果你看一下图 16-4 中的 InvinciBagel 角色,我们真的应该能够使用 14 到 16 条线定义一个围绕角色的碰撞多边形,这些线完美地封装了角色,并且使用少很多倍(事实上,少 16 倍,正如你将在本章的下一节中看到的)的数据,这相当于少 16 倍的内存,少 16 倍的处理(1600%的效率)开销,如果不是更多的话。

因此,我想向您展示在 GIMP 中定义您自己的自定义碰撞多边形 SVG 数据形状对象的更复杂的工作过程,使用尽可能少的线条(数据点之间)。这实质上等同于碰撞检测 SVG 路径形状数据优化,因为我已经向您展示了其他新媒体元素的数据优化工作流程,现在没有理由停止这种趋势!所以接下来,让我们在本章的下一节详细看看如何将我们的碰撞检测 SVG 路径形状数据开销减少 1600%。

创建优化的碰撞多边形:使用路径工具

让我们重新开始,要么关闭前一个项目,并使用文件➤打开重新打开 sprite1.png 文件,或删除路径调板中的前一个选择路径。这一次,而不是模糊选择工具,使用路径凳子,并在工具箱的选项(底部)部分选择设计编辑模式,并选择多边形复选框选项。这将使我们的线条保持漂亮和笔直,就像你在 Blender 等 3D 建模包中看到的多边形一样。在 InvinciBagel 角色的头发中单击,然后在他的左肩上单击另一个点,如图 16-8 所示。这将自动为您在两点之间绘制一条线段(折线)。单击肘部的第三个点、手腕的第四个点、脚趾的第五个点、膝盖的第六个点、大腿的第七个点、脚后跟的第八个点,以此类推,使用直线创建一个轮廓,该轮廓完美地包含 InvinciBagel 运行状态。

A978-1-4842-0415-3_16_Fig8_HTML.jpg

图 16-8。

Open sprite1.png, select the Paths Tool, and start to draw a simple Path

正如你在图 16-9 中看到的,我只用了 15 个点创建了碰撞多边形。您可以让多边形保持开放,只需添加您在本书第一部分学到的 Z 字符,即可创建一个闭合的多边形。正如你在图 16-9 的右侧所看到的,多边形与精灵非常一致,所以在游戏过程中,碰撞的结果看起来像是发生在精灵的像素上,而不是碰撞多边形上,尽管碰撞多边形路径数据在 GIMP 中是可见的,但在游戏过程中是不可见的。

通过选择并右键单击图 16-9 中间显示的未命名路径,导出您的手绘路径,并使用导出路径菜单选项将其导出为文件 sprite1svghand.svg,就像您在图 16-5 中所做的一样。如果你想在 GIMP 中命名路径,你可以双击路径对话框中的路径名,如果你愿意,给它一个名字。如果您想在原生 GIMP 中保存您的工作。xcf 文件格式,您也可以使用文件➤保存菜单序列,并给文件命名,如 sprite1svgpath15points.xcf

A978-1-4842-0415-3_16_Fig9_HTML.jpg

图 16-9。

Insert 15 strategically placed points to define a collision shape and use the Export Path to export SVG data

接下来使用你的文本编辑器(比如记事本)的文件打开对话框,如图 16-6 所示,打开最新的 sprite1svghand.svg 文件,这样你就可以看到相对于 GIMP 模糊选择工具选择工作流程在本章第一节为我们提供的近 100 个数据点对,你保存了多少数据。

正如您在图 16-10 中看到的,有 14 乘以 3 (42)个数据对,这还不到我们在之前的工作流程中拥有的数据量的一半。这很奇怪,因为理论上应该只有 15 个数据对,所以让我们做一些调查工作,看看这些数据可能会发生什么。

正如 NetBeans 8 并不总是利用“最佳”或正确的工作流程来满足我们特定的 Java 8 游戏创作目标一样,我们也有可能在 GIMP 上遇到相同类型的问题。如果是这种情况,我们将不得不自己动手,并通过添加我们自己的定制工作流程步骤来进行干预,以实现我们知道的 Java 8 游戏开发所需的精确结果。幸运的是,这个 SVG 路径数据使用 XML“容器”中的文本数据,所以如果需要,我们应该能够将自己的步骤添加到这个工作流程中。最后,你会发现开发一个专业的 Java 8 游戏并不像玩游戏本身那么容易!

在对 SVG 数据(d=)进行更仔细的检查后,您首先会看到,所使用的数字精度水平对于这个应用来说是不必要的。因为我们试图将碰撞数据点精度与像素精度相匹配,所以我们可以使用整数,而不是用于 SVG 数据的浮点数。让我们自己动手,将这些浮点数的小数部分向上或向下舍入到最接近的整数。这样做将消除当前使用的浮点精度。这将是我们的第二轮优化(第一次是手绘多边形)。另外,在数据的最末端添加一个“Z”闭合路径命令,如图 16-10 所示,形成一个闭合的多边形。

A978-1-4842-0415-3_16_Fig10_HTML.jpg

图 16-10。

Open SVG Path data in a text editor and add a Z “close polygon” command to the end of the data

正如你在图 16-11 中看到的,第一轮优化会给你明显更少的数据,大大简化了碰撞数据。然而问题是,如果我们把这个 SVG 数据放回 GIMP 2.8,你的碰撞多边形看起来还会完全一样吗?接下来让我们仔细看看可以回答这个问题的工作流程。将该文件保存为 sprite1svghandintegerxml.txt,这样我们在需要时就有了优化的数据,如 16-11 所示。

A978-1-4842-0415-3_16_Fig11_HTML.jpg

图 16-11。

Remove the floating point values, by rounding them up or down, to the nearest integer values

在 GIMP 中优化 SVG 路径冲突形状:使用导入路径

让我们再次开始,要么关闭前一个项目,并使用文件➤打开重新打开 sprite1.png 文件,或删除路径调板中的前一个选择路径。正如你在图 16-12 中看到的,路径调色板是空的,我们可以在调色板的空白区域内右击,并选择底部的导入路径选项。

A978-1-4842-0415-3_16_Fig12_HTML.jpg

图 16-12。

Right-click in empty Paths palette, select Import Path option

一旦你点击图 16-12 中的导入路径菜单项,你将从 SVG 对话框中得到导入路径。选择“所有文件()"下拉菜单选项,然后点击 sprite1svghandintegerxml.txt 文件然后点击打开按钮,如图 16-13 所示。这将在 GIMP 中打开编辑过的整数碰撞路径数据。

A978-1-4842-0415-3_16_Fig13_HTML.jpg

图 16-13。

Open the sprite1svghandintegerxml.txt file

如图 16-14 所示,碰撞多边形 SVG 数据的整数表示与浮点表示相同,如图 16-9 所示。由于在 SVG 数据字符串的末尾添加了一个 SVG“Z”命令,碰撞多边形现在是闭合的。我们正在让我们的碰撞数据更加优化!

A978-1-4842-0415-3_16_Fig14_HTML.jpg

图 16-14。

Collision polygon is correct using integer data (left); deselect visibility in Layers Palette (right) to see SVG

您可能已经注意到,除了起点和终点,每个点都有三个相同的 X,Y 数据点坐标值,对于起点和终点,我们有数据点对而不是数据点三元组。如果你回想一下表 16-1 ,唯一使用三元组(三个)X,Y 数据点对的 SVG 命令是 C 或三次贝塞尔(样条)曲线。果然,如图 16-15 所示,在 M (moveto) opening SVG 命令的正下方是 C 命令。这解释了为什么 GIMP 在你的多边形的每个点上放三个数据点。所有数据点三元组具有相同值的原因是因为我们检查了 GIMP 中的多边形选项。这会将样条曲线控制手柄放在“远离”或看不见的地方,直接放在 X,Y 数据点的顶部。这定义了零曲率,或如图 16-14 所示的正方形多边形结构。

A978-1-4842-0415-3_16_Fig15_HTML.jpg

图 16-15。

Remove duplicate point data for those points in the interior of the collision polygon, to further optimize

让我们导出去掉了三元组的 SVG 数据,如图 16-15 所示,并使用我们新发现的导入路径工作流程,看看 GIMP 中的结果是什么样的,主要是为了学习,因为我们还没有完全完成优化工作流程。

将图 16-15 所示的 XML 数据保存为 sprite 1 svghanditegerxmlpromized . txt,然后使用图 16-12 和 6-13 所示的相同导入路径工作流程,将这个进一步优化的 SVG 数据集导入到 GIMP 中。正如你在图 16-16 中看到的,从数据集中移除那些三次贝塞尔曲线控制柄也移除了你的碰撞多边形的多边形性质。因此,我们需要对我们的 SVG 数据做一些进一步的工作来纠正这一点。

A978-1-4842-0415-3_16_Fig16_HTML.jpg

图 16-16。

Import the latest SVG data with data triplets deleted into GIMP to see curve without tension handle data

将这些不一致的(精灵轮廓的)曲线变成我们之前的相同碰撞多边形的解决方案非常简单,你可能已经猜到是什么了。因为我们希望数据点之间是直线,所以我们需要将这个“C”改为“l”。这将把 curveto SVG 命令变成 lineto SVG 命令。

正如你在图 16-17 中看到的,我们的碰撞多边形数据几乎在我们预期的地方,包含 16 个数据点对和一个 Z 闭合命令来创建一个 16 边碰撞多边形。我们可以删除第一个重复的数据对,将数据集减少到 15 个数据点对,这是我们在 GIMP 中“制定”的。接下来,让我们再次使用 GIMP 中的导入路径工作流程,看看我们是否得到了如图 16-14 所示的相同的正方形多边形结果。

A978-1-4842-0415-3_16_Fig17_HTML.jpg

图 16-17。

Change your SVG Path data from using the C (curve) data type to the L (line) data type representation

最终的 SVG 数据集如图 16-18 所示,您可以将该数据复制并粘贴到您的 Java 代码以及一个空的记事本文档中,并将其保存为自己的文件,名为 iBshape1svg.txt,这样我们接下来就可以做一些数学运算,看看我们从自动 GIMP 创建的碰撞多边形到我们手动优化的自定义碰撞多边形减少了多少数据。如果您在 GIMP 中导入图 16-18 所示的碰撞数据,您将得到图 16-14 所示的所需碰撞多边形,有 15 个数据点对,而不是 100!

A978-1-4842-0415-3_16_Fig18_HTML.jpg

图 16-18。

To use optimized SVG Path data set, select the part after d= (including quotes), and paste into the code

确定冲突数据优化:计算 SVG 数据的数据足迹

让我们算出我们的数据占用优化百分比。从图 16-18 所示的文件中挑选出来的 SVG 数据,并放入我们将在 Bagel()构造函数方法调用中使用的格式中,可以在图 16-19 的顶部看到。

A978-1-4842-0415-3_16_Fig19_HTML.jpg

图 16-19。

Open folder in your workstation containing the SVG data, and mouse-over file to see the number of bytes

将鼠标悬停在两个 iBshape1svg 文件上,如图 16-19 和 16-20 所示,并获取文件大小,或者右键单击文件,并使用属性对话框来查找文件中的字节数。这应该是 97 字节和 1605 字节。

A978-1-4842-0415-3_16_Fig20_HTML.jpg

图 16-20。

Mouse-over the original Fuzzy Select Tool generated SVG data to get the data footprint, and do the math

要找出数据占用优化,只需将这些数字相互除即可。如果你用 97 除以 1605 (97/1605=0.0604),你会发现 97 是 1605 的 6%,数据占用空间减少了 94%。如果将 1605 除以 97 (1605/97=16.546),则意味着文件减少了 16.546 倍,数据占用空间减少了 16.55 倍。这是计算器上彼此的倒数(1/x ),所以你可以从任何一个方向看。因此,1605 字节比 97 字节多 1,655%的数据,即 97 字节是 1605 字节的 6%(或少 94%的数据)。不管你怎么看,你已经为你的游戏节省了大量的内存、处理和 JAR 文件数据,而这仅仅是为了一个精灵!请记住,一旦您在游戏逻辑中实现了碰撞检测,优化您的碰撞多边形以使用少于 16 倍的内存和少于 16 倍的 CPU 处理开销对您的游戏流畅度非常重要,我们将在查看 CodeAndWeb 的 PhysEd 工具后进行这一操作。

创建和优化物理数据:使用 PhysEd

我想用几页纸向您展示 GIMP 的一种替代方案,它将物理和碰撞合并到一个统一的游戏开发工具中,相对于它为游戏开发所做的一切来说,它是非常便宜的。PhysicsEditor 或 PhysEd(或 PE)来自 CodeAndWeb GmbH,该公司由另一位致力于 iOS 游戏开发的作家 Andreas Loew 所有。让我们快速看一下如何使用这个专业的游戏开发工具来定义精灵的碰撞多边形,然后我们就可以开始碰撞检测编码了。使用绿色立方体 PE 图标安装并启动 PE,使用图 16-21 所示的导入精灵按钮打开你的 sprite1.png 文件,使用屏幕底部的缩放滑块放大 600%,就像我们在 GIMP 中做的一样。

A978-1-4842-0415-3_16_Fig21_HTML.jpg

图 16-21。

Launch PhysicsEditor and use the Add Sprites button to open the sprite1.png file and zoom into it 600%

接下来点击精灵编辑区域顶部的追踪器图标(它是中间的图标,看起来像魔棒),这将打开追踪器对话框,如图 16-22 所示。在此对话框中,您可以设置缩放、追踪算法容差、追踪模式和帧模式,并查看作为这些设置结果的顶点数。

A978-1-4842-0415-3_16_Fig22_HTML.jpg

图 16-22。

Using the Tracer utility in PhysEd to set the Tolerance, Trace mode, Frame mode and Vertices

一旦你得到了你想要的视觉效果,这是通过调整各种跟踪对话框设置来实现的,点击 OK 按钮,你将回到 PhysicsEditor 主用户界面窗口。

然后,你可以通过用鼠标点击并拖动这些点来逐个数据点地细化你的碰撞多边形结构数据点,如图 16-23 所示。如果你比较图 16-22 中的碰撞多边形和图 16-23 中的碰撞多边形,你可以看到我已经优化了几个数据点,以更好地符合精灵的轮廓。

碰撞多边形离精灵的轮廓越近,你最好把它放在反走样的上面,反走样位于精灵像素和透明度之间的像素边缘,或者如果你想玩一个更有挑战性的游戏,甚至可以放在反走样的里面。最终,你的碰撞多边形将不得不在你的游戏开发和游戏测试周期中被调整,这样你才能在游戏中得到最真实的结果。如果您正在为强大的平台(如游戏控制台)进行开发,您可以向碰撞多边形添加数据点,如果您正在为单处理器或双处理器平台(如 HTML5 手机或 ITV)进行开发,请使用较少的数据。

A978-1-4842-0415-3_16_Fig23_HTML.jpg

图 16-23。

Fine-tune your vertex placement for the collision polygon in the PhysicsEditor, then select your Exporter

替换虚拟碰撞数据:InvinciBagel.java

接下来,让我们对 InvinciBagel.java 类中的 Java 代码做一些修改,这样你就可以真正使用我们在本章中开发的高度优化的碰撞多边形数据。我们将使用只有 15 个数据点的数据集,这样我们就可以在 NetBeans 8.0 代码编辑窗格中看到所有的碰撞多边形数据。正如你在图 16-24 中看到的,我们将需要把 Bagel()构造器方法调用放在三个不同的行上:一个用于实例化的iBagel = new Bagel(this)部分,另一个用于碰撞多边形 SVG 数据字符串对象,另一个用于 xLocation,yLocation 和 Image 对象列表。我们还将使用我们从表 16-1 中学到的 SVG 命令知识,为我们的可平铺砖块元素创建碰撞多边形数据。在本章的后面,我们将使用这些砖块“道具”元素来开发我们的碰撞检测 Java 代码。我们将仅使用我们的大脑来创建这些更简单的碰撞多边形,通过实现与砖块图像的分辨率相关的数字逻辑,以及角像素位置在 X,Y 坐标空间中的位置,结合我们在表 16-1 中了解到的 SVG 路径绘制命令。你正在成为一名相当专业的游戏开发人员!

我们还将使用我们在第三章学到的 Java 代码注释技术来(暂时)从场景中移除较大的长满苔藓的岩石道具,并将我们的代码放在舞台上有 InvinciBagel 角色和几块砖块的地方。然后,我们可以使用这些基本对象来开始我们的代码开发。collide()方法以及它将如何使用 castDirector CastingDirector 类(object)作为冲突处理指南。

如果您希望看到 iPR1 和 iPV1 对象暂时从中删除。createGameActors()以及您的。addGameActorNodes()和。createCastingDirection()方法,您可以在图 16-25 和 16-26 中看到这段 Java 代码的注释。接下来,让我们复制并粘贴您在本章前面创建的 SVG 碰撞多边形数据集。打开 iBshape1svg.txt 文件,如图 16-20 所示,使用编辑➤复制菜单序列或 CTRL-C 快捷键选择并复制 svg 数据。一定要包括引号。将 Bagel()方法中的数据粘贴到。createGameActors()方法,虚拟 SVG 数据曾经在这里,如图 16-24 所示。

A978-1-4842-0415-3_16_Fig24_HTML.jpg

图 16-24。

Copy and paste your 15 data point collision polygon SVG data in place of dummy data in Bagel() method

更新 iBagel Bagel()构造函数方法后,使用碰撞多边形的准确 SVG 数据,更新 iP0 固定演员道具,使用四个角像素的坐标,创建一个方形碰撞多边形,如图 16-25 所示。任何图像的左上角原点都将是 0,0,所以第一个 SVG 命令将是 M0,0,或“移动到原点”接下来,我们想使用 L 命令画一个“lineto ”,左下角,它将使用 L0,32 命令和数据集,因为这个砖块图像是 32 像素高(Y ), 72 像素宽(X)。

A978-1-4842-0415-3_16_Fig25_HTML.jpg

图 16-25。

Create collision polygon SVG data for the iP0 fixed Actor props using a prop0.png image 32´72 resolution

下一个数据对不需要以 L 命令开头,因为如果没有明确指定,SVG 数据解析算法的任何实现都将假定前一个数据对使用的命令。砖块图像的右下角将使用 X,Y 坐标 72,32。该图像的右上角将使用 X,Y 坐标 72,0。Z 命令可以用来连接这个道具图像的右上角和原点,这样我们就可以在砖块的顶部进行碰撞检测,在这个特定的用例中,使用一个闭合的多边形。正如您在图 16-25 中看到的,您可以在 SVG 数据中使用逗号或空格,因此这两种方法调用都应该有效:

iPR0 = new Prop("``M0 0 L0 32 72 32 72 0 Z

OR:

iPR0 = new Prop("``M0,0 L0,32 72,32 72,0 Z

第二个示例在 NetBeans 8 中使用了相同的空间,并且更好地显示了数据点对。我将注释掉与 iP1 演员道具对象相关的所有代码,如图 16-25 和 16-26 所示,以便这些较大的道具演员对象现在被“禁用”,不会出现在舞台上(和场景中),并且不会干扰我们实现碰撞检测的基本代码开发。

A978-1-4842-0415-3_16_Fig26_HTML.jpg

图 16-26。

Comment out code related to iP1 object in addGameActorNodes() and createCastingDirection() methods

现在,我们准备使用“运行➤项目”工作流程,并确保 invincibagel 角色以及我们将用于测试碰撞检测代码的四块金砖都已就位,并且大型苔藓岩石对象不再出现在舞台上的任何地方。正如你在图 16-27 中看到的,我们已经设置好了开发基本碰撞检测代码的场景,现在我们可以专注于将 Java 8 游戏代码放在适当的位置,然后我们开始实现进一步的场景设计、游戏设计、游戏逻辑、物理和评分引擎代码,所有这些我们将在下一个第十七章中实现。

A978-1-4842-0415-3_16_Fig27_HTML.jpg

图 16-27。

Use the Run ➤ Project work process and test the game

在我们切换到 Bagel.java 类来实现冲突检测之前,我们需要对 InvinciBagel.java 类做一两个基本的修改。由于我们将在冲突代码中使用 CastingDirector 对象,我们必须删除CastingDirector castDirector;声明中的私有访问控制关键字,如图 16-28 所示。我们正试图使 InvinciBagel 类中的尽可能多的变量保持私有,以后如果我们需要从另一个类(比如 Bagel.java)访问它们,就让它们受到包保护。

A978-1-4842-0415-3_16_Fig28_HTML.jpg

图 16-28。

Remove the private access control modifier keyword for CastingDirector castDirector object declaration

接下来我们要做的是从。createCastingDirection()方法中的 addCurrentCast()方法调用参数列表。我们这样做是为了让 InvinciBagel(我们将检查它与其余 Actor 对象的冲突,这些对象包含在 CURRENT_CAST List 对象中)不检查它自身的冲突!新的更短的方法调用(减去 iBagel 对象)应该看起来像下面的 Java 代码,在图 16-29 的底部高亮显示:

castDirector.addCurrentCast(``iPR0, iPH0, iPV0, iPB0

A978-1-4842-0415-3_16_Fig29_HTML.jpg

图 16-29。

Remove the iBagel object from the front of the list of objects passed into the .addCurrentCast() method

现在,我们准备开始在 Bagel.java 类中放置方法,该类将通过检查每个对象的节点(ImageView)和碰撞形状(SVGPath)的边界来检查 iBagel 对象与 CURRENT_CAST List 对象中的对象的碰撞。我们接下来要讨论的内容可能会有些复杂,但是如果你真的想创建一个合法的游戏标题,你需要能够知道游戏元素何时相互交叉(在游戏开发术语中称为碰撞)。让我们现在就潜入那个深渊吧!

Bagel 类冲突检测:。检查冲突( )

在图 16-29 所示的项目窗格中右键单击 Bagel.java 类,并选择打开选项,在 NetBeans 8 中打开 Bagel.java 选项卡。走进去。update()方法,并在代码行的开头使用两个正斜杠注释掉 playAudioClip()方法调用。这将有效地关闭我们现有的按键事件(KeyCode)按键持续呼叫的音频。我这样做的原因是因为我将使用这个音频来“指出”我们将要创建的代码中检测到的冲突。接下来,在方法的末尾创建一个. checkCollision()方法调用。update()方法。这个方法将调用碰撞检测 Java 代码,我们将在本章的剩余部分中对每个被处理的脉冲事件进行处理。这与。update()方法。

新的。图 16-30 中所示的 update()方法应该类似于下面的 Java 方法体:

public void``update

setXYLocation();

setBoundaries();

setImageState();

moveInvinciBagel();

// playAudioClip();

checkCollision();

}

A978-1-4842-0415-3_16_Fig30_HTML.jpg

图 16-30。

Comment out the playAudioClip() method call to turn off all audio and add a checkCollision() method call

创建一个空的public void checkCollision(){}方法体,这将移除你在图 16-30 中看到的红色波浪错误高亮,这样我们就可以开始编写代码来查看当前演员列表<演员>对象,这样我们就可以确定是否有任何演员与游戏舞台上的主要无敌角色相交(发生冲突)。

在这个里面。checkCollision()方法,我们将需要一个 Java for“counter”循环结构。该结构将用于遍历 CURRENT_CAST List 对象数组;在这种情况下,这些是道具对象,用于检查与 InvinciBagel 对象的任何碰撞(相交)。for 循环的第一部分是迭代器变量的声明和初始化,在我们的例子中,int i=0;声明名为“I”的整数类型迭代器变量,初始化为一个零计数值。

for 循环的第二部分是迭代条件,在本例中是“迭代直到到达 CURRENT_CAST List 对象的末尾,或者到达代表列表大小(因此是列表中的最后一项)的i<invinciBagel.castDirector.getCurrentCast().size()。for 循环语句条件的第三部分是要迭代的数量,由于我们想要遍历列表中的每个对象< Actor >来检查冲突,我们将在“I”变量或i++上使用熟悉的 Java ++操作符。for 循环{…}花括号内是我们希望在循环的每次迭代中执行的内容;在这种情况下,这将用于列表<演员>对象中的每个道具演员对象。这是一个很好的构造,可以用来从第一个元素到最后一个元素迭代任何列表对象。

在 for 循环内部,我们将创建一个“本地”Actor 对象引用变量,我们将使用invinciBagel.castDirector.getCurrentCast().get(i)方法链将它设置为等于 List(i)中的当前元素。Java 方法链是创建紧凑的 Java 代码结构的一种很酷的方式,不是吗?一旦我们用 CURRENT_CAST(i) cast 成员对象加载了该 Actor 对象,我们将调用。collide()方法,使用 collide(object);Java 语句。记住我们安装了这个抽象。collide()方法,所以在我们了解了更多关于 Node 和 Shape 类的知识,以及它们的一些可用于确定交集(冲突)的关键方法和属性之后,接下来我们必须“覆盖”并编写该方法。

public void checkCollision() {

for(``int i=0``; i``<``invinciBagel.castDirector.getCurrentCast``().size()

Actor``object``= invinciBagel.castDirector.getCurrentCast()``.get(i)

collide (object);

}

}

这是确定冲突的第一个(简单的)部分,在第一个 checkCollision()方法中,从。update()方法,由脉冲事件管理器执行。它遍历列表 CURRENT_CAST,并为每个 Actor 对象调用collide(object);方法,以查看 InvinciBagel 角色是否与它发生了冲突。正如你在图 16-31 中看到的,这个 Java 代码是无错的,我们将要编码的下一个 Java 方法体是public boolean collide()方法,我们之前在第八章中在我们的抽象 Hero 类中声明使用它。

A978-1-4842-0415-3_16_Fig31_HTML.jpg

图 16-31。

添加一个 for 循环,使用从 0 计数到 CURRENT_CAST 中的对象数。getCurrentCast()。size()

在我们编写。collide()方法,这是我们将在整本书中编写的较难的方法体之一,我们需要了解一些与 javafx.scene 包的节点类及其 Bounds 属性有关的更复杂的主题。getBoundsInLocal()和。getBoundsInparent()方法调用。我们还将查看 javafx.scene.shape 包的 shape 类及其。intersect(Shape shape1,Shape shape2)方法调用。我们将在我们的public boolean collide(Actor object) {...}方法中使用所有这些,所以在我们编写这个复杂而密集(但令人兴奋)的 Java 结构之前,我们需要先掌握这些高级知识。

定位节点对象:使用 Bounds 对象

关于碰撞检测,我们需要考虑的第一件事是 javafx.geometry 包的 Bounds 类。这个公共抽象类(及其创建的对象)在 javafx.scene 包的节点类中用于保存节点的边界。正如你可能已经猜到的,这是我们将利用来确定碰撞检测的事情之一,结合我们的碰撞 SVGPath 形状数据,我们将在我们看一下如何限制对象及其相关对象后进入。getBoundsInLocal()和。getBoundsInParent()方法,可以为我们工作。

这个 Bounds 类是使用 java.lang.Object 主类从头开始创建的,包含 X、Y 和 Z 坐标,以及宽度、高度和深度值。由于我们将在 2D 工作,我们将使用 X 和 Y,以及宽度和高度,Bounds 对象的值(属性)。Bounds 类有一个直接已知的子类,称为 BoundingBox。如果您想知道,“直接子类”意味着 BoundingBox 类声明说 BoundingBox 扩展了边界,而“已知”类是已经正式添加到 Java 8 JDK 中的类。

“未知”类的一个例子是你自己定制的边界子类,如果你要写一个的话。javafx.geometry.Bounds 类的 Java 8 类层次结构如下所示:

java.lang.Object

> javafx.geometry. Bounds

Bounds 类用于创建 Bounds 对象。这些用于描述节点对象的边界,我们知道这是 JavaFX 场景图节点对象。Bounds 对象的一个重要特征是它可以有负的宽度、高度或深度。任何 Bounds 对象属性(properties)的负值用于指示 Bounds 对象为空。我们将在后面的代码中使用它来确定何时没有发生冲突。正如我在本书前面指出的,有时你必须采取“相反”的方法来寻找解决方案,或者正确的工作过程,以实现你的游戏设计和编程目标。

使用节点局部边界。getBoundsInLocal()方法

JavaFX 场景图节点类中的一个重要方法是公共最终边界。getBoundsInLocal()方法。此方法是 getter 方法,用于检索 boundsInLocal 属性的值。boundsInLocal 属性是“只读”的,它为包含它的节点保存一个矩形 Bounds 对象。它包含的数据表示节点的未变换(原始)局部坐标空间。“未变换”表示旋转、平移或缩放之前的节点坐标,表示节点对象的原始(默认)坐标。

对于扩展 Shape 类的节点类(对象)(我们的 ImageView 节点没有扩展),局部边界还将包括实现非零形状(或路径)笔划所需的空间,因为这可能会扩展到形状几何图形的“外部”,这是由这些位置和大小属性定义的。局部边界对象还包括您可能已经设置的任何剪辑路径区域,以及您可能已经设置的任何特殊效果的范围。

boundsInLocal 属性将始终具有非空值,需要注意的是,该方法不考虑节点对象的可见性,因此计算仅基于节点的几何形状。每当节点对象的几何体发生变化时,都会自动计算 boundsInLocal 属性。

我们将使用。getBoundsInLocal()方法在 InvinciBagel SVGPath 形状数据与另一个场景演员 Prop SVGPath 碰撞形状数据的交叉点上执行,以确定交叉点的宽度是否为负一,边界对象中的-1 是否为空,或者没有交叉点,这表示没有碰撞。接下来,让我们看看 boundsInParent 属性,它包含 boundsInLocal 数据和转换。

使用节点父边界。getBoundsInParent()方法

JavaFX 场景图节点类中需要理解的另一个重要方法是公共最终边界。getBoundsInParent()方法。此方法是检索 boundsInParent 属性值的 getter 方法。boundsInParent 属性是一个“只读”属性,用于保存包含它的节点的矩形边界对象。它包含的数据表示节点的变换(修改)坐标空间。“Transformed”表示节点坐标加上自节点对象的默认、初始或原始状态以来发生的任何变换。它被命名为“boundsInParent ”,因为边界对象矩形数据需要相对于父节点对象的坐标系。这表示节点对象的“可视”边界,正如您在屏幕上看到的,在节点被移动、旋转、缩放、倾斜等之后。

boundsInParent 属性是通过获取由 boundsInLocal 属性定义的局部界限,并应用所有已发生的转换(包括对。为以下节点属性设置()方法:scaleX、scaleY、rotate、layoutX、layoutY、translateX、translateY 和 transforms(Java observable list)。就像 boundsInLocal 属性一样,该 boundsInParent 属性将始终包含一个非空值。

结果 Bounds 对象将包含在节点对象的父对象的坐标空间内,但是,为了能够计算 boundsInParent 属性,节点对象不需要有父对象(它可以是场景的场景图形根)。就像。getBoundsInLocal()方法,该方法不考虑节点对象的可见性,所以不要错误地认为可以通过“隐藏”Actor 节点来规避冲突检测代码,在我们的例子中,Actor 节点是一个名为 spriteFrame 的 ImageView 节点子类。

由于它会计算对 boundsInLocal 属性的更改,因此只要节点的几何图形发生更改,或者该节点对象发生任何变换,就会计算 boundsInParent 属性,这是合乎逻辑的。正因为如此,基于依赖于该变量的表达式来计算节点中的任何这些值都是错误的。例如,Node 对象的 X 或 Y 变量或 translateX、translateY 不应使用此 boundsInParent 属性来计算以定位节点,因为先前的定位数据包含在此属性中,因此会创建循环引用(有点类似于臭名昭著的无限循环场景)。

使用节点交集。相交(限制对象)方法

JavaFX 场景图节点类中涉及碰撞检测的另一个重要方法是 public boolean intersects(Bounds local Bounds)方法。如果 Bounds 对象与使用参数传递到方法中的 Bounds 对象相交,则此方法将返回 true 值,该 Bounds 对象指定此方法正在调用的节点对象的本地坐标空间,该 Bounds 对象是您尝试确定与之相交的节点的 Bounds 对象。例如,为了确定包含 InvinciBagel 子画面的 ImageView 边界是否与包含 Prop 子画面之一的 ImageView 边界相交,我们将使用以下 Java 代码格式:

iBagel.spriteFrame.getBoundsInParent()``.intersects

需要注意的是,就像。getBoundsInLocal()和。getBoundsInParent()方法,该方法也不考虑节点对象的可见性属性。出于这个原因,相交测试将只基于讨论中的节点对象的几何图形,以及我们的 ImageView spriteFrame 节点对象的几何图形,这将是它们的方形(InvinciBagel)或矩形(Prop)图像容器的几何图形(尺寸)。Node 类 intersects(Bounds localBounds)函数的默认行为是检查并查看传入此方法调用的 Bounds 对象的本地坐标是否与。intersects()方法调用正在被“关闭”接下来,让我们仔细看看 Shape 超类,以及它的。intersect()方法。

使用形状类相交。intersect()方法

就像一个节点类(对象)有一个交集方法叫做。intersects()用于确定节点对象何时相交,Shape 类也有一个 intersection 方法,可用于 Shape 对象相交。这个方法被称为。intersect()(而不是 intersect),因此这些方法使用不同的命名约定。这样做可能是为了让开发人员不会混淆 Node.intersects()和 Shape.intersect()方法。我们将在本章后面的碰撞检测代码中使用这两种方法——首先检测一个 ImageView 节点何时与另一个 ImageView 节点重叠,然后在我们的 SVG 数据碰撞多边形(每个 Actor 对象中名为 spriteBounds 的 SVGPath 对象)之间执行布尔相交操作。

Shape 类的静态方法格式。intersect()方法在其参数列表中接受两个 Shape 对象,该方法返回一个新的 Shape 对象。创建的新 Shape 对象表示两个输入形状的交集。正如您所想象的,如果您使用 Shape 类及其子类来实现这一目的,您也可以使用这种方法为您的矢量作品生成布尔交集。该方法的格式是static Shape intersect(Shape shape1, Shape shape2),我们将使用它的方式如下所示:

Shape``shape``= SVGPath.``intersect``(invinciBagel.``iBagel``.getSpriteBound(),``object``.getSpriteBound());

Shape 类之所以。intersect()方法对我们的碰撞检测应用如此有效是因为我们以后可以使用. getBoundsInLocal()。getWidth()方法链,通过查找-1 空值来确定是否发生了冲突,或者更确切地说,是否没有发生冲突。我们将调用这个方法链来寻找这个新 Shape 对象的-1,我们将在。collide(object)方法,使用下面一行代码:

if (intersection.``getBoundsInLocal().getWidth() != -1``) { collisionDetect =``true

这种方式。intersect()方法的工作原理是,它只处理占据输入形状的几何数据,在我们的应用中,它是 Actor 对象内部的 spriteBound SVGPath 对象。这就是为什么我们使用 SVGPath 形状对象(用于最大碰撞框架或多边形定义),纯粹是为了它的几何数据值,也是为什么我们在我们的 Actor 对象设计中安装了这个 spriteBounds SVGPath 形状对象容器,因为我们打算将这些几何数据用于我们的碰撞检测,而不是用于矢量插图艺术作品(例如,我们没有抚摸或填充 SVGPath,请不要开玩笑,我们现在正在学习游戏开发)。

这意味着中的算法所考虑的输入形状对象的几何区域。intersect()方法仅“基于数学”。这意味着算法独立于正在处理的形状(子类)的类型,也独立于用于填充或描边的 Paint 对象的配置(为了学习 Java 8 游戏开发,请再次暂时抵制窃笑的诱惑)。

在最终相交计算之前,输入形状对象的区域被转换到父节点对象的坐标空间(想想:boundsInParent)。这样,生成的形状将只包括那些包含在使用参数表传递给该方法的两个输入形状对象的区域中的区域(更准确地说是数学几何区域)。因此,如果在生成的 Shape 对象中有任何数据(除了-1 空数据值之外的数据),我们将在接下来编写的代码中称之为 intersection,那么就存在某种程度的交集,甚至一小块交集区域(重叠)也需要发出检测到碰撞的信号。

现在,我们已经准备好使用我们在本章过去几页中学到的技术 Java 8 类和方法信息,只用大约十几行 Java 代码来创建核心冲突检测逻辑。我们将在本章结束前添加另外十几行代码,来处理我们游戏中的碰撞。

重写抽象的 Hero 类:。collide()方法

最后,覆盖和实现公共布尔 collide (Actor object)方法的时候到了,我们在第八章的抽象 Hero 类中安装了这个方法。这是我们玩游戏的一个关键方法,因为它决定了我们的主要无敌角色何时与游戏中的其他元素接触。这种接触的结果是得分,以及游戏屏幕上视觉上的变化,所以在下一章中,我们将在 InvinciBagel 游戏中实现游戏元素,这是非常关键的。我们要做的第一件事是安装一个名为 collisionDetect 的布尔“flag”变量,并在。collide()方法。用于设置 collisionDetect 标志的该语句的 Java 代码应如下所示:

boolean``collisionDetect

确定是否发生冲突的下一步是使用 Java 条件 if()语句。这允许我们使用 Node 类测试包含 sprite 图像资产的 ImageView 节点对象是否相交。intersects()方法调用与。getBoundsInParent()方法调用每个 spriteFrame ImageView 节点对象。第一个 spriteFrame.getBoundsInParent()方法调用是我们方法链接的调用。intersects()方法调用 off,因为我们试图做的是确定与我们的主要游戏角色的冲突。因为我们想要引用 InvinciBagel 对象(invinciBagel 类)及其 iBagel Actor 对象,所以这个构造将采用 invinciBagel . iBagel . sprite frame . getboundsinparent()的形式。intersects()方法调用结构。因为我们测试冲突的 Actor 对象的 Bounds 对象需要在。intersects(Bounds localBounds)方法调用,我们需要使用。getSpriteFrame()方法调用我们在第八章开发的。从我们传递到。collide()方法我们将方法链接到。getBoundsInParent()方法调用,产生 object.getSpriteFrame()。getBoundsInParent()结构,然后将它放在。intersect()方法调用。完整的条件 if()结构如下所示:

if ( invinciBagel. iBagel .spriteFrame.getBoundsInParent(). intersects

object .getSpriteFrame().getBoundsInParent() ) ){second level of collision detection in here}

第一级冲突检测代码将检查 iBagel spriteFrame ImageView 节点与 spriteFrame ImageView 节点对象的交集,该对象包含在将传递到此的每个 Actor 对象中。我们当前正在编写的 collide(Actor object)方法调用。

在我们调用更“昂贵”的形状相交算法之前,这部分代码将检查“顶级”ImageView 节点邻近性(碰撞),我们接下来将实现该算法,以便确认更确定的碰撞发生(SVGPath 碰撞多边形相交)。请记住我们放入其中的代码。对于调用 collide(Actor 对象)方法的 for 循环的每次迭代(对于每个角色成员),都将处理 collide()方法。

第一个条件 if()语句中的 Java 代码创建一个名为 intersection 的 Shape 对象,并将其设置为 SVGPath.intersect()方法调用的结果,该调用引用 iBagel SVGPath Shape 对象和传递到 collide(Actor 对象)方法中的 SVGPath Shape 对象。这两个对象都调用一个. getSpriteBound()方法,我们在第八章的中创建了这个方法,来访问。intersect(Shape1,Shape2)方法调用格式。为了便于阅读,使用两行代码格式化的 Java 代码应该如下所示:

Shape intersection =``SVGPath.intersect``( invinciBagel.``iBagel

object .getSpriteBound() );

有了这个相交数据后,我们将使用另一个条件 if()语句来查看该相交形状对象是否包含任何碰撞数据,如果包含(也就是说,如果它不包含-1 值),则表示发生了碰撞。

第二个嵌套的 if()语句将利用。getBoundsInLocal()。getWidth()方法链,从交叉点形状对象调用,将检查它是否为空(返回-1 值),或者是否发生了冲突。如果相交形状对象的 Bounds 对象包含除-1 之外的任何数据值,将发生冲突检测。在 if()语句体中,如果存在任何数据,collisionDetect 布尔标志将设置为 true(用!= -1).条件 if()语句的 Java 代码应该如下所示:

if(intersection.``getBoundsInLocal().getWidth()``!=``-1``) {``collisionDetect = true;

为了测试冲突代码,我将 invinciBagel.playiSound0()方法调用放在 if(collisionDetect){}条件语句中。这就是为什么我注释掉了?中的 playAudioClip()方法调用。update()方法,这样在碰撞过程中我唯一能听到的就是音频回放。这是测试碰撞代码的一种快速、简单、有效的方法,至少目前是这样。由于这是公共布尔 collide(Actor object)方法,我还将在 if()体的末尾放置一行代码return true;,它从方法调用中返回一个 true 值。我将语句放在这个条件 if()结构中,这样,如果需要,我们可以使用从。collide()方法调用。update()方法进行其他处理,如果我们想的话。这件事的本质。collide()方法的目的是检测是否发生了冲突,然后返回一个值,这样我们就可以在。collide()方法,或者更有效地在。checkCollision()方法,使用一个if(collide(object)=true){invinciBagel.playiSound0();}构造,而不是我们在这里用。collide()方法。我这样做的原因是,至少现在是这样,因为我正在测试这个。collide()方法,所以我把允许我测试。collide()方法。collide()方法,因为这是我现在在 ide 中工作的地方。Java 代码如下所示:

if(``collisionDetect

invinciBagel.playiSound0();

return``true

}

return false;

完整的。在我们开始操作 CastingDirector 类和对象列表对象之前,collide(Actor object)方法结构应该如下所示,这也可以在图 16-32 : 中看到

@Override

public boolean collide(Actor``object

boolean``collisionDetect

if ( invinciBagel.iBagel.spriteFrame.``getBoundsInParent()``.``intersects

object``.getSpriteFrame().``getBoundsInParent()

Shape``intersection

SVGPath.``intersect``(invinciBagel.iBagel.getSpriteBound(),``object

if (intersection.``getBoundsInLocal().getWidth()``!=``-1

collisionDetect``=``true

}

}

if(``collisionDetect

invinciBagel.playiSound0();

return true ;

}

return false ;

}

A978-1-4842-0415-3_16_Fig32_HTML.jpg

图 16-32。

Creating the basic collision detection code and testing it using the invinciBagel.playiSound0() method call

现在我们准备添加管理 CastingDirector 对象的 Java 代码,以删除 Actor 对象。

如果检测到碰撞:操纵 CastingDirector 对象

在. playiSound0()方法调用下添加一行代码,访问 CastingDirector 对象 invinciBagel.castDirector. Next 键入一个句点键,访问一个方法帮助器弹出窗口,如图 16-33 所示,并选择。addToRemovedActors()。

A978-1-4842-0415-3_16_Fig33_HTML.jpg

图 16-33。

Add a line of code under the .playiSound0() method call, and type invinciBagel.castDirector, and a period

双击。addToRemoveActors(Actor… actors)选项在这个弹出的帮助器对话框中,你需要使用圆括号内的参数列表区将 Actor 对象,恰当命名的 object,传递到方法调用中,如图 16-34 底部突出显示的。这将把这个刚刚卷入碰撞的演员对象(幸运的是,没有人受伤)添加到 REMOVED_ACTORS HashSet <演员>对象中。

A978-1-4842-0415-3_16_Fig34_HTML.jpg

图 16-34。

Add Actor object that was passed into the .collide() method to parameter list of addToRemovedActors()

我们不仅需要使用 CastingDirector 类(object)从游戏角色中删除这个冲突的 Actor 对象,我们还需要从场景图形根中删除 ImageView 节点。接下来让我们看看如何去做,然后我们将看看如何从 CURRENT_CAST 列表中删除 Actor 对象。

从场景图中删除演员:。getChildren()。移除()

现在,我们已经通过将 InvinciBagel 与之冲突的演员添加到 REMOVED_ACTORS HashSet 对象中,有效地将其从角色转换中移除,下一步是将它从 JavaFX 场景图中移除,我们当前使用 StackPane UI 布局容器类来移除它。如果您还记得在本书的前面,我们了解到 StackPane 类可以用作基于图层的布局容器,并使用居中的 XY 网格排列其内容。在本章的后面,我们将学习如何使用一个组对象(类)作为我们的场景图根,来实现更典型的左上角 0,0 XY 位置索引方案,并作为一种优化,因为我们没有特别使用任何 StackPane 类属性或方法,并且因为组类是一个更基本的类,在节点超类层次结构中处于更高的位置。通过这种方式,您将体验到使用中心屏幕 0,0 位置引用(StackPane),以及传统的左上角 0,0 屏幕位置引用(作为场景图形根的组类)。

使用. getChildren()从场景图形根中移除节点对象。中的. getChildren.add()方法调用方式完全相反。addGameActorNodes()方法。添加一行代码,键入 invinciBagel.root 和句点,如图 16-35 所示,打开方法帮助器弹出。

A978-1-4842-0415-3_16_Fig35_HTML.jpg

图 16-35。

Add a line of code under an .addToRemovedActors() method call and type invinciBagel.root and a period

因为我们是从 Bagel.java 类中删除 ImageView 节点,所以在添加。getChildren()方法调用,使用下面的 invinciBagel.root.getChildren()代码如图 16-35 所示。双击 remove(Object o)选择并将您传递的对象添加到。collide()方法放入。移除()方法,如图 16-36 所示。这将创建最终的invinciBagel.root.getChildren().remove(object); Java 编程语句。

A978-1-4842-0415-3_16_Fig36_HTML.jpg

图 16-36。

Investigate red error highlight under the Scene Graph root StackPane object reference in the method call

将鼠标悬停在突出显示的警告上,您将看到 NetBeans 看到我们正在将自定义 Actor 对象传递给。getChildren()。remove()方法链,而不是 ImageView 节点对象。错误消息还告诉我们,InvinciBagel.java 类中的private StackPane root;声明不允许我们访问这个对象,除非我们删除这个私有访问控制修饰符关键字。让我们首先修复最严重的(错误)问题,然后通过使用 object.getSpriteFrame()方法调用修复“对 java.util.Collection.remove 的可疑方法调用”。remove()方法调用。

点击 NetBeans 中的 InvinciBagel.java 编辑选项卡,删除StackPane root;对象声明前面的私有关键字,如图 16-37 突出显示。

A978-1-4842-0415-3_16_Fig37_HTML.jpg

图 16-37。

Remove the private access control modifier keyword from in front of the StackPane root; Java statement

一旦你做了这样的修改,你将会看到在你当前的 Bagel.java 编程语句中的红色波浪错误高亮消失,如图 16-38 所示,现在我们所要担心的是移除警告高亮,该警告高亮与传递一个要从场景图中移除的 Actor 对象有关,而不是它所期望的节点对象(spriteFrame ImageView)。这是因为 JavaFX 场景图管理节点类,以及节点子类,如 ImageView,不接受非节点子类,如我们在第八章中设计的 Actor 类,并在后续章节中实现。我们的 Actor 类(object)包含一个 Node 对象,一个 ImageView 节点子类,因此我们必须使用 Java 点标记法包含对该对象的引用。remove()方法可以有效地查看 Actor 对象的“内部”,以到达(访问)这个节点对象。

将光标放在。移除我们正在实现的()方法调用,并键入一个句点。这将弹出属性和方法帮助器,如图 16-38 所示。双击 getSpriteFrame()方法,我们在第八章的中创建了这个方法,从当前的 Actor 对象中调用它,这个对象已经被传递到。collide()方法。这将把 ImageView 节点对象传递到。remove()方法调用,警告高亮也将消失,留下干净的代码。

A978-1-4842-0415-3_16_Fig38_HTML.jpg

图 16-38。

Add a period after object in the .remove() method to open method helper and select .getSpriteFrame()

如图 16-39 所示,您所有的 Java 代码都没有错误,现在您已经播放了一个声音,将这个 Actor 对象添加到 HashSetREMOVED _ ACTORS 数据集,并从 JavaFX 场景图根(当前是一个 StackPane 布局容器)中删除了这个 Actor 的 spriteFrame ImageView 节点对象。我们现在准备通过使用一个. resetRemovedActors()方法调用,从当前演员列表<演员>对象中删除这个演员对象。

A978-1-4842-0415-3_16_Fig39_HTML.jpg

图 16-39。

Remove the currently collided with Actor object ImageView Node from the Scene Graph using .remove()

在我们更新了 CURRENT_CAST 列表之后,我们就完成了基本的冲突管理编码,我们可以看看如何优化。checkCollision()和。collide()方法更有效地协同工作。

重置删除的执行元列表:。resetRemovedActors()方法

当冲突发生时,我们需要在一系列编程语句中做的最后一件事是重置 REMOVED_ACTORS HashSet 对象。正如你在第一章 0 中回忆的,这个方法从当前演员列表<演员>对象中移除被移除的演员,这样演员对象就完全从游戏中移除了。除了我们使用 ear 测试该方法的. playiSound0()方法调用之外,这是我们必须编写的最简单的 Java 编程语句之一,应该类似于下面的 Java 代码:

invinciBagel.castDirector.``resetRemovedActors()

A978-1-4842-0415-3_16_Fig40_HTML.jpg

图 16-40。

The finished .collide() method is error-free and only 19 lines of code, and is ready for further optimization

如果您使用运行➤项目工作流程,并且现在测试您的游戏代码,您不仅会听到音频,还会看到道具对象在与 InvinciBagel 对象碰撞后消失。我现在将音频留在这段代码中,这样您就可以测试这些演员对象是否真的消失了,方法是让 InvinciBagel 在道具对象所在的区域上空运行(或飞行)以确保(用您的耳朵)它真的消失了。接下来,我们将进一步优化代码,实现我前面提到的 if(collide(object))方法,让。collide()方法只是返回一个 true(检测到冲突)或 false(没有检测到冲突)值,就像一个行为正常的布尔方法应该做的那样。

优化碰撞检测处理:if(collide(object))

为了优化冲突检测过程,我们将冲突时执行的代码移到。Java 代码的一个if(collide(object)){}条件 if 块中的 checkCollision()方法。这允许我们从 collide()方法中消除一个布尔 collisionDetect 变量,使它更加精简。我们现在所做的一切。collide()方法将return true;语句传递回调用实体,在本例中是。checkCollision()方法,如果检测到冲突。这也允许我们完全消除 if(碰撞检测)结构。

在优化之前,我们在。checkCollision()方法,以及。collide()方法。优化后,我们少了 5 行 Java 代码,每个方法有 10 行代码。这些方法的新 Java 结构是无错误的,如图 16-41 所示,看起来像下面的 Java 代码:

public void``checkCollision

for(int i=0; i<invinciBagel.castDirector.getCurrentCast().size(); i++) {

Actor``object

If (``collide``(``object

invinciBagel.playiSound0();

invinciBagel.castDirector.addToRemovedActors(``object

invinciBagel.root.getChildren().remove(``object

invinciBagel.castDirector.resetRemovedActors();

}

}

}

@Override

public``boolean collide``(Actor``object

if (invinciBagel.iBagel.spriteFrame.getBoundsInParent().intersects(

object .getSpriteFrame().getBoundsInParent() ) ) {

Shape intersection =

SVGPath.intersect(invinciBagel.iBagel.getSpriteBound(),``object

if (intersection.getBoundsInLocal().getWidth() !=``-1

return true;

}

}

return false;

}

A978-1-4842-0415-3_16_Fig41_HTML.jpg

图 16-41。

Optimizing the interaction between the collide() method and the checkCollision() method

使用“运行➤项目”工作流程来确保代码像方法优化前一样工作。

优化场景图:使用组类

因为除了闪屏使用之外,我们并没有真正使用 StackPane 进行图像层合成或 UI 设计,所以让我们添加另一个优化,并为我们的游戏使用 Groupclass (object ),因为它在类层次结构的更高层,比更专业的 StackPane 类(object)更接近节点超类。我们将使用更短更简单的对象➤节点➤父➤组层次结构,而不是使用使用更复杂对象➤节点➤父➤区域➤窗格➤堆栈窗格层次结构的类。这种优化的意义在于,使用组根节点会在系统内存中使用更少的代码(属性和方法)。因为我们没有特别使用任何高度专门化的 StackPane 属性或方法,所以我们可以通过使用 Group 类来执行主要的优化。一个组对象(类)使用固定的对象定位,所以我们必须重新编写 HBox 和 Actor 对象的代码,但是只有在我们使用“运行➤项目”工作流程来查看使用组与使用 StackPane 的确切区别之后。毕竟,这都是为了学习这些类是如何工作的!将 StackPane 对象的导入、声明和实例化更改为 Group 对象的 Java 代码应类似于以下 Java 代码,如图 16-42 所示:

import javafx.scene.``Group

public class InvinciBagel extends Application {    // all other object declarations omitted

Group root;

public void start(Stage primaryStage) {        // all other object instantiations omitted

root = new``Group

A978-1-4842-0415-3_16_Fig42_HTML.jpg

图 16-42。

Replace the StackPane UI container with the Group (Node subclass) import, declaration and instantiation

完成这些更改并运行➤项目后,您将看到图 16-43 左半部分所示的结果。游戏现在使用左上角的 0,0 原点,所以我们必须修改现有代码的其余部分。让我们现在做那件事。

A978-1-4842-0415-3_16_Fig43_HTML.jpg

图 16-43。

Showing the Group Scene Graph result (left) and corrective modification to HBox, iBagel and Prop (right)

分别用 WIDTH/2 和 HEIGHT/2 替换对 Bagel()类构造函数的 iBagel 实例化调用中的 0,0 XY 位置。这在构造函数方法调用中使用了屏幕大小常量和计算代码,这将把 iBagel 放在屏幕的中心。在 Prop 对象实例中也添加一些不同的数字,将这些瓷砖放置在屏幕的不同区域,这样我们就可以在 InvinciBagel 拾取它们时开发一些评分代码。最后,用一个.setLayoutY(365);方法调用替换.setAlignment(Pos.BOTTOM_LEFT);方法调用,实现 HBox UI 按钮元素的固定定位,回到我们使用 StackPane 之前的位置。如果您使用运行➤项目工作流程,您将看到如图 16-43 右半部分所示的结果。

A978-1-4842-0415-3_16_Fig44_HTML.jpg

图 16-44。

Use .setLayoutY() method to position HBox, WIDTH/2 and HEIGHT/2 to position iBagel, and new Prop X,Y

当你测试你的碰撞代码时,你会发现你不能到达其中的一些区域!接下来我们来解决这个问题。

进入 Bagel.java 类,修改类顶部的屏幕边界常量,使左边界和上边界使用 0,0 原点值,因为 Group 类使用屏幕的左上角作为参考点,而不是像 StackPane 那样使用屏幕的中心。要计算 rightBoundary 值,您需要使用 sprite_PIXELS_X 常量获取屏幕的宽度并减去 SPRITE 的宽度。类似的方法也可以用于 bottomBoundary 值,它将采用屏幕的高度,并使用 sprite_PIXELS_Y 常量减去 SPRITE 的高度。正如你在图 16-45 中看到的,我们不必改变。setBoundaries()方法,这要感谢我们设置代码的模块化、逻辑化和组织化的方式。

A978-1-4842-0415-3_16_Fig45_HTML.jpg

图 16-45。

Update the Bagel.java class with new Boundary values for the right, left, bottom and top Boundary value

在我们结束本章之前,让我们为我们的游戏添加一个评分引擎框架,以便这个碰撞检测例程除了播放声音和从游戏中删除演员之外,还调用一个 scoringine 方法。

创建评分引擎方法:。scoringEngine()

在我们结束本章之前,让我们为评分引擎建立一个框架,这样我们就可以把整个下一章的重点放在游戏上。在 checkCollision()方法之后创建一个private void scoringEngine(Actor object){}空方法,并在 checkCollision()方法内部的if(collide(object))条件 if()结构的末尾添加一个scoringEngine(object);方法调用。正如你在图 16-46 中看到的,你会在。scoringEngine()方法声明。这是因为对象引用没有在方法体内部实现。

我们将在下一章中介绍这一点,因此我们不必担心来自 NetBeans 的警告,而且我们已经准备好了评分引擎基础结构。

A978-1-4842-0415-3_16_Fig46_HTML.jpg

图 16-46。

Add a private void scoringEngine(Actor object) empty method, and call if inside .checkCollision() method

你已经在这一章完成了很多,关于碰撞检测和你的评分引擎。干得好!

摘要

在第十六章中,我们开始讨论一些更高级的主题,我们将在本书的最后几章中讨论这些主题。碰撞检测是游戏开发的基本主题之一,无论是使用 Java 8 还是其他平台。在本章中,我们学习了 SVG 数据格式规范,因此我们可以利用 JavaFX SVGPath 类,它允许我们访问自定义路径形状,如果它们是闭合的,通常称为多边形,以及哪些碰撞形状或碰撞多边形通常是闭合的。我们研究了 SVG 规范中的七个主要命令,所有这些命令都可以在绝对或相对模式下使用。

接下来,我们详细了解了如何让 GIMP 2.8 为我们创建自定义的碰撞形状 SVG 数据字符串,这是一个完全自动化的过程,仅使用 GIMP 工具(算法),允许我们简单地单击透明区域,并让 GIMP 生成选择集。我们将选择集转换为碰撞路径,然后将路径导出为 SVG 数据。这个工作过程允许我们在任何游戏资产数字图像中生成具有透明(alpha 通道数据)区域的复杂碰撞多边形。

这种自动生成的碰撞数据非常“沉重”,因此我们研究了一个更复杂的工作过程,允许我们使用更少的数据点对手动创建碰撞多边形。这个过程包括使用 GIMP 中的路径工具手工创建一个碰撞多边形,这给了我们更多的控制来生成尽可能少的数据点。我们还看了 CodeAndWeb PhysicsEditor 软件,它可以跨所有游戏开发平台使用。

在下一章,我们将看看如何在游戏中实现增强的游戏元素,包括得分引擎、对手、宝藏、自动攻击引擎和物理模拟。

十七、增强游戏性:创建得分引擎,增加宝藏和敌人自动攻击引擎

现在,我们已经为游戏实现了碰撞检测,并为评分引擎奠定了基础,让我们完成从。checkCollisions()方法,然后添加一些更多的游戏元素来利用这个评分引擎,以及使游戏玩起来更有趣。为了实现我们的评分引擎显示,我们将在 InvinciBagel.java 类中创建一个 gameScore 整数变量和 scoreText 文本对象。我们还将创建一个 scoreFont 字体对象,并使用它来设置 scoreText 文本对象的样式,使其更加突出。我们还将了解如何在。scoringEngine()方法,并使用它来确定 InvinciBagel 与哪种类型的 Actor 对象发生了冲突。当然,我们会相应地增加 gameScore 变量。

我们还将创建一个 Treasure.java 职业,这样我们就可以给游戏添加有价值的奖励,让我们的无敌猎犬角色在躲避敌人不断的攻击时获得奖励,这些敌人来自无敌猎犬,或简称为 iBeagle,它既可以发射致命子弹(iBullets),也可以发射奶油干酪球(iCheese)抛射物。在本章中,我们还将创建这些 Enemy.java 和 Projectile.java 类,因此,在本章的课程中,我们将使用您在本书中学到的所有知识来创建一些非常高级的 Java 8 类和方法。

一旦我们有了敌人和抛射职业,真正的挑战是创建一个自动攻击引擎,这样游戏本身就可以和玩家对抗,这样我们就可以创建这个游戏的单人版本。我们将通过将 Enemy.java 类连接到 GamePlayLoop.java 类,通过调用敌人类来实现这一点。来自 GamePlayLoop 类的 update()方法。handle()方法,这将允许我们为 iBeagle 敌人对象利用 JavaFX 脉冲计时引擎。一旦完成,我们就可以给敌人编码,赋予它自己的生命,让它随机出现在屏幕上,并通过发射致命的子弹或美味的奶酪球来攻击无敌面包圈。

我们将逻辑地逐步构建这个自动攻击引擎,首先让 iBeagle 敌人出现在屏幕的两侧,然后翻转以正确面对 iBagel 角色。然后,我们将添加编程逻辑,将 iBeagle 动画到屏幕上,然后退出屏幕。然后我们会让他射出一颗子弹或奶油奶酪球,然后我们会添加一些时间代码,以使他的运动更加真实。

之后,我们将在外观、位置和移动上添加随机性,这样游戏玩家就无法判断 iBeagle 攻击来自哪里。之后,我们将添加物理模拟,以便子弹和奶油干酪球受到阻力和重力的影响,所有这些都将使游戏越来越逼真,随着这一章的进行,直到书的结尾!

我希望你在这本书的过程中享受你的学习经历,就像我喜欢为你写这本书一样。现在,随着我们对 Java 8 和 JavaFX 类以及编程技术的了解越来越多,让我们开始让我们的游戏变得越来越具有挑战性和专业性。

创建乐谱 UI 设计:文本和字体对象

要实现评分引擎的显示,我们需要做的第一件事是在 NetBeans 中打开“InvinciBagel.java”选项卡,添加一个整数变量来保存数字分数累积,并添加一个文本 UI 元素对象来在屏幕底部显示分数,与其他 UI 元素放在一起。JavaFX 中的 Text 类用于处理游戏中的文本元素,甚至有自己的 javafx.scene.text 包,因为文本是应用中的重要元素。整数数据类型允许您的游戏玩家获得数十亿分,因此这应该足以容纳您的游戏玩家可以获得的任何数量级的分数。我们将把这些 Java 变量和对象声明(在图 17-1 中突出显示)放在 InvinciBagel.java 类的最顶端,就在宽度和高度常量声明之后,Java 代码应该如下所示:

int``gameScore

Text``scoreText

A978-1-4842-0415-3_17_Fig1_HTML.jpg

图 17-1。

Add an integer variable named gameScore and initialize it to 0, then add a Text object named scoreText

将鼠标悬停在红色波浪错误突出显示上,并单击包含此错误的代码行旁边,以选择它(用浅蓝色显示)。接下来,使用 Alt-Enter 工作进程弹出错误解决帮助器,如图 17-1 底部所示,双击“为 javafx.scene.text.Text 添加导入”选项,让 NetBeans 为您编写此文本类导入语句。

现在我们准备打开。createSplashScreenNodes()方法,并使用 Java new 关键字和 Text()构造函数方法实例化这个名为 scoreText 的文本对象。完成此操作后,您可以调用。setText()方法,并使用 String.valueOf()方法引用 gameScore 整数,并使用。setLayoutX()和。setLayoutX()方法,使用下面的 Java 代码如图 17-2 所示:

scoreText =``new

scoreText``.setText``(String.valueOf(``gameScore

scoreText``.setLayoutY

scoreText``.setLayoutX

A978-1-4842-0415-3_17_Fig2_HTML.jpg

图 17-2。

Instantiate a scoreText object using a Java new keyword and call the .setText() and .setLayout() methods

现在,我们将使用 365,565 的 X,Y 位置值,如图 17-2 所示。为了能够样式化这个文本对象,我们需要声明一个私有的字体对象,名为 scoreFont,如图 17-3 所示。在红色错误突出显示处再次使用 Alt-Enter 工作流程,并选择“为 javafx.scene.text.Font 添加导入”选项。

A978-1-4842-0415-3_17_Fig3_HTML.jpg

图 17-3。

Add a private Font scoreFont declaration and use Alt-Enter and select “Add import javafx.scene.text.Font”

在我们可以使用这个 scoreFont 字体对象来设置 scoreText 文本对象的样式之前,我们需要使用root.getChildren().add(scoreText); Java 语句将这个 scoreText 文本节点对象添加到 JavaFX 场景图中,该语句在图 17-4 中突出显示。如果你忘记这样做,当你点击“玩游戏”按钮后,你将只能在游戏屏幕上看到白色!

A978-1-4842-0415-3_17_Fig4_HTML.jpg

图 17-4。

Add the scoreText Text Node object to the JavaFX Scene Graph, using a .getChildren().add() method chain

现在我们可以看到场景中的文本对象,下一步是使用 Java new 关键字和 Font(String fontName,int fontSize)构造函数方法调用实例化 scoreFont Font 对象。完成此任务的 Java 语句在图 17-5 中突出显示,应该如下所示:

scoreFont =``new``Font(``"Verdana", 20

A978-1-4842-0415-3_17_Fig5_HTML.jpg

图 17-5。

Instantiate scoreFont object with a Java new keyword and Font(String fontName, int fontSize) constructor

工作流程的下一步是使用。setFont()方法,从 scoreText 对象中调用,并使用以下代码设置文本对象以利用 scoreFont 字体对象,如图 17-5 的底部所示。还可以使用对 scoreText 文本对象调用的. setFill()方法来设置文本对象的颜色;现在,我们将使用一种颜色。黑色常数。

scoreText``.setFont

scoreText``.setFill

现在,我们可以测试 scoreText 文本对象的位置,并优化我们的 X,Y 屏幕位置值,以便分数就在 HBox UI 按钮库的旁边。我发现我必须将 Y 位置向下移动 20 个像素到 385 的像素位置,而我必须将 X 位置向右移动 5 个像素,到 445 的像素位置,如图 17-6 所示,在屏幕截图的左半部分,在 InvinciBagel 游戏的右下角,法律按钮旁边。

A978-1-4842-0415-3_17_Fig6_HTML.jpg

图 17-6。

Test the scoreText and scoreFont objects using the Run ➤ Project work process to refine their placement

我们将在本章的下一节添加一个文本对象标签,上面写着“SCORE:”来标记玩家的分数。第二轮编码的结果显示在图 17-6 的右半部分,在右下角。

创建乐谱标签:添加第二个文本对象

让我们在 scoreText 文本对象的前面添加一个文本标签,而不是在 UI 按钮库(HBox)的右侧只显示一个数字。我们将创建一个 scoreLabel 文本对象,并使用我们在上一节中创建的相同字体对象来设置该文本对象的样式。我将使用scoreText.setFill(Color.BLUE); Java 语句将分数文本的数字部分更改为蓝色,并使用以下 Java 代码将 SCORE: label 改为黑色,这也可以在图 17-7 中看到:

scoreText``.setFill``(``Color.BLUE

scoreLabel =``new

scoreLabel``.setText``("``SCORE:

scoreLabel``.setLayoutY

scoreLabel``.setLayoutX

scoreLabel.setFont(scoreFont);

scoreLabel``.setFill``(``Color.BLACK

A978-1-4842-0415-3_17_Fig7_HTML.jpg

图 17-7。

Add the scoreLabel object instantiation and configuration method calls underneath the scoreText object

正如你在图 17-8 中看到的,你需要记住将第二个 scoreLabel 文本节点添加到场景图根对象,它以前是一个 StackPane,但现在是一个 Group 对象。这是使用一个方法链来完成的,现在你应该已经非常熟悉了:root.getChildren().add(scoreLabel);注意,addNodesToStackPane()方法已经开始被更多地使用了,因为我们在游戏中加入了更多的 UI 元素。尽管 gameScore 变量会在游戏过程中动态更新,但这些文本对象本质上是静态的,因为它们是在启动时声明、实例化、配置和定位的,就像其他 UI 元素一样。

A978-1-4842-0415-3_17_Fig8_HTML.jpg

图 17-8。

Add the scoreLabel Text Node object to the JavaFX Scene Graph using a .getChildren().add() method chain

正在创建评分引擎逻辑:。scoringEngine()

在这个游戏中,我想评分的基础是对象的类型,因为我们将有固定的 Actor 子类对象供无敌组合评分,例如 Prop、PropV、PropH、PropB、宝藏、投射物、敌人等等。由于 switch-case 结构不支持在其评估逻辑中使用对象,我们将不得不使用条件 if()语句,以及 Java instanceof 操作符,从它的名字就可以看出,它用于确定对象类型或实例;在这种特殊情况下,首先,如果 Actor 对象是 Prop、PropV、PropH 或 PropB 类的实例。scoringEngine()方法的基本 Java 代码结构为每个 Actor 对象类型计算一个 if(),然后在 invinciBagel 对象内部设置由 scoreText 文本对象显示的 gameScore 变量。这种编程逻辑都可以在图 17-9 中看到。Java 代码应该是这样的:

private void``scoringEngine``(Actor``object

if(``object instanceof Prop``)  { invinciBagel.gameScore``+=5

if(object instanceof``PropV``) { invinciBagel.gameScore``+=4

if(object instanceof``PropH``) { invinciBagel.gameScore``+=3

if(object instanceof``PropB``) { invinciBagel.gameScore``+=2

invinciBagel.scoreText``.setText``(String``.valueOf

}

A978-1-4842-0415-3_17_Fig9_HTML.jpg

图 17-9。

Code basic conditional if() statements inside of the .scoringEngine() method using instanceof

确保不要在这些条件 if()语句中调用使用. setText()方法更新 scoreText UI 元素的invinciBagel.scoreText.setText(String.valueOf(invinciBagel.gameScore));语句。相反,请注意,我把它放在了方法的末尾,在所有这些计算完成之后。我这样做是为了在条件 if()对象处理结构的末尾,只需要调用这一行代码一次。也就是说,如果将这个 scoreText 对象分数更新语句放在每个条件 if()语句代码体结构中,Java 代码仍然可以工作。然而,我试图向您展示如何编写代码,通过使用相对较少(十几行或更少)的代码来完成主要的 Java 8 游戏开发任务,从而完成大量工作。

接下来,我们将从。checkCollision()方法,我们将把它放在 scoringEngine()方法中。只要是这两个方法中的一个,它就会在碰撞时被调用,不管怎样。我这样做的原因是,当玩家发现宝藏或被炮弹击中时,我们可能会想要播放不同的音效。这样,当检测到不同类型的碰撞时,您的音频将与得分和游戏相关联,而不仅仅是为任何碰撞播放音频。

我们的条件 if()结构代码体使用花括号,这允许我们将 Java 语句添加到每种类型的碰撞(计分)对象实例(类型)中。因此,除了增加(或减少,我们将在后面添加)分数,我们还可以为不同的对象使用不同的声音(音频剪辑)。让我们添加。playSound()方法接下来调用这些条件 if()代码块,这样我们就有了代码,当主要角色捡起一个宝藏,或者当他被敌人(无敌猎犬)角色发射的炮弹击中(或接住)时,或者当他与场景中的道具碰撞时,就会触发声音效果。

这是通过使用以下 Java 条件 if()结构来实现的,也可以在图 17-10 中看到:

private void``scoringEngine``(Actor``object

if(``object instanceof Prop

invinciBagel.``gameScore

invinciBagel.``playiSound0

}

if(object instanceof``PropV

invinciBagel.``gameScore

invinciBagel.``playiSound1

}

if(object instanceof``PropH

invinciBagel.``gameScore

invinciBagel.``playiSound2

}

if(object instanceof``PropB

invinciBagel.``gameScore

invinciBagel.``playiSound3

}

invinciBagel.scoreText``.setText``(String``.valueOf``(invinciBagel.``gameScore

}

现在,我们有了更多的游戏互动,因为记分板将在每次碰撞时立即正确更新,并同时播放一段音频剪辑,以指示已检测到碰撞、已发生的碰撞类型(好的或坏的)以及记分板已更新,因为毕竟这个编程逻辑包含在 scoringine()方法中,因此应该以某种方式与行为或得分相关。

在我们测试了这个条件 if()编程逻辑并确保一切按预期运行之后,我们可以看看如何在其中添加优化。scoringEngine()方法,然后我们将准备添加更多的演员类型,如 Treasure.java 类,我们将添加下一步。之后,我们可以添加一些敌人到 InvinciBagel 游戏中,因为在本章的过程中,我们会继续添加一些功能,使我们的游戏更加有趣和刺激。

A978-1-4842-0415-3_17_Fig10_HTML.jpg

图 17-10。

Add .playiSound() method calls in each if() statement body to play different audio for each type

使用“运行➤项目”工作流程测试游戏。如图 17-11 所示,记分牌起作用了!

A978-1-4842-0415-3_17_Fig11_HTML.jpg

图 17-11。

Use Run ➤ Project to test the game, and collide with the objects on the screen, updating your scoreboard

优化 scoringEngine()方法:使用逻辑 If Else If

虽然前面的 if()语句系列可以完成我们在这里尝试的工作,但是我们确实需要模拟break;语句,如果我们使用 Java switch-case 语句类型,我们就可以使用它了。为了优化这种方法,一旦确定了碰撞中涉及的物体类型,我们就要打破这种评估系列。我们可以通过使用 Java else-if 功能将所有这些条件 if()语句连接在一起来做到这一点。如果这个结构中的一个条件被满足,if-else-if 结构的其余部分不被评估,这相当于 switch-case 结构中的一个break;语句。为了进一步优化,您可能希望将最常见的(该对象类型的场景中可碰撞对象数量最多的)对象放在 if-else-if 结构的顶部,将最不常见的对象放在该结构的底部。你所要做的就是通过使用 Java else 关键字将 if()条件块连接在一起,如图 17-12 所示。这创建了一个更紧凑、处理优化的条件求值结构,并使用了更少的 Java 代码行:

private void scoringEngine(Actor``object

if(``object

invinciBagel.gameScore+=5;

invinciBagel.playiSound0();

}``else if

invinciBagel.gameScore+=4;

invinciBagel.playiSound1();

}``else if

invinciBagel.gameScore+=3;

invinciBagel.playiSound2();

}``else if

invinciBagel.gameScore+=2;

invinciBagel.playiSound3();    }

invinciBagel.scoreText.``setText``(String.valueOf(invinciBagel.``gameScore

}

A978-1-4842-0415-3_17_Fig12_HTML.jpg

图 17-12。

Make all the previously unrelated if() structures into one if-else-if stucture by inserting else in between ifs

增加游戏的赏金:Treasure.java 类

为了让我们的游戏更刺激,让我们添加一个 Treasure.java 类,这样我们的游戏玩家在玩游戏时就有东西可以找了,这样他们就可以在评分引擎就位后给自己的分数加分。这种类型的 Treasure 对象将利用 hasValu 和 isBonus 布尔标志,它们是我们在抽象 Actor 类中安装的,我们将在 Treasure()构造函数方法中将它们设置为 true 值,接下来我们将编写代码,同时覆盖所需的。update()方法,以便在开发的后期我们可以添加动画和宝藏处理逻辑。与 Prop、PropV、PropH 和 PropB 类一样,该类将使用 xLocation 和 yLocation 参数来设置 spriteFrame ImageView 节点对象的 translateX 和 translateY 属性,该对象将作为 Treasure()构造函数方法编程逻辑的一部分,位于 Treasure 对象(Actor 对象类型,也称为 Actor 子类)的内部。该类的 Java 代码也可以在图 17-13 中看到,应该如下所示:

package invincibagel;

import javafx.scene.image.Image;

public class``Treasure

public Treasure(String SVGdata, double xLocation, double yLocation, Image... spriteCels){

super(SVGdata, xLocation, yLocation, spriteCels);

spriteFrame.setTranslateX(xLocation);

spriteFrame.setTranslateY(yLocation);

hasValu = true;

isBonus = true;

}

@Override

public void update() {   // Currently this is an Empty but Implemented Method   }

}

A978-1-4842-0415-3_17_Fig13_HTML.jpg

图 17-13。

Create a new Java class named Treasure.java extends Actor and create Treasure() and update() methods

使用宝藏类:在游戏中创建宝藏

现在我们有了一个 Treasure.java 类,我们可以为游戏创建宝物了。如你所知,这将在 InvinciBagel.java 课上完成。声明 package protected(因为我们将在 Bagel.java 的 InvinciBagel 之外引用它们)Treasure 对象,并将它们命名为 iTR0 和 iTR1(代表 InvinciBagel Treasure)。将 iT0 和 iT1 图像对象声明添加到私有图像对象复合声明的末尾。分别使用 Java new 关键字、Image()构造函数方法以及 treasure1.png 和 treasure2.png 图像资源实例化 iT0 和 iT1 图像对象。之后,您可以创建 iTR0 和 iTR1 宝藏对象,使用 Treasure()构造函数方法,SVG 路径数据为 0,0,0,64,64 和 64,0,位置分别为 50,105 和 533,206。确保将这些新的 Treasure 对象的 spriteFrame ImageView 节点添加到 JavaFX 场景图中,使用。getChildren()。add()方法链,从根组对象调用。如图 17-14 所示,代码没有错误,我们现在准备测试新的 Treasure.java 类和我们添加到 InvinciBagel.java 类中的 Java 代码,看看我们是否能在游戏场景中获得宝藏。您的 Java 代码应该如下所示:

Treasure``iTR0``,``iTR1

private Image iB0, iB1, iB2, iB3, iB4, iB5, iB6, iB7, iB8, iP0, iP1,``iT0``,``iT1

iT0 =  new Image(" /treasure1.png ", 64, 64, true, false, true);  // .loadImageAssets()

iT1``=  new Image("``/treasure2.png

iTR0 = new Treasure("M0 0 L0 64 64 64 64 0 Z", 50 , 105 , iT0);  // .createGameActors() Method

iTR1 = new Treasure("M0 0 L0 64 64 64 64 0 Z", 533 , 206

root.getChildren().add(``iTR0

root.getChildren().add(``iTR1

A978-1-4842-0415-3_17_Fig14_HTML.jpg

图 17-14。

After adding declarations at the top of the class, instantiate the objects, and add them to a Scene Graph

让我们花一点时间,用一个运行➤项目的工作流程,看看我们的宝贝演员是否在屏幕上!

正如你在图 17-15 的左半部分所看到的,所有的宝物在场景中都是可见的,我们已经准备好开始增强我们的视觉效果了。scoringine()方法,支持负面评分以及添加宝藏评分值。

A978-1-4842-0415-3_17_Fig15_HTML.jpg

图 17-15。

Check Treasure placement (left) and test negative collision values (right) and Treasure collision detection

正如你很快会看到的(在图 17-15 的右边),Java int (integer)数据类型支持负值,所以你只需要在你的。scoringEngine()方法是将您希望游戏玩家避免的 Actor 对象的+=更改为-=。在我们开发场景中,我们将使用与道具对象的碰撞来给出一个负分。

加宝碰撞检测:更新中。scoringEngine()

通过将+=值更改为-=1 或-=2 来更改您的道具演员对象评分,以减少记分板值,然后在现有 if-else-if 链的末尾添加一个 else-if{}部分,以支持宝藏对象,我们将为其评分+=5,以增加 5 分。完成这项工作的 Java 代码,如图 17-16 所示,应该如下所示:

private void scoringEngine(Actor``object

if(object instanceof Prop)         {

invinciBagel.gameScore``-=1

invinciBagel.playiSound0();

} else if(object instanceof PropV) {

invinciBagel.gameScore``-=2

invinciBagel.playiSound1();

} else if(object instanceof PropH) {

invinciBagel.gameScore``-=1

invinciBagel.playiSound2();

} else if(object instanceof PropB) {

invinciBagel.gameScore``-=2

invinciBagel.playiSound3();

} else if(``object``instanceof``Treasure

invinciBagel.gameScore``+=5

invinciBagel.``playiSound4

invinciBagel.scoreText.setText(String.valueOf(invinciBagel.gameScore));

}

A978-1-4842-0415-3_17_Fig16_HTML.jpg

图 17-16。

Add a Treasure else-if structure to end of the .scoringEngine() method conditional if() structure

你需要记住将宝藏对象添加到 castDirector 对象中,如图 17-17 所示,因为碰撞检测引擎使用这个 CastingDirector 类(对象)来管理碰撞检测过程。如果你不做这一步,无敌舰队将会直接越过宝藏物体而看不到(撞上)它们!

A978-1-4842-0415-3_17_Fig17_HTML.jpg

图 17-17。

Add iTR0 and iTR1 Treasure objects inside the .addCurrentCast(Actor. . .) method call

现在,我们已经将这两个宝藏对象添加到 castDirector CastingDirector 对象中,碰撞检测编程逻辑将“看到”它们,并与宝藏对象发生碰撞,从而触发评分引擎对游戏进行正确评分。测试在你的游戏中实现宝藏的最终代码,并确保它可以工作,然后我们可以继续添加对手,让他们向无敌的角色发射射弹。

添加敌人:敌人和抛射体类

既然我们已经在游戏中加入了正面的(宝藏)元素,那就让我们在游戏中加入一些负面的(敌人和抛射物)元素,让我们在开发工作过程中保持“平衡”。我们将使用演员超类,而不是英雄超类来创建敌人和投射物类(物体)。这是因为这样做,我们有一个更优化的游戏,因为我们只使用一个单一的。collide()方法(记住每个 Hero 对象都实现了一个. collide()方法)供 JavaFX 脉冲事件引擎处理。当我把这个游戏转换成多人游戏时(这个游戏的代码超出了这个标题的初学者性质),我会想让敌人职业成为一个英雄子类,这样敌人角色就可以像 InvinciBagel 一样与诸如宝藏和投射物之类的东西碰撞。因为敌人的阶级仍然有一个。update()方法,它可以在屏幕上四处移动,它可以(并将)从隐藏中出来,向 InvinciBagel 角色发射子弹(负面效果)和奶油干酪球(正面效果)。英雄职业的唯一区别是。collide()方法,对于这个版本的游戏,有一个。collide()方法来处理每个脉冲允许我们优化游戏播放处理,同时仍然拥有一个具有街机游戏将包括的不同游戏功能的游戏。正如您在 Enemy()构造函数方法中看到的,敌人角色 isAlive、isBonus 和 hasValue,因此在使用。setTranslateX()和。setTranslateY(),在使用 super()构造函数将 SVG 数据、图像和初始 X,Y 位置数据传递给 Actor()构造函数之后。在图 17-18 中可以看到 Enemy.java 类的 Java 代码没有错误,应该看起来像下面的 Java 类结构:

package invincibagel;

import javafx.scene.image.Image;

public class``Enemy``extends``Actor

public``Enemy

super(SVGdata, xLocation, yLocation, spriteCels);

spriteFrame.setTranslateX(xLocation);

spriteFrame.setTranslateY(yLocation);

isAlive = true;

isBonus = true;

hasValu = true;

}

@Override

public void``update

// Currently Empty Method

}

}

A978-1-4842-0415-3_17_Fig18_HTML.jpg

图 17-18。

Create an Enemy.java class, override the .update() method, and code your Enemy() constructor method

工作流程的下一步是使用 GIMP 为敌方物体创建碰撞多边形,如图 17-19 所示,仅使用 9 个数据点。使用你在第十六章中学到的 SVG 数据创建工作流程。

A978-1-4842-0415-3_17_Fig19_HTML.jpg

图 17-19。

Use GIMP to create a nine-point collision polygon for your Enemy Actor

在我们进入 InvinciBagel 类来声明和实例化一个名为 iBeagle 的敌人对象之前,让我们创建 Projectile.java 类,这样我们的敌人对象就有抛射物来射向 InvinciBagel 角色!

创建奶油干酪子弹:编码 Projectile.java 类

既然我们已经在游戏中加入了正面(宝藏)元素,那我们就在游戏中加入一些负面(敌人和弹丸)元素,让我们在开发工作过程中保持“平衡”!创建一个 Projectile.java 类和构造函数方法,将 isFixed 设置为 false(因为射弹会飞)并将 isBonus 和 hasValu 设置为 true,这样就设置了对象属性。这个 Projectile.java 类的 Java 代码如图 17-20 所示,如下所示:

package invincibagel;

import javafx.scene.image.Image;

public class``Projectile

public``Projectile

super(SVGdata, xLocation, yLocation, spriteCels);

spriteFrame.setTranslateX(xLocation);

spriteFrame.setTranslateY(yLocation);

isFixed = false;

isBonus = true;

hasValu = true;

}

@Override

public void``update

}

A978-1-4842-0415-3_17_Fig20_HTML.jpg

图 17-20。

Create a Projectile.java class, override the .update() method, and code a Projectile() constructor method

给游戏增加敌人和投射物:InvinciBagel.java

在 NetBeans 中打开 InvinciBagel.java 选项卡,在 iBagel Bagel 对象下声明一个名为 iBeagle 的敌方对象和两个名为 iBullet 和 iCheese 的抛射体对象,如图 17-21 中突出显示的。接下来,通过在第二个私有映像声明的末尾添加对象名,声明三个映像对象 iE0、iC0 和 iC1。

A978-1-4842-0415-3_17_Fig21_HTML.jpg

图 17-21。

Declare iBeagle, iBullet, iCheese (Enemy and Projectile) objects, and iE0, iC0, iC1 (Image) objects

将 enemy.png、bullet.png 和 cheese.png 的图像资产复制到/src 文件夹中,如图 17-22 所示。

A978-1-4842-0415-3_17_Fig22_HTML.jpg

图 17-22。

Copy the enemy.png, bullet.png, and cheese.png image assets into your InvinciBagel/src project folder

然后,你可以通过用鼠标点击并拖动这些点来逐个数据点地细化你的碰撞多边形结构数据点,如图 17-23 所示。如果你比较图 17-22 中的碰撞多边形和图 17-23 中的碰撞多边形,你可以看到我已经优化了几个数据点,以更好地符合精灵的轮廓。

Enemy``iBeagle

Projectile``iBullet``,``iCheese

private Image iB0, iB1, iB2, iB3, iB4, iB5, iB6, iB7, iB8, iP0, iP1, iT0, iT1,``iE0``,``iC0``,``iC1

iE0 =  new Image(" /enemy.png ", 70, 116, true, false, true);       // .loadImageAssets()

iC0``=  new Image("``/bullet.png

iC1``=  new Image("``/cheese.png

iBeagle = new Enemy("M0 6 L0 52 70 52 70 70 70 93 115 45 115 0 84 0 68 16 Z", 520 , 160

iBullet = new Projectile("M0 4 L0 16 64 16 64 4 Z", 8 , 8

iCheese = new Projectile("M0 0 L0 32 29 32 29 0 Z", 96 , 8 , iC1); // .createGameActors() Method

root.getChildren().add(``iBeagle

root.getChildren().add(``iBullet

root.getChildren().add(``iCheese

castDirector.addCurrentCast(iPR0, iPH0, iPV0, iPB0, iTR0, iTR1,``iBeagle``,``iBullet``,``iCheese

A978-1-4842-0415-3_17_Fig23_HTML.jpg

图 17-23。

Instantiate Image and Projectile objects and add them to JavaFX Scene Graph and CastingDirector object

当我们向游戏中添加游戏设计元素时,让我们继续学习如何使用 splashscreen ImageView 节点对象来实现游戏背景图像板。我们将这样做,以便我们的白色 iBeagle,以及(奶油)iCheese 和 iBullet 对象,使用增强的蓝色背景颜色,更好地突出显示给游戏玩家。

添加背景图像:使用。toBack()方法

在我们开始为敌人和投射物体编写碰撞和得分例程之前,让我们在向游戏添加物体的过程中添加一个图像资产(物体)以在背景板中使用。将图 17-24 左下角所示的 skycloud.PNG8 位 png 8 图像资产从图书库中复制到 netbeans projects/InvinciBagel/src 文件夹中。完成此操作后,在闪屏相关图像资产的私有图像对象声明的末尾添加一个 skyCloud 图像对象声明,如图 17-24 顶部所示。正如您所看到的,在您在 Java 代码中实现(使用)它之前,这个对象下会有一个警告高亮显示。接下来,实例化一个 skyCloud 对象。loadImageAssets(),如图 17-24 底部所示,使用以下代码:

private Image splashScreen, instructionLayer, legalLayer, scoresLayer,``skyCloud

skyCloud = new Image(" /skycloud.png

A978-1-4842-0415-3_17_Fig24_HTML.jpg

图 17-24。

Declare and instantiate a skyCloud Image object that references a skycloud.png background image asset

现在您有了一个 skyCloud 对象,因此使用. setImage()方法来设置一个 splash screen background ImageView 节点,以便在 GAME Button . setonaction(action event)代码块中使用该图像资源,这样当您单击 PLAY GAME 按钮时,该图像将被设置为背板图像。此外,请确保使用。值为 true 的 setVisible()方法调用,以便 ImageView 节点可见。如图 17-25 所示的 Java 代码应该是这样的:

splashScreenBackground.setImage(``skyCloud

splashScreenBackground.setVisible(``true

由于您已经将此代码添加到了 gameButton 的事件处理代码中,因此您必须“反击”这一举动,方法是使用下面的 Java 代码向其他三个按钮事件处理器添加相同的方法调用,以将图像资产设置回闪屏图像资产,如图 17-25 中的其他三个按钮事件处理方法所示:

splashScreenBackground.setImage(``splashScreen

A978-1-4842-0415-3_17_Fig25_HTML.jpg

图 17-25。

Use a splashScreenBackplate.setImage() method call to install a skyCloud Image object in gameButton

使用运行➤项目来测试代码。正如您在图 17-26 左侧看到的,我们有一个 z 索引问题!

A978-1-4842-0415-3_17_Fig26_HTML.jpg

图 17-26。

Install the skyCloud background Image (left), and use a .toBack() method call to set proper z-index (right)

因为我们想让我们的背景图像在最低(零)z-index 处,所以它将在我们所有游戏资产的后面,但也让所有闪屏资产在最高 z-index 处,所以那些图像板将覆盖(在)我们所有游戏资产的前面,我们通常必须实现另一个 ImageView 合成板,才能做到这一点。但是,有一组方便的 z 索引相关的节点类方法,允许我们使用 SplashScreenBackplate ImageView 对象同时保存游戏 splashscreen 和游戏背景图像板!这是我想实现的 ImageView 节点优化之一,以保持我们的游戏节点最少,减少内存和处理开销。将 ImageView 放在游戏资源后面的代码将调用。toBack()方法,该方法将该节点重新定位到 JavaFX 场景图形节点堆栈的后面(底层)。这相当于将该节点的 z 索引设置为零。在图 17-26 的顶部可以看到用浅蓝色突出显示的 Java 语句,您的gameButton.setOnAction((ActionEvent)->{}事件处理结构的完整 Java 代码应该如下所示:

gameButton.setOnAction((ActionEvent) -> {

splashScreenBackplate.setImage(``skyCloud

splashScreenBackplate.setVisible(``true

splashScreenBackplate``.toBack()

splashScreenTextArea.setVisible(``false

});

A978-1-4842-0415-3_17_Fig27_HTML.jpg

图 17-27。

Use a .toBack() method call in the gameButton code, and .toFront() method call in the other Button code

您可能已经意识到,我们需要在其他三个按钮事件处理结构中“对抗”这种移动。我们将使用。toBack()方法调用,这当然是。toFront()方法调用。如您所见,我们不仅需要调用。toFront()方法,但也可以从 splashScreenBackplate ImageView 节点对象以及保存 UI 按钮控件的 buttonContainer HBox 对象中删除。我们将需要调用所有这些 Splashscreen 和 UI 对象的这个方法,以便所有这些对象都回到 JavaFX 场景图节点堆栈的前面。如图 17-27 所示的 Java 代码如下所示:

helpButton .setOnAction((ActionEvent) -> {

splashScreenBackplate.setImage(``splashScreen

splashScreenBackplate.toFront();

splashScreenBackplate.setVisible(``true

splashScreenTextArea.setVisible(``true

splashScreenTextArea.setImage(instructionLayer);

splashScreenTextArea.toFront();

buttonContainer.toFront();

});

使用随机数生成器:java.util.Random

java.util 包包含编程实用程序,您可以在 Java 8 游戏开发中使用这些程序,您可能已经从包名中猜到了。对于游戏程序员来说,最重要的 Java 工具之一是 Random 类及其 Random()构造函数方法。这个类可以用来创建随机数生成器对象,它生成随机数(或布尔值)供游戏编程逻辑使用。我们将使用这个类来生成 int(整数,用于随机屏幕位置)和 boolean(用于“猜测敌人从哪里来”函数)随机值。这些将确保游戏玩家不能通过在游戏过程中识别模式来预测游戏。Java 8 Random 类是专门使用 Java Object master 类生成随机数的临时代码。Random 类的类层次结构如下所示:

java.lang.Object

> java.util. Random

Random 类有两个构造函数,一个是我们将使用的 Random()构造函数,另一个是重载的 Random(long seed)构造函数,您可以在其中为该类实现的随机数生成器指定种子值。一旦你有了一个随机对象,你可以调用 22 种方法中的一种来生成不同类型的随机值。本章中我们将用到的方法调用是。nextInt(int bound)和。nextBoolean()方法调用。如果您想知道,还有一个. nextInt()方法,但是我们需要生成一个特定范围内的随机数(从零到屏幕底部),并且。nextInt(int bound)允许在方法调用内部生成一个从零到指定整数界限(boundary)的随机数。

我们将在 Enemy.java 类中使用随机类作为我们敌人的攻击策略(和代码)。让我们继续在 Enemy.java 类的顶部声明并实例化一个名为 randomNum 的随机对象,如图 17-28 所示,使用下面的复合(声明加实例化)Java 8 编程语句:

protected static final Random``randomNum

在本章的剩余部分,我们将使用这个随机(数字生成器)对象来为我们的敌人 iBeagle 对象添加随机攻击位置、攻击面和子弹,这样他就可以尽最大努力打倒 InvinciBagel 角色(游戏玩家),或者如果可以的话,至少让他产生大量的负得分点!

A978-1-4842-0415-3_17_Fig28_HTML.jpg

图 17-28。

Declare and instantiate a Random Number Generator at the top of the Enemy.java class using Random()

发动攻击:编码敌人的冲击

首先,我们需要使用下面两条 Java 语句声明计数器变量,如图 17-29 所示:

int``attackCounter

int``attackFrequency

A978-1-4842-0415-3_17_Fig29_HTML.jpg

图 17-29。

Add integer variables at the top of the Enemy.java class; set attackCounter=0, and attackFrequency=250

现在我们准备开始在 Enemy.java 类中放置半打相当复杂的方法。这些将使用 GamePlayLoop 类的。handle()方法,利用 JavaFX 60 FPS 脉冲计时事件引擎,驱动完全自动化、完全随机化的敌人攻击。在本章余下的时间里,我们将要编写的代码相当于把计算机处理器变成了我们游戏玩家的对手。我们来写代码吧!

敌人阶级攻击的基础。update()方法

让我们从编写我们的 iBeagle 敌人自动攻击引擎的基础开始。我们要做的第一件事是“节流”60 FPS 脉冲引擎,并确保攻击每四秒钟发生一次。这是使用 if()结构中的 attackCounter 和 attackFrequency 变量完成的,这两个变量之间进行计数。如果 attackCounter 达到 250,它将被重置为 0,并调用 initiateAttack()方法。否则,attackCounter 使用+=1 递增。您也可以使用 attackCounter++来完成这个任务。图 17-30 中间突出显示的基本条件 if()结构的代码应该类似于下面的 Java 方法:

public void update() {

if(``attackCounter >= attackFrequency

attackCounter=0;

initiateAttack();

} else {

attackCounter+=1;

}

}

A978-1-4842-0415-3_17_Fig30_HTML.jpg

图 17-30。

Create a conditional if using attackCounter inside the .update( ) method that calls initiateAttack( ) method

在这公认的高级章节中,我们将编写的大部分“自动攻击”代码将利用这一点。update()方法,该方法将从。从 GamePlayLoop 类运行游戏的 handle()方法。我安装这个的原因是。update()方法,您需要在每个 Actor 子类中覆盖它,以防您想要在游戏中制作动画。如果参与者是静态的,则。update()方法只是作为一个空的或未使用的方法存在。

在屏幕两侧攻击:。initiateAttack()方法

让你的攻击来自屏幕两侧的方法是有一个布尔变量,可以设置为右(真)或左(假),我们将称之为 takeSides。在敌方类的顶部声明这个变量,然后在你的。update()方法。在这个里面。initiateAttack()方法,创建一个空的 if-else 结构if(takeSides){}else{}来保存您的攻击编程逻辑,因此private void initiateAttack(){if(takeSides){}else{}}如果您正在编写超级紧凑的 Java 代码(这是有效的 Java 代码,但是到目前为止什么也没有做)。如果您遵循行业标准的 Java 8 代码格式化实践,您将用来实现这个空基础设施的 Java 方法体可以在图 17-31 中看到,应该如下所示:

boolean``takeSides

private void``initiateAttack()

if(``takeSides

// Empty Statement

} else { // Empty Statement }

}

A978-1-4842-0415-3_17_Fig31_HTML.jpg

图 17-31。

Add an if-else structure inside of the initiateAttack() method, to alternate between the left and right sides

在 if(takeSides){}结构中,使用spriteFrame.setTranslateX(500);将敌人对象(在 InvinciBagel 类中命名为 iBeagle)设置为 X 位置 500,并在屏幕上设置为随机高度,使用 spriteFrame.setTranslateY()方法调用并结合我们在本章上一节中安装的随机数生成器对象。如果你输入你的 randomNum 对象名,然后点击句点键,你会看到一些方法调用选项,如图 17-32 所示。双击 nextInt(int bound)选项,并在。setTranslateY()方法。

A978-1-4842-0415-3_17_Fig32_HTML.jpg

图 17-32。

Use a randomNum Random object inside of the .setTranslateY() method and use a period to call selector

接下来您要做的是声明一个名为 attackBoundary 的整型变量,该变量将在。nextInt()方法调用,这样,如果您愿意,以后可以在 Enemy.java 类顶部的一个易于编辑的位置更改 Y 轴(屏幕底部)边界。Java 语句应该如下所示:

int``attackBoundary

现在我们已经准备好完成翻转 iBeagle 敌人角色(sprite)的 Java 代码,这样他就面向正确的方向,然后我们就可以测试代码了。用合乎逻辑的、易于理解的步骤编写复杂的 Java 代码是很重要的。通过这种方式,您可以在编写代码时对其进行测试,确保编程逻辑的每个组件都能正常工作,而不会增加额外的复杂性。在本章中你会看到这个工作过程,因为我们在 Enemy.java 类中开发了一个健壮的自动攻击算法。这个自动攻击代码可以用于你将来创建的任何敌人物体;因此,您在本章中编写的代码将涵盖过多的坏人攻击!

这里需要注意的是,由于 takeSides 是布尔型的,并且只能有两个值——true 或 false,所以我们只需要实现一个简单的条件 if-else 结构。这是因为如果我们的 if(takeSides)条件等于 true,那么我们知道 false 值(else 条件)逻辑结构将处理 takeSides=false 场景。

在这两个 if{}和 else{}攻击逻辑处理结构中,我们将围绕 Y 轴翻转 sprite 图像,记住设置 isFlipH 变量以备将来使用,将 sprite X 位置设置为屏幕的一侧或另一侧,将 sprite Y 位置设置为屏幕上的随机高度值,然后将 takeSides 布尔变量设置为与其当前 true 或 false 数据值相反的值。这样,iBeagle 敌人演员对象将在屏幕的左侧和右侧交替出现。稍后,我们将使用。nextBoolean()方法,来自 Random 类,使攻击不可预测。请记住,我们是从简单开始的,随着代码的开发,复杂性会增加。基本 initiateAttack()方法体的 Java 代码在图 17-33 中显示无误,应该如下所示:

private void``initiateAttack()

if(``takeSides

spriteFrame.setScaleX(``1

this.setIsFlipH(``false

spriteFrame.setTranslateX(``500

spriteFrame.setTranslateY(``randomNum``.nextInt(``attackBoundary

takeSides =``false

}``else

spriteFrame.setScaleX(``-1

this.setIsFlipH(``true

spriteFrame.setTranslateX(``100

spriteFrame.setTranslateY(``randomNum``.nextInt(``attackBoundary

takeSides =``true

}

}

A978-1-4842-0415-3_17_Fig33_HTML.jpg

图 17-33。

Add the logic inside the if-else structure that flips the sprite and positions it on either side of the screen

给敌人提供能量。update()方法:使用 GamePlayLoop。handle()方法

在我们开始测试 Enemy.java 类中的代码之前。update()方法,我们必须将其“连接”到。GamePlayLoop.java 类中的 handle()方法。如您所知,该方法是我们进入 JavaFX 脉冲计时事件处理引擎的入口,该引擎以闪电般的 60 FPS 速度驱动我们的游戏。现在我们的游戏循环。handle()方法将更新 iBagel InvinciBagel 字符以及 iBeagle 敌人自动攻击编程逻辑。很容易看出为什么 InvinciBagel 和 InvinciBeagle 是敌人;这是一种身份危机,有点像那些拼错的域名纠纷!如图 17-34 所示,您的 Java 代码看起来如下:

@Override

public void``handle

invinciBagel.``iBagel

invinciBagel.``iBeagel

}

A978-1-4842-0415-3_17_Fig34_HTML.jpg

图 17-34。

Add a call to the invinciBagel.iBeagle.update() method inside of the GamePlayLoop .handle() method

使用“运行➤项目”工作流程来测试您的第一轮(级别)敌人自动攻击 Java 代码。如图 17-35 所示,InvinciBeagle 出现在屏幕的两侧,沿着 Y 轴的随机位置,并在屏幕的左侧和右侧之间交替出现。我让子弹和奶油干酪球投射体演员对象暂时在屏幕上可见,在左上角。我们最终会把这些放到屏幕外,每隔几秒钟就用无敌小猎犬的强力火箭筒向它射击。

为了保持我们对 JavaFX 场景图节点对象的使用得到优化,我们将在投射体对象击中 InvinciBagel 时重用它们。这种“子弹回收”将在几个方法中使用 Java 编程逻辑来完成,我们将在本章中继续编写越来越高级的游戏编程逻辑。

A978-1-4842-0415-3_17_Fig35_HTML.jpg

图 17-35。

Use the Run ➤ Project to test your code; left half shows left side attack, right half shows right side attack

现在我们准备让敌人在屏幕上和屏幕外移动,以增加惊喜的元素。我们将对这个动画进行编码,而不是使用另一个动画类,因为我们试图只使用一个 AnimationTimer 类(object)作为优化策略来做所有的事情,到目前为止效果非常好。

增加惊奇的元素:动画你的敌人攻击

为了让我们的敌人走上舞台,我们需要定义布尔变量来保持“屏幕上不在屏幕上”的状态,我称之为“屏幕上”,以及一个开关,一旦敌人出现在屏幕上,我可以按下它,告诉他发动攻击,我将命名为 callAttack。我们还需要整数变量来保存当前敌人 sprite 的左右 X 位置,名为 spriteMoveR 和 sprite Mover,以及一个目的变量来保存我们希望敌人停止和发射投射物体的位置。在图 17-36 的顶部附近可以看到突出显示的 Java 声明语句,应该类似于下面的 Java 代码:

boolean``onScreen

boolean``callAttack

int``spriteMoveR``,``spriteMoveL``,``destination

我们需要做的第一件事。update()方法,就是添加一个 if(!callAttack) if-else 条件结构围绕着我们已经有的 if(attack counter > = attack frequency)结构。我们将把attackCounter = 0;初始化语句留在这个内部循环中,我们将添加一个spriteMoveR = 700;和一个spriteMoveL = -70;初始化语句。这些会把敌人的精灵放在舞台两边的屏幕外。

callAttack 布尔标志允许我们在。update()和。如您所见,在。update()方法,在 attackCounter (timer)让玩家有足够的时间在敌人攻击后恢复理智之后,这个 callAttack 变量被设置为 true (attack)值。在更复杂的版本中。initiateAttack()方法,您将把这个 callAttack 变量设置为 false(延迟攻击)值,启动 attackCounter。

让我们也做一个 Java 代码优化。我们在 initiateAttack()方法中进行了两次 setTranslateY()方法调用,并且只进行了一次方法调用(这表示随机对象的使用节省了 100%)。nextInt()方法调用)。一旦所有这些编程语句就绪,您就可以最终将 callAttack 布尔变量设置为真值,以便下一次 if(!callAttack)条件 if-else 结构被调用,该结构底部的 else 部分将执行,并将调用 initiateAttack()方法。这种方法是真正繁重的工作,直到将敌人角色动画显示在屏幕上,让他暂停并开火,然后在 InvinciBagel 执行(碰撞)他之前撤退到屏幕外,获得 10 个有价值的得分点。

一旦在条件编程逻辑的 attackCounter timer 部分内将 callAttack 变量设置为 true 值,该 if-else 编程结构的 else 部分将调用 initiateAttack()函数。

一旦您的条件 if()计时器逻辑到期(完成其倒计时),您的敌人 sprite 将沿 Y 轴随机定位,并使用 spriteMoveR 和 spriteMoveL 变量移动到其起始位置,下次 callAttack 变量设置为 false 时,attackCounter 将重置为零。在 Java 语句的“设置发起攻击”序列的末尾,使用以下代码将 callAttack 设置为 true,如图 17-36 所示:

public void update() {

if(``!callAttack

if(attackCounter``>=

attackCounter =``0

spriteFrame.setTranslateY(randomNum.``nextInt

spriteMoveR =``700

spriteMoveL =``-70

callAttack``=``true

} else { attackCounter+=1; }

}``else``{``initiateAttack()

}

A978-1-4842-0415-3_17_Fig36_HTML.jpg

图 17-36。

Add callAttack, spriteMove and destination variables and an if(callAttack)-else programming structure

接下来,我们将重写 if(takeSides)逻辑结构,删除 Y 轴随机数定位语句,重新定位 takeSides 布尔标志程序逻辑,并添加 if(!屏幕上)嵌套结构,位于. setTranslateX()方法调用周围。这将允许我们在屏幕上和屏幕外制作敌人角色精灵的动画。

在 if(!takeSides)结构,您将保留设置 sprite 镜像(面向方向)的前两条语句,但删除。setTranslateY()方法调用,因为现在已经在。update()方法。添加 if(!屏幕上)条件结构,其中您将把目标位置初始化为 500 像素,然后嵌套另一个计数器 if(spriteMoveR >= destination)结构,在该结构中,您将使用spriteMoveR-=2;spriteFrame.setTranslateX(spriteMoveR);将 sprite 移动两个像素/脉冲,以便在计数器改变时实际移动 sprite,从而利用 sprite 动画(移动)逻辑中的计数器变量。

if-else 结构的 else 部分将(最终)使用我们即将编写的. shootpulse()方法发射射弹,由于 sprite 现在在屏幕上,我们将把屏幕上的布尔标志变量设置为 true 值,这将触发第二个 if(屏幕上)条件逻辑结构。这将使用敌人精灵出现在屏幕上的一半速度(每次计数器迭代移动一个像素)从屏幕上移除敌人精灵。

第二个嵌套条件 if(屏幕上)结构的逻辑与第一个非常相似。您将把目的地设置为 700 像素(将敌人的精灵放回屏幕外,并再次置于视野之外),这一次,您将使用+=1 而不是-=2 进行迭代,这不仅会使敌人向相反的方向移动,因为是正的而不是负的,而且还会使用用于发动攻击的初始速度的一半,因为您移动了一个像素而不是两个像素。

if(屏幕上)条件 if-else 结构的真正区别在于编程逻辑的 else 部分,我们不仅将屏幕上的布尔标志变量设置回 false,而且还将 takeSides 布尔变量设置为 true,这样敌人将使用屏幕的另一侧进行下一次攻击尝试。

最后,由于攻击序列已经完成,我们还将 callAttack 布尔标志变量设置为 false。如您所知,这将在。update()方法,这将给你的玩家几秒钟的时间从被攻击中恢复过来。整个 if(!idspnonenote)的 Java 结构。takeSides)条件结构及其嵌套的 if(屏幕上)条件结构在图 17-37 中突出显示,应该如下所示:

private void initiateAttack() {

if(``!takeSides

spriteFrame.setScaleX(1);

this.setIsFlipH(false);

if(``!onScreen

destination =``500

if(spriteMoveR``>=

spriteMoveR``-=2

spriteFrame.setTranslateX(``spriteMoveR

}``else

// ShootProjectile();

onScreen``=``true

}

if(``onScreen

destination =``700

if(spriteMoveR``<=

spriteMoveR``+=1

spriteFrame.setTranslateX(``spriteMoveR

}``else

onScreen``=``false

takeSides``=``true

callAttack``=``false

}

}

A978-1-4842-0415-3_17_Fig37_HTML.jpg

图 17-37。

Add an if(onScreen) level of processing inside the if(!takeSides) logic to animate sprite from the right side

在第二个 if(takeSides)结构中,在 if(!屏幕上)结构,将目标位置初始化为 100 像素,并嵌套另一个计数器 if(spriteMoveL <= destination)结构。这一次,你将使用spriteMoveL+=2;spriteFrame.setTranslateX(spriteMoveL);在相反的方向移动精灵,每脉冲移动精灵两个像素。在 else 部分,将屏幕上的布尔标志变量设置为 true 值。

第二个嵌套条件 if(屏幕上)结构的逻辑也与第一个类似。您将目的地设置为-70 像素,这一次,您将使用-=1 而不是+=1 进行迭代。在编程逻辑的 else 部分,我们将再次将屏幕上的布尔标志变量设置回 false,并将 takeSides 布尔变量设置回 false,这样敌人将再次交替使用屏幕的另一边进行下一次攻击。

最后,由于攻击序列已经完成,我们将再次将 callAttack 布尔标志变量设置为 false。如您所知,这将在。update()方法,这将给你的玩家几秒钟的时间从被攻击中恢复过来。图 17-38 中突出显示了整个 if(takeSides)条件结构的 Java 结构及其嵌套的 if(onScreen)条件结构,应该如下所示:

if(``takeSides

spriteFrame.setScaleX(-1);

this.setIsFlipH(true);

if(``!onScreen

destination =``100

if(spriteMoveL``<=

spriteMoveL``+=2

spriteFrame.setTranslateX(``spriteMoveL

}``else

// ShootProjectile();

onScreen``=``true

}

if(``onScreen

destination =``-70

if(spriteMoveL``>=

spriteMoveL``-=1

spriteFrame.setTranslateX(``spriteMoveL

}``else

onScreen``=``false

takeSides``=``false

callAttack``=``false

}

}

A978-1-4842-0415-3_17_Fig38_HTML.jpg

图 17-38。

Add an if(onScreen) level of processing inside the if(takeSides) logic to animate a sprite from the left side

武器化的敌人:射击抛射物体

现在我们已经有了敌人在屏幕两边的动画,我们需要增加的下一个复杂层次是投射物体的射击。我们将首先实现 iBullet 对象(负分数生成),然后实现 iCheese 对象(正分数生成),这两个对象都需要我们的 Enemy.java 类能够看到 InvinciBagel 类。因此,我们需要做的第一件事是使用 Java this 关键字修改 Enemy()构造函数方法以接受 InvinciBagel 对象,就像我们对 Bagel()构造函数方法所做的那样。进入 InvinciBagel.java 类,将 this 关键字添加到敌方()构造函数参数列表的前端(开头),如图 17-39 中突出显示的,使用以下修改后的敌方()构造函数方法调用:

iBeagle = new``Enemy``(``this

A978-1-4842-0415-3_17_Fig39_HTML.jpg

图 17-39。

Add a Java this keyword inside the Enemy() constructor method call to pass over the InvinciBagel object

要做到这一点,您还需要更新您的 Enemy.java 类来容纳 InvinciBagel 对象。在 Enemy.java 类的顶部添加一个InvinciBagel invinciBagel;对象声明,并通过在参数列表定义的头端(开头)添加一个 InvinciBagel iBagel 参数来编辑您的 Enemy()构造函数方法以支持该对象。在 Enemy()构造函数方法内部,将 invinciBagel InvinciBagel 对象设置为等于传递到其参数列表中的 Enemy()构造函数方法的 iBagel InvinciBagel 对象引用。修改后的 Enemy()构造函数方法体的 Java 代码如图 17-40 所示,应该如下所示:

InvinciBagel invinciBagel;

public Enemy(``InvinciBagel iBagel

String SVGdata, double xLocation, double yLocation, Image... spriteCels) {

super(SVGdata, xLocation, yLocation, spriteCels);

invinciBagel = iBagel;

spriteFrame.setTranslateX(xLocation);

spriteFrame.setTranslateY(yLocation);

isAlive = true;

isBonus = true;

hasValu = true;

}

A978-1-4842-0415-3_17_Fig40_HTML.jpg

图 17-40。

Modify the Enemy() constructor method in the Enemy class to add an InvinciBagel object named iBagel

现在敌人的职业(对象)可以看到不可战胜的职业(对象),我们准备添加投射物。

创建射弹基础设施:添加射弹变量

为了给敌人职业增加投射支持,我们需要在职业的顶端声明四个新的变量。这些将包括 randomLocation,一个新的变量,我们将用于敌人角色和他发射的炮弹;randomOffset,一个新的变量,它将保持垂直(Y)偏移,允许我们微调射弹的位置,以便它从火箭筒中出来;bulletRange 和 bulletOffset 允许我们进行 X 定位。我们将把 randomLocation 变量设置为等于 random num . nextint(attack boundary)逻辑,它过去位于。setTranslateY()方法调用,并给这个变量加 5,创建 randomOffset 变量的数据值。这些方法的新 Java 结构是无错误的,如图 17-41 所示,应该看起来像下面的 Java 代码:

int spriteMoveR, spriteMoveL, destination;

int``randomLocation``,``randomOffset

public void update() {

if(callAttack) {

if(attackCounter >= attackFrequency) {

attackCounter=0;

spriteMoveR = 700;

spriteMoveL = -70;

randomLocation``=``randomNum.nextInt(attackBoundary)

spriteFrame.setTranslateY(``randomLocation

randomOffset``=``randomLocation + 5

callAttack = true;

} else { attackCounter+=1; }

} else {     initiateAttack(); }

}

A978-1-4842-0415-3_17_Fig41_HTML.jpg

图 17-41。

Add randomLocation, randomOffset, bulletRange, and bulletOffset variables to control bullet placement

还要注意,在图 17-41 的底部,我们还添加了一个if(shootBullet){shootProjectile();}条件 if 结构到。update()方法,以及在 Enemy.java 类的顶部添加一个boolean shootBullet = false;声明。在我们编写。方法,让我们将 shootBullet 标志设置添加到。initiateAttack()方法,以及在 if(!屏幕上)。

调用. shootpropellet()方法:将 shootBullet 设置为 True

在每个 if(takeSides)条件 if-else 结构中,在语句的 else 部分添加一个 bulletOffset 变量设置(480 或 120 ),并将 shootBullet 设置为 true。如图 17-42 所示,Java 代码将如下所示:

if(takeSides) {

spriteFrame.setScaleX(1);

this.setIsFlipH(false);

if(!onScreen) {

destination = 500;

if(spriteMoveR >= destination) {

spriteMoveR-=2;

spriteFrame.setTranslateX(spriteMoveR);

} else {

bulletOffset``=``480

shootBullet``=``true

onScreen = true;    }

A978-1-4842-0415-3_17_Fig42_HTML.jpg

图 17-42。

Add bulletOffset values and shootBullet=true statements into the sprite-reached-destination else body

现在您已经在。update()方法,该方法将调用。shootProjectile()方法从 iBeagle 敌方角色 bazooka 中发射抛射体对象,我们可以开始创建代码,以类似于我们创建敌方动画功能的方式来制作 iBullet 对象(以及后来的 iCheese 对象)的动画。

射弹:编码。shootProjectile()方法

如果您还没有创建一个空的private void shootProjectile(){}方法结构来消除代码中那些红色的波浪状错误,现在您可以这样做了。在这个方法中,我们将再次使用 if(!takeSides)和 if(takeSides)条件结构,以分离舞台每一侧的不同逻辑。这类似于我们在动画中将敌人角色放到屏幕上。initiateAttack()方法。第一个 Java 语句将定位 Y 位置,这次使用。对 iBullet.spriteFrame ImageView 对象的 setTranslateY()方法调用。randomOffset 变量调整相对于火箭筒的子弹位置。接下来的两个。setScaleX 和。setScaleY()方法调用将项目符号图像比例减半(0.5),并使用-0.5 值翻转项目符号。有趣的是,任何负值,不仅仅是-1,都会围绕一个轴镜像。下一行代码将 bulletRange 变量设置为-50,然后 if(bullet offset>= bullet range)条件语句使用每脉冲四个像素的高速设置将项目符号设置为动画。它的编码方式与我们对敌人精灵的编码方式相同,即使用 bulletOffset 变量,该变量用于。在条件语句的 if 部分内调用 iBullet.spriteFrame ImageView 对象的 setTranslateX()方法。if-else 语句的 else 部分将 shootProjectile 变量设置为 false,所以只发射一次!

if(takeSides)条件 if 结构的 Java 代码是类似的,只是它使用+=4、bullet range 624 和 if(bulletOffset <= bulletRange) evaluation statement. Your Java code for these if(!takeSides) and if(takeSides) structures inside of the shootProjectile() method body can be seen in Figure 17-43 ,应该如下所示:

private void``shootProjectile()

if(``!takeSides

invinciBagel.iBullet.spriteFrame.setTranslateY(``randomOffset

invinciBagel.iBullet.spriteFrame.setScaleX(``-0.5

invinciBagel.iBullet.spriteFrame.setScaleY(``0.5

bulletRange``=``-50

if(``bulletOffset >= bulletRange

bulletOffset-=4 ;

invinciBagel.iBullet.spriteFrame.setTranslateX(``bulletOffset

}``else``{``shootBullet``=``false

}

if(``takeSides

invinciBagel.iBullet.spriteFrame.setTranslateY(``randomOffset

invinciBagel.iBullet.spriteFrame.setScaleX(``0.5

invinciBagel.iBullet.spriteFrame.setScaleY(``0.5

bulletRange``=``624

if(``bulletOffset <= bulletRange

bulletOffset+=4 ;

invinciBagel.iBullet.spriteFrame.setTranslateX(``bulletOffset

}``else``{``shootBullet``=``false

}

}

A978-1-4842-0415-3_17_Fig43_HTML.jpg

图 17-43。

Add private void shootProjectile() method and code the if(!takeSides) and if(takeSides) if-else statements

如果你现在使用运行➤项目的工作流程,你会看到你的 iBeagle 在屏幕上显示动画,射出一颗子弹,然后迅速退出屏幕。我们需要添加到代码中的下一层真实感是让 iBeagle 在瞄准和发射子弹时暂停一秒钟,因为目前看起来他好像碰到了一个无形的障碍,并从屏幕上反弹回来。我们将通过在。update()方法。让我们接着做,这样我们就有了一个完全专业的敌人自动攻击序列!

让敌人在开火前暂停:pauseCounter 变量

为了让敌人在屏幕上暂停,让他的射击动作看起来更真实,也为了让 InvinciBagel 角色有一些时间去尝试擒抱他(稍后我们会给它分配 10 个得分点),让我们在 Enemy.java 类的顶部添加一个整数 pauseCounter 变量和一个布尔 launchIt 变量,如图 17-44 中突出显示的。在 if(shootBullet)条件语句中,在 shootBullet()方法调用之后,放置一个 if(pauseCounter > = 60)条件结构,并在其中将 launchIt 设置为 true,并将 pauseCounter 变量重置为零。在条件的 else 部分,使用 pauseCounter++将 pauseCounter 递增 1,然后我们所要做的就是将 launchIt 布尔标志实现到我们的 initiateAttack()方法中,我们将有一个敌人角色,他在瞄准和射击 InvinciBagel 角色时会从容不迫。你的这个 if(shootBullet) if-else 结构的 Java 代码可以在图 17-44 中看到,应该看起来像下面的代码:

if(``shootBullet

shootProjectile();

if(``pauseCounter >= 60

launchIt = true ;

pauseCounter = 0;

}``else``{``pauseCounter++

}

A978-1-4842-0415-3_17_Fig44_HTML.jpg

图 17-44。

Add a pauseCounter variable to create a timer, creating a one-second delay, so Enemy doesn't bounce

射出子弹:使用 launchIt 变量扣动扳机

在每个 if(takeSides)和 if(!takeSides)条件 if 结构,将 if(屏幕上)结构改为 if(屏幕上&&启动)结构,然后将一个launchit = false;语句添加到这个修改后的结构的 else 部分。新 if()结构的 Java 代码如图 17-45 所示,如下所示:

if(``onScreen``&&``launchIt

destination = 700;

if(spriteMoveR <= destination) {

spriteMoveR+=1;

spriteFrame.setTranslateX(spriteMoveR);

} else {

onScreen   = false;

takeSides  = true;    // This will be``false``if inside of the``if(takeSides)

callAttack = false;

launchIt``=``false

}

A978-1-4842-0415-3_17_Fig45_HTML.jpg

图 17-45。

Add a launchIt flag to the if(onScreen) condition to make that code structure wait for the pauseCounter

对于这个 if(屏幕和启动)结构的 if(takeSides)版本,确保将 destination 更改为-70,使计数器 if(sprite level > = destination)和计数器 update spriteMoveL-=1;以及 takeSides 在这个条件结构的 else 部分中等于 false。接下来,让我们在 Bagel 类中更新我们的评分引擎。

更新。scoringEngine()方法:使用。等于( )

让我们对最后三个对象使用不同的方法——else-if 结构,而不是使用 if(object instanceof Actor)进行更一般的对象类型比较,我们将使用更精确的。equals()方法,允许我们指定对象本身,比如 if(object . equals(invincibagel . I bullet)。你可以在图 17-46 中看到完整的 if-else 结构,最后三个敌方对象的 Java 代码如下所示:

} else if(object.``equals``(invinciBagel.``iBullet

invinciBagel.gameScore``-=5

invinciBagel.playiSound5;

} else if(object.``equals``(invinciBagel.``iCheese

invinciBagel.gameScore``+=5

invinciBagel.playiSound0;

} else if(object.``equals``(invinciBagel.``iBeagle

invinciBagel.gameScore``+=10

invinciBagel.playiSound0;   }

A978-1-4842-0415-3_17_Fig46_HTML.jpg

图 17-46。

Adding the iBullet, iCheese and iBeagle object.equals() if-else structures to the .scoringEngine() method

在这一点上,如果您使用运行➤项目工作流程,您应该有一个 iBeagle 拍摄子弹或奶油奶酪球。当你抓到有无敌手角色的 iBeagle,你应该得到十分,或者,如果你抓到一个奶油芝士球,你应该得到五分。如果你被子弹击中,你应该会失去五分。

当你测试 InvinciBagel 游戏应用时,你会注意到,一旦你被子弹、奶酪球击中,或者当你抓住 iBeagle 时,它们就不会回来了!这是因为碰撞检测编程逻辑会在无敌手收集到某个物体(宝藏)或与之发生碰撞(道具或投射物)时将该物体从游戏中移除。

因此,我们开发的下一步将是添加编程逻辑,一旦 iBullet、iCheese 或 iBeagle 对象被您在第十六章中放置的冲突检测编程逻辑删除,就将它们添加回 castDirector 对象。要做到这一点,我们必须为 CastingDirector.java 类编写一个增强代码,编写新的。loadEnemy(),。loadBullet()和。loadCheese()方法,并将实现这三个新方法的适当编程逻辑添加到。initiateAttack()方法。

将项目符号添加到剪辑:更新。addCurrentCast()

因为我们要将单个(一次一个,不是未婚)演员对象添加回 CURRENT_CAST 列表,所以我们需要修改。addCurrentCast()方法,因为它当前只接受一个参与者。。。我们需要它容纳一个添加的 Actor 对象,就像 addToRemovedActors()方法当前所做的那样。你可以看到。图 17-47 底部的 addToRemovedActors()方法。的。addCurrentCast()方法被设计为在 InvinciBagel.java 类中静态使用,在 start()方法中,一次添加所有的角色转换成员(记住静态与动态)。现在我将向您展示如何重新设计它,以允许在游戏过程中进行“一次性”添加,这是该方法的动态使用,因为列表<演员>将在游戏过程中实时动态修改。升级。addCurrentCast()方法,只需将。addAll()方法调用在具有 if(actors.length > 1) if-else 结构的方法内,原始代码在 if()部分内,一个CURRENT_CAST.add(actors[0]);语句在 else 部分内,以适应单个 Actor 方法调用。新方法结构的 Java 代码如图 17-47 所示,应该如下所示:

public void addCurrentCast(Actor... actors) {

if(``actors.length > 1

CURRENT_CAST.addAll(Arrays.asList(actors));

}``else

CURRENT_CAST.add(``actors[0]

}

}

需要注意的是,我们也可以通过重载这个来实现这个目标。addCurrentCast()方法。如果您想以这种方式实现这一点,Java 代码将类似于以下方法体:

public void addCurrentCast(``Actor... actors

CURRENT_CAST.``addAll``(Arrays.asList(``actors

}

public void addCurrentCast(``Actor actor

CURRENT_CAST.``add``(``actor

}

一旦您的游戏设计变得更加高级,并且舞台上有了装饰性的演员对象,您就可以在 if()结构中实现 COLLIDE_CHECKLIST,该结构需要迭代(仅)场景中需要进行碰撞处理的演员对象。

在我们游戏设计的这一点上,我们正在处理所有角色对象的碰撞,因此,我们还不需要实现 COLLIDE_CHECKLIST List 数组。我把它包含在职业设计中是为了更彻底,因为我通过展望未来来设计游戏的基础职业,为了让我创建一个先进的(完整的)游戏引擎,我需要什么。也就是说,我们可能没有足够的页面来让初学者的标题变得更高级,但是如果你需要在游戏中使用它,功能就在那里,在这一章之后,你会有很多使用 if()结构的经验!

A978-1-4842-0415-3_17_Fig47_HTML.jpg

图 17-47。

升级 CastingDirector 类中的 addCurrentCast()方法,以接受参数列表中的单个对象

既然我们可以在单个基础上添加 cast 成员,那么是时候编写允许我们检查 CURRENT_CAST List 数组的方法了,以确保有 iBullet、iCheese 和 iBeagle Actor 对象可供我们在自动攻击引擎的下一次迭代中使用。这些方法要做的是在 CURRENT_CAST 列表中查找这些 Actor 对象,如果它们不存在,就将它们中的一个添加到列表中,这样它就为我们的条件 if()语句和随机数生成器一起创建的任何类型的攻击做好了准备!

我们将编写三个方法,一个用于致命抛射,称为。load bullet();一种是健康的抛射物,叫做。loadCheese();另一个用于敌方对象,称为 loadEnemy()。这将给我们最终的灵活性,在我们游戏开发的后期,在它自己的“补充功能”中调用每种类型的功能性演员对象类型

每个方法都将使用 for()循环和. size()方法调用来遍历整个 CURRENT_CAST 列表。这样,从第一个元素(零)到最后一个元素(列表的大小),整个列表都被处理。

如果在转换中没有找到该类型的 Actor 对象,也就是说,如果完成了 for()循环,并且在使用 object.equals()在整个 CURRENT_CAST 列表中查找匹配之后没有找到该类型的对象,那么将执行 for 循环之后的两个语句。

第一条语句将向 CURRENT_CAST 列表添加一个该类型的 Actor 对象,然后第二条语句将向 JavaFX SceneGraph 添加一个该类型的 Actor 对象。此时,该方法完成,然后将控制权返回给调用实体。

如果在 CURRENT_CAST List 数组中找到该类型的 Actor 对象,将调用一个return ;语句来立即退出该方法,并将控制返回给调用实体。这意味着不会执行 for()循环末尾的语句,这些语句将该类型的新 Actor 对象添加到造型中,并将新 Actor 对象添加到 JavaFX 场景图形根对象中。

这个return;语句是使这个方法工作的核心组件,因为如果任何该类型的 Actor 对象存在,一个重复的 Actor 对象将不会被添加到 List < Actor >数组中,这将导致错误发生。这是一个很好的方法来确保我们只为每种类型的投射物和敌人对象使用一个节点,这允许我们优化内存和处理器开销。这三种方法的 Java 8 编程结构非常相似,如图 17-48 所示。三个私有 void 方法体应该如下所示:

private void``loadBullet()

for``(int i=0; i<invinciBagel.castDirector.getCurrentCast().``size()

Actor``object``= invinciBagel.castDirector.getCurrentCast().``get(i)

if(``object.equals``(invinciBagel.``iBullet

return;

}

}

invinciBagel.castDirector.``addCurrentCast

invinciBagel.root.``getChildren().add

}

private void``loadCheese()

for``(int i=0; i<invinciBagel.castDirector.getCurrentCast().``size()

Actor``object``= invinciBagel.castDirector.getCurrentCast().``get(i)

if(``object.equals``(invinciBagel.``iCheese

return;

}

}

invinciBagel.castDirector.``addCurrentCast

invinciBagel.root.``getChildren().add

}

private void``loadEnemy()

for``(int i=0; i<invinciBagel.castDirector.getCurrentCast().``size()

Actor``object``= invinciBagel.castDirector.getCurrentCast().``get(i)

if(``object.equals``(invinciBagel.``iBeagle

return;

}

}

invinciBagel.castDirector.``addCurrentCast

invinciBagel.root.``getChildren().add

}

A978-1-4842-0415-3_17_Fig48_HTML.jpg

图 17-48。

Create loadBullet(), loadCheese() and loadEnemy() methods, to add another Enemy or Projectile to game

现在所有这些方法都准备好了,我们可以在。initiateAttack()自动攻击方法体,并让它们工作,检查在我们发动下一次攻击之前是否需要添加一个投射物或敌人物体。调用这些方法调用的适当位置应该是在敌人对象出现在屏幕上,并且抛射体对象已经启动之后,这意味着这些方法调用需要出现在 if(屏幕和启动)结构的 else{}代码体的末尾。实现这三个方法调用的 Java 代码如图 17-49 所示,应该如下所示:

if(``onScreen``&&``launchIt

destination = 700;

if(spriteMoveR <= destination) {

spriteMoveR+=1;

spriteFrame.setTranslateX(spriteMoveR);

}``else

onScreen   = false;

takeSides  = true;    // This will be false if inside of the if(takeSides) structure

callAttack = false;

launchIt   = false;

loadBullet();

loadCheese();

loadEnemy();

}

}

A978-1-4842-0415-3_17_Fig49_HTML.jpg

图 17-49。

Add calls to loadBullet(), loadCheese() and loadEnemy() to end of “move Enemy offscreen” else structure

接下来,让我们为我们的游戏添加一个独特的转折,有一个奶油干酪球形式的抛射物,如果无敌面包圈能够将自己置于被它击中的位置,它将产生积极的分数。

射击奶油芝士球:不同的子弹类型

为了适应不同类型的射弹,我们需要声明一个新的布尔变量,我们将其命名为 bulletType,这样我们就可以拥有致命的射弹(iBullet)和健康的射弹(iCheese)。在。update()方法,在将 callAttack 设置为 true 以启动攻击序列之前,您将把这个 bulletType 变量设置为调用。由 randomNum 随机对象组成的 nextBoolean()方法。图 17-50 中突出显示的 Java 代码应该类似于以下 Java 语句:

boolean``bulletType

bulletType = randomNum.nextBoolean();

A978-1-4842-0415-3_17_Fig50_HTML.jpg

图 17-50。

Add a boolean bulletType variable at top of the Enemy class, then set it equal to .nextBoolean() method

为了实现这个布尔 bulletType 标志,我们需要将您的 if(!idspnonenote)转换为。takeSides) evaluator 成为 if(!bulletType &&!takeSides)赋值器,以便同时考虑屏幕的侧面和项目符号类型的布尔标志。如图 17-51 所示,新的条件 if()结构应该类似于下面的 Java 代码:

if(``!bulletType``&&``!takeSides

invinciBagel.``iBullet

invinciBagel.``iBullet

invinciBagel.``iBullet

bulletRange = -50;

if(bulletOffset >= bulletRange) {

bulletOffset-=4;

invinciBagel.``iBullet

} else { shootBullet = false; }

}

if(``!bulletType``&&``takeSides

invinciBagel.``iBullet

invinciBagel.``iBullet

invinciBagel.``iBullet

bulletRange = 624;

if(bulletOffset <= bulletRange) {

bulletOffset+=4;

invinciBagel.``iBullet

} else { shootBullet = false; }

}

接下来,我们需要对 bulletType 等于 true 值进行同样的转换。这将意味着使用奶油干酪作为抛射体对象类型。一旦我们将这些条件 if()结构放置到位,我们将为 takeSides 和 bulletType 的每个逻辑组合准备一个 if()结构。最后两个条件 if()结构应该如下所示:

if(``bulletType``&&``!takeSides

invinciBagel.``iCheese

invinciBagel.``iCheese

invinciBagel.``iCheese

bulletRange = -50;

if(bulletOffset >= bulletRange) {

bulletOffset-=4;

invinciBagel.``iCheese

} else { shootBullet = false; }

}

if(``bulletType``&&``takeSides

invinciBagel.``iCheese

invinciBagel.``iCheese

invinciBagel.``iCheese

bulletRange = 624;

if(bulletOffset <= bulletRange) {

bulletOffset+=4;

invinciBagel.``iCheese

} else { shootBullet = false; }

}

A978-1-4842-0415-3_17_Fig51_HTML.jpg

图 17-51。

Add bulletType to conditional if statement evaluation in shootProjectile() method, to shoot cream cheese

调整游戏:微调用户体验

现在,让我们花一点时间对代码做一些调整,自动攻击逻辑正在工作,以确保我们的游戏是专业的。在游戏启动时,将 iBeagle、iBullet 和 iCheese 对象置于屏幕之外,方法是使用与 sprite 图像资源的宽度相同(或更大)的负 X 位置值来更改这些对象的每个构造函数方法中的 X 和 Y 位置参数,如图 17-52 所示。

A978-1-4842-0415-3_17_Fig52_HTML.jpg

图 17-52。

Modify X and Y parameters for all Enemy and Projectile constructor methods, to place them off-screen

你可能也注意到了投射物在敌人的火箭筒上面,所以让我们改变 iBeagle 对象的 z-index,这样投射物就从枪后面来了。为此,请将您的。在 iBullet 之后,iBagel 之前,在。addGameActorNodes()方法,如图 17-53 所示。

A978-1-4842-0415-3_17_Fig53_HTML.jpg

图 17-53。

Change z-index of iCheese and iBullet in the addGameActorNodes() method so they’re before iBeagle

现在让我们给这个自动攻击游戏增加更多的真实感,随机选择敌人精灵从哪边出现,这样玩家就不知道会发生什么。目前,我们正在交替两边,所以我们需要在代码中的战略位置添加一个随机的 takeSides 布尔标志。接下来我们来写这段代码。

随机化自动攻击:使用。带 takeSides 的 nextBoolean

尽管我们在完成攻击后退出 initiateAttack()方法之前设置了 takeSides 布尔标志,但并没有说我们不能在调用攻击之前通过在 if(attack counter > = attack frequency)结构中将 callAttack 设置为 true 来再次设置它,您可以在图 17-54 中看到我已经使用以下 Java 语句完成了这一点:

takeSides = randomNum.nextBoolean;

A978-1-4842-0415-3_17_Fig54_HTML.jpg

图 17-54。

Set the boolean takeSides variable to use a .nextBoolean() method call off a randomNum Random object

现在您已经在敌人内部以这种方式设置了 takeSides 布尔标志变量。update()方法,您可以通过移除takeSides = true;takeSides = false;语句来进一步优化您的代码,这两个语句当前位于您的 if(屏幕上的&launch it)else 语句中(参考图 17-45 )。

因为这些交替的布尔值现在将被。在 if(!idspnonenote)中调用 nextBoolean()方法。callAttack)结构,它们可以被安全地移除,因为 if(!callAttack)条件结构随机设置这两个布尔值。这样做的结果是,现在 iBeagle 敌人演员对象将随机出现在屏幕的任何一侧,游戏玩家无法预测攻击来自哪里。

使用一个 Run ➤项目工作流程,玩游戏并测试游戏,如图 17-55 所示。你会注意到的第一件事是,我们所有的 z 索引字符层排序是正确的。你还会看到你的敌人和投射物在游戏启动时是不可见的,这是向专业的最终用户体验又迈进了一步。

你会注意到让无敌舰队就位要困难得多,因为你不知道敌人的攻击会从哪里、什么时候、从什么方向来!嗯,这并不完全正确,因为我们需要随机化 attackFrequency 变量,以使“当他出现”部分不会在均匀的时间间隔内被触发。既然我们的目标是让这个游戏越来越具有挑战性和专业性,那就让我们开始吧!

A978-1-4842-0415-3_17_Fig55_HTML.jpg

图 17-55。

Use a Run ➤ Project work process to test the game and the enemy attack, bullet types, and scoring engine

在这一章的剩余部分,我们将增加一些功能,使游戏更具挑战性和真实性。我们将添加一些重要的游戏设计元素,如随机化、人工智能和物理模拟,所有这些都将使您的 Java 8 游戏更加专业和受欢迎。在我们完成核心 Java 8 游戏开发周期的第一轮(初学者)之前,您需要对这些概念有所了解。

加入出其不意的元素:随机攻击频率

现在,我们已经使屏幕上的进入点以及用于攻击的屏幕侧边完全随机,让我们进入第四维(时间)并使攻击发生的时间也随机。这是通过随机化 attackFrequency 变量来实现的,在此之前我们已经将它设置为 4.167 秒(250/60FPS)。我们将在。initiateAttack()方法,我们在其中设置您的布尔标志设置并调用。load()方法,我们创建这些方法是为了确保自动攻击引擎总是有敌人和投射物可以使用。我们将把一个随机值插入到 if(屏幕和启动)条件结构的 else 部分末尾的 attackFrequency 变量中,这样当。update()方法开始将此变量用于其攻击延迟计数器编程逻辑。自从。nextInt(int bounds)方法调用结构给了我们一个介于零和上限之间的随机整数,为了得到一个介于一秒(60)和九秒(60+480)之间的攻击延迟范围,我们需要将 randomNum 生成的值加上 60。语句的 nextInt (480)部分,然后将 attackFrequency 变量设置为该值。这个攻击频率随机化语句的 Java 代码如图 17-56 所示,应该如下所示:

attackFrequency =``60 +``randomNum.nextInt(``480

A978-1-4842-0415-3_17_Fig56_HTML.jpg

图 17-56。

Add an attackFrequency statement incorporating a .nextInt(bounds) method in if (onScreen && launchIt)

正如您在使用“运行➤项目”工作流程时所看到的,您不再能够计算任何给定的敌人攻击何时开始!让我们让自动攻击引擎变得更加智能,通过告诉它 InvinciBagel 角色在屏幕上的位置(Y 坐标),这样自动攻击引擎就可以更有效地针对他!

瞄准不可战胜的怪物:增加敌人的人工智能

为了让游戏玩起来更有挑战性,我们应该做的下一件事是告诉自动攻击引擎 iBagel 在屏幕上的位置,这是一项人工智能收集任务,因为我们控制了所有的 Java 代码,所以变得更容易!为此,我们将创建一个保存 InvinciBagel Y 屏幕高度位置值的变量,而不是使用 randomLocation 随机 Y 屏幕高度位置值,从而为敌人提供有关 iBagel 对象在屏幕上的位置的内部信息。这是使用 iBagel Hero 对象的 iY 属性完成的,我们使用。getiY() getter 方法,然后在。setTranslateY()方法调用。我们使用 integer(而不是 double)作为 iBagelLocation 数据类型,所以我们需要“转换”从。getiY()方法,以便它与 iBagelLocation 变量兼容。如图 17-57 所示的 Java 代码应该如下所示:

int``iBagelLocation

iBagelLocation = (int) invinciBagel.iBagel. getiY()

spriteFrame.setTranslateY(``iBagelLocation

randomOffset =``iBagelLocation

A978-1-4842-0415-3_17_Fig57_HTML.jpg

图 17-57。

Declare iBagelLocation integer variable, cast a double iY variable to it, and use it to create randomOffset

如果您使用“运行➤项目”工作流程来测试您的代码,您将会看到投射物现在瞄准了 invincibagel 角色,无论您将他放在屏幕的什么位置!这对于利用致命的 iBullet 抛射体 Actor 对象的攻击来说是非常好的,但是当自动攻击引擎使用 iCheese 抛射体 Actor 对象时,就太容易得分了。因此,如果自动攻击引擎要发射奶油干酪球,我们将需要添加另一层使用 randomLocation 变量的代码,如果自动攻击引擎要发射致命(真实)子弹,则需要添加 iBagelLocation 变量。我们将把这个逻辑结构放在。update()方法,其中我们创建了 randomLocation 和 iBagelLocation 变量值(使用。nextBoolean()或。getiY()方法调用),就在确定 bulletType 之后,使用 randomNum 随机对象的. nextBoolean()方法调用。

我们将要创建的 if(bulletType)条件 if 结构将使用一个. setTranslateY()方法调用,传递一个 randomLocation 参数,如果 bulletType 等于一个真值(iCheese),并将使用。如果 bulletType 等于 false 值(iBullet ),则使用 if-else 结构的 else{}部分传递 iBagelLocation 参数的 setTranslateY()方法调用。在 if-else 结构每一部分的第二行代码中,我们将记住设置 randomOffset 变量,在该结构的 if 部分中向 randomLocation 变量添加五个像素,或者在 else 部分中向 iBagelLocation 变量添加五个像素,使用下面的代码,如图 17-58 所示:

if(attackCounter >= attackFrequency) {

attackCounter=0;

spriteMoveR = 700;

spriteMoveL = -70;

randomLocation = randomNum.nextInt(attackBoundary);

iBagelLocation = (int) invinciBagel.iBagel.getiY();

bulletType = randomNum.nextBoolean();

if(``bulletType

spriteFrame.setTranslateY(``randomLocation

randomOffset =``randomLocation

}``else

spriteFrame.setTranslateY(``iBagelLocation

randomOffset =``iBagelLocation

}

callAttack = true;

} else { attackCounter+=1; }

A978-1-4842-0415-3_17_Fig58_HTML.jpg

图 17-58。

Add an if-else structure after the bulletType, randomLocation, and iBagelLocation to locate by bulletType

给子弹增加重力:游戏物理学导论

由于物理计算倾向于使用分数而不是整数,我们需要将 randomOffset 从复合整数声明中取出,并使其成为一个double randomOffset;声明,如图 17-59 所示。此外,您需要为 bulletGravity 和 cheeseGravity 声明双变量,并将它们的值设置为 0.2 和 0.1。

A978-1-4842-0415-3_17_Fig59_HTML.jpg

图 17-59。

Declaring bulletGravity and cheeseGravity double variables, and converting randomOffset to a double

我们想要做的是在每一帧的随机偏移(Y 位置)上添加一个 bulletGravity(或 cheeseGravity)因子,这样我们就可以在镜头上获得轻微的“拖尾”效果。这将模拟重力将抛射体拉向地球。我们将把它放在 if(bulletOffset >= bulletRange)计数器循环中,这样重力因子只在投射物在屏幕上飞行可见时应用。在图 17-60 中可以看到将 bulletGravity 和 cheeseGravity 调整到投射物体轨迹的 Java 代码,它应该类似于下面的代码:

private void shootProjectile() {

if(!bulletType && !takeSides) {

invinciBagel.iBullet.spriteFrame.setTranslateY(randomOffset);

invinciBagel.iBullet.spriteFrame.setScaleX(-0.5);

invinciBagel.iBullet.spriteFrame.setScaleY(0.5);

bulletRange = -50;

if(bulletOffset >= bulletRange) {

bulletOffset``-=6

invinciBagel.iBullet.spriteFrame.setTranslateX(bulletOffset);

randomOffset = randomOffset + bulletGravity;

} else { shootBullet = false; }

}

if(!bulletType && takeSides) {

invinciBagel.iBullet.spriteFrame.setTranslateY(randomOffset);

invinciBagel.iBullet.spriteFrame.setScaleX(0.5);

invinciBagel.iBullet.spriteFrame.setScaleY(0.5);

bulletRange = 624;

if(bulletOffset <= bulletRange) {

bulletOffset``+=6

invinciBagel.iBullet.spriteFrame.setTranslateX(bulletOffset);

randomOffset = randomOffset + bulletGravity;

} else { shootBullet = false; }

}

if(bulletType && !takeSides) {

invinciBagel.iCheese.spriteFrame.setTranslateY(randomOffset);

invinciBagel.iCheese.spriteFrame.setScaleX(-0.5);

invinciBagel.iCheese.spriteFrame.setScaleY(0.5);

bulletRange = -50;

if(bulletOffset >= bulletRange) {

bulletOffset``-=4

invinciBagel.iCheese.spriteFrame.setTranslateX(bulletOffset);

randomOffset = randomOffset + cheeseGravity;

} else { shootBullet = false; }

}

if(bulletType && takeSides) {

invinciBagel.iCheese.spriteFrame.setTranslateY(randomOffset);

invinciBagel.iCheese.spriteFrame.setScaleX(0.5);

invinciBagel.iCheese.spriteFrame.setScaleY(0.5);

bulletRange = 630;

if(bulletOffset <= bulletRange) {

bulletOffset``+=4

invinciBagel.iCheese.spriteFrame.setTranslateX(bulletOffset);

randomOffset = randomOffset + cheeseGravity;

} else { shootBullet = false; }

}

}

A978-1-4842-0415-3_17_Fig60_HTML.jpg

图 17-60。

Adding physics simulation of gravity to the Projectile object's trajectory in the .shootProjectile() method

摘要

在第十七章,也是最后一章,我们使用了我们在本书过程中构建的游戏引擎的所有基础元素,并为 InvinciBagel 游戏创建了基本的游戏玩法,包括一个得分引擎和一个自动攻击引擎,以及随机攻击策略和基本的物理模拟来增强真实性。我们学习了如何使用 JavaFX 8.0 Text 和 Font 类来创建游戏上的记分牌输出,以及如何使用 Java 8 Random 类作为随机数生成器来使我们的自动攻击引擎看起来像有自己的生命一样。我们还通过编写一个 Treasure.java 类为游戏添加了奖金,并创建了一个. scoringine()方法来跟踪和组织我们的得分算法。我们学习了更多关于优化的知识,使用带有break;的 if-else-if 循环,还学习了如何使用return;来提前中断一个方法,我们使用这两种技术在我们的游戏逻辑中获益匪浅。我们在游戏中添加了敌人角色和投射物,并学习了如何在游戏背后实现背景板。我试图超越一本基本的初学者 Java 8 书籍,向您展示创建游戏引擎基础架构所涉及的工作流程,包括设计思维过程,如何利用 Java 8 和 JavaFX 8.0 中的关键类,以及如何使用新的媒体资产和优化技术。

posted @ 2024-08-06 16:33  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报