-NET-开发者的-Unity-游戏开发指南-全-

.NET 开发者的 Unity 游戏开发指南(全)

原文:zh.annas-archive.org/md5/662d86698b8db28e6139753bb11eed9e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

作为世界上最广泛使用的游戏引擎之一,Unity 提供了易于使用且功能强大的游戏开发工具,这无疑吸引了众多开发者选择它来开发自己的游戏。然而,现代游戏开发所需工具不仅限于游戏引擎;其他工具和服务,如云服务,在游戏开发中的应用也越来越广泛。在本书中,我们将探讨如何使用 Unity 游戏引擎和 Microsoft Game Dev,包括 Microsoft Azure 云和 Microsoft Azure PlayFab 服务,来创建游戏。

从理解 Unity 游戏引擎的基本原理开始,你将逐渐熟悉 Unity 编辑器和用 C#编写 Unity 脚本的关键概念,这将为你制作自己的游戏做好准备。

然后,你将学习如何使用 Unity 的内置模块,例如 UI 系统、动画系统、物理系统,以及如何在游戏中集成视频和音频,使你的游戏更加有趣。

随着你逐步阅读各章节,我将带你深入了解高级主题,例如计算机图形学中涉及的数学知识、如何在 Unity 中使用新的可脚本化渲染管线创建后处理效果、如何使用 Unity 的 C#作业系统实现多线程,以及如何使用 Unity 的实体组件系统(ECS)以数据导向的方式编写游戏逻辑代码,从而提高游戏性能。

在阅读过程中,你还将了解 Microsoft Game Dev、Azure 云服务、Azure PlayFab,以及如何使用 Unity3D PlayFab SDK 访问 PlayFab API 以从云中保存和加载数据。

在阅读完本书后,你将熟悉 Unity 游戏引擎,对 Azure 云有高层次的理解,并准备好开发自己的游戏。

本书面向对象

本书面向具有中级.NET 和 C#编程经验的开发者,他们希望学习使用 Unity 进行游戏开发。假设读者具备基本的 C#编程经验。

本书涵盖内容

第一章Hello Unity,介绍了 Unity 游戏引擎的基本原理。从 Unity 的安装过程开始,然后探索编辑器,你还将了解 Unity 提供的.NET 配置文件和脚本后端,最后,你将全面了解 Unity。

第二章Unity 中的脚本概念,从上一章继续,详细介绍了 Unity 中的脚本。它首先介绍了 Unity 脚本中最常用的类,然后解释了脚本的生命周期。它还涵盖了如何在 Unity 中创建新的脚本,并将脚本作为组件附加到 GameObject 上,并通过 Unity 包管理器演示了如何添加或删除包。

第三章, 使用 Unity UI 系统开发 UI,介绍了在 Unity 中常用的不同类型的 UI 元素。此外,本章还讨论了如何通过使用模型-视图-视图模型MVVM)架构模式在 Unity 中开发 UI。最后,探讨了针对 Unity UI 的优化技巧。

第四章, 使用 Unity 动画系统创建动画,涵盖了 Unity 动画系统最重要的概念,如动画剪辑、Animator 控制器、Avatar 和 Animator 组件。在这里,你将使用动画系统实现 3D 和 2D 动画。最后,探讨了针对 Unity 动画系统的优化技巧。

第五章, 与 Unity 物理系统协同工作,概述了 Unity 提供的物理解决方案,包括两个内置物理解决方案,即 NVIDIA PhysX 引擎和 Box2D 引擎。它还涵盖了 Unity 物理系统中的关键概念,如碰撞体和刚体。在这里,你将实现一个基于物理的乒乓球游戏。最后,探讨了针对 Unity 物理系统的优化技巧。

第六章, 在 Unity 项目中集成音频和视频,涵盖了 Unity 音频系统和视频系统中的关键概念,如音频剪辑资产、Audio Source 组件、Audio Listener 组件和 Video Player 组件。最后,探讨了针对 Unity 音频系统的优化技巧。

第七章, 在 Unity 中理解计算机图形学的数学原理,涵盖了与计算机图形学相关的数学,例如坐标系、向量、矩阵和四元数。

第八章, Unity 中的可脚本渲染管线概述,介绍了在 Unity 中选择的三种现成的渲染管线,即传统的内置渲染管线和基于可脚本渲染管线的两个预制渲染管线,分别是通用渲染管线和高清晰度渲染管线。它还涵盖了如何使用通用渲染管线资产来配置渲染管线,以及如何使用体积框架将后处理效果应用于游戏。最后,探讨了针对通用渲染管线的优化技巧。

第九章, 在 Unity 中使用面向数据的技术堆栈,介绍了面向数据设计的概念以及面向数据设计与传统面向对象设计的区别。它还探讨了 Unity 中的面向数据的技术堆栈DOTS)及其构成的三个技术模块——即 C#作业系统、ECS 和 Burst 编译器。

第十章, Unity 和 Azure 中的序列化系统与资产管理,讨论了 Unity 中的二进制序列化、YAML 序列化和 JSON 序列化。它还涵盖了 Unity 中的资产工作流程,并以探索如何在 Azure 云中创建 Azure Blob 存储服务以及如何将 Azure 中的可寻址内容加载到 Unity 项目中结束。

第十一章, 使用 Microsoft Game Dev、Azure 云、PlayFab 和 Unity 进行工作,讨论了 Microsoft Game Dev、Microsoft Azure 云和 Azure PlayFab 是什么,以及为什么您应该考虑在游戏开发中使用它们。在这里,您将通过 Azure PlayFab 的 API 在 Unity 项目中实现注册、登录和排行榜功能。

要充分利用本书

本书假设您对.NET 和 C#有一定了解。本书涵盖了基本概念、Unity 游戏引擎的高级主题,以及其他技术,如 Microsoft Azure 云和 Azure PlayFab。

您还需要在您的计算机上安装长期支持LTS)版本的 Unity – 推荐使用 2020 或更高版本。您可以在第一章**,Hello Unity中找到如何在您的计算机上安装 Unity 的说明。所有代码示例都在 Windows 操作系统上的 Unity 2020.3.24 上进行了测试。然而,它们也应该适用于未来的版本发布。

您还需要一个 Microsoft Azure 云订阅,您可以在以下链接申请免费 Azure 账户:azure.microsoft.com/en-in/free/

如果您希望从我们的 GitHub 存储库下载示例项目,您将需要一个 Git 客户端;我们推荐 GitHub Desktop,因为它是最容易使用的。您可以从以下链接下载:desktop.github.com

如果您使用的是 Windows 操作系统,您还可以考虑使用 Git for Windows。您可以从以下链接下载:git-scm.com/download/win

如果您使用的是本书的数字版,我们建议您自己输入代码或从本书的 GitHub 存储库(下一节中有一个链接)访问代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。

下载示例代码文件

本书代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Game-Development-with-Unity-for-.NET-Developers。如果代码有更新,它将在现有的 GitHub 存储库中更新。

我们还有其他来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们!

下载彩色图像

我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781801078078_ColorImages.pdf

使用的约定

本书使用了多种文本约定。

文本中的代码: 表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“如果某些内容是通过OnCollisionEnter在对象碰撞开始时生成的,并且您想在对象碰撞结束时销毁它们,那么您应该考虑使用OnCollisionExit。”

代码块设置如下:

using UnityEngine;
public class TriggerTest : MonoBehaviour
{
    private void OnTriggerStay(Collider other)
    {
        Debug.Log($"{this} stays {other}");
    }
}
} 

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

using UnityEngine;
public class PingPongBall : MonoBehaviour
{
    [SerializeField] private Rigidbody _rigidbody;
    [SerializeField] private Vector3 _initialImpulse;
    private void Start()
    {
        _rigidbody.AddForce(_initialImpulse,
          ForceMode.Impulse);
    }
}

粗体: 表示新术语、重要词汇或您在屏幕上看到的词汇。例如,菜单或对话框中的文字以粗体显示。以下是一个示例:“选择3D 对象 | 平面以在编辑器中创建一个新的平面对象。”

小贴士或重要注意事项

它看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈: 如果您对本书的任何方面有疑问,请通过 mailto:customercare@packtpub.com 给我们发邮件,并在邮件主题中提及书名。

勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。

盗版: 如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 mailto:copyright@packt.com 与我们联系,并在邮件中附上材料的链接。

如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了《面向.NET 开发者的 Unity 游戏开发》,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区都至关重要,并将帮助我们确保我们提供高质量的内容。

第一部分:Unity 基础概念

在本书的这一部分,我们将探讨 Unity 游戏引擎的基础知识,并介绍 Unity 脚本的一些关键概念,以便您为制作自己的游戏做好准备。

本节包括以下章节:

  • 第一章, Hello Unity

  • 第二章, Unity 中的脚本概念

第一章:第一章:你好,Unity

在我们开始使用 Unity 开发游戏之前,我认为首先了解 Unity 本身是很好的。许多人,尤其是对游戏和游戏开发感兴趣的人,都知道 Unity 是一个广泛使用的游戏引擎,您可能玩过许多使用 Unity 开发的游戏。但您可能不熟悉如何使用 Unity 来开发游戏。例如,有多个不同的 Unity 版本可供选择,那么您如何选择适合您的版本呢?Unity 提供了不同的订阅计划,但哪个订阅计划适合您的具体情况?

如果您之前从未使用过 Unity,那么首先学习如何使用 Unity 编辑器是必要的。除了 Unity 编辑器之外,Unity 引擎还提供了哪些功能来帮助游戏开发者开发游戏?了解 Unity 中的功能也很重要。如果您是.NET 开发者,那么您可能熟悉 Visual Studio。您需要了解如何使用 Visual Studio 来开发 Unity 游戏。但开发 Unity 游戏与开发.NET 应用程序是不同的。

我问的问题太多了吗?不用担心——这一章将帮助您解答这些问题。

在本章中,我们将介绍如何选择正确的 Unity 版本,并提供通过 Unity Hub 或 Unity 安装程序下载和安装 Unity 的概述。然后,我们将为您的情况选择正确的订阅计划。到这时,您应该已经安装了 Unity 并打开了 Unity 编辑器。

如果您刚刚开始使用 Unity 编辑器,您可能不知道如何使用它。我们将首先探索 Unity 编辑器,然后讨论 Unity 提供的不同功能。然后,我们将介绍 Unity 中的.NET 配置文件和 Unity 提供的脚本后端。最后,我们将展示如何使用 Visual Studio 来开发 Unity 游戏。

在本章中,我们将涵盖以下关键主题:

  • 开始使用 Unity 编辑器

  • 在 Unity 中处理不同的功能

  • .NET/C#和 Unity 中的脚本

  • 使用 Visual Studio 构建 Unity 游戏

技术要求

在开始之前,我强烈建议您首先检查您的系统是否可以运行 Unity 编辑器。以下表格给出了运行 Unity 编辑器的最低要求:

开始使用 Unity 编辑器

无论您是独立游戏开发者还是为公司团队工作,在安装或下载 Unity 之前,您需要做两件事:

  • 选择适合您的 Unity 版本。

  • 选择适合您的订阅计划。

因此,在介绍如何安装 Unity 和探索 Unity 编辑器之前,让我们首先介绍 Unity 版本和订阅计划。我们希望您通过阅读这些内容,可以找到适合您的版本并选择一个合适的订阅计划。

选择适合您的 Unity 版本

现在,Unity 每年提供两个不同的版本。它们如下所示:

  • Tech Stream 版本

  • 长期支持LTS)版本:

![图 1.1 – Unity 版本发布图片

图 1.1 – Unity 版本发布

您可能不确定在您的项目中使用哪个版本的 Unity 最佳,因此我将解释这两个不同的版本,以便您了解如何选择适合您的版本。

LTS 版本为开发者提供最大稳定性和对其项目的全面支持,并且是每年最后一个技术流版本。LTS 版本没有新功能或 API 变更。LTS 版本的更新解决崩溃,修复错误和任何小问题。正如我在本节开头提到的,每年,Unity 会发布 LTS 版本的新版本,每个版本从发布之日起支持 2 年。

因此,如果您正在寻找性能和稳定性,或者您的项目已经处于生产状态或开发过程中,使用最新的长期支持版本是一个好主意,以确保最佳性能和稳定性。

注意

在撰写本文时(2022 年 4 月),有两个长期支持版本发布,分别是 Unity 2020 LTS 和 Unity 2019.4。Unity 2020 LTS 是最新的长期支持版本,具有与 Unity 2020.2 技术流版本相同的特性集。另一方面,Unity 2019.4 现在是遗留的长期支持版本。

技术流版本为想要探索最新进行中特性的开发者提供了一个选项,他们可以使用这些版本为未来的项目做准备。与长期支持版本不同,技术流版本每年发布两次(通常在第一季度和第四季度发布),并且只支持到下一个技术流版本正式发布。

因此,如果您正在为您的下一个项目做准备或正在做原型设计和实验,您应该尝试技术流版本。

注意

在撰写本文时(2022 年 4 月),最新的技术流版本是 Unity 2021.2。

通过阅读本节,我希望您已经了解了 Unity 版本发布的情况,并且应该能够根据您的具体情况选择正确的 Unity 版本。

在撰写本书时,我选择了最新的 LTS 版本,Unity 2020.3。

选择适合您的订阅计划

Unity 是一个广泛使用的游戏引擎,许多独立游戏开发者使用 Unity 来开发他们的游戏。但从技术上讲,Unity 不是一个免费的游戏引擎。在本节中,我将介绍 Unity 提供的几种不同的订阅计划。希望您阅读本节后,能够选择适合您情况的订阅计划。

Unity 提供了一系列计划,从为个人学习者提供的免费个人计划到大型组织使用的企业计划:

![图 1.2 – 计划和定价页面图片

图 1.2 – 计划和定价页面

由于每个 Unity 计划有不同的资格要求,您应选择适合您项目的正确计划。接下来,我将介绍订阅计划:

  • 个人计划 是免费的,包括 Unity 的所有基本功能。如果您作为个人工作,并且在过去 12 个月内从您的 Unity 项目中获得的收入或资金少于 10 万美元,您可以选择此计划。此外,如果您是学生或教育工作者,您还可以获得额外的好处,但在那之前,您需要加入 GitHub 学生开发者包 以进行验证。

  • 专业计划 是一个付费计划,提供更多功能性和培训资源,例如高级云诊断和启动画面定制。如果您在过去 12 个月内使用 Unity 获得的收入超过 10 万美元但少于 20 万美元,您应该选择此计划。

  • 专业计划 也是一个付费计划。与 Plus 计划相比,您可以通过使用专业计划从 Unity 获得更多技术支持。如果您的组织在过去 12 个月内从任何来源获得的收入超过 20 万美元,您必须使用专业计划或 企业计划

  • 企业计划 专门针对至少有 20 名成员的团队,并且提供的支持比专业计划更多。例如,Unity 的客户成功经理将被分配到您的组织,以提供指导、协调资源和作为内部倡导者。

希望这一部分对您选择适合您情况的正确 Unity 计划有所帮助。接下来,让我们下载并安装 Unity 编辑器!

下载和安装 Unity 编辑器

下载和安装 Unity 编辑器有两种不同的方法。下载和安装 Unity 的第一种和推荐的方法是使用 Unity Hub

Unity Hub 是一个管理工具,可以用来管理您所有的 Unity 项目和 Unity 安装。我们可以采取以下步骤来安装 Unity Hub 和 Unity 编辑器:

  1. 要安装 Unity Hub,请访问 unity3d.com/get-unity/download 上的 下载 Unity 页面:

图 1.3 – 下载 Unity 页面

图 1.3 – 下载 Unity 页面

如您在前一张截图的 系统要求 部分所见,Unity Hub 支持 WindowsMac OS XUbuntuCentOS

  1. 安装 Unity Hub 非常简单;您只需选择 Unity Hub 安装的文件夹。然后,点击 安装 按钮:

图 1.4 – Unity Hub 安装

图 1.4 – Unity Hub 安装

  1. 安装 Unity Hub 后,选择 运行 Unity Hub 选项,然后点击 完成 按钮以启动 Unity Hub:

图 1.5 – 完成 Unity Hub 安装

图 1.5 – 完成 Unity Hub 安装

我在撰写本文时使用的是 Unity Hub 的最新版本(版本 3.0.0)。如果您使用过 Unity Hub 的早期版本,您会发现新版本的 Unity Hub 的启动页面完全不同。

  1. 你需要一个 Unity 账户才能访问 Unity 编辑器和 Unity Hub。如果你还没有 Unity 账户,那么你需要创建一个新的:

图 1.6 – Unity Hub

图 1.6 – Unity Hub

  1. 当你首次登录 Unity Hub 时,你将被要求添加一个有效许可证,如以下截图顶部所示。点击 管理许可证 按钮以打开 许可证 设置面板:

图 1.7 – 点击管理许可证按钮

图 1.7 – 点击管理许可证按钮

  1. 有两个按钮可供你添加新的许可证。你可以点击右上角的 添加 按钮,或者点击 添加许可证 按钮:

图 1.8 – 许可证设置面板

图 1.8 – 许可证设置面板

然后,你有不同的选项来激活许可证。我们已经在上一节讨论了不同的 Unity 订阅计划:

图 1.9 – 添加新许可证

图 1.9 – 添加新许可证

  1. 添加新的许可证后,我们可以开始探索 Unity Hub。从 项目 视图,你可以找到由 Unity Hub 跟踪的 Unity 项目的列表。你还可以通过点击 项目 视图右上角的 新建项目 按钮创建一个全新的项目,或者通过点击 打开 按钮导入现有项目:

图 1.10 – 项目视图

图 1.10 – 项目视图

  1. 要安装 Unity 编辑器,请打开 安装 视图,在那里你可以管理多个版本的 Unity 编辑器的安装:

图 1.11 – 安装视图

图 1.11 – 安装视图

Unity Hub 安装并管理着一系列的 Unity 编辑器。类似于 项目 视图,你可以下载并安装新的 Unity 编辑器,或者导入一个未被 Unity Hub 管理的现有 Unity 编辑器,例如我们使用 Unity 安装程序安装的 Unity 编辑器。

  1. 通过点击 安装编辑器 视图中的 安装编辑器 按钮打开 安装 Unity 编辑器 面板。然后,你将看到每个发布版本的最新版本:

图 1.12 – 安装 Unity 编辑器

图 1.12 – 安装 Unity 编辑器

注意

Unity 2018 LTS 已达到其支持周期的结束,因此你不应该安装它。

我们将使用 Unity 2020 LTS 版本的最新版本,因此我们需要在此处安装 Unity 2020.3.13f1

图 1.13 – 安装 Unity 2020.3.13f1

图 1.13 – 安装 Unity 2020.3.13f1

然后,我们需要选择需要安装的模块。如前一个截图所示,Microsoft Visual Studio Community 2019 将默认安装,这将作为我们在 Unity 中开发游戏的 集成开发环境IDE)。

注意

如果您想更改安装位置,您可以在首选项面板的安装设置中更改它。

下载并安装完成后,我们就准备好开始探索 Unity 编辑器了!

有时,您可能需要一个通过 Unity Hub 无法获取的特定版本,例如一些较旧的 Unity 版本。在这种情况下,您还可以通过第二种方式安装 Unity 编辑器,即通过Unity 安装程序。您可以使用 Unity 安装程序下载 Unity 的早期版本:

![Figure 1.14 – Unity 下载存档页面

![Figure_1.14_B17146.jpg]

图 1.14 – Unity 下载存档页面

现在,按照以下步骤通过 Unity 安装程序安装 Unity 编辑器:

  1. 要下载 Unity 的早期版本,您应该访问 Unity 下载存档页面unity3d.com/get-unity/download/archive

  2. 点击下一步按钮,并选择您想要下载和安装的 Unity 组件。Unity 安装程序应该类似于以下截图:

![Figure_1.15 – Unity 安装程序

![Figure_1.15_B17146.jpg]

图 1.15 – Unity 安装程序

  1. Unity 编辑器默认选中;为了构建不同平台的游戏,您还需要选择相应的构建支持组件。例如,如果您想为运行在 Android 设备上的 Android 游戏构建,您需要下载并安装Android 构建支持组件:

![Figure_1.16 – 选择组件

![Figure_1.16_B17146.jpg]

图 1.16 – 选择组件

  1. 点击下一步按钮,然后您需要选择下载和安装位置

![Figure_1.17 – 选择下载和安装位置

![Figure_1.17_B17146.jpg]

图 1.17 – 选择下载和安装位置

  1. 在指定下载和安装这些文件的位置后,点击下一步按钮下载 Unity:

![Figure_1.18 – 下载和安装

![Figure_1.18_B17146.jpg]

图 1.18 – 下载和安装

下载和安装完成后,Unity 编辑器图标将出现在您的桌面上。

探索 Unity 编辑器

我们需要做的第一件事是使用 Unity Hub 创建一个新的 Unity 项目。正如我在上一节中提到的,我们将通过点击项目视图右上角的新建项目按钮来创建一个全新的项目。

![Figure_1.19 – 创建新项目

![Figure_1.19_B17146.jpg]

图 1.19 – 创建新项目

如前一张截图所示,我们可以为这个新项目选择不同的 Unity 编辑器版本,Unity 为我们提供了一些内置的项目模板,例如2D3DHDRPURP模板。您还可以从 Unity 下载并安装更多模板,例如VR模板和AR模板。在项目设置部分,您可以设置项目的名称和项目位置。

在这里,我们将选择默认的 UnityBook。然后,点击 创建项目 按钮。之后,您之前选择的 Unity 编辑器将启动并为您打开一个新项目:

图 1.20 – Unity 编辑器

图 1.20 – Unity 编辑器

如前一张截图所示,Unity 编辑器布局为您组织了最重要的窗口。具体来说,默认布局将编辑器界面分为五个关键区域。从上到下,它们如下所示:

  1. 工具栏

  2. 层级窗口

  3. 场景视图和游戏视图

  4. 检查器窗口

  5. 项目窗口

接下来,我将按顺序介绍这些 UI 区域。

工具栏

工具栏始终位于 Unity 编辑器界面的顶部,它由几个控制组组成:

图 1.21 – 工具栏

图 1.21 – 工具栏

从左到右,工具栏中的第一个工具是 变换工具集。变换工具用于 场景 视图,允许您在场景中平移并移动、旋转和缩放单个 GameObject:

图 1.22 – 变换工具集

图 1.22 – 变换工具集

下一个工具是 ** Gizmo 处理位置切换集**,它用于定义 场景 视图中任何变换工具 Gizmo 的位置:

图 1.23 – Gizmo 处理位置切换集

图 1.23 – Gizmo 处理位置切换集

然后,您可以在中间找到 播放、暂停和单步按钮。您可以在 游戏 视图中使用这些按钮:

图 1.24 – 播放、暂停和单步按钮

图 1.24 – 播放、暂停和单步按钮

在右侧,让我们首先看看 Unity Plastic SCM 按钮,它允许您直接在 Unity 编辑器中访问 Plastic SCM 版本控制和源代码管理工具。您可以点击 按钮打开 Unity 服务 窗口,在那里您可以访问 Unity 提供的许多云服务,例如 云构建 服务、分析 服务和 广告 服务。

您也可以从 账户 下拉菜单访问您的 Unity 账户。右侧还有两个其他下拉菜单,即 图层布局;您可以使用 图层 下拉菜单控制 场景 视图中哪些对象出现,并使用 布局 下拉菜单更改或创建新的 Unity 编辑器布局:

图 1.25 – Unity Collaborate 和 Unity Services 按钮,以及 Unity 账户、图层和布局下拉菜单 Unity 账户、图层和布局下拉菜单

图 1.25 – Unity Collaborate 和 Unity Services 按钮,以及 Unity 账户、图层和布局下拉菜单

层级窗口

第二个区域是层次结构窗口。如图所示,Unity 编辑器中的层次结构窗口以场景的形式显示所有内容;场景中的事物,如主摄像机方向光和 3D 立方体,被称为游戏对象

我们还可以在层次结构窗口中组织游戏世界中所有的对象:

![图 1.26 – 层次结构窗口图片 1.26

图 1.26 – 层次结构窗口

在场景中创建新的游戏对象非常简单。你只需要在层次结构窗口上右键单击,就会弹出一个菜单,你可以从中选择要创建的对象:

![图 1.27 – 创建新的游戏对象图片 1.27

图 1.27 – 创建新的游戏对象

值得注意的是,Unity 使用父子层次结构来组织游戏对象,因此你可以创建一个对象作为另一个对象的子对象。如果你想创建一个新的游戏对象作为另一个游戏对象的子对象,那么你只需要首先选择父游戏对象,然后右键单击以创建子游戏对象:

![图 1.28 – 父子层次结构图片 1.28

图 1.28 – 父子层次结构

创建父子层次结构的另一种方法是直接将现有的游戏对象拖动到层次结构窗口中的父游戏对象上:

![图 1.29 – 父子层次结构图片 1.29

图 1.29 – 父子层次结构

如前图所示,我们将名为Cube的游戏对象拖动到名为Child的游戏对象上以创建父子层次结构。

![图 1.30 – 隐藏和显示游戏对象图片 1.30

图 1.30 – 隐藏和显示游戏对象

层次结构窗口的另一个功能是,它允许你在场景视图中隐藏和显示游戏对象,而不会改变它们在游戏视图或最终应用程序中的可见性。

场景视图和游戏视图

默认 Unity 编辑器布局的中心是场景视图和游戏视图,这是 Unity 编辑器中最重要的窗口。场景视图是你正在创建的游戏世界的交互式视图:

![图 1.31 – 场景视图图片 1.31

图 1.31 – 场景视图

你可以使用场景视图来操纵游戏对象,并从不同的角度查看它们。此外,场景视图中还有一些有用的工具,例如位于场景视图右上角的场景Gizmo 工具:

![图 1.32 – 场景 Gizmo 工具图片 1.32

图 1.32 – 场景 Gizmo 工具

它显示了场景视图相机的当前方向,并允许你快速修改视图角度和投影模式。

如果你想修改场景视图相机的设置,你可以点击Gizmos按钮旁边的相机按钮以打开场景相机设置窗口:

![图 1.33 – 场景相机设置

![Figure_1.33_B17146.jpg]

Figure 1.33 – The Scene Camera settings

在这里,您可以调整场景视图相机的某些设置,例如视野相机速度

可视网格是您可以在场景视图中使用的另一个有用工具,可以帮助您通过将 GameObject 移动到最近的网格位置来对齐 GameObject:

![Figure 1.34 – Toggle the visibility of the grid]

![img/Figure_1.34_B17146.jpg]

Figure 1.34 – 切换网格的可见性

如您在前面的屏幕截图中所见,您还可以将 GameObject 移动到沿XYZ轴投影的网格上。

我想介绍的最后一个有用的场景视图工具是绘制模式

![Figure 1.35 – The draw mode in the Scene]

![Figure_1.35_B17146.jpg]

Figure 1.35 – The draw mode in the Scene

如果您的项目使用 Unity 的内置渲染管线,这很有用,因为场景中的不同绘制模式可以帮助您理解和调试其中的光照。

在默认布局中,游戏视图也出现在与场景视图相同的区域。您可以通过点击游戏按钮从场景视图切换到游戏视图:

![Figure 1.36 – Click the Game button to switch to the Game view]

![img/Figure_1.36_B17146.jpg]

![Figure 1.36 – 点击游戏按钮切换到游戏视图]

游戏视图代表您最终发布的游戏。游戏视图的内容是从您游戏中的相机渲染的。在游戏视图中,您不能像在场景视图中那样随意修改观看角度和投影模式。您需要修改相机对象设置以实现此功能:

![Figure 1.37 – The Game view]

![img/Figure_1.37_B17146.jpg]

![Figure 1.37 – The Game view]

您可以直接在游戏视图中运行您的游戏,通过在工具栏上点击播放按钮。需要注意的是,在播放模式下,您所做的任何更改都是临时的,并在退出时重置;因此,在播放模式下进行大量更改不是一个好主意。

我想介绍游戏视图中三个工具,即纵横比播放时最大化统计信息

纵横比下拉菜单在您为不同纵横比的不同屏幕开发游戏时非常有用。您可以选择不同的值来测试您的游戏在这些屏幕上的外观,并且您甚至可以通过点击菜单底部的加号按钮添加自定义值:

![img/Figure_1.38_B17146.jpg]

Figure 1.38 – The Free Aspect drop-down menu

第二个功能称为播放时最大化,当您进入播放模式时,它可以最大化游戏视图以进行全屏预览:

![img/Figure_1.39_B17146.jpg]

Figure 1.39 – The Maximize On Play button

第三个功能称为统计信息。这个功能很有用,因为它可以显示关于您游戏音频和图形的渲染统计信息。因此,您可以在播放模式下使用它来监控您游戏的表现:

![img/Figure_1.40_B17146.jpg]

图 1.40 – 统计窗口

场景视图中,你可以查看和调整你正在创建的游戏世界。在游戏视图中,你可以看到你的最终游戏。因此,这个区域在编辑器中非常重要。接下来,让我们看看与场景中特定 GameObject 相关的 UI 区域。

检查器窗口

如果你想要修改 GameObject 或 GameObject 上的组件的属性,你需要使用检查器窗口。

你可以在场景视图或层次结构窗口中选择一个 GameObject,然后你将在检查器窗口中看到它的属性和组件:

图 1.41 – GameObject 的检查器窗口

你可以直接在检查器窗口中修改这些属性,该窗口还提供了一些有用的工具,可以帮助你修改你的 GameObject。

例如,如果你想复制 GameObject 上组件的值,你可以在组件上右键单击,然后会弹出一个菜单;从那里,你可以选择复制组件命令:

图 1.42 – 复制组件

不仅可以在场景视图中检查 GameObject,还可以在项目窗口中检查数字资源。你可以在项目窗口中选择一个数字资源,然后检查器窗口将显示控制 Unity 在运行时导入和使用该资源的设置的设置:

图 1.43 – 资源的检查器窗口

在本节中,我们学习了如何通过检查器窗口查看和修改 GameObject 和资源的属性。

项目窗口

我将要介绍的最后一个窗口是文件夹中的assets文件:

图 1.44 – 项目窗口

项目窗口是导航和查找游戏内资源的主要方式。它提供了两种搜索资源的方式,按类型或标签:

图 1.45 – 通过类型搜索资源

在 Unity 编辑器中导入外部数字资源或直接创建资源非常容易。你只需在项目窗口上右键单击,就会弹出一个菜单,你可以从中创建一个新的资源或导入现有的资源:

图 1.46 – 创建资源

希望通过阅读本节,你现在对 Unity 编辑器有了很好的理解。接下来,我将介绍什么是游戏引擎,以及 Unity 作为游戏引擎提供了哪些重要功能。

在 Unity 中处理不同的功能

现在,Unity 不再仅仅是一个游戏引擎,而是一个在各个行业中广泛使用的创意工具。然而,Unity 仍然保留了其游戏引擎的根源,并且仍然是最受欢迎的游戏引擎之一。要学习如何使用 Unity 开发游戏,你必须首先了解 Unity 作为游戏引擎为游戏开发者提供了哪些功能。

实际上,几乎所有的游戏引擎都为游戏开发者提供了类似于 Unity 的功能模块。所以,第一个问题是,游戏引擎究竟是什么?

什么是游戏引擎?

游戏引擎这个术语在游戏行业中广泛使用,但并非每个人都了解这个术语的含义,尤其是新游戏开发者。所以,我将解释什么是游戏引擎,同时介绍 Unity 中的相应功能。

游戏引擎不仅仅是计算机图形渲染器。当然,渲染是游戏引擎的一个重要功能,但创建游戏的过程远比仅仅渲染复杂得多。

作为一名游戏开发者,您需要导入不同类型的数字资产,例如 3D 模型、2D 纹理和音频,而且这些数字资产中的大多数都不是在游戏引擎内部创建的。因此,游戏引擎应该提供管理数字资产的功能。除了数字资产之外,您还需要使用脚本来添加游戏逻辑,以引导这些资产执行正确的行为,例如角色交互。

UI是游戏中的另一个重要组成部分,甚至有些游戏玩法是基于 UI 的。因此,一个好的游戏引擎应该提供易于使用且功能强大的 UI 工具包,以开发游戏的用户界面。

您可以使用其他软件来开发动画文件并将它们导入到游戏引擎中,但为了使动画文件在游戏中正确播放和控制,游戏引擎需要提供动画功能。

同时,物理效果是现代游戏中常见的功能,因此一个强大的游戏引擎应该提供物理功能,以便游戏开发者不需要从头开始实现物理效果。

毫无疑问,将视频和音频添加到您的游戏中可以使您的游戏更加生动有趣。特别是音频,合适的背景音乐和一些适当的声音效果可以使您的游戏感觉完全不同。即使只是一个原型,背景音乐和声音效果也可以使游戏更加完整和更加专业。因此,尽管许多人经常在谈论游戏引擎时忽略视频和声音的功能,但我认为没有视频和音频功能的游戏引擎不是一个好的游戏引擎。

正如您所看到的,游戏引擎中有很多功能供游戏开发者开发他们的游戏。游戏引擎整合了创建游戏的各个方面,以创建完整的游戏用户体验。因此,在游戏开发中,您将处理不同的功能。例如,您可能需要适当地管理数字资产并为您的游戏引擎创建适当的数字资产以优化运行时的性能,或者您可能需要了解如何使用您所使用的游戏引擎提供的脚本功能来为您的游戏开发逻辑。

作为最受欢迎的游戏引擎之一,Unity 也提供了上述功能。在接下来的小节中,我将介绍 Unity 中的这些功能。

Unity 中的功能

与其他优秀的游戏引擎一样,Unity 也为游戏开发者提供了许多功能。您将在接下来的章节中了解这些功能。

图形

我要介绍的第一个功能是 Unity 中的 图形。您可以使用 Unity 的图形功能在各种平台上创建美观、优化的图形:

图 1.47 – Unity HDRP 模板场景

渲染管线执行一系列操作,将场景内容渲染到屏幕上。Unity 中有三种可用的渲染管线:

  • 内置渲染管线,这是 Unity 中的默认渲染管线。您无法修改此渲染管线。

  • 通用渲染管线URP),它允许开发者针对不同平台自定义和创建优化的图形。

  • 高清晰度渲染管线HDRP),它专注于高端平台上的尖端、高保真图形。

此外,您还可以通过使用 Unity 中的 可脚本化渲染管线 API 来创建自己的渲染管线。我们将在第八章,“Unity 中的可脚本化渲染管线”中详细介绍它。

脚本

脚本是 Unity 的另一个基本功能。您需要脚本来实现游戏中的游戏逻辑。

Unity 引擎内部使用原生 C/C++ 构建,但它提供了 C# 脚本 API,因此您不需要学习 C/C++ 就可以创建游戏。在接下来的章节中,您将了解更多关于脚本的概念。

UI

UI 对于游戏非常重要,Unity 为游戏开发者提供了三种不同的 UI 解决方案:

  • 即时模式图形用户界面IMGUI

  • Unity UIuGUI)包

  • UI 工具包

IMGUI 是 Unity 中相对较旧的 UI 解决方案,不建议用于构建运行时 UI。UI Toolkit 是最新的 UI 解决方案;然而,它仍然缺少您可以在 uGUI 包和 IMGUI 中找到的一些功能。uGUI 包是 Unity 中成熟的 UI 解决方案,在游戏行业中得到广泛应用。我们将在第三章,“使用 Unity UI 系统开发 UI”中详细介绍 uGUI 包。

动画

动画可以使您的游戏更加生动。Unity 提供了一个强大的动画功能,称为 Mecanim,它允许您重定向动画,在运行时控制其权重,并从动画播放中调用事件。

我们将在第四章,“使用 Unity 动画系统创建动画”中介绍 Unity 的动画系统。

物理引擎

物理模拟是某些类型游戏中的必备功能,某些游戏玩法甚至完全基于物理模拟。Unity 中有不同的物理引擎实现,您可以根据游戏需求选择一个。

我们将在 第五章 中介绍 Unity 的物理引擎实现,使用 Unity 物理系统

视频 和 音频

优秀的背景音乐、音效和视频可以使您的游戏脱颖而出。这是一个不容忽视的功能。Unity 提供了 视频和音频 功能,允许您的游戏在不同的平台上播放视频,并支持实时混音和全 3D 空间音效。

我们将在 第六章 中更详细地讨论视频和音频,在 Unity 项目中集成音频和视频

资产

您可以将您的数字资产文件导入到 Unity 编辑器中,例如 3D 模型和 2D 纹理。Unity 提供了一个资产导入管道来处理这些导入的资产。您还可以自定义导入设置来控制 Unity 在运行时如何导入和使用这些资产。

我们将在 第十章 中介绍 资产管理序列化Unity 和 Azure 中的序列化系统和资产管理

我们简要介绍了游戏引擎需要提供的功能以及 Unity 提供的功能。接下来,让我们介绍 .NET/C# 和 Unity 中的脚本。

.NET/C# 和 Unity 脚本

Unity 是一个用 C/C++ 编写的游戏引擎,但为了使游戏开发者更容易开发游戏,Unity 提供了 C#(发音为 C-sharp)作为脚本编程语言,以便在 Unity 中编写游戏逻辑。这是因为与 C/C++ 相比,C# 更容易学习。此外,它是一种“托管语言”,这意味着它会自动为您管理内存 – 分配和释放内存,防止内存泄漏等。

在本节中,我们将介绍 .NET/C# 和 Unity 中的脚本。

Unity 中的 .NET 配置文件

Unity 游戏引擎使用 Mono,一个开源的 ECMA CLI、C# 和 .NET 实现,用于脚本。您可以在 GitHub 上跟踪 Unity 对 Mono 的分支开发:github.com/Unity-Technologies/mono/tree/unity-master-new-unitychanges

Unity 提供了不同的 .NET 配置文件。如果您使用的是 Unity 的旧版本,即在 Unity 2018 之前,您可能会发现它在 Player 设置面板中提供了两个 API 兼容性级别(编辑 | 项目设置 | Player | 其他设置),即 .NET 2.0 子集.NET 2.0。首先,如果您使用的是 Unity 的旧版本,那么我强烈建议您更新您的 Unity 版本。其次,Unity 中的 .NET 2.0 子集.NET 2.0 配置文件与微软的 .NET 2.0 配置文件紧密一致。

如果您使用的是 Unity 的现代版本,即 Unity 2019 或更高版本,您会发现 Unity 支持另外两个 .NET 配置文件,即 .NET Standard 2.0.NET 4.x

图 1.48 – Api 兼容性级别设置

注意

.NET Standard 2.0 配置文件的名字可能有点误导,因为它与 Unity 旧版本中的.NET 2.0.NET 2.0 子集配置文件无关。

.NET Standard 是.NET API 的正式规范,所有.NET 平台都必须实现。这些.NET 平台包括.NET Framework、.NET Core、Xamarin 和 Mono。你可以在 GitHub 上找到.NET Standard 仓库:github.com/dotnet/standard

另一方面,Unity 中的.NET 4.x 配置文件与.NET Framework 的.NET 4 系列配置文件(.NET 4.5、.NET 4.6、.NET 4.7 等)相匹配。

因此,在 Unity 中使用.NET Standard 2.0 配置文件是个不错的选择,而且你应该只为了兼容性原因选择.NET 4.x 配置文件。

Unity 中的脚本后端

除了.NET 配置文件外,Unity 还提供了两种不同的脚本后端,分别是MonoIL2CPP(代表中间语言到 C++):

图 1.49 – 脚本后端设置

你可以在相同的设置面板中更改项目的脚本后端,该面板可以通过访问编辑 | 项目设置 | 玩家 | 其他设置找到。

两种脚本后端之间的关键区别在于它们如何编译你的 Unity 脚本 API 代码(C#代码):

  • Mono 脚本后端使用即时编译JIT)编译,在运行时按需编译代码。它将你的 Unity 脚本 API 代码编译成常规.NET DLL。而且,正如我在前面的章节中提到的,Unity 使用一个支持 C#的 Mono 运行时的实现来执行脚本。

  • 或者,IL2CPP 脚本后端使用即时编译AOT)编译,在运行前编译整个应用程序。它不仅将你的 Unity 脚本 API 代码编译成.NET DLL,还将所有托管程序集转换为标准 C++代码。此外,IL2CPP 的运行时是由 Unity 开发的,它是 Mono 运行时的替代品:

图 1.50 – IL2CPP 脚本后端

图 1.50所示,IL2CPP 不仅将 C#代码编译成托管程序集,而且还进一步将程序集转换为 C++代码,然后编译 C++代码成原生二进制格式。

显然,与 Mono 相比,IL2CPP 编译代码需要更多的时间,那么为什么我们还需要 IL2CPP 呢?

好吧,首先,IL2CPP 使用 AOT 编译,编译时间较长,但当你为特定平台发布游戏时,二进制文件是完整指定的,这意味着与 Mono 相比,代码生成得到了极大的改进。

其次,值得注意的是,当为iOSWebGL构建时,IL2CPP 是唯一的脚本后端。除了 iOS 和 WebGL,Unity 在 Unity 2018.2 中增加了对Android 64 位的支持,以符合Google Play 商店政策,该政策要求从 2019 年 8 月 1 日起,你在 Google Play 上发布的应用需要支持 64 位架构:

图片

图 1.51 – Android 64 位 ARM 架构不支持 Mono 脚本后端

如前述截图所示,Android 64 位 ARM 架构不支持 Mono 脚本后端。在这种情况下,你必须选择 IL2CPP 脚本后端。

因此,无论我们是为了更好的代码生成还是针对某些特定的平台或架构使用 IL2CPP,花费更多的时间进行编译仍然是 IL2CPP 的缺点。那么,我们应该如何优化 IL2CPP 的编译时间呢?我认为以下提示会有所帮助:

  • 不要删除之前的build文件夹,并在与文件夹相同的目录下使用 IL2CPP 脚本后端构建你的项目。这是因为我们可以使用增量构建,这意味着 C++编译器只重新编译自上次构建以来已更改的文件。

  • 将你的项目和目标构建文件夹存储在固态硬盘SSD)上。这是因为当选择 IL2CPP 时,编译过程会将 IL 代码转换为 C++并编译它,这涉及到大量的读写操作。一个更快的存储设备将加快这一过程。

  • 在构建项目之前,请禁用防病毒软件。当然,这取决于你的安全策略。

好吧,我希望通过阅读本节,你现在对 Unity 的脚本系统有了大致的了解,例如 Unity 中的.NET 配置文件、两个脚本后端以及一些 IL2CPP 的优化技巧。

在下一节中,你将学习如何设置你的开发环境并使用广泛使用的 Visual Studio 在 Unity 中开发游戏。

使用 Visual Studio 构建 Unity 游戏

在开始编写任何代码之前,选择合适的发展工具非常重要。微软的Visual Studio不仅是一个广泛使用的 IDE,而且当你在 Windows 或 macOS 上安装 Unity 时,它还是默认安装的开发环境:

图片

图 1.52 – Visual Studio 安装程序

在安装 Visual Studio 时,Visual Studio Tools for Unity也会被安装。这是一个免费扩展,它为在 Unity 中编写和调试 C#提供支持。

如果你没有通过 Unity Hub 安装 Visual Studio,请确保你已安装此扩展。你可以在Visual Studio 安装程序中检查:

图片

图 1.53 – 安装 Visual Studio Tools for Unity

安装 Unity 编辑器和 Visual Studio Community 2019 后,你可以在 Unity 编辑器的首选项窗口中检查外部脚本编辑器设置:

图 1.54_B17146

图 1.54 – 外部脚本编辑器设置

此外,您还可以通过修改此设置来选择其他脚本编辑器,例如Visual Studio CodeJetBrains Rider

然后,我们可以在 Unity 编辑器中创建一个名为NewBehaviourScript的新 C#脚本文件,并双击它以在 Visual Studio 中打开:

图 1.55_B17146

图 1.55 – Unity API 的 IntelliSense

如前述截图所示,脚本文件中默认有两个内置方法,即StartUpdate。Visual Studio 支持 Unity API 的IntelliSense,因此我们可以快速编写代码:

图 1.56_B17146

图 1.56 – 调试您的代码

在 Visual Studio 中调试您的代码也非常容易。在前述截图中,我在Start方法内设置了断点,并在 Visual Studio 中点击了附加到 Unity按钮:

图 1.57_B17146

图 1.57 – 点击“附加到 Unity”按钮

为了运行此代码,我将此脚本附加到场景中的 GameObject 上,并在 Unity 编辑器中点击播放按钮以在游戏视图中运行游戏。

图 1.58_B17146

图 1.58 – 调试器在断点处停止

然后,调试器将在断点处停止,您可以看到游戏当前的状态。

摘要

在本章中,我们首先选择了适合您需求的 Unity 发布和订阅计划。然后,您学习了如何使用 Unity Hub 安装和管理 Unity 编辑器,并探索了 Unity 编辑器的五个重要区域——工具栏、层次结构窗口、场景视图和游戏视图、检查器窗口和项目窗口。然后,您被介绍到 Unity 编辑器的工具栏和 Unity 提供的窗口。我们还讨论了游戏引擎是什么,并探索了 Unity 为开发者提供的不同游戏开发功能。然后,我们介绍了 Unity 中的.NET 配置文件和 Unity 提供的脚本后端;您现在应该知道 Mono 脚本后端和 IL2CPP 脚本后端的区别。最后,我们演示了如何为 Unity 编辑器设置 Visual Studio 以编写代码。

在下一章中,我们将从 Unity 脚本的基本概念详细介绍开始,例如 GameObject、组件,以及一些特殊且重要的组件,如Transform。我们还将向您介绍脚本实例的生命周期。然后,我们将讨论如何通过脚本创建对象以及如何通过 C#代码访问 GameObject 或组件。还将介绍 Unity 脚本的一些最佳实践。最后,我们将介绍 Unity 中的包管理器

第二章:第二章:Unity 中的脚本概念

在上一章中,我们以高级别讨论了 Unity 中的脚本。在本章中,我们将详细介绍这个主题。我们已经知道 Unity 内部是用 C/C++编写的,但它为游戏开发者提供了许多 C# API,并允许我们用 C#实现游戏逻辑。这意味着我们不仅可以编写自己的类,还可以使用许多内置类。因此,在创建自己的 C#类之前,让我们先了解一下 Unity 的内置类。Unity 脚本的生命周期也是另一个重要的话题,因为我们需要使用 Unity 提供的事件函数来实现游戏逻辑。然后,我们将介绍如何在 Unity 编辑器中创建脚本并将其用作组件。

在本章中,我们将涵盖以下关键主题:

  • 理解 Unity 脚本的概念

  • 脚本实例的生命周期

  • 创建脚本并将其用作组件

  • 包和 Unity 包管理器

技术要求

您可以在以下存储库中找到完整的代码示例:github.com/PacktPublishing/Game-Development-with-Unity-for-.NET-Developers

在开始之前,我想提到本章将使用以下软件:

  • Visual Studio 2019

  • Unity 的 Visual Studio 工具

  • Unity 2020.3+

理解 Unity 脚本的概念

让我们从理解 Unity 脚本的概念开始。我们知道 Unity 不是一个开源引擎;除了企业用户和订阅了 Pro 计划的用户外,其他人无法访问 Unity 的源代码。然而,Unity 的 C# API 是开源的。因为 C# API 只是一个包装器,它不包括引擎的内部逻辑。但 Unity 的开源 C# API 也是我们理解 Unity 脚本编程的好参考。您可以在 GitHub 上访问它:github.com/Unity-Technologies/UnityCsReference

GameObject-组件架构

首先,我想让您知道 Unity 是一个基于组件的系统。因此,在 Unity 游戏开发中您经常听到的两个术语是GameObject组件。GameObject 不过是一个组件的容器。它代表游戏世界中的对象,但它本身没有任何功能。另一方面,组件实现了真正的功能,并且可以被附加到 GameObject 上,为特定对象提供功能。

![图 2.1 – 带有相机组件的主相机 GameObject]

![图 2.01_B17146.jpg]

图 2.1 – 带有相机组件的主相机 GameObject

例如,您可以在 Unity 编辑器中的默认场景中找到一个主相机对象。它是通过将相机组件附加到 GameObject 创建的。

你可以通过启用或禁用 GameObject 或特定组件来启用或禁用附加到此对象的函数集,从而启用或禁用特定功能。

这种方法与传统面向对象编程不同。它有点像乐高积木;当一个对象需要某种类型的函数时,你只需向其中添加相关组件即可。

Unity 中的常用类

Unity 提供了许多内置的 C# 类,所以我将介绍一些我们在 Unity 开发中经常使用的类。

MonoBehaviour 类

在 Unity 开发中,你最常遇到的是 MonoBehaviour 类。这是因为它是所有 Unity 脚本的基类。

让我们在 Unity 编辑器中创建一个新的脚本文件,并将其命名为 ChapterTwo.cs

图 2.2 – 默认脚本

图 2.2 – 默认脚本

然后,我们通过双击它在 Visual Studio 中打开它。你可以看到我们的新 ChapterTwo 类继承自 MonoBehaviour

那么,为什么 MonoBehaviour 那么重要呢?因为它为游戏开发者提供了一个与 Unity 引擎交互的框架。例如,如果你想将脚本附加到场景中的 GameObject 上,该类必须继承自 MonoBehaviour 类;否则,脚本无法添加到 GameObject。当你尝试将不继承自 MonoBehaviour 的类附加到 GameObject 上时,Unity 编辑器将弹出以下错误消息:

图 2.3 – 无法添加脚本错误

图 2.3 – 无法添加脚本错误

没有使用 MonoBehaviour 类,你的代码将无法访问 Unity 的内置方法和事件,例如在每一个新的脚本文件中默认创建的 StartUpdate 函数。

MonoBehaviour 是 Unity 中最重要的类。StartUpdate 是 Unity 中最常见的内置函数。每次你创建一个新的脚本文件时,它们都会出现在这个新文件中。但如果你想修改创建脚本的模板,这也是可能的;你只需修改存储在这里的脚本模板:

  • %EDITOR_PATH%\Data\Resources\ScriptTemplates

  • %EDITOR_PATH%/Data/Resources/ScriptTemplates

图 2.4 – ScriptTemplates 文件夹

图 2.4 – ScriptTemplates 文件夹

GameObject 类

我们已经知道,场景中的对象被称为 GameObject 类来表示它。

当你在场景中创建一个新的空 GameObject 时,你会发现这个新的 GameObject 包含一个名称、一个标签、一个层和一个 Transform 组件。

图 2.5 – GameObject

图 2.5 – GameObject

您也可以从检查器窗口中修改它是否是一个静态对象。如果 GameObject 在运行时不会移动,您应该在检查器窗口右上角检查静态属性复选框。这是因为 Unity 中的许多系统可以在编辑器中预先计算静态 GameObject 的信息,以提高运行时的性能。

正如我们之前提到的,GameObject 是一个可以包含各种组件的容器。因此,在脚本编写中,GameObject类主要提供了一套用于管理组件的方法,例如AddComponent方法用于向 GameObject 添加新组件,以及GetComponent方法用于访问附加到 GameObject 的组件。

让我们在场景中创建一个内置的 3D 立方体对象,并查看这个立方体的检查器窗口。

![Figure 2.6 – 场景中的立方体对象

![Figure 2.06 – B17146.jpg]

Figure 2.6 – 场景中的立方体对象

如您在前面的截图中所见,这个 GameObject 被称为Cube,并且有四个附加到这个立方体对象上的组件,即TransformCube (Mesh Filter)Mesh RendererBox Collider。这些组件为该对象提供了渲染和物理模拟功能。因此,GameObject 只是组件的容器,具体功能来自特定的组件。您可以通过在检查器窗口中点击添加组件按钮来添加新组件,或者通过代码在运行时添加组件。

除了组件外,GameObject类还提供了一系列方法来查找其他 GameObject,在 GameObject 之间发送消息,或创建和销毁 GameObject。例如,您可以使用GameObject.Find方法通过名称查找 GameObject 并返回它,或者使用GameObject.FindWithTag方法通过标签查找 GameObject。您还可以使用Instantiate方法创建一个新的GameObject实例,以及使用Destroy方法销毁一个GameObject实例。

![Figure 2.7 – 类中的[SerializeField]属性

![Figure 2.07 – B17146.jpg]

Figure 2.7 – 类中的[SerializeField]属性

值得注意的是,使用某些方法在运行时动态查找特定的 GameObject 实例将带来额外的开销,因此获取另一个 GameObject 实例引用的最简单方法是通过声明一个公共GameObject字段或使用[SerializeField]属性并声明一个私有字段来保持类的封装性。如图所示,我更喜欢第二种方法。我们将在后面的章节中进一步介绍 Unity 中的序列化。

现在,您会发现GameObject字段在检查器中是可见的。您只需将 GameObject 从场景或层次结构面板拖动到这个变量上即可分配它。

![Figure 2.8 – GameObject 变量

![Figure 2.08 – B17146.jpg]

Figure 2.8 – GameObject 变量

Transform 类

当您在场景中创建一个新的 GameObject 时,将自动创建Transform类的实例。这是因为场景中的每个 GameObject 都有位置、旋转和缩放属性,而Transform类用于在 Unity 中存储和操作 GameObject 的位置、旋转和缩放。因此,在 Unity 中创建 GameObject 而不包含Transform组件是不可能的,并且您也不能从 GameObject 中移除它。

您可以通过直接修改 Unity 编辑器中Transform组件的属性来移动、旋转或缩放 GameObject,或者您可以在运行时通过访问Transform类的实例来修改它们。

![图 2.9 – 变换组件图片

图 2.9 – 变换组件

Unity 中的预制件

预制件是 Unity 中的一个重要概念。游戏开发者可以使用预制件来保存 GameObject、组件和属性,以便在用 Unity 开发游戏时重用这些资源。在实例化预制件时,预制件充当资源模板。接下来,让我们看看如何在 Unity 中创建一个新的预制件。

如何创建预制件

首先,让我们谈谈如何创建预制件。以一个“哑铃”为例。它由一个立方体和两个球体对象组成。

![图 2.10 – 如何创建预制件图片

图 2.10 – 如何创建预制件

我们可以通过以下步骤创建这个哑铃对象的预制件:

  1. 首先,在层次面板中找到名为BarbellObject的目标 GameObject,如图图 2.10所示。

  2. 将目标 GameObject 从层次面板拖动到项目面板以创建其预制件。新创建的预制件文件在 Unity 编辑器中显示为蓝色立方体图标。

![图 2.11 – 预制件文件图片

图 2.11 – 预制件文件

  1. 在这一点上,如果我们再次查看层次面板,我们可以发现BarbellObject的名称文本以及它左侧的小立方体图标已经从白色变为蓝色,因为它现在是一个预制件实例。这样,我们就可以在层次面板上区分一个对象是否是预制件实例。

![图 2.12 – 预制实例图片

图 2.12 – 预制件实例

如您所见,创建一个新的预制件并不复杂。接下来,让我们探讨如何编辑已创建的预制件。

如何编辑预制件

Unity 为开发者提供了两种编辑预制件的方法,如下所示:

  • 第一种方法是在预制件模式中编辑预制件。

  • 第二种方法是通过其实例编辑预制件。

让我们先从预制件模式开始。

预制件模式是一种专门设计来支持单独编辑预制件的模式。预制件模式允许在单独的场景中查看和编辑预制件的内容。您可以通过以下方式进入预制件模式:

  1. 第一种方法是点击层次视图中预制件实例的箭头按钮。

![图 2.13 – 进入预制件模式图片

图 2.13 – 进入预制模式

  1. 第二种方法是选择项目面板中的预制件文件。在检查器面板中会显示一个带有“打开预制件”字样的按钮。点击它以进入预制模式。

![图 2.14 – 进入预制模式

![图 2.14 – 进入预制模式

图 2.14 – 进入预制模式

  1. 您也可以在项目面板中双击预制件文件以进入预制模式。

进入预制模式后,您可以在此处修改预制件,您还可以发现导航栏将显示在场景视图上方,如图下截图所示:

![图 2.15 – 预制模式

![图 2.15 – 预制模式

图 2.15 – 预制模式

  1. 使用导航按钮在游戏场景和预制模式之间切换。此外,在层次结构视图的顶部,还会显示一个标题栏,显示当前打开的预制件名称。点击标题栏中的左箭头按钮也可以用来返回游戏场景。

除了预制模式外,我们还可以通过修改层次结构面板中的预制件实例来修改预制件。让我们按照以下步骤修改BarbellObject预制件:

  1. 选择 12 之间的一个球体,如图下截图所示:

![图 2.16 – 修改预制件实例

![图 2.16 – 实例化预制件

图 2.16 – 修改预制件实例

  1. 当选择预制件实例的根节点时,检查器面板中会出现三个按钮,即打开选择覆盖。点击覆盖下拉窗口可以查看所有修改的数据项,例如属性和组件。

![图 2.17 – 打开“覆盖”下拉窗口

![图 2.17 – 实例化预制件

图 2.17 – 打开“覆盖”下拉窗口

  1. 在这个下拉窗口中,我们可以丢弃或应用所有修改。在这里,我们应该点击应用全部按钮,将此修改应用于预制件。

![图 2.18 – 点击“应用全部”按钮

![图 2.18 – 实例化预制件

图 2.18 – 点击“应用全部”按钮

通过前面描述的两种方法,我们可以在 Unity 中轻松修改预制件。接下来,让我们谈谈如何使用 C# 代码在运行时实例化预制件。

如何实例化预制件

在 Unity 开发中,我们可以使用 Instantiate 方法在运行时创建预制件的实例。Instantiate 方法有几个变体。这里展示了常用的实例化方法变体:

public static Object Instantiate(Object original, Vector3
  position, Quaternion rotation);
public static Object Instantiate(Object original, Vector3
  position, Quaternion rotation, Transform parent);

我们使用 Instantiate 方法的这两种变体来实例化预制件,这两种变体都可以用来指定实例的位置和朝向,后者还可以指定实例的父级。

让我们通过以下示例学习如何通过调用 Instantiate 方法来实例化预制件:

  1. 首先,让我们创建一个名为 TestInstantiatePrefab 的新脚本。在这个脚本中,我们将为脚本中的 Prefab 分配一个引用,并调用 Instantiate 来创建这个 Prefab 的新实例,并为新对象分配一个父对象:

    using UnityEngine;
    public class TestInstantiatePrefab : MonoBehaviour
    {
        [SerializeField]
        private GameObject _prefab;
        [SerializeField]
        private Transform _parent;
        private GameObject _instance;
        private void Start()
        {
            var position = new Vector3(0f, 0f, 0f);
            var rotation = Quaternion.identity;
            _instance = Instantiate(_prefab, position,
              rotation, _parent);
        }
    }
    
  2. 然后,我们还需要将此脚本附加到场景中的 GameObject 上,将 Prefab 分配给此脚本的 _prefab 字段,并将此 GameObject 分配为稍后创建的 Prefab 实例的父对象,如下面的截图所示:

![图 2.19 – 设置组件和属性图片

图 2.19 – 设置组件和属性

  1. 点击 InstantiatePrefab 对象:

![图 2.20 – 创建 Prefab 的新实例图片

图 2.20 – 创建 Prefab 的新实例

在本节中,我们讨论了 Unity 中的一个重要概念,即 Prefab。通过阅读本节,你应该了解 Prefab 是什么,如何创建 Prefab,如何编辑 Prefab,以及如何使用 C# 代码在运行时实例化 Prefab。

Unity 中的特殊文件夹

除了上一节中介绍的一些常用类和概念外,Unity 中还有一些用于不同目的的特殊文件夹。其中一些文件夹与 Unity 中的脚本编写相关。它们如下所示:

  • 资产文件夹

  • 编辑器文件夹

  • 插件文件夹

  • 资源文件夹

  • StreamingAssets文件夹

让我们逐一来看。

资产文件夹

当创建 Unity 项目时,会创建一个 资产 文件夹来存储各种资源,从模型和纹理到将在本 Unity 项目中使用的脚本文件。这也是你在开发 Unity 项目时主要使用的文件夹。

编辑器文件夹

编辑器文件夹用于存储编辑器的脚本文件。例如,你可以在编辑器文件夹中创建一些编辑器脚本,以向默认的 Unity 编辑器添加更多功能。Unity 会根据脚本文件的位置,在四个独立阶段编译这些脚本。在每个阶段,Unity 都会为该阶段创建一个单独的 C# 项目文件(.csproj)。编辑器文件夹中的脚本在运行时不可用。如果编辑器文件夹位于 插件 文件夹中,则将创建一个名为 Assembly-CSharp-Editor-firstpass 的 CSharp 项目文件;否则,将创建一个名为 Assembly-CSharp-Editor 的 CSharp 项目文件。

插件文件夹

你应该将插件或需要首先编译的代码放在 插件 文件夹中,Unity 将首先编译此文件夹中的代码。对于位于此文件夹中的脚本,将创建一个名为 Assembly-CSharp-firstpass 的 CSharp 项目文件。Unity 将为所有其他脚本创建一个名为 Assembly-CSharp 的 CSharp 项目文件,这些脚本位于 资产 文件夹中,但不在 插件 文件夹和 编辑器 文件夹中。

![图 2.21 – 不同阶段的 CSharp 项目文件图片

图 2.21 – 不同阶段的 CSharp 项目文件

还有其他一些特殊文件夹,例如 Resources 文件夹和 StreamingAssets 文件夹。我们将在后面的章节中介绍它们。

在本节中,我们讨论了 Unity 的 GameObject 组件架构,并介绍了一些 Unity 中最常用的内置类以及与 Unity 脚本相关的特殊文件夹。接下来,我们将学习与 Unity 脚本相关的一个重要话题,即脚本实例的生命周期。

脚本实例的生命周期

在上一节中,我们介绍了 Unity 脚本的基本概念。现在,我们将解释与 Unity 脚本相关的一个重要话题:脚本实例的生命周期。

我们已经知道 Unity C# API 不包括引擎的内部逻辑,脚本上的事件函数是由引擎的 C/C++ 代码触发的。因此,为了正确使用 Unity 引擎,了解事件函数的执行顺序和 Unity 中 C# 脚本的生命周期非常重要。

根据它们的目的,我们可以将 Unity 事件函数分为以下几类:

  • 初始化

  • 更新

  • 渲染

让我们接下来讨论它们。

初始化

如果你熟悉 .NET 应用程序的开发,你可能会对 Unity 中的脚本初始化感到惊讶,因为 Unity 脚本不使用构造函数进行初始化。相反,Unity 提供了一些引擎事件函数来初始化脚本实例。

实际上,我们已经看到了一个用于初始化目的的 Unity 事件函数。是的,它是创建新的 Unity 脚本时默认创建的 Start() 函数。

然而,Start() 函数并不是在创建脚本的新实例时首先被触发的事件函数。当一个场景开始时,场景中每个对象的 Awake() 事件函数总是先于任何 Start() 函数被调用。除了 Awake() 会首先被调用之外,Start()Awake() 的工作方式相似。它们都在初始化期间被调用一次。现在,你可能会有一个疑问:既然我们已经有 Start 函数了,为什么我们还需要 Awake 函数?

这是因为 Awake 函数对于分离初始化很有用。例如,在游戏开始之前使用 Awake 来初始化对象的引用和变量是一个好主意。这意味着你不应该在 Awake 函数中访问其他对象的引用,而应该使用 Start 来传递不同对象的引用信息。

你可能会感到困惑,所以让我给你展示一些代码。让我们考虑一个有两个类的情况,即 AwakeAndStartAAwakeAndStartB。在第一个类中,有一个 List<int> 变量和一个 List<int> 属性,List 变量是在 AwakeAndStartAAwake 函数中设置的:

public class AwakeAndStartA : MonoBehaviour
{
    private List<int> _listRef;
    public List<int> ListRef => _listRef;
    private void Awake()
    {
        _listRef = new List<int>();
    }
}

现在,我们得到第二类:

public class AwakeAndStartB : MonoBehaviour
{
    private void Awake()
    {
      var comp =
       GameObject.Find("A").GetComponent<AwakeAndStartA>();
      Debug.Log($"comp is null > {comp is null}");
      Debug.Log(comp.ListRef.Count);
    }
}

AwakeAndStartB 类试图获取 AwakeAndStartA 类的引用,并在其 Awake 函数中访问 AwakeAndStartAListRef 属性。

如果我们运行代码,我们将得到以下输出;也就是说,对象 B 可以访问对象 A,但不能在 Awake 函数中访问对象 A 的变量或属性。这是因为我们不应该假设一个 GameObject 的 Awake 设置的引用可以在另一个 GameObject 的 Awake 中使用。

图 2.22 – 空引用异常

图 2.22 – 空引用异常

因此,为了在对象 B 中使用 ListRef,我们可以在 Start 函数中获取引用。让我们将打印列表中元素数量的代码从 Awake 函数移动到 Start 函数:

public class AwakeAndStartB : MonoBehaviour
{
    private void Start()
    {
      var comp =
       GameObject.Find("A").GetComponent<AwakeAndStartA>();
      Debug.Log($"comp is null > {comp is null}");
      Debug.Log(comp.ListRef.Count);
    }
}

这次,代码将打印出正确的数字,如图 图 2.23 所示:

图 2.23 – 列表中包含的元素数量为 0

图 2.23 – 列表中包含的元素数量为 0

Start 和 Awake 函数之间的另一个区别是,如果一个脚本组件在场景中没有启用,它的 Start 函数将不会被调用,但 Awake 函数总是会调用,正如您可以在下面的屏幕截图中所见:

图 2.24 – Awake 函数始终被调用

图 2.24 – Awake 函数始终被调用

有一个用于初始化的第三个事件函数,即 OnEnable 函数。如果脚本组件在场景中启用,则此函数将在 Awake 函数之后和 Start 函数之前被调用。然而,OnEnable 函数与 Awake/Start 函数之间有一个很大的区别;即 OnEnable 函数可以被多次调用。此函数在组件变为启用时被调用。

Update

对于一个游戏,Update 是一个非常重要的函数,因为游戏玩法逻辑是由 Update 驱动的。Unity 为不同目的提供了三个不同的 Update 函数。它们如下所示:

  • FixedUpdate

  • Update

  • LateUpdate

FixedUpdate 用于物理模拟。因此,如果您的游戏不包含物理模拟,则不应使用此函数。FixedUpdate 函数在每个固定帧率帧上被调用,并且它可以在单个帧中被多次调用。这是因为确保物理模拟中有固定增量时间非常重要。现在,您可能又感到困惑了。让我为您解释一下。

默认情况下,物理模拟需要每 0.02 秒更新一次。您可以在 项目设置 | 时间 | 固定时间步长 中更改此值。

图 2.25 – 固定时间步长设置

图 2.25 – 固定时间步长设置

让我们考虑一个游戏帧率本身较低的情况,例如,25 FPS。这意味着游戏将花费 0.04 秒来更新一帧。那么,问题是如何确保物理模拟的固定增量时间?

答案并不复杂。Unity 只需要在每个帧调用Update函数之前调用FixedUpdate两次,在这个例子中,FixedUpdate每 0.02 秒被调用一次。以下截图显示了结果:

![Figure 2.26 – 在一帧中 FixedUpdate 被调用两次Figure 2.26 – B17146.jpg

图 2.26 – 在一帧中 FixedUpdate 被调用两次

因此,只有在你项目中使用物理模拟时才使用FixedUpdate函数。如果你的项目不包含物理模拟,那么你不应该使用它。

Update函数是在创建新脚本时默认创建的另一个函数。它是 Unity 中实现任何类型游戏逻辑最常用且最重要的函数。如果脚本组件在场景中启用,则Update将在每帧调用一次。

用于更新的第三个函数是LateUpdate函数。正如其名称所示,LateUpdate将在Update函数之后被调用。因此,我们可以用它来实现每帧的两步更新。例如,你有一个在场景中的 GameObject 集合,需要在Update函数中移动和旋转,你将使用场景中的相机来跟踪这些 GameObject 的运动。为了确保所有 GameObject 都已经完全移动,你可以在LateUpdate函数中实现平滑的相机跟随。

渲染

对于一个游戏,除了游戏逻辑之外,另一个重要的方面是游戏的图形和渲染。在这里,我将介绍三个常用的渲染事件函数。它们如下:

  • OnBecameVisible/OnBecameInvisible

  • OnRenderImage

  • OnGUI

当渲染器对任何相机可见时,将调用OnBecameVisible,而OnBecameInvisible则相反。

![Figure 2.27 – OnBecameVisible/OnBecameInvisibleFigure 2.27 – B17146.jpg

图 2.27 – OnBecameVisible/OnBecameInvisible

如前一个截图所示,当 Cube 对象移出相机的视野时,OnBecameInvisible将被调用,如果它进入相机的视野,OnBecameVisible将被调用。

如果你的游戏逻辑非常复杂,那么你可以使用OnBecameVisible/OnBecameInvisible来避免不必要的性能开销。例如,当一个 GameObject 移出视野时,该 GameObject 的功能可以被暂停。

OnRenderImage 在 Unity 中用于实现 后处理 效果。这个函数将在场景完全渲染后调用,然后你可以将全屏效果应用到图像上,这可以大大提高你游戏的外观。以下截图显示了带有后处理和无后处理的图像之间的差异:

图 2.28 – 无后处理的场景(Unity)

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_2.28_B17146.jpg)

图 2.28 – 无后处理的场景(Unity)

图 2.29 所示,应用后处理增强了场景的整体外观,并产生了惊人的效果:

图 2.29 – 带有后处理的场景(Unity)

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_2.29_B17146.jpg)

图 2.29 – 带有后处理的场景(Unity)

值得注意的是,为了正确使用 OnRenderImage,你需要将实现此函数的脚本附加到相机组件所附加的 GameObject 上:

public class PostProcessing : MonoBehaviour
{
[SerializeField] 
private Material _mat;
    private void OnRenderImage(RenderTexture src,
      RenderTexture dest)
    {
        Graphics.Blit(src, dest, _mat);
    }
}

有时,你可能需要创建一些 UI 来进行原型设计或进行测试。然后,OnGUI 是你的理想选择。你可以创建一个 OnGUI 函数:

public class OnGUITest : MonoBehaviour
{
    private void OnGUI()
    {
        if (GUI.Button(new Rect(10, 10, 200, 100),
          "Button"))
        {
            Debug.Log("Hello World!");
        }
    }
}

GUI 行是一个 if 语句。这是因为当按钮被点击时,if 块中的代码需要被执行。具体来说,以前面的代码为例,当游戏运行且按钮被点击时,这个 if 语句返回 true 并执行 if 块中的 Debug.Log("Hello World") 行,在 控制台 窗口中打印出 Hello World

图 2.30 – IMGUI

图 2.30 – IMGUI

前面的截图显示了一个 IMGUI 按钮,以及点击此按钮在控制台窗口中打印的消息。

在本节中,我们解释了脚本实例的生命周期以及 Unity 引擎提供的常用事件函数。在下一节中,我们将探讨如何创建一个与引擎交互的脚本文件,并将其作为组件添加到场景中的 GameObject。

创建脚本并将其用作组件

除了 Unity 的内置组件之外,我们还可以创建脚本组件。当你创建一个脚本并将其附加到一个 GameObject 上时,你可以在 GameObject 的 检查器 窗口中看到你创建的组件,就像 Unity 的内置组件一样。

如何在 Unity 中创建一个新的脚本

在 Unity 中创建一个新的 C# 脚本非常简单。我将介绍两种不同的方法来实现这一点。

首先,你可以在 Unity 编辑器的 项目 面板中右键单击,然后会弹出一个菜单。你只需要选择 创建 | C# 脚本,然后 Unity 编辑器将在 项目 面板中指定的文件夹中创建一个 C# 文件。

图 2.31 – 从创建菜单创建新的 C# 脚本

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_2.31_B17146.jpg)

图 2.31 – 从创建菜单创建新的 C# 脚本

新脚本的默认文件名为 NewBehaviourScript.cs。你可以在创建时更改名称。

图 2.32 – 创建脚本时更改脚本名称

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_2.32_B17146.jpg)

图 2.32 – 创建脚本时更改脚本名称

例如,在之前的屏幕截图中,新的 C#文件将被创建在Assets/Chapter 2/Scripts文件夹中。这样,新创建的脚本将不会自动附加到场景中的 GameObject 上。您需要稍后手动将其添加到 GameObject。

另一方面,您也可以直接创建脚本并将其附加到 GameObject。您需要做的是在场景中选择一个 GameObject,然后点击项目中的Assets文件夹。

图 2.33 – 从检查器窗口创建新脚本

图 2.33 – 从检查器窗口创建新脚本

与在NewBehaviourScript.cs中创建新脚本类似。

图 2.34 – 创建脚本时更改脚本名称

图 2.34 – 创建脚本时更改脚本名称

如果您想在 IDE 中打开脚本,我们已将 Visual Studio 2019 设置为 Unity 项目的 IDE;您可以通过双击脚本文件在 Visual Studio 2019 中打开它。您会发现 C#类的名称与脚本文件的名称相同。

图 2.35 – C#类名和脚本文件名

图 2.35 – C#类名和脚本文件名

在场景中将脚本作为组件添加到 GameObject

在上一节中,我们介绍了如何创建一个新的脚本并将其自动附加到 GameObject 上。但我们仍然需要学习如何在编辑器中手动将脚本添加到 GameObject,并在运行时通过 C#代码将脚本组件添加到 GameObject。

在编辑器中向 GameObject 添加脚本组件

在 Unity 编辑器中将脚本作为组件添加到 GameObject 的最简单方法是拖动脚本文件到 GameObject。

然而,以下两种情况可能会导致脚本未添加到 GameObject:

  • 文件名和类名不同。这就是为什么在创建脚本时脚本名称与类名相同的原因。然而,您可能不小心更改了其中一个。因此,如果您无法将脚本添加到 GameObject,请首先检查文件名和类名。

  • 第二个原因相对明显:脚本中存在编译错误。在这种情况下,控制台窗口将打印出编译错误。您需要修复所有这些错误,以便可以将它们添加到 GameObject。

图 2.36 – 无法添加脚本消息

图 2.36 – 无法添加脚本消息

您还可以从检查器窗口将脚本组件添加到 GameObject。以下步骤演示了如何操作:

  1. 选择您想要附加脚本的 GameObject。

  2. 点击添加组件按钮,在检查器窗口中。不仅我们创建的脚本会被添加,而且许多内置组件也可以添加到 GameObject。

  3. 为了快速找到需要添加的脚本,我们可以在搜索框中输入脚本的名称。

  4. 最后,在下拉框中选择目标脚本。

图 2.37 – 从检查器窗口添加组件

图 2.37 – 从检查器窗口添加组件

在运行时向 GameObject 添加脚本组件

除了在编辑器中手动将脚本组件添加到 GameObject 之外,我们还可以在运行时通过 C# 代码将组件添加到 GameObject。

让我们打开在 Visual Studio 2019 中刚刚创建的 Test.cs 文件,并添加一个新字段,如图所示:

[SerializeField] 
private HelloWorld _helloWorld;

注意

字段是直接在类或结构体中声明的任何类型的变量。

在这里,你可以看到 HelloWorld 类型的私有字段名为 _helloWorld,你还会发现 _helloWorld 的声明上放置了一个 [SerializeField] 属性。这是为了允许 Unity 序列化这个私有字段。我们将在后面的章节中讨论 Unity 中的序列化系统,但你应该理解,当 Unity 序列化脚本时,它默认只序列化公共字段。如果一个变量可以被 Unity 序列化,那么它可以在 Unity 编辑器中显示和修改。因此,你可以在这里使用公共字段。然而,通常来说,只使用字段来存储具有私有或受保护访问级别的变量是一个好主意。这就是为什么 Unity 为开发者提供了 [SerializeField] 属性,这将强制 Unity 序列化私有字段。

然后,我们将 Test 脚本组件拖动到场景中的 GameObject 上,以将其添加到 GameObject。

图 2.38 – 带有测试组件的 GameObject

图 2.38 – 带有测试组件的 GameObject

你可以看到,在之前的屏幕截图中,附加到 GameObject 的 Test 脚本组件的序列化字段是 Test.cs 脚本,用于将 HelloWorld 脚本组件附加到同一个 GameObject,并将对新的 HelloWorld 组件的引用分配给此字段。

因为我们只想让代码运行一次,我们可以修改 Start 函数,如下所示:

    void Start()
    {
        _helloWorld =
          gameObject.AddComponent<HelloWorld>();
    }

这里,我们正在调用 AddComponent<T> 方法,这是一个泛型方法,用于将 HelloWorld 组件添加到这个 GameObject,并且它将返回附加组件的引用,因此我们可以将此值分配给 _helloWorld 字段。

注意

泛型方法是一种使用类型参数声明的函数。前面的代码展示了如何使用 HelloWorld 作为类型参数来调用 AddComponent<T> 方法。

值得注意的是,除了泛型方法之外,还有一个 AddComponent 的版本,即 AddComponent(string className),这是一个带有字符串参数的方法。它已被弃用,因此你不应该再使用此方法,而应使用泛型版本。

通过点击 Unity 编辑器中的 播放 按钮来玩游戏。

![图 2.39 – 在运行时附加 HelloWorld 组件图片

图 2.39 – 在运行时附加 HelloWorld 组件

查看附加到 GameObject 的 HelloWorld 组件,并将对该组件的引用分配给 Test 组件的字段。

干得好。现在我们已经学会了如何将组件添加到场景中的 GameObject。接下来,让我们通过 C# 代码探索如何通过同一 GameObject 或不同 GameObject 访问组件。

访问附加到 GameObject 的组件

当我们开发 Unity 项目时,我们经常需要访问其他组件,因为我们可以重用不同组件定义的功能。

在这里,让我们向 HelloWorld.cs 脚本中添加一些代码,以便在编辑器的控制窗口中打印 Hello World! 消息:

    public void SayHi()
    {
        Debug.Log("Hello World!");
    }

注意

SayHi 方法中的 Debug.Log 行是一个常用的方法,可以将帮助您调试游戏的消息打印到控制窗口。Debug 类还提供了许多其他方法,例如 LogErrorLogWarningAssert

我们可以将其视为我们希望在多个脚本中重用的功能。然后,我们还需要创建一个新的脚本,名为 TestGetComponent.cs。这是我们将放置代码以在运行时访问 HelloWorld 组件的脚本:

public class TestGetComponent : MonoBehaviour
{
    void Update()
    {
        var helloWorld =
          gameObject.GetComponent<HelloWorld>();
        if (helloWorld == null)
        {
            return;
        }
        helloWorld.SayHi();
    }
}

如我们所知,Update 函数在游戏的每一帧都会运行,为了演示如何访问组件,我们可以将代码放在 Update 函数中,如 TestGetComponent 类的代码所示。

![图 2.40 – 带有 TestGetComponent 组件的 GameObject图片

![图 2.40 – 带有 TestGetComponent 组件的 GameObject 然后,我们将 TestGetComponent 脚本作为组件附加到同一个 GameObject 上,播放游戏,并查看控制窗口。Hello World! 消息出现在那里。![图 2.41 – Hello World! 出现在控制窗口图片

图 2.41 – Hello World! 出现在控制窗口

注意

由于性能原因,建议不要在每一帧都使用此函数。

在这种情况下,我们访问了附加到同一 GameObject 的其他组件。此外,我们还可以访问不同 GameObject 上的其他组件。

首先,我们需要获取目标 GameObject 的引用。在这里,我们可以在编辑器中将引用对象分配给这个脚本,或者使用 GameObject.Find 方法在运行时查找目标对象。从游戏性能的角度来看,不要在每帧都会被调用的方法,如 Update 中调用 GameObject.Find 方法来查找目标对象。如果你无法在编辑器中将引用分配给脚本,例如,引用对象是在运行时动态创建的,那么你可以使用这个方法来查找目标对象并缓存目标对象,而不是在每一帧都查找目标对象。在这个例子中,我们可以在 Start 方法中查找目标对象并将其缓存,如下所示:

private GameObject _targetGameObject;
    private void Start()
    {
        // Using Find method to find game objects is not
           recommended,
        // this is just to demonstrate how to call this
           method to find
        // the target object at runtime.
        _targetGameObject =
          GameObject.Find("GameObjectTest");
    }

然后,让我们修改 TestGetComponent 类的 Update 函数,如下所示:

    void Update()
{

        var helloWorld =
          _targetGameObject.GetComponent<HelloWorld>();
        if (helloWorld == null)
        {
            return;
        }
        helloWorld.SayHi();
    }

这里,我们使用 GameObject.Find(string name) 函数通过名称查找 GameObject 并返回它。目标 GameObject 的名称是 GameObjectTest

有其他函数可以在运行时用于查找 GameObject,例如 GameObject.FindWithTag(string tag),它返回一个带有 tag 标签的活动 GameObject。但是,为了正确使用此功能,必须在标签管理器中首先声明标签。您可以从 项目设置 | 标签和层 中管理这些标签。

然而,正如我们之前提到的,Find 方法及其变体不建议用于查找 GameObject。这个例子只是为了演示如何在运行时调用该方法来查找目标对象,如果你需要在运行时查找动态创建的对象。

接下来,我们创建一个新的 GameObject 并将其 TestGetComponent 脚本附加到它上。同时,从名为 GameObjectTest 的目标 GameObject 中移除 TestGetComponent 脚本。

![Figure 2.42 – A GameObject with the TestGetComponent component]

![Figure 2.42_B17146.jpg]

Figure 2.42 – A GameObject with the TestGetComponent component

玩游戏并查看控制台窗口。同样的 Hello World! 消息再次出现在那里。

在本节中,我们学习了如何在 Unity 中创建新的脚本,如何将脚本作为组件附加到 GameObject 上,并讨论了如何在运行时通过代码访问组件以重用功能。接下来,让我们探索 Unity 包管理器和 Unity 中的包。

包和 Unity 包管理器

如果你是一名 .NET 开发者,那么我相信你必须知道 NuGet 包管理器。Unity 中的包管理器与 NuGet 非常相似,它使游戏开发者能够共享和消费有用的代码。但它们是不同的。在 Unity 中,你可以重用不仅是有用的代码,还有数字资产、着色器、插件和图标。Unity 中的包是一个包含前面提到的内容的容器。

在本节中,我将介绍 Unity 中的包和包管理器,以便您了解 Unity 中的包机制以及如何使用 Unity 包管理器来管理包。

Unity 包管理器

Unity 为游戏开发者提供了一个名为 Unity 包管理器的工具,用于管理项目中的包并向项目中添加新包。我们可以通过点击窗口 | 包管理器来打开包管理器窗口。

图 2.43 – 从窗口菜单打开包管理器窗口

图 2.43 – 从窗口菜单打开包管理器窗口

默认情况下,此窗口显示您项目中的已安装包及其版本。如果某个包有新版本可用,版本号旁边将显示一个升级图标。您还可以对这些包进行排序,例如按名称升序或按发布日期降序排序。

图 2.44 – Unity 包管理器

图 2.44 – Unity 包管理器

在窗口的右侧,将显示当前选中包的详细信息,例如包名、发布者、发布日期、版本号、文档链接和描述。您还可以通过点击窗口右下角的移除按钮从您的项目中移除一个包。

图 2.45 – 切换包列表

图 2.45 – 切换包列表

此窗口还可以显示不同的列表。例如,您可以通过从下拉菜单中选择我的资产选项来查看、下载和导入从 Unity Asset Store(assetstore.unity.com/)购买的资产。

从 Asset Store 购买的资产可能是免费的或付费的。Asset Store 提供了各种资产,从纹理、模型和动画到整个项目示例应有尽有。

图 2.46 – Unity 注册表中的包

图 2.46 – Unity 注册表中的包

您还可以从 Unity 注册表中安装包。通过从下拉菜单中选择Unity 注册表选项,您可以浏览 Unity 注册表中注册的所有包。如果您想安装一个包,您需要选择它并点击窗口右下角的安装按钮。

除了从 Unity 注册表中安装包之外,Unity 包管理器还提供了其他安装包的方法,即从本地文件夹安装新包、从本地 tarball 文件安装新包以及使用 Git URL 安装新包。

图 2.47 – 安装新包

图 2.47 – 安装新包

您可以通过点击包管理器窗口右上角的+按钮使用这三种不同的方式添加新的包。

Unity 游戏引擎的一些内置功能也作为包提供。您可以通过从下拉菜单中选择内置选项来查看所有内置包的列表。在这里,您可以管理这些内置功能。您可以通过禁用不需要的包来减小游戏的运行时构建大小。例如,如果您开发的游戏没有 VR 或 AR 功能,您可以通过单击包管理器窗口右下角的禁用按钮来禁用 XR 相关的包。

图 2.48 – 内置包

图 2.48 – 内置包

包是一个容器,包含满足项目各种需求的功能。您可以通过添加包来为您的游戏添加新功能。例如,AR Foundation 包将提供增强现实功能。您也可以删除包以减小游戏的大小。因此,使用包使 Unity 游戏开发更加灵活和松耦合。

然而,如果您不小心,使用包也可能使您的游戏充满错误。这是因为不同的包可能处于不同的状态。

图 2.49 – 使用 Unity 包管理器(Unity)的包生命周期

图 2.49 – 使用 Unity 包管理器(Unity)的包生命周期

由 Unity 开发和维护的包可能处于以下两种状态之一:

  • 预览包

  • 验证包

预览包表示它目前可以用于测试,并且可能在后续版本中经历许多变化。Unity 不能保证对预览包的未来支持,因此您不应在生产中使用它们。

默认情况下,您在包管理器窗口中找不到处于预览状态的包。如果您确实需要使用预览包,例如测试未来项目的新的功能,您可以按照以下步骤允许包管理器窗口显示预览状态的包:

  1. 通过单击齿轮图标然后单击高级项目设置项,打开包管理器的项目设置窗口。

图 2.50 – 高级项目设置

图 2.50 – 高级项目设置

  1. 检查启用预览包选项。

图 2.51 – 包管理器设置

图 2.51 – 包管理器设置

  1. 然后,查看包管理器窗口。您将看到预览包出现在包列表中。在包列表中,所有处于预览状态的包都标记为预览

图 2.52 – 预览包

图 2.52 – 预览包

另一方面,处于验证状态的包意味着它可以用于生产。只有经过严格测试并且 Unity 保证支持该验证包,包才会被视为验证包。

默认情况下,包管理器窗口显示已验证的包列表。处于已验证状态的包被标记为已验证

图 2.53 – 已验证包

图 2.53 – 已验证包

摘要

在本章中,我们首先介绍了 Unity 脚本编程中最常用的几个类,然后解释了脚本实例的生命周期和重要事件函数,并讨论了 Unity 如何初始化脚本以及脚本中游戏逻辑的更新。

我们还讨论了如何在 Unity 中创建一个新的脚本以及如何将脚本作为组件附加到 GameObject 上。除了在编辑器中手动添加组件外,我们还可以使用 C#代码在运行时动态添加组件或访问组件。

最后,我们演示了如何通过 Unity 包管理器添加或删除包以提供功能或减小游戏大小。同时,我们还解释了预览包和已验证包之间的区别。

在下一章中,我们将学习 Unity 中的 UI 系统,同时,我们还将介绍如何在 Unity 中优化 UI 性能。

第二部分:使用 C#脚本与 Unity 内置模块协同工作

在对 Unity 游戏引擎有一个总体了解并知道如何在 Unity 中编写脚本之后,我们可以开始逐个学习 Unity 引擎中的主要模块,例如在 Unity 中创建 UI 和在游戏中应用物理。

本部分包括以下章节:

  • 第三章, 使用 Unity UI 系统开发 UI

  • 第四章, 使用 Unity 动画系统创建动画

  • 第五章, 与 Unity 物理系统协同工作

  • 第六章, 在 Unity 项目中集成音频和视频

第三章:第三章:使用 Unity UI 系统开发 UI

UI 对于游戏来说非常重要,Unity 为游戏开发者提供了三种不同的 UI 解决方案。它们是即时模式图形用户界面IMGUI)、Unity UIuGUI)包和UI 工具包。IMGUI 是 Unity 中相对较旧的 UI 解决方案,不建议用于构建运行时 UI。UI 工具包是最新 UI 解决方案;然而,它仍然缺少一些在 uGUI 包和 IMGUI 中可以找到的功能。uGUI 包是 Unity 中成熟的 UI 解决方案,在游戏行业中得到广泛应用。因此,本章将介绍如何使用 uGUI 开发您游戏的 UI。

在本章中,我们将涵盖以下关键主题:

  • Unity 中的 C# 脚本和常见 UI 元素

  • Unity 中的 C# 脚本和 UI 事件系统

  • 模型-视图-视图模型MVVM)模式和 UI

  • 提高性能的 UI 性能提示

让我们开始吧!

Unity 中的 C# 脚本和常见 UI 组件

自 Unity 2019 以来,uGUI 已作为内置包提供在 Unity 编辑器中;因此,我们可以在项目窗口中直接查看 uGUI 包的内容,它还包括 C# 源代码。

![图 3.1 – uGUI 包]

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_3.1_B17146.jpg)

图 3.1 – uGUI 包

如我们在上一章中提到的,Unity 开发工作流程主要围绕组件的结构构建。uGUI 也不例外。它是一个带有组件集的GameObject

图 3.1所示,我们可以找到许多常用 UI 元素的 C# 源代码,例如文本滑动条切换。然而,一些 UI 组件是在引擎内部使用 C++ 代码实现的,例如画布,此类组件的代码无法从 Unity 编辑器中查看。

在本节中,我们将介绍 Unity 中常用的 UI 组件。根据其功能,我们可以将这些组件分为以下四个类别:

  • 画布

  • 图像和原始图像

  • 文本

  • 可选择的 UI 组件

画布

画布是 uGUI 中最基本且最重要的 UI 组件。要正确且高效地使用 uGUI,首先理解画布是至关重要的。

画布是用于在 uGUI 中渲染 UI 元素的组件。所有 UI 元素都应该位于画布区域内,这在场景中创建起来非常简单。

![图 3.2 – 从层次结构窗口创建画布]

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_3.2_B17146.jpg)

图 3.2 – 从层次结构窗口创建画布

图 3.2所示,您可以按照以下步骤创建一个新的画布:

  1. 层次结构窗口中右键单击以打开菜单。

  2. 选择UI | 画布

除了可以从层次结构窗口创建一个新的画布对象外,我们还可以通过点击GameObject | UI | 画布来创建一个新的画布对象。

![图 3.3 – 从 GameObject 菜单创建画布]

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_3.3_B17146.jpg)

图 3.3 – 从 GameObject 菜单创建 canvas

如你在图 3.2图 3.3中看到的,我们还可以从这些菜单创建其他不同的 UI 元素,例如文本按钮图像滑块。由于所有 UI 元素都是Canvas的子元素,如果你想直接创建一个新的 UI 元素而没有 Canvas,将自动创建一个新的Canvas对象。新的 UI 元素将成为Canvas对象父对象的子对象。

![图 3.4 – Canvas 对象图 3.4 – B17146.jpg

图 3.4 – Canvas 对象

一旦创建了一个Canvas对象,我们可以看到,除了这个 GameObject 上附加的Canvas组件外,还有Rect TransformCanvas ScalerGraphic Raycaster组件。如前所述,Canvas是用于渲染 UI 元素的组件,因此所有 UI 组件都必须是 Canvas 的子组件;否则,它们将不会被 Unity 渲染。

我们将按顺序分别探索它们。

Canvas组件

如果你选择场景中的Canvas对象,你可能会惊讶地发现其位置很奇怪。默认情况下,它不在主相机的视场内。

![图 3.5 – 场景中具有屏幕空间 - 堆叠渲染模式的 Canvas 对象图 3.5 – B17146.jpg

图 3.5 – 场景中具有屏幕空间 - 堆叠渲染模式的 Canvas 对象

这是因为附加到这个 GameObject 上的Canvas组件提供了三种不同的渲染模式,如下所示:

  • 屏幕空间 - 堆叠

  • 屏幕空间 - 相机

  • 世界空间

![图 3.6 – 渲染模式图 3.6 – B17146.jpg

图 3.6 – 渲染模式

屏幕空间 - 堆叠渲染模式将 UI 元素放置在屏幕上,这些元素是在场景之上渲染的。因此,用于渲染游戏场景的场景中的相机不会影响 UI 的渲染。这是Canvas组件提供的默认渲染模式。

如其名所示,屏幕空间 - 相机渲染模式与上一个类似。然而,从名称中可以看出,第二种渲染模式将受到相机的影响。

![图 3.7 – 屏幕空间 - 相机渲染模式图 3.7 – B17146.jpg

图 3.7 – 屏幕空间 - 相机渲染模式

如你在图 3.7中看到的,如果选择了屏幕空间 - 相机渲染模式,我们需要为这个 Canvas 指定一个相机并设置它们之间的距离。此外,如果我们仍然在场景中选择这个 Canvas,我们会发现它已经被移动到这个特定相机的视场内。

![图 3.8 – 场景中具有屏幕空间 - 相机渲染模式的 Canvas 对象图 3.8 – B17146.jpg

图 3.8 – 场景中具有屏幕空间 - 相机渲染模式的 Canvas 对象

在这种情况下,UI 元素是由这个相机渲染的,这意味着相机的设置会影响 UI 的外观。这与Screen Space - Overlay渲染模式不同。

图 3.9显示,当此相机的Field of View值从 100 变为 30 时,游戏场景和 UI 发生了变化:

Figure 3.9 – 相机的视场(FoV)在上半部分为 100,下半部分为 30

图 3.9 – 相机的视场(FoV)在上半部分为 100,下半部分为 30

最后一种渲染模式是World Space。在这种模式下,画布将像场景中的任何其他 GameObject 一样工作。这种模式与Screen Space - Camera渲染模式之间最大的区别是我们可以手动调整画布的大小、位置,甚至旋转角度,就像一个普通 GameObject 一样。

图 3.10所示,我们可以使用此Canvas对象的Rect Transform组件来调整其WidthRotation值:

Figure 3.10 – 世界空间渲染模式

图 3.10 – 世界空间渲染模式

图 3.11显示了在场景中手动设置WidthRotation值后的Canvas对象:

Figure 3.11 – 场景中具有世界空间渲染模式的 Canvas 对象

图 3.11 – 场景中具有世界空间渲染模式的 Canvas 对象

在这里,我们使用RectTransform组件来设置画布的大小。每个 UI 对象都将包含一个RectTransform组件,就像每个普通 GameObject 都将包含一个 Transform 组件一样。接下来,我们将探讨RectTransform组件。

Rect Transform 组件

Rect Transform组件与常规的Transform组件类似。最大的区别是前者用于 UI 元素而不是常规的 GameObject。当创建一个新的 UI 元素对象时,Rect Transform组件将自动附加到它上。

观察此组件,你可以看到一些在Transform组件上可以看到的属性,例如PositionRotationScale。还有一些独特的属性。

Figure 3.12 – 一个 Rect Transform 组件

图 3.12 – 一个 Rect Transform 组件

这些独特的属性是AnchorPivot。我们将依次讨论这些。

锚点

锚点是指示区域四个角位置的数值,从AnchorMin.xAnchorMin.y的角度看,右上角由AnchorMax.xAnchorMax.y表示。默认情况下,左下角是 0.5 和 0.5,右上角也是 0.5 和 0.5,相对于父对象居中,如图 3.12所示。

我们可以直接修改锚点的值 – 例如,我们可以将左下角从 0.5 和 0.5 更改为 0 和 0,这样父元素和子元素的左下角就相同了。然后,我们将右上角从 0.5 和 0.5 更改为 0.5 和 1,这意味着子元素的右上角位置是父元素右上角 x 轴位置的一半。结果如图 3.13 所示:

图 3.13 – 修改锚点

图 3.13 – 修改锚点

锚点在 Unity 中开发 UI 时非常有用。例如,如果您想在屏幕顶部显示 UI,例如标题,您需要指定从父元素顶部的距离。如果您想在屏幕底部显示 UI,例如页脚,您需要指定从父元素底部的距离。

为了使开发者更容易使用锚点,Unity 提供了一些锚点预设,如图 3.14 所示:

图 3.14 – 锚点预设

图 3.14 – 锚点预设

中心点

中心点 是这个矩形区域的起点。中心点 的值是在 0 和 1 之间的归一化值。当 UI 元素缩放或旋转时,它将围绕该点进行缩放或旋转:

![图 3.15 – 以中心为中心沿 z 轴旋转 45 度]

沿 z 轴以右上角为中心旋转 45 度

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_3.15_B17146.jpg)

图 3.15 – 以中心为中心沿 z 轴旋转 45 度,以右上角为中心沿 z 轴旋转 45 度]

图 3.15 展示了以中心为中心沿 z 轴的 45 度旋转,其 中心点 值为 0.5 和 0.5,以及以右上角为中心沿 z 轴的 45 度旋转,其 中心点 值为 1 和 1。

Canvas Scaler 组件

除了 Canvas 组件外,还会自动创建一个 Canvas Scaler 组件。Canvas Scaler 组件用于控制画布内 UI 元素的整体缩放和像素密度。通过使用 Canvas Scaler,我们可以实现分辨率无关的 UI 布局:

图 3.16 – Canvas Scaler 组件

图 3.16 – Canvas Scaler 组件

Canvas Scaler 组件提供的 UI 缩放模式 有三种类型:

  • 恒定像素尺寸

  • 与屏幕尺寸缩放

  • 恒定物理尺寸

如果画布渲染模式是 ScreenSpace - OverlayScreenSpace - Camera,则我们可以设置 UI 缩放模式。另一方面,如果画布渲染模式是 World Space,则无法修改 UI 缩放模式。接下来,我们将介绍这三种不同的模式。

恒定像素尺寸 是默认的 UI 缩放模式。在此模式下,UI 元素的大小将保持像素大小不变,无论屏幕大小如何。

图 3.17 – 在不同屏幕尺寸下显示的 Hello World UI 文本(上半部分为 1920 x 1080,下半部分为 3840 x 2160)

图 3.17 – 在不同屏幕大小下显示的 Hello World UI 文本(上半部分为 1920 x 1080,下半部分为 3840 x 2160)

图 3.17所示,一个Hello World UI 文本将保持其像素大小不变。当屏幕分辨率相对较低(1920 x 1080)时,文本将显示得更大。当屏幕分辨率较高(3840 x 2160)时,文本将显示得较小。

如果您想在不同的屏幕分辨率下保持 UI 元素显示的一致性,根据屏幕大小缩放模式是一个理想的选择。

图 3.18 – 根据屏幕大小缩放模式

图 3.18 – 根据屏幕大小缩放模式

UI 缩放模式设置为根据屏幕大小缩放时,UI 元素的位置和大小将根据参考分辨率属性中的像素值指定,如图 3.18所示。

如果当前屏幕分辨率大于参考分辨率,画布将被缩放以适应屏幕分辨率。反之,如果当前屏幕分辨率小于参考分辨率,画布将缩小以适应屏幕分辨率。

当屏幕分辨率比例与参考分辨率比例相同时,缩放和缩小 UI 元素非常容易。但当屏幕分辨率比例与参考分辨率比例不同时,缩放画布会导致其变形。为了避免这种情况,画布的分辨率也将取决于屏幕匹配模式的设置,您也可以在图 3.18中看到。默认情况下,屏幕匹配模式设置为匹配宽度和高度,这允许您以宽度或高度作为参考,或介于两者之间的值来缩放画布区域。

UI 缩放模式设置为常量物理大小时,UI 元素的位置和大小以物理单位如毫米英寸指定。

图 3.19 – 常量物理大小模式

图 3.19 – 常量物理大小模式

除了画布缩放器组件外,还会自动创建另一个组件,我们将在下一节中查看。

图形射线投射器组件

如其名称所示,图形射线投射器组件用于对画布内的一组 UI 元素进行射线投射,以确定哪个 UI 元素被击中。因此,它可以将在玩家输入转换为 UI 事件。需要注意的是,场景中需要有事件系统组件,图形射线投射器才能正常工作。关于事件系统组件,我们将在“C#脚本和 Unity 中的 UI 事件系统”部分中稍后介绍。

这在您需要确定光标是否位于场景中的 UI 元素上时很有用,例如 UI 文本或 UI 图像。例如,假设您想让玩家能够将 UI 图像拖放到游戏中以改变其位置,那么您必须知道玩家的光标是否位于 UI 图像上,并在拖动时获取光标移动的数据。在这种情况下,您需要创建一个脚本,实现UnityEngine.EventSystems命名空间中定义的IPointerDownHandlerIDragHandler接口,这意味着当玩家点击和拖动图像时,您可以获取事件,如下所示:

using UnityEngine;
using UnityEngine.EventSystems;
public class DragAndDropExample : MonoBehaviour,
  IPointerDownHandler, IDragHandler
{
    private RectTransform _rectTransform;
    public void OnPointerDown(PointerEventData eventData)
    {
        Debug.Log("This UI image is clicked!!!");
        _rectTransform = GetComponent<RectTransform>();
    }
    public void OnDrag(PointerEventData eventData)
    {
        Debug.Log("This UI image is being dragged!!!");

            if (RectTransformUtility
             .ScreenPointToWorldPointInRectangle
             (_rectTransform, eventData.position,
             eventData.pressEventCamera, 
             out var cursorPos))
            {
                  _rectTransform.position = cursorPos;
            }
    }
}

让我们按以下方式分解代码:

  • 我们使用using关键字添加UnityEngine.EventSystems命名空间以获取与点击和拖动 UI 元素相关的事件。

  • DragAndDropExample类实现了两个接口,即IPointerDownHandlerIDragHandler

    • 具体来说,我们在IPointerDownHandler接口中实现了OnPointerDown方法,该方法将在 UI 元素被点击时被调用。

    • 我们在IDragHandler接口中实现了OnDrag方法。当发生拖动时,每次光标移动时都会调用此方法。

  • OnPointerDown方法的实现中,该方法接受PointerEventData作为参数,获取RectTransform组件的实例,并将其分配给_rectTransform字段。

  • OnDrag方法的实现中,该方法也接受PointerEventData作为参数,获取光标位置,并修改_rectTransform字段的position属性以移动 UI 元素。

为了使此脚本工作,您需要将脚本附加到场景中您想要拖放的那个 UI 元素上。

Figure 3.20 – Dragging and dropping a UI image

Figure 3.20 – Dragging and dropping a UI image

Figure 3.20 – Dragging and dropping a UI image

图 3.20显示了基于Graphic Raycaster组件的 UI 图像拖放交互。

之前描述的组件在创建Canvas对象时自动创建。接下来,我们将介绍其他 UI 元素。

Image

显示图像是 UI 的一个重要功能。uGUI 提供了两种显示图像的组件类型——Image组件和Raw Image组件。

我们现在将解释这些功能以及如何正确使用它们。

图像组件

您可以使用Image组件在 UI 上显示图像。

Figure 3.21 – Creating a new image

Figure 3.21 – Creating a new image

图 3.21 – 创建新图像

图 3.21所示,您可以按照以下步骤创建一个新图像:

  1. Hierarchy窗口中右键单击以打开菜单。

  2. 选择UI > Image

如果您想在游戏 UI 中创建背景图像,也可以选择UI > Panel。面板实际上就是一个图像。

Figure 3.22 – The Image component

Figure 3.22 – The Image component

图 3.22 – 图像组件

在这种情况下,我们创建一个面板作为背景。如图图 3.22所示,这里我们指定一个名为SF 背景的纹理作为此图像组件的源图像。需要注意的是,当导入 Unity 时,图像组件使用的纹理必须设置为精灵类型。

图 3.23 – 纹理导入设置

图 3.23 – 纹理导入设置

纹理类型可以在纹理的导入设置面板中设置,如图图 3.23所示。

注意

图像精灵是用于 UI 和其他 2D 游戏元素中的 2D 图形对象。

使用精灵作为图像源的优势在于,在调整精灵大小时,角落不会拉伸或扭曲。

图 3.24 – 精灵编辑器

图 3.24 – 精灵编辑器

这是因为 Unity 中的精灵编辑器提供了九宫格分割图像的选项,将图像分割成九个区域。如图图 3.24所示,在这种情况下,当图像被调整大小时,图像的角落将保持不变。

注意

九宫格分割是 UI 实现中的一种常见技术。使用九宫格分割的主要优势是它可以很好地处理图像的拉伸。一旦图像被拉伸,就会出现扭曲和模糊等问题,但图像的一些部分可以被拉伸。例如,UI 背景框,其中通常中间部分是纯色,可以被拉伸,但图像的四个角落可能有不能拉伸的特殊图案。在这种情况下,我们可以使用九宫格分割技术将整个图像分成九个网格,其中四个角落各在一个网格中。然后,我们只能拉伸和放大图像的中间部分,而保持四个角落不变。

因此,在大多数情况下,使用图像组件来显示 UI 图像是首选的选择。

原始图像组件

原始图像组件是另一个用于在游戏 UI 上显示图像的组件。

图 3.25 – 创建新的原始图像

图 3.25 – 创建新的原始图像

图 3.25所示,您可以创建一个新的图像,如下所示:

  1. 层次结构窗口中右键单击以打开菜单。

  2. 选择UI > 原始图像

原始图像组件和图像组件之间的区别是,图像组件的源必须是精灵类型。相反,原始图像接受任何纹理。此外,原始图像组件的功能比图像组件简单,如下面的截图所示:

图 3.26 – 原始图像组件

图 3.26 – 原始图像组件

以下代码片段展示了如何修改图像原始图像组件显示的图像:

using UnityEngine;
using UnityEngine.UI;
public class ImageAndRawImage : MonoBehaviour
{
[SerializeField] 
private Image _image;
[SerializeField] 
private Sprite _sprite;
[SerializeField] 
private RawImage _rawImage;
[SerializeField] 
private Texture _texture;
    void Start()
    {
        _image.sprite = _sprite;
        _rawImage.texture = _texture;
    }
}

应注意,为了能够在代码中访问 UI 相关的类,我们需要使用UnityEngine.UI命名空间。

UI 的另一个重要部分是 文本。接下来,让我们探索 uGUI 提供的用于显示文本的两个组件。

文本

在 uGUI 中显示字符的最简单方法是使用 Text 组件。然而,仅使用 Text 来调整字符间距和表达装饰也是一件麻烦事。TextMeshPro 是另一个选项,它提供了华丽的字符表达。在本节中,我们将依次探索 TextTextMeshPro 组件。

文本组件

Text 组件是自 uGUI 早期以来常用以显示 UI 文本的组件。为游戏 UI 创建文本非常简单;只需遵循以下步骤:

  1. 层次 窗口中右键点击以打开菜单。

  2. UI > 文本 下选择。

图 3.27 – 创建文本

图 3.27 – 创建文本

在画布中创建一个 Text 对象;我们可以在 Unity 编辑器的 场景 视图中找到它,如图 图 3.28 所示:

图 3.28 – 场景视图中的文本

图 3.28 – 场景视图中的文本

您可以看到文本内容在一个白色框架中,这代表附加到这个 Text 对象上的 Rect Transform 组件,并标识其大小。如果更改字体大小导致文本内容超出这个白色框架,文本内容将无法显示。因此,在更改字体大小时,请记住考虑 TextRect Transform 组件。

除了更改字体大小外,您还可以更改使用的字体或启用 富文本

图 3.29 – 文本组件

图 3.29 – 文本组件

图 3.29 所示,您可以在文本中添加 <b></b>, <i></i>, 和 <color></color> 标签,以提供文本的样式变化。

然而,Text 组件提供的功能相对简单。当 Text 组件发生变化时,用于显示文本的多边形需要重新计算,这会导致图形重建,可能会引起潜在的性能问题,并且当以高分辨率显示时,该组件渲染的文本看起来非常模糊。因此,在原始的 Text 组件之后,Unity 还为 UI 提供了另一个文本解决方案。接下来,我们将介绍 TextMesh Pro 组件。

TextMeshPro 组件

TextMeshPro (TMP) 是 Unity 提供的终极 UI 文本解决方案。它是一种强大的文本渲染机制,可以用来替换 Text 组件。TextMesh Pro 被设计用来利用 Signed Distance Field (SDF) 渲染,使其能够在任何分辨率下渲染出美丽的文本。您还可以为 TextMesh Pro 创建自定义着色器,以获得轮廓和柔化阴影等效果。

应注意,它不包括在默认的Unity UI包中,但包含在TextMeshPro包中。因此,如果您在创建 UI 文本时找不到TextMesh Pro,那么您应该首先检查此包是否已添加到您的项目中。

图 3.30 – 创建 TextMeshPro 对象

图 3.30 – 创建 TextMeshPro 对象

为游戏 UI 创建TextMeshPro文本非常简单;只需遵循以下步骤:

  1. 层次结构 窗口中右键单击以打开菜单。

  2. 选择 UI > 文本 > TextMeshPro

图 3.31 – TextMeshPro 组件

图 3.31 – TextMeshPro 组件

如图 3.31 所示,TextMeshPro渲染的文本比文本组件渲染的文本更清晰。

除了渲染更清晰的文本外,TextMeshPro 还提供了对文本格式和布局的改进控制。如图 3.32 所示,您可以通过编辑器直接更改文本的样式。有几种常见的样式可供选择,例如 粗体斜体。同样,您也可以使用标签来修改文本样式,就像 文本 组件一样,并且可以使用 间距选项对齐换行 等功能来控制文本布局。

此外,您还可以实现更多渲染效果,例如点击着色器的轮廓选项以向文本添加轮廓效果。

图 3.32 – TextMeshPro 组件

图 3.32 – TextMeshPro 组件

使用 TextMesh Pro 来实现您的 UI 文本是一个不错的选择。

可选择 UI 组件

您可以使用 uGUI 中的可选择组件来处理交互。这些组件包括 按钮切换滑动条下拉菜单输入字段滚动条。在本节中,我们将主要讨论最常用的组件,即 按钮 组件。

按钮

为游戏 UI 创建一个 按钮 元素非常简单;只需遵循以下步骤:

  1. 层次结构 窗口中右键单击以打开菜单。

  2. 选择 UI > 按钮 - TextMeshPro

图 3.33 – 创建按钮对象

图 3.33 – 创建按钮对象

如图 3.33 所示,在菜单中有两种创建按钮的选项,即 按钮按钮 -TextMeshPro。在这里,我们选择 按钮 -TextMeshPro,以便按钮上的文本内容由TextMeshPro渲染。

图 3.34 – 一个图像组件和一个按钮组件被附加到按钮上

图 3.34 – 一个图像组件和一个按钮组件被附加到按钮上

一旦创建了一个默认按钮对象,该对象不仅包含Button组件,还包含Image组件。这是因为Button组件仅提供与用户交互的功能;它不提供图形显示的功能。因此,按钮的图像需要一个Image组件来显示。

选择状态

Selectable类,即NormalHighlightedPressedSelectedDisabled,这些状态由名为Selectable.SelectionState的枚举定义。因此,如图图 3.34所示,“Transition”部分对应这五种不同的选择状态,这意味着当用户与该按钮交互时,该按钮将根据不同的状态提供不同的反馈。

onClick

按钮的重要作用是接收用户点击并触发相应的事件。在 Unity 中,设置按钮onClick事件非常简单。您可以在编辑器中手动设置按钮onClick事件,或者通过编程方式设置按钮onClick事件。

为了在编辑器中的按钮上设置一个新事件,我们可以点击“+”按钮,位于“On Click ()”部分的底部,如图图 3.35所示。这将创建一个新的动作。

![Figure 3.35 – 在编辑器中设置新的 onClick 事件

![img/Figure_3.35_B17146.jpg]

图 3.35 – 在编辑器中设置新的 onClick 事件

我们还可以通过编程方式设置按钮onClick事件;以下代码展示了如何进行此操作:

using UnityEngine;
using UnityEngine.UI;
public class ButtonClickExample : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        var button = GetComponent<Button>();
        button.onClick.AddListener(() =>
        {
            Debug.Log(You have clicked the button!);
        });
    }
}

在本节中,我们了解了常用的 UI 组件,并对 Unity 提供的 UI 解决方案 uGUI 有了了解。接下来,我们将探索 Unity 中的 UI 事件系统。如果场景中没有事件系统,UI 元素如按钮就无法与玩家交互,因此这是一个重要的主题。

C#脚本和 Unity 中的 UI 事件系统

EventSystem是一个将事件发送到支持键盘、鼠标、屏幕触摸等输入的游戏对象的机制。EventSystem 由多个用于发送事件的模块组成。如果场景中没有EventSystem对象,那么在创建画布时,将自动创建一个EventSystem对象。

![Figure 3.36 – EventSystem

![img/Figure_3.36_B17146.jpg]

图 3.36 – EventSystem

图 3.36所示,EventSystem对象的Inspector窗口暴露了非常少的函数。这是因为 EventSystem 被设计为各种输入模块之间协作的管理器。

应注意,场景中最多只能有一个EventSystem对象。如果场景中有多个EventSystem对象,将显示警告信息,如图图 3.37所示:

![Figure 3.37 – A warning message when there are multiple EventSystem objects

![img/Figure_3.37_B17146.jpg]

图 3.37 – 存在多个 EventSystem 对象时的警告信息

当游戏运行时,EventSystem将寻找附加到同一 GameObject 的InputModule组件。这是因为InputModule是负责EventSystem主要逻辑的类。我们也可以找到本例中使用的输入模块,如图 3.36 所示,即独立输入模块。接下来,我们将介绍输入模块。

输入模块

Unity 提供了两个内置输入模块,即独立输入模块触摸输入模块。在过去,独立输入模块用于键盘、鼠标和游戏控制器,触摸输入模块用于触摸屏,如智能手机。如今,独立输入模块与所有平台兼容,触摸输入模块已被弃用,因此你可以将输入模块视为独立输入模块。

输入模块的目的是将特定于硬件的输入(如触摸、游戏手柄、鼠标和游戏控制器)映射到通过消息系统发送的事件。

新的 Input System 包

除了这个默认的内置输入模块之外,Unity 还提供了一个新的、更强大、更灵活、可配置的Input System包。

图 3.38 – Input System 包

图 3.38 – Input System 包

如果你想使用新的输入系统,那么你需要从包管理器窗口安装该包,如图 3.38 所示。此外,新创建的EventSystem组件默认仍将使用传统的独立输入模块组件,因此你需要手动将其替换为新的InputSystemUIInputModule组件,如图 3.39 所示:

图 3.39 – 替换为 InputSystemUIInputModule

图 3.39 – 替换为 InputSystemUIInputModule

通过阅读本节,我们了解到为了确保游戏 UI 能够正确响应玩家输入,需要一个EventSystem组件和一个输入模块。接下来,让我们继续讨论如何使用模型-视图-视图模型(MVVM)模式在 Unity 中创建 UI。

模型-视图-视图模型 (MVVM) 模式和 UI

在 Unity 开发中,一个常见的挑战是找到优雅的方法将组件彼此解耦,尤其是在开发 UI 时,因为它涉及到 UI 逻辑和 UI 渲染。模型-视图-视图模型MVVM)是一种软件架构模式,它帮助开发者将视图模型(UI 逻辑)与视图(UI 图形)分离。在本节中,我们将探讨如何在 Unity 中实现 MVVM 模式。

图 3.40 – MVVM

图 3.40 – MVVM

如其名所示,MVVM 由三部分组成:

  • DatabasePlayerPrefs,在 Unity 中存储玩家偏好设置,等等。

  • MonoBehaviour 并附加到 UI 对象。其主要作用是管理 UI 元素并触发 UI 事件,但它本身不实现任何具体的 UI 逻辑。

  • MonoBehaviour。它不需要考虑 UI 的外观;它只需要实现具体逻辑。

我们可以看到 MVVM 中有三个部分,那么它们应该如何连接呢?通常,我们使用两种方式来连接它们:

  • ViewModelView。绑定到数据的元素将自动反映每个数据变化。通过使用数据绑定,ViewModel可以修改 View 中 UI 控件的价值。

  • 事件驱动编程:这种方法用于从由用户操作触发的 View 中引发事件,然后由 ViewModel 处理。

对于 Unity,有一些成熟的 MVVM 框架实现,例如Loxodon Framework,这是一个专门针对 Unity 构建的轻量级 MVVM 框架。您可以在 GitHub 上找到它的仓库(github.com/vovgou/loxodon-framework)或直接通过 Unity Asset Store 将其添加到您的项目中。

图 3.41 – Loxodon Framework

图 3.41 – Loxodon Framework

由于我们的下一个示例将使用这个框架,我建议您首先将此框架导入到您的项目中。导入此框架后,您应该在项目的Assets文件夹中找到它。

图 3.42 – LoxodonFramework 文件夹

图 3.42 – LoxodonFramework 文件夹

现在,让我们按照以下步骤在 Unity 中使用LoxodonFramework实现一个示例 MVVM UI:

  1. 首先,让我们在我们的游戏场景中设置LoxodonFramework。我们需要创建一个新的画布,并将GlobalWindowManager组件添加到这个画布上,如图图 3.43所示。一个GlobalWindowManager组件是一个用于管理视图的容器。

图 3.43 – GlobalWindowManager 组件

图 3.43 – GlobalWindowManager 组件

  1. 接下来,我们需要定义一个视图。正如我们之前提到的,视图在 Unity 中代表 UI 元素。从下面的代码中可以看出,这个视图相对简单,只包含一个按钮 UI 元素和一个文本 UI 元素,并且这个SampleView类继承自 Loxodon Framework 中的Window类。在下面的代码中,您还可以找到BindingSet类,它用于绑定和连接ViewModelView的属性:

    using UnityEngine;
    using UnityEngine.UI;
    using Loxodon.Framework.Views;
    using Loxodon.Framework.Binding;
    using Loxodon.Framework.Binding.Builder;
    using Loxodon.Framework.ViewModels;
    using TMPro;
    public class SampleView : Window
    {
        [SerializeField]
        private Button _submitButton;
        [SerializeField]
        private TextMeshProUGUI _message;
        private SampleViewModel _viewModel;
        protected override void OnCreate(IBundle bundle)
        {
            _viewModel = new SampleViewModel();
            BindingSet<SampleView, SampleViewModel>
    bindingSet =
              this.CreateBindingSet(_viewModel);
            bindingSet.Bind(_message).For(v =>
              v.text).To(vm => vm.Message).OneWay();
            bindingSet.Bind(_submitButton).For(v =>
              v.onClick).To(vm => vm.Submit);
            bindingSet.Build();
        }
    }
    

让我们分解这个例子:

  • 这个SampleView类的两个_submitButton_message字段分别引用一个Button组件和一个TextMeshProUGUI组件。

  • OnCreate方法中,我们首先创建一个BindingSet实例来绑定SampleView到其对应的ViewModel类——即SampleViewModel。我们将在后面介绍如何创建SampleViewModel类。

  • 然后,我们通过调用BindingSetBind方法将SampleView_message字段的text属性绑定到SampleViewModel中的Message属性。在代码中可以看到,我们这里使用的是OneWay绑定,这意味着只有视图模型可以修改视图中的 UI 元素的值。

  • 我们还绑定SampleView_submitButton字段的onClick事件到SampleViewModel中的Submit方法。最后,我们调用BindingSetBuild方法来构建绑定。

  1. 同时,我们还需要在 Unity 场景中创建这些所需的 UI 元素,如图所示。让我们称它为SampleUI

图 3.44 – 设置 UI 元素

图 3.44 – 设置 UI 元素

  1. 然后,让我们创建一个名为Resources的新文件夹,并将此示例 UI 的预制件从Hierarchy窗口拖动到Resources文件夹中,如图所示。到目前为止,我们已经创建了 UI 元素和一个代表 MVVM 架构中 UI 元素的View组件。SampleUI可以从场景中移除,因为我们将在运行时加载其预制件并创建 UI。

图 3.45 – SampleUI 预制件

图 3.45 – SampleUI 预制件

  1. 我们还需要一个SampleViewModel类,它实现了具体逻辑。SampleViewModel类继承自 Loxodon 框架中的ViewModelBase类,逻辑在Submit方法中实现,该方法修改Message属性。在之前创建的视图中,我们将按钮的onClick事件绑定到SampleViewModel类中的Submit方法,并且还将视图的 Text UI 元素的text属性绑定到SampleViewModelMessage属性。因此,在Submit方法修改Message属性后,修改后的消息内容将显示在 UI 上:

    using Loxodon.Framework.ViewModels;
    public class SampleViewModel : ViewModelBase
    {
        private string _message;
        private int _count;
        public SampleViewModel() { }
        public string Message
        {
            get { return _message; }
            set => Set<string>(ref _message, value,
              Message);
        }
        public void Submit()
        {
            _count++;
            Message = $The number of times the button is
              clicked: {_count};
        }
    }
    
  2. 最后,需要启动代码来注册服务和创建用户界面。以下启动代码支持在以下代码中加载ApplicationContext类的预制件;我们使用它来存储可以被 Loxodon 框架中的其他类访问的数据和服务。然后,代码注册了IUIViewLocator服务来加载 UI 预制件并创建 UI 元素:

    public class Startup : MonoBehaviour
    {
        private container.Register<IUIViewLocator>(new
              ResourcesViewLocator ());
            var bundle = new
              BindingServiceBundle
              (_context.GetContainer());
            bundle.Start();
        }
        private IEnumerator Start()
        {
            // Create a window container
            var winContainer =
              WindowContainer.Create(MAIN);
            yield return null;
            IUIViewLocator locator =
              _context.GetService<IUIViewLocator>();
            var sampleView =
              locator.LoadWindow<SampleView>(winContainer,
              SampleUI);
            sampleView.Create();
            ITransition transition =
              sampleView.Show().OnStateChanged((w, state)
              =>
            {
            });
            yield return transition.WaitForDone();
        }
    }
    
  3. 让我们运行游戏。如图所示,我们创建了一个显示消息文本的视图和一个用于更新消息信息的SampleViewModel类,视图将通过数据绑定更新 UI 文本以显示最新信息。

图 3.46 – 带有 MVVM 的示例 UI

图 3.46 – 带有 MVVM 的示例 UI

这样,UI 图形和 UI 逻辑就被分离开了。UI 设计师和程序员可以同时工作,而不需要相互依赖,从而提高了 Unity 中 UI 开发的效率。

在本节中,我们讨论了如何使用 MVVM 在 Unity 中实现 UI。接下来,我们将学习在 Unity 中实现 UI 时需要注意的事项——即优化 UI 性能。

提高 UI 性能的性能提示

UI 是游戏的重要组成部分,因此如果你没有正确实现它,可能会引起潜在的性能问题。在本节中,我们将讨论在 Unity 中实现游戏 UI 的最佳实践,以优化由 UI 引起的性能问题。

Unity Profiler

第一条最佳实践建议是熟练使用 Unity Profiler。Profiler 是一个工具,你可以用它来获取关于你游戏性能数据,包括CPU 使用率GPU 使用率渲染内存UIUI 详细信息。为了查看关于 UI 的性能数据,请执行以下步骤:

  1. 单击窗口 > 分析 > Profiler以打开Profiler窗口。

  2. Profiler窗口中单击UIUI 详细信息模块区域,以查看与 UI 相关的性能数据,例如布局渲染消耗的 CPU 时间。

![Figure 3.47 – 分析窗口中的 UI 区域img/Figure_3.47_B17146.jpg

图 3.47 – 分析窗口中的 UI 区域

除了UIUI 详细信息区域外,Profiler窗口中的CPU 使用率区域还提供了与 UI 相关的性能信息。在CPU 使用率区域,你可以看到特定标记(如UGUI.Rendering.RenderOverlays)消耗的 CPU 时间,如下面的截图所示:

![Figure 3.48 – 分析窗口中的 CPU 使用率区域img/Figure_3.48_B17146.jpg

图 3.48 – 分析窗口中的 CPU 使用率区域

这只是对 Profiler 工具的简要介绍。在接下来的章节中,我们将详细讨论 Unity Profiler。

多个画布

第二条最佳实践建议是在 Unity 中实现 UI 时需要考虑的一个重要方面,尤其是在你的游戏 UI 非常复杂时。如果需要,你可能需要创建多个画布来管理和显示不同的 UI 元素。正如我们之前提到的,一个画布生成表示放置在其上的 UI 元素的网格,并在 UI 元素更改时重新生成网格。

假设你在一个画布中构建了整个游戏的 UI,包含数千个 UI 元素,当画布上的一个或多个 UI 元素发生变化时,用于显示 UI 的所有网格都会重新生成。这可能会很昂贵,你可能会遇到持续几毫秒的 CPU 峰值。

因此,根据 UI 元素的更新频率创建多个不同的画布来管理它们是一个好主意。例如,进度条和计时器等频繁更新的动态 UI 元素可以放在一个画布中,而 UI 面板和背景图像等不常更新的静态 UI 元素可以放在另一个画布中。当然,没有一劳永逸的方法;您需要根据项目来管理画布。

使用精灵图集

当我们讨论 UI 图像时,我们已经介绍了精灵是用于 UI 和其他 2D 游戏元素的两个维图形对象。当将新纹理导入 Unity 编辑器时,我们可以将此纹理的纹理类型设置为精灵。因此,您的游戏项目可能包含大量的精灵文件。如果是这样,许多精灵被视为独立的个体,这可能会降低渲染性能。这是因为 Unity 将为场景中的每个精灵发出绘制调用,多个绘制调用可能会消耗大量资源并负面影响您的游戏性能。

注意

绘制调用是调用图形 API 来绘制对象(例如,绘制一个三角形)。

如以下截图所示,有两个绘制调用用于渲染Button1Button2,因为这些两个按钮使用了两个不同的纹理:

![图 3.49 – 多个绘制调用

![img/Figure_3.49_B17146.jpg]

图 3.49 – 多个绘制调用

因此,将几个纹理或精灵组合成一个组合纹理是一个好主意。

我们可以执行以下步骤来使用 Unity 提供的精灵图集来组合纹理:

  1. 如果精灵图集打包被禁用,请在编辑 > 项目设置 > 编辑器 > 精灵打包器 > 模式中启用它。

  2. 点击资产 > 创建 > 2D > 精灵图集以创建精灵图集资产。

![图 3.50 – 创建精灵图集

![img/Figure_3.50_B17146.jpg]

图 3.50 – 创建精灵图集

  1. 在精灵图集资产的打包对象下拉菜单中,选择+符号以添加纹理或文件夹到精灵图集。

然而,我们仍然需要意识到,尽管精灵图集可以有效地减少绘制调用的数量,但不当的使用很容易导致内存浪费。当一个精灵在图集中激活时,Unity 将加载属于该精灵的图集中的所有精灵。如果图集中有大量的精灵,即使场景中只引用了一个精灵,整个图集也将被加载,这会导致大量内存消耗。为了解决这个问题,可以根据其用途将精灵打包到多个较小的图集中。例如,用于登录面板的精灵可以打包为登录面板图集,而用于游戏角色面板的精灵可以打包为角色面板图集。

摘要

在本章中,我们首先介绍了 uGUI 解决方案中最常用的 UI 组件类,例如CanvasRect TransformImage组件。然后,我们解释了 Unity 中的事件系统、遗留的输入模块以及 Unity 提供的更强大的新输入系统包。

我们还讨论了在 Unity 中开发 UI 时,如何通过使用 MVVM 架构模式将组件相互解耦。

最后,我们探讨了在 Unity 中实现游戏 UI 的一些最佳实践,以优化由 UI 引起的性能问题。

在下一章中,我们将学习 Unity 中的动画系统,同时,我们也会介绍如何在 Unity 中优化动画性能。

第四章:第四章:使用 Unity 动画系统创建动画

不论是 2D 游戏还是 3D 游戏,如果你想使游戏生动有趣,良好的动画是必不可少的。作为一个非常流行的游戏引擎,Unity 提供了易于使用且功能强大的动画开发工具。在本章中,我们将探讨 Unity 中的动画系统,有时也称为Mecanim,以使你的游戏中的场景和角色不是静态的,而是动态的。然后,我们将通过两个示例演示如何在 Unity 中实现 3D 和 2D 动画。最后,我们将介绍如何提高 Unity 动画系统的性能。

本章我们将涵盖以下关键主题:

  • 探索 Unity 动画系统的概念

  • 在 Unity 中实现 3D 动画

  • 在 Unity 中实现 2D 动画

  • 提高 Unity 动画系统的性能

到本章结束时,你将能够创建 Unity 中的 3D 和 2D 动画,以及了解如何通过 C#代码控制动画以及如何优化动画性能。

技术要求

在开始之前,我建议你首先从 Unity Asset Store 下载Unity-Chan! Modelassetstore.unity.com/packages/3d/characters/unity-chan-model-18705

这个可爱的 3D 女孩模型资产由 Unity Technologies Japan 制作,所有开发者都可以下载并使用它来制作游戏。

本资产包含以下内容:

  • 美丽的纹理 3D 模型

  • "Unity-Chan!"原始着色器

  • 31 个动画

  • 31 个静态姿势

  • 由混合形状制成的 12 种表情

  • 一个示例移动场景和其他示例场景

现在,让我们开始吧!

探索 Unity 动画系统的概念

动画是游戏开发的一个重要方面。在本节中,我们将首先学习 Unity 动画系统的基础概念。具体来说,我们将介绍以下概念:

  • Animation Clips 是什么以及如何在 Unity 中创建一个 Animation Clip

  • 如何创建 Animator Controller 来管理一组角色动画

  • 如何使用 Avatar 系统与动画绑定一起工作

  • Animator 组件是什么以及如何使用它将动画分配给 GameObject

让我们继续前进!

Animation Clips

Unity 中的动画可以从简单的立方体旋转到复杂的角色动作,它们都是基于Animation Clips的,这些 Clips 用于在 Unity 中存储基于关键帧的动画。

我们可以在 Unity 编辑器中手动创建一个 Animation Clip 文件,通过Animation窗口实现一些简单的传统关键帧动画效果,例如简单的移动、旋转等。

以下步骤展示了如何在场景中动画化一个 GameObject:

  1. Hierarchy窗口(右侧)中右键单击,从弹出菜单中选择3D Object | Cube以在场景中创建一个新的Cube对象。

图 4.1 – 在场景中创建一个新的 Cube 对象

  1. 在场景视图中选择Cube对象。然后导航到窗口 | 动画 | 动画以打开动画窗口。除了从菜单中打开此窗口外,我们还可以使用Ctrl + 6快捷键打开它:

图片

图 4.2 – 打开动画窗口

  1. 动画窗口中点击创建按钮以创建一个新的动画剪辑:

图片

图 4.3 – 动画窗口

  1. 点击添加属性按钮以显示可以动画化的可用属性列表。如图图 4.4所示,我们不仅可以修改位置旋转,还可以修改其他组件的属性。在这里,我们可以通过点击旁边的+按钮将缩放添加为将被动画化的属性:

图片

图 4.4 – 添加属性

  1. 当添加属性时,默认创建两个关键帧:第一个关键帧和第二个关键帧分别位于时间轴上的0:001:00。因此,我们需要创建第三个关键帧来改变0.5,如图图 4.5所示:

图片

图 4.5 – 添加关键帧

  1. 为了预览动画,点击播放按钮播放动画剪辑。你会看到立方体的体积迅速缩小然后缓慢增大。

图片

图 4.6 – 播放动画剪辑

我们还可以使用录制模式在 Unity 中创建动画剪辑,如下步骤所示:

  1. 创建新的 GameObject 和打开动画窗口的步骤与之前相同。所以,让我们直接开始如何使用录制模式为场景中的球体对象创建动画剪辑。我们可以点击录制按钮以启用关键帧录制模式,如图图 4.7所示:

图片

图 4.7 – 启用关键帧录制模式

  1. 点击录制按钮后,它将进入录制模式。现在,我们可以通过在时间轴上拖动来修改我们想要的时间点:

图片

图 4.8 – 在时间轴上拖动

在录制模式下,无论你是在场景中移动、旋转还是缩放目标 GameObject,Unity 都会自动将当前时间点的关键帧添加到动画剪辑中。在这里,我们可以将 GameObject 从其原始位置(0, 0, 0)移动到新的位置,比如(1, 0, 0)。你可以在下面的图中看到 Unity 为球体对象创建了关键帧:

图片

图 4.9 – Unity 创建关键帧

  1. 最后,再次点击录制按钮退出录制模式,并点击播放按钮播放我们刚刚创建的动画剪辑:

图片

图 4.10 – 播放动画剪辑

此外,将外部动画资产导入 Unity 编辑器也可以自动创建动画剪辑文件。

图片

图 4.11 – 导入动画资产后自动创建的动画片段

通用FBX文件、Autodesk® 3ds Max® (.max)文件、本地Autodesk® Maya® (.mb 或.ma)文件和Blender™ (.blend)文件等动画文件需要在 Unity 项目中导入后才能被 Unity 使用。动画文件导入后,Unity 将生成动画片段文件。我们可以通过在 Unity 编辑器中双击动画片段文件来打开Animation窗口以查看动画片段,如图图 4.11所示。

现在您已经了解了什么是动画片段以及如何在 Unity 中创建一个新的动画片段,让我们继续下一个概念:Animator Controller。

Animator Controller

想象一下我们的游戏角色拥有多个动画。例如,假设一个角色可以跑和攻击——管理这两个动画对于角色来说非常重要。在 Unity 项目中,我们使用Animator Controller资产来安排和维护角色或其他动画 GameObject 的动画集。

一个Animator Controller将引用它使用的动画片段,并使用所谓的状态机来管理各种动画状态及其之间的转换。

我们可以导入我们之前下载的Unity-Chan!模型资产。

图 4.12_B17146

图 4.12 – Unity-Chan!模型 ActionCheck 场景

此资产提供了多个演示场景;我们选择打开Assets/unity-chan!/Unity-chan! Model/Scenes文件夹。

图 4.13_B17146

图 4.13 – Unity-Chan!模型

图 4.13所示,Unity-Chan!模型已在场景中设置。如果我们打开此模型使用的 Animator Controller 文件,我们可以看到此模型使用的所有动画片段以及Animator窗口中显示的状态机中的动画片段之间的转换,如图图 4.14所示。

图 4.14_B17146

图 4.14 – Animator Controller

我们还可以按照以下步骤在 Unity 编辑器中手动创建Animator Controller资产:

  1. 选择项目视图,右键单击以打开菜单。

  2. 选择创建 | Animator Controller以创建一个新的Animator Controller资产:

图 4.15_B17146

图 4.15 – 创建新 Animator Controller

  1. 双击我们刚刚创建的Animator Controller资产以打开Animator窗口。

  2. 在这里,直接将POSE01动画片段拖入Animator窗口以创建一个新的状态。在状态机中,状态由一个方框表示,因为POSE01动画片段是我们拖入的第一个动画,所以我们可以看到这个动画连接到了 Animator Controller 的入口点,这表明这个动画将是默认动画。图 4.16_B17146

图 4.16 – 创建新状态

  1. 通过将POSE02动画片段拖入Animator窗口来创建第二个状态。

  2. 选择 POSE01 状态,右键单击以打开菜单,然后选择 创建过渡 以在 POSE01POSE02 之间创建过渡。

图 4.17

图 4.17 – 创建过渡

现在,我们已经创建了一个动画控制器资产,并将一些动画剪辑添加到了状态机中。

Avatar

与我们之前为 Unity 内置的 Cube 模型创建的动画不同,从外部工具导入到 Unity 编辑器的模型可能更复杂。例如,Unity-Chan! 模型是一个类似人类的模型。在 Unity 中,模型由 三角形 网格表示,而三角形由 顶点 组成。当模型被动画化时,顶点的位置将被修改。显然,当许多顶点组成一个模型时,单独移动每个顶点是一个低效的操作。因此,计算机动画中的一种常见技术是在动画过程中不单独移动每个三角形,而是在动画之前对模型进行蒙皮。这种技术称为 骨骼动画骨架

Unity 使用一个名为 Avatar 的系统来识别动画模型是否为人类布局以及模型的哪些部分对应于头部、身体、手臂、腿部等。

图 4.18

图 4.18 – 导入设置

我们可以通过在 Unity 编辑器中点击模型来打开 Unity-Chan! 的 导入设置 窗口。如图 4.18 所示,我们可以在窗口的 骨架 选项卡中指定其骨架类型,在这种情况下,该模型的 动画类型人类。动画系统将尝试将模型的现有骨骼结构与 Avatar 骨骼结构相匹配。如果骨骼结构可以成功映射,则将自动创建一个 Avatar 资产,如图 4.19 所示。

图 4.19

图 4.19 – unitychanAvatar

注意

骨骼骨骼动画 中相互连接的部件的分层集合。蒙皮 使得三角形的每个顶点都依赖于骨骼。

另一方面,如果动画系统无法自动将模型的现有骨骼结构与 Avatar 骨骼结构相匹配,我们需要手动配置 Avatar。此外,即使骨骼结构可以成功映射,有时我们可能想要手动调整以达到更好的效果。在这种情况下,我们也可以通过配置 Avatar 资产来修改它。

图 4.20

图 4.20 – 配置 Avatar

按照以下步骤进行配置:

  1. 点击模型以打开其 导入设置 窗口。

  2. 在窗口的 骨架 选项卡中点击 配置 按钮以打开 Avatar 检查器 窗口。

  3. 按照如图 4.20 所示,在 Avatar 检查器 窗口中配置骨骼。

Avatar 遮罩

在我们将模型骨骼与 Unity 的 Avatar 系统骨骼结构之间创建映射之后,我们可以播放这个角色的动画。然而,有时我们可能不想动画化角色的所有骨骼。一个常见的例子是行走动画可能涉及角色摆动手臂,但如果他们拿起电话打电话,他们的手臂应该握住电话而不是在行走时摆动。在这种情况下,我们希望将动画限制在特定的身体部位,Unity 提供的Avatar Mask资产可以帮助我们实现这个目标。

我们可以通过选择资产 | 创建 | Avatar Mask来创建一个新的Avatar Mask资产,如图图 4.21所示。

图片

图 4.21 – 创建 Avatar Mask

在创建一个新的Avatar Mask资产后,我们可以配置它以定义动画的哪些部分应该被遮罩。如图图 4.22所示,Avatar Mask 检查器窗口允许我们点击一个类人身体图来选择或取消选择某些部分进行遮罩。在这里,我们遮罩了 Unity-Chan!的一个手臂,这意味着某些动画在运行时不会影响这个手臂。

图片

图 4.22 – Avatar Mask

为了让这个 Avatar Mask 资产生效,我们需要将其应用到 Animator Controller 上。

图片

图 4.23 – 应用 Avatar Mask 资产

在这个例子中,我们将按照以下步骤将此 Avatar Mask 资产应用到我们之前创建的 Animator Controller 中:

  1. 双击新 Animator Controller文件以打开Animator窗口。

  2. 点击基础层项的齿轮图标以打开层设置面板。

  3. 然后,点击Mask字段旁边的单选按钮,并从弹出的选择 AvatarMask窗口中选择新的Avatar Mask资产来应用,如图图 4.23所示。

这样,我们就可以将动画限制在特定的身体部位。

接下来,我们将探讨 Unity 动画开发解决方案中的另一个重要概念,即Animator组件。通过使用 Animator,我们可以在游戏中使用这个 Animator Controller 资产。

动画组件

在前面的章节中,我们探讨了 Animation Clips、Animator Controllers 和 Avatar 在 Unity 中的使用。然而,仅仅创建 Animation Clips、Animator Controllers 和 Avatar 资产是不够的,以在游戏场景中动画化角色。我们仍然需要 Animator 组件将动画分配给场景中的 GameObject。

注意

Animator Controllers 和 Animator 组件名称相似但功能不同。Animator 组件使用关联的 Animator Controller 将动画应用到 GameObject 上。

如果你在一个 GameObject 上看到 Animator 组件,你会发现它将汇集我们之前讨论的所有各种资产。它是 Unity 动画解决方案中绑定系统的根,因此非常重要。

图片

图 4.24 – Animator 组件

在这里,我们可以将 Unity-Chan!模型拖入场景以创建一个新的角色 GameObject,并将 Animator 组件添加到 GameObject 中,如图 4.24 所示。这是 Animator 组件的配置方式:

  • Animator 组件需要引用 Animator Controller,它定义了要使用的动画剪辑。我们可以将我们在Animator Controller部分创建的 Animator Controller 分配给它,名称为新 Animator Controller

  • 由于 Unity-Chan!模型是一个人形模型,因此需要为这个 Animator 组件提供相应的 Avatar 资产。

  • Animator 组件的应用根运动设置决定了是否将根节点的位置或旋转的任何更改应用于动画。

  • Animator 组件的更新模式设置决定了 Animator 组件的更新模式。有三个不同的选项,即正常动画物理未缩放时间

图片

图 4.25 – 更新模式设置

  • 最后的设置是裁剪 模式,它决定了 Animator 组件的动画是否应在屏幕外播放。有三个不同的选项,即始终动画裁剪更新变换完全裁剪

图片

图 4.26 – 裁剪模式设置

阅读本节后,我们对 Unity 动画系统的概念有了了解。我们将在下一节中使用这个系统创建 3D 动画!

在 Unity 中实现 3D 动画

在前面的章节中,我们已经介绍了 Unity 动画系统中的某些重要概念,例如动画剪辑Animator 控制器AvatarAnimator 组件。在本节中,你将学习如何使用这些概念为 3D 模型实现动画。

导入动画资产

首先,我们需要知道如何从/Assets/unity-chan!/Unity-chan! Model/Art/Animations文件夹将动画资产导入 Unity,如图下所示:

图片

图 4.27 – 动画文件夹

在这里,我们可以从项目窗口中选择一个动画资产以打开其导入设置窗口。

图片

图 4.28 – 动画导入设置

如图 4.28 所示,在检查器窗口中点击动画以切换到动画选项卡,你可以看到包含在动画资产中的所有动画剪辑。

动画压缩

此外,在动画选项卡中,我们还可以找到与导入相关的设置。

图片

图 4.29 – 动画压缩设置

图 4.29 所示,有一个名为 动画压缩 的设置,其默认值为 关闭,这意味着 Unity 在导入时不会减少关键帧的数量。在这种情况下,Unity 将保持最高精度的动画,但代价是动画文件大小很大。如果减少动画的大小,无论是在我们的硬盘上还是在内存中,都很重要,我们可以考虑其他两个 动画压缩 选项,即 关键帧减少最佳

图 4.30 – 最佳

如果值为 0.5;值越小,精度越高。

如果选择 最佳 选项,Unity 将决定如何压缩动画剪辑。

动画事件

我们还可以修改单个动画剪辑的属性。在列表中选择一个动画剪辑后,我们可以向下滚动以查看该特定动画剪辑的设置。

图 4.31 – 动画剪辑的设置

如前图所示,动画剪辑有一个添加动画事件的选项,这允许我们在时间轴的指定点调用脚本中的函数。

为了创建一个新的动画事件,首先,我们需要在时间轴上定位我们想要添加事件的位置,然后点击左上角的 添加事件 按钮。时间轴上会创建一个小白标记,表示新事件。

在创建新事件后,我们还需要按照以下步骤进行配置:

  1. 图 4.32 所示,有多个字段需要填写,我们在附加到 GameObject 的脚本中的 PrintStringFromAnimationEvent 函数中输入了名称 PrintStringFromAnimationEvent。其他几个字段可以为这个函数传入不同类型的参数,例如 FloatIntString

图 4.32 – 添加动画事件

  1. 设置事件后,请记住点击 应用 按钮以使事件配置生效。

同时,我们需要实现一个函数,其名称必须与已填入函数字段的名称完全匹配,即 PrintStringFromAnimationEvent

    public void PrintStringFromAnimationEvent(string
      stringValue)
    {
        Debug.Log("PrintStringFromAnimationEvent is called
          with a value of " + stringValue);
    }

这个函数将接受一个字符串类型的参数;一旦这个动画事件被触发,这个函数将被调用,并且字符串值将在 控制台 窗口中打印出来,如图所示:

图 4.33 – 在控制台窗口中打印字符串值

现在我们已经了解了如何将动画资产导入 Unity 以及如何设置动画事件,让我们将注意力转向设置动画控制器!

配置动画控制器

在导入动画资源后,我们需要设置一个 Animator Controller 来引用将在我们的游戏中使用的这些 Animation Clips。实际上,当我们之前介绍它时,我们已经创建了一个 Animator Controller,并引用了两个将使用的 Animation Clips。然而,我们没有配置这个 Animator Controller;例如,我们没有配置如何在这两个动画之间切换。在本节中,我们将探讨如何配置 Animator Controller 并使用 C#代码在不同动画之间切换。

调整动画速度

我们可以通过在Animator窗口中选择状态来查看 Animator Controller 中特定状态的设置。

图片

图 4.34 – 动画状态的设置

有多个设置,例如1。如果设置为0.5,则2的播放速度将使运动时间的播放速度是正常速度的两倍,播放时间是正常的一半。

动画器参数

如我们在图 4.35中看到的,还有其他需要使用参数的设置。这些参数被称为动画参数,它们是在 Animator Controller 中定义的变量,可以从 C#脚本中访问和分配值。因此,它们是使用 C#代码控制动画的重要部分。为了添加新参数和编辑现有参数,我们应该通过点击右上角的Parameters按钮切换到Animator窗口的Parameters部分。

图片

图 4.35 – 参数部分

如前图所示,参数可以是以下四种类型之一:

  • 浮点型

  • 整型

  • 布尔型

  • 触发器

作为演示,我们可以添加一个名为SpeedMultiplier的新参数。然后,我们再次打开POSE01动画状态的设置,并在Multiplier设置之后勾选Parameter复选框,你将看到新创建的SpeedMultiplier参数出现。

图片

图 4.36 – SpeedMultiplier 参数

如我们之前提到的,这些参数可以使用 C#代码访问和分配值。因此,我们可以创建一个新的脚本来访问和设置SpeedMultiplier参数的值,如下面的代码片段所示:

using UnityEngine;
public class AnimationParametersTest : MonoBehaviour
{
[SerializeField] 
private Animator _animator;
[SerializeField] 
private float _speedMultiplier;
    // Start is called before the first frame update
    void Start()
    {
        if(_animator == null)
        {
            _animator = GetComponent<Animator>();
        }
        _animator.SetFloat("SpeedMultiplier",
          _speedMultiplier);
    }
}

在这里,我们创建一个新的 C#脚本名为AnimationParametersTest,并获取 Animator 组件的引用,然后通过调用 Animator 组件的SetFloat方法来设置参数的值,因为此参数的类型是 float。同样,Animator 组件也有SetIntegerSetBoolSetTrigger方法,用于设置不同类型参数的值。

配置过渡

动画参数也可以用来实现动画切换。我们可以使用动画过渡来连接两个动画状态并在它们之间切换。然而,默认情况下,动画过渡将自动在两个连接的动画状态之间切换,但我们在开发游戏时显然更喜欢能够控制动画的切换。

图 4.37

图 4.37 – 动画过渡

我们可以设置一个过渡,仅在特定条件为真时发生,并且可以使用动画参数来确定这些条件是否满足,因此我们可以在这里使用它们来控制动画的切换。

以下步骤展示了如何从 C#代码中添加一个新参数、设置条件以及控制不同动画的切换:

  1. 切换到名为Runbool变量参数;其默认值为false,如图下截图所示。

图 4.38

图 4.38 – 新参数

  1. 选择要应用条件的过渡;这里我们选择从POSE01POSE02的过渡。

  2. Go To Run到这个过渡中,如图图 4.39所示。

图 4.39

图 4.39 – 命名过渡

  1. 在同一窗口的底部,列出了此过渡的所有条件。通过点击+按钮,为这个Go To Run过渡添加一个新条件,如图所示:

图 4.40

图 4.40 – 添加新条件

  1. 添加新条件后,我们还需要选择一个参数,其值被视为条件。这里的参数是Run。当Run参数的值为true时,可以认为条件已满足。

图 4.41

图 4.41 – 选择参数作为条件

  1. 然而,Run参数的默认值是false。因此,为了从POSE1切换到POSE2,创建一个 C#脚本,设置值如下:

图 4.42

图 4.42 – C#代码片段

Input.GetKey方法会在用户按下由KeyCode标识的键时返回true;否则,它将返回false,然后我们使用这个值来设置Run参数的值。因此,我们可以通过按键来控制动画的切换。

在阅读本节后,我们已经学会了如何在 Unity 中实现 3D 模型的动画以及如何通过 C#代码控制动画。接下来,我们将讨论如何实现 2D 资源的动画。

在 Unity 中实现 2D 动画

在本节中,我们将使用我们之前探索的工具在 Unity 中实现 2D 动画。

2D 动画的实现与 3D 动画的实现不同。2D 动画的常见实现技术是使用Sprite Animations,这些是为 2D 资源创建的动画片段。

创建精灵动画有许多方法;我们可以在 Unity 编辑器的动画窗口中直接创建它们,或者在像 Aseprite 这样的流行动画精灵编辑器或 Piskel 这样的免费在线精灵编辑器等外部工具中创建它们。

在这里,我们使用由外部工具创建的精灵动画。您可以从 Unity Asset Store 在此处下载此资产:assetstore.unity.com/packages/2d/characters/free-pixel-mob-113577

图片

图 4.43 – 精灵表

下载资产后,我们可以发现此图像包含许多不同的精灵,如图图 4.43所示。我们称之为精灵表,它是一个包含连续精灵的图像,通常用于 2D 资产的动画。

注意

如果一个图像包含一组非连续的精灵图像,我们称之为精灵图集,它通常用于实现 UI。

然后,我们应该通过以下步骤将此图像文件导入到 Unity 编辑器中:

  1. 图 4.44所示,由于此图像包含一系列精灵图像,我们在导入设置窗口中将精灵模式设置为多个

图片

图 4.44 – 精灵表的导入设置

  1. 点击精灵编辑器按钮,在 Unity 中打开精灵编辑器精灵编辑器提供了允许我们修改精灵表的工具,例如将精灵表切割成单个精灵。

图片

图 4.45 – Unity 中的精灵编辑器

  1. 通过点击16,将1的值更改,然后点击下拉菜单底部的切片按钮,并关闭精灵编辑器,如图下所示截图:

图片

图 4.46 – 通过单元格计数网格

  1. 然后,我们可以在项目窗口中选择此精灵表资产以展开它,您将看到其中所有的单个精灵:

图片

图 4.47 – 精灵表中的精灵

到目前为止,我们已经导入了资产并创建了这些精灵。下一个问题是,我们如何使用这些精灵在 Unity 中创建动画剪辑?答案并不复杂。我们只需要选择组成我们想要创建的动画剪辑的精灵并将它们拖入场景。Unity 编辑器将自动创建动画剪辑并询问我们选择存储动画剪辑文件的文件夹,如图下所示截图:

图片

图 4.48 – 创建动画剪辑文件

在这个例子中,我们从精灵表中选择了前八个精灵并将它们拖动到 Unity 编辑器的场景视图中。然后,我们将动画剪辑文件重命名为walk并保存。

Unity 将创建动画剪辑文件,正如我之前提到的,还将创建一个新的 Animator Controller 资产。在场景中还会创建一个新的 GameObject,并附加 Animator 组件,它引用 Animator Controller,如图 图 4.49 所示。

图 4.49 – 场景中的新 GameObject

现在,我们可以通过在 Unity 编辑器中点击 播放 按钮来运行游戏并播放动画,我们可以看到 行走 动画正在播放!

图 4.50 – 播放行走动画

阅读本节后,您已经学会了如何为 2D 资产实现动画;接下来,我们将分享一些提高动画性能的技巧。

提升 Unity 动画系统的性能

在 Unity 中,动画的实现可能会导致过度的内存使用和 CPU 开销。在本节中,我们将讨论如何避免由动画引起的性能问题。具体来说,我们将首先介绍 Unity 性能分析器 工具及其如何用于查看与动画相关的性能指标,然后我们将探讨如何减少动画的 CPU 开销和内存占用。

Unity 性能分析器

首先,我们应该学习如何使用工具来查看和定位性能瓶颈,而不是依赖于主观猜测和经验。当然,经验的重要性不言而喻,但使用工具将帮助您更快地定位问题。

Unity 编辑器为开发者提供了一个性能分析器工具,我们可以使用它来查看游戏的详细内存使用情况和实时 CPU 开销。

为了查看关于动画 CPU 开销的性能数据,我们应该遵循以下步骤:

  1. 点击 窗口 | 分析 | 性能分析器 打开 性能分析器 窗口。

  2. 性能分析器 窗口中点击 CPU 使用率 模块区域,以查看 CPU 开销的性能数据,例如 Animator.Update 消耗的 CPU 时间,如图 图 4.51 所示。

图 4.51 – Unity 性能分析器

  1. Unity 性能分析器还允许我们切换到 层次结构 视图和 时间轴 视图,在某些情况下这会更加直观。

图 4.52 – 性能分析器窗口中的时间轴视图

除了 CPU 使用率 模块外,我们还可以在 内存 模块中查看游戏的详细内存消耗。

  1. 性能分析器 窗口中点击 内存 模块区域,以查看内存消耗的性能数据。默认显示模式是 简单 模式,内存消耗在 性能分析器 窗口中按类型计数。例如,纹理 的内存使用量约为 106.3 MB,网格 的内存使用量约为 4.5 MB,如图所示:

图 4.53 – 性能分析器窗口中的内存数据

  1. 简单模式相比,详细模式功能更强大。我们可以通过在上左角的下拉菜单中选择详细来从简单模式切换到详细模式。

图 4.54 – 切换到详细模式

详细模式不会像简单模式那样实时显示内存消耗数据。相反,我们需要手动点击取样本****播放模式按钮来在当前时间采样游戏内存。

图 4.55 – 取内存样本

根据游戏中创建的对象数量或消耗的内存量,采样时间将不同。但一旦采样完成,我们将看到详细的内存开销;例如,在下面的屏幕截图中,有 82 个动画剪辑占用了 50.1 MB 的内存。

图 4.56 – 分析器窗口中的详细内存数据

从前面的介绍中,我们可以看到动画的优化应主要关注 CPU 负载和内存消耗。因此,当使用 Unity 的动画系统实现动画时,以下两个最佳实践需要考虑。

动画师剔除模式

为了减少动画的 CPU 负载,我们应该将动画师窗口的剔除模式属性设置为剔除更新变换完全剔除

图 4.57 – 动画师剔除模式

通过将其设置为剔除更新变换,当动画师在屏幕上不可见时,Unity 将禁用动画系统的一些功能,如重定向、逆运动学(IK)变换。如果设置为完全剔除,当动画师在屏幕上不可见时,Unity 将完全禁用动画。因此,可以减少 CPU 负载的目标可以达成。

动画压缩

另一个最佳实践是在动画导入设置窗口中将动画压缩设置为以节省内存。

图 4.58 – 动画压缩

通过将其设置为关键帧减少,Unity 将在导入时减少关键帧,并在将动画存储在文件中时压缩关键帧。如果设置为最优,Unity 将决定如何压缩,无论是通过减少关键帧还是使用密集格式。

摘要

在本章中,我们首先介绍了 Unity 动画系统的一些最重要的概念,例如动画剪辑、动画控制器、Avatar 和动画组件。然后,我们演示了如何在 Unity 中实现 3D 动画,包括如何将动画资产导入 Unity 编辑器、如何在动画剪辑上创建动画事件、如何设置动画参数通过 C# 代码控制动画等。

我们还讨论了如何在 Unity 中实现 2D 动画。2D 动画的实现方式与 3D 动画不同。2D 动画的一种常见实现技术是使用精灵动画,这些动画剪辑是为 2D 资产创建的。

最后,我们探讨了在 Unity 中实现动画的一些最佳实践,以优化由动画系统引起的性能问题。

在下一章中,我们将学习 Unity 中的物理系统,同时,我们也会介绍如何在 Unity 中优化物理性能。

第五章:第五章: 使用 Unity 物理系统

游戏中的物理模拟不仅是为了实现游戏的现实感而不可或缺的功能。将物理模拟添加到您的游戏中通常可以提高游戏的趣味性和可玩性。一般来说,它决定了物体如何移动以及它们如何相互碰撞,例如玩家与墙壁之间的碰撞和重力的影响。作为一个流行的游戏引擎,Unity 为开发者提供了各种工具,允许开发者将物理模拟功能集成到他们的游戏中。

本章我们将涵盖以下关键主题:

  • Unity 物理系统中的概念

  • 使用物理系统进行脚本编写

  • 基于物理系统创建简单游戏

  • 提高物理系统的性能

到本章结束时,你将能够正确且高效地在 Unity 中应用物理模拟,为你的游戏添加更多真实感或趣味性。

现在,让我们开始吧!

技术要求

你可以在 GitHub 上的以下存储库中找到完整的代码示例:github.com/PacktPublishing/Game-Development-with-Unity-for-.NET-Developers

Unity 物理系统中的概念

模拟是游戏中的一个有用功能。Unity 为不同的目的提供了不同的工具。例如,如果我们想开发一个 3D 游戏,那么我们可以使用与Nvidia PhysX 引擎集成的内置 3D 物理。如果我们想为 2D 游戏添加物理模拟,那么我们可以选择与Box2D 引擎集成的内置 2D 物理。

注意

PhysX是由 Nvidia 作为 Nvidia GameWorks 软件套件的一部分开发的开源实时物理引擎中间件 SDK。Box2D是一个免费的开源 2D 物理模拟引擎。

除了这些内置的物理解决方案之外,Unity 还提供了物理引擎包。这些是Unity Physics包和Havok Physics for Unity包,以及它们用于 Unity 的数据导向技术堆栈(DOTS)项目。我们将在后面的章节中介绍 DOTS。

注意

Havok Physics主要设计用于视频游戏,允许在 3D 中实时进行刚体的碰撞和动力学。

在本章中,我们将重点关注内置的物理知识,并首先学习 Unity 物理系统的基本概念。

Collider

与渲染功能类似,物理引擎也需要了解游戏场景中GameObjects的形状,以便正确执行物理模拟。在开发 Unity 项目时,我们可以使用Collider组件来定义 GameObject 的形状,以便进行物理碰撞计算。

应该注意的是,由碰撞体定义的形状不必与模型的形状完全相同。我们甚至可以创建一个没有模型显示的碰撞体。例如,我们可以在场景中创建一个新的立方体,并且一个碰撞体组件将自动创建并附加到这个立方体上。然后,可以从以下图像中修改碰撞体的形状;其形状与模型的形状不同。

![图 5.1 – 修改碰撞体的形状(绿色框架)图片

图 5.1 – 修改碰撞体的形状(绿色框架)

为了减少物理模拟的复杂性并提高游戏性能,我们经常使用一些粗糙的形状,例如盒子碰撞体球体碰撞体。接下来,我们将探讨最常用的碰撞体之一,即盒子碰撞体。

原始碰撞体

Unity 为游戏开发者提供了一套原始碰撞体,包括球体碰撞体和盒子碰撞体。盒子碰撞体是 Unity 中最常用的碰撞体之一。它将自动创建并分配到场景中的立方体对象上,正如我们在图 5.1中看到的那样。我们还可以手动将一个新的盒子碰撞体添加到 GameObject 中,如下所示:

  1. 通过点击创建空对象按钮在场景中创建一个新的 GameObject。

![图 5.2 – 创建新的 GameObject图片

图 5.2 – 创建新的 GameObject

  1. 选择这个新创建的 GameObject,并在检查器窗口中点击添加组件按钮。

![图 5.3 – 添加组件图片

图 5.3 – 添加组件

  1. 在这里,我们可以在搜索框中选择盒子碰撞体以将盒子碰撞体组件添加到这个 GameObject 中。

![图 5.4 – 添加盒子碰撞体图片

图 5.4 – 添加盒子碰撞体

现在我们已经添加了一个新的盒子碰撞体组件,这个盒子碰撞体的属性如下截图所示:

![图 5.5 – 盒子碰撞体的属性图片

图 5.5 – 盒子碰撞体的属性

顶部的编辑碰撞体按钮允许我们编辑场景中这个盒子的形状。在此按钮下方,有一个是否触发复选框,如果启用,则表示此碰撞体将用作触发器。我们将在稍后介绍更多关于触发器的细节。此碰撞体的第三个属性是材质属性,用于引用物理材质实例。材质属性的默认值是 null,我们可以分配一个物理材质实例来调整碰撞对象的摩擦和弹跳效果。最后两个属性,中心大小,用于修改这个盒子的位置和大小。

如我们之前提到的,类似于盒子碰撞体,Unity 还提供了其他具有原始形状的碰撞体,例如球体碰撞体。

我们在物理碰撞模拟精度要求不高的情况下使用它们,但如果游戏需要精确的物理碰撞模拟,我们也可以使用另一个碰撞体,即网格碰撞体

网格碰撞体

有时,我们需要开发一些需要高物理模拟精度的游戏项目。在这种情况下,游戏对象的物理形状通常需要与游戏对象的模型网格形状一致。这就是为什么我们需要网格碰撞体的原因。

创建并添加网格碰撞体到游戏对象的方法有很多。因为网格碰撞体需要网格的信息,所以,创建网格碰撞体的第一种方法是将模型导入 Unity 编辑器。您可以通过勾选生成碰撞体复选框来导入自动附加网格碰撞体的网格,如图图 5.6所示:

![图 5.6 – 生成碰撞体

![图 5.06 – B17146.jpg

图 5.6 – 生成碰撞体

Unity 还允许我们手动将网格碰撞体组件添加到游戏对象。添加网格碰撞体的步骤与上一节中添加箱形碰撞体的步骤类似。选择目标游戏对象后,点击添加组件按钮,然后选择物理 > 网格碰撞体将其添加到游戏对象,如图图 5.7所示:

![图 5.7 – 将网格碰撞体添加到游戏对象

![图 5.07 – B17146.jpg]

图 5.7 – 将网格碰撞体添加到游戏对象

由于模型网格可能由许多顶点和三角形组成,并且网格碰撞体将基于网格生成,因此网格碰撞体的计算成本远大于之前介绍过的碰撞体。即使默认情况下,Unity 也不会计算网格碰撞体之间的碰撞,而只计算网格碰撞体与原始碰撞体(如箱形碰撞体和球形碰撞体)之间的碰撞。

为了启用网格碰撞体之间的碰撞检测,我们需要通过检查网格碰撞体组件的凸形复选框来降低它们的复杂性,如下截图所示:

![图 5.8 – 网格碰撞体的属性![图 5.08 – B17146.jpg 图 5.8 – 网格碰撞体的属性通过勾选此复选框,网格碰撞体将被限制为 255 个三角形。如果我们同时查看场景中的游戏对象,我们可以看到网格碰撞体仅大致与模型的网格一致,并且复杂性已经大大降低。![图 5.9 – 凸形网格碰撞体图片

图 5.9 – 凸形网格碰撞体

然而,如果我们现在运行游戏,我们会发现没有任何物理效果应用于游戏;例如,物体不会因为重力而落下。这是因为我们的游戏仍然缺少一个重要的组件。让我们接下来探索这个问题!

刚体

刚体组件是 Unity 中应用物理效果的必备组件。通过将刚体添加到 GameObject,物理将控制该 GameObject,例如对其应用重力。刚体通常与碰撞体一起使用;如果两个刚体相互碰撞,除非两个 GameObject 都附加了碰撞体,否则它们之间不会有碰撞效果,而是会相互穿过。

现在,让我们在场景中的 GameObject 上添加一个刚体组件:

  1. 通过点击3D 对象 > 立方体按钮在场景中创建一个新的立方体。

![Figure 5.10 – 创建一个新的立方体

![img/Figure_5.10_B17146.jpg]

图 5.10 – 创建一个新的立方体

  1. 选择这个新创建的立方体,并在检查器窗口中点击添加组件按钮。正如图 5.11所示,一个盒子碰撞体已经被附加到立方体上:

![Figure 5.11 – 添加组件

![img/Figure_5.11_B17146.jpg]

图 5.11 – 添加组件

  1. 在这里,我们可以选择物理 > 刚体按钮来向这个立方体添加一个刚体组件。

![Figure 5.12 – 添加盒子碰撞体

![img/Figure_5.12_B17146.jpg]

图 5.12 – 添加盒子碰撞体

现在我们已经添加了一个新的刚体组件,这个刚体的属性在图 5.13中展示:

![Figure 5.13 – 刚体的属性

![img/Figure_5.13_B17146.jpg]

图 5.13 – 刚体的属性

图 5.13所示,刚体的使用重力属性默认是勾选的,这意味着这个刚体将对立方体应用重力。如果我们此时运行游戏,我们会发现立方体会在重力作用下向下坠落。

除了使用重力属性外,刚体还有其他属性,我们将在下面介绍这些属性。

刚体组件的第一个属性是质量,它决定了刚体在相互碰撞时的反应。接下来是阻力属性,它决定了当物体在力的作用下移动时受到的空气阻力有多大。默认值是,这意味着当立方体受到力移动时没有空气阻力。角阻力属性与阻力属性类似,区别在于它决定了空气阻力在物体由于扭矩旋转时对物体的影响程度。

Is Kinematic 属性很重要,因为它决定了这个 GameObject 是否将由 Unity 的物理系统控制。默认情况下,它是禁用的。如果我们启用它,这个 GameObject 将不再由物理驱动。当您发现 Rigidbody 的运动很颠簸时,Interpolate 属性很有用。Interpolate 的默认值是 None,但 Unity 允许我们为这个属性选择不同的选项,例如 InterpolateExtrapolate,分别表示变换基于前一帧的变换进行平滑,或者变换基于下一帧的估计变换进行平滑,如下面的截图所示:

图 5.14 – Interpolate 属性的选项

图 5.14 – Interpolate 属性的选项

接下来是 Collision Detection 属性。有时,如果 Rigidbody 移动得太快,导致物理引擎没有及时检测到碰撞,那么调整这个属性可能是一个好主意。Unity 也为我们提供了不同的 Collision Detection 选项;这些是 DiscreteContinuousContinuous DynamicContinuous Speculative

图 5.15 – Collision Detection 属性的选项

图 5.15 – Collision Detection 属性的选项

Discrete 选项是默认值,用于检测正常碰撞。如果您遇到了快速对象碰撞的问题,那么 Continuous 是一个好的选择,但您应该记住,ContinuousDiscrete 相比会影响性能。

如果您想限制 Rigidbody 的运动,例如限制 Rigidbody 在某个方向上移动或不能在某个轴上旋转,那么您可以通过修改 Constraints 属性来实现。

图 5.16 – Constraints 属性

图 5.16 – Constraints 属性

图 5.16 所示,您可以选择一个轴来防止 Rigidbody 沿着该轴移动。

通过 Rigidbody 组件,我们为 GameObject 添加物理效果,但有时我们不想让 GameObject 根据物理模拟的结果移动,而只想能够检测两个对象之间的碰撞并触发某些事件。这在游戏中是一个常见的功能;例如,玩家进入某个区域后触发相应的逻辑。接下来,我们将介绍 Unity 提供的另一个功能来实现这样的要求。

触发器

除了提供碰撞效果外,碰撞体还可以用作触发器。然而,与用作普通碰撞体不同,当触发器启用时,Rigidbody 碰撞时没有碰撞效果。但是,物理效果仍然有效;例如,触发器仍然会受到重力的影响,但它不会与其他 Rigidbody 发生碰撞。

在开发 Unity 项目时,触发器用于检测来自其他 GameObject 的外部交互,并在脚本中的OnTriggerEnterOnTriggerStayOnTriggerExit函数中执行代码。这三个函数代表交互的三个不同阶段,即进入、停留和退出。我们将在下一节中详细介绍这些函数。目前,让我们通过以下步骤创建一个触发器:

  1. 选择我们之前创建的立方体对象以打开检查器窗口。

  2. 启用附加到这个立方体对象上的 Box Collider 组件的Is Trigger属性,如图下所示:

图 5.17 – 启用 Is Trigger 属性

图 5.17 – 启用 Is Trigger 属性

现在,这个立方体被设置为触发器,它将不再阻止其他刚体。由于它现在是一个触发器,我们可以用它来创建游戏关卡。例如,当玩家触摸这个立方体时,它将触发一个陷阱。

作为提醒,Unity 还提供了用于 2D 的物理组件。如果您想开发 2D 游戏并需要在游戏中应用物理效果,那么您可以轻松地以相同的方式添加这些物理组件的 2D 版本。

通过阅读本节,我们已经学习了 Unity 物理系统的一些概念,例如碰撞体、刚体和触发器。接下来,我们将继续探讨如何使用 C#脚本与物理系统交互。

使用物理系统进行脚本编写

在本节中,我们将探讨如何通过 C#脚本与物理系统交互。类似于上一节,我们也将分别介绍碰撞体、触发器和刚体的 C#方法。我们将从碰撞体的 C#方法开始。

碰撞方法

当碰撞体不被用作触发器时,刚体之间的碰撞仍然会发生。当发生碰撞时,会调用这三个方法,参数类型是Collision类,它提供了一些描述碰撞的信息,例如接触点和碰撞的冲击速度。

OnCollisionEnter

第一种方法是OnCollisionEnter,当这个碰撞体开始接触另一个碰撞体时被调用。当您希望这个对象受到物理碰撞的影响,同时在碰撞发生时执行一些游戏逻辑时,它非常有用。例如,当子弹击中游戏中的目标时,可以为其生成相应的爆炸效果,如下面的 C#代码片段所示:

using UnityEngine;
public class CollisionTest : MonoBehaviour
{
[SerializeField] 
private Transform _explosionPrefab;
    private void OnCollisionEnter(Collision collision)
    {
        var contact = collision.contacts[0];
        var rotation =
          Quaternion.FromToRotation(Vector3.up,
          contact.normal);
        var position = contact.point;
        Instantiate(_explosionPrefab, position, rotation);
        Destroy(gameObject);
    }
}

在代码片段中,我们访问了由碰撞对象提供的接触点数据,并在该点实例化了爆炸资产。

OnCollisionStay

OnCollisionStay 是我们将要探索的第二个方法。只要两个物体发生碰撞,OnCollisionStay 就会在每一帧被调用一次。由于这个方法将在物体碰撞期间被调用,因此它适合用来实现一些需要持续一段时间的逻辑。一个有趣的例子如下:假设你正在开发一款直升机游戏,并且当你想要直升机在接触地面时以最大强度的 60%运行引擎。在这种情况下,我们可以使用以下代码片段来实现这个功能:

using UnityEngine;
public class CollisionTest : MonoBehaviour
{
    private void OnCollisionStay(Collision collision)
    {
        if (collision.gameObject.name == "Ground")
        {
            //Reduce engine strength to 60%
        }
    }
}

OnCollisionExit

我要在这里介绍的最后一种方法是 OnCollisionExit。正如这个方法的名字所暗示的,当这个碰撞体停止接触另一个碰撞体时,它将被调用。如果在物体碰撞的开始通过 OnCollisionEnter 生成了一些内容,并且你希望在物体碰撞结束时销毁它们,那么你应该考虑使用 OnCollisionExit

using UnityEngine;
public class CollisionTest : MonoBehaviour
{
    private bool _isGrounded;
    private void OnCollisionEnter(Collision collision)
    {
        _isGrounded = true;
    }
    private void OnCollisionExit(Collision collision)
    {
        _isGrounded = false;
    }
}

上述代码片段展示了如何使用 OnCollisionExit 来重置 _isGrounded 字段。

我们已经介绍了在碰撞体中使用的典型方法。现在,我们将看看如何在 Unity 项目中使用触发器。

触发器方法

实际上,我们仍然使用碰撞体来实现触发器,只需要检查碰撞体组件的触发器选项。此时,碰撞体将不再产生物理碰撞效果,但会激活触发器事件。

常用来实现触发器的三个事件分别是 OnTriggerEnterOnTriggerStayOnTriggerExit。这三个方法在两个 GameObject 发生碰撞时被调用,参数类型是 Collider 类,它提供了关于参与此碰撞的其他碰撞体的信息。

OnTriggerEnter

第一种方法是 OnTriggerEnter,它在碰撞体开始接触另一个碰撞体时被调用。在这种情况下,应该启用触发器选项。当你想在周围元素上触发一些操作但不想产生物理碰撞效果时,这个方法很有用。例如,你可以用它来实现游戏中的陷阱。

它的使用也非常简单。我们只需要在方法的定义中包含将被触发的游戏逻辑,如下面的代码片段所示:

using UnityEngine;
public class TriggerTest : MonoBehaviour
{
    private void OnTriggerEnter(Collider other)
    {
        Debug.Log($"{this} enters {other}");
    }
}

当这个 GameObject 与另一个 GameObject 发生碰撞时,通过这个 GameObject 进入另一个 GameObject 的字符串将在控制台窗口中打印出来。

OnTriggerStay

OnTriggerStay 是我们将要探索的第二个方法。与之前讨论的 OnCollisionStay 方法类似,OnTriggerStay 将在其他碰撞体接触此触发器时在所有帧中被调用。这种方法也适合在游戏中实现类似陷阱的游戏玩法;例如,玩家进入有毒迷雾后将继续受到伤害:

using UnityEngine;
public class TriggerTest : MonoBehaviour
{
    private void OnTriggerStay(Collider other)
    {
        Debug.Log($"{this} stays {other}");
    }
}

在这里,我们只需要在OnTriggerStay方法的定义中放置将被触发的游戏逻辑,如前面的代码片段所示。

OnTriggerExit

我在这里要介绍的最后一种方法是OnTriggerExit。当其他碰撞体离开触发器时,将调用此方法。此方法适用于一些任务,例如销毁其他碰撞体进入此触发器时创建的 GameObject,重置状态等。以下代码片段展示了如何在OnTriggerExit中销毁一个 GameObject:

using UnityEngine;
public class TriggerTest : MonoBehaviour
{
    private void OnTriggerExit(Collider other)
    {
        Destroy(other.gameObject);
    }
}

Rigidbody 的方法

Rigidbody组件为我们提供了直接与 Unity 中的物理系统交互的能力。我们可以使用 C#脚本中Rigidbody组件提供的方法来对此 Rigidbody 施加力,我们还可以对模拟爆炸效果的 Rigidbody 施加力。

应注意,正如我们在第二章中提到的,Unity 中的脚本概念,在脚本中,建议使用FixedUpdate函数进行物理更新,因此我们应该在FixedUpdate函数中调用 Rigidbody 方法以应用物理效果。现在,让我们探索一些常用的方法。

AddForce

AddForce方法是与物理相关的最常用的方法之一。正如其名称所暗示的,我们可以调用此方法向 Rigidbody 施加力。AddForce函数的签名如下:

public void AddForce(Vector3 force,
  [DefaultValue("ForceMode.Force")] ForceMode mode);

如您所见,此方法需要两个参数,即世界坐标系中的力矢量和要应用的力类型。AddForce允许我们定义一个力矢量并选择如何将此力应用到 GameObject 上,以影响我们的 GameObject 的运动方式。

第一个参数,force,是一个矢量类型,指定了施加到该对象上的力的方向。

另一方面,ForceMode类型参数mode决定了应用的力类型。ForceMode是一个enum类型,它定义了四种不同的力类型。默认情况下,AddForce方法将使用其质量向 Rigidbody 添加持续的力量。在下一节中,我将详细介绍不同类型的力模式。

ForceMode

ForceMode定义在UnityEngine命名空间中,我们可以在以下代码片段中看到其定义:

namespace UnityEngine
{
    //
    // Summary:
    //     Use ForceMode to specify how to apply a force
           using Rigidbody.AddForce.
    public enum ForceMode
    {
        //
        // Summary:
        //     Add a continuous force to the rigidbody,
               using its mass.
        Force = 0,
        //
        // Summary:
        //     Add an instant force impulse to the
               rigidbody, using its mass.
        Impulse = 1,
        //
        // Summary:
        //     Add an instant velocity change to the
               rigidbody, ignoring its mass.
        VelocityChange = 2,
        //
        // Summary:
        //     Add a continuous acceleration to the
               rigidbody, ignoring its mass.
        Acceleration = 5
    }
}

如前面的代码片段所示,有四种类型的力模式,即ForceImpulseVelocityChangeAcceleration

是默认模式,在此模式下,需要施加更大的力来推动或扭曲质量较大的物体,因为这与 Rigidbody 的质量有关。它将为 Rigidbody 添加持续的力量。

如果我们将Impulse模式作为参数,那么AddForce方法将向 Rigidbody 应用一个瞬时的力脉冲。此模式适用于模拟爆炸或碰撞产生的力。与Force模式一样,Impulse模式也取决于 Rigidbody 的质量。

VelocityChange是这里的第三种模式。如果我们选择此模式,则 Unity 将使用单个函数调用立即应用速度变化。需要注意的是,VelocityChange模式与冲量模式和模式不同。VelocityChange模式不依赖于 Rigidbody 的质量,这意味着VelocityChange将以相同的方式改变每个 Rigidbody 的速度。

最后的模式是加速度模式。如果选择此模式,则 Unity 将为 Rigidbody 添加持续加速度。与速度变化模式一样,加速度模式也忽略 Rigidbody 的质量,这意味着AddForce将以相同的方式移动每个 Rigidbody。

到目前为止,我们已经学习了AddForce方法可用的不同力模式。接下来,让我们创建一个新的 C#脚本,并通过调用AddForce对立方体施加力:

using UnityEngine;
public class RigidbodyMethods : MonoBehaviour
{
[SerializeField] 
private Rigidbody _rigidbody;
[SerializeField] 
private float _thrust = 50f;
    private void Start()
    {
        _rigidbody = GetComponent<Rigidbody>();
    }
    private void FixedUpdate()
    {
        if (Input.GetKey(KeyCode.F))
        {
            _rigidbody.AddForce(transform.forward *
              _thrust);
        }
        if (Input.GetKey(KeyCode.A))
        {
            _rigidbody.AddForce(transform.forward *
              _thrust, ForceMode.Acceleration);
        }
    }
}

如代码所示,我们可以通过按键盘上的F键对 Rigidbody 施加持续力,并通过按键盘上的A键对 Rigidbody 施加持续加速度。

MovePosition

有时候,我们只想移动我们的 GameObject,而不想处理力。Rigidbody 的MovePosition方法可以帮助我们实现这个目标。

MovePosition函数的签名如下:

public void MovePosition(Vector3 position);

在这里,我们需要一个参数位置来提供 Rigidbody 对象移动到的新位置。为了使 Rigidbody 平滑移动,我们通常使用插值来实现帧之间的平滑过渡。由于MovePosition仍然是 Rigidbody 的方法,我们仍然在FixedUpdate函数中调用它,如下面的代码片段所示:

using UnityEngine;
public class RigidbodyMethods : MonoBehaviour
{
[SerializeField] 
private Rigidbody _rigidbody;
[SerializeField] 
private float _speed = 50f;
    private void Start()
    {
        _rigidbody = GetComponent<Rigidbody>();
    }
    private void FixedUpdate()
    {
        var direction = new
          Vector3(Input.GetAxis("Horizontal"), 0,
          Input.GetAxis("Vertical"));
        _rigidbody.MovePosition(transform.position +
          direction * Time.deltaTime * _speed);
    }
}

在这里,我们获取用户输入作为移动方向,并将移动应用到当前位置。您还可以看到,移动向量乘以deltaTimespeed,这是为了实现平滑移动。

在阅读本节之后,我们学习了如何通过 C#脚本与物理系统交互。但最好是我们自己使用物理系统实现一个简单的游戏,这就是我们在下一节要做的事情!让我们继续。

基于物理系统创建一个简单的游戏

我们已经学习了 Unity 物理系统的概念,并讨论了如何使用 C#代码与物理系统交互。接下来,我们将使用我们所学到的知识在 Unity 中创建一个简单的基于物理的乒乓球游戏。

首先,让我们执行以下步骤来创建一个平面对象作为乒乓球桌:

  1. 右键单击Hierarchy窗口打开菜单。

  2. 选择3D Object > Plane在编辑器中创建一个新的平面对象。

图 5.18 – 创建平面对象

图 5.18 – 创建平面对象

  1. 重命名Table

  2. 选择2,我们可以看到默认情况下已经为这个对象添加了一个 Mesh Collider。

图 5.19 – “Table”的检查器窗口

图 5.19 – “Table”的检查器窗口

  1. 通过选择3D Object > Cube创建四个立方体对象作为桌子上的墙壁,这个过程与创建平面对象的过程类似。默认情况下,每个立方体对象都添加了一个 Box Collider。

  2. 我们可以通过使用编辑器中的工具轻松调整这四个立方体对象的位置、大小和旋转,以创建桌子上的墙壁。

图 5.20 – 在桌子上创建墙壁

图 5.20 – 在桌子上创建墙壁

为了让桌子看起来不那么无聊,我们可以将不同的材质应用到墙壁和桌子上。现在我们已经设置了乒乓球桌,如下面的图片所示:

图 5.21 – 乒乓球桌

图 5.21 – 乒乓球桌

接下来,我们需要创建两个玩家,即Player1Player2。为了保持简单,我们仍然使用两个立方体对象作为玩家:

  1. 选择3D Object > Cube在场景中创建一个新的立方体对象。

  2. 将立方体对象重命名为Player1

  3. 调整3的位置和大小。

图 5.22 – Player1 的检查器窗口

图 5.22 – Player1 的检查器窗口

  1. 让我们重复前面的步骤来创建另一个玩家。

  2. 我们可以使用不同的颜色来识别Player1Player2,以便区分它们,如下面的图所示:

图 5.23 – Player1 和 Player2

图 5.23 – Player1 和 Player2

现在我们已经在我们的简单游戏中有了Player对象。接下来,我们将向我们的游戏中添加一个乒乓球:

  1. 选择3D Object > Sphere在场景中创建一个新的Sphere对象。

  2. Ball重命名。

  3. 选择Ball打开其检查器窗口。我们可以看到,默认情况下已经为球添加了一个 Sphere Collider。

图 5.24 – 球形 Collider 组件

图 5.24 – 球形 Collider 组件

  1. 然后,我们需要通过点击Add Component按钮并选择Physics > Rigidbody来向这个球添加一个Rigidbody组件。

图 5.25 – 添加 Rigidbody 组件

图 5.25 – 添加 Rigidbody 组件

  1. 然后,我们将这个Rigidbody组件的Interpolate选项从None更改为Interpolate,以便根据前一帧的变换使变换平滑。

图 5.26 – 更改 Interpolate 选项

图 5.26 – 更改 Interpolate 选项

  1. 然后,我们将这个Rigidbody组件的Collision Detection选项从Discrete更改为Continuous Dynamic,以便正确处理快速移动的乒乓球。

图 5.27 – 更改 Collision Detection 选项

图 5.27 – 更改 Collision Detection 选项

  1. 由于现实世界的乒乓球在撞击障碍物时会弹回,为了模拟这种弹跳效果,我们需要通过在项目窗口中点击 创建 > 物理材质 来创建一个物理材质。

![图 5.28 – 创建物理材质图片

图 5.28 – 创建物理材质

  1. 现在让我们选择新创建的物理材质,打开检查器窗口,将 0.4 更改为 0,将 0 更改为 1。同时,设置 MultiplyMaximum,如下截图所示:

![图 5.29 – 物理材质设置图片

图 5.29 – 物理材质设置

  1. 然后,将此物理材质分配给球体碰撞器的 材质 选项。

![图 5.30 – 将物理材质分配给球体碰撞器图片

图 5.30 – 将物理材质分配给球体碰撞器

现在我们已经设置了将在我们的游戏中使用的乒乓球。接下来,让我们创建一个新的 C# 脚本来对球施加力以移动它:

using UnityEngine;
public class PingPongBall : MonoBehaviour
{
    [SerializeField] private Rigidbody _rigidbody;
    [SerializeField] private Vector3 _initialImpulse;
    private void Start()
    {
        _rigidbody.AddForce(_initialImpulse,
          ForceMode.Impulse);
    }
}

在这个脚本中,我们使用之前学到的 AddForce 方法以及 Impulse 力模式来对球施加脉冲力。力的方向和大小由 _initialImpulse 变量提供。这可以在编辑器中设置。

现在让我们将这个脚本附加到球上,并为 _initialImpulse 变量提供一个值。

![图 5.31 – 乒乓球(脚本)图片

图 5.31 – 乒乓球(脚本)

如前一个截图所示,_initialImpulse 变量的值为 (8, 0, 8),这意味着我们向 Rigidbody 添加了一个指向桌面右下角的即时力脉冲。

让我们玩游戏并看看会发生什么。

![图 5.32 – 球被弹起图片

图 5.32 – 球被弹起

从图片中,我们可以看到游戏中的乒乓球撞击墙壁并弹起。接下来,我们将向玩家对象添加更多逻辑,以便我们可以在游戏中控制它们。

然而,在我们开始为玩家对象编写 C# 代码之前,我们应该首先为它们中的每一个添加一个 Rigidbody 组件,并将 Rigidbody 组件设置调整为以下截图所示:

![图 5.33 – 玩家 Rigidbody 组件设置图片

图 5.33 – 玩家 Rigidbody 组件设置

如您从截图中所见,我们首先将质量设置为 1000 并通过取消选中 使用重力 选项来禁用重力效果。

然后,请注意,我们已经限制了 Rigidbody 的运动。由于玩家对象将仅沿 x 轴移动而不会旋转,我们只保留 Rigidbody 沿 x 轴的运动而不加约束。

接下来,我们还需要配置这两个不同玩家的控制,如下步骤所示:

  1. 通过在编辑器中选择 Edit > Project Settings 来打开 项目设置 窗口。

图 5.34 – 打开项目设置窗口

图 5.34 – 打开项目设置窗口

  1. 从左侧的导航中选择 输入管理器 以打开 输入管理器 窗口。

图 5.35 – 打开输入管理器窗口

图 5.35 – 打开输入管理器窗口

  1. 我们将在本窗口中定义玩家 1 和玩家 2 的输入轴和相关动作,以便我们可以使用上箭头键和下箭头键以及 w 键和 s 键来分别控制这两个玩家对象的移动,如下面的截图所示:

图 5.36 – 设置玩家的输入控制

图 5.36 – 设置玩家的输入控制

到目前为止,我们已经设置了玩家对象所需的 Rigidbody 组件和输入控制,然后我们可以编写一个 C# 脚本来控制我们游戏中的玩家对象。

记得我们之前介绍的 MovePosition 方法吗?这里,我们将使用这个方法来移动玩家对象:

using UnityEngine;
public class Player : MonoBehaviour
{
[SerializeField] 
private Rigidbody _rigidbody;
[SerializeField] 
private float _speed = 10f;
[SerializeField] 
private bool _isPlayerOne;
    private void Start()
    {
        _rigidbody = GetComponent<Rigidbody>();
    }
    private void FixedUpdate()
    {
        var inputAxis = _isPlayerOne ? "PlayerOneMove" :
          "PlayerTwoMove";
        var direction = new
          Vector3(Input.GetAxis(inputAxis), 0, 0);
        _rigidbody.MovePosition(transform.position +
          direction * Time.deltaTime * _speed);
    }
}

如前述代码所示,此脚本将首先确定对象是哪个玩家,获取相应的输入设置,然后根据玩家的输入确定对象的移动方向。

现在,让我们将此脚本附加到这两个玩家对象上并开始游戏!

图 5.37 – 乒乓球游戏

图 5.37 – 乒乓球游戏

如前述图像所示,我们现在可以使用 ws 键以及 键来控制玩家 1 和玩家 2 的移动,并且正如预期的那样,当乒乓球击中玩家时,它会弹跳。

在本节中,我们制作了一个简单的基于物理的游戏,现在我们将介绍如何在 Unity 中开发游戏时优化物理系统的性能。

提高物理系统的性能

物理模拟需要大量的计算,尤其是在高物理精度要求的情况下。因此,正确理解如何使用 Unity 的物理系统并减少不必要的计算开销非常重要。

Unity 性能分析器

首先,我们应该学习如何使用工具来查看和定位由 Unity 中的物理系统引起的性能瓶颈。

Unity 编辑器中的 Profiler 工具是我们推荐的工具,它允许我们轻松查看各种性能数据并定位与物理系统相关的性能问题。

以我们刚刚制作的乒乓球游戏为例,我们可以执行以下步骤来查看此游戏的性能数据:

  1. 通过点击 Play 按钮在编辑器中启动游戏。

图 5.38 – 在编辑器中玩游戏

图 5.38 – 在编辑器中玩游戏

  1. 点击 Window > Analysis > Profiler 或使用键盘快捷键 Ctrl + 7 (Command + 7 在 macOS 上) 打开 性能分析器 窗口。

  2. Profiler 窗口中点击 CPU 使用率 模块区域以查看 CPU 负载的性能数据,例如 FixedUpdate.PhysicsFixedUpdate 消耗的 CPU 时间,如下所示:

图 5.39 – Unity Profiler

图 5.39 – Unity Profiler

除了 CPU 使用率 模块外,我们还可以查看物理系统的详细信息,例如在特定时刻的刚体数量和接触数量,如下面的截图所示:

图 5.40 – 分析器中的物理数据

图 5.40 – 分析器中的物理数据

接下来,我们将介绍一些提高物理系统性能的技巧。

增加固定时间步长

减少物理计算成本的一个想法是减少物理系统每秒的更新次数。我们可以执行以下步骤来增加这个 固定时间步长 设置以实现这一目标:

  1. 通过在编辑器中选择 编辑 > 项目设置 来打开 项目设置 窗口。

  2. 从左侧的导航中选择 时间 以打开 时间 窗口。

图 5.41 – 时间设置

图 5.41 – 时间设置

  1. 固定时间步长 的默认值是 0.02,这意味着物理系统每秒将更新 50 次。为了减少每秒的更新次数,我们可以增加这个值。

减少不必要的基于层的碰撞检测

Unity 默认使用一种相当低效的物理碰撞检测模式;也就是说,碰撞检测是在所有 GameObject 上进行的。我们可以通过修改 Unity 的 物理设置 中的 层碰撞矩阵 字段,并为不同的 GameObject 设置不同的层来减少碰撞检测的数量。以下步骤演示了如何修改它:

  1. 通过在编辑器中选择 编辑 > 项目设置 来打开 项目设置 窗口。

  2. 从左侧的导航中选择 物理 以打开 物理 窗口。

  3. 您可以在 物理 窗口的底部找到 层碰撞矩阵,您可以在 图 5.42 中看到默认情况下一切都是相互碰撞的。我们应该只启用需要碰撞检测的层。

图 5.42 – 层碰撞矩阵

图 5.42 – 层碰撞矩阵

在本节中,我们介绍了如何使用 Unity 的 Profiler 工具查看物理系统的性能数据,并探讨了如何优化物理系统的性能。

摘要

在本章中,我们首先介绍了 Unity 提供的物理解决方案,包括两个内置的物理解决方案,Nvidia PhysX 引擎Box2D 引擎,Unity 还提供了物理引擎包,即Unity Physics 包Havok Physics for Unity 包。然后,我们探讨了 Unity 物理系统中的一些最重要的概念,例如Collider 组件Rigidbody 组件Triggers。我们还讨论了如何在 Unity 中创建一个新的脚本以与 Unity 的物理系统交互。

然后,我们演示了如何在 Unity 中实现基于物理的乒乓球游戏。

最后,我们探讨了在 Unity 中应用物理模拟的一些最佳实践,以优化由物理系统引起的性能问题。

在下一章中,我们将讨论如何在 Unity 中实现视频和音频功能。

第六章:第六章:在 Unity 项目中集成音频和视频

在前面的章节中,我们已经讨论了如何使用 C#脚本在 Unity 中开发游戏逻辑,如何高效实现 UI,如何实现动画,以及如何将物理模拟集成到您的游戏中。然而,在游戏开发中,声音这一特性常常被忽视。正确使用音效可以增强游戏的沉浸感,与游戏背景相匹配的背景音乐可以触发玩家的情感共鸣。有时,在游戏中播放视频也是增加游戏乐趣的一种方式。毫无疑问,将视频和音频添加到您的游戏中可以使您的游戏更加生动有趣。

在本章中,我们将介绍以下关键主题:

  • Unity 音频系统和视频系统中的概念

  • 使用音频和视频进行脚本编写

  • 使用 Unity 开发 Web 应用程序时需要注意的事项

  • 提高音频系统的性能

到本章结束时,您将能够正确且高效地在 Unity 中实现音频和视频,为您的游戏增添更多真实感和乐趣。

现在,让我们开始吧!

技术要求

您可以在以下 GitHub 仓库中找到完整的代码示例:github.com/PacktPublishing/Game-Development-with-Unity-for-.NET-Developers

Unity 音频系统和视频系统中的概念

Unity 提供了视频和音频功能,允许您的游戏在不同的平台上播放视频,并支持实时混合和全 3D 空间音效。在本节中,我们将介绍 Unity 音频系统和视频系统的重要概念。

音频剪辑

为了能够在 Unity 中播放音频,我们首先需要将音频文件导入 Unity 编辑器。音频数据将被保存在 Unity 中的一个音频剪辑对象中。您可以从以下链接下载并导入超科幻游戏音频武器包 Vol. 1assetstore.unity.com/packages/audio/sound-fx/weapons/ultra-sci-fi-game-audio-weapons-pack-vol-1-113047。您可以在以下屏幕截图中看到这一点:

图 6.1 – 超科幻游戏音频武器包 Vol. 1

图 6.1 – 超科幻游戏音频武器包 Vol. 1

本包中包含的音频文件格式为.wav。除了可以导入 Unity 的.wav文件外,Unity 还支持导入以下格式的文件:

  • .aif

  • .mp3

  • .ogg

  • .xm

  • .mod

  • .it

  • .s3m

在导入这些音频文件后,我们可以选择其中一个打开导入设置,如图图 6.2所示:

图 6.2 – 音频导入设置

图 6.2 – 音频导入设置

如您在导入设置中看到的,Unity 支持单声道和多声道音频资源,最多可达八个声道。Unity 还提供了许多导入选项。让我们介绍一些重要的选项。

加载类型

Unity 为游戏开发者提供了三种不同的方式在运行时加载音频资源。我们可以通过修改导入设置窗口中的加载类型属性来决定 Unity 如何加载这个音频文件。

![图 6.3 – 加载类型]

![img/Figure_6.03_B17146.jpg]

图 6.3 – 加载类型

三种方法如下:

  • 加载时解压缩:这是加载类型的默认值。如果音频文件较小,例如 UI 声音或脚步声,我们应该选择此选项。这是因为,在这种情况下,音频文件将以原始大小解压缩并解码到内存中。优点是它将以最小的 CPU 使用率准备好按需播放。

  • 内存中压缩:与加载时解压缩形成对比,通过选择此方法,Unity 将压缩的音频数据存储在内存中,并在播放音频时需要 CPU 进行解压缩和解码。

  • 流式传输:这与前两种方法完全不同。如果我们选择这种方法,Unity 不会将音频数据加载到内存中,而是从磁盘流式传输。这种方法使用的内存最少,但代价是 CPU 和磁盘使用率最高。

压缩格式

除了刚刚介绍的加载类型属性外,压缩格式属性对于音频资源也非常重要。Unity 支持多种音频压缩格式,根据不同的目标平台有不同的格式可供选择。例如,如果目标平台是Windows,以下格式可用:

![图 6.4 – Windows 上的压缩格式]

![img/Figure_6.04_B17146.jpg]

图 6.4 – Windows 上的压缩格式

另一方面,如果目标平台是Android,除了之前的格式外,它还支持 MP3 格式。

![图 6.5 – Android 上的压缩格式]

![img/Figure_6.05_B17146.jpg]

图 6.5 – Android 上的压缩格式

我们将在这里探讨不同的压缩格式:

  • PCM脉冲编码调制PCM)是一种无损、未压缩的格式,是计算机中数字音频的标准形式。它提供高质量,但文件大小非常大。如图 6.6所示,当选择PCM格式时,此音频文件的导入大小等于其原始大小。

![图 6.6 – PCM 格式]

![img/Figure_6.06_B17146.jpg]

图 6.6 – PCM 格式

  • Vorbis: 这是压缩格式的默认值。Vorbis是一种非常有效的音频压缩格式。与PCM音频相比,这种压缩产生的文件更小,但质量较低。如果我们选择Vorbis选项,导入的音频文件大小将远小于其原始大小。有一个质量滑块,允许我们调整压缩质量。

图 6.7 – Vorbis 格式

图 6.7 – Vorbis 格式

  • ADPCMADPCM代表自适应差分脉冲编码调制。虽然名称与 PCM 相似,但它是一种有损压缩格式。但与 Vorbis 不同,在 Unity 中无法调整其压缩比。压缩文件大小将始终比 PCM 小 3.5 倍。

图 6.8 – ADPCM 格式

图 6.8 – ADPCM 格式

  • MP3:这在移动平台上可用,例如 Android。MP3 格式与 Vorbis 类似,是一种非常有效的音频压缩格式。还有一个质量滑块,允许我们调整压缩质量。

图 6.9 – MP3 格式

图 6.9 – MP3 格式

在我们设置这些音频文件的导入设置后,它们可以作为音频剪辑导入到 Unity 编辑器中。

图 6.10 – 音频剪辑

图 6.10 – 音频剪辑

图 6.10所示,我们可以在项目窗口中找到这些音频剪辑,音频剪辑的图标将显示其波形。

音频源

为了在游戏场景中播放我们刚刚创建的音频剪辑,我们还需要设置一个音频源。然后这个音频剪辑可以被拖到音频源或从 C#脚本中使用。

让我们按照以下步骤首先创建一个音频源:

  1. 层次结构窗口中右键单击以打开菜单。

  2. 选择创建空对象以在场景中创建一个新的 GameObject。提醒一下,作为音频源的 GameObject 不一定是静态对象。在许多情况下,音频源需要移动,例如模拟游戏中发射炮弹的效果。但为了简单起见,我们不会在此为该 GameObject 添加移动逻辑。

图 6.11 – 创建音频源对象

图 6.11 – 创建音频源对象

  1. 选择这个新创建的 GameObject,并单击添加组件按钮以打开组件列表。

  2. 选择音频 | 音频源以向此 GameObject 添加音频源组件。

图 6.12 – 添加音频源组件

图 6.12 – 添加音频源组件

现在我们已经在游戏场景中创建了一个新的音频源组件。这个音频源组件的属性如图 6.13所示。

图 6.13 – 音频源的属性

图 6.13 – 音频源的属性

我们将在这里探讨其中的一些:

  • AudioClip: 在这里,我们发现音频源的第一个属性是对音频剪辑的引用。我们可以在编辑器中直接将音频剪辑资产拖拽到这个字段。

  • Output: 我们不需要设置这个属性,因为默认情况下,这个音频源的输出会被场景中的音频监听器拾取。只有当你想要将声音输出到音频混音器组时,才需要设置这个属性。

  • C#脚本中的Play方法。

除了音频源之外,为了在场景中发出声音,还需要一个音频监听器来接收来自源的声音。接下来,我们将讨论音频监听器

音频监听器

通常来说,你不需要担心场景中缺少音频监听器,因为当创建场景时,默认情况下,音频监听器会附加到场景中的主相机上,如图所示。

图 6.14 – 音频监听器

图 6.14 – 音频监听器

在现实生活中,声音是由听者听到的,音频监听器是 Unity 中听者的表示。如果你在游戏场景中正确设置了音频源,音频剪辑可用,但在运行游戏时你听不到声音,那么你可以首先检查场景中是否有音频监听器。通常,监听器是附加到相机上的。

要听到声音,我们需要确保有一个音频监听器可用,但同时也应注意的是,场景中不能有超过一个音频监听器,否则你将在控制台窗口中看到以下警告信息。所以,请确保场景中始终只有一个音频监听器。

图 6.15 – 请确保场景中始终只有一个音频监听器

图 6.15 – 请确保场景中始终只有一个音频监听器

在介绍了 Unity 中关于音频的一些重要概念之后,接下来让我们讨论与 Unity 中视频相关的概念。

视频剪辑

与音频剪辑类似,我们还需要将外部视频文件导入到 Unity 编辑器中,以生成视频剪辑。Unity 支持视频文件的典型文件扩展名,例如以下这些:

  • .mp4

  • .mov

  • .webm

  • .wmv

导入视频文件后,我们可以选择打开导入设置,如图 6.16 所示:

图 6.16 – 视频剪辑的导入设置

图 6.16 – 视频剪辑的导入设置

默认情况下,转码选项是禁用的,这意味着 Unity 将使用默认设置导入这个视频文件。如果我们启用这个选项,Unity 将允许我们修改这些设置,如图 6.17 所示,我们将会介绍其中的一些。在导入设置窗口的底部,我们还可以通过点击播放按钮直接预览视频。

现在,让我们检查并启用转码选项,并探索一些这些导入设置。

图 6.17 – 视频导入设置

图 6.17 – 视频导入设置

  • 尺寸:默认情况下,Unity 不会调整原始视频的大小,但如果你想在 Unity 中调整视频文件的大小,你可以更改尺寸选项。你将找到一个预设列表,例如半分辨率,你也可以自定义新的大小。

![图 6.18 – 尺寸选项图片

图 6.18 – 尺寸选项

  • 编解码器:Unity 提供了将视频剪辑资产转码为以下视频编解码器之一的选项:H264H265VP8,如以下图所示。自动编解码器的默认值。当然,你也可以自己选择视频编解码器。H264是最佳的本机支持的硬件加速视频编解码器。

![图 6.19 – 编解码器选项图片

图 6.19 – 编解码器选项

  • 保持透明度:如你在图 6.19中看到的,在这种情况下保持透明度不是一个选项。这是因为此选项只能在视频文件包含透明通道时勾选。如果你的视频文件包含透明通道,并且你希望在游戏中播放视频时保持透明通道,则勾选此选项。

  • 水平翻转:正如其名所示,如果此选项被启用,Unity 将水平翻转视频,将左侧切换到右侧。

  • 垂直翻转:与水平翻转类似,如果此选项被启用,Unity 将垂直翻转视频,使其上下颠倒。

  • 导入音频:如果你的原始视频文件包含音频轨道,那么你可以通过勾选此选项来决定是否导入视频的音频轨道。

设置导入设置后,我们可以点击应用来转码视频。转码过程可能需要一些时间。

![图 6.20 – 转码视频图片

图 6.20 – 转码视频

现在我们已经将视频文件导入到 Unity 编辑器中,接下来我们需要设置一个视频播放器来播放视频剪辑。

视频播放器

按照以下步骤创建一个视频播放器:

  1. 层次窗口中右键单击以打开菜单。

  2. 选择 VideoPlayer

  3. 选择这个新创建的 GameObject,并点击添加组件按钮以打开组件列表。

  4. 选择视频 | 视频播放器以向此 GameObject 添加视频播放器组件。

![图 6.21 – 添加视频播放器组件图片

图 6.21 – 添加视频播放器组件

现在我们已经在游戏场景中创建了一个新的视频播放器。此视频播放器的属性如图 6.22所示:

![图 6.22 – 视频播放器组件属性图片

图 6.22 – 视频播放器组件属性

接下来,我们将探索一些这些属性:

  • :在 Unity 中,视频播放器可以从视频剪辑资产或从 URL 播放视频。默认情况下,视频播放器需要一个视频剪辑资产作为视频源,但在这里我们也可以选择将 URL 作为视频源。

图 6.23 – 选择视频源类型

图 6.23 – 选择视频源类型

图 6.23 – 选择视频源类型

  • 在 C#脚本中的Play方法来触发在运行时其他点的视频播放。

  • 回放速度:我们可以通过调整这个滑块来增加或减少回放速度。默认值是 1。

渲染模式

这是一个非常重要的设置,因此我们将详细解释它。如果您刚刚设置了视频播放器,将视频剪辑资产拖动到属性,并播放游戏,您会发现没有任何事情发生。这是因为视频播放器中渲染模式的默认值是渲染纹理,这意味着您应该首先创建并分配一个渲染纹理到视频播放器的目标纹理属性。然后视频播放器将输出视频到这个渲染纹理,正如您在图 6.24中可以看到的:

图 6.24 – 设置目标纹理属性

图 6.24 – 图 6.24_B17146.jpg

图 6.24 – 设置目标纹理属性

然而,在这个阶段,我们只将视频渲染到渲染纹理中,视频没有在游戏场景中播放。为了在游戏场景中播放此视频,我们可以在场景中创建一个新的原始图像UI 元素,并将此渲染纹理分配给原始图像UI 元素。

图 6.25 – 原始图像 UI 元素

图 6.25 – 图 6.25_B17146.jpg

图 6.25 – 原始图像 UI 元素

现在,让我们再次播放游戏,视频按预期播放。

图 6.26 – 播放视频

图 6.26 – 图 6.26_B17146.jpg

图 6.26 – 播放视频

我们还可以更改渲染模式。正如您在图 6.27中可以看到的,其他选项包括以下内容:

  • 相机远平面,在相机场景后渲染视频内容,允许开发者更改 alpha 通道的值以使视频内容透明,可以用作背景视频播放器。

  • 相机近平面,在相机场景前渲染视频内容,允许开发者更改 alpha 通道的值以使视频内容透明,可以用作前景视频播放器。

  • 材质覆盖:在 Unity 中,材质用于描述模型的表面外观。如果选择此模式,视频内容将通过目标材质的用户指定属性传递,而不是在屏幕或渲染纹理上绘制。此模式通常用于在 Unity 中制作 360 度全景视频。

  • 仅 API,它不会渲染视频内容,但允许开发者通过 API 访问视频内容。

图 6.27 – 渲染模式列表

图 6.27 – 图 6.27_B17146.jpg

图 6.27 – 渲染模式列表

例如,我们将选择相机远平面作为渲染模式。在这里,我们不需要提供渲染纹理,而是需要一个相机,正如您在下面的图中可以看到的,它允许我们修改Alpha值。

图 6.28 – 相机远平面

图 6.28 – 图 6.28_B17146.jpg

图 6.28 – 相机远平面

如果我们玩游戏,这次视频会再次播放。

![Figure 6.29 – 播放视频]

![img/Figure_6.29_B17146.jpg]

![Figure 6.29 – 播放视频]

在本节中,我们学习了 Unity 音频和视频系统的一些概念。现在,让我们探索如何在 Unity 中编写 C# 代码来控制音频和视频。

音频和视频脚本化

在本节中,我们将探讨如何通过 C# 脚本与音频和视频系统进行交互。与上一节类似,我们也将分别介绍 Audio SourceVideo Player 的 C# 方法。我们首先从 Audio Source 的 C# 方法开始。

AudioSource.Play

我们将要介绍的第一个函数是 AudioSourcePlay 函数。Play 函数的签名如下:

public void Play();

调用此函数播放音频剪辑非常简单直接。然而,如果您需要处理更复杂的场景,例如延迟播放音频剪辑,您可以调用 PlayDelayed 函数,该函数将以指定的秒数延迟播放剪辑。

注意

Play 函数有一个重载版本,它需要一个 delay 参数。然而,现在它已被弃用。建议开发者使用 PlayDelayed 函数而不是旧的 Play (delay) 函数。

以下为 PlayDelayed 函数的函数签名:

public void PlayDelayed(float delay);

它需要一个参数,delay,该参数以相对于 44.1 kHz 参考速率的样本数指定。

现在,让我们创建一个新的 C# 脚本,首先获取场景中音频源的引用,并通过调用 Play 函数播放分配给它的音频剪辑:

using UnityEngine;
public class AudioPlayer : MonoBehaviour
{
[SerializeField] 
private AudioSource _audioSource;
    private void Start()
    {
        if(_audioSource == null)
        {
            _audioSource = GetComponent<AudioSource>();
        }
    }
    public void OnClickPlayAudioButton()
    {
        _audioSource.Play();
    }
}

然后,我们将这个新创建的脚本拖放到场景中的 Audio Source GameObject 上,将其附加到 GameObject 作为新的组件。

![Figure 6.30 – 将脚本附加到 GameObject]

![Figure 6.30_B17146.jpg]

![Figure 6.30 – 将脚本附加到 GameObject]

在这里,我们可以手动拖动 GetComponent<AudioSource>() 函数来在代码中获取 AudioSource 组件。

接下来,我们将在场景中创建一个 UI 按钮,并将按钮绑定到 OnClickPlayAudioButton 函数,以便当按钮被点击时,Audio Source 将播放音频剪辑。

![Figure 6.31 – 创建一个按钮]

![img/Figure_6.31_B17146.jpg]

![Figure 6.31 – 创建一个按钮]

现在我们可以运行游戏并点击按钮来播放场景中的音效。此功能在实现音频效果时非常有用;例如,当玩家开枪时,可以播放子弹的声音,等等。

AudioSource.Pause

音频源可以用来播放背景音乐。在某些情况下,我们可能希望背景音乐暂停,例如当玩家进入不同的场景或触发新的剧情时。在这种情况下,我们可以考虑使用 Pause 函数来暂停播放背景音乐剪辑。

Pause 函数的函数签名非常简单,如下所示:

public void Pause();

我们可以为之前创建的 AudioPlayer 类创建另一个函数:

    public void OnClickPauseAudioButton()
    {
        _audioSource.Pause();
    }

由于我们之前下载的资产包只包含短时长的声音效果,为了演示暂停背景音乐的功能,我们可以从 Unity Asset Store 下载并导入Free Music Tracks For Games,链接如下:assetstore.unity.com/packages/audio/music/free-music-tracks-for-games-156413

![Figure 6.32 – 游戏免费音乐轨道图片

Figure 6.32 – 游戏免费音乐轨道

然后替换由AudioSource引用的声音效果剪辑为新背景音乐剪辑。接下来,我们将创建另一个 UI 按钮并将按钮绑定到新创建的OnClickPauseAudioButton函数。

现在,我们可以运行游戏。如果你点击第一个按钮,背景音乐将播放;如果你点击第二个按钮,我们可以暂停音乐。

AudioSource还提供了一个UnPause函数来取消暂停已暂停的播放,以及一个isPlaying属性来检查当前音频剪辑是否正在播放。

以下是UnPause函数的签名:

public void UnPause();

我们可以使用它们来实现更灵活的暂停和继续音乐播放功能,如下代码片段所示:

    public void OnClickPauseAudioButton()
    {
        if(_audioSource.isPlaying)
        {
            _audioSource.Pause();
        }
        else
        {
            _audioSource.UnPause();
        }
    }

这样,我们可以点击第二个按钮来暂停音乐播放,再次点击以继续播放音乐。

AudioSource.Stop

在某些情况下,你可能希望游戏中的背景音乐停止并从头开始播放,而不是暂停并继续播放。AudioSourceStop函数在这里是一个合适的解决方案。

Stop函数的签名也非常简单,如下代码片段所示:

public void Pause();

让我们在 C#脚本中创建另一个函数来停止背景音乐并从头开始播放:

   public void OnClickStopAndPlayAudioButton()
    {
        if(_audioSource.isPlaying)
        {
            _audioSource.Stop();
        }
        else
        {
            _audioSource.Play();
        }
    }

我们还将创建第三个 UI 按钮并将按钮绑定到OnClickStopAndPlayAudioButton函数。

运行游戏并点击此按钮,背景音乐开始播放。再次点击停止背景音乐,如果第三次点击,背景音乐将从开头重新播放。

VideoPlayer.clip

默认情况下,VideoPlayer组件将播放它所引用的视频剪辑。然而,我们通常需要能够在游戏运行时更改视频,而不是创建许多不同的 Video Player 实例。因此,我们可以通过 C#代码直接修改VideoPlayer的剪辑属性:

using UnityEngine;
using UnityEngine.Video;
public class VideoManager : MonoBehaviour
{
[SerializeField] 
private VideoPlayer _videoPlayer;
[SerializeField] 
private VideoClip _videoClip;
    void Start()
    {
        if (_videoPlayer == null)
        {
            _videoPlayer = GetComponent<VideoPlayer>();
        }
    }
    public void OnClickChangeVideoClip()
    {
        _videoPlayer.clip = _videoClip;
    }
}

在此情况下,我们创建一个新的 C#脚本名为VideoManager,该脚本将获取目标VideoPlayer组件的引用以及视频剪辑资产的引用。还有一个名为OnClickChangeVideoClip的函数,该函数将被绑定到 UI 按钮上以更改正在播放的视频剪辑。

与设置音频源相比,设置视频播放器稍微复杂一些,因为我们还需要为Video Player选择一个渲染模式选项。为了简单起见,这里我们选择相机近平面选项,并使用场景中的主相机来渲染视频剪辑的每一帧,如图图 6.33所示。

图 6.33 – 视频播放器

图 6.33 – 视频播放器

然后,我们还需要将新创建的脚本VideoManager分配给同一个 GameObject。

图 6.34 – 视频管理器

图 6.34 – 视频管理器

如您在图 6.34中看到的,我们不仅将Video Player的引用分配给了VideoManager脚本,还将一个视频剪辑资源引用分配给了它。

第三件事是创建一个新的 UI 按钮,并将按钮与之前提到的OnClickChangeVideoClip函数绑定。

图 6.35 – UI 按钮

图 6.35 – UI 按钮

让我们在编辑器中运行游戏并点击按钮来更改视频剪辑。

图 6.36 – 更改视频剪辑

图 6.36 – 更改视频剪辑

图 6.36所示,视频播放器组件的视频剪辑已更改为我们想要播放的视频剪辑。

VideoPlayer.url

有时,从视频剪辑资源播放视频并不是一个好主意。例如,我们不希望因为包含视频文件而增加游戏的大小,或者我们想要开发基于 WebGL 的游戏,而 WebGL 不支持视频剪辑资源。那么,使用 URL 提供视频资源就成为一个明显的解决方案。因此,让我们添加另一个名为OnClickSetVideoURL的功能,以便让游戏场景中的 Video Player 播放 URL 指向的视频:

[SerializeField] private string _videoURL;
…
    public void OnClickSetVideoURL()
    {
        _videoPlayer.url = _videoURL;
    }

我们还需要创建一个新的 UI 按钮,并将按钮与OnClickSetVideoURL函数绑定。

图 6.37 – 设置视频 URL

图 6.37 – 设置视频 URL

运行游戏并点击设置视频 URL按钮来播放 URL 中的视频,如图所示。

注意

Unity 不支持从 YouTube 播放视频,因此您可以将视频资源托管在其他平台上,例如 Azure 云。

VideoPlayer.Play

在前两个示例中,无论我们设置视频剪辑资源还是视频 URL,视频播放器都会自动播放视频。这是因为我们默认启用了唤醒时播放选项,如图图 6.38所示。

图 6.38 – 在唤醒时播放

图 6.38 – 在唤醒时播放

通常,我们更喜欢能够自己控制何时播放视频。因此,禁用此选项并使用脚本中的 C#代码来控制播放是一个好主意,如下面的代码块所示:

    public void OnClickPlay()
    {
        _videoPlayer.Play();
    }

在这里,我们将创建第三个 UI 按钮,并将按钮与OnClickPlay函数绑定。

图 6.39 – 播放视频

图 6.39 – 播放视频

这次,如果我们运行游戏并点击视频播放器播放功能来播放视频,如图图 6.39所示。

VideoPlayer.frame 和 VideoPlayer.frameCount

谈到控制视频播放,视频进度条是一个有用的功能。我们也可以在 Unity 中实现视频进度条。接下来,让我们讨论如何使用视频播放器frameframeCount属性来实现视频进度条。

frameCount属性是只读的,它提供了当前视频内容中的帧数。另一方面,frame属性可以修改,并提供了当前帧的帧索引。因此,我们首先创建一个 UI 滑块,然后根据frameframeCount的值修改视频播放器组件的滑块值,如图图 6.40所示。

图 6.40 – 创建滑块

图 6.40 – 创建滑块

我们还需要修改 C# 脚本来获取滑块的引用并根据frameframeCount的值更新滑块的值:

using UnityEngine;
using UnityEngine.Video;
using UnityEngine.UI;
public class VideoManager : MonoBehaviour
{
    [SerializeField] private VideoPlayer _videoPlayer;
    [SerializeField] private VideoClip _videoClip;
    [SerializeField] private string _videoURL;
    [SerializeField] private Slider _progressBar;
    void Start()
    {
        if (_videoPlayer == null)
        {
            _videoPlayer = GetComponent<VideoPlayer>();
        }
    }
    private void Update()
    {
        _progressBar.value = (float)_videoPlayer.frame /
          (float)_videoPlayer.frameCount;
    }
    public void OnClickChangeVideoClip()
    {
        _videoPlayer.clip = _videoClip;
    }
    public void OnClickSetVideoURL()
    {
        _videoPlayer.url = _videoURL;
    }
    public void OnClickPlay()
    {
        _videoPlayer.Play();
    }
}

在这种情况下,我们使用UnityEngine.UI命名空间,因为我们需要从我们的代码中访问 UI 滑块。并且我们实现了Update函数来更新滑块的值。

让我们再次运行游戏并播放视频。

图 6.41 – 进度条

图 6.41 – 进度条

我们可以看到,随着视频的播放,进度条也会更新。

在本节中,我们探讨了并演示了如何使用 C# 代码来控制音频和视频,例如如何播放音频和视频,暂停音频和视频,以及通过 C# 代码实现进度条。

然而,如果你使用 Unity 开发 Web 应用程序,那么你可能会遇到其他问题。让我们继续探索。

使用 Unity 开发 Web 应用时需要注意的事项

Unity 是一个跨平台的游戏引擎,这意味着我们可以将使用相同代码库和资源的游戏部署到不同的平台,包括 WebGL。然而,如果你使用 Unity 为 Web 平台开发游戏,这里有一些关于实现视频播放器的注意事项。

URL

首先,VideoPlayer.clip属性在 WebGL 上不受支持,这意味着你可以通过在编辑器中播放视频剪辑资产中的视频内容来实现你的视频播放器解决方案。然而,一旦你构建并部署你的 Web 应用程序到服务器并运行它,即使所需的视频资产已打包并部署在一起,视频也不会播放。

图 6.42 – WebGL

图 6.42 – WebGL

图 6.42所示,当我们运行 Web 应用程序并点击播放视频按钮时,什么也不会发生。

在这种情况下,我们必须通过VideoPlayer.url属性提供视频源。如果视频文件已托管在另一个云平台上,则可以直接使用上一节中介绍的方法播放 URL 指向的视频。此外,VideoPlayer.url还支持本地绝对或相对路径。因此,我们也可以构建和部署视频文件和其他游戏内容。需要注意的是,在这种情况下,我们不再使用 Unity 的视频剪辑资产,而是直接使用原始视频文件,并将这些视频文件放在名为StreamingAssets的文件夹中。

注意

StreamingAssets是 Unity 项目的一个特殊文件夹名称。此文件夹中的文件以原始格式可用。

在这里,我们可以在项目的根目录下创建一个新的文件夹,将其重命名为StreamingAssets,然后将原始视频文件放入此文件夹。

![图 6.43 – StreamingAssets 文件夹图 6.43 – B17146.jpg

图 6.43 – StreamingAssets 文件夹

图 6.43所示,视频文件保持其原始格式,并未转换为 Unity 视频剪辑资产。

接下来,让我们创建另一个 C#脚本,以演示如何使视频播放器加载此视频文件并在浏览器中播放:

using System.IO;
using UnityEngine;
using UnityEngine.Video;
public class VideoManagerForWeb : MonoBehaviour
{
    [SerializeField] private VideoPlayer _videoPlayer;
    [SerializeField] private string _videoFileName;
    void Start()
    {
        if (_videoPlayer == null)
        {
            _videoPlayer = GetComponent<VideoPlayer>();
        }
    }
    public void OnClickSetVideoURL()
    {
        _videoPlayer.url =
          Path.Combine(Application.streamingAssetsPath,
          _videoFileName);
    }
}

在此脚本中,我们通过Application.streamingAssetsPath属性在运行时获取文件夹的路径,并将该路径分配给VideoPlayerurl属性。

现在,我们不是在编辑器中运行游戏,而是构建并部署它作为一个网络应用程序,然后在浏览器中运行。

![图 6.44 – 在浏览器中播放视频图 6.44 – B17146.jpg

图 6.44 – 在浏览器中播放视频

这次视频在浏览器中按预期播放。

帧率

当使用 Unity 开发 WebGL 应用程序时,您还应注意视频的帧率。在 Unity 中,帧率以每秒帧数的形式表示。

让我们打印出我们在编辑器中使用的示例视频的长度、帧数和帧率信息。

![图 6.45 – 帧率图 6.45 – B17146.jpg

图 6.45 – 帧率

如您在此处所见,本视频的帧数为 213 帧,视频长度为 7.1 秒,帧率为 30 FPS。

然而,由于 WebGL 平台底层的实现,即HTML5 <video>的 JavaScript API 没有公开帧率信息,帧率始终假设为 24 FPS,即使视频的实际帧率为 30 FPS。因此,视频的帧/秒始终为 24,这在实现 WebGL 的视频进度条时应该注意。

在本节中,我们讨论了由于 Web 平台的一些限制,使用 Unity 开发视频功能时需要注意的事项。接下来,我们将探讨如何使用 Unity 提供的性能分析工具定位由音频引起的性能问题以及如何解决这些问题。

提高音频系统的性能

在游戏开发中,音频的重要性往往被忽视。有时这也反映在性能优化上。游戏开发者通常在其他性能领域投入更多精力,例如图形渲染的性能优化。但随着游戏变得越来越复杂,音频也可能导致性能问题,例如更大的内存使用等。在本节中,我们将探讨如何在 Unity 中优化音频性能。

Unity 分析器

首先,我们应该学习如何使用 Unity 分析器工具来查看和定位由 Unity 中的音频系统引起的性能瓶颈:

  1. 点击窗口 | 分析 | 分析器或使用键盘快捷键Ctrl + 7(在 macOS 上为command + 7)打开分析器窗口。

  2. 点击音频模块区域在分析器窗口中查看音频系统的性能数据。您可以了解正在播放的音频源数量、正在使用的音频剪辑数量以及音频使用的内存量等信息,如图图 6.46所示:

图 6.46 – 音频分析器

图 6.46 – 音频分析器

图 6.46所示,总音频内存的值为 38.9 MB,这非常糟糕,因为目前只有一个是音频源正在播放声音。因此,我们可以点击标记为简单的下拉菜单并切换到详细视图。

图 6.47 – 切换到详细视图

图 6.47 – 切换到详细视图

  1. 我们可以获取有关音频系统的更多信息,并识别占用 38.9 MB 内存的具体音频资产。

图 6.48 – 详细视图

图 6.48 – 详细视图

接下来,我们将介绍如何减少此音频资源占用的内存。

使用强制单声道来节省内存

如果我们检查这个音频资产,我们会发现音频资产是立体声的,如图图 6.49所示。

图 6.49 – 音频剪辑

图 6.49 – 音频剪辑

然而,由于游戏场景中只有一个音频源,这意味着声音从一个点发出,因此在这里使用立体声的效果会丢失,但内存消耗是单声道的两倍。因此,如果游戏不需要立体声且需要减少内存开销,我们只需在音频剪辑的导入设置中启用强制单声道选项,将立体声音频剪辑转换为单声道音频剪辑即可。

图 6.50 – 启用强制单声道

图 6.50 – 启用强制单声道

然后让我们再次播放音频。这次我们发现这个音频剪辑的内存消耗已从 38.9 MB 降至 20.2 MB,几乎减半。

图 6.51 – 内存消耗已降低

图 6.51 – 内存消耗已降低

在本节中,我们介绍了如何使用 Unity 的 Profiler 工具查看音频系统的性能数据,并探讨了如何优化音频系统的性能。

摘要

在本章中,我们首先介绍了 Unity 提供的音频和视频功能,然后探讨了 Unity 音频系统和视频系统中最重要的一些概念,例如音频剪辑资源、音频源组件、音频监听器组件、视频播放器组件等等。我们还讨论了如何在 Unity 中创建一个新的脚本以与 Unity 的音频系统和视频系统交互。

然后,我们演示了如何为 Web 平台实现视频,因为 WebGL 不支持 Unity 的视频剪辑资源,并且由于底层实现原因,视频帧率始终假设为 24 FPS。这些需要注意。

最后,我们探讨了如何查看和定位由 Unity 音频系统引起的性能瓶颈。

在下一章中,我们将介绍 Unity 中计算机图形学的数学原理。

第三部分:Unity 中的高级脚本

在本节中,我们将介绍 Unity 中的高级主题,例如可脚本渲染管线、面向数据的技术堆栈DOTS)以及 Unity 中的序列化。此外,我们还将涵盖如何使用 Microsoft Azure 云进行资产管理以及在云中托管玩家数据。

本部分包括以下章节:

  • 第七章, 在 Unity 中理解计算机图形学的数学原理

  • 第八章, Unity 中的可脚本渲染管线

  • 第九章, 在 Unity 中使用面向数据的技术堆栈

  • 第十章, Unity 和 Azure 中的序列化系统和资产管理

  • 第十一章, 使用 Microsoft Game Dev、Azure Cloud、PlayFab 和 Unity 进行工作

第七章:第七章:理解 Unity 中计算机图形学的数学

数学是游戏开发中经常讨论的一个主题。尽管 Unity 为游戏开发者提供了许多辅助函数来减少在 Unity 中使用数学的复杂性,但仍然需要具备一些关于计算机图形学的基本数学知识,例如坐标系、向量、矩阵和四元数。

在本章中,我们将探讨以下关键主题:

  • 从坐标系开始

  • 处理向量

  • 使用变换矩阵

  • 处理四元数

到本章结束时,你将具备计算机图形学的数学知识,并知道如何在脚本中正确地使用向量、矩阵、四元数和欧拉角。

现在,让我们开始吧!

从坐标系开始

像许多文件一样,大多数模型文件都是二进制文件。当游戏引擎,如 Unity,需要渲染一个模型时,模型的日期,如模型的顶点数组和顶点数组的索引,将通过游戏引擎的渲染管道提取和处理。

注意

你可以在www.khronos.org/opengl/wiki/Rendering_Pipeline_Overview找到更多关于计算机图形学中渲染管道的信息。

图形渲染管道主要包含两个功能:一个是将对象的 3D 坐标转换为屏幕空间中的 2D 坐标,另一个是为屏幕上的每个像素着色。最后,3D 模型将在 2D 屏幕上渲染。

在渲染管道的过程中,将涉及大量的坐标系转换工作,正如你在图 7.1中看到的。因此,这是一个重要的主题,我们将在本节中介绍有关坐标系的信息:

图 7.1 – 坐标变换过程(CC BY 4.0)

图 7.1 – 坐标变换过程(CC BY 4.0)

理解左手坐标系和右手坐标系

坐标系是一种几何系统,通常使用数字来确定空间中某点的位置。

在数学中,有许多不同类型的坐标系,例如数轴坐标系笛卡尔坐标系极坐标系。在计算机图形学中,笛卡尔坐标系是最常用的。

图 7.2 – 坐标系

图 7.2 – 坐标系

笛卡尔坐标系在我们的日常生活中也非常常见,即使用 x 轴、y 轴和 z 轴来描述物体的坐标信息。当用于描述 3D 空间时,笛卡尔坐标系可以是左手坐标系右手坐标系。正如它们的名称所暗示的,我们实际上可以通过使用左手和右手来区分这两种坐标系。

![Figure 7.3 – 坐标系统 (CC BY-SA 3.0)

![img/Figure_7.03_B17146.jpg]

Figure 7.3 – 坐标系统 (CC BY-SA 3.0)

Figure 7.3所示,我们可以通过可视化拇指指向x轴、食指指向y轴和中指指向z轴来区分左手坐标系和右手坐标系。

![Figure 7.4 – Unity 中的左手坐标系

![img/Figure_7.04_B17146.jpg]

Figure 7.4 – Unity 中的左手坐标系

如果我们在 Unity 编辑器中查看,我们可以看到 Unity 使用的是左手坐标系,如图Figure 7.4所示。

本地空间

坐标空间是 3D 位置和变换存在于坐标系中的空间,例如本地空间世界空间。在 Unity 中,我们经常使用本地空间或世界空间。本地空间与父子关系的概念相关,这意味着它使用 GameObject 层次结构中父节点的原点和轴。父 GameObject 的位置、旋转和缩放将影响由其定义的本地空间。因此,这在处理单个 GameObject 的变换时不是很有用,但在处理一组 GameObject 时非常有用。

例如,在Figure 7.5中,这五个立方体对象都是名为LocalSpace的 GameObject 的子对象:

![Figure 7.5 – LocalSpace 父对象

![img/Figure_7.05_B17146.jpg]

Figure 7.5 – LocalSpace 父对象

我们可以看到,0。现在,让我们将此父对象沿y轴向下移动 2 个单位,并围绕y轴旋转 45 度。

![Figure 7.6 – LocalSpace 父对象

![img/Figure_7.06_B17146.jpg]

Figure 7.6 – LocalSpace 父对象

Figure 7.6所示,所有这些立方体都沿y轴向下移动了 2 个单位,并围绕y轴旋转了 45 度。然而,如果我们查看Inspector窗口中单个立方体的位置和旋转,我们可以看到这些值没有改变。这是因为,目前,它们位于由父对象定义的本地空间中,单个立方体相对于其父对象的位置和旋转没有改变。

![Figure 7.7 – 本地空间子对象

![img/Figure_7.07_B17146.jpg]

Figure 7.7 – 本地空间子对象

我们可以通过 C#代码在运行时更改子对象的本地位置、本地旋转和本地缩放,如下所示:

public class LocalSpaceTest : MonoBehaviour
{
    private Vector3 _localPosition = new Vector3(-2, 0, 0);
    private Vector3 _localScale = new Vector3(1, 2, 1);
    private Transform _transform;
    private void Start()
    {
        _transform = gameObject.transform;
        _transform.localPosition = _localPosition;
        _transform.localScale = _localScale;
    }
}

将此脚本附加到名为Cube (1)的子对象上并运行游戏。我们可以在下面的屏幕截图中看到,相对于父对象,子对象沿x轴移动了 2 个单位,沿y轴相对于父对象放大了 2 倍:

![Figure 7.8 – 更改本地位置和本地缩放

![img/Figure_7.08_B17146.jpg]

Figure 7.8 – 更改本地位置和本地缩放

在本节中,我们讨论了本地空间。接下来,我们将探索世界空间。

世界空间

与由父 GameObject 定义的局部空间不同,世界空间是整个场景的坐标系。场景的中心是世界空间的起点。

让我们在场景中创建一个新的 Cube 对象,这次,这个新立方体不是其他 GameObject 的子对象。

![Figure 7.9 – World space![Figure 7.09 – World space 图 7.9 – 世界空间如图 7.9所示,当立方体的位置为0时,立方体位于场景的中心。如果我们将立方体位置中的x值从0改为1,那么立方体将在世界空间的x轴上前进 1 个单位。![Figure 7.10 – Moving in world space![Figure 7.10 – Moving in world space 图 7.10 – 在世界空间中移动我们也可以在 C#脚本中修改 GameObject 在世界空间中的位置、旋转和缩放。以下代码片段展示了如何进行此操作:csusing UnityEngine;public class WorldSpaceTest : MonoBehaviour{    void Start()    {        transform.position = new Vector3(0, 1, 0);    }}````position`属性是变换的世界空间位置。除了直接修改`position`或`rotation`属性外,我们还可以调用以下方法来同时修改对象的`position`和`rotation`属性:cspublic void SetPositionAndRotation(Vector3 position, Quaternion   rotation);我们可以看到,此方法需要一个`Vector3`类型的参数和一个`Quaternion`类型的参数。我们将在*与向量一起工作*和*与四元数一起工作*部分分别介绍向量和四元数。## 屏幕空间如本节开头所述,坐标系可以用来确定空间中的点。这不仅指 3D 空间,也指 2D 空间。屏幕空间是由观众屏幕定义的空间。这意味着屏幕空间将内容投影到屏幕上。在屏幕空间中,坐标是 2D 的;(**0,0**)是左下角,而(screen.width, screen.height)是右上角,如以下截图所示:![Figure 7.10 – Moving in world space![Figure 7.11 – Screen space 图 7.11 – 屏幕空间 2D 元素通常在屏幕空间中描述,最常见的是 UI。屏幕空间的另一个常见用途是获取鼠标输入的位置。原因很明显:鼠标在屏幕上移动。以下代码片段演示了如何在 C#脚本中获取鼠标的位置:csusing UnityEngine;public class ScreenSpaceTest : MonoBehaviour{    void Update()    {        Vector2 mousePosition = Input.mousePosition;        Debug.Log($"Mouse Position: {mousePosition}");    }}````Input`类的`mousePosition`属性将返回屏幕空间中的当前鼠标位置,前面的代码将打印鼠标位置到控制台窗口,如图 7.12所示:![Figure 7.12 – Mouse position![Figure 7.12 – Mouse position 图 7.12 – 鼠标位置在获得鼠标的屏幕空间位置后,我们可以使用 Unity 的`Camera`类提供的方法将屏幕空间位置转换为世界空间位置。此外,Unity 允许我们创建一个从相机通过屏幕点延伸到游戏世界的射线。这可以帮助我们处理游戏中的一种常见情况,即我们需要知道玩家在 3D 游戏世界中点击了什么,尽管玩家实际上只能点击 2D 屏幕。一些方法的签名如下:cspublic Ray ScreenPointToRay(Vector3 pos);public Vector3 ScreenToWorldPoint(Vector3 position);正如我们刚才提到的,`ScreenPointToRay`方法非常有用,因为它从相机返回一个指向世界空间中鼠标位置的`Ray`实例。我希望你仍然记得我们在上一章中介绍的物理系统中的 Collider 组件,因为我们可以使用这个方法向 Collider 发射射线并获取 Collider 的详细信息,并且它还可以用于在 Unity 编辑器的场景视图中绘制线条以帮助调试。接下来,我们将修改之前的代码以实现一个可以检测鼠标点击位置是否有碰撞体,并在有碰撞体的情况下在场景视图中绘制红线的功能:cs    Ray _ray;    private void FixedUpdate()    {      _ray =        Camera.main.ScreenPointToRay(Input.mousePosition);      if (Physics.Raycast(_ray, out RaycastHit hit, 50))      {          Debug.DrawLine(_ray.origin, hit.point,            Color.red);        }    }如代码片段所示,我们调用`ScreenPointToRay`方法从场景中主相机的位置创建一个指向鼠标方向的射线,然后通过调用`Physics.Raycast`使用这个射线检测场景中的碰撞体,最后调用`Debug.DrawLine`在场景视图中绘制一条红线,如下面的截图所示:图 7.13 – 绘制红线

图 7.13 – 绘制红线

图 7.13中,顶部是游戏视图,即游戏运行窗口,底部是场景视图,即调试窗口。

我们在本节中介绍了坐标系。接下来,我们将讨论另一个非常重要的主题:向量。

与向量一起工作

在游戏开发中,我们使用向量来定义方向和位置。如图所示,我们画一条线连接两个点来表示一个向量。在这种情况下,向量从原点开始,即图上的点B (0, 0),到点A (6, 2)

图 7.14 – 2D 位置

图 7.14 – 2D 位置

我们可以看到这个向量由两个分量组成,即xy。它们代表沿x轴和y轴从原点起测量的距离。因此,这个向量可以用来定义点(x*x+y*y)的位置。

在 Unity 中,我们将使用 Vector2 结构来表示 2D 向量和点。Vector2 的长度属性返回这个 2D 向量的长度值。

3D 向量与 2D 向量类似,但我们也需要考虑z轴的值。3D 向量的长度是(x*x+y*y+z*z)的平方根。

Unity 还提供了 Vector3 结构来表示 3D 向量和点。如果你查看场景中 GameObject 的检查器窗口,你会发现在对象的 位置旋转缩放 属性都是 Vector3 类型,如下面的图所示:

图 7.15 – GameObject 的变换

图 7.15 – GameObject 的变换

向量加法

由于向量可以用来描述位置,它们也可以用来描述随时间变化的位置。一个移动的物体有一个速度,这是物体在给定方向上的速度。

图 7.16 – 向量加法

图 7.16 – 向量加法

图 7.16 所示,假设一个物体当前位于点 A,并且它的速度是每分钟 (1, 1),这意味着物体将在 x 轴和 y 轴上各移动 1 个单位。因此,我们将它的当前位置向量与其速度向量相加,以计算 1 分钟后它将到达的位置:

(6, 2) + (1, 1) = (7, 3)

这个物体在 1 分钟后的新位置是 (7, 3)。

如何减去向量

向量减法和向量加法非常相似。我们可以反转第二个向量的方向并使用向量加法。让我们仍然使用之前的例子。假设一个移动的物体当前位于点 A,并且它的速度是每分钟 (-1, -1),这意味着物体将在 x 轴和 y 轴上各移动 -1 个单位。

图 7.17 – 向量减法

图 7.17 – 向量减法

让我们再次将它的当前位置向量与其速度向量相加,以计算 1 分钟后它将到达的位置:

(6, 2) - (1, 1) = (6, 2) + (-1, -1) = (5, 1)

这个物体在 1 分钟后的新位置是 (5, 1)。

在 Unity 中,我们可以使用 C# 代码进行向量加法和向量减法,如下所示:

    private void Start()
    {
        var vector1 = new Vector3(1, 1, 1);
        var vector2 = new Vector3(1, 2, 3);
        var addVector = vector1 + vector2;
        var subVector = vector1 - vector2;
        Debug.Log($"Addition: {addVector}");
        Debug.Log($"Subtraction: {subVector}");
    }

在前面的代码片段中,我们创建了两个 3D 向量 (1, 1, 1)(1, 2, 3)。然后,我们分别对它们进行加法和减法操作,并将结果打印到 控制台 窗口。

图 7.18 – 向量加法和向量减法

图 7.18 – 向量加法和向量减法

为了在 Unity 中移动对象,需要了解向量的知识。但有时我们不必在代码中直接计算向量加法或减法的结果。这是因为 Unity 为我们提供了 Transform.Translate 函数来移动对象。当然,我们仍然需要传递一个向量参数来提供速度:

    private void Update()
    {
        transform.Translate(_speed * Time.deltaTime *
          Vector3.forward);
    }

前面的代码片段演示了如何通过调用 Transform.Translate 函数来移动对象。

点积

除了向量加法和向量减法之外,在游戏开发中常用的 3D 向量操作还包括点积操作和向量积操作。我们将在本节和下一节中分别介绍它们。

首先,我们将探索 Unity 中的点积。点积或标量积接受两个向量并返回一个单一的标量值。

假设有两个 3D 向量,分别命名为vector1vector2;点积的计算过程非常简单,如下所示:

标量值 = (x1 * x2) + (y1 * y1) + (z1 * z2)

在游戏开发中,向量点积操作经常被用来判断这两个向量是否垂直。如果它们的点积操作结果是 0,则两个向量是垂直的。如果结果是正数,则两个向量之间的角度小于 90 度。如果结果是负数,则两个向量之间的角度大于 90 度。

接下来,我们可以在 Unity 编辑器中创建两个 3D 向量来演示如何使用向量点积操作。

图 7.19 – 编辑器中的两个 3D 向量

图 7.19 – 编辑器中的两个 3D 向量

图 7.19所示,绿色线代表第一个向量,它是(0, 5, 0),黄色线代表另一个向量,它是(5, 0, 5)。这两个向量点积操作的结果如下:

0 = (0 * 5) + (5 * 0) + (0 * 5)

同时,我们可以在图 7.19中看到这两个向量是垂直的。

如果第一个向量是(0, 5, 5),这两个向量点积操作的结果如下:

25 = (0 * 5) + (5 * 0) + (5 * 5)

图 7.20所示,这次两个向量不是垂直的,夹角小于 90 度:

图 7.20 – 编辑器中的两个 3D 向量

图 7.20 – 编辑器中的两个 3D 向量

如果第一个向量是(0, 1, -1),这两个向量点积操作的结果如下:

-5 = (0 * 5) + (1 * 0) + (-1 * 5)

图 7.21所示,这次两个向量不是垂直的,夹角大于 90 度:

图 7.21 – 编辑器中的两个 3D 向量

图 7.21 – 编辑器中的两个 3D 向量

Unity 为我们提供了一个函数来计算两个 3D 向量点积的结果,如下所示:

public static float Dot(Vector3 lhs, Vector3 rhs);

这是一个静态函数,我们可以在我们的 C#代码中直接调用它:

public class VectorTest : MonoBehaviour
{
    private Vector3 _vectorA = new Vector3(0, 1, -1);
    private Vector3 _vectorB = new Vector3(5, 0, 5);
    private void Start()
    {
        var result = Vector3.Dot(_vectorA, _vectorB);
        Debug.Log(result);
    }
}

向量积

另一方面,向量积接受两个向量,但返回的是另一个向量,而不是单一的标量值。这个向量垂直于原始的两个向量。

图 7.22 – 向量积(CC BY-SA 4.0)

图 7.22 – 向量积(CC BY-SA 4.0)

与点积相比,叉积的计算过程更为复杂。前图展示了这一过程。

Unity 还提供了一个有用的函数来计算两个 3D 向量的叉积结果,如下所示:

public static Vector3 Cross(Vector3 lhs, Vector3 rhs);

这是一个静态函数,我们可以在我们的 C#代码中直接调用它:

    void FixedUpdate()
    {
        var vector1 = new Vector3(0, 1, 0);
        var vector2 = new Vector3(1, 0, 1);
        Debug.DrawLine(Vector3.zero, vector1, Color.green);
        Debug.DrawLine(Vector3.zero, vector2,
          Color.yellow);
        var resultVector = Vector3.Cross(vector1, vector2);
        Debug.DrawLine(Vector3.zero, resultVector,
          Color.cyan);
    }

在此代码片段中,我们计算了向量 1 和向量 2 的叉积结果,同时,我们还在 Unity 编辑器中绘制了这三个向量,如图所示:

img/Figure_7.23_B17146.jpg

img/Figure_7.23_B17146.jpg

img/Figure_7.23_B17146.jpg

在本节中,我们介绍了向量并探讨了如何在 Unity 脚本中正确使用向量。接下来,让我们继续探讨计算机图形学中的另一个重要概念,即矩阵。

与变换矩阵一起工作

在游戏开发中,变换矩阵也是一个常用术语。具体来说,我们使用变换矩阵来编码变换,包括平移、旋转和缩放变换。

Unity 为我们提供了 C#中的Matrix4x4结构体来表示标准的 4x4 变换矩阵。

![图 7.24 – 一个 4x4 矩阵img/Figure_7.24_B17146.jpg

![图 7.24 – 一个 4x4 矩阵如图 7.24所示,变换矩阵是一个数字网格。尽管这是一个常用术语,但我们很少直接在脚本中使用这个矩阵。这是因为矩阵的计算相对繁琐,而 Unity 作为一个易于使用的游戏引擎,已经为我们封装了复杂的计算在Transform类中,我们只需要调用一些函数。因此,在本节中,我们只简要介绍变换矩阵。在我们开始之前,你应该知道变换包括平移、旋转、缩放,这些操作可以用矩阵表示。我们将在以下小节中逐一讨论它们。## 平移矩阵我们可以使用平移矩阵来移动一个对象。以下图示展示了平移矩阵以及如何通过乘以平移矩阵来移动原始向量:img/Figure_7.25_B17146.jpg

img/Figure_7.25_B17146.jpg

图 7.25 – 平移矩阵(CC BY 4.0)

让我们创建一个 C#脚本,并在 Unity 中直接使用矩阵来演示如何移动一个对象:

using UnityEngine;
public class MatrixTest : MonoBehaviour
{
    void Start()
    {
        var translationMatrix = new Matrix4x4(
            new Vector4(1, 0, 0, 0),
            new Vector4(0, 1, 0, 0),
            new Vector4(0, 0, 1, 0),
            new Vector4(3, 2, 1, 1)
        );
        var newPosition =
          translationMatrix.MultiplyPoint
(transform.position);
        transform.position = newPosition;
    }
}

如此代码片段所示,我们使用了四个Vector4实例来创建一个Matrix4x4结构体的实例。需要注意的是,我们用来创建矩阵的每个Vector4代表矩阵的一列,而不是一行。因此,代码创建了一个如图所示的矩阵:

img/Figure_7.26_B17146.jpg

img/Figure_7.26_B17146.jpg

图 7.26 – 创建矩阵

然后,我们通过调用Matrix4x4MultiplyPoint函数来计算对象的新位置,其中参数是对象的原位置。最后,我们将对象的位置设置为这个新向量。

图 7.27 – 改变对象的位置

图 7.27 – 改变对象的位置

如果我们在原点创建一个对象并运行此脚本,结果将是对象移动到点 (3, 2, 1),如图 图 7.27 所示。

旋转矩阵

同样,矩阵也可以用来旋转一个对象,即旋转矩阵。这次,我们也在 C# 脚本中创建 Matrix4x4 的实例,但不是调用其构造函数,而是调用此函数:

public static Matrix4x4 Rotate(Quaternion q);

Rotate 函数是 Matrix4x4 的静态函数,它创建并返回一个旋转矩阵。此函数需要一个四元数类型的参数。我们将在下一节介绍四元数。

现在,让我们编写一些代码来使用 Matrix4x4 旋转对象:

        var rotation = Quaternion.Euler(0, 90, 0);
        var rotationMatrix = Matrix4x4.Rotate(rotation);
        var newPosition =
          rotationMatrix.MultiplyPoint(transform.position);
        transform.position = newPosition;

此代码将点从其原始位置移动到绕 y 轴旋转 90 度的位置。

图 7.28 – 旋转矩阵

图 7.28 – 旋转矩阵

让我们将此对象的原始位置设置为 (1, 0, 0),然后运行代码。此对象的新位置应该是 (0, 0, -1),如图中所示。

图 7.29 – 实际结果

图 7.29 – 实际结果

运行代码后,我们可以看到实际结果与我们预期的相符。

缩放矩阵

当我们缩放一个向量时,我们将保持其方向不变,并通过我们想要缩放的量来改变其长度。我们还可以使用缩放矩阵将一个点从原点缩放出去。你可以想象一个模型由许多顶点组成。当我们缩放一个模型时,我们实际上是在扩展或收缩构成它的顶点的位置。

Unity 还为我们提供了一个函数,可以直接在 C# 脚本中创建缩放矩阵:

public static Matrix4x4 Scale(Vector3 vector);

Matrix4x4,它创建并返回一个缩放矩阵:

    private void ScalingMatrixTest()
    {
        var scale = new Vector3(3, 2, 1);
        var scalingMatrix = Matrix4x4.Scale(scale);
        var newPosition =
          scalingMatrix.MultiplyPoint(transform.position);
        transform.position = newPosition;
    }

为了演示如何将缩放矩阵应用于一个点,我们创建了前面的代码片段。正如你在代码中所见,我们创建了一个新的 Vector3 来表示缩放因子。然后,我们通过调用 Matrix4x4.Scale 函数创建了一个缩放矩阵,最后,我们将这个矩阵应用于一个点。

让我们在场景中创建一个新的 GameObject,并将此 GameObject 定位到位置 (1, 1, 0)。

图 7.30 – 位置在 (1, 1, 0) 的 GameObject

图 7.30 – 位置在 (1, 1, 0) 的 GameObject

然后,将其脚本附加到它上面并运行脚本。

图 7.31 – 将缩放矩阵应用于此对象

图 7.31 – 将缩放矩阵应用于此对象

图 7.31 所示,此对象的新位置是 (3, 2, 0)。这是因为此缩放矩阵将其原始位置沿 x 轴增加了三倍,沿 y 轴增加了两倍。

图 7.32 – 缩放一个点

图 7.32 – 缩放一个点

如我们在这节开头提到的,在 Unity 开发中,矩阵操作是相对低级的操作。Unity 为我们提供了许多函数来掩盖矩阵的复杂性。开发者通常不直接使用矩阵,但作为一个重要概念,我们仍然需要理解一些与之相关的概念。然而,当涉及到对象旋转时,Unity 通常使用另一种类型来保存旋转数据。如果你对此感兴趣,让我们继续探索四元数

与四元数一起工作

在 Unity 中,变换的旋转以四元数的形式内部存储,它有四个分量,即xyzw。然而,这四个分量并不代表角度或轴,我们开发者通常不需要直接访问它们。你可能感到困惑,因为如果你查看变换的检查器窗口,你会发现旋转以 Vector3 的形式显示。

图 7.33 – 检查器窗口中的旋转属性

图 7.33 – 检查器窗口中的旋转属性

这是因为尽管 Unity 使用四元数来内部存储旋转,除了四元数之外,旋转还可以用xyz三个角度值来表示,即欧拉角

因此,为了方便开发者编辑,Unity 在检查器中显示等效欧拉角的值。

那么,为什么 Unity 不直接使用欧拉角来存储旋转呢?它由三个轴角度组成,并且以人类易于阅读的格式存在。这是因为欧拉角受到陀螺仪锁定的影响,这意味着“自由度”丢失了。

另一方面,使用四元数旋转不会引起陀螺仪锁定问题。因此,Unity 使用四元数来内部存储旋转。但你需要记住的是,四元数的四个分量并不代表角度,所以我们不会单独修改一个分量的值,直接修改四元数是非常复杂的。幸运的是,Unity 在Quaternion结构中为我们提供了许多内置的 C#函数,以便轻松管理四元数旋转。在 Unity 中使用Quaternion结构和其函数来管理旋转值是我们的最佳选择。

我们可以根据其目的将这些函数分为三组,即创建旋转、操作旋转和与欧拉角一起工作。让我们接下来探索它们。

创建旋转

我们将要介绍的第一个函数是LookRotation,该函数的函数签名如下:

public static Quaternion LookRotation(Vector3 forward,
  [DefaultValue("Vector3.up")] Vector3 upwards);

这是一个静态函数;你可以传入参数来指定其前进和向上方向,然后它会根据传入的参数返回正确的旋转值。

在下面的示例中,我们设置了一个场景,其中有两个对象,分别命名为 targetplayer,并创建了一个名为 LookAtScript.cs 的新 C# 脚本。然后,我们将此脚本附加到玩家对象上,如图 图 7.34 所示。蓝色立方体代表玩家,红色球体代表目标对象:

![图 7.34 – 场景中的对象图 7.34 – B17146.jpg

图 7.34 – 场景中的对象

在下面的脚本中,我们演示了如何实现一个函数,使玩家无论目标移动到何处,都能始终面向目标对象:

using UnityEngine;
public class LookAtTest : MonoBehaviour
{
    [SerializeField] private Transform _targetTransform;
    private void Update()
    {
        if (_targetTransform == null) return;
        var dir = _ targetTransform.position –
          transform.position;
        transform.rotation = Quaternion.LookRotation(dir);
    }
}

首先,我们计算了从玩家到目标的方向。然后,我们调用 Quaternion.LookRotation 函数来计算旋转值。

![图 7.35 – 玩家面向目标图 7.35 – B17146.jpg

图 7.35 – 面向目标的玩家

最后,我们将目标对象和玩家移动,使它们面向目标,如图 图 7.35 所示。

操作旋转

有一些函数用于操作旋转,Quaternion.Slerp 就是其中之一。以下是其函数签名:

public static Quaternion Slerp(Quaternion a, Quaternion b, 
  float t);

这是一个静态函数。调用 Quaternion.Slerp 的结果是对象将开始缓慢旋转,然后在中间加快速度。

在下面的示例中,我们仍然使用之前设置的场景,这次创建一个名为 OrbitScript.cs 的新 C# 脚本。然后,我们将此脚本附加到玩家对象上以实现重力轨道效果。

![图 7.36 – 将脚本附加到 GameObject图 7.36 – B17146.jpg

图 7.36 – 将脚本附加到 GameObject

OrbitScript.cs 的代码如下:

using UnityEngine;
public class OrbitScript : MonoBehaviour
{
    [SerializeField] private Transform _target;
    void Update()
    {
        if (_target == null) return;
        var dir = _target.position - transform.position;
        var targetRotation = Quaternion.LookRotation(dir);
        var currentRotation = transform.localRotation;
        transform.localRotation =
          Quaternion.Slerp(currentRotation, targetRotation,
          Time.deltaTime);
        transform.Translate(0, 0, 5 * Time.deltaTime);
    }
}

在此脚本中,我们重用了 LookAtScript.cs 中的某些代码。我们首先计算了玩家朝向目标的角度。但与之前的脚本不同,我们没有直接修改玩家的旋转,而是使用两个临时变量 targetRotationcurrentRotation 保存了目标旋转和玩家当前旋转。然后,调用 Quaternion.Slerp 函数使玩家逐渐转向目标,这也是实现重力轨道效果的关键。最后,我们调用 transform.Translate 函数以保持玩家向前移动。

![图 7.37 – 运行游戏图 7.37 – B17146.jpg

图 7.37 – 运行游戏

如果我们运行游戏,我们会发现玩家会在目标周围移动并转向面向目标,如图 图 7.37 所示。

使用欧拉角

如果在某些情况下你更愿意使用欧拉角而不是四元数,Unity 允许你将欧拉角转换为四元数,但你不应该从四元数中检索欧拉角,并在修改后将其应用到四元数上,因为这可能会引起问题。

Quaternion.Euler 是这些我们可以用来将欧拉角转换为四元数的函数之一。以下是其函数签名:

public static Quaternion Euler(Vector3 euler);

此函数需要一个 Vector3 类型的参数,该参数提供了围绕 x 轴、y 轴和 z 轴的角度。基于这些数据,此函数返回相应的四元数旋转。

以下代码片段演示了如何在脚本中正确使用欧拉角:

using UnityEngine;
public class EulerAnglesTest : MonoBehaviour
{
    private float _xValue;
    private void Update()
    {
        _xValue += Time.deltaTime * 5;
        var eulerAngles = new Vector3(_xValue, 0, 0);
        transform.rotation = Quaternion.Euler(eulerAngles);
    }
}

在代码中,我们创建了围绕 x 轴旋转的欧拉角,然后调用了 Quaternion.Euler 函数将欧拉角转换为四元数。

![图 7.38 – 将欧拉角转换为四元数图 7.38 – 将欧拉角转换为四元数

图 7.38 – 将欧拉角转换为四元数

将此脚本附加到一个立方体上并运行游戏。你会发现立方体围绕 x 轴旋转。

在本节中,我们向您介绍了四元数,并探讨了如何在 Unity 脚本中正确使用四元数。需要注意的是,在 Unity 中,旋转不仅可以由四元数表示,也可以由欧拉角表示。当使用欧拉角来表示旋转时,其格式对人类来说易于阅读,但由于陀螺仪锁定的影响,Unity 仍然使用四元数来内部保存旋转。

摘要

本章首先介绍了计算机图形学中的坐标系概念,然后讨论了 Unity 使用的坐标系。接着,我们讨论了向量的概念以及如何在 Unity 中执行向量运算,如向量加法、向量减法、点积和叉积。

我们还介绍了矩阵的概念,并演示了如何在 Unity 中使用矩阵进行平移、旋转和缩放。

最后,我们探讨了如何创建旋转以及在四元数中操作旋转,并演示了如何在脚本中正确使用欧拉角。

通过阅读本章,你现在应该对计算机图形学有了更多的数学知识。在下一章中,我们将介绍 Unity 中的可脚本渲染管线。

第八章:第八章:Unity 中的可脚本化渲染管线

第七章“理解 Unity 中计算机图形学的数学”,我们学习了在计算机图形学中使用的数学知识。这些知识是通用的计算机图形学知识,所有 3D 软件和游戏引擎都使用这些数学概念。对于游戏引擎来说,渲染是其最重要的功能之一。在本章中,我们将具体探讨 Unity 提供的渲染功能。

本章将探讨以下关键主题:

  • 可脚本化渲染管线的介绍

  • 与 Unity 的通用渲染管线一起工作

  • 通用渲染管线着色器和材质

  • 提高通用渲染管线的性能

到本章结束时,你将了解可脚本化渲染管线是什么,以及如何在你的项目中启用基于可脚本化渲染管线的通用渲染管线高清晰度渲染管线。你还将知道如何使用通用渲染管线资产来配置你的渲染管线,以及如何使用体积框架将后处理效果应用到你的游戏中。你还将了解如何创建可以在通用渲染管线中使用的自定义着色器和材质,如何使用 Unity 的帧调试器工具查看渲染过程的信息,以及如何使用SRP 批处理器减少你项目中的绘制调用次数。

听起来很令人兴奋!现在,让我们开始吧!

可脚本化渲染管线的介绍

自 2004 年首次发布以来,Unity 游戏引擎已发展成为世界上使用最广泛的实时内容创作平台。大量游戏都是使用 Unity 游戏引擎开发的。同时,Unity 正在迅速应用于传统产业的 内容设计和生产过程,包括 VR、AR 和 MR 模拟应用、建筑设计展示、汽车设计和制造,甚至电影和电视动画制作。基于计算机图形学的实时渲染技术的发展是 Unity 引擎快速增长和广泛应用的重要原因。

在 Unity 2018 版本之前,开发者只能使用 Unity 提供的内置渲染管线。由于 Unity 引擎本身是一个封闭源代码引擎,Unity 中的内置渲染管线对开发者来说就像一个黑盒,开发者无法了解 Unity 引擎内部渲染的具体逻辑实现。此外,使用 Unity 内置渲染管线开发的游戏将在不同平台上使用相同的渲染逻辑。对于开发者来说,为不同平台定制渲染管线非常困难。

随着可脚本渲染管线的发布,开发者可以直接在 GitHub 上查看其代码,并使用 C#代码控制渲染过程,为他们的游戏或应用程序定制独特的渲染管线。

注意

你可以在 GitHub 上找到可脚本渲染管线的代码:github.com/Unity-Technologies/Graphics

可脚本渲染管线是 Unity 为开发者提供的一个工具箱,通过它开发者可以在 Unity 中自由实现特定的渲染功能。为了方便开发者,基于可脚本渲染管线提供了两个预构建的渲染管线,即通用渲染管线和高清渲染管线。

通过使用基于可脚本渲染管线的预构建渲染管线,我们可以直接修改渲染管线中的特定功能,而无需从头开始实现一个新的管线,同时获得优秀的渲染效果和持续更新。因此,当使用 Unity 开发游戏时,有三个现成的渲染管线可供选择:

  • 传统的内置渲染管线

  • 通用渲染管线

  • 高清渲染管线

当然,你也可以选择基于可脚本渲染管线开发自己的渲染管线。

以下是一些使用通用渲染管线或高清渲染管线在 GitHub 上制作的开源项目,你可以下载并使用。

Fontainebleau 示例

第一个项目是使用高清渲染管线制作的Fontainebleau 示例项目:

图 8.1 – Fontainebleau 示例

图 8.1 – Fontainebleau 示例

如图 8.1 所示,这个项目允许你以第一人称模式在森林中行走。你可以在这里找到项目:github.com/Unity-Technologies/FontainebleauDemo

Spaceship 示例

我要介绍的第二个项目是Spaceship 示例项目。这是一个可玩的高质量 AAA 第一人称模式演示,如图 8.2 所示:

图 8.2 – Spaceship 示例

图 8.2 – Spaceship 示例

在这个开源项目中,你可以看到如何实现 GPU 加速的粒子效果,例如逼真的火焰、烟雾和电火花视觉效果。你可以在这里找到项目:github.com/Unity-Technologies/SpaceshipDemo

BoatAttack 示例

如果你选择将通用渲染管线作为你游戏的渲染管线,那么这个开源项目值得一看:

图 8.3 – BoatAttack 示例

图 8.3 – BoatAttack 示例

图 8.3所示,这是一款使用通用渲染管线制作的赛船游戏。您可以在以下链接找到该项目:github.com/Unity-Technologies/BoatAttack

除了 GitHub 上的开源项目外,Unity 还为开发者提供了 Unity 的资产商店上的免费资源,供开发者学习和使用。

这里有一些样本。

异教徒:数字人

我首先想分享的是资产商店上的异教徒:数字人项目:

图 8.4 – 资产商店中的异教徒:数字人

图 8.4 – 异教徒:资产商店中的数字人

图 8.4所示,这是一个免费项目,展示了如何制作具有真实皮肤、眼睛、眉毛等的数字人。您可以在以下链接找到该项目:assetstore.unity.com/packages/essentials/tutorial-projects/the-heretic-digital-human-168620

异教徒:VFX 角色

第二个项目也来自资产商店上的异教徒,并且可以免费下载:

图 8.5 – 异教徒:VFX 角色

图 8.5 – 异教徒:VFX 角色

图 8.5所示,该项目展示了如何在 Unity 中使用高清渲染管线创建基于 VFX 的角色。您可以在以下链接找到该项目:assetstore.unity.com/packages/essentials/tutorial-projects/the-heretic-vfx-character-168622

好吧,在介绍了许多基于 Scriptable Render Pipeline 的开源免费项目之后,您现在对这种渲染管线更感兴趣了吗?如果是这样,那么我们将简要介绍基于 Scriptable Render Pipeline 的两个预构建渲染管线,即通用渲染管线和高清渲染管线。

通用渲染管线

通用渲染管线是 Unity 中基于 Scriptable Render Pipeline 的预构建渲染管线。正如其名称所暗示的,这个渲染管线可以在 Unity 支持的所有平台上使用。不同的管线不能混合,所以一旦您选择使用通用渲染管线,内置渲染管线和高清渲染管线将不会启用。

Unity 默认使用传统的内置渲染管线,但您可以在项目中以不同的方式启用通用渲染管线。

如果您想开发一个新项目,那么您可以使用Unity Hub提供的3D 示例场景(URP)项目模板来创建一个新的通用渲染管线项目:

图 8.6 – 3D 示例场景(URP)项目模板

图 8.6 – 3D 示例场景(URP)项目模板

图 8.6所示,3D 示例场景(URP)项目模板配置了项目设置以在项目中使用通用渲染管线:

![图 8.7 – 新的 URP 项目图片

图 8.7 – 新的 URP 项目

在新项目创建完成后,您可以看到使用通用渲染管线渲染的示例场景,如图 8.7 所示。

然而,如果您想将现有项目从内置渲染管线切换到通用渲染管线,使用通用渲染管线重新创建一个新项目并不适合您的项目。在这种情况下,选择使用 Unity 的包管理器安装通用渲染管线是一个更合适的选择:

![图 8.8 – 打开包管理器窗口图片

图 8.8 – 打开包管理器窗口

可以通过在 Unity 编辑器的工具栏中点击窗口 | 包管理器来打开包管理器窗口,如图中所示:

![图 8.9 – 包管理器图片

图 8.9 – 包管理器

图 8.9所示,您可以在包列表中找到通用 RP包并将其安装到您的项目中。

在本节中,我们简要介绍了通用渲染管线及其在项目中的安装方法。更详细的介绍将在使用 Unity 的通用渲染管线部分进行。接下来,让我们继续简要探索高清渲染管线及其在项目中的安装方法。

高清渲染管线

高清渲染管线是基于 Unity 中的可脚本渲染管线构建的另一个预构建渲染管线。与通用渲染管线不同,它不支持 Unity 支持的所有平台,仅支持高端平台。以下表格显示了高清渲染管线支持的平台:

图 8.10

图 8.10 – 高清渲染管线支持的平台

如前表所示,高清渲染管线目前主要用于如游戏机或桌面电脑等平台。如果您正在开发面向移动设备的项目,那么高清渲染管线不是一个合适的选择。

由于我们希望涵盖尽可能多的使用场景,本章将主要关注通用渲染管线的使用——因此对高清渲染管线的简要介绍。然而,如果您想尝试它或确实需要使用高清渲染管线,安装过程与之前描述的安装通用渲染管线非常相似。

首先,如果您正在启动一个新项目,您可以使用 Unity Hub 提供的3D 示例场景(HDRP)项目模板来创建一个新的高清渲染管线项目。

图 8.11 所示,3D 示例场景 (HDRP) 项目模板配置了项目设置以在项目中使用高清渲染管道:

![图 8.11 – 3D 示例场景 (HDRP) 模板图片

图 8.11 – 3D 示例场景 (HDRP) 模板

在等待新项目创建完成后,你可以查看使用高清渲染管道渲染的示例场景,如图 图 8.12 所示:

![图 8.12 – 新的 HDRP 项目图片

图 8.12 – 新的 HDRP 项目

当然,你还可以从 Unity 的包管理器中安装 高清渲染管道 包:

![图 8.13 – 通过包管理器安装高清渲染管道图片

图 8.13 – 通过包管理器安装高清渲染管道

图 8.13 所示,你可以在包列表中找到 高清渲染管道 包并将其安装到你的项目中。

在本节中,我们简要介绍了高清渲染管道以及如何在你的项目中安装它。

通过阅读本节,脚本渲染管道简介,你现在应该已经了解了脚本渲染管道是什么以及如何在你的项目中安装基于脚本渲染管道的通用渲染管道和高清渲染管道。接下来,我们将详细讨论如何在你的项目中正确使用通用渲染管道。

让我们开始吧!

使用 Unity 的通用渲染管道

通用渲染管道被 Unity 开发者广泛使用。它不仅用于开发 PC 或游戏机游戏;你还可以用它来开发移动游戏。

我们可以通过 Unity Hub 项目模板创建一个新的通用渲染管道项目。通过项目模板,Unity 将自动为我们设置所有渲染管道资源。该项目还包含一个示例场景,如图 图 8.14 所示。你可以在该场景中找到一个摄像机、一个方向光、一个聚光灯、一个后处理体积、反射探针和一些模型:

![图 8.14 – 通用渲染管道的示例场景图片

图 8.14 – 通用渲染管道的示例场景

首先,这个示例场景是一个很好的起点。我们将用它来解释如何使用通用渲染管道。

探索示例场景

让我们先探索这个示例场景。如图 图 8.14 所示,这个场景并不复杂,但它包含了通用渲染管道的大部分功能。我们将分别介绍场景中的这些组件。

主摄像机

让我们从场景中的主摄像机开始。我们可以在 层次结构 窗口中选择 Main Camera 以打开其 检查器 窗口,如图下所示:

![图 8.15 – 主摄像机对象图片

图 8.15 – 主摄像机对象

Main Camera对象上附加了一个Camera组件,它提供了与相机对象相关的所有功能。你可以设置背景、剔除遮罩、抗锯齿设置、相机的透视设置等。

你还需要注意的一个组件是通用附加相机数据组件,你可以在图 8.15 的底部找到它。如果你使用的是通用渲染管线,Unity 不允许你从相机中移除它,因为这个组件用于内部存储数据。

方向光

在这个示例项目中只有一个方向光,用于模拟阳光。你可以通过修改场景中光对象上附加的Light组件的设置来修改光的颜色、强度和阴影效果。你还可以修改光对象的Transform组件的旋转属性来调整方向光的指向,如图 8.16 所示。

图 8.16 – 方向光对象

图 8.16 – 方向光对象

在这个示例中,这个光的强度值为2,并且使用了柔和的阴影。

聚光灯

Unity 中有四种类型的灯光,分别是方向光点光聚光灯区域光。在这个示例场景中,除了用于模拟阳光的方向光外,还有一个聚光灯用于模拟聚光灯:

图 8.17 – 聚光灯对象

图 8.17 – 聚光灯对象

如前图所示,Unity 中聚光灯对象的效果类似于现实世界中的聚光灯。聚光灯的设置与 Unity 中方向光的设置类似:

图 8.18 – 聚光灯设置

图 8.18 – 聚光灯设置

你仍然可以通过修改Spot Light对象上附加的Light组件的设置来修改聚光灯的颜色、强度和阴影效果,并且你也可以修改这个聚光灯的范围和内/外聚光角度,如图 8.18 所示。

后处理体积

现在,让我们来看看示例场景中的后处理体积对象。在游戏开发中,后处理是一种常用于向渲染图像添加各种效果的技巧,常见的效果如色调映射、景深、光晕、抗锯齿和运动模糊:

图 8.19 – 后处理体积

图 8.19 – 后处理体积

通用渲染管道提供了体积组件和体积配置文件对象来管理应用于渲染图像的不同后期处理效果,如图图 8.19所示。使用体积组件的一个优点是组件和特定设置可以解耦。体积组件上的所有设置都来自相关的体积配置文件对象。我们将在稍后详细讨论体积配置文件对象。

在这个示例场景中,应用了色调映射光晕晕影效果。如果你对未经后期处理的原始渲染图像感兴趣,让我们看看当我们禁用这个后期处理体积时会发生什么:

图 8.20 – 原始图像(顶部)与后期处理图像(底部)

图 8.20 – 原始图像(顶部)与后期处理图像(底部)

图 8.20显示了示例场景的原始图像和后期处理图像的比较。

反射探头

反射探头可以通过对其周围场景进行采样,为场景中相关的模型提供高效的反射信息,从而使场景中模型的表面具有逼真的反射效果:

图 8.21 – 场景中的反射探头

图 8.21 – 场景中的反射探头

在这个示例场景中,我们可以看到有三个反射探头作为名为Reflection Probes的 GameObject 的子对象,如图所示。

如果我们选择这些反射探头中的一个,那么相应的反射探头将在场景视图中显示,并显示反射信息,如图图 8.22所示:

图 8.22 – 查看场景中的反射信息

图 8.22 – 查看场景中的反射信息

由于不同位置的反射探头会获得不同的反射信息,为了正确使用反射信息,它们需要放置在适当的位置。虽然“适当的位置”的定义因场景而异,但一个一般的指导原则是,你应该将反射探头放置在场景中任何将被显著反射的大物体附近。例如,将反射探头放置在场景中墙壁中心和角落的周围。当然,这并不意味着可以忽略场景中的所有小物体。例如,与墙壁相比,场景中的篝火可能是一个小物体,但反射篝火的光线对于创建场景的真实渲染同样重要。

通用渲染管道资产

由于这个新的示例项目是使用通用渲染管道模板创建的,Unity 已经自动为我们设置好了一切,以便使通用渲染管道正常工作。然而,如果你的项目正在使用内置渲染管道进行开发,并且你想切换到使用通用渲染管道,或者如果你的项目已经使用通用渲染管道开发,但你想使用另一个渲染管道,那么了解如何在 Unity 中设置它是非常必要的。

注意

在本章中,我们正在使用通用渲染管道中的 前向渲染路径。所谓的 渲染路径 指的是与光照和着色相关的一系列操作。Unity 的内置渲染管道提供了不同的渲染路径,例如前向渲染路径和 延迟渲染路径。从通用渲染管道的 12.0.0 版本开始,开发者也可以在管道中使用延迟渲染路径,但这超出了本章的范围。如果你对这个主题感兴趣,你可以在 docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/rendering/deferred-rendering-path.htmldocs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/urp-universal-renderer.html#rendering-path-comparison 找到更多信息。

我们将引导你完成以下步骤,学习如何在 Unity 中为你的项目设置渲染管道:

  1. 让我们从 项目设置 窗口开始。你可以在 Unity 编辑器的工具栏中通过 编辑 | 项目设置 打开这个窗口。

  2. 接下来,点击左侧分类列表中的 图形 项以打开 图形 设置面板,如图 8.23 所示:

![图 8.23 – 图形设置图片

图 8.23 – 图形设置

让我们详细看看 可脚本渲染管道设置 属性:

  • 图形 设置的 可脚本渲染管道设置 属性与一个名为 UniversalRP-HighQuality通用渲染管道资产 类型的对象相关联,该对象在项目使用模板创建时自动创建。

  • 如果 图形 设置的 可脚本渲染管道设置 属性设置为无,那么 Unity 将使用默认的内置渲染管道。

  1. 你可以在项目的 Assets > Settings 文件夹中找到这个 通用渲染管道资产 对象:

![图 8.24 – 通用渲染管道资产对象图片

图 8.24 – 通用渲染管道资产对象

  1. 然后,选择UniversalRP-HighQuality对象以打开检查器窗口,这样我们就可以检查该通用渲染管线资产对象的详细信息。如图图 8.25所示,一个通用渲染管线资产对象为当前的通用渲染管线提供各种设置,例如渲染功能和渲染质量:

图 8.25 – 该通用渲染管线资产对象的检查器窗口

图 8.25 – 该通用渲染管线资产对象的检查器窗口

  • 接下来,让我们逐一查看这些设置。我们可以在通用部分配置渲染管线的通用设置,如图图 8.26所示。例如,如果启用了深度纹理选项,你可以在着色器代码中访问渲染管线生成的深度图:

图 8.26 – 通用设置

图 8.26 – 通用设置

注意

在游戏开发中,深度纹理用于表示从摄像机视角看 3D 空间中对象的深度信息。

  • 我们还可以控制全局渲染质量:

图 8.27 – 质量设置

图 8.27 – 质量设置

图 8.27中,我们启用了全局HDR选项,全局抗锯齿设置为2x。我们还可以通过调整渲染缩放滑块来修改渲染分辨率。

  • 作为实时渲染的一个重要因素,通用渲染管线的灯光也可以在灯光部分进行配置,如图图 8.28所示:

图 8.28 – 灯光设置

图 8.28 – 灯光设置

设置面板中的主光源是游戏场景中最亮的方向性光源。你可以决定是否启用它以及是否允许它投射阴影。

  • 图 8.29中,你可以在灯光设置下找到阴影设置。你可以在此处修改参数以调整 Unity 中的阴影外观:

图 8.29 – 阴影设置

图 8.29 – 阴影设置

  • 最后,让我们探索高级设置:

图 8.30 – 高级设置

图 8.30 – 高级设置

这里,我们可以检查SRP 批处理器选项以启用 SRP 批处理器功能,从而提高通用渲染管线的性能,我们将在提高通用渲染管线性能部分详细解释。我们还可以修改调试日志输出的级别,如图图 8.30所示。

注意

如果你的项目中没有通用渲染管线资产,你可以在 Unity 编辑器的工具栏中点击资产 > 创建 > 渲染 > 通用渲染管线 > 管线资产来创建一个新的:

在本节中,我们介绍了 Unity 中的通用渲染管线资产以及如何通过更改图形设置的脚本渲染管线设置属性在不同的渲染管线之间切换。接下来,我们将探讨通用渲染管线中的另一个重要资产,即体积配置文件。

体积框架和后处理

体积框架由脚本渲染管线为游戏开发者提供。通过使用此框架,开发者可以将组件与其特定设置解耦。基于脚本渲染管线的渲染管线,如通用渲染管线和高清渲染管线,使用此框架。

正如我们之前提到的,通用渲染管线使用体积组件和体积配置文件对象来管理应用于渲染图像的不同后处理效果。以下步骤演示了如何启用体积框架并应用一些后处理效果到示例项目中:

  1. 首先,我们需要在场景中的 GameObject 上添加一个体积组件来启用体积框架,如图所示:

![Figure 8.31 – Adding a Volume component](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure 8.31 – 添加体积组件)

img/Figure_8.31_B17146.jpg

图 8.31 – 添加体积组件

  1. 图 8.31中,该体积组件的配置文件属性为0,因此我们可以通过点击其下方的新建按钮创建一个新的体积配置文件,或者分配一个现有的体积配置文件给它。在这里,我们将创建一个新的体积配置文件

![Figure 8.32 – Creating a new Volume Profile file](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure 8.32 – 创建新的体积配置文件)

img/Figure_8.32_B17146.jpg

图 8.32 – 创建新的体积配置文件

  1. 点击添加覆盖按钮打开体积覆盖面板,然后点击后处理项打开后处理覆盖列表:

![Figure 8.33 – Add Override](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure 8.33 – 添加覆盖)

img/Figure_8.33_B17146.jpg

图 8.33 – 添加覆盖

  1. 图 8.34中,您可以在后处理覆盖列表中看到许多后处理效果。您可以选择要应用于渲染图像的效果,例如辉光

![Figure 8.34 – Post-processing effects](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure 8.34 – 后处理效果)

img/Figure_8.34_B17146.jpg

图 8.34 – 后处理效果

  1. 最后,让我们修改辉光效果配置;检查阈值强度选项,并将它们的值分别设置为0.94,如图所示:

![Figure 8.35 – Setting up the Bloom effect](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure 8.35 – 设置辉光效果)

img/Figure_8.35_B17146.jpg

图 8.35 – 设置辉光效果

完成前面的步骤后,切换到游戏视图。应用辉光效果后,我们可以在图 8.36中看到游戏场景:

![Figure 8.36 – The applied Bloom effect image (top) versus the original image (bottom)](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure 8.36 – 应用辉光效果后的图像(顶部)与原始图像(底部)对比)

img/Figure_8.36_B17146.jpg

图 8.36 – 应用辉光效果后的图像(顶部)与原始图像(底部)对比

在本节中,我们首先通过探索模板中包含的示例场景来学习通用渲染管道的功能。然后,我们介绍了如何在不同的渲染管道和通用渲染管道资产之间切换。最后,我们演示了如何使用体积框架在通用渲染管道中实现后处理。我们旅程的下一站是探索对 Unity 中的渲染至关重要的着色器和材质。

通用渲染管道着色器和材质

着色器材质对于在 Unity 中渲染模型至关重要。着色器用于提供算法来计算每个像素的颜色。材质为与其关联的着色器提供各种参数,以确定如何渲染模型,例如提供纹理作为着色器的输入并定义着色器如何采样纹理:

图 8.37 – 材质和着色器

图 8.37 – 材质和着色器

如果我们在场景中选择一个模型,例如安全帽模型,材质设置将在检查器窗口中显示,如图图 8.37所示。

常用着色器

每种材质都可以与指定的着色器相关联,并且该着色器所需的参数将在检查器窗口中显示。在使用通用渲染管道时,常用的着色器是通用渲染管道/Lit,并且这个安全帽模型也是使用这个着色器渲染的。通过调整各种参数,通用渲染管道/Lit着色器可以用于渲染不同的材质表面,如金属、玻璃和木材。

注意

命名为Lit的着色器意味着这个着色器将执行光照计算。命名为Unlit的着色器表示该着色器在计算像素颜色时不会考虑光照因素。

我们可以通过选择不同的着色器通过着色器下拉窗口来更改材质相关的着色器,如图图 8.38所示:

图 8.38 – 着色器

图 8.38 – 着色器

一旦我们确定了与材质关联的着色器,我们就可以通过这个材质提供各种特定着色器的参数:

图 8.39 – 着色器的参数

图 8.39 – 着色器的参数

图 8.39中,开发者可以在表面输入部分为通用渲染管道/Lit着色器指定各种贴图。与这些贴图参数相关的纹理用于为着色器提供不同的信息。以下将详细解释:

  • 基础贴图用于向着色器提供表面的基础颜色。

  • 金属贴图用于向着色器提供金属工作流程信息,以确定表面“金属”程度。

  • 法线贴图用于向模型表面添加原始模型上不存在的更多细节。

  • 遮挡贴图用于向着色器提供信息,以模拟环境光照产生的阴影。

不同着色器所需的参数可能不同,由于着色器算法的不同,最终的渲染结果也有所不同:

另一方面,Unity 还为开发者提供了一个功能,可以自动将现有材质升级到通用渲染管线材质。您可以在 Unity 编辑器工具栏中通过点击编辑 > 渲染管线 > 通用渲染管线 > 将项目材质升级到通用 RP 材质来找到它,如前图所示。

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_8.43_B17146.jpg)

图 8.40 – 无光照着色器

因此,当将正在使用内置渲染管线开发的项目更改为使用通用渲染管线时,开发者经常会遇到一个称为“材质错误”的问题:

![图 8.41 – 安全帽模型![图 8.41 – 安全帽模型图 8.41 – 安全帽模型使用此材料渲染的安全帽模型将只显示基本颜色,并且将不再受任何光照的影响。您可以在前面的屏幕截图中看到安全帽模型与周围模型之间的差异。## ![图 8.40 – 无光照着色器正如我们在本章开头提到的,如果您选择使用通用渲染管线,内置渲染管线将不再可用。这不仅包括内置渲染管线本身,还包括与内置渲染管线一起使用的着色器。![图 8.40 – 无光照着色器![图 8.42 – 内置标准着色器不能在通用渲染管线中使用例如,如果我们将与此材质关联的着色器更改为通用渲染管线/无光照,那么在表面输入部分中只剩下基础贴图以提供表面的基础颜色:图 8.42 – 内置标准着色器不能在通用渲染管线中使用![图 8.42 – 无光照着色器因此,如果您的项目正在使用内置渲染管线进行开发,但需要切换到使用通用渲染管线,那么为了确保通用渲染管线可以正确工作,您需要将现有的材质升级到通用渲染管线材质。您可以手动修改与现有材质关联的着色器,例如将内置的标准着色器替换为通用渲染管线/Lit着色器:![图 8.43 – 升级项目材质例如,我们可以将渲染安全帽模型所使用的着色器从通用渲染管线/无光照更改为内置的标准着色器。然后,您可以看到安全帽模型显示为粉红色,这意味着材质中存在错误,如前图所示。图 8.43 – 升级项目材质升级项目材质到通用渲染管线材质图 8.44 – 材质升级器

图 8.44 – 材质升级器

然后,将弹出材质升级器窗口。如图 8.44 所示,更改是不可逆的,因此如果你想要将项目中所有的材质升级到通用渲染管线材质,并且已经备份了项目,请点击继续按钮。

注意

这个材质升级器工具只能将内置着色器升级到通用渲染管线着色器,但不能升级开发者创建的自定义着色器。因此,自定义着色器仍然需要手动修改。

创建着色器和着色器图

有时,你可能想要创建一个新的着色器来实现一些可以与通用渲染管线一起使用的自定义功能。有两种方法可以实现这一点——你可以创建一个新的着色器文件或着色器图文件。

创建新的着色器文件

首先,即使我们的项目使用通用渲染管线,我们仍然可以使用传统的办法,在 Unity 内置的渲染管线中使用着色器模板来创建自定义着色器文件。如图 8.45 所示,我们可以点击资产 > 创建 > 着色器来创建一个新的着色器:

图 8.45 – 创建着色器文件

图 8.45 – 创建着色器文件

列出了一些内置的着色器模板,例如CustomShader

图 8.46 – 自定义着色器

图 8.46 – 自定义着色器

然后,在我们的项目中创建了一个着色器文件,如图中所示。你可以通过双击它来在你的 IDE 中打开着色器源文件,然后你可以使用 Unity 的ShaderLab 语言来编写着色器代码,该代码定义了 Unity 如何计算每个像素的渲染颜色。

注意

在 Unity 的 ShaderLab 语言中如何编写着色器超出了本章的范围,但如果你感兴趣,可以在docs.unity.cn/Packages/com.unity.render-pipelines.universal@7.7/manual/writing-custom-shaders-urp.html找到更多信息。

除了创建着色器文件,我们还可以创建一个新的自定义着色器图文件来渲染场景中的这些模型。让我们继续。

创建新的着色器图文件

与创建着色器文件的传统方法相比,创建一个新的着色器图文件更容易。着色器图功能首次在 Unity 2018 中引入。在开发着色器图文件时,你不需要编写着色器代码,而是直接使用可视化节点进行开发。

我们仍然可以创建一个新的无光照着色器,但这次,我们将使用着色器图而不是着色器文件,如下面的步骤所示:

  1. 如图 8.47 所示,点击CustomShaderGraph

图 8.47 – 创建新的着色器图文件

图 8.47 – 创建新的着色器图文件

  1. 创建了一个新的着色器图文件,其扩展名为.shadergraph,如图所示:

图 8.48 – Shader 图文件

图 8.48 – Shader 图文件

  1. 双击此文件,这次 Shader 图文件将不会在 IDE 中打开,而是在 Unity 编辑器中直接显示一个视觉节点编辑器,如图 图 8.49 所示:

图 8.49 – Shader 图编辑器

图 8.49 – Shader 图编辑器

这个视觉节点编辑器有很多内容需要理解,所以让我们更详细地了解一下:

  • 在 Unity 中,一个着色器通常由两部分组成,即顶点程序和片段程序。

  • 顶点程序通常用于将模型的顶点在屏幕空间中的 3D 坐标转换为 2D 坐标。我们在上一章中已经介绍了坐标系的知识。在这个例子中,顶点程序中有三个节点,即位置法线切线

  • 或者,片段程序确定像素的颜色,在这个例子中,片段程序只有一个节点,名为基础颜色

  • 您还可以在右下角的主预览窗口中预览此着色器的结果,如图 图 8.49 所示。我们刚刚创建的着色器将用蓝色渲染像素。

现在我们已经创建了一个新的 Shader 图文件,并在 Shader 图编辑器中打开它,这允许我们编辑、添加和删除节点,接下来我们将看看如何在 Shader 图文件中编辑一个节点。让我们开始吧!

在 Shader 图中编辑节点的属性

我们可以在 Shader 图文件中编辑现有节点的属性。如我们之前提到的,有一个名为基础颜色的节点,所以让我们按照以下方式编辑此节点:

图 8.50 – 编辑基础颜色节点

图 8.50 – 编辑基础颜色节点

  1. 在 Shader 图编辑器中,选择片段部分中的基础颜色节点。

  2. 点击此节点的颜色输入以打开颜色选择器窗口。

  3. 在颜色选择器窗口中选择您想要使用的颜色 – 在这种情况下,我们为基础颜色节点选择了黄色。主预览窗口显示了着色器正在发生的情况,如图 图 8.50 所示。

如您所见,修改现有节点非常简单;除了修改节点外,我们还可以创建一个新的节点,为着色器提供更多数据。让我们继续。

在 Shader 图中添加新节点

开发着色器通常涉及采样纹理并返回一个颜色值供着色器使用。让我们执行以下步骤,向我们的示例着色器添加一个新节点,以添加采样纹理的能力:

  1. 在 Shader 图编辑器中右键单击,并从弹出菜单中选择创建节点

图 8.51 – 创建节点

图 8.51 – 创建节点

  1. 创建节点窗口的右上角搜索栏中输入texture,然后在结果列表中选择采样纹理 2D项:

图 8.52 – 选择 Sample Texture 2D 节点

图 8.52 – 选择 Sample Texture 2D 节点

  1. 图 8.53所示,创建了一个新的Sample Texture 2D节点。点击此节点的Texture插槽以提供纹理资产:

图 8.53 – 点击节点的 Texture 插槽

图 8.53 – 点击节点的 Texture 插槽

  1. 从弹出的选择纹理窗口中选择一个纹理:

图 8.54 – 选择纹理

图 8.54 – 选择纹理

  1. 然后,如图 8.55所示,Sample Texture 2D节点从其Texture输入采样纹理并获取纹理的颜色:

图 8.55 – 从纹理资产加载数据

图 8.55 – 从纹理资产加载数据

现在,我们在 Shader Graph 文件中添加了一个新的节点,但从纹理采样得到的颜色仍然存储在Sample Texture 2D节点中。接下来,我们需要将其与Fragment部分的Base Color节点连接起来,以便着色器可以使用从该纹理获取的颜色渲染像素。

在 Shader Graph 中连接两个节点

我们可以通过在 Shader Graph 文件中连接两个节点,将数据(如颜色)从一个节点传递到另一个节点,因此让我们按照以下步骤将Sample Texture 2D节点与Base Color节点连接起来:

  1. 图 8.56所示,点击RGBA(4)输出的旁边的单选按钮:

图 8.56 – 点击单选按钮

图 8.56 – 点击单选按钮

  1. 之后,将出现一条可以自由拖动的线:

图 8.57 – 出现一条线

图 8.57 – 出现一条线

  1. 将此线拖动到Base Color节点的颜色输入。如图 8.58所示,我们连接了这两个节点,主预览窗口显示着色器已使用从纹理获取的颜色渲染了像素:

图 8.58 – 连接两个节点

图 8.58 – 连接两个节点

现在您应该知道如何创建 Shader Graph 文件以及如何修改、添加和连接其中的节点。作为开发者,在使用 Shader Graph 时,我们不需要编写着色器代码,Unity 将根据 Shader Graph 文件的内容自动生成着色器代码。

在本节中,我们介绍了与通用渲染管道着色器和材质相关的知识,然后演示了如何将内置材质升级为通用渲染管道材质,最后探讨了如何创建一个可以在通用渲染管道中使用的自定义着色器。接下来,我们将继续讨论如何查找性能问题并提高通用渲染管道的性能。

提高通用渲染管道的性能

渲染是游戏引擎的主要功能之一。因此,了解如何有效地使用 Unity 的渲染管线非常重要。在本节中,我们将讨论的主题是性能。

帧调试器

首先,我们应该学习如何使用工具来查看和定位由 Unity 中的渲染引起的性能瓶颈。

Unity 编辑器中的帧调试器工具是我们推荐的工具,它允许我们轻松查看 Unity 中渲染一帧的整个过程。

让我们按照以下步骤来了解 Unity 的渲染管线是如何渲染您游戏的一帧的:

  1. 通过点击播放按钮在编辑器中启动游戏:

![Figure 8.59 – 在编辑器中播放游戏

![img/Figure_8.59_B17146.jpg]

图 8.59 – 在编辑器中播放游戏

  1. 在 Unity 编辑器的工具栏中点击窗口 > 分析 > 帧调试器以打开帧调试器窗口,如图图 8.60所示:

![Figure 8.60 – 打开帧调试器

![img/Figure_8.60_B17146.jpg]

图 8.60 – 打开帧调试器

  1. 帧调试器窗口中,点击启用按钮来捕捉您游戏当前帧的快照,如图图 8.61所示:![Figure 8.61 – 帧调试器

    ![img/Figure_8.61_B17146.jpg]

图 8.61 – 帧调试器

  1. 图 8.62中,我们可以看到有109个绘制调用,这些调用调用图形 API,如OpenGLDirect3DVulkan来绘制对象。我们还可以选择一个特定的绘制调用来查看其详细信息:

![Figure 8.62 – 查看绘制调用信息

![img/Figure_8.62_B17146.jpg]

图 8.62 – 查看绘制调用信息

通过帧调试器工具,我们可以理解整个渲染过程并查看特定绘制调用的信息,这为我们提供了确定如何提高渲染性能的见解。例如,在图 8.62中,我们可以看到使用了33个绘制调用来渲染不透明对象。因此,减少这里的绘制调用数量是我们应该做的事情。接下来,我们将介绍如何使用 SRP Batcher 来实现这一点。

SRP Batcher

SRP Batcher是 Scriptable Render Pipeline 提供的一项功能,因此每个基于 Scriptable Render Pipeline 的渲染管线都可以使用此功能来减少绘制调用次数并提高渲染性能。

为了确保 SRP Batcher 可以在您的项目中正确工作,您需要确保两件事。第一是启用通用渲染管线的SRP Batcher功能,第二是确保您项目中的着色器与 SRP Batcher 兼容。

首先,让我们确保渲染管线中启用了 SRP Batcher。正如我们在《通用渲染管线资产》子节中提到的,我们可以通过检查我们项目使用的通用渲染管线资产文件的高级设置中的SRP Batcher选项来启用它,如图图 8.63所示:

![图 8.63 – 启用 SRP Batcher图片

图 8.63 – 启用 SRP Batcher

接下来,让我们检查我们用于渲染这些不透明对象的着色器是否与 SRP Batcher 兼容:

![图 8.64 – 着色器的 SRP Batcher 兼容性状态图片

图 8.64 – 着色器的 SRP Batcher 兼容性状态

我们可以在检查器窗口中找到着色器的 SRP Batcher 兼容性状态,如图中所示。在这里,使用的是通用渲染管线/Lit着色器,它与 SRP Batcher 兼容。

现在,让我们运行游戏并再次检查帧调试器:

![图 8.65 – 绘制调用次数减少图片

图 8.65 – 绘制调用次数减少

正如你在图 8.65中可以看到的,总的绘制调用次数已经从109减少到91,用于渲染不透明对象的绘制调用次数已经从33减少到20,并且每个绘制调用都被标记为SRP Batch

在本节中,我们首先介绍了如何使用 Unity 的帧调试器工具来查看整个渲染过程和特定绘制调用的信息。然后,我们还探讨了如何通过使用 SRP Batcher 来减少绘制调用次数并提高渲染性能。

摘要

本章介绍了 Unity 中可以选择的三个现成的渲染管线,即传统的内置渲染管线和基于可脚本渲染管线(Scriptable Render Pipeline)的两个预制渲染管线——通用渲染管线和高清渲染管线。同时,我们还介绍了一些使用这些渲染管线的开源项目,供你学习和使用。

然后,我们讨论了如何在 Unity 中使用通用渲染管线,首先通过探索一个示例场景,然后解释了如何使用通用渲染管线资产来配置你的渲染管线以及如何使用体积框架来对你的游戏应用后处理效果。

我们还介绍了着色器和材质的概念,演示了如何将内置材质升级为通用渲染管线材质,并探讨了如何创建一个可以在通用渲染管线中使用的自定义着色器。

最后,我们探讨了如何使用 Unity 的帧调试器工具来查看渲染过程的信息以及如何使用 SRP Batcher 来减少绘制调用次数。

通过阅读本章,你现在应该已经理解了如何在 Unity 中正确地使用通用渲染管线。在下一章中,我们将介绍如何在 Unity 中使用面向数据的技术堆栈DOTS)。

第九章:第九章: Unity 中的面向数据的技术堆栈

Unity 引擎是一个非常友好的开发者引擎。在开发游戏逻辑时,Unity 的GameObject-Components架构可以帮助开发者快速开发功能,而在 Unity 中给 GameObject 添加新行为只需将其对应的组件附加到它上。然而,随着今天游戏变得越来越复杂,这种方法虽然对开发者非常友好,尤其是对熟悉传统面向对象编程OOP)模型的人来说,但对于游戏性能和项目可维护性来说并不理想。

因此,Unity 引入了面向数据的技术堆栈DOTS),允许开发者使用一种面向数据而非面向对象的替代编程哲学来编写游戏代码。它还引入了多线程功能以优化游戏性能。

本章将涉及以下关键主题:

  • DOTS 概述

  • Unity 中的多线程和 C#作业系统

  • 在 Unity 中与 ECS 协作

  • 使用 C#和 Burst 编译器

通过阅读本章,你将了解 DOTS 是什么,以及面向数据设计与传统面向对象设计的区别。你还将了解到如何使用 Unity 的C#作业系统来实现多线程以提高游戏性能,如何使用 Unity 的实体组件系统ECS)以面向数据的方式编写游戏逻辑代码,以及如何使用Burst 编译器优化为 Unity 游戏生成的原生代码。

技术要求

本章的示例项目已在 GitHub 上提供。你可以在这里找到它:github.com/PacktPublishing/Game-Development-with-Unity-for-.NET-Developers/tree/main/Chapter9-DOTS

DOTS 概述

DOTS 是 Unity 中的一种新编程模式,也是近年来在 Unity 开发者社区中讨论很多的话题。

![Figure 9.1 – 基于 DOTS 的 Megacity 演示]

![Figure_9.01_B17146.jpg]

![Figure 9.1 – 基于 DOTS 的 Megacity 演示]

如果你之前有.NET 编程经验,你会熟悉面向对象编程OOP)模式。OOP 在软件行业中得到了广泛应用,使用 Unity 开发游戏也不例外,直到 Unity 引入了 DOTS。毫无疑问,OOP 对于许多程序员来说是一个老习惯。因此,在讨论为什么 Unity 引入 DOTS 之前,我们首先会谈谈在使用 OOP 进行 Unity 开发时可能遇到的问题。

面向对象设计模式与 DOTS

首先,让我们来谈谈面向对象编程(OOP)的概念。我们可以在维基百科上找到一些有用的信息。这些概念包括对象/类继承接口信息隐藏多态。以下链接提供了详细的解释:en.wikipedia.org/wiki/Object-oriented_design

如果我们专注于对象/类和继承,我们会发现这两个概念是面向对象编程(OOP)和 DOTS 之间最大的区别。

让我们从对象/类的概念开始。在传统的面向对象编程模式中,一个类是一组紧密耦合的数据和行为,这些数据和行为作用于这些数据。这里有一个例子:

using UnityEngine;
public class Monster : MonoBehaviour
{
    # region Data
    private string _name;
    private float _hp;
    private Vector3 _position;
    private bool _isDead;
    #endregion
    #region Behavior
    public void Attack(Monster target){}
    public void Move(float speed){}
    public void Die(){}
    #endregion
}

如您在代码中所见,我们有一个名为 Monster 的类,它包含一些关于其位置、健康、名称以及是否死亡的数据。此外,这个类还可以独立于其数据表现出真实对象的行为。每个 Monster 对象都可以攻击目标、移动自己或死亡。

到目前为止,一切都很完美;程序中的对象就像现实世界中的对象一样,仿佛它们有自己的生命,这也符合人类经验。接下来,让我们讨论面向对象编程(OOP)的继承概念。我们可以通过继承扩展类的数据和行为,并重用其中的一些代码。

假设在这个例子中我们意识到并不是所有的怪物都会攻击其他怪物;有些可能会攻击人类。从编程的角度来看,人类和怪物有很多共同点:位置、健康以及是否死亡。但有些怪物可能不会被杀死,而人类不能飞来移动。在现实生活中,我们有一个怪物和人类的超集,即生物。让我们将怪物和人类共享的数据放在 Creature 类中,这样它们都可以拥有这些数据,但我们不必再次输入,如下所示:

public class Creature : MonoBehaviour
{
    #region Data

    private string _name;
    private float _hp;
    private Vector3 _position;
    private bool _isDead;

    #endregion
}
public class Monster : Creature
{
    # region Data
    private bool _canFly;
    #endregion
    #region Behavior
    public void Attack(Creature target){}
    public void Move(float speed){}
    public void Die(){}

    #endregion
}

基本上,如果我们继续这个想法,最终会得到一些复杂的类图,其中包含各种不同的生物,例如怪物、人类、动物和植物。我们甚至还没有考虑性能问题,就已经发现面向对象编程(OOP)会给我们带来很多麻烦。

现在,让我们看看面向对象编程(OOP)如何滥用硬件。随着技术的发展,处理器硬件变得越来越快,但常常被忽视的一点是,如果数据不能足够快地从内存提交到处理器,那么无论处理器有多快,它都不会像预期的那样工作得那么快。缓存,位于处理器核心附近,是一种更小、更快的内存。当处理器发出内存访问请求时,它首先会检查缓存中是否有数据。如果存在(这被称为缓存命中),数据将直接返回,无需访问主内存;如果不存在,必须首先将主内存中的相应数据加载到缓存中,然后再返回给处理器。CPU 通常使用具有多个缓存级别的层次结构;例如,在两级缓存系统中,L1 缓存靠近处理器端,而 L2 缓存靠近内存端。

CPU 缓存的设计基于几个假设。缓存之所以有效,主要是因为程序运行时对内存的访问具有局部性。这种局部性包括空间局部性时间局部性。也就是说,我们需要执行一系列相关操作的数据片段可能在内存中非常接近,或者我们刚刚用于一个操作的数据可能很快就会用于另一个操作。利用这种局部性,缓存可以实现极高的命中率。

然而,面向对象编程(OOP)常常滥用硬件。我们仍然以 Monster 类为例。假设一个 Monster 对象将占用 56 字节内存,我们遍历怪物列表并调用它们的 Move() 函数来改变怪物的位置属性。

伪代码如下:

    public void Update()
    {
        for (var i = 0; i < _monsters.count; i++) 
        { 
            _monsters[i].Move(speed); 
        }
}

这段代码实际上每帧都会修改一组 Vector3 数据,但这段数据在内存中的分配方式是怎样的?以下图表显示了当 64 字节缓存行被分割成 8 字节块时,怪物对象在内存中的分配情况:

图 9.2 – 内存中的数据布局(OOP)

图 9.2 – 内存中的数据布局(OOP)

从图中我们可以看出,每帧将要修改的位置数据在内存中是不连续的,这意味着我们的游戏无法有效地使用高速内存,即缓存。

现在,让我们看看 Unity 中的新编程模式,DOTS。

与面向对象编程(OOP)不同,DOTS 的哲学是针对数据而不是对象进行设计,重点是优先组织和优化数据,使其内存访问尽可能高效。让我们仍然以 Monster 类为例,看看在使用 DOTS 时其数据在内存中的分配情况。

图 9.3 – 内存中的数据布局(DOTS)

图 9.3 – 内存中的数据布局(DOTS)

您还记得吗?当移动一个怪物时,我们实际上只需要 12 个字节的怪物位置数据,因此代码只需要加载和处理所有怪物的位置数据来移动它们。使用 DOTS 允许我们将所有这些位置数据打包到一个数组中,并更有效地分配内存,如图中所示。在内存中将数据放置在连续的数组中提高了数据局部性,这导致缓存命中率极高,从而提高了代码性能。

因此,Unity 的 DOTS 是如何使开发者的代码运行得更高效的?嗯,Unity 中的 DOTS 不仅仅是将编程范式从面向对象转换为面向数据;它实际上包括一系列新的技术模块,即以下内容:

  • C#作业系统

  • ECS

  • Burst 编译器

每个模块都包含一个或多个 Unity 包。我们可以通过 Unity 的包管理器安装相应的功能。接下来,我们将简要介绍这三个模块,分别。

C# 作业系统

通过使用 C#作业系统,我们可以在 Unity 中编写高效的异步代码,充分利用硬件。

![图 9.4 – 使用 C#作业系统的技术演示图片

图 9.4 – 使用 C#作业系统的技术演示

前面的图显示了使用 Unity 的 C#作业系统开发的演示项目,展示了场景中成千上万的“士兵”攻击敌人。您可以在 GitHub 上找到这个项目:github.com/Unity-Technologies/UniteAustinTechnicalPresentation

我们将在Unity 中的多线程和 C#作业系统部分详细讨论 C#作业系统。

ECS

ECS的全称是实体组件系统。它是 Unity 中 DOTS 的核心部分,围绕使用面向数据的设计构建,这与您可能习惯的面向对象设计非常不同。

![图 9.5 – 大都市演示图片

图 9.5 – 大都市演示

前面的图显示了 Unity 使用 ECS 的令人印象深刻的演示项目,名为Megacity,开发者可以在此处下载:unity.com/megacity

![图 9.6 – 大都市下载页面图片

图 9.6 – 大都市下载页面

我们将在在 Unity 中使用 ECS部分详细介绍 ECS。

Burst 编译器

Unity 中的 Burst 编译器是一种高级编译技术。使用 DOTS 制作的 Unity 项目可以使用 Burst 技术来提高它们的运行时性能。Burst 在 C#的一个子集上工作,称为高性能 C#HPC#),并在 LLVM 编译器框架下应用高级优化方法来生成高效的二进制文件,从而实现设备能量的高效利用。

我们将在稍后的部分介绍如何在您的项目中使用它,使用 C#和 Burst 编译器

本节介绍了与 DOTS 相关的知识,例如 DOTS 包含哪些技术模块,其设计理念与传统面向对象编程(OOP)有何不同,以及它解决了哪些问题。然而,DOTS 并不是 OOP 的替代品;它只是为 Unity 中的游戏开发者提供了一种另一种高效的编程模式。例如,你仍然可以使用 C# Job System 以传统的 Unity GameObject-Components 风格实现多线程编程,而不是自己维护线程池。好吧,接下来,让我们探索如何在 Unity 中实现高效的多线程编程。

Unity 中的多线程和 C# Job System

在开发.NET 项目时,异步编程非常常见。但与许多熟悉.NET 开发的开发者所想的不同,Unity 对异步编程的支持最初并不友好。

协程

在 Unity 2017 之前,如果游戏开发者想要处理异步操作,一个常见的场景是等待网络响应。理想的解决方案是在 Unity 中使用协程

我们可以在 Unity 中如下启动协程:

    void Start()
    {
        var url = "https://jiadongchen.com";
        StartCoroutine(DownloadFile(url));
    }
    private static IEnumerator DownloadFile(string url)
    {
        var request = UnityWebRequest.Get(url);
        request.timeout = 10;
        yield return request.SendWebRequest();
        if (request.error != null)
        {
            Debug.LogErrorFormat("error: {0}, url is: {1}",
              request.error, url);
            request.Dispose();
            yield break;
        }
        if (request.isDone)
        {
            Debug.Log(request.downloadHandler.text);
            request.Dispose();
            yield break;
        }
    }

如代码所示,我们使用StartCoroutine函数启动协程,并在协程内部使用yield语句暂停执行。然而,协程本质上仍然是单线程的,只是将任务分散到多个帧中,而不是多线程。

async/await

Unity 在 2017 年引入了async/await运算符,允许游戏开发者在其游戏中使用async/await编写异步代码,但它仍然不像一个正常的.NET/C#程序。这是因为 Unity 引擎自行管理这些线程,大部分逻辑都在 Unity 的主线程上运行,包括作为脚本的 C#代码以及引擎的 C++代码。我们可以使用Unity Profiler工具来查看 CPU 时间线。如下面的截图所示,Unity 引擎默认在主线程上运行脚本:

Figure 9.7 – CPU 时间线

Figure 9.07 – CPU 时间线

图 9.7 – CPU 时间线

场景中有 50 个 GameObject,每个都附加了一个MainThreadExample脚本。你可以看到,这 50 个脚本中的Update函数是依次执行的。

你可以对不同类型的任务进行多线程处理;例如,在单独的线程中执行一些 Vector3 数学运算是没有问题的。但是,只要任务需要访问 Unity 主线程之外的 transform 或 GameObject,程序就会抛出异常。

让我们来看一个例子。以下代码的目的是改变 GameObject 的缩放,并使用async/await在另一个线程中执行操作:

using System.Threading.Tasks;
using UnityEngine;
public class AsyncExceptionTest : MonoBehaviour
{
    private async void Start()
    {
        await ScaleObjectAsync();
    }
    private async Task<Vector3> ScaleObjectAsync()
    {
        return await Task.Run(() => transform.localScale = new
          Vector3(2, 2, 2));
    }
}

将此脚本附加到场景中的 GameObject 上,然后在 Unity 编辑器中点击播放按钮来运行脚本。操作的结果是 GameObject 的缩放没有改变,并抛出了UnityException: get_transform 只能从主线程调用异常,如下面的截图所示:

图 9.8 – 异常

图 9.8 – 异常

因此,你应该注意这一点,不要从 Unity 的主线程以外的线程访问变换或 GameObject。

正如我们之前提到的,我们可以在单独的线程中进行数学运算。因此,为了使之前的代码正确运行,我们可以在不同的线程中计算缩放值,访问 Transform 组件,并在 Unity 的主线程中修改其 localScale 属性:

    private async Task ScaleObjectAsync()
    {
        var newScale = Vector3.zero;
        await Task.Run(() => newScale = CalculateSize());
        transform.localScale = newScale;
    }
    private Vector3 CalculateSize()
    {
        Debug.Log("Threads");
        return new Vector3(2, 2, 2);
    }

这次,一切都很顺利,如果我们再次查看 Unity Profiler,我们可以在脚本线程部分找到这些线程的时间线,如下面的截图所示:

图 9.9 – 脚本线程

图 9.9 – 脚本线程

然而,作为一名开发者,即使在 C# 中编写线程安全和高效的代码也仍然存在许多挑战,如下所示:

  • 编写线程安全代码很难。

  • 竞态条件,其中计算的输出取决于两个或多个线程被调度的顺序。

  • 不高效的上下文切换;切换线程时耗时。

Unity 中的 C# 作业系统是一个专注于解决这些挑战的解决方案,以便我们可以享受多线程带来的好处。接下来,让我们探索如何在我们的 Unity 项目中使用 C# 作业系统。

与 C# 作业系统一起工作

作业系统最初是 Unity 引擎的内部线程管理系统,但随着开发者对 Unity 中多线程编程需求的增长,Unity 引入了 C# 作业系统,这使得开发者能够在 C# 脚本中轻松地编写多线程并行处理代码,以提高游戏性能。游戏开发者不需要自己实现复杂的线程池来确保每个线程正常运行。C# 作业系统与 Unity 的原生作业系统集成,C# 脚本代码和 Unity 引擎的 C++ 代码共享线程。

这种合作形式允许游戏开发者以作业系统所需的方式编写代码;Unity 引擎为游戏开发者处理多线程,开发者不再需要担心编写多线程代码时可能遇到的问题,因为 C# 作业系统不会创建任何托管线程,而是使用 Unity 的多核工作线程,给它们分配任务,这些任务在 Unity 中被称为作业

安装 Jobs 包

为了在项目中安装和启用作业系统,您首先需要安装Jobs包,如下面的截图所示:

图 9.10 – Jobs 包

图 9.10 – 作业包

然而,作业包目前仍然处于预览状态,如前面的屏幕截图所示,Unity 包管理器默认不显示预览状态的包。因此,如果您找不到作业包,则需要按照以下步骤操作以允许显示预览状态的包:

  1. 通过单击 Unity 编辑器工具栏中的编辑 | 项目设置…项打开项目设置窗口,如图以下屏幕截图所示:

图 9.11 – 打开项目设置窗口

图 9.11 – 打开项目设置窗口

  1. 接下来,点击左侧分类列表中的包管理器项以打开包管理器设置面板。

图 9.12 – 打开包管理器设置

图 9.12 – 打开包管理器设置

  1. 在下面的屏幕截图中,您可以看到启用预览包选项默认未选中。让我们检查它以在 Unity 包管理器中启用预览包。

图 9.13 – 启用预览包

图 9.13 – 启用预览包

完成后,您应该能够找到作业包并将其安装到您的项目中。

接下来,让我们通过一个示例来了解如何使用作业系统来提高游戏性能。

如何使用 C# 作业系统

在本例中,我们将首先使用 Unity 的传统方式,即 GameObject+Components 方式,在游戏场景中创建 10,000 个卡通汽车,每个汽车包含一个用于移动的组件。

图 9.14 – 汽车模型

图 9.14 – 汽车模型

本例中使用的汽车模型来自 Unity 资产商店,您可以从这里下载它们:assetstore.unity.com/packages/3d/vehicles/land/mobile-toon-cars-free-99857。然后,按照以下步骤操作:

  1. 让我们创建我们的第一个 C# 脚本,命名为CarSpawner,以在场景中生成汽车。在这个脚本中,我们可以通过按空格键从汽车预制体创建 10,000 个新的汽车实例。正如您在以下代码中可以看到的,在Update方法内部,我们使用Input.GetKeyDown(KeyCode.Space)方法来检查是否按下了空格键。如果按下了空格键,则调用CreateCars方法来创建新的汽车实例:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    public class CarSpawner : MonoBehaviour
    {
    [SerializeField] 
    private List<GameObject> _carPrefabs;
    [SerializeField] 
    private float _rightSide, _leftSide, _frontSide,
      _backSide;
        private void Update()
        {
            if(Input.GetKeyDown(KeyCode.Space))
            {
                CreateCars(10000);
            }
        }
        private void CreateCars(int count)
        {
            for(var i = 0; i < count; i++)
            {
                var posX = Random.Range(_rightSide,
                  _leftSide);
                var posZ = Random.Range(_frontSide,
                  _backSide);
                var pos = new Vector3(posX, 0f, posZ);
                var rot = Quaternion.Euler(0f, 0f, 0f);
                int index = Random.Range(0,
                  _carPrefabs.Count);
                var carPrefab = _carPrefabs[index];
                var carInstance = Instantiate(carPrefab, pos,
                  rot);
            }
        }
    }
    
  2. 接下来,我们还需要另一个脚本,该脚本将附加到每个汽车对象上以移动它们。如您所见,这个Movement脚本相对简单;它将 GameObject 向前移动:

    using UnityEngine;
    public class Movement : MonoBehaviour
    {
    [SerializeField] 
    private float _speed;
        private void Update()
        {
            transform.position += transform.forward *
              _speed * Time.deltaTime;
        }
    }
    
  3. 然后,将此Movement脚本附加到汽车预制体上。

图 9.15 – 汽车预制体

图 9.15 – 汽车预制体

  1. 在 Unity 编辑器中点击播放按钮运行示例,并按空格键在场景中生成 10,000 辆车。如图所示,当场景中有 10,000 辆车时,每秒帧数FPS)的值大约为 12

![图 9.16 – FPS]

图片

图 9.16 – FPS

  1. 我们可以查看此示例的 CPU 使用时间线。按 Ctrl + 7 或点击 Movement.Update,以查看这 10,000 辆车的移动操作是在主线程上执行,而作业工作者处于空闲状态。

![图 9.17 – CPU 时间线]

图片

图 9.17 – CPU 时间线

显然,当我们看到所有逻辑都在主线程上执行时,作为游戏开发者,我们肯定希望能够在其他线程上运行一些操作。然而,在我们开始编写实际代码之前,我们应该稍微了解一下如何在 Unity 中编写 job 化代码。

在 Unity 的 Job 系统中,每个作业都可以看作是一个方法调用。当编写一个新的作业时,你必须遵循以下这些要点:

  • 为了确保数据在内存中连续分布,并减少 垃圾回收GC)压力,作业必须是一个值类型,这意味着它必须是一个结构体,而不是一个类。

  • 一个新的作业结构需要实现 IJob 接口。IJob 接口有许多变体,例如 IJobParallelForIJobParallelForBatchIJobParallelForTransform。在实现这些接口时,我们需要实现 Execute 方法。值得注意的是,实现不同的 IJob 接口变体时,Execute 方法所需的参数不同,这使我们能够处理不同的场景。例如,一个实现 IJobParallelForTransform 接口的新 job 可以并行访问转换数据,例如位置、旋转和缩放数据。

以下是一个实现 IJobParallelFor 接口的示例作业的代码:

using Unity.Jobs;
public struct SampleJob : IJobParallelFor
{
    public void Execute(int index)
    {
        throw new System.NotImplementedException();
    }
}

我们已经创建了一个新的作业,但如何让它工作呢?嗯,我们必须安排它。通常,安排作业非常简单。以下代码演示了如何安排它:

SampleJob job = new SampleJob();
JobHandle handle = job.Schedule();
handle.Complete();

我们已经介绍了一些关于如何创建新作业以及如何让它工作的基础知识。现在,让我们使用 Job 系统重写 Movement 脚本来将移动车辆的操作分配到不同的线程上运行:

  1. 首先,让我们创建一个移动车辆的作业。您可以在下面的新 MotionJob 脚本中找到它。MotionJob 是一个结构体而不是类,并实现了 IJobParallelForTransform 接口,因此这个作业可以访问位置数据并对其进行修改:

    using UnityEngine;
    using UnityEngine.Jobs;
    public struct MotionJob : IJobParallelForTransform
    {
        public float Speed, DeltaTime;
        public Vector3 Direction;
        public void Execute(int index, TransformAccess
     transform)
        {
            transform.position += Direction * Speed *
              DeltaTime;
        }
    }
    
  2. 接下来,我们需要另一个名为 JobsManager 的脚本,用于创建作业,为其提供转换数据(具体来说,在脚本中,我们使用 TransformAccessArray 结构来提供这些数据),并安排它。此外,此脚本与之前的 CarSpawner 脚本类似。它检查空格键是否被按下,如果按下空格键,则在游戏场景中创建 10,000 辆车。首先,让我们看看如何在 Unity 的 Job 工作线程上创建和安排一个作业。在 Update 方法中,我们创建一个新的 MotionJob 对象,并向它传递数据,例如 deltaTimespeeddirection 以创建一个新的作业,然后我们调用 _motionJob.Schedule 将作业分配到不同的线程:

    using UnityEngine;
    using UnityEngine.Jobs;
    using Unity.Jobs;
    using System.Collections.Generic;
    public class JobsManager : MonoBehaviour
    {
    [SerializeField] 
    private List<GameObject> _carPrefabs;
    [SerializeField] 
    private float _rightSide, _leftSide, _frontSide,
      _backSide, _speed;
        private TransformAccessArray _transArrays;
        private JobHandle _jobHandle;
        private MotionJob _motionJob;
        private void Start()
        {
            _transArrays = new
              TransformAccessArray(10000);
            _jobHandle = new JobHandle();
        }
        private void Update()
        {
            _jobHandle.Complete();
            if(Input.GetKeyDown(KeyCode.Space))
            {
                CreateCars(10000);
            }
            // Create the Job
            _motionJob = new MotionJob()
            {
                DeltaTime = Time.deltaTime,
                Speed = _speed,
                Direction = Vector3.forward
            };
            // Provide the transform data and schedule the
               Job.
            _jobHandle = _motionJob.Schedule(_transArrays);
        }
    
  3. 接下来,让我们看看如何在代码中创建车辆。由于这次我们只需要这些车辆的位置数据,在 CreateCars 方法中,我们将汽车的转换数据添加到 TransformAccessArray 中,以便我们刚刚创建的作业可以访问 TransformAccessArray 以获取这些转换数据。CreateCars 方法如下:

        private void CreateCars(int count)
        {
            _jobHandle.Complete();
            _transArrays.capacity = _transArrays.length +
              count;
            for (var i = 0; i < count; i++)
            {
                var posX = Random.Range(_rightSide,
                  _leftSide);
                var posZ = Random.Range(_frontSide,
                  _backSide);
                var pos = new Vector3(posX, 0f, posZ);
                var rot = Quaternion.Euler(0f, 0f, 0f);
                int index = Random.Range(0,
                  _carPrefabs.Count);
                var carPrefab = _carPrefabs[index];
                var carInstance = Instantiate(carPrefab,
                  pos, rot);
                _transArrays.Add(carInstance.transform);
            }
    }
    
  4. 这次,我们不再需要在运行时将移动组件附加到每辆车的实例上来移动车辆,因此我们需要移除之前附加到汽车预制体上的 移动 组件。

图 9.18 – 移除移动组件

图 9.18 – 移除移动组件

  1. 在 Unity 编辑器中点击 播放 按钮来运行示例,并按空格键在场景中生成 10,000 辆车。如图所示,当场景中有 10,000 辆车时,这次帧率值约为 19。在一个有 10,000 辆车移动的场景中,游戏的帧率几乎翻倍:

图 9.19 – 帧率

图 9.19 – 帧率

  1. 让我们按 Ctrl + 7 或在 Unity 编辑器的工具栏中点击 窗口 | 分析 | 性能分析器 项来打开 性能分析器 窗口,查看这次 CPU 使用的时间线。在这里,我们可以看到 MotionJob 在 Unity 中分布在多个 Job 工作线程上,而不是在主线程上运行。

图 9.20 – 在 Job 工作线程上运行

图 9.20 – 在 Job 工作线程上运行

通过这个示例,我们看到了如何使用 Unity 中的 Job 系统来提高游戏的运行性能。

在本节中,我们讨论了与在 Unity 中使用异步编程相关的话题。接下来,我们将讨论 DOTS 中的另一个重要话题——即,ECS。

在 Unity 中使用 ECS

Unity 始终围绕组件的概念展开;例如,我们可以向 GameObject 添加一个 Movement 组件,使对象可以移动。我们还可以向 GameObject 添加一个 Light 组件,使其发出光线。我们还可以添加 AudioSource 组件,使 GameObject 可以发出声音。在这种情况下,GameObject 是一个容器,游戏开发者可以将其附加不同的组件以提供不同的行为。我们可以称这种架构为GameObject-Components关系。在这个架构中,我们使用传统的 OOP 编程范式来编写组件,将数据和行为耦合在一起。在上一节面向对象设计模式与 DOTS中,我们也讨论了 OOP 对游戏性能的影响。

因此,为了解决这些问题,Unity 引入了 ECS,允许开发者在 Unity 中编写面向数据的代码。在 ECS 中,数据和行为是分离的,这可以大大提高内存使用效率,从而提高性能。

注意

这里所说的所谓行为,具体来说,是方法

如其名称所示,ECS 由三部分组成,即以下内容:

  • Entity

  • Component

  • System

我们将在以下各节中分别介绍它们。

Entity

当使用 ECS 时,我们更多地谈论实体,而不是 GameObject。你可能会认为实体和 GameObject 之间没有太大区别,因为你可能会将实体视为组件的容器,就像 GameObject 一样。然而,情况并非如此。实体只是一个整数 ID。它既不是对象,也不是容器。它的功能是将其组件的数据关联起来。

EntityManager 和 World

如果你想在你的 C#代码中创建新的实体,Unity 提供了EntityManager类来管理实体,你可以使用它来创建实体、更新实体和销毁实体。ECS 使用World类来组织实体,并且在一个World中只能存在一个EntityManager实例。

当我们默认点击World时,因此我们可以通过以下代码获取默认World中存在的EntityManager

var entityManager =
  World.DefaultGameObjectInjectionWorld.EntityManager;

Archetypes

ECS 将内存中具有相同组件集的所有实体组合在一起。ECS 将这种类型的组件集称为EntityManager,以创建一个包含一组组件的 Archetype:

ComponentType[] types;
var archetype = entityManager.CreateArchetype(types);

NativeArray

毫无疑问,我们还需要一个数组来存储新创建的实体。但在 ECS 中,我们将使用与.NET 编程中的传统数组不同的容器,即NativeArray

NativeArray提供了一个 C#包装器,用于访问原生内存,以便游戏开发者可以直接在托管和原生内存之间共享数据。因此,对NativeArray的操作不会像.NET 中的常见数组那样生成 GC 的托管内存,并且需要元素是值类型,即结构体。以下伪代码显示了如何创建一个新的NativeArray并创建新的实体:

var entityArray = new NativeArray<Entity>(count,
  Allocator.Temp);
entityManager.CreateEntity(entityArchetype, entityArray);

Component

在 ECS 中,也存在组件,但 ECS 中的组件与之前在谈论 GameObject-Components 关系时提到的 Movement "组件"是不同的概念。在 ECS 引入之前,我们通常将附加到 GameObject 上的MonoBehaviour视为组件。MonoBehaviour包含数据和行为。ECS 的不同之处在于实体和组件没有任何行为逻辑;它们只包含数据,逻辑操作将由 ECS 中的系统处理。

一个组件必须是一个结构体而不是一个类,并且需要实现以下接口之一:

  • IComponentData

  • ISharedComponentData

  • IBufferElementData

  • ISystemStateComponentData

  • ISharedSystemStateComponentData

IComponentData接口是常用的。以下使用它作为示例来展示如何在 ECS 中创建一个新的组件:

using Unity.Entities;
public struct SampleComponent : IComponentData
{
    public int Value;
}

如果您尝试将此SampleComponent添加到场景中的 GameObject,您会发现您不能这样做,因为它没有继承自MonoBehaviour类。但是,您可以将[GenerateAuthoringComponent]属性添加到您的组件中,将其标记为作者组件,如下所示:

using Unity.Entities;
[GenerateAuthoringComponent]
public struct SampleComponent : IComponentData
{
    public int Value;
}

即使不继承自 MonoBehaviour,作者组件也可以添加到 GameObject 中。

系统

我们已经知道,在使用 ECS 时,数据和行为是解耦的。在 ECS 中,所有逻辑都由系统处理,它接受一组实体并根据分组实体中包含的数据执行请求的行为。正如我们所知,使用 ECS 可以使我们的代码高效地访问内存,实际上,ECS 中的系统还可以与 C# Job System 结合使用,以有效地利用多线程并进一步提高游戏性能。

我们可以在 ECS 中创建一个新的系统。以下代码是一个示例:

using Unity.Entities;
public class SampleSystem : SystemBase
{
    protected override void OnUpdate()
    {
        Entities.ForEach((ref SampleComponent sample) =>
        {
            sample.Value = -1;
        }).
        ScheduleParallel();
    }
}

在这个例子中,这个新的SampleSystem继承自SystemBase类,在OnUpdate中有一个ScheduleParallel Lambda 函数Entites.ForEach循环用于使用 C# Job System 调度工作到 Unity 的 Job 工作线程。

通过这些简要介绍,我相信您已经对 ECS 有一个大致的了解。接下来,让我们在我们的项目中安装 ECS。

安装 Entities 和 Hybrid Renderer 包

为了在您的项目中安装和启用 ECS,您首先需要安装Entities包,如下面的截图所示:

![图 9.21 – Entities 包

![img/Figure_9.21_B17146.jpg]

图 9.21 – Entities 包

如前一个截图所示,Entities包也处于预览状态。尽管我们在前面的子节中检查了启用预览包选项,但包管理器仍然没有显示此包。这是因为从 Unity 2020.1 开始,此包不再托管在 Unity Registry 上,而是托管在 GitHub 上,因此我们需要遵循以下步骤来安装它:

  1. 可以通过点击 Unity 编辑器工具栏中的窗口 | 包管理器项来打开包管理器窗口,如下截图所示:

![图 9.22 – 打开包管理器窗口图片

图 9.22 – 打开包管理器窗口

  1. 点击左上角的+按钮以添加来自其他来源的包。

![图 9.23 – 从其他来源添加包图片

图 9.23 – 从其他来源添加包

  1. 点击com.unity.entities,然后进入并点击添加按钮。

![图 9.24 – 添加 Entities 包图片

图 9.24 – 添加 Entities 包

  1. 然后,等待包安装完成。

![图 9.25 – 安装 Git 包图片

图 9.25 – 安装 Git 包

完成后,你应该能在你的项目中找到已安装的Entities包。

有时我们还需要另一个包,即混合渲染器包。此包帮助我们渲染 ECS 实体。

安装com.unity.rendering.hybrid的过程。

![图 9.26 – 安装混合渲染器包图片

图 9.26 – 安装混合渲染器包

等待包安装完成,然后你会在项目中找到它已安装。

![图 9.27 – 混合渲染器包图片

图 9.27 – 混合渲染器包

接下来,我们将使用之前的例子来了解如何使用 ECS 结合 C# Job System 的使用来进一步提高基于 C# Job System 的游戏性能。

如何使用 ECS

在这个例子中,我们将创建一个新的组件,实体,以及一个新的系统,并使用 C# Job System 将工作分配给 Unity 的 Job 工作线程。让我们开始吧!

  1. 首先,我们将为数据创建一个组件脚本。在这种情况下,是汽车的速度:

    using Unity.Entities;
    public struct CarSpeed : IComponentData
    {
        public float SpeedValue;
    }
    
  2. 接下来,我们还需要一个名为CarsManager的正常脚本,以访问World中的EntityManager对象来创建原型和实体。在这里,我们将向这些实体添加一些 ECS 预制的组件,例如Translation,它只包含实体位置数据,以及RenderMesh,它包含实体图形属性数据:

    using UnityEngine;
    using Unity.Collections;
    using Unity.Mathematics;
    using Unity.Entities;
    using Unity.Rendering;
    using Unity.Transforms;
    using Random = UnityEngine.Random;
    public class CarsManager : MonoBehaviour
    {
    [SerializeField] 
    private Mesh _mesh;
    [SerializeField] 
    private Material _material;
    [SerializeField] 
    private int _count = 10000;
    [SerializeField] 
    private float _rightSide, _leftSide, _frontSide,
      _backSide, _speed;
        private void Start()
        {
            var entityManager =
              World.DefaultGameObjectInjectionWorld
              .EntityManager;
            // Create entity achetype
            var entityArchetype =
              entityManager.CreateArchetype(
                typeof(CarSpeed),
                typeof(Translation),
                typeof(LocalToWorld),
                typeof(RenderMesh),
                typeof(RenderBounds));
            var entityArray = new
              NativeArray<Entity>(_count, Allocator.Temp);
    
            // Create entities
            entityManager.CreateEntity(entityArchetype,
              entityArray);
            for (int i = 0; i < entityArray.Length; i++)
            {
              var entity = entityArray[i];
              entityManager.SetComponentData(entity, new
                CarSpeed { SpeedValue = 1f });
              entityManager.SetComponentData(entity, new
                Translation { Value = new
                float3(Random.Range(_rightSide,
                _leftSide),0,
                Random.Range(_frontSide, _backSide)) });
         entityManager.SetSharedComponentData(entity, new
           RenderMesh
                {
                    mesh = _mesh,
                    material = _material
                });
            }
            entityArray.Dispose();
            _information.CarCounts = _count;
        }
    }
    
  3. 然后,将此CarsManager脚本附加到场景中的 GameObject 上,并分配适当的属性,例如汽车的网格和速度值。

![图 9.28 – CarsManager 对象图片

图 9.28 – CarsManager 对象

  1. 到目前为止,我们已经设置了组件和实体。接下来要做的就是创建系统。系统也是处理游戏逻辑的地方。在这个例子中,我们将使用系统来移动这些汽车。正如您在下面的代码中所看到的,与在传统的 Update 方法中搜索组件然后在每个实例上运行时操作相比,使用 ECS,我们只需静态声明我们需要处理所有带有 TranslationCarSpeed 组件的实体。要找到所有这些实体,我们只需找到匹配特定“组件集”的原型,这是由系统完成的:

    using Unity.Entities;
    using Unity.Transforms;
    public class CarMotionSystem : SystemBase
    {
        protected override void OnUpdate()
        {
            var deltaTime = Time.DeltaTime;
            Entities.ForEach((ref Translation translation,
              ref CarSpeed carSpeed) =>
            {
                translation.Value.z += carSpeed.SpeedValue
                  * deltaTime;
            }).
            ScheduleParallel();
        }
    }
    
  2. 在 Unity 编辑器中点击播放按钮来运行示例。如图所示,当场景中有 10,000 辆汽车时,这次 FPS 的值约为 260!在这个有 10,000 辆移动汽车的场景中,使用 ECS 相比于原始的传统实现,将游戏的帧率提高了近 30 倍:

图 9.29 – 使用 ECS 提高游戏性能

图 9.29 – 使用 ECS 提高游戏性能

  1. 如果我们查看这个游戏场景的层次结构面板,我们不会在列表中看到任何汽车对象。这是因为当使用 ECS 时,不会创建传统的 GameObject 和传统组件,而是使用 ECS 的实体和组件来组织数据。

图 9.30 – 没有创建任何 GameObject

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_9.30_B17146.jpg)

图 9.30 – 没有创建任何 GameObject

  1. 为了查看场景中使用的实体、组件和系统,我们可以使用实体调试器来查看这些信息。通过从 Unity 编辑器的工具栏中点击窗口 | 分析 | 实体调试器项,我们可以打开实体调试器窗口。

图 9.31 – 打开实体调试器窗口

图 9.31 – 打开实体调试器窗口

  1. 在实体调试器窗口中,我们可以看到实体列表以及系统列表。如图所示,有 10,002 个实体,包括 10,000 个汽车实体:

图 9.32 – 实体调试器

图 9.32 – 实体调试器

  1. 如果我们在实体列表中选择一个实体,该实体的检查器窗口将打开,显示该实体的所有组件及其数据。

图 9.33 – 实体检查器窗口

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_9.33_B17146.jpg)

图 9.33 – 实体检查器窗口

  1. 最后,让我们在性能分析器窗口中查看 CPU 使用情况的时间线。如果您忘记了如何打开此窗口,只需按 Ctrl + 7 或在 Unity 编辑器工具栏中点击窗口 | 分析 | 性能分析器。在这里,我们可以看到,正如我们预期的那样,C# 作业系统将 ECS 工作分配到多个作业工作线程:

图 9.34 – ECS 和作业系统

图 9.34 – ECS 和作业系统

通过前面的步骤,我们将 Unity 中的传统 GameObject-Components 风格开发方法更改为使用 ECS 的开发方法,采用面向数据的设计方法,并使用 C# Job 系统,充分利用多线程编程,从而提高游戏的运行效率。

接下来,让我们讨论 DOTS 中的另一项技术,即 Burst 编译器。

使用 C# 和 Burst 编译器

Unity 中的 Burst 编译器 是一种高级编译技术,可以将 .NET 代码的一个子集转换为针对 Unity 游戏高度优化的本地代码。需要注意的是,它不是一个通用编译器,而是一个为 Unity 设计的编译器,旨在使 Unity 游戏运行得更快。

Burst 在 C# 的一个子集 HPC# 上工作,因此让我们接下来探索这个 C# 子集。

高性能 C# (HPC#)

HPC# 是 C# 的一个子集。标准 C# 语言使用“堆上的对象”的概念,并使用垃圾回收器自动回收未使用的内存。因此,作为开发者,我们无法控制数据在内存中的分配方式。另一方面,HPC# 不支持引用类型,即类,以避免在堆上进行分配并禁用垃圾回收器。此外,一些函数,如 try-catch-finally,在 HPC# 中也不受支持。

总结来说,我们可以在 HPC# 中使用以下类型:

  • 值类型,如 int、float、bool 和 char,枚举类型和结构体类型

  • Unity 中的 NativeArray

启用 Burst 编译器

Burst 编译器通常与 Unity 中的 C# Job 系统一起使用以优化作业的代码。正如我们所知,作业是一个值类型结构体,因此它非常适合与 Burst 编译器一起使用。在作业中启用它非常简单:只需将 [BurstCompile] 属性添加到作业结构体中,如下面的代码所示:

using Unity.Jobs;
using Unity.Burst;
[BurstCompile]
public struct SampleJobWithBurst : IJobParallelFor
{
    public void Execute(int index)
    {
        throw new System.NotImplementedException();
    }
}

如果您也想在 Unity 编辑器中启用 Burst 编译器,您可以在工具栏中的 作业 | Burst 找到相应的设置。

Figure 9.35 – Settings of Burst

Figure 9.35 – Settings of Burst

图 9.35 – Burst 设置

通过阅读本节,您应该了解 Burst 编译器和 HPC# 是什么。您还应该知道 Burst 编译器通常与 Unity 中的 C# Job 系统一起使用,以及如何在作业代码中启用它以生成更高效的本地二进制代码。

摘要

本章首先介绍了数据导向设计是什么,以及数据导向设计与传统面向对象设计的区别。然后,我们探讨了 Unity 中的 DOTS 以及构成它的三个技术模块,即 C# Job 系统、ECS 和 Burst 编译器。

之后,我们详细讨论了如何在 Unity 中实现异步编程,并使用示例演示了如何使用 Unity 的 C# Job 系统实现多线程以提高游戏性能。

我们还介绍了 ECS 的概念,讨论了 ECS 与 Unity 中传统的 GameObject-Components 架构之间的区别,并演示了如何使用 ECS 和 C# Job 系统来进一步提高游戏性能。

最后,我们探讨了 Burst 编译器和 HPC#是什么,以及如何使它们为您的 Unity 游戏生成高度优化的本地代码。

通过阅读本章,您现在应该已经理解了如何在 Unity 中正确地使用 DOTS。在下一章中,我们将讨论与 Unity 中的资源管理和序列化相关的话题。

第十章:第十章:Unity 和 Azure 中的序列化系统与资产管理

在上一章,第九章在 Unity 中使用面向数据的技术堆栈,我们学习了什么是面向数据的技术堆栈以及如何使用这项技术利用多核处理器来提高您游戏的表现力。在本章中,我们将介绍 Unity 开发中的其他一些重要主题,即 Unity 中的 序列化资产管理。通常,一个游戏不仅包含代码,还由许多不同类型的资产组成,如模型、纹理和音频。因此,了解 Unity 中的序列化系统以及资产工作流程可以帮助您更好地使用 Unity 开发游戏。

在本章的最后部分,我们还将探讨一个有趣的话题——如何使用 Azure 云存储服务来托管 Unity 游戏的内容,并通过使用 Unity 的 可寻址资产系统从 Azure 云将内容加载到 Unity 游戏中。

以下关键主题将包含在我们的学习路径中:

  • Unity 中的序列化系统

  • Unity 中的资产工作流程

  • 介绍 Unity 中的特殊文件夹

  • 使用 Unity 的可寻址资产系统与 Azure Blob 存储结合

到本章结束时,您不仅将了解 Unity 中的序列化系统和资产管理,还将熟悉 Azure 云存储服务。

听起来很激动!

技术要求

由于本章将涵盖 Azure 的存储账户服务,如果您没有可用的 Azure 账户,我建议您在开始本章之前先设置一个免费的 Azure 试用账户。您可以通过以下链接创建一个带有 200 美元信用额的免费 Azure 试用账户:

azure.microsoft.com/en-us/free/

![Figure 10.1 – Microsoft Azure 页面img/Figure_10.01_B17146.jpg

Figure 10.1 – Microsoft Azure 页面

现在,让我们开始吧!

Unity 中的序列化系统

在开发游戏时,添加可靠的内容保存和加载功能是开发过程中的一个关键部分。如果您使用的是游戏引擎编辑器,例如 Unity 引擎编辑器,您还需要一些常见的编辑器功能,如撤销、保存编辑器设置等。所有这些,无论是游戏在运行时保存或加载内容,还是开发者使用编辑器开发游戏,都是建立在 序列化基础上的。

Unity 的序列化系统是什么?

那么,什么是 反序列化

在 Unity 中,有三种序列化格式,即以下内容:

  • 二进制序列化

  • YAML 序列化

  • JSON 序列化

Unity 中的 YAML 和二进制序列化

Unity 创建的资产,如 场景预制体,默认将保存为 YAML 格式。例如,如果我们在这个章节中打开场景,即 Chapter10.unity,在文本编辑器如 OcclusionCullingSettingsRenderSettings 中。如果你向下滚动,你还可以找到这个场景中包含的 GameObjects 和组件。

![图 10.2 – YAML 格式的场景图片

图 10.2 – YAML 格式的场景

图 10.2 所示,毫无疑问,YAML 格式是可读的,并且使得版本控制工具易于使用。然而,YAML 是一种基于文本的格式,因此你也可以选择使用二进制序列化来更有效地使用空间并提高安全性。让我们执行以下步骤来设置 Unity 的序列化模式:

  1. 通过点击 Unity 编辑器工具栏中的 编辑 | 项目设置... 项来打开 项目设置 窗口,如下面的截图所示:

![图 10.3 – 打开项目设置窗口图片

图 10.3 – 打开项目设置窗口

  1. 接下来,点击设置面板,如 图 10.4 所示:

![图 10.4 – 编辑器设置面板图片

图 10.4 – 编辑器设置面板

  1. 资产序列化 部分,我们可以看到默认的 模式 选项是 强制文本。在这种模式下,所有由 Unity 创建的资产都将使用 YAML 格式进行序列化。如果你使用像 Git 这样的版本管理工具,这也是推荐的设置,因为使用纯文本序列化通常可以避免无法解决的合并冲突。如 图 10.5 所示,在下拉窗口中,我们可以选择 强制二进制 模式将所有资产转换为二进制格式,我们还可以选择 混合 模式选项以保留当前资产的序列化格式;也就是说,以二进制格式序列化的资产仍然是二进制格式,而使用 YAML 格式序列化的资产仍然是 YAML 格式。然而,新创建的资产将以二进制格式进行序列化。

![图 10.5 – 资产序列化模式图片

图 10.5 – 资产序列化模式

  1. 在这里,我们可以选择 强制二进制 模式,并在我们的文本编辑器中再次检查相同的场景文件。场景文件已转换为二进制格式,如下面的截图所示:

![图 10.6 – 二进制格式的场景文件图片

图 10.6 – 二进制格式的场景文件

如我们之前提到的,序列化也是实现 Unity 编辑器的一个重要部分。不仅 Unity 创建的资产,如游戏场景,会被 Unity 序列化,Unity 编辑器中的各种设置也会被 Unity 序列化。

在项目根目录中,我们可以找到 ProjectSettings 文件夹,这是在创建项目时由 Unity 编辑器自动创建的,如 图 10.7 所示:

图 10.7 – ProjectSettings 文件夹

图 10.7 – ProjectSettings 文件夹

双击此文件夹以打开它。我们可以在其中找到当前项目的所有设置文件。

图 10.8 – ProjectSettings 文件夹中的设置文件

图 10.8 – ProjectSettings 文件夹中的设置文件

接下来,我们仍然使用文本编辑器打开一个设置文件,例如 GraphicsSettings.asset,并分别使用 Unity 的二进制序列化模式和文本序列化模式来序列化此文件。图 10.9 展示了以二进制格式序列化的设置文件:

图 10.9 – 二进制格式的设置文件

图 10.9 – 二进制格式的设置文件

另一方面,你可以在 图 10.10 中看到以 YAML 格式序列化的设置文件:

图 10.10 – YAML 格式的设置文件

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_10.10_B17146.jpg)

图 10.10 – YAML 格式的设置文件

到目前为止,我们已经讨论了 Unity 的二进制序列化和基于文本的 YAML 序列化,但我们还没有涵盖 Unity 提供的 JSON 序列化。接下来,让我们看看 Unity 中的 JSON 序列化。

Unity 中的 JsonUtility 类和 JSON 序列化

如果你之前有开发 .NET 项目的经验,你可能对 JSON 序列化很熟悉。你可以选择 .NET 提供的解决方案,例如使用 System.Runtime.Serialization.Json 命名空间中定义的 DataContractJsonSerializer 类或使用 System.Text.Json 命名空间中定义的 JsonSerializer 类,还有来自开源社区的解决方案,例如 Newtonsoft.Json,这是一个非常流行的 .NET JSON 框架。Unity 还为游戏开发者提供了 Unity 开发中的 JSON 序列化功能,即 JsonUtility 类。我们可以调用 JsonUtilityToJson 方法将对象序列化为 JSON 字符串,反之,JsonUtilityFromJson 方法可以将 JSON 字符串反序列化为对象。接下来,让我们看看如何在 Unity 中使用 JsonUtility 类的示例:

  1. 通过在 项目 窗口中点击 创建 | 文件夹 项,创建一个名为 Scripts 的新文件夹。

图 10.11 – 创建 Scripts 文件夹

图 10.11 – 创建 Scripts 文件夹

  1. 双击 Scripts 文件夹以进入它,然后在此文件夹中创建一个新的 C# 脚本,命名为 PlayerData,并将以下内容添加到该脚本中。PlayerData 结构用于存储玩家的数据,稍后该结构的一个对象将被序列化为 JSON 字符串。你应该注意,结构体或类的字段应该是 public 的;否则,Unity 序列化器将忽略这些字段:

    public struct PlayerData
    {
        public string Name;
        public int Age;
        public float HP;
        public float Attack;
        public PlayerData(string name, int age, float hp,
          float attack)
        {
            Name = name;
            Age = age;
            HP = hp;
            Attack = attack;
        }
    }
    
  2. 接下来,我们还需要在同一个文件夹中创建另一个 C#脚本,并将其命名为JSONSerializationSampleJSONSerializationSample中的代码如下。在Start方法中,我们创建一个新的PlayerData对象,并为其字段赋值,然后调用JsonUtility.ToJson方法将此对象序列化为 JSON 字符串,并将字符串打印到控制台窗口:

    using UnityEngine;
    public class JSONSerializationSample : MonoBehaviour
    {
        private void Start()
        {
            var playerData = new PlayerData("player1", 50,
              100, 100);
            var jsonString =
              JsonUtility.ToJson(playerData);
            Debug.Log(jsonString);
        }
    }
    
  3. 在场景中创建一个新的 GameObject,将其JSONSerializationSample脚本附加到它上,并在编辑器中运行游戏。以下截图所示的 JSON 字符串将被打印出来:

![Figure 10.12 – JSON 字符串img/Figure_10.12_B17146.jpg

图 10.12 – JSON 字符串

  1. 将 JSON 字符串反序列化为对象相对简单;你只需要调用JsonUtility.FromJson<T>,这是一个泛型方法。如果你不了解 C#中的泛型方法,泛型方法是使用类型参数声明的。因此,让我们回到JSONSerializationSample并更新Start方法中的代码。这段代码将反序列化 JSON 字符串到一个新的对象,并且对象的Name字段将在控制台窗口中打印出来:

    using UnityEngine;
    public class JSONSerializationSample : MonoBehaviour
    {
        private void Start()
        {
            var playerData = new PlayerData("player1", 50,
              100, 100);
            var jsonString =
              JsonUtility.ToJson(playerData);
            Debug.Log(jsonString);
            var deserializedObject =
             JsonUtility.FromJson<PlayerData>(jsonString);
            Debug.Log(deserializedObject.Name);
        }
    }
    
  2. 在编辑器中运行游戏。这个玩家的名字将按以下截图所示打印出来:

![Figure 10.13 – 反序列化 JSON 字符串img/Figure_10.13_B17146.jpg

图 10.13 – 反序列化 JSON 字符串

  1. 如果你希望PlayerData作为另一个类的字段,并且想要序列化这个类,PlayerData需要标记为[System.Serializable]属性,否则,作为字段的PlayerData将无法正确序列化。因此,让我们回到PlayerData并更新代码以添加[System.Serializable]属性:

    [System.Serializable]
    public struct PlayerData
    {
       //No Change
    }
    

现在你已经知道了如何在 Unity 中使用JsonUtility类将对象序列化为 JSON 字符串,并将 JSON 字符串反序列化为对象,是时候讨论 Unity 的JsonUtility类的优缺点了。

Unity 的 JsonUtility 类的优缺点

让我们从 Unity 的JsonUtility类的优点开始。在 Unity 中使用JsonUtility类可以在序列化和反序列化 JSON 方面实现相对较高的性能。JsonUtilityToJson方法和FromJson方法内部使用 Unity 序列化器,并且对 Unity 的一些内置类型(如Vector2Vector3)有更好的支持。此外,由于它是由 Unity 游戏引擎提供的,因此无需安装额外的包。

然而,与Newtonsoft.Json等其他流行的 JSON 框架相比,JsonUtility的功能有限。两个最明显的限制是JsonUtility不支持字典的序列化,并且根元素必须是对象,而不是数组或列表。让我们看看JsonUtility类的限制示例:

  1. Scripts文件夹中创建一个新的 C#脚本,命名为TeamData,并将以下内容添加到该脚本中。如下面的代码所示,这个类有两个字段,一个PlayerData列表和一个字典:

    using System.Collections.Generic;
    public class TeamData
    {
        public List<PlayerData> Players;
        public Dictionary<string, PlayerData> Roles;
        public TeamData()
        {
            Players = new List<PlayerData>();
            Roles = new Dictionary<string, PlayerData>();
        }
    }
    
  2. 接下来,我们还需要在同一个文件夹中创建另一个 C#脚本,并将其命名为JsonUtilityLimitationsSampleJsonUtilityLimitationsSample中的代码如下。在Start方法中,我们创建一个新的TeamData对象,向Players列表中添加一个元素,并向Roles字典中添加一个键值对。然后,调用JsonUtility.ToJson方法将此对象序列化为 JSON 字符串,并将字符串打印到控制台窗口:

    using UnityEngine;
    public class JsonUtilityLimitationsSample :
      MonoBehaviour
    {
        private void Start()
        {
            var playerData = new PlayerData("player1", 50,
              100, 100);
            var teamData = new TeamData();
            teamData.Players.Add(playerData);
            teamData.Roles.Add("leader", playerData);
            var jsonStringFromTeamData =
              JsonUtility.ToJson(teamData);
            Debug.Log(jsonStringFromTeamData);
        }
    }
    
  3. 在编辑器中运行游戏;你会发现只有Players列表被序列化,但Roles字典没有按预期序列化,如下面的截图所示。这是因为JsonUtility不支持在 Unity 中序列化字典。

图 10.14 – Roles 字典未序列化

图 10.14 – Roles 字典未序列化

  1. 然后,让我们回到JsonUtilityLimitationsSample,并更新Start方法中的代码,尝试单独序列化Players列表:

    public class JsonUtilityLimitationsSample :
      MonoBehaviour
    {
        private void Start()
        {
            // No Change
            var jsonStringFromList =
              JsonUtility.ToJson(teamData.Players);
            Debug.Log(jsonStringFromList);
        }
    }
    
  2. 再次在编辑器中运行游戏,你会发现这次Players列表没有被序列化,如下面的截图所示。这是因为如果使用JsonUtility进行序列化,根元素必须是一个对象,而不是数组或列表。

图 10.15 – Players 列表未序列化

图 10.15 – Players 列表未序列化

Newtonsoft.Json 框架

在开发过程中遇到前面例子中提到的问题真是个头疼的问题,因此尝试一些其他的 JSON 框架可能也是值得的。接下来,我们将使用Newtonsoft.Json来修改前面的例子,以便TeamData类中的Roles字典和单个Players列表可以正确地序列化为 JSON 字符串:

  1. 首先,如果你的项目中没有安装Newtonsoft.Json包,你可以通过 Unity 的包管理器来安装它。你可以在工具栏中点击Window | Package Manager项来打开它。

图 10.16 – 打开包管理器

图 10.16 – 打开包管理器

  1. 然后,点击左上角的+来打开下拉菜单,并从下拉菜单中选择Add package from git URL…项。

图 10.17 – 从 git URL 添加包

图 10.17 – 从 git URL 添加包

  1. 在出现的输入框中输入com.unity.nuget.newtonsoft-json,点击Add按钮,等待包管理器安装此包。

图 10.18 – 添加 Newtonsoft.Json

图 10.18 – 添加 Newtonsoft.Json

  1. 在项目中安装包后,我们可以在 C# 脚本中使用 Newtonsoft.Json 框架,因此让我们回到 JsonUtilityLimitationsSample.cs 并更新代码:

    using UnityEngine;
    using Newtonsoft.Json;
    public class JsonUtilityLimitationsSample :
      MonoBehaviour
    {
        private void Start()
        {
          var playerData = new PlayerData("player1", 50,
            100, 100);
          var teamData = new TeamData();
          teamData.Players.Add(playerData);
          teamData.Roles.Add("leader", playerData);
          var jsonStringFromTeamData =
            JsonConvert.SerializeObject(teamData);
          Debug.Log(jsonStringFromTeamData);
          var jsonStringFromList =
            JsonConvert.SerializeObject(teamData.Players);
          Debug.Log(jsonStringFromList);
        }
    }
    

让我们按以下方式分解代码:

  • 我们使用 using 关键字添加 Newtonsoft.Json 命名空间,它提供了用于 JSON 序列化和反序列化的类和方法。

  • Start 方法中,我们将 JsonUtility.ToJson 方法替换为在 Newtonsoft.Json 命名空间中定义的 JsonConvert.SerializeObject 方法。

  1. 运行游戏。您会发现 TeamData 对象的 Roles 字典字段按预期进行了序列化,而作为根元素的 Players 列表也正确地进行了序列化。

图 10.19 – Newtonsoft.Json 正确工作

图 10.19 – Newtonsoft.Json 正确工作

在本节中,我们解释了 Unity 的序列化系统以及如何在您的 Unity 项目中使用 JSON 序列化。现在我认为您已经准备好继续探索游戏项目中资产是如何被 Unity 引擎管理的了!

Unity 中的资产工作流程

Unity 的资产工作流程是另一个非常有趣的话题,它也与序列化密切相关。那么,Unity 中的 asset 是什么呢?如果您查看一个 Unity 项目,您会在该项目的根目录下找到一个名为 Assets 的文件夹,资产就是存储在这个文件夹中的文件。

在 Unity 开发中,根据其来源,资产可以分为以下两类:

  • 导入到 Unity 中的外部资产;在这种情况下最常见的是 模型纹理音频。它们通常由第三方工具创建,例如 Maya3Ds MaxPhotoshop,然后导入到 Unity 中使用。

  • 由 Unity 本身创建的资产,例如 PrefabScene 文件。

不论是导入的资产还是由 Unity 创建的资产,Unity 对它们执行以下三个操作:

  1. Unity 将为该资产分配一个 GUID。

  2. 然后,Unity 将自动创建一个元数据文件来存储有关资产的一些附加信息,例如该资产的 GUID 和导入设置。图 10.20 展示了一个自动创建的元数据文件示例。当名为 SampleTexture 的 PNG 文件导入到 Unity 项目中时,Unity 自动创建一个名为 SampleTexture.PNG.meta 的元数据文件。

图 10.20 – 元数据文件

图 10.20 – 元数据文件

  1. 最后,Unity 将处理资产文件,将其内容转换为 Unity 的内部表示,并将内部表示存储在项目根目录下的 Library 文件夹中。我们将在介绍 Library 文件夹时详细说明。

图 10.21 – Library 文件夹

图 10.21 – Library 文件夹

在理解了 Unity 的资产工作流程之后,让我们更详细地介绍这个工作流程中涉及的三件事:GUID 和文件 ID、元文件以及 Libary 文件夹。

GUID 和文件 ID

当我们讨论 Unity 的资产工作流程时,GUID 和文件 ID 显然是一个重要的话题。这是因为无论我们使用 Unity 创建资产还是导入外部资产,Unity 都必须唯一地识别这个资产,而这个唯一值就是 GUID。文件 ID 通常与 GUID 一起使用;它不是用来识别资产,而是用来识别对象内另一个对象的引用。

现在我们对 GUID 和文件 ID 有了一个初步的了解,是时候更深入地探索 GUID 和文件 ID 了!

GUID

正如我们刚才提到的,Unity 将一个 GUID 分配给 Assets 文件夹中的每个资产,作为资产的标识符。我们可以使用文本编辑器打开与该资产关联的元文件,以在 Unity 引擎中找到该资产的 GUID。

现在我们将执行以下步骤来创建一个新的 C# 脚本作为资产,并在 Unity 中检查这个 C# 脚本的 GUID

  1. Scripts 文件夹中创建一个新的 C# 脚本,命名为 AssetSample,并将以下内容添加到该脚本中。如下代码所示,这个类有一个 Texture 字段:

    public class AssetSample : MonoBehaviour
    {
        [SerializeField]
        private Texture _texture;
    }
    
  2. 在文件资源管理器中,紧挨着 C# 脚本文件创建了一个名为 AssetSample.cs.meta 的元文件,如下截图所示:

图 10.22 – AssetSample.cs.meta 文件

图 10.22 – AssetSample.cs.meta 文件

  1. 在文本编辑器中打开 AssetSample.cs.meta 文件,你会发现这个 C# 脚本资产在 Unity 中的 GUID 是 e35f96b75211edd4bad6451a26675090,如下截图所示:

图 10.23 – 此 C# 脚本的 GUID

图 10.23 – 此 C# 脚本的 GUID

阅读完本文后,你应该知道如何在 Unity 中找到资产的 GUID;然而,文件 ID 存储在哪里,Unity 是如何使用它来创建和维护对象之间的引用的?那么,让我们通过另一个示例继续我们的旅程。

文件 ID

我们之前提到,Unity 使用 File ID 来引用对象内的另一个对象,这是在该对象内引用的对象的唯一 ID。

现在,让我们通过一个示例来学习如何找到 File IDs 以及 Unity 是如何使用 File IDs 来维护对象之间的引用关系的。在这个示例中,我们仍然将使用我们刚才创建的 AssetSample 脚本,现在让我们开始吧!

  1. 首先,在场景中创建一个新的 GameObject,命名为 AssetSampleGameObject。你已经知道,一个 Transform 组件会自动创建并附加到这个 GameObject 上,如图 图 10.24 所示:

图 10.24 – 创建 AssetSampleGameObject

图 10.24 – 创建 AssetSampleGameObject

  1. 将一个AssetSample组件附加到AssetSampleGameObject上,然后从AssetSampleTexture字段分配一个纹理。然后,将另一个AssetSample组件附加到同一个 GameObject 上;然而,这次我们将AssetSample设置为None并保存场景。

![图 10.25 – 向 GameObject 添加 AssetSample 组件图片

图 10.25 – 向 GameObject 添加 AssetSample 组件

  1. 确保您的项目资产序列化模式现在是强制文本(我们已在Unity 中的 YAML 和二进制序列化部分讨论了此主题),然后使用文本编辑器从文件资源管理器打开场景文件。您将在场景文件中看到很多内容,如下面的截图所示:

![图 10.26 – 在文本编辑器中打开场景文件图片

图 10.26 – 在文本编辑器中打开场景文件

这个文件为我们提供了大量信息,记录了场景中的 GameObject、组件和引用的资产。因此,让我们将其分解:

  • 首先,我们可以在文件中找到名为AssetSampleGameObject的 GameObject 的记录。在下面的截图中,您可以看到有三个组件附加到这个 GameObject 上,分别具有文件 ID306521988306521989306521990

![图 10.27 – AssetSampleGameObject 记录图片

图 10.27 – AssetSampleGameObject 记录

  • 如果我们搜索这三个File IDs,我们可以在文件中找到三个组件的记录 – 一个Transform组件,当 GameObject 创建时创建并附加到该 GameObject 上,以及两个MonoBehaviour组件,代表 C#脚本组件。

![图 10.28 – 文件 ID图片

图 10.28 – 文件 ID

  • 那么,文件 ID 和 GUID 之间的区别是什么?如果我们关注这两个MonoBehaviour组件,我们可以看到这两个组件的m_Script字段都引用了具有 GUIDe35f96b75211edd4bad6451a26675090的相同 C#脚本。

![图 10.29 – MonoBehaviour 组件图片

图 10.29 – MonoBehaviour 组件

因此,我们可以发现,尽管这两个组件对象引用的是同一个 C#脚本,即AssetSample,但它们是AssetSample的两个不同实例;第一个MonoBehaviour组件对象的文件 ID 是306521989,第二个MonoBehaviour组件对象的文件 ID 是306521990

此外,一个实例的_texture字段引用了一个纹理资产,而另一个实例的_texture字段没有引用任何纹理资产。

通过阅读本节,我们了解到 Unity 使用 GUID 来识别资产,使用文件 ID 来识别引用的对象。

元文件

我们已经知道,元文件记录了其关联资产在 Unity 项目中的 GUID,并且元文件还记录了该资产的导入设置。在本节中,我们将讨论那些看似不起眼但实际上非常重要的元文件。

元文件和版本管理

对于刚开始使用 Unity 的开发者来说,一个常见的错误就是没有注意到这些自动生成的元文件。一个这样的例子是在使用 Git 或其他版本控制系统来管理 Unity 项目的版本时忽略元文件。

如果您还记得上一节的内容,Unity 会为每个资产分配一个 GUID,使用这个 GUID 来识别资产,并在元文件中记录这个 GUID。

因此,如果您的版本管理系统中不包括元文件,您的 Unity 开发进度可能会受到影响。

为了说明这一点,让我们想象一个场景,当不包含元文件的 Unity 项目从远程仓库克隆到同事的本地机器上时,Unity 编辑器将重新导入这些资产,为它们分配新的 GUID 并创建元文件来存储这些信息。结果,您 Unity 项目中之前存在的对象之间的引用将不再有效。

例如,假设我们之前创建的 AssetSample C# 脚本的 AssetSample.cs.meta 元文件没有被版本管理系统管理,那么在另一台计算机上克隆并打开项目后,您将遇到如图 10.30 所示的 Script Missing 错误:

图 10.30 – 脚本缺失错误

图 10.30 – 脚本缺失错误

到目前为止,脚本实际上存在,但由于其 GUID 已经被重新生成,之前的引用关系无效。

因此,在开发 Unity 项目时,请确保元文件被您的版本管理工具管理。

元文件中的导入设置

除了存储资产的 GUID 之外,元文件还存储了该资产的导入设置。当然,本小节中将要讨论的元文件主要是指第三方软件中创建并导入到 Unity 编辑器中的资产元文件,例如模型、纹理和音频。

让我们以一个音频资产的元文件为例,看看资产的导入设置是如何保存的。

我们在这里使用的音频资产来自 Unity 的 Asset Store,您可以从这里下载它:assetstore.unity.com/packages/audio/sound-fx/weapons/ultra-sci-fi-game-audio-weapons-pack-vol-1-113047

图 10.31 – 音频包

图 10.31 – 音频包

在将音频导入 Unity 项目后,我们可以选择Ultra SF Game Audio Weapons Pack v.1文件夹中的第一个音频文件,在 Unity 编辑器中打开音频的检查器窗口,它显示了资产的导入设置。然后我们使用文本编辑器在文件夹资源管理器中打开同一音频资产的元文件,如图图 10.32所示,我们可以看到元文件中的AudioImporter对应于编辑器中的导入设置:

图 10.32 – WPN_SCI-FI_FIRE_01 音频的导入设置和元文件

图 10.32 – WPN_SCI-FI_FIRE_01 音频的导入设置和元文件

纹理资产和模型资产的导入设置也存储在其元文件中。以下截图显示了纹理和模型的导入设置:

![图 10.33 – 纹理的导入设置(左)和模型的导入设置(右)]

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_10.33_B17146.jpg)

图 10.33 – 纹理的导入设置(左)和模型的导入设置(右)

由于元文件存储了资产的导入设置,一旦我们在 Unity 编辑器中修改了资产的导入设置,相应的元文件将被更新。

导入设置通常会影响 Unity 处理这些资产的方式,因此确保导入设置可以根据项目需求进行管理非常重要。例如,在许多移动游戏项目中,我们应该检查音频导入设置中的Force To Mono选项,以减少此音频文件的内存使用。

接下来,让我们看看如何在 Unity 中通过 C#脚本管理导入设置。

资产后处理器类和导入流程

Unity 为游戏开发者提供了AssetPostprocessor类,以便将其钩入 Unity 的资产导入流程。当导入资产时,我们可以根据资产类型管理导入流程。

在以下示例中,我们将创建一个新的 C#脚本,以在 Unity 项目中所有音频文件的导入设置中启用Force To Mono选项:

  1. Scripts文件夹中创建一个子文件夹,命名为Editor。这是因为我们将创建的 C#类将继承自AssetPostprocessor类,这是一个编辑器类,因此它需要放置在Editor文件夹中。

图 10.34 – 创建编辑器文件夹

图 10.34 – 创建编辑器文件夹

  1. 双击Editor文件夹进入,在此文件夹中创建一个新的 C#脚本,命名为AssetImporterSample,然后向此脚本中添加以下内容:

    using UnityEditor;
    public class AssetImporterSample : AssetPostprocessor
    {
        private void OnPreprocessAudio()
        {
            var audioImporter =
              (AudioImporter)assetImporter;
            if(audioImporter == null)
            {
                return;
            }
            audioImporter.forceToMono = true;
            audioImporter.SaveAndReimport();
        }
    }
    

让我们分解一下它是如何工作的:

  • 首先,代码使用了UnityEditor命名空间。这是因为AssetPostprocessor类定义在这个命名空间中,这也意味着AssetImporterSample C#脚本是在 Unity 编辑器中使用,而不是在运行时使用。

  • AssetImporterSample 类继承自 AssetPostprocessor 类并实现了 OnPreprocessAudio 方法,该方法将在音频资产导入之前被调用。我们还可以实现其他类似的方法,以便在其他资产类型导入时被调用。例如,OnPreprocessTexture 方法将在纹理资产导入之前被调用,而 OnPreprocessModel 方法将在模型资产导入之前被调用。

  • OnPreprocessAudio 方法中,我们可以获取 AudioImporter 的实例,将 forceToMono 选项设置为 true,然后保存并重新导入资产以确保资产的新导入设置生效。

  1. 保存 C# 脚本后,Unity 编辑器应修改项目中这些音频资产的导入设置,然后重新导入,如图 图 10.35 所示:

图 10.35 – 导入音频资产

图 10.35_B17146.jpg

图 10.35 – 导入音频资产

  1. 现在让我们选择一个音频文件来检查其导入设置。如图 图 10.36 所示,新的导入设置按预期工作:

图 10.36 – 新的导入设置

图 10.36_B17146.jpg

图 10.36 – 新的导入设置

在本小节中,我们介绍了如何使用 C# 代码来管理资产导入流程。接下来,让我们探索 Unity 中的另一个资产工作流程主题 – Library 文件夹。

Library 文件夹

在 Unity 项目中,Unity 将处理和转换外部资产为 Unity 内部格式资产,并将它们保存在 Library 文件夹中。因为 Library 文件夹中存储的数据是缓存数据,可以根据导入设置从源资产文件中重新生成,所以 Library 文件夹通常不应包含在版本管理系统中。

注意

除了 Library 文件夹外,还有一些其他 Unity 文件夹需要从版本管理中排除,包括 Temp, Obj,Logs。如果您使用 Git 作为版本管理工具,您可以在以下链接中找到 Unity 项目的 .gitignore 文件:github.com/github/gitignore/blob/main/Unity.gitignore

您可以在 Unity 项目的根目录中找到 Library 文件夹,如图 图 10.37 所示。如果您的 Unity 项目根目录中没有 Library 文件夹,您需要使用 Unity 编辑器打开项目。Unity 编辑器将导入 Assets 文件夹中的资产并自动生成 Library 文件夹。

图 10.37 – Library 文件夹

图 10.37_B17146.jpg

图 10.37 – Library 文件夹

双击 Library 文件夹进入它,您将看到 ScriptAssemblies 子文件夹,其中保存了项目中 C# 代码的组件,您还可以看到 PackageCache 子文件夹,其中保存了项目使用的 Unity 包的缓存。除了这些,您还可以看到 Artifacts 子文件夹,其中保存了由 Unity 处理的资产。

![图 10.38 – 资产夹

![图 10.38 – 图 10.38_B17146.jpg]

图 10.38 – 资产夹

在本节中,我们介绍了 Unity 的资产工作流程,涵盖了 GUID、文件 ID、元文件和文件夹等主题。接下来,让我们看看与 Unity 中的资产管理相关的由开发者创建和管理的特殊文件夹。

介绍 Unity 中的特殊文件夹

我们已经在第二章“Unity 中的脚本概念”中介绍了一些与 Unity 脚本相关的特殊文件夹。在本节中,我们将介绍剩余的特殊文件夹,这些文件夹与 Unity 中的资产管理相关。

资源文件夹

首先,让我们看看 Unity 中的资源文件夹。资源是 Unity 中的一个特殊文件夹名称,但 Unity 不会自动为你创建一个资源文件夹。如果你想使用资源文件夹来管理资产,你需要自己创建它。需要注意的是,在一个 Unity 项目中,Assets目录中可以有多个资源文件夹。

Unity 提供了Resources.Load方法来加载资源文件夹中的资产。接下来,我们将通过一个示例来学习如何使用资源文件夹来管理资产:

  1. 通过点击项目窗口中的创建 | 文件夹项,创建一个名为资源的新文件夹。

![图 10.39 – 创建资源文件夹

![图 10.39 – 图 10.39_B17146.jpg]

图 10.39 – 创建资源文件夹

  1. 创建一个空 GameObject,命名为SamplePrefab,并将其拖入资源文件夹以创建一个新的预制体,如图 10.40 所示:

![图 10.40 – SamplePrefab

![图 10.40 – 图 10.40_B17146.jpg]

图 10.40 – SamplePrefab

  1. ResourcesLoadExample中创建一个新的 C#脚本,并将以下内容添加到该脚本中:

    using UnityEngine;
    public class ResourcesLoadExample : MonoBehaviour
    {
        private GameObject _gameObjectInstance;
        private void Start()
        {
            var samplePrefab =
              Resources.Load<GameObject>("SamplePrefab");
            if(samplePrefab != null)
            {
                _gameObjectInstance =
                  Instantiate(samplePrefab);
            }
        }
    }
    

让我们分解一下它是如何工作的:

  1. Start方法中,我们调用Resources.Load方法,并将要加载的资产的路径作为参数传递给此方法,即SamplePrefab

  2. 然后,如果预制体资产被加载,我们将实例化它以在游戏场景中创建一个新的 GameObject。

  3. 创建一个新的 GameObject,并将ResourcesLoadExample脚本附加到它上。通过点击 Unity 编辑器中的播放按钮运行游戏。我们可以看到如预期地创建了一个新的预制体实例。

![图 10.41 – 从资源文件夹加载资产

![图 10.41 – 图 10.41_B17146.jpg]

图 10.41 – 从资源文件夹加载资产

通过这个例子,我们可以看到使用资源文件夹来管理资产非常方便,尤其是在你需要快速开发原型时,但是以下原因不建议在 Unity 项目中使用资源文件夹来管理资产:

  • 当 Unity 编辑器构建游戏时,资源文件夹中的资产将被包含在构建中,即使这些资产没有被使用,因此不恰当地使用资源文件夹可能会导致构建的游戏文件过大。此外,它还会影响游戏的启动速度。

  • 使用资源文件夹将使得游戏内容的增量升级变得非常困难或不可能。

现在我们已经了解了Resources文件夹,我们知道它们在哪些情况下将是合适的,例如开发快速原型,以及它们的局限性。

接下来,我们将继续介绍 Unity 中的另一个特殊文件夹,即StreamingAssets

StreamingAssets 文件夹

在 Unity 中,StreamingAssets也是一个特殊的文件夹名称。我们实际上已经在第六章在 Unity 项目中集成音频和视频中讨论过这一点。在本小节中,我们将更详细地讨论它。

我们之前提到,Unity 会以 Unity 引擎理解的形式处理Assets文件夹中的资源,但有一个例外。

Unity 项目中的StreamingAssets文件夹中的资源将保持原始格式,并且当 Unity 构建游戏时,这些资源不会与其他资源一起构建到游戏中。相反,文件夹中的所有资源都将复制到目标设备上的特定文件夹中。

由于这个特殊文件夹的位置在不同平台上不同,Unity 提供了Application.streamingAssetsPath属性,以便我们可以从 C#代码中访问到这个文件夹的正确路径。

以下代码片段来自示例中使用的第六章在 Unity 项目中集成音频和视频。我们可以看到如何在 C#代码中使用Application.streamingAssetsPath

    public void OnClickSetVideoURL()
    {
        _videoPlayer.url =
          Path.Combine(Application.streamingAssetsPath,
          _videoFileName);
    }

Resources文件夹类似,Unity 不会自动为您创建StreamingAssets文件夹。如果您想使用它,您需要自己创建,如图 10.42 所示:

![图 10.42 – 创建 StreamingAssets 文件夹图片

图 10.42 – 创建 StreamingAssets 文件夹

然后,我们可以在StreamingAssets文件夹中放置一个音频 WAV 文件。如图所示,这个 WAV 文件的图标与我们在 Unity 中已经熟悉的音频剪辑图标不同。这是因为 Unity 没有处理 WAV 文件;它仍然保持其原始格式。

![图 10.43 – 在 StreamingAssets 文件夹中放置 WAV 文件图片

图 10.43 – 在 StreamingAssets 文件夹中放置 WAV 文件

在本节中,我们探讨了Resources文件夹和StreamingAssets文件夹,这些是 Unity 中的特殊文件夹,并了解了它们的功能如何帮助您更好地使用 Unity 开发游戏。

接下来,我们将介绍另一个有趣的主题;如何在 Azure Cloud 中使用 Azure Blob 存储与 Unity 的 Addressable Asset 系统。

Azure Blob 存储与 Unity 的 Addressable Asset 系统

在本节中,我们将介绍 Microsoft Azure Cloud 中的 Azure Blob 存储服务以及如何使用它与 Unity 的 Addressable Asset 系统。

Azure Blob 存储是 Azure 中的 Azure 存储帐户类型之一。其他类型的 Azure 存储帐户包括队列文件共享。其中,Blob 存储非常适合存储大量非结构化数据,如二进制数据。

注意

您可以在 Microsoft 的 Azure 云中找到有关 Azure 存储帐户的更多信息资源,请参阅docs.microsoft.com/en-us/azure/storage/common/storage-introduction

Unity 的 Addressable Asset 系统正如其名所示,提供了一种方便的方法,可以根据特定的地址在本地或远程服务器上加载特定的资产。在上一节讨论“资源”文件夹时,我们讨论了在使用它管理资产时存在的各种限制,而 Addressable Asset 系统可以很好地解决这些问题;例如,可以很好地控制游戏包的大小,无需在游戏构建中包含不必要的资产,并且可以将资产托管在远程服务器上,例如 Azure 云,以在游戏中增量更新资产。

注意

在 Addressable Asset 系统引入之前,开发者也可以使用 AssetBundles 来管理资产;AssetBundles 不在我们需要讨论的范围之内,但如果您感兴趣,可以在docs.unity3d.com/Manual/AssetBundlesIntro.html上了解更多信息。

好的,现在我们已经了解了 Azure Blob 存储和 Addressable Asset 系统的概念。接下来,我们将探讨如何使用 Azure Blob 存储来托管资产,并使用 Addressable Asset 系统来管理它们。

让我们开始吧!

设置 Azure Blob 存储服务

首先,请确保您有一个可用的 Azure 订阅。您可以在本章开头介绍的页面上申请免费的 Azure 试用帐户。

如果一切准备就绪,我们就可以在 Azure 中创建我们的第一个资源,即Azure 资源组

创建新的资源组

通常,资源组是我们 Azure 云中的第一个资源。这是因为资源组是用于存放其他 Azure 资源的容器。

我们可以仅通过几个步骤就创建一个资源组:

  1. 使用您的帐户登录到 Azure 门户页面portal.azure.com/

图 10.44 – Azure 门户页面

图 10.44 – Azure 门户页面

  1. Azure 门户页面默认不显示门户菜单。我们可以点击页面左上角的显示门户菜单按钮来打开门户菜单。

图 10.45 – 显示门户菜单

图 10.45 – 显示门户菜单

  1. 从门户菜单中选择资源组

图 10.46 – 选择资源组服务

图 10.46 – 选择资源组服务

  1. 然后资源组页面将打开。点击此页面上的创建按钮,如图 10.47所示:

图 10.47 – 创建资源组

图 10.47 – 创建资源组

  1. 然后,您将看到rg-unitybook-dev-001。选择资源组的区域为(亚太地区) 澳大利亚东部,然后点击审查 + 创建以验证此资源组的设置并创建它,如图 10.48所示:

图 10.48 – 创建资源组

图 10.48 – 创建资源组

我们已在 Azure 中创建了一个资源组。接下来,让我们创建一个 Azure 存储账户资源。

注意

您可以在 Microsoft Azure Cloud 的命名约定中找到更多信息,请参阅docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming

创建新的 Azure 存储账户资源

为了设置 Azure Blob 存储服务,我们需要创建一个 Azure 存储账户,为将要托管的第一批资产在 Azure 中提供一个唯一的命名空间。

我们将执行以下步骤:

  1. 返回 Azure 门户页面,重复之前介绍的步骤以打开门户菜单,然后这次点击存储账户,如图 10.49所示:

图 10.49 – 点击存储账户

图 10.49 – 点击存储账户

  1. 存储账户页面将打开。点击此页面上的创建按钮,如图 10.50所示:

图 10.50 – 创建存储账户

图 10.50 – 创建存储账户

  1. 与创建资源组类似,分别在unitybookchapter10(亚太地区) 澳大利亚东部上。其他设置可以保留默认值,然后点击审查 + 创建按钮来创建资源。

图 10.51 – 创建存储账户

图 10.51 – 创建存储账户

  1. 我们可以点击页面右上角的“通知”按钮来查看资源部署的进度。当资源部署完成后,我们可以点击转到资源来进入资源页面。

图 10.52 – 通知

图 10.52 – 通知

  1. 图 10.53所示,已创建一个名为unitybookchapter10的存储账户:

图 10.53 – 存储账户页面

图 10.53 – 存储账户页面

到目前为止,我们已在 Azure 中设置了一个存储账户资源。接下来,让我们设置 Blob 存储。

创建容器

如我们在此节开头所述,Blob 存储是一种 Azure 存储账户类型,因此我们可以在刚刚打开的存储账户页面上找到 Blob 存储的设置。我们可以执行以下步骤来设置 Blob 存储:

  1. 首先,我们需要创建一个容器,类似于我们在计算机文件系统中的目录,用于组织一组文件,并在 Azure 云上组织一组 blob 的容器。向下滚动存储账户页面左侧的菜单,在 Data storage 部分中,我们可以看到四种不同的存储类型。然后,选择 Containers

图 10.54 – 选择容器

图 10.54 – 选择容器

  1. 然后,点击如图 图 10.55 所示的 + 容器 按钮:

图 10.55 – 点击 + 容器按钮

图 10.55 – 点击 + 容器按钮

  1. remotedata 中作为容器的名称,并且为了简单起见,我们将 Public access level 设置为 Blob 以允许匿名访问容器内的 blob。

图 10.56 – 创建新的容器

图 10.56 – 创建新的容器

注意

为了安全起见,您应该尝试以更安全的方式管理对 blob 的访问,例如,使用 访问密钥 进行授权,或使用 共享访问签名 (SAS) 来委派访问。如果您感兴趣,可以在 docs.microsoft.com/en-us/azure/storage/blobs/authorize-data-operations-portal 上了解更多信息。

现在我们已经设置了 Azure Blob 存储,我们还需要在 Unity 中使用 Addressable Assets 系统来创建资产包并将它们部署到 Azure。

安装 Addressable Assets 系统包

默认情况下,Addressable Asset 系统在 Unity 项目中不可用。因此,我们需要首先安装 Addressables 包。

图 10.57 所示,我们可以在 Unity 的包管理器中找到此包,并将其安装到我们的项目中。

图 10.57 – 安装 Addressables 包

图 10.57 – 安装 Addressables 包

安装完成后,您可以在 Unity 编辑器的 Window 菜单中找到 Addressables 项。

图 10.58 – Addressables 项

图 10.58 – Addressables 项

接下来,让我们使用 Addressable Assets 系统构建可寻址内容。

构建可寻址内容

构建可在 Azure 云上托管的可寻址内容听起来很复杂,但我们可以将此任务进一步分解为以下任务:

  1. 首先,将资产标记为可寻址。

  2. 然后,启用远程目录。

  3. 最后,构建内容。

现在,让我们继续探索第一个任务。

标记可寻址资产

在 Unity 编辑器中,我们可以轻松地将资产标记为地址化。在我们标记地址化资产之前,让我们首先创建一个新的资产。我们可以在场景中创建一个新的立方体,命名为 SampleContentOnAzure,并将其拖到 项目 窗口中以创建一个新的预制件资产。

然后,选择此新预制件以打开其 检查器 窗口,您可以在窗口中看到 图 10.59 中所示的 地址化 复选框:

![图 10.59 – 标记地址化资产图片

图 10.59 – 标记地址化资产

通过勾选此复选框,我们将标记预制件资产为地址化。

启用远程目录

在启用地址化资产设置中的远程目录之前,我们首先可以创建一个新的配置文件,该配置文件定义了诸如 RemoteLoadPath 这样的变量。

创建配置文件

因此,让我们按照以下步骤创建一个配置文件:

  1. 在工具栏中,点击 窗口 | 资产管理 | 地址化 | 配置文件

![图 10.60 – 打开配置文件窗口图片

图 10.60 – 打开配置文件窗口

  1. 地址化配置文件 窗口中,点击 创建 按钮,然后在下拉菜单中选择 配置文件 以创建一个新的配置文件。

![图 10.61 – 创建新配置文件图片

图 10.61 – 创建新配置文件

  1. 然后,将此新配置文件重命名为 AzureCloud 并输入与 RemoteLoadPath 变量相关的 Azure Blob 容器 URL。

![图 10.62 – 设置新配置文件图片

图 10.62 – 设置新配置文件

如果您不知道 Azure Blob 容器的 URL,您可以在 Azure 中容器属性页面上找到它,如下面的截图所示:

![图 10.63 – 容器属性图片

图 10.63 – 容器属性

创建新的地址化组

接下来,我们还需要创建一个新的 地址化组,这是一个用于地址化资产及其数据的容器,并可以确定组内的资产是否托管在远程服务器上或存储在本地。然后,我们可以将需要托管在远程服务器上的资产放置在这个新组中,而无需更改默认组中配置的本地位置。

让我们执行以下步骤:

  1. 在工具栏中,点击 窗口 | 资产管理 | 地址化 | 组

![图 10.64 – 打开组窗口图片

图 10.64 – 打开组窗口

  1. 地址化组 窗口中,点击 创建 按钮,然后选择 组 > 打包资产 来创建一个新的组。

![图 10.65 – 创建新组图片

图 10.65 – 创建新组

将其重命名为 Azure 远程组

![图 10.66 – Azure 远程组图片

图 10.66 – Azure 远程组

  1. 将活动配置文件从 默认 更改为 AzureCloud,以便可寻址资产系统可以访问 AzureCloud 中的变量。

图 10.67 – 激活 AzureCloud 配置文件

图 10.67 – 激活 AzureCloud 配置文件

  1. 可寻址组 窗口中选择 Azure 远程组以打开其 检查器 窗口,并使用在 AzureCloud 配置文件中定义的远程路径设置 内容打包和加载

图 10.68 – 设置可寻址组

图 10.68 – 设置可寻址组

  1. 默认情况下,标记的可寻址资产将位于 默认本地组 下;我们需要将其移动到我们刚刚创建的 Azure 远程组。

图 10.69 – 将资产移动到 Azure 远程组

图 10.69 – 将资产移动到 Azure 远程组

  1. 最后,我们还需要在 C# 代码中设置一个标签,使用此键的 Addressables.LoadResourceLocationsAsync 方法。

图 10.70 – 设置标签

图 10.70 – 设置标签

现在我们已经设置了可寻址组,接下来,让我们继续启用构建远程内容的能力。

启用构建远程目录复选框

  1. 返回工具栏,点击 窗口 | 资产管理 | 可寻址 | 设置 以打开 可寻址资产设置 窗口。

图 10.71 – 打开可寻址资产设置窗口

图 10.71 – 打开可寻址资产设置窗口

  1. 滚动窗口,你将找到 内容更新 部分。然后,勾选 构建远程目录 复选框,并分别设置 构建路径加载路径 字段。

图 10.72 – 构建远程目录设置

图 10.72 – 构建远程目录设置

现在你已经知道了如何在可寻址资产系统中启用远程目录,我认为你已经准备好了,迫不及待地想要学习如何构建内容。让我们开始吧!

构建内容

现在是时候在以下步骤的帮助下构建内容了:

  1. 返回 可寻址组 窗口,点击 播放模式脚本,并在下拉菜单中选择如图 图 10.73 所示的 使用现有构建(需要构建组)

图 10.73 – 设置播放模式脚本

图 10.73 – 设置播放模式脚本

注意

Unity 为开发者提供了三个构建脚本以创建播放模式数据。在这里,我们使用 使用现有构建 模式,这与部署的游戏构建最匹配。你可以在 docs.unity3d.com/Packages/com.unity.addressables@1.9/manual/AddressableAssetsDevelopmentCycle.html 找到有关可寻址资产系统构建脚本的更多信息。

  1. 然后,点击构建 | 新构建 | 默认构建脚本来构建内容。

图 10.74 – 构建内容

图 10.74 – 构建内容

  1. 等待构建完成,然后你可以在项目根目录下的ServerData文件夹中找到构建的内容。

图 10.75 – ServerData

图 10.75 – ServerData

现在你已经知道了如何在 Addressable Asset 系统中构建可寻址内容,接下来,让我们继续学习如何将内容部署到 Azure 云。

将内容部署到 Azure 云

要将我们刚刚构建的可寻址内容部署到 Azure 云,请按照以下步骤操作:

  1. 导航到我们在 Azure 中创建的remotedata容器,然后点击上传按钮。

图 10.76 – 远程数据容器页面

图 10.76 – 远程数据容器页面

  1. 然后将出现一个上传 blob面板。选择您想要上传的文件,然后点击上传按钮。

图 10.77 – 上传内容

图 10.77 – 上传内容

  1. 等待上传完成,然后我们可以在remotedata容器中的 blob 列表中看到我们的可寻址内容。

图 10.78 – Azure 中的可寻址内容

图 10.78 – Azure 中的可寻址内容

现在你已经知道了如何将可寻址内容部署到 Azure 云,接下来,让我们继续探索如何从 Azure 加载内容到你的游戏中。

从 Azure 云加载可寻址内容

由于我们使用 Addressable Asset 系统来管理资产,因此从 Azure 云加载内容到游戏也需要使用 Addressable Asset 系统提供的方法。

让我们开始吧!

  1. Scripts文件夹中创建一个新的 C#脚本,命名为LoadAddressableContentFromAzureCloud,并将以下内容添加到该脚本中:

    using UnityEngine;
    using UnityEngine.AddressableAssets;
    public class LoadAddressableContentFromAzureCloud :
      MonoBehaviour
    {
        [SerializeField]
        private string _assetKey;
        private void Start()
        {
            GetContentFromAzureCloud();
        }
        private async void GetContentFromAzureCloud()
        {
            var resourceLocations = await
              Addressables.LoadResourceLocationsAsync
              (_assetKey).Task;
            foreach (var resourceLocation in
              resourceLocations)
            {
                await Addressables.InstantiateAsync
                  (resourceLocation).Task;
            }
        }
    }
    

如你在代码中所见,我们首先提供了_assetKey,其值为我们在上一节中设置的资产的标签。然后,我们调用Addressables.LoadResourceLocationsAsync方法来加载内容,并调用Addressables.InstantiateAsync来实例化一个 GameObject。

  1. 创建一个新的 GameObject,将其Asset Key的值设置为Azure,并将LoadAddressableContentFromAzureCloud脚本附加到它上,然后在 Unity 编辑器中通过点击播放按钮运行游戏。我们可以看到如预期地创建了一个新的预制体实例。

图 10.79 – 从 Azure 云加载可寻址内容

图 10.79 – 从 Azure 云加载可寻址内容

通过阅读本节,你了解了 Microsoft Azure 云中的 Azure Blob 存储服务是什么,以及如何使用 Unity 的 Addressable Asset 系统来托管和更新游戏内容。本节也标志着本章的结束!

摘要

在本章中,我们已经走得很远了。我们首先介绍了 Unity 的序列化系统,讨论了 Unity 中的二进制序列化、YAML 序列化和 JSON 序列化。然后我们探讨了 Unity 中的资产工作流程,涵盖了诸如 GUID、文件 ID、元文件、Library 文件夹以及如何从 C# 代码中管理资产导入管道等重要概念。接下来,我们详细讨论了 Resources 文件夹和 StreamingAssets 文件夹,这些是 Unity 中的特殊文件夹,并理解了它们的功能可以帮助你更好地使用 Unity 开发游戏。最后,我们详细介绍了 Azure Blob 存储和 Unity 的可寻址资产系统,从如何在 Azure 云中创建 Azure Blob 存储服务到如何将可寻址内容从 Azure 加载到 Unity 项目中。这是一段令人惊叹的旅程!

在本章中你获得的知识将帮助你根据需求在 Unity 中选择合适的序列化模式,合理管理资产,并利用 Azure 云实现游戏内容的增量更新。

在下一章,我们将继续这段美好的旅程,探索如何使用 Unity、Microsoft Game Dev 和 Azure 云来创建游戏。

第十一章:第十一章:与 Microsoft Game Dev、Azure 云、PlayFab 和 Unity 一起工作

这是本书的最后一章。在前面的章节中,我们学习了可以使用 Unity 引擎开发游戏的各个模块,例如 UI 模块、物理模块和动画模块,还涵盖了某些高级主题 – 例如,Unity 的渲染管线和新的面向数据技术堆栈。此外,在 第十章 中,Unity 和 Azure 中的序列化系统和资产管理,我们不仅讨论了 Unity 的序列化系统和资产管理,还涵盖了与 Microsoft Azure 云相关的某些知识。

本章将继续探讨 Microsoft Game Dev(之前称为 Microsoft Game Stack)、Microsoft Azure 云Microsoft Azure PlayFab,因为现代游戏开发所需工具不仅限于游戏引擎;其他工具和服务,如云,在游戏开发中的应用越来越广泛。

我们的学习路径将包括以下关键主题:

  • 介绍 Microsoft Game Dev、Microsoft Azure 云和 Azure PlayFab

  • 为 Unity 项目设置 Azure PlayFab

  • 使用 Azure PlayFab 在 Unity 中注册和登录玩家

  • 在 Unity 中使用 Azure PlayFab 实现排行榜

到本章结束时,你将了解 Microsoft Game Dev、Microsoft Azure 云和 Microsoft Azure PlayFab 是什么,以及如何在 Unity 项目中设置 Azure PlayFab 并使用 Azure PlayFab 的 API 实现注册、登录和排行榜功能。

听起来很激动人心!现在,让我们开始吧!

技术要求

你可以在以下 GitHub 仓库中找到本章将使用的示例项目,即 Chapter11-AzurePlayFabAndUnitygithub.com/PacktPublishing/Game-Development-with-Unity-for-.NET-Developers

介绍 Microsoft Game Dev、Microsoft Azure 云和 Azure PlayFab

我们已经学习了如何使用 Unity 引擎开发游戏。然而,现代游戏开发不仅需要游戏引擎,还需要其他工具,例如云服务。

Microsoft Game Dev

2019 年,微软宣布了 Microsoft Game Stack,现在称为 Microsoft Game Dev,旨在为游戏开发者提供他们轻松创建和运营游戏所需的工具和服务:

图 11.1 – Microsoft Game Dev 产品(来自 Game Dev 网站)

图 11.1 – Microsoft Game Dev 产品(来自 Game Dev 网站)

微软游戏开发中的这些工具和服务不仅包括 DirectX、Visual Studio、Xbox 服务、App Center 和 Havok,这些都是游戏开发者完成游戏开发和内容创作时常用的,还包括基于云的服务,如微软 Azure 云和 Azure PlayFab,所有这些共同构成了一个强大的生态系统,每个游戏开发者都可以使用,如图 图 11.1 所示。

微软 Azure 云和 Azure PlayFab 是微软游戏开发的重要组成部分。不仅越来越多的现代游戏需要多人支持,而且单机游戏将玩家数据存储在云中的情况也越来越普遍。因此,云在游戏开发中的重要性日益增加。

在 2022 年 3 月的游戏开发者大会上,微软宣布了一个新的计划,ID@Azure,旨在帮助游戏开发者使用微软 Azure 云和 Azure PlayFab 服务来开发游戏。任何游戏开发者都可以申请加入该计划,无论他们是独立游戏开发者还是游戏工作室。加入该计划后,您可以获得高达 5,000 美元的 Azure 信用额度,因此您可以访问许多云服务,获得免费的 Azure PlayFab 标准计划,获得专家支持等等。

注意

如果您对 ID@Azure 计划感兴趣,您可以在 aka.ms/idazure 找到更多信息。

现在您已经了解了微软游戏开发是什么,让我们继续探讨微软 Azure 云和 Azure PlayFab 是什么。

微软 Azure 云

微软 Azure 是一个云计算服务平台,您可以在其中找到以下服务:

  • 云计算服务,例如 Azure 应用服务、Azure 函数和 Azure 虚拟机

  • 数据库服务,例如 Cosmos DB、Azure SQL 数据库和 Azure Cache for Redis

  • 存储服务,例如 Azure 存储帐户和数据湖存储

  • 网络服务,例如 Azure 应用网关、Azure 防火墙和 Azure 负载均衡器

  • 分析服务,例如 Azure 数据工厂和 Azure Synapse 分析

  • 安全服务,例如 Azure 防御者和 Azure DDoS 保护

  • 人工智能服务,例如 Azure 认知服务和 Azure 机器人服务

在游戏行业中,游戏服务器通常部署在尽可能靠近玩家的数据中心,这不仅减少了网络延迟,还满足了一些国家和地区的数据主权要求:

图 11.2 – 微软 Azure 全球基础设施

图 11.2 – 微软 Azure 全球基础设施

根据微软的数据,微软 Azure 云覆盖了全球 140 个国家和地区,可用的区域数量超过任何其他云平台。巨大的全球覆盖范围有助于游戏开发者快速为目标国家或地区部署游戏服务。

注意

您可以在infrastructuremap.microsoft.com/explore找到有关 Microsoft Azure 全球基础设施的更多信息。

除了使用 Azure 数据中心托管游戏外,游戏开发者还可以在 Microsoft Azure 云上使用 Azure 虚拟机开发游戏。2022 年 3 月在游戏开发者大会上宣布了一款新的 Azure 游戏开发虚拟机,它针对游戏开发者定制,并预装了如 Microsoft 游戏开发工具包、Visual Studio 2019 社区版和 Blender 等工具,以实现云上游戏制作。

注意

如果您对 Azure 游戏开发虚拟机感兴趣,您可以在aka.ms/gamedevvmdocs找到更多信息。

Azure PlayFab

PlayFab 是构建和运营实时游戏的完整后端服务。2018 年初,微软收购了 PlayFab。现在,PlayFab 已加入 Azure 家族,并将其名称更改为 Azure PlayFab,成为 Azure 的一部分。Azure PlayFab 将 Azure 云与 PlayFab 结合;Azure 云带来可靠性、全球可访问性和企业级安全性,而 PlayFab 为游戏开发者提供完整的游戏后端服务。

作为完整的后端服务解决方案,Azure PlayFab 主要提供以下功能,供游戏开发者开发游戏:

  • 游戏开发者可以使用内置的身份验证来启用玩家注册、登录,甚至跨设备跟踪玩家

  • 能够创建动态扩展的多玩家服务器并在云上管理玩家数据

  • 能够轻松在后台服务器上实现排行榜

    注意

    Azure PlayFab 还提供其他用于维护和运营游戏的服务,例如Liveops(即实时操作)和数据分析服务,这些服务可以用来管理游戏内容,例如在不发布新版本的情况下更新游戏,以及每日报告和分析游戏数据。它们超出了我们在这里的需求,但如果您感兴趣,您可以在docs.microsoft.com/en-us/gaming/playfab了解更多信息。

在本章的剩余部分,我们将集成 Azure PlayFab 到 Unity 项目中,以实现玩家注册、登录、数据保存、加载和排行榜。

让我们继续前进!

在 Unity 项目中设置 Azure PlayFab

在本例中,我们将向 Unity 中的Flappy Bird风格游戏添加玩家注册、登录、数据保存、加载和排行榜功能:

图 11.3 – Unity 项目

图 11.3 – Unity 项目

接下来,我们将首先创建一个新的 Azure PlayFab 账户,在 Azure PlayFab 中设置游戏工作室和游戏标题,然后在此 Unity 项目中设置 Azure PlayFab SDK。

创建新的 Azure PlayFab 账户

首先,我们需要一个新的 Azure PlayFab 账户。要创建新的 Azure Playfab 账户,让我们执行以下步骤:

  1. 访问 Microsoft Azure PlayFab 的主页 playfab.com/ 并点击右上角的注册按钮以打开注册页面:

![图 11.4 – Azure PlayFab 的主页图 11.04 – B17146.jpg

图 11.4 – Azure PlayFab 的主页

  1. 在注册页面输入您的电子邮件地址和密码,然后点击创建免费账户按钮:

![图 11.5 – 创建免费账户图 11.05 – B17146.jpg

图 11.5 – 创建免费账户

  1. 您将收到来自 Azure PlayFab 的验证邮件以验证您的电子邮件地址;点击验证您的电子邮件地址

![图 11.6 – Azure PlayFab 的电子邮件地址验证图 11.06 – B17146.jpg

图 11.6 – Azure PlayFab 的电子邮件地址验证

  1. 邮件地址验证完成后,您可以使用您刚刚创建的 Azure PlayFab 账户登录,您可以在 Azure PlayFab 开发者门户中看到您的游戏工作室和已设置的游戏标题,在 Azure PlayFab 中也称为游戏管理器

![图 11.7 – Azure PlayFab 中的我的游戏工作室图 11.07 – B17146.jpg

图 11.7 – Azure PlayFab 中的我的游戏工作室

现在,我们已经创建了新的 Azure PlayFab 账户,我们可以开始了解如何在 Azure PlayFab 中设置游戏工作室和游戏标题。

在 Azure PlayFab 中设置游戏工作室和游戏标题

在 Azure PlayFab 中创建账户后,下一个任务是设置您自己的游戏工作室和游戏标题:

  1. 默认游戏工作室称为我的游戏工作室,这也没有什么意义,因此您可以点击右侧的... | 工作室设置以打开编辑工作室页面:

图 11.08 – B17146.jpg

图 11.8 – 打开编辑工作室页面

  1. UnityBook 上点击保存工作室按钮以保存:

![图 11.9 – 将工作室名称更改为 UnityBook图 11.09 – B17146.jpg

图 11.9 – 将工作室名称更改为 UnityBook

  1. 类似地,默认游戏标题是我的游戏,这也没有什么意义。如图所示,您可以点击齿轮按钮,然后编辑标题信息以打开编辑标题页面:

![图 11.10 – 打开编辑标题页面图 11.10 – B17146.jpg

图 11.10 – 打开编辑标题页面

  1. Chapter11-AzurePlayfabAndUnity 上点击保存标题以保存:

![图 11.11 – 更改标题名称图 11.11 – B17146.jpg

图 11.11 – 更改标题名称

现在,我们已经设置了 Azure PlayFab 中的游戏工作室和游戏标题,让我们将注意力转向在 Unity 项目中设置 Azure PlayFab SDK!

在 Unity 项目中设置 Azure PlayFab SDK

为了从 Unity 访问 Azure PlayFab 中的 API,我们首先需要将 Azure PlayFab SDK 导入到 Unity 项目中:

  1. 您可以在 docs.microsoft.com/en-us/gaming/playfab/sdks/unity3d/ 找到 Azure PlayFab SDK。在这里,您还可以找到 Unity PlayFab SDK GitHub 仓库的链接,如图 Figure 11.12 所示:

![Figure 11.12 – Azure PlayFab SDK 下载链接图片

Figure 11.12 – Azure PlayFab SDK 下载链接

  1. 将您刚刚下载的 UnitySDK 包拖放到 Unity 编辑器中。将弹出的 导入 Unity 包 窗口,在这里您可以预览包的内容,然后点击 导入 按钮将其导入到这个 Unity 项目中:

![Figure 11.13 – 导入 Azure PlayFab SDK图片

Figure 11.13 – 导入 Azure PlayFab SDK

  1. SDK 导入后,您将在 Unity 编辑器工具栏中找到 PlayFab 菜单。然后,您可以点击 PlayFab > MakePlayFabSharedSettings 以打开 PlayFabSharedSettings 窗口,在那里您需要配置设置以将此 Unity 项目连接到 Azure PlayFab 中的游戏标题:

![Figure 11.14 – PlayFab SDK 已导入图片

Figure 11.14 – PlayFab SDK 已导入

  1. Play Fab 共享设置 窗口中,您应提供游戏标题的标题 ID 和开发者密钥,如图 Figure 11.15 所示:

![Figure 11.15 – 游戏标题共享设置窗口图片

Figure 11.15 – Play Fab 共享设置窗口

  1. 为了找到游戏标题 ID 和开发者密钥,您需要返回 Azure PlayFab 的开发者门户,在那里您可以在游戏标题项中找到游戏标题 ID:

![Figure 11.16 – 游戏标题 ID图片

Figure 11.16 – 游戏标题 ID

  1. 开发者密钥与 Azure PlayFab 中的游戏标题紧密相关,因此您需要在开发者门户上首先点击标题项以打开游戏标题的概述页面。如图所示,您需要点击齿轮按钮,然后点击 标题设置 以打开游戏标题的设置页面:

![Figure 11.17 – 游戏标题概述页面图片

Figure 11.17 – 游戏标题概述页面

  1. 在游戏标题的设置页面中,选择 密钥 选项卡以切换到密钥设置,在那里您可以找到默认的开发者密钥:

![Figure 11.18 – 密钥页面图片

Figure 11.18 – 密钥页面

  1. 返回 Unity 中的 游戏标题共享设置 窗口,并使用您刚刚从 Azure PlayFab 开发者门户获取的标题 ID 和开发者密钥设置标题 ID 和开发者密钥。

现在我们已经为这个 Unity 项目设置了 Azure PlayFab SDK,你应该已经了解了 Azure PlayFab,包括 Azure PlayFab 开发者门户(也称为游戏管理器),如何设置游戏工作室和游戏标题,以及如何将 Azure PlayFab 的 SDK 导入 Unity 项目并将 Azure PlayFab 中的游戏标题连接到项目。接下来,让我们继续探讨如何通过 Azure PlayFab 注册和登录玩家。

使用 Azure PlayFab 在 Unity 中注册和登录玩家

技术要求部分提到的演示项目中,你可以在AzurePlayFabIntegration 文件夹 | StartScene中找到注册和登录 UI 面板,我们将使用它来实现注册和登录功能:

![图 11.19 – UI 面板上的注册标签页(左)和登录标签页(右)]

](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/gmdev-unt-dn-dev/img/Figure_11.19_B17146.jpg)

图 11.19 – UI 面板上的注册标签页(左)和登录标签页(右)

图 11.19所示,像许多常见的注册和登录页面一样,我们示例中的注册和登录 UI 面板也有两个标签页,即注册标签页和登录标签页,可以通过点击面板上的红色提醒文本进行切换。注册标签页要求玩家提供用户名、电子邮件和密码以在 Azure PlayFab 中创建新的玩家账户,而登录标签页仅要求玩家提供电子邮件和密码进行登录。

在 Azure PlayFab 中注册玩家

接下来,让我们首先看看如何实现注册功能:

  1. AzurePlayFabIntegration文件夹中创建一个新的文件夹并命名为Scripts

图 11.20 – 创建一个 Scripts 文件夹

图 11.20 – 创建一个 Scripts 文件夹

  1. Scripts文件夹中创建一个新的 C#脚本,命名为AzurePlayFabAccountManager,并添加以下代码:

    using System.Text;
    using System.Security.Cryptography;
    using UnityEngine;
    using UnityEngine.UI;
    using PlayFab;
    using PlayFab.ClientModels;
    public class AzurePlayFabAccountManager :
      MonoBehaviour
    {
        [SerializeField]
        private InputField _userName, _email, _password;
        [SerializeField]
        private Text _message;
        public void OnSignUpButtonClick()
        {
            var userRequest = new
              RegisterPlayFabUserRequest
            {
                Username = _userName.text,
                Email = _email.text,
                Password = Encrypt(_password.text)
            };
            PlayFabClientAPI.RegisterPlayFabUser(userRequest,
              OnRegisterSuccess, OnError);
        }
      public void
       OnRegisterSuccess(RegisterPlayFabUserResult result)
        {
            _message.text = "created a new account!";
            var displayNameRequest = new
              UpdateUserTitleDisplayNameRequest
            {
                DisplayName = result.Username
            };
               PlayFabClientAPI.
                 UpdateUserTitleDisplayName(display
                 NameRequest, OnUpdateDisplayNameSuccess, 
                 OnError);
        }
        public void OnError(PlayFabError error)
        {
            _message.text = error.ErrorMessage;
        }
        private static string Encrypt(string input)
        {
            var md5 = new MD5CryptoServiceProvider();
            var bytes = Encoding.UTF8.GetBytes(input);
            bytes = md5.ComputeHash(bytes);
            return Encoding.UTF8.GetString(bytes);
        }
    }
    

这是一个相当长的脚本;让我们如下分解代码:

  • 我们使用using关键字添加System.Security.CryptographySystem.Text命名空间,以便在Encrypt方法中加密密码。

  • 我们使用using关键字添加PlayFabPlayFab.ClientModels命名空间,以便访问 Azure PlayFab 提供的 API。

  • fields部分,我们引用了三个InputField UI 元素来提供用户名、电子邮件地址和密码。同时,我们还获取了Text UI 元素的引用来显示来自 Azure PlayFab 的消息。

  • 我们创建一个RegisterPlayFabUserRequest的新实例,并调用PlayFabClientAPI.RegisterPlayFabUser来在 Azure PlayFab 中注册此用户。

  • 我们还有两个回调函数 —— OnRegisterSuccess,当接收到结果时会被调用,以及OnError,当发生错误时会被调用。

  • OnRegisterSuccess 中,我们创建了一个新的 UpdateUserTitleDisplayNameRequest 实例,并调用 PlayFabClientAPI.UpdateUserTitleDisplayName 来更新用户在注册时的用户名作为显示名;否则,用户的显示名默认为空字符串。此外,您还可以使用此方法允许用户在未来更改账户的显示名。

  1. AzurePlayFabAccountManager.cs 拖放到场景中的 SignupAndLogin GameObject 上,并将 UI 元素分配到相应的字段,如图 图 11.21 所示:

图 11.21 – 设置 AzurePlayFabAccountManager

图 11.21 – 设置 AzurePlayFabAccountManager

  1. 选择附加了 AzurePlayFabAccountManager 的对象,最后,选择在按钮点击时将在 AzurePlayFabAccountManager 类中调用的方法,如图 图 11.22 所示:

图 11.22 – 设置注册按钮

  1. 运行游戏,并在向 Azure PlayFab 发送的 register user 请求中输入用户名、电子邮件地址和密码。如图所示,创建了一个新账户:

图 11.23 – 创建了一个新账户

图 11.23 – 创建了一个新账户

  1. 让我们回到 Azure PlayFab 中游戏标题的仪表板。在仪表板中,您可以看到有一个新的 API 调用和一个新用户已被创建。然后,我们也可以点击 Players 按钮打开 Players 页面以获取更多信息:

图 11.24 – 游戏标题仪表板

图 11.24 – 游戏标题仪表板

  1. 查看玩家页面上的玩家列表;您可以看到我们刚刚创建的新账户。还有一些关于账户的信息,例如最后登录时间、账户创建时间以及玩家登录的国家:

图 11.25 – 玩家页面

图 11.25 – 玩家页面

现在我们已经实现了注册功能,是时候为已有账户的玩家实现登录功能了。

在 Azure PlayFab 中登录玩家

通过以下步骤,我们将要求玩家提供电子邮件和密码进行登录,如果登录成功,他们将跳转到我们的 Flappy Bird-风格游戏场景:

  1. 返回到 AzurePlayFabAccountManager 脚本,并添加以下代码:

    // ... pre-existing code ...
    using UnityEngine.SceneManagement;
    //... pre-existing code ...
        public void OnLoginButtonClick()
        {
            var userRequest = new
              LoginWithEmailAddressRequest
            {
                Email = _email.text,
                Password = Encrypt(_password.text)
            };
            PlayFabClientAPI.
              LoginWithEmailAddress(userRequest,
      OnLoginSuccess, OnError);
        }
        public void OnLoginSuccess(LoginResult result)
        {
            _message.text = "login successful!";
            StartGame();
        }
        private static void StartGame()
        {
            SceneManager.LoadScene(1);
        }
    

让我们按以下方式分解新添加的代码:

  • 首先,使用 using 关键字添加了 UnityEngine.SceneManagement 命名空间。这是因为如果玩家成功登录,我们需要将场景从登录场景切换到游戏场景,而与场景加载相关的逻辑定义在这个命名空间中。

  • 我们创建了一个新的 LoginWithEmailAddressRequest 实例,并调用 PlayFabClientAPI.LoginWithEmailAddress 来将玩家登录到 Azure PlayFab。

  • 除了使用电子邮件登录外,Azure PlayFab 还提供多种登录方式,例如通过调用 PlayFabClientAPI.LoginWithFacebook 使用 Facebook 访问令牌登录,以及调用 PlayFabClientAPI.LoginWithGameCenter 使用 iOS Game Center 玩家标识符登录。

  • 当玩家成功登录时,将调用 SceneManager.LoadScene 方法。SceneManager.LoadScene 方法接受一个 int 参数,即目标场景的索引。

  • 本例中有两个场景 – 第一个是 StartScene,索引为 0,允许玩家在此处注册或登录;第二个是 GameScene,索引为 1,允许玩家玩游戏,因此我们使用索引 1StartScene 切换到 GameScene

  1. 选择 AzurePlayFabAccountManager 所附加的对象,最后选择在 OnLoginButtonClick 方法中调用的 AzurePlayFabAccountManager 类中定义的方法,如图 图 11.26 所示:

![img/Figure_11.26_B17146.jpg]

图 11.26 – 设置登录按钮

  1. 运行游戏,切换到登录选项卡,输入电子邮件地址和密码,然后点击 登录 按钮向 Azure PlayFab 发送登录用户请求,如下图所示:

![Figure 11.27 – The login tab

![img/Figure_11.27_B17146.jpg]

图 11.27 – 登录选项卡

  1. 然后,如果玩家成功登录,游戏场景将被加载,游戏将开始,如下图所示:

![Figure 11.28 – 游戏进行中

![img/Figure_11.28_B17146.jpg]

图 11.28 – 游戏进行中

在本节中,你学习了如何使用 Azure PlayFab API 注册用户,如何通过 Azure PlayFab API 更新 Azure PlayFab 中的用户显示名称,以及如何使用它从 Unity 游戏中登录 Azure PlayFab。接下来,我们将探讨如何在 Unity 游戏中使用 Azure PlayFab 实现排行榜。

在 Unity 中使用 Azure PlayFab 实现排行榜

今天的大多数游戏都使用排行榜,这表明谁是游戏中的最佳表现者,并增加了玩家对游戏的参与度。在本节中,我们将探讨如何在我们的 Unity 项目中使用 Azure PlayFab 实现排行榜。

在 Azure PlayFab 中设置排行榜

为了使用 Azure PlayFab 的排行榜功能,我们首先需要在 Azure PlayFab 的开发者门户中设置一个排行榜:

  1. 返回 Azure PlayFab 中游戏标题的仪表板。在仪表板中,你将在左侧列中找到 排行榜 选项;点击它以打开 排行榜 页面:

![Figure 11.29 – The Leaderboards option

![img/Figure_11.29_B17146.jpg]

图 11.29 – 排行榜选项

  1. 如下图所示,尚未创建排行榜,因此请点击 新建排行榜 按钮在 Azure PlayFab 中创建一个新的排行榜:

![Figure 11.30 – Creating a new leaderboard

![img/Figure_11.30_B17146.jpg]

图 11.30 – 创建新的排行榜

  1. 新排行榜 设置面板中,我们将为此排行榜设置三个属性,即 统计名称重置频率聚合方法,如图 11.31 所示:

图 11.31 – 设置新的排行榜

图 11.31 – 设置新的排行榜

让我们逐一解释这三个属性:

  • UnityBookGame 在此示例中。

  • 重置频率 决定了排行榜应该多久重置一次。有五个选项:

    • 手动:这是默认值,我们保留重置频率设置为这个值,以便排行榜不会自动重置。

    • 每小时:每小时自动重置排行榜。

    • 每日:每天自动重置排行榜。

    • 每周:每周自动重置排行榜。

    • 每月:每月自动重置排行榜。

  • 聚合方法 决定了玩家分数的保存方式。有四个选项:

    • 最后:这是默认选项;无论新值是否高于现有值,都会更新为新的值。

    • 最小值:始终使用最低值。

    • 最大值:始终使用最高值。我们选择此选项以保存游戏中玩家的最高分数。

    • 总和:将此值添加到现有值。

  1. 点击 新排行榜 设置面板中的 保存 按钮;然后,我们在 Azure PlayFab 中设置了一个空排行榜:

图 11.32 – 一个新的空排行榜

图 11.32 – 一个新的空排行榜

  1. 为了允许 Unity 游戏向 Azure PlayFab 发布玩家统计信息请求,我们还需要在 Azure PlayFab 中启用 允许客户端发布玩家统计信息 选项。因此,首先点击左上角的齿轮图标,然后选择 标题设置 选项以打开游戏标题的设置页面,如图所示:

图 11.33 – 打开标题设置页面

图 11.33 – 打开标题设置页面

  1. 在设置页面,点击 API 功能 选项卡以切换到 API 功能 设置:

图 11.34 – API 功能设置

  1. 滚动到 启用 API 功能 部分,启用 允许客户端发布玩家统计信息 选项,并保存,如图 11.35 所示:

图 11.35 – 启用允许客户端发布玩家统计信息选项

现在我们已经在 Azure PlayFab 中创建并设置了排行榜,让我们继续探索如何使用 Azure PlayFab API 从 Unity 更新玩家的分数。

使用 Azure PlayFab API 从 Unity 更新玩家的分数

当玩家完成游戏并且分数高于之前时,我们希望更新 Azure PlayFab 中的排行榜上的玩家分数。让我们执行以下步骤来实现它:

  1. Scripts 文件夹中创建一个新的 C# 脚本,命名为 AzurePlayFabLeaderboardManager,并添加以下代码:

    using System.Collections.Generic;
    using UnityEngine;
    using PlayFab;
    using PlayFab.ClientModels;
    public class AzurePlayFabLeaderboardManager :
      MonoBehaviour
    {
        public void UpdateLeaderboardInAzurePlayFab(int
           score)
        {
            var scoreUpdate = new StatisticUpdate
            {
                StatisticName = "UnityBookGame",
                Value = score
            };
            var scoreUpdateList = new
              List<StatisticUpdate> { scoreUpdate };
            var scoreRequest = new
              UpdatePlayerStatisticsRequest
            {
                Statistics = scoreUpdateList
            };
            PlayFabClientAPI.UpdatePlayerStatistics
              (scoreRequest, OnUpdateSuccess, OnError);
        }
        public void OnUpdateSuccess
          (UpdatePlayerStatisticsResult result) 
        {
            Debug.Log("Update Success!");
        }
        public void OnError(PlayFabError error) 
        {
            Debug.LogError(error.ErrorMessage);
        }
    }
    

让我们按以下方式分解代码:

  • UpdateLeaderboardInAzurePlayFab 方法中,我们创建了一个新的 StatisticUpdate 类实例,它封装了需要更新排行榜的数据。在这里,我们提供了排行榜的名称和玩家的分数。

  • 然后,我们创建了一个 StatisticUpdate 列表并添加了我们刚刚创建的 StatisticUpdate 实例到它里面。

  • 之后,我们创建了一个新的 UpdatePlayerStatisticsRequest 类实例,它封装了我们用来更新排行榜的 StatisticUpdate 列表,并调用 PlayFabClientAPI.UpdatePlayerStatistics 来更新 Azure PlayFab 中的排行榜。

  • 我们还有两个回调函数 – OnUpdateSuccess,当接收到结果时被调用,以及 OnError,当发生错误时被调用。

  1. 然后,我们需要确保当游戏结束时将调用 UpdateLeaderboardInAzurePlayFab 方法。因此,让我们打开 BasicGame | Scenes 文件夹中的示例项目的 Game 场景,如图 图 11.36 所示:

图 11.36 – 示例游戏场景

  1. AzurePlayFabManager 中创建一个新的 GameObject,然后将 AzurePlayFabLeaderboardManager.cs 拖放到它上面,如图 图 11.37 所示:

Figure 11.37 – Setting up the GameObject

图 11.37 – 设置 GameObject

  1. 接下来,我们需要修改示例项目中现有的 C# 脚本。您可以在 BasicGame > Scripts 文件夹中找到 ExampleGameManager.cs 文件;双击它以打开它,如图 图 11.38 所示:

图 11.38 – ExampleGameManager.cs 文件

  1. 将以下代码添加到 ExampleGameManager 类中:

    // ... pre-existing code ...
      [SerializeField]
      private AzurePlayFabLeaderboardManager 
        _azurePlayFabLeaderboardManager;
    // ... pre-existing code ...
      public void GameOver()
        {
        _azurePlayFabLeaderboardManager.
          UpdateLeaderboardInAzurePlayFab(score);
        }
    

让我们按以下方式分解添加的代码:

  • 首先,我们添加一个新的字段来获取 AzurePlayFabLeaderboardManager 实例的引用。

  • 然后,我们在 GameOver 中调用 UpdateLeaderboardInAzurePlayFab 方法来更新排行榜。

  1. 请记住将 AzurePlayFabLeaderboardManager 实例的引用分配到我们刚刚添加的字段:

Figure 11.39 – Assigning the reference to the field

图 11.39 – 分配引用到字段

  1. 让我们回到 Start 场景并在编辑器中运行游戏,使用我们在上一节中创建的玩家账户登录并玩游戏。在下面的屏幕截图中,我们在游戏结束时获得了 4 分:

图 11.40 – 游戏中有 4 分

  1. 同时,转到 Azure PlayFab 的 Leaderboard 仪表板,您可以看到玩家的最高分数是 4 分,并且排名第一:

Figure 11.41 – Azure PlayFab 中的排行榜

图 11.41 – Azure PlayFab 中的排行榜

我们已经调用了 API 来更新 Azure PlayFab 中 Unity 排行榜上玩家的分数。我们的下一个挑战将是从中加载排行榜数据。

在 Unity 中从 Azure PlayFab 加载排行榜数据

在示例项目中,你还可以在BasicGame | Scenes | GameScene中找到排行榜面板,我们将使用它来显示从 Azure PlayFab 加载的前 10 名玩家。默认情况下,此 UI 面板未激活,因此现在在Game视图中看不到它:

图 11.42 – 排行榜面板

图 11.42 – 排行榜面板

在我们开始探索如何从 Azure PlayFab 加载排行榜信息之前,让我们注册更多玩家并向UnityBookGame排行榜添加更多项目,如下面的图所示:

图 11.43 – UnityBookGame 排行榜

图 11.43 – UnityBookGame 排行榜

然后,我们的第一个任务是调用 Unity 中的 Azure PlayFab API 以获取排行榜数据。为此,请按照以下步骤操作:

  1. 返回到AzurePlayFabLeaderboardManager脚本,并添加以下代码:

     // ... pre-existing code ...
        public void LoadLeaderboardDataFromAzurePlayFab()
        {
            var loadRequest = new GetLeaderboardRequest
            {
                StatisticName = "UnityBookGame",
                StartPosition = 0,
                MaxResultsCount = 10
            };
            PlayFabClientAPI.GetLeaderboard(loadRequest,
              OnLoadSuccess, OnError);
        }
    // ... pre-existing code ...
        public void OnLoadSuccess(GetLeaderboardResult
          result)
        {
            Debug.Log("Load Success!");
        }
    

让我们分解添加的代码,如下:

  • 我们创建了一个新方法,并将其命名为LoadLeaderboardDataFromAzurePlayFab

  • LoadLeaderboardDataFromAzurePlayFab方法中,我们创建了一个新的GetLeaderboardRequest实例,并调用PlayFabClientAPI.GetLeaderboard来从UnityBookGame排行榜中检索最多10条记录,起始索引为0

  • 我们还添加了一个新的回调OnLoadSuccess,当收到结果时在控制台窗口中打印"Load Success!"。

  1. 然后,返回到ExampleGameManager.cs并更新以下代码:

    public void GameOver()
    {
      _azurePlayFabLeaderboardManager.
        UpdateLeaderboardInAzurePlayFab(score);
      _azurePlayFabLeaderboardManager.
        LoadLeaderboardDataFromAzurePlayFab();
     }
    

新添加的代码将在游戏结束后从 Azure PlayFab 获取排行榜信息。

  1. 游戏结束后,运行游戏并查看控制台窗口;我们成功从 Azure PlayFab 加载了排行榜数据,如下面的截图所示:

图 11.44 – 加载成功!

图 11.44 – 加载成功!

现在我们已经从 Azure PlayFab 收到了成功的结果,我们的下一个任务是将在 Unity 项目中排行榜 UI 面板中显示这些数据:

  1. 再次返回到AzurePlayFabLeaderboardManager脚本并更新以下代码:

    // ... pre-existing code ...
        [SerializeField]
        private GameObject _leaderboardUIPanel;
        [SerializeField]
        private List<Text> _itemsText;
    // ... pre-existing code ...
        public void OnLoadSuccess(GetLeaderboardResult
          result)
        {
            _leaderboardUIPanel.SetActive(true);
            CreateRankingItemsInUnity(result.Leaderboard);
        }
        private void CreateRankingItemsInUnity
          (List<PlayerLeaderboardEntry> items)
        {
            foreach(var item in items)
            {
                var itemText = _itemsText[item.Position];
                itemText.text = $"{item.Position + 1}:
                {item.Profile.DisplayName} –
                  {item.StatValue}";
            }
        }
    

让我们分解新添加的代码,如下:

  • fields部分,我们引用场景中的LeaderboardGameObject;这是因为排行榜面板默认未激活,我们希望在游戏结束时激活它以显示从 Azure PlayFab 加载的排行榜信息。此外,我们获取用于显示排行榜上每个项目的TextUI 元素列表的引用。

  • OnLoadSuccess中,我们激活场景中的LeaderboardGameObject,并从 Azure PlayFab 接收排行榜信息。然后,调用CreateRankingItemsInUnity方法,该方法以List<PlayerLeaderboardEntry>作为参数。

  • CreateRankingItemsInUnity中,我们更新了Text UI 元素,以显示有关每个项目的信息,包括玩家的排名、显示名称和得分。

  1. 不要忘记将这些 UI 元素相应地分配到游戏场景中AzurePlayFabManager GameObject 的新增字段中,如图图 11.45所示:

图片 11.45_B17146

![图 11.45 – 将 UI 元素分配给新添加的字段 1. 让我们回到开始场景并运行游戏。在下面的屏幕截图中,我们可以看到排行榜 UI 面板在游戏结束时出现,并显示了前 10 名玩家的排名信息:![图 11.46 – Unity 中的排行榜图片 11.46_B17146

![图 11.46 – Unity 中的排行榜

通过阅读本节,你应该现在知道如何在 Azure PlayFab 中设置排行榜,如何使用 Azure PlayFab API 从 Unity 游戏中更新排行榜数据,以及如何使用 Azure PlayFab API 从 Azure PlayFab 获取排行榜数据并在 Unity 中显示。

摘要

本章是本书的最后一章,也是我们漫长冒险的最终阶段。在这个过程中,你学习了如何使用 Unity 游戏引擎开发游戏的各种不同主题,这可能包括你熟悉的领域,例如如何在 Unity 中使用 MVVM 架构模式实现 UI,或者可能包含你之前从未接触过的内容,例如渲染管线和相关数学知识。我希望你喜欢这段旅程,并准备好迎接新的挑战。

除了 Unity 引擎本身之外,本章还重点介绍了 Microsoft Game Dev、Microsoft Azure 云和 Azure PlayFab。我们讨论了它们是什么,以及为什么我们应该考虑在游戏开发中使用它们。然后,我们通过一个示例项目演示了如何创建新的 Azure PlayFab 开发者账户,在 Unity 项目中设置 Azure PlayFab SDK,并通过 Azure PlayFab API 在 Unity 中实现注册、登录和排行榜功能。

通过阅读本章和本书,我希望你现在明白,游戏开发所需的知识不仅仅局限于如何使用游戏引擎;它还涉及编程、计算机图形学,甚至云服务方面的知识。

虽然本章是本书的结束,但这本书只是你游戏开发者旅程的开始。继续学习,不断成长!

嗨!

我是陈家栋,著有《.NET 开发者用 Unity 进行游戏开发》。我真心希望您喜欢阅读这本书,并发现它对提高您使用 Unity 和 Microsoft Azure 云开发游戏的生产力和效率有所帮助。

如果您能在 Amazon 上留下对《.NET 开发者用 Unity 进行游戏开发》的评论,分享您的想法,这将对我们(以及其他潜在读者!)非常有帮助!

点击以下链接或扫描二维码留下您的评论:

packt.link/r/1801078076

您的评论将帮助我们了解这本书哪些地方做得好,哪些地方可以改进以供未来版本使用,所以您的评论真的非常受重视。

祝好,

Packt.com

订阅我们的在线数字图书馆,全面访问超过 7000 本书籍和视频,以及行业领先的工具,帮助您规划个人发展并提升职业生涯。更多信息,请访问我们的网站。

第十二章:为什么订阅?

  • 使用来自 4000 多名行业专业人士的实用电子书和视频,节省学习时间,增加编码时间

  • 通过为您量身定制的技能计划提高您的学习效果

  • 每月免费获得一本电子书或视频

  • 全文可搜索,方便快速获取关键信息

  • 复制粘贴、打印和收藏内容

您知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?您可以在packt.com升级到电子书版本,并且作为印刷书客户,您有权获得电子书副本的折扣。有关更多信息,请联系我们 customercare@packtpub.com。

www.packt.com,您还可以阅读一系列免费的技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。

您可能还会喜欢以下书籍

如果您喜欢这本书,您可能会对 Packt 出版的以下其他书籍感兴趣:

精通 Adobe Photoshop Elements

精通 Adobe Photoshop Elements

Unity 2018.1 实战游戏开发

Raymundo Barrera

ISBN: 978-1-78646-543-6

  • 通过使用行业标准技术,在 Unity 中建立游戏开发的基础

  • 设计和实现原型,快速迭代

  • 建立可重用的框架,使开发更加顺畅

  • 掌握 Unity 的最新功能,保持领先

  • 掌握您在 Unity 中从概念到发布开发专业游戏所需的最佳实践和技术

精通 Adobe Captivate 2019 - 第五版

定制 ASP.NET Core 6.0

Jürgen Gutsch

ISBN: 978-1-80323-360-4

  • 探索 ASP.NET Core 6 中的各种应用程序配置和提供者

  • 启用并使用缓存来提高您应用程序的性能

  • 了解 .NET 中的依赖注入,并学习如何添加第三方 DI 容器

  • 发现中间件的概念,并为 ASP.NET Core 应用编写您的中间件

  • 在您的 API 驱动型项目中创建各种 API 输出格式

  • 熟悉为您的 ASP.NET Core 应用选择不同的托管模型

Packt 正在寻找像您这样的作者

如果您有兴趣成为 Packt 的作者,请访问 authors.packtpub.com 并今天申请。我们已与成千上万的开发者和技术专业人士合作,就像您一样,帮助他们将见解与全球技术社区分享。您可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交您自己的想法。

您可能还会喜欢的其他书籍

posted @ 2025-10-23 15:08  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报