Unity5-安卓精要-全-

Unity5 安卓精要(全)

原文:zh.annas-archive.org/md5/f55d0ecf7d416168243be63d69c588de

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

学习 Unity 5 引擎,掌握为 Android 设备设计和构建令人惊叹的现实世界游戏和应用。为你的 Android 游戏和应用设计美丽的特效、动画、物理行为和其他不同的现实世界特性和技术。优化你的项目和任何其他现实世界项目的 Android 设备。在实践中了解更多关于访问 Android 功能、渲染高端图形、使用资源包扩展你的项目,以及当然,学习在 Android 平台上的部署等知识。

Unity 本质上是一个适用于多个平台(Android、iOS、BlackBerry、Windows Phone、Windows、PlayStation、Xbox、Mac、Linux 和 Web)的开发环境。Unity 是创建 2D 和 3D 游戏及应用的非常流行且有效的技术。这项技术提供了许多有用且有效的工具来解决各种问题。Unity 渲染引擎提供了高质量图形的实时渲染,无需过多成本和努力。

Android 平台和游戏行业正以前所未有的速度发展。大多数使用 Unity 的程序员,尤其是那些刚开始接触这项技术的程序员,都希望学习如何从最流行的现实世界游戏和应用中重新创建不同的功能部分,这将引起社区极大的关注。

无论你是 Unity 5 的新手还是专家,这本书都将为你提供成功设计、实施、构建和提升你的 Android 游戏或应用质量所需的所有技能。通过按顺序完成每一章的步骤,你将迅速掌握 Unity 5 引擎的关键特性,以在实践中将现实世界的 Android 游戏和应用功能实现。

本书旨在针对想要学习为 Android 设备开发、优化和发布游戏的合格 Unity 开发者。了解更多关于 Unity 5 引擎的知识,以构建令人惊叹的现实世界 Android 游戏。

从一开始,这本书将解释 Unity 5 在 Android 平台上的设置配置。使用实际的技术和技巧以及实践中的窍门来实现创新和用户友好的功能。探索如何提升你的 Android 游戏并增强其性能。通过基于物理的着色器和全局照明,开启高质量图形渲染的奇妙世界。发现 Unity 5 引擎中的 Android 特性以及更多关于将原生 C#和 JavaScript 代码转换为 Unity 脚本的内容。

本书旨在涵盖与典型现实世界游戏和应用相关的 fundamentals,并探索概念的基本概述,但重点是提供开发 Android 游戏和应用的实用技能。

Unity 5 for Android Essentials 将教你如何使用 Unity Technologies 团队提供的不同工具来掌握你的游戏设计和开发流程。这本书将是一本实用的指南,帮助你利用 Unity 5 框架来为 Android 设备构建出色的游戏和应用。

本书涵盖内容

第一章, 设置和配置 Android 平台,解释了如何为 Android 设备配置 Unity 5。在本章中,我们还将探讨 Unity 5 中的 APK 扩展文件。在本章的最后,你将为 Android 设备构建一个非常简单和基础的游戏示例。

第二章, 访问 Android 功能,涵盖了如何在 Unity 5 中使用 Java 和 C 语言创建 Android 平台的插件。读者将实际学习如何编写简单的 Android 平台插件。此外,读者还将学习如何进行反盗版检查、检测屏幕方向、处理振动支持、确定设备型号,以及做更多有用的事情。

第三章, Android 设备的高端图形,主要探讨如何使用基于物理的着色器来提高游戏和应用程序的质量。本章还将描述 Unity 5 中的全局照明。在本章的最后,你将优化着色器代码。

第四章, Unity 5 中的动画、音频、物理和粒子系统,将涵盖 Unity 5 中的新 Mecanim 动画功能。之后,你将学习 Unity 5 中的新音频功能。在本章的最后,你将探索 Unity 5 中的物理和粒子系统。

第五章, Unity 5 Pro 中的资源包,包括 Unity 5 中资源包的概述。你将学习如何为 Android 设备实时下载新的代码和数据。在本章的最后,你将发现资源包在实际中的安全技术。

第六章, 优化和转换技术,介绍了遮挡剔除和细节级别优化技术的使用。你将学习如何优化 Unity C#和 Unity JS 代码。最后,你将了解如何将 Unity C#代码转换为 Unity JavaScript 代码,反之亦然。

第七章,故障排除和最佳实践,涵盖了为 Android 设备优化任何游戏,并教你找到任何瓶颈。在这一章中,你将发现一些针对 Android 平台的故障排除技术。在本章末尾,你将学习到许多专业人士在全世界范围内用于他们的脚本和着色器的最佳实践。

在线章节从头开始开发 Glow Hockey展示了在 Unity 5 中从头开始开发 Android 市场上最受欢迎的游戏(Glow Hockey 有大约 1 亿到 5 亿次的安装play.google.com/store/apps/details?id=com.natenai.glowhockey&hl=en)是多么容易。此外,你还将学习如何优化你的项目和任何其他现实世界项目以适应 Android 设备。本章还将涵盖更多有用的细节和功能。你可以在这个章节中找到www.packtpub.com/sites/default/files/downloads/9191OT_BonusChapter.pdf

你需要这本书的内容

你需要以下软件来使用这本书:

  • Windows 或 Mac OS X

  • Java 开发工具包(JDK)

  • Android SDK

  • Unity3D

这本书适合谁

这本书是为那些对 Unity 架构有基本了解、编程和着色以及那些是专家 Unity 开发者的人而写的。对于那些想在 Unity 中快速找到游戏或应用开发中的许多不同问题和问题的解决方案的人来说,这本书非常有帮助。

也许你已经对 Unity 5 引擎有所了解,但之前从未使用过;或者,你可能了解编程,但对使用 Unity 5 开发 Android 游戏和应用还不是很熟悉。在所有情况下,这本书将快速教会你掌握高质量的 Android 游戏和应用。这本书适合任何想要探索 Unity 各种功能并找到全球范围内广泛使用的技巧、解决方案、技巧和窍门的人。如果你对 C#或 JavaScript 有基本经验,并且对 Unity 工作流程感到足够舒适,那么这本书最适合你。

无论你是否之前开发过游戏,只要你想要在 Unity 中开始制作新的精彩现实世界游戏和应用,这本书都会帮助你。这本书适合你和你团队中的每个人,从初学者到专家开发者!

惯例

在这本书中,你会发现许多不同风格的文本,用于区分不同类型的信息。以下是一些这些风格的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“我们可以通过使用include指令来包含其他上下文。”

代码块设置如下:

using UnityEngine;

public class YourClassName: MonoBehaviour {
  void OnCollisionExit (Collision collision) {
    Debug.Log ("OnCollisionExit :" + collision.gameObject.name);
  }
}

当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:

using UnityEngine;

public class YourClassName: MonoBehaviour {
  void OnCollisionExit (Collision collision) {
    Debug.Log ("OnCollisionExit :" + collision.gameObject.name);
  }
}

任何命令行输入或输出都应如下所示:

# cp /usr/src/asterisk-addons/configs/cdr_mysql.conf.sample
 /etc/asterisk/cdr_mysql.conf

新术语重要词汇将以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“点击下一步按钮将您带到下一个屏幕。”

注意

警告或重要注意事项将以这样的框显示。

小贴士

小技巧和窍门看起来像这样。

读者反馈

读者反馈始终欢迎。告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中获得最大收益的标题非常重要。

要发送给我们一般反馈,只需发送电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书名。

如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在,您已经成为 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

勘误

尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata来报告它们,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过选择您的标题从www.packtpub.com/support查看任何现有勘误。

侵权

互联网上版权材料的侵权是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以追究补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供疑似侵权材料的链接。

我们感谢您在保护我们作者方面的帮助,以及我们为您提供有价值内容的能力。

问题和答案

如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章. 设置和配置安卓平台

本章将讨论在 Windows 和 Mac OS X 平台上安装安卓 SDK。此外,读者将了解如何为安卓设备配置 Unity 5。在本章中,我们还将探索 Unity 5 中的 APK 扩展文件。在本章中,读者将在安卓设备上构建发光曲棍球项目(我们将在本书最后一章从头开始使用 Unity 5 创建这款游戏)。在本章结束时,读者将探索 Unity Pro 和 Unity Basic 特定功能和规则的并列比较。

本章将涵盖以下主题:

  • 为安卓设备配置 Unity 5

  • Unity 5 中的 APK 扩展文件

  • 为安卓设备构建

  • Unity 许可证比较概述

为安卓设备配置 Unity 5

一旦你安装了安卓 SDK 并设置了 Unity,你必须为你的每个安卓项目配置正确的设置。我们将从考虑以下截图所示的分辨率和显示选项开始我们的审查。为了在 Unity 中访问安卓平台设置,你需要导航到编辑 | 项目设置 | 玩家菜单,然后点击带有安卓图标的按钮。另外,获取安卓平台设置的其他方法是导航到文件 | 构建设置。打开窗口后,你需要点击底部的玩家设置按钮。

默认方向选项在多个移动平台之间共享。此设置是必要的,以便指示为你的游戏或应用程序设计的屏幕方向。所有设置的默认值都是自动旋转。例如,如果你的项目仅设计为屏幕的纵向方向,那么你需要选择纵向纵向颠倒的值:

为安卓设备配置 Unity 5

我们可以选择以下属性之一:纵向纵向颠倒横向右横向左自动旋转。它们相当简单,不言自明。你只需稍微玩一下它们,就能了解它们的真正用途。隐藏状态栏复选框无需解释,因为其含义很明显。

我们将要调查的下一个选项被称为使用 32 位显示缓冲区。你可以决定是否让显示缓冲区处理 16 位颜色值(如果没有启用 32 位),或者是否处理 32 位颜色值。记住,你只需要在出现某些碎片时激活此设置,因为它会极大地影响性能。显示加载指示器字段确保以下行为:不显示反转大反转小

为安卓设备配置 Unity 5

如前图所示,有很多设置;然而,除了在构建前必须调整的少数几个之外,大多数都可以使用默认值。您不配置包标识符选项就无法为您的 Android 设备构建 APK 文件,该选项在移动平台之间共享。包标识符字符串必须与您构建的游戏的配置文件相匹配。标识符的基本结构是com.公司.产品名称。包版本负责保存描述发布索引的数字。此外,Unity 允许我们指定您的 Android 项目将支持的最低 API 级别。您还可以设置应用程序的名称和图标。其他设置很明显,不需要额外解释。更详细的信息可以在 Unity 的官方文档中找到。

Unity 5 中的 APK 扩展文件

Google Play 要求您的游戏和应用程序的大小不超过 50 MB。对于大多数应用程序和游戏来说,这个大小已经足够了。或者,您可能希望为您的项目提供出色的图形和其他占用大量空间的媒体文件。通过扩展 APK 文件,Google Play 使开发者的生活变得更加简单和容易。扩展文件存储在设备的一个共享文件夹存储中,您的游戏有足够的访问权限。

概述

每个文件的大小不能超过 2 GB,您可以为它选择任何格式。当然,最好的方式是在下载过程中仅使用压缩文件,以节省带宽。您可以为您的 APK 添加一个或两个扩展文件。每个扩展文件背后都隐藏着它的意义:

  • 第一个扩展文件被称为主文件,应该用于游戏中需要的额外资源。这就是为什么这个扩展文件是主要的。

  • 第二个扩展文件被称为补丁,用于更新主文件。这就是为什么它是可选的。

您应该知道,开发者控制台不允许您仅通过上传新的扩展文件来更新现有的 APK 文件。

格式

对于您的扩展文件,您可以使用任何所需的格式,例如 MP3、MP4、AVI、RAR、ZIP、DOC 和 PDF。JOBB 工具可以帮助您封装和加密您的资源和补丁。

更新过程

在大多数情况下,Google Play 会自动为您完成所有工作。因此,在用户在他们的设备上下载或上传您的扩展文件时,您通常不需要做任何事情。然而,有时您的游戏必须通过从 Google Play 的应用程序许可服务接收 URL 来自己下载这些文件。

下载游戏扩展文件的基本步骤如下:

  1. 在游戏启动事件中,您应该在Android/obb/<package-name>/目录中查找扩展文件。

  2. 在第一步中,如果您发现您的扩展文件已经在该目录中,那么您可以继续玩游戏。

  3. 如果扩展文件不在该目录中,您应执行以下两个步骤。

  4. 您必须接收游戏扩展文件的 URL、名称和大小。在下载任何内容之前,您应该知道下载的位置和内容。

  5. 在获取所有必要的下载扩展文件的信息后,您可以获取您的文件并将它们放入与 Google Play 告诉您相同的名称的 Android/obb/<package-name>/ 目录中。

注意

以下列出的注意事项是从官方 Android 开发者文档页面 developer.android.com/google/play/expansion-files.html 中摘录的:

  • Google Play 为您的扩展文件提供的 URL 对每个下载都是唯一的,并且每个 URL 在提供给您的应用后不久就会过期。

  • 无论您的应用是否免费,Google Play 只有在用户从 Google Play 购买您的应用时才会返回扩展文件 URL。

  • 在请求和下载过程中可能会发生各种错误,您必须优雅地处理这些错误。

  • 网络连接性在下载过程中可能会改变,因此您应该处理此类变化。如果中断,在可能的情况下继续下载。

  • 在后台下载时,您应提供一个指示下载进度的通知,通知用户已完成,并在选择时将用户带回到您的应用。

在 Unity 5 中设置扩展文件

导航到 Player Settings | Publishing Settings 菜单,在底部您将看到一个名为 Split Application Binary 的选项。当此选项启用时,您的项目将分为代码的 .apk 文件,以及所有其他资源和数据的 .obb 文件。

让我们看看与扩展文件(.obb)加载相关的关键方面的列表:

  • 扩展文件不需要上传到 Google Play 服务器。

  • 如果您已决定在 Google Play 上发布 .apk.obb 文件,那么您需要包含下载扩展文件的代码。

  • 在 Unity Asset Store 中,您可以找到一个用于在正确位置下载和提取扩展文件的优秀插件。此插件的网址为 u3d.as/content/unity-technologies/google-play-obb-downloader/2Qq

  • 在测试 .obb 文件之前,您需要登录您的 Google 账户。

为 Android 设备构建

在 Unity 中创建新项目后,调整全局质量设置是一个好主意,如下所示图所示。其中大部分会影响您的游戏性能。让我们更深入地了解 QualitySettings,因为它在为 Android 设备构建应用程序之前是必须的:

为 Android 设备构建

Unity 允许您为您的质量设置创建一个模板;您还可以选择 Unity 默认提供的模板之一。这些设置极大地影响了您应用程序的性能和图形质量。对于资源非常有限的移动平台来说,这尤其重要。您需要在自己的目标平台上调整设置,以找到最适合您质量和性能的最佳模板设置。要访问这些设置,您需要导航到编辑 | 项目设置 | 质量。您可以为 Unity 支持的所有平台分别选择不同的模板。此设置窗口分为两个主要部分。上半部分,如前一张图所示,用于管理模板,而下半部分,如以下图所示,负责实际的设置。

每个模式(甚至 Unity 内置模板)都可以按照您的意愿命名。对于 Unity 支持的所有平台,您可以选择几个可访问的设置模板,以及一个默认模板。默认设置模板以绿色突出显示。您的设置应尽可能简单,尤其是对于移动平台。Unity 允许您通过点击带有篮子图标的图标来创建新的设置模板和删除它们。

为 Android 设备构建

我们打算介绍的第一部分是渲染,如前一张截图所示。它包含影响正向渲染模式下仅限于像素光计数选项。正向渲染路径通过一个或多个遍历渲染每个对象,具体取决于影响该对象的光源。光源本身也根据其设置和强度被正向渲染以不同的方式处理。

渲染部分包含纹理质量属性,有四个现有选项:全分辨率半分辨率四分之一分辨率八分之一分辨率。这让你可以选择是否以最大分辨率或其分数(较低分辨率具有更少的处理开销)显示纹理。始终记住,在任何项目中,你需要在这两个特征之间找到黄金平衡:质量和性能。下一个属性名为各向异性纹理,它允许你选择三个值:禁用按纹理强制开启。这描述了各向异性纹理将被如何使用。在维基百科(en.wikipedia.org/wiki/Anisotropic_filtering)上,我们可以读到关于各向异性过滤的以下内容:“在 3D 计算机图形学中,各向异性过滤(缩写为AF)是一种增强计算机图形学表面纹理图像质量的方法,这些表面相对于相机以斜角观看,其中纹理的投影(而不是渲染在它上面的多边形或其他原语)看起来是非正交的(因此这个词的起源:“an”表示不是,“iso”表示相同,“tropic”来自 tropism,与方向相关;各向异性过滤不会在各个方向上过滤相同)”。

你接下来要学习的属性是抗锯齿。可以通过选择禁用选项来关闭抗锯齿,或者通过选择2x4x8x 多采样选项来开启它。下一个设置只是切换粒子的软混合,其名称为软粒子。这是渲染部分的最后一个选项。

下一个部分是阴影,其名称完全描述了其功能和目的。我们可以从三个开放值中选择一个:硬阴影和软阴影仅硬阴影禁用阴影。如果你选择阴影分辨率选项的最高分辨率,可能会产生很大的处理开销。可能的设置如下:非常高。在阴影投影选项中,有两个独立的程序用于预测来自方向光线的阴影。如果我们选择紧密拟合,那么它将渲染更高分辨率的阴影,如果相机移动,有时可能会略微晃动。下一个选项,也是阴影投影的最后一个选项,是稳定拟合值,它是紧密拟合的对立面。这意味着稳定拟合会渲染较低分辨率的阴影,但在相机移动时不会出现任何伪影。接下来是阴影级联设置,它会影响处理开销。更高的级联循环可以处理更多的处理开销。不要忘记黄金平衡。级联的可获得选项如下:无级联两级级联四级级联

docs.unity3d.com/460/Documentation/Manual/DirectionalShadowDetails.html

注意

在移动平台上,方向光的真实阴影总是使用一个阴影级联,并且是硬阴影

方向光通常在户外游戏中用作主光——阳光或月光。观看距离可以非常大,尤其是在第一人称和第三人称游戏中,阴影通常需要调整以获得最佳的质量与性能平衡。

阴影距离的值决定了我们能看到阴影的距离。超过这个长度的阴影是可见的,其他的则不可见。

在以下文本中,我们将探讨所谓的其他部分。它包含五个选项,用于调整任何项目。让我们从第一个选项——混合权重开始。在这个设置中,我们可以选择三个非常重要的值,这些值对性能有很大影响。值越低,性能越好。这个设置表示在动画过程中可以影响给定顶点的骨骼数量。我们可以选择1 根骨骼2 根骨骼4 根骨骼;不多也不少。下一个特性对性能影响很大,但由于其几乎不可见的伪影,它不是关于质量的首要问题。这个设置的名称是垂直同步计数,而这个伪影被称为撕裂。如果我们想避免这种伪影,就需要将渲染与显示设备的刷新率同步,但不要忘记性能。同步可能会大幅降低性能,因此你应该准备好应对这种情况。对于垂直同步计数参数,只有三个现有的选项:第一个选项是与每个垂直空白VBlank)同步,第二个值是与每第二个垂直空白同步,第三个选项允许我们禁用所有同步,从而加快应用程序的运行速度。我们研究的下一个设置是LOD 偏差。这个值只在 Unity 需要决定选择哪个 LOD 级别时才会发挥作用。例如,当有两个 LOD 级别可供选择时,LOD 偏差通过只选择一个值来提供帮助。这个值在零到一之间设置,作为一个分数。越接近零,选择的级别就越不详细,反之亦然。现在还有两个剩余的选项我们将考虑。第一个是最大 LOD 级别,它的目的是记住你可以在项目中使用的最高 LOD 级别的数字。第二个是粒子射线投射预算,它需要与质量级别的粒子系统碰撞,这个数字描述了物理近似中射线投射的最高值。

至于最大 LOD 级别参数,所有值将小于此数字的模型将不会包含在构建中;Unity 将忽略它们,这可以显著减少你的应用程序或游戏消耗的内存量。此参数的初始默认值为零,这意味着无论模型的细节程度如何,每个模型都将包含在你的构建中。对于每个平台,根据其配置,Unity 将使用可能的最小 LOD 级别。

如果 Android SDK 安装和 Unity 设置成功,你可以安全地创建项目的构建版本。为此,你需要导航到文件 | 构建设置,在打开的窗口中,你可以创建如图所示的各个支持平台的构建版本。如果你已正确安装 Android SDK 并配置了 Android,且在 Unity 中设置了质量和玩家设置,你可以安全地点击窗口右下角的构建按钮或构建并运行按钮(如果你的 Android 设备已正确配置并通过 USB 连接),以同时导出 .apk 文件并将项目部署到连接的设备上。

现在,是时候构建 Glow Hockey(我们将在本书最后一章从头开始使用 Unity 5 创建这个游戏)。首先,你应在 Unity 编辑器中创建一个新项目。你可以按自己的意愿命名。要创建新项目,你应该点击窗口右下角的创建项目按钮。

创建新项目后,将显示 Unity 编辑器。

Glow Hockey 是 Unity 支持的各个平台上项目部署的一个很好的例子。在这个游戏中,有许多不同的效果、动画、音效、物理效果以及许多其他来自 Unity 的方面。在 Android 平台上部署此项目后,你可以测试 Unity 支持的各种功能。

打开主Glow Hockey场景后,你可以按自己的意愿进行任何更改或进行实验。然而,在本章中,我们的目标只是将这个游戏构建在 Android 设备上。我们不会在本章中对项目进行任何更改。

为 Android 设备构建

打开构建设置窗口后,你应该选择Android平台,然后你可以通过点击窗口右下角的构建按钮来创建一个 .apk 文件,以便与你的朋友分享,例如。此外,你可以按构建并运行按钮来导出 .apk 文件,就像第一种情况一样,并且同时通过 USB 线缆将此项目部署到连接的设备上。

现在,让我们在下一节中了解更多关于 Unity 许可证比较的信息。

为 Android 设备构建

Unity 许可证比较概述

本节基于 Unity 许可证比较概述。链接unity3d.com/unity/licenses将向您展示 Unity Pro 和 Unity Basic 的具体功能和规则的并列比较。

导航网格、寻路和人群模拟

寻路是 Unity 的内置功能。该系统允许您轻松地从起点找到正确的路径,避免在路径上遇到的障碍。在使用此功能之前,您必须先在 Unity 编辑器中烘焙导航数据。在那里,您必须指定哪些楼层对象或地面可以行走,哪些对象是障碍物——所有其他问题在调用带有起点和终点参数的寻路函数后,Unity 将无需任何努力解决。如果您有强烈的愿望,您可以创建自己的寻路系统和人群模拟。Unity 中的寻路系统适用于基本和 Pro 许可证。

细节级别(LOD)支持

细节级别LOD)允许您通过为您的网格提供几个不同级别的质量来非常有效地优化您的生产力。每个级别都会根据与摄像机的距离在摄像机视图中显示。也就是说,当摄像机距离太远时,显示最复杂和详细的网格并不理想,因为网格上所有这些细节的力量将完全不可见,这浪费了宝贵的资源。这对整体性能来说并不好。只有当摄像机足够近,您可以看到网格上的所有这些细节时,才应该显示细节网格。LOD 仅由 Unity Pro 许可证支持。如果您只有 Unity Basic 许可证,那么您可以非常容易地创建自己的 LOD 系统。优化思想可以很容易地由您自己实现。关键是根据网格与摄像机的距离实时(或多或少详细)更改网格以进行渲染,这反过来又会减少硬件的不必要成本。

音频过滤器

音频过滤器允许你在实时中通过编程创建不同的声音效果。想象一下这样的场景,在游戏中,当你的角色在沙地上行走时,你必须播放一个声音。然而,如果玩家突然进入隧道,那么声音应该与在沙地上行走的声音不同。为了解决这个问题,你可以选择可能的场景之一。解决这个问题的第一个方案在于,你可以创建或已经使用了一个适用于隧道行走等场景的现成声音。对于每种情况,你都会有各种现成的声音。或者,如果你在游戏中有很多不同的情况需要播放不同的声音,这种方法会消耗大量内存。与 Unity 软件提供的实时音频过滤器提供的第二种解决方案相比,这种方法不够灵活。音频过滤器仅由 Unity Pro 许可证支持。

视频播放和流式传输

现在,许多应用程序和游戏需要播放不同的视频。视频可能占用大量内存,这对于移动设备来说尤其是一个严重的问题。为了减少视频内容的额外内存成本,Unity 允许你通过互联网视频流进行广播。此功能仅适用于 Unity Pro 许可证。

使用资源包的完整流式传输

资源包仅由 Unity Pro 许可证支持。此功能极大地帮助优化了创建高质量游戏或应用程序的方式。此功能允许开发者通过互联网流式传输内容,例如,向游戏中添加新角色、新建筑、新武器、新纹理等等。

十万美元的营业额

此项并非 Unity 的功能,而应被视为 Unity 的条件或要求。该条件指出,如果在上一财年,你(个人)或你的组织,收入超过 10 万美元(含),那么你必须使用 Unity Pro 许可证;也就是说,你完全没有权利使用 Unity Basic 许可证。这可以被认为是 Unity 相当合理且逻辑的条件。毕竟,如果你或你的组织收入超过 10 万美元(或正好 10 万美元),那么你或你的组织可以毫无问题地购买 Unity Pro 许可证。

Mecanim – IK Rigs

Unity 中的新动画系统,被称为 Mecanim,允许你使用各种不同和有用的功能,但一个特殊的机会和关键特性是逆运动学IK)绑定。Mecanim 仅支持正确配置了 Avatar 的人形角色使用 IK 绑定。这个特性的意义在于你可以调用一个函数,并传递一个最终点,腿应该放置的位置以击中球(例如,如果你正在创建足球游戏);之后,IK 绑定系统会为你自动完成所有剩余的工作。例如,你的角色必须拿起桌子上的杯子,但在那之前他需要从椅子上站起来,走到放有所需杯子的桌子上,只有在那之后,你的角色才能用手拿起杯子。这些动作将播放动画。所有这些艰苦的工作都将完全依赖于 IK 绑定系统,你只需要指定终点。IK 绑定仅由 Unity Pro 许可证支持。

Mecanim – 同步层和额外曲线

Mecanim 还允许你同时使用不同的动画状态;例如,一个健康满值的角色会正常行走,但每次健康值下降约 20%,角色就会开始变得糟糕并走得更慢,然后开始跛行。当角色的健康变得非常糟糕时,他开始在地上爬行。这种方法使用同步层选项来分组不同的动画状态。这极大地简化了通过同步层的可重用性在不同情况下创建各种条件的过程。

在实时中动态修改同步层是可能的,这样你可以多次使用你的状态机,配合不同的动画,但条件保持不变。因此,开发者不需要为所有动画创建许多不同的状态机,而只需创建几个,并在播放不同动画时重复使用它们。这个功能仅由 Unity Pro 许可证支持。

额外曲线允许你在动画中添加新的曲线,以控制不同的动画参数。在 Unity 编辑器中管理你的动画曲线既简单又非常方便。这个功能仅由 Unity Pro 许可证支持。

自定义启动画面

这个功能有以下含义:在使用 Unity 基本许可证时,每次你的应用程序启动,你的用户都会看到 Unity 标志图像。如果你想用你的图像替换那个标志,那么你需要购买一个 Unity Pro 许可证。

构建大小精简

这是一个非常重要的 Unity 功能,特别是对于移动设备。使用此功能,Unity 允许您从构建中移除所有多余的内容。Unity 在这一点上极大地帮助了您,因为它只包含您游戏中使用的资产在最终构建中。此外,此功能还允许您在最终构建中仅包含 Unity 引擎中用于您游戏的那些部分。此功能仅由 Unity Pro 许可证支持。

带全局光照和区域光照的光照贴图

所有 Unity 许可证都支持光照贴图。Unity 允许您为静态对象烘焙光照和阴影。通过添加此功能提供的全局光照和区域光照,您可以增加游戏的现实感,但此功能仅由 Unity Pro 许可证支持。

HDR 和色调映射

高动态范围HDR)和色调映射功能对于提高游戏中图像质量非常有用,但它需要大量的资源投入。您必须非常小心地使用这种昂贵的操作,以及 Unity 中的许多其他昂贵功能。此功能允许您使用比通常更多的颜色,例如,您可以在房间中创建晨光。此功能仅由 Unity Pro 许可证支持。

遮挡剔除

此功能对于优化非常有用。Unity 会排除所有不必要的对象进行渲染,例如那些在墙后或远离摄像机的对象。否则,隐藏的对象将浪费处理器时间和内存。您可以轻松地为特定任务创建具有相同想法的系统。此功能仅由 Unity Pro 许可证支持。

光探针

此功能用于补充光照贴图优化方法或所谓的光照烘焙,后者仅用于静态对象,而动态对象看起来则差得多。光探针解决了动态对象的问题,但它们必须非常小心和温和地使用,以免损害您应用程序或游戏的性能。此功能仅由 Unity Pro 许可证支持。

静态批处理

此功能可以通过减少静态对象的大量绘制调用来优化您游戏场景中的渲染过程。此功能允许我们减少许多不必要的绘制调用。它仅适用于静态对象,且仅由 Unity Pro 许可证支持。

渲染到纹理效果

此 Unity 功能非常有趣且经常很有用。当您想直接将相机渲染到图像而不是屏幕上时,此功能非常有用。之后,您可以对该图像进行任何操作;例如,您可以在游戏中创建一个电视盒子。此外,您还可以使用该图像执行后期处理效果,等等。然而,此功能非常昂贵,因此请谨慎使用。此功能仅由 Unity Pro 许可证支持。

全屏后期处理效果

此功能还可以创建非常有趣的效果。另一方面,此功能应非常谨慎地使用,尤其是在移动平台上,因为它可能需要大量的资源来执行。在优化时,您不应忘记其高昂的成本。例如,您可以为 Formula 1 游戏(汽车以非常高的速度行驶)创建运动模糊效果。您还可以使用此功能创建光晕效果,使物体像霓虹灯一样发光。此功能仅由 Unity Pro 许可证支持。

在使用寻路系统寻找正确路径的过程中,可能会有动态障碍物,您的角色应该避开。您可以在代码中将对象编程设置为一定时间的障碍物。管理优先级的能力会影响寻找正确路径。此功能仅由 Unity Pro 许可证支持。

.NET 套接字支持

使用 .NET 套接字的能力使您能够创建各种网络游戏,以及直接连接到设备而无需服务器。此功能由 Unity Basic 和 Unity Pro 许可证支持。

分析器和 GPU 分析

这对于分析您的项目非常有用。优化应从查找应用程序或游戏中的瓶颈开始。为了更有效,在搜索项目中的瓶颈时,您应该拥有良好的工具。您可以自己创建这样的工具,或者可以使用现成的解决方案。Unity 提供的解决方案之一是分析器工具。此功能仅由 Unity Pro 许可证支持。只有 Unity Basic 许可证的用户必须自己创建分析器工具。这就是为什么在本书的结尾,我们将开发一个非常简单的代码分析器工具。

实时方向阴影

灯光和阴影是大多数游戏中的关键方面。世界各地的许多开发者都在尝试实现最逼真的灯光来创建他们的游戏。在具有逼真灯光的世界中没有阴影比有阴影还要糟糕。另一方面,要创建这样一个逼真的世界需要大量的资源,例如时间和内存。这对于移动设备尤为重要。您需要在质量和性能之间找到平衡。此功能由 Unity Basic 和 Unity Pro 许可证支持。

脚本访问资产管道

此功能也非常有用。使用此功能,您可以自动化大量资产或构建的处理。例如,想象一下您需要在每个纹理上添加水印。如果只有几个纹理,那么可以手动完成,但如果有很多纹理,比如几百个,那么自动化处理每个图片将非常有用。有关更多信息,您可以查看 Unity 的官方文档。Unity 提供了各种方便处理您的资产和构建的功能。此功能仅由 Unity Pro 许可证支持。

摘要

在本章中,我们探讨了如何在 Windows 和 Mac OS X 上安装 Android SDK。在制作第一个构建之前,我们还介绍了各种 Unity 设置。之后,我们查看了 Unity 中为 Android 设备的 APK 扩展文件。然后,我们讨论了 Android 的构建设置。我们为 Android 平台创建了一个非常简单且小巧的游戏构建。在本章的最后,我们考虑了逐步关键点和 Unity Pro 与 Unity Basic 许可证的差异。

下一章包含了许多关于 Android 平台的有趣细节。你将学习如何在 Unity 中为 Android 平台创建插件。你还将了解到如何进行反盗版检查、检测屏幕方向、处理振动支持、确定设备代系以及许多其他有用的事情。让我们继续前进!

第二章 访问 Android 功能

在本章中,你将了解如何在 Unity 5 中使用 Java 和 C 语言创建 Android 平台的插件。你将实际学习如何编写简单的 Android 平台插件。此外,读者还将学习如何进行反盗版检查、检测屏幕方向、处理振动支持、确定设备型号以及其他更多有用的功能。本章将涵盖以下主题:

  • 为 Android 平台创建 Java 和原生 C 插件

  • Unity 5 中的 Android 脚本 API

  • 在 Unity 5 中访问 Android 传感器和功能

为 Android 平台创建 Java 和原生 C 插件

在 Unity 中为 Android 平台创建 Java 或原生 C 插件之前,你应该安装 Android NDK。如果你不知道如何构建共享库,那么你应该了解更多关于这个流程的信息。关于 Android NDK 的信息可以在网上找到,例如,Android 官方文档可以在 developer.android.com/tools/sdk/ndk/index.html 找到,或者在 Packt Publishing 的许多不同书籍中找到;例如,可以访问 www.packtpub.com/application-development/android-ndk-beginner's-guide。关于 Android NDK 的信息超出了本书的范围。

你的游戏或应用的一些部分可以使用原生代码语言实现,例如 CC++Android NDK 是一个具有多个功能和可能性的工具集。你不需要在每一个项目中都使用 Android NDK,但在某些游戏或应用中,重用一些自定义或第三方代码库(例如 CC++)将非常有帮助。还有更多可能的用例。

注意

在使用 Android NDK 之前,你应该记住这种方法并不总是必要的,而且几乎总是会增加代码的复杂度。

使用 C 语言创建插件

让我们探索一个非常简单且基本的插件示例,该示例是用 C 编程语言编写的,如下面的代码所示:

extern "C" {
  float Unity5AndroidPluginNativeC() {
    return 5.5f;
  }
}

在将我们的简单插件示例构建为共享库之后,你应该将其放入 Assets/Plugins/Android 目录。现在,让我们看看如何像下面这样在 Unity C# 脚本中使用我们的原生 C 插件:

[DLLImport ("NameOfYourPlugin")]
private static extern float Unity5AndroidPluginNativeC();

注意

注意,你无法在插件名称中指定库扩展名,例如 .lib.so。此外,你应该用 Unity C# 代码包裹整个原生 C 代码,以便检查你的应用程序正在运行的平台以及你是否可以使用这个原生 C 插件。

你有机会在 Unity 中使用预编译的 Android 库。

让我们看看如何使用 Java 编程语言为我们的 Android 游戏和应用程序赋予 Unity 5 的能力。为了在 Unity 中使用此高级功能,你必须将 Java 代码导出为 JAR 文件。并非每个项目都需要高级功能,但这项知识仍然会对你很有用。

注意

Android 插件的 Unity 库位于 WindowsC:\Program Files\Unity\Editor\Data\PlaybackEngines\androidplayer\development\bin\classes.jarC:\Program Files\Unity\Editor\Data\PlaybackEngines\androidplayer\release\bin\classes.jar

Android 插件的 Unity 库位于 Mac OS XUnity/Contents/PlaybackEngines/AndroidPlayer/development/bin/classes.jarUnity/Contents/PlaybackEngines/AndroidPlayer/release/bin/classes.jar

在 Java (Eclipse IDE) 中创建插件

接下来,让我们看看如何在 Eclipse IDE 中使用 Java 编程语言创建我们的自定义插件。你也可以选择任何其他你感到舒适的 IDE。首先,你需要创建一个新项目,如图所示:

在 Java (Eclipse IDE) 中创建插件

一旦选择 Android 应用程序项目,你应该点击窗口底部的下一步 >按钮。之后,你将看到如图所示的窗口:

在 Java (Eclipse IDE) 中创建插件

你应该设置应用程序名称项目名称包名称的值,根据你的意愿或如前图所示,用于我们的简单插件示例。此外,你还可以设置其他设置,如最小所需 SDK目标 SDK编译方式主题。之后,点击窗口底部的下一步 >按钮,你将看到以下窗口:

在 Java (Eclipse IDE) 中创建插件

你可以使用前图所示的设置。打开此窗口后,你应该点击窗口底部的下一步 >按钮,之后将打开以下窗口:

在 Java (Eclipse IDE) 中创建插件

你可以使用这里显示的默认设置。此外,你可以在窗口底部点击下一步 >按钮,之后你将看到以下窗口:

在 Java (Eclipse IDE) 中创建插件

在这里,你应该根据你的意愿或如早期截图所示设置活动名称布局名称导航类型字段。然后,你可以点击底部右角的完成按钮,如图所示。

接下来,让我们将 Unity 的 classes.jar 库复制到如图所示的 libs 文件夹中:

在 Java (Eclipse IDE) 中创建插件

现在,打开前图所示的 UnityFlashlightActivity.java 文件。此文件是由 Android 开发工具ADT,Eclipse IDE 的插件)自动生成的。

在插件中编写 Java 代码

现在是时候更改你的UnityFlashlightActivity代码,以便你可以在 Unity 脚本中使用这个 Android 功能。为此,你应该从UnityPlayerActivity继承UnityFlashlightActivity类,而不是从 Android SDK 提供的简单活动继承。新的代码如下所示:

package com.packtpub.unityflashlight;
import com.unity3d.player.UnityPlayerActivity;
import android.os.Bundle;

public class UnityFlashlightActivity extends UnityPlayerActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
}

下一步是创建新的Flashlight.java类,如下所示:

在插件中编写 Java 代码

首先,我们应该声明我们的包名和需要导入的类,如下代码所示:

package com.packtpub.unityflashlight;

import com.unity3d.player.UnityPlayerActivity;
import android.content.pm.PackageManager;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;

然后,让我们声明我们的Flashlight类及其变量,如下所示:

public class Flashlight {
public UnityPlayerActivity unityPlayerActivity;
   public boolean isActiveFlashlight;

private Camera _cameraHardware;

cameraHardware变量是 Android SDK 提供的Camera类的对象,用于使用硬件摄像头的不同功能。现在,是时候为这个类编写一个构造函数,如下所示:

public Flashlight(UnityPlayerActivity upa) {
  // Unity Player Activity
unityPlayerActivity = upa;

  // Is Flashlight turned ON or OFF on the device
isActiveFlashlight = false;

// Receiving back hardware camera     
_cameraHardware = Camera.open();
}

我们将使用unityPlayerActivity变量来访问 Android 上下文和由 Android SDK 提供的PackageManager类。我们还将使用isActiveFlashlight变量来打开和关闭设备的闪光灯。最后一个变量_cameraHardware将用于访问设备上的闪光灯。现在,是时候编写一个函数来检查 Android 设备是否具有闪光灯功能。函数代码如下所示:

public boolean HardwareHasFlashlight() { 
       return (
unityPlayerActivity.
getPackageManager().
hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)
       );
}

下一步是描述一个函数,该函数将在 Android 设备上打开闪光灯,如果它具有这个硬件功能:

public void ActivateFlashlight() {
if(HardwareHasFlashlight()) {
          isActiveFlashlight = true;

           _cameraHardware = Camera.open();     

           Parameters params = _cameraHardware.getParameters();

           params.setFlashMode(Parameters.FLASH_MODE_TORCH);

           _cameraHardware.setParameters(params);

      // Turn ON a flashlight
           _cameraHardware.startPreview();
       }
}

打开和关闭硬件闪光灯

现在,让我们编写一个函数来关闭 Android 设备上的闪光灯,如果它具有这个硬件功能:

public void DeactivateFlashlight() {
if(HardwareHasFlashlight()) {
       isActiveFlashlight = false;

    // Turn OFF a flashlight
        _cameraHardware.stopPreview();

        _cameraHardware.release();
}
}

这个类中最简单的方法如下所示。接下来,我们应该使用以下代码关闭我们的Flashlight类:

public boolean IsActiveFlashlight() {
       return isActiveFlashlight;
}
}

注意

如果你收到unityPlayerActivity.getPackageManager()函数的错误,那么你需要将AndroidManifest.xml文件中的最小 Android SDK 版本更改为如下代码行所示:

<uses-sdk android:minSdkVersion="9"/>

现在,让我们稍微修改一下我们的 UnityFlashlightActivity,如下所示:

package com.packtpub.unityflashlight;

import com.unity3d.player.UnityPlayerActivity;

import android.os.Bundle;

public class UnityFlashlightActivity extends UnityPlayerActivity {
  public Flashlight flashlight = new Flashlight(this);

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
}

在 Unity 侧

在这里,我们将在项目中为我们的 Android 插件测试创建一个新场景:

在 Unity 侧

按照你的意愿命名这个新场景,然后你应该在项目中创建一个新的Assets/Plugins/Android文件夹。你可以在该文件夹中放入AndroidManifest.xml、JAR 文件和 Android 资源文件。

从 Eclipse 导出和导入 JAR 库到 Unity

现在,让我们回到 Eclipse 编辑器,在Flashlight项目上鼠标右键单击。点击导出…按钮,如下所示:

从 Eclipse 导出和导入 JAR 库到 Unity

你将看到如下截图所示的窗口。选择JAR 文件,然后点击窗口底部的Next >按钮。

从 Eclipse 导出和导入 JAR 库到 Unity

在以下窗口中,选择您希望选择的选项,或者只需像下面这样设置它们:

从 Eclipse 导入和导出 JAR 库到 Unity

选择要作为我们新 Asset/Plugins/Android 文件夹目标的 JAR 文件路径。

导入 AndroidManifest

下一步是在 Asset/Plugins/Android 文件夹中创建一个新的 AndroidManifest.xml 文件。清单声明如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    package="com.packtpub.unityflashlight"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="9"/>

    <application
        android:icon="@drawable/app_icon"
        android:label="@string/app_name">
        <activity
            android:name="com.packtpub.unityflashlight.UnityFlashlightActivity"
            android:configChanges = "keyboardHidden|orientation"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

  <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />

</manifest>

在 Unity 脚本中使用 Java 插件

在这里,我们将创建一个非常简单的 FlashlightActivity.cs 脚本,如下所示:

using UnityEngine;

public static class FlashlightActivity
{
  #if UNITY_ANDROID && !UNITY_EDITOR
    public static AndroidJavaClass activityClass = new AndroidJavaClass("com.packtpub.unityflashlight.UnityFlashlightActivity");
    public static AndroidJavaClass unityActivityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
    public static AndroidJavaObject activityObj = unityActivityClass.GetStatic<AndroidJavaObject>("currentActivity");
  #else
    public static AndroidJavaClass activityClass;
    public static AndroidJavaClass unityActivityClass;
    public static AndroidJavaObject activityObj;
  #endif
}

然后,我们将按照以下所示实现 Flashlight.cs

using UnityEngine;

public static class Flashlight
{
  #if UNITY_ANDROID && !UNITY_EDITOR
    private static AndroidJavaObject flashlight = FlashlightActivity.activityObj.Get<AndroidJavaObject>("flashlight");
  #else
    private static AndroidJavaObject flashlight;
  #endif

  public static bool HardwareHasFlashlight()
  {
    if (Application.platform == RuntimePlatform.Android)
      return flashlight.Call<bool>("HardwareHasFlashlight");
    else
      return false;
  }

  public static bool IsActiveFlashlight()
  {
    if (Application.platform == RuntimePlatform.Android)
      return flashlight.Call<bool>("IsActiveFlashlight");
    else
      return false;
  }

  public static void ActivateFlashlight()
  {
    if (Application.platform == RuntimePlatform.Android)
      flashlight.Call("ActivateFlashlight");
  }

  public static void DeactivateFlashlight()
  {
    if (Application.platform == RuntimePlatform.Android)
      flashlight.Call("DeactivateFlashlight");
  }
}

这个类非常简单,我们不会对其进行解释。这个类只是调用 Android Java 方法。最后,让我们再创建一个非常简单的类,它应该附加在我们新场景的 MainCamera 上。文件名为 FlashlightTest.cs,其代码如下所示:

using UnityEngine;

public class FlashlightTest : MonoBehaviour {
  void Start () {
    Flashlight.ActivateFlashlight();
  }

  void OnApplicationQuit() {
    Flashlight.DeactivateFlashlight();
  }
}

最后,我们可以为 Android 设备创建一个新的构建,只需一个场景,如下截图所示:

在 Unity 脚本中使用 Java 插件

我们可以访问许多不同的 Android 属性,这些属性在 Unity 5 中可供使用。您应该使用 Unity 定义的 UNITY_ANDROID 常量来有条件地编译 Android 特定的 C# 代码。此外,在本章中,您将学习如何在 Unity 脚本中使用不同的 Android 功能和属性。Unity 中的大多数 Android 功能都由 HandheldInput 类提供。让我们看看您将要探索的 Android 功能:

  • 反盗版检查

  • 震动

  • 活动指示器

  • 屏幕方向

  • 系统信息

反盗版检查

让我们先探索 反盗版 检查。非常常见,如果不是总是如此,您应该保护您的 Android 游戏或应用程序免受黑客的侵害,他们破解游戏和应用程序以免费重新分发。借助 Unity 的帮助,我们可以进行反盗版检查,以显示我们的游戏或应用程序在构建后是否已被更改。

您应该检查 Unity 库提供的 Application.genuine 布尔属性。如果此属性为 false,则可以通知用户这是一个破解版本,或者您可以删除一些功能,也可以执行任何其他操作或它们的组合。Application.genuine 检查是一个非常昂贵的操作,因此最好不要频繁检查此属性,而只是在需要时检查。

震动

为了使 Android 设备震动,您应该调用 Unity 库提供的 Handheld.Vibrate() 方法。

活动指示器

您可以使用 活动指示器 来处理慢速操作。Android 内置了一个活动指示器。让我们探索一个非常简单的代码示例,如下所示:

using UnityEngine;
using System.Collections;

public class ShowActivityIndicator : MonoBehaviour {
  IEnumerator ActivityIndicatorExample()
  {
    #if UNITY_ANDROID 
      Handheld.SetActivityIndicatorStyle(
        AndroidActivityIndicatorStyle.Small
      );
    #endif

    Handheld.StartActivityIndicator();
    yield return new WaitForSeconds(0);
    Application.LoadLevel(1);
  }

  void OnGUI()
  {
    if( GUI.Button(new Rect(50, 50, 300, 300), "Start") ) {
      StartCoroutine(ActivityIndicatorExample());
    }
  }
}

屏幕方向

此外,您还可以在 Unity 脚本中控制 Android 屏幕方向。您可以检测屏幕旋转或强制将屏幕旋转到特定方向。为了获取当前设备方向,您应该访问Screen.orientation属性。您还可以将此属性设置为任何所需的屏幕方向以强制旋转。您可以在 Unity 手册中找到有关不同屏幕方向属性和常量的更多信息。

系统信息

如果您需要更多关于您系统的信息,您可以使用 Unity 库提供的SystemInfo类的静态变量。有关这些变量的更多信息可以在官方 Unity 文档中找到,位于docs.unity3d.com/ScriptReference/SystemInfo.html

您可以将以下脚本附加到任何场景中的任何对象上,以获取有关您的 Android 设备的信息。让我们更仔细地看看这段代码内部发生了什么。首先,我们需要声明我们的类及其属性,如下所示:

using UnityEngine;

public class ShowSystemInfo : MonoBehaviour {
  public Vector2 scrollPosition;

  private Vector2 _v1, _v2;

之后,让我们创建我们的OnGUI函数,它将显示有关任何设备的信息。代码如下所示:

  void OnGUI()
  {
    GUILayout.BeginVertical();

    scrollPosition = GUILayout.BeginScrollView(
      scrollPosition, 
      GUILayout.Width(Screen.width), 
      GUILayout.Height(Screen.height)
    );

    GUILayout.Label("SystemInfo.deviceModel <<<===>>> " + SystemInfo.deviceModel);
    GUILayout.Label("SystemInfo.deviceName <<<===>>> " + SystemInfo.deviceName);
    GUILayout.Label("SystemInfo.deviceType <<<===>>> " + SystemInfo.deviceType.ToString());
    GUILayout.Label("SystemInfo.deviceUniqueIdentifier <<<===>>> " + SystemInfo.deviceUniqueIdentifier);
    GUILayout.Label("SystemInfo.graphicsDeviceID <<<===>>> " + SystemInfo.graphicsDeviceID.ToString());
    GUILayout.Label("SystemInfo.graphicsDeviceName <<<===>>> " + SystemInfo.graphicsDeviceName);
    GUILayout.Label("SystemInfo.graphicsDeviceVendor <<<===>>> " + SystemInfo.graphicsDeviceVendor);
    GUILayout.Label("SystemInfo.graphicsDeviceVendorID <<<===>>> " + SystemInfo.graphicsDeviceVendorID.ToString());
    GUILayout.Label("SystemInfo.graphicsDeviceVersion <<<===>>> " + SystemInfo.graphicsDeviceVersion);
    GUILayout.Label("SystemInfo.graphicsMemorySize <<<===>>> " + SystemInfo.graphicsMemorySize.ToString());
    GUILayout.Label("SystemInfo.graphicsPixelFillrate <<<===>>> " + SystemInfo.graphicsPixelFillrate.ToString());
    GUILayout.Label("SystemInfo.graphicsShaderLevel <<<===>>> " + SystemInfo.graphicsShaderLevel.ToString());
    GUILayout.Label("SystemInfo.npotSupport <<<===>>> " + SystemInfo.npotSupport.ToString());
    GUILayout.Label("SystemInfo.operatingSystem <<<===>>> " + SystemInfo.operatingSystem);
    GUILayout.Label("SystemInfo.processorCount <<<===>>> " + SystemInfo.processorCount.ToString());
    GUILayout.Label("SystemInfo.processorType <<<===>>> " + SystemInfo.processorType);
    GUILayout.Label("SystemInfo.supportedRenderTargetCount <<<===>>> " + SystemInfo.supportedRenderTargetCount.ToString());
    GUILayout.Label("SystemInfo.supports3DTextures <<<===>>> " + SystemInfo.supports3DTextures.ToString());
    GUILayout.Label("SystemInfo.supportsAccelerometer <<<===>>> " + SystemInfo.supportsAccelerometer.ToString());
    GUILayout.Label("SystemInfo.supportsComputeShaders <<<===>>> " + SystemInfo.supportsComputeShaders.ToString());
    GUILayout.Label("SystemInfo.supportsGyroscope <<<===>>> " + SystemInfo.supportsGyroscope.ToString());
    GUILayout.Label("SystemInfo.supportsImageEffects <<<===>>> " + SystemInfo.supportsImageEffects.ToString());
    GUILayout.Label("SystemInfo.supportsInstancing <<<===>>> " + SystemInfo.supportsInstancing.ToString());
    GUILayout.Label("SystemInfo.supportsLocationService <<<===>>> " + SystemInfo.supportsLocationService.ToString());
    GUILayout.Label("SystemInfo.supportsRenderTextures <<<===>>> " + SystemInfo.supportsRenderTextures.ToString());
    GUILayout.Label("SystemInfo.supportsRenderToCubemap <<<===>>> " + SystemInfo.supportsRenderToCubemap.ToString());
    GUILayout.Label("SystemInfo.supportsShadows <<<===>>> " + SystemInfo.supportsShadows.ToString());
    GUILayout.Label("SystemInfo.supportsSparseTextures <<<===>>> " + SystemInfo.supportsSparseTextures.ToString());
    GUILayout.Label("SystemInfo.supportsStencil <<<===>>> " + SystemInfo.supportsStencil.ToString());
    GUILayout.Label("SystemInfo.supportsVibration <<<===>>> " + SystemInfo.supportsVibration.ToString());
    GUILayout.Label("SystemInfo.systemMemorySize <<<===>>> " + SystemInfo.systemMemorySize.ToString());

    GUILayout.EndScrollView();
    GUILayout.EndVertical();
  }

最后,我们应该创建Update函数,以便更改信息列表的position Y值,以便能够滚动信息列表:

  void Update() {
    if (Input.touchCount > 0) {
      if (TouchPhase.Began == Input.GetTouch(0).phase) {
        _v1 = _v2 = Input.GetTouch(0).position;
      } else if (TouchPhase.Moved == Input.GetTouch(0).phase) {
        _v2 = _v1;

        _v1 = Input.GetTouch(0).position;

        scrollPosition.y += (_v1.y > _v2.y ? -1 : 1) * Vector2.Distance(_v1, _v2);
      }
    } else {
      if (Input.GetMouseButtonDown(0)) {
        _v1 = _v2 = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
      } else if (Input.GetMouseButton(0)) {
        _v2 = _v1;

        _v1 = new Vector2(Input.mousePosition.x, Input.mousePosition.y);

        scrollPosition.y += (_v1.y > _v2.y ? -1 : 1) * Vector2.Distance(_v1, _v2);
      }
    }
  }
}

在 Unity 5 中访问 Android 传感器和功能

让我们更深入地了解一些在文本后面的简单代码示例中展示的 Android 传感器和功能。

加速度

您还可以在 Unity 中访问 Android 加速度。让我们升级我们之前的示例,以便我们可以看到我们 Android 设备的一个更多方面。新的代码片段如下所示:

GUILayout.Label("\n\n A C C E L E R A T I O N");
    GUILayout.Label("Input.acceleration = (" + Input.acceleration.x + ", " + Input.acceleration.y + ", " + Input.acceleration.z + ")");

在您的 Android 设备上运行此测试后,您将看到加速度值如何实时快速变化。

陀螺仪

Unity 在 Android 设备上提供了陀螺仪访问,如新代码片段所示。在使用陀螺仪之前,您只需按照以下方式启用它:

    Input.gyro.enabled = true;

GUILayout.Label("\n\n G Y R O S C O P E");
    GUILayout.Label("Input.gyro.attitude <<<===>>> " + Input.gyro.attitude.ToString());
    GUILayout.Label("Input.gyro.enabled <<<===>>> " + Input.gyro.enabled.ToString());
    GUILayout.Label("Input.gyro.gravity <<<===>>> " + Input.gyro.gravity.ToString());
    GUILayout.Label("Input.gyro.rotationRate <<<===>>> " + Input.gyro.rotationRate.ToString());
    GUILayout.Label("Input.gyro.rotationRateUnbiased <<<===>>> " + Input.gyro.rotationRateUnbiased.ToString());
    GUILayout.Label("Input.gyro.updateInterval <<<===>>> " + Input.gyro.updateInterval.ToString());
    GUILayout.Label("Input.gyro.userAcceleration <<<===>>> " + Input.gyro.userAcceleration.ToString());

指南针

Unity 在 Android 设备上提供了指南针访问,如新代码片段所示。在使用指南针之前,您只需按照以下方式启用它:

    Input.compass.enabled = true;

    GUILayout.Label("\n\n C O M P A S S");
    GUILayout.Label("Input.compass.enabled <<<===>>> " + Input.compass.enabled.ToString());
    GUILayout.Label("Input.compass.headingAccuracy <<<===>>> " + Input.compass.headingAccuracy.ToString());
    GUILayout.Label("Input.compass.magneticHeading <<<===>>> " + Input.compass.magneticHeading.ToString());
    GUILayout.Label("Input.compass.rawVector <<<===>>> " + Input.compass.rawVector.ToString());
    GUILayout.Label("Input.compass.timestamp <<<===>>> " + Input.compass.timestamp.ToString());
    GUILayout.Label("Input.compass.trueHeading <<<===>>> " + Input.compass.trueHeading.ToString());

我们可以在以下屏幕截图中看到从前面的代码示例中接收到的所有值:

指南针

摘要

在本章中,我们探讨了为 Unity 编写 Java 和原生 C 插件的具体细节。我们实践了为 Android 平台开发简单插件。此外,我们还探讨了如何进行反盗版检查、检测屏幕方向、处理振动支持、获取设备名称、获取设备型号以及获取更多有用的信息。我们学习了如何在实践中访问加速度、陀螺仪****和指南针传感器及其特性和属性。

在下一章中,你将学习如何开发高端图形。你将探索如何在 Unity 中编写简单的 Cg 着色器。你还将了解全球范围内在游戏制作中广泛使用的精彩技巧、窍门和技术。你还将获取有关全局照明以及如何优化你的着色器的信息。

第三章. Android 设备的高端图形

主要来说,本章将探讨如何使用不同的技术和基于物理的着色器来提高游戏和应用程序的质量。在本章中,首先我们将检查在游戏开发和生产阶段经常使用的不同照明技术。其次,本章将描述 Unity 5 中的全局照明。在章节的结尾,读者将优化一段着色器代码。

本章将涵盖以下主题:

  • 基于物理的着色器

  • 全局照明

  • 着色器优化实践

基于物理的着色器

Unity 使得使用现成的着色器或使用 Cg 语言或表面着色器框架编写自己的着色器变得非常容易。表面着色器是用 Cg 编写的,但它们执行了大量工作,你不必在每次创建新着色器时都编写。表面着色器语言使用基于组件的方法,或者说是一种更抽象的方法,它通过使用复杂的照明模型来简化复杂着色器的编写。在使用表面着色器框架时,图形程序员不需要反复处理纹理坐标和矩阵变换。在本章中,我们将详细描述编写性能友好的着色器的不同技术和方法,并展示在全世界开发的各种游戏和应用中使用的良好视觉质量效果。

首先,让我们从着色器的基本原理和概念开始。着色器是为三维图形中使用的图形管道的多个阶段之一预编译的程序,用于确定对象或图像的最终参数。它可能包括对任意复杂性的光吸收和散射、纹理映射、反射和折射、着色、表面位移和后期处理效果的描述。

基本着色器概念

可编程着色器灵活且有效。看似复杂的表面可以用简单的几何形状来可视化。例如,着色器可以用来在完全平坦的表面上绘制陶瓷瓷砖的三维表面。

在 Unity 中,着色器分为三种类型:顶点几何片段(像素)

顶点着色器

顶点着色器处理映射到多边形顶点上的数据。这些数据对应于空间中顶点的坐标、纹理坐标、切线向量、双法线向量和法线向量。顶点着色器可用于透视顶点变换、生成纹理坐标、进行光照计算等。

几何着色器

与顶点着色器相比,几何着色器能够处理不仅是一个顶点,而且是一组顶点(三角形、四边形等)构成的整个原始形状。它可以被切割(两个顶点)和三角化(三个顶点),并且可以处理三角形原始形状的相邻顶点(相邻性)信息,最多可达六个顶点。此外,几何着色器可以“即时”生成原始形状,而不需要使用中央处理器。它们最初被用于Nvidia 8系列。

像素/片段着色器

像素着色器与位图的片段一起工作。像素着色器在图形管道的最后阶段使用,用于生成图片的片段。

着色语言

着色语言通常包含特殊的数据类型,如矩阵、采样器、向量和一组内置变量和常量,以便于与不同的 3D 库集成。由于计算机图形学有许多应用领域,为了满足市场的不同需求,开发者创建了大量的着色语言。

这些着色语言专注于提供最高质量的图像。在最高抽象级别描述材料属性,以便工作不需要任何特殊技能或对编程硬件的了解。这类着色器通常由艺术家创建,以确保“正确类型”的纹理映射、光源和其他艺术和科学方面的同时实现。

处理这些着色器通常是一个资源密集型任务。为此工作所需的总体计算能力可以非常高,因为它用于创建逼真的图像。这类相似计算的主要部分是通过大型计算机集群的可视化来完成的。

Cg

由 Nvidia 与 Microsoft 共同开发的 Cg 着色语言(实际上,Microsoft 的相同语言被称为HLSL,并包含在 DirectX 9 中)。Cg 在 Unity 中使用,代表C for Graphics。该语言与 C 非常相似,并使用类似的数据类型(intfloat和特殊的 16 位浮点类型—half)。Cg 还支持函数和结构。该语言具有独特的优化,如打包数组——类型声明如float a [4]float4 a是不同的类型。第二个声明是打包数组。打包数组操作比传统操作更快。尽管该语言由 Nvidia 开发,但它与其他图形卡(例如 ATI 卡)没有问题。然而,请注意,所有着色程序都有其独特的特性,可以从专业来源获得。

Unity 中的 Cg 着色器

此外,您应该知道 Unity 5 自带内置的着色器,这些着色器非常有用,尤其是在许多不同游戏中所需的一些基本内容方面。现在,让我们开始我们的奇妙之旅,进入 Unity 着色器的 Cg 语言世界。通常,着色器使用漫反射组件或照明模型。首先,您必须很好地理解在您的着色器中应该优化什么。基本上,您应该尽量避免复杂的计算和劳动密集型函数。在本章中,首先,我们将检查在游戏开发和生产阶段经常使用的不同照明模型技术。照明是着色器的一个基本方面。因此,程序员经常使用他们的大致计算来加速性能。

早期,计算机图形学使用的是固定功能的照明模型,这并不是一个非常灵活的解决方案,因为它只给图形程序员提供了一个单一的照明模型,而这个模型只能通过设置有限的一组参数和纹理来调整。与之前使用单一固定照明模型不同,如今,开发者使用非常灵活的可编程方法,借助 Cg 着色语言,特别是 Unity 中出色的表面着色器,来创建不同的照明模型。

着色器中的漫反射部分通常会指定光线以所有方向从表面反射的确切方式。您可能会发现这非常类似于镜子反射太阳光以不同角度和所有方向的工作。然而,事实并非如此,我们将在本章稍后尽可能详细地展示这种差异。

主要区别在于,像镜子这样的反射表面会反射周围环境的图像,而漫反射照明模型会将阳光反射回视野中。

为了创建一个简单且基本的漫反射照明模型,你需要创建一个包含发射颜色、环境颜色以及当然是从所有光源来的颜色总积累的着色器。在接下来的代码中,我们将向您展示的技术和技巧将帮助您创建自己的照明模型,以及探索各种行业技巧,帮助您理解仅使用纹理创建更复杂照明模型的基本思想,这将极大地提高您的生产力。换句话说,使用预制的纹理来创建照明模型可以大大提高您的生产力。

让我们从以下代码中显示的最简单的表面着色器示例开始。此代码由 Unity 编辑器生成:

// The first line of our shader code specifies the name of the
// shader in order to further select it from a list of all 
// shaders.
Shader "PacktPub/SimpleDiffuseLighting"
{
  // Next is the properties block of parameters of the shader 
  // known as Properties, which is followed by a block of the 
  // shader code known as SubShader.
        Properties
        {
                _MainTex ("Base (RGB)", 2D) = "white" {}
        }

        SubShader
        {
                Tags {"RenderType" = "Opaque"}
                LOD 200

                CGPROGRAM
                #pragma surface surf Lambert

                sampler2D _MainTex;
                struct Input
                {
                        float2 uv_MainTex;
                };

                void surf (Input IN, inout SurfaceOutput o)
                {
                        half4 c = tex2D (_MainTex, IN.uv_MainTex);
                        o.Albedo = c.rgb;
                        o.Alpha = c.a;
                }
                ENDCG
        }

    // The shader specified as FallBack will be executed 
    // instead of our shader.
        FallBack "Diffuse"
}

让我们更详细地考虑Properties块。在编写着色器时,属性是一些非常重要的元素。属性允许艺术家设置他们自己的纹理或其他设置以自定义视觉效果。您可以通过 Unity 材质调整所选着色器的属性。

Unity 会按顺序解析每个着色器代码,以查找内置结构。Properties块是 Unity 正在寻找的这些内置结构之一。以下是一个Properties块结构的示例:

Properties
{
        _YourVariableName ("Inspector GUI Name", Color) = (1,1,1,1)
}
//      Variable Name   Inspector GUI Name     Type  Default Value

每次你创建一个新属性时,你都需要给你的变量命名。变量名用于你的着色器代码中,而检查器 GUI 名称将在 Unity 编辑器中显示。类型可以是以下任何一个:

  • Range (min, max): 这些是从minmax形式的滑块实数值

  • Color: 这将在 Unity 检查器中打开颜色选择器,以便选择所需的颜色值

  • 2D: 这用于添加纹理

  • Rect: 这是一个非 2 的幂纹理

  • Cube: 这是一个立方体贴图纹理

  • Float: 这些是没有滑块的实数值

  • Vector: 这是一个包含实数的四分量向量

Properties结构的末尾,我们指定了默认值。

一个自定义的漫反射光照模型

在你编写自己的漫反射光照模型之前,我们将考虑我们的新属性:

Properties
{
        _FirstColor ("First Color", Color) = (1,1,1,1)
        _SecondColor ("Second Color", Color) = (0,0,0,0)
        _PowValue ("Pow Value", Range(0,10)) = 5.5
}

接下来,我们需要在我们的着色器中声明这些新属性:

float4 _FirstColor;
float4 _SecondColor;
float  _PowValue;

在着色器代码中宣布属性之后,我们可以像以下示例中那样使用这些变量:

void surf (Input IN, inout SurfaceOutput surface)
{
        float4 c = pow(_FirstColor + _SecondColor, _PowValue);
        surface.Albedo = c.rgb;
        surface.Alpha = c.a;
}

因此,你应该有一个如下的着色器:

Shader "PacktPub/YourDiffuseLighting"
{
        Properties
        {
                _FirstColor ("First Color", Color) = (1,1,1,1)
                _SecondColor ("Second Color", Color) = (0,0,0,0)
                _PowValue ("Pow Value", Range(0,10)) = 3.5
        }

        SubShader
        {
                Tags {"RenderType" = "Opaque"}
                LOD 200

                CGPROGRAM
                #pragma surface surf Lambert

                float4 _FirstColor;
                float4 _SecondColor;
                float  _PowValue;
                float4 c;
                struct Input
                {
                        float2 uv_MainTex;
                };

                void surf (Input IN, inout SurfaceOutput surface)
                {
                        c = pow(_FirstColor + _SecondColor, _PowValue);
                        surface.Albedo = c.rgb;
                        surface.Alpha = c.a;
                }
                ENDCG
        }

        FallBack "Diffuse"
}

是时候创建你自己的漫反射光照模型了。在大多数情况下,内置的光照并不适合游戏或应用中的特定任务。特定的优化问题需要独特的解决方案。为了覆盖内置的光照函数,你需要注册,下一行代码中的SubShader块:

#pragma surface surf YourName

现在,我们可以像以下示例中那样描述我们的自定义光照函数:

inline float4 LightingYourName 
(SurfaceOutput surface, float3 lightDirection, float attenuation)
{
        float delta = max(0, dot(surface.Normal, lightDirection));
        c.rgb = (surface.Albedo * _LightColor0.rgb) * 
                                                (delta * attenuation * 2);
        c.a = surface.Alpha;
        return c;
}

现在,让我们系统地查看基本元素。#pragma指令指定了用于光照的函数名称。我们使用了在Lighting.cginc文件中定义的内置功能Lambert,现在我们指定了我们的函数名称以供将来使用。在建立这个光照函数时,有必要记住,函数的名称最终将使用Lighting + <Your Function Name>的第一个单词来形成,例如,如果你决定将函数命名为SunShine,那么你的光照函数名称将是LightingSunShine。有三种创建自定义光照函数的方法,它们通过输入参数的不同而不同,如下所示:

  • float4 Lighting<YourName> (SurfaceOutput surface, float3 lightDirection, float attenuation) {}: 当你不需要视图方向值时,你应该使用这个函数进行前向渲染

  • float4 Lighting<YourName> (SurfaceOutput surface, float3 lightDirection, float3 viewDirection, float attenuation) {}: 当你需要视图方向值时,你应该使用这个函数进行前向渲染

  • float4 Lighting<YourName>_PrePass (SurfaceOutput surface, float4 light) {}: 你应该使用这个函数进行延迟渲染

最终,您应该得到以下着色器:

Shader "PacktPub/YourLightingModel"
{
        Properties
        {
                _FirstColor ("First Color", Color) = (1,1,1,1)
                _SecondColor ("Second Color", Color) = (0,0,0,0)
                _PowValue ("Pow Value", Range(0,10)) = 3.5
        }

        SubShader
        {
                Tags {"RenderType" = "Opaque"}
                LOD 200

                CGPROGRAM
                #pragma surface surf YourName

                float4 _FirstColor;
                float4 _SecondColor;
                float  _PowValue;
                float4 c;

                struct Input
                {
                        float2 uv_MainTex;
                };

                inline float4 LightingYourName (
                        SurfaceOutput surface, 
                        float3 lightDirection, 
                        float attenuation
                ){
                        float delta = max(0, dot(surface.Normal, lightDirection));
                        c.rgb = (surface.Albedo * _LightColor0.rgb) *
                                                (delta * attenuation * 2);
                        c.a = surface.Alpha;
                        return c;
                }

                void surf (Input IN, inout SurfaceOutput surface)
                {
                        c = pow(_FirstColor + _SecondColor, _PowValue);
                        surface.Albedo = c.rgb;
                        surface.Alpha = c.a;
                }
                ENDCG
        }

        FallBack "Diffuse"
}

基本反射环境

接下来,让我们看看在全世界专业圈子里广为人知的一些想法和技术,这些想法和技术被用来编写具有良好视觉效果的性能友好型着色器。以下示例基于您表面的环境反射。这个着色器的简单源代码如下:

Shader "PacktPub/BasicReflectionEnvironment"
{
        Properties 
        {
                _DiffuseTint ("Diffuse Tint", Color) = (1,1,1,1)
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _CubeMapTexture ("Cube Map Texture", CUBE) = ""{}
                _ReflectionCount ("Reflection Count", Range(0.01, 1)) = 0.17
        }

        SubShader 
        {
                Tags {"RenderType"="Opaque"}
                LOD 200

                CGPROGRAM
                #pragma surface surf Lambert

                sampler2D _MainTex;
                samplerCUBE _CubeMapTexture;

                float4 _DiffuseTint;
                float _ReflectionCount;

                float4 c;

                struct Input 
                {
                        float2 uv_MainTex;
                        float3 worldRefl;
                };

                void surf (Input IN, inout SurfaceOutput surface) 
                {
                        c = tex2D (_MainTex, IN.uv_MainTex) * _DiffuseTint;
                        surface.Emission = texCUBE(_CubeMapTexture, IN.worldRefl).rgb * _ReflectionCount;
                        surface.Albedo = c.rgb;
                        surface.Alpha = c.a;
                }
                ENDCG
        }

        FallBack "Diffuse"
}

遮罩纹理反射

下一个新着色器实现了一种新技术,它使用纹理来遮罩您环境的反射,如下所示:

Shader "PacktPub/MaskedTextureReflection"
{
        Properties 
        {
                _DiffuseTint ("Diffuse Tint", Color) = (1,1,1,1)
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _ReflectionCount ("Reflection Count", Range(0, 1)) = 1
                _CubeMapTexture ("Cube Map Texture", CUBE) = ""{}
                _MaskedTextureReflection ("Masked Texture Reflection", 2D) = ""{}
        }

        SubShader
        {
                Tags {"RenderType"="Opaque"}
                LOD 200

                CGPROGRAM
                #pragma surface surf Lambert

                sampler2D _MainTex;
                sampler2D _MaskedTextureReflection;

                samplerCUBE _CubeMapTexture;

                float4 _DiffuseTint;
                float _ReflectionCount;

                float4 c;

                struct Input 
                {
                        float2 uv_MainTex;
                        float3 worldRefl;
                };

                void surf (Input IN, inout SurfaceOutput surface) 
                {
                        c = tex2D (_MainTex, IN.uv_MainTex);
                        float3 reflectionTexCube = texCUBE(_CubeMapTexture, IN.worldRefl).rgb;
                        float4 reflectionMaskTexel = tex2D(_MaskedTextureReflection, IN.uv_MainTex);

                        surface.Albedo = c.rgb * _DiffuseTint;
                        surface.Emission = (reflectionTexCube * reflectionMaskTexel.r) * _ReflectionCount;
                        surface.Alpha = c.a;
                }
                ENDCG
        }

        FallBack "Diffuse"
}

灯光模型技术

让我们考虑各种灯光模型实现技术和方法,这些技术和方法,就像之前的着色器一样,在全球游戏产业以及电影和卡通中被广泛使用。

照明球体模型

首先,我们想考虑LitSphere灯光模型。这个想法非常简单直接——我们只需使用一个 2D 纹理来完全烘焙我们的灯光。或者,有必要考虑到并不要忘记这个技术是静态的,并且不会改变灯光,直到用于烘焙灯光的纹理被更改。这项技术提供了非常高质量的灯光,并且优化得足够好,但它不是动态的。换句话说,它不依赖于可以实时改变的角度或距离(从摄像机或观众),因为这项技术不依赖于场景中的灯光。让我们按照以下方式探索这个着色器:

Shader "PacktPub/LitSphere" 
{
        Properties 
        {
                _DiffuseTint ("Diffuse Tint", Color) = (1,1,1,1)
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _NormalMapTexture ("Normal Map Texture", 2D) = "bump" {}
        }

        SubShader
        {
                Tags {"RenderType"="Opaque"}
                LOD 200

                CGPROGRAM
                #pragma surface surf YourUnlit vertex:vert

                sampler2D _MainTex;
                sampler2D _NormalMapTexture;
                float4 _DiffuseTint;

                float4 c;
                float2 uv;

                inline float4 LightingYourUnlit (SurfaceOutput surface, float3 lightDirection, float attenuation)
                {
                        c.rgb = float4(1,1,1,1) * surface.Albedo;
                        c.a = surface.Alpha;

                        return c;
                }

                struct Input 
                {
                        float2 uv_MainTex;
                        float2 uv_NormalMapTexture;

                        float3 tangentOne;
                        float3 tangentTwo;
                };

void vert (inout appdata_full v, out Input inputData) 
{
        UNITY_INITIALIZE_OUTPUT(Input, inputData);

        TANGENT_SPACE_ROTATION;

        inputData.tangentOne = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz);
        inputData.tangentTwo = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz);          
}

                void surf (Input IN, inout SurfaceOutput surface) 
                {
                        surface.Normal = UnpackNormal(tex2D(_NormalMapTexture, IN.uv_NormalMapTexture)).rgb;

                        uv.x = dot(IN.tangentOne, surface.Normal);
                        uv.y = dot(IN.tangentTwo, surface.Normal);

                        c = tex2D (_MainTex, uv * 0.5 + 0.5);
                        surface.Albedo = c.rgb * _DiffuseTint;
                        surface.Alpha = c.a;
                }
                ENDCG
        } 

        FallBack "Diffuse"
}

创建灯光模型和其他视觉效果有许多不同的技术和方法;我们无法将所有想法和技术都放入这本书中,因为这超出了本书的范围。您也可以实现自己的新想法和技术;这取决于您的想象力。之前不同方法编写着色器的示例被世界各地的开发者广泛用于创建高质量的实时渲染,以及优化。此外,您还可以编写与模型顶点一起工作的着色器,这样您就可以非常简单地创建一个从原始平面播放波浪动画的着色器。

真实感渲染

Solid Angle的 Marcos Fajardo——这家公司是渲染器Arnold背后的公司——指出,世界上的越来越多的制作工作室要么已经到达那里,要么正在过渡到以下引言:

"整个行业都在进行这个过程,这是一件大事。我过去十年左右一直在做这件事,看到它最终发生我真的很高兴。"

Fajardo 可以被称为该行业全球变革最伟大的捍卫者和活动家之一。Solid Angle 真的是大规模运动的前沿,该运动将基于物理的材质和灯光的路径追踪 GI 用于生产决策(即,当预算较小且时间框架更紧凑时)。

“诚实”方法流行的基础是希望“一石二鸟”,简化全世界艺术家的生活,并实现更逼真的画面。

使用一些较老的技术链,艺术家可以通过几百个光源(其中每个光源都需要完成其角色,一个用于材料的突出显示,另一个用于该材料的镜面反射,第三个和第四个用于第二个材料的眩光和反射,再加上十个来模拟全局照明,等等)获得场景,开发者用 C++编写了非常复杂的着色器,代码充满了技巧和调整。照明设计师通常只是坐着,依次打开和关闭灯光——很容易理解为什么他们中的一些人是如此必要的。

大多数公司并没有计算到引入自然光源和材料本身就能快速渲染的事实,但人们期望这将极大地简化艺术家的工作。实际上,一小时的这种工作比一小时的渲染要贵几十倍。

全局照明

在 2014 年 3 月 17 日于旧金山开始的游戏开发者大会期间,Unity Technologies 公司推出了其流行的游戏引擎 Unity 的第五代产品。与上一版本相比,其中一个最重要的特点是实时全局照明的新系统——Enlighten,该系统由来自英国公司Geomerics的专家参与实施。

全局照明

Unity 5 展示了支持 WebGL 标准网络模块优化asm.js、物理引擎 NVIDIA PhysX 3.3、植被创建和动画系统 SpeedTree、高级着色器系统、实时预览光图功能,以及 Unity Cloud 中的跨平台音频广告网络,这有助于移动游戏的推广。此外,第五代引擎将能够在 64 位环境中运行,通过新的多线程调度器显著简化工作流程,并为你提供实时更改和改进创建游戏资源(资产列表)和直观界面的机会。

全局照明

本工具包的最新版本,传统上在小型开发者团队中是最畅销的,现在设计时也考虑到了大型公司。这使得它成为了 CryEngine 和 Unreal Engine 4 等高科技新一代游戏引擎的竞争对手。随着 Epic Games 的第四代引擎,它最近还与 Mozilla 签署了另一项协议,允许浏览器和移动设备上的三维和二维游戏开发者使用 Unity 5。

全局照明

在 Unity 5 中,开发者将能够使用来自公司Imagination Technologies的实时光线追踪 PowerVR 查看覆盖率图。这项技术减少了处理时间,从而提供了非常好的性能。开发者将能够使用 Unity 5 中新的着色器系统从现实世界中创建各种材料。

全局照明是用于三维图形中更真实地模拟光的一系列算法的名称。这些算法不仅考虑来自光源的直接光线(直接照明),还考虑来自各种表面的反射光线(间接照明)。

理论上,反射折射阴影是全球照明的例子,因为对于它们来说,必须考虑一个对象对另一个对象模拟的影响(与对象暴露在直接光线下的情况相比)。然而,在实践中,漫反射或全息的模拟被称为全局照明。

应用全局照明算法获得的图像通常比仅应用直接照明算法的渲染过程中的图像更真实。然而,计算全局照明需要更多的时间。

下图仅由直接照明算法处理:

全局照明

下图是通过全局照明算法处理的:

全局照明

着色器优化实践

现在,是时候讨论我们如何优化我们的着色器了。或者,是时候考虑其他方法了,例如优化内置数据类型,这可以显著减少 Unity 着色器内存的开销。我们考虑对所有支持的平台进行 Unity 着色器优化,没有任何排除。

非常常见,你需要优化着色器以实现相同的效果,但使用更少的纹理,例如。首先,当优化着色器代码时,我们希望将你的注意力引向变量的类型。如果你愿意牺牲计算的精度以降低质量来提高性能,那么你应该使用halffixed变量类型而不是float。例如,你可以在你的着色器代码的任何地方使用half类型的变量:

inline half4 LightingCarVehicle (SurfaceOutput surface, half3 lightDirection, half3 viewDirection, half attenuation)

你也可以在以下语句中将float替换为half

inline float4 LightingCarVehicle (SurfaceOutput surface, float3 lightDirection, float3 viewDirection, float attenuation)
  • float:这些变量具有 32 位精度

  • half:这些变量具有 16 位精度

  • fixed:这些变量具有 11 位精度

例如,让我们按照以下方式优化我们之前的着色器代码CarVehicle.shader

Shader "PacktPub/OptimizedCarVehicle"
{
        Properties 
        {
                _DiffuseTint ("Diffuse Tint", Color) = (1,1,1,1)
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _DiffuseIntensity ("Diffuse Intensity", Range(0.01, 17)) = 7.7
                _SpecularColor ("Specular Color", Color) = (1,1,1,1)
                _SpecularIntensity ("Specular Intensity", Range(0.01, 50)) = 17
                _ReflectionCubeMap ("Reflection Cube Map", CUBE) = "" {}
                _BRDFTexture ("BRDF Texture", 2D) = "white" {}
                _ReflectionIntensity ("Reflection Intensity", Range(0.01, 11.0)) = 5.0
                _ReflectionCount ("Reflection Count", Range(0.01, 1.0)) = 0.17
                _FalloffSpread ("Falloff Spread", Range(0.01, 17)) = 5.3
        }

        SubShader 
        {
                Tags {"RenderType"="Opaque"}
                LOD 200

                CGPROGRAM
                #pragma surface surf CarVehicle

                samplerCUBE _ReflectionCubeMap;

                sampler2D _MainTex;
                sampler2D _BRDFTexture;

                fixed _SpecularIntensity;
                fixed _DiffuseIntensity;
                fixed _FalloffSpread;
                fixed _ReflectionCount;
                fixed _ReflectionIntensity;

                fixed4 _DiffuseTint;
                fixed4 _SpecularColor;

                fixed4 c;
                fixed3 halfVec;
                fixed falloff;
                fixed delta;
                fixed halfVecDotSurfaceNormal;
                fixed s;

                inline fixed4 LightingCarVehicle (SurfaceOutput surface, fixed3 lightDirection, fixed3 viewDirection, fixed attenuation)
                {
                        halfVec = normalize (lightDirection + viewDirection);
                        delta = max (0, dot (surface.Normal, lightDirection));

                        halfVecDotSurfaceNormal = 1 - dot(halfVec, normalize(surface.Normal));
                        halfVecDotSurfaceNormal = pow(clamp(halfVecDotSurfaceNormal, 0.0, 1.0), _DiffuseIntensity);
                        c = tex2D(_BRDFTexture, fixed2(delta, 1 - halfVecDotSurfaceNormal));

                        s = pow (max (0, dot (surface.Normal, halfVec)), surface.Specular * _SpecularIntensity) * surface.Gloss;

                        c.rgb = (surface.Albedo * _LightColor0.rgb * c.rgb + _LightColor0.rgb * _SpecularColor.rgb * s)* (attenuation * 2);
                        c.a = surface.Alpha + _LightColor0.a * _SpecularColor.a * s * attenuation;

                        return c;
                }

                struct Input 
                {
                        fixed2 uv_MainTex;

                        fixed3 worldRefl;

                        fixed3 viewDir;
                };

                void surf (Input IN, inout SurfaceOutput surface) 
                {
                        c = tex2D (_MainTex, IN.uv_MainTex);

                        falloff = pow(saturate(1 - dot(normalize(IN.viewDir), surface.Normal)), _FalloffSpread);

                        surface.Albedo = c.rgb * _DiffuseTint;
                        surface.Emission = pow((texCUBE(_ReflectionCubeMap, IN.worldRefl).rgb * falloff), _ReflectionIntensity) * _ReflectionCount;
                        surface.Specular = c.r;
                        surface.Gloss = 1.0;
                        surface.Alpha = c.a;
                }
                ENDCG
        }

        FallBack "Diffuse"
}

要了解如何更快、更好地开发着色器,你必须明白这只能通过全面优化,使用各种技术和方法来实现。让我们将我们的着色器优化过程分为以下三个类别:

  • 变量内存优化

  • 优化使用的纹理的数量和大小

  • 计算算法优化

所提到的所有点都已经讨论过了。关于如何优化你的着色器的一些想法已经在本章前面考虑过了,我们将展示一些更有趣的方法和技术。我们希望这本书中的大多数方法、技术、方法和想法将极大地帮助你在生产中同时实现所需的质量和性能。

此外,在优化着色器时,你需要记住并知道代码应该尽可能小。这意味着你的代码中不应该有任何不必要的部分。前几章中描述的许多想法,尤其是在前面第四章中关于 C#和 JavaScript 代码优化的讨论,非常适合优化你的着色器代码。此外,我们希望你注意,你的着色器代码的执行频率极大地影响了性能。非常常见的是,着色器开发者使用非常好的技术来优化他们的着色器。他们更倾向于使用顶点着色器而不是像素着色器;这在大多数情况下会大大提高你的性能,因为像素的数量远多于顶点。因此,处理像素着色器执行频率的代码将远大于顶点。

让我们也考虑可能优化你的着色器的指令:

  • approxview: 在许多情况下,这种近似已经足够好了。当你需要获取每个顶点的归一化视图方向而不是每个像素的视图方向时,你应该使用这个指令。

  • halfasview: 这将在视图和光源方向之间(半向量)计算并归一化,并且光照函数将接收一个half向量作为参数而不是视图向量。

  • noforwardadd: 在单次渲染着色器的情况下,即使有多个光源,以及当你想要使着色器更小的时候,这个指令是你最好的选择。这个着色器将只支持前向渲染中的单一方向光。其余的光源仍然可以像顶点光或球谐函数那样产生影响。其余的光源可以用于球谐函数或顶点光效果。

  • exclude_path:prepass: 使用这个指令的着色器将不接受来自延迟渲染器的任何自定义光照。

  • noambient: 当你在着色器中禁用球谐函数和环境光时,应该使用这个指令。这可以略微提高你的性能。

  • nolightmap: 这个指令禁用了 Unity 的内部光照贴图系统。换句话说,它不会执行光照贴图检查。

Alpha 测试在移动设备上非常昂贵,因此你应该在移动设备上非常精确地使用透明着色器。你必须只在必要时使用 Alpha 测试。例如,让我们这样覆盖优化的着色器:

Shader "PacktPub/OptimizedShaderExample"
{
        Properties 
        {
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _SpecularWidth ("Specular Width", Range(0.01, 1)) = 0.5
                _NormalMapTexture ("Normal Map Texture", 2D) = "bump"{}
        }

        SubShader 
        {
                Tags {"RenderType"="Opaque"}
                LOD 200

                CGPROGRAM
                #pragma surface surf OptimizedBlinnPhong exclude_path:prepass nolightmap noforwardadd halfasview

                sampler2D _MainTex;
                sampler2D _NormalMapTexture;
                half _SpecularWidth;

                half4 c;
                half d;
                half s;

                struct Input 
                {
                        half2 uv_MainTex;
                };

                inline half4 LightingOptimizedBlinnPhong (SurfaceOutput surface, half3 lightDir, half3 halfDir, half atten)
                {
                        d = max(0, dot(surface.Normal, lightDir));
                        s = pow(max(0, dot(surface.Normal, halfDir)), surface.Specular * 128) * surface.Gloss;

                        c.rgb = (surface.Albedo * _LightColor0.rgb * d + _LightColor0.rgb * s) * (atten * 2);
                        c.a = 0.0;

                        return c;
                }

                void surf (Input IN, inout SurfaceOutput surface) 
                {
                        c = tex2D(_MainTex, IN.uv_MainTex);

                        surface.Albedo = c.rgb;
                        surface.Gloss = c.a;
                        surface.Alpha = 0.0;
                        surface.Specular = _SpecularWidth;
                        surface.Normal = UnpackNormal(tex2D(_NormalMapTexture, IN.uv_MainTex)).rgb;
                }
                ENDCG
        }

        FallBack "Diffuse"
}

最佳实践

在我们探索了照明计算领域中的各种想法、方法和技术之后,让我们来看看最佳实践,以便轻松维护许多不同的着色器。让我们考虑我们的着色器代码的可重用性;例如,Unity 中的各种照明函数。为了避免每次为新的着色器编写相同的照明函数代码,最好是将照明函数代码编写一次,并在必要时在任何着色器中使用它,因为程序员使用不同的框架和库。这种做法将帮助您为您的着色器创建一个框架,这将极大地促进轻松开发和轻松的着色器维护。在前面的示例中,我们使用了内置的 CgIncludes 文件,如 LambertBlinnPhong 照明函数。Unity 为我们创建了这些照明模型。Unity 帮助我们减少编写性能友好且质量上乘的着色器的努力。

您可以查看 Unity 内置 CgIncludes 文件中嵌入的代码,这些文件位于名为 CgIncludes 的目录中。没有这些文件,在 Unity 中编写着色器将会困难得多。这就是为什么 Unity 表面着色器如此高效。让我们按照以下方式创建自己的 CgInclude 文件:

#ifndef YOUR_NAME_INCLUDE
#define YOUR_NAME_INCLUDE

half4 _YourColorVariable;

inline half4 LightingOptimizedLambert (SurfaceOutput surface, half3 lightDirection, half attenuation)
{
        half diffuseValue = max(0, dot(surface.Normal, lightDirection));
        diffuseValue = (diffuseValue + 0.5) * 0.5;

        half4 tmpColor;
        tmpColor.rgb = surface.Albedo * _LightColor0.rgb * ((diffuseValue * _YourColorVariable.rgb) * attenuation * 2);
        tmpColor.a = surface.Alpha;

        return tmpColor;
}

#endif

现在,让我们考虑下一个着色器的代码如下,您可以看到如何使用您的 CgInclude 文件与照明函数一起使用,以及您如何声明变量 _YourColorVariable

Shader "PacktPub/UsingCgIncludeOptimzedLambert"
{
        Properties 
        {
                _YourColorVariable ("Your Color Variable", Color) = (1,1,1,1)

                _DiffuseTint ("Diffuse Tint", Color) = (1,1,1,1)
                _MainTex ("Base (RGB)", 2D) = "white" {}
                _NormalMapTexture ("Normal Map Texture", 2D) = "bump" {}
                _CubeMapTexture ("Cube Map Texture", CUBE) = ""{}
                _ReflectionCount ("Reflection Count", Range(0,1)) = 0.17
        }

        SubShader
        {
                Tags {"RenderType"="Opaque"}
                LOD 200

                CGPROGRAM
                #include "YourCgIncludeOptimizedLambert.cginc"
                #pragma surface surf OptimizedLambert

                samplerCUBE _CubeMapTexture;

                sampler2D _MainTex;
                sampler2D _NormalMapTexture;

                float4 _DiffuseTint;
                float _ReflectionCount;

                float4 c;

                struct Input 
                {
                        float2 uv_MainTex;

                        float2 uv_NormalMapTexture;

                        float3 worldRefl;

                        INTERNAL_DATA
                };

                void surf (Input IN, inout SurfaceOutput surface) 
                {
                        c = tex2D (_MainTex, IN.uv_MainTex);

                        surface.Normal = UnpackNormal(tex2D(_NormalMapTexture, IN.uv_NormalMapTexture)).rgb;
                        surface.Emission = texCUBE (_CubeMapTexture, WorldReflectionVector(IN, surface.Normal)).rgb * _ReflectionCount;
                        surface.Albedo = c.rgb * _DiffuseTint;
                        surface.Alpha = c.a;
                }
                ENDCG
        }

        FallBack "Diffuse"
}

因此,您可以创建自己的着色器框架。您还可以使用本章中的示例,并将所有代码以 CgIncludes 文件的形式放置。这将极大地帮助您避免代码重复,极大地简化着色器开发,并促进它们的优化。

摘要

在本章中,您学习了大量关于编写着色器和它们的优化知识。我们从简单的着色器代码开始,检查了 Unity 表面着色器中的基本元素。接下来,我们编写了自己的自定义漫反射照明模型。我们还探讨了全局照明。我们通过更改着色器变量类型以及编写特定指令来探索各种优化技术。在本章的结尾,我们介绍了使用 CgIncludes 文件开发着色器的最佳实践,并学习了如何使用其代码。

下一章将介绍 Unity 5 中的传统和 Mecanim 动画系统。你还将开发一个简单的自定义精灵动画系统,并探索如何在 Unity 5 中导入、设置和播放音频文件。在下一章的结尾,你将了解 Unity 5 中的物理和粒子系统。

第四章:Unity 5 中的动画、音频、物理和粒子系统

在本章中,您将学习 Unity 5 中的新 Mecanim 动画功能和令人惊叹的新音频功能。在本章结束时,您将探索 Unity 5 中的物理和粒子系统。

本章将涵盖以下主题:

  • Unity 5 中的新 Mecanim 动画功能

  • Unity 5 中的新音频功能

  • Unity 5 中的物理和粒子系统效果

Unity 5 中的新 Mecanim 动画功能

Unity 5 为 Mecanim 动画系统提供了一些新的令人惊叹的可能性。让我们看看在 Unity 5 中已知的新功能。

状态机行为

现在,您可以从 StateMachineBehaviour 继承您的类,以便能够将它们附加到您的 Mecanim 动画状态。此类具有以下非常重要的回调:

  • OnStateEnter

  • OnStateUpdate

  • OnStateExit

  • OnStateMove

  • OnStateIK

StateMachineBehaviour 脚本的行为类似于 MonoBehaviour 脚本,您可以将它们附加到您希望的对象上;对于 StateMachineBehaviour 也是如此。您可以使用此解决方案,无论是否有任何动画。

状态机转换

Unity 5 为 Mecanim 动画系统引入了一个新的令人惊叹的功能,称为状态机转换,以便构建更高的抽象级别。此外,创建了入口和退出节点。通过这两个额外的节点到 StateMachine,您现在可以根据您特殊条件和需求分支您的起始或结束状态。

注意

可能的过渡混合如下:StateMachine | StateMachineState | StateMachineState | State

此外,您还可以重新排列您的层或参数。这是通过一个非常简单且实用的拖放方法实现的新的用户界面。

资产创建 API

在 Unity 5 中,通过在 Unity 编辑器中使用脚本引入了另一个令人惊叹的可能性,以便以编程方式创建资产,例如层、控制器、状态、StateMachine 和混合树。您可以使用由 Unity 引擎维护的高级 API 和低级 API,在低级 API 中,您应该手动管理所有资产。您可以在 Unity 文档页面上找到有关这两个 API 版本的信息。

直接混合树

新引入的 BlendTree 类型的新功能被称为直接映射。它为 BlendTree 子项的权重提供直接映射和动画器参数。

注意

Unity 5 的可能性通过两个用于 Mecanim 动画系统的有用功能得到了增强:

  • 摄像机可以缩放、环绕和平移

  • 您可以在运行时访问您的参数

通过 Unity 5 API 以编程方式创建资产

以下代码片段是自我解释的,相当简单,直接明了。我列出它们只是为了作为一个非常有用的提醒。

创建控制器

要创建控制器,您可以使用以下代码:

var animatorController = UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath ("Assets/Your/Folder/Name/state_machine_transitions.controller");

添加参数

要向控制器添加参数,您可以使用以下代码:

animatorController.AddParameter("Parameter1", UnityEditor.Animations.AnimatorControllerParameterType.Trigger);
animatorController.AddParameter("Parameter2", UnityEditor.Animations.AnimatorControllerParameterType.Trigger);
animatorController.AddParameter("Parameter3", UnityEditor.Animations.AnimatorControllerParameterType.Trigger);

添加状态机

要添加状态机,您可以使用以下代码:

var sm1 = animatorController.layers[0].stateMachine;
var sm2 = sm1.AddStateMachine("sm2");
var sm3 = sm1.AddStateMachine("sm3");

添加状态

要添加状态,您可以使用这里提供的代码:

var s1 = sm2.AddState("s1");
var s2 = sm3.AddState("s2");
var s3 = sm3.AddState("s3");

添加过渡

要添加过渡,您可以使用以下代码:

var exitTransition = s1.AddExitTransition();
exitTransition.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, "Parameter1");
exitTransition.duration = 0;

var transition1 = sm2.AddAnyStateTransition(s1);
transition.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, "Parameter2");
transition.duration = 0;

var transition2 = sm3.AddEntryTransition(s2);
transition2.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, "Parameter3");
sm3.AddEntryTransition(s3);
sm3.defaultState = s2;

var exitTransition = s3.AddExitTransition();
exitTransition.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, "Parameter3");
exitTransition.duration = 0;

var smt = rootStateMachine.AddStateMachineTransition(sm2, sm3);
smt.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, "Parameter2");
sm2.AddStateMachineTransition(sm1, sm3);

深入了解新的音频功能

让我们从新的惊人的音频混音可能性开始。

现在,您可以在 Unity 5 中实现真正的音频子混音。

在下面的图中,您可以看到一个游戏所需的不同声音类别的简单示例:

深入新的音频功能

现在在 Unity 5 中,您可以在分类内混合不同的声音集合,并在一个地方调整音量控制和效果,这样可以节省大量时间和精力。Unity 5 中这个新的出色音频功能允许您为游戏创建出令人惊叹的情绪和氛围。

每个音频混音器都可以拥有一个音频组(AudioGroups)的层级结构:

深入新的音频功能

音频混音器不仅可以做很多有用的事情,还可以在一个地方混合不同的声音组。不同的音频效果按顺序应用于每个音频组。

现在,您正在接近 Unity 5 音频系统中的惊人、出色和闪亮的新功能!一个回调脚本OnAudioFilterRead,它使得直接在脚本中处理样本成为可能,之前完全由代码处理。

Unity 现在还支持自定义插件来创建不同的效果。有了这些创新,Unity 5 的音频系统现在拥有自己的合成器应用,这使得它比以前更容易、更灵活。

情绪转换

如前所述,游戏的情绪可以通过声音的混合来控制。这可以通过引入新的声源和音乐或环境声音来实现。另一种常见的方法是通过移动混合状态来实现。通过改变音量部分的混合并将其转移到不同的效果参数状态,这是一种非常有效的方法,可以将情绪引导到您想要的方向。

在内部,一切都是音频混音器识别图片的能力。图片捕捉了音频混音器中所有参数的状态。从调查湿度级别到音频组音调级别的一切都可以被捕捉并在各种参数之间移动。

情绪转换

您甚至可以在游戏中的大量图片之间创建复杂的混合状态,创造出各种可能性和目标。

想象一下,在不向脚本中写入一行代码的情况下安装所有这些功能。

Unity 5 中的物理和粒子系统效果

Unity 中的 2D 和 3D 物理在概念上非常相似,因为它们使用相同的概念,如刚体、关节和碰撞体。然而,Box2D 比 Unity 的 2D 物理引擎具有更多功能。在 Unity 中混合 2D 和 3D 物理引擎(内置、自定义、第三方)并不成问题。因此,Unity 为您的创新游戏和应用提供了简单易用的开发方式。

如果您需要在项目中开发一些真实的物理效果,那么您不应该编写自己的库、框架或引擎,除非有特定要求。然而,您应该尝试使用具有许多已制作功能的现有物理引擎、库或框架。

让我们开始介绍 Unity 内置的物理引擎。如果您需要将对象置于 Unity 内置的物理管理之下,只需将 Rigidbody 组件附加到该对象即可。之后,您的对象可以与其世界中的其他实体发生碰撞,重力也会对其产生影响。换句话说,Rigidbody 将进行物理模拟。在您的脚本中,您可以通过向它们添加矢量力来移动任何 Rigidbody。

不建议移动非运动学 Rigidbody 的 Transform 组件,因为它将无法正确与其他物品发生碰撞。相反,您可以向您的 Rigidbody 施加力和扭矩。

Rigidbody 也可以用来开发带有轮子碰撞体的汽车,以及使用您的脚本对其施加力。此外,Rigidbody 不仅用于车辆,您还可以用它来解决任何其他物理问题,如飞机、带有各种施加力脚本的机器人以及关节。

利用 Rigidbody 最有用的方式是将其与一些原始碰撞体(Unity 内置)如BoxColliderSphereCollider一起使用。接下来,我们将向您介绍关于 Rigidbody 的两个需要注意的事项:

  • 在您的对象层次结构中,您绝不应该同时将带有 Rigidbody 组件的子对象和父对象放在同一时间

  • 不建议缩放 Rigidbody 的父对象

在 Unity 中,物理学的最重要的基本组成部分之一是 Rigidbody 组件。这个组件会在附加的对象上激活物理计算。如果你的对象需要响应碰撞(例如,在玩台球时,球会相互碰撞并向不同方向散射),那么你必须在你的 GameObject 上附加一个Collider组件。如果你已经为你的对象附加了 Rigidbody 组件,那么你的对象将通过物理引擎移动,我建议你不要通过更改其在Transform组件中的位置或旋转来移动你的对象。如果你需要某种方式来移动你的对象,你应该应用作用在对象上的各种力,这样 Unity 物理引擎就会承担计算碰撞和移动动态对象的全部责任。此外,在某些情况下,可能需要 Rigidbody 组件,但你的对象只能通过在Transform组件中更改其位置或旋转属性来移动。有时,有必要使用没有 Rigidbody 计算对象及其运动物理的组件。也就是说,你的对象将通过你的脚本或,例如,通过运行你的动画来移动。为了解决这个问题,你应该只激活其IsKinematic属性。有时,当IsKinematic开启和关闭时,需要使用这两种模式的组合。你可以在你的代码或动画中直接更改IsKinematic参数,以创造这两种模式的共生体。

小贴士

频繁地在你的代码或动画中更改IsKinematic属性可能会导致你的性能开销。因此,你应该非常小心地使用它,并且只有在真正需要的时候才使用。

动力学 Rigidbody 对象由IsKinematic切换选项定义。如果一个 Rigidbody 是Kinematic,则该对象将不会受到碰撞、重力或力的 影响。

注意

3D 物理引擎有一个 Rigidbody 组件,2D 物理引擎有一个类似的 Rigidbody2D 组件。

动力学 Rigidbody 可以与其他非动力学 Rigidbody 交互。在使用动力学 Rigidbody 的情况下,你应该通过你的脚本或动画来转换其Transform组件的位置和旋转值。当动力学和非动力学 Rigidbody 之间发生碰撞时,动力学对象将正确唤醒非动力学 Rigidbody。此外,如果第二个对象位于第一个对象之上,第一个 Rigidbody 将对其施加摩擦力。

让我们列出一些动力学 Rigidbody 的可能使用示例:

  • 有时候你需要你的对象受到物理管理,但有时需要从你的脚本或动画中明确控制。例如,你可以将Rigidbody附加到你的动画人物的骨骼上,并通过关节将它们连接起来,以便将你的实体用作布娃娃。如果你通过 Unity 的动画系统控制你的角色,你应该勾选IsKinematic复选框。有时,如果你在打击英雄,你可能需要你的英雄受到 Unity 内置物理引擎的影响。在这种情况下,你应该取消勾选IsKinematic复选框。

  • 如果你需要一个可以推动不同物品但自身不动的移动物品。如果你有一个移动平台并且需要在上面放置一些Rigidbody对象,你应该勾选IsKinematic复选框,而不是简单地附加一个没有Rigidbody的碰撞器。

  • 你可能需要通过使用可用的一个关节来启用你的动画Rigidbody对象的IsKinematic属性,该对象具有真实的Rigidbody跟随者。

之前我提到了碰撞器,但现在正是详细讨论这个组件的时候。在 Unity 中,物理引擎可以计算碰撞。你必须通过附加Collider组件来指定你的对象的几何形状。在大多数情况下,碰撞器不需要与你的多边形网格形状相同。因此,使用简单的碰撞器是理想的,这会显著提高你的性能;否则,使用更复杂的几何形状,你可能会显著增加物理碰撞的计算时间。Unity 中的简单碰撞器被称为原始碰撞器:BoxColliderBoxCollider2DSphereColliderCircleCollider2DCapsuleCollider。此外,没有人禁止你组合不同的原始碰撞器来创建一个更真实的几何形状,与MeshCollider相比,物理引擎可以非常快速地处理这种形状。因此,为了加速你的性能,你应该尽可能使用原始碰撞器。你还可以挂载不同原始碰撞器的子对象,这将根据父Transform组件改变其位置和旋转。Rigidbody组件必须仅附加到你的实体层次结构中的 GameObject 根上。

Unity 为 3D 物理提供了一个MeshCollider组件,为 2D 物理提供了一个PolygonCollider2D组件。MeshCollider组件将使用你的对象的网格作为其几何形状。在PolygonCollider2D中,你可以在 Unity 中直接编辑并创建任何 2D 几何形状用于你的 2D 物理计算。为了在不同的网格碰撞器之间响应碰撞,你必须启用Convex属性。你肯定会为了更精确的物理计算而牺牲性能,但如果你在质量和性能之间找到了合适的平衡,那么你只能通过正确的方法来实现良好的性能。

当对象具有Collider组件但没有 Rigidbody 组件时,它们是静态的。因此,您不应该通过更改其Transform组件中的属性来移动或旋转它们,因为这会在物理引擎上留下沉重的印记,因为物理引擎应该重新计算各种对象的多个多边形,以进行正确的碰撞和光线投射。动态对象是具有 Rigidbody 组件的对象。静态对象(带有Collider组件且没有 Rigidbody 组件)可以与动态对象(带有Collider和 Rigidbody 组件)交互。此外,静态对象不会像动态对象那样因碰撞而移动。

此外,Rigidbodies 可以进入休眠状态以提高性能。Unity 提供直接在代码中使用以下函数来控制 Rigidbody 组件中休眠的能力:

  • Rigidbody.IsSleeping()

  • Rigidbody.Sleep()

  • Rigidbody.WakeUp()

在物理管理器中定义了两个变量。您可以直接从 Unity 菜单中打开物理管理器:编辑 | 项目设置 | 物理

  • Rigidbody.sleepVelocity:默认值是0.14。这表示线性速度的下限(从零到无穷大)低于此值时,对象将进入休眠状态。

  • Rigidbody.sleepAngularVelocity:默认值是0.14。这表示角速度的下限(从零到无穷大)低于此值时,对象将进入休眠状态。

Rigidbodies 在以下情况下会唤醒:

  • 另一个 Rigidbody 对静止的 Rigidbody 产生冲击

  • 另一个 Rigidbody 通过关节连接

  • 在调整 Rigidbody 属性时

  • 在添加力向量时

注意

动力学 Rigidbody 可以唤醒其他休眠的 Rigidbodies,而静态对象(带有Collider组件且没有 Rigidbody 组件)不能唤醒您的休眠 Rigidbodies。

集成到 Unity 中的 PhysX 物理引擎在移动设备上运行良好,但移动设备当然比强大的桌面电脑资源要少得多。

让我们看看一些优化 Unity 中物理引擎的要点:

  • 首先,请注意,您可以通过在时间管理器中调整Fixed Timestep参数来减少物理执行时间更新的成本。如果您增加该值,可以提高您游戏或应用程序中物理的质量和精度,但您将失去处理时间。这可能会大大降低您的生产力,换句话说,它可能会增加 CPU 开销。

  • 允许的最大时间步长表示在最坏情况下物理处理将花费多少时间。

  • 物理的总处理时间取决于场景中唤醒的刚体和碰撞器,以及碰撞器的复杂程度。

Unity 提供了使用物理材料设置各种属性的能力,例如摩擦力和弹性。例如,你游戏中的冰块可能具有非常低的摩擦力或等于零(最小值),而跳跃的球可能具有非常高的摩擦力或等于一(最大值),并且也具有非常高的弹性。你应该对不同对象的物理材料设置进行实验,并选择最适合你以及最佳性能的解决方案。

触发器不需要物理引擎进行大量的处理成本,并且可以极大地帮助提高你的性能。触发器在以下情况下非常有用,例如,在你的游戏中,你需要识别所有自动在傍晚或夜间打开的灯光附近的区域,如果玩家位于其触发区域或换句话说,位于其碰撞几何形状内。你可以按自己的意愿设计这些区域。Unity 触发器允许编写三个回调函数,这些函数将在你的对象进入触发器时被调用,当你的对象停留在触发器内时被调用,以及当这个对象离开触发器时被调用。因此,你可以注册这些函数中的任何一个,例如必要的指令,例如进入触发器区域时打开手电筒或退出触发器区域时关闭它。重要的是要知道,在 Unity 中,静态对象(没有 Rigidbody 组件的对象)不会使你的回调函数进入区域触发器,如果你的触发器不包含 Rigidbody 组件;换句话说,至少这些对象中必须有一个具有 Rigidbody 组件,以便不忽略你的回调。在两个触发器的情况下,至少应该有一个带有 Rigidbody 组件的对象附加到你的回调函数上,这样就不会被忽略。记住,当两个对象都附加了 Rigidbody 和 Collider 组件,并且至少有一个是触发器时,则将调用触发器回调而不是碰撞回调。我还想指出,你的回调函数将为碰撞或触发区域中包含的每个对象调用。此外,你可以通过在代码中将标志 isTrigger 的值设置为 truefalse 来直接控制你的碰撞器是否是触发器。当然,你可以混合这两种选项以获得最佳性能。所有碰撞回调函数只有在两个交互的刚体中至少有一个不是运动学刚体时才会被调用。我建议你考虑碰撞回调函数的代码示例。

第一个示例回调函数,将在碰撞事件开始时被调用:

void OnCollisionEnter (Collision collision)

第二个示例回调函数,将在保持碰撞状态时被调用:

void OnCollisionStay (Collision collision)

第三个示例回调函数,将在碰撞事件结束时被调用:

void OnCollisionExit (Collision collision)

第四个示例回调函数,将在碰撞事件开始时被调用,它进行了更多的优化,因为它避免了碰撞输入参数,从而避免了额外的计算:

void OnCollisionEnter ()

第五个示例回调,在保持碰撞状态时被调用,因为它避免了碰撞输入参数,从而避免了额外的计算,所以更加优化:

void OnCollisionStay ()

第六个示例回调,在碰撞事件结束时被调用,因为它避免了碰撞输入参数,从而避免了额外的计算,所以更加优化:

void OnCollisionExit ()

第七个示例回调,将在进入触发碰撞器时被调用:

void OnTriggerEnter (Collider collider)

第八个示例回调,将在保持触发碰撞器状态时被调用:

void OnTriggerStay (Collider collider)

第九个示例回调,将在退出触发碰撞器时被调用:

void OnTriggerExit (Collider collider)

第十个示例回调,将在没有碰撞器输入参数的情况下进入触发时被调用。这个回调将比带有碰撞器输入参数的回调更快:

void OnTriggerEnter ()

第十一个示例回调,将在没有碰撞器输入参数的情况下进入触发时被调用。这个回调将比带有碰撞器输入参数的回调更快:

void OnTriggerStay ()

第十二个示例回调,将在没有碰撞器输入参数的情况下退出触发时被调用。这个回调将比带有碰撞器输入参数的回调更快:

void OnTriggerExit ()

现在,让我们来谈谈关节。如果你需要将一个 Rigidbody 连接到另一个 Rigidbody 以便围绕特定点进行旋转,例如一个铰链门,那么你应该使用 HingeJoint(对于 2D,合适的名称是 HingeJoint2D)。Unity 还提供了其他类型的关节;例如,弹簧关节适用于需要开发蹦床或类似物品的情况。然而,我强烈建议你不要到处都使用关节,因为这可能会破坏你的性能。只使用真正必要的东西,并且尽可能少用。提高性能最重要的方法是移除所有不必要的元素。

你还可以使用CharacterController组件来创建第一人称游戏。CharacterController组件使用其自己的物理计算,与 Rigidbody 分开。

这些对于在y轴周围的不同表面上行走非常方便,无需旋转,并在 Rigidbody 组件的情况下保持必要的平衡。CharacterController组件与 Rigidbody 相比,所需的处理时间也少得多。这就是为什么你应该尽可能使用CharacterController而不是 Rigidbody,但尽量像其他所有东西一样,尽可能少用。CharacterController组件包含CapsuleCollider,它沿着y轴向上。接下来,你将研究以下截图所示的CharacterController属性:

Unity 5 中的物理和粒子系统效果

粒子系统性能技巧和窍门

粒子系统使用大量的小粒子,换句话说,就是大量的图形对象,以创建不同的效果,如灰尘、雨、雪、火、爆炸、烟雾、瀑布、落叶、雾、星星、星系、烟花、各种魔法效果等。通常,粒子系统发射多个粒子,这些粒子有自己的寿命,之后它们逐渐消失并被重新发射。还有一些使用粒子系统创建毛发、头发、草地的技术,其中粒子不会消失,但它们可以存活很长时间。

注意

粒子系统可以是 2D 或 3D。

从数学上讲,每个粒子表示为一个具有附加属性的质量点,例如外观、速度、空间中的方向、角速度等。在程序过程中,每个粒子通过一个特定的公式改变其状态,该公式对所有系统中的粒子都是通用的。例如,粒子可能受到重力的影响,改变其大小、颜色、速度等。所有计算完成后,粒子将被可视化。粒子可以通过点、三角形、精灵甚至完整的全三维模型来可视化。

目前,粒子系统没有统一的实现。在不同的游戏和应用程序中,粒子的 3D 建模属性、行为和外观可能存在根本性的不同。

在大多数实现中,新粒子是由所谓的发射器发射的。如果发射器是一个点,那么新粒子将出现在相同的位置。因此,可以模拟,例如,爆炸——发射器是其中心。发射器可以是一条线、线段或一个平面;例如,雨滴或雪花粒子应该出现在高水平的水平面上。发射器可以是一个任意几何对象,在这种情况下,新粒子将出现在其整个表面上。

在粒子的整个生命周期中,粒子很少处于静止状态。粒子可以移动、旋转、改变颜色和/或透明度,并且可以处理三维对象。通常,粒子设置最大寿命,之后粒子消失。

在三维、实时应用或计算机游戏中,通常认为粒子不会相互投射阴影,也不会在环境的几何形状上投射阴影,它们不会吸收和发射光线。如果没有这些,简化的粒子系统将需要更多的资源;在吸收光线的情况下,粒子需要按距离摄像机的顺序排序,而在每个粒子需要绘制几次阴影的情况下。

与 Unity 内置的粒子系统相比,Shuriken 模块的遗留系统

  1. Shuriken 模块的一些属性无法在您的脚本中实现。

  2. 同时,在您的脚本中也可以实现遗留粒子系统的属性。

  3. 您可以根据以下代码示例开启和关闭emission

      public ParticleSystem yourParticleSystemVariable;
      void YourMethodName() {
        yourParticleSystemVariable.enableEmission = false;
      }
    

在发射爆炸中的粒子时,你应该使用以下代码示例中的Emit函数:

  public ParticleSystem yourParticleSystemVariable;
  void YourMethodName() {
    yourParticleSystemVariable.Emit(123); //emits 123 particles
  }

与激活和停用emission属性不同,你还可以像下面这样控制发射器中的粒子:

  public ParticleSystem yourParticleSystem;
  private ParticleSystem.Particle[] 
      yourParticlesList = new ParticleSystem.Particle[1750];
  void YourMethodName() {
    int len = yourParticleSystem.GetParticles(yourParticlesList);
    for(int i=0; i < len; i++) {
          yourParticlesList[i].color = new Color(0,0,1,0.5f);
      }
    yourParticleSystem.SetParticles(yourParticlesList, len);
  }

让我们列出Particle类的可获取属性(docs.unity3d.com/ScriptReference/ParticleSystem.Particle.html):

  • lifetime

  • startLifetime

  • position

  • rotation

  • color

  • size

  • velocity

  • randomValue

  • angularVelocity

注意

在 Unity 中创建 Shuriken 粒子系统非常简单。你只需导航到GameObject | Create Other | Particle Systems。这将为你创建一个 Shuriken 粒子系统的实例,你可以对其进行操作。

要创建一个旧版粒子系统,你必须创建一个void GameObject 或将旧版粒子系统连接到一个可用的 GameObject。

粒子系统技巧

在现代游戏中,有许多事情会降低帧率,粒子几乎是最主要的原因之一。一个关键因素是粒子容易产生大量的不必要的渲染,而这些渲染并没有显示在你的不透明几何体上。

过度绘制增加的原因是,对于粒子,我们倾向于有大量的不同原语(通常是四边形)被覆盖,可能是为了复制火焰或烟雾等效果。通常,每个粒子原语都是透明的(alpha 混合),因此 z 缓冲区不会被更新,当像素被写入时,我们最终在不同的时间渲染到像素上。(有趣的是,对于模糊几何体,我们确实会写入 z 缓冲区,因此在一个可能的 z 预扫描、从前到后排序项目、GPU 上的渐进式 z 裁剪和常规深度测试之间,效果是我们几乎没有任何过度绘制。)

因此,过度绘制会导致fillrate(每秒可以渲染的像素数量)和bandwidth(每秒可以交换到/从 GPU 的信息量)的使用增加,这两者可能都是稀缺资源。

我们都认为粒子可能会引起相当多的问题。幸运的是,有许多事情能够改善粒子系统的渲染方面。

  • 使用不透明粒子:例如,使烟雾效果真正厚重,使得(一些或全部的)分子板变得不透明,具有固定的图案 alpha。对于一些粒子,例如碎片、岩石或类似物体,使用轻量级几何粒子而不是带有 alpha 边界的精灵。

  • 使用更丰富的粒子:给单个分子精灵增加更多活力,这样你需要的就少。使用翻书表面来制作例如火焰和烟雾的涌动,而不是堆叠精灵。

  • 粒子总数:使用图形卡上的适配器计数器来获取已渲染的粒子像素数,并在超过某个临界点时停止释放或绘制粒子(这个临界点可能被动态设置)。

  • 减少状态变化:在粒子之间共享着色器。例如,你可以通过丢弃远离粒子的特性(例如,在最早的机会丢弃法线贴图)来实现这一点。

  • 以前后预乘 alpha 风格制作粒子:使用预乘 alpha(这是协作的),你可以从前向后混合粒子,而不是普通的从后向前请求。这里的想法是利用前后吸引来填充当 alpha 变得(接近)强时填充模板缓冲区,最后,完全停止绘制粒子(当它们通常不会对视觉场景有很大帮助时)。

  • 将粒子聚集成一个分子实体:而不是分别绘制两个重叠的粒子,你可以构建一个包含两个粒子的单一(更大的)分子,并在着色器中以简单的方式混合这两个粒子。这倾向于减少我们进行的帧缓冲区理解的量,因为我们只需要混合一个分子。

摘要

本章介绍了 Unity 5 中的新 Mecanim 动画功能。你了解了 Unity 5 中新的出色音频功能。在本章的最后,你探索了 Unity 5 中的物理和粒子系统。在本章中,你涵盖了在 Unity 内置物理和粒子系统中进行性能时许多有用的细节。你探索了刚体及其相关的技巧和窍门。你学习了运动学刚体和休眠刚体、碰撞体、静态碰撞体、原始碰撞体、物理材质、触发器、关节、角色控制器、交互式布料以及许多其他有用的物理定义、细节、注意事项、技巧和窍门。你还学习了关于粒子系统技巧和窍门以及如何为你的任何对象创建一个简单的对象池系统。

下一章将包括 Unity 5 中资源包的概述。你还将学习如何实时为 Android 设备下载新的代码和数据。在本章的最后,你将发现资源包在实际应用中的安全性技术。

第五章. Unity 5 Pro 中的资产包

本章将包括 Unity 5 中资产包的概述。您将学习如何为 Android 设备实时下载新代码和数据。在本章结束时,读者将发现资产包在实践中的安全性技术。

本章将涵盖以下主题:

  • Unity 5 中资产包概述

  • 为 Android 设备实时下载新代码和数据

  • 实践中的安全性技术

Unity 5 中资产包概述

资产包是Unity Pro特有的功能。资产包的两个主要思想是:

  • 在您的应用程序中轻松下载内容

  • 在您的应用程序中上传新内容

Unity 允许将您的资产导出为文件,这些文件被称为资产包。您的应用程序可以在需要时下载这些压缩文件。这种方法将通过流式传输以下内容来减少最终构建大小:预制体、动画、二进制文件、纹理、音频剪辑、网格和场景,其中将使用资产包。Unity 支持所有其他资产类型。对于二进制文件,您应将扩展名设置为.bytes,Unity 将识别这些文件为TextAsset。要使用资产包,您只需创建它们并将它们上传到您的服务器。在 Unity 编辑器中,您可以从场景中的资产构建资产包。在需要将您的资产包上传到服务器的情况下,您可以使用任何数据通信协议;例如,SSH、FTP、FTPS、SFTP 或任何其他协议,具体取决于您的选择。在实时情况下,您在脚本中编写的应用程序将下载必要的资产包,以便进一步处理这些导出文件中的打包资产。

我们将介绍您应该做什么来创建AssetBundle文件。为此任务,您应使用名为BuildPipeline的 Unity 编辑器类。

注意

如果您在脚本中使用任何 Unity 编辑器类,那么您应该始终记住将这些脚本保存在名为Editor的文件夹中,该文件夹位于项目中的任何子目录的Assets文件夹内。

现在,让我们创建一个简单的 C#脚本以创建AssetBundle。首先,我们应该导入两个 Unity 命名空间:

  using UnityEngine;
  using UnityEditor;

在这些代码行之后,您应该声明您的公共类;例如,您可以创建一个非常简单的类声明:

  public class BuilderAssetBundle {
    // the code you will see below
  }

在下一步中,我们将创建一个具有MenuItem的静态函数,以便将来从 Unity 编辑器菜单中选择:

  [MenuItem("PacktPub/AssetBundles/Build Asset Bundle")]
  static void Build() {
    // the code you will see below
  }

然后,您需要用单个指令或一行代码或类似的内容填充您的静态函数,根据您的需求,您将看到已经完成的简单类来构建您的AssetBundle

  BuildPipeline.BuildAssetBundle(
    Selection.activeObject, 
   Selection.GetFiltered(typeof(Object),SelectionMode.DeepAssets),
    "Assets/Your/Path/To/YourAssetBundle.unity3d",
    BuildAssetBundleOptions.CollectDependencies | 
    BuildAssetBundleOptions.CompleteAssets
  );

此函数创建一个压缩的Assets/Your/Path/To/YourAssetBundle.unity3d文件,其中包含打包的资产列表(来自项目文件夹中的任何资产),如果AssetBundle创建成功则返回true,否则返回false。此函数的第一个变量Selection.activeObject指示要使用哪个对象来从AssetBundle中检索打包的资产,使用AssetBundle.mainAsset属性。如果你不使用它,可以将此值设置为null。第二个变量是array Object[],指定需要打包的资产。第三个变量非常简单,它只是你想要保存AssetBundle文件的位置。通过具体调整BuildAssetBundleOptions标志,你可以指定自动包含所有依赖项或仅包含完整资产。此外,使用这些选项,你可以通过设置UncompressedAssetBundle标志来指定你不想在AssetBundle文件中压缩资产。如果你想,你可以在通过WWW.LoadFromCacheOrDownload调用下载导出的文件时检查你的资产包的CRC校验和。要创建资产包,你可以使用三个不同的函数:

  • BuildPipeline.BuildAssetBundle:这将构建任何类型的资产。

  • BuildPipeline.BuildStreamedSceneAssetBundle:这将仅包含场景。

  • BuildPipeline.BuildAssetBundleExplicitAssetNames:这个函数与BuildPipeline.BuildAssetBundle类似,但有一些区别。在这个函数中,你可以指定所有包含对象的字符串标识符。

注意

你可以为 Web-Player 平台创建资产包并在独立平台上使用它们,反之亦然。或者,对于移动平台,你可以仅使用它们的构建文件。例如,你可以为 Android 设备创建资产包并仅用于 Android 平台,但不能用于 iOS 平台,反之亦然。对于 iOS 平台的构建文件,你只能在其内部使用。

为了使用你的类来创建资产包,你需要进行两个简单的步骤:

  1. 你应该在项目中选择一个或多个资产,这些资产将被打包到你的AssetBundle中。

  2. 从 Unity 菜单中,你应该导航到PacktPub | AssetBundles | Build Asset Bundle

创建AssetBundle的完成脚本应类似于以下代码所示:

using UnityEngine;
using UnityEditor;

public class BuilderAssetBundle {
  [MenuItem("PacktPub/AssetBundles/Build Asset Bundle")]
  static void Build() {
    BuildPipeline.BuildAssetBundle(
      Selection.activeObject,
      Selection.GetFiltered(
        typeof(Object),
        SelectionMode.DeepAssets
      ),
      "Assets/Your/Path/To/YourAssetBundle.unity3d",
      BuildAssetBundleOptions.CollectDependencies | 
      BuildAssetBundleOptions.CompleteAssets
    );
  }
}

我们已经实现了资产包的创建,现在让我们探索如何在脚本中使用它们。要使用你的资产包,你应该遵循以下两个简单的步骤:

  1. 从任何本地存储(如硬盘)或远程存储(如任何 Web 服务器)下载你的AssetBundle文件,Unity 提供了WWW辅助类来处理此类问题。

  2. 加载,或者说,从你的AssetBundle文件中解包你的资产,以便在游戏中进一步使用。

在以下代码中,我们打算了解使用AssetBundle的情况。首先,我们需要创建一个简单的 C#脚本,例如命名为ImporterAssetBundle。在代码编辑器中打开脚本,并按以下所示进行更改:

using UnityEngine;
using System.Collections;

public class ImporterAssetBundle : MonoBehaviour {
  void Start() {
    StartCoroutine(Import());
  }

  public IEnumerator Import() {
    using (WWW wwwData = WWW.LoadFromCacheOrDownload(
        "http://your-domain.com/your/path/url/new2.unity3d",
        23 // your asset bundle version, as an example only
    )) {
      yield return wwwData;
      GameObject obj = www.assetBundle.mainAsset as GameObject;
      Instantiate(obj);
      www.assetBundle.Unload(false);
    }
  }
}

将此脚本作为组件附加到你的 GameObject 上。此脚本中的 URL 仅作为示例,因此你应该设置你自己的AssetBundle文件 URL。基于这个版本,Unity 的缓存系统将决定是否下载你的文件。如果此文件已经以相同的版本号缓存,那么 Unity 将加快你的应用程序的速度。在这个脚本中,我们在Start事件中调用了Import协程,但你可以在需要的地方调用此函数,并且可以按需多次调用。在Import协程中,我们首先使用WWW.LoadFromCacheOrDownload,其唯一目的是通过提供 URL 路径和版本号来加载所需的AssetBundle文件。在下载过程中,Unity 将不会执行yield return www命令之后的指令。只有当下载完成后,Unity 才会运行我们协程示例中的下一个命令。

  • 使用www实例从下载的AssetBundle中检索mainAsset作为 GameObject 实例

  • 在当前场景中实时创建检索到的GameObject实例的新实例

  • 卸载用于此AssetBundle的所有内存(以提高性能)。

由于制作项目是一个迭代过程,你可能会多次修改你的资源,这可能会迫使你在每次更改后重新制作资源包,以便能够测试它们。尽管在 Unity 编辑器中可以加载资源包,但这并不是最佳解决方案。相反,在 Unity 编辑器中进行测试时,你应该使用辅助函数Resources.LoadAssetAtPath来避免需要使用和更新资源包。该函数允许你以从资源包中加载的方式加载你的资源,但我们将跳过构建过程,你的资源始终是可用的。让我们创建一个新的 C#脚本,它将改进我们的最后一个示例,并添加一些异常处理以及Resources.LoadAssetAtPath在 Unity 编辑器导入时的功能。创建一个新的脚本,例如命名为MyAssetBundleImporter。下一步是在文件的开始处声明我们将使用的所需命名空间:

  using UnityEngine;
  using System.Collections;
After these lines we will declare our public class:
  public class MyAssetBundleImporter {
    // the code you will see below
  }

让我们在类中声明一个public属性,用于从你的AssetBundle中检索的对象:

  public Object assetBundleObject;

此外,在这个类中,让我们声明我们的public和特定的AssetBundle结构,以及其公共属性,以便进一步使用:

  public struct AssetBundleStruct {
    public string assetSourceName;
    public string assetSourcePath;
    public string assetBundleUrl;
    public int assetBundleVersion;
  }

接下来,我们将声明此脚本的核心,即 Unity 将作为协程执行的功能:

  public IEnumerator Import<T>(AssetBundleStruct abs) where T : Object {}

协程的第一步是将我们的assetBundleObject初始化为null

  assetBundleObject = null;

在那行代码之后,让我们声明主要条件,以决定如何导入你的资源包:

  #if UNITY_EDITOR
    // 1st part, the code you will see below
  #else
    // 2nd part, the code you will see below
  #endif

让我们为if/else/endif预处理器语句的第一部分编写代码:

  assetBundleObject = Resources.LoadAssetAtPath(
    abs.assetSourcePath, typeof(T)
  );

  if (null == assetBundleObject){
    Debug.LogError("AssetBundle ERROR Path: " + abs.assetSourcePath);
    Debug.LogError("Asset Bundle could not be found !!!");
  }
  yield break;

现在,我们将为if/else/endif预处理器语句的第二部分编写代码:

  WWW www;
  if (Caching.enabled) {
    while (false == Caching.ready) {
      yield return null;
    }
    www = WWW.LoadFromCacheOrDownload(
      abs.assetBundleUrl, abs.assetBundleVersion
    );
  } else {
    www = new WWW(abs.assetBundleUrl);
  }

  yield return www;

  if (null != www.error) {
    Debug.LogError(www.error);
    www.Dispose();
    yield break;
  }

  AssetBundle ab = www.assetBundle;
  www.Dispose();
"  www = null;
  if (string.Empty == abs.assetSourceName || null == abs.assetSourceName) {
    assetBundleObject = ab.mainAsset;
  } else {
    assetBundleObject = ab.Load(abs.assetSourceName, typeof(T));
  }

  ab.Unload(false);

让我们创建一个简单的脚本,继承自MonoBehaviour,这将展示你如何使用你的示例类MyAssetBundleImporter

using UnityEngine;
using System.Collections;

public class ExampleMyImporterUsage : MonoBehaviour {
  public MyAssetBundleImporter.AssetBundleStruct abs;

  private string _tmpStr;
  private Object _tmpObj;
  void Start() {
    abs = new MyAssetBundleImporter.AssetBundleStruct();
    abs.assetBundleUrl = "http://yourapp.com/your/bundle.unity3d";
    abs.assetBundleVersion = 0;
    abs.assetSourceName = "YourPrefabName";

    StartCoroutine(Import());
  }

  IEnumerator Import() {
    MyAssetBundleImporter mabi = new MyAssetBundleImporter();
    yield return StartCoroutine(mabi.Import<GameObject>(abs));
    if (null != mabi.assetBundleObject) {
      _tmpObj = Instantiate(mabi.assetBundleObject);
    }
  }

  void OnGUI() {
    if (null != _tmpObj) {
      _tmpStr = _tmpObj.name + " was successfully created.";
      GUILayout.Label(_tmpStr);
    } else {
      GUILayout.Label("ERROR: Cannot import your AssetBundle.");
    }
  }
}

让我们描述一下最后一个脚本中发生了什么。首先,我们声明了一个公共的AssetBundleStruct变量和两个私有变量。然后,我们在Start方法中初始化我们的AssetBundleStruct变量。接下来,我们在Start函数中调用协程Import。在Import协程中,我们创建了我们类MyAssetBundleImporter的一个实例,并使用我们初始化的结构abs调用它的Import协程。如果我们导入了对象并且它不等于null,那么我们在场景中实例化那个 GameObject。此外,我们展示了简单的GUI标签,指示我们是否成功导入了AssetBundle。为了使用这个(只有如果你的AssetBundle已经创建并上传)脚本,你只需将其作为组件附加到你的场景中的 GameObject 上,并设置正确的值。如果这两个简单的步骤已经完成,那么你就可以在 Unity 编辑器或构建中播放和测试你的游戏。

如果你需要从你的AssetBundle获取包含的所有对象的数组,你应该使用名为AssetBundle.LoadAll的函数。在需要获取字符串标识符列表的情况下,你应该保留一个特定的TextAsset作为映射,以保存你的资源名称。

在以下步骤中,我们将展示一个简单的例子,关于在构建资源包之前调整不同的纹理压缩。我们只需要记住两个简单的步骤:

  1. 为了在构建资源包之前强制重新导入你的资源,你应该使用AssetDatabase.ImportAsset函数。

  2. 之后,你应该使用AssetPostprocessor.OnPreprocessTexture来调整纹理压缩的正确值。

现在,让我们编写一个简单的代码示例,你可以在你的项目中使用,就像这本书中的其他示例一样:

  using UnityEngine;
  using UnityEditor;  

和往常一样,我们声明所需的命名空间(记住这个脚本将使用UnityEditor并且应该位于Editor文件夹中,我们之前已经讨论过这个 Unity 要求)。下一步是定义一个简单的类,使用以下代码:

  public class TextureFormatAssetBundles {
    // the code you will see below
  }

在简单的类声明之后,我们需要设置一个publicstatic变量,同时还要设置三个不同但基本的功能(你可以使用任何想要的纹理格式;在这个例子中,我们使用DXT1DXT5ETC_RGB4):

  public static TextureImporterFormat tif;

  [MenuItem("PacktPub/AssetBundles/Create Asset Bundle DXT1")]
  static void SetTextureFormatDXT1() {
    tif = TextureImporterFormat.DXT1;
    CreateAssetBundle();
  }

  [MenuItem("PacktPub/AssetBundles/Create Asset Bundle DXT5")]
  static void SetTextureFormatDXT5() {
    tif = TextureImporterFormat.DXT5;
    CreateAssetBundle();
  }

  [MenuItem("PacktPub/AssetBundles/Create Asset Bundle ETC_RGB4")]
  static void SetTextureFormatETC_RGB4() {
    tif = TextureImporterFormat.ETC_RGB4;
    CreateAssetBundle();
  }

现在,我们可以编写我们的主函数CreateAssetBundle,它将完成所有脏活累活:

  static void CreateAssetBundle() {
    // the code you will see below
  }

在我们的第一步中,我们调用 EditorUtility.SaveFilePanel 方法以显示 Unity 对话框并从中获取选定的路径字符串。此外,我们还需要从这个函数返回一个空的位置变量:

  string selectedPath = EditorUtility.SaveFilePanel(
    "Save", // TITLE
    string.Empty, // DIRECTORY PATH
    "Your AssetBundle Name", // DEFAULT FILE NAME
    "unity3d" // FILE EXTENSION
  );

  if (selectedPath.Length == 0) return;

下一步是声明一个包含我们选择对象的数组:

  Object[] selectedAssets = Selection.GetFiltered(
    typeof(Object), SelectionMode.DeepAssets
  );

此外,我们必须在循环中处理我们数组中的每个纹理,以便使用 AssetDatabase.GetAssetPath 方法获取该资产源路径。对于纹理,我们必须使用 AssetDatabase.ImportAsset 函数强制进行纹理预处理:

  for (int i=0; i < selectedAssets.Length; i++) {
    Object obj = selectedAssets[i];
    if ((obj is Texture) == false) continue;
    string texturePath = AssetDatabase.GetAssetPath(
      (UnityEngine.Object) obj
    );
    AssetDatabase.ImportAsset(texturePath);
  }

之后,我们必须构建我们的 AssetBundle

  BuildPipeline.BuildAssetBundle(
    Selection.activeObject,
    selectedAssets,
    selectedPath,
    BuildAssetBundleOptions.CollectDependencies |
    BuildAssetBundleOptions.CompleteAssets
  );

在最后阶段,我们可以用我们选择的资产数组初始化 Selection.objects 列表变量,以便查看所有这些:

  Selection.objects = selectedAssets;

现在,我们应该创建一个非常简单的类(这个脚本应该放在我们之前提到的 Editor 文件夹中),它继承自 AssetPostprocessor 类,如下所示:

using UnityEngine;
using UnityEditor;

public class TextureAssetsPreprocessor : AssetPostprocessor {
  void OnPreprocessTexture() {
    TextureImporter ti = assetImporter as TextureImporter;
    ti.textureFormat = TextureFormatAssetBundles.tif;
  }
}

为 Android 设备实时下载新的代码和数据

在从你的包中检索资产时,你可以使用三个不同的函数:

  • AssetBundle.Load: 这将按给定名称加载一个对象;它也会阻塞主线程。

  • AssetBundle.LoadAsync: 这将按给定名称加载一个对象;它不会阻塞主线程。对于大型资产,请使用此方法。

  • AssetBundle.LoadAll: 这将加载你的 AssetBundle 中的所有对象。

在卸载资产时,可以使用 AssetBundle.Unload 方法。让我们看看以下代码中异步方法的简单用法示例,没有任何异常处理和任何检查(就像 skeleton):

using UnityEngine;
using System.Collections;

public class GetAssetBundleAsync : MonoBehaviour {
  public string assetBundleUrl = "http://yourweb.com/yourBundle.unity3d";
  public int assetBundleVersion = 1;

  IEnumerator Start() {
    WWW www = WWW.LoadFromCacheOrDownload(
      assetBundleUrl, assetBundleVersion
    );

    yield return www;

    AssetBundle ab = www.assetBundle;

    AssetBundleRequest abr = ab.LoadAsync(
      "YourObjName", typeof(GameObject)
    );

    yield return abr;

    GameObject go = abr.asset as GameObject;

    ab.Unload(false);
    www.Dispose();
  }
}

管理已加载的资产包

如果之前没有卸载之前的包,则无法加载资产包:

  AssetBundle ab = www.assetBundle;

小贴士

尽量始终保留导入的资产引用,以避免多次导入相同的资产。

Unity 将抛出异常,并且你的资产包变量(在我们的例子中是 ab 变量)将变为 null

小贴士

尽可能早地卸载你的 AssetBundle

你可以使用以下简单的脚本(如下所示)为你加载的包使用。所有这些代码应该对你来说都很清楚,所以让我们看看这个 C# 脚本:

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

static public class YourAssetBundleDispatcher {
  static Dictionary<string, YourBundleReference> dictionaryBundles;

  static YourAssetBundleDispatcher() {
         dictionaryBundles = new Dictionary<string, YourBundleReference>();
  }

     private class YourBundleReference {
         public AssetBundle ab = null;

         public int assetBundleVersion;
         public string assetBundleUrl;

         public YourBundleReference(string url, int version) {
               assetBundleUrl = url;
               assetBundleVersion = version;
         }
     };

  public static AssetBundle 
        RetrieveAssetBundle(string abUrl, int abVersion) {
         string bundleKey = abUrl + abVersion.ToString();

    YourBundleReference ybr;

    if (dictionaryBundles.TryGetValue(bundleKey, out ybr))
               return ybr.ab;
         else
               return null;
     }

     public static IEnumerator ImportAssetBundle(string abUrl, int abVersion){
          string bundleKey = abUrl + abVersion.ToString();

    if (dictionaryBundles.ContainsKey(bundleKey)) {
               yield return null;
         } else {
                using(WWW www = WWW.LoadFromCacheOrDownload(abUrl, abVersion)){
                   yield return www;

                   if (www.error != null)
                         throw new Exception("WWW ERROR:" + www.error);
        YourBundleReference ybr = new YourBundleReference(
          abUrl, abVersion
        );
                   ybr.ab = www.assetBundle;
                   dictionaryBundles.Add(bundleKey, ybr);
                }
         }
  }

  public static void Dispose(string abUrl, int abVersion, bool flag) {
    string bundleKey = abUrl + abVersion.ToString();

    YourBundleReference ybr;

         if (dictionaryBundles.TryGetValue(bundleKey, out ybr)){
               ybr.ab.Unload(flag);
               ybr.ab = null;
               dictionaryBundles.Remove(bundleKey);
         }
     }
}

你可以使用如下所示的调度器类:

using UnityEngine;
using System.Collections;

class DispatcherUsage : MonoBehaviour {
  public string assetBundleUrl;
     public int assetBundleVersion;

     AssetBundle ab;

    void Start() {
    Debug.Log("Importing your Asset Bundle");
            ab = YourAssetBundleDispatcher.RetrieveAssetBundle(
      assetBundleUrl, assetBundleVersion
    );
             if(null != ab) StartCoroutine(ImportAssetBundle());
  }

     IEnumerator ImportAssetBundle() {
         yield return StartCoroutine(
      YourAssetBundleDispatcher.ImportAssetBundle(
        assetBundleUrl, assetBundleVersion
      )
    );

         ab = YourAssetBundleDispatcher.RetrieveAssetBundle(
      assetBundleUrl, assetBundleVersion
    );
  }

  void OnDisable() {
         YourAssetBundleDispatcher.Dispose(
      assetBundleUrl, assetBundleVersion, false
    );
  }
}

注意

有可能通过调用 Unity 的 GameObject.Instantiate 函数来克隆之前实例化的对象,以避免不必要的导入包。

资产包和二进制数据

Unity 将具有 .bytes 扩展名的二进制文件视为 TextAsset,它可以包含在你的 AssetBundle 中。以下是一个显示的 C# 脚本示例:

using UnityEngine;
using System.Collections;

public class BinaryDataExample : MonoBehaviour {
  string assetBundleUrl = 
    "http://yourweb.com/path/to/yourAssetBundle_1.unity3d";

  IEnumerator Start() {
    WWW www = WWW.LoadFromCacheOrDownload(assetBundleUrl, 1);
    yield return www;

    AssetBundle ab = www.assetBundle;

    TextAsset textAsset = ab.Load(
      "YourBinaryFileName", typeof(TextAsset)
    ) as TextAsset;

    byte[] yourBinaryData = textAsset.bytes;
  }
}

资产包和脚本

你可以用脚本作为 TextAsset 文件构建你的资产包,这些文件只有在预编译成程序集时才能执行。以下代码展示了这个示例:

using UnityEngine;
using System.Collections;

public class AssetBundleScript : MonoBehaviour {
  string assetBundleUrl = 
    "http://yourweb.com/your/asset/bundle_test.unity3d";

  IEnumerator Start () {
    WWW www = WWW.LoadFromCacheOrDownload (assetBundleUrl, 1);
        yield return www;

        AssetBundle ab = www.assetBundle;

        TextAsset textAsset = ab.Load(
      "yourBinaryAssetName", typeof(TextAsset)
    ) as TextAsset;

        var assmbl = System.Reflection.Assembly.Load(textAsset.bytes);

    GameObject gameObj = new GameObject();
        gameObj.AddComponent(
      assmbl.GetType(
        "Your_ClassName_Inherited_From_MonoBehaviour"
      )
    );
  }
  }

资产包依赖

许多你的资源将依赖于其他资源,例如材质、纹理、着色器等。你可以构建包含所有这些资源的包,但这种方法可以减小 AssetBundle 文件的大小。此外,如果所有这些依赖项都用于你的其他包,这种方法将不会有效。这将浪费太多内存。相反,你可以创建一个包含所有这些共享依赖项的单独资源包,该资源包将被其他包使用。在使用这些依赖项的情况下,首先,你应该调用名为 BuildPipeline.PushAssetDependencies 的函数,然后你的共享包可以构建用于其他包。因此,在每次新关卡之前,你应该始终调用此函数,告诉 Unity 将你的包放入其堆栈中以便其他包进一步使用。在资源包创建结束时,你应该始终通过名为 BuildPipeline.PopAssetDependencies 的命令清空这个包堆栈。在你的应用程序中,你应该始终导入所有你的共享包,然后才能导入具有这些依赖项的其他包。让我们看看如何保存具有共享着色器的单独 AssetBundle(如图所示)在这个动作中:

using UnityEngine;

public class YourAssetBundleShaders : MonoBehaviour {
        public Shader[] assetBundleShaders;
}

创建一个空的 GameObject 并将此脚本附加到它上,并在你的项目文件中的任何位置将其保存(在填充着色器数组之后)作为一个预制体。接下来的步骤是创建一个 C# 脚本来生成资源包,如图所示,其中 YourAssetBundle_2 需要 YourAssetBundle_1,而 YourAssetBundle_3 依赖于第一个和第二个包。这只是一个示例,你应该根据你的需求进行更改:

using UnityEngine;
using UnityEditor;

public class AssetBundleGenerator {
  [MenuItem("PacktPub/AssetBundles/Generate all accessible Asset Bundles")]
       static void Generate() {
         BuildAssetBundleOptions options = 
                 BuildAssetBundleOptions.CollectDependencies | 
                    BuildAssetBundleOptions.CompleteAssets | 
                    BuildAssetBundleOptions.DeterministicAssetBundle;

    BuildPipeline.PushAssetDependencies();

             BuildPipeline.BuildAssetBundle(
      AssetDatabase.LoadMainAssetAtPath(
        "Assets/YourAssetName_1.prefab"
      ), 
      null, 
      "Your/Path/To/YourAssetBundle_1.unity3d", 
      options
    );

    BuildPipeline.PushAssetDependencies();
    BuildPipeline.BuildAssetBundle(
      AssetDatabase.LoadMainAssetAtPath(
        "Assets/YourAssetName_2.prefab"
      ), 
      null, 
      "YourPath/To/YourAssetBundle_2.unity3d", 
      options
    );

    BuildPipeline.BuildAssetBundle(
      AssetDatabase.LoadMainAssetAtPath(
        "Assets/YourAssetName_3.prefab"
      ), 
      null, 
      "YourPath/To/YourAssetBundle_3.unity3d", 
      options
    );           

    BuildPipeline.PopAssetDependencies();
    BuildPipeline.PopAssetDependencies();
  }

  [MenuItem("PacktPub/AssetBundles/Rebuild Asset Bundle")]
       static void Rebuild() {
    BuildAssetBundleOptions options = 
      BuildAssetBundleOptions.CollectDependencies | 
      BuildAssetBundleOptions.CompleteAssets | 
      BuildAssetBundleOptions.DeterministicAssetBundle;

    BuildPipeline.PushAssetDependencies();
    BuildPipeline.BuildAssetBundle(
      AssetDatabase.LoadMainAssetAtPath(
        "Assets/YourAssetBundleName_1.prefab"
      ), 
      null, 
      "YourPath/To/YourAssetBundle_1.unity3d", 
      options
    );

    BuildPipeline.PopAssetDependencies();
  }
  }

实践中的安全性技术

接下来,C# 脚本示例将涵盖如何保护你的资源包的内容(如图所示):

using UnityEngine;
using System.Collections;

public class AssetBundleSecurityFirst : MonoBehaviour {
  string assetBundleUrl = 
    "http://yourweb.com/path/to/yourAssetBundle.unity3d";

  IEnumerator Start() {
    WWW www = WWW.LoadFromCacheOrDownload(assetBundleUrl, 1);
    yield return www;

    TextAsset textAsset = www.assetBundle.Load(
      "YourEncryptedAssetName", typeof(TextAsset)
    ) as TextAsset;

    /*byte[] yourDecryptedBytes = AnyDecryptionFunction(
      textAsset.bytes // your encrypted bytes
    );*/
  }
}

另一种安全的方式是加密整个资源包,而不是仅仅加密 TextAsset 数据,如图中所示的前一个代码。或者,在此方法中,你不能使用 WWW.LoadFromCacheOrDownload 方法。你始终需要从 WWW 流中导入你的包,如图中所示的下述代码:

using UnityEngine;
using System.Collections;

public class AssetBundleSecuritySecond : MonoBehaviour {
  string assetBundleUrl = 
    "http://yourweb.com/path/to/yourAssetBundle.unity3d";

  IEnumerator Start () {
    WWW www = new WWW(assetBundleUrl);
    yield return www;

    /*byte[] yourEncryptedBytes = www.bytes;
    byte[] yourDecryptedBytes = 
      AnyDecryptionFunction(yourEncryptedBytes);

    AssetBundleCreateRequest assetBundleCreateRequest = 
      AssetBundle.CreateFromMemory(yourDecryptedBytes);

    yield return assetBundleCreateRequest;

    AssetBundle ab = assetBundleCreateRequest.assetBundle;*/

    // Here you can use your AssetBundle. 
    // The AssetBundle was not cached.
  }
}

最后且最好的保护方法是将你的加密 AssetBundle 作为 TextAsset 放置在另一个(未加密的)AssetBundle 内部。因此,我们可以像这里所示一样使用 Unity 的缓存系统来处理我们的资源包:

using UnityEngine;
using System.Collections;

public class AssetBundleSecurityThird : MonoBehaviour {
  string assetBundleUrl = 
    "http://yourweb.com/path/to/yourAssetBundle.unity3d";

  IEnumerator Start() {
    WWW www = WWW.LoadFromCacheOrDownload(assetBundleUrl, 1);
    yield return www;

    TextAsset textAsset = www.assetBundle.Load(
      "YourEncryptedAsset", typeof(TextAsset)
    ) as TextAsset;

    /*byte[] yourEncryptedBytes = textAsset.bytes;
    byte[] yourDecryptedBytes = 
      AnyDecryptionFunction(yourEncryptedBytes);

    AssetBundleCreateRequest assetBundleCreateRequest = 
      AssetBundle.CreateFromMemory(yourDecryptedBytes);
    yield return assetBundleCreateRequest;

    AssetBundle ab = assetBundleCreateRequest.assetBundle;*/
    // Here you can use your AssetBundle. The AssetBundle was cached.
  }
}

摘要

在本章中,我们发现了资源包。这里有很多代码示例。我们学习了如何构建和导入你的资源包,以及如何通过不同的方法加密资源包中的数据。此外,在本章中,我们还探讨了如何创建和使用你的 AssetBundle 依赖项,以及如何使用二进制数据与资源包和可执行脚本。

在下一章中,我们将介绍不同的优化技术。你将在实践中学习如何使用遮挡剔除和细节级别优化技术。你将看到如何优化原生 C#和 Unity 脚本。最后,你将了解如何将原生 C#和 JavaScript 代码转换为 Unity 脚本。

第六章。优化和转换技术

本章将向你介绍遮挡剔除OC)和细节级别在优化技术中的使用。此外,你将学习如何优化 Unity C#和 Unity JS 代码。最后,你将看到如何将 Unity C#代码转换为 Unity JavaScript 代码,反之亦然。

本章将涵盖以下主题:

  • 遮挡剔除和优化技术中的细节级别

  • Unity C#和 Unity JS 优化技巧和窍门

  • 将 Unity C#代码转换为 Unity JavaScript 代码,反之亦然

遮挡剔除和优化技术中的细节级别

让我们更仔细地看看 Unity 中遮挡剔除的基本原理(仅限 Pro 许可证)以及如何在项目中使用它们以实现出色的性能。

你可以从 Unity 菜单中打开遮挡剔除编辑器,如以下截图所示:

遮挡剔除和优化技术中的细节级别

遮挡剔除机制的主要目的是筛选和过滤掉相机区域中不可见的对象,以提高优化效果。这主要意味着对象只有在必要时才会使用资源,从而帮助你创建一个运行速度更快的游戏或应用。

棱锥剔除与遮挡剔除不同,因为它禁用了相机视图外的渲染器,但不会禁用与其他渲染器重叠的渲染器;例如,如果一堵墙遮挡了一个对象,它对相机将是不可见的。使用遮挡剔除,你可以自动利用棱锥剔除的优势。通过使用视觉遮挡剔除技术,我们可以在以下两张截图中的两个示例中看到:

遮挡剔除和优化技术中的细节级别

在这里显示的截图中,你可以看到遮挡剔除的效果:

遮挡剔除和优化技术中的细节级别

Unity 中的遮挡剔除过程使用一个虚拟相机来扫描整个场景,并创建一组可能可见的对象的层次结构。然后,这些信息将在你的游戏或应用中实时使用,以减少绘制调用次数并提高性能。

为了使用遮挡剔除,你需要为场景中要处理的每个对象设置遮挡静态标签。此外,你还可以使用另一个对象的标签,称为被遮挡静态,如下一张截图所示。被遮挡的对象可能被其他对象遮挡,并在类似情况下被禁用以提高性能,但这些对象不能与其他对象重叠。因此,它们将提高你整个项目的性能。

遮挡剔除和优化技术中的细节级别

注意

在相机将要渲染对象的地方创建遮挡剔除区域也非常重要。

我们刚刚介绍了通过遮挡剔除方法进行优化的基本和关键方面。我们无法在本章中描述所有设置和功能的细节。以下章节将描述各种想法、方法、方法和优化和改进性能的方式。下一节的目的是指引你走上提高性能的正确道路。你将使用本章中描述的所需优化技术,如果需要,你可以从网络上找到有关方法、实现和定制的更详细信息。现在让我们考虑另一种称为细节级别LOD)的优化技术。

通过 LOD 进行优化

LOD 优化技术是一种通过减少场景中总的多边形、纹理和其他资源数量来降低帧渲染复杂性的方法,总体上降低其复杂性。一个简单的例子是,主要角色模型由 10,000 个多边形组成。在处理阶段靠近相机的情况下,使用所有多边形很重要。然而,在远离相机的地方,在最终图像中,它只占用几个像素;处理所有 10,000 个多边形是没有意义的。也许在这种情况下,几百个多边形,甚至几块和纹理,专门为大约相同的显示模型准备,就足够了。相应地,在中等距离处,使用由比最简单模型多、比最复杂模型少的三角形组成的模型是有意义的。

LOD 方法通常用于使用多个难度级别(几何或其他)来建模和渲染三维场景,这些难度级别与对象与相机之间的距离成比例。改变复杂性,尤其是在模型中的三角形数量,可以自动进行,基于最高复杂性的三维模型,但也可以基于几个预定义的具有不同细节级别的模式。使用具有较少细节的模型在不同距离处,你可以减少渲染设计复杂性,几乎不会影响整体图像的细节。

当场景中的对象数量很大,并且它们位于与相机不同距离的位置时,这种方法特别有效。例如,考虑一个体育游戏,如足球游戏或冰球模拟器。当角色远离相机时,使用低多边形角色模型,但当它靠近时,模型被大量多边形所取代。这个例子非常简单,它表明该方法的核心是基于模型的两个细节级别,但没有人费心创建多个细节级别。为了产生变化,LOD 级别不应过于明显,因此对象细节逐渐增加。

考虑以下影响细节级别技术水平的因素:屏幕上对象的总数(当画面中有一个或两个角色时,使用复杂模型,而当有 10-20 个角色时,切换到更简单的模型)或帧率(预定的有限帧率值,随着细节级别的变化而变化,例如帧率低于 30 会降低屏幕上模型的复杂性,而 60 帧则提高复杂性)。影响细节级别水平的其他可能因素如下:对象移动的速度(例如,对于正在移动的火箭,你看到它快速移动,但蜗牛则缓慢移动),从游戏的角度来看角色的重要性(例如,在足球中,你看到最接近且最常使用的玩家模型通常使用更复杂的几何和纹理)。这都取决于特定开发者的愿望和能力。最重要的是不要过度;频繁且明显的细节级别变化会令人烦恼。

我们想提醒您,细节级别不一定仅指几何形状。此方法还可以用于节省其他资源:纹理(尽管 GPU 使用米派映射,但有时在动态地改变纹理以保持某些细节方面是有意义的),照明技术(靠近的对象使用复杂算法,而远处的对象使用主照明),以及纹理技术。

Unity C#和 Unity JS 优化技巧和窍门

首先,我们将考虑与 JavaScript 编程语言相关的优化方面的某些方面。尽量在 JavaScript 中避免使用动态类型。无疑,对性能最好的解决方案是静态类型。变量的动态类型使用会在执行代码时消耗,以找到特定变量的适当数据类型,这在原则上可以也应该通过指定所有变量的数据类型来避免。不良和良好的示例如下:

// Dynamic Typing, BAD FOR YOUR PERFORMANCE
var yourVariableName = 23;

// Static Typing, GOOD FOR YOUR PERFORMANCE
var myGo : GameObject = null;

以下示例展示了如果你想要提高性能,你不应该做什么。这个例子使用动态类型来定义我们的变量yourVariableName,这反过来又以负面方式影响了整个系统的性能。在调用此对象任何函数之前,都会花费时间搜索正确的对象类型并检查被调用的函数是否可访问。不良的示例如下:

function Start() {
  var yourVariableName = GetComponent(YourScriptName);
  yourVariableName.YourFunctionName();
}

为了避免在不必要的开销上浪费 CPU 时间,你应该始终为所有变量使用静态类型以提高性能:

function Start() {
  var yourVariableName : YourScriptName = GetComponent(YourScriptName);
  yourVariableName.YourFunctionName();
}

小贴士

尽可能使用静态类型而不是动态类型

你可以使用#pragma严格预处理器指令来帮助你记住在 JavaScript 脚本中始终使用静态类型而不是动态类型。你应该在脚本顶部,在任意代码之前写入此指令。如果在脚本中使用了#pragma严格和动态类型,编译器将抛出错误。因此,这个预处理器指令强制你只使用静态类型。

我们还想提及其他优化代码的技术。其中之一是缓存组件或变量的技术。在优化过程中,你首先需要关注代码中经常执行的功能,特别是像UpdateFixedUpdate这样的回调函数,以及每帧或几乎每帧都会调用的类似函数。换句话说,这些函数每秒会被调用很多次。因此,在这些高风险函数中对任何组件或变量的引用取决于具体情况。当然,对于整体系统性能来说,有些情况下这些操作对性能影响不大,而有些情况下,由于许多不必要的开销,性能几乎会降到零。在这些函数中,最好每次不要调用 Unity 方法GetComponent,因为这会非常频繁地查找组件或其他 Unity 库中的类似函数来查找对象等。相反,你可以在需要获取所需组件或对象(s)时调用所需的功能,并将它们存储在局部变量或数组中,就像你喜欢的样子。以下示例展示了这一点:

// BAD for your performance
void Update() {
  transform.position = new Vector3(0.0f, 1.0f, -1.0f);
}

// Second example:
// GOOD for your performance
private Transform _t;
void Start() {
  _t = transform;
}

void Update() {
  _t.position = new Vector3(0.0f, 1.0f, -1.0f);
}

第二个示例中显示的代码比第一个示例中的相应代码快得多,因为 Unity 不会在每次Update周期或每帧查找 transform 组件。

小贴士

你应该只在必要时调用函数,不多也不少,正好在需要的时候。

对你的代码和整个系统来说,最好的优化是代码尽可能小,或者说根本不执行任何不必要的操作。不必要的计算会导致不必要的开销;对于移动设备来说,这个问题通常是最尖锐的。以下示例展示了一个小优化示例,但这并不是你性能的最佳解决方案。在每一帧之后,检查两点之间的距离会消耗你宝贵的时间:

private Transform yourTransform:
void Update() {
  if (Vector3.Distance(yourTransform.position, transform.position) > 200) {
    return;
  }
  // your next code may be here ...
}

为了不浪费时间去纠正不必要的错误,你应该使用OnBecameInvisibleOnBecameVisible回调。有了这些回调,Unity 会在没有相机能看到(对于OnBecameInvisible)或至少有一个相机能看到(对于OnBecameVisible)你的渲染器的事件中调用。当然,这些回调在某些情况下很有用,在其他情况下则不然。例如,如果你的对象不包含渲染器组件,那么你需要想出一个方法来相应地启用或禁用代码的执行。这两个回调的一个简单示例如下:

void OnBecameVisible() {
  enabled = true;
}

void OnBecameInvisible() {
  enabled = false;
}

为了达到所需的性能,你需要在代码中注意许多细节,以及本书中讨论的其他许多细节。在大多数情况下,代码优化会阻碍可读性,从而阻碍对代码的理解。请记住这一点,或者至少不要忘记。就像生活中的其他事情一样,我们需要找到一个平衡点,换句话说,就是质量和性能之间的黄金平衡。

让我们看看静态函数的行为以及我们需要多少时间来处理它们,因为与调用非静态函数相比,使用这些函数可以显著减少函数调用的时间。如果我们检查编译代码时静态函数会发生什么的问题,然而,这并不是什么秘密,所有的代码都会被转换成机器代码或汇编代码,这是最低的编程级别。如果我们考虑调用静态函数的汇编指令,我们会看到它需要的机器指令比调用非静态函数少,因此 CPU 时间也更少。

在调用中,每个通过值传递参数的函数都需要内存复制。如前所述,这可能会影响你的性能。因此,最好通过引用而不是值来调用函数。解决这个问题很容易。提高性能的最好方法是使用在函数中使用的类或对象的局部变量。你可以在函数内部创建一组变量,这反过来又会显著增加具有许多变量的函数调用对内存和 CPU 时间的消耗。函数必须在栈上记住,以便有进一步操作这些变量的机会。即使这些变量在函数中没有使用,它们仍然在栈上,并且会占用内存空间。

以下讨论将集中在常量上。常量不需要 RAM 分配,因为它们的值直接嵌入到指令流中。使用常量而不是创建大量局部或全局变量可以显著提高软件的性能,避免内存和 CPU 时间的开销。

静态变量(类的变量)以及静态函数(类的方法)需要的 CPU 时间较少,因为静态变量属于整个类,而不是属于此类的一个对象。用于搜索资源的时间正在减少,这在优化中具有明显的优势。对于任何对象的变量或函数,机器指令将被执行以定位它们所属的适当对象,这显然需要 CPU 时间和内存的开销。

ifswitch 语句可以很容易地相互替换;例如,为了提高代码的可理解和可读性,或者优化所有相同的代码。如果你通过任何反汇编器查看编译后的机器指令和方向,你可以看到这两个表达式的区别。例如,switch 语句在编译后成为 ago-to 机制,这反过来又使得在转换表中通过机器指令进行跳转。它首先需要找到所需的转换,然后是那些前往命令汇编的部分。如果构造的行为在低位不同,它将像在高级编程语言中一样表现为正常分支;例如,在我们的案例中,C# 语言。在某些情况下,某些 switch 设计可能比相同的 if/else if/else 设计执行得更快。这些两种结构的性能完全取决于它们的正确应用,换句话说,正确的使用。例如,让我们考虑两个简单的案例,其中第一个案例将会有更快更有效的 switch,而在第二种情况下,if 设计在性能上优于 switch 解决方案,如下面的代码示例所示:

using UnityEngine;
using System.Diagnostics;

public class IfSwitchTestFirstCase : MonoBehaviour {
  public const int CYCLES_COUNTER = 100000000;

  bool IfTest(int yourIntegerExample)
  {
    if (yourIntegerExample == 0 || yourIntegerExample == 1) {
      return true;
    }

    if (yourIntegerExample == 2 || yourIntegerExample == 3) {
      return false;
    }

    if (yourIntegerExample == 4 || yourIntegerExample == 5) {
      return true;
    }

    return false;
  }

  bool SwitchTest(int yourIntegerExample)
  {
    switch (yourIntegerExample)
    {
      case 0:
      case 1:
        return true;

      case 2:
      case 3:
        return false;

      case 4:
      case 5:
        return true;

      default:
        return false;
    }
  }

  void Start() {
    Stopwatch ifTimer = Stopwatch.StartNew();
    for (int i = 0; i < CYCLES_COUNTER; i++)
    {
      IfTest(i);
    }
    ifTimer.Stop();

    Stopwatch switchTimer = Stopwatch.StartNew();
    for (int i = 0; i < CYCLES_COUNTER; i++)
    {
      SwitchTest(i);
    }
    switchTimer.Stop();

    UnityEngine.Debug.Log(
      "IF time = " +
      (
(double)(ifTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES_COUNTER
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Switch time = " +
      (
(double)(switchTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES_COUNTER
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X,Intel Core i5 2.7 GHz 系统上,在 Unity 编辑器中测试第一个示例后,结果如下:

IF time = 11.54 nanoseconds average per cycle
Switch time = 8.76 nanoseconds average per cycle

基于前面的结果,我们可以说 switch 的设计对性能的影响更好,但这并不总是正确的。现在让我们考虑第二种情况,其中 if 结构证明是优化设计的最佳解决方案,如下面的代码示例所示:

using UnityEngine;
using System.Diagnostics;

public class IfSwitchTestSecondCase : MonoBehaviour {
  public const int CYCLES_COUNTER = 100000000;

  int SwitchTest(int yourIntegerExample)
  {
    switch (yourIntegerExample)
    {
      case 0:
      {
        return 11;
      }

      case 1:
      {
        return 22;
      }

      default:
      {
        return -11;
      }
    }
  }

  int IfTest(int yourIntegerExample)
  {
    if (0 == yourIntegerExample)
    {
      return 11;
    }

    if (1 == yourIntegerExample)
    {
      return 22;
    }

    return -11;
  }

  void Start() {
    Stopwatch switchTimer = Stopwatch.StartNew();
    for (int i = 0; i < CYCLES_COUNTER; i++)
    {
      SwitchTest(0);
      SwitchTest(0);
      SwitchTest(0);
      SwitchTest(0);
      SwitchTest(0);
      SwitchTest(0);
      SwitchTest(1);
      SwitchTest(1);
      SwitchTest(1);
      SwitchTest(1);
    }
    switchTimer.Stop();

    Stopwatch ifTimer = Stopwatch.StartNew();
    for (int i = 0; i < CYCLES_COUNTER; i++)
    {
      IfTest(0);
      IfTest(0);
      IfTest(0);
      IfTest(0);
      IfTest(0);
      IfTest(0);
      IfTest(1);
      IfTest(1);
      IfTest(1);
      IfTest(1);
    }
    ifTimer.Stop();

    UnityEngine.Debug.Log(
      "IF time = " +
      (
(double)(ifTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES_COUNTER
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Switch time = " +
      (
(double)(switchTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES_COUNTER
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X,Intel Core i5 2.7 GHz 系统上,在 Unity 编辑器中测试第二个示例后,结果如下:

IF time = 54.46 nanoseconds average per cycle
Switch time = 64.24 nanoseconds average per cycle

由于不同的情况需要不同的设计,最重要的事情是你在编译代码后要理解在建筑机器中发生的事情的真正含义。然后,你将更容易做出正确的选择来改进和提升你的性能。在前面讨论的两个例子中,我们看到了在不同的情境下,看似完全等价的ifswitch设计,在性能方面可能会有速度和效率上的差异。我们也看到,不同的情境会因不同的设计而获得性能优势,尽管它们具有相同的语义,或者说,以不同形式设计的相同算法。然而,当处理任何问题时,意义不会改变,除了我们已经考虑过的执行时间。

以下二维数组可以用单维数组的形式使用,这将提高你的性能。例如,我们有一个具有N行和M列的二维数组:表格大小是 N × M:

// [i, j] from float 2D array (table)
// 0 ≤ i ≤ N - 1
// 0 ≤ j ≤ M - 1
float2Darray[i, j] = 123.321f;

在将二维数组优化为单维数组的情况下,我们可以这样引用我们大小为 N × M 的表格中的元素(i, j):

// [i, j] from float 1D array
// 0 ≤ i ≤ N - 1
// 0 ≤ j ≤ M - 1
float1Darray[(i * M) + j] = 123.321f;

下面是一个完整的 Unity C#示例代码,如下所示:

using UnityEngine;
using System.Diagnostics;

public class Array2Dvs1D : MonoBehaviour {
  public const int N = 1000, M = 1500;

  float[,] float2Darray;
  float[] float1Darray;

  void Start() {
    float2Darray = new float[N, M];
    float1Darray = new float[N * M];

    Stopwatch array2DTimer = Stopwatch.StartNew();
    for (int i = 0; i < N; i++)
    {
      for (int j = 0; j < M; j++)
      {
        // [i, j] from float 2D array
        // 0 ≤ i ≤ N - 1
        // 0 ≤ j ≤ M - 1
        float2Darray[i, j] = 123.321f;
      }
    }
    array2DTimer.Stop();

    Stopwatch array1DTimer = Stopwatch.StartNew();
    for (int i = 0; i < N; i++)
    {
      for (int j = 0; j < M; j++)
      {
        // [i, j] from float 1D array
        // 0 ≤ i ≤ N - 1
        // 0 ≤ j ≤ M - 1
        float1Darray[(i * M) + j] = 123.321f;
      }
    }
    array1DTimer.Stop();

    UnityEngine.Debug.Log(
      "Array 1D time = " +
      (
(double)(array1DTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / (N * M)
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Array 2D time = " +
      (
(double)(array2DTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / (N * M)
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X 上,Intel Core i5 2.7 GHz,在 Unity 编辑器中测试此示例后,得到了以下结果:

Array 1D time = 3.24 nanoseconds average per cycle
Array 2D time = 7.87 nanoseconds average per cycle

如我们所见,相同思想的实现存在差异,但形式不同,如二维数组和单维数组。也考虑以下简单的示例代码,如下所示,它展示了与单维数组相比的两级数组在执行速度上的比较:

using UnityEngine;
using System.Diagnostics;

public class LeveledArray2Dvs1D : MonoBehaviour {
  public const int N = 1000, M = 1500;

  float[][] float2Darray;
  float[] float1Darray;

  void Start() {
    float2Darray = new float[N][];
    float1Darray = new float[N * M];

    for (int i = 0; i < N; i++)
    {
      float2Darray[i] = new float[M];
    }

    Stopwatch array2DTimer = Stopwatch.StartNew();
    for (int i = 0; i < N; i++)
    {
      for (int j = 0; j < M; j++)
      {
        // [i][j] from float 2D array
        // 0 ≤ i ≤ N - 1
        // 0 ≤ j ≤ M - 1
        float2Darray[i][j] = 123.321f;
      }
    }
    array2DTimer.Stop();

    Stopwatch array1DTimer = Stopwatch.StartNew();
    for (int i = 0; i < N; i++)
    {
      for (int j = 0; j < M; j++)
      {
        // [i, j] from float 1D array
        // 0 ≤ i ≤ N - 1
        // 0 ≤ j ≤ M - 1
        float1Darray[(i * M) + j] = 123.321f;
      }
    }
    array1DTimer.Stop();

    UnityEngine.Debug.Log(
      "Leveled Array 1D time = " +
      (
(double)(array1DTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / (N * M)
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Leveled Array 2D time = " +
      (
(double)(array2DTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / (N * M)
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X 上,Intel Core i5 2.7 GHz,在 Unity 编辑器中测试此示例后,得到了以下结果:

Leveled Array 1D time = 3.23 nanoseconds average per cycle
Leveled Array 2D time = 3.36 nanoseconds average per cycle

为了方便起见,如果你对它的性能满意,你可以使用两级数组。你需要从你的任务开始做出正确的决定,不要忘记代码的可读性和性能之间的平衡点。

至于字符串和字符数组,让我们看看它们中哪一个更快、更高效。在下面的代码示例中,我们展示了两个变量之间的测试性能:

using UnityEngine;
using System.Diagnostics;

public class StringCharArray : MonoBehaviour {
  public const int LENGTH = 1000;

  string str;
  char[] charArray;

  void Start() {
    charArray = new char[LENGTH];

    Stopwatch charArrayTimer = Stopwatch.StartNew();
    for (int i = 0; i < LENGTH; i++)
    {
      charArray[i] = (i % 10).ToString()[0];
    }
    charArrayTimer.Stop();

    str = string.Empty;
    Stopwatch stringTimer = Stopwatch.StartNew();
    for (int i = 0; i < LENGTH; i++)
    {
      str += (i % 10).ToString();
    }
    stringTimer.Stop();

    UnityEngine.Debug.Log(
      "String time = " +
      (
(double)(stringTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / LENGTH
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Char Array time = " +
      (
(double)(charArrayTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / LENGTH
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X 上,Intel Core i5 2.7 GHz,在 Unity 编辑器中测试此示例后,得到了以下结果:

String time = 1274.00 nanoseconds average per cycle
Char Array time = 369.00 nanoseconds average per cycle

差异很明显,但这个优化的可读性迅速下降。就像生活中的一切都需要一个强大的资产负债表,或者说,黄金法则。下面是另一个例子,其中我们比较了StringBuilder和字符数组的表现,如下所示:

using UnityEngine;
using System.Text;
using System.Diagnostics;

public class StringBuilderCharArray : MonoBehaviour {
  public const int LENGTH = 1000;

  StringBuilder str;
  char[] charArray;

  void Start() {
    charArray = new char[LENGTH];

    Stopwatch charArrayTimer = Stopwatch.StartNew();
    for (int i = 0; i < LENGTH; i++)
    {
      charArray[i] = (i % 10).ToString()[0];
    }
    charArrayTimer.Stop();

    str = new StringBuilder();
    Stopwatch stringBuilderTimer = Stopwatch.StartNew();
    for (int i = 0; i < LENGTH; i++)
    {
      str.Append((i % 10).ToString());
    }
    stringBuilderTimer.Stop();

    UnityEngine.Debug.Log(
      "String Builder time = " +
      (
(double)(stringBuilderTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / LENGTH
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Char Array time = " +
      (
(double)(charArrayTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / LENGTH
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X 上,Intel Core i5 2.7 GHz,在 Unity 编辑器中测试此示例后,得到了以下结果:

String Builder time = 463.00 nanoseconds average per cycle
Char Array time = 370.00 nanoseconds average per cycle

StringBuilder的性能略逊于字符数组。然而,不要忘记,对于垃圾回收器来说,StringBuilder进行了非常好的优化,并且不会因为大量数据而产生内存泄漏。在软件开发过程中,你必须解决各种问题。如果你对每一个决定都充满信心并坚定地接受任何批评,那么成功就不会太远。你绝对应该优先考虑你的任务,让所有人都看到你需要在哪里做出妥协。

在下一步中,我们将检查和研究 C# 中集合的性能。在某些情况下,集合非常有用,但你始终要记住,它是对普通数组的包装。当使用大量数据时,集合可能会利用大量的处理时间成本,这反过来又会对你的代码的整体性能产生负面影响。在以下代码示例中,列表集合的执行速度与传统的单维数组进行了比较:

using UnityEngine;
using System.Collections.Generic;
using System.Diagnostics;

public class ListVsArray : MonoBehaviour {
  public const int LENGTH = 1000000;

  List<int> intList;
  int[] intArray;

  int tmpInt;

  void Start() {
    intList = new List<int>();
    intArray = new int[LENGTH];

    Stopwatch intArrayTimer = Stopwatch.StartNew();
    for (int i = 0; i < LENGTH; i++)
    {
      intArray[i] = i;
      tmpInt = intArray[i]++;
    }
    intArrayTimer.Stop();

    Stopwatch listTimer = Stopwatch.StartNew();
    for (int i = 0; i < LENGTH; i++)
    {
      intList.Add(i);
      tmpInt = intList[intList.Count - 1]++;
    }
    listTimer.Stop();

    UnityEngine.Debug.Log(
      "Integer List time = " +
      (
(double)(listTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / LENGTH
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Integer Array time = " +
      (
(double)(intArrayTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / LENGTH
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X,Intel Core i5 2.7 GHz 上,在 Unity 编辑器中测试此示例后,得到了以下结果:

Integer List time = 36.68 nanoseconds average per cycle
Integer Array time = 5.54 nanoseconds average per cycle

如前文所述的结果所示,与简单的单维数组相比,列表集合在性能上明显较差。接下来,如下面的代码示例所示,ArrayList类的性能与相同的简单单维数组进行了比较:

using UnityEngine;
using System.Collections;
using System.Diagnostics;

public class ArrayListVsArray : MonoBehaviour {
  public const int LENGTH = 1000000;

  ArrayList intArrayList;
  int[] intArray;

  int tmpInt;

  void Start() {
    intArrayList = new ArrayList();
    intArray = new int[LENGTH];

    Stopwatch intArrayTimer = Stopwatch.StartNew();
    for (int i = 0; i < LENGTH; i++)
    {
      intArray[i] = i;
      tmpInt = intArray[i] + 23;
    }
    intArrayTimer.Stop();

    Stopwatch arrayListTimer = Stopwatch.StartNew();
    for (int i = 0; i < LENGTH; i++)
    {
      intArrayList.Add(i);
      tmpInt = (int)intArrayList[intArrayList.Count - 1] + 23;
    }
    arrayListTimer.Stop();

    UnityEngine.Debug.Log(
      "Integer Array List time = " +
      (
(double)(arrayListTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / LENGTH
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Integer Array time = " +
      (
(double)(intArrayTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / LENGTH
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X,Intel Core i5 2.7 GHz 上,在 Unity 编辑器中测试此示例后,得到了以下结果:

Integer Array List time = 183.36 nanoseconds average per cycle
Integer Array time = 4.78 nanoseconds average per cycle

与前一个示例相比,差异惊人,并且在使用List类时更为明显。因此,我们展示了简单单维数组与集合相比的明显优势,当处理大量数据时,这可能会极大地破坏你的性能。对于简单单维数组来说,不能说的就是它们是各种集合的构建块。一如既往,选择权在你。最重要的是不要忘记优化决策中的基本公理。让我们看看另一个示例,这里以Dictionary类为例,如代码示例所示:

using UnityEngine;
using System.Collections.Generic;
using System.Diagnostics;

public class DictionaryVsArray : MonoBehaviour {
  public const int CYCLES = 1000000;

  Dictionary<int, int> dictionary;
  int[] intArray;

  int tmpInt;

  void Start() {
    dictionary = new Dictionary<int, int>();
    intArray = new int[CYCLES];

    Stopwatch intArrayTimer = Stopwatch.StartNew();
    for (int i = 0; i < CYCLES; i++)
    {
      intArray[i] = i + 117;
      tmpInt = intArray[i] + 23;
    }
    intArrayTimer.Stop();

    Stopwatch dictionaryTimer = Stopwatch.StartNew();
    for (int i = 0; i < CYCLES; i++)
    {
      dictionary.Add(i, i + 117);
      tmpInt = (int)dictionary[dictionary.Count - 1] + 23;
    }
    dictionaryTimer.Stop();

    UnityEngine.Debug.Log(
      "Integer Dictionary time = " +
      (
(double)(dictionaryTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Integer Array time = " +
      (
(double)(intArrayTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X,Intel Core i5 2.7 GHz 上,在 Unity 编辑器中测试此示例后,得到了以下结果:

Integer Dictionary time = 132.75 nanoseconds average per cycle
Integer Array time = 4.63 nanoseconds average per cycle

此外,我还想向你展示以下使用Hashtable集合进行性能测试的代码示例,如代码示例所示:

using UnityEngine;
using System.Collections;
using System.Diagnostics;

public class HashtableVsArray : MonoBehaviour {
  public const int CYCLES = 1000000;

  Hashtable hashtable;
  int[] intArray;

  int tmpInt;

  void Start() {
    hashtable = new Hashtable();
    intArray = new int[CYCLES];

    Stopwatch intArrayTimer = Stopwatch.StartNew();
    for (int i = 0; i < CYCLES; i++)
    {
      intArray[i] = i + 117;
      tmpInt = intArray[i] + 23;
    }
    intArrayTimer.Stop();

    Stopwatch hashtableTimer = Stopwatch.StartNew();
    for (int i = 0; i < CYCLES; i++)
    {
      hashtable.Add(i, i + 117);
      tmpInt = (int)hashtable[hashtable.Count - 1] + 23;
    }
    hashtableTimer.Stop();

    UnityEngine.Debug.Log(
      "Integer Hashtable time = " +
      (
(double)(hashtableTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Integer Array time = " +
      (
(double)(intArrayTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X,Intel Core i5 2.7 GHz 上,在 Unity 编辑器中测试此示例后,得到了以下结果:

Integer Hashtable time = 539.59 nanoseconds average per cycle
Integer Array time = 4.52 nanoseconds average per cycle

如你所见,所有集合在性能上都显著劣于简单的一维数组,但在许多情况下,由于代码的可读性更好,因此具有优势。然而,你将失去大量的 CPU 时间和内存,并不得不牺牲代码的清晰度。对于其他集合和所有其他你感兴趣的结构,你可以很容易地根据前面讨论的示例对你的系统性能进行自己的测试。

我们不会忽略在代码分支结构中经常使用的循环。与 foreach 等其他循环相比,forwhiledo-while 循环是最快的。使用循环时的另一个技巧是,我们可以像这里的代码示例所示的那样减少循环的迭代次数:

using UnityEngine;
using System.Diagnostics;

public class LoopsTest : MonoBehaviour {
  public const int CYCLES = 1000000;

  int [] tmpInt;
  int i, _optimizedCycles;

  void Start() {
    tmpInt = new int[CYCLES];

    Stopwatch doWhileLoopTimer = Stopwatch.StartNew();
    i = 0;
    do
    {
      // do while loop test
      tmpInt[i] = i + 123;
      i++;
    } while (i < CYCLES);
    doWhileLoopTimer.Stop();

    Stopwatch whileLoopTimer = Stopwatch.StartNew();
    i = 0;
    while (i < CYCLES)
    {
      // while loop test
      tmpInt[i] = i + 123;
      i++;
    }
    whileLoopTimer.Stop();

    Stopwatch forLoopTimer = Stopwatch.StartNew();
    for (i = 0; i < CYCLES; i++)
    {
      // for loop test
      tmpInt[i] = i + 123;
    }
    forLoopTimer.Stop();

    _optimizedCycles = Mathf.CeilToInt(CYCLES / 5);
    Stopwatch optimizedTimer = Stopwatch.StartNew();
    for (i = 0; i < _optimizedCycles; i++)
    {
      // optimized for loop test
      tmpInt[i*5] = i*5 + 123;
      if (CYCLES > i*5+1) tmpInt[i*5+1] = i*5 + 124;
      if (CYCLES > i*5+2) tmpInt[i*5+2] = i*5 + 125;
      if (CYCLES > i*5+3) tmpInt[i*5+3] = i*5 + 126;
      if (CYCLES > i*5+4) tmpInt[i*5+4] = i*5 + 127;
    }
    optimizedTimer.Stop();

    Stopwatch foreachTimer = Stopwatch.StartNew();
    i = tmpInt.Length - 1;
    foreach (int intElement in tmpInt)
    {
      // foreach test
      tmpInt[i] = intElement;
      i--;
    }
    foreachTimer.Stop();

    UnityEngine.Debug.Log(
      "Optimized For loop time = " +
      (
(double)(optimizedTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "For loop time = " +
      (
(double)(forLoopTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "While loop time = " +
      (
(double)(whileLoopTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Do While loop time = " +
      (
(double)(doWhileLoopTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Foreach time = " +
      (
(double)(foreachTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X 上,Intel Core i5 2.7 GHz,在 Unity 编辑器中测试此示例后,得到了以下结果:

Optimized For loop time = 2.89 nanoseconds average per cycle
For loop time = 3.63 nanoseconds average per cycle
While loop time = 3.72 nanoseconds average per cycle
Do While loop time = 3.72 nanoseconds average per cycle
Foreach time = 5.62 nanoseconds average per cycle

结果不言而喻。因此,不要忘记优化你的循环。第一步是关注那些有大量迭代的循环,因为它们可能会降低你的性能几个数量级。在优化任何东西之前,你需要找到你代码中的瓶颈,然后才能决定你更喜欢或需要的优化技术。

至于 foreach 循环,我们建议你只在特殊情况下使用这个循环。让我们看看每个循环的小样本代码,以及它如何转换成完全不同的代码循环。下一个示例展示了一个简单的 foreach 循环:

foreach (YourType yt in yourCollection) 
{
  yt.YourAction();
}

接下来,让我们看看 foreach 循环中的一段代码会发生什么。正如我们在这里的代码示例中所看到的,我们的循环在使用了枚举对象后变成了一个 while 循环。代码如下:

using (YourType.Enumerator e = this.yourCollection.GetEnumerator()) 
{
  while (e.MoveNext())
  {
    YourType yt = (YourType)e.Current;
    yt.YourAction();
  }
}

至于字符,最好使用单个字符而不是由单个字符组成的字符串。符号是按值传递的,并且只需要两个字节的内存,而一个字符的字符串则需要超过 20 字节的内存,因为字符串是按引用传递的。

我还想提到 ToString 函数,它最好只在必要时使用,否则你可能会降低你的性能。例如,对于字符使用此函数并不总是精确的:很少有合理的决定。一般来说,你应该记住代码优化的一个简单而最重要的公理——执行的代码越少,使用的 CPU 时间和内存就越少,这会显著提高你的生产力。让我们看看这里展示的一个简单的代码示例,它涵盖了优化整数到字符串转换的简单选项之一:

using UnityEngine;
using System.Diagnostics;

public class IntegerToStringTest : MonoBehaviour {
  public const int CYCLES = 1000;

  string str;

  void Start() {
    str = "";
    Stopwatch toStringTimer = Stopwatch.StartNew();
    for (int i = 0; i < CYCLES; i++)
    {
      str += i.ToString();
    }
    toStringTimer.Stop();

    str = "";
    Stopwatch optimizedToStringTimer = Stopwatch.StartNew();
    for (int i = 0; i < CYCLES; i++)
    {
      str += string.Empty + i;
    }
    optimizedToStringTimer.Stop();

    UnityEngine.Debug.Log(
      "ToString time = " +
      (
(double)(toStringTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES
      ).ToString("0.00 nanoseconds average per cycle")
    );

    UnityEngine.Debug.Log(
      "Optimized ToString time = " +
      (
(double)(optimizedToStringTimer.Elapsed.TotalMilliseconds * 1000 * 1000) / CYCLES
      ).ToString("0.00 nanoseconds average per cycle")
    );
  }
}

在 Mac OS X 上,Intel Core i5 2.7 GHz,在 Unity 编辑器中测试此示例后,我得到了以下结果:

ToString time = 18229.00 nanoseconds average per cycle
Optimized ToString time = 13576.00 nanoseconds average per cycle

尝试根据前面的示例为你的函数构建自己的测试。在优化过程中,你应该经常依赖自己的直觉来找到所有问题的原因,但要做出正确的决定,你需要依赖测试的结果,即纯粹数学和明确的数字。

将 Unity C# 代码转换为 Unity JavaScript 代码以及相反

以下是一个示例,说明了如何轻松地将你的 Unity C# 代码转换为 Unity JavaScript 代码以及相反。你可以在网上找到很多不同的免费自动工具,你可以使用这些工具尽早完成 Unity 脚本之间的转换。例如,你可以在 www.m2h.nl/files/js_to_c.php 将 Unity JS 转换为 Unity C#。

JavaScript 变量和类型

默认情况下,Unity JS 代码中的变量是公共的,在 Unity 检查器中可见。为了将你的变量从 Unity 检查器或其他类中隐藏,你应该使用 private 关键字标记所有这些变量,如下面的简单示例所示:

// private variables are invisible in Unity Inspector
private var length : float = 2.9;

// visible in Unity Inspector
var title : String = "Title";

// visible in Unity Inspector
var isLoop : boolean = false;

C# 变量和类型

在 C# 中,浮点值必须在末尾有一个小写 f 或大写 F。否则,它将被视为双精度值。另外,注意在 JS 代码中,字符串类型应该用首字母大写。然而,在 C# 代码中,你可以用小写字母看到我们这里的简单示例:

// public variables are visible in Unity Inspector
public float length = 2.9f;

// is invisible in Unity Inspector
string title = "Title";

// is invisible in Unity Inspector
private bool isLoop = false;

默认情况下,在 Unity C# 代码中,变量是私有的,在 Unity 检查器中不可见。为了在 Unity 检查器中显示你的变量,你应该使用 public 关键字标记所有这些变量,就像我们之前的简单示例中所示。

在 Unity JS 中转换类型

你可以使用以下代码在 Unity JS 中转换类型:

var length : float = 0.08; // variable with float type
var number : int = length; // converting float to integer
print(number); // prints "0" in Unity console

在 Unity C# 中转换类型

你可以使用以下代码在 Unity C# 中转换类型:

float length = 0.08f; // variable with float type
int number = (int)length; // converting float to integer
Debug.Log(number); // prints "0" in Unity console

Unity JS 函数与 Unity C# 函数

以下代码片段显示了在 Unity JS 和 Unity C# 中编写的代码的语法:

// Unity JS Function
function YourFunctionName (yourStringVarName : String) {
    print(yourStringVarName);
}

// Unity C# Function
public void YourFunctionName (string yourStringVarName) {
    Debug.Log(yourStringVarName);
}

Unity JS 返回与 Unity C# 返回

在 Unity JS 中,你不需要声明返回类型,就像这里简单函数示例中所示:

function JSReturnString() {
    return "Hello World !";
}

在 Unity C# 中,你必须始终声明一个 return 类型:

public string CSharpReturnString() {
    return "Hello World !";
}

Unity JS 中的 yield 与 Unity C# 中的 yield

在 JS 中,yield 语句非常简单,就像 return 关键字一样。你可以直接使用 yield 语句,无需任何声明,就像这里简单示例中所示:

function Start() {
    yield YourFunc();
    yield new WaitForSeconds(1.7);
    print("[Start] FINISH");
}

function YourFunc() {
    print("[YourFunc] START");
    yield new WaitForSeconds(0.8);
    print("[YourFunc] FINISH");
}

//Output will be similar as shown below:
// [YourFunc] START
// [YourFunc] FINISH
// [Start] FINISH

在 C# 代码中,你应该在你的方法声明中声明 IEnumerator 类型,如下面的示例所示:

IEnumerator Start() { 
    yield return StartCoroutine(YourMethod());
    yield return new WaitForSeconds(1.7f);
    Debug.Log("[Start] FINISH");
}

IEnumerator YourMethod() {
    Debug.Log("[YourMethod] START");
    yield return new WaitForSeconds(0.8f);
    Debug.Log("[YourMethod] FINISH");
}

//Output will be similar as shown below:
// [YourMethod] START
// [YourMethod] FINISH
// [Start] FINISH

Unity JS 指令与 Unity C# 指令

Unity 有许多 脚本指令,例如 AddComponentMenu。语法差异如下面的代码所示:

// Unity JS example
@script AddComponentMenu ("Your Company Name/Your Action Name")
class YourFunctionName extends MonoBehaviour {}

// Unity C# example
[AddComponentMenu("Your Scope Name/Your Action Name")]
public class YourMethodName : MonoBehaviour {}

摘要

本章介绍了关于遮挡剔除和 LOD 优化技术的不同细节。此外,本章展示了如何优化 Unity C# 和 Unity JS 代码。最后,你看到了 Unity C# 和 Unity JS 代码之间的主要语法差异,并学习了如何轻松地在它们之间转换。

在下一章中,你将探索如何使用不同的技术,如基于物理的着色器和 Unity 5 中的全局照明,来提高游戏和应用程序的质量。本章结束时,你将了解如何优化任何着色器代码。

第七章. 故障排除和最佳实践

主要地,本章将探讨如何使用不同的技术和基于物理的着色器来提高游戏和应用程序的质量。其次,本章将描述 Unity 5 中的全局照明。在本章结束时,你将优化着色器代码。

本章将涵盖以下主题:

  • 使用内置 Android 分析器测量性能

  • 使用 Unity 分析器工具调试 Android 设备

  • 脚本和着色器中的最佳实践

使用内置 Android 分析器测量性能

让我们看看我们可以从 Unity 5 的内置 Android 分析器中看到哪些信息。

一般 CPU 活动

接下来,我们将讨论我们可以从内置分析器中获得的信息。为了更容易理解这些消息的结构,我们将它们分为几个组。第一个组包括一般信息,换句话说,是 CPU 的整体性能统计信息。

你将看到在参数值cpu-player中消耗的总 CPU 时间。在 CPU 端由 OpenGLES 驱动代码消耗的时间将在名为cpu-ogles-drv的参数值中看到。接下来,让我们考虑以下参数,称为cpu-waits-gpu。对于非常小的值,此选项不会出现在内置分析器中。此值显示了在 GPU 端等待渲染结束所消耗的 CPU 时间。接下来,让我们考虑以下参数,称为msaa-resolve。此值显示了在抗锯齿方法上消耗的 CPU 时间。让我们考虑以下参数,称为cpu-present。此值显示了在执行 OpenGLES 的presentRenderbuffer函数上消耗的 CPU 时间。让我们也看看这个组中最后一个参数的值,称为frametime。此值显示了在 CPU 端执行帧所花费的时间。

注意

Android 硬件的刷新率锁定在大约 60 Hz,因此你将得到大约~16.7 ms 的帧时间(大约是 16.7 毫秒,我们通过计算得到——1000 毫秒除以 60 Hz)。

渲染统计信息

现在,让我们考虑以下基于渲染的统计信息组。这个组中只包含四个参数。第一个参数被称为draw call。这个值的真正含义是显示每帧的绘制调用量。这个组的第二个参数被称为tris。这个值表示渲染器将处理的三角形数量。这个组的第三个参数被称为verts。这个值显示了渲染器将处理的顶点数量。静态几何体的上限数量为 10,000 个顶点,而皮肤几何体的上限则要低得多。最后,这个组中的最后一个参数,我们将在这个组中研究,被称为batched。这个参数的值对你的性能有很大影响,所以尽量尽可能减少这个值。这个值展示了 Unity 引擎自动批处理的绘制调用、三角形和顶点的数量。

注意

为了提高 Unity 引擎的批处理效率,你应该尽可能地为所有可用的对象使用共享材质。

详细 Unity 玩家统计信息

现在,考虑以下更详细的统计信息组。在内置分析器的详细统计信息中,第一个参数被称为physx。这个值表示物理引擎执行所花费的时间。文本参数被称为animation。这个值表示骨骼动画所花费的时间。详细统计信息中的第三个参数被称为culling。这个值表示花费在对象执行剔除上的时间。内置分析器详细统计信息中的第四个参数被称为skinning。这个值表示将动画应用到皮肤网格所需的时间。在这个详细统计信息中的第五个参数被称为batching。这个值显示了在批处理几何体执行上花费的时间。

注意

与批处理动态几何体相比,批处理静态几何体成本较低。

在详细统计信息中的第六个参数被称为render。这个值代表了渲染可见对象所花费的执行时间。第七个参数被称为fixed-update-count。这个值显示了当前帧FixedUpdate执行时间的上下限值。尽量尽可能减少这个值,因为它可以降低你的性能。

脚本详细统计信息

只有三组可获取的参数。第一个参数被称为update。这个值决定了你的脚本中每个Update函数执行的用时。下一个参数被称为fixedUpdate。这个值展示了你的脚本中每个FixedUpdate函数执行的用时。接下来的参数被称为coroutines。这个值决定了你的脚本中协程执行的用时。

脚本分配内存的详细统计信息

让我们根据您脚本内存分配的详细统计信息来介绍以下一组统计数据。这里只有四个参数。第一个参数被称为分配堆

此值表示可用于分配的内存。如果我们需要的内存超过了堆中可用的内存,垃圾回收器将被调用。然而,如果垃圾回收器无法为我们释放更多内存,那么堆的大小将会增加。下一个参数被称为已用堆。此值表示对象分配的堆大小。每次创建新的类实例时,它都会增加,但在垃圾回收器再次被调用之前,结构体不会增加。下一个参数被称为最大收集次数。此值显示了在过去 30 帧中垃圾回收器调用的数量。在这个组中的最后一个参数,也是内置分析器的最后一个参数,被称为收集总持续时间。此值显示了在过去 30 帧中垃圾回收器调用所使用的总毫秒数。

使用 Unity 分析器工具调试 Android 设备

我们可以从菜单中打开 Unity 分析器窗口,它展示了整个 Unity 分析器工具。在接下来的章节中,我们将更深入地探讨 Unity 分析器区域。

在开始之前,我们需要了解这个工具是如何工作的,以及它有多么简单易用。首先,让我们更详细地看看 Unity 分析器工具窗口的结构,并分别介绍其各个部分。正如我们可以在下一张截图中所看到的,有四个主要视觉部分:

  • 分析器控件

  • 使用区域

  • 分析器时间线

  • 信息表

接下来的章节将专注于 Unity 分析器工具的独特部分。让我们深入探讨这个工具中最有趣的部分。

关于可视化分析器,您可以连接到运行您应用程序的设备,以进一步分析您软件的性能。为了连接到其他设备,分析器必须在同一本地网络中(但这不是必要的充分条件)。活动分析器选项允许您从所需连接的列表中选择您的设备。除此之外,您应该通过构建设置中的开发构建启用复选框启动应用程序。在这些设置中,您还会看到自动连接分析器选项,这是必要的,以指示 Unity 每次启动应用程序时是否应该连接到分析器。

以下是一些 Unity 分析器按钮:

  • 记录

  • 深度分析器

  • 配置文件编辑器

  • 活动分析器

  • 清除

使用 Unity 分析器工具调试 Android 设备

如果你查看性能分析器窗口的顶部,你会看到一个工具栏,我们将在本章后面更详细地探讨它。通过工具栏上的按钮,你可以启用或禁用性能分析器记录数据。此外,你可以清除收集到的信息或在帧集中导航,还有更多功能;我们稍后会详细讨论。在工具栏中,我们可以看到当前按钮。点击此按钮后,我们将自动进入一个帧,并查看其实现的最后细节。如果你的游戏是在 Unity 编辑器中运行的,它将被挂起,这意味着它将被暂停。当你使用箭头按钮向前或向后切换帧时,它也会被挂起,这些箭头按钮离当前按钮不远。此外,请注意,性能分析器不会保留所有帧,而只保留最近的一定数量的帧。此外,如果你从左到右在工具栏的性能分析器上滑动,我们会看到一个清除按钮,用于清除收集到的所有数据。之后,我们会看到一个活动性能分析器按钮,它允许你选择设备或 Unity 编辑器进行进一步的性能分析。

接下来,我们看到一个名为性能编辑器的按钮;如果你点击此按钮,你将开始获取 Unity 编辑器的详细执行统计信息。在此按钮的左侧,你会看到一个深度分析按钮。当此按钮被激活时,它将提供关于你所有脚本和函数调用的信息。深度分析可能会显著减慢你的应用程序或游戏,因为它需要花费大部分时间进行处理,并需要大量的内存空间。请记住,非常深入的深度分析只有在用于小型项目时才会有效,否则你可能会面临 Unity 无法获取必要资源并挂起的风险,随后你将不得不重新启动 Unity 编辑器。此外,深度分析不仅适用于小型项目,对于测试游戏或应用程序的关键方面也非常有用。你可以在深度分析中使用代码,并为你脚本中的特定代码片段打开和关闭它。只有必要的代码部分将被分析。

Profiler.BeginSampleProfiler.EndSample调用是代码性能分析的起点和终点,这意味着这两个函数调用之间的代码将被分析,并在底部性能分析器窗口中显示详细统计信息。我们将在本章稍后讨论性能分析脚本。从深度分析按钮的左侧是一个名为记录的按钮,它用于启用或禁用性能分析,正如我们之前提到的。好吧,工具栏中最左侧的按钮被称为添加性能分析器,它用于显示不同的性能分析区域:CPUGPU渲染内存音频3D 物理2D 物理。我们将在本章稍后讨论这些性能分析区域。

如果你的游戏或应用程序以特定的帧率运行或与垂直空白同步,那么 Unity 将保持平均等待所有帧同步的时间,这被称为Wait For Target FPS参数,并在性能分析器中显示。默认情况下,等待时间的信息不会发布在 Unity 性能分析器中。要更改指定的默认行为,你需要启用View SyncTime

性能分析器时间线

在性能分析器窗口的上方是一个图表,它实时显示性能分析器的负载数据。统计数据在每个帧中处理,并且只保存在最后几百帧的历史记录中。如果你选择其中一个帧进行进一步考虑,你将在底部性能分析器中看到细节,这反过来又取决于所选的时间线区域(例如,CPUGPU音频)。你可以添加和删除各种时间线区域。此外,请注意,左侧的彩色方块显示不同的时间线区域。实际上,不仅仅是彩色方块;它们是单选按钮。因此,在优化应用程序时,消除不必要的数据将会更容易。

CPU 区域

CPU 区域清楚地显示了在 CPU 侧具体位置和总共花费了多少时间。如果你选择它,那么你就已经触发了 CPU 区域。之后,你将看到底部性能分析器仅显示关于应用程序在 CPU 上执行的足够详细的信息。此外,你可以选择两种不同的显示详细信息的模式:

  • 层次结构模式通过分组数据来显示信息

  • 组层次结构模式显示逻辑上分布的组信息;例如,渲染组、脚本组、物理组以及许多其他组

CPU 性能分析器的其他区域包括加载音频动画粒子玩家循环AI网络

渲染区域

渲染区域显示了如下截图所示的渲染统计信息。时间线图示了渲染的绘制调用三角形顶点的数量。正如我们所见,以下截图的下半部分和下一张图中显示的游戏视图渲染统计非常相似。进一步地,我们将详细说明以下截图中的信息:

渲染区域

以下截图与相同的统计信息非常相似:

渲染区域

每帧时间和 FPS 渲染一帧所花费的时间(以毫秒计);表示每秒帧数。
绘制调用 渲染网格的数量。
批处理(绘制调用) 批处理绘制调用的数量。
三角形和顶点 绘制的几何形状(三角形和顶点)的数量。
使用的纹理 这表示每帧使用了多少个纹理以及每个纹理所需的内存量。
渲染纹理 这显示了每帧激活的渲染纹理切换的次数;此外,它还展示了渲染纹理使用的内存量以及渲染纹理的数量。
屏幕 这显示了屏幕大小及其抗锯齿级别和内存使用情况。
VRAM 使用量 这大致表示了视频内存(VRAM)的使用量;此外,还表示了你的显卡有多少内存。
VBO 总数 顶点缓冲区对象VBO)是上传到显卡的网格数量。
可见皮肤网格 这显示了渲染的皮肤网格的数量。
动画 这表示可以播放多少个动画。

内存区域

在分析这个区域时,你可以选择两种可用的模式之一,以不同的显示模式。第一种模式是显示非常简单的统计信息,第二种模式是显示非常详细的统计信息。我们将在以下章节中更详细地介绍这两种模式:

简单视图

我们从最简单的显示模式统计信息开始(如下截图所示)。这以比详细统计信息更简单的方式显示了每个分析帧的内存使用情况:

简单视图

Unity 性能分析器的简单视图

为了有效地使用内存,Unity 试图提前保留一定量的内存,以池的形式,或者说作为备份缓冲区,这大大提高了性能。统计内存,或者说关于内存消耗多少以及用于什么的描述方法,将在底部性能分析窗口中显示。以下是这些统计参数:

  • Unity:这表示在原生 Unity 代码中分配使用的内存量

  • Mono:这显示了堆的大小以及垃圾回收器使用的内存量

  • Gfx Driver:这表示驱动程序在着色器、网格、渲染目标和纹理上使用的内存量

  • FMOD:这显示了音频驱动器使用的内存量

  • 剖析器:这表示 Unity 剖析器使用的内存量

内存区域显示基本类型对象和资产的信息:纹理、网格、材质、动画、音频和对象计数。

详细视图

在详细视图中,您可以使用 获取样本 按钮保存当前状态以供进一步分析。为了获得有关内存使用的如此详细的信息,Unity 剖析器需要花费时间收集所有必要的信息,这就是为什么您不应该认为您可以实时接收信息的原因。

详细视图

剖析器将显示有关内存消耗位置和内容的信息。以下是一个将消耗内存的对象组列表:

  • 来自原生代码的引用

  • 场景对象

  • 内置资源

  • 标记为不保存

在您点击列表中的某个对象后,Unity 将在 项目 视图或 场景 视图中突出显示所选项目。当在 Unity 编辑器中分析您的应用程序时,统计信息将不如在特定设备上准确。与 Unity 编辑器执行相关的某些成本也会显示在平均值中,这些平均值对您的应用程序并不真实。因此,为了更精确地分析您的应用程序,连接到真实设备并在该情况下分析统计数据是最佳决定。

音频区域

这显示了音频区域显示的信息。

物理区域

以下是在物理 3D 区域显示的信息列表(如以下截图所示):

  • 活动物体:这表示唤醒的 Rigidbodies 的数量

  • 睡眠物体:这显示了睡眠的 Rigidbodies 的数量

  • 接触点数量:这显示了场景中所有碰撞体之间的总接触点数

  • 静态碰撞体:这表示有多少碰撞体附加在非 Rigidbody 对象上

  • 动态碰撞体:这展示了有多少碰撞体附加在 Rigidbody 对象上

物理区域

Unity 剖析器的详细视图

GPU 区域

在 GPU 区域的剖析窗口中显示的统计信息与 CPU 区域显示的统计信息非常相似。

注意

在 Mac 上,只有 OSX 10.7 狮子版及更高版本支持 GPU 剖析。

实践技巧

全世界许多专业开发者都使用两种不同的性能优化技术。

Unity 中的高速、离屏粒子技术

下一技术是优化由 NVIDIA 在 GPU Gems 3 中引入的粒子系统。为了实现目标的第一步是将粒子渲染到RenderTexture或换句话说,渲染到比屏幕尺寸小的另一个渲染目标。这个想法的第二步是将粒子重新混合到屏幕上。首先,我们需要深度缓冲区。当我们渲染到另一个渲染目标时,我们需要深度缓冲区来进行 z 测试。以下代码行中,你可以在AwakeStart回调中注册,例如:

this.camera.depthTextureMode = DepthTextureMode.depth;

让我们考虑以下代码用于高速、离屏粒子:

// create the off-screen particles textureRenderTexture yourParticlesRenderTexture = RenderTexture.GetTemporary(
  Screen.width, // yourLowerResolutionIntegerValue 
  Screen.height, // yourLowerResolutionIntegerValue 
  0
);

yourLowerResolutionIntegerValue确定质量。最高值意味着最差的质量和最佳的性能,反之亦然。

第二部分非常简单,就是调整你的主相机的属性,如图所示:

yourMainCamera.targetTexture = yourParticlesRenderTexture;
yourMainCamera.backgroundColor = Color.black;
yourMainCamera.cullingMask = yourLayerMask.value;
yourMainCamera.depthTextureMode = DepthTextureMode.None;
yourMainCamera.clearFlags = CameraClearFlags.SolidColor;

下一步骤包括将粒子渲染和混合到场景中:

Shader.SetGlobalVector(
  "_Your_Camera_Depth_Texture_Size",
  Vector4(
    this.camera.pixelWidth, this.camera.pixelHeight, 0.0, 0.0
  )
);
depthCamera.RenderWithShader(
  Shader.Find("Pro/Unity/Performance/Particles/Off-Screen"), 
  "RenderType"
);
Material yourMixedMaterial = YouClassHelper.GetMaterialByShader(
  Shader.Find("Pro/Unity/Performance/Particles/Off-Screen")
);
Vector2 yourTexelOffset = Vector2.Scale(
  source.GetTexelOffset(), 
  Vector2(source.width, source.height)
);
Graphics.BlitMultiTap(
  yourParticlesRenderTexture, source, yourMixedMaterial,  yourTexelOffset
);

注意

总是释放粒子渲染纹理以获得更好的性能。

你可以将你的RenderTexture渲染到目的地,如图所示:

RenderTexture.ReleaseTemporary(yourParticlesRenderTexture);
Graphics.Blit(source, destination);

池化技术

下一技术是一个基本的池化系统(如列表 3-1 所示),用于 Unity 以及 Shuriken 粒子。将池组件放在你的 GameObject 上,设置名称和预制体。池在实体被创建时调用OnCreateEvent策略(因此将你的初始化代码放在StartAwake回调中),当重用项目进入池时调用OnLiberationEvent系统。OnCreateEvent策略为池提供了创建事件,以便你可以存储它并在以后重用你的GameObject

YourPoolClass.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class YourPoolClass : MonoBehaviour
{
      private static readonly Dictionary<string, YourPoolClass> 
      namesOfObjects = new Dictionary<string, YourPoolClass>();

      public static YourPoolClass GetPoolByName(string name) { 
    return namesOfObjects[name]; 
  }

      [SerializeField]
      private string nameOfYourPool = string.Empty;

      [SerializeField]
      private Transform yourPoolPrefab = null;

      [SerializeField]
      private int initialObjectCounter = 23;

      [SerializeField]
      private bool isParentEnabled = true;

      private readonly Stack<Transform> yourObjectsStack = new Stack<Transform>();

      void Awake()
      {
    System.Diagnostics.Debug.Assert(yourPoolPrefab);
    namesOfObjects[nameOfYourPool] = this;

          for (int i = 0; i < initialObjectCounter; i++)
          {
                var t = Instantiate(yourPoolPrefab) as Transform;
                AdjustingYourObject(t);
                LiberationObject(t);
          }
      }

      public Transform GetObject(Vector3 position = new Vector3())
      {
          Transform t = null;

          if (yourObjectsStack.Count > 0) 
    {
                t = yourObjectsStack.Pop();
          } 
    else 
    {
                Debug.LogWarning(
        nameOfYourPool + " pool error!", this
      );
                t = Instantiate(yourPoolPrefab) as Transform;
          }

          t.position = position;
          AdjustingYourObject(t);

          return t;
      }

      private void AdjustingYourObject(Transform obj)
      {
         if (isParentEnabled)
         {
                obj.parent = transform;
          }

          obj.gameObject.SetActiveRecursively(true);
          obj.BroadcastMessage(
      "OnCreateEvent", 
      this, 
      SendMessageOptions.DontRequireReceiver
    );
      }

      public void LiberationObject(Transform obj)
      {
         obj.BroadcastMessage(
      "OnLiberationEvent", 
      this, 
      SendMessageOptions.DontRequireReceiver
    );
          obj.gameObject.SetActiveRecursively(false);
          yourObjectsStack.Push(obj);
      }
}

这就是使用此池化系统的方法:

using UnityEngine;

public class YourPoolExampleUsage : MonoBehaviour {
  void Start() {
    YourPoolClass pool = YourPoolClass.GetPoolByName("Bang");
    Transform obj = pool.GetObject(Vector3.zero);
  }
}

在使用YourPoolClass粒子系统的情况下,你应该使用以下代码:

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(ParticleSystem))]
public class YourPoolParticleSystem : MonoBehaviour
{
    private YourPoolClass yourPoolClass;

    void OnCreateEvent(YourPoolClass ypc)
    {
        yourPoolClass = ypc;

        particleSystem.renderer.enabled = true;
        particleSystem.time = 0;
        particleSystem.Clear(true);
        particleSystem.Play(true);
    }

    void OnLiberationEvent()
    {
        particleSystem.Stop();
        particleSystem.time = 0;
        particleSystem.Clear(true);
        particleSystem.renderer.enabled = false;
    }

    void Update()
    {
        if (!particleSystem.IsAlive(true) && particleSystem.renderer.enabled)
        {
            yourPoolClass.LiberationObject(transform);
        }
    }
}

可脚本化分析器工具

开发者可以使用 Unity 分析器来分析自己的代码或某些代码片段,这一点非常重要。为了在 Unity 分析器中显示有关某些函数或代码部分的统计信息,你只需要在两个调用Profiler.BeginSampleProfiler.EndSample之间包含你的代码。之后,你可以使用可视化的 Unity 分析器工具来搜索代码中的瓶颈和峰值。

注意

分析器仅在 Unity Pro 中可用。在独立游戏中,分析器可以使用Profiler.logProfiler.enabled将所有分析信息转储。

要创建自己的工具,你可以使用以下 Unity API 调用:

  • FindObjectsOfTypeAll

  • FindObjectsOfType

  • GetRuntimeMemorySize

  • GetMonoHeapSize

  • GetMonoUsedSize

  • Profiler.BeginSample

  • Profiler.EndSample

  • UnloadUnusedAssets

  • System.GC.GetTotalMemory

  • Profiler.usedHeapSize

Unity 分析器技巧

有能力将分析信息导出为二进制文件,稍后可以再次导入。这是通过以下方式通过脚本实现的:

  function Start () {
        Profiler.logFile = "yourName.log";

        Profiler.enableBinaryLog = true; // writes to "yourName.log.data"

        Profiler.enabled = true;
  }

并重新导入到分析器中:

Profiler.AddFramesFromFile ("yourName.log");

获取分析器帧信息到脚本的 API 在以下地方未公开:

UnityEditorInternal.ProfilerDriver

尚未记录,但在UnityEditorInternal命名空间中完全开放。其他合适的 API:

Profiler.BeginSample("Your Label Name");
Profiler.EndSample();
Profiler.GetRuntimeMemorySize(o : Object) : int

创建一个简单的分析器

现在是时候从头开始开发我们自己的简单且非常有用的分析器工具了。在未来,您将能够使用我们简单分析器中的这些脚本为您所有的项目以及本书中讨论的任何其他示例。当然,如果您有强烈的愿望或必须这样做,您可以修改所有方法以满足您特定的需求,或者如果您这项功能足够满足您的任务,您也可以以原始形式使用它们。首先,让我们看看一个非常简单的类,这是我们的简单代码分析器工具的核心类。在下面的代码中,您可以看到一个非常简单的ExampleProfilerClass

列表 1-3. ExampleProfilerClass.cs

using UnityEngine;

public class ExampleProfilerClass
{
  int counter = 0;

  float startedTime = 0;
  float totalTime = 0;
  float endTime = 0;
  float elapsedTime = 0;

  bool wasStartedFlag = false;

  public string indexStr;

  public float TotalTime {
    get { 
      return totalTime; 
    }
  }

  public int Counter {
    get { 
      return counter;
    }
  }

  public ExampleProfilerClass(string indexStr)
  {
    this.indexStr = indexStr;
  }

  void ShowError() {
    Debug.LogError("ExampleProfilerClass {START / END} ERROR: [index] = [" + indexStr + "]");  
  }

  public void Start() {
    if (wasStartedFlag) { 
      ShowError(); 
    }

    counter++;

    wasStartedFlag = true;

    startedTime = Time.realtimeSinceStartup;
  }

  public void End() {
    endTime = Time.realtimeSinceStartup;

    if (false == wasStartedFlag) { 
      ShowError(); 
    }

    wasStartedFlag = false;

    elapsedTime = (endTime - startedTime);

    totalTime += elapsedTime;
  }

  public void ClearStatistics() {
    wasStartedFlag = false;

    totalTime = 0;

    counter = 0;
  }
}

您需要将ExampleProfilerClass脚本附加到场景中的某个对象上。代码非常直接和简单,正如本书中所有其他示例一样。我们的分析器代码的整个代码在列表 1-4 中展示:

列表 1-4. SimpleProfiler.cs

using UnityEngine;
using System.Collections.Generic;

public class SimpleProfiler : MonoBehaviour {
  float startedTime = 0;
  float followingTime = 1;
  float totalTimeInMilliSeconds = 0;
  float averageTimeInMilliSeconds = 0;
  float framesPerSecond = 0;
  float savedTimeInMilliSeconds = 0;
  float percentageSavedFromTotal = 0;
  float timeInMilliSecondsPerFrame = 0;
  float timeInMilliSecondsPerCall = 0;
  float callsNumberPerFrame = 0;

  int frameCount = 0;
  int colWidth = 30;

  static Dictionary<string, ExampleProfilerClass> statistics = new Dictionary<string, ExampleProfilerClass>();

  string profilerInfo = "ALREADY STARTED !";

  Rect windowRect = new Rect(25, 25, 800, 300);

  void Awake() {
    startedTime = Time.time;
  }

  void OnGUI() {
    GUI.Box(windowRect,"Simple Profiler");
    GUI.Label(windowRect, profilerInfo);
  }

  public static void Start(string indexStr) {
    if (false == statistics.ContainsKey(indexStr)) {
      statistics[indexStr] = new ExampleProfilerClass(indexStr);
    }

    statistics[indexStr].Start();
  }

  public static void End(string indexStr) {
    statistics[indexStr].End();
  }

  void Update() {
    frameCount++;

    if (Time.time > followingTime)
    {
      profilerInfo = "\n\n\n";

      totalTimeInMilliSeconds = (Time.time - startedTime) * 1000;
      averageTimeInMilliSeconds = (totalTimeInMilliSeconds / frameCount);
      framesPerSecond = (1000 / (totalTimeInMilliSeconds / frameCount));

      profilerInfo += "Frames per Second: ";
      profilerInfo += framesPerSecond.ToString("0.#") + " frames; \nAverage Frame Time: ";
      profilerInfo += averageTimeInMilliSeconds.ToString("0.#") + " ms \n\n\n";
      profilerInfo += "Time Percentages".PadRight(colWidth);
      profilerInfo += "ms per Frame".PadRight(colWidth);
      profilerInfo += "ms per Call".PadRight(colWidth);
      profilerInfo += "Calls number per Frame".PadRight(colWidth);
      profilerInfo += "NameIndex";
      profilerInfo += "\n";

      foreach(ExampleProfilerClass statisticsRecord in statistics.Values)
      {
        savedTimeInMilliSeconds = (statisticsRecord.TotalTime * 1000);
        percentageSavedFromTotal = (savedTimeInMilliSeconds * 100) / totalTimeInMilliSeconds;
        callsNumberPerFrame = statisticsRecord.Counter / (float)frameCount;
        timeInMilliSecondsPerCall = savedTimeInMilliSeconds / statisticsRecord.Counter;
        timeInMilliSecondsPerFrame = savedTimeInMilliSeconds / frameCount;

        profilerInfo += (percentageSavedFromTotal.ToString("0.000") + "%").PadRight(colWidth);
        profilerInfo += (timeInMilliSecondsPerFrame.ToString("0.000") + " ms").PadRight(colWidth);
        profilerInfo += (timeInMilliSecondsPerCall.ToString("0.0000") + " ms").PadRight(colWidth);
        profilerInfo += (callsNumberPerFrame.ToString("0.000")).PadRight(colWidth);
        profilerInfo += (statisticsRecord.indexStr);
        profilerInfo += "\n";

        statisticsRecord.ClearStatistics();
      }

      frameCount = 0;

      startedTime = Time.time;

      followingTime = Time.time + 1;
    }   
  }
}

以下是一个简单的测试代码,它在循环中执行数学运算。您可以将此脚本挂载到场景中的任何对象(或同时挂载到多个对象)上,仅用于测试您的SimpleCodeProfiler工具。

列表 1-5. TestProfilerCode.cs

using UnityEngine;

public class TestProfilerCode : MonoBehaviour {
  float tmpFloat;

  void Update () {
    SimpleProfiler.Start("YOUR_UNIQUE_LABEL_1");

    for (int i = 0; i < 10; i++) {
      for (int degree = 0; degree < 360; degree++) {
        tmpFloat = Mathf.Cos(degree * Mathf.Deg2Rad);
      }
    }

    SimpleProfiler.End("YOUR_UNIQUE_LABEL_1");

    ///////////////////////////////////////////////////

    SimpleProfiler.Start("YOUR_UNIQUE_LABEL_2");

    for (int i = 0; i < 50; i++) {
      for (int degree = 0; degree < 180; degree++) {
        tmpFloat = Mathf.Sqrt(Mathf.Cos(degree * Mathf.Deg2Rad) + Mathf.Sin(degree * Mathf.Deg2Rad));
      }
    }

    SimpleProfiler.End("YOUR_UNIQUE_LABEL_2");
  }
}

摘要

在本章中,我们研究了 Unity 中优化的选择。我们首先发现了不同的 Unity 性能区域。我们探索了内置的 Unity 分析器和它的日志信息结构。在本章中,我们特别讨论了 Unity 的分析器工具及其窗口部分。我们发现了如何将分析器附加到不同的平台和设备。在本章末尾,我们讨论了许多专业人士使用的最佳实践。我们还发现了 Unity 分析器编程区域,并创建了我们自己的非常简单的分析器工具。

在附加章节中,该章节可在网上获取,我将向您展示如何从零开始轻松开发在 Android Play Store 上最受欢迎的游戏(Glow Hockey 在play.google.com/store/apps/details?id=com.natenai.glowhockey&hl=en)的 Unity 5 版本,该游戏下载量约为 1 亿至 5 亿。您将看到如何为任何屏幕分辨率和任何屏幕尺寸创建相机。此外,您还将看到在实践中使用物理是多么简单。您将通过实践学习如何设计美观的效果、动画、物理行为以及其他不同真实世界的功能和技巧,用于您的 Android 游戏和应用。您将看到如何优化您的项目以及任何其他真实世界的 Android 设备项目。章节中还将涵盖更多有用的功能和特性。

posted @ 2025-10-25 10:31  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报