阿里云网站续费怎么操作友情链接的英文
协议:CC BY-NC-SA 4.0
五、游戏设计介绍:概念、多媒体和使用场景生成器
在本章中,您将通过学习在 JavaFX 中使用场景图范例的最佳方式,了解 JavaFX Scene Builder 工具和 FXML,以及为什么(或为什么不)在某些类型的 Java 游戏开发场景中使用这些工具,来建立您对 JavaFX 多媒体引擎的了解。您还将研究基本的游戏设计优化概念、游戏类型以及适用于 Java 平台的游戏引擎,包括物理引擎,如 JBox2D 和 Dyn4J,以及 3D 游戏引擎,如 LWJGL 和 JMonkey。最后,你将考虑新媒体的概念,你将需要了解整合数字图像,数字音频,数字视频和动画到你的游戏制作管道。我们还将看看一些免费的开源多媒体制作工具,它们是你在第一章中安装的,现在可以用来制作 Java 8 游戏。
首先,你将重温静态(固定)与动态(实时)的基本概念,这在第三章(常量与变量)和第四章(脉冲)中有所涉及,也是游戏优化的基本原则之一。这一点很重要,因为您会希望您的游戏能够在用于玩游戏的所有不同平台和设备上流畅运行,即使设备只使用单处理器(这在当今实际上很少见,大多数设备都采用双核(双处理器)或四核(四处理器)CPU)。
接下来,你将学习游戏设计的概念、技术和术语,包括精灵、碰撞检测、物理模拟、背景板、动画、图层、关卡、逻辑和人工智能。你还将研究可以设计的不同类型的游戏,以及它们之间的区别。
然后,您将探索多媒体资产在当今视觉(和听觉)令人印象深刻的游戏中所扮演的角色。您将了解数字成像、数字视频、动画以及数字音频的原理,因为您将在本书的课程中使用许多这些新媒体资产类型,并且需要这些基础知识才能使用它们。
最后,您将深入了解您在第二章中生成的引导 JavaFX 应用代码,以及 Java。main()方法和 JavaFX。start()方法使用 Stage()构造函数方法创建 primaryStage Stage 对象,并在其中使用 scene()构造函数方法创建一个名为 Scene 的场景对象。您将了解如何使用 Stage 类中的方法来设置场景、标题舞台和显示舞台,以及如何创建和使用 StackPane 和 Button 类(对象)以及如何向按钮添加 EventHandler。
高级概念:静态与动态
我想从一个高层次的概念开始,涉及到我将在本章中谈到的一切,从您可以创建的游戏类型,到游戏优化,到 JavaFX 场景生成器和 JavaFX 场景图。不管你是否意识到,在探索 Java 常量的概念时,你已经在第三章回顾了这个概念,Java 常量是固定的或静态的,不会改变,而 Java 变量是动态的,会实时改变。类似地,JavaFX 场景图中的 UI 设计可以是静态的(固定的和不可移动的)或动态的(动画的、可拖动的或可换肤的,这意味着您可以更改 UI 外观以适应您的个人喜好)。
这些概念在游戏设计和开发中非常重要的原因是,您设计用来运行或渲染游戏的游戏引擎必须不断检查其动态部分,以查看它们是否发生了变化并需要响应(更新分数、移动精灵位置、播放动画帧、更改游戏角色的状态、计算碰撞检测、计算物理,等等)。这种对每个帧更新的检查(以及随后的处理)(在 JavaFX 中称为脉冲;参见第四章),以确保你所有的变量、位置、状态、动画、碰撞、物理等都符合你的 Java 游戏引擎逻辑,真的可以加起来,而且,在某些时候,做所有这些工作的处理器可能会过载,这会降低它的速度!
这种增强游戏动态性的所有实时、逐帧检查过载的结果是,游戏运行的帧速率将会降低。没错,像数字视频和动画一样,Java 8 游戏也有帧率,但是 Java 8 游戏帧率是基于你的编程逻辑的效率。你游戏的帧率越低,游戏玩起来就越不流畅,至少对于动态的、实时的游戏来说是这样,比如街机游戏;一款游戏玩起来有多流畅,关系到玩家的用户体验有多流畅。
出于这个原因,静态与动态的概念对于游戏设计的每个方面都非常重要,并且使某些类型的游戏比其他类型的游戏更容易实现出色的用户体验。我将在本章后面讨论不同类型的游戏(参见“游戏类型:谜题、棋盘游戏、街机游戏、混合游戏”一节),但是,正如你可能想象的,棋盘游戏本质上更静态,街机游戏更动态。也就是说,有一些优化方法可以让游戏保持动态,也就是说,看起来好像有很多事情在进行,而从处理的角度来看,实际发生的事情是很容易管理的。这是游戏设计的众多技巧之一,说到底,这是关于优化的。
Android (Java)编程中最重要的静态与动态设计问题之一是使用 XML 的 UI 设计(静态设计)与使用 Java 的 UI 设计(动态设计)。Android 平台允许使用 XML 而不是 Java 来完成 UI 设计,这样非程序员(设计者)可以为应用进行前端设计。JavaFX 允许使用 FXML 做完全相同的事情。要做到这一点,你必须创建一个 FXML JavaFX 应用,正如你在第二章中看到的那样(参见图 2-4,右侧,第三个选项,“JavaFX FXML 应用”)。这样做会将 javafx.fxml 包和类添加到应用中,让您设计 UI,使用 fxml,然后让您的 Java 编程逻辑“膨胀”它们,以便设计由 JavaFX UI 对象组成。
值得注意的是,使用 FXML 会给应用开发和编译过程增加另一层,其中包含 FXML 标记及其翻译和处理。我将在本章后面演示如何做到这一点,以防您的设计团队希望使用 FXML 而不是 Java 进行 UI 设计工作流程(参见“JavaFX Scene Builder:使用 FXML 进行 UI 设计”一节)。我这样做是因为我想涵盖 JavaFX 中的所有设计选项,包括 FXML,以确保这本书完整地涵盖了使用 Java 8 和 JavaFX 8.0 可以做些什么。然而,归根结底,这是一个 Java 8 编程的题目,所以我在本书中的主要焦点将是使用 Java 8,而不是 FXML。
在任何情况下,我关于使用 XML(或 FXML)创建 UI 设计的观点是,这种方法可以被视为静态的,因为设计是预先使用 XML 创建的,并且在编译时使用 Java“膨胀”。Java 膨胀方法使用设计者提供的 FXML 结构来创建场景图,该场景图基于使用 FXML 创建的 UI 设计结构(层次结构)填充有 JavaFX UI(类)对象。我将在本章的后面向您概述这是如何工作的,这样您就可以了解这是如何工作的(参见“JavaFX Scene Builder:使用 FXML 进行 UI 设计”一节)。
游戏优化:平衡静态元素与动态元素
游戏优化归结为平衡不需要实时处理的静态元素和需要持续处理的动态元素。过多的动态处理,尤其是在不需要的时候,会让你的游戏变得不稳定。这就是为什么游戏编程是一种艺术形式:它需要平衡以及伟大的人物,故事线,创造力,幻觉,预期,准确性,最后,优化。
表 5-1 中列出了动态游戏中一些不同的游戏组件优化考虑事项。如您所见,游戏的许多方面都可以进行优化,使处理器的工作负载明显不那么“繁忙”如果这些主要的动态游戏处理区域中有一个因为处理器每帧的宝贵周期而“失控”,这将极大地影响游戏的用户体验。我将在本章的下一节介绍游戏术语(精灵、碰撞检测、物理模拟等等)。
表 5-1。
Aspects of Game Play That Can Be Optimized to Minimize System Memory and Processor Cycle Usage
游戏方面 | 基本优化原则 |
---|---|
精灵位置(移动) | 移动精灵尽可能多的像素,以实现屏幕上的平滑移动。 |
冲突检出 | 仅在必要时检查屏幕上对象之间的碰撞(非常接近)。 |
物理模拟 | 尽量减少场景中需要进行物理计算的对象数量。 |
精灵动画 | 最大限度地减少需要循环的帧数,以创建流畅的动画效果。 |
背景动画 | 最小化有动画的背景区域,使整个背景看起来有动画,但实际上没有。 |
游戏逻辑 | 尽可能高效地编程游戏逻辑(模拟或人工智能)。 |
记分板更新 | 仅在得分时更新记分牌,并将得分更新最小化至每秒最多一次。 |
用户界面设计 | 使用静态 UI 设计,以便脉冲事件不用于 UI 元素定位或 CSS3。 |
考虑到所有这些游戏编程领域,使得游戏编程成为一项极其棘手的工作!
值得注意的是,这些方面中的一些共同作用,为玩家创造了一个特定的幻觉。例如,精灵动画将创建角色奔跑、跳跃或飞行的幻觉,但如果不将该代码与精灵定位(运动)代码相结合,将无法实现幻觉的真实性。为了微调一个错觉,动画的速度(帧速率)和移动的距离(每帧像素)可能需要调整(我喜欢称之为微调)以获得最真实的结果。我们将在第一章 3 节中做这件事。
如果你可以移动游戏元素(主要玩家精灵,目标精灵,敌人精灵,背景)更多的像素和更少的次数,你将节省处理周期。花费处理时间的是移动的部分,而不是距离(移动了多少像素)。类似地,对于动画,实现令人信服的动画所需的帧越少,保存这些帧所需的内存就越少。请记住,您正在优化内存使用以及处理周期。检测碰撞是游戏编程逻辑的主要部分;重要的是不要检查游戏元素之间的冲突,这些元素不是“在玩”(在屏幕上),也不是活动的,并且彼此不靠近。
自然力(物理模拟)和游戏逻辑(如果没有很好地编码(优化),是处理器最密集的方面。这些都是我将在本书的后面部分涉及的主题,当你更高级的时候(见第十六章和第十七章)。
游戏设计概念:精灵,物理,碰撞
让我们来看看构建游戏需要了解的各种游戏设计组件,以及可以用来实现游戏的这些方面的 Java 8(或 JavaFX)包和类,我喜欢称之为游戏组件。这些可以包括游戏元素本身(通常称为精灵)以及处理引擎,您可以自己编写代码,或者导入预先存在的 Java 代码库,如物理模拟和碰撞检测。
精灵是游戏的基础,定义你的主角,用来伤害主角的投射物,以及发射这些投射物的敌人。精灵是 2D 图形元素,可以是静态的(固定的,单个图像)或动态的(动画,几个图像的无缝循环)。精灵将基于编程逻辑在屏幕上移动,这决定了游戏如何运行。精灵需要与背景图像和其他游戏元素以及其他精灵合成,因此用于创建精灵的图形需要支持透明背景。
在第四章中,我向你介绍了阿尔法通道和透明度的概念。你需要用你的精灵来达到同样的最终效果,来创造一个无缝的游戏视觉体验。游戏的下一个最重要的方面是碰撞检测,因为如果你的精灵只是在屏幕上从彼此身边飞过,并且当他们相互接触或“相交”时,从来没有做过任何酷的事情,那么你真的不会有一个很好的游戏!一旦你添加了一个碰撞检测引擎(由交叉逻辑处理例程组成),你的游戏就可以确定任何两个精灵何时接触(边缘)或彼此重叠。碰撞检测将调用(触发)其他逻辑处理例程,这些例程将确定当任何两个给定的精灵(如投射物和主要角色)相交时会发生什么。例如,当投射物与主角相交时,伤害点可能会增加,生命力指数可能会降低,或者死亡动画可能会开始。相比之下,如果一件宝物与主角相交(被主角捡到),则可能会产生能量或能力点,生命力指数可能会增加,或者可能会启动“我找到了”的庆祝动画。正如你所看到的,除了精灵(角色、投射物、宝物、敌人、障碍等等)本身,游戏的碰撞检测是游戏的基本设计元素之一,这就是为什么我在本书的早期就介绍了这个概念。
对你的游戏来说,下一个重要的概念是真实世界的物理模拟。像重力这样的东西的加入;摩擦;弹跳;拖;加速度;运动曲线,如 JavaFX 插值器类提供的;和高度精确的碰撞检测的基础上增加了额外的真实感。
最后,添加到游戏中的最专有的属性或逻辑结构(Java 代码)是自定义游戏逻辑,它使您的游戏在市场中真正独一无二。这个逻辑应该保存在它自己的 Java 类或方法中,与物理模拟和碰撞检测代码分开。毕竟,如果您学习了 OOP 概念并将它们应用到您的编程逻辑中,Java 8 会使您的代码模块化结构良好!
当你开始把所有这些游戏组件加在一起时,它们开始让游戏变得更可信,也更专业。一个伟大游戏的关键目标之一是暂停信念,这意味着你的玩家完全相信前提、角色、目标和游戏。这是任何内容制作人,无论是电影制作人、电视制作人、作家、歌曲作者、Java 8 游戏程序员还是应用开发人员,都追求的目标。如今,游戏的创收能力与任何其他内容分发类型一样,如果不是更多的话。
接下来,让我们看看可以创建的不同类型的游戏,以及这些游戏在应用精灵、碰撞检测、物理模拟和游戏逻辑等核心游戏组件方面有何不同。
游戏类型:谜题、棋盘游戏、街机游戏、混合游戏
就像我在这一章中谈到的其他东西一样,游戏本身可以通过使用静态和动态的分类方法来分类。静态游戏不受处理器限制,因为它们本质上倾向于基于回合而不是基于手眼协调,因此,在某种意义上,它们更容易流畅地工作;只有游戏规则的编程逻辑和吸引人的图形需要被放置和调试。对于开发新类型的游戏类型来说,也存在一个重要的机会,这种游戏类型以前所未见的创造性的新方式使用静态和动态游戏玩法的混合组合。我自己正在研究其中的一些!
因为这是一个 Java 8 编程的标题,所以我将从这个角度来处理所有事情,这恰好是将游戏分成不同类别(静态、动态、混合)的一个好方法,所以让我们先讨论静态(固定图形)、回合制游戏。这些游戏包括棋类游戏、益智游戏、知识游戏、记忆游戏和策略游戏,所有这些游戏的受欢迎程度和可销售性都不容小觑。
静态游戏的酷之处在于,它们可以像动态游戏一样有趣,而且处理开销明显更少,因为它们不必达到 60FPS 的实时处理目标,就可以流畅、专业地玩游戏。这是因为游戏的本质根本不是基于移动,而是基于做出正确的战略移动,但只有当轮到你这样做的时候。
在静态游戏中可能涉及一些形式的碰撞检测,关于哪个游戏棋子已经移动到游戏棋盘或游戏面上的给定位置;然而,没有碰撞检测使处理器过载的危险,因为游戏板的其余部分是静态的,除了在特定玩家的回合中被有策略地移动的一个棋子。
策略游戏的处理逻辑更多的是基于策略逻辑的编程,在给定正确的移动顺序的情况下,允许玩家获得最终的胜利,而动态游戏编程逻辑更多地关注游戏精灵之间发生的冲突。动态游戏的重点是分数,躲避弹丸,寻找宝藏,完成水平目标,杀死敌人。
具有大量相关规则的复杂策略游戏,如国际象棋,可能比动态游戏有更多的编程逻辑例程。然而,因为代码的执行对时间不敏感,所以无论平台和 CPU 有多强大,最终的游戏都将是流畅的。当然,游戏规则集逻辑必须完美无缺,这种类型的游戏才能真正做到专业,所以,最终,静态和动态游戏都很难编码,尽管原因不同。
动态游戏可以被称为动作游戏或街机游戏,并且包括显示屏上的大量运动。这些高度动态的游戏几乎总是涉及射击,例如在第一人称射击游戏(例如,Doom,Half-Life)以及第三人称射击游戏(生化危机,侠盗猎车手)类型中,或者偷东西,或者躲避东西。还有就是障碍赛导航范式,常见于平台游戏,比如大金刚,超级马里奥。
值得注意的是,任何类型的游戏都可以使用 2D 或 3D 图形和资源,甚至是 2D 和 3D 资源的组合来制作,正如我在第四章中指出的,这是 JavaFX 允许的。
有这么多流行的游戏类型,总是有机会通过使用静态(策略)游戏类型和动态(动作)游戏类型的混合方法来创建一种全新的游戏类型。
游戏设计资产:新媒体内容概念
让你的游戏变得高度专业并让买家满意的最强大的工具之一是你在第一章中下载并安装的多媒体制作软件。在我继续讲下去之前,我需要花一些时间向您提供关于 Java 8 中支持的四种主要新媒体资产类型的基础知识,使用 JavaFX 8 多媒体引擎:数字图像,用于精灵、背景图像和 2D 动画;向量形状,用于 2D 插图、碰撞检测、3D 对象、路径和曲线;数字音频,用于音效、旁白和背景音乐;数字视频,在游戏中用于动画背景循环(天空中飞翔的鸟,飘动的云,等等)曾经高度优化。如图 5-1 所示,这四个主要流派,或者区域,都是通过 JavaFX 场景图安装的,使用了我在第四章中描述的包。将使用的一些主要类是 ImageView、AudioClip、Media、MediaView、MediaPlayer、Line、Arc、Path、Circle、Rectangle、Box、Sphere、Cylinder、Shape3D、Mesh 和 MeshView。
图 5-1。
How new media assets are implemented, using Scene Graph through the JavaFX API in Java 8 via NetBeans
因为在 Java 8 游戏设计和编程管道中使用这些新媒体元素之前,您需要有一定的技术基础,所以我将从数字图像和矢量插图开始,介绍这四个新媒体领域的基本概念。
数字成像概念:分辨率、色深、Alpha、图层
JavaFX(因此 Java8)支持大量流行的数字图像文件(数据)格式,这给了游戏设计者极大的灵活性。其中一些已经存在很久了,例如 CompuServe 的图形交换格式(GIF)和联合图像专家组(JPEG)格式。有些 JavaFX 图形文件格式更现代,比如可移植网络图形(PNG 发音为“ping”),这是您将在游戏中使用的文件格式,因为它可以产生最高的质量水平并支持图像合成。Java 支持的所有这些主流数字图像文件格式在 HTML5 浏览器中也受支持,并且因为 Java 应用可以与 HTML 应用和网站一起使用,这确实是一个非常合乎逻辑的协同作用!
最古老的 CompuServe GIF 格式是无损数字图像文件格式。之所以称为无损压缩,是因为它不会丢弃图像数据来获得更好的压缩结果。GIF 压缩算法不像 PNG 格式的压缩算法那样精细(强大), GIF 只支持索引颜色,这是它获得压缩(较小文件大小)的方式。如果您的游戏图像资产已经以 GIF 格式创建,您将能够毫无问题地在 Java 8 游戏应用中使用它们(除了效率较低的图像压缩算法和没有合成功能)。
Java 8 (JavaFX)支持的最流行的数字图像文件格式是 JPEG,它使用“真彩色”色深,而不是索引色深,以及所谓的有损数字图像压缩,其中压缩算法“丢弃”图像数据,以便它可以实现更小的文件大小(图像数据将永远丢失,除非您聪明地保存您的原始图像!).
如果您在压缩后放大 JPEG 图像,您将看到一个变色区域(效果),这在原始图像中是不存在的。图像中的一个或多个退化区域通常被称为压缩伪影。这只会发生在有损图像压缩中,在 JPEG(和运动图像专家组[MPEG])压缩中很常见。
Tip
我建议您在 Java 8 游戏中使用 PNG 数字图像格式。这是一种专业的图像合成格式,你的游戏本质上是一个实时精灵合成引擎,所以你需要使用 PNG32 图像。
PNG 有两个真彩色文件版本:PNG24(不能用于图像合成)和 PNG32(带有用于定义透明度的 alpha 通道)。
我为你的游戏推荐 PNG,因为它有一个不错的图像压缩算法,并且是一种无损图像格式。这意味着 PNG 有很好的图像质量以及合理的数据压缩效率,这将使你的游戏发行文件更小。PNG32 格式的真正力量在于它能够使用透明度和抗锯齿(通过其 alpha 通道)与其他游戏图像合成。
数字图像分辨率和纵横比:定义图像大小和形状
您可能知道,数字图像是由 2D(二维)像素阵列组成的(“像素”代表图片[pix]元素[el])。图像的清晰度由其分辨率表示,分辨率是图像宽度(或 W,有时称为 x 轴)和高度(或 H,有时称为 y 轴)维度中的像素数量。图像的像素越多,分辨率越高。这类似于数码相机的工作原理,因为图像捕捉设备(称为相机 CCD)的像素越多,可以实现的图像质量就越高。
要计算图像像素的总数,请将宽度像素乘以高度像素。例如,一个宽视频图形阵列(VGA) 800 × 480 图像包含 384,000 个像素,正好是八分之三兆字节。这就是你如何找到你的图像的大小,包括使用的千字节(或兆字节)和显示屏上的高度和宽度。
使用图像纵横比来指定数字图像资产的形状。纵横比是数字图像的宽高比,定义了正方形(1:1 纵横比)或矩形(也称为宽屏)数字图像形状。具有 2:1(宽屏)宽高比的显示器,例如 2,160 × 1,080 分辨率,现在已经上市。
1:1 纵横比的显示器(或图像)总是完美的正方形,2:2 或 3:3 纵横比的图像也是如此。例如,你可能会在智能手表上看到这个长宽比。值得注意的是,定义图像或屏幕形状的是这两个宽度和高度(x 和 y)变量之间的比率,而不是实际的数字本身。
纵横比应该始终表示为冒号两边可以达到(减少)的最小数字对。如果你在高中时注意学习最小公分母,那么长宽比对你来说很容易计算。我通常通过继续将冒号的每一边一分为二来计算长宽比。比如你拿 SXGA 1280×1024 分辨率来说,1280×1024 的一半是 640 × 512,640 × 512 的一半是 320×256;320 × 256 的一半是 160 × 128,再一半是 80 × 64,再一半是 40 × 32,再一半是 20×16;20 × 16 的一半是 10 × 8,其中的一半给出了 SXGA 的 5 × 4 纵横比,这可以通过在两个数字之间使用冒号来表示,例如纵横比为 5:4。
数字图像色彩理论和色彩深度:定义精确的图像像素颜色
每个数字图像像素的颜色值可以由红、绿和蓝(RGB)三种不同颜色的量来定义,这三种颜色在每个像素中以不同的量存在。消费电子显示屏利用加色,其中每个 RGB 颜色通道的光波长相加在一起,产生 1680 万种不同的颜色值。加色用于液晶显示器(LCD)、发光二极管(LED)和有机发光二极管(有机发光二极管)显示器。它与印刷中使用的减色法相反。为了向您展示不同的结果,在减色模式下,将红色与绿色(油墨)混合将产生紫色,而在加色模式下,将红色与绿色(浅色)混合将产生鲜艳的黄色。加色可以提供比减色更宽的颜色范围。
为每个像素保存的每个红色、绿色和蓝色值有 256 个亮度级别。这允许您为每个红色、绿色和蓝色值设置 8 位值控制颜色亮度变化,从最小值 0 (#00 或关闭,全暗或黑色)到最大值 255 (#FF 或全开,最大颜色贡献)。用于表示数字图像像素颜色的位数被称为图像的色深。
数字成像行业中使用的常见色深包括 8 位、16 位、24 位和 32 位。我将在这里概述这些,以及它们的格式。最低色深存在于 8 位索引的彩色图像中。这些具有最多 256 个颜色值,并使用 GIF 和 PNG8 图像格式来保存这种索引颜色类型的数据。
中等色深图像具有 16 位色深,因此包含 65,536 种颜色(计算为 256 × 256)。它受 TARGA (TGA)和标记图像文件格式(TIFF)数字图像格式支持。如果您想在 Java 8 游戏中使用 GIF、JPEG 和 PNG 之外的数字图像格式,请导入第三方 ImageJ 库。
真彩色颜色深度图像将具有 24 位颜色深度,因此将包含超过 1600 万种颜色。这被计算为 256 × 256 × 256,产生 16,777,216 种颜色。支持 24 位颜色深度的文件格式包括 JPEG(或 JPG)、PNG、BMP、XCF、PSD、TGA、TIFF 和 WebP。JavaFX 支持其中的三种:JPG、PNG24 (24 位)和 PNG32 (32 位)。使用 24 位色深将为您提供最高的质量水平。这就是为什么我推荐你的 Java 游戏使用 PNG24 或 PNG32。接下来,让我们看看如何通过 alpha 通道表示图像像素透明度值,以及如何在 Java 8 游戏中使用这些值实时合成数字图像!
数字图像合成:在图层中使用 Alpha 通道和透明度
合成是将多层数字影像无缝融合在一起的过程。正如你所想象的,这对于游戏设计和开发来说是一个极其重要的概念。当您想要在显示器上创建一个看起来像是单个图像(或动画)的图像,而实际上它是两个或多个合成图像层的无缝集合时,合成非常有用。您想要设置图像或动画合成的一个主要原因是,通过将它们放在不同的层上,允许对这些图像中的各种元素进行编程控制。
要实现这一点,您需要一个 alpha 通道透明度值,该值可用于控制给定像素与其他层(在其上方和下方)上的另一个像素(在相同的 x,y 图像位置)的混合量的精度。
像其他 RGB 通道一样,alpha 通道有 256 个透明度级别。在 Java 编程中,alpha 通道由# AARRGGBB 数据值的十六进制表示中的前两个槽来表示(我将在下一节中详细介绍)。Alpha 通道 ARGB 数据值使用八个数据槽(32 位)而不是 24 位图像中使用的六个数据槽(#RRGGBB),24 位图像实际上是一个没有 alpha 通道数据的 32 位图像。
因此,24 位(PNG24)图像没有 alpha 通道,不会用于合成,除非它是合成层堆栈中的底部图像板。相比之下,PNG32 图像将用作 PNG24(背景板)或 PNG32(较低的 z 顺序合成层)之上的合成层,这将需要此 alpha 通道功能来显示(通过 alpha 通道透明度值)图像合成中的某些像素位置。
数字图像 alpha 通道和图像合成的概念是如何影响 Java 游戏设计的?主要的优点是能够将游戏画面以及画面中的精灵、投射物和背景图形元素分解成多个组件层。这样做的原因是为了能够将 Java 8 编程逻辑(或 JavaFX 类)应用于单独的图形图像元素,以控制游戏屏幕的某些部分,否则如果它是一个单独的图像,您将无法单独控制这些部分。
图像合成的另一部分,称为混合模式,也是专业图像合成功能的重要因素。JavaFX 混合模式通过使用 Blend 类和 javafx.scene.effect 子包中的 BlendMode 常量值来应用(参见第四章)。这个 JavaFX 混合效果包为 Java 游戏开发人员提供了许多与 Photoshop(和 GIMP)为数字图像制作人员提供的相同的图像合成模式。这将 Java 8(通过 JavaFX)变成了一个强大的图像合成引擎,就像 Photoshop 一样,混合算法可以在非常灵活的水平上进行控制,使用自定义的 Java 8 代码。JavaFX 混合模式常量包括 ADD、SCREEN、OVERLAY、DARKEN、LIGHT、MULTIPLY、DIFFERENCE、EXCLUSION、SRC_ATOP、SRC_OVER、SOFT_LIGHT、HARD_LIGHT、COLOR_BURN 和 COLOR_DODGE。
用 Java 8 游戏代码表示颜色和 Alpha:使用十六进制记数法
现在您已经知道了什么是色深和 alpha 通道,以及在任何给定的数字图像中使用四种不同的图像通道(alpha、红色、绿色和蓝色[ARGB])的组合来表示颜色和透明度,重要的是理解作为程序员,您应该如何在 Java 8 和 JavaFX 中表示这四种 ARGB 图像颜色和透明度通道值。
在 Java 编程语言中,颜色和 alpha 不仅用于 2D 数字图像(通常称为位图图像),还用于 2D 插图(通常称为矢量图像)。颜色和透明度值也经常在许多不同的颜色设置选项中使用。正如您已经看到的 Stage 对象和 Scene 对象,您可以为舞台、场景、布局容器(StackPane)或 UI 控件等设置背景色(或透明度值)。
在 Java 中(JavaFX 也是如此),不同级别的 ARGB 颜色强度值用十六进制表示。十六进制(或 hex)是基于原始的 16 进制计算机记数法。这在很久以前被用来表示 16 位数据值。与更常见的从 0 到 9 计数的 Base10 不同,Base16 记数法从 0 到 F 计数,其中 F 表示 15 的 Base10 值(0 到 15 产生 16 个数据值)。
Java 中的十六进制值总是以 0 和 x 开头,就像这样:0xFFFFFF。这个十六进制颜色值表示颜色。白色常数,不使用 alpha 通道。在这种 24 位十六进制表示中,六个槽中的每一个都代表一个 16 进制值,因此要获得每种 RGB 颜色所需的 256 个值需要两个槽,即 16 × 16 = 256。因此,要使用十六进制表示法表示 24 位图像,您需要在井号后有六个槽来保存六个十六进制数据值(每个数据对表示 256 个级别的值)。如果您乘以 16 × 16 × 16 × 16 × 16 × 16,您将获得 16,777,216 种颜色,这在使用 24 位真彩色图像数据时是可能的。
十六进制数据槽以下列格式表示 RGB 值:0xRRGGBB。对于 Java 常量颜色。白色,十六进制颜色数据值表示中的所有红色、绿色和蓝色通道都处于全亮度(最大颜色值)设置。如果你把所有这些颜色加在一起,你会得到白光。
黄色表示红色和绿色通道打开,蓝色通道关闭,因此颜色是十六进制表示。因此,黄色为 0xFFFF00,其中红色和绿色通道槽完全打开(FF 或 255 Base10 数据值),蓝色通道槽完全关闭(00 或 0 值)。
ARGB 值的八个十六进制数据槽将使用以下格式保存数据:0xAARRGGBB。因此,对于颜色。白色,十六进制颜色数据值表示中的所有阿尔法、红色、绿色和蓝色通道将处于它们的最大亮度(或不透明度),并且阿尔法通道将完全不透明,即不透明,如 FF 值所表示的。因此,颜色的十六进制值。白色常数将是 0xFFFFFFFF。
100%透明的 alpha 通道可以通过将 alpha 槽设置为 0 来表示,正如您在创建一个无窗口的 Java 8 应用时所观察到的那样(参见第四章)。因此,您可以使用 0x00000000 和 0x00FFFFFF 之间的任何值来表示透明图像像素值。重要的是要注意,如果 alpha 通道值等于完全透明,那么可以包含在其他六个(RGB)十六进制数据值槽中的 16,777,216 个颜色值将完全无关紧要,因为该像素是透明的,将被评估为不存在,因此不会合成在最终图像或动画合成图像中。
数字图像遮罩:使用 Alpha 通道创建游戏精灵
alpha 通道在游戏设计中的主要应用之一是遮蔽图像或动画(一系列图像)的区域,以便它可以在游戏图像合成场景中用作游戏精灵。蒙版是使用 alpha 通道透明度值从数字图像中剪切出主题的过程,以便主题可以放置在它自己的虚拟层上。这是使用数字成像软件包完成的,例如 GIMP。
数字图像-合成软件包,如 Photoshop 和 GIMP,包含用于遮罩和图像合成的工具。如果不进行有效的遮罩,就无法进行有效的图像合成,因此对于希望将图形元素(如图像精灵和精灵动画)集成到游戏设计中的游戏设计人员来说,这是一个需要掌握的重要领域。数字图像蒙版技术已经存在很长时间了!
遮罩可以自动为您完成,使用专业的蓝屏(或绿屏)背景,以及可以自动提取那些精确颜色值的计算机软件来创建遮罩,遮罩被转换为 alpha 通道(透明度)信息(数据)。也可以使用数字图像软件,通过算法选择工具之一,结合各种锐化和模糊算法,手动进行遮罩。
在本书的过程中,你会学到很多关于这个工作过程的知识,使用常见的开源软件包,比如 GIMP。掩蔽可能是一个复杂的工作过程。这一章的目的是让你接触到基础知识,这些知识是你阅读这本书的过程的基础。
遮罩过程中的一个关键考虑因素是在被遮罩的对象(主题)周围获得平滑、清晰的边缘。这是为了当你把一个被遮罩的物体(在这种情况下,是一个游戏精灵)放到新的背景图像上时,它看起来就像是在第一个地方被拍摄的一样。成功做到这一点的关键在于你的选择工作过程,这需要以适当的方式使用数字图像软件选择工具,如 GIMP 中的剪刀工具,或 Photoshop 中的魔棒工具。选择正确的工作流程至关重要!
例如,如果您想要遮罩的对象周围有统一颜色的区域(可能您是对着蓝色屏幕拍摄的),您将使用具有适当阈值设置的魔棒工具来选择除对象之外的所有内容。然后,反转选择,这将为您提供包含该对象的选择集。通常,正确的工作流程需要以相反的方式进行。其他选择工具包含复杂的算法,可以查看像素之间的颜色变化。这些对于边缘检测非常有用,您可以将其用于其他选择方法。
平滑数字图像合成:使用抗锯齿平滑图像边缘
抗锯齿是一种流行的数字图像合成技术,在这种技术中,数字图像中的两种相邻颜色沿着两种颜色区域的边界混合在一起。当图像缩小时,这会欺骗观众的眼睛看到更平滑(更少锯齿)的边缘,从而消除所谓的图像锯齿。抗锯齿通过使用平均颜色值(一个颜色范围,是两种颜色结合在一起的中间部分)提供了令人印象深刻的结果,沿着边缘只有几个需要平滑的彩色像素。
让我们看一个例子,看看我在说什么。图 5-2 显示了一个图层上看起来非常清晰的红色圆圈,覆盖了一个背景图层上的黄色填充颜色。我放大了红色圆圈的边缘,然后制作了另一个截图,放在缩小的圆圈的右边。此屏幕截图显示了一系列抗锯齿颜色值(黄色-橙色、橙色到橙色、红色-橙色),正好位于红色和黄色的边缘,即圆形与背景的交界处。
图 5-2。
A red circle composited on a yellow background (left) and a zoomed-in view (right) showing antialiasing
值得注意的是,JavaFX 引擎将使用 Java2D 软件渲染器或使用 Prism 引擎渲染的硬件(可以使用 OpenGL 或 DirectX ),针对所有背景颜色和背景图像对 2D 形状和 3D 对象进行抗锯齿处理。您仍将负责正确合成,即使用每个图像的 alpha 通道为多层图像提供抗锯齿。
数字图像优化:使用压缩、索引颜色和抖动
影响数字图像压缩的因素有很多,您可以使用一些基本的技术以较小的数据占用量获得较高质量的结果。这是优化数字图像的主要目标;为您的应用(在本例中为游戏)获得尽可能小的数据占用空间,同时获得最高质量的视觉效果。让我们从对数据占用空间影响最大的几个方面入手,研究一下对于任何给定的数字图像来说,这些方面是如何对数据占用空间优化产生影响的。有趣的是,它们的重要性顺序与我迄今为止提出的数字成像概念的顺序相似。
影响最终数字图像资产文件大小的最关键因素是我所称的数据足迹,即数字图像的像素数量或分辨率。这是合乎逻辑的,因为需要存储每个像素,以及包含在它们的三个(24 位)或四个(32 位)通道中的颜色和 alpha 值。在保持图像清晰的同时,分辨率越小,生成的文件也就越小。
对于 24 位 RBG 图像,原始(或未压缩)图像的大小计算为宽×高× 3,对于 32 位 ARGB 图像,为宽×高× 4。例如,未压缩的 24 位真彩色 VGA 图像将具有 640 × 480 × 3,相当于 921,600B 的原始(raw)未压缩数字图像数据。要确定此原始 VGA 图像中的千字节数,您需要进行如下划分:921,600 ÷ 1,024,这是一千字节中的字节数,在 truecolor VGA 图像中会有 900KB 的数据。
通过优化数字影像分辨率来优化原始(未压缩)图像大小非常重要。这是因为一旦图像从游戏应用文件解压缩到系统内存中,这就是它将要占用的内存量,因为图像将使用 24 位(RGB)或 32 位(ARGB)表示法逐个像素地存储在内存中。这也是我游戏开发用 PNG24 和 PNG32,而不是索引色(GIF 或 PNG8)的原因之一;如果操作系统要将颜色转换到 24 位色彩空间,那么出于质量原因,您应该使用 24 位色彩空间,并处理(接受)稍大的应用文件大小。
图像颜色深度是压缩图像的数据足迹的第二个最重要的因素,因为图像中的像素数乘以 1 (8 位)、2 (16 位)、3 (24 位)或 4 (32 位)颜色数据通道。这种小文件大小是 8 位索引彩色图像仍然被广泛使用的原因,尤其是 GIF 图像格式。
如果用于构成图像的颜色变化不太大,索引色图像可以模拟真彩色图像。索引彩色影像仅使用 8 位数据(256 种颜色)来定义图像像素颜色,使用多达 256 种最佳选择颜色的调色板,而不是 3 个 RGB 颜色通道或 4 个 ARGB 颜色通道,每个通道包含 256 种颜色级别。同样,需要注意的是,一旦您使用 GIF 或 PNG8 编解码器将 24 位图像压缩为 8 位图像,您就只能使用原始的 16,777,216 种颜色中的(最多)256 种。这就是为什么我提倡使用 PNG24 或 PNG32 图像,而不是 GIF 或 PNG1 (2 色)、PNG2 (4 色)、PNG4 (16 色)或 PNG8 (256 色)图像,JavaFX 也支持这些图像。
根据任何给定的 24 位源图像中使用的颜色数量,使用 256 种颜色来表示最初包含 16,777,216 种颜色的图像可能会导致一种称为条带的效果。这是由于(通过压缩)得到的 256 色(或更少)调色板中相邻颜色之间的转换不是渐变的,因此看起来不是平滑的颜色渐变。索引的彩色图像有一个视觉校正条带的选项,称为抖动。
抖动是一种算法过程,它沿着图像中任何相邻颜色之间的边缘制作点图案,以欺骗眼睛看到第三种颜色。抖动会给你一个最大的颜色感知数量(65,536;256 × 256),但只有当这 256 种颜色中的每一种都与其他 256 种颜色中的每一种相邻时,才会出现这种情况。尽管如此,您仍然可以看到创建额外颜色的潜力,并且您会对索引颜色格式在某些压缩场景中(对于某些图像)可以达到的效果感到惊讶。
让我们拍摄一张真彩色图像,如图 5-3 所示,并将其保存为 PNG5 索引彩色图像格式,向您展示这种抖动效果。需要注意的是,虽然 PNG5 在 Android 和 HTML5 中受支持,但在 JavaFX 中不受支持,所以如果您自己做这个练习,请选择 2 色、4 色、16 色或 256 色选项!该图展示了奥迪 3D 图像中驾驶员侧后挡泥板上的抖动效果,因为它包含灰色渐变。
图 5-3。
A truecolor PNG24 image created with Autodesk 3ds Max, which you are going to compress as PNG5
有趣的是,在 8 位索引彩色图像中,使用小于 256 色的最大值是允许的。这通常是为了进一步减少影像的数据足迹。例如,仅使用 32 种颜色就可以获得良好效果的图像实际上是一个 5 位图像,从技术上来说,它被称为 PNG5,尽管这种格式本身通常被称为用于索引颜色使用级别的 PNG8。
我已经设置了这张索引色 PNG 图像,如图 5-4 所示,使用 5 位颜色(32 色,或 PNG5)来清晰地说明这种抖动效果。正如您在图像预览区域中看到的那样,在图的左侧,抖动算法在相邻的颜色之间制作点图案,以创建额外的颜色。
另外,请注意,您可以设置使用抖动的百分比。我经常选择 0%或 100%的设置;但是,您可以在这两个极端值之间的任何地方微调抖动效果。您也可以在抖动算法之间进行选择,因为正如您可能已经猜到的那样,抖动效果是使用抖动算法创建的,抖动算法是索引文件格式(在本例中为 PNG8)压缩例程的一部分。
我使用扩散抖动,这给了不规则形状的梯度一个平滑的效果,就像在汽车挡泥板上看到的那样。您也可以使用更随机的噪波选项,或者不太随机的模式选项。扩散选项通常给出最好的结果,这就是为什么我在使用索引色时选择它(这并不常见)。
正如你所想象的,抖动给图像增加了更难压缩的数据模式。这是因为对于压缩算法来说,图像中的平滑区域(如梯度)比尖锐过渡(边缘)或随机像素模式(如相机 CCD 的抖动或“噪声”)更容易压缩。
因此,应用抖动选项总是会增加几个百分点的数据占用空间。请务必检查应用和不应用抖动(在“导出”对话框中选择)的结果文件大小,以查看它是否值得提供更好的视觉效果。请注意,索引彩色 PNG 图像也有一个透明度选项(复选框),但 PNG8 图像中使用的 alpha 通道只有 1 位(开/关),而不是 PNG32 中的 8 位。
图 5-4。
Setting dithering to the diffusion algorithm and 32 colors (5 bit), with 100 percent dithering for PNG5 output
您还可以通过添加 alpha 通道来定义合成的透明度,从而增加图像的数据足迹。这是因为通过添加一个 alpha 通道,你将在被压缩的图像中添加另一个 8 位颜色通道(或者透明通道)。如果您需要 alpha 通道来定义图像的透明度,以支持未来的合成要求,例如将图像用作游戏精灵,那么除了包含 alpha 通道数据之外,没有太多选择。
如果您的 alpha 通道包含全零(或使用全黑填充颜色),这将定义您的图像为完全透明,或者包含全 FF 值(或使用全白填充颜色),这将定义您的图像为完全不透明,您将基本上(实际上)定义一个不包含任何有用的 alpha 数据值的 alpha。因此,需要移除透明图像,并且需要将不透明图像定义为 PNG24 而不是 PNG32。
最后,大多数用于遮罩数字图像 RGB 层中的对象的 alpha 通道应该能够很好地压缩。这是因为 alpha 通道主要是白色(不透明)和黑色(透明)的区域,在这两种颜色之间的边缘有一些中灰度值来反走样蒙版(见图 5-2 )。这些灰色区域包含 alpha 通道中的抗锯齿值,并将在图像的 RGB 层中的对象与任何背景颜色或可能在它后面使用的背景图像之间提供视觉上平滑的边缘过渡。
其原因是,在 alpha 通道图像蒙版中,8 位透明度渐变(从白到黑)定义了透明度级别,这可以被认为是每像素混合(不透明度)强度。因此,蒙版(包含在 alpha 通道中)中每个对象的边缘上的中灰度值将基本上用于平均对象边缘和任何目标背景的颜色,无论它可能包含什么颜色(或图像)值。这为任何可能使用的目标背景(包括动画背景)提供了实时抗锯齿。
数字视频和动画:帧、速率、循环、方向
有趣的是,我刚刚谈到的所有关于数字图像的概念同样适用于数字视频和动画,因为这两种格式都使用数字图像作为其内容的基础。数字视频和动画通过使用帧将数字成像扩展到第四维(时间)。这两种格式由有序的帧序列组成,随时间快速显示。
术语“帧”来自电影工业,在电影工业中,即使在今天,电影帧也以每秒 24 帧(24FPS)的速度通过电影放映机,这产生了运动的幻觉。因为数字视频和动画都是由包含数字图像的帧的集合组成的,所以当涉及到内存数据占用优化工作过程(对于动画资产)以及数字视频文件大小数据占用优化工作过程时,以每秒帧数表示的帧速率的概念也是非常重要的。如前所述,在 JavaFX 中,动画的这个属性存储在动画对象速率变量中(见第四章)。
关于动画对象或数字视频资源中的帧的优化概念与关于图像中的像素(数字图像的分辨率)的优化概念非常相似:使用的越少越好!这是因为动画或视频中的帧数会使每一帧所使用的系统内存和文件大小数据占用空间都成倍增加。在数码视频中,不仅每帧(图像)的分辨率,帧速率(在“压缩设置”对话框中指定)也会影响文件大小。在本章的前面,您已经了解到,如果您将图像中的像素数乘以其颜色通道数,您将获得图像的原始数据足迹。对于动画和数字视频,您现在将再次将该数字乘以需要用于创建运动错觉的帧数。
因此,如果您的游戏有一个动画 VGA (RGB)背景板(请记住,每帧为 900KB ),它使用五帧来创建运动的幻觉,那么您正在使用 900KB × 5 或 4,500KB (4.5MB)的系统内存来保存该动画。当然,这对于一个背景来说占用了太多的内存,这也是为什么你将使用静态背景和精灵覆盖来在不到一兆字节的空间内达到完全相同的效果。数字视频的计算有点不同,因为它有数百或数千帧。对于数字视频,您可以将原始图像数据大小乘以数字视频设置为回放的每秒帧数(帧速率)(此帧速率值在压缩过程中指定),然后将结果乘以视频文件中包含的内容持续时间的总秒数。
继续 VGA 示例,您知道 24 位 VGA 图像有 900KB。这使得将这带到下一个级别的计算变得容易。数字视频传统上以 30FPS 运行,因此 1 秒钟的标准清晰度原始(未压缩)数字视频将是 30 个图像帧,每个图像帧为 900KB,总数据量为 27,000KB!您可以看到为什么 MPEG-4 H.264 AVC 等视频压缩文件格式非常重要,它可以压缩数字视频所产生的大量原始数据。JavaFX 媒体包使用最令人印象深刻的视频压缩编解码器之一(“codec”代表 code-decode),它在 HTML5 和 Android 中也受支持,即前面提到的 MPEG-4 H.264 AVC(高级视频编解码器)。这对于开发人员资产优化非常方便,因为一个数字视频资产可以跨 JavaFX、HTML5 和 Android 应用使用。万一你想在你的游戏背景中使用数字视频(我不推荐),接下来我将讲述数字视频压缩和优化的基础知识。
数字视频压缩概念:比特率、数据流、标清、高清、UHD
让我们从商业视频中使用的主要或标准分辨率开始。这些也是常见的设备屏幕分辨率,可能是因为如果屏幕像素分辨率与屏幕上全屏播放的视频像素分辨率匹配,将会出现零缩放,这可能会导致缩放伪像。在高清晰度出现之前,视频是标准清晰度(SD),使用 480 像素的垂直分辨率。VGA 是标清分辨率,720 × 480 可以称为宽标清分辨率。高清(HD)视频有两种分辨率,1,280 × 720,我称之为伪高清,1,920×1080,业界称之为真高清。这两种高清分辨率都具有 16:9 的宽高比,用于电视和独立电视、智能手机、平板电脑、电子书阅读器和游戏控制台。现在还有一种超高清(UHD)分辨率,具有 4,096 × 2,160 像素。
视频流是一个比分辨率更复杂的概念,因为它涉及到在大范围内播放视频数据,例如 Java 8 游戏应用和远程视频数据服务器之间的视频数据,远程视频数据服务器将保存您潜在的大量数字视频资产。此外,运行 Java 游戏应用的设备将与远程数据服务器实时通信,在视频播放时接收视频数据包(之所以称为流,是因为视频从视频服务器通过互联网流入硬件设备)。MPEG-4 H.264 AVC 格式编解码器(编码器-解码器对)支持视频流。
你需要理解的最后一个概念是比特率。比特率是视频压缩过程中使用的关键设置,因为比特率代表您的目标带宽,或每秒钟能够容纳一定数量的比特流的数据管道大小。比特率设置还应该考虑任何给定的支持 Java 的设备中存在的 CPU 处理能力,使您的数字视频的数据优化更具挑战性。幸运的是,如今大多数设备都配备了双核或四核 CPU!
一旦比特通过数据管道,它们也需要被处理并显示在设备屏幕上。因此,数字视频资产的比特率不仅要针对带宽进行优化,还要考虑到 CPU 处理能力的变化。一些单核 CPU 可能无法在不丢帧的情况下解码高分辨率、高比特率的数字视频资产,因此,如果您打算将较旧或较便宜的消费电子设备作为目标,请确保优化低比特率视频资产。
数字视频数据足迹优化:使用编解码器及其设置
如前所述,您的数字视频资产将被压缩,使用称为编解码器的软件工具。视频编解码器有两个“方面”:一方面编码视频数据流,另一方面解码视频数据流。视频解码器将是使用它的操作系统、平台(JavaFX)或浏览器的一部分。解码器主要针对速度进行优化,因为回放的平滑度是一个关键问题,而编码器则针对减少其生成的数字视频资产的数据占用量进行了优化。因此,编码过程可能需要很长时间,这取决于工作站包含多少个处理核心。大多数数字视频内容制作工作站应该支持八个处理器内核,比如我的 64 位 AMD 八核工作站。
编解码器(编码器端)类似于插件,因为它们可以安装到不同的数字视频编辑软件包中,使它们能够编码不同的数字视频资源文件格式。因为 Java 和 JavaFX 8 支持 MPEG-4 H.264 AVC 格式,所以您需要确保您使用的数字视频软件包之一支持使用这种数字视频文件格式对数字视频数据进行编码。不止一家软件制造商生产 MPEG-4 编码软件,因此在编码速度和文件大小方面,会有不同的 MPEG-4 AVC 编解码器产生不同的(更好或更差的)结果。如果你想制作专业的数字视频,我强烈推荐你使用的专业解决方案是 Sorenson Squeeze Pro。
还有一个名为 EditShare LightWorks 12 的开源解决方案,计划到 2014 年原生支持输出到 MPEG4 编解码器。优化(设置压缩设置)数字视频数据文件大小时,有许多变量会直接影响数字视频数据的占用空间。我将按照对视频文件大小的影响,从最重要到最不重要的顺序来讨论这些参数,以便您知道调整哪些参数来获得您想要的结果。
与数字图像压缩一样,视频每帧的分辨率或像素数是开始优化过程的最佳位置。如果您的用户使用 800 × 480 或 1,280 × 720 的智能手机、电子阅读器或平板电脑,那么您不需要使用真正的高清 1,920 × 1,080 分辨率来获得数字视频资产的良好视觉效果。有了超高密度(小点距)显示器,你可以将 1,280 的视频放大 33 %,看起来相当不错。这种情况的例外可能是高清或 UHD(通常称为 4K 独立电视)游戏的目标是独立电视;对于这些巨大的 55 至 75 英寸(屏幕)场景,您可能希望使用行业标准的真正高清 1,920 × 1,080 分辨率。
假设数字视频本身的实际秒数无法缩短,下一个优化级别将是每秒视频使用的帧数(或 FPS)。如前所述,这就是所谓的帧速率,与其设置视频标准 30FPS 帧速率,不如考虑使用电影标准 24FPS 帧速率,甚至多媒体标准 20FPS 帧速率。您甚至可以使用 15FPS 的帧速率,这是视频标准的一半,具体取决于内容中的移动量(和速度)。请注意,15FPS 的数据量是 30FPS 的一半(编码数据减少了 100%)。对于某些视频内容,这将与 30FPS 内容一样回放(看起来)。对此进行测试的唯一方法是在编码过程中尝试帧速率设置。
获得较小数据占用空间的下一个最佳设置是您为编解码器设置的比特率。比特率等于所应用的压缩量,因此设定了数字视频数据的质量水平。值得注意的是,您可以简单地使用 30FPS、1,920 分辨率的高清视频,并指定低比特率上限。如果您这样做,结果将不会像您尝试使用较低的帧速率和分辨率以及较高(质量)的比特率设置那样好看。对此没有固定的规则,因为每个数字视频资源都包含完全唯一的数据(从编解码器的角度来看)。
获得较小数据占用空间的第二个最有效的设置是编解码器用来对数字视频进行采样的关键帧的数量。视频编解码器通过查看每一帧,然后在接下来的几帧中仅对变化或偏移进行编码来应用压缩,从而编解码器算法不必对视频数据流中的每一帧进行编码。这就是为什么会说话的头部视频比每个像素都在每帧上移动的视频(如使用快速摄像机平移或快速视野[FOV]缩放的视频)编码更好。
关键帧是编解码器中的一项设置,它强制编解码器不时对您的视频数据资源进行新的采样。关键帧通常有一个自动设置,它允许编解码器决定采样多少关键帧,还有一个手动设置,它允许您指定关键帧采样的频率,通常是每秒特定次数或整个视频的持续时间(总帧数)。
大多数编解码器通常具有质量或清晰度设置(滑块),用于控制压缩前应用到视频帧的模糊量。如果您不熟悉这个技巧,对图像或视频应用轻微的模糊(这通常是不可取的)可以实现更好的压缩,因为图像中的尖锐过渡(边缘)比柔和过渡更难编码,需要更多的数据来再现。也就是说,我会将质量(或清晰度)滑块保持在 80%到 100%之间,并尝试使用我在这里讨论的其他变量之一来减少您的数据占用量,例如降低分辨率、帧速率或比特率。
最终,对于任何给定的数字视频数据资产,您都需要微调许多不同的变量,以实现最佳的数据占用空间优化。重要的是要记住,对于数字视频编解码器来说,每个视频资源看起来都是不同的(数学上)。由于这个原因,不可能有可以被开发来实现任何给定压缩结果的标准设置。也就是说,随着时间的推移,调整各种设置的经验最终会让您感受到,为了获得所需的最终结果,您必须根据不同的压缩参数来更改各种设置。
数字音频概念:振幅、频率、样本
你们这些音响发烧友知道,声音是通过在空气中发送声波脉冲而产生的。数字音频很复杂;这种复杂性的一部分来自于需要将使用扬声器纸盆创建的模拟音频技术与数字音频编解码器连接起来。模拟扬声器通过脉冲产生声波。我们的耳朵以完全相反的方式接收模拟音频,捕捉和接收那些空气脉冲或不同波长的振动,然后将它们转换回我们大脑可以处理的数据。这就是我们“听到”声波的方式;然后,我们的大脑将不同的音频声波频率解释为不同的音符或音调。
声波产生各种音调,这取决于声波的频率。宽的或不常见的(长的)波产生低音,而更频繁的(短的)波长产生高音。有趣的是,不同频率的光会产生不同的颜色,所以模拟声音(音频)和模拟光(颜色)有着密切的相关性。你很快就会看到,数字图像(和视频)之间还有许多其他相似之处,这些相似之处也将贯穿到你的数字新媒体内容制作中。
声波的音量将由其振幅或波的高度(或大小)决定。因此,如果你在 2D 观察,声波的频率等于声波在 x 轴上的间距,振幅等于声波在 y 轴上的高度。
声波可以独特地成形,允许它们“搭载”各种声音效果。一种“纯”或基线类型的声波称为正弦波(您在高中三角学中学过,使用正弦、余弦和正切数学函数)。熟悉音频合成的人都知道,其他类型的声波也用于声音设计,例如锯齿波,它看起来像锯子的边缘(因此得名),以及脉冲波,它仅使用直角进行整形,产生即时的开和关声音,转换为音频脉冲(或突发)。
甚至在声音设计中使用随机波形(如噪声)来获得尖锐的声音效果。通过使用最近获得的数据足迹优化知识,您可能已经确定,声波(以及一般的新媒体数据)中出现的“混乱”或噪声越多,编解码器就越难压缩。因此,由于数据中的混乱,更复杂的声波将导致更大的数字音频文件。
将模拟音频转换为数字音频数据:采样、精度、高清音频
将模拟音频(声波)转换为数字音频数据的过程称为采样。如果你在音乐行业工作,你可能听说过一种叫做采样器的键盘(甚至是架装式设备)。采样是将模拟音频波分割成片段的过程,以便您可以使用数码音频格式将波形存储为数码音频数据。这就把无限精确的模拟声波变成了离散的数字数据,也就是说,变成了 0 和 1。使用的 0 和 1 越多,无限精确(原始)模拟声波的再现就越精确。
采样的音频声波的每个数字段称为样本,因为它在精确的时间点对声波进行采样。您想要的采样精度(分辨率)将决定用于再现模拟声波的 0 和 1 的数量,因此采样精度由用于定义每个波切片高度的数据量决定。与数字成像一样,这种精度被称为分辨率,或者更准确地说(没有双关语),样本分辨率。采样分辨率通常用 8 位、12 位、16 位、24 位或 32 位分辨率来定义。游戏大多利用 8 位分辨率来实现爆炸等效果,清晰度在这些效果中并不重要;12 位分辨率,用于清晰的口语对话和更重要的音频元素;背景音乐可能是 16 位分辨率。
在数字成像和数字视频中,分辨率由像素数来量化,而在数字音频中,则由用于定义每个模拟音频样本的数据位数来量化。同样,与数字成像一样,像素越多,质量越好,而数字音频的样本分辨率越高,声音再现越好。因此,更高的采样分辨率,使用更多的数据来再现给定的声波样本,将产生更高质量的音频回放,代价是更大的数据足迹。这就是 16 位音频(通常称为 CD 音质音频)听起来比 8 位音频更好的原因。根据所涉及的音频,12 位音频可能是一个很好的折衷方案。
在数字音频领域,有一种新型的音频样本,在消费电子行业被称为高清音频。HD 数字音频广播电台使用 24 位样本分辨率,因此每个音频样本或声波片段包含 16,777,216 位样本分辨率。一些较新的硬件设备现在支持高清音频,如你看到的智能手机广告中的“高清音频”,这意味着它们有 24 位音频硬件。如今,笔记本电脑(包括 PC)以及游戏机和 ITV 也标配了 24 位音频播放硬件。
需要注意的是,对于 Java 8 游戏来说,HD 音频可能不是必需的,除非您的游戏是面向音乐的,并且使用了高质量的音乐,在这种情况下,您可以通过 WAVE 文件格式使用 HD 音频样本。
另一个考虑因素是数字音频采样频率(也称为采样速率),它衡量在 1 秒的采样时间帧内,以特定采样分辨率采样的数量。就数字图像编辑而言,采样频率类似于数字图像中包含的颜色数量。您可能很熟悉“CD 音质音频”这个术语,它被定义为使用 16 位采样分辨率和 44.1kHz 采样速率(采集 44,100 个样本,每个样本具有 16 位采样分辨率,即 65,536 位音频数据)。您可以通过将采样比特率乘以采样频率,再乘以音频片段中的秒数,来确定音频文件中的原始数据量。显然,这可能是一个巨大的数字!音频编解码器在优化数据方面非常出色,数据占用空间非常小,音质损失非常小。
因此,数字图像和数字视频中存在的完全相同的权衡也发生在数字音频中:包含的数据越多,结果质量越高,但总是以更大的数据足迹为代价。在视觉媒体中,数据覆盖区的大小是使用色深、像素来定义的,在数字视频和动画的情况下,还使用帧来定义。在听觉媒体中,它是通过采样分辨率结合采样率来定义的。数字音频行业目前最常见的采样率包括 8kHz、22kHz、32kHz、44.1kHz、48kHz、96KHz、192kHz,甚至 384kHz。
较低的采样率,如 8kHz、11kHz 和 22kHz,是您将在游戏中使用的采样率,因为经过精心优化,这些采样率可以产生高质量的音效和街机音乐。这些速率对于采样任何基于语音的数字音频也是最佳的,例如电影对白或电子书旁白轨道。较高的音频采样速率(如 44.1kHz)更适合音乐,需要高动态范围(高保真)的声音效果(如隆隆雷声)可以使用 48kHz。更高的采样率将允许音频再现展示电影院(THX)的声音质量,但这不是大多数游戏所要求的。
数字音频流:专属音频与流音频
与数字视频数据一样,数字音频数据既可以包含在应用分发文件(对于 Java,是. JAR 文件)中,也可以使用远程数据服务器进行流式传输。与数字视频类似,流式数字音频数据的优势在于它可以减少应用文件的数据占用量。缺点是可靠性。许多相同的概念同样适用于音频和视频。流式音频将节省数据空间,因为您不必在中包含所有繁重的新媒体数字音频数据。JAR 文件。所以,如果你计划编写一个自动点唱机应用,你可能想考虑流式传输你的数字音频数据;否则,请尝试优化您的数字音频数据,以便您可以将它们(受控)包含在中。JAR 文件。这样,当应用的用户需要时,数据总是可用的!
流式数字音频的缺点是,如果用户的连接(或音频数据服务器)中断,您的数字音频文件可能并不总是呈现给最终用户使用您的游戏应用播放和收听!数字音频数据的可靠性和可用性是流传输和捕获之间权衡的另一个关键因素。这同样适用于数字视频资产。
同样,与数字视频一样,数字音频流的一个主要概念是数字音频数据的比特率。正如您在上一节中了解到的,比特率是在压缩过程中定义的。需要支持较低比特率带宽的数字音频文件将对音频数据进行更多的压缩,这将导致较低的质量。这些将在更多的设备上更平滑地传输(回放),因为更少的比特可以更容易地快速传输和处理。
JavaFX 中的数字音频:支持的数字音频编解码器和数据格式
JavaFX 中的数字音频编解码器比数字视频编解码器多得多,因为只有一种视频编解码器,即 MPEG-4 H.264 AVC。Android 音频支持包括. MP3 (MPEG-3)文件,Windows WAVE(脉码调制[PCM]音频)。WAV 文件、. MP4(或. M4A) MPEG-4 AAC(高级音频编码)音频和 Apple 的 AIFF (PCM)文件格式。JavaFX(以及 Java 8)支持的最常见的格式是流行的. MP3 数字音频文件格式。由于音乐下载网站,如 Napster 或 Soundcloud,你们大多数人都熟悉 MP3 数字音频文件,我们大多数人收集这种格式的歌曲用于 MP3 播放器和基于 CD-ROM 或 DVD-ROM 的音乐收藏。MP3 数字音频文件格式很受欢迎,因为它具有相当好的压缩比和质量,并得到广泛支持。
在 Java 8 应用中,MP3 是一种可以接受的格式,只要使用最佳的编码工作流程,尽可能获得最高的质量水平。值得注意的是,像 JPEG(用于图像)一样,MP3 是一种有损音频文件格式,其中一些音频数据(以及质量)在压缩过程中被丢弃,并且无法恢复。
JavaFX 有两种无损音频压缩编解码器,AIFF 和 WAVE。你们中的许多人对这些都很熟悉,因为它们分别是苹果 Macintosh 和微软 Windows 操作系统使用的原始音频格式。这些文件使用 PCM 音频,这是无损的(在这种情况下,因为没有应用任何压缩!).“PCM”,如上所述,代表“脉冲编码调制”,指的是它所保存的数据格式。
PCM 音频通常用于 CD-ROM 内容以及电话应用。这是因为 PCM WAVE audio 是一种未压缩的数字音频格式,没有对数据流应用 CPU 密集型压缩算法。因此,解码(CPU 数据处理)对于电话设备或 CD 播放器来说不是问题。
因此,当您开始将数字音频资产压缩成各种文件格式时,您可以使用 PCM 作为基准文件格式。它允许您查看 PCM (WAVE)与 MP3 和 MP4 音频压缩结果之间的差异,以了解您的 JAR 文件获得了多少数据占用优化;更重要的是,你还可以看到你的采样分辨率和采样频率优化将如何影响游戏音频效果所使用的系统内存。即使您使用 MP3 或 MP4 格式,在音频资产可以与 AudioClip 类一起使用并在 Java 8 游戏中用作声音效果之前,仍然必须将其解压缩到内存中。
因为 WAVE 或 AIFF 文件不会有任何质量损失(因为也不需要解压缩),PCM 数据可以直接从 JAR 文件放入系统内存!这使得 PCM 音频非常适合持续时间短(0.1 到 1 秒)的游戏音效,并且可以高度优化,使用 8 位和 12 位采样分辨率以及 8kHz、22kHz 或 32kHz 采样频率。最终,对于任何给定的数字音频数据,要找出 JavaFX 支持的哪种音频格式具有最佳的数字音频压缩结果,唯一真正的方法是使用您知道受支持且高效的主要编解码器对您的数字音频进行编码。稍后我将概述这一工作过程,当您向游戏添加音频时,您将使用相同的源音频样本观察不同格式之间的相对数据足迹结果(参见第一章 5)。然后,您将听取音频播放质量,这样您就可以做出关于质量和文件大小之间最佳平衡的最终决定。这是开发 JavaFX 数字音频资产以在 Java 8 游戏中使用的工作流程。
JavaFX 还支持流行的 MPEG-4 AAC 编解码器。这些数字音频数据可以包含在 MPEG4 容器(. mp4、. m4a、. m4v)或文件扩展名中,并且可以使用任何操作系统回放。值得注意的是,JavaFX 不包含 MPEG-4 解码器,而是支持所谓的多媒体容器,这意味着 JavaFX 使用操作系统的 MPEG-4 解码器。
出于这个原因,并且因为在线听力研究已经得出结论,MP3 格式比 MP4 具有更好的质量(对于音乐),您将通过 Media 和 MediaPlayer 类使用 MP3 格式来获得更长形式的音频(游戏背景音乐循环)。您将通过 JavaFX 慷慨提供的 audio clip digital audio sequencing engine(class)使用 PCM WAVE audio 格式的短格式(1 秒或更短)音频(游戏音效,如枪声、铃声、叫喊声、咕哝声、笑声、欢呼声和其他此类数字音频资产)。
数字音频优化:从 CD 质量的音频开始,然后反向工作
优化您的数字音频资产以便在市场上最广泛的硬件设备上播放,将比优化您的数字视频或数字图像(以及动画)更容易。这是因为目标屏幕分辨率和显示器宽高比之间的差异比硬件设备之间的数字音频播放硬件支持类型之间的差异大得多(具有 24 位高清音频播放硬件兼容性的新硬件可能除外)。所有硬件都可以很好地播放数字音频资产,因此音频优化是一个“一个音频资产影响所有设备”的场景,而对于视觉(视频、图像、动画)部分,您可以看到大到 4,096 × 2,160 像素(4K iTV 电视机)和小到 320 × 320 像素(翻盖手机和智能手表)的显示屏。
重要的是要记住,用户的耳朵不能感知数字音频的质量差异,而用户的眼睛可以感知数字图像、2D 动画和数字视频的质量差异。一般来说,对于 Java 游戏音频的支持,在所有硬件设备上有三个主要的数字音频支持“最佳点”。
通过使用 8kHZ 或 22kHz 的采样速率以及 8 位或 12 位的采样分辨率,较低质量的音频(如短旁白轨道、人物感叹词和短时声音效果)可以获得非常高的质量。中等质量的音频,如长旁白轨道、持续时间较长的声音效果和循环背景(环境)音频,通过使用 22kHz 或 32kHz 采样速率以及 12 位或 16 位采样分辨率,可以达到非常高的质量水平。
高质量的音频资产,如音乐,应进行优化,以接近 CD 质量的音频,并将使用 32kHz 或 44.1kHz 的采样速率,以及 16 位数据采样分辨率。对于处于音频频谱超高端的高清质量音频,您可以使用 48kHz 采样速率和 24 位数字音频数据采样分辨率。还有一种未命名的“中间某处”高端音频规格,使用 48kHz 采样率,以及 16 位数据采样分辨率,这恰好是杜比 THX 过去在电影院使用的高端音频体验技术(过去)。
最终,它归结为数字音频数据足迹优化工作流程中出现的质量-文件大小平衡结果,这可能是惊人的。因此,在所有这些硬件设备上优化数字音频资产的初始工作流程是创建 44.1kHz 或 48kHz 的基准 16 位资产,然后使用 JavaFX 支持的不同格式对其进行优化(压缩)。一旦工作流程完成,您就可以看到哪些数字音频资产提供了最小的数据占用空间,以及最高质量的数字音频回放。之后,您可以将 44.1KHz 或 48kHz 数据降低到 32kHz 并保存下来,首先使用 16 位分辨率,然后使用 12 位分辨率。接下来,重新打开原始的 48kHz 数据,下采样至 22kHz 采样频率,并使用 16 位和 12 位分辨率导出该数据,依此类推。稍后,当您将数字音频资产添加到 Java 8 游戏中时,您将执行此工作流程,因此您将看到整个流程(参见第一章 5)。
接下来,我们来看看 JavaFX Scene Builder,以及它是如何使用 FXML 来让设计人员可视化地设计 JavaFX 应用的。在本书的课程中,我不会使用 Scene Builder 或 FXML(只是 Java 8 代码和 JavaFX 类),所以请注意!
JavaFX 场景生成器:使用 FXML 进行 UI 设计
JavaFX Scene Builder 是一个可视化设计工具,可生成 FXML (JavaFX 标记语言)UI 场景图结构,以定义 JavaFX 应用的前端设计。然后,可以在 Java 8 中“膨胀”这个 FXML UI 定义,以创建应用的 JavaFX 场景图、节点、组和 SubScene 对象,其中填充了定义 UI 设计的 javafx.scene.control 包类(对象)。Oracle 提供 Scene Builder 可视化开发工具和 FXML 的目的是允许非程序员(表面上是 UI 设计师)为他们的 Java 8 应用设计前端 UI,以便 Java 程序员可以专注于后端功能应用任务处理逻辑。
因为 FXML 和 Scene Builder 针对 UI 设计进行了优化(排列控件,如按钮、文本输入字段、单选按钮、复选框等),所以在本书的整个过程中,我不打算使用 Scene Builder 和 FXML。不过,我将在本章中介绍它,以便您知道如何在其他 JavaFX 应用中使用它。我的理由是,除了最初的游戏闪屏,它包含一些 UI 按钮对象,显示游戏说明,列出贡献者,跟踪高分,保存当前游戏状态,并开始玩游戏,UI 设计不会是本书的主要焦点。
要使用 FXML,在使用 Scene Builder 可视化 UI 设计工具后不久,您必须创建一种特殊的 FXML 应用,正如您在第二章(见图 2-4)中创建 JavaFX 游戏时所学。创建 FXML 应用会导入 javafx.fxml 包和类。这允许 Java 8 代码膨胀由 UI 设计者创建的 FXML 结构,以便程序员可以使用它们将 Java 逻辑附加到各种 UI 控件。Android 操作系统也是这样做的,使用基本的 XML,但是在 Android 中这种方法不是可选的;这是做事方式的一部分。在 JavaFX 8 中(如图 2-4 所示),它只是一个选项。如果你想进一步研究基于 XML 的 Android UI 设计,可以看看我的书 Pro Android UI (Apress,2014)。
为您编写 FXML UI 设计构造的 Scene Builder 可视化布局工具是一个所见即所得的拖放界面设计工具。设计者所要做的就是将任何 JavaFX UI 控件从包含 javafx.scene.control 包中每个控件类(对象)的 UI 控制面板拖放到编辑屏幕上(参见第四章)。这个场景生成器集成到 NetBeans 8.0 中,以便于访问和与 JavaFX 集成,以防程序员也需要使用它来快速为他们的客户端设计 UI 原型。对于不想在 NetBeans IDE 中工作的设计人员,还有一个独立版本的场景构建器工具,版本 2.0。
您可以在 FXML 编辑和预览之间实时切换,查看 UI 设计和布局的变化,而无需编译 Java 应用。您还可以将所有 CSS 样式实时应用于场景构建器工具和 FXML 结构,并查看这些代码更改的结果,同样,无需任何 Java 编译!此外,您可以使用第三方 JAR 文件或 FXML 定义将自定义 UI 控件添加到 UI 控制面板库中。
场景构建器工具包 API 是开源的。这使您可以自定义 Scene Builder 的 UI 面板和控件的集成,允许您将 Scene Builder 集成到其他 ide 中,如 Eclipse 或 IntelliJ IDEA。GUI 组件(控件)库中最近添加了一个富文本 TextFlow 容器,提供了富文本编辑功能。有了这些新功能,您可以构建多节点、富文本结构,将其他 UI 元素或新的媒体元素类型与 TextFlow 元素集成在一起。
对于 3D“发烧友”来说,Scene Builder 可视化设计编辑器和 FXML 也完全支持 3D。可以使用场景构建器工具加载甚至保存 3D 对象,并且可以使用检查器面板实时编辑(和查看)对象的所有属性。现在还不能使用 Scene Builder 从头开始创建 3D 对象,您也不能在此时分配或编辑复杂的网格或材质属性,但我相信这些功能会随着计划添加到 JavaFX 8 中的高级 3D OpenGL ES 功能一起出现。
接下来,让我们深入了解一下 FXML 标记语言。之后,您将检查一个实际的 FXML UI 定义结构,并且您将确切地看到当前 JavaFX 应用的 UI 设计是如何使用 FXML UI 定义构造的。正如您将看到的,FXML 使 UI 设计变得更加容易!
FXML 定义:XML UI 定义结构的剖析
FXML 结构基于 JavaFX 类(对象)及其属性,FXML 标记和参数结构(您可以轻松创建)允许您使用标记语言更轻松地“模拟”前端 UI。FXML 结构使您可以更容易地构建场景图层次,FXML 标记及其参数(您将在下一节中看到)与 JavaFX API 类 1:1 匹配。
一旦创建了 UI 设计,Java 程序员就可以使用 javafx.fxml 类和方法,根据 Java 对象将 UI 布局容器和 UI 控件排列扩展到 javafx 场景和场景图结构中。然后,可以在应用 Java 代码中使用 UI 设计。如前所述,FXML 对于设计包含大量按钮、表单、复选框等的复杂、静态(固定)UI 设计布局非常有用。
Hello World UI FXML 定义:使用 FXML 复制当前的 UI 设计
在 FXML 结构中定义的第一件事是 FXML 处理指令。每个处理指令都以小于号、问号序列() and ends with the reversal of that sequence (question mark, greater-than sign [?>)开始。第一个处理指令声明了 XML 语言的用法、版本(1.0)和要使用的文本字符集语言编码格式(在本例中,是 UTF-8[通用字符集转换格式,8 位])。因为它是 8 位的,所以在这个国际字符集中有 256 个字符,它被设计成跨越许多基于日耳曼字符的语言,即使用 A 到 Z 字母表(包括重音字符)的语言。
XML 语言和字符集声明之后是处理指令。它们导入 Java 语言、实用程序和 javafx.scene 包以及 javafx.scene.layout 和 javafx.scene.control 包,这些包用于设计 UI 布局和布局包含的 UI 控件。
例如,您在当前应用中使用的 StackPane UI 布局容器位于 javafx.scene.layout 包中,而 button UI 控件元素位于 javafx.scene.control 包中。因为 FXML UI 布局容器是这个结构中的父元素,所以它首先出现,或者在您将要创建的嵌套 FXML UI 定义结构之外。
在中,您将嵌套 StackPane 类(object)的子类,使用<子类>标签(XML 标签使用<箭头括号>编码)。嵌套在这些<子>标签中的是 UI 控件元素(在本例中,是一个按钮控件,所以您将使用<按钮>标签)。请注意,在箭头括号内使用了类(对象)的专有名称来创建 FXML 标签,因此这是非常符合逻辑的,应该很容易学习并融入到您的 UI 设计工作过程中:
<?``xml``version="1.0" encoding="``UTF-8
<? import java.lang.* ?><? import java.util.* ?><? import javafx.scene.* ?><? import javafx.scene.layout.* ?><? import javafx.scene.control.* ?>
<``StackPane``id="root" prefHeight="250" prefWidth="300"
<children>
<``Button``id="btn" text="Say 'Hello World'" layoutX="125" layoutY="116"
</children></StackPane>
接下来,让我们看一下标签和参数语法,以便您总是知道如何构造 FXML UI 布局和控件定义文件。一个没有子元素的 UI 元素,比如之前的 UI 控件,将使用一个简写的标签开闭语法,使用<类名开始标签和正斜杠,大于号结束标签(/ >,像这样:
<Button``id="btn" text="Say 'Hello World'" layoutX="125" layoutY="116"
请注意,配置标记的参数(等同于对象的属性(或创建对象的类中的变量))嵌套在标记本身中,并使用变量名和等于运算符,以及引号中指定的数据值,如前面的代码所示。
嵌套了对象的 FXML 标签将使用这个不同的<类名>开始标签。在这个标签内(后)列出的嵌套标签之后是一个< /ClassName >结束标签。这允许标记语法指定(成为)其内部的<子>标记的容器,正如您在这里的示例中看到的,其中开始和结束 FXML 标记根据它们的嵌套(内部)层次结构进行排序:
<StackPane``id="root" prefHeight="250" prefWidth="300"
<children>
<Button``id="btn" text="Say 'Hello World'" layoutX="125" layoutY="116"
</children></StackPane>
如您所见,可以将参数放在父标记的开始标记中,方法是将它们放在开始标记的< ClassName 部分和大于号之间。如果需要的话,这就是为任何参数配置父标记的方式,就像指定 StackPane 大小和名称(在 FXML 中称为 id)一样。
摘要
在第五章中,您仔细了解了一些更重要的游戏设计和新媒体概念,您将在 Java 8 游戏开发工作流程中使用这些概念,这样您就拥有了创建游戏所必需的基础知识。您还学习了 JavaFX Scene Builder 和 FXML,只是为了掌握这些概念,因为我将使用 Java 8 代码和 JavaFX 类来完成本书中的所有工作,以满足我的 Android 书籍读者的要求(“我们如何仅使用 Java 代码来完成这些工作?我们不想使用 XML 来创建我们的应用!”是我这几天不断听到的口头禅)。
首先,您研究了静态与动态的关键概念,以及这对游戏设计和游戏优化的重要性,因为如果游戏优化不是游戏设计、开发和优化过程中的一个持续考虑因素,过多的动态会使旧的单核甚至双核 CPU 过载。
接下来,您探索了游戏设计(和开发)的一些关键组件,如精灵、碰撞检测、物理模拟、背景动画、UI 设计、评分引擎和游戏逻辑。您看了一下这些如何应用于静态游戏,或没有连续运动的游戏,如策略游戏、棋盘游戏、谜题、知识游戏、记忆游戏和动态游戏,以及使用连续运动的游戏,如平台游戏、街机游戏、第一人称射击游戏、第三人称射击游戏、驾驶游戏等。
您对新媒体资产类型以及数字图像、动画、数字视频和数字音频的概念和术语进行了高水平的技术概述。你学习了像素;决议;以及纵横比如何定义图像、动画或视频的形状,以及颜色深度和 alpha 通道透明度,以及如何使用十六进制表示法定义这些内容。然后,您研究了时间的第四维度,了解了帧、帧速率和比特率,并查看了数字音频、采样频率和采样分辨率。最后,您学习了 JavaFX 场景图和 FXML 如何工作以及如何在您当前的游戏中使用它们。
在下一章中,您将研究 JavaFX 场景图并为 Java 8 游戏应用创建基础设施,包括闪屏(游戏的主屏幕和主 UI)。
六、游戏设计的基础:JavaFX 场景图和InvinciBagel
游戏基础设施
在这一章中,你将开始从用户界面(UI)和用户体验的角度,以及从“引擎盖下”游戏引擎、精灵引擎、碰撞引擎和物理引擎的角度来设计你的 InvinciBagel 游戏的基础设施。你将记住优化,因为你必须通过这本书的其余部分工作,所以你不会得到一个如此广泛或复杂的场景图,脉冲系统不能有效地更新一切。这意味着保持主要的游戏 UI 屏幕(场景或子场景节点)最少(三个或四个);确保 3D 和媒体引擎(数字音频和数字视频)使用自己的线程;并检查驱动游戏的功能性“引擎”都是逻辑编码的,使用它们自己的类和适当的 Java 8 编程约定、结构、变量、常数和修饰符(见第三章)。
首先,您将了解游戏将为用户提供的顶级、正面 UI 屏幕设计,包括用户在启动应用时看到的 InvinciBagel“品牌”闪屏。该屏幕上有按钮控件,用于访问其他信息屏幕,您可能希望尽量减少这些信息屏幕的数量,因为它们可能是场景节点(主游戏屏幕)或 ImageView 节点(其他信息屏幕)。这些游戏支持屏幕将包含用户为了有效地玩游戏而需要知道的东西,例如游戏说明和高分屏幕。您还将包括一个法律免责声明屏幕(以使您的法律部门满意),其中还将包含为创建游戏引擎和游戏资产而工作的各种程序员和新媒体工匠的学分。
您将开发的 InvinciBagel 游戏设计基础的下一个级别是 InvinciBagel 游戏的底层或背面(游戏用户看不到),游戏引擎组件 Java 类设计方面。其中包括一个游戏引擎,它将使用一个 Java FX . animation . animation timer 类来控制对游戏界面屏幕的游戏更新;一个精灵引擎,它将使用 Java 列表数组和集合来管理游戏精灵;碰撞引擎,当两个精灵之间发生碰撞时,它将进行检测并做出响应;一个物理引擎,它将把力和类似的物理模拟应用到游戏中,以便精灵加速并逼真地对重力做出反应;和一个演员引擎,它将管理 InvinciBagel 游戏中每个演员的特征。
最后,您将修改现有的 InvinciBagel.java 应用子类,为游戏播放屏幕和其他三个功能信息屏幕实现一个新的闪屏和按钮,为 InvinciBagel 游戏应用提供这些顶级 UI 特性和基础 UI 屏幕基础结构。这将最终让你进入一些 Java 和 JavaFX 编程,因为你为游戏创造了基础。
游戏设计基础:主要功能屏幕
你要设计的第一件事就是你的游戏用户将与之交互的顶级或者最高级别的用户界面。这些都将通过包含在主要 InvinciBagel 船级社代码中的 InvinciBagel splash(品牌)屏幕进行访问。如前所述,此 Java 代码将扩展 javafx.application.Application 类,并将启动应用,显示其闪屏,以及查看指令、玩游戏、查看高分或查看游戏的法律免责声明和游戏创作者(程序员、艺术家、作家、作曲家、声音设计师等)的选项。在图 6-1 中可以看到一个显示游戏的高级图表,从顶部的功能 UI 屏幕开始,向下发展到操作系统级别。
图 6-1。
Primary game functional screens and how they are implemented through Java and JavaFX API, using a JVM
这将需要向 StackPane 布局容器父节点添加另外三个按钮节点,并为闪屏背景图像容器添加一个 ImageView 节点。必须首先将 ImageView 节点添加到 StackPane 中,使其成为 StackPane 中的第一个子节点(z-order = 0),因为这个 ImageView 包含我称之为闪屏 UI 设计的背景板。因为它在背景中,所以图像需要在按钮 UI 控件元素的后面,这些元素的 z 顺序值为 1 到 4。
这意味着您将在应用的场景图中使用六个节点对象(一个父节点和五个子节点)来创建 InvinciBagel 闪屏!说明和信用屏幕将使用另一个 ImageView 节点,因此您已经有了六个节点,高分屏幕可能会使用另外两个(ImageView 和 TableView)节点,因此在您考虑为游戏屏幕添加节点之前,您可能会在场景图中有八个以上的节点来创建游戏支持基础结构,当然,这是您希望游戏获得最佳性能的地方。
如果你考虑一下,这真的没有那么糟糕,因为这些屏幕都是静态的,不需要更新,也就是说,它们包含的(UI)元素是固定的,不需要使用 脉冲系统更新,所以你应该仍然有 JavaFX 脉冲引擎的 99%的能力来处理 InvinciBagel 游戏 GamePlayLoop 引擎。事实上,随着 Java 8 和 JavaFX 8 继续提高其平台 API 和类的效率,您实际上可能会有更多的处理能力用于游戏(精灵运动、碰撞、物理、动画等),因此将处于良好的状态。
GamePlayLoop 将使用 javafx.animation 包及其 AnimationTimer 类为您处理游戏代码。你总是需要知道你要求脉冲引擎处理多少场景图节点对象,因为,如果这个数字变得太大,它将开始影响游戏的性能。
Java 类结构设计:游戏引擎支持
接下来,让我们来看看 InvinciBagel 游戏的功能结构将如何被整合,也就是说,在你的 Java 8 游戏编程代码中,这就是本书的全部内容!正面 UI 屏幕的外观和底层编程逻辑的外观之间确实没有关联,因为大多数编程代码都将用于在游戏屏幕上创建游戏体验。游戏说明和法律和信用屏幕将只是图像(ImageView),并将在图像中嵌入文本(导致使用更少的场景图形节点)或在 ImageView 的顶部合成一个透明的 TextView。高分屏幕将需要一点编程逻辑,这将在游戏开发的最后完成,因为游戏逻辑必须被创建和播放,才能在第一时间产生高分(参见第十七章)!
图 6-2 显示了完成 InvinciBagel 游戏所需的主要功能区组件。该图显示了位于层次顶部的 InvinciBagel 应用子类,它创建了顶层和场景,以及包含在它下面(或里面)的场景图。
图 6-2。
Primary game functional classes and how they are implemented under the Scene and Scene Graph levels
在 InvinciBagel 场景对象(实际上是在 InvinciBagel 应用子类中创建的)下面,是功能类的更广泛的结构设计,您将需要在本书的剩余部分中编写代码。图中所示的引擎(类)将创建您的游戏功能,如游戏引擎(游戏循环)、逻辑引擎(游戏播放逻辑)、精灵引擎(演员管理)、演员引擎(演员属性)、得分引擎(游戏得分逻辑)、动画引擎(动画逻辑)、碰撞检测和物理模拟。您将必须创建所有这些 Java 类函数来完全实现 InvinciBagel 游戏的全面的 2D 游戏引擎。
我称之为 GamePlayLoop 类的游戏引擎是创建 AnimationTimer 对象的主要类,该对象调用连续处理游戏循环的脉冲事件。如您所知,这个循环将调用。handle()方法,该方法又包含方法调用,这些方法调用最终将访问您将创建的用于管理 actors (sprite 引擎)的其他类;在屏幕上移动它们(演员引擎);检测任何碰撞(碰撞引擎);检测到碰撞后应用游戏逻辑(逻辑引擎);并应用物理的力量来为游戏提供逼真的效果,如重力和加速度(物理引擎)。
从第七章开始,你将构建这些不同的引擎,它们将被用来创造游戏体验。我将根据每个引擎和它们需要做什么来对章节主题进行分层,这样从学习和编码的角度来看,一切都是有逻辑的。
JavaFX 场景图设计:最小化 UI 节点
最小化场景图的技巧是使用尽可能少的节点来实现一个完整的设计,如图 6-3 所示,这可以通过一个 StackPane 根节点、一个 VBox 分支(父)节点和七个叶(子)节点(一个 TableView、两个 ImageView 和四个按钮 UI 控件)来实现。当你接下来开始编码场景图时(最后!),您将仅使用 14 个对象,仅导入 12 个类,来使您在上一节中设计的 InvinciBagel 游戏的整个顶层成为现实。TableView 将覆盖 ImageView 组合,其中包含设计的信息屏幕层。这个 TableView 对象将在游戏设计的后期添加。ImageView 背板将包含 InvinciBagel 艺术品;ImageView 合成层将包含三个不同的透明图像,根据 ActionEvents(按钮控件的点击)无缝覆盖背板图像;VBox 父 UI 布局容器将包含四个按钮控件。您还将创建一个 Insets 对象来保存填充值,以微调按钮库对齐方式。
图 6-3。
Primary splash screen Scene Graph node hierarchy, the objects it contains, and the assets it references
因为按钮对象不能单独定位,所以我不得不使用 HBox 类以及 Insets 类和 Pos 类来包含和定位按钮控件。在这一章中,我将介绍你将用于这个高级设计的类,这样你就可以对你将要添加到 InvinciBagel 类中来创建这个顶级 UI 设计的每个类有一个大概的了解。
我为匹配四个不同按钮所需的四个不同屏幕优化场景图形使用的方法是使用一个 ImageView 作为背板来包含 InvinciBagel 闪屏插图,然后再使用一个 ImageView 来包含使用透明度(alpha 通道)的不同合成图像(覆盖)。这样,您可以仅使用两个 ImageView 场景图形节点对象来模拟四个不同的屏幕。
最后,TableView 场景图节点将包含高分表的表结构。这将通过分数引擎创建,你将在最后创建,在你完成整个游戏设计和编程。现在,你将离开高分和玩游戏按钮代码未实现。
场景图代码:优化你当前的 InvinciBagel 类
我知道您渴望在 InvinciBagel 类代码上工作,所以让我们清理、组织和优化现有的 Java 代码来实现这个顶级 UI 屏幕设计。首先,将对象声明和命名 Java 代码放在 InvinciBagel 类的顶部。这更有组织性,你的类中的所有方法将能够看到和引用这些对象,而不需要使用 Java 修饰关键字。正如你在图 6-4 中看到的,这些包括你现有的场景 scene 对象,根 StackPane 对象,和 btn 按钮对象(我把它重命名为 gameButton)。我添加了另外三个按钮对象,分别名为 helpButton、scoreButton 和 legalButton,它们都是使用一行 Java 代码声明和命名的,还添加了两个 ImageView 对象,分别名为 splashScreenbackplate 和 splashScreenTextArea。您还需要创建四个 Image 对象来保存数字图像资产,这些资产将显示在 ImageView 节点中;我已经用一个复合 Java 语句将它们命名为 splashScreen、instructionLayer、legalLayer 和 scoresLayer 并声明了它们。最后,声明并命名 buttonContainer VBox 对象和 buttonContainerPadding Insets 对象。只要您使用 Alt+Enter 快捷键,选择正确的 javafx 包和类路径,NetBeans 就会为您编写导入语句。进口显示在图的顶部。
图 6-4。
Declaring and naming the 14 objects that will make up your Scene Graph hierarchy at the top of the class
在本章中,您将详细了解所有这些 JavaFX 类,以便了解它们的用途以及它们能为您的 Java 应用做些什么。
场景图设计:简化现有的。start()方法
现在,您可以优化。start()方法,这样它只有一二十行代码。首先,将场景图节点创建 Java 例程模块化到它们自己的 createSplashScreenNodes()方法中,该方法将在。start()方法,如图 6-5 所示。在此方法中创建所有节点后,创建一个 addNodesToStackPane()方法将节点添加到 StackPane 根节点,然后让三行 primaryStage 代码配置和管理 Stage 对象,最后是 ActionEvent 处理代码例程,这些例程将按钮 UI 控件“连接”到单击按钮时要执行的 Java 代码。
图 6-5。
Organize the .start() method with the createSplashScreenNodes() and addNodesToStackPane() methods
如您所见,在复制了。对于每个按钮对象,当您折叠 EventHandler 例程时,您有九行代码:一行用于创建节点,一行用于向根添加节点,三行用于 Stage 对象设置,四行用于 UI 按钮事件处理。如果你考虑到你在游戏结构顶层增加的功能数量(游戏、指令、法律、积分、记分牌),这是非常紧凑的。
按照正确的顺序做事很重要,因为一些 Java 代码是基于其他 Java 代码的。因此,对象声明排在第一位;然后在。start()方法,创建(实例化)节点。一旦声明、命名和实例化(创建)了它们,就可以将它们添加到 StackPane 根节点,然后配置(使用。setTitle()方法)并将场景 scene 对象添加到 primaryStage Stage 对象中。setScene()方法。在你的对象进入系统内存之后,你才能够处理 ActionEvent 处理例程,这些例程被附加到你的四个按钮 UI 控件上。接下来,让我们确保将在 createSplashScreenNodes()方法中引用的数字图像资产位于正确的 NetBeans 文件夹中。
场景图形资源:在项目中安装 ImageView 的图像资源
要在 Java 代码中引用 JAR 文件中的数字图像资产,必须在文件名前插入一个正斜杠。但是,在引用文件之前,您必须将这些图像文件从图书存储库中复制到计算机/计算机名/用户/用户/my documents/netbeans projects/InvinciBagel/src 文件夹中,如图 6-6 的左侧(和顶部)所示。您还可以看到这些数字图像资产是如何合成的,因为 invincibagelsplash PNG24 的背景板上有一个位置可以覆盖其他三个 PNG32 图像。复合 ImageView 资产中看到的白色区域实际上是透明的!现在,你准备好了!
图 6-6。
Windows 7 Explorer file management utility, showing a PNG24 splash screen and three PNG32 overlays
JavaFX UI 类:HBox、Pos、Insets 和 ImageView
让我们从编码中休息一下,深入了解一些新的类,您将使用它们来完成您的顶级游戏应用 UI 设计。其中包括 Pos 类(定位);Insets 类(填充);HBox 类(UI 布局容器);图像类(数字图像容器);ImageView 类(数字图像显示);以及 TableView 类(表格数据显示),您将在这里学习该类,但在游戏完全完成后,您将在游戏开发的后续代码中实现该类。您将按照从最简单(Pos)到最复杂(TableView)的顺序检查这些,然后编写。createSplashScreenNodes()和。addNodesToStackPane()方法,它们使用这些新的类(对象)。
JavaFX Pos 类:通用屏幕位置常量
Pos 类是一个 Enum 类,代表“枚举”这个类包含一个常量列表,这些常量被转换成整数值以在代码中使用。常量值(在本例中是定位常量,如 TOP、CENTER 和 BASELINE)使程序员更容易在代码中使用这些值。
Pos 类的 Java 类扩展层次结构从 java.lang.Object masterclass 开始,逐步发展到 java.lang.Enum 类,最后以 javafx.geometry.Pos 类结束。如图 6-4 所示(l. 6),Pos 位于 JavaFX geometry 包中,并使用以下子类层次结构:
java.lang.Object> java.lang.Enum<Pos>
> javafx.geometry.
Pos
Pos 类有一组常量,用于提供通用的水平和垂直定位和对齐(见表 6-1 )。正如您将在下一节中看到的,您将不得不使用 Insets 类和对象来获得您想要的像素精确定位。您将使用 BOTTOM_LEFT 常量将按钮控件组定位在初始屏幕的左下角。
表 6-1。
Pos Class Enum Constants That Can Be Used for Positioning and Alignment in JavaFX
位置常数 | 定位结果(对象) |
---|---|
基线 _ 中心 | 在基线上,垂直地;在中心,水平地 |
基线 _ 左侧 | 在基线上,垂直地;在左边,水平地 |
基线 _ 右侧 | 在基线上,垂直地;在右边,水平地 |
底部中心 | 在底部,垂直地;在中心,水平地 |
左下角 | 在底部,垂直地;在左边,水平地 |
右下 | 在底部,垂直地;在右边,水平地 |
中心 | 在中心,垂直和水平 |
中央 _ 左侧 | 在中心,垂直地;在左边,水平地 |
中间 _ 右侧 | 在中心,垂直地;在右边,水平地 |
顶部 _ 中间 | 在顶部,垂直地;在中心,水平地 |
左上角 | 在顶部,垂直地;在左边,水平地 |
右上方 | 在顶部,垂直地;在右边,水平地 |
因为 Pos 类提供了一般化的定位,所以它应该与 Insets 类结合使用,以实现像素级的精确定位。接下来让我们看看 Insets 类,因为它也在 javafx.geometry 包中。
JavaFX Insets 类:为用户界面提供填充值
insets 类是一个公共类,它直接扩展了 java.lang.Object masterclass,这意味着 Insets 类是从头开始编写的,以提供矩形区域内的 Insets 或 offsets。想象一个相框,在里面放一块垫子,或者在相框外面和里面的图片之间放一个漂亮的边框。这就是 insets 类用两个构造函数方法做的事情:一个提供相等或均匀的 insets,另一个提供不相等或不均匀的 Insets。
您将使用提供不相等 insets 值的构造函数,如果您正在构建一幅图片,这将看起来非常不专业!Insets 类的 Java 类层次结构从 java.lang.Object 主类开始,并使用该类创建 javafx.geometry.Insets 类。如图 6-4 所示(l. 5),Insets 包含在 JavaFX geometry 包中,就像 Pos 类一样,并使用以下类层次结构:
java.lang.Object
> javafx.geometry.
Insets
Insets 类提供了一组四个双偏移值,用于指定矩形的四个边(上、右、下、左),在构造函数方法中应该按照这个顺序指定。您将使用 Insets 类(对象)来微调按钮控件组的位置,您将使用 HBox 布局容器来创建该控件组。将这些 Insets 对象视为在另一个框内绘制一个框的方式,它显示了您希望矩形内的对象围绕其边缘“尊重”的间距。Insets 对象的简单构造函数将使用以下格式:
Insets(double``topRightBottomLeft
此构造函数对所有间距边使用单个值(topRightBottomLeft),重载的构造函数允许您分别指定每个值,如下所示:
Insets(double``top``, double``right``, double``bottom``, double``left``)
这些值需要按此顺序指定。记住这一点的简单方法是使用模拟时钟。钟的顶部有“12”,右边有“3”,底部有“6”,左边有“9”。所以,从正午开始(对于你们这些西方流派的爱好者来说),总是顺时针工作,就像指针绕着钟面移动一样,你将有一个很好的方法来记住如何在“不均匀值”构造方法中指定 Insets 值。您将很快使用 insets 类来定位按钮控件组,该控件组最初“卡”在初始屏幕设计的左下角,远离屏幕的左侧和底部,使用这四个 Insets 定位参数中的两个。
JavaFX HBox 类:在设计中使用布局容器
因为按钮对象不容易定位,所以我将把这四个按钮对象放在 javafx.scene.layout 包中的一个布局容器中,该包名为 HBox,代表水平框。这个公共类将事情安排在一行中,因为您希望按钮在闪屏的底部对齐,所以您使用四个按钮控件节点的父节点,这些节点将成为这个 HBox 分支节点的子节点(叶节点)。这将创建一组 UI 按钮,它们可以作为闪屏设计的一个单元一起定位(移动)。
HBox 类是一个公共类,它直接扩展 javafx.layout.Pane 超类,后者又扩展 javafx.layout.Region 超类。javafx.layout.Region 超类扩展了 javafx.scene.parent 超类,后者又扩展了 javafx.scene.Node 超类,后者扩展了 java.lang.Object 主类。如图 6-4 所示(l. 11),HBox 包含在 javafx.scene.layout 包中,就像 StackPane 类一样,它使用如下的类层次结构:
java.lang.Object> javafx.scene.Node> javafx.scene.Parent> javafx.scene.layout.Region> javafx.scene.layout.Pane
> javafx.scene.layout.
HBox
如果 HBox 指定了边框或填充值,HBox 布局容器的内容将遵循该边框或填充规范。填充值是使用 Insets 类指定的,您将在这个微调的 UI 控件库应用中使用它。
您将使用 HBox 类(object ),以及 Pos 类常量和 Insets 类(object ),将 UI 按钮对象分组在一起,然后,微调它们作为按钮控件库的位置。因此,这个 HBox 布局容器将成为按钮 UI 控件(或叶节点)的父节点(或分支节点)。
可以把 HBox 对象想象成一种水平排列子对象的方式。这些可能是您的图像资产,它将使用基本的 HBox 构造函数(零间距),或者 UI 控件,如按钮,它们排列在一起但有间距,使用重载的构造函数之一。创建 HBox 对象的最简单的构造函数将使用下面的空构造函数方法调用格式:
HBox()
您将用于创建 HBox 对象的重载构造函数将使用一个间距值在 HBox 内的子按钮对象之间留出一些空间,使用以下构造函数方法调用格式:
HBox(double``spacing
还有另外两种重载构造函数方法调用格式。这将允许您在构造函数方法调用本身中指定子节点对象(在本例中为按钮对象),如下所示:
HBox(double``spacing``, Nodes...``children``) - or, with``zero spacing
HBox(Nodes...``children
你将会使用“长表格”。getChildren()。addAll()方法链,但是您也可以通过使用以下构造函数来声明 HBox 及其按钮节点对象:
HBox buttonContainer = new HBox(12, gameButton, helpButton, scoreButton, legalButton);
如果子对象被设置为可调整大小,HBox 布局容器将基于不同的屏幕大小、纵横比和物理分辨率来控制子元素的大小调整。如果 HBox 区域将容纳子对象的首选宽度,它们将被设置为该值。此外,fillHeight 属性(布尔变量)设置为 true 作为默认值,指定子对象是否应该填充(放大)HBox 高度值。
HBox 的对齐是由 Alignment 属性(属性或变量)控制的,该属性默认为 Pos 类(Pos)中的 TOP_LEFT 常量。TOP_LEFT)。如果 HBox 的大小超过了其指定的宽度,子对象将使用它们的首选宽度值,多余的空间不会被使用。值得注意的是,HBox 布局引擎将对托管子元素进行布局,而不考虑它们的可见性属性(属性或变量)设置。
现在,我已经讨论了 JavaFX geometry 和 layout 类,您将使用它们来创建 UI(一组按钮对象)设计,让我们来看看 javafx.scene.image 包中与数字图像相关的类,它将允许您实现数字图像合成管道,您将在 HBox UI 布局容器对象中保存的这四个 JavaFX 按钮 UI 控件元素对象的后面放置该管道。
JavaFX Image 类:在设计中引用数字图像
Image 类是一个公共类,它直接扩展了 java.lang.Object masterclass,这意味着 Image 类也是从头开始编写的,以提供图像加载(引用)和缩放(调整大小)。您可以锁定缩放的纵横比,并指定缩放算法(质量)。支持 java.net.URL 类支持的所有 URL。这意味着你可以从网上加载图片(www.servername.com/image.png
);从 OS(文件:image . png);或者从 JAR 文件中,使用正斜杠(/image.png)。
Image 类的 Java 类层次结构从 java.lang.Object 主类开始,并使用该类创建 javafx.scene.image.Image 类。如图 6-4 所示(l. 9),Image 包含在 JavaFX image 包中,就像 ImageView 类一样,使用如下的类层次结构:
java.lang.Object
> javafx.scene.image.
Image
Image 类提供了六种不同的(重载的)Image()构造函数方法。这些函数从简单的 URL 到一组指定 URL、宽度、高度、比例、平滑和预加载选项的参数值。当您使用所有构造函数方法中最复杂的方法编写 Image()构造函数时,您将很快看到,这些方法应该在构造函数方法中按此顺序指定,其格式如下:
Image(String``url``, double``requestedWidth``, double``requestedHeight``, boolean``preserveRatio,``boolean``smooth,``boolean``backgroundLoading``)
Image 对象的简单构造函数仅指定 URL,并使用以下格式:
Image(String``url
如果您要加载图像,并让构造函数方法将图像缩放到不同的宽度和高度(通常较小,以节省内存),同时使用最高质量的重新采样(平滑像素缩放)锁定(保留)纵横比,则该图像对象构造函数使用以下格式:
Image(String``url``, double``scaleWidth``, double``scaleHeight``, boolean``preserveAspect``, boolean``smooth``)
如果希望在后台(异步)加载图像,使用其“本机”或物理分辨率和本机纵横比,image()构造函数使用以下格式:
Image(String``url``, boolean``backgroundLoading
两个 Image()构造函数方法也使用 java.io.InputStream 类,该类向 Image()构造函数方法提供输入数据的实时流(类似于视频或音频流,只针对 java 应用进行了定制)。这两种图像对象构造器格式采用以下格式(简单和复杂):
Image(InputStream``is
Image(InputStream``is,``double``newWidth``, double``newHeight``, boolean``preserveAspect``, boolean``smooth``)
因此,Image 类(对象)用于准备数字图像资产以供使用,即从 URL 读取其数据;如果有必要,调整它们的大小(使用你喜欢的平滑和纵横比锁定);并异步加载它们,而应用中的其他事情正在进行。需要注意的是,图像类(或对象)并不显示图像资产:图像类只是加载它;如果需要,缩放它;并将它放在系统内存中供您的应用使用。
要显示图像对象,您需要使用第二个类(对象),称为 ImageView 类。ImageView 对象可以用作场景图和引用上的节点,然后将图像对象数据“绘制”到布局容器上,该容器保存 ImageView 节点(在这种情况下,是叶 ImageView 节点的 StackPane 场景图根和父节点)。我将在下一节介绍 ImageView 类。
从数字图像合成的角度来看,StackPane 类(对象)是图像合成引擎,也可以称为层管理器,每个 ImageView 对象代表层堆栈中的一个层。图像对象包含图像视图层中的数字图像数据,或者如果需要,包含多个图像视图中的数字图像数据,因为图像对象和图像视图对象是分离的,并且彼此独立存在。
JavaFX ImageView 类:在设计中显示数字图像
ImageView 类是一个公共类,它直接扩展 javafx.scene.Node 超类,后者是 java.lang.Object 的扩展(参见第四章)。因此,ImageView 对象是 JavaFX 场景图中的一种节点对象,用于使用 Image 对象中包含的数据绘制视图。该类具有允许图像重采样(调整大小)的方法,并且与 image 类一样,您可以锁定缩放的纵横比以及指定重采样算法(平滑质量)。
ImageView 类的 Java 类层次结构从 java.lang.Object 主类开始,并使用该类创建 javafx.scene.Node 类,然后使用该类创建 ImageView 节点子类。如图 6-4 所示(l. 10),和 Image 类一样,ImageView 包含在 JavaFX image 包中。ImageView 类使用以下 Java 类继承层次结构:
java.lang.Object> javafx.scene.Node
> javafx.scene.image.
ImageView
ImageView 类提供了三种不同的(重载的)ImageView()构造函数方法。这些范围从一个空的构造函数(这是您稍后将在代码中使用的一个构造函数);转换为一个将图像对象作为其参数的函数;转换为以 URL 字符串对象作为参数并自动创建图像对象的方法。要创建 ImageView 对象,简单(空)ImageView()构造函数方法使用以下格式:
ImageView()
您将使用这个构造函数方法,这样我就可以向您展示如何使用?setImage()方法调用将图像对象加载到 ImageView 对象中。如果您想避免使用。setImage()方法调用时,可以使用重载的构造函数方法,其格式如下:
ImageView(Image``image
因此,要“显式”设置一个 ImageView 并将其连接到 Image 对象,如下所示:
splashScreenBackplate =``new
splashScreenBackplate.setImage(``splashScreen
您可以使用重载的构造函数方法将其压缩成一行代码,结构如下:
splashScreenBackplate =``new``ImageView(``splashScreen
如果您想绕过创建和加载图像对象的过程,也有一个构造函数方法,它使用以下格式:
ImageView(String``url
要在后台(异步)加载图像,使用其本机(默认)分辨率和本机纵横比,image()构造函数使用以下格式:
splashScreen
= new Image("/invincibagelsplash.png", 640, 400, true, false, true);
splashScreenBackplate = new ImageView();
splashScreenBackplate.setImage(``splashScreen
如果不想指定图像尺寸、背景图像加载和平滑缩放,或者不想锁定任何缩放的纵横比,可以将前面三行 Java 代码压缩到以下构造函数中:
splashScreenBackplate = new ImageView("/invincibagel.png"); // uses third constructor method
至少在开始时(出于学习目的),我会用长时间的方式来做这件事,我会一直使用 Image()构造函数方法显式地加载图像对象,这样您就可以指定所有不同的属性,并看到您在这个 Java 编程逻辑中使用的所有不同的图像资产。我想在这里向您展示快捷方式代码,因为一旦您开始使用 ImageViews 作为精灵,您将在本书的后面使用这种方法(参见第八章)。你可以对你的精灵使用这种快捷方式,因为你不需要缩放它们,也因为它们已经高度优化,不需要后台加载。
接下来,让我们快速看一下 TableView 类,它将保存高分表。虽然你不会在这里实现它,但我会介绍这个类,因为它是你在本章中创建和实现的顶级 UI 设计的一部分。
JavaFX TableView 类:在设计中显示数据表
TableView 类是直接扩展 javafx.scene.control.Control 超类的公共类,javafx.scene.layout.Region 是 javafx.scene.layout.Region 的扩展,javafx.scene.Parent 是 javafx.scene.Node 场景图超类的扩展(见第四章)。因此,TableView < S >对象是一种 UI 控件(表格)和 JavaFX 场景图中的节点对象,用于使用 S 对象构建表格,每个对象都包含要在表格中显示的数据。在本书的后面部分,你将使用这些 S 对象将数据写入一个 TableView < S >对象,在得分超过当前列表中的得分之后。
TableView 类的 Java 类层次结构从 java.lang.Object 主类开始,并使用该类创建 javafx.scene.Node 类,然后使用该类创建父类。这用于创建一个区域类,该区域类又创建一个控件类,该控件类用于创建 TableView 类。TableView 类具有以下 Java 类继承层次结构:
java.lang.Object> javafx.scene.Node> javafx.scene.Parent> javafx.scene.layout.Region> javafx.scene.control.Control
> javafx.scene.control.
TableView<S>
TableView 类提供了两种不同的(重载的)TableView()构造函数方法,一种是空的构造函数,另一种是接受 ObservableList 对象的构造函数,该对象以表数据项作为参数。创建空 TableView 对象的简单(空)TableView()构造函数方法将使用以下格式:
TableView()
第二种构造函数类型使用 javafx.collections 包中的 ObservableList 类(object ),这是一种列表类型,它允许数据更改事件侦听器跟踪列表中发生的任何更改。此 TableView 对象构造函数方法调用使用以下格式:
TableView(ObservableList<S>``items
我认为现在已经有足够的类背景信息了,所以让我们开始为你的第一个类编写代码。createSplashScreenNodes()方法,该方法将实例化并设置场景图形的所有节点对象!
场景图形节点:。createSplashScreenNodes()
使用 createSplashScreenNodes()方法要做的第一件事是编写空方法结构的代码,并添加已经存在于引导代码中的节点对象创建代码,引导代码是 NetBeans 在第二章中为您生成的。这包括按钮节点的节点对象、StackPane 根节点和名为 Scene 的场景对象。您将在。start()方法,因为该对象是使用。start(Stage primaryStage)构造函数方法调用。Button 对象已经被重命名为 gameButton(原来是 btn),所以您有三行对象实例化代码和一行配置代码,如下所示:
root
= new StackPane();
scene = new Scene(``root
gameButton = new Button();gameButton.setText("PLAY GAME");
需要注意的是,因为在场景 scene 对象的构造函数方法调用中使用了根 StackPane 对象,所以这一行代码需要放在前面(在使用它之前,必须先创建您的根对象!).接下来您需要创建的是 HBox 布局容器对象,它将保存您的四个按钮 UI 控件。您还将为 HBox 设置对齐属性;添加一个 Insets 对象以包含填充值;然后使用这四行 Java 代码将这些填充添加到 4 HBox 对象中:
buttonContainer = new HBox(``12
buttonContainer.setAlignment(Pos.``BOTTOM_LEFT
buttonContainerPadding
= new Insets(0, 0, 10, 16);
buttonContainer.setpadding(``buttonContainerPadding
接下来,让我们采用一种方便的程序员快捷方式,将两行 gameButton(实例化和配置)代码复制并粘贴到 HBox 代码下面(因为按钮在 HBox 内部,这只是为了视觉组织,而不是为了使代码工作),然后在单独的行上再复制并粘贴三次。这将允许您通过创建以下四个按钮 Java 代码,分别将游戏更改为帮助、得分和合法:
gameButton = new Button();gameButton.setText("PLAY GAME");helpButton = new Button();helpButton.setText("INSTRUCTIONS");scoreButton = new Button();scoreButton.setText("HIGH SCORES");legalButton = new Button();legalButton.setText("LEGAL & CREDITS");
现在您已经创建了 HBox 按钮 UI 控件布局容器和按钮,您还需要再编写一行代码,使用。getChildren()。addAll()方法链,像这样:
buttonContainer.``getChildren``().``addAll
接下来,让我们添加您的图像合成节点对象(image 和 ImageView ),以便您可以为 InvinciBagel 闪屏添加插图,以及装饰您的说明、法律免责声明和制作人员名单的面板覆盖图,并为您的(最终)游戏高分表添加背景和屏幕标题。我使用两个 ImageView 对象来包含这两层;让我们首先设置最底部的背板图像层,方法是使用以下 Java 代码实例化 image 对象,然后实例化 ImageView 对象,并将它们连接在一起:
splashScreen = new Image("/invincibagelsplash.png", 640, 400, true, false, true);splashScreenBackplate = new ImageView();splashScreenBackplate.setImage(splashScreen); // this Java statement connects the two objects
最后,让我们对图像合成板做同样的事情,也就是说,ImageView 将保存包含 alpha 通道(透明度)值的不同图像对象,这些图像对象将为 InvinciBagel 闪屏插图(由才华横溢的 2D 艺术家 Patrick Harrington 创建)创建面板图像的覆盖图:
instructionLayer = new Image("/invincibagelinstruct.png", 640, 400, true, false, true);splashScreenTextArea = new ImageView();splashScreenTextArea.setImage(instructionLayer); // this Java statement connects the two objects
如图 6-7 所示,你的场景图节点创建(见 InvinciBagel 类的顶部)和节点对象实例化和配置(见 createSplashScreenNodes()方法)已经就绪并且没有错误。您仍然需要为其他两个屏幕添加图像对象,但是这里有足够的代码能够使用 addNodesToStackPane()方法将这些节点对象添加到场景图中,然后测试代码以确保它能够工作。根据 NetBeans IDE,此代码没有错误!
图 6-7。
Coding the createSplashScreenNodes() method; instantiating and configuring the nodes in the Scene Graph
接下来,让我们在 addNodesToStackPane()方法中将节点对象添加到 StackPane 场景图形根对象中。
向场景图添加节点:。addStackPaneNodes()
最后,您必须创建一个方法,将您已经创建的节点对象添加到场景图形根,在本例中是一个 StackPane 对象。您将使用。getChildren()。add()方法链将子节点对象添加到父 StackPane 根场景图节点。这是通过三行简单的 Java 代码完成的,如下所示:
root.getChildren().add(splashScreenBackplate);root.getChildren().add(splashScreenTextArea);root.getChildren().add(buttonContainer);
正如你在图 6-8 中看到的,Java 代码是没有错误的,根对象在类的顶部看到了它的声明。单击代码中的根对象会创建这种突出显示,它通过代码跟踪对象的使用。这是一个非常酷的 NetBeans 8.0 技巧,当您想要跟踪代码中的对象时,应该使用它。
图 6-8。
Coding the addNodesToStackPane() method, using the .getChildren() method chained to the .add() method
这里要注意的重要事情是节点对象添加到 StackPane 根场景图形对象的顺序。这会影响图像合成的合成层顺序,以及这些数字图像元素之上的 UI 元素合成。添加到 StackPane 的第一个节点将位于层堆栈的底部;这需要是 splashScreenBackplate ImageView 节点对象,如图所示。
下一个要添加的节点将是 splashScreenTextArea ImageView 节点对象,因为带有面板覆盖的透明图像需要放在 Pat Harrington 的 InvinciBagel 闪屏 2D 作品的正上方。之后,您可以放置 UI 设计,在本例中,可以使用 buttonContainer HBox 节点对象一次性完成,该节点对象包含所有的按钮对象。请注意,您不必向这个 StackPane 根场景图形对象添加按钮,因为您已经使用了。getChildren()。addAll()方法链,用于将按钮 UI 控件添加到 HBox(父对象)节点分支对象下的场景图形层次中。现在,您可以开始测试了!
测试 InvinciBagel 应用:脉动场景图形
单击 NetBeans IDE 顶部的绿色播放箭头,然后运行项目。这将弹出如图 6-9 所示的窗口(我已经删除了在第四章中添加的 Java 代码,演示如何创建一个无窗口应用)。所以,你又有了 windows“chrome ”,至少现在是这样。正如您所看到的,您只使用了十几条 import 语句(外部类)、几十行 Java 代码和场景图根(StackPane)对象下的六个子节点,就获得了非常好的结果。如您所见,JavaFX 在将背板、合成图像覆盖和按钮库覆盖合成为一个无缝、专业的结果方面做得非常好!
图 6-9。
Run the InvinciBagel application, and make sure that the StackPane compositing class is working correctly
因为您只复制和粘贴了每个按钮的 EventHandler 例程,并且更改了按钮对象的名称,而没有更改这些例程中的代码,所以按钮对象仍然可以正常工作(将文本写入控制台)并且不会导致编译器错误。但是,他们不会做您希望他们做的事情,即更改图像覆盖,以便设计左侧的面板包含您希望它向用户显示的标题和文本。
这将通过调用。setImage()方法,该方法将根据用户单击的按钮 UI 控件,将 splashScreenTextArea ImageView 对象设置为 instructionLayer、scoresLayer 或 legalLayer Image 对象。在添加最后两个 Image 对象实例之前,您不能实现这个事件处理代码!
完成 InvinciBagel UI 屏幕设计:添加图像
让我们以 createSplashScreenNodes()方法结束,在该方法的末尾再添加两行,以添加两个图像对象,引用 invincibagelcreds.png 和 invincibagelscores.png 的 32 位 PNG32 数字图像资产。这是通过使用下面两行使用 new 关键字的 Java 对象实例化代码来实现的:
legalLayer = new Image( "/invincibagelcreds.png", 640, 400, true, false, true );scoresLayer = new Image( "/invincibagelscores.png", 640, 400, true, false, true );
如图 6-10 所示,代码是没有错误的,因为你已经将四个 PNG 文件复制到你的项目的/src 文件夹中。您不需要其他代码行(ImageView 对象实例化,。setImage()),因为您将使用 splashScreenTextArea ImageView 对象来保存这最后两个图像对象。因此,您可以节省所使用的场景图节点对象,因为您使用单个 ImageView 场景图节点对象来基于按钮事件显示三个不同的图像对象(覆盖)。
图 6-10。
Adding legalLayer and scoresLayer Image object instantiations to add the other image composite plates
这意味着,您将对 splashScreenTextArea ImageView 对象进行的 splashScreenTextArea.setImage()方法调用将被放置在三个按钮对象的 ActionEvent EventHandler 编程构造中,这三个按钮对象在被单击时会触发图像合成覆盖。第四个按钮对象将启动游戏,所以现在,在按钮事件处理结构中只有一个 Java 注释,使它成为一个“空”逻辑结构。现在,让我们看看如何完成这些 EventHandler 构造的编码,这样您就可以完成这个 UI 设计,并继续创建前面提到的游戏引擎。
交互性:连接 InvinciBagel 按钮以供使用
在所有这些重复的按钮事件处理结构中,您需要用对。setImage()方法,以便您可以将图像合成板 ImageView 设置为 Image 对象,该对象包含您想要覆盖由 Pat Harrington 创建的 InvinciBagel 背板作品的数字图像资产。您已经在 createSplashScreenNodes()方法中编写了两次这个代码结构,所以如果您想要一个快捷方式,您可以将代码行直接复制到您刚刚编写的两个 Image 对象实例化之上。
那个。因此,setOnAction()事件处理 Java 代码结构如下所示:
helpButton
.setOnAction(new EventHandler<ActionEvent>() {
@Overridepublic void handle(ActionEvent event) {
splashScreenTextArea.setImage(``instructionLayer
}});
scoreButton
.setOnAction(new EventHandler<ActionEvent>() {
@Overridepublic void handle(ActionEvent event) {
splashScreenTextArea.setImage(``scoresLayer
}});
legalButton
.setOnAction(new EventHandler<ActionEvent>() {
@Overridepublic void handle(ActionEvent event) {
splashScreenTextArea.setImage(``legalLayer
}});
如图 6-11 所示,您的事件处理代码没有错误,您已经准备好再次运行和测试了!
图 6-11。
Modify the body of the .handle() method for each of four Button controls to complete the infrastructure
如您所见,您暂时将 gameButton.setOnAction()事件处理结构保留为空;在下一章中,您将创建主要的游戏界面和一个脉冲事件处理引擎(结构),该引擎将通过调用您将在本书过程中编写的各种功能引擎来运行该游戏。
您现在也将高分屏幕的底部保留为空白,以便您可以在场景图形根堆栈窗格中用 TableView 节点对象覆盖两个 ImageView 层。在开发完 Java 8 游戏之后,您将完成高分按钮 UI 元素的复合。
现在,是时候对游戏应用的顶层 UI 部分进行最终测试了,以确保所有的 UI 按钮元素(对象)都能正常工作,并按照您的设计(编码)完成它们的功能。之后,您将再次运行 NetBeans 8.0 Profiler,以确保您刚刚创建的场景图形层次结构确实为您将从现在开始创建的游戏引擎留出了 99%的可用 CPU 处理能力。
测试最终的 InvinciBagel UI 设计
再次单击 NetBeans IDE 8.0 顶部的绿色播放箭头,然后运行您的项目。这将调出如图 6-12 所示的窗口。正如您所看到的,当您单击“法律和学分”按钮 UI 元素时,该覆盖图与 InvinciBagel artwork 背板无缝合成,如图左侧所示;当您单击“高分”按钮 UI 元素(控件)时,高分表格背景将就位,如图右侧所示。如您所见,javafx.image 包中的类提供了关于合成的原始结果
图 6-12。
The other two Image objects shown composited, using the background plate and compositing ImageViews
接下来,让我们来看看你在本章中编写的场景图实现占用了多少 CPU 周期,因为你想确保 100%静态的顶级 UI 设计,以便游戏中使用的唯一动态元素是游戏引擎(和相关引擎)本身。因为脉冲分辨率引擎遍历场景图层次可能会变得“昂贵”,所以这里需要非常小心!
请记住,您的主要目标是创建一个顶级的 UI 设计,用于启动游戏播放屏幕和循环,同时还实现一个允许您的用户显示说明、法律免责声明和制作人员名单的 UI,并负责设置一个用于显示高分表的区域。同时,您的任务是节省 99%的处理能力供以后使用,通过 JavaFX 脉冲引擎处理游戏逻辑、精灵运动、精灵动画、碰撞检测、得分和物理。
剖析 InvinciBagel 场景图的脉冲效率
重要的是,游戏 UI 设计不要从 CPU 中取走任何处理能力,因为游戏引擎将需要所有的处理能力。如图 6-13 所示,您可以使用 Profile>Profile Project(InvinciBagel)菜单序列来运行 Profiler,并对当前(顶层 UI)应用的 CPU 统计数据进行截图。
图 6-13。
Profiling the Scene Graph UI design thus far to make sure that it does not use any perceptible CPU cycles
正如您在图的右侧的 Total Time 列中所看到的,createSplashScreenNodes()方法需要 279 毫秒,或者大约十分之三秒的时间来执行,并且您的场景图被创建。执行 addNodesToStackPane()方法大约需要 3 毫秒,即千分之三秒。
如果您查看线程分析输出并单击 UI 按钮元素,您将会看到线程上出现一个彩色点,显示了按钮单击的处理开销,正如您所看到的,每次单击不到十分之一秒(查看最右边的调用列,了解我测试了多少次按钮单击函数)。我突出显示了 threads 视图,在那里我单击了 High Scores,然后是 Legal 和 Credits 按钮 UI 元素(参见图 6-14 )。正如您在这个视图中看到的,当前的设计使用了最少的资源。
图 6-14。
Profiling the Scene Graph UI design thus far to make sure that it does not use any perceptible thread overhead
Java 8 及其 JavaFX 引擎衍生了近十几个线程,所以你的游戏应用已经是高度多线程的了,甚至不需要在这个时间点!Oracle 的团队正在努力使 JavaFX 成为首屈一指的游戏引擎,所以性能会越来越好,这对 Java 8 游戏开发者来说是个好消息!
摘要
在第六章中,您已经开始着手为您的游戏进行实际的顶级 UI 设计,概述底层游戏引擎组件设计,并找出最有效的场景图节点设计。然后,您回到 Java 8 游戏编程,并重新设计了您现有的引导 Java 8 代码,这些代码最初是由 NetBeans 8.0 创建的。
因为 NetBeans 生成的 Java 代码设计并不适合您的目的,所以您完全重写了它,使它更有条理。这是通过创建两个自定义 Java 方法来实现的。createSplashScreenNodes()和。addNodesToStackPane(),用于模块化场景图节点创建过程,以及将三个父(和叶)节点对象添加到场景图根(在本例中,是 StackPane 对象,它用于其多层 UI 对象合成功能)。
接下来,您从 javafx.geometry 包中了解了一些用于实现这些新方法的 JavaFX 类,包括 Pos 类和 Insets 类;javafx.scene.image 包中的 Image 和 ImageView 类;HBox 类,来自 javafx.scene.layout 包;以及 javafx.scene.control 包中的 TableView 类。你编写了新的。createSplashScreenNodes()方法,该方法使用 Insets 对象、Image 和 ImageView 对象以及四个 Button 对象来实例化和配置 HBox 对象。一旦实例化和配置了所有这些场景图节点,您就可以编写一个。addNodesToStackPane()方法将节点对象添加到 StackPane 根对象,以便它们可以由 Stage 对象显示,Stage 对象引用场景图形的根对象。接下来,您测试了您的顶级游戏应用 UI 设计。然后,添加最后几个图像对象,并添加 ActionEvent EventHandler 程序逻辑。最后,您对应用进行了概要分析,以确保它是高效的。
在下一章中,我将介绍 JavaFX 脉冲引擎和 AnimationTimer 类,以便您可以为 Java 8 游戏引擎创建基础结构,该引擎将实时处理您的游戏事件。
七、游戏循环的基础:JavaFX 脉冲系统和游戏处理架构
现在,您已经为您的用户创建了学习如何玩游戏、开始游戏、查看高分以及查看法律免责声明和 Ira H. Harrison Rubin 的 InvinciBagel 知识产权游戏制作致谢名单所需的顶级 UI 屏幕,让我们进入正题,为您的 InvinciBagel 游戏创建游戏播放计时循环。从用户体验的角度来看,这是最重要的,并且对于您将在本书剩余部分创建的不同游戏引擎的正常运行也是至关重要的,包括精灵引擎、碰撞检测引擎、动画引擎、评分引擎和物理引擎。你将永远记住游戏的流畅度;JavaFX 脉冲系统的高效、优化实现在游戏的这个阶段是至关重要的(没有双关语)。为此,我将在本章中详细介绍 javafx.animation 包,以及它的所有函数类之间的区别。
首先,您将探索 javafx.animation 包中的两个动画超类:Animation 和 AnimationTimer。之后,您将了解动画、时间轴和过渡,以及这些类及其任何子类(如 PathAnimation 和 TranslateAnimation)如何允许您访问 JavaFX 脉冲事件计时系统。现在,你需要使用脉冲,如果你想创建一个面向行动的街机类型的 Java 8 游戏!
您还将仔细查看整个 javafx.animation 包的整体结构,因为您需要在 Java 8 游戏循环中使用其中一个类。您将通过使用整个包的图表来完成这个任务,这样您就可以对它的所有类是如何相互关联的有一个总体的了解。然后,您将详细检查所有 JavaFX 动画类之间的类层次结构。除了 AnimationTimer、Interpolator、KeyFrame 和 KeyValue,所有这些 javafx.animation 包类都使用 JavaFX Animation 超类进行子类化(使用 Java extends 关键字)。
最后,您将把新的 GamePlayLoop 类添加到 invincibagel 包中,该包将作为 invincibagel 应用子类中的 GamePlayLoop 对象创建,实现定时循环。这个 GamePlayLoop 类将包含一个. handle()方法,以及一个. start()方法和一个. stop()方法,这将允许您在 GamePlayLoop 运行时控制 GamePlayLoop 计时事件,并确定它何时处于潜伏状态(停止或暂停)。
我将创建一个图表,显示这个 InvinciBagel 游戏的类和对象层次结构,这样您就可以开始想象您正在编写的这些类和您正在创建的对象是如何组合在一起的。这就好像使用 Java 8 和 JavaFX 编写游戏代码本身就是一个(益智)游戏!很酷的东西。
游戏循环处理:利用 JavaFX 脉冲
即使在开发团队中的 Oracle 员工中,一个主要问题是实现游戏计时循环引擎的哪种设计方法应该与 JavaFX 动画包(类套件)中包含的类一起使用。事实上,这正是本章的全部内容:使用 javafx.animation 包及其类,这些类利用了 JavaFX 脉冲事件计时引擎。在包的类层次结构的顶层,如图 7-1 所示,AnimationTimer 和 Animation 类提供了获取这些脉冲事件的主要方法,以便它们为您进行实时处理。在这一部分,你将看到它们的不同之处以及它们的设计用途,包括它们应该用于的游戏种类。除了插值器(运动曲线应用)、关键帧(关键帧定义)和 KeyValue(关键帧自定义)之外,javafx.animation 包中的所有类都可以用于控制脉冲事件。
图 7-1。
Javafx.animation package subclass hierarchy; top level classes all coded from scratch with java.lang.Object
有四种基本方法来实现(访问)JavaFX 脉冲事件计时系统,以创建游戏计时循环。这些不同的方法适用于不同类型的游戏,我之前已经讨论过了(见第五章)。这些游戏类型从需要使用 脉冲事件引擎来实现特殊效果(过渡子类)或自定义动画(时间轴类,结合关键帧类和可能的 KeyValue 类)的静态游戏(棋盘游戏、益智游戏)到需要以每秒 60 次的游戏播放刷新率对 脉冲事件系统进行必要的核心访问的高度动态游戏(AnimationTimer 类)。
最高级别(视觉上,最低级别,显示在图的左下方)是使用 javafx.animation 包中预先构建的 Transition 子类,如 PathTransition 类(或对象),用于游戏精灵或投射物的路径,或 TranslateTransition,用于翻译(移动)屏幕上的东西。所有这些过渡子类都是为你编码的;你所要做的就是使用它们,这就是为什么在这个特殊的讨论中,我把它标为最高的功能级别。这种高水平的预建功能带来了内存和处理价格;这是因为,正如你从 Java 继承中所了解到的(参见第三章), path transition 类包含了它所有的方法、变量和常量,以及在类层次结构中位于它之上的所有类的方法、变量和常量。
这意味着整个 PathTransition 类、Transition 超类、Animation 超类和 java.lang.Object masterclass 都包含在该类的内存占用中,并且还可能包含处理开销,这取决于如何使用该类实现对象。这是一个需要考虑的问题,因为你在 JavaFX 动画包中的位置越低,它就越昂贵,你对为你编写的代码的控制就越多,而不是你自己编写的定制代码。
编码自定义游戏循环的下一个最高级别的方法是子类化 javafx.animation.Transition 类,以创建您自己的自定义过渡子类。这一级和前一级都被认为是顶级方法,最适用于静态但有动画效果的游戏,或者动态性较差的游戏。
中级解决方案是使用 Timeline 类及其相关的 KeyFrame 和 KeyValue 类,它们非常适合于实现拖放工具(如 Flash)中基于时间轴的动画类型。您会发现,如果您在网上查看 JavaFX 游戏引擎讨论,这是一种流行的方法,因为许多动画都是通过创建单个关键帧对象,然后使用时间轴对象来处理脉冲事件来实现的。
使用时间轴对象方法允许您指定处理游戏循环的帧速率,例如 30FPS。这将适用于可以使用较低帧速率的动态性较低的游戏,因为它们不涉及大量的帧间游戏处理,如精灵移动、精灵动画、碰撞检测或物理计算。请务必注意,如果使用 Timeline 对象(类),您将在系统内存中为帧速率和至少一个关键帧对象引用(这些是 Timeline 类定义的一部分)定义变量,以及从 Animation 超类继承的属性(变量),如 status、duration、delay、cycleCount、cycleDuration、autoReverse、currentRate、currentTime 和 on finished(action event)object property。
如果您熟悉创建动画,您会看到时间轴,以及至少一个关键帧对象和存储在每个关键帧对象中的潜在的大量 KeyValue 对象,显然是为创建基于时间轴的动画而设计的(优化的)。虽然这是一个非常强大的功能,但它也意味着使用时间轴和关键帧对象进行游戏循环处理将会创建近十几个内存分配区域,这些区域甚至可能不会在您的游戏中使用,或者可能不会针对您的游戏设计实现进行优化设计(编码)。
幸运的是,还有另一个与 javafx.animation 包计时相关的类,它没有这种预构建类的开销,所以我称之为最底层的方法,在这种方法中,您必须在一个简单的。handle()函数,它在每次传递时访问 JavaFX 脉冲引擎。
低级的解决方案包括使用 AnimationTimer 类,这样命名是因为 Java (Swing)已经有了一个 Timer 类(javax.swing.Timer),Java 的实用程序类(java.util.Timer)也是如此,如果您是一个足够高级的程序员,也可以使用它来处理所有线程同步问题(和编码)。
因为这是一本初学者级别的书,所以您将坚持使用 Java 8 游戏引擎(JavaFX 8)循环您的游戏。JavaFX 在 javafx.animation 包中有自己的 Timer 类,称为 AnimationTimer,以免与 Swing GUI Toolkit 的 Timer 类混淆(由于遗留代码的原因,它仍然受支持)。许多新开发人员对这个类名的“动画”部分感到困惑;不要假设这个定时器类是用于动画的;它的核心是为了计时。就访问 javafx 脉冲计时系统而言,该类是 javafx.animation 包中最低级别的类,本质上仅用于访问脉冲计时系统。其他的都被剥离了。
因此,AnimationTimer 类是为您实现提供最少系统开销(使用的内存)的类。在全速 60FPS 时,它将具有最高的性能,假设。handle()方法得到了很好的优化。这是用于快速、高动态游戏的类,例如街机游戏或射击游戏。出于这个原因,这是您将在游戏中使用的类,因为您可以继续构建游戏引擎框架并添加功能,而不会耗尽电量。
在本书中,您将使用最底层的方法,以防您将 Java 8 游戏开发推向极限,并且正在创建一个高度动态的、充满动作的游戏。JavaFX AnimationTimer 超类非常适合这种类型的游戏应用,因为它处理它的。每个 JavaFX 脉冲事件的 handle()方法。脉冲事件目前被限制在 60FPS,这是专业动作游戏的标准帧速率(也称为刷新率)。您将从 AnimationTimer 超类中派生出 GamePlayLoop.java 类的子类。
有趣的是,大多数现代 iTV LCD、有机发光二极管和 LED 显示屏产品也以这种精确的刷新率(60Hz)更新,尽管较新的显示器将以两倍于此的速率(120Hz)更新。具有 240Hz 刷新率的显示器也即将问世,但因为这些 120Hz 和 240Hz 刷新率显示器使用 60Hz 的偶数倍(2 倍或 4 倍),所以 60FPS 是为当今的消费电子设备开发游戏的合理帧速率。接下来,让我们在你的游戏中实现 GamePlayLoop.java 类,它将子类化 AnimationTimer 来访问脉冲。
创建一个新的 Java 类:GamePlayLoop.java
让我们使用 javafx.animation 包中的 AnimationTimer 超类来创建一个自定义的 GamePlayLoop 类(以及最终的对象)和所需的。handle()方法来处理您的游戏进行计算。如图 7-2 所示,在 NetBeans 8.0 中,右键单击项目层次结构窗格中的 invincibagel 包文件夹即可完成此操作。这将向 NetBeans 显示在创建新的 Java 类后,您希望将它放在哪里。
图 7-2。
Right-click the invincibagel package folder, and use the New ➤ Java Class menu sequence
点击新建➤ Java 类,将打开新建 Java 类对话框,如图 7-3 所示。将该类命名为 GamePlayLoop,保留 NetBeans 设置的其他默认值,具体取决于您右键单击 invincibagel 包文件夹,然后单击 Finish。
图 7-3。
Name the new Java class GamePlayLoop, and let NetBeans set up the other fields
NetBeans 将为 GamePlayLoop.java 类创建一个引导基础结构,带有一个包和一个类声明,如图 7-4 所示。现在,添加一个 extends 关键字和 AnimationTimer。
图 7-4。
NetBeans creates a GamePlayLoop.java class and opens it in an editing tab in the IDE, for you to edit
鼠标悬停在错误上,按 Alt+Enter,选择添加导入,如图 7-5 所示。
图 7-5。
Subclass an AnimationTimer superclass with an extends keyword: press Alt+Enter, and select Add import
一旦 NetBeans 添加了import javafx.animation.AnimationTimer;
编程语句,您就可以开始创建这个类了,它将为您利用 JavaFX 脉冲引擎,并包含您所有的核心游戏循环处理,或者对将执行各种类型处理的类和方法的调用,例如精灵移动、精灵动画、碰撞检测、物理模拟、游戏逻辑、音频处理、AI、记分牌更新等等。
创建 GamePlayLoop 类结构:实现你的。handle()方法
请注意,一旦 NetBeans 为您编写了 import 语句,GamePlayLoop 类名下方就会出现另一个红色波浪状错误高亮显示。将鼠标放在上面,查看与这个最新错误相关的错误消息。如图 7-6 所示。每个 AnimationTimer 子类所需的 handle()方法还没有在这个 GamePlayLoop.java 类中实现(也称为 overridden ),所以接下来必须这样做。也许你甚至可以让 NetBeans 帮你写代码;让我们来看看,看看!
图 7-6。
Once you extend and import AnimationTimer, NetBeans throws an error: class does not implement the .handle()
正如您在弹出的错误消息的左下方看到的,您可以使用 Alt+Enter 组合键来打开一个帮助器对话框,它将为您提供几个解决方案,其中一个将实际编写未实现的。handle()方法。选择实现所有抽象方法,如图 7-7 所示,以蓝色突出显示。双击此选项后,NetBeans 将为您编写此方法结构:
@Override
public void handle (long``now
throw new UnsupportedOperationException("Not supported yet.");}
请注意,an @Override 关键字位于公共 void 句柄方法访问关键字、返回类型关键字和方法名称之前。这告诉 Java 编译器。handle()方法将替换(覆盖)AnimationTimer 的。handle()方法,这就是为什么该错误指示您必须重写抽象方法。手柄(长)。
你肯定不希望你的。handle()方法在游戏循环中每秒抛出 60 个 UnsupportedOperationException()错误;但是,您现在将把它留在这里,以便您可以看到它的作用,并了解更多关于 NetBeans 错误控制台的信息。
图 7-7。
Take a coding shortcut: press Alt+Enter to bring up a helper dialog, and select Implement all abstract methods
如图 7-8 所示,一旦选择了实现所有抽象方法选项,Java 代码就没有错误了,类的基本包-导入-类-方法结构也就就位了。现在,您应该能够使用该类创建一个 GamePlayLoop 对象,所以让我们换个方式,在 InvinciBagel Java 类中进行一些编程,在该类中,您创建一个 GamePlayLoop 对象,然后分析应用以查看它做了什么。
图 7-8。
NetBeans creates a public void handle(long now) bootstrap method with UnsupportedOperationException
创建 GamePlayLoop 对象:添加脉冲控制
接下来,您需要声明、命名和实例化一个名为 gamePlayLoop 的 GamePlayLoop 对象,使用您创建的新类,结合 Java new 关键字。点击 InvinciBagel.java 选项卡,如图 7-9 所示,在声明 gamePlayLoop 对象的 Insets 对象声明下添加一行代码,命名为 GamePlayLoop,如下所示:
GamePlayLoop``gamePlayLoop
图 7-9。
Click the InvinciBagel.java editing tab, and declare a GamePlayLoop object named gamePlayLoop at the top
正如您所看到的,代码是没有错误的,因为 NetBeans 已经找到了您的 GamePlayLoop 类,它包含了被覆盖的。handle()方法,其父 AnimationTimer 类有一个构造函数方法,可以使用 GamePlayLoop 类创建 AnimationTimer(类型)对象,扩展 AnimationTimer。
现在,您必须使用 Java new 关键字在内存中实例化或创建 GamePlayLoop 对象的实例。这在游戏第一次开始时完成一次,这意味着实例需要放入。start()方法。
您可以在创建所有其他场景图节点对象和 ActionEvent EventHandler 对象后,使用以下 Java 代码行来完成此操作(另请参见图 7-10 ):
gamePlayLoop =``new
这个代码放置的逻辑(在最后)是根据创建和配置来设置所有静态对象,然后在最后创建动态对象,该对象将处理脉冲相关的逻辑。
图 7-10。
At the end of the .start() method, instantiate the gamePlayLoop object by using the Java new keyword
分析 GamePlayLoop 对象:运行 NetBeans Profiler
让我们使用配置文件➤项目配置文件菜单序列来运行 NetBeans Profiler,以确定您是否可以在任何配置文件视图中看到您创建的 GamePlayLoop 对象。如图 7-11 所示,GamePlayLoop < init >调用用了不到 2 毫秒的时间在内存中设置 GamePlayLoop 对象供您使用,使用的开销很小。
图 7-11。
Use a Profile ➤ Profile Project menu sequence to start the Profiler and look at GamePlayLoop memory use
接下来,让我们通过向下滚动 Profiler 选项卡来研究 threads analysis 窗格,如图 7-11 的左上角所示。查找线程图标 NetBeans 会询问您是否要启动线程分析工具;一旦你同意,它将打开螺纹标签(见图 7-12 )。
图 7-12。
Click the Threads icon, seen at the left of the screen, and open the Threads tab; the same eleven threads are running
如果您想知道为什么在图 7-12 所示的线程对象中看不到任何“信号”,就像您在上一章中单击按钮对象时所做的那样,您的假设是正确的,您应该在该图的某个地方看到 JavaFX 脉冲 engine 计时事件,所有的线程栏都是纯色的,因此没有动作或脉冲事件触发。我将让您根据需要经常使用 NetBeans profiling 实用程序,以便熟悉它,因为许多开发人员都避免使用该工具,因为他们还不习惯使用它。
你没有看到任何事件的原因是仅仅创建游戏循环对象是不够的。它内部的 handle()方法来抓取脉冲事件。因为它是一个定时器对象(确切地说,是一个动画定时器),像任何定时器一样,它需要启动和停止。接下来让我们为游戏循环创建这些方法。
控制你的游戏循环:。开始( )和。停止( )
因为 AnimationTimer 超类具有。开始()和。stop()方法控制类(对象)何时(使用. start()方法调用)和何时(不使用. stop()方法调用)处理脉冲事件,您只需在方法代码中使用 Java super 关键字将这些函数“向上”传递给 AnimationTimer 超类。您将重写。使用 Java @Override 关键字启动()方法,然后使用以下方法编程结构将方法调用功能传递给 AnimationTimer 超类:
@Overridepublic void start() {
super
.start();
}
的。stop()方法结构将被覆盖,方法功能以完全相同的方式传递给超类,使用下面的 Java 方法编程结构:
@Overridepublic void stop() {
super
.stop();
}
如图 7-13 所示,GamePlayLoop 类代码是无错误的,现在您可以在 InvinciBagel 类中编写启动 GamePlayLoop AnimationTimer 对象的代码,这样您就可以在分析应用时看到 脉冲对象。
图 7-13。
Adding .start() and .stop() methods to the GamePlayLoop class and using the Java super keyword properly
你需要打这个电话。start()方法关闭名为 gamePlayLoop 的 GamePlayLoop 对象。您刚刚创建的 start()方法。点击 InvinciBagel.java 选项卡,如图 7-14 所示,在 GamePlayLoop 对象实例化下面添加一行代码,调用。start()方法关闭名为 gamePlayLoop 的 GamePlayLoop 对象,如下所示:
gamePlayLoop.``start
如您所见,方法调用已经就绪,Java 代码没有错误,因为 NetBeans 现在可以找到。GamePlayLoop 类中的 start()方法。接下来,让我们使用运行➤项目序列并测试一两个脉冲,以确定现在将会发生什么,GamePlayLoop AnimationTimer 子类已经使用。start()方法调用。看看在。handle()方法就可以了!
图 7-14。
Call a .start() method off the gamePlayLoop object to start GamePlayLoop AnimationTimer
如图 7-15 所示,您会得到与中的内容相关的重复错误。handle()方法。
图 7-15。
Click Run ➤ Project, and open the Output pane to see errors being generated in .handle()
显然,NetBeans 8.0 并不总是为它为我们编写的引导方法编写最佳代码,所以让我们删除代码的throw new UnsupportedOperationException("Not implemented yet.");
行(参见图 7-13 )。在它的位置,你将插入一个 Java 注释,这会创建一个空方法,如图 7-16 所示。这将允许您的游戏应用运行。虽然游戏应用窗口启动时抛出了错误,但是场景图形的组件没有写入场景,只能看到默认的白色背景色。如果您在 NetBeans 中继续学习,您将会观察到这一点。
图 7-16。
Replace throw new UnsupportedOperationException(); with a comment, creating an empty method
现在,让我们再次使用“剖析➤剖析项目”( InvinciBagel)工作流程,看看 NetBeans 的“实时结果”和“线程”选项卡中是否出现了新内容。点击图 7-17 左侧所示的实时结果图标,并在选项卡中启动实时结果分析器。注意,GamePlayLoop 对象是使用< init >创建的,而 AnimationTimer 是使用 invincibagel 启动的。分析器输出中的 GamePlayLoop.start()条目。
如您所见,初始化每个事件队列只需要几分之一毫秒,包括 脉冲事件和所有四个 ActionEvent EventHandler 事件处理队列。这与我们的最大游戏优化方法一致,使用静态场景图节点,并且不在 GamePlayLoop 内做任何事情,这些事情会消耗更多的系统资源(内存和处理周期),而不是在创建充满动作的街机游戏时完成各种任务所绝对需要的资源。
现在您已经创建并启动了 GamePlayLoop 对象,让我们来看看线程档案器!
图 7-17。
Use a Profile ➤ Profile Project menu sequence to start the Profiler, and look at GamePlayLoop memory use
再次,向下滚动图 7-17 左上角所示的 Profiler 选项卡,找到图 7-18 左上角显示的 Threads 图标。NetBeans 将询问您是否要启动线程分析工具;一旦你同意,它将打开线程标签。如图 7-18 所示,脉冲引擎正在运行,显示线程 6 的几个脉冲事件。有趣的是,一旦 JavaFX 确定。handle()方法为空,脉冲事件系统不会继续处理这个空。handle()方法并使用不必要的脉冲事件,这表明 JavaFX 脉冲事件系统具有一定的智能。
图 7-18。
Click the Threads icon, seen at the left side of the screen, and open a Threads tab; AnimationTimer pulses can be observed on Thread-6
InvinciBagel 图:包、类和对象
接下来,让我们以图表的形式看看你当前的包、类和对象层次结构(见图 7-19 ),看看你在创建你的游戏引擎方面处于什么位置。在图的右侧是 InvinciBagel 类,它保存场景图,以及 Stage、Scene 和 StackPane 对象,这些对象保存并显示您的闪屏 UI 设计。图的左边是 gamePlayLoop 类,它将包含游戏处理逻辑调用,并在 InvinciBagel 类中声明和实例化为 GamePlayLoop 对象,但不是场景图形层次的一部分。很快,您将开始构建图表中显示的其他功能区域,以便您可以控制您的精灵,检测精灵之间的碰撞,并模拟真实世界的物理力,使游戏更加逼真。随着您阅读本书并创建您的 Java 8 游戏,您将会看到该图的补充。
图 7-19。
Current invincibagel package, class, and object hierarchy, after addition of the GamePlayLoop
接下来,在进入 GamePlayLoop AnimationTimer 类和对象之前,您将在当前空的中放置一些相对简单的 Java 代码。handle()方法。您将这样做,以确保脉冲引擎正在处理,并看看 60FPS 有多快!(我不得不承认,我的好奇心占了上风!).
测试 GamePlayLoop:制作 UI 容器动画
让我们围绕 InvinciBagel 闪屏逆时针移动一个现有的场景图节点,例如 HBox 布局容器父(分支)节点,它包含四个 UI 按钮控件元素。您将通过使用一个简单的 if-else Java 循环控制编程结构来读取(使用. get()方法)和控制(使用. set()方法)控制(在本例中)屏幕位置角的 Pos 常量。
首先,在 GamePlayLoop 类的顶部声明一个名为 location 的 Pos 对象。然后,单击突出显示的错误消息,按 Alt+Enter,并选择“导入 Pos 类”选项,以便 NetBeans 为您编写导入语句。接下来在。handle()方法,添加一个 if-else 条件语句,该语句计算这个名为 location 的 Pos 对象,并将其与表示显示屏四个角的四个 Pos 类常量进行比较,这四个角包括 BOTTOM_LEFT、BOTTOM_RIGHT、TOP_RIGHT 和 TOP_LEFT。您的 Java 代码应该类似于下面的 if-else 条件语句 Java 程序结构(参见图 7-20 ):
Pos``location
@Overridepublic void handle(long now) {
location``= InvinciBagel.buttonContainer.``getAlignment()
if
(location == Pos.BOTTOM_LEFT) {
InvinciBagel.buttonContainer.setAlignment(Pos.BOTTOM_RIGHT);
}``else if
InvinciBagel.buttonContainer.setAlignment(Pos.TOP_RIGHT);
}``else if
InvinciBagel.buttonContainer.setAlignment(Pos.TOP_LEFT);
}``else if
InvinciBagel.buttonContainer.setAlignment(Pos.BOTTOM_LEFT);}}
如图所示,您的代码没有错误,您已经准备好使用“运行➤项目”工作流程并观看 60FPS 的焰火了!准备好享受炫目的速度吧!
图 7-20。
Create an if-else loop that moves the HBox UI counterclockwise around the four corners of a splash screen
接下来,让我们最后一次运行实时结果分析器和线程分析器,看看您的脉冲引擎是否正在启动!一旦你这样做了,你就会知道你已经成功地为你的游戏实现了你的 GamePlayLoop 计时引擎,然后你就可以把你的注意力转移到开发你的游戏精灵,碰撞检测,物理和逻辑上了!
剖析游戏循环:脉冲引擎
现在,让我们最后一次使用“剖析➤剖析项目”( invincibagel)工作流程,看看 NetBeans 的“实时结果”和“线程”选项卡中是否出现了新内容。点击图 7-21 左侧所示的实时结果图标,并在选项卡中启动实时结果分析器。注意,GamePlayLoop 对象是使用< init >创建的;使用 invincibagel 启动了一个动画定时器。分析器输出中的 GamePlayLoop.start()条目;有一个不可战胜的怪物。GamePlayLoop.handle(long)条目,这意味着您的游戏计时循环正在被处理。
正如您所看到的,调用列显示了有多少脉冲访问了。GamePlayLoop 中的 handle()方法。处理 3,532 个脉冲只需要 40.1 毫秒,因此使用新的 Java 8 计时分辨率,每个脉冲相当于 0.0114 毫秒,即 114 纳秒。因此,您当前用于测试脉冲的代码,或者至少是 JavaFX 脉冲引擎,是高效运行的。
图 7-21。
Run the Live Results Profiler
当然,您需要从。handle()方法,然后进入下一章,在下一章中,您将开始处理这个方法中的游戏资产和逻辑。
接下来,让我们最后一次向下滚动图 7-21 左上角显示的 Profiler 选项卡,点击图 7-22 左上角显示的 Threads 图标,打开 Threads 选项卡。正如您所看到的,脉冲引擎正在运行,可以看到在 Thread-6 和 JavaFX 应用线程中处理脉冲事件。
鉴于空虚。handle()方法处理来自 Thread-6 中的 GamePlayLoop 对象(见图 7-18 ),可以假设 Thread-6 中的脉冲事件来自 GamePlayLoop AnimationTimer 子类。这意味着 JavaFX 应用线程中显示的脉冲事件显示了。handle()方法正在访问 InvinciBagel 类中 stackPane 场景图形根中包含的 buttonContainer HBox 对象。
图 7-22。
Run the Threads Profiler
现在,您已经有了一个低开销、速度极快的游戏处理循环,您可以开始创建您的其他(精灵、碰撞、物理、得分、逻辑等等)游戏引擎了!一个搞定了,还有一大堆要做!
摘要
在第七章中,您编写了在本书的课程中将设计和编码的许多游戏引擎中的第一个,GamePlayLoop 游戏播放计时类和对象,它们允许您利用强大的 JavaFX 脉冲事件处理系统。
首先,您研究了 javafx.animation 包中的不同类,以及使用 animation、Transition、Timeline 和 AnimationTimer 类来利用 JavaFX 脉冲事件处理系统的不同方法。
之后,您学习了如何在 NetBeans 中创建新的 Java 类,然后扩展了 AnimationTimer 超类以创建 GamePlayLoop 子类,它将以 60FPS 的速度处理您的游戏逻辑。您看到了如何使用 NetBeans 来帮助您编写这个新子类的大部分代码,包括 package 和 class 语句、import 语句和 bootstrap。handle()方法。
接下来,您进入 InvinciBagel.java 类,使用您创建的新类声明并命名了一个新的 gamePlayLoop GamePlayLoop 对象。然后,您测试了代码并对其进行了概要分析,以查看 Threads Live Results 选项卡中是否出现了任何新条目。您还测试了。NetBeans 为您编码的 handle()方法,并将其更改为空方法,以消除由脉冲事件引擎引发的重复错误。接下来,您实现了。开始()和。stop()方法,使用 Java super 关键字,这样您就可以控制对 脉冲引擎的使用,如果您想要添加额外的 Java 语句,比如保存游戏状态,稍后,当 脉冲引擎启动和停止时。您再次测试并分析了应用,以观察您的进展。最后,您将一些测试代码放在。handle()方法,这样您就可以再次测试和分析应用,以确保脉冲事件引擎快速一致地处理您放在。handle()方法。
在下一章,你将会看到如何创建和实现抽象类,这些抽象类将会被用来创建你的游戏精灵。一旦我们有了这些,它将允许我们在后面的章节中实时地在你的新游戏循环引擎中的显示屏上显示它们,制作它们的动画,并处理它们的运动。
八、创建你的演员引擎:为你的游戏设计角色并定义他们的能力
既然我们已经在第七章的中创建了游戏计时循环,让我们在第八章的中进入一些临时代码,并创建公共抽象类框架,我们可以用它来创建我们将在无敌百吉游戏中使用的不同类型的精灵。这实质上等同于你的游戏的“角色引擎”,因为你将定义和设计你的游戏将包括的各种类型的游戏组件作为角色,这两个类将用于创建所有其他类,这些类将用于创建你的游戏中的对象(组件)。这些将包括诸如无敌面包圈本身(面包圈类)、他的对手(敌人类)、他在游戏中寻找的所有宝藏(宝藏类)、射向他的东西(抛射体类)、他在上面和周围导航的东西(道具类),所有这些都提供了无敌面包圈必须尝试和实现的游戏目标。
在本章中,我们将创建两个公共抽象类结构。第一个,Actorclass,将是另一个,Hero 子类的超类。这两个抽象类可以在书中用来创建我们的固定精灵,它们是不动的精灵(障碍和宝藏),使用 Actor 超类,和在屏幕上移动的精灵,使用 Actor 类的 Hero 子类。英雄类将为运动精灵(超级英雄,和他在多人游戏版本中的主要敌人)提供额外的方法和变量。这些将跟踪像碰撞和物理特性,以及演员动画(运动状态)。在屏幕上有大量的动作会让游戏玩起来更有趣,并且让我们让游戏对玩家来说更有挑战性。
预先创建 Actor 引擎将使您获得创建公共抽象类的经验。正如你在第三章中所回忆的,公共抽象类在 Java 中被用来创建其他类(和对象)结构,但并不直接用在实际的游戏编程逻辑中。这就是为什么我称创建这两个“蓝图”类为创建演员引擎,因为我们本质上定义了游戏的最低级别,在这一章中的“演员”。
随着本书中游戏设计的进展,我们将使用 Actor (fixed sprite)类创建 Treasure 子类,用于在游戏过程中 InvinciBagel 将获得的“固定”宝藏。我们还将使用这个 Actor 超类创建 Prop 类,用于 InvinciBagel 必须成功地向上、向上、向下、绕过或通过的游戏中的障碍。我们还将使用 Actor 超类的 Hero 子类创建在屏幕上移动的精灵,比如 Bagel 类。我们最终会创造一个敌人职业和抛射职业。
除了在本章中设计两个关键的公共抽象演员类,我们还将使用不到 10 个 PNG32 数字图像资源来定义我们的主要无敌角色的数字图像状态。我们将在本章中这样做,以便在我们想要在本书的下一章中使用这些类和精灵图像状态之前,我们将在下一章中查看事件处理,以便玩家可以控制 InvinciBagel 在屏幕上的位置以及什么状态(站立、奔跑、跳跃、飞跃、飞行、着陆、错过、蹲伏等)。)他用它来导航他世界中的障碍。
游戏角色设计:预先定义属性
任何流行游戏的基础都是角色——英雄和他的主要敌人——以及游戏的障碍、武器库(投射物)和宝藏。这些“角色”中的每一个都需要使用 Java 中的变量来定义属性,这些属性使用系统内存区域来实时跟踪每个角色在游戏过程中发生了什么。我将尝试在第一次就正确地做到这一点,就像您希望在第一次定义数据库时,定义一个数据库记录结构来保存将来需要的数据一样。这可能是你游戏开发的一个具有挑战性的阶段,因为你需要展望未来,确定你希望你的游戏和它的演员有什么样的功能,然后把这些放到你的演员的能力(变量和方法)前面。图 8-1 让你了解在本章的过程中我们将为游戏角色安装的 24 个属性中的一些,因为我们创建了一百多行代码来实现我们的游戏角色引擎。
图 8-1。
Design a public abstract Actor superclass and a public abstract Hero subclass to use to create sprite classes
如你所见,我试图得到一个平衡的变量数量;在这种情况下,在固定精灵演员类和运动精灵英雄类之间,每个大约有 12 个。正如你从第三章中所知道的,因为我们将要创建的 Hero 子类扩展了 Actor 超类,它实际上有 24 个属性或特征,因为它假设了所有的超类变量,除了有自己的。一个设计上的挑战是将尽可能多的这些属性放在 Actor 超类中,这样固定的精灵就有尽可能多的灵活性。一个很好的例子是,在第一轮设计中,我在 Hero 类中有枢轴点(pX 和 pY 变量),但后来我想了想“如果我想稍后旋转固定精灵(障碍和宝藏)以获得更高的设计效率会怎么样”,所以我将这些变量放在 Actor 超类中,将这种枢轴(旋转)功能赋予固定和运动精灵。
我在 Hero 类中“上移”到 Actor 超类的另一个变量是 List 属性。在这个设计过程中,我对自己说,“如果出于某种原因,我希望我的固定精灵有不止一个图像状态呢?”我还将 Actor 类从使用简单的 Rectangle Shape 对象升级为使用 SVGPath Shape 子类,这样我就可以使用比矩形更复杂的形状来定义碰撞几何体(这就是 spriteBounds 变量的含义),以支持游戏后期更复杂的高级障碍结构。
还要注意,我在 Actor 类中有 spriteFrame ImageView,它保存 sprite 图像资产,因为固定和运动 sprite 都使用图像,所以我可以将 ImageView 放入 Actor 超类中。我在 Actor 超类中使用 imageStates 列表,这样固定精灵就可以像运动精灵一样访问不同的“视觉状态”。正如您可能已经猜到的,List 是一个填充了 JavaFX Image 对象的 Java List 对象。Actor 类中的 iX 和 iY 变量是图像(或初始)位置 X 和 Y,它们在游戏级别布局上放置一个固定的 sprite,但当由 Hero 子类假定时,也将保持运动 sprite 的当前 sprite 位置。其他变量保存布尔状态(活/死等。)和寿命、损坏、偏移、碰撞或我们稍后需要的物理数据。
无敌精灵图像:视觉动作状态
除了设计用于实现游戏中的角色、宝藏和障碍的最佳演员引擎类之外,另一个要优化的重要内容是游戏的主要角色,以及角色根据玩家的角色移动而在动画的不同状态之间移动。从内存优化的角度来看,我们能够完成所有这些的图像帧越少越好。正如你在图 8-2 中看到的,我将只使用九种不同的精灵图像资源来提供所有的无敌角色运动状态;其中一些可以以多种方式使用:例如,通过使用 pX 和 pY 变量,这将允许我们围绕我们选择的任何枢轴点旋转这些 sprite 帧。这方面的一个例子是飞行状态的中心轴点放置,如图 8-2 中间所示,通过将该图像顺时针旋转 50 度(水平方向)到 100 度(倾斜向下飞,而不是向上飞),我们就可以起飞(向上飞)、飞行和着陆(向下飞)。
图 8-2。
The nine primary character motion sprites for the InvinciBagel character that will be used during the game
尽管我们在 sprite Actor 引擎抽象类中提供了偏移和枢轴点功能,但这并不意味着我们不应该确保我们的运动 sprite 图像状态彼此之间很好地同步。这使得我们不必经常使用这些旋转或偏移功能来获得良好的视觉效果。这就是我所说的子画面注册,包括不同的子画面状态相对于彼此的最佳定位。
在图 8-3 中可以看到一些将相互使用的子画面帧之间的子画面注册的例子。例如,开始运行 imageStates[1]精灵应该以与站立(或等待)imageStates[0]精灵相同的脚位置开始其运行周期,如图 8-3 左侧所示。此外,相对于开始运行 sprite 的 imageStates[1],运行的 imageStates[2] sprite 应该尽可能保持其主体部分不动。一个准备着陆的 imageStates[6] sprite 应该相对于着陆的 imageStates[7] sprite 真实地改变脚的位置。
图 8-3。
Sprite registration (alignment) to make sure the transition motion is smooth
相对于所有其他精灵,您想要做的是优化精灵注册,将所有数字图像精灵放入相同的正方形 1:1 纵横比分辨率图像格式,并将它们全部放在数字图像合成软件包(如 GIMP 或 Photoshop)的层中。然后使用移动工具和轻推(使用键盘上的箭头键移动单个像素)每个精灵到适当的位置,相对于您打开可见性(使用每个层左侧的眼睛图标打开/关闭)的两个层。结果如图 8-3 所示。
创建执行元超类:修复执行元属性
让我们开始编写我们的公共抽象演员类,这将是我们在本书中为游戏创建的所有精灵的基础。我不会重新讨论如何在 NetBeans 中创建新类(参见图 7-2 ),因为您已经在第七章中了解到了这一点,所以创建一个 Actor.java 类,使用公共抽象类 Actor 声明它,并将前五行代码放在类的顶部,声明一个名为 imageStates 的 List < Image >,创建一个新的 ArrayList < >对象,以及一个名为 spriteFrame 的 ImageView、一个名为 spriteBound 的 SVGPath 和双变量 iX 和 iY。对所有这些进行保护,这样任何子类都可以访问它们,如图 8-4 所示。对于与 List 类(对象)、ArrayList 类(对象)、Image 类(对象)、ImageView 类(对象)和 SVGPath 类(对象)所需的导入语句相关的红色错误下划线,您需要使用 Alt-Enter 工作流程。一旦 NetBeans 为您编写了这些代码,声明 List < Image > ArrayList、spriteFrame ImageView、SVGPath collision Shape 对象以及包含 sprite 的 X 和 Y 位置的 double 变量的十几行代码应该类似于下面的 Java 类结构:
package invincibagel;import java.util.ArrayList;import java.util.List;import javafx.scene.image.Image;import javafx.scene.image.ImageView;import javafx.scene.shape.SVGPath;
public``abstract
protected List<Image>``imageStates
protected ImageView``spriteFrame
protected SVGPath``spriteBound
protected double``iX
protected double``iY
}
图 8-4。
Create a New Class in NetBeans, name it public abstract class Actor, and add in the primary Actor variables
这五个变量或属性持有任何 sprite 的“核心”属性;spriteFrame ImageView 和它保存的图像资产(一个到多个可见状态)的 List ArrayList(这定义了子画面的外观)、spriteBound 碰撞形状区域(定义了被认为与子画面相交的区域)以及子画面在显示屏上的 X,Y 位置。
这五个变量也将在稍后使用 Actor()构造函数方法和 Hero()构造函数方法进行配置。首先我们将创建 Actor()构造函数;之后,我们将添加所有其他变量,我们需要每个 Actor 子类都包含这些变量。
在我们为 Actor 类创建了所有其他变量(这些变量不是使用 Actor()构造函数方法设置的)之后,我们将初始化这些变量以在构造函数方法中保存它们的默认值,最后我们将让 NetBeans 创建。get()和。使用一个你会喜欢的自动编码函数为我们的变量设置()方法。
我们将编码并传递给这个 Actor()构造函数的参数将包括名为 SVGdata 的 String 对象,它将包含一个定义 SVGPath 冲突形状的文本字符串,以及 sprite X,Y 位置和一个逗号分隔的图像对象列表。SVGPath 类有一个. setContent()方法,可以读取或“解析”原始 SVG 数据字符串,因此我们将使用它将字符串 SVG data 变量转换为 SVGPath 碰撞形状对象。
我们将不会在本章或下一章中实现碰撞代码或 SVGPath Shape 对象,但我们需要将它们放在适当的位置,这样我们可以在后面的第十六章碰撞检测处理以及如何使用 GIMP 和 PhysEd (PhysicsEditor)软件包创建碰撞多边形数据中使用它们。
我们将创建的 Actor 构造函数方法将遵循以下构造函数方法格式:
public Actor(String``SVGdata``, double``xLocation``, double``yLocation``, Image...``spriteCels``)
稍后,如果我们需要创建更复杂的 Actor()构造函数方法,我们可以通过添加其他更高级的参数来“重载”该方法,例如 pivot point pX 和 pY,或者 isFlipH 或 isFlipV 布尔值,以允许我们水平或垂直镜像固定的 sprite 图像。您的 Java 代码将如下所示:
public Actor(String``SVGdata``, double``xLocation``, double``yLocation``, Image...``spriteCels``) {
spriteBound = new SVGPath();
spriteBound.setContent(``SVGdata
spriteFrame = new ImageView(``spriteCels[0]
imageStates.addAll(Arrays.asList(``spriteCels
iX =``xLocation
iY =``yLocation
}
请注意,使用 Java new 关键字调用的 ImageView 构造函数通过使用 spriteCels[0]注释,使用逗号分隔的列表传递您正在传递的 List ArrayList 数据的第一帧(Image)。如果您要创建一个允许您设置轴心点数据的重载方法,它可能如下所示:
public Actor(String``SVG``, double``xLoc``, double``yLoc``, double``xPivot``, double``yPivot``, Image...``Cels``){
spriteBound = new SVGPath();
spriteBound.setContent(``SVG
spriteFrame = new ImageView(``Cels[0]
imageStates.addAll(Arrays.asList(``Cels
iX =``xLoc
iY =``yLoc
pX =``xPivot
pY =``yPivot
}
如图 8-5 所示,您需要使用 Alt-Enter 工作流程,并让 NetBeans 为您的 Arrays 类编写导入语句。一旦你这样做了,你的代码就不会有错误。
图 8-5。
Create a constructor method to set up fixed Actor sprite subclasses with collision shape, Image list, location
接下来,让我们编写这个类的另一个关键方法,抽象方法。update()方法,然后我们可以添加我们将需要的 Actor 类的其余固定 sprite 属性。之后,我们可以初始化 Actor()构造函数方法中的附加变量。最后,我们将学习如何为 Actor 类创建“getter 和 setter”方法,然后继续使用这个新的自定义 Actor 超类来创建我们的另一个 Hero motion sprites 子类。
创建一个。update()方法:连接到 GamePlayLoop 引擎
对于任何 sprite 类来说,除了创建它的构造函数方法之外,最重要的方法是。update()方法。那个。update()方法将包含 Java 8 代码,告诉 sprite 在 GamePlayLoop 的每个脉冲上做什么。因为这个原因,这个。update()方法将用于将使用我们的 Actor 超类和 Hero 子类创建的 Actor sprite 子类“连接”到我们在第七章中创建的 GamePlayLoop 计时引擎中。
因为我们需要一个。update()方法作为游戏中每个 Actor 对象(actor sprite)的一部分,我们需要包含一个“空的”(目前)抽象。我们当前正在编写的 Actor 超类中的 update()方法。
正如您在第三章中了解到的,这个公共抽象方法在 Actor 超类中是空的,或者更准确地说,是未实现的,但是需要在任何 Actor 子类中实现(也就是说,需要被实现)(或者再次声明为抽象方法),包括我们稍后将要编码的 Hero 子类。
该方法被声明为 public abstract void,因为它不返回任何值(它只是在每个 JavaFX 脉冲事件上执行)并且不包含{…}花括号,因为它里面(还)没有任何代码体!声明公共抽象(空的或未实现的)方法的单行代码应该如下所示:
public``abstract``void``update
正如你在图 8-6 中看到的,这个方法实现起来非常简单,一旦你在你的 Actor()构造函数方法下添加了这个新方法,你的 Java 8 代码再次没有错误,你就可以准备添加更多的代码了。
图 8-6。
Add an Arrays import statement to support constructor method; add a public abstract .update() method
接下来,我们将为我们的固定精灵演员超类添加其余的属性(或变量),这需要我们提前考虑,在创建这个游戏期间,我们希望能够用我们的精灵完成什么。
向 Actor 类添加 Sprite 控件和定义变量
从编码的角度来看,这个过程的下一部分很简单,因为我们将在 Actor 类的顶部声明更多的变量。然而,从设计的角度来看,这更加困难,因为它要求我们尽可能地提前思考,并推测我们的精灵演员(固定精灵和运动精灵)需要什么样的可变数据,以便能够在游戏的构建和游戏过程中做我们想做的一切。
在 iX 和 iY 变量之后,我要声明的第一个附加变量是 pX 和 pY 枢轴点变量。我最初将它们放在 Hero 子类中,一旦我们完成了这个 Actor 超类的创建,接下来我们将创建它。我将这些“升级”到演员超类级别的原因是因为我想拥有旋转固定精灵(宝藏和障碍)以及运动精灵的灵活性。在关卡和场景设计方面,这给了我更多的权力和灵活性。这些支点 X 和 Y 变量将被声明为受保护的 double 数据变量,并使用下面两行 Java 代码来完成:
protected double``pX
protected double``pY
接下来,我们需要在 Actor 类(对象)定义中添加一些布尔“标志”。这些将指示关于所讨论的精灵对象的某些事情,例如它是活的(对于固定精灵这将总是假的)还是死的,或者它是固定的(对于固定精灵这将总是真的,对于不在运动中的运动精灵也是真的)还是移动的,或者奖励对象,指示它们的捕获(碰撞)的附加点(或寿命),或者有价值的,指示它们的获取(碰撞)的附加能力(或寿命)。最后,我定义了一个水平翻转和垂直翻转标志,与没有这些标志的情况相比,使用(固定的或运动的)精灵图像资源给了我四倍的灵活性。
由于 JavaFX 可以在 X 或 Y 轴上翻转或镜像图像,这意味着我可以使用 FlipV 反转精灵方向(向左或向右),或使用 FlipH 反转方向(向上或向下)。
这六个额外的布尔标志固定(演员)sprite 属性将通过使用受保护的布尔数据变量来声明,使用以下六行 Java 8 代码,如图 8-7 所示(没有错误,没有减少):
protected boolean``isAlive
protected boolean``isFixed
protected boolean``isBonus
protected boolean``hasValu
protected boolean``isFlipV
protected boolean``isFlipH
图 8-7。
Add the rest of the variables needed to support rotation (pivot point), and sprite definition states
接下来,我们将在 Actor()构造函数方法中初始化这些变量。如果您想使用参数列表将这些布尔标志的设置传递给 Actor()构造函数方法,请记住,您可以创建任意多的重载构造函数方法格式,只要每个格式的参数列表都是 100%唯一的。在本书的后面部分,我们可能会这样做,例如,如果我们需要一个构造函数方法来为布局设计的目的旋转我们的固定精灵,或者一个围绕给定的轴翻转它,例如,为了相同的确切目的,或者一个两者都做的方法,这将给我们一个九参数 Actor()构造函数方法调用。
在 Actor 构造函数方法中初始化 Sprite 控件和定义变量
现在,我们将把轴心点 pX 和 pY 初始化为 0(左上角原点),所有布尔标志的值都设为 false,只有 isFixed 变量除外,对于固定的 sprite,它的值总是设为 true。我们将在当前 Actor()构造函数方法中使用以下八行 Java 代码,并在该方法中处理使用方法参数配置 Actor 对象的前四行代码的下面使用这些代码:
pX = 0;pY = 0;isAlive = false;isFixed = true;isBonus = false;hasValu = false;isFlipV = false;isFlipH = false;
我们也可以使用复合初始化语句来实现这一点。这将把代码减少到三行:
px = pY =
0;
isFixed = true;
isAlive = isBonus = hasValu = isFlipV = isFlipH =
false;
正如你在图 8-8 中看到的,我们现在已经编写了近 30 行无错误的 Java 8 代码,我们准备创建剩下的。get()和。set()方法将组成公共抽象 Actor 超类。
图 8-8。
Add initialization values to the eight new fixed sprite pivot and state definition variables you just declared
Actor 类中的其余方法通常称为“getter”和“setter”方法,因为这些方法提供了对类内部数据变量的访问。使用 getter 和 setter 方法是正确的做法,因为这样做实现了 Java 封装的概念(和优势),它允许 Java 对象成为对象属性(可变数据值)和行为(方法)的自包含容器。
访问参与者变量:创建 Getter 和 Setter 方法
NetBeans 的一个真正强大的(并且节省时间的)功能是,它将编写您的所有。get()和。自动为每个对象和数据变量设置()方法。我们将在本书中尽可能使用这一便利的特性,因此您可以习惯于使用这一节省时间的特性来为您编写大量 Java 8 代码,加速您的 Java 8 游戏代码生产输出!您可以通过使用源菜单及其插入代码子菜单来访问该自动编码功能,如图 8-9 所示。可以看到,还有一个键盘快捷键(Alt-Insert);使用其中任何一个都会调出浮动生成菜单,该菜单在图 8-9 的底部中央以红色高亮显示。
图 8-9。
Use Source ➤ Insert Code menu (or Alt+Insert) to bring up a Generate Getter and Setter dialog and select all
点击在生成浮动菜单中间高亮显示的 Getter 和 Setter 选项,将出现一个生成 Getter 和 Setter 对话框,如图 8-9 右侧所示。确保层次结构是打开的,并且 Actor 旁边的复选框被选中,这将自动选择该类中的所有变量,在这种情况下,在图 8-9 的右侧也显示了十几个被选中的变量。
一旦所有这些都被选中,点击对话框底部的 Generate 按钮,生成 24。get()和。如果 NetBeans 8.0 没有提供这一便利的 IDE 功能,则必须手动键入 set()方法。
这些。get()和。由 NetBeans 8.0 源代码➤插入代码➤生成➤ Getters 和 Setters 菜单序列生成的 set()方法将为您提供以下 24 个 Java 方法代码构造,相当于我们在公共抽象 Actor 类中定义的 12 个变量中的每一个都有两个方法:
public List<Image>``getImageStates()
return imageStates;}
public void``setImageStates(List<Image> imageStates)
this.imageStates = imageStates;}
public ImageView``getSpriteFrame()
return spriteFrame;}
public void``setSpriteFrame(ImageView spriteFrame)
this.spriteFrame = spriteFrame;}
public SVGPath``getSpriteBound()
return spriteBound;}
public void``setSpriteBound(SVGPath spriteBound)
this.spriteBound = spriteBound;}
public double``getiX()
return iX;}
public void``setiX(double iX)
this.iX = iX;}
public double``getiY()
return iY;}
public void``setiY(double iY)
this.iY = iY;}
public double``getpX()
return pX;}
public void``setpX(double pX)
this.pX = pX;}
public double``getpY()
return pY;}
public void``setpY(double pY)
this.pY = pY;}
public boolean``isAlive()
return isAlive;}
public void``setIsAlive(boolean isAlive)
this.isAlive = isAlive;}
public boolean``isFixed()
return isFixed;}
public void``setIsFixed(boolean isFixed)
this.isFixed = isFixed;}
public boolean``isBonus()
return isBonus;}
public void``setIsBonus(boolean isBonus)
this.isBonus = isBonus;}
public boolean``hasValu()
return hasValu;}
public void``setHasValu(boolean hasValu)
this.hasValu = hasValu;}
public boolean``isFlipV()
return isFlipV;}
public void``setIsFlipV(boolean isFlipV)
this.isFlipV = isFlipV;}
public boolean``isFlipH()
return isFlipH;}
public void``setIsFlipH(boo lean isFlipH)
this.isFlipH = isFlipH;}
注意,除了。get()和。set()方法生成,对于布尔变量还有一个附加的。是()方法,它是代替。get()方法。因为我已经使用“is”前缀命名了布尔标志,所以我将删除第二个“Is ”,以便这些“double is”方法更具可读性。我还将对 hasValu 方法做同样的事情,这样在方法调用中查询布尔设置就更自然了,例如。hasValu()、isFlipV()、isBonus()、isFixed()或。例如 isFlipH()。为了可读性,我建议您对代码进行同样的编辑。
现在我们准备创建我们的 Hero 子类,它将向我们在 Actor 类中创建的 13 个属性添加另外 11 个属性,使总数达到 24 个。Hero 类中的这 11 个附加属性将用于可以在屏幕上移动的可移动精灵(我喜欢称之为运动精灵)。在我们游戏的单人版本中,我们的无敌英雄角色将是主要的英雄角色对象,在未来的多人版本中,这将包括无敌英雄角色对象和敌人英雄角色对象。
创建英雄超类:动作演员属性
接下来让我们创建我们的公共抽象英雄类!这个类将是我们在本书中为游戏创建动作精灵的基础。在 NetBeans 中创建您的 Hero.java 类,并将其声明为public abstract class Hero extends Actor
。由于我们已经在 Actor 类中完成了许多“繁重的工作”,所以您不必创建 ImageView 来保存 sprite 图像资产,也不必创建 List < Image > ArrayList 对象,该对象加载了一个由 Image 对象填充的 List 对象,或者创建一个 SVGPath Shape 对象来保存碰撞形状 SVG polyline(或多边形)路径数据。
由于我们不必声明任何主属性,因为这些属性是从 Actor 超类继承的,所以我们要做的第一件事是创建一个 Hero()构造函数方法。这将包含字符串对象中的碰撞形状数据,sprite X,Y 位置,以及将加载到 List ArrayList 对象中的图像对象。在我们创建了一个基本的 Hero()构造函数方法之后,我们将完成计算你的运动精灵需要包含的其他属性(或变量),就像我们在设计 Actor 超类时所做的一样。
请记住,您已经有了使用 Actor()方法在 Actor 类中构造的 spriteBound SVGPath Shape 对象、imageStates List ArrayList 对象、SpriteFrames Image 对象以及 iX 和 iY 变量。为了能够编写我们的 Hero()构造函数方法,我们还需要这些。由于这些都已经就绪,由于 Hero 类声明中的 java extends 关键字,我们所要做的就是使用 super()构造函数方法调用,并将这些变量从 Hero()构造函数向上传递给 Actor()构造函数。这将使用 Java super 关键字自动将这些变量传递给 Hero 类供我们使用。
因此,我们已经具备了编写核心 Hero()构造函数方法所需的一切,现在让我们开始吧。Hero()构造函数将接受与 Actor()构造函数相同数量的复杂参数。这些包括碰撞形状数据,包含在名为 SVGdata 的字符串对象中,子画面的“初始位置”X 和 Y 位置,以及子画面的图像对象(cels 或 frames)的逗号分隔列表,我将其命名为 Image…斯普里特塞尔。这张照片…designation 需要在参数列表的末尾,因为它是“开放式的”,这意味着参数列表将传入一个或多个图像对象。您的代码将如下所示:
public void Hero(String``SVGdata``, double``xLocation``, double``yLocation``, Image...``spriteCels``) {
super(``SVGdata``,``xLocation``,``yLocation``,``spriteCels``);
}
通过使用 super()将核心构造函数传递到 Actor 超类 Actor()构造函数方法,您之前编写的代码(在 Actor()构造函数内部)将使用 Java new 关键字和 SVGPath Shape 子类创建 spriteBound SVGPath Shape 对象,并将使用 SVGPath 类。setContent()方法,以便加载 SVGPath Shape 对象以及要用于 sprite 图像状态的碰撞形状。设置了 iX 和 iY 的初始位置,imageStates 列表数组加载了从参数列表末尾传入的 sprite 图像对象。
值得注意的是,因为我们是这样设置的,所以 Hero 类可以访问 Actor 类所拥有的一切(十三个强大的属性)。实际上,反过来看可能更“突出”,演员(固定精灵)类拥有英雄(运动精灵)类的所有能力。这种能力应该用于关卡设计 wow factor,包括多图像状态(List Array)、自定义 SVGPath 碰撞形状功能、自定义枢轴点位置,以及围绕 X 轴(FlipV = true)或 Y 轴(FlipH = true)或两个轴(FlipH = FlipV = true)翻转(镜像)精灵图像的能力。将这些功能放入你的 Actor 引擎(Actor 和 Hero 抽象超类)只是第一步;在你的游戏设计和编程中出色地使用它们,随着时间的推移,你会继续构建和完善游戏,这是本章中奠定基础的最终目标。正如你在图 8-10 中看到的,我们的基本(核心)构造函数代码是没有错误的。
图 8-10。
创建一个公共抽象类 Hero extends Actor 并添加一个构造函数方法和一个 super()构造函数
添加更新和冲突方法:。更新()和。碰撞()
现在我们有了一个基本的构造函数方法,稍后我们会添加它,让我们添加所需的抽象。update()方法,以及. collide()方法,因为运动精灵正在移动,因此可能会与物体发生碰撞!首先让我们添加public abstract void .update();
方法,因为它是我们的 Actor 超类所需要的。这样做实质上是向下传递(或者向上传递,如果您愿意的话)了实现需求。update()方法,从 Actor 超类到 Hero 子类,并继续到 Hero 的任何未来子类(这将使 Hero 成为一个超类,并更好地反映其名称)。未来的非抽象(函数)类将实现这一点。update()方法,该方法将用于完成游戏编程逻辑的所有繁重工作。正如你在图 8-11 中看到的,运动精灵(Hero 子类)也需要有一个碰撞检测方法,我称之为。collide(),因为这是一个更短的名字,至少现在,除了返回一个布尔值 false(这里没有冲突,老板!)布尔数据值。的 Java 代码。collide()方法结构将把一个 Actor 对象作为它的参数,因为这是你的 Hero 对象将与之碰撞的对象,应该如下所示:
public boolean``collide``(``Actor object
return``false
}
图 8-11。
Add the @Override public abstract void .update() and public boolean .collide(Actor object) methods
接下来,我们再给这个英雄类增加十一个变量。这些将保存适用于运动精灵的数据值,这些精灵必须处理与物体的碰撞,并遵守物理定律。我们还需要一些东西,比如寿命变量,以及一个保存累积伤害(点数)的变量,如果敌人互相射击,伤害就会累积。我们将添加受保护的变量,如 X 和 Y 速度,X 和 Y 偏移(用于微调物体相对于精灵的位置),碰撞形状旋转和缩放因子,最后是摩擦,重力和反弹因子。
向 Hero 类添加 Sprite 控件和定义变量
我们需要做的下一件事是确保我们需要保存运动精灵数据的所有变量都在 Hero 类的顶部定义,如图 8-12 所示。NetBeans 将使用这些信息为 Hero 类创建 getter 和 setter 方法。Java 代码应该是这样的:
protected double``vX
protected double``vY
protected double``lifeSpan
protected double``damage
protected double``offsetX
protected double``offsetY
protected double``boundScale
protected double``boundRot
protected double``friction
protected double``gravity
protected double``bounce
图 8-12。
Add eleven variables at the top of Hero class defining velocity, lifespan, damage, physics, collision
在我们添加所有 22 个 getter 和 setter 方法之前,总共是 11 个。get()和 11。set()方法,为了匹配我们新的 Hero 类变量,让我们回过头来完成我们的 Hero()构造函数方法,并初始化我们刚刚在 Hero 类顶部添加的这十一个变量。
在 Hero 构造函数中初始化 Sprite 控件和定义变量
让我们给我们的英雄演员对象(运动精灵)1000 个单位的寿命,并设置其他变量为零,你可以看到我已经使用复合初始化语句来节省八行代码。如图 8-13 所示,代码没有错误,Java 编程语句应该采用以下格式:
lifespan =``1000
vX = vY = damage = offsetX = offsetY =``0
boundScale = boundRot = friction = gravity = bounce =``0
图 8-13。
Add initialization for your eleven variables inside of your constructor method using compound statements
在我们生成 getter 和 setter 方法之前,让我们看看如何使用复合变量声明语句的组合,以及如果我们不显式地指定它们,Java 将为我们的变量设置哪些默认变量类型值,以减少编写整个 Hero 类所需的代码量,从 25 行代码(如果不使用复合变量初始化语句,则为 33 行代码)减少到 14 行代码。
如果不计算带一个花括号(三个)的代码行,我们说的是不到十几行 Java 语句,包括包、类和导入声明,来编码整个公共抽象类。这是相当令人印象深刻的,考虑到核心类给了我们多少运动精灵的力量和能力。当然,在我们添加了 22 个 getter 和 setter 方法(每个方法有 3 行代码)后,我们将有大约 80 行代码,没有空格。值得注意的是,NetBeans 将为我们编写超过 75%的此类代码!相当酷。
通过复合语句和缺省变量值优化 Hero 类
在让 NetBeans 为我们编写 getter 和 setter 方法之前,我将做两件主要的事情来减少 Hero 类的主要部分的代码量。第一种方法是对所有相似的数据类型使用复合声明,首先声明受保护的 double 和受保护的 float 修饰符和关键字,然后在它们后面列出所有变量,用逗号分隔,这在编程术语中称为“逗号分隔”。11 个 Hero 类变量声明的 Java 代码现在将如下所示:
protected double vX, vY, lifeSpan, damage, offsetX, offsetY;protected float boundScale, boundRot, friction, gravity, bounce;
正如你在图 8-13 和 8-14 中看到的,我们为初始化做了相同类型的复合语句:
lifeSpan = 1000;vX = vY = damage = offsetX = offsetY = 0;boundScale = boundRot = friction = gravity = bounce = 0;
如果您碰巧正在 HDTV 显示屏上进行编辑,也可以只用两行代码来完成:
lifeSpan = 1000;vX = vY = damage = offsetX = offsetY = boundScale = boundRot = friction = gravity = bounce = 0;
接下来,如果我们依靠 Java 编译器将变量初始化为零,因为如果没有指定初始化值,double 和 float 变量将被初始化为,我们可以将这两行代码减少为一行代码:
lifeSpan = 1000;
现在我们已经完成了 Hero()构造函数方法的“核心”,让 NetBeans 编写一些代码吧!
图 8-14。
Optimize your Java code by using compound declarations, and leveraging default initialization values
访问英雄变量:创建 Getter 和 Setter 方法
在你的。collide()方法,并将光标放在那里,这将向 NetBeans 显示您希望它放置将要生成的代码的位置。这在图 8-15 中由源菜单后面的浅蓝色阴影线显示。使用源>插入代码菜单序列或 Alt-Insert 击键组合,当生成浮动弹出菜单出现在这条蓝线下时(这显示了选中的代码行),选择 Getter 和 Setter 选项,在图 8-15 中高亮显示,并选择所有的英雄职业。确保选择了所有的英雄类变量,或者通过使用英雄类主选择复选框,或者通过使用每个变量的复选框 UI 元素,如图 8-15 的右侧所示。
图 8-15。
Use the Source ➤ Insert Code ➤ Generate ➤ Getter and Setter menu sequence and select all class variables
单击“生成 Getters 和 Setters”对话框底部的“生成”按钮后,您将看到 22 个新方法,它们都是 NetBeans 为您编写的全新方法。这些方法如下所示:
public double``getvX()
return vX;}
public void``setvX(double vX)
this.vX = vX;}
public double``getvY()
return vY;}
public void``setvY(double vY)
this.vY = vY;}
public double``getLifeSpan()
return lifeSpan;}
public void``setLifeSpan(double lifeSpan)
this.lifeSpan = lifeSpan;}
public double``getDamage()
return damage;}
public void``setDamage(double damage)
this.damage = damage;}
public double``getOffsetX()
return offsetX;}
public void``setOffsetX(double offsetX)
this.offsetX = offsetX;}
public double``getOffsetY()
return offsetY;}
public void``setOffsetY(double offsetY)
this.offsetY = offsetY;}
public float``getBoundScale()
return boundScale;}
public void``setBoundScale(float boundScale)
this.boundScale = boundScale;}
public float``getBoundRot()
return boundRot;}
public void``setBoundRot(float boundRot)
this.boundRot = boundRot;}
public float``getFriction()
return friction;}
public void``setFriction(float friction)
this.friction = friction;}
public float``getGravity()
return gravity;}
public void``setGravity(float gravity)
this.gravity = gravity;}
public float``getBounce()
return bounce;}
public void``setBounce(float bounce)
this.bounce = bounce;}
值得注意的是,使用 Hero 类创建的对象也可以访问我们之前为 Actor 类生成的 getter 和 setter 方法。如果你想知道 Java 关键字在所有这些中意味着什么。set()方法,它引用的是使用 Actor 或 Hero 类构造函数方法创建的当前对象。因此,如果您调用。iBagel Bagel 对象(我们将在第十章中创建)的 setBounce()方法,这个关键字指的是这个(iBagel) Bagel 对象实例。因此,如果我们想要设置 50%的反弹因子,我们将使用我们的新。setBounce() setter 方法:
iBagel.``setBounce``(``0.50
接下来让我们看看这些 sprite Actor 类是如何与我们在本书中编写的其他类相适应的。在那之后,我们将总结我们在这一章中学到的东西,我们可以进入这本书的未来章节,并使用这些类为我们的游戏创建精灵,就像我们学习如何在游戏中使用精灵一样。
更新游戏设计:演员或英雄如何融入
让我们更新一下我在第七章(图 7-19)中介绍的图表,以包括 Actor.java 和 Hero.java 类。正如你在图 8-16 中看到的,我不得不切换。更新()物理和。collide()图的冲突部分,因为 Actor 类只包括。update()方法,而 Hero 类包含了这两种方法。自从。collide()方法将在。update()方法,我也用 chrome 球体连接了图的这两个部分。
那个。GamePlayLoop 对象中的 handle()方法将调用这些。update()方法,所以这里也有联系。Actor 和 Hero 类与 InvinciBagel 类之间存在联系,因为使用这些抽象类创建的所有游戏 sprite 对象都将在该类的方法中声明和实例化。
我们在开发我们的游戏引擎框架方面取得了很大的进展,同时,我们也看到了 Java 8 编程语言的一些核心特性是如何为我们所用的。在下一章的事件处理中,我们将会看到强大的 Java 8 lambda expressions 新特性,所以关于 Java 8 前沿特性的更多知识将会在游戏中出现。希望你和我一样激动!
图 8-16。
The current invincibagel package class (object) hierarchy, now that we have added Actor and Hero classes
摘要
在第八章中,我们写了第二轮的游戏引擎,我们将在本书中设计和编码,演员(固定精灵)超类,和它的英雄(运动精灵)子类。一旦我们在第十章和后续章节开始创建游戏精灵,英雄职业也将成为超职业。从本质上讲,在这一章中,你学习了如何创建公共抽象类,这些类将在本书中用来定义我们的 sprite 对象。这相当于为我们游戏中的所有演员(精灵)做了所有繁重的工作(精灵设计和编码工作),使我们从现在开始为我们的游戏创建强大的固定和运动精灵变得更加容易。我们正在首先建立我们的知识库和我们的游戏引擎框架!
我们首先看一下这些演员和英雄类将如何设计,以及我们将使用它们创建什么类型的实际精灵类。我们查看了九个 sprite 图像资源,以及这些资源如何通过仅使用九个资源来覆盖广泛的运动,并查看了如何相对于彼此“注册”sprite“状态”。
接下来,我们设计并创建了我们的 Actor 超类,以处理固定的精灵,如 props 和 treasure,创建了基本的 List 、ImageView、SVGPath、iX 和 iY 变量以及一个构造函数方法,该方法使用这些来定义固定的精灵外观、位置和碰撞边界。然后,我们添加了一些额外的变量,我们将需要在未来的游戏设计方面,并了解如何让 NetBeans 写。get()和。set()方法。
接下来,我们设计并创建了我们的 Hero 子类,它扩展了 Actor 来处理运动精灵,例如不可战胜的妖怪自己和他的敌人,以及投射物和移动挑战。我们创建了基本的构造函数方法来设置 Actor 超类中的变量,这次是为了定义运动精灵图像、初始位置和碰撞边界。然后,我们添加了一些额外的变量,这将是我们在未来的游戏设计方面需要的,并再次看到了 NetBeans 将如何编写我们的。get()和。为我们设置()方法,看起来总是很有趣!
最后,我们看了一下更新的 invincibagel 包、类和对象结构图,看看在本书的前八章中我们已经取得了多大的进步。这越来越令人兴奋了!
在下一章中,我们将看看如何控制游戏精灵,我们将使用本章中创建的演员引擎来创建游戏精灵。接下来的第九章将涵盖 Java 8 和 JavaFX 事件处理,这将允许我们的游戏玩家使用事件处理来操纵(控制)这些演员精灵。