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

128 阅读1小时+

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

协议:CC BY-NC-SA 4.0

十八、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 游戏用户界面的未使用部分。在这种情况下,这将是游戏屏幕的右下角部分。