Unity 2020 游戏开发实用指南(四)

原文:zh.annas-archive.org/md5/36713AD44963422C9E116C94116EA8B8

译者:飞龙

协议:CC BY-NC-SA 4.0

第十八章:为构建敌人实现游戏 AI

如果没有玩家需要利用角色的能力来应对不同的情景,那么游戏还有什么意义呢?每个游戏都对玩家施加不同类型的障碍,而我们游戏中的主要障碍就是敌人。创建具有挑战性和可信度的敌人可能会很复杂,它们需要像真实角色一样行为,并且足够聪明,不容易被杀死,但也不至于太容易。我们将使用基本但足够好的 AI 技术来实现这一点。

在本章中,我们将研究以下 AI 概念:

  • 使用传感器收集信息

  • 使用 FSM 做出决策

  • 执行 FSM 动作

使用传感器收集信息

AI 首先通过获取周围的信息,然后分析这些数据来确定行动,最后执行所选择的行动,正如你所看到的,没有信息我们什么也做不了,所以让我们从这部分开始。我们的 AI 可以使用多种信息源,比如关于自身的数据(生命和子弹)或者游戏状态(胜利条件或剩余敌人),这些都可以通过我们迄今为止看到的代码轻松找到,但一个重要的信息源也是 AI 的感知。根据我们游戏的需求,我们可能需要不同的感知,比如视觉和听觉,但在我们的情况下,视觉就足够了,所以让我们学习如何编写它。

在本节中,我们将研究以下传感器概念:

  • 创建三过滤器传感器

  • 使用 Gizmos 进行调试

让我们开始看看如何使用三过滤器方法创建传感器。

创建三过滤器传感器

编写感知的常见方法是通过三过滤器方法来丢弃视线之外的敌人。第一个过滤器是距离过滤器,它将丢弃太远无法看到的敌人,然后是角度检查,它将检查我们视野内的敌人,最后是射线检查,它将丢弃被障碍物遮挡的敌人,比如墙壁。在开始之前,我想给出一个建议:我们将在这里使用向量数学,深入讨论这些主题超出了本书的范围。如果你不理解某些内容,可以随意复制并粘贴屏幕截图中的代码,并在网上查找相关概念。让我们按照以下方式编写传感器:

  1. 创建一个名为0,0,0``0,0,0)1,1,1的空GameObject,这样它就会与敌人对齐。虽然我们当然可以直接将所有 AI 脚本放在敌人身上,但我们之所以这样做只是为了分离和组织:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.1 - AI 脚本容器

  1. 创建一个名为Sight的脚本,并将其添加到 AI 子对象中。

  2. 创建两个float类型的字段,分别命名为distanceangle,另外创建两个LayerMask类型的字段,分别命名为obstaclesLayersObjectsLayersdistance将用作视觉距离,angle将确定视野锥的幅度,ObstacleLayers将被我们的障碍物检查使用,以确定哪些对象被视为障碍物,ObjectsLayers将用于确定我们希望视线检测到的对象类型。我们只希望视线看到敌人;我们对墙壁或道具等对象不感兴趣。LayerMask是一种属性类型,允许我们在代码中选择一个或多个层,因此我们将通过层来过滤对象。稍后你将看到我们如何使用它:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.2 - 用于参数化我们视线检查的字段

  1. Update中,调用Physics.OverlapSphere,如下一个截图所示。此函数在由第一个参数指定的位置创建一个虚拟球体,并使用第二个参数(distance属性)中指定的半径来检测第三个参数(ObjectsLayers)中指定的层中的对象。它将返回一个包含在球体内找到的所有对象碰撞器的数组,这些函数使用物理学来进行检查,因此对象必须至少有一个碰撞器。这是我们将使用的方法,以获取视野距离内的所有敌人,并且我们将在接下来的步骤中进一步对它们进行过滤。

重要说明

完成第一个检查的另一种方法是只检查到玩家的距离,或者如果寻找其他类型的对象,则检查到包含它们列表的管理器,但我们选择的方式更加灵活,可以用于任何类型的对象。

另外,您可能希望检查Physics.OverlapSphereNonAlloc版本的此函数,它执行相同的操作,但通过不分配数组来返回结果,因此性能更高。

  1. 遍历函数返回的对象数组:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.3 - 获取特定距离处的所有对象

  1. 要检测对象是否落在视野锥内,我们需要计算我们的观察方向和对象本身方向之间的角度。如果这两个方向之间的角度小于我们的锥角,我们认为对象落在我们的视野内。我们可以开始检测朝向对象的方向,这是通过归一化对象位置与我们位置之间的差异来计算的,就像下面的截图中所示的那样。您可能会注意到我们使用bounds.center而不是transform.position;这样,我们检查对象的中心方向而不是其枢轴。请记住,玩家的枢轴在地面上,射线检查可能会在玩家之前与其发生碰撞:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.4 - 从我们的位置计算朝向碰撞器的方向

  1. 我们可以使用Vector3.Angle函数来计算两个方向之间的角度。在我们的情况下,我们可以计算朝向敌人的方向和我们的前向量之间的角度:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.5 - 计算两个方向之间的角度

重要信息

如果您愿意,您可以使用Vector3.Dot,它将执行点积。Vector3.Angle实际上使用了这个函数,但是为了将点积的结果转换为角度,它需要使用三角函数,这可能会导致昂贵的计算。无论如何,我们的方法更简单快速,只要您没有大量传感器(50+,取决于目标设备),这在我们的情况下不会发生。

  1. 现在检查计算出的角度是否小于angle字段中指定的角度。请注意,如果我们设置为 90 度,实际上将是 180 度,因为如果Vector3.Angle函数返回,例如,30,它可以是 30 度向左或向右。如果我们的角度为 90 度,它可以是左侧或右侧的 90 度,因此它将检测到 180 度弧中的对象。

  2. 使用Physics.Line函数在我们的位置和碰撞体位置之间创建一条虚拟线,以检测在第三个参数中指定的层(obstacles层)中的对象,并返回一个boolean,指示该射线是否击中了某物体。这个想法是使用这条线来检测我们和检测到的碰撞体之间是否有障碍物,如果没有障碍物,这意味着我们对该对象有直线视线。再次提醒,这个函数依赖于障碍物对象有碰撞体,而在我们的情况下,我们有(墙壁、地板等):外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.6 - 使用线性投射检查传感器和目标对象之间的障碍物

  1. 如果对象通过了三个检查,这意味着这是我们当前看到的对象,所以我们可以将它保存在一个名为detectedObjectCollider类型字段中,以便其他 AI 脚本稍后使用这些信息。考虑使用break来停止for循环,以防止浪费资源检查其他对象,并在for之前将detectedObject设置为null,以清除上一帧的结果,所以在这一帧中,如果我们没有检测到任何东西,它将保持空值,这样我们就可以注意到传感器中没有东西:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.7 - 完整的传感器脚本

重要信息

在我们的情况下,我们只是使用传感器来寻找玩家,这是传感器负责寻找的唯一对象,但如果你想使传感器更高级,你可以保持一个检测到的对象列表,将通过三个测试的每个对象放入其中,而不仅仅是第一个对象。

  1. 在编辑器中,根据需要配置传感器。在这种情况下,我们将ObjectsLayer设置为Player,这样我们的传感器将专注于具有该层的对象,并将obstaclesLayer设置为Default,这是我们用于墙壁和地板的层:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.8 - 传感器设置

  1. 为了测试这一点,只需在玩家面前放置一个移动速度为 0 的敌人,选择其 AI 子对象,然后播放游戏,看看属性在检查器中是如何设置的。还可以尝试在两者之间放置障碍物,并检查属性是否显示为“None”(null)。如果没有得到预期的结果,请仔细检查你的脚本、它的配置,以及玩家是否有Player层,障碍物是否有Default层。此外,你可能需要稍微提高 AI 对象,以防止射线从地面下方开始并击中地面:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.9 - 传感器捕捉玩家

即使我们的传感器工作了,有时检查它是否工作或配置正确需要一些我们可以使用Gizmos创建的视觉辅助工具。

使用 Gizmos 进行调试

当我们创建我们的 AI 时,我们将开始检测到一些边缘情况的错误,通常与错误配置有关。你可能认为玩家在敌人的视线范围内,但也许你没有注意到视线被物体遮挡,特别是当敌人不断移动时。调试这些情况的一个好方法是通过仅在编辑器中可见的视觉辅助工具,称为Gizmos,它允许你可视化不可见的数据,比如视线距离或执行线性投射以检测障碍物。

让我们开始看如何通过绘制代表视线距离的球体来创建Gizmos,方法如下:

  1. Sight脚本中,创建一个名为OnDrawGizmos的事件函数。这个事件只在编辑器中执行(不在构建中执行),是 Unity 要求我们绘制Gizmos的地方。

  2. 使用Gizmos.DrawWireSphere函数,将我们的位置作为第一个参数,距离作为第二个参数,以在我们的位置绘制一个半径为我们距离的球体。您可以检查随着更改距离字段而 Gizmo 大小的变化:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.10 - 球体 Gizmo

  1. 可选地,您可以更改 Gizmo 的颜色,设置Gizmos.color然后调用绘图函数:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.11 - Gizmos 绘图代码

重要信息

现在你不断地绘制Gizmos,如果你有很多敌人,它们可能会用太多的Gizmos污染场景视图。在这种情况下,可以尝试使用OnDrawGizmosSelected事件函数,它只在对象被选中时绘制Gizmos

  1. 我们可以使用Gizmos.DrawRay来绘制代表锥体的线,它接收要绘制的线的起点和线的方向,可以乘以某个值来指定线的长度,如下面的屏幕截图所示:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.12 - 绘制旋转线

  1. 在屏幕截图中,我们使用Quaternion.Euler根据我们想要旋转的角度生成一个四元数。如果将这个四元数乘以一个方向,我们将得到旋转后的方向。我们正在取我们的前向矢量,并根据角度字段旋转它,以生成我们的锥体视觉线。此外,我们将这个方向乘以视距,以绘制线条,使其能够看到我们的视线有多远;您将看到线条如何与球体的末端匹配:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.13 - 视觉角线

我们还可以绘制线条投射,检查障碍物,但是由于这些取决于游戏的当前情况,例如通过前两个检查的对象及其位置,因此我们可以使用Debug.DrawLine,它可以在Update方法中执行。这个版本的DrawLine设计为仅在运行时使用。我们在编辑器中看到的Gizmos也是在编辑器中执行的。让我们尝试以下方式:

  1. 首先,让我们调试LineCast未检测到任何障碍物的情况,因此我们需要在我们的传感器和对象之间绘制一条线。我们可以在调用LineCastif语句中调用Debug.DrawLine,如下面的屏幕截图所示:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.14 - 在 Update 中绘制一条线

  1. 在下一个屏幕截图中,您可以看到DrawLine的效果:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.15 - 指向检测到的对象的线

  1. 当视线被对象遮挡时,我们还希望以红色绘制一条线。在这种情况下,我们需要知道 Line Cast 的命中位置,因此我们可以使用函数的一个重载,它提供了一个out参数,可以提供有关线碰撞的更多信息,例如命中的位置、法线和碰撞的对象,如下面的屏幕截图所示:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.16 - 获取有关 Linecast 的信息

重要信息

请注意,Linecast并不总是与最近的障碍物发生碰撞,而是与它在线上检测到的第一个对象发生碰撞,这可能会按顺序变化。如果您需要检测最近的障碍物,请查找该函数的Physics.Raycast版本。

  1. 我们可以使用这些信息在else子句中绘制从我们的位置到命中点的线,当线与某物发生碰撞时:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.17 - 在我们遇到障碍物时绘制一条线

  1. 在下一个屏幕截图中,您可以看到结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.18 - 当障碍物遮挡视线时的线

现在我们的传感器已经完成,让我们使用它们提供的信息来使用**有限状态机(FSM)**做出决策。

使用 FSM 做出决策

我们在过去使用 Animator 时探讨了 FSM 的概念。我们了解到 FSM 是一组状态的集合,每个状态代表对象可以执行的动作,以及一组决定状态切换方式的转换。这个概念不仅在动画中使用,而且在许多编程场景中都有应用,其中一个常见的应用是在 AI 中。我们可以用 AI 代码替换状态中的动画,就得到了 AI FSM。

在本节中,我们将研究以下 AI FSM 概念:

  • 创建 FSM

  • 创建转换

让我们开始创建我们的 FSM 骨架。

创建 FSM

创建我们自己的 FSM,我们需要回顾一些基本概念。记住,FSM 可以为它可以执行的每个可能动作都有一个状态,而且一次只能执行一个动作。在 AI 方面,我们可以巡逻,攻击,逃跑等。还要记住,状态之间存在转换,确定改变一个状态到另一个状态需要满足的条件,就 AI 而言,这可以是用户靠近敌人开始攻击或生命值低开始逃跑。在下一个截图中,你可以找到一个门的两种可能状态的简单提醒示例:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.19 - FSM 示例

有几种方法可以为 AI 实现 FSM;你甚至可以使用 Animator,或者从 Asset Store 下载一些 FSM 系统。在我们的情况下,我们将采取尽可能简单的方法,一个带有一组If语句的单个脚本,这可能很基础,但仍然是理解概念的良好开始。让我们通过以下方式实现它:

  1. 在 Enemy 的 AI 子对象中创建一个名为EnemyFSM的脚本。

  2. 创建名为EnemyStateenum,其中包含GoToBaseAttackBaseChasePlayerAttackPlayer值。我们将在我们的 AI 中拥有这些状态。

  3. 创建一个名为currentStateEnemyState类型字段,它将保存我们的 Enemy 的当前状态:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.20 - EnemyFSM 状态定义

  1. 创建三个以我们定义的状态命名的函数。

  2. 根据当前状态在Update中调用这些函数:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.21 - 基于 If 的 FSM

重要信息

是的,你完全可以在这里使用 switch,但我更喜欢常规的if语法。

  1. 在编辑器中测试如何改变currentState字段将改变哪个状态是活动的,看到在控制台中打印的消息:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.22 - 状态测试

如你所见,这是一个非常简单但完全功能的方法,所以让我们继续使用这个 FSM,创建它的转换。

创建转换

如果你记得在 Animator Controller 中创建的转换,那些基本上是一组条件,如果转换所属的状态处于活动状态,则检查这些条件。在我们的 FSM 方法中,这简单地转换为在状态内检测条件的 If 语句。让我们按照以下方式创建我们提出的状态之间的转换:

  1. 在我们的 FSM 脚本中添加一个名为sightSensorSight类型字段,并将 AI GameObject拖到该字段中,将其连接到那里的Sight组件。由于 FSM 组件与Sight位于同一对象中,我们也可以使用GetComponent,但在高级 AI 中,你可能有不同的传感器检测不同的对象,所以我更喜欢为这种情况准备我的脚本,但选择你最喜欢的方法。

  2. GoToBase函数中,检查Sight组件检测到的对象是否不为null,这意味着我们的视线内有东西。如果我们的 AI 正在前往基地,但在路上检测到一个对象,我们必须切换到Chase状态以追击玩家,所以我们改变状态,如下面的屏幕截图所示:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.23 - 创建转换

  1. 此外,我们必须在靠近必须受损的对象时切换到AttackBase。我们可以创建一个Transform类型的字段,称为baseTransform,并将基地生命对象拖放到那里,以便我们可以检查距离。记得添加一个名为baseAttackDistancefloat字段,以使该距离可配置:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.24 - 前往基地转换

  1. ChasePlayer的情况下,我们需要检查玩家是否不在视线内,以切换回GoToBase状态,或者我们是否足够接近玩家以开始攻击它。我们将需要另一个distance字段,用于确定攻击玩家的距离,我们可能希望为这两个目标设置不同的攻击距离。考虑在转换中进行早期返回,以防止在没有对象时尝试访问传感器检测到的对象的位置时出现null引用异常:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.25 - 追击玩家转换

  1. 对于AttackPlayer,我们需要检查Player是否不在视线内,以返回到GoToBase,或者它是否足够远,以返回追击它。您可以注意到我们将PlayerAttackDistance乘以1.1,使停止攻击的距离比开始攻击的距离大一点;这将防止在玩家接近该距离时快速在攻击和追击之间切换。您可以使其可配置,而不是硬编码1.1外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.26 - 攻击玩家转换

  1. 在我们的情况下,AttackBase不会有任何转换。一旦敌人靠近基地足够攻击它,即使玩家开始向它射击,它也会保持这样。一旦到达那里,它的唯一目标就是摧毁基地。

  2. 记得你可以使用Gizmos来绘制距离:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.27 - FSM 小工具

  1. 在点击播放之前,测试选择 AI 对象的脚本,然后移动玩家,检查状态在检查器中的变化。您还可以保留每个状态中的原始打印消息,以在控制台中查看它们的变化。记得设置攻击距离和对象的引用。在屏幕截图中,您可以看到我们使用的设置:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.28 - 敌人 FSM 设置

现在我们将遇到的一个小问题是,生成的敌人将没有必要的引用,无法进行基地变换的距离计算。如果您尝试将场景中的敌人的更改应用到预制件(None),您将注意到这一点。请记住,预制件不能包含对场景中对象的引用,这使得我们的工作变得复杂。一个替代方法是创建BaseManager,一个保存对伤害位置的引用的单例,这样我们的EnemyFSM就可以访问它。另一个方法可能是利用GameObject.Find等函数来找到我们的对象。

在这种情况下,我们将尝试后者。即使它可能比 Manager 版本的性能要差一些,我还是想向你展示如何使用它来扩展你的 Unity 工具集。在这种情况下,只需在Awake中将baseTransform字段设置为GameObject.Find的返回值,使用BaseDamagePoint作为第一个参数,它将查找一个叫这个名字的对象,就像下面的截图一样。同时,可以自由地从baseTransform字段中删除 private 关键字;现在通过代码设置了它,将其显示在编辑器中除了用于调试之外没有太大意义。你会看到,现在我们生成的敌人将改变状态:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.29 - 按名称在场景中搜索对象

现在我们的 FSM 状态已经编码并且过渡正常,让它们做点什么吧。

执行 FSM 动作

现在我们需要做最后一步 - 让 FSM 做一些有趣的事情。在这里,我们可以做很多事情,比如射击基地或玩家,并将敌人移向其目标(玩家或基地)。我们将使用 Unity 路径规划系统NavMesh来处理移动,这是一个允许我们的 AI 计算和穿越两点之间的路径并避开障碍物的工具,需要一些准备工作才能正常工作。

在本节中,我们将讨论以下 FSM 动作概念:

  • 计算我们场景的路径规划

  • 使用路径规划

  • 添加最后的细节

让我们开始为路径规划准备我们的场景。

计算我们场景的路径规划

路径规划算法依赖于场景的简化版本。在实时中分析复杂场景的完整几何形状几乎是不可能的。表示从场景中提取的路径规划信息的方法有很多,比如图形和NavMesh几何。Unity 使用后者 - 一个简化的网格,类似于跨越 Unity 确定为可行走区域的所有区域的 3D 模型。在下一个截图中,你可以找到一个在场景中生成的NavMesh的示例,即浅蓝色的几何体:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.30 - 场景中可行走区域的 NavMesh

生成NavMesh可能需要几秒到几分钟,这取决于场景的大小。这就是为什么 Unity 的路径规划系统在编辑器中计算一次,所以当我们分发我们的游戏时,用户将使用预先生成的NavMesh。就像光照贴图一样,NavMesh被烘焙到一个文件中以供以后使用。与光照贴图一样,主要的警告是NavMesh对象在运行时不能改变。如果你销毁或移动地板砖,AI 仍然会走在那个区域。NavMesh也没有注意到地板不在了,所以你不能以任何方式移动或修改这些对象。幸运的是,在我们的情况下,我们不会在运行时遭受场景的任何修改,但是请记住,有一些组件,比如NavMeshObsacle,可以在这些情况下帮助我们。

要为我们的场景生成NavMesh,请执行以下操作:

  1. 选择任何可行走的对象以及其上的障碍物,比如地板、墙壁和其他障碍物,并将它们标记为Static。你可能还记得Static复选框也会影响光照贴图,所以如果你希望一个对象不参与光照贴图但对NavMesh的生成有贡献,你可以点击静态检查左侧的箭头,并选择NavMesh生成速度。在我们的情况下,使地形可通行会大大增加生成时间,我们永远不会在那个区域玩。

  2. 窗口|AI|导航中打开NavMesh面板。

  3. 选择NavMesh

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.31 - 生成 NavMesh

基本上你需要做的就是这些。当然,还有很多设置可以调整,比如NavMesh,但是由于我们的场景简单明了,所以默认设置就足够了。

现在,让我们让我们的 AI 在 NavMesh 周围移动。

使用路径规划

为了制作一个使用 NavMesh 移动的 AI 对象,Unity 提供了 NavMeshAgent 组件,它将使我们的 AI 粘附在 NavMesh 上,防止对象离开它。它不仅会自动计算到指定目的地的路径,还会通过模拟人类移动方式的转向行为算法来沿着路径移动对象,在拐角处减速并使用插值进行转向,而不是瞬间转向。此外,该组件能够躲避场景中运行的其他 NavMeshAgent,防止所有敌人聚集在同一位置。

让我们通过以下方式使用这个强大的组件:

  1. 选择敌人 Prefab 并向其添加 NavMeshAgent 组件。将其添加到根对象,称为 Enemy,而不是 AI 子对象 - 我们希望整个对象移动。你会看到对象周围有一个圆柱体,表示对象在 NavMesh 中所占据的区域。请记住,这不是一个碰撞体,所以它不会用于物理碰撞:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.32 - NavMeshAgent 组件

  1. 移除 ForwardMovement 组件;从现在开始,我们将使用 NavMeshAgent 来驱动我们敌人的移动。

  2. 在 EnemyFSM 脚本的 Awake 事件函数中,使用 GetComponentInParent 函数来缓存 NavMeshAgent 的引用。这将类似于 GetComponent - 它将在我们的 GameObject 中查找组件,但如果组件不存在,这个版本将尝试在所有父级中查找该组件。记得添加 using UnityEngine.AI 行来在这个脚本中使用 NavMeshAgent 类:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.33 - 缓存父级组件引用

重要信息

你可以想象,还有一个 GetComponentInChildren,它首先在 GameObject 中搜索组件,然后在必要时在所有子对象中搜索。

  1. 在 GoToBase 状态函数中,调用 NavMeshAgent 引用的 SetDestination 函数,传递基本对象的位置作为目标:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.34 - 设置我们的 AI 的目的地

  1. 保存脚本并在场景中测试一下,或者使用波次生成的敌人进行测试。你会看到敌人永远不会停止朝着目标位置前进,甚至在它们的有限状态机状态在靠近目标时发生变化时也会进入对象内部。这是因为我们从未告诉 NavMeshAgent 停止,我们可以通过将代理的 isStopped 字段设置为 true 来实现这一点。你可能想调整基本攻击距离,使敌人停下来的位置更近或更远:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.35 - 停止代理移动

  1. 我们可以对 ChasePlayer 和 AttackPlayer 做同样的操作。在 ChasePlayer 中,我们可以将代理的目的地设置为玩家的位置,在 AttackPlayer 中,我们可以停止移动。在这种情况下,AttackPlayer 可以再次返回到 GoToBase 或 ChasePlayer,所以你需要在这些状态或在进行转换之前将 isStopped 代理字段设置为 false。我们将选择前者,因为这个版本将覆盖其他也会停止代理的状态而不需要额外的代码。我们将从 GoToBase 状态开始:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.36 - 重新激活代理

  1. 然后,继续进行 Chase Player:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.37 - 重新激活代理并追逐玩家

  1. 最后,继续进行攻击玩家:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.38 - 停止移动

  1. 您可以调整NavMeshAgentAccelerationSpeedAngular Speed属性来控制敌人的移动速度。还记得将更改应用到生成的敌人 Prefab 中。

现在我们的敌人有了移动,让我们完成 AI 的最后细节。

添加最后的细节

这里有两件事情还没有完成,敌人没有射击任何子弹,也没有动画。让我们开始通过以下方式修复射击:

  1. 在我们的EnemyFSM脚本中添加一个bulletPrefab字段,类型为GameObject,以及一个名为fireRatefloat字段。

  2. 创建一个名为Shoot的函数,并在AttackBaseAttackPlayer中调用它:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.39 - 射击函数调用

  1. Shoot函数中,放置与PlayerShooting脚本中使用的类似代码,以特定的射击速率射击子弹,如下截图所示。记得在敌人 Prefab 中设置敌人层,以防止子弹伤害到敌人自身。您可能还希望稍微提高 AI 脚本以在另一个位置射击子弹,或者更好地,添加一个shootPoint变换字段,并在敌人中创建一个空对象作为生成位置。如果这样做,考虑使空对象不旋转,以便敌人的旋转正确影响子弹的方向:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.40 - 射击函数代码

重要信息

PlayerShootingEnemyFSM之间找到了一些重复的射击行为。您可以通过创建一个名为Weapon的行为来修复这个问题,该行为具有一个名为Shoot的函数,用于实例化子弹并考虑射击速率,并在两个组件内调用它以进行重复利用。

  1. 当代理停止时,不仅移动停止,而且旋转也停止。如果玩家在敌人受到攻击时移动,我们仍然需要敌人面对它以向其方向射击子弹。我们可以创建一个LookTo函数,该函数接收要查看的目标位置,并在AttackPlayerAttackBase中调用它,传递要射击的目标:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.41 - LookTo 函数调用

  1. 通过获取我们的父对象到目标位置的方向来完成LookTo函数,我们使用transform.parent访问我们的父对象,因为记住,我们是子 AI 对象,移动的对象是我们的父对象。然后,我们将方向的Y分量设置为0,以防止方向指向上方或向下方 - 我们不希望我们的敌人垂直旋转。最后,我们将父对象的前向矢量设置为该方向,以便立即面向目标位置。如果您愿意,您可以用四元数插值替换它,以使旋转更加平滑,但现在让我们尽可能保持简单:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 18.42 - 面向目标

最后,我们可以使用与玩家相同的 Animator Controller 为敌人添加动画,并使用其他脚本设置参数,具体步骤如下:

  1. 为敌人添加一个Animator组件,如果还没有的话,并设置与玩家相同的控制器;在我们的情况下,这也被称为Player

  2. 创建并添加一个脚本到 Enemy 根对象,名为NavMeshAnimator,它将获取NavMeshAgent的当前速度并将其设置到 Animator 控制器中。这将类似于VelocityAnimator脚本,并负责更新 Animator 控制器的velocity参数以匹配对象的速度。我们没有在这里使用它,因为NavMeshAgent不使用Rigidbody来移动。它有自己的速度系统。实际上,如果我们愿意,我们可以将Rigidbody设置为kinematic,因为它移动但不受物理影响:

总结

通过这样,我们结束了本书的第二部分,关于 C#脚本。在接下来的短篇中,我们将完成游戏的最后细节,从优化开始。图 18.43 - 将 NavMeshAgent 连接到我们的 Animator 控制器

  1. 图 18.45 - 打开射击动画

通过这样,我们已经完成了所有的 AI 行为。当然,这个脚本足够大,值得在将来进行一些重构和拆分,一些动作,如停止和恢复动画和NavMeshAgent可以以更好的方式完成。但是通过这样,我们已经原型化了我们的 AI,并且可以测试直到我们对它满意,然后我们可以改进这段代码。

图 18.44 - 访问父级的 Animator 引用

  1. ](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_18.43_B14199.jpg)

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_18.46_B14199.jpg)

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_18.45_B14199.jpg)

  1. 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在所有非射击状态(如GoToBaseChasePlayer)中关闭boolean

Shoot函数中打开Shooting animator参数,以确保每次射击时该参数被设置为true(选中):![图 18.45 打开射击动画

第十九章:场景性能优化

欢迎来到本书的第三部分——我很高兴您已经到达这一部分,因为这意味着您几乎完成了一个完整的游戏!在本章中,我们将讨论优化技术,以审查游戏的性能并改进它,因为良好和稳定的帧率对于任何游戏都至关重要。性能是一个广泛的主题,需要对几个 Unity 系统有深入的了解,并且可能需要涵盖几本书。我们将研究如何衡量性能,并探索我们对系统的更改的影响,通过测试了解它们的工作原理。

在本章中,我们将研究以下性能概念:

  • 优化图形

  • 优化处理

  • 优化内存

通过本章结束时,您将能够收集运行游戏的三个主要硬件部件的性能数据——GPU、CPU 和 RAM。您将能够分析这些数据,以检测可能的性能问题,并了解如何解决最常见的问题。

优化图形

性能问题最常见的原因与资源的错误使用有关,特别是在图形方面,因为缺乏对 Unity 图形引擎工作方式的了解。我们将探讨 GPU 在高层次上的工作方式以及如何改进其使用。

在本节中,我们将研究以下图形优化概念:

  • 图形引擎简介

  • 使用帧调试器

  • 使用批处理

  • 其他优化

我们将首先概述图形渲染的高级概述,以更好地理解我们稍后在帧调试器中收集的性能数据。根据调试器的结果,我们将确定可以应用批处理的领域(这是一种将多个对象的渲染过程合并在一起,从而降低成本的技术),以及其他常见的优化要点。

图形引擎简介

现今,无论是计算机、移动设备还是游戏机,每个游戏设备都有一个视频卡——一组专门用于图形处理的硬件。它与 CPU 有微妙但重要的区别。图形处理涉及处理成千上万的网格顶点和渲染数百万像素,因此 GPU 被设计为长时间运行短程序,而 CPU 可以处理任何长度的程序,但并行化能力有限。拥有这些处理单元的原因是,我们的程序可以在需要时使用每一个。

问题在于图形不仅依赖于 GPU。CPU 也参与其中,进行计算并向 GPU 发出命令,因此它们必须共同工作。为了实现这一点,两个处理单元需要进行通信,因为它们(通常)是物理上分开的,它们需要另一种硬件来实现这一点——总线,最常见的类型是外围组件互联PCI Express)总线。

PCI Express 是一种连接类型,允许大量数据在 GPU 和 CPU 之间传输,但问题在于,即使速度非常快,如果在两个单元之间发出大量命令,通信时间也会很明显。因此,关键概念在于,图形性能主要通过减少 GPU 和 CPU 之间的通信来改善:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.1 – 通过 PCI Express 总线进行 CPU/GPU 通信

重要说明

现今,新的硬件架构允许 CPU 和 GPU 共存于同一芯片组中,减少了它们的通信时间,甚至共享内存。遗憾的是,该架构不允许视频游戏所需的必要处理能力。很可能我们只会在高端游戏中看到它的应用,但在不久的将来甚至永远也不会。

图形引擎的基本算法是使用裁剪算法确定哪些对象是可见的,根据它们的相似性对它们进行排序和分组,然后向 GPU 发出绘制命令以渲染这些对象组,有时会多次(如第八章**,使用通用渲染管线进行照明)。在这里,主要的通信形式是那些绘制命令,通常称为绘制调用,我们在优化图形时的主要任务是尽量减少它们。问题在于有几个绘制调用的来源需要考虑,例如照明和对象的比例,以查看它们是否是静态的。研究它们中的每一个将需要很长时间,即使这样,Unity 的新版本也可能引入具有自己绘制调用的新图形功能。相反,我们将探索一种使用帧调试器发现这些绘制调用的方法。

使用帧调试器

帧调试器是一个工具,允许我们查看 Unity 渲染引擎发送到 GPU 的所有绘制命令或绘制调用的列表。它不仅列出它们,还提供有关每个绘制调用的信息,包括检测优化机会所需的数据。通过使用帧调试器,我们可以看到我们的更改如何修改绘制调用的数量,从而使我们对我们的努力得到即时反馈。

重要提示

请注意,减少绘制调用有时不足以提高性能,因为每个绘制调用的处理时间可能不同;但通常,这种差异不足以考虑。此外,在某些特殊的渲染技术中,例如光线追踪或光线行军,单个绘制调用可能耗尽我们所有的 GPU 功率。这在我们的游戏中不会发生,所以我们现在不会考虑这一点。

让我们使用帧调试器通过以下方式分析我们游戏的渲染过程:

  1. 打开帧调试器(窗口 | 分析 | 帧调试器)。

  2. 播放游戏,如果要分析性能,请单击窗口左上角的启用按钮(在播放时按Esc重新获得鼠标控制):外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.2 - 启用帧调试器

  1. 单击游戏选项卡以打开游戏视图。

重要提示

有时,同时看到场景游戏面板是有用的,您可以通过将它们中的一个拖动到 Unity 底部来实现它们的分离和可见。

  1. 将滑块从禁用按钮右侧缓慢向右拖动,以查看场景是如何渲染的。每一步都是在 CPU 中执行的给定游戏帧的绘制调用。您还可以观察窗口左侧的列表如何在那一刻突出显示执行的绘制调用的名称:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.3 - 分析我们帧的绘制调用

  1. 单击列表中的任何绘制调用,并观察窗口右侧的详细信息。

如果您不习惯于编码引擎或着色器,大多数可能会让您感到困惑,但您可以看到其中一些具有称为为什么这个绘制调用不能与上一个批处理在一起的可读部分,它告诉您为什么两个对象没有一起绘制在单个绘制调用中。我们将稍后检查这些原因:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.4 - 帧调试器中的批处理中断原因

  1. 播放模式下打开窗口,禁用地形并查看绘制调用的数量如何立即改变。有时,只需打开和关闭对象就足以检测到导致性能问题的原因。还可以尝试禁用后期处理和其他与图形相关的对象,如粒子。

即使我们不完全了解这些绘制调用来自何处,我们至少可以通过修改 Unity 中的设置来开始,以查看这些更改的影响。没有比通过测量工具逐个切换并查看这些更改的影响更好地了解 Unity 这样庞大的东西的方法。

现在,让我们讨论减少绘制调用的基本技术,并在 Frame Debugger 中看到它们的效果。

使用批处理

我们在之前的章节中讨论了几种优化技术,其中照明是最重要的。如果你在实施这些技术时测量绘制调用,你会注意到这些行动对绘制调用数量的影响。然而,在本节中,我们将专注于另一种称为批处理的图形优化技术。批处理是将多个对象分组在单个绘制调用中一起绘制的过程。你可能会想为什么我们不能只在一个绘制调用中绘制所有东西,虽然从技术上讲这是可能的,但需要满足一组条件才能合并两个对象,通常情况下是合并材质。

记住,材质是作为图形配置文件的资产,需要在发出绘制调用之前指定一个SetPass调用,这是 CPU/GPU 通信的另一种形式,用于设置第一个对象的SetPass调用被第二个对象重用,并且这打开了批处理对象的机会。如果它们共享相同的设置,Unity 可以在 CPU 中将网格组合成一个,并将组合的网格在单个绘制调用中发送到 GPU。

有几种减少材质数量的方法,比如删除重复的材质,但最有效的方法是通过一个叫做纹理合并的概念。这意味着将不同对象的纹理合并成一个。这样,由于该纹理可以应用于多个对象,并且具有自己纹理的对象需要自己的材质。遗憾的是,Unity 中没有自动系统来合并三维对象的纹理,就像我们在 2D 中使用的纹理图集对象。Asset Store 中可能有一些系统,但自动系统可能会有一些副作用。这项工作通常由艺术家完成,所以在与专门的 3D 艺术家合作时(或者如果你自己是艺术家),请记住这个技术:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.5 - 不同金属物体的碎片

让我们通过以下方式使用 Frame Debugger 来探索批处理:

  1. 搜索我们当前想要使用的渲染管线资产(编辑 | 项目设置 | 图形 | 可编程渲染设置):外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.6 - 可编程渲染管线设置

  1. 高级部分取消选择SRP 批处理器。我们稍后会讨论这个:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.7 - 禁用 SRP 批处理器

  1. 为测试创建一个新的空场景(文件 | 新建场景)。

  2. 创建两种不同颜色的材质。

  3. 创建两个立方体,将一个材质放入第一个立方体,另一个放入第二个。

  4. 打开 Frame Debugger 并单击启用以查看我们立方体的绘制调用列表:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.8 - 立方体的绘制调用

  1. 选择第二个绘制网格立方体调用并查看批处理中断的原因。它应该说对象有不同的材质。

  2. 在两个立方体上使用一个材质,然后再次查看列表。现在你会注意到我们只有一个绘制网格立方体调用。你可能需要再次禁用和启用 Frame Debugger 才能正确刷新。

现在,我挑战你尝试相同的步骤,但是创建球体而不是立方体。如果您这样做,您可能会注意到即使具有相同的材质,球体也没有被批处理!这就是我们需要介绍动态批处理的概念的地方。

请记住,游戏对象有一个静态复选框,用于通知几个 Unity 系统该对象不会移动,以便它们可以应用几个优化。没有勾选此复选框的对象被视为动态。到目前为止,我们用于测试的立方体和球体都是动态的,因此 Unity 需要在每帧中组合它们,因为它们可以移动,并且组合不是“免费”的。其成本与模型中的顶点数直接相关。您可以从 Unity 手册中获取确切的数字和所有必要的考虑,如果搜索Unity Batching,手册将显示出来。但是,可以说,如果对象的顶点数足够大,该对象将不会被批处理,这样做将需要发出两个以上的绘制调用。这就是为什么我们的球体没有被批处理;球体的顶点太多了。

现在,如果我们有静态对象,情况就不同了,因为它们使用第二个批处理系统——静态批处理器。这个概念是一样的。合并对象以在一个绘制调用中渲染它们,再次这些对象需要共享相同的材质。主要区别在于,这个批处理器将批处理比动态批处理器更多的对象,因为合并是在场景加载时进行一次,然后保存在内存中以在下一帧中使用,这会消耗内存,但每帧节省大量处理时间。您可以使用我们用来测试动态批处理器的相同方法来测试静态版本,只需勾选球体的静态复选框,然后在播放模式下查看结果;在编辑模式下(不播放时),静态批处理器不起作用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.9-一个静态球体及其静态批处理

在继续之前,让我们讨论为什么我们禁用了 SRP 批处理器以及它如何改变我们刚刚讨论的内容。在其 2020 年版中,Unity 推出了 URP(通用渲染管线),一个新的渲染管线。除了几项改进之外,现在相关的是 SRP 批处理器,一个在动态对象上工作的新批处理器,没有顶点或材质限制(但有其他限制)。SRP 批处理器不依赖于与批处理对象共享相同的材质,而是可以批处理使用相同着色器的材质的对象,这意味着我们可以有,例如,100 个对象,每个对象有 100 种不同的材质,它们将被批处理,而不管顶点数多少,只要材质使用相同的着色器和变体:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.10-材质的 GPU 数据持久性,这使得 SRP 批处理器存在

一个着色器可以有几个版本或变体,所选的变体是根据设置选择的。我们可以有一个不使用法线贴图的着色器,将使用不计算法线的变体,这可能会影响 SRP 批处理。因此,使用 SRP 批处理基本上没有任何缺点,所以继续打开它。尝试创建尽可能多的具有尽可能多材质的球体,并在帧调试器中检查它将生成的批次数量。只需考虑,如果您需要处理在 URP 之前完成的项目,这将不可用,因此您需要了解适当的批处理策略。

其他优化

如前所述,有许多可能的图形优化,因此让我们简要讨论基本的优化,从细节级别LOD)开始。LOD 是根据对象到相机的距离改变网格的过程。例如,当房子很远时,这可以减少绘制调用,如果您用一个减少了细节的组合网格替换了一个由多个部分和零件组成的房子。使用 LOD 的另一个好处是,由于顶点数减少,您减少了绘制调用的成本。

要使用此功能,请执行以下操作:

  1. 创建一个空对象并将模型的两个版本作为子对象。您需要使用具有不同细节级别的多个版本的模型,但现在,我们只是要使用一个立方体和一个球来测试这个功能:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.11 – 一个带有两个 LOD 网格的单个对象

  1. 将 LOD 组件添加到父对象。

  2. 默认的 LOD 组准备支持三个 LOD 网格组,但由于我们只有两个,右键单击一个并单击删除。您还可以选择在之前插入以添加更多 LOD 组:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.12 – 移除 LOD 组

  1. 选择LOD 0,最高细节 LOD 组,并单击下面的渲染器列表中的添加按钮,将球添加到该组。您可以添加任意数量的网格渲染器。

  2. 选择LOD 1并添加立方体:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.13 – 将渲染器添加到 LOD 组

  1. 拖动两个组之间的线以控制每个组占用的距离范围。当您拖动它时,您将看到相机需要切换组的预览距离。此外,您还有被剔除的组,即相机不会渲染任何组的距离。

  2. 只需在编辑模式下移动相机,以查看网格是如何交换的。

  3. 这里需要考虑的一点是,对象的碰撞体不会被禁用,因此只需在 LOD 子对象中放置渲染器。将 LOD 0 的形状碰撞体放在父对象中,或者只是从 LOD 组对象中移除碰撞体,除了组 0。

另一个要考虑的优化是截锥体裁剪。默认情况下,Unity 会渲染相机视图区域或截锥体内的任何对象,跳过不在其中的对象。该算法足够便宜,因此始终使用,并且无法禁用。但是,它确实有一个缺陷。如果有一堵墙遮挡了其后的所有物体,即使它们被遮挡,它们仍然会落入截锥体内,因此仍然会被渲染。在实时中检测一个网格的每个像素是否遮挡另一个网格的每个像素几乎是不可能的,但幸运的是,我们有一个变通方法:遮挡剔除。

遮挡剔除是分析场景并确定在场景的不同部分中可以看到哪些对象的过程,将它们分成部分并分析每个部分。由于这个过程可能需要相当长的时间,因此在编辑器中进行,就像进行光照贴图一样。正如你可以想象的那样,它只对静态对象起作用。要使用它,请执行以下操作:

  1. 将不应移动的对象标记为静态,或者如果您只希望将此对象视为遮挡剔除系统的静态对象,请选中静态复选框右侧的箭头旁边的遮挡者被遮挡者复选框。

  2. 打开遮挡剔除窗口(窗口 | 渲染 | 遮挡剔除)。

  3. 保存场景并在窗口底部单击烘焙按钮,然后等待烘焙过程。如果在烘焙过程之前不保存场景,它将不会执行。

  4. 遮挡剔除窗口中选择可视化选项卡。

  5. 遮挡裁剪窗口可见时,选择摄像机并拖动它,看看随着摄像机移动对象是如何被遮挡的:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.14 – 左边是正常场景,右边是带有遮挡裁剪的场景

请注意,如果将摄像机移出计算区域,处理将不会发生,Unity 只会计算靠近静态对象的区域。您可以通过创建一个空对象并添加一个遮挡区域组件,设置其位置和大小以覆盖摄像机将到达的区域,最后重新烘焙裁剪来扩展计算区域。尝试合理设置立方体的大小。计算的区域越大,磁盘中存储生成数据所需的空间就越大。您可以使用多个这样的区域来更精确地计算,例如,在一个 L 形场景中,您可以使用两个:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.15 – 遮挡区域

如果你发现对象没有被遮挡,可能是遮挡对象(在这种情况下是墙)不够大。你可以增加对象的大小或者在窗口的烘焙选项卡中减少最小遮挡者设置。这样做会进一步细分场景以检测更小的遮挡者,但这将占用更多磁盘空间来存储更多数据。所以再次,要合理设置这个选项。

我们仍然可以应用一些其他技术到我们的游戏中,但我们已经讨论过的足够了。所以现在,让我们开始讨论其他优化领域,比如处理领域。

优化处理

虽然图形通常占据生成一帧所需时间的大部分,但我们不应低估糟糕优化的代码和场景的成本。游戏中仍然有一些部分是在 CPU 中计算的,包括图形处理的一部分(如批处理计算)、Unity 物理、音频和我们的代码。在这里,我们遇到的性能问题比图形方面多得多,所以再次,与其讨论每一个优化,不如学习如何发现它们。

在本节中,我们将研究以下 CPU 优化概念:

  • 检测 CPU 和 GPU 负载

  • 使用 CPU 使用率分析器

  • 一般的 CPU 优化技术

我们将从讨论 CPU-和 GPU 受限的概念开始,这些概念侧重于优化过程,确定问题是 GPU 还是 CPU 相关。稍后,就像 GPU 优化过程一样,我们将看看如何收集 CPU 的性能数据并解释它以检测可能应用的优化技术。

检测 CPU 和 GPU 负载

与帧调试器一样,Unity Profiler 允许我们通过一系列性能分析器模块收集有关游戏性能的数据,每个模块都旨在收集关于不同 Unity 系统的每帧数据,例如物理、音频,最重要的是 CPU 使用情况。这个最后的模块允许我们看到 Unity 处理帧所调用的每个函数,也就是从我们脚本执行的函数到其他系统,比如物理和图形。

在探索 CPU 使用率之前,我们可以在这个模块中收集的一个重要数据是我们是 CPU-还是 GPU-受限。如前所述,一帧使用 CPU 和 GPU 并行处理。当 GPU 执行绘图命令时,CPU 可以以非常高效的方式执行物理和我们的脚本。但现在,假设 CPU 完成了它的工作,而 GPU 仍在工作。CPU 可以开始处理下一帧吗?答案是否定的。这将导致不同步,所以在这种情况下,CPU 将需要等待。这就是所谓的 CPU 受限,我们也有相反的情况,GPU 受限,当 GPU 比 CPU 更早完成时。

重要提示

值得一提的是,在移动设备上,有时最好降低游戏的帧率以减少电池消耗,使游戏在帧之间空闲一会儿,但这可能会导致命令和输入的响应变慢。为了解决这个问题,Unity 创建了一个包,可以在可配置的帧数之后跳过渲染过程,从而保持处理工作但跳过渲染。因此,自然而然地,这些帧将仅受 CPU 限制。

集中我们的优化工作非常重要,因此如果我们发现游戏受 GPU 限制,我们将专注于 GPU 图形优化;如果受 CPU 限制,我们将专注于其他系统和 CPU 图形处理的优化。要检测我们的游戏是哪一种情况,可以按照以下步骤进行:

  1. 打开ProfilerWindow | Analysis | Profiler)。

  2. 在左上角的Profiler Modules下拉菜单中,勾选GPU以启用 GPU 分析器:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.16 – 启用 GPU 分析器

  1. 玩游戏并选择CPU 使用率分析器,在Profiler窗口的左侧部分点击其名称。

  2. 观察窗口中间带有CPUGPU标签的条形图。它应该显示 CPU 和 GPU 消耗了多少毫秒。数字较高的那个将限制我们的帧率,并确定我们是受 GPU 限制还是受 CPU 限制:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.17 – 确定我们是受 CPU 限制还是受 GPU 限制

  1. 点击标有 Timeline 的按钮,选择 Hierarchy:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.18 – 选择 Hierarchy

  1. 当您尝试打开 GPU 分析器时,有可能会看到不支持的消息,在某些情况下会发生这种情况(例如在某些 Mac 设备上)。在这种情况下,另一种查看我们是否受 GPU 限制的方法是在选择 CPU 使用率分析器时,在 CPU/GPU 标签旁边的搜索栏中搜索waitforpresent外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.19 – 搜索 waitforpresent

  1. 在这里,您可以看到 CPU 等待 GPU 的时间有多长。检查0.00,这是因为 CPU 没有等待 GPU,这意味着我们不受 GPU 限制。在前面的截图中,您可以看到我的屏幕显示0.00,而 CPU 花费了9.41ms,GPU 花费了6.73ms。因此,我的设备受 CPU 限制。

现在我们可以检测我们是受 CPU 限制还是受 GPU 限制,然后集中我们的优化工作。到目前为止,我们讨论了如何对 GPU 过程的一部分进行分析和优化。现在,如果我们发现我们受 CPU 限制,让我们看看如何对 CPU 进行分析。

使用 CPU 使用率分析器

对 CPU 进行分析的方式与对 GPU 进行分析的方式类似。我们需要获取 CPU 执行的操作列表并尝试减少它们,这就是 CPU 使用率分析器模块的作用——这是一个工具,允许我们查看 CPU 在一个帧中执行的所有指令。主要区别在于 GPU 主要执行绘制调用,而我们有几种类型的绘制调用,而 CPU 可能有数百种不同的指令需要执行,有时其中一些是无法删除的,例如物理更新或音频处理。在这些情况下,我们希望减少这些功能的成本,以防它们消耗太多时间。因此,重要的一点是要检测哪个功能花费了太多时间,然后减少其成本或删除它,这需要对底层系统有更深入的了解。让我们首先开始检测这个功能。

当您在打开Profiler选项卡时玩游戏,您将看到一系列图形显示我们游戏的性能,在 CPU 使用率分析器中,您将看到图形被分成不同的颜色,每种颜色代表帧处理的不同部分。您可以查看分析器左侧的信息来了解每种颜色的含义,但让我们讨论最重要的部分。在下面的截图中,您可以看到图形应该是什么样子的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.20 – 分析 CPU 使用率图

如果您查看图形,您可能会认为图表的深绿色部分占用了大部分性能时间,虽然这是真的,但您也可以从图例中看到深绿色代表其他,这是因为我们是在编辑器中对游戏进行分析。编辑器的行为不会完全像最终游戏那样。为了使其运行,它必须进行大量额外的处理,这些处理在游戏中不会执行,因此您能做的最好的事情就是直接在游戏的构建版本中进行分析。在那里,您将收集到更准确的数据。我们将在下一章讨论如何进行构建,所以现在我们可以忽略那个区域。现在我们可以简单地点击其他标签左侧的彩色方块,以禁用图表中的该测量,以便稍微清理一下。如果您还看到大片黄色,那是指 VSync,基本上是等待我们的处理与显示器的刷新率匹配所花费的时间。这也是我们可以忽略的东西,所以您也应该禁用它。在下一个截图中,您可以查看图形颜色类别以及如何禁用它们:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.21 – 从分析器中禁用 VSync 和其他功能

现在我们已经清理了图形,我们可以通过查看带有 ms 标签的线(在我们的情况下,5ms (200 FPS))来很好地了解我们游戏的潜在帧率,这表明低于该线的帧率超过 200 FPS,而高于该线的帧率低于 200 FPS。在我的情况下,性能非常好,但请记住,我是在一台性能强大的机器上测试的。最佳的分析方法不仅是在游戏的构建版本中(作为可执行文件),而且还要在目标设备上进行分析,这应该是我们打算运行游戏的最低规格硬件。我们的目标设备在很大程度上取决于游戏的目标受众。如果我们正在制作休闲游戏,我们可能会针对移动设备,因此我们应该在尽可能低规格的手机上测试游戏,但如果我们的目标是硬核玩家,他们可能会有一台强大的机器来运行我们的游戏。

重要提示

如果您的目标是硬核玩家,当然,这并不意味着我们可以制作一个非常未优化的游戏,但这将为我们提供足够的处理空间来增加更多细节。无论如何,我强烈建议您避免那些类型的游戏,如果您是初学者,因为它们更难开发,您可能会意识到这一点。先从简单的游戏开始。

通过观察图形颜色,您可以看到 CPU 端渲染的成本是浅绿色,图表显示它占用了大部分处理时间,这实际上是正常的。然后,在蓝色中,我们可以看到脚本执行的成本,这也占用了相当大的部分,但同样,这也是相当正常的。此外,我们还可以观察到一点橙色,那是物理,还有一点浅蓝色,那是动画。记得检查分析器中的彩色标签,以记住每种颜色代表什么。

现在,这些彩色条代表一组操作,所以如果我们认为渲染条代表 10 个操作,我们如何知道包括哪些操作?又如何知道这些操作中哪些占用了最多的性能时间?在这 10 个操作中,可能有一个单独的操作导致了这些问题。这就是分析器底部部分的用处。它显示了帧中调用的所有功能的列表。使用它,按照以下步骤进行:

  1. 清除我们之前使用的搜索栏。它将按名称过滤功能调用,而我们希望看到它们全部。如果尚未在那里,请记得从时间轴切换到层次结构模式。

  2. 点击时间 ms列,直到出现向下的箭头。这将按成本降序排列调用。

  3. 点击图表中引起你注意的帧 - 可能是消耗更多处理时间的最高的帧之一。这将使分析器立即停止游戏并显示有关该帧的信息。

重要提示

查看图表时需要考虑两件事。如果你看到峰值明显高于其他帧,这可能会导致游戏出现瞬间卡顿,这会影响性能。此外,你还可以寻找一长串时间消耗较高的帧。也要尽量减少它们。即使这只是暂时的,玩家也会很容易察觉到它的影响。

  1. PlayerLoop可能会出现为消耗时间最长的帧,但这并不是很有信息性。你可以通过点击其左侧的箭头来展开它以进一步探索。

  2. 点击每个功能以在图表中突出显示。处理时间较长的功能将以较粗的条形突出显示,这些是我们将要关注的功能:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.22 - 在图表中突出显示的渲染相机功能

  1. 你可以继续点击箭头以进一步探索功能,直到达到极限。如果想要更深入,可以在分析器的顶部栏中启用深度分析模式。这将提供更多细节,但要注意这个过程是昂贵的,会使游戏变慢,改变图表中显示的时间,使其看起来比实际时间要长得多。在这里,忽略数字,看看根据图表,一个功能占用了多少进程。你需要停止,启用深度分析,然后再次播放才能使其生效。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.23 - 启用深度分析

有了这些知识,我们可以开始改善游戏性能(如果低于目标帧率),但每个功能都是由 CPU 调用并以其独特的方式进行改进,这需要对 Unity 的内部工作有更深入的了解。这可能需要涉及几本书,而且内部工作会随着版本的变化而变化。相反,你可以通过在互联网上查找有关特定系统的数据,或者通过禁用和启用对象或代码的部分来探索我们行为的影响,就像我们在帧调试器中所做的那样。分析需要创造力和推理来解释和相应地对所获得的数据做出反应,因此你需要一些耐心。

现在我们已经讨论了如何获取与 CPU 相关的分析数据,让我们讨论一些常见的减少 CPU 使用率的方法。

CPU 优化的一般技术

在 CPU 优化方面,有许多可能导致性能不佳的原因,包括滥用 Unity 的功能,大量的物理或音频对象,不正确的资源/对象配置等。我们的脚本也可能以非优化的方式编写,滥用或错误使用昂贵的 Unity API 函数。到目前为止,我们已经讨论了使用 Unity 系统的几种良好实践,例如音频配置,纹理大小,批处理,以及查找函数,如GameObject.Find并用管理器替换它们。因此,让我们讨论一些关于常见情况的具体细节。

让我们首先看看大量对象对性能的影响。在这里,您可以创建大量配置为Physics.ProcessingRigidbody的对象,该函数负责此增加:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.24 – 多个对象的物理处理

另一个测试是看看多个对象的影响,可以创建大量的音频源。在下面的截图中,您可以看到我们需要重新启用其他,因为音频处理属于该类别。我们之前提到其他属于编辑器,但它也可以包括其他进程,所以请记住这一点:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.25 – 多个对象的物理处理

因此,要发现这些问题,您可以开始禁用和启用对象,看它们是否增加了时间。最后一个测试是关于粒子。创建一个系统,产生足够多的粒子以影响我们的帧率,并检查性能分析器。在下面的截图中,您可以看到粒子处理函数在图表中被突出显示,表明它花费了大量时间:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.26 – 粒子处理

然后,在脚本方面,我们还有其他需要考虑的事情,其中一些是所有编程语言和平台共有的,例如迭代长列表的对象,滥用数据结构和深度递归。然而,在本节中,我主要将讨论特定于 Unity 的 API,从printDebug.Log开始。

这个函数对于在控制台中获取调试信息很有用,但也可能很昂贵,因为所有日志都会立即写入磁盘,以避免在游戏崩溃时丢失宝贵的信息。当然,我们希望在游戏中保留这些宝贵的日志,但我们不希望它影响性能,那么我们该怎么办呢?

一种可能的方法是保留这些消息,但在最终构建中禁用非必要的消息,例如信息性消息,保持错误报告功能处于活动状态。一种方法是通过编译器指令,例如下面截图中使用的指令。请记住,这种if语句是由编译器执行的,如果条件不满足,编译时可以排除整个代码部分:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.27 – 禁用代码

在前面的截图中,您可以看到我们正在询问此代码是由编辑器编译还是用于开发构建,这是一种特殊类型的构建,旨在用于测试(在下一章中将详细介绍)。您还可以使用编译器指令创建自己的日志记录系统,因此您不需要在每个要排除的日志中使用它们。

还有一些其他脚本方面的问题可能会影响性能,不仅在处理方面,还在内存方面,所以让我们在下一节中讨论它们。

优化内存

我们讨论了如何对两个硬件部分——CPU 和 GPU 进行性能分析和优化,但是还有另一部分硬件在我们的游戏中扮演着关键角色——RAM。这是我们放置所有游戏数据的地方。游戏可能是内存密集型应用程序,与其他几种应用程序不同的是,它们不断执行代码,因此我们需要特别小心。

在本节中,我们将讨论以下内存优化概念:

  • 内存分配和垃圾收集器

  • 使用内存分析器

让我们开始讨论内存分配的工作原理以及垃圾收集在这里扮演的角色。

内存分配和垃圾收集器

每次实例化一个对象,我们都在 RAM 中分配内存,在游戏中,我们将不断地分配内存。在其他编程语言中,除了分配内存,您还需要手动释放它,但是 C#有一个垃圾收集器,它是一个跟踪未使用内存并清理它的系统。该系统使用引用计数器,跟踪对象存在多少引用,当计数器达到0时,意味着所有引用都变为 null,对象可以被释放。这个释放过程可以在几种情况下触发,最常见的情况是当我们达到最大分配内存并且想要分配一个新对象时。在这种情况下,我们可以释放足够的内存来分配我们的对象,如果不可能,内存就会被扩展。

在任何游戏中,您可能会不断地分配和释放内存,这可能导致内存碎片化,意味着存活对象内存块之间存在小空间,这些空间大多是无用的,因为它们不足以分配一个对象,或者可能空间的总和足够大,但我们需要连续的内存空间来分配我们的对象。在下图中,您可以看到一个经典的例子,试图将一个大块内存放入碎片化产生的小间隙中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.28 – 尝试在碎片化的内存空间中实例化对象

一些垃圾收集系统,例如常规 C#中的系统,是分代的,这意味着内存根据其内存的“年龄”被分成代桶。新的内存将放在第一个桶中,这些内存往往会频繁分配和释放。因为这个桶很小,所以在其中工作是快速的。第二个桶中有在第一个桶的先前释放扫描过程中幸存的内存。该内存被移动到第二个桶中,以防止它被不断检查是否幸存了该过程,并且可能该内存将持续整个程序的生命周期。第三个桶只是第二个桶的另一层。这个想法是大部分时间,分配和释放系统将在第一个桶中工作,并且由于它足够小,因此可以快速地分配、释放和压缩内存。

问题在于 Unity 使用自己的垃圾收集系统版本,该版本是非分代和非压缩的,这意味着内存不会分成桶,并且内存不会被移动以填补空隙。这表明在 Unity 中分配和释放内存仍然会导致碎片化问题,如果您不调节内存分配,您可能最终会得到一个执行非常频繁的昂贵垃圾收集系统,在我们的游戏中产生中断,您可以在 Profiler CPU Usage 模块中看到它呈现为淡黄色。

处理这个问题的一种方法是尽量避免内存分配,不必要时避免它。有一些微调可以做到这一点,但在查看这些之前,再次重申,首先获取有关问题的数据非常重要,然后再开始修复可能不是问题的事情。这个建议适用于任何类型的优化过程。在这里,我们仍然可以使用 CPU 使用率分析器来查看 CPU 在每帧中执行的每个函数调用分配了多少内存,只需查看GC Alloc列,该列指示函数分配的内存量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.29 – Sight 的 Update 事件函数的内存分配

在前面的截图中,我们可以看到我们的函数分配了太多的内存,这是因为场景中有大量的敌人。但这并不是借口;我们在每一帧都分配了这么多 RAM,所以我们需要改进这一点。有几件事情可能导致我们的内存被分配,所以让我们讨论一些基本的事情,从返回数组的函数开始。

如果我们审查 Sight 代码,我们会发现唯一分配内存的时刻是在调用Physics.OverlapSphere时,这是显而易见的,因为它是一个返回数组的函数,这是一个返回可变数量数据的函数。为了做到这一点,它需要分配一个数组并将该数组返回给我们。这需要在创建函数的一侧——Unity 上完成,但在这种情况下,Unity 给我们提供了两个版本的函数——我们正在使用的版本和NonAlloc版本。通常建议使用第二个版本,但 Unity 使用另一个版本来使初学者编码更简单。NonAlloc版本如下截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.30 – Sight 的 Update 事件函数的内存分配

这个版本要求我们分配一个足够大的数组,以保存我们的OverlapSphere变量可以找到的最大数量的碰撞体,并将其作为第三个参数传递。这使我们能够只分配一次数组,并在每次需要时重复使用它。在前面的截图中,您可以看到数组是静态的,这意味着它在所有 Sight 变量之间是共享的(它们不会并行执行Update函数)。这将很好地工作。请记住,该函数将返回检测到的对象数量,因此我们只需迭代该计数。数组中可以存储先前的结果。

现在,检查一下你的性能分析器,注意分配的内存量已经大大减少。我们的函数内可能仍然存在一些内存分配,但有时无法将其保持为0。但是,您可以尝试使用深度分析或通过注释一些代码来查看造成这种情况的原因,并查看哪些注释可以消除分配。我向您挑战尝试一下。此外,OverlapSphere并不是唯一可能发生这种情况的情况。还有其他情况,比如GetComponents函数系列,与GetComponent不同,它不仅找到给定类型的第一个组件,而是找到所有组件,因此请注意 Unity 的任何返回数组的函数,并尝试用不分配版本替换它,如果有的话。

另一个常见的内存分配来源是字符串连接。记住字符串是不可变的,这意味着如果你连接两个字符串,它们是无法改变的。需要生成一个足够大的第三个字符串来容纳前两个字符串。如果你需要大量连接,考虑使用string.Format,如果你只是在模板字符串中替换占位符,比如在消息中放置玩家的名字和他们得到的分数,或者使用StringBuilder,这是一个只保存所有要连接的字符串的类,当需要时,将它们一起连接起来,而不是像**+**运算符一样一个接一个地连接它们。还要考虑使用 C#的新字符串插值功能。你可以在下面的截图中看到一些例子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.31 – C#中的字符串管理

最后,一个经典的技术是对象池,适用于需要不断实例化和销毁对象的情况,比如子弹或特效。在这种情况下,使用常规的InstantiateDestroy函数会导致内存碎片,但对象池通过分配可能需要的最大数量的对象来解决这个问题。它通过取其中一个预分配的函数来替换Instantiate,并通过将对象返回到池中来替换Destroy。一个简单的对象池可以在下面的截图中看到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.32 – 一个简单的对象池

有几种方法可以改进这个池,但现在它已经很好了。请注意,当从池中取出对象时,需要重新初始化对象,你可以使用OnEnable事件函数或创建一个自定义函数来通知对象这样做。

现在我们已经探讨了一些基本的内存分配减少技术,让我们来看看一个新的内存分析器工具,它是在 Unity 的最新版本中引入的,可以更详细地探索内存。

使用内存分析器

使用这个分析器,我们可以检测每帧分配的内存,但它不会显示到目前为止分配的总内存,这对于研究我们如何使用内存很有用。这就是内存分析器可以帮助我们的地方。这个相对较新的 Unity 包允许我们对每个分配的对象进行内存快照,包括本地和托管端的对象——本地指的是内部的 C++ Unity 代码,托管指的是属于 C#端的任何东西(也就是我们的代码和 Unity 的 C#引擎代码)。我们可以使用可视化工具探索快照,并快速看到哪种类型的对象消耗了最多的 RAM,以及它们如何被其他对象引用。

要开始使用内存分析器,请执行以下操作:

  1. 安装包管理器窗口 | 包管理器)。记得将包模式设置为Unity 注册表并启用预览包(齿轮图标 | 高级项目设置 | 启用预览包)。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.33 – 启用预览包

  1. 窗口 | 分析 | 内存分析器中打开内存分析器

  2. 玩游戏并在内存分析器窗口中点击捕获玩家按钮:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.34 – 启用预览包

  1. 点击快照旁边的打开按钮以打开树视图,在这里你可以看到内存按类型分成块:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.35 – 内存块

  1. 在我们的案例中,我们可以看到RenderTexture使用了最多的内存,这属于在场景中显示的图像,以及一些用于后处理效果的纹理。尝试禁用PPVolume对象并拍摄另一个快照以检测差异。

  2. 在我的情况下,这减少了 130 MB。还有其他用于其他效果的纹理,例如 HDR。如果您想探索剩余 MB 的来源,请单击块以将其细分为其对象,并根据纹理的名称进行猜测:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.36 - 内存块详细信息

  1. 您可以在 Texture2D 块类型中重复相同的过程,该类型属于我们模型材质中使用的纹理。您可以查看最大的纹理并检测其使用情况 - 也许这是一个从未被近距离看到的大纹理,无法证明其大小。然后,我们可以使用纹理导入设置的最大尺寸来减小其大小。

重要提示

与任何性能分析器一样,直接在构建中进行性能分析总是有用的(关于这一点,我们将在下一章中详细介绍),因为在编辑器中拍摄快照将捕获编辑器使用的大量内存,并且在构建中不会使用。这种情况的一个例子是加载不必要的纹理,因为编辑器可能在您单击它们以在检查器窗口中查看其预览时加载了它们。

请注意,由于内存分析器是一个包,其用户界面可能经常发生变化,但其基本思想将保持不变。您可以使用此工具来检测是否以意外的方式使用内存。在这里需要考虑的一个有用的事情是 Unity 在加载场景时加载资产的方式,这包括在加载时加载场景中引用的所有资产。这意味着您可以有一个例如,具有对材质的引用的预制体数组,甚至如果您不实例化它们的任何实例,预制体也必须在内存中加载,导致它们占用空间。在这种情况下,我建议您探索地址可寻址性的使用,它提供了一种动态加载资产的方式。但现在让我们保持简单。

您可以通过性能分析器做更多事情,例如访问所有对象的列表视图,并观察每个对象的每个字段及其引用,以查看使用它的对象(从主菜单,转到 TreeMap | Table | All objects),但对于初学者来说,我发现那个视图有点混乱。内存分析器引用导航系统的一个很好的替代方案是使用性能分析器的内存模块。这是内存分析器的基本版本,不会向您显示带有良好树状视图的内存,也不会提供内存分析器可以提供的详细信息,但提供了一个更简单的引用导航器版本,这在大多数情况下已经足够了。

要使用它,请执行以下操作:

  1. 打开性能分析器(窗口 | 分析 | 性能分析器)。

  2. 在播放模式下,通过性能分析器模块列表向下滚动,并选择内存。

  3. 在 Gather object references 切换打开的情况下,单击 Take Sample Playmode。

  4. 探索弹出的列表,打开类别并选择一个资产。在下面的屏幕截图中,您可以看到我已经选择了纹理,并且在右侧面板上,我可以探索引用。这个纹理被一个名为 base color 的材质使用,该材质被一个名为 floor_1_LOD0 的 GameObject 中的网格渲染器引用。您甚至可以单击引用列表中的项目以突出显示引用对象:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19.37 - 参考列表

正如您所看到的,内存分析器和分析器中的内存模块做了类似的事情。它们可以为您拍摄内存快照以供分析。我相信随着时间的推移,Unity 将统一这些工具,但目前,根据它们的优势和劣势,例如内存分析器比较两个快照以分析差异的能力,或者探索内存的低级数据的能力,比如查看哪个托管对象正在使用哪个本机对象(这是相当高级的,大多数情况下是不必要的)。您可以使用内存模块来分析引用,查看哪个对象正在使用哪个纹理以及原因。

总结

优化游戏并不是一项容易的任务,特别是如果您不熟悉每个 Unity 系统的内部工作原理。遗憾的是,这是一项艰巨的任务,没有人知道每个系统的细节,但是通过本章学习的工具,我们有一种方法通过探索来探索变化如何影响系统。我们学会了如何对 CPU、GPU 和 RAM 进行分析,以及任何游戏中关键硬件是什么,并且涵盖了一些常见的良好实践方法,以避免滥用它们。

现在,您可以诊断游戏中的性能问题,收集关于三个主要硬件部件(CPU、GPU 和 RAM)性能的数据,然后利用这些数据来集中优化工作,应用正确的优化技术。性能很重要,因为您的游戏需要顺畅运行,给用户带来愉快的体验。

在下一章中,我们将看到如何创建我们游戏的构建版本,与其他人分享,而无需安装 Unity。

第二十章:构建项目

因此,我们已经达到了一个可以用真实人员测试游戏的阶段。问题在于,我们不能假装人们会安装 Unity,打开一个项目,然后点击播放。他们希望收到一个漂亮的可执行文件,双击即可立即播放。在本章中,我们将讨论如何将我们的项目转换为易于共享的可执行格式。

在本章中,我们将讨论以下构建概念:

  • 构建项目

  • 调试构建

构建项目

在软件开发中(包括视频游戏),将我们项目的源文件转换为可执行格式的结果称为构建。生成的可执行文件经过优化,以获得最大可能的性能。由于项目的不断变化性质,我们无法在编辑游戏时获得性能。在编辑游戏时,准备资产到最终形式将是耗时的。此外,生成的文件具有难以阅读的格式。它们不会将纹理、音频和源代码文件放在那里供用户查看。它们将以自定义文件结构格式化,因此在某种程度上,它们受到用户窃取的保护。

重要提示

实际上,有几种工具可以从视频游戏中提取源文件,尤其是从 Unity 这样广泛使用的引擎中。您可以提取资产,如纹理和 3D 模型,甚至有一些程序可以直接从 VRAM 中提取这些资产,因此我们无法保证这些资产不会在游戏之外使用。最终,用户在他们的磁盘上拥有这些资产的数据。

当您将目标定为 PC、Mac 或 Linux 等桌面平台时,构建过程非常简单,但在构建之前,我们需要牢记一些设置。我们将要看到的第一个配置是场景列表。我们已经讨论过这一点,但现在是一个很好的时机来记住,将此列表的第一个元素设置为将首先加载的场景非常重要。记住,您可以通过转到File | Build Settings并将所需的起始场景拖到列表顶部来实现这一点。在我们的情况下,我们将游戏场景定义为第一个场景,但在一个真正的游戏中,最好创建一个使用 UI 和一些图形的主菜单场景:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.1 - 场景列表顺序

您可以在这里更改的另一个设置是目标平台 - 将为其创建构建的目标操作系统。通常,这是设置为您正在开发的相同操作系统,但是,如果您例如在 Mac 上开发,并且希望为 Windows 构建,只需设置exe而不是app。您可能会看到 Android 和 iOS 作为其他目标平台,但制作移动游戏需要其他考虑,我们不会在本书中讨论:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.2 - 目标平台

在同一个窗口中,您可以单击左下角的Player Settings按钮,或者只需打开Edit | Project Settings窗口,然后单击Player类别,即可访问其余的构建设置。Unity 将生成的可执行文件称为游戏玩家。在这里,我们有一组配置,将影响构建或玩家的行为,以下是基本配置列表:

  • 产品名称:这是窗口标题栏和可执行文件中游戏的名称。

  • 公司名称:这是开发游戏的公司名称,Unity 用它来创建某些文件路径,并将其包含在可执行信息中。

  • 默认图标:在这里,您可以选择一个纹理作为可执行文件的图标。

  • 光标热点属性指的是您希望光标执行点击操作的图像像素。

  • 分辨率和呈现:关于我们的游戏分辨率将如何处理的设置。

  • 分辨率和演示|默认为本机分辨率:勾选此项并且游戏在全屏模式下运行时,Unity 将使用系统当前使用的分辨率。您可以取消选中此项并设置所需的分辨率。

  • **启动图像:**关于游戏在首次加载后显示的启动图像的设置。

  • 启动图像|显示启动画面:这将启用 Unity 启动画面,作为游戏的介绍显示标志。如果您有 Unity Pro 许可证,您可以取消选中此项,以创建自定义的启动画面。

  • 启动图像|标志列表:在这里,您可以添加一组图像,Unity 将在启动游戏时显示。如果您免费使用 Unity,则强制在此列表中显示 Unity 标志。

  • 全部顺序以显示每个标志,一个接一个地显示,或者选择Unity 标志下方,以显示您的自定义介绍标志,并始终显示 Unity 标志下方:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.3 – 玩家设置

在配置这些设置后,下一步是进行实际构建,可以通过在“文件|构建设置”窗口中点击“构建”按钮来完成。这将要求您设置构建文件的创建位置。我建议您在桌面上创建一个空文件夹,以便轻松访问结果。请耐心等待 - 根据项目的大小,这个过程可能需要一些时间:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.4 – 构建游戏

这里可能出现的问题是具有非构建兼容脚本 - 仅在编辑器中执行的脚本,主要是编辑器扩展。我们没有创建任何这样的脚本,所以如果构建后在控制台中出现错误消息,类似于以下截图,那可能是因为某个 Asset Store 包中的某个脚本。在这种情况下,只需删除在构建错误消息之前在控制台中显示的文件。如果碰巧有一个您的脚本在其中,请确保您的脚本中没有任何using UnityEditor;行。这将尝试使用编辑器命名空间,该命名空间不包含在构建编译中以节省磁盘空间:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.5 – 构建错误

这基本上就是您需要知道的一切。您已经生成了您的游戏!需要注意的是,在构建时指定的文件夹中创建的每个文件都必须共享,不仅仅是可执行文件。Data文件夹包含所有资产,并且在共享 Windows 构建游戏时包含这些文件是很重要的。对于 Linux 和 Mac 构建,只生成一个文件(分别是x86/x86_64app packages):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.6 – 一个由 Windows 生成的文件夹

最后一个建议 - 注意构建窗口中的仅构建脚本复选框。如果只更改了代码并希望测试该更改,请勾选它并进行构建。这将使过程比常规构建更快。只需记住,如果您在编辑器中更改了其他内容,请取消选中此项,因为如果您勾选了它,这些更改将不会包含在内。

现在我们已经构建了,您可以通过双击可执行文件来测试它。现在您已经尝试了您的构建,我们可以讨论如何使用与我们在编辑器中使用的相同的调试和性能分析工具来测试我们的构建。

调试构建

在理想的世界中,编辑器和构建将表现相同,但遗憾的是这并不是真的。编辑器准备在快速迭代模式下工作。代码和资源在使用之前经过最少的处理,以便经常快速地进行更改,这样我们就可以轻松测试我们的游戏。当游戏构建完成时,将应用一系列优化和与编辑器项目的差异,以确保我们能够获得最佳性能,但这些差异可能导致游戏的某些部分表现不同,使得玩家的分析数据与编辑器不同。这就是为什么我们要探索如何在构建中调试和分析我们的游戏。

在本节中,我们将研究以下构建调试概念:

  • 调试代码

  • 性能分析

让我们开始讨论如何调试构建的代码。

调试代码

由于玩家代码编译方式不同,我们可能会在构建中遇到在编辑器中没有发生的错误,并且我们需要以某种方式进行调试。我们有两种主要的调试方式——通过打印消息和断点。所以,让我们从第一种消息开始。如果你运行了可执行文件,你可能已经注意到没有控制台可用。全屏只有游戏视图,这是有道理的;我们不想用烦人的测试消息来分散用户的注意力。幸运的是,消息仍然被打印出来,但是在一个文件中,所以我们可以去那个文件中查找它们。

位置根据操作系统而变化。在这个列表中,你可以找到可能的位置:

  • ~/.config/unity3d/CompanyName/ProductName/Player.log

  • ~/Library/Logs/Company Name/Product Name/Player.log

  • C:\Users\username\AppData\LocalLow\CompanyName\ProductName\Player.log

在这些路径中,你必须用我们之前设置的Player设置属性的值来更改CompanyNameProductName,这两个属性的值是相同的,username用你在 Windows 中执行游戏的账户名。请注意,文件夹可能是隐藏的,所以在你的操作系统中启用显示隐藏文件的选项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.7 - 显示隐藏文件

在那个文件夹里,你会找到一个名为Player的文件;你可以用任何文本编辑器打开它并查看消息。在这种情况下,我使用了 Windows,所以目录路径看起来像下面的截图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.8 - 调试目录

除了从资产商店下载任何自定义包之外,还有一种方法可以直接在游戏中查看控制台的消息,至少是错误消息——通过创建一个开发构建。这是一个特殊的构建,允许扩展的调试和分析能力,以换取不像最终构建那样完全优化代码,但对于一般调试来说足够了。你可以通过在文件 | 构建设置窗口中勾选开发构建复选框来创建这种构建:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.9 - 开发构建复选框

请记住,这里只会显示错误消息,所以你可以做一个小技巧,用Debug.LogError替换printDebug.Log函数调用,这样也会在控制台中打印消息,但会有一个红色图标。请注意,这不是一个好的做法,所以限制使用这种消息进行临时调试。对于永久记录,使用日志文件或在资产商店中找到一个自定义的运行时调试控制台。

请记住,要使开发构建起作用,你需要重新构建游戏;幸运的是,第一次构建需要最长的时间,接下来会更快。这次,你只需点击构建并运行按钮,就可以在之前构建的文件夹中进行构建:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.10 - 调试错误消息

在下一个截图中,您可以看到运行时显示的错误。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.11 – 开发构建中的错误消息

此外,您也可以像我们在*第十三章中解释的那样使用常规断点。将 IDE 附加到玩家上后,它将显示在目标列表中。但是为了使其工作,您不仅需要在构建窗口中勾选开发构建**,还需要勾选脚本调试。在这里,当勾选了脚本调试后,会显示一个额外的选项,允许您暂停整个游戏直到调试器附加上,这个选项叫做等待托管调试器。如果您想要测试一些立即发生并且不允许您足够时间附加调试器的事情,这将非常有用:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.12 – 启用脚本调试

我们有另一种方式来查看消息,但这需要性能分析器的工作,所以让我们借此机会讨论如何对编辑器进行性能分析。

性能分析

这次我们将使用与上一章相同的工具来对玩家进行性能分析。幸运的是,差异很小。与上一节一样,您需要在开发模式下构建玩家,在构建窗口中勾选开发构建复选框,然后性能分析器应该会自动检测到它。

让我们开始使用性能分析器来进行构建,具体操作如下:

  1. 通过构建来玩游戏。

  2. 使用Alt + Tab(Mac 上为command + tab)切换到 Unity。

  3. 打开性能分析器。

  4. 点击菜单中的播放模式,选择包含Player的项目。因为我使用的是 Windows,所以它显示为WindowsPlayer

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.13 – 对玩家进行性能分析

请注意,当您点击一个帧时,游戏不会像在编辑器中那样停止。如果您想要在特定时刻专注于帧,您可以点击记录按钮(红色圆圈)使性能分析器停止捕获数据,这样您就可以分析到目前为止捕获的帧。

此外,您还可以看到当性能分析器附加到玩家时,控制台也会附加,因此您可以直接在 Unity 中看到日志。请注意,此版本需要 Unity 打开,并且我们不能期望测试我们游戏的朋友们也有它。您可能需要点击控制台上的Player按钮,并勾选玩家日志记录才能使其工作:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.14 – 在附加性能分析器后启用玩家日志记录

帧调试器也已启用以与玩家一起工作。您需要在帧调试器中点击编辑器按钮,然后再次,您将在可能的调试目标列表中看到玩家;选择它后,像往常一样点击启用。请注意,绘制调用的预览不会出现在游戏视图中,而是出现在构建本身中。如果您在全屏模式下运行游戏,可能需要在 Unity 和构建之间来回切换:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.15 – 调试游戏玩家的帧

您也可以在窗口模式下运行游戏,将全屏模式属性设置为窗口,并设置一个小于您的桌面分辨率的默认分辨率,以便同时在 Unity 和玩家中看到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.16 – 启用窗口模式

最后,内存分析器还支持对玩家进行分析,你可以在窗口顶部的第一个按钮点击时显示的列表中选择玩家,然后点击捕获玩家

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20.17 – 对玩家进行内存快照

就是这样。正如你所看到的,Unity Profilers 被设计为可以轻松集成到玩家中。如果你开始从中获取数据,你会发现与编辑器分析相比,特别是在内存分析器中,会有很大的不同。

总结

在本章中,我们看到了如何创建游戏的可执行版本,并正确配置它,以便你不仅可以与朋友分享,还可以与世界分享!我们还讨论了如何对我们的构建进行分析;记住,这样做将为我们提供比分析编辑器更准确的数据,这样我们就可以更好地提高游戏的性能。

但在此之前,让我们讨论一些最后的细节。这些不是 Unity 相关的细节,而是游戏相关的细节;在向除自己和在游戏开发过程中看到游戏的任何人之外的人展示游戏之前和之后,你需要考虑的事情。我们将在下一章中进行讨论。

第二十一章:润色

我们到了!我们现在有了一个完全开发好的游戏,所以我们现在可以赚点钱了吗?很遗憾,不行。一个成功的游戏依赖于大量的细化;细节决定成败!而且,不要对赚钱感到太兴奋;这是你的第一个游戏,还有很多与开发无关的任务要完成。现在是时候讨论我们已经取得的成就能做些什么了。

在本章中,我们将讨论以下概念:

  • 迭代你的游戏

  • 发布你的游戏

迭代你的游戏

我们即将完成我们的第一个游戏迭代。我们有一个想法,我们实现了它,现在是时候测试它了。在这次测试之后,我们将得到一些可以改进的反馈,所以我们将制定改进的想法,实施它们,测试它们,然后重复。这就是迭代。

在这一部分,我们将讨论以下迭代概念:

  • 测试和反馈

  • 解决反馈

让我们首先讨论如何正确地在人们身上测试游戏。

测试和反馈

除了强大的营销策略外,你的游戏的成功还取决于游戏的前 10 分钟。如果你不能在那段时间内吸引玩家的注意,你肯定会失去他们。你的游戏的第一印象很重要。那前 10 分钟必须是完美的,但遗憾的是,我们对游戏的感知在这里并不重要。你花了好几个小时玩它,你知道每个关卡的每一寸地方,知道如何正确地控制你的角色,以及你的游戏的所有机制和动态—这是你的游戏。你爱它就是它。这是一个巨大的成就。现在,一个从未玩过游戏的人不会有同样的感觉。这就是为什么测试如此重要。

第一次让别人玩你的游戏时,你会感到震惊—相信我,我也有过这种经历。你会注意到玩家可能不会理解游戏。他们不会理解如何控制玩家或者如何赢得游戏,并且会卡在你从未想象过会困难的关卡部分。到处都是 bug,一团糟—但那很好!那正是测试你的游戏的目的,获得有价值的信息或反馈。如果你正确对待这些反馈,这些反馈将使你的游戏变得更好。

在测试会话中,有两个主要的反馈来源—观察和用户反馈。观察是默默地看着玩游戏的人,看他们如何玩—他们首先按下哪些键,当发生某事时他们的反应是什么,当他们以意想不到的方式开始感到沮丧时是什么时候(有些游戏依赖于沮丧,比如黑暗之魂),并且通常检查玩家是否得到了你期望的确切体验。

观察的沉默部分至关重要。你必须与玩家交谈,尤其是不要给他们任何提示或帮助,至少在他们完全迷失并且测试会话无法继续进行而需要帮助的情况下才可以—这种情况本身也是一种有用的反馈形式。你必须观察玩家在他们自然状态下的表现,就像他们在家里玩游戏时的情况一样。如果不是这样,收集到的反馈将是有偏见的,也不会有用。在测试大型游戏时,甚至会在盖塞尔室进行测试。这些房间有一块只能从一侧看到的玻璃—就像一个审讯室,但不那么可怕。这样,玩家就不会感到任何被观察的压力:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21.1 – 盖塞尔室

第二个来源是直接反馈,基本上是询问测试人员在测试后对游戏的印象。在这里,你可以先让测试人员告诉你他们的体验,并提供任何反馈,然后你可以开始问与该反馈相关的问题或与测试相关的其他问题。这可能包括问题,比如“你觉得控制方式如何?游戏中哪一部分让你最沮丧?哪一部分最有回报?你愿意为这个游戏付费吗?”

在从测试人员那里接受反馈时需要考虑的一件重要事情是他们是谁。他们是朋友、亲戚还是完全陌生人?当与亲近的人一起测试时,反馈可能不会有用。他们会试图淡化游戏的不足之处,因为他们可能认为你让他们玩游戏是为了得到赞美,但事实并非如此。你需要真实、严厉、客观的反馈——这是你真正改进游戏的唯一途径。

所以,除非你的朋友对你非常诚实,否则请尝试在陌生人身上测试你的游戏。这可以是你教育机构的其他学生,或者你的工作场所,或者街上的随机人。尝试去游戏展会上展示独立游戏。此外,在测试时要考虑你的目标受众。如果你的游戏是休闲手机游戏,你不应该把它带到“毁灭”聚会上,因为你大多会收到无关的反馈。了解你的受众并寻找他们。此外,考虑到你可能需要在至少 10 个人身上测试你的游戏。你会注意到也许有一个人不喜欢这个游戏,其他 9 个人喜欢。就像统计学一样,你的样本必须足够大才能被认为是有效的。

此外,即使我们说我们的感知不重要,如果你运用常识并对自己诚实,你也可以从自己的游戏测试中获得反馈。但既然我们已经收集了反馈,我们可以用它做些什么呢?

解释反馈

你得到了你想要的——关于你的游戏的大量信息。现在你该怎么办?嗯,这取决于反馈。你有不同类型和不同的解决方法。最容易解决的反馈是错误,例如,当我放进钥匙时门没有打开,无论我射了多少子弹敌人都不会死,等等。要解决这些问题,你必须逐步进行玩家的操作,以便你能够重现问题。一旦你重现了它,调试你的游戏以查看错误——也许是由于空值检查或场景中的错误配置引起的。

尽量收集关于情况的尽可能多的细节,比如问题发生的时间和级别,玩家拥有的装备,玩家剩下的生命次数,或者玩家是在空中还是蹲下——任何能让你达到完全相同情况的数据。有些错误可能很棘手,有时会在最奇怪的情况下发生。你可能会认为发生 1%的奇怪错误可以忽略不计,但请记住,如果你的游戏成功,将会有数百,甚至数千名玩家玩——这 1%可能会严重影响你的玩家群。

然后,你需要平衡反馈。你可能会得到反馈,比如子弹不够,生命太多,敌人太难,游戏太容易,或者游戏太难。这必须与你的目标一起考虑。你真的希望玩家子弹或生命不够吗?你希望敌人难以击败吗?在这种情况下,玩家觉得困难的事情可能正是你想要的体验,这就是你需要记住目标受众的地方。也许给你反馈的用户不是你期望玩游戏的人(再次想想《黑暗之魂》的例子,这款游戏并不适合所有人)。但如果玩家是目标受众,你可能需要平衡。

平衡是当你需要微调游戏数字,比如子弹数量、波数、敌人、敌人的生命、敌人的子弹等等。这就是为什么我们暴露了大量脚本属性——这样它们就可以很容易地改变。这可能是一个复杂的过程。让所有这些数字一起运作是困难的。如果你把一个属性增加得太多,可能需要减少另一个属性。你的游戏基本上就是一个大的计算表格。实际上,大多数游戏设计师都精通使用电子表格来做这件事——平衡游戏,进行计算,并看看改变一个单元格如何改变另一个单元格——在进行艰难的测试之前,先玩游戏。

在下面的截图中,你可以看到我们如何准备我们的Player对象在编辑器中轻松配置:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21.2 – 影响游戏玩法的一些属性

你还可以得到一些反馈,比如“我不明白玩家为什么会这样做”,“我不明白反派的动机”,等等。这可能很容易被低估,但要记住,你的游戏机制、美学和故事(如果有的话)必须保持一致。如果其中一个元素失败了,其他元素也有失败的风险。如果你的游戏设定在未来,但你的主要武器是一把金属剑,你需要以某种方式证明它的存在,也许通过一个故事点。如果你的敌人想要摧毁世界,但看起来是一个善良的人,你需要以某种方式证明这一点。这些细节是使游戏可信的关键。

最后,你会得到感知反馈,比如“这个游戏没能让我娱乐”或“我不喜欢这个游戏”。如果你问对了问题,这些反馈可以转化为其他反馈,但有时测试人员不知道问题出在哪里;游戏可能在他们眼中感觉不对劲。当然,这本身并不有用,但不要低估它。这可能是你需要进行进一步测试的暗示。

在游戏开发中,当你认为游戏已经完成时,你会发现你刚刚开始开发它。测试会让你意识到游戏直到玩家对游戏满意才算完成,这可能需要比准备第一个版本更多的时间,所以要准备好不断迭代游戏。

大型游戏可能需要花费数年时间才能制作出第一个原型,在游戏的早期阶段进行测试,有时会使用虚假资产来隐藏可能泄露游戏信息或让竞争对手意识到他们的计划的敏感信息。一些开发者甚至会发布一个基于主游戏的迷你游戏,故事和美学不同,只是为了测试一个想法。此外,还有软发布,游戏会发布但只针对受限的受众——也许是特定国家,不是你的主要受众和收入来源——以在将游戏发布到全球之前测试和迭代游戏。

所以,请耐心等待。测试是游戏真正开发的地方,但在所有这些广泛的测试会话结束并且游戏完成后,下一步是什么?发布!

发布你的游戏

我们在这里——重要时刻!我们有金版构建,这是游戏的最终版本。我们是不是应该把它直接投放到目标商店(比如 Steam、Google Play 商店、苹果应用商店等)?嗯…实际上,我们还有很多工作要做,这些工作在达到金版构建之前就应该开始了。所以,让我们探讨一下额外的工作是什么,以及应该在哪个阶段进行。

在这一部分,我们将研究以下发布阶段:

  • 预发布

  • 发布

  • 发布后

让我们从讨论预发布阶段开始。

预发布

在发布前和最好在开始开发游戏之前,要决定你将在哪里销售你的游戏。如今,这意味着选择一个数字商店——对于新兴独立开发者来说,销售实体游戏副本不是一个选择。你有几个选择,但对于 PC 来说,最常见的地方是 Steam,这是一个知名平台,允许你以 100 美元的价格将游戏上传到平台上。经过审核后,就可以发布了。在 iOS 上,唯一的方式是使用 App Store,它每年收取 100 美元的费用。最后,在 Android 上,你可以使用 Play 商店,它允许你以 25 美元的一次性付款发布游戏。游戏机有更严格的要求,所以我们不会提及它们。

在选择了一个数字商店之后,如果你没有做任何准备就发布你的游戏,你的游戏可能会在同一天发布的众多游戏中迅速被遗忘。如今,竞争是激烈的,可能会有数十款游戏在同一天发布,所以你必须以某种方式突出你的游戏。有很多方法可以做到这一点,但这需要数字营销方面的经验,这可能很困难。这需要除了常规开发者技能之外的技能。如果你坚持自己做而不雇人,这里有一些你可以做的事情。

首先,你可以创建一个游戏社区,比如一个博客或者群组,在这里你可以定期发布关于你的游戏的信息。这包括开发进展的更新、新功能的截图、新概念艺术等等。你的工作是吸引玩家的兴趣,即使游戏还没有发布,也要让他们对你的游戏保持兴趣,为了让他们在游戏发布时立刻购买。在这里,你需要有创意来保持他们对游戏的兴趣——变化发布的内容,也许与社区分享一些迷你游戏,让他们有机会赢得奖品,或者发布问卷调查或赠品;真的,做任何能吸引你观众注意的事情。

另外,尽量在离发布日期不是太近也不是太远的时候发展社区。这样,你就不会因为长时间等待而失去玩家的注意力,也可以对游戏的期望值诚实。游戏在开发过程中会发生很多变化,范围可能会从最初的设计中减少。你需要处理炒作,这可能是危险的。

当然,我们需要人们加入社区,所以你必须在某个地方发布它。你可以付费广告,但除了成本和难以使它们相关之外,还有其他免费的方法。你可以把你的游戏免费送给一位影响者,比如一个 YouTuber 或 Instagrammer,让他们玩你的游戏并向他们的观众发表评论。如果影响者不喜欢游戏,这可能会很困难,因为他们会诚实,这对你来说可能是不利的。所以,你真的需要确保给他们一个精心制作的版本,但不一定是最终版本。你也可以接触一些付费的影响者,但同样,这需要花钱。

你还有其他免费的选择,比如进入论坛或群组,发布关于你的游戏的信息,但要明智。不要让你的帖子感觉像廉价的广告——知道你在哪里发布。有些群体不喜欢这类帖子,会拒绝它们。试着寻找允许这种自我宣传的地方。有些群体就是为此而设立的,所以在某些社区中避免侵入性。

最后,你还有另一个选择,就是联系出版商,一家专门从事这种营销的公司。他们会拨款用于出版,并有专人负责管理你的社区,这可能会给你带来很大的帮助。你会有更多时间来创建你的游戏,但也会有一些缺点。首先,当然,他们会从你的游戏收入中抽成,而且根据出版商的不同,这个比例可能会很高。然而,你需要权衡一下,通过自己的营销能获得的收入。此外,出版商会要求你改变你的游戏以满足他们的标准。有些要求你的游戏本地化(支持多种语言),或者要求你的游戏支持某些控制器,有一定的教程方式等等。最后,要考虑到某些出版商与某些类型的游戏有关联,所以如果你正在创建一个激烈的动作游戏,你不会选择与休闲游戏出版商合作。找到适合你的出版商:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21.3 - 一些知名的出版商,其中一些不开发他们自己的游戏,只是发布它们

现在我们已经为发布做好了准备,那么我们如何发布游戏呢?

发布

除了你的游戏可能需要在所选的数字商店平台上进行的所有设置和集成(这取决于你的受众),在发布时还有一些需要考虑的事情。

一些商店可能会有一个审查过程,包括玩你的游戏,看看它是否符合商店的标准。举个例子,在写这本书的时候,苹果应用商店要求他们发布的每款游戏都必须有某种社交登录选项(比如 Facebook、Google 等),并且还要支持苹果登录。如果你不遵守这些规定,他们就不会接受你的游戏。另一个例子是 PS Vita,它要求你的游戏支持与前后触摸板的某种交互。所以,要早早了解这些要求。如果你不注意,它们会对你的游戏发布产生很大影响。

除了这些要求,当然还有其他需要满足的标准,比如是否有成人或暴力内容。考虑一个支持你所创建游戏类型的平台。有些甚至可能要求你从娱乐软件评级委员会ESRB)或类似的评级机构获得评级。你需要注意的另一个常见要求是游戏不应该崩溃,至少不应该在游戏的常规工作流程中崩溃。此外,游戏必须表现良好,不能有严重的性能问题,有时,你的初始游戏下载大小不能超过指定的最大限制,你通常可以通过在游戏本身中下载内容来解决这个问题(查找Addressables Unity 包)。再次强调,所有这些要求都取决于商店。

即使满足了这些要求,检查它们的过程可能需要时间 - 几天、几周,甚至有时几个月。所以,在确定发布日期时要记住这一点。在大型游戏机上,这个过程可能需要几个月,有时开发者会利用这段时间创建著名的第一天补丁,这是一个修复 bug 的补丁,虽然不会阻止游戏发布,但有助于整体游戏体验。这是一个值得商榷但可以理解的做法。

最后,记住发布的第一天至关重要。你将出现在商店的新发布部分,这是你获得最多曝光的地方。之后,所有的曝光主要都依赖于你的营销和销售。一些商店允许你被推荐。你可以直接与商店的代表交谈,看看你能否做到这一点。如果商店对你的游戏感兴趣,他们可能会推荐你(或者你可能需要为此付费)。第一天很重要,所以要做好准备。

现在,游戏已经发布并交到了玩家手中。我们的工作完成了吗?几年前,也许是真的,但现在不是。我们仍然需要进行发布后的工作。

发布后

即使游戏已经发布,这并不是停止测试的借口。如果你的游戏被成千上万的人玩,你实际上可以获得更多的反馈。遗憾的是,你不能在那里观察他们,但你可以自动化信息收集过程。你可以通过让你的代码向服务器报告分析数据来做到这一点,就像 Unity Analytics 包所做的那样。即使这些信息不像面对面测试那样直接,但通过这种方式可以收集大量的数据和统计信息,你可以通过更新实时改进游戏,这是老游戏无法像今天这样轻松做到的。没有游戏是完美的,有时由于时间压力,你可能需要提前发布游戏,所以准备好在发布后定期更新游戏。有一些游戏在发布时表现不佳,但后来得以重生。不要低估最后的行动。你已经花了太多时间来放弃你发布不佳的游戏。

此外,如果你的货币化模式依赖于应用内购买,这意味着人们会在战利品箱或装饰物上花钱,你将需要不断更新内容。这将使玩家继续玩你的游戏。他们玩得越多,就会在游戏上花费更多的钱。你可以利用通过分析收集的信息,不仅修复你的游戏,还可以决定哪些内容被玩家消费最多,并专注于那些内容。你还可以进行 A/B 测试,即向不同用户发布两个版本的更新,看哪个版本最成功。这使你可以在实时游戏中测试想法。正如你所看到的,还有很多工作要做。此外,使用指标来跟踪玩家是否对你的游戏失去兴趣,如果是,为什么——是有难度的关卡吗?游戏太容易了吗?关注你的玩家群体。在你创建的社区中向他们提问,或者只是看评论——用户通常愿意告诉你他们希望如何改进他们最喜欢的游戏。

总结

开发游戏只是工作的一部分;要使其成功发布可能是一项巨大的任务。有时,这可能比游戏本身的成本还要高。因此,除非你是为了乐趣而制作游戏,如果你想靠制作游戏谋生,你将需要学会如何管理发布,或者雇佣能够帮助你的游戏的预发布、发布和发布后阶段的人员,这可能是一个明智的举措。

当然,本章仅对这个重要主题进行了简单介绍,所以我建议您如果想认真对待游戏开发的这一部分,可以阅读一些额外的材料。一个非常详细和简洁的信息来源是Extra Credits YouTube 频道,提供了传递有价值信息的短视频。此外,还有一本名为《游戏设计的艺术:透镜之书》的好书,提供了对游戏设计的全面介绍。

恭喜,您已经完成了本书的第三部分!您已经获得了开始游戏开发职业生涯并选择其中一些角色的基本知识。我建议您在阅读更多关于这个主题的书籍之前将所学知识付诸实践。获取信息很重要,但将信息转化为知识的唯一途径是通过实验。只需确保平衡理论和实践。

在本书的下一部分中,我们将探讨一些可能对您感兴趣的额外主题,首先介绍扩展现实应用程序。

第二十二章:Unity 中的增强现实

如今,新技术扩展了 Unity 的应用领域,从游戏到各种软件,比如模拟、培训、应用等等。在 Unity 的最新版本中,我们看到了在**增强现实(AR)**领域的许多改进,这使我们能够在现实之上添加一层虚拟,从而增强我们的设备可以感知的内容,从而创建依赖于真实世界数据的游戏,比如摄像头的图像、我们的真实位置和当前的天气。这也可以应用于工作环境,比如查看建筑地图或检查墙内的电气管道。欢迎来到本书的额外部分,在这里我们将讨论如何使用 Unity 的 AR Foundation 包创建 AR 应用程序。

在本章中,我们将研究以下 AR Foundation 概念:

  • 使用 AR Foundation

  • 为移动设备构建

  • 创建一个简单的 AR 游戏

在本章结束时,你将能够使用 AR Foundation 创建 AR 应用程序,并且将拥有一个完全功能的游戏,使用其框架,以便你可以测试框架的能力。

让我们开始探索 AR Foundation 框架。

使用 AR Foundation

在 AR 方面,Unity 有两个主要工具来创建应用程序:Vuforia 和 AR Foundation。Vuforia 是一个几乎可以在任何手机上工作的 AR 框架,并且包含了基本 AR 应用程序所需的所有功能,但高级功能需要付费订阅。另一方面,完全免费的 AR Foundation 框架支持我们设备的最新 AR 本地功能,但只受新设备支持。选择其中一个取决于你要构建的项目类型和目标受众。然而,由于本书旨在讨论最新的 Unity 功能,我们将探讨如何使用 AR Foundation 来创建我们的第一个 AR 应用程序,以便检测现实世界中图像和表面的位置。因此,我们将开始探索其 API。

在本节中,我们将研究以下 AR Foundation 概念:

  • 创建一个 AR Foundation 项目

  • 使用跟踪功能

让我们从讨论如何准备我们的项目,以便它可以运行 AR Foundation 应用程序开始。

创建一个 AR Foundation 项目

创建 AR 项目时需要考虑的一些事情是,我们不仅会改变我们编写游戏的方式,还会改变游戏设计方面。AR 应用程序有差异,特别是用户交互的方式,还有一些限制,比如用户始终控制摄像头。我们不能简单地将现有游戏移植到 AR 中而不改变游戏的核心体验。这就是为什么在本章中,我们将致力于一个全新的项目;到目前为止,我们创建的游戏要想在 AR 中运行良好,改变它将会太困难了。

在我们的案例中,我们将创建一个游戏,用户控制一个移动“标记”的玩家,这是一个可以打印的物理图像,可以让我们的应用程序识别玩家在现实世界中的位置。我们将能够在移动图像的同时移动玩家,并且这个虚拟玩家将自动向最近的敌人射击。这些敌人将从用户需要放置在家中不同部分的特定生成点生成。例如,我们可以在墙上放置两个生成点,并将我们的玩家标记放在房间中间的桌子上,这样敌人就会朝着它们走去。在下面的图片中,你可以看到游戏将会是什么样子的预览:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.1 - 完成的游戏。圆柱体是敌人生成器,胶囊体是敌人,立方体是玩家。这些都被手机显示的标记图像定位

我们将以与创建第一个游戏相同的方式开始创建基于 URP 的新项目。需要考虑的是,AR Foundation 可以与其他管道一起使用,包括内置管道,以防您想在已有项目中使用它。如果您不记得如何创建项目,请参考[第二章](B14199_02_Final_SK_ePub.xhtml#_idTextAnchor040),设置 Unity。一旦进入新的空白项目中,就像我们之前安装其他软件包一样,从软件包管理器中安装 AR Foundation 软件包;也就是说,从窗口|软件包管理器。记得设置软件包管理器,以便显示所有软件包,而不仅仅是项目中的软件包(窗口左上角的软件包按钮需要设置为Unity Registry)。在撰写本书时,最新版本是 4.0.2。请记住,您可以使用查看其他版本按钮,该按钮出现在列表中软件包项下的软件包左侧的三角形上,以显示其他版本选项。如果您找到比我的更新版本,可以尝试使用该版本,但通常情况下,如果某些功能与我们想要的不同,请安装此特定版本:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.2 - 安装 AR Foundation

在安装其他所需软件包之前,现在是讨论 AR Foundation 框架的一些核心思想的好时机。这个软件包本身什么也不做;它定义了移动设备提供的一系列 AR 功能,比如图像跟踪、云点和对象跟踪,但如何实现这些功能的实际实现包含在提供程序软件包中,比如 AR Kit 和 AR Core XR 插件。这样设计是因为,根据您想要使用的目标设备,这些功能的实现方式会发生变化。例如,在 iOS 中,Unity 使用 AR Kit 来实现这些功能,而在 Android 中,它使用 AR Core;它们是特定于平台的框架。

需要考虑的是,并非所有 iOS 或 Android 设备都支持 AR Foundation 应用程序。在互联网上搜索 AR Core 和 AR Kit 支持的设备时,您可能会找到受支持设备的更新列表。撰写本文时,以下链接提供了受支持设备列表:

此外,没有 PC 提供程序包,因此迄今为止测试 AR Foundation 应用程序的唯一方法是直接在设备上进行测试,但测试工具即将发布。在我的情况下,我将为 iOS 创建一个应用程序,因此除了AR Foundation软件包外,我还需要安装ARKit XR插件。但是,如果您想为 Android 开发,请安装ARCore XR插件(如果您针对两个平台,请安装两者)。我将使用 ARKit 软件包的 4.0.2 版本,但在撰写本书时,ARCore 推荐的版本是 4.0.4 通常,AR Foundation提供程序软件包的版本匹配,但应用与选择AR Foundation版本时相同的逻辑。在下面的屏幕截图中,您可以看到软件包管理器中的ARKit软件包:

图 22.3 - 安装特定于平台的 AR 提供程序包

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_22.3_B14199.jpg)

图 22.3 - 安装特定于平台的 AR 提供程序包

现在我们有了所需的插件,我们需要为 AR 准备一个场景,如下所示:

  1. 文件|新场景中创建一个新场景。

  2. 删除主相机;我们将使用另一个。

  3. 游戏对象|XR菜单中,创建一个AR 会话对象。

  4. 在同一个菜单中,创建一个AR 会话起源对象,其中包含一个相机外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.4 - 创建会话对象

  1. 您的层次结构应如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.5 - 起始 ARSCcene

AR 会话对象将负责初始化 AR 框架,并处理 AR 系统的所有更新逻辑。AR 会话原点对象将允许框架相对于场景定位跟踪对象,如图像和点云。设备会通知跟踪对象相对于设备认为的“原点”的位置。这通常是您在应用程序开始检测对象时指向的房屋的第一个区域,因此 AR 会话原点对象将代表该区域。最后,您可以检查原点内的相机,其中包含一些额外的组件,其中最重要的是AR 姿势驱动器,它将使您的相机对象随着您的设备移动。由于设备的位置是相对于会话原点对象的点,因此相机需要在原点对象内部。

在 URP 项目(我们的情况)中的一个额外步骤是,您需要设置渲染管道,以便支持在应用程序中渲染相机图像。为此,请转到创建项目时生成的Settings文件夹,查找Forward Renderer文件,并选择它。在Renderer Features列表中,单击添加渲染器功能按钮,然后选择AR 背景渲染器功能。请注意,如果您使用的是早于 AR Foundation 和 Provider 插件 4.0.0 版本的版本,则此选项可能不可用。在以下截图中,您可以看到前向渲染器资产应该是什么样子的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.6 - 添加对 URP 的支持

就是这样!我们已经准备好开始探索 AR Foundation 组件,以便我们可以实现跟踪功能。

使用跟踪功能

对于我们的项目,我们将需要 AR 中最常见的两种跟踪功能(但不是唯一的):图像识别和平面检测。第一种是检测特定图像在现实世界中的位置,以便我们可以将数字对象放在其上,例如玩家。第二种,平面检测,是识别现实生活中的表面,如地板、桌子和墙壁,以便我们知道可以放置对象的位置,例如敌人的生成点。只有水平和垂直表面被识别(某些设备上只有垂直表面)。

我们需要做的第一件事是告诉我们的应用程序它需要检测哪些图像,如下所示:

  1. 向项目添加一个图像,您可以打印或在手机上显示。有一种在现实世界中显示图像的方式是必要的来测试这一点。在这种情况下,我将使用以下图像:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.7 - 要跟踪的图像

重要提示

尽量获取包含尽可能多特征的图像。这意味着图像具有许多细节,如对比度、锐利的角落等。这些是我们的 AR 系统用来检测的;细节越多,识别就越好。在我们的情况下,我们使用的 Unity 标志实际上并没有太多细节,但有足够的对比度(只是黑白)和锐利的角落,以便系统识别它。如果您的设备在检测时出现问题,请尝试其他图像(经典的 QR 码可能会有所帮助)。

请注意,某些设备可能会对某些图像(例如本书中建议的图像)产生问题。如果在测试时出现问题,请尝试使用其他图像。您将在本章的后续部分在您的设备上测试这一点,所以请记住这一点。

  1. 通过单击Project Panel中的**+按钮并选择XR** | Reference Image Library来创建一个包含我们希望应用程序识别的所有图像的资产,创建一个参考图像库:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.8 – 创建参考图像库

  1. 选择库资产并单击添加图像按钮以向库中添加新图像。

  2. 将纹理拖到纹理槽(标有None的槽)。

  3. 打开Specify Size并将Physical Size设置为图像在现实生活中的大小,以米为单位。在这里尽量准确;在某些设备上,如果这个值不正确,可能会导致图像无法被跟踪:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.9 – 添加要识别的图像

既然我们已经指定了要检测的图像,让我们通过在真实世界的图像顶部放置一个立方体来测试这一点:

  1. 创建一个立方体的预制体并向其添加AR Tracked Image组件。

  2. AR Tracked Image Manager组件添加到AR Session Origin对象中。这将负责检测图像并在其位置创建对象。

  3. Image Library资产拖到组件的Serialized Library属性中,以指定要识别的图像。

  4. Cube预制体拖到组件的Tracked Image Prefab 属性中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.10 – 设置 Tracked Image Manager

就是这样!我们将看到一个立方体在现实世界中与图像相同的位置生成。请记住,您需要在设备上测试这一点,我们将在下一节中进行测试,所以现在让我们继续编写我们的测试应用程序:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.11 – 放置在手机显示的图像顶部的立方体

我们还要准备我们的应用程序,以便它可以检测和显示相机识别的平面表面。只需将AR Plane Manager组件添加到AR Session Origin对象即可:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.12 – 添加 AR Plane Manager 组件

当我们在房子上移动相机时,这个组件将检测表面平面。检测它们可能需要一段时间,所以重要的是要可视化检测到的区域,以确保它正常工作。我们可以通过组件引用手动获取有关平面的信息,但幸运的是,Unity 允许我们轻松可视化平面。让我们来看一下:

  1. 创建一个平面的预制体,首先在GameObject | 3D Object | Plane中创建平面。

  2. 添加一个Line Renderer。这将允许我们在检测到的区域边缘上画一条线。

  3. 0.01Color属性设置为黑色,并取消选中Use World Space外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.13 – 设置 Line Renderer

  1. 记得为Line Renderer创建一个合适的着色器材质,并将其设置为渲染器的材质:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.14 – 创建 Line Renderer 材质

  1. 另外,创建一个透明材质并在MeshRenderer平面中使用。我们希望能透过它看到真实表面,以便轻松地看到下面的真实表面:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.15 – 用于检测平面的材质

  1. Plane预制体添加AR PlaneAR Plane Mesh Visualizer组件。

  2. 将预制体拖动到AR Plane Manager组件的Plane Prefab属性中的AR Session Origin对象:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.16 – 设置平面可视化预制件

现在,我们有一种方法来看到平面,但看到它们并不是我们唯一能做的事情(有时,我们甚至不希望它们可见)。平面的真正力量在于将虚拟对象放置在现实表面上,点击特定平面区域,并获取其现实位置。我们可以使用 AR Plane Manager 或访问可视化平面的 AR Plane 组件来访问平面数据,但更简单的方法是使用AR Raycast Manager组件。

Unity 物理系统的Physics.Raycast函数,您可能还记得,用于创建从一个位置开始并朝着指定方向的虚拟射线,以使它们击中表面并检测确切的击中点。由AR Raycast Manager提供的版本,与物理碰撞体不同,它与跟踪对象发生碰撞,主要是点云(我们不使用它们)和我们正在跟踪的“平面”。我们可以通过以下步骤测试这个功能:

  1. AR Raycast Manager组件添加到AR Session Origin对象中。

  2. AR Session Origin对象中创建一个名为InstanceOnPlane的自定义脚本。

  3. ARRaycastManager中。您需要在脚本顶部添加using UnityEngine.XR.ARFoundation;行,以便在我们的脚本中可用。

  4. 创建一个List<ARRaycastHit>类型的私有字段并实例化它;Raycast 将检测我们的射线击中的每个平面,而不仅仅是第一个:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.17 – 存储射线击中的列表

  1. KeyCode.Mouse0下按下。在 AR 应用中,鼠标是用设备的触摸屏模拟的(您还可以使用Input.touches数组来支持多点触控)。

  2. if语句中,添加另一个条件来调用AR Raycast ManagerRaycast函数,将鼠标的位置作为第一个参数,将击中列表作为第二个参数。

  3. 这将向玩家触摸屏幕的方向投射射线,并将击中的结果存储在我们提供的列表中。如果有东西被击中,它将返回true,否则返回false外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.18 – 发射 AR 射线

  1. 添加一个公共字段来指定要在我们触摸的位置实例化的预制件。您可以只创建一个球体预制件来测试这个;这里不需要为预制件添加任何特殊组件。

  2. 在列表中存储的第一个击中的Pose属性的PositionRotation字段中实例化预制件。击中是按距离排序的,所以第一个击中是最近的。您的最终脚本应如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.19 – 射线投射器组件

在本节中,我们学习了如何使用 AR Foundation 创建新的 AR 项目。我们讨论了如何安装和设置框架,以及如何检测现实图像的位置和表面,然后如何将对象放置在其上。

正如您可能已经注意到的,我们从未点击播放按钮来测试这个,遗憾的是,在撰写本书时,我们无法在编辑器中测试这个。相反,我们需要直接在设备上测试这个。因此,在下一节中,我们将学习如何为 Android 和 iOS 等移动设备构建。

为移动设备构建

Unity 是一个非常强大的工具,可以非常轻松地解决游戏开发中最常见的问题之一,其中之一是为多个目标平台构建游戏。现在,为这些设备构建我们的项目的 Unity 部分很容易,但是每个设备都有其与 Unity 无关的细微差别,用于安装开发构建。为了测试我们的 AR 应用程序,我们需要直接在设备上测试它。因此,让我们探索如何使我们的应用程序在 Android 和 iOS 上运行,这是最常见的移动平台。

在深入讨论这个话题之前,值得一提的是,以下程序随时间变化很大,因此您需要在互联网上找到最新的说明。Unity Learn 门户网站(learn.unity.com/tutorial/building-for-mobile)可能是一个很好的选择,如果本书中的说明失败,请先尝试这里的步骤。

在本节中,我们将研究以下移动构建概念:

  • 为 Android 构建

  • 为 iOS 构建

让我们首先讨论如何构建我们的应用程序,以便在 Android 手机上运行。

为 Android 构建

与其他平台相比,创建 Android 构建相对容易,因此我们将从 Android 开始。请记住,您需要一台能够运行 AR Foundation 应用程序的 Android 设备,请参考本章第一节中提到的关于 Android 支持设备的链接。我们需要做的第一件事是检查我们是否已安装了 Unity 的 Android 支持并配置了我们的项目以使用该平台。要做到这一点,请按照以下步骤操作:

  1. 关闭 Unity 并打开Unity Hub

  2. 进入Installs部分,找到您正在使用的 Unity 版本。

  3. 单击 Unity 版本右上角的三个点按钮,然后单击Add Modules外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.20 – 向 Unity 版本添加模块

  1. 确保勾选Android Build Support以及单击左侧箭头时显示的子选项。如果没有,请勾选它们,然后单击右下角的Done按钮进行安装:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.21 – 向 Unity 添加 Android 支持

  1. 打开我们在本章中创建的 AR 项目。

  2. 进入Build SettingsFile | Build Settings)。

  3. 从列表中选择Android平台,然后单击窗口右下角的Switch Platform按钮:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.22 – 切换到 Android 构建

要在 Android 上构建应用程序,我们需要满足一些要求,例如安装 Java SDK(而不是常规的 Java 运行时)和 Android SDK,但幸运的是,Unity 的新版本会处理这些。只是为了再次确认我们已安装所需的依赖项,请按照以下步骤操作:

  1. 进入Unity Preferences(Windows 上为Edit | Preferences,Mac 上为Unity | Preferences)。

  2. 单击External Tools

  3. 检查 Android 部分上所有标有Installed with Unity的选项是否都已被选中。这意味着我们将使用 Unity 安装的所有依赖项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.23 – 使用已安装的依赖项

还有一些额外的与 Android AR Core 相关的设置需要检查,您可以在developers.google.com/ar/develop/unity-arf/quickstart-android找到。如果您使用的是更新版本的 AR Core,这些设置可能会发生变化。您可以按照以下步骤应用它们:

  1. 进入Player SettingsEdit | Project Settings | Player)。

  2. 取消选中Multithreaded RenderingAuto Graphics API

  3. Graphics APIs列表中删除Vulkan

  4. Minimum API Level设置为Android 7.0

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.24 – AR Core 设置

现在,您可以像往常一样从文件 | 构建设置构建应用,使用构建按钮。这一次,输出将是一个单独的 APK 文件,您可以通过将文件复制到您的设备并打开它来安装。请记住,为了安装未从 Play 商店下载的 APK 文件,您需要设置您的设备允许安装未知应用。这个选项的位置可能会有很大不同,取决于您使用的 Android 版本和设备,但这个选项通常位于安全设置中。一些 Android 版本在安装 APK 时会提示您查看这些设置。

现在,我们可以每次想要创建构建时复制和安装生成的 APK 构建文件。但是,我们可以让 Unity 使用构建和运行按钮为我们完成这些工作。这个选项在构建应用程序后,会查找通过 USB 连接到您的计算机的第一个 Android 设备,并自动安装应用程序。为了使这个工作,我们需要准备好我们的设备和 PC,具体操作如下:

在您的设备上,在设置部分找到构建号,其位置可能会根据设备而变化。在我的设备上,它位于关于手机 | 软件信息部分:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.25 – 查找构建号

  1. 轻点几次,直到设备显示您现在是一个程序员。这个过程会在设备中启用隐藏的开发者选项,您现在可以在设置中找到它。

  2. 打开开发者选项并打开USB 调试,这允许您的 PC 在您的设备上拥有特殊权限。在这种情况下,它允许您安装应用程序。

  3. 从您手机制造商的网站上安装 USB 驱动程序到您的计算机上。例如,如果您有一部三星设备,请搜索三星 USB 驱动程序。另外,如果您找不到,您可以搜索Android USB 驱动程序来获取通用驱动程序,但如果您的设备制造商有自己的驱动程序,这可能不起作用。在 Mac 上,这一步通常是不必要的。

  4. 连接您的设备(如果已连接,请重新连接)。设备上将出现允许 USB 调试的选项。选择始终允许并点击确定外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.26 – 允许 USB 调试

  1. 接受出现的允许数据提示。

  2. 如果这些选项不出现,请检查您的设备的USB 模式是否设置为调试而不是其他任何模式。

  3. 在 Unity 中,使用构建和运行按钮进行构建。

  4. 如果您在检测我们实例化播放器的图像时遇到问题,请记得尝试另一张图片(在我这里是 Unity 标志)。这可能会根据您的设备能力而有很大不同。

就是这样!现在您的应用程序已经在您的设备上运行了,让我们学习如何在 iOS 平台上做同样的事情。

为 iOS 构建

在 iOS 开发时,您需要花一些钱。您需要运行 Xcode,这是一款只能在 OS X 上运行的软件。因此,您需要一台可以运行它的设备,比如 MacBook,Mac mini 等。可能有办法在 PC 上运行 OS X,但您需要自己找出来并尝试。除了在 Mac 和 iOS 设备(iPhone,iPad,iPod 等)上花钱外,您还需要支付 99 美元/年的 Apple 开发者账户费用,即使您不打算在 App Store 上发布应用程序(可能有替代方案,但同样,您需要自己找到)。

因此,要创建 iOS 构建,您应该执行以下操作:

  1. 获取一台 Mac 电脑。

  2. 获取一个 iOS 设备。

  3. 创建一个 Apple 开发者账户(在撰写本书时,您可以在developer.apple.com/上创建一个)。

  4. 从 App Store 上安装 Xcode 到您的 Mac 上。

  5. 检查 Unity Hub 中是否安装了 iOS 构建支持。有关此步骤的更多信息,请参考在 Android 上构建部分:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.27 - 启用 iOS 构建支持

  1. 构建设置下切换到 iOS 平台,选择 iOS 并点击切换平台按钮:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.28 - 切换到 iOS 构建

  1. 点击构建设置窗口中的构建按钮,然后等待。

您会注意到构建过程的结果是一个包含 Xcode 项目的文件夹。Unity 无法直接创建构建,因此它生成了一个项目,您可以使用我们之前提到的 Xcode 软件打开。在本书中使用的 Xcode 版本(11.4.1)创建构建的步骤如下:

  1. 双击生成的文件夹中的.xcproject文件:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.29 - Xcode 项目文件

  1. 转到Xcode | 首选项

  2. 帐户选项卡中,点击窗口左下角的**+**按钮,并使用您注册为苹果开发者的苹果帐户登录:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.30 - 帐户设置

  1. 连接您的设备,并从窗口左上角选择它,现在应该显示通用 iOS 设备外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.31 - 选择设备

  1. 在左侧面板中,点击文件夹图标,然后点击Unity-iPhone设置以显示项目设置。

  2. 目标列表中,选择Unity-iPhone,然后点击签名和功能选项卡。

  3. 个人团队中:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.32 - 选择团队

  1. 如果看到一个com.XXXX.XXXX),然后点击重试,直到问题解决。一旦找到一个有效的,设置在 Unity 中(播放器设置下的包标识符)以避免在每次构建中都需要更改它。

  2. 点击窗口左上角的播放按钮,等待构建完成。在这个过程中,您可能会被提示输入密码几次,请务必这样做。

  3. 构建完成后,请记得解锁设备。会有提示要求您这样做。请注意,除非您解锁手机,否则流程将无法继续。

  4. 完成后,您可能会看到一个错误,说应用无法启动,但已经安装了。如果尝试打开它,会提示您需要信任应用的开发者,您可以通过转到设备的设置来执行。

  5. 从那里,转到通用 | 设备管理,并选择列表中的第一个开发者。

  6. 点击蓝色的信任…按钮,然后信任

  7. 尝试再次打开应用程序。

  8. 如果在实例化播放器的图像上遇到问题,请记得尝试另一张图像(在我的情况下是 Unity 标志)。这可能会有很大的变化,取决于您设备的能力。

在本节中,我们讨论了如何构建一个可以在 iOS 和 Android 上运行的 Unity 项目,从而使我们能够创建移动应用程序 - 特别是 AR 移动应用程序。与任何构建一样,我们可以遵循方法进行分析和调试,就像我们在查看 PC 构建时所看到的那样,但我们不打算在这里讨论。现在我们已经创建了我们的第一个测试项目,我们将通过向其添加一些机制将其转换为一个真正的游戏。

创建一个简单的 AR 游戏

正如我们之前讨论的,我们的想法是创建一个简单的游戏,我们可以在移动真实图像的同时移动我们的玩家,并通过点击放置一些敌人生成器,比如墙壁、地板、桌子等。我们的玩家将自动射击最近的敌人,敌人将直接射击玩家,所以我们唯一的任务就是移动玩家以避开子弹。我们将使用与本书的主要项目中使用的非常相似的脚本来实现这些游戏机制。

在本节中,我们将开发以下 AR 游戏功能:

  • 生成玩家和敌人

  • 编写玩家和敌人的行为

首先,我们将讨论如何使我们的玩家和敌人出现在应用程序中,特别是在现实世界的位置,然后我们将使它们移动并相互射击,以创建指定的游戏机制。让我们从生成开始。

生成玩家和敌人

让我们从玩家开始,因为这是最容易处理的:我们将创建一个带有我们希望玩家拥有的图形的预制体(在我的情况下,只是一个立方体),一个带有0.050.050.05Rigidbody。由于原始立方体的大小为 1 米,这意味着我的玩家将是5x5x5厘米。您的玩家预制体应如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.33 – 起始“玩家”预制体

敌人将需要更多的工作,如下所示:

  1. 创建一个名为Spawner的预制体,其中包含您希望生成器具有的图形(在我的情况下是一个圆柱体)和其真实大小。

  2. 添加一个自定义脚本,每隔几秒生成一个预制体,如下截图所示。

  3. 您将注意到使用Physics.IgnoreCollision来防止生成器与Spawner对象发生碰撞,获取两个对象的碰撞体并将它们传递给函数。您也可以使用层碰撞矩阵来防止碰撞,就像我们在本书的主要项目中所做的那样,如果您愿意的话:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.34 – 生成器脚本

  1. 创建一个带有所需图形(在我的情况下是一个胶囊体)和一个勾选了Is Kinematic复选框的Rigidbody组件的Enemy预制体。这样,敌人将移动但不受物理影响。记得考虑敌人的真实大小。

  2. 将生成器的Prefab属性设置为在所需的时间频率生成我们的敌人:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.35 – 配置生成器

  1. AR Session Origin对象中添加一个新的SpawnerPlacer自定义脚本,使用 AR 射线系统在玩家点击的地方实例化一个预制体,如下截图所示:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.36 – 放置生成器

  1. 设置SpawnerPlacer的预制体,以便生成我们之前创建的生成器预制体。

这就是第一部分的全部内容。如果您现在测试游戏,您将能够点击应用程序中检测到的平面,并看到生成器开始创建敌人。您还可以查看目标图像,看到我们的立方体玩家出现。

现在我们在场景中有了这些对象,让我们让它们做一些更有趣的事情,从敌人开始。

编写玩家和敌人的行为

敌人必须朝着玩家移动以射击他们,因此它需要访问玩家的位置。由于敌人是实例化的,我们无法将玩家引用拖到预制体上。然而,玩家也已经被实例化,所以我们可以向玩家添加一个使用单例模式的PlayerManager脚本(就像我们在管理器中所做的那样)。要做到这一点,请按照以下步骤进行:

  1. 创建一个类似于下图所示的PlayerManager脚本,并将其添加到玩家:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.37 – 创建 PlayerManager 脚本

  1. 现在敌人已经有了对玩家的引用,让我们通过添加一个LookAtPlayer脚本使它们朝向玩家,如下所示:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.38 – 创建 LookAtPlayer 脚本

  1. 此外,添加一个简单的MoveForward脚本,如下面截图中所示的脚本,使LookAtPlayer脚本使敌人面向玩家,这个沿 z 轴移动的脚本就足够了:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.39 – 创建 MoveForward 脚本

现在,我们将处理玩家的移动。记住,我们的玩家是通过移动图像来控制的,所以这里实际上是指旋转,因为玩家需要自动瞄准并射击最近的敌人。要做到这一点,请按照以下步骤进行:

  1. 创建一个Enemy脚本并将其添加到Enemy预制件中。

  2. 创建一个像下面截图中所示的EnemyManager脚本,并将其添加到场景中的一个空的EnemyManager对象中:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.40 – 创建 EnemyManager 脚本

  1. Enemy脚本中,确保在EnemyManager中注册对象,就像我们之前在本书的主项目中使用WavesManager一样:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.41 – 创建 Enemy 脚本

  1. 创建一个像下面截图中所示的LookAtNearestEnemy脚本,并将其添加到Player预制件中,使其朝向最近的敌人:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.42 – 瞄准最近的敌人

现在,我们的对象旋转和移动如预期般进行,唯一缺少的是射击和造成伤害:

  1. 创建一个像下面截图中所示的Life脚本,并将其添加到Life中,而不需要每帧检查生命是否已经降至零。我们创建了一个Damage函数来检查是否造成了伤害(执行了Damage函数),但本书项目的另一个版本也可以工作:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.43 – 创建 Life 组件

  1. 创建一个带有所需图形的Bullet预制件,带有Is Kinematic选中的Rigidbody组件的碰撞体(一个运动学触发碰撞体),以及适当的真实尺寸。

  2. MoveForward脚本添加到Bullet预制件中使其移动。记得设置速度。

  3. Spawner脚本添加到PlayerEnemy组件中,并将Bullet预制件设置为要生成的预制件,以及所需的生成频率。

  4. Bullet预制件添加一个像下面截图中所示的Damager脚本,使子弹对其触及的物体造成伤害。记得设置伤害:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.44 – 创建 Damager 脚本 – 第一部分

  1. Destroy时间添加一个像下面截图中所示的AutoDestroy脚本:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 22.45 – 创建 Damager 脚本 – 第二部分

就是这样!正如你所看到的,我们基本上使用了几乎与主游戏中使用的相同的脚本来创建了一个新的游戏,主要是因为我们设计它们是通用的(而且游戏类型几乎相同)。当然,这个项目还有很大的改进空间,但我们已经有了一个很好的基础项目,可以在此基础上创建令人惊叹的 AR 应用程序。

总结

在本章中,我们介绍了 AR Foundation Unity 框架,探讨了如何设置它,以及如何实现几个跟踪功能,以便我们可以将虚拟对象放置在现实对象之上。我们还讨论了如何构建我们的项目,使其可以在 iOS 和 Android 平台上运行,这是我们在撰写时测试我们的 AR 应用程序的唯一方法。最后,我们创建了一个简单的 AR 游戏,基于我们在主项目中创建的游戏,但修改了它,使其适用于 AR 场景的使用。

有了这些新知识,您将能够开始作为 AR 应用程序开发人员的道路,通过检测真实对象的位置,创建可以用虚拟对象增强真实对象的应用程序。这可以应用于游戏、培训应用程序和模拟。您甚至可能能够找到新的使用领域,因此利用这项新技术及其新的可能性!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值