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

91 阅读1小时+

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

协议:CC BY-NC-SA 4.0

十四、3D 模型层次创建:使用原语创建游戏板

现在,您已经了解了如何使用 JavaFX Phong 着色器算法及其各种颜色和效果映射通道来“蒙皮”您的 3D 基本体,并且您已经创建了丰富多彩、高度优化的游戏板方形纹理贴图,是时候添加一些自定义方法来构建游戏板并使用纹理贴图设置 Phong 着色器对象了。我们将需要创建一个createGameBoardNodes()方法来组织组成我们的 3D 游戏板的 3D 原始素材,因为createBoardGameNodes()方法应该(也确实)包含更高级的节点子对象实例化和配置,例如场景、根、UI 堆栈面板、3D 游戏板组、相机和照明,以及四个名为 Q1 到 Q4 的游戏板象限组对象(象限 1 到 4)。我们还将创建其他 19 个游戏棋盘方块对象,命名为 Q1S1 到 Q1S5、Q2S1 到 Q2S5、Q3S1 到 Q3S5 和 Q4S1 到 Q4S5,以保持对象名称简短。将这些对象命名为 Quadrant1Square1 (Q1S1)的缩写版本将使使用这些缩写术语的 Java 代码可读性更好。

在本章中,您将构建 SceneGraph 的 gameBoard Group 分支,它位于 SceneGraph 根目录下,紧挨着您已经构建好的 uiLayout 分支。在您的游戏板组分支下,我们将游戏板分成四个象限,因此游戏板的中间可以有四个更大的 300x300 单位区域,我们可以用于游戏,每个象限都有 20 个周边游戏板方块中的 5 个作为子对象。使用三层 3D 基本对象层次,我们可以将整个游戏板作为一个整体访问(例如,旋转它),将每个象限作为一个单元访问(例如,悬浮它或应用着色器效果),并访问层次底部的单个游戏板方块(叶节点子对象)。我们开始工作吧!在本章中,我们要编写数百行新的 Java 代码来实现图元、着色器、图像和场景图层次节点。

原始创建方法:createGameBoardNodes()

因为创建 24 个原语(4 个中心棋盘象限和 20 个周边正方形)将需要 100 多个 Java 语句(使用 new、setTranslateX()、setTranslateZ()、setMaterial()等进行实例化)。),让我们专门创建一个方法来保存我们的游戏板对象及其实例化和配置语句。这样,一个 createBoardGameNodes()方法将创建全局和顶级节点子类对象(场景、根、相机、灯光、uiLayout 分支、gameBoard 分支、Q1 到 Q4 分支等。).在本章的后面,我们还将把 PhongMaterial 着色器创建逻辑提取到另一个自定义 createMaterials()方法中,在这里我们将创建几十个自定义着色器对象来为这个游戏板的各种组件提供外观。要让 NetBeans 9 为您创建这个新方法,请在 start()方法的第一部分,在 createBoardGameNodes()方法调用之后添加一行代码,然后键入以下 Java 方法调用,命名您的新方法:

createGameBoardNodes();

NetBeans 将意识到这不是一个有效的方法调用,并将使用红色波浪下划线突出显示它。

使用您的 Alt+Enter 工作流程,在 javafxgame 中双击您的创建方法“createGameBoardNodes()”。JavaFXGame 选项,在图 14-1 中突出显示,让 NetBeans 为您创建一个空的方法体结构。接下来,从 createBoardGameNodes()中删除与 box Box 对象相关的代码,并将该代码放入这个新方法中。我们还将删除圆柱杆和球体,这样它们就不会干扰你的游戏板设计。

A336284_1_En_14_Fig1_HTML.jpg

图 14-1。

Open the start() method; type a createGameBoardNodes() method call after createBoardGameNodes()

剪切并粘贴你的盒子原始代码。createBoardGameNodes()到。createGameBoardNodes()并将该框重命名为 Q1S1。删除除实例化和着色器方法调用之外的所有 Java 语句,如图 14-2 所示:

A336284_1_En_14_Fig2_HTML.jpg

图 14-2。

Copy the primitive code to createGameBoardNodes(); delete everything except the instantiation and .setMaterial

Q1S1 = new Box(150, 5, 150);
Q1S1.setMaterial(phongMaterial);

使用以下代码将引用该框的 Java 代码更改为引用 Q1S1,这也显示在图 14-3 中:

A336284_1_En_14_Fig3_HTML.jpg

图 14-3。

Be sure to change all referencing from the box to Q1S1 in createBoardGameNodes() and addToSceneGraph()

light.getScope().addAll(Q1S1);

您还必须打开 addNodesToSceneGraph()方法,并将游戏板节点代码行内的框更改为 Q1S1,这样 Q1S1 游戏板方块将在我们接下来要做的测试渲染中可见。稍后,我们将在本声明中引用 Q1 到第四季度象限,然后使用这些分支节点引用游戏棋盘方块对象,这是我们接下来要做的,以创建三层层次结构。您生成的 Java 语句应该看起来像下面的 Java 9 代码,在图 14-4 的中间用黄色和浅蓝色突出显示:

A336284_1_En_14_Fig4_HTML.jpg

图 14-4。

Add the first Q1S1 game board square to the gameBoard Group node for now so it will compile the test render

gameBoard.getChildren().add(Q1S1);

如果您使用运行➤项目工作流程,此时您将在图 14-5 中看到,我们已经将 3D 场景重置为一个游戏棋盘方块,并且我们可以开始构建与该方块相关的游戏棋盘的其他部分。

A336284_1_En_14_Fig5_HTML.jpg

图 14-5。

Use the Run ➤ Project work process to test render the reconfiguration of the 3D Scene from Box to square

现在是时候开始在 SceneGraph 的 3D 游戏板组分支下构建 SceneGraph 层次结构了。游戏板组将包含从 Q1 到第四季度的四个象限组分支。这些象限组节点对象中的每一个都将包含一个盒基本象限(游戏板中心的四分之一)和该象限附属的五个游戏板方块。q1 到 q4 象限平面对象也将是四倍于(300x300)游戏棋盘正方形大小的盒子图元。

我将把游戏板组对象实例化移到根组实例化之下,然后在 createBoardGameNodes()方法的顶部添加 Q1 到 Q4 组对象实例化,以便 Java 代码顺序反映父子层次结构。您的叶对象(最底部的节点)将在 createGameBoardNodes()方法中创建,包括 q1 到 q4 象限平面对象,它们是 Q1 到 Q4 组(分支)节点的叶节点。

如果您愿意,您可以使用方便的复制粘贴程序员的技巧,键入第一个 q1 组对象的实例化语句,然后再复制粘贴三次,将 Q1 更改为 Q2 到 q4,因为在这一点上,我们只是创建了四个空的象限组节点,我们将引用它们上面的 gameBoard 组节点和它们下面的 Q1S1 到 Q4S5(以及 Q1 到 Q4)叶节点。生成的 Java 代码应该如下所示,在图 14-6 的顶部用黄色、红色和蓝色突出显示:

A336284_1_En_14_Fig6_HTML.jpg

图 14-6。

Add four Group branch node object instantiations under your gameBoard Group, named Q1 through Q4

gameBoard = new Group();

Q1 = new Group();

Q2 = new Group();

Q3 = new Group();

Q4 = new Group();

现在,我们需要从 gameBoard 分支节点中删除 Q1S1 叶节点,并将其替换为 Q1 到 Q4 分支节点。为了在我们为 3D 场景选择运行➤项目(渲染)时显示 Q1S1 盒图元,您需要创建第二个“节点生成器”。getChildren()。Q1 (Q1S1 对象的父分支)的 add()方法链,以便游戏板节点引用 Q1 节点,后者引用 Q1S1 节点。

您重新配置的 addNodesToSceneGraph()方法语句现在将有六个 Java 语句,并且您的游戏板场景图层次结构,从根到游戏板方块,现在跨越了三个 Java 9 语句,看起来应该像 addNodesToSceneGraph()方法中的以下 Java 语句,如图 14-7 的中间用黄色和蓝色突出显示(相关声明也在顶部突出显示):

A336284_1_En_14_Fig7_HTML.jpg

图 14-7。

Replace a Q1S1 reference in the gameBoard node builder with Q1 through Q4, and add a Q1 node builder

root.getChildren().addAll(gameBoard, uiLayout);

gameBoard.getChildren().addAll(Q1, Q2, Q3, Q4);

Q1.getChildren().add(Q1S1);

接下来,让我们添加 q1 盒子游戏板中心象限,它将是 Q1S1 游戏板正方形的父对象;因此,这是下一个合乎逻辑的补充。因为你已经在你的类的顶部声明了 q1 到 q4 的盒子对象,如图 14-6 到 14-8 所示,你可以先把这个 q1 对象添加到你的 Q1 分支节点,或者你可以先用一个 300,5,300 (X,Y,Z)参数在 createGameBoardNodes()方法中实例化它,然后再把它添加到 addNodesToSceneGraph()方法中,如图 14-8 和

A336284_1_En_14_Fig8_HTML.jpg

图 14-8。

Change the .add() method call to an .addAll() method call; add the q1 Box primitive for your first quadrant

Q1.getChildren().addAll(q1, Q1S1);   // In addNodesToSceneGraph() method body

q1 = new Box(300, 5, 300);           // In createGameBoardNodes() method body

图 14-9 显示了一个运行➤项目的工作流程,显示了一个游戏棋盘方块和渲染为 0,0 的象限。

A336284_1_En_14_Fig9_HTML.jpg

图 14-9。

Select Run ➤ Project and render your 3D Scene; both the quadrant and game board square are at 0,0,0

准备放置游戏板场景图节点

在我们开始围绕它们的周长定位 4 个象限和 20 个正方形之前,让我们为场景图的其余部分和着色器(PhongMaterial)使用的所有纹理贴图放置基础设施。使用剪切和粘贴将其他三个 Q2 到 Q4 场景图组节点添加到您的 addNodesToSceneGraph()方法中,如图 14-10 中以浅蓝色突出显示的内容。请注意,您可以使用。getChildren()。addAll()方法链,即使列表中只有一个 Node 子类 object 元素!您的 Java 语句将如下所示:

A336284_1_En_14_Fig10_HTML.jpg

图 14-10。

Instantiate your other three quadrant Box primitives and your other three quadrant branch node objects

Q2.getChildren().addAll(q2);

Q3.getChildren().addAll(q3);

Q4.getChildren().addAll(q4);

在此过程中,创建另外三个第 2 季度到第 4 季度的游戏板中心象限,以便我们可以将它们添加到 q2 到第 4 季度的节点构建声明中。同样,由于这些对象是在你的类的顶部声明的,你可以以任何你想要的顺序构造这些语句;只是不要使用运行➤项目来渲染场景,因为你不会看到对象,直到它们被实例化并添加到场景图层次。框实例化 Java 代码应该如下所示,如图 14-10 底部所示:

q2 = new Box(300, 5, 300);

q3 = new Box(300, 5, 300);

q4 = new Box(300, 5, 300);

正如你在图 14-9 中看到的,象限 1 位于游戏棋盘方格 1 的下方,并且不在它们的角接触的位置。因此,将象限 1 的 q1 长方体对象对角移动 225 个单位。这相当于棋盘游戏正方形边的长度再加上 50%,即 225 个单位。如果您只使用 150 个单位,象限角将位于游戏棋盘方格的中心。创建这种对齐的代码如下所示。setTranslateX()和。setTranslateZ() Java 方法调用,如图 14-11 中间用黄色和蓝色高亮显示:

A336284_1_En_14_Fig11_HTML.jpg

图 14-11。

Move the q1 quadrant to the X,Z location 225,225 so that it is internal to square Q1S1 with the corners touching

private void createGameBoardNodes() {
    q1.setTranslateX(225);
    q1.setTranslateZ(225);
    Q1S1 = new Box(150, 5, 150);
    Q1S1.setMaterial(phongMaterial);
    q2 = new Box(300, 5, 300);
    q2.setVisible(false);
    q3 = new Box(300, 5, 300);
    q3.setVisible(false);
    q4 = new Box(300, 5, 300);
    q4.setVisible(false);
}

还要注意,我使用。setVisible(false)方法调用,这样我可以先处理象限 1 的 q1 框及其五个游戏棋盘方块子对象,因为我将先处理象限 1,向您展示我正在使用的工作流程,然后是象限 2,然后是象限 3,依此类推。如果可能的话,将复杂的任务分解成子任务是很有用的,这样你就不会在开发过程中不知所措。由于 SceneGraph 层级设置为使用 gameBoard 分支下的四个棋盘象限,这就是我将如何着手构建游戏棋盘,一次一个象限(在本例中为 Q1)。请注意,我的游戏棋盘方块名称也与此匹配,因此我有一个优势,因为我的游戏棋盘方块对象,在本例中为 Q1S1 到 Q1S5,与象限组对象名称 Q1 匹配。由于我不能将 q1 组对象名称复制为盒子象限对象名称,所以我必须将小写的 Q1 到 q4 用于我的象限平面基本体,这很好,因为我仍然知道发生了什么,并且因为游戏板的象限部分远不如游戏板正方形本身重要。

让我们使用“运行➤项目”工作流程渲染 3D 场景,看看两个长方体图元是否仍然重叠,或者它们的位置是否正确。正如您在图 14-12 中所看到的,游戏棋盘方块的角和第一象限的角现在已经对齐,您可以开始看到游戏棋盘将如何布局。

A336284_1_En_14_Fig12_HTML.jpg

图 14-12。

Use the Run ➤ Project work process to see if the two 3D primitives are precisely aligned corner to corner

虽然这是我想看到的结果,但在考虑如何访问每个象限及其子方格时,我想让方格以 QxSy 1 到 5 的顺序围绕游戏板,如果我从每个象限的角上的方格 1 开始,这将不起作用!好好想想吧!因此,我实际上需要将这个正方形的位置从 0,0 (X,Z)移动到 300,0 (X,Z)。我将在创建自定义方法体来保存我的着色器后执行此操作。

由于我将有几十个着色器,我将快速创建另一个自定义方法来保持着色器创建的分离和组织,以便我可以根据需要折叠和展开着色器相关的代码。

编写 Phong 着色器创建方法代码:createMaterials()

由于着色器是 pro Java 9 游戏设计管道的重要组成部分,让我们为它们提供自己的方法体,并将 PhongMaterial 对象代码从 createBoardGameNodes()移到这个新的 createMaterials()方法中。在 loadImageAssets()之后的 start()方法的顶部添加一行代码,因为这些代码在着色器中以及 createGameBoardNodes()之前使用;这些对象将使用在此 createMaterials()方法体中创建的着色器。键入 createMaterials()和分号来调用不存在的方法;然后使用 Alt+Enter 组合键并选择“在 javafxgame 中添加 createMaterials()方法。JavaFXGame”选项。让我们也改变我们的 PhongMaterial 名称为 Shader1。我们可以在这个方法体中命名前 20 个着色器,也可以在您的类的顶部命名,在那里我已经为名为 diffuse1 到 diffuse 20 和 Shader1 到 Shader 20 的图像对象添加了声明,以预期我们将要编写的代码。剪切并将 PhongMaterial 代码粘贴到 createMaterials()中的“live”中,并删除高光属性。如图 14-13 所示的代码应该如下所示:

A336284_1_En_14_Fig13_HTML.jpg

图 14-13。

Add Image object declarations diffuse1 through diffuse20 and create a createMaterials() shader method

Image         diffuse1 ... diffuse20;      // Object Declarations at top of class
PhongMaterial Shader1  ... Shader20;
...
Shader1 = new PhongMaterial(Color.WHITE);  // Create Diffuse Shader in createMaterials()
Shader1.setDiffuseMap(diffuse1);

接下来,让我们移除我们在第十三章中创建的 loadImageAssets()中所有与特效贴图相关的代码,除了 diffuseMap,它将被重命名为 diffuse1。复制并粘贴 diffuse1 实例化四次,并引用接下来的四个游戏板正方形纹理贴图,gameboardsquare2.pnggameboardsquare5.png

从着色器的角度来看,您现在已经准备好构建游戏板的第一个象限了。在 loadImageAssets()方法的后半部分,现在应该有五个(diffuseMap)图像对象,分别命名为 diffuse1 到 diffuse5。这些将包含 diffuseMap 属性纹理贴图,定义暖色(红色、橙色、黄色)将被映射到游戏棋盘上的哪个方格,这些方格是游戏棋盘象限(1)的子方格,我们将首先对其进行布局。我们将首先布置绿色象限,然后是蓝色和紫色象限。

应使用以下 Java 语句添加(最终)24 个漫射颜色纹理贴图(漫射贴图属性)图像对象中的前五个,这些语句在图 14-14 的底部突出显示:

A336284_1_En_14_Fig14_HTML.jpg

图 14-14。

Instantiate diffuse1 through diffuse5 in loadImageAssets() using your first five PNG diffuse texture maps

diffuse1 = new Image("/gameboardsquare.png",  256, 256, true, true, true);
diffuse2 = new Image("/gameboardsquare2.png", 256, 256, true, true, true);
diffuse3 = new Image("/gameboardsquare3.png", 256, 256, true, true, true);
diffuse4 = new Image("/gameboardsquare4.png", 256, 256, true, true, true);
diffuse5 = new Image("/gameboardsquare5.png", 256, 256, true, true, true);

接下来,关闭 loadImageAssets()方法体。打开新的 createMaterials()方法体,将 Shader1 Java 语句复制并粘贴到其下四次。然后将它们重命名为 Shader2 到 Shader5。设置代表游戏棋盘正方形 diffuseMap 属性的图像对象,以引用您刚刚创建的 diffuse2 到 diffuse5 图像对象。

这可以通过使用以下十个 Java 语句来完成,它们在图 14-15 的底部用黄色和蓝色突出显示:

A336284_1_En_14_Fig15_HTML.jpg

图 14-15。

Copy and paste the Shader1 Java code block four times underneath itself to create Shader2 through Shader5

Shader1 = new PhongMaterial(Color.WHITE);
Shader1.setDiffuseMap(diffuse1);
Shader2 = new PhongMaterial(Color.WHITE);
Shader2.setDiffuseMap(diffuse2);
Shader3 = new PhongMaterial(Color.WHITE);
Shader3.setDiffuseMap(diffuse3);
Shader4 = new PhongMaterial(Color.WHITE);
Shader4.setDiffuseMap(diffuse4);
Shader5 = new PhongMaterial(Color.WHITE);
Shader5.setDiffuseMap(diffuse5);

完成游戏板的构建:象限 2 到 4

关闭 createMaterials()方法,然后重新打开 createGameBoardNodes()方法。使用Q1S1.setTranslate X ( 300 )将 location 语句添加到 Q1S1 对象中,将第一个子方块定位到我们希望的位置,在象限的开始,顺时针方向。

接下来,复制并粘贴您的三个 Q1S1 游戏板 square 语句四次,以创建其余的 square 对象,我们也必须重新配置这些对象,直到 X,Z 位置参数和着色器对象引用被关注。

Q2S2 只需要将自己定位在离 0,0 原点 150 个单位的位置,因为你的正方形是 150 乘 150。这是通过改变定位方法调用到.setTranslate X ( 150 )来实现的。确保还设置了.setMaterial( Shader2 )来引用正确的着色器,该着色器然后引用(并应用)diffuse2 图像对象作为 diffuseMap 属性。

Q2S3 是唯一不需要重新定位的方块,因为它将位于 0,0 原点。我在示例代码中添加了名为.setTranslate X ( 0 )的方法(但在 NetBeans 9 中没有)。请确保还设置了.setMaterial( Shader3 )来引用正确的着色器,该着色器然后引用(并应用)diffuse3 图像对象作为 diffuseMap 属性。

Q2S4 只需要将自己定位在距离 0,0 原点 150 单位的位置,但这次是在 Z 方向。这是通过改变定位方法调用到.setTranslate Z ( 150 )来实现的。确保设置.setMaterial( Shader4 )来引用正确的 Shader4 对象,该对象然后引用(并应用)diffuse4 图像对象作为 diffuseMap 属性。

Q2S5 需要从 0,0 开始在 Z 方向上定位自己 300 个单位。这是用定位方法调用.setTranslate Z ( 300 )来完成的。确保设置.setMaterial( Shader5 )来引用正确的 Shader5 对象,然后它引用(并应用)diffuse5 图像对象作为 diffuseMap 属性。图 14-16 中突出显示的 Java 代码应该如下所示:

A336284_1_En_14_Fig16_HTML.jpg

图 14-16。

Copy and paste the Q1S1 statements four times underneath themselves and reconfigure their method calls

private void createGameBoardNodes() {
    q1.setTranslateX(225);
    q1.setTranslateZ(225);
    Q1S1 = new Box(150, 5, 150);
    Q1S1.setTranslateX(300);
    Q1S1.setMaterial(Shader1);
    Q1S2 = new Box(150, 5, 150);
    Q1S2.setTranslateX(150);
    Q1S2.setMaterial(Shader2);
    Q1S3 = new Box(150, 5, 150);
    Q1S3.setTranslateX(0);        // This statement can be omitted, as default X location is 0
    Q1S3.setMaterial(Shader3);
    Q1S4 = new Box(150, 5, 150);
    Q1S4.setTranslateZ(150);
    Q1S4.setMaterial(Shader4);
    Q1S5 = new Box(150, 5, 150);
    Q1S5.setTranslateZ(300);
    Q1S5.setMaterial(Shader5);
    q2 = new Box(300, 5, 300);
    q2.setVisible(false);         // Set q2 through q4 quadrant objects to visible=false for now
}

在我们可以看到 3D 场景中呈现的这些新对象之前,我们需要将它们添加到 addNodesToSceneGraph()方法体中的 SceneGraph 层次结构中。将 Q1S2 到 Q1S5 框对象添加到 Q1 组对象,如图 14-17 中用黄色和浅蓝色突出显示的。

A336284_1_En_14_Fig17_HTML.jpg

图 14-17。

Add your other three Q2 to Q4 Group objects to the gameBoard Group and the other four squares to Q1

让我们也完成场景图层次的第二层(q2 到 q4 分支节点),并将 Q2 到 Q4 盒平面基本体添加到其他三个 Q2 到 Q4 组节点,以将游戏板的内部象限添加到场景图层次。我们在工作过程中的这一点上这样做,以便我们能够在游戏板的中心部分工作,因为我们是一次构建一个象限。

因为我们基本上完成了第一个象限,所以我们将其他三个放入场景图中,这样当我们构建剩余的游戏板象限和它们的游戏板方块时,它们将会呈现(可见),这些方块将围绕每个相应象限的周界附着到它们。

从优化的角度来看,我们只用了九个就为 2D UI 和 3D 游戏板组件创建了一个相对复杂的场景图层次。getChildren()。addAll()方法链 Java 编程语句,如图 14-17 所示。这是相对紧凑的,因为我们以高度组织的方式引用了几十个 2D 和 3D 游戏组件叶节点,并且只使用了九个场景图层次结构语句。

添加其他四个正方形和其他三个象限可以通过使用以下 Java 编程语句来完成,这些语句在图 14-17 的底部以黄色和浅蓝色突出显示:

root.getChildren().addAll(gameBoard, uiLayout);
gameBoard.getChildren().addAll(Q1, Q2, Q3, Q4);
Q1.getChildren().addAll(q1, Q1S1, Q1S2, Q1S3, Q1S4, Q1S5);

Q2.getChildren().addAll(q2);

Q3.getChildren().addAll(q3);

Q4.getChildren().addAll(q4);

图 14-18 显示了运行➤项目 JavaFX 9 代码测试的工作流程。正如你所看到的,我们已经有了四分之一的游戏板,它看起来非常适合组装所有这些素材的第一轮,包括 3D 盒子图元和 2D 纹理贴图图像,我们已经在类的顶部声明并即将创建。

A336284_1_En_14_Fig18_HTML.jpg

图 14-18。

Use the Run ➤ Project work process to see if the completed 3D game board quadrant is aligning properly

复制粘贴五个扩散图像语句,创建扩散 6 到 20,如图 14-19 所示。

A336284_1_En_14_Fig19_HTML.jpg

图 14-19。

Copy and paste 5 diffuse texture Image instantiations 3 times and create all 20 diffuse Image objects

关闭 loadImageAssets()方法体,现在漫反射图像实例化已经就绪,打开 createMaterials()方法,执行完全相同的操作,复制前五个着色器 Java 语句对,并在它们自身下面再粘贴三次。更改每个语句的编号部分,以便创建 Shader6 到 Shader20 Java 语句对。这些都可以在图 14-20 中用黄色突出显示。

A336284_1_En_14_Fig20_HTML.jpg

图 14-20。

Copy and paste 5 Shader PhongMaterial instantiations 3 times and create all 20 Shader objects

现在,让我们通过返回 createGameBoardNodes()方法体来创建游戏板的第二象限,并通过复制 Q1S1 到 Q1S5 语句来创建 Box 原语 Q2S1 到 Q2S5 的第二部分代码,将它们再次粘贴到它们自身的下面,然后更改对象名称和方法调用参数(这样,您就不必再次将这些 Java 语句的大部分键入 NetBeans 9 IDE)。

q2 盒子对象(第二象限)将需要沿 z 轴向外移动 300 个单位(象限大小为 300x300),因此 q2.setTranslateZ()方法参数需要从 225 增加到 525,以完成第二象限游戏板组件定位,如图 14-22 所示,如果您想向前看。

Q2S1 需要沿 z 轴(从 0,0 原点)将自己定位 450 个单位,因为 Q1S5 位于 300 加上 150,即 450。这是通过改变定位方法调用到.setTranslate Z ( 450 )来实现的。确保设置.setMaterial( Shader6 )来引用正确的着色器,该着色器引用(并应用)diffuse6 图像对象作为 diffuseMap 属性。

Q2S2 需要将自己沿 z 轴定位 600 个单位(从 0,0 原点),因为 450 加 150 等于 600。这是通过改变定位方法调用到.setTranslate Z ( 600 )来实现的。请确保还设置了.setMaterial( Shader7 )来引用正确的着色器,该着色器然后引用(并应用)diffuse7 图像对象作为 diffuseMap 属性。

Q2S3 需要将自己沿 z 轴定位 750 个单位(从 0,0 原点),因为 600 加 150 等于 750。这是通过改变定位方法调用到.setTranslate Z ( 750 )来实现的。确保还设置了.setMaterial( Shader8 )来引用正确的着色器,该着色器然后引用(并应用)diffuse8 图像对象作为 diffuseMap 属性。

Q2S4 也需要从 0,0 原点沿 z 轴将其自身定位 750 个单位,但这一次,我们需要在 X 方向上将这个方块推过 150 个单位,以便沿游戏棋盘布局的顶部向右移动。这是通过转换到使用两个定位方法调用来完成的。一个是.setTranslate X ( 150 ),另一个是.setTranslation Z ( 750 )。请确保将.setMaterial( Shader9 )设置为引用正确的 Shader9 对象,然后该对象将 diffuse9 图像对象作为 diffuseMap 属性进行引用(并应用)。

Q2S5 需要在 X 方向上从 0,0 开始定位 300 个单位,以及在 Z 方向上定位 750 个单位,使得该正方形位于该游戏板的顶部中间附近,在游戏板的另一侧,从正方形 1 开始。这也是使用两个位置方法调用完成的,对.setTranslate Z ( 750 )和对.setTranslate X ( 300 )。请确保将.setMaterial( Shader10 )设置为引用正确的 Shader10 对象,该对象随后会引用(并应用)diffuse10 图像对象作为 diffuseMap 属性。

用于构建游戏棋盘第二象限的 Java 代码如图 14-21 所示,位于构建第一象限的代码之后,应如下所示(为便于阅读,将代码隔开):

A336284_1_En_14_Fig21_HTML.jpg

图 14-21。

Instantiate and configure game board squares Q2S1 through Q2S5 inside of createGameBoardNodes()

private void createGameBoardNodes() {
    ...
    q2 = new Box(300, 5, 300);      // Java code creating a second quadrant for the gameboard
    q2.setTranslateX(225);
    q2.setTranslateZ(525);

    Q2S1 = new Box(150, 5, 150);
    Q2S1.setTranslateZ(450);
    Q2S1.setMaterial(Shader6);

    Q2S2 = new Box(150, 5, 150);
    Q2S2.setTranslateZ(600);
    Q2S2.setMaterial(Shader7);

    Q2S3 = new Box(150, 5, 150);
    Q2S3.setTranslateZ(750);
    Q2S3.setMaterial(Shader8);

    Q2S4 = new Box(150, 5, 150);
    Q2S4.setTranslateZ(750);
    Q2S4.setTranslateX(150);
    Q2S4.setMaterial(Shader9);

    Q2S5 = new Box(150, 5, 150);
    Q2S5.setTranslateZ(750);
    Q2S5.setTranslateX(300);
    Q2S5.setMaterial(Shader10);

    q3 = new Box(300, 5, 300);
    q3.setVisible(false);
    ...                            // The third quadrant configuration code will go in here
    q4 = new Box(300, 5, 300);
    q4.setVisible(false);
}

如图 14-22 所示,使用运行➤项目来确认游戏板的构建已经完成了一半!

A336284_1_En_14_Fig22_HTML.jpg

图 14-22。

Quadrants 1 and 2 are now coded and aligning properly

接下来为 gameBoard 组分支的最终场景图构造代码添加 Java 语句。你的 3D 场景层次应该如下图所示,在图 14-23 中用黄色和浅蓝色突出显示:

A336284_1_En_14_Fig23_HTML.jpg

图 14-23。

Add all remaining SceneGraph Node object “wiring” code to add the rest of the squares to the quadrants

root.getChildren().addAll(gameBoard, uiLayout);
gameBoard.getChildren().addAll(Q1, Q2, Q3, Q4);
Q1.getChildren().addAll(q1, Q1S1, Q1S2, Q1S3, Q1S4, Q1S5);
Q2.getChildren().addAll(q2, Q2S1, Q2S2, Q2S3, Q2S4, Q2S5);
Q3.getChildren().addAll(q3, Q3S1, Q3S2, Q3S3, Q3S4, Q3S5);
Q4.getChildren().addAll(q4, Q4S1, Q4S2, Q4S3, Q4S4, Q4S5);

接下来,让我们通过返回到 createGameBoardNodes()方法体并为 Box 原语 q3S1 到 Q3S5(以及 Q3 中心象限)创建第三部分代码,来为您的游戏板创建第三象限。只需复制 Q2S1 到 Q2S5 语句,并再次将它们粘贴到自身之下(在 q3 实例化和配置语句之后,将分组的节点逻辑地保存在 Java 代码体中)。接下来,您将再次更改您的对象名称和方法调用参数(这样您就不必再次将这些 Java 9 语句中的大部分输入到 NetBeans 9 IDE 中),以便从您的第一个象限开始对角定位您的方块。

q3 盒子对象(第三象限)将需要沿 x 轴和 z 轴向外移动 300 个单位(象限大小为 300x300),因此q3.setTranslateX()方法参数也需要从 225 增加到 525,以完成第三象限游戏板组件定位,如图 14-24 所示。

A336284_1_En_14_Fig24_HTML.jpg

图 14-24。

Use your Run ➤ Project work process to see that quadrants 1 to 4 are now coded and aligning properly

Q3S1 需要沿 x 轴从 0,0 原点定位 450 个单位,沿 z 轴定位 750 个单位。这是通过将您的 locational 方法调用更改为.setTranslateX(450)并离开(或者添加w,这取决于您复制了哪个 Java 代码).setTranslate Z ( 750 )。请确保还设置了.setMaterial( Shader11 )来引用正确的着色器编号,该编号然后引用(并应用)diffuse11 图像对象作为 diffuseMap 属性。

Q3S2 需要沿 x 轴从 0,0 原点定位 600 个单位,沿 z 轴定位 750 个单位。这是通过将位置方法调用更改为.setTranslate X ( 600 )并离开(或添加,取决于您复制的 Java 代码块).setTranslate Z ( 750 ))来完成的。请确保还设置了.setMaterial( Shader12 )来引用正确的着色器编号,该编号然后引用(并应用)diffuse12 图像对象作为 diffuseMap 属性。

Q3S3 需要沿 z 轴从 0,0 原点定位 750 个单位,沿 x 轴定位 750 个单位,这使其与 0,0 原点成对角线。这是通过将位置方法调用更改为.setTranslate Z ( 750 ),然后添加第二个.setTranslate X ( 750 )方法调用来完成的。请确保还设置了.setMaterial( Shader13 )来引用您正确的 Phong 着色器对象编号,该编号引用(并应用)diffuse13 图像对象作为您的 diffuseMap 属性。

Q3S4 还需要从 0,0 原点沿 X 方向将自己定位 750 个单位,但这一次,我们还需要将这个方块沿 Z 方向再向下拉 150 个单位,以便沿游戏棋盘布局的右侧向下移动。这通过再次使用两个定位方法调用来完成。一个将是.setTranslate Z ( 600 ),另一个仍将被设置为.setTranslation X ( 750 )。同样,请确保将.setMaterial( Shader14 )设置为引用匹配的 Shader14 对象,然后该对象引用并应用 diffuse14 图像对象作为 diffuseMap 属性。

Q3S5 需要在 X 方向上从 0,0 和 450 个单位定位自己 750 个单位在 Z 方向上,以便这个正方形位于这个游戏板的中间右侧附近。这也是使用两个位置方法调用完成的,对.setTranslate X ( 750 )和对.setTranslate Z ( 450 )。请确保将.setMaterial( Shader15 )设置为引用正确的 Shader15 对象,然后该对象引用(并应用)diffuse15 图像对象作为 diffuseMap 属性。

用于构建游戏棋盘第二象限的 Java 代码应该如下所示:

private void createGameBoardNodes() {
    ...
    q3 = new Box(300, 5, 300);    // Java code creating a third quadrant for the gameboard
    q3.setTranslateX(525);
    q3.setTranslateZ(525);
    Q3S1 = new Box(150, 5, 150);
    Q3S1.setTranslateZ(750);
    Q3S1.setTranslateX(450);
    Q3S1.setMaterial(Shader11);
    Q3S2 = new Box(150, 5, 150);
    Q3S2.setTranslateZ(750);
    Q3S2.setTranslateX(600);
    Q3S2.setMaterial(Shader12);
    Q3S3 = new Box(150, 5, 150);
    Q3S3.setTranslateZ(750);
    Q3S3.setTranslateX(750);
    Q3S3.setMaterial(Shader13);
    Q3S4 = new Box(150, 5, 150);
    Q3S4.setTranslateZ(600);
    Q3S4.setTranslateX(750);
    Q3S4.setMaterial(Shader14);
    Q3S5 = new Box(150, 5, 150);
    Q3S5.setTranslateZ(450);
    Q3S5.setTranslateX(750);
    Q3S5.setMaterial(Shader15);
    ...                           // Your fourth quadrant configuration code will go in here
    q4 = new Box(300, 5, 300);
    q4.setVisible(false);
}

最后,让我们通过返回 createGameBoardNodes()方法体并为 Box 原语 Q4S1 到 Q4S5 创建第四段代码来创建游戏板的第四象限。只需复制你的 Q3S1 到 Q3S5 语句(以及 q4 语句)并再次粘贴在它们自己的下面;然后更改对象名和方法调用参数(这样您就不必再次在 NetBeans 9 中键入所有这些 Java 语句)。

q4 盒子对象(第四象限)将需要沿 z 轴向下移动 300 个单位,因此您的 q4.setTranslateZ()方法参数需要从 525 减少到 225,以完成第四象限游戏板组件定位,如图 14-24 所示。

Q4S1 需要沿 Z(从 0,0 原点)定位 300 个单位,并沿 X 向右定位 750 个单位。这是通过改变定位方法调用到.setTranslate Z ( 300 )来实现的。确保将.setMaterial( Shader16 )设置为引用正确的着色器,该着色器然后引用并应用 diffuse16 图像对象作为 diffuseMap 属性。

Q4S2 需要沿 Z(从 0,0 原点)定位 150 个单位,沿 x 定位全部 750 个单位。这是通过将定位方法调用更改为.setTranslate Z ( 150 )来完成的。请确保将.setMaterial( Shader17 )设置为引用正确的着色器,然后该着色器引用并应用 diffuse17 图像对象作为 diffuseMap 属性。

Q4S3 只需要将自己定位在距离 0,0 原点沿 X 方向 750 个单位的位置,因为它位于右角。这意味着唯一需要的定位方法调用是.setTranslate X ( 750 )。确保将.setMaterial( Shader18 )设置为引用正确的着色器,该着色器然后引用并应用 diffuse18 图像对象作为 diffuseMap 属性。

Q4S4 只需要将自己定位在距离原点 0,0 沿 X 方向 600 个单位的位置,就可以将这个方块向原点方向拉回 150 个单位,以便将其沿游戏棋盘布局的底部向左移动。这是通过仅使用.setTranslate X ( 600 )方法调用来完成的。确保设置.setMaterial( Shader19 )来引用正确的 Shader9 对象,该对象引用(并应用)diffuse19 图像对象作为 PhongMaterial diffuseMap 属性。

Q4S5 需要在 X 方向上从 0,0 开始定位自己 450 个单位,因此您的最后一个游戏方块位于该游戏板的底部中间附近。这也是使用一个定位方法调用.setTranslate X ( 450 )来完成的。确保设置.setMaterial( Shader20 )来引用正确的 Shader20 对象,然后它引用(并应用)diffuse20 图像对象作为 diffuseMap 属性。用于构建游戏棋盘最后一个象限的 Java 代码应该类似于下面的 Java 代码:

private void createGameBoardNodes() {
    ...
    q4 = new Box(300, 5, 300);      // Java code creating a second quadrant for the gameboard
    q4.setTranslateX(525);
    q4.setTranslateZ(225);
    Q4S1 = new Box(150, 5, 150);
    Q4S1.setTranslateX(750);
    Q4S1.setTranslateZ(300);
    Q4S1.setMaterial(Shader16);
    Q4S2 = new Box(150, 5, 150);
    Q4S2.setTranslateX(750);
    Q4S2.setTranslateZ(150);
    Q4S2.setMaterial(Shader17);
    Q4S3 = new Box(150, 5, 150);
    Q4S3.setTranslateX(750);
    Q4S3.setMaterial(Shader18);
    Q4S4 = new Box(150, 5, 150);
    Q4S4.setTranslateX(600);
    Q4S4.setMaterial(Shader19);
    Q4S5 = new Box(150, 5, 150);
    Q4S5.setTranslateX(450);
    Q4S5.setMaterial(Shader20);
}

图 14-24 显示了运行➤项目 Java 代码测试的工作流程,展示了一个完成的 3D 游戏板。

有一两个渲染异常是可见的,例如 Q1S2 方块,它看起来像是位于 Q1S1 方块之上。这很奇怪,因为代码是精确的,并且基于 150 的倍数,所以它应该像其他代码一样精确对齐。因为这个问题不在代码上,我们将在下一章看看如何处理这个渲染异常。使用。setRotationAxis(旋转。y 轴)和。setRotate(30)方法将游戏板旋转 30 度,就像您之前所做的那样,以查看旋转游戏板层次所使用的枢轴点。这个 Java 9 测试代码应该放在你的 createBoardGameNodes()方法中,如图 14-25 中突出显示的。

A336284_1_En_14_Fig25_HTML.jpg

图 14-25。

Add .setRotationAxis(Rotate.Y_AXIS) and a .setRotate(30) method call to the gameBoard Group object

我们这样做的原因是,在我们离开这一章之前,我们需要检查一下,这个游戏板是否作为一个层级在工作。也就是说,如果我们围绕 y 轴旋转它,它会以游戏板组的中心为支点,还是会围绕游戏板的 0,0 原点角方块的中心旋转(旋转)?

正如你在图 14-26 中看到的,游戏板组对象确实在定义它自己的 0,0 中心,使用它所有组节点子节点的平均中心。我们从游戏棋盘的 6 × 150 (900)构造中知道,这个 0,0 中心在原点之间的 X 和 Z (450,450)上偏移 450(900 的一半),或者在直线单位(对角线上)上偏移 625(450+450 的 1/2)。通过用可整除的整数来构造事物,我们将能够在后面的章节中使用整数(int)作为我们的游戏代码,这为 JavaFX 游戏引擎节省了内存和处理。

A336284_1_En_14_Fig26_HTML.jpg

图 14-26。

Rotate the gameBoard Group Node object 30 to 45 degrees to see where it defines its center for rotation

在我们结束本章之前,让我们看看是否可以通过使用不同的相机(算法)类来改善我们的游戏板渲染结果,因为我们似乎有一些盒子表面渲染顺序和定位问题。如果不是相机对象在方块之间造成了这些轻微的脊,我们将不得不进一步寻找这个问题的解决方案,因为我们需要获得一个逼真的游戏板,看起来像我们在现实生活中用来玩游戏的纸板游戏板。正如你现在所知道的,pro Java 9 游戏开发是一个迭代的过程,所以你知道我们最终会解决它!

更改摄像机:使用 ParallelCamera 类

接下来,我将把 camera 对象从 PerspectiveCamera 更改为 ParallelCamera,这既是为了给你一些使用它们的经验,也是为了看看这种面顺序渲染问题(看起来在正方形中重叠)在两个 Camera 类算法之间(在两个 Camera 子类之间)是否有任何不同。这很简单,只需将类顶部的声明从 PerspectiveCamera 更改为 ParallelCamera,并确保对 createBoardGameNodes()中的实例化语句进行相同的更改,如此处以及图 14-27 所示:

A336284_1_En_14_Fig27_HTML.jpg

图 14-27。

Change the Scene camera object to use a ParallelCamera class (algorithm) instead of PerspectiveCamera

ParallelCamera camera;
...
camera = new ParallelCamera();
camera.setTranslateZ(0);
camera.setNearClip(0.1);
camera.setFarClip(5000.0);

接下来,让我们进入 gameButton 事件处理代码块并移除。setFieldOfView(1)方法调用只需注释掉该行代码,正如您所看到的,这是一个巧妙而常见的代码调试技巧。

我们这样做是因为新的 ParallelCamera 对象不支持该特定的方法调用。我们还会将 camera.setTranslateZ()方法调用更改为游戏板的对角线值,我计算该值以将相机视图放置在游戏板中心(625)。

我还会将 camera.setTranslateX()方法调用设置为游戏板宽度 225 的四分之一,如图 14-28 中间高亮显示的 camera 对象代码所示。

A336284_1_En_14_Fig28_HTML.jpg

图 14-28。

Remove the FOV setting code, change .setTranslateX() to 225, .setTranslateZ() to 625, and Y = 0

我正在改进这段代码,以便更好地查看游戏板,并使其更好地适应窗口,这样当我们在随后的动画和游戏性章节中旋转它时,它将在任何旋转方向上完美地适应场景,同时它也在动画中随机旋转以选择主题象限。

我要做的下一件事是“调整”onStart()中的相机值,以适应窗口中的游戏板。正如你在图 14-29 中看到的,我们需要展平摄像机视图(30 度)并稍微调整 X、Y、Z 位置。

A336284_1_En_14_Fig29_HTML.jpg

图 14-29。

The game board is almost fitting perfectly in the window; let’s adjust the camera angle and spacing next!

正如你在图 14-30 中看到的,我将旋转调整到 30°,Z 轴调整到 500°,Y 轴调整到-300°,X 轴调整到-260°。

A336284_1_En_14_Fig30_HTML.jpg

图 14-30。

Set the camera rotation at 30 degrees, the Z location to 500, the Y location to -300, and the X location to -260

正如您在图 14-31 中看到的,我们现在已经设置了游戏板的“极限”以适合窗口,使用这些新的相机设置和 ParallelCamera 算法,这似乎比 PerspectiveCamera 更少扭曲游戏板。如果我们现在旋转游戏板,它应该都留在窗口(可视)区域内。

A336284_1_En_14_Fig31_HTML.jpg

图 14-31。

Use your Run ➤ Project work process to see if the new camera algorithm and settings fit the game board

摘要

在第十四章中,我们构建了 JavaFX SceneGraph 层次结构的 3D 部分,即根节点下的 gameBoard 组(节点子类)分支节点(我们之前已经创建了 uiLayout StackPane 节点子类)。我们创建了一个由四个名为 q1 到 q4 的游戏棋盘象限组分支节点组成的子组,每个节点包含四分之一的游戏棋盘内部,这些节点是名为 Q1 到 Q4 的长方体基本体,以匹配它们的组节点父对象。在这些象限下面,我们分组了五个游戏棋盘方形叶节点对象,它们将在游戏设计中与象限游戏功能相对应。

我们创建了两个新的方法体,一个用于创建游戏棋盘方块,因为有几十个方块,另一个用于创建 Phong 材质,因为将会有几十个方块!这让事情井井有条。我们现在有八个(如果算上 main()的话有九个,它仍然处于引导代码状态)方法体,其中七个是定制的,我们有 400 多行 Java 代码,全部组织成逻辑折叠和扩展部分。我们为每个游戏棋盘方块创建了彩色着色器,将适当的漫反射纹理贴图映射到每个方块上。

在第十五章中,我们将进一步完善我们的游戏板设计和 Java 代码组织,并为游戏玩家创建一种在 3D 空间中操纵游戏板的方式,以便他们(或游戏 AI 代码)可以访问他们感兴趣的游戏板内容和主题部分。

十五、3D 游戏界面创建:使用球体原语创建一个界面节点

现在,您已经创建了多层游戏板组节点(子类)层次,并对其进行了测试,以查看它是否像一个 3D 模型一样旋转,是时候添加一个球体 3D 图元了,这样我们就可以创建一个 3D 用户界面元素,供用户在游戏过程中用来创建随机的“旋转”。我们还将为这些设置 Phong 着色器对象,并再次使用 GIMP 2.8.22 为游戏板象限创建(从头开始)其余的漫反射纹理贴图,以及为球体图元创建 3D“spinner”UI 纹理贴图。该旋转器将在每个玩家的回合中使用,以随机旋转游戏板来选择主题类别。你总是需要将你的 pro Java 9 游戏与其他游戏区分开来,所以我们将是独一无二的,并旋转游戏板本身来选择象限(主题类别),这不能用现实生活中的游戏板来完成,但可以用虚拟的 i3D 游戏板来完成。我们将把 Java 9 代码添加到类的顶部以及 createMaterials()、addNodesToSceneGraph()和 loadImageAssets()方法体中。我们将创建自定义的 PNG24 漫反射纹理添加到您的项目源(/src)文件夹中。我们将重新安排 createGameBoardNodes()方法来重新组织象限 3D 图元,我们将在本章中完成游戏板设计的内部部分。我们将把象限一起放在这个方法的顶部。

在这一章中,我们还将看看你是如何解决在创作专业品质游戏的过程中遇到的问题的。在这种情况下,有一个人脸渲染的问题,是我们在对游戏板建模时遇到的;它应该渲染顺利(顶部平坦),但渲染与游戏板广场重叠,这是不应该发生的。也有一些小的 Y(高度)变化,一旦它们被漫反射纹理映射,就会使象限看起来凹陷。(请记住,在第十四章中,中心象限在没有应用着色器的情况下看起来是平坦的,但是一旦我们继续对它们进行处理,它们也会显示出这些渲染工件,这一点您将在本章的后面部分看到。)

完成 3D 资源:主题象限和微调器

让我们继续设计和开发棋盘游戏的 3D 组件,包括使用 GIMP 为游戏棋盘内部开发纹理贴图,以及使用 3D spinner UI 元素为游戏创建随机旋转,就像在现实生活中的棋盘游戏一样。我们将使用 Java 9 (JavaFX API)类来完成这项工作,这样我们就可以只使用 Java 9 APIs 和我们的数字图像资源(背景图像和纹理贴图)来创建游戏。到目前为止,我们已经用了大约 400 行代码完成了这项工作!在本章中,我们将添加另外 10 %( 440)来“修饰”象限,并在屏幕的左上角添加一个“spinner”UI 元素。在类的顶部,我们需要做的第一件事是再添加五个名为 diffuse21 到 diffuse25 的图像对象声明,以及五个名为 Shader21 到 Shader25 的声音材料对象声明。这显示在下面的 Java 代码中,并且在图 15-1 中的类的顶部显示为绿色(一些代码用黄色和蓝色突出显示):

A336284_1_En_15_Fig1_HTML.jpg

图 15-1。

Add objects at the top of your class for diffuse texture maps and shaders for your quadrants and a spinner

Image ... diffuse21, diffuse22, diffuse23, diffuse24, diffuse25;
PhongMaterial ... Shader21, Shader22, Shader23, Shader24, Shader25;

剪切并粘贴最后五个 diffuse16 到 diffuse20 图像对象声明,创建五个名为 diffuse21 到 diffuse25 的新声明,并对它们进行配置以供使用,如图 15-2 所示:

A336284_1_En_15_Fig2_HTML.jpg

图 15-2。

Create five new diffuse image map objects in your loadImageAssets() method and configure them for use

diffuse21 = new Image("/gameboardquad1.png", 512, 512, true, true, true);
diffuse22 = new Image("/gameboardquad2.png", 512, 512, true, true, true);
diffuse23 = new Image("/gameboardquad3.png", 512, 512, true, true, true);
diffuse24 = new Image("/gameboardquad4.png", 512, 512, true, true, true);
diffuse25 = new Image("/gameboardspin.png",  256, 256, true, true, true);

我们将在本章的下一节使用 GIMP 创建这些漫射纹理贴图数字图像素材。打开 createMaterials()方法体,并添加相应的 Shader21 到 Shader25 对象实例化和配置语句,这些语句“连接着色器”以引用漫反射纹理贴图图像对象资源。

如果您愿意,也可以使用复制和粘贴来完成此操作,就像您对漫反射纹理贴图图像对象所做的那样。创建新着色器并将其引用到漫反射纹理贴图图像对象素材的 Java 代码应类似于以下 Java 代码语句块,在图 15-3 中高亮显示:

A336284_1_En_15_Fig3_HTML.jpg

图 15-3。

Create five new Shader PhongMaterial objects in your createMaterials() method and wire them to diffuseMap objects

Shader21 = new PhongMaterial(Color.WHITE);
Shader21.setDiffuseMap(diffuse21);
Shader22 = new PhongMaterial(Color.WHITE);
Shader22.setDiffuseMap(diffuse22);
Shader23 = new PhongMaterial(Color.WHITE);
Shader23.setDiffuseMap(diffuse23);
Shader24 = new PhongMaterial(Color.WHITE);
Shader24.setDiffuseMap(diffuse24);
Shader25 = new PhongMaterial(Color.WHITE);
Shader25.setDiffuseMap(diffuse25);

你现在将不得不再次利用 GIMP 来创建你的象限和旋转纹理贴图来专业地纹理化你的棋盘游戏元素。当前版本是 2.8.22。

创建您的象限和微调器漫射颜色纹理贴图

使用 GIMP 文件➤新工作进程创建一个透明(空)的漫反射纹理贴图合成,这次使其为 512×512 像素,因为象限框对象 q1 到 q4 在两个轴上都是方形框对象的两倍大(或总共四倍大)。这在数学上与游戏棋盘方格所用的 256 像素纹理贴图尺寸加倍相匹配。单击圆形(或椭圆)选择工具,在图 15-4 的顶部高亮显示,并再次使用工具图标下方的椭圆选择工具选项选项卡,为圆形设置精确的大小和位置,因为我们希望白色圆形完美地位于每个游戏板象限的中心。我将圆的大小设置为 400(相等的宽度和高度值创建一个完美的圆;任何变化都将创建一个椭圆或卵形),我将其余部分(512 - 400 = 112 / 2 = 56)相除,得到我的 X,Y 位置值 56,它也以红色突出显示。

A336284_1_En_15_Fig4_HTML.jpg

图 15-4。

Create the four quadrant texture maps at 512-pixel resolution using 60 percent of the corner square color value

选中背景层,确保前景色样设置为白色,并使用编辑➤填充前景颜色(白色)选项在合成层堆栈的最底部为所有四个象限纹理贴图创建白色中心,如图 15-4 所示。右键单击背景层,使用新建层命令,创建一个名为 GameBoardQuadrant 的新层。使用“选择➤反转”菜单序列来反转选择,并选择 GameBoardQuadrant 层来指定该层保存您的外部颜色填充。打开 gameboardsquare3.png 文件,使用吸管工具选择它的橙色值。单击 FG 颜色(前景)样本,调用“拾色器”对话框,并将值(V)滑块设置为 60%颜色(40%白色),为位于对角的象限创建角正方形的彩色版本。如图 15-2 中的图像对象代码所示,使用编辑➤填充 FG 颜色来填充中心圆周围的区域,使用文件➤导出为将文件保存到项目的/src 文件夹,并将其命名为 boardgamequad1.png。重复这个过程:创建一个新的层,得到一个角方块颜色值,变亮 40%,填充前景色,将图像导出为 PNG24 来创建另外三个编号的 boardgamequad PNG24 素材,它们显示在图 15-4 最左侧各自的层中。你也可以在图 15-4 的右上角看到我打开来采样颜色值的 gameboardsquare 3、8、13 和 18 图像素材。吸管工具位于椭圆选择工具的右下方。

当我们在 GIMP 中时,让我们打开我们的纹理贴图创建 GIMP XCF 文件,其中包含我们在第十三章中创建的所有不同的贴图类型,包括着色器和材质,并使用您的沙滩球漫反射纹理创建一个 3D 旋转球纹理,当它旋转时读取“SPIN ”, S 和 I 为白色(彩色), P 和 N 为黑色。

打开 Pro _ Java _ 9 _ Games _ Development _ Texture _ Maps GIMP XCF 文件,选择文本工具,如图 15-5 红框所示。设置您的文本选项使用 Arial Heavy,设置字体大小为 48,并选择抗锯齿。点击色样,选择白色,在绿色条纹中间输入大写字母 S,如图 15-5 所示。右键单击 S 图层,选择复制图层工具,设置文本工具色样为黑色,选择黄色的 S,如图 15-5;然后键入大写的 P 来替换 s。您可以使用右箭头键使用移动工具(四个相连的箭头)来移动文本元素,以便它与 s 保持精确对齐。将其置于白色条纹的中心,然后对 I 和 N 文本元素重复该过程,直到您创建了单词 SPIN。

A336284_1_En_15_Fig5_HTML.jpg

图 15-5。

Create the word SPIN twice on your beach ball texture map to create your animated spinner texture map

一旦你做了所有四个字母一次,你可以使用相同的右键单击一个层,并使用复制层工作过程来复制这些字母;然后使用移动工具将字母定位在其他四个条纹上,如图 15-6 右侧所示。

A336284_1_En_15_Fig6_HTML.jpg

图 15-6。

Replicate the four SPIN letters twice in the center of each of the eight stripes, at exactly the same height

要使用 GIMP 移动工具,首先单击文本元素,这将向移动工具显示您要移动的内容,然后使用右箭头键将字母定位在下一个条纹上。使用右箭头键而不是用鼠标拖动字母将使字母保持在完全相同的像素高度位置,使字母彼此完全对齐。

正如你将在图 15-6 中看到的,这个工作过程将产生一个统一的、专业的地图结果。尽管您的字母在 GIMP 画布上看起来很拥挤,但当映射到球体基本体的曲率上时,结果是非常可读的,即使是在动画中,因为曲面的曲率似乎将这些字母“拉伸”得更远。

一旦你对旋转纹理贴图感到满意,使用文件➤导出为菜单序列,并保存你的游戏面板旋转。png 文件到您的C:/Users/Name/Documents/NetBeansProjects/JavaFXGame/src/文件夹中,如图 15-7 所示。请注意,我们的 boardgamesquare PNG24 文件经过了很好的优化,有 680 字节,而我们的 boardgamequad 文件每个只有 10KB。如果你点击一个文件名,你会在对话框的右边得到一个很好的纹理贴图预览。这是一个很棒的特性,特别是对于相似的文件名,因为你可以点击任何文件名来预览它,GIMP 会把那个文件名放在 name 字段中;那你就把末尾的数字改了当打字快捷键就行了!

A336284_1_En_15_Fig7_HTML.jpg

图 15-7。

Name your new diffuse color texture map file gameboardspin.png and then save it into your /src folder

接下来,让我们开始将漫射纹理贴图应用到我们的 3D 棋盘游戏元素中,并完成 3D 贴图的创建。

3D 游戏棋盘象限的纹理映射:Java 代码

打开 createGameBoardNodes()方法体,将 q1 到 q4 的对象代码剪切并粘贴到方法的顶部,这样象限框元素 q1 到 q4 就可以在同一个 Java 9 语句块中进行实例化和配置。您现在可以更清楚地看到相对于每个象限的不同 225 和 525 组合的 X、Z 移动模式,没有相同的 X、Z 坐标对,它们会与您的象限重叠。

q1 .setMaterial( Shader21 );添加到第一个中,如图 15-8 所示,使用以下 Java 代码:

A336284_1_En_15_Fig8_HTML.jpg

图 15-8。

Cut and paste all quadrant Box primitive code to one place and start adding shaders using .setMaterial()

q1 = new Box(300, 5, 300);
q1.setMaterial(Shader21);
q1.setTranslateX(225);
q1.setTranslateZ(225);
q2 = new Box(300, 5, 300);
q2.setTranslateX(225);
q2.setTranslateZ(525);
q3 = new Box(300, 5, 300);
q3.setTranslateX(525);
q3.setTranslateZ(525);
q4 = new Box(300, 5, 300);
q4.setTranslateX(525);
q4.setTranslateZ(225);

图 15-9 显示了游戏板象限 1 纹理映射和测试渲染的运行➤项目工作流程。正如你所看到的,一旦你的漫反射纹理贴图被应用,面顺序渲染问题就出现了!

A336284_1_En_15_Fig9_HTML.jpg

图 15-9。

Select Run ➤ Project to render and preview the first quadrant texture map application (the face order bug appears)

接下来,添加您的。setMaterial()方法调用您的其他三个象限框图元,并在方法调用参数列表中引用您正确的 Shader22 到 Shader24 PhongMaterial 对象。完成后,Shader 对象到 Box 原语的连接应该看起来像下面的 Java 代码,在图 15-10 中用黄色突出显示:

A336284_1_En_15_Fig10_HTML.jpg

图 15-10。

Complete the Shader object wiring to Box primitives for all quadrants so we can see the finished gameboard

q1 = new Box(300, 5, 300);
q1.setMaterial(Shader21);
q1.setTranslateX(225);
q1.setTranslateZ(225);

q2 = new Box(300, 5, 300);
q2.setMaterial(Shader22);
q2.setTranslateX(225);
q2.setTranslateZ(525);

q3 = new Box(300, 5, 300);
q3.setMaterial(Shader23);
q3.setTranslateX(525);
q3.setTranslateZ(525);

q4 = new Box(300, 5, 300);
q4.setMaterial(Shader24);
q4.setTranslateX(525);
q4.setTranslateZ(225);

正如你在图 15-11 中看到的,象限盒图元现在呈现出与 i3D 游戏板的其余部分相同的面渲染顺序问题。让我们暂时停止编码,看看是否可以找到其他 Java 开发人员在 JavaFX 9 游戏开发中遇到这种特殊 3D 模型人脸渲染问题的证据。正如你所想象的,用来做这项研究的工具是搜索引擎。

A336284_1_En_15_Fig11_HTML.jpg

图 15-11。

The diffuse texture mapping looks very professional, other than the face depth and rendering anomalies

在 JavaFX 9 dev 论坛上提交错误报告之前,让我们看看我是如何找到面部顺序呈现问题的解决方案的,我尝试使用 Google 搜索引擎和目标明确的关键字来完成这个问题。

使用 Google 解决 JavaFX 异常:使用 StackOverflow

要找到有类似问题的开发人员,请使用 Google 搜索引擎,输入您在屏幕上看到的最常见或最可能的问题描述。在这种情况下,这将是“错误的重叠形状”或“框面顺序渲染的问题”有时你可能不得不尝试几个不同的关键字字符串。在这种情况下,有几个正确的答案,那就是打开一个叫做深度缓冲的特性。这是一种处理密集型算法,因此默认情况下是关闭的。由于我们也得到一些锯齿状的边缘,我们可以打开另一个处理密集型算法,称为反走样。这两个都可以在重载的 Scene()构造函数中访问,所以只需对我们的 Scene scene 对象实例化做一个简单的修改就可以解决这两个问题!以下是关于此问题及其解决方案的两个 StackOverflow 答案的示例:

stackoverflow.com/questions/19589210/overlaping-shapes-wrong-overlapping-shapes-behaviour

   --OR--

stackoverflow.com/questions/28567195/javafx-8-3d-z-order-overlapping-shape-behaviour-is-wrong

场景的重载构造函数方法允许您将深度缓冲和抗锯齿作为 3D 场景对象的默认行为,类似于以下 Java 代码:

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

因此,我们需要添加 depthBuffer=true 和 SceneAntialiasing。平衡到我们在 createBoardGameNodes()方法中使用的 Scene()构造函数,正如你在图 15-12 (红色矩形)中看到的,我把它添加到了scene = new Scene(root, 1280, 640); Java 9 场景对象实例化语句的末尾。这将切换您的构造函数方法调用,以利用不同的重载构造函数方法来创建您的 3D 场景。

让我们添加一个名为 spinner 的 3D UI 元素,玩家可以使用它来随机旋转游戏板以选择主题。

创建 3D 用户界面元素:3D 微调器随机发生器

现在让我们重用我们的球体原始代码和沙滩球纹理图来创建一个 3D 用户界面(UI)元素,玩家可以单击它来旋转棋盘,以选择一个随机的主题(topic)类别。在类的顶部声明球体并将其命名为 spinner。然后用半径 60 实例化它,并用 Shader25 和 X,Y 位置-200,-500 配置它,这使它位于屏幕的左上角。使用 Y 旋转轴并将旋转值设置为 25 度,尝试将单词 SPIN 面向用户。您的 Java 代码,如图 15-12 所示,将如下所示:

A336284_1_En_15_Fig12_HTML.jpg

图 15-12。

Add a Sphere primitive named spinner, set the material and translation parameters, and fix the face order render bug

Sphere spinner;
...

spinner = new Sphere(60);
spinner.setMaterial(Shader25);
spinner.setTranslateX(-200);
spinner.setTranslateY(-500);
spinner.setRotationAxis(Rotate.Y_AXIS);
spinner.setRotate(25);

scene = new Scene(root, 1280, 640, true, SceneAntialiasing.BALANCED);

在我们能够渲染您的 3D 场景并查看新的微调器 UI 以确定我们是否需要以任何方式调整漫射纹理贴图之前,我们需要将其添加到 JavaFX 场景图。我将把它添加到顶部,直接在根目录下,因为 3D 用户界面最终会有自己的层次结构,就像 2D uiLayout 和 3D 游戏板一样。通过这种方式,如果我们想在任何时候影响 3D UI 元素作为一个整体,我们可以使用一行代码来引用 3D UI 分支,这将影响它下面的所有叶节点。现在,微调器将是根下面的一个叶节点。添加微调器的 Java 代码如图 15-13 所示,应该如下所示:

A336284_1_En_15_Fig13_HTML.jpg

图 15-13。

Add your spinner Sphere object to the top of the SceneGraph hierarchy using root.getChildren().addAll()

root.getChildren().addAll(gameBoard, uiLayout, spinner);

现在,我们可以使用“运行➤项目”工作流程并测试我们的新 Java 代码,将球体旋转器 UI 添加到我们在这个 3D 场景中创建的棋盘游戏中。我们还将能够看到,通过使用更复杂的重载 Scene()构造函数方法(具有五个参数,而不是只有三个参数),添加抗锯齿和深度缓冲算法(分别检查正确的面顺序渲染和在渲染过程中对粗糙边缘应用平滑)是否解决了我们的视觉质量问题。

正如你在图 15-14 中看到的,3D 游戏板层次结构,包括二十几个 3D 基本对象,如游戏板方块和游戏板象限,现在被渲染为一个内聚的 3D 模型。它最终看起来像你在大多数流行的棋盘游戏中看到的棋盘游戏(纸板游戏板),并且每个方块和象限将能够在你的代码中被单独访问和控制,即使游戏板模型看起来只是 3D 场景中的一个 3D 对象。这是我们在过去几章中努力学习和实现的。

A336284_1_En_15_Fig14_HTML.jpg

图 15-14。

The face rendering order problem has been fixed, and we're now getting a smooth, thin cardboard game

另一方面,spinner UI 元素没有给我们最初想要的视觉效果,它是一个沙滩球类型的对象,前面写着单词 SPIN。这是可以的,因为我们知道 pro Java 9 games 开发是一个迭代的细化过程,所以让我们考虑如何缩小单词 SPIN down,以便在 Sphere 原语上一次显示四个字母,而不是像当前呈现的那样只有两个字母。

缩小文本以使单词 SPIN 适合球体图元的四分之一,以及增加彩色条纹的数量(和厚度)的最简单方法是使该纹理贴图为 512 像素纹理。这将缩小所有的文本元素,使四个适合,我们可以复制和粘贴条纹和颜色移动他们添加更多的颜色。

接下来,让我们回到 GIMP,看看增强 spinner UI 漫反射纹理贴图的工作过程。

增强 3D 微调器纹理贴图:提高分辨率

如果您在优化的工作流程中使用 GIMP 的工具和算法,为 3D spinner UI 元素创建更详细的 512x512 像素纹理贴图的工作流程会比您想象的容易得多。我们可以在仅仅十几或两个“动作”中使分辨率加倍、条纹加倍、条纹颜色加倍、文本元素加倍,GIMP 会以我们所要求的最高质量水平为您完成所有的像素操作。

图 15-15 显示了移动的 GIMP 合成结果。首先要做的是添加另外 256 个像素到文档的右侧,使用图像➤画布大小➤宽度=512 ➤调整大小(按钮),这将添加 256 个像素的透明度到纹理贴图合成的右半部分。选择背景层和白色色样,使用画笔工具(第四行,第四个图标)填充背景层的右半部分,使其为 100%白色。接下来,右键单击彩色地图图层,选择复制图层选项,这将创建彩色地图复制图层,如图 15-15 所示。选择该图层,使用彩色➤色相-饱和度算法(菜单序列),将四种颜色都移动 60 度左右,创建四种不同的颜色,如图 15-15 所示。接下来,要将(y 或高度)尺寸调整为 512 个匹配像素,这一次使用图像➤缩放图像菜单序列,通过单击宽度和高度之间的链图标解锁纵横比,并将高度值设置为 512。这将拉伸颜色条来填充图像,这样你就不必像最初创建沙滩球纹理图那样做大量的选择-移动-填充工作。这种缩放操作也将使文本组件变高,这将使它们在球体微调器 UI 中更具可读性,尤其是在它旋转的时候。最后,右键单击 S、P、I 和 N 的第 2 层,并为每个层创建第 3 层和第 4 层。使用移动工具和右箭头键来精确定位它们。

A336284_1_En_15_Fig15_HTML.jpg

图 15-15。

Use a 256-pixel beach ball texture map to create a more detailed 512-pixel Sphere spinner UI texture map

最后,使用“文件”“导出为”覆盖项目/src/文件夹中的当前 gameboardspin.png 文件。由于该文件名已经在 diffuse25 图像对象实例化语句中被引用,您所要做的就是将宽度和高度值从 256 更改为 512,当映射到相同大小的球体图元上时,这将有助于减少球体上的条纹和文本元素(字母)的大小,因此将显示四个字母(SPIN)而不是两个字母(SP,如图 15-14 所示)。此时,您所要做的就是将旋转值调整到 20 到 30 度之间,以便单词 SPIN 将位于名为 spinner 的球体对象的中心,这样用户就知道当单击这个球体 spinner UI 对象时会发生什么。

您的 diffuse25 对象实例化的新 Java 9 语句应该类似于下面的 Java 代码,在图 15-16 中也用黄色和蓝色突出显示:

A336284_1_En_15_Fig16_HTML.jpg

图 15-16。

Modify the width and height resolution parameters and change them from 256 pixels to 512 pixels each

diffuse25 = new Image("/gameboardspin.png", 512, 512, true, true, true);

接下来需要做的事情是“调整”所有微调器对象配置设置,使球体图元变大一点,并将其移近屏幕的角落,以便它远离游戏板。我做了半径 64 和 Y 平移-512 来进一步上移。我发现 30 度的旋转值使单词 SPIN 居中。图 15-17 中突出显示的 Java 代码应该如下所示:

A336284_1_En_15_Fig17_HTML.jpg

图 15-17。

Tweak the Sphere spinner settings to radius 64 to make the spinner bigger and rotate it to 30 degrees to see SPIN

Sphere spinner;
...

spinner = new Sphere(64);
spinner.setMaterial(Shader25);
spinner.setTranslateX(-200);
spinner.setTranslateY(-512);
spinner.setRotationAxis(Rotate.Y_AXIS);
spinner.setRotate(30);

图 15-18 显示了运行➤项目的 Java 代码测试工作流程。正如你所看到的,游戏板现在看起来非常专业,3D 旋转器 UI 看起来像一个旋转器,并使用大的可读字母标记为 SPIN。

A336284_1_En_15_Fig18_HTML.jpg

图 15-18。

The spinner Sphere UI element now looks more like a spinner, and the word SPIN is now visible to the user

现在我们已经完成了 3D 棋盘游戏的设计和编码,我们可以回到 javafx.graphics 模块中一些常用于游戏的更具技术性的 JavaFX 类。技术含量最高的领域之一是 3D 动画,我们将在接下来的内容中对旋转器和游戏板等进行动画制作,从而将 3D boardGame 节点层次带入第四维时间!之后,我们可以添加交互性,使之成为 i3D 桌游!在我们继续添加动画之前,我只需要让事情变得完美和一个 3D 旋转器到位。

摘要

在第十五章中,我们构建了 3D“spinner”球体原始 UI 元素,它允许用户在游戏板上随机旋转以选择主题象限。我们还完成了游戏板的纹理映射,并找出了如何修复面部渲染异常,这些异常使游戏板模型(游戏的核心)无法正确渲染,因此无法获得专业的外观。该解决方案涉及使用更复杂的场景对象实例化,包括打开深度缓冲算法的标志以及启用所有 3D 对象的场景范围抗锯齿的常数。这消除了 Y 维度(高度)面部渲染错误,以及我们在游戏板组件边缘看到的锯齿状边缘。

我们为四个游戏棋盘中心象限和 spinner UI 元素创建了五个新的纹理贴图,该元素允许玩家旋转棋盘以确定他们的下一步行动,在这种情况下,是一个教育主题。

我们添加了漫射纹理贴图图像对象和利用这些纹理贴图的 Phong 着色器定义。我们还添加了 Java 代码,将 spinner UI 添加到 SceneGraph 层次结构中,并获得了更多使用 GIMP 的实践。

在第十六章中,我们将学习 JavaFX 9 中所有强大的动画相关过渡类。

十六、3D 游戏动画创建:使用动画过渡类

现在,您已经创建了多层游戏板组节点(子类)层次结构,对该层次结构下的所有 3D 组件进行了纹理处理,确保您的 3D 游戏板模型中心旋转,并创建了一个 3D 旋转器 UI 来随机旋转该游戏板 3D 模型(层次结构)以选择一个随机象限,现在是时候使用自定义的createAnimationAssets()方法为旋转器添加动画对象以创建在游戏过程中使用的随机“旋转”了。我们还将设置 3D 对象鼠标单击事件处理代码来触发动画和逻辑,这将在进行旋转之前随机化您的旋转变换参数。

在本章中,我们将详细介绍抽象动画和过渡超类以及所有强大的属性过渡子类,您可以在 i3D 棋盘游戏中将它们实现为不同类型的动画对象。我们将为您的游戏板和旋转器制作旋转动画,以及旋转器的平移(运动)动画。

动画 3D 素材:动画超类

public abstract Animation 类扩展了 Object,保存在 javafx.animation 包中,其他与动画相关的类也是如此,其中一些我们将在游戏中使用,本章将详细介绍。Animation 超类有两个直接已知的子类,Timeline 和 Transition。Transition 有十个预定义的动画(算法)子类,随时可以应用到您的游戏开发中,因此我们将重点关注它们,因为它们可以立即有效地使用。javafx.animation 包可以写一整本书,而我只有一章,所以我将介绍用来创建 pro Java 9 游戏的最有效的动画类。

动画超类的 Java 9 类层次结构向我们展示了该类是临时编码的,以提供对象动画功能,因为它没有自己的超类,因此看起来像下面的类层次结构:

java.lang.Object
  > javafx.animation.Animation

抽象 Animation 类不能直接创建动画对象,但它确实为 JavaFX API 中使用的所有动画类提供了核心功能。唯一的例外是 AnimationTimer 类(一个脉冲引擎),它实现了一个核心定时器脉冲引擎(因此,它更像一个定时器类,而不是动画类),非常适合基于 2D 精灵的游戏。我在 Java 8 游戏开发入门中进入了这个类,在那里我将详细介绍 i2D 游戏开发。在本书中,我更关注 i3D 游戏的开发,所以我将借此机会介绍一些其他有用的(也是固定的或预编码的)动画过渡类。

通过设置 cycleCount 属性,动画可以运行有限的次数。要使任何动画“乒乓”(即从开始到结束再到开始来回运行),请将 autoReverse 属性设置为 true 否则,使用一个假布尔值,我们将在我们的 pro Java 9 游戏中使用它来随机地向一个方向旋转 i3D 游戏板。

要在实例化和配置动画对象后播放它,可以调用 play()方法或 playFromStart()方法。动画对象的 currentRate 属性设置您的速度和方向。通过反转 currentRate 的数值,您可以切换您的播放方向。每当“持续时间”属性被“满足”(耗尽、结束、耗尽、达到、过期等)时,动画将停止。

通过使用具有不定常数的 cycleCount 属性,可以为动画对象设置不定的持续时间(有时称为循环或无限循环)。以这种方式配置的动画对象会重复运行,直到调用 stop()方法。这将停止正在运行的动画,并将其回放重置到起点(属性设置)。也可以通过调用 pause()来暂停动画,下一个 play()调用将从动画暂停的地方继续播放动画,除非您使用. playFromStart()方法调用。接下来让我们看看动画超类的属性。这些都是由 Transition 超类及其所有子类继承的,所以在本书的剩余部分中,您将在您的 Pro Java 9 游戏开发代码中使用它们。

autoReverse BooleanProperty 用于定义动画对象是否应该在交替循环中反转其方向。currentRate 是一个 ReadOnlyDoubleProperty,用于指示动画对象的其他设置正在播放的当前速度(和方向,由正值或负值表示)。

current time ReadOnlyObjectProperty用于定义动画对象回放位置,cycleCount IntegerProperty 用于定义播放动画对象的周期数。cycle duration ReadOnlyObjectProperty是一个只读变量,可用于指示动画对象的一个周期的持续时间。这是从时间 0 到动画结束所花费的时间,默认速率为 1。

delay ObjectProperty 用于延迟动画的开始时间,onFinished ObjectProperty 属性包含在动画对象播放结束时触发的 ActionEvent。rate DoubleProperty 用于定义动画播放的速度和方向。注意,由于硬件的限制,这个速率并不总是可能的,所以有一个 currentRate 属性来保存实际达到的回放速率。

status ReadOnlyObjectProperty<animation.status>属性包含动画对象的枚举状态常量。枚举动画。Status helper 类包含三个常量:PAUSED、RUNNING 和 STOPPED。</animation.status>

total duration ReadOnlyObjectProperty属性保存一个只读变量以指示动画对象的总持续时间,该变量乘以 cycleCount 属性以计算它重复的次数。所以,duration 是一个周期,totalDuration 等于(delay + (duration * cycleCount))。

动画有一个静态(嵌套)类,它是一个动画。Status 类,保存表示状态的可能状态的枚举常量。这些包括暂停、运行和停止。

Animation 有一个数据字段,即 static int 不定字段,该字段用于指定一个动画,该动画将无限重复自身,直到。调用 stop()方法。

Animation 有两个重载的构造函数,一个是简单的(空参数区域)构造函数,创建一个空的或未配置的动画对象,另一个用目标帧速率配置动画对象。这些构造函数方法(它们的子类的构造函数方法格式,因为它们不能在您的代码中直接使用)应该看起来像下面的 Java 代码:

protected Animation()                        // Protected: Cannot Be Directly Instantiated
protected Animation(double targetFramerate)

你可以用几十种方法来控制你的动画对象,在这一章中,就是各种各样的过渡子类。这些从动画类继承方法,通过转换类,到各种属性转换类,我们将用于 Java 9 游戏。

autoReverseProperty()方法调用返回一个 BooleanProperty,该属性定义动画对象是否将在(交替)播放周期之间反转其方向。currentRateProperty()方法调用返回一个只读 double 变量,用于指示动画对象播放的当前速度和方向。

a。rateProperty()方法调用返回动画预期播放的速度和方向的双值。. statusProperty()方法调用返回动画的 ReadOnlyObjectProperty <animation.status>状态,而. currentTimeProperty()方法调用返回动画对象的播放位置。那个。cycleCountProperty()使用代表 cycleCount 属性的整数值返回动画对象中的循环数。</animation.status>

那个。cycleDurationProperty()方法返回一个只读变量,表示动画一个周期的持续时间,即从时间 0.0 开始以默认速率 1.0 播放到动画结束所用的时间。那个。delayProperty()方法调用返回延迟动画对象开始的延迟属性的持续时间。

a。totalDurationProperty()方法调用返回只读的持续时间属性设置,以指示动画对象的总持续时间。值得注意的是,该值将包括所有动画重复周期。

那个。getCuePoints()方法调用返回包含动画对象提示点的 ObservableMap 。这些提示点应该用来标记动画对象中的重要位置。那个。getCurrentRate()方法调用将为动画对象的 CurrentRate 属性返回双精度值。

的。getCurrentTime()方法调用将返回动画对象的 CurrentTime 属性的值。

的。getCycleCount()方法调用将返回动画对象 CycleCount 属性的整数值。getCycleDuration()方法调用将返回 CycleDuration 属性的值。的。getDelay()方法调用将返回 Delay 属性的值。

a。getOnFinished()方法调用将返回 OnFinished 属性的 EventHandler 值,而. getRate()方法调用将返回 Rate 属性的 double 值。那个。getStatus()方法调用将返回动画。状态属性的状态值。

那个。getTargetFramerate()方法调用将返回目标帧速率,即动画对象运行的最大帧速率(使用每秒帧数)。

那个。getTotalDuration()方法调用将返回 TotalDuration 属性的持续时间值。

那个。isAutoReverse()方法调用将返回 AutoReverse 属性的值。

虚空。jumpTo(持续时间)方法调用将跳转到动画对象中的给定位置,就像 void 一样。jumpTo(String cuePoint)方法调用,使用 cuePoint 参数而不是 Duration 参数。

那个。onFinishedProperty()方法调用返回在动画对象播放结束时触发的 ObjectProperty >动作。虚空。pause()方法调用用于暂停动画对象的回放循环。虚空。play()方法调用将从动画对象的当前位置按照 rate 属性指示的方向播放动画对象。

虚空。playFrom(持续时间)方法调用是一种方便的方法,它将从特定的位置播放动画,就像 void 一样。使用 cuePoint 而不是持续时间的 playFrom(String cuePoint)方法调用。一片空白。playFromStart()方法调用将从动画对象的初始位置向前播放动画对象。一片空白。setAutoReverse(布尔值)方法调用可用于设置 AutoReverse 属性的值。

虚空。setCycleCount(int value)方法调用可用于设置 CycleCount 属性的值。受保护的空间。setCycleDuration(持续时间值)方法调用可用于设置 CycleDuration 属性的值。虚空。setDelay(持续时间值)方法调用可用于设置 Delay 属性的值。虚空。set OnFinished(EventHandlervalue)方法调用可用于设置 onFinished 属性的值。

虚空。setRate(double value)方法调用可用于设置动画对象的 Rate 属性值。受保护的空间。setStatus(动画。Status value)方法调用可用于设置 Status 属性的常数值。

虚空。stop()方法调用用于停止动画对象播放循环,需要注意的是,该方法调用会将播放头重置到其初始开始位置,因此它可以用作重置。接下来,让我们看看另一个抽象超类 Transition,它是动画的一个子类,用于创建属性转换。

自动对象动画:过渡超类

公共抽象过渡超类与其子类一起保存在 javafx.animation 包中,这些子类是预定义的算法,用于应用不同类型的属性动画,而不必使用时间轴或动画计时器或设置关键帧。因此,Transition 子类是动画类的最高(最高级)形式,非常适合 pro Java 9 游戏开发,因为它们允许您将时间集中在游戏开发上,而不是重新发明 Java 动画代码。这就是为什么我们要介绍这些类来快速实现游戏动画!Transition 超类的 Java 类层次结构如下所示:

java.lang.Object
  > javafx.animation.Animation
    > javafx.animation.Transition

已知的可以快速有效地实现以增强 Java 游戏开发过程的直接子类包括 RotateTransition、ScaleTransition 和 TranslateTransition,以调用基本的 3D 对象转换(这些也可以用于 2D、文本和 UI 元素);以及用于处理 2D(即向量)对象的 FadeTransition、FillTransition、StrokeTransition 和 path transition(fade transition 也适用于文本和 UI 元素)。还有两个子类用于创建复合(或复杂)动画,它们无缝地结合了这些其他类型的属性转换。其中包括 ParallelTransition 和 SequentialTransition,parallel transition 同时执行属性转换,sequential transition 串行执行一串属性转换(一个接一个)。还有一个 PauseTransition 子类用于将“等待状态”引入复杂的动画,这将允许更多的运动真实感添加到您试图创建的特殊动画效果中。

抽象过渡超类包含了基于过渡的动画所需的所有基本功能。这个类提供了一个定义属性动画的框架,如果你愿意,也可以用来定义你自己的转换子类。游戏中使用的大多数类型的过渡都已经提供了(淡入淡出、变换、路径等)。),因此您所要做的就是实现已经创建、调试和优化的代码。

Transition 子类都需要实现一个叫做.interpolate(double)的方法。只要 Transition 子类(object)正在运行,就会在动画对象的每个循环中调用该方法。除了。interpolate()方法,任何扩展 Transition 的子类都需要使用Animation.setCycleDuration(javafx.util.Duration)方法调用来设置动画周期的持续时间。

例如,应该使用 duration 属性设置此持续时间(如 RotateTransition.duration 中所示)。但是它也可以由扩展类来计算,如在 ParallelTransition 和 FadeTransition 中所做的那样。

过渡类有一个类型为 ObjectProperty 的插值器属性,用于控制每个过渡周期的加速和减速时间。从 Animation 超类继承的属性包括 autoReverse、currentRate、currentTime、cycleCount、cycleDuration、delay、onFinished、status、Rate 和 totalDuration 属性。还有一个动画。Status 嵌套类,继承自 Animation 类。

有两个重载的构造函数;一个构造一个空的转换子类对象,另一个构造一个帧速率配置的转换子类。这些看起来像下面的两个构造函数方法:

Transition()
Transition(double targetFramerate)

最后,这个抽象超类增加了六个方法,其中大部分都与 interpolate 属性相关。这个类也继承了我们在上一节中介绍的方法。getCachedInterpolator()方法返回启动 Transition 子类时设置的插值器属性。那个。getInterpolator()方法将获取插值器属性的值,而 void。set Interpolator(Interpolator value)方法将为插值器属性设置一个值。如前所述,受保护的抽象虚空。interpolate(double)方法需要由 Transition 子类提供,而。interpolatorProperty()方法控制加速和减速的时间。

最后是。getParentTargetNode()方法调用将返回为 Transition 子类播放动画的目标节点。接下来,让我们详细看看其中一个 Transition 子类,然后我们可以在 JavaFXGame Java 代码中实现它来旋转(动画旋转)游戏板组节点。

制作 3D 对象旋转动画:使用 RotateTransition 类

public final RotateTransition 类将用于创建旋转动画并扩展 Transition 超类。它将与所有其他动画和动画计时器相关的类一起存储在 javafx.animation 包中。RotateAnimation 子类的 Java 类层次结构应该类似于下面的 Java 类层次结构:

java.lang.Object
  > javafx.animation.Animation
    > javafx.animation.Transition
      > javafx.animation.RotateTransition

RotateTransition 类(对象)可用于创建持续时间与其持续时间设置一样长的旋转动画。这是通过定期更新它所附加到的节点的 rotate 变量来完成的。应使用度数指定旋转角度值。旋转从 fromAngle 属性(如果提供)开始,否则将从节点的当前(上一个)旋转值开始。旋转将停止使用 toAngle 值(如果提供),否则将使用 start 值加上 byAngle 值。如果 toAngle 和 byAngle 都已指定,toAngle 值将优先。

RotateTransition 将属性添加到从动画和过渡中继承的属性中,这有助于定义旋转过渡算法。这些属性包括 axis ObjectProperty 属性,用于指定 RotateTransition 对象的旋转轴;node ObjectProperty 属性,用于指定应该受 RotateTransition 影响的目标节点对象;以及 duration ObjectProperty 属性,用于指定 RotateTransition 的持续时间。

byAngle DoubleProperty 可用于指定从 RotateTransition 开始的增量停止角度值。fromAngle DoubleProperty 可用于指定 RotateTransition 的起始角度值。toAngle double 属性可用于指定 RotateTransition 的停止角度值。

前面讨论的嵌套类、字段和属性是从动画和过渡中继承的。

RotateTransition 类有三个重载的构造函数方法。一个创建未配置的 RotateTransition,一个创建持续时间配置的 RotateTransition,一个创建持续时间和节点对象配置的 RotateTransition。这三个构造函数方法看起来像下面的 Java 代码:

RotateTransition()
RotateTransition(Duration duration)
RotateTransition(Duration duration, Node node)

除了从这个类扩展的动画和过渡超类继承的方法之外,还有 19 个方法专门用于这个类。那个。axisProperty()方法调用使用 ObjectProperty 格式指定 RotateTransition 的旋转轴。那个。byAngleProperty()方法为 RotateTransition 对象指定增量停止角度值,该值是从开始角度的偏移量。

那个。durationProperty()方法使用 ObjectProperty 指定 RotateTransition 的持续时间。那个。fromAngleProperty()方法使用 DoubleProperty 指定此 RotateTransition 的起始角度值。那个。getAxis()方法调用使用 Point3D 对象获取轴属性的值。

那个。getByAngle()方法将获得 ByAngle 属性的 double 值。那个。getFromAngle()方法调用将获得 FromAngle 属性的 double 值。那个。getToAngle()方法调用将获得 ToAngle 属性的 double 值。那个。getNode()方法调用将获取节点属性的节点对象值,而。getDuration()方法调用将获得 Duration 属性的值。受保护的空间。如你所知,interpolate(double value)方法调用必须由 Transition 超类的子类实现提供。那个。nodeProperty()方法指定 RotateTransition 的目标 ObjectProperty 。

虚空。setAxis(Point3D value)方法调用用于设置属性轴的值。虚空。setByAngle(double value)方法调用用于设置 ByAngle 属性的值。虚空。setDuration(持续时间值)方法调用用于设置属性 Duration 的值。虚空。setFromAngle(double value)方法调用用于设置 FromAngle 属性的值。虚空。setNode(节点值)方法调用用于设置属性节点的值。虚空。setToAngle(double value)方法调用用于将属性值设置为 Angle。那个。toAngleProperty()方法使用 DoubleProperty 指定 RotateTransition 的停止角度值。接下来让我们实现 rotGameBoard 和 rotSpinner RotateTransition 对象,给您一些实际操作的经验。

RotateTransition 示例:设置 RotateAnimation 素材

让我们使用下面的 Java 语句创建一个 createAnimationAssets()方法来保存 RotateTransition、TranslateTransition 和其他 Transition 子类对象,在图 16-1 中用黄色(和红色波浪下划线)突出显示:

A336284_1_En_16_Fig1_HTML.jpg

图 16-1。

Add a createAnimationAssets() method call at the end of the custom method call list in the start() method

createAnimationAssets();

记得双击 javafxgame 中的创建方法“createAnimationAssets()”。JavaFXGame 选项,并让 NetBeans 为您编写一个引导方法。在本章的这一节中,您将使用 RotateTransition 对象实例化和配置代码替换占位符 Java 代码。

你需要做的第一件事是声明一个 RotateTransition 对象在类的顶部使用,并将其命名为 rotGameBoard,因为这是该对象将要做的事情。在你的内心。createAnimationAssets()方法,实例化 rotGameBoard 对象并配置为播放五秒;然后将其连接到游戏板组节点,如下面的 Java 9 代码所示,并在图 16-2 中用浅蓝色和黄色突出显示:

A336284_1_En_16_Fig2_HTML.jpg

图 16-2。

Declare a rotGameBoard object at the top of your class and instantiate it inside createAnimationAssets()

RotateTransition rotGameBoard;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
}

现在,您可以开始使用各种。set()方法调用,您在本章的前一节中已经了解了。使用设置 Y 旋转轴。setAxis(旋转。并使用. setCycleCount(1)方法调用将 cycleCount 属性设置为一个周期。使用. setRate(0.5)方法调用 rotGameBoard 对象,将 rate 属性设置为 50%的速度。核心动画对象设置的 Java 语句应该类似于下面的 Java 9 语句,它们在图 16-3 的底部突出显示:

A336284_1_En_16_Fig3_HTML.jpg

图 16-3。

Configure the rotGameBoard RotateTransition object with a y-axis, cycleCount of 1, and rate of 50 percent speed

RotateTransition rotGameBoard;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
}

你已经知道为什么我们用 y 轴旋转;然而,你可能想知道为什么我们只使用一个周期。原因是,一旦我们通过指定 fromAngle 和 toAngle 值使这个 RotateTransition 交互,这些值将在每个 rotGameBoard.play()方法调用之前使用来自随机旋转生成器的代码进行设置,我们将在后面编写代码,我们将使用这些角度之间的差异来控制旋转的次数(目前这是 1080 或三次旋转);因此,我们仅使用一个循环。出于代码测试的目的,我使用了三次旋转。

速率设置 1 太快了,无法获得平滑的旋转动画,游戏板不应该旋转得那么快,所以我将这个默认值 1.0 减少了 50%到 0.5,以向您展示速率变量如何为您提供微调的速度控制。

接下来,让我们添加所需的插值器类常数规格,这将是默认的线性,因为我们需要一个平滑、均匀的旋转。这是使用。setInterpolator()方法调用和插值器。线性常数。最后,我们要添加两个最重要的配置语句,它们告诉 RotateTransition 引擎旋转的开始角度(fromAngle 属性)和结束角度(toAngle 属性)。使用这些将允许我们控制旋转在哪个象限开始(45、135、225 或 315)和结束。现在,我们将使用从 45 度角开始的三次完整旋转(1080 ),对于 toAngle 来说是 1125°。要开始(并测试)动画,您还需要一个. play()方法调用,如下面完整的 Java 方法体所示,并在图 16-4 的底部以黄色和浅蓝色突出显示:

A336284_1_En_16_Fig4_HTML.jpg

图 16-4。

Configure the rotGameBoard object with a LINEAR interpolator, a fromAngle of 45, and a toAngle of 1125

RotateTransition rotGameBoard;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotGameBoard.setFromAngle(45);
    rotGameBoard.setToAngle(1125);
    rotGameBoard.play();
}

图 16-5 显示了运行➤项目的工作过程,其中一个游戏板处于其旋转周期的中间。屏幕截图无法显示平滑的运动,但您可以知道游戏板不在其四个象限的“静止”位置之一(45、135、225、315 度),因为游戏板的点不在屏幕底部的中心。在图 16-5 中,当 3D 游戏板组节点还在动画时,我按了一个 PrintScreen 键。

A336284_1_En_16_Fig5_HTML.jpg

图 16-5。

Use your Run ➤ Project work process, click Start Game, and watch your gameboard spin around smoothly

同样重要的是要注意,当您测试您的动画代码时,您需要在应用启动时立即单击 Start Game 按钮 UI 元素(稍后,这将通过单击 3D spinner UI 元素来触发,您可能已经猜到了)。这是为了让您可以看到您的动画特性,这是我们在本章中开发的,因为目前您的 Java 9 代码在构造和配置动画(过渡子类)对象后立即开始播放生命周期。所以,点击你的开始游戏 2D 用户界面按钮,只要它出现!

稍后,当我们进入如何捕捉 3D 对象上的鼠标点击(或屏幕触摸)时,例如您的 spinner UI 元素,我们将通过单击 spinner UI 元素来触发 rotGameBoard.play(),以随机旋转游戏板来选择新的象限。当下一个玩家的回合准备好时,我们将触发 rotSpinner.play(),以便他们可以旋转游戏板。在本书的剩余部分,我们将开发这个动画代码的复杂性。

在本章的后面,我们将使用 TranslateTransition 和 RotateTransition,使用 ParallelTransition,这将允许我们将 3D spinner UI 元素在视图内外进行动画处理,以便玩家知道何时使用它来随机旋转游戏板,以选择一个新的象限(一个新的内容主题动物-植物-矿物或地标类别)用于游戏循环。

接下来,让我们添加 rotSpinner RotateTransition 对象。首先,通过在类顶部的 rotGameBoard 对象名称后添加 rotSpinner 对象名称,将 RotateTransition 声明转换为复合语句。剪切并粘贴 rotGameBoard 语句,将 rotGameBoard 更改为 rotSpinner,并确保将实例化的节点参数从 GameBoard 更改为 Spinner。从角度改变到 30 度(你在第十五章中开发的初始值)和角度到 1110 度(1080 + 30)。您的 Java 9 代码应该看起来像下面的方法体,它也在图 16-6 的底部突出显示:

A336284_1_En_16_Fig6_HTML.jpg

图 16-6。

Add a rotateSpinner RotateTransition object and configure it with the same parameters as rotGameBoard

RotateTransition rotGameBoard, rotSpinner;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotGameBoard.setFromAngle(45);
    rotGameBoard.setToAngle(1125);
    rotGameBoard.play();
    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5);
    rotSpinner.setInterpolator(Interpolator.LINEAR);
    rotSpinner.setFromAngle(30);
    rotSpinner.setToAngle(1110);
    rotSpinner.play();
}

使用运行➤项目工作流程查看游戏板和旋转器的旋转,如图 16-7 所示。

A336284_1_En_16_Fig7_HTML.jpg

图 16-7。

Select Run ➤ Project and click Start Game to preview the game board and spinner rotation

正如你所看到的,唯一的问题是你的“SPIN”微调器是向后旋转的,我们希望单词 SPIN 向前旋转,所以我们需要通过将 fromAngle 设置为 30 和 toAngle 设置为-1050(1080 = 30-1050)来改变方向。这里显示了最终的 Java 代码块,在图 16-8 中用黄色和蓝色突出显示:

A336284_1_En_16_Fig8_HTML.jpg

图 16-8。

Adjust the rotSpinner.setToAngle() method call to spin in a negative direction so the spinner UI spins forward

RotateTransition rotGameBoard, rotSpinner;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotGameBoard.setFromAngle(45);
    rotGameBoard.setToAngle(1125);
    rotGameBoard.play();
    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5);
    rotSpinner.setInterpolator(Interpolator.LINEAR);
    rotSpinner.setFromAngle(30);
    rotSpinner.setToAngle(-1050); // Reverse rotation direction using a negative toAngle value
    rotSpinner.play();
}

接下来,让我们看看平移过渡,它可用于在 3D 场景中以 X、Y 或 Z 维度移动对象。我们将使用它来将我们的 spinner UI 元素带到(离开)屏幕上,因为在游戏过程中需要它来允许玩家随机旋转游戏板来选择他们的新主题象限。

制作节点移动的动画:使用 TranslateTransition 类

public final TranslateTransition 类扩展了 public abstract Transition 超类,保存在 javafx.graphics 模块的 javafx.animation 包中。TranslateTransition 创建移动(平移)动画,其持续时间与其 duration 属性一样长。通过以插值器常数定义的间隔更新正在设置动画的节点的 translateX、translateY 和 translateZ 变量(属性)来创建运动。翻译将从“from”值(fromX,fromY,fromZ)开始,如果提供了一个;否则,算法将使用节点对象的当前位置(translateX,translateY,translateZ)值。翻译停止在“to”值(toX,toY,toZ),如果提供的话;否则,它将使用起始值加上 byX、byY 或 byZ 值。如果同时指定了“到”(toX,toY,toZ)和“到”(byX,byY,byZ)值,则“到”值(toX,toY,toZ)优先。

java.lang.Object
  > javafx.animation.Animation
    > javafx.animation.Transition
      > javafx.animation.TranslateTransition

TranslateTransition 类有 11 个属性,其中 9 个涉及到 X、Y 和 Z 3D 坐标的 to、from 和 by 规范。另外两个是持续时间属性和节点属性,它们定义动画的时间长度以及它影响的节点对象。byX 属性用于为 TranslateTransition 指定增量停止 X 坐标双精度值,该值从起始值计算得出。byY 属性用于为 TranslateTransition 指定从起始值计算的增量停止 Y 坐标双精度值。byZ 属性用于为 TranslateTransition 指定从起始值计算的增量停止 Z 坐标双精度值。fromX 属性用于指定 TranslateTransition 的起始 X 坐标双精度值。fromY 属性用于指定 TranslateTransition 的起始 Y 坐标双精度值。fromZ 属性用于指定 TranslateTransition 的起始 Z 坐标双精度值。toX 属性用于指定 TranslateTransition 的停止(静止或最终)X 坐标值。toY 属性用于指定 TranslateTransition 的停止(静止或最终)Y 坐标值。toZ 属性用于指定 TranslateTransition 对象的停止(静止或最终)Z 坐标值。

TranslateTransition 有三个重载的构造函数方法;一个为空,一个指定了持续时间,一个指定了持续时间和节点属性。它们看起来像这样:

TranslateTransition()
TranslateTransition(Duration duration)
TranslateTransition(Duration duration, Node node)

这个类有将近 36 种方法,其中 27 种(9 组,每组 3 种)处理 from、to 和 by 属性。这是因为对于每个 X、Y 和 Z 属性,都有一个. get()、一个. set()和一个. property()方法。还有一些方法可用于持续时间、节点和插值器属性。所有的 X、Y 和 Z 方法都使用 double 值。A .byXProperty()方法用于将停止 X 坐标值指定为从 TranslateTransition 开始的增量偏移量。那个。byYProperty()方法用于将偏移增量停止 Y 坐标值指定为距 TranslateTransition 起点的偏移。那个。byZProperty()方法用于将增量停止点 X 坐标值指定为距 TranslateTransition 起点的偏移量。

那个。fromXProperty()方法调用用于指定 TranslateTransition 的起始 X 坐标值。

那个。fromYProperty()方法调用用于指定 TranslateTransition 的起始 Y 坐标值。

那个。fromZProperty()方法调用用于指定 TranslateTransition 的起始 Z 坐标值。

那个。getByX()方法调用用于获取 ByX 属性的值。那个。getByY()方法调用用于获取 ByY 属性的值。那个。getByZ()方法调用用于获取 ByZ 属性的值。

那个。getFromX()方法调用用于从 FromX 获取属性值。那个。getFromY()方法调用用于从 mY 获取属性值。那个。getFromZ()方法调用用于从 FromZ 获取属性值。那个。getToX()方法调用用于获取属性 ToX 的值。那个。getToY()方法调用用于获取 ToY 属性的值。那个。getToZ()方法调用用于获取属性 ToZ 的值。

虚空。setByX(double value)方法调用用于设置(指定)属性 ByX 的值。虚空。setByY(double value)方法调用用于设置(指定)属性值 ByY。虚空。setByZ(double value)方法调用用于设置(指定)ByZ 属性的值。虚空。setFromX(double value)方法调用用于设置(指定)属性 FromX 的值。虚空。setFromY(double value)方法调用用于设置(指定)属性 FromY 的值。虚空。setFromZ(double value)方法调用用于设置(指定)属性 FromZ 的值。

虚空。setToX(double value)方法调用用于设置(指定)属性 ToX 的值。虚空。setToY(double value)方法调用用于设置(指定)属性 ToY 的值。一片空白。setToZ(double value)方法调用用于设置(指定)属性 toX 的值。

那个。toXProperty()方法调用用于指定 TranslateTransition 对象的停止 X 坐标值。那个。toYProperty()方法调用用于指定 TranslateTransition 对象的停止 Y 坐标值。那个。toZProperty()方法调用用于指定 TranslateTransition 对象的停止 Z 坐标值。

那个。durationProperty()方法调用将返回 TranslateTransition 的当前持续时间属性。使用. getDuration()方法调用来获取 TranslateTransition duration 属性的持续时间值。虚空。setDuration(持续时间值)可用于设置(指定)Duration 属性的持续时间值。

那个。nodeProperty()方法调用将返回 TranslateTransition 的目标节点 node 属性。那个。getNode()方法调用将获取(读取)TranslateTransition 的节点属性的节点对象引用值。虚空。setNode(节点值)方法调用将为 TranslateTransition 节点属性设置节点值。虚空。插值(double frac)方法调用总是需要由 Transition 的子类提供。

接下来,让我们实现一个 TranslateTransition 动画对象,该对象将 spinner UI 元素移入和移出屏幕。这些动画对象最终将被命名为 moveSpinnerOn 和 moveSpinnerOff。之后,我们将进入 ParallelTransition 类,并结合移动和旋转来旋转屏幕上的 spinner UI 元素,从左角到右角。

TranslateTransition 示例:设置平移动画资源

让我们将 TranslateTransition 动画对象添加到您的游戏项目中,方法是在类的顶部声明一个名为 moveSpinner 的对象,然后在 RotateTransition Java 代码之后,在 createAnimationAssets()方法中实例化它。引用微调器节点,并使用五秒钟的持续时间。接下来,使用以下 Java 语句,将 moveSpinnerOn 动画对象配置为在屏幕顶部移动 1150 X 个单位(实际上是 1350 个单位,因为微调器当前处于-200),并将 cycleCount 属性设置为一个周期,在图 16-9 中以黄色突出显示:

A336284_1_En_16_Fig9_HTML.jpg

图 16-9。

Declare a moveSpinnerOn TranslateTransition at the top of the class and instantiate it in createAnimationAssets()

TranslateTransition moveSpinnerOn;
...

moveSpinnerOn = new TranslateTransition(Duration.seconds(5), spinner);
moveSpinnerOn.setByX(1150);
moveSpinnerOn.setCycleCount(1);

接下来,让我们看一下 ParallelTransition 类,因为我们需要使用此对象算法来将 spinner rotSpinner 动画对象与 moveSpinnerOn 动画对象相结合,这样您的最终结果是一个在屏幕顶部移动的同时旋转的 spinner。

合并动画属性:使用 ParallelTransition 类

公共 final 类 ParallelTransition 扩展了抽象 Transition 超类,可以在 javafx.animation 包中找到,这个包可以在 javafx.graphics 模块中找到。该过渡并行播放一系列动画对象,这意味着同时(一个接一个被称为串行或顺序)。如果没有使用方法调用(通常是构造函数方法)显式指定 Node 属性,则此转换的子级将继承 Node node 属性。其原因是 ParallelTransition 只是将现有的动画对象合并在一起,因此在合并的动画中可能已经指定了一个节点。ParallelTransition 的 Java 类层次结构如下所示:

java.lang.Object
  > javafx.animation.Animation
    > javafx.animation.Transition
      > javafx.animation.ParallelTransition

ParallelTransition 类只有一个本地属性,即 ObjectProperty 节点属性,它是组合动画将应用到的节点对象。如果未指定节点,将使用子动画对象节点属性。如果指定了一个节点,则该节点将被设置(即指定)给所有本身没有定义任何目标节点属性的子转换。

ParallelTransition 类包含四个重载的构造函数方法。第一个创建一个空对象,第二个指定子动画对象的列表,第三个指定要受影响的节点,第四个指定要受影响的节点对象和子动画对象的列表。第二个和第四个构造函数方法是最常用的。我们将为我们的子动画对象使用第二个构造函数;两个引用的动画过渡对象都将微调器节点对象指定为 ParallelAnimation 对象的目标。这些构造函数方法的 Java 代码如下所示:

parallelTransition = new ParallelTransition();
parallelTransition = new ParallelTransition(Animation... children);
parallelTransition = new ParallelTransition(Node node);
parallelTransition = new ParallelTransition(Node node, Animation... children);

ParallelTransition 类只有大约六个方法调用,您需要掌握它们。那个。getChildren()方法调用将返回动画对象的 ObservableList ,这些对象将作为单个统一的动画一起播放。

那个。getNode()方法调用可以用来获取(poll)Node 属性的 Node 对象值,以及 void。setNode(Node value)方法调用可用于设置(指定)Node 属性的 Node 对象值。

还有一个受保护的节点。getParentTargetNode()方法调用,该调用将返回没有指定节点属性的过渡的子动画对象的目标节点。要指定父目标节点属性,必须使用第四个构造函数方法,该方法为 ParallelTransition(父节点)指定节点属性。否则,将使用第二个构造函数方法,子动画对象的 node 属性将定义动画对象将影响哪个节点对象。

并行转换。nodeProperty()方法调用将返回您的 parallel transition(parent)object property值,该值将使用第三个或第四个构造函数方法或。setNode(节点)。如果指定(设置)了此节点,它将用于所有未明确定义其目标节点的子转换。

最后,受保护的空间。interpolate(double value)方法调用需要由抽象 Transition 超类的所有子类实现提供。

接下来,让我们设置一个 ParallelTransition 对象,它将 rotSpinner 和 moveSpinnerOn 动画对象无缝地结合在一起。

ParallelTransition 对象:合并 rotSpinner 和 moveSpinnerOn

让我们通过在类的顶部声明一个名为 spinnerAnim 的 ParallelTransition 动画对象来将它添加到游戏项目中,然后在 RotateTransition 和 TranslateTransition Java 代码之后,在 createAnimationAssets()方法中实例化它。在构造函数方法中,引用 moveSpinnerOn 和 rotSpinner 动画子对象,然后调用。spinnerAnim 对象的 play()方法。请注意,我已经注释掉了 rotSpinner.play()方法调用,并且没有向 moveSpinnerOn 动画对象添加. play()方法调用,因为这是在 spinnerAnim ParallelTransition 对象中完成的。该并行(混合)动画的设置将使用以下 Java 语句完成,这些语句在图 16-10 中也用黄色和蓝色突出显示:

A336284_1_En_16_Fig10_HTML.jpg

图 16-10。

Declare a spinnerAnim ParallelTransition at the top of the class and instantiate it in createAnimationAssets()

ParallelTransition spinnerAnin;
...

spinnerAnin = new ParallelTransition(moveSpinnerOn, rotSpinner);
spinnerAnim.play();

当您选择运行➤项目时,一个旋转器从游戏的左侧旋转到右侧,如图 16-11 所示。

A336284_1_En_16_Fig11_HTML.jpg

图 16-11。

Select Run ➤ Project, click Start Game, and watch the spinner animate

在下一章之后,当我们讨论 3D 场景事件处理以及 PickResult 类时,我们可以开始完成 spinner UI 元素的动画,以便它在需要时出现在屏幕上,当用户不再需要旋转游戏板时离开屏幕。

我想专门用一章来介绍动画对象,向您展示预编码的 Transition 子类如何为您提供 Java 9 代码来将动画添加到您的游戏中,并向您展示如何设置大多数动画对象及其代码。我还想向您展示如何放置 createAnimationAssets()方法,以便您可以添加动画对象,从现在开始,这些对象将在您的 pro Java 9 游戏开发中拥有自己的位置。

摘要

在第十六章中,我们学习了动画超类和过渡超类,以及一些重要的过渡子类,RotateTransition 和 TranslationTransition,它们允许我们在游戏中移动和旋转 3D 对象。我们还看了 ParallelTransition 子类,它允许我们组合这些动画对象来创建更复杂的动画对象。我们还为我们的游戏构造了动画对象,这将允许用户对游戏板应用随机旋转来选择主题象限,并在随机旋转游戏板时将旋转的 spinner UI 元素移进移出屏幕。

我们为 JavaFXGame 类创建了一个名为 createAnimationAssets()的新自定义方法,该方法将保存为 pro Java 9 游戏设计创建的所有动画对象,这些动画对象使用 Transition 子类,如 RotateTransition、TranslateTransition、ScaleTransition 和 ParallelTransition。

在第十七章中,我们将学习 3D 场景元素的鼠标事件处理,这样我们可以点击球体 3D 旋转器 UI 来旋转游戏板,这样我们最终可以点击每个游戏板方块来选择教育问题类别并提出问题供玩家回答。

十七、i3D 游戏方块选择:对 3D 模型使用PickResult

现在,您已经创建了多层游戏板组节点(子类)层次,对该层次下的所有 3D 组件进行了纹理处理,确保该层次中心旋转,创建了一个旋转器 UI 来将游戏板 3D 模型(层次)随机旋转到一个随机象限,并使用您的createAnimationAssets()方法将动画对象添加到您的游戏设计中,是时候让您的 3D 游戏元素具有交互性了。我们将设置您的 3D 对象鼠标单击事件处理代码,该代码将用于触发 3D 旋转器动画和选择游戏棋盘方块。

在本章中,我们将详细了解公共 PickResult 类和公共 MouseEvent 类,并在自定义 createSceneProcessing()方法中使用它们来设计我们自己的游戏,该方法将用于处理 i3D 游戏元素(盒子和球体对象)事件处理,以便我们的玩家可以与 3D 游戏组件进行交互。

选择您的 3D 资源:PickResult 类

公共类 PickResult 扩展 Object 并保存在 javafx.scene.input 包中,该包包含输入事件处理实用程序,如 clipboard、GestureEvent、SwipeEvent、TouchEvent 和 ZoomEvent。PickResult 对象包含一个 pick 事件的结果,在这个游戏中,来自鼠标或触摸。支持在其构造函数方法中使用 PickResult 对象的输入类包括 MouseEvent、MouseDragEvent、DragEvent、GestureEvent、ContextMenuEvent 和 TouchPoint。每个类中都有一个. getPickResult()方法调用,它返回 PickResult 对象,该对象包含 Java 游戏开发过程中需要处理的所有选择信息。

PickResult 类的 Java 类层次结构向我们展示了该类是临时编码的,以提供 3D 对象选择功能;它没有自己的超类,因此看起来像下面的 Java 9 类层次结构:

java.lang.Object
  > javafx.scene.input.PickResult

PickResult 类包含一个数据字段,静态 int FACE_UNDEFINED 数据字段,它表示一个未定义的面。我们通常会使用这个类来选择整个节点(旋转器、象限 q1 到 q4、正方形 Q1S1 到 Q4S5 以及类似的 3D 游戏元素),而不是单个的多边形面或纹理贴图像素,这也是可能的。

PickResult 类中的前两个构造函数方法创建处理 2D 和 2.5D 场景拾取场景结果的 PickResult 对象。第一个构造函数使用 EventTarget 对象和(double) sceneX 和 sceneY 属性为 2D 场景创建 PickResult 对象。此构造函数方法使用以下 Java 语句语法:

PickResult(EventTarget target, double sceneX, double sceneY);

第二个构造函数为“非 Shape3D”目标创建一个 PickResult 对象。由于它使用 Point3D 对象和距离,我称之为 2.5D PickResult 场景,因为它不支持基于 Shape3D 类的 3D 图元。但是,它支持 Point3D 对象和场景对象的距离概念。此构造函数方法使用以下 Java 语句语法:

PickResult(Node node, Point3D point, double distance)

第三个构造函数为 Shape3D 目标创建一个 PickResult 对象,这是我们用来创建 i3D 游戏的对象。创建此构造函数方法的 Java 语法应该类似于下面的 Java 语句:

PickResult(Node node, Point3D point, double dist, int face, Point2D texCoord);

第四个构造函数为包含法线的导入的 3D 对象目标创建 PickResult 对象。如果您从外部 3D 建模软件包(如 Blender)导入高级 3D 模型,将会用到这一点。创建这个高级构造函数方法的 Java 语法应该类似于下面的 Java 语句:

PickResult(Node node, Point3D point, double distance, int face, Point3D normal, Point2D texCoor)

PickResult 类支持六个。get()方法调用,返回相交距离、相交面、相交节点、相交法线、相交点或相交纹理坐标(texCoord)。getIntersectedDistance()方法调用将以双精度值的形式返回当前摄像机位置和交叉点之间的相交距离。

那个。getIntersectedFace()方法调用将返回一个整数,表示所选节点的相交面。如果节点没有用户指定的面,如 Shape3D 图元之一,或者在边界上拾取,此方法将返回 FACE_UNDEFINED 常量。getIntersectedNode()方法调用将返回一个相交的节点作为节点对象,并且是我们将用来选择微调 UI 和游戏板节点元素的方法调用。

那个。getIntersectedNormal()方法调用将返回拾取的 Shape3D 对象或导入的 3D 几何图形的相交法线。那个。getIntersectedPoint()方法调用将使用拾取的节点对象的本地坐标系返回相交点(Point3D 对象)。那个。getIntersectedTexCoord()方法调用将以 Point2D 对象格式返回拾取的 3D 形状的相交纹理坐标。

接下来,让我们看看另一个重要的事件处理类 MouseEvent。这是 InputEvent 的子类,用于将鼠标事件处理附加到我们用来创建 i3D 棋盘游戏模拟的 3D 图元上。

MouseEvent 类:捕捉 3D 图元上的鼠标单击

公共 MouseEvent 类扩展了 InputEvent 超类。MouseEvent 与其子类 MouseDragEvent 和其他 InputEvent 超类子类一起保存在 javafx.scene.input 包中。MouseEvent 实现了可序列化和可克隆的 Java 接口。这个类用于实现或“捕获”鼠标事件,以供 Java 游戏逻辑处理,你将在本章中学习如何操作。当鼠标事件(如单击)发生时,光标下的第一个(顶部或前部)节点对象被“选取”,mouse event 被传递到该节点对象事件处理结构。通过使用 javafx.event 包中存储的公共 EventDispatcher Java 接口描述的捕获和冒泡阶段来传递事件。因此,MouseEvent 类的 Java 层次结构应该如下所示:

java.lang.Object
  > java.util.EventObject
    > javafx.event.Event

      > javafx.scene.input.InputEvent

        > javafx.scene.input.MouseEvent

鼠标指针(光标)位置在几种不同的坐标系中可用。可以使用相对于 MouseEvent 的 Node 对象原点(以及相对于场景对象)的 X,Y 坐标,使用相对于包含节点的场景原点的 sceneX,sceneY 坐标,甚至使用相对于包含鼠标指针的显示屏原点的 screenX,screenY 坐标。在这个特殊的 i3D BoardGame 项目中,我们将比较被点击的节点和我们的游戏处理逻辑。

有许多特定于 MouseEvent 对象的事件字段。它们是静态的,使用大写字母,因为它们是由 InputEvent 的 MouseEvent 类型提供的不同类型事件的“硬编码”常量。任何静态事件类型被用作一个公共的“超类型”来表示任何鼠标事件类型。

DRAG_DETECTED 静态事件类型将被传递给被识别为拖动手势的源的任何节点对象。当鼠标按钮被点击(在同一个节点上按下并释放)时,将传递一个 MOUSE_CLICKED 静态事件类型。这是我们将用于我们的 i3D 棋盘游戏。您还可以捕获鼠标被按下和鼠标被释放的事件。当鼠标按钮被按下时,会产生一个 MOUSE_PRESSED 静态事件类型,当鼠标按钮被释放时,会产生一个 MOUSE_RELEASED 静态事件类型。

您还可以在鼠标移动到一个节点上,然后又离开该节点而没有任何鼠标点击发生时处理事件!当鼠标进入一个节点对象但没有被点击(这称为悬停)时,将会传递一个 MOUSE _ ENTERED 静态事件类型。当鼠标第一次进入节点(越过其边缘边界)时,将传递一个 MOUSE _ ENTERED _ TARGET 静态事件类型< MouseEvent >。类似地,当鼠标退出一个节点对象时,一个鼠标退出的静态事件类型<鼠标事件>将被传递。当鼠标第一次退出节点对象(越过边界)时,将会传递 MOUSE_EXITED_TARGET 静态事件类型< MouseEvent >。

最后,当鼠标在没有按钮被按下或释放的情况下在节点对象中移动时,将会传递 MOUSE_MOVED 静态事件类型。当使用按下(按住)的鼠标按钮移动鼠标时(称为拖动操作),将传递 MOUSE _ DRAGGED 静态事件类型。

我们不会专门构造(实例化)MouseEvent 对象。我们会用。setOnMouseClick()事件处理构造,作为其功能的一部分,它将为我们进行构造。然而,为了完整起见,我将在这里包含这两个重载的构造函数方法。第一个构造了一个新的 MouseEvent 事件对象,具有空的源和目标,看起来像下面的 Java 9 构造函数方法语法:

MouseEvent(EventType<? extends MouseEvent> eventType, double x, double y, double screenX, double
           screenY, MouseButton button, int clickCount, boolean shiftDown, boolean controlDown,
           boolean altDown, boolean metaDown, boolean primaryButtonDown, boolean
           middleButtonDown, boolean secondaryButtonDown, boolean synthesized, boolean
           popupTrigger, boolean stillSincePress, PickResult pickResult)

第二个构造了一个新的 MouseEvent 事件对象,类似于下面的 Java 语法:

MouseEvent(Object source, EventTarget target, EventType<? extends MouseEvent> eventType, double
           x, double y, double screenX, double screenY, MouseButton button, int clickCount,
           boolean shiftDown, boolean controlDown, boolean altDown, boolean metaDown, boolean
           primaryButtonDown, boolean middleButtonDown, boolean secondaryButtonDown, boolean
           synthesized, boolean popupTrigger, boolean stillSincePress, PickResult pickResult)

MouseEvent 类中有 27 个方法可以帮助您控制鼠标事件的处理。的。copyFor(Object newSource,Event target new target)mouse Event 方法调用将复制事件对象,以便它可以用于不同的源和目标。

那个。copyFor(Object newSource,EventTarget newTarget,Event type extends MouseEvent>Event type)MouseEvent 方法调用还将创建给定事件对象的副本,并用给定的 mouse Event 字段进行替换。

静态 MouseDragEvent。copyForMouseDragEvent(MouseEvent e,Object source,EventTarget target,EventType type,Object gestureSource,PickResult pickResult)方法调用将创建 MouseDragEvent 类型的 mouse event 的副本。

那个。getButton()方法调用将轮询 MouseEvent 对象,以查看哪个鼠标按钮(如果有)负责生成该事件对象。那个。getClickCount()方法调用将返回与事件对象关联的整数(int)鼠标单击次数。

那个。getEventType()方法调用将返回该事件对象的 EventType extends MouseEvent>事件类型。那个。getPickResult()方法调用将返回 PickResult 对象关于该选择的信息。

那个。getSceneX()方法调用将返回事件相对于包含 MouseEvent 源的场景原点的水平位置的 double 值。那个。getSceneY()方法调用将返回事件相对于包含 MouseEvent 源的场景原点的垂直位置的 double 值。

那个。getScreenX()方法调用将返回事件绝对水平位置的 double 值。那个。getScreenY()方法调用将返回事件绝对垂直位置的 double 值。

那个。getX()方法调用将返回事件相对于 MouseEvent 源原点的水平位置的 double 值。那个。getY()方法调用将返回事件相对于 MouseEvent 源原点的垂直位置的 double 值。那个。getZ()方法调用将返回事件深度位置的 double 值,该值相对于 MouseEvent 源的原点。

那个。isAltDown()方法调用可用于确定在此事件中 Alt 修饰键是否被按住。它返回一个真或假(布尔值)。那个。isControlDown()方法调用可用于确定在此事件期间 Ctrl 修饰键是否被按住。它还返回真或假(布尔值)。那个。isMetaDown()方法调用可用于确定元修饰键在此事件中是否被按住。它还返回真或假(布尔值)。那个。isShiftDown()方法调用可用于确定在此事件中 SHIFT 修饰键是否被按住。它还返回真或假(布尔值)。

那个。应使用 isDragDetect()方法调用来确定此 MouseEvent 之后是否会有 DRAG_DETECTED 事件,并返回布尔值 true(检测到拖动)或 false(未检测到拖动)。

安。isMiddleButtonDown()方法调用可用于确定鼠标中键是否被按住。如果您的中间按钮(鼠标键#2)当前被按下,它将返回一个真布尔值。

那个。应该使用 isPopupTrigger()方法调用来确定该事件是否是平台的弹出菜单触发事件。如果鼠标事件实际上是平台的弹出菜单触发事件,它将返回 true。

那个。如果您的主鼠标按钮(按钮#1,通常是鼠标左键)当前被按下,isPrimaryButtonDown()方法调用将返回真布尔值。那个。如果您的辅助按钮(按钮#2,通常是鼠标右键)当前被按下,isSecondaryButtonDown()方法调用将返回真布尔值。

那个。isShortcutDown()方法调用将返回在此 MouseEvent 期间主机平台的公共快捷方式修饰符是否被按住。

那个。isStillSincePress()方法调用使用一个布尔值来指示自从在此事件之前发生的最后一次鼠标按下事件以来,鼠标光标是否停留在系统提供的滞后区域中。

那个。isSynthesized()方法调用返回一个布尔值,该值指示 MouseEvent 是否是使用触摸屏设备而不是通常的鼠标事件源设备(如鼠标、跟踪球、跟踪板或类似的鼠标模拟硬件设备)合成的。

最后,一片虚空。setDragDetect(boolean dragDetect)方法调用用于在使用鼠标、跟踪板或触摸屏设备将 MouseEvent 处理与拖动检测结合使用时增强拖动检测行为。

实现微调用户界面功能:鼠标事件处理

让我们创建一个 createSceneProcessing()方法来保存场景对象的创建、配置和事件处理 Java 代码。必须在创建根组节点对象之后创建场景场景对象,因此必须在创建这些节点对象的 createBoardGameNodes()方法调用之后调用该方法。这是使用下面的 Java 语句完成的,在图 17-1 中也用浅蓝色(和红色波浪下划线)突出显示:

A336284_1_En_17_Fig1_HTML.jpg

图 17-1。

Add the createSceneProcessing() method call, after the createBoardGameNodes() method in the start method

createSceneProcessing();

记得双击 javafxgame 中的创建方法“createSceneProcessing()”。JavaFXGame 选项,让 NetBeans 9 为您编写一个引导方法。您将使用场景对象实例化和配置代码替换占位符 Java 代码,然后添加 MouseEvent 处理逻辑。

你需要做的第一件事是打开你的 createBoardGameNodes()方法,选择所有的场景场景对象实例化和配置 Java 9 代码,目前有三个 Java 9 语句;然后右键单击选择集并选择 Cut 选项,从该方法体中删除 Java 代码。

在你的内心。createSceneProcessing()方法,通过选择那一行代码并右键单击它来替换您的引导代码(未实现的错误代码);选择“粘贴”来替换您从 createBoardGameNodes()方法中“剪切”的三行代码。最后,在方法的末尾添加一行代码,开始构建您的场景对象的事件处理;键入 scene,然后键入句点,再键入 setOnMouse,这将弹出一个包含所有 MouseEvent 事件的帮助器对话框。以下是现有语句的 Java 代码和一个空的事件处理 lambda 表达式基础结构(用于更改),如图 17-2 中用蓝色突出显示的:

A336284_1_En_17_Fig2_HTML.jpg

图 17-2。

Cut and paste the Scene object code into the new method and call .setOnMouseClicked() off the scene object

private void createSceneProcessing() {
    scene = new Scene(root, 1280, 640, true, SceneAntialiasing.BALANCED);
    scene.setFill(Color.BLACK);
    scene.setCamera(camera);
    scene.setOnMouseClicked(event-> { ... } );  // This is an Empty OnMouseClicked Event Handler
}                                              //  Structure is using a Lambda Expression Format

双击您的 setOnMouseClicked(EventHandler super MouseEvent>value)(void)选项,在图 17-2 中用亮蓝色显示,并在。setOnMouseClicked()方法调用参数区域来创建空的事件处理基础结构,这将在 NetBeans 9 中产生零错误。正如我以前在本书中说过的,当你写代码时,要确保它在你的 IDE 中始终没有错误!

现在您可以开始配置 onMouseClicked()事件处理,正如您所看到的,它使用了 Java 8 中引入的简化的 lambda 表达式。lambda(简称 lambda)需要的只是事件名称和一个箭头,Java 编译器会计算出使用哪种类型的事件处理对象(EventHandler)以及需要处理哪种类型的事件对象(MouseEvent)。您的逻辑放在花括号内,您可以专注于事件处理逻辑要做的事情,即声明一个名为 picked 的节点对象并用. getPickResult()的结果加载它。getIntersectedNode()方法链。确保在 Java 语句的Node picked(初始)部分下出现红色波浪错误下划线时使用 Alt+Enter,并从弹出的帮助器对话框中选择“import javafx.scene.Node”选项,以指示 NetBeans 9 为您编写节点类导入语句。如果你愿意,你可以输入等号(=)和事件,然后点击句号;NetBeans 弹出帮助程序将允许您选择。getPickResult()方法。双击它将其插入,然后再次使用句点来显示弹出帮助程序。这次选择。getIntersectedNode()方法调用。添加分号以结束 Java 语句。用于鼠标事件处理的 Java 语句应该如下所示,并显示在图 17-3 的底部:

A336284_1_En_17_Fig3_HTML.jpg

图 17-3。

Configure event handling as a lambda expression, create a Node named picked, and get an intersected Node

private void createSceneProcessing() {
    scene = new Scene(root, 1280, 640, true, SceneAntialiasing.BALANCED);
    scene.setFill(Color.BLACK);
    scene.setCamera(camera);
    scene.setOnMouseClicked(event->{
        Node picked = event.getPickResult().getIntersectedNode();

    });
}

现在,您已经创建并加载了一个名为 picked 的节点对象,它与用户用鼠标(或触摸屏,也生成鼠标事件)点击的 BoardGame 中的节点对象一起使用,我们需要添加条件处理逻辑(人工智能)来告诉游戏如何操作。您需要做的第一件事是过滤掉所有不在 3D 节点对象上的单击,这是通过使用if ( picked != null )构造来完成的,它表示如果拾取的节点对象不为空,则继续。下一个嵌套的 if()语句查找与拾取的节点对象相同(==或等效)的微调器节点对象。如果这等于一个真值,rotGameBoard 动画对象将通过使用。play()方法调用,旋转游戏板组节点。如果您使用“运行➤项目”工作流程并测试这段代码,它可以完美地工作,尽管您必须等到最后一章的代码完成(我们将在接下来修复它,因为我们将动画对象更改为鼠标事件触发)。

整个完整的 Java 9 结构只有八行代码;这将随着我们构建游戏逻辑而增长。此处显示了完整的 Java 方法体代码,并在图 17-4 中以黄色和蓝色突出显示:

A336284_1_En_17_Fig4_HTML.jpg

图 17-4。

Evaluate the picked Node object using two nested if{} constructs, testing for null and then for the spinner UI Node

private void createSceneProcessing() {
    scene = new Scene(root, 1280, 640, true, SceneAntialiasing.BALANCED);
    scene.setFill(Color.BLACK);
    scene.setCamera(camera);
    scene.setOnMouseClicked(event->{
        Node picked = event.getPickResult().getIntersectedNode();
        if (picked != null) {
            if (picked == spinner) {
                rotGameBoard.play();

            }
        }
    });
}

要使微调器 UI 在屏幕上呈现动画效果,我们首先必须将它在屏幕外的初始位置设置在其当前起始位置的左侧。进入 createBoardGameNodes()并将 TranslateX 属性从-200 更改为-350。这将从视图中移除微调器,就在屏幕的左侧。稍后我们将更改。将 moveSpinnerIn 中的 setByX()方法设置为 150,因此它位于-200。这是使用此处和图 17-5 中所示的 Java 代码完成的:

A336284_1_En_17_Fig5_HTML.jpg

图 17-5。

Prepare for implementing the interactive spinner UI by setting its initial position off-screen value to the -350 X location

spinner.setTranslateX(-350);

注意 TranslateY 是-512;这将 3D 旋转器 UI 放置在屏幕的顶部,不在游戏棋盘视图的范围内,并且在旋转器动画显示到-150 X 位置时位于屏幕的左上角。

接下来,让我们重新编码我们的 createAnimationAssets()方法体,以便它只实例化和配置我们的动画对象,而不触发它们,这将在游戏过程中由用户通过鼠标点击(或屏幕触摸,因为这些也将生成鼠标事件,扩大我们的目标消费电子设备)来完成。

移除。play()方法调用 rotGameBoard、rotSpinner 和 spinnerAnim 动画对象构造,然后更改 movespinner on translate transition 对象的。setByX()方法调用引用 150 个单位。这将把你的 3D 旋转器 UI 从屏幕外的新-350 位置移动到屏幕的左上角。触发这个动画的逻辑位置,第一次将旋转器带到屏幕上,应该是在开始游戏按钮 UI 事件处理方法中,我们很快就会这样做。我们还将在本章稍后创建 rotSpinner 动画对象,它将在被单击时旋转 3D spinner UI,以便当玩家启动 3D 游戏板的每个随机旋转时它也旋转。

除了在“开始游戏”按钮事件处理中将这个 i3D 旋转器显示在屏幕上之外,我们还将在每个玩家的回合结束时将它显示在屏幕上(在第二十一章),以便下一个玩家知道随机旋转游戏板来选择新的教育问题类别(象限)。我们将在第二十章中制作它的离线动画,当游戏板完成它的相机旋转动画对象时。关于如何在 JavaFX 中将您的交互性(事件处理)与您的不同动画对象相集成,以便您可以获得无缝且响应迅速的游戏效果,这一章中还有很多内容需要学习。

您的新 createAnimationAssets() Java 方法体现在应该如下所示,在图 17-6 中也用浅蓝色和黄色突出显示:

A336284_1_En_17_Fig6_HTML.jpg

图 17-6。

Remove all .play() method calls and change the .setByX() method call to 150 to bring the spinner on-screen

RotateTransition rotGameBoard, rotSpinner;
TranslateTransition moveSpinnerOn;
ParallelTransition spinnerAnim;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotGameBoard.setFromAngle(45);
    rotGameBoard.setToAngle(1125);
                                                                      // .play() removed

    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5);
    rotSpinner.setInterpolator(Interpolator.LINEAR);
    rotSpinner.setFromAngle(30);
    rotSpinner.setToAngle(-1110);                                     // .play() removed

    moveSpinnerOn = new TranslateTransition(Duration.seconds(5), spinner);
    moveSpinnerOn.setByX(150);
    moveSpinnerOn.setCycleCount(1);
    spinnerAnim = new ParallelTransition(moveSpinnerOn, rotSpinner);
                                                                      // .play() removed

}

在 gameButton 事件处理程序的末尾添加一条spinnerAnim.play();语句,如图 17-7 所示。

A336284_1_En_17_Fig7_HTML.jpg

图 17-7。

Add the spinnerAnim.play() method call to the end of your gameButton event-handling method construct

现在使用您的运行➤项目工作流程来测试您的代码,您可以看到微调器在游戏开始时没有显示(在您单击隐藏 uiLayout StackPane 节点对象的按钮之后),并且缓慢而平稳地旋转到游戏屏幕左上角的视图中。

下一件事,我们需要做的是创建一个单独的 rotSpinner 动画对象,以便我们可以在游戏板旋转的同时进行 3D spinner UI 旋转,以保持连续性。您会发现,如果在 MouseEvent 处理构造中调用 rotSpinner.play,将会得到一个错误,因为 rotSpinner 是 spinnerAnim ParallelAnimation 对象的一部分;因此,我们需要复制一个 rotSpinner 构造,并创建一个 rotSpinnerIn 构造以在 spinnerAnim ParallelAnimation 中使用,让 rotSpinner 动画在玩家随机旋转游戏板时免费供我们调用。

为此,选择所有与 rotSpinner 相关的 Java 代码,右键单击选择集,然后选择 Copy 然后在这个代码块之后添加一行(空格)代码,右键单击,并选择 Paste 复制这个代码块。然后,您所要做的就是在“rotSpinner”的末尾添加“In ”,并创建一个 rotSpinnerIn 代码块,它做同样的事情,但不是 ParallelTransition 构造的组件。在 spinnerAnim ParallelTransition 对象的对象实例化(构造函数方法)中引用新的 rotSpinnerIn 动画对象。

正如你所看到的,唯一的问题是你的“旋转”旋钮旋转到了错误的 1110 度角,正如我们在第十六章中编码的那样。在下一节中,我将把它设置为-1050。代码如下所示,如图 17-8 所示:

A336284_1_En_17_Fig8_HTML.jpg

图 17-8。

Copy and paste the rotSpinner object code under itself to create a rotSpinnerIn, and reference in spinnerAnim

RotateTransition rotGameBoard, rotSpinner;
TranslateTransition moveSpinnerOn;
ParallelTransition spinnerAnim;
...
private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotGameBoard.setFromAngle(45);
    rotGameBoard.setToAngle(1125);
    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5); 

    rotSpinner.setInterpolator(Interpolator.LINEAR);
    rotSpinner.setFromAngle(30);
    rotSpinner.setToAngle(-1110);
    rotSpinnerIn = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinnerIn.setAxis(Rotate.Y_AXIS);
    rotSpinnerIn.setCycleCount(1);
    rotSpinnerIn.setRate(0.5);
    rotSpinnerIn.setInterpolator(Interpolator.LINEAR);
    rotSpinnerIn.setFromAngle(30);
    rotSpinnerIn.setToAngle(-1110);
    moveSpinnerOn = new TranslateTransition(Duration.seconds(5), spinner);
    moveSpinnerOn.setByX(150);
    moveSpinnerOn.setCycleCount(1);
    spinnerAnim = new ParallelTransition(moveSpinnerOn, rotSpinnerIn);
}

现在,我可以在条件事件处理结构中添加一个rotSpinner.play() ; Java 语句,而不会产生任何错误,这样,当点击旋转器 UI 时,它会沿着游戏板旋转相同的时间和速率。完整的 Java 代码如下所示,在图 17-9 中用黄色突出显示:

A336284_1_En_17_Fig9_HTML.jpg

图 17-9。

Add rotSpinner.play() after rotGameBoard.play() in the mouse event handling construct so both will animate

private void createSceneProcessing() {
    scene = new Scene(root, 1280, 640, true, SceneAntialiasing.BALANCED);
    scene.setFill(Color.BLACK);
    scene.setCamera(camera);
    scene.setOnMouseClicked(event->{
        Node picked = event.getPickResult().getIntersectedNode();
        if (picked != null) {
            if (picked == spinner) {
                rotGameBoard.play();
                rotSpinner.play();

            }
        }
    });
}

让我们使用一个运行➤项目的工作流程并测试我们的代码。单击开始游戏按钮对象,注意屏幕上只包含游戏板。然后,spinner UI 出现,旋转到位(旋转到错误的“大头针”位置,我们将很快修复)。点击旋转器,旋转器和游戏板旋转,如图 17-10 所示。

A336284_1_En_17_Fig10_HTML.jpg

图 17-10。

The spinner UI element now animates on-screen and also rotates when clicked to spin the game board

使用 java.util.Random:生成随机旋转

公共类 Random 扩展 Object 并实现 Serializable。它保存在 java.util 包中,有两个已知的直接子类 SecureRandom 和 ThreadLocalRandom。这个类的一个实例可以用来创建一个随机数生成对象,它将生成一个“伪随机数”流为了创建随机的游戏旋转器 UI 功能,这些数字将足够随机。该类的算法使用 48 位种子,该种子使用线性同余公式进行修改。如果你想更详细地研究这个算法,你可以参考 Donald Knuth 的《计算机编程的艺术》(第 2 卷,第 3.2.1 节)。因此,Random 类的 Java 类层次结构如下所示:

java.lang.Object
  > java.util.Random

值得注意的是,如果使用相同的种子创建随机对象的两个不同实例,并且对每个对象进行相同的方法调用序列,则算法会生成(返回)相同的数值结果序列。在某些应用中,这实际上是可取的;因此,为了保证相同的结果,java.util.Random 类实现了特定的算法。类 Random 的子类被允许使用替代算法来增加安全性或多线程的使用,只要它们遵守所有方法的通用契约。

java.util.Random 的实例是线程安全的。但是,在多个线程中同时使用同一个 java.util.Random 实例可能会遇到争用,从而导致性能下降。您应该考虑为您的多线程游戏设计使用 ThreadLocalRandom 子类。

此外,java.util.Random 的实例在加密方面并不安全。相反,您应该考虑使用 SecureRandom 子类来获得一个加密安全的伪随机数生成器,供敏感且需要高级安全性的应用使用。

这个类有两个重载的构造函数方法。第一个创建一个随机数生成器,第二个创建一个随机数生成器,并使用长格式给它一个种子值。这些构造函数方法看起来像下面的 Java 代码:

Random()             // We'll be using this in our code later on during this chapter
Random(long seed)

这个类有 22 个方法,可以用来从 random 对象获得随机数结果。的。doubles()方法调用将返回一个称为 DoubleStream 的无限数值流,其中包含伪随机双精度值。这些值中的每一个都将介于零(含)和一(不含)之间。还有三个额外的超载。doubles()方法调用。的。doubles(double randomNumberOrigin,double randomNumberBound)方法调用将返回伪随机双精度值的无限绑定流,每个值都符合方法调用参数区域中指定的给定绑定原点(包括)和绑定限制(不包括)。的。doubles(long streamSize)方法调用将返回一个流,该流产生给定 streamSize 数量的伪随机双精度值,这些值介于零(含)和一(不含)之间。最后,还有一个. doubles(long streamSize,double randomNumberOrigin,double randomNumberBound)方法调用,它返回一个流,该流产生符合给定 streamSize 数量的伪随机双精度值的流,每个值都符合给定的绑定原点(包含)和绑定限制(不包含)。

那个。ints()方法调用将返回一个无限的伪随机 int(整数)数值流,称为 IntStream。还有三个额外的超载。ints()方法调用,包括。ints(int randomNumberOrigin,int randomNumberBound)方法调用,它将返回一个无限的伪随机 int (integer)值流,每个值都符合参数区域中指定的绑定原点(包含)和绑定限制(不包含)值。那个。ints(long streamSize)方法调用将返回一个随机值流,这将产生一个流大小,该流大小是使用 streamSize 参数指定的,该参数建立了所需数量的伪随机 int (integer)值。

最后是。ints(long streamSize,int randomNumberOrigin,int randomNumberBound)方法调用将返回一个数值(整数)流,该流产生参数区域中指定的 streamSize 数量的伪随机 int 值,每个值都符合指定的绑定原点(包括)和绑定限制(不包括),这也是从方法调用参数区域获取的。

那个。longs()方法调用将返回一个无限的伪随机长数值流,称为 LongStream。还有三个额外的超载。longs()方法调用,包括一个. longs(long randomNumberOrigin,int randomNumberBound)方法调用,它将返回一个无限的伪随机长值流,每个长值都符合参数区域中指定的绑定原点(包含)和绑定限制(不包含)值。那个。longs(long streamSize)方法调用将返回一个随机长值流,该流产生使用 streamSize 参数指定的流大小,该参数建立所需数量的伪随机长值。

最后,一个. long(long streamSize,int randomNumberOrigin,long randomNumberBound)方法调用将返回一个数字长值流,该流产生参数区域中指定的 streamSize 数量的伪随机长值,每个值都符合指定的绑定原点(包括)和绑定限制(不包括),这也是从方法调用参数区域获取的。

受保护的 int。next(int bits)方法调用将使用整数位数作为参数规范来生成下一个伪随机整数。的。nextBoolean()方法调用将从随机数生成器对象的序列中返回一个伪随机的、均匀分布的布尔值。这个方法可能不应该用于这个游戏的用例,因为 next()被设计为由其他 random()方法调用。

虚空。nextBytes(byte[] bytes)方法调用将生成一个参数提供的字节数组,并用随机字节值填充它。那个。nextDouble()方法调用将通过使用随机数生成器对象的序列返回一个介于值 0.0 和 1.0 之间的伪随机、均匀分布的 Double 值。那个。nextFloat()方法调用将使用随机数生成器对象的序列返回一个伪随机的、均匀分布的浮点值,介于 0.0 和 1.0 之间。

那个。nextGaussian()方法调用将从这个随机数生成器对象的序列中返回一个伪随机、高斯分布的双精度值,其平均值为 0.0,标准差为 1.0。那个。nextInt()方法调用将从这个随机数生成器对象的序列中返回下一个伪随机、均匀分布的 Int(整数)值。

那个。nextLong()方法调用将从这个随机数生成器对象的序列中返回下一个伪随机、均匀分布的长值。

虚空。setSeed(long seed)方法调用可用于设置(或重新设定)随机数生成器对象的种子,在方法调用的参数区域内使用单个长值种子规范。

最后是。nextInt(int bound)方法调用是我们将在本章的最后一节使用的方法,它将返回一个伪随机的、均匀分布的 int (integer)值,该值介于 0(包括 0)和指定值(不包括 0)之间,在我们的示例中为 4,从随机数生成器对象的 random int 序列中提取。

随机象限选择:使用带条件 If()的随机

既然我们已经很好地设置了旋转器和游戏板旋转以及鼠标事件处理,足以将两者连接在一起,为游戏板创建一个随机旋转,我们需要向代码中添加一个随机数发生器算法,以便每次单击旋转器时,游戏板都会被随机设置到一个新的象限。我们将使用至少三次旋转,以便旋转足够长,对玩家来说完全是随机的。让我们在类的顶部声明一个名为 Random 的随机对象,然后使用 Alt+Enter 组合键打开 NetBeans 9 弹出助手。最后,选择(双击)“为 java.util.Random 添加导入”选项,如图 17-11 中蓝色部分所示。

A336284_1_En_17_Fig11_HTML.jpg

图 17-11。

Declare a Random object named random at the top of class; use Alt+Enter to add import java.util.Random

由于我们希望在实际使用随机数生成器“引擎”之前实例化(创建)它,所以让我们的代码在游戏应用启动时实例化(创建并加载到系统内存中)随机数生成器。

这表明我们将把 Random()构造函数方法代码放在您的。start()方法,在 ActionEvent 处理构造之前,在所有节点、场景和舞台对象创建并添加到 SceneGraph 之后。为了更好地衡量,我们将把它放在创建素材、图像、动画以及最终数字音频样本和其他新媒体素材的所有自定义方法之后,我们将使用它们来创建专业的 Java 9 游戏。

我们可以这样做,因为这个随机对象(名为 Random)直到玩家单击开始游戏按钮对象进入 3D 场景,然后单击 spinner 3D UI (Sphere)元素时才被使用。因此,您可以将这个随机对象实例化放在。start()方法,从第一行代码到最后一行代码,只要在您开始在您的自定义 createSceneProcessing()方法中生成任何 MouseEvent 处理方法调用之前创建了该对象(加载到系统内存中),我们将在本章中继续增强该方法。打开你的。NetBeans 9 中的 start()方法体,在你的自定义方法调用之后添加一行代码,使用下面的 Java 代码实例化你的名为 Random 的随机对象,也如图 17-12 所示:

A336284_1_En_17_Fig12_HTML.jpg

图 17-12。

Instantiate the random Random object in the .start() method so that it is loaded into memory and ready

public void start(Stage primaryStage) {
    ...                                  //  Custom Methods Up Here
    random = new Random();
    ...                                  //  ActionEvent Handling Constructs Down Here
}

现在,您已经准备好在 MouseEvent 处理代码中调用微调器逻辑内部的这个随机数生成器,它告诉您的游戏在单击 3D 微调器 UI 时该做什么。显然,要做的第一件事是检查 NULL 以查看点击是否在 3D 场景元素上,如果是,则查看点击的是否是 3D 微调器。

如果单击了微调器,那么 if(picked==spinner)之后的第一行代码将是一个. nextInt(bound)方法调用,上边界值为 4(下边界为零)。这为我们提供了四个象限中的随机结果(从 0 到 3,因为 4 的上限是唯一的,因此不在随机数选择范围内使用),这是我们需要在游戏的四个象限中随机选择的结果。

在调用 RotateTransition 动画对象之前添加一行代码,并创建一个名为 spin 的新 int 变量,它将保存您的random.nextInt(4)方法调用的结果。添加一个等号(=)运算符,然后键入 random 和一个句点,这将打开 NetBeans 9 method helper 弹出选择器下拉列表。

选择。nextInt(int bound) (int)选项,在图 17-13 中用蓝色显示,然后双击它插入到你的代码中。将默认值 0(通过生成零到零的结果来关闭随机数生成器)更改为 4,告诉随机数生成器随机生成四个整数值,这将为您的玩家旋转提供四个不同的象限结果。此时的 Java 代码应该看起来像下面的 Java 嵌套 if()结构,在图 17-13 中也用蓝色突出显示(也在构建中):

A336284_1_En_17_Fig13_HTML.jpg

图 17-13。

Add an int variable named spin and then type random and a period and select nextInt(int bound ) set to 4

if (picked != null) {
    if (picked == spinner) {
        int spin = random.nextInt(4);

        rotGameBoard.play();
        rotSpinner.play();
    }
}

在我们编写这个自旋逻辑之前,我们需要移除。setFromAngle()和。setToAngle()方法调用 createAnimationAssets()方法中 rotGameBoard 语句块,这将 rotGameBoard 动画对象逻辑简化为五个必需的语句(实例化、轴、周期、速率和插值器)。在我们确定从 toAngle 和 fromAngle 到 byAngle 的切换将正确地使用最少的代码行和零错误生成正在进行的游戏板旋转后,我们将为您的 rotSpinner 执行此操作。

我们在这里做的是使用 createAnimationAssets()来创建和配置动画对象,然后在 if()条件语句中使用. setByAngle(),该语句评估随机的随机对象结果,并将其放入 spin integer 中,这是我们接下来要做的。这种方法也将减少这个方法体中的代码量,减少到不到 24 行代码(除非我们在本书概述的设计和开发过程中添加游戏板动画)。rotGameBoard 代码如图 17-14 所示,现在看起来像这样:

A336284_1_En_17_Fig14_HTML.jpg

图 17-14。

Remove the .setFromAngle(45) and .setToAngle(1125) method calls from the rotGameBoard object code

private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5);
    rotSpinner.setInterpolator(Interpolator.LINEAR);
    rotSpinner.setFromAngle(30);
    rotSpinner.setToAngle(-1050);
    ...
}

确保游戏板旋转在一个象限结束的最简单的方法是将游戏板旋转初始化为 45 度,并使用。setByAngle()为每次 if()计算旋转 90 度增量(加上三次旋转)。这样我们得到 0 的 1080,1 的 1170,2 的 1260 和 3 的 1350。Java if()结构如图 17-15 所示,如下所示:

A336284_1_En_17_Fig15_HTML.jpg

图 17-15。

Add if() constructs, setting the .setByAngle() method call to four different 90-degree increments plus 1080

if (picked == spinner) {
    int spin = random.nextInt(4);
    if (spin == 0) {
        rotGameBoard.setByAngle(1080);  // Zero degrees plus 1080

    }
    if (spin == 1) {
        rotGameBoard.setByAngle(1170);  // 1080 plus 90 degrees is 1170

    }
    if (spin == 2) {
        rotGameBoard.setByAngle(1260);  // 1080 plus 180 degrees is 1260

    }
    if (spin == 3) {
        rotGameBoard.setByAngle(1350);  // 1080 plus 270 degrees is 1350

    }
    rotGameBoard.play();
    rotSpinner.play();
}

使用运行➤项目工作流程,多次点击 spinner UI 进行测试,如图 17-16 所示。

A336284_1_En_17_Fig16_HTML.jpg

图 17-16。

The game board now randomly lands on a different quadrant with each 3D spinner click

返回 createAnimationAssets()方法并移除。setFromAngle()和。setToAngle()方法从 rotSpinner 动画对象中调用,产生如下 Java 代码,如图 17-17 所示:

A336284_1_En_17_Fig17_HTML.jpg

图 17-17。

Remove rotSpinner.setFromAngle() and rotSpinner.setToAngle() method calls in createAnimationAssets

private void createAnimationAssets() {
    rotGameBoard = new RotateTransition(Duration.seconds(5), gameBoard);
    rotGameBoard.setAxis(Rotate.Y_AXIS);
    rotGameBoard.setCycleCount(1);
    rotGameBoard.setRate(0.5);
    rotGameBoard.setInterpolator(Interpolator.LINEAR);
    rotSpinner = new RotateTransition(Duration.seconds(5), spinner);
    rotSpinner.setAxis(Rotate.Y_AXIS);
    rotSpinner.setCycleCount(1);
    rotSpinner.setRate(0.5);
    rotSpinner.setInterpolator(Interpolator.LINEAR);
}

返回到 createSceneProcessing()方法,使用 rotGameBoard.setByAngle()方法调用中使用的负角度值添加 rotSpinner.setByAngle()方法调用,使用此代码,也如图 17-18 所示:

A336284_1_En_17_Fig18_HTML.jpg

图 17-18。

Add the rotSpinner.setByAngle() method calls to the random spin logic, this time subtracting 1080 plus 90

if (picked == spinner) {
    int spin = random.nextInt(4);
    if (spin == 0) {
        rotGameBoard.setByAngle(1080);
        rotSpinner.setByAngle(-1080);  // Zero degrees minus 1080
    }
    if (spin == 1) {
        rotGameBoard.setByAngle(1170);
        rotSpinner.setByAngle(-1170);  // -1080 minus 90 degrees is -1170
    }
    if (spin == 2) {
        rotGameBoard.setByAngle(1260);
        rotSpinner.setByAngle(-1260);  // -1080 minus 180 degrees is -1260
    }
    if (spin == 3) {
        rotGameBoard.setByAngle(1350);
        rotSpinner.setByAngle(-1350);  // -1080 minus 270 degrees is -1350
    }
    rotGameBoard.play();
    rotSpinner.play();
}

现在使用一个运行➤项目的工作流程,并彻底测试我们在本章中开发的代码。正如你在图 17-19 中所看到的(因为它不是动画或互动的,就像我们现在的游戏一样),每次你点击 3D 旋转器,你会在 3D 旋转器 UI 上得到不同的象限和不同的颜色序列,而它仍然总是说“旋转”!

A336284_1_En_17_Fig19_HTML.jpg

图 17-19。

The game board randomly lands on a different quadrant , and the spinner always lands on the word SPIN

在本章中,我们添加了一些相当复杂的功能,我们仍然有大约 500 行 Java 代码,正如您在 NetBeans 9 底部的图 17-18 中看到的那样(第 504 行是课程的结尾)。令人印象深刻,伙计们!

摘要

在第十七章中,我们学习了 MouseEvent、PickResult 和 Random 类,它们允许我们完成 3D spinner UI 的实现,并让它在旋转的 3D spinner 沙滩球的每个后续旋转中选择一个随机象限。我们还构建了一个新的自定义 createSceneProcessing()方法,该方法包含您的 MouseEvent 处理逻辑,以及您的用于处理(现在的)i3D 基本对象的逻辑,我们的 i3D 游戏板和旋转器是由这些基本对象组成的(构建时使用)。在这个新方法中,我们开始构建一个条件 if()结构来评估鼠标点击,以及基于点击的内容游戏逻辑需要发生什么。在接下来的几章中,当我们设计和开发我们的游戏模型时,我们显然会扩展这个逻辑。

我们还获得了更多使用 RotateTransition 类方法的经验,方法是使用。setFromAngle()和。setToAngle()旋转动画配置参数为单个。setByAngle()旋转动画配置方法,减少了 12 行 Java 代码。

在第十八章中,我们将开发你的游戏内容,这样我们就可以完成你的鼠标事件处理代码(在第 19 和 20 章中)用于游戏棋盘方格和游戏棋盘象限。