Java9-游戏开发高级教程-全-

Java9 游戏开发高级教程(全)

协议:CC BY-NC-SA 4.0

一、Java 的不同面貌:创建一个 Java 9 开发工作站

欢迎来到专业 Java 9 游戏开发书。在第一章中,我将讨论 Java 的各种版本,这些版本目前仍被用于为 Android 等开源平台以及基于 WebKit 的开源浏览器(如 Google Chrome、Mozilla Firefox、Apple Safari 和 Opera)开发软件应用。在了解了从 JDK 1.6(也称为 Java 6)到 JDK 1.9(最近发布为 Java 9)的 Java 版本之后,需要使用哪些版本来开发这些流行平台的各种版本。我们还需要详细了解如何创建一个专业的 Java 9 软件开发工作站,以便在本书的剩余部分使用。这将包括其他软件,如新媒体内容制作软件包,这些软件包可以与您的 Java 软件开发包一起用于创建游戏和物联网(IoT)应用。

工作站的核心将是 Java 8 SDK 或软件开发工具包,也称为 JDK 或 Java 开发工具包,或者是 2017 年推出的新 Java 9 JDK,比 Java 8 更加模块化,但具有相同的类和方法来创建游戏或物联网用户体验。这个事实将允许我们在本书的过程中安全地关注 Java 8 和 Java 9。这是因为,就我们的目的而言,这些基本上是相同的,允许我们关注最新的 Java APIs,而不是您正在使用的 Java 版本。事实上,由于我们将重点讨论 Java 的多媒体 API,通常称为 JavaFX,所以您在本书中学到的内容也可以用 Java 7 编码!Android 最近升级到 Java 7 和 Java 8 兼容(从 Java 6)。

我们还将为您设置 NetBeans 9.0 IDE 或集成开发环境,这将使 Java 8 或 9 游戏的编码变得更加容易。预计在 2017 年第四季度 Java 9 发布后会使用 NetBeans 9,因为 NetBeans 9 IDE 将进行重大升级,以适应 Java 9 的新模块化特性,并允许您混合功能模块,为任何类型的应用开发创建自定义 Java 包集合(API 版本)。

配置好 Java JDK 和 NetBeans IDE 后,我们将为您安装最新的开源新媒体内容创建软件包,包括用于数字成像的 GIMP、用于数字插图的 InkScape、用于数字视频编辑或特殊效果的 DaVinci Resolve、用于数字音频编辑的 Audacity、用于特殊效果和 3D 的 Fusion、用于业务和项目管理的 Open Office 4 Suite、用于 3D 建模、纹理、动画、渲染、粒子系统、流体动力学或特殊效果的 Blender,以及用于虚拟星球的 Terragen 4。

在本章的最后,我甚至会推荐一些其他的专业级软件包,你应该考虑将它们添加到我们将在本章中创建的专业游戏开发工作站中。这样,当我们完成第一章的时候,你将拥有一个对你的企业非常有价值的生产资源。希望仅这第一章就值得你为这整本书所付出的,因为你可以花 500 美元购买一个强大的 64 位工作站,并在几个小时内使它价值五位数!

我们还将讨论新的 Java 9 内容生产工作站的一些硬件要求和注意事项。最后,请注意,本书中的 Java 代码在 Java 8 IDE(或集成开发环境)中也能很好地工作,所以本书可以很容易地被称为专业 Java 8 游戏开发书!

Java 二分法:在各种平台上使用的版本

有许多不同版本的 Java 仍然被广泛用于许多不同流行平台的开发,包括用于 32 位 Android 的 Java 6(Android 的版本 1.x、2.x、3.x 和 4.x 是 32 位的),以及用于早期 64 位 Android 版本(5.0、5.1 和 6.0)的 Java 7,用于最新 Android 版本(7.0、7.1.2、8.0)的 Java 8,以及用于 Windows 10 操作系统、Ubuntu Linux 操作系统(和其他 Linux)的 Java 9

值得注意的是,Java 有三个主要版本;Java ME 或 Micro Edition 针对嵌入式设备进行了优化,Java SE 或 Standard Edition 用于“客户端”以及移动消费电子设备和 iTV 设备,Java EE 或 Enterprise Edition 可被视为“服务器端”范例,因为大型企业计算环境通常是基于服务器的,而不是“对等的”(纯客户端,除了客户端-服务器交互之外,还可能有客户端-客户端交互)。

于 2006 年 12 月发布的 Java 6(十多年前)仍然被广泛用于与 Eclipse IDE 一起为所有 32 位版本的 Android 开发应用,从版本 1.0 到版本 4.4。这是因为这是谷歌最初指定用于开发 32 位 Android 应用的 Java 版本,当时 Android 1.0 于 2008 年 9 月发布。值得注意的是,Google 使用 Open Java 项目创建了 Java 6 的自定义版本,但这不会影响编程 API,因为如果您在 NetBeans IDE 或 IntelliJ IDEA 中使用 Java 6,而不是使用 Eclipse IDE,这些类、方法和接口的功能仍然相同。

当谷歌将 Android 升级到 64 位 Linux 内核时,在 Android 5.x 中,使用了基于 IntelliJ 的 Android Studio IDEA,他们升级到使用 Java 7,Java 7 也有 64 位版本。Java 7 于 2011 年 7 月发布。因此,如果您正在为高级平台开发 Android 5-6 应用,如 Android Wear,这在 Apress 的 my Pro Android Wearables (2015)标题中有所介绍,或者 Android TV 或 Android Auto,在 Apress 的 Android Apps for Absolute 初学者(2017)标题中有所介绍,您将希望利用 Java 7。JavaFX 8 和 JavaFX 9 中的 JavaFX 8 引擎也被移植到 Java 7 中;然而 Java 7 今年退役了。Android 还在用 Java 6,7,8。

在撰写本书时,Java 8 是 Java SE 的最新版本,此外,它还具有强大的 JavaFX 8.0 多媒体引擎,该引擎也与 Java 7 兼容,尽管 JavaFX 8.0 APIs 在 Android APIs 中还没有得到本机支持。然而,开发 JavaFX 8 或 9 应用在 Android 操作系统和 iOS 平台上运行是可能的,这使得这本书对我们的读者更有价值!Java 8 支持所有流行的浏览器,Android 7、7.1.2 和 8.0,以及所有四种流行的操作系统,包括 Windows 7、8.1 和 10,所有 Linux 发行版、Macintosh OS/X 和 Oracle 的 Open Solaris。Java 8 于 2014 年 3 月发布,增加了一个名为 Lambda Expressions 的强大新功能,我们将在本书中介绍,因为这是一种编写更紧凑代码的方式,通常也更高效地处理多处理器(和多线程)。

Java 9 是 Java 的下一个主要版本。Java 9 于 2017 年 9 月 22 日发布。Java 语言开发人员正在改造的 Java 9 中的主要新特性是使 Java 9 语言 API 模块化。这将允许 Java 9 开发者在“模块”(代码库)中“混合和匹配”特性,并创建他们自己的定制的、优化的 Java 版本。对于定制开发环境或定制应用,这些定制 Java 版本将完全按照开发人员需要的方式工作。截至本书发布时,NetBeans 9 仍在开发中。

作为游戏开发人员,或者物联网开发人员,这意味着您可以创建几个游戏开发定制 Java 版本级别,或者几个定制物联网开发 Java 版本级别。从 Java 7 版本开始,如果需要的话,添加 Lambda 表达式(一种编码快捷方式,我们将在后面介绍)来创建 Java 8 版本,或者打包成定制模块(Java 9 中的一个新特性)来为所有流行的 OS 平台创建 Java 9 版本。如果您使用 JavaFX 多媒体/游戏引擎,Java 8 和 Java 9 APIs 中都有最新的 JavaFX 特性。

我想向读者指出,他们可以优化他们的游戏程序逻辑,以跨越几个版本的 Java,优化 Java 7 (Android 5 或 6)到 Java 8 (Android 7,8 和现代操作系统)到 2017 年 9 月 22 日在图书发行前问世的 Java 9 版本。这也可以在不做任何重大代码更改的情况下完成,因为除了使用 Lambda 表达式之外,核心 JavaFX 游戏处理逻辑存在于所有这些 Java 版本中。

Java 开发工作站:必需的硬件

为了从我们将在本章过程中安装的所有专业开源软件中获得最佳效果,您将需要一个强大的 64 位工作站,运行付费操作系统,如 Windows 10 或 OSX,或免费操作系统,如 Ubuntu LTS 17。我在几个工作站上使用 Windows 10,在几个工作站上使用 Ubuntu LTS 17.10。你还需要一个大显示器,最好是高清的(1920 x 1080)或者 UHD 的(3840 x 2160)。如果你算一下,一台 UHD 显示器是四个高清显示器在一个边框里,UHD 显示器现在是 300 到 500 美元。我在感恩节大减价时花了 250 美元买了一个。我使用的高清尺寸从 32 英寸到 43 英寸不等,UHD 尺寸从 44 英寸到 55 英寸不等,像素密度很小。

一台计算机工作站应该配备(包含)至少 8GB 的 DDR3 系统内存(16GB 或 32GB 的系统内存会更好)。该存储器应以 1333、1600、1866 或 2133 兆赫的时钟速度循环。尖端系统通常配备以 2400 兆赫时钟速度运行的 DDR4 系统内存。DDR4 内存还配有 16GB DIMMS,因此您可以在工作站主板上安装 48GB、64GB 或 128GB 内存。我会为运行 Fusion 9、DaVinci Resolve 14、Blender 2.8、JavaFX 9 或其他 i3D 制作软件的工作站这样做。

系统内存运行得越快,计算机处理数据的速度就越快,CPU 获得所需处理数据的速度也就越快。这就把我们带到了进行处理的工作站的“大脑”或 CPU/GPU。同样的概念也成立;64 位 CPU 每秒可以处理的指令越多,您在更短的时间内完成的任务就越多,您的 i3D 应用执行给定功能的速度就越快。

如今,几乎所有 64 位工作站都将配备多核处理器,通常称为 CPU 或中央处理器。流行的 CPU 包括 AMD 锐龙(四核、六核或八核)、9590(八核或八核)或更昂贵的英特尔 i7,它有四核、六核、八核和十核版本。像 AMD 锐龙一样,英特尔 i7 的每个内核都有两个线程,因此对于一个操作系统来说,这些处理器看起来像 8、12、16 或 20 个内核的处理器,这就是它们比 AMD FX 9590 系列处理器更贵的原因。我使用 AMD 锐龙或英特尔 i7 处理器,这取决于应用。例如,Android Studio 3 针对英特尔硬件架构进行了优化,在 AMD FX CPU 上模拟 Android 虚拟设备(AVD)的速度不够快,无法顺利进行开发和测试。

为了存储数据,你还需要一个硬盘驱动器。现在的电脑通常配备 1TB 的硬盘,你甚至可以得到 2TB、3TB、4TB、6TB 或 8TB 硬盘的工作站。如果您正在处理具有 UHD 或 4K 屏幕分辨率的游戏(或 3D、电影、特效或视频素材),请选择 3GB 或 4GB 型号。如果您希望您的系统快速引导(启动),并快速将软件加载到内存中,请确保将 SSD(固态硬盘)作为您的主要(C:\ for Windows,或 C:/ for Linux)硬盘分配。这些硬盘比传统的 1tb 硬盘更贵,但你只需要 64GB 或 128GB 来存放你的操作系统和软件。我有一个 256GB 的固态硬盘,512GB 的固态硬盘和 768GB 的固态硬盘也变得更加实惠。

具有这些功能的工作站基本上已经成为商品,价格在 500 美元到 750 美元之间,可以在沃尔玛或百思买购买,或者在网上的 www.PriceWatch.com 购买,在那里你可以比较我在本章这一节提到的任何组件的市场价格。如果你是 Java 9 游戏开发的新手,如果你还没有合适的工作站,去沃尔玛或 PriceWatch.com,购买你负担得起的 3D 多核(购买 4、6 或 8 核)64 位计算机,运行 Windows 10 或 Ubuntu LTS 17,至少有 8、16 或 32g 的 DDR3 系统内存。你还需要一个相当大的硬盘驱动器,至少 750GB,甚至 1.5TB 或 2TB 的硬盘驱动器,以及 AMD(镭龙)或 nVidia (GeForce)的 3D GPU,用于 JavaFX 9 以及 Fusion、Blender 和 DaVinci Resolve 的实时 i3D 渲染。

在本章的剩余部分中,我将假设您刚刚购买了一台经济实惠的 64 位工作站,我们将完全从头开始创建一个 premiere Java 9 游戏和物联网开发工作站!如果你已经有一个现有的游戏开发工作站,我将包括一个简短的部分,向你展示如何从 Windows 中删除过时的 Java 开发软件,以便我们都可以从头开始。

为 Java 9 游戏开发准备一个工作站

假设您已经有一个专业级工作站用于新媒体内容开发和游戏开发,您可能需要移除过时的 JDK 或 IDE,以确保您拥有最新的软件。在本节中,您要做的第一件事是确保您已经删除了任何过时的 Java 版本,如 Java 6 或 Java 7,以及任何过时的 NetBeans 版本,如 NetBeans 6 或 NetBeans 7。这包括从工作站卸载(移除或完全删除)过时的 Java 开发软件版本。我不得不在我的一个四核 AMD 工作站上这样做,以便为 NetBeans 9.0 IDE 开发 Java 9 和 JavaFX 9 应用和游戏腾出空间,因此本节中的屏幕截图显示了 Windows 7 操作系统。您将通过使用操作系统软件管理实用程序来实现这一点。在 Windows 上,这是“程序和功能”实用程序。这可以在 Windows 控制面板下找到,在图 1-1 的中间一列(第七行)以蓝色突出显示。

A336284_1_En_1_Fig1_HTML.jpg

图 1-1。

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

如果您有一个全新的工作站,您将不必删除任何以前的软件。对于 Linux 和 Mac 来说,有类似的软件安装和卸载工具,如果你碰巧正在使用这些操作系统的话。由于大多数开发人员使用的是 64 位版本的 Windows 7、8.1 或 10,因此本书只使用 64 位操作系统平台。

值得注意的是,Java 9 现在只有 64 位版本,所以你必须有一个 64 位的工作站,正如我在本书的前一节中所指出的(事实上,现在你甚至不能买一台新的 32 位计算机)。

自定义 Windows 操作系统“chrome”(窗口 UI 元素)、桌面和已安装软件包的方式是通过 Windows 控制面板及其 50 多个实用程序图标。其中之一是程序和功能图标(在 Windows 版本 7 到 10 中),在图 1-1 中可以看到它被选中为蓝色。

请注意,在早期版本的 Windows (Vista 或 XP)中,此程序实用程序图标的标签会有所不同,如:添加或删除程序。它仍然以同样的方式工作,选择软件,右键单击,并删除旧版本。我不建议使用过时的 Vista 或 XP,因为高级 Java 9 JDKs 和 ide 不再支持它们。

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

如图 1-2 所示,在我的 Windows 7 开发工作站上,我安装了一个旧版本的 Java 8u131,占用了 442 兆的硬盘空间,安装于 2017 年 4 月 22 日。这用于运行 NetBeans 9 的“Alpha”版本,该版本在 Java 8 上运行。要删除一个软件,通过点击选择它(它会变成蓝色),或者点击图 1-2 顶部所示的卸载按钮,或者你也可以右击(蓝色)软件包(删除)选项,并从出现的上下文菜单中选择卸载。

我在屏幕截图中留下了工具提示:“卸载”,这样你就可以看到,如果你将鼠标“悬停”在程序和功能实用程序中的任何东西上,它会告诉你该特定功能的用途。

A336284_1_En_1_Fig2_HTML.jpg

图 1-2。

Select versions of Java older than Java 9 and click the Uninstall option at the top, or right-click and uninstall

单击卸载按钮后,该实用程序将删除旧版本的 Java 8。首先删除较小版本的 Java 8(非 JDK ),然后删除较大版本(完整 JDK ),因为需要完整 JDK 来删除较小的 JDK 以及任何旧版本的 NetBeans。如果要删除 NetBeans IDE,您需要安装 Java 8,因为 NetBeans IDE 是用 Java 编写的,需要安装 Java JDK 才能卸载。

一旦去掉完整的 Java 8 JDK,就只剩下 Java 9 的(Alpha)版本(如果你是我,写这本书的话,就是这样),如图 1-3 所示,标注为 9.0.0.0 版本。如果您想保留您的旧 Java 项目文件,请确保备份您的 Java 项目文件文件夹,如果您还没有这样做的话。确保您定期备份工作站的硬盘驱动器,以便不会丢失任何 3D、内容制作和编码工作。

我移除了 Java 9 JDK 软件的任何 Alpha 或 Beta 版本,方法是再次点击它(它会变成蓝色),然后或者点击图 1-3 顶部的卸载按钮,或者你也可以右键单击蓝色软件包(移除)选项,并从上下文相关菜单(通过右键单击打开)中选择卸载。

A336284_1_En_1_Fig3_HTML.jpg

图 1-3。

Select Alpha versions of Java 9, and click the Uninstall option at the top, or, right-click, and select Uninstall

现在,我已经从我的工作站上删除了 Java 的过时版本,我将从互联网上获取最新的 Java 9 开发工具包(JDK)版本,并将其安装在我的 Windows content development 工作站上。

下载并安装 Oracle Java 9 JDK

既然已经从您的工作站中删除了 Java 的过时版本,那么您需要上网并访问 Oracle 网站来获取最新的 Java 9 开发 JDK 和 IDE,因为毕竟这是一本专业的 Java 9 游戏开发书籍。在我写这本书的时候,我将向你展示如何使用直接下载的 URL 来做到这一点,以及它们现在的位置。如果这些链接已经更改,只需使用谷歌搜索“Java 9 JDK 下载”该下载目前位于甲骨文技术网,如图 1-4 中屏幕截图的顶部所示。

A336284_1_En_1_Fig4_HTML.jpg

图 1-4。

The JDK9 Download link at oracle.com/technetwork/java/javase/downloads/jdk9-downloads-3848520.htm

在下载用于 64 位 Windows 的 360 兆字节 JDK9 安装程序文件之前,您需要单击“Accept License Agreement”选项旁边的单选按钮,该选项位于下载表的左上角。

一旦您接受本许可协议,五个特定于操作系统的链接将被激活使用,包括 Linux、Mac OS/X、Windows (7 到 10)和 Solaris。确保下载的 Java JDK 软件与您的操作系统相匹配。如您所见,现在只有 64 位(或 x64)版本可用于 64 位系统。

要启动下载的 JDK9 安装程序,请右键单击该文件,并使用管理员权限(或在 Linux 上作为超级用户)以管理员身份运行来安装它。接受六个对话框中的默认设置,如图 1-5 所示。

A336284_1_En_1_Fig5_HTML.jpg

图 1-5。

Install the Java 9 JDK on the workstation, accepting the default settings in the six Java 9 installation dialogs

如果你想检查你的系统上是否安装了 Java 9,只需使用与图 1-1 到 1-3 中相同的控制面板实用程序。如图 1-6 所示,Java 9.0 的真实版本(不是 alpha 版本)现在安装在我的系统上,大小为 763 兆字节,在我的情况下,安装于 2017 年 10 月 7 日。

A336284_1_En_1_Fig6_HTML.jpg

图 1-6。

Find the JDK-for the latest (currently 9.0.1) Java 9 version and make sure it is installed

接下来,让我们安装 Java 8,它目前用于运行 NetBeans 8.2(您可能已经在使用该 IDE 进行开发),并且目前也用于运行 NetBeans 9.0 IDE (beta),我在本书中使用了该 IDE,因为最终 Java 9 和 NetBeans 9 将一起用于开发 Java 9 游戏。在过渡期间,NetBeans 9.0 在 Java 8 上运行,所以我添加了一两节来介绍它是如何工作的,以供可能使用这种配置的早期读者使用。

下载并安装 Oracle Java 8 JDK

你可能想知道为什么我们现在正在下载 Java 8 的最新版本(目前更新 152 ),因为这是一本 Java 9 游戏本。原因是尽管 Java 9 JDK 于 9 月份发布,但 NetBeans 9 IDE 版本仍处于测试阶段(我写这本书时它仍处于 alpha 阶段),这意味着 NetBeans 9(测试版)仍运行在 Java 8 之上,这是由于 Java 9 中模块的复杂性(这意味着程序员仍在模块化 NetBeans 9,以便它将在 Java 9 中编码)。一旦 NetBeans 9 发布,它很可能会直接运行在 Java 9 JDK 之上。有一种方法可以访问 Oracle Tech Network 上的一个网页,该网页同时链接到 Java 8u144 和 Java 9.0,位于 URLwww.oracle.com/technetwork/java/javase/overview/index.html,如图 1-7 所示。两个 JDK 的下载链接都位于网页的最底部,所以只需单击 Java SE 8 update 144 JDK(已经升级到 8u152)的下载链接。

A336284_1_En_1_Fig7_HTML.jpg

图 1-7。

The Oracle Tech Network Java SE Overview webpage, which has links to Java 9 JDK as well as to Java 8u144

正如你所看到的,在网页的中间也有一个红色的甲骨文 JDK 8 警告的公共更新。Java 8 没有太多的 bug,毕竟它已经经历了超过 144 次的更新,非常稳定!从某种意义上说,Java 9 是一种重写,因为它已经被重新模块化,所以所有 API 类和包(到模块中)的“连接”都被重做,这就是为什么 NetBeans 9(用 Java 编码)没有与 Java 9 同时完成(编码和调试)。Java 和 NetBeans 的早期版本同时(或接近同时)发布,并有 NetBeans 捆绑包下载(在图 1-8 的顶部显示了 NetBeans 8.2,通过右侧的下载图标)。

A336284_1_En_1_Fig8_HTML.jpg

图 1-8。

The Oracle Tech Network Java SE Download webpage which has links to Java 8u144 JDK at the very bottom

图 1-8 中的 Java SE 下载页面是上一页的下载链接将带您进入的页面。在页面底部,您会找到 Java SE 8u144 部分,其中有三个下载按钮。最上面的第一个按钮写着 JDK。这是您想要单击以开始 JDK 8u144 下载的按钮。这将把你带到 Oracle . com/tech network/Java/javase/downloads/JDK 8-downloads-2133151 . html 的页面,如图 1-9 所示。

单击接受许可证单选按钮以启用所有下载链接,然后单击适用于您的操作系统版本的链接。

A336284_1_En_1_Fig9_HTML.jpg

图 1-9。

The JDK8 Download link at oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.htm

注意,对于 Java 8,有 32 位(i586)和 64 位(x64)版本,以及 ARM CPU 版本,这给了我们十几个选择。为您的操作系统选择 64 位版本,以匹配您为 Java 9.0 安装的版本。

现在,我们可以安装 NetBeans 9.0 集成开发环境(简称 IDE),它将使用我们刚刚安装的 Java 8 运行时引擎(JRE)来运行 Java 代码,这些代码将为您创建 NetBeans 9 IDE。

因为 NetBeans 9 正在从 Oracle 过渡到 Apache,所以目前实际上有两个代码存储库。首先,我将向您展示我在写书时使用的服务器,它由 Oracle 托管,其次,我将向您展示由 Apache 托管的服务器,它使用了一个名为 Jenkins 的测试版存储库,还有一个到 GIT 的链接,如果您愿意,您可以从零开始构建 NetBeans IDE。最终还会有一个 Java 9 和 NetBeans 9 的“捆绑包”作为单一安装。这当然是最简单也是最需要的,但是目前还不存在,所以我将介绍构建和安装 NetBeans 9.0 的更高级的方法,因为它还没有完成。目前这使得安装变得复杂,但是我对此无能为力,只能提供所有这些额外的信息,以便您可以在最终的 NetBeans 9 on Java 9 捆绑包发布之前,为 Java 9 和 JavaFX 9 开发安装并运行 NetBeans 9.0。就 Pro Java 9 游戏开发而言,这给了你一个领先他人的开始。

安装 Oracle NetBeans 9.0(开发)IDE

由于 NetBeans 9 仍在开发中,我将向您展示我是如何从 Oracle 获得 NetBeans 9 发行版的,并在下一节中展示普通公众最终将如何从 Apache 获得 NetBeans 9 IDE。通过这种方式,您将了解下载和安装 NetBeans 9 的所有方法。Oracle 存储库(在软件正式转移到 Apache 之前将一直存在)位于 bits.netbeans.org/download/trunk/nightly/latest/,看起来像大家都熟悉的原始 NetBeans 下载页面,如图 1-10 所示。我建议使用该软件最简单(最小)的 Java SE 版本,因为它包括我们在本书中涉及的三个 API(NetBeans、Java 和 JavaFX)。单击第一个下载(免费,97MB)按钮,开始 NetBeans 下载过程。

A336284_1_En_1_Fig10_HTML.jpg

图 1-10。

The Oracle NetBeans 9.0 IDE download page located at bits.netbeans.org/download/trunk/nightly/latest/

一旦这个安装文件下载完毕,右击它,选择以管理员身份运行(Linux 上的超级用户),你会看到第一个欢迎对话框,在六个对话框截图的左上角,如图 1-11 所示。

单击“下一步”按钮开始默认(完全)安装,您将看到 NetBeans IDE 9 许可协议对话框,如图 1-11 中上部所示。选择“我接受许可协议中的条款”复选框(用红色圈出),然后单击“下一步”按钮,前进到 NetBeans IDE 编译安装对话框,如图 1-11 右侧所示。第三个对话框指定了 Program Files 目录中的安装位置,还指定了用于 Java 开发的 JDK。请注意,NetBeans 9 足够聪明,可以选择 Java 9 而不是 Java 8(您已经安装了 Java 9 和 Java 8,因为您可以在任何给定的工作站上安装多个 Java 版本),并定义您将为哪个版本的 Java 开发游戏(以前必须在 NetBeans 中手动设置)。保持默认设置,点击下一步按钮进入总结对话框,如图 1-11 左下方所示。确保选中“检查更新”,以便 NetBeans 9 会自动更新自身。

A336284_1_En_1_Fig11_HTML.jpg

图 1-11。

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

单击“安装”按钮后,NetBeans 将安装基本 IDE,如图 11-1 中底部的对话框所示,通过进度条和下面的解压缩文件文本向您显示它正在做什么。设置完成后,您将看到“设置完成”对话框,该对话框将为您提供“通过提供匿名使用数据为 NetBeans 项目做贡献”复选框选项我选择了这个选项,以帮助 NetBeans 开发人员。

我们将在第一章中安装各种游戏开发和游戏素材开发软件包,最后一步是通过启动软件来测试安装,以确保它能够运行。

这是通过在您的桌面(双击桌面图标以启动它们)或任务栏(称为快速启动图标,只需单击即可启动)中找到软件图标并启动软件来完成的。在 NetBeans 9 IDE 的情况下,结果应该类似于左侧的图 1-12 。

要确认 NetBeans 9 是如何设置的,请使用关于菜单序列的帮助➤,它位于图 1-12 的右侧,显示了您的产品版本、正在使用的 Java JDK 版本、正在使用的 Java 运行时环境(JRE )( JRE 是 JDK 安装的一部分)来运行 NetBeans 9.0、正在使用的操作系统以及用户目录位置和缓存目录位置。如果您在安装此 IDE 的后续版本时遇到问题,请尝试删除这两个文件(即删除\dev 文件夹),因为它们包含以前 NetBeans 安装的信息,这可能会误导下一次 NetBeans 安装。

A336284_1_En_1_Fig12_HTML.jpg

图 1-12。

Launch NetBeans using the Desktop or Quick Launch (Silver Cube), and make sure the software will launch

接下来,我将向您展示如何以及在哪里安装 Apache NetBeans 产品,因为在某个时间点,NetBeans 9.0 将完成从 Oracle 到 Apache 的转移(就像 Open Office 一样)。我不确定这将在何时发生,可能在 2018 年的某个时候,但我不能等那么久来发布这本书,所以我将简单地向您展示您可以获得 NetBeans 9.0 的所有不同方法。注意,如果您想在 Java 8 的基础上使用 8.2,这也很好,因为 JavaFX 8(和 JavaFX 9)类(API)没有改变。这是因为 Java 9(和 NetBeans 9)的重点只是将模块引入工作流并使 IDE 工作,所以 JavaFX 被单独留下,重点是 Java 的其他部分(正如您将看到的,JavaFX 是 Java 多媒体/游戏引擎)。

安装 Apache NetBeans 9(开发)IDE

接下来,我们将了解用于 NetBeans 的 Apache Jenkins 和 GIT 存储库,在当前正在进行的传输完成后,软件将在这里“着陆”。Apache Jenkins 的 NetBeans 站点位于 https://builds.apache.org/job/incubator-netbeans-windows/,是一个所谓的“孵化器”站点。孵化器用于孵化鸡蛋,因此这里的推论是,在“孵化”NetBeans 9 on Java 9 捆绑包之前,这是您可以在 NetBeans 9 IDE 软件仍处于开发阶段时获得它的地方。你可以在图 1-13 中看到 Apache Jenkins 网站此时的样子(这有可能会改变)。如你所见,它有很多选项。

A336284_1_En_1_Fig13_HTML.jpg

图 1-13。

Apache Jenkins' NetBeans is located at https://builds.apache.org/job/incubator-netbeans-windows/

Jenkins 的左上方有 Jenkins 软件孵化器功能导航链接,因此您可以返回到仪表板(主页),获得开发状态,查看构建之间的更改,查看依赖关系图,获得构建时责任报告,查看 GIT 轮询日志,获得嵌入式构建状态,查看测试结果分析器,跳过构建并打开 Blue Ocean。Blue Ocean 是一个免费的、开源的、持续更新的工具,它让你感觉你是软件开发团队的一员。

下面是构建历史。这是一个包含构建的窗格,在构建时,带有进度条、构建时间和完成估计。如果你点击其中的一个版本(如果已经完成),它会打开另一个窗口(browser 选项卡),显示该版本的详细信息和下载链接。如图 1-14 所示。

要下载其中一个 ZIP 文件,请右键单击该文件,并使用“另存为”功能,将该 ZIP 文件保存在硬盘上要从中解压缩 NetBeans 9 的位置(目录/文件夹)。请注意,这是一种不同于安装程序的方法。exe 或。msi)将采取,因为安装程序会将文件与其他已安装的应用一起放入 Program Files 文件夹,并将创建桌面图标和任务栏快速启动图标。

有人告诉我,Linux 版本也可以在 Windows 下运行,但是在某些时候,Mac OS/X、Linux 和 Windows 可能会有不同的版本。我还在开发人员列表中添加了一个请求,请求建立一个 Ubuntu LTS Linux 17 PPA 存储库,以便 Ubuntu LTS 可以自动更新 NetBeans 9.0 IDE,几乎不需要最终用户干预。如果你还没有看过 Ubuntu LTS 17.10 或 18.04,你可能想现在就看;你会惊讶于 Ubuntu Linux (Debian,另一个主要的 Linux 发行版是 Fedora)相对于 OSX 或 Windows 已经走了多远。

一旦你解压 NetBeans 9(我把我的文件夹命名为 NetBeans-9-Build ),进入/bin(二进制)文件夹,右键单击 netbeans64.exe,并使用 Run as Administrator。在启动品牌屏幕和加载进度条之后,您将看到一个许可协议对话框,您必须接受(同意)该对话框才能启动 IDE 软件。

A336284_1_En_1_Fig14_HTML.jpg

图 1-14。

Click a version to get a build page, shown is builds.apache.org/job/incubator-netbeans-linux/74/

接下来,让我们去下载十几个最流行的(免费的)开源新媒体内容开发软件包,这样您就拥有了 Pro Java 9 游戏开发业务最终需要的所有强大的专业工具。这代表了数以万计的(你的货币是美元,我的是美元)付费软件包,所以这第一章最终会对所有读者变得相当有价值。

之后,我将告诉你一些我在工作站上使用的其他令人印象深刻的开源软件,这样,如果你想在本章结束之前组装最终的软件开发工作站,你可以这样做,以硬件(和操作系统)的成本创建一个非常有价值的内容制作工作站。

安装新媒体内容制作软件

JavaFX 9 支持许多新媒体元素的“类型”,我称之为“素材”, Java FX 9 是 Java 9 的新媒体“引擎”,因此您将使用它作为专业 Java 9 游戏开发的基础。在本章的剩余部分,您将安装领先的开源软件的主要新媒体类型包括:SVG 数字插图、数字图像合成、数字音频编辑、数字视频编辑、VFX 或视觉效果、3D 建模和动画、虚拟世界创建、角色动画、歌曲创作、数字音频采样、办公效率(是的,您还必须销售您的游戏)等等。

下载并安装 InkScape for SVG 数字插图

由于 JavaFX 支持 Adobe Illustrator 和 Freehand 等数字插图软件包中常用的 2D 或“矢量”技术,我们将下载并安装流行的开源数字插图软件包 InkScape,该软件包最近在版本控制方面从 0.48 大幅提升到 0.92,并具有专业功能。InkScape 可用于 Linux、Windows 和 Macintosh 操作系统,就像我们将在本章安装的所有软件包一样,因此读者可以使用他们喜欢的任何平台来开发游戏。如果您想了解更多关于数字插图和 SVG 的知识,可以看看来自 Apress 的数字插图基础知识。

要在互联网上找到 InkScape 软件包,请使用谷歌搜索引擎,并输入 InkScape。访问网站,点击左上方的下载菜单或右边的下载图标,如图 1-15 所示。下载图标将代表您正在使用的操作系统,由网站代码自动检测,该代码会轮询您的系统正在使用的操作系统,并通过单击自动为您提供正确的版本。

A336284_1_En_1_Fig15_HTML.jpg

图 1-15。

Google the word InkScape, go to the inkscape.org website, click on the download icon, or download menu

下载 InkScape 软件后,右键单击文件名,并以管理员身份运行,在您的工作站上安装该软件。如果您愿意,可以使用本章前面使用的程序和功能实用程序来卸载以前的 Inkscape 版本。

安装软件后,在任务栏上创建一个快速启动图标,这样只需单击鼠标即可启动 InkScape。接下来,您将安装一个名为 GIMP 的流行数字图像软件包,它允许您使用 JPEG、PNG、WebP 或 GIF 数字图像格式为游戏创建“光栅”或基于像素的艺术作品。

下载并安装 GIMP 进行数字图像合成

因为 JavaFX 还支持利用“光栅”图像技术的 2D 图像,该技术将图像表示为像素阵列。这是付费数字图像合成软件包中使用的内容,如 Adobe Photoshop 和 Corel Painter。我们将下载并安装流行的开源数字图像编辑和合成软件包“Gimp”GIMP 适用于 Linux、Windows、Solaris、FreeBSD 和 Macintosh 操作系统。如果你想了解更多关于数字图像合成的知识,看看来自 Apress 的数字图像合成基础知识。要在互联网上找到 GIMP 软件,使用谷歌搜索,并输入 GIMP。网站如图 1-16 所示。

A336284_1_En_1_Fig16_HTML.jpg

图 1-16。

Google Search GIMP; go to gimp.org; click the Download link for 2.8.22, or for 2.10 (currently 2.9.6 beta)

单击 Download 链接(或右键单击,并在单独的选项卡中打开它),并单击 Download GIMP 2.8.22(或更高的版本,如新的 2.10 或 3.0 版本,目前正在测试 2.9.6,很快将是 2.9.8),它代表您正在使用的操作系统。

下载页面会自动检测你用的是什么 OS,给你正确的 OS 版本;就我而言,我在 Windows7、Windows 10 和 Ubuntu LTS Linux 17.04 上使用 GIMP,因为我在我的每一个工作站上都安装了它。不用说,开源软件比付费软件包有更多的优势。

下载完软件后,安装最新版本的 GIMP,然后为您的工作站任务栏创建一个快速启动图标,就像您为 InkScape 所做的那样。

接下来,我们将安装一个强大的数字音频编辑和特效软件包 Audacity。

下载并安装 Audacity 进行数字音频编辑

JavaFX 支持利用数字音频技术的 2D(和 3D)数字音频。数字音频通过采集数字音频“样本”来表示模拟音频数字音频内容通常使用诸如 Cakewalk Sonar 的数字音频合成和序列器软件包来创建。如果您想了解更多关于数字音频编辑的知识,请查看来自 Apress 的《数字音频编辑基础》一书。在本节中,我们将下载并安装流行的开源数字音频编辑和优化软件包“Audacity”Audacity 可用于 Linux、Windows 和 Macintosh 操作系统。要在互联网上找到 Audacity 软件包,请使用 Google 搜索引擎,并输入 Audacity,这会显示 Audacity 团队网站。进入该网站,如左上方图 1-17 所示。点击下载 Audacity 链接(或使用下载菜单)并点击 Audacity for Windows(或您正在使用的操作系统版本)。我也在 Ubuntu Linux LTS OS 17.04 上使用 Audacity 2.1.3。

A336284_1_En_1_Fig17_HTML.jpg

图 1-17。

Google the word Audacity, go to audacityteam.org, and click a Download Audacity link matching your OS

下载并安装 Audacity 的最新版本,目前是 2.1.3,并为您的工作站任务栏创建快速启动图标,就像您为 InkScape 和 GIMP 所做的那样。当你读到这篇文章时,Audacity 2.2.0 可能已经发布了,它增加了新的用户界面设计和许多很酷的新数字音频编辑、合成和增音功能。

接下来,您将安装一个专业的非线性数字视频编辑和“颜色计时”(也称为颜色校正)软件包,用于故事片,该软件包最近从 12.5 版升级到 14 版,名为 Black Magic Design DaVinci Resolve。仅仅在一两年前,这个软件包曾经要花费数千美元!

下载并安装用于数字视频的 DaVinci Resolve 14

JavaFX 9 支持数字视频,它利用了基于“光栅”像素的动态视频技术。这将视频表示为一系列帧,每个帧包含一个基于像素阵列的数字图像。数字视频素材通常是使用数字视频编辑和色彩计时软件包(如 AfterEffects 和 EditShare LightWorks)创建的。在本节中,我们将下载并安装最新版本的开源数字视频编辑软件 DaVinci Resolve 14。该软件包适用于 Windows 10、Mac OSX 和 Ubuntu Linux 以及其他发行版。要找到达芬奇密码,使用谷歌搜索并输入达芬奇密码。点击中间如图 1-18 所示的下载按钮,或者滚动到页面底部,点击免费下载按钮。

A336284_1_En_1_Fig18_HTML.jpg

图 1-18。

Google the word DaVinci Resolve; go to BlackMagicDesign.com webpage; click on the Download button

安装软件,并为任务栏创建一个快速启动图标,就像您对所有其他软件所做的那样。如果你想了解更多关于数字视频编辑的知识,可以看看来自 Apress 的数字视频编辑基础知识。接下来,我们将安装一个高级特效,3D 建模和动画,以及 VR 包,名为 BlackMagic Fusion。

下载并安装 Blackmagic Fusion 以获得视觉效果

JavaFX 还支持特效管道,因为所有的新媒体类型都可以使用 Java 9 代码无缝地组合在一起。SFX 同时利用基于“光栅”像素的运动视频技术、静态图像合成、数字音频、3D、i3D 和 SVG 数字插图,因此与 3D 建模和动画一样先进。BlackMagicDesign 的 Fusion 曾经是一个付费软件包,直到它被开源。有一个专业版以前要 999 美元现在要 299 美元!如果你是认真的多媒体,购买这个!

您首先必须在BlackMagicDesign.com网站上注册,才能下载和使用该软件。该软件包适用于 Linux、Windows 10 和 Macintosh 操作系统。要在互联网上找到 Fusion,使用谷歌搜索引擎并输入 Fusion 9,你将被指引到图 1-19 所示的地方。单击代表您正在使用的操作系统的下载按钮。此下载页面将自动检测您使用的操作系统;在我的情况下,Windows。

A336284_1_En_1_Fig19_HTML.jpg

图 1-19。

Google the word Fusion 9; go to the blackmagicdesign.com download page; click on the download button

在 BlackMagicDesign.com 网站上注册,如果你还没有这样做,一旦你被批准,你就可以下载并安装 Fusion 9 的最新版本。安装软件,并为任务栏创建一个快速启动图标,就像您对其他软件所做的那样。如果你想详细了解融合,Apress.com 最近有一本名为《VFX 基础》的书,该书更详细地介绍了融合和视觉效果合成管道。

接下来,我们将安装一个名为 Blender 的 3D 建模和动画包。

下载并安装用于 3D 建模和动画的 Blender

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

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

A336284_1_En_1_Fig20_HTML.jpg

图 1-20。

Google the words Blender 3D, go to www.blender.org and click on the blue Download Blender 2.79 button

下载并安装用于 3D 地形或世界创建的 Terragen

另一个令人印象深刻的(免费的,对于基础版本,或者如果你在教育行业)3D 世界生成软件包是英国 Planetside Software 的 Terragen 4.1。你可以在 Planetside.co.uk 下载基本版,也可以加入他们的论坛。我也在我的几本 Android 应用开发书籍中使用过这个软件,所以我知道它在多媒体应用、交互式电视或 iTV 以及游戏等项目中使用效果很好。它也被专业电影制作人使用,因为它的质量水平非常原始。由于我们在本书中涉及 3D,您可能希望了解 Terragen,因为它价格实惠,并且被电视制片人和电影工作室使用。要在互联网上找到 Terragen 软件,请使用谷歌搜索引擎并输入 Terragen 4.1。点击链接,将弹出 Planetside 软件网站,如图 1-21 所示。

A336284_1_En_1_Fig21_HTML.jpg

图 1-21。

Google the word Terragen; go to the planetside.co.uk website; click on a blue GET IT button to download

单击 GET IT download 链接下载并安装 Terragen,然后为该软件创建一个快速启动图标。如果你喜欢这个 3D 软件,一定要升级到软件的专业版,这是非常实惠的。

下载并安装 Daz Studio Pro 以制作角色动画

对于专业的 3D 角色建模和动画,如果有机会的话,一定要看看位于 daz3d.com 的 DAZ 3D 公司的 3D 软件包。DAZ Studio PRO 目前的版本是 4.9,没错,是免费的!你必须登录并注册,就像你为黑魔法设计软件所做的那样,但这只是一个小小的代价!这个网站上还有一个免费的 3D 建模软件包叫做 Hexagon。DAZ 3D 网站上最贵的软件是 Carrara(150 美元)或 Carrara Pro(285 美元)。DAZ 工作室的大部分收入来自销售各种类型的角色模型,所以去看看他们吧,因为他们是 3D 内容(虚拟)世界中不可忽视的力量!

要在互联网上找到 Daz Studio Pro 软件,使用谷歌搜索引擎并键入 Daz Studio Pro 5 下载。该链接会将您带到 daz3d.com/daz_studio 页面,如图 1-22 所示。点击下载链接下载并安装最新版本的 Daz Studio Pro,然后创建您的快速启动图标。

A336284_1_En_1_Fig22_HTML.jpg

图 1-22。

Google the words Daz Studio Pro, go to www.daz3d.com , and download the latest version of Daz Studio

其他开源新媒体软件包

我在新媒体内容制作业务中也使用了大量其他专业水平的开源软件包。我想让你知道其中的一些会很好,以防你没有听说过它们。这些将为您在本章中构建的新媒体制作工作站增加更多功能和多样性。值得注意的是,在进行所有这些大量下载和安装的过程中,您已经为自己节省了数千美元(或您的本地货币单位),这些钱本来可以花在类似的付费内容制作软件包上。我想我的座右铭可以说是:“第一次就做对,并确保一直做下去”,所以我将继续向您介绍其他一些免费的,甚至是一些更易于使用(不是免费的,但非常实惠)的新媒体内容制作软件包,我通常在我的 3D 内容制作工作站上安装这些软件包。

除了过去价格接近六位数的 DaVinci Resolve 软件包之外,开源软件的最佳价值之一是一个商业生产力软件套件,它在 Oracle 收购 Sun Microsystems 后被收购,然后成为开源软件。Oracle 将其 OpenOffice 软件套件转移到流行的 Apache 开源项目,就像他们目前对 NetBeans 9 所做的一样。

Open Office 4.3 是一个完整的办公效率软件套件,其中包含六个成熟的商务效率软件包!由于您的内容制作代理实际上是一个成熟的企业,您可能应该了解办公软件,因为这是一个非常可靠的开源软件产品。您可以在:OpenOffice.org找到它,这个流行的商业软件包已经被像您这样精明的专业人士下载了超过一亿次,所以,这不是一个玩笑,正如他们所说的那样!

对于用户界面(UI)设计原型,Evolus.vn 提供了一个名为 Pencil 2.0.6 的免费软件包,允许您在用 Java、Android 或 HTML5 创建用户界面设计之前,轻松地制作用户界面设计原型。该软件位于pencil.evolus.vn,可用于 Linux 发行版、Windows 7 和 8.1 以及 Macintosh OS/X。

对 Audacity 2 数字音频编辑软件的一个很好的补充是 Rosegarden MIDI sequencing 和 music composition and scoring 软件,它可以用于音乐创作,并打印出音乐出版的结果乐谱。Rosegarden 目前正在从 Linux 移植到 Windows。请注意,功能最全的版本是针对 Linux 的,如图 1-23 所示。可以使用谷歌搜索或者在RoseGardenMusic.com找到它,目前它的版本是 17.04(与 LTS 的 Ubuntu 相同)。这就是通常所说的“千载难逢的两次”版本。

A336284_1_En_1_Fig23_HTML.jpg

图 1-23。

Rosegarden is a MIDI, music scoring, and notation program for Linux which is being ported to Windows 10

另一个令人印象深刻的音频、MIDI 和声音设计软件包叫做 Qtractor,这是一个基于硬盘驱动器的音频采样器、编辑器和声音设计软件包,如图 1-24 所示。因此,如果你运行的是 Linux 操作系统,一定要谷歌搜索、下载并安装这个专业级数字音频合成软件包,你可以在 SourceForge 的Qtractor.SourceForge.net URL 网址找到它。

A336284_1_En_1_Fig24_HTML.jpg

图 1-24。

Qtractor, the hard disk based digital audio editing software for Linux

另一个令人印象深刻的免费 3D 建模和动画软件是 Caligari TrueSpace 7.61,当它由 Caligari Corporation(后来被微软收购)的罗曼·奥曼迪开发时,曾经花费近 1000 美元,你可以在多个网站上找到它,只需在谷歌上搜索 Caligari TrueSpace 3D。

另一个你应该看看的 3D 渲染软件是 POVRay。POV 代表“视觉暂留”,该软件被称为“光线跟踪器”,是一种高级渲染引擎,可与任何 3D 建模和动画软件包配合使用,使用高级光线跟踪渲染算法生成令人印象深刻的 3D 场景。最新版本可在 www.povray.org 网站上找到。它是 3.7,最新版本是 64 位,并且是多核(多线程)兼容的,可以免费下载,这就是我在这里告诉你的原因。

另一个专为 POVRay 设计的时尚 3D 建模软件包是 Bishop 3D。该软件可以用来创建自定义的 3D 对象,然后可以导入到 POVRay(然后导入到 JavaFX)中,以便在您的 pro Java 游戏中使用。最新版本是适用于 Windows 7 或 10 的 1.0.5.2。该软件可以在 www.bishop3d.com 上找到,最新版本是 8MB 下载,目前可以免费下载。

另一个你应该看看的免费 3D 细分建模软件是 Wings3D。这个软件可以用来创建 3D 对象,然后导入 JavaFX 在你的游戏中使用。最新版本是 2.1.5,于 2016 年 12 月发布,适用于 Windows 10、Macintosh OS/X 和 Ubuntu Linux。该软件可以在wings3d.com上找到,最新版本为 64 位,下载容量为 16MB,目前可以免费下载。

接下来我将向你展示我是如何在任务栏上组织一些基本的操作系统工具和开源软件的。在接下来的几章中,我们将开始学习使用新媒体素材背后的原理,之后,我们将学习如何使用 NetBeans 9 创建 JavaFX 9 项目,然后,在我们开始学习有关强大的 JavaFX 9.0 多媒体游戏引擎的细节之前,我们将在下一章中学习 Java 编程语言。

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

有一些操作系统实用程序,如计算器、文本编辑器(在 Windows 中称为记事本)和文件管理器(在 Windows 中称为资源管理器),我在任务栏中保留了快速启动图标,因为它们在编程和新媒体内容开发工作流程中经常使用。我还在我的任务栏上保留了各种新媒体开发、编程和办公效率应用作为快速启动图标,如图 1-25 所示,其中显示了十几个应用,包括我们刚刚安装的所有应用(按安装顺序排列),以及其他一些应用,包括 OpenOffice 4.3、DAZ Studio Professional 4.9 和 Bryce Professional 7.1。

A336284_1_En_1_Fig25_HTML.jpg

图 1-25。

Make Taskbar Quick Launch Icons for key system utilities, NetBeans 9 and new media production software

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

恭喜你!您刚刚创建了您的新媒体 Java 游戏和物联网开发工作站,该工作站经过了高度优化,将允许您创建您的客户可以想象的任何新媒体 Java 游戏或物联网项目!

摘要

在第一章中,您确保自己拥有开发创新 Java 游戏或物联网项目所需的一切,包括最新版本的 Java 9、JavaFX 9.0、NetBeans 9 和所有最新的开源新媒体软件。这包括获得最新的 Java 9 JDK 和 NetBeans 9 IDE 软件,然后我们安装 Java 9 和 NetBeans 9。在那之后,你为一群专业的、开源的、新媒体内容工具做了同样的事情。

二、内容创作导论:2D 新媒体素材基础

由于上一章的介绍,现在您已经有了一个专业的 Java 游戏和物联网开发工作站,让我们直接进入并了解大多数新媒体内容开发软件包所基于的基本 2D 内容开发概念和原则。Blender 是个例外,它基于更高级的 3D 内容开发,我们将在下一章讨论。在我们开始学习 Java、NetBeans 和 JavaFX 之前,我们需要先介绍这些基础多媒体材料,因为 JavaFX 多媒体引擎为使用可缩放矢量图形(SVG)的数字插图提供了令人难以置信的支持;使用光栅(位图)图像格式(如 PNG、JPEG 或 GIF)的数字成像;使用 MP3、MPEG4 AAC、WAV 或 AIFF (PCM)等音频格式的数字音频,以及使用 JavaFX 内部渲染引擎的 3D。我假设您不会创建基于文本的游戏,而是创建交互式新媒体应用,所以我想先介绍一些与编码无关的主题。一旦我们开始使用 NetBeans、Java 和 JavaFX APIs 编码,我们将永远不会停止编码。

在本章中,您将详细了解 JavaFX 支持的每种 2D 新媒体内容类型背后的概念,包括数字插图(矢量)、数字图像(光栅)、数字视频(运动)和数字音频(波形)。我们这样做是为了让你掌握基础知识,能够使用你在第一章下载并安装的免费开源多媒体内容制作工具进行游戏设计。

我想介绍的第一件事是数字图像的基本新媒体素材类型,因为它将被用作许多其他新媒体素材类型的基本输入素材。例如,您的数字视频只是一系列随时间快速播放的数字图像,以创建一种运动错觉。您的 2D 矢量插图素材可以使用 JavaFX ImagePattern 类填充数字图像数据,您的 3D 矢量素材可以将数字图像素材用于着色器和纹理贴图,我们将在第三章中对此进行介绍,包括高级 3D 内容创建和用于实现这些 3D 内容元素的相关 JavaFX 包和类。

接下来,我将介绍数字视频的概念、技术和“行话”,包括帧、帧速率、比特率以及其他添加第四维时间的概念,从而将静态数字图像素材制作成动画数字视频素材。这些概念也与动画有关,包括 2D 动画和 3D 动画。我们将在第三章讨论 2D 矢量和 3D 矢量概念,因为它们密切相关。

最后,我们将看看数字音频的概念;数字音频与数字视频密切相关,因为它可以包含在数字视频文件格式中。数字音频也可以独立存在,因此我们还将介绍 JavaFX 中的数字音频格式支持,以及数字音频素材数据足迹优化工作流程。因此,我们将在本章涵盖所有 2D (X,Y 数据表示)新媒体形式,2D 矢量插图除外,它与 3D 矢量渲染密切相关,我们将在第三章的第一部分涵盖。

游戏设计素材:新媒体内容概念

让你的游戏内容变得专业并在视觉上让你的客户满意的最强大的工具之一是你在第一章下载并安装的多媒体制作软件。在我深入阅读本书之前,我需要花一些时间向您提供关于 Java 通过 JavaFX 多媒体引擎支持的四种主要类型的新媒体素材的基础知识。这些包括数字图像,用于精灵,背景图像和 2D 动画;向量形状,用于 2D 插图、碰撞检测、2D 形状、路径和曲线;数字音频,用于音效、旁白和背景音乐;和数字视频,在游戏中用于动画背景循环(天空中飞翔的鸟,飘动的云,等等。)和高度优化的视频播放。正如你在图 2-1 中看到的,这四个 2D 流派,或者区域,都是使用 JavaFX 场景图安装在你的游戏中的。还有一个新的媒体领域,我想称之为交互式 3D (i3D)。i3D 为 Java 8 和 9 带来了实时 3D 渲染,我们将在下一章介绍 OpenGL ES。

A336284_1_En_2_Fig1_HTML.jpg

图 2-1。

How 2D or audio new media assets are implemented in Scene Graph using JavaFX 9, Java 9, and NetBeans 9

因为在您能够在 Java 游戏设计(或编程)管道中创建或正确实现这些新媒体元素之前,您需要有一个技术基础,所以我将对四个新媒体领域中的每一个领域的核心概念进行回顾。仅有的两个在概念上相关的是 2D 动画和数字视频,因为它们都涉及到使用第四维度的时间和帧,所以我将把它们放在一起讨论。由于数字音频也涉及到时间的第四维度,我将用数字音频的概念来结束;最后,我们将快速了解数字内容优化,以便您的专业 Java 游戏和物联网项目紧凑且下载快速。

数字成像概念:分辨率、色深、Alpha、图层

JavaFX 支持最流行的数字图像文件(数据)格式,这给了我们游戏设计者极大的灵活性。由于 JavaFX 8 API 现在是 Java 8 和 9 的一部分,这意味着 Java 也支持这些图像格式。其中一些数字图像格式已经存在了几十年,如 CompuServe 图形信息格式(GIF)或广泛使用的联合图像专家组(JPEG)格式。一些 JavaFX 数字图像格式更加现代;例如,便携式网络图形(PNG,发音为“ping”)是我们将在游戏中使用的文件格式,因为它可以产生最高的质量水平,并支持图像合成,我们将很快了解这一点。Java 支持的所有这些主流数字图像文件格式在 HTML5 浏览器中也受支持,由于 Java 应用可以在 HTML 应用或网站中使用,这确实是一个非常合乎逻辑的协同作用!如果需要更广泛的数字图像文件格式支持,也可以使用名为 ImageJ 的第三方数字图像库。

最古老的格式是一种称为 CompuServe GIF 的无损数字图像文件格式。之所以称之为无损,是因为它不会丢弃(丢失)任何源图像数据来实现压缩结果。GIF 压缩算法不像 PNG 格式那样精细(不像 PNG 格式那样强大),GIF 只支持索引颜色,这是它获得压缩的方式(文件更小)。在这一部分的后面,我们将详细了解颜色深度(索引色与真彩色)。如果你所有的游戏图像资源都是使用 GIF 格式创建的,你就可以在你的 Java 游戏中毫无问题地使用它们,而不是低效的图像压缩和有限的图像合成能力。

Java 通过 JavaFX 支持的最流行的数字图像格式是 JPEG。JPEG 使用“真彩色”色深,而不是索引色深。稍后我们将讨论色彩理论和色彩深度。JPEG 使用所谓的有损数字图像压缩。这是因为压缩算法会“丢弃”图像数据,以便获得更小的文件大小。这个图像数据就永远丢失了,除非你聪明点,把原来的 raw 图像保存下来!

如果您在压缩后放大 JPEG 图像,您会看到变色或脏污的区域,而这些区域在原始图像中是不存在的。在数字成像行业中,图像中的退化区域被称为压缩伪像。这只会发生在有损图像压缩中,在 JPEG(和 MPEG)压缩中很常见。

我推荐您在专业 Java 游戏中使用的数字图像格式是可移植网络图形文件格式。PNG 有两个真彩色文件版本;一个名为 PNG24,不能用于图像合成,另一个名为 PNG32,带有一个用于定义透明度的 alpha 通道,我们将在稍后介绍。还有一个索引(最多 256;可以更少)彩色版的 PNG 格式,称为 PNG8。

我为你的游戏推荐 PNG 的原因是因为它有一个不错的图像压缩算法,并且是一种无损图像格式。这意味着 PNG 有很好的图像质量以及合理的数据压缩效率,这将使你的游戏发行文件更小。PNG32 格式的真正力量在于它能够使用透明度和抗锯齿(通过其 alpha 通道)与其他游戏图像进行合成。

数字图像分辨率和长宽比:定义您的图像大小和形状

您可能知道,数字图像是由二维(2D)像素阵列组成的。Pixels 是图片(pix)元素(els)的缩写。图像中的像素数量由其分辨率表示,分辨率是图像宽度(或 W,有时称为 x 轴)和高度(或 H,有时称为 y 轴)维度中的像素数量。图像的像素越多,分辨率就越高。这与数码相机的工作原理类似,因为图像捕捉设备(通常是相机的电荷耦合器件(CCD),用于捕捉图像数据)中的像素越多,可以实现的图像质量就越高。

要计算图像像素的总数,请将宽度像素乘以高度像素。例如,一个宽 VGA 800x480 图像将包含 384,000 个像素,正好是 1MB 的 3/8。这就是你如何找到你的图像的大小,包括使用的千字节(或兆字节)和显示屏上的高度和宽度。

使用图像纵横比来指定数字图像素材的形状。纵横比是数字图像的宽高比,定义了正方形(1:1 纵横比)或矩形(也称为宽屏)数字图像形状。具有 2:1(宽屏)宽高比的显示器,例如 2160x1080 分辨率,已经广泛应用。

1:1 纵横比的显示器或图像总是完美的正方形,2:2 或 3:3 纵横比的图像也是如此。例如,物联网开发人员可能会在智能手表上看到这个长宽比。值得注意的是,定义图像或显示屏形状的是这两个宽度和高度数字之间的比率,或者 X 和 Y 变量,而不是实际的数字本身。实际数字定义了屏幕的分辨率或总像素阵列能力。

纵横比应该始终表示为纵横比冒号两边可以达到(减少)的最小数字对。如果你在高中时注意学习最小公分母,那么长宽比对你来说很容易计算。我通常通过继续将冒号的每一边除以 2 来计算长宽比。例如,如果您采用 SXGA 1280x1024 分辨率,则 1280x1024 的一半是 640x512,而 640x512 的一半是 320x256。320x256 的一半是 160x128,一半是 80x64,一半是 40x32,一半是 20x16。20x16 的一半是 10x8,而其中的一半给你 SXGA 5:4 的长宽比。

数字图像色彩理论和色彩深度:定义精确的图像像素颜色

每个数字图像像素的颜色值可以由三种不同颜色(红色、绿色或蓝色(RGB ))的量来定义,这三种颜色在每个像素中以不同的量存在。消费电子显示屏利用加色,即每个 RGB 颜色通道的光波长相加在一起,以创建 1680 万个不同的颜色值。在 LCD、LED 或有机发光二极管显示器中使用加色。它与印刷中使用的减色法相反。为了向您展示不同的结果,在减色模式下,将红色与绿色(油墨)混合将产生紫色,而在加色模式下,将红色与绿色(浅色)混合将产生鲜艳的黄色。加色可以提供比减色更宽的颜色范围。

为每个像素保存的每个红色、绿色和蓝色值有 256 个亮度级别。这允许您设置 8 位数据值范围,或 0 到 255,控制每个红色、绿色和蓝色值的颜色亮度变化。该数据使用十六进制记数法表示,从最小值零(#00 或关,全黑或黑色)到最大值 255 (#FF 或全开,或贡献最大 RGB 颜色,形成白色)。

用于表示支持的数字图像像素颜色数量的位数被称为图像的颜色深度,并使用“2 的幂”,就像 3D 用于纹理映射一样,我们将在下一章讨论这一点。因此,PNG8 图像使用 256 种颜色,PNG7 使用一半的颜色(128),PNG6 使用一半的颜色(64),PNG5 使用一半的颜色(32),因此 PNG 4 使用 16,PNG3 使用 8,PNG2 使用 4,PNG1 使用 2,或黑白(开或关)。通常,您会希望使用完整的 256 色,因为 JavaFX 仅支持 PNG8、PNG4 或 PNG1,所以如果您要使用索引彩色影像,请使用 PNG8。

数字成像行业中常用的颜色深度包括 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 支持其中的三种:JPEG、PNG24 (24 位)和 PNG32 (32 位)。使用真彩色深度 24 位或 32 位影像将为您提供最高水平的质量。这就是为什么我一直建议你在 Java 9 游戏和物联网项目中使用 PNG24 或 PNG32 格式。

接下来,让我们看看如何通过使用 PNG32 图像的 alpha 通道来表现图像的透明度。

数字图像合成:对图层使用 Alpha 通道和透明度

接下来,让我们看看如何使用 alpha 通道定义数字图像像素透明度值,以及如何使用这些值为 Java 游戏实时合成数字图像。合成是将多个数字影像层无缝融合在一起的过程。正如你所想象的,这对于游戏设计和开发来说是一个非常重要的概念。当您想要在显示器上创建一个看起来像是单个图像(或动画)但实际上是(多个)合成图像层的无缝集合的图像时,合成非常有用。您想要设置图像或动画合成的主要原因之一是,通过将每个元素放在不同的层上,允许对这些图像中的各种元素进行编程控制。

要实现这一点,您需要有一个 alpha 通道透明度值,您可以利用它来精确地控制该像素与它下面的其他层上相同 X,Y 图像位置中的像素的混合。在数字成像软件中,每个图像层的透明度值通过使用棋盘图案来表示,您可以在图 2-2 的右侧看到。

A336284_1_En_2_Fig2_HTML.jpg

图 2-2。

Showing the checkerboard representation of transparent pixels in an image, as well as the RGBA channels

在 GIMP 的左边你可以看到 Alpha 层,我选择的是蓝色。它包含 MindTaffy 徽标的透明度值。GIMP 通道调色板是我选择向您显示这些颜色和 alpha 通道(红色、绿色、蓝色、alpha)的选项卡,它为每个层分别保存这些颜色(和 Alpha)通道,允许您对每个图像复合层中的每个像素进行难以置信的控制。

像其他 RGB 通道一样,alpha 通道有 256 个级别,但这些值不是红色、绿色或蓝色,而是透明度级别。在 Java 编程中,alpha 通道由十六进制表示中的前两个槽表示,以#AARRGGBB 数据值格式描述。我们将在下一节详细介绍这一点。Alpha 加颜色通道 ARGB 数据值利用八个槽(32 位)数据,而不是 24 位图像中使用的六个数据槽(#RRGGBB),这可以被认为是具有零(无)alpha 通道数据的 32 位图像。

因此,24 位(PNG24)图像没有 alpha 通道,不会用于合成,除非它是合成层堆栈中的背景(底部)图像板。另一方面,PNG32 影像将用作 PNG24(背景板)影像之上的合成层,或者用作 z 顺序较低的 PNG32 合成层之上的合成层,这些合成层将需要其 alpha 通道功能,以便通过这些 alpha 通道透明度值显示图像合成中需要某种透明度(或不透明度)的某些像素位置。

数字图像 alpha 通道和图像合成的概念是如何影响 Java 游戏设计的?你一定在想!主要优势是能够将游戏画面、精灵、投射物和背景图形元素分解成多个组件层。这样做的原因是为了能够将 Java 编程逻辑(或 JavaFX 或 SVG 特效)应用于单个图形图像元素,以控制游戏画面的各个部分。如果没有 2D 合成方法,你将无法单独控制游戏组件,因为对大多数设备来说,逐个像素的处理过于密集。

图像合成的另一部分,称为混合模式,也是专业图像合成功能的重要因素。JavaFX 混合模式是通过使用包含 BlendMode 常量值的 Blend 类来应用的,该常量值位于 javafx.scene.effect 子包中,我们将在本书的后面部分介绍该子包。这个 JavaFX blend effect 类为 Java 游戏开发人员提供了许多与 Photoshop 或 GIMP 为数字图像制作人员提供的图像合成模式相同的模式。这将 Java 和 JavaFX 变成了一个强大的图像合成引擎,就像 GIMP 一样,混合算法可以在非常灵活的级别上进行控制,使用自定义的 Java 代码。一些 JavaFX 混合模式常量包括 ADD、SCREEN、OVERLAY、DARKEN、LIGHT、MULTIPLY、DIFFERENCE、EXCLUSION、SRC_ATOP、SRC_OVER、SOFT_LIGHT、HARD_LIGHT、COLOR_BURN 和 COLOR_DODGE 常量。

在 Java 游戏逻辑中表示颜色和 Alpha:使用十六进制记数法

现在,您已经知道了什么是色深和 alpha 通道,并且在任何给定的数字图像中,颜色和透明度是通过使用四种不同的 alpha、红色、绿色和蓝色(ARGB)图像通道的组合来表示的,现在重要的是,作为程序员,我们应该如何在 Java 和 JavaFX 中表示这四种 ARGB 图像颜色和透明度通道值。

在 Java 编程语言中,颜色和 alpha 不仅用于 2D 数字图像,通常称为位图图像,还用于 2D 插图,通常称为矢量图像。颜色和透明度值也经常在许多不同的颜色设置选项中使用。例如,您可以为 JavaFX Stage、场景、布局容器(如 StackPane)、矢量形状填充或 UI 控件设置背景色(或透明度值),以及其他内容,如 3D 素材特征。我们将在以后的章节中讨论 3D 和 JavaFX。

在 Java 和 JavaFX API 中,不同级别的 ARGB 颜色强度值用十六进制表示。十六进制,或简称为“hex ”,是基于原始的 16 进制计算机符号。这在很久以前被用来表示 16 位数据值。与更常见的从 0 到 9 计数的 Base10 不同,Base16 记数法从 0 到 F 计数,其中 F 表示 15 的 Base10 值(0 到 15 产生 16 个数据值)。

Java 中的十六进制值总是以 0 和 x 开头,所以白色的 24 位颜色值应该是这样的:0xFFFFFF。这个十六进制颜色值代表 Java 的颜色。白色常数,不使用 alpha 通道。白色的 32 位颜色值看起来像 0xFFFFFFFF,alpha 通道数据完全不透明。带有透明 alpha 通道的白色,可能根本不是白色,而是“透明的”,使用十六进制编码如下:0x00FFFFFF。我在 Java 代码中一般用 0x00000000 来表示一个清晰(透明)的 alpha+颜色值。

24 位十六进制表示中的每个槽代表一个 16 进制值,因此要获得每种 RGB 颜色所需的 256 个值需要 2 个槽,因为 16 乘以 16 等于 256。因此,要使用十六进制表示法表示 24 位图像,我们需要在 0x 后面有六个槽来保存这六个十六进制数据值(每个数据对表示 256 个级别的值)。如果乘以 16×16×16×16×16×16,您应该得到 16,777,216 种颜色,这些颜色可以通过使用 24 位来寻址,也称为真彩色数字图像数据。

十六进制数据槽以下列格式表示 RGB 值:0xRRGGBB。对于 Java 常量颜色。白色,十六进制颜色数据值表示中的所有红色、绿色和蓝色通道都处于全(最大颜色值)亮度设置。如果你把所有这些颜色相加,你将得到白光。

黄色表示红色和绿色通道打开,蓝色通道关闭,因此颜色的十六进制表示法。因此,黄色将为 0xFFFF00,其中红色和绿色通道槽完全打开(FF,或 255 Base10 数据值),蓝色通道槽完全关闭(00,或零值)。

ARGB 值的八个十六进制数据槽将保存以下格式的数据:0xAARRGGBB。因此,对于颜色。白色,十六进制颜色数据值表示中的所有阿尔法、红色、绿色和蓝色通道将处于它们的最大亮度(或不透明度),并且阿尔法通道是完全不透明的,即不透明的,如 FF 值所表示的。因此,颜色为 32 位十六进制值。白色常数将是 0xFFFFFFFF。

100%透明的 alpha 通道可以由设置为零的 alpha 槽来表示,从而创建“清晰”的图像。因此,您可以使用 0x00000000 和 0x00FFFFFF 之间的任何数据值来表示透明图像像素值。重要的是要注意,如果 alpha 通道值等于此完全透明度级别,那么将包含在其他六个(RGB)十六进制数据值槽中的 16,777,216 个颜色值将无关紧要,因为该像素将被评估为不存在,因为它是透明的,因此不会在最终图像或动画合成图像中合成,所以它的颜色是没有意义的(根本无关紧要)。

数字图像对象遮罩:使用 Alpha 通道合成游戏精灵

alpha 通道在游戏设计中的主要应用之一是遮蔽图像或动画(图像系列)的区域,以便它可以在游戏图像合成场景中用作游戏精灵。蒙版是从数字图像中“剪切”出主题的过程,以便可以使用 alpha 通道透明度值将主题放在自己的图层上。这是使用数字成像软件包完成的,如图 2-2 所示。

数字图像合成软件包,如 Photoshop 或 GIMP 功能工具,用于遮罩和图像合成。如果不进行有效的遮罩,就无法进行有效的图像合成,因此对于希望将图形元素(如图像精灵和精灵动画)集成到游戏设计中的游戏设计师来说,这是一个需要掌握的重要领域。数字图像蒙版技术已经存在很长时间了!

可以使用专业的蓝屏(或绿屏)背景以及可以自动提取精确颜色值来创建蒙版的计算机软件,自动为您完成蒙版。这个遮罩被转换成 alpha 通道(透明度)信息(数据)。还可以通过使用数字图像软件、通过结合各种锐化和模糊算法使用算法选择工具之一来手动进行遮罩。

在本书的过程中,我们将会使用通用的开源软件包,比如 GIMP,来学习很多关于这个工作过程的知识。掩蔽可能是一个复杂的工作过程,完全掌握这个过程可能需要跨越几个章节,而不是试图将所有内容放入书中的一个章节(这一章)。本章旨在向您展示我们在书中所承担的工作流程的基础知识。

遮罩过程的一个关键考虑因素是在被遮罩的对象(主题)周围获得平滑、清晰的边缘。这是为了当你将一个被遮罩的物体(在本书中,它将是一个游戏精灵)放入(覆盖)新的背景图像中时,它将看起来像是在第一个地方被拍摄的一样(就像它在视频中一样)。

成功做到这一点的关键在于像素选择工作过程,这涉及到使用数字图像软件选择工具,如 GIMP 中的剪刀工具或 Photoshop 中的魔棒工具。这些必须以正确的方式(顺序)使用才能完全有效。使用正确的选择工作流程至关重要!

例如,如果您想要遮罩的对象周围有统一颜色的区域(可能您是对着蓝屏拍摄的),您将使用具有适当阈值设置的魔棒工具来选择除对象之外的所有内容。然后反转选择,这将为您提供包含该对象的选择集。通常,正确的工作流程包括逆向处理某些事情。其他选择工具包含复杂的算法,可以查看像素之间的颜色变化。这些对于边缘检测是有用的,我们可以将其用于其他选择方法。

平滑数字图像合成:使用抗锯齿平滑图像边缘

抗锯齿是一种流行的数字图像合成技术,其中数字图像中位于不同颜色的两个区域之间的边缘上的两种相邻颜色沿着该边缘混合在一起。这将有助于在缩小图像时使边缘看起来更平滑(更少锯齿)。这样做是为了“欺骗”观众的眼睛看到更平滑的边缘,并消除所谓的图像锯齿。抗锯齿通过使用平均颜色值提供了令人印象深刻的结果,该平均颜色值仅使用需要变得更平滑的边缘上的几个彩色像素。所谓平均颜色值,我指的是某个颜色范围,它是沿着图像锯齿状边缘聚集在一起的两种颜色之间的一部分。这只需要半打左右的中间色。我创造了一个例子来展示我所说的。参见图 2-3 。

A336284_1_En_2_Fig3_HTML.jpg

图 2-3。

A red circle composited on a yellow background (left) and a zoomed-in view (right) showing anti-aliasing

正如你所看到的,我在一个图层上创建了一个清晰的红色圆圈,在背景图层上叠加了一个黄色填充。我放大了红色圆圈形状的边缘,拍了另一张截图,并把它放在缩小的圆圈的右边。这显示了从黄橙色到橙色到红橙色抗锯齿颜色值的范围,正好在圆与背景相遇的边缘处的红色和黄色之间。

值得注意的是,JavaFX 引擎将使用 Java2D 软件渲染器或硬件渲染的 i3D,使用 Prism 引擎(可以使用 OpenGL 或 DirectX ),针对背景颜色和背景图像消除 2D 形状和 3D 对象的锯齿。您仍将负责正确合成,也就是说,通过有效地使用 alpha 通道为您的多层影像提供抗锯齿,这一点我们在本章前面已经了解过。

数字图像数据优化:使用压缩、索引颜色和抖动

影响数字图像压缩的因素有很多,您可以使用一些基本的技术以较小的数据占用量获得较高质量的结果。这是优化数字图像的主要目标;为您的应用(在本例中是一个游戏)获得尽可能小的数据占用空间,同时获得最高质量的视觉效果。我们将从对数据足迹影响最大的几个方面入手,研究这些方面对任何给定数字图像的数据足迹优化有何贡献。有趣的是,这些类似于我们在成像这一节到目前为止所涉及的数字成像概念的顺序。

影响最终数字图像素材文件大小的最关键因素,我喜欢称之为数据足迹,将是像素数或数字图像的分辨率。这是合乎逻辑的,因为需要存储每个像素,以及包含在其三个(24 位)或四个(32 位)通道中的颜色和 alpha 值。在保持图像清晰的同时,图像分辨率越小,生成的文件也就越小。

对于 24 位 RBG 图像或 32 位 ARGB 图像,原始(或未压缩)图像大小的计算方法是宽度乘以高度乘以 3,宽度乘以高度乘以 4。例如,未压缩的真彩色 24 位 VGA 图像将具有 640 乘以 480 乘以 3,等于 921,600 字节的原始(raw)未压缩数字图像数据。要确定此原始 VGA 图像中的千字节数,您需要将 921,600 除以 1024(千字节中的字节数),这将在真彩色 VGA 图像中得到 900 KB 的数据。

通过优化数字影像分辨率来优化原始(未压缩)图像大小非常重要。这是因为一旦图像从游戏应用文件解压缩到系统内存中,这就是它将要占用的内存量,因为图像将使用 24 位(RGB)或 32 位(ARGB)表示法逐个像素地存储在内存中。这是我在游戏开发中使用 PNG24 和 PNG32 而不是索引颜色(GIF 或 PNG8)的原因之一,因为如果操作系统要将颜色转换为 24 位颜色“空间”,那么出于质量原因,我们应该利用 24 位颜色空间,并处理(接受)稍大的应用文件大小。

图像颜色深度是压缩图像的数据足迹的第二个最重要的因素,因为图像中的像素数乘以一个(8 位)、两个(16 位)、三个(24 位)或四个(32 位)颜色数据通道。这种小文件大小是 8 位索引彩色图像仍然被广泛使用的原因,尤其是使用 GIF 图像格式。

如果用于构成图像的颜色变化不太大,索引彩色图像可以模拟真彩色图像。索引彩色影像仅使用 8 位数据(256 种颜色)来定义图像像素颜色,使用多达 256 种最佳选择颜色的调色板,而不是 3 个 RGB 颜色通道或 4 个 ARGB 颜色通道,每个通道包含 256 种颜色级别。同样,重要的是要注意,在通过压缩将 24 位图像转换为 8 位图像之后,一旦在系统内存中解压缩并转换回游戏所用的 24 位 RGB 或 ARGB 数据模型(系统内存外使用的表示),您就只能使用原始 16.8M 颜色中的潜在(最多)256 种颜色了!这就是为什么我提倡使用 PNG24 或 PNG32 图像,而不是 JavaFX 也支持的 gif 或 PNG1 (1 色)、PNG2 (4 色)、PNG4 (16 色)和 PNG8 (256 色)图像。

根据任何给定的 24 位源图像中使用的颜色数量,使用 256 种颜色来表示最初包含 16,777,216 种颜色的图像可能会导致一种称为条带的效果。这是在结果(来自压缩)256(或更少)调色板中相邻颜色之间的转换不是渐进的,因此看起来不是平滑的颜色渐变。索引的彩色图像有一个视觉校正条带的选项,称为抖动。

抖动是一种算法过程,沿着图像中任何相邻颜色之间的边缘创建点图案,以欺骗眼睛认为使用了第三种颜色。抖动将为您提供 65,536 种颜色(256x256)的最大感知量,但只有当这 256 种颜色中的每一种都与其他(不同)256 种颜色中的一种相邻时,才会出现这种情况(这是必要的)。尽管如此,您仍然可以看到创建额外颜色的潜力,并且您会对索引颜色格式在某些压缩场景中(对于某些图像)可以达到的效果感到惊讶。

让我们拍摄一张真彩色图像,如图 2-4 所示,并将其保存为 PNG5 索引彩色图像格式,向您展示这种抖动效果。需要注意的是,尽管在 Android 和 HTML5 中支持 PNG5,但在 JavaFX 中不支持,所以如果您自己做这个练习,请选择 PNG1 (2)、PNG2 (4)、PNG4 (16)或完整 PNG8 (256)颜色选项!

A336284_1_En_2_Fig4_HTML.jpg

图 2-4。

This is a true-color PNG24 image created with Autodesk 3ds Max that we are going to compress as PNG5

我们将看看奥迪 3D 图像中驾驶员侧后挡泥板上的抖动效果,因为它包含一个我们将应用此抖动效果的灰色渐变。您可以在图 2-4 中看到 24 位源数字图像。

有趣的是,8 位索引彩色图像允许使用少于 256 种的最大颜色。这通常是为了进一步减少影像的数据足迹。例如,仅使用 32 种颜色就可以获得良好效果的图像实际上是一个 5 位图像,从技术上来说,它被称为 PNG5,尽管这种格式本身通常被称为用于索引颜色使用级别的 PNG8。请记住,JavaFX 只支持 PNG4 (16 色)或 PNG8 (256 色),因此对于 Java 游戏中的这个图像,您应该使用 PNG8 或 256 色。

我将使用 Photoshop 将这张索引色 PNG5 图像(如图 2-5 所示)设置为使用 5 位颜色(32 色),以便您可以清楚地看到这种抖动效果。正如你所看到的,在图 5-4 左侧的 Photoshop 图像预览区域,抖动算法在相邻颜色之间创建点图案,以便创建额外的颜色。

A336284_1_En_2_Fig5_HTML.jpg

图 2-5。

Setting dithering to the Diffusion algorithm and 32 colors (5-bit color) with 100 percent dithering for PNG5 output

另外,请注意,您可以设置使用抖动的百分比。我经常选择 0%或 100%的设置;但是,您可以在这两个极值之间的任何位置微调抖动效果,以微调生成的文件大小,因为这些抖动点图案会引入更多数据来压缩和增加文件大小。

您还可以在抖动算法之间进行选择,因为您可能已经猜到,这些不同的抖动效果是通过使用最终与索引文件格式压缩兼容(受其支持)的抖动算法以数学方式创建的,索引文件格式压缩使用调色板来保存用于像素的颜色值。

我使用扩散抖动,这给了不规则形状的梯度一个平滑的效果,就像在汽车挡泥板上看到的那样。您也可以使用更随机的“噪波”选项,或更不随机的“图案”选项。扩散选项通常给出最好的结果,这就是为什么我在使用索引颜色时使用它(这并不经常)。

正如你所想象的,抖动会将数据模式添加到你的图像中。这些更难压缩。这是因为图像中的平滑区域(如渐变或填充区域)通常更容易被这些压缩算法压缩,而尖锐的过渡(抗锯齿边缘)或随机像素图案通常是由抖动产生的,或者可能是由例如带有不合格 CCD 的相机的“噪声”产生的。

因此,应用抖动选项总是会增加几个百分点的数据占用空间。请务必检查应用和不应用抖动(在“导出”对话框中选择)的结果文件大小,以查看这是否值得它提供的改进的视觉效果。请注意,索引彩色 PNG 图像也有一个透明度选项复选框,但 PNG8 图像中使用的 alpha 通道只有 1 位(开/关),而不是 PNG32 中的 8 位。

到目前为止,我们了解到的最后一个可以增加图像数据量的概念是添加 alpha 通道来定义合成的透明度。这是因为添加 alpha 通道会给正在压缩的图像添加另一个 8 位颜色通道(更准确地说,是透明度或 alpha 通道)。如果您需要一个 alpha 通道来定义图像的透明度,很可能是为了支持未来的合成需求,例如将图像用作游戏精灵,在这方面没有太多的选择,所以包括 alpha 通道。

如果您的 alpha 通道包含全零(即,使用全黑填充颜色,这会将您的图像定义为完全透明),或者如果您的 alpha 通道包含全白填充颜色,会将您的图像定义为完全不透明或背景板),您实际上(在实际使用中)定义了一个不包含任何有用的 alpha 数据值的 alpha 通道。因此,需要移除未使用的 alpha 通道,并且需要将不透明图像定义为 PNG24 而不是 PNG32,以节省数据占用空间。

最后,大多数用于遮罩数字图像 RGB 层中的对象的 alpha 通道应该能够很好地压缩。这是因为它们主要是白色(不透明)和黑色(透明)的区域,沿着两种颜色之间的边缘有一些中等灰度值,以消除蒙版的锯齿(见图 2-2 )。这些灰色区域包含 alpha 通道中的消除锯齿透明度值,这将始终为您提供图像的 RGB 图层中的对象与任何背景颜色或可能在它后面使用的背景图像之间的平滑边缘过渡。实质上,alpha 通道中的反走样为您提供了 alpha 通道所服务的对象的实时合成,因为您可以将视频放在它的后面,在背景板中,alpha 反走样将实时保证视频的每一帧上具有不同边缘颜色混合的平滑边缘结果。

其原因是,由于 alpha 通道图像蒙版使用 8 位透明度渐变,范围从白色到黑色,并定义透明度级别而不是颜色,这应该被视为每像素混合,或不透明度强度值。因此,alpha 通道中包含的遮罩中每个对象的边缘上的中等灰度值将用于基本上平均对象边缘和任何目标背景的颜色,无论背景板可能包含什么颜色值、图像资源、插图资源、动画资源或视频资源。

这为可能使用的任何目标背景提供了实时抗锯齿,即使您的对象是静态对象,因为 alpha 通道提供的抗锯齿甚至可以使用动画背景。

数字视频或动画:帧、帧速率、循环、方向

有趣的是,我们刚刚谈到的数字图像的所有概念同样适用于数字视频和 2D 动画,因为这两种第四维(基于时间的)新媒体格式都使用数字图像作为其内容的基础。数字视频和 2D 动画一样,通过引入一种叫做帧的东西,将数字成像扩展到了时间的第四维度。数字视频和动画由有序的帧序列组成,这些帧随着时间的推移快速显示,以产生运动的幻觉,使图像栩栩如生。

术语帧来自电影工业,即使在今天,电影帧也是通过电影放映机以每秒 24 帧(通常缩写为 24 FPS)的帧速率放映的。这就产生了运动的错觉。由于数字视频和动画都是由包含数字图像的帧的集合组成的,所以当涉及到内存数据占用优化工作过程(对于动画素材)和数字视频文件大小数据占用优化工作过程时,以每秒帧数表示的帧速率的概念也是非常重要的。在 JavaFX 中,您将很快了解到,动画的这个属性存储在动画对象的 rate 变量中。

关于动画对象或数字视频素材中的帧的优化概念非常类似于关于图像中的像素(数字图像的分辨率)的优化概念;用的越少越好!这是因为动画或视频中使用的帧数会使所使用的系统内存和所使用的每一帧的文件大小数据量成倍增加。在数码视频中,不仅每帧(图像)的分辨率会极大地影响文件大小,在“压缩设置”对话框中指定的每秒帧数或帧速率也会影响文件大小。在本章的前面,我们了解到,如果我们将图像中的像素数量乘以其颜色通道的数量,我们将获得图像的原始数据足迹。对于动画或数字视频,我们现在将这个数字再乘以需要使用的总帧数,以便创建一个运动的幻觉。

因此,如果我们的游戏有一个动画 VGA (RGB)背景板(请记住,每帧是 900KB ),它使用五帧来创建运动的幻觉,我们使用 900KB 乘以五,或 4500KB(或 4.5MB)的系统内存来保存该动画。当然,这对于一个背景来说占用了太多的内存,这就是为什么我们将使用静态背景和精灵覆盖来在不到一兆字节的时间里达到同样的最终效果。数字视频的计算稍有不同;和数字视频一样,你有成百上千的帧。对于数字视频,您可以将原始图像数据大小乘以每秒帧数(帧速率),这是数字视频设置的回放速率(此帧速率值在压缩过程中指定),然后将结果乘以视频文件中包含的内容持续时间的总秒数。

继续前面使用的 VGA 示例,您现在知道一个 24 位 VGA 映像有 900KB。这使得下一步的计算变得非常容易。数字视频通常以 30 FPS 的速度运行,因此在屏幕上播放之前,系统内存中一秒钟的标清(SD 或 VGA)原始(未压缩)数字视频将是 30 个图像帧,每个图像帧为 900KB,总内存数据量为 27000KB,约为 27MB!

您可以看到为什么拥有 MPEG-4 H.264 AVC 格式这样的数字视频压缩文件格式是极其重要的,这种格式可以显著压缩数字视频所产生的大量原始数据。

JavaFX 多媒体包使用最令人印象深刻的视频压缩编解码器之一(codec 代表 COde-DECode ),它在 HTML5 和 Android 中也受支持:前面提到的 MPEG-4 H.264 高级视频编解码器(AVC)。这种跨越当今三大“开放平台”(Java、HTML5 和 Android)的“交叉开放平台支持”对于开发人员素材优化来说极其方便,因为一个数字视频素材可以跨 Java、JavaFX、HTML5 和 Android 应用使用。如果没有安装 H.264,JavaFX 引擎中还包含一个“本地”数字视频编解码器,称为 VP6。接下来,在深入探讨数字音频之前,我将介绍数字视频素材压缩和数据占用优化的基础知识。然后,在下一章,我们将进入 3D 的复杂性,这样你就对游戏中的新媒体元素有了一个完整的基础理解。

数字视频压缩概念:比特率、数据流、标清、高清和 UHD

让我们从商业视频中使用的主要或标准分辨率开始。这些也恰好是常见的消费电子设备屏幕分辨率,可能是因为如果显示屏像素分辨率与屏幕上“全屏”播放的视频像素分辨率匹配,将会出现零“缩放”,这可能会导致缩放伪像。在 HDTV 或高清出现之前,视频被称为标清(SD ),使用 480 像素的标准像素垂直分辨率。VGA 是标清分辨率,720 x480 可以称为宽标清分辨率。高清(HD)视频有两种分辨率,1280×720,我称之为伪高清,1920×1080,业界称之为真高清。这两种高清分辨率都具有 16:9 的宽高比,用于电视机和 iTV 电视机、智能手机、平板电脑、电子书阅读器和游戏机。现在还有一款 UHD 分辨率为 4096×2160 像素的产品。IMAX 分辨率为 4096 x 4096,因此 UHD 在水平方向(即 x 轴方向)拥有 IMAX 分辨率,这非常令人印象深刻,因为消费者现在只需 1000 美元就可以在他们的客厅拥有 IMAX!

视频流是一个比分辨率更复杂的概念,因为它涉及到在大范围内播放视频数据,例如 Java 游戏应用和远程视频数据服务器之间的视频数据,这些服务器将保存您潜在的大量数字视频素材。流媒体是复杂的,因为运行 Java 游戏应用的设备将与远程数据服务器实时通信,在视频播放时接收视频数据包!这就是为什么它被称为流;因为视频通过互联网从视频服务器流入硬件设备。MPEG-4 H.264 AVC 格式编解码器(编码器-解码器对)支持视频流。

本节中我们需要介绍的最后一个概念是比特率的概念。比特率是视频压缩过程中使用的关键设置;比特率代表您的目标带宽或数据管道大小,每秒钟能够容纳一定数量的比特流。您的比特率设置还应该考虑任何给定的支持 Java 的设备中存在的 CPU 处理能力,这使得您的数字视频的数据优化更具挑战性。幸运的是,如今大多数设备都配备了双核或四核 CPU!

这样做的原因是,一旦这些位通过数据管道传输,它们也需要被处理并显示在设备屏幕上。因此,数字视频素材的比特率不仅需要针对带宽进行优化,还需要考虑 CPU 处理能力的变化。一些单核 CPU 可能无法在不丢帧的情况下解码高分辨率、高比特率的数字视频素材。如果你的目标是旧的或更便宜的消费电子设备,比如第三世界国家使用的设备,请确保优化低比特率视频素材。

数字视频数据足迹优化:视频编解码器的重要设置

正如您在上一节中了解到的,数字视频资源是使用称为编解码器的软件实用程序进行压缩的。视频编解码器有两个“方面”:一方面编码视频数据流,另一方面解码视频数据流。视频解码器将成为使用它的操作系统、平台(JavaFX)或浏览器的一部分。解码器主要针对速度进行了优化,因为回放的平滑度是一个关键问题,编码器也进行了优化,以减少其生成的数字视频素材的数据占用量。因此,编码过程可能需要更长的时间,这取决于工作站包含多少个处理核心。大多数数字视频内容制作工作站应该支持八个处理器内核,比如我的 64 位 AMD 八核工作站。

编解码器(编码器端)类似于插件,因为它们可以安装到不同的数字视频编辑软件包中,以便能够对不同的数字视频资源文件格式进行编码。由于 Java 和 JavaFX 9 本身支持 ON2 VP6 格式和 MPEG4,如果安装了它,您需要确保您使用的数字视频软件包支持使用这些数字视频文件格式之一对数字视频进行编码。

需要注意的是,不止一家软件制造商生产 MPEG4 编码软件,因此就编码速度和文件大小而言,将会有不同的 MPEG4 H.264 AVC 编解码器产生不同(更好或更差)的结果。我更喜欢 MainConcept H.264 编解码器。一个专业的解决方案是 Sorenson Squeeze Pro,它支持 ON2 VP6,如果你想制作专业的数字视频,我强烈推荐你使用它。

我将在图 2-6 中向您展示 Sorenson Squeeze Pro 的数字视频压缩设置(预设)对话框,然后我们将在本章本节的剩余部分讨论一些重要的设置。

A336284_1_En_2_Fig6_HTML.jpg

图 2-6。

Digital video compression Presets dialog for the Sorenson Squeeze Pro digital video compression utility

还有一个名为 EditShare LightWorks 14 的开源解决方案,计划在 2018 年前原生支持开源编解码器的输出。目前,我将不得不为这本书使用 Squeeze Pro 11,直到 2018 年的某个时候 EditShare LightWorks 14 添加对 JavaFX(以及 HTML5 和 Android)的编解码器支持。优化数字视频数据文件大小(设置压缩设置)时,有大量变量会直接影响数字视频数据的占用空间。我将按照它们影响视频文件大小的顺序来讨论这些问题,从影响最大到影响最小,以便您知道调整哪些参数来获得您想要的结果。

与数字图像压缩一样,视频每帧的分辨率或像素数是开始优化过程的最佳位置。如果您的用户使用 1024x640 或 1280x720 的智能手机、电子阅读器或平板电脑,那么您不需要使用 1920 x 1080 分辨率的真高清来获得数字视频素材的良好视觉效果。有了今天的超精细密度(小点距)显示器,你可以将一个 1280 的视频放大 33%,而且看起来相当不错。这种情况的例外可能是高清或 UHD(通常称为 4K iTV)游戏,目标是 iTV 设置;对于这些 65 到 96 英寸的大屏幕场景,您可能希望使用 1920x1080 分辨率的行业标准真高清。

假设数字视频本身的实际秒数无法缩短,下一级优化将是每秒视频使用的帧数(或 FPS)。这就是所谓的帧速率,不要将视频标准帧速率设置为 30 FPS,如图 2-6 的左上角所示,设置为 1:1,或者每个源帧压缩一帧,而是考虑使用 24 FPS 的电影标准帧速率,甚至 20 FPS 的多媒体标准帧速率。您甚至可以使用 15 FPS 的帧速率,这是视频标准 30 FPS 的一半,这相当于图 2-6 所示的帧速率字段的 1:2 设置,这取决于内容内的移动量(和速度)。请注意,15 FPS 的数据量是 30 FPS 的一半(编码的数据量减少了 100%)。对于某些视频内容,这将回放(看起来)与 30 FPS 的内容相同。测试的唯一方法是尝试不同的帧速率设置,并在视频优化(编码)过程中观察结果。

获得较小数据占用空间的下一个最佳设置是您为编解码器设置的比特率。这显示在图 2-6 的左侧,用红色圈出。比特率等同于应用的压缩量,因此设定了数字视频数据的质量水平。值得注意的是,您可以简单地使用 30 FPS、1920 分辨率的高清视频,并指定低比特率上限。如果您这样做,结果看起来就不会像您第一次尝试使用较低的帧速率和(或)较低的分辨率进行压缩,同时使用较高(质量)的比特率设置时那样专业。这方面没有固定的经验法则,因为每个数字视频素材都包含 100%不同且唯一的数据(从编解码器算法的角度来看)。

获得更小数据足迹的第二个最有效设置是关键帧的数量,编解码器使用它对您的数字视频资源进行采样。该设置在图 2-6 的右侧用红色圈出。视频编解码器通过查看每一帧,然后对接下来几帧中的任何像素变化进行编码来应用压缩,因此编解码器算法不必对视频数据流中的每一帧进行编码。这就是为什么会说话的头部视频比每个像素都在每帧上移动的视频(如摄像机平移的视频)编码更好。

关键帧是编解码器中的设置,它强制编解码器不时对视频数据资源进行新的采样。关键帧通常有一个自动设置,允许编解码器决定采样多少个关键帧,还有一个手动设置,允许您指定关键帧采样的频率,通常是每秒特定次数或整个视频时间长度(总帧数)的特定次数。

一些编解码器设置对话框具有质量或清晰度设置(滑块),用于控制压缩前应用到视频帧的模糊量。如果您不知道这个技巧,对您的图像或视频应用轻微的模糊(这通常是不可取的)可以实现更好的压缩,因为图像中的尖锐过渡(尖锐边缘)比柔和过渡更难编码(这需要更多的数据来重现)。也就是说,我会将质量(或清晰度)滑块保持在 85%和 100%的质量水平之间,然后尝试使用我们在此讨论的其他变量来减少数据占用空间,例如降低分辨率、帧速率或比特率。

最终,对于任何给定的数字视频数据素材,您都需要对许多变量进行微调,以实现最佳的数据占用优化。请务必记住,每个数字视频资源与数字视频编解码器“看起来”不同(数学上)。由于这个原因,不可能有可以被开发来实现任何给定压缩结果的标准设置。也就是说,随着时间的推移,调整各种设置的经验最终会让您对需要更改的各种设置有更好的感觉,以获得想要的最终结果。

数字音频概念:振幅、频率、样本、波形

你们这些音响发烧友已经知道声音是通过在空气中发送声波脉冲而产生的。数字音频很复杂;部分复杂性来自于需要将使用扬声器纸盆创建的“模拟”音频技术与数字音频编解码器连接起来。模拟扬声器通过脉冲产生声波。我们的耳朵以完全相反的方式接收模拟音频,捕捉和接收那些空气脉冲或不同波长的振动,然后将它们转换回我们大脑可以处理的“数据”。这就是我们“听到”声波的方式;然后,我们的大脑将不同的音频声波频率解释为不同的音符或音调。

声波根据每个声波的频率产生不同的音调。宽的或不常见的(长的)波产生低的(低音)音调,而更频繁的(短的)波长产生更高的(高音)音调。有趣的是,不同频率的光会产生不同的颜色,所以模拟声音(音频)和模拟光(颜色)之间有密切的相关性。您很快就会看到,数字图像(和视频)与数字音频之间还有许多其他相似之处,它们将贯穿到您的数字新媒体内容制作中。

声波的音量由声波的振幅或波的高度(或大小)决定。因此,如果你在 2D 观察,声波的频率等于声波在 x 轴上的间距,振幅等于声波在 y 轴上的高度。

声波可以独特地成形,允许声波“搭载”各种音效。“纯”或基线类型的声波称为正弦波,这是您在高中三角学中使用正弦、余弦和正切数学函数学到的。熟悉音频合成的人都知道,在声音设计中还可以使用其他类型的声波,例如看起来像锯子边缘的锯齿波(因此得名)或仅使用直角整形的脉冲波,这种波可以产生即时的开/关声音,并转化为合成数字音频的脉冲(或突发)。

甚至在声音设计中使用随机波形(如噪声)来获得“尖锐”的声音效果。通过使用最近获得的关于数据足迹优化的知识,您可能已经确定,声波(以及一般的新媒体数据)中出现的“混乱”或噪声越多,编解码器就越难压缩它们。因此,由于数据中的混乱,更复杂的声波将导致更大的数字音频文件。

将模拟音频转换为数字音频数据:采样、精度和高清音频

将模拟音频(声波)转换为数字音频数据的过程称为采样。如果你在音乐行业工作,你可能听说过一种叫做采样器的键盘(甚至是架装式设备)。采样是将模拟音频波分割成片段的过程,以便您可以使用数字音频格式将波形存储为数字音频数据。这将一个无限精确的模拟声波转换成离散的数字数据,也就是 0 和 1。使用的 0 和 1 越多,无限精确(原始)模拟声波的再现就越精确。被采样的音频声波的每个数字段被称为样本,因为它在那个精确的时间点对声波进行采样。样本精度决定了用于再现模拟声波的 0 和 1 的数量,因此样本的精度取决于用于定义每个波切片高度的数据量。图 2-7 显示了我使用 Audacity 采样的一个按钮音效,使用 32 位浮点采样精度和 48 kHz 采样速率,我们将在接下来讨论。

A336284_1_En_2_Fig7_HTML.jpg

图 2-7。

Stereo sample of button sound effect in Audacity using 32-bit float sample accuracy and 48kHz sample rate

就像数字成像一样,这种采样精度被称为分辨率,或者更准确地说(没有双关语),采样分辨率。采样分辨率通常用 8 位、12 位、16 位、24 位或 32 位分辨率来定义。Java 游戏大多利用 8 位分辨率来实现效果,如清晰度不太重要的爆炸,使用 12 位分辨率来实现清晰的语音对话和更重要的音频效果素材,并可能使用 CD 质量的 16 位分辨率来实现背景音乐或需要展现原始音频质量的音频元素。

在数字成像和数字视频中,分辨率以像素数来量化,而在数字音频中,分辨率以使用多少位数据来定义每个模拟音频样本来量化。就像数字成像一样(像素越多,质量越好),样本分辨率越高,声音再现越好。因此,更高的采样分辨率,使用更多的数据来再现给定的声波样本,将产生更高的音频回放质量,代价是更大的数据足迹。这就是为什么 16 位音频(通常称为 CD 音质音频)听起来比 8 位音频更好的原因。根据所涉及的音频,12 位可能是一个很好的折衷方案。

在数字音频领域,消费电子行业有一种新型 24 位音频样本,称为高清音频。高清数字音频广播电台使用 24 位采样分辨率。每个音频样本或声波片段可能包含高达 16,777,216 位的声波采样分辨率,尽管很少使用所有这些位。

一些新的硬件设备现在支持高清音频,例如你在广告中看到的智能手机,具有“高清质量”的音频。这意味着他们有 24 位音频硬件。如今,个人电脑和笔记本电脑以及游戏机和独立电视也标配了 24 位音频播放硬件,因此支持高质量的音频。

需要注意的是,对于 Java 9 游戏来说,HD 音频可能不是必需的,除非您的游戏是面向音乐的,并且使用了高质量的音乐,在这种情况下,您可以通过 WAVE 文件格式使用 HD 音频样本。

除了数字音频采样分辨率,我们还有数字音频采样频率。这是在一秒钟的采样时间帧内,以特定采样分辨率采集的样本数量。在数字图像编辑中,采样频率类似于数字图像中包含的颜色数量。采样频率也可以称为采样率。您可能对术语 CD 音质音频很熟悉,它被定义为使用 16 位采样分辨率和 44.1 kHz 采样速率。这需要 44,100 个样本,每个样本包含 16 位的样本分辨率,即这 44,100 个样本中包含 65,536 位的音频数据。通过将采样比特率乘以采样频率,再乘以音频片段中的秒数,可以计算出音频文件中的原始数据。你可以看到这可能是一个巨大的数字!音频编解码器在优化采样声波数据方面非常出色,数据占用空间非常小,音质损失非常小。

因此,我们在数字成像和数字视频中的权衡同样存在于数字音频中。我们包含的数据越多,我们获得的结果质量就越高!然而,这总是以更大的数据占用为代价。在视觉媒体中,数据足迹的数量是使用颜色深度、像素来定义的,在数字视频和动画的情况下,还使用帧来定义。在数字音频媒体中,它由采样分辨率和采样速率共同定义。数字音频行业目前最常见的采样率包括 8kHz、11.25 kHz、22.5kHz、32kHz、44.1kHz、48kHz、96kHz、192kHz,甚至 384kHz。

我们将在游戏中使用较低的采样率,如 8kHz、22kHz 和 32kHz,经过精心优化,这些采样率可以产生高质量的音效和街机音乐。这些速率对于采样任何“基于语音的”数字音频也是最佳的,例如电影对白或电子书旁白音轨。较高的采样率允许音频再现展现剧院音质,但大多数游戏并不需要。

数字音频素材回放:强制音频回放与流式音频

就像数字视频数据一样,数字音频数据可以被捕获,保存在应用分发文件中(在 Java 中,这是一个 JAR 文件);或者,可以使用远程数据服务器流式传输数字音频。与数字视频类似,流式数字音频数据的优势在于它可以减少应用文件的数据占用空间,就像流式数字视频数据一样。缺点是可靠性。许多相同的概念同样适用于音频和视频。流式音频将节省数据空间,因为您不必在 JAR 文件中包含所有繁重的新媒体数字音频数据,所以如果您计划编写自动点唱机应用,您可能需要考虑流式传输您的数字音频数据。否则,请尝试优化您的数字音频数据,以便您可以将它(受控)包含在 JAR 文件中。这样,当应用的用户需要时,它总是可用的!

流式数字音频的缺点是,如果用户的连接(或音频数据服务器)中断,您的数字音频文件可能无法始终呈现给您的最终用户使用您的游戏应用播放和收听。数字音频数据的可靠性和可用性是在“流式音频数据与捕获式数字音频数据”权衡的另一方面要考虑的关键因素。同样的权衡也适用于数字视频素材。

就像数字视频一样,流式传输数字音频的一个主要概念是数字音频数据的比特率。正如您在上一节中了解到的,该比特率是在压缩过程中定义的。与数字视频一样,需要支持较低比特率带宽的数字音频文件将对音频数据进行更多压缩,这将导致质量下降。这些将在更多的设备上更平滑地传输(回放),因为可以更容易地快速传输和处理更少的位。

JavaFX 中的数字音频素材:数字音频编解码器和数据格式支持

JavaFX 中的数字音频编解码器比数字视频编解码器多得多,因为只有两种视频编解码器,即 MPEG-4 H.264 AVC 或 ON2 VP6。JavaFX 音频支持包括 MP3 (MPEG3)文件、Windows Wave(脉冲编码调制[PCM]音频)WAV 文件、MP4(或 M4A) MPEG-4 AAC 音频和 Apple 的 AIFF (PCM)文件格式。JavaFX 支持的最常见的音频格式是 MP3 数字音频文件格式。MP3 数字音频文件格式受欢迎的原因是因为它具有良好的压缩比和质量比,并得到广泛支持。

MP3 将是一种可接受的数字音频格式,用于 Java 游戏或物联网应用,只要您使用最佳的编码工作流程获得最高的质量水平。值得注意的是,MP3 是一种有损音频文件格式,就像 JPEG 用于数字图像一样,其中一些音频数据以及一些原始音频样本质量在压缩过程中被丢弃,以后无法恢复。

JavaFX 有两种无损音频压缩编解码器,称为 AIFF 和 WAVE。您可能熟悉这些数字音频格式,因为它们是分别用于 Apple 和 Microsoft Windows 操作系统的原始音频格式。这些文件使用 PCM 音频,这是无损的,在这种情况下,因为没有应用任何压缩!脉码调制指的是它所保持的数据格式。

PCM 音频通常用于 CD-ROM 内容以及电话应用。这是因为 PCM Wave 音频是一种未压缩的数字音频格式,它没有应用于数据流的 CPU 密集型压缩算法;因此,解码(CPU 数据处理)对于电话设备或 CD 播放器来说不是问题。

因此,当我们开始将数字音频素材压缩成这些不同的文件格式时,我们将使用 PCM 作为我们的基准文件格式。我们不仅可以看到 PCM (Wave)和 MP3 或 MP4 音频压缩结果之间的差异,从而了解我们对 JAR 文件进行了多少数据占用优化,更重要的是,我们可以看到我们的采样分辨率和采样频率优化将如何影响用于游戏音频效果的系统内存。即使我们使用 MP3 或 MP4 格式,在音频素材可以与AudioClip类一起使用之前,它仍然必须被解压缩到内存中,并在 Java 游戏中用作声音效果。

由于 Wave 或 AIFF 文件不会有任何质量损失,因为也不需要解压缩,这种脉冲编码调制数据可以直接从 JAR 文件放入系统内存!这使得 PCM audio 非常适合持续时间短(0.1 到 1 秒)的游戏音效,并且可以高度优化,使用 8 位和 12 位采样分辨率以及 8kHz、22kHz 或 32kHz 采样频率。最终,对于任何给定的数字音频数据,要找出 JavaFX 支持的哪种音频格式具有最佳的数字音频压缩结果,唯一真正的方法是用我们知道受支持且高效的主要编解码器对您的数字音频进行编码。当我们在游戏中添加音频时,我们将在第二十一章中经历这一工作过程,并将观察使用相同源音频样本的不同格式之间的相对数据足迹结果。然后,我们将听取音频播放质量,以便我们可以作出我们的最终质量文件大小的决定。这是开发 JavaFX 数字音频素材所需的工作流程,可用于专业 Java 游戏开发工作流程。

JavaFX 还支持流行的 MPEG-4 高级音频编码(AAC)编解码器。这种数字音频数据可以包含在 MPEG4 容器(.mp4.m4a.m4v)或文件扩展名中,并且都可以使用所有操作系统来回放。值得注意的是,JavaFX 不包含 MPEG-4 解码器,而是支持所谓的“多媒体容器”。这意味着它使用主机操作系统的 MPEG-4 解码器进行解码。

出于这个原因,并且因为在线听力研究已经得出结论,MP3 比 MP4 格式具有更好的质量(对于音乐),我们将使用 MP3 音频文件格式用于更长形式的音频(游戏背景音乐循环),我们将通过MediaMediaPlayer类来使用。我们将使用 PCM Wave 音频格式的短格式音频(游戏声音效果,如枪声、铃声、叫喊声、咕哝声、笑声、欢呼声等类似的一秒钟或更短的数字音频素材),我们将通过 JavaFX 慷慨提供的AudioClip数字音频排序引擎(类)使用它。

数字音频优化:从 CD 质量的音频开始,向后工作

优化您的数字音频素材以便在市场上最广泛的硬件设备上播放,将比优化您的数字视频或数字图像(以及动画)更容易。这是因为目标屏幕分辨率和显示器宽高比的差异要比硬件设备之间的数字音频播放硬件支持的差异大得多(具有 24 位高清音频播放硬件兼容性的新硬件可能例外)。所有硬件都可以很好地播放数字音频素材,因此音频优化是一个“一个音频素材影响所有设备”的场景,而对于视觉(视频、图像、动画)部分,您可以看到从 4096x2160 像素(4K iTV 电视机)到 320x320 像素(翻盖手机和智能手表)的显示屏。

重要的是要记住,用户的耳朵不能感知数字音频的质量差异,而用户的眼睛可以感知数字图像、2D 动画或数字视频的质量差异。一般来说,在所有硬件设备中,有三个主要的数字音频支持“最佳点”,您应该将它们作为 Java 游戏音频支持的目标。

通过使用 8 kHZ、11.25 kHz 或 22.5 kHz 采样速率以及 8 位或 12 位采样分辨率,较低质量的音频(如短旁白轨道、人物惊呼或短时声音效果)可以获得非常高的质量。中等质量的音频,如长旁白轨道、长持续时间声音效果、循环背景(称为:环境)音频等,可以通过使用 22.5 kHz 或 32 kHz 采样速率以及 12 位或 16 位采样分辨率来达到非常高的质量水平。

高质量的音频素材,如音乐,应该进行优化,以接近 CD 质量的音频,并使用 32 kHz 或 44.1 kHz 的采样速率,以及 16 位数据采样分辨率。对于这种音频频谱的超高端高清质量音频,可以使用 48 kHz 采样速率和 24 位数字音频数据采样分辨率。还有一个未命名的“中间某处”高端音频规范,使用 48 kHz 采样率和 16 位数据采样分辨率,这恰好是杜比 THX 用于其高端音频体验技术的标准。这是在《星球大战》的电影院里使用的。

最终,它归结为从数字音频数据足迹优化工作过程中出现的质量到文件大小的结果,这可以产生一些惊人的结果。因此,在所有这些硬件设备上优化您的数字音频素材的初始工作流程是创建 44.1 kHz 或 48 kHz 的“基线”16 位素材,然后使用 JavaFX 支持的不同格式优化(压缩)它们。一旦工作流程完成,您就可以看到哪些数字音频素材提供了最小的数据占用空间,以及最高质量的数字音频回放。之后,您可以将 48 kHz 或 44.1 kHz 数据降至 32 kHz,先用 16 位分辨率保存,再用 12 位分辨率保存。之后,重新打开原始 48 kHz 数据,下采样至 22.5 kHz 采样频率,并使用 16 位或 12 位分辨率导出该数据,等等。我们将在本书后面的第二十一章中执行这个工作过程,这样你就可以体验音频工作过程。

您将使用开源的 Audacity 2.1.3 数字音频编辑和工程软件包来执行这个工作过程。您在第一章下载并安装了这个软件包,理想情况下,您安装了所有这些免费的 VST、奈奎斯特、LV2 和 LADSPA 插件,以及用于 AC3、AMR-NB、M4A 和 WMA 音频格式的 LAME MPEG3 编码器和 FFMPEG 编码器。如果您还没有这样做的话,您会希望这样做,这样您就拥有了适用于您的 Java 工作站的绝对最强大的数字音频编辑和工程套件。

如果你还没有这样做,也不想现在做,在我们学习如何使用AudioClipMediaMediaPlayer类将数字音频元素添加到你的 Java 9 游戏环境的那一章,我会告诉你如何做。这真的很简单。您所要做的就是下载这些插件,并将它们放在主 Audacity 软件安装文件夹下的正确插件文件夹中,这样添加效果就不难了。

摘要

在第二章中,我们仔细研究了一些更重要的 2D 新媒体概念,我们将在我们的 pro Java 游戏和物联网开发工作流程中使用这些概念,以便您对 JavaFX 8 support 已将其添加到您的 Java 9 环境中的这些 2D 多媒体素材有一个坚实的基础知识。请注意,Java 和 JavaFX 版本是不同步的,例如 Java 6 使用 JavaFX 1.x,Java 7 使用 JavaFX 2.x。它们更接近于同步版本,因为 Java 8 使用 JavaFX 8,但是 Java 9 的重点是模块化语言,而不是 JavaFX 9,所以目前 Java 9 平台可能使用 JavaFX 8 技术。

我首先介绍了最重要和最基础的 2D 新媒体概念,因为它们与 Java FX 8 有关,Java FX 8 是 Java 8 和 Java 9 的新媒体引擎,也与 Android、iOS、Windows、Linux OS 和 HTML5 开发有关。对于游戏开发人员来说,理解新媒体概念与 Java 9 和 JavaFX 编码实践一样重要,因为新媒体使您的游戏更加身临其境、引人入胜和视觉刺激。如果你想深入了解这些新媒体话题,我在 www.apress.com 有一系列新媒体“基础”书籍。每本书都特别关注一个新媒体领域(音频、视频、VFX、插图、绘画等)。).

由于 3D 要复杂得多,我把这些基础信息留到了第三章;我想把这本书的章节保持在合理的长度,以便最佳地消化所有这些技术信息。3D 和 i3D 新媒体素材明显不同于 2D 和 i2D 新媒体素材,因为这些素材为已经很复杂的 scratch 新媒体内容创作增加了全新的维度、深度数据和 z 轴。这有助于将基本的 2D(面积)数学转化为向量或矩阵代数(学士水平到硕士或博士水平);因此,3D 比 2D 复杂一个数量级。

我们还研究了一些相当先进的数字成像概念、JavaFX 支持的格式、技术和数据占用优化。这些信息将允许您从您在专业 Java 游戏或物联网应用中使用的每个像素中提取最大效用和性能。

您了解了所有关于像素、分辨率以及纵横比如何定义图像、动画或视频的形状的知识,还了解了颜色深度、图层、通道以及 alpha 通道透明度如何允许您实现图像合成管道。您学习了如何使用十六进制表示法定义颜色以及 alpha 通道透明度值。我们研究了先进的数字成像概念,如遮罩、抖动、抗锯齿、混合和转换模式。

接下来,我们研究了时间的第四维度,并了解了如何将帧、帧速率和比特率与我们在“数字成像”一节中学到的概念结合起来,为我们的新媒体素材添加运动,从而创建 2D 动画或数字视频。我们研究了不同的格式和编码它们的编解码器,并研究了一些将编辑和编码数字视频的软件包,如 DaVinci Resolve、Lightworks 和 Sorenson Squeeze。

最后,我们看了数字音频,了解了采样频率和采样分辨率,JavaFX 支持的 MP3 和 PCM 音频格式,以及如何使用这些编解码器来优化数字音频,以使用最少的系统内存。我们研究了如何为我们的 pro Java 9 游戏和物联网应用获得最小的数据占用空间,同时仍然将高质量的产品放入游戏和应用商店。

在下一章中,我们将了解 3D 和 i3D 的概念、原理、格式、优化和工作流程,我们将在您的 pro Java 9 游戏和物联网应用中使用它们。当我们查看诸如 3D 角色建模、骨骼、装配和动画、粒子系统、物理系统(物理力)、流体动力学、视觉效果(VFX)和特殊效果(SFX)、纹理映射、着色器和 UVW 映射坐标等内容时,您将感受到 3D 相对于 2D 新媒体素材有多复杂。

三、高级 3D 内容渲染:3D 素材概念和原则

现在,您已经了解了 2D 新媒体开源内容开发软件包(GIMP、Lightworks、Audacity 和 DaVinci Resolve)所基于的 2D(光栅和音频)内容开发概念和原则,我们将在本章中通过了解 Inkscape (2D 矢量或形状)、Blender (3D 矢量或多边形)和 Fusion (2D 和 3D 视觉效果)来结束对新媒体素材的学习。我们之所以在本章而不是在 2D 内容章节中介绍 Inkscape 2D,是因为我们可以使用关于 2D 矢量图形如何工作的基本概念作为 2D 矢量图形和 3D 矢量图形之间的概念桥梁。这是因为 3D 向量的工作方式就像 2D 向量在 2D X 和 Y 维度中的工作方式一样,只是在 3D X、Y 和 Z 维度中。出于这个原因,我们将从学习 Inkscape 2D 矢量插图或数字插图开始本章,这样我们就可以建立在 2D 矢量知识的基础上,然后学习更复杂的 3D 矢量图形软件包。

我首先介绍顶点(点)和样条线(连接点的直线或曲线)的基本概念,因为它们为 2D 形状或 3D 几何图形提供了基础。这很重要,因为这是你决定成为 2D 矢量插画师或 3D 矢量建模师(或者两者都是)的基础。使用顶点和样条线可以成为一个完整的职业,所以一定要掌握前几节。

接下来,我们将进入你如何把一个空的 2D 形状或者一个 3D 线框变成一个立体的东西。这是通过对 2D 形状使用颜色填充、渐变或图案填充(这些也可以用于 3D 模型)以及对 3D 几何图形使用纹理贴图来完成的。纹理贴图使用 UVW 贴图将 2D 纹理贴图定位到 3D 几何体上。

在我们涵盖了适用于 2D 和 3D 空间的所有概念之后,我们可以进入只在 3D 中遇到的事物。其中包括 3D 渲染,这是将 3D 模型转换为 3D 图像的过程,3D 模型具有 3D 几何图形以及附有 3D UVW 纹理映射坐标的 2D 纹理映射。我把 3D 图像称为静态 3D,因为 3D 技术被用来制作不移动的图像,因此是静态的或固定的。还有 3D 动画,其特点是运动,很像数字视频,以及交互式 3D (i3D),其中编程逻辑嵌入在 3D 对象或场景层次中,这是 3D 的最高级。

动画进入第四维时间,就像数字视频一样,3D 动画为 3D 新媒体素材开发工作流程增加了另一层复杂性。3D 动画就像数字视频一样利用关键帧,因此所有这些相同的概念都适用,例如帧速率;它还有一些其他概念,如 JavaFX 支持的运动曲线,它可以改变加速和减速的速率,为 3D 动画和 JavaFX 中的 2D 动画提供逼真的运动,因为它们是独立的功能。

交互式 3D 包括将代码插入到称为场景图的对象层次结构中,场景图以分层格式保存资源、代码和其他元素。场景图是在 Amiga 时代由 3D 软件包发明的。开创这种设计和开发方法的 3D 软件包是 Realsoft OY 的 Real 3D,今天称为 Realsoft 3D。幸运的是,JavaFX 9 还具有广泛的场景图形 API,这使它非常适合创建交互式 3D 和交互式 2D 游戏以及物联网应用。

互动 2D 素材:2D 矢量内容概念

还有一种类型的 2D 素材,我们没有在第二章中详细介绍,因为它的概念与 3D 直接相关,所以我决定从逻辑上把这一信息放在本章的开头,以便信息更好地流动。2D 和 3D 在顶点和样条线的使用上非常相似,我们将在接下来学习。2D 使用 X,Y 维度,这是一个平面(或平面区域,如果你愿意),三维使用 X,Y,Z 维度(这是一个立方体区域,如果你愿意)。

因此,在本节中,我们将了解如何在 2D 空间中放置点或顶点,然后使用直线或曲线样条将它们连接在一起,并用纯色、颜色渐变或平铺图像图案填充闭合形状,从而创建 2D 矢量插图。JavaFX 9 提供了大量支持这些 2D 元素的 2D 类,以及一个用于导入所有这些 2D 数据元素的SVGPath类,如果您选择使用 Inkscape 的话。

您将使用 JavaFX APIs 在 Java 中使用的 2D 素材或对象通常被称为形状,尽管它们在技术上也是几何图形,因为形状本身就是几何图形!通常在行业中,3D 被称为 3D 几何,2D 被称为 2D 形状。2D 和 3D 素材的基础都是从空间中称为顶点的点开始的。这些是用(直)线或(非直)曲线连接的。接下来我们来看看这些。

平面上的点:2D 顶点、模型参考原点、枢轴点、虚拟点

现在不要激动——这不是《飞机上的蛇》的续集;这只是一个关于 2D 形状的基础的讨论,它像 3D 模型一样,是基于空间中的点。因为 2D 空间由一个 X,Y 平面组成,我们在一个 2D 平面上放置点。空间中的每个点在专业术语中称为顶点,因为这毕竟是 Pro Java 9 游戏开发。您可以在平面 X,Y 空间中使用这些顶点来创建 2D 形状,也可以在立方体 X,Y,Z 空间中创建 3D 几何体,我们将在本章稍后介绍。

我给这一节加副标题“平面上的点”的原因是因为顶点是用 2D 平面上的 X,Y 网格放置在 2D 空间中的,并且是在 3D 空间的 X,Y,Z 立方体区域中。这个 2D 网格的原点位于 0,0。通常这是屏幕的左上角,对于 3D 立方体区域,该参考原点将位于 0,0,0。

对于 2D 形状和 3D 对象,此原点可以重新定位,因此不同的包将从平面或立方体的不同角引用此网格。稍后您将在 JavaFX 中看到这一点,Java FX 有不同的引用坐标的方式。该平面或立方体区域内的另一个参考点用于 2D 或 3D 对象的旋转,称为轴心点。

例如,如果您希望锤子 2D 形状(或 3D 模型)在手柄末端附近旋转,就像它在现实生活中旋转一样,您可以通过将枢轴从 2D(或 3D)建模空间中的默认(中心)位置向下移动到锤子手柄的末端来实现这一点。对于这个应用,枢轴点将成为该对象轴的中心。枢轴点是素材需要如何旋转的参考原点,而网格(空间)原点将提供如何相对于彼此定位这些点的参考。因此,旋转算法将使用建模网格原点以及枢轴点位置。这些通常是不同的点坐标;然而,在某些情况下,它们可能是同一点。

原点和枢轴都使用轴来表示空间中的点。这个轴可以移动,在 3D 软件中看起来像一个星形,在 2D 软件中看起来像一个加号。事实上,轴实际上是 2D 形状或 3D 几何图形中的一个独立对象,它甚至可以像其他 2D 或 3D 对象元素一样,使用 JavaFX 和 Java 代码来创建与形状或几何图形如何随时间旋转相关的特殊效果。还有一个“虚拟点”,用于特殊效果和高级应用,非常类似于枢轴,但用于其他目的,也用轴来表示。在本书的后面,你会看到这个轴元素对于 Java 游戏是多么重要。

连接 2D 点:矢量线和样条曲线连接你的 2D 顶点

由于顶点在数学上是无限小的,这使得这些小点基本上是不可见的,你需要将它们连接起来,以制作一些你可以可视化的东西。最简单的体现是直线,称为矢量(在 3D 渲染中有时也称为光线)。向量从一个顶点开始向外投影,直到碰到第二个顶点,第二个顶点定义了向量的方向。矢量本身是直的,所以它被认为是直线,而不是曲线。正如你将要看到的,曲线在数学上比直线复杂得多。

由于我们经常想要一条无限平滑的曲线作为我们的 2D 形状或 3D 几何的一部分,我们将需要使用一种不同类型的数学构造,称为样条。样条曲线无限光滑的原因是因为它是一种使用数学方程定义的曲率,对于所有计算机程序员(鉴于本 Pro Java 9 游戏开发书的专业性质,我希望是所有人),可以通过使用较小的数字(如使用浮点数而不是整数)来提高其分辨率。

大多数类型的样条的数学基础被称为贝塞尔曲线,它是以数学家皮埃尔·艾蒂安·贝塞尔的名字命名的,皮埃尔·艾蒂安·贝塞尔是 1910 年至 1999 年间的法国工程师。Pierre 是 3D 实体、几何建模和物理建模领域的创始人之一,也是表示曲线的专业领域的领导者,尤其是在 CAD CAM 和 3D 系统方面。贝塞尔曲线有几种数学格式,包括三次或二次贝塞尔曲线,它们是使用不同类型的数学方程定义的,这些方程定义了如何构造每条曲线。

这些曲线中最简单的是线性贝塞尔曲线,它可用于创建直线(矢量)射线,并且仅使用两个控制点来定义曲线。如果您可以仅使用线性贝塞尔曲线定义形状,您的游戏或物联网应用将使用更少的处理和内存。这是因为需要处理的控制点更少。正如你在图 3-1 的顶部所看到的,Inkscape 使用蓝色绘制控制点及其手柄。如果你想在 Inkscape 中试试这个,点击图 3-1 左侧所示的样条/直线工具,点击创建一个点,点击其他地方的第二个点添加一条直线,然后点击第三个点并拖动创建一条曲线!一旦你掌握了窍门,这就相当容易了;也就是说,事实是,你在本书中学到的一切都需要大量的练习才能达到专业水平。

A336284_1_En_3_Fig1_HTML.jpg

图 3-1。

Creating an open shape in Inkscape using vectors and splines, closing that shape, and then filling the shape

要调整线性贝塞尔曲线的曲率,可以移动刚添加的顶点的两个手柄。如果你想要一条直线,只要点击添加顶点,直线就会把它们连接起来。另一方面,如果您想要制作一条曲线,请向下单击以添加顶点,并在单击鼠标的同时(按住鼠标),拖出贝塞尔曲线控制点手柄。

第二种最复杂的贝塞尔曲线是二次贝塞尔曲线,以用于描述它的二次数学算法的类型命名。二次贝塞尔曲线有三个控制点,而不是两个,因此它需要更多的处理,但通过使用控制柄对“调整”曲线的曲率提供了更多的控制。

最复杂的是三次贝塞尔曲线,以用于描述它的三次数学算法的类型命名,它有四个控制点,而不是三个,因此它的处理更加密集,但同样,它对调整曲线的曲率提供了更多的控制。

在 Adobe Illustrator 中,控制点被细分为使用手柄和锚点。用于影响曲率的控制柄位置是控制柄。锚点是描述贝塞尔曲线开始和结束位置的顶点。Inkscape 对锚点使用了不同的术语,称之为节点。

还有一种称为 NURBS 或非均匀有理 B 样条的 3D 建模方法,它与贝塞尔样条表示法相关,但针对 3D X,Y,Z 空间的使用进行了优化。NURBS 更复杂,并且允许创建平滑、有机的 3D 几何图形表示。迈克尔·吉布森的 3D 灵感时刻是真正负担得起的 NURBS 建模工具之一,仅售 295 美元;它基于原始的 SGI Alias 波前 NURBS 建模 API。

填充形状内部:颜色填充、渐变和图案

如果使用这些顶点和样条线(或矢量/线)创建的 2D 形状是闭合的,那么它们可以填充各种东西,如纯色、颜色渐变或平铺图像图案。这显示在图 3-1 的底部。要闭合曲线,请绘制最终的矢量(直线)或样条(曲线),直到鼠标光标位于起始顶点上,当该顶点发生变化时(在 Inkscape 中,它会从黑色变为红色),向下单击以创建闭合的形状。要填充您刚刚在 Inkscape 中关闭的形状,请单击填充工具,它与样条线/线条工具及其工具提示一起显示在左侧,然后单击底部色板上的颜色,用该颜色填充所选形状。

Inkscape 和 JavaFX 使用的 2D 矢量图形文件格式是可缩放矢量图形(SVG ),所以如果您保存您的 Inkscape 项目,它将使用.svg扩展名,例如ProjectName.svg。如果你想了解更多关于 SVG 的知识,可以看看 Apress.com 的数字插图基础。接下来让我们看看 i3D 媒体素材,Java 8 和 9 使用 JavaFX 9 新媒体引擎完全支持这些素材。

交互式 3D 资源:3D 矢量内容概念

最高级类型的多媒体素材是交互式 3D 矢量对象,可以使用 Java 和 JavaFX API(类和方法)或使用这种方法与 3D 建模包(如第一章中讨论的那些)或 3D 动画包(如 Autodesk 3ds Max,这是我从其第一个版本开始使用的;3D Studio DOS 或者 Blender,已经接近类似水平的专业功能)。i3D 素材由 3D 矢量几何图形组成,使用 2D 光栅图像进行表面处理(我们在第二章中了解到),并在其模型和场景层次结构中包含编程逻辑,这将使它们变得栩栩如生。

在本章的这一节,我们将学习 3D 物体如何从网格到表面模型。我们还将在本章中查看动画、运动曲线、对象层次、轴放置、虚拟对象、粒子系统、流体动力学、头发和毛发动力学、刚体动力学、软体动力学、布料动力学、绳索动力学以及相关的 3D 主题。如你所见,3D 是迄今为止最复杂、最有趣的新媒体类型。

通过使用场景图形对象层次结构内部的编程逻辑,可以进一步使这些 3D 对象具有交互性,场景图形对象层次结构定义了 3D 对象的每个部分将做什么,并且是 JavaFX 9 的一个组成部分。让我们从头开始。我将向您展示 3D 素材从 3D 几何体到 3D 模型、3D 层次结构再到 3D 对象的各种属性。这是最复杂的多媒体,也是 HTML5(使用 WebGL2)、Android 8(使用 Vulkan)以及 Java 8 和 JavaFX 中最不常见的新媒体素材类型。

3D 的基础:网格的几何学

与 2D 形状新媒体元素一样,3D 新媒体元素的最低层是顶点以及这些顶点之间的连接。在 3D 中,你仍然有顶点,但是它们之间的连接变得有点复杂。在 2D 中,顶点、向量(射线或直线)和它们之间的样条线(曲线)是空的(未填充的)、封闭的形状或开放的形状,它们不能被填充,因为它们是开放的,并且会溢出。3D 几何图形之间的连接(在纹理映射之前,在 3D 行业中有时被称为网格或线框,因为这是 3D 几何图形在纹理映射或蒙皮之前的样子),被称为顶点之间的“边”和边之间的“面”。

空间中的点:三维顶点的原点

就像 2D 顶点(在 Illustrator 中称为锚点,在 Inkscape 中称为节点)一样,顶点是 3D 几何和有机(NURBS、Catmull-Rom 样条线和散列面片)建模的基础。顶点定义模型的基础结构(无论是边还是样条)在 3D 空间中的位置,在 3D 中,顶点数据可以保存表面颜色数据、法线数据、UVW 纹理映射数据和顶点 XYZ 位置数据。熟悉 3D 扫描仪的人可能会熟悉点云这个术语,所以顶点仍然是我们在 3D 行业所做的一切的基础。

对于 Java 8 和 9 编码,JavaFX 9 有一个VertexFormat类,可以保存顶点数据,包括你的顶点位置,法线信息(我们很快会涉及法线),以及 UVW 纹理映射坐标。因此,您可以通过使用 Java 代码为 Java 9 游戏或物联网应用放置顶点,也可以使用 3D 建模器,如 Daz Hexagon、MoI 3D 或 Nevercenter SILO,或者 3D 建模和动画包,如 Blender 或 Autodesk 3ds Max。

连接 3D 顶点:边桥接 3D 顶点

大多数 3D 几何图形使用称为边的东西来连接两个顶点。一条边是一个矢量或直线,所以它看起来像 3D 空间中剃刀的边缘。形成一个多边形需要三条或更多的边,这是我们接下来要讲的。建模三维几何图形时,可以选择顶点、边、多边形或整个对象。

如果您已经使用更高级的基于样条的建模范式创建了 3D 几何体,例如使用 MoI 3D 的 NURBS、使用 SILO 2 的四边形(仅 160 美元)或使用 Animation:Master 的散列面片(仅 80 美元),您将需要将这些格式抽取为多边形或三角形,这将在接下来进行介绍。抽取过程将这些范例中使用的无限平滑的曲线变成了直边的集合。这是使用抽取(平滑度)数值因子(滑块或设置)完成的,该因子通常在文件导出函数中提供,该函数将样条线建模格式从基于曲线的建模器输出到多边形几何模型格式。

创建曲面:三条边形成多边形,四条边形成四边形

将三条边以三角形的形式放在一起后,您就有了一个多边形,它可以作为一个表面来承载皮肤或纹理,使 3D 数据看起来更真实。多边形有时被称为三角形、三角形或面形,一些建模者使用被称为“四边形”的正方形多边形如果您的渲染引擎像 JavaFX 和它的TriangleMesh类一样需要三角形,您可以将四边形抽取成三角形。在这种情况下,抽取算法相当简单,因为它只是在四边形曲面的两个对角之间插入一条边,从而创建两个角度特征相等(镜像)的三角形。最佳三角形来自正方形多边形,并且具有 45-45-90 度的拐角角度配置。经验法则是,三角形越均匀(正方形),渲染得越好,而“狭长”或长而细的三角形可能会导致渲染瑕疵,但通常不会。

一旦你有了一个表面(通常是一个三角形,如图 3-2 所示),基本立方体上的面是四边形,并且你已经定义了它的法线(我们接下来会了解),那么你就可以应用纹理贴图了。我们将在本章的下一个主要部分讨论纹理映射。还有另一个与相邻多边形或面相关的原则,叫做平滑组,我们将在讨论完曲面法线后再来看看。因此,至少,一个表面(多边形,三角形,四边形,面)将拥有一个法线,一个或多个纹理贴图,和一个平滑组。

A336284_1_En_3_Fig2_HTML.jpg

图 3-2。

Use the “Display face normals as lines” button to show direction normals for each quad face as light blue lines

指定表面朝向的方向:表面法线的概念

如果你知道如何在你的 3D 软件中打开“显示法线”功能,你可以看到面表面的法线,它将显示为一条从面的中心出来的线,正如你在图 3-2 中看到的浅蓝色。

Blender 2.8 中也有用于显示顶点法线的切换(按钮),顶点法线从顶点指向外,因此对于该模型,顶点法线从立方体的角(45 度)对角指向外,这与面法线的结果完全相反,面法线从面(曲面,四边形)的中心出来,指向直上(90 度,直上,像摩天大楼一样)。正如你在图 3-2 中看到的,显示的两条法线实际上与 x 轴(红色)和 y 轴(绿色)对齐,它们与立方体成 90 度相交。

轴向导位于 3D 编辑模式视图的左下角,它也显示在 Blender UI 左下角的 XYZ 轴向导下。这个表面法线的功能相当简单;它告诉渲染引擎表面面向哪个方向。在这种情况下,这个立方体将渲染为一个立方体,无论你给它什么纹理(皮肤)来着色。同样的逻辑也适用于顶点法线;它将显示渲染引擎处理 3D 几何图形的哪一侧进行表面渲染。

如果这个立方体几何体中的法线指向内部而不是外部,那么立方体在渲染时将完全不可见。在 3D 软件中有一个翻转法线操作(算法),用于通用地反转模型的法线方向(所有法线翻转 180 度)。这将在渲染场景时使用,渲染场景时导入的对象不可见。

当 3D 导入工具将导入的 3D 几何体的法线指向(翻转)错误的方向时,或者当其他 3D 工具的导出器将法线导出到错误的方向(相对于导入法线的软件)时,会出现翻转的法线。这在你的 3D 工作流程中是很常见的,所以如果你要经常在 3D 或者 i3D 中工作的话,至少要使用几次翻转法线功能。

如果你需要一些东西(比如一座房子),其中的 3D 几何体必须从外部和内部渲染,这在 i3D(比如虚拟世界)中很常见,你必须创建双面的几何体,尤其是面。然后你需要应用一个双面纹理贴图和 UVW 贴图,我们将在本章的下一节讨论 3D 纹理贴图的概念和技术时涉及到。

值得注意的是,对于 i3D,具有双面纹理的双面几何图形需要更多的渲染引擎处理,并且渲染是基于用户对交互式 3D 环境、世界或模拟的探索而实时实现的,因此 JavaFX 将同时导航、处理和渲染 i3D 场景,这需要大量的处理器周期才能顺利完成,因此数据优化非常重要。

虽然可以在 JavaFX 中为顶点指定法线,但是法线通常是基于每个面指定的。这就是为什么VertexFormat类有两种格式的原因。一个支持定义一次法线的多边形的位置和纹理,因为使用三个顶点定义法线不如只使用一个面有效,另一个是当你想使用顶点而不是多边形定义法线时的VertexFormat数据格式。

平滑曲面:使用平滑组使多边形看起来像样条曲线

你可能见过被渲染为实体(而不是线框)的 3D 模型,但看起来仍然像是被凿过的;也就是说,您可以看到渲染的多边形(面)就像它们是平面一样。在这种情况下,渲染引擎关闭了平滑。如果在启用平滑的情况下进行渲染,这种效果会消失,几何体看起来就像预期的那样,无限平滑,就像是使用样条线创建的,而实际上是使用多边形。让渲染引擎进行平滑会更有效,因此有一个称为平滑组的东西,它应用于每个面,告诉渲染器何时在两个面之间进行平滑,何时不进行平滑,这就留下了通常称为接缝的部分。平滑组使用简单的整数。如果数字在面的每一侧都匹配(对于该边相对侧的每个相邻面),则渲染为平滑过渡(颜色渐变)。如果数字不同,它将呈现为一条接缝;也就是说,该边清晰可见,因为该边每一侧的颜色渐变是不同的(颜色渐变在两个面上不是无缝的,也称为多边形)。

在某些 3D 软件包中,例如 Autodesk 3D Studio Max,您可以在用户界面中看到该平滑组编号模式,并且可以实际选择每个边旁边使用的(整数)编号。您也可以选择边两侧的数字,这是一种更复杂的方法,但为 3D 建模师提供了更精确的平滑控制。

在 Blender 等其他工具中,编号是隐藏的,平滑组功能是通过使用“标记接缝”、“清除接缝”、“标记锐化”和“清除锐化”等命令来“显示”的。这些命令可以在 Blender Edges 菜单中找到,如图 3-3 的左侧所示,其中标记锐化选项显示为淡蓝色。

A336284_1_En_3_Fig3_HTML.jpg

图 3-3。

Set edge smoothing in Blender using the Edges menu (Ctrl-E when in Edit Mode) command called Mark Seam or Mark Sharp

在 Blender 中,一些 3D 建模者(人,而不是软件)会犯错误,试图通过实际分割 3D 几何图形本身的边缘来暴露其 3D 几何图形中的接缝或锐边,这将实现这种视觉效果,但也可能在 3D 几何图形拓扑优化工作过程中引起问题。如果您熟悉映射中使用的术语拓扑,拓扑非常相似,指的是如何构建 3D 几何图形,以及如何渲染,因为渲染引擎是“基于数学的”,就像 3D 几何图形一样。

3D 模型的拓扑是 3D 几何图形的构造,即顶点、边和面相对于彼此放置的位置,或者是基于样条的有机 3D 模型的构造,其中放置了控制点、控制柄和类似的基于样条的拓扑(以及它们的放置顺序)。换句话说,3D 建模是复杂的!

通过使用 Blender 中的“标记接缝”或“标记锐边”修改器,可以避免分割几何体边来实现接缝。这些特定的混合器修改器实际上是基于平滑组的,因此实现了这种平滑(或边缝)效果,而不会实际影响 3D 几何体拓扑。

Blender 修改器在渲染之前应用,因此不会影响基础 3D 几何体的实际数学拓扑。Blender 修改器始终是一种更灵活的 3D 内容创建方法,因为它在渲染引擎级别应用平滑(或任何其他所需的效果),而不是在 3D 几何体拓扑级别,使 3D 网格保持完整。就像在 Pro Java 9 游戏开发(和物联网设计)中的任何事情一样,如果你能实现预期的效果和最终结果,越简单越好,因为越简单就意味着处理器开销越少。

蒙皮你的三维模型:2D 纹理映射概念

完成 3D 几何图形(3D 模型的基础)后,您可以对其应用纹理贴图,为 3D 模型创建实体外观,并为其添加细节和特殊效果,使其外观越来越逼真。如果您想知道 3D 几何图形和 3D 模型之间的区别,3D 几何图形只是网格或线框,而 3D 模型可以(应该)应用纹理贴图。如果您购买第三方 3D 模型,您希望它们看起来像您渲染它们时的样子,而不仅仅是纯灰色,这是在没有应用任何纹理映射(和没有顶点颜色)信息的情况下渲染的模型的样子。事实上,你会在网上找到的一些 3D 模型(免费或付费)甚至没有应用平滑组,所以你会有一些面,一些平滑,一些纹理到不同层次的细节。有些甚至可能翻转了法线,甚至不会出现在 3D 场景中,直到您对它们应用翻转法线操作或修改器。通常,您必须对任何预先存在的模型进行额外的建模、平滑和纹理映射工作,而不是从头开始创建。我通常尝试从头开始创建一切,因此我可以控制并熟悉底层的 3D 几何拓扑,以及如何将我的平滑组、UVW 贴图坐标、着色器和纹理贴图应用于模型。我们将在本节中讨论所有这些内容。

纹理贴图基础:概念,通道,阴影,效果和 UVW 坐标

纹理映射与正确创建几何拓扑一样复杂;事实上,3D 的每一个领域都同样复杂,这就是为什么 3D 是迄今为止最复杂的新媒体类型,也是为什么 3D 故事片雇佣艺术家来专门关注(工作)和处理我们在本章中看到的每一个领域。纹理映射是 3D 建模中能够使用 2D 矢量或 2D 光栅图像资源的主要领域之一。

值得注意的是,3D 纹理映射还有一个更复杂的领域,也称为纹理,它使用 3D 纹理算法,通常称为体积纹理,来创建贯穿 3D 对象的纹理效果,就好像它是一个实心的而不是空心的(这里是双面的)3D 对象。

纹理映射背后的基本概念是采用 2D 素材,例如我们在上一章中了解到的素材,并将这些 2D 素材应用到 3D 几何体的表面。这是通过使用 UVW 或 3D 映射坐标来实现的,以显示您希望 2D 图像(平面)如何定向或投影到您的 3D 几何表面拓扑上。现在,我想让你迅速从书上抬起头来,对听得见的人喊道:“我真的需要将这个样条拓扑抽取为多边形拓扑,以便我可以使用 UVW 纹理映射坐标将着色器应用到生成的几何体上,并将这个 3D 模型导出到我的 JavaFX 场景图形层次中。”然后继续阅读,就像什么都没有发生过一样,即使你刚刚向每个人展示了你的互动多媒体制作天才。

您可以使用纹理通道将多个纹理贴图添加到 3D 几何体的表面,纹理通道类似于您在 2D 图像合成软件中使用的图层。JavaFX 目前支持四个最重要的纹理通道:漫反射纹理贴图(基本的 ARGB 颜色值),镜面纹理贴图(表面有光泽或暗淡),照明纹理贴图(也称为发光贴图),以及凹凸纹理贴图。

3D 软件包支持其他纹理贴图通道类型,以获得额外的纹理贴图效果。为了能够将这些引入 JavaFX,您必须使用一个称为烘焙的过程。烘焙纹理贴图包括将所有尚不支持的纹理通道渲染到一个单一的漫反射纹理贴图中,因为这是 JavaFX 8 和 9 支持的。这与您在更高级的 3D 动画包中获得的视觉效果非常相似。

正如你在图 3-4 中看到的,Blender 2.8 也使用场景图,就像大多数现代 3D 软件包一样,JavaFX 也提供这种场景图功能;我们将在第八章中讲述它。球体几何体和纹理贴图在场景图形层次中组合在一起,这是我为您展开的。

A336284_1_En_3_Fig4_HTML.jpg

图 3-4。

Using a Scene Graph (right) to apply a gold texture map and shader (bottom) to a sphere object in Blender

随着时间的推移,理想情况下,JavaFX 9 将添加更多的纹理通道支持,并为开发人员提供更多关于 3D 新媒体资源使用的视觉灵活性,因为透明区域(不透明贴图)和表面细节(法线贴图)是关于高级纹理映射支持的两个最重要的区域。这些最终需要使用 JavaFX API 添加到 Java 中,以便开发人员能够为 Java 游戏创建逼真的 i3D 模型。

纹理通道的集合和控制这些通道相互关系的任何代码,以及它们相对于彼此如何合成、应用和渲染,称为着色器定义。着色器在 3D 行业中也通常被称为材质。在本章的下一节,我们将讨论着色器和着色器语言,因为这是 3D 和 i3D 游戏开发的另一个专门而复杂的领域。我还在我的书《VFX 基础》(Apress,2016)中使用开源 Fusion 8.2.1 详细介绍了着色器构造。

最后,一旦在着色器中定义了纹理,您将需要将这些 2D 素材定向到您的 3D 几何图形,这是通过使用纹理映射坐标来完成的,通常通过称为 UVW 映射的东西来完成,在我们继续进行第四维和动画之前,我们也将在它自己的特定部分中进行介绍。

纹理贴图设计:着色器通道和着色器语言

着色器设计本身就是一种艺术形式;成千上万的着色器艺术家在 3D 电影、游戏和电视节目中工作,确保用于“着色”或“蒙皮”3D 几何图形的着色器使最终的 3D 模型看起来尽可能真实,这通常是 3D 的目标,以取代更昂贵的摄像机拍摄(和重拍)。

基本着色器由一系列 2D 矢量形状、2D 光栅图像或体积纹理组成,保存在不同类型的通道中,这些通道应用不同类型的效果,如漫反射(颜色)、镜面反射(光泽)、发光(照明)、凹凸(地形)、法线(高度)、不透明度(透明度)和环境(环境)贴图。体积着色器本质上也是 3D 的,因此不使用 2D 图像作为输入,而是使用复杂的算法定义来产生穿过 3D 对象的 3D 着色器,这就是为什么它被称为体积着色器。这些 3D 体积着色器也可以设置动画,并可以根据它们在 3D 空间中的位置来更改颜色和透明度。

在此基础上,高级着色器语言(如 Open GL 着色器语言(GLSL))使用代码来指定这些通道如何相互关联,如何应用或处理这些通道中包含的数据,以及如何基于复杂因素(如时间、方向或 3D 空间中的位置)在这些通道中提供其他更复杂的数据应用。着色器的复杂特性也意味着着色器的渲染时处理更加耗时,并且处理周期越长,着色器就变得越复杂。所需的处理器周期通常很昂贵,因为复杂的着色器能够产生照片级的真实效果。

这可能是 JavaFX 9.0 目前支持四种基本(也是最容易处理的)着色器的主要原因。随着硬件变得更加强大(你会在更多的消费电子产品中看到六核、八核和十核 CPU),JavaFX 可能会添加最后两个重要的着色器通道:不透明度(或透明度映射)和法线映射。

纹理贴图方向:纹理贴图投影类型和 UVW 坐标

将 2D 纹理贴图通道中的细节特征(尤其是基本的漫反射颜色通道)与 3D 几何体正确对齐是非常重要的,否则在渲染时会出现一些非常奇怪或至少视觉上不正确的结果。这需要在 3D X,Y,Z 空间中完成,特别是对于体积纹理和 2D 纹理,以定义它们如何投影到或包围 3D 几何图形。

最简单的方法之一是应用纹理贴图的投影类型和相关设置,这将自动为你设置 UVW 贴图的数值。这些 UVW 地图坐标值将定义 2D 影像平面如何映射到 3D 空间中的 3D 几何上,这是 2D 空间和 3D 空间之间的一种桥梁,并且可以手动设置或调整 UVW 浮点值以微调您的视觉效果。

其中最简单的是平面投影,您可以将它想象为好像您的纹理贴图在 3D 对象的前面,并且您用灯光穿过它,因此它看起来像漫反射纹理贴图中的颜色在 3D 对象上。平面投影对于计算机来说是最简单的处理方式,因此如果您能够获得专业 Java 游戏或物联网应用所需的结果,请使用它。但是,它通常用于静态渲染的 3D 影像,因为一旦您将(相机)移动到 3D 模型的侧面,这种类型的投影贴图就不会提供照片级的真实效果。

相机投影类似于平面投影。相机投影将您的纹理从相机镜头(100%平行于镜头)投影到 3D 对象表面,就像幻灯机一样。这可以用于在场景中投影视频背景,以便您可以在它们面前建模或最终制作 3D 素材的动画。如果相机移动,相机投影与镜头前部保持平行。这有时被称为广告牌模式(或投影)。

下一个最简单的是圆柱投影,它比纹理贴图从一个方向到 3D 对象的(固有的)2D 平面投影提供了更多的纹理贴图的 3D 应用。一个圆柱体将围绕你的对象,在上下(z 轴)维度上,将图像投射到你的对象周围!所以,如果你绕着它走,在另一个维度上会有独特的纹理细节,这是平面投影所不能提供的。

一种更复杂的投影叫做球面投影。这提供了比从 X 和 Y 方向沿着 Z 维度将纹理贴图圆柱投影到 3D 对象上更完整的 3D 纹理贴图应用。球形投影试图处理所有三个(X,Y,Z)轴投影方向。

与球形投影类似的是立方体投影,它类似于立方体格式的六个平面投影;这给出了类似于球形投影的结果。将立方投影应用于 3D 对象时,对象的面会根据多边形法线的方向或与面的接近程度指定给立方纹理贴图的特定面。然后使用平面投影方法从立方体纹理贴图的每个面投影纹理,或者对于一些 3D 软件包可能使用球形投影贴图。

如果使用体积纹理,空间投影是三维 UVW 纹理投影,它通过 3D 对象的体积进行投影。它通常与需要内部结构的材质(如木材、大理石、海绵、玛瑙等)的程序纹理或体积纹理一起使用。如果使 3D 对象变形或变换相对于 3D 对象的纹理映射坐标,将会显示体积或程序纹理的不同部分。

还有一个更简单的纹理贴图叫做 UV 贴图(没有 W 维度)。这将纹理应用于二维,而不是三维,并且更容易处理,因为它的数据更少。我们可能会使用 3D 软件在 JavaFX 外部映射我们的 3D 模型,然后使用模型导入器将已经纹理映射的 3D 对象导入到 Java 中,因为从 JavaFX 8 开始,一些更高级的 3D 映射支持的类尚未添加到 JavaFX API 中。

为 3D 模型设置动画:关键帧、运动曲线和 IK

创建 3D 几何体并使用着色器和贴图坐标对其进行纹理贴图后,您可能希望使其以某种方式移动,例如飞行飞机模型。您在第二章中学到的关于数字视频素材和 2D 动画素材的概念同样适用于 3D 动画。

线性动画:轨迹、关键帧、循环和范围

最简单的 3D 动画类型,和 2D 动画一样,是线性动画,它适合许多类型的动画。图 3-5 显示了如何在 Blender 2.8 中使用插入关键帧菜单向立方体对象添加关键帧。

A336284_1_En_3_Fig5_HTML.jpg

图 3-5。

Using the Insert Keyframe Menu in Blender 2.8 with a Cube object selected to add a Delta Scale keyframe

键盘上的 I 热键用于访问此插入关键帧菜单,并选择立方体对象。大多数 3D 软件包都有通常所说的轨迹编辑器,允许您向轨迹添加关键帧和运动曲线。每个轨迹将与一个 3D 模型相关,如果您的 3D 模型使用子组件分组,那么将有组和子组以及组或子组内的单个组件的轨迹。

线性动画使用的处理能力最少,因此效率最高。如果可以使用线性动画来实现动画目标,请尽可能使用最少数量的轨迹和最少数量的关键帧,因为这将使用最少的系统内存。

如果动画运动是重复的,使用无缝循环而不是长距离。一个无缝运动循环比包含同一运动的多个副本的长范围占用的内存少。对于线性动画来说,使用循环是一个很好的优化原则。接下来,让我们看看一些更复杂的动画类型,包括非线性动画(在一条直线上,具有均匀间隔的关键帧)以及角色动画和程序动画,这些动画用于刚体或软体物理(物理)模拟、布料动力学、头发和毛发动力学、粒子系统和流体动力学等。

非线性动画:运动路径和运动曲线

更复杂类型的非线性动画不太规则,通常看起来更真实,尤其是在涉及人体运动和简单物理模拟的情况下,它将为动画 3D 对象或元素(层次结构中的子对象)实现移动路径。JavaFX 有一个Path类,可以用作你自己的复杂动画或游戏精灵运动的运动路径。为了给沿该路径的运动增加更多的复杂性,可以使用运动曲线,以便运动本身可以加速或减速,模拟重力和摩擦等情况。使用这些运动曲线可视化表示的数学算法被称为插值器,JavaFX 有一个Interpolator类,包含各种最标准的(如果有效使用,仍然非常强大)运动曲线算法。

非线性不规则运动关键帧的一个很好的例子是一个橡胶球在弯曲的道路上反弹。道路的弯曲路径将使用您的运动路径来确保球停留在道路曲率上,并且球地板符合道路的坡度(角度)。球的反弹将使用运动曲线,有时也称为运动插值器,以使每次反弹看起来更真实,因为它在空间中的运动随着时间的推移而加速和减速。在这种情况下,这将控制你的球对地面的反应。

图 3-6 显示了屏幕底部的 Blender 时间线编辑器;您可以看到两个旋转关键帧显示为垂直黄线,当前帧设置显示为垂直绿线。

A336284_1_En_3_Fig6_HTML.jpg

图 3-6。

The Blender 2.8 Timeline Editor , with two keyframes at frame 0 and frame 10, and the current frame 6 setting

包含许多交互元素的复杂物理模拟无法使用关键帧来完成,尽管如果您有大量时间,理论上是可行的;然而,这是无利可图的(不值得你浪费时间)。就像将运动曲线应用于关键帧回放利用插值算法一样,程序动画算法更进一步,不仅影响关键帧的计时,还影响关键帧数据本身(X、Y、Z 数据、旋转数据、缩放数据等)。).

因为程序动画是以算法的形式出现的,所以它非常有效,因为一旦算法被创建,它就可以被一次又一次地使用,而不需要额外的工作。这些程序动画算法在 3D 中创建了许多特殊效果类型,包括刚体动力学和软体动力学(物理模拟)、绳索和链条动力学、布料动力学、头发和毛皮动力学、粒子系统、流体动力学、肌肉和皮肤弯曲动力学、对口型动力学和面部表情动力学。我们将在稍后介绍程序动画,因为我们在这一章的每一节都是从较低级的概念发展到较高级的概念。

接下来让我们概括一下角色动画;这是 JavaFX 可能支持的下一种动画类型,因为 JavaFX 导入程序支持导入更复杂类型的 3D 数据,包括高级类型的动画,如角色动画。

角色动画:骨骼、肌肉、皮肤、正向和反向运动学

更复杂的动画类型是角色动画,角色动画制作人是 3D 电影、游戏或电视内容制作团队中最受欢迎的职位之一。角色动画涉及许多复杂的层,包括为角色的骨骼设置“骨骼”层次,使用反向运动学来控制骨骼(角色)的运动,将肌肉附加到骨骼并定义它们如何弯曲,将肌肉附加到皮肤,甚至添加衣服和布料动态以穿着角色。在 3D 角色动画中,事情以与现实生活中非常相似的方式完成,以便逼真地模拟现实生活,这通常是 3D、i3D 和 VR 试图做的事情。

因此,使用角色动画来模拟生物就像不使用直接编码的动画一样复杂,正如你现在所知道的,这被称为程序动画。

在角色动画的最底层,你有骨骼;骨骼使用反向运动学算法,告诉骨骼它的运动范围(旋转),这样你就不会像《驱魔人》中那样肘部弯曲或头部旋转!你猜对了,骨骼以层次结构连接成骨架。该骨架是您稍后为角色设置动画(关键帧)的对象。您还可以通过将肌肉和皮肤附加到骨骼来模拟肌肉和皮肤,并定义骨骼运动将如何为角色弯曲肌肉和拉伸皮肤。正如您可能想象的那样,设置所有这些是一个复杂的过程;这是角色动画的一个领域,叫做索具。如果你需要添加服装,有一个新的 3D 领域叫做 cloth dynamics,它定义了服装在风中如何移动、起皱和吹动,还有类似的程序动画算法,旨在增加真实感。接下来让我们看看这个,以及其他一些类似的高级程序动画和模拟 FX 算法。

程序动画:物理,流体或布料动力学,粒子系统,头发

最复杂的动画类型是程序动画,因为它需要使用代码来完成,编写计算 3D 向量和矩阵以及物理和流体动力学方程的代码与游戏编程代码一样复杂,如果不是更复杂的话(取决于游戏的复杂性)。在 3D 包中,这种编码通常使用 C++、Python 或 Java 来完成,而 Pro Java 9 游戏开发中的程序化 3D 动画将通过使用 Java 9 APIs 和 JavaFX 8 APIs 的组合来完成。程序化是最复杂但也是最强大的 3D 动画类型,这也是为什么程序化动画程序员是目前 3D 电影、游戏、物联网和互动电视(iTV)行业中另一个更受欢迎的 3D 职位空缺的原因。

3D 建模和动画包中有许多“功能”,如 Blender 或 3D Studio Max,它们实际上是程序动画算法插件,向用户展示用户界面以指定参数,这些参数将在程序动画应用于 3D 模型或复杂的 3D 模型层次(通过使用 3D 软件或 JavaFX 场景图创建,如图 3-4 右侧所示的场景图)时控制程序动画的结果。我们刚刚讨论了一个复杂的骨骼-装配-肌肉-皮肤角色模型层次,可以将 cloth dynamics 应用于该层次,以使服装在 3D 角色奔跑、战斗、驾驶、跳舞等时逼真地移动。

程序动画算法控制的功能(其中许多包括真实世界物理模拟支持)的示例通常添加到高级 3D 动画软件包中,包括 3D 粒子系统、流体动力学、布料动力学、绳索动力学、毛发动力学、软体动力学和刚体动力学。

JavaFX 3D 支持:几何图形、动画和场景包

JavaFX 中有三个顶级包,包含对 2D 和 3D 新媒体资源类型的所有支持。javafx.geometry 包使用 Point2D 和 Point3D 类支持低级 3D 几何构造,例如顶点,使用 Bounds 和 BoundingBox 类支持区域。javafx.animation 包使用时间轴、关键帧、关键值和插值器类支持时间轴、关键帧和运动曲线等低级动画构造。javafx.scene 包包含了许多嵌套包,我喜欢称之为子包,包括用于 2D 或 3D 形状构造的 javafx.scene.shape,如 Mesh、TriangleMesh 和 MeshView 类;javafx.scene.transform 包支持 2D 和 3D 变换,包括旋转、缩放、剪切和变换类;javafx.scene.paint 包包含着色类,如 Material 和 PhongMaterial 类;以及 javafx.scene.media 包(MediaPlayer 和 MediaView 类)。

JavaFX API 3D 建模支持:点、多边形、网格、变换、着色

我将把 JavaFX 3D 素材支持分成两个图,一个用于静态 3D(渲染图像),一个用于动态 3D (3D 动画)。Interactive 3D 将使用 JavaFX 3D 的所有功能以及一些 Java API 功能。第一个图,图 3-7 ,展示了 JavaFX 包支持的四个主要领域。对于创建可用于静态 3D 影像的 3D 模型,以及用于动画 3D 的其他 JavaFX APIs 和用于交互式 3D 游戏、物联网应用和 3D 模拟的 Java APIs,这些都非常重要。

A336284_1_En_3_Fig7_HTML.jpg

图 3-7。

High-level diagram of JavaFX 3D modeling asset support for geometry, shape, transform, and texture map

javafx.geometry包包含 Java 和 JavaFX 中所有 3D 或 2D 几何图形的基础,即顶点(点)和空间(边界)。Point2D类支持顶点(2D 空间中的一个点)和向量(2D 空间中从一个点发出的一条线)表示。一个Point3D类也支持顶点(3D 空间中的一个点)和向量(3D 空间中从一个点发出的一条线)表示。Bounds超类表示 JavaFX 场景图节点及其包含的对象的边界。Bounds超类的BoundingBox子类包含场景图形节点对象在 2D 或 3D 空间中的边界的更专业的表示。

javafx.scene.shape包包含用于创建 3D 几何图形的MeshMeshViewTriangleMesh对象(类),而javafx.scene.transform包包含用于将 3D 空间变换应用到 3D 空间中的 3D 几何图形的旋转、缩放、剪切和变换对象(类)。javafx.scene.paint 包包含 Material 和 PhongMaterial 对象(类),允许您使用不同的着色器算法在 javafx 中对 3D 对象进行纹理处理。接下来,让我们仔细看看 JavaFX API 为我们提供了什么来支持第四维时间,以便我们可以为您的 Pro Java 9 游戏(或物联网应用或 3D 模拟)添加 3D 动画功能。

JavaFX API 3D 动画支持:时间轴、关键帧、键值、插值器

正如您可能想象的那样,在 JavaFX 中实现 2D 和 3D 矢量动画的大多数关键类(没有双关语)都存储在 javafx.animation 包中,如图 3-8 所示。例外情况是 Camera 超类及其两个子类 PerspectiveCamera(透视投影)和 ParallelCamera(正交投影)。时间轴对象(类)保存动画定义,动画定义由关键帧对象(类)组成,关键帧对象又由包含实际变换指令数据的 KeyValue 对象组成。一个关键帧对象可以保存一个 KeyValue 对象数组,因此一个关键帧可以保存几个不同的 KeyValue 转换数据对象。还有一个插值器类,它包含许多高级算法,用于将运动曲线应用到时间轴对象内部的关键帧对象。目前支持的插值算法包括离散或离散时间插值、EASE_IN 和 EASE_OUT 以及 EASE_BOTH(缓入缓出)和线性直线(均匀间隔)插值,这显然是处理强度最低的。

A336284_1_En_3_Fig8_HTML.jpg

图 3-8。

High-level diagram of JavaFX 3D animation support , showing javafx.animation and javafx.scene packages

现在你已经对 2D 矢量插图和三维矢量渲染和动画概念有了一个坚实的(三维)概述,在我们进入第四章的游戏理论、概念、优化等之前,我们将稍微休息一下。

摘要

在第三章中,我们仔细研究了一些与 2D 矢量插图和 3D 矢量渲染、纹理和动画相关的更重要的新媒体概念,您将在您的专业 Java 游戏开发工作流程中使用这些概念,以便您提前掌握这些内容的基础知识。

我从介绍 2D 矢量图形概念开始,这些概念也适用于 3D 矢量图形,包括顶点(顶点或点)、矢量(射线或直线)和样条(带控制手柄的曲线)。我们还研究了如何用纯色、颜色渐变或平铺图像图案填充这些 2D 形状。

然后,我们在这些概念的基础上,带您进入 3D 矢量图形,在那里我们学习了多边形、三角形、四边形、面和边,所有这些都与顶点结合在一起,创建 3D 几何图形。我们研究了如何使用纹理贴图、UVW 贴图和投影贴图使 3D 几何线框(也称为网格对象)看起来立体,以及如何将所有这些以材质或着色器的形式结合在一起。

接下来,我们看了 3D 动画,它比 2D 动画或数字视频复杂得多,因为在高端,它包括角色动画、程序动画和算法特效,其中包括物理模拟数学和控制大量粒子等内容,这些内容产生强大的基于代码的动画系统,如群集模拟、头发和毛皮动力学、群体模拟、流体动力学、布料动力学、软体和刚体动力学。在下一章,我们将看看游戏设计流派。

最后,我们看了 JavaFX 9 APIs,我们将在本书中使用它来实现我们在本章中学到的所有 3D 概念和原理。当我们实现 Java 9 游戏的组件时,我们将详细研究这些。

在下一章,我们将全面了解游戏理论和与创建游戏相关的概念,使用 Java 9 和新媒体素材来实现我们的游戏设计和游戏目标。

四、游戏设计导论:游戏设计概念、类型、引擎和技术

让我们在前两章中学到的新媒体素材知识的基础上,看看如何利用这些强大的像素、帧、样本和向量来创建 pro Java 9 游戏和物联网应用,以及为什么(或为什么不)在某些类型的 pro Java 9 游戏开发类型和场景中使用它们。我们将了解高级游戏概念、基本游戏设计风格和游戏设计优化概念,以及适用于 Java 平台的开源游戏引擎,包括 JBox2D、JBullet、Jinngine 和 Dyn4J 等物理引擎,以及 LWJGL 和 JMonkey 等 3D 游戏引擎。

我想介绍的第一件事是静态(固定)与动态(实时)的基本概念,因为它适用于游戏类型、游戏设计以及游戏优化。我已经在第 2 (图像对音频-视频)和第 3 (渲染 3D 对 3D 动画对交互式 3D)章节中介绍了静态(图像,渲染静态 3D 图像)对动态(数字视频,2D 和 3D 动画,交互式 3D,数字音频)的概念。这个简单的概念是对游戏类型进行分类的好方法,也是游戏优化的基本原则,正如你将看到的。在这一章中,我们将对游戏设计,新媒体的引入,以及不同的游戏设计方法和策略在内存占用和 CPU 处理周期上的花费有一个高层次的概述。

为什么这很重要,为什么我们在本书的第一部分中“预先思考”所有这些游戏设计因素,是因为您应该希望您的游戏能够在用于玩游戏的所有不同平台和消费电子设备上流畅地运行,即使这些设备采用单核处理器。如今单核处理器实际上非常罕见。入门级消费电子设备现在具有双核(双处理器)、四核(四处理器)、六核(六处理器)或八核(八处理器)CPU。流畅游戏的反面会被归类为生硬或不流畅的游戏,这不是一个好的用户体验(UX)。用户体验源于用户界面设计、游戏概念、新媒体素材和代码优化的结合,以及每个用户对你的游戏设计感兴趣和好奇的程度。

接下来我将介绍游戏设计和开发的不同方面或组成部分。这些包括游戏设计和开发的概念、技术和“行话”,我想确保你能跟上速度。这些主题包括 2D 精灵、3D 模型、人工智能、层、级别、碰撞检测、物理模拟、背景板动画、游戏逻辑、游戏设计、用户界面以及类似的游戏设计和开发方面,这些都可以被视为游戏“组件”,因为每一个都为专业的 Java 9 游戏添加了不同的属性和功能。最后,我将介绍您可以设计和开发的不同类型或流派的游戏,只是为了让您的左右脑同时工作,然后我将探讨一些技术问题以及素材和代码优化考虑因素,了解这些流派之间的差异。

高级概念:静态与动态游戏

我想从一个高层次的概念开始,它涉及到我将在本章中讨论的所有内容,从您可以创建的游戏类型到游戏的优化,再到您的 JavaFX 场景图的构建。我们在第二章和第三章中看到了这个概念,我们将在下一章中再看一遍,届时我们将看到固定的或静态的不变的 Java 常量与动态的、实时变化的 Java 变量的概念。类似地,JavaFX 场景图中的用户界面设计可以是静态的(固定的或不可移动的)或动态的(动画的、可拖动的或可换肤的),这意味着您可以更改 UI 外观以适应您的个人喜好。

这些概念在游戏设计和开发中如此重要的原因是因为你的游戏引擎,你将设计来“运行”或“渲染”你的游戏,将需要不断地检查(处理)你的游戏的动态部分,看看它们是否已经改变,因此需要一个响应。响应需要处理,并且需要执行(处理)Java 代码来更新分数、移动游戏板位置、播放动画帧、改变游戏角色的状态、计算碰撞检测、计算物理、应用游戏逻辑等等。每个游戏周期(JavaFX 中称为脉冲)上的这些动态检查要求(和随后的处理)会更新,以确保变量、位置、状态、动画、碰撞、物理等符合您的 Java 游戏引擎逻辑,并且可以真正累加。这就是为什么在你的游戏设计中静态和动态的平衡是重要的;在某些时候,处理所有这些工作的处理器可能会过载,从而降低游戏速度。

这种增强游戏动态的所有实时、每脉冲检查过载的结果是,游戏运行的帧速率可能会降低。没错,就像数字视频和动画一样,游戏也有帧率,但游戏帧率是基于你的编程逻辑的效率。游戏的帧率越低,游戏性就变得越不流畅,至少对于街机游戏这样的动态、实时游戏是如此。游戏的流畅程度与用户体验的“无缝”程度有关。

因此,静态与动态的概念对于游戏设计的每个方面都非常重要,并且使得某些类型的游戏比其他类型的游戏更容易获得更好的用户体验。我们将在本章的下一节讨论不同类型的游戏,但是正如你所想象的,棋盘游戏本质上更“静态”,而街机游戏本质上更“动态”。也就是说,有一些游戏优化方法,我们将在本书中讨论,可以使游戏保持动态(看起来好像有很多正在进行),当从 CPU 的处理角度来看,真正正在进行的事情,从处理角度来看,变得可管理。这是游戏设计的众多技巧之一,说到底,都是关于以这样或那样的方式优化。

我在 Android (Java)编程书籍中涉及的最重要的静态与动态设计问题之一是使用 XML 的 UI 设计(静态设计)与使用 Java 的 UI 设计(动态设计)。Android 平台将允许使用 XML 而不是 Java 来完成 UI 设计,以便非程序员(设计者)可以为应用进行前端设计。JavaFX 允许通过使用 JavaFX 标记语言(FXML)来完成完全相同的事情。

为此,您必须创建 FXML JavaFX 应用,正如您在第六章中在 NetBeans 9 中创建游戏应用时所看到的那样。该选项将javafx.fxml包和类添加到您的应用中,允许您使用 FXML 设计 UI,然后让您的 Java 编程逻辑“膨胀”它们,以便设计成为 JavaFX UI 对象。需要注意的是,使用 FXML 会在应用开发和编译过程中增加另一层处理器开销,包括 FXML 标记及其翻译(和处理)。出于这个原因,并且因为,归根结底,这是一本专业的 Java 9 游戏开发书,而不是 FXML 标记标题,所以在本书中,我将重点介绍如何使用 Java 9 和 JavaFX APIs 做所有事情,而不是使用 FXML 做事情。

在任何情况下,我关于使用 XML(或 FXML)创建 UI 设计的观点是,这种 XML 方法可以被视为“静态的”,因为设计是预先使用 XML 创建的,并在编译时使用 Java 进行“膨胀”。Java 膨胀方法使用设计者提供的 FXML 设计来创建场景图,该场景图基于使用 FXML 定义的 UI 设计结构填充有 JavaFX UI 对象。静态 UI 被设计为固定的,为游戏玩家处理用户界面,当你的游戏被加载时,它被一次性放入内存。

游戏优化:平衡静态元素与动态

游戏优化归结为平衡不需要实时处理的静态元素和需要持续处理的动态元素。过多的动态处理,尤其是在不需要的时候,会让你的游戏变得不稳定或生硬。这就是为什么游戏编程是一种艺术形式;它需要“平衡”,以及伟大的人物,故事情节,创造力,幻想,预期,准确性,最后,优化。

例如,表 4-1 描述了在动态游戏中优化不同游戏组件的一些注意事项。正如你所看到的,游戏中有很多地方可以优化,使处理器的工作负载大大减少。如果这些主要的动态游戏处理区域中有任何一个因为处理器宝贵的“每帧周期数”而“失控”,都会极大地影响游戏的用户体验。我们将进入游戏术语(精灵,碰撞检测,物理模拟等。)在本章的下一节。

表 4-1。

Some Aspects of Gameplay That Can Be Optimized

玩游戏方面 基本优化原则
精灵位置(移动) 移动精灵尽可能多的像素,同时仍然实现平滑的运动外观
精灵动画 最大限度地减少需要循环的帧数,以创建流畅的动画效果
冲突检出 仅在必要时检查屏幕上对象之间的碰撞(非常接近)
物理模拟 尽量减少场景中需要进行物理计算的对象数量
群体/人群模拟 最大限度地减少需要循环的成员数量,以创造一种人群或羊群的假象
粒子系统 最大限度地减少粒子的复杂性和制造错觉所需的粒子数量
相机动画(3D) 尽量减少相机动画,除非它是游戏不可或缺的一部分(绝对需要)
背景动画 最小化动画背景区域,使整个背景看起来是动画,但实际上不是
数字视频解码 尽量减少数字视频的使用,除非它是游戏不可或缺的一部分(绝对需要)
游戏性(或人工智能)逻辑 尽可能高效地设计/编写游戏逻辑、模拟或人工智能
记分板更新 通过绑定更新记分板,并将显示更新减少到最大每秒一次
用户界面设计 使用静态用户界面设计,这样脉冲事件就不会用于 UI 元素定位

考虑所有这些游戏内存和 CPU 处理优化问题将使你的专业 Java 9 游戏设计和 Java 编码,确实是一个相当棘手的努力。不用说,很多投入到专业 Java 9 游戏中。

值得注意的是,其中一些共同作用,给玩家创造了一种错觉;例如,精灵动画将创建角色奔跑、跳跃或飞行的幻觉,但如果不将该代码与精灵定位(运动)代码相结合,就无法实现幻觉的真实性。要微调幻像,可能需要调整动画的速度(帧速率)和移动的距离(每帧移动的像素)。我喜欢称这些调整为微调。调整意味着手动插入数据值,以获得最真实的最终结果。游戏开发是一个迭代的过程;尽管你可能试图坐下来预先设计你的游戏,然后创建新的媒体素材并编写 Java 代码,但修改是不可避免的。

在本书中,我们将会深入到专业 Java 游戏设计和开发的许多领域,但是为了在这里更详细地阐明这些,当我们在考虑这些因素时,如果你可以移动游戏元素(主要玩家精灵,投射精灵,敌人精灵,背景图像,3D 模型)更多的像素和更少的次数,你将会节省处理周期。需要处理时间的是移动的部分,而不是距离(移动了多少像素)。类似地,对于动画,实现令人信服的动画所需的帧越少,保存帧所需的内存就越少。同样的原则也适用于数字视频数据的处理,无论您的数字视频素材是捕获的(包含在 JAR 文件中)还是来自远程服务器的流。无论您是否在传输数据,解码数字视频帧都是处理器密集型的,并且会占用游戏其他组件的宝贵 CPU 周期,这可能也需要大量处理。

同样重要的是要记住,我们在优化内存使用和处理器周期,这两者是相辅相成的。使用的存储单元越少,处理器检索数据的努力就越少,因为一次一个存储地址地读取和处理存储单元;要处理的地址越少,意味着正在进行的处理越少。因此,内存优化也应被视为一种处理周期优化。

有了群集、群体动力学和粒子系统效果,当使用像这样的处理密集型特效时,需要处理的元素更少,每个元素的复杂性更低,这将很快增加。这些类型的基于粒子的特效为任何游戏、电影或电视剧添加了大量“哇”的因素,但也需要实时处理大量数据,这可能是处理密集型的。我们将在第五章中讨论数组。

检测碰撞是许多不同游戏类型的游戏编程逻辑的另一个主要部分,例如街机游戏、棋盘游戏和虚拟现实游戏,例如第一人称射击游戏。不盲目检查(处理)游戏元素之间的碰撞是非常重要的。请确保排除不在“游戏中”(在屏幕上)或不活动、彼此不靠近或永远不会相互碰撞的游戏资源(静态元素)。正如您可能想象的那样,冲突检测考虑、优化和编程本身就是艺术形式,关于这个主题的书籍已经有很多了,所以请客观地看待这个有趣的主题,并自己研究它,尤其是如果您对创建可能会发生大量冲突的 Pro Java 9 游戏感兴趣的话。

为物理模拟计算自然力是处理器最密集的,像碰撞一样,许多书籍都写了物理编程的每个单独领域,包括刚体或软体动力学,布料动力学,绳索动力学,头发和毛皮(实际上是连接的圆柱体粒子系统),以及流体动力学或驾驶动力学(用于驾驶游戏)。如果这些类型的物理模拟中的任何一种都没有经过精心的编码和优化,那么根据游戏玩家在他们玩游戏的消费电子硬件中拥有多少处理器内核,整个游戏用户体验可能会陷入停滞。

2D 与 3D 渲染:幕后的静态与动态

静态 3D 游戏,例如国际象棋,其中游戏棋盘是静态的,除非你将一个游戏棋子移动到棋盘上的新位置,否则游戏棋盘可能看起来是静态的;然而,由于它们利用 3D 实时渲染来创建虚拟现实环境,因此“在引擎盖下”系统可能会忙于实时渲染几何图形、照明、相机和材质,这取决于您如何在场景图形中设计所有这些元素以及如何设置 Java 游戏处理逻辑。正如我们在第三章中看到的,3D 新媒体素材的性质比 2D 新媒体素材复杂一个数量级,同样的事情也适用于 3D 游戏和 2D 游戏。2D 游戏确实有一个“渲染”元素,称为双缓冲,下一帧在显示在屏幕上之前在内存中合成。然而,3D 渲染实际上是创建像素颜色和 alpha 值,而不是简单地在 X,Y 位置组织它们。3D 渲染基于 3D 几何图形、变换、材质、着色器、贴图坐标、灯光位置和相机位置,从头开始创建像素颜色值和 X、Y 位置。

接下来,让我们更详细地了解一下专业 Java 游戏设计方面和一些核心游戏概念的考虑事项,并了解一下适用于所有游戏类型的一些核心优化原则。

游戏组件:2D、3D、碰撞、物理和人工智能

让我们来看看为了能够构建一个游戏,你需要理解的各种游戏设计概念、方面和组件,以及我们可以使用哪些 Java(或 JavaFX)包和类来实现游戏性的这些方面,我喜欢称之为游戏性设计和开发的组件。这些可能包括游戏元素本身,在游戏行业中通常称为 2D 游戏的精灵或 3D 游戏的模型,以及处理引擎,我们将自己编码或导入预先存在的 Java 代码库,如人工智能、物理模拟、粒子系统、反向运动学或碰撞检测。我将花一些时间来介绍这些组件,为什么它们适用于一个专业的 Java 游戏,以及如果你决定使用这些游戏组件时应该记住的一些优化注意事项。

2D 精灵:街机风格游戏的基础

让我们从最古老的电子游戏形式之一的基础开始,街机游戏。被称为精灵的 2D 素材定义了我们的主角,用来伤害主角的投射物,主角收集的宝物,以及发射这些投射物的敌人。精灵是 2D 图形元素,可以是静态的(固定的,单个图像)或动态的(动画,几个图像的无缝循环)。精灵可以是矢量(形状)或光栅(基于图像)资源。如果它们是基于图像的,它们通常是 PNG32 并带有 alpha 通道,以便它们可以在游戏设计的其余部分上实时合成,并使结果看起来像是数字视频,也就是说,就像是用相机拍摄并在屏幕上播放,而不是根据游戏玩家的输入(通常是游戏控制器、键盘按键或 iTV 遥控器)实时合成。

一个精灵将根据指示游戏如何运行的编程逻辑在屏幕上移动。精灵需要与场景图形中的背景图像和其他游戏元素以及其他玩家的精灵合成,因此用于创建精灵的 PNG32 图形需要支持透明背景。这也是为什么我讨论了遮蔽 2D 物体的主题——在你的游戏中用作精灵。

这也是为什么我在第二章中向你介绍了阿尔法通道透明度的概念,因为我们需要用精灵来达到同样的最终效果,这样我们才能在游戏中获得无缝的视觉体验。我们将在本书的后面介绍如何使用 GIMP 创建使用 alpha 通道的图形,这样你就可以创建专业级的精灵,与你的游戏图形无缝合成。

由于 3D 模型都在同一个渲染空间中,3D 渲染引擎会为您处理这个透明度因素,如果您使用 3D 模型而不是 2D 精灵,您不必担心游戏的每个 3D 组件都有 alpha 通道。接下来让我们看看 3D 模型,作为对这一点的跟进。

3D 模型:游戏角色扮演风格的基础

一种较新的电子游戏形式涉及实时渲染的虚拟世界,这要归功于 OpenGL ES 3 和 Vulkan 等实时渲染平台的出现,这些平台用于 Android、HTML5、JavaFX 和 DirectX (DirectX 用于 Xbox 和 Windows 等微软产品)。3D 模型提供了更加灵活、先进和逼真的游戏,因为它们将所有其他新媒体类型(除了数字音频)结合成一个;因为纹理、材质和着色器可以使用数字图像和数字视频;因为 3D 几何网格对象可以使用它们的实际几何来计算与实际对象的碰撞,并且是在三维空间而不是二维空间。此外,3D 素材可以对多种灯光和摄像机做出实时反应,您可以通过 i3D 获得一个更加强大的游戏设计环境,尽管从数学和 Java 编码的角度来看更加复杂。

由于 3D 和 i3D 非常复杂,因此有大量的优化考虑因素,例如优化网格(几何体),这是一个称为低多边形建模的过程,它涉及到少量使用点、边和面,然后使用平滑组来提供平滑的曲率,这也可以通过添加更多几何体来实现。在第二章中,我还提到了一些优化原则,比如使用更少的像素,更低的色深,以及更少的通道用于纹理贴图,也就是图像。同样,对于 3D 动画,在第二章中,我介绍了一些用于动画数据的优化原则,这类似于数字视频,例如使用更少的像素、更少的帧、更低的色深和更少的通道,以及更简单的插值算法。

3D 和 i3D 游戏的另一个优化与用于照亮虚拟世界的灯光数量有关。灯光计算往往很昂贵,以至于大多数游戏引擎,包括 JavaFX,都将允许的灯光数量限制在 8 个或更少。可以使用的灯光对象越少,渲染引擎需要进行的计算就越少,游戏运行的帧速率就越高(越快)。

同样的考虑也适用于 3D 动画软件中的相机。例如,当渲染到胶片时,可以根据需要渲染尽可能多的场景摄影机视图,而不会(实时)影响处理器。当消费电子设备处理 3D 时(相对于工作站的大规模渲染场),最大限度地减少摄像机的数量变得非常重要,因为每个摄像机都输出相当于未压缩的原始数字视频数据流。在这种情况下,您有一个 3D 渲染引擎实时生成另一个未压缩的 2D 动画(视频)资源,这再次占用了您的大量处理能力,因此仅当您的游戏绝对需要实时平视显示器(HUD)时才使用它,例如,用于实时第二游戏视角。

碰撞检测:游戏素材交互的基础

对于某些类型的游戏来说,游戏性的另一个重要组成部分或方面是碰撞检测,因为如果你的游戏元素只是在屏幕上飞过彼此,并且当它们相互接触或“相交”时从未做过任何酷的事情,那么你真的不会有一个很好的游戏!想象一下没有任何碰撞检测的弹球游戏或台球!一旦你添加了一个由相交逻辑处理例程组成的碰撞检测引擎,你的游戏将能够通过处理它们的几何组成部分(通常是边、线、曲线或它们的边界(BoundingBox)的相交来确定任何 2D 矢量精灵或 3D 模型何时相互接触或重叠。

碰撞检测将调用(即触发)相关的游戏逻辑处理例程,这些例程将确定当任何给定的 2D 精灵或 3D 模型(如投射物和主要角色)相交时会发生什么。例如,当投射物与主角相交时,伤害点可能会增加,生命力指数可能会降低,或者可能会启动垂死挣扎动画。另一方面,如果一个宝物与一个主要角色相交(也就是说,被一个主要角色拾起),力量或能力点可能会增加,生命力指数可能会增加,或者“我找到了”庆祝动画可能会开始。

正如你所看到的,根据你正在创建的游戏类型,游戏的碰撞检测引擎很可能是你游戏背后的基础设计元素之一,除了你的 i2D 精灵或 i3D 模型,它们代表了你的角色,投射物,宝藏,敌人,障碍和道具本身,这就是为什么我以这个顺序介绍这些。一旦检测到碰撞,通常你的物理模拟代码会被触发,向游戏玩家显示物体在碰撞后需要如何相互反应。接下来让我们来看看。

物理模拟:游戏真实感的基础

另一个重要的组成部分,或者说属性,是真实世界的物理模拟。像 JavaFX Interpolator类提供的重力、摩擦、反弹、阻力、风、加速、减速和运动曲线以及类似的力的添加,都将在已经非常逼真的精灵、同步动画序列、场景背景和高度精确的碰撞检测的基础上增加额外的真实感。

重力、摩擦力、阻力和风是在 Java 代码中模拟或调整 2D 精灵或 3D 模型运动的最简单的因素。我在我的书《Java 8 游戏开发入门》(Apress,2014)的结尾提到了这一点。反弹类似于运动曲线中加速和减速的数学计算,可用于模拟单次反弹,但不能模拟物理模拟的反弹衰减。

你可以编写自己的 Java 方法来将物理学应用到你的专业 Java 游戏中,或者你可以使用第三方 Java 库,这些库可以在 SourceForge、GitHub 或code.google.com等网站上获得。我将介绍一些用于 2D 和 3D 游戏引擎、2D 和 3D 物理模拟引擎、2D 和 3D 碰撞检测引擎、正向和反向运动学引擎等的库。我将在本章的下一个主要部分介绍这一点,以防万一你想在你的专业 Java 游戏或物联网应用中使用这些,而不是编写你自己的。

有趣的是,我将在本章的“Java 引擎:游戏、物理和逆运动学”部分介绍的大多数开源第三方物理引擎不仅实现了物理模拟,还实现了碰撞检测。这是因为这两件事在现实生活中紧密联系在一起。为了很好地处理碰撞,物理模拟需要无缝集成到代码中。在现实生活中,碰撞之前和之后都涉及物理学,这些引擎寻求重新创建现实生活中(完全可信)的实现结果。请注意,从 Java 9 开始,为了在新的 Java 9 模块系统中正确使用,这些产品需要进行“模块化”。

人工智能:游戏逻辑的基础

最后,您可以添加到游戏中的最专有的属性或逻辑结构(Java 代码)是自定义游戏逻辑,它使您的游戏在市场上真正独一无二。这种人工智能(AI)编程逻辑应该保持在自己的 Java 类和方法中,与物理模拟或碰撞检测代码分开。毕竟,Java 使模块化变得容易,这种游戏智能就像是你的职业 Java 游戏的裁判。它监督玩家,对手,障碍,宝藏,得分,处罚,等等,确保游戏体验每次对每个人都是一样的!这与体育赛事或比赛中裁判的职能相同。

有针对 Java 的第三方 AI 引擎;然而,我建议在这个领域,你可以从头开始编写你的游戏逻辑代码,以便它与你的用户界面(UI)代码、你的评分引擎代码、你的动画引擎代码、你的演员(精灵或模型)运动代码以及你的碰撞处理代码集成,这是任何第三方 AI 规则引擎都无法做到的。

当你开始把所有这些游戏组件加在一起,它开始让游戏更可信,也更专业。一个伟大游戏的关键目标之一是“暂停信仰”,这简单地意味着你的玩家百分之百地“买入”前提、角色、目标和游戏性。这是任何内容制作人,无论是电影制作人、电视剧制作人、作者、歌曲作者、游戏程序员还是应用开发人员,都追求的目标。如今,游戏具有与任何其他内容分发类型相同的创收能力,如果不是更多的话,并且您可以直接向公众分发它们,而无需电影工作室、唱片制作人或电视网络等中间人。这是最重要的部分,因为您将获得“70%您,30%商店”的分成,而不是“70%经销商,30%您”的分成!

Java 引擎:游戏、物理和逆运动学

有许多开源的第三方游戏引擎,物理和碰撞引擎,人工智能引擎,甚至反向(或正向)运动学(IK)引擎都可以很容易地在互联网上找到。大多数都在 SourceForge 或 GitHub 或code.google.com上,使用基本的谷歌搜索就能找到。大多数都是 JAR 格式的。

游戏引擎:JMonkey 和轻量级 Java 游戏库

LWJGL 是一个开源的跨平台 Java 库,对于 3D 图形(OpenGL)、3D 音频(OpenAL)和并行计算(OpenCL)应用的开发非常有用(图 4-1 )。API 访问是直接的、高性能的,而且还被包装在适合 Java 生态系统的类型安全层中。其他高级 3D 游戏引擎也可能使用 LWJGL。

A336284_1_En_4_Fig1_HTML.jpg

图 4-1。

The Lightweight Java Game Library 3 is an open source Java game library compatible with Java and JavaFX

JMonkey 也是一个免费的开源游戏引擎,面向想要创作 i3D 游戏的 Java 游戏开发者(图 4-2 )。该软件完全用 Java 编程,旨在提供广泛的可访问性和快速部署。

A336284_1_En_4_Fig2_HTML.jpg

图 4-2。

jMonkeyEngine 3.0 is an open source cross-platform game engine compatible with Java and JavaFX

值得注意的是,我将向您展示如何仅使用 Java (8 或 9)和 JavaFX 8 或 9 创建游戏,因为这正是本书的内容:使用并学习原生 Java APIs(其中一个是 JavaFX)来制作游戏或物联网应用。在我们开始之前,我想让您了解两个领先的 Java 游戏平台。

物理和碰撞引擎:Jbox2D,JBullet,Dyn4j,Jinngine

有许多第三方物理引擎也支持碰撞检测,因此您可以使用这些碰撞物理代码库中的任何一个将碰撞和物理同时添加到您的游戏中,只需将 JAR 文件导入到您的项目中并调用适当的 API 即可。

Jbox2D 是 Box2D C++物理引擎在 Java 中使用的“端口”或重新编码。Box2D 是一个开源的、基于 C++的物理和碰撞引擎,用于模拟 i2D (X,Y)空间中的刚体动力学。Box2D 由 Erin Catto 开发,在 zlib 许可下发布,不需要正式的使用确认;但是,如果您使用 C++ Box2D 中的 Jbox2D API 端口,建议您在您的 pro Java 游戏中使用 Box2D。

JBullet 是用于 Java 的 Bullet 2.7 3D C++物理引擎的部分端口。Bullet 2.87 是一个开源的 C++物理和碰撞引擎,用于模拟 3D (X,Y,Z)空间中的刚体动力学。子弹碰撞检测和物理库是由 Advanced Micro Devices 开发的,也称为 AMD。如果您想了解更多信息,可以在 http://bulletphysics.org 找到。JBullet 是在 zlib 许可下发布的,不需要正式的使用确认。但是,如果您使用来自 Bullet 2.7 C++物理引擎的 JBullet API 部分端口,建议您在您的 pro Java 游戏中使用 JBullet。

图 4-3 所示的 dyn4j 引擎是一个兼容 Java 6 和 7 的 2D 碰撞检测和物理引擎,这意味着它可以与本书中涉及的 Java 8 和 9 版本一起工作。Dyn4j 被设计为稳定、可扩展、优化(快速)且相对易于使用。Dyn4j 可免费用于商业用途,也可用于非商业用途。它是由它的作者 William Bittle 根据 BSD 许可模型授权的。

A336284_1_En_4_Fig3_HTML.jpg

图 4-3。

Dyn4j is an open source, 2D collision detection and physics engine that is available under the BSD license

Jinngine 是一个用 Java 编写的开源轻量级 3D 物理引擎,它为您提供实时碰撞,以及实时物理计算功能。用户可以通过调用 API 函数来指定几何图形、关节和参数,从而设置和模拟物理。摩擦是通过实现一个近似的共隆摩擦定律来模拟的。这个物理引擎只专注于碰撞和物理,没有渲染功能,并且使用基于速度的算法方法构建,该方法使用高效的 NCP 解算器进行求解。您可以使用 jinngine 作为您的物理引擎,也可以使用该引擎的其他组件,就像它是一个 Java 代码库一样,例如,如果您只想实现碰撞检测,或者如果您只想利用接触点生成功能。接下来,让我们看看反向运动学,或 IK 引擎,它在角色动画中用于定义骨骼结构及其关节运动限制。

反向运动学和机器人引擎:JRoboOp 和 JavaFX-IK

JRoboOp 是一个开源的 Java 库(包),设计用于 IK 机器人模拟和 3D 机器人模型的可视化。该引擎模拟机器人反向运动学以及机器人动力学,并基于名为 ROBOOP 的 C++库。这个库是由蒙特利尔理工大学的 Richard Gourdeau 开发的,它与 Java 5 和更高版本以及 JavaFX 1.3 和更高版本兼容,这意味着它可以很好地与我们将在本书中讨论的 Java 7、Java 8、JavaFX 9 和 Java 9 版本一起工作。这个包是在 GNU 公共许可证(GPL)下发布的。

JavaFX-IK 库是大约两年前专门为 JavaFX 创建的,可以在 GitHub 的 https://github.com/netopyr/javafx-ik 下载。它是在 Apache 许可证 2.0 版下授权的。IK 软件由德国弗莱堡的高级软件工程师 Michael Heinrichs 创建,它允许您使用骨骼对象在 JavaFX 场景图形中创建骨骼对象结构。

接下来,让我们看看可以创建的不同类型的游戏,以及这些游戏在应用精灵、碰撞检测、物理模拟和游戏 AI 逻辑等核心游戏组件方面有何不同。

游戏类型:益智游戏、棋盘游戏、街机游戏、射击游戏或虚拟现实游戏

就像我们在这一章中讨论的其他东西一样,游戏本身也可以用“静态对动态”的方法来分类。静态游戏不受“处理器限制”,因为它们往往是“基于回合”的,而不是“基于手眼协调”的,所以从某种意义上说,它们更容易顺利工作,因为只有游戏“规则”的编程逻辑和吸引人的图形需要到位和调试。一个重要的机会也存在于开发新类型的游戏类型,这些游戏类型以前所未见的创造性方式使用静态和动态游戏的混合组合。我自己也在研究其中的一些!

静态游戏:策略、知识、记忆和棋盘游戏

由于这是一本专业的 Java 游戏编程书籍,我将从这个重要的(对游戏开发而言)静态与动态的角度出发,将游戏分为三个独立的类别(静态、动态和混合)是一个非常巧妙的方法。先来涵盖静态(固定图形),回合制游戏。这些游戏包括“基于移动”或“回合制”的游戏,如棋盘游戏、益智游戏、知识游戏、记忆游戏和策略游戏,所有这些游戏的受欢迎程度和可销售性都不容小觑,尤其是在家庭游戏中。并非所有的游戏客户都是青少年男性,这类游戏也最有可能被用于寓教于乐,这是一个时下流行的词,教育和娱乐融合在一起,以进一步推动教育部分的成功。缺乏有趣、有效的教育内容游戏,因此这是一个重要的游戏商机。

关于静态游戏,需要记住的重要一点是,它们可以像动态游戏一样有趣。静态游戏本质上具有明显更少的处理开销,因为它们不必为了实现流畅、专业的游戏而达到 60 FPS 的实时处理目标。这是因为游戏的本质不是基于连续的运动,而是基于做出正确的战略行动。当轮到你的时候,你就会移动,这就是为什么这些类型的静态游戏通常被称为基于移动的游戏。

静态游戏中可能会涉及到一些基本的“碰撞检测”形式,即哪些游戏棋子被移动到了游戏棋盘或游戏面上的哪个位置。然而,对于静态游戏,不存在冲突检测使处理器过载的危险,因为游戏板的其余部分是静态的,只有一个棋子在特定玩家的回合中被有策略地移动。一旦确定碰撞的过程完成,就不需要(实时)碰撞检测,直到下一轮由单人玩家(在单人游戏中)或对手(在多人游戏中)进行。

策略游戏的处理逻辑更多的是基于策略逻辑的编程,在给定正确的移动顺序的情况下,允许玩家实现给定的最终“胜利”,而动态游戏编程逻辑更多地关注游戏精灵之间发生的冲突。动态游戏侧重于分数,分数是通过躲避射弹、寻找宝藏、降落目标、杀死敌人以及完成这些类型的关卡目标来获得的,以便进入下一个关卡,玩家可以获得更高的分数。

具有大量相关规则集的复杂策略游戏,例如国际象棋,甚至可能具有比动态游戏功能复杂得多的编程逻辑例程。然而,由于代码的执行没有那么时间敏感,因此无论平台和 CPU 多么强大,最终的游戏都将是流畅的,因为玩家愿意等待游戏来验证移动的有效性并在适当的情况下得分。当然,游戏规则集逻辑必须完美无缺,这种类型的游戏才能被视为真正的专业游戏。因此,最终,不管是静态的还是动态的游戏,至少是伟大的游戏,都很难编写代码,尽管原因大相径庭。接下来让我们来看看动态游戏,这种游戏往往有很高的知名度,吸引年轻玩家,而且往往是个人玩的,而不是团体、学生或家庭玩的。

动态游戏:街机、射击、平台和动作游戏

动态游戏可以被称为动作游戏或街机游戏,并且包括显示屏上的大量运动。这些高度动态的游戏几乎总是涉及射击,例如第一人称射击游戏(例如《毁灭战士》和《半条命》)以及第三人称射击游戏(生化危机和侠盗猎车手),或者偷窃东西或躲避可怕的东西。动作运动游戏,例如足球、英式足球、棒球、篮球、高尔夫和曲棍球,在动态游戏类型中也非常流行,并且它们几乎总是使用真实感、3D 虚拟世界或虚拟现实游戏模拟环境来创建。驾驶游戏是这种类型的另一个化身,也倾向于使用实时 i3D 游戏渲染技术为驾驶者提供超现实的驾驶模拟。

还有障碍课程导航范例,如在平台游戏如大金刚、吃豆人或超级马里奥兄弟中常见的。平台游戏往往是街机游戏,这是典型的 2D 或 2.5D,这是所谓的等距。街机游戏 ZAXXON 是 2D 等轴游戏的一个很好的例子,看起来像 3D 或 Tempest,其中几何形状爬上几何井,玩家从边上向下射击,以防止攀爬的形状到达顶部。

值得注意的是,任何类型的游戏都可以使用 2D 或 3D 图形资源制作,甚至可以使用 2D 和 3D 资源的组合制作,这是 JavaFX 9.0 所允许的,我称之为混合。

混合游戏:创造性地利用 JavaFX 的机会

从 JavaFX 场景图形素材的角度来看,混合游戏将是一个同时使用 2D 和 3D 素材的游戏,其中大部分我们在第 2 和 3 章中介绍过。还有另一种类型的混合,它可以跨越不同的游戏类型,这一点我们在前一节中已经介绍过了。有这么多受欢迎的游戏类型,总是有一个极好的机会通过使用混合游戏方式来创建一个全新的游戏类型。例如,想象一下从静态(战略)游戏类型(如棋盘游戏)中提取一些特征,并添加动态(动作)游戏类型的元素。这方面的一个很好的例子是国际象棋,当棋子进入对方的棋盘方格时,它们会战斗到死。

在我开始 Java 8 游戏开发时(Apress,2014),我使用 JavaFX 8.0 创建了一个混合游戏引擎,它支持平台游戏、射击游戏和寻宝游戏的属性。正如你在图 4-4 中看到的,在那本书中创建的 BagelToons InvinciBagel 游戏引擎,我在那里介绍了 i2D 游戏的开发,具有通常在不同类型的 2D 游戏中发现的元素,包括超级英雄、敌人、射击、宝藏、障碍、藏身处、建筑、汽车、景观、魔毯猫、安全破解、食物等等。

A336284_1_En_4_Fig4_HTML.jpg

图 4-4。

My i2D game development book called Beginning Java 8 Games Development covers using sprites to develop games

简而言之,Java 的定位是允许游戏开发者交付先锋的混合游戏,包含 2D 和 3D 素材,以及高质量的 16 位 48 kHz 和 24 位 48 kHz 数字音频。凭借一点点创造力和你在本书过程中积累的知识,你应该能够完成以前从未做过的事情。对于混合游戏将受益的领域来说尤其如此,例如教育领域(教育娱乐)和工作场所(业务流程游戏化)。这是因为 Java 广泛用于操作系统和浏览器,以及 64 位平台,如 Android 5 - 8,这些平台在消费电子产品中拥有大多数市场份额和制造商追随者。也就是说,需要指出的是,JavaFX(还)不适合具有高清或 UHD 高帧速率的 i3D VR 实时 3D 渲染游戏,比如那些使用 C++为 PlayStation 或 Xbox 等定制游戏主机创建的游戏。

摘要

在第四章中,我们仔细研究了一些更重要的游戏设计概念,我们将在我们的专业 Java 游戏开发工作流程中使用这些概念,这样你就可以在本书的第一部分提前了解这些东西的基础知识。

我首先介绍了静态与动态的关键概念,以及它们对游戏设计和游戏优化的重要性,因为如果游戏优化不是游戏设计、开发和优化过程中的一个持续考虑因素,过多的动态会使旧的单核甚至双核 CPU 过载。

接下来,您了解了游戏设计和开发的一些关键组件,如精灵位置、精灵动画、碰撞检测、物理模拟、群集或群体动力学、粒子系统、背景动画、相机动画、数字视频流、用户界面设计、评分引擎和游戏 AI 逻辑。

我们看了这些如何应用于静态游戏,即没有连续运动的游戏,如基于移动的策略游戏、棋盘游戏、谜题、知识游戏和记忆游戏,然后看了这些如何应用于动态游戏,即使用连续运动的游戏,如平台游戏、街机游戏、第一人称射击游戏、第三人称射击游戏、驾驶游戏、体育游戏、科幻游戏和类似的游戏,其中大量利用了 3D 实时渲染以及各种类型的物理系统和粒子系统模拟。

我们还看了一些最流行的第三方游戏引擎,物理(和碰撞)引擎,和反向运动学引擎。我们看了一些不同类型的游戏,以及它们的特点,这样你就可以发挥你的创造力,思考你想创建什么类型的专业 Java 游戏。

在下一章中,我们将看一看 Java 编程语言,并复习或入门,以确保每个人都了解 Java 编程语言 API 组件,如包、类、接口、方法、常量、变量、修饰符等。

五、Java 入门:Java 概念和原理介绍

让我们通过回顾 Java 编程语言背后的核心编程语言概念和原则,来确保我们所有的读者都在第五章的同一页上。重要的是,我们用这一章来给我们的读者一个 Java“入门”或全面的概述,并且在一章中简明地回顾编程语言。您在本书第一章中安装的 Java 9 JDK(和 JRE)将成为您的专业 Java 游戏和物联网应用以及 NetBeans 9 IDE 的基础。(我们将在下一章介绍 NetBeans,这样您就可以看到您将用来编写 Java 9 游戏或物联网应用的 IDE 如何作为代码编辑器和应用测试工具。)

我们将在本章中讨论的大多数核心 Java 结构和原则都可以追溯到 Java 编程语言的很久以前,大部分可以追溯到 Java 5(1.5)或 Java 6 (1.6)。我们还将介绍 Java 7 (1.7)和 Java 8 (1.8)(最新版本)中添加的功能,以及计划在 2017 年第三季度发布的 Java 9 (1.9)中的新功能。这些版本的 Java 在数十亿台设备上使用。Java 6 用于 32 位 Android 2.x、3.x 和 4.x 操作系统和应用;64 位 Android 5.x 和 6 操作系统及应用中使用 Java 7;Java 8 用于 Android 7 到 8,以及流行的操作系统(包括微软 Windows、苹果 Macintosh、Open Solaris 和大量流行的 Linux 发行版,如 SUSE、Ubuntu、Mint、Fedora 和 Debian);Java 9 现在已经面向大众发布了。

随着本书的深入,您当然会学到 Java 8 的新的高级概念,如 Lambda 表达式,以及 Java 8 和 Java 9 组件,如 JavaFX 多媒体引擎。本章将涵盖最基本的 Java 编程语言概念、技术和原则,涵盖目前在计算机、iTV 和手持设备上广泛使用的五个主要 Java 版本。

我们将从最简单的概念开始,逐步深入到更难的概念,因此我们将从 Java 的最高级别开始,即 API 及其模块,然后逐步深入到这些模块中 Java 编程结构的“实际操作”部分,包括包、类、接口、方法、常量和变量。

在进入 Java 的结构部分(如包、类和方法)之前,您将了解一下 Java 语法,包括什么是 Java 关键字,如何界定 Java 编程结构,以及如何在 Java 代码中添加函数注释。然后,我们将介绍应用编程接口(API)的顶级概念,什么是包,以及如何导入和使用作为 API 一部分的 Java 包提供的现有代码,以及如何创建包含您自己的游戏和物联网应用代码的自定义 Java 包。

您将看到保存在 Java 包中的构造,这些构造被称为 Java 类。您将了解类包含的方法、变量和常数;关于什么是超类和子类;以及什么是嵌套类和内部类以及如何使用它们。最后,您将了解 Java 对象以及它们如何构成面向对象编程(OOP)的基础。您将了解什么是构造函数方法,以及它如何使用一种特殊的方法创建 Java 对象,这种方法与包含它的类同名。

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

关于编写 Java 语法,您需要马上理解几件事情。语法控制着 Java 如何“解析”关于编程语言的东西。解析代码语法可以让 Java 理解你想对你的编程逻辑做什么。理解主要的语法规则很重要,因为它们允许 Java 编译器理解你是如何构造 Java 代码的。Java 编译是 Java 编程过程的一部分,JDK 编译器(程序)将 Java 代码转换成字节码。这由安装在最终用户计算机系统上的 JRE Java 运行时引擎执行(运行)。这个 Java 编译器需要知道你的代码中哪些部分是 Java 编程逻辑,哪些部分是对自己的注释(或者对你项目编程团队其他成员的注释);Java 代码块开始和结束的位置;在这些 Java 代码块中,您的 Java 编程语句或指令开始和结束的地方。一旦编译器明白了这一点,它就可以解析语句,并把它们从代码转换成字节码。

让我们从评论开始,因为这个话题是最容易把握的。向 Java 代码添加注释有两种方法:单行或行内注释,可以放在每一行 Java 代码逻辑之后;多行或块注释,可以放在一行 Java 代码或一个 Java 代码块(Java 代码结构)之前(或之后)。

单行注释用于添加关于一行 Java 代码或一条 Java 编程语句正在做什么的注释。这个注释解释了那行 Java 代码在您的整个代码结构中要完成什么。Java 中的单行注释将以双正斜杠字符序列开始。例如,如果你想在你将在第六章创建的 BoardGame bootstrap 代码中注释你的一个 import 语句,你可以在代码行后添加两个正斜杠。这是您的 Java 代码行经过单行注释后的样子;在 NetBeans 右下角的图 5-1 中也有显示:

A336284_1_En_5_Fig1_HTML.jpg

图 5-1。

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

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

让我们也看看多行注释,显示在图 5-1 的顶部,在package invincibagel语句的上方,我们将在本章的下一节学习。如您所见,块注释的处理方式不同,使用星号旁边的单个正斜杠开始注释,与此相反,使用星号然后使用单个正斜杠结束多行注释(这种注释也称为块注释)。这是在你的职业 Java 游戏中添加短(单行)或长(多行)注释的两种方式。

需要注意的是,不能“嵌套”多行注释。简单地使用一个更大的多行注释!

如果你想知道,这个 InvinciBagel 项目是我在《Java 8 游戏开发入门》一书中教读者如何创建的 i2D 街机游戏,这本书是我为一个介绍使用 Java 8 和 JavaFX 8 开发 i2D 游戏的出版社写的。那本书中的所有原则都适用于 Pro Java 9 游戏开发,所以我在这里使用这些代码。

我通常将单行注释排成一行,看起来相当有序。块注释的 Java 惯例是将星号排成一行,在开始注释分隔符中有一个星号,在结束注释分隔符中有一个星号。这显示在 NetBeans 中 InvinciBagel.java 代码编辑器选项卡顶部的图 5-1 中。

还有第三种类型的注释,称为 Javadoc 注释,在本书的专业 Java 游戏开发中不会用到,因为代码是用来创建游戏的,不会向公众发布。如果您打算编写一个 Java 游戏引擎,供其他人用来创建游戏,这就是您使用 Javadoc 注释向您的 pro Java 游戏引擎添加文档的时候了。JDK 有一个 Javadoc 工具,用于处理 Javadoc 注释并将它们添加到 NetBeans 9 IDE 中。Javadoc 注释类似于多行注释,但它使用两个星号字符来创建 Javadoc 注释的开始分隔符,正如我在这里所做的:

/** This is an example of the Java Documentation (Javadoc) type of Java code commenting
     This is a type of comment that will automatically generate your Java documentation!

*/

如果您想在 Java 语句或编程结构的正中间插入一个注释,作为一名专业的 Java 游戏开发人员,您绝对不应该这样做,您应该使用多行注释格式,如下所示:

import  /* This line of code imports the Stage class */  javafx.stage.Stage;

这不会产生任何错误,但是会让你的代码读者感到困惑,所以不要用这种方式注释代码。但是,下面使用单行注释格式对此进行注释的方式会产生错误:

import  // This line of code will not successfully import the Stage class  javafx.stage.Stage;

这是因为编译器将只看到单词 import,因为该单行注释解析到行尾,而多行注释使用块注释分隔符序列(星号和正斜杠)专门结束。出于这个原因,Java 编译器会对第二个未正确注释的代码抛出一个错误,实质上是问“导入什么?”因为不能导入任何东西,所以必须从 Java 包中导入一个 Java 类。

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

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

package invincibagel;

我们将在下一节讨论 API 和包,以及如何使用 import 语句访问它们。导入语句也用分号字符分隔(也如图 5-1 所示)。import 语句以 import 关键字、要导入的包和类开始,最后是分号分隔符,如下面的 Java 编程语句所示:

import javafx.application.Application;

接下来我们应该看看花括号{…}。像多行注释分隔符一样,花括号具有一个左{花括号,它界定(或向编译器显示)一组 Java 语句的开始,以及一个右}花括号,它界定(或向编译器显示)一组 Java 编程语句的结束。花括号允许您将 Java 编程语句嵌套在其他 Java 结构中。在本书中,我们将会经常讨论嵌套 Java 结构。

正如你在图 5-2 中看到的,用这些花括号分隔的 Java 代码块可以相互嵌套(包含),允许更复杂的 Java 代码结构。图 5-2 显示了你的类中第一个(最外面的)使用花括号的代码块。里面是你的start()方法,里面是你的。setOnAction()方法调用,其中有一个 handle()方法定义。随着本章的进行,我们将看看所有这些 Java 代码都做了什么。我现在想让你想象的是,这些花括号是如何允许你的方法(和类)定义它们自己的代码块(结构)的,每个代码块都是一个更大的 Java 结构的一部分,最大的 Java 结构是 InvinciBagel 类。每个左花括号都有一个匹配的右花括号,还要注意代码的缩进,这样最里面的 Java 代码结构向右缩进得最远。每个 Java 代码块都缩进了额外的四个字符或空格。如您所见,该类没有缩进(零),start()方法在。setOnAction()方法在中是八个空格,handle()方法在中是十二个空格。请注意,NetBeans 9 会为您缩进每个 Java 代码结构。

A336284_1_En_5_Fig2_HTML.jpg

图 5-2。

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

请注意,每个红色方块中的嵌套 Java 代码以花括号开始,以花括号结束。现在,您已经熟悉了各种 Java 代码注释方法,以及 Java 编程语句需要如何定界(包括单独的和作为 Java 代码块),接下来您将了解各种 Java 代码结构。您将看到如何使用它们,它们能为您的应用和游戏做些什么,以及为了实现您的 Java 编程结构使用了哪些重要的 Java 关键字。

Java 包:按函数组织 Java API

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

要安装给定版本的 API 级别,您需要安装它的软件开发工具包(SDK)。Java SDK 有一个特殊的名字,Java 开发工具包(JDK)。熟悉 Android 开发(Android 实际上是 Linux 操作系统之上的 Java)的人都知道,每次添加一些新特性时,都会发布不同的 API 级别。这是因为运行 Android 的硬件设备增加了需要支持的新硬件功能,而不是因为谷歌想每隔几个月发布一个新的 SDK。Android 在短短几年内发布了超过 26 个不同的 API 级别,而 Java SE 在十多年里只发布了 9 个。目前,在数十亿台消费电子设备中,只有四个 Java API 级别(Java 6、7、8 和 9)在积极使用。

Java 6 与 Eclipse 的 ADT IDE 一起用于开发 32 位 Android(版本 1.5 到 4.4),Java 7 与 Android Studio 一起用于开发 64 位 Android(版本 5.x、6、7.x),Java 8 与 IntelliJ IDE 一起用于开发 Android Studio 3.0,Java 9 跨 Windows、Macintosh、Linux 和 OpenSolaris 操作系统使用。我有三个不同的工作站,分别针对 Java API 平台和 IDE 软件包进行了优化,这样我就可以同时为 32 位 Android 设备(Java 6)、Android 5 到 6 (Java 7)、HTML5 和 Android 7 到 8 (Java 8)以及 JavaFX 9 (Java 9)开发应用。幸运的是,你可以在 www.PriceWatch.com 花几百块钱得到一个强大的 Windows 10 或者 Ubuntu LTS 18 六核(或者八核)64 位 pro Java 9 游戏开发工作站。

除了 API 级别(您安装并正在使用的 SDK),Java 编程语言中最高级别的结构是一个包。Java 包使用 package 关键字在 Java 代码的顶部声明您自己的应用包。正如你将在第六章中看到的,这需要是声明的第一行代码而不是注释(并且在本章前面的图 5-1 中显示)。只能有一个包声明并且只能声明一个包,而且必须是第一个 Java 语句!您将在第六章中使用的 NetBeans 9 中的新项目系列对话框将为您创建您的包,并根据您希望在应用中执行的操作导入您需要使用的其他包。在我们的例子中,这些将是 JavaFX 9 包,因此我们可以利用 JavaFX 新媒体引擎。Java 9 进一步将包分组为模块,这些模块被添加到主 Java 程序逻辑之外的应用中。

正如您可能已经从名称中确定的那样,Java 包捆绑了您将在本章中学习或复习的所有 Java 编程结构。这些包括与您的应用相关的类、接口和方法,因此 gameboard 包将包含您的所有代码,以及您为使用代码而导入的所有代码,这是创建、编译和运行您的棋盘游戏所需要的。接下来我们将研究导入的概念和 Java import 关键字,因为它与包的概念密切相关。

Java 包对于组织和包含您自己的所有应用代码很有用,但是对于组织和包含 SDK(API)的 Java 代码更有用,您将利用这些代码和您自己的 Java 编程逻辑来创建您的专业 Java 游戏或物联网应用。从 Java 9 开始,Java 包现在将由功能模块来组织,我们将在本章末尾介绍这些功能模块,因为模块不会影响你的 Java 游戏编程逻辑;它们只是在一个高层次上组织事情,允许您优化您的发行版,以便您可以为您的 Java 游戏发行版获得最小的下载量,以供您的目标游戏终端用户使用。

通过使用 Java import 关键字,您可以使用作为您正在开发的 API 的一部分的任何类,该关键字与您想要使用的包和类一起被称为 import 语句。这个 import 语句以 import 关键字开始,接下来是包和类引用路径(完整的专有名称),然后需要使用分号终止该语句。如图 5-1 所示,用于从 javafx.event 包中导入 JavaFX EventHandler 类的导入语句应该如下所示:

import javafx.event.EventHandler;

import 语句通知 Java 编译器,它需要将指定的外部包放入您的包中(将它导入到您的包中),因为您将使用 import 关键字引用的类中的方法(和常量),以及它存储在哪个包中。如果您在自己的 Java 9 类中使用一个类、方法或接口,比如您将在第六章中创建的 BoardGame 类,并且您没有通过使用 import 语句声明该类,Java 9 编译器将抛出一个错误。这是因为它无法定位或引用要在您的包中使用的类,因此它无法导入该功能。

Java 类:模块化游戏的 Java 结构

包级别下的第二大 Java 编程结构是 Java 类级别,正如您在 import 语句中看到的那样,它引用包含类的包和类本身。就像一个包组织所有相关的类一样,一个类组织所有相关的方法、变量和常量,有时还包括其他嵌套类,我们将在本章的下一节讨论这些。

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

Java 类也可以用来创建 Java 对象,我们将在了解类、嵌套类、方法和数据字段之后讨论这些。Java 对象是使用 Java 类构造的。它们与 Java 类以及该类的构造函数方法同名,这一点我们将在本章稍后介绍。

正如你在图 5-2 中看到的,你使用 Java class 关键字和你的类名来声明你的类。你也可以用 Java 修饰关键字作为声明的开头,我们将在本章的后面介绍。Java 修饰符关键字总是放在 Java class 关键字之前,使用以下格式:

<Java modifier keywords here> class <your custom class name goes here>

Java 类的一个强大特性是,它们可以用来模块化您的 Java 游戏代码,这样您的核心游戏应用特性就可以成为一个高级类的一部分,可以对该类进行子类化,以创建该类的更专业的版本。用 Java 类层次术语来说,一旦一个类被用来创建一个子类,它就变成了超类。一个类通常会使用 Java extends 关键字子类化另一个超类。

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

<Java modifier keywords here> class <your class name here> extends <superclass name here>

当您使用您的类扩展一个超类时,该类成为该超类的子类,您可以在您的子类中使用该超类的所有特性(嵌套类、内部类、方法、构造函数、变量和常量)。您可以做到这一点,而不必在类的主体中显式地重写(重新编码)这些 Java 构造,这将是多余的(并且是无组织的),因为您的类扩展了超类,使其成为您的类的一部分。我们将在本章的下一节讨论嵌套类和内部类,以防你想知道它们是什么。

您的类的主体在花括号内编码(图 5-2 中外部的红色框),它跟在您的类和 javafx.application.Application 超类(在这种特殊情况下)声明之后。这就是为什么你要先学习或复习 Java 语法;您在此基础上构建了类声明,然后是包含类定义(变量、常量、方法、构造函数、嵌套类、内部类)结构的 Java 语法。

注意图 5-2 中 InvinciBagel 类从 JavaFX 应用包中扩展了 Application 超类。这样做为 InvinciBagel 类提供了托管或运行 JavaFX 8 应用所需的一切。这个 JavaFX 8 应用类所做的是“构造”您的应用对象,以便它可以使用系统内存,调用。init()方法(初始化任何可能需要初始化的东西),并调用。start()方法,如图 5-2 (第二个红框中)。这个。start()方法是放置 Java 代码语句的地方,这些语句最终将被用来“启动”(即启动或运行)InvinciBagel i2D arcade game Java 8 应用。这款 Java 8 的游戏也将在 Java 9 下运行,无需修改。

当最终用户使用完 i2D InvinciBagel Java 应用时,应用类使用 Application()构造函数方法创建的应用对象将调用它的。stop()方法并从系统内存中移除您的应用。这将释放内存空间,供最终用户用于其他用途。我们将很快进入方法、构造器和对象,因为我们正在从高级包和类构造进展到较低级的方法和对象构造,这样我们就可以在前进的过程中从高级概述到较低级来处理学习过程。您可能想知道 Java 类是否可以相互嵌套。也就是说,Java 类可以包含其他 Java 类吗?答案是肯定的,他们可以。接下来让我们仔细看看 Java 嵌套类的概念。

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

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

静态嵌套类,我称之为嵌套类,用于创建包含它们的类所使用的实用程序,有时只是用来包含包含它们的类所使用的常量。开发 Android 应用的人都熟悉嵌套类,因为它们在 Android API 中非常常用,要么用于保存实用方法,要么用于包含 Android 常量,这些常量用于定义屏幕密度设置、动画运动插值曲线类型、对齐常量和用户界面元素缩放设置等。在第四章中,我们讨论了与游戏相关的静态概念,对于代码来说,这具有相同的含义。Java 常量可以被认为是固定的,或者不能被改变。

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

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

类似地,构造函数方法(我们将很快学习构造函数方法)将被写成应用。Parameters(),因为构造函数方法必须与包含它们的类同名。除非您正在编写供其他开发人员使用的代码,这是最常使用嵌套类的地方,如 JavaFX 应用类或 Android 8 操作系统中的许多嵌套类(实用程序或常量提供者),否则您更有可能使用非静态嵌套类。这些非静态嵌套类通常被称为 Java 游戏的内部类。

一个嵌套类,技术上称为静态嵌套类,是使用 static 关键字(修饰符)来声明的,这一点你将在本章的后面部分学到。所以,如果你要创造棋盘游戏。SplashScreen 嵌套类、BoardGame 类和 SplashScreen 嵌套类声明类似于以下代码:

public class BoardGame extends Application {
    static class SplashScreen {
        // The Java code that creates and displays your splashscreen is in here
    }
}

需要注意的是,如果您使用 import Java FX . Application . Application . Parameters(作为示例)来导入嵌套类,您可以在此时引用您的类中的嵌套类,只需使用参数类名,而不必使用完整的类名“path ”,该类名显示了您的类的代码如何通过其父类到达使用应用的嵌套类。参数(类名。NestedClassName)引用。

正如你将在本书中多次看到的,方法也可以用点符号来访问。因此,不使用类名。如果使用 import 语句导入这个嵌套类,只需使用 NestedClassName.MethodName 即可。这是因为 import 语句已经用于建立这个嵌套类的完整“引用路径”,通过它的包含类,所以您不必这样做。

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

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

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

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

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

public class BoardGame extends Application {
    private class SplashScreen {
        // The Java code that creates and displays your splashscreen is in here
    }
}

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

public class BoardGame 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 BoardGame extends Application {
    public class SplashScreen {
        // The Java code that creates and displays your splashscreen is in here
    }
}

您还可以声明一个内部类来保护,这意味着它可以被父类或包含类的任何子类访问。在我们讲述了 Java 方法和 Java 变量之后,我们将进入 Java 修饰符。

如果您在不是类的低级 Java 编程结构(如方法或迭代控制(通常称为循环)结构)中声明了一个类,从技术上来说,它被称为局部类。这个局部类只在代码块内部可见,因此它不允许使用(或者说不允许使用)类修饰符,比如 static、public、protected 或 private。

局部类的用法类似于局部变量,只是它是一个更复杂的 Java 编码结构,而不是一个简单的局部使用的数据字段值。这在游戏中并不常用,因为你通常希望你的游戏被“功能性地”划分成功能类,这些功能类中的方法和变量显然有不同的用途和原因,以保持使用 Java 的组织或封装来清晰地定义游戏设计和处理的复杂性。从第六章开始,我们将在整本书的每一章中设计不同的游戏功能组件。通过这种方式,我们最大限度地利用 Java 的特性来创建游戏设计。

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

Java 方法:核心逻辑函数 Java 构造

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

声明方法:修饰符、返回类型和方法名

方法声明以访问控制修饰符关键字开始,可以是 public、protected、private 或 package private(这是通过根本不使用任何访问控制修饰符关键字来指定的)。如图 5-2 所示,你的。start()方法已使用公共访问控制修饰符声明。我们将在本章后面更详细地讨论访问修饰符关键字。

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

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

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

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

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

您可以使用数据类型和参数名称对来提供任意多的参数,每对之间用逗号分隔。方法也可以没有参数,在这种情况下,参数括号是空的,左括号和右括号紧挨着;这就是我在本书中写方法名的方式,这样你就知道它们是方法。我在方法名前面使用了点号(符号),后面使用了括号字符,比如。开始()或。stop()等等,这样你就知道我引用的是一个 Java 方法。

定义您的方法的编程逻辑将包含在方法的“主体”中,正如您已经了解的,主体在定义方法开始和结束的花括号中。方法中的 Java 编程逻辑可以包括变量声明、程序逻辑语句、迭代控制结构和迭代循环等,所有这些我们都将在本书中用来创建我们的 Java 游戏。

重载方法:提供唯一的参数列表

Java 中还有另一个适用于方法的概念,在我继续之前,我将在本节中介绍它,这就是重载 Java 方法。重载 Java 方法特指使用相同的方法名,但使用不同的参数列表配置。重载意味着如果你定义了不止一个同名的方法,Java 编译器将能够判断出使用哪个重载的方法。

Java 编译器通过查看参数数据类型以及它们被传递给被调用方法的顺序来区分重载方法。然后,Java 编译器使用参数列表的唯一性作为各种类型的指纹,来辨别要使用哪个同名的方法(具有相同的名称)。因此,为了使 Java 方法重载特性能够正确工作,您的参数列表配置必须彼此完全不同。

在本书的过程中,我们将学习如何使用和如何编写 Java 方法,从介绍 NetBeans 9 的第六章开始,一直到本书的结尾,所以我在这里不打算花太多时间在它们上面,只是定义它们是什么以及它们在 Java 类中如何声明和使用的基本规则。

构造器方法:将 Java 类转换成 Java 对象

在本章的这一节,我将详细介绍一种特殊类型的 Java 方法,称为构造函数方法。这是一种特殊类型的方法,可用于创建(构造)Java 对象,我们将在本章稍后介绍,在我们介绍了可用于创建、定义和连接这些 Java 对象的所有不同类型的 Java 语法和编程结构之后。Java 对象恰好是面向对象编程(OOP)的基础,所以我们将在这里看一下构造器方法;在本章后面讨论 Java 对象本身之前,理解这一点是很重要的。因为我们在本节中讨论方法,所以这是研究构造函数最合理的地方,因为构造函数方法有时被资深 Java 游戏开发人员(简称为)调用,而您正在成为这样的开发人员。

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

一个 Java 类可以包含一个与该类同名的构造函数方法,并且可以使用该类创建 Java 对象。构造函数方法使用它的 Java 类作为蓝图,在系统内存中创建该类的实例,从而创建 Java 对象。构造函数方法总是返回一个 Java 对象,因此不使用其他方法通常使用的任何其他 Java 返回类型(void、String、float、int、byte 等。).我们将在本章后面讨论这些 Java 返回类型。因为您正在创建一个新的 Java 对象,所以应该使用 Java new 关键字调用构造函数方法。

您可以在图 5-2 的第 20、28 和 30 行中的引导 JavaFX 代码中看到这样的例子。这些行是通过使用以下对象声明、命名和创建 Java 代码结构分别创建 Button、StackPane 和 Scene 对象的地方,如下所示:

<Java class name> <object instance name> =

                             new <Java constructor method name><parameter list><semicolon>

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

以当前 Java 代码第 20 行中的按钮对象创建为例,您使用 equals“运算符”左侧的 Java 语句部分告诉 Java 语言编译器,您想要使用 JavaFX 按钮类作为对象蓝图来创建名为 btn 的按钮类型对象。这“声明”了按钮类(对象类型)并给它一个惟一的名称。(在本章的稍后部分,我们将很快介绍操作符。)

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

实例化 Java 对象的方法是调用或利用 Java new 关键字和对象构造器方法调用。因为这发生在 equals 操作符的右侧,所以对象实例化的结果被放入声明的对象中,该对象位于 Java 语句的左侧。当我们在本章后面讨论操作符时,你会看到,这就是等于操作符的作用,它是一个有用的操作符。

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

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

Button btn;          // Declare a Button object named btn
btn = new Button();  //  Instantiate btn object using Java new keyword and Button() constructor

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

如果在类内部声明一个对象,因此在类中包含的所有方法之外,类中的所有方法都可以访问(查看和使用)该对象。类似地,在一个方法中声明的任何东西对该方法来说都是“局部的”,只对该方法的其他“成员”是“可见的”,这意味着该方法范围内的所有 Java 语句都在{…}分隔符内。如果您想在类中实现这个单独的对象声明,在。start()方法,在当前的 BoardGame 类中,您的类的前几行 Java 代码将变成如下 Java 编程逻辑:

public class BoardGame extends Application {
     Button btn;

     @Override
     public void start(Stage primaryStage) {
        btn = new Button();

        btn.setText("Say 'Hello World'");
        // other programming statements continue here
     }
}

当对象声明和实例化被分开时,可以根据可见性的需要将它们放在方法的内部(或外部)。在前面的代码中,BoardGame 类的其他方法可以调用前面显示的 btn.setText()方法调用,而 Java 编译器不会“抛出”错误。图 5-2 中按钮对象的声明方式,只有。start()方法可以“看到”对象,所以只有。start()方法可以实现 btn.setText()方法调用。

创建构造器方法:设计和编码 Java 对象结构

构造函数方法是一种特殊类型的方法,用于在系统内存中创建对象。这与其他方法有很大的不同(如果您使用不同的编程语言,您习惯于将它们称为函数)。Java 中的非构造方法用于执行某种复杂的计算或某种形式的封装(模块化)处理。constructor 方法用于在内存中创建 Java 对象,而不是执行其他一些编程功能,Java new 关键字与 constructor 方法的结合使用就证明了这一点,constructor 方法在内存中创建该唯一类类型的新 Java 对象。因此,构造函数方法将定义一个独特类型的 Java 对象的内部结构。如果希望在实例化 Java 对象的同时配置它,可以定义构造函数方法参数列表,以允许调用实体用特定(自定义)数据值填充对象结构。这样,您可以通过在构造函数的参数列表中传递不同的属性来创建不同类型的对象。

在这一节中,我们将创建几个示例构造函数方法,向您展示关于如何创建构造函数方法的基础知识以及它通常包含的内容。假设您正在为游戏创建一个对象。您可以使用以下 Java 代码结构声明一个公共 BoardGame()构造函数方法,例如:

public BoardGame() {
    int     healthIndex = 1000;     // Defines units of Health

    int     scoreIndex  = 0;       //  Defines units of Scoring

    int     boardIndex  = 0;      //   Current Game Board Location

    boolean turnActive  = false; //    Flag showing if current turn

}

使用BoardGame playerName = new BoardGame();构造函数方法调用调用的构造函数方法创建了一个名为 playerName 的 BoardGame 游戏玩家对象。该对象具有 1000 单位的生命值,没有当前分数,因为该对象在游戏板的第一个方块上,并且当前没有移动,因为当前没有轮到他们。

接下来,让我们探索一下重载这个构造函数方法的概念,我们在前面已经学过了,并创建另一个构造函数方法,它具有允许我们在创建 BoardGame 对象的同时定义 healthIndex 和 turnActive 变量的参数。构造函数方法如下所示:

public BoardGame(int startingHealthIndex, boolean isTurnActive) {
    int healthIndex = startingHealthIndex;

    int scoreIndex;
    int boardIndex;
    boolean turnActive = isTurnActive;

}

在这个版本中,我仍然将 scoreIndex 和 boardIndex 变量初始化为零,这是一个整数值的默认值,所以我不必在这段代码中使用 lifeIndex = 0 或 hitsIndex = 0,只是为了向您展示编写这两个语句的可选方法。由于 Java 编程语言支持方法重载,如果您使用BoardGame playerOne = new BoardGame(1250, true);方法调用来实例化一个 BoardGame 对象,将会使用正确的构造函数方法来创建该对象。这个名为 playerOne 的 BoardGame 对象的健康指数为 1250 个健康单位,得分为零,位于第一个游戏棋盘位置,目前轮到他们了。

Java 关键字this可用于访问使用构造函数方法创建的数据字段。例如,在对象的代码中,this . startinghealthindex = value;将该对象自己的内部数据字段设置为您指定的值。您还可以使用 this()在同一个类构造中调用另一个构造函数方法。

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

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

下一层,从 API 到包到类到方法,是在这些 Java 类和方法中操作的实际数据值。在 Java 中,这被称为数据字段。数据保存在称为字段的东西中,就像在数据库设计中一样。Java 数据字段可以是动态的,也可以是可变的,这就是为什么它们通常被称为“变量”,并且可以在 Java 游戏或物联网应用的操作过程中发生变化。或者,它们可以是静态的(固定的),这使得数据是永久的,在这种情况下,它将被称为常数。常量是一种特殊类型的变量,我们将在下一节中讨论,因为在 Java 编程语言中正确地声明一个常量比声明一个 Java 变量要复杂(高级)一些。

就 Java 行话(约定)而言,在类的顶部声明的变量被称为成员变量、字段或数据字段,尽管所有的变量和常量都可以被认为是基本级别的数据字段。

在方法或其他低级 Java 编程结构(嵌套在类或方法中)内部声明的变量称为局部变量,因为它只能在用花{…}括号分隔的编程结构内部局部“看到”或使用。最后,在方法声明、构造函数方法定义或方法调用的参数列表区域内传递的变量被称为参数,这并不奇怪。

变量是保存 Java 对象或软件属性的数据字段,在软件执行过程中,这些属性会发生变化。正如你所想象的,这对于游戏编程来说尤其重要。最简单的变量声明形式可以通过使用一个 Java 数据类型关键字以及您希望在 Java 程序逻辑中用于该特定变量的名称来实现。在上一节的构造函数方法中,我们声明了一个名为 scoreIndex 的整数变量来保存你的对象在游戏过程中累积的分数。我们定义了变量数据类型,并使用以下 Java 变量声明编程语句对其进行命名:

int scoreIndex; // This could be coded as: int scoreIndex = 0; (default integer value is zero)

正如您在上一节构造函数方法中看到的,您可以使用 equals 运算符将变量初始化为一个初始值,以及一个与声明的数据类型相匹配的数据值。这里有一个例子:

boolean turnActive = false; // Could be: boolean turnActive; (default boolean value is false)

此 Java 语句声明了一个布尔数据类型变量,并将其命名为 turnActive,位于 equals 运算符的左侧,然后将声明的变量设置为 false 值,这表示该玩家的回合未激活。这类似于对象的声明和实例化,只是 Java new 关键字和构造函数方法被数据值本身所取代,因为现在声明的是变量(数据字段),而不是创建的对象。我们将在本章的下一节讨论不同的数据类型(我们已经讨论过整数、布尔和对象)。

你也可以在变量声明中使用 Java 修饰符关键字,我将在本章的下一节向你展示如何声明一个不可变的变量,也称为常量,它在内存中是固定的或锁定的,不能以任何方式改变或更改,所以它保持不变,你猜对了。

我们将在下一节常量之后的小节中讨论 Java 访问修饰符关键字,因为它们与所有 Java 构造都相关。所以,现在我几乎已经完成了从最大的 Java 构造(或包)到最小的(或数据字段)的讨论,我们将开始讨论那些适用于 Java 所有级别(类、方法、数据字段)的主题。这些 Java 概念的复杂性会随着我们阅读 Java 初级章节的结束而增加,因为我想从更简单的高级概念开始,然后深入到更复杂的低级概念。在本章的最后,我们还将介绍如何使用新的 Java 9 模块特性来打包您的 Java 项目以供分发,这将允许您优化您的 Pro Java 9 游戏的数据占用,并使其更加安全。Java 9 应该在这本书向公众发布的同时发布,所以我把这本书变成了一本 Java 9 的书。Java 8 游戏开发入门书中的所有内容仍然适用于 Java 9 开发。

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

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

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

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

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

final int SCREEN_HEIGHT_PIXELS = 480;

final int SCREEN_WIDTH_PIXELS = 640;

还有一个“空白”final,它是一个非静态的 final 变量,其初始化将被推迟到您的构造函数方法体。同样重要的是要注意,每个对象都有自己的非静态最终变量的副本。

如果您希望由您的类的构造函数方法创建的所有对象能够“看到”并使用该常量,您还必须在最终的修饰符关键字之前添加 Java static 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 类),能够看到这些常量,您可以使用下面的 Java 代码在 static modifier 关键字之前使用 Java public modifier 关键字来声明这些常量:

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 保留字,因为结构的 Java 逻辑(至少对于类和方法来说)包含在大括号{…}分隔符内,这些分隔符位于类关键字和类名之后,或者方法名和参数列表之后。修饰符关键字出现在所有这些关键字之前,可以与 Java 类、方法、数据字段(变量和常量)以及 Java 接口一起使用,我们将在稍后讨论这些。

正如您在图 5-2 底部看到的。main()方法,它是由 NetBeans 9 为 BoardGame 类定义创建的(它使用了我们将在下面介绍的 public 修饰符),您可以使用多个 Java 修饰符关键字。那个。main()方法首先使用一个公共修饰符关键字,这是访问控制修饰符关键字,然后它使用一个静态修饰符关键字,这是非访问控制修饰符关键字。接下来让我们讨论 Java 访问控制修饰符,之后,我们将讨论更复杂的非访问控制修饰符。随着 Java 9 中由 Java 模块特性提供的额外安全保护,这些访问控制修饰符变得更加重要,Java 模块特性控制您的包和 API 是如何捆绑和分发的。

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

让我们先讨论访问控制修饰符,因为它们首先在任何非访问控制修饰符关键字和任何返回类型关键字之前声明;它们在概念上也更容易理解。有四种访问控制修饰符级别可以应用于任何 Java 编程结构。如果您没有声明任何访问控制修饰符关键字,那么一个“默认”的包私有访问控制级别将被应用于该 Java 代码结构,这允许它对您的 Java 包内的任何 Java 编程结构“可见”,从而可供其使用。在这种情况下,这将是棋盘游戏包。

其他三个 Java 访问控制修改级别都有自己的访问控制修饰符关键字,包括 public、private 和 protected 关键字。这些都是根据它们所做的事情而恰当命名的,所以您可能已经对如何应用它们来公开共享您的代码或者保护它不被公开使用有了一个相当好的想法,但是为了确保万无一失,让我们在这里详细地讨论其中的每一个。如您所知,访问控制和安全性一样,是目前 Java 软件的重要问题,无论是在代码内部还是外部,这就是 Java 9 增加模块的原因。我们将从最少的访问控制(安全性)开始,首先是公共访问控制修饰符。

Java 公共修饰符:独立于实例存在的变量或方法

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

值得注意的是,如果您试图访问和利用的公共类存在于您自己的包之外的另一个包中(在我们的例子中,您自己的包将被命名为 boardgame),那么您将需要使用 Java import 关键字来创建一个 import 语句,以便能够利用该公共类。这就是为什么,在本书结束时,你会在你的 JavaFXGame.java 课程的顶端有几十个重要的陈述。您将利用代码库中预先存在的 Java 和 JavaFX 类,这些类已经通过使用公共访问控制修饰符关键字进行了编码、测试、优化和公开,以便您可以创建利用 Java APIs 的 pro Java 9 游戏和物联网应用。

由于 Java 中的类继承,公共类中的所有公共方法和公共变量都将被该类的子类继承(一旦被子类化,就成为超类)。你可以在 Invincibagel 类关键字前面看到一个公共访问控制修饰符关键字的例子,如图 5-2 所示。

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

Java protected access modifier 关键字可由数据字段(变量和常量)和方法(包括构造函数方法)使用,但不能由类或接口使用。我们将在本章后面讨论 Java 接口。protected 关键字允许超类中的变量、方法和构造函数只能被其他包(如 boardgame 包)中该超类的子类访问,或者被包含这些受保护成员的类(Java 构造)的同一个包中的任何类访问。使用这个访问控制修饰符就像给原始 Java 代码上了一把锁;要使用原始代码(更不用说添加代码并修改其预期用途),您必须扩展或继承受保护的类,然后您可以覆盖它的方法。

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

您可能想知道什么时候会有人想要这样做并像这样保护 Java 代码结构?当你在设计一个更大的项目时,比如 Android 操作系统 API,你通常会希望不直接使用最高层的方法和变量,或者直接在类外使用,或者直接在类内使用。

在这种情况下,当其他人正在使用您的代码结构时,您宁愿您的原始 Java 代码在一个单独定义的、开发人员编码的子类结构中使用。这“隔离”了超类代码,因此它将直接保持不变,在某种意义上,保证了原始的方法、字段和意图被维护,因为它们是 Java 代码作者(包所有者)最初想要的,防止其他人的修改。这确保了你的 API 和它的超类永远作为一个“蓝图”供其他 Java 9 开发者用来创建他们自己的(Android,JavaFX 等)。)游戏、商业实用工具和物联网应用。

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

Java 私有修饰符:允许本地访问的字段、方法或构造函数

Java private access control 修饰符关键字可以由数据字段(变量或常量)和方法使用,包括构造函数方法和接口,但不能由类使用。我们将在本章后面讨论 Java 接口。private access control 关键字允许类中的变量、方法和构造函数只能在该类内部被访问,从 Java 9 开始,现在允许使用私有接口。这个私有访问控制关键字允许 Java 实现一个称为封装的概念,其中一个类(以及使用该类创建的对象)可以封装自己,可以说对外部 Java 世界隐藏了它的“内部”。这种封装在 Java 9 中通过使用模块得到了进一步的增强,我们将在本章的末尾讨论这一点。封装的 OOP 概念可用于允许团队创建(和调试)他们自己的类和对象。这样,其他人的 Java 代码都无法破解存在于类内部的代码,因为它的方法、变量、常量、接口和构造函数都是私有的。封装还可以用来保护代码和资源(素材)不被公众访问。

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

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

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

您将在自己的专业 Java 游戏和物联网应用编程中大量使用这种默认的包私有访问控制级别,因为通常您会在自己的包中创建自己的应用,供用户在 Java 9 新的增强安全性 Java 模块系统(Project Jigsaw)的完整、编译和可执行状态下使用。

从 Java 9 开始,您还将把您的包安装到一个核心 JavaFX 模块中,可能是 javafx.media 或 javafx.graphics。正如您将在本章的最后一节看到的,正确使用 public 和 private 关键字将允许您充分利用 Java 9 的新模块功能。我们将在本章结束时详细介绍模块,在我们介绍了 Java 的许多早期版本中存在的所有其他核心 Java 编程语言特性之后,这些特性今天仍在 Java 6 (32 位 Android)、Java 7 (64 位 Android 5 到 6)和 Java 8 (64 位 Android 7 到 8 和当前版本的 Java,直到 Java 9 在 2017 年最后一个季度发布)中使用。

但是,如果您正在开发游戏引擎供其他游戏开发者使用,那么您很可能最终会更多地使用我们在本节中讨论的其他三个访问控制修饰符关键字,以便您能够精确地控制其他人如何实现您的游戏引擎的 Java 代码结构。接下来,让我们看看非访问控制修饰符关键字,这在智力上更具挑战性!

非访问控制修饰符:Final、Static 和 Abstract

不专门为 Java 构造提供访问控制特性的 Java 修饰符关键字被称为非访问控制修饰符关键字。这些包括经常使用的 static、final 和 abstract 修饰符关键字,以及不经常使用的 synchronized 和 volatile 修饰符关键字,它们用于更高级的线程控制,我将在这个专业级编程标题的后面介绍。我将在这一节中介绍这些关键字,这样,如果您在此之前在 Java 编程中遇到它们,您就会知道它们的含义。

我将按照复杂性的顺序介绍这些概念,从开发人员最容易理解的到面向对象编程开发人员最难理解的。面向对象编程就像冲浪,看起来很难,直到你练习了很多次,然后突然有一天你就明白了。

Java Final 修饰符:不能修改变量引用、方法或类

我们已经看到了最后一个修饰符关键字,因为它用于声明一个常量和一个静态关键字。最终数据字段变量可以初始化(设置)一次。final 引用变量是一种特殊类型的 Java 变量,它包含对内存中某个对象的引用,不能被更改(重新分配)来引用不同的对象。然而,保存在(最终)被引用对象内部的数据是可以更改的,因为只有对对象本身的引用才是最终的引用变量,它实际上是使用 Java final 关键字“锁定”的。

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

public class JavaFXGame extends Application {
     Button btn;
     @Override
     public final void start(Stage primaryStage) {
        btn = new Button();                          // other Java statements can be added
     }
}

这将防止任何子类(公共类 JavaFXGame3D 扩展 JavaFXGame)对 JavaFXGame 游戏引擎(JavaFX)的初始设置进行任何更改。start()方法适用于你的游戏应用,你将在第 7 和 8 章看到,涵盖了 JavaFX 9 多媒体引擎。使用 final modifier 关键字声明的类不能被扩展(也称为子类),从而锁定该类以防将来被使用。

Java 静态修饰符:独立于实例存在的变量或方法

正如您已经看到的,static 关键字可以与 final 关键字结合使用来创建一个常量。static 关键字用于创建 Java 构造(方法或变量),这些构造独立存在,或者位于使用定义静态变量或静态方法的类创建的任何对象实例之外。类中的静态变量将强制该类的所有实例共享该变量中的数据。在其他编程语言中,这通常被称为全局变量,由代码创建的任何东西都可以访问和共享。

类似地,静态方法也将存在于该类的实例化对象之外,并将由所有这些对象共享。静态方法不会引用自身“外部”的变量,例如实例化对象的变量。

通常,静态方法将从其声明类中引用其局部或静态变量和常数,还将使用该方法的参数列表接受变量。然后,它将提供基于这些参数的处理或计算,并使用方法自身的静态或局部常量或变量以及编程逻辑。

由于 static 是一个应用于类实例的概念,因此本质上比任何类本身都要低,因此 Java 类不会使用 static nonaccess control 修饰符关键字来声明。

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

Java abstract modifier 关键字与保护实际代码的关系比与运行时放入内存的代码(对象实例和变量等)的关系更大。abstract 关键字允许您指定如何将代码用作超类,也就是说,一旦它被扩展,如何在子类中实现它。因此,abstract modifier 关键字只适用于类和方法,而不适用于数据字段(变量和常量),因为这些数据结构保存值,而不是代码(编程逻辑)构造。

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

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

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

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

易失性与运行的应用的可见性属性相关联。当一个变量被声明为 volatile 时,写操作将影响变量的主存副本,因此在任何 CPU 或内核上运行的任何线程都将观察到这种变化。当一个变量没有被声明为 volatile 时,写操作将被写入到一个缓存的副本中,因此只有做出改变的线程能够观察到这个改变。只有在 Java 9 游戏绝对需要时才使用 volatile。

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

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

Java synchronized 修饰符关键字也用于开发多线程应用,在本书中,我们不会为 Java 9 游戏开发引擎这样做。synchronized 修饰符的作用是告诉运行应用的 Java 虚拟机(JVM ),声明为 synchronized 的方法一次只能被一个线程访问。这个概念类似于数据库访问中的 synchronized 概念,所以不会有数据记录访问冲突。因此,synchronized modifier 关键字还可以防止访问您的方法(在系统内存中)的线程之间的冲突,方法是一次“序列化”一个方法的访问,这样就不会发生多个线程同时访问内存中的一个方法(冲突)的情况。synchronized 关键字与正在运行的应用的可见性和互斥性属性相关联。许多多线程场景不需要互斥,只需要可见性,因此在这些情况下使用 synchronized 关键字而不是 volatile 关键字会被认为是矫枉过正(与优化相反)。

现在我们已经介绍了主要的 Java 构造(类、方法和字段)和基本修饰符(公共、私有、受保护、静态、最终、抽象等)。)关键词,让我们来看看花括号:{ }现在,学习用于创建 Java 编程逻辑的工具,这些逻辑将最终定义您的 pro Java 9 游戏。

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

因为我们已经讨论了变量和常量,所以您已经遇到了一些 Java 数据类型。让我们进入下一个主题,因为对于我们目前从容易理解到更难的主题的进展来说,这还不算太高级!Java 中有两种主要的数据类型分类:原始数据类型,如果您使用过不同的编程语言,这可能是您最熟悉的;引用(对象)数据类型,如果您使用过另一种面向对象的编程语言,例如 LISP、Python、Objective-C、Ruby、Groovy、Modula、Object COBOL、ColdFusion、C++和 C# (C Sharp 和。NET)。

原始数据类型:字符、数字和布尔值

Java 编程语言中有八种原始数据类型,如表 5-1 所示。我们将在本书中使用这些来创建我们的 JavaFXGame i3D Java 9 游戏,所以我现在不打算深入讨论其中的每一个,只是说布尔数据通常在游戏中用于保存“标志”或“开关”(开/关),char 数据通常用于包含 Unicode 字符或用于创建更复杂的 String 对象(本质上是一个 char 数组),其余的数据用于保存不同大小和分辨率的数值。整数值保存整数,而浮点值保存分数(小数点值)。

为变量的“作用域”或使用范围使用正确的数值数据类型是很重要的,因为正如你在表 5-1 中看到的,大数值数据类型可以比小数值数据类型多使用八倍的内存。请注意,布尔数据值可以比长整型或双精度型数值小 64 倍,因此设计 Java 9 游戏来利用大量的布尔值可能是一种令人难以置信的内存优化技术。不要使用任何超过你完成游戏处理目标绝对需要的数值分辨率,因为内存是一种宝贵的资源。

表 5-1。

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

数据类型 默认 二进制大小 定义 范围
布尔 错误的 1 位(或 1 字节 8 位) true 或 false 值 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

接下来,让我们看一下引用数据类型,之所以这样命名,是因为它们引用内存中更复杂的数据结构,如对象和数组,这两者都包含更复杂的数据结构,这些数据结构要么保存复杂的数据和方法子结构(对象),要么保存更广泛的数据列表(数组)。在数据类型后面的部分,我将从逻辑上介绍 Java 操作符,它们“操作”这些 Java 数据结构。

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

面向对象编程(OOP)语言也有引用数据类型,它在内存中提供对另一个包含更复杂数据结构的结构的引用,如对象或数组。这些更复杂的数据结构是使用代码创建的。在 Java 中,这是一个类。有一种或另一种类型的 Java 数组类创建数据数组(像简单的数据库),以及任何 Java 类中的构造函数方法,甚至是您创建的自定义类,它们可以在内存中创建对象结构,其中可以包含 Java 代码(方法)以及数据(字段)。

因为引用数据类型是对内存位置的引用,所以默认值总是 null,这意味着对象还没有被创建,因为没有引用。由于存在不同的数组和数据集类,数组也是引用对象,但由于它们是由类构造函数方法创建的,所以它们实际上是对象。底线是引用数据类型是使用类创建的,并且总是一种或另一种类型的对象,在内存中被引用。通常这种引用是静态的和/或最终的,因此存储位置是固定的,从而优化了存储器的使用。接下来,让我们看一下 Java 操作符,它们被用来对我们刚刚讨论过的不同 Java 数据类型进行操作(也就是说,对它们执行操作)。

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

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

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

Java 算术运算符:基础数学

Java 算术运算符是专业 Java 游戏编程中最常用的运算符,尤其是在动态动作类游戏中,游戏中的事物在屏幕上以精确、高度受控的像素数量移动。不要低估简单的算术运算符,就像在 OOP 语言的框架中一样。使用 Java 结构可以创建复杂得多的数学方程,例如方法,这些方法利用 Java 提供的其他强大工具利用这些基本算术运算符,我们将在本章中回顾(学习)这些工具。

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

表 5-2。

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

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

要实现算术运算符,请将希望接收算术运算结果的数据字段(变量)放在等号赋值运算符的左侧(我们也将在本章的这一节讨论赋值运算符),将希望执行算术运算的变量放在等号的右侧。下面是一个添加 X 和 Y 变量并将结果赋给 z 变量的示例:

Z = X + Y;   // Using the Addition Operator

如果你想从 X 中减去 Y,你应该用减号而不是加号,如果你想把 X 和 Y 值相乘,你应该用星号而不是加号。如果你想用 X 除以 Y,你应该使用一个正斜杠字符,而不是加号。如果你想得到 X 除以 Y 的余数,你可以使用一个百分号。下面是这些基本算术运算在代码中的样子:

Z = X - Y;    // Subtraction    Operator
Z = X * Y;   //  Multiplication Operator
Z = X / Y;  //   Division       Operator
Z = X % Y; //    Modulus        Operator

如果您的 Java 代码涉及被零(0)除,您应该小心。将整数除以 0 将导致算术异常。将浮点值除以 0 将得到+无穷大、-无穷大或 NaN。在游戏开发环境中,您可能会遇到这种情况,您必须重新设计您的编程逻辑,以确保这些情况不会干扰您的游戏。

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

Java 关系运算符:进行比较

在某些情况下,Java 关系运算符可用于在两个变量之间或变量和常量之间进行逻辑比较。这些你从初中开始应该也很熟悉了,它们包括等于、不等于、大于、小于、大于等于、小于等于。大于使用箭头的开放端(人字形),因为开放跨度大于封闭跨度,小于使用箭头的封闭端(人字形),因为封闭跨度小于开放跨度。这是一个很好的视觉观察方式。当您这样做时,您可以立即看到在关系运算符 X > Y 中,X 大于 Y。在 Java 中,等于关系运算符在被比较的数据字段之间并排使用两个等号,并在等号之前使用一个感叹号来表示不等于,如表 5-3 所示,该表显示了关系运算符以及每个运算符的示例和描述。

表 5-3。

Java Relational Operators, an Example Where A=10 and B=20, and a Description of the Relational Operation

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

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

这些关系运算符返回布尔值 true 或 false。因此,它们也在 Java 的控制(循环)结构中被大量使用,也在游戏编程逻辑中被用来控制游戏将采取的路径(结果)。例如,假设您想要确定游戏板的左边缘在哪里,以便当游戏块 3D 对象被移动到左边时不会从板上掉下来。使用这种关系比较:

boolean gameBoardEdge = false;      // boolean variable gameBoardEdge initialized to be false
gameBoardEdge = (GamePieceX <= 0); //  boolean gameBoardEdge set to TRUE if left side reached

请注意,我使用了< =小于或等于(是的,Java 也支持负数),因此,如果游戏棋子已经越过了屏幕的左侧(x=0),gameBoardEdge 布尔标志将被设置为 true 值,游戏移动编程逻辑可以通过改变移动方向(因此游戏棋子不会从游戏棋盘上掉落)或完全停止移动(因此游戏棋子停在边缘)来处理这种情况。

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

Java 逻辑操作符:处理组和对立面

Java 逻辑运算符有点类似于布尔运算(并集、交集等)。)因为它们将布尔值相互比较,然后基于这些比较做出决定。Java 逻辑运算符将允许您确定两个布尔变量是否持有相同的值,这被称为 AND 运算,或者一个布尔变量是否与另一个不同,这被称为 or 运算。还有第三个逻辑运算符,称为 NOT 运算符,它将反转任何比较的布尔操作数的值,甚至反转没有比较的布尔操作数的值,如果你只是想在游戏编程逻辑中翻转开关或反转布尔标志。正如您可能已经猜到的,AND 运算符使用两个 AND 符号,就像这样:&&。OR 运算符使用两个竖线,就像这样:||。NOT 运算符使用感叹号,就像这样:!。所以,如果我说我不是在开玩笑,我会写!开玩笑(嘿,那会是一件很棒的程序员 t 恤)。表 5-4 显示了 Java 逻辑操作符,每一个都有一个例子,还有一个简短的描述。

表 5-4。

Java Logical Operators, an Example Where A=true and B=false, and a Description of the Logical Operation

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

让我们使用逻辑运算符来增强我在上一节中使用的游戏逻辑示例,以确定当玩家移动游戏棋子时(也就是说,当轮到他们时),他们是否从游戏棋盘上掉了下来(移动到边缘之外)。

修改后的代码将包含一个逻辑 AND 运算符,如果 gameBoardEdge = true 且 turnActive = true,该运算符会将 fellOffBoard 布尔变量设置为 true 值。确定这一点的 Java 代码将类似于以下 Java 语句:

boolean gameBoardEdge = false;     // boolean variable gameBoardEdge is initialized to be false
gameBoardEdge = (GamePieceX < 0);  // boolean gameBoardEdge set TRUE if past (before) left side

fellOffBoard = (gameBoardEdge && turnActive) // It's your turn, but you fell off the left edge!

现在,您已经开始练习声明和初始化变量,并使用关系和逻辑运算符来确定游戏棋子的回合、边界和位置。接下来,我们来看看 Java 赋值操作符。

Java 赋值运算符:将结果赋给变量

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

表 5-5。

Java Assignment Operators, What That 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 时将其值设置为 10,则可以使用以下 Java 编程逻辑编写条件运算符编程语句:

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

需要注意的是,在。和之后:必须与等于运算符另一侧的数据变量类型一致。例如,您不能指定以下内容:

int x = (y > z) ? "abc" : 20;

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

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

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

在本章的这一节,我们将首先看一看决策控制结构,如 Java Switch-Case 结构和 If-Then-Else 结构,然后我们将看一看 Java 的循环控制结构,包括 For、While 和 Do-While 迭代(循环)控制结构。

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

一些最强大的 Java 逻辑控制结构,特别是当涉及到专业 Java 游戏开发时,是那些允许你定义游戏决策的结构,当你的游戏应用运行时,你希望你的游戏程序逻辑为你作出决策。其中一个称为开关,提供逐案“平面”决策矩阵,另一个称为 if-else,提供级联决策树,评估“如果这样,就这样,如果不这样,就这样,如果不这样,就这样,如果不这样,就这样,如果都不这样,就这样。”这两者都可以用来创建一种评估结构,在这种结构中,事物按照您希望的顺序和方式进行评估。

让我们从 Java switch 语句开始,它使用 Java switch 关键字和这个决策树顶部的一个表达式。在决策树内部,switch 构造使用 Java case 关键字为 switch 语句表达式求值的每个结果提供 Java 语句块。如果表达式求值没有使用 switch 语句结构内(即花括号{}内)的这些情况,则可以提供一个 Java default 关键字和一个 Java 语句代码块,以便在这些情况都没有被调用时执行您想要的操作。

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

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;
}

case 语句中使用的变量可以是五种 Java 数据类型之一:char(字符)、byte、short、string 或 int(整数)。您通常希望在每个 case 语句代码块的末尾提供 Java break 关键字,至少在需要“排他地”切换值的用例中,并且对于每次调用 switch 语句,只有一个值是可行的(或允许的)。

默认语句不需要使用任何 break 关键字。

如果您没有在每个 case 逻辑块中提供 Java break 关键字,则可以通过 switch 语句在同一个过程中对多个 case 语句进行求值。这将在表达式求值树从顶部(第一个 case 代码块)到底部(最后一个 case 代码块或默认关键字代码块)的过程中完成。

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

假设你想在你的游戏中决定当游戏角色移动时(行走、跳跃、跳舞等)游戏角色移动动画的名称。).游戏人物动画例程(方法)将基于游戏人物在移动时所做的事情来调用,例如行走(W)、跳跃(J)、跳舞(D)或空闲(I)。假设这些“状态”保存在 char 类型的名为 gpState 的数据字段中,该字段保存一个字符。你的 switch-case 代码构造使用这些游戏棋子状态指示器来调用一个正确的方法,一旦已经进行了一个回合,并且需要进行移动。这应该类似于下面的 Java 伪代码(原型代码):

switch(gpState) {           // Evaluate gpState char, execute case code blocks accordingly
    case 'W' :
        gamePieceWalking(); // Java method controlling Walk sequence if GamePiece is walking

        break;
    case 'J' :
        gamePieceJumping(); // Java method controlling Jump sequence if GamePiece is jumping

        break;
    case 'D' :
        gamePieceDancing(); // Java method controlling Dance sequence if GamePiece is dancing

        break;
    default :
        gamePieceIdle();   // Java method controlling processing if a GamePiece is idle

这个 switch -case 逻辑结构在 switch()语句的求值部分内对 gpState 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 嵌套越深。下面是一个例子,说明我之前为 BoardGame 游戏编写的 switch-case 语句如何转化为 Java 编程结构中的嵌套 if-else 决策结构:

if(gpState = 'W') {
    gamePieceWalking();
} else if(gpState = 'J') {
      gamePieceJumping();
  } else if(gpState = 'D') {
        gamePieceDancing();
    } else {
          gamePieceIdle();
      }

正如您所看到的,这个 if-else 决策树结构与我们之前创建的 switch-case 非常相似,只是决策代码结构相互嵌套,而不是包含在一个“平面”结构中。作为一般的经验法则,对于一值和二值评估,我会使用 if 和 if-else,对于三值或更多值评估场景,我会使用 switch-case。我在涉及 Android 的书籍中广泛使用了开关盒结构,例如面向绝对初学者的 Android 应用(Apress,2017 年)和 Pro Android 可穿戴设备(Apress,2015 年)。

接下来,让我们看看 Java 中广泛使用的其他类型的条件控制结构,即“循环”或迭代编程结构。这些迭代条件结构将允许您通过使用 for 循环执行任何编程语句块预定的次数,或者通过使用 while 或 do-while 循环直到实现 Java 编程目标。

正如你所想象的,这些迭代控制结构对你的游戏控制逻辑非常有用。

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

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

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

for(initialization; boolean expression; update equation) {
    programming statement one;
    programming statement two;
}

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

如果你想在棋盘上对角移动游戏棋子 40 个像素,你的 for 循环如下:

for (int x; x < 40; x = x + 1) { // Note: the x = x + 1 statement could also be coded as x++
    gamePieceX++;  // Note: gamePieceX++ could be coded gamePieceX = gamePieceX + 1;
    gamePieceY++;  // Note: gamePieceY++ could be coded gamePieceY = gamePieceY + 1;
}

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

while (boolean expression)  {
    programming statement one;
    programming statement two;
    expression incrementation;
}

使用 while 循环结构编写将游戏块移动 40 个像素的 for 循环,如下所示:

int x = 0;

while(x < 40) {
    invinciBagelX++;
    invinciBagelY++;
    x++;
}

do-while 循环和 while 循环之间的唯一区别是,在 do-while 循环中,循环逻辑编程语句在求值之前执行,而不是像在 while 循环中那样在求值之后执行。因此,前面的示例将使用 do- while 循环编程结构编写,该结构在花括号内有一个 Java 编程逻辑结构,在 Java do 关键字之后,在右花括号之后有 while 语句,编码如下:

int x = 0;

do {
    invinciBagelX++;
    invinciBagelY++;
    x++;
   }

while(x < 40);

您还应该注意,对于 do {…} while(…);构造中,while 求值语句(以及整个 do-while 编程构造)需要以分号结束,而 while(…){…}结构则不需要。

如果您希望确保 while 循环结构中的编程逻辑至少执行一次,请使用 do-while,因为求值是在循环逻辑执行之后执行的。如果您想确保循环内部的逻辑只在求值成功之后或任何时候执行(这是编写代码的更安全的方式),请使用 while 循环结构。

Java 对象:在 Java 中使用 OOP 虚拟现实

我把最好的留到最后,Java 对象,是因为它们可以用一种或另一种方式构造,使用我在这一章中已经介绍过的所有概念,也因为它们是面向对象编程(OOP)语言的基础,在这里是 Java 7、8 和 9。java 编程语言中的一切都基于 Java 语言的 Object 超类(我喜欢称之为 master 类),它在 java.lang 包中,因此它的 import 语句将引用 java.lang.Object,这是 Java 对象类的完整路径名。所有其他 Java 类都是使用这个类创建的,或者说是子类化的,因为 Java 中的所有东西都是对象。

注意,您的 Java 编译器会自动为您导入这个 java.lang 包!Java 对象是用来“虚拟化”现实的,它允许你在日常生活中看到的对象(或者,在你的游戏中,你根据自己的想象创建的对象)被逼真地模拟。这是通过使用数据字段(变量和常量)和你在本章中学到的方法来完成的。这些 Java 编程结构将组成对象特征或属性(常量)、状态(变量)和行为(方法)。

Java 类构造将组织每个对象定义(常量、变量和方法),并将产生该对象的一个实例。它通过使用设计和定义对象的类的构造函数方法,以及通过使用在本章中学到的各种 Java 关键字和编程构造来实现这一点。在这一节中,我将向您介绍如何做到这一点,我想如果您是 Java 9 的新手,您会发现这非常有趣。

思考 Java 对象的一种方式是把它们当成名词,即存在于自身之中的事物(对象)!使用方法创建的对象行为类似于动词,即名词可以做的事情。举个例子,让我们考虑一下我们生活中最受欢迎的东西:汽车。我们完全可以把这辆车作为游戏的一部分或者作为棋盘游戏的另一个组成部分加入到我们的棋盘游戏中。

接下来让我们定义汽车对象属性。一些特性或属性不会改变,并保存在常量中,可定义如下:

  • 颜色(糖果苹果红)
  • 发动机类型(燃气、柴油、氢气、丙烷或电动)
  • 传动系统类型(2WD 或 4WD)

一些实时改变、定义汽车并保存在变量中的状态可以定义如下:

  • 方向(北、南、东或西)
  • 速度(每小时 15 英里)
  • 档位设置(1、2、3、4 或 5)

以下是汽车应该能够做的一些事情,即汽车的行为,定义为方法:

  • 加速
  • 变速
  • 刹车
  • 转动轮子
  • 打开音响
  • 用前灯
  • 使用转向灯

你明白了。现在停止幻想你的新游戏,让我们回到学习物体上来!

图 5-3 展示了 Java 对象结构,以这辆车为例。它显示了汽车的特征或属性,这些特征或属性对于定义汽车对象和可用于汽车对象的行为至关重要。

A336284_1_En_5_Fig3_HTML.jpg

图 5-3。

The anatomy of a car GamePiece object, with methods encapsulating variables or constants inside a class

这些属性和行为将为外界定义一辆汽车,就像您的 pro Java 9 游戏应用对象将为您的 Java 9 和 JavaFX 9 游戏应用所做的那样。

对象可以像您希望的那样复杂,Java 对象也可以在其对象结构或对象层次结构中嵌套或包含其他 Java 对象。一个对象层次结构就像一个树形结构,当你沿着树形结构向上(或向下)移动时,有一个主干、分支,然后是子分支,非常类似于 JavaFX 或 3D 软件场景图,你可以在第三章中看到(在图 3-4 的右边)。

您每天使用的层次结构的一个很好的例子是多级目录或文件夹结构,它位于您计算机的硬盘驱动器上。

硬盘上的目录或文件夹将包含其他目录或文件夹,这些目录或文件夹又可以包含其他目录和文件夹,从而允许创建复杂的组织层次结构。

你会注意到,在现实生活中,物体可以由其他物体组成。例如,一个汽车引擎对象由数百个离散的对象组成,这些对象共同作用,使引擎对象作为一个整体工作。

这种从简单对象中构造更复杂对象的方法也可以在 OOP 语言中实现,其中复杂的 Java 对象层次结构可以包含其他 Java 对象。这些 Java 对象中的许多可能是使用预先存在的或以前开发的 Java 代码创建的,这是模块化编程的目标之一。

作为练习,您应该练习识别您周围房间中的不同复杂对象,然后将它们的定义或描述分解为状态(可变状态或恒定特征)以及行为(对象可以或将要做的事情)和对象及子对象层次。

这是一个很好的练习,因为这是您最终需要开始思考的方式,以便在更大的 Java 编程语言框架内使用 JavaFX 引擎在专业的面向对象游戏编程工作中获得更大的成功。

编码对象:将对象设计转化为 Java 代码

为了进一步说明这一点,让我们为汽车对象示例构造一个基本类。要创建一个 Car 类,可以使用 Java 关键字 class,后跟您正在编写的新类的自定义名称,然后是包含您的 Java 代码类定义的花括号。通常放入类中的第一件事(在大括号{}内)是数据字段(变量)。这些变量将保存汽车对象的状态或特征。在这种情况下,您将有六个数据字段,它们将定义汽车的当前档位、当前速度、当前方向、燃料类型、颜色和传动系统(两轮或四轮驱动),如前面为该汽车对象指定的那样。因此,有了图 5-3 中的六个变量,汽车类的定义最初看起来会像这样:

class Car {
    int speed = 15;
    int gear = 1;
    int drivetrain = 4;
    String direction = "N";
    String color = "Red";
    String fuel = "Gas";
}

注意这个例子是如何在它们自己的行上隔开花括号:{ },以及缩进某些行。这是 Java 编程惯例,这样您就可以更容易、更清楚地将包含在花括号内的 Java 类结构中的代码结构可视化,类似于 Java 9 代码结构的“鸟瞰图”。

由于我们使用等号为所有这些变量指定了一个起始值,请记住这些变量都将包含这个缺省值或起始数据值。这些初始数据值将在构造时被设置(在系统内存中)为汽车对象的默认值,因为它们被设置为你的类的“起始”变量数据值。

Java 类定义文件的下一部分将包含您的方法。Java 方法将定义您的 Car 对象将如何运行,也就是说,它将如何“操作”您在保存 Car 对象当前“操作状态”的类的顶部定义的变量方法“调用”将调用变量状态更改,方法也可以将数据值“返回”给“调用”或“调用”该方法的实体,如已成功更改的数据值,甚至是某个等式的结果。

例如,应该有一种方法允许您通过将对象的档位变量或属性设置为不同的值来换档。此方法将被声明为 void,因为它执行一个函数,但不返回任何函数。在这个汽车类和汽车对象定义的例子中,我们将有四个方法,如图 5-3 所示。

那个。shiftGears()方法会将汽车对象的 gear 属性设置为传递到。shiftGears()方法。您应该允许将一个整数传递给该方法,以允许“用户错误”,就像您在现实世界中驾驶汽车时,用户可能会意外地从第一档换到第四档一样。

void shiftGears (int newGear) {
        gear = newGear;
}

那个。accelerateSpeed()方法获取你的对象速度状态变量,然后将你的加速因子加到该速度变量上,这将导致你的对象加速。这是通过获取对象的当前速度设置或状态,并向其添加加速因子,然后将该加法操作的结果设置回原始速度变量,以便对象的速度状态现在包含新的(加速的)速度值。

void accelerateSpeed (int acceleration) {
        speed = speed + acceleration;
}

那个。applyBrake()方法获取对象的速度状态变量,并从当前速度中减去一个制动因子,从而使对象减速或制动。这是通过获取对象的当前速度设置并从中减去制动系数,然后将相减的结果设置回原始速度变量来完成的,这样对象的速度状态现在包含更新的(减速的)制动值。

void applyBrake (int brakingFactor) {
        speed = speed - brakingFactor;
}

那个。turnWheel()方法很简单,很像。shiftGears()方法,只不过它使用一个字符串值 N、S、E 或 W 来控制汽车转弯的方向。什么时候。使用转轮(“W”)时,汽车对象将向左转。什么时候。使用 turn wheel(“E”)时,汽车将向右转,当然,假设汽车对象当前正在向北行驶,根据其默认的方向设置,它是向北行驶的。

void turnWheel (String newDirection) {
        direction = newDirection;
}

使 Car 对象函数进入类内部的方法在变量声明之后,如下所示:

class Car {
    int speed = 15;
    int gear = 1;
    int drivetrain = 4;
    String direction = "N";
    String color = "Red";
    String fuel = "Gas";

    void shiftGears (int newGear) {
        gear = newGear;
    }

    void accelerateSpeed (int acceleration) {
        speed = speed + acceleration;
    }

    void applyBrake (int brakingFactor) {
        speed = speed - brakingFactor;
    }

    void turnWheel (String newDirection) {
        direction = newDirection;
    }
}

这个 Car 类将允许您定义一个 Car 对象,即使您没有明确包含 Car()构造函数方法,我们将在接下来讨论这个方法。这就是为什么你的变量设置将成为你的汽车对象的默认值。但是,最好编写自己的构造函数方法,这样您就可以完全控制对象的创建,并且不必将变量预先初始化为某个值。因此,您要做的第一件事是不定义变量声明,删除等号和初始数据值,如下所示:

class Car {
    String name;

    int speed;
    int gear;
    int drivetrain;
    String direction;
    String color;
    String fuel;
    public Car (String carName) {
        name = carName;

        speed = 15;
        gear = 1;
        drivetrain = 4;
        direction = "N";
        color = "Red";
        fuel = "Gas";
    }
}

相反,Car()构造函数方法本身将设置数据值,作为 Car 对象的构造和配置的一部分。如您所见,我添加了一个字符串名称变量来保存汽车对象的名称(carName 参数)。

Java 构造函数方法与常规的 Java 方法有许多不同之处。首先,它不会使用任何数据返回类型,比如 void 和 int,因为它是用来创建一个 Java 对象而不是执行一个函数的。它不返回 nothing (void 关键字)或 number (int 或 float 关键字),而是返回 java.lang.Object 类型的对象。注意,每个需要创建 java 对象的类都有一个与类同名的构造函数,因此构造函数是一种方法类型,它的名称可以(并且应该总是)以大写字母开头。正如我提到的,如果您没有编写构造函数,Java 编译器会为您创建一个!

构造函数方法和任何其他方法之间的另一个区别是,构造函数需要利用公共访问控制修饰符,而不能使用任何非访问控制修饰符。如果您想知道如何修改前面的 Car()构造函数方法,比方说,如果您不仅想使用构造函数方法命名 Car 对象,还想使用重载的 Car()构造函数方法调用来定义它的速度、方向和颜色,您可以通过使用以下代码为构造函数创建一个更长的参数列表来实现这个更高级的目标:

class Car {
    String name;
    int speed;
    int gear;
    int drivetrain;
    String direction;
    String color;
    String fuel;
    public Car (String carName, int carSpeed, String carDirection, String carColor) {
        name = carName;
        speed = carSpeed;
        gear = 1;
        drivetrain = 4;
        direction = carDirection;
        color = carColor;
        fuel = "Gas";
    }
}

这里需要注意的是,只要包含非公共构造函数的类不需要从它们的包之外实例化,构造函数方法就可以不使用 public 关键字来声明。如果您想使用 this()编写一个调用另一个构造函数的构造函数,您也可以这样做。比如 Car()可能会执行构造函数方法调用 this("myCar ",10,1,4," N "," red ");这将是合法的 Java 代码。

要使用重载的 Car()类构造函数和 Java new 关键字创建一个新的 Car 对象,可以使用如下 Java 代码:

Car carOne = new Car();                          // Creates a Car object using default values
Car carTwo = new Car("Herbie", 25, "W", "Blue"); // Creates a customized Car object

构造对象的语法类似于声明变量,但也使用 Java new 关键字:

  • 定义对象类型汽车。
  • 给汽车对象起一个名字(carOne,carTwo 等。)可以在 Java 代码类中引用。
  • 使用默认的 Car()构造函数方法创建通用或默认的 Car 对象,或者…
  • 使用具有不同值参数的重载汽车(名称、速度、方向、颜色)构造函数。

使用这些 Car 对象调用 Car 对象方法需要使用一种叫做点符号的东西,这种符号用于将 Java 结构相互链接或引用。一旦 Java 对象被声明、命名和实例化,您就可以“脱离它”调用方法例如,这可以使用以下 Java 代码来完成:

 objectName.methodName(parameter list variable);

因此,要切换到第三档,对于名为 carOne 的汽车对象,您可以使用以下 Java 编程语句:

carOne.shiftGears(3);

这“调用”或“调用”了。shiftGears()方法“关闭”carOne Car 对象,并“忽略”gear 参数,该参数包含一个整数值 3,然后将其放入 newGear 变量中,该变量由。shiftGears()方法内部代码,用于更改汽车对象实例的齿轮属性,设置一个新值 3。

Java 点符号将 Java 方法调用“连接”到 Java 对象实例,然后 Java 对象实例调用或“调用”该 Java 对象实例的方法。仔细想想,Java 的工作方式是符合逻辑的,也是很酷的。

扩展 Java 对象结构:Java 继承

Java 还支持开发不同类型的增强类(以及相应的对象)。这是通过使用一种叫做继承的面向对象技术来实现的。继承是指可以使用原始超类对更专门化的类(更独特定义的对象)进行子类化;在这种情况下,应该是汽车。继承过程如图 5-4 所示。一旦一个类通过“子类化”被用于继承,它就成为超类。最终,在链的最顶端只能有一个超类,但是可以有无限数量的子类。所有子类都从超类继承方法和字段。Java 中这方面的终极例子是 java.lang.Object 超类(我有时称之为 master 类),它用于创建 Java 9 中的所有其他类。

A336284_1_En_5_Fig4_HTML.jpg

图 5-4。

The inheritance of a Car object superclass will allow you to create an SUV Car object and a Sport Car object

作为使用 Car 类继承的一个例子,您可以从 Car 类中“继承”Suv 类,使用 Car 类作为超类。这是使用 Java extends 关键字来创建 Suv 类定义的,该关键字扩展了 Car 类定义。除了扩展适用于所有类型汽车对象的所有属性(常数)、状态(变量)和行为(方法)之外,这个 SUV 类将只定义那些适用于 Suv 类型汽车对象的附加属性(常数)、状态(变量)和行为(方法)。这是 Java extends 关键字为这个子类化(继承)操作提供的功能,这是 Java 9 OOP 语言中代码模块化的一个更重要和有用的特性。您可以在图 5-4 中直观地看到这种模块化,每个子类的附加汽车功能以橙色添加。这是组织代码的好方法!

Suv 汽车对象子类可能有额外的。onStarCall()和。定义的 turnTowLightOn()方法,除了继承通常的汽车对象操作方法之外,还允许汽车对象换挡、加速、踩刹车、转动方向盘。

类似地,您也可以生成第二个子类,称为 Sport 类,它创建跑车对象。这些可能包括。activateOverdrive()方法来提供更快的齿轮传动。openTop()方法放下活顶。要使用超类创建子类,可以通过在类声明中使用 Java extends 关键字从超类扩展子类。因此,Java 类结构看起来就像这样:

class Suv extends Car {
    void applyBrake (int brakingFactor) {
         super.applyBrake(brakingFactor);
         speed = speed - brakingFactor;
    }
}

这扩展了 Suv 对象,使其能够访问(本质上是包含)Car 对象所具有的所有数据字段和方法。这使得开发人员只需关注新的或不同的数据字段和方法,这些数据字段和方法与 Suv 对象和常规或“主”汽车对象定义的区别相关。

要从您正在编码的子类中引用超类的方法之一,您可以使用 Java super 关键字。例如,在新的 Suv 类中,您可能想要使用 Car 超类。applyBrake()方法,然后将一些特定于 Suv 的附加功能应用于制动器。你称汽车为物体的。通过在 Java 代码中使用 super.applyBrake()来调用 applyBrake()方法。前面显示的 Java 代码将为 Car 对象的。applyBrake()方法,在 Suv 对象内部。applyBrake()方法,方法是使用这个 super 关键字来访问汽车对象的。applyBrake()方法,然后添加附加逻辑,使 brakingFactor 应用两次。这为 SUV 对象提供了两倍于标准汽车的制动力,这是 Suv 所需要的。

这个 Java 代码使 Suv 的制动力加倍的原因是因为 SUV 对象的。applyBrake()方法首先调用汽车对象的。使用 Suv 子类中的一行 Java 代码从 Car 超类中调用 applyBrake()方法。applyBrake()方法。接下来的 Java 代码行通过再次应用 brakingFactor 来增加速度变量,使您的 SUV 对象的刹车功能加倍。

Java 接口:定义类使用模式

在许多 Java 应用中,Java 类必须符合特定的使用模式。有一种专门的 Java 构造,称为接口,可以实现该接口,以便应用开发人员确切地知道如何实现这些 Java 类,包括提醒开发人员正确实现类需要方法。定义一个接口将允许您的类通知使用该类的其他开发人员,为了正确利用您的 Java 类的基础结构,您的类的行为(哪些 Java 方法)必须被实现。

接口本质上规定了类和开发社区的其余部分之间的编程契约。通过实现 Java 接口,Java 编译器可以在构建时强制执行契约。如果一个类“声明”要实现一个公共接口,那么在该类成功编译之前,由该 Java 接口定义“定义”的所有方法都必须出现在实现该接口的类的源代码中。

当在复杂的、基于 Java 的编程框架(如 Android uses)中工作时,接口尤其有用,开发人员利用这些框架在 Java 类上构建应用,Google Android OS 开发团队成员专门为此编写了 Java 类。Java 接口应该像路线图一样使用,向开发人员展示如何最好地实现和利用由另一个 Java 编程结构中的 Java 类提供的 Java 代码结构。

基本上,Java 接口保证给定类中的所有方法将作为一个互通的、相互依赖的集体编程结构一起实现,保证实现该功能集体所需的任何单个函数不会被无意中遗漏。一个类“呈现”给使用 Java 语言的其他开发人员的这个公共接口使得使用该类更加可预测,并允许开发人员在编程结构和目标中安全地使用该类,其中特定最终使用模式的类适合于他们的实现。从 Java 9 开始,你也可以定义私有接口在你的应用内部使用。

下面是一个 ICar 接口,它强制所有的汽车实现这个接口中定义的所有方法。这些方法必须实现并存在,即使它们没有被使用,也就是说,花括号内没有代码。这也保证了 Java 应用的其余部分知道每个 Car 对象可以执行所有这些行为,因为实现 ICar 接口为所有 Car 对象定义了一个公共接口。对于当前 Car 类中的那些方法,实现 ICar 公共接口的方式如下:

public interface ICar {
    void shiftGears (int newGear);
    void accelerateSpeed (int acceleration);
    void applyBrake (int brakingFactor);
    void turnWheel (String newDirection);
}

要实现一个接口,您需要使用 Java implements 关键字,如下所示,然后像以前一样定义所有的方法,只是现在除了 void 返回数据类型之外,还必须使用公共访问控制修饰符来声明这些方法。因此,您将在 void 关键字之前添加 public 关键字,这将允许其他 Java 类能够调用这些方法,即使这些类在不同的包中。毕竟这是一个公共接口,任何开发者(或者更准确地说,任何类)都应该能够访问它。下面是您的 Car 类应该如何通过使用 Java implements 关键字来实现这个 ICar 接口:

class Car implements ICar {
    String name = "Generic";
    int speed = 15;
    int gear = 1;
    int drivetrain = 4;
    String direction = "N";
    String color = "Red";
    String fuel = "Gas  ";

    public void shiftGears (int newGear) {
        gear = newGear;
    }
    public void accelerateSpeed (int acceleration) {
        speed = speed + acceleration;
    }
    public void applyBrake (int brakingFactor) {
        speed = speed - brakingFactor;
    }
    public void turnWheel (String newDirection) {
        direction = newDirection;
    }
}

Java 接口不能使用任何其他 Java 访问控制修饰符关键字,因此它不能声明为 private(在 Java 9 之前)或 protected。需要注意的是,只有那些在接口定义中声明的方法才需要实现。我在类定义顶部的数据字段是可选的。在这个例子中,这些是为了显示它与 Car 类是并行的,我之前没有使用接口就声明了 Car 类。除了使用 implements 关键字之外,没有太大的区别,只是实现一个接口告诉 Java 编译器检查并确保开发人员包含了使 Car 类正常工作的所有必要方法。

Java 9 的新特性:模块化和拼图项目

您可能想知道为什么我最后才讨论 Java 9 和它的新模块,这有几个原因,在我们讨论 Java 9 的新特性之前,我将首先解释一下。Java 9 中的任何新特性都不会影响您的游戏代码,这很好,因为您可以在 Java 6、Java 7 和 Java 8 中编写相同的基本 Java 游戏代码;这意味着你的游戏可以在还没有使用 Java 9 的地方运行,而且可能在一段时间内都不会。由于 32 位 Android 使用 Java 6,64 位 Android 使用 Java 7 (Android 5 和 6)和 Java 8 (Android 7、8 和更高版本),这意味着您可以用 Java 编写跨越十年平台的游戏逻辑。由于 Java 9 比原计划于 2015 年第四季度发布的版本晚了几年,我不得不使用 Java 8 为这本书开发代码,这本书与 Java 9 同时发布。幸运的是,Jigsaw 项目(Java 9 的主要特性)影响了编程语言的模块化,而不是模块内部的代码,这与 Java 8 和 JavaFX 8 是一样的。因此,出于本书的目的,即编写专业 Java 游戏逻辑,Java 8 和 Java 9 之间没有重大变化。一个游戏是否使用 Java 9 特性模块化并不影响性能(游戏性),只影响它的分布,所以我在本章的最后讨论这个模块化特性,因为它是关于游戏性能最不重要的 Java 方面。

我确实想在 Java 中包含这些模块,因为从 Java 9 开始,模块现在是一个核心特性,尽管它们只影响专业 Java 游戏的打包,而不影响游戏实际上是如何编码和优化内存和处理器使用的。

Java 9 模块的定义:包的集合

Java 9 的定义特性是 JEP 200(模块化 JDK),它代表 JDK 增强提案 200。这是 JEP 201(模块化源代码)、JEP 220(模块化运行时)、JEP 260(封装 API)和 JEP 261(模块系统)之上的“伞式”JEP,它们封装了实现模块化 JDK (JEP 200)需要完成的内容。

目前,Java 8 和 JavaFX 8.0 就像是两种不同的编程语言合二为一。因此,Java 9 JDK 首先要模块化的是 JavaFX 8(现在改名为 JavaFX 9),因为这是你要用来创建游戏的,我们将在这一节详细讨论。如果您是企业(业务应用)Java 9 开发人员,这个 Java 9 模块系统将允许您排除所有“繁重的”JavaFX API 库、包、类等等。然而,这把刀也有反作用,所以如果你只打算开发一个 i2D 或 i3D Java 游戏,你只需要声明并包含 javafx.graphics 模块,你的游戏发行包(模块)不需要包含游戏不需要的过多的其他 Java APIs,因为它只关注图形和事件处理(屏幕上的多媒体视觉效果以及它们如何与玩家交互)。

Java 模块包含 Java 包的集合,因此 Java 模块在当前存在的 Java 包-类-方法-变量层次结构之上添加了另一个层次结构级别。一个 Java 包只能属于一个模块,并且不能在模块之间拆分 Java 包。这使得包的组织对于您自己的游戏和 JavaFX 来说更加重要,Java FX 在 Java 9 中已经被组织成包和模块。我们将在本章的后半部分学习这一点。

Java 包允许您按功能组织,而模块允许您按特性组织。这允许数据(和代码)足迹优化。例如,我们不会在我们的 Pro Java 9 游戏中使用 JavaFX Swing、标准 UI 控件、FXML 或 WebKit 因此,我们不需要在发行版中包含这些代码模块。

Java 模块的属性:显式、自动或未命名

创建 Java 模块有三种方式:显式、隐式和匿名。开发人员通过指定一个module-info.java文件来有意创建一个显式模块,该文件定义了显式模块中的其他模块和包。显式模块定义了需求:输入(所需的 API 包)和导出:输出(已发布的包)。导出的包对 Java 环境是可见的,因此可以被执行。仅要求(输入或读取包)模块定义文件中指定的包可以被该模块访问(利用)。在本章的这一节,我将在稍后向你展示这个定义的格式。

如果开发人员没有提供一个module-info.java文件,并且如果它在一个不包含module-info.java定义文件的模块路径上找到一个 JAR 文件,那么 Java 9 环境也可以创建一个隐式模块,也称为自动模块,因此 Java 9 环境会自动为 JAR 文件的内容创建一个隐式模块。在这种情况下,它将自动导出所有需要的包,要求(输入或读取)所有需要的模块,并包括任何未命名的模块,我们将在接下来讨论。

最后,Java 9 环境还可以通过在类路径上添加不在 JAR 文件中并且没有开发人员提供的module-info.java文件的类来创建一个未命名的模块。这使得 Java 9 环境可以容纳较旧的 Java 6 到 8 项目,方法是将它们制作成未命名的模块,这样它们仍然可以在 Java 9 环境中运行,即使它们是在还不存在module-info.java的 Java 早期版本中创建的。显式模块不能要求未命名的模块,这意味着旧版本 Java 软件的开发者必须创建一个module-info.java定义文件来将他们的软件带入 Java 9 模块化领域。

如果应用的主类在未命名的模块中,那么所有的默认模块都将被加载以确保 Java 9 应用能够运行,而模块化的好处(减少发行版的数据占用)将会丧失。如果你用运行你的 Pro Java 9 游戏所需的最少的包定义了一个module-info.java文件,那么 javapackager 实用程序将能够为你生成一个只包含所需模块的捆绑应用。

Java 9 模块层次结构的一个例子:JavaFX 模块

由于 JavaFX 是 Oracle 模块化的第一个 API,也是我们将需要用来创建 Pro Java 9 游戏的原生 Java 多媒体 API,因此使用它作为模块如何工作的示例是有意义的,这同时将向我们展示 JavaFX 是如何模块化的,以及我们可能需要为我们自己的 Java 9 模块定义文件“要求”(输入)哪些 JavaFX 模块。使用 Java 9 模块,JavaFX 可以直接链接到 JDK 的“映像”中,不需要引用外部 JFXRT。JAR 文件。JFXSWT 等第三方 jar。JAR 将成为自动模块。这个 JFXSWT。JAR 将被重命名为 JAVAFX-SWT。JAR for Java 9,这样当自动模块使用 JAR 文件派生其名称时,它将变成 JAVAFX.SWT。

Java 9 运行时环境(JRE)包含七个 JavaFX 模块,您可以根据需要在自己的游戏模块中“要求”这些模块。您需要的这些资源越少,游戏的数据和内存占用就越优化,就有越多的系统资源可以满足您的游戏处理需求。

表 5-6 向您展示了新的 JavaFX 模块层次结构,哪些模块(基本和图形)是任何 JavaFX 使用所必需的,以及哪些非基本和非图形 JavaFX 模块需要其他 JavaFX 模块。

表 5-6。

Seven Core JavaFX Modules Contained in the Java 9 Runtime Environment for Use in New Media Applications

模块名 必需的? 用于 必需的 JavaFX 8 模块
javafx.base 事件、实用程序、Beans、集合 无(这是一个基础 JavaFX 库)
javafx.graphics 舞台、图像、几何图形、动画 javafx.base
javafx.controls 用户界面控制模块 javafx.graphics (require 也将导入 base)
javafx.media 音频/视频媒体播放器模块 javafx.graphics
javafx.fxml JavaFX 标记语言模块 javafx.graphics
javafx.swing Java Swing 兼容性模块 javafx.graphics
javafx.web WebKit 支持模块 javafx.controls,javafx.media,javafx.graphics

例如,Java FX . web(WebKit API web engine)将需要 javafx.controls(用于音频和视频传输条用户界面元素的 UI 元素)、javafx.media(音频或视频回放编解码器支持)和 javafx.graphics(应用、舞台、场景、几何图形、图像、形状、画布、效果、文本和动画支持)。

由于模块化应用需要在那个module-info.java文件中列出依赖关系,让我们看看如何在 JavaFX 中查找使用 WebKit 支持 API 的应用。下面是module-info.java的语法:

module myWebKitApp.app { requires javafx.web; }

您可能想知道为什么在module-info.java文件的module myWebKitApp.app { … } module use声明中没有明确要求 javafx.web 模块要求的那些其他模块,人们可能会怀疑这些模块看起来应该更像这样:

module myWebKitApp.app      {
   requires javafx.base;
   requires javafx.graphics;
   requires javafx.controls;
   requires javafx.media;
   requires javafx.web;
}

这是因为链下游模块所需的模块是作为语法的一部分自动导入的。因此,如果我们创建一个不使用“固定”UI 元素(在 JavaFX 中称为控件)或 WebKit、FXML 或 Java Swing 的 JavaFX 游戏,我们可以只使用基本、图形和媒体模块。

因为 javafx.graphics 模块需要 javafx.base,而 javafx.media 模块需要 javafx.graphics,所以您只需在一行代码中编写整个模块声明 Java 文件,如下所示:

module ProJava9GamesDevelopment.app { requires javafx.media; }

在这三个 JavaFX 模块(base、graphics 和 media)之间,您拥有 Java 9 中游戏所需的一切,只要您制作自己的用户界面元素控制图形,这是游戏开发中的标准做法。在本书的课程中,我们将会看到这些核心模块中包含了哪些包、类和方法。当我介绍一个给定的包和类时,我会让你知道它是哪个模块的一部分。

Java 9 模块的目的:安全、强大的封装

多年来最大的抱怨之一是 Java 不像其他平台那样安全,这阻止了它作为数字内容分发渠道的广泛使用。喜欢所有的分发格式(Kindle,Android,HTML5 等。),安全的 DRM 也是需要的,并且在明年即将推出,但是 API 本身的内部安全性也是一个需要特别在 Java 中解决的问题,正如您将在本节中看到的,Java 9 Modules 强封装规则将通过阻止对 Java 内部 API 以及私有封装的访问来实现这一点。

Java 9 之前的应用必须模块化,以使用前面讨论的module-info.java定义文件“锁定它们”。只有显式导出的包才可见;用于创建应用的内部 API 将不再可见(可访问)。试图访问未显式导出的包中的任何类型都将引发错误。高级程序员将无法使用反射调用. setAccessible()方法来强制访问。对于测试,目前有一个命令行开关,允许访问非导出包。这将被称为- add-exports,应该仅在绝对必要时使用。

导出的包中的安全性也增加了,因为现在只有使用公共访问控制修饰符声明的类型才是可访问的。试图访问未使用公共访问控制声明的显式导出包中的任何类型都会引发错误。程序员将无法使用反射调用. setAccessible()方法来强制访问。为了测试,目前有一个命令行开关允许访问非公共类型。正如您可能已经猜到的,这将被称为- add-exports-private,应该只在绝对必要时使用。

强封装允许您有选择地只支持或公开 API 的部分(模块)。在这种情况下,我们使用 JavaFX API,我将在本书中向您展示如何仅使用两个核心 JavaFX 包(基础和图形)和 MediaPlayer(媒体)包,以及高度优化的新媒体资源来创建一个健壮的 i3D 游戏。我们将不需要包括“沉重的”WebKit (web)、Swing (swing)、FXML (fxml)或 UI (controls)包,这些包将减少分发文件数据占用空间,并增加这个 Pro Java 9 游戏的安全性。您访问(要求)的每个模块将列出其公开导出的包,并且您的公共类和方法将是 API 的一部分。

创建 Pro Java 9 游戏模块:使用 Exports 关键字

让我们继续看看如何设置我们将使用 JavaFX API 创建的 Java 9 游戏,方法是看我们如何使用 exports 关键字将您的 BoardGame 包添加到我们将用来创建游戏的 Java(Java FX)API 的其余部分。JavaFX launcher 将构造您的应用子类的一个实例,它将被称为 BoardGame 或类似的东西。你可以在图 5-4 中看到,这是在 Java 8 游戏开发初期创建的 InvinciBagel i2D 游戏。您可以使用 exports 关键字和包名称以及 to 关键字和模块名称,将包含此应用子类的包导出到 javafx.graphics 模块(包含将构成此游戏的大多数包和类)。下面是 Java 代码:

module BoardGame.app  {
       requires javafx.media;
       exports  boardgame.pkg to javafx.graphics;
}

需要注意的是,如果您使用 FXML,它允许非程序员设计 UI 布局,这将需要 javafx.fxml 模块,该模块需要能够访问您的私有变量和方法。这将需要在您的module-info.java声明文件中包含 exports private 关键字字符串,如下所示:

module BeginnerBoardGame.app  {
       requires javafx.media;
       requires javafx.fxml;

       exports private beginnerboardgame.pkg to javafx.fxml;
}

我们在这本专业的 Java 9 游戏开发书中没有使用 FXML,因为我们将使用 Java 和外部新媒体内容开发应用做所有事情,就像专业的 Java 9 游戏开发人员一样。这将使您的应用更加安全和紧凑,因为使用 FXML 的库在大小和范围上都是巨大的;使用 CSS、HTML5 和 JavaScript 的库(javafx.web)以及使用通用用户界面控件或小部件集合的库(javafx.swing 和 javafx.controls)也是如此。

资源封装:进一步的模块安全措施

Java 9 模块性不仅包括 Java 代码,还包括应用资源,在 Java 9 游戏中,包括音频、视频、图像、矢量(SVG)和动画资源。您可能知道,通过这些文件格式可以像通过文本文件格式一样容易地破坏安全性。阻止 Java 在 web、电子邮件、应用、电子书或 iTV 设备中起飞的唯一问题是安全性问题,看起来 Oracle 决心很快解决这个问题。Java 的另一个问题是一个巨大的 Java 运行时的部署和这个运行时的许多不同版本。新的模块特性将允许开发者优化他们的发行版,只包含需要的 Java 组件。

使用java.lang.Class类封装资源,并且只能使用Class.getResource()方法检索资源。从 Java 9 开始,不像以前的 Java 版本那样有ClassLoader.getResource()方法调用访问。

您也不能再使用 URL 来访问类中的资源,因此/path/name/voiceover.mp3 将不再有效。这再次使 Java 9 发行版更加安全。其他一些模块仍然允许 URL 访问,比如 javafx.web 和 Java FX . media;然而,我们将使用捕获的(即 JAR 内部的)媒体素材,并且我们不打算通过不需要 javafx.web (WebKit API)模块来将我们的游戏向互联网开放。包含资源的包必须是可访问的(导出的),以便资源“可见”并且不隐藏在公众视野之外。这是通过您的exports boardgame.pkg to javafx.graphics;行代码完成的。由于 javafx.media 也依赖于 javafx.graphics,exports boardgame.pkg to javafxmedia;也应该可以工作,因为你的模块需求链是从 Java FX . media➤Java FX . graphics➤Java FX . base 开始的,如表 5-6 所示。

如果您想将您的新媒体和设计素材外部化(对于我的 Android 和 Java 9 开发,我从来不这样做),有几个 JavaFX APIs 仍然接受 URL 对象或使用字符串 URL 值指定的 URL。其中包括 CSS(Java FX . web 中的级联样式表);FXML(JavaFX . FXML 中的 Java FX 标记语言);图像、音频和视频素材(javafx.media 和 Java FX . graphics);以及 HTML 和 JavaScript(WebKit WebEngine Java FX . web)。

摘要

在第五章中,我们回顾了 Java 编程语言中一些更重要的概念和结构。当然,我不可能在一章中涵盖 Java 的所有内容,所以我坚持使用本书中你将用来创建游戏的关键概念、构造和关键字。大多数 Java 书籍都有 1000 页或更多,所以如果你想真正深入纯 Java,我推荐 Apress 的 Pro Java 编程书籍。当然,随着本书的深入,我们将学习更多关于 Java 的知识,以及 JavaFX 9.0 引擎的类。

我们首先通过查看 Java 的语法(包括 Java 注释和分隔符)来了解 Java 的高级视图,然后我们了解了什么是应用编程接口(API)。我们还学习了 Java API 包含的 Java 包。

接下来,我们讨论了 Java 类,包括嵌套类和内部类,因为这些 Java 包包含 Java 类。我们了解到 Java 类有一个构造函数方法,可以用来实例化类中的对象。

Java 的下一层是方法,就像你在其他编程语言中熟悉的函数一样,我们看到了一种必需的 Java 方法,叫做构造函数方法。

接下来,我们看了 Java 如何使用字段或数据字段来表示数据,我们还看了不同类型的数据字段,如常量(固定数据字段)和变量(或可以改变值的数据字段)。

之后,我们仔细查看了 Java 访问控制修饰符关键字,包括 public、private 和 protected 访问控制关键字,然后我们查看了 nonaccess 修饰符关键字,包括 final、static、abstract、volatile 和 synchronized nonaccess control 修饰符关键字。

在我们介绍完基本的代码结构以及如何修改它们来完成我们想要它们做的事情之后,我们看了主要的 Java 数据类型,比如 boolean、char、byte、int、float、short、long 和 double,然后我们看了用于处理这些数据类型或将这些数据类型“桥接”到我们的编程逻辑的 Java 操作符。我们研究了用于数值的算术运算符、用于布尔值的逻辑运算符、用于数据值之间关系的关系运算符、允许我们建立任何条件变量赋值的条件运算符,以及允许我们为变量赋值(或在变量之间赋值)的赋值运算符。

接下来,我们看了 Java 逻辑控制结构,包括决策(我喜欢称之为决策树)控制结构和循环,或者迭代逻辑控制结构。我们学习了 Java switch-case 结构、if-else 结构、for 循环结构和 do-while 循环结构。

接下来,我们看了 Java 对象,学习了如何使用 Java 类、方法和构造器方法定义对象属性、状态和行为,我们看了 Java OOP 语言的继承和公共 Java 接口概念,学习了如何使用 Java 代码实现这些概念。

最后,我们看了 Java 9 中添加的 Java 模块,并通过 JavaFX API(第一个模块化的主要 Java API)的例子学习了如何定义模块层次结构和模块类型。我们了解了 Java 9 模块的优点,以及这是重新设计 Java 语言以获得公众对该语言的期望和我们一直期待的安全级别的一个重要步骤。

在下一章中,我们将在本书的剩余部分探讨 NetBeans 9 集成开发环境(IDE)以及如何为您的游戏开发创建基础(项目和核心 API)。

六、设置 Java 9 IDE:NetBeans 9 简介

让我们从第六章开始,了解 NetBeans 9 集成开发环境(IDE)的重要特性和特征,因为它是您将用来创建专业 Java 9 游戏和物联网应用的主要软件。尽管 Java 9 JDK 是您的专业 Java 9 游戏以及 NetBeans 9 IDE 的基础,但我们将从学习 NetBeans 开始我们的 Java 游戏编码之旅,NetBeans 是您的 Java 游戏项目的“前端”(或窗口,通过它您可以查看和工作)。

NetBeans 9 是 Java 9 JDK 的官方 IDE,因此,它将是您在本书中使用的。这并不是说你不能使用另一个 IDE,比如 Eclipse 或 IntelliJ,它们分别是 32 位 Android 4.4 和 64 位 Android Studio 3.0 的官方 IDE。我更喜欢将 NetBeans 9 用于我的新媒体应用,将游戏开发用于我的 Java 9 和 JavaFX 游戏以及物联网应用软件开发编程范例。

这不仅是因为 NetBeans 9 可以集成第三方插件,如 Gluon 的 JavaFX Scene Builder,还因为它是一个 HTML5+CSS4+JS IDE,我通常使用 Java 9、JavaFX、Android 4.4 和 Android 8.0 为我的客户创建我设计的所有东西,也使用 HTML5。我这样做是为了让内容可以在封闭的、专有的操作系统和平台上运行,这些操作系统和平台到目前为止还没有命名。正如你们大多数人所知,我更喜欢开放(源代码)软件和平台,正如你们可能在第一章中观察到的那样,因为它们“天生”是开放的,可免费用于商业用途,广泛可用,受到 99%的主要制造商的支持,并且不需要批准过程。或者我只为一个特定的硬件平台或一个操作系统发布应用。

值得注意的是,NetBeans 9 支持许多其他流行的编程语言,例如 C、C++、Groovy 和 PHP。我使用 NetBeans 9 进行 HTML、CSS 和 JavaScript 网站和应用开发,因为 NetBeans 正在迅速成为一流的 Java、JavaFX 和 HTML5 应用开发环境。

我们要做的第一件事是看一下 NetBeans 版本 9 中的新增功能。NetBeans 8.2 于 2016 年第四季度发布,大约在 Java 8 发布一年半之后。这种版本号同步并非巧合,因为 NetBeans 8.0 是在 Java 8 之后发布的,而 NetBeans 9 很可能会在 2017 年第四季度 Java 9 之后发布。在本章中,我们将了解为什么您希望使用 NetBeans 9,而不是旧版本的 NetBeans。

我们接下来要做的事情是了解 NetBeans 9 IDE 的各种属性,这些属性使它成为 Pro Java 9 游戏开发的宝贵工具。在这本书的过程中,我们将会看到它为你提供的所有令人惊奇的特性;在本章中,您将无法获得其中一些功能的实际操作经验,因为我们还需要开始创建您的游戏,并因此将引导代码库或应用基础结构放置到位,以便您可以真正体验这些 NetBeans 9 IDE 功能。

因此,在本章的后半部分,您将学习如何使用 NetBeans 9 创建 Java 9 和 JavaFX 9 项目。这样,您就可以通过创建一个真实世界的 i3D 棋盘游戏,开始稳步前进,并使您的 Pro Java 9 游戏开发成为现实,您将在本书的过程中开发该游戏。

NetBeans 9 的新特性:Java 9 模块集成

NetBeans 9 是该软件在稳定版 8.2 之后的下一个主要修订版,现在集成了 Java 9 模块系统、Java 9 运行时版(JRE)和 JUnit Java 测试套件,因此这些不必单独下载。如果您正在下载 NetBeans 9 for HTML5+CSS+JS、PHP 或 C++,您不再需要下载 JDK 或 JRE。这可以在 NetBeans IDE 下载包页面上看到,如图 6-1 所示,这也是为什么有针对 HTML5/JS、PHP 和 C/C++的 32 位(x86)或 64 位(x64)预编译 NetBeans 9 版本的原因。

A336284_1_En_6_Fig1_HTML.jpg

图 6-1。

The Java SE Edition NetBeans download bundle contains the NetBeans platform, Java SE, and JavaFX SDKs

也就是说,如果您使用任何其他版本(Java SE、Java EE 或 All ),就像本书一样,JRE 不包括在内。这是因为您将下载 JDK,正如您在第一章中所做的那样,以便能够使用这些 Java SE(或 EE,对于大公司而言)版本,正如您已经看到的,JRE 包含在下载和安装过程中。NetBeans 9 还支持 Apache Ant 和 Maven 存储库的最新版本。在本章的剩余部分,我将介绍自 2014 年第一季度 NetBeans 版发布以来的一些新功能。我将使用子部分对它们进行分类,并根据相关主题为读者进行组织。

Java 9 支持:模块、Ant、Java Shell、多版本

NetBeans 9 将与 Java 9 同时发布,因此它的主要目标是支持 Java SE 9 版本的所有特性和功能。这将包括新的 Java 9 模块特性,这将提高安全性,并使开发者能够优化他们的 Java 9 游戏发行版的数据足迹。这将包括基于 Ant 和基于 Maven 的 Java 9 项目,因此 Ant 和 Maven 构建系统将被升级以支持 Java 9 模块。

Java 9 SE 应用项目最初将支持单模块开发(包括所有模块)以及支持多模块开发的新项目类型,因此您可以挑选 Java 9 模块。我们最终将只使用几个核心 JavaFX 模块(base、graphics 和 media ),这样我们的发行版数据占用空间将会显著减少,但我们将在本书的最后完成,因为这是一个更高级的主题。

Apache Ant 正在更新,以获得对 JDK 9 的支持,涵盖基本的 Ant 任务,并且当 NetBeans 9 在 JDK 9 上运行或 JDK 9 被设置为您项目的 Java 平台时,Java 9 SE 发行版中的所有工具都将正确工作。NetBeans Profiler 现在可以与 JDK 9 应用协同工作,Java shell 支持以及与 NetBeans 9 IDE 的集成已经添加到 NetBeans 9 项目的每个级别。NetBeans 9 及其集成的 Java 9 支持现在可以正确处理多版本 JAR 文件。

最后,NetBeans 9 项目将很快转移到 Apache 上。该提案可在 https://wiki.apache.org/incubator/NetBeansProposal 进行审核。该提案涵盖了这一变化将如何影响 NetBeans 9 版本。在 NetBeans 9.0 和 9.0.1 版本中,将会迁移源代码、错误、构建作业和相关服务。

IDE 用户体验:更多信息和智能编码

NetBeans 8.1 引入了一个改进的代码导航窗格,它现在可以区分游戏的 Java 方法所包含的超类或接口,以及方法名及其返回类型。代码补全(我们将在本章的下一个主要部分中介绍)在 NetBeans 8.1(以及 8.2 和 9 等更高版本)的几乎所有方面都得到了显著改进,包括改进了最相关的 Java 代码插入项的预选、改进了前缀自动补全、改进了子词自动补全以及改进了 Java 枚举值的自动补全。

Java 代码分析:完全重新设计的 Java 分析套件

NetBeans 在 8.1 版中对其 Java 代码分析套件进行了全面的检查,包括简化的分析器设置、无需预先设置的新的单击式 Java 代码分析,以及只需选中代码分析结果旁边的复选框即可选择方法或类进行详细分析的能力。附加到正在运行的进程的能力有所提高,并且选定的 PID 会被记住以供后续会话使用。新特性包括监控 CPU 利用率、从分析的应用中转储线程的能力、在 CPU 分析视图中显示所选线程的合并结果的能力,以及改进的实时应用分析视图。

其他新的评测器功能包括 CPU 评测结果中的实时正向和实时反向调用树、系统内存评测结果中的实时分配树,以及在评测会话期间简化的设置调整。

该 NetBeans 分析引擎是 8.1(及更高版本)ide 中改进最多的,包括连接到正在运行的进程时显著加快的连接速度,对当前分析方法的传出调用的限制,以及为某些预选类分析系统内存性能的能力。所有这些都将有助于优化 Pro Java 9 游戏开发,因为游戏需要最高性能。

profiler 用户界面(UI)现在变得更加完美和专业,具有一个统一的分析窗口,在一个可定制的托管视图中包含所有操作、设置和结果。有一个单独的快照窗口窗格,您可以使用它来管理持久性分析数据。

还有一个全新的、100%重新实现的 profiler 表和树表区域,它提供了具有本机外观的分析外观,允许开发人员无缝集成代码开发和优化。

与 NetBeans IDE 的其余部分的分析器集成也有了很大的改进;此外,还有一个更加完美的 profile 菜单,添加了一个名为 Attach to Project 的新操作,并且在代码导航器中添加了 Profile 类和 Profile 方法操作。在本书的后面,当我们需要在 Java 9 游戏中实现系统内存和 CPU 优化时,我们将会看到 NetBeans 9 Profiler。

NetBeans 9 的主要属性:智能 IDE

在本节中,我将向您全面介绍 NetBeans 9 的所有强大功能,以便您了解安装在开发工作站上的这个 IDE 工具有多强大,以及掌握它的所有功能有多重要,这样您就可以作为一名专业的 Java 9 游戏或物联网应用开发人员来运用所有这些功能。IDE 是您使用 JavaFX APIs 编写的 Java 9 代码和您的计算机之间的接口;它允许您可视化您的代码,将其组织成逻辑方法,在您的计算机上测试它,分析它相对于您的系统内存和处理器周期的最佳运行情况,并将其打包以便通过网站在互联网上分发,或者作为 Windows、OS/X、Linux 或 OpenSolaris 的独立应用用于桌面计算机,甚至作为 Android OS 或 Tizen OS 的嵌入式设备应用。理想情况下,iOS、Opera OS 和 Chrome OS 也将在 2018 年前支持 Java 9 应用,因为 Android 和 Tizen 已经在 Linux 内核之上的 Java(Android OS)和 Linux 内核之上的 html 5(ti Zen OS)平台上占据了最大的市场份额。

NetBeans 9 是智能的:将您的代码编辑放在 Hyperdrive 中

虽然 IDE 确实非常类似于文字处理器,只是针对创建模块化代码结构而不是编写业务文档进行了优化,但是 NetBeans 9 这样的集成开发环境在开发人员的编程工作过程中提供的帮助要比文字处理器在作者的写作和文档创作工作过程中提供的帮助大得多。文字处理器主要用于使用桌面出版功能格式化文本以使其看起来像样,纠正拼写错误,以及纠正语法和句子结构。

例如,您的文字处理器不会对您为业务编写的内容提出实时建议,而 NetBeans 9 IDE 会在您实时编写代码时查看您正在编写的内容,并在您创建代码时帮助您完成 Java 代码语句和 Java 代码结构的编写。因此,NetBeans 9.0 可以说比工作处理器具有更高的人工智能商数,如 Microsoft Office、Corel WordPerfect、Apache Open Office 或 Ubuntu Libre Office current feature。

NetBeans 要做的事情之一是为您完成 Java 代码行,以及对代码语句应用颜色以突出不同类型的构造(类、方法、变量、常量、数组、引用),如图 6-2 所示。NetBeans 将应用行业标准代码缩进,使 Java 代码更易于阅读,无论是对您自己还是对您的 Pro Java 9 游戏和物联网应用开发团队的其他成员。

A336284_1_En_6_Fig2_HTML.jpg

图 6-2。

NetBeans includes Files, Services, Projects, Navigator, and Output Panes (left top to bottom) as well as a Java editor

NetBeans 还将提供匹配或缺失的代码结构元素,如括号、冒号和分号,以便您在创建复杂、深度嵌套或异常密集的编程结构时不会迷路。当我把你从 Java Game Developer 带到 Pro Java 9 Game Developer 时,你将创建具有这些特征的高级 Java 构造,随着你在本书中 Java 代码复杂性的进步,我将肯定指出当我们在你的游戏中实现它时,密集、复杂或深度嵌套的 Java 8 和 Java 9 代码。

NetBeans 还可以提供引导代码,例如我们将在本章稍后创建的 JavaFX 游戏应用引导代码,因为我知道您急于开始创建您的 Pro Java 9 游戏。NetBeans 9 提供了您可以填写和自定义的代码模板、编码提示和技巧以及代码重构工具。随着您的 Java 9 代码变得越来越复杂,它成为代码重构的逻辑候选者,这可以使代码更容易理解,更容易升级,并且更高效。NetBeans 还可以自动重构您的代码。

如果您想知道什么是代码重构,它是改变现有计算机代码的结构,使其更有效或更具可伸缩性,而不改变其外部行为(即,它完成了什么)。例如,NetBeans 可以采用传统的 Java 7 代码,并通过实现 Java 8 中引入的 Lambda 表达式来提高效率。

NetBeans 9 还将提供一种或另一种类型的弹出帮助器对话框,其中包含方法、常量、资源引用(在本书中编写 Pro Java 9 游戏时,您将了解所有这些内容),甚至是关于如何构造 Java 语句的建议。例如,NetBeans 9 会建议何时使用 Java 8 Lambda 表达式来简化代码并兼容多线程。

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

你的文字处理器不能做的另一件事是允许你给它增加功能,NetBeans 9 可以用它的插件架构做到这一点。描述这种类型的体系结构的术语是可扩展的,这意味着,如果需要,它可以扩展以包括附加的特性。例如,如果您想扩展 NetBeans 9 以允许您用 Python 编程,您可以这样做。NetBeans 9 也可以以这种方式支持 COBOL 或 BASIC 等较老的语言,尽管由于目前大多数流行的消费电子设备都使用 Java、XML、JavaScript、SVG 和 HTML5,我不太确定为什么有人愿意花时间这样做。我谷歌了一下,发现有人在 NetBeans 中用 Python 和 COBOL 编程,所以有现实证明这个 IDE 确实是可扩展的。

可能是因为其可扩展性,NetBeans 9 IDE 支持许多流行的编程语言,包括客户端的 C、C++、Java SE、Javadoc、JavaScript、XML、HTML5 和 CSS,以及服务器端的 PHP、Groovy、Java EE 和 Java Server Pages (JSP)。客户端软件在终端用户持有或使用的设备上运行(在 iTV 机的情况下),而服务器端软件在某处的服务器上远程运行,并且当软件在服务器上运行时,通过因特网或类似网络与终端用户对话。

客户端软件将更高效,因为它位于运行它的硬件设备的本地,并且因此它更具可伸缩性,因为不涉及经历任何过载的服务器。随着越来越多的人在任何给定的时间点使用服务器端软件,服务器过载总是会发生。您创建的 Java SE 9 和 JavaFX 游戏或物联网交付物往往位于客户端,在网站中交付并使用网站,但也可以通过 JNLP 下载在客户端使用,或者下载 JAR 或针对给定操作系统平台编译的可执行文件。

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

显然,项目管理功能在任何主流 IDE 中都必须非常强大,NetBeans 9 包含了大量的项目管理功能,这些功能允许您以多种不同的分析方式查看您的专业 Java 游戏开发项目及其相应的文件以及这些文件之间的相互关系。有六种主要的项目管理视图或窗格,可以用来观察项目中各种类型的相互关系。图 6-2 显示了 bootstrap pro Java 9 游戏开发 JavaFX 项目,我们将在本章稍后创建该项目。

图 6-2 显示了为这个新项目打开的六个主要项目管理窗格或窗口,这样你就可以准确地看到它们将向你展示什么。一个优秀的编程 IDE 需要能够管理可以变得非常庞大的项目,包括超过一百万行代码,并且包含在项目文件夹层次结构中的数百个文件夹中。这可能涉及数千个文本(Java 9 代码)文件,以及数百个文件形式的新媒体素材,有些是基于文本的(SVG、XML),有些是二进制数据格式的(JPEG、MPEG)。

“项目”窗格显示构成 Java 9 游戏项目的 Java 源代码包、库和模块。这可以在图 6-2 的左下方看到。顶部的窗格是文件窗格,显示硬盘上的项目文件夹及其文件层次结构。

下面的服务窗格显示了数据库、服务器、存储库、Docker 和构建主机,以便在项目中使用它们。这些主要是服务器端技术,这些技术通常用于大型开发团队,所以我们不打算详细讨论这些,因为这是一本为单人游戏设计者编写的书。

项目窗格应该始终保持打开,在 ide 的左侧,从图 6-7 开始,您将在本章的所有图中看到。“项目”窗格或窗口为您提供了 Java 9 游戏项目中所有项目源代码和资源(内容)的主要访问点。文件窗格不仅显示项目文件夹和文件层次结构,还显示每个文件中的数据、JavaFX 新媒体素材和 Java 9 代码层次结构。

“导航”窗格显示在 NetBeans IDE 底部的“文件”、“项目”和“服务”窗格下面,它显示了 Java 代码结构中存在的关系。在本例中,这是 JavaFXGame 类。start()方法,而。main()方法,我们将在第七章中学习,在我们学习了所有关于 NetBeans 9 IDE 的知识以及如何使用它来创建一个名为 JavaFXGame 的 Java 9 游戏项目之后,我们将很快完成这个项目。

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

NetBeans 9 的可扩展插件功能支持许多平台的 design-your-UI 拖放设计工具,包括 Java SE、Java EE、Java ME、JavaFX 和 Swing,以及 C、C++、PHP、HTML5 和 CSS4。NetBeans 9 支持为您编写应用 UI 代码的可视编辑器,因此您所要做的就是使屏幕上的可视内容看起来像您希望它在游戏应用中的样子。由于游戏使用 JavaFX 新媒体游戏引擎,NetBeans 支持 Gluon JavaFX Scene Builder Kit,这是一种高级 JavaFX 用户界面设计可视化(拖放)编辑器。

由于 JavaFX 具有 PRISM 游戏引擎以及 3D(使用 OpenGL ES 或嵌入式系统)支持,因此我们将主要关注本书的 i3D,因为我在《Java 8 游戏开发入门》(Apress,2014)中已经介绍过 i2D。这本书的假设是,读者将希望构建最先进的专业 Java 游戏,这将等同于利用 JavaFX 引擎的 3D 和 i3D,Java FX 引擎现在是 Java 8 和 9 的一部分(以及 Lambda 表达式)。最有效的方法是使用 Java 代码,而不是拖放式代码生成器。

开发 pro Java 9 游戏的最快方法是利用 Java 和 JavaFX 环境慷慨提供的高级代码和编程结构,供您在创建尖端应用时使用。在这种情况下,这将是 pro Java 游戏,其中包含强大的新媒体元素,如 2D 矢量、3D 矢量、数字音频、视频和数字图像,这些元素组合在一起成为一个统一的 2D 和 3D 混合内容创建管道。

NetBeans 9 对 Bug 不友好:用调试器消除 Bug

每一种计算机编程语言都有一个普遍的假设,即一个“bug”,或者没有完全按照您的要求运行的代码,其对编程项目的负面影响会随着问题得不到解决的时间越长而越大。正因如此,可以说这些虫子一“出生”就需要被“压扁”。NetBeans 9 有大量的错误查找代码分析工具,可以使用集成的 NetBeans 调试器来访问这些工具。NetBeans 9 还支持与第三方 Find Bugs 3.0.1 项目的集成,该项目可以在 SourceForge.net 上找到,如果你想下载单机版,可以在findbugs.sourceforge.net找到。

这些工具将我们在本章本节开始时讨论的实时“键入时”代码纠正和编码效率工具提升到高级调试的下一个级别。

您的 Java 代码不会变得那么复杂,直到本书的稍后部分,所以当我们需要在后面的章节中使用这些高级工具时,当您的知识库更高级时,我们将会介绍它们是如何工作的。

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

NetBeans 还有一个称为 Profiler 的东西,这是 NetBeans IDE 在 8.1 版中彻底改进的领域之一,正如我在前面的“NetBeans 代码分析”一节中指出的那样。NetBeans Profiler 工具会在 Java 8 或 Java 9 代码实际运行时查看这些代码,并告诉您它使用内存和 CPU 周期的效率。这种剖析分析将允许您优化代码,并使其更有效地使用关键系统资源,如线程、系统内存和处理周期。这对于 Pro Java 9 游戏开发来说非常重要,因为分析复杂的游戏可以帮助您在嵌入式系统上优化游戏的“流畅度”,与普通的六核和八核 CPU 相比,嵌入式系统不是很强大(例如,在单核或双核 CPU 上),或者在使用双核或四核 CPU 的不太强大的计算机系统上。

这个分析器是一个动态的软件分析工具,因为它在 Java 代码运行时查看您的代码,而 FindBugs 代码分析工具可以说是一个静态的软件分析工具,因为它只是在编辑器中查看您的代码,而不是“编译”并运行在系统内存中。因为我已经在第四章中讨论了静态和动态的重要性,所以你知道动态处理对于你的职业 Java 游戏开发工作过程来说是多么强大和 CPU 密集型。同样的考虑也适用于实时调试。NetBeans 调试器还允许您在代码运行时逐句通过代码,因此该工具可以被视为“混合体”,它跨越了静态(编辑)和动态(执行)代码分析模式之间的鸿沟。

为 Pro Java 9 游戏及其 JavaFX PRISM 引擎创建项目基础后,在本章的下一节中,如果您喜欢使用 IDE 顶部的 Profile 菜单,可以运行 Profiler。然而,如果你这样做,你根本看不到什么,因为 Hello World bootstrap 应用实际上什么都不做。

因此,在添加实时渲染的 3D 素材等内容时,我们将进入 NetBeans Profiler。在本章中,我将尽量让您“提前”了解 NetBeans 9 的这些关键功能,而不需要使用很多页面,这样您就可以轻松使用该软件,并且在本书的过程中,当 IDE 相关的任何内容(有时是字面意思)出现时,您不会感到惊讶或“措手不及”。

事不宜迟,让我们启动 NetBeans 9 并创建您的基于引导 JavaFX API 的 pro Java 9 游戏项目,这样我们就可以在本章中对您的 Pro Java 9 游戏进行一些 Java 9 和 JavaFX 编程。

创建 Pro Java 9 游戏项目:JavaFXGame

让我们言归正传,为您将在本书的课程中创建的 pro Java 9 游戏创建一个项目基础,以便您在本书的每一章中都朝着您的最终目标前进。我将在本书的过程中向您展示如何创建一个原创游戏,以便您看到创建一个不存在的游戏所涉及的过程,而不是大多数游戏编程书籍复制市场上已经存在的游戏或将素材拖放到预建的游戏引擎中。为了开始 Java 8 游戏开发(Apress,2014),我得到了我的客户的许可,允许读者在这本书的过程中看到创建 i2D InvinciBagel 游戏的过程。对于这本书,我将创建 i3D 棋盘游戏引擎,用于我自己的iTVboardgame.com网站。

单击任务栏上的快速启动图标,或者双击桌面上的图标以启动 NetBeans 9,您将看到 NetBeans 9 启动屏幕。此屏幕会显示一个进度条,告诉您如何配置 NetBeans 9 IDE 以供使用。这包括将 IDE 的各种组件加载到计算机的系统内存中,以便在 pro Java 9 游戏开发过程中可以流畅地实时使用 IDE。

将 NetBeans 9 IDE 加载到系统内存后,初始 NetBeans 9 起始页将显示在您的显示屏上,如图 6-3 所示。单击“起始页”选项卡右侧的 x。这将关闭此介绍页(选项卡),并显示 NetBeans 9 IDE,如图 6-4 左侧所示。

A336284_1_En_6_Fig3_HTML.jpg

图 6-3。

Close the Start Page tab (upper left) by clicking the x on the right side of the tab to reveal NetBeans 9 IDE

这将显示我称之为“原始”的 IDE,IDE 中没有活动的项目。现在好好享受吧,我们很快就会为你的项目组件在这个 IDE 中填充窗口(我称之为浮动调色板窗格,因为整个 IDE 都在我称之为窗口的地方)。你可以在图 6-4 中看到这个空 IDE 的一部分,没有太多可看的,因为目前只有顶部菜单和快捷图标,也在 IDE 的顶部,目前没有太多其他可见的。

如果您想知道,您退出的起始页仅在您第一次启动 NetBeans IDE 时显示,但是如果您希望以后打开此起始页选项卡,以便您可以浏览演示和教程部分,您可以这样做!要随时打开此起始页,您可以使用 NetBeans 9.x 帮助菜单,然后选择起始页子菜单。我通常会表示一个菜单序列,如帮助➤开始菜单,只是为了你将来的参考。如果你在本书后面看到这样的结构,它是嵌套子菜单的级联菜单序列。

在 NetBeans 9.0 IDE 中,您要做的第一件事是创建一个新的 JavaFXGame Java 项目。为此,我们将使用 NetBeans 9.0 新项目系列对话框。这是我在上一节中谈到的一个有用的 Java 编程特性,它使用正确的 JavaFX 库创建您的引导项目。main()和。start()方法、java 语句和 import 语句,所有这些都将在下一章中学习。点击你的 NetBeans 9 IDE 左上角的文件菜单,如图 6-4 所示,然后选择新建项目菜单项,恰好是第一个菜单项。

A336284_1_En_6_Fig4_HTML.jpg

图 6-4。

Use the File ➤ New Project menu sequence (upper left) to open the NetBeans 9 New Project series of dialogs

请注意,在新项目菜单项的右侧,列出了一个 Ctrl+Shift+N 快捷键组合,以防您想记住它。

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

系列中的第一个对话框是选择项目对话框,显示在图 6-5 的右侧。因为您将在游戏中使用强大的 JavaFX 新媒体引擎,所以在左侧的类别选择器窗格中,从所有编程语言类别的列表中选择 JavaFX 类别,对于步骤 2,该类别标有红色数字 2。

A336284_1_En_6_Fig5_HTML.jpg

图 6-5。

Use the Choose Project dialog to specify a JavaFX Application for your Pro Java Game

接下来,从右侧标有红色数字 3 的项目选择器窗格中选择 JavaFX Application,执行步骤 3。我们选择这个是因为你的 pro Java 9 游戏将是一种 JavaFX API 应用。您可以在描述窗格(显示为红色数字 4)中阅读每个项目类型的描述,最后单击下一步按钮前进到下一个对话框,在图 6-5 中显示为红色数字 5。

请记住,Oracle 决定将 JavaFX API(当时是库,现在是模块)集成到 Java 7 和 Java 8 中,因此 JavaFX 游戏现在只是一个 Java 游戏,而在 Java 7 之前(在 Java 6 中),JavaFX 2.0 是它自己独立的编程语言!JavaFX 引擎(您将在下一章中了解更多)必须完全重新编码为 Java 7(和 Java 8) API 或库集合(现在在 Java 9 中它变成了模块),才能成为 Java 9 编程语言的无缝集成组件。

JavaFX API 将取代 Abstract Windowing Toolkit (AWT)和 Swing (UI 元素),尽管这些旧的 UI 设计库仍然可以在 Java 项目中使用,但它们通常只由遗留(旧的)Java 代码使用,因此这些项目仍然可以在 Java 1.02、2、3、4、5、6、7、8 和 9 下编译和运行。您将编译并运行这个新的基于 JavaFX API 的项目,该项目是您在本章的这一节中创建的,因此您将看到 JavaFX 正在 Java 9 下运行。JavaFX 的当前版本是 9,因为 Oracle 使版本号与 Java 9 相匹配,然而,这些类与我在 Java 8 游戏开发入门书中使用的是相同的。

请注意,在其他窗格下面有一个描述窗格,它会告诉您所选择的内容。在这种情况下,这将是一个启用了 JavaFX 特性的新 Java 应用,其中“enabled”意味着 JavaFX 9 API 库将通过一系列 import 语句包含(并启动)在 Java 应用项目的类和方法中,您将很快在代码中看到这一点。您将在第七章中了解所有这些 Java 9 和 JavaFX 9 代码的作用,这将涵盖 JavaFX 9 及其许多用户界面设计和多媒体相关特性。

点击 Next 按钮,进入新 Java 项目系列对话框中的下一个对话框,即名称和位置对话框,如图 6-6 所示。该对话框允许您使用项目位置和项目文件夹数据字段设置应用项目名称,该名称将用于创建类名和包名,以及您希望项目存储在硬盘上的位置,这些也在图 6-6 中显示。

A336284_1_En_6_Fig6_HTML.jpg

图 6-6。

Name the project JavaFXGame, and leave all other naming conventions the way NetBeans set them

将项目命名为 JavaFXGame,保留默认的项目位置、项目文件夹、JavaFX 平台,并完全按照 NetBeans 为您配置的方式创建应用类设置,因为此 NetBeans 对话框将根据项目名称自动为您实现所有的类和包命名约定。

完成后,您可以单击“完成”按钮,这将告诉 NetBeans 9 为您创建 JavaFX 游戏应用,并在 NetBeans 9 IDE 中打开它,以便您可以开始使用它并了解 JavaFX API。

让 NetBeans 9.0 按照应该的方式为您做事情通常是一个好主意。如图 6-6 所示,NetBeans 使用该对话框中的项目位置和项目文件夹数据字段为用户文件夹和文档子文件夹创建逻辑C:\Users\Walls\Documents\NetBeansProjects\JavaFXGame文件夹。

对于项目文件夹数据字段,NetBeans 将(逻辑上)创建名为 JavaFXGame 的子文件夹。这将位于 NetBeansProjects 文件夹下,就像是您自己创建的一样,只有 NetBeans 9 为您完成了这项工作。

对于 JavaFX 平台选择下拉列表,NetBeans 9 默认为最新的 Java 9 JDK,也称为 JDK 1.9,具有最新的 JavaFX API(现在是 Java 7、8 和 9 语言的集成部分)。

在这一点上,我们不打算实现一个定制的预加载器项目,尽管如果我有时间和剩余的页数,我可能会在本书的后面部分重新讨论这个问题。因此,请不要选中此选项,这样您就可以学习自己创建这个预加载器 Java 9 项目代码,而不是让 NetBeans 9 为您完成。

因为您没有创建共享库的多个应用,所以不要选中使用专用文件夹存储库复选框,最后,确保正确配置了创建应用类。Java 9 类应该命名为 JavaFXGame,应该包含在 javafxgame 包中。

在这个配置中,包路径和类名将是javafxgame.JavaFXGame。这将跟在 PackageName 后面。ClassName Java 类和包名范例、camelCase 大小写和路径,使用点符号句点字符将包名连接到类名的头部,显示它保存的位置。

在第七章中,我将查看图 6-7 中所示的 Java 代码的一些基本组件,因为在本章中,我们将主要关注 NetBeans 9.0 IDE 及其特性,在第七章中,我们将完全专注于 JavaFX 编程语言,然后在第八章中,我们将讨论场景图。

如图 6-7 所示,NetBeans 已经写好了 package 语句,7 条 JavaFX API package import 语句,以及public class JavaFXGame extends Application声明;子类化你的 JavaFXGame 类,使用一个 JavaFX Application超类,创建一个启动public void start(Stage primaryStage)的方法,创建一个. main()方法来管理你的主 JavaFX 线程public static void main(String[] args)

如图 6-7 所示,NetBeans 9 将重要的 Java 编程语句关键字着色,将关键字用蓝色表示,字符串对象用橙色表示,内部 Java 和系统引用用绿色表示,注释用灰色表示。NetBeans 9 IDE 插入的有关 Java 9 代码的警告和建议用黄色标出,而阻止编译可执行文件(JAR)的 Java 9 编码错误用红色标出。

第 20 行还显示 NetBeans 提供了将按钮对象事件处理转换为 Lambda 表达式的功能,方法是给它加黄色下划线(警告:这可以转换为 Java 8 Lambda 表达式)。

A336284_1_En_6_Fig7_HTML.jpg

图 6-7。

Examine the bootstrap JavaFX code NetBeans created for you, based on the Name and Location dialog

如果您愿意,也可以更改这些颜色,但是我建议您使用 Oracle NetBeans 9 和更早版本实现的行业标准编码颜色,因为这些颜色已经随着时间的推移变得标准化了。

在运行此引导代码以确保 NetBeans 9 为您编写的引导 Java 9 代码能够正常工作之前,您需要将此代码编译为可执行格式,以便使用系统内存运行。NetBeans 9 还为您管理编译和运行过程,即使这些操作实际上利用了 Java 开发工具包(JDK)提供的实用程序。

接下来,我们来看看如何使用 NetBeans 9 完成这一任务,使用 NetBeans 9 的“运行”菜单,其中包含运行、测试、构建、清理、编译、检查、验证、生成 JavaDoc 以及其他与运行相关的 Java 编译功能。

在 NetBeans 9 中编译一个 Pro Java 9 游戏项目

为了向您展示如何在运行和测试 Java 游戏代码之前编译它,我在这里向您展示了一步一步的工作过程,这样您就可以接触到编译/构建/运行/测试 Java 代码测试过程的每一步。点击 Run 菜单和 Run Project (JavaFXGame) (first)菜单项,构建、编译并运行您的 Java 9 和 JavaFX 代码,如图 6-8 所示。您也可以使用 F6 快捷键,如菜单项选择右侧所示。现在您的项目已经准备好进行测试了!

A336284_1_En_6_Fig8_HTML.jpg

图 6-8。

Use Run ➤ Run Project (JavaFXGame) to build and run the project to make sure the NetBeans IDE is working

图 6-9 显示了 NetBeans 9.0 构建/编译/运行进度条,该进度条在编译期间将始终出现在 NetBeans 9.0 IDE 的右下角。还显示了输出窗格,最大化以便我们可以看到 Ant 构建过程做了什么,我们将在本章的下一节中更详细地了解它。

这里需要注意的是,只要您使用文件➤保存菜单序列或 Ctrl-S 键盘快捷键,NetBeans 9.0 就会编译您的项目代码,因此,如果您在创建引导代码后立即使用了 NetBeans IDE 的保存功能,您就不需要执行此编译过程,我刚刚向您演示了如何手动执行此过程,因为此过程会在您每次保存 Java 游戏项目时自动完成。

图 6-9 中还显示了输出窗格或窗口正上方的。Java 代码编辑窗格或窗口中的 start()方法。左边是一个减号图标,周围有一个正方形。这应该用于折叠或隐藏该方法的内容。只需单击代码编辑窗口左侧的减号图标即可。

减号图标将变成加号图标,以便折叠的代码块可以“展开”(取消折叠)。现在,我们已经了解了如何在 NetBeans 9 中编译项目,以及如何在 JavaFXGame.java 项目的代码中折叠和展开逻辑方法代码块(Java 类的逻辑功能组件)的视图,是时候运行这些代码了,看看它是否有效。如果是这样的话,我们可以进入第七章,开始学习 JavaFX API 以及它给 Java 9 编程环境带来的新的媒体开发能力。

在 NetBeans 9 中运行您的专业 Java 游戏项目

既然您已经创建并编译了使用 JavaFX game 项目的引导 Java 9,那么是时候运行或执行引导代码了,看看它能做什么。正如您已经了解到的,您可以使用 NetBeans 顶部的“运行➤”“运行项目”菜单序列来访问“运行项目”菜单项,或者如图 6-9 左上角所示,您可以使用看起来像绿色视频传输播放按钮的快捷图标。如果你把鼠标放在上面,你会得到一个浅黄色的工具提示,显示一个运行项目(JavaFXGame) (F6)弹出帮助信息。在编写 Java 9 和 Android Studio 书籍时,我通常会使用较长的菜单序列,而不是快捷图标,这样做只是为了更彻底。这向读者显示了 IDE 菜单系统中所有东西的位置,所以所有东西都被包含了。如果还没有,现在就运行新的 JavaFXGame 应用。一旦运行了编译好的 Java 9 和 JavaFX 代码,就会在 NetBeans IDE 上打开一个窗口,在其中运行您的软件,如图 6-9 右侧所示。目前它使用流行的 Hello World 示例应用。

A336284_1_En_6_Fig9_HTML.jpg

图 6-9。

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

在 Java 9 代码编辑窗格和代码编辑器窗格底部的输出选项卡之间的分隔线上方单击并按住鼠标左键,向上拖动分隔线,调整相对窗口空间的大小。该空间在 JavaFXGame.java 代码编辑窗格和输出 JavaFXGame 信息窗格之间共享。做这个调整大小的操作将会显示你的输出标签和它的编译信息内容,如图 6-9 所示。

这个 Output 选项卡将包含 NetBeans 9 的不同类型的输出,例如编译操作输出、运行操作输出(如图 6-9 所示)、探查器操作输出(我们将在本书后面的内容中对其进行分析),甚至是您的应用本身的输出(我们将在这里查看)。

你可能已经注意到,在图 6-7 中,你的 bootstrap Java 和 JavaFX 应用的代码在第 23 行使用了一个绿色的System. out .println("Hello World!"); Java 语句,所以如果你想看到你当前运行的应用打印到输出窗格(这就是 out 的意思,通常被称为输出控制台),你可以在你的 IDE 上当前运行的 Hello World 应用中点击“Hello World”按钮。

一旦你点击按钮,就会出现“你好,世界!”应该出现在 Output 选项卡中的红色文本下,表示它正在执行 JavaFXGame.jar 文件。JAR 文件是 Java 归档文件(J 代表 Java,AR 代表归档文件),是 Java 9 应用可用的可分发格式之一。

编译过程的一部分涉及到创建这个文件,所以如果编译后的版本可以工作,那么当所有的应用设计、编程、测试和优化完成后,您就可以准备好发布 JAR 文件了。

JAR 文件不包含您实际的 JavaFX 代码,而是一个压缩的、加密的“Java bytestream”版本的应用,JRE 可以执行和运行它(就像 NetBeans 9 现在所做的那样)。JavaFXGame.jar 文件前面的“路径”告诉您 NetBeans 9 将 jar 文件编译到硬盘上的哪个位置,以及它当前从哪个位置访问它以便能够运行它。在我的系统上,这个位置如下:

C:\Users\Walls\Documents\NetBeansProjects\JavaFXGame\dist\run1381287366\JavaFXGame.jar

让我们看看其他一些输出选项卡文本,以便了解 NetBeans 是如何运行项目的 JAR 文件的。首先使用ant -f source-path jfxsarun调用 Ant 构建系统,由于在 JDK 中找不到 Java 可执行文件,所以它会在运行时中找到一个。然后它初始化(init:),并在 JAR Dependencies (deps-jar:)部分创建一个\build目录并更新 build-JAR-properties 文件。然后创建\build\classes\build\empty\build\generated-sources\ap-source-output目录。然后,Ant 会将项目编译到\build\classes目录,如果构建(编译)成功(没有错误),Ant 会创建\dist发行文件夹,并将 JAR 文件存放在那里。

然后 Ant 使用 JavaFX Ant API 启动ant-javafx.jar并部署 JavaFX API,将 JavaFX JAR 文件复制到一个\dist\run1381287366文件夹中。最后,Ant 使用jfx-project-run:运行 JavaFX 项目,执行 Java 9 和 JavaFX 代码,这相当于运行(和测试)它,如 NetBeans 9 IDE 顶部所示。

Ant 是创建 JAR 文件的“构建引擎”或构建工具,如果您愿意,还可以在 NetBeans 中使用其他构建引擎,如 Maven 和 Gradle,因为正如您现在所知道的,NetBeans 是可扩展的。由于 Ant 可以追溯到最早,并且是“遗留”构建系统,我们将在本书的过程中使用它。

摘要

在第六章中,我们介绍了 NetBeans 9“官方”集成开发环境(IDE ),它将作为 Java 9 游戏开发工作流程的基础和主要工具。这是因为这个 IDE 是您编写、编译、运行、测试和调试 Java 9(和 JavaFX API)代码的地方,也是您的新媒体(图像、音频、视频、3D 几何、纹理、字体、形状等)的地方。)使用 NetBeansProject 文件夹及其子文件夹存储和引用素材。我们首先概括介绍了 NetBeans 9 及其新功能,如 Java 9 模块支持,以及 NetBeans 8、8.1 和 8.2 中添加的一些最新的传统功能。这些强大的特性使 NetBeans 9.0 成为 Java 9 的官方 IDE。这些特性将帮助程序员第一次快速、高效、有效地开发 Pro Java 9 游戏代码。在这个概述之后,我们使用新的项目系列对话框和 JavaFX 应用引导 Java 代码模板创建了一个 Pro Java 9 游戏项目。

我们浏览了新的➤ Java 应用系列对话框,并为我们的游戏创建了一个 JavaFX 框架,这将允许我们使用新的媒体素材。之后,我们了解了如何使用 NetBeans 9 编译(构建和运行)应用,以及如何使用 NetBeans 运行应用。我们查看了 Output 选项卡以及它如何用于编译器输出,并查看了 Ant 构建过程,以了解它如何将 Java 9 与其 JavaFX APIs 结合起来。

在下一章中,我们将浏览 JavaFX 编程语言,如果你愿意的话,可以称为“JavaFX 初级读本”,并检查 JavaFX 引导应用中的 JavaFX 代码(如图 6-7 所示),这样你就会知道这个 JavaFX 代码在做什么。我们还将研究全面的 JavaFX API 的模块化组件。

七、JavaFX 9 简介:JavaFX 新媒体引擎概述

在第七章中,让我们以您在前两章中复习的 Java 9 编程语言和 NetBeans 9 IDE 的知识为基础;我们将详细回顾构成 JavaFX 9 新媒体引擎的功能、组件和核心类。这个 JavaFX 9 新媒体 UI 和 UX API 是使用在第六章中看到的javafx包添加到 Java 中的,当时你创建了 bootstrap pro Java 9 游戏应用。之前的 JavaFX 8 API 是随 Java 8 发布的,也兼容 Java 7,以及 Android 和 iOS。JavaFX 软件包对游戏编程非常重要,因为它们包含了游戏编程需要使用的高级新媒体类,包括使用场景图将场景组件组织成层次结构的类、用户界面布局和设计的类、2D 数字插图(称为矢量图形)的类,以及数字图像(称为光栅图形)、2D 动画(矢量和光栅)、数字视频、数字音频、3D 渲染、网页渲染引擎(WebKit)等的类。我们将在这一章中触及所有这些,这样你就知道 JavaFX 已经作为一个 API 被添加到 Java 中,你可以为 Java 9 游戏做些什么了。

在本书的早期深入 API 细节概述的基本原理是激发您大脑的创造性,以便您可以开始思考 JavaFX 新媒体引擎功能如何支持您的专业 Java 游戏概念和设计。您不仅需要知道 JavaFX 可以为您的游戏开发做些什么,而且所有的 API 类都是相互关联的,因此您需要了解 JavaFX 新媒体引擎的各个组件是如何组合在一起的。JavaFX 使用一组复杂的 API 来实现令人难以置信的“前端”功能,我喜欢称之为引擎。这是因为它为实现用户界面(UI)和用户体验(UX)带来了内在的力量,使您的专业 Java 游戏和物联网应用“获胜”。因此,请耐心听我解释这些“基础”章节,它们涵盖了如何掌握您的 IDE (NetBeans 9)、您的基础编程语言(Java 9)和新的媒体引擎(JavaFX 8),后者现在是一个集成的 Java 平台 API,并且在浏览器支持、功能和受欢迎程度方面都在快速增长。

在本章中,您将回顾 JavaFX QUANTUM toolkit、PRISM 渲染技术、WebKit web 引擎、GLASS windowing 技术、JavaFX 媒体引擎、JavaFX 场景图和 JavaFX API。

一旦你看到 JavaFX 是如何在最高层次上组合在一起的,就像你在 Java 9 的第五章中所做的那样,你将会看到一些你将会用来构建专业 Java 游戏的关键类。其中包括节点,以及以下内容:组、场景、舞台、布局、控制、堆栈面板、形状、几何体、媒体、图像、摄像机、效果、画布、绘画和动画。我们已经在第六章中看过 JavaFX 应用类;我们将继续学习这个类,以及可用于构建复杂多媒体项目(如游戏)的各种类。

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

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

正如我在涉及新媒体的第 2 和第三章中所做的那样,我想从 JavaFX 的最高层次开始,也就是场景图。这是在图 2-1 和图 3-7 中最顶层显示的新媒体素材类型的右下一层。JavaFX API 的场景图 Java 代码也可以通过使用 Gluon 拖放式 JavaFX 场景构建器来构建,该构建器可以集成到 NetBeans 9 中,正如您在第六章中所了解的那样,也可以作为独立使用。因为这是专业 Java 9 游戏开发,我们将会看到如何“临时编码”所有这些场景结构。

如图 7-1 所示,JavaFX 场景图架构位于 JavaFX API 之上,JavaFX API 是 JavaFX 包的集合,如 javafx.scene 或 javafx.application,它最终允许您构建场景图并设计 Java FX 新媒体作品。在这种情况下,它将是一个专业的 Java 游戏。请注意,JavaFX API 不仅连接(在此图中使用钢轴承来表示桥)到它上面的场景图架构,还连接到它下面的 Java API 及其 JavaFX Quantum 工具包。如您所见,Java JDK(和 API)将 JavaFX 新媒体引擎连接到 NetBeans 9 和 JVM。JVM 允许 Java 在 Java 当前支持的各种平台以及未来(原生支持)的平台(如 Android 8 和 iOS)上分发你的 Pro Java 游戏。

A336284_1_En_7_Fig1_HTML.jpg

图 7-1。

JavaFX Component Architecture from Scene Graph at the top down through Java, NetBeans, JVM, and OSs

连接到 JavaFX API 的 Quantum toolkit 将我接下来要谈到的所有强大的新媒体相关引擎捆绑在一起。Quantum toolkit 处理所有这些引擎的线程管理,以便您的游戏代码(位于 JavaFX 主线程上)和游戏的新媒体素材(音频、视频、3D 矢量、2D 矢量)位于自己的线程上(A/V 使用专用线程,WebKit、窗口或 3D 渲染也是如此),可以通过双核、四核、六核和八核 CPU 上的独立线程(或进程)使用独立的处理器,这些处理器在当今的计算机和嵌入式消费电子设备中非常常见。我已经展示了新媒体引擎,它们非常重要,足以在图 7-1 的第四层中拥有自己的线程。

Glass Windowing Toolkit 控制 JavaFX 的窗口管理。它负责控制显示中的任何离散区域,如舞台或弹出窗口,如对话框。Glass 还管理事件处理队列,并将事件传递给 JavaFX 进行处理,它还设置计时器,这一点您将在本书稍后进入游戏时了解,以及 pulse 毫秒如何控制游戏的计时。

在图 7-1 中间可以看到,还有一个 WebKit 引擎和一个媒体播放器引擎。这些也由 Quantum toolkit 管理。WebKit 引擎可以呈现 HTML5、CSS3/4 和 JavaScript 内容。这意味着您可以创建在 JavaFX 游戏中无缝运行的 web 内容。媒体播放器媒体播放引擎卸载(处理)数字音频和数字视频素材的播放、UI 控制和导航。

Quantum toolkit 下面最重要的新媒体引擎是 Prism 引擎,我喜欢称之为“PRISM 游戏引擎”,因为它使用 Java2D 渲染 2D 内容,如果用户使用 Windows 7、8 或 10 平台,则使用 OpenGL (Macintosh、Linux 或嵌入式操作系统)或 DirectX 渲染 3D 内容。我在一些生产工作站上使用 Windows 7.2 和 10。Windows XP 和 Vista 支持已停止,因为大多数计算机和消费电子设备现在都支持 64 位(Windows XP 是 32 位的,只能处理 3.24 GB 的内存)。

PRISM 所做的是“桥接”主要操作系统平台以及消费电子嵌入式设备上的强大游戏引擎(DirectX 和 OpenGL),以便 JavaFX 可以将复杂的渲染任务处理从 nVidia (GeForce)、AMD(镭龙)、ARM、高通或英特尔“卸载”到 GPU 硬件。这使得 JavaFX/Java 游戏速度更快,并允许游戏使用更少的 CPU 处理能力来将游戏资源渲染到屏幕上。这反过来允许更多的 CPU 处理能力用于游戏逻辑,如人工智能或碰撞检测。在我们掌握了 JavaFX 引擎的特性及其场景图层次和架构之后,我们将在本书的下两章,JavaFX 初级读本和关于场景图设计的第八章中进入游戏设计的这些领域。

值得注意的是,游戏开发人员不需要了解 Quantum(线程)、Glass(窗口)或 Prism(渲染)引擎的内部工作原理,就可以利用它们强大的新媒体功能。在本书中,您将重点关注顶层场景图架构,以及该图的 JavaFX 和 Java API 级别。您还将涉及 NetBeans 9 IDE 级别,我们刚刚在第六章中讨论了这一级别,但是我们还将在本书的剩余部分中更详细地探讨它的特性。

就图 7-1 中图表的较低层而言,NetBeans 9.0 将为每个操作系统平台生成一个 Java 字节码文件,该文件可由定制 JVM 或 Java 9 虚拟机读取。这个 JVM,如图 7-1 底部所示,可以通过下载 Java 9 Java 运行时引擎(JRE)为任何给定的操作系统平台安装,你已经在第一章中遇到过,当你把它作为你的 Java 9 JDK 安装的一部分安装时。

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

在 iOS 8 和 Android 8 上已经有一个用于运行 JavaFX 应用的工作进程,尽管这种支持还不是“原生的”,所以 JavaFX 应用还不能直接在这些操作系统中运行。如果你对这方面的最新信息感兴趣,只需谷歌“Android 上的 JavaFX”或“iOS 上的 JavaFX”,你可以打赌,到 2018 年,Android OS、iOS、黑莓和 Tizen OS 设备将“原生”运行 JavaFX 应用。这将允许您使用这个 Java 和 JavaFX 动态组合“一次编码,随处运行”!我所说的原生是指,有一天你将能够使用 JetBrains IntelliJ 2017 Android Studio 直接将 Java(和 JavaFX engine)应用导出到 Android 8,或者导出到 iOS 或 Tizen OS,甚至可能使用 NetBeans 9 直接导出到 Android 8。这将增加 NetBeans 9 目前对 Windows、Mac、Open Solaris 和 Linux 发行版的支持。

Note

JetBrains IntelliJ IDEA 现在是用于创建 64 位 Android 8 应用的官方 IDE。这个 IDE 在我的 Android Apps for Absolute 初学者(Apress,2017)中有所涉及,其中涉及使用 Java 8 使用 IntelliJ IDEA 开发 64 位 Android 5 到 8 应用。IntelliJ 也包含在我的 Pro Android 可穿戴设备中(Apress,2015)。我预计到 2018 年底,Java 9 还将支持可穿戴设备和设备。

让我们从图 7-1 所示的图的顶部开始,看看 JavaFX 场景图和 javafx.scene 包,它使用 16 个强大而有用的 Java 类在 JavaFX API 中实现场景图。

JavaFX 场景包:16 个 Java 场景类

在我们的高级概述之后,我想做的第一件事是看看最重要的 JavaFX 包之一,javafx.scene 包。正如你在第 2 和 3 章中看到的,JavaFX 包不止一个。正如你在图 6-7 顶部看到的,你的 JavaFXGame.java 应用已经在使用四个不同的 JavaFX 包了。javafx.scene 包包含 16 个强大的 Java 类(记住 javafx 是用原生 Java 重新编码的),包括 Camera、ParallelCamera 和 PerspectiveCamera、Cursor 和 ImageCursor、LightBase、PointLight 和 AmbientLight,以及场景图形类(节点、父节点、组、场景和子场景)和一些实用程序类,如图 7-2 所示。如您所见,我已经对这 16 个 javafx.scene 包类进行了逻辑分组。我在此图的场景图部分中使用了场景类,因为使用此场景类创建的场景对象将包含使用其他四个节点、父、组和子场景场景场景图相关类及其子类创建的场景图对象。在这一章的后面,我们将会详细介绍所有这些场景图类。

A336284_1_En_7_Fig2_HTML.jpg

图 7-2。

The Java javafx.scene package and 16 core Scene Graph, Scene Utility, Lighting, Camera, and Cursor classes

JavaFX 的场景图架构类从最高级别的节点超类开始,连同其父类、子场景子类和父类的组子类,我们将在本书的后面使用它们来创建我们的游戏场景图层次。这些核心节点类用于创建游戏的 JavaFX 场景图形层次,并用于组织和分组使用 JavaFX 媒体素材和图形设计包创建的对象,这些包包含在 javafx.media 和 javafx.graphics Java 9 模块中。

有三个场景工具类,正如我所说的,它们允许您在任何时候拍摄场景或其任何场景图形节点的快照(如截图),以及打开或关闭场景抗锯齿,如果您在场景中使用 3D 基本体(使用数学而不是网格定义的几何体)。javafx.scene 包中的另一半(八个)类用于场景照明、场景摄影机和场景的光标控制。

在我们创建游戏时,我们将在以后的章节中讨论这些 javafx.scene 类,在此之前,我们将了解用于创建、分组、管理和操纵 javafx 场景内容的场景图形类。因此,我将覆盖 javafx.scene 包的类,如图 7-2 所示,从图的左侧移动到图的右侧,按照您可能最经常使用的类到最不经常使用的类的顺序。也就是说,所有这些类(可能除了快照)对 i3D 游戏都非常重要。

JavaFX 场景类:定义维度和背景颜色

javafx.scene 包中的两个主要类是 scene 类和 Node 类。我们将在下一节中介绍 Node 类及其父类、组类和子场景子类,因为这些类及其子类(如 JavaFXGame 类中使用的 StackPane 类)可用于在 JavaFX 中实现场景图架构。此外,在某种意义上,在我的图 7-2 和 7-3 中,节点类及其子类可以被视为在场景类的“下面”,尽管节点类不是场景类的子类。事实上,节点(场景图)类及其子类,或者更确切地说,使用这些类创建的对象,实际上包含在场景对象本身内部,就像在现实生活的舞台作品中按场景分组一样。出于这个原因,我们将首先看看如何使用 Scene 类及其 Scene()构造函数方法为 JavaFX 应用创建场景对象。这一节将为你在第五章中学到的关于重载构造函数方法的内容提供一个很好的强化,因为创建一个场景对象需要几种不同的方法。

这个场景类用于创建场景对象,使用 Scene()构造函数方法。这需要一至五个参数,取决于您选择使用六个重载构造函数方法中的哪一个。其中包括以下构造函数方法,这些方法具有六种不同的重载参数列表数据字段配置:

Scene(Parent root)
Scene(Parent root, double width, double height)
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 fill)

图 6-7 和第 28 行的 Java 代码中所示的当前 bootstrap Java 和 JavaFX 代码中使用的构造函数是第二个构造函数,到目前为止,它的结构(调用)如下:

Scene scene = new Scene(root, 300, 250);

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

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

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

StackPane root = new StackPane(); // StackPane subclassed from Parent; so Parent root node type

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

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

Scene 3Dscene = new Scene(root, 300, 250, true, true);

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

Scene Graph 并不是 JavaFX 独有的,现在可以在许多新媒体内容创建软件包中看到,例如 3D、数字音频、声音设计、数字视频和特效。场景图是内容数据结构的可视化表示,类似于倒置的树,根节点在顶部,分支节点和叶节点从根节点出来。我第一次看到场景图方法进行场景设计是在我使用芬兰 RealSoft OY 的一个名为 Real3D 的 Amiga 4000 软件包进行 3D 建模、渲染和动画制作的时候。从那时起,这种方法被大量的 3D、数字视频和特效软件包复制,现在是 JavaFX 组织场景内容的方式。出于这个原因,你们中的许多人可能对这种设计范例很熟悉,因此也很适应。场景图数据结构不仅允许您构建、组织和设计 JavaFX 场景及其内容,而且如果您正确设置了场景图,它还允许您将不透明度、状态、事件处理程序、变换和特殊效果应用于场景图层次的整个逻辑分支。图 7-3 显示了一个基本的场景图树,根节点在顶部,根节点下面是分支节点和叶子节点。

A336284_1_En_7_Fig3_HTML.jpg

图 7-3。

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

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

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

层次结构中的最后一级是“叶”节点,叶节点是分支的末端。因此,叶节点可以没有子节点。重要的是要注意叶节点可以直接脱离根节点,如图 7-3 所示。可以通过使用父类、组类或子场景类来创建分支节点,如图 7-2 所示,或者使用它们的任何子类,例如 WebView、Region、Pane 或 StackPane 类。

位于分支最末端的对象(即叶节点)的示例包括 JavaFX 类(实例化为对象),这些类可以使用参数进行配置。示例包括形状、文本或控件。这些是设计或内容组件,因此没有设计任何子组件(子对象),因此根据其类函数设计的性质,它们本质上必须位于树和分支的末端。

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

父类的三个子类可以用作分支节点。这些包括用于分组子(叶节点)对象的 Group 类,以便不透明度、变换和特殊效果可以同时应用于组节点;Region 类,用于对 2D 的子对象(叶节点)进行分组以形成屏幕布局,如果您愿意,可以使用 CSS3 对其进行样式化;WebView 类,用于管理 WebEngine 类,它在 WebView 中呈现 HTML5、JS 和 CSS 内容。

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

接下来,让我们看看图 7-2 中间列出的八个类,它们提供了一些强大的多媒体工具来控制你的应用的光标,以及为你的 2D 和 3D JavaFX 应用提供定制的灯光特效和定制的相机功能。在这种情况下,这可能是游戏,但也可能是电子书或 iTV 节目或任何需要 JavaFX 为 Java 9 APIs 提供的强大新媒体功能的物联网。

图 7-2 中间部分列出的更一般化的类(光标、光源、相机)是父类,更专门化的类(图像光标、点光源、平行相机等。)后面列出的是这些父类的子类。除了 LightBase 类,这似乎是显而易见的!

正如您可能已经猜到的(正确),JavaFX Cursor 类可用于控制在任何给定时间使用的应用光标图形(箭头、手、闭合的手、张开的手、调整大小、移动、文本、等待、无)。ImageCursor 子类可用于定义和提供基于图像的自定义光标,使用自定义光标图像中的 X 和 Y 位置来定义其“单击点”(也称为光标的“热点”)的位置。

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

如果设置正确,您也可以在 2D 游戏中使用这些照明类,或者在“混合”2D 和 3D 游戏中使用照明,由于 JavaFX 支持它,我们也将在本书的稍后部分研究这一点。

Camera 类及其 ParallelCamera 和 PerspectiveCamera 子类可用于拍摄或录制场景,并可用于 3D、2D 和混合游戏应用。其中两个相机类 camera 和 ParallelCamera 不要求运行 JavaFX 应用(在本例中为游戏)的平台上具备 3D (GPU)功能。在 3D 软件中,平行相机视图有时称为正交投影。

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

PerspectiveCamera 类提供了一个用于 3D 场景的复杂得多的相机,它将支持 3D 查看量。与 LightBase 类及其子类一样,PerspectiveCamera 类将需要运行 pro Java 9 游戏(或物联网应用)的硬件平台(称为目标平台)的 3D 功能。

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

接下来,让我们仔细看看场景实用程序类。在此之后,我们将进一步了解九个 javafx.scene 子包中的一些,如 javafx.scene.text、javafx.scene.image、javafx.scene.shape 和 javafx.scene.layout。

JavaFX 场景工具:场景快照和反走样

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

让我们先把 SceneAntialiasing 类解决掉。你在第二章中学习了抗锯齿,我向你展示了它如何使用一种算法来平滑两种不同颜色交汇处的锯齿边缘,通常是在图像合成的对角线或圆形区域。图像合成是将两个独立的图像分层放置,形成一个合成图像。有时,这两个(或更多)图像层中的图像分量之间的不同边缘需要被平滑。平滑(反走样)是需要的,这样最终的图像合成看起来就像一个无缝的图像,这是艺术家或游戏设计师的意图。有趣的是,我们已经使用 StackPane 类(窗格是层)在 JavaFXGame 应用中实现了 JavaFX“层引擎”。“层堆栈”图像合成方法在游戏以及 Photoshop 或 GIMP 等软件中很常见。

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

接下来让我们看看 SnapshotParameters 类(object),它用于设置(包含)一个渲染属性参数,该参数将由 SnapshotResult 类(object)使用。这些参数包括将使用哪种类型的相机(平行或透视)对象,用于 3D 的 depthBuffer 是打开的(对于 3D 为 true)还是关闭的(对于 2D 为 false),用于包含结果快照图像数据的 Paint 对象,用于包含任何变换数据的 Transform 对象,以及用于定义要渲染的视口区域的 Rectangle2D 对象。这将是快照尺寸和屏幕上快照结果左上角设置的 X,Y 位置。

这个 SnapshotResult 类(更重要的是使用这个类创建的对象)包含生成的快照图像数据、请求的参数和场景图中的源节点。因此,这个类支持的三个方法是显而易见的:一个. getImage()方法将获取快照图像,一个. getSource()方法将获取源节点,一个. getSnapshotParameters()方法将获取快照参数。

场景子包:九个与场景相关的包

你可能会想“咻!javafx.scene 包概述中包含了很多内容!”事实上,核心 javafx.scene 包中有许多类,涵盖场景创建、场景图形组织和场景实用程序,如照明、相机、光标、屏幕截图(“sceneshots”)和设置实用程序。javafx.scene 包中还有很多,我称之为“子包”,它们是 javafx.scene 包下面的包,使用另一个包名(描述)的另一个点来引用。事实上,还有 9 个 javafx.scene 包,正如你在表 7-1 中看到的,它们涵盖了画布绘制、纹理绘制、特效、UI 布局、数字成像、事件处理、文本和字体、形状(2D 和 3D 几何)、2D 和 3D 变换等内容。在本章中,我们将会看到所有这些 javafx.scene 子包类和概念,并且在本书的课程中会用到它们中的许多。本章的这一节将更详细地介绍 javafx.scene 子包,您将在游戏开发中使用的许多功能都可以在这些子包中找到。这就是为什么我给你一个 JavaFX 提供的概述,以便它在一个地方完成,我们可以使用 JavaFX 9 APIs 开始 Pro Java 9 游戏编码,并使用所有这些多媒体功能来创建游戏体验。

表 7-1。

The Nine Second-Level JavaFX Scene Subpackages with Primary Function and Description of the Functional Classes

包名 功能 包装内容和功能描述
javafx.scene .画布 直接绘图 为自定义绘图图面提供 Canvas 类(和 Canvas 对象)
javafx.scene.effect 特技 特殊效果类:发光,混合,开花,阴影,反射,运动模糊
javafx.scene.image 数字成像 数字图像类:图像、图像视图、可写图像、像素格式
javafx.scene.input 事件处理 提供与将用户输入获取到 JavaFX 应用相关的类
javafx.scene.layout UI 布局 用户界面布局容器类:TilePane、GridPane、FlowPane 等。
javafx.scene.paint 纹理(油漆) 油漆类:油漆,颜色,线性梯度,径向梯度,停止,材料等。
javafx.scene.shape 几何学 2D 和 3D 几何类:网格,形状,Shape3D,圆,线,路径,弧等。
javafx.scene.text 文本和字体 提供文本呈现和字体呈现类:textFlow、Text、font 等。
javafx.scene.transform 转换 提供变换类:变换,仿射,旋转,缩放,剪切,平移

让我们从包含最少类的包开始,先把它们去掉。尽管该表按字母顺序列出了子包,但第一个包 javafx.scene.canvas 包含两个类:一个用于创建 canvas 对象的 Canvas 类和一个用于控制在画布上绘图的调用的 GraphicsContext 类。

下一个子包 javafx.scene.effect 包含特殊效果类。这些对于 pro Java 9 游戏开发非常有用,所以这是我将在本节中详细介绍的子包之一。

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

javafx.scene.input 子包包含用于从 javafx 应用的用户获取输入的类,包括鼠标和键盘输入、手势、触摸屏、滚动、缩放或滑动输入、剪贴板内容以及其他类型的输入。使用事件处理功能处理输入和动作,在本书中将会详细介绍这些功能,并且您已经在您的 Pro JavaFX 9 应用中体验过这些功能,如您的 bootstrap Java 9 代码的第 20 行到第 25 行所示(如图 6-7 )。

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

javafx.scene.paint 子包包含一个 Stop 类;Paint 超类和 Color、ImagePattern、LinearGradient 和 RadialGradient 子类;以及 3D Material 超类及其 PhongMaterial 子类。熟悉 3D 内容制作的人会认识这种 Phong 着色器算法,它允许不同的表面外观(塑料、橡胶等)。)待模拟。这些 Material 和 PhongMaterial 类需要 i3D 功能才能在回放硬件上成功运行,就像 SceneAntialiasing、PerspectiveCamera 和 LightBase 类和子类一样。这些都需要 GPU 硬件加速或者软件渲染。

抽象 paint 类创建绘制对象的子类,Color 类为这些对象着色(用颜色填充它们),LinearGradient 和 RadialGradient 是用颜色渐变填充对象的 Paint 子类,Stop 类允许您定义渐变颜色在渐变内部的开始和停止位置,这就是它的名称的来源。最后,还有您的 ImagePattern 类,它可以用可平铺的图像模式填充 Shape 对象,这对游戏非常有用。

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

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

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

既然我们已经查看了 javafx.scene 包及其相关子包中大量重要且有用的类(对象),那么让我们看看其他 18 个顶级 javafx 包,了解 JavaFX 为应用开发提供的其他关键功能,当然,我们将重点关注那些可用于游戏开发的功能,这是我们在本章中一直在做的,并将在整本书中继续做的。

javafx.graphics 模块:18 个多媒体包

有 18 个顶级 javafx.graphics 模块包是最常用的包(除了核心 javafx.base 模块包)。它们遵循 javafx.packagename 名称格式(不是 javafx.graphics.packagename)。其中一些,比如 scene 和 css,也有子包级别。我们在前面的九个 javafx.scene 包及其子包中看到了这一点,所以我们在这里就不看了。javafx.graphics 模块是创建 Pro Java 9 游戏的三个关键模块之一,另外两个是 javafx.base 和 javafx.media。由于表 7-2 中包含了九个 javafx.graphics 模块包,这实质上意味着从 JavaFX API 模块的角度来看,javafx.graphics 模块总共有 18 个包类别,如表 7-1 中列出的九个。从 JavaFX 8 开始,Oracle 的 JavaFX 9 开发团队对这些模块包进行了重组,以实现更好的模块化(功能优化)。例如,如果你的 3D 游戏不需要音频或视频,你可以只使用基本和图形模块。由于我们需要音频,我们将使用基本、图形和媒体模块,或者七个 JavaFX API 模块中的三个(JavaFX API 包代码立即减少了 57%)。我想给你一个 javafx.graphics 模块包中这 18 个功能区域的概述,如表 7-1 和 7-2 所示,并仔细看看每个图形区域(矢量、光栅、动画、CSS)将做什么。

表 7-2。

javafx.graphics Module Top-Level (Nonscene) Packages, with Primary Functions and Description of Function

包名 功能 包装内容描述
javafx.animation 动画 类:动画计时器、时间轴、过渡、插值器、关键帧、键值
javafx .应用 应用 提供应用(初始化、启动、停止方法)、预加载程序、参数、平台
javafx.concurrent 穿线 提供线程类:Task、Service、ScheduledService、WorkerStateEvent
javafx.css 半铸钢ˌ钢性铸铁(Cast Semi-Steel) 提供与在 JavaFX 中实现级联样式表(CSS)相关的类
javafx.css.converter 半铸钢ˌ钢性铸铁(Cast Semi-Steel) 提供与在 JavaFX 中实现 CSS 相关的类
javafx.geometry 3D 几何图形 提供 3D 几何图形
javafx.print 印刷 提供打印
javafx.scene 场景控制 与场景创建、组织和控制相关的类(见表 7-1
javafx.stage 舞台创作 提供舞台创作

其中一些我们已经介绍过了,比如 javafx.application 包,我们在第六章中已经了解过了,还有 javafx.scene 包及其子包,我们在上一节中已经介绍过了。

表 7-2 中的第一个包是 javafx.animation 包。因为动画对 Java 游戏很重要,所以让我们在本章的下一节讨论它。我还将介绍 javafx.geometry 和 javafx.stage,因为 Java 9 游戏所需的表 7-2 中的核心包是动画、应用、几何、场景和 stage。

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

javafx.animation 包包含 animation 超类和 Timeline、AnimationTimer、Interpolator、KeyFrame 和 KeyValue 类。它还包含了 transition 超类和十个 Transition 子类,所有这些我们都将在本章的这一节中介绍,因为动画是 pro Java 9 游戏开发的一个重要设计元素。由于 JavaFX 9 API 的存在,这些动画类已经为我们编写好了,所以我们所要做的就是正确地使用这些类来为游戏添加动画。您将在这些类上花费大量时间,所以我将详细介绍每一个类,以便您了解每个类是如何工作的,哪些类可以协同工作,以及您需要哪些类来实现您自己的 Java 9 游戏逻辑解决方案。

JavaFX Animation 类:JavaFX 中动画对象的基础

Animation 类(更准确地说是 Animation 对象)为 JavaFX 中的动画提供了核心功能。Animation 类包含两个(重载的)Animation()构造函数方法。它们包括Animation()Animation(double targetFramerate),它们将在内存中创建动画对象,该对象将从包含其他子对象的高级对象中控制您的动画及其回放特征和生命周期。

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

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

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

有一个用于设置动画播放速率属性的. setRate()方法,一个用于设置延迟属性的. setDelay()方法,以及。setCycleCount()和。用于控制循环特性的 setCycleDuration()方法。可以想象,也有类似的。get()方法来“获取”这些动画对象变量的当前设置值(或属性、特性、参数、特征;不过,你更喜欢看你的数据领域是好的)。

使用用 ActionEvent 对象加载的 onFinished 属性,可以指定动画完成回放时要执行的动作。这将在动画到达每个循环的结尾时执行,正如您可以想象的,在专业 Java 游戏中使用这一特殊功能可以触发一些非常强大的东西。

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

JavaFX Timeline 类:JavaFX 属性时间轴管理的动画子类

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

> java.lang.Object
  > javafx.animation.Animation
    > javafx.animation.Timeline

时间轴对象可用于定义一种特殊的动画对象,它由对象类型为 WritableValue 的 JavaFX 值(属性)组成。所有 JavaFX 属性的类型都是 WritableValue,所以这个类可以用来制作 JavaFX 中任何东西的动画,这意味着您可以用它来做的事情仅限于您的想象。

时间轴动画是使用 KeyFrame 对象定义的,通过前面提到的 KeyFrame 类创建。毫不奇怪,这个关键帧类允许您创建和管理位于时间轴对象内部的关键帧对象。熟悉动画的人都知道,关键帧为对象或数据值的动画中的不同点设置不同的插值数据值,以创建平滑的移动。

关键帧对象将始终由时间轴对象根据时间变量(使用 KeyFrame.time 访问)和要设置动画的属性进行处理,这些属性使用关键帧对象的值进行定义,并使用 KeyFrame.values 变量进行访问。

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

您将在本书中使用的插值器类根据时间轴方向在时间轴对象中插入这些关键帧对象。插值是根据开始值和结束值创建中间帧或“补间”帧的过程。如果您想知道方向是如何推断的,它保存在 Animation 超类的 rate 属性和只读 currentRate 属性中,Animation 超类是扩展 Timeline 子类的一部分。

反转 rate 属性的值(即使其为负值)将反转(切换)播放方向,读取 currentRate 属性时也适用相同的原则(负值表示反向或向后)。最后,KeyValue 类(对象)用于保存每个关键帧对象中的数据值。一个关键帧对象存储多个(根据需要)KeyValue 对象,每个数据值使用一个 KeyValue 对象。

JavaFX 过渡类:用于过渡和特效应用的动画子类

JavaFX Transition 类是 JavaFX Animation 超类的子类,因此其继承层次结构如下所示,从名为 java.lang.Object 的 Java 主类开始,向下发展到 Transition 类:

> java.lang.Object
  > javafx.animation.Animation
    > javafx.animation.Transition

transition 类是一个公共抽象类,因此,它只能用于(子类化或扩展)创建 Transition 子类。事实上,已经为你创建了十个这样的子类来创建你自己的过渡特效。其中包括 SequentialTransition、FadeTransition、FillTransition、PathTransition、PauseTransition、RotateTransition、ScaleTransition、TranslateTransition、ParallelTransition 和 StrokeTransition 类。这些子类的 Java 9 类继承层次结构如下所示:

> java.lang.Object
  > javafx.animation.Animation
    > javafx.animation.Transition
      > javafx.animation.PathTransition

作为动画的子类,过渡类包含了动画的所有功能。你可能最终会直接使用这十个自定义过渡类,因为它们提供了你可能想要使用的不同类型的过渡(淡入淡出、填充、基于路径、基于笔画、旋转、缩放、移动或平移等)。).随着本书的进展,我们将学习如何使用其中的一些,所以我将继续学习 AnimationTimer 类。

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

JavaFX AnimationTimer 类不是 JavaFX Animation 超类的子类,因此它的继承层次结构如下所示;它以名为 java.lang.Object 的 Java 主类开始,以 AnimationTimer 结束:

> java.lang.Object
  > javafx.animation.AnimationTimer

这意味着 AnimationTimer 类是专门为 JavaFX 提供 AnimationTimer 功能的临时代码,它与 Animation(或 Timeline 或 Transition)类或子类没有任何关系。因此,如果您想将该类与占用 javafx.animation 包的 Animation、Interpolator、KeyFrame 和 KeyValue 类组合在一起,则该类的名称可能会有些误导。它和这些类没有任何关系!这个类允许你实现自己的动画(或游戏引擎)定时器,并自己编写代码!在 Java 8 游戏开发入门中,我展示了如何为 i2D 游戏做这件事。

这个 AnimationTimer 类也被声明为公共抽象类,就像 Transition 类一样。因为它是一个抽象类,所以只能用来(子类化或扩展)创建 AnimationTimer 子类。与 Transition 类不同,它没有为您创建的子类;您必须从头开始创建自己的 AnimationTimer 子类。

AnimationTimer 类看似简单,因为它只有一个您必须“覆盖”或替换的方法,该方法包含在公共抽象类中。handle()方法。该方法包含您希望在 JavaFX 引擎的舞台和场景处理周期的每一帧上执行的编程逻辑,它被优化为以 60 FPS(每秒 60 帧)播放,这恰好非常适合游戏。JavaFX 使用脉冲系统,该系统基于新的 Java 纳秒时间单位(Java 7 之前的版本使用毫秒)。

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

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

JavaFX 中的脉冲管理系统被“封顶”或“节流”到 60 FPS。这是一种优化,因此我们前面讨论的所有 JavaFX 线程都有足够的“处理空间”来做它们需要做的事情。根据您在 pro Java 9 游戏逻辑中所做的事情,JavaFX 应用将自动生成多达三个线程。一个基本的业务应用可能只使用主要的 JavaFX 线程,但是一个 i3D 游戏也会产生 Prism 渲染线程,如果 pro Java 9 游戏也使用音频和/或视频(通常会这样),它也会产生一个媒体播放线程,如果它还实现了一个社交媒体接口或元素,它也会产生 WebKit 渲染线程。因此,正如您将看到的,健壮的 Java 9 游戏将需要仔细的处理器时间管理。

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

当场景图中发生变化时,例如 UI 控件定位、CSS 样式定义或动画正在播放,会安排一个脉冲事件,并最终“触发”该事件以同步场景图中元素的状态。JavaFX 游戏设计的诀窍是优化脉冲事件,使它们专注于游戏逻辑(动画、碰撞检测)。出于这个原因,对于 pro Java 9 游戏,您会希望最小化脉冲引擎需要处理的非 ameplay 更改(UI 控制位置、样式更改)。您将通过为静态设计系统使用场景图来实现这一点,也就是说,设计固定的视觉元素(UI、背景图像等)。)不被脉冲引擎改变。这将保存“脉冲”用于动画或互动游戏的动态元素。

我的意思是,你将使用场景图来设计你的游戏结构,但不会使用动态编程逻辑通过场景图实时操纵静态设计节点(UI、背景、装饰),因为需要使用 pulse 系统来执行这些 UI 更新,我们很可能需要这些实时处理事件来用于我们的 Pro Java 9 游戏处理。又来了:静态对动态的游戏设计。

JavaFX pulse 系统允许开发人员异步或无序地处理事件,并在纳秒级上调度任务。接下来,我们将看看如何使用. handle()方法在脉冲中调度代码。

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

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

那个。开始()和。stop()方法是从 AnimationTimer 超类调用的,尽管这两个方法也可以被覆盖;只需确保最终在被覆盖的代码方法中调用 super.start()和 super.stop()。如果将它作为内部类添加到当前 JavaFX public void 中,其代码结构可能如下所示。start()方法结构(可以参考图 6-7 ,提醒一下):

public void start(Stage primaryStage) {
    Button btn = new Button;
    btn.setText("Say 'Hello World'");
    btn.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            System.out.println("Hello World!);
        }
    }
    new AnimationTimer() {
        @Override
        public void handle(long now) {
            // Program logic that gets processed on every pulse which JavaFX processes
        }
    }.start();

            // Rest of start() method code regarding Stage and Scene objects is in here
}

前面的编程逻辑展示了如何“动态地”构造 AnimationTimer 内部类,以及 Java 点链接是如何工作的,因为对 AnimationTimer 超类的. start()方法调用被附加到新的 AnimationTimer()构造函数的末尾。在一条语句中,有 AnimationTimer 创建(new 关键字)、声明(构造函数方法)和执行(链接到 AnimationTimer 对象构造的 start()方法调用)。

如果您想为游戏逻辑的核心部分(如碰撞检测)创建一个更复杂的 AnimationTimer 实现,将游戏计时逻辑放入它自己的(自定义)AnimationTimer 子类而不是内部类中会是一个更好的方法(也就是说,一个更好的 pro Java 9 游戏设计)。如果您要创建多个 AnimationTimer 子类,以便可以实现自定义脉冲事件处理,这一点尤其正确。您可以同时运行多个 AnimationTimer 子类,但是我建议您不要忘乎所以,不要使用太多的 AnimationTimer 子类,而是优化您的 Java 代码,只使用一个。

要使用 Java extends关键字结合 AnimationTimer 超类来创建您自己的名为BoardGamePulseEngine的 AnimationTimer 类,请实现此 AnimationTimer 类定义和这些必需的 AnimationTimer 超类方法来创建您的“空”JavaFX pulse board 游戏逻辑计时引擎。

public class BoardGamePulseEngine extends AnimationTimer {
    @Override
    public void handle(long now) {   // Program logic here that gets processed on every pulse
    }
    @Override
    public void start() {
        super.start();
    }
    @Override
    public void stop() {
        super.stop();
    }
}

在我们学习了 Java 9、NetBeans 9、JavaFX 9 和 SceneGraph 的基础知识之后,我们将在本书的后面创建动画代码(第章第 8 )。本章中的代码示例只是向您展示如何实现这些 JavaFX 动画的示例。接下来,让我们来看看 JavaFX Stage 类,在这里我将向您展示一些代码,以使您的 JavaFX 环境透明,这样您的游戏就可以悬浮在操作系统桌面上,这是 Windows 中一种称为“无窗口 ActiveX 控件”的效果,它允许您创建虚拟的 i3D 对象。

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

javafx.stage 包包含可被视为“顶级”的类,javafx 应用使用这些类的显示。在你的用例中,这是 pro Java 9 游戏。这个阶段是游戏的“顶层”,因为它向应用的最终用户展示了游戏的场景。在 Stage 对象中有 Scene 对象,在这些对象中有 SceneGraph 节点对象,它们包含组成 Java 9 游戏或 Java 9 IoT 应用的元素。因此,JavaFX Stage 对象是您将在 Java 9 游戏中使用的最高级对象。

另一方面,从操作系统的角度来看,这个包中的类可以被认为是提供低级服务的。这些选项包括 Stage、Screen、Window、WindowEvent、PopupWindow、Popup、DirectoryChooser 和 FileChooser,以及 FileChooser。ExtensionFilter 嵌套类。这些类将用于连接设备显示硬件、操作系统窗口管理、文件管理和目录(文件夹)管理功能。这是因为 Stage 类(对象)向 OS 请求这些功能,而不是使用 Java 或 JavaFX APIs 来实际实现它们,所以 OS 实际上是在您的 Java 9 游戏或 Java 9 IoT 应用请求 OS 提供这些 OS 前端实用程序时产生这些服务。

如果您想获得运行 JavaFX 应用的硬件设备所使用的显示硬件的描述,您将需要使用 Screen 类。该类通过提供一个. getScreens()方法来支持多屏幕(第二个屏幕是常见的行业术语)方案,该方法可以访问 ObservableList 对象(一个允许侦听器跟踪发生的更改的列表对象),该对象将包含一个包含所有当前可用屏幕的列表数组。有一个“主”屏幕,可以使用。getPrimary()方法调用。您可以通过使用. getDpi()方法调用来获取当前屏幕硬件的物理分辨率。也有。getBounds()和。getVisualBounds()方法调用可用的分辨率。

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

您可以使用 Stage 类在 JavaFX 应用编程逻辑中创建辅助阶段。主 Stage 对象始终由 JavaFX 平台使用公共 void start(Stage primaryStage)方法调用来构造(正如您已经在 NetBeans 9 创建的引导 JavaFX 9 应用的第六章中看到的)。所有 JavaFX Stage 对象都必须使用主 JavaFX 应用线程来构造,并在其中进行修改,这一点我在前面讨论脉冲事件处理时已经讨论过。由于阶段相当于运行它的操作系统平台上的一个窗口,因此某些属性是只读的,需要在操作系统级别进行控制。这些是布尔属性(变量),它们包括 alwaysOnTop、全屏、图标化和最大化。

所有 Stage 对象都有一个 StageStyle 属性和一个 Modality 属性,可以使用常量进行设置。stageStyle 常量包括 StageStyle。装饰,舞台风格。无装饰,舞台风格。透明,舞台风格。统一的舞台风格。实用常数。设备常量包括设备。无,模态。应用模态和模态。WINDOW_MODAL 常量。在我们讨论完 javafx.stage 包之后,在下一节中,我将向您展示如何使用这个 StageStyle 属性和透明常数来做一些真正令人印象深刻的事情,这些属性和常数将使您的基于 javafx 的 Java 9 游戏和物联网应用与市场上的其他应用区别开来。

Popup 类可用于从头开始创建自定义弹出通知,甚至自定义游戏组件。或者,您可以使用 PopupControl 类及其 ContextMenu 和 Tooltip 子类来提供预定义的(即针对您自己的实现预编码的)JavaFX 图形用户界面(GUI)控件。

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

接下来,让我们将您当前的 JavaFXGame 应用的 Stage 对象提升一个层次,并向您展示如何使您的 Java 9 (JavaFX 9)游戏成为无窗口(浮动)应用!这是 JavaFX 9 众多令人印象深刻的特性之一,您可以在您的 Pro Java 9 游戏开发管道中加以利用。

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

让我们将 JavaFXGame 应用的 primaryStage Stage 对象(由我们的.start(Stage primaryStage)方法构造函数创建)设置为透明的,这样 HelloWorld 按钮(UI 控件)就可以浮动在您的操作系统桌面上(或者在本例中,浮动在 NetBeans 9 的顶部)。这是 JavaFX 可以做的事情,你很少看到利用,它将允许你创建 i3D 游戏,看起来“浮动”在你的用户的操作系统桌面上。对于 i3D 虚拟对象,至少在 Windows 7、8 和 10 操作系统上,这将被称为“无窗口 ActiveX 控件”移除窗口“chrome”或装饰在其他高级操作系统(如 Linux 和 Mac)中也应该得到支持,并且有一个程序调用来确定这种“使用 alpha 通道(透明)移除除我的内容之外的所有内容”功能是否到位,因此您可以实现纯色或背景图像的后备计划。这个很酷的小技巧(我想我会在书的开头向你展示一些很酷很强大的东西)部分是通过使用 StageStyle 实现的。透明常数,您刚刚了解了它与。Stage 类中的 initStyle()方法。StageStyle 是一个“helper”类,填充了 stage(或 OS 窗口,最终)装饰常数,其中一个是透明的。我们将使用的后备是未修饰的(一个正常的操作系统窗口)。

添加 StageStyle 常量:使用。initStyle(StageStyle style)方法调用

如图 7-4 所示,我在 Java 9 代码中添加了新的第 26 行(用浅蓝色突出显示),并键入 primaryStage 阶段对象名;然后我点击句点键,将一个 Java 点链插入到我想要使用的方法中。此时,NetBeans 9 将打开一个弹出的方法选择器帮助器对话框(实际上更像是一个选择器 UI);寻找。initStyle(StageStyle style)方法,如图 7-4 所示。单击该方法会以蓝色选中它,双击它会将其插入到您的代码中。接下来,我们将对方法的参数做同样的事情,使用相同的工作过程,允许(或诱使)NetBeans 9 为您进行 Java 编码工作。

A336284_1_En_7_Fig4_HTML.jpg

图 7-4。

Call an .initStyle() method off of the primaryStage Stage object, using dot notation to invoke a helper menu

正如您在图 7-4 中看到的,我在 NetBeans 9 helper 中单击了 initStyle(StageStyle style)选项,这将在您正在编写的代码行上方显示一个 Javadoc 窗口,其中包含有关该方法的文档。您可以使用这种方法来了解一个对象支持哪些方法,方法是键入对象名,按句点键,然后选择每个方法来查看它的作用。

如图 7-5 所示,Stage 对象是使用.start(Stage primaryStage)方法调用声明创建的,并使用。setTitle(),。initStyle(),。setScene(),最后是。中的 show()方法调用。start()方法结构。

现在,我将在 Java 9 代码中保留一个. setTitle()方法调用,但是请记住,一旦这个无窗口的应用处理开始工作,这个标题就是窗口 chrome(标题栏 UI 元素)的一部分。一旦这些都消失了(包括标题栏),标题属性的设置将成为一个争论点。

如果您在应用开发工作流程的这个阶段关注内存优化,那么您应该删除它。setTitle()方法调用,因为 Title 属性会占用内存空间,而且由于使用了 StageStyle,甚至看不到它。StageStyle(实际上是窗口样式)属性的透明常数。

在里面。initStyle()方法类型,键入所需的 StageStyle 类(对象)和句点,以调出下一个帮助器选择器。这次是常量选择器,如图 7-5 所示。选择透明选项,读取上面的 Javadoc 信息,然后双击它以完成代码语句,该语句应如下所示:

primaryStage.initStyle(StageStyle.TRANSPARENT); // Insert StageStyle Class TRANSPARENT Constant

正如您在 Javadoc 信息弹出窗口的图 7-5 中所看到的,将自动为要取消装饰的透明窗口(舞台)装饰样式编码一个回退(降级)方法。它的特点是背景颜色为白色,并且仍然移除了标准的操作系统窗口镶边(标题栏、最小化、最大化、关闭、调整大小等)。).接下来,让我们测试我们的代码,看看按钮现在是否悬浮在它后面的任何东西上(在这种情况下,这是 NetBeans)。

A336284_1_En_7_Fig5_HTML.jpg

图 7-5。

Type StageStyle and a period in the method parameter area to pop up a constant selector NetBeans helper

接下来使用运行图标(或运行菜单)并运行应用。正如您在图 7-6 中看到的,我们试图实现的并没有成功,窗口的 chrome 元素消失了,但是透明度值并不明显。

A336284_1_En_7_Fig6_HTML.jpg

图 7-6。

Run Project to see if the Stage object is transparent; clearly there is an object set to off-white background color

正如您所看到的,有一个灰白色值(用于 iTV set 应用,因为一些 iTV set 不支持 255,255,255 白色)与 NetBeans 9 用于其代码编辑器窗格的 255,255,255 白色形成鲜明对比。

在您的处理管道中,一定还有其他东西尚未使用透明度值定义您的舞台背景。透明度使用十六进制值 0x00000000 来定义,这表示所有 aarggbb(Alpha、红色、绿色、蓝色)透明度和颜色值都被关闭。您需要开始将应用中的 JavaFX 组件视为层(目前有 Stage、Scene、StackPane、Button)。

在本书的第二章中,您学习了数字成像概念,如色深、alpha 通道、图层、混合、抖动,以及所有与处理 2D 平面中的像素相关的有趣技术信息。

接下来,我们应该尝试设置这个透明值的是从 Stage 开始的 JavaFX 场景图层次中的下一级,它包含场景图本身。下一个最顶层的组件,正如你在本章中学到的,是场景对象,它也有一个背景颜色值参数或属性。

因此,下一步是尝试使用十六进制值 0x00000000 或 Java 9 Color 类常量将该属性设置为零不透明度和颜色,这将实现完全相同的目标。

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

如果您查看 Scene 类文档,您会注意到有一个. setFill(颜色值)方法接受一个颜色(类或对象)值,所以接下来让我们尝试一下。如图 7-7 所示,我调用了。使用一个scene.setFill();方法从名为 Scene 的场景对象中设置 Fill()方法,NetBeans 让我从下拉帮助器中选择该方法。

A336284_1_En_7_Fig7_HTML.jpg

图 7-7。

Add a new line of code and type in the scene’s Scene object and a period to invoke a method helper selector

选择并双击.setFill(Paint value)方法,然后在参数区域键入 Java 9 颜色类名(颜色是 Paint 的子类)。接下来,键入句点,调出 Java 9 Color helper 类中包含的常数,如图 7-8 所示,找到并选择一个透明常数。正如您在 Javadoc 帮助器窗格中看到的,ARGB 颜色值是期望的#00000000。

A336284_1_En_7_Fig8_HTML.jpg

图 7-8。

Type the Color class name and period in the parameter area, and find and select a TRANSPARENT constant

再次运行应用,查看透明度是否显示出来。如图 7-9 所示,它仍然不透明。由于我们使用 StackPane 对象来实现 BoardGame 应用中的层,这是我们需要尝试设置透明度值的下一个级别。JavaFX 使用 Color 类常量来确定其所有 UI 对象的默认背景颜色值。如果我是 JavaFX 9 团队的一员,我会主张将它改为彩色。透明常数,但当然,这可能会让新用户感到困惑,因为 alpha 通道和合成层是高级概念和主题,这就是为什么它们在本 pro Java 9 游戏开发书的第二章的开头,涵盖了数字图像合成和相关概念。请注意,在图 7-9 中,NetBeans 已经为您导入了 Java Color 类,因为您在scene.setFill(Color.TRANSPARENT); Java 语句中使用了它。

A336284_1_En_7_Fig9_HTML.jpg

图 7-9。

Set the Scene object fill color to TRANSPARENT, and notice that NetBeans codes a Color class import statement

javafx.scene.layout.StackPane 类是 javafx.scene.layout.Region 类的子类,该类有一个. setBackground()方法来设置背景(类或对象)值。同样,必须有一个透明的值常量可用,或类似的东西,如背景。空,因为您需要始终将背景值设置为透明,特别是对于 pro Java 9 游戏设计,您需要灵活性来实现高级 2D 和 3D 合成和渲染管道。这种对透明性的支持也适用于 Android UI 容器。

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

一旦使用root.setBackground(Background.EMPTY); Java 语句调用名为 root 的 StackPane 对象的方法,NetBeans 9 将帮助您找到常数。这一次更容易作为背景。空常数恰好是。setBackground()方法调用。如果您想查看所有后台助手类常量,请键入 root.setBackground(Background。进入 NetBeans 9,并查看出现在“常量”弹出帮助器选择器窗格中的结果。

如图 7-10 所示,NetBeans 9 提供了一个方法选择器下拉菜单,一旦您选择并双击。setBackground(背景值)方法,NetBeans 9 将为您编写代码语句,自动插入默认的空常量,该常量使用点标记法从背景类中调用。正如您将在红色的图 7-11 中看到的,NetBeans 还会在 Java 类的顶部编写后台类的导入语句。

A336284_1_En_7_Fig11_HTML.jpg

图 7-11。

The transparency now goes through all objects (layers), and the button is now rendered on the OS directly

A336284_1_En_7_Fig10_HTML.jpg

图 7-10。

Add a line of code after the root StackPane object, type root and a period, and select setBackground()

现在,您可以通过运行项目工作流程(通过“运行”菜单或 NetBeans IDE 左上角的绿色 play transport 图标)再次测试您的无窗口(透明)JavaFX 应用版本。

正如您在图 7-11 中所看到的,我们现在已经实现了我们的目标,在 NetBeans IDE Java 代码编辑窗格的顶部只显示了按钮对象,这是正在运行的 Java 代码窗口下的下一个应用。下面是操作系统桌面。

您还可以看到 NetBeans 添加了您的后台类导入语句以及九行 StackPane (root)和 Scene (scene)对象 Java 9 代码,我们添加这些代码是为了使最终结果出现在图 7-11 的第 25 至 33 行中。一定要理解这些物体的创造过程,以及它们是如何相互链接,或者“连接”的,正如我喜欢称之为的那样,成为不可阻挡的功能交织。理解 Java 9 编程语句的顺序几乎和 Java 9 语句本身的构造一样重要。

例如,在编写第 25 行代码之前,不能编写第 28 行代码,在第 25 行中,实例化了根 StackPane 对象,这样就可以用它来创建场景的 scene 对象。

我在 NetBeans 9 中单击了根对象,以告诉 IDE 向我显示该对象在类中的用法,正如您将在图 7-11 中看到的,在 Java 9 代码中使用黄色突出显示跟踪该对象。随着你的 pro Java 9 游戏代码变得越来越复杂,这个很酷的特性变得越来越重要。正如我在第六章中提到的,我们将在本书的许多章节中讨论方便的 NetBeans 9 特性。

最后一个测试是确保我们的 JavaFX 应用在操作系统桌面上是透明的。将 NetBeans 9 IDE 拖到一边,并在桌面背景图像的顶部看到您的按钮 UI 元素,您可以在图 7-12 中看到它现在工作正常。

A336284_1_En_7_Fig12_HTML.jpg

图 7-12。

The JavaFX application seamlessly composited on top of the Windows 7 OS desktop's wallpaper

您还可以看到代码工作时的折叠和展开图标(代码左侧的加号和减号图标)。我已经关闭(折叠)了。start()方法并打开了。main()方法。单击减号将关闭。main()方法,单击加号图标将打开导入语句和。start()方法代码体。

我关闭了 import 语句块。start()方法代码块向您展示了该类的五个主要区域:javafxgame 包声明、导入语句块、JavaFXGame 应用子类声明以及任何 JavaFX 9 游戏(或 IoT)应用所需的两个主要方法,它们是。开始()和。main()。

使用 2D、3D 和 alpha 通道,可以使用这种 StageStyle 创建一些非常酷的应用。透明功能,所以我想我应该在本书的早期向您展示这一点,以便我可以在 JavaFX“概述”一章中获得一些关于增强您的 JavaFX IoT 应用和 pro Java games 编码体验的很酷的技巧和提示。定义一个游戏或 i3D 虚拟物体,让它悬浮在操作系统桌面上,这是一种罕见的视觉冲击效果。

现在,我们已经从回顾与 pro Java 9 游戏开发最直接相关的所有 JavaFX 9 APIs 中获得了一个有趣的编码休息,让我们回到您可能想要了解的关于交互性、UI 设计、图表、音频或视频媒体素材以及关于与互联网和社交媒体平台的接口的其他 JavaFX 模块、包和类产品。我们还将简要介绍一些您不会使用的 API!

现在我们已经看了 javafx.stage 包,接下来让我们看看 javafx.geometry 包。

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

尽管术语“几何”在技术上适用于 2D 和 3D 素材,但它们包含在 javafx.scene.shape 包中,我们在本章前面已经介绍过。javafx.geometry 包更像是一个“实用程序”包,包含从头开始构建 2D 或 3D 构造的基础类。因此,该包包含 Bounds 超类及其 BoundingBox 子类等类,以及 Insets、Point2D、Point3D、Dimension2D 和 Rectangle2D 几何内容创建实用程序类。这个 javafx.geometry 包中的所有这些类,除了 BoundingBox 类,都是直接从 java.lang.Object master 类扩展而来的,这意味着它们都是为提供点(也称为顶点)、矩形、尺寸、边界和插入(内部边界)而开发的(从头开始编码),用作 Java 9 游戏的几何实用程序。

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

Point2D(double X, double Y)

Point3D(double X, double Y, double Z)

Rectangle2D 类(对象)可用于定义一个矩形 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 包,因为它为我们提供了事件超类及其 ActionEvent 子类,用于处理动作事件,例如 UI 元素使用或动画关键帧处理事件使用。因为您将在 pro Java 9 游戏(或物联网应用)中使用 ActionEvent,所以我将在这里查看它的跨包(Java 到 JavaFX)类继承层次结构,因为这也将向您展示 JavaFX Event 类的起源。这是可能的,因为 JavaFX API 是 Java API 的一部分(在 Java API 之下)。

Java.lang.Object
  > java.util.EventObject
    > javafx.event.Event

      > javafx.event.ActionEvent

JavaFXGame 应用已经通过 EventHandler 接口及其。handle()方法,您将实现该方法来告诉 Java 应用如何处理该事件,该事件一旦发生就是一个 ActionEvent(编程术语被触发)。这个。然后,handle()方法“捕捉”被触发的事件,并根据这个“主体”内部的 Java 9 编程逻辑对其进行处理。handle()方法。

正如你从第五章中所知道的,Java 接口是一种提供空方法的类型,这些方法被声明使用,但是还不包含任何 Java 构造。未实现的方法在使用时需要由 Java 程序员来实现。这个 Java 接口只定义了需要实现哪些方法;在这种情况下,它是一个“处理”ActionEvent 的方法,以便以某种方式处理该事件。

值得注意的是,Java 接口定义了一个需要编码的方法,但并没有为您编写方法代码,因此它是一个“路线图”,指示您必须做些什么来完成现有的编程结构,或者与现有的编程结构进行交互。在这种情况下,这是一个用于处理 ActionEvent 对象的 Java 编程结构,或者更准确地说,是一个用于在 action event 被触发后处理它们的编程结构。

与本 JavaFX 新媒体引擎概述章节中涵盖的所有内容一样,在应用这些 JavaFX 9 编程结构、JavaFX 场景图构造和新媒体素材设计概念的过程中,您将很快深入了解如何使用这些包、类、嵌套类、接口、方法、常量和数据字段(变量)的细节。

JavaFX UI 元素:使用 javafx.scene.control 类

javafx.scene.control 包和 javafx.scene.chart 包在 javafx.controls 模块中,我们接下来将介绍这两个包。这个包包含了所有的用户界面控件(在 Android 中它们被称为“widgets”,我喜欢称它们为 UI“elements”)类,比如 Alert、Button、Cell、CheckBox、ChoiceDialog、ContextMenu、control、DatePicker、ColorPicker、Label、ProgressBar、Slider、Label、RadioButton、ScrollBar 和 TextField。由于 javafx.scene.control 中有 100 多个类,我甚至不打算在这里一一介绍,因为关于这个 Java 9 模块可以写一整本书。如果您想回顾这些类,只需使用 Google 或在 Oracle Java 网站上参考 javafx.control 模块,您就可以连续几天仔细阅读这些类的功能。对于这个模块,“reference”是一个关键词,因为当你需要实现一个给定的 UI 元素时,你需要单独地引用这个包和它的类。我将尝试使用我自己的 3D UI 元素和代码来创建本书中的 i3D 游戏,这样我就不必在发行版中包含这个 javafx.controls 模块,从而节省了在发行版中包含 100 多个控件类(更不用说十几个图表类)的开销,这些控件类甚至都没有被利用。

JavaFX 业务图表:使用 javafx.scene.chart 类

javafx.scene.chart 包位于具有预定义 UI 控件(UI 元素)的 javafx.controls 模块中。这个包包含了业务图表类,比如 Chart、ScatterChart、StackedAreaChart、XYChart、PieChart、LineChart、BarChart、StackedBarChart、AreaChart、BubbleChart 等等,用于业务应用,这是完全不同的一本书,所以在本书中我们不会涉及图表。事实上,对于我的游戏,我将使用 3D UI 方法,这将意味着我根本不需要包括 javafx.controls 模块(大量的类),这意味着我的游戏模块将只需要包括 javafx.base、javafx.media 和 javafx.graphics,使发行版的下载量大大减少(base 只有 10 个包,media 有 9 个,而 graphics 有 18 个,正如您在本章中所看到的)。

JavaFX 媒体控件:使用 javafx.scene.media 类

javafx.scene.media 包包含在 javafx.media 模块中,其中包含用于播放音频和视频媒体资源的类,包括 media、MediaPlayer 和 MediaView 类,以及 AudioClip、AudioEqualizer、EqualizerBand、Track、VideoTrack 和 SubtitleTrack 类。媒体类(或对象)引用或包含音频或视频媒体素材,MediaPlayer 播放该素材,MediaView(尤其是在视频的情况下)显示数字音频或视频媒体素材以及用于媒体回放的传输。

我们将在本书稍后为您的 pro Java 9 游戏添加数字音频音效时使用 AudioClip 类,只要我们使用该模块的数字音频部分,如果我们必须将它包含在您的应用(模块)发行版中,我们也可以利用数字视频素材(视频类)功能。

JavaFX Web 渲染:使用 javafx.scene.web 类

javafx.scene.web 包包含在 javafx.web 模块中,其中包含用于在场景中渲染 web (Internet)资源的类。这个包包含一组类,包括 WebEngine、WebView、WebEvent、WebHistory 和 HTMLEditor。正如您可能想象的那样,WebEngine 类(嘿,有人称这些算法为引擎)执行在 JavaFX 场景中显示 HTML5、CSS3、CSS4 和 JavaScript 的处理,WebView 创建节点以在 JavaFX 场景图中显示 WebEngine 输出。WebHistory 类(最终是对象)保存从 web 引擎实例化到从内存中删除的互联网“会话”,这是访问过的网页的历史,而 WebEvent 类将 JavaScript web 事件处理与 JavaFX 9 事件处理“连接”起来。我们不会将 javafx.web 模块用于我们将在本书的课程中创建的 i3D 游戏,因为我将重点关注可用于提供视觉上最专业的 i3D 游戏效果的核心 API。

其他 JavaFX 包:Print、FXML、Beans 和 Swing

在阅读完 JavaFX 概述章节之前,您应该仔细看看其他几个 JavaFX 包,因为这些包包含您可能希望在专业 Java 游戏开发中使用的类,但它们提供了更专业的功能,如打印、使用第三方 Java 代码、使用 AWT 和 Swing 等较旧的 UI 范例,以及使用 XML(特别是 FXML)将 UI 设计卸载给非程序员。这些 API 包括 javafx.print 包(javafx.graphics 模块)、javafx.fxml 包(javafx.fxml 模块)、javafx.beans 包(javafx.base 模块)和 javafx.embed.swing 包(javafx.swing 模块)。除非您的项目有特殊的需求,否则您不太可能在您的 Java 游戏设计和开发工作过程中使用这些。其中最明显的是 javafx.print,用于允许打印机与您的 pro Java 9 游戏一起工作。如果您需要使用旧的 Swing UI 元素,有一个 javafx.swing 模块可以实现这一点,但会给 Java 9 游戏发行版增加更大的数据量。javafx.beans 包将允许您使用 Java Beans(第三方或附加类),javafx.fxml 模块将允许您使用 Java Fxml,这种 XML 语言允许用户界面和图形设计卸载到 XML,而不是 Java 编码。这使得不熟悉 Java 的设计者也可以参与游戏项目。Android 操作系统和 Android Studio IDE 也使用这种方法,它们使用 XML 来完成许多顶级设计任务,因此设计人员不必是程序员。

摘要

在第七章中,您了解了 JavaFX 9 API 中一些最重要的包、概念、组件、类、构造函数、常量和变量(属性、参数、数据字段)。这是一个令人印象深刻的包含 36 个包的 7 个 Java 9 模块的集合,我使用表格简洁地概述了其中的许多模块,然后逐一介绍了它们。我这样做是因为本章中概述的大多数(如果不是全部)包和类最终会以某种方式用于新媒体、2D、3D 和混合 2D+3D pro Java 9 游戏开发。当我说全面概述时,我的意思是让我们看看在 Java 9 下使用 JavaFX 9 开发游戏所需的一切。

当然,我无法在一章中涵盖 JavaFX 9 API 中的每个功能类,所以我首先概述了图 7-1 中的 JavaFX API 新媒体引擎,以及它如何与上面的 JavaFX 场景图以及这些 API 下面的 Java FX API、NetBeans 9 和目标操作系统集成。您的 Java 9 游戏发行版和操作系统是通过 Java 虚拟机(JVM)连接起来的。这为 JavaFX 提供了跨众多流行平台和消费电子设备的扩展操作系统支持,从智能手机到平板电脑到 iTV 电视机,以及所有基于流行 WebKit 引擎的主流 web 浏览器(Chrome、Firefox 和 Opera)。

通过查看构成 JavaFX 引擎的结构,包括 JavaFX 场景图、JavaFX APIs、Quantum、Prism、Glass、WebKit 和媒体播放器引擎,您对 JavaFX 有了一个高层次的技术了解。您了解了这些多线程、渲染、窗口、媒体和 web 引擎是如何与 Java 9 APIs 和 JDK 以及 NetBeans 9 及其生成的 JVM 字节码进行交互的,NetBeans 9 及其生成的 JVM 字节码受当前运行在从 96 英寸 UHD iTV 电视机到 4 英寸智能手机等十几种不同消费电子设备类型之上的各种操作系统平台的支持。

我讲述了 JavaFX 的核心概念,例如使用 JavaFX 场景图和 JavaFX 脉冲事件系统,我们将在本书的整个过程中利用它们来创建一个 pro Java 9 游戏,从下一章开始,我们将开始设计游戏,并讲述如何使用 JavaFX 场景图来开发处理层次结构。

我深入研究了一些用于 pro Java 9 游戏设计的关键 JavaFX 包、子包和类,如应用、场景、形状、效果、布局、控制、媒体、图像、舞台、动画、几何、事件、fxml 和 web,以及它们相关的 Java 9 模块、包、子包、类和子类。在某些情况下,我甚至讨论了它们的接口、嵌套(助手)类和数据常量。

您在这篇 JavaFX 9 API 评论中休息了一下,向 JavaFXGame 应用添加了一些代码,使它成为一个“无窗口”的应用,能够“漂浮”在任何流行的 OS 桌面上。您了解了如何通过使用十六进制设置为 0x00000000 的 alpha 通道或使用表示 100% alpha 透明度的等效常数(如 Color)来使 Stage、Scene 和 StackPane 对象的背景属性透明。透明,舞台风格。TRANSPARENT,或者 Background.EMPTY .你也看到了 Group (Node)类和 object 本来就有一个透明的背景;当您将场景图形的顶级节点从堆栈面板更改为组(更好的顶级节点)时,根本不需要设置组背景透明度。

我不得不在本章中使用 NetBeans 9 IDE、Java 9 编程语言和 JavaFX 9 API 进行一些工作,这样我们就可以开始逐渐添加越来越多的代码,直到(很快)剩余的章节完全编码,因为所有这些基础材料,包括新媒体素材设计、API、IDE、游戏概念、JVM、UI、UX、3D 渲染引擎、2D 回放引擎、WebKit、静态与动态、游戏优化等等。 已经在你们的头脑中牢牢地就位,因为在本书的整个过程中,你们需要建立在这些先进知识的基础上。

在下一章中,您将看到 JavaFX 9 场景图。您将开始构建您在本章中学到的场景图形结构,并开始构建游戏的基础,包括用于启动游戏的按钮元素的 UI“面板”。我还会解释你的游戏规则,显示高分,给出制作学分,并包括法律免责声明。我知道您急于开始构建您的 pro Java 9 游戏基础设施,您将在下一章开始认真地做这件事,创建自定义方法并使用 JavaFX APIs 添加新的 Java 代码,开始为您的 JavaFXGame 类创建顶层。实际上,通过学习如何在 JavaFX 9 场景图形层(应用到场景到组到堆栈面板到 VBox 到按钮)内部(并通过它们)实现透明性,您在本章中已经开始做了一些工作。

八、JavaFX 9 场景图层次结构:Java 9 游戏设计的基础

让我们从用户界面和用户体验的角度,以及“引擎盖下”游戏引擎、3D 精灵引擎、碰撞引擎和物理引擎的角度,开始设计我们的 i3D JavaFXGame 游戏的基础设施,从而建立我们在第八章的前几章中学到的 JavaFX、游戏设计、多媒体和 Java 的新知识。我们将牢记优化,正如我们在本书剩余部分必须做的那样,这样我们就不会得到一个如此广泛或复杂的场景图,以至于脉冲系统无法有效地更新一切。这意味着将主游戏 UI 屏幕(StackPane 节点)保持在最少(四个或五个),以便将大部分处理能力留给 3D 游戏渲染(组节点),并确保媒体播放器(数字音频或数字视频)使用自己的线程,如果使用这种类型的媒体的话。(音频(尤其是视频)数据量非常大,可能需要大量处理。)你还需要确保驱动游戏的功能“引擎”都是模块化和逻辑编码的,使用它们自己的类,并利用你在第五章学到的适当的 Java 编程约定、结构、方法、变量、常量和修饰语。这将是一项艰巨的任务,从本章开始,将需要数百页才能完成,现在我已经确保你们都掌握了 Java、JavaFX、NetBeans、2D 和 3D 新媒体概念的知识。

我要介绍的第一件事是顶级的、正面的用户界面屏幕设计,这是你的游戏在启动 Java 应用时提供给用户的。这将包括用户在启动应用时看到的棋盘游戏“品牌”闪屏。该屏幕的一侧有按钮控件,用于访问包含说明、致谢、法律免责声明等信息的屏幕。这些 UI 屏幕是 StackPane 节点层,我们希望尽量减少它们的数量。StackPane 对象旨在包含堆叠的图像(合成)层。这些游戏支持屏幕将包含用户为了有效地玩游戏而需要知道的信息。这包括基于文本的信息,如游戏说明、演职员表、法律免责声明和高分屏幕。我们将包括法律免责声明,以使法律部门满意,并将突出强调致力于创建游戏和游戏素材的程序员和新媒体工匠的贡献。

在本章中,我们将概念化的棋盘游戏设计基础的下一个层次是游戏引擎组件 Java 类设计方面。这些将不会被游戏用户看到,但仍然非常重要。它们可能包括一个使用 JavaFX pulse 控制游戏更新的游戏引擎,一个管理游戏 3D 游戏精灵的 3D 精灵引擎,一个在两个精灵之间发生任何碰撞时进行检测和响应的碰撞引擎,一个将力和类似物理模拟应用于游戏的物理引擎,以便 3D 精灵加速和真实反弹,最后是一个 3D 演员引擎,它将管理为 JavaFXGame 游戏实例化的单个演员的特征。您将修改现有的JavaFXGame.java类来实现一个 UI,用按钮控件访问提供顶级用户界面游戏信息特性所需的功能信息屏幕。您将了解几个用于组织和定位的新 JavaFX 类,包括 Group、VBox、Insets 和 Pos 类。

游戏设计基础:主要功能屏幕

你要为你的游戏设计的第一件事是顶级的,或者说是最高级别的用户界面屏幕,你的游戏用户将会与它交互。这定义了用户第一次打开你的游戏时的用户体验。这些屏幕将使用您的 JavaFXGame Splash(品牌)屏幕访问,包含在主要 JavaFXGame.java 类代码中。正如您已经看到的,此 Java 代码将扩展 javafx.application.Application 类,并将启动应用,显示一个闪屏,以及查看说明、玩游戏、查看高分或查看游戏法律免责声明和游戏创作者(程序员、艺术家、作家、作曲家、声音设计师等)的选项。).图 8-1 显示了游戏的高层图,从顶部的功能 UI 屏幕开始,向下发展到 JavaFXGame.java 代码,然后到 API、JVM 和 OS 级别。

A336284_1_En_8_Fig1_HTML.jpg

图 8-1。

JavaFXGame functional screens and how they’ll be implemented in Java 9 and JavaFX 9 by using the JVM

这将需要您向 StackPane 布局容器父分支节点添加另外四个按钮节点,并最终(在第九章)添加一个 ImageView 节点作为闪屏图像容器。这个 ImageView 节点必须添加到 StackPane“背板”中,以便成为 StackPane 中的第一个子节点(z-order=0),因为 ImageView 包含我所说的闪屏 UI 设计的背景板。因为它在背景中,所以图像需要在按钮 UI 控件节点(SceneGraph)元素之后,这些元素的 z 顺序值为 1 到 5。

这意味着最初您将只使用八个 JavaFX SceneGraph 节点对象:一个父根组节点、第二个 StackPane 布局“分支”节点和 VBox UI 容器节点中的五个“叶”按钮控件节点来创建您的 JavaFXGame(功能)信息屏幕。您的说明、法律免责声明和信用屏幕将利用 TextFlow 和 ImageView 节点,因此在第九章之后,我们将有十个节点对象。你可以使用 VBox 节点来包含 UI 按钮,我们将在本章中把游戏 UI 导航基础设施放到你的游戏应用中。这是在我们考虑添加一个组“分支”节点,以及在它下面的分支和叶节点对象来包含 3D 游戏画面之前。当然,这是您希望 Java 游戏获得最佳脉冲更新性能的地方。

如果你仔细想想,这真的没有那么糟糕,因为这些 UI 屏幕都是静态的,不需要更新。也就是说,包含在这些节点对象中的 UI 元素是固定的,不需要使用 pulse 系统进行任何更新,因此您应该还有 JavaFX pulse 引擎 99%的能力来处理我们将在本书中编写的 JavaFXGame 游戏引擎。你总是需要知道你要求 pulse 引擎处理多少 SceneGraph 节点对象,因为如果这个数字变得太大,它将开始影响游戏的 i3D 性能。如果 i3D 游戏性能受到影响,游戏将不会流畅,这将影响你的用户体验(UX)。我们保持静态的节点对象越多,每个脉冲上需要处理的就越少。

Java 类结构设计:游戏引擎支持

接下来让我们看看 JavaFXGame 代码需要如何“在引擎盖下”组装的功能结构。这将使用您的 Java 9 游戏编程代码来完成,我们将在本书中创建这些代码。正面 UI 屏幕的外观和你的底层编程逻辑的外观之间真的没有关联,因为你的游戏的大部分编程代码总是致力于在游戏屏幕上创造游戏体验。游戏说明、法律和信用屏幕将只是复合在背景图像(保存在 ImageView 对象中)上的文本(保存在 TextFlow 对象中)。记分牌和高分屏幕将需要更多一点的编程逻辑,我们将在本书的结尾进行,因为游戏逻辑需要为评分引擎创建(和播放)并能够生成高分。

图 8-2 显示了 JavaFXGame 完成所需的主要功能游戏组件。该图显示了层次结构顶部的一个JavaFXGame.java应用子类。这将在 JavaFXGame 应用的下面或内部创建顶级 JavaFXGame 场景对象及其包含的 SceneGraph。这些功能区域既可以实现为方法,也可以实现为类。在本书中,我们使用方法实现了一个 i3D 游戏。

A336284_1_En_8_Fig2_HTML.jpg

图 8-2。

Primary game engine functions, representing Java methods that you will need to code for your game

在 JavaFXGame 场景对象(在 JavaFXGame.java 应用子类中创建)的下面,是功能性 Java 9 类的更广泛的结构设计,您将需要在本书的剩余部分中对其进行编码。这些引擎(类),如图 8-2 所示,将创建你的游戏功能,如游戏引擎(游戏处理循环)、逻辑引擎(游戏逻辑)、精灵引擎(3D 几何管理)、演员引擎(角色的属性)、分数引擎(游戏分数逻辑)、渲染引擎(实时渲染)、碰撞检测、物理模拟。您将需要创建所有这些 Java 方法,以便为 i3D 棋盘游戏实现一个全面的游戏引擎。

游戏引擎类,我称之为 GamePulse.java,是创建 AnimationTimer 对象的主要类,该对象基于不断触发游戏循环的脉冲事件在高级别处理游戏逻辑。如您所知,该循环将调用 handle()方法,该方法又包含最终访问您将创建的其他类的方法调用,这些类用于管理 3D 几何图形(sprite 引擎)、在屏幕上移动 3D 对象(actor 引擎)、检测碰撞(碰撞引擎)、在检测到所有碰撞后应用游戏逻辑(逻辑引擎),以及应用物理力来为您的游戏提供逼真的效果,如摩擦、重力和风(物理引擎)。在本书的剩余部分,你将构建一些引擎,这些引擎将用于为你的玩家创造游戏体验。我们将根据每个引擎和它们需要处理的内容对章节主题进行逻辑分层,因此从学习和编码的角度来看,一切都是逻辑结构化的。

JavaFX 场景图设计:最小化 UI 节点

最小化场景图的技巧是使用尽可能少的节点来实现一个完整的 UI 设计,正如你在图 8-3 中看到的,我已经用一个组根节点对象、一个 StackPane 布局“分支”节点对象、一个 VBox 分支节点对象和八个叶(子)节点(一个 TableView、一个 ImageView、一个 TextFlow 和五个按钮 UI 控件)完成了这个。正如您将看到的,当我们接下来开始编码场景图时,我将只使用 12 个对象并只导入 12 个类来实现我们在上一节中设计的 JavaFXGame 类的整个顶级 UI。TableView 和 TextFlow 对象将覆盖在 ImageView 对象之上,ImageView 对象包含 UI 设计的背景图像。这个 TableView 对象将在本书的后面添加,并将使用来自评分引擎的代码进行更新,如图 8-2 所示,您将在未来的章节中对其进行编码。

A336284_1_En_8_Fig3_HTML.jpg

图 8-3。

Game Scene Graph Node hierarchy, objects that Nodes contain, and new media assets they reference

ImageView 背板将包含 BoardGame 插图,如果需要,您可以使用 ImageView 容器来保存不同的数字图像素材。这样,基于您的 ActionEvent 对象,处理按钮控件上的单击,您可以为每个信息屏幕使用不同的背景图像素材。VBox 父 UI 布局容器将控制五个按钮控件的布局(间距)。还有一个 Inset 对象,您将创建它来保存 UI 按钮填充值,以微调按钮对象相对于彼此的对齐方式。

由于按钮对象不能单独定位,我不得不使用 VBox 类和 Insets 类来专业地包含和定位按钮控件。在本章中,我们将讨论您将用来创建这个高级设计的类,以便您对要添加到 JavaFXGame 中的每个类有一个概述,以便为您的 JavaFXGame.java 应用子类创建这个顶级 UI 设计。

我们为需要匹配五个不同按钮的五个不同屏幕优化您的场景图形使用的方法是使用一个 ImageView 作为背板来包含游戏启动时的 BoardGame 闪屏插图。当用户单击您的 UI 按钮时,您可以使用 Java 代码让 ImageView 使用一个 ImageView 场景图节点对象引用不同的图像。您的 TextFlow 对象将覆盖 ImageView 上的文本资源。

最后,可能有一个 SceneGraph 节点,它将包含高分表的数据结构。这将通过一个分数引擎来创建,我们将在以后讨论游戏分数方法和技术时创建这个引擎。现在,我们将留下分数和游戏代码未实现。接下来让我们看看一些新的 JavaFX UI 设计类。

JavaFX 设计:使用 VBox、Pos、Insets 和 Group

在深入编码之前,让我们深入了解一些新的 JavaFX 类,我们将利用它们来完成这些顶级游戏应用 UI 和 SceneGraph 设计。其中包括 Pos 类(定位)、Insets 类(填充)、VBox 类(垂直 UI 布局容器)和 Group 类(场景图形节点分组)。在下一章中,我们将介绍 Image(图像素材持有者)、ImageView(图像背板显示)和 TextFlow(文本数据显示)类。我们将按照从最简单(Pos)到最复杂(Group)的顺序来看这些,然后您将对您的 bootstrap JavaFX 项目代码进行相当广泛的更改,这将把这些新的类(和对象)添加到您的 JavaFX 场景图层次结构中,并重新组织它以更好地适应您的游戏。

JavaFX Pos 类:使用常量的通用定位

Pos 类是 Enum 类,是 enumeration 的简称。它包含一个常量列表,这些常量实际上被转换成整数值,以便在您的代码中使用。常量值使程序员更容易在他们的代码中使用这些值。在这种情况下,它将定位常量前缀,例如顶部、中心或基线。

Pos 类的 Java 类扩展层次结构从 java.lang.Object 主类开始,经过 java.lang.Enum 类,以 javafx.geometry.Pos 类结束。您引用了图 8-10 中代码第 56 行的 Pos。Pos 位于 javafx.geometry 包中,使用下面的子类层次结构:

java.lang.Object
  > java.lang.Enum<Pos>
    > javafx.geometry.Pos

正如您将在下一节中看到的,您必须使用 Insets 类和对象来获得您想要的像素精确定位。因为这是一个 Enum 类,所以除了 Pos 类为您提供的用于 Java 游戏中的广义和相对定位的常量之外,本节没有太多要学习的。

因此,Pos 类非常适合一般的定位,使用上、下、左、右以及基线(主要用于相对于字体的定位)。其中的每一个都有一个居中选项,所以通过使用这个 helper 类中提供的十几个常量,您可以实现您需要的任何类型的通用定位。

对于一般位置的示例,请参考您的网站设计经验,您可以设计一个网页,以便它可以缩放以适应不同的窗口大小和形状。这与像素精确定位有很大不同,在像素精确定位中,从固定屏幕尺寸和形状的位置 0,0 开始,将元素精确地放置在您想要的位置!

游戏设计通常使用像素精确定位,但在本章中,我将向您展示如何将一组 UI 按钮定位在一般位置(例如用户屏幕的右上角或左下角),以便您可以尽可能多地使用 JavaFX API 实用程序类(这个类在 javafx.geometry 包中)。

你将使用 TOP_RIGHT 常量,如图 8-10 中的第 56 行所示,将你的按钮控制库定位在你的棋盘游戏用户界面设计的右上角,远离主要的中央 3D 视图。

Pos 类提供了一组常量,我将在表 8-1 中总结这些常量,用于提供“通用的”水平和垂直定位和对齐。

表 8-1。

The Pos Class Enum Constants That Can Be Used for Positioning and Alignment in JavaFX

Pos 类常数 总体定位结果
基线 _ 中心 将对象垂直放置在基线上,水平放置在中心
基线 _ 左侧 将对象垂直放置在基线上,水平放置在左边
基线 _ 右侧 将对象垂直放置在基线上,水平放置在右侧
底部中心 将对象垂直放置在底部,水平放置在中央
左下角 将对象垂直放置在底部,水平放置在左侧
右下 将对象垂直放置在底部,水平放置在右侧
中心 将对象垂直放置在中心,水平放置在中心
中央 _ 左侧 将对象垂直放置在中央,水平放置在左侧
中间 _ 右侧 将对象垂直放置在中央,水平放置在右侧
顶部 _ 中间 将对象垂直放置在顶部,水平放置在中间
左上角 将对象垂直放置在顶部,水平放置在左侧
右上方 将对象垂直放置在顶部,水平放置在右侧

Pos 类提供广义定位;它可以与 Insets 类结合使用,以提供更精确的像素定位。接下来让我们看看 Insets 类,因为它也在 javafx.geometry 包中。

JavaFX Insets 类:为用户界面提供填充值

insets 类是一个公共类,它直接扩展了 java.lang.Object 主类,这意味着 Insets 类是“临时编码”的,以在矩形区域内提供 Insets 或 offsets。想象一个相框,在它的外面和里面的图片之间留有一个“无光泽”或吸引人的边框。这就是 Insets 类用两个构造函数方法做的事情;一个提供相等或均匀的插入,一个提供不相等或不均匀的插入。

我们将使用提供不相等的插入值的构造函数,如果我们正在构造一幅图片,这会看起来非常不专业!Insets 类的 Java 类层次结构从 java.lang.Object 主类开始,并使用该类创建 javafx.geometry.Insets 类。正如你将在本章后面的图 8-11 的代码行 58 中看到的,Insets 类被设置为在两边提供 0 个像素,在两边提供 10 个像素。这会将按钮组从用户显示屏的角落推开。JavaFX Insets 类包含在 javafx.scene.geometry 包中,就像 Pos 类一样,并使用以下 Java 9 类层次结构:

java.lang.Object
  > javafx.scene.geometry.Insets

Insets 类提供了一组四个双偏移值,指定矩形的上、右、下和左侧,并且应该在构造函数方法中按该顺序指定,正如您在编写代码时看到的那样。您将使用这个 Insets 类(对象)来“微调位置”您的按钮控件组,您将使用 VBox 布局容器(您将在下一节中学习)来创建它。将这些 Insets 对象视为在另一个框内绘制一个框的方法,这显示了您希望矩形内的对象围绕其边缘“尊重”的间距。这通常被称为填充,尤其是在 Android Studio 和 HTML5 编程中。

用于创建 Insets 对象的最简单的 Insets()构造函数将使用以下格式:

Insets(double topRightBottomLeft)

此构造函数对所有间距边使用单个值(topRightBottomLeft),重载的构造函数允许您分别指定每个值,如下所示:

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

这些值需要按此顺序指定。记住这一点的一个好方法是想象一个模拟时钟。这个钟的顶部有 12,右侧有 3,底部有 6,左侧有 9。因此,只需记住从正午开始指定顺时针方向(对于西方电影类型的爱好者来说),当使用“不均匀值”构造方法时,您将有一个很好的方法来记住如何指定 Insets 值。

您正在使用 Insets 类来定位您的按钮控件库,它最初会“卡”在 BoardGame 用户界面设计的左下角。Insets 对象将允许您使用这四个 Insets 参数中的两个,将按钮控件推离屏幕的右侧和 VBox 的顶部。

JavaFX VBox 类:为您的设计使用布局容器

因为按钮对象不容易定位,所以我将把这五个按钮对象放入 javafx.scene.layout 包中的一个布局容器中,该包名为 VBox,代表垂直框。这个公共类将事物排列成一列,由于您希望按钮在 BoardGame 的边上对齐,所以它是您将用于五个按钮控件节点的父节点,这些节点将成为这个 VBox 分支节点的子叶节点。这将创建一个 UI 按钮控件的“银行”,可以作为 UI 和闪屏设计的一个单元一起定位(移动)。

VBox 类是一个公共类,它直接扩展 javafx.scene.layout.Pane 超类,后者又扩展 javafx.scene.layout.Region 超类,后者扩展 javafx.scene.parent 超类,后者扩展 javafx.scene.Node 超类,后者扩展 java.lang.Object 主类。正如你在第 55 行看到的,在图 8-10 中,你将使用 VBox 作为按钮控件定位用户界面布局容器。这个 VBox 类包含在 javafx.scene.layout 包中,就像 StackPane 类一样,它使用以下 Java 类层次结构:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.Parent
      > javafx.scene.layout.Region
        > javafx.scene.layout.Pane
          > javafx.scene.layout.VBox

如果 VBox 指定了边框或填充值,则 VBox 布局容器内的内容将“遵守”该边框和填充规范。填充值是使用 Insets 类指定的,我们在前面已经介绍过,您将在这个微调的用户界面控件按钮库应用中使用它。

您正在使用 VBox 类(object ),以及 Pos 类常量和 Insets 类(object ),以便将您的 UI 按钮对象组合在一起,并在以后将其微调为按钮控件库。因此,这个 VBox 布局容器将成为 UI 按钮控件(或叶节点)的父节点(以及分支节点)。

可以将 VBox 对象想象成一种使用列将子对象垂直排列在一起的方式。这可能是您的图像素材,排列在彼此的顶部,将使用基本的 VBox 构造函数(零像素间距)或 UI 控件,如按钮排列在彼此的顶部,间隔开,使用一个重载的构造函数。

创建 VBox 对象的最简单的构造函数将使用下面的空构造函数方法调用:

VBox()

您将用于创建 VBox 对象的重载构造函数将有一个间距值,用于在 VBox 内的子按钮对象之间留出一些空间。它使用以下构造函数方法调用格式:

VBox(double spacing)

还有另外两种重载构造函数方法调用格式。这将允许您在构造函数方法调用本身内部指定子节点对象(在我们的例子中,这些是按钮对象),如下所示:

VBox(double spacing, Nodes... children)  

此构造函数将在节点对象数组之间指定零像素的间距值:

VBox(Nodes... children)

我们将使用“简短形式”。getChildren()。addAll()方法链向您展示这是如何做到的,但我们也可以通过使用以下构造函数来声明我们的 VBox 及其按钮节点对象:

VBox uiContainer = new VBox(10, gameButton, helpButton, scoreButton, legalButton, creditButton);

如果子对象设置为可调整大小,VBox 布局容器将根据不同的屏幕大小、纵横比和物理分辨率来控制子元素的大小调整。如果 VBox 区域将容纳子对象的首选宽度,它们将被设置为该值。有一个 boolean fillWidth 属性(property),它被设置为 true 作为其默认值。这指定子对象是否应该填充(放大)VBox 宽度值。

VBox 的对齐方式由 alignment 属性(属性或变量)控制,该属性默认为 Pos 类(Pos)中的 TOP_LEFT 常量。TOP_LEFT)。如果 VBox fillWidth 属性为 false,并且 VBox 的大小超过其指定的宽度,则子对象使用其首选宽度值,多余的空间将无法利用。fillWidth 的默认设置为 true,子级宽度将被调整以适合 VBox 宽度。请务必注意,VBox UI 布局引擎将对托管子元素进行布局,而不考虑它们的可见性属性(也称为属性、特征或对象变量)设置。

你还会注意到,我们在这一章中添加的类本来就有透明或空的背景(我称之为背板),所以我们不必像在第七章中那样做任何额外的工作来保持 alpha。

现在我们已经花了几页讨论了 javafx.scene.layout 和 javafx.geometry 包中的一些类,您将使用它们来创建您的 UI(按钮对象库)设计,让我们仔细看看 javafx.scene 包中与 SceneGraph 分组相关的类。这些类将允许我们实现高级的 SceneGraph 层次结构,您需要将该层次结构放置在 VBox UI 布局容器对象中的五个 JavaFX 按钮控件 UI 元素(对象)旁边,VBox UI 布局容器对象位于 StackPane UI 层合成对象的内部。当我们在本书稍后进入 3D 和 i3D 时,这个组(节点)容器对象将保存你的 i3D 游戏对象层次。

JavaFX Group 类:高级场景图节点分组

Group 类是一个公共类,它直接扩展 javafx.scene.Parent 超类,后者扩展 javafx.scene.Node 类,后者扩展 java.lang.Object master 类。因此,组对象是 JavaFX 场景图中的一种父(分支)节点对象,用于对其他分支和叶节点对象进行分组。Group 类使用以下 Java 类继承层次结构:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.Parent
      > javafx.scene.Group

组父节点对象包含子节点对象的可观察列表,每当呈现该组父节点对象时,将按预定顺序呈现该列表。组节点对象将采用其子节点的集合(汇总)界限;但是,它不能直接调整大小。应用到组的任何变换、效果或状态都将应用到(传递到)该组节点的所有子节点,但不会应用到组本身。

这意味着这些应用的变换和效果将不包括在组父节点的布局边界中;但是,如果变换和效果是直接在该组内的子节点对象上设置的,则它们将包含在该组的布局边界中。因此,要影响组父节点的布局界限,您将通过转换组 ObservableList 的成员,而不是通过转换组对象本身,从内到外进行操作。

默认情况下,在布局过程中,组父节点将自动缩放其托管子对象集,使其大小可调整为首选大小。这确保了区域或控件子对象将随着其状态的改变而适当地缩放。如果应用需要禁用这种自动调整大小的行为,那么它应该将 autoSizeChildren 设置为 false。重要的是要注意,如果任何子对象的首选大小属性被更改,它们不会自动调整大小,因为 autoSizeChildren 已被设置为 false。这个Group()构造函数将创建一个空组。

Group()

重载的Group(Collection<Node>)构造函数方法将使用下面的构造函数方法构造一个由 Java 集合<节点>组成的组,该组包含给定的节点对象子对象的 Java 集合:

Group(Collection<Node> children)

第二个重载的Group(Node…)构造函数方法将构造一个由子节点对象的 Java 列表组成的组,在构造函数方法参数区域内构造为逗号分隔的列表。这可以通过使用以下构造函数方法格式来实现:

Group(Node... children)

既然您已经对本章中使用的各种类有了一个概述,那么让我们回到组织 JavaFXGame 类的代码上来,这样它就符合我们对游戏 SceneGraph 所做的事情。

场景图代码:优化 JavaFXGame 类

我知道您渴望开发 JavaFXGame 类代码,所以让我们清理、组织和优化现有的 Java 9 代码,以实现如图 8-3 所示的大部分顶级用户界面和场景图设计,以便您在本章创建顶级 Java 9 游戏框架的过程中取得一些进展。您要做的第一件事是将所有对象声明和命名语句放在 JavaFXGame 类的顶部,在导入块和 Java 类声明之后。这些对象声明将出现在所有方法之前。许多程序员习惯于在代码的顶部声明全局变量,也可以用同样的方式在 Java 代码的顶部声明一个空的对象声明。这种方法更有条理,这个类中的所有方法都能够“看到”(访问或引用)这些对象,而不需要使用任何 Java 修饰关键字。这是因为对象声明位于 JavaFXGame 类的顶部,而不在该类包含的任何方法内部,所以以这种方式完成的所有声明对于在它们“下面”声明的所有方法都是“可见的”。如图 8-4 所示,我正在添加一个新的组对象,我将其命名为 root,因为它将成为新的场景图根。注意 Group 下的红色波浪下划线错误,因为没有 import 语句告诉 Java 9 您想要使用 Group 类。使用 Alt+Enter 组合键弹出 NetBeans helper 弹出窗口,选择为 javafx.scene.Group 添加导入选项,如图 8-4 所示。

A336284_1_En_8_Fig4_HTML.jpg

图 8-4。

Declare the scene Scene object and the root Group object at the top of the JavaFXGame class before .start()

正如你所看到的,我还将你现有的场景场景对象的声明移到了类的顶部,所以,代替Scene scene = new Scene();,我们现在有了下面的场景对象声明 Java 代码结构,如图 8-5 所示:

A336284_1_En_8_Fig5_HTML.jpg

图 8-5。

Organize the .start() method by creating createBoardGameNodes() and addNodesToSceneGraph() methods

public class JavaFXGame extends Application {
    Scene scene;
    public void start(Stage primaryStage) {
        scene = new Scene(root, 300, 250);
    }
}

接下来,我们将对 StackPane 对象做同样的事情,我将把它重命名为 uiLayout,因为根对象现在是一个组节点类对象。添加 StackPane uiLayout 声明,如图 8-5 所示,然后将图 8-5 中红框所示的 Java 代码改为使用 uiLayout 名称代替根名称,如下:

uiLayout = new StackPane;
uiLayout.setBackground(Background.EMPTY);
uiLayout.getChildren().add(btn);

我将 uiLayout StackPane 代码放在场景实例化之前。我们将移动对象实例化,除了 Stage 对象(它需要成为。start()方法),变成自己的。createBoardGameNodes()方法,之后我们在 JavaFXGame.java 类的顶部创建了对象声明和命名块。

请记住,如果您通过在类的顶部使用类名来声明任何对象,并且该对象下面出现红色波浪下划线,您只需使用 Alt+Enter 击键组合并选择 import Java FX . package name . class name 选项,就可以让 NetBeans 为您编写导入语句。

正如您在图 8-4 中看到的,在弹出的帮助器对话框中通常有不止一个可能的导入语句,因此请确保从 JavaFX API 中选择类,因为这是我们将用于富媒体、物联网和游戏开发的类;Java 9 APIs 中所有的多媒体制作特性都保存在那里。

在我们新的顶级 Group SceneGraph 节点子类中,还有 java.security.acl.Group 类和第二个 Java FX . swing . Group layout . Group helper 类。因为我们在这里没有使用 Swing UI 元素(Java 5)和 ACL 安全性,所以我们知道要选择的正确导入语句是 javafx.scene.Group 选项。

JavaFX 对象声明:方法的全局类访问

让我们添加 JavaFX 对象声明和我们已经讨论过的新类的名称,以及在下一章设计游戏的 UI 视觉效果和闪屏元素时需要的 ImageView 和 TextFlow 对象。添加一个名为 uiContainer 的 VBox 对象(按钮对齐)、一个名为 uiPadding 的 Insets 对象、一个名为 boardGameBackPlate 的 ImageView 对象、一个名为 infoOverlay 的 TextFlow 对象以及五个名为 splashScreen、helpLayer、legalLayer、creditLayer 和 scoreLayer 的 Image 对象。向名为 helpButton、legalButton、creditButton 和 scoreButton 的按钮声明中再添加四个按钮对象,并将引导代码生成的 btn 按钮对象改为名为 gameButton。在下面的 Java 9 代码中,以及在图 8-6 中,您可以看到九行声明代码块,其中一些将是具有一个类名和多个对象名的复合声明(例如下面的 Image 和 Button,很快,我们还将有多个名为 root 和 gameBoard 的组对象):

A336284_1_En_8_Fig6_HTML.jpg

图 8-6。

Declare five new object types at the top of the JavaFXGame class and rename the btn object gameButton

Scene scene;
Group root;
StackPane uiLayout;
VBox uiContainer;
Insets uiPadding;
ImageView boardGameBackPlate;
TextFlow infoOverlay;
Image splashScreen, helpLayer, legalLayer, creditLayer, scoreLayer;     //  Compound Declaration
Button gameButton, helpButton, legalButton, creditButton, scoreButton; // a Compound Declaration

正如您在图 8-6 中看到的红色轮廓,NetBeans 将为您编写五个新的导入语句,只要您在 JavaFXGame 类的顶部键入这些对象声明和命名语句时按 Alt+Enter。

正如你看到的用黄色突出显示的,我已经将 bootstrap btn 按钮重命名为 gameButton,并将它的.setText("Hello World")更改为.setText("Start Game"),以更直接地反映这个按钮 UI 元素最终将完成什么,因为我们在本书的整个过程中会继续完善这个 Java 9 类代码。

我还将uiLayout.getChildren().add(btn);改为uiLayout.getChildren().add(gameButton);,以反映这个类中当前影响这个按钮对象的所有 Java 9 代码的名称变化。所有这些都在图 8-6 中用红色方框、蓝色线条选择和黄色对象参考选择突出显示。

只要您使用 Alt+Enter 组合键,NetBeans 9 就会为您编写这五个新的导入语句。确保选择具有正确 javafx 包类路径的选项。接下来,让我们优化您的。start()方法,卸载游戏对象实例化(Stage 除外,它是您的.onCreate(Stage primaryStage)方法的一部分),以便所有非 Stage 对象创建都使用.createBoardGameNodes()方法完成。

场景图设计:优化棋盘游戏。start()方法

现在我们可以优化。start()方法,这样它使用的代码不到十几行(如果你想向前看,请参见图 8-16 )。我想做的第一件事是将场景图节点创建 Java 构造模块化到它们自己的 createBoardGameNodes()方法中,该方法将在。start()方法,如图 8-7 所示。在方法的顶部添加一行代码,键入createBoardGameNodes();,使用 Alt+Enter 组合键让 NetBeans 9 在类的底部为您创建这个方法基础结构。此外,确保添加了root = new Group();对象实例化,因为您重命名了 StackPane 对象 uiLayout(如图 8-5 所示)。

A336284_1_En_8_Fig7_HTML.jpg

图 8-7。

Add a createBoardGameNodes() method call at the top of the .start() method and add root = new Group()

剪切并粘贴当前在。start()方法(您将在稍后添加到此)到 createBoardGameNodes()方法中,以替换 bootstrap 方法中的“尚不支持”错误代码行,如图 8-8 所示(选中)。新的。完成这个 Java 9 代码重新配置操作后,createBoardGameNodes()方法应该如下所示:

A336284_1_En_8_Fig8_HTML.jpg

图 8-8。

Select all non-Stage and non-event-handling code in the start() method and cut and paste it in new method

private void createBoardGameNodes() {
    root = new Group();
    scene = new Scene(root, 640, 400);
    scene.setFill(Color.TRANSPARENT);
    gameButton = new Button();
    gameButton.setText("Start Game");
    uiLayout = new StackPane();
    uiLayout.setBackground(Background.EMPTY);
    uiLayout.getChildren().add(gameButton);
}

请注意,我们将所有内容都从。start()方法,它不需要被“托管”在那里。因为 primaryStage Stage 对象是由。start()方法参数,我们将把所有 primaryStage 对象引用留在该方法中,以及所有事件处理结构,它们需要在应用启动时放置到位。其他的都将放在 createBoardGameNodes()和另一个 addNodesToSceneGraph()方法中,我们将在本章后面创建该方法来保存. getChildren.add()或。getChildren()。addAll()方法调用。

所以在。start()方法,我们将首先调用 createBoardGameNodes()来创建所有的 SceneGraph 节点对象(即 Node、Parent 或 Group 的所有子类),然后调用 addNodesToSceneGraph()方法来使用。getChildren()。add()方法链或。getChildren()。addAll()方法调用链。这种组织方法允许我们在构建 Java 9 游戏时向场景图中添加新节点。

接下来,让我们创建第二个 addNodesToSceneGraph()方法,我们可以用它来组织、重新配置和扩展 JavaFX 游戏应用开发工作流程的 SceneGraph 节点构建部分。

添加场景图节点:addNodesToSceneGraph()

接下来,您需要创建一个方法,将我们已经创建的 SceneGraph 节点对象以及我们将要使用 VBox 构造函数实例化的节点对象添加到场景图形根对象中,在本例中,它现在是一个组对象。这个新的更高级别的 SceneGraph 根组对象将保存您的高级游戏功能的 StackPane UI 面板,以及我们将创建的用于保存 SceneGraph 的 3D 游戏分支的另一个组对象。在某种意义上,我们已经在使用 JavaFX 9 来创建一个混合应用,因为游戏 UI (StackPane)分支将是 2D,而游戏本身(组)将是 3D 的。我们将使用。getChildren()。add()方法链或。getChildren()。addAll()方法链,用于将“子”节点(节点、父节点或组的子类)对象添加到名为 root 的“父”组对象,该对象现在是 JavaFX 场景图的“根”。

为了创建第二个方法,我们将遵循与创建第一个定制方法相同的工作流程。在第createBoardGameNodes();行代码后立即添加一行代码,然后键入 addNodesToSceneGraph();作为第二行代码。

NetBeans 9 用红色波浪错误下划线突出显示后,使用 Alt+Enter 组合键并选择创建方法“addNodesToSceneGraph”到 javafxgame。JavaFXGame 选项,如图 8-9 突出显示。我还用红色突出显示了当前在 createBoardGameNodes()方法体中的一个语句,它将被重新定位到这个新的 addNodesToSceneGraph()方法体中。这将替换原有的throw new UnsupportedOperationException() Java 语句,NetBeans 将该语句放入所有新创建的引导程序方法中,这些方法是使用这个特定的工作过程创建的,在这个过程中,您可以让 NetBeans 为您编写新的方法代码。

A336284_1_En_8_Fig9_HTML.jpg

图 8-9。

After creating addNodesToSceneGraph() method, copy the uiLayout.getChildren() method chain to new method

剪切 uiLayout.getrChildren()。添加(game button);语句,并将其粘贴到占位符上,抛出新的 UnsupportedOperationException()代码行,替换该代码。一旦我们在下一节实例化了这些新节点,我们将使用这个方法向场景图添加更多的节点。

向 createBoardGameNodes()添加新的 UI 场景图形节点

让我们将本章前面所学的那些新的 UI 设计和定位 JavaFX 类对象(VBox、Pos、Insets)添加到 JavaFXGame 类和我们创建的 createBoardGameNodes()方法中,该方法包含我们的 JavaFX 9 场景图节点对象创建(和配置)Java 9 语句。

使用以下 Java 对象实例化代码创建一个名为 uiContainer 的新 VBox,该代码将 Java new 关键字与 VBox()构造函数方法结合使用:

uiContainer = new VBox();  // Create a Vertical Box UI element container named "uiContainer"

设置 VBox 与位置的对齐。方法从 Pos 助手类中调用 TOP_RIGHT 常量。setAlignment()方法通过使用下面的 Java 语句,显示在图 8-10 的构造下:

A336284_1_En_8_Fig10_HTML.jpg

图 8-10。

Inside of the .setAlignment() method parameter area, type Pos.TOP_RIGHT and hit Alt+Enter to import

uiContainer.setAlignment(Pos.TOP_RIGHT); // Set VBox Alignment to TOP_RIGHT via Pos helper class

使用 Alt+Enter 击键组合来消除红色波浪错误下划线,并确保选择问题的正确解决方案,在本例中是 javafx.geometry.Pos 选项的 Add import,它列在最前面(最有可能是正确的解决方案),并且它是允许在您的代码中使用 Pos 类的解决方案。

下一步,我们将使用uiPadding = new Insets(0,0,10,10); Java 实例化语句创建 uiPadding Insets 对象,如图 8-11 中的第 58 行所示。最后,我们将通过使用uiContainer.setPadding(uiPadding);方法调用将 uiPadding Insets 对象“连接”到 uiContainer VBox 对象。这种联系在图 8-11 中用黄色显示,显示了 Insets 声明、实例化和实现之间的联系。

A336284_1_En_8_Fig11_HTML.jpg

图 8-11。

Create a uiPadding Insets object and wire it to the uiContainer VBox object using .setPadding(uiPadding);

我们已经将我们的按钮对象重命名为 gameButton (was btn),所以我们现在有 6 行对象实例化代码和 5 行对象配置代码,如图 8-11 所示,使用下面的 Java 9 代码:

private void createBoardGameNodes()  {
    root = new Group();
    scene = new Scene(root, 300, 250);
    scene.setFill(Color.TRANSPARENT);
    gameButton = new Button();
    gameButton.setText("Start Game");
    uiLayout = new StackPane();
    uiLayout.setBackground(Background.EMPTY);
    uiContainer = new VBox();
    uiContainer.setAlignment(Pos.TOP_RIGHT);
    uiPadding = new Insets(0,0,10,10);
    uiContainer.setpadding(uiPadding);
}

重要的是要注意,因为在场景 scene 对象的构造函数方法调用中使用了根组对象,所以这一行代码需要首先出现,以便在使用它之前创建根组对象。

接下来,让我们使用方便的程序员的快捷方式,在uiContainer.setPadding(uiPadding);方法调用下剪切并粘贴你的两个游戏按钮实例化和配置代码行,然后在它自身下复制并粘贴代码四次,如图 8-12 底部突出显示的,使用在第六章中创建的修改的游戏按钮(btn)引导 UI 元素创建所有十个用户界面按钮元素。

A336284_1_En_8_Fig12_HTML.jpg

图 8-12。

Create 10 Button object instantiation and configuration statements at the end of createBoardGameNodes()

这将允许您将 gameButton 分别更改为 helpButton、scoreButton、legalButton 和 creditButton,以创建五个独特的 UI Button 对象。按钮的 Java 9 游戏代码应该如下所示:

gameButton = new Button();
gameButton.setText("Start Game");
helpButton = new Button();
helpButton.setText("Game Rules");
scoreButton = new Button();
scoreButton.setText("High Scores");
legalButton = new Button();
legalButton.setText("Disclaimers");
creditButton = new Button();
creditButton.setText("Game Credits");

在 addNodesToSceneGraph()中添加新的 UI 设计节点

正如你在图 8-13 中看到的,Java 代码是没有错误的,我现在已经声明并实例化了另一个名为 gameBoard 的组对象。这将包含 SceneGraph 的 3D game elements 分支,因此 Group 对象声明现在已经成为类顶部的复合语句。我在代码中单击了 gameBoard 对象,以创建该对象声明的高亮跟踪,在 createBoardGameNodes()中实例化,并在 addNodesToSceneGraph()中使用,这表明如果在类的顶部声明,就可以在任何需要的地方使用对象。这种单击要跟踪的对象名称的方法是 NetBeans 9 的一个有用的技巧,只要您想跟踪对象的使用情况,就可以使用这种方法。我将经常在截图中使用它来突出我在添加新的 Java 代码时正在做的事情(以及为什么)。

A336284_1_En_8_Fig13_HTML.jpg

图 8-13。

Add gameBoard Group object and add Node objects to SceneGraph using .getChildren().add and .addAll()

接下来,让我们确保我们的节点被正确地添加到场景图中。在场景图形的根(顶部)之外,这是一个组对象,我们将有另一个 gameBoard 组对象来保存 i3D 游戏元素和素材,以及 uiLayout StackPane 对象。使用以下语句将它们添加到根组中:

root.getChildren().add(gameBoard);        // Add new i3D Game Group Node to root Group Node

root.getChildren().add(uiLayout);         // Add uiLayout StackPane Node to root Group Node

接下来,我们将 uiContainer VBox 布局容器分支节点添加到 uiLayout StackPane 分支节点,并将五个按钮 UI 元素叶节点添加到 uiContainer VBox。这是使用两行 Java 9 代码完成的,如下所示:

uiLayout.getChildren().add(uiContainer);      // Add VBox Vertical Layout Node to StackPane Node

uiContainer.getChildren().addAll(gameButton,  // Add All UI Button Nodes to the VBox Node
                                 helpButton,
                                 legalButton, creditButton, scoreButton);

图 8-13 显示了这个场景图构造代码。我在对象层次上使用了颜色填充,可视化地显示节点对象(更准确地说是节点子类对象),它们是场景、根或分支节点。(如果您想查看这些 JavaFX 根、分支和叶节点对象层次结构,如图 8-3 所示。)

这里要注意的重要事情是您将节点对象添加到组根场景图形对象的顺序。该顺序会影响 3D 元素之上的场景渲染合成以及 UI 元素合成的合成层顺序。添加到根组的第一个节点将位于场景合成(渲染)堆栈的底部。因此,这需要是游戏板组节点对象,它将保存 i3D 游戏,以便节点首先被添加到场景图形根,如果你向下看,它在底部,或者如果你向前看,它在场景合成和渲染堆栈的后面。你可以在图 8-13 中看到这一点。

下一个要添加的节点将是您的 uiLayout StackPane 节点对象,因为您的 2D 用户界面(浮动)面板将需要覆盖在您的 3D 游戏板的顶部。将这些顶级节点对象放入场景图形层次后,我们可以将 uiContainer VBox 节点对象添加到 StackPane 节点对象,该对象将包含所有按钮控件叶节点对象。请注意,我们使用的是。getChildren()。addAll()方法链将按钮控件对象添加到 VBox 中,因为我们可以在。addAll()方法调用(链)从。getChildren()方法。

在第九章中,我们还将添加一个名为 boardGameBackPlate 的 ImageView 对象和一个名为 infoOverlay 的 TextFlow 对象。在第九章中,我还需要实例化五个图像对象来保存内存中的数字图像素材,以便我们在本章中声明的图像对象可以被实现。如您所知,我们使用复合 Java 语句将这些对象命名为 splashScreen、helpLayer、legalLayer、creditLayer 和 scoreLayer,就像我们对 Button 对象所做的那样。

交互性:创建 BoardGame 按钮 UI 控件

接下来您需要做的是将 gameButton.setOnAction()事件处理 Java 代码结构复制到您的。start()方法,然后在它本身下面再粘贴四次,以创建 helpButton、legalButton、creditButton 和 scoreButton 按钮控件对象事件处理结构。出于测试目的,在这一阶段,您可能希望将 System.out.println 语句更改为每个语句在输出控制台窗口中打印一条唯一的消息,这样您就可以确保五个按钮 UI 元素中的每一个都是唯一的,并且能够正确地处理其按钮事件。在继续添加更多的 Java 代码从而增加应用的复杂性之前,务必确保 Java 9 代码结构在每个阶段(也就是说,在每次更改或增强之后)都能正常工作。在开发过程中,这比一次编写所有代码花费的时间要长一点,但是可以节省调试时间。

如果您想知道图 8-14 中的黄色波浪下划线警告(或建议)是什么,以及我将鼠标放在您的事件处理程序 ActionEvent 处理构造EventHandler<ActionEvent>() { public void handle(){...} });下的黄色突出显示上时生成的弹出消息,这是因为可以使用较少的代码将该表达式转换为 lambda 表达式。请注意,这样做将确保您的代码只能在 Java 8 和 Java 9 下运行。如果您想在使用 Java 6 和 Java 7 的 Android 中使用您的代码,您可能只想保留这些稍微长一点的 Java 代码结构,因为它们做完全相同的事情。

A336284_1_En_8_Fig14_HTML.jpg

图 8-14。

Copy the gameButton event processing code and paste it underneath itself and create your helpButton event handling

完成后,新的事件处理结构应该看起来像下面的 Java 代码,如图 8-15 中间所示:

gameButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        System.out.println("Starting Game");
    }
});

helpButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        System.out.println("Game Instructions");
    }
});

scoreButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        System.out.println("High Score");
    }
});

legalButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        System.out.println("Copyrights");
    }
});

creditButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        System.out.println("Credits");
    }
});

正如你在图 8-15 中所看到的,你的事件处理代码是没有错误的,你已经准备好运行和测试你的 JavaFXGame.java 游戏应用,以确保你的场景图形层次被渲染到屏幕上,并且你的按钮 UI 控制对象正在正确地处理事件。一旦你确定你的场景图是在这个高层次上为你的游戏构建的,并且你的核心用户界面处理 Java 9 代码结构也在适当的位置并且正常工作,你就可以在下一章中继续添加数字图像素材并微调所有的 UI 元素定位,这样你就可以在游戏的最高层次上使一切看起来和运行起来都正确。

A336284_1_En_8_Fig15_HTML.jpg

图 8-15。

Copy the gameButton and helpButton and paste them to create your scoreButton, legalButton, and creditButton

正如您在图 8-16 中看到的,在您复制。setOnAction()事件处理构造,当您使用屏幕左侧的减号图标折叠 EventHandler 例程时(在图 8-16 的左侧用红色圆圈显示),您在。start()方法。第一行代码将调用一个方法来创建节点对象并配置它们,第二行代码将调用一个方法来将这些节点对象添加到场景图形层次结构中,第 3 行到第 6 行将配置 Stage 对象,第 7 行到第 11 行将设置 UI 按钮控件对象事件处理。如果考虑到您正在添加到游戏基础架构顶层的功能数量,这是相对紧凑的,包括为您的游戏、说明、法律免责声明、演职员表和记分牌显示创建顶层(根和分支节点)场景图形结构和用户界面设计元素。

A336284_1_En_8_Fig16_HTML.jpg

图 8-16。

Click the Run (Play) icon at the top of NetBeans and test your code to make sure your UI design is working

接下来,是时候测试重组 JavaFXGame 类并为游戏应用创建 UI 设计结构和场景图层次结构的代码了。让我们确保所有这些 UI 按钮元素(对象)都正常工作。

测试你的棋盘游戏:处理场景图

单击图 8-16 中用红色圈出的 NetBeans 9 IDE 顶部显示的绿色播放箭头,并运行您的 JavaFXGame 项目。这将调出 VBox UI 布局容器,在图 8-16 的中上部用红色圈出。正如您所看到的,您正在获得专业的结果,没有崩溃,使用了大约 12.5 个导入语句(外部类),几十行 Java 代码,以及场景图形根组节点对象下不到 12 个子节点。优化您的场景图形层次非常重要,因为 JavaFX 将用来处理您的游戏设计结构的每个脉冲事件都必须遍历该层次,因此它越紧凑,您的游戏性能就越好,您的用户体验也就越流畅。因此,您应该从一开始就优化一切。正如你在图 8-17 底部的 Output-JavaFXGame 标签中看到的红色圆圈,我已经测试了所有按钮 UI 控件对象的事件处理结构。

A336284_1_En_8_Fig17_HTML.jpg

图 8-17。

Click each of the Button objects and make sure your event-handling code is printing the right messages

我这样做是为了确保每个按钮都实现了自己的事件处理,并且在我单击五个 UI 按钮控件中的每一个时都打印出正确的 System.out.println()文本消息。

稍后,我们可以将此 System.out.println()方法调用替换为另一个控制 ImageView 对 Image 对象的引用的方法调用,从而允许我们在您的用户界面设计 ImageView 数字图像背板支架的数字图像素材之间进行切换。

由于我们只复制和粘贴了每个按钮的 EventHandler 例程,并且只更改了按钮对象的名称和在这些例程中执行的代码,所以这些按钮对象应该仍然可以正常工作(将文本写入控制台)并且不会导致任何编译器错误。但是,它们最终不会按照您的要求去做,也就是更改 ImageView 对象(UI 背板)底层中引用的 Image 对象,或者使用 TextFlow 在该对象上放置适当的文本。这是你在下一章将要编写的代码;您还将做一些 UI 设计调整,将按钮库放在显示屏上的适当位置。如图 8-16 所示,虽然按钮控件对象确实在 VBox UI 容器节点内的 TOP_RIGHT 位置对齐,但是 VBox 本身还没有在其父(分支)StackPane 节点对象内对齐。就像第七章中的透明胶片一样,VBox(在 StackPane 中)和 StackPane(在组中)必须正确定位。

恭喜你,你保持了在第七章中添加的改进,并采用了新的方法来组织你的场景图层次,并改进了这个场景图,以包括你的 i3D 游戏分支,我们将在本书的后半部分开始添加对象和素材。

摘要

在第八章中,我们改进了游戏的实际顶级用户界面设计,概述了底层游戏引擎组件设计,并使用不到十几个节点实现大多数顶级游戏用户界面结构,找出了最有效的场景图节点设计,从而让您了解了我们的 JavaFXGame.java 代码。通过重新设计现有的 JavaFXGame.java 引导 Java 代码,您又回到了 Java 游戏编程中,这些代码最初是由 NetBeans 9 在第六章中为您创建的。由于 NetBeans 9 生成的 Java 9 代码设计对您的目的来说不是最佳的,所以您对它进行了重大的重写,使它更加模块化、简化和有组织。

您通过创建两个新的 Java 方法做到了这一点:。createBoardGameNodes()和。addNodesToSceneGraph()。这样做是为了模块化场景图节点创建过程,也是为了模块化添加两个父分支节点和五个控制叶节点对象到场景图根,在这种情况下恰好是组节点对象。在该目录下,您有一个名为 uiLayout 的 StackPane 分支节点,它用于多层 UI 对象合成功能,还有一个名为 gameBoard 的组分支节点,您将使用它来保存您将在本书剩余部分构建的 i3D 游戏对象层次结构。

您了解了我们将在这些新方法中实现的一些 JavaFX 类。其中包括 javafx.scene.geometry 包中的 Pos 类和 Insets 类、javafx.scene.layout 包中的 VBox 类以及 javafx.scene 包中的 Group 类。你编写了新的。createBoardGameNodes()方法,该方法使用 Inset 对象、StackPane uiLayout 分支节点对象、Group gameBoard 分支节点对象和五个 UI 按钮控件叶节点对象来实例化和配置 VBox 对象。

一旦实例化并配置了所有场景图节点,您就可以构建您的。addNodesToSceneGraph()方法将场景图节点对象添加到组根对象。这样做是为了在 Stage 对象中显示正确的场景图节点层次,它将引用和加载场景图根组节点对象以及我们在它下面构建的层次。

最后,您创建了其他四个按钮 UI 控件对象,并添加了 ActionEvent EventHandler 程序逻辑。这就完成了我们本章的编程任务,这些任务与为 JavaFXGame.java Java 9 游戏应用设置场景图层次和用户界面设计基础结构有关。

完成所有编码后,您就可以在 NetBeans 9 中测试您的顶级 Java 9 游戏应用用户界面设计和场景图层次结构了。

在下一章中,你将会在你的用户界面设计中添加很酷的数字图像资源,并在定位和对齐方面下功夫,同时让所有东西都与你的 UI 按钮对象一起工作。

九、JavaFX 9 用户界面设计:Java 9 游戏设计的前端

让我们通过继续设计你的 i3D 棋盘游戏的前端用户界面基础设施来建立你在第八章中建立的顶级场景图架构。这将在 StackPane 分支节点内部完成,使用该节点下的三个主要节点。VBox 分支节点保存按钮叶节点,ImageView 叶节点显示不同的图像对象,TextFlow 叶节点显示(流动)覆盖在 ImageView 节点顶部的不同文本描述。这七个叶子节点将共同组成你游戏的顶层用户界面设计。StackPane 节点将充当背景图像板(holder ),而 ImageView 叶节点将保存五个不同的图像对象,这些对象为五个按钮中的每一个引用您的数字图像部分素材。StartGame 背景图像素材将被视为闪屏。在 StackPane 层次结构中 ImageView 的顶部是 TextFlow 叶节点,它将作为前景文本信息的持有者,并根据单击的按钮控件对象引用不同的文本数据。在 TextFlow 层之上是 VBox 分支节点层,它将包含五个按钮叶节点。这将保持、对齐和定位您的五个按钮控件对象,最终将使用事件处理程序将不同的图像对象交换到您的 ImageView 对象中,并将不同的文本数据交换到您的 TextFlow 对象中。

既然你已经声明了你的五个背景图像对象(以及你的 ImageView 和 TextFlow 对象,用于 JavaFXGame.java 类的顶部),我要介绍的第一件事就是你如何使用你在第八章开始构建的场景图层次来完成你的用户界面设计。

接下来,我们需要介绍 javafx.scene.image 和 javafx.scene.text 包中的四个新 JavaFX 类,在本章中,您将对它们进行实例化和配置,以便在 Java 游戏中使用。这些类包括 Image 类、ImageView 类、Text 类和 TextFlow 类。

接下来您需要做的事情是创建将被加载到 Image 对象中的背景图像,这样您就可以在以后测试您的 Java 代码,从而确保它能够正常工作。

之后,您将学习一个很酷的技巧,将另一个合成层添加到您的合成管道中,而无需将另一个节点对象添加到场景图形层次中。这将包括了解如何利用 JavaFX 背景类(对象)以及 JavaFX 背景图像类(对象)来利用节点子类的背景属性作为 pro Java game 数字图像合成管道中的另一个图像对象保持层。

所有这些都将涉及到向您的。createBoardGameNodes()方法和您的。addNodesToSceneGraph()方法将 ImageView 背景图像板和 TextFlow 信息文本覆盖放置在 VBox 按钮控件组的后面。您还将在 JavaFXGame 类中添加两个新的 Java 方法来加载图像资源(loadImageAssets()方法)和创建文本资源(createTextAssets()方法)。为了让你的用户界面更有条理、更专业,你需要进行大量的编码、重新编码、重新布线(对象引用变化)和参数调整,我们将在学习完本章关于用户界面的课程中用到的一些 JavaFX API 类后开始。

UI 设计基础:完成场景图

在这一章中,你要做的第一件事就是完成顶层用户界面屏幕的场景图设计。这意味着实例化您的 ImageView 数字图像显示底板,它将保存您的背景图像对象,这些对象将引用您的数字图像素材。在本章中,您将引用这些使用 GIMP 2.10 创建的数字图像素材。在场景图层次结构中的 ImageView 之上,您将添加一个 TextFlow 信息容器;因此,您的文本内容将位于背景图像的上方,而不是下方。最后,在这两个叶节点对象之上是 VBox 按钮控件分支节点对象,您已经在第八章中创建并实现了它。图 9-1 显示了最终的场景图层次(在图 7-3 中展开你在第七章看到的通用根、分支和叶场景图)。这一次,我为您的 pro Java 9 游戏应用进行了定制。注意,在 i3D 组分支节点叶节点对象上没有连接器,因为我们还没有在 Java 代码中实现它们。

A336284_1_En_9_Fig1_HTML.jpg

图 9-1。

BoardGame user interface design Scene Graph hierarchy, showing the root, branch, and leaf Node objects

这将需要您向 StackPane 布局容器父分支节点添加两个叶节点,如图 9-1 左下方所示。在我们开始编写 Java 代码来实例化。createBoardGameNodes()方法,并将其添加到场景图形层次结构中,让我们对您将在本章中放置的新 Java 语句中使用的每个类进行概述。

JavaFX 9 UI 合成:ImageView 和 TextFlow

接下来,让我们看看主要的 JavaFX 类,这些类可以用来为游戏启动画面和文本信息画面创建基本的合成管道,这些屏幕将位于您在前一章中创建的 UI 按钮库的旁边(和下面)。游戏说明、高分、法律和信用屏幕实质上是由背景图像(保存在 ImageView 对象中)合成的文本(保存在 TextFlow 对象中)。启动画面将与开始游戏按钮相关联,并且将在启动游戏应用时显示;当按下开始游戏按钮时,它将变得不可见。这是因为 StackPane UI 构造位于比根组和游戏板组节点对象更高的 z 顺序上,在场景图中它们位于它的上面。这意味着 StackPane 中任何不透明的东西都会覆盖(遮挡视线)场景图形根正下方的 i3D 游戏板组,如图 8-3 所示。我们先来看看图像类。

JavaFX 图像类:在设计中引用数字图像

Image 类是一个公共类,它直接扩展了 java.lang.Object 主类,这意味着 Image 类也是“临时编码”的,以提供图像加载(引用)和缩放(调整大小)。您可以锁定缩放的纵横比,也可以指定缩放(算法)质量。支持所有由java.net.URL类支持的 URL。这意味着您可以从互联网( www.domainname.com/imagename.png )、操作系统文件系统(file:imagename.png)或使用正斜杠字符(/imagename.png)的 JAR 文件中加载图像。

JavaFX Image 类是 javafx.scene.image 包的一部分。javaFX Image 类的类层次结构源自 java.lang.Object 主类,并使用以下 Java 类层次结构:

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)

如果您希望加载图像,并让 constructor 方法使用最高质量的重新采样(平滑像素缩放)将图像缩放到不同的宽度和高度(通常会更小,以获得更好的质量),同时锁定(保留)纵横比,则可以使用以下格式的 image 对象构造函数:

Image(String url, double scaleWidth, double scaleHeight, boolean preserveAspect, boolean smooth)

如果您希望使用图像的“原始”或“物理”(默认)分辨率和原始纵横比来加载图像,并让它在后台(异步)加载图像,您可以对 image()构造函数使用以下格式:

Image(String url, boolean backgroundLoading)

还有两个 Image()构造函数方法使用 java.io.InputStream 类。这个类为 Image()构造函数方法提供了一个低级的 Java 输入流。通常,您会使用 URL 来引用您的数码图像文件。这两种图像对象构造器格式采用以下格式。简单的格式如下:

Image(InputStream is)

复杂的 InputStream 构造函数方法允许您指定宽度、高度、纵横比锁定和图像缩放插值平滑算法(开/真或关/假)。第二种格式如下所示:

Image(InputStream is, double newWidth, double newHeight, boolean preserveAspect, boolean smooth)

因此,Image 类(object)用于准备要使用的数字图像素材,也就是说,从 URL 读取其数据,必要时调整其大小(使用您喜欢的任何平滑和纵横比锁定),甚至在应用中进行其他操作时异步加载它。值得注意的是,这个图像类(或对象)不显示您的图像素材;它只是加载它,根据需要进行缩放,并将其放入系统内存,以便在您的应用中使用。

为了显示一个图像对象,你需要使用第二个类(对象),叫做 ImageView,我们将在本章的下一节中讨论它。此 ImageView 对象实现为场景图形和引用中的叶节点,然后将图像对象数据“绘制”到包含此 ImageView 节点的布局容器上。在我们的例子中,这是叶 ImageView 节点之上的 uiLayout StackPane 父(或分支)节点。

从数字图像合成的角度来看,StackPane 类(对象)是层合成引擎,也可以称为层管理器,ImageView 对象表示层堆栈中的一个数字图像层。图像对象包含显示在 ImageView 层内部或多个 ImageView 中的数字图像数据(如果需要的话),因为图像对象和 ImageView 对象是分离的,因此彼此独立存在。我试图最小化场景图节点的使用,所以我使用一个 ImageView 图像板和一个文本信息合成板来创建用户界面屏幕,然后使用代码来切换它们。

JavaFX ImageView 类:在设计中显示数字图像

ImageView 类是一个公共类,它直接扩展 javafx.scene.Node 超类,后者是 java.lang.Object master 类的扩展。因此,ImageView 对象是 JavaFX 场景图中的一种节点对象,用于使用 Image 对象中包含的数据绘制图形视口。该类具有允许图像重采样(调整大小)的方法,并且与 image 类一样,您可以锁定缩放的纵横比,以及指定重采样算法(平滑质量,通过使用像素插值)。

正如您在 Java 代码的第 24 行中所看到的,如图 8-6 所示,您将使用一个名为 boardGameBackPlate 的 ImageView 对象来显示您的图像对象数据。与 Image 类一样,这个 ImageView 类也包含在 javafx.scene.image 包中。ImageView 类的 Java 类层次结构从 java.lang.Object 主类开始,并使用该类创建 javafx.scene.Node 类,然后使用该类创建 javafx.scene.image.ImageView 节点子类。ImageView 类使用以下 Java 类继承层次结构:

java.lang.Object
  > javafx.scene.Node

    > javafx.scene.image.ImageView

ImageView 类提供了三种不同的(重载的)ImageView()构造函数方法。这些构造函数从空的 ImageView 构造函数(您将在后面的代码中使用它)到将 Image 对象作为其参数的构造函数,再到将 URL String 对象作为参数并自动创建 Image 对象的构造函数。最简单的空参数列表 ImageView()构造函数方法将创建一个(空的)ImageView 对象(即没有要显示的图像对象但可以保存图像对象的对象)。它将使用以下格式:

ImageView()

我们将使用这个构造函数方法,这样我就可以向您展示如何使用?setImage()方法调用将图像对象加载到 ImageView 对象中。如果您想避免使用。setImage()方法调用,您可以使用另一个重载的构造函数方法。该 ImageView 对象构造函数将使用以下格式:

ImageView(Image image)

因此,我将显式设置一个 ImageView 并将其连接到 Image 对象的方式如下所示:

boardGameBackPlate = new ImageView();      // This uses empty constructor method approach
boardGameBackPlate.setImage(splashScreen);

这可以使用重载的构造函数方法压缩成一行代码,结构如下:

boardGameBackPlate = new ImageView(splashScreen);  // using the overloaded constructor method

如果您还想跳过创建和加载图像对象的过程,还有另一个构造函数方法,它使用以下格式:

ImageView(String url)

如果您希望使用图像的“原始”或“物理”(默认)分辨率和原始纵横比来加载图像,并让它在后台(异步)加载图像,image()构造函数将使用以下格式:

backPlate = new Image("/backplate8.png", 1280, 640, true, false, true);
boardGameBackplate = new ImageView();
boardGameBackplate.setImage(backPlate); // use empty ImageView constructor method approach

如果您不想指定图像尺寸、背景图像加载或平滑缩放,并且您想锁定任何缩放的纵横比,您可以将前面三行 Java 代码压缩到下面的一个构造函数中:

boardGameBackPlate = new ImageView("/backplate8.png");   // uses third constructor method

至少在开始时,出于学习的目的,我会用长时间的方式来做这件事,我将总是使用 Image()构造函数方法“显式地”加载图像对象,这样我们就可以指定所有不同的属性,这样您就可以看到您在 Java 9 编程逻辑中使用的所有不同的图像素材。我想在这里向您展示快捷方式代码,因为如果您开始将 ImageViews 用作 2D 精灵,您以后可能会想要使用这种快捷方式。你可以对你的精灵使用这种快捷方式,因为你不会缩放他们,因为他们是如此高度优化,以至于后台加载选项,节省了很长的加载时间,将是不必要的。

JavaFX TextFlow 类:在设计中使用文本对象(内容)

TextFlow 类是一个公共类,允许开发人员创建文本段落。文本段落是多行文本的容器,每一行都用“换行符”分隔,在 Java 代码中用“escape n”序列表示。

因此,TextFlow 类将使用以下 Java 类继承层次结构:

java.lang.Object
  > javafx.scene.Node

    > javafx.scene.Parent
      > javafx.scene.layout.Region
        > javafx.scene.layout.Pane
          > javafx.scene.text.TextFlow

该 TextFlow 对象是 JavaFX 场景图中的节点对象类型,可用于使用文本对象中包含的数据呈现文本段落,与 ImageView 呈现图像对象中包含的数据的方式非常相似。但是,TextFlow 可以一次处理多个文本对象,允许您使用类似。setFill()和。setFont()。TextFlow 是一个专门的文本布局类,旨在呈现通常称为富文本格式(RTF)的内容。有些人称之为桌面出版,它涉及到使用不同的字体、风格或颜色来增强基于文本的内容的呈现。有趣的是,javafx.scene.text 保存在 javafx.graphics 模块中,而不是 javafx.controls 模块中。这一点很重要,因为如果您想优化(而不是使用)JavaFX 9 UI 控件类(100 个或更多),您仍然可以仅使用 javafx.base 和 javafx.graphics 模块使用 Image、ImageView、Text 和 3D geometry 对象创建自己的 UI 元素,这为您提供了创建 pro Java 9 i3D 游戏所需的一切。

TextFlow 对象可用于在单个 TextFlow 对象中布局多个文本节点。TextFlow 对象使用其内部每个文本节点对象的文本、字体和样式设置,加上其自己的最大宽度和文本对齐样式属性,来确定呈现每个子文本对象的位置。

由于 TextFlow 对象的包装功能,单个文本节点可以跨越多行,并且由于双向(bidi)重新排序,文本节点的可视位置可以不同于逻辑位置。Java Bidi 对象提供了关于用来创建它的文本的双向重新排序的信息。例如,要正确显示从右向左(RTL)而不是从左向右(LTR)阅读的阿拉伯语或希伯来语文本,这是必需的。

当然,除了文本节点对象之外,任何其他节点对象类型都将被视为 TextFlow 对象布局中嵌入的“富内容”对象。它将使用其首选的宽度、高度和基线偏移值插入到内容中,并相对于父 TextFlow 对象中的其他文本对象进行间隔和对齐。

当文本节点对象位于 TextFlow 对象内部时,它的一些属性将被忽略。例如,文本节点对象的 X 和 Y 属性将被忽略,因为子文本节点的位置是由父 TextFlow 对象决定的。同样,文本节点中的换行宽度将被忽略,因为用于换行的最大宽度将继承 TextFlow 对象的最大宽度属性。TextFlow 布局的环绕宽度将由 Region 对象的当前宽度决定。这可以由应用通过设置 TextFlow 对象的 preferred width 属性来指定。如果不需要换行特性,应用可以将首选宽度设置为两倍。MAX_VALUE 或 Region。USE_COMPUTED_SIZE。应该在任何子文本节点对象中使用换行符或\n(转义符)分隔段落,如下面的粗体代码示例所示。

在 TextFlow 对象中呈现文本节点对象时,该对象的 pickOnBounds 属性值将被设置为 false。发生这种情况是因为单个文本节点对象中的内容可能会被 TextFlow 算法分割,并由于换行和双向重新排序而被放置在 TextFlow 中的不同位置。TextFlow 算法将对每个托管子文本节点对象进行布局,而不考虑该子文本节点的 visibility 属性值,为设置为不可见的文本节点对象留出间隙。以下是 TextFlow 对象创建工作流程的示例:

Text titleText = new Text("Welcome to iTVboardgame! \n");
titleText.setFill(Color.RED).setFont(Font.font("Helvetica", FontPosture.ITALIC, 40));
Text pressPlayText = new Text("Press the Start Game Button to Start!");
pressPlayText.setFill(Color.BLUE).setFont(Font.font("Helvetica", FontWeight.BOLD, 10));
TextFlow gameTextFlow = new TextFlow(titleText, pressPlayText);

TextFlow 类有两个属性:DoubleProperty lineSpacing 属性,它使用文本行之间的像素定义垂直间距;object propertyText alignment 属性,它定义水平文本对齐常量,如 LEFT、RIGHT、CENTER 或 JUSTIFY。

TextFlow 类有两个构造函数方法;第一个有一个空参数区域,并构造一个空的 TextFlow 文本布局对象。此构造函数方法将使用以下格式:

TextFlow()

前面使用的第二个 TextFlow 构造函数方法使用子文本(或富媒体)节点对象创建一个 TextFlow,这些子文本(或富媒体)节点对象使用逗号分隔的列表传递到参数区域,格式如下:

TextFlow(Node... children)

第二个构造函数方法采用节点对象的参数列表数组的原因是因为 TextFlow 对象支持“富文本布局”,这是文本对象和其他支持富媒体(图像、形状、几何图形、网格、动画、视频等)的节点对象的组合。).

让我们回到编码,实例化和配置图像,图像视图,文本和文本流对象,以便您可以将它们添加到您现有的场景图层次结构中,以实现图 9-1 所示的内容。之后,在第十章中,我们可以在你的按钮动作事件处理程序中编写代码,它将基于点击定制你的用户界面。

用户界面编码:UI 合成管道

要进入用户界面设计,您将需要实例化 ImageView 和 TextFlow 对象,将它们添加到场景图的层次结构中的适当位置,将数字图像导入到您的项目中,创建一种用数字图像素材加载图像对象的方法,创建一种用适当的信息创建文本对象的方法,最后调整场景图和 UI 元素以微调 UI 最终结果。

实例化合成层:。createBoardGameNodes()

由于您已经在第八章中声明了 boardgameb 背板 ImageView 和 infoOverlay TextFlow 并编写了这些类的导入语句,接下来您需要做的是使用 Java new 关键字以及它们的基本(空参数列表)构造函数方法将它们实例化为对象。您将在 createBoardGameNodes()方法中做到这一点,以保持事情高度有序。为了镜像场景图形层次,您将在 StackPane 之后和 VBox 之前实例化它们,因为这将是您将使用的合成(层)顺序。正如你在图 9-2 中看到的,Java 代码是没有错误的,你有一个场景图根、i3D 游戏板分支和 UI 布局分支,它们仅使用系统内存中的一个根和三个分支节点对象进行实例化,包括一个组节点、一个 StackPane 和一个 VBox(Insets 对象是一个实用程序对象,而不是一个场景图节点)。

A336284_1_En_9_Fig2_HTML.jpg

图 9-2。

Instantiate boardGameBackPlate and infoOverlay objects inside of your createBoardGameNodes() method

如果算上持有 SceneGraph 的场景对象,内存中有五个游戏组织对象。向其中添加 Stage 对象,该对象是使用。start()方法和由 JavaFXGame 类创建的应用对象扩展了应用声明,并且您已经使用系统内存中的七个对象为 pro Java 9 游戏开发创建了顶级基础结构。使用 ImageView 和 TextFlow 显示,我们在系统内存中仍然有不到 10 个对象。一旦我们用数字图像素材加载了您的五个图像对象,并设置了五个 UI 按钮对象,您在内存中仍然只有不到 20 个对象,这仍然是非常好的优化。在本章的后面,您还将添加八个文本对象,但它们不是以像素为中心的,所以它们不会占用太多内存。我们还将使用一些实用对象,比如 Insets,但即使有这些,在你开始添加核心 3D 对象组成你的 i3D 棋盘游戏之前,你仍然会有 30 个以下的对象。接下来,让我们将 ImageView 和 TextFlow 添加到场景图中,将它们放在 VBox UI 按钮库的后面,以便它们首先渲染。

将 UI 背板添加到场景图形:addNodesToSceneGraph()

对于位于场景、3D 游戏板和 StackPane 之上但在 VBox 按钮库之后的 ImageView 合成层和 TextFlow 信息板,您将需要。getChildren()。在根方法调用之后,uiContainer 方法调用之前,从 uiLayout StackPane 对象中调用 add()方法。这如图 9-3 所示,将在 addNodesToSceneGraph()方法结构中使用以下两个 Java 语句:

A336284_1_En_9_Fig3_HTML.jpg

图 9-3。

Add boardGameBackPlate and infoOverlay to your Scene Graph in the addNodesToSceneGraph() method

uiLayout.getChildren().add(boardGameBackPlate); // Add ImageView backplate behind TextFlow Node
uiLayout.getChildren().add(infoOverlay);      //  Add TextFlow information overlay second

由于按钮对象不能单独定位,我不得不使用 VBox 类和 Insets 类来包含和定位一组垂直的按钮控件。现在我们已经准备好编写两个素材加载方法了。

素材加载方法:loadImageAssets()和 createTextAssets()

在本书的过程中,当我们创建这个游戏时,我们想做的下一件事是创建另外两个专用的方法来加载图像对象素材和创建文本对象素材。这就创建了一个专门的“基于方法的工作流程”来为你的游戏添加元素。在中实例化。createBoardGameNodes(),添加到。addNodesToSceneGraph(),引用。loadImageAssets(),并在。createTextAssets()。正如你在图 9-4 中看到的,我已经把这两个新的方法调用放在了。start()方法,并让 NetBeans 为它们创建空方法,当我们向游戏中添加资源时,我们将向其中添加 Java 代码。我把它们放在 start 方法的顶部,这样你的应用就可以首先把这些资源加载到系统内存中,这样当你的 pro Java 游戏代码需要它们的时候,它们就在那里了,这样我们就不需要使用任何专门的预加载器了。此外,这些对象在被其他方法调用之前必须就位,因此需要首先调用它们,在设置更高级的对象并将它们添加到 SceneGraph 层次结构的方法之前。稍后,我们可以使用 NetBeans 9 profiler 确保素材加载不到一秒钟,一旦我们有了 3D 对象渲染和游戏处理逻辑等需要确保高度优化的内容,并且在处理场景图时不会占用太多 Pulse 引擎的 60 FPS 中断(时间片)。

A336284_1_En_9_Fig4_HTML.jpg

图 9-4。

Create empty methods for loadImageAssets() and createTextAssets() to create your image and text assets

在我们继续之前,我们需要创建一些新的媒体素材来与图像对象一起使用,我们将使用 PNG32 数字图像素材实例化和引用(加载),这将利用 alpha 通道。这种 alpha 通道数据将允许我们在任何背景图像上合成这些徽标或屏幕字幕,如果我们选择继续下去,甚至可以在 i3D 游戏板本身上合成。在下一节中,我将在 Autodesk 3D Studio Max 中创建一个 iTVBoardGame 徽标,然后将其导出为 OBJ 文件,并使用冷(或者热)岩石纹理进行渲染。然后,我们将有一个专业的 Java 9 游戏 3D 闪屏标题,供你在本章稍后完善你的 UI 设计时使用。

创建闪屏素材:在 2D 管道中使用 3D 素材

正如你在图 9-5 中看到的,我已经使用 2D 文本工具创建了一个“快速和肮脏”的 iTVBoardGame 3D 徽标,然后使用斜角修改器挤压它,正如你在截图右上角的 Autodesk 3D Studio Max 版本的场景图形层次(在 3D Studio Max 中称为修改器列表)中看到的。我后来使用文件➤输出功能输出一个波前。OBJ 3D 文件格式,是 JavaFX 9 支持的几种 3D 文件导入格式之一。根据我们需要导入的 3D 数据类型,我们可能会使用这种格式或其他格式,因为每种格式都支持不同类型的 3D 数据和功能,如纹理贴图、UVW 贴图、反向运动学(IK)骨骼动画数据、网格变形、动画、相机数据、照明数据等。JavaFX 能够导入相当多的高级 3D 格式,如 Collada (DAE)、FrameBox (FBX)、3D Studio 3DS 和 OBJ。

A336284_1_En_9_Fig5_HTML.jpg

图 9-5。

I created an iTVBoardGame logo with 3D Studio Max, exported it to an OBJ file format, and rendered it

我将把这个网格数据导入渲染引擎,添加一个胡桃木纹理贴图,渲染它,然后把 2D 像素数据导出为 2D 图像素材。我将确保它有一个 alpha 通道,所以它看起来仍然像是一个 3D 对象,即使事实上它不是。这就是业内所说的 2.5D。

如果我们以后想要旋转它等等,当我们了解了 JavaFX 中 i3D 内容生产管道的更多信息时,我们可以在本书的后面将其作为 3D 素材导入。JavaFX 提供的混合 2D+3D 环境(API)的优势之一是能够决定什么 3D 是“幻觉”(像 2.5D,或立体),什么 3D 是“真实”的 i3D。立体 3D(主要是电影)不是真正的 3D,因为你不能走在场景和所有角色的后面。在一个 i3D 游戏中,比如《光环》或《疯狂足球》,你可以,因为它是一个完全虚拟的现实。

接下来,让我们来看一些我为在 ImageView 对象中使用而创建的 UI 屏幕标题数字图像对象,我将向您展示如何将它们添加到 NetBeansProject 文件夹层次结构中的适当文件夹中。NetBeans 9 可以“看到”这些 PNG32 数字图像素材后,您就可以编写 loadImageAssets()方法,该方法会将 PNG32 数据加载到系统内存中的图像对象中,以便 ImageView 可以引用和显示它们。

将图像资源添加到项目中:使用\src\文件夹

正如您在图 9-6 的顶部所看到的,我的 Windows 7 64 位四核 AMD 工作站上的路径从 Users 文件夹开始,看起来像 C:\ Users \ Walls \ my documents \ netbeans projects \ Java FX game \ src \ credits . png。正如您所看到的,我根据文件内部的内容来命名 PNG32 文件,尽管它们看起来像是在白色背景上,但实际上它们是透明的。将图书素材存储库中的文件复制到您的项目文件夹中,然后您将能够在您的代码中引用它们。

A336284_1_En_9_Fig6_HTML.jpg

图 9-6。

Copy the PNG32 files for the digital image titles for the UI screens to /NetBeansProjects/JavaFXGame/src/

一种加载图像资源的方法:。loadImageAssets()

打开空的 loadImageAssets()方法结构,添加五个 Image()构造函数方法,用正确的图像素材及其规范实例化和加载图像对象,如图 9-7 中突出显示的。

A336284_1_En_9_Fig7_HTML.jpg

图 9-7。

Instantiate and reference your five Image objects inside of the loadImageAssets() method you just created

如图 9-7 所示的 Java 代码应该是这样的:

splashScreen = new Image("/welcome.png", 1280, 640, true, false, true);
helpLayer = new Image("/instructions.png", 1280, 640, true, false, true);
legalLayer = new Image("/copyrights.png", 1280, 640, true, false, true);
creditLayer = new Image("/credits.png", 1280, 640, true, false, true);
scoreLayer = new Image("/highscores.png", 1280, 640, true, false, true);

这个构造函数方法格式所做的是在 JAR 文件中加载带有数字图像素材的图像,使用“根”或正斜杠字符引用,因为该文件位于/src/文件夹中。第二个和第三个条目代表图像的 X 和 Y 分辨率,第四个 true 条目打开纵横比锁定。第五个假输入关闭双线性插值,第六个真输入打开背景图像加载,作为速度优化。

一种创建文本资源的方法:。createTextAssets()

打开空的 createTextAssets()方法结构,添加八个 Text()构造函数方法,用正确的信息实例化和加载文本对象。如图 9-8 所示的代码应该是这样的:

A336284_1_En_9_Fig8_HTML.jpg

图 9-8。

Declare eight Text objects at top of class; instantiate and load Text objects in the createTextAssets() method

playText = new Text("Press the Start Game Button to Start! \n");
moreText = new Text("Use other buttons for instructions, copyrights, credits and high scores.");
helpText = new Text("To play game roll dice, advance gamepiece, follow gameboard instruction.");
cardText = new Text("If you land on square that requires card draw it will appear in UI area.");
copyText = new Text("Copyright 2015 Wallace Jackson, All Rights Reserved. \n");
riteText = new Text("Visit the iTVboardGame.com website on the Internet: www.iTVboardgame.com");
credText = new Text("Digital Imaging, 3D Modeling, 3D Texture Mapping, by Wallace Jackson. \n");
codeText = new Text("Game Design, User Interface Design, Java Programming by Wallace Jackson.");

接下来,您可能希望使按钮控件的宽度一致,这将通过使用 Button.setMaxWidth()方法来实现。当您升级您的场景对象构造器以支持 iTV 1280x720 时,您将能够看到 TOP_RIGHT Pos 常量的运行,并且统一的按钮块将看起来更专业。

使用 Button.setMaxWidth()方法:使按钮一致

您需要做的第一件事是将场景场景对象的 iTV 宽度设置为 1280,高度设置为 640,这样您就可以使用 2:1 的宽高比,并且具有 pro Java 9 游戏应用支持的最低 iTV 屏幕分辨率。正如你在图 9-9 中看到的,我在下面的 Java 代码中升级了 Scene()构造函数方法来使用这些新的应用窗口屏幕尺寸,如图 9-9 顶部的第 83 行所示:

A336284_1_En_9_Fig9_HTML.jpg

图 9-9。

Use the .setMaxWidth() method call to set your Button UI objects to 125 so that they have a uniform width

scene = new Scene(root, 1280, 640);

接下来要做的事情是设置 boardgameb 背板 ImageView 来包含欢迎消息图像。您将使用闪屏图像的. setImage()方法来实现这一点,在图 9-9 的第 89 行使用以下代码:

boardGameBackPlate.setImage(splashScreen);

最后,为了使按钮对象的宽度统一为 125 像素,使用.setMaxWidth(125)方法调用,调用五个按钮 UI 对象中的每一个(如图 9-9 ,代码行 97、100、103、106 和 109 所示)。

我将 VBox 配置为以 10 个像素间隔其子节点,在 VBox(10)构造函数方法调用中放置值 10。我将 Insets()的间距值增加到 Insets(16)。运行项目以查看变更,如图 9-10 所示。

A336284_1_En_9_Fig10_HTML.jpg

图 9-10。

Use the Run ➤ Project work processing to see your Button bank design improvements in spacing and width

接下来,将 backplate8.png 和 alphalogo.png 图像素材复制到源文件夹,如图 9-11 所示。

A336284_1_En_9_Fig11_HTML.jpg

图 9-11。

Use your file management utility to copy the backplate8.png and alphalogo.png assets to your /src folder

接下来,让我们使用 StackPane 的背景图像功能。目前我们使用的是 Background 类中的空常量,所以让我们用 BackgroundImage 对象替换它,您的背景类也将支持它。接下来让我们看看如何连接它,以便我们可以通过使用一个未使用的功能(StackPane 背景)而不是通过添加另一个 ImageView 对象来进一步优化您的场景图,大多数人都倾向于这样做。

使用 StackPane 背景:利用所有合成层

JavaFX StackPane 类支持. setBackground(背景背景)方法调用,该方法调用又支持 BackgroundImage 对象,该对象可以用 Image 对象加载或与 EMPTY 常量一起使用。这意味着您可以在 StackPane UI 布局容器对象的背景中引用图像资源,所以让我们来看看如何利用这一点,这样您就可以使用五个合成层(舞台背景、场景背景、ImageView、TextFlow、StackPane ),只需使用您添加到场景图中的节点。所有这些当前都设置为空或透明,或者包含带有 alpha 的 PNG32。使用以下代码,将 backPlate 和 alphaLogo 对象名称添加到类顶部的现有图像声明复合 Java 语句中:

Image splashScreen, helpLayer, legalLayer, creditLayer, scoreLayer, backPlate, alphaLogo;

接下来,在类的顶部声明一个名为 uiBackgroundImage 的 BackgroundImage 对象,并使用 Alt+Enter 让 NetBeans 9 为您编写导入语句。接下来,添加一个名为 uiBackground 的背景对象(注意,在第六章中已经导入了背景类,所以您可以利用背景。空常量),如下面的 Java 代码所示,以及图 9-12 中突出显示的:

A336284_1_En_9_Fig12_HTML.jpg

图 9-12。

Add a backgroundImage object named uiBackgroundImage; use the .setBackground() method to load it

BackgroundImage uiBackgroundImage;  // Object Declaration at the top of the JavaFXGame class
Background uiBackground;           //  Object Declaration at the top of the JavaFXGame class

在 loadImageAssets()方法的开头,实例化 backPlate 和 alphaLogo 图像对象,然后使用以下 Java 代码加载它们及其相关的数字图像素材,如图 9-12 所示:

backPlate = new Image("/backplate8.png", 1280, 640, true, false, true);
alphaLogo = new Image("/alphalogo.png",  1200, 132, true, false, true);

在这个 loadImageAssets()方法的最后,实例化您的 uiBackgroundImage 对象并使用 backplate Image 对象加载它,我们将使用它作为优化的 8 位 PNG8 背景图像,用于我们使用 Node 子类(StackPane、ImageView 和 VBox)创建的复合闪屏图像;你可以使用下面的 Java 代码来实现,如图 9-12 所示:

uiBackgroundImage = new BackgroundImage(backPlate,
                                         BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT,
                                        BackgroundPosition.CENTER, BackgroundSize.DEFAULT);

最后,您需要实例化 uiBackground 背景对象,并使用其构造函数方法,用您在前面的 Java 代码行中刚刚创建的 uiBackgroundImage 背景图像对象来加载它。这将使用下面的代码行来完成,在图 9-12 中的 loadImageAssets()方法中突出显示:

uiBackground = new Background(uiBackgroundImage);

在 createBoardGameNodes()方法中,调用。setBackground()方法,并传递一个 uiBackground 背景对象,替换背景。空常数,使用图 9-12 中的代码:

uiLayout.setBackground(uiBackground);

使用“运行➤项目”查看背板图像是否在您的 StackPane 的背景中,如图 9-13 所示。

A336284_1_En_9_Fig13_HTML.jpg

图 9-13。

Use the Run ➤ Run Project (JavaFXGame) menu sequence to test your new compositing pipeline Java code

现在我们准备使用 textFlow 和 Text 对象将文本层添加到合成管道中。

使用 TextFlow:设置信息叠加对象

打开 createTextAssets()方法,为 playText 和 moreText 对象添加两个方法调用,确保它们使用颜色。白色常量来填充字体,并选择一种广泛支持的 Helvetica 字体,使用其常规字体,并为字体高度设置一个大的 50 像素。使用 FontPosture 辅助类(font face 常量)将 playText 和 moreText 对象设置为使用常规字体样式。在 moreText 对象内添加转义换行符或\n 字符序列,将其分成两行。图 9-14 中间突出显示的新文本对象配置应类似于以下 Java 代码:

A336284_1_En_9_Fig14_HTML.jpg

图 9-14。

Add .setFill() and .setFont() methods to your SplashScreen text

playText = new Text("Press the Start Game Button to Start! \n");
playText.setFill(Color.WHITE);
playText.setFont(Font.font("Helvetica", FontPosture.REGULAR, 50));
moreText = new Text("Use other buttons for instructions, \n copyrights, credits and scores.");
moreText.setFill(Color.WHITE);
moreText.setFont(Font.font("Helvetica", FontPosture.REGULAR, 50));

打开 createBoardGameNodes()方法,在 infoOverlay 对象实例化之后,添加一行代码并调用。使用 infoOverlay 对象的值 240 设置 Translate X()方法。然后添加另一行代码并调用。值为 420 的 setTranslateY()方法。这将把 TextFlow 容器放置在 ImageView 对象(当前是一条欢迎消息)的下面,这样您的合成文本块将位于屏幕的底部。这些语句的 Java 代码应该如下所示(在图 9-15 中突出显示):

A336284_1_En_9_Fig15_HTML.jpg

图 9-15。

Add Text objects to your TextFlow object and .setTranslateX() and .setTranslateY()

infoOverlay.setTranslateX(240);
infoOverlay.setTranslateY(420);

打开 addNodesToStackPane()方法。在该方法的末尾,添加一个 infoOverlay 对象并调用。getChildren()。addAll()方法,用逗号分隔 playText 和 moreText 对象,如图 9-16 所示。

A336284_1_En_9_Fig16_HTML.jpg

图 9-16。

Add infoOverlay object to end of .addNodesToSceneGraph() and use .addAll() to add playText and moreText objects to infoOverlay

该语句的 Java 代码应如下所示,并在图 9-16 中突出显示:

infoOverlay.getChildren().addAll(playText, moreText);

正如你在图 9-17 中看到的,白色的文本对象看起来很酷,你的图像合成管道看起来好像是用专业的数字成像软件创建的。接下来,让我们添加一个徽标的图像合成层。

A336284_1_En_9_Fig17_HTML.jpg

图 9-17。

Run the project and check the result of adding a TextFlow object to the splash screen compositing pipeline

这个 UI 设计看起来越来越专业,按钮放在岩石露头上,还有“欢迎!”文本在屏幕设计中居中。但是,UI 设计仍然需要一个品牌标志,并且有一个额外的空间将第三行推到右边,需要固定(删除)。让我们在下一节做这件事。

使用 StackPane:添加更多数字图像合成层

让我们在 JavaFXGame 类的顶部添加另一个名为 logoLayer 的 ImageView 对象声明,将 ImageView 声明变成一个ImageView boardGameBackPlate, logoLayer;复合 Java 语句。打开 createBoardGameNodes()方法,为该对象添加一个对象实例化,然后调用. setImage()方法将它连接到您之前在导入该数字图像素材时创建的 alphaLogo 图像对象。接下来,您将添加 logoLayer 对象的两个方法调用,一个用于 X 缩放,一个用于 Y 缩放,使用相同的 80%值 0.8,以便我们锁定此缩放操作的纵横比(也称为统一缩放)。最后,您将使用-225 值将徽标从屏幕中心沿 y 轴上移 225 个像素,因为 StackPane 使用 0,0 中心屏幕引用模型,而不是标准的 0,0 左上角像素引用模型。我们还将使用-75 像素的 X 平移值将徽标向左拉回 75 像素。新 logoLayer ImageView 对象实例化、素材引用和转换(位置/转换和比例)配置代码可以在图 9-18 的中间高亮显示,应该类似于下面的 Java 9 代码语句块:

A336284_1_En_9_Fig18_HTML.jpg

图 9-18。

Create a logoLayer ImageView referencing the alphaLogo image; set scale to 80 percent and position to -75,-225

logoLayer = new ImageView();
logoLayer.setImage(alphaLogo);
logoLayer.setScaleX(0.8);
logoLayer.setScaleY(0.8);
logoLayer.setTranslateX(-75);
logoLayer.setTranslateY(-225);

接下来,您必须在 addNodesToSceneGraph()方法中将这个新的 logoLayer ImageView 添加到合成层的 StackPane uiLayout 容器中。在这里,由于我们正在向 root 和 uiLayout SceneGraph 层次结构添加多个节点子类,我们将从使用。getChildren()。使用. getChildren()将()方法链添加到。addAll()方法链,将该方法中的 Java 语句数量从八个减少到四个。

添加的顺序会影响合成层的顺序,因此对于。add()语句,这等同于从上到下。添加的第一个语句(顶部)在合成层堆栈的底部(有点违反直觉,不是吗?).

与。addAll()方法,这将变为从左到右,因此首先添加的对象(左)位于合成层堆栈的底部。一个新的 addNodesToStackPane()方法结构,使用。getChildren()。因此,addAll()方法调用看起来如下,如图 9-19 底部突出显示的:

A336284_1_En_9_Fig19_HTML.jpg

图 9-19。

Consolidate six .add() method calls into two .addAll() method calls off of a SceneGraph root and UI branch

private void addNodesToSCeneGraph() {
    root.getChildren().addAll(gameBoard, uiLayout);
    uiLayout.getChildren().addAll(boardGameBackPlate, logoLayer, infoOverlay, uiContainer);
    uiContainer.getChildren().addAll(gameButton, helpButton, legalButton,
                                     creditButton, scoreButton);
    infoOverlay.getChildren().addAll(playText, moreText);
}

正如您在图 9-20 中看到的,我已经添加了徽标并修复了文本段落(左侧)对齐方式,删除了\n 后面的空格,这是违反直觉的,因为它留下了\ n 版权并且没有转义版权。作为一名 Java 程序员,您需要知道在这种情况下,编译器会查看转义符(),并且只查看其后的一个字母(在本例中是 n 或 newline),然后继续将字符作为文本内容的一部分进行解析。

A336284_1_En_9_Fig20_HTML.jpg

图 9-20。

Run the project and check the result of adding ImageView object to the splash screen compositing pipeline

徽标已经添加到合成层容器(StackPane)中,调整了大小(缩放)以适合按钮库,并上移(平移)到按钮库的中心。这一切看起来很平衡和专业;它在场景图中使用很少的节点,在系统内存中使用很少的对象,所以它是优化的。

由于我们没有使用我在第七章中向您展示的透明性(技巧),让我们通过恢复到默认的修饰 StageStyle 类常量来替换 OS chrome,这可以通过删除 primaryStage.initStyle()方法调用来实现。相反,我将保留 Java 语句,并将使用透明常量改为使用修饰常量,以防将来我们希望以不同的方式修饰 Stage 对象。这可以通过将图 9-21 中第 46 行所示的这行代码改为以下 Java 代码来实现:

A336284_1_En_9_Fig21_HTML.jpg

图 9-21。

Revert to StageStyle.DECORATED and add an iTVBoardGame (JavaFX 9 Game) title for your OS window

primaryStage.initStyle(StageStyle.DECORATED);

接下来,通过替换在第六章中为您创建的引导代码中的“Hello World”占位符文本,为您的操作系统窗口 chrome 添加标题。我将使用 iTVBoardGame 来匹配 3D 徽标,并使用括号添加“JavaFX 9 Game ”,以向用户阐明该应用是在什么平台上构建的。执行此操作的代码在上方的图 9-21 中以橙色显示。initStyle()方法,应该类似于下面的 Java 语句:

primaryStage.setTitle("iTVBoardGame (JavaFX 9 Game)");

正如你在图 9-22 中看到的,我们现在有了一个初始的应用启动闪屏,带有徽标、背景图像、用户界面按钮库和部分标题图像层。在闪屏的情况下,这是“欢迎!”

A336284_1_En_9_Fig22_HTML.jpg

图 9-22。

Run the project to make sure the OS chrome has been replaced and the window title is in place and correct

我们剩下要做的唯一一件事是完成其他四个部分的图像对象的实现,并在其他四个 TextFlow 对象中实现字体样式和颜色。所有这些对象都将在您的事件处理代码中被调用,我们将在第十章中添加这些代码(涵盖 JavaFX 9 和 Java 9 游戏中的事件处理和效果)。

在所有这些 2D 屏幕设计、UI 设计和事件处理编码完成后,我们可以在本书的后半部分开始进入 3D 和 i3D。

完成 UI 设计对象的创建和配置

让我们打开 createTextAssets()方法并添加。setFill()和。setFont()方法调用其他六个文本对象,将它们的颜色设置为与保存手写文本图像的 boardGameBackPlate ImageView 相匹配,并将它们的字体样式设置为 Helvetica 常规。这是一个相对简单的练习;得到的方法体如图 9-23 所示,应该类似于下面的 Java 方法体和 Java 语句:

A336284_1_En_9_Fig23_HTML.jpg

图 9-23。

Finish configuring your Text objects using .setFill() and .setFont() methods with Color and Helvetica values

private void createTextAssets(){
    playText = new Text("Press the PLAY GAME Button to Start!\n");
    playText.setFill(Color.WHITE);
    playText.setFont(Font.font("Helvetica", FontPosture.REGULAR, 50));
    moreText = new Text("Use other buttons for instructions,\ncopyrights, credits and scores.");
    moreText.setFill(Color.WHITE);
    moreText.setFont(Font.font("Helvetica", FontPosture.ITALIC, 50));
    helpText = new Text("To play game roll the dice, advance\ngame piece and
                         follow game board\ninstructions. ");
    helpText.setFill(Color.GREEN);
    helpText.setFont(Font.font("Helvetica", FontPosture.REGULAR, 50));
    cardText = new Text("If you land on a square\nthat requires you draw a card, it
                         will\nappear in the floating UI text area.");
    cardText.setFill(Color.GREEN);
    cardText.setFont(Font.font("Helvetica", FontPosture.REGULAR, 50));
    copyText = new Text("Copyright 2015 Wallace Jackson.\nAll Rights Reserved.\n");
    copyText.setFill(Color.PURPLE);
    copyText.setFont(Font.font("Helvetica", FontPosture.REGULAR, 50));
    riteText = new Text("Visit the iTVboardGame.com website on\nthe Internet
                         at www.iTVboardgame.com");
    riteText.setFill(Color.PURPLE);
    riteText.setFont(Font.font("Helvetica", FontPosture.REGULAR, 50));
    credText = new Text("Digital Imaging, 3D Modeling, 3D\nTexture Mapping
                         by Wallace Jackson.\n");
    credText.setFill(Color.BLUE);
    credText.setFont(Font.font("Helvetica", FontPosture.REGULAR, 50));
    codeText = new Text("Game Design, User Interface Design,\nJava Programming
                         by Wallace Jackson.");
    codeText.setFill(Color.BLUE);
    codeText.setFont(Font.font("Helvetica", FontPosture.REGULAR, 50));
}

在本章中,你已经在整合你的专业 Java 游戏闪屏设计、用户界面设计、顶级场景图和类(和方法)基础设施方面取得了很大的进步。您还了解了 Image、ImageView、Text、TextFlow、Background 和 BackgroundImage 类。拍拍自己的背,如果你有那么灵活的话,然后从 Java 9 编程中稍微休息一下!

您还没有完成 pro Java 9 游戏开发的用户界面设计部分。在本书的后面,当我们使它成为交互式的(第十章)和当我们通过添加一个透视相机到场景根(第十一章)将你现在的 2D 场景转换成 3D 场景时,使它成为一个 2D 平面时,准备好进一步完善它,所有这些都需要改变 StackPane 对象合成层管道属性和坐标参考系统。正如我所说的,这本书的每一章都会变得越来越复杂,直到你对 Java 9、JavaFX 和 NetBeans 9 的了解足以创建你能想象的任何 i3D 游戏设计!

摘要

在第九章中,您使用 JavaFX Image、ImageView、Background、BackgroundImage、Text 和 TextFlow 类向游戏的实际顶级用户界面设计的合成管道中添加了更多代码。你做的第一件事是完成 JavaFX 场景图层次结构设计,我在图 9-1 中可视化了它,展示了场景图如何使用一个根节点对象、三个分支节点对象和七个叶节点对象(五个按钮对象、一个 ImageView 对象和一个 TextFlow 对象)。稍后,您将为游戏的 3D 部分添加更多分支和叶节点对象,主要用于保存 JavaFX 提供的 3D 对象(基本体)或您自己的 3D 网格几何体。在本章中,您为徽标合成添加了第二个 ImageView 节点。

接下来,您了解了一些 JavaFX 类,我们将在本章和下一章中在这些新方法中实现这些类。其中包括 Image 类和 ImageView 类,都来自 javafx.scene.image 包。您还了解了 javafx.scene.text 包中的 Text 和 TextFlow 类。

您实例化并配置了这些新的合成节点对象,然后编写了两个新方法来处理您的图像和文本素材。loadImageAssets()方法将数字影像素材实例化并配置到图像对象中,createTextAssets()方法将文本信息素材实例化并配置到文本对象中,以便以后与场景图形中的 TextFlow 节点对象一起使用。

然后,您使用 125 像素的值使按钮宽度一致,并随后学习了如何使用 background 和 BackgroundImage 类(对象),以便能够利用 StackPane 的 Background 属性作为合成管道中的另一个合成层,而无需向 JavaFX 场景图形层次添加任何更多的节点对象。这加强了我对 pro Java 游戏开发的优化方法,即利用场景图中每个节点给你的一切,这样你就可以将每个脉冲事件中遍历的节点总数保持在绝对最低限度。

在下一章中,您将学习 JavaFX 中的事件处理类,并在 ActionEvent EventHandler 程序逻辑中实现 Java 代码。您还将学习如何在这个过程中实现一些很酷的特殊效果,因为其中一些将由 Java 9 语句在您的事件处理程序中触发。在那之后,我们将进入 3D 和你需要知道的进入 3D 领域的职业和素材。

十、用户界面设计交互性:事件处理和图像效果

现在,您已经完成了闪屏和用户界面设计的场景图层次结构,让我们回到第十章中的 JavaFXGame 主应用类编码,并完成您已经准备好但实际上是“空的”事件处理框架的实现(除了几个 System.out.println 调用来测试您的按钮控件节点对象)。我们将在本章中概述的 Java 和 JavaFX 事件处理将实现用户界面,玩家将使用该界面来了解和启动您的 Java 9 游戏。在本书中,你将使用其他类型的事件处理(击键和鼠标),我们将在本章中讨论。您将添加 Java 游戏 UI 编程逻辑,它可以被视为游戏的交互引擎。有许多与游戏交互的方式,包括箭头键,被称为消费电子设备和现代遥控器的 pad 一个键盘;一只老鼠;轨迹球;游戏控制器;触摸屏;甚至是高级硬件,包括陀螺仪和加速度计。您将为您的 pro Java 9 游戏开发做出的一个重要选择是,您的玩家将如何使用他们玩游戏的硬件设备和游戏支持的硬件输入功能与您的 Java 游戏进行交互。

在本章中,您将学习不同类型的 JavaFX 事件类型,它们包含在javafx.event、javafx.scene.input 和 java.util 包中。您将覆盖 ActionEvent,因为您当前在您的用户界面设计中使用它,以及输入事件,如 MouseEvent 和 KeyEvent。

除了通过添加事件处理继续在 JavaFXGame Java 代码上工作之外,您将在本章中学习 JavaFX 特效,以确保我在本书中涵盖了 Java 中所有很酷的东西。这些 JavaFX 特效存储在 javafx.scene.effect 包中,为 javafx 和 Java 提供了与 GIMP 等数字图像合成软件包相同的特效优势。

事件处理:增加游戏的互动性

有人会说事件处理是游戏开发的基础。这是因为如果你没有一种方法来与游戏逻辑和游戏元素进行交互,你就真的没有一款游戏。在本章的这一节,我将介绍 JavaFX 事件处理类,您将实现 ActionEvent 处理结构,以便您的用户可以利用您在过去几章中设计的用户界面。在我们开始剖析 Java 和 JavaFX 包、类、接口和方法之前,我首先要说的是可以为 pro Java 游戏处理的不同类型的输入硬件事件。这些可以使用 iTV 遥控器或智能手机 DPAD 上的箭头键、键盘、鼠标或轨迹球以及智能手机、平板电脑或 iTV 电视机上的触摸屏来生成。还有自定义输入硬件,包括游戏机上的游戏控制器和现在的 iTV 电视机,智能手机和平板电脑中的陀螺仪和加速度计,以及自由形式的手势和动作控制器,如 Leap Motion,VR Glove 和 Razer Hydra Portal。

控制器的类型:我们应该处理什么类型的事件?

要考虑的一个关键问题是,支持游戏相关事件的最合理方法是什么,如箭头键、鼠标点击、触摸屏事件、游戏控制器按钮(A、B、C 和 D)以及更高级的控制器,如 Android、Kindle、Tizen、HTML5 OS 和 iOS 消费电子设备上可用的陀螺仪和加速度计。这个决定是由游戏运行的硬件设备决定的;如果一个游戏需要在任何地方运行,那么最终将需要处理不同事件类型的代码,甚至是不同的事件处理编程方法。在本章的这一节中,我们将进一步了解 Java 和 JavaFX 目前支持哪些输入事件,以便为您的游戏开发提供一个概览。

有趣的是,Java 和 JavaFX 应用已经可以在两个流行的嵌入式平台上运行,Android 和 iOS,我会在不久的将来的某个时候在开源平台(Opera,Tizen,Chrome,Ubuntu 和 Firefox)和当前支持 Java 8 或 9 技术的专有平台(Windows,Samsung Bada,RIM Blackberry,LG WebOS,OpenSolaris)上投入资金。Java 9 的未来是光明的,这要归功于 JavaFX、Java 平台几十年来的发展势头以及新的高级 i3D 硬件平台支持!

Java 和 JavaFX 事件包:java.util 和 javafx.event

正如您在事件处理结构的新 EventHandler 声明中看到的,javafx.event 包的 EventHandler 公共接口扩展了 java.util 包的 EventListener 接口,是创建和处理事件对象的方式,或者使用匿名内部类(Java 7)结构(我们正在使用它,因为它与 Android 兼容),或者使用 lambda 表达式(Java 8)。现在,您已经熟悉了如何编写这种类型的事件处理结构,在本书中,我将继续使用 Java 匿名内部类方法编写方法。也就是说,您可以将鼠标悬停在任何 Java 7 代码下的黄色波浪下划线高亮处,并让 NetBeans 9 将其转换为使用更简化的 Java 8 lambda 表达式。通过这种方式,您可以创建兼容 Java 7 (64 位 Android 5 和 6)、Java 8 (64 位 Android 7 和 8)和 Java 9(PC OS 和未来版本的 Android)游戏代码交付管道的游戏。在本节中,我们将查看 ActionEvent 和 InputEvent EventObject 子类类别,以便您了解 JavaFX 中的主要事件。这些来自 java.util.EventObject 超类,我们将看看它们如何应用于处理动作、击键、鼠标事件、触摸事件和类似的高级输入事件类型。

JavaFX ActionEvent 类:从 java.util 创建。EventObject 超类

到目前为止,您在本书中用于用户界面按钮控件事件处理的 ActionEvent 类(和对象)是 javafx.event 包的 Event 超类的子类,该超类本身是 java.util 包的 EventObject 超类的子类,该超类是 java.lang.Object 主类的子类。这个类还有一个已知的直接子类 MediaMarkerEvent 类。因此,类层次结构如下所示:

java.lang.Object
  > java.util.EventObject

    > javafx.event.Event
      > javafx.event.ActionEvent

ActionEvent 类与 EventHandler 公共接口一起包含在 javafx.event 包中。正如您可能已经猜到的,ActionEvent 对象是一个表示某种类型的动作的事件对象。这种类型的事件对象可以用来表示各种各样的事物。正如你所看到的,当一个按钮被触发时使用它,例如,当一个关键帧结束播放时,以及在其他类似的内部软件使用中也使用它。ActionEvent 是在 JavaFX 1.x 版本中引入的,在 JavaFX 1.x 版本(1.0 到 1.3)中不可用。它保留在 JavaFX 7 for Java 7 中(两者现在都已停止使用),保留在 JavaFX 8 for Java 8 中,现在保留在 JavaFX 9 for Java 9 中。

ActionEvent 对象有两个数据字段(属性)。第一个是静态 EventType 动作特征,这是 ActionEvent 的唯一有效 EventType。然而,还有一个 ActionEvent 对象的超类型,它采用静态 EventType ANY 的形式,为开发人员提供了一个能够表示所有动作事件类型的通用超类型。因此,如果希望 Java 代码处理任何 ActionEvent 对象,请使用此数据字段;如果希望 Java 代码处理特定的 ActionEvent 对象,请使用 ACTION 数据字段。

此 ActionEvent 类还支持两个构造函数方法。默认的空参数列表 ActionEvent()构造函数方法使用默认的事件类型 ACTION 创建一个新的 ActionEvent 对象。还有一个 ActionEvent(Object source,EventTarget)构造函数方法,它将使用指定的事件对象源和 EventTarget 目标创建新的 ActionEvent。

此 ActionEvent 类还支持两种方法。第一个是 ActionEvent copy for(Object new source,EventTarget newTarget)方法,用于使用指定的事件源和目标创建并返回事件的副本。第二个是 EventType extends ActionEvent> getEventType()方法,该方法将获取调用它的事件对象的事件类型。

我们将用于游戏的 i3D 组件的所有其他事件相关类都包含在 javafx.scene.input 包中。在本节的剩余部分,我将重点关注 javafx.scene.input 包,因为您已经学会了如何为 Java 7 编写新的 EventHandler { … }结构。如果您指示 NetBeans 9 将此转换为 Lambda 表达式,它将采用 Java 8 的(ActionEvent) -> { … }代码结构格式。

现在是时候学习如何在 Java 游戏开发工作流程中使用其他类型的事件,称为输入事件。让我们看看 javafx.scene.input 包及其 25 个输入事件相关的类。

JavaFX 输入事件类:javafx.scene.input 包

尽管 java.util 和 javafx.event 包包含核心的 eventObject、Event 和 EventHandler 类来“处理”您的事件,但在确保事件得到处理(处理)的基础级别上,还有另一个名为 javafx.scene.input 的 javafx 包,它包含您有兴趣用来处理(处理)您可能正在创建的不同类型游戏的玩家输入的类。这些事件被称为输入事件,它们不同于动作事件和脉冲事件,您已经了解了这些事件。

有趣的是,javafx.scene.input 包中支持的许多输入事件类型更适合智能手机和平板电脑等消费电子(行业术语是嵌入式)设备。这告诉我,JavaFX 正在被定位(设计)用于开源平台,如 Android OS、Firefox OS、Tizen OS、Bada OS、Opera OS、Ubuntu OS 或 Chrome OS。JavaFX 9 具有“专门化”事件,如 GestureEvent、SwipeEvent、TouchEvent 和 ZoomEvent,它们支持新的嵌入式设备市场中的特定功能。这些输入事件类支持高级触摸屏设备功能,如手势、页面滑动、触摸屏输入处理和多点触摸显示功能,如双指“捏”或“展开”触摸输入,例如分别用于放大和缩小屏幕上的内容。

我们将在本书中涵盖更多“通用”输入类型,这些类型在个人电脑(台式机、笔记本电脑、笔记本电脑、上网本和较新的“专业”平板电脑,如 Surface Pro 4)和嵌入式设备(包括智能手机、平板电脑、电子阅读器、iTV 电视机、游戏控制台、家庭媒体中心、机顶盒等)上都受支持。这些设备还将处理这些更广泛的(在它们的实现中)按键事件和鼠标事件类型的输入事件,因为鼠标事件和按键事件对于传统软件包总是受支持的。例如,触摸屏支持鼠标点击事件,但是定位设备(鼠标、轨迹球、控制器、DPAD 等)不支持触摸屏事件。).所以如果可以的话,用键盘鼠标事件!

有趣的是,触摸屏显示器将“处理”鼠标事件以及触摸事件,这对于确保您的游戏能够在尽可能多的不同平台上运行来说非常方便。我经常在我的 Android 书籍中使用这种使用鼠标事件处理的方法,以便用户可以使用触摸屏和 DPAD 中心(点击)按钮来生成鼠标点击事件,而不必专门使用触摸事件。对于触摸屏用户来说,尽可能使用鼠标(点击)事件的另一个好处是,如果您使用触摸事件,您将无法进行其他操作。也就是说,您的游戏应用只能在触摸屏设备上运行,而不能在具有某种鼠标硬件的设备(如 iTV、笔记本电脑、台式机、上网本等)上运行。

同样的原则也适用于按键事件,尤其是开发人员在游戏中使用的箭头键,因为这些键可以在键盘和遥控器的箭头小键盘上、游戏控制器上以及大多数智能手机的 DPAD 上找到。我还将向您展示如何包含备用键映射,以便您的玩家可以决定他们更喜欢使用哪种输入法来玩您的 pro Java 9 游戏。接下来让我们看看 KeyCode 和 KeyEvent 类。

KeyCode 类:使用枚举常量来定义玩家在游戏中使用的键

由于许多游戏使用箭头小键盘进行导航(通常是 A、S、D 和 W 键),并且有时使用这些键到游戏控制器的 GAME_A、GAME_B、GAME_C 和 GAME_D 按钮的替代映射,所以让我们先仔细看看 JavaFX KeyCode 类。此类是一个公共枚举类,它保存当按下或释放某个键时计算的键的枚举常数值。这个类是 KeyEvent 类获取 keycode 常量值的地方,keycode 常量值用于(处理)确定播放器在任何特定的键事件调用中使用了哪个键。KeyCode 类的 Java 和 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 游戏编程逻辑来读取和操作这些布尔值,这些游戏引擎类将实时处理这些关键事件,以便您的 pro Java 游戏运行良好,并且您的用户体验良好。接下来,让我们看看处理这些 KeyCode 对象的 KeyEvent 对象。

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 和 JavaFX 类层次结构从 java.lang 包跳转到 java.util 包、javafx.event 包、javafx.scene.input 包。键事件层次结构如下所示:

java.lang.Object
  > java.util.EventObject
    > javafx.event.Event
      > javafx.scene.input.InputEvent
        > javafx.scene.input.KeyEvent

EventHandler 对象生成的 KeyEvent 对象表示发生了击键。此键事件通常是使用场景图形节点对象之一生成的,如可编辑文本 UI 控件;但是,在您的游戏中,您可能要将场景图形节点对象层次级别之上的关键事件处理直接附加到名为 Scene 的场景对象。这将通过不向场景图中的任何节点对象附加任何关键事件处理来最小化场景图脉冲处理开销。在您的游戏中,这是包含 uiLayout StackPane 对象和 gameBoard 组对象的根组对象。

每当按下并按住、释放或键入(按下并立即释放)某个键时,就会生成一个 KeyEvent 对象。根据这个按键动作本身的性质,您的 KeyEvent 对象被传递到。onKeyPressed(),。onKeyTyped(),或者。onKeyReleased()方法,以便在. handle()方法中进行进一步处理。

游戏通常使用按键和按键释放事件,因为用户通常按住按键来移动游戏中的角色。另一方面,键入事件往往是“高级”事件,通常不依赖于操作系统平台或键盘布局。键入的按键事件(。onKeyTyped()方法调用)将在输入 Unicode 字符时生成。它们用于为 UI 控件(如文本字段)获取字符输入,并用于业务应用,如日历、计算器、电子邮件客户端和文字处理程序。

在一个简单的例子中,击键事件将通过使用一次击键及其立即释放来产生。此外,可以使用按键事件的组合来产生替代字符。例如,使用 Shift 键和“A”键类型(按下并立即释放)产生大写字母 A。

生成键类型的 KeyEvent 对象通常不需要按键释放。需要注意的是,在某些极端情况下,直到释放按键时才会生成按键类型的事件;这方面的一个很好的例子是使用老式的 Alt 键和数字键盘输入方法输入 ASCII 字符代码序列的过程,这种方法“过去”在 DOS 中使用,并保留到 Windows 操作系统中。值得注意的是,对于不生成 Unicode(可见、可打印)字符的计算机键盘键,如 Shift、Control (Ctrl)或 Alternate)键(通常称为修饰键),不会生成键类型的 KeyEvent 对象。

KeyEvent 类有一个字符变量(我倾向于称之为字符特征,但我不会这样做),对于键入键的事件,它总是包含一个有效的 Unicode 字符,对于按下键或释放键的事件,它总是包含 CHAR_UNDEFINED 字符。字符输入仅针对键入的事件进行报告,因为按键和按键释放事件不一定与字符输入相关联。因此,您的字符变量保证只对键类型的事件有意义。

对于按键和按键释放的 KeyEvent 对象,KeyEvent 类中的 code 变量将包含您的 KeyEvent 对象的 keycode,该 KeyCode 是使用您之前学习过的 KeyCode 类定义的。对于键入事件,这个代码变量总是包含常量 KeyCode.UNDEFINED。所以正如你所看到的,按键和按键释放被设计成不同于键入事件的用法,这就是为什么你可能会在游戏事件处理中使用按键和按键释放事件。按键和按键释放事件是低级事件,每当按下或释放给定的按键时都会生成。它们是“轮询”不产生字符输入的键的唯一方法。您的按键被按下或释放会使用 code 变量指示给操作系统,该变量包含一个虚拟键码。

添加键盘事件处理:使用 KeyEvents

我认为这些背景信息已经足够让你理解一个如何为 pro Java 游戏实现 KeyEvent 处理的基本例子了。这相当简单,所以我将向您简要介绍一下这里是如何完成的,因为我们正在讨论 KeyEvent 类。这在我的《Java 8 游戏开发入门》一书中也有涉及。您要做的第一件事是在 JavaFXGame 类的顶部添加一行代码,并使用一个复合声明语句声明四个布尔变量,分别命名为 up、down、left 和 right,如下所示:

boolean up, down, left, right;

由于布尔变量的默认值为 false,这将表示某个键未被按下,即当前释放或未使用的键。因为这也是键盘上按键的默认状态,所以这将是该应用的正确默认值。因为在 Java 中,布尔变量默认为 false,所以您不必显式初始化这些变量。

我通过使用。setOnKeyPressed()方法调用名为 Scene 的场景对象,我已经实例化了它。在这个方法调用中,我创建了新的 EventHandler ,就像对 ActionEvent 所做的那样。代码如下所示:

scene.setOnKeyPressed( new EventHandler<KeyEvent>() { a .handle() method will go in here } );

KeyEvent 对象处理恰好是实现高效 Java switch 语句的完美应用。您可以为想要处理的每个 JavaFX KeyCode 常量添加一个 case。它们包含在名为 Event 的 KeyEvent 对象中,该对象被传递到此。handle()方法通过它的参数区域。

然后,使用您的从 switch()评估区域内的 KeyEvent 对象中提取键码。事件 KeyEvent 对象的 getCode()方法调用。这可以通过使用下面的 Java 开关情况 Java 代码编程结构来完成:

scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
    @Override
    public void handle(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; // No break; statement is needed here, if there
                                             // are no more case statements to be added later
        }
    }
});

这将为您提供基本的按键事件处理结构,您可以将它添加到您的 pro Java 游戏中,并使用您的代码轮询这四个布尔变量,以找出您的用户正在使用他们的箭头键做什么。如果您指示 NetBeans 9 将这个 Java 7 结构转换成 Java 8 lambda 表达式,您将得到一个紧凑的结构。在 lambda expression Java 8 代码结构中,许多东西,如公共 void handle()声明和新的 EventHandler ()声明,将被隐含或假定(但仍存在于编译器中)。使用 lambda 表达式将简化代码,将三层嵌套的代码块减少到只有两层嵌套的代码块,并将 11 行代码减少到 8 行。Lambda 表达式对于编写更紧凑的代码来说确实很优雅,但是它并没有向您展示正在使用的类(对象)、修饰符和返回类型的所有情况。这就是为什么我选择使用更明确的 Java 7(和更早版本)代码结构的原因,在 Java 5 到 7 中使用,在 Java 8 中引入 Lambda 表达式之前使用。Java 9 支持这两种方法。

您生成的 Java 8 lambda 表达式代码结构将类似于以下 Java 代码结构:

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;
    }
});

接下来你要做的是创建 OnKeyPressed 结构的对立面,从而创建一个 OnKeyReleased 结构。这将使用相同的代码结构,只是真值会变成假值,并且。setOnKeyPressed()方法调用将改为. setOnKeyReleased()方法调用。最简单的方法是选择。setOnKeyPressed()结构,并将其复制粘贴到自身下面。Java 代码如下所示:

scene.setOnKeyReleased(new EventHandler<KeyEvent>() {
    @Override
    public void handle(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;
        }
    }
});

使用 lambda 表达式通过“隐式”声明和使用类(如本章实例中的 EventHandler 类)所做的一件有趣的事情是,它减少了类代码顶部的 import 语句的数量。这是因为,如果在代码中没有专门使用某个类(写下了它的名称),则该类的 import 语句不必与其他 import 语句一起放在代码的顶部。您还会注意到,如果您转换为 lambda 表达式,NetBeans 9 左边的代码折叠加号或减号图标将会消失。这是因为 lambda 表达式是一个基本的 Java 代码语句,而不是内部类或方法之类的构造或结构,在将其转换为 lambda 表达式之前是这样的。

既然你已经看了如何设置按键事件处理结构,让我们来看看添加一个备用按键映射到游戏中经常使用的 ASDW 按键是多么容易。这是通过为键盘上的 A、S、D 和 W 字符添加一些 case 语句,并将它们设置为我们已经设置好的 up、down、left 和 right 布尔等价物(UP、DOWN、LEFT 和 RIGHT 变量)来实现的。

例如,这将允许用户用左手使用 A 和 D 字符,用右手使用上下箭头来简化游戏。稍后,如果您想要使用游戏控制器及其对 KeyCode 类 GAME_A、GAME_B、GAME_C 和 GAME_D 常量的支持来为游戏添加更多功能,那么您将不得不为您的游戏添加这些新功能,只需为该类顶部的 up、down、left 和 right 变量添加另外四个布尔变量(A、B、C 和 D ),并添加另外四个 case 语句。

这四个 W(上)、S(下)、A(左)和 D(右)case 语句一旦添加到 switch 语句中,就会使您的 KeyEvent 对象及其事件处理 Java 代码看起来像下面 15 行 Java 代码:

scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
    @Override
    public void handle(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;
        }
    }
});

正如你所看到的,现在用户可以使用任意一组按键,或者同时使用两组按键来控制游戏。现在对。setOnKeyReleased()事件处理结构使用复制粘贴工作流程,并将值更改为 false。那个。setOnKeyReleased()事件处理 Java 代码如下所示:

scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
    @Override
    public void handle(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;
        }
    }
});

接下来,让我们切换回 Java 编码模式,为您的用户界面设计实现事件处理。

完成用户界面设计:编写事件处理代码

让我们通过在。handle()方法。此方法清除文本对象,然后将文本对象添加到 infoOverlay TextFlow 对象中,并使用。setBackground()和。setImage()方法调用。正如你在图 10-1 中看到的,我总是首先使用。getChildren()。clear()方法调用,然后我使用. getChildren.addAll()方法调用将正确的文本对象添加到 TextFlow 对象中。然后我使用。setTranslateX()和。setTranslateY()方法调用 infoOverlay TextFlow 来定位该容器(和层)。之后,我使用。setBackground()方法调用来设置 uiLayout VBox 对象的背景图像(用于闪屏)或背景。其他四个按钮对象为空,这允许颜色。要显示的白色背景色。最后,我使用。setImage()方法调用,使用正确的图像对象设置 boardgameb 背板 ImageView 对象。在游戏规则(帮助)按钮的情况下,这是一个帮助者图像对象引用。对于第一个 helpButton 事件处理程序,此。handle()方法代码体将包括以下 Java 语句:

A336284_1_En_10_Fig1_HTML.jpg

图 10-1。

Implement the code in the handle() method to reconfigure your helpButton UI compositing layer objects

helpButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(helpText, cardText);
        infoOverlay.setTranslateX(130);
        infoOverlay.setTranslateY(360);
        uiLayout.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(helpLayer);
    }
} );

现在,当您使用您的 Run ➤项目工作流程时,游戏规则按钮 UI 控件将触发用户界面设计,该设计已优化为向您的游戏玩家显示您的说明(帮助)屏幕,如图 10-2 所示。在本章中,我们将使用 Java 代码对此设计做进一步的设计“调整”。

A336284_1_En_10_Fig2_HTML.jpg

图 10-2。

Test your UI Button event handling with the Start Game and Game Rules buttons, switching back and forth

此代码通过使用 Background.EMPTY 在 StackPane 对象中安装透明度来配置合成堆栈(Stage ➤根➤ StackPane ➤ ImageView ➤ TextFlow)中的不同对象,以显示操作系统的默认白色背景,即场景(和 Stage)对象。boardGameBackPlate ImageView 包含一个透明指令脚本字体投影 PNG32 图像,它允许白色背景颜色通过。TextFlow 和两个 Text 对象也支持透明度并添加了游戏说明,因此信息屏幕是一个漂亮、可读的白色,文本预设为 Color.GREEN。如果您单击 Start Game 按钮(我们接下来将编写该按钮,以将其自身重置为默认设置),您可以在闪屏和新的帮助文本之间切换,尽管在闪屏上有一些错误,因为 Game Button 事件处理程序需要重置特性,我们接下来将恢复白色文本、文本位置、闪屏图像和欢迎图像,因为该按钮更改了对象特性。

接下来,让我们将这些 Java 语句复制并粘贴到 gameButton 事件处理结构中,然后我们将使用正确的文本、背景、图像对象和像素位置值来配置您的方法调用参数区域。清除 TextFlow 对象,然后使用。addAll()方法。接下来,使用 240 整数值为您的设置 TextFlow 容器的 X,Y 像素位置(它在屏幕上的位置)。setTranslateX()方法调用,并为您的。setTranslateY()方法调用。使用 uiBackground 对象加载 uiLayout StackPane 对象的背景。setBackground()方法调用,然后使用。setImage()方法调用。这都是通过在。handle()方法,如图 10-3 中间高亮显示:

A336284_1_En_10_Fig3_HTML.jpg

图 10-3。

Implement the code in the handle() method to configure gameButton default UI compositing layer objects

gameButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(playText, moreText);
        infoOverlay.setTranslateX(240);
        infoOverlay.setTranslateY(420);
        uiLayout.setBackground(uiBackground);
        boardGameBackPlate.setImage(splashScreen);
    }
} );

请注意,在图 10-3 中,我使用代码编辑窗格左边的加号(+)图标在 NetBeans 9 IDE 中打开了 gameButton 和 helpButton 事件处理结构,这样您就可以从 Java 代码的角度看到您的。handle()方法设置所有合成管道对象特征,以便仅使用少量不同的变量和对象设置来控制每个不同按钮对象的屏幕设计。这是一个例子,说明当您以最佳方式设置 JavaFX 场景图时,Java 可以有多么强大。

使用“运行➤项目”工作流程,再次在“开始游戏”和“游戏规则”按钮 UI 控件之间切换。你会看到游戏规则按钮不再弄乱你的开始游戏屏幕,如图 10-4 所示。

A336284_1_En_10_Fig4_HTML.jpg

图 10-4。

Test your UI Button event handling with the Start Game and Game Rules, switching back and forth

接下来,让我们将 helpButton Java 语句复制并粘贴到 legalButton 事件处理结构中,然后通过使用正确的文本、背景、图像对象和像素位置值来配置这些方法调用参数区域。再次清除 TextFlow 对象,然后使用。addAll()方法。接下来,设置 TextFlow 容器的 X,Y 像素位置(它在屏幕上的位置)。setTranslateX()方法调用,并为。setTranslateY()方法调用。用背景加载你的 uiLayout 对象背景。使用您的。setBackground()方法调用,然后使用。setImage()方法调用。这都是通过在。handle()方法,如图 10-5 中间高亮显示:

A336284_1_En_10_Fig5_HTML.jpg

图 10-5。

Implement the code in the handle() method to reconfigure your legalButton UI compositing layer objects

legalButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(copyText, riteText);
        infoOverlay.setTranslateX(200);
        infoOverlay.setTranslateY(370);
        uiLayout.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(legalLayer);
    }
} );

接下来,使用您的运行➤项目工作流程,并确保您的免责声明按钮是在白色背景上以可读、有组织的格式配置您的文本对象。正如你将在图 10-6 中看到的,你的 UI 屏幕看起来不错,你可以继续通过再次使用复制和粘贴来创建一个游戏积分按钮对象事件处理结构。

A336284_1_En_10_Fig6_HTML.jpg

图 10-6。

Test UI Button event handling with the Start Game, Game Rules, and Disclaimers, switching back and forth

接下来,让我们将 helpButton Java 语句复制并粘贴到 legalButton 事件处理结构中,然后通过使用正确的文本、背景、图像对象和像素位置值来配置这些方法调用参数区域。再次清除 TextFlow 对象,然后使用。addAll()方法。接下来,设置 TextFlow 容器的 X,Y 像素位置(它在屏幕上的位置)。setTranslateX()方法调用,并为。setTranslateY()方法调用。用背景加载你的 uiLayout 对象背景。使用您的。setBackground()方法调用,然后使用。setImage()方法调用。这都是通过在。handle()方法,如图 10-7 中间高亮显示:

A336284_1_En_10_Fig7_HTML.jpg

图 10-7。

Implement the code in the handle() method to reconfigure your creditButton UI compositing layer objects

creditButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(credText, codeText);
        infoOverlay.setTranslateX(240);
        infoOverlay.setTranslateY(370);
        uiContainer.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(creditLayer);
    }
} );

接下来,使用您的“运行➤项目”工作流程,并确保您的 Credits TextFlow 对象以可读格式将其所有文本对象定位在屏幕上。如图 10-8 所示,你的 UI 屏幕看起来很棒。我们暂时不实现高分按钮,因为稍后我们将创建一个评分引擎和高分表。

A336284_1_En_10_Fig8_HTML.jpg

图 10-8。

Implement the code in the handle() method to reconfigure the creditButton UI compositing layer objects

特效:javafx.scene.effects 包

javafx.scene.effect 包包含所有 javafx 特效的基础超类。不奇怪,这叫效果类。Effect 类有 17 个已知的 2D 数字图像合成效果的直接子类,比如你会在 GIMP 2.10 中找到的,它们也包含在这个包中。其中包括 Blend、Bloom、BoxBlur、ColorAdjust、ColorInput、DisplacementMap、DropShadow、FloatMap、GaussianBlur、Glow、ImageInput、InnerShadow、Lighting、MotionBlur、PerspectiveTransform、Reflection、SepiaTone 和 Shadow 类。对于 2D,这个包还包含了轻超类和轻。遥远,轻盈。点,和光。Spot 子类,我们将在本书的 3D 部分使用。

让我们先来看看 JavaFX Effect 超类。该类是一个公共抽象类,扩展了 java.lang.Object 主类。这意味着它是由 JavaFX 开发团队从头开始创建的,专门用于在 JavaFX 中提供基于图像(基于像素)的特殊效果,并提供照明支持,可用于 2D 和 3D。提供的效果很像 GIMP 3 或 Photoshop 在各自的数字成像软件包中提供的效果。

因此,JavaFX Effect Java 类的层次结构如下所示:

java.lang.Object
  > javafx.scene.effect.Effect

Effect 类为 JavaFX 中所有特效实现的创建提供了一个抽象或“基础”类。JavaFX 中的效果对象(及其子类)将始终包含一个生成图像对象的像素图形算法。这将是对源图像对象中像素的算法修改,在 2D 和 3D 中都有效。

通过设置名为 Node.effect 的属性,也就是节点类(或从节点子类创建的对象)的效果属性,效果对象也可以与场景图形节点(而不是图像对象)相关联。

有些效果(如 ColorAdjust)会改变源像素的颜色特征(色调、亮度和饱和度),而其他效果(如 Blend)会通过算法(通过 Porter-Duff)将多个图像组合在一起。

DisplacementMap 和 PerspectiveTransform 特殊效果类将在 2D 空间中扭曲或移动源图像的像素,以模拟 3D 空间,通常称为“2.5D”或“等轴”空间光学效果。

所有 JavaFX 特效都至少定义了一个输入。此外,该输入可以设置为另一个效果对象,允许开发人员将效果对象链接在一起。这允许开发者组合效果结果,允许创建复合或混合特效。此输入也可以保持“未指定”,在这种情况下,效果会将其算法应用于使用. setEffect()方法调用附加到的节点对象的图形渲染(像素表示或渲染结果),或者应用于已提供的图像对象。

需要注意的是,特效处理是一个有条件的特性。条件功能。效果枚举类和常量将定义一组条件(支持的)特效功能。这些功能可能并非在所有操作系统或所有嵌入式平台上都可用,尽管“现代”消费电子设备通常可以使用其硬件 GPU 图形处理能力来支持效果处理以及 i3D 渲染。

如果您的专业 Java 游戏应用想要轮询硬件平台以确定是否有任何特定的效果功能可用,您可以使用 Platform.isSupported ()方法调用来查询效果支持。如果您在不支持条件功能的平台上使用任何条件功能,都不会导致异常。一般来说,条件特性将被忽略,这样您就不必编写任何特定的错误捕获或错误处理 Java 代码。

接下来,让我们看看如何在 UI 设计中实现一两个这样的特殊效果,并向 TextFlow 对象添加投影,以便使用增加的对比度使它显示的文本更具可读性。之后,我们将看看如何在可见光谱范围内改变数字图像的颜色。

创建特殊效果:添加 createSpecialEffects()方法

让我们跟随组织 Java 代码的趋势,创建一个名为。createSpecialEffects()。让 NetBeans 9 在 createTextAssets()方法调用之后,通过添加一行代码在 start()方法中调用它来创建一个空的private void createSpecialEffects() {...}基础架构,如图 10-9 中突出显示的。这里的逻辑是,我们将首先加载图像,然后定义效果,然后创建文本。

A336284_1_En_10_Fig9_HTML.jpg

图 10-9。

Add a createSpecialEffects() method call at the top of .start() so that NetBeans creates the method body

接下来,我们将用特效代码替换 createSpecialEffects()方法中的引导代码。

投影:向 TextFlow 对象添加投影

现在是时候将 Java 代码添加到空的 createSpecialEffects()方法中来设置投影效果了。稍后,您将通过使用。setEffect()方法调用。首先,您需要在类的顶部声明一个名为 DropShadow 的 dropShadow 对象,并使用 Alt+Enter 工作进程让 NetBeans 为您生成一个导入语句。接下来,在 createSpecialEffects()方法中,使用 Java new 关键字和 DropShadow()构造函数方法实例化该对象。接下来,使用。setRadius()方法调用 dropShadow 对象来设置一个 4.0 像素的阴影半径(它从源展开的程度)。接下来,使用。setOffsetX()和。setOffsetY()方法使用 3.0 像素的设置调用,以使阴影向右斜向偏移(使用负值表示相反方向)。最后,使用. setColor()方法调用来指定暗灰色颜色类常量。图 10-10 中突出显示的代码应如下所示:

A336284_1_En_10_Fig10_HTML.jpg

图 10-10。

Code your private void createSpecialEffects() method body to create and configure a DropShadow object

DropShadow dropShadow;
...
private void createSpecialEffects() {
    dropShadow = new DropShadow();
    dropShadow.setRadius(4.0);
    dropShadow.setOffsetX(3.0);
    dropShadow.setOffsetY(3.0);
    dropShadow.setColor(Color.DARKGRAY);
}

接下来,打开 createTextAssets()方法体,添加一个.setEffect(dropShadow)方法调用每个文本对象,将它们绑定到投影效果和你为对象设置的设置,如图 10-11 所示。

A336284_1_En_10_Fig11_HTML.jpg

图 10-11。

Add a .setEffect(dropShadow) method call to each of your Text objects in the createTextAssets() method

另一个流行且有用的特效是调整像素颜色值,这是你想在你的 pro Java 9 游戏开发中使用的。javafx.scene.effect 包中有一个强大的 ColorAdjust 特殊效果类,允许开发人员调整图像的数字成像属性,包括使用对比度。setContrast(),亮度使用。setBrightness(),饱和度使用。setSaturation()和 hue (color)使用。setHue()。接下来我们来了解一下这个。

颜色调整:调整色调、饱和度、对比度和亮度

让我们使用。setHue()方法调用 ColorAdjust 对象,以允许我们对 PNG32 透明徽标数字图像素材的色温进行“颜色转换”,以便它在视觉上与我们在本章中改进的每个按钮控件用户界面设计的所有其他屏幕设计元素颜色值的颜色相匹配。在类的顶部声明一个名为 ColorAdjust 的 colorAdjust 对象。在 createSpecialEffects()方法中,使用 ColorAdjust()构造函数实例化该对象,然后使用浮点 0.4 值调用该对象的. setHue()方法,将当前图像颜色值在色轮周围向前移动 40%。图 10-12 中间和底部突出显示的 Java 代码如下所示:

A336284_1_En_10_Fig12_HTML.jpg

图 10-12。

Add a colorAdjust object instantiation and use a .setHue (0.4) method call off of the object to configure it

DropShadow dropShadow;
ColorAdjust colorAdjust;
...
private void createSpecialEffects()  {
    dropShadow = new DropShadow();
    dropShadow.setRadius(4.0);
    dropShadow.setOffsetX(3.0);
    dropShadow.setOffsetY(3.0);
    dropShadow.setColor(Color.DARKGRAY);
    colorAdjust = new ColorAdjust();
    colorAdjust.setHue(0.4);        }

ColorAdjust 效果对象实现的下一步是添加。在 helpButton.setOnAction()事件处理程序中,对 logoLayer ImageView 对象调用 setEffect(colorAdjust)方法。这将使透明徽标 PNG32 图像中的棕色像素变为绿色,同时保持透明像素不变,因为它们具有零颜色值(和最大透明度值)。如果这些像素是使用部分颜色值和部分透明度值定义的,那么部分颜色值将向前移动 40%。

我加了这个。setEffect()方法调用紧接在你的boardGameBackPlate.setImage(helpLayer);方法调用之后,因为我们现在需要对徽标图像复合层进行颜色转换,正如你在图 10-13 中看到的高亮显示。logoLayer 对象的 Effect 对象被设置为 colorAdjust 对象,该对象随后被设置为色调值 0.4(40%)。

A336284_1_En_10_Fig13_HTML.jpg

图 10-13。

Use a Run ➤ Project work process to make sure that the drop shadow special effects are working well

你可能想知道为什么我已经在 createSpecialEffects()方法中设置了 40%的色调。原因是 createSpecialEffects()方法中的设置可以被视为“默认”设置,而我必须在 helpButton 事件处理程序代码中指定它(再次)的原因是其他按钮处理程序将设置不同的色调值。您的 helpButton.setOnAction()事件处理代码现在应该如下所示:

helpButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(helpText, cardText);
        infoOverlay.setTranslateX(130);
        infoOverlay.setTranslateY(360);
        uiLayout.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(helpLayer);
        logoLayer.setEffect(colorAdjust);
        colorAdjust.setHue(0.4);
    }
});

现在是时候使用“运行➤项目”工作流程,并确保 TextFlow 对象上的投影效果使您的文本对象更具可读性,并与屏幕字幕图像上的投影相匹配。您可以在图 10-14 中看到,Java 代码中有些地方出了问题,因为 JavaFX 应用运行了,但没有对文本进行阴影处理。让我们检查一下 Java 代码执行语句的顺序,看看是否有什么地方出了问题!

A336284_1_En_10_Fig14_HTML.jpg

图 10-14。

Declare and instantiate a ColorAdjust object named colorAdjust and use .setHue() to shift the color 40 percent

由于 Java 代码在方法中的顺序是正确的,我怀疑调用方法的顺序很可能是这个问题的根源。让我们看看 start()方法内部的方法调用顺序,如图 10-9 顶部所示。请注意,createSpecialEffects()是在 createTextAssets()之后调用的,但是我们使用的是。setEffect(dropShadow)方法调用在 createTextAssets()方法内部,所以我们必须将 createSpecialEffects()方法调用移到 createTextAssets()方法调用之上,如图 10-13 所示,这样你的效果在使用之前就已经设置好了。如果你在你正在做的事情的过程中追踪这种逻辑,那么 Java 代码是非常合乎逻辑的!

正如你在图 10-16 中看到的,这解决了问题,你的投影效果渲染正确。

下一步需要修改的是您的 legalButton.setOnAction()事件处理结构,使屏幕上的所有内容都变成紫色。这可以通过将你的 logo 的色调改变 40%来实现,这次是在色轮的负方向。使用浮点数,色轮的右正 180 度范围从 0.0 到 1.0,左负 180 度范围从 0.0 到-1.0。

在图 10-15 的底部,高亮显示了您的 legalButton 事件处理 Java 语句的 Java 代码。它应该类似于下面的 Java 代码:

A336284_1_En_10_Fig16_HTML.jpg

图 10-16。

Use the Run ➤ Project work process and make sure that the drop shadow effect is rendering correctly

A336284_1_En_10_Fig15_HTML.jpg

图 10-15。

Add a .setEffect (colorAdjust) method call off logoLayer and call .setHue(-0.4) to change the color shift

legalButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(copyText, riteText);
        infoOverlay.setTranslateY(200);
        infoOverlay.setTranslateY(370);
        uiLayout.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(legalLayer);
        logoLayer.setEffect(colorAdjust);
        colorAdjust.setHue(-0.4);
    }
} );

图 10-17 显示了我的 Run ➤项目按钮处理程序测试工作流程,显示了投影效果和免责声明按钮控件中的色调(颜色)移动,进一步完善了设计。我在所有不同的按钮元素之间来回点击,以确保所有的属性都没有以不可取的方式重置任何其他按钮屏幕设计属性,这就是我将所有正确的变量放在所有事件处理代码体中的原因,以便没有方法调用忽略设置(未指定/传递)。

A336284_1_En_10_Fig17_HTML.jpg

图 10-17。

Use the Run ➤ Project work process and make sure that the color hue shift matches the rest of the design

您需要做的最后一项修改是对 creditButton.setOnAction()事件处理结构进行修改,以使屏幕上的所有内容呈现出漂亮的蓝色。这可以通过围绕一个色轮将你的标志色调向负方向偏移 90%来实现。图 10-18 中间突出显示了处理 Java 语句的 creditButton 事件的代码,如下所示:

A336284_1_En_10_Fig18_HTML.jpg

图 10-18。

Copy and paste the colorAdjust.setHue(-0.9) and logoLayer.setEffect(colorAdjust) Java code in creditButton

creditButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event)    {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(credText, codeText);
        infoOverlay.setTranslateY(240);
        infoOverlay.setTranslateY(370);
        uiLayout.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(creditLayer);
        logoLayer.setEffect(colorAdjust);
        colorAdjust.setHue(-0.9);
    }
});

使用“运行➤”项目的工作流程,确保色轮周围这 90%的负偏移现在将徽标变成鲜艳的蓝色,这与用户界面设计的其余部分看起来很好。正如你在图 10-19 中看到的,演职员表按钮控制屏幕现在在颜色和阴影效果上都匹配了。

A336284_1_En_10_Fig19_HTML.jpg

图 10-19。

Use the Run ➤ Project work process and make sure that the color hue shift matches the rest of the design

我们将编写 scoreButton.setOnAction()事件处理程序的内部代码,当我们在本书后面介绍实现评分引擎和高分 UI 设计时,您将有一个可以就地评分的棋盘游戏。

在您的 pro Java 9 游戏开发中实现许多其他特殊效果的工作过程也可以用同样的方式来实现——为您想要使用的效果声明一个类,在 createSpecialEffects()方法中实例化它,使用该类中的方法设置效果配置的参数,最后使用。setEffect(effectClassNameHere)方法调用对象名称。

您会发现 JavaFX 特效包和类在这种实现方法中特别灵活,因为您可以将大多数软件包中常见的所有特效应用于 JavaFX 9 中的几乎任何对象或场景图形层次结构,通常使用大约十几行代码,有时甚至更少。

一旦你知道了如何在 JavaFX 中创建和应用这些特效,你的 pro Java 9 游戏开发创造力就会提升一个数量级。这是因为这些效果可以应用于场景图形层次结构中的任何地方,以及 2D、成像和 3D 渲染管道中的任何地方。

在本书的后面部分,我会尝试使用更多的 JavaFX 9 Effect 子类,因为我会给每一章增加更多的复杂性,并且随着我们在本书中的进展。

摘要

在第十章中,我们使用 ActionEvent 处理结构将交互性添加到用户界面设计中,了解了 InputEvent 对象和 MouseEvent 及 KeyEvent 对象处理,并了解了如何应用 javafx.scene.effects 包中包含的利用 JavaFX Effect 超类的特殊效果。

接下来,通过使用 java.util 和 javafx.event 包及其 eventObject、Event、ActionEvent 和 InputEvent 类,您了解了 Java 9 和 JavaFX 中如何处理允许交互的事件。我们讨论了不同类型的 InputEvent 对象,如 MouseEvents、TouchEvents 和 KeyEvents,然后您实现了 ActionEvent 处理,以使用户界面的(中间三个)说明、法律免责声明和 Production Credits 部分(按钮对象)具有交互性。

最后,您了解了 javafx.scene.effects 包以及 javafx 为开发人员提供的许多特殊效果。我们查看了 Effect 超类,并讨论了如何实现 DropShadow 类(和对象)和 ColorAdjust 类(和对象),以便您可以通过向 TextFlow 对象添加阴影来美化用户界面,提高可读性(对比度),并对顶部徽标数字图像素材进行颜色转换,以匹配每个按钮控件对象用户界面设计的颜色方案。

在第十一章中,我们将看看如何配置您的 JavaFX 游戏来利用 3D 资源。这涉及 Camera 超类及其 ParallelCamera 和 PerspectiveCamera 子类。我们还将学习如何在你的 3D 场景中创建光线,以便相机物体可以“看见”我们将看看 LightBase 超类及其 AmbientLight 和 PointLight 子类,它们是专门为 3D 场景应用中的照明设计而提供的。

十一、3D 场景配置:使用透视相机和点光源

现在,您已经完成了闪屏和用户界面设计的 2D 场景图层次,让我们回到第十一章中的 JavaFXGame 主要应用类编码,并开始设计 3D 游戏棋盘场景基础设施,这将是棋盘游戏及其游戏性的渲染和照明的基础。我们将了解基本的 3D 场景组件,您会发现这些组件预装在 3D 软件包(如 Blender 或 Autodesk 3D Studio Max)中(适用于所有默认或空场景)。之后,我们可以在第十二章进入 JavaFX 图元(盒子、平面、圆柱体、圆盘、球体和药丸)以及在第十三章使用材质和纹理贴图进行着色。

在本章中,您将了解 JavaFX 9 Camera和 LightBase 子类的不同类型,这些子类包含在核心 javafx.scene 包中,而 Java FX . scene 包又包含在 javafx.graphics 模块中(从 Java 9 开始)。我们将讨论 PerspectiveCamera,因为您将在本章中创建的基本 3D 场景基础架构中使用它,以及 ParallelCamera,另一个更适合您的 2D 或 2.5D 游戏开发管道的相机子类。Camera 是一个抽象超类,不能直接使用。我们还将了解 public LightBase 抽象超类及其两个核心照明子类 AmbientLight 和 PointLight。

我们还将通过向 JavaFX SceneGraph 添加 3D 渲染、相机和照明来继续处理您的 JavaFXGame Java 代码,以便您可以开始向您的 3D 游戏添加 3D 元素,这将在我们讲述 JavaFX Shape3D 类及其图元子类(在第十二章)并使用着色器以及将材质和纹理贴图应用于该 3D 几何图形(在第十三章)之后进行。

关于 JavaFX 如何能够在 3D 场景中可视化(渲染)游戏的 3D 几何资源及其纹理贴图,我们还有很多需要了解的,所以让我们开始了解场景相机对象。

使用 3D 相机:给 3D 游戏添加视角

任何 3D 渲染管道的顶层都是场景摄像机,因为这是处理 3D 场景中所有事情的设备,然后将数据交给渲染引擎。在这种情况下,它是 PRISM 软件渲染器(在没有 GPU 的情况下),或者它可能是您正在玩 3D 游戏的消费电子设备(PC、手机、平板电脑、iTV 电视机、笔记本电脑、游戏控制台、机顶盒)上的 OpenGL 硬件渲染引擎。如果您仍在使用 Windows,它可能还包括 DirectX 3D 渲染。相机对象(在我们的例子中,这将是一个 PerpectiveCamera 对象)专门用于 3D 场景渲染;我们将在本章的这一节讨论它。它是 JavaFX 9 SceneGraph 不可或缺的一部分,因此它有自己的 Scene.setCamera(Camera)方法调用。此方法调用用于将相机对象添加到场景图根,以确保它位于场景图渲染层次的最顶端(根)。它不使用。getChildren()。add()方法链,因此,它将被设置在 createSceneGraphNodes()方法中,在本章的这一节,当我们为您的 pro Java 9 游戏设置这个 Camera 对象时,您将会看到这一点。我们还将介绍 ParallelCamera,它更适合 2D 游戏。

JavaFX Camera 类:定义相机的抽象超类

公共 JavaFX Camera 超类是一个抽象类,仅用于创建不同类型的相机。目前有一个正交或平行摄影机子类(对象)或透视摄影机子类(对象)。您的应用不应该试图直接扩展这个抽象的 Camera 类;如果您尝试这样做,Java 将抛出一个 UnsupportedOperationException,您的 pro Java 9 游戏将无法编译或运行。Camera 类保存在核心 javafx.scene 包的 javafx.graphics 模块中,是 Node 的子类,因为它最终是 SceneGraph 顶部的一个节点。Camera 类实现了 Styleable 接口,因此它可以被样式化,并且它包含 EventTarget 接口,因此它可以处理事件。JavaFX Camera 类的 Java 9 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.Camera

camera 类是用于渲染场景的任何 Camera 子类的基类。相机用于定义场景的坐标空间如何呈现在用户正在观看的 2D 窗口(舞台)上。默认摄影机(如果您没有专门创建一个,我们将在本节稍后进行)将被定位在场景中,这样它在场景坐标空间中的投影平面在 Z=0 处(正好在中间),并且在正 Z 方向上看向屏幕。出于这个原因,我们将在代码中使我们的相机远离屏幕中心(-1000)1000 个单位,因为 i3D 游戏板将位于“中心舞台”并位于 0,0,0 (X,Y,Z)。

从摄影机到投影平面的距离(以 Z 为单位)可以由摄影机所附着的场景(也是生成的投影平面)的宽度和高度以及摄影机对象的 fieldOfView 参数来确定。相机对象的 nearClip 和 farClip 属性是在这个抽象类中定义的仅有的两个属性或特征,并且是在 JavaFX 所谓的眼睛坐标空间中指定的。该空间由观察者的眼睛在相机对象的原点处定义,并且投影平面在眼睛前方的正 Z 方向上是一个单位。任何相机子类(如 PerspectiveCamera)的 nearClip 和 farClip 属性都可以使用。setNearClip()和。setFarClip()方法调用。这是两个 PerspectiveCamera 类(object)方法调用,我们将在本章的后半部分使用它们来配置 SceneGraph camera 对象。

JavaFX PerspectiveCamera 类:您的 3D 透视相机

JavaFX PerspectiveCamera 类扩展了 Camera 类,用于创建 PerspectiveCamera(对象),该对象用于渲染 i3D 场景。PerspectiveCamera 类也保存在核心 javafx.scene 包的 javafx.graphics 模块中;它是 Node 的子类,是 JavaFX 场景图顶部的一个节点。PerspectiveCamera 类还实现了 Styleable 接口,以便可以对其进行样式化,并实现了 EventTarget 接口,以便可以处理事件。JavaFX PerspectiveCamera 类的 Java 9 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.Camera
      > javafx.scene.PerspectiveCamera

PerspectiveCamera 对象定义透视投影的观察体积。想象一个截断的面向右侧的金字塔,因为大多数相机都是在 Blender 或 3D Studio Max 等 3D 软件中可视化表示的。

这个类有两个(重载的)构造函数方法。一个有一个空的参数区域,像这样:

camera = new PerspectiveCamera();

第二个使用布尔值 fixedEyeAtCameraZero 属性(或参数或特征),这是我们将在摄像机对象声明、实例化和配置 Java 代码中使用的属性,如下所示:

camera = new PerspectiveCamera(true);

当然我们也会在类的顶部声明一个 PerspectiveCamera camera,用 Alt+Enter 让 NetBeans 9 为我们写一个这个类的 import 语句。PerspectiveCamera 有一个 fieldOfView 值,可用于更改相机投影的视野(FOV)角度,以度为单位。我将保留 FOV 的默认值,并假设这个默认的 FOV 给出了最佳的视觉效果,这是由 JavaFX 开发团队决定的。

我在游戏和模拟中使用 i3D 的倾向是“推拉”,或沿着 Z(场景内外)变换轴移动相机,而不是使用 FOV 值变化,因为即使在现实生活中,改变相机镜头(如从 24 毫米到 105 毫米)往往会更剧烈地改变视角。根据我的经验,使用不同的 3D 虚拟相机,这种视角的变化在虚拟 3D 中比使用真实相机时更加剧烈。

默认情况下,在创建(实例化)时,透视摄影机位于场景的中心,并沿着正 z 轴观察(指向下方)。如果使用 PerspectiveCamera(false)构造 PerspectiveCamera,那么由该相机定义的坐标系的 0,0 原点将位于面板的左上角,y 轴指向下方,z 轴指向远离查看者的方向(进入屏幕)。如果将 PerspectiveCamera 节点添加到场景图中,则转换后的相机位置和方向将定义相机的位置和相机正在观察的方向。在默认相机中,fixedEyeAtCameraZero 为 false,眼睛位置 Z 值在 Z 中进行调整,以便使用指定的视场生成的投影矩阵将使用设备无关的像素在 Z = 0 处(在投影平面上)生成单位。这符合你的平行摄像机的特点。调整场景大小时,投影平面(Z = 0)上场景中的对象将保持相同的大小,但场景中或多或少的内容是可见的,这比 3D 相机更适合 2D 相机和 2D 滚动条的使用,在 3D 相机中,调整相机大小将会缩放场景。这就是为什么 PerspectiveCamera 通常使用 PerspectiveCamera(true)进行实例化的原因,我们将在本章的这一节稍后进行。

当 fixedEyeAtCameraZero 设置为 true 时,眼睛位置在相机的局部坐标中固定在(0,0,0)。将使用默认的(或指定的)fieldOfView 属性生成投影矩阵,并且将在窗口(视口或舞台对象)上映射投影体积,使得它将在投影平面的点处的或多或少的设备无关像素上被“拉伸”(缩放)。当场景大小属性更改时,场景中的对象将按比例缩小或增大,但内容的可见范围(边界)将保持不变。

如果您计划变换(移动或推拉)相机对象,JavaFX 开发团队建议将此 fixedEyeAtCameraZero 设置为 true。当 fixedEyeAtCameraZero 设置为 false 时变换相机可能会导致最终用户感觉不直观的结果。

请注意,PerspectiveCamera 是一个有条件的 3D 特征。您可以轮询条件功能。确定给定用户的设备是否支持该特性的布尔变量(在本例中,支持 i3D)。这将使用以下 Java 代码结构来完成,该结构设置一个布尔变量来反映系统对 3D 渲染的支持:

boolean supportFor3D = Platform.isSupported(ConditionalFeature.SCENE3D);

最后,该类有一个名为 verticalFieldOfView 的布尔属性,用于定义 FieldOfView 属性是否将应用于投影的垂直维度。这在逻辑上意味着,如果这是假的,增加或减少 FOV 将改变投影的宽度,但不改变(垂直)高度,如果这是真的,它将改变(缩放)相机投影的水平(宽度)和垂直(高度)维度,这将表面上比仅改变相机投影平面的一个维度更好地保持纵横比。

接下来,让我们看一下 ParallelCamera 类,我们将在 Camera 子类的覆盖范围中包括它,以保持一致性,尽管这个相机更适合用于 2D 游戏和可能的正交 3D 应用。

JavaFX ParallelCamera 类:你的 2D 空间平行相机

JavaFX ParallelCamera 类也扩展了 Camera 类,并用于创建 ParallelCamera(对象),该对象用于渲染 i2D 场景。这个 ParallelCamera 类也保存在核心 javafx.scene 包的 javafx.graphics 模块中;它是 Node 的子类,是 JavaFX 场景图顶部的一个节点。ParallelCamera 类还实现了 Styleable 接口,以便可以对其进行样式化,并实现了 EventTarget 接口,以便可以处理事件。因此,JavaFX ParallelCamera 类的 Java 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.Camera
      > javafx.scene.ParallelCamera

JavaFX 9 创建的默认相机将始终是一个 ParallelCamera,这就是为什么我们在本章中编写特定相机和 LightBase 对象创建的原因。例如,如果您只是创建了一个球体对象,而没有创建任何 Camera 子类对象或任何 LightBase 子类对象,JavaFX 运行时将自动创建一个 ParallelCamera 对象和一个 AmbientLight 对象,以便 Shape3D 子类(球体)对渲染器可见。

如果一个场景只包含 2D 变换,那么它不需要透视相机,因此会使用平行相机,这不会渲染 3D 对象的所有特征。ParallelCamera 更适合 Java 8 游戏开发入门(Apress,2014)中所涉及的内容。这种相机定义了平行的观察体积,在 3D 行业中也称为正交投影。实质上,正投影相当于一个矩形平面。

ParallelCamera 始终位于窗口的中心,并将沿着 z 轴的正方向看。ParallelCamera(相对于 PerspectiveCamera)的不同之处在于,该相机定义的场景坐标系的原点位于屏幕的左上角,y 轴位于屏幕的左侧,x 轴位于屏幕顶部的右侧,z 轴指向远离观察者的方向(屏幕表示中的距离)。

ParallelCamera 对象中使用的单位使用像素坐标表示,因此这就像 2D 数字成像软件包和我们用于 2D UI 设计的 2D StackPane 图像合成层对象一样,它也引用屏幕左上角 0,0 (X,Y)处的坐标。这是另一个事实,这是一个更符合逻辑的相机子类,用于 i2D 游戏,而不是 i3D 游戏。

这个类只有一个构造函数方法,它使用一个空的参数区域,如下所示:

Camera2D = new ParallelCamera();

接下来,让我们看看如何创建 PerspectiveCamera 对象,我们将在 pro Java 9 游戏中使用它。我们将了解如何对其进行初始配置,以及如何将其添加到 JavaFX SceneGraph 的根目录中。

向场景添加透视相机:使用。setCamera()

我们需要添加到 JavaFXGame 类顶部的第一件事是使用 PerspectiveCamera 摄像机声明 PerspectiveCamera 对象;Java 语句,它将在 PerspectiveCamera 对象下显示一个红色波浪下划线指示器(类用法)。使用 Alt+Enter 快捷键让 NetBeans 9 为您编写 import 语句,然后打开 createSceneGraphNodes()方法,以便您可以将该照相机对象添加到场景图的顶部(根)。使用 camera = new perspective camera(true)在根组实例化下实例化这个 camera 对象;构造函数语句。然后,在下一行中,使用-1000 值调用. setTranslateZ()方法,将相机从 3D 场景的 0,0,0 中心移动 1,000 个单位。

使用. setNearClip()方法调用摄影机对象,将 nearClip 摄影机对象属性设置为 0.1,并使用。setFarClip()方法调用。最后,使用。setCamera()方法调用了场景对象,并在.setCamera( camera )方法调用中使用相机对象作为参数传递相机对象。将场景对象的背景值设置为颜色。通过使用。setFill()方法调用,这样您的 3D 对象会很突出,使用这些 Java 语句:

PerspectiveCamera camera;
...
createSceneGraphNodes() {
    camera = new PerspectiveCamera(true);
    camera.setTranslateZ(-1000);
    camera.setNearClip(0.1);
    camera.setFarClip(5000.0);
...
    scene.setFill(Color.BLACK);
    scene.setCamera(camera);

正如你在图 11-1 中看到的,你的代码是没有错误的,摄像机现在已经设置好并连接到你的 3D 场景,我们现在已经把它转换成一个 3D 场景。它现在是一个 3D 场景,因为它在其渲染管道的顶部(即在其根处)使用了 PerspectiveCamera,所以它下面的所有对象现在都将使用 3D 透视图。

A336284_1_En_11_Fig1_HTML.jpg

图 11-1。

Add a PerspectiveCamera object declaration at the top of the class and then instantiate it and configure it

接下来,让我们使用运行➤项目工作流程来看看您通过添加 PerspectiveCamera 创建的新 3D 场景如何影响您现有的 2D UI 设计;现在,这是一个“混合”2D 和 3D 应用,是 JavaFX 应用的最高级类型。这是真的,因为我们需要在一个无缝合成环境中结合 2D 和 3D 素材,这是一个极其复杂的任务。两个最先进的电影和特效合成软件包 Fusion 和 Nuke 完成了 2D 与 3D 的融合。事实上,如果你想了解更多关于将 2D 和 3D 素材结合到一个管道中的信息,请查看 VFX 基本面(Apress,2016)。正如你在图 11-2 中看到的,你的场景图根的 StackPane 分支,以及它下面的所有东西,都被它(正确地)携带着,并且现在从层次顶部的 PerspectiveCamera 和它的屏幕的 0,0,0 中心(视觉上)引用,你在这一章的前面已经学过。

A336284_1_En_11_Fig2_HTML.jpg

图 11-2。

Run the Project and notice that the StackPane is now located at the PerspectiveCamera 0,0,0 center origin

我尝试的第一件事是通过使用。settranslate(-640)和。setTranslateY(-320),这在一定程度上起了作用,因为结果看起来像图 11-2;但是,它在左上角,整个 StackPane 布局可见,并且缩小了 200%(四倍,或四分之一屏幕)。

这告诉我,StackPane 是一个 2D 对象,从技术上讲是一个“平面”,它与相机投影平面“完全平行”,面向相机对象的 z 轴。相比之下,现在 StackPlane 是 3D 渲染管道的一部分,因为它是 PerspectiveCamera 的子级(在渲染器处理管道之下)。

这意味着 StackPane 及其所有子元素(VBox、ImageView 和 TextFlow)都是通过 PerspectiveCamera 对象进行处理的。这包括它的所有算法和坐标系统(以及类似的“交战规则”,如果你愿意的话),所有这些都改变了它将被渲染到屏幕(场景对象)的方式和位置。

我尝试的下一件事是使用。setTranslateX)和。setTranslateY(0)方法调用。这是通过添加以下两个 Java 语句来实现的,将 StackPane 重定位到。start()方法位于 uiLayout StackPane 对象实例化 Java 语句之后。

这里显示了这些 Java 代码,并且在图 11-3 的中间附近用蓝色突出显示:

A336284_1_En_11_Fig3_HTML.jpg

图 11-3。

Add the .setTranslateX() and .setTranslateY() method calls off the uiLayout StackPane object, both set to zero

uiLayout = new StackPane();
uiLayout.setTranslateX(0);
uiLayout.setTranslateY(0);

注意图 11-3 中您正在使用。setTranslateX()和。setTranslateY()在 logoLayer ImageView 上,以及在 infoOverlay TextFlow 上,每个都保持它们相对于 uiLayout StackPane 的位置。

这种相对定位的保留是因为您已经在 SceneGraph 层次中建立了父子关系,这就是为什么这是一个强大的场景构造工具,适用于任何类型的场景,无论是 i2D、i3D 还是混合场景。这也将是非常重要的,因为我们在本书中开发了你的 pro Java 9 游戏的 i3D 部分,因为我们将需要在你的游戏的 3D 部分进行更多的整体转换,而不是简单地将你的 UI 控制面板放在相机前面,这样它就会阻挡 3D 游戏的视图(至少现在是这样;随着我们不断完善 Java 代码和游戏设计,我们可能会在以后更改这个 UI 设计)。这正是游戏设计和编码在现实生活中发生的方式;游戏开发是一个旅程,而不是目的地。

使用运行➤项目工作流程,看看我们是否更接近同步您的 2D UI 覆盖图及其背后的 3D 场景。正如你在图 11-4 中看到的,UI 面板现在在你屏幕的中央,尽管缩小了。因此,我们将继续细化我们的对象属性。接下来,我们将使用相机对象的 Z 平移变量来使相机更接近 3D 场景,以实现我们想要的最终结果。

A336284_1_En_11_Fig4_HTML.jpg

图 11-4。

Run the project; your StackPane is now centered, but your camera object Z translation is too far out

我认为这是因为相机的 Z 平移距离这个新的 3D 场景的中心 1000 个单位。因此,接下来我将尝试将 camera.setTranslateZ()方法调用参数从-1000 减少到-500,以查看 3D 合成中 2D 的最终变化。

完成这一修改的 Java 代码应该如下所示,并在图 11-5 的顶部以蓝色突出显示:

A336284_1_En_11_Fig5_HTML.jpg

图 11-5。

Move the camera object 50 percent closer to the 3D scene projection plane by setting .setTranslateZ() to -500

camera.setTranslateZ(-500);

再次使用“运行➤项目”工作流程。正如你在图 11-6 中看到的,你的 UI 屏幕现在大了 50 %,所以我们需要缩小我们的。将 TranslateZ()设置为零,使 StackPane 与 3D 场景投影平面同步。

A336284_1_En_11_Fig6_HTML.jpg

图 11-6。

Run the project to see that the StackPane is still centered, but the camera Z translate value is still too far out

完成此任务的 Java 代码如下所示,在图 11-7 中可以看到用蓝色突出显示的代码:

A336284_1_En_11_Fig7_HTML.jpg

图 11-7。

Set the camera.setTranslateZ() method call to zero to synchronize the StackPane and projection plane

camera.setTranslateZ(0);

接下来,使用“运行➤项目”工作流程来查看您是否已经实现了将 StackPane 节点分支及其子节点与 3D 相机对象的投影平面同步的视觉目标。正如你在图 11-8 中看到的,你的 UI 屏幕看起来很棒,按钮也工作正常。

A336284_1_En_11_Fig8_HTML.jpg

图 11-8。

Use Run ➤ Project to see that your StackPane is perfectly synchronized (visually) with the camera projection plane

在我们开始学习 LightBase(超类)对象以及 AmbientLight 和 PointLight 子类之前,让我们确保我们之前的所有其他 2D UI 代码仍在工作,并做我们希望它做的事情。当向 Java 9 代码添加主要特性或更改时,花时间做这件事总是很重要的。

StackPane UI 测试:确保其他一切仍然工作

点击游戏规则按钮,如图 11-8 所示,并确保游戏说明的用户界面屏幕在外观上仍然可读和专业,尽管我们将在游戏发布前进一步完善该用户界面。从图 11-9 中可以看到,指令屏幕确实仍然可读;然而,颜色。白色背景颜色已被替换为彩色。黑色因为我们设置了新的 3D 场景对象来使用这个为其填充颜色值,如图 11-1 所示,使用了一个scene. setFill (Color. BLACK );的 Java 语句。这意味着我们现在需要将 StackPane 的背景颜色值设置为 Color。白色在场景合成(现在是渲染)管道中用白色填充我们的 UI 屏幕。由于 StackPane 位于场景之上,VBox、ImageView 和 TextFlow 之下,因此这是要设置颜色的逻辑对象。白色背景填充颜色。这将涉及到在位于。start()方法,而不是更改许多与设置文本对象颜色和 DropShadow 属性相关的 Java 语句,更不用说使用。setLightness()方法调用来使标题图像的文本元素变亮。这也让我有机会向您展示如何避开 StackPane 对象(类)没有. setFill()方法的限制,这意味着我们必须创建一个复杂的方法链,其中包含两个嵌套的“方法内部的对象实例化”Java 构造,我们在. setBackground(Background)方法调用中创建一个新的 Background 对象和一个新的 BackgroundFill 对象,并将 BackgroundFill 配置为白色。

A336284_1_En_11_Fig9_HTML.jpg

图 11-9。

Click the Game Rules Button control to see whether the Instructions section is rendering correctly. It is now black!

这个方法调用的基本 Java 9 编程语句结构(包含两个嵌套的对象实例化)将如下所示(最初;接下来我们会进一步配置)Java 9 编程结构:

uiLayout.setBackground( new Background( new BackgroundFill(Color.WHITE) ) );

这显示在图 11-10 中,尽管在 BackgroundFill 下有红色波浪下划线,因为我们需要使用 Alt+Enter 击键组合让 NetBeans 从 javafx.scene.layout (package)导入 BackgroundFill 类。这在图中也以蓝色突出显示为“Add import for Java FX . scene . layout . background fill”,双击它可以让 NetBeans 9 为您编写此导入语句。

A336284_1_En_11_Fig10_HTML.jpg

图 11-10。

Code your private void createSpecialEffects() method body to create and configure a DropShadow object

NetBeans 从最基本的(无导入语句)开始评估问题。因此,一旦为 BackgroundFill 对象准备好了 import 语句,NetBeans 就会继续评估该语句,从内部(BackgroundFill)到外部(到新的背景对象,再到。setBackground()方法调用)。

原来 BaclgroundFill 类的构造函数方法需要几个参数,而不仅仅是颜色。白色填充规格。这是因为 BackgroundFill 类将创建圆角并支持 Insets 对象规范,因此 BackgroundFill 构造函数的正确构造函数方法格式应该如下所示:

backgroundFill = new BackgroundFill(Paint, CornerRadii, Insets);

因此,对于我们的用法,一个完整的白色背景填充构造函数方法将使用 EMPTY 常量,没有任何填充或圆角边缘,因此看起来像下面的 Java 实例化:

new BackgroundFill( Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY );

一旦导入了 BackgroundFill 类,NetBeans 就会向我们显示出new BackgroundFill(Color.WHITE)对象实例化有问题,这通过整个代码结构下面的红色波浪下划线来表示,如图 11-11 所示。我将鼠标放在我正在构建的 Java 语句部分,NetBeans 9 会弹出对该问题的解释,它显示在浅黄色框中,周围有黑色轮廓。

A336284_1_En_11_Fig11_HTML.jpg

图 11-11。

Set uiLayout StackPane background to white using .setBackground(new Background(new BackgroundFill(Color.WHITE)));

我想要白色填充,所以我用了圆角半径。空和 Insets。按照构造函数方法参数的要求,最后两个参数按此顺序为空。最终的方法调用是这样的,如图 11-12 所示:

A336284_1_En_11_Fig12_HTML.jpg

图 11-12。

Add BackgroundFill constructor method parameters (Paint, CornerRadii, and Insets) to the method call

.setBackground(new Background(new BackgroundFill(Color.White,CornerRadii.EMPTY,Insets.EMPTY) ));

最后,使用“运行➤项目”工作流程来查看您是否已经完成了白色背景色填充的目标。正如您在图 11-13 中看到的,您的 UI 屏幕看起来又很棒,UI 按钮也工作正常。

A336284_1_En_11_Fig13_HTML.jpg

图 11-13。

The StackPane now has a Color.WHITE background fill, preventing scene Color.BLACK from showing

让我们在 Legal 和 Credits 按钮事件处理结构中实现这一修复,并将我们的应用恢复到 100%的工作状态。如图 11-14 所示,我已经将这个 uiLayout StackPane 对象后台属性配置 Java 9 代码结构复制并粘贴到您的 legalButton 和 creditButton 事件处理基础设施中,代码编译无误。

A336284_1_En_11_Fig14_HTML.jpg

图 11-14。

Copy and paste the uiLayout.setBackground() construct from helpButton to legalButton and creditButton

如果您使用您的运行➤项目工作流程并测试这三个按钮 UI 元素,您将会看到,由于我们使用了 StackPane 的背板(背景对象)来保存设置为 Color.WHITE 的颜色辅助类常量的 Paint 对象,您在前面章节中所做的所有艰苦设计工作都已完全恢复

实现开始游戏按钮:隐藏你的用户界面

我们要做的下一件事是注释掉 gameButton 事件处理程序代码中的所有代码(这样我们可以在以后恢复它们,如果我们想的话),然后添加一些新的语句,这些语句将隐藏(将 visibility 设置为 false)SceneGraph 的 StackPane 分支;我们还将把 camera.setTranslateZ()方法调用设置为我们最初想要使用的-1000 值。当我们构建游戏时,我们将添加额外的配置和控制语句到这个关于 i3D 游戏的按钮中,正如你现在看到的,它将“活”在 StackPane UI 控制面板后面。

正如你在图 11-15 中看到的,我已经注释掉了与你的 StackPane UI 合成管道相关的代码;我添加了关于从视图中移除 UI 控制面板以及将 3D 场景相机对象设置到游戏开始时我们想要的位置的语句。新的代码语句看起来像下面的 Java 代码,并在图 11-15 中间高亮显示:

A336284_1_En_11_Fig15_HTML.jpg

图 11-15。

Add a .setVisible(false) method call off of uiLayout and a .setTranslateZ(-1000) method call off the camera

uiLayout.setVisible(false);
camera.setTranslateZ(-1000);

现在,当您使用“运行➤项目”工作流程并单击“开始游戏”按钮时,您的 StackPane 将会消失,空的(黑色)3D 场景将会显示出来。

现在是时候使用 JavaFX 9 LightBase 超类及其 AmbientLight 和 PointLight 子类来学习 3D 场景照明了。在我们的 JavaFXGame 类中实现它们之前,在我们结束关于核心 3D 场景元素(相机和灯光基础)的这一章之前,我们将详细介绍这些元素,这些元素需要在我们的 i3D pro Java 9 游戏设计和开发管道中作为 SceneGraph 的根。开始兴奋了吗?灯光,摄像机…动作事件!

使用 3D 照明:为 3D 游戏添加照明

JavaFX 9 中有两组不同的照明 API。一个是用于 3D 场景的,包含在 javafx.scene 包中,提供了一个抽象的 LightBase 超类和“concrete”(可在代码中作为可构造的对象使用)子类 AmbientLight 和 PointLight。另一个是抽象的 Light 超类,包含在 javafx.scene.effect 包中;这个包包含 2D 数字成像效果,正如我们在本书前面提到的。对于 3D 的使用,我们将重点放在灯光基础,环境光线和点光源类上,最初使用点光源类,因为我们可以使用该类获得最生动和真实的效果。

JavaFX LightBase 类:定义光的抽象超类

公共 JavaFX LightBase 超类是一个抽象类,仅用于创建不同类型的灯光。目前,3D 场景有一个普通或“环境”级别的照明,由模拟灯泡属性的 AmbientLight 子类(对象)或 PointLight 子类(对象)提供。您的应用不应试图直接扩展抽象的 LightBase 类;如果您尝试这样做,Java 将抛出一个 UnsupportedOperationException,您的 pro Java 9 游戏将无法编译或运行。LightBase 类保存在核心 javafx.scene 包的 javafx.graphics 模块中,是 Node 的子类,因为它最终是 SceneGraph 顶部的一个节点。LightBase 类实现了一个 Styleable 接口,这样它就可以被样式化,还实现了一个 EventTarget 接口,这样它就可以处理事件。因此,JavaFX LightBase 类的 Java 9 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.LightBase

LightBase 类为构造对象的子类提供了通用属性的定义,这些对象用于在 3D 场景中表示(“投射”)某种形式的光。这些 LightBase 对象属性应该包括光源的初始颜色,以及光源最初是打开(启用)还是关闭(禁用)。请务必注意,由于这是一个 3D 特征,因此它是一个有条件的特征。参考我在本章的 PerspectiveCamera 一节中给出的例子,了解如何设置代码来检测 ConditionalFeature。SCENE3D 标志。

LightBase 子类有两个属性(或者属性或特征,如果你喜欢这些术语的话);一个是 color 或 ObjectProperty ,它指定从光源发出的光的颜色,第二个是一个名为 lightOn 的 BooleanProperty,它允许打开和关闭光。

LightBase 抽象类有两个重载的受保护构造函数方法。一个没有参数,创建默认颜色。白光源,使用此构造函数方法调用格式:

protected LightBase()

第二个重载的受保护构造函数方法允许子类使用下面的构造函数方法调用格式为灯光指定颜色值:

protected LightBase(Color color)

LightBase 类有七个方法,每个 LightBase 子类都可以使用(继承)这些方法,包括 AmbientLight 和 PointLight 子类,所以在这里要注意这些方法,因为我只介绍一次。

colorProperty()方法指定光源的 ObjectProperty ,而 getColor()方法获取光源的颜色值属性。getScope()方法将获得一个 ObservableList ,其中包含一个节点列表,该列表指定了 LightBase 子类(object)的层次结构范围。

isLightOn()方法调用返回光源的布尔值 On(真)或 off(假),lightOnProperty()方法调用将设置光源 BooleanProperty lightOn 的布尔数据值。

最后,void setColor(Color value)方法将设置 light color 属性的数据值,void setLightOn(boolean value)方法将设置 LightBase 子对象 lightOn boolean value 属性的数据值。

接下来,让我们分别仔细看看 AmbientLight 和 PointLight 具体类。

JavaFX AmbientLight 类:均匀地照亮 3D 场景

公共 JavaFX AmbientLight 类是一个具体的类,用于为 3D 场景创建常规或“环境”级别的照明。对于给定的 3D 场景实例,通常只定义一个 AmbientLight 实例。AmbientLight 类保存在核心 javafx.scene 包的 javafx.graphics 模块中,是 LightBase 的子类,light base 是 Node 子类,因为它最终是 SceneGraph 顶部的节点。AmbientLight 类还实现了一个 Styleable 接口,以便可以对其进行样式化,还实现了一个 EventTarget 接口,以便可以处理事件。因此,JavaFX AmbientLight 类的 Java 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.LightBase
      > javafx.scene.AmbientLight

如果需要,AmbientLight 类为 3D 场景定义环境光源对象。环境光可以被定义为来自看起来从每个方向进入场景的看不见的光源对一个区域的整体或一般的照明量。所有 AmbientLight 对象属性都继承自 LightBase 超类,并且应该包括光源的初始颜色以及光源最初是打开(启用)还是关闭(禁用)。同样需要注意的是,由于这是一个 3D 特征,因此它是一个有条件的特征。

AmbientLight 有两个重载的构造函数方法;第一个使用(默认)颜色创建一个未配置的 AmbientLight 对象类。白色光源,使用以下 Java 实例化编程格式:

AmbientLight ambient = new AmbientLight();

第二个重载构造函数方法使用非 color 的指定颜色创建一个新的 PointLight 实例。白色,使用以下 Java 实例化编程格式:

AmbientLight ambientaqua = new AmbientLight(Color.AQUA);

接下来,让我们详细了解一下点光源的具体类,然后我们可以在本章结束之前将点光源对象添加到您的 3D 场景中,在本书的剩余部分中,我们可以将 3D 对象放入 3D 渲染场景环境中。

JavaFX PointLight 类:戏剧性地照亮你的 3D 场景

公共 JavaFX PointLight 类是一个具体的类,用于为 3D 场景创建局部或“点源”照明实例。3D 场景中通常有多个点光源实例,以允许艺术家实现模拟真实世界光源的复杂照明模型。PointLight 类保存在核心 javafx.scene 包的 javafx.graphics 模块中,是 LightBase 的子类,light base 是 Node 子类,因为它最终是 SceneGraph 顶部的节点。PointLight 类还实现了一个 Styleable 接口,以便可以对其进行样式化,还实现了一个 EventTarget 接口,以便可以处理事件。因此,JavaFX PointLight 类的 Java 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.LightBase
      > javafx.scene.PointLight

PointLight 类根据需要为 3D 场景定义点光源(比如灯泡)对象。尝试使用尽可能少的点光源对象,因为它们的渲染成本很高(计算或处理它们的算法)。点光源被定义为局部光发射点,可以制作动画来创建各种特殊效果。所有点光源对象属性都继承自 LightBase 超类,并且应该包括光源的初始颜色以及光源最初是打开(启用)还是关闭(禁用)。同样需要注意的是,由于这是一个 3D 特征,因此它也是一个条件特征。

聚光灯有两个重载的构造方法。第一个使用默认颜色创建一个未配置的点光源对象类。白色光源,使用以下 Java 实例化编程格式:

PointLight light = new PointLight();

第二个重载构造函数方法使用非 color 的指定颜色创建一个新的 PointLight 实例。白色,使用以下 Java 实例化编程格式:

PointLight aqualight = new PointLight(Color.AQUA);

接下来,让我们仔细看看添加一个 PointLight 对象作为 JavaFXGame 类基础设施的光源的工作过程。

为游戏的 3D 场景添加灯光:使用点光源对象

接下来,让我们在 JavaFXGame 代码中添加一个点光源,这样我们就可以在下一章学习 3D 图元了。在 JavaFXGame 类顶部声明一个名为 Light 的 PointLight 对象;然后使用 Alt+Enter 组合键弹出帮助器弹出窗口,选择“为 javafx.scene.PointLight 添加导入”选项,如图 11-16 底部用黄色和蓝色突出显示。

A336284_1_En_11_Fig16_HTML.jpg

图 11-16。

Declare a PointLight named light at the top of your JavaFXGame class and hit Alt+Enter and Add Import.

因为你还需要灯光照明,在你的点光源声明后添加一个球体声明,这样我们就有东西来测试我们的代码了,如图 11-17 顶部用黄色突出显示的。接下来,在 scene.setCamera(相机)之后实例化你的点光源;方法调用。我使用了第二个更显式的构造函数方法,但给了它默认的颜色。白色,在我们看了材质以及它们如何与光色值相互作用之后,我们可能会改变它。使用light.setTranslateZ(-25);方法调用将灯光向下移动一点,使其不在球体内部(在 0,0,0 处)。接下来,使用一个light.getScope().add(sphere);方法链,将球体对象添加到点光源对象“看到”的范围内请注意,这允许您让不同的灯光对象影响 3D 场景中的不同 3D 对象,这是一个非常强大的功能。图 11-17 底部突出显示了点光源和球体对象声明、实例化和配置 Java 语句的 Java 代码,应该类似于以下 Java 代码:

A336284_1_En_11_Fig17_HTML.jpg

图 11-17。

Declare a sphere and light object, and in createBoardGameNodes instantiate and configure them for use.

PointLight light;
Sphere sphere;
...
private void createBoardGameNodes() {
    ...
    light = new PointLight(Color.WHITE);
    light.setTranslateY(-25);
    light.getScope().add(sphere); // "Wire" the Sphere and Light together via .getScope().add()

    sphere = new Sphere(100);
    ...
}

你需要做的最后一件事是将你的点光源和球体连接到彼此,连接到相机,连接到场景图的游戏板 3D 分支。getChildren()。在您的。addNodesToSceneGraph()方法。

图 11-18 中间突出显示了 addNodesToSceneGraph()方法 Java 语句的代码,看起来应该是这样的 Java 方法体:

A336284_1_En_11_Fig18_HTML.jpg

图 11-18。

Add the sphere to the gameBoard branch of the root Node so the primitive is added to your scenegraph.

private void addNodesToSceneGraph() {
    root.getChildren().addAll(gameBoard, uiLayout);
    gameBoard.getChildren().add(sphere);
    uiLayout.getChildren().addAll(boardGameBackPlate, logoLayer, infoOverlay, uiContainer);
    ...
}

使用您的“运行➤项目”工作流程,确保我们在本章中完成的所有代码升级和添加都正常工作,并为您提供在 pro Java 9 游戏开发的早期阶段应该预期的最终结果。

确保三个(中间)游戏规则、免责声明和游戏积分按钮已经恢复到最大工作容量(现在再次填充白色背景)。此外,在 NetBeans 9 中,“高分”按钮仍然应该向输出控制台打印出一条文本消息,并且“开始游戏”现在应该移除 StackPane uiLayout 覆盖面板并显示 3D 场景。

3D 场景中应该有一个 3D 球体对象图元,我们将在下一章学习 3D 场景中的 3D 对象,在 3D 行业中称为模型、几何体、网格和图元。因为 3D 图元还没有纹理映射或颜色值,并且因为点光源对象被设置为颜色。白色,这应该是一个被白光照亮的浅灰色球体。

正如你在图 11-19 中看到的,开始游戏按钮控件现在隐藏了闪屏、UI 设计、按钮控件、文本流、文本元素和格式的整个 2D 合成管道,只需要一个简单的 Java 语句uiLayout.setVisible(false);,因为我们已经在本书中设置了 StackPane 父节点和 VBox、ImageView 和 TextFlow 子层次结构。一旦管道从视图中隐藏,我们就可以看到 3D 场景,为了测试 PerspectiveCamera 和 PointLight 对象,我们临时添加了一个球体对象原语。

A336284_1_En_11_Fig19_HTML.jpg

图 11-19。

Use your Run ➤ Project work process and test the 3D Scene infrastructure that you have put into place

我们现在能够使用 JavaFX APIs 进行 3D 建模和 3D 纹理映射。

摘要

在第十一章中,我们通过添加 PerspectiveCamera 对象为JavaFXGame.java添加了 3D 场景功能,该对象允许使用 X、Y 和 Z 维度以及场景对象的 3D 透视图来渲染 3D 素材。我们还添加了一个点光源对象来模拟灯泡光源来照亮这些 3D 素材,以及一个球体对象(“基本体”)来测试我们的基本 3D 场景设置。

您学习了抽象的 Camera 超类及其 ParallelCamera(用于 2D 或正交 3D 场景)和 PerspectiveCamera,我们将使用它们进行最有效的 3D 或 i3D 场景渲染。然后,我们学习了如何在 JavaFXGame 中声明、实例化和配置 PerspectiveCamera,改变它的操作方式。

然后,我们测试了我们的 2D UI 元素和层次结构,并观察到它们现在位于 3D 空间中的 2D“平面”上。我们修改了 Java 代码来补偿坐标空间的变化,将用户界面恢复为全屏。

然后,我们测试了所有的 UI 按钮对象,发现我们新的 3D 场景黑色背景颜色影响了我们的信息屏幕,并且非常巧妙地使用了一个复杂的嵌套 Java 语句来创建和插入颜色。白色背景将对象填充到 StackPane 对象的背景对象中。这解决了这个问题,用白色填充替换了一个合成层的透明度,并在我们现在的混合 3D 和 2D 合成管道中添加了另一个不透明层。这个问题解决后,我们改变了 gameButton 事件处理程序中的逻辑,允许最终用户通过隐藏 UI 覆盖来启动游戏,并显示正确点亮的测试球体原语。

在下一章中,我们将了解 JavaFX Shape3D 超类及其子类,继续学习创建 pro Java 9 游戏的 i3D 部分所需的基础知识。

十二、3D 模型设计和原语:使用 JavaFX 9 Shape3D

现在,您已经通过将相机对象添加到场景根和专门设计用于 3D 资源的点光源对象完成了基本(空)3D 场景的设置,让我们开始了解一些关于 3D 资源本身的基础知识。这些素材以预定义的基本 3D 形状(称为图元)以及更多定制 3D 几何素材(业内通常称为网格或线框 3D 素材)的形式出现。JavaFX 9 在 javafx.graphics 模块的javafx.scene.shape包中提供了七个类,专门为您创建 3D 几何图形(图元或网格),我们将在本章中了解它们。在第十二章中,我们还将回到 JavaFXGame 主应用类编码,并开始向场景图的游戏板组节点添加 3D 图元,以练习向 JavaFXGame 应用添加 3D 素材。虽然我们可以在 Blender 等 3D 软件包中完成这项工作,但棋盘游戏非常简单(正方形、球形、圆柱形),我们完全可以在 JavaFX 代码中完成这项工作,这意味着我们不需要导入(和分发)3D 模型,而是可以编写代码来“凭空”模拟您的 i3D 游戏。这也将教会您更多关于 Java 9 和 JavaFX 9 中的 3D APIs,因为您可以学习如何仅使用最新的 Java 和 JavaFX APIs 来建模复杂的对象(例如您的棋盘游戏的游戏板)。

在本章中,您将了解 javafx.scene.shape 包中包含的不同类型的 JavaFX 3D 类。我们将讨论球体,它可以用来创建一个球体,你已经在第十一章中使用它来测试你的 3D 场景设置。我们还将看看另外两个基本类,Box 和 Cylinder,它们可以用来创建平面和圆盘基本体。这些原语基于 Shape3D 超类,我们将首先研究这个超类。我们还将了解更高级的 TriangleMesh 类,它允许您构建基于多边形的网格对象,最后是 Mesh 和 MeshView 类层次,它允许您渲染在外部 3D 建模和渲染软件包(如 Blender 2.8(开源)或 Autodesk 3D Studio Max(付费软件包))中创建的 3D 网格对象。

JavaFX Shape3D 超类:图元或网格视图

公共抽象 Shape3D 超类用于创建四个主要的 3D 类:长方体、球体、圆柱体和网格视图。您将使用这些类为您的 pro Java 9 游戏开发创建和显示 3D 素材。其中三个子类创建图元,这些图元是通过算法创建的预定义 3D 对象,MeshView 子类允许在 3D 场景中渲染基于多边形几何体的更详细的复杂 3D 模型。需要注意的是,还有一个 javafx.scene.shape.Shape 超类与 javafx.scene.shape.Shape3D 不相关(类层次结构方面);它用于 2D 形状,如 SVG 2D 数字插图语言中常见的形状,这在 Java 8 游戏开发入门(Apress,2014)和数字插图基础(Apress,2016)中有所介绍。

Shape3D 超类是 Node 的子类,正如我们将在 JavaFXGame 代码中使用的大多数具体类一样。像 Camera 和 LightBase 超类一样,这个 Shape3D 超类实现了 Styleable 和 EventTarget 接口,这样它的子类(对象)就可以被样式化和处理事件(可以是交互式的)。因此,Java 9 类层次结构跨越了 Java 和 JavaFX APIs,如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.shape.Shape3D

创建 Shape3D 基类(抽象的或未直接实例化的)是为了给表示 3D 几何形状的 3D 对象提供公共属性的定义。三个主要的 3D 属性包括“材质”(或着色器和纹理贴图),应用于形状的可填充内部或形状的轮廓,我们将在第十三章中讨论;“绘制模型”属性,定义 JavaFX 9 渲染引擎如何向查看者呈现几何图形(作为实体或线框模型);以及定义要剔除哪些面的“面剔除”属性。面剔除是一种优化,渲染引擎将通过不渲染场景中模型中的所有多边形来获得更好的性能(更快的 FPS)。由于渲染器正在从相机拍摄 3D 场景并渲染 2D 视图,因此这种“背面剔除”不会渲染模型背向相机(对相机不可见)的部分上的任何面(多边形)。正面剔除将进行相反的操作,只渲染背面的多边形,这基本上是渲染多边形的内部,模型正面(多边形)变得隐藏或不可见。还有 CullFace。关闭面剔除优化算法的 NONE 常量。卡夫斯。“后退”是默认设置,也是您通常想要使用的设置,除非您使用的是 CullFace。前面得到一些特殊的内部体绘制效果,这一章结束后,你会知道如何实验,如果你愿意的话。

如你所知,3D 渲染,以及 Shape3D 的任何子类,都是一个条件特性,你可以在你的代码中检查,就像我们在上一章中提到的那样。让我们深入了解任何 Shape3D 子类对象的三个对象设置(属性、特性、特征),它们定义了 3D 渲染引擎将如何渲染它。

cullFace 对象属性将定义哪个 CullFace 优化算法(前、后或无)将用于此 Shape3D 对象。这很可能会影响专业 Java 9 3D 游戏的性能。

drawMode ObjectProperty 将定义用于呈现 Shape3D 对象的绘制模式。您的两个选项包括 DrawMode。实心 3D 对象的填充和绘制模式。线框表示的线。

材质对象属性定义了 Shape3D 对象将用作“皮肤”的材质我们将在第十三章中学习所有关于着色算法、材质和纹理贴图的知识,这一章涵盖了材质。

抽象 Shape3D 超类的受保护(不可直接使用)构造函数如下所示:

protected Shape3D()

现在让我们进入将成为所有 Shape3D 子类的一部分的方法。这很方便,因为我们可以在一个地方涵盖所有这些方法。这些可以用在任何原始三维形状或网格视图。

那个。cullFaceProperty()方法为 Shape3D 对象定义 ObjectProperty ,而。getCullFace()方法允许您轮询 Shape3D 对象的当前 CullFace 常量设置。还有就是。setCullFace(CullFace value)方法,该方法允许您更改 Shape3D 对象的 CullFace 常量设置。

的。drawModeProperty()方法为 Shape3D 对象定义 ObjectProperty ,而。getDrawMode()方法允许您轮询 Shape3D 对象的当前 DrawMode 常量设置。还有就是。setDrawMode(DrawMode 值)方法,该方法允许您更改 Shape3D 对象的 DrawMode 常量设置。

那个。materialProperty()方法为 Shape3D 对象定义 ObjectProperty ,而您的。getMaterial()方法允许您轮询 Shape3D 对象的当前材质对象设置。还有就是。setMaterial(Material value)方法,该方法允许您更改 Shape3D 对象的材质对象设置。

接下来,让我们单独看看 Shape3D 子类,因为我们将在 JavaFXGame 中利用它们。

JavaFX 球体:为 3D 游戏创建球体图元

因为我们已经在前一章中创建了一个名为 Sphere 的球体对象来测试 PerspectiveCamera 和 PointLight 3D 场景设置 Java 代码,所以让我们先来看看 Shape3D 子类。该类保存在 javafx.scene.shape 包中,是 Shape3D 的子类,因此它具有以下 Java 类层次结构:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.shape.Shape3D
      > javafx.scene.shape.Sphere

Sphere 类定义了一个具有指定大小的三维球体。球体是使用程序员输入的半径尺寸(大小)通过算法创建的 3D 几何图元。该球体最初总是以 3D 原点 0,0,0 为中心。因此,球体对象有一个定义球体半径的 radius DoubleProperty 以及三个从 javafx.scene.shape.Shape3D 继承的 cullFace、drawMode 和 material 属性。

Sphere 类包含三个重载的构造函数方法,其中一个没有参数,它创建一个半径为 1.0 的球体实例。这看起来像下面的 Java 9 Sphere 实例化:

sphere = new Sphere();

第二个构造方法,也就是我们在第十一章中使用的方法,允许你使用双数值指定半径。这类似于下面的球体实例化 Java 代码:

sphere = new Sphere(100);

第三个构造函数允许您通过多个分段参数指定半径和网格密度,类似于下面的 Java 语句,该语句创建了一个半径为 100 单位、分段数为 24 的球体:

sphere = new Sphere(100, 24)

除了从 Shape3D 类继承的方法之外,Sphere 类还有一些自己独特的方法,包括。getDivisions()方法,该方法轮询球体对象以查看它使用了多少个分区;那个。radiusProperty()方法,它定义了球体对象的半径;那个。getRadius()方法,获取当前半径的值;还有。setRadius(double value)方法,该方法将半径值设置为不同的值。

JavaFX 圆柱体:为游戏创建圆柱体或磁盘图元

接下来,让我们来看看公共的 Cylinder Shape3D 子类,它可以用来创建圆柱形 3D 对象,因为它是一个具体的(可用的)类,也实现了 Styleable 和 EventTarget 接口。该类保存在 javafx.scene.shape 包中,是 Shape3D 的子类,因此它将具有以下 Java 类层次结构:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.shape.Shape3D
      > javafx.scene.shape.Cylinder

Cylinder 类用于定义具有指定半径和高度的三维圆柱体。圆柱体是一种 3D 几何图元算法,采用半径(double)属性和高度(double)属性。它最初以 0,0,0 原点为中心,半径使用 z 轴方向,高度使用 y 轴方向。

除了半径和高度属性,它还将继承 Shape3D cullFace、drawMode 和 material 属性。它有三个重载的构造函数方法,一个是默认的(空),一个是半径和高度,第三个是半径、高度和等份。

第一个空构造函数方法创建一个半径为 1.0、高度为 2.0 的圆柱体对象的新实例。它具有以下 Java 语句格式:

cylinder = new Cylinder();

第二个构造函数方法使用开发人员指定的半径和高度创建一个圆柱体对象的新实例。它具有以下 Java 语句格式:

cylinder = new Cylinder(50, 250);

第三个构造函数方法使用开发人员指定的半径、高度和分辨率(确定平滑度的分割数)创建一个圆柱体对象的新实例。它具有以下 Java 语句格式:

cylinder = new Cylinder(50, 250, 24);

半径有三种方法,高度有三种方法,还有一种。getDivisions()方法用于轮询 Divisions 属性,该属性必须使用第三个构造函数方法格式进行设置,因为没有。setDivisions()方法调用或 divisionsProperty()方法调用。

双份。getHeight()方法将轮询(获取)圆柱体对象的 Height 属性的值。DoubleProperty heightProperty()方法定义圆柱体对象的高度属性或 Y 维度。最后,void setHeight(double value)方法允许开发人员设置圆柱体对象的 Height 属性值。

double getRadius()方法将轮询(获取)圆柱体对象的 Radius 属性的值。DoubleProperty radiusProperty()方法定义圆柱体对象的半径属性或 Z 维度。最后,void setRadius(double value)方法允许开发人员为圆柱体对象设置 Radius 属性的值。

最后,让我们来看一个 Box 原语类,它允许创建各种有用的形状。

JavaFX 盒子:为 3D 游戏创建盒子、柱子和平面

接下来,让我们来看看 public Box Shape3D 子类,它可以用来创建正方形、矩形和平面 3D 对象,因为它是一个具体的(可用的)类,也实现了 Styleable 和 EventTarget 接口。该类保存在 javafx.scene.shape 包中,是 Shape3D 的子类,因此它将具有以下 Java 类层次结构:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.shape.Shape3D
      > javafx.scene.shape.Box

Box 类定义了一个三维盒子,通常称为立方体图元,具有指定的大小。Box 对象是一个 3D 几何基本体,除了三个继承的 cullFace、drawMode 和 material Shape3D 属性之外,还具有三个 double 属性(深度、宽度和高度)。在实例化时,它最初以原点为中心。

Box 类有两个重载的构造方法。一个创建默认的 2,2,2 立方体,看起来像下面的 Java 代码:

box = new Box();

第二种构造函数方法允许您指定多维数据集的维度,如下所示:

box = new Box(10, 200, 10); // Creates a Post (or Tall Rectangle) Primitive
box = new Box(10, 0.1, 10); // Creates a Plane (or a Flat Surface) Primitive

正如您可能已经猜到的,Box 类中有九个可用的方法,每个属性三个。这是我们将用来创建大部分游戏板基础设施的类,所以我们可能会经常使用它们。

DoubleProperty depthProperty()方法用于定义盒子的深度或 Z 维度。double getDepth()方法可用于从 Box 对象获取(轮询)深度属性的值。void setDepth(double value)方法可用于为 Box 对象的 Depth 属性设置或指定新值。

DoubleProperty heightProperty()方法用于定义框的高度或 Y 维度。double getHeight()方法可用于从 Box 对象获取(轮询)Height 属性的值。void setHeight(double value)方法可用于设置或指定 Box 对象的 Height 属性的新值。

DoubleProperty widthProperty()方法用于定义宽度,即盒子的 X 维度。double getWidth()方法可用于从 Box 对象获取(轮询)Width 属性的值。void setWidth(double value)方法可用于设置或指定 Box 对象的 Width 属性的新值。

接下来,让我们看看在 JavaFXGame 代码中实际实现不同的原语需要做些什么!

使用原语:向 JavaFXGame 类添加原语

让我们将另外两个基本对象 Box 和 Cylinder 添加到 JavaFXGame 类中,这样我们就可以了解面剔除和绘制模式。我们将为它自己的章节 13 保存材料,因为着色器和纹理贴图值得它们自己的章节和重点讨论。在类的顶部声明一个名为 box 的 Box 对象,并使用 Alt+Enter 让 NetBeans 9 帮助您编写导入语句。正如您在图 12-1 中所看到的,将正确的类添加到您的 Java 9 游戏中是非常重要的,因为还有一个 javax.swing.Box 类(在弹出助手下拉列表中排在第二位)用于 2D UI 设计,并且在列表的顶部是(NetBeans 最佳猜测)用作 3D 原语的 javafx.scene.shape.Box!双击第一个(正确的)类,让 NetBeans 为您编写导入语句。

A336284_1_En_12_Fig1_HTML.jpg

图 12-1。

Declare a Box object at the top of the class; use Alt+Enter, and select Add import for javafx.scene.shape.Box

使用第二个构造函数在 createBoardGameNodes()方法中实例化 box 对象,如图 12-2 所示。请记住,您需要将这个 box 节点添加到。addNodesToSceneGraph()方法。

A336284_1_En_12_Fig2_HTML.jpg

图 12-2。

Instantiate the Box in createBoardGameNodes, and set the depth, height, and width to 100, 100, 100

这可以通过将您当前的gameBoard.getChildren().add(sphere); Java 语句修改为gameBoard .getChildren(). addAll (sphere, box );来轻松实现,如下图所示和图 12-3 :

A336284_1_En_12_Fig3_HTML.jpg

图 12-3。

Use the .addAll() method to add a box object to the SceneGraph in the addNodesToSceneGraph() method

box = new Box(100, 100, 100);                   // in .createBoardGameNodes() method

gameBoard.getChildren().addAll(sphere, box);   //  in .addNodesToSceneGraph() method

声明 Box 对象后,在 createBoardGameNodes()方法中实例化该对象,使用与球体相同的 100 单位值。您将能够看到大小之间的关系,因为它们都是在 0,0,0 处创建的。对于 Box 构造函数方法,这需要三个(double)值,它们都应该是 100。

接下来,在类的顶部声明一个名为 pole 的圆柱体,并在。createBoardGameNodes()方法,使用 50 的宽度、250 的高度和 24 的用于网格(线)绘制表示的部分或分割的数量。

这应该看起来像下面的 Java 代码,在图 12-4 中用黄色和蓝色突出显示:

A336284_1_En_12_Fig4_HTML.jpg

图 12-4。

Create a Cylinder object named pole and instantiate it with a radius of 50, a height of 250, and 24 divisions

Cylinder pole;                        // Declare object for use at the top of your class
...
pole = new Cylinder(50, 250, 24);     // in .createBoardGameNodes() method

如果您在此时使用 Run ➤项目工作流程,您将看不到 pole 对象,因为您还没有将它添加到 JavaFX 场景图中。打开 addNodesToSceneGraph()方法,将 pole 对象添加到参数区域(parens)内包含的 Java 列表的末尾。这都是通过在。handle()方法,如图 12-5 中间高亮显示:

A336284_1_En_12_Fig5_HTML.jpg

图 12-5。

Add a pole Cylinder object to SceneGraph, at the end of the gameBoard.getChildren().addAll() method call

gameBoard.getChildren().addAll(sphere, box, pole);    // in addNodesToSceneGraph() method

正如您将看到的,当我们在 3D 场景中渲染此代码时,您在 3D 合成中将对象添加到场景图的顺序与 2D 合成堆栈中的 2D 素材层顺序类似,因为 3D 图元将显示为“在彼此前面”越晚将对象添加到场景图中的游戏板组,越晚将它们渲染到屏幕上。因此,添加到场景图的最后一个图元将被渲染在它之前的所有其他图元之上,而添加到场景图的第一个图元将首先被渲染(即,在所有其他 3D 图元的下面或后面)。

在大多数 3D 软件包中,位于 0,0,0(场景中心)的三个图元将在彼此内部进行渲染。这告诉我们一些关于 JavaFX 的非常重要的 3D 艺术家的事情,那就是你不能使用 JavaFX 原语执行建设性的实体几何(CSG)建模。CSG 是 3D 建模的早期形式之一,涉及使用基本的 3D 图元结合布尔运算来创建更复杂的 3D 模型。

让我们使用您的运行➤项目工作流程,看看 JavaFX 是如何渲染这三个位于 0,0,0 的原语的。如图 12-6 所示,圆柱体对象在一个盒子对象的前面,盒子对象在一个球体对象的前面。大多数 3D 软件包会将其渲染为球体内部的一个盒子,可能盒子的角穿过球体(取决于比例),圆柱体的末端会从球体的顶部和底部出来。我按照这个特定的顺序做了这个练习,因为对于开发人员来说,在构建 Java 9 游戏时,意识到他们能做什么和不能做什么是至关重要的。您可以在 JavaFX 中实现这种布尔效果,方法是使用从三维建模器(如 MOI3D、SILO 或 Blender)导入的网格对象,其中布尔操作已在 JavaFX 9 外部完成。

A336284_1_En_12_Fig6_HTML.jpg

图 12-6。

Use the Run ➤ Project to see these three primitives in the Z-order that you added them to the SceneGraph

接下来,让我们使用一些 3D 图元修改(移动和旋转)方法调用,将它们从中心场景移开,并旋转立方体,使其看起来不像 2D 对象。这都可以通过使用。setTranslateX()和 setRotate()方法调用 box 和 pole 对象,如图 12-7 底部所示:

A336284_1_En_12_Fig7_HTML.jpg

图 12-7。

Use setTranslateX(250) to move primitives 250 units apart and use setRotate(45) to rotate the box 45 degrees

box.setTranslateX(500);
box.setRotate(45);
pole.setTranslateX(250);

接下来,使用“运行➤项目”工作流程来分别查看基本数据。如图 12-8 所示。setRotate()方法使用 z 轴进行旋转,因此您的 3D 对象仍然呈现为 2D 对象。让我们解决这个问题!

A336284_1_En_12_Fig8_HTML.jpg

图 12-8。

All three primitives are now spaced apart; the box still looks 2D

要更改您的。setRotate()方法用来配置它的旋转算法,还有第二个。setRotationAxis()方法,可用于更改默认旋转。旋转的 z 轴设置。x 轴常量,正如你在 Rotate 类中看到的点符号。

显然,正如您现在所了解的那样。setRotationAxis()方法调用必须在。setRotate(45)方法调用,以便在实际使用旋转算法之前改变旋转轴。

在 box . settranslate(500)之后添加一个. setRotateAxis()方法调用 off 您的 box 对象;方法调用,使用旋转。用于配置旋转算法的 X_AXIS 常量。Java 语句序列应该类似于下面的 Java 代码,可以在图 12-9 的底部看到:

A336284_1_En_12_Fig9_HTML.jpg

图 12-9。

Add a .setRotationAxis() method call off box after the box.setTranslateX(500); and set it to Rotate.X_AXIS

box.setTranslateX(500);
box.setRotationAxis(Rotate.X_AXIS);
box.setRotate(45);
pole = new Cylinder(50, 250, 24);
pole.setTranslateX(250);

接下来,使用运行➤项目工作流程,再次单独查看您的原语。如图所示,在图 12-10 中。setRotate()方法现在使用 z 轴进行旋转,因此您的 3D 对象现在呈现为 3D 对象,您可以看到阴影(不同面上的颜色或亮度差异)。

A336284_1_En_12_Fig10_HTML.jpg

图 12-10。

Now all primitives are oriented in such a way that their default light gray shading is visible in the renderer

随着本书的深入,我们将了解 JavaFX 中旋转 3D 对象的更复杂的方法,因为旋转在 3D 中是一个非常复杂的主题,似乎不是“在表面上”(没有双关语)。旋转在它的算法中使用了比转换更复杂的数学体系,其中一些复杂性会渗透到表面,因此必须由所有专业 Java 9 3D 游戏开发人员来处理和理解。

现在我们已经将 JavaFX 中提供的三个基本图元分开,并以一种在渲染视图中向您显示更多面和边的方式面对,我们将进一步了解面剔除和绘制模式对几何图形的影响。我们将在第十三章中为自己保存实物的创建和应用;材质对象创建是一个核心的 3D 主题(纹理映射),应该作为它自己的主题来对待,因为 3D 对象的着色决定了它的视觉质量。

接下来,让我们看看绘制模式(在大多数 3D 软件包中称为渲染模式),以便您在开发 pro Java 9 游戏时可以查看对象的 3D 线框表示。

Shape3D 绘制模式属性:实体几何图形和线框

现在我们已经在屏幕上排列了三个主要的 JavaFX 原语,让我们看看 Shape3D 超类的 drawMode 属性,它由这些原语中的每一个继承。这个属性使用了 DrawMode 类中的一个常量,您可能已经猜到了,目前可用的两个常量是 DrawMode。FILL 和 draw mode . LINE。FILL 常量为您提供实体模型几何图形表示,LINE 常量为您提供线框模型几何图形表示。我们将使用。本节中的 setDrawMode(drawMode)方法调用将我们的三个图元从实体模型更改为线框模型,以便我们可以更改线框的分辨率或分割,并查看这样做的效果,以便我们可以围绕 X 维度旋转球体,查看其线框构造的外观以及分割属性如何更改它在 3D 场景中的外观(渲染)。然而,首先,我有点厌倦了在 3D 场景的左上角看这些图元,所以我们将使用。setTranslateZ(-500)将相机对象拉近 100%(或将基本体放大 100%),并使用。setTranslateY(300)方法将图元置于视图的水平中心。稍后我们将使用。settranslate(-300)方法调用使图元在视图的垂直中心居中。

打开你的。start()方法和 gameButton 事件处理代码块,并更改。setTranslateZ()方法调用值从-1000 到-500。然后添加一个. setTranslateY()方法调用关闭 camera 对象并传递给它一个-300 场景单位的数据值,如图 12-11 以及下面的 Java 代码语句所示:

A336284_1_En_12_Fig11_HTML.jpg

图 12-11。

Zoom the camera object in 100 percent using .setTranslateZ(-500), and move it down with .setTranslateY(-300)

camera.setTranslateZ(-500);
camera.setTranslateY(-300);

接下来,让我们打开 createBoardGameNodes()方法,并为每个图元添加一个. setDrawMode (DrawMode. LINE )方法调用,将它们的渲染模式从立体几何设置为线框几何,这样我们就可以看到它们的底层结构。在图 12-12 中用黄色突出显示的 Java 语句应该如下所示:

A336284_1_En_12_Fig12_HTML.jpg

图 12-12。

Set the drawMode property to LINE for all primitives with a .setDrawMode(DrawMode.LINE) method call

sphere.setDrawMode(DrawMode.LINE);
box.setDrawMode(DrawMode.LINE);
pole.setDrawMode(DrawMode.LINE);

接下来,使用运行➤项目工作流程,再次查看您的原语。正如你所看到的,在图 12-13 中,你的图元正在使用线框表示法进行渲染,并在 3D 场景中的 Y 维度上居中。

A336284_1_En_12_Fig13_HTML.jpg

图 12-13。

All three primitives are now rendered in wireframe mode and are centered vertically

使用将相机置于 X 维度的中心。使用下面的代码 setTranslateX(),如图 12-14 所示:

A336284_1_En_12_Fig14_HTML.jpg

图 12-14。

Add a .setTranslateX(-300) to move your primitives to the vertical (X dimension) center of your 3D Scene

camera.setTranslateX(-300);

请注意,当您移动相机对象时,它保持直视前方,而在 3D 软件包中,有一个相机“目标”保持锁定在场景的中心或场景中的 3D 对象上。在 JavaFX 中,相机对象是用一条直线(称为光线或向量)固定的,这条直线从相机的后面穿过前面,沿着相机对象指向的方向延伸到无穷远处。所以,在 3D 软件中,如果你向上移动相机,它的视野向下旋转,相机和它的主体之间有一个链接(或线)。

如果您想要 JavaFX 中的这种行为,您必须手动旋转相机,因为 JavaFX 相机超类当前没有指定目标属性(目标功能)。随着本书的进展,我们将关注 PerspectiveCamera 对象以及如何在 3D 场景中以更高级的方式利用它,因为相机是 i3D 场景的一个重要方面,也是 pro Java 9 i3D 游戏开发过程中使用的一个重要工具。

在我们再次渲染 3D 场景之前,因为我们从代码中知道它现在将会很好地居中,足以让我们查看分割和面剔除等属性,并了解这些属性如何影响组成 3D 图元的多边形,所以让我们使用重载的(第二个)球体(大小,分割)构造函数方法格式,并降低球体对象的网格分辨率,以优化保存该 3D 对象所需的内存量。您还将向前旋转它,以便可以看到球体构造的顶部,同时将圆柱体的分辨率降低 100%,从 24 等份减少到 12 等份。我总是使用可被 4 整除的除法值(90 度乘以 4 是 360),如果打开面剔除,一半的除法甚至都没有渲染。这些都可以通过使用以下 Java 语句来完成,这些语句在图 12-15 中(和底部)突出显示:

A336284_1_En_12_Fig15_HTML.jpg

图 12-15。

Construct your Sphere with 12 divisions, X rotate it 90 degrees, and reduce your Cylinder to 12 divisions

sphere = new Sphere(100, 12);
sphere.setRotationAxis(Rotate.X_AXIS);
sphere.setRotate(90);
sphere.setDrawMode(DrawMode.LINE);

现在是时候使用运行➤项目的工作过程,并渲染我们的场景。正如你在图 12-16 中看到的,我们的 3D 图元更接近 3D 场景的中心,更容易查看,并且使用更少的数据来构建。正如你在图 12-13 中看到的,这个球体使用了 48 个分区来构建。这使用了几百个多边形,可以计算为 48×48×2 = 192;192 个多边形需要大量内存来处理(存储和渲染),因为每个多边形都有大量数据来定义它(模型中的位置、大小、方向、颜色、法线方向、平滑组)。

A336284_1_En_12_Fig16_HTML.jpg

图 12-16。

Your primitives are now centered in the 3D Scene Camera view, and you can see the Sphere construction

当我们在下一节面剔除中渲染这些图元时,您将看到立方体和圆柱体在外观上并没有真正改变,因此圆柱体的分区减少了 100 %( 24 到 12 ),这是一次成功的优化。球体缩小了 200 %(从 48 到 12 ),这是一个很大的变化,平滑的错觉在球体周围有点分散,尤其是从顶部渲染时,这就是为什么我把它向前旋转了 90 度。

接下来,让我们看看使用背面剔除的渲染算法的优化,以及在使用实体模式渲染 3D 图元时,较低的分辨率(较少的划分)如何影响它们的视觉质量。

Shape3D 面剔除属性:优化渲染管道

Shape3D cullFace 属性和 cullFace 类用于控制 3D 场景的面和多边形渲染优化。默认为 CullFace。没有,所以你需要使用代码来打开这个优化,我将在本章的这一节向你展示如何做。我认为模型在剔除人脸后看起来更好(对比度更高),如果你对你的 pro Java 9 游戏进行了足够好的优化,它应该可以在所有平台和设备上很好地运行,而不必剔除模型中一半的人脸。也就是说,一旦你知道如何做到这一点,你应该很容易在测试阶段进行实验,看看它如何影响游戏的视觉质量和流畅度。

让我们继续向 createBoardGameNodes()方法添加代码,为基本对象设置背面剔除。首先,我们需要改变你的基本体的 drawMode 属性,在你的球体,盒子和柱子上使用. setDrawMode (DrawMode. FILL )来填充实体模型。在对每个基本对象调用这个方法之后,添加一个. setCullFace(CullFace。BACK)方法调用。如果您在 NetBeans 9 中使用弹出助手工作进程,您将会看到它使用默认的 CullFace 编写您的代码。无设置,因此您必须将它更改为 CullFace。返回以开启此渲染管道优化算法。

背面剔除语句的 Java 代码在图 12-17 的底部突出显示,看起来应该像下面的 Java 代码:

A336284_1_En_12_Fig17_HTML.jpg

图 12-17。

Add method calls to .setCullFace() with the value CullFace.BACK off of all of the primitives in your Scene

sphere.setDrawMode(DrawMode.FILL);
sphere.setCullFace(CullFace.BACK);
box.setDrawMode(DrawMode.FILL);
box.setCullFace(CullFace.BACK);
pole.setDrawMode(DrawMode.FILL);
pole.setCullFace(CullFace.BACK);

图 12-18 显示了一个运行➤项目 Java 代码测试的工作流程,显示了背面剔除算法的安装和对 3D 场景图元的操作。请注意,在您的球体上,降低的几何体分辨率(更少的划分)会导致网格上的一些平滑问题,其中网格拓扑通过平滑算法显示。我会将球体划分增加到 24 以减轻这种情况,这仍然是对默认设置的 100%优化。

A336284_1_En_12_Fig18_HTML.jpg

图 12-18。

The renderer is now rendering half as much 3D data, and your lower resolution can be seen on the Sphere

另请注意,当背面剔除打开时,立方体(长方体)基本体上的明暗对比的对比度(面之间明暗颜色的差异)要小得多。你做的自定义纹理映射越多,这个问题就越小(不太明显)(在下一章及以后的章节中讨论),但这可能是为什么默认为 FaceCull 类和这个方法调用的 NONE,因为面剔除优化可能以某种方式影响当前算法代码中的对比度(质量)。我以这种方式设置这一章,以便您可以看到这一点,因为最基本的图元之一是使用默认的中灰色着色器颜色显示面部之间的对比度明显降低,如果您比较图 12-10 和图 12-18 ,您可以看到对比度从高到几乎为零。

接下来,让我们看看三个与网格相关的类,mesh、TriangleMesh 和 MeshView,看看它们做什么,以及它们如何相互关联,因为它们将允许您渲染使用 3D 软件创建的复杂网格对象。

JavaFX Mesh 超类:构造一个三角形网格

理解抽象 Mesh 超类及其与 TriangleMesh 子类的关系很重要,triangle Mesh 子类可用于“手动编码”复杂的网格对象,以及它与 MeshView 类的关系,Mesh view 类实际上是 Shape3D 的子类,而不是 Mesh 的!这是为了使 MeshView 可以继承(扩展)Shape3D 的 cullFace、drawMode 和 material 属性,当然,这些属性对于使网格对象变得逼真是至关重要的(尤其是 material 属性和 Material 类)。正如您将看到的,MeshView 构造函数接受一个网格对象。这是复杂 3D 对象所基于的核心类(算法),因此 Mesh 和 MeshView 是用于 pro Java 9 游戏开发的最关键的类。如果出于某种原因,您想要编码复杂的多边形几何体,也称为“三角形网格”,这不是一个最佳的工作流程,您可以使用 triangle mesh,我们将详细介绍它。

更好的工作流程是使用外部 3D 软件包,将 3D 对象直接导入网格对象,然后由 MeshView 对象引用。这是一种更快的方式来让一个先进的 i3D 游戏快速有效地运行起来,也是一种将专业技术人员引入 i3D 游戏开发工作流程的方式。

JavaFX Mesh 超类:您的原始 3D 模型数据容器

公共抽象 Mesh 超类看似简单,只有一个 Mesh()构造函数方法和一个 TriangleMesh 子类(用于使用 Java 代码加载网格数据),因此我们将在这里首先介绍它。它本质上是一个用于包含 3D 数据的对象,与其他以 3D 模型为中心的类一起包含在 javafx.scene.shape 包中。Java 类的层次结构如下所示,因为 Mesh 类被临时编码为保存 3D 网格表示的 Java 类:

java.lang.Object

  > javafx.scene.shape.Mesh

这是一个基类,用于表示不是 JavaFX Shape3D 图元的复杂 3D 几何表面。请注意,这显然是一个有条件的功能,因为复杂的 3D 几何图形将需要一个 3D 渲染管道来对您的 pro Java 9 游戏开发有用。轮询 ConditionalFeature。场景 3D 将是必要的。

如前所述,构造函数方法非常简单,类似于下面的 Java 代码:

protected Mesh() // Protected Code Cannot Be Used Directly (but can be used by a subclass)

接下来,让我们看看 MeshView 类,它将引用、保存在内存中,并使用渲染引擎在 3D 场景中显示该网格对象。这个类是网格引擎和 Shape3D 之间的“桥梁”。

JavaFX MeshView 类:格式化并显示 3D 网格数据

公共 MeshView 类几乎和 Mesh 类一样简单,只有两个重载的 MeshView()构造函数方法,没有子类,所以接下来我将在这里介绍它。它是 Shape3D 的子类,存储在 javafx.scene.shape 包中。它实现了 Styleable 和 EventTarget 接口,就像三个原语类一样。它用于使用网格对象中保存的原始 3D 模型数据来定义 3D 表面。MeshView 类的 Java 类层次结构如下所示,因为 MeshView 类需要继承所有这些关键的 Shape3D 渲染特征:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.shape.Shape3D

      > javafx.scene.shape.MeshView

MeshView 对象有一个 ObjectProperty mesh 属性,它指定 MeshView 的 3D 网格数据,该数据是从第二个重载的构造函数方法参数或使用. getMesh(mesh)方法调用获得的。此类(对象)还从 javafx.scene.shape.Shape3D 类继承核心 Shape3D 属性,这些属性您已经介绍过(除了 material ),它们是 cullFace、drawMode 和 material。

有两种重载的构造函数方法。创建一个空的网格视图,以便在将来加载网格对象(3D 数据),这当然会利用以下 Java 语句格式:

meshView = new MeshView();

第二个重载构造函数方法调用使用以下对象实例化 Java 语句格式实例化 MeshView 对象,并同时用网格对象(3D 几何数据)加载它:

meshView = new MeshView(yourMeshNameHere);

MeshView 类有三个用于处理网格对象的方法调用,包括获取属性 Mesh 的网格对象值的 getMesh(Mesh)方法调用,为调用此方法的任何 MeshView 指定 3D 网格(网格对象)数据的 ObjectProperty meshProperty()方法调用,以及为 MeshView 属性 Mesh 设置网格对象值的 void setMesh(Mesh value)方法调用。

在我们介绍 TriangleMesh 类之前,让我们先看一下 VertexFormat 类,它将通过指定用于给定 3D 模型(即网格对象及其 3D 模型数据)的顶点数据格式来定义顶点数据。

JavaFX VertexFormat 类:定义你的 3D 顶点数据格式

public final VertexNormal 类还扩展了 Java Object master 类,这意味着该类是临时编码的,以定义数据点数组、其纹理坐标及其法线的格式,如果外部 3D 模型以 JavaFX 导入/导出软件支持的各种数据格式导出的话。该类是 Mesh、TriangleMesh 和 MeshView 类的实用程序类,这可以从它的最终修饰符看出,这意味着它不能被子类化。与我们介绍的其他六个类一样,它保存在 javafx.graphics 模块的 javafx.scene.shape 包中,其类层次结构如下所示:

java.lang.Object
  > javafx.scene.shape.VertexFormat

VertexFormat 类(对象)定义了两个不同的数据格式常量,它们反映了 3D 网格对象中每个顶点所包含的 3D 数据的类型。静态顶点格式 POINT_NORMAL_TEXCOORD 字段将指定包含点坐标、法线和纹理坐标数据的顶点格式。静态 VertexFormat POINT_TEXCOORD 字段将指定包含点坐标和纹理坐标数据的顶点格式。我建议使用支持法线的格式,因为可以用来定义 3D 模型的数据越多,渲染器就可以更准确地渲染它们,因此也更专业。

该类中有五种方法用于处理顶点及其法线、点和纹理坐标数据组件。的。getVertexIndexSize()方法将返回表示顶点索引的整数个组件索引。的。getNormalIndexOffset()方法将返回给定顶点内法线组件的面数组的整数索引偏移量。的。getPointIndexOffset()方法将返回给定顶点内点组件的面数组中的整数索引偏移量。的。getTexCoordIndexOffset()方法将返回顶点内纹理坐标组件的面数组中的索引偏移量。String toString()方法将返回 VertexFormat 的字符串(文本)数据,允许您以可读格式查看顶点数据。

接下来,我们来看看 TriangleMesh 对象,它是最复杂的;它允许您使用 Java 代码创建 3D 模型。在本章中,我们不会看到这样的例子,因为这不是获得快速、专业的 i3D 游戏开发 3D 模型创建结果的最有效方式。

这是因为使用专业的 3D 建模、纹理、渲染和动画软件包,如开源的 Blender.org、Autodesk 3D Studio Max、Maya 或 NewTek Lightwave,是创建专业 3D 模型的最合理的工作流程。

由于大量的 3D 数据导入文件格式,可以更快地创建 3D 模型,然后 pro Java 9 游戏开发人员可以使用 JavaFX 9 importer 格式之一将高质量的 3D 数据作为网格对象引入 JavaFX。

我们将在第十三章中查看纹理映射后,在第十四章中查看这一工作过程,以便我们更好地理解什么是纹理映射,因为它也用于第三方 3D 建模软件包中。使用第三方开发工具,如 Fusion、Blender、Audacity、Gimp 和 Inkscape,通常会产生更好的结果。

JavaFX TriangleMesh 类:创建一个 3D 多边形网格对象

public TriangleMesh 类是 Mesh 超类的一个子类,它不实现任何接口,因为它用于创建要存储在 Mesh 对象内部的 3D 数据,很像使用许多流行的 3D 文件格式导入程序导入 JavaFX 的 3D 模型,我们将在第十四章中介绍这些导入程序。TriangleMesh 存储在 javafx.graphics 模块的 javafx.scene.shape 包中,其 Java 类层次结构如下所示:

java.lang.Object
  > javafx.scene.shape.Mesh

    > javafx.scene.shape.TriangleMesh

TriangleMesh 对象用于定义 3D 多边形网格。该对象将使用两个 VertexFormat 常量之一,并包括一组包含顶点组件的独立数据数组对象,这些组件包括点、法线、纹理坐标和定义网格的各个三角形的面数组。正如我在本章中不止一次提到的,通过使用支持建模的外部 3D 软件包,如 Blender、Hexagon、Lightwave、Maya 或 3D Studio Max,可以完全避免这种低层次的复杂性,并加速过去。

请注意,JavaFX 术语点等同于 3D 软件术语顶点。JavaFX 9 使用 vertex 来指代顶点(点)及其所有关联的属性,包括其法线位置和关联的 UV 纹理贴图坐标。因此,在三角形网格方法名称和方法描述中提到的点(我们将在本章的后半部分介绍)实际上是指 3D 空间中的 3D 点(x,y,z)位置数据,代表一个顶点的空间位置。

类似地,术语点(或点的集合)用于表示表示多个顶点的 3D 点集。术语法线用于表示 3D 空间中的 3D 向量(nx,ny,nz ),其表示单个顶点的方向,该方向告诉渲染引擎面面向哪个方向,因此它可以在面的正确侧渲染纹理。术语法线(或法线数据的集合)用于表示多个顶点的 3D 向量集。

术语“纹理坐标”用于表示单个顶点的一对 2D 纹理坐标(u,v ),而术语“纹理坐标”(纹理坐标的集合)用于表示跨多个顶点的纹理坐标组。

最后,术语“面”用于表示一组三个交错点、法线(这些是可选的,取决于指定的相关 VertexFormat 字段类型)和纹理坐标,它们一起表示一个三角形的几何拓扑。术语面(面的集合)用于表示一组三角形(每个用一个面表示),这通常是 3D 多边形模型的组成部分。困惑了吗?正如我所说的,使用导入/导出工作流并让高级 3D 建模软件用户界面完成所有工作是获得令人难以置信的结果的更好方法,而不是试图使用 Java 将点、法线和 UV 坐标放置到 3D 空间中。我在本书中尝试做的是向您展示创建混合 2D 和 3D 游戏的最快、最简单和最优化的方法,以便您可以创建市场上任何游戏玩家从未体验过的 pro Java 9 游戏。

这个 TriangleMesh 类(object)有一个 ObjectProperty vertexFormat 属性,该属性将用于使用 vertexFormat 实用程序类指定这个 TriangleMesh 的顶点格式,因此这将是 vertex format。POINT_TEXCOORD 或 VertexFormat。点 _ 法线 _TEXCOORD。

TriangleMesh 类有两个重载的构造函数方法。第一个(空的)使用默认的 VertexFormat 创建一个 TriangleMesh 类的实例。POINT_TEXCOORD 格式类型,如下所示:

triangleMesh = new TriangleMesh(); // Creates Points & Texture Map Only Polygonal Mesh Object

第二个构造函数方法使用在方法调用的参数区域中指定的 VertexFormat 创建 TriangleMesh 的新实例。这类似于下面的 Java 实例化语句:

normalTriangleMesh = new TriangleMesh(VertexFormat.POINT_NORMAL_TEXCOORD) // Includes Normals

有十几种方法用于处理 TriangleMesh 对象构造;接下来让我们来看看它们。

那个。getFaceElementSize()方法将返回表示给定人脸的元素数量。使用此方法确定任何给定面使用的数据(点、法线、纹理贴图)。

ObservableFaceArray getFaces()方法将获取 TriangleMesh 对象中的整个面数组,包括点的索引、法线(仅当 VertexFormat。POINT_NORMAL_TEXCOORD 是为网格指定的),以及 TEXCOORD 数组。使用它从你的 TriangleMesh 对象中提取多边形数据。

ObservableIntegerArray getface smoothinggroups()方法将从 TriangleMesh 对象中获取一个 faceSmoothingGroups 数据数组。平滑组定义渲染的 3D 对象的表面着色(平滑)中接缝出现的位置。我们在本书前面的第三章中讨论过这个话题。

那个。getNormalElementSize()方法将返回表示 TriangleMesh 对象中法线的元素数量。这告诉你有多少法线被用来表示表面方向。

ObservableFloatArray getNormals()方法将获取三角形网格对象的法线数组。

的。getPointElementSize()方法将返回表示 TriangleMesh 对象中 XYZ 点的元素数量。这将告诉你有多少顶点(顶点计数)在你的三角网格的三维模型。

ObservableFloatArray getPoints()方法用于获取 TriangleMesh 的点数据数组。

那个。getTexCoordElementSize()方法将返回许多表示 TextureMesh 对象中纹理坐标的数据元素。使用它来确定模型中 UV 贴图坐标的数量。

ObservableFloatArray getTexCoords()方法将获取 TriangleMesh 对象的 tex coords 数组。使用此选项从 TextureMesh 3D 多边形对象中提取纹理坐标数据(仅限)。

VertexFormat getVertexFormat()方法将从 TriangleMesh 对象内部获取 VertexFormat 属性的值。使用它来确定该 3D 模型数据是否支持法线。

虚空。setVertexFormat(VertexFormat value)方法用于设置 TriangleMesh 对象的 VertexFormat 属性值。确保对象内的数据数组与此设置正确匹配。

object propertyvertexFormatProperty()方法可用于指定三角形网格的顶点格式;它可以是 VertexFormat。POINT_TEXCOORD 或 VertexFormat。点 _ 法线 _TEXCOORD。

在下一章我们学习了更多关于着色器、纹理和贴图的知识后,我们将进入 3D 软件并学习导入工作流,该工作流允许我们将强大的 3D 软件连接到 JavaFX 9 游戏引擎。

摘要

在第十二章中,我们学习了 javafx.scene.shape 包中允许您使用 3D 模型的类,包括使用 Box、Sphere 和 Cylinder 类的基本体以及使用 MeshView、VertexFormat 和 TriangleMesh 类的多边形对象。这些类都基于抽象网格和 Shape3D 超类。

您学习了如何创建 3D 基本体以及如何设置它们的属性,学习了面剔除和线框,并且观察了在 3D 场景中移动(平移)相机对象时它是如何工作的。

您了解了算法(代码)生成的图元和更高级的多边形网格对象之间的区别,以及为 pro Java 9 游戏设计和开发管道创建 3D 模型的不同工作流程,我们将在接下来的几章中继续学习。

在下一章中,我们将了解使用抽象材质超类及其 PhongMaterial 子类的 JavaFX 纹理映射,并了解更多关于着色器、纹理、纹理映射以及环境、漫射、镜面反射和自发光属性等相关主题的信息。

十三、3D 模型着色器创建:使用 JavaFX 9 PhongMaterial

现在,您已经了解了 JavaFX API 中包含的 3D 资源(称为图元),让我们开始了解一些关于如何使用 2D 图像资源“装扮”这些 3D 资源的基础知识,我们将使用着色器将这些资源转化为可应用于 3D 表面的材质。JavaFX 支持 Phong 着色器,它包含几个通道,这些通道接受称为纹理贴图的特殊图像,这些纹理贴图应用不同的效果,如着色、照明、曲面凹凸、曲面光泽等。JavaFX 在 javafx.graphics 模块的javafx.scene.paint包中提供了两个核心着色器类,专门为您“着色”或表面 3D 几何体(基本体或网格),我们将在本章中了解它们。我们还将看看如何使用 GIMP 2.8.22 基于像素和数学快速准确地创建纹理贴图,从而提供准确的纹理贴图结果。我们还将回到我们的 JavaFXGame 主要应用类编码,并开始将 Phong 着色器材质添加到 3D 图元中以获得一些实践。您可以在像 Blender 这样的 3D 软件包中完成这项工作,但是棋盘游戏非常简单(正方形、球形、圆柱形),我们只需使用 JavaFX 代码就可以完成这项工作。这意味着我们不需要导入(和分发)3D 模型,而是可以编写代码来“凭空”为你的 i3D 游戏建模这也将教会您更多关于 Java 9 和 JavaFX 中的 3D APIs,因为您将学习如何仅使用 Java 9 及其 JavaFX APIs 来建模复杂的 3D 对象。

在本章中,您将了解 JavaFX 3D 着色器类层次,它包含在 javafx.scene.paint 包中。在 Java 9 和 Android 8 中,Paint 类将像素颜色和属性应用于画布,在本例中是 3D 图元的表面。paint 包包含与此“蒙皮”或纹理映射目标相关的类。您将涵盖 Material,一个保存顶级着色器定义的超类,以及 PhongMaterial 类,后者可用于为 3D 图元创建纹理贴图或“皮肤”(在第十二章中介绍)。

JavaFX 材质超类:i3D 着色器属性

public abstract Material 超类用于创建 PhongMaterial 类,您将使用该类为在 pro Java 9 游戏设计和开发中使用的 i3D 原语创建由 Shape3D 子类使用的 Material 属性。从外部 3D 软件包导入的高级模型在 3D 软件生产环境中已经应用了材质(有时称为着色器)和纹理贴图,导入后,它们将位于使用 MeshView 对象显示的网格对象中,因此在大多数实际应用中,您不会总是在此低级直接使用 PhongMaterial 类来着色高级 3D 对象。Material 超类比 Mesh 更像一个空壳,因为它只有一个空的构造函数,没有属性或方法!Material 类是 javafx.scene.paint 包的一部分,具有以下 Java 类层次结构:

java.lang.Object
  > javafx.scene.paint.Material

一个空的构造函数方法是受保护的,这意味着它不会被直接实例化。然而,这个构造函数方法功能是在 PhongMaterial 子类中实现的,如 PhongMaterial(),我们将在本章的下一节中介绍。

protected Material()

接下来,让我们看看 PhongMaterial 子类,它代表 Phong 着色器渲染算法。这就是我们将在本章中直接使用(和学习)的内容,为我们在第十二章中创建的 3D 图元上色。

JavaFX PhongMaterial: Phong 着色算法和属性

公共 PhongMaterial 类扩展了 Material 类,为 JavaFX 3D 场景定义 Phong(算法)着色器材质、其颜色设置及其纹理贴图。该类保存在 javafx.graphics 模块的 javafx.scene.paint 包中,并且是 Material 的子类,因此您将拥有以下 Java 类层次结构:

java.lang.Object
  > javafx.scene.paint.Material
    > javafx.scene.paint.PhongMaterial

JavaFX 9 中的 Phong 着色(材质和纹理渲染)算法描述了点光源对象和环境光对象(如果存在)与 PhoneMaterial 对象所应用到的 3D 图元表面之间的交互。PhongMaterial 对象在应用漫反射和镜面反射颜色着色时反射光线,就像真实生活中的光线一样。当光线被有色物体反射时,光线本身也变得有颜色。PhongMaterial 算法支持 AmbientLight 对象设置(如果存在),并支持自发光或“辉光”贴图,以便您可以应用特殊效果来进一步增强着色器的真实感。

根据 JavaFX 9 PhongMaterial 文档,几何表面上任何给定点的着色都是以下四个部分的数学函数:环境光、漫反射、镜面反射和自发光贴图。这些对象的子组件(算法输入)包括环境光(对象),点光源(对象),漫反射颜色(设置),漫反射颜色贴图(图像对象),高光颜色(设置),高光功率(设置),高光贴图(图像对象),自发光或辉光贴图(图像对象)。

如果有多个 AmbientLight 对象,则 AmbientLight 光源的最终颜色(在这种情况下,它们的值将被简单地相加(这就是我建议使用一个对象的原因),将使用以下等式进行计算:

For each AmbientLight (Object) Source [i]: { ambient += AmbientLightColor[i] } // Color Summed

点光源算法计算要高级得多,这就是为什么我建议在 Pro Java 9 3D 游戏中使用点光源,因为它允许对 PhongMaterial 对象的表现进行微调控制,并添加更具戏剧性的照明(衰减、阴影、更高的对比度等)。)添加到您的 3D 场景中,使其更加逼真。值得注意的是,这些等式中使用的周期指的是点积数学运算。

For each PointLight (Object) Source [i]:
{    diffuse  += (SurfaceToLightVector . Normal) * PointLightSourceColor[i]

     specular += ( (NormalizedReflectionVector . NormalizedViewVector)
               ^ (specularPower * intensity(specularMap)) )
               * PointLightSourceColor[i]
}

渲染结果中的颜色值将使用以下输入组件算法进行计算:

color = ((ambient + diffuse) * diffuseColor * diffuseMap
      + specular * specularColor * specularMap
      + selfIlluminationMap

这里列出这些是为了完整性,因为它们在 PhongMaterial 文档中有概述,而不是因为你需要成为一名高级着色数学家才能开发 pro Java 9 游戏。也就是说,这将使您了解我们将在本章中探索的着色器输入组件如何在 Phong 着色器算法中相互交互,以及如何通过足够的贴图和参数调整,微调这些输入中的任何一个可以让您获得任何您想要的专业表面渲染结果!

PhongMaterial 类中有七个属性,告诉您可以使用哪些类型的纹理贴图和颜色规范来绘制 3D 图元。这些也可以在所有标准 3D 包中获得,因此在 JavaFX 9 外部创建和纹理化的模型也可以访问这些(实际上还有更多)。

ObjectProperty bumpMap 是一个图像对象,用于模拟 3D 模型上的凸起或表面高度的微小变化。这可用于向 3D 模型添加精细的表面细节,这些细节实际上不是模型的几何表面拓扑的一部分,但是凹凸贴图会使其看起来像是模型的物理拓扑的一部分。凹凸贴图有时会被错误地称为法线贴图,如 JavaFX 9 文档中所示。文档说“PhongMaterial 的凹凸贴图是以 RGB 图像形式存储的法线贴图”,所以我写信给 Oracle,询问他们 bump map 属性是凹凸贴图还是更高级的法线贴图!我所希望的是,它最初是一个凹凸贴图算法,随着时间的推移,它被升级为支持更复杂的法线贴图算法,同时保留属性名称 bump map,以便不破坏现有代码。法线贴图可以创建更好的表面效果。

ObjectProperty diffuseColor 表示材质的漫射或基础表面颜色。通过使用漫反射颜色贴图或漫反射贴图,可以更改对象表面的颜色。如果您的 3D 软件具有比导入 JavaFX 更高级的着色贴图类型,可以使用一种称为烘焙的技术,其中 3D 渲染器的着色器管线和纹理贴图结果可以渲染到漫反射贴图图像中,然后导出(作为 TIFF、BMP、PNG 或 TGA 24 位 RGB 图像)并用作 JavaFX 中的漫反射贴图图像对象。让我们接下来看一看,事实上,因为我们已经基本上涵盖了它!

ObjectProperty diffuseMap 属性引用一个图像对象,该图像对象的数据定义了一个将使用 UV 纹理坐标映射到使用 PhongMaterial 的 3D 图元表面上的漫射贴图。

object propertyself illumination map 属性引用一个图像对象,该图像对象的数据定义了一个发光或照明贴图(使用灰度图像对象表示照明强度),该贴图将使用 PhongMaterial 使用 UV 纹理坐标映射到 3D 图元的表面上。

object propertyspecular color 属性指定 PhongMaterial 的镜面反射颜色。这是镜面高光的颜色(见图 13-5 ),它改善了 3D 原始表面的视觉特性。

ObjectProperty specularMap 属性引用一个图像对象,该图像对象的像素数据定义了 3D 图元表面上的一个区域,该区域将使用镜面反射贴图响应镜面反射颜色(一个灰度图像对象,表示将应用或不应用镜面反射颜色)。这应该使用 UV 纹理坐标映射到一个基本体上,并且将影响基本体表面的反射映射区域的亮度。

DoubleProperty specularPower 属性用于指定镜面高光的功率(我喜欢把它看作焦点)。该属性在球体和圆柱体(弯曲的)基本体上特别明显,如图 13-8 所示,其具有应用于 phongMaterial 的高(紧密或聚焦)镜面高光功率值 100。

PhongMaterial 类有三个重载的构造函数方法。第一个使用默认颜色创建 PhongMaterial 对象的新实例。白色扩散颜色属性。这将使用以下 Java 代码:

phongMaterial = new PhongMaterial();

第二个构造函数将使用指定的 diffuseColor 属性创建 PhongMaterial 对象的新实例。这将使用下面的 Java 代码和颜色类 GOLD 常量,我们将在后面的代码中用到:

phongMaterial = new PhongMaterial(Color.GOLD);

第三个构造函数允许您指定漫射颜色和四种不同类型的效果图。这是最方便的构造方法,一旦我们进入游戏设计和开发的更高级阶段,我们就会用到它。这个高级构造函数方法将采用以下 Java 代码语句格式:

phongMaterial = new PhongMaterial(Color diffuseColor, Image diffuseMap, Image specularMap,
                                                      Image bumpMap, Image selfIlluminationMap)

最后,让我们来看看 22 种方法,让您可以处理所有这些声音材料组件。这些允许您使用 Java 代码动态地或交互地更改 PhongMaterial。这将允许你在你的 3D 和 i3D 游戏属性上创建一些非常令人印象深刻的效果,正如你将在本书中看到的。

object propertydiffuseColor property()方法调用将返回调用它的 PhongMaterial 的 diffuse color 属性。这是一个颜色值,用于设置图元的基本(或基础)颜色。

object propertyspecularColorProperty()方法调用为调用它的 PhongMaterial 返回一个 specularColor 属性。这是一个颜色值,用于设置图元的镜面反射(或高光)颜色。

double property specularPowerProperty()方法调用为调用它的 PhongMaterial 返回 double specularpower 属性。这是一个双精度值,用于设置图元的镜面反射(或高光)能力。

object propertybupmapproperty()方法调用将返回调用它的 PhongMaterial 的 bumpMap 属性。这是存储为 RGB 图像对象的法线贴图。

object propertydiffuseMap property()方法调用将返回调用它的 PhongMaterial 的 diffuse map 属性。这是一个作为 RGB 图像对象存储的漫反射颜色贴图。

object propertyselfIlluminationMap property()方法调用将返回调用它的 PhongMaterial 的 self illumination map 属性。该自发光贴图存储为 RGB 图像对象。

object propertyspecularMapproperty()方法调用返回调用它的 PhongMaterial 的 spectrurmap 属性。该高光颜色贴图存储为 RGB 图像对象。

getBumpMap()方法调用获取 PhongMaterial 属性 BumpMap 的图像对象。

getDiffuseColor()方法调用获取 PhongMaterial 属性 DiffuseColor 的颜色值。

getDiffuseMap()方法调用获取 PhongMaterial 属性 DiffuseMap 的图像对象。

getSelfIlluminationMap()方法调用获取 selfIlluminationMap 属性的图像对象。

getSpecularColor()方法调用获取 PhongMaterial 属性 SpecularColor 的颜色值。

getSpecularMap()方法调用获取 PhongMaterial 属性 specularMap 的 Image 对象。

getSpecularPower()方法调用为 PhongMaterial 属性 specularPower 获取一个双精度值。

void setBumpMap(Image image)方法调用为属性 BumpMap 设置图像引用。

void setDiffuseColor(Color color)方法调用为属性 DiffuseColor 设置颜色值。

void setDiffuseMap(Image image)方法调用为属性 DiffuseMap 设置图像引用。

void setselfIlluminationMap(Image)方法调用为 selflightionmap 属性设置图像。

void setspecularColor(Color Color)方法调用设置 spectrorcolor 属性的颜色值。

void 镜象映射(Image image)方法调用为属性镜象映射设置图像对象。

void setspecularPower(double value)方法调用设置属性 spectrorpower 的值。

toString()方法调用转换非文本(二进制、数字等)形式的任何数据。)格式化成文本格式。

接下来,让我们在 JavaFXGame 类中实现一些核心颜色属性,看看它们是如何工作的。

实现 PhongMaterial:指定颜色和功率值

既然我们已经熟读了声材料课,那就言归正传吧。让我们在 JavaFXGame 类的顶部声明一个 PhongMaterial 对象,并将其命名为 phongMaterial。在 light 对象代码后的 createBoardGameNodes()方法中,使用第二个重载的构造函数方法添加一个 PhongMaterial 实例化,并将漫反射颜色设置为 color。金色,如图 13-1 以及以下 Java 代码语句中突出显示的:

A336284_1_En_13_Fig1_HTML.jpg

图 13-1。

Declare and instantiate your phongMaterial object and configure its diffuse color value to be Color.GOLD

PhongMaterial phongMaterial;                    // Declared at the top of the JavaFXGame class
...

phongMaterial = new PhongMaterial(Color.GOLD);  // In the createBoardGameNodes() method body

如您所知,您的 PhongMaterial 对象可以配置颜色值并加载酷炫效果纹理贴图(图像对象),但除非您使用 Shape3D 类 setMaterial(Material)方法调用(您在上一章中了解到)将 3D 基本体和 Phong 着色器定义连接在一起,否则您将看不到应用到 3D 对象表面的着色器。

球体对象实例化后,使用点标记法添加一个对球体对象的 setMaterial(phongMaterial)方法调用,如图 13-2 中黄色突出显示。将这个相同的方法调用添加到 pole Cylinder 对象和 box Box 对象中。在截图之前,我在 NetBeans 9 中用黄色单击了 phongMaterial 着色器对象,以突出显示它的所有用法,从声明到实例化再到用法。您添加的语句的 Java 代码应该如下所示:

A336284_1_En_13_Fig2_HTML.jpg

图 13-2。

Wire the phongMaterial to the three primitives, using a setMaterial(phongMaterial) method call off each

sphere.setMaterial(phongMaterial);
box.setMaterial(phongMaterial);
pole.setMaterial(phongMaterial);

使用您的 Run ➤项目工作流程,查看 phongMaterial 渲染,如图 13-3 所示。

A336284_1_En_13_Fig3_HTML.jpg

图 13-3。

Showing the phongMaterial object with the diffuseColor property set to a Color.GOLD value

接下来,让我们使用 setSpecularColor()方法和颜色将高光颜色添加到 phongMaterial shader 对象中。黄色常数。在 phongMaterial 对象实例化之后添加一行代码,然后键入 phongMaterial 对象名称。点击句点键,从弹出的帮助选择器中选择 setSpecularColor(颜色值)选项,双击它将其插入到 Java 语句中。在参数区域内键入 Color,然后键入句点键,并通过向下滚动或键入 Y 跳转到 Y 颜色常量来选择黄色常量。

您生成的 Java 语句应该看起来像下面的 Java 代码,在图 13-4 的中间用黄色和浅蓝色突出显示:

A336284_1_En_13_Fig4_HTML.jpg

图 13-4。

Call the setSpecularColor() method off of the phongMaterial object, passing the Color.YELLOW constant

phongMaterial.setSpecularColor(Color.YELLOW);

如果您使用您的运行➤项目工作流程,在这一点上,您将看到您的图元表面的外观发生了巨大的变化,图元的边缘变得更加圆滑。事实上,如果你比较图 13-3 和图 13-5 ,你会发现长方体基本体根本不受高光颜色的影响,除非你设置它的动画,在这种情况下,当它平行于点光源时,偶然的面会被高光颜色着色。

A336284_1_En_13_Fig5_HTML.jpg

图 13-5。

Run your project to see the PhongShader object configured to use a Color.YELLOW specularColor property

但是,随着使用“镜面反射颜色”属性添加镜面反射高光,圆柱体和球体类(对象)基本体的外观发生了巨大变化。我用黄色给它一个金属的外观,但如果你用白色(默认),它会看起来更正常。请注意,点光源可以设置为白色,并且您可以在点光源照射到原始曲面之前对其进行调节(添加颜色过滤器)。

因此,如果你正在寻找真实感,确保你的点光源和镜面颜色值匹配!

PhongMaterial 类(object)的 specularPower 属性(attribute)控制曲面的亮度,至少在弯曲的对象上是如此。如图 13-3 所示,镜面高光为零,产生了所谓的无光泽表面。需要注意的是,调用 setspectrorpower(0)不会移除镜面高光。事实上,那样会适得其反,给你一个巨大的“爆炸”高光,看起来很糟糕。接下来让我们研究一下这个属性,然后我们可以继续研究所有其他属性。其余的属性涉及地图和它们的图像对象,这将涉及数字成像软件,在我们的例子中是 GIMP 2.10(或者 3.0,如果已经发布的话)。

让我们使用双精度数据值为 12 的 setSpecularPower()方法调用,将 specularPower 属性设置添加到 phongMaterial shader 对象。从技术上讲,这在 Java 代码中被标注为“12.0d”。但是,由于一个整数(只有 12)数据值符合 Double 规范,所以您可以只使用 12,Java 构建和编译过程将理解您正在做什么,并确保它被配置为 Double 值(在运行时)。

在 PhongMaterial 对象实例化之后添加一行代码,并键入 phongMaterial 对象名称。点击句点键,从弹出的助手选择器中选择 setspecrorpower(Double value)选项,双击它将其插入到 Java 语句中。在参数区域内键入 12 或 12.0d。

您生成的 Java 代码将看起来像下面两个 Java 语句中的一个,如图 13-6 底部的三分之一所示:

A336284_1_En_13_Fig6_HTML.jpg

图 13-6。

Call the setSpecularPower() method off of the phongMaterial object, passing the double value of 12

sphere.setSpecularPower(12);    // If you use Integer (simpler) format Java will convert for you
sphere.setSpecularPower(12.0d); // You can also use the 12.0d (double) required numeric format

我愚弄了这个值,改变它,并通过运行➤项目渲染。如图 13-5 所示,默认值似乎在 20 左右,即 20.0d。改变该值会产生非常细微的变化;较低的数值将用于扩大镜面高光(尝试零设置,但不要在游戏中使用它,除非是为了特殊效果),而较高的数值将限制它在任何曲面上的精确位置。平坦的表面不会受到太大的影响,如果有的话。

使用“运行➤项目”工作过程来查看 12 的“镜面反射功率”设置将如何扩展镜面反射高光。这可以从图 13-7 中看出。

A336284_1_En_13_Fig7_HTML.jpg

图 13-7。

A specularPower property set to 12 will expand the specular highlight on the surface

接下来,将 setspecularpower()方法调用值更改为 100(或 100.0d),然后使用“运行➤项目”工作流程来查看具有更高镜面反射能力的图元,这将使它们更闪亮,或更“有光泽”,如图 13-8 所示。

A336284_1_En_13_Fig8_HTML.jpg

图 13-8。

A specularPower property set at 100 will actually contract or reduce the specular highlight on the surface

既然我们已经在本章的第一部分介绍了基本的漫反射颜色、镜面反射颜色和镜面反射能量属性,让我们更进一步,开始应用在 GIMP 中创建的图像,学习使用四个纹理贴图效果(凹凸/法线、漫反射、镜面反射和发光或自发光)通道的高级纹理贴图。

使用外部图像资源:创建纹理贴图

PhongMaterial 类及其算法最强大的功能是支持的四个纹理贴图属性。这为您提供了四个着色器通道来影响您的曲面颜色(漫反射贴图):光泽(镜面贴图)、照明(自照明贴图)和高度(凹凸贴图或法线贴图)。想象一下这种类似于数字图像层合成的情况,其中这四个通道将由 Phong 着色器渲染算法进行组合,然后将镜面反射颜色和能量应用到曲面(由 specularMap 属性图像对象引导,如果 PhongMaterial 着色器管道中存在)。

使用外部第三方软件:使用 GIMP 创建地图

Java 9 和 JavaFX 的设计足够灵活,允许您使用高级(专业)第三方软件,如 GIMP(数字图像合成)、Blender (3D 建模)、Fusion(特效)、Inkscape (SVG 内容)或 Audacity(数字音频编辑)。纹理贴图通常在专业的像素编辑和图层合成软件中制作和细化得最好,如免费的开源 GIMP 2.8(即将成为 GIMP 3.0),它非常强大。

www.gimp.org 下载 GIMP 并安装。然后启动它,这样你就可以和我一起创建一些纹理贴图,这些贴图将恰当地展示你在本章前几页学到的四种不同类型的纹理贴图通道。使用文件➤新建菜单序列并访问创建新图像对话框,如图 13-9 红色 1 所示,并将宽度和高度字段设置为 2 的幂。渲染器最适用于二进制或 2 的幂的数字,包括 2、4、8、16、32、64、128、256 等。大多数游戏使用 256 像素的纹理贴图,所以我在这里使用这个尺寸。将色彩空间下拉列表设置为 RGB,并将背景色填充下拉列表设置为白色。使用图层➤新建图层菜单系列创建一个新图层,或者右键单击图层面板中的背景图层(红色 3)并选择新建图层,这将打开如图 13-9 中红色 2 所示的新建图层对话框。将图层名称设置为灰度贴图,将图层填充类型设置为透明,然后单击确定按钮创建图层。使用相同的工作过程来创建第二层,称为彩色地图,如红色 3 所示。选择灰度地图层以显示 GIMP 在哪里应用你的下一个图像创建“移动”(操作),并选择一个矩形选择工具,在图 13-9 中右上方中间显示为按下。一个矩形选择工具选项(红色 4)将出现在图的右下角,在这里您可以精确地(像素精确地)设置选择的位置和大小设置。

A336284_1_En_13_Fig9_HTML.jpg

图 13-9。

Create a 256-pixel image, add layers to hold your color and grayscale maps, and create eight striped areas

接下来,在 GIMP 画布上画出任意大小的矩形选择,如图 13-10 右侧所示。在位置字段中,设置 0,0,在大小字段中,设置 32,256。这将把选区放在八分之一跨度和画布的左侧。单击 GIMP 工具图标下前景/背景颜色样本旁边的小黑底白字图标,将 FG 颜色设置为黑色,将 BG 颜色设置为白色;然后使用你的编辑➤填充 FG 颜色菜单序列,用黑色填充四个条纹的第一个。由于该层是透明的,背景是白色的,合成的结果将是一个黑白纹理贴图(最终是四个黑白相间的条纹)。接下来,将选区向右拖动,放置第二个条纹填充;然后编辑位置字段以设置 64,0,并将大小字段设置为 32,256。再次使用编辑➤填充 FG 颜色,并将选择拖动到位置(或将位置字段设置为)128,0,选择填充 FG 颜色,并将选择拖动到位置(或将位置字段设置为)192,0。最后,最后一次选择用 FG 颜色填充,完成黑白效果(凹凸,高光)应用纹理贴图。在图 13-10 中的第二层灰度贴图中可以看到黑白(或透明)纹理贴图。

A336284_1_En_13_Fig10_HTML.jpg

图 13-10。

Create a beach ball texture in the Color Map layer and an on/off (black/white) grayscale striped texture

现在,我们已经创建了(更容易的)镜面反射或凹凸贴图效果图像资源,让我们创建一个颜色来显示漫反射颜色贴图将如何工作。稍后,我们将把它们结合起来使用(在不同的着色器通道中),并试验这些 PhongMaterial 属性能为我们的 pro Java 9 游戏开发做些什么。

为了确保你的颜色数据与你的效果(灰度)数据是分开的,选择颜色贴图层,它会变成蓝色表示它被选中,如图 13-10 左侧所示。如果愿意,可以通过单击图层左侧的眼睛图标来关闭灰度地图图层的可见性。在位置字段中设置 0,0,在大小字段中设置 32,256。这将再次把选区放在八分之一跨度和画布的左侧。点击 FG/BG 色样(颜色选择器)上的黑色方块,弹出颜色选择器对话框,设置绿色,如图 13-10 所示。一旦你点击确定,这将设置 FG 颜色为绿色,BG 颜色将保持白色。使用编辑➤填充 FG 颜色菜单序列,用绿色填充四个条纹中的第一个。由于该层是透明的,背景是白色的,因此合成的结果将是一个绿色和白色的纹理贴图(最终是四个交替的彩色和白色条纹)。接下来,将选区向右拖动 64 个像素,将其定位为第二个条纹填充;然后编辑位置字段以设置 64,0,并将大小字段设置为 32,256。使用拾色器设置蓝色,并再次使用编辑➤填充 FG 颜色创建第二个蓝色条纹。接下来,将 64 个像素向右拖动到位置 128(或将位置字段设置为 128,0),使用拾色器选择黄色前景(FG)颜色,并使用 FG 颜色填充第三个条纹。最后,将选择拖动到位置 192,0(或使用位置字段设置),使用拾色器选择红色前景(FG)颜色,然后最后一次使用“编辑➤填充 FG 颜色”菜单序列来完成沙滩球颜色(漫射,发光)应用纹理贴图创建。图 13-10 显示了 GIMP 中的最终结果。

我还将创建一个纹理贴图,交替使用 25%的灰色和 50%的灰色条纹来显示不同效果的应用,如镜面和自发光,以及如何通过使用不同的灰色阴影来控制效果应用的强度或大小。您可以创建第三个贴图,作为重新创建我们之前用于彩色和黑白纹理贴图的工作流程的“练习回合”。要导出你在 GIMP 2.10 中创建的任何纹理贴图,你可以使用文件➤导出图像作为菜单序列,这将打开导出图像对话框,如图 13-11 所示。

A336284_1_En_13_Fig11_HTML.jpg

图 13-11。

Export to C:\Users\Name\Documents\NetBeansProjects\JavaFXGame\src

如图 13-11 所示,您可以使用该对话框顶部的文件导航部分来定位您的 NetBeansProjects 文件夹。我用文件名中的描述、颜色数和像素数来命名文件。确保使用您的 JavaFXGame 文件夹和\src\子文件夹,其中保存了游戏的源代码,就像我们在本书中一直做的那样。一旦文件位于正确的文件夹中,它们将对 NetBeans 9 可见,并且我们可以在代码中将它们用作图像对象素材。接下来,让我们回到 PhongMaterial 对象编码,并进一步探索着色器管线的创建,因为这是让您的 Pro Java 9 i3D 游戏看起来非常壮观的一种方式。

在 PhongMaterial 中使用纹理贴图:着色器特殊效果

在 JavaFX 中使用 Image 对象的第一步是将 Image 对象的名称添加到类顶部的 Image 对象复合声明语句中。我将图像对象命名为与它们将被使用的属性相同的名称。接下来,由于我们有一个 loadImageAssets()方法,我们将添加四个引用包含纹理映射数据的 PNG 文件的图像实例化语句。如图 13-12 所示的 Java 代码应该如下所示:

A336284_1_En_13_Fig12_HTML.jpg

图 13-12。

Declare and instantiate Image objects to hold texture map data for a diffuse, specular, glow, or bump map

Image diffuseMap, specularMap, glowMap, bumpMap // plus the other Image objects already in use
...
diffuseMap = new Image("/beachball5color256px", 256, 256, true, true, true);
specularMap = new Image("/beachball3grayscale256px", 256, 256, true, true, true);
glowMap = new Image("/beachball2grayscale256px", 256, 256, true, true, true);
bumpMap = new Image("/beachball3grayscale256px", 256, 256, true, true, true);

接下来,进入 createBoardGameNodes()方法,将漫反射和镜面反射颜色设置更改为一种颜色。白色值,并添加对 phongMaterial 对象的 setDiffuseMap(diffuseMap)方法调用。在图 13-13 中用蓝色突出显示的漫反射颜色纹理贴图语句的 Java 代码应该如下所示:

A336284_1_En_13_Fig13_HTML.jpg

图 13-13。

Add a diffuseMap to the shader pipeline to add some surface color and set the specular and diffuse colors to white

phongMaterial = new PhongMaterial(Color.WHITE);
phongMaterial.setSpecularColor(Color.WHITE);
phongMaterial.setSpecularPower(20);
phongMaterial.setDiffuseMap(diffuseMap);

接下来,使用运行➤项目工作流程,再次查看您的原语。正如你所看到的,在图 13-14 中,你的基本体的表面现在正在使用一个漫射贴图来控制它们的表面着色,并且球体 3D 基本体现在看起来像一个沙滩球。

A336284_1_En_13_Fig14_HTML.jpg

图 13-14。

A diffuse color texture map is now painting the surface of the primitive, making a sphere into a beach ball

接下来,让我们将你的球体旋转 25 度,以便黄色和白色条纹之间的描绘发生在镜面高光中,我们在前面的代码中将其扩展回默认设置 20。

我们将使用 beachball3grayscale256px.png 形象素材;它有八个条纹,其中四个是 100%开(白色),两个是 75%开(25%灰色),两个是 50%开(半功率,或 50%灰色)。这将“静音”或减少沙滩球白色部分的镜面眩光,因为镜面贴图定义了镜面效果的强度或数量(闪亮度)。

我们将保留调用 phongMaterial 的 setDiffuseMap(diffuseMap)方法,因为我们试图在本章中构建一个高级着色器渲染管道,以将 PhongMaterial 类推到专业着色器效果创建管道的极限,就像我们在 3D 软件中一样,但只使用 JavaFX API 和 Java 9 语句。

因此,在 setDiffuseMap()方法调用之后,我们将添加一行 Java 代码,调用 phongMaterial 对象的 setSpecularMap(),然后传入在 loadImageAssets()方法中已经设置为 beachball3grayscale256px.png 图像素材的 specularMap 图像对象,如图 13-12 所示。这将通过使用以下 Java 语句来完成,这些语句在图 13-15 的底部突出显示:

A336284_1_En_13_Fig15_HTML.jpg

图 13-15。

Add a SpecularMap Image reference to the shader pipeline to control the specular highlight intensity

phongMaterial = new PhongMaterial(Color.WHITE);
phongMaterial.setSpecularColor(Color.WHITE);
phongMaterial.setSpecularPower(20);
phongMaterial.setDiffuseMap(diffuseMap);
phongMaterial.setSpecularMap(specularMap);
sphere = new Sphere(100, 24);
sphere.setRotationAxis(Rotate.Y_AXIS);
sphere.setRotate(25);
sphere.setMaterial(phongMaterial);

现在是时候再次使用运行➤项目的工作过程,并把这个着色器管线渲染到您的 3D 场景中。正如你在图 13-16 中看到的,球体上的高光似乎被黄色和白色之间的线切断了。这是由纹理贴图的交替区域的高光贴图(降低高光强度)造成的。这也可以在圆柱体原语上看到。现在你可能已经注意到了,为了放大视图,我减少了相机与场景中心的距离,从 250 个单位减少到 100 个单位,并且我增加了 3D 图元的大小,这样我们可以更清楚地看到纹理映射效果。

A336284_1_En_13_Fig16_HTML.jpg

图 13-16。

The specular highlight on the curved surface sphere and pole objects is now brighter on the colored area

接下来,让我们把你的球体旋转回 5 度,这样你的黄色部分就在高光的中心,白色条纹在两边。这将更准确地向您展示自发光贴图的威力。

我们将使用 beachball2grayscale256px.png 图像素材,它有八个条纹。其中四个是 100%打开(白色),四个是 100%关闭(黑色),就效果处理纹理贴图而言,这是最极端的情况,因为这等同于完全应用(白色或全部打开 255 值)或不应用(黑色或零)。

这种自发光贴图(在 3D 软件中通常称为发光贴图)将像光源一样打开用白色贴图的 3D 图元部分,而黑色区域将不会被照亮,并将使用现有的纹理贴图管道。更多的灰色将增加更多的光,因此 25%的灰色将模拟 25%的照明(25%的光强度)。我们将保留调用 phongMaterial 的 setDiffuseMap()和 setSpecularMap()方法,因为我们试图构建一个高级着色器渲染管道,并将 PhongMaterial 类推到专业着色器效果创建管道的极限,就像在 3D 软件中一样,但只使用 JavaFX API 和 Java 9 语句。

因此,在 setSpecularMap()方法调用之后,我们将调用 phongMaterial 对象的 setselflightionmap(glowMap)方法,并传入 glow map 图像对象,设置为在 loadImageAssets()方法中实例化的 beachball2grayscale256px.png 图像素材,如图 13-12 所示。这将通过使用以下 Java 语句来完成,这些语句在图 13-17 的底部用黄色和浅蓝色突出显示:

A336284_1_En_13_Fig17_HTML.jpg

图 13-17。

Add the SelfIlluminationMap Image reference to the shader pipeline to control self-illumination intensity

phongMaterial = new PhongMaterial(Color.WHITE);
phongMaterial.setSpecularColor(Color.WHITE);
phongMaterial.setSpecularPower(20);
phongMaterial.setDiffuseMap(diffuseMap);
phongMaterial.setSpecularMap(specularMap);
phongMaterial.setSelfIlluminationMap(glowMap);
sphere = new Sphere(100, 24);
sphere.setRotationAxis(Rotate.Y_AXIS);
sphere.setRotate(5);
sphere.setMaterial(phongMaterial);

图 13-18 显示了运行➤项目 Java 代码测试工作流程,在所有三个原语上都有自发光映射。白色区域已转化为光源,彩色区域仍显示漫反射和镜面反射贴图特征。selfIlluminationMap 属性代码的抗锯齿算法部分似乎有一点问题,正如您在球体图元的周界边缘上看到的那样。

A336284_1_En_13_Fig18_HTML.jpg

图 13-18。

The self-illumination map turns white area on 3D primitives into a light source, leaving color areas alone

接下来,让我们看看如何使用我们到目前为止所学的知识,并在 GIMP 2.8.22 中创建一些着色器的纹理贴图组件,以便在我们开始使用 Java 9 中的 JavaFX 9 APIs 构建 i3D 游戏时,在第十四章中创建的游戏板组节点层次中使用。

游戏板纹理:创建游戏板方块

理解抽象网格超类及其与 TriangleMesh 子类(可用于“手工编码”复杂网格对象)的关系,以及它与 MeshView 类(实际上是 Shape3D 的子类,而不是 Mesh 的子类)的关系非常重要!这是为了使 MeshView 可以继承(扩展)Shape3D 的 cullFace、drawMode 和 material 属性,当然,这些属性对于使网格对象变得逼真是至关重要的(尤其是 material 属性和 Material 类)。正如您将看到的,MeshView 构造函数采用一个网格对象,因此这是复杂 3D 对象所基于的核心类(算法);因此,Mesh 和 MeshView 是用于 pro Java 9 游戏开发的关键类。如果出于某种原因,您想要编码复杂的多边形几何图形,也称为“三角形网格”(这不是一个最佳的工作流程),您可以使用 Triangle Mesh,我们将详细介绍它。

更好的工作流程是使用外部 3D 软件包,将 3D 对象直接“导入”到网格对象中,然后由 MeshView 对象引用。这是一个工作流程,我们将用整整一章来讲述如何使用这些 JavaFX 类来“建模”3D 游戏,这样您就不必导入任何“数据繁重”的网格对象。导入 3D 资源可以更快地快速高效地启动和运行高级 i3D 游戏,也是将专业人员引入 i3D 游戏开发工作流程的一种方式。

准备创建游戏板:代码重新配置

让我们为下一章将要做的事情(构建我们的 i3D 游戏板)做好准备,并为我们的 gameButton 事件处理程序、createBoardGameNodes()方法、addNodesToSceneGraph()方法和 loadImageAssets()方法重新配置 Java 代码体。让我们从相机对象推拉切换,设置相机 Z = 0,而不是使用 FOV 来放大和缩小场景。由于我们现在要删除球体和圆柱体基本体,我们将 X 和 Y translate 属性设置为-500,并将相机绕 X 轴旋转 45 度,以便它向下看游戏板。进行这些摄像机调整的 Java 代码如图 13-19 所示,如下所示:

A336284_1_En_13_Fig19_HTML.jpg

图 13-19。

Reconfigure your camera object to dolly to Z = 0, rotate 45 degrees, and zoom in with FOV = 1

camera.setTranslateZ(0);
camera.setTranslateY(-500);
camera.setTranslateX(-500);
camera.setRotationAxis(Rotate.X_AXIS);
camera.setRotate(-45);
camera.setFieldOfView(1);

接下来,让我们删除球体球体和极柱圆柱体实例化和配置语句;如果你愿意,你可以把声明放在类的顶部,因为我们以后会用到它们。

要制作一个游戏板正方形,它将在游戏板的周边使用,并且将是 150 个单位的正方形和 5 个单位的薄(高),我们将留下 Box box 对象并用 Box(150,5,150)方法调用来构造它。我现在也将它旋转 45 度,使点(角)面向相机对象。我们可以保留 PhongMaterial 代码,因为一旦我们在 GIMP 中创建了 diffuseMap,我们所要做的就是在 loadImageAssets()方法中更改文件名,这将在我们创建游戏棋盘正方形纹理贴图之后进行。不要忘记,如果你忘记删除我们已经从 SceneGraph 节点中删除的对象,你将在编译期间得到一个致命的错误。

如前所述,一个盒子构造器方法是非常基本的,看起来像图 13-20 中的 Java 代码:

A336284_1_En_13_Fig20_HTML.jpg

图 13-20。

Remove sphere and pole instantiations and configurations and change the box dimensions to 150, 5, 150

box = new Box(150, 5, 150);
box.setRotationAxis(Rotate.Y_AXIS);
box.setRotate(45);
box.setMaterial(phongMaterial);

接下来,让我们从游戏板组对象中移除这些(当前)未使用的极点和球体原语,这将把我们的 addAll()方法调用改回 add()方法调用。如果您忘记这样做,并试图选择运行➤项目,它将不会编译。产生的 Java 语句如图 13-21 所示,如下所示:

A336284_1_En_13_Fig21_HTML.jpg

图 13-21。

Remove the pole and sphere objects from your gameBoard.getChildren().addAll() method call for now

gameBoard.getChildren().add(box);

现在让我们回到 GIMP,给我们的纹理贴图合成添加一层,创建一个游戏棋盘方块。

创建你的游戏板正方形漫射纹理:使用 GIMP

让我们在本章中完成一些游戏棋盘方块的设计,所以在下一章中,我们要做的就是设计棋盘的中心,创建四周的方块,并对图像进行颜色转换,以创建方块之间的轮廓。我们将在 GIMP 中使用我们在本章前面使用的相同方法来完成此操作,使用相同的 32x256 条纹,只是这一次四个条纹将位于游戏棋盘正方形的周边。我们将使用 RGB 255,0,0(纯红色),这样我们就可以用 GIMP 中的算法对这个值进行颜色转换。

打开您的多层 GIMP XCF 文件,右键单击顶层,并使用新的➤图层菜单项创建一个空的透明层。关闭除白色背景图层之外的其他图层中的所有可见性(眼睛)图标。将层名称设置为 GameBoardTile。确保选择这一层,使它变成蓝色,以显示 GIMP 在哪里应用您的下一个图像创建“移动”(操作)。

选择你的矩形选择工具,在图 13-22 的中上方显示为按下。矩形选择工具选项将出现在工具图标的下面,如图的底部中间所示,您可以(再次)精确地(精确到像素)设置您的选择的位置和大小设置。

A336284_1_En_13_Fig22_HTML.jpg

图 13-22。

Use the same Rectangle Select technique we used earlier in the chapter to create a game board square

接下来,在 GIMP 画布上画出任意大小的矩形选区,如图 13-22 右侧所示。在位置字段中,设置 0,0,在大小字段中,设置 32,256。这将把选区放在八分之一跨度和画布的左侧。单击顶部颜色上位于 GIMP 工具图标下的大前景色/背景色样本,并将 FG 颜色设置为红色。然后使用你的编辑➤填充 FG 颜色菜单序列,用红色填充前四个条纹。由于这一层是透明的,背景是白色的,合成的结果将是一个红白纹理贴图(最终四个重叠的红色条纹)。

接下来,将选区向右拖动,放置在第二个条纹填充的位置。然后编辑位置字段,将其设置为 0,0,并反转大小字段,将其设置为 256,32,如图 13-22 所示。再次选择编辑➤填充 FG 颜色。一半的游戏板广场漫射颜色纹理地图已创建在短短几个步骤!

让我们通过再次拖动选择到 256 像素纹理贴图画布右侧的位置(或将位置字段设置为)224,0 来完成其他两个周界条纹。请确保将您的大小数据字段设置回 32,256(宽度、高度),然后再次使用“编辑➤填充 FG 颜色”将右边的周界条纹填充为红色(也是 JavaFX 中的颜色类常量)。最后,拖动选择位置(或将位置字段设置为)0,224,然后最后一次使用编辑➤填充 FG 颜色,以完成黑白效果(凹凸,镜面反射)应用纹理贴图。

除了能够为您的漫反射颜色纹理贴图对周边颜色进行颜色转换,以创建几十个独特的游戏棋盘方块,因为内部颜色是白色,不会受到影响(白色、黑色和灰色没有颜色值可以进行颜色转换)。

使用我们在本章中学到的其他概念和代码技术,我们将能够创建其他 PhongMaterial 类着色器对象,当游戏棋子落在特定的游戏棋盘方格上时,这些对象将高亮显示、发光或给当前活动的游戏棋盘方格涂上不同于所有其他方格的颜色。

值得注意的是,这将只使用一个单一的漫反射颜色纹理贴图(680 字节或 1 千字节数据/内存的三分之二)来完成,从而交互式地为您的游戏提供更专业的用户体验。我还将使用黑色、白色和灰色创建一个效果纹理图(可能是两个或三个),这将与红白一个像素一个像素地匹配,为我提供游戏代码中最具过程性(外科手术)的效果应用。白色周边(和黑色内部)将允许我只隔离彩色区域以获得特殊效果,而黑色周边(和白色内部)允许我隔离游戏棋盘方块的内部以获得特殊效果。我们将把这几个纹理与数字成像(第二章)和漫射和镜面颜色控制结合起来。

最后,确保使用 GIMP 的文件导出为工作流程,如图 13-11 所示,将完成的游戏棋盘正方形漫反射纹理贴图数据保存在 NetBeansProject 文件夹和 JavaFXGame 子文件夹中正确的 source assets 文件夹下的一个名为 gameboardsquare.png 的文件中。现在,我们所要做的就是将这个文件名引用交换到 loadImageAssets()方法体内的 diffuseMap 图像对象实例中,并且我们可以使用 box()构造器方法在我们之前创建的新的 box Box 对象配置中利用它(参见之前的图 13-20 )。

打开 loadImageAssets()方法体,编辑 diffuseMap 图像对象实例化,使其引用从 GIMP 导出到NetBeansProject\JavaFXGame\src\文件夹的 gameboardsquare.png 文件。新图像实例化的 Java 语句应该如下所示,在图 13-23 中用黄色和浅蓝色突出显示:

A336284_1_En_13_Fig23_HTML.jpg

图 13-23。

Change your diffuseMap Image object instantiation statement to reference your gameboardsquare.png file

diffuseMap = new Image("/gameboardsquare.png", 256, 256, true, true, true);

图 13-24 显示了运行➤项目 Java 代码测试的工作流程;您可以看到新的游戏棋盘方形盒子盒子对象映射了新的漫反射颜色纹理贴图,这是您刚刚使用 GIMP 2.8.22(或更高版本)创建的。

A336284_1_En_13_Fig24_HTML.jpg

图 13-24。

We now have a game board square, which will be duplicated around the perimeter (in the next chapter)

边缘上有一点白色(JavaFX 目前不允许每边框对象映射),我们将在以后的章节中通过调整camera.setRotate()方法调用值来最小化它,直到它变得不那么明显。

在结束本章的着色器管线创建和纹理贴图之前,我还想说明最后一点,即如何使用 JavaFX 为 3D 图元设置皮肤。你可能想知道为什么我对这个纹理使用 PNG24 (24 位)图像格式,而不是更优化的 PNG8 格式。嗯,这个 PNG24 编解码器在将 256 × 256 × 3 (196,608)字节压缩为 680 字节方面做得非常好,数据减少了 290 : 1 或 99.67%!

从更技术性的角度来看,Java 将在内存中使用 24 位 RGB 颜色表示,因此,如果我们使用了 8 位索引彩色图像,那么当它被加载到内存中时,就会被简单地转换回 24 位颜色值图像。因此,我倾向于尽可能使用 PNG24 和 PNG32 图像,特别是对于 3D 纹理贴图,无论如何,对于 pro Java 9 游戏设计和开发应用,这些贴图主要是 32x32、64x64、128x128、256x256 和 512x512。对于照片图像,您也可以使用 JPEG。

摘要

在第十三章中,我们学习了 javafx.scene.paint 包中允许您使用 3D 着色器、纹理贴图和材质的类,包括基于抽象材质超类的 PhongMaterial 类。我们了解到 Material 类基本上是一个“空”类或一个“外壳”来容纳一个“材质”对象(Shape3D 类中的属性),而 heavy lifting(算法)在 PhongMaterial 子类中。我们相当详细地研究了这个类中的属性、构造函数和方法调用,以便您知道 PhongMaterial 对象可以做什么,然后我们研究了如何用 Java 代码实现这些(bumpMap 除外,它在我正在使用的当前 JavaFX 9 代码库中不起作用,因此我们将在本书的后面部分再次讨论)。

您了解了如何使用 GIMP 创建纹理贴图资源(目前本书的版本为 2.8.22,但我预计 2.10 将于 2017 年推出,3.0 将于 2018 年推出),以及如何通过在最佳工作流程中使用 GIMP 的工具来创建平衡的、像素精度的 2 次方纹理贴图,这些贴图针对专业 3D 游戏开发进行了优化。

然后,我们看了看如何在四个当前的纹理贴图“通道”中实现这些纹理贴图素材,这些通道目前通过 JavaFX 9 PhongMaterial 类提供给我们。我们看到了这些纹理贴图通道如何允许我们微调材质属性的渲染方式,从而允许我们为 Java 9 游戏创建更加专业的外观。

最后,我们为我们的游戏板方块创建了 diffuseColor 属性纹理贴图,将 box Box 对象转化为这些游戏板方块中的一个,并将新的纹理贴图应用到新的 3D 原始“plane”对象,为我们在下一章将要做的事情(在场景图中创建我们的游戏板分支)做准备,以便它看起来像我们正在创建的游戏板。如您所知,我建议以这样一种方式进行您的 pro Java 9 游戏开发,即在您编写 Java 9 代码、创建新媒体素材以及将您的 pro Java 9 游戏内容和交付内容“变形”成您最终想要的样子时,您可以看到 JavaFX 9 将要做什么。Pro Java 9 游戏开发是一个精炼的过程,所以这就是我写这本书的方式。我将向您展示我实际上是如何使用 NetBeans 9 IDE 以及 Java 9 和 JavaFX 9 APIs“凭空蒸发一个 3D 棋盘游戏应用”的。

在第十四章中,我们将进一步完善我们的 Java 代码组织,创建新的方法并重组一些现有的方法,以创建并整合我们的 i3D 棋盘游戏的核心 gameboard。我们将在游戏板组节点(分支)下创建一个嵌套的组 3D 层次结构,并查看 3D 图元 X、Y、Z 定位和相关概念,这些概念适用于无缝布局 3D 游戏板,以便未来的 Java 代码可以以逻辑、最佳的方式访问和引用其组件和子组件。就像数据库设计一样,你如何设计你的 SceneGraph 极大地影响了你的 Pro Java 9 3D 游戏在未来如何运行。我们保持设计、层次结构和 3D 对象命名模式越简单、越直接,我们在为交互性、动画、运动、碰撞检测等编写未来代码时就会处于更好的状态。此时,您应该开始对 Java 9 和 JavaFX 9 为您提供的可能性感到兴奋了。

十四、3D 模型层次创建:使用原语创建游戏板

现在,您已经了解了如何使用 JavaFX Phong 着色器算法及其各种颜色和效果映射通道来“蒙皮”您的 3D 基本体,并且您已经创建了丰富多彩、高度优化的游戏板方形纹理贴图,是时候添加一些自定义方法来构建游戏板并使用纹理贴图设置 Phong 着色器对象了。我们将需要创建一个createGameBoardNodes()方法来组织组成我们的 3D 游戏板的 3D 原始素材,因为createBoardGameNodes()方法应该(也确实)包含更高级的节点子对象实例化和配置,例如场景、根、UI 堆栈面板、3D 游戏板组、相机和照明,以及四个名为 Q1 到 Q4 的游戏板象限组对象(象限 1 到 4)。我们还将创建其他 19 个游戏棋盘方块对象,命名为 Q1S1 到 Q1S5、Q2S1 到 Q2S5、Q3S1 到 Q3S5 和 Q4S1 到 Q4S5,以保持对象名称简短。将这些对象命名为 Quadrant1Square1 (Q1S1)的缩写版本将使使用这些缩写术语的 Java 代码可读性更好。

在本章中,您将构建 SceneGraph 的 gameBoard Group 分支,它位于 SceneGraph 根目录下,紧挨着您已经构建好的 uiLayout 分支。在您的游戏板组分支下,我们将游戏板分成四个象限,因此游戏板的中间可以有四个更大的 300x300 单位区域,我们可以用于游戏,每个象限都有 20 个周边游戏板方块中的 5 个作为子对象。使用三层 3D 基本对象层次,我们可以将整个游戏板作为一个整体访问(例如,旋转它),将每个象限作为一个单元访问(例如,悬浮它或应用着色器效果),并访问层次底部的单个游戏板方块(叶节点子对象)。我们开始工作吧!在本章中,我们要编写数百行新的 Java 代码来实现图元、着色器、图像和场景图层次节点。

原始创建方法:createGameBoardNodes()

因为创建 24 个原语(4 个中心棋盘象限和 20 个周边正方形)将需要 100 多个 Java 语句(使用 new、setTranslateX()、setTranslateZ()、setMaterial()等进行实例化)。),让我们专门创建一个方法来保存我们的游戏板对象及其实例化和配置语句。这样,一个 createBoardGameNodes()方法将创建全局和顶级节点子类对象(场景、根、相机、灯光、uiLayout 分支、gameBoard 分支、Q1 到 Q4 分支等。).在本章的后面,我们还将把 PhongMaterial 着色器创建逻辑提取到另一个自定义 createMaterials()方法中,在这里我们将创建几十个自定义着色器对象来为这个游戏板的各种组件提供外观。要让 NetBeans 9 为您创建这个新方法,请在 start()方法的第一部分,在 createBoardGameNodes()方法调用之后添加一行代码,然后键入以下 Java 方法调用,命名您的新方法:

createGameBoardNodes();

NetBeans 将意识到这不是一个有效的方法调用,并将使用红色波浪下划线突出显示它。

使用您的 Alt+Enter 工作流程,在 javafxgame 中双击您的创建方法“createGameBoardNodes()”。JavaFXGame 选项,在图 14-1 中突出显示,让 NetBeans 为您创建一个空的方法体结构。接下来,从 createBoardGameNodes()中删除与 box Box 对象相关的代码,并将该代码放入这个新方法中。我们还将删除圆柱杆和球体,这样它们就不会干扰你的游戏板设计。

A336284_1_En_14_Fig1_HTML.jpg

图 14-1。

Open the start() method; type a createGameBoardNodes() method call after createBoardGameNodes()

剪切并粘贴你的盒子原始代码。createBoardGameNodes()到。createGameBoardNodes()并将该框重命名为 Q1S1。删除除实例化和着色器方法调用之外的所有 Java 语句,如图 14-2 所示:

A336284_1_En_14_Fig2_HTML.jpg

图 14-2。

Copy the primitive code to createGameBoardNodes(); delete everything except the instantiation and .setMaterial

Q1S1 = new Box(150, 5, 150);
Q1S1.setMaterial(phongMaterial);

使用以下代码将引用该框的 Java 代码更改为引用 Q1S1,这也显示在图 14-3 中:

A336284_1_En_14_Fig3_HTML.jpg

图 14-3。

Be sure to change all referencing from the box to Q1S1 in createBoardGameNodes() and addToSceneGraph()

light.getScope().addAll(Q1S1);

您还必须打开 addNodesToSceneGraph()方法,并将游戏板节点代码行内的框更改为 Q1S1,这样 Q1S1 游戏板方块将在我们接下来要做的测试渲染中可见。稍后,我们将在本声明中引用 Q1 到第四季度象限,然后使用这些分支节点引用游戏棋盘方块对象,这是我们接下来要做的,以创建三层层次结构。您生成的 Java 语句应该看起来像下面的 Java 9 代码,在图 14-4 的中间用黄色和浅蓝色突出显示:

A336284_1_En_14_Fig4_HTML.jpg

图 14-4。

Add the first Q1S1 game board square to the gameBoard Group node for now so it will compile the test render

gameBoard.getChildren().add(Q1S1);

如果您使用运行➤项目工作流程,此时您将在图 14-5 中看到,我们已经将 3D 场景重置为一个游戏棋盘方块,并且我们可以开始构建与该方块相关的游戏棋盘的其他部分。

A336284_1_En_14_Fig5_HTML.jpg

图 14-5。

Use the Run ➤ Project work process to test render the reconfiguration of the 3D Scene from Box to square

现在是时候开始在 SceneGraph 的 3D 游戏板组分支下构建 SceneGraph 层次结构了。游戏板组将包含从 Q1 到第四季度的四个象限组分支。这些象限组节点对象中的每一个都将包含一个盒基本象限(游戏板中心的四分之一)和该象限附属的五个游戏板方块。q1 到 q4 象限平面对象也将是四倍于(300x300)游戏棋盘正方形大小的盒子图元。

我将把游戏板组对象实例化移到根组实例化之下,然后在 createBoardGameNodes()方法的顶部添加 Q1 到 Q4 组对象实例化,以便 Java 代码顺序反映父子层次结构。您的叶对象(最底部的节点)将在 createGameBoardNodes()方法中创建,包括 q1 到 q4 象限平面对象,它们是 Q1 到 Q4 组(分支)节点的叶节点。

如果您愿意,您可以使用方便的复制粘贴程序员的技巧,键入第一个 q1 组对象的实例化语句,然后再复制粘贴三次,将 Q1 更改为 Q2 到 q4,因为在这一点上,我们只是创建了四个空的象限组节点,我们将引用它们上面的 gameBoard 组节点和它们下面的 Q1S1 到 Q4S5(以及 Q1 到 Q4)叶节点。生成的 Java 代码应该如下所示,在图 14-6 的顶部用黄色、红色和蓝色突出显示:

A336284_1_En_14_Fig6_HTML.jpg

图 14-6。

Add four Group branch node object instantiations under your gameBoard Group, named Q1 through Q4

gameBoard = new Group();

Q1 = new Group();

Q2 = new Group();

Q3 = new Group();

Q4 = new Group();

现在,我们需要从 gameBoard 分支节点中删除 Q1S1 叶节点,并将其替换为 Q1 到 Q4 分支节点。为了在我们为 3D 场景选择运行➤项目(渲染)时显示 Q1S1 盒图元,您需要创建第二个“节点生成器”。getChildren()。Q1 (Q1S1 对象的父分支)的 add()方法链,以便游戏板节点引用 Q1 节点,后者引用 Q1S1 节点。

您重新配置的 addNodesToSceneGraph()方法语句现在将有六个 Java 语句,并且您的游戏板场景图层次结构,从根到游戏板方块,现在跨越了三个 Java 9 语句,看起来应该像 addNodesToSceneGraph()方法中的以下 Java 语句,如图 14-7 的中间用黄色和蓝色突出显示(相关声明也在顶部突出显示):

A336284_1_En_14_Fig7_HTML.jpg

图 14-7。

Replace a Q1S1 reference in the gameBoard node builder with Q1 through Q4, and add a Q1 node builder

root.getChildren().addAll(gameBoard, uiLayout);

gameBoard.getChildren().addAll(Q1, Q2, Q3, Q4);

Q1.getChildren().add(Q1S1);

接下来,让我们添加 q1 盒子游戏板中心象限,它将是 Q1S1 游戏板正方形的父对象;因此,这是下一个合乎逻辑的补充。因为你已经在你的类的顶部声明了 q1 到 q4 的盒子对象,如图 14-6 到 14-8 所示,你可以先把这个 q1 对象添加到你的 Q1 分支节点,或者你可以先用一个 300,5,300 (X,Y,Z)参数在 createGameBoardNodes()方法中实例化它,然后再把它添加到 addNodesToSceneGraph()方法中,如图 14-8 和

A336284_1_En_14_Fig8_HTML.jpg

图 14-8。

Change the .add() method call to an .addAll() method call; add the q1 Box primitive for your first quadrant

Q1.getChildren().addAll(q1, Q1S1);   // In addNodesToSceneGraph() method body

q1 = new Box(300, 5, 300);           // In createGameBoardNodes() method body

图 14-9 显示了一个运行➤项目的工作流程,显示了一个游戏棋盘方块和渲染为 0,0 的象限。

A336284_1_En_14_Fig9_HTML.jpg

图 14-9。

Select Run ➤ Project and render your 3D Scene; both the quadrant and game board square are at 0,0,0

准备放置游戏板场景图节点

在我们开始围绕它们的周长定位 4 个象限和 20 个正方形之前,让我们为场景图的其余部分和着色器(PhongMaterial)使用的所有纹理贴图放置基础设施。使用剪切和粘贴将其他三个 Q2 到 Q4 场景图组节点添加到您的 addNodesToSceneGraph()方法中,如图 14-10 中以浅蓝色突出显示的内容。请注意,您可以使用。getChildren()。addAll()方法链,即使列表中只有一个 Node 子类 object 元素!您的 Java 语句将如下所示:

A336284_1_En_14_Fig10_HTML.jpg

图 14-10。

Instantiate your other three quadrant Box primitives and your other three quadrant branch node objects

Q2.getChildren().addAll(q2);

Q3.getChildren().addAll(q3);

Q4.getChildren().addAll(q4);

在此过程中,创建另外三个第 2 季度到第 4 季度的游戏板中心象限,以便我们可以将它们添加到 q2 到第 4 季度的节点构建声明中。同样,由于这些对象是在你的类的顶部声明的,你可以以任何你想要的顺序构造这些语句;只是不要使用运行➤项目来渲染场景,因为你不会看到对象,直到它们被实例化并添加到场景图层次。框实例化 Java 代码应该如下所示,如图 14-10 底部所示:

q2 = new Box(300, 5, 300);

q3 = new Box(300, 5, 300);

q4 = new Box(300, 5, 300);

正如你在图 14-9 中看到的,象限 1 位于游戏棋盘方格 1 的下方,并且不在它们的角接触的位置。因此,将象限 1 的 q1 长方体对象对角移动 225 个单位。这相当于棋盘游戏正方形边的长度再加上 50%,即 225 个单位。如果您只使用 150 个单位,象限角将位于游戏棋盘方格的中心。创建这种对齐的代码如下所示。setTranslateX()和。setTranslateZ() Java 方法调用,如图 14-11 中间用黄色和蓝色高亮显示:

A336284_1_En_14_Fig11_HTML.jpg

图 14-11。

Move the q1 quadrant to the X,Z location 225,225 so that it is internal to square Q1S1 with the corners touching

private void createGameBoardNodes() {
    q1.setTranslateX(225);
    q1.setTranslateZ(225);
    Q1S1 = new Box(150, 5, 150);
    Q1S1.setMaterial(phongMaterial);
    q2 = new Box(300, 5, 300);
    q2.setVisible(false);
    q3 = new Box(300, 5, 300);
    q3.setVisible(false);
    q4 = new Box(300, 5, 300);
    q4.setVisible(false);
}

还要注意,我使用。setVisible(false)方法调用,这样我可以先处理象限 1 的 q1 框及其五个游戏棋盘方块子对象,因为我将先处理象限 1,向您展示我正在使用的工作流程,然后是象限 2,然后是象限 3,依此类推。如果可能的话,将复杂的任务分解成子任务是很有用的,这样你就不会在开发过程中不知所措。由于 SceneGraph 层级设置为使用 gameBoard 分支下的四个棋盘象限,这就是我将如何着手构建游戏棋盘,一次一个象限(在本例中为 Q1)。请注意,我的游戏棋盘方块名称也与此匹配,因此我有一个优势,因为我的游戏棋盘方块对象,在本例中为 Q1S1 到 Q1S5,与象限组对象名称 Q1 匹配。由于我不能将 q1 组对象名称复制为盒子象限对象名称,所以我必须将小写的 Q1 到 q4 用于我的象限平面基本体,这很好,因为我仍然知道发生了什么,并且因为游戏板的象限部分远不如游戏板正方形本身重要。

让我们使用“运行➤项目”工作流程渲染 3D 场景,看看两个长方体图元是否仍然重叠,或者它们的位置是否正确。正如您在图 14-12 中所看到的,游戏棋盘方块的角和第一象限的角现在已经对齐,您可以开始看到游戏棋盘将如何布局。

A336284_1_En_14_Fig12_HTML.jpg

图 14-12。

Use the Run ➤ Project work process to see if the two 3D primitives are precisely aligned corner to corner

虽然这是我想看到的结果,但在考虑如何访问每个象限及其子方格时,我想让方格以 QxSy 1 到 5 的顺序围绕游戏板,如果我从每个象限的角上的方格 1 开始,这将不起作用!好好想想吧!因此,我实际上需要将这个正方形的位置从 0,0 (X,Z)移动到 300,0 (X,Z)。我将在创建自定义方法体来保存我的着色器后执行此操作。

由于我将有几十个着色器,我将快速创建另一个自定义方法来保持着色器创建的分离和组织,以便我可以根据需要折叠和展开着色器相关的代码。

编写 Phong 着色器创建方法代码:createMaterials()

由于着色器是 pro Java 9 游戏设计管道的重要组成部分,让我们为它们提供自己的方法体,并将 PhongMaterial 对象代码从 createBoardGameNodes()移到这个新的 createMaterials()方法中。在 loadImageAssets()之后的 start()方法的顶部添加一行代码,因为这些代码在着色器中以及 createGameBoardNodes()之前使用;这些对象将使用在此 createMaterials()方法体中创建的着色器。键入 createMaterials()和分号来调用不存在的方法;然后使用 Alt+Enter 组合键并选择“在 javafxgame 中添加 createMaterials()方法。JavaFXGame”选项。让我们也改变我们的 PhongMaterial 名称为 Shader1。我们可以在这个方法体中命名前 20 个着色器,也可以在您的类的顶部命名,在那里我已经为名为 diffuse1 到 diffuse 20 和 Shader1 到 Shader 20 的图像对象添加了声明,以预期我们将要编写的代码。剪切并将 PhongMaterial 代码粘贴到 createMaterials()中的“live”中,并删除高光属性。如图 14-13 所示的代码应该如下所示:

A336284_1_En_14_Fig13_HTML.jpg

图 14-13。

Add Image object declarations diffuse1 through diffuse20 and create a createMaterials() shader method

Image         diffuse1 ... diffuse20;      // Object Declarations at top of class
PhongMaterial Shader1  ... Shader20;
...
Shader1 = new PhongMaterial(Color.WHITE);  // Create Diffuse Shader in createMaterials()
Shader1.setDiffuseMap(diffuse1);

接下来,让我们移除我们在第十三章中创建的 loadImageAssets()中所有与特效贴图相关的代码,除了 diffuseMap,它将被重命名为 diffuse1。复制并粘贴 diffuse1 实例化四次,并引用接下来的四个游戏板正方形纹理贴图,gameboardsquare2.pnggameboardsquare5.png

从着色器的角度来看,您现在已经准备好构建游戏板的第一个象限了。在 loadImageAssets()方法的后半部分,现在应该有五个(diffuseMap)图像对象,分别命名为 diffuse1 到 diffuse5。这些将包含 diffuseMap 属性纹理贴图,定义暖色(红色、橙色、黄色)将被映射到游戏棋盘上的哪个方格,这些方格是游戏棋盘象限(1)的子方格,我们将首先对其进行布局。我们将首先布置绿色象限,然后是蓝色和紫色象限。

应使用以下 Java 语句添加(最终)24 个漫射颜色纹理贴图(漫射贴图属性)图像对象中的前五个,这些语句在图 14-14 的底部突出显示:

A336284_1_En_14_Fig14_HTML.jpg

图 14-14。

Instantiate diffuse1 through diffuse5 in loadImageAssets() using your first five PNG diffuse texture maps

diffuse1 = new Image("/gameboardsquare.png",  256, 256, true, true, true);
diffuse2 = new Image("/gameboardsquare2.png", 256, 256, true, true, true);
diffuse3 = new Image("/gameboardsquare3.png", 256, 256, true, true, true);
diffuse4 = new Image("/gameboardsquare4.png", 256, 256, true, true, true);
diffuse5 = new Image("/gameboardsquare5.png", 256, 256, true, true, true);

接下来,关闭 loadImageAssets()方法体。打开新的 createMaterials()方法体,将 Shader1 Java 语句复制并粘贴到其下四次。然后将它们重命名为 Shader2 到 Shader5。设置代表游戏棋盘正方形 diffuseMap 属性的图像对象,以引用您刚刚创建的 diffuse2 到 diffuse5 图像对象。

这可以通过使用以下十个 Java 语句来完成,它们在图 14-15 的底部用黄色和蓝色突出显示:

A336284_1_En_14_Fig15_HTML.jpg

图 14-15。

Copy and paste the Shader1 Java code block four times underneath itself to create Shader2 through Shader5

Shader1 = new PhongMaterial(Color.WHITE);
Shader1.setDiffuseMap(diffuse1);
Shader2 = new PhongMaterial(Color.WHITE);
Shader2.setDiffuseMap(diffuse2);
Shader3 = new PhongMaterial(Color.WHITE);
Shader3.setDiffuseMap(diffuse3);
Shader4 = new PhongMaterial(Color.WHITE);
Shader4.setDiffuseMap(diffuse4);
Shader5 = new PhongMaterial(Color.WHITE);
Shader5.setDiffuseMap(diffuse5);

完成游戏板的构建:象限 2 到 4

关闭 createMaterials()方法,然后重新打开 createGameBoardNodes()方法。使用Q1S1.setTranslate X ( 300 )将 location 语句添加到 Q1S1 对象中,将第一个子方块定位到我们希望的位置,在象限的开始,顺时针方向。

接下来,复制并粘贴您的三个 Q1S1 游戏板 square 语句四次,以创建其余的 square 对象,我们也必须重新配置这些对象,直到 X,Z 位置参数和着色器对象引用被关注。

Q2S2 只需要将自己定位在离 0,0 原点 150 个单位的位置,因为你的正方形是 150 乘 150。这是通过改变定位方法调用到.setTranslate X ( 150 )来实现的。确保还设置了.setMaterial( Shader2 )来引用正确的着色器,该着色器然后引用(并应用)diffuse2 图像对象作为 diffuseMap 属性。

Q2S3 是唯一不需要重新定位的方块,因为它将位于 0,0 原点。我在示例代码中添加了名为.setTranslate X ( 0 )的方法(但在 NetBeans 9 中没有)。请确保还设置了.setMaterial( Shader3 )来引用正确的着色器,该着色器然后引用(并应用)diffuse3 图像对象作为 diffuseMap 属性。

Q2S4 只需要将自己定位在距离 0,0 原点 150 单位的位置,但这次是在 Z 方向。这是通过改变定位方法调用到.setTranslate Z ( 150 )来实现的。确保设置.setMaterial( Shader4 )来引用正确的 Shader4 对象,该对象然后引用(并应用)diffuse4 图像对象作为 diffuseMap 属性。

Q2S5 需要从 0,0 开始在 Z 方向上定位自己 300 个单位。这是用定位方法调用.setTranslate Z ( 300 )来完成的。确保设置.setMaterial( Shader5 )来引用正确的 Shader5 对象,然后它引用(并应用)diffuse5 图像对象作为 diffuseMap 属性。图 14-16 中突出显示的 Java 代码应该如下所示:

A336284_1_En_14_Fig16_HTML.jpg

图 14-16。

Copy and paste the Q1S1 statements four times underneath themselves and reconfigure their method calls

private void createGameBoardNodes() {
    q1.setTranslateX(225);
    q1.setTranslateZ(225);
    Q1S1 = new Box(150, 5, 150);
    Q1S1.setTranslateX(300);
    Q1S1.setMaterial(Shader1);
    Q1S2 = new Box(150, 5, 150);
    Q1S2.setTranslateX(150);
    Q1S2.setMaterial(Shader2);
    Q1S3 = new Box(150, 5, 150);
    Q1S3.setTranslateX(0);        // This statement can be omitted, as default X location is 0
    Q1S3.setMaterial(Shader3);
    Q1S4 = new Box(150, 5, 150);
    Q1S4.setTranslateZ(150);
    Q1S4.setMaterial(Shader4);
    Q1S5 = new Box(150, 5, 150);
    Q1S5.setTranslateZ(300);
    Q1S5.setMaterial(Shader5);
    q2 = new Box(300, 5, 300);
    q2.setVisible(false);         // Set q2 through q4 quadrant objects to visible=false for now
}

在我们可以看到 3D 场景中呈现的这些新对象之前,我们需要将它们添加到 addNodesToSceneGraph()方法体中的 SceneGraph 层次结构中。将 Q1S2 到 Q1S5 框对象添加到 Q1 组对象,如图 14-17 中用黄色和浅蓝色突出显示的。

A336284_1_En_14_Fig17_HTML.jpg

图 14-17。

Add your other three Q2 to Q4 Group objects to the gameBoard Group and the other four squares to Q1

让我们也完成场景图层次的第二层(q2 到 q4 分支节点),并将 Q2 到 Q4 盒平面基本体添加到其他三个 Q2 到 Q4 组节点,以将游戏板的内部象限添加到场景图层次。我们在工作过程中的这一点上这样做,以便我们能够在游戏板的中心部分工作,因为我们是一次构建一个象限。

因为我们基本上完成了第一个象限,所以我们将其他三个放入场景图中,这样当我们构建剩余的游戏板象限和它们的游戏板方块时,它们将会呈现(可见),这些方块将围绕每个相应象限的周界附着到它们。

从优化的角度来看,我们只用了九个就为 2D UI 和 3D 游戏板组件创建了一个相对复杂的场景图层次。getChildren()。addAll()方法链 Java 编程语句,如图 14-17 所示。这是相对紧凑的,因为我们以高度组织的方式引用了几十个 2D 和 3D 游戏组件叶节点,并且只使用了九个场景图层次结构语句。

添加其他四个正方形和其他三个象限可以通过使用以下 Java 编程语句来完成,这些语句在图 14-17 的底部以黄色和浅蓝色突出显示:

root.getChildren().addAll(gameBoard, uiLayout);
gameBoard.getChildren().addAll(Q1, Q2, Q3, Q4);
Q1.getChildren().addAll(q1, Q1S1, Q1S2, Q1S3, Q1S4, Q1S5);

Q2.getChildren().addAll(q2);

Q3.getChildren().addAll(q3);

Q4.getChildren().addAll(q4);

图 14-18 显示了运行➤项目 JavaFX 9 代码测试的工作流程。正如你所看到的,我们已经有了四分之一的游戏板,它看起来非常适合组装所有这些素材的第一轮,包括 3D 盒子图元和 2D 纹理贴图图像,我们已经在类的顶部声明并即将创建。

A336284_1_En_14_Fig18_HTML.jpg

图 14-18。

Use the Run ➤ Project work process to see if the completed 3D game board quadrant is aligning properly

复制粘贴五个扩散图像语句,创建扩散 6 到 20,如图 14-19 所示。

A336284_1_En_14_Fig19_HTML.jpg

图 14-19。

Copy and paste 5 diffuse texture Image instantiations 3 times and create all 20 diffuse Image objects

关闭 loadImageAssets()方法体,现在漫反射图像实例化已经就绪,打开 createMaterials()方法,执行完全相同的操作,复制前五个着色器 Java 语句对,并在它们自身下面再粘贴三次。更改每个语句的编号部分,以便创建 Shader6 到 Shader20 Java 语句对。这些都可以在图 14-20 中用黄色突出显示。

A336284_1_En_14_Fig20_HTML.jpg

图 14-20。

Copy and paste 5 Shader PhongMaterial instantiations 3 times and create all 20 Shader objects

现在,让我们通过返回 createGameBoardNodes()方法体来创建游戏板的第二象限,并通过复制 Q1S1 到 Q1S5 语句来创建 Box 原语 Q2S1 到 Q2S5 的第二部分代码,将它们再次粘贴到它们自身的下面,然后更改对象名称和方法调用参数(这样,您就不必再次将这些 Java 语句的大部分键入 NetBeans 9 IDE)。

q2 盒子对象(第二象限)将需要沿 z 轴向外移动 300 个单位(象限大小为 300x300),因此 q2.setTranslateZ()方法参数需要从 225 增加到 525,以完成第二象限游戏板组件定位,如图 14-22 所示,如果您想向前看。

Q2S1 需要沿 z 轴(从 0,0 原点)将自己定位 450 个单位,因为 Q1S5 位于 300 加上 150,即 450。这是通过改变定位方法调用到.setTranslate Z ( 450 )来实现的。确保设置.setMaterial( Shader6 )来引用正确的着色器,该着色器引用(并应用)diffuse6 图像对象作为 diffuseMap 属性。

Q2S2 需要将自己沿 z 轴定位 600 个单位(从 0,0 原点),因为 450 加 150 等于 600。这是通过改变定位方法调用到.setTranslate Z ( 600 )来实现的。请确保还设置了.setMaterial( Shader7 )来引用正确的着色器,该着色器然后引用(并应用)diffuse7 图像对象作为 diffuseMap 属性。

Q2S3 需要将自己沿 z 轴定位 750 个单位(从 0,0 原点),因为 600 加 150 等于 750。这是通过改变定位方法调用到.setTranslate Z ( 750 )来实现的。确保还设置了.setMaterial( Shader8 )来引用正确的着色器,该着色器然后引用(并应用)diffuse8 图像对象作为 diffuseMap 属性。

Q2S4 也需要从 0,0 原点沿 z 轴将其自身定位 750 个单位,但这一次,我们需要在 X 方向上将这个方块推过 150 个单位,以便沿游戏棋盘布局的顶部向右移动。这是通过转换到使用两个定位方法调用来完成的。一个是.setTranslate X ( 150 ),另一个是.setTranslation Z ( 750 )。请确保将.setMaterial( Shader9 )设置为引用正确的 Shader9 对象,然后该对象将 diffuse9 图像对象作为 diffuseMap 属性进行引用(并应用)。

Q2S5 需要在 X 方向上从 0,0 开始定位 300 个单位,以及在 Z 方向上定位 750 个单位,使得该正方形位于该游戏板的顶部中间附近,在游戏板的另一侧,从正方形 1 开始。这也是使用两个位置方法调用完成的,对.setTranslate Z ( 750 )和对.setTranslate X ( 300 )。请确保将.setMaterial( Shader10 )设置为引用正确的 Shader10 对象,该对象随后会引用(并应用)diffuse10 图像对象作为 diffuseMap 属性。

用于构建游戏棋盘第二象限的 Java 代码如图 14-21 所示,位于构建第一象限的代码之后,应如下所示(为便于阅读,将代码隔开):

A336284_1_En_14_Fig21_HTML.jpg

图 14-21。

Instantiate and configure game board squares Q2S1 through Q2S5 inside of createGameBoardNodes()

private void createGameBoardNodes() {
    ...
    q2 = new Box(300, 5, 300);      // Java code creating a second quadrant for the gameboard
    q2.setTranslateX(225);
    q2.setTranslateZ(525);

    Q2S1 = new Box(150, 5, 150);
    Q2S1.setTranslateZ(450);
    Q2S1.setMaterial(Shader6);

    Q2S2 = new Box(150, 5, 150);
    Q2S2.setTranslateZ(600);
    Q2S2.setMaterial(Shader7);

    Q2S3 = new Box(150, 5, 150);
    Q2S3.setTranslateZ(750);
    Q2S3.setMaterial(Shader8);

    Q2S4 = new Box(150, 5, 150);
    Q2S4.setTranslateZ(750);
    Q2S4.setTranslateX(150);
    Q2S4.setMaterial(Shader9);

    Q2S5 = new Box(150, 5, 150);
    Q2S5.setTranslateZ(750);
    Q2S5.setTranslateX(300);
    Q2S5.setMaterial(Shader10);

    q3 = new Box(300, 5, 300);
    q3.setVisible(false);
    ...                            // The third quadrant configuration code will go in here
    q4 = new Box(300, 5, 300);
    q4.setVisible(false);
}

如图 14-22 所示,使用运行➤项目来确认游戏板的构建已经完成了一半!

A336284_1_En_14_Fig22_HTML.jpg

图 14-22。

Quadrants 1 and 2 are now coded and aligning properly

接下来为 gameBoard 组分支的最终场景图构造代码添加 Java 语句。你的 3D 场景层次应该如下图所示,在图 14-23 中用黄色和浅蓝色突出显示:

A336284_1_En_14_Fig23_HTML.jpg

图 14-23。

Add all remaining SceneGraph Node object “wiring” code to add the rest of the squares to the quadrants

root.getChildren().addAll(gameBoard, uiLayout);
gameBoard.getChildren().addAll(Q1, Q2, Q3, Q4);
Q1.getChildren().addAll(q1, Q1S1, Q1S2, Q1S3, Q1S4, Q1S5);
Q2.getChildren().addAll(q2, Q2S1, Q2S2, Q2S3, Q2S4, Q2S5);
Q3.getChildren().addAll(q3, Q3S1, Q3S2, Q3S3, Q3S4, Q3S5);
Q4.getChildren().addAll(q4, Q4S1, Q4S2, Q4S3, Q4S4, Q4S5);

接下来,让我们通过返回到 createGameBoardNodes()方法体并为 Box 原语 q3S1 到 Q3S5(以及 Q3 中心象限)创建第三部分代码,来为您的游戏板创建第三象限。只需复制 Q2S1 到 Q2S5 语句,并再次将它们粘贴到自身之下(在 q3 实例化和配置语句之后,将分组的节点逻辑地保存在 Java 代码体中)。接下来,您将再次更改您的对象名称和方法调用参数(这样您就不必再次将这些 Java 9 语句中的大部分输入到 NetBeans 9 IDE 中),以便从您的第一个象限开始对角定位您的方块。

q3 盒子对象(第三象限)将需要沿 x 轴和 z 轴向外移动 300 个单位(象限大小为 300x300),因此q3.setTranslateX()方法参数也需要从 225 增加到 525,以完成第三象限游戏板组件定位,如图 14-24 所示。

A336284_1_En_14_Fig24_HTML.jpg

图 14-24。

Use your Run ➤ Project work process to see that quadrants 1 to 4 are now coded and aligning properly

Q3S1 需要沿 x 轴从 0,0 原点定位 450 个单位,沿 z 轴定位 750 个单位。这是通过将您的 locational 方法调用更改为.setTranslateX(450)并离开(或者添加w,这取决于您复制了哪个 Java 代码).setTranslate Z ( 750 )。请确保还设置了.setMaterial( Shader11 )来引用正确的着色器编号,该编号然后引用(并应用)diffuse11 图像对象作为 diffuseMap 属性。

Q3S2 需要沿 x 轴从 0,0 原点定位 600 个单位,沿 z 轴定位 750 个单位。这是通过将位置方法调用更改为.setTranslate X ( 600 )并离开(或添加,取决于您复制的 Java 代码块).setTranslate Z ( 750 ))来完成的。请确保还设置了.setMaterial( Shader12 )来引用正确的着色器编号,该编号然后引用(并应用)diffuse12 图像对象作为 diffuseMap 属性。

Q3S3 需要沿 z 轴从 0,0 原点定位 750 个单位,沿 x 轴定位 750 个单位,这使其与 0,0 原点成对角线。这是通过将位置方法调用更改为.setTranslate Z ( 750 ),然后添加第二个.setTranslate X ( 750 )方法调用来完成的。请确保还设置了.setMaterial( Shader13 )来引用您正确的 Phong 着色器对象编号,该编号引用(并应用)diffuse13 图像对象作为您的 diffuseMap 属性。

Q3S4 还需要从 0,0 原点沿 X 方向将自己定位 750 个单位,但这一次,我们还需要将这个方块沿 Z 方向再向下拉 150 个单位,以便沿游戏棋盘布局的右侧向下移动。这通过再次使用两个定位方法调用来完成。一个将是.setTranslate Z ( 600 ),另一个仍将被设置为.setTranslation X ( 750 )。同样,请确保将.setMaterial( Shader14 )设置为引用匹配的 Shader14 对象,然后该对象引用并应用 diffuse14 图像对象作为 diffuseMap 属性。

Q3S5 需要在 X 方向上从 0,0 和 450 个单位定位自己 750 个单位在 Z 方向上,以便这个正方形位于这个游戏板的中间右侧附近。这也是使用两个位置方法调用完成的,对.setTranslate X ( 750 )和对.setTranslate Z ( 450 )。请确保将.setMaterial( Shader15 )设置为引用正确的 Shader15 对象,然后该对象引用(并应用)diffuse15 图像对象作为 diffuseMap 属性。

用于构建游戏棋盘第二象限的 Java 代码应该如下所示:

private void createGameBoardNodes() {
    ...
    q3 = new Box(300, 5, 300);    // Java code creating a third quadrant for the gameboard
    q3.setTranslateX(525);
    q3.setTranslateZ(525);
    Q3S1 = new Box(150, 5, 150);
    Q3S1.setTranslateZ(750);
    Q3S1.setTranslateX(450);
    Q3S1.setMaterial(Shader11);
    Q3S2 = new Box(150, 5, 150);
    Q3S2.setTranslateZ(750);
    Q3S2.setTranslateX(600);
    Q3S2.setMaterial(Shader12);
    Q3S3 = new Box(150, 5, 150);
    Q3S3.setTranslateZ(750);
    Q3S3.setTranslateX(750);
    Q3S3.setMaterial(Shader13);
    Q3S4 = new Box(150, 5, 150);
    Q3S4.setTranslateZ(600);
    Q3S4.setTranslateX(750);
    Q3S4.setMaterial(Shader14);
    Q3S5 = new Box(150, 5, 150);
    Q3S5.setTranslateZ(450);
    Q3S5.setTranslateX(750);
    Q3S5.setMaterial(Shader15);
    ...                           // Your fourth quadrant configuration code will go in here
    q4 = new Box(300, 5, 300);
    q4.setVisible(false);
}

最后,让我们通过返回 createGameBoardNodes()方法体并为 Box 原语 Q4S1 到 Q4S5 创建第四段代码来创建游戏板的第四象限。只需复制你的 Q3S1 到 Q3S5 语句(以及 q4 语句)并再次粘贴在它们自己的下面;然后更改对象名和方法调用参数(这样您就不必再次在 NetBeans 9 中键入所有这些 Java 语句)。

q4 盒子对象(第四象限)将需要沿 z 轴向下移动 300 个单位,因此您的 q4.setTranslateZ()方法参数需要从 525 减少到 225,以完成第四象限游戏板组件定位,如图 14-24 所示。

Q4S1 需要沿 Z(从 0,0 原点)定位 300 个单位,并沿 X 向右定位 750 个单位。这是通过改变定位方法调用到.setTranslate Z ( 300 )来实现的。确保将.setMaterial( Shader16 )设置为引用正确的着色器,该着色器然后引用并应用 diffuse16 图像对象作为 diffuseMap 属性。

Q4S2 需要沿 Z(从 0,0 原点)定位 150 个单位,沿 x 定位全部 750 个单位。这是通过将定位方法调用更改为.setTranslate Z ( 150 )来完成的。请确保将.setMaterial( Shader17 )设置为引用正确的着色器,然后该着色器引用并应用 diffuse17 图像对象作为 diffuseMap 属性。

Q4S3 只需要将自己定位在距离 0,0 原点沿 X 方向 750 个单位的位置,因为它位于右角。这意味着唯一需要的定位方法调用是.setTranslate X ( 750 )。确保将.setMaterial( Shader18 )设置为引用正确的着色器,该着色器然后引用并应用 diffuse18 图像对象作为 diffuseMap 属性。

Q4S4 只需要将自己定位在距离原点 0,0 沿 X 方向 600 个单位的位置,就可以将这个方块向原点方向拉回 150 个单位,以便将其沿游戏棋盘布局的底部向左移动。这是通过仅使用.setTranslate X ( 600 )方法调用来完成的。确保设置.setMaterial( Shader19 )来引用正确的 Shader9 对象,该对象引用(并应用)diffuse19 图像对象作为 PhongMaterial diffuseMap 属性。

Q4S5 需要在 X 方向上从 0,0 开始定位自己 450 个单位,因此您的最后一个游戏方块位于该游戏板的底部中间附近。这也是使用一个定位方法调用.setTranslate X ( 450 )来完成的。确保设置.setMaterial( Shader20 )来引用正确的 Shader20 对象,然后它引用(并应用)diffuse20 图像对象作为 diffuseMap 属性。用于构建游戏棋盘最后一个象限的 Java 代码应该类似于下面的 Java 代码:

private void createGameBoardNodes() {
    ...
    q4 = new Box(300, 5, 300);      // Java code creating a second quadrant for the gameboard
    q4.setTranslateX(525);
    q4.setTranslateZ(225);
    Q4S1 = new Box(150, 5, 150);
    Q4S1.setTranslateX(750);
    Q4S1.setTranslateZ(300);
    Q4S1.setMaterial(Shader16);
    Q4S2 = new Box(150, 5, 150);
    Q4S2.setTranslateX(750);
    Q4S2.setTranslateZ(150);
    Q4S2.setMaterial(Shader17);
    Q4S3 = new Box(150, 5, 150);
    Q4S3.setTranslateX(750);
    Q4S3.setMaterial(Shader18);
    Q4S4 = new Box(150, 5, 150);
    Q4S4.setTranslateX(600);
    Q4S4.setMaterial(Shader19);
    Q4S5 = new Box(150, 5, 150);
    Q4S5.setTranslateX(450);
    Q4S5.setMaterial(Shader20);
}

图 14-24 显示了运行➤项目 Java 代码测试的工作流程,展示了一个完成的 3D 游戏板。

有一两个渲染异常是可见的,例如 Q1S2 方块,它看起来像是位于 Q1S1 方块之上。这很奇怪,因为代码是精确的,并且基于 150 的倍数,所以它应该像其他代码一样精确对齐。因为这个问题不在代码上,我们将在下一章看看如何处理这个渲染异常。使用。setRotationAxis(旋转。y 轴)和。setRotate(30)方法将游戏板旋转 30 度,就像您之前所做的那样,以查看旋转游戏板层次所使用的枢轴点。这个 Java 9 测试代码应该放在你的 createBoardGameNodes()方法中,如图 14-25 中突出显示的。

A336284_1_En_14_Fig25_HTML.jpg

图 14-25。

Add .setRotationAxis(Rotate.Y_AXIS) and a .setRotate(30) method call to the gameBoard Group object

我们这样做的原因是,在我们离开这一章之前,我们需要检查一下,这个游戏板是否作为一个层级在工作。也就是说,如果我们围绕 y 轴旋转它,它会以游戏板组的中心为支点,还是会围绕游戏板的 0,0 原点角方块的中心旋转(旋转)?

正如你在图 14-26 中看到的,游戏板组对象确实在定义它自己的 0,0 中心,使用它所有组节点子节点的平均中心。我们从游戏棋盘的 6 × 150 (900)构造中知道,这个 0,0 中心在原点之间的 X 和 Z (450,450)上偏移 450(900 的一半),或者在直线单位(对角线上)上偏移 625(450+450 的 1/2)。通过用可整除的整数来构造事物,我们将能够在后面的章节中使用整数(int)作为我们的游戏代码,这为 JavaFX 游戏引擎节省了内存和处理。

A336284_1_En_14_Fig26_HTML.jpg

图 14-26。

Rotate the gameBoard Group Node object 30 to 45 degrees to see where it defines its center for rotation

在我们结束本章之前,让我们看看是否可以通过使用不同的相机(算法)类来改善我们的游戏板渲染结果,因为我们似乎有一些盒子表面渲染顺序和定位问题。如果不是相机对象在方块之间造成了这些轻微的脊,我们将不得不进一步寻找这个问题的解决方案,因为我们需要获得一个逼真的游戏板,看起来像我们在现实生活中用来玩游戏的纸板游戏板。正如你现在所知道的,pro Java 9 游戏开发是一个迭代的过程,所以你知道我们最终会解决它!

更改摄像机:使用 ParallelCamera 类

接下来,我将把 camera 对象从 PerspectiveCamera 更改为 ParallelCamera,这既是为了给你一些使用它们的经验,也是为了看看这种面顺序渲染问题(看起来在正方形中重叠)在两个 Camera 类算法之间(在两个 Camera 子类之间)是否有任何不同。这很简单,只需将类顶部的声明从 PerspectiveCamera 更改为 ParallelCamera,并确保对 createBoardGameNodes()中的实例化语句进行相同的更改,如此处以及图 14-27 所示:

A336284_1_En_14_Fig27_HTML.jpg

图 14-27。

Change the Scene camera object to use a ParallelCamera class (algorithm) instead of PerspectiveCamera

ParallelCamera camera;
...
camera = new ParallelCamera();
camera.setTranslateZ(0);
camera.setNearClip(0.1);
camera.setFarClip(5000.0);

接下来,让我们进入 gameButton 事件处理代码块并移除。setFieldOfView(1)方法调用只需注释掉该行代码,正如您所看到的,这是一个巧妙而常见的代码调试技巧。

我们这样做是因为新的 ParallelCamera 对象不支持该特定的方法调用。我们还会将 camera.setTranslateZ()方法调用更改为游戏板的对角线值,我计算该值以将相机视图放置在游戏板中心(625)。

我还会将 camera.setTranslateX()方法调用设置为游戏板宽度 225 的四分之一,如图 14-28 中间高亮显示的 camera 对象代码所示。

A336284_1_En_14_Fig28_HTML.jpg

图 14-28。

Remove the FOV setting code, change .setTranslateX() to 225, .setTranslateZ() to 625, and Y = 0

我正在改进这段代码,以便更好地查看游戏板,并使其更好地适应窗口,这样当我们在随后的动画和游戏性章节中旋转它时,它将在任何旋转方向上完美地适应场景,同时它也在动画中随机旋转以选择主题象限。

我要做的下一件事是“调整”onStart()中的相机值,以适应窗口中的游戏板。正如你在图 14-29 中看到的,我们需要展平摄像机视图(30 度)并稍微调整 X、Y、Z 位置。

A336284_1_En_14_Fig29_HTML.jpg

图 14-29。

The game board is almost fitting perfectly in the window; let’s adjust the camera angle and spacing next!

正如你在图 14-30 中看到的,我将旋转调整到 30°,Z 轴调整到 500°,Y 轴调整到-300°,X 轴调整到-260°。

A336284_1_En_14_Fig30_HTML.jpg

图 14-30。

Set the camera rotation at 30 degrees, the Z location to 500, the Y location to -300, and the X location to -260

正如您在图 14-31 中看到的,我们现在已经设置了游戏板的“极限”以适合窗口,使用这些新的相机设置和 ParallelCamera 算法,这似乎比 PerspectiveCamera 更少扭曲游戏板。如果我们现在旋转游戏板,它应该都留在窗口(可视)区域内。

A336284_1_En_14_Fig31_HTML.jpg

图 14-31。

Use your Run ➤ Project work process to see if the new camera algorithm and settings fit the game board

摘要

在第十四章中,我们构建了 JavaFX SceneGraph 层次结构的 3D 部分,即根节点下的 gameBoard 组(节点子类)分支节点(我们之前已经创建了 uiLayout StackPane 节点子类)。我们创建了一个由四个名为 q1 到 q4 的游戏棋盘象限组分支节点组成的子组,每个节点包含四分之一的游戏棋盘内部,这些节点是名为 Q1 到 Q4 的长方体基本体,以匹配它们的组节点父对象。在这些象限下面,我们分组了五个游戏棋盘方形叶节点对象,它们将在游戏设计中与象限游戏功能相对应。

我们创建了两个新的方法体,一个用于创建游戏棋盘方块,因为有几十个方块,另一个用于创建 Phong 材质,因为将会有几十个方块!这让事情井井有条。我们现在有八个(如果算上 main()的话有九个,它仍然处于引导代码状态)方法体,其中七个是定制的,我们有 400 多行 Java 代码,全部组织成逻辑折叠和扩展部分。我们为每个游戏棋盘方块创建了彩色着色器,将适当的漫反射纹理贴图映射到每个方块上。

在第十五章中,我们将进一步完善我们的游戏板设计和 Java 代码组织,并为游戏玩家创建一种在 3D 空间中操纵游戏板的方式,以便他们(或游戏 AI 代码)可以访问他们感兴趣的游戏板内容和主题部分。

十五、3D 游戏界面创建:使用球体原语创建一个界面节点

现在,您已经创建了多层游戏板组节点(子类)层次,并对其进行了测试,以查看它是否像一个 3D 模型一样旋转,是时候添加一个球体 3D 图元了,这样我们就可以创建一个 3D 用户界面元素,供用户在游戏过程中用来创建随机的“旋转”。我们还将为这些设置 Phong 着色器对象,并再次使用 GIMP 2.8.22 为游戏板象限创建(从头开始)其余的漫反射纹理贴图,以及为球体图元创建 3D“spinner”UI 纹理贴图。该旋转器将在每个玩家的回合中使用,以随机旋转游戏板来选择主题类别。你总是需要将你的 pro Java 9 游戏与其他游戏区分开来,所以我们将是独一无二的,并旋转游戏板本身来选择象限(主题类别),这不能用现实生活中的游戏板来完成,但可以用虚拟的 i3D 游戏板来完成。我们将把 Java 9 代码添加到类的顶部以及 createMaterials()、addNodesToSceneGraph()和 loadImageAssets()方法体中。我们将创建自定义的 PNG24 漫反射纹理添加到您的项目源(/src)文件夹中。我们将重新安排 createGameBoardNodes()方法来重新组织象限 3D 图元,我们将在本章中完成游戏板设计的内部部分。我们将把象限一起放在这个方法的顶部。

在这一章中,我们还将看看你是如何解决在创作专业品质游戏的过程中遇到的问题的。在这种情况下,有一个人脸渲染的问题,是我们在对游戏板建模时遇到的;它应该渲染顺利(顶部平坦),但渲染与游戏板广场重叠,这是不应该发生的。也有一些小的 Y(高度)变化,一旦它们被漫反射纹理映射,就会使象限看起来凹陷。(请记住,在第十四章中,中心象限在没有应用着色器的情况下看起来是平坦的,但是一旦我们继续对它们进行处理,它们也会显示出这些渲染工件,这一点您将在本章的后面部分看到。)

完成 3D 资源:主题象限和微调器

让我们继续设计和开发棋盘游戏的 3D 组件,包括使用 GIMP 为游戏棋盘内部开发纹理贴图,以及使用 3D spinner UI 元素为游戏创建随机旋转,就像在现实生活中的棋盘游戏一样。我们将使用 Java 9 (JavaFX API)类来完成这项工作,这样我们就可以只使用 Java 9 APIs 和我们的数字图像资源(背景图像和纹理贴图)来创建游戏。到目前为止,我们已经用了大约 400 行代码完成了这项工作!在本章中,我们将添加另外 10 %( 440)来“修饰”象限,并在屏幕的左上角添加一个“spinner”UI 元素。在类的顶部,我们需要做的第一件事是再添加五个名为 diffuse21 到 diffuse25 的图像对象声明,以及五个名为 Shader21 到 Shader25 的声音材料对象声明。这显示在下面的 Java 代码中,并且在图 15-1 中的类的顶部显示为绿色(一些代码用黄色和蓝色突出显示):

A336284_1_En_15_Fig1_HTML.jpg

图 15-1。

Add objects at the top of your class for diffuse texture maps and shaders for your quadrants and a spinner

Image ... diffuse21, diffuse22, diffuse23, diffuse24, diffuse25;
PhongMaterial ... Shader21, Shader22, Shader23, Shader24, Shader25;

剪切并粘贴最后五个 diffuse16 到 diffuse20 图像对象声明,创建五个名为 diffuse21 到 diffuse25 的新声明,并对它们进行配置以供使用,如图 15-2 所示:

A336284_1_En_15_Fig2_HTML.jpg

图 15-2。

Create five new diffuse image map objects in your loadImageAssets() method and configure them for use

diffuse21 = new Image("/gameboardquad1.png", 512, 512, true, true, true);
diffuse22 = new Image("/gameboardquad2.png", 512, 512, true, true, true);
diffuse23 = new Image("/gameboardquad3.png", 512, 512, true, true, true);
diffuse24 = new Image("/gameboardquad4.png", 512, 512, true, true, true);
diffuse25 = new Image("/gameboardspin.png",  256, 256, true, true, true);

我们将在本章的下一节使用 GIMP 创建这些漫射纹理贴图数字图像素材。打开 createMaterials()方法体,并添加相应的 Shader21 到 Shader25 对象实例化和配置语句,这些语句“连接着色器”以引用漫反射纹理贴图图像对象资源。

如果您愿意,也可以使用复制和粘贴来完成此操作,就像您对漫反射纹理贴图图像对象所做的那样。创建新着色器并将其引用到漫反射纹理贴图图像对象素材的 Java 代码应类似于以下 Java 代码语句块,在图 15-3 中高亮显示:

A336284_1_En_15_Fig3_HTML.jpg

图 15-3。

Create five new Shader PhongMaterial objects in your createMaterials() method and wire them to diffuseMap objects

Shader21 = new PhongMaterial(Color.WHITE);
Shader21.setDiffuseMap(diffuse21);
Shader22 = new PhongMaterial(Color.WHITE);
Shader22.setDiffuseMap(diffuse22);
Shader23 = new PhongMaterial(Color.WHITE);
Shader23.setDiffuseMap(diffuse23);
Shader24 = new PhongMaterial(Color.WHITE);
Shader24.setDiffuseMap(diffuse24);
Shader25 = new PhongMaterial(Color.WHITE);
Shader25.setDiffuseMap(diffuse25);

你现在将不得不再次利用 GIMP 来创建你的象限和旋转纹理贴图来专业地纹理化你的棋盘游戏元素。当前版本是 2.8.22。

创建您的象限和微调器漫射颜色纹理贴图

使用 GIMP 文件➤新工作进程创建一个透明(空)的漫反射纹理贴图合成,这次使其为 512×512 像素,因为象限框对象 q1 到 q4 在两个轴上都是方形框对象的两倍大(或总共四倍大)。这在数学上与游戏棋盘方格所用的 256 像素纹理贴图尺寸加倍相匹配。单击圆形(或椭圆)选择工具,在图 15-4 的顶部高亮显示,并再次使用工具图标下方的椭圆选择工具选项选项卡,为圆形设置精确的大小和位置,因为我们希望白色圆形完美地位于每个游戏板象限的中心。我将圆的大小设置为 400(相等的宽度和高度值创建一个完美的圆;任何变化都将创建一个椭圆或卵形),我将其余部分(512 - 400 = 112 / 2 = 56)相除,得到我的 X,Y 位置值 56,它也以红色突出显示。

A336284_1_En_15_Fig4_HTML.jpg

图 15-4。

Create the four quadrant texture maps at 512-pixel resolution using 60 percent of the corner square color value

选中背景层,确保前景色样设置为白色,并使用编辑➤填充前景颜色(白色)选项在合成层堆栈的最底部为所有四个象限纹理贴图创建白色中心,如图 15-4 所示。右键单击背景层,使用新建层命令,创建一个名为 GameBoardQuadrant 的新层。使用“选择➤反转”菜单序列来反转选择,并选择 GameBoardQuadrant 层来指定该层保存您的外部颜色填充。打开 gameboardsquare3.png 文件,使用吸管工具选择它的橙色值。单击 FG 颜色(前景)样本,调用“拾色器”对话框,并将值(V)滑块设置为 60%颜色(40%白色),为位于对角的象限创建角正方形的彩色版本。如图 15-2 中的图像对象代码所示,使用编辑➤填充 FG 颜色来填充中心圆周围的区域,使用文件➤导出为将文件保存到项目的/src 文件夹,并将其命名为 boardgamequad1.png。重复这个过程:创建一个新的层,得到一个角方块颜色值,变亮 40%,填充前景色,将图像导出为 PNG24 来创建另外三个编号的 boardgamequad PNG24 素材,它们显示在图 15-4 最左侧各自的层中。你也可以在图 15-4 的右上角看到我打开来采样颜色值的 gameboardsquare 3、8、13 和 18 图像素材。吸管工具位于椭圆选择工具的右下方。

当我们在 GIMP 中时,让我们打开我们的纹理贴图创建 GIMP XCF 文件,其中包含我们在第十三章中创建的所有不同的贴图类型,包括着色器和材质,并使用您的沙滩球漫反射纹理创建一个 3D 旋转球纹理,当它旋转时读取“SPIN ”, S 和 I 为白色(彩色), P 和 N 为黑色。

打开 Pro _ Java _ 9 _ Games _ Development _ Texture _ Maps GIMP XCF 文件,选择文本工具,如图 15-5 红框所示。设置您的文本选项使用 Arial Heavy,设置字体大小为 48,并选择抗锯齿。点击色样,选择白色,在绿色条纹中间输入大写字母 S,如图 15-5 所示。右键单击 S 图层,选择复制图层工具,设置文本工具色样为黑色,选择黄色的 S,如图 15-5;然后键入大写的 P 来替换 s。您可以使用右箭头键使用移动工具(四个相连的箭头)来移动文本元素,以便它与 s 保持精确对齐。将其置于白色条纹的中心,然后对 I 和 N 文本元素重复该过程,直到您创建了单词 SPIN。

A336284_1_En_15_Fig5_HTML.jpg

图 15-5。

Create the word SPIN twice on your beach ball texture map to create your animated spinner texture map

一旦你做了所有四个字母一次,你可以使用相同的右键单击一个层,并使用复制层工作过程来复制这些字母;然后使用移动工具将字母定位在其他四个条纹上,如图 15-6 右侧所示。

A336284_1_En_15_Fig6_HTML.jpg

图 15-6。

Replicate the four SPIN letters twice in the center of each of the eight stripes, at exactly the same height

要使用 GIMP 移动工具,首先单击文本元素,这将向移动工具显示您要移动的内容,然后使用右箭头键将字母定位在下一个条纹上。使用右箭头键而不是用鼠标拖动字母将使字母保持在完全相同的像素高度位置,使字母彼此完全对齐。

正如你将在图 15-6 中看到的,这个工作过程将产生一个统一的、专业的地图结果。尽管您的字母在 GIMP 画布上看起来很拥挤,但当映射到球体基本体的曲率上时,结果是非常可读的,即使是在动画中,因为曲面的曲率似乎将这些字母“拉伸”得更远。

一旦你对旋转纹理贴图感到满意,使用文件➤导出为菜单序列,并保存你的游戏面板旋转。png 文件到您的C:/Users/Name/Documents/NetBeansProjects/JavaFXGame/src/文件夹中,如图 15-7 所示。请注意,我们的 boardgamesquare PNG24 文件经过了很好的优化,有 680 字节,而我们的 boardgamequad 文件每个只有 10KB。如果你点击一个文件名,你会在对话框的右边得到一个很好的纹理贴图预览。这是一个很棒的特性,特别是对于相似的文件名,因为你可以点击任何文件名来预览它,GIMP 会把那个文件名放在 name 字段中;那你就把末尾的数字改了当打字快捷键就行了!

A336284_1_En_15_Fig7_HTML.jpg

图 15-7。

Name your new diffuse color texture map file gameboardspin.png and then save it into your /src folder

接下来,让我们开始将漫射纹理贴图应用到我们的 3D 棋盘游戏元素中,并完成 3D 贴图的创建。

3D 游戏棋盘象限的纹理映射:Java 代码

打开 createGameBoardNodes()方法体,将 q1 到 q4 的对象代码剪切并粘贴到方法的顶部,这样象限框元素 q1 到 q4 就可以在同一个 Java 9 语句块中进行实例化和配置。您现在可以更清楚地看到相对于每个象限的不同 225 和 525 组合的 X、Z 移动模式,没有相同的 X、Z 坐标对,它们会与您的象限重叠。

q1 .setMaterial( Shader21 );添加到第一个中,如图 15-8 所示,使用以下 Java 代码:

A336284_1_En_15_Fig8_HTML.jpg

图 15-8。

Cut and paste all quadrant Box primitive code to one place and start adding shaders using .setMaterial()

q1 = new Box(300, 5, 300);
q1.setMaterial(Shader21);
q1.setTranslateX(225);
q1.setTranslateZ(225);
q2 = new Box(300, 5, 300);
q2.setTranslateX(225);
q2.setTranslateZ(525);
q3 = new Box(300, 5, 300);
q3.setTranslateX(525);
q3.setTranslateZ(525);
q4 = new Box(300, 5, 300);
q4.setTranslateX(525);
q4.setTranslateZ(225);

图 15-9 显示了游戏板象限 1 纹理映射和测试渲染的运行➤项目工作流程。正如你所看到的,一旦你的漫反射纹理贴图被应用,面顺序渲染问题就出现了!

A336284_1_En_15_Fig9_HTML.jpg

图 15-9。

Select Run ➤ Project to render and preview the first quadrant texture map application (the face order bug appears)

接下来,添加您的。setMaterial()方法调用您的其他三个象限框图元,并在方法调用参数列表中引用您正确的 Shader22 到 Shader24 PhongMaterial 对象。完成后,Shader 对象到 Box 原语的连接应该看起来像下面的 Java 代码,在图 15-10 中用黄色突出显示:

A336284_1_En_15_Fig10_HTML.jpg

图 15-10。

Complete the Shader object wiring to Box primitives for all quadrants so we can see the finished gameboard

q1 = new Box(300, 5, 300);
q1.setMaterial(Shader21);
q1.setTranslateX(225);
q1.setTranslateZ(225);

q2 = new Box(300, 5, 300);
q2.setMaterial(Shader22);
q2.setTranslateX(225);
q2.setTranslateZ(525);

q3 = new Box(300, 5, 300);
q3.setMaterial(Shader23);
q3.setTranslateX(525);
q3.setTranslateZ(525);

q4 = new Box(300, 5, 300);
q4.setMaterial(Shader24);
q4.setTranslateX(525);
q4.setTranslateZ(225);

正如你在图 15-11 中看到的,象限盒图元现在呈现出与 i3D 游戏板的其余部分相同的面渲染顺序问题。让我们暂时停止编码,看看是否可以找到其他 Java 开发人员在 JavaFX 9 游戏开发中遇到这种特殊 3D 模型人脸渲染问题的证据。正如你所想象的,用来做这项研究的工具是搜索引擎。

A336284_1_En_15_Fig11_HTML.jpg

图 15-11。

The diffuse texture mapping looks very professional, other than the face depth and rendering anomalies

在 JavaFX 9 dev 论坛上提交错误报告之前,让我们看看我是如何找到面部顺序呈现问题的解决方案的,我尝试使用 Google 搜索引擎和目标明确的关键字来完成这个问题。

使用 Google 解决 JavaFX 异常:使用 StackOverflow

要找到有类似问题的开发人员,请使用 Google 搜索引擎,输入您在屏幕上看到的最常见或最可能的问题描述。在这种情况下,这将是“错误的重叠形状”或“框面顺序渲染的问题”有时你可能不得不尝试几个不同的关键字字符串。在这种情况下,有几个正确的答案,那就是打开一个叫做深度缓冲的特性。这是一种处理密集型算法,因此默认情况下是关闭的。由于我们也得到一些锯齿状的边缘,我们可以打开另一个处理密集型算法,称为反走样。这两个都可以在重载的 Scene()构造函数中访问,所以只需对我们的 Scene scene 对象实例化做一个简单的修改就可以解决这两个问题!以下是关于此问题及其解决方案的两个 StackOverflow 答案的示例:

stackoverflow.com/questions/19589210/overlaping-shapes-wrong-overlapping-shapes-behaviour

   --OR--

stackoverflow.com/questions/28567195/javafx-8-3d-z-order-overlapping-shape-behaviour-is-wrong

场景的重载构造函数方法允许您将深度缓冲和抗锯齿作为 3D 场景对象的默认行为,类似于以下 Java 代码:

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

因此,我们需要添加 depthBuffer=true 和 SceneAntialiasing。平衡到我们在 createBoardGameNodes()方法中使用的 Scene()构造函数,正如你在图 15-12 (红色矩形)中看到的,我把它添加到了scene = new Scene(root, 1280, 640); Java 9 场景对象实例化语句的末尾。这将切换您的构造函数方法调用,以利用不同的重载构造函数方法来创建您的 3D 场景。

让我们添加一个名为 spinner 的 3D UI 元素,玩家可以使用它来随机旋转游戏板以选择主题。

创建 3D 用户界面元素:3D 微调器随机发生器

现在让我们重用我们的球体原始代码和沙滩球纹理图来创建一个 3D 用户界面(UI)元素,玩家可以单击它来旋转棋盘,以选择一个随机的主题(topic)类别。在类的顶部声明球体并将其命名为 spinner。然后用半径 60 实例化它,并用 Shader25 和 X,Y 位置-200,-500 配置它,这使它位于屏幕的左上角。使用 Y 旋转轴并将旋转值设置为 25 度,尝试将单词 SPIN 面向用户。您的 Java 代码,如图 15-12 所示,将如下所示:

A336284_1_En_15_Fig12_HTML.jpg

图 15-12。

Add a Sphere primitive named spinner, set the material and translation parameters, and fix the face order render bug

Sphere spinner;
...

spinner = new Sphere(60);
spinner.setMaterial(Shader25);
spinner.setTranslateX(-200);
spinner.setTranslateY(-500);
spinner.setRotationAxis(Rotate.Y_AXIS);
spinner.setRotate(25);

scene = new Scene(root, 1280, 640, true, SceneAntialiasing.BALANCED);

在我们能够渲染您的 3D 场景并查看新的微调器 UI 以确定我们是否需要以任何方式调整漫射纹理贴图之前,我们需要将其添加到 JavaFX 场景图。我将把它添加到顶部,直接在根目录下,因为 3D 用户界面最终会有自己的层次结构,就像 2D uiLayout 和 3D 游戏板一样。通过这种方式,如果我们想在任何时候影响 3D UI 元素作为一个整体,我们可以使用一行代码来引用 3D UI 分支,这将影响它下面的所有叶节点。现在,微调器将是根下面的一个叶节点。添加微调器的 Java 代码如图 15-13 所示,应该如下所示:

A336284_1_En_15_Fig13_HTML.jpg

图 15-13。

Add your spinner Sphere object to the top of the SceneGraph hierarchy using root.getChildren().addAll()

root.getChildren().addAll(gameBoard, uiLayout, spinner);

现在,我们可以使用“运行➤项目”工作流程并测试我们的新 Java 代码,将球体旋转器 UI 添加到我们在这个 3D 场景中创建的棋盘游戏中。我们还将能够看到,通过使用更复杂的重载 Scene()构造函数方法(具有五个参数,而不是只有三个参数),添加抗锯齿和深度缓冲算法(分别检查正确的面顺序渲染和在渲染过程中对粗糙边缘应用平滑)是否解决了我们的视觉质量问题。

正如你在图 15-14 中看到的,3D 游戏板层次结构,包括二十几个 3D 基本对象,如游戏板方块和游戏板象限,现在被渲染为一个内聚的 3D 模型。它最终看起来像你在大多数流行的棋盘游戏中看到的棋盘游戏(纸板游戏板),并且每个方块和象限将能够在你的代码中被单独访问和控制,即使游戏板模型看起来只是 3D 场景中的一个 3D 对象。这是我们在过去几章中努力学习和实现的。

A336284_1_En_15_Fig14_HTML.jpg

图 15-14。

The face rendering order problem has been fixed, and we're now getting a smooth, thin cardboard game

另一方面,spinner UI 元素没有给我们最初想要的视觉效果,它是一个沙滩球类型的对象,前面写着单词 SPIN。这是可以的,因为我们知道 pro Java 9 games 开发是一个迭代的细化过程,所以让我们考虑如何缩小单词 SPIN down,以便在 Sphere 原语上一次显示四个字母,而不是像当前呈现的那样只有两个字母。

缩小文本以使单词 SPIN 适合球体图元的四分之一,以及增加彩色条纹的数量(和厚度)的最简单方法是使该纹理贴图为 512 像素纹理。这将缩小所有的文本元素,使四个适合,我们可以复制和粘贴条纹和颜色移动他们添加更多的颜色。

接下来,让我们回到 GIMP,看看增强 spinner UI 漫反射纹理贴图的工作过程。

增强 3D 微调器纹理贴图:提高分辨率

如果您在优化的工作流程中使用 GIMP 的工具和算法,为 3D spinner UI 元素创建更详细的 512x512 像素纹理贴图的工作流程会比您想象的容易得多。我们可以在仅仅十几或两个“动作”中使分辨率加倍、条纹加倍、条纹颜色加倍、文本元素加倍,GIMP 会以我们所要求的最高质量水平为您完成所有的像素操作。

图 15-15 显示了移动的 GIMP 合成结果。首先要做的是添加另外 256 个像素到文档的右侧,使用图像➤画布大小➤宽度=512 ➤调整大小(按钮),这将添加 256 个像素的透明度到纹理贴图合成的右半部分。选择背景层和白色色样,使用画笔工具(第四行,第四个图标)填充背景层的右半部分,使其为 100%白色。接下来,右键单击彩色地图图层,选择复制图层选项,这将创建彩色地图复制图层,如图 15-15 所示。选择该图层,使用彩色➤色相-饱和度算法(菜单序列),将四种颜色都移动 60 度左右,创建四种不同的颜色,如图 15-15 所示。接下来,要将(y 或高度)尺寸调整为 512 个匹配像素,这一次使用图像➤缩放图像菜单序列,通过单击宽度和高度之间的链图标解锁纵横比,并将高度值设置为 512。这将拉伸颜色条来填充图像,这样你就不必像最初创建沙滩球纹理图那样做大量的选择-移动-填充工作。这种缩放操作也将使文本组件变高,这将使它们在球体微调器 UI 中更具可读性,尤其是在它旋转的时候。最后,右键单击 S、P、I 和 N 的第 2 层,并为每个层创建第 3 层和第 4 层。使用移动工具和右箭头键来精确定位它们。

A336284_1_En_15_Fig15_HTML.jpg

图 15-15。

Use a 256-pixel beach ball texture map to create a more detailed 512-pixel Sphere spinner UI texture map

最后,使用“文件”“导出为”覆盖项目/src/文件夹中的当前 gameboardspin.png 文件。由于该文件名已经在 diffuse25 图像对象实例化语句中被引用,您所要做的就是将宽度和高度值从 256 更改为 512,当映射到相同大小的球体图元上时,这将有助于减少球体上的条纹和文本元素(字母)的大小,因此将显示四个字母(SPIN)而不是两个字母(SP,如图 15-14 所示)。此时,您所要做的就是将旋转值调整到 20 到 30 度之间,以便单词 SPIN 将位于名为 spinner 的球体对象的中心,这样用户就知道当单击这个球体 spinner UI 对象时会发生什么。

您的 diffuse25 对象实例化的新 Java 9 语句应该类似于下面的 Java 代码,在图 15-16 中也用黄色和蓝色突出显示:

A336284_1_En_15_Fig16_HTML.jpg

图 15-16。

Modify the width and height resolution parameters and change them from 256 pixels to 512 pixels each

diffuse25 = new Image("/gameboardspin.png", 512, 512, true, true, true);

接下来需要做的事情是“调整”所有微调器对象配置设置,使球体图元变大一点,并将其移近屏幕的角落,以便它远离游戏板。我做了半径 64 和 Y 平移-512 来进一步上移。我发现 30 度的旋转值使单词 SPIN 居中。图 15-17 中突出显示的 Java 代码应该如下所示:

A336284_1_En_15_Fig17_HTML.jpg

图 15-17。

Tweak the Sphere spinner settings to radius 64 to make the spinner bigger and rotate it to 30 degrees to see SPIN

Sphere spinner;
...

spinner = new Sphere(64);
spinner.setMaterial(Shader25);
spinner.setTranslateX(-200);
spinner.setTranslateY(-512);
spinner.setRotationAxis(Rotate.Y_AXIS);
spinner.setRotate(30);

图 15-18 显示了运行➤项目的 Java 代码测试工作流程。正如你所看到的,游戏板现在看起来非常专业,3D 旋转器 UI 看起来像一个旋转器,并使用大的可读字母标记为 SPIN。

A336284_1_En_15_Fig18_HTML.jpg

图 15-18。

The spinner Sphere UI element now looks more like a spinner, and the word SPIN is now visible to the user

现在我们已经完成了 3D 棋盘游戏的设计和编码,我们可以回到 javafx.graphics 模块中一些常用于游戏的更具技术性的 JavaFX 类。技术含量最高的领域之一是 3D 动画,我们将在接下来的内容中对旋转器和游戏板等进行动画制作,从而将 3D boardGame 节点层次带入第四维时间!之后,我们可以添加交互性,使之成为 i3D 桌游!在我们继续添加动画之前,我只需要让事情变得完美和一个 3D 旋转器到位。

摘要

在第十五章中,我们构建了 3D“spinner”球体原始 UI 元素,它允许用户在游戏板上随机旋转以选择主题象限。我们还完成了游戏板的纹理映射,并找出了如何修复面部渲染异常,这些异常使游戏板模型(游戏的核心)无法正确渲染,因此无法获得专业的外观。该解决方案涉及使用更复杂的场景对象实例化,包括打开深度缓冲算法的标志以及启用所有 3D 对象的场景范围抗锯齿的常数。这消除了 Y 维度(高度)面部渲染错误,以及我们在游戏板组件边缘看到的锯齿状边缘。

我们为四个游戏棋盘中心象限和 spinner UI 元素创建了五个新的纹理贴图,该元素允许玩家旋转棋盘以确定他们的下一步行动,在这种情况下,是一个教育主题。

我们添加了漫射纹理贴图图像对象和利用这些纹理贴图的 Phong 着色器定义。我们还添加了 Java 代码,将 spinner UI 添加到 SceneGraph 层次结构中,并获得了更多使用 GIMP 的实践。

在第十六章中,我们将学习 JavaFX 9 中所有强大的动画相关过渡类。

十六、3D 游戏动画创建:使用动画过渡类

现在,您已经创建了多层游戏板组节点(子类)层次结构,对该层次结构下的所有 3D 组件进行了纹理处理,确保您的 3D 游戏板模型中心旋转,并创建了一个 3D 旋转器 UI 来随机旋转该游戏板 3D 模型(层次结构)以选择一个随机象限,现在是时候使用自定义的createAnimationAssets()方法为旋转器添加动画对象以创建在游戏过程中使用的随机“旋转”了。我们还将设置 3D 对象鼠标单击事件处理代码来触发动画和逻辑,这将在进行旋转之前随机化您的旋转变换参数。

在本章中,我们将详细介绍抽象动画和过渡超类以及所有强大的属性过渡子类,您可以在 i3D 棋盘游戏中将它们实现为不同类型的动画对象。我们将为您的游戏板和旋转器制作旋转动画,以及旋转器的平移(运动)动画。

动画 3D 素材:动画超类

public abstract Animation 类扩展了 Object,保存在 javafx.animation 包中,其他与动画相关的类也是如此,其中一些我们将在游戏中使用,本章将详细介绍。Animation 超类有两个直接已知的子类,Timeline 和 Transition。Transition 有十个预定义的动画(算法)子类,随时可以应用到您的游戏开发中,因此我们将重点关注它们,因为它们可以立即有效地使用。javafx.animation 包可以写一整本书,而我只有一章,所以我将介绍用来创建 pro Java 9 游戏的最有效的动画类。

动画超类的 Java 9 类层次结构向我们展示了该类是临时编码的,以提供对象动画功能,因为它没有自己的超类,因此看起来像下面的类层次结构:

java.lang.Object
  > javafx.animation.Animation

抽象 Animation 类不能直接创建动画对象,但它确实为 JavaFX API 中使用的所有动画类提供了核心功能。唯一的例外是 AnimationTimer 类(一个脉冲引擎),它实现了一个核心定时器脉冲引擎(因此,它更像一个定时器类,而不是动画类),非常适合基于 2D 精灵的游戏。我在 Java 8 游戏开发入门中进入了这个类,在那里我将详细介绍 i2D 游戏开发。在本书中,我更关注 i3D 游戏的开发,所以我将借此机会介绍一些其他有用的(也是固定的或预编码的)动画过渡类。

通过设置 cycleCount 属性,动画可以运行有限的次数。要使任何动画“乒乓”(即从开始到结束再到开始来回运行),请将 autoReverse 属性设置为 true 否则,使用一个假布尔值,我们将在我们的 pro Java 9 游戏中使用它来随机地向一个方向旋转 i3D 游戏板。

要在实例化和配置动画对象后播放它,可以调用 play()方法或 playFromStart()方法。动画对象的 currentRate 属性设置您的速度和方向。通过反转 currentRate 的数值,您可以切换您的播放方向。每当“持续时间”属性被“满足”(耗尽、结束、耗尽、达到、过期等)时,动画将停止。

通过使用具有不定常数的 cycleCount 属性,可以为动画对象设置不定的持续时间(有时称为循环或无限循环)。以这种方式配置的动画对象会重复运行,直到调用 stop()方法。这将停止正在运行的动画,并将其回放重置到起点(属性设置)。也可以通过调用 pause()来暂停动画,下一个 play()调用将从动画暂停的地方继续播放动画,除非您使用. playFromStart()方法调用。接下来让我们看看动画超类的属性。这些都是由 Transition 超类及其所有子类继承的,所以在本书的剩余部分中,您将在您的 Pro Java 9 游戏开发代码中使用它们。

autoReverse BooleanProperty 用于定义动画对象是否应该在交替循环中反转其方向。currentRate 是一个 ReadOnlyDoubleProperty,用于指示动画对象的其他设置正在播放的当前速度(和方向,由正值或负值表示)。

current time ReadOnlyObjectProperty用于定义动画对象回放位置,cycleCount IntegerProperty 用于定义播放动画对象的周期数。cycle duration ReadOnlyObjectProperty是一个只读变量,可用于指示动画对象的一个周期的持续时间。这是从时间 0 到动画结束所花费的时间,默认速率为 1。

delay ObjectProperty 用于延迟动画的开始时间,onFinished ObjectProperty 属性包含在动画对象播放结束时触发的 ActionEvent。rate DoubleProperty 用于定义动画播放的速度和方向。注意,由于硬件的限制,这个速率并不总是可能的,所以有一个 currentRate 属性来保存实际达到的回放速率。

status ReadOnlyObjectProperty<animation.status>属性包含动画对象的枚举状态常量。枚举动画。Status helper 类包含三个常量:PAUSED、RUNNING 和 STOPPED。</animation.status>

total duration ReadOnlyObjectProperty属性保存一个只读变量以指示动画对象的总持续时间,该变量乘以 cycleCount 属性以计算它重复的次数。所以,duration 是一个周期,totalDuration 等于(delay + (duration * cycleCount))。

动画有一个静态(嵌套)类,它是一个动画。Status 类,保存表示状态的可能状态的枚举常量。这些包括暂停、运行和停止。

Animation 有一个数据字段,即 static int 不定字段,该字段用于指定一个动画,该动画将无限重复自身,直到。调用 stop()方法。

Animation 有两个重载的构造函数,一个是简单的(空参数区域)构造函数,创建一个空的或未配置的动画对象,另一个用目标帧速率配置动画对象。这些构造函数方法(它们的子类的构造函数方法格式,因为它们不能在您的代码中直接使用)应该看起来像下面的 Java 代码:

protected Animation()                        // Protected: Cannot Be Directly Instantiated
protected Animation(double targetFramerate)

你可以用几十种方法来控制你的动画对象,在这一章中,就是各种各样的过渡子类。这些从动画类继承方法,通过转换类,到各种属性转换类,我们将用于 Java 9 游戏。

autoReverseProperty()方法调用返回一个 BooleanProperty,该属性定义动画对象是否将在(交替)播放周期之间反转其方向。currentRateProperty()方法调用返回一个只读 double 变量,用于指示动画对象播放的当前速度和方向。

a。rateProperty()方法调用返回动画预期播放的速度和方向的双值。. statusProperty()方法调用返回动画的 ReadOnlyObjectProperty <animation.status>状态,而. currentTimeProperty()方法调用返回动画对象的播放位置。那个。cycleCountProperty()使用代表 cycleCount 属性的整数值返回动画对象中的循环数。</animation.status>

那个。cycleDurationProperty()方法返回一个只读变量,表示动画一个周期的持续时间,即从时间 0.0 开始以默认速率 1.0 播放到动画结束所用的时间。那个。delayProperty()方法调用返回延迟动画对象开始的延迟属性的持续时间。

a。totalDurationProperty()方法调用返回只读的持续时间属性设置,以指示动画对象的总持续时间。值得注意的是,该值将包括所有动画重复周期。

那个。getCuePoints()方法调用返回包含动画对象提示点的 ObservableMap 。这些提示点应该用来标记动画对象中的重要位置。那个。getCurrentRate()方法调用将为动画对象的 CurrentRate 属性返回双精度值。

的。getCurrentTime()方法调用将返回动画对象的 CurrentTime 属性的值。

的。getCycleCount()方法调用将返回动画对象 CycleCount 属性的整数值。getCycleDuration()方法调用将返回 CycleDuration 属性的值。的。getDelay()方法调用将返回 Delay 属性的值。

a。getOnFinished()方法调用将返回 OnFinished 属性的 EventHandler 值,而. getRate()方法调用将返回 Rate 属性的 double 值。那个。getStatus()方法调用将返回动画。状态属性的状态值。

那个。getTargetFramerate()方法调用将返回目标帧速率,即动画对象运行的最大帧速率(使用每秒帧数)。

那个。getTotalDuration()方法调用将返回 TotalDuration 属性的持续时间值。

那个。isAutoReverse()方法调用将返回 AutoReverse 属性的值。

虚空。jumpTo(持续时间)方法调用将跳转到动画对象中的给定位置,就像 void 一样。jumpTo(String cuePoint)方法调用,使用 cuePoint 参数而不是 Duration 参数。

那个。onFinishedProperty()方法调用返回在动画对象播放结束时触发的 ObjectProperty >动作。虚空。pause()方法调用用于暂停动画对象的回放循环。虚空。play()方法调用将从动画对象的当前位置按照 rate 属性指示的方向播放动画对象。

虚空。playFrom(持续时间)方法调用是一种方便的方法,它将从特定的位置播放动画,就像 void 一样。使用 cuePoint 而不是持续时间的 playFrom(String cuePoint)方法调用。一片空白。playFromStart()方法调用将从动画对象的初始位置向前播放动画对象。一片空白。setAutoReverse(布尔值)方法调用可用于设置 AutoReverse 属性的值。

虚空。setCycleCount(int value)方法调用可用于设置 CycleCount 属性的值。受保护的空间。setCycleDuration(持续时间值)方法调用可用于设置 CycleDuration 属性的值。虚空。setDelay(持续时间值)方法调用可用于设置 Delay 属性的值。虚空。set OnFinished(EventHandlervalue)方法调用可用于设置 onFinished 属性的值。

虚空。setRate(double value)方法调用可用于设置动画对象的 Rate 属性值。受保护的空间。setStatus(动画。Status value)方法调用可用于设置 Status 属性的常数值。

虚空。stop()方法调用用于停止动画对象播放循环,需要注意的是,该方法调用会将播放头重置到其初始开始位置,因此它可以用作重置。接下来,让我们看看另一个抽象超类 Transition,它是动画的一个子类,用于创建属性转换。

自动对象动画:过渡超类

公共抽象过渡超类与其子类一起保存在 javafx.animation 包中,这些子类是预定义的算法,用于应用不同类型的属性动画,而不必使用时间轴或动画计时器或设置关键帧。因此,Transition 子类是动画类的最高(最高级)形式,非常适合 pro Java 9 游戏开发,因为它们允许您将时间集中在游戏开发上,而不是重新发明 Java 动画代码。这就是为什么我们要介绍这些类来快速实现游戏动画!Transition 超类的 Java 类层次结构如下所示:

java.lang.Object
  > javafx.animation.Animation
    > javafx.animation.Transition

已知的可以快速有效地实现以增强 Java 游戏开发过程的直接子类包括 RotateTransition、ScaleTransition 和 TranslateTransition,以调用基本的 3D 对象转换(这些也可以用于 2D、文本和 UI 元素);以及用于处理 2D(即向量)对象的 FadeTransition、FillTransition、StrokeTransition 和 path transition(fade transition 也适用于文本和 UI 元素)。还有两个子类用于创建复合(或复杂)动画,它们无缝地结合了这些其他类型的属性转换。其中包括 ParallelTransition 和 SequentialTransition,parallel transition 同时执行属性转换,sequential transition 串行执行一串属性转换(一个接一个)。还有一个 PauseTransition 子类用于将“等待状态”引入复杂的动画,这将允许更多的运动真实感添加到您试图创建的特殊动画效果中。

抽象过渡超类包含了基于过渡的动画所需的所有基本功能。这个类提供了一个定义属性动画的框架,如果你愿意,也可以用来定义你自己的转换子类。游戏中使用的大多数类型的过渡都已经提供了(淡入淡出、变换、路径等)。),因此您所要做的就是实现已经创建、调试和优化的代码。

Transition 子类都需要实现一个叫做.interpolate(double)的方法。只要 Transition 子类(object)正在运行,就会在动画对象的每个循环中调用该方法。除了。interpolate()方法,任何扩展 Transition 的子类都需要使用Animation.setCycleDuration(javafx.util.Duration)方法调用来设置动画周期的持续时间。

例如,应该使用 duration 属性设置此持续时间(如 RotateTransition.duration 中所示)。但是它也可以由扩展类来计算,如在 ParallelTransition 和 FadeTransition 中所做的那样。

过渡类有一个类型为 ObjectProperty 的插值器属性,用于控制每个过渡周期的加速和减速时间。从 Animation 超类继承的属性包括 autoReverse、currentRate、currentTime、cycleCount、cycleDuration、delay、onFinished、status、Rate 和 totalDuration 属性。还有一个动画。Status 嵌套类,继承自 Animation 类。

有两个重载的构造函数;一个构造一个空的转换子类对象,另一个构造一个帧速率配置的转换子类。这些看起来像下面的两个构造函数方法:

Transition()
Transition(double targetFramerate)

最后,这个抽象超类增加了六个方法,其中大部分都与 interpolate 属性相关。这个类也继承了我们在上一节中介绍的方法。getCachedInterpolator()方法返回启动 Transition 子类时设置的插值器属性。那个。getInterpolator()方法将获取插值器属性的值,而 void。set Interpolator(Interpolator value)方法将为插值器属性设置一个值。如前所述,受保护的抽象虚空。interpolate(double)方法需要由 Transition 子类提供,而。interpolatorProperty()方法控制加速和减速的时间。

最后是。getParentTargetNode()方法调用将返回为 Transition 子类播放动画的目标节点。接下来,让我们详细看看其中一个 Transition 子类,然后我们可以在 JavaFXGame Java 代码中实现它来旋转(动画旋转)游戏板组节点。

制作 3D 对象旋转动画:使用 RotateTransition 类

public final RotateTransition 类将用于创建旋转动画并扩展 Transition 超类。它将与所有其他动画和动画计时器相关的类一起存储在 javafx.animation 包中。RotateAnimation 子类的 Java 类层次结构应该类似于下面的 Java 类层次结构:

java.lang.Object
  > javafx.animation.Animation
    > javafx.animation.Transition
      > javafx.animation.RotateTransition

RotateTransition 类(对象)可用于创建持续时间与其持续时间设置一样长的旋转动画。这是通过定期更新它所附加到的节点的 rotate 变量来完成的。应使用度数指定旋转角度值。旋转从 fromAngle 属性(如果提供)开始,否则将从节点的当前(上一个)旋转值开始。旋转将停止使用 toAngle 值(如果提供),否则将使用 start 值加上 byAngle 值。如果 toAngle 和 byAngle 都已指定,toAngle 值将优先。

RotateTransition 将属性添加到从动画和过渡中继承的属性中,这有助于定义旋转过渡算法。这些属性包括 axis ObjectProperty 属性,用于指定 RotateTransition 对象的旋转轴;node ObjectProperty 属性,用于指定应该受 RotateTransition 影响的目标节点对象;以及 duration ObjectProperty 属性,用于指定 RotateTransition 的持续时间。

byAngle DoubleProperty 可用于指定从 RotateTransition 开始的增量停止角度值。fromAngle DoubleProperty 可用于指定 RotateTransition 的起始角度值。toAngle double 属性可用于指定 RotateTransition 的停止角度值。

前面讨论的嵌套类、字段和属性是从动画和过渡中继承的。

RotateTransition 类有三个重载的构造函数方法。一个创建未配置的 RotateTransition,一个创建持续时间配置的 RotateTransition,一个创建持续时间和节点对象配置的 RotateTransition。这三个构造函数方法看起来像下面的 Java 代码:

RotateTransition()
RotateTransition(Duration duration)
RotateTransition(Duration duration, Node node)

除了从这个类扩展的动画和过渡超类继承的方法之外,还有 19 个方法专门用于这个类。那个。axisProperty()方法调用使用 ObjectProperty 格式指定 RotateTransition 的旋转轴。那个。byAngleProperty()方法为 RotateTransition 对象指定增量停止角度值,该值是从开始角度的偏移量。

那个。durationProperty()方法使用 ObjectProperty 指定 RotateTransition 的持续时间。那个。fromAngleProperty()方法使用 DoubleProperty 指定此 RotateTransition 的起始角度值。那个。getAxis()方法调用使用 Point3D 对象获取轴属性的值。

那个。getByAngle()方法将获得 ByAngle 属性的 double 值。那个。getFromAngle()方法调用将获得 FromAngle 属性的 double 值。那个。getToAngle()方法调用将获得 ToAngle 属性的 double 值。那个。getNode()方法调用将获取节点属性的节点对象值,而。getDuration()方法调用将获得 Duration 属性的值。受保护的空间。如你所知,interpolate(double value)方法调用必须由 Transition 超类的子类实现提供。那个。nodeProperty()方法指定 RotateTransition 的目标 ObjectProperty

虚空。setAxis(Point3D value)方法调用用于设置属性轴的值。虚空。setByAngle(double value)方法调用用于设置 ByAngle 属性的值。虚空。setDuration(持续时间值)方法调用用于设置属性 Duration 的值。虚空。setFromAngle(double value)方法调用用于设置 FromAngle 属性的值。虚空。setNode(节点值)方法调用用于设置属性节点的值。虚空。setToAngle(double value)方法调用用于将属性值设置为 Angle。那个。toAngleProperty()方法使用 DoubleProperty 指定 RotateTransition 的停止角度值。接下来让我们实现 rotGameBoard 和 rotSpinner RotateTransition 对象,给您一些实际操作的经验。

RotateTransition 示例:设置 RotateAnimation 素材

让我们使用下面的 Java 语句创建一个 createAnimationAssets()方法来保存 RotateTransition、TranslateTransition 和其他 Transition 子类对象,在图 16-1 中用黄色(和红色波浪下划线)突出显示:

A336284_1_En_16_Fig1_HTML.jpg

图 16-1。

Add a createAnimationAssets() method call at the end of the custom method call list in the start() method

createAnimationAssets();

记得双击 javafxgame 中的创建方法“createAnimationAssets()”。JavaFXGame 选项,并让 NetBeans 为您编写一个引导方法。在本章的这一节中,您将使用 RotateTransition 对象实例化和配置代码替换占位符 Java 代码。

你需要做的第一件事是声明一个 RotateTransition 对象在类的顶部使用,并将其命名为 rotGameBoard,因为这是该对象将要做的事情。在你的内心。createAnimationAssets()方法,实例化 rotGameBoard 对象并配置为播放五秒;然后将其连接到游戏板组节点,如下面的 Java 9 代码所示,并在图 16-2 中用浅蓝色和黄色突出显示:

A336284_1_En_16_Fig2_HTML.jpg

图 16-2。

Declare a rotGameBoard object at the top of your class and instantiate it inside createAnimationAssets()

RotateTransition rotGameBoard;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
}

现在,您可以开始使用各种。set()方法调用,您在本章的前一节中已经了解了。使用设置 Y 旋转轴。setAxis(旋转。并使用. setCycleCount(1)方法调用将 cycleCount 属性设置为一个周期。使用. setRate(0.5)方法调用 rotGameBoard 对象,将 rate 属性设置为 50%的速度。核心动画对象设置的 Java 语句应该类似于下面的 Java 9 语句,它们在图 16-3 的底部突出显示:

A336284_1_En_16_Fig3_HTML.jpg

图 16-3。

Configure the rotGameBoard RotateTransition object with a y-axis, cycleCount of 1, and rate of 50 percent speed

RotateTransition rotGameBoard;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
}

你已经知道为什么我们用 y 轴旋转;然而,你可能想知道为什么我们只使用一个周期。原因是,一旦我们通过指定 fromAngle 和 toAngle 值使这个 RotateTransition 交互,这些值将在每个 rotGameBoard.play()方法调用之前使用来自随机旋转生成器的代码进行设置,我们将在后面编写代码,我们将使用这些角度之间的差异来控制旋转的次数(目前这是 1080 或三次旋转);因此,我们仅使用一个循环。出于代码测试的目的,我使用了三次旋转。

速率设置 1 太快了,无法获得平滑的旋转动画,游戏板不应该旋转得那么快,所以我将这个默认值 1.0 减少了 50%到 0.5,以向您展示速率变量如何为您提供微调的速度控制。

接下来,让我们添加所需的插值器类常数规格,这将是默认的线性,因为我们需要一个平滑、均匀的旋转。这是使用。setInterpolator()方法调用和插值器。线性常数。最后,我们要添加两个最重要的配置语句,它们告诉 RotateTransition 引擎旋转的开始角度(fromAngle 属性)和结束角度(toAngle 属性)。使用这些将允许我们控制旋转在哪个象限开始(45、135、225 或 315)和结束。现在,我们将使用从 45 度角开始的三次完整旋转(1080 ),对于 toAngle 来说是 1125°。要开始(并测试)动画,您还需要一个. play()方法调用,如下面完整的 Java 方法体所示,并在图 16-4 的底部以黄色和浅蓝色突出显示:

A336284_1_En_16_Fig4_HTML.jpg

图 16-4。

Configure the rotGameBoard object with a LINEAR interpolator, a fromAngle of 45, and a toAngle of 1125

RotateTransition rotGameBoard;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotGameBoard.setFromAngle(45);
    rotGameBoard.setToAngle(1125);
    rotGameBoard.play();
}

图 16-5 显示了运行➤项目的工作过程,其中一个游戏板处于其旋转周期的中间。屏幕截图无法显示平滑的运动,但您可以知道游戏板不在其四个象限的“静止”位置之一(45、135、225、315 度),因为游戏板的点不在屏幕底部的中心。在图 16-5 中,当 3D 游戏板组节点还在动画时,我按了一个 PrintScreen 键。

A336284_1_En_16_Fig5_HTML.jpg

图 16-5。

Use your Run ➤ Project work process, click Start Game, and watch your gameboard spin around smoothly

同样重要的是要注意,当您测试您的动画代码时,您需要在应用启动时立即单击 Start Game 按钮 UI 元素(稍后,这将通过单击 3D spinner UI 元素来触发,您可能已经猜到了)。这是为了让您可以看到您的动画特性,这是我们在本章中开发的,因为目前您的 Java 9 代码在构造和配置动画(过渡子类)对象后立即开始播放生命周期。所以,点击你的开始游戏 2D 用户界面按钮,只要它出现!

稍后,当我们进入如何捕捉 3D 对象上的鼠标点击(或屏幕触摸)时,例如您的 spinner UI 元素,我们将通过单击 spinner UI 元素来触发 rotGameBoard.play(),以随机旋转游戏板来选择新的象限。当下一个玩家的回合准备好时,我们将触发 rotSpinner.play(),以便他们可以旋转游戏板。在本书的剩余部分,我们将开发这个动画代码的复杂性。

在本章的后面,我们将使用 TranslateTransition 和 RotateTransition,使用 ParallelTransition,这将允许我们将 3D spinner UI 元素在视图内外进行动画处理,以便玩家知道何时使用它来随机旋转游戏板,以选择一个新的象限(一个新的内容主题动物-植物-矿物或地标类别)用于游戏循环。

接下来,让我们添加 rotSpinner RotateTransition 对象。首先,通过在类顶部的 rotGameBoard 对象名称后添加 rotSpinner 对象名称,将 RotateTransition 声明转换为复合语句。剪切并粘贴 rotGameBoard 语句,将 rotGameBoard 更改为 rotSpinner,并确保将实例化的节点参数从 GameBoard 更改为 Spinner。从角度改变到 30 度(你在第十五章中开发的初始值)和角度到 1110 度(1080 + 30)。您的 Java 9 代码应该看起来像下面的方法体,它也在图 16-6 的底部突出显示:

A336284_1_En_16_Fig6_HTML.jpg

图 16-6。

Add a rotateSpinner RotateTransition object and configure it with the same parameters as rotGameBoard

RotateTransition rotGameBoard, rotSpinner;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotGameBoard.setFromAngle(45);
    rotGameBoard.setToAngle(1125);
    rotGameBoard.play();
    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5);
    rotSpinner.setInterpolator(Interpolator.LINEAR);
    rotSpinner.setFromAngle(30);
    rotSpinner.setToAngle(1110);
    rotSpinner.play();
}

使用运行➤项目工作流程查看游戏板和旋转器的旋转,如图 16-7 所示。

A336284_1_En_16_Fig7_HTML.jpg

图 16-7。

Select Run ➤ Project and click Start Game to preview the game board and spinner rotation

正如你所看到的,唯一的问题是你的“SPIN”微调器是向后旋转的,我们希望单词 SPIN 向前旋转,所以我们需要通过将 fromAngle 设置为 30 和 toAngle 设置为-1050(1080 = 30-1050)来改变方向。这里显示了最终的 Java 代码块,在图 16-8 中用黄色和蓝色突出显示:

A336284_1_En_16_Fig8_HTML.jpg

图 16-8。

Adjust the rotSpinner.setToAngle() method call to spin in a negative direction so the spinner UI spins forward

RotateTransition rotGameBoard, rotSpinner;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotGameBoard.setFromAngle(45);
    rotGameBoard.setToAngle(1125);
    rotGameBoard.play();
    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5);
    rotSpinner.setInterpolator(Interpolator.LINEAR);
    rotSpinner.setFromAngle(30);
    rotSpinner.setToAngle(-1050); // Reverse rotation direction using a negative toAngle value
    rotSpinner.play();
}

接下来,让我们看看平移过渡,它可用于在 3D 场景中以 X、Y 或 Z 维度移动对象。我们将使用它来将我们的 spinner UI 元素带到(离开)屏幕上,因为在游戏过程中需要它来允许玩家随机旋转游戏板来选择他们的新主题象限。

制作节点移动的动画:使用 TranslateTransition 类

public final TranslateTransition 类扩展了 public abstract Transition 超类,保存在 javafx.graphics 模块的 javafx.animation 包中。TranslateTransition 创建移动(平移)动画,其持续时间与其 duration 属性一样长。通过以插值器常数定义的间隔更新正在设置动画的节点的 translateX、translateY 和 translateZ 变量(属性)来创建运动。翻译将从“from”值(fromX,fromY,fromZ)开始,如果提供了一个;否则,算法将使用节点对象的当前位置(translateX,translateY,translateZ)值。翻译停止在“to”值(toX,toY,toZ),如果提供的话;否则,它将使用起始值加上 byX、byY 或 byZ 值。如果同时指定了“到”(toX,toY,toZ)和“到”(byX,byY,byZ)值,则“到”值(toX,toY,toZ)优先。

java.lang.Object
  > javafx.animation.Animation
    > javafx.animation.Transition
      > javafx.animation.TranslateTransition

TranslateTransition 类有 11 个属性,其中 9 个涉及到 X、Y 和 Z 3D 坐标的 to、from 和 by 规范。另外两个是持续时间属性和节点属性,它们定义动画的时间长度以及它影响的节点对象。byX 属性用于为 TranslateTransition 指定增量停止 X 坐标双精度值,该值从起始值计算得出。byY 属性用于为 TranslateTransition 指定从起始值计算的增量停止 Y 坐标双精度值。byZ 属性用于为 TranslateTransition 指定从起始值计算的增量停止 Z 坐标双精度值。fromX 属性用于指定 TranslateTransition 的起始 X 坐标双精度值。fromY 属性用于指定 TranslateTransition 的起始 Y 坐标双精度值。fromZ 属性用于指定 TranslateTransition 的起始 Z 坐标双精度值。toX 属性用于指定 TranslateTransition 的停止(静止或最终)X 坐标值。toY 属性用于指定 TranslateTransition 的停止(静止或最终)Y 坐标值。toZ 属性用于指定 TranslateTransition 对象的停止(静止或最终)Z 坐标值。

TranslateTransition 有三个重载的构造函数方法;一个为空,一个指定了持续时间,一个指定了持续时间和节点属性。它们看起来像这样:

TranslateTransition()
TranslateTransition(Duration duration)
TranslateTransition(Duration duration, Node node)

这个类有将近 36 种方法,其中 27 种(9 组,每组 3 种)处理 from、to 和 by 属性。这是因为对于每个 X、Y 和 Z 属性,都有一个. get()、一个. set()和一个. property()方法。还有一些方法可用于持续时间、节点和插值器属性。所有的 X、Y 和 Z 方法都使用 double 值。A .byXProperty()方法用于将停止 X 坐标值指定为从 TranslateTransition 开始的增量偏移量。那个。byYProperty()方法用于将偏移增量停止 Y 坐标值指定为距 TranslateTransition 起点的偏移。那个。byZProperty()方法用于将增量停止点 X 坐标值指定为距 TranslateTransition 起点的偏移量。

那个。fromXProperty()方法调用用于指定 TranslateTransition 的起始 X 坐标值。

那个。fromYProperty()方法调用用于指定 TranslateTransition 的起始 Y 坐标值。

那个。fromZProperty()方法调用用于指定 TranslateTransition 的起始 Z 坐标值。

那个。getByX()方法调用用于获取 ByX 属性的值。那个。getByY()方法调用用于获取 ByY 属性的值。那个。getByZ()方法调用用于获取 ByZ 属性的值。

那个。getFromX()方法调用用于从 FromX 获取属性值。那个。getFromY()方法调用用于从 mY 获取属性值。那个。getFromZ()方法调用用于从 FromZ 获取属性值。那个。getToX()方法调用用于获取属性 ToX 的值。那个。getToY()方法调用用于获取 ToY 属性的值。那个。getToZ()方法调用用于获取属性 ToZ 的值。

虚空。setByX(double value)方法调用用于设置(指定)属性 ByX 的值。虚空。setByY(double value)方法调用用于设置(指定)属性值 ByY。虚空。setByZ(double value)方法调用用于设置(指定)ByZ 属性的值。虚空。setFromX(double value)方法调用用于设置(指定)属性 FromX 的值。虚空。setFromY(double value)方法调用用于设置(指定)属性 FromY 的值。虚空。setFromZ(double value)方法调用用于设置(指定)属性 FromZ 的值。

虚空。setToX(double value)方法调用用于设置(指定)属性 ToX 的值。虚空。setToY(double value)方法调用用于设置(指定)属性 ToY 的值。一片空白。setToZ(double value)方法调用用于设置(指定)属性 toX 的值。

那个。toXProperty()方法调用用于指定 TranslateTransition 对象的停止 X 坐标值。那个。toYProperty()方法调用用于指定 TranslateTransition 对象的停止 Y 坐标值。那个。toZProperty()方法调用用于指定 TranslateTransition 对象的停止 Z 坐标值。

那个。durationProperty()方法调用将返回 TranslateTransition 的当前持续时间属性。使用. getDuration()方法调用来获取 TranslateTransition duration 属性的持续时间值。虚空。setDuration(持续时间值)可用于设置(指定)Duration 属性的持续时间值。

那个。nodeProperty()方法调用将返回 TranslateTransition 的目标节点 node 属性。那个。getNode()方法调用将获取(读取)TranslateTransition 的节点属性的节点对象引用值。虚空。setNode(节点值)方法调用将为 TranslateTransition 节点属性设置节点值。虚空。插值(double frac)方法调用总是需要由 Transition 的子类提供。

接下来,让我们实现一个 TranslateTransition 动画对象,该对象将 spinner UI 元素移入和移出屏幕。这些动画对象最终将被命名为 moveSpinnerOn 和 moveSpinnerOff。之后,我们将进入 ParallelTransition 类,并结合移动和旋转来旋转屏幕上的 spinner UI 元素,从左角到右角。

TranslateTransition 示例:设置平移动画资源

让我们将 TranslateTransition 动画对象添加到您的游戏项目中,方法是在类的顶部声明一个名为 moveSpinner 的对象,然后在 RotateTransition Java 代码之后,在 createAnimationAssets()方法中实例化它。引用微调器节点,并使用五秒钟的持续时间。接下来,使用以下 Java 语句,将 moveSpinnerOn 动画对象配置为在屏幕顶部移动 1150 X 个单位(实际上是 1350 个单位,因为微调器当前处于-200),并将 cycleCount 属性设置为一个周期,在图 16-9 中以黄色突出显示:

A336284_1_En_16_Fig9_HTML.jpg

图 16-9。

Declare a moveSpinnerOn TranslateTransition at the top of the class and instantiate it in createAnimationAssets()

TranslateTransition moveSpinnerOn;
...

moveSpinnerOn = new TranslateTransition(Duration.seconds(5), spinner);
moveSpinnerOn.setByX(1150);
moveSpinnerOn.setCycleCount(1);

接下来,让我们看一下 ParallelTransition 类,因为我们需要使用此对象算法来将 spinner rotSpinner 动画对象与 moveSpinnerOn 动画对象相结合,这样您的最终结果是一个在屏幕顶部移动的同时旋转的 spinner。

合并动画属性:使用 ParallelTransition 类

公共 final 类 ParallelTransition 扩展了抽象 Transition 超类,可以在 javafx.animation 包中找到,这个包可以在 javafx.graphics 模块中找到。该过渡并行播放一系列动画对象,这意味着同时(一个接一个被称为串行或顺序)。如果没有使用方法调用(通常是构造函数方法)显式指定 Node 属性,则此转换的子级将继承 Node node 属性。其原因是 ParallelTransition 只是将现有的动画对象合并在一起,因此在合并的动画中可能已经指定了一个节点。ParallelTransition 的 Java 类层次结构如下所示:

java.lang.Object
  > javafx.animation.Animation
    > javafx.animation.Transition
      > javafx.animation.ParallelTransition

ParallelTransition 类只有一个本地属性,即 ObjectProperty 节点属性,它是组合动画将应用到的节点对象。如果未指定节点,将使用子动画对象节点属性。如果指定了一个节点,则该节点将被设置(即指定)给所有本身没有定义任何目标节点属性的子转换。

ParallelTransition 类包含四个重载的构造函数方法。第一个创建一个空对象,第二个指定子动画对象的列表,第三个指定要受影响的节点,第四个指定要受影响的节点对象和子动画对象的列表。第二个和第四个构造函数方法是最常用的。我们将为我们的子动画对象使用第二个构造函数;两个引用的动画过渡对象都将微调器节点对象指定为 ParallelAnimation 对象的目标。这些构造函数方法的 Java 代码如下所示:

parallelTransition = new ParallelTransition();
parallelTransition = new ParallelTransition(Animation... children);
parallelTransition = new ParallelTransition(Node node);
parallelTransition = new ParallelTransition(Node node, Animation... children);

ParallelTransition 类只有大约六个方法调用,您需要掌握它们。那个。getChildren()方法调用将返回动画对象的 ObservableList ,这些对象将作为单个统一的动画一起播放。

那个。getNode()方法调用可以用来获取(poll)Node 属性的 Node 对象值,以及 void。setNode(Node value)方法调用可用于设置(指定)Node 属性的 Node 对象值。

还有一个受保护的节点。getParentTargetNode()方法调用,该调用将返回没有指定节点属性的过渡的子动画对象的目标节点。要指定父目标节点属性,必须使用第四个构造函数方法,该方法为 ParallelTransition(父节点)指定节点属性。否则,将使用第二个构造函数方法,子动画对象的 node 属性将定义动画对象将影响哪个节点对象。

并行转换。nodeProperty()方法调用将返回您的 parallel transition(parent)object property值,该值将使用第三个或第四个构造函数方法或。setNode(节点)。如果指定(设置)了此节点,它将用于所有未明确定义其目标节点的子转换。

最后,受保护的空间。interpolate(double value)方法调用需要由抽象 Transition 超类的所有子类实现提供。

接下来,让我们设置一个 ParallelTransition 对象,它将 rotSpinner 和 moveSpinnerOn 动画对象无缝地结合在一起。

ParallelTransition 对象:合并 rotSpinner 和 moveSpinnerOn

让我们通过在类的顶部声明一个名为 spinnerAnim 的 ParallelTransition 动画对象来将它添加到游戏项目中,然后在 RotateTransition 和 TranslateTransition Java 代码之后,在 createAnimationAssets()方法中实例化它。在构造函数方法中,引用 moveSpinnerOn 和 rotSpinner 动画子对象,然后调用。spinnerAnim 对象的 play()方法。请注意,我已经注释掉了 rotSpinner.play()方法调用,并且没有向 moveSpinnerOn 动画对象添加. play()方法调用,因为这是在 spinnerAnim ParallelTransition 对象中完成的。该并行(混合)动画的设置将使用以下 Java 语句完成,这些语句在图 16-10 中也用黄色和蓝色突出显示:

A336284_1_En_16_Fig10_HTML.jpg

图 16-10。

Declare a spinnerAnim ParallelTransition at the top of the class and instantiate it in createAnimationAssets()

ParallelTransition spinnerAnin;
...

spinnerAnin = new ParallelTransition(moveSpinnerOn, rotSpinner);
spinnerAnim.play();

当您选择运行➤项目时,一个旋转器从游戏的左侧旋转到右侧,如图 16-11 所示。

A336284_1_En_16_Fig11_HTML.jpg

图 16-11。

Select Run ➤ Project, click Start Game, and watch the spinner animate

在下一章之后,当我们讨论 3D 场景事件处理以及 PickResult 类时,我们可以开始完成 spinner UI 元素的动画,以便它在需要时出现在屏幕上,当用户不再需要旋转游戏板时离开屏幕。

我想专门用一章来介绍动画对象,向您展示预编码的 Transition 子类如何为您提供 Java 9 代码来将动画添加到您的游戏中,并向您展示如何设置大多数动画对象及其代码。我还想向您展示如何放置 createAnimationAssets()方法,以便您可以添加动画对象,从现在开始,这些对象将在您的 pro Java 9 游戏开发中拥有自己的位置。

摘要

在第十六章中,我们学习了动画超类和过渡超类,以及一些重要的过渡子类,RotateTransition 和 TranslationTransition,它们允许我们在游戏中移动和旋转 3D 对象。我们还看了 ParallelTransition 子类,它允许我们组合这些动画对象来创建更复杂的动画对象。我们还为我们的游戏构造了动画对象,这将允许用户对游戏板应用随机旋转来选择主题象限,并在随机旋转游戏板时将旋转的 spinner UI 元素移进移出屏幕。

我们为 JavaFXGame 类创建了一个名为 createAnimationAssets()的新自定义方法,该方法将保存为 pro Java 9 游戏设计创建的所有动画对象,这些动画对象使用 Transition 子类,如 RotateTransition、TranslateTransition、ScaleTransition 和 ParallelTransition。

在第十七章中,我们将学习 3D 场景元素的鼠标事件处理,这样我们可以点击球体 3D 旋转器 UI 来旋转游戏板,这样我们最终可以点击每个游戏板方块来选择教育问题类别并提出问题供玩家回答。

十七、i3D 游戏方块选择:对 3D 模型使用PickResult

现在,您已经创建了多层游戏板组节点(子类)层次,对该层次下的所有 3D 组件进行了纹理处理,确保该层次中心旋转,创建了一个旋转器 UI 来将游戏板 3D 模型(层次)随机旋转到一个随机象限,并使用您的createAnimationAssets()方法将动画对象添加到您的游戏设计中,是时候让您的 3D 游戏元素具有交互性了。我们将设置您的 3D 对象鼠标单击事件处理代码,该代码将用于触发 3D 旋转器动画和选择游戏棋盘方块。

在本章中,我们将详细了解公共 PickResult 类和公共 MouseEvent 类,并在自定义 createSceneProcessing()方法中使用它们来设计我们自己的游戏,该方法将用于处理 i3D 游戏元素(盒子和球体对象)事件处理,以便我们的玩家可以与 3D 游戏组件进行交互。

选择您的 3D 资源:PickResult 类

公共类 PickResult 扩展 Object 并保存在 javafx.scene.input 包中,该包包含输入事件处理实用程序,如 clipboard、GestureEvent、SwipeEvent、TouchEvent 和 ZoomEvent。PickResult 对象包含一个 pick 事件的结果,在这个游戏中,来自鼠标或触摸。支持在其构造函数方法中使用 PickResult 对象的输入类包括 MouseEvent、MouseDragEvent、DragEvent、GestureEvent、ContextMenuEvent 和 TouchPoint。每个类中都有一个. getPickResult()方法调用,它返回 PickResult 对象,该对象包含 Java 游戏开发过程中需要处理的所有选择信息。

PickResult 类的 Java 类层次结构向我们展示了该类是临时编码的,以提供 3D 对象选择功能;它没有自己的超类,因此看起来像下面的 Java 9 类层次结构:

java.lang.Object
  > javafx.scene.input.PickResult

PickResult 类包含一个数据字段,静态 int FACE_UNDEFINED 数据字段,它表示一个未定义的面。我们通常会使用这个类来选择整个节点(旋转器、象限 q1 到 q4、正方形 Q1S1 到 Q4S5 以及类似的 3D 游戏元素),而不是单个的多边形面或纹理贴图像素,这也是可能的。

PickResult 类中的前两个构造函数方法创建处理 2D 和 2.5D 场景拾取场景结果的 PickResult 对象。第一个构造函数使用 EventTarget 对象和(double) sceneX 和 sceneY 属性为 2D 场景创建 PickResult 对象。此构造函数方法使用以下 Java 语句语法:

PickResult(EventTarget target, double sceneX, double sceneY);

第二个构造函数为“非 Shape3D”目标创建一个 PickResult 对象。由于它使用 Point3D 对象和距离,我称之为 2.5D PickResult 场景,因为它不支持基于 Shape3D 类的 3D 图元。但是,它支持 Point3D 对象和场景对象的距离概念。此构造函数方法使用以下 Java 语句语法:

PickResult(Node node, Point3D point, double distance)

第三个构造函数为 Shape3D 目标创建一个 PickResult 对象,这是我们用来创建 i3D 游戏的对象。创建此构造函数方法的 Java 语法应该类似于下面的 Java 语句:

PickResult(Node node, Point3D point, double dist, int face, Point2D texCoord);

第四个构造函数为包含法线的导入的 3D 对象目标创建 PickResult 对象。如果您从外部 3D 建模软件包(如 Blender)导入高级 3D 模型,将会用到这一点。创建这个高级构造函数方法的 Java 语法应该类似于下面的 Java 语句:

PickResult(Node node, Point3D point, double distance, int face, Point3D normal, Point2D texCoor)

PickResult 类支持六个。get()方法调用,返回相交距离、相交面、相交节点、相交法线、相交点或相交纹理坐标(texCoord)。getIntersectedDistance()方法调用将以双精度值的形式返回当前摄像机位置和交叉点之间的相交距离。

那个。getIntersectedFace()方法调用将返回一个整数,表示所选节点的相交面。如果节点没有用户指定的面,如 Shape3D 图元之一,或者在边界上拾取,此方法将返回 FACE_UNDEFINED 常量。getIntersectedNode()方法调用将返回一个相交的节点作为节点对象,并且是我们将用来选择微调 UI 和游戏板节点元素的方法调用。

那个。getIntersectedNormal()方法调用将返回拾取的 Shape3D 对象或导入的 3D 几何图形的相交法线。那个。getIntersectedPoint()方法调用将使用拾取的节点对象的本地坐标系返回相交点(Point3D 对象)。那个。getIntersectedTexCoord()方法调用将以 Point2D 对象格式返回拾取的 3D 形状的相交纹理坐标。

接下来,让我们看看另一个重要的事件处理类 MouseEvent。这是 InputEvent 的子类,用于将鼠标事件处理附加到我们用来创建 i3D 棋盘游戏模拟的 3D 图元上。

MouseEvent 类:捕捉 3D 图元上的鼠标单击

公共 MouseEvent 类扩展了 InputEvent 超类。MouseEvent 与其子类 MouseDragEvent 和其他 InputEvent 超类子类一起保存在 javafx.scene.input 包中。MouseEvent 实现了可序列化和可克隆的 Java 接口。这个类用于实现或“捕获”鼠标事件,以供 Java 游戏逻辑处理,你将在本章中学习如何操作。当鼠标事件(如单击)发生时,光标下的第一个(顶部或前部)节点对象被“选取”,mouse event 被传递到该节点对象事件处理结构。通过使用 javafx.event 包中存储的公共 EventDispatcher Java 接口描述的捕获和冒泡阶段来传递事件。因此,MouseEvent 类的 Java 层次结构应该如下所示:

java.lang.Object
  > java.util.EventObject
    > javafx.event.Event

      > javafx.scene.input.InputEvent

        > javafx.scene.input.MouseEvent

鼠标指针(光标)位置在几种不同的坐标系中可用。可以使用相对于 MouseEvent 的 Node 对象原点(以及相对于场景对象)的 X,Y 坐标,使用相对于包含节点的场景原点的 sceneX,sceneY 坐标,甚至使用相对于包含鼠标指针的显示屏原点的 screenX,screenY 坐标。在这个特殊的 i3D BoardGame 项目中,我们将比较被点击的节点和我们的游戏处理逻辑。

有许多特定于 MouseEvent 对象的事件字段。它们是静态的,使用大写字母,因为它们是由 InputEvent 的 MouseEvent 类型提供的不同类型事件的“硬编码”常量。任何静态事件类型被用作一个公共的“超类型”来表示任何鼠标事件类型。

DRAG_DETECTED 静态事件类型将被传递给被识别为拖动手势的源的任何节点对象。当鼠标按钮被点击(在同一个节点上按下并释放)时,将传递一个 MOUSE_CLICKED 静态事件类型。这是我们将用于我们的 i3D 棋盘游戏。您还可以捕获鼠标被按下和鼠标被释放的事件。当鼠标按钮被按下时,会产生一个 MOUSE_PRESSED 静态事件类型,当鼠标按钮被释放时,会产生一个 MOUSE_RELEASED 静态事件类型

您还可以在鼠标移动到一个节点上,然后又离开该节点而没有任何鼠标点击发生时处理事件!当鼠标进入一个节点对象但没有被点击(这称为悬停)时,将会传递一个 MOUSE _ ENTERED 静态事件类型。当鼠标第一次进入节点(越过其边缘边界)时,将传递一个 MOUSE _ ENTERED _ TARGET 静态事件类型< MouseEvent >。类似地,当鼠标退出一个节点对象时,一个鼠标退出的静态事件类型<鼠标事件>将被传递。当鼠标第一次退出节点对象(越过边界)时,将会传递 MOUSE_EXITED_TARGET 静态事件类型< MouseEvent >。

最后,当鼠标在没有按钮被按下或释放的情况下在节点对象中移动时,将会传递 MOUSE_MOVED 静态事件类型。当使用按下(按住)的鼠标按钮移动鼠标时(称为拖动操作),将传递 MOUSE _ DRAGGED 静态事件类型

我们不会专门构造(实例化)MouseEvent 对象。我们会用。setOnMouseClick()事件处理构造,作为其功能的一部分,它将为我们进行构造。然而,为了完整起见,我将在这里包含这两个重载的构造函数方法。第一个构造了一个新的 MouseEvent 事件对象,具有空的源和目标,看起来像下面的 Java 9 构造函数方法语法:

MouseEvent(EventType<? extends MouseEvent> eventType, double x, double y, double screenX, double
           screenY, MouseButton button, int clickCount, boolean shiftDown, boolean controlDown,
           boolean altDown, boolean metaDown, boolean primaryButtonDown, boolean
           middleButtonDown, boolean secondaryButtonDown, boolean synthesized, boolean
           popupTrigger, boolean stillSincePress, PickResult pickResult)

第二个构造了一个新的 MouseEvent 事件对象,类似于下面的 Java 语法:

MouseEvent(Object source, EventTarget target, EventType<? extends MouseEvent> eventType, double
           x, double y, double screenX, double screenY, MouseButton button, int clickCount,
           boolean shiftDown, boolean controlDown, boolean altDown, boolean metaDown, boolean
           primaryButtonDown, boolean middleButtonDown, boolean secondaryButtonDown, boolean
           synthesized, boolean popupTrigger, boolean stillSincePress, PickResult pickResult)

MouseEvent 类中有 27 个方法可以帮助您控制鼠标事件的处理。的。copyFor(Object newSource,Event target new target)mouse Event 方法调用将复制事件对象,以便它可以用于不同的源和目标。

那个。copyFor(Object newSource,EventTarget newTarget,Event type extends MouseEvent>Event type)MouseEvent 方法调用还将创建给定事件对象的副本,并用给定的 mouse Event 字段进行替换。

静态 MouseDragEvent。copyForMouseDragEvent(MouseEvent e,Object source,EventTarget target,EventType type,Object gestureSource,PickResult pickResult)方法调用将创建 MouseDragEvent 类型的 mouse event 的副本。

那个。getButton()方法调用将轮询 MouseEvent 对象,以查看哪个鼠标按钮(如果有)负责生成该事件对象。那个。getClickCount()方法调用将返回与事件对象关联的整数(int)鼠标单击次数。

那个。getEventType()方法调用将返回该事件对象的 EventType extends MouseEvent>事件类型。那个。getPickResult()方法调用将返回 PickResult 对象关于该选择的信息。

那个。getSceneX()方法调用将返回事件相对于包含 MouseEvent 源的场景原点的水平位置的 double 值。那个。getSceneY()方法调用将返回事件相对于包含 MouseEvent 源的场景原点的垂直位置的 double 值。

那个。getScreenX()方法调用将返回事件绝对水平位置的 double 值。那个。getScreenY()方法调用将返回事件绝对垂直位置的 double 值。

那个。getX()方法调用将返回事件相对于 MouseEvent 源原点的水平位置的 double 值。那个。getY()方法调用将返回事件相对于 MouseEvent 源原点的垂直位置的 double 值。那个。getZ()方法调用将返回事件深度位置的 double 值,该值相对于 MouseEvent 源的原点。

那个。isAltDown()方法调用可用于确定在此事件中 Alt 修饰键是否被按住。它返回一个真或假(布尔值)。那个。isControlDown()方法调用可用于确定在此事件期间 Ctrl 修饰键是否被按住。它还返回真或假(布尔值)。那个。isMetaDown()方法调用可用于确定元修饰键在此事件中是否被按住。它还返回真或假(布尔值)。那个。isShiftDown()方法调用可用于确定在此事件中 SHIFT 修饰键是否被按住。它还返回真或假(布尔值)。

那个。应使用 isDragDetect()方法调用来确定此 MouseEvent 之后是否会有 DRAG_DETECTED 事件,并返回布尔值 true(检测到拖动)或 false(未检测到拖动)。

安。isMiddleButtonDown()方法调用可用于确定鼠标中键是否被按住。如果您的中间按钮(鼠标键#2)当前被按下,它将返回一个真布尔值。

那个。应该使用 isPopupTrigger()方法调用来确定该事件是否是平台的弹出菜单触发事件。如果鼠标事件实际上是平台的弹出菜单触发事件,它将返回 true。

那个。如果您的主鼠标按钮(按钮#1,通常是鼠标左键)当前被按下,isPrimaryButtonDown()方法调用将返回真布尔值。那个。如果您的辅助按钮(按钮#2,通常是鼠标右键)当前被按下,isSecondaryButtonDown()方法调用将返回真布尔值。

那个。isShortcutDown()方法调用将返回在此 MouseEvent 期间主机平台的公共快捷方式修饰符是否被按住。

那个。isStillSincePress()方法调用使用一个布尔值来指示自从在此事件之前发生的最后一次鼠标按下事件以来,鼠标光标是否停留在系统提供的滞后区域中。

那个。isSynthesized()方法调用返回一个布尔值,该值指示 MouseEvent 是否是使用触摸屏设备而不是通常的鼠标事件源设备(如鼠标、跟踪球、跟踪板或类似的鼠标模拟硬件设备)合成的。

最后,一片虚空。setDragDetect(boolean dragDetect)方法调用用于在使用鼠标、跟踪板或触摸屏设备将 MouseEvent 处理与拖动检测结合使用时增强拖动检测行为。

实现微调用户界面功能:鼠标事件处理

让我们创建一个 createSceneProcessing()方法来保存场景对象的创建、配置和事件处理 Java 代码。必须在创建根组节点对象之后创建场景场景对象,因此必须在创建这些节点对象的 createBoardGameNodes()方法调用之后调用该方法。这是使用下面的 Java 语句完成的,在图 17-1 中也用浅蓝色(和红色波浪下划线)突出显示:

A336284_1_En_17_Fig1_HTML.jpg

图 17-1。

Add the createSceneProcessing() method call, after the createBoardGameNodes() method in the start method

createSceneProcessing();

记得双击 javafxgame 中的创建方法“createSceneProcessing()”。JavaFXGame 选项,让 NetBeans 9 为您编写一个引导方法。您将使用场景对象实例化和配置代码替换占位符 Java 代码,然后添加 MouseEvent 处理逻辑。

你需要做的第一件事是打开你的 createBoardGameNodes()方法,选择所有的场景场景对象实例化和配置 Java 9 代码,目前有三个 Java 9 语句;然后右键单击选择集并选择 Cut 选项,从该方法体中删除 Java 代码。

在你的内心。createSceneProcessing()方法,通过选择那一行代码并右键单击它来替换您的引导代码(未实现的错误代码);选择“粘贴”来替换您从 createBoardGameNodes()方法中“剪切”的三行代码。最后,在方法的末尾添加一行代码,开始构建您的场景对象的事件处理;键入 scene,然后键入句点,再键入 setOnMouse,这将弹出一个包含所有 MouseEvent 事件的帮助器对话框。以下是现有语句的 Java 代码和一个空的事件处理 lambda 表达式基础结构(用于更改),如图 17-2 中用蓝色突出显示的:

A336284_1_En_17_Fig2_HTML.jpg

图 17-2。

Cut and paste the Scene object code into the new method and call .setOnMouseClicked() off the scene object

private void createSceneProcessing() {
    scene = new Scene(root, 1280, 640, true, SceneAntialiasing.BALANCED);
    scene.setFill(Color.BLACK);
    scene.setCamera(camera);
    scene.setOnMouseClicked(event-> { ... } );  // This is an Empty OnMouseClicked Event Handler
}                                              //  Structure is using a Lambda Expression Format

双击您的 setOnMouseClicked(EventHandler super MouseEvent>value)(void)选项,在图 17-2 中用亮蓝色显示,并在。setOnMouseClicked()方法调用参数区域来创建空的事件处理基础结构,这将在 NetBeans 9 中产生零错误。正如我以前在本书中说过的,当你写代码时,要确保它在你的 IDE 中始终没有错误!

现在您可以开始配置 onMouseClicked()事件处理,正如您所看到的,它使用了 Java 8 中引入的简化的 lambda 表达式。lambda(简称 lambda)需要的只是事件名称和一个箭头,Java 编译器会计算出使用哪种类型的事件处理对象(EventHandler)以及需要处理哪种类型的事件对象(MouseEvent)。您的逻辑放在花括号内,您可以专注于事件处理逻辑要做的事情,即声明一个名为 picked 的节点对象并用. getPickResult()的结果加载它。getIntersectedNode()方法链。确保在 Java 语句的Node picked(初始)部分下出现红色波浪错误下划线时使用 Alt+Enter,并从弹出的帮助器对话框中选择“import javafx.scene.Node”选项,以指示 NetBeans 9 为您编写节点类导入语句。如果你愿意,你可以输入等号(=)和事件,然后点击句号;NetBeans 弹出帮助程序将允许您选择。getPickResult()方法。双击它将其插入,然后再次使用句点来显示弹出帮助程序。这次选择。getIntersectedNode()方法调用。添加分号以结束 Java 语句。用于鼠标事件处理的 Java 语句应该如下所示,并显示在图 17-3 的底部:

A336284_1_En_17_Fig3_HTML.jpg

图 17-3。

Configure event handling as a lambda expression, create a Node named picked, and get an intersected Node

private void createSceneProcessing() {
    scene = new Scene(root, 1280, 640, true, SceneAntialiasing.BALANCED);
    scene.setFill(Color.BLACK);
    scene.setCamera(camera);
    scene.setOnMouseClicked(event->{
        Node picked = event.getPickResult().getIntersectedNode();

    });
}

现在,您已经创建并加载了一个名为 picked 的节点对象,它与用户用鼠标(或触摸屏,也生成鼠标事件)点击的 BoardGame 中的节点对象一起使用,我们需要添加条件处理逻辑(人工智能)来告诉游戏如何操作。您需要做的第一件事是过滤掉所有不在 3D 节点对象上的单击,这是通过使用if ( picked != null )构造来完成的,它表示如果拾取的节点对象不为空,则继续。下一个嵌套的 if()语句查找与拾取的节点对象相同(==或等效)的微调器节点对象。如果这等于一个真值,rotGameBoard 动画对象将通过使用。play()方法调用,旋转游戏板组节点。如果您使用“运行➤项目”工作流程并测试这段代码,它可以完美地工作,尽管您必须等到最后一章的代码完成(我们将在接下来修复它,因为我们将动画对象更改为鼠标事件触发)。

整个完整的 Java 9 结构只有八行代码;这将随着我们构建游戏逻辑而增长。此处显示了完整的 Java 方法体代码,并在图 17-4 中以黄色和蓝色突出显示:

A336284_1_En_17_Fig4_HTML.jpg

图 17-4。

Evaluate the picked Node object using two nested if{} constructs, testing for null and then for the spinner UI Node

private void createSceneProcessing() {
    scene = new Scene(root, 1280, 640, true, SceneAntialiasing.BALANCED);
    scene.setFill(Color.BLACK);
    scene.setCamera(camera);
    scene.setOnMouseClicked(event->{
        Node picked = event.getPickResult().getIntersectedNode();
        if (picked != null) {
            if (picked == spinner) {
                rotGameBoard.play();

            }
        }
    });
}

要使微调器 UI 在屏幕上呈现动画效果,我们首先必须将它在屏幕外的初始位置设置在其当前起始位置的左侧。进入 createBoardGameNodes()并将 TranslateX 属性从-200 更改为-350。这将从视图中移除微调器,就在屏幕的左侧。稍后我们将更改。将 moveSpinnerIn 中的 setByX()方法设置为 150,因此它位于-200。这是使用此处和图 17-5 中所示的 Java 代码完成的:

A336284_1_En_17_Fig5_HTML.jpg

图 17-5。

Prepare for implementing the interactive spinner UI by setting its initial position off-screen value to the -350 X location

spinner.setTranslateX(-350);

注意 TranslateY 是-512;这将 3D 旋转器 UI 放置在屏幕的顶部,不在游戏棋盘视图的范围内,并且在旋转器动画显示到-150 X 位置时位于屏幕的左上角。

接下来,让我们重新编码我们的 createAnimationAssets()方法体,以便它只实例化和配置我们的动画对象,而不触发它们,这将在游戏过程中由用户通过鼠标点击(或屏幕触摸,因为这些也将生成鼠标事件,扩大我们的目标消费电子设备)来完成。

移除。play()方法调用 rotGameBoard、rotSpinner 和 spinnerAnim 动画对象构造,然后更改 movespinner on translate transition 对象的。setByX()方法调用引用 150 个单位。这将把你的 3D 旋转器 UI 从屏幕外的新-350 位置移动到屏幕的左上角。触发这个动画的逻辑位置,第一次将旋转器带到屏幕上,应该是在开始游戏按钮 UI 事件处理方法中,我们很快就会这样做。我们还将在本章稍后创建 rotSpinner 动画对象,它将在被单击时旋转 3D spinner UI,以便当玩家启动 3D 游戏板的每个随机旋转时它也旋转。

除了在“开始游戏”按钮事件处理中将这个 i3D 旋转器显示在屏幕上之外,我们还将在每个玩家的回合结束时将它显示在屏幕上(在第二十一章),以便下一个玩家知道随机旋转游戏板来选择新的教育问题类别(象限)。我们将在第二十章中制作它的离线动画,当游戏板完成它的相机旋转动画对象时。关于如何在 JavaFX 中将您的交互性(事件处理)与您的不同动画对象相集成,以便您可以获得无缝且响应迅速的游戏效果,这一章中还有很多内容需要学习。

您的新 createAnimationAssets() Java 方法体现在应该如下所示,在图 17-6 中也用浅蓝色和黄色突出显示:

A336284_1_En_17_Fig6_HTML.jpg

图 17-6。

Remove all .play() method calls and change the .setByX() method call to 150 to bring the spinner on-screen

RotateTransition rotGameBoard, rotSpinner;
TranslateTransition moveSpinnerOn;
ParallelTransition spinnerAnim;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotGameBoard.setFromAngle(45);
    rotGameBoard.setToAngle(1125);
                                                                      // .play() removed

    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5);
    rotSpinner.setInterpolator(Interpolator.LINEAR);
    rotSpinner.setFromAngle(30);
    rotSpinner.setToAngle(-1110);                                     // .play() removed

    moveSpinnerOn = new TranslateTransition(Duration.seconds(5), spinner);
    moveSpinnerOn.setByX(150);
    moveSpinnerOn.setCycleCount(1);
    spinnerAnim = new ParallelTransition(moveSpinnerOn, rotSpinner);
                                                                      // .play() removed

}

在 gameButton 事件处理程序的末尾添加一条spinnerAnim.play();语句,如图 17-7 所示。

A336284_1_En_17_Fig7_HTML.jpg

图 17-7。

Add the spinnerAnim.play() method call to the end of your gameButton event-handling method construct

现在使用您的运行➤项目工作流程来测试您的代码,您可以看到微调器在游戏开始时没有显示(在您单击隐藏 uiLayout StackPane 节点对象的按钮之后),并且缓慢而平稳地旋转到游戏屏幕左上角的视图中。

下一件事,我们需要做的是创建一个单独的 rotSpinner 动画对象,以便我们可以在游戏板旋转的同时进行 3D spinner UI 旋转,以保持连续性。您会发现,如果在 MouseEvent 处理构造中调用 rotSpinner.play,将会得到一个错误,因为 rotSpinner 是 spinnerAnim ParallelAnimation 对象的一部分;因此,我们需要复制一个 rotSpinner 构造,并创建一个 rotSpinnerIn 构造以在 spinnerAnim ParallelAnimation 中使用,让 rotSpinner 动画在玩家随机旋转游戏板时免费供我们调用。

为此,选择所有与 rotSpinner 相关的 Java 代码,右键单击选择集,然后选择 Copy 然后在这个代码块之后添加一行(空格)代码,右键单击,并选择 Paste 复制这个代码块。然后,您所要做的就是在“rotSpinner”的末尾添加“In ”,并创建一个 rotSpinnerIn 代码块,它做同样的事情,但不是 ParallelTransition 构造的组件。在 spinnerAnim ParallelTransition 对象的对象实例化(构造函数方法)中引用新的 rotSpinnerIn 动画对象。

正如你所看到的,唯一的问题是你的“旋转”旋钮旋转到了错误的 1110 度角,正如我们在第十六章中编码的那样。在下一节中,我将把它设置为-1050。代码如下所示,如图 17-8 所示:

A336284_1_En_17_Fig8_HTML.jpg

图 17-8。

Copy and paste the rotSpinner object code under itself to create a rotSpinnerIn, and reference in spinnerAnim

RotateTransition rotGameBoard, rotSpinner;
TranslateTransition moveSpinnerOn;
ParallelTransition spinnerAnim;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotGameBoard.setFromAngle(45);
    rotGameBoard.setToAngle(1125);
    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5); 

    rotSpinner.setInterpolator(Interpolator.LINEAR);
    rotSpinner.setFromAngle(30);
    rotSpinner.setToAngle(-1110);
    rotSpinnerIn = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinnerIn.setAxis(Rotate.Y_AXIS);
    rotSpinnerIn.setCycleCount(1);
    rotSpinnerIn.setRate(0.5);
    rotSpinnerIn.setInterpolator(Interpolator.LINEAR);
    rotSpinnerIn.setFromAngle(30);
    rotSpinnerIn.setToAngle(-1110);
    moveSpinnerOn = new TranslateTransition(Duration.seconds(5), spinner);
    moveSpinnerOn.setByX(150);
    moveSpinnerOn.setCycleCount(1);
    spinnerAnim = new ParallelTransition(moveSpinnerOn, rotSpinnerIn);
}

现在,我可以在条件事件处理结构中添加一个rotSpinner.play() ; Java 语句,而不会产生任何错误,这样,当点击旋转器 UI 时,它会沿着游戏板旋转相同的时间和速率。完整的 Java 代码如下所示,在图 17-9 中用黄色突出显示:

A336284_1_En_17_Fig9_HTML.jpg

图 17-9。

Add rotSpinner.play() after rotGameBoard.play() in the mouse event handling construct so both will animate

private void createSceneProcessing() {
    scene = new Scene(root, 1280, 640, true, SceneAntialiasing.BALANCED);
    scene.setFill(Color.BLACK);
    scene.setCamera(camera);
    scene.setOnMouseClicked(event->{
        Node picked = event.getPickResult().getIntersectedNode();
        if (picked != null) {
            if (picked == spinner) {
                rotGameBoard.play();
                rotSpinner.play();

            }
        }
    });
}

让我们使用一个运行➤项目的工作流程并测试我们的代码。单击开始游戏按钮对象,注意屏幕上只包含游戏板。然后,spinner UI 出现,旋转到位(旋转到错误的“大头针”位置,我们将很快修复)。点击旋转器,旋转器和游戏板旋转,如图 17-10 所示。

A336284_1_En_17_Fig10_HTML.jpg

图 17-10。

The spinner UI element now animates on-screen and also rotates when clicked to spin the game board

使用 java.util.Random:生成随机旋转

公共类 Random 扩展 Object 并实现 Serializable。它保存在 java.util 包中,有两个已知的直接子类 SecureRandom 和 ThreadLocalRandom。这个类的一个实例可以用来创建一个随机数生成对象,它将生成一个“伪随机数”流为了创建随机的游戏旋转器 UI 功能,这些数字将足够随机。该类的算法使用 48 位种子,该种子使用线性同余公式进行修改。如果你想更详细地研究这个算法,你可以参考 Donald Knuth 的《计算机编程的艺术》(第 2 卷,第 3.2.1 节)。因此,Random 类的 Java 类层次结构如下所示:

java.lang.Object
  > java.util.Random

值得注意的是,如果使用相同的种子创建随机对象的两个不同实例,并且对每个对象进行相同的方法调用序列,则算法会生成(返回)相同的数值结果序列。在某些应用中,这实际上是可取的;因此,为了保证相同的结果,java.util.Random 类实现了特定的算法。类 Random 的子类被允许使用替代算法来增加安全性或多线程的使用,只要它们遵守所有方法的通用契约。

java.util.Random 的实例是线程安全的。但是,在多个线程中同时使用同一个 java.util.Random 实例可能会遇到争用,从而导致性能下降。您应该考虑为您的多线程游戏设计使用 ThreadLocalRandom 子类。

此外,java.util.Random 的实例在加密方面并不安全。相反,您应该考虑使用 SecureRandom 子类来获得一个加密安全的伪随机数生成器,供敏感且需要高级安全性的应用使用。

这个类有两个重载的构造函数方法。第一个创建一个随机数生成器,第二个创建一个随机数生成器,并使用长格式给它一个种子值。这些构造函数方法看起来像下面的 Java 代码:

Random()             // We'll be using this in our code later on during this chapter
Random(long seed)

这个类有 22 个方法,可以用来从 random 对象获得随机数结果。的。doubles()方法调用将返回一个称为 DoubleStream 的无限数值流,其中包含伪随机双精度值。这些值中的每一个都将介于零(含)和一(不含)之间。还有三个额外的超载。doubles()方法调用。的。doubles(double randomNumberOrigin,double randomNumberBound)方法调用将返回伪随机双精度值的无限绑定流,每个值都符合方法调用参数区域中指定的给定绑定原点(包括)和绑定限制(不包括)。的。doubles(long streamSize)方法调用将返回一个流,该流产生给定 streamSize 数量的伪随机双精度值,这些值介于零(含)和一(不含)之间。最后,还有一个. doubles(long streamSize,double randomNumberOrigin,double randomNumberBound)方法调用,它返回一个流,该流产生符合给定 streamSize 数量的伪随机双精度值的流,每个值都符合给定的绑定原点(包含)和绑定限制(不包含)。

那个。ints()方法调用将返回一个无限的伪随机 int(整数)数值流,称为 IntStream。还有三个额外的超载。ints()方法调用,包括。ints(int randomNumberOrigin,int randomNumberBound)方法调用,它将返回一个无限的伪随机 int (integer)值流,每个值都符合参数区域中指定的绑定原点(包含)和绑定限制(不包含)值。那个。ints(long streamSize)方法调用将返回一个随机值流,这将产生一个流大小,该流大小是使用 streamSize 参数指定的,该参数建立了所需数量的伪随机 int (integer)值。

最后是。ints(long streamSize,int randomNumberOrigin,int randomNumberBound)方法调用将返回一个数值(整数)流,该流产生参数区域中指定的 streamSize 数量的伪随机 int 值,每个值都符合指定的绑定原点(包括)和绑定限制(不包括),这也是从方法调用参数区域获取的。

那个。longs()方法调用将返回一个无限的伪随机长数值流,称为 LongStream。还有三个额外的超载。longs()方法调用,包括一个. longs(long randomNumberOrigin,int randomNumberBound)方法调用,它将返回一个无限的伪随机长值流,每个长值都符合参数区域中指定的绑定原点(包含)和绑定限制(不包含)值。那个。longs(long streamSize)方法调用将返回一个随机长值流,该流产生使用 streamSize 参数指定的流大小,该参数建立所需数量的伪随机长值。

最后,一个. long(long streamSize,int randomNumberOrigin,long randomNumberBound)方法调用将返回一个数字长值流,该流产生参数区域中指定的 streamSize 数量的伪随机长值,每个值都符合指定的绑定原点(包括)和绑定限制(不包括),这也是从方法调用参数区域获取的。

受保护的 int。next(int bits)方法调用将使用整数位数作为参数规范来生成下一个伪随机整数。的。nextBoolean()方法调用将从随机数生成器对象的序列中返回一个伪随机的、均匀分布的布尔值。这个方法可能不应该用于这个游戏的用例,因为 next()被设计为由其他 random()方法调用。

虚空。nextBytes(byte[] bytes)方法调用将生成一个参数提供的字节数组,并用随机字节值填充它。那个。nextDouble()方法调用将通过使用随机数生成器对象的序列返回一个介于值 0.0 和 1.0 之间的伪随机、均匀分布的 Double 值。那个。nextFloat()方法调用将使用随机数生成器对象的序列返回一个伪随机的、均匀分布的浮点值,介于 0.0 和 1.0 之间。

那个。nextGaussian()方法调用将从这个随机数生成器对象的序列中返回一个伪随机、高斯分布的双精度值,其平均值为 0.0,标准差为 1.0。那个。nextInt()方法调用将从这个随机数生成器对象的序列中返回下一个伪随机、均匀分布的 Int(整数)值。

那个。nextLong()方法调用将从这个随机数生成器对象的序列中返回下一个伪随机、均匀分布的长值。

虚空。setSeed(long seed)方法调用可用于设置(或重新设定)随机数生成器对象的种子,在方法调用的参数区域内使用单个长值种子规范。

最后是。nextInt(int bound)方法调用是我们将在本章的最后一节使用的方法,它将返回一个伪随机的、均匀分布的 int (integer)值,该值介于 0(包括 0)和指定值(不包括 0)之间,在我们的示例中为 4,从随机数生成器对象的 random int 序列中提取。

随机象限选择:使用带条件 If()的随机

既然我们已经很好地设置了旋转器和游戏板旋转以及鼠标事件处理,足以将两者连接在一起,为游戏板创建一个随机旋转,我们需要向代码中添加一个随机数发生器算法,以便每次单击旋转器时,游戏板都会被随机设置到一个新的象限。我们将使用至少三次旋转,以便旋转足够长,对玩家来说完全是随机的。让我们在类的顶部声明一个名为 Random 的随机对象,然后使用 Alt+Enter 组合键打开 NetBeans 9 弹出助手。最后,选择(双击)“为 java.util.Random 添加导入”选项,如图 17-11 中蓝色部分所示。

A336284_1_En_17_Fig11_HTML.jpg

图 17-11。

Declare a Random object named random at the top of class; use Alt+Enter to add import java.util.Random

由于我们希望在实际使用随机数生成器“引擎”之前实例化(创建)它,所以让我们的代码在游戏应用启动时实例化(创建并加载到系统内存中)随机数生成器。

这表明我们将把 Random()构造函数方法代码放在您的。start()方法,在 ActionEvent 处理构造之前,在所有节点、场景和舞台对象创建并添加到 SceneGraph 之后。为了更好地衡量,我们将把它放在创建素材、图像、动画以及最终数字音频样本和其他新媒体素材的所有自定义方法之后,我们将使用它们来创建专业的 Java 9 游戏。

我们可以这样做,因为这个随机对象(名为 Random)直到玩家单击开始游戏按钮对象进入 3D 场景,然后单击 spinner 3D UI (Sphere)元素时才被使用。因此,您可以将这个随机对象实例化放在。start()方法,从第一行代码到最后一行代码,只要在您开始在您的自定义 createSceneProcessing()方法中生成任何 MouseEvent 处理方法调用之前创建了该对象(加载到系统内存中),我们将在本章中继续增强该方法。打开你的。NetBeans 9 中的 start()方法体,在你的自定义方法调用之后添加一行代码,使用下面的 Java 代码实例化你的名为 Random 的随机对象,也如图 17-12 所示:

A336284_1_En_17_Fig12_HTML.jpg

图 17-12。

Instantiate the random Random object in the .start() method so that it is loaded into memory and ready

public void start(Stage primaryStage) {
    ...                                  //  Custom Methods Up Here
    random = new Random();
    ...                                  //  ActionEvent Handling Constructs Down Here
}

现在,您已经准备好在 MouseEvent 处理代码中调用微调器逻辑内部的这个随机数生成器,它告诉您的游戏在单击 3D 微调器 UI 时该做什么。显然,要做的第一件事是检查 NULL 以查看点击是否在 3D 场景元素上,如果是,则查看点击的是否是 3D 微调器。

如果单击了微调器,那么 if(picked==spinner)之后的第一行代码将是一个. nextInt(bound)方法调用,上边界值为 4(下边界为零)。这为我们提供了四个象限中的随机结果(从 0 到 3,因为 4 的上限是唯一的,因此不在随机数选择范围内使用),这是我们需要在游戏的四个象限中随机选择的结果。

在调用 RotateTransition 动画对象之前添加一行代码,并创建一个名为 spin 的新 int 变量,它将保存您的random.nextInt(4)方法调用的结果。添加一个等号(=)运算符,然后键入 random 和一个句点,这将打开 NetBeans 9 method helper 弹出选择器下拉列表。

选择。nextInt(int bound) (int)选项,在图 17-13 中用蓝色显示,然后双击它插入到你的代码中。将默认值 0(通过生成零到零的结果来关闭随机数生成器)更改为 4,告诉随机数生成器随机生成四个整数值,这将为您的玩家旋转提供四个不同的象限结果。此时的 Java 代码应该看起来像下面的 Java 嵌套 if()结构,在图 17-13 中也用蓝色突出显示(也在构建中):

A336284_1_En_17_Fig13_HTML.jpg

图 17-13。

Add an int variable named spin and then type random and a period and select nextInt(int bound ) set to 4

if (picked != null) {
    if (picked == spinner) {
        int spin = random.nextInt(4);

        rotGameBoard.play();
        rotSpinner.play();
    }
}

在我们编写这个自旋逻辑之前,我们需要移除。setFromAngle()和。setToAngle()方法调用 createAnimationAssets()方法中 rotGameBoard 语句块,这将 rotGameBoard 动画对象逻辑简化为五个必需的语句(实例化、轴、周期、速率和插值器)。在我们确定从 toAngle 和 fromAngle 到 byAngle 的切换将正确地使用最少的代码行和零错误生成正在进行的游戏板旋转后,我们将为您的 rotSpinner 执行此操作。

我们在这里做的是使用 createAnimationAssets()来创建和配置动画对象,然后在 if()条件语句中使用. setByAngle(),该语句评估随机的随机对象结果,并将其放入 spin integer 中,这是我们接下来要做的。这种方法也将减少这个方法体中的代码量,减少到不到 24 行代码(除非我们在本书概述的设计和开发过程中添加游戏板动画)。rotGameBoard 代码如图 17-14 所示,现在看起来像这样:

A336284_1_En_17_Fig14_HTML.jpg

图 17-14。

Remove the .setFromAngle(45) and .setToAngle(1125) method calls from the rotGameBoard object code

private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5);
    rotSpinner.setInterpolator(Interpolator.LINEAR);
    rotSpinner.setFromAngle(30);
    rotSpinner.setToAngle(-1050);
    ...
}

确保游戏板旋转在一个象限结束的最简单的方法是将游戏板旋转初始化为 45 度,并使用。setByAngle()为每次 if()计算旋转 90 度增量(加上三次旋转)。这样我们得到 0 的 1080,1 的 1170,2 的 1260 和 3 的 1350。Java if()结构如图 17-15 所示,如下所示:

A336284_1_En_17_Fig15_HTML.jpg

图 17-15。

Add if() constructs, setting the .setByAngle() method call to four different 90-degree increments plus 1080

if (picked == spinner) {
    int spin = random.nextInt(4);
    if (spin == 0) {
        rotGameBoard.setByAngle(1080);  // Zero degrees plus 1080

    }
    if (spin == 1) {
        rotGameBoard.setByAngle(1170);  // 1080 plus 90 degrees is 1170

    }
    if (spin == 2) {
        rotGameBoard.setByAngle(1260);  // 1080 plus 180 degrees is 1260

    }
    if (spin == 3) {
        rotGameBoard.setByAngle(1350);  // 1080 plus 270 degrees is 1350

    }
    rotGameBoard.play();
    rotSpinner.play();
}

使用运行➤项目工作流程,多次点击 spinner UI 进行测试,如图 17-16 所示。

A336284_1_En_17_Fig16_HTML.jpg

图 17-16。

The game board now randomly lands on a different quadrant with each 3D spinner click

返回 createAnimationAssets()方法并移除。setFromAngle()和。setToAngle()方法从 rotSpinner 动画对象中调用,产生如下 Java 代码,如图 17-17 所示:

A336284_1_En_17_Fig17_HTML.jpg

图 17-17。

Remove rotSpinner.setFromAngle() and rotSpinner.setToAngle() method calls in createAnimationAssets

private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5);
    rotSpinner.setInterpolator(Interpolator.LINEAR);
}

返回到 createSceneProcessing()方法,使用 rotGameBoard.setByAngle()方法调用中使用的负角度值添加 rotSpinner.setByAngle()方法调用,使用此代码,也如图 17-18 所示:

A336284_1_En_17_Fig18_HTML.jpg

图 17-18。

Add the rotSpinner.setByAngle() method calls to the random spin logic, this time subtracting 1080 plus 90

if (picked == spinner) {
    int spin = random.nextInt(4);
    if (spin == 0) {
        rotGameBoard.setByAngle(1080);
        rotSpinner.setByAngle(-1080);  // Zero degrees minus 1080
    }
    if (spin == 1) {
        rotGameBoard.setByAngle(1170);
        rotSpinner.setByAngle(-1170);  // -1080 minus 90 degrees is -1170
    }
    if (spin == 2) {
        rotGameBoard.setByAngle(1260);
        rotSpinner.setByAngle(-1260);  // -1080 minus 180 degrees is -1260
    }
    if (spin == 3) {
        rotGameBoard.setByAngle(1350);
        rotSpinner.setByAngle(-1350);  // -1080 minus 270 degrees is -1350
    }
    rotGameBoard.play();
    rotSpinner.play();
}

现在使用一个运行➤项目的工作流程,并彻底测试我们在本章中开发的代码。正如你在图 17-19 中所看到的(因为它不是动画或互动的,就像我们现在的游戏一样),每次你点击 3D 旋转器,你会在 3D 旋转器 UI 上得到不同的象限和不同的颜色序列,而它仍然总是说“旋转”!

A336284_1_En_17_Fig19_HTML.jpg

图 17-19。

The game board randomly lands on a different quadrant , and the spinner always lands on the word SPIN

在本章中,我们添加了一些相当复杂的功能,我们仍然有大约 500 行 Java 代码,正如您在 NetBeans 9 底部的图 17-18 中看到的那样(第 504 行是课程的结尾)。令人印象深刻,伙计们!

摘要

在第十七章中,我们学习了 MouseEvent、PickResult 和 Random 类,它们允许我们完成 3D spinner UI 的实现,并让它在旋转的 3D spinner 沙滩球的每个后续旋转中选择一个随机象限。我们还构建了一个新的自定义 createSceneProcessing()方法,该方法包含您的 MouseEvent 处理逻辑,以及您的用于处理(现在的)i3D 基本对象的逻辑,我们的 i3D 游戏板和旋转器是由这些基本对象组成的(构建时使用)。在这个新方法中,我们开始构建一个条件 if()结构来评估鼠标点击,以及基于点击的内容游戏逻辑需要发生什么。在接下来的几章中,当我们设计和开发我们的游戏模型时,我们显然会扩展这个逻辑。

我们还获得了更多使用 RotateTransition 类方法的经验,方法是使用。setFromAngle()和。setToAngle()旋转动画配置参数为单个。setByAngle()旋转动画配置方法,减少了 12 行 Java 代码。

在第十八章中,我们将开发你的游戏内容,这样我们就可以完成你的鼠标事件处理代码(在第 19 和 20 章中)用于游戏棋盘方格和游戏棋盘象限。

十八、3D 游戏设计:使用 GIMP 和 Java 创建你的游戏内容

现在,您已经创建了多层 3D 游戏板组节点(子类)层次,对该层次下的所有 3D 图元进行了纹理处理,配置了 RotationTransition 动画算法(对象)以使游戏板栩栩如生,并创建了 3D 旋转器 UI 以将游戏板 3D 模型(层次)旋转到随机象限,现在是时候完成游戏设计并创建组成游戏的视觉素材了。这些将在游戏过程中替换纹理贴图图像资源;我们将使用现有的 24 个棋盘游戏组件,并将它们变形为不同的内容配置,用与您的教育游戏相关的内容替换旋转的游戏棋盘。

在这一章中,我们将看看创建替换纹理贴图的工作过程,这些贴图将在游戏过程中根据随机旋转和玩家鼠标点击(或屏幕触摸)改变图像对象资源引用,以将内容添加到游戏棋盘方块和象限中。虽然这一章没有深入探讨 Java 9,但需要注意的是,开发专业的 Java 9 游戏涉及到数字图像工匠,以及数字音频工程师、3D 建模师、3D 纹理艺术家、动画制作人、2D 插画师和 VFX 艺术家。因此,我们需要在本书中涵盖一些非 Java 主题,这是其中的一章。内容设计工作流程中的一章将允许我们涵盖开发一个被大众认为是“专业”的游戏需要什么。在本书中,我将使用许多新媒体类型,这样我就不会留下任何漏网之鱼了!

设计你的游戏:创建象限定义

由于这是一个针对学龄前儿童以及自闭症、智力障碍和学习障碍者的教育游戏,我们需要保持分类简单。与我们的配色方案相匹配的常年分类范例之一是动物、植物或矿物,这将为我们留下一个正方形用于其他主题,例如人和著名的地方。显然,我们的绿色象限将是蔬菜,因为人们说“吃你的绿色”,橙色象限将是动物,因为狮子、老虎、猫、狗和其他动物正是橙色的阴影。我们的蓝色象限将是矿物的,因为像蓝宝石和紫水晶这样的矿物存在于这个冷色系光谱中。这就剩下了粉色象限,我们可以在每次旋转后决定对其进行分类。这些游戏棋盘方格随机选择的主题将使用高质量的图像进行视觉呈现,我们将在本章中使用专业级 GIMP 开发替代纹理映射数字图像素材。

游戏板象限:创建游戏象限内容(GIMP)

我将向您展示在 GIMP 中创建一个游戏板纹理素材(为您的动物象限创建一只鹦鹉)的工作过程,这样该章节就不会膨胀到数百页,因为您最终需要为您的 24 个游戏板元素创建数百个图像素材。让我们启动 GIMP(目前版本为 2.8.22)并创建一个新的图像合成来开发内容纹理!要开始你的象限(和游戏棋盘方块)纹理贴图层合成构造,只需启动 GIMP 并使用文件➤打开菜单序列打开你的/NetBeansProjects/JavaFXGame/src/文件夹中的 gameboardquad1.png 文件。这将使其成为最底层,如图 18-1 的左侧(蓝色高亮显示)所示。打开另外三个 gameboardquad 纹理贴图,如图 18-1 右上方的三个选项卡所示。选择每个选项卡,使用选择➤全部菜单序列,然后使用编辑➤副本。点击第一个标签,包含你的多层复合,并使用编辑➤粘贴为➤新层菜单序列添加这三层在第一层(橙色)之上,如图 18-1 左侧所示。用破折号和单词动物、植物、矿物和其他将这些层重命名为 gameboardquad,如图 18-1 所示。选择第四层(粉色,最顶层),使用文件➤打开为层菜单序列,将 SteelHoop.png 24 位图像文件添加到合成的顶层,给出如图 18-1 中预览区域所示的结果。现在,让我们在网上找到一个动物图像,我们可以在纹理的钢圈区域内使用。

A336284_1_En_18_Fig1_HTML.jpg

图 18-1。

Create a quadrant texture composite with four quadrant diffuse color maps; then add a steel decorative hoop

我用于商业用途(如这个教育游戏和书籍)的免版税图像的网站是 Pexels.com。进入 www.pexels.com ,如果在首页没有看到鹦鹉,在搜索栏输入 parrot。下载一个鹦鹉图片,如图 18-2;然后右键单击下载的图像(在浏览器中它自己的选项卡中)并选择复制图像。进入 GIMP,使用剪贴板菜单序列中的文件➤创建➤,将数字图像数据粘贴到 GIMP 中它自己的合成文件中(并在右上方的一个新选项卡下)。

A336284_1_En_18_Fig2_HTML.jpg

图 18-2。

Use File ➤ Create ➤ From Clipboard to paste content copied from the Pexels.com download in GIMP for editing

我们需要这个图像的一个正方形区域来使用游戏棋盘正方形纹理和游戏棋盘象限纹理的圆形部分。这将使用图 18-3 所示的矩形选择工具创建,设置为 2160x2160 的正方形区域,其大小将调整 500%以适应游戏板象限内 432x432 的圆形区域。

定位您的正方形选择以优化您内容的可识别部分,如图 18-3 所示。

A336284_1_En_18_Fig3_HTML.jpg

图 18-3。

Set the Rectangle Select tool’s Size properties to 2160x2160 and then drag the selection into an optimal position

由于 Pexels.com 图像的像素大小不同,我只需找到每个象限的中心区域所需的目标 432 像素正方形图像大小的偶数倍,并为此设置矩形选择工具。一旦设置了选择方块,您就可以拖动它的内部区域来微调它的位置,以显示其中的最大内容。然后使用编辑➤复制菜单序列将数据复制到剪贴板,然后使用文件➤从剪贴板菜单序列创建➤来创建新的正方形图像,如图 18-4 所示,我们将对其进行五倍缩减采样,以达到 432x432。这是通过使用图像➤缩放图像菜单序列打开缩放图像对话框来完成的,在该对话框中,您将用 432 替换 2160(保持纵横比锁定)并单击缩放按钮。

A336284_1_En_18_Fig4_HTML.jpg

图 18-4。

Use the Image ➤ Scale Image work process and reduce the 2160-pixel image 500 percent to be 432 pixels square

下一步是将这个 432 像素的正方形图像放在你的象限纹理贴图的 512 像素正方形区域的中心,这是我们在复制它之前要做的,因为这是一个更简单的工作过程。为此,我们将简单地使用图像➤画布大小菜单序列,然后将画布大小从 432x 432 增加到 512x 512。请确保单击对话框的中心按钮,否则您的图像将位于调整大小的画布的左上角,这个居中过程将允许您在没有图像的地方获得透明度(alpha 通道)值,这正是我们想要实现的结果。

正如您在“设置图像画布大小”对话框中看到的,一旦您单击图 18-5 中以浅蓝色显示的中间按钮,该对话框将计算图像整个周长周围的 X 偏移和 Y 偏移值(在本例中为 40 像素),如 512–432 = 80/2 = 40。最后,单击调整大小按钮,将这个 432 像素的正方形变成 512 像素的正方形,使用透明度居中,因此它将在钢圈中居中。现在,您已经准备好使用全选➤,然后编辑➤复制菜单序列将数据放入剪贴板,数据将被粘贴到不同的选项卡上。

A336284_1_En_18_Fig5_HTML.jpg

图 18-5。

Use the Set Image Canvas Size dialog to resize the canvas to 512 pixels

确保将图层下拉选择器设置为所有图层,以包含图层中生成的透明区域。如果你忘记这样做,只需右键单击新的 512 像素层,并使用图层图像大小选项。确保在使用“选择➤全部”和“编辑➤副本”之前完成此操作,以便同时选择透明度和影像。

下一步是点击象限纹理合成选项卡,如图 18-6 所示,然后选择最底层(动物)层,这样当你粘贴居中的鹦鹉方块图像时,它位于象限的基础纹理之上,钢环装饰之下。我们将使用这个钢箍图像和它的透明度来切掉鹦鹉图像的角,以便它与钢箍图像无缝集成。

为了完成这个 GIMP“移动”,你将选择钢箍层,在图 18-6 中显示为选中的蓝色,然后单击魔棒工具,在 GIMP 工具图标区域显示为选中(按下)。单击钢圈中心(透明)区域内的魔棒工具,这将选择钢圈内的这个区域。您需要扩展这个选择区域,以便鹦鹉图像实际上位于钢圈边缘的下面,或者您将看到鹦鹉图像的边缘周围有一条缝,一旦它被剪切出来,它就位于钢圈的内部。

要做到这一点,在你看到你的钢圈里面的选择后,如图 18-6 所示,你会想要使用选择➤增长菜单序列和扩展选择,这样它实际上看起来像是在钢圈的顶部,从 1 到 9 个像素的任何地方。(我一般至少用 2 个,保险起见;在这种情况下,我使用了 4 个像素。)

A336284_1_En_18_Fig6_HTML.jpg

图 18-6。

Select the Steel Hoop layer and Magic Wand tool and click inside the hoop to create a selection

此时,由于选择了钢箍层,因此选择确实在钢箍的顶部。但是,一旦选择了剪贴板层(如果您愿意,可以通过双击层名称来重命名 Parrot ),该选择将位于该层的顶部(以及在该层上使用),因此将位于钢圈层的下方。

将增大选择对话框值设置为 4,点击确定按钮,如图 18-7 底部所示。

A336284_1_En_18_Fig7_HTML.jpg

图 18-7。

Use the Grow Selection dialog and expand the selection area by 4 pixels beyond the interior

下一步是选择您的剪贴板层,其中包含您的鹦鹉图像,使其受到选择的影响,我们从钢圈层(内部)透明度“挑选”。然后选择选择➤反转菜单序列。这将“保留”圆圈内的内容,删除圆圈外的内容(也就是说,只要您点击键盘上的 delete 键)。这将移除图 18-7 中突出的图像边角。

如图 18-8 所示,这一工作过程的最终结果是一个完全平滑的图像合成,圆形的鹦鹉图像在钢圈装饰的内部(和后面)。图 18-8 还显示了制作完美游戏板象限纹理贴图的最后一步,即将内容旋转 45 度,这样当你的 3D 游戏板旋转到它的点上时,图像相对于观看的玩家被正确定位。

A336284_1_En_18_Fig8_HTML.jpg

图 18-8。

Once you delete the corners, you have a perfect compositing result and are ready to rotate the parrot 45 degrees

接下来需要做的事情是旋转剪贴板(鹦鹉)层 45 度,这应该是无缝的,因为鹦鹉图像已经使用基于数学的工作过程居中和四舍五入。由于你的剪贴板图层仍然被选中,你所要做的就是选择图层➤变换➤任意旋转,如图 18-8 所示,打开旋转对话框,如图 18-9 所示。

A336284_1_En_18_Fig9_HTML.jpg

图 18-9。

Rotate the parrot 45 degrees using a Rotate dialog so that it is upright for the game board quadrant spin

在角度文本字段中输入 45 度值来旋转鹦鹉图像,以便在游戏板微调器选择橙色动物象限后,它将是直立的。将你的中心 X 和中心 Y 旋转坐标放在 512 像素纹理贴图的正中心,这将是一个 256 的值,如图 18-9 所示。

图 18-9 中还显示了旋转工具网格,它覆盖正在旋转的图像。这将允许您使用 16x16 的直线网格覆盖,更精确地显示内容是如何旋转的。

旋转工具和网格的设置可以在“旋转”对话框的左侧看到,您可以在其中设置网格线(称为参考线)的数量,设置图像预览选项,设置旋转方向,以及设置剪辑和图像不透明度。如您所见,GIMP 2.8.22 可以成为专业 Java 9 游戏开发者的强大工具。

请注意,旋转栅格导向显示在钢箍层上,因为它在层合成中处于打开状态(显示)。如果你想在圆形的鹦鹉图像上看到旋转向导,你可以关闭钢圈和游戏板四动物层的眼睛图标。请记住,只有所选图层才会受到图层➤变换➤旋转操作的影响,因为 GIMP 是一个模态软件包,只能对所选图层、工具、颜色、选择集和选项的组合进行操作。这使得它相对复杂,但同样的功能使它比非模态数字成像软件更强大。

单击旋转按钮完成旋转算法设置,并将旋转应用于图像。现在您所要做的就是将图像作为 gamequad1bird1.png 导出到 netbeans projects/Java FX game/src 文件夹中。然后,我们可以进入 Java loadImageAssets()方法,通过更改 diffuse21 贴图来引用此图像,而不是默认图像,从而测试新的纹理贴图。(我们这样做是暂时的,以便我们可以看到它在 3D 游戏棋盘象限上呈现时的样子。)

打开 NetBeans 9 和 JavaFXGame 项目。然后,通过使用空白处的加号(+)图标打开 loadImageAssets()方法体,并临时编辑用于橙色游戏棋盘象限的 diffuse21 纹理,以引用您刚刚使用以下 Java 语句创建的 gamequad1bird1.png 文件,该文件也显示在图 18-10 中,以黄色和蓝色突出显示:

A336284_1_En_18_Fig10_HTML.jpg

图 18-10。

Set the diffuse21 Image object to temporarily reference the gamequad1bird1.png texture map you created

...

diffuse21 = new Image("/gamequad1bird1.png", 512, 512, true, true, true);
...

现在我们需要做的就是测试新代码,看看当 3D 旋转器随机选择这个游戏棋盘象限(主题)让玩家回答时,游戏棋盘落在动物(橙色)象限时是什么样子。

这可能需要多次旋转尝试,因为 random 类的 Random 对象(随机数生成器引擎)实际上非常有效,可以在 3D 旋转器的每次后续旋转中提供随机游戏板象限结果。

使用运行➤项目工作流程旋转微调器,直到选择象限 1,如图 18-11 所示。

A336284_1_En_18_Fig11_HTML.jpg

图 18-11。

The parrot quadrant texture map rendered on the game board surface

游戏棋盘方块:在 GIMP 中创建游戏方块内容

游戏棋盘方格定义符合象限定义,为玩家提供了五个与象限类别相关的不同主题以供选择。在游戏板随机旋转为他们选择类别并随机加载主题内容后,玩家可以决定自己的命运(问题)。打开第二个(256 像素)纹理贴图文件,我们用游戏棋盘方块模板处理,如图 18-12 (第三个标签)所示。也打开 Pexels.com 图像和 2160 像素的正方形区域,我们将使用它来表示鹦鹉。我们需要做的第一件事是将 2160 像素的图像缩小到 192 像素,以适应颜色区域。由于周长为 32 像素(两个维度上的中心区域为 256-(2×32)= 192 像素),使用图像➤缩放图像工作流程将图像缩小到 192 像素,如图 18-12 左下方所示。

A336284_1_En_18_Fig12_HTML.jpg

图 18-12。

This time scale your 2160-pixel image to 192 pixels so it perfectly fits inside your game board squares

接下来,使用视图➤缩放➤ 100%(称为实际像素视图模式)菜单序列,“正常化”您正在查看的图像,然后进行相同的“居中透明”工作过程,就像我们使用图像➤画布大小菜单序列对象限所做的那样。将画布扩展并居中回到 256 像素,以匹配游戏板正方形纹理贴图的大小,如图 18-13 所示,这样图像就会完美贴合你的纹理贴图。

A336284_1_En_18_Fig13_HTML.jpg

图 18-13。

Resize the canvas to 256 pixels and center the 192-pixel image inside the 32 pixels of transparency to center

使用“选择➤全部”菜单序列,然后使用“编辑➤复制”菜单序列,将透明的 32 像素边界和内部的 192 像素图像数据复制到操作系统剪贴板中。(是的,剪贴板实际上是操作系统的一部分,因此您可以在所有不同的运行应用之间剪切、复制和粘贴数据。)

选择 256 像素游戏板正方形纹理合成选项卡和游戏板正方形图层下面的图层,并使用编辑➤粘贴为图层菜单序列将图像粘贴到红色边框下面。请注意,在这种情况下,您也可以将该层粘贴到游戏板方形边缘颜色层的顶部;因为我们在合成层中使用了所有的直线,每一层在数学上“像素对像素”地邻接另一层,所以没有重叠的像素,这在我们的圆形象限合成中不是这样。

我将使用不同的名称 gameboardsquarecontent1.xcf 保存这个纹理贴图文件,这样它只包含第一个游戏棋盘方块的图像和边缘装饰。最终将会有 20 个这样的 XCF 文件,Q1S1 到 Q4S5 游戏板节点象限子节点各一个。

当我们添加内容时,这些文件的大小会均匀地增加,这样你就不会有一个庞大的文件需要处理。这种方法将使您的 pro Java 9 游戏开发工作过程更有条理。

请注意,图 18-14 中的截图仍然使用第十三章中的 Pro _ Java _ 9 _ Games _ Development _ Texture _ maps 4 XCF 文件,该文件涵盖了 3D 图元着色器和纹理映射概念以及 Java 编码。

A336284_1_En_18_Fig14_HTML.jpg

图 18-14。

Select the 256 texture map composite tab, select the layer under the red square, and select Edit ➤ Paste as Layer

在本章的后面,我们将生成 20 个游戏棋盘方格内容生成 XCF 文件,这些文件将累积数字图像内容,Java 代码最终将从初始随机旋转为游戏选择的每个游戏棋盘方格(即,随机选择的象限中的五个方格)中选择这些内容。这种方法允许我们随机化象限以及象限中每个游戏棋盘方格的内容。

最后,让我们使用 GIMP 文件➤导出为菜单序列,并将这个新的漫反射纹理贴图保存在/netbeans projects/Java FX game/src 文件夹中作为 gamesquare1bird1.png。注意在你的文件管理器中,这个纹理贴图的大小小于 80KB,比你的默认纹理 1KB 要大很多。高质量的 24 位内容总是会增加应用的数据占用量。如果您想进一步优化此数据足迹,您应该使用 GIMP 中的图像➤模式➤索引菜单序列,将您的图像转换为 8 位索引彩色图像,并使用 Floyd-Steinberg 抖动生成最佳调色板(256 色)。这将把 gamesquare1bird1.png 的大小减少到 27.4KB,因为它现在是一个 PNG8 映像,具有高质量的结果。

你如何命名这些文件是很重要的,因为你的游戏 Java 代码,我们将在下一章开始写,将会根据这些命名成分做出随机的决定逻辑。显然,gamesquare1(名称的第一部分)将定义映射哪个 gamesquare (Q1S1 到 Q4S5)。第二部分是子类化。在这种情况下,它是“鸟”,但也可能是“猫”、“狗”或“牛”等等。最后一部分是随机数发生器必须从多少个选项中选择,所以如果游戏棋盘方格 1 有鸟 1 到鸟 5,你的随机对象将从 0 到 5 中选择。这样,当您添加新内容时(在 20 个集合中,或者每个游戏棋盘方格一个图像主题中),您可以增加随机对象的随机数——生成 Java 代码,以便在您添加内容时添加新的最大随机数(零穿过选择上限)。

接下来,让我们在 3D 游戏板上测试第一个游戏板方块,方法是将 diffuse1 图像对象引用从空白(默认)游戏板方块纹理贴图替换为其中有图像的贴图。图 18-15 的顶部显示了您的 Java 9 图像对象实例化(和加载)构造,应该类似于以下代码:

A336284_1_En_18_Fig15_HTML.jpg

图 18-15。

Test the first game board square by swapping in the new texture map image in the diffuse1 instantiation

diffuse1 = new Image("/gamesquare1bird1.png", 256, 256, true, true, true);

使用运行➤项目工作流程,确保内容面向正确的方向,如图 18-16 所示。

A336284_1_En_18_Fig16_HTML.jpg

图 18-16。

The parrot is facing out of the edge of the game board, so there’s no need to rotate the image

因为我们需要至少两张游戏棋盘方格的纹理贴图,所以去 Pexels.com 找另一张鸟的图片来做第二张 gamesquare1bird0.png 的图片。我们将从零开始对图像文件进行编号,以更好地匹配随机数生成器的输出。我发现了一只巨大的鹰(或者可能是一只鹰;我们将在后面的章节中研究游戏板的内容,以确保一切都是正确的),如图 18-17 所示。

A336284_1_En_18_Fig17_HTML.jpg

图 18-17。

Paste the second bird image data into GIMP and use Scale Image to find the lowest common resolution

由于这是一个低分辨率的图像,我们将使用 689(高度)作为正方形的尺寸,因此使用矩形选择工具并在位置 428,0 输入一个 689,689 大小的正方形,如图 18-18 的左侧所示。

A336284_1_En_18_Fig18_HTML.jpg

图 18-18。

Create a 689x689 pixel square for the image since it’s not an HD | UHD image with thousands of pixels

使用编辑➤复制菜单序列将这些数据复制到你的操作系统的剪贴板,然后使用 GIMP 文件➤从剪贴板创建菜单序列将方形图像数据粘贴到它自己的编辑标签中,如图 18-19 所示。使用图像➤缩放图像菜单序列,将 689 像素数据缩小到 192 像素。然后使用图像➤画布大小菜单序列,进入设置图像画布大小对话框,如图 18-19 所示。使用对话框中的中心按钮,将图像画布的大小扩展到 256 像素见方,同时将图像数据放在透明度的中间。请记住,在对图像应用调整大小操作后,选择层下拉列表中的全部调整层大小或右键单击以调用层到图像大小。

A336284_1_En_18_Fig19_HTML.jpg

图 18-19。

Scale 689 pixels to 192 pixels and then use Set Image Canvas Size to add your 32-pixel transparent perimeter

接下来,使用“选择➤全部”和“编辑➤复制”菜单序列来选择图像和透明数据;然后单击游戏板正方形 1 纹理贴图选项卡(第一个选项卡),并单击游戏板颜色正方形下面的层。然后使用编辑➤粘贴为层菜单序列,将第二个鸟的图像粘贴到你的合成中,如图 18-20 所示。在图 18-20 右上角的第三个选项卡的预览图标中可以看到图 18-19 的操作结果。

A336284_1_En_18_Fig20_HTML.jpg

图 18-20。

Paste your 256-pixel image plus transparency under your GameBoardSquare1 edge coloring texture layer

如果你仔细观察它的用户界面,你会发现 GIMP 很好地向你展示了你的工作过程。您还可以通过选择不同的预览图标大小以及使用您自己的信息性(描述性)文本标签命名层来自定义 UI。

要改变图层图标预览的大小,我将在本章的后面做一点,使用小箭头在图层面板的右上角,在你的笔刷编辑器标签的旁边,在模式(正常)下拉选择箭头的上面。你可以选择预览大小➤微小到预览大小➤巨大,给你八个不同的图标大小的选择。

现在你已经创建了 192 像素的游戏棋盘方块插页,如图 18-20 所示,你需要使用编辑➤撤销工作流程来返回到 689x689 的原始图像方块,这样你就可以为每个游戏棋盘方块创建一个游戏棋盘象限版本。我们将在下一步这样做,这样我们就可以创建象限纹理。

一旦玩家在随机选择的象限中选择了五个游戏棋盘方格中的一个,您的 Java 代码(最终)会将所选的问题图像放入游戏棋盘象限,并就此向玩家提问。

要返回到 689 像素的正方形图像,请在 GIMP 中选择包含正方形图像数据的选项卡,并使用“编辑➤”“撤消”菜单序列来撤消您之前应用的所有选择、画布大小调整和图像缩放操作,以创建用于 256 像素游戏板正方形漫反射颜色纹理贴图的层数据。

这样做是为了进行类似的工作过程(加上 45 度旋转)来创建 512 像素的象限纹理贴图。这是为了当玩家点击包含相同图像的游戏棋盘方块时,将会有更大(装饰的)版本的图像内容主题(问题)进行预览。

每次你使用编辑➤撤销,你会看到 GIMP 在软件中重新创建以前的图像编辑状态,这样你就可以直观地看到你什么时候回到你原来的 689x689 图像方块。如果你回去太远,你会看到整个原始图像从 Pexels.com,因为也有一个编辑➤重做命令,你可以很容易地回到广场图像版本!撤销/重做功能对于像这样的重复工作过程是非常强大的,我们需要使用相同的原始图像数据源创建多个纹理贴图。

创建第二象限纹理,如图 18-21 所示,通过使用图像➤图像尺寸将 689 像素的图像调整为 432 像素。接下来,使用“图像➤画布大小”工作流程,通过将画布大小增加到 512 像素并单击“中心”按钮,将此图像数据置于透明区域的中心。使用“图层到图像大小”选项将图层的透明像素包含在图像像素中,然后使用“选择➤全部”和“编辑➤副本”将所有图像和透明数据传输到操作系统剪贴板中。选择一个层下的钢箍层,并使用编辑➤粘贴为层插入它。

A336284_1_En_18_Fig21_HTML.jpg

图 18-21。

Create a 432-pixel image inside the 512-pixel texture with a transparent border; paste it underneath the hoop

将此图像数据粘贴到钢圈图层下,并将剪贴板图层旋转 45 度,如图 18-22 所示。

A336284_1_En_18_Fig22_HTML.jpg

图 18-22。

Rotate the image layer 45 degrees after inverse-selecting and deleting corners protruding from the hoop

接下来,使用文件➤导出作为工作过程,并保存您的第二个 gameboardquadrant1bird0.png 文件(因为我已经决定从零开始编号,以匹配随机数生成器的输出)。

让我们通过使用下面的 Java 9 代码更改 diffuse1 和 diffuse21 图像对象文件名引用来预览第二个游戏板 square 1 和游戏板 quadrant 1 纹理,如图 18-23 所示:

A336284_1_En_18_Fig23_HTML.jpg

图 18-23。

Change the diffuse1 and diffuse21 Image object texture map reference to test the two new texture maps

diffuse1  = new Image("/gamesquare1bird0.png", 256, 256, true, true, true);

diffuse21 = new Image("/gamequad1bird0.png", 512, 512, true, true, true);

使用您的“运行➤项目”工作流程,确保内容看起来不错,并且面向正确的方向,如图 18-24 所示。祝贺您,您已经完成了 80 个纹理贴图中的 4 个,为了测试 random.nextInt(2)方法调用 Java 9 代码,您需要准备好这些贴图,Java 9 代码将为每个游戏棋盘方格随机选择两个图像。

A336284_1_En_18_Fig24_HTML.jpg

图 18-24。

Use Run ➤Project and render the quadrant 1 and square 1 texture maps to check the orientation and quality

如果每个游戏棋盘方格有 4 个随机图像可供选择,则需要 160 个图像;如果每个游戏棋盘方格有 8 个随机图像可供选择,则需要使用您在本章中学到的工作流程创建 320 个图像。

重要的是要记住,游戏棋盘方形角图像需要旋转 45 度,所有象限图像也是如此。一些游戏棋盘的正方形侧面图像(正方形 4、5、9、10、14、15、19 和 20)需要旋转 90 度才能在游戏棋盘上正确“面朝外”。在本章中,我们将会看到所有这些数字成像场景,对于一个没有编程的章节来说,这将会是一个相当长的章节,有很多 GIMP 截图。

也就是说,专业的 Java 游戏开发涉及的不仅仅是编码,因为 JavaFX 9 支持六种新的媒体类型,包括 3D、数字插图(SVG)、数字图像(PNG)、数字音频等等!

现在我们已经创建了 GameSquare1.xcf 游戏板 Square1 图像合成的基础,让我们创建其他的,用正确的周长颜色值替换纹理贴图的顶部装饰部分,并使用相同的文件名保存它们,同时每次将末尾的数字增加 1,直到您拥有所有 20 个。之后,你所要做的就是添加图像层到每一个中,为游戏棋盘方格和游戏棋盘象限创建棋盘游戏内容。事实证明,创建 Pro Java 9 游戏需要大量的艰苦工作!

我们可以使用文件➤另存为菜单序列来保存文件的另一个版本,一旦我们改变了游戏板正方形周长的颜色值,并用替代内容替换图像层。要以外科手术般的精确度做到这一点,最简单的方法是使用文件➤打开为层,并在合成的层中打开 gameboardsquare2.png 纹理贴图,使用滴管(拾色器)工具单击周边颜色以将 FG 前景色设置为该值,选择 PaintCan(颜色填充器)工具,选择透明的游戏板方形装饰层,然后单击方形颜色区域(在本例中为红色)中的 PaintCan 工具。这将用下一个(橙色)颜色值填充红色。然后,您所要做的就是删除具有默认(空白)游戏棋盘方块颜色参考的层,并使用文件➤另存为来保存 GameSquare2.xcf 合成文件,该文件现在已准备好供您填充要在第二个棋盘游戏方块中使用的图像数据。

请注意,在图 18-25 中,我将图层图像预览图标做得更大了,这样你就可以更详细地看到我对游戏棋盘 square 2 图像、边框装饰颜色等所做的工作。如果您正在使用新的 UHD (4K)显示器,这可能也是必要的,因为所有东西看起来都有点小(除非 UHD 显示器是 60 英寸或更大)。我的大多数 UHD 桌面显示器都是 43 英寸的,因为它们价格实惠。

A336284_1_En_18_Fig25_HTML.jpg

图 18-25。

Create a GameSquare2.xcf compositing file with a red-orange border and image assets to use for square 2

接下来,让我们看看如何将 GameSquare3.xcf (corner)中的图像内容旋转 45 度,以及如何将 GameSquare4.xcf 和 GameSquare5.xcf 中的图像内容旋转 90 度,使其远离游戏板,就像方块 1 和 2 中的内容一样。

如图 18-26 所示,第三个(以及第八、第十三和第十八个)游戏棋盘角方块的彩色方块周边装饰内的数字图像内容需要顺时针旋转 45 度,就像游戏棋盘象限纹理贴图一样。这将使数字图像内容在每次游戏板旋转后面向玩家。

A336284_1_En_18_Fig26_HTML.jpg

图 18-26。

Create a GameSquare3.xcf compositing file with an orange border and two image assets to use for square 3

由于这个原因,我不得不使用更高的像素分辨率值,而不是将高清正方形图像缩小到 192 像素,因为旋转会暴露一些透明像素;你可以在图 18-26 中的第二层和第三层预览图像中看到这一点(记住,灰色棋盘方块代表透明像素)。

在图 18-26 中,您可以在主要 GIMP 图像预览(画布)区域的角落中看到一点点这种透明度。我使用了 264 的下采样值,这并不完美(268 或 272 会更好),但这对于本章中我们现在所处的内容开发和代码测试阶段来说已经足够了。

我怀疑是否有任何玩家会注意到纹理贴图图像的远处角落里的这一小撮透明像素,尤其是在它被映射到游戏棋盘的方格上之后。我将使用 Java 代码渲染棋盘游戏(就像我在本章中一直做的那样),一旦我使用这些工作过程为游戏棋盘的象限 1 完成了 20 个纹理贴图。如果你想提前看到这一点,并确认很难看到游戏棋盘方块 3 的纹理贴图有任何问题,请随意这样做(图 18-28 )。

然而,在你发布你的游戏之前,确保所有对角游戏板的正方形角落都有足够大的图像数据(比如,旋转前 272 像素,只是为了确保没有角落伪像)!

第四个和第五个(以及第九个、第十个、第十四个、第十五个、第十九个和第二十个)游戏棋盘方块的彩色方块周边装饰内的数字图像内容需要顺时针旋转 90 度,如图 18-27 所示,以便数字图像内容在每次游戏棋盘旋转后面向玩家。

A336284_1_En_18_Fig27_HTML.jpg

图 18-27。

The game board squares 4, 5, 9, 10, 14, 15, 19, and 20 will need to use an image rotated clockwise 90 degrees

在这种情况下,我们仍然会将您的高清方形图像缩小到 192 像素,在周边添加 64 个额外像素(居中时为 32 个),并且,正如您在图 18-27 中的第二层和第三层预览图像中看到的,使用图层➤变换➤顺时针旋转 90°菜单序列。一旦创建了五个纹理贴图,您将在 loadImageAssets()方法中引用它们,并使用运行➤项目工作流程来测试它们,以查看它们如何映射到象限 1 游戏棋盘正方形子元素上,如图 18-28 所示。

A336284_1_En_18_Fig28_HTML.jpg

图 18-28。

Render six quadrant and square texture maps to check their orientation and quality

我要做的最后一件事是改变 diffuse6 到 diffuse10 的图像对象引用,以及 diffuse 22 的图像对象引用,以测试纹理贴图将如何应用到与象限 2 相连的游戏棋盘方格。这将告诉我,如果,以及如何,我将需要改变我的图像旋转值,因为我开发蔬菜象限纹理贴图。这是使用图 18-10 、 18-15 和 18-23 中所示的工作流程完成的,你的一半漫反射纹理贴图将引用你正在开发的纹理贴图,暂时使用你的 loadImageAssets()方法体代码。对于真正的棋盘游戏,这将在事件处理方法主体中交互地完成,基于玩家的鼠标点击,结合条件处理和随机数生成。

分别复制 diffuse1 到 diffuse5 和 diffuse6 到 diffuse10 中引用的文件名,并将最后一个数字从零更改为一(或者反过来,如果您愿意,可以混合使用的图像)。在本章结束时,您可以看到运行➤项目工作流程的结果,如图 18-29 所示。

A336284_1_En_18_Fig29_HTML.jpg

图 18-29。

Add the quadrant 1 texture maps to the quadrant 2 Java code and test the texture map orientation

图 18-28 显示了使用运行➤项目工作流程在游戏板上渲染的六个漫射纹理贴图。

正如你在图 18-29 中看到的,你将需要调整你在本章中学到的游戏板每个象限的工作过程(直到旋转值)。只要你测试你的纹理贴图,就像你在本章中学到的那样,这应该不是什么问题,并且会给你一些 GIMP 的练习。

在创建您自己的自定义数字图像内容时,您将获得本章中描述的工作流程的大量实践。GIMP 是一个令人惊奇的软件包,它的新版本很快就会问世,在某些情况下,它会超越昂贵的数字图像合成器软件包的功能!

摘要

在第十八章中,我们学习了更多关于 GIMP 中创建大量游戏内容所需的工作流程,我们需要这些内容来为学龄前儿童、自闭症患者和学习障碍者创建专业级教育游戏。我向您展示了为第一象限完成足够内容的工作过程,以便能够开发我们将在下一章中开发的随机内容选择 Java 代码。然后,您可以使用相同的工作流程来开发其他三个游戏板象限的内容。

您了解了如何从 Pexels.com 获得免费的商业用途内容,如何将其复制到操作系统剪贴板,以及如何使用从剪贴板创建➤特性在 GIMP 的选项卡中打开它。您学习了如何制作用于游戏棋盘象限和正方形纹理贴图的正方形图像数据,以及如何将其居中、裁剪和旋转。

在第十九章中,我们将实际开发 Java 代码来实现更多的游戏性和你在本章中学到的如何创建的游戏内容,这样我们就可以在完成游戏棋盘方格和游戏棋盘象限的鼠标事件处理代码(在第十九章中)方面取得一些进展。

十九、游戏内容引擎:人工智能逻辑与随机内容选择方法

现在你已经有了游戏板动画,并且在每次点击 3D spinner UI 元素时随机选择了一个象限,我们需要弄清楚如何以某种方式跟踪这些旋转,这将持续地告诉我们我们已经到达了哪个象限。这样,我们可以将正确的漫射纹理贴图映射到该象限的游戏棋盘方格上。我们将使用简单的数学方法来做这件事,使用 int(整数),因为一个圆中的 360 度和一个象限中的 90 度都可以被整除。这将是一个有趣的章节,因为人工智能经常可以使用紧凑的 Java 代码来编码!一旦确定了一种方法,就需要对逻辑进行大量的思考,还要进行一些测试来提炼这些值。

在这一章中,我们将创建两个新的 int (integer)变量:spinDeg表示旋转角度,它是玩家旋转的旋转角度的累加器(或总和),以及 quadrantLanding,它将保存一个简单而强大的计算的最新结果,它将始终告诉我们最新的旋转落在哪个象限。

我们将创建六个新方法,包括用于确定当前象限的 calculateQuadrantLanding()方法,用于在每次新旋转之前将游戏板方块纹理贴图重置为默认值的 resetTextureMaps()方法,以及用于保存随机数生成的 populateQuadrantOne()到 populateQuadrantFour()以及为玩家的每次随机旋转挑选游戏板方块内容的条件 if()处理。

编码随机自旋跟踪器:余数算子

为了得到一个玩家在一次旋转后降落的最新象限的结果,我们需要跟踪整个旋转次数后的百分比,特别是当我们旋转三次加上偏移时。因此,对于 if()条件 1 中的 45 + 1080,这将是象限一。对于 if()条件 2 中的 45 + 1170,这将是象限二。对于 if()条件 3 中的 45 + 1260,这将是象限三。对于 if()条件 4,对于象限四,这将是 45 + 1350。然而,对于随后的旋转,这不会总是从 45 度开始的相同偏移,因此我们需要保留一个spinDeg total 变量,并将每个旋转角度相加,得到一个总数,我们可以将这个总数除以 360,得到完整的旋转,然后在 Java 中使用余数%操作数,得到游戏板象限已经到达的完整旋转之后的角度旋转。在 Java 代码中,该等式类似于以下内容:

int spinDeg = 45;                        // Initialize at 45 degrees
int quadrantLanding;                    //  Initialize at zero

spinDeg = spinDeg + lastSpinRotation;  //   Total Spin Angle Accumulator
quadrantLanding = spinDeg % 360;      //    Resting Angle Offset Calculation

象限变量将始终包含四个值之一:45(粉色或其他象限)、135(蓝色或矿物象限)、225(绿色或蔬菜象限)或 315(橙色或动物象限)。我们将创建一个名为 calculateQuadrantLanding()的方法,我们将在实现随机旋转的 MouseEvent 处理程序的末尾调用该方法。

实现自旋跟踪器功能:创建空方法

让我们创建两个整数变量和五个新方法的基础结构,我们需要它们来保存我们将在本章中编写的 Java 代码。这段代码将跟踪游戏板在每次旋转后停留在哪个象限,然后用随机内容处理游戏板方块的“群体”,这是我们在前一章中部分(25 %)开发的。我将使用象限 1(橙色)内容来测试我们在本章中制作的逻辑,因为我还没有创建开发初始代码所需的数百个图像素材(每个游戏棋盘方格六个,或开始时 120 个)。以后只需增加随机数生成器的上限值并更新 populateQuadrant()方法的逻辑,就可以添加更多的素材。我们在这一章要做的将会有几百行代码,所以我们会在这一章做很多编码工作,让游戏随机选择内容让玩家解决。

在 JavaFXGame 类的顶部声明一个名为 spinDeg 的 int,并将其设置为游戏板在启动时旋转的 45 度。此外,声明一个初始化为零(缺省值,因此不需要 0)的象限区域变量来保存象限旋转增量(45、135、225 或 315)。在类的底部创建五个空的公共 void 方法(您不必总是强迫 NetBeans 为您创建 Java 代码)。这应该类似于图 19-1 中用浅蓝色和黄色显示的 Java 语句和方法结构:

A336284_1_En_19_Fig1_HTML.jpg

图 19-1。

Declare int spinDeg and quadrantLanding variables; create empty quadrant content population methods

int spinDeg = 45;   // Gameboard is always rotated to point/corner; initialize to 45 degrees
int quadrantLanding;
...
private void calculateQuadrantLanding() {...}  // Empty Method Constructs will compile clean
private void populateQuadrantOne()      {...}
private void populateQuadrantTwo()      {...}
private void populateQuadrantThree()    {...}
private void populateQuadrantFour()     {...}

我们需要做的第一件事是将每次旋转的旋转量添加到 spinDeg“累加器”变量中。这将在 createSceneProcessing()方法体中的 MouseEvent 处理程序的if(picked == spinner)逻辑内完成,在设置每个随机象限的四个if(spin == randomNum)条件语句的每一个内完成。

在你的内心。createSceneProcessing()方法,并在 if(picked == spinner)条件构造内部,为四个随机旋转 if(spin == random)条件构造中的每一个添加一个累加器语句 spinDeg += degrees。

这些应该分别是spinDeg += 1080;spinDeg += 1170;spinDeg += 1260;spinDeg += 1350;。正如您所看到的,传递给动画对象的角度值也应该是添加到 spinDeg 累加器变量的相同值,这样您就有了用户旋转的所有角度增量的记录。

在 spinner random number picked conditional if()结构体的底部,添加对 calculateQuadrantLanding()方法的调用,以便在旋转发生后,您可以计算该 pick 的偏移量(象限),并将该整数数据值植入(写入)到您的 quadrantLanding 变量中,以便在您的其他游戏逻辑中使用,我们将在本章稍后编写代码。接下来我们将编写 calculateQuadrantLanding()方法。

spinDeg 累加器和 calculateQuadrantLanding()方法调用的 Java 代码应如下所示,并在图 19-2 中用蓝色和黄色突出显示:

A336284_1_En_19_Fig2_HTML.jpg

图 19-2。

Add a spinDeg accumulator to your spinner UI mouse click conditional if() logic to track where the quadrant is

if (picked == spinner) {

    int spin = random.nextInt(4);      // Random Number Generator determines next quadrant

    if (spin == 0) {
        rotGameBoard.setByAngle(1080);
        rotSpinner.setByAngle(-1080);
        spinDeg += 1080;               // Add 1080 to the spinDeg total
    }

    if (spin == 1) {
        rotGameBoard.setByAngle(1170);
        rotSpinner.setByAngle(-1170);
        spinDeg += 1170;               // Add 1170 to the spinDeg total
    }

    if (spin == 2) {
        rotGameBoard.setByAngle(1260);
        rotSpinner.setByAngle(-1260);
        spinDeg += 1260;               // Add 1260 to the spinDeg total
    }

    if (spin == 3) {
        rotGameBoard.setByAngle(1350);
        rotSpinner.setByAngle(-1350);
        spinDeg += 1350;               // Add 1350 to the spinDeg total
    }

    rotGameBoard.play();
    rotSpinner.play();

    calculateQuadrantLanding();        //  Call Method to calculate quadrantLanding variable
}

接下来,打开空的 calculateQuadrantLanding 并添加 quadrantLanding 变量和一个等号来设置我们将要在 equals 运算符右侧定义的方程。

由于 spinDeg 累加器是我们要分解为完整旋转加上四分之一旋转偏移的值,接下来键入 spinDeg 变量,它将始终保存玩家所做的每次旋转的累积“记录”。

要找到已经着陆的最新象限,只需通过将该累计总值除以 360(一次完整旋转的度数)从该累计总值中移除所有完整旋转,以保留(提取)完整旋转之外的增量,这将指示玩家最近旋转着陆的象限。

幸运的是,Java 语言中有一个叫做余数运算符的运算符,它可以帮您完成这项工作,让您不必构建任何复杂的方程。此余数运算符在要从中提取余数的变量后使用一个%(百分比)符号,在%符号后是要除以(在本例中为累加器)变量的数字,在本例中是旋转一整圈(360)的度数。如果使用伪代码,这将是 totalspindegreasaccumulated % OneFullSpin = degrees remaining。calculateQuadrantLanding()方法的 Java 代码应该如下所示,显示在图 19-3 的底部:

A336284_1_En_19_Fig3_HTML.jpg

图 19-3。

Add a calculateQuadrantLanding() method and code method body and call calculateQuadrantLanding() at the end of each MouseEvent

private void calculateQuadrantLanding() {
    quadrantLanding = spinDeg % 360;       // Remainder of spinDeg accumulator after all 360 spins
    System.out.println(quadrantLanding); // Print Angle Offset to Output Pane for Debugging Use
}

让我们使用一个运行➤项目的工作流程,看看输出窗格现在是否告诉我们自旋将落在哪个象限。这是你最终想要对玩家隐藏的信息,这样他们的“目的地”象限就不会在游戏棋盘停止旋转之前暴露出来,否则会破坏预期和游戏乐趣。

我所做的是创建图 19-4 中的截图,将 NetBeans 9 输出窗格定位(并调整大小)在游戏窗口的后面(和左边),以便可以看到角度增量(余数)的 println 输出。当我测试 i3D spinner UI 和游戏棋盘旋转周期时,这个 println 输出通知触发以确保它们现在随机并准确地落在随机游戏的不同颜色象限上(就像掷骰子,只旋转棋盘)。

当你点击微调按钮时,一个计算出来的旋转角度偏移量就会显示出来(现在的计算机速度很快),所以你可以提前知道游戏板将要落在哪个象限。目前,我们只是试图让游戏板在每次随机旋转时落在不同的象限上,并查看象限变量中的角度值,以便我们可以在未来的代码中测试这些。我们还希望旋转许多次,以确保这些象限角度偏移值每次都是完全相同的四个整数,并且它们不会变化,因为我们只想在代码中测试四个象限角度偏移值。此变量中的任何其他值都将“破坏”此代码。幸运的是,所有涉及象限和旋转的都涉及偶数!

至于哪个角度偏移值属于每个颜色象限,我们还没有测试和改进 Java 代码。这是我们在本章将要做的一部分,以确保我们确切地知道我们的 Java 9 游戏代码和 i3D 游戏棋盘旋转象限旋转着陆视觉结果之间发生了什么。

我们还希望看到我们得到了一个随机象限选择结果,它在图 19-4 的左下角用红色圈出。

A336284_1_En_19_Fig4_HTML.jpg

图 19-4。

Test the code to make sure that the remainder output represents one of the four quadrant rotation offsets

既然您已经确定 calculateQuadrantLanding()方法工作正常,随机象限选择也相对正常,我们需要处理填充您选择的游戏棋盘方格的代码。

Spin: OnFinished()事件处理后填充象限

现在,我们已经在 createSceneProcessing()方法中修改了 MouseEvent 处理构造,让我们打开 createAnimationAssets()方法,并在 rotGameBoard 动画对象中添加一些事件处理,以便我们可以在动画对象的旋转周期完成时触发一些代码来填充游戏板方块。我们这样做的原因是,如果我们在旋转之前填充游戏棋盘上的方块,玩家就会知道旋转的游戏棋盘会停在哪里!此外,我想向您展示如何“连接”一个动画对象,以便它可以在播放结束后触发其他事件和代码构造,正如您可能会想到的那样,这对于 pro Java 9 游戏开发非常重要。我们将从实现一个空的事件处理基础设施开始,我们将使用它来保存条件 if() Java 逻辑,告诉游戏板方块如何使用对四个 populateQuadrant()方法之一的方法调用来填充它们自己,这四个方法是 populateQuadrantOne()到 populateQuadrantFour()。在这里,我们必须首先推测象限图中的哪个角度偏移值应该等于每个游戏棋盘象限颜色空间(橙色或动物、绿色或植物、蓝色或矿物、粉色或其他主题)。

创建初始(空)OnFinished 事件处理程序 lambda 表达式所需的 Java 9 代码如下:在图 19-5 中也用红色、黄色和蓝色突出显示:

A336284_1_En_19_Fig5_HTML.jpg

图 19-5。

The empty createAnimationAssets() setOnFinished() event handling infrastructure for the rotGameBoard Animation

rotGameBoard.setOnFinished( event-> { ... } );

接下来,让我们使用一系列条件 if()语句来编写 rotGameBoard.setOnFinished()事件处理方法体。其中的每一个都将计算四个象限角度偏移整数值中的一个,并将该值“连接”到对位于类末尾的这四个 populateQuadrant()方法之一的方法调用。

然后,这些 populateQuadrant()方法将从您的不同内容图像中随机选择每个游戏棋盘方格,我目前有十五个(象限 1 的五个附加游戏棋盘方格中的每一个有三个)。因此,我们将有一个 random.nextInt(3)方法,该方法将从每个方块的三个图像素材中进行选择,设置漫射图像对象以引用选定的数字图像素材,然后设置该游戏棋盘方块的着色器以将该图像对象重新加载到该纹理贴图的内存中。

为了测试这一点,我们需要编写至少一个 populateQuadrant()方法;逻辑上的一个是 populateQuadrantOne()方法,因为我们已经创建了游戏棋盘象限内容(游戏棋盘方格 1 到 5)。在我们测试完这个之后。setOnFinished()事件处理程序来查看这些角度偏移是否确实将我们带到了正确的象限,然后我们将通过 populateQuadrantFour()方法体代码创建 populateQuadrantTwo(),方法体代码(临时)将象限 1 中的内容用作“虚拟”内容,仅用于代码测试目的。

你的新。setOnFinished()事件处理方法主体最初将从 45 度开始,并通过 315 度进行处理;它应该看起来像下面的代码,在图 19-6 中用蓝色和黄色突出显示:

A336284_1_En_19_Fig6_HTML.jpg

图 19-6。

In the .setOnFinished() event handler, check the quadrantLanding variable for degree offsets to determine quadrant

rotGameBoard.setOnFinished(event-> {
    if (quadrantLanding == 45)  { populateQuadrantOne();   }
    if (quadrantLanding == 135) { populateQuadrantTwo();   }
    if (quadrantLanding == 225) { populateQuadrantThree(); }
    if (quadrantLanding == 315) { populateQuadrantFour();  }
});

对于您的第一个 populateQuadrantOne()方法体,您将拥有五个游戏棋盘方格中每一个方格的部分。第一条语句将为该正方形生成您的随机数选择,第二条语句将对其进行评估,最后一条语句将使用。setDiffuesMap(图像对象)方法调用。该方法体的 Java 语句应该如下所示,如图 19-7 所示:

A336284_1_En_19_Fig7_HTML.jpg

图 19-7。

Create the image load and texture map change logic in the populateQuadrantOne() method based on random number selected

int pickS1 = random.nextInt(3);
if (pickS1 == 0){diffuse1 = new Image("/gamesquare1bird0.png", 256, 256, true, true, true);}
if (pickS1 == 1){diffuse1 = new Image("/gamesquare1bird1.png", 256, 256, true, true, true);}
if (pickS1 == 2){diffuse1 = new Image("/gamesquare1bird2.png", 256, 256, true, true, true);}
Shader1.setDiffuseMap(diffuse1);
int pickS2 = random.nextInt(3);
if (pickS2 == 0){diffuse2 = new Image("/gamesquare2bird0.png", 256, 256, true, true, true);}
if (pickS2 == 1){diffuse2 = new Image("/gamesquare2bird1.png", 256, 256, true, true, true);}
if (pickS2 == 2){diffuse2 = new Image("/gamesquare2bird2.png", 256, 256, true, true, true);}
Shader2.setDiffuseMap(diffuse2);
int pickS3 = random.nextInt(3);
if (pickS3 == 0){diffuse3 = new Image("/gamesquare3bird0.png", 256, 256, true, true, true);}
if (pickS3 == 1){diffuse3 = new Image("/gamesquare3bird1.png", 256, 256, true, true, true);}
if (pickS3 == 2){diffuse3 = new Image("/gamesquare3bird2.png", 256, 256, true, true, true);}
Shader3.setDiffuseMap(diffuse3);
int pickS4 = random.nextInt(3);
if (pickS4 == 0){diffuse4 = new Image("/gamesquare4bird0.png", 256, 256, true, true, true);}
if (pickS4 == 1){diffuse4 = new Image("/gamesquare4bird1.png", 256, 256, true, true, true);}
if (pickS4 == 2){diffuse4 = new Image("/gamesquare4bird2.png", 256, 256, true, true, true);}
Shader4.setDiffuseMap(diffuse4);
int pickS5 = random.nextInt(3);
if (pickS5 == 0){diffuse5 = new Image("/gamesquare5bird0.png", 256, 256, true, true, true);}
if (pickS5 == 1){diffuse5 = new Image("/gamesquare5bird1.png", 256, 256, true, true, true);}
if (pickS5 == 2){diffuse5 = new Image("/gamesquare5bird2.png", 256, 256, true, true, true);}
Shader5.setDiffuseMap(diffuse5);

接下来,使用您的“运行➤项目”工作流程并测试您的代码,您将在图 19-8 中看到,NetBeans 输出窗格显示,在第一次单击微调器 UI 后,quadrantlanding 变量中留下的角度偏移为 45 °,我最初将它(在原始代码中设置)为象限 1,正如您在图 19-6 中我的原始代码中看到的那样。

A336284_1_En_19_Fig8_HTML.jpg

图 19-8。

Test the OnFinished code with the quadrant content code, and notice that the angle offsets are off by one

然而,屏幕上选择的象限是象限 4,这意味着我必须将 onFinished()事件处理条件 if()代码中的角度偏移量移动 1,以便 315 移动到评估语句的顶部,将其他三个角度偏移量向下推一个角度评估。

我们接下来将测试的新 Java 代码如图 19-9 所示,它将所有内容旋转一周,现在看起来像下面的条件 if()求值语句块:

A336284_1_En_19_Fig9_HTML.jpg

图 19-9。

Shift the angle offset evaluation down (over) by one, bringing 315 to the top and pushing the others down

rotGameBoard.setOnFinished(event->{
    if (quadrantLanding == 315) { populateQuadrantOne();   }
    if (quadrantLanding == 45)  { populateQuadrantTwo();   }
    if (quadrantLanding == 135) { populateQuadrantThree(); }
    if (quadrantLanding == 225) { populateQuadrantFour();  }
});

让我们再次利用运行➤项目的工作过程,并测试这个新代码。当您单击旋转器时,当随机数生成器选取 315 时,游戏板现在停止在橙色象限,如图 19-10 所示。现在我们可以继续向 populateQuadrantTwo()添加代码,并继续我们的测试过程。

A336284_1_En_19_Fig10_HTML.jpg

图 19-10。

Your logic now works, as evidenced by the Output Pane’s 315 value and the correct quadrant positioning

选择 populateQuadrantOne()中的 Java 代码,复制粘贴到 populateQuadrantTwo 中,将 pick 整数名称和对象名称的 1 到 5 的值改为 6 到 10,但不改变文件名,如图 19-11 所示。

A336284_1_En_19_Fig11_HTML.jpg

图 19-11。

Duplicate populateQuadrantOne code in populateQuadrantTwo and configure the squares 6 through 10

int pickS6 = random.nextInt(3);
if (pickS6 == 0){diffuse6 = new Image("/gamesquare1bird0.png", 256, 256, true, true, true);}
if (pickS6 == 1){diffuse6 = new Image("/gamesquare1bird1.png", 256, 256, true, true, true);}
if (pickS6 == 2){diffuse6 = new Image("/gamesquare1bird2.png", 256, 256, true, true, true);}
Shader6.setDiffuseMap(diffuse6);
int pickS7 = random.nextInt(3);
if (pickS7 == 0){diffuse7 = new Image("/gamesquare2bird0.png", 256, 256, true, true, true);}
if (pickS7 == 1){diffuse7 = new Image("/gamesquare2bird1.png", 256, 256, true, true, true);}
if (pickS7 == 2){diffuse7 = new Image("/gamesquare2bird2.png", 256, 256, true, true, true);}
Shader7.setDiffuseMap(diffuse7);
int pickS8 = random.nextInt(3);
if (pickS8 == 0){diffuse8 = new Image("/gamesquare3bird0.png", 256, 256, true, true, true);}
if (pickS8 == 1){diffuse8 = new Image("/gamesquare3bird1.png", 256, 256, true, true, true);}
if (pickS8 == 2){diffuse8 = new Image("/gamesquare3bird2.png", 256, 256, true, true, true);}
Shader8.setDiffuseMap(diffuse8);
int pickS9 = random.nextInt(3);
if (pickS9 == 0){diffuse9 = new Image("/gamesquare4bird0.png", 256, 256, true, true, true);}
if (pickS9 == 1){diffuse9 = new Image("/gamesquare4bird1.png", 256, 256, true, true, true);}
if (pickS9 == 2){diffuse9 = new Image("/gamesquare4bird2.png", 256, 256, true, true, true);}
Shader9.setDiffuseMap(diffuse9);
int pickS10 = random.nextInt(3);
if (pickS10 == 0){diffuse10 = new Image("/gamesquare5bird0.png", 256, 256, true, true, true);}
if (pickS10 == 1){diffuse10 = new Image("/gamesquare5bird1.png", 256, 256, true, true, true);}
if (pickS10 == 2){diffuse10 = new Image("/gamesquare5bird2.png", 256, 256, true, true, true);}
Shader10.setDiffuseMap(diffuse10);

现在让我们使用一个运行➤项目的工作流程,看看象限是否填充了正确的内容,因为我们已经将“哑元”(象限 1)内容放入了 populatequadranttwo()方法体。当我们单击微调器时,代码现在应该选择一个随机象限,然后用内容填充该象限。除了游戏面板前面的象限充满随机图像之外的任何视觉结果都意味着代码中仍然有一些错误,我们仍然需要继续我们的游戏开发调试过程!

正如您在图 19-12 中看到的,第一次旋转 45 度的角度偏移,我们知道是象限 3(粉色或其他内容),选择了正确的内容,但是 onFinished 事件处理构造填充的是象限 2 (225 度角度偏移),而不是正确的象限 3!我们还有一些调试工作要做!

A336284_1_En_19_Fig12_HTML.jpg

图 19-12。

Use Run ➤ Project to test your code; something is still wrong with the quadrantLanding conditional if() code

因为我们不能简单地在条件 if()求值查找矩阵中再次旋转值,所以这里一定发生了别的事情!在考虑旋转时,即使 RotateTransform 中的值是正的,我记得在使用负值来纠正微调器方向以使其向前移动时,游戏板实际上是向后或逆时针旋转的,这对于游戏板旋转来说看起来更好。因此,我不想改变这一点!这意味着我需要回到最初的 45、135、225、315 评估,并简单地反转,因为游戏板实际上在数学上是向后旋转的,所以您的正确评估顺序应该反转为 315(象限 1)、225(象限 2)、135(象限 3),然后是 45(象限 4)。

populateQuadrant()方法对的这个新的角度评估顺序应该可以一劳永逸地解决我们的问题,所以让我们回到 OnFinished()事件处理基础结构中的 createSceneProcessing()方法体,重新排序这些 quadrantLanding = = angle 值,从 315 度开始,每次减少 90 度,直到 45 度。你可以在这里看到,你的代码和思想逻辑必须同步,才能成功地创建你的游戏逻辑!

请注意,用“虚拟内容”填充这些象限可以让您更好地确定您的游戏逻辑发生了什么,并且仍然可以将代码放在适当的位置,稍后您只需在所有游戏板内容开发完成后更改几个字符。正如你可能从上一章中了解到的,这需要和编写游戏代码一样长的时间,甚至可能更长,这取决于你要在游戏中包含多少内容。

我将尝试在每个游戏棋盘方格中提供至少三幅图像(主题或问题)。但是,对于一个专业的 Java 9 游戏,您可能希望至少有九个(使用 random.nextInt(9)方法调用)来获得内容选择频率更随机的内容外观。由于我必须在短时间内写这本书,我将无法在开发游戏逻辑、编码和截图的同时完成这本书。

除了尝试这个新的。setOnFinished()事件处理 Java 代码块,我从 populateQuadrantOne()方法体中复制粘贴 Java 代码创建 populateQuadrantThree()方法体并编辑,为下一轮测试创建游戏方块内容。如果成功了,我会对 populateQuadrantFour()做同样的事情。

经过这些修改后,您的 OnFinished()事件处理条件 if() Java 代码应该看起来就像下面的代码块,它也在图 19-13 的底部用黄色和浅蓝色突出显示:

A336284_1_En_19_Fig13_HTML.jpg

图 19-13。

Reorder the angle offset if() statements so that they evaluate to the reverse direction of the game board spin

rotGameBoard.setOnFinished( event-> {
    if (quadrantLanding == 315) { populateQuadrantOne();   }
    if (quadrantLanding == 225) { populateQuadrantTwo();   }
    if (quadrantLanding == 135) { populateQuadrantThree(); }
    if (quadrantLanding == 45)  { populateQuadrantFour();  }
});

如图 19-14 所示,当我选择运行➤项目来测试代码时,象限和内容是正确的,尽管象限 1 内容的旋转对于其他象限是不正确的。

A336284_1_En_19_Fig14_HTML.jpg

图 19-14。

The new angle offset evaluation code now provides the correct game board quadrant landing position

接下来,让我们完成 populateQuadrantThree()和 populateQuadrantFour()方法的创建,以便在测试游戏棋盘旋转和象限着陆代码时,我们可以在所有游戏棋盘方格中看到数字图像(视觉)内容;正如你所看到的,仍然有游戏内容设计工作要做,关于游戏板正方形图像的方向取决于它们在哪个象限被使用。

将您的 populateQuadrantTwo()(或您的 populateQuadrantOne()内容)代码结构复制并粘贴到空的 populateQuadrantThree()方法体中,并将 pickS、diffuse 和 Shader 值的范围更改为 11 到 15。暂时不要考虑图像对象引用,因为您只创建了一组象限图像资源。

将 populateQuadrantTwo()(或 populateQuadrantOne()内容)代码结构复制并粘贴到空的 populateQuadrantFour()方法体中,并将 pickS、diffuse 和 Shader 值更改为 16 到 20。暂时不要考虑图像对象引用,因为您只创建了一组象限图像资源。

图 19-15 显示了 populateQuadrantFour()的 Java if()结构,看起来像这样的 Java 代码:

A336284_1_En_19_Fig15_HTML.jpg

图 19-15。

Copy the populateQuadrantOne code to populateQuadrantThree and populateQuadrantFour and modify it

int pickS16 = random.nextInt(3);
if (pickS16 == 0){diffuse16 = new Image("/gamesquare1bird0.png", 256, 256, true, true, true);}
if (pickS16 == 1){diffuse16 = new Image("/gamesquare1bird1.png", 256, 256, true, true, true);}
if (pickS16 == 2){diffuse16 = new Image("/gamesquare1bird2.png", 256, 256, true, true, true);}
Shader16.setDiffuseMap(diffuse16);

int pickS17 = random.nextInt(3);
if (pickS17 == 0){diffuse17 = new Image("/gamesquare2bird0.png", 256, 256, true, true, true);}
if (pickS17 == 1){diffuse17 = new Image("/gamesquare2bird1.png", 256, 256, true, true, true);}
if (pickS17 == 2){diffuse17 = new Image("/gamesquare2bird2.png", 256, 256, true, true, true);}
Shader17.setDiffuseMap(diffuse17);

int pickS18 = random.nextInt(3);
if (pickS18 == 0){diffuse18 = new Image("/gamesquare3bird0.png", 256, 256, true, true, true);}
if (pickS18 == 1){diffuse18 = new Image("/gamesquare3bird1.png", 256, 256, true, true, true);}
if (pickS18 == 2){diffuse18 = new Image("/gamesquare3bird2.png", 256, 256, true, true, true);}
Shader18.setDiffuseMap(diffuse18);

int pickS19 = random.nextInt(3);
if (pickS19 == 0){diffuse19 = new Image("/gamesquare4bird0.png", 256, 256, true, true, true);}
if (pickS19 == 1){diffuse19 = new Image("/gamesquare4bird1.png", 256, 256, true, true, true);}
if (pickS19 == 2){diffuse19 = new Image("/gamesquare4bird2.png", 256, 256, true, true, true);}
Shader19.setDiffuseMap(diffuse19);

int pickS20 = random.nextInt(3);
if (pickS20 == 0){diffuse20 = new Image("/gamesquare5bird0.png", 256, 256, true, true, true);}
if (pickS20 == 1){diffuse20 = new Image("/gamesquare5bird1.png", 256, 256, true, true, true);}
if (pickS20 == 2){diffuse20 = new Image("/gamesquare5bird2.png", 256, 256, true, true, true);}
Shader20.setDiffuseMap(diffuse20);

使用“运行➤项目”工作流程,并多次单击微调器 UI 来彻底测试代码。您应该看到的是,每次游戏板落在正确的象限上,如 NetBeans 9 输出窗格中所指定的那样,游戏板方格将使用动物图像填充正确的象限,如图 19-16 所示。

A336284_1_En_19_Fig16_HTML.jpg

图 19-16。

All the angle spin evaluations are now correct, eventually filling all Shaders with texture map image data

请注意,纹理贴图的颜色和方向与每个象限的配色方案和方向还不匹配,但话说回来,当你在 GIMP 中工作时,你可以使用这个渲染预览向你展示你需要做什么来实现这一点,我在上一章关于游戏内容的 18 中向你展示了如何做。

纹理映射管理:编码一个 resetTextureMaps()方法

从图 19-16 中可以看出,下一个编程任务是编写方法,在下一个旋转动画开始之前将游戏板重置为默认(空)状态。这是通过用新的图像资源引用重新实例化图像对象(当前没有 setImageReference()方法调用,尽管应该有),将漫射颜色贴图数字图像引用设置回它们的默认文件来完成的。这将迫使 Java 9 对内存中先前引用的图像进行垃圾收集(重新分配),并用引用的新图像数据替换它。您还必须在下一行代码中引用关联的 Shader 对象,以将 Shader 对象重新插入内存,新的引用数据指向刚刚加载到内存中的新图像对象。由于我们将始终使用您的默认(空白)纹理贴图,这可以在一个 resetTextureMaps()方法中实现,该方法不会改变,但在每次后续随机游戏板旋转开始之前(即,在处理您的if(pressed == spinner)条件 if()构造中的其余语句之前),当调用该方法时,会将您的 3D 游戏板方块重置为其默认的未填充(带有数字图像主题或问题内容)状态。

回到你的 createSceneProcessing()方法,在你的if (picked == spinner) { ... }构造的顶部添加一个resetTextureMaps();方法调用,如图 19-17 所示。NetBeans 会弹出一个帮助菜单,为您编写方法体,因此在 javafxgame 中选择并双击创建方法“resetTextureMaps()”。JavaFXGame 选项,在图 19-17 中间用蓝色显示。

A336284_1_En_19_Fig17_HTML.jpg

图 19-17。

Add the resetTextureMaps() method call to the MouseClick event handling code; press Alt+Enter to have NetBeans create it

使用复制粘贴编程技术创建这个新的方法体相对容易。您只需键入引用 gameboardsquare.png 的新图像实例化 Java 语句,并键入新 Shader1 语句,将 setDiffuseMap()方法设置为 diffuse1 图像对象。之后,您所要做的就是选择这两行代码,在前两行代码下面再复制 19 次,将 1 更改为数字 2 到 20,并将这些数字添加到 PNG 文件名的末尾,这将引用不同颜色的默认游戏棋盘方形纹理贴图资源。

这将产生以下 40 条 Java 编程语句,在图 19-18 中也用浅蓝色和黄色显示:

A336284_1_En_19_Fig18_HTML.jpg

图 19-18。

Re-create the default Shader and diffuse statements for the blank game board in the body of resetTextureMaps()

private void resetTextureMaps() {
    diffuse1 = new Image("/gameboardsquare.png", 256, 256, true, true, true);
    Shader1.setDiffuseMap(diffuse1);
    Diffuse2 = new Image("/gameboardsquare2.png", 256, 256, true, true, true);
    Shader2.setDiffuseMap(diffuse2);
    Diffuse3 = new Image("/gameboardsquare.png3", 256, 256, true, true, true);
    Shader3.setDiffuseMap(diffuse3);
    diffuse4 = new Image("/gameboardsquare.png4", 256, 256, true, true, true);
    Shader4.setDiffuseMap(diffuse4);
    Diffuse5 = new Image("/gameboardsquare.png5", 256, 256, true, true, true);
    Shader5.setDiffuseMap(diffuse5);
    Diffuse6 = new Image("/gameboardsquare.png6", 256, 256, true, true, true);
    Shader6.setDiffuseMap(diffuse6);
    Diffuse7 = new Image("/gameboardsquare.png7", 256, 256, true, true, true);
    Shader7.setDiffuseMap(diffuse7);
    Diffuse8 = new Image("/gameboardsquare.png8", 256, 256, true, true, true);
    Shader8.setDiffuseMap(diffuse8);
    Diffuse9 = new Image("/gameboardsquare.png9", 256, 256, true, true, true);
    Shader9.setDiffuseMap(diffuse9);
    diffuse10 = new Image("/gameboardsquare.png10", 256, 256, true, true, true);
    Shader10.setDiffuseMap(diffuse10);
    diffuse11 = new Image("/gameboardsquare.png11", 256, 256, true, true, true);
    Shader11.setDiffuseMap(diffuse11);
    diffuse12 = new Image("/gameboardsquare.png12", 256, 256, true, true, true);
    Shader12.setDiffuseMap(diffuse12);
    diffuse13 = new Image("/gameboardsquare.png13", 256, 256, true, true, true);
    Shader13.setDiffuseMap(diffuse13);
    diffuse14 = new Image("/gameboardsquare.png14", 256, 256, true, true, true);
    Shader14.setDiffuseMap(diffuse14);
    diffuse15 = new Image("/gameboardsquare.png15", 256, 256, true, true, true);
    Shader15.setDiffuseMap(diffuse15);
    diffuse16 = new Image("/gameboardsquare.png16", 256, 256, true, true, true);
    Shader16.setDiffuseMap(diffuse16);
    diffuse17 = new Image("/gameboardsquare.png17", 256, 256, true, true, true);
    Shader17.setDiffuseMap(diffuse17);
    diffuse18 = new Image("/gameboardsquare.png18", 256, 256, true, true, true);
    Shader18.setDiffuseMap(diffuse18);
    diffuse19 = new Image("/gameboardsquare.png19", 256, 256, true, true, true);
    Shader19.setDiffuseMap(diffuse19);
    Diffuse20 = new Image("/gameboardsquare.png20", 256, 256, true, true, true);
    Shader20.setDiffuseMap(diffuse20);
}

请注意,我只显示了图 19-18 中一半的语句,因为我的高清显示器无法显示所有语句,但仍然显示(包含)NetBeans IDE UI。我猜是时候升级到 4K UHD 显示器了!

使用一个运行➤项目的工作流程,如图 19-19 所示,来彻底测试我们到目前为止开发的代码。它工作稳定。

A336284_1_En_19_Fig19_HTML.jpg

图 19-19。

As you see in the Output Pane, subsequent random spinner clicks now populate the correct quadrant

我们已经添加了更多的核心游戏功能,这些功能是在本章中控制和随机化您的游戏棋盘方块内容和游戏所需要的,并且我们已经设置了您的 Java 代码,以便在您每次添加一个方块内容图像时,只需增加random.nextInt( bounds )(绑定变量),就可以轻松地添加更多内容。这使得我们的游戏很容易扩展,这对专业的 Java 9 游戏设计很重要。

您还需要添加 if()语句(或者,如果您有两个或三个以上的内容选项可供选择,更有可能的是,改为使用 Java case 语句)来添加代码逻辑,以允许游戏从给定方块的不同图像选项中随机选择。我们将在下一章中增强这些代码,因为我们将继续用 populateQuadrant()方法自己的(非虚拟)内容对其进行改进,并为玩家添加点击游戏方块的功能,并使用我们在第十七章中已经开发的象限纹理贴图将所选内容填充到当前象限中。在这一点上,我们将准备添加与游戏广场内容选择直接相关的游戏,挑战我们的游戏玩家的知识体系,并在这个过程中教育他们。

请注意,我们仍然只有不到 700 行的 Java 代码,包含 17 个方法(平均每个方法 39 行),正如您在 NetBeans 底部的图 19-18 中看到的那样(在我添加最后 20 个 Java 语句之前,类的末尾是第 655 行,所以,基本上我们在第 675 行)。

摘要

在第十九章中,我们学习了如何实现游戏棋盘方块内容的随机选择,同时实现更多的游戏代码,这些代码可以智能地跟踪游戏棋盘的旋转,并使用 Java 数学运算符和简单而强大的编程算法和结构来确定象限在每次旋转中的位置。我们在角度偏移评估的顺序和这些指向的 populateQuadrant()方法中调试了几个问题,并且我们找到了一种将图像加载到内存中的方法,而无需一次在系统内存中声明二十多个游戏板漫反射纹理图像。这种方法允许我们向游戏应用添加数百个内容图像,而不会产生内存不足的错误。

我们构造了几个新的自定义方法,包括 resetTextureMaps()、calculateQuadrantLanding()、populateQuadrantOne()、populateQuadrantTwo()、populateQuadrantThree()和 populateQuadrantFour()。

我们在 createSceneProcessing()方法的鼠标事件处理逻辑中添加了更多的游戏逻辑,这样在每次旋转中,游戏 AI 逻辑将会跟踪游戏启动后的每一次旋转。这将允许我们开发一种算法,计算每次旋转时的着陆角度,这将为游戏逻辑提供每次旋转的当前“着陆象限”的知识,这对我们将开发的所有其他游戏逻辑至关重要。

我们开发了一个优雅的解决方案,仅使用整数(角度中的度数)或整数(整数),通过使用 Java % remainder 运算符将 spinDeg 累加器变量除以 360 度,仅保留 delta(最新的象限角度偏移),该运算符将分子(spinDeg total)除以除数(360 度),并将余数放在等号(=)运算符另一侧的象限变量中。

我们开发了四个 populateQuadrant()方法来保存代码,这些代码为每个象限的五个游戏棋盘方格随机选择内容。这些方法可以随着游戏内容的增加而扩展。

我们还开发了一个 resetTextureMaps()方法,在下一次旋转之前将游戏板重置为默认的空白状态。我们看到了如何“重用”图像实例化,引用不同的纹理贴图。这将请求 Java 9 执行垃圾收集来重新加载图像内容内存位置,而不是必须将游戏内容的每个纹理贴图的图像对象加载到系统内存中,这将导致内存不足错误!

在第二十章中,你将开发额外的游戏代码基础设施,它将处理当玩家点击(选择)游戏方块内容本身时发生的事情,这样你就可以完成鼠标事件处理代码,该代码涉及与 3D 旋转器 UI 和每个游戏棋盘方块相关的点击事件。我们还将在下一章中添加相机动画对象,这样你的相机对象就可以在更靠近你的电路板的地方制作动画了!

二十、编写游戏:设置游戏方法和动画相机视图

到目前为止,你有你的游戏随机象限选择逻辑编码,你正在跟踪每个旋转象限着陆。现在,我们需要将大部分 Java 代码放在适当的位置,用游戏棋盘方格随机内容选择填充这四个象限,这五个方格中的每一个都连接到任何给定的象限。我们还需要创建代码,允许玩家通过单击图像来选择一个主题问题。这将使用图像填充象限,并将相机移动到适当的位置,以便选定的图像更大(更易查看)。这意味着在这一章中,我们还将涉及到Animation对象与Camera对象的结合使用。

在本章中,我们将创建十几个新的 setupQSgameplay()方法,这些方法将包含为每个游戏棋盘方块设置下一级游戏性(关于图像内容的问题)的代码。这样,当玩家单击给定的游戏棋盘方格时,将调用该方法来设置“问答”体验。

这意味着我们将在本章中增加几百行代码;幸运的是,我们将使用一种最佳的“编码一次,然后复制、粘贴和修改”的方法,因此不会像您想象的那样需要大量的输入。一旦我们完成了游戏内容选择和显示基础设施的大部分编码,并测试了每个象限以确保其正常工作,我们就可以执行第二十一章中代码的问答部分,然后编写第二十二章中的评分引擎,以完成大部分“核心”游戏体验。

选择游戏内容:selectQSgameplay()方法

为了允许玩家选择一个游戏棋盘方格来测试他们的知识,我们必须添加 createSceneProcessing()方法,该方法包含我们对鼠标单击 3D 场景图节点对象的事件处理。在本章之前,这是 3D spinner UI 元素,但现在我们必须再添加 20 个事件处理条件 if()处理语句,这样,如果单击 20 个游戏棋盘方格中的一个,就会调用其对应的 selectQxSxgameplay()方法来处理该方格内容的游戏逻辑。我们将从编码和测试第一个 if (picked == Q1S1)结构开始。由于游戏内容的视觉效果(纹理贴图)已经创建,使用第十八章中概述的工作流程,这些方法将正确地显示那些图像素材,并触发玩家需要掌握的游戏性问题以得分。这些 if()语句将查找选取的节点,并将播放器发送到正确的 selectQSgameplay()方法。此结构的伪代码如下所示:

if (pickedNode == Q1S1) { call the selectQ1S1gameplay() method }
if (pickedNode == Q2S2) { call the selectQ2S2gameplay() method } // and so on, out through Q4S5

一旦我们创建了几个这样的语句,我们就可以使用 Alt+Enter 组合键,让 NetBeans 为我们创建一个空的方法结构。一旦我们创建了方法结构,我们就可以使用复制和粘贴来创建 20 个方法,在创建时测试每个象限的代码,直到 20 个都完成为止。

游戏棋盘方格交互:OnMouseClick()事件处理

让我们创建第一个事件处理条件 if()语句,查找 Q1S1 到 Q4S5 方形节点鼠标单击事件。我将把 20 个游戏棋盘方块节点求值放在if(picked != null)外部 if()结构内部(紧接其后)和if(picked == spinner)结构之前,因为这些结构只是在单击一个盒子节点时调用一个方法。值得注意的是,我不能使用 switch-case 结构,因为目前该结构与对象求值不兼容,只与字符串、枚举和数值求值兼容。这应该类似于这里显示的 Java 语句和方法调用(在图 20-1 中用浅蓝色和黄色突出显示):

A336284_1_En_20_Fig1_HTML.jpg

图 20-1。

Add an if(picked==Q1S1) conditional evaluation; use Alt+Enter to create a setupQ1S1gameplay() method

if (picked != null) {
     if (picked == Q1S1) { setupQ1S1gameplay(); }
     ... // 3D spinner UI processing logic will go after all game board square processing logic
}

一旦您键入第一个 if()条件求值,您的方法名称将带有红色下划线,因为该方法尚不存在。使用 Alt+Enter 工作进程让 NetBeans 9 为您编写代码,并在 javafxgame 中选择创建方法“setupQ1S1gameplay()”。JavaFXGame 选项,如图 20-1 中蓝色部分所示。

在 setupQ1S1gameplay()方法中,用三个 if()随机选择来替换 bootstrap 错误代码,用于 square 1 (int pickS1)条件求值。这将告诉你的游戏当三个不同的随机选择数字(0,1 或 2)产生时该做什么。这应该看起来像下面的 Java 代码,如图 20-2 中突出显示的:

A336284_1_En_20_Fig2_HTML.jpg

图 20-2。

Add conditional if() structures for random number generator result processing inside setupQ1S1gameplay

private void setupQ1S1gameplay() {
   if (pickS1 == 0) {}
   if (pickS1 == 1) {}
   if (pickS1 == 2) {}
}

它们下面有红色波浪错误下划线的原因是因为 pickS1 当前在 populateQuadrantOne()方法中被声明为 int (integer ),所以 pickS1 变量当前是局部的,需要被设置为“package protected ”(不使用 public、private 或 protected 关键字),这样整个类都可以访问它。

这将通过将 pickS1 声明移动到类的顶部来实现,这样类(和包)中的所有方法都可以引用从随机数生成器加载的数据值。您可以将 pickS1 添加到 int quadrantLanding 中,并创建一个复合语句,在一行代码中声明要使用的所有整数变量。

当您修改每个 populateQuadrant()方法的 Java 代码时,您可以将 pickS1 到 pickS20 添加到这个复合语句中,或者您可以先添加所有 20 个新的 int 变量,然后再从所有 populateQuadrant()方法中删除 int 声明,此时您的代码结构(如图 20-3 所示)将是无错误的。

A336284_1_En_20_Fig3_HTML.jpg

图 20-3。

Declare int pickS1 at the top of the class so it can be used in the populateQuadrant() and setupQSgameplay() methods

最初,我们在 populateQuadrant()方法中使用该语句来声明和加载 pickSn int (integer)变量,该变量包含来自随机数生成器对象的游戏棋盘方格的当前内容结果。

int pickS1 = random.nextInt(3);  // Next declare int at top of class, so it needs to be removed!

既然我们已经声明了这些具有“全局”(而不是本地)访问权的整数随机数“持有者”,前面的 Java 9 语句将变得更加简单,看起来像下面这样,如图 20-3 所示:

pickS1 = random.nextInt(3);

请注意,附加到每个象限的每个游戏棋盘方格的随机数是在 populateQuadrant()方法中生成的,用于选择和设置所使用的图像素材。我们还将在 setupQ1S1gameplay()方法中使用这个随机数结果来确定显示哪个象限纹理贴图图像,如果玩家单击了那个正方形来选择他们问题的内容。这是因为每个方块有不止一个图像。

由于这个 setupQ1S1gameplay()方法是在 Q1S1 节点对象上单击鼠标的结果,所以您需要做的第一件事是将游戏棋盘象限 1 的默认纹理贴图改为纹理贴图,它与被单击的游戏棋盘方块 1 中显示的内容相匹配。稍后还会添加其他 Java 语句来设置图像内容的问答选项,但是让我们从玩家在单击游戏板内容时将获得的视觉反馈开始。

因为目前有三个不同的内容图像可能填充游戏板 square 1,所以您将有三个 if()构造,它们将包含与每个内容选择相关的 Java 语句。随机数生成器已经在 populateQuadrantOne()方法中随机选择了这三个内容图像中的一个,使用 pickS1 变量存储该选择。因此,从逻辑上讲,我们应该使用这个变量(您现在已经将它作为“全局”游戏变量)来确定将 diffuse21 象限纹理图像对象设置到哪个象限纹理贴图,使用 Image()对象构造函数以及图像资源名称、分辨率和渲染设置。然后,您所要做的就是使用 setDiffuseMap()方法调用设置 Shader21 对象来引用这个(新的)diffuse21 图像对象。这将针对三个内容选项中的每一个进行,gamequad1bird0 到 gamequad1bird2 图像文件名是主要的代码元素,它将在 setupQ1S1gameplay()方法内的三个不同的条件 if()结构之间进行更改。这使得复制粘贴编码工作流程成为可以利用的逻辑流程。

您的 setupQ1S1gameplay()方法的 Java 代码应该类似于下面的代码,如图 20-4 的顶部所示,并复制粘贴到图的底部,以创建 setupQ1S2gameplay()方法:

A336284_1_En_20_Fig4_HTML.jpg

图 20-4。

The setupQ1S1gameplay method is error-free and can be finished and copied and pasted to create setupQ1S2gameplay()

private void setupQ1S1gameplay() {
    if (pickS1 == 0) {
        diffuse21 = new Image("gamequad1bird0.png", 512, 512, true, true, true);
        Shader21.setDiffuseMap(diffuse21);
    }
    if (pickS1 == 1) {
        diffuse21 = new Image("gamequad1bird1.png", 512, 512, true, true, true);
        Shader21.setDiffuseMap(diffuse21);
    }
    if (pickS1 == 2) {
        diffuse21 = new Image("gamequad1bird2.png", 512, 512, true, true, true);
        Shader21.setDiffuseMap(diffuse21);
    }
}

让我们使用“运行➤项目”工作流程,看看当我们单击正方形 1 时,它是否会将正确的图像放在象限的中心。正如你将在图 20-5 中看到的,Java 代码工作了,我们可以创建其他 19 个方法。

A336284_1_En_20_Fig5_HTML.jpg

图 20-5。

Clicking quadrant 1’s square 1 (Q1S1) texture maps the game board quadrant with the correct image asset

现在您已经有了一个 setupQ1S1gameplay()方法代码模板,在它本身下面剪切并粘贴四次,在方法名和 if()代码结构中通过 Q1S5 将 Q1S1 更改为 Q1S2,通过 pickS5 将 pickS1 更改为 pickS2。此外,更改 image()实例中的图像文件名,以匹配您在第十八章中创建的 PNG 纹理贴图图像素材。完成后,如图 20-6 所示的 Java 代码应该如下所示:

A336284_1_En_20_Fig6_HTML.jpg

图 20-6。

Copy and paste the setupQ1S1gameplay() method four times; edit each to create the other four methods

private void setupQ1S2gameplay() {
    if (pickS2 == 0) {diffuse21 = new Image("gamequad1s2bird0.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
    if (pickS2 == 1) {diffuse21 = new Image("gamequad1s2bird1.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
    if (pickS2 == 2) {diffuse21 = new Image("gamequad1s2bird2.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
}
private void setupQ1S3gameplay() {
    if (pickS3 == 0) {diffuse21 = new Image("gamequad1s3bird0.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
    if (pickS3 == 1) {diffuse21 = new Image("gamequad1s3bird1.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
    if (pickS3 == 2) {diffuse21 = new Image("gamequad1s3bird2.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
}
private void setupQ1S4gameplay() {
    if (pickS4 == 0) {diffuse21 = new Image("gamequad1s4bird0.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
    if (pickS4 == 1) {diffuse21 = new Image("gamequad1s4bird1.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
    if (pickS4 == 2) {diffuse21 = new Image("gamequad1s4bird2.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
}
private void setupQ1S5gameplay() {
    if (pickS5 == 0) {diffuse21 = new Image("gamequad1s5bird0.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
    if (pickS5 == 1) {diffuse21 = new Image("gamequad1s5bird1.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
    if (pickS5 == 2) {diffuse21 = new Image("gamequad1s5bird2.png", 512, 512, true, true, true);
                      Shader21.setDiffuseMap(diffuse21); }
}

在测试所有五个附加的象限 1 方块之前,需要确保前五个 OnMouseClicked 事件处理程序条件 if()语句在 createSceneProcessing()方法体中就位。在图 20-1 中选择你的第一个if (picked == Q1S1)条件 if()语句,复制并粘贴它四次。将您的 Q1S1 引用更改为 Q1S2 至 Q1S5,将 setupQ1S1gameplay()更改为 setupQ1S2gameplay()。您的新 OnMouseClicked 事件处理方法体应该看起来像下面的 Java 9 代码,在图 20-7 中也用浅蓝色和黄色显示:

A336284_1_En_20_Fig7_HTML.jpg

图 20-7。

Copy the first if(picked==Q1S1) construct four more times to create the Q1S2 through Q1S5 if() constructs

scene.setOnMouseClicked(event-> {
    Node picked = event.getPickResult().getIntersectedNode();
    if (picked != null) {
         if (picked == Q1S1)    { setupQ1S1gameplay(); }
         if (picked == Q1S2)    { setupQ1S2gameplay(); }
         if (picked == Q1S3)    { setupQ1S3gameplay(); }
         if (picked == Q1S4)    { setupQ1S4gameplay(); }
         if (picked == Q1S5)    { setupQ1S5gameplay(); }
         if (picked == spinner) { resetTextureMaps();
                                  int spin = random.nextInt(4);
                                  if (spin == 0) {
                                  ... // 3D spinner UI logic
                                  }
         }
    }
});

从本章可以看出,我们现在进入了 Java 编码过程的一部分,在接下来的几章中,当我们添加游戏内容时,我们将生成数百行(如果不是数千行)代码。

这将包括这一章,在这一章中,我们添加了基础设施,玩家可以单击图像来选择他们要回答的游戏问题,我们还添加了相机动画来更好地查看这些内容。它还包括下一章,我们将添加问题和每个问题的答案,以及显示这些答案选项的 2D 用户界面。我们还将在第二十一章中使用 JavaFX 9 AudioClip 类添加数字音频旋转和缩放音效。

在第二十二章,我们会添加一个评分引擎,代码也相当多。因此,从现在开始,随着我们继续充实我们的游戏功能,你的 Java 9 代码行将会显著增加。完成后,我们将了解 NetBeans 9 如何让您测试代码的运行情况,然后对其进行优化。让我们使用一个运行➤项目的工作流程,看看当我们单击方块 2 到 5 时,是否会在象限的中心放置一个正确的图像。正如你在图 20-8 中看到的,Java 代码工作了,我们可以创建其他 15 个方法。

A336284_1_En_20_Fig8_HTML.jpg

图 20-8。

Use a Run ➤ Project work process to test; ensure each square populates the quadrant with the correct image

现在我们可以将这些 setupQ1S1gameplay()到 setupQ1S5gameplay()方法结构复制粘贴到它们自身下面,来创建 setupQ2S1gameplay()到 setupQ2S5gameplay()方法结构。在测试代码之前,您还需要添加接下来的五个if(picked == Q2S1)if(picked == Q2S5)事件处理结构(如图 20-7 所示),并确认您的 populateQuadrantTwo()方法引用了正确的图像素材。一旦你编辑了它们,你的新方法应该看起来像下面的代码,如图 20-9 所示:

A336284_1_En_20_Fig9_HTML.jpg

图 20-9。

Create the setupQ2S1gameplay() to setupQ2S5gameplay() methods using the diffuse22 and Shader22 objects

private void setupQ2S1gameplay() {
   if (pickS6 == 0) {diffuse22 = new Image("gamequad2s1vegi0.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
   if (pickS6 == 1) {diffuse22 = new Image("gamequad2s1vegi1.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
   if (pickS6 == 2) {diffuse22 = new Image("gamequad2s1vegi2.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
}
private void setupQ2S2gameplay() {
   if (pickS7 == 0) {diffuse22 = new Image("gamequad2s2vegi0.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
   if (pickS7 == 1) {diffuse22 = new Image("gamequad2s2vegi1.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
   if (pickS7 == 2) {diffuse22 = new Image("gamequad2s2vegi2.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
}
private void setupQ2S3gameplay() {
   if (pickS8 == 0) {diffuse22 = new Image("gamequad2s3vegi0.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
   if (pickS8 == 1) {diffuse22 = new Image("gamequad2s3vegi1.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
   if (pickS8 == 2) {diffuse22 = new Image("gamequad2s3vegi2.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
}

private void setupQ2S4gameplay() {
   if (pickS9 == 0) {diffuse22 = new Image("gamequad2s4vegi0.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
   if (pickS9 == 1) {diffuse22 = new Image("gamequad2s4vegi1.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
   if (pickS9 == 2) {diffuse22 = new Image("gamequad2s4vegi2.png", 512, 512, true, true, true);
                     Shader22.setDiffuseMap(diffuse22); }
}

private void setupQ2S5gameplay() {
   if (pickS10 == 0) {diffuse22 = new Image("gamequad2s5vegi0.png", 512, 512, true, true, true);
                      Shader22.setDiffuseMap(diffuse22); }
   if (pickS10 == 1) {diffuse22 = new Image("gamequad2s5vegi1.png", 512, 512, true, true, true);
                      Shader22.setDiffuseMap(diffuse22); }
   if (pickS10 == 2) {diffuse22 = new Image("gamequad2s5vegi2.png", 512, 512, true, true, true);
                      Shader22.setDiffuseMap(diffuse22); }
}

正如您在图 20-6 和 20-10 中看到的,我已经能够为象限 1 和象限 2 中的每个方块创建所有三个不同的内容选项,这相当于已经创建了 60 个图像素材(5 个方块× 3 个选项× 2 个图形× 2 个象限)。正如你从第十八章中概述的工作流程中所认识到的,这是一大堆数字图像素材。对于象限 3 和象限 4,我还有同样多的工作(另外 60 个数字图像素材)要完成。创建任何专业的 Java 游戏都是大量的工作,这就是为什么充满数字工匠的大型团队几乎总是参与其中。注意,我还选择了 6 到 10 个全局变量。

A336284_1_En_20_Fig10_HTML.jpg

图 20-10。

Confirm the populateQuadrantTwo() method image assets cross-reference to the createQSgameplay() methods

在我写完这一章的时候,我将为这四个象限中的每一个创建至少三个图像素材,这样在我们分别在第 21 和 22 章开始开发问答和评分引擎 Java 代码之前,我们已经完成了 120 个图像素材(编码和测试需要的)。

我们还将开始在这些章节中添加其他很酷的新媒体元素,如数字音频和更多的 2D 用户界面元素,因此我们还有更多令人兴奋的 JavaFX 游戏引擎主题要介绍!

通常,创建新媒体数字素材(数字图像、数字音频、数字插图、数字视频、3D 建模或动画、视觉效果、粒子系统、流体动力学等等)的工作比创建 Java 9 代码要多得多!如果您有多个内容制作工作站,您将有不同的计算机处理(渲染、合成、编码、建模、纹理映射、动画制作等)pro Java 9 游戏开发工作流程中使用的不同新媒体素材。

让我们再次使用运行➤项目的工作流程,并彻底测试您的第二象限的新代码,确保您的所有游戏棋盘方块图像都显示出来,并且当它们被单击时,它们会用正确的(四倍大)图像填充您的象限纹理图。正如您在图 20-11 中看到的,游戏棋盘正方形图像和游戏棋盘象限图像都很清晰,易于识别,可用作游戏内容。

A336284_1_En_20_Fig11_HTML.jpg

图 20-11。

Use a Run ➤ Project work process to test ; make sure each square populates the quadrant with the correct image

让我们复制并粘贴另外五个方法体,并为象限 3 创建 setupQSgameplay()方法。确保您的图像素材名称与您用于 populateQuadrantThree()的名称相匹配。Java 代码应该看起来像下面的方法体,在图 20-12 中也用黄色显示:

A336284_1_En_20_Fig12_HTML.jpg

图 20-12。

Create the setupQ3S1gameplay() through setupQ3S5gameplay() methods, using diffuse23 and Shader23 objects

private void setupQ3S1gameplay() {
   if (pickS11 == 0) {diffuse23 = new Image("gamequad3s1rock0.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
   if (pickS11 == 1) {diffuse23 = new Image("gamequad3s1rock1.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
   if (pickS11 == 2) {diffuse23 = new Image("gamequad3s1rock2.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
}
private void setupQ3S2gameplay() {
   if (pickS12 == 0) {diffuse23 = new Image("gamequad3s2rock0.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
   if (pickS12 == 1) {diffuse23 = new Image("gamequad3s2rock1.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
   if (pickS12 == 2) {diffuse23 = new Image("gamequad3s2rock2.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
}
private void setupQ3S3gameplay() {
   if (pickS13 == 0) {diffuse23 = new Image("gamequad3s3rock0.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
   if (pickS13 == 1) {diffuse23 = new Image("gamequad3s3rock1.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
   if (pickS13 == 2) {diffuse23 = new Image("gamequad3s3rock2.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
}
private void setupQ3S4gameplay() {
   if (pickS14 == 0) {diffuse23 = new Image("gamequad3s4rock0.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
   if (pickS14 == 1) {diffuse23 = new Image("gamequad3s4rock1.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
   if (pickS14 == 2) {diffuse23 = new Image("gamequad3s4rock2.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
}
private void setupQ3S5gameplay() {
   if (pickS15 == 0) {diffuse23 = new Image("gamequad3s5rock0.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
   if (pickS15 == 1) {diffuse23 = new Image("gamequad3s5rock1.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
   if (pickS15 == 2) {diffuse23 = new Image("gamequad3s5rock2.png", 512, 512, true, true, true);
                      Shader23.setDiffuseMap(diffuse23); }
}

确保打开 populateQuadrantThree()方法,并检查所使用的图像资源,以确保它们使用相同的图像名称。例外情况是,您的游戏正方形图像资源是 256 像素正方形,而游戏象限图像资源是 512 像素正方形版本,并且以“gamequad”而不是“game square”开始。

在这两种方法之间,所有 gamesquare 和 gamequad 图像都被加载到 24 个游戏板纹理贴图中,这些贴图用于游戏板着色器,这些着色器将在游戏过程中的任何给定时间装饰游戏板的表面。这二十多种方法(四种用于象限,二十种用于方块)确保您的游戏板在任何给定回合的游戏中都看起来正确,确保游戏板方块具有随机选择的主题内容,并且游戏象限显示内容的大版本。

所有这 24 种方法都是以这样一种方式设置的,即随着时间的推移可以添加内容,将 random.nextInt()方法调用更改为下一个最大的上限,以添加一个级别的内容。一旦游戏设计,包括其他新媒体素材,如更多的动画,数字音频,3D 和游戏问题(所有这些我们仍然需要创建和编码),在接下来的几章中完成,你就可以这样做了。在最初的代码完成后,你将会修改和增加游戏的内容和关卡。你可以重新设计你的游戏结构,就像我们在本书中所做的那样,根据游戏扩展的需要添加更多的类或方法。

populateQuadrantThree()方法,如图 20-13 所示,增加了第三轮图像内容,在文件名末尾用 2 表示。这些素材是我自己在另一台机器上创建的(在您的情况下,可能是由您的图形设计员工创建的),同时我继续在一台四核 Windows 7 机器上处理 Java 9 代码。

A336284_1_En_20_Fig13_HTML.jpg

图 20-13。

Confirm the populateQuadrantThree() method image assets cross-reference to the createQSgameplay() methods

确保将五个 if(picked == Q3S1)到 if(picked == Q3S5)语句添加到 createSceneProcessing()中的 OnMouseClick 事件处理中,以便将新方法与您不断增长的游戏体验联系起来。

如图 20-14 所示,使用运行➤项目工作流程并测试与象限 3 相关的代码,以确保象限和内容图像都正确显示,并且看起来清晰专业。

A336284_1_En_20_Fig14_HTML.jpg

图 20-14。

Use a Run ➤ Project work process to test; make sure each square populates the quadrant with the correct image

最后,让我们创建最后五个 setupQSgameplay()方法,如图 20-15 所示,如下所示:

A336284_1_En_20_Fig15_HTML.jpg

图 20-15。

Create the setupQ4S1gameplay() through setupQ4S5gameplay() methods, using diffuse24 and Shader24 objects

private void setupQ4S1gameplay() {
   if (pickS16 == 0) {diffuse24 = new Image("gamequad4s1fame0.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
   if (pickS16 == 1) {diffuse24 = new Image("gamequad4s1fame1.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
   if (pickS16 == 2) {diffuse24 = new Image("gamequad4s1fame2.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
}
private void setupQ4S2gameplay() {
   if (pickS17 == 0) {diffuse24 = new Image("gamequad4s2fame0.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
   if (pickS17 == 1) {diffuse24 = new Image("gamequad4s2fame1.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
   if (pickS17 == 2) {diffuse24 = new Image("gamequad4s2fame2.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
}
private void setupQ4S3gameplay() {
   if (pickS18 == 0) {diffuse24 = new Image("gamequad4s3fame0.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
   if (pickS18 == 1) {diffuse24 = new Image("gamequad4s3fame1.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
   if (pickS18 == 2) {diffuse24 = new Image("gamequad4s3fame2.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
}
private void setupQ4S4gameplay() {
   if (pickS19 == 0) {diffuse24 = new Image("gamequad4s4fame0.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
   if (pickS19 == 1) {diffuse24 = new Image("gamequad4s4fame1.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
   if (pickS19 == 2) {diffuse24 = new Image("gamequad4s4fame2.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
}

private void setupQ4S5gameplay() {
   if (pickS20 == 0) {diffuse24 = new Image("gamequad4s5fame0.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
   if (pickS20 == 1) {diffuse24 = new Image("gamequad4s5fame1.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
   if (pickS20 == 2) {diffuse24 = new Image("gamequad4s5fame2.png", 512, 512, true, true, true);
                      Shader24.setDiffuseMap(diffuse24); }
}

同样,将您在 setupQ4S1gamedesign()到 setupQ4S5gamedesign()中所做的与您在 populateQuadrantFour()中所做的进行比较。通过比较数字 20-15 和 20-16 ,确保一切同步。

A336284_1_En_20_Fig16_HTML.jpg

图 20-16。

Confirm the populateQuadrantFour() method image assets cross-reference the createQSgameplay() methods

让我们使用图 20-17 所示的运行➤项目工作流程来测试第四象限的新代码。

A336284_1_En_20_Fig17_HTML.jpg

图 20-17。

Test with a Run ➤ Project work process; make sure each square populates the quadrant with the correct image

为了节省一些屏幕截图,我没有显示五个事件处理 if()结构的添加,这五个事件处理 if()结构是您在本章中填充每个新象限和匹配 setupQSgameplay()方法时添加的,每个象限添加了近 50 行 Java 9 代码。

这将导致以下 20 个 Java 编程 if()结构——我们将用调用来填充这些结构以触发摄像机动画、数字音频样本(声音效果)等——被添加到自定义 createSceneProcessing()方法体中的 onMouseClicked()事件处理程序基础结构中。

在图 20-18 中,可以看到这 20 个条件 if()结构被选择为浅蓝色和黄色。请注意,我使用了一个红色方块来突出显示这些新的 setupQSgameplay()方法,这些方法是我们在本章的第一部分“导航器”窗格的“游戏代码方法成员”部分中添加的,它显示在 NetBeans 9 的最左侧窗格中。

A336284_1_En_20_Fig18_HTML.jpg

图 20-18。

You now have all the setupQSgameplay() methods and are calling them in an OnMouseClicked event handler

如果你想看到你所有的图像资源,它们都是纹理贴图,因为这是一个 i3D 游戏,你可以使用你的操作系统文件管理工具并导航到你的/MyDocuments/NetBeansProjects/JavaFXGame/src/文件夹,如图 20-19 所示。我几乎无法将所有这些游戏素材(大约 34MB)放入一张截图中!我可能必须将这 120 个图像素材优化成 PNG8 图像素材,这将把这个数据占用空间减少到大约 10MB。使用游程编码(RLE,也称为 ZIP 文件压缩)可以进一步优化它们。这些图像中的大多数可以很好地转换为 256 色(带抖动)。

A336284_1_En_20_Fig19_HTML.jpg

图 20-19。

Use your file management software to view all of the game image (texture map) assets in the /src folder

在下一章中,我们还将使用一个名为 Audacity 2.1.3 的用于 Windows、Mac 和 Linux 的专业数字音频编辑和增甜包来创建我们的音频素材并学习一些数字音频编码技巧。

接下来,让我们通过将相机缩放到选定的游戏象限来给游戏添加一些“哇”的因素。

相机动画:选择后定位游戏板

接下来让我们添加一些 3D 相机动画,以便在玩家单击他们想要用于其回合的正方形后,相机对象移动并从-30 度转到-60 度,这将使象限及其图像更接近(和更平行)相机。这将使象限图像对玩家来说更大,也将为屏幕左右两侧的 2D 叠加面板提供更多空间。这些将包含我们的用户界面,记分板,和选择的游戏板广场视觉内容的答案。大部分内容将在接下来的几章中创建,所以我们基本上完成了游戏外部部分的 i3D 和 UI 编程。在接下来的章节中,我们将开始游戏编程的内部(问答)和音频部分。

将 rotCameraDown 对象声明添加到类顶部的 RotateTransition 复合语句中;然后在 createAnimationAssets()方法的末尾添加该对象的实例化,使用 5 秒作为持续时间设置,并引用 camera 对象。将 cycleCount 变量设置为 1,将 Rate 设置为 0.75,以获得更适中的移动速率。将延迟设置为 1(持续时间。一个)并使用插值器。目前的线性插值器值。最后,将 fromAngle 变量设置为当前的-30 度,将 toAngle 变量设置为目标的-60 度。这段 Java 代码可以在 createAnimationAssets()方法的末尾看到,在图 20-20 中用黄色突出显示。

A336284_1_En_20_Fig20_HTML.jpg

图 20-20。

Add a rotCameraDown animation at the end of the createAnimationAssets() method from -30 to -60

如图 20-20 所示,调用 RotateTransition 对象的 camera 对象的 Java 9 代码应该如下所示:

rotCameraDown = new RotateTransition(Duration.seconds(5), camera);
rotCameraDown.setAxis(Rotate.X_AXIS);
rotCameraDown.setCycleCount(1);
rotCameraDown.setRate(0.75);
rotCameraDown.setDelay(Duration.ONE);
rotCameraDown.setInterpolator(Interpolator.LINEAR);
rotCameraDown.setFromAngle(-30);
rotCameraDown.setToAngle(-60);

由于我们还希望将 camera 对象移动-175 个单位,从 500 到 325,同时我们将 camera 对象向下旋转-30 度,因此我们将在类顶部的 TranslateTransition 对象复合声明语句中添加一个 moveCameraIn 对象。在 createAnimationAssets()方法的最后,我们将使用 2 秒钟实例化这个对象,并将其附加到 camera 对象。然后,我们将使用 setByZ(-175)将其配置为在 Z 方向移动-175 个单位,cycleCount 设置为 1。该动画对象的 Java 代码应该如下所示:

moveCameraIn = new TranslateTransition(Duration.seconds(2), camera);
moveCameraIn.setByZ(-175);
moveCameraIn.setCycleCount(1);

最后,为了制作一个复合动画,我们将添加一个 cameraAnimIn ParallelTransition 对象声明,在类的顶部制作一个复合声明,然后我们将在 createAnimationAssets()中实例化该对象。

我们将把 moveCameraIn 和 rotCameraDown 动画对象添加到这个 ParallelTransition 对象中,就在对象实例化语句内部,因此我们只需要一行代码就可以将这两个动画无缝地结合在一起。Java 代码也显示在图 20-22 的末尾,应该如下所示:

cameraAnimIn = new ParallelTransition(moveCameraIn, rotCameraDown);

接下来,让我们使用一个运行➤项目的工作过程,并测试这个代码,看看它是如何工作的!正如你在图 20-21 中看到的,象限在屏幕上处于一个很好的位置,所以我们所要做的就是将 3D 旋转器 UI 移出屏幕。为了实现这一点,我们将向 cameraAnimIn ParallelTransition 添加一个 moveSpinnerOff 动画对象,以便将相机旋转到游戏板上的过程也包括将 3D 微调器移出游戏屏幕。

A336284_1_En_20_Fig21_HTML.jpg

图 20-21。

The camera now points down 60 degrees at the game board displaying the content better

这将使动画序列看起来更专业。每当我们需要再次旋转游戏板时,我们可以使用原始的 spinnerAnim ParallelTransition 对象将 3D spinner UI 显示在屏幕上。

接下来让我们创建 moveSpinnerOff 动画对象,然后我们可以将它添加到我们刚刚创建的 cameraAnimIn 对象中,以创建一个更复杂的 ParallelTransition 动画对象,以便在您的游戏代码中使用。

在类顶部的复合 TranslateTransition 声明语句中声明一个 moveSpinnerOff 对象,然后在 createAnimationAssets()方法体中,在 moveCameraIn 语句之后和 cameraAnimIn ParallelTransition 对象实例化之前实例化它,因为我们要将它添加到此复合动画过渡中。通过这种方式,我们想要制作动画的一切都在完全相同的时间发生。

我们将在 2 秒钟内快速移动微调器,移动量与我们将微调器移动到屏幕上的量相同,即 150 个单位(这次是负的)。Java 代码应该如下所示,如方法底部的图 20-22 所示,用黄色和浅蓝色突出显示:

A336284_1_En_20_Fig22_HTML.jpg

图 20-22。

Add a moveSpinnerOff Animation object to a cameraAnimIn ParallelTransition object to remove the spinner

moveCameraIn = new TranslateTransition(Duration.seconds(2), camera);
moveCameraIn.setByZ(-175);
moveCameraIn.setCycleCount(1);

moveSpinnerOff = new TranslateTransition(Duration.seconds(3), spinner);
moveSpinnerOff.setByX(-150);
moveSpinnerOff.setCycleCount(1);

cameraAnimIn = new ParallelTransition(moveCameraIn, rotCameraDown, moveSpinnerOff);

这四个新的动画对象将为您的 i3D 游戏增加相当多的专业性,将您的相机视图动画化到一个非常优越的位置,为每个核心游戏会话从屏幕上移除微调器 i3D UI 元素,旋转 3D 相机的平面,使其与您的象限内容更加平行,并将所有这些移动合并到一个并行转换动画序列中。

这为下一章做好了准备,在下一章中,我们将通过使用 ParallelTransition 对象为游戏面板旋转和相机缩放添加数字音频音效。这将使这两个 3D 动画对象对我们的 i3D 桌面游戏玩家来说更加有趣。

最后,让我们使用一个运行➤项目的工作流程来测试代码。正如你在图 20-23 中看到的,它工作得很好,我们在屏幕的左右两侧有一些不错的区域来覆盖我们的 2D 用户界面区域,我们将在接下来的几章中创建这些区域来完成 i3D 棋盘游戏。

A336284_1_En_20_Fig23_HTML.jpg

图 20-23。

The cameraAnimIn Animation object now works as expected, removing the spinner

在接下来的几章中,我们不仅会继续添加数字音频音效,还会继续添加挑战玩家的问题和答案。我们还将添加一个评分引擎,它将跟踪他们成功识别内容。

摘要

在第二十章中,我们了解了更多关于如何完成游戏棋盘方块内容的随机选择的实现。我们实现了 onMouseEvent 处理代码,当玩家点击游戏棋盘上的方块并选择内容后,该代码会将象限纹理贴图放置到位。我们还实现了相机动画代码,一旦选择了游戏板方块,它就会改变游戏板的视图,以便象限显示更大的图像。这基本上让我们开始编写单个方块(和象限,一旦方块被选中)的游戏性,在这里一个关于内容的视觉问题被回答并评分,我们将在第 21 和 22 章中创建。这些代码的大部分将进入每个方块的 setupQSgameplay()方法中,这是我们在本章中要做的基础。之后,我们将着眼于创建一个评分引擎,数字音频效果,以提高游戏性,也许更多的动画对象。这将使游戏更具 3D 互动性,并增加更多的专业性。

这是你繁重的编码章节之一(下一章也是)。您构造了 20 个自定义方法,setupQ1S1gameplay()到 setupQ4S5gameplay(),并且在 OnMouseClick()事件处理基础结构中放置了指向这些方法的条件 if()结构。您还在所有 populateQuadrant()方法之间交叉检查了图像素材,最后,一起测试了所有代码以确保它正常工作。

我们还在 setupAnimationAssets()方法中添加了一些动画对象,以继续添加一些很酷的“wow”因素,包括一个关键动画,该动画将玩家从“全局”游戏板旋转选择模式带入更“本地”的游戏板内容游戏模式。在本书的后面,当答案和得分完成时,我们当然会反转这个动画,并动画回到更倾斜的视图,这是以最佳方式查看游戏棋盘旋转所需要的。

在第二十一章中,你将开发额外的游戏代码基础设施来处理玩家点击(选择)游戏方块内容时发生的事情。这意味着回到开发更多的 2D 游戏元素,以容纳将弹出的问题和答案内容,并覆盖 3D 游戏板的未使用部分。如您所见,开发一个专业的 Java 9 游戏是一项巨大的编程工作量!

二十一、问答:完成启动方法和数字音频

现在,您的玩家可以为每个棋盘游戏方块单击多个图像来选择要回答的视觉问题,我们可以在他们自己的 UI 中添加这些问题的答案。这将使用第二个 qaLayout StackPane 对象和四个子 Button 对象来完成,这将我们的 SceneGraph 层次结构扩展为四个分支(一个用于 3D,一个用于 2D UI,一个用于 3D UI,一个用于 2D 答案内容)。在下一章中,当我们实现我们的评分引擎和游戏右侧的 2D 评分内容 UI 区域时,我们将为评分添加第五个顶级分支。

在本章中,我们将继续使用所有基于文本的答案内容来填充 20 个setupQSgameplay()方法,这些内容与我们在第二十章中添加的视觉效果(问题)相匹配。我们还将向您的 SceneGraph 添加 qaLayout 分支,它包括一个 StackPane 背景和四个大按钮 UI 元素。玩家将使用这些来选择正确的答案,揭示该方块的视觉代表什么。

这意味着在本章中你将增加几百行代码。幸运的是,您可以使用一种最佳的“编写一次代码,然后复制、粘贴和修改”的方法,因此不需要太多的输入!我只需要向您展示如何向一个 setupQ1S1gameplay()方法添加一组答案,然后您将能够添加其余的视觉问题的答案选项,因此我不需要在本章的代码(文本)和图形中实际添加数百行 Java 9 代码。但是,我必须将它们添加到源代码中,您可以下载源代码。

一旦我们完成游戏“答案选择和显示”基础设施的大部分编码,并测试每个象限以确保其工作,我们就可以在第二十二章创建 Java 代码的得分部分。我们还将研究 JavaFX AudioClip 类,它允许我们添加音频音效。这将使用 JavaFX API(环境)的另一个新媒体组件(数字音频)进一步增强 pro Java 9 游戏体验。

完成游戏:添加 qaLayout 分支

本章第一部分的主要任务是通过在游戏中添加选择答案的用户界面来完成游戏。我们还将为 setupQSgameplay()方法加载每个可视问题的文本(按钮标签)答案。我们将在这一章的前半部分这样做,然后在这一章的后半部分给游戏添加一些音效。我们将从一点自定义方法组织开始,并对我们的方法进行分层,以便有一个用于 3D 节点组件,一个用于游戏启动时看到的 2D UI 节点组件,一个用于选择答案的 2D UI 节点组件(我们将在本章中创建),在下一章中,一个用于 2D UI 节点组件,用于创建评分引擎。在对 Java 代码进行了一些重新配置之后,我们将使用 StackPane 来保存四个大按钮 UI 元素,从而为问答面板创建 UI 基础结构。在我们创建了将这个 UI 放置到位的基本代码之后,我们将“调整”它的设置,以在我们在第二十章中创建的相机放大动画对象中最佳地工作,因为这个动画移动了相机的位置和旋转,这无疑将改变 2D Q&UI 窗格在您的显示器上的视觉呈现方式。

添加另一个组织层:createUInodes()方法

让我们将 createBoardGameNodes()方法分成两个部分,一个用于创建 3D 场景对象(如点光源、平行摄像机、游戏板、3D 旋转 UI 和游戏板象限),另一个用于保存我们在本书前几章中创建的 2D UI 对象。这将在每个方法体中放置 20 到 30 条语句,并在我们创建 createQAnodes()方法来保存节点对象之前更好地组织场景图的每个部分,这些节点对象将创建和配置我们的问答面板,这是我们接下来要做的。选择应该看起来像图 21-1 中用蓝色显示的 34 个 Java 语句。

A336284_1_En_21_Fig1_HTML.jpg

图 21-1。

Select and cut your uiLayout branch Node statements and paste them into a createUInodes() method body

右键单击选择集,如图 21-1 中蓝色部分所示,选择一个剪切选项,从 createBoardGameNodes()方法中删除语句。这将把它们放到你的操作系统剪贴板中,然后粘贴到我们将要创建的新的 createUInodes()方法中。

在 start()方法中,在createBoardGameNodes();之后添加一行代码,并创建一个对 createUInodes()方法的方法调用,该方法尚不存在。使用 Alt+Enter 组合键让 NetBeans 为您创建此方法,创建后如图 21-2 中黄色和浅蓝色所示。

NetBeans 9 将创建这个新的方法和占位符错误代码,我们将选择它(确保只选择错误代码语句,而不是方法体),然后使用粘贴将错误代码语句替换为 34 个 Java 语句,这些语句创建了我们的 uiLayout SceneGraph 节点层次结构,它当前位于操作系统剪贴板中。我还选择了整个方法体(必须在用剪贴板代码替换引导错误代码之后完成),并将其从类的末尾剪切并粘贴到 createBoardGameNodes()方法体之后。这使得 20 个 setupQSgameplay()方法体留在了类的末尾,因为我们将在这些类中添加问题(图片)答案,作为游戏制作工作的一部分,必须在本章中完成。

A336284_1_En_21_Fig2_HTML.jpg

图 21-2。

Create the createUInodes() method call after the createBoardGameNodes() method call and use Alt+Enter

这两个新方法如图 21-3 所示,只是对之前方法的 Java 代码进行了重新配置。在创建 createQAnodes()方法之前,我们只是在这里进行了一点代码组织。

A336284_1_En_21_Fig3_HTML.jpg

图 21-3。

You’ve now reorganized the Node object creation into the createUInodes() and createBoardGameNodes() methods

在 createUInodes()方法后添加一行代码,并键入 createQAnodes()方法名和一个分号,如图 21-4 中突出显示的。使用 Alt+Enter 组合键,让 NetBeans 编写引导方法主体代码。

A336284_1_En_21_Fig4_HTML.jpg

图 21-4。

Add a line of code after the createUInodes() method call, add the createQAnodes() method call, and press Alt+Enter

使用剪切和粘贴将 createQAnodes()方法移动到 createAnimationAssets()方法调用之后,如图 21-5 所示。将 qaLayout StackPane 对象添加到类顶部的声明中,使其成为复合语句。然后在 createQAnodes()方法中实例化 qaLayout StackPane,并使用 setTranslate()方法将其配置在-250 和-425 X,Y 位置。另外,设置一种颜色。白色背景色,并使用 setPrefSize()方法调用为 StackPane 设置一个 400x500 的首选大小,如图 21-5 中突出显示的。

A336284_1_En_21_Fig5_HTML.jpg

图 21-5。

Declare and instantiate a qaLayout object and configure it for location, color, and size in createQAnodes()

一旦完成,如图 21-5 所示的 Java 9 代码应该看起来像下面的 Java 9 语句:

StackPane uiLayout, qaLayout;    // Declaration at the top of the JavaFXGame class
...

qaLayout = new StackPane();      // Statements inside of the createQAnodes() method body
qaLayout.setTranslateX(-250);
qaLayout.setTranslateY(-425);
qaLayout.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY,
                                                         Insets.EMPTY) ) );
qaLayout.setPrefSize(400, 500);

在我们可以渲染 i3D 场景以查看初始问答布局结果(最终将进行微调)之前,我们需要使用 getChildren()将 qaLayout StackPane 添加到 addNodesToSceneGraph()方法中的 SceneGraph 根对象。addAll()方法链。否则,它将不会显示在“运行➤”项目中使用的渲染中。

另请注意,此 qaLayout StackPane 需要放置在第二个位置(新的 SceneGraph 层次结构中现在包含的四个顶级节点分支的第二个位置),以便它位于 gameBoard 3D 游戏板模型的前面,uiLayout 用户界面 StackPane 和 3D spinner 游戏板 spin Sphere 3D UI 元素的后面。

这一增加显示在下面的 Java 9 代码语句中,并在图 21-6 的中间以浅蓝色和黄色突出显示:

A336284_1_En_21_Fig6_HTML.jpg

图 21-6。

Add the qaLayout StackPane object to the root.getChildren.addAll() method call list in the second position

root.getChildren().addAll(gameBoard, qaLayout, uiLayout, spinner);

让我们使用运行➤项目工作流程,看看我们对游戏左侧问答面板的基本 2D StackPane 配置参数的猜测有多准确。正如你将在图 21-7 中看到的,Java 代码正在工作,但是在你的 StackPane 背景颜色中有一些半透明的问题,并且由于在 Z 维度中的(未指定的)位置而与游戏板相交。此外,我们在预放大相机设置中进行了布局,所以一旦我们修复了这个 Z 定位问题,我们可能还需要调整一些或所有的位置和大小设置,这些都是我们最初设置的(在为了我们的 Q & A UI 测试而深入游戏代码之前)。这将允许我们快速生成 Java 代码,并在以后对其进行调整。

A336284_1_En_21_Fig7_HTML.jpg

图 21-7。

Use your Run ➤ Project work process and test your new Java code to see whether it gives you the desired results

使用以下 setTranslateZ() Java 方法调用将 qaLayout StackPane 对象的 Z 位置向屏幕前方移动-75 个单位,在图 21-8 中用浅蓝色和黄色突出显示:

A336284_1_En_21_Fig8_HTML.jpg

图 21-8。

Add a setTranslateZ(-75) method call off the qaLayout object to move it 75 units toward the front screen

qaLayout.setTranslateZ(-75);

同样,使用运行➤项目工作流程,并通过向前移动 z 轴来测试这个新的 Java 代码,看看它是否给出了您想要的结果。正如你在图 21-9 中看到的,StackPane 现在被正确地渲染成一个白色的正方形。

A336284_1_En_21_Fig9_HTML.jpg

图 21-9。

Use your Run ➤ Project work process and test your new Java code to see whether it gives you the desired results

下一个任务是在类的顶部添加四个应答按钮元素 a1Button 到 a4Button 声明,并在 createQAnodes()方法中实例化和配置这些按钮对象。我使用 setMaxSize()将它们调整为 350 个单位宽和 100 个单位高,并使用 setTranslateY()将它们放置在-180、-60、60 和 180 处。出于 UI 设计测试的目的,我使用 setText()方法将它们命名为答案 1 到答案 4。实现这四个按钮 UI 元素所需的 Java 9 代码如图 21-10 和 21-11 所示,如下所示:

A336284_1_En_21_Fig10_HTML.jpg

图 21-10。

Add four 350x100 Button UI objects at Y location -180, -60, 60, 180, labeled Answer One through Answer Four

Button gameButton, ..., a1Button, a2Button, a3Button, a4Button;   // at top of JavaFXGame class

a1Button = new Button();                                // statements in createQAnodes() method
a1Button.setText("Answer One");
a1Button.setMaxSize(350, 100);
a1Button.setTranslateY(-180);

a2Button = new Button();
a2Button.setText("Answer Two");
a2Button.setMaxSize(350, 100);
a2Button.setTranslateY(-60);

a3Button = new Button();
a3Button.setText("Answer Three");
a3Button.setMaxSize(350, 100);
a3Button.setTranslateY(60);

a4Button = new Button();
a4Button.setText("Answer Four");
a4Button.setMaxSize(350, 100);
a4Button.setTranslateY(180);
...                                               // Remember to add Button Nodes to SceneGraph

qaLayout.getChildren().addAll(a1Button, a2Button, a3Button, a4Button); // addNodesToSceneGraph()

请记住,我们必须通过在 Q1 到 Q4 节点对象之后添加 qaLayout 节点并调用 getChildren()来将这些按钮对象添加到场景图层次结构中。addAll()方法链,将四个按钮对象作为子对象添加到 SceneGraph 层次结构中。Java 语句在图 21-11 中突出显示。

A336284_1_En_21_Fig11_HTML.jpg

图 21-11。

Add the four Button UI elements to the SceneGraph , using a qaLayout.getChildren().addAll() method call

同样,使用“运行➤项目”工作流程,通过添加“答案按钮”对象来测试这个新的 Java 代码,看看它是否给出了您想要的结果。正如你在图 21-12 中看到的,StackPane 正在渲染,我们需要处理的只是按钮表面使用的字体系列和字体大小,这样文本就很大,玩家可以阅读。

A336284_1_En_21_Fig12_HTML.jpg

图 21-12。

Use your Run ➤ Project work process and test your first try at your Q&A hierarchy creation and rendering

在每个 setText()方法调用之后添加一个最终的 setFont()方法调用,以设置字体系列,在本例中是一个漂亮的、可读的 Arial 黑色字体,以及字体大小。最初,我们在这个按钮上能装的最大尺寸是 30 个单位,这是相当大的。在 setFont()方法调用中,我们嵌套了一个 Font.font()方法调用,它创建了这个 Font 对象,用 Arial 黑色字体加载它,并将其大小设置为 30。这显示在下面的 Java 代码中,并在图 21-13 中突出显示:

A336284_1_En_21_Fig13_HTML.jpg

图 21-13。

Add the setFont (Font.font(“Arial Black”, 30)) method call to each Button after the setText() method call

a1Button = new Button();
a1Button.setText("Answer One");
a1Button.setFont(Font.font("Arial Black", 30));
a1Button.setMaxSize(350, 100);
a1Button.setTranslateY(-180);
a2Button = new Button();
a2Button.setText("Answer Two");
a2Button.setFont(Font.font("Arial Black", 30));
a2Button.setMaxSize(350, 100);
a2Button.setTranslateY(-60);
a3Button = new Button();
a3Button.setText("Answer Three");
a3Button.setFont(Font.font("Arial Black", 30));
a3Button.setMaxSize(350, 100);
a3Button.setTranslateY(60);
a4Button = new Button();
a4Button.setText("Answer Four");
a4Button.setFont(Font.font("Arial Black", 30));
a4Button.setMaxSize(350, 100);
a4Button.setTranslateY(180);

在我们编写代码以在 JavaFXGame 代码的剩余部分中实际实现这个新的问答 UI 之前,让我们最后一次利用运行➤项目工作流程。我们需要隐藏 StackPane 和子按钮对象,直到需要向玩家显示答案选项。我们还需要在相机动画的结尾显示这个 StackPane,这将使相机倾斜和缩放到游戏板上,改变相机的角度和距离,这可能会改变 StackPane 和 Button 对象的渲染方式,因此需要“调整”大小和位置设置。当我们在 start()方法和 createAnimationAssets()方法中实现完 StackPane 和 Button 对象的问答 UI 设计后,我们可以返回到前面的代码,调整数值以微调它在自顶向下的“游戏板问答视图”中的外观

正如你在图 21-14 中看到的,这些按钮 UI 对象上使用的字体系列和字体大小在可读性方面与图 21-12 有很大的不同。我能看到的唯一问题是面板有点太高了,在边缘和按钮 UI 元素之间有太多的空间,我们将在稍后在我们已经编写的 Java 代码中实现这个 Q & A UI 之后解决这个问题。记住,游戏开发是一个迭代的过程!

A336284_1_En_21_Fig14_HTML.jpg

图 21-14。

Use your Run ➤ Project work process and make sure that the text on the buttons is readable

接下来,让我们休息一下,在当前代码中实现这个 StackPane 和按钮的外观。

在 JavaFXGame 中实现新的问答用户界面

在开始游戏按钮 gameButton.setOnAction()事件处理基础设施中,我们需要做的第一件事是在游戏启动时隐藏问答 UI 面板。之后,一旦相机放大到游戏板象限,我们将需要显示这个问答 UI 面板,这将需要在 createAnimationAssets()方法体的末尾添加一个 setOnFinished()方法调用。要在单击开始游戏按钮时隐藏 qaLayout Q&A panel StackPane,只需复制 gameButton.setOnAction()基础结构内 handle()事件处理程序中的第一个 Java 语句,并将其粘贴到自身下方;然后将 uiLayout 更改为 qaLayout,如下图所示,如图 21-15 中突出显示的:

A336284_1_En_21_Fig15_HTML.jpg

图 21-15。

Hide the Q&A UI panel on the game startup by using qaLayout.setVisible(false) in the start() method

gameButton.setOnAction(new EventHandler<ActionEvent>() {  // Using non-Lambda Expression Format
    @Override
    public void handle(ActionEvent event) {
        uiLayout.setVisible(false);
        qaLayout.setVisible(false);
        camera.setTranslateZ(500);
        camera.setTranslateY(-300);
        camera.setTranslateX(-260);
        camera.setRotationAxis(Rotate.X_AXIS);
        camera.setRotate(-30);
        spinnerAnim.play();
    }
});

第一次隐藏问答 UI 面板(直到需要它时)后要做的下一件事是,一旦玩家选择了他们想要玩的游戏棋盘方块,在 3D 相机旋转并移动到游戏棋盘后立即显示它。这里的理论是,由于新的摄像机焦距(单元重新定位)和摄像机角度(从 30 度旋转到 60 度),新的问答面板视觉特征可能已经改变。换句话说,不同的呈现参数可能已经改变了 StackPane、Button 甚至字体特征中的任何一个(或全部)。

事实上,毫不奇怪,这确实发生了,所以在我们实现了新的 cameraAnimIn.setOnFinished()事件处理程序之后,我们需要返回到 createQAnodes()方法体中,并“调整”Q&A UI 面板参数,使其与“问题答案选择”游戏视图的左下角更紧密地对齐。我们还将收紧回答按钮 UI 元素周围的间距,并增加字体大小!

在 cameraAnimIn ParallelTransition 对象实例化之后,添加您对 cameraAnimIn 对象的 setOnFinished()方法调用,并放置一个 QA layout . set visible(true);事件处理基础结构中的语句,以便在玩家单击他们认为会知道答案的游戏棋盘方格后,摄像机放大到随机选择的游戏棋盘象限后,可以看到您的问答 UI 面板。

这里显示了这个新的 Java 代码结构,在图 21-16 中用蓝色和黄色突出显示:

A336284_1_En_21_Fig16_HTML.jpg

图 21-16。

Add a cameraAnimIn.setOnFinished() method call and add qaLayout.setVisible(true) to the event handler

private void createAnimationAssets() {
    ...
    cameraAnimIn = new ParallelTransition(moveCameraIn, rotCameraDown, moveSpinnerOff);
    cameraAnimIn.setOnFinished(event->{
        qaLayout.setVisible(true);
    });
}

正如你在图 21-17 中看到的,当你使用运行➤项目来测试你刚刚添加的 setOnFinished()代码时,你会看到改变你的摄像机视图和位置已经改变了你的 Q & A 面板布局。

A336284_1_En_21_Fig17_HTML.jpg

图 21-17。

Use Run ➤ Project to see whether the camera has changed the Q&A panel layout

接下来,让我们调整 createQAnodes()方法体 StackPane 和 Button UI 对象配置,以便问答 UI 面板出现在游戏视图的左下角,尽可能远离游戏板方块,并且所有按钮对象仍然相对较大,间隔均匀,并使用尽可能大(和可读)的字体系列和字体大小。

调整问答面板:优化 createQAnodes()设置

让我们开始调整 createQAnodes()方法体中保存的对象配置设置的参数,从 StackPane 开始。我们将使用 setTranslateY()方法调用将它移动 20 个单位,从-405 移动到-385;使用 setPrefSize()方法调用将大小减少 40 个单位,从 400 减少到 360;并将高度增加 154 个单位,从 500 增加到 654,同样使用 setPrefSize()方法调用。我编辑了 setText()方法调用来为按钮 UI 元素添加更长的答案占位符,使用答案选项 1(到 4)而不是答案选项 1(到 4)来更好地用文本填充按钮。通过调用 setFont()方法,我将字体大小增加了 10 %,达到 33 个单位,这样我就可以看到按钮表面上的文本有多大。我使用 setMaxSize()方法调用将按钮高度增加了 40 %,将按钮高度从 100 个单位增加到 140 个单位。这个按钮高度的更改还要求我使用 setTranslateY()方法调用将 StackPane 上按钮间距的 Y 间距从-160、-60、60 和 160 更改为-240、-80、80 和 240。

这里显示了新的(调整后的)Java 9 代码,包括新的 createQAnodes()方法,以及图 21-18 :

A336284_1_En_21_Fig18_HTML.jpg

图 21-18。

Recalibrate the createQAnodes() settings to adjust the Q&A Panel location, size, Button spacing, and font

private void createQAnodes()  {
    qaLayout = new StackPane();
    qaLayout.setTranslateX(-250);
    qaLayout.setTranslateY(-385);
    qaLayout.setBackground(new Background(new BackgroundFill(Color.WHITE,
                                                             CornerRadii.EMPTY,
                                                             Insets.EMPTY) ) );
    qaLayout.setPrefSize(360, 654);

    a1Button = new Button();
    a1Button.setText("Answer Choice 1");
    a1Button.setFont(Font.font("Arial Black", 33));
    a1Button.setMaxSize(350, 140);
    a1Button.setTranslateY(-240);
    a2Button = new Button();
    a2Button.setText("Answer Choice 2");
    a2Button.setFont(Font.font("Arial Black", 33));
    a2Button.setMaxSize(350, 140);
    a2Button.setTranslateY(-80);
    a3Button = new Button();
    a3Button.setText("Answer Choice 3");
    a3Button.setFont(Font.font("Arial Black", 33));
    a3Button.setMaxSize(350, 140);
    a3Button.setTranslateY(80);
    a4Button = new Button();
    a4Button.setText("Answer Choice 4");
    a4Button.setFont(Font.font("Arial Black", 33));
    a4Button.setMaxSize(350, 140);
    a4Button.setTranslateY(240);
}

正如你在图 21-19 中看到的,Q & A UI 面板现在在你的游戏面板视图的左下角。按钮很大,并与漂亮的、大的、可读的文本紧密相连,问答用户界面不妨碍你的每个数字图像素材(游戏的视觉组件),现在看起来相当专业。

A336284_1_En_21_Fig19_HTML.jpg

图 21-19。

Use a Run ➤ Project work process to see if the Q&A panel layout has been restored to its large readable state

向 setupQSgameplay()方法添加应答按钮内容

现在,我们可以通过在每个 setupQSgameplay()方法体内的每个 if(pickSn == n)条件 if()求值语句中添加四个相对简单的 Java 语句,来简单地为每个游戏棋盘添加问答 UI 面板答案。为了用真实的答案数据测试我们的用户界面,我们需要做的就是添加第一个 setup Q1 S1 gameplay()if(picks 1 = = 0)部分,添加对 diffuse 和 Shader21 对象配置的四个 setText()方法调用,这些对象配置已经在该代码部分中就位,以控制您的图像。

Java 代码也显示在图 21-20 的末尾,应该如下所示:

A336284_1_En_21_Fig20_HTML.jpg

图 21-20。

Add the four a1Button through a4Button object answers (one is correct) using the setText() method call

private void setupQ1S1gameplay() {
    if (pickS1 == 0) {
        diffuse21 = new Image("gamequad1bird0.png", 512, 512, true, true, true);
        Shader21.setDiffuseMap(diffuse21);
        a1Button.setText("Falcon Hawk");
        a2Button.setText("Seagull");
        a3Button.setText("Peacock");
        a4Button.setText("Flamingo");
    }
}

使用您的“运行➤项目”工作流程来测试添加真实答案按钮对象的代码,如图 21-21 所示。

A336284_1_En_21_Fig21_HTML.jpg

图 21-21。

Use a Run ➤ Project work process to see how answer Button data looks when you test square 1

为了获得一些实践,现在在 setupQSgameplay()方法中创建另外 59 组问题答案。接下来,让我们放置一个 AudioClip 对象(类),这样我们就可以将声音效果附加到我们的游戏棋盘旋转动画中。

游戏的数字音频:使用 AudioClip 类

在本章中,我们也来看看如何将数字音频素材添加到您的游戏中。这将需要使用 javafx.media 模块,这将使您的发行版更大,因为该模块需要添加到您的发行版 JAR 中,并且包括 MediaPlayer(用于音频和视频)和 AudioClip 类,等等。AudioClip 类用于较短的音频“片段”,技术上称为样本。如果您想要播放较长格式的数字音频(比如歌曲)或数字视频,您将需要使用 MediaPlayer 类。游戏通常使用较短格式的音频,因此我们将在这里介绍 AudioClip 类;它本质上是一个数字音频音序器,这是一个非常强大的工具,无论是对于游戏开发者,还是对于声音设计师和歌曲作者。

公共的 final AudioClip 类扩展了 java.lang.Object,这意味着它被临时编码为数字音频音序器。它保存在 javafx.media 模块的 javafx.scene.media 包中,因此,该类的 Java 类层次结构如下所示:

java.lang.Object
  > javafx.scene.media.AudioClip

AudioClip 对象可用于包含以最小延迟播放的数字音频短片段。与媒体对象类似,从网络或 JAR 加载剪辑,但行为不同。例如,由 MediaPlayer 对象播放的媒体对象不能“播放”它们自己,而您的 AudioClip 对象可以。AudioClips 可以立即重用,因此它们的延迟为零,使用的内存更少,这对游戏来说很重要。

AudioClip 对象的回放行为被 Oracle 称为“一劳永逸”一旦调用了该类的 play()方法之一,唯一可操作的控件就是 stop()方法。我们将使用这两种方法。

一个 AudioClip 对象也可以同时播放多次!要使用 MediaPlayer 中的媒体对象完成相同的任务,必须为并行播放的每个声音创建新的 MediaPlayer 对象。这对于游戏场景来说不是最佳的,这就是为什么我们在这里不讨论媒体和 MediaPlayer 对象。

媒体对象和 MediaPlayer 更适合长格式的音频,如歌曲或有声读物。这主要是因为音频剪辑(在内存中)存储整个数字音频素材的原始、未压缩(PCM)音频数据,对于长音频剪辑来说,这通常相当大。媒体播放器在存储器中只有足够的“预滚动”的解压缩音频数据来播放一小段时间;因此,对于较长的剪辑,MediaPlayer 的内存效率要高得多,尤其是在它们已经过压缩的情况下,例如,使用 MP3(数字音频)或 MPEG4(数字视频)文件格式或 OGG Vorbis(数字音频)、FLAC(数字音频)或 WebM (ON2 VP6、VP8 或 VP9)数字视频格式。

AudioClip 类有六个数字音频属性,它们影响声音平衡、循环、位置、优先级、速率和音量。这些属性包括 balance DoubleProperty 和 pan DoubleProperty,前者控制 AudioClip 对象的(相对)左右音量,后者控制 audioClip 对象的相对“中心”位置。rate DoubleProperty 控制播放音频剪辑的相对速率(速度), volume DoubleProperty 控制播放音频剪辑的相对音量。调用 play()方法时,cycleCount IntegerProperty 控制音频剪辑的播放次数。priority IntegerProperty 控制 AudioClip 对象相对于其他 AudioClip 对象的相对优先级。

有一个静态 int 不定数据字段,当 cycleCount 设置为该值时,AudioClip 将连续循环,直到使用 stop()方法调用停止为止,我们很快就会了解到这一点。

只有一个 AudioClip 构造函数方法,它采用源 URL 并使用以下格式:

AudioClip(String sourceURL)

接下来,让我们看看 AudioClip 类允许我们调用 AudioClip 对象的方法。DoubleProperty balanceProperty()方法调用允许您获取 AudioClip 对象的相对左右音量。integer property cycleCountProperty()方法调用允许您在调用 play()方法时获取将播放 AudioClip 对象的次数。DoubleProperty panProperty()方法调用允许您获取 AudioClip 对象的(相对)中心。integer property priority property()方法调用允许您获取该音频剪辑对象相对于其他音频剪辑对象的(相对)优先级设置。DoubleProperty rateProperty()方法调用允许您获得正在播放的音频剪辑的(相对)速率。DoubleProperty volumeProperty()方法调用允许您获取播放音频剪辑时的(相对)音量。

除了六个 AudioClip 属性方法之外,还有七个 get()方法允许您获取 AudioClip 属性的值,包括其数字音频源文件。double getBalance()方法调用将用于获取 AudioClip 的默认平衡级别。int getCycleCount()方法调用将用于获取 AudioClip 对象的默认循环计数。double getPan()方法调用将用于获取 AudioClip 对象的默认声相值。int getPriority()方法调用将用于获取 AudioClip 对象的默认播放优先级值。double getRate()方法调用将用于获取 AudioClip 对象的默认回放速率。String getSource()方法调用将用于获取用于创建 AudioClip 对象的源 URL。double getVolume()方法调用将用于获取 AudioClip 对象的默认音量。

还有七个 set()方法允许您设置 AudioClip 属性的值,包括数字音频源文件。void setBalance(double balance)方法调用应用于设置 AudioClip 对象的默认平衡级别。void setCycleCount(int count)方法调用应用于设置 AudioClip 对象的默认循环计数。void setPan(双声相)方法调用应用于设置 AudioClip 对象的默认声相值。

void setPriority(int priority)方法调用应该用于设置默认播放优先级。void setRate(double rate)方法调用应该用于设置默认回放速率。应使用 void setVolume(double value)方法调用来设置默认音量级别。还有五种方法,可用于在播放时控制 AudioClip 对象。

布尔 isPlaying()方法调用将用于指示音频剪辑当前是否正在播放。void play()方法调用应该用于使用默认参数播放 AudioClip 对象。应该使用 void play(double volume)方法调用,使用除音量之外的所有默认参数来播放音频剪辑。应使用 void play(双音量、双平衡、双速率、双声相、int priority)方法调用,使用给定的音量、平衡、速率、声相和优先级参数来播放音频剪辑。最后,应该使用 void stop()方法调用来立即停止 AudioClip 对象的所有回放。现在,我们可以在游戏中将数字音频素材实现为 AudioClip 对象,为游戏板旋转和相机缩放等操作提供音频音效。

实现音频剪辑:添加数字音频素材声音效果

我们需要做的第一件事是在 JavaFXGame 类的顶部声明两个名为 spinnerAudio 和 cameraAudio 的 AudioClip 对象,如下面的 Java 9 代码所示,并在图 21-22 中突出显示:

A336284_1_En_21_Fig22_HTML.jpg

图 21-22。

Declare spinnerAudio and cameraAudio AudioClip at the top of the class; use Alt+Enter to add an import

AudioClip spinnerAudio, cameraAudio;

接下来,在 loadImageAssets()方法调用的正下方创建一个 loadAudioAssets()方法调用,并再次使用 Alt+Enter 键,如图 21-23 所示,让 NetBeans 为您创建空的引导方法体。

A336284_1_En_21_Fig23_HTML.jpg

图 21-23。

Create a loadAudioAssets() method call after loadImageAssets(); use Alt+Enter to have NetBeans code it

在类方法结构中向上移动这个新方法体,使其靠近 loadImageAssets()方法,并开始添加 spinner audio = new audio clip();实例化语句,如图 21-24 正在构建中所示。当我们构造语句的内部(String sourceURL)部分时,这个实例化将变得更加复杂。这看起来像下面的代码,它正在 NetBeans 中构建,并在图 21-24 中突出显示:

A336284_1_En_21_Fig24_HTML.jpg

图 21-24。

Add your internal getResource(String) portion of the AudioClip instantiation statement for spinnerAudio

spinnerAudio = new AudioClip( JavaFXGame.class.getResource(String sourceURL) );

让我们看几页纸,找到一个专业级 CD 和 HD 数字音频样本网站,其中有免费的商业用途 WAVE 文件(未压缩的 PCM 数字音频,44.1 KHz,16 位和 24 位分辨率)。

寻找免费的商业用数字音频:99Sounds.org

在我们引用 spinnerAudio 数字音频素材的内部文件名之前,我们需要创建我们接下来将使用的数字音频素材,所以让我们先这样做。幸运的是,我发现了一个名为 99Sounds 的数字音频样本库网站,它拥有数千兆字节的电影质量音频样本,可以免费用于商业项目,只要它们不在另一个库中作为数字音频样本重新分发。它们使用标准的 44.1 Hz、CD 质量和音频采样速率,分辨率为 16 位或 24 位,使用未压缩的 PCM (WAVE)格式。如果你想了解更多关于数字音频编辑软件和工作流程的内容,请查阅 Apress.com 的《数字音频编辑基础》。我从 www.99SOUNDS.org 下载了所有的样本库,因为我为客户和我自己的公司制作了很多游戏、网站、电子书、iTV 节目、智能手表和类似的数字产品。图 21-25 显示了我下载的文件浏览器应用列表和几十个文件夹(现在在我的硬盘上的一个C:\Audio文件夹下)。

A336284_1_En_21_Fig25_HTML.jpg

图 21-25。

Go to www.99sounds.org and download free digital audio samples for all your pro Java 9 game projects

我将使用 Massamolla 系列中的第 24 个样本,该样本位于节奏序列文件夹中,如图 21-25 所示。这个例子叫做螺丝刀槽,是 32 位 44.1 Hz 波形格式;它使用 1411 Kbps 的压缩率,长度为 18 秒,其中我们将循环 7 秒以减少内存数据占用。我们还将把它转换成一个单声道样本以节省内存,并将创建这个文件的几个版本。

数据足迹优化:使用 Audacity 创建游戏音频

请注意图 21-25 中的文件大小为 3159 千字节,这对于旋转音频来说是太大的内存了!我们将为低质量的音频组件削减几乎 3MB 的文件大小,并创建一个略大于半兆字节大小的高质量音频素材,因此这应该是所有游戏开发者感兴趣的部分!启动 Audacity 2.1.3(或更高版本),我假设您已经下载并安装了它。使用文件➤打开菜单序列打开图 21-25 所示的螺丝刀槽样本;找到其重复声音中的第一个大间隙,如图 21-26 用绿线所示,大约在这个文件的 7.6 秒。

A336284_1_En_21_Fig26_HTML.jpg

图 21-26。

Open Audacity 2 and find a point at 7.6 seconds, which will loop seamlessly for the gameboard spin audio

选择两个立体声音轨中超过 7.6 秒的音频样本部分,如图 21-27 所示;使用“编辑➤删除”菜单序列从样本中移除此音频数据。

A336284_1_En_21_Fig27_HTML.jpg

图 21-27。

Select the portion between 7.6 and 18.3 seconds and use your Edit ➤ Delete menu sequence to remove it

这会立即删除大约五分之三(18.3 的 7.6 大约是五分之二)的数字音频数据。我们要做的下一步是将两个立体声音轨合并成一个单声道音轨,再次将数据量减少 100%。选择两个立体声音频轨道,如图 21-28 所示,并使用轨道➤立体声轨道至单声道菜单序列将这两个(立体声)数字音频轨道合并为一个单声道数字音频轨道。

A336284_1_En_21_Fig28_HTML.jpg

图 21-28。

Select the entire sample on both tracks and use Tracks ➤ Stereo Track to Mono to combine the sample

接下来,我们需要将样本分辨率减半,将 32 位(浮点)音频样本降低到 16 位 PCM 分辨率,通常称为“CD 质量”音频。这可以使用波形音频最左侧单声道 44100Hz 32 位浮点指示器顶部的下拉箭头来完成,如图 21-29 所示。

A336284_1_En_21_Fig29_HTML.jpg

图 21-29。

A seven-second Mono 44.1Hz 32-bit float sample is now more than 400 percent less data than the original sample

点击此下拉箭头,进入主菜单底部的格式子菜单,如图 21-30 左下方所示。这将显示所选的 32 位浮点格式,并允许您选择 16 位 PCM (CD)或 24 位 PCM (HD)音频分辨率格式。由于我们试图为游戏音频素材节省系统内存,我们将选择 16 位、44.1 KHz 的格式。

A336284_1_En_21_Fig30_HTML.jpg

图 21-30。

Reduce the sample format another 100 percent from 32-bit to 16-bit resolution using the sample drop-down arrow

现在,我们将通过将采样速率从 44.1 KHz 降至 22.05 Hz(正好一半)来创建中低质量的数字音频素材。将数据减少 100 %(一半)或 200 %(四分之一)会得到完美的结果,因为没有余数(偶数除法)。为此,使用轨道编辑面板底部的项目速率(Hz)下拉选择器,并选择 22050 而不是 44100,如图 21-31 左侧红色圆圈所示。还可以看到分辨率已经降低到了 16 位。使用 Audacity 用户界面左上角的播放按钮(也用红色圈出)预览数字音频,看看您是否能听到音频质量的任何差异。作为效果听起来还是很棒的。

A336284_1_En_21_Fig31_HTML.jpg

图 21-31。

Reduce the sample format by another 100 percent, from 44.1 to 22.05 KHz using the Project Rate drop-down menu

最后,让我们通过将其采样速率从 44.1 KHz 降低到 11.025 KHz,将该音效样本再降低 100%(始终从最高可能采样速率采样到目标采样速率,以便为算法提供最高质量的数据来施展魔法)。正如您在图 21-32 中看到的,我们大胆使用了 44.1 KHz、16 位音频样本数据(见左中的设置),并将项目速率(Hz)下拉菜单设置为 11.025 KHz。您可以使用 Play 按钮再次预览音频质量,如果您这样做了,您将会看到质量水平已经下降,但质量水平仍然非常适合重复的游戏棋盘旋转动画音频反馈声音效果。

A336284_1_En_21_Fig32_HTML.jpg

图 21-32。

Reduce the sample format by another 100 percent from 44.1 to 11.025 KHz, using the Project Rate drop-down menu

在图 21-33 中,我将所有三个 Audacity 文件➤另存为对话框合并成一个以节省空间;在这一章中,我们有很多东西需要了解,包括 NetBeans 和 Audacity。第一个面板编号为 1,显示您的 44.1 KHz 16 位文件被保存为 spinner。在NetBeansProjects/JavaFXGame/src/文件夹中。图的第二部分显示了保存为 spinner22.wav 的 22.05 KHz 16 位版本,图的第三部分显示了保存为 spinner11.wav 的 11.025 KHz 16 位版本。这三种音频资源的文件大小约为 658KB、329KB 和 165KB。因为这些是 16 位 PCM。wav 文件,用来存储该文件的内存量恰好也是用来部署游戏中使用的文件的系统内存量。

A336284_1_En_21_Fig33_HTML.jpg

图 21-33。

Use Audacity’s File ➤ Save function to export 44, 22, and 11 Hz, 16-bit audio versions to /JavaFXGame/src

现在我们可以继续 AudioClip 实例化语句,并在我们的游戏逻辑中使用新的音频样本!

使用 toExternalForm()将 URI 引用作为字符串对象加载

现在我们可以添加这个微调器。然后将该方法调用链接到 toExternalForm()方法调用,后者将 spinner.wav 音频资源转换为 AudioClip 构造函数方法所需的外部(URI 字符串)形式。确保将根(/)正斜杠添加到 spinner.wav 中,以便可以在根源文件(/src)文件夹中看到它。该语句的 Java 代码如图 21-34 所示:

A336284_1_En_21_Fig34_HTML.jpg

图 21-34。

Go back to the instantiation and add the spinner.wav audio file and the toExternalForm() method chain

spinnerAudio = new AudioClip( JavaFXGame.class.getResource("\spinner.wav").toExternalForm() );

由于游戏板旋转超过 7 秒,您还需要添加一个 setCycleCount()方法调用,并使用以下 Java 9 代码将其设置为不定(无限循环)数据值,如图 21-35 所示:

A336284_1_En_21_Fig35_HTML.jpg

图 21-35。

Add a slash (/), or root, to spinner.wav. Then add a spinnerAudio.setCycleCount(AudioClip.INDEFINITE) method call

spinnerAudio.setCycleCount(AudioClip.INDEFINITE);

既然已经设置了 spin AudioClip 素材,我们现在必须在用鼠标单击 spinner UI 时触发它。

在 createSceneProcessing()中触发微调器音频播放

要播放 AudioClip 对象,我们需要将一个spinnerAudio.play();方法调用插入到您的if (picked == spinner)结构的事件处理中,就在 calculateQuadrantLanding()方法调用之前。

在图 21-36 的底部突出显示了用于添加的 Java 9 代码。

A336284_1_En_21_Fig36_HTML.jpg

图 21-36。

Trigger a spinnerAudio.play() in the createSceneProcessing() method in an if(picked == spinner) structure

要停止 spinnerAudio AudioClip 对象的播放,您需要在 setOnFinished()事件处理代码结构中调用 spinnerAudio AudioClip 的 stop()方法,该方法在 createAnimationAssets()方法体中的 rotGameBoard 动画对象中调用。

这样,当动画对象完成动画制作时,就会调用您的 spinnerAudio.stop()方法,当游戏板停止旋转时,游戏板旋转音频也会停止。

我使用以下代码将此代码放在事件处理结构的最末端,在图 21-37 的末端用浅蓝色和黄色突出显示:

A336284_1_En_21_Fig37_HTML.jpg

图 21-37。

Stop the spinnerAudio object in the createAnimationAssets() method in a rotGameBoard.setOnFinished() event handler

rotGameBoard.setOnFinished(event-> {
    if (quadrantLanding == 315) { populateQuadrantOne();   }
    if (quadrantLanding == 225) { populateQuadrantTwo();   }
    if (quadrantLanding == 135) { populateQuadrantThree(); }
    if (quadrantLanding == 45)  { populateQuadrantFour();  }
    spinnerAudio.stop();
});

接下来,让我们创建我们的相机动画对象音频,这一次匹配音频长度到动画长度。

相机动画音频:匹配音频长度到动画

对于 camera 对象动画 AudioClip,我们将五秒钟的动画与五秒钟的音频相匹配,这样我们就不需要循环播放音频,因此也不需要使用 stop()方法调用,因为 AudioClip 将在五到六秒的长度后停止播放。重要的是,你要看到这两种游戏设计的主要数字音频方法:循环音频根据需要开始和停止,以及定时音频。正如你在图 21-38 中所看到的,我从 Project Pegasus 琶音系列中选择了有节奏的冰川样本,并对其进行了微调,使其长度约为 5.5 秒。正如您所看到的,这个样本是 48000 Hz,所以我还创建了 16000 Hz (1/3)和 8000 Hz (1/6)的中低质量 16 位版本,它们分别是 526KB、176KB 和 88KB。这使得 CD 音质的声音约为 1MB,高质量的声音约为半兆字节。

A336284_1_En_21_Fig38_HTML.jpg

图 21-38。

Match almost six seconds of Rhythmic Glacier audio with almost six seconds of camera zoom animation

现在,我们可以用这个 camera.wav 资源加载第二个 cameraAudio AudioClip 对象,并在我们的代码中使用它。

由于您已经在类的顶部声明了 cameraAudio AudioClip,下一步将是在 spinnerAudio AudioClip 对象及其实例化和配置语句之后,在 loadAudioAssets()方法中实例化它。完成此操作后,我们可以再次将 play()触发器添加到 createSceneProcessing()代码中。

您不需要向 createAnimationAssets()on finished()事件处理程序添加 stop()方法调用,因为声音只播放一次,并且大约在动画对象完成移动和旋转照相机对象的同时过期。如果您希望更紧密地同步这些内容,请使用我们对微调器音频资源使用的相同方法,循环一个较短的音频剪辑,然后在 setOnFinished()事件处理程序内调用 stop()方法。

第二个实例化的 Java 代码与第一个相同(除了音频素材的文件名),如下所示,在图 21-39 中间用浅蓝色和黄色突出显示:

A336284_1_En_21_Fig39_HTML.jpg

图 21-39。

Add a cameraAudio AudioClip to the loadAudioAssets() method and reference the new camera.wav asset

cameraAudio = new AudioClip( JavaFXGame.class.getResource("/camera.wav").toExternalForm() );

如果您想要添加更多的数字音频声音效果,您可以简单地模仿这些 AudioClip 对象中的一个或另一个,例如,在 i3D spinner UI 元素出现在屏幕上时向其添加音频,添加与 Q&A 会话有关的音频,或者甚至添加当开始游戏按钮等待玩家单击时循环播放的音频。因此,随着您继续开发和改进这个 pro Java 9 游戏设计,您可能会扩展这个 loadAudioAssets()方法。

当玩家单击游戏棋盘方块以选择问答会话中使用的内容时,相机动画和音频在 createSceneProcessing()方法的不同部分被触发。因此,play()方法不是在if(picked == spinner)中被调用,而是在if(picked == Q1S1)或其他 19 个游戏棋盘方格条件 if()语句之一中被调用。如图 21-40 所示的 Java 代码应该如下所示:

A336284_1_En_21_Fig40_HTML.jpg

图 21-40。

To trigger a cameraAudio AudioClip , add a cameraAudio.play() method call to the OnMouseClicked event handler

if (picked == Q1S1) {
    setupQ1S1gameplay();
    cameraAnimIn.play();
    cameraAudio.play();

}

为了练习本章所讲的内容,创建另外 19 个 setupQSgameplay()方法调用,包括每个主题的问题,以及触发摄像机音频放大的 cameraAudio.play()调用。

摘要

在第二十一章中,我们学习了如何为每个游戏棋盘方格创建答案选项。我们还为玩家创建了 StackPane 和 Button 对象来记录他们的答案。您需要创建另一个 Java 代码来回答每个游戏棋盘上的随机选项,并输入此代码来完成游戏,这样您就可以进入下一章,在下一章中,我们将创建计分引擎、记分牌和跟踪游戏这一部分的高分代码。

我们还学习了如何使用 JavaFX AudioClip sequencer 将数字音频素材添加到游戏中,它拥有合成器拥有的所有核心音乐合成、声音跟踪和触发工具。我们创建了一个定时的数字音频剪辑和一个循环(播放到停止)版本的数字音频,以便您了解如何添加数字音频素材,以便在玩家的游戏体验中为他们提供听觉反馈。

在第二十二章中,你将开发一个评分引擎代码基础设施,它将处理玩家选择正确的游戏方块内容答案时发生的事情。这意味着我们将再次开发 2D 游戏元素来保存记分牌内容,这将弹出并覆盖更多的 3D 游戏用户界面的未使用部分。在这种情况下,这将是游戏屏幕的右下角部分。

二十二、评分引擎:创建评分 UI 布局并对内容评分

现在,您已经编写了游戏板方块答案选择逻辑,并且编写了游戏板旋转和摄像机动画序列的声音效果,我们需要将另一半 Java 代码放入适当的位置,查看用户选择(点击)了什么答案,并相应地更新记分板。我们将跟踪正确和错误的答案,并使用简单而有效的评分界面实时鼓励玩家。本章中的工作过程需要我们在屏幕右侧创建一个乐谱 UI 面板(panel ),我们将使用名为scoreLayout的 StackPane 和名称也以 Score 开头的文本对象来创建它。

在这一章中,我们将实现一个单人游戏和评分模型来让你的评分用户界面就位,因为很多游戏玩家都想根据游戏内容来玩游戏,作为一种学习体验。也就是说,仍然需要为每个按钮 UI 元素编写大量代码来查看答案是否是正确答案;如果是,代码将增加“正确:”的分数,如果不是,将增加“错误:”的分数。

这意味着在你学会如何实现本章中的评分逻辑后,你仍然需要增加几百行 Java 代码。这将为你所有的答案打分,这些答案是你在前一章中学会的。

幸运的是,我们将使用最佳的“编写一次代码,然后复制、粘贴和修改”方法,因此不应该像上一章那样涉及太多的输入。真正的工作将是在你学习完如何实现评分(在当前章节中)后,创建答案(第章第二十一章)和评分逻辑(本章)。

还有一个上一章的小错误,我们将通过移动。setVisible(false)从开始游戏按钮到 JavaFX application start()方法启动序列调用问答 UI 面板,这将在游戏启动时(而不是在单击按钮时)隐藏问答 UI 面板(以及后来的乐谱 UI 面板)。

闪屏渲染错误:启动时隐藏用户界面面板

您可能已经注意到,在前一章的游戏运行➤项目测试渲染中,JavaFX 错误地渲染了游戏闪屏上方的部分问答 UI 面板,如图 22-1 的左上角所示。这个 Q&UI 面板应该真正位于你的闪屏之后,因为你已经在 root.addChildren.()addAll()方法链的节点对象参数列表序列中的 addNodesToSceneGraph()方法中指定了呈现顺序。通过添加 i3D 元素来使您的场景成为 3D(或“混合”2D+3D 场景)实体,这也可能是 Z 单位位置(方位)设置问题。因此,有两种方法可以调查并修复这个小的渲染问题。由于我们已经设置了 X、Y、Z 显示单元,并且可以有效地实现我们想要在 i3D 游戏渲染管道中实现的功能,解决这个问题最简单的方法就是在游戏启动时自动隐藏 UI 面板(因为我们在游戏启动时隐藏了它),而不是手动使用开始游戏按钮 UI 元素。这是在 JavaFX 所要求的 start()方法的顶部完成的,而不是在一个事件处理结构中完成的,该事件处理结构与开始游戏按钮 UI 元素上的初始单击相连接。

A336284_1_En_22_Fig1_HTML.jpg

图 22-1。

Let’s fix the Q&A Button pane rendering bug that affects the startup screen first before developing the scoring

首先,从 gameButton 事件处理代码中移除qaLayout.setVisible(false); Java 语句,并将其放在。start()方法,以便自动处理该隐藏。

请记住,您的 qaLayout StackPane 将在 createQAnodes()方法中创建,因此该语句需要出现在您的createQAnodes();自定义方法调用之后,也就是在该自定义方法之前调用的任何方法之后。这很好,因为这些只是设置将在游戏中使用的资源引用和对象。

这最终成为解决这个视觉缺陷的一个更快更容易的方法;因为我们已经准备在游戏启动时隐藏这个面板,所以在更早的时候(自动地)而不是在事件处理逻辑中隐藏(设置可见性为假)这个面板既创建了更干净的代码,又节省了我们弄清楚为什么会发生这种情况(显然是 3D 空间中的 Z 单位设置问题)以及如何为 qaLayout StackPane 对象添加(和调整)Z 位置代码的时间,这不会弄乱您当前的原始结果(除了在这个初始闪屏显示上)。

这个简单修改的 Java 代码在图 22-2 的中间突出显示,应该类似于下面的 Java 9 语句,现在可以在您的 public void start() core JavaFX 9 方法的第一部分找到:

A336284_1_En_22_Fig2_HTML.jpg

图 22-2。

Remove the .setVisible() call from your gameButton handler and place it in .start() after createQAnodes()

public void start()  {
   loadImageAssets();
   loadAudioAssets();
   createSpecialEffects();
   createTextAssets();
   createMaterials();
   createBoardGameNodes();
   createUInodes();
   createQAnodes();

   qaLayout.setVisible(false);
   createSceneProcessing();
   createGameBoardNodes();
   ...
}

使用图 22-3 中的运行➤项目工作流程,在你的闪屏中查看对该问题的修复。

A336284_1_En_22_Fig3_HTML.jpg

图 22-3。

Your Q&A UI panel is now hidden on startup, in the top of your .start() method

现在我们已经修复了这个小的(代码方面的)闪屏呈现问题,我们可以继续创建您的 Score UI 布局设计,从 scoreLayout StackPane 对象和包含其装饰的 Text 对象开始。

记分板 UI 设计:createScoreNodes()方法

让 NetBeans 为我们创建一个 createScoreNodes()自定义方法体,方法是在我们刚刚添加的qaLayout.setVisible(false);语句后添加一行代码,然后使用 Alt+Enter 击键组合来触发 NetBeans 9 的这一自动化方法编码。此处显示了 Java 语句,并在图 22-4 中间突出显示:

A336284_1_En_22_Fig4_HTML.jpg

图 22-4。

Create a createScoreNodes() method after the qaLayout logic and use Alt+Enter to have NetBeans code it

public void start()  {
   loadImageAssets();
   loadAudioAssets();
   createSpecialEffects();
   createTextAssets();
   createMaterials();
   createBoardGameNodes();
   createUInodes();
   createQAnodes();
   qaLayout.setVisible(false);
   createScoreNodes();

   ...
}

将类底部的方法复制到 createQAnodes()方法之后,如图 22-5 底部所示。将 qaLayout 语句从 createQAnodes()方法复制到 createScoreNodes()方法中,并将。setTranslateX()方法从-250 调用到 250,以将其镜像到显示器的另一个角落。

A336284_1_En_22_Fig5_HTML.jpg

图 22-5。

Add a StackPane named scoreLayout at the top of the class and instantiate and configure it like qaLayout

您将保持其他四个复制的 Java 语句不变(除了将 qaLayout 更改为 scoreLayout),因为除了 X 位置之外,您希望“镜像”高度、深度、背景颜色和首选 StackPane 大小。使用以下 Java 代码将此 scoreLayout 添加到 SceneGraph 中,这些代码也在图 22-6 中突出显示:

A336284_1_En_22_Fig6_HTML.jpg

图 22-6。

To render the scoreLayout StackPane , you must first add it to the root.getChildren().addAll() method chain

private void addNodesToSceneGraph() {
    root.getChildren().addAll(gameBoard, uiLayout, qaLayout, scoreLayout, spinner);
    ...
}

让我们再次使用“运行➤项目”工作流程,并测试您的乐谱 UI 面板的新代码,以确保使用. settranslatex()方法调用值将乐谱 UI 面板设计镜像到屏幕右侧足够远的位置。正如你在图 22-7 中看到的,根据我们的猜测,我们离游戏的右角还差 400 个单位。因此,我们需要将值 250 更改为 650,以将 StackPane 容器进一步向右移动,并防止 2D StackPane UI 容器对象与您的 i3D 游戏棋盘节点层次结构相交。

A336284_1_En_22_Fig7_HTML.jpg

图 22-7。

As you can see, this StackPane is intersecting with the game board and needs to move right 400 units in X

完成乐谱 UI 背景和容器的 Java 9 代码在图 22-8 中高亮显示。setTranslateX()方法调用(从 250 X 单位到 650 X 单位)应该类似于下面的代码:

A336284_1_En_22_Fig8_HTML.jpg

图 22-8。

Change the scoreLayout.setTranslateX() method call from 250 to 650 to move the Score UI panel by 400 units

scoreLayout.setTranslateX(650);

我们需要做的下一件事是将 Java 代码放在适当的位置,它将在游戏启动时隐藏分数 UI 面板,就像它隐藏问答 UI 面板一样。一旦您添加了您的scoreLayout.setVisible(false); Java 语句,您的新 start()方法代码应该看起来像下面的代码,如图 22-9 所示:

A336284_1_En_22_Fig9_HTML.jpg

图 22-9。

Add the .setVisible(false) method call off scoreLayout after the createScoreNodes() method to hide the panel on startup

public void start()  {
   loadImageAssets();
   loadAudioAssets();
   createSpecialEffects();
   createTextAssets();
   createMaterials();
   createBoardGameNodes();
   createUInodes();
   createQAnodes();
   qaLayout.setVisible(false);
   createScoreNodes();
   scoreLayout.setVisible(false);
   ...
}

正如您在图 22-10 中看到的,您仍然需要使用您的。setOnFinished(event)事件处理基础结构。这段代码已经就绪,因为我们已经在摄像机动画完成后展示了 Q & A UI 面板。因此,我们所要做的就是在cameraAnimIn.setOnFinished(event->{});结构的末尾添加scoreLayout.setVisible(true);语句,在图 22-10 的中间用浅蓝色和黄色突出显示。在能够测试您的 Score UI 面板之前,您必须将这个 Java 9 代码放在适当的位置。

A336284_1_En_22_Fig10_HTML.jpg

图 22-10。

To show the scoreLayout StackPane, add a .setVisible(true) method call in cameraAnimIn.setOnFinished()

再次,使用运行➤项目的工作流程,并确保您的游戏闪屏和游戏板旋转回到他们的“干净”的外观;然后旋转并选择游戏棋盘方格 1 来调用 cameraAnimIn 对象。setOnFinished(event)事件处理方法逻辑,此时显示两个 StackPane UI 容器。

这允许我们在摄像机角度改变后测试 Score UI 容器代码。如图 22-11 所示,我们所要做的就是通过改变。将-395(如图 22-8 所示)的 setTranslateY()方法调用为-385 的值,以获得完美镜像的乐谱 UI 面板结果。

A336284_1_En_22_Fig11_HTML.jpg

图 22-11。

Use Run ➤ Project to render the Score panel, via the .setOnFinished() event handler, showing the initial pane position

现在,我们可以使用不同颜色的文本对象来“装饰”scoreLayout StackPane 的内部,我们可以使用漂亮的大字体和暗原色(RGB)值来标记 Score UI 面板的各个部分。

添加乐谱 UI 容器设计元素:文本对象

将 scoreTitle 文本对象添加到类顶部的文本复合语句,然后将 scoreTitle 添加到 scoreLayout.addChildren()。你的 addNodesToSceneGraph()方法中的 addAll()方法链,如图 22-12 所示。Java 代码应该如下所示,同样在图 22-12 的顶部以黄色显示:

A336284_1_En_22_Fig12_HTML.jpg

图 22-12。

Add a scoreTitle Text object , instantiate it, and configure it in createScoreNodes(). Then add it to the SceneGraph

private void addNodesToSceneGraph() {
    root.getChildren().addAll(gameBoard, uiLayout, qaLayout, scoreLayout, spinner);
    gameBoard.getChildren().addAll(Q1, Q2, Q3, Q4);
    Q1.getChildren().addAll(q1, Q1S1, Q1S2, Q1S3, Q1S4, Q1S5);
    Q2.getChildren().addAll(q2, Q2S1, Q2S2, Q2S3, Q2S4, Q2S5);
    Q3.getChildren().addAll(q3, Q3S1, Q3S2, Q3S3, Q3S4, Q3S5);
    Q4.getChildren().addAll(q4, Q4S1, Q4S2, Q4S3, Q4S4, Q4S5);
    qaLayout.getChildren().addAll(a1Button, a2Button, a3Button, a4Button);
    scoreLayout.getChildren().addAll(scoreTitle);
    uiLayout.getChildren().addAll(boardGameBackPlate, logoLayer, infoOverlay, uiContainer);
    uiContainer.getChildren().addAll(gameButton, helpButton, legalButton, creditButton,
                                     scoreButton);
    infoOverlay.getChildren()addAll(platText, moreText);
}

使用设置 scoreLayout StackPane 中 scoreTitle 标题的对齐方式。用 Pos 调用 setAlignment()方法。TOP_CENTER 常量,它将这个深红色的分数标题置于 StackPane 容器的顶部和中间。有趣的是,文本对象对齐是在父 StackPane 容器中设置的。我们可以在以后使用。setTranslateX()和。setTranslateY()方法调用文本子对象来微调乐谱 UI 面板中的对齐方式,我们将在接下来的几页中充实这个设计。

在 createScoreNodes()方法的底部实例化 scoreTitle 文本对象,然后使用。setFont()方法。使用 Arial 黑色字体,在 72 磅的大字体下具有醒目的可读性。使用。setFill()方法调用并将颜色从黑色更改为深红色,以便乐谱标题可以在乐谱 UI 面板的顶部轻松查看。图 22-12 底部突出显示的 Java 代码如下所示:

private void createScoreNodes()  {
    scoreLayout = new StackPane();
    scoreLayout.setTranslateX(650);
    scoreLayout.setTranslateY(-385);
    scoreLayout.setTranslateZ(-75);
    scoreLayout.setBackground(new Background(new BackgroundFill(Color.WHITE,
                                                                CornerRadii.EMPTY,
                                                                insets.EMPTY) ) );
    scoreLayout.setPrefSize(360, 654);
    scoreLayout.setAlignment(Pos.TOP_CENTER);
    scoreTitle = new Text("SCORE");
    scoreTitle.setFont( Font.font("Arial Black", 72) );
    scoreTitle.setFill(Color.DARKRED);
}

图 22-13 显示了运行➤项目的工作流程,以预览“乐谱”标题如何在乐谱窗格中工作。

A336284_1_En_22_Fig13_HTML.jpg

图 22-13。

Use the Run ➤ Project work process to preview the Score UI panel with its new Dark Red title heading

如图 22-14 中突出显示的,我们在类的顶部声明了一个 scoreRight 文本对象,并将其添加到 scoreLayout.addChildren()中。addAll()方法链,这样就可以在我们将要做的测试渲染中看到它。

A336284_1_En_22_Fig14_HTML.jpg

图 22-14。

Add a scoreRight object at the top of the class, instantiate and configure it, and add it to the SceneGraph

我在 scoreTitle 对象后添加了 scoreRight 对象实例化,并将其配置为使用深蓝色、Arial 黑色字体,字体大小为 72 磅。我添加了 X 和 Y 坐标,最初将其定位在 scoreLayout StackPane 中的(-50,150)。图 22-14 显示了代码,如下所示:

private void createScoreNodes()  {
    ...
    scoreTitle = new Text("SCORE");
    scoreTitle.setFont( Font.font("Arial Black", 72) );
    scoreTitle.setFill(Color.DARKRED);
    scoreRight = new Text("Right:");
    scoreRight.setFont( Font.font("Arial Black", 72) );
    scoreRight.setFill(Color.DARKBLUE);
    scoreRight.setTranslateX(-50);
    scoreRight.setTranslateY(150);
}

图 22-15 显示了一个运行➤项目的工作流程,以预览右:heading 如何在配乐窗格中工作。

A336284_1_En_22_Fig15_HTML.jpg

图 22-15。

Use a Run ➤ Project work process to preview the Score UI panel with new Dark Blue “Right:” score heading

因为这个特殊的 i3D 棋盘游戏设计是为即将入学的孩子设计的,所以让我们也包括一个“错误:”分数跟踪标题,并在每个答案后添加一些鼓励,如干得好!或者再次旋转。编写这段代码的最快方法是复制并粘贴 scoreRight 代码,并将其本身放在下面,将 scoreRight 更改为 score error,同时将颜色更改为红色,并将 X,Y 位置更改为-25,300。这显示在下面的 Java 9 代码中,并在图 22-16 的底部以黄色突出显示:

A336284_1_En_22_Fig16_HTML.jpg

图 22-16。

Add scoreWrong object at the top of the class, instantiate and configure it, and add it to the SceneGraph

private void createScoreNodes()  {
    ...
    scoreLayout.setPrefSize(360, 654);
    scoreLayout.setAlignment(Pos.TOP_CENTER);
    scoreTitle = new Text("SCORE");
    scoreTitle.setFont( Font.font("Arial Black", 72) );
    scoreTitle.setFill(Color.DARKRED);
    scoreRight = new Text("Right:");
    scoreRight.setFont( Font.font("Arial Black", 72) );
    scoreRight.setFill(Color.DARKBLUE);
    scoreRight.setTranslateX(-50);
    scoreRight.setTranslateY(150);
    scoreWrong = new Text("Wrong:");
    scoreWrong.setFont( Font.font("Arial Black", 72) );
    scoreWrong.setFill(Color.RED);
    scoreWrong.setTranslateX(-25);
    scoreWrong.setTranslateY(300);
}

图 22-17 显示了一个运行➤项目的工作流程,用于测试 Java 代码中的红色错误:文本标题。

A336284_1_En_22_Fig17_HTML.jpg

图 22-17。

Use a Run ➤ Project work process to preview the Score panel and Red “Wrong:” text

接下来,让我们在类顶部的复合语句中添加一个 scoreCheer 文本对象声明。正如你在图 22-18 顶部看到的黄色,你的复合语句现在有两行,一行用于启动(闪屏)UI 文本对象,另一行用于乐谱 UI 文本对象。

A336284_1_En_22_Fig18_HTML.jpg

图 22-18。

Declare the scoreCheer Text object at the top of the class; then add it to your scoreLayout SceneGraph branch

由于已经声明了该对象,因此可以将其添加到 scoreLayout.getChildren()中。addAll()方法链,如图 22-18 所示,甚至在 NetBeans 9 中实例化它并且不创建错误之前。图 22-18 底部用浅蓝色突出显示的 Java 语句应该如下所示:

scoreLayout.getChildren().addAll(scoreTitle, scoreRight, scoreWrong, scoreCheer);

将 score error Java 语句复制并粘贴到它们下面,并将 score error 更改为 scoreCheer。将 scoreCheer 设为深绿色,并将 scoreRight 和 scoreCheer 上的字体大小分别减小到 64 磅和 56 磅,以便它们更适合 scoreLayout。记住,我们需要空间来容纳代表这些分数的数字!由于 scoreWrong 更宽(因为字体中使用的字母),我将此降低到 60 分。我将标题在 Y 维度上多间隔了 10 个单位,并通过使用-56、-44 和-2 的 X 位置将它们排列在左侧,如图 22-19 中粗体和突出显示的(至少是 scoreGrade 语句):

A336284_1_En_22_Fig19_HTML.jpg

图 22-19。

Add the scoreCheer Text object in DarkGreen and tweak the other Text object font sizes and XY locations

    scoreRight = new Text("Right:");
    scoreRight.setFont( Font.font("Arial Black", 64) );
    scoreRight.setFill(Color.DARKBLUE);
    scoreRight.setTranslateX(-56);
    scoreRight.setTranslateY(160);
    scoreWrong = new Text("Wrong:");
    scoreWrong.setFont( Font.font("Arial Black", 60) );
    scoreWrong.setFill(Color.RED);
    scoreWrong.setTranslateX(-44);
    scoreWrong.setTranslateY(310);
    scoreCheer = new Text("Great Job!");
    scoreGrade.setFont( Font.font("Arial Black", 56) );
    scoreGrade.setFill(Color.DARKGREEN);
    scoreGrade.setTranslateX(-2);
    scoreGrade.setTranslateY(460);

图 22-20 显示了用于渲染新文本对象标题及其调整后的字体大小和定位设置的运行➤项目工作流程。请注意,由于我们尚未扩散答案或评分逻辑,只有猎鹰(图 22-11 或 15 中的方块 1 选项 1)显示了代表答案选项的按钮标签。

A336284_1_En_22_Fig20_HTML.jpg

图 22-20。

Use the Run ➤ Project work process to preview the Score panel and Text object headings you have added

现在,我们准备将分数引擎逻辑添加到我们在第二十一章中放置的问答按钮元素中,并将文本答案正确和答案错误选项添加到我们的分数 UI 设计中。这将显示由 Q&UI 按钮元素生成的分数。之后,我们可以计算等级,并将其分配给字母等级的第七个文本元素。

评分引擎:计算答案分数的逻辑

让我们添加一个名为 createQAprocessing()的自定义方法来保存我们的评分引擎逻辑。在 createQAnodes()方法中创建的四个按钮元素的 setOnAction(event)事件处理。正如你在图 22-21 中看到的,这需要在我们在 createQAnodes()和 createScoreNodes()中设置 Q & A 并对 UI 设计评分之后,在我们在 createSceneProcessing()中调用这个 Q & A 事件处理之前。所以,在 scoreLayout.setVisible(false)后面加一行 Java 代码;语句和 createSceneProcessing()之前;声明,如图 22-21 中黄色高亮部分所示。使用 Alt+Enter 工作进程,通过让 NetBeans 9 编写引导方法代码和错误语句来删除红色波浪下划线,我们将很快替换这些代码和语句。这将打开一个下拉菜单,其中包含 javafxgame 中的创建方法“createQAprocessing()”。JavaFXGame 选项,双击该选项即可执行(单击将选中该单选,如图 22-21 )。

A336284_1_En_22_Fig21_HTML.jpg

图 22-21。

Add the createQAprocessing() method call after createQAnodes() and createScoreNodes() and use Alt+Enter

对这四个动作事件处理结构中的第一个进行编码的最简单的方法是进入您的。start()方法,并将您在本书前面创建的按钮事件处理结构复制并粘贴到这个新创建的 createQAprocessing()方法中。在使用 Paste 命令之前,请确保完全选择 NetBeans 引导错误语句代码行,以便 ActionEvent 处理代码替换此引导错误语句。

更改。setOnAction()方法调用从调用到 a1Button,并删除此事件处理构造中的处理语句,使其成为一个空事件处理程序,以便我们可以从头开始构建分数处理逻辑。事件处理程序的 Java 代码如下所示,如图 22-22 所示:

A336284_1_En_22_Fig22_HTML.jpg

图 22-22。

Mouse over the event handling structure, and notice NetBeans wants to convert to a lambda expression

private void createQAprocessing() {
    a1Button.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle (ActionEvent event) {
            ... // An Empty ActionEvent Handling Structure
        }
    });
}

Alt+Enter 生成的 Java 代码将是相同的空事件处理结构,使用 lambda 表达式方法,这将删除八行代码中的三行,即 37.5%的编码结构。

您的 Java 9 代码应该如下所示,您得到的 lambda 表达式如图 22-24 所示。图 22-23 显示了调用 NetBeans Alt+Enter 键后的工作过程。选择“使用 lambda 表达式”选项,该选项将在 NetBeans 9 IDE 中执行一个算法,该算法将为您重写 Java 代码,并将其转换为更短的 lambda 表达式编程格式。

A336284_1_En_22_Fig23_HTML.jpg

图 22-23。

Use the Alt+Enter keystroke and select and double-click the “Use lambda expression” option to convert

private void createQAprocessing() {
    a1Button.setOnAction(ActionEvent event) -> {

        ... // Empty ActionEvent Handling Lambda Expression Structure

    });
}

在您的空 ActionEvent 处理 lambda 表达式中,我们将为每个 Button 对象提供条件 if()结构,该结构将查看 picked Node 对象和 pickSn Random 对象,以确定我们正在处理哪个游戏棋盘方块(Q1S1 到 Q4S5)和随机数生成器值(pickS1 到 pickS20)。这将告诉我们正在查看哪些内容,然后我们的评分引擎逻辑将对该选择进行评分。

在 a1Button.setOnAction()构造中,添加一个 if(picked == Q1S1)来开始这个编码过程。请注意,NetBeans 错误突出显示了选中的节点对象,因为它当前是 createSceneProcessing()方法的本地(私有)对象,如图 22-24 所示。接下来,我们将不得不使这个选中的节点对象成为一个全局(公共)变量。

A336284_1_En_22_Fig24_HTML.jpg

图 22-24。

NetBeans error highlights picked Node in the if() statement because it is local to createSceneProcessing()

在你的类的顶部声明选中的节点,如图 22-25 中突出显示的,以消除这个错误。

A336284_1_En_22_Fig25_HTML.jpg

图 22-25。

Remove the Node declaration from createSceneProcessing(); relocate it to the top of the class, making it global

现在,我们将在 createQAprocessing()方法中查看的所有对象都已声明,因此它们对整个类都是可见的,我们可以继续使用位于 a1Button 事件处理构造中的if (picked == Q1S2 && pickS1 == 0) { ... }结构对 a1Button 事件处理进行编码。

在类的顶部声明一个 rightAnswer integer 和 rightAnswers Text 对象,作为 int 变量和 Text 对象的复合声明语句的一部分,因为我们将要编写的 Java 9 代码将利用这些。

我们在 if()构造中要做的是(如果按钮 1 包含正确答案)将 rightAnswer 整数加 1,然后通过使用. setText()方法调用将 rightAnswers 文本对象设置为这个 right answer 值。在里面。setText()方法我们将使用 String.valueOf()方法将 rightAnswer 整数转换为字符串值并使用。setText()将 scoreCheer 设置为伟大的工作!。正确答案处理的代码,在 Q1S1 选项 0(第一个答案选项)的情况下是正确的(一个正确答案),应该看起来像下面的代码,如图 22-26 中突出显示的。它被编码成两行(四行,包括 lambda 表达式),以允许 20 个 board square score 逻辑处理 Java 构造适合 createQAprocessing()方法体内的 120 行代码。

A336284_1_En_22_Fig26_HTML.jpg

图 22-26。

Code a compact if() statement evaluating Q1S1 and pickS1 to see whether the a1Button answer is a correct one

a1Button.setOnAction(ActionEvent event) -> {
    if (picked == Q1S1 && pickS1 == 0) { rightAnswer = rightAnswer + 1;
        rightAnswers.setText(String.valueOf(rightAnswer)); scoreCheer.setText("Great Job!"); }
});

为了能够显示这个 rightAnswer 整数,我们需要在 createScoreNodes()中将 rightAnswers 文本对象添加到您的 UI 设计中。这是使用复制粘贴技术完成的。将 Java 代码的 scoreRight 块直接复制到它自己的下面。设置颜色为黑色,X 位置为 96。Y 位置应该保持不变,以便对齐“右”文本对象。通过在构造函数方法中使用“0”字符串值,将初始文本值设置为零。

图 22-27 底部显示的 Java 代码应该类似于以下代码:

A336284_1_En_22_Fig27_HTML.jpg

图 22-27。

Add the rightAnswers Text object to createScoreNodes() to display the result of your integer calculation

rightAnswers = new Text("0");                            // Initializes rightAnswers to Zero
rightAnswers.setFont(Font.font("Arial Black", 64));
rightAnswers.setFill(Color.BLACK);
rightAnswers.setTranslateX(96);
rightAnswers.setTranslateY(160);

图 22-28 显示了用于渲染 rightAnswers 文本对象及其设置的运行➤项目工作流程。

A336284_1_En_22_Fig28_HTML.jpg

图 22-28。

Use a Run ➤ Project work process to preview the Score panel and the rightAnswers Text object placement

其他按钮元素使用类似的代码,只不过它们将向一个 wrongAnswer int 变量添加一个。这意味着您将复制您创建了三次的 a1Button 构造,通过 a4Button 将 a1Button 更改为 a2Button。将 rightAnswer 改为 wrongAnswer,将 rightAnswers 改为 wrongAnswers,如图 22-29 所示。

A336284_1_En_22_Fig29_HTML.jpg

图 22-29。

Copy and paste a1Button construct three times underneath itself and change the object and variable names

还要改“干得好!”“再次旋转”为了能够显示一个错误答案整数,我们需要添加一个错误答案文本对象到 createScoreNodes()。这是通过复制粘贴过程完成的。将 score error Java 代码块直接复制到其自身下面。设置颜色为黑色,X 位置为 96。Y 位置保持不变,以对齐两个文本对象。在构造函数方法中使用“0”字符串值将初始文本值设置为零。

图 22-30 底部显示的 Java 9 代码看起来应该类似于下面的代码:

A336284_1_En_22_Fig30_HTML.jpg

图 22-30。

Add the wrongAnswers Text object to createScoreNodes() to display the result of the integer calculation

wrongAnswers = new Text("0");                            // Initializes wrongAnswers to Zero
wrongAnswers.setFont(Font.font("Arial Black", 64));
wrongAnswers.setFill(Color.BLACK);
wrongAnswers.setTranslateX(96);
wrongAnswers.setTranslateY(160);

请记住,为了查看正确答案和错误答案答案结果文本对象值持有者,您必须将它们添加到 scoreLayout StackPane 内的 SceneGraph 层次结构中。getChildren()。addAll()语句。

我只用了一个屏幕截图来展示将这些文本节点添加到 StackPane 中以节省本章的空间,因为我们有许多 Java 代码要做来完成您的棋盘游戏评分和分级基础结构。一旦我们完成了对这段代码的测试,您所要做的就是在 createQAprocessing()方法体中复制其他 59 个选项的评分代码,为其他 19 个游戏棋盘方格创建评分。这将需要与您的其他 59 组答案相匹配,您将复制并粘贴这些答案,然后使用我们在第二十一章中创建的代码进行编辑。

然后你会得到你所有的答案和分数!你可以在下一章开始“防错”你的代码,以确保多个 UI 元素在需要之前不会被点击。请记住,这些是年幼的儿童,智力受到挑战,玩教育游戏,所以你需要这个用户界面的保护。scoreLayout.getChildren().addAll()方法链的 Java 代码看起来像下面的 Java 代码,如图 22-31 所示:

A336284_1_En_22_Fig31_HTML.jpg

图 22-31。

Be sure to add any new Node objects to the SceneGraph hierarchy so they will be visible at render time

scoreLayout.getChildren().addAll( scoreTitle, scoreRight, scoreWrong, scoreCheer,
                                  rightAnswers, wrongAnswers);

图 22-32 显示了用于呈现新的错误答案文本对象及其设置的运行➤项目工作流程;如您所见,这对齐了 score (integer)元素,如果数值字段向右扩展,则为更大的分数(10 和 100)留出了空间,我们将在下一节 score 代码测试中确定这一点。

A336284_1_En_22_Fig32_HTML.jpg

图 22-32。

Use your Run ➤ Project work process to preview your Score UI panel and your wrongAnswers Text object

接下来,让我们测试一下我们刚刚编写的评分代码,看看评分 UI 设计是否正确地响应了分数向两位数的递增。也就是说,数值的增加是向右扩展还是向左扩展?还是从中心向外扩展?一旦我们弄清楚了这一点,我们就可以进一步“调整”(优化)我们的评分 UI 设计。

分数 UI 测试:显示更大的整数

由于我们还没有实现“玩家验证”Java 代码,我们将在下一章中实现,这样玩家就不能在每个游戏周期中多次单击 UI 元素(3D 旋转器、游戏棋盘方块、按钮)来“游戏”系统(或导致呈现错误出现),我们目前可以多次单击按钮元素。这允许我们测试记分板 UI,以找出大于 9 的数字将如何显示,以便我们可以“调整”X 位置,并将数字(正确和错误)间隔到左侧(当前间距)、最右侧或标签中心(标题)和乐谱 UI 面板的右边缘。如图 22-33 所示,我已经点了十次正确的(猎鹰 Hawk)答案,看看数字会如何移动。正如你所看到的,数字从中心向外扩展,这可以通过比较 10 和 2 来看出,而不是向左或向右。因此,我们需要将这 120 个单位向右移动。现在你的得分值将能够扩大到两位数或三位数。

A336284_1_En_22_Fig33_HTML.jpg

图 22-33。

Use your Run ➤ Project work process and click the Button elements to increment (test) your scoring code

递增。从 96 到 120 的 rightAnswers 和 wrongAnswers 文本对象的 setTranslateX()方法调用。这将使乐谱 UI 的数字部分在标签(标题)和乐谱 UI 面板的右侧居中。您的代码现在应该看起来如下,它在图 22-34 的中间和底部突出显示:

A336284_1_En_22_Fig34_HTML.jpg

图 22-34。

Expand the X position of each numeric element 24 units to the right, from a value of 96 to a value of 120

rightAnswers = new Text("0");
rightAnswers.setFont(Font.font("Arial Black", 64));
rightAnswers.setFill(Color.BLACK);

rightAnswers.setTranslateX(120);                    // Update X position 24 units from 96 to 120
rightAnswers.setTranslateY(160);
wrongAnswers = new Text("0");
wrongAnswers.setFont(Font.font("Arial Black", 64));
wrongAnswers.setFill(Color.BLACK);

wrongAnswers.setTranslateX(120);                    // Update X position 24 units from 96 to 120
wrongAnswers.setTranslateY(160);

同样,使用图 22-35 中所示的“运行➤项目”工作流程来渲染游戏,并导航到分数 UI 面板,以查看这是否会分隔数字显示,以便 10 到 99 的分数在分数 UI 面板中显示时看起来很棒。

A336284_1_En_22_Fig35_HTML.jpg

图 22-35。

Use the Run ➤ Project work process and click the Button elements to increment (test) double-digit scores

大多数玩家不会在一次游戏中玩(旋转)游戏板数百次,所以这对这个游戏来说应该很好。但是,请注意,三位数(100 到 999)也应该合适。

然而,如果你期待这么多的游戏性,你可能想要在分数 UI 设计的左侧再多留 4 到 8 个单位的标签(标题),这样就可以轻松容纳你的三位数游戏分数。

现在你已经准备好“增殖”我们在这一章关于计分和第二十一章中写的代码来创建整个游戏基础设施。这将在我们已经创建了超过 22 章的 1000(或更多)行 Java 代码的基础上再增加 1000 行代码,以将整个 i3D 棋盘游戏基础设施放置到位。接下来就说说怎么做这个吧。这是关于 2D 内容(图像、答案选项和得分)的大量工作,但它将与我们迄今为止使用 JavaFX 9 APIs 创建的 i3D 棋盘游戏无缝集成。

完成游戏:添加答案和分数

为 60 个不同的游戏棋盘方格选项添加 4 个答案涉及 240 个不同的内容选项(和代码行),为这 60 个不同的游戏棋盘方格选项添加得分涉及另外 480 行代码,如果包括 lambda 表达式容器,可能还要多一点。这是一个相当容易无错执行的大量工作的原因是,我们已经创建了一个代码设计,可以复制并粘贴到适当的位置,并且可以创建、插入和跟踪文本值,以便游戏内容在播放时能够正确工作。也就是说,不要期望为您的专业 Java 9 游戏开发管道创建内容会比创建 Java 9 代码更容易,因为游戏设计和游戏开发涉及大量的新媒体、内容、策略和编码工作,最终会产生专业的结果。

我会一次添加一个游戏棋盘方格的答案和分数,直到所有 20 个游戏棋盘方格都就位。添加未来的游戏板广场选项可以很容易地完成。您只需使用 random.nextInt(n)变量将 pickS1 到 pickS20 变量增加 1,即可添加 4 个不同的随机图像主题到每个游戏棋盘方格中。添加额外的一轮随机内容将相当于添加 20 轮新的按钮回答(80 个回答选项)并在您的评分逻辑中对这 80 个新回答进行评分,这将相当于 160 行代码,或者每个游戏板内容深度添加大约 240 行代码。增加游戏板内容的深度意味着玩家玩游戏的时间越长,看到的重复内容就越少。如果您愿意,还可以添加代码来跟踪已使用的内容。

一旦你将剩下的内容扩展成答案和评分逻辑,你就完成了游戏设计和开发工作流程的大部分。在接下来的章节中,我们将研究防错 UI 设计,以便用户在玩游戏的过程中被迫正确使用它,以及使用新的 Java 9 NetBeans IDE 进行优化和代码分析等事情。

摘要

在第二十二章中,我们学习了如何在 i3D 棋盘游戏设计的右下角实现一个乐谱 UI 面板。我们还学习了如何使用问答面板中的按钮 UI 元素上的 ActionEvent 处理来更改记分板数字部分的分数,这是我们在上一章 21 中创建的。这基本上把我们放在一个位置,我们可以完成编码和评分的个人广场(和象限,一旦广场被选中),游戏,其中一个关于内容的视觉问题得到回答和评分。(在我开始写第二十三章之前,我必须这么做。)

这意味着这是您的另一个繁重的编码章节,因为您构造了 20 个自定义方法,setupQ1S1gameplay()到 setupQ4S5gameplay()。您还在 createQAprocessing()事件处理基础结构中为每个按钮元素放置了条件 if()结构。您仍然需要确保交叉检查所有棋盘游戏方法之间的图像素材,最后,您需要一起测试所有代码,以确保它对每个游戏棋盘方格都正常工作。

在第二十三章中,作为游戏性保护的一部分,我们当然会在回答和评分完成后反转相机动画,并以更倾斜的视角显示动画,这是查看游戏棋盘旋转的最佳方式。我们还将防止点击任何可以点击的 UI 元素,这样用户就只能选择一个主题,例如,可以旋转一次白板。我们决不会结束游戏设计工作流程!

二十三、完成游戏代码和通过玩家验证你的事件处理

既然您的 i3D UI 及其事件处理(更不用说您的大多数动画和数字音频)都已就绪并正常工作,那么是时候在您的代码库中完成加载基本级别的内容(60 幅图像、240 个答案和得分)了。我们将在这一章的第一部分做这个,所以我可以向你展示我做了什么来完成游戏,并且在整本书中没有代码连续性的损失。大部分编码都是复制粘贴,这要归功于我设置游戏设计代码的方式,以及在测试游戏时确保答案匹配且运行良好。

在本章中,我们将用与视觉效果(问题)相匹配的基于文本的答案内容填充 20 个 setupQSgameplay()方法。我们还将完成 createQAprocessing()方法,该方法保存更新评分 UI 面板的答案评分代码。玩家将使用这些来选择正确的答案,揭示该方块的视觉代表什么,并对他们的答案进行评分。这意味着在这一章中你将会增加几百行代码,在你完成之前将会增加 1750 行代码。

一旦我们完成了游戏“答案显示、选择和评分”基础设施的大部分编码,并测试了每个方块以确保其工作正常,我们就可以创建 Java 代码的防错部分了。这导致了一个专业的游戏,确保玩家正确使用它。这包括使用布尔变量(称为标志)来保存“点击”变量;一旦玩家单击了旋转器、游戏棋盘方块或回答按钮 UI 元素,elementClick 变量就会被设置为 false,这样游戏玩家就不能再次单击它并“游戏”游戏代码。

例如,您的玩家可能多次单击您的正确答案按钮 UI 元素,这将运行到记分板的“右:”部分!我称之为代码的“用户检验”或“错误检验”,这是一个相当复杂的过程(正如你将在本章中看到的),有时会深入到几个层次。例如,我们将首先保护所有游戏棋盘方格不被点击两次,然后再下一层,保护一个象限的游戏棋盘方格,这样玩家在每一轮游戏中只能选择选定的象限。

我们还将添加最终的动画,它将摄像机带回到游戏的游戏棋盘旋转视图,以便玩家可以调用随机旋转来选择下一个象限(动物、植物、矿物或地点主题)。这将通过在棋盘游戏 UI 设计的顶层添加一个亮黄色的 Let's Play Again 按钮元素来实现。在这一章中我们有大量的工作要做,所以让我们开始吧!

完成游戏:填充游戏方法

这一章的第一部分将向你展示我如何把 Java 代码放在适当的位置来完成游戏。我们将在 setupQSgameplay()方法中向四个按钮 UI 元素添加答案选项(其中有二十个,第一个已经在第二十一章中编写过,以向您展示 Java 代码是如何工作的),然后我们将在四个 Q &按钮 UI 元素的 ActionEvent 处理方法内的 createQAprocessing()方法中添加这些答案的得分。

添加答案选项:完成 setupQSgameplay()方法

在游戏内容开发的这一阶段,并不是在 setupQSgameplay()方法中复制和粘贴 Java 9 代码会占用您的大部分时间,而是确认正确答案和创建错误答案会难倒玩家并导致“错误:”答案。一旦为每个随机选择添加了四个答案选项,setupQ1S1gameplay()的方法体看起来就像图 23-1 中的 18 个 Java 语句。

A336284_1_En_23_Fig1_HTML.jpg

图 23-1。

Find and add a correct answer to a different button (and three incorrect answers) for each random image

将你对应的对错答案处理添加到我们在第二十二章创建的 createQAprocessing()评分引擎中。正确答案(正确答案和正确答案)在图 23-2 中用黄色突出显示。

A336284_1_En_23_Fig2_HTML.jpg

图 23-2。

Add matching correct or incorrect answer score processing to the createQAprocessing() method for Q1S1

非常仔细地执行这个工作过程是很重要的,这样您的 20 个 setupQSgameplay()方法中的每一个方法的正确和不正确的答案都可以与 createQAprocessing()计分算法的方法体完美匹配。为了让你的评分引擎准确地给游戏评分,这些必须完美匹配,正如你通过比较数字 23-1 和 23-2 在一个问题接一个问题和一个答案接一个答案的基础上所看到的。

你可以边走边测试,一个游戏板一个游戏板地测试,或者完成后一次性测试。或者你可以两种方式都做,我就是这么做的,尝试生成在编译时和运行时都没有错误的代码。在我写这本书的时候,有成千上万行代码和一个处于测试阶段的 Java 9(和 JavaFX 9) API,这显然不是一件容易的事情,尤其是当我每周都要上交一个完整的章节的时候。

使用运行➤项目工作流程渲染代码和 3D,并在您的第一象限测试游戏棋盘 square 1,如图 23-3 所示。我建议一次做一个方格(和一个象限),这样你就可以利用代码“模式”,这可以通过比较图 23-1 和 23-2 看出。您可以根据 Java 代码对象名和变量名直观地判断出您正在使用哪个游戏棋盘方格、游戏棋盘象限、按钮编号和随机问题选择,我专门为此目的使用了这些名称。

A336284_1_En_23_Fig3_HTML.jpg

图 23-3。

Use a Run ➤ Project work process and test your Q1S1 answers and scoring logic before moving on to Q1S2

正如你在图 23-4 中突出显示的,setupQ1S2gameplay()方法代码非常相似,除了 pickS2 随机对象和图像引用,当然还有正确和错误答案的按钮标签。

A336284_1_En_23_Fig4_HTML.jpg

图 23-4。

Add correct and incorrect answers to Q1S2 Button objects, which as you can see is similar to the Q1S1 method

正如您在游戏设计和编程的这一阶段所看到的,主要目标是选择最佳按钮答案标签,并将其正确“连接”到 setupQAprocessing()评分引擎方法,以便正确计算分数!这就是为什么我建议一次一个方块地对每个游戏板进行编码,并小心地将它们绑定到 setupQAprocessing()评分引擎方法!确保对每个游戏棋盘方格进行足够好的测试,这样您就可以确保单击正确答案按钮会将“右:”分数标签的整数文本值加 1。

如图 23-5 所示,我已经添加了评分引擎方法的 Java 代码来评估这些答案,用黄色突出显示。我选择了代码中的 Q1S2 框(正方形)对象,以突出显示对它的引用。

A336284_1_En_23_Fig5_HTML.jpg

图 23-5。

Add matching correct or incorrect answer score processing to the createQAprocessing() method for Q1S2

使用图 23-6 所示的运行➤项目工作流程,并测试 Q1S2 游戏棋盘方块逻辑,通过将“right:”score ui panel integer text 对象加 1 来查看它是否为正确答案(duckling)打分。

A336284_1_En_23_Fig6_HTML.jpg

图 23-6。

Use a Run ➤ Project work process and test your Q1S2 answers and scoring logic before moving on to Q1S3

接下来,我完成了第三个到第五个棋盘游戏设置方法,并测试了它们的代码,以确保我在 createQAprocessing()方法体中连接了正确的答案按钮计分逻辑。一旦我们完成添加评分逻辑,这个方法体中的代码将达到大约 500 行,稍后,一个变量将在选择答案后锁定按钮单击事件处理。接下来会有一些非常酷的代码,我会在本章稍后介绍,当我们编写代码来“防止玩家”在 UI 上多次点击鼠标。

正如你将在图 23-7 中看到的,前六个游戏棋盘方格(已经完成 30%)的答案评分逻辑是用三十多行代码填充按钮 1 的 IDE 屏幕,这意味着我们为所有四个按钮完成了十几行代码(144 行)。

A336284_1_En_23_Fig7_HTML.jpg

图 23-7。

Add matching correct or incorrect answer score processing to the createQAprocessing() method for Q2S1

图 23-7 底部高亮显示的代码也显示在被测图左侧的图 23-8 中。当我单击第一个按钮元素(Chard)时,评分引擎将“Right:”分数加 1,如数字从 0 增加到 1 所示。

A336284_1_En_23_Fig8_HTML.jpg

图 23-8。

Use a Run ➤ Project work process to test Q2S1 through Q2S5 answers and scoring logic before moving on

此时,您必须完成接下来的四个 Q2S2 到 Q2S5 游戏棋盘 square setupQSgameplay()方法,以及相应的 createQAprocessing()评分引擎逻辑,以便对您的棋盘游戏的一半内容完成一半。如图 23-8 右侧所示,为了节省空间,我没有展示所有这些工作(及其测试工作过程)所涉及的所有游戏棋盘旋转屏幕截图。完成这个游戏内容所需的 600 行代码需要做大量的工作(createQAprocessing()大约需要 480 行,setupQSgameplay()方法大约需要 100 行),所以这花了我大约一天的时间来编码和测试。我拍摄了一些截图,我将在本章的这一部分展示。

当您将游戏棋盘方块内容和计分逻辑添加到游戏中时,请确保经常使用“运行➤项目”工作流程来测试添加答案按钮对象的新 Java 代码,以及将这些内容连接到 createQAprocessing()计分引擎方法的代码,以查看它是否能提供您想要的游戏效果。如图 23-8 所示,第二象限的答案和评分工作正常,我可以继续做第三象限了。

正如您在图 23-9 中看到的,第三象限的答案和评分现在工作正常,我可以继续添加第四象限的答案和评分代码。此时,您的棋盘游戏应该运行得相当好,我们现在可以开始添加代码,防止游戏玩家多次单击 UI 元素。

A336284_1_En_23_Fig9_HTML.jpg

图 23-9。

Use a Run ➤ Project work process to test the Q3S1 through Q3S5 answers and scoring logic before moving on

正如你在图 23-10 中看到的,游戏内容现在已经准备好了,我们可以开始玩家验证了。

A336284_1_En_23_Fig10_HTML.jpg

图 23-10。

Use the Run ➤ Project work process to test the Q4S1 through Q4S5 answers and scoring logic to finish 20 squares

接下来,让我们通过添加布尔变量来防止重复点击,从而对您当前的代码进行“玩家验证”。

玩家验证代码:控制玩家事件的使用

游戏“理论上”已经结束了,我们可以相信玩家只需点击(一次)正确的 i3D 和 i2D UI 元素就可以玩游戏了。然而,这种特定游戏的预期观众包括未成年儿童、智障人士、残疾玩家和自闭症玩家。因此,我们将放置一些控件,确保玩家只需点击一次正确的 UI 元素就可以玩这个游戏。让我们通过声明(类的顶部)并在 rotGameBoard.setOnFinished()中添加一个设置为 true 的 squareClick 布尔变量来开始这个过程,如下图所示:

A336284_1_En_23_Fig11_HTML.jpg

图 23-11。

Add a squareClick boolean variable set to true at the end of your rotGameBoard.setOnFinished() handler

rotGameBoard.setOnFinished(event-> {
    if (quadrantLanding == 315) { populateQuadrantOne();   }
    if (quadrantLanding == 225) { populateQuadrantTwo();   }
    if (quadrantLanding == 135) { populateQuadrantThree(); }
    if (quadrantLanding == 45)  { populateQuadrantFour();  }
    spinnerAudio.stop();
    squareClick = true;
});

因此,从逻辑上讲,我们已经将游戏棋盘方块的点击“保护”连接到游戏棋盘旋转动画对象。另外,请注意,通过将所有点击保护变量初始化为没有(缺省)值的布尔值,我们实际上已经将所有点击保护设置为 false,或者“点击被锁定”,只需在 Java 类的顶部声明这些变量。这样,我们就不需要 start()方法体中的任何clickProtect = false;语句。

注意在图 23-11 中,我已经在你的类的顶部使用一个复合 Java(布尔)声明声明了 spinnerClick 和 buttonClick。这是因为一旦玩家点击了 i3D spinner UI 元素、游戏棋盘方块和 Q & A 按钮 UI 元素,我们就想要“锁定”它们。这是为了防止多次点击,从而防止多次点击回答按钮(以提高分数)。它还确保您的 i3D 动画和音频通话在每轮游戏中仅触发一次,以防止玩家看到(或听到)类似错误的内容。你真的不希望动画在预期的视觉效果中间重新开始,即使你告诉它重新开始(在播放周期中足够快,这通常是一次以上的点击所做的),所以我们将在一次点击后锁定点击!

接下来,让我们为 spinnerClick 设置锁定,从 MouseEvent 处理代码中的 if(picked == spinner)条件求值开始。我们需要将&& spinnerClick == true 添加到 if(picked == spinner)中,以评估是否允许我们在游戏中的该点单击微调器。如果是的话,我们会立即将 spinnerClick 设置为 false 值,因为 spinner 动画对象(以及象限着陆处理)也在这个代码块中启动。我们将启用单击。setOnFinished()处理程序,这将阻止玩家在旋转的时候点击你的 i3D spinner UI 元素!酷!

i3D spinner 的条件 if() Java 代码基础设施中新增的防鼠标点击功能在这里以粗体显示,并在图 23-12 的顶部以浅蓝色和黄色突出显示:

A336284_1_En_23_Fig12_HTML.jpg

图 23-12。

Add && spinnerClick == true to the if() evaluation for the spinner in your MouseEvent handling structure

if(picked == spinner && spinnerClick == true) {
    spinnerClick = false;
    ...
}

接下来,我们将为这个 rotSpinner 动画对象添加一个. setOnFinished()事件处理程序,一旦 rotSpinner 动画对象完成,它就将布尔 spinnerClick 变量设置为 false 值。这是错误的,因为我们不希望旋转器(或游戏板)再次旋转,直到玩家选择了一个方块和相应的答案按钮 UI 元素来注册(和评分)他们的答案。

但是,在微调器再次出现在屏幕上(这是 rotSpinnerIn 动画对象)后,我们确实希望打开微调器鼠标单击。为此,我们将在。setOnFinished()事件处理逻辑。再说一次,Java 编程在这里是合乎逻辑的,如果你只是想在你的游戏管道中实现什么,这并不奇怪。像大多数游戏逻辑一样,一次考虑所有的事情非常多,所以一开始可能很难做到,直到你习惯于一次考虑所有的实时游戏逻辑,因为它涉及到处理实时交互式游戏所涉及的逻辑(处理管道)。这就是为什么游戏开发被大多数人认为是困难的,因为作为一个程序员,你需要一次“把你的脑袋包起来”所有的游戏代码。

此处显示了 createSceneProcessing()方法中的新 Java 代码,以及在图 23-13 中用浅蓝色和黄色突出显示的代码块,这些代码块设置了用于 rotSpinner 和 rotSpinnerIn 的逻辑:

A336284_1_En_23_Fig13_HTML.jpg

图 23-13。

Set spinnerClick to false in rotSpinner and to true in rotSpinnerIn to control when the spinner is to be clicked

rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
...
rotSpinner.setOnFinished(event-> {
    spinnerClick = false;
});

rotSpinnerIn = new RotateTransition(Duration.seconds(5), spinner);
...
rotSpinnerIn.setOnFinished(event-> {
    spinnerClick = true;
});

我们需要确定(和编码)的下一件事是什么时候我们允许按钮 UI 元素被点击。从逻辑上讲,这应该在 cameraAnimIn 动画对象的末尾,也是在。setOnFinished()事件处理程序构造,在 qaLayout 和 scoreLayout StackPane 2D UI 面板(及其内容或子面板)通过使用。setVisible(true)方法调用每个 StackPane UI 容器对象。由于 buttonClick 作为(声明)缺省值是 false,这就像使用buttonClick = true; Java 语句一样简单。

一旦单击了一个应答按钮 UI 对象,buttonClick 将再次被设置为 false,以防止任何按钮 UI 对象(即使是同一个)被单击,直到再次播放 cameraAnimIn 动画对象。接下来,我们将在 createQAprocessing()计分方法中将此 Java 代码放入每个 ActionEvent 处理结构中,该结构附加到中四个按钮对象的每一个。setOnAction()事件处理构造。

您的新 cameraAnimIn Java 9 代码现在应该如下所示,并且在图 23-14 的底部以浅蓝色和黄色突出显示:

A336284_1_En_23_Fig14_HTML.jpg

图 23-14。

Add a buttonClick = true; statement to the end of the cameraAnimIn.setOnFinished() event handler code

cameraAnimIn = new ParallelTransition( moveCameraIn, rotCameraDown, moveSpinnerOff );
cameraAnimIn.setOnFinished(event-> {
    qaLayout.setVisible(true);
    scoreLayout.setVisible(true);
    buttonClick = true;
});

现在摄像机已经在游戏板表面附近设置了动画,并且 buttonClick 布尔变量已经设置为 true 以允许单击按钮来选择答案,我们需要告诉 buttonClick 变量在一个按钮(a1Button 到 a4Button)被单击时自动关闭(false)。

为此,我们需要用一个if(buttonClick == true)条件评估层“包装”每个按钮 ActionEvent 事件处理构造的分数处理内容。这将只允许在 buttonClick 打开(true)时进行事件处理,然后在处理结束时使用简单的buttonClick = false; Java 语句关闭事件处理。这将是退出if(buttonClick == true) Java 代码构造之前的最后一条语句。

您的 Java 代码应该如下所示,这也在图 23-15 的开头和图 23-16 的结尾突出显示,因为这四个按钮 UI 对象的 ActionEvent 处理结构跨越了每个按钮的 120 多行 Java 代码。setOnAction()事件处理基础结构:

A336284_1_En_23_Fig16_HTML.jpg

图 23-16。

At the end of each if(buttonClick==true) construct, set buttonClick = false; to turn off the Button click function

A336284_1_En_23_Fig15_HTML.jpg

图 23-15。

Use a conditional if(buttonClick == true) statement at the top of each Button event processing structure

private void createQAprocessing() {
    a1Button.setOnAction( (ActionEvent event) -> {
        if (buttonClick == true) { // Evaluates if (buttonClick == true) then {not yet clicked}
            if (picked == Q1S1 && pickS1 == 0)
            ...
        buttonClick = false;   // If this Button has been clicked then set buttonClick to false
        }
    });
}

将这个相同的 if(buttonClick == true)放在每个 Button.setOnAction()构造之前,并将 buttonClick = false 在每个 Button.setOnAction()事件处理构造的末尾,如图 23-15 和图 23-16 所示。

要重新打开所有这些事件处理,我们需要一个“让我们再玩一次”按钮和。setOnAction()事件处理程序。

让我们再玩一次按钮:重置播放器事件处理

一旦玩家点击一个回答按钮 UI 对象,所有的游戏棋盘方块、旋转器和按钮 UI 对象都将被锁定!为下一轮游戏解锁的最佳方式是在游戏板中间添加一个大的黄色“让我们再玩一次”按钮(如果您需要提前查看,如图 23-23 所示),用户将单击该按钮旋转另一个时间,随机选择一个新主题和另一个图像进行识别。在本章的这一节,我们将把这个按钮元素添加到你的场景图的根中,开发按钮的代码,并完成你的播放器校对。

让我们通过将 againButton 添加到类顶部的复合按钮声明中来设置 againButton 的基础结构,然后使用. getChildren()将 againButton 添加到您的场景图根中。addAll()方法链。此处显示了执行此操作所需的 Java 代码,并且在图 23-17 的顶部用黄色突出显示:

A336284_1_En_23_Fig17_HTML.jpg

图 23-17。

Declare an againButton Button object at the top of your class and add it to your SceneGraph root object

Button ... a1Button, a2Button, a3Button, a4Button, againButton;
...

root.getChildren().addAll(gameBoard, uiLayout, qaLayout, scoreLayout, spinner, againButton);

在 createBoardGameNodes()中实例化并配置 againButton,位于 X,Y (200,-400),大小为(300,150),使用 34 号 Arial 黑色字体,如图 23-18 所示。标签为“让我们再玩一次”,因为它触发了一轮游戏。

A336284_1_En_23_Fig18_HTML.jpg

图 23-18。

Instantiate and configure againButton in the createBoardGameNodes() method and use a large size and font

因为我们不希望这个按钮在玩家选择答案后才可见,所以我们将 againButton 设置为在启动时不可见。为此,我们在 createBoardGameNodes()方法调用之后,在 start()方法的顶部使用. setVisible(false)方法 call off againButton。这看起来像下面的代码,在图 23-19 中突出显示:

A336284_1_En_23_Fig19_HTML.jpg

图 23-19。

Set your againButton visibility to false in the start() method, after the createBoardGameNodes() method

againButton.setVisible(false);

接下来,将againButton.setVisible(true); Java 语句添加到每个 Button.setOnAction()构造的末尾,以打开“让我们再玩一次”按钮的可见性,如图 23-20 中用黄色和蓝色突出显示的。

A336284_1_En_23_Fig20_HTML.jpg

图 23-20。

Call againButton.setVisible(true); at end of each answer Button event handler, after buttonClick = false;

由于我们只估计了图 23-18 中按钮的位置和大小,让我们使用运行➤项目工作流程,这样我们就可以看到按钮 UI 元素是否位于游戏板设计的四色交叉点的中心。正如你在图 23-21 中看到的,我们需要做一些调整,因为按钮在内容的象限图像上。

A336284_1_En_23_Fig21_HTML.jpg

图 23-21。

Use the Run ➤ Project work process to test the againButton code to see whether it is located and sized properly

还要注意,我们需要为按钮设置黄色背景,因为象限使用粉色、蓝色、绿色和橙色。在.``setBackground``(new``Background``(new``BackgroundFill``(``color.YELLOW``)))方法链中添加一个黄色值(不要忘记空的 CornerRadii 和 Insets),如图 23-22 中蓝色高亮显示。增加你的。将 setMinSize()设置为 300,200;将您的字体大小增加到 35;并将 X,Y,Z 重新定位到(190,-580,100)。

A336284_1_En_23_Fig22_HTML.jpg

图 23-22。

Add a Yellow background color and adjust the translate values and size values to center the againButton

使用“运行➤项目”查看最终的按钮 UI 元素样式。如图 23-23 所示,看起来很棒!

A336284_1_En_23_Fig23_HTML.jpg

图 23-23。

Use a Run ➤ Project work process to test the againButton to see if it is located, sized, and colored properly

接下来,将. setOnAction()事件处理构造添加到 againButton,以便当单击按钮时,可以关闭问答和计分(StackPane)面板,并将 buttonClick、squareClick 和 spinnerClick 变量重置为 false (off ),以便可以使用 3D 微调器、游戏板方块和回答按钮 UI 元素。这些可见性和防点击重置语句的初始 Java 代码在图 23-24 中以蓝色显示。

A336284_1_En_23_Fig24_HTML.jpg

图 23-24。

Add the .setOnAction() method call to againButton; start adding mouse click and visibility event handling

我们需要创建的下一件事是 camera animout parallel transition 动画对象,它将把 camera 对象动画显示回您的完整游戏板(旋转)视图,因为我们将调用。在 againButton.setOnAction()事件处理构造中动画对象构造的 play()方法。因此,让我们在本章下一节创建 ParallelTransition 动画对象,因为这将是一个相对复杂的任务。

相机缩小:另一个平行转换

首先,让我们创建一个与我们在本书前面创建的 rotCameraDown RotateTransition 动画对象完全相反的对象,方法是将此动画对象代码复制并粘贴到 cameraAnimIn 对象下,因为我们将要创建 cameraAnimOut 对象。除了将对象名从 rotCameraDown 改为 rotCameraBack,并在。setFromAngle()和。setToAngle()方法调用。此处显示了完成此任务的 Java 9 代码,并在图 23-25 中用黄色和蓝色突出显示:

A336284_1_En_23_Fig25_HTML.jpg

图 23-25。

Add the rotCameraBack RotateTransition object in createAnimationAssets() and instantiate and configure it for use

rotCameraBack = new RotateTransition(Duration.seconds(5), camera);
rotCameraBack.setAxis(Rotate.X_AXIS);
rotCameraBack.setCycleCount(1);
rotCameraBack.setRate(0.75);
rotCameraBack.setDelay(Duration.ONE);
rotCameraBack.setInterpolator(Interpolator.LINEAR);
rotCameraBack.setFromAngle(-60);
rotCameraBack.setToAngle(-30);

接下来,让我们创建一个与 move camera in translate transition 动画对象完全相反的对象,方法是将此动画对象代码复制并粘贴到 rotCameraBack 对象下。除了将对象名从 moveCameraIn 更改为 moveCameraOut,并将。setByZ()方法调用。这里显示了完成此任务的 Java 代码,在图 23-26 中用黄色和蓝色突出显示:

A336284_1_En_23_Fig26_HTML.jpg

图 23-26。

Create a moveCameraOut TranslateTransition Animation object and change the .setByZ() value to 175

moveCameraOut = new TranslateTransition(Duration.seconds(2), camera);
moveCameraOut.setByZ(175);
moveCameraOut.setCycleCount(1);

我们可以使用现有的 moveSpinnerOn 动画对象,该动画对象用作 spinnerAnim ParallelTransition 的组件之一,以便在 ParallelTransition 将相机带回其原始游戏板旋转位置和方向时,将微调器移回屏幕上。这将证明该动画对象可以在多个 ParallelTransition 对象中使用,这是一种编码优化,因为编码构造可以用于多个目的。在图 23-27 的顶部,您可以看到这个已经编码的动画以黄色突出显示。

A336284_1_En_23_Fig27_HTML.jpg

图 23-27。

Create cameraAnimOut ParallelTransition and reference moveCameraOut, rotCameraback, and moveSpinnerOn

因此,现在我们可以创建您的 parallel transition camera animout 对象,它将并行或完全同时播放 moveCameraOut、rotCameraBack 和 moveSpinnerOn 动画对象!这将只需要一行代码来实例化 cameraAnimOut 对象,并使用其构造函数方法来加载带有其他三个动画对象引用的对象。最后,我们将添加第二行代码,调用该对象的. setOnFinished()方法,以便在相机缩小后将 spinnerClick 布尔变量重置为 false,这样玩家就可以再次使用 i3D spinner UI 元素来随机旋转游戏板。

执行此操作的 Java 代码应该如下所示,并在图 23-27 的底部以浅蓝色和黄色突出显示:

cameraAnimOut = new ParallelTransition(moveCameraOut, rotCameraBack, moveSpinnerOn);

现在,我们可以继续完成 againButton.setOnAction()构造的代码,细化结果。

完成再次播放按钮:resetTextureMaps()

我们现在将扩展 againButton.setOnAction()事件处理基础结构中的五行代码,以便我们调用新的 camera Animation 对象和现有的 AudioClip 对象,将动画和数字音频添加到游戏的一部分,使玩家返回到缩小视图,在那里他们可以随机旋转游戏板以选择新内容来测试他们的知识库。我们还将把您的 resetTextureMaps()方法调用从 createSceneProcessing()方法中移到这个游戏重置事件处理方法中,以便在游戏结束时,就在相机从游戏板缩放回来之前(以及在播放相机缩放音频效果以匹配该动画之前),游戏板的方块和象限被重置为空白。作为此过程的一部分,我们还将隐藏 againButton 按钮 UI 元素,因为我们不希望该按钮 UI 元素覆盖我们的 i3D 旋转器和游戏板旋转以随机选择下一个象限的视图。

在 qaLayout 和 scoreLayout 可见性调用之后,在 againButton 上添加一个. setVisible(false)方法调用。接下来,添加一个 resetTextureMaps()调用和。play()在方法结束时调用 cameraAnimOut 和 cameraAudio。

用于事件处理的 Java 9 代码现在看起来应该如下所示,如图 23-28 所示:

A336284_1_En_23_Fig28_HTML.jpg

图 23-28。

Call resetTextureMaps(), cameraAnimOut.play(), and cameraAudio.play() in againButton.setOnAction()

againButton = new Button();
againButton.setText("Let's Play Again!);
againButton.setFont(Font.font( "Arial Black", 35) );
againButton.setBackground(new Background(new BackgroundFill(Color.Yellow,
                                                            CornerRadii.EMPTY, Insets.Empty);
againButton.setMinSize(300, 200);
againButton.setTranslateX(190);
againButton.setTranslateY(-580);
againButton.setTranslateZ(100);
againButton.setOnAction( (ActionEvent event) -> {
    qaLayout.setVisible(false);
    scoreLayout.setVisible(false);
    againButton.setVisible(false);

    buttonClick = false;
    squareClick = false;
    spinnerClick = false;
    resetTextureMaps();

    cameraAnimOut.play();

    cameraAudio.play();

}

现在我们可以添加。setOnFinished()事件处理构造到 cameraAnimOut 动画对象,以在相机动画返回到游戏棋盘旋转视图时自动打开 spinnerClick 功能,以便玩家可以单击 i3D spinner UI 元素重新开始游戏过程。该功能的 Java 代码在这里显示为单行代码,并在图 23-29 中以浅蓝色和黄色突出显示:

A336284_1_En_23_Fig29_HTML.jpg

图 23-29。

Add a cameraAnimOut.setOnFinsihed() event handler that sets the spinnerClick variable to a true value

cameraAnimOut = new ParallelTransition(moveCameraOut, rotCameraBack, moveSpinnerOn);
cameraAnimOut.setOnFinished( event-> { spinnerClick = true; } );

使用一个运行➤项目的工作流程,如图 23-30 所示,来测试一个完整的周期(或者两轮游戏)。

A336284_1_En_23_Fig30_HTML.jpg

图 23-30。

Use Run ➤ Project to test code; notice that clicking another quadrant square sets that quadrant image

注意图 23-30 中,我们的测试过程揭示了另一个问题!事实证明,方形点击的测试不够“深入”,不足以保证完美的游戏性!当某个象限已经“登陆”或被随机选择进行游戏时,您可以单击另一个象限的方块。这需要我们给游戏增加另一层保护,我们必须创建四个 squareClick 变量(每个象限一个)来真正彻底地保护我们的游戏。让我们在本章的下一节中修改我们的代码,使用 squareClick1 到 squareClick4 布尔变量并在每个象限的基础上进行测试来完成这个任务。

象限级保护:每象限平方点击

到目前为止,让我们更改 squareClick 代码,以适应每象限正方形检查。我们要做的第一件事是将类顶部的 squareClick 更改为 squareClick1 到 squareClick4(以匹配您的象限)。我们还需要更改 createSceneProcessing()方法中的测试,以将 squareClickN 变量与四个象限中的每一个相匹配,因此例如if(picked == Q1 S1 && squareClick1 )将被修改,而if(picked == Q2 S1 && squareClick2 )等等,如图 23-31 中突出显示的(象限 4)。这个改动的 Java 代码相当微妙,代码量很大,有些重复,这里就不一一列举了。图 23-32 显示了我提到的轻微(但重要)的修改。

A336284_1_En_23_Fig32_HTML.jpg

图 23-32。

Select and delete the squareClick = true; statement in createAnimationAssets(), as we are now moving it

A336284_1_En_23_Fig31_HTML.jpg

图 23-31。

Change your squareClick code to squareClick1 through squareClick4 to match up with the quadrant involved

从 createAnimationAssets()方法中删除 squareClick 引用,因为我们将在四个 populateQuadrant()方法中基于象限来控制方形点击,这是一个更合理的做法。

如图 23-32 所示,我选择了 createAnimationAssets()square click 语句进行删除。

正如你在图 23-33 中所看到的,我也选择了 createBoardGameNodes() squareClick = false;语句进行删除,因为我们将在你的 createSceneProcessing()方法中执行此操作。事实上,这已经显示在图 23-31 中,在屏幕截图的最右侧以黄色突出显示,这是它逻辑上的归属。

A336284_1_En_23_Fig33_HTML.jpg

图 23-33。

Select and delete the squareClick = false; statement in createBoardGameNodes(), as we’ve already moved it

您希望将四个 squareClick 变量设置为 true(允许在这个象限的正方形上单击)的地方是在每个 populateQuadrantNumber()方法调用的末尾,以完成设置。这允许单击其中一个方块,以便为该象限的主题选择内容。

squareClick1 变量位于 populateQuadrantOne()的末尾,squareClick2 变量位于 populateQuadrantTwo()的末尾,squareClick3 变量位于 populateQuadrantThree()的末尾,squareClick4 变量位于 populateQuadrantFour()的末尾。

现在有一个 squareClickN 变量与四个象限中的每一个都相关。这更好地匹配了游戏模式,因为现在我们可以有选择地只在玩家所在的游戏棋盘象限打开鼠标点击,而在其他三个象限关闭游戏棋盘方块。这将防止图 23-30 中未被发现的测试,其中未被随机数发生器选择的象限仍然可以被播放。由于这在视觉上看起来不正确(如您所见),我们将通过逐个象限地关闭方块来解决这个问题,这将解决这个问题,尽管需要更复杂的防玩家 Java 代码。

populateQuadrantOne()方法体的 Java 代码如下所示,在图 23-34 的底部用浅蓝色和黄色突出显示:

A336284_1_En_23_Fig34_HTML.jpg

图 23-34。

Add a squareClick1 = true; statement at the end of populateQuadrantOne() and the other three in the other three methods

private void populateQuadrantOne() {
    pickS1 = random.nextInt(3);
    if (pickS1 == 0){diffuse1 = new Image("/gamesquare1bird0.png", 256, 256, true, true, true);}
    if (pickS1 == 0){diffuse1 = new Image("/gamesquare1bird1.png", 256, 256, true, true, true);}
    if (pickS1 == 0){diffuse1 = new Image("/gamesquare1bird2.png", 256, 256, true, true, true);}
    Shader1.setDiffuseMap(diffuse1);
    ...
    squareClick1 = true;
}

做同样的平方 clickn = true;Java 语句来打开 squareClick 函数,一旦为当前一轮游戏选择了一个方块,该函数将被关闭。如图 23-35 所示,游戏现在可以正常运行了。您可以单击与当前象限无关的微调按钮和方块,这些单击将被忽略,就像单击第一个应答按钮之后的任何按钮一样。这种“错误检验”或“玩家检验”使得 i3D 游戏更加专业。

A336284_1_En_23_Fig35_HTML.jpg

图 23-35。

Use Run ➤ Project and test the final error-proofing code and Play Again User Interface

恭喜你,基本的游戏已经完成,我们可以看看优化和剖析。

摘要

在第二十三章中,我们学习了如何创建玩家验证逻辑来强制正确使用 i3D 旋转器 UI、游戏板方块和回答按钮 UI 元素。这包括使用大约六个布尔变量,它们被用作“标志”,以关闭玩家在每轮游戏中多次点击 UI 元素的能力。我们保护了 i3D spinner UI,按钮 UI answers,以及每个象限的游戏棋盘方块,防止它们被“误用”到游戏系统中并累积未得分数。这是 pro Java 9 游戏设计和开发的一个重要部分,以确保您的游戏逻辑以预期的方式运行。

我们还在本章的第一部分完成了游戏内容的添加,添加了近 600 行 Java 代码,并将当前的 pro Java 9 游戏开发项目增加到近 1750 行 Java 代码。

在第二十四章中,您将了解游戏优化、使用 NetBeans 9 的评估以及 NetBeans 9 Profiler。

二十四、使用 NetBeans 优化游戏素材和代码以及游戏分析

现在你的游戏运行了,玩家一次点击(回合)使用它,我们可以看看它使用了多少内存,所有这些素材有多大。我们还可以想办法将数字音频和图像素材缩小 2 到 4 倍。首先使用 GIMP 进行数据占用优化,然后使用 NetBeans 9 Profiler 对当前的 24 位图像素材和 CD 质量的 16 位 44.1KHz 音频素材进行分析。通过这种方式,我们可以看到高端多媒体素材是否占用了太多的内存和 CPU 开销,或者我的开发系统是否处理得很好,我的开发系统是沃尔玛的一款老式 4GB Win7 Acer 四核微塔式机(几年前售价 300 美元)。我一直在这个系统上使用 NetBeans 9 开发 Java 9,没有发生任何事故。目前的系统是六核或八核,内存为 8MB 或 16MB,因此 Java 9 开发可以在旧系统上轻松完成,不需要像 Unity、Android 或 Lumberyard 等其他 i3D 平台那样需要先进的系统。

在本章中,我们将转换您的数字图像资源,以使用 8 位(索引)颜色而不是 24 位“真实”颜色来创建纹理贴图,并且我们将运行 NetBeans Profiler 来查看您的 Java 代码在运行游戏时使用了多少内存和 CPU 处理。

优化纹理贴图:转换为 8 位颜色

目前,您的 source ( /src/)文件夹中的数字图像素材大约为 24MB,即 24,000,000 字节,这对于一个有 120 个不同图像的 i3D 棋盘游戏来说是相当不错的(平均每个图像大约 200KB)。然而,如果我们能把它压缩到 10MB(每张图片 84KB),这将会大大减少我们游戏发行包的大小。实现图像“重量”或大小减少 300%到 400%的方法是使用 8 位颜色(索引色)以及“抖动”或点图案,用于模拟比用于表示索引图像的最大 256 色更多的颜色。小到中等的纹理贴图,这正是我们在游戏棋盘的正方形和象限中使用的,可以很好地处理索引色。这样做的原因是因为抖动可以被放大(近距离)看到,但当图像被观看得更远时(从远处看或缩小),这种视觉效果就会消失。我将在本章的这一节向您展示这一点,我们将把所有 120 个图像素材从 24 位转换为 8 位索引颜色。

创建索引颜色纹理:在 GIMP 中更改颜色模式

让我们以这样一种方式优化我们的图像素材,即我们不必对我们的 Java 代码做任何重大的改变。为了保持我们的 Java 代码不变,我们将使用相同的文件名,并将它们放在不同的文件夹中,在/src/ called /8bit/下。因此,我们将拥有索引颜色资源的/src/8bit/ path 和 24 位高质量资源的/filename 路径。使用操作系统文件管理实用程序在当前/src 文件夹下创建一个名为/8bit/的文件夹(目录),其中包含原始的真彩色图像资源。图 24-1 显示了这个新文件夹。

A336284_1_En_24_Fig1_HTML.jpg

图 24-1。

Create the /JavaFXGame/src/8bit/ folder to hold optimized versions of your 120 texture map image assets

使用文件➤打开在 GIMP 中打开第一张 gamequad1bird0.png 纹理贴图图像,然后使用图像➤模式➤索引菜单序列将 24 位颜色空间(颜色模式)转换为 8 位,如图 24-2 所示。这将打开“索引颜色转换”对话框,允许您选择多种颜色和一种抖动算法。8 位模式将位数减少了 300%或更多,抖动算法模拟了超过 256 种颜色。

A336284_1_En_24_Fig2_HTML.jpg

图 24-2。

Use a File ➤ Open menu sequence to open a texture map and use Image ➤ Mode ➤ Indexed to convert it to 8-bit

我使用了最大允许的 256 种颜色(0 到 255),方法是选择生成最佳调色板单选按钮,如图 24-3 中最左边的对话框所示,以及正常的 Floyd-Steinberg 颜色抖动算法。然后我点击对话框右下角的转换按钮。要将 8 位图像导出到/src/8bit/文件夹,使用 GIMP 文件➤导出为菜单序列,双击 8 位文件夹(在图 24-3 的第二个面板中高亮显示),点击导出按钮(保持 24 位文件名不变,如第三个面板中高亮显示)。

A336284_1_En_24_Fig3_HTML.jpg

图 24-3。

Set the conversion to 256-color Floyd-Steinberg, convert, and save in the /src/8bit folder with the same file name

正如你在图 24-4 中看到的,如果我们将一幅图像(gamequad1bird1.png)转换成 8 位索引色后放大到第二象限,你可以清楚地看到背景、鸟嘴和钢箍中的抖动。有趣的是,当你使用图像作为纹理贴图(缩小)时,你看不到这种抖动!当我们在 Java 代码中实现这些变化时,我将在本章的后面向您展示这一点(在图 24-26 和 24-27 )。

A336284_1_En_24_Fig4_HTML.jpg

图 24-4。

Click the Magnify Glass (Zoom) Tool and zoom in 300 percent (three times) to see the color dithering algorithm

正如你在图 24-5 中看到的,你的真彩色(24 位)图像在质量上是原始的,但是使用了数倍的数据。当缩小时(用作纹理贴图),两个图像看起来几乎相同,这就是为什么我们将您的 24 位图像转换为 8 位图像,因为我们可以从 24MB 的数字图像素材增加到不到 8MB,而几乎没有损失 i3D 游戏板纹理贴图的感知质量,至少从玩家的角度来看是这样。

A336284_1_En_24_Fig5_HTML.jpg

图 24-5。

Undo the indexed color, click the Magnify Glass Tool, and again zoom in 300 percent to see the (original) true-color data

使用如图 24-6 所示的文件➤关闭对话框,在将索引图像文件保存到/src/8bit 文件夹后关闭该文件。因为您从/src 文件夹中打开了 24 位文件,所以您希望确保单击“放弃更改”,这样您将得到原始的 24 位 PNG24 文件和新导出(保存)的 8 位 PNG8 文件,它们使用相同的名称,但保存在不同的文件夹中。这一点很重要,你要注意 120 次,这样你就有 120 个 PNG24 文件和 120 个 PNG8 文件在不同的目录中。要更改这些图像的引用,您只需将/8bit/filename.png 路径更改添加到您已创建的索引颜色素材文件夹名称中,然后 i3D 游戏将使用这些较小的文件大小来纹理映射您的游戏棋盘方块和象限。

A336284_1_En_24_Fig6_HTML.jpg

图 24-6。

Click Discard Changes to keep a 24-bit version

如图 24-7 所示,第一象限完成,8 位文件大小从 76KB 到 104KB 不等。

A336284_1_En_24_Fig7_HTML.jpg

图 24-7。

The first quadrant texture maps have all been reduced more than 300 percent and still look fantastic as texture maps

正如你在图 24-8 中看到的,我们已经将象限纹理贴图数据从 4MB 减少到 1.33MB。

A336284_1_En_24_Fig8_HTML.jpg

图 24-8。

Preview data reduction in File Explorer

正如你在图 24-9 中看到的,我继续减少所有 60 个图像素材的象限纹理贴图。

A336284_1_En_24_Fig9_HTML.jpg

图 24-9。

Go into the /src/8bit folder, select all 60 images, right-click the selection, and open Properties

正如你在图 24-10 中看到的,我现在也已经为你的游戏棋盘方块完成了这些 8 位图像。

A336284_1_En_24_Fig10_HTML.jpg

图 24-10。

Go into the /src/8bit folder and select all 60 images; right-click the selection and open Properties

如图 24-7 、 24-9 和 24-10 所示,这些索引彩色图像越小,它们看起来就越像真彩色图像,尽管在许多情况下它们要小几倍(三到四倍)!在本章的第一部分,我们将介绍如何将图像优化为 8 位(索引)颜色,因为这是减少分发文件数据占用空间(代码和素材包中图像素材的大小)的有效方法。

其中一些图像,例如红色的柿子椒,将非常适合索引色,因为红色光谱、白色背景和绿色边框颜色可以非常接近于仅使用 256 种颜色和紧密匹配的颜色之间的微妙抖动来表示真彩色图像,这在放大时甚至看不到。图 24-11 显示了象限纹理贴图(左半部)和方形纹理贴图(右侧)的真彩色和索引图像结果(来自图 24-9 和 24-10 所示的选择类型)。

A336284_1_En_24_Fig11_HTML.jpg

图 24-11。

Right-click the selected square and quadrant images in both folders; use Properties to preview the optimization

我们已经将游戏棋盘象限纹理贴图的数据占用空间从 17,041,285 字节减少到 6,208,570 字节,减少了 10,832,715 字节。这意味着象限图像的数据占用空间减少了 65 %(三分之二)。这是 512 像素的正方形,对于一个 i3D 游戏来说是相当大的(高质量),所以 60 张图片的 6MB 是很好的质量,每象限图片大约 100KB,正如你在 GIMP 中已经在图 24-7 中看到的。

我们还将游戏棋盘正方形纹理贴图的数据占用空间从 4,516,845 字节减少到 1,701,334 字节,减少了 2,815,511 字节。游戏板正方形图像的数据占用空间减少了 63%。这些是 256 像素的正方形,这是 i3D 游戏的主流(高质量),因此 1.7MB 的 60 张图像是很好的质量,每个游戏棋盘正方形图像大约 28KB,或者每个游戏主题选择大约 128KB 的图像数据。

要引用这些优化的素材,只需在 Java 代码中的文件名前添加/8bit/ path,这将在我使用原始的 24 位数字图像素材和 CD 质量的数字音频素材对当前代码进行分析之后进行。始终使用最高质量的素材来分析您的代码,以便您可以看到内存和 CPU 周期是否受到过大的新媒体元素的影响(数据占用方面)。就职业 Java 游戏而言,这是 NetBeans profiler 将告诉您的大部分内容。是的,你的 Java 逻辑很重要。无限循环问题会很快在 profiler 中出现,但非优化的动画对象构造、过大的纹理贴图、过长的数字音频声音效果、没有很好优化的数字视频以及使用过多多边形(过多几何体)的 i3D 资源也会出现。这就是为什么我们在本书前三分之一的时间里研究了各种新媒体的概念和原理,因为新媒体的优化影响了游戏的玩法。

NetBeans 9 Profiler:测试内存和 CPU 使用率

要调用 NetBeans 9 Profiler,只需使用 Profile 菜单和位于该菜单顶部的 Profile 项目(JavaFXGame)选项,如图 24-12 所示。还显示了 40 个自定义方法、必需的 start()和 main()方法,以及自从创建 JavaFXGame 引导应用以来我们添加的 1700 行 Java 9 代码。NetBeans 9 分析会话可以显示程序执行期间计算机上发生的大量复杂的“幕后”操作,以及与服务器的交互,甚至 SQL 数据库访问模式。因此,在本章中,我们不会涉及 NetBeans 9 分析系统的所有功能;但是,如果您对 Java 软件概要分析感兴趣,您当然应该利用自己的时间,在各种 64 位工作站上使用您的其他 Java 9 软件开发项目,探索和试验概要分析器选项。

A336284_1_En_24_Fig12_HTML.jpg

图 24-12。

Invoke a Profiler ➤ Profile Project (JavaFXGame) menu sequence to start a NetBeans profiling session

一旦您第一次调用了分析器,您将在 IDE 中得到一个选项卡,其中包含了分析 UI 和生成的分析器数据 UI,如图 24-13 所示。JavaFXGame 选项卡有一个性能分析图标,左上角有一个配置会话下拉菜单 UI 元素,还有一个配置和启动性能分析指令序列,它将概述性能分析程序选项类型,并准确地告诉您如何选择要使用的选项。

A336284_1_En_24_Fig13_HTML.jpg

图 24-13。

Once you invoke the NetBeans 9 profiler, you’ll get a JavaFXGame Profiling tab and configuration instructions

我们将首先查看遥测分析模式,因为这向我们展示了游戏如何使用系统内存和 CPU 周期,以及线程、类和垃圾收集如何影响游戏在您的开发系统上的运行。这是大部分关键的游戏代码处理信息,我们希望首先查看这些信息,以确保您的 i3D 棋盘游戏以最佳方式(即高效地)使用 Java 9 和 JavaFX 9。

单击选项卡窗格左上角的配置会话 UI 选择器旁边的向下箭头,并选择遥测选项(在图 24-14 中以浅蓝色突出显示),以启动 NetBeans 遥测分析会话。保持默认的“使用已定义的分析点”选项处于选中状态,以允许 NetBeans 9 最初为您配置此分析会话。如果发现了异常情况,您可以在稍后的分析会话中设置定制的分析点,以进一步尝试确定 Java 游戏代码有什么问题。现在让我们希望我们在本书中对以最佳方式做事的关注得到了回报。无论哪种方式,NetBeans profiler 都会揭示这一点!

A336284_1_En_24_Fig14_HTML.jpg

图 24-14。

Drop down the Configure Session menu and select the Telemetry option to profile your memory and CPU

然后,JavaFXGame 分析窗格将显示 CPU(和垃圾收集)图形的 UI 基础结构、(系统)内存实时使用图形、垃圾收集处理图形以及线程和类图形,如图 24-15 所示。还没有收集到数据,因为还没有使用 Profile 项目图标激活(启动)概要分析,该图标显示在图的顶部,带有淡黄色的弹出描述符。

A336284_1_En_24_Fig15_HTML.jpg

图 24-15。

Once you click the Profile Project Icon or Menu Item, the JavaFXGame Profile pane will populate with empty UI elements

单击您的 Profile 项目图标,您将得到“Profiler 现在将执行您的机器和目标 JVM 的初始校准”消息,如图 24-16 所示,位于五个对话框的最左边。请记住,NetBeans Profiler 正在分析您的系统和 Java 9 JVM,因此,如果您在 8 核、12 核或 16 核计算机上进行分析(比如一个新的 AMD 锐龙 5 或 7 系统,具有 16GB 的 DDR4-2400),您将获得与我在只有 4GB DDR 3-1333 内存的旧四核 Acer AMD 3.11GHz 系统上获得的结果不同的结果。我使用这样一个旧的 Windows 7 系统的原因是为了展示 Java 9 和 NetBeans 9 有多么优化,这样你就可以使用一台不能用于 Amazon Lumberyard 或 Android Studio 3.0 或 Unity development 的计算机来开发一个专业的 JavaFX i3D 游戏。

A336284_1_En_24_Fig16_HTML.jpg

图 24-16。

Once you start a profiler, you’ll get a series of dialogs for calibrating and configuring this profiling process

如果出现 Windows 7 防火墙对话框,点击允许访问按钮,如图 24-16 第二个对话框所示。然后选择显示此校准数据的详细信息,并单击确定按钮继续。您将看到一个对话框,其中显示了一些已获得的校准数据。单击该对话框的“确定”按钮后,您将看到一个“连接到目标虚拟机”对话框,其中显示了一个进度条,指示 NetBeans 9 IDE 将游戏代码和内容加载到系统内存中,以便执行校准,并最终收集和显示性能分析数据。

A336284_1_En_24_Fig17_HTML.jpg

图 24-17。

An Output Pane will open, showing your Java 9 code being run in the NetBeans Profiler Agent Utility

如图 24-17 所示,接下来你会看到的是执行游戏 Java 代码的输出面板。

关闭输出窗格,再次显示 Profiler 遥测 UI。游戏和剖析程序现在应该共享屏幕了。您在游戏中所做的任何事情都会实时反映在这些 NetBeans Profiler 遥测面板中,如图 24-18 所示。我使用红色 Arial 文本注释接下来的五个图形,以阐明我正在测试游戏的五个主要阶段中的哪一个,您可以从 profiler UI 数据中看到 3D 动画、音频回放、纹理贴图加载(或卸载)、事件处理和 Java 代码处理在 CPU 处理(百分比)开销、系统内存使用(大部分将用于加载数字图像或保存和播放数字音频, 以及保存我们用来进行 3D 建模、3D 纹理、3D 动画和音频的 JavaFX API 类)、垃圾收集、线程使用和(单个游戏原型)类使用。

A336284_1_En_24_Fig18_HTML.jpg

图 24-18。

The Animation object moving a rotating 3D spinner UI onto the screen uses 0 to 5 percent of the CPU’s capacity

正如你在图 24-18 中看到的,将你的 i3D spinner UI 移动到屏幕上只使用了百分之几的 CPU,只有一两秒钟,所以这似乎是很好的编码。一旦 i3D 微调器“着陆”在屏幕上,点击它的用户界面,其分析数据如图 24-19 所示,就 CPU 使用而言,看起来也是高度优化的。请注意,可以在垃圾收集窗格和线程和类窗格中看到着陆的游戏棋盘象限(五个)正方形图像的数量。当您的随机数生成时,此活动会达到峰值,五个游戏方格会加载随机选择的图像素材,然后这些素材会被放入系统内存中。

A336284_1_En_24_Fig19_HTML.jpg

图 24-19。

The board spin uses garbage collection to load images into memory and threads to pick a random number

选取一个方块会调用垃圾收集来加载象限图像,当选取一个方块时,CPU 线程处理会出现峰值,代表垃圾收集、事件处理、问答和分数处理,如图 24-20 所示。

A336284_1_En_24_Fig20_HTML.jpg

图 24-20。

A square pick uses garbage collection to load imagery into memory and uses threads to display UI panels

另一方面,选择答案会调用零垃圾收集来加载影像,如图 24-21 所示。它使用很少(几乎没有)CPU 开销来增加乐谱面板和显示文本资源。

A336284_1_En_24_Fig21_HTML.jpg

图 24-21。

Picking an answer (Button) involves the least amount of overhead and just minor CPU overhead for scoring

使用“让我们再玩一次”按钮对象重置游戏,其所有处理使用的开销与 3D 棋盘旋转(图 24-19 )一样多,如图 24-22 所示。垃圾收集重置所有纹理贴图,相机动画改变游戏视角,然后事件处理锁定不需要的点击,音频回放播放相机动画音频效果,类似的处理密集型代码重置另一轮游戏。

A336284_1_En_24_Fig22_HTML.jpg

图 24-22。

Clicking a Let’s Play Again Button object invokes a second flurry of CPU and memory use for special effects such as audio and animation

考虑到这包括图像数据、CD 质量的音频效果和 JavaFX 动画(过渡)类,以及 AudioClip、图像、StackPane、按钮、3D 图元、文本、(SceneGraph)节点和实用程序(Inset、Color、Pos 等), 60MB 的内存使用率也相当不错。)用法,所有这些都利用了我们用来构建这个 i3D 专业级 Java 9 游戏的 JavaFX 类。

这些内存开销中很少一部分可以直接归因于您编写的 1,700 行 Java 代码,这些代码是为了组装这个游戏而编写的;99%的内存使用可归因于加载新的媒体素材和许多 JavaFX 9 类,这些类访问和运行这些新的媒体素材。

正如你在图 24-23 中看到的,一旦你完成了对你的 Java 9 游戏的分析,你将得到一个信息对话框,显示“被分析的应用已经完成执行”单击 OK 终止 VM,您将看到一个摘要方框(黑色),显示分配的内存堆大小(71MB)和运行性能分析会话所使用的内存总量(66MB)。如果您认为 66MB 是很大的内存,请考虑这台机器有 4,096MB 的内存,66MB 代表 1.6%的内存。许多现代智能手机、iTV 电视机、平板电脑和笔记本电脑都有 8GB 的系统内存,因此整个游戏生态系统和基础设施只占不到 1%的系统资源。在 2GB 的智能手机或(古老的)计算机系统上,这将代表大约 3%的系统资源。这对于一个动画的、交互式的 3D 棋盘游戏来说是相当不错的,所以 Java 做得非常好!

A336284_1_En_24_Fig23_HTML.jpg

图 24-23。

Once you are finished profiling, NetBeans will give you a memory used summary and Information dialog

接下来,让我们看看如何优化我们的 Java 代码,因为我用来写这本书的代码就是我所说的“原型”代码。它在技术上是正确的,但是(还)没有利用任何可能实现高级 Java 语言语法或特性(如数组或哈希表)的 Java 编码结构。这样做的原因是,我试图帮助新的游戏开发者和程序员“可视化”Java 游戏逻辑(代码)在他们头脑中正在做的事情,而最简单的方法是以一种显示代码试图做什么的方式可视化地编码它。

请注意,Java 9 的编译、构建和执行过程也将做大量的工作来“在幕后”优化这段代码,因为前面关于概要分析的部分将展示这段 Java 9 代码在没有以任何方式进行专门的“程序员优化”的情况下是如何优化的。此外,从一个程序员到下一个程序员,有许多不同的方法可以做到这一点,因此我想更多地关注 JavaFX 游戏 API、游戏设计和开发工作流程以及游戏素材开发,而不是标准的 Java 代码优化,后者在数百本其他 Apress 书籍中有很好的介绍。

在 Java 9 游戏代码优化思想这一章的后面部分,我们将会看到一些 Java 代码优化。首先,让我们在游戏中完成 8 位索引图像资源的实现,并播放它,看看索引色 PNG8 资源和真彩色 PNG24 图像资源之间是否有任何视觉差异。

实现索引颜色影像:添加路径

将图像资源从真彩色更改为索引色就像将/8 位路径添加到 populateQuadrant()方法和 setupQSgameplay 方法一样简单。对于 loadImageAssets()和 resetTextureMaps()方法,您不必这样做,因为这些方法使用没有索引的纹理贴图,因为它们已经很小(几千字节),并且可以保持为真彩色图像。这是因为它们不包含数字图像,因为这些是空白纹理,用于在每一轮游戏旋转之前使游戏板看起来空无一物。我截图了 populateQuadrantOne()方法显示添加的/8 位路径,如图 24-24 所示。

A336284_1_En_24_Fig24_HTML.jpg

图 24-24。

Add an /8bit path in front of the current image file name reference to point to your new indexed imagery

你需要把这个相同的/8 位路径添加到你的 20 种设置游戏性的方法中的数字图像引用的前面。这些是为每个象限(Q)和正方形(S)命名的,作为您的 setupQSgameplay()方法,我在自定义的 40 个方法和 2 个必需方法(start()和 main() Java 方法)的末尾留了下来。

我截取了第一个 setupQ1S1gameplay()方法的截图,以显示我已经在数字图像引用的前面安装了/8bit 路径。我这样做是为了让您的新 8 位(索引色)PNG8 数字图像将被用作棋盘游戏的纹理贴图,而不是我们一直使用的 24 位真彩色数字图像。

我们这样做是为了让我们可以使用“运行➤项目”工作流程来测试您的游戏,看看在使用 325%的小索引彩色图像而不是真彩色 24 位 PNG 图像玩 i3D 游戏时,是否有任何视觉差异。图 24-25 显示了 20 种方法中的第一种,必须通过在数字图像参考名称的前面(头部)添加一个/8 位文件夹路径来进行“路径修改”。要轻松做到这一点,只需复制/8 位路径一次,并在 populateQuadrant()方法中粘贴 60 次,在 setupQSgameplay()方法中粘贴 60 次。

A336284_1_En_24_Fig25_HTML.jpg

图 24-25。

Add an /8bit path in front of the current image file name reference to point to your new indexed imagery

接下来,让我们使用图 24-26 所示的 Run ➤项目工作流程,看看游戏板在随机旋转后看起来是否相同。正如你所看到的,它看起来和我们在整本书中使用真彩色图像时几乎一模一样。我们需要做的下一件事是放大,看看 8 位图像如何支撑。

A336284_1_En_24_Fig26_HTML.jpg

图 24-26。

Use a Run ➤ Project work process; spin the game board to see whether the new 8-bit color images look the same

点击带有颜色渐变的图像,在本例中是旧金山海湾大桥,如图 24-27 所示。这将向我们展示大象限图像,以便我们可以看到是否有任何抖动模式。

A336284_1_En_24_Fig27_HTML.jpg

图 24-27。

Select an image that will show dithering to zoom the game board in closer to see whether the image looks the same

正如你所看到的,当它被用作 i3D 游戏板上的纹理贴图时,没有可见的抖动图案(点状伪像),因此这表明我们可以在这个 i3D 游戏板上成功地将索引色用于纹理贴图,而不会遭受任何可感知的交付质量下降。这给出了一个专业的结果,每样东西看起来都像是使用了真彩色,直到每个象限中的钢圈,它看起来仍然像钢一样,没有任何可察觉的抖动。接下来,让我们看看如何优化您的 16 位数字音频素材。

优化音频:以较低的采样率使用 16 位

我们在如何使用 Audacity 2.1.3 优化数字音频方面做得很好,所以我建议使用 16 位音频采样分辨率并优化采样率(48、44、32、22、16、11 或 8kHz),直到您听到质量变化。我们已经有 16 位 44.1 kHz(目前使用)和 16 位 22.05 kHz,这是我们每个音效样本的一半数据,但听起来质量非常相似。如果您想使用更小的数字音频内存,您可以简单地在 loadAudioAssets()方法体中引用更优化的音频资源。

在这一点上,我将把这个内存与质量的决定完全留给您。如果您想回到 Audacity 并优化其他三个采样速率,如 THX (48 kHz)或 32 kHz,甚至 16 kHz,您可以聆听每个采样速率产生的 16 位音频质量,并决定每个数字音频质量级别需要使用多少系统内存。

请注意,您可以对游戏中的每个数字音频资源使用不同的采样速率。一些声音效果将保持较低的采样速率(11 和 16 kHz),而其他声音效果(音乐、声乐)可能需要较高的采样速率(22 和 32 kHz)。不过,我建议全面使用 16 位采样分辨率,因为它更适合内存,因为内存“分块”为 8 位、16 位和 32 位,而您想要无缝匹配。

Java 游戏代码优化:利用 Java 技巧

您可能已经注意到,在本书的整个过程中,当我们设计和构建我们的 pro Java 9 游戏时,我一直在使用长格式的、易于阅读(和理解)的 Java 代码。这段代码是合法的,可能会被 Java 9 编译和构建(以及执行)软件阶段优化,所以它不是“坏”代码,但是有一些优化过程和构造会使它明显更短、更精简。为一本书完成这个过程的问题是,每个 Java 程序员都有不同的首选方法,那么我应该选择哪种方法呢?还是我展示了大部分?不幸的是,这本书的页数是固定的,涵盖了新媒体素材开发、游戏设计、开发和测试,以及类似的需要掌握的广泛主题,以便成为一名专业的 Java 9 游戏开发人员。在这一章的最后一节,我将介绍一些其他的东西,你可能想自己添加到这个棋盘游戏中,用你所学到的东西进行一些练习。

让我们先看看 populateQuadrant()方法代码。populateQuadrantOne()方法以下面的 Java 序列开始,象限 1 中的五个游戏棋盘方格各有一个这样的构造:

pickS1 = random.nextInt(3);
if (pickS1 == 0) { diffuse1 = new Image("/gamesquare1bird0.png", 256, 256, true, true, true); }
if (pickS1 == 1) { diffuse1 = new Image("/gamesquare1bird1.png", 256, 256, true, true, true); }
if (pickS1 == 2) { diffuse1 = new Image("/gamesquare1bird2.png", 256, 256, true, true, true); }
Shader1.setDiffuseMap(diffuse1);

您将通过用以下代码替换它来简化这段代码(一旦您知道它可以工作,在原型化之后),这也消除了条件 if() CPU 处理和内存开销:

pickS1 = random.nextInt(3);
diffuse1 = new Image("/gamesquare1bird" + pickS1 + ".png", 256, 256, true, true, true);
Shader1.setDiffuseMap(diffuse1);

的确,这使得更难看到你在游戏代码中做了什么,但是代码以相同的方式运行,并且几乎是代码行数的一半,允许你将 populateQuadrantN()方法从 26 行代码减少到 16 行代码,这减少了 38%的代码,或者所有四种方法减少了 40 行代码。

接下来,考虑第十九章中的以下代码块,它可能需要 17 到 25 行代码来编写:

if (picked == spinner) {
    int spin = random.nextInt(4);
    if (spin == 0) {
        rotGameBoard.setByAngle(1080); rotSpinner.setByAngle(-1080); spinDeg += 1080;
    }
    if (spin == 1) {
        rotGameBoard.setByAngle(1170); rotSpinner.setByAngle(-1170); spinDeg += 1170;
    }
    if (spin == 2) {
        rotGameBoard.setByAngle(1260); rotSpinner.setByAngle(-1260); spinDeg += 1260;
    }
    if (spin == 3) {
        rotGameBoard.setByAngle(1350); rotSpinner.setByAngle(-1350); spinDeg += 1350;
    }
    rotGameBoard.play();
    rotSpinner.play();
    calculateQuadrantLanding();
}

下面的 Java 代码块是基于数组的,相当于前面的代码。它要短得多,只有 10 行 Java 9 代码,减少了 41%到 60%(取决于如何在 if (spin == n) { … })中编写代码):

if (picked == spinner) {
    int spin = random.nextInt(4);
    double[] angles = { 1080, 1170, 1260, 1350 };

    rotGameBoard.setByAngle(angles[spin]);
    rotSpinner.setByAngle(-angles[spin]);
    spinDeg += angles[spin];
    rotGameBoard.play();
    rotSpinner.play();
    calculateQuadrantLanding();
}

在计算出您的随机旋转值之后,这个代码片段声明了一个双精度值的四元素数组,它表示象限着陆角度。然后,我使用 spin 值(random.nextInt(4)输出四个随机象限值中的一个,范围从 0 到 3)来访问一个角度值(通过 angles[spin]),该角度值被传递给 setByAngle,并且也被添加到 spinDeg 变量中。

请注意,如果 spinDeg 是 int (32 位整数)类型,则必须在赋值前将 double angle 值转换为(int ),否则将面临 Java 编译器错误。在这种情况下,您可以用 Java 9 代码spinDeg += (int) angle[spin];替换spinDeg += angle[spin];来避免这个 Java 编译器错误。

如果您不想三次指定 angles[spin],也可以将该值存储在一个 angle 变量中,并使用这个 double angle 变量,如下面的 Java 代码所示:

if (picked == spinner) {
    int spin = random.nextInt(4);
    double[] angles = { 1080, 1170, 1260, 1350 };
    double angle = angles[spin];

    rotGameBoard.setByAngle(angle);
    rotSpinner.setByAngle(-angle);
    spinDeg += angle;
    rotGameBoard.play();
    rotSpinner.play();
    calculateQuadrantLanding();
}

正如您所看到的,有许多方法可以为这个游戏编写 Java 代码,这将减少使用的代码行,甚至可能稍微减少游戏使用的 CPU 的几个百分点,如本章的 NetBeans 9 性能分析部分所示。由于每个人都有自己的编码优化风格和方法,我将把 Java 9 代码优化留给您,并利用书中材料的(较长的)原型代码。这将让您更好地了解我在游戏设计和开发工作流程中对新媒体素材所做的工作,并将本书内容集中在使用 Java 9 及其强大的 JavaFX 9 API 进行专业游戏设计和开发上。

最后,让我们在本章中增加一个章节,看看我们还可以利用哪些 JavaFX 9 API 类来进一步扩展这个 i3D 游戏,就像你最终会做的那样。您可以从第三方导入程序包中导入 i3D 模型(不幸的是,这还不是 JavaFX 的“原生”部分,所以我在本书中坚持使用 JavaFX i3D APIs)并添加数字视频素材,只要您仔细优化它们。因为 JavaFX 9 模块(分发包)还没有完全完成(离 Java 9 发布还有一个月,或者更久)。一旦 Oracle 发布了该书,包含 JavaFX 9 模块(游戏分发包)的附录将作为该书可下载源代码的一部分提供。要下载这本书的源代码,导航到 www.apress.com/9781484209745 并点击下载源代码按钮。

未来扩展:添加数字视频和 3D 模型

通过使用第三方网站 InteractiveMesh.org 的 i3D 导入软件,以及添加使用 Black Magic Design 的 DaVinci Resolve 14 等创建的数字视频资源,您可以向您的 i3D 棋盘游戏添加更复杂的新媒体。你可以使用一些专业的东西来优化你的视频,比如 Sorenson Media 的 Squeeze Desktop Pro 11。这将为您提供更多使用 JavaFX 9 更高级的数字视频和 3D APIs 的经验。

我下一步要做的事情之一是为指令、信用、法律信息等提炼 2D 启动代码。既然一个游戏已经有了原型,那么重温一下闪屏图形可能也是一个好主意。请记住,Pro Java 9 游戏开发,尤其是 i3D 游戏,是一个改进的过程,因为组成 i2D 和 i3D 游戏素材的数百个新媒体组件通常经过改进,以使游戏符合游戏开发者 artisan 的愿景。

一旦你完成了游戏原型,你就可以像我们之前提到的那样进行代码优化,如果有必要的话,甚至可以为不同的特性或功能创建不同的类。我的技术编辑同意我的观点,这个游戏不需要额外的类,因为我试图使用 JavaFX API 中已经编码的类来创建一个 i3D 棋盘游戏。事实上,我正在导入(使用)44 个 Java 或 JavaFX 类来创建这个游戏,所以仅仅因为我有一个 JavaFXGame 主类将所有的东西绑在一起,实际上就有 45 个类创建了这个游戏。其中 44 个已经由 Sun Microsystems 创建、编码和优化,后来由 Oracle 在收购 Sun 后创建、编码和优化。我在本书中试图做的是展示如何创建一个专业的 Java 9 游戏,通过简单地优化使用 JavaFX API 类来利用这些公司在过去十年中的所有工作,并最大限度地减少开发人员创建一个 i3D 棋盘游戏的实际工作量。随着 Oracle 继续改进这些类,JavaFX 9 将继续成为一个更加强大和令人印象深刻的游戏引擎,理想情况下,iOS 和 Android 8 支持将继续发展和改进。

摘要

在最后的第二十四章中,我们讨论了各种素材(数字图像和数字音频)的优化,以及 Java 代码的优化。我们学习了 NetBeans Profiler,以及如何查看运行我们的游戏使用了多少系统内存。我们还查看了有多少百分比的 CPU 被用来处理我们的 Java 代码,以及垃圾收集何时将我们的纹理贴图加载到系统内存中。我们还研究了线程何时被用于处理内存位置、指令、循环、随机数生成以及类似的 Java 代码指令。

我们还研究了其他一些我们可以在游戏中改进并在未来添加到游戏中的东西,以便进一步利用 JavaFX 9 类。只是要确保使用 Profiler 来监视系统内存和 CPU 的使用情况。我使用我的一个“弱”(4GB,四核 AMD 3.11GHz 宏基)工作站,所以我在一台“次主流”计算机上测试代码,同时我有足够的能力和内存来流畅地运行 NetBeans 9、Java 9 和 JavaFX 9。这证明了 NetBeans 9、Java 9 和 JavaFX 的效率。

我希望您喜欢这二十几章,它们涵盖了新媒体素材开发以及 Java 9 和 JavaFX 9 游戏开发,重点是 JavaFX 9 API 的 i3D 部分,因为我开始的 Java 8 游戏开发标题侧重于 JavaFX API 的 i2D 部分。我在 Apress ( www.apress.com )也有几本新媒体素材(内容)创作书籍,涵盖数字图像合成、数字音频编辑、数字视频编辑、数字插图(SVG)矢量编辑、数字绘画、视觉特效(VFX)创作,使用 Fusion 8。所有这些书都使用免费的商业用专业开源软件包,如 GIMP、Inkscape、Audacity 和 Fusion。一旦 Black Magic Design 完成 DaVinci Resolve 14(一个非线性编辑套件),我会将它添加到我的开源内容制作套件中,我会在我为 JavaFX 9、Android 8 或 HTML 5.1 内容制作设置的每个工作站上安装该套件。对于新硬件,我正在看 AMD 的新锐龙 7 1700,它只使用 65W 的功率以 3.0GHz(超频的话是 3.7GHZ)运行 16 个 64 位线程;它拥有镭龙 7000 GPU,并在大多数主板上支持 64GB DDR 4-2400 MHz 内存(四个插槽)、USB 3.1、24 位音频、M2 SSD 卡、超高速硬盘访问等。装有 Windows 10 的全负荷系统售价不到 1000 美元。游戏编码快乐!

posted @ 2024-08-06 16:33  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报