Java8 游戏开发入门手册(四)
十三、动画化您的演员形象状态:基于关键事件处理设置图像状态
现在我们已经将你的 Java 代码组织成 Bagel.java 类中的逻辑方法,并且确保我们所有的 Java 代码都符合第十一章和第十二章中的标准,是时候进入一些更复杂的代码结构了,当用户移动角色时,这些代码结构将在屏幕上激活我们的无敌角色。例如,如果角色正在正东或正西行进(仅使用左键或右键,沿直线行进),他应该在跑(在 imageStates(1)和 imageStates(2)列表<图像>元素之间交替)。如果向上键也被按下,他应该向左键或右键的方向跳跃,如果向下键被按下,他应该准备向左键或右键的方向降落。
我们还需要实现 Actor 类的 isFlipH 属性,以便角色根据他行进的方向面向正确的方向。我们将使用 JavaFX 功能来“翻转”或“镜像”任何围绕中心 Y 轴(isFlipH)或中心 X 轴(isFlipV)的图像,而不是使用另一个图像。一旦精灵动画状态与您在上一章中放置的运动代码相结合,您会惊讶于这个角色将变得多么逼真,并且我们仍然只使用了九个精灵状态图像(到目前为止,我们的新媒体资产使用的总数据量不到 84KB)。
在本章中,我们将只使用 Java 代码,并且只使用 Java FX animation timer(GamePlayLoop)超类来制作所有的角色动画。这样,我们通过仅使用 javafx.animation 包中使用最少内存开销的类来访问 脉冲事件计时引擎,从而优化了 JavaFX 脉冲引擎在游戏中的使用。AnimationTimer 类是最简单的类,没有类变量,只有一个要实现的. handle()方法,但它也是最强大的,因为它允许您编写自己的所有代码。
这种方法允许我们编写自定义代码,根据按下的键和移动来激活角色,而不是根据时间轴上的关键帧(及其键值)来触发预定义的时间轴对象。我在游戏引擎方面保持简单,并把所有的复杂性放到我们定制的游戏代码中。这将为我们以后省去很多麻烦,尝试“同步”基于关键帧和基于时间轴的线性动画,这将我们带入一个基于线性时间轴的范例,如 Flash 使用的。从 Java 编码的角度来看,这种 100%的 Java 8 编码方法当然更困难,但它给了我们更大的能力,以实现事件处理、屏幕移动、角色动画、物理和碰撞检测的无缝集成。设置大量预构建的 JavaFX Animation 子类可能最终会得到相同的结果,但代码会不那么优雅,并且可能更难以在其上构建游戏的未来版本。
我们所有的角色状态动画都将使用. setImageState()方法创建,该方法将从。update()方法,所以,我们将继续在我们的角色的运动和动画中进行组织。
无敌动画。setImageState()方法
在本章中,我们将创建一个(相当复杂的)方法,称为。setImageState(),它将根据在任何给定时刻按下的键来设置 InvinciBagel 角色的动画或运动状态。方法之前调用. setImageState()方法。中的 moveInvinciBagel()方法。update()方法将用于将角色的九个图像单元(帧)之一与角色的运动相结合。这将创造动画的幻觉,并且将在不使用任何动画时间线的情况下实现这一点。从游戏优化的角度来看,这意味着运行我们的 GamePlayLoop 的 JavaFX 引擎可以将其资源集中在单个动画(脉冲)引擎上。正如你在图 13-1 中看到的,我们需要添加一个.setImageState();方法调用,在。方法之前的 update()方法。moveInvinciBagel()方法调用并在。setBoundaries()方法调用。这样做之后,您必须创建一个空方法来消除错误突出显示。Java 代码如下所示:
private void``setImageState()
如图 13-1 所示,这个空的代码框架不会在代码中生成任何红色错误或黄色警告高亮。我们目前非常有条理,通过在这个 Bagel 类中仅使用四个方法调用,完成了所有的 KeyEvent 处理、边界检测、sprite 动画和 sprite 移动。update()方法。
图 13-1。
Create the private void setImageState( ) method; place a setImageState( ) method call in .update( ) method
我们要检查的第一件事是没有移动:也就是说,没有按下任何键,这样我们就可以正确地实现我们在上一章开发精灵移动算法时使用的“等待”InvinciBagel 状态。
InvinciBagel 等待状态:如果没有按键,则设置 imageState(0)
我们放置的第一个条件 if()语句将是默认或“无按键”状态,即 sprite zero,它显示不耐烦地等待移动和动画的 InvinciBagel。我们想要在圆括号内的 if 求值区域内寻找的是上、下、左、右变量的假值,它们都发生在同一时刻。到目前为止,我们一直在寻找一个真实的值,使用。isUp(),。isDown(),。isLeft()和。isRight()方法调用 invinciBagel 对象引用。在这种情况下,我们希望寻找一个错误的值。
要做到这一点,我们需要使用 Java 一元感叹号!操作员。这反转了布尔值,所以在我们的例子中,来自这些方法调用之一的 false 值将由一个!invinciBagel.isUp()构造来表示。为了查明是否有多个值同时为假,我们需要实现 Java 条件 AND 运算符,它使用两个连续的&字符,就像这个&&so;在这种情况下,我们将使用其中的三个& &条件 AND 运算符,来告诉 Java 编译器我们希望 Right 和 Left 以及 Down 和 Up 都为 false。所有这些逻辑都将进入 if()求值区域(在括号内)。在大括号内,如果满足 if()求值区域(如果 up、down、left 和 right 都为 false)的话,我们将使用。setImage()方法调用。在方法调用内部,我们将使用。对 imageStates List < Image >对象调用 get()方法,从 List < Image >对象中获取第一个图像引用 imageStates(0)。这是无敌的“等待”精灵细胞,显示他不耐烦地等待被移动(动画)。该构造的 Java 代码看起来像下面的 Java 编程结构(为了更容易阅读和学习,我对其进行了缩进):
if``(``!
! invinciBagel.isLeft() &&
! invinciBagel.isDown() &&
! invinciBagel.isUp() ) {
spriteFrame.``setImage``(imageStates.``get(0)
如图 13-2 所示,第一个 if()语句表示“如果没有按下任何箭头键”,是无错误的。如果您使用“运行➤项目”工作流程,并测试此代码,您将获得与上一章相同的结果!为了查看这段代码是否有效,我们必须首先让 InvinciBagel 运行,这样当我们使用箭头键停止移动他时,我们会得到这种不耐烦的“等待”状态,这实际上使它更有效(有趣),当这种等待是在本章中我们将要实现的所有动画运动的上下文中时!
图 13-2。
Add a conditional if() statement that checks for no movement, and sets the wait sprite image state (zero)
接下来,让我们开始实现一些其他的角色图像单元,尝试让我们的角色动画化!
InvinciBagel 运行状态:如果按键设置图像状态(1 & 2)
正如你在第八章中看到的,我将尝试只用两个精灵元素实现角色的动画运行状态,imageState(1)和 imageState(2)。正如你将要看到的,无论是从图像资产的角度,还是从编码的角度,这都是你能得到的最优化的了。考虑到您不能使用单个图像状态来制作任何东西(如运行周期)的动画,这一点尤其正确。也就是说,我们将在这一章中通过使用单个 cel,设计良好的精灵状态,结合我们在第十二章中放置的精灵运动代码,来创建许多非常真实的动画。if(invinciBagel.isRight())和if(invinciBagel.isLeft())语句结构最初会非常简单明了,但是随着我们在本章中增加细化功能,它们会变得更加复杂。我们将首先为这些奠定基础,然后添加 up 和 down 条件 if()语句,然后我们将细化左右键事件处理。在用于左右箭头键(以及 A 和 D 键移动)的 if()构造内部,我们将使用我们在第一个(不耐烦等待状态)if()构造中使用的相同的链式方法调用,只是这里我们将从 List < Image >对象调用 imageStates(1)或 imageStates(2) sprite cels,而不是使用 imageStates(0) sprite cel。如果按下右键或左键(true ),将 sprite 图像状态更改为状态 1 或 2 的 Java 代码应如下所示:
if(invinciBagel.``isRight
spriteFrame.``setImage``(imageStates.``get(1)
}
if(invinciBagel.``isLeft
spriteFrame.``setImage``(imageStates.``get(2)
}
如图 13-3 所示,Java 代码没有错误,我们已经准备好使用运行>项目工作流程测试这种初步运行模式。如果你快速连续按下左右箭头键,你会看到无敌跑!
图 13-3。
Add conditional if() statements that check for left/right movement, and sets the run sprite image states
因为这不是我们想让我们的游戏玩家让 InvinciBagel 跑起来的方式(因为它的本质是停止他在屏幕上的移动,因为它只是简单的跛脚),让我们快速地将你的上下键支持到位,这样我们就可以回来工作,在左右键上工作,这样我们就可以让运行周期工作了!
InvinciBagel 飞行状态:如果按键设置图像状态(3 & 4)
if(invinciBagel.isDown())和if(invinciBagel.isUp())条件 if()结构与左右键结构相同,除了它们调用 imageStates(3)和 imageStates(4)列表元素,以允许 InvinciBagel 角色“进来着陆”(cel 3)和“起飞飞行”(cel 4)。随着我们在本章中添加更多的图像状态,并将这个 cel 动画代码与我们的运动和边界代码相结合,您将会越来越有兴趣测试本章的编码结果!如果您想走程序员的捷径,请复制并粘贴。isRight()和。isLeft()构造,只需更改。得到(1)和。开始做某事。得到(3)和。得到(4)。如图 13-4 所示,代码目前非常紧凑,组织良好;结构化;和逻辑;在仅仅六行 Java 代码中,我们已经实现了九个图像状态中的一半以上!当然,我们仍然需要添加细化代码,以实现方向改变的精灵镜像和运行周期定时细化。控件的 Java 代码。isUp()和。isDown()方法结构应该如下所示:
if(invinciBagel.isDown()) {
spriteFrame.``setImage``(imageStates.``get(3)
}
if(invinciBagel.isUp()) {
spriteFrame.``setImage``(imageStates.``get(4)
}
正如你在图 13-4 中看到的,我们的代码是没有错误的,我们准备给左右箭头键事件处理代码增加几层复杂性,因为这两个键定义了 InvinciBagel 行进的方向(东和西)。因此,这两个条件 if()语句结构尤其需要变得更加复杂,因为向东(向右)行进将使用原始(isFlipH = false)子画面,而向西(向左)行进将利用每个子画面的镜像版本(isFlipH = true),围绕中心 Y 轴“翻转”图像资产。
图 13-4。
Add conditional if() statements that check for up/down movement and sets jump/land sprite image state
镜像精灵:将你的图像资产从 9 增加到 36
现在让我们回到现有的。isLeft()和。isRight()条件求值语句,在本章的课程中,这些语句将变得相当“健壮”(复杂),让我们添加恶意镜像功能。JavaFX API 将其镜像功能“隐藏”在 ImageView 类的。setScaleX()方法调用。虽然我们不打算缩放我们的图像资产,因为这样做会导致原始 PNG32 图像资产中的伪像,但有一个鲜为人知的技巧,您可以将-1(负 100%缩放因子)值传递给. setScaleX()方法,以围绕 Y 轴翻转或镜像图像资产(或进入)。setScaleY()方法,绕 X 轴翻转或镜像)。显然,我们还需要在另一个条件 if()结构中“撤销”这一操作,方法是将 1(正 100%缩放因子)传递到同一个方法调用中,这(通常)没有太大意义,因为我们的图像比例已经是 100%(未缩放),但是考虑到-1 缩放翻转因子可能已经预先设置,这就是我们如何确保镜像被禁用,并且我们再次将原始 sprite 图像资产用于该特定状态。您新升级的实现 sprite 镜像的 Java 语句现在应该看起来像下面的代码,它也在图 13-5 中突出显示:
if(invinciBagel.``isRight
spriteFrame.``setImage``(imageStates.``get(1)
spriteFrame.``setScaleX
}
if(invinciBagel.``isLeft
spriteFrame.``setImage``(imageStates.``get(1)
spriteFrame.``setScaleX``(``-1
}
正如你在图 13-5 中看到的,你的 Java 代码仍然没有错误。有趣的是,我们在 spriteFrame ImageView 对象上调用 sprite mirroring 方法,而不是在这个 ImageView 内部的图像资产上。这相当重要,因为这意味着我们可以在。isRight()和。isLeft()来翻转 ImageView 中显示的任何精灵状态(图像)!这是高度优化的编程!
图 13-5。
Add a .setScaleX() method call to the .isRight() and .isLeft() evaluations to flip the sprite around the Y axis
现在,我们的 sprite 镜像代码已经就绪,我们需要处理运行周期的问题,这是通过使用我们的条件 if()处理交替完成 imageStates(1)和 imageStates(2)来实现的。
动画您的运行周期:创建一个嵌套的 If-Else 结构
本章中精灵动画的下一步是为我们的角色制作一个运行循环的动画,这是我们通常用 JavaFX 关键帧和时间轴类来做的,但是我们在这里只用十几行代码就完成了。因为我们已经使用了 AnimationTimer 类,所以这是最佳的方法,并且只需要使用一个布尔变量就可以实现。因为我们的运行周期有两个 cel,所以我们可以使用这个布尔变量并在 true 和 false 之间改变它的值。如果这个我们称之为 animator 的布尔值为 false,我们将在 imageStates(1)中显示 cel,这是我们开始奔跑的位置(脚着地)。如果 animator 为 true,我们将在 imageStates(2)中显示 cel,这是我们全力奔跑的位置(双脚全力运动)。在 Bagel.java 类的顶部创建布尔动画变量。NetBeans 给了我一个“变量未初始化”的警告,所以我显式地将它设置为默认的布尔值 false,因为我希望运行周期总是从一只脚离开地面开始。变量声明语句应该如下所示,显示在图 13-6 的最上方:
boolean``animator``=``false
因为我们希望运行周期总是从 imageStates(1)脚离开地面开始,所以我们将在“没有按下箭头键”代码语句中添加一行animator=false;代码。该语句现在将做两件不同的事情:设置 imageStates(0)等待 sprite 图像引用;并确保 animator 变量被初始化为假值,这确保运行周期从脚踏实地开始,就像在现实生活中一样。“没有按下箭头键”条件 if()结构的新 Java 代码应该如下所示:
if(``!``invinciBagel.isRight()
!``invinciBagel.isLeft()
!``invinciBagel.isDown()
! invinciBagel.isUp() ) {
spriteFrame.setImage(imageStates.``get(0)
animator=false; }
控件的 Java 代码。isRight()和。isLeft()条件 if()结构现在将变得更加健壮,因为我们将不得不在确定右箭头键是否被按下的语句中嵌套另一个 if-else 条件语句。如果 right 为 true,它会将 ScaleX 属性设置为 1(非镜像),然后添加一个条件 If()语句来查看 animator 布尔变量的值是否为 false。如果 animator 为 false,我们使用便捷的方法链来获取 imageStates(1),并将该图像资产设置为 spriteFrame ImageView 将使用的 cel。之后,我们需要将 animator 变量设置为真值,以便稍后可以设置 imageStates(2)完整运行的精灵图像。如果 animator 为真,那么该结构的 else-if 部分将确认 animator 为真,如果为真,则再次使用spriteFrame.setImage(imageStates.get(2));方法链获取 imageStates(2)并将 animator 设置为假。该语句的新代码如图 13-6 所示,应该如下所示:
if(invinciBagel.``isRight()
spriteFrame.setScaleX(``1
if(``!animator
spriteFrame.setImage(imageStates.``get(1)
animator=true;
} else if(``animator
spriteFrame.setImage(imageStates.``get(2)
animator=false;
}
}
值得注意的是,在这种情况下你可以去掉else if(animator),只使用一个没有 if(动画)部分的 else。然而,通过在 if-else-if-else 结构中嵌套更多的代码,我们将使右(左)按键结构变得更加复杂,所以为了可读性以及未来的代码开发目的,我将保持这种方式。如图 13-6 所示,代码是无错的。
图 13-6。
Nest an if-else logic structure alternating between sprite cels 1 and 2 using the boolean animator variable
现在,您可以将完全相同的代码结构实现到您的。isLeft()条件 if()结构。由于玩家将使用左键或右键(但不是两者一起使用,至少直到我们在游戏开发的后期开始添加那些隐藏的“复活节彩蛋”功能),我们可以在两个。isRight()和。isLeft()条件 if()构造,允许我们在这里做一点内存使用优化。如您所见,唯一的区别是 ScaleX 属性被设置为镜像 sprite 图像(使用-1 值),因此,the if(invinciBagel.isLeft())条件 if()结构的 Java 代码应该如下所示:
if(invinciBagel.``isLeft()
spriteFrame.setScaleX(``-1
if(``!animator
spriteFrame.``setImage``(imageStates.``get(1)
animator=true;
} else if(``animator
spriteFrame.``setImage``(imageStates.``get(2)
animator=false;
}
}
正如你在图 13-7 中看到的,这段 Java 代码没有错误,你已经准备好使用你的 Run >项目工作流程,并测试 InvinciBagel 运行周期,这样你就可以看到你的超级英雄能跑多快(或者 JavaFX 中的 脉冲引擎能跑多快,使用你的 GamePlayLoop 类的 AnimationTimer 超类)。当你测试你的 Java 代码时,你会发现你的超级英雄角色跑得比人类可能的速度快得多(也比一个百吉饼跑得快得多);事实上,精灵动画细胞交替如此之快,它看起来像一个模糊的运行动画!
图 13-7。
Duplicate the nested if-else statement in .isLeft() structure, so InvinciBagel character runs both directions
接下来,我们需要添加一些 Java 代码来控制 InvinciBagel 精灵的运行速度。我们将使用两个整数变量来实现这一点:一个用作帧计数器,另一个保存运行速度值,稍后我们可以根据 vX(沿 X 轴的速度)变量来更改该值,以获得运行周期动画速度和精灵在屏幕上移动速度之间的真实匹配。
控制运行周期速度:设置动画节流程序逻辑
为了能够“节流”我们的运行周期 sprite 动画以实现不同的速度,我们需要引入一个名为 framecounter 的“计数器”变量,它将在我们将 false (sprite cel 1) animator 值更改为 true (sprite cel 2)之前计数到一定数量的帧。我们还将使用 runningspeed 变量,这样我们的动画速度就不会被硬编码,而是存在于一个变量中,我们可以在以后更改它。这允许我们对运行周期动画的速度(真实性)进行微调控制。在 Bagel.java 类的顶部声明这两个整数(int)变量,并将 framecounter 变量初始化为零,并将 runningspeed 变量的值设置为 6。既然都是假的(!animator)和 true (animator)二级 if()结构将使用这个“数到 6”变量,我们所做的数学运算将等于 6+6=12,分为 60FPS 脉冲计时循环,这意味着我们将未节流的动画减慢 500%(五倍,因为 60/12=5)。Bagel 类顶部的变量声明语句应该类似于下面的 Java 代码,也显示在图 13-8 的中间:
int``framecounter``=``0
int``runningspeed``=``6
正如你在图 13-8 中看到的,我点击了 framecounter 变量,所以它被高亮显示,你可以看到它在初始化语句中的使用,我们需要把它放入“没有按下箭头键”条件 if()结构中,就像我们处理 animator 变量一样。该 if()结构的代码如图 13-8 所示,如下所示:
if(``!``invinciBagel.isRight()
!``invinciBagel.isLeft()
!``invinciBagel.isDown()
! invinciBagel.isUp() ) {
spriteFrame.setImage(imageStates.``get(0)
animator=false;
framecounter=0;
}
就像我们希望 animator 布尔变量在所有箭头键未被使用时被重置为 false 值一样,我们也希望 framecounter 整数变量在所有箭头键未被使用时(即,同时处于释放状态)被重置为零值。正如你所看到的,我们不仅在这个条件语句中设置了 sprite 的等待图像状态,我们还用它来重置我们的其他变量。
图 13-8。
Add int variables at top of the class for framecounter and runningspeed and set to zero in no movement if
现在我们准备好了。isRight()和。isLeft()条件 if 结构甚至更复杂,因为我们将嵌套我们的 Java 逻辑三嵌套条件 if 结构更深,以允许我们将 framecounter 和 runningspeed 整数变量合并到我们的条件 if()结构中。这将使我们的动画代码在将 animator false 值更改为 true 之前“等待”六个脉冲事件周期,然后在将其改回 false 之前再等待六个脉冲事件周期。
这是相当复杂的 Java 代码,至少对于一个初学 Java 8 的人来说是这样,但是游戏编程本来就是一项复杂的任务,所以让我们继续学习如何为我们的运行周期动画编写这种节流机制的代码。
我想在本书中向您介绍一些高级主题,而这一个(实现速度节流)是我们无法避免的,因为这种使用简单布尔交替图像状态逻辑结构的运行速度在我们的游戏中是不可行的,考虑到 JavaFX 脉冲事件计时引擎令人难以置信的速度及其“控制台游戏”60FPS 的帧速率,这使得我们的 InvinciBagel sprite 运行周期看起来不仅不现实,而且看起来很痛苦!
也就是说,至少这是我们的 Java 8 编码在本章中将要达到的复杂程度,所以请抓紧时间,在下一节中,当我们创建 16 行 Java 代码(嵌套的条件 if()结构)时,请享受这段旅程。这将是大量的工作,但由此产生的运行周期油门控制将是非常值得的努力!
编写运行周期节流器:三重嵌套 If-Else 结构
我们要对布尔 animator if()结构进行的修改是在animator=true;语句的“周围”放置一个if(framecounter >= runningspeed){...}结构,这样 animator 在六个脉冲事件循环发生之前不会变为真。如果 framecounter 等于(或由于某种原因大于)六,animator 变为 true,framecounter 重置为零,并使用 imageStates(2)。如果 framecounter 小于 6,语句的 else 部分使用framecounter+=1;语句将 framecounter 递增 1。在这个结构的两个部分中,我们将 framecounter 代码包装在两个if(animator)代码周围,如图 13-9 所示:
if(invinciBagel.``isRight()
spriteFrame.setScaleX(``1
if(``!animator
spriteFrame.setImage(imageStates.get(``1
if(``framecounter >= runningspeed
animator=``true
framecounter=0;
} else {``framecounter+=1;
} else if(``animator
spriteFrame.setImage(imageStates.get(``2
if(``framecounter >= runningspeed
animator=``false
framecounter=0;
} else {``framecounter+=1;
}
}
图 13-9。
Add a third level of if-else nesting that prevents cels from alternating too quickly by using a framecounter
现在对条件 if()结构进行同样的修改。唯一的区别是 ScaleX 属性为-1(镜像精灵图像),如图 13-10 中的以下代码所示:
if(invinciBagel.``isLeft()
spriteFrame.setScaleX(``-1
if(``!animator
spriteFrame.``setImage``(imageStates.get(``1
if(``framecounter >= runningspeed
animator= true;
framecounter=0;
} else {``framecounter+=1;
} else if(``animator
spriteFrame.``setImage``(imageStates.get(``2
if(``framecounter >= runningspeed
animator= false;
framecounter=0;
} else {``framecounter+=1;
}
}
图 13-10。
Duplicate the if-else structure from the isRight() structure into the isLeft() structure using framecounter
现在又到了使用运行➤项目工作流程和测试运行周期代码的时候了,现在代码展示了一个平滑、均匀、真实的运行周期。您可以使用值 7 或 8 将跑步速度微调为稍慢,或使用值 4 或 5 将跑步速度微调为稍快。当我们为跑步增加更快的 vX 值(比如 vX = 2)时,我们可以将跑步速度设置为 3 或 4 来匹配它,使游戏更加真实。
我在本章测试代码时注意到的另外一件事,我想在这里纠正的是用于着陆的 sprite cel 图像。我一直在使用 imageStates(3),它是“着陆”的图像,实际上应该更好地用于冲突情况。让我们保存这个 sprite cel 图像状态,以便稍后在碰撞检测代码开发阶段使用,以表示与曲面的碰撞(刚刚着陆或在碰撞图像上)。我想在按下向下箭头键时使用的图像实际上是 imageStates(6),即“准备着陆”图像。修改后的 Java 代码将如下所示,并在图 13-11 中突出显示:
if(invinciBagel.isDown()) {
spriteFrame.``setImage``(imageStates.``get(6)
}
让我们确保我们的 InvinciBagel 精灵动画的专业性随着本章的每一节都在提高,并使用运行➤项目工作流程和测试所有四个箭头键。一定要用左右键(左上、左下、右上和右下)来测试上下键,以真正了解到目前为止您所编写的 Java 代码的能力。我们还有很长的路要走,但已经取得了令人印象深刻的成绩!
图 13-11。
Change .isDown() sprite cel to imageStates(6) using the imageStates.get(6) method call to use correct cel
优化运行周期处理:关闭飞行和着陆状态的处理
我想做的下一件事在概念上也是先进的,但使用的代码要少得多。作为一个优化迷,我担心的是当玩家使用上下箭头键时,animator、framecounter 和 runningspeed 变量以及使用它们的编程逻辑可能会占用内存,所以我想在条件 if()结构的顶部放一个语句,将。setScaleX() sprite 镜像代码保持不变,但如果使用了 up 和 down 键,则会关闭其余的处理逻辑。排除运行周期逻辑的 Java 代码应该基于上下箭头键变量,它们都显示为 false,表明玩家只使用了左键或右键。这个排除逻辑如图 13-12 所示,看起来像下面的 Java 代码(以粗体显示):
if(invinciBagel.``isRight()
spriteFrame.setScaleX(``1
if(``!animator``&& (``!invinciBagel.isDown()``&&``!invinciBagel.isUp()
spriteFrame.setImage(imageStates.get(1));
if(framecounter >= runningspeed) {
animator=true;
framecounter=0;
} else { framecounter+=1; }
} else if(animator) {
spriteFrame.setImage(imageStates.get(2));
if(framecounter >= runningspeed) {
animator=false;
framecounter=0;
} else { framecounter+=1; }
}
}
图 13-12。
Add if() statement logic to exclude processing of nested if-else hierarchy if down or up keys being pressed
我这样做的原因是因为当向上或向下键被按下时,跳跃(或飞行)或准备着陆精灵 cel 图像被显示。出于这个原因,优化指示我需要关闭所有“sprite cel 1 和 sprite cel 2 之间的交替”处理代码。我想这样做,以便当 imageStates(1)和 imageStates(2)甚至没有在 spriteFrame ImageView“容器”中使用(显示)时,这种持续的处理不会在内存和线程(CPU)中进行
为了实现这个优化目标,我在第二个嵌套 if()循环的外部添加了一个新的评估级别。这一层包含了使用 animator、framecounter 和 runningspeed 变量以及相关处理的所有 sprite cel 变化(1 到 2 以及相反)。这个新的更复杂的评估语句保证了如果满足新的条件,所有的运行周期处理逻辑都不会被执行。
这个新条件具体说的是,如果 animator 变量为假,并且 up 和 down 键都没有被使用(为假),则处理剩余的逻辑,在两个 sprite 图像状态之间切换(以及在每次切换之前等待一定数量的脉冲事件)。这意味着,如果按下向上或向下键中的任何一个(从而显示飞行或着陆精灵 cel 图像状态),则根本不会处理超过该点的任何编程逻辑,从而为我们将添加到游戏中的许多其他事情节省 CPU 处理周期,这些事情将需要将这种“节省”的处理开销用于其他与游戏相关的逻辑。
你可能想知道为什么我没有在这个编程结构的 else if(animator)部分添加同样的扩展条件。原因是循环的这一部分永远不会被执行,除非第一部分得到处理,我们用这个新语句排除了第一部分。这是因为在这个循环的第一部分,我们设置了animator=true;,如果按下了向上或向下键,这将永远不会发生(现在我们已经添加了扩展条件)。
真正酷的是,if(invinciBagel.isRight())和if(invinciBagel.isLeft())条件 if()结构现在可以用于镜像所有 sprite cel 状态图像资产,当您的玩家使用左右键设置角色行进的方向时,并且只有当左右键(仅)用于使角色运行时,该 Java 代码处理的运行周期部分才会发生。
确保你在条件 if()结构的if(invinciBagel.isLeft())部分实现了相同的扩展条件,如下所示,也可以在图 13-13 中看到。
if(invinciBagel.``isLeft()
spriteFrame.setScaleX(``-1
if(``!animator``&& (``!invinciBagel.isDown()``&&``!invinciBagel.isUp()
spriteFrame.setImage(imageStates.get(1));
if(framecounter >= runningspeed) {
animator=true;
framecounter=0;
} else { framecounter+=1; }
} else if(animator) {
spriteFrame.setImage(imageStates.get(2));
if(framecounter >= runningspeed) {
animator=false;
framecounter=0;
} else { framecounter+=1; }
}
}
图 13-13。
Add if() statement logic to exclude processing of nested if() hierarchy if down/up keys pressed to isRight()
接下来,让 ASDW 键成为他们自己的精灵控制键,而不是模仿箭头键。
添加事件处理:赋予 ASDW 键功能
因为我还需要实现几个 sprite 状态,所以我将升级事件处理器,使用双手游戏场景,使用 ASDW 箭头键或 ABCD(彩色)游戏控制器按钮,使用以下代码升级:
scene.``setOnKeyPressed
switch (event.getCode()) {
case UP: up = true; break;
case DOWN: down = true; break;
case LEFT: left = true; break;
case RIGHT: right = true; break;
case W:``wKey
case S:``sKey
case A:``aKey
case D:``dKey
}
});
scene.``setOnKeyReleased
switch (event.getCode()) {
case UP: up = true; break;
case DOWN: down = true; break;
case LEFT: left = true; break;
case RIGHT: right = true; break;
case W:``wKey
case S:``sKey
case A:``aKey
case D:``dKey
}
});
正如你在图 13-14 中看到的,代码是没有错误的,因为我在位于 InvinciBagel.java 类顶部的布尔上下左右复合声明语句中添加了 wKey、sKey、aKey 和 dKey 变量。
图 13-14。
Create event handling for the WSAD keys specifically, by using wKey, sKey, aKey, and dKey variables
接下来,我们需要使用源代码➤插入代码➤生成➤ Getters 和 Setters 的工作过程,让 NetBeans 编写八个。是()和。set()方法,这样我们就可以访问 Bagel.java 类中的变量。
创建 ASDW 键获取和设置方法:NetBeans 插入代码
将光标放在 InvinciBagel.java 类的右花括号前,如图 13-15 中的浅蓝色区域所示,使用源代码菜单选择插入代码选项。在生成浮动菜单中选择 Getter 和 Setter 选项以及 aKey、dKey、sKey 和 wKey 选项,如图 13-15 所示,这样 NetBeans 将生成 8 个。是()和。基于这个新的布尔声明语句的 set()方法也显示在该图的顶部:
private boolean up, down, left, right,``wKey``,``aKey``,``sKey``,``dKey``;
图 13-15。
Use the Source ➤ Insert Code ➤ Generate ➤ Getter and Setter sequence and select aKey, dKey, sKey, wKey
NetBeans 在类末尾生成的八种方法结构类似于下面的 Java 代码:
public boolean``iswKey()
return wKey;
}
public void``setwKey(boolean wKey)
this.wKey = wKey;
}
public boolean``isaKey()
return aKey;
}
public void``setaKey(boolean aKey)
this.aKey = aKey;
}
public boolean``issKey()
return sKey;
}
public void``setsKey(boolean sKey)
this.sKey = sKey;
}
public boolean``isdKey()
return dKey;
}
public void``setdKey(boolean dKey)
this.dKey = dKey;
}
正如您在图 13-16 中看到的,NetBeans 已经为我们添加到 InvinciBagel.java 类中的新 KeyEvent 处理变量创建了所有八个 getter 和 setter 方法。代码是没有错误的,我们准备回到 Bagel.java 类,并利用这些新的方法调用来实现跳转和回避。
图 13-16。
Hold a left arrow (or A) and up arrow (or W) key down at the same time, and move the Actor diagonally
添加跳跃和躲避动画:使用 W 和 S 键
让我们开始为使用另一组四个键的基础设施就位,以使我们的玩家在实现精灵(角色)动作(动画)时有更大的灵活性。使用键盘时,这些将使用 ASDW 键,使用游戏控制器时,这些将使用 JavaFX 键码类常量 GAME_A、GAME_B、GAME_C 和 GAME_D。在添加更多游戏玩法逻辑后,我将在游戏开发过程中稍后添加游戏控制器事件处理。基本跳转和回避(投射)子画面 cel 图像资产的实现将通过使用以下两个基本 Java 条件 if()结构来完成,这两个结构通过if(invinciBagel.iswKey())使用 W 键,通过if(invinciBagel.issKey())处理结构使用 S 键,如下所示,以及图 13-17 :
if(invinciBagel.``iswKey()
spriteFrame.``setImage``(imageStates.``get(5)
}
if(invinciBagel.``issKey()
spriteFrame.``setImage``(imageStates.``get(8)
}
图 13-17。
Add if() statements at the bottom of the method for .iswKey() and issKey for Jump and Evade animation
现在我们已经在游戏事件处理和角色动画处理中实现了 imageStates(0)、imageStates(1)、imageStates(2)、imageStates(4)、imageStates(5)、imageStates(6)和 imageStates(8)。其他精灵更适合用于碰撞检测,所以让我们把它们留到本书后面的章节。
最新详细信息:设置 isFlipH 属性
接下来,我想添加两行代码,我称之为“簿记”,因为我们已经在 Actor 超类中安装了 isFlipH 和 isFlipV 属性,所以如果我们镜像 X 或 Y 轴周围的精灵,我们需要确保正确设置这些变量,以反映 Actor 对象的更改(没有双关的意思),以便我们可以在游戏应用编程逻辑的其他领域中使用这些信息。我们将使用 Java this 关键字来引用我们在 Bagel.java 类中编码的 iBagel 对象,并调用。setIsFlipH()方法,如果 ScaleX 属性设置为 1,则使用 false 值;如果 ScaleX 属性设置为-1,则使用 true 值。需要注意的是,我们并不是绝对必须使用 Java 这个关键字;我使用它是为了意思清楚,所以你也可以简单地使用方法调用而不用点符号,就像这样:setIsFlipH(false);如果你喜欢的话。在图 13-18 中可以看到做这个简单簿记加法的 Java 代码,如下所示:
if(invinciBagel.``isRight()
spriteFrame.``setScaleX``(``1
this``.setIsFlipH(``false
} if(invinciBagel.``isLeft()
spriteFrame.``setScaleX``(``-1
this``.setIsFlipH(``true
}
图 13-18。
Be sure to set the isFlipH property for the Bagel object using .setIsFlipH() called off of the this (object)
接下来,让我们测试所有新的精灵动画代码,看看它是否做了我们认为逻辑上应该做的事情!代码很复杂,至少对于主要的左右箭头键字符移动来说是如此,但是它组织得很好,而且非常有逻辑性,从逻辑的角度来看,我看不出它有任何问题,但是使用 NetBeans 彻底测试它是找出答案的唯一方法!让我们接下来做那件事。要查看子画面图像状态,请参考第八章(图 8-2)。
测试 invincibagel 精灵动画状态:运行➤项目
现在是时候使用 NetBeans 运行➤项目工作流程并测试。setImageState()方法,该方法现在在。setXYLocation()和。setBoundaries()方法,但在。moveInvinciBagel()方法。因此,现在的逻辑进展是检查按键,基于此设置 X 和 Y 位置,检查以确保您没有越过任何边界,设置精灵动画(图像)状态,然后定位精灵。正如你在图 13-19 中看到的,当你使用左箭头键或右箭头键时,不可战胜的角色现在真实地在屏幕上运行。
图 13-19。
Testing the InvinciBagel character animation; showing here is the running animation using isFlipH mirror
如果您按下向上箭头键,同时(或之后)您按下向左或向右箭头键,结果如图 13-20 所示,InvinciBagel 将起飞并开始朝那个方向飞行,同样具有逼真的动画运动。
图 13-20。
Testing the InvinciBagel character animation; showing here is the leap up animation, using isFlipH mirror
如果你按下向下箭头键,同时(或甚至之后)你按下向左或向右箭头键,结果,如图 13-21 所示,是 InvinciBagel 将在准备着陆时下降,同样具有逼真的动画运动。
图 13-21。
Testing the InvinciBagel character animation; showing here is the landing animation, using isFlipH mirror
现在我们的箭头键正在调用主要的 run-leap-fly-land 游戏播放状态,让我们测试一下我们在本章末尾放置的这个新的 S 和 W 键逻辑,以便我们可以调用一些不太常用的(躲避子弹和跳过)sprite cel 状态,这将为我们在本章中放置的这个角色动画添加更多的多样性,只使用了大约 56 行代码(InvinciBagel.java 中有 26 行,Bagel.java 中有 36 行)。正如您在图 13-22 左侧看到的,当我们使用 S 键时,invincibegel 角色会侧转,这样子弹就会从他面前飞过;在屏幕截图的右侧,您可以看到当我们按下 W 键时,invincibegel 角色会越过物体!
图 13-22。
Testing InvinciBagel character animation; shown here is the evade (left) animation, and jumping (right)
在介绍动画的这一章中,你给这个游戏的主要无敌角色添加了相当多的“惊奇元素”。这将有助于使这款游戏成为各年龄段的流行游戏。
摘要
在第十三章中,我们使用高度优化的代码在 InvinciBagel 应用中实现了 sprite 动画。我们将七个基本的精灵形状与我们在第十一章中开发的按键移动和我们在第十二章中开发的边界检测结合起来,以创建一个真正活起来的完全动画化的无敌角色,并基于六个基本游戏控制键(上、下、左、右、W 和 S)的使用来实现这一点。
我们学习了如何使用 JavaFX ImageView 类 ScaleX 属性及其特殊用例设置来围绕 Y 轴翻转或镜像 ImageView“图像容器”内部的图像资产。这允许我们仅使用 9 个基本的精灵状态创建 36 个精灵图像状态,我们已经将它们作为图像资产导入到 List ArrayList 对象中。这是一种优化形式,因为它允许我们使用不到 84KB 的图像资产,而不是 336KB 的图像资产,用于我们的主要游戏超级英雄,不可战胜的角色。
接下来,我们学习了如何实现一个 Boolean animator 变量,我们使用它在两个不同的 sprite cel 运行状态之间制作动画,在我们的列表中是 imageStates(1)和 imageStates(2)。最终的运行周期对于专业使用来说太快了,所以接下来,我们添加了一个 framecounter 变量来减慢移动速度,并添加了一个 runningspeed 变量,允许我们对精灵的运行速度进行微调控制,我们将能够在稍后的游戏逻辑中利用这一点。
接下来,我们优化了我们的代码,这样,如果运行周期没有显示,也就是说,如果 InvinciBagel 正在飞行或着陆,就不会使用用于运行周期的变量和处理代码。我们还确保使用 this.setIsFlipH()方法调用在我们的 Actor 超类中(因此在我们的 Bagel 对象中)设置 isFlipH 属性。
接下来,我们在 InvinciBagel.java 类的事件处理代码中添加了四个新的游戏控制键,添加了四个新的私有布尔变量 aKey、sKey、dKey 和 wKey,并让 NetBeans 自动为它们创建 Getter 和 Setter 方法。在我们进行了增强之后,我们在 S 键上添加了精灵躲避动作图像,在 W 键上添加了精灵跳过动作图像,以使我们的游戏成为一个双手游戏,并为使用专业游戏控制器硬件做好准备。
最后,我们用其余的事件处理、演员和英雄类、CastingDirector 类、GamePlayLoop 类和 Bagel 类 sprite 移动和边界代码测试了这个新的 sprite 动画代码,这些代码是我们在前面六章(第七章到第十二章)中编写的。我们的精灵运动和动画无缝地配合在一起,当我们在游戏舞台上导航无敌的角色时,可以提供专业的效果。
在下一章中,我们将看看如何在游戏中添加固定的游戏精灵(道具演员)对象,这样当我们进入碰撞检测时,我们就有东西可以使用了,这样我们就可以开始真正使用我们最近的 CastingDirector 类了。
十四、设置游戏环境:使用Actor超类创建固定的精灵类
现在,我们已经在创建你的主要无敌角色方面取得了很大的进展,使用了 Bagel.java 类中的一些关键方法,以及演员和英雄超类,所有这些都与第七章到第十三章中的 GamePlayLoop 类和 CastingDirector 类一起放置到位,现在是时候添加固定的精灵对象了,我称之为“道具”,到场景和舞台上(屏幕上)。这些固定的物体对游戏来说几乎和主角本身一样重要,因为它们可以用来提供障碍、屏障、免受敌人攻击的保护以及主要游戏英雄角色要克服的各种挑战。我们还需要场景中的这些固定对象,以便在我们的碰撞检测程序逻辑中使用,并测试我们的 CastingDirector 类(object)。
如果你还记得,在第八章中,我们用固定精灵创建了演员超类,用运动精灵创建了英雄超类。我从运动精灵开始,因为主要的游戏角色是运动精灵,即使运动精灵更复杂,就其本质而言,玩起来也更有趣!现在我需要拿出一个章节,将 Actor 超类付诸实施,并创建一些与 Prop 相关的子类。我们将使用这些 Actor 子类在我们的场景(舞台)中填充平台、障碍物、障碍、桥梁、宝藏和类似的游戏固定位置对象,您将想要将这些对象添加到场景(舞台)中,以创建游戏世界并增强游戏体验(也称为用户体验)。
我们将使用 Actor 超类创建四个固定道具子类,这将使构建固定场景(当您创建了多个场景时,也称为关卡)变得容易。Prop.java 类将“按原样”使用您的固定 sprite 图像资产,而 PropH.java 类将使用 JavaFX spriteFrame.setScaleX(-1); Java 语句将 isFlipH 属性设置为 true,并围绕 Y 轴镜像图像资产。PropV.java 类将使用 JavaFX spriteFrame.setScaleY(-1); Java 语句将 isFlipV 属性设置为 true,并围绕 X 轴镜像图像资产。PropB.java 类(B 代表“两者”)将 isFlipV 属性和 isFlipH 属性都设置为 true,这将使用两个 JavaFX spriteFrame.setScale(-1); Java 语句镜像 X 和 Y 轴周围的图像资产。
一旦我们创建了这四个道具相关的演员子类,我们将使用它们来放置固定的对象到场景中,以创建这个游戏的第一级。这样,当我们进入碰撞检测章节时,真实游戏中的一切都将就位,我们将能够开始编码碰撞检测逻辑;然后,最终,一个自动攻击引擎;然后是游戏运行逻辑,它决定了如何实现评分引擎。
这一章对于创造一个更有特色的游戏是有价值的。任何游戏设计的主要部分,在这种情况下,它是 Ira H. Harrison Rubin 的 InvinciBagel 角色和游戏,是建立角色(无论是在单人游戏还是多人游戏中,英雄和他或她的敌人)参与的环境对游戏的成功和流行至关重要,因为这些固定元素是为玩家创造游戏挑战的主要部分。
创建 Prop.java 类:扩展 Actor.java
在 NetBeans 8 中打开 InvinciBagel 项目,右键单击包含您的。java 文件并选择新的➤ Java 类菜单序列。在如图 14-1 所示的“新建 Java 类”对话框中,将类命名为 Prop,并接受 NetBeans 建议的其他默认设置,然后单击“完成”按钮。
图 14-1。
Right-click on the project folder and select New ➤ Java Class; using a New Java Class dialog, name it Prop
一旦 NetBeans 创建了一个引导类,添加 Java extends 关键字和 Actor,如图 14-2 所示。
图 14-2。
Add a Java extends keyword and the Actor superclass. Mouse-over the error highlighting underneath Prop
使用建议的 Alt-Enter 工作流程,如图 14-3 所示,并选择“实现所有抽象方法”
图 14-3。
Use the Alt-Enter work process to pop up a helper dialog filled with suggestions regarding fixing the error
移除 throw new UnsupportedOperationException()并创建一个空的。update()方法。
图 14-4。
NetBeans generates abstract method for you; remove the throw new statement; create an empty method
Java 代码在类声明语句下仍然有一个红色波浪状的错误高亮,如图 14-5 所示。
图 14-5。
Overriding .update() method doesn’t remove the error, so mouse-over again, to reveal needed constructor
将鼠标放在上面,您会看到需要使用下面的 Java 代码编写一个 Prop()构造函数方法:
public``Prop
要删除图 14-6 中的错误,使用 Alt-Enter,选择为 javafx.scene.image.Image 添加导入。
图 14-6。
Add a public Prop() constructor, and mouse-over the error highlight, and select Add import for Image class
如果您仍然看到红色波浪错误突出显示,那么一旦您导入了 Image 类引用,这是因为您需要将 Prop()构造函数方法定义中的参数列表传递到 Actor 类。这是使用 Java super 关键字(有时称为 super()构造函数(方法))完成的,使用以下 Java 语句:
super (SVGdata, xLocation, yLocation, spriteCels);
因为这个 Prop 类使用了一个默认的,或者说未剪裁(未镜像)的 imageStates(0)图像,所以这是我们需要做的第一件事,来创建一个符合抽象 Actor 类的可用 Prop 类。另外,记住 Actor 类为我们初始化了所有的标志属性,这要感谢一个详细的设计过程。图 14-7 中可以看到这个类的 Java 代码,它现在包括了基本的超级构造函数,并且现在是无错误的:
图 14-7。
Add the super() constructor method call inside of the Prop() constructor, to get rid of the error highlight
我们需要做的下一件事是,使用传入构造函数方法的 xLocation 和 yLocation 值来定位固定的 sprite。setTranslateX()和。setTranslateY()方法。请记住,您在第十二章中使用了这些方法。moveInvinciBagel()方法。在这个 Prop()构造函数方法中,我们将再次使用这些方法,将这些固定的精灵放置在舞台上,在这里,您的构造函数方法参数指示对象构造函数在屏幕上定位它们。
重要的是要记住,由于我们在第八章中所做的工作,Actor 超类的 Actor()构造方法已经为我们执行了iX=xLocation;和iY=yLocation; sprite iX 和 iY 属性设置。因此,我们在 Prop()构造函数方法中所要做的就是在构造函数方法内部调用spriteFrame.setTranslateX(xLocation);和spriteFrame.setTranslateY(yLocation);,并且在我们的 super()构造函数方法调用之后。请注意,在代码中,在 super()构造函数方法调用中以及在。setTranslateX()和。setTranslateY()方法调用,以便在 Prop 对象实例化期间将固定精灵定位在舞台上,这样我们就不必在代码中的其他地方执行此操作。该类的 Java 代码和构造函数方法如下所示:
package invincibagel;
import javafxscene.image.Image;
public class``Prop``extends``Actor
public Prop(String SVGdata, double``xLocation``, double``yLocation
super (SVGdata, xLocation , yLocation
spriteFrame.setTranslateX(``xLocation
spriteFrame.setTranslateY(``yLocation
}
@Override
public void update() {
// empty method
}
}
正如你在图 14-8 中所看到的,这段 Java 代码是没有错误的,并且在场景(舞台)中定义和放置道具所需要的一切都已经就绪,这要感谢 Actor (fixed) sprite 类的良好设计。这包括 SVG 碰撞形状数据、X 和 Y 位置(场景中的位置)以及一个或多个图像资源。我们包含一个。一个固定的 sprite 类中的 update()方法允许我们拥有动画(不止一个图像单元)道具,如果我们想在我们的游戏设计过程中变得有趣,并增加游戏视觉设计的 wow 因素。
图 14-8。
Position a fixed sprite on the Stage in the constructor method using a .setTranslateX() and .setTranslateY()
接下来让我们创建 PropH、PropV 和 PropB(代表“两者”)类。这些将使用构造函数方法自动为我们围绕 X,Y 或 X 和 Y 轴镜像道具,因此对象将固有地镜像图像。
镜像属性类:在构造函数中设置 isFlip 属性
为了使我们构建场景的过程更容易,因为一切(碰撞、位置、动画)都是构造函数方法调用的一部分,道具之间的区别在于它们是如何镜像的(X 轴或 Y 轴,或者两个轴)。在这一节中,我们将在构造函数方法中创建为我们翻转精灵的 Prop 类的变体,这样我们所要做的就是创建 PropX 对象类型来获得我们想要的效果。创建第一个 PropH 类,如图 14-9 所示。
图 14-9。
Right-click on the project folder and select New ➤ Java Class; using a New Java Class dialog, name it PropH
将前面创建的基本 Prop 类的内容,包括 package、import 和 super()构造函数,复制到 PropH 类中,然后将 Prop 改为 PropH,如图 14-10 所示。
图 14-10。
Create a PropH class structure identical to the Prop class structure, except using PropH, instead of Prop
将 Java this 关键字添加到 super()构造函数下的新行中的 PropH()构造函数方法中,然后使用(键入)句点键打开方便的 NetBeans 方法选择器弹出窗口,如图 14-11 所示。
图 14-11。
Add a Java this keyword reference to PropH (object) inside of the constructor and open up method helper
双击。setIsFlipH 方法,在图 14-11 中突出显示,并在方法调用参数区插入一个真值。完成的this.setIsFlipH(true); Java 语句如图 14-12 所示。
图 14-12。
Call the .setIsFlipH() method, off of a Java this (PropH) object reference, and pass it the true setting value
值得注意的是,如果我们只在构造函数方法中使用this.setFlipH(true);方法调用,我们仍然需要观察 isFlipH 是这样设置的,并在代码中的其他地方调用您的.spriteFrame.setScaleX(-1);方法调用。让我们继续将这个方法调用放入您的构造函数方法中,这样我们所有的 PropH 对象都将自动使它们的图像资产围绕 Y 轴翻转。如果你想知道为什么?setScaleX(-1)方法镜像 Y 轴周围的图像资产,这也是我想知道的,但我所做的是将 isFlipH 与。setScaleX(-1)(因为 X 是 H 或水平轴)。也就是说,我会。setScaleY(-1)围绕垂直 Y 轴的镜像,对我来说更有意义。添加另一行代码,键入 spriteFrame 并按下句点键,然后选择。在弹出窗口中设置 ScaleX,如图 14-13 所示。
图 14-13。
Call a .setScaleX() method, off of the spriteFrame object variable reference, and pass it a -1 setting value
在弹出的方法选择器中双击 setScaleX(double value) void 选项,然后添加-1 值,完成方法调用。现在所有 PropH 对象将自动镜像图像资源 Y 轴!PropH 构造函数的最终代码。setTranslateX()和。setTranslateY()方法调用将在舞台上实际定位固定的 sprite 位置,如图 14-14 所示,类似于下面的 Java 代码:
import javafxscene.image.Image;
public class``PropH``extends``Actor
public PropH(String SVGdata, double``xLocation``, double``yLocation
super (SVGdata, xLocation , yLocation
this``.setIsFlipH(``true
spriteFrame.``setScaleX``(``-1
spriteFrame.setTranslateX(``xLocation
spriteFrame.setTranslateY(``yLocation
}
}
图 14-14。
A completed PropH() constructor method creates a fixed PropH object flipped along the horizontal X axis
接下来,执行与 PropH 类相同的工作过程,创建一个 PropV 类,将 isFlipV 属性设置为 true,并实现spriteFrame.setScaleY(-1);方法调用,如图 14-15 所示。
图 14-15。
A completed PropV() constructor method creates a fixed PropV object flipped along the vertical Y axis
接下来,执行与 PropV 类相同的工作过程,并创建 PropB 类。这个类将把 isFlipV 和 isFlipH 属性都设置为 true,并实现一个spriteFrame.setScaleX(-1);方法调用和一个spriteFrame.setScaleY(-1);方法调用。这最后一个 PropB 类的 Java 类结构将沿 X 和 Y 轴镜像固定道具图像,如图 14-16 所示,类似于以下代码:
public class``PropB``extends``Actor
public PropB(String SVGdata, double``xLocation``, double``yLocation
super (SVGdata, xLocation , yLocation,
this.``setIsFlipH``(``true
spriteFrame.``setScaleX``(``-1
this.``setIsFlipV``(``true
spriteFrame.``setScaleY``(``-1
spriteFrame.setTranslateX(``xLocation
spriteFrame.setTranslateY(``yLocation
}
}
正如你在图 14-16 中看到的,代码是没有错误的,你现在有了一个 PropB 类,它将为你的场景创建固定的对象,这些对象同时围绕 X 轴和 Y 轴翻转或镜像!有了这四个不同的固定道具类,我们就可以快速轻松地设计场景(以及最终的游戏关卡)元素,除了声明正确的道具类、引用正确的图像资源以及 X、Y 轨迹(位置)和碰撞多边形 SVG 数据之外,什么都不用做。
图 14-16。
A completed PropB() constructor method creates a fixed PropB object flipped along both the X and Y axis
现在我们准备使用这四个新的固定精灵类,并学习如何添加场景(舞台)元素。
使用道具类:创建固定场景对象
在我们开始编写 InvinciBagel.java 类的代码之前,您应该已经在 NetBeans 中打开了它自己的标签,并使用这些道具类将固定的精灵设计元素添加到我们的游戏中,我想向您展示如何去掉文件名旁边那些讨厌的小扳手图标。在创建了这四个新的道具相关的类之后,你的 IDE 屏幕上现在可能已经有几个了,所以让我们来学习如何让这些消失吧!如图 14-17 所示,如果右击扳手图标旁边的文件名,选择编译文件菜单选项,或者使用 F9 功能键,扳手就会消失。本质上,扳手表示您正在处理该文件,也就是说,您已经对该文件中的代码进行了更改,并且没有通过编译它来检查错误并保存它,从而使该代码永久化。
图 14-17。
If you want to get rid of the little wrench icon next to the file name, right-click the file, and Compile File
添加属性和图像声明:属性和图像对象
让我们首先声明我们需要创建的对象,以将固定精灵添加到我们游戏的场景和舞台对象中,这些对象最初将是一个名为 iPR0 的道具对象,它代表 InvinciBagel Prop zero,最终随着本章的进行,会有一个名为 iPH0 的 PropH 对象、一个名为 iPV0 的 PropV 对象和一个名为 iPB0 的 PropB 对象,这样您就有了使用所有这些类的经验,并且我们可以确保它们都按照我们设计的方式工作。我们还将在图像 iB0 到 iB8 声明的末尾添加一个 iP0 图像对象声明,然后再添加一个 iP1 图像对象声明。Java 语句如图 14-18 所示,如下所示:
Prop``iPR0
private Image iB0, iB1, iB2, iB3, iB4, iB5, iB6, iB7, iB8,``iP0
图 14-18。
Add Prop object declaration, name it iPR0 (invincibagel Prop zero), and add an iP0 to Image declaration
在我们可以实例化图像对象之前,我们需要将图像资产复制到/src 文件夹中,因此使用文件管理软件包(我使用的是 Windows Explorer)将 prop0.png 和 prop1.png png 8 图像文件与您已经安装在游戏项目中的其他十几个图像资产一起复制进来,如图 14-19 所示。
图 14-19。
Use your file management software to copy the prop0 and prop1 PNG8 files into the project /src folder
接下来,让我们实例化包含一个适当图像的图像对象,一个可平铺的砖块,这样我们就可以使用我们的四个 InvinciBagel.java 方法。loadImageAssets(),。createGameActors(),。addGameActorNodes(),最后是。createCastingDirection()将固定道具对象安装到游戏的场景对象和游戏的舞台对象中。
实例化图像对象:使用。loadImageAssets()方法
在 NetBeans 中打开“InvinciBagel.java”选项卡,并打开 loadImageAssets()方法。现在添加一个引用 prop0.png 文件及其 72x32 像素大小的 iP0 图像对象实例化语句。创建 iP0 图像对象的 Java 代码,如图 14-20 所示,应该类似于下面的 Java 语句:
iP0 = new Image(" /prop0.png ", 72 , 32 , true, false, true);
图 14-20。
Add the iP0 Image object instantiation using the prop0.png file name and the 72 by 32 pixel image size
现在我们准备添加我们的第一个道具对象到我们的游戏中来添加一个固定的精灵,这将允许我们使用在本章中放入 invincibagel 包代码库中的类来创建我们的游戏设计。
使用道具对象添加固定精灵:。addGameActors()
在你之后。loadImageAssets()方法在游戏设计过程中,我们设计了另外三个方法“容器”,用于向游戏中添加演员对象。这些是按照我们在这个过程中需要使用它们的顺序来调用的,所以让我们继续按照这个顺序来组织和使用它们。我们需要执行的第一个实例化创建了一个新的(使用 Java new 关键字)Prop 对象。现在,我们将再次使用我们的“虚拟”SVG 形状数据,以及屏幕 X 和 Y 位置的 0,0 中心,最后是 iP0 图像资源,我们刚刚在本章的前几节中声明并实例化了它。在图 14-21 中可以看到的 Java 实例化语句应该类似于下面的 Java 代码行:
iPR0 = new Prop("M150 0 L75 200 L225 200 Z", 0, 0, iP0
下一步是将 iPR0 属性对象的 ImageView 节点对象添加到场景图形根对象。这是使用. getChildren()完成的。add()方法链引用了 iPR0 属性对象内的 spriteFrame ImageView 对象,使用点标记法,使用以下 Java 语句,如图 14-21 所示:
root``.getChildren().add(``iPR0.spriteFrame
接下来,使用。addCurrentCast()方法将 iPR0 对象添加到 castDirector 对象,如图 14-21 所示:
castDirector``.addCurrentCast(``iPR0
图 14-21。
Instantiate an iPR0 Prop object, add it to the root Scene Graph, and add it to the CurrentCast List
接下来,使用运行➤项目工作流程测试代码。结果如图 14-22 左侧所示。
图 14-22。
Testing InvinciBagel 0,0 prop placement (left) and changing the z-index in .addGameActorNodes() (right)
正如您所看到的,InvinciBagel 字符在 Prop 对象的后面,这意味着我们需要改变。addGameActorNodes()方法,因为节点对象添加到场景图的顺序决定了它们的 z 索引或 z 顺序。如图 14-23 所示,我已经移动了。add()方法调用将 iPR0 添加到 iBagel 的 IP r0 之上,这样你就可以在图 14-22 的右侧看到,InvinciBagel 角色现在位于道具对象的顶部。当然,一旦我们增加了碰撞检测,这就不是问题了。让我们使用相同的工作流程,并在场景中添加一个 PropH 对象,这样我就可以向您展示如何翻转和镜像这个平铺图像,以便为您的 Java 8 游戏开发创建无缝的可平铺结构。
让我们使用相同的命名约定,将 PropH 对象命名为 iPH0。在图 14-23 中可以看到的 Java 实例化语句应该类似于下面的 Java 代码行:
iPH0 = new PropH ("M150 0 L75 200 L225 200 Z", 0, 0, iP0
添加固定 sprite 属性的下一步是将 iPH0 属性对象的 ImageView 节点对象添加到场景图形根对象中。这是通过使用。getChildren()。add()方法链,该方法链通过使用点标记来引用位于 iPH0 PropH 对象内部的 spriteFrame ImageView 对象。这是使用下面的 Java 语句完成的,该语句也显示在图 14-23 中:
root``.getChildren().add(``iPH0.spriteFrame
最后,我们将使用。addCurrentCast()方法,我们在第十章中创建的,使用下面一行 Java 代码,将这个 iPH0 对象添加到 castDirector CastingDirector 对象内部的 CURRENT _ CAST ListArrayList 对象,也显示在图 14-23 的底部:
castDirector``.addCurrentCast(``iPH0
图 14-23。
Instantiate an iPH0 PropH object, add it to the root Scene Graph, and add it to a CurrentCast List
正如你在图 14-23 中看到的,我也已经将道具对象的 0,0 坐标改为 0,148,并将道具对象的坐标改为 72,148。这将把 Y 轴镜像的 PropH 对象无缝地放置在 Prop 对象的右侧。如果您现在想查看无缝平铺效果,可以使用“运行➤项目”工作流程。如果您当前没有运行 NetBeans 8 IDE,并且您想要预测图 14-26 ,您现在可以看到这种平铺(镜像)效果。最后,我还将把 PropV 和 PropB 类(对象)集成到这个镶嵌中。
让我们使用完全相同的工作流程,并在场景中添加一个 PropV 对象,这样我们就可以看到如何围绕 X 轴翻转和镜像这个 tile 图像,从而为 Java 8 游戏开发创建更加复杂的无缝 tileable 构造。让我们使用相同的命名约定,并将这个新的 PropV 对象命名为 iPV0。您的新 Java 实例化语句,可以在图 14-24 中看到,应该看起来像下面这行 Java 代码:
iPV0 = new PropV ("M150 0 L75 200 L225 200 Z", 0, 0, iP0
添加固定 sprite 属性的下一个逻辑步骤是将该 iPV0 PropV 对象的 ImageView 节点对象添加到场景图形根 StackPane 对象中。这是通过使用。getChildren()。add()方法链,它引用 spriteFrame ImageView 对象,该对象位于 iPV0 PropV 对象内部,使用点标记法。这可以通过使用下面的 Java 编程语句来完成,该语句也显示在图 14-24 中:
root``.getChildren().add(``iPV0.spriteFrame
最后,我们将使用。addCurrentCast()方法,我们在第十章中创建的,使用下面一行 Java 代码,将这个 iPV0 对象添加到 castDirector CastingDirector 对象内部的 CURRENT _ CAST ListArrayList 对象,这也显示在图 14-24 的最底部:
castDirector``.addCurrentCast(``iPV0
图 14-24。
Instantiate an iPV0 PropV object, add it to the root Scene Graph, and add it to a CurrentCast List
最后,让我们将一个 PropB 对象添加到场景中,这样我就可以向您展示如何同时围绕 X 轴和 Y 轴翻转(镜像)可平铺图像。我们将遵循我们的命名约定,将 PropB 对象命名为 iPB0。在图 14-25 中可以看到的实例化语句应该类似于下面的 Java 代码:
iPB0 = new PropB ("M150 0 L75 200 L225 200 Z", 0, 0, iP0
接下来,让我们将 iPB0 PropB 对象的 ImageView 节点添加到场景图形根对象中。这是使用. getChildren()完成的。add()方法链。这引用了一个 spriteFrame ImageView 对象,它位于 iPB0 PropB 对象内部,使用点标记。这是使用下面的 Java 语句完成的,该语句也显示在图 14-25 中:
root``.getChildren().add(``iPB0.spriteFrame
最后,我们将使用。我们在第十章的中创建的 addCurrentCast()方法,用于将此 iPB0PropB 对象添加到 castDirector CastingDirector 对象内部的 CURRENT _ CAST ListArrayList对象,可以在图 14-25 的底部看到,该方法使用以下单行 Java 代码:
castDirector``.addCurrentCast(``iPB0
既然我们已经将本章前半部分创建的所有四个 Actor 子类投入使用,我们可以测试应用,看看不同的 X 轴和 Y 轴镜像对 prop0.png 砖块做了什么。
图 14-25。
Instantiate an iPB0 PropB object, add it to the root Scene Graph, and add it to a CurrentCast List
使用运行➤项目并测试游戏,看看每个砖块是如何被不同地镜像的,如图 14-26 所示。
图 14-26。
Run ➤ Project; Prop, PropH, and PropV shown at the left, and all four Prop subclasses shown at the right
接下来,我们来看看如何使用大型场景道具对象来合成舞台上的背景元素。
使用更大的场景道具:用 JavaFX 合成
我们创建的这四个道具演员子类的一个真正好处是,它们允许我们利用 PNG8(背景图像资产)和 PNG32(带有 alpha 通道的真彩色合成图像)图像资产在我们的游戏场景和舞台对象中进行数字图像合成。如果我们不通过实现碰撞检测来对运动精灵游戏角色使用固定道具,并且如果我们在后台保留这些固定道具,通过观察我们在。addGameActorNodes()方法就 Actor z-index 而言,我们可以使用我们为角色和障碍开发的相同合成引擎来优化游戏视觉元素。我们可能根本不需要在游戏中使用任何背景图片。至少,这允许我们添加更简单的背景图片,比如一个有云的基本天空,或者一个日落。这些压缩更好,由于他们的简单,可以使用 PNG8 图像与原始的结果。接下来让我们添加一个更大的固定 sprite 道具,它的宽度接近 500 像素,高度接近 100 像素。如图 14-27 所示,我们需要添加的第一件事是另一个道具对象,我们将命名为 iPR1,另一个图像对象,我们将命名为 iP1,使用以下代码:
Prop``iPR0,``iPR1
private``Image``iB0, iB1, iB2, iB3, iB4, iB5, iB6, iB7, iB8, iP0,``iP1
图 14-27。
Add an iPR1 Prop declaration (shown with all other PropH, PropV and PropB declarations) and iP1 Image
复制你的 iP0 镜像实例,创建一个 iP1 镜像,引用 prop1.png,如图 14-28 所示。
图 14-28。
Add the iP1 Image object instantiation using the prop1.png file name, and the 496 by 92 pixel image size
复制您的 iPR0 对象实例化和添加代码,创建一个 iPR1 对象,如图 14-29 所示。
图 14-29。
Instantiate an iPR1 Prop object, add it to the root Scene Graph, and add it to the CurrentCast List
为了在屏幕顶部放置一块苔藓,我在构造函数方法调用中使用了 0,-150 屏幕坐标。现在使用运行➤项目工作流程来看看结果,如图 14-30 左侧所示。
图 14-30。
Run the project; the Prop iPR1 is shown at the left, and iPV1 PropV mirrored object is shown at the right
你也可以“翻转”大型道具,创造一些非常酷的效果。创建一个 iPV1 声明和实例化,如图 14-31 所示,并创建一个 PropV 对象,它将沿着 X 轴镜像苔藓岩石。这可以在图 14-30 的右侧看到。在这一章中,我们已经在使用固定精灵道具类能力方面取得了很大的进步。
图 14-31。
Instantiate an iPV1 PropV object, add it to the root Scene Graph, and add it to a CurrentCast List
摘要
在第十四章中,我们创建了固定精灵“道具”类,允许我们设计游戏场景和固定对象,我们的运动精灵演员对象将与之交互。我们首先创建了 Prop 类,它扩展了我们在第八章中创建的 Actor 类。我们使用了。setTranslateX()和。setTranslateY()方法在构造函数方法中使用 xLocation 和 yLocation 参数来定位舞台上的 ImageView,类似于我们使用。moveInvinciBagel()类,仅在固定精灵的情况下,在构造函数方法内部,移动仅进行一次,以将道具定位在舞台上的场景中。
接下来,我们创建了更复杂的 PropH 和 PropV 类,它们除了在场景中定位固定的精灵之外,还围绕 Y 轴(PropH)和 X 轴(PropV)自动镜像它们。我们还创建了一个 PropB (B 代表 Both)类,它将自动镜像 X 轴和 Y 轴周围的固定 sprite 图像资产。
接下来,我们学习了如何通过在我们的 InvinciBagel.java 主游戏设计类中声明、实例化和添加(到 JavaFX 场景图以及 CastingDirector 对象)这些道具类来实现它们。我们学习了如何在屏幕上定位我们的固定精灵,以及如何创建无缝的镜像平铺效果,并测试了我们的新道具类,以确保它们可以用于设计游戏关卡。
最后,我们用更大的道具测试了这四个新的固定 sprite 类中的两个,如果我们不通过将它们添加到 CastingDirector 对象中来定义用于碰撞检测的固定 sprite 对象,可以想象它们可以用于场景背景合成。这将有助于减少游戏的数据足迹,最大限度地减少大型 PNG24 背景图像的使用,允许我们用小得多的 PNG8 背景图像替换这些数据繁重的图像。这些可以描绘场景“设置”,如雨天、多云的天空或生动的日落。
在下一章,我们将看看如何在游戏代码中添加一个数字音频音效引擎。这个音频引擎将使用 JavaFX AudioClip 类为游戏添加音效排序功能,以便我们在添加物理和碰撞检测等内容时,所有基本的新媒体元素(固定和运动图像以及音频反馈)都可以工作。通过这种方式,我们可以开始添加多媒体,在游戏体验中利用玩家的所有感官。
十五、实现游戏音频资源:使用 JavaFX AudioClip类音频序列引擎
既然我们已经使用抽象的 Actor(fixed sprite)和 Hero(motion sprite)超类创建了 motion sprite 和 fixed sprite 类,我们需要将代码放入播放游戏音频资产的位置。虽然一般不认为数字图像(视觉)资产重要,但数字音频(听觉)资产对游戏质量非常重要。事实上,您会惊讶于这些优秀的音频资产能为您的 Java 8 游戏产品增加多少感知价值。在本章中,我们将学习如何使用诸如 Audacity 2.0.6 这样的开源工具,为您的 Java 8 游戏开发优化和实现数字音频资产。
幸运的是,JavaFX AudioClip 引擎(实际上,它是 javafx.scene.media 包中的一个类)可以为我们的游戏开发带来很多动力。这是因为该类本质上被设计为音频排序引擎,能够控制音频资产性能的各个方面,以及使用 6 个八度音阶(三个向上和三个向下)的音高移位功能来创建新的音频资产。在本章开始时,我们将详细学习这个类,然后在我们的主要 InvinciBagel.java 类中实现它,并在新的 Bagel.java 类中使用它。playAudioClip()方法,我们将在该类的 primary 中对其进行编码和调用。update()方法。
在我们详细了解 JavaFX AudioClip 类之后,我们将开始使用流行的 Audacity 2.0.6 数字音频编辑(和音频效果)软件,我们在第一章中安装了该软件,当时我们安装了所有的开源游戏开发软件工具。我们将使用 Audacity 完成一个音频资产创建和优化过程。我们将使用我们在第五章中学到的概念,涵盖新媒体内容创作概念,并优化数字音频文件以实现 800%的数据空间节省。我们将这样做,以便我们的数字音频资产不使用超过 64KB 的系统内存;事实上,我们将获得六个 16 位数字音频资产,以使用不到 62KB 的数据。
一旦我们创建了六个音频资产,它们将与我们用来控制无敌角色的六个不同的键相匹配,我们将创建。InvinciBagel 类中的 loadAudioAssets()方法,并了解如何声明 AudioClip 和 URL 对象。在里面。loadAudioAssets()方法然后我们将一起使用这两个类(对象),以便为游戏创建我们的数字音频资产,并将它们安装到玩家的计算机系统(或消费电子设备)内存中。
一旦这六个 AudioClip 对象就位,我们将让 NetBeans 为 AudioClip 对象创建六个 Setter 方法,然后对它们进行“变形”。setiSound()方法添加到。我们需要的 playiSound()方法。完成之后,我们将进入 Bagel.java 类,并添加一个. playAudioClip()方法。在这个方法中,我们将调用。playiSound()方法,基于玩家按下的键。
在本章中,我们有很多内容要讲,所以让我们从深入了解 JavaFX AudioClip 类及其各种属性和方法开始,我们可以用它们来调用令人印象深刻的音频序列!解决了这个问题之后,我们就可以进入有趣的内容,开始使用 Audacity 2.0.6 和 NetBeans 8 进行创作了!
JavaFX AudioClip 类:一个数字音频序列器
public final AudioClip 类是 javafx.scene.media 包的一部分,本质上是一个数字音频样本回放和音频排序引擎,旨在使用简短的音频片段或“样本”来创建更复杂的音频演奏。这就是为什么这个类是用于 Java 8 游戏开发的完美的数字音频媒体播放类。从下面显示的 Java 继承层次结构可以看出,JavaFX AudioClip 类是 java.lang.Object master 类的直接子类。这意味着 AudioClip 类已经被“临时编码”,专门用于作为数字音频排序引擎。
java.lang.Object
> javafx.scene.media. AudioClip
您的 AudioClip 对象将分别引用内存中的一个数字音频样本。这些可以以几乎为零的延迟触发,这使得这个类成为用于 Java 8 游戏开发的完美类。AudioClip 对象的加载方式与媒体(长格式音频或视频)对象类似,使用 URL 对象,但行为方式有很大不同。例如,媒体对象不能自己播放;它需要一个 MediaPlayer 对象,如果包含数字视频,还需要 MediaView 对象。媒体对象将更适合于长格式的数字音频资产(如音乐),这些资产不能同时放入内存中,并且必须进行流式传输以获得最佳的内存利用率。MediaPlayer 只能将足够的解压缩数字音频数据“预滚动”到内存中,以便播放一小段时间,因此 MediaPlayer 方法对于较长的数字音频剪辑(尤其是经过压缩的数字音频剪辑)来说更加节省内存。
AudioClip 对象可以在实例化后立即使用,这一点您将在本章后面看到,对于 Java 8 游戏开发来说,这是一个重要的属性。AudioClip 对象回放行为可以说是“触发并忘记”,这意味着一旦您的 playiSound()方法调用之一被调用,您在数字音频样本上的唯一可操作控制就是调用 AudioClip。stop()方法。
有趣的是,您的 AudioClip 对象也可能被触发(播放)多次,audio clip 甚至可以被同时触发,这一点您将在本章稍后部分看到。要使用 Media 对象达到同样的效果,您必须为要并行播放的每个声音创建一个新的 MediaPlayer 对象。
AudioClip 对象如此通用(响应迅速)的原因是因为您的 AudioClip 对象存储在内存中。AudioClip 对象使用代表整个声音的原始、未压缩的数字音频样本。它以原始的、未压缩的状态存储在内存中,这就是为什么在本章的下一节我们将使用 WAVE 音频文件格式。这种音频格式应用零压缩,因此,优化的数字音频样本的最终文件大小也将代表这些样本中的每一个将利用的系统内存量。
然而,AudioClip 类真正令人印象深刻的是它通过其属性和三个重载属性赋予开发人员的能力。play()方法调用。使用 AudioClip 不定常数,您可以设置样本回放优先级,将音高(声音的频率)向上移动 8 倍(较高八度)或向下移动 1/8(较低八度),在空间频谱的任何位置移动声音,控制声音的左右平衡,控制声音的音量(或振幅),以及控制声音播放的次数,从一次到永远。
除了这些 AudioClip 属性的 getter 和 setter 方法之外,还有。播放()和。stop()方法以及三个(重载的)方法。play()方法:一个用于简单(默认)回放;您可以在其中指定音量;您可以在其中指定音量、平衡、频率(音高移位系数)、声相和样本优先级。
控制 AudioClip 对象的关键是调用三者之一。play()方法,具体取决于您希望如何控制样本回放。使用。play()进行简单的回放,就像我们在本章将要做的那样;或者使用。播放(双倍音量)以从 0.0(关)到 1.0(满)的相对音量播放您的 AudioClip 对象;或者使用。play(双音量、双平衡、双速率、双声相、双优先级)以相对音量(0.0 到 1.0)、相对平衡(-1.0 左、0.0 中、1.0 右)、音高移位率(0.125 到 8.0)、相对声相(-1.0 左、0.0 中、1.0 右)播放您的 AudioClip 对象;以及优先级整数,其指定哪些样本优先于其他样本播放,哪些样本由于资源和优先级低而可能不被播放。接下来让我们开始使用 Audacity 来优化我们的示例!
创建和优化数字音频:Audacity 2.0.6
发布 Audacity 的最新版本——在我写这篇文章的时候是 2.0.6,用你的麦克风录下你说“left”这个词的声音幸运的是,我的 NetBeans 8 和 Java 8 开发工作站也是我的 Skype 工作站,我在一个支架上有一个基本的 Logitech 可调麦克风,我可以用它来制作基本的音频文件,我们需要将音频文件放入我们在第章第 1 3 中放置的每个不同精灵动作的位置。在本章的这一节,我将介绍数字音频文件的“临时创建”和优化工作流程,对于我们在本章的编码部分需要使用的六个数字音频文件中的每一个,您都可以自己完成这项工作。如果您想简单地了解使用 JavaFX AudioClip 类实现数字音频资产的 Java 8 编码部分,您也可以使用本书软件资源库中包含的六个音频文件。我建议大家复习一下如何优化未压缩的音频,以便在系统内存中使用,因为我们将要获取 113KB 的原始音频数据,从中剔除 99KB 的数据,并将其再减少 88%,只有 14KB。
正如您在图 15-1 的左侧所看到的,我已经记录了口语单词“left”,并使用 Audacity 选择工具只选择了记录会话中包含音频数据的那部分,以较暗的灰色阴影显示。因为您可以在 Audacity 波形编辑区域看到数字音频波形表示,所以您可以看到您刚才记录的数据所在的录音部分。录音中不包含任何数字音频数据的部分看起来就像一条直线。
获取原始数字音频数据的最快方式,这是我们真正想要的。WAV 文件,并且是我们希望使用 Audacity 进行优化的唯一数据,是使用文件➤导出选择菜单序列。这将允许我们使用 WAVE PCM 数字音频格式直接将选定的音频数据写入 left.wav 文件。这样做之后,我们就可以开始一个新的 Audacity 编辑会话,只需打开该文件,开始数字音频内容优化过程。
图 15-1。
Launch Audacity, record your voice saying the word “left,” and then select the wave and Export Selection
在 Audacity 中使用导出选择菜单选项后,您将得到导出文件对话框,如图 15-2 所示。在对话框底部的文件名:字段中键入 left,然后单击保存按钮。
图 15-2。
In the Export File dialog, name the file “left” using the .WAV audio file type
现在,我们在 left.wav 音频文件中只剩下口语词,并已保存该文件,以便我们可以看到基线原始 32 位 44.1kHz 数字音频将为我们提供什么数据足迹(以及我们游戏的内存足迹,如果我们要按原样使用该文件),我们可以结束录制会话,因为它已经达到了目的。我们将通过使用文件➤关闭菜单序列来完成此操作,该菜单序列可以在图 15-3 的左侧看到。
接下来,我们将返回并使用文件➤打开菜单序列打开 left.wav 文件,该文件仅包含我们希望优化的数字音频数据段。这可以在图 15-3 (菜单序列)的中间看到,在屏幕截图的右边你可以看到一个选择一个或多个音频文件(文件打开)的对话框。请注意,left.wav 文件显示了它的原始数据量(113 KB)。
我们可以使用这个原始数据占用量作为基线,看看在我们即将开始的优化过程中,我们减少了多少次数据占用量(一旦完成,最终将减少 8.8 倍)。
图 15-3。
Close the current editing session, then Open the “left” file, noting its raw file size, for further optimization
当你点击打开按钮并在 Audacity 中第一次打开一个文件时,你会得到一个警告对话框,如图 15-4 所示。这将建议您可以制作原始文件的副本以在编辑会话中使用,而不是使用原始文件。这就是多媒体行业中所谓的“非破坏性”编辑,并且总是一个非常好的想法,因为它本质上为您提供了一个备份文件(原件),作为工作流程的一部分。
选择“编辑前制作文件副本(更安全)”单选按钮选项,并选中“不再警告,并始终使用我上面的选择”复选框,这将使 Audacity 2 成为一个非破坏性的非线性数字音频编辑软件包。单击“确定”按钮,我们就可以开始您的数字音频数据优化工作流程了。我们将优化我们的数字音频数据,但不压缩它,我将进入为什么这是下一步。
图 15-4。
Enable non-destructive audio editing in Audacity
优化与压缩:音频内存占用
你可能想知道为什么我使用未压缩的脉冲编码调制(PCM)波。wav)文件格式,而不是许多人用来存储数字音频音乐文件的行业标准的. MP3 文件格式。在我们开始优化过程之前,我将在前面介绍这样做的原因。在数字音频领域,数据占用优化实际上分为两个阶段。首先,优化采样分辨率(32 位原始录音、24 位 HD 音频、16 位 CD 质量音频和 8 位低质量音频)和采样频率(44.1kHz、22.05kHz、11.025kHz 是主要频率级别,仍然可以保持足够的数据以获得高质量的结果),然后应用压缩。压缩会影响文件大小;在这种情况下,它是你。Java 资源文件。
那么,为什么我们不把我们的文件压缩成 MP3 格式,使我们的。JAR 文件小几千字节?其主要原因是因为 MP3 是一种“有损”格式,它丢弃了音频样本的原始数据(和质量)。因为 JavaFX AudioClip 类将获取我们的数字音频资产并将其解压缩到内存中,所以如果我们使用 MP3,内存中包含的音频数据质量将比使用 WAV 格式时低。鉴于我们将在样本优化工作过程中获得至少 8 倍的数据占用空间减少(我们将在本章的下一节中学习),并且我们所有的数字音频资产都将被优化到 4KB 到 14K 之间的数据占用空间,相对于样本质量的降低,MP3 压缩不会给我们带来任何真正的 JAR 文件数据占用空间的减少,这将“消耗”我们的“成本”。游戏音频是短脉冲音效和音乐循环,所以我们可以使用 WAV 文件格式,仍然可以得到一个很好的结果,而不必使用任何压缩。另一个优点是,你在我们的文件管理软件中看到的 WAV 文件的数据大小也是样本将使用的内存量。
音频采样分辨率和频率:优化您的内存占用
内存数据占用减少过程的第一步是获取原始 32 位数据采样速率,并将其降低 100%,从 32 位浮点数据降至 16 位 PCM 数据,如屏幕截图左下方的图 15-5 所示。在 Audacity 样本编辑区域左侧的灰色信息面板中找到下拉箭头,我在这个截图中用红色圈出了它,因为如果您不习惯使用它,很难找到它。这将为您提供一个菜单,允许您设置数据显示(波形或频谱图)和设置样本数据格式,这是我们想要用来选择 16 位 PCM 选项而不是 32 位浮点选项的子菜单。不要用这个菜单设置你的采样率,因为它会降低你的声音(如果你以后想用这个作为特殊效果,你可以试试)。接下来,我们将了解设置采样速率的正确工作流程。
图 15-5。
Click the drop-down menu arrow at the left and select the Set Sample Format ➤ 16-bit PCM setting option
重要的是要注意,如果您在进行这种数据位级别的更改后保存您的文件,您的文件大小将不会改变!您可能已经注意到,“导出文件”对话框正在以 16 位 PCM WAVE 格式保存您的文件,因此它正在做与您在内存中所做的相同的调整,以调整磁盘上的文件大小。我在这里简单地介绍了这一步骤,以便您对整个过程有一个全面的了解,首先是降低采样数据速率,然后是降低采样频率速率,这是我们接下来要做的事情,最后是解决立体声与单声道采样问题,这将是我们在本章这一部分的最后一步。
每次应用工作流程时,这些“优化措施”都可以将文件大小(和内存占用)减少 100%或更多。事实上,当我们将采样频率从 44,100 降低到 11,025 时,我们将减少 200%的数据占用空间(从 44,100 降低到 22,050,然后从 22,050 降低到 11,025)。
设置音频采样频率:减少 200%的内存数据占用
在 Audacity 中为您的项目设置数字音频采样频率的正确方法是使用项目采样频率设置的下拉对话框。这可以在图 15-6 的左下角看到,在一个红色方框内突出显示。选择 11025 频率设置,这会将音频数据(将这些视为声波的垂直切片)采样速率从每秒 44,100 次降低到每秒 11,025 个数据切片,或者首先将采样的音频数据降低 4 倍,这是由于数据中的采样频率步长(在这种情况下,您应该将其视为使用的内存,而不是使用的文件空间)优化工作流程而导致的 200%的数据占用空间减少。
图 15-6。
Reduce audio sampling frequency by four times by reducing it from 44100 per second to 11025 per second
您可以在 44、100 和 8000 采样频率之间尝试这七种不同的设置,因为每种设置都有不同的质量水平,8000 的质量太低,不适用于声音样本,但它可能适合“脏”或“嘈杂”的声音,如爆炸。
如果你想听听这些不同的设置听起来像什么,当然,在你选择了每一个之后,点击图 15-6 左上角显示的音频传输按钮中的绿色播放(向右的三角形)。你会看到 32,000 频率听起来就像 44,100 频率,22,050 频率也是如此。16,000 或 11,025 的频率速率听起来并不“明亮”,但仍然可用,因此我使用 11,025 的速率,以获得甚至 4 倍的数据下采样。这是因为均匀的 2 倍(100%)或 4 倍(200%)缩减采样将始终提供最佳结果。这是因为所涉及的数学没有留下“部分”样本(或像素,因为同样的概念适用于成像)。
接下来,让我们使用文件➤导出工作流程。如果您想在菜单上看到这个,这个菜单顺序可以在图 15-1 中看到,并且显示在导出选择选项上方的文件菜单上。在图 15-7 所示的“导出文件”对话框中,您可以使用不同的文件名保存新版本的文件,这样,left.wav 文件中既有原始的未压缩数据,也有 left 立体声. wav 文件中的新的压缩(立体声)数据。将该文件命名为左立体声,点击保存按钮,将其保存为未压缩的 16 位 PCM WAV 文件,如图 15-7 左侧所示。
图 15-7。
Use the File ➤ Export dialog, name the file leftstereo, Save the file, then use File ➤ Open to check its file size
接下来我们要做的是使用图 15-3 中显示的相同工作流程,并使用文件➤打开菜单序列,打开选择一个或多个音频文件对话框,如图 15-7 右侧所示,这将允许我们将鼠标悬停在 leftstereo.wav 文件上,并看到其大小为 28.0 KB,比原始的 112 KB 源文件大小少四倍,正如我们所预期的那样!
因此,我们将这个表示单词“left”的音频文件的内存需求从 1/9 兆字节(112 KB)减少到 1/36 兆字节(28KB)。这意味着您可以拥有 36 个这样大小的音频资产,并且仍然只使用 1 兆字节的系统内存!当我创建其他五个音频资产时,这一个被证明是最大的,最小的(up 和 s,你可能已经猜到了)每个都不到 4KB!
我们数字音频优化工作流程的最后一步是将这些数据从立体声文件转换为单声道文件。我们这样做是因为我们的游戏音频资产不需要同一个口语词的两个副本。大部分游戏音频特效也是如此,比如激光爆破、爆炸;单声道音频在这些类型的音频音效情况下工作得很好。这一点尤其正确,因为 JavaFX AudioClip 类及其声相和平衡功能还允许我们使用单声道数字音频资源模拟立体声效果,如果我们愿意的话。
这还会将我们的数据占用空间再减少 100%,为我们提供 14KB 的音频文件。我们可以将 72 个这种大小的数字单声道音频资产放入 1 兆字节的系统内存中,因此使用单声道(单声道)音频资产而不是立体声数字音频资产是一件非常好的事情,这也是我们接下来要讨论的原因。
立体声与单声道音频:再减少 100%的内存占用
我们的数字音频优化工作流程的最后一个阶段是将我们的数字音频数据从使用立体声音频资产转换为单声道音频资产。我们将这样做,因为在这种情况下,我们不需要为我们的游戏音频资产复制两个相同的口语单词。对于大多数游戏相关的数字音频特效来说也是如此,比如激光爆破和爆炸。在这些类型的音频音效环境中,单声道音频的效果与立体声音频一样好。这一点尤其正确,因为 JavaFX AudioClip 类为开发人员提供了音频平移和平衡功能。这将允许开发者使用单声道音频资产来模拟立体声效果。Audacity 能够将立体声音频资产(两个声道,一个左声道和一个右声道)数据组合成一个听起来相同的单声道音频资产。
如果我们使用图 15-8 中的菜单序列所示的 Audacity Tracks ➤立体声音轨转单声道算法,将我们的两个立体声音轨合并成一个单声道音轨,它将会再减少 100%的数字音频数据占用空间,为我们提供一个 14KB 的音频文件。我们可以将 72 个这样大小的数字音频资产放入 1 兆字节的系统内存中。事实上,通过使用我在本章的这一节中展示的(未压缩的,不多不少的)数字音频数据占用优化,我已经成功地将所有六个数字音频资产放入了不到 62 KB 的内存占用中。
图 15-8。
Use the Tracks ➤ Stereo Track to Mono algorithm to combine the stereo samples into one Mono sample
在使用这种立体声到单声道的算法之前,我想让你做的是点击 Audacity 顶部的 Play transport 按钮,仔细听几次立体声音频资源。接下来,使用图 15-8 所示的菜单序列,调用立体声到单声道的合并算法。在您看到单个单声道音频资产后,您可以向前看,并在图 15-9 中看到,再次点击播放传输按钮,然后听音频样本,现在它是单声道的,看看您是否可以检测到任何差异。
使用专门的音效,这种差异甚至更难察觉(如果你能察觉的话)。Audacity 2.0.6 软件包是一个完全专业的数字音频编辑、增甜和音效程序,正如你在这里看到的,你可以使用该软件的正确工作流程来实现专业的游戏音频开发结果,这就是为什么我让你在第一章中下载并安装它。为了确保您拥有最强大的 Audacity 2.0.6 版本,请确保您下载了所有的 LADSPA、VST、Nyquist、LV2、LAME 和 FFMPEG 插件,然后将它们安装在 C:\ Program Files \ Audacity \ Plug-Ins 文件夹中,并重新启动 Audacity。
准备编码:导出资产并将其复制到项目中
让我们使用文件➤导出菜单序列导出最终的单声道文件并将其命名为 leftmono.wav,如图 15-9 所示。您可以使用相同的工作流程记录其他五个文件,或者使用我创建的资产,如果您愿意的话。
图 15-9。
Again use the File ➤ Export menu sequence and name the file leftmono and select the WAV 16-bit PCM
使用操作系统文件管理实用程序将六个音频资产复制到 InvinciBagel/src 文件夹中,如图 15-10 所示,以便我们在本章的剩余部分编写代码时参考。
图 15-10。
Use your file management software to copy the six .WAV files into the InvinciBagel/src project folder
现在我们准备好回到 Java 8 编码中。首先,我们将在 InvinciBagel.java 类中添加代码,以实现六个 AudioClip 对象,这些对象将引用我们的音频资产,然后,在 Bagel.java 类中,我们将在一个新的。playAudioClip()方法,我们将把它添加到我们的 Bagel 类的 primary。update()方法。
向 InvinciBagel.java 添加音频:使用 AudioClip
要实现 AudioClip 声音引擎,我们需要做的第一件事是声明六个私有 AudioClip 对象,在 InvinciBagel.java 类的顶部使用一个复合声明语句。我将用下面一行 Java 代码将这些声音命名为 0 到 5,如图 15-11 所示:
private``AudioClip
如图 15-11 所示,您必须使用 Alt-Enter 工作流程,并选择“为 javafx.scene.media.AudioClip 添加导入”选项,并让 NetBeans 8 为您编写 AudioClip 类导入语句。
图 15-11。
Add the private AudioClip compound declaration statement for your iSound0 through iSound5 objects
引用音频剪辑资源:使用 java.net.URL 类
与 JavaFX 中的图像对象(可以使用简单的正斜杠字符和文件名来引用)不同,数字音频资产不容易引用,需要使用 URL 类,它是 java.net(网络)包的一部分。URL 类用于创建 URL 对象,该对象提供统一资源定位符(URL)文件引用,它本质上是指向“数据资源”的“指针”,而“数据资源”通常是新的媒体资产,在我们的 Java 8 游戏开发中,它是/src 文件夹中的一个 WAVE 音频文件。
像 AudioClip 类一样,URL 类也是临时编写的,以提供 URL 对象,正如您从 Java 类层次结构中看到的那样,它看起来像下面这样:
java.lang.Object
> java.net. URL
我们实现六个 AudioClip 对象的第二步是使用复合声明语句在 InvinciBagel.java 类的顶部声明六个名为 iAudioFile0 到 iAudioFile5 的私有 URL 对象,这可以在图 15-12 中看到,看起来像下面的单行 Java 代码:
private``URL
如图 15-12 所示,您必须再次使用 Alt-Enter 工作流程,并选择“为 java.net.URL 添加导入”选项,并再次让 NetBeans 8 为您编写 URL 类导入语句。
图 15-12。
Add a private URL compound declaration statement for the iAudioFile0 through iAudioFile5 URL objects
现在我们准备编写加载 URL 对象的 Java 代码,然后使用这些代码实例化我们的 AudioClip 对象。为了在该类中使用自定义方法来维护我们的高级代码组织,让我们将 loadAudioAssets()方法添加到我们的。start()方法接下来,然后创建一个私有的 void loadAudioAssets()方法来保存我们的 AudioClip 相关代码,以防我们将来要在我们的游戏中添加六个以上的数字音频资产。
值得注意的是,由于 AudioClip 类在音高移动和 2D 空间(从左到右)音频移动方面的多功能性,您应该不需要像您想象的那样多的音频资源,因为您甚至可以将六个设计良好的音频资源变成数百种不同的游戏音效。
正在添加您的音频资产加载方法:。loadAudioAssets()
在你的 start()方法中创建一个方法调用,如图 15-13 所示,名为loadAudioAssets() ;,并在loadImageAssets();方法调用之前按逻辑方法顺序放置它。为了消除红色波浪错误突出显示,在 createSceneEventHandling()方法之后添加一个private void loadAudioAssets(){}空方法。通过这种方式,您的数字音频资源将在您的 KeyEvent 处理设置完成后,并且在您的数字图像资源被引用和加载到内存之前被引用和加载到内存中。
图 15-13。
Create the private void .loadAudioAssets() method to hold the AudioClip object instantiation statements
在你的内心。loadAudioAssets()方法体,您将有两个 Java 代码语句,因为加载数字音频资产比引用数字图像资产要复杂一些。首先,您将使用 getClass()加载您的第一个 iAudioFile0 URL 对象。getResource()方法链。这个方法链用您想要使用的数字音频样本资源加载 URL(统一资源定位器)对象,并将为您的类对象执行此操作(在本例中,这是 InvinciBagel 类,因为这是我们编写此代码的 Java 类)。
您正在寻找的获取 URL 的资源位于。getResource()方法调用,并使用与您用于数字图像资源的资源引用相同的格式,在本例中是“/leftmono.wav”文件引用,它由。getResource()方法转换为二进制 URL 数据格式。该 Java 代码如图 15-14 所示,看起来像下面的 Java 方法体:
private void``loadAudioAssets()
iAudioFile0 =``getClass().getResource
iSound0 = new AudioClip(iAudioFile0``.toString()
}
有趣的是,第二行代码使用了。toString()方法调用,它将这个 URL 对象转换回 String 对象,这是用音频资源 URL 加载 AudioClip()构造函数方法所必需的。你可能在想:为什么不用iSound0 = new AudioClip("/leftmono");?您可以尝试这样做,但是,您必须使用“file:/Users/Users/my documents/netbeans projects/InvinciBagel/src/left mono . wav”对目录进行“硬编码”
我使用了 URL 对象方法,以便您能够从 JAR 文件内部引用这个音频文件,而不是使用上面显示的方法,该方法需要硬盘驱动器上的“绝对”位置。因此,这个 getClass()。getResource()方法链正在将“相对”引用数据添加到此 URL 对象中。InvinciBagel 类需要这个相对引用数据,以便能够从 NetBeans 8 项目 InvinciBagel/src 文件夹内部以及 Java 8 游戏应用的 JAR 文件内部引用 WAV 音频资源文件。
图 15-14。
Instantiate and load the URL object, and then use it inside of the iSound0 AudioClip object instantiation
接下来,使用您信任的程序员的快捷方式,将这两行代码复制并粘贴五次,这是创建其他五个 AudioClip 对象的简单方法。将 iAudioFile0 和 iSound0 上的零分别更改为 iAudioFile1 至 iAudioFile5 和 iSound1 至 iSound5。然后将 WAV 音频文件名引用分别改为 rightmono.wav、upmono.wav、downmono.wav、wmono.wav、smono.wav。您完成的 loadAudioAssets()方法应该看起来像下面的 Java 方法体,如图 15-15 所示:
private void loadAudioAssets() {
iAudioFile0``= getClass().getResource("/``leftmono.wav
iSound0 = new AudioClip(``iAudioFile0
iAudioFile1``= getClass().getResource("/``rightmono.wav
iSound1 = new AudioClip(``iAudioFile1
iAudioFile2``= getClass().getResource("/``upmono.wav
iSound2 = new AudioClip(``iAudioFile2
iAudioFile3``= getClass().getResource("/``downmono.wav
iSound3 = new AudioClip(``iAudioFile3
iAudioFile4``= getClass().getResource("/``wmono.wav
iSound4 = new AudioClip(``iAudioFile4
iAudioFile5``= getClass().getResource("/``smono.wav
iSound5 = new AudioClip(``iAudioFile5
}
因为我们已经将 AudioClip 对象设为私有,所以我们需要在 InvinciBagel 类中创建方法,可以使用方法调用从我们的 Bagel.java 类(以及稍后开发的其他类)中调用这些方法。
图 15-15。
Create more AudioClip objects referencing the rightmono, upmono, downmono, wmono, and smono files
使用您的源代码➤插入代码➤生成➤ Setters 工作过程打开生成 Setters 对话框,如图 15-16 所示,并选择 iSound0 到 iSound5 对象,这样 NetBeans 就创建了 6 个。setiSound()方法。
图 15-16。
Use the Generate Setters dialog and create six .setiSound() methods
现在我们准备使用这六个新的。NetBeans 为我们编写的 setiSound()方法,我们并不需要它,因为我们的音频剪辑是使用。loadAudioAssets()方法,来创建六个。事实上,我们确实需要 playiSound()方法来回放这六个数字音频资产。让我们接下来做那件事。
提供对音频剪辑的访问。playiSound()方法
我们将要做的是我认为是另一个程序员的捷径,但我没有复制和粘贴,而是使用 NetBeans 源代码➤插入代码函数为我要更改的 iSound 对象创建 Setter 方法。setiSound()到。playiSound()方法,这样我就不必键入所有这六个方法体。如图 15-17 所示,NetBeans 为我们创建了六个完整的方法体,我们所要做的就是移除方法参数区域内的 AudioClip iSound 引用,将 setiSound()改为 playiSound(),最后将this.iSound0 = iSound0;语句改为this.iSound0.play();。我们将为这六个人中的每一个人做这件事。setiSound()方法体,这将允许我们快速创建六个。playiSound()方法体。
图 15-17。
Edit these six .setiSound() methods, created by NetBeans, at the bottom of the InvinciBagel.java class
编辑过程相对简单:选中 setiSound 的 set 部分,在 set 上键入“play”;选择参数区域的内部(AudioClip iSound#),并按 delete 或 backspace 键删除它;最后,在 this.iSound 和 the 之间的方法内部,选择 Java 语句的“= isound”部分;分号并键入。改为播放()。完整的 Java 方法体如图 15-18 所示,应该如下所示:
public void playiSound0() {
this.``iSound0
}
public void playiSound1() {
this.``iSound1
}
public void playiSound2() {
this.``iSound2
}
public void playiSound3() {
this.``iSound3
}
public void playiSound4() {
this.``iSound4
}
public void playiSound5() {
this.``iSound5
}
图 15-18。
Turn .setiSound() methods into .playiSound() methods by adding calls to the AudioClip .play() method
触发了。java 中的 playiSound()方法。playAudioClip()方法
现在我们已经声明了,universal resource 定位(引用)并实例化了我们的 AudioClip 对象,并创建了。playiSound()方法允许我们从 InvinciBagel.java 类的“外部”触发这些数字音频样本,我们可以进入 Bagel.java 类,并编写一些允许我们触发这些音频对象的代码,以查看 AudioClip 类的工作情况。用我们现有的代码做到这一点的最好方法是使用我们用来移动我们的运动精灵对象的事件处理器代码,也允许我们为我们当前为游戏设置的每个按键事件触发这些声音中的一个。这就是为什么我用它们将被触发的键来命名这些文件。我们需要在 Bagel.java 类中做的第一件事,类似于我们在 InvinciBagel 类中做的,是对。update()方法引用了空的私有 void playAudioClip()方法。这个方法调用和空的方法体如图 15-19 所示。
图 15-19。
Create an empty .playAudioClip() method in the Bagel.java class and add a call to it inside of .update()
在 playAudioClip()方法体中,我们需要创建条件 if()结构,类似于我们在第一章 2 中为 sprite 移动所做的。我们将通过 invinciBagel.playiSound()对象引用和条件 if()语句内的方法调用,将按键事件处理(左、右、上、下、w、s)与说出每个按键(AudioClip 对象 iSound0 到 iSound5)的音频文件相匹配,如图 15-20 所示,使用以下 Java 代码:
private void``playAudioClip()
if(invinciBagel.isLeft()) { invinciBagel.playiSound0(); }
if(invinciBagel.isRight()) { invinciBagel.playiSound1(); }
if(invinciBagel.isUp()) { invinciBagel.playiSound2(); }
if(invinciBagel.isDown()) { invinciBagel.playiSound3(); }
if(invinciBagel.iswKey()) { invinciBagel.playiSound4(); }
if(invinciBagel.issKey()) { invinciBagel.playiSound5(); }
}
图 15-20。
Add conditional if() statements to the .playAudioClip() method that call the correct .playiSound() method
现在,我们已经将数字音频添加到了我们的游戏引擎基础设施中,接下来我们所要做的就是将画外音替换为声音效果,我们将完成游戏开发中的数字音频部分。如果您使用运行➤项目工作流程并测试代码,您会发现使用 JavaFX 可以快速触发音频样本。
摘要
在第十五章中,我们将注意力从游戏在我们眼中的样子(视觉)转移到了它在我们耳中的声音(听觉),并花了一章来实现 AudioClip 类的代码,这样我们就可以触发数字音频音效。
首先,我们看一下 JavaFX AudioClip 类。我们了解了为什么它非常适合用于我们的音频游戏开发,包括短音乐循环(使用不定常数设置)或快速音效。
接下来,我们学习了如何使用 Audacity 2.0.6 优化数字音频资产。我们学习了优化数字音频的工作过程,以便只占用十几千字节的系统内存,以及如何优化音频,以至于我们甚至不必应用压缩,特别是因为 Java 8 支持的音频压缩编解码器是“有损”编解码器,一旦音频数据被解压缩到系统内存中,就会降低音频数据的质量。
最后,我们在 InvinciBagel.java 类中使用. loadAudioAssets()方法实现了 AudioClip 对象,然后创建了六个。playiSound()方法来允许外部类访问和播放这些数字音频资产。我们还在 Bagel.java 类中添加了一个. playAudioClip()方法,该方法根据按下的键触发音频样本。在下一章,我们将看看如何在我们的游戏代码中加入碰撞检测。
十六、碰撞检测:为游戏角色创建 SVG 多边形,并编写代码来检测碰撞
现在,我们已经为游戏音效和短循环音乐实现了数字音频,并且实现了创建运动精灵(角色)和固定精灵(道具)的数字图像相关类,我们现在将深入研究新媒体的另一个主要类型或领域:矢量。矢量用于 2D 插图软件(InkScape)以及 3D 建模和动画软件(Blender),并使用数学来定义用于创建 2D 或 3D 艺术品的形状。这使得 vectors 成为定义自定义碰撞形状的完美解决方案,它可以完美地包围我们的 sprite,因此我们不使用复杂的像素阵列来检测碰撞,而是使用更简单(内存和处理器效率更高)的碰撞多边形,它将完美地包围我们的 sprite。
幸运的是,javafx.scene.shape 包中的 JavaFX SVGPath 类允许我们使用自定义 SVG 路径(形状)数据来定义 sprite 碰撞边界。不仅如此,这个 SVGPath 类(object)也非常高效,因为它没有属性,只有几个方法和一个简单的 SVGPath()构造函数方法,正如你在第八章中已经看到的。这意味着使用 SVGPath 类(对象)相对来说是内存和处理器高效的。事实上,我们需要使用的唯一方法是。setContent()方法,我们在第八章中的 Actor 类构造函数方法中使用了它。因为我们将在游戏启动时做一次,SVG 路径碰撞数据将被加载到系统内存中,并将在我们的碰撞检测例程中使用,我们将在本章后面的部分中进行处理。
在本章中,我们将详细了解如何定义 SVG 路径数据。这是由万维网(W3)联盟(也称为 W3C)规定的,该组织定义了 HTML5。该规范在他们的 w3.org 网站上,位于 http://www.w3.org/TR/SVG/paths.html 网址,如果你想查看更多细节。
在我们详细了解了 SVG 或可缩放矢量图形(如果您想知道这代表什么)数据格式之后,我们将开始使用流行的 GIMP 2.8 数字图像编辑软件,您在第一章中安装了该软件,并学习如何创建碰撞多边形。我们还将了解如何使用 PhysEd (PhysicsEditor)碰撞多边形生成软件。这是来自一家名为 CodeAndWeb GmbH 的公司,该公司生产专业的、价格合理的游戏资产创建软件。
我们将使用 GIMP 2.8 完成碰撞多边形向量资源创建和优化过程,使用快速和脏工作过程,这允许 GIMP 100%为您创建碰撞多边形形状,以及一个更“复杂”的工作过程,其中您可以使用 GIMP 的路径工具手动创建自己的碰撞多边形。在我们学习了如何创建与 JavaFX SVGPath 类兼容的碰撞多边形之后,我们将在本章的剩余部分讨论 Java 8 游戏编程,创建碰撞检测引擎(代码),它将允许我们检测 InvinciBagel 角色何时与游戏环境中的任何其他演员对象(场景和舞台对象)接触。这是这本书开始变得更高级(有用)的地方。
SVG 数据格式:手工编码矢量形状
SVG 数据字符串中的数字(2D 空间中的 X,Y 数据点位置)数据可以使用十种不同的字母。每个版本都有大写(绝对参考)和小写(相对参考)版本。我们将使用绝对参考,因为我们需要这些数据点与 sprite 图像中的像素位置匹配,我们将“附加”或分组这些 SVG 路径数据字符串,以提供碰撞检测数据指南。正如您在表 16-1 中看到的,SVG 数据命令为您定义 Java 8 游戏开发的自定义曲线提供了很大的灵活性。您甚至可以将所有这些可扩展的矢量命令与您的 Java 8 代码相结合,以创建以前从未体验过的交互式矢量(数字插图)艺术作品,但由于这是一个游戏开发标题,我们将使用这些信息来开发高度优化的碰撞多边形,这些多边形仅使用十几个 X,Y 数据点(介于 12 和 15 之间)来定义一个相对详细的碰撞多边形,它将包含我们的精灵图像,并提供高度精确的(至少从游戏玩家的角度来看)碰撞结果。
表 16-1。
SVG data commands to use for creating SVG path data string (source: Worldwide Web Consortium w3.org)
| SVG 命令名 | 标志 | 类型 | 参数 | 描述 |
|---|---|---|---|---|
| 动起来了 | M | 绝对的 | x,Y | 使用绝对坐标在 X,Y 处定义路径的起点 |
| 动起来了 | m | 亲戚 | x,Y | 使用相对坐标在 X,Y 处定义路径的起点 |
| 帕尔帕思 | Z | 绝对的 | 没有人 | 通过从最后一个坐标到第一个坐标画一条线来闭合 SVG 路径 |
| 帕尔帕思 | z | 亲戚 | 没有人 | 通过从最后一个坐标到第一个坐标画一条线来闭合 SVG 路径 |
| 利托 | L | 绝对的 | x,Y | 从当前点到下一个坐标绘制一条线 |
| 利托 | l | 亲戚 | x,Y | 从当前点到下一个坐标绘制一条线 |
| 水平直线 | H | 绝对的 | X | 从当前点到下一个坐标绘制一条水平线 |
| 水平直线 | h | 亲戚 | X | 从当前点到下一个坐标绘制一条水平线 |
| 垂直线条 | V | 绝对的 | Y | 从当前点到下一个坐标绘制一条垂直线 |
| 垂直线条 | v | 亲戚 | Y | 从当前点到下一个坐标绘制一条垂直线 |
| 弯曲的 | C | 绝对的 | X,Y,X,Y,X | 从当前点到下一点绘制一条三次贝塞尔曲线 |
| 弯曲的 | c | 亲戚 | X,Y,X,Y,X | 从当前点到下一点绘制一条三次贝塞尔曲线 |
| 短平滑曲线 | S | 绝对的 | X,Y,X,Y | 从当前点到下一点绘制一条三次贝塞尔曲线 |
| 短平滑曲线 | s | 亲戚 | X,Y,X,Y | 从当前点到下一点绘制一条三次贝塞尔曲线 |
| 二次贝塞尔曲线 | Q | 绝对的 | X,Y,X,Y | 绘制二次贝塞尔曲线(当前点到下一点) |
| 二次贝塞尔曲线 | q | 亲戚 | X,Y,X,Y | 绘制二次贝塞尔曲线(当前点到下一点) |
| 短二次贝塞尔曲线 | T | 绝对的 | x,Y | 绘制一个短的二次贝塞尔曲线(当前点到下一点) |
| 短二次贝塞尔曲线 | t | 亲戚 | x,Y | 绘制一个短的二次贝塞尔曲线(当前点到下一点) |
| 椭圆弧 | A | 绝对的 | rX,rY,红色 | 从当前点到下一点绘制椭圆弧 |
| 椭圆弧 | a | 亲戚 | rX,rY,红色 | 从当前点到下一点绘制椭圆弧 |
了解如何使用强大的 SVG 数据“路径绘制”命令的最佳方式是开始学习创建基于 SVG 数据的碰撞多边形路径的工作过程。我们将学习如何使用 GIMP 2.8.14 实现这一点,使用一种“快速而简单”的方法,让 GIMP 完成 100%的路径创建工作。之后,我们将学习另一种使用 GIMP 手工完成这项工作的方法。第二种方法为您提供了 100%的路径创建控制。在这个 SVG 主题的最后,我还将向您展示如何使用另一个专用的碰撞和物理开发工具 PhysEd,它来自一家创新的游戏开发软件工具公司,位于德国乌尔姆,名为 CodeAndWeb GmbH。
创建和优化碰撞数据:使用 GIMP
对我们来说幸运的是,流行的开源数字图像编辑软件包 GIMP(目前版本为 2.8.14)具有足够的路径功能,并且能够将其导出为 SVG 数据集,从而允许该软件用作成熟的碰撞多边形创建工具。GIMP 软件使用路径工具支持路径(显示为带有贝塞尔手柄的曲线数据点旁边的老式钢笔笔尖),您可以在图 16-1 中看到(工具箱图标第二行中左起第二个图标)。GIMP 支持路径的原因是,它通常不是一个矢量(基于路径)软件包,而是一个光栅(基于像素)软件包,因为创建路径然后将其“转换”到选择区域的能力对于数字图像合成技术人员来说非常有用。事实上,我们将使用 GIMP 的能力来做与此相反的事情:也就是说,将算法创建的选择集转换成路径数据,这将形成我们完全自动化的、“快速而肮脏的”SVG 路径数据创建过程的基础。我们首先来看看这个,因为它快速、简单、有效(但是“数据量很大”)。让我们从启动 GIMP 开始,并使用文件➤打开过程来打开您的项目/src 文件夹中的 sprite1.png png 32 数字图像资产。正如你在图 16-1 中看到的,我已经放大了图像。这允许我看到我们将要使用 GIMP 工具箱创建的碰撞数据路径(最初,这将是一个选择)。
图 16-1。
Launch GIMP, and use File ➤ Open to open the sprite1.png file
点击 GIMP 模糊选择工具(图标看起来像火花或魔杖),它显示为选中状态(按下,如“陷入”,而不是悲伤),如图 16-2 。单击图像中显示棋盘图案的任何区域,这在任何数字成像软件包以及许多其他类型的软件(例如 CodeAndWeb GmbH 的游戏资产创建软件包)中总是表示透明的。正如你所看到的,在图 16-2 中,你会在图像的透明区域周围得到一个动画的“爬行的蚂蚁”轮廓,因为透明区域刚刚被模糊选择工具算法选中,该算法选择连续颜色值的区域。
图 16-2。
Click in a transparent area using Fuzzy Select Tool to select Actor
在这种情况下,模糊选择工具选择透明度值。事实上,您应该注意到,您必须选择一个选项来选择图像中的透明区域,使用模糊选择工具选项对话框,该对话框可以在工具箱工具选项浮动调色板的底部看到。正如你所看到的,我已经指示 GIMP 通过选择“选择透明区域”选项,不仅要“查看”图像中的 RGB 板,还要查看 Alpha 板。
现在我们有了包含除了我们想要选择的 sprite 字符之外的所有内容的选择集,我们需要找出一种方法来获得与我们现在选择的内容相反的内容。无可否认,选择透明区域要比选择无敌手角色的不同颜色区域容易得多!幸运的是 GIMP 有一个算法可以完全反转选择,选择所有没有被选中的,取消选中所有被选中的。
在 GIMP 2.8 选择菜单下,找到反转选项,或者使用 Control+I 键盘快捷键,显示在反转选项旁边的菜单上,所有这些都可以在图 16-3 的左半部分看到。一旦你这样做了,你会注意到动画“行进的蚂蚁”不再围绕数字图像的正方形周长(范围)行进,它们只围绕 InvinciBagel 字符,这意味着选择已经被反转,我们想要选择的现在包含在选择集中。
下一步是将此光栅像素选择集(数组)转换为矢量(路径)数据表示。我们在 GIMP 中这样做的方法是使用选择➤路径菜单序列,如图 16-3 的右半部分所示。这将把 InvinciBagel 字符周围的选择转换为矢量路径数据,这就是我们想要剔除的。
图 16-3。
Invert selection using Select ➤ Invert, so only Actor is selected (left); use Select ➤ To Path to convert to path
一旦你将像素选择转换成矢量路径,你将得到如图 16-4 所示的结果。
图 16-4。
Right-click on Selection Path in Paths Palette, and Export Path
在另一个浮动调色板中,点击路径选项卡,如图 16-4 右侧所示。GIMP 有两个主要的浮动工具窗口;一个是 GIMP 工具箱,包含工具、选项、画笔、图案和渐变,另一个包含四个选项卡,代表您的数字图像的合成层、通道、选择路径,甚至还有一个撤销缓冲区,它为您提供了自启动以来在 GIMP 中所做的每个“移动”的“历史记录”。
选择名为“选择”的路径(名为“选择”的路径图层将变为蓝色)。接下来右键单击名为 Selection 的路径,在您可以对选择路径进行操作的菜单底部,您会看到一个导出路径菜单选项。这是另一个关键的 GIMP 算法,使我们能够创建和输出碰撞多边形的工作过程。
选择这个导出路径选项将为我们导出当前 InvinciBagel 字符选择路径数据,作为包含 SVG 数据的基于文本的(XML)文件,这是我们需要在 Bagel()构造函数方法调用中使用的。该数据将包含在第一个字符串 SVGdata 参数中,并将替换我们到目前为止一直用作占位符的“虚拟数据”。
一旦你调用导出路径菜单选项,你会看到导出路径到 SVG 对话框,如图 16-5 所示。如你所见,我选择了对话框底部的“导出活动路径”选项,因为我只想要一个碰撞多边形路径数据对象,我将该文件命名为 sprite1svgdata.svg,并将其保存在我的 C:\ Clients \ BagelToons \ InvinciBagelGame \ Shape _ Data 文件夹中。
图 16-5。
Select “Export the active path” option, in the Export Path to SVG dialog, and name the file sprite1svgdata
工作流程的下一步是在文本编辑器中打开我们在图 16-5 中导出的 sprite1svgdata.svg 文件:对于 Windows 用户,这将是记事本,对于 Macintosh 用户,这将是文本编辑,对于 Linux 用户,这可能是 vi 或 vim。
图 16-6 显示了 Windows 记事本的文件打开对话框,你可能会注意到,默认情况下,记事本会查找。txt(文本文件类型)文件扩展名,表示该文件中有文本数据。但是,在。svg 文件扩展名(类型),以 XML 数据的形式。我们需要使用对话框右下角的下拉菜单来告诉记事本查看所有可用的文件,并允许我们决定哪些文件包含文本数据,哪些不包含文本数据。
一旦你选择了“所有文件”选项,你会看到 sprite1svgdata 文件,你可以双击它打开文件(或者单击它选中它,然后点击打开按钮)。
图 16-6。
Use a text editor (like Notepad) and select “All Files” option and Open sprite1svgdata
如果您想在 Java 代码中使用这些数据,只需选择引号中 d =(数据等于)后面的部分,包括引号,您需要用引号来表示一个字符串,如图 16-7 中蓝色部分所示。
图 16-7。
Select the SVG data (including quote characters) for the SVG Path representation, and use it in your code
如图 16-7 所示,有 32 乘以 3 的数据对,接近 100 个数据点,这是一个很大的数据处理量,尤其是当两个都定义了 SVGPath 碰撞检测数据的对象发生碰撞时!
如果你看一下图 16-4 中的 InvinciBagel 角色,我们真的应该能够使用 14 到 16 条线定义一个围绕角色的碰撞多边形,这些线完美地封装了角色,并且使用少很多倍(事实上,少 16 倍,正如你将在本章的下一节中看到的)的数据,这相当于少 16 倍的内存,少 16 倍的处理(1600%的效率)开销,如果不是更多的话。
因此,我想向您展示在 GIMP 中定义您自己的自定义碰撞多边形 SVG 数据形状对象的更复杂的工作过程,使用尽可能少的线条(数据点之间)。这实质上等同于碰撞检测 SVG 路径形状数据优化,因为我已经向您展示了其他新媒体元素的数据优化工作流程,现在没有理由停止这种趋势!所以接下来,让我们在本章的下一节详细看看如何将我们的碰撞检测 SVG 路径形状数据开销减少 1600%。
创建优化的碰撞多边形:使用路径工具
让我们重新开始,要么关闭前一个项目,并使用文件➤打开重新打开 sprite1.png 文件,或删除路径调板中的前一个选择路径。这一次,而不是模糊选择工具,使用路径凳子,并在工具箱的选项(底部)部分选择设计编辑模式,并选择多边形复选框选项。这将使我们的线条保持漂亮和笔直,就像你在 Blender 等 3D 建模包中看到的多边形一样。在 InvinciBagel 角色的头发中单击,然后在他的左肩上单击另一个点,如图 16-8 所示。这将自动为您在两点之间绘制一条线段(折线)。单击肘部的第三个点、手腕的第四个点、脚趾的第五个点、膝盖的第六个点、大腿的第七个点、脚后跟的第八个点,以此类推,使用直线创建一个轮廓,该轮廓完美地包含 InvinciBagel 运行状态。
图 16-8。
Open sprite1.png, select the Paths Tool, and start to draw a simple Path
正如你在图 16-9 中看到的,我只用了 15 个点创建了碰撞多边形。您可以让多边形保持开放,只需添加您在本书第一部分学到的 Z 字符,即可创建一个闭合的多边形。正如你在图 16-9 的右侧所看到的,多边形与精灵非常一致,所以在游戏过程中,碰撞的结果看起来像是发生在精灵的像素上,而不是碰撞多边形上,尽管碰撞多边形路径数据在 GIMP 中是可见的,但在游戏过程中是不可见的。
通过选择并右键单击图 16-9 中间显示的未命名路径,导出您的手绘路径,并使用导出路径菜单选项将其导出为文件 sprite1svghand.svg,就像您在图 16-5 中所做的一样。如果你想在 GIMP 中命名路径,你可以双击路径对话框中的路径名,如果你愿意,给它一个名字。如果您想在原生 GIMP 中保存您的工作。xcf 文件格式,您也可以使用文件➤保存菜单序列,并给文件命名,如 sprite1svgpath15points.xcf
图 16-9。
Insert 15 strategically placed points to define a collision shape and use the Export Path to export SVG data
接下来使用你的文本编辑器(比如记事本)的文件打开对话框,如图 16-6 所示,打开最新的 sprite1svghand.svg 文件,这样你就可以看到相对于 GIMP 模糊选择工具选择工作流程在本章第一节为我们提供的近 100 个数据点对,你保存了多少数据。
正如您在图 16-10 中看到的,有 14 乘以 3 (42)个数据对,这还不到我们在之前的工作流程中拥有的数据量的一半。这很奇怪,因为理论上应该只有 15 个数据对,所以让我们做一些调查工作,看看这些数据可能会发生什么。
正如 NetBeans 8 并不总是利用“最佳”或正确的工作流程来满足我们特定的 Java 8 游戏创作目标一样,我们也有可能在 GIMP 上遇到相同类型的问题。如果是这种情况,我们将不得不自己动手,并通过添加我们自己的定制工作流程步骤来进行干预,以实现我们知道的 Java 8 游戏开发所需的精确结果。幸运的是,这个 SVG 路径数据使用 XML“容器”中的文本数据,所以如果需要,我们应该能够将自己的步骤添加到这个工作流程中。最后,你会发现开发一个专业的 Java 8 游戏并不像玩游戏本身那么容易!
在对 SVG 数据(d=)进行更仔细的检查后,您首先会看到,所使用的数字精度水平对于这个应用来说是不必要的。因为我们试图将碰撞数据点精度与像素精度相匹配,所以我们可以使用整数,而不是用于 SVG 数据的浮点数。让我们自己动手,将这些浮点数的小数部分向上或向下舍入到最接近的整数。这样做将消除当前使用的浮点精度。这将是我们的第二轮优化(第一次是手绘多边形)。另外,在数据的最末端添加一个“Z”闭合路径命令,如图 16-10 所示,形成一个闭合的多边形。
图 16-10。
Open SVG Path data in a text editor and add a Z “close polygon” command to the end of the data
正如你在图 16-11 中看到的,第一轮优化会给你明显更少的数据,大大简化了碰撞数据。然而问题是,如果我们把这个 SVG 数据放回 GIMP 2.8,你的碰撞多边形看起来还会完全一样吗?接下来让我们仔细看看可以回答这个问题的工作流程。将该文件保存为 sprite1svghandintegerxml.txt,这样我们在需要时就有了优化的数据,如 16-11 所示。
图 16-11。
Remove the floating point values, by rounding them up or down, to the nearest integer values
在 GIMP 中优化 SVG 路径冲突形状:使用导入路径
让我们再次开始,要么关闭前一个项目,并使用文件➤打开重新打开 sprite1.png 文件,或删除路径调板中的前一个选择路径。正如你在图 16-12 中看到的,路径调色板是空的,我们可以在调色板的空白区域内右击,并选择底部的导入路径选项。
图 16-12。
Right-click in empty Paths palette, select Import Path option
一旦你点击图 16-12 中的导入路径菜单项,你将从 SVG 对话框中得到导入路径。选择“所有文件(。)"下拉菜单选项,然后点击 sprite1svghandintegerxml.txt 文件然后点击打开按钮,如图 16-13 所示。这将在 GIMP 中打开编辑过的整数碰撞路径数据。
图 16-13。
Open the sprite1svghandintegerxml.txt file
如图 16-14 所示,碰撞多边形 SVG 数据的整数表示与浮点表示相同,如图 16-9 所示。由于在 SVG 数据字符串的末尾添加了一个 SVG“Z”命令,碰撞多边形现在是闭合的。我们正在让我们的碰撞数据更加优化!
图 16-14。
Collision polygon is correct using integer data (left); deselect visibility in Layers Palette (right) to see SVG
您可能已经注意到,除了起点和终点,每个点都有三个相同的 X,Y 数据点坐标值,对于起点和终点,我们有数据点对而不是数据点三元组。如果你回想一下表 16-1 ,唯一使用三元组(三个)X,Y 数据点对的 SVG 命令是 C 或三次贝塞尔(样条)曲线。果然,如图 16-15 所示,在 M (moveto) opening SVG 命令的正下方是 C 命令。这解释了为什么 GIMP 在你的多边形的每个点上放三个数据点。所有数据点三元组具有相同值的原因是因为我们检查了 GIMP 中的多边形选项。这会将样条曲线控制手柄放在“远离”或看不见的地方,直接放在 X,Y 数据点的顶部。这定义了零曲率,或如图 16-14 所示的正方形多边形结构。
图 16-15。
Remove duplicate point data for those points in the interior of the collision polygon, to further optimize
让我们导出去掉了三元组的 SVG 数据,如图 16-15 所示,并使用我们新发现的导入路径工作流程,看看 GIMP 中的结果是什么样的,主要是为了学习,因为我们还没有完全完成优化工作流程。
将图 16-15 所示的 XML 数据保存为 sprite 1 svghanditegerxmlpromized . txt,然后使用图 16-12 和 6-13 所示的相同导入路径工作流程,将这个进一步优化的 SVG 数据集导入到 GIMP 中。正如你在图 16-16 中看到的,从数据集中移除那些三次贝塞尔曲线控制柄也移除了你的碰撞多边形的多边形性质。因此,我们需要对我们的 SVG 数据做一些进一步的工作来纠正这一点。
图 16-16。
Import the latest SVG data with data triplets deleted into GIMP to see curve without tension handle data
将这些不一致的(精灵轮廓的)曲线变成我们之前的相同碰撞多边形的解决方案非常简单,你可能已经猜到是什么了。因为我们希望数据点之间是直线,所以我们需要将这个“C”改为“l”。这将把 curveto SVG 命令变成 lineto SVG 命令。
正如你在图 16-17 中看到的,我们的碰撞多边形数据几乎在我们预期的地方,包含 16 个数据点对和一个 Z 闭合命令来创建一个 16 边碰撞多边形。我们可以删除第一个重复的数据对,将数据集减少到 15 个数据点对,这是我们在 GIMP 中“制定”的。接下来,让我们再次使用 GIMP 中的导入路径工作流程,看看我们是否得到了如图 16-14 所示的相同的正方形多边形结果。
图 16-17。
Change your SVG Path data from using the C (curve) data type to the L (line) data type representation
最终的 SVG 数据集如图 16-18 所示,您可以将该数据复制并粘贴到您的 Java 代码以及一个空的记事本文档中,并将其保存为自己的文件,名为 iBshape1svg.txt,这样我们接下来就可以做一些数学运算,看看我们从自动 GIMP 创建的碰撞多边形到我们手动优化的自定义碰撞多边形减少了多少数据。如果您在 GIMP 中导入图 16-18 所示的碰撞数据,您将得到图 16-14 所示的所需碰撞多边形,有 15 个数据点对,而不是 100!
图 16-18。
To use optimized SVG Path data set, select the part after d= (including quotes), and paste into the code
确定冲突数据优化:计算 SVG 数据的数据足迹
让我们算出我们的数据占用优化百分比。从图 16-18 所示的文件中挑选出来的 SVG 数据,并放入我们将在 Bagel()构造函数方法调用中使用的格式中,可以在图 16-19 的顶部看到。
图 16-19。
Open folder in your workstation containing the SVG data, and mouse-over file to see the number of bytes
将鼠标悬停在两个 iBshape1svg 文件上,如图 16-19 和 16-20 所示,并获取文件大小,或者右键单击文件,并使用属性对话框来查找文件中的字节数。这应该是 97 字节和 1605 字节。
图 16-20。
Mouse-over the original Fuzzy Select Tool generated SVG data to get the data footprint, and do the math
要找出数据占用优化,只需将这些数字相互除即可。如果你用 97 除以 1605 (97/1605=0.0604),你会发现 97 是 1605 的 6%,数据占用空间减少了 94%。如果将 1605 除以 97 (1605/97=16.546),则意味着文件减少了 16.546 倍,数据占用空间减少了 16.55 倍。这是计算器上彼此的倒数(1/x ),所以你可以从任何一个方向看。因此,1605 字节比 97 字节多 1,655%的数据,即 97 字节是 1605 字节的 6%(或少 94%的数据)。不管你怎么看,你已经为你的游戏节省了大量的内存、处理和 JAR 文件数据,而这仅仅是为了一个精灵!请记住,一旦您在游戏逻辑中实现了碰撞检测,优化您的碰撞多边形以使用少于 16 倍的内存和少于 16 倍的 CPU 处理开销对您的游戏流畅度非常重要,我们将在查看 CodeAndWeb 的 PhysEd 工具后进行这一操作。
创建和优化物理数据:使用 PhysEd
我想用几页纸向您展示 GIMP 的一种替代方案,它将物理和碰撞合并到一个统一的游戏开发工具中,相对于它为游戏开发所做的一切来说,它是非常便宜的。PhysicsEditor 或 PhysEd(或 PE)来自 CodeAndWeb GmbH,该公司由另一位致力于 iOS 游戏开发的作家 Andreas Loew 所有。让我们快速看一下如何使用这个专业的游戏开发工具来定义精灵的碰撞多边形,然后我们就可以开始碰撞检测编码了。使用绿色立方体 PE 图标安装并启动 PE,使用图 16-21 所示的导入精灵按钮打开你的 sprite1.png 文件,使用屏幕底部的缩放滑块放大 600%,就像我们在 GIMP 中做的一样。
图 16-21。
Launch PhysicsEditor and use the Add Sprites button to open the sprite1.png file and zoom into it 600%
接下来点击精灵编辑区域顶部的追踪器图标(它是中间的图标,看起来像魔棒),这将打开追踪器对话框,如图 16-22 所示。在此对话框中,您可以设置缩放、追踪算法容差、追踪模式和帧模式,并查看作为这些设置结果的顶点数。
图 16-22。
Using the Tracer utility in PhysEd to set the Tolerance, Trace mode, Frame mode and Vertices
一旦你得到了你想要的视觉效果,这是通过调整各种跟踪对话框设置来实现的,点击 OK 按钮,你将回到 PhysicsEditor 主用户界面窗口。
然后,你可以通过用鼠标点击并拖动这些点来逐个数据点地细化你的碰撞多边形结构数据点,如图 16-23 所示。如果你比较图 16-22 中的碰撞多边形和图 16-23 中的碰撞多边形,你可以看到我已经优化了几个数据点,以更好地符合精灵的轮廓。
碰撞多边形离精灵的轮廓越近,你最好把它放在反走样的上面,反走样位于精灵像素和透明度之间的像素边缘,或者如果你想玩一个更有挑战性的游戏,甚至可以放在反走样的里面。最终,你的碰撞多边形将不得不在你的游戏开发和游戏测试周期中被调整,这样你才能在游戏中得到最真实的结果。如果您正在为强大的平台(如游戏控制台)进行开发,您可以向碰撞多边形添加数据点,如果您正在为单处理器或双处理器平台(如 HTML5 手机或 ITV)进行开发,请使用较少的数据。
图 16-23。
Fine-tune your vertex placement for the collision polygon in the PhysicsEditor, then select your Exporter
替换虚拟碰撞数据:InvinciBagel.java
接下来,让我们对 InvinciBagel.java 类中的 Java 代码做一些修改,这样你就可以真正使用我们在本章中开发的高度优化的碰撞多边形数据。我们将使用只有 15 个数据点的数据集,这样我们就可以在 NetBeans 8.0 代码编辑窗格中看到所有的碰撞多边形数据。正如你在图 16-24 中看到的,我们将需要把 Bagel()构造器方法调用放在三个不同的行上:一个用于实例化的iBagel = new Bagel(this)部分,另一个用于碰撞多边形 SVG 数据字符串对象,另一个用于 xLocation,yLocation 和 Image 对象列表。我们还将使用我们从表 16-1 中学到的 SVG 命令知识,为我们的可平铺砖块元素创建碰撞多边形数据。在本章的后面,我们将使用这些砖块“道具”元素来开发我们的碰撞检测 Java 代码。我们将仅使用我们的大脑来创建这些更简单的碰撞多边形,通过实现与砖块图像的分辨率相关的数字逻辑,以及角像素位置在 X,Y 坐标空间中的位置,结合我们在表 16-1 中了解到的 SVG 路径绘制命令。你正在成为一名相当专业的游戏开发人员!
我们还将使用我们在第三章学到的 Java 代码注释技术来(暂时)从场景中移除较大的长满苔藓的岩石道具,并将我们的代码放在舞台上有 InvinciBagel 角色和几块砖块的地方。然后,我们可以使用这些基本对象来开始我们的代码开发。collide()方法以及它将如何使用 castDirector CastingDirector 类(object)作为冲突处理指南。
如果您希望看到 iPR1 和 iPV1 对象暂时从中删除。createGameActors()以及您的。addGameActorNodes()和。createCastingDirection()方法,您可以在图 16-25 和 16-26 中看到这段 Java 代码的注释。接下来,让我们复制并粘贴您在本章前面创建的 SVG 碰撞多边形数据集。打开 iBshape1svg.txt 文件,如图 16-20 所示,使用编辑➤复制菜单序列或 CTRL-C 快捷键选择并复制 svg 数据。一定要包括引号。将 Bagel()方法中的数据粘贴到。createGameActors()方法,虚拟 SVG 数据曾经在这里,如图 16-24 所示。
图 16-24。
Copy and paste your 15 data point collision polygon SVG data in place of dummy data in Bagel() method
更新 iBagel Bagel()构造函数方法后,使用碰撞多边形的准确 SVG 数据,更新 iP0 固定演员道具,使用四个角像素的坐标,创建一个方形碰撞多边形,如图 16-25 所示。任何图像的左上角原点都将是 0,0,所以第一个 SVG 命令将是 M0,0,或“移动到原点”接下来,我们想使用 L 命令画一个“lineto ”,左下角,它将使用 L0,32 命令和数据集,因为这个砖块图像是 32 像素高(Y ), 72 像素宽(X)。
图 16-25。
Create collision polygon SVG data for the iP0 fixed Actor props using a prop0.png image 32´72 resolution
下一个数据对不需要以 L 命令开头,因为如果没有明确指定,SVG 数据解析算法的任何实现都将假定前一个数据对使用的命令。砖块图像的右下角将使用 X,Y 坐标 72,32。该图像的右上角将使用 X,Y 坐标 72,0。Z 命令可以用来连接这个道具图像的右上角和原点,这样我们就可以在砖块的顶部进行碰撞检测,在这个特定的用例中,使用一个闭合的多边形。正如您在图 16-25 中看到的,您可以在 SVG 数据中使用逗号或空格,因此这两种方法调用都应该有效:
iPR0 = new Prop("``M0 0 L0 32 72 32 72 0 Z
OR:
iPR0 = new Prop("``M0,0 L0,32 72,32 72,0 Z
第二个示例在 NetBeans 8 中使用了相同的空间,并且更好地显示了数据点对。我将注释掉与 iP1 演员道具对象相关的所有代码,如图 16-25 和 16-26 所示,以便这些较大的道具演员对象现在被“禁用”,不会出现在舞台上(和场景中),并且不会干扰我们实现碰撞检测的基本代码开发。
图 16-26。
Comment out code related to iP1 object in addGameActorNodes() and createCastingDirection() methods
现在,我们准备使用“运行➤项目”工作流程,并确保 invincibagel 角色以及我们将用于测试碰撞检测代码的四块金砖都已就位,并且大型苔藓岩石对象不再出现在舞台上的任何地方。正如你在图 16-27 中看到的,我们已经设置好了开发基本碰撞检测代码的场景,现在我们可以专注于将 Java 8 游戏代码放在适当的位置,然后我们开始实现进一步的场景设计、游戏设计、游戏逻辑、物理和评分引擎代码,所有这些我们将在下一个第十七章中实现。
图 16-27。
Use the Run ➤ Project work process and test the game
在我们切换到 Bagel.java 类来实现冲突检测之前,我们需要对 InvinciBagel.java 类做一两个基本的修改。由于我们将在冲突代码中使用 CastingDirector 对象,我们必须删除CastingDirector castDirector;声明中的私有访问控制关键字,如图 16-28 所示。我们正试图使 InvinciBagel 类中的尽可能多的变量保持私有,以后如果我们需要从另一个类(比如 Bagel.java)访问它们,就让它们受到包保护。
图 16-28。
Remove the private access control modifier keyword for CastingDirector castDirector object declaration
接下来我们要做的是从。createCastingDirection()方法中的 addCurrentCast()方法调用参数列表。我们这样做是为了让 InvinciBagel(我们将检查它与其余 Actor 对象的冲突,这些对象包含在 CURRENT_CAST List 对象中)不检查它自身的冲突!新的更短的方法调用(减去 iBagel 对象)应该看起来像下面的 Java 代码,在图 16-29 的底部高亮显示:
castDirector.addCurrentCast(``iPR0, iPH0, iPV0, iPB0
图 16-29。
Remove the iBagel object from the front of the list of objects passed into the .addCurrentCast() method
现在,我们准备开始在 Bagel.java 类中放置方法,该类将通过检查每个对象的节点(ImageView)和碰撞形状(SVGPath)的边界来检查 iBagel 对象与 CURRENT_CAST List 对象中的对象的碰撞。我们接下来要讨论的内容可能会有些复杂,但是如果你真的想创建一个合法的游戏标题,你需要能够知道游戏元素何时相互交叉(在游戏开发术语中称为碰撞)。让我们现在就潜入那个深渊吧!
Bagel 类冲突检测:。检查冲突( )
在图 16-29 所示的项目窗格中右键单击 Bagel.java 类,并选择打开选项,在 NetBeans 8 中打开 Bagel.java 选项卡。走进去。update()方法,并在代码行的开头使用两个正斜杠注释掉 playAudioClip()方法调用。这将有效地关闭我们现有的按键事件(KeyCode)按键持续呼叫的音频。我这样做的原因是因为我将使用这个音频来“指出”我们将要创建的代码中检测到的冲突。接下来,在方法的末尾创建一个. checkCollision()方法调用。update()方法。这个方法将调用碰撞检测 Java 代码,我们将在本章的剩余部分中对每个被处理的脉冲事件进行处理。这与。update()方法。
新的。图 16-30 中所示的 update()方法应该类似于下面的 Java 方法体:
public void``update
setXYLocation();
setBoundaries();
setImageState();
moveInvinciBagel();
// playAudioClip();
checkCollision();
}
图 16-30。
Comment out the playAudioClip() method call to turn off all audio and add a checkCollision() method call
创建一个空的public void checkCollision(){}方法体,这将移除你在图 16-30 中看到的红色波浪错误高亮,这样我们就可以开始编写代码来查看当前演员列表<演员>对象,这样我们就可以确定是否有任何演员与游戏舞台上的主要无敌角色相交(发生冲突)。
在这个里面。checkCollision()方法,我们将需要一个 Java for“counter”循环结构。该结构将用于遍历 CURRENT_CAST List 对象数组;在这种情况下,这些是道具对象,用于检查与 InvinciBagel 对象的任何碰撞(相交)。for 循环的第一部分是迭代器变量的声明和初始化,在我们的例子中,int i=0;声明名为“I”的整数类型迭代器变量,初始化为一个零计数值。
for 循环的第二部分是迭代条件,在本例中是“迭代直到到达 CURRENT_CAST List 对象的末尾,或者到达代表列表大小(因此是列表中的最后一项)的i<invinciBagel.castDirector.getCurrentCast().size()。for 循环语句条件的第三部分是要迭代的数量,由于我们想要遍历列表中的每个对象< Actor >来检查冲突,我们将在“I”变量或i++上使用熟悉的 Java ++操作符。for 循环{…}花括号内是我们希望在循环的每次迭代中执行的内容;在这种情况下,这将用于列表<演员>对象中的每个道具演员对象。这是一个很好的构造,可以用来从第一个元素到最后一个元素迭代任何列表对象。
在 for 循环内部,我们将创建一个“本地”Actor 对象引用变量,我们将使用invinciBagel.castDirector.getCurrentCast().get(i)方法链将它设置为等于 List(i)中的当前元素。Java 方法链是创建紧凑的 Java 代码结构的一种很酷的方式,不是吗?一旦我们用 CURRENT_CAST(i) cast 成员对象加载了该 Actor 对象,我们将调用。collide()方法,使用 collide(object);Java 语句。记住我们安装了这个抽象。collide()方法,所以在我们了解了更多关于 Node 和 Shape 类的知识,以及它们的一些可用于确定交集(冲突)的关键方法和属性之后,接下来我们必须“覆盖”并编写该方法。
public void checkCollision() {
for(``int i=0``; i``<``invinciBagel.castDirector.getCurrentCast``().size()
Actor``object``= invinciBagel.castDirector.getCurrentCast()``.get(i)
collide (object);
}
}
这是确定冲突的第一个(简单的)部分,在第一个 checkCollision()方法中,从。update()方法,由脉冲事件管理器执行。它遍历列表 CURRENT_CAST,并为每个 Actor 对象调用collide(object);方法,以查看 InvinciBagel 角色是否与它发生了冲突。正如你在图 16-31 中看到的,这个 Java 代码是无错的,我们将要编码的下一个 Java 方法体是public boolean collide()方法,我们之前在第八章中在我们的抽象 Hero 类中声明使用它。
图 16-31。
添加一个 for 循环,使用从 0 计数到 CURRENT_CAST 中的对象数。getCurrentCast()。size()
在我们编写。collide()方法,这是我们将在整本书中编写的较难的方法体之一,我们需要了解一些与 javafx.scene 包的节点类及其 Bounds 属性有关的更复杂的主题。getBoundsInLocal()和。getBoundsInparent()方法调用。我们还将查看 javafx.scene.shape 包的 shape 类及其。intersect(Shape shape1,Shape shape2)方法调用。我们将在我们的public boolean collide(Actor object) {...}方法中使用所有这些,所以在我们编写这个复杂而密集(但令人兴奋)的 Java 结构之前,我们需要先掌握这些高级知识。
定位节点对象:使用 Bounds 对象
关于碰撞检测,我们需要考虑的第一件事是 javafx.geometry 包的 Bounds 类。这个公共抽象类(及其创建的对象)在 javafx.scene 包的节点类中用于保存节点的边界。正如你可能已经猜到的,这是我们将利用来确定碰撞检测的事情之一,结合我们的碰撞 SVGPath 形状数据,我们将在我们看一下如何限制对象及其相关对象后进入。getBoundsInLocal()和。getBoundsInParent()方法,可以为我们工作。
这个 Bounds 类是使用 java.lang.Object 主类从头开始创建的,包含 X、Y 和 Z 坐标,以及宽度、高度和深度值。由于我们将在 2D 工作,我们将使用 X 和 Y,以及宽度和高度,Bounds 对象的值(属性)。Bounds 类有一个直接已知的子类,称为 BoundingBox。如果您想知道,“直接子类”意味着 BoundingBox 类声明说 BoundingBox 扩展了边界,而“已知”类是已经正式添加到 Java 8 JDK 中的类。
“未知”类的一个例子是你自己定制的边界子类,如果你要写一个的话。javafx.geometry.Bounds 类的 Java 8 类层次结构如下所示:
java.lang.Object
> javafx.geometry. Bounds
Bounds 类用于创建 Bounds 对象。这些用于描述节点对象的边界,我们知道这是 JavaFX 场景图节点对象。Bounds 对象的一个重要特征是它可以有负的宽度、高度或深度。任何 Bounds 对象属性(properties)的负值用于指示 Bounds 对象为空。我们将在后面的代码中使用它来确定何时没有发生冲突。正如我在本书前面指出的,有时你必须采取“相反”的方法来寻找解决方案,或者正确的工作过程,以实现你的游戏设计和编程目标。
使用节点局部边界。getBoundsInLocal()方法
JavaFX 场景图节点类中的一个重要方法是公共最终边界。getBoundsInLocal()方法。此方法是 getter 方法,用于检索 boundsInLocal 属性的值。boundsInLocal 属性是“只读”的,它为包含它的节点保存一个矩形 Bounds 对象。它包含的数据表示节点的未变换(原始)局部坐标空间。“未变换”表示旋转、平移或缩放之前的节点坐标,表示节点对象的原始(默认)坐标。
对于扩展 Shape 类的节点类(对象)(我们的 ImageView 节点没有扩展),局部边界还将包括实现非零形状(或路径)笔划所需的空间,因为这可能会扩展到形状几何图形的“外部”,这是由这些位置和大小属性定义的。局部边界对象还包括您可能已经设置的任何剪辑路径区域,以及您可能已经设置的任何特殊效果的范围。
boundsInLocal 属性将始终具有非空值,需要注意的是,该方法不考虑节点对象的可见性,因此计算仅基于节点的几何形状。每当节点对象的几何体发生变化时,都会自动计算 boundsInLocal 属性。
我们将使用。getBoundsInLocal()方法在 InvinciBagel SVGPath 形状数据与另一个场景演员 Prop SVGPath 碰撞形状数据的交叉点上执行,以确定交叉点的宽度是否为负一,边界对象中的-1 是否为空,或者没有交叉点,这表示没有碰撞。接下来,让我们看看 boundsInParent 属性,它包含 boundsInLocal 数据和转换。
使用节点父边界。getBoundsInParent()方法
JavaFX 场景图节点类中需要理解的另一个重要方法是公共最终边界。getBoundsInParent()方法。此方法是检索 boundsInParent 属性值的 getter 方法。boundsInParent 属性是一个“只读”属性,用于保存包含它的节点的矩形边界对象。它包含的数据表示节点的变换(修改)坐标空间。“Transformed”表示节点坐标加上自节点对象的默认、初始或原始状态以来发生的任何变换。它被命名为“boundsInParent ”,因为边界对象矩形数据需要相对于父节点对象的坐标系。这表示节点对象的“可视”边界,正如您在屏幕上看到的,在节点被移动、旋转、缩放、倾斜等之后。
boundsInParent 属性是通过获取由 boundsInLocal 属性定义的局部界限,并应用所有已发生的转换(包括对。为以下节点属性设置()方法:scaleX、scaleY、rotate、layoutX、layoutY、translateX、translateY 和 transforms(Java observable list)。就像 boundsInLocal 属性一样,该 boundsInParent 属性将始终包含一个非空值。
结果 Bounds 对象将包含在节点对象的父对象的坐标空间内,但是,为了能够计算 boundsInParent 属性,节点对象不需要有父对象(它可以是场景的场景图形根)。就像。getBoundsInLocal()方法,该方法不考虑节点对象的可见性,所以不要错误地认为可以通过“隐藏”Actor 节点来规避冲突检测代码,在我们的例子中,Actor 节点是一个名为 spriteFrame 的 ImageView 节点子类。
由于它会计算对 boundsInLocal 属性的更改,因此只要节点的几何图形发生更改,或者该节点对象发生任何变换,就会计算 boundsInParent 属性,这是合乎逻辑的。正因为如此,基于依赖于该变量的表达式来计算节点中的任何这些值都是错误的。例如,Node 对象的 X 或 Y 变量或 translateX、translateY 不应使用此 boundsInParent 属性来计算以定位节点,因为先前的定位数据包含在此属性中,因此会创建循环引用(有点类似于臭名昭著的无限循环场景)。
使用节点交集。相交(限制对象)方法
JavaFX 场景图节点类中涉及碰撞检测的另一个重要方法是 public boolean intersects(Bounds local Bounds)方法。如果 Bounds 对象与使用参数传递到方法中的 Bounds 对象相交,则此方法将返回 true 值,该 Bounds 对象指定此方法正在调用的节点对象的本地坐标空间,该 Bounds 对象是您尝试确定与之相交的节点的 Bounds 对象。例如,为了确定包含 InvinciBagel 子画面的 ImageView 边界是否与包含 Prop 子画面之一的 ImageView 边界相交,我们将使用以下 Java 代码格式:
iBagel.spriteFrame.getBoundsInParent()``.intersects
需要注意的是,就像。getBoundsInLocal()和。getBoundsInParent()方法,该方法也不考虑节点对象的可见性属性。出于这个原因,相交测试将只基于讨论中的节点对象的几何图形,以及我们的 ImageView spriteFrame 节点对象的几何图形,这将是它们的方形(InvinciBagel)或矩形(Prop)图像容器的几何图形(尺寸)。Node 类 intersects(Bounds localBounds)函数的默认行为是检查并查看传入此方法调用的 Bounds 对象的本地坐标是否与。intersects()方法调用正在被“关闭”接下来,让我们仔细看看 Shape 超类,以及它的。intersect()方法。
使用形状类相交。intersect()方法
就像一个节点类(对象)有一个交集方法叫做。intersects()用于确定节点对象何时相交,Shape 类也有一个 intersection 方法,可用于 Shape 对象相交。这个方法被称为。intersect()(而不是 intersect),因此这些方法使用不同的命名约定。这样做可能是为了让开发人员不会混淆 Node.intersects()和 Shape.intersect()方法。我们将在本章后面的碰撞检测代码中使用这两种方法——首先检测一个 ImageView 节点何时与另一个 ImageView 节点重叠,然后在我们的 SVG 数据碰撞多边形(每个 Actor 对象中名为 spriteBounds 的 SVGPath 对象)之间执行布尔相交操作。
Shape 类的静态方法格式。intersect()方法在其参数列表中接受两个 Shape 对象,该方法返回一个新的 Shape 对象。创建的新 Shape 对象表示两个输入形状的交集。正如您所想象的,如果您使用 Shape 类及其子类来实现这一目的,您也可以使用这种方法为您的矢量作品生成布尔交集。该方法的格式是static Shape intersect(Shape shape1, Shape shape2),我们将使用它的方式如下所示:
Shape``shape``= SVGPath.``intersect``(invinciBagel.``iBagel``.getSpriteBound(),``object``.getSpriteBound());
Shape 类之所以。intersect()方法对我们的碰撞检测应用如此有效是因为我们以后可以使用. getBoundsInLocal()。getWidth()方法链,通过查找-1 空值来确定是否发生了冲突,或者更确切地说,是否没有发生冲突。我们将调用这个方法链来寻找这个新 Shape 对象的-1,我们将在。collide(object)方法,使用下面一行代码:
if (intersection.``getBoundsInLocal().getWidth() != -1``) { collisionDetect =``true
这种方式。intersect()方法的工作原理是,它只处理占据输入形状的几何数据,在我们的应用中,它是 Actor 对象内部的 spriteBound SVGPath 对象。这就是为什么我们使用 SVGPath 形状对象(用于最大碰撞框架或多边形定义),纯粹是为了它的几何数据值,也是为什么我们在我们的 Actor 对象设计中安装了这个 spriteBounds SVGPath 形状对象容器,因为我们打算将这些几何数据用于我们的碰撞检测,而不是用于矢量插图艺术作品(例如,我们没有抚摸或填充 SVGPath,请不要开玩笑,我们现在正在学习游戏开发)。
这意味着中的算法所考虑的输入形状对象的几何区域。intersect()方法仅“基于数学”。这意味着算法独立于正在处理的形状(子类)的类型,也独立于用于填充或描边的 Paint 对象的配置(为了学习 Java 8 游戏开发,请再次暂时抵制窃笑的诱惑)。
在最终相交计算之前,输入形状对象的区域被转换到父节点对象的坐标空间(想想:boundsInParent)。这样,生成的形状将只包括那些包含在使用参数表传递给该方法的两个输入形状对象的区域中的区域(更准确地说是数学几何区域)。因此,如果在生成的 Shape 对象中有任何数据(除了-1 空数据值之外的数据),我们将在接下来编写的代码中称之为 intersection,那么就存在某种程度的交集,甚至一小块交集区域(重叠)也需要发出检测到碰撞的信号。
现在,我们已经准备好使用我们在本章过去几页中学到的技术 Java 8 类和方法信息,只用大约十几行 Java 代码来创建核心冲突检测逻辑。我们将在本章结束前添加另外十几行代码,来处理我们游戏中的碰撞。
重写抽象的 Hero 类:。collide()方法
最后,覆盖和实现公共布尔 collide (Actor object)方法的时候到了,我们在第八章的抽象 Hero 类中安装了这个方法。这是我们玩游戏的一个关键方法,因为它决定了我们的主要无敌角色何时与游戏中的其他元素接触。这种接触的结果是得分,以及游戏屏幕上视觉上的变化,所以在下一章中,我们将在 InvinciBagel 游戏中实现游戏元素,这是非常关键的。我们要做的第一件事是安装一个名为 collisionDetect 的布尔“flag”变量,并在。collide()方法。用于设置 collisionDetect 标志的该语句的 Java 代码应如下所示:
boolean``collisionDetect
确定是否发生冲突的下一步是使用 Java 条件 if()语句。这允许我们使用 Node 类测试包含 sprite 图像资产的 ImageView 节点对象是否相交。intersects()方法调用与。getBoundsInParent()方法调用每个 spriteFrame ImageView 节点对象。第一个 spriteFrame.getBoundsInParent()方法调用是我们方法链接的调用。intersects()方法调用 off,因为我们试图做的是确定与我们的主要游戏角色的冲突。因为我们想要引用 InvinciBagel 对象(invinciBagel 类)及其 iBagel Actor 对象,所以这个构造将采用 invinciBagel . iBagel . sprite frame . getboundsinparent()的形式。intersects()方法调用结构。因为我们测试冲突的 Actor 对象的 Bounds 对象需要在。intersects(Bounds localBounds)方法调用,我们需要使用。getSpriteFrame()方法调用我们在第八章开发的。从我们传递到。collide()方法我们将方法链接到。getBoundsInParent()方法调用,产生 object.getSpriteFrame()。getBoundsInParent()结构,然后将它放在。intersect()方法调用。完整的条件 if()结构如下所示:
if ( invinciBagel. iBagel .spriteFrame.getBoundsInParent(). intersects
object .getSpriteFrame().getBoundsInParent() ) ){second level of collision detection in here}
第一级冲突检测代码将检查 iBagel spriteFrame ImageView 节点与 spriteFrame ImageView 节点对象的交集,该对象包含在将传递到此的每个 Actor 对象中。我们当前正在编写的 collide(Actor object)方法调用。
在我们调用更“昂贵”的形状相交算法之前,这部分代码将检查“顶级”ImageView 节点邻近性(碰撞),我们接下来将实现该算法,以便确认更确定的碰撞发生(SVGPath 碰撞多边形相交)。请记住我们放入其中的代码。对于调用 collide(Actor 对象)方法的 for 循环的每次迭代(对于每个角色成员),都将处理 collide()方法。
第一个条件 if()语句中的 Java 代码创建一个名为 intersection 的 Shape 对象,并将其设置为 SVGPath.intersect()方法调用的结果,该调用引用 iBagel SVGPath Shape 对象和传递到 collide(Actor 对象)方法中的 SVGPath Shape 对象。这两个对象都调用一个. getSpriteBound()方法,我们在第八章的中创建了这个方法,来访问。intersect(Shape1,Shape2)方法调用格式。为了便于阅读,使用两行代码格式化的 Java 代码应该如下所示:
Shape intersection =``SVGPath.intersect``( invinciBagel.``iBagel
object .getSpriteBound() );
有了这个相交数据后,我们将使用另一个条件 if()语句来查看该相交形状对象是否包含任何碰撞数据,如果包含(也就是说,如果它不包含-1 值),则表示发生了碰撞。
第二个嵌套的 if()语句将利用。getBoundsInLocal()。getWidth()方法链,从交叉点形状对象调用,将检查它是否为空(返回-1 值),或者是否发生了冲突。如果相交形状对象的 Bounds 对象包含除-1 之外的任何数据值,将发生冲突检测。在 if()语句体中,如果存在任何数据,collisionDetect 布尔标志将设置为 true(用!= -1).条件 if()语句的 Java 代码应该如下所示:
if(intersection.``getBoundsInLocal().getWidth()``!=``-1``) {``collisionDetect = true;
为了测试冲突代码,我将 invinciBagel.playiSound0()方法调用放在 if(collisionDetect){}条件语句中。这就是为什么我注释掉了?中的 playAudioClip()方法调用。update()方法,这样在碰撞过程中我唯一能听到的就是音频回放。这是测试碰撞代码的一种快速、简单、有效的方法,至少目前是这样。由于这是公共布尔 collide(Actor object)方法,我还将在 if()体的末尾放置一行代码return true;,它从方法调用中返回一个 true 值。我将语句放在这个条件 if()结构中,这样,如果需要,我们可以使用从。collide()方法调用。update()方法进行其他处理,如果我们想的话。这件事的本质。collide()方法的目的是检测是否发生了冲突,然后返回一个值,这样我们就可以在。collide()方法,或者更有效地在。checkCollision()方法,使用一个if(collide(object)=true){invinciBagel.playiSound0();}构造,而不是我们在这里用。collide()方法。我这样做的原因是,至少现在是这样,因为我正在测试这个。collide()方法,所以我把允许我测试。collide()方法。collide()方法,因为这是我现在在 ide 中工作的地方。Java 代码如下所示:
if(``collisionDetect
invinciBagel.playiSound0();
return``true
}
return false;
完整的。在我们开始操作 CastingDirector 类和对象列表对象之前,collide(Actor object)方法结构应该如下所示,这也可以在图 16-32 : 中看到
@Override
public boolean collide(Actor``object
boolean``collisionDetect
if ( invinciBagel.iBagel.spriteFrame.``getBoundsInParent()``.``intersects
object``.getSpriteFrame().``getBoundsInParent()
Shape``intersection
SVGPath.``intersect``(invinciBagel.iBagel.getSpriteBound(),``object
if (intersection.``getBoundsInLocal().getWidth()``!=``-1
collisionDetect``=``true
}
}
if(``collisionDetect
invinciBagel.playiSound0();
return true ;
}
return false ;
}
图 16-32。
Creating the basic collision detection code and testing it using the invinciBagel.playiSound0() method call
现在我们准备添加管理 CastingDirector 对象的 Java 代码,以删除 Actor 对象。
如果检测到碰撞:操纵 CastingDirector 对象
在. playiSound0()方法调用下添加一行代码,访问 CastingDirector 对象 invinciBagel.castDirector. Next 键入一个句点键,访问一个方法帮助器弹出窗口,如图 16-33 所示,并选择。addToRemovedActors()。
图 16-33。
Add a line of code under the .playiSound0() method call, and type invinciBagel.castDirector, and a period
双击。addToRemoveActors(Actor… actors)选项在这个弹出的帮助器对话框中,你需要使用圆括号内的参数列表区将 Actor 对象,恰当命名的 object,传递到方法调用中,如图 16-34 底部突出显示的。这将把这个刚刚卷入碰撞的演员对象(幸运的是,没有人受伤)添加到 REMOVED_ACTORS HashSet <演员>对象中。
图 16-34。
Add Actor object that was passed into the .collide() method to parameter list of addToRemovedActors()
我们不仅需要使用 CastingDirector 类(object)从游戏角色中删除这个冲突的 Actor 对象,我们还需要从场景图形根中删除 ImageView 节点。接下来让我们看看如何去做,然后我们将看看如何从 CURRENT_CAST 列表中删除 Actor 对象。
从场景图中删除演员:。getChildren()。移除()
现在,我们已经通过将 InvinciBagel 与之冲突的演员添加到 REMOVED_ACTORS HashSet 对象中,有效地将其从角色转换中移除,下一步是将它从 JavaFX 场景图中移除,我们当前使用 StackPane UI 布局容器类来移除它。如果您还记得在本书的前面,我们了解到 StackPane 类可以用作基于图层的布局容器,并使用居中的 XY 网格排列其内容。在本章的后面,我们将学习如何使用一个组对象(类)作为我们的场景图根,来实现更典型的左上角 0,0 XY 位置索引方案,并作为一种优化,因为我们没有特别使用任何 StackPane 类属性或方法,并且因为组类是一个更基本的类,在节点超类层次结构中处于更高的位置。通过这种方式,您将体验到使用中心屏幕 0,0 位置引用(StackPane),以及传统的左上角 0,0 屏幕位置引用(作为场景图形根的组类)。
使用. getChildren()从场景图形根中移除节点对象。中的. getChildren.add()方法调用方式完全相反。addGameActorNodes()方法。添加一行代码,键入 invinciBagel.root 和句点,如图 16-35 所示,打开方法帮助器弹出。
图 16-35。
Add a line of code under an .addToRemovedActors() method call and type invinciBagel.root and a period
因为我们是从 Bagel.java 类中删除 ImageView 节点,所以在添加。getChildren()方法调用,使用下面的 invinciBagel.root.getChildren()代码如图 16-35 所示。双击 remove(Object o)选择并将您传递的对象添加到。collide()方法放入。移除()方法,如图 16-36 所示。这将创建最终的invinciBagel.root.getChildren().remove(object); Java 编程语句。
图 16-36。
Investigate red error highlight under the Scene Graph root StackPane object reference in the method call
将鼠标悬停在突出显示的警告上,您将看到 NetBeans 看到我们正在将自定义 Actor 对象传递给。getChildren()。remove()方法链,而不是 ImageView 节点对象。错误消息还告诉我们,InvinciBagel.java 类中的private StackPane root;声明不允许我们访问这个对象,除非我们删除这个私有访问控制修饰符关键字。让我们首先修复最严重的(错误)问题,然后通过使用 object.getSpriteFrame()方法调用修复“对 java.util.Collection.remove 的可疑方法调用”。remove()方法调用。
点击 NetBeans 中的 InvinciBagel.java 编辑选项卡,删除StackPane root;对象声明前面的私有关键字,如图 16-37 突出显示。
图 16-37。
Remove the private access control modifier keyword from in front of the StackPane root; Java statement
一旦你做了这样的修改,你将会看到在你当前的 Bagel.java 编程语句中的红色波浪错误高亮消失,如图 16-38 所示,现在我们所要担心的是移除警告高亮,该警告高亮与传递一个要从场景图中移除的 Actor 对象有关,而不是它所期望的节点对象(spriteFrame ImageView)。这是因为 JavaFX 场景图管理节点类,以及节点子类,如 ImageView,不接受非节点子类,如我们在第八章中设计的 Actor 类,并在后续章节中实现。我们的 Actor 类(object)包含一个 Node 对象,一个 ImageView 节点子类,因此我们必须使用 Java 点标记法包含对该对象的引用。remove()方法可以有效地查看 Actor 对象的“内部”,以到达(访问)这个节点对象。
将光标放在。移除我们正在实现的()方法调用,并键入一个句点。这将弹出属性和方法帮助器,如图 16-38 所示。双击 getSpriteFrame()方法,我们在第八章的中创建了这个方法,从当前的 Actor 对象中调用它,这个对象已经被传递到。collide()方法。这将把 ImageView 节点对象传递到。remove()方法调用,警告高亮也将消失,留下干净的代码。
图 16-38。
Add a period after object in the .remove() method to open method helper and select .getSpriteFrame()
如图 16-39 所示,您所有的 Java 代码都没有错误,现在您已经播放了一个声音,将这个 Actor 对象添加到 HashSetREMOVED _ ACTORS 数据集,并从 JavaFX 场景图根(当前是一个 StackPane 布局容器)中删除了这个 Actor 的 spriteFrame ImageView 节点对象。我们现在准备通过使用一个. resetRemovedActors()方法调用,从当前演员列表<演员>对象中删除这个演员对象。
图 16-39。
Remove the currently collided with Actor object ImageView Node from the Scene Graph using .remove()
在我们更新了 CURRENT_CAST 列表之后,我们就完成了基本的冲突管理编码,我们可以看看如何优化。checkCollision()和。collide()方法更有效地协同工作。
重置删除的执行元列表:。resetRemovedActors()方法
当冲突发生时,我们需要在一系列编程语句中做的最后一件事是重置 REMOVED_ACTORS HashSet 对象。正如你在第一章 0 中回忆的,这个方法从当前演员列表<演员>对象中移除被移除的演员,这样演员对象就完全从游戏中移除了。除了我们使用 ear 测试该方法的. playiSound0()方法调用之外,这是我们必须编写的最简单的 Java 编程语句之一,应该类似于下面的 Java 代码:
invinciBagel.castDirector.``resetRemovedActors()
图 16-40。
The finished .collide() method is error-free and only 19 lines of code, and is ready for further optimization
如果您使用运行➤项目工作流程,并且现在测试您的游戏代码,您不仅会听到音频,还会看到道具对象在与 InvinciBagel 对象碰撞后消失。我现在将音频留在这段代码中,这样您就可以测试这些演员对象是否真的消失了,方法是让 InvinciBagel 在道具对象所在的区域上空运行(或飞行)以确保(用您的耳朵)它真的消失了。接下来,我们将进一步优化代码,实现我前面提到的 if(collide(object))方法,让。collide()方法只是返回一个 true(检测到冲突)或 false(没有检测到冲突)值,就像一个行为正常的布尔方法应该做的那样。
优化碰撞检测处理:if(collide(object))
为了优化冲突检测过程,我们将冲突时执行的代码移到。Java 代码的一个if(collide(object)){}条件 if 块中的 checkCollision()方法。这允许我们从 collide()方法中消除一个布尔 collisionDetect 变量,使它更加精简。我们现在所做的一切。collide()方法将return true;语句传递回调用实体,在本例中是。checkCollision()方法,如果检测到冲突。这也允许我们完全消除 if(碰撞检测)结构。
在优化之前,我们在。checkCollision()方法,以及。collide()方法。优化后,我们少了 5 行 Java 代码,每个方法有 10 行代码。这些方法的新 Java 结构是无错误的,如图 16-41 所示,看起来像下面的 Java 代码:
public void``checkCollision
for(int i=0; i<invinciBagel.castDirector.getCurrentCast().size(); i++) {
Actor``object
If (``collide``(``object
invinciBagel.playiSound0();
invinciBagel.castDirector.addToRemovedActors(``object
invinciBagel.root.getChildren().remove(``object
invinciBagel.castDirector.resetRemovedActors();
}
}
}
@Override
public``boolean collide``(Actor``object
if (invinciBagel.iBagel.spriteFrame.getBoundsInParent().intersects(
object .getSpriteFrame().getBoundsInParent() ) ) {
Shape intersection =
SVGPath.intersect(invinciBagel.iBagel.getSpriteBound(),``object
if (intersection.getBoundsInLocal().getWidth() !=``-1
return true;
}
}
return false;
}
图 16-41。
Optimizing the interaction between the collide() method and the checkCollision() method
使用“运行➤项目”工作流程来确保代码像方法优化前一样工作。
优化场景图:使用组类
因为除了闪屏使用之外,我们并没有真正使用 StackPane 进行图像层合成或 UI 设计,所以让我们添加另一个优化,并为我们的游戏使用 Groupclass (object ),因为它在类层次结构的更高层,比更专业的 StackPane 类(object)更接近节点超类。我们将使用更短更简单的对象➤节点➤父➤组层次结构,而不是使用使用更复杂对象➤节点➤父➤区域➤窗格➤堆栈窗格层次结构的类。这种优化的意义在于,使用组根节点会在系统内存中使用更少的代码(属性和方法)。因为我们没有特别使用任何高度专门化的 StackPane 属性或方法,所以我们可以通过使用 Group 类来执行主要的优化。一个组对象(类)使用固定的对象定位,所以我们必须重新编写 HBox 和 Actor 对象的代码,但是只有在我们使用“运行➤项目”工作流程来查看使用组与使用 StackPane 的确切区别之后。毕竟,这都是为了学习这些类是如何工作的!将 StackPane 对象的导入、声明和实例化更改为 Group 对象的 Java 代码应类似于以下 Java 代码,如图 16-42 所示:
import javafx.scene.``Group
public class InvinciBagel extends Application { // all other object declarations omitted
Group root;
public void start(Stage primaryStage) { // all other object instantiations omitted
root = new``Group
图 16-42。
Replace the StackPane UI container with the Group (Node subclass) import, declaration and instantiation
完成这些更改并运行➤项目后,您将看到图 16-43 左半部分所示的结果。游戏现在使用左上角的 0,0 原点,所以我们必须修改现有代码的其余部分。让我们现在做那件事。
图 16-43。
Showing the Group Scene Graph result (left) and corrective modification to HBox, iBagel and Prop (right)
分别用 WIDTH/2 和 HEIGHT/2 替换对 Bagel()类构造函数的 iBagel 实例化调用中的 0,0 XY 位置。这在构造函数方法调用中使用了屏幕大小常量和计算代码,这将把 iBagel 放在屏幕的中心。在 Prop 对象实例中也添加一些不同的数字,将这些瓷砖放置在屏幕的不同区域,这样我们就可以在 InvinciBagel 拾取它们时开发一些评分代码。最后,用一个.setLayoutY(365);方法调用替换.setAlignment(Pos.BOTTOM_LEFT);方法调用,实现 HBox UI 按钮元素的固定定位,回到我们使用 StackPane 之前的位置。如果您使用运行➤项目工作流程,您将看到如图 16-43 右半部分所示的结果。
图 16-44。
Use .setLayoutY() method to position HBox, WIDTH/2 and HEIGHT/2 to position iBagel, and new Prop X,Y
当你测试你的碰撞代码时,你会发现你不能到达其中的一些区域!接下来我们来解决这个问题。
进入 Bagel.java 类,修改类顶部的屏幕边界常量,使左边界和上边界使用 0,0 原点值,因为 Group 类使用屏幕的左上角作为参考点,而不是像 StackPane 那样使用屏幕的中心。要计算 rightBoundary 值,您需要使用 sprite_PIXELS_X 常量获取屏幕的宽度并减去 SPRITE 的宽度。类似的方法也可以用于 bottomBoundary 值,它将采用屏幕的高度,并使用 sprite_PIXELS_Y 常量减去 SPRITE 的高度。正如你在图 16-45 中看到的,我们不必改变。setBoundaries()方法,这要感谢我们设置代码的模块化、逻辑化和组织化的方式。
图 16-45。
Update the Bagel.java class with new Boundary values for the right, left, bottom and top Boundary value
在我们结束本章之前,让我们为我们的游戏添加一个评分引擎框架,以便这个碰撞检测例程除了播放声音和从游戏中删除演员之外,还调用一个 scoringine 方法。
创建评分引擎方法:。scoringEngine()
在我们结束本章之前,让我们为评分引擎建立一个框架,这样我们就可以把整个下一章的重点放在游戏上。在 checkCollision()方法之后创建一个private void scoringEngine(Actor object){}空方法,并在 checkCollision()方法内部的if(collide(object))条件 if()结构的末尾添加一个scoringEngine(object);方法调用。正如你在图 16-46 中看到的,你会在。scoringEngine()方法声明。这是因为对象引用没有在方法体内部实现。
我们将在下一章中介绍这一点,因此我们不必担心来自 NetBeans 的警告,而且我们已经准备好了评分引擎基础结构。
图 16-46。
Add a private void scoringEngine(Actor object) empty method, and call if inside .checkCollision() method
你已经在这一章完成了很多,关于碰撞检测和你的评分引擎。干得好!
摘要
在第十六章中,我们开始讨论一些更高级的主题,我们将在本书的最后几章中讨论这些主题。碰撞检测是游戏开发的基本主题之一,无论是使用 Java 8 还是其他平台。在本章中,我们学习了 SVG 数据格式规范,因此我们可以利用 JavaFX SVGPath 类,它允许我们访问自定义路径形状,如果它们是闭合的,通常称为多边形,以及哪些碰撞形状或碰撞多边形通常是闭合的。我们研究了 SVG 规范中的七个主要命令,所有这些命令都可以在绝对或相对模式下使用。
接下来,我们详细了解了如何让 GIMP 2.8 为我们创建自定义的碰撞形状 SVG 数据字符串,这是一个完全自动化的过程,仅使用 GIMP 工具(算法),允许我们简单地单击透明区域,并让 GIMP 生成选择集。我们将选择集转换为碰撞路径,然后将路径导出为 SVG 数据。这个工作过程允许我们在任何游戏资产数字图像中生成具有透明(alpha 通道数据)区域的复杂碰撞多边形。
这种自动生成的碰撞数据非常“沉重”,因此我们研究了一个更复杂的工作过程,允许我们使用更少的数据点对手动创建碰撞多边形。这个过程包括使用 GIMP 中的路径工具手工创建一个碰撞多边形,这给了我们更多的控制来生成尽可能少的数据点。我们还看了 CodeAndWeb PhysicsEditor 软件,它可以跨所有游戏开发平台使用。
在下一章,我们将看看如何在游戏中实现增强的游戏元素,包括得分引擎、对手、宝藏、自动攻击引擎和物理模拟。