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

183 阅读1小时+

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

协议:CC BY-NC-SA 4.0

九、控制你的演员形象:实现 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 对象使游戏精灵在屏幕上移动的高级概念,以便我们在进入高级主题如数字音频、碰撞检测和物理模拟之前获得更真实的精灵动画。