Unity-2020-游戏开发实用指南-全-

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

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

我仍然记得我生命中的那一刻,当我害怕告诉我的父母我要学习游戏开发。在那个时候和我所在的地方,大多数父母认为那是一个孩子的愿望,是一个没有未来的职业,但我足够固执,不在乎并追随我的梦想。如今,游戏开发是最大的产业之一,产生的收入甚至超过了电影产业。

当然,追逐我的梦想比我想象的更困难。追求这个特定梦想的人迟早都必须面对这样一个事实,即开发游戏是一项需要在不同领域具有深入知识的艰巨任务。不幸的是,大多数人因为这种困难程度而放弃,但我坚信通过适当的指导和工具,你可以使自己的职业道路更容易跟随。在我的情况下,帮助我降低学习曲线的是学习使用 Unity。

欢迎来到这本关于 Unity 2020 的书。在这里,你将学习如何使用最新的 Unity 功能以最简单的方式创建你的第一个视频游戏。Unity 是一个提供强大但易于使用功能的工具,用来解决游戏开发中最常见的问题,如渲染、动画、物理、声音、特效等。我们将使用所有这些功能来创建一个简单但完整的游戏,学习处理 Unity 所需的所有细微差别。

在本书结束时,你将能够以一种方式使用 Unity,使你能够开始深入研究你感兴趣的游戏开发领域,或者简单地使你能够创建业余游戏,只是为了乐趣。Unity 是一个多功能的工具,既可以用于专业项目,也可以用于业余项目,而且每天都有越来越多的人在使用它。

值得一提的是,Unity 不仅可以用来创建游戏,还可以用来创建任何类型的交互式应用程序,从简单的移动应用到复杂的培训或教育应用(称为严肃游戏),使用最新的技术,如增强现实和虚拟现实。因此,即使我们在这里创建游戏,你正在开始一条可能通向许多可能专业化的学习路径。

这本书适合谁

由于书的结构,具有不同背景的人可以利用整本书或其中的部分。如果你具有基本的面向对象编程(OOP)知识,但以前从未创建过游戏,或者从未在 Unity 中创建过游戏,你会发现本书是游戏开发和 Unity 概念的一个很好的介绍,从基础到高级。即使你是一名经验丰富的 Unity 开发人员,想要学习如何使用其最新功能,你也会发现本书大部分内容都很有用。

另一方面,如果你没有任何编程知识,你也可以从本书中受益,因为大多数章节不需要编程经验来学习。这些章节将为你提供一个强大的基本技能集,你可以从中开始学习如何在 Unity 中编码,当你学会了这些基础的编码知识后,你可以进入本书的面向脚本的章节。

本书涵盖的内容

第一章《从零开始设计游戏》是我们在打开 Unity 之前讨论本书中要创建的游戏的细节的地方。

第二章《设置 Unity》是你将学习如何在计算机上安装和设置 Unity 的地方,也是你将创建你的第一个项目的地方。

第三章《与场景和游戏对象一起工作》是我们将学习有关场景和游戏对象的概念的地方,这些是 Unity 用来描述你的游戏世界由什么组成的一部分。

第四章,使用地形和 ProBuilder 进行灰盒设计,是我们将创建我们的第一个级别布局,并使用 Unity 的地形和 ProBuilder 功能进行原型设计。

第五章,导入和集成资产,介绍了图形。由于 Unity 不是用来创建图形的工具,而是用来显示它们的,我们将学习如何通过将图形导入 Unity 来改进我们场景的艺术。

第六章,使用 URP 和 Shader Graph 的材质和效果,我们将学习如何使用最新的 Unity 渲染系统(Universal Render Pipeline),以及如何使用 Shader Graph 功能创建效果。

第七章,使用粒子系统和 VFX 图创建视觉效果,是您将学习如何使用两个主要的 Unity 工具来创建视觉效果,比如水和火:粒子系统和 VFX 图。

第八章,使用通用渲染管线进行照明,涵盖了照明。照明是一个足够大的概念,有自己的章节。在这里,我们将加深对通用渲染管线的了解,特别是看看它的照明能力。

第九章,使用后期处理进行全屏效果,深入研究了效果和后期处理。为了获得大多数现代游戏所具有的电影效果,我们将学习如何在我们场景图形的顶部添加一层效果,使用通用渲染管线的后期处理功能。

第十章,声音和音乐集成,涉及一个经常被忽视的领域:声音。在大多数初学者开发者看来被低估了,这里我们将学习如何正确地将声音和音乐添加到我们的游戏中,并考虑其对性能的影响。

第十一章,用户界面设计,探讨了用户界面(UI)的使用。在所有基于图形的向用户传达信息的方式中,使用 UI 是最直接的。我们将学习如何使用 Unity UI 系统以文本、图像和生命条的形式显示信息。

第十二章,使用 Animator、Cinemachine 和 Timeline 创建动画,让我们摆脱简单的静态场景。在这一章中,我们将开始使用动画移动角色,并使用最新的 Unity 功能创建过场动画。

第十三章,使用 C#进行 Unity 脚本编程的介绍,是本书的第一个编程章节。我们将学习如何按照 Unity 的方式使用 C#创建我们的第一个脚本。

第十四章,实现移动和生成,是我们将学习如何编写对象的移动以及如何生成它们。从现在开始,假设具有一般的编程知识。

第十五章,物理碰撞和生命系统,是您将学习如何配置对象的物理设置以侦测它们何时发生碰撞并对碰撞做出反应。为了将其付诸实践,我们将创建一个生命系统。

第十六章,胜利和失败条件,是我们将检测游戏何时应该结束的地方,当玩家赢得游戏或输掉游戏时。

第十七章UI、声音和图形脚本,我们将制作先前创建的 UI,以显示游戏的相关和当前信息,如玩家的健康和得分。此外,我们将研究在必要时播放声音以及使用视觉效果来反映玩家的行为。

第十八章实现游戏 AI 以构建敌人,我们将使用 Unity 的几个功能创建基本的 AI,以在游戏中创建具有挑战性的敌人。

第十九章场景性能优化,探讨了性能问题。使我们的游戏表现良好并不容易,但如果我们想发布游戏,这当然是必须的。在这里,我们将学习如何分析游戏的性能并解决最常见的性能问题。

第二十章构建项目,我们将学习如何将 Unity 项目转换为可执行格式,以便将其分发给其他人并在没有安装 Unity 的情况下运行。

第二十一章最后的修饰,我们将简要讨论在完成本书后如何推进游戏的开发,讨论诸如如何迭代和发布游戏等主题。

第二十二章Unity 中的增强现实,为您介绍了增强现实AR)。在这个额外的章节中,我们将学习如何使用 Unity 的 AR Foundation 包创建 AR 应用程序,这是最近发布的一种使用 Unity 创建 AR 应用程序的方式。

为了充分利用本书

在本书的各章中,您将开发一个完整的项目,虽然您可以只阅读各章,但我强烈建议您在阅读本书的过程中练习项目中的所有步骤,以获得适当学习这里所讨论概念所需的经验。各章的设计使您可以根据自己的喜好定制游戏,但请考虑不要偏离主要思想。

项目文件按章节分成文件夹,并以累积方式设计,每个文件夹只包含该章引入的新文件或与以前章节不同的文件。这意味着,例如,如果一个文件自第一章以来没有改变,您将在第二章以后的文件夹中找不到它。您可以在每个章节文件夹中打开场景文件,以查看游戏在该章节结束时应该是什么样子。这样可以让您看到每章节中发生的变化,并且意味着您可以轻松识别必要的变化。例如,如果由于某种原因,您无法完成第三章,您可以直接从已解决的第四章文件夹中继续。

我们建议您自己输入代码,或者通过 GitHub 存储库访问代码(链接在下一节中提供)。这样做将有助于避免与复制和粘贴代码相关的任何潜在错误。

请注意,本书及其示例是使用 Unity 2020.1.0f1 编写的,这是目前可用的最新版本。这是 Unity 2020 的第一个版本,虽然可能有更新版本,但请考虑,如果您使用它们,书中所示的截图或步骤可能会有轻微差异,但并不难解决。

下载示例代码文件

您可以从您的帐户在www.packt.com下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

您可以按照以下步骤下载代码文件:

  1. 登录或注册,请访问www.packt.com

  2. 选择支持选项卡。

  3. 点击代码下载

  4. 搜索框中输入书名,并按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压缩或提取文件夹:

  • Windows 的 WinRAR/7-Zip

  • Mac 的 Zipeg/iZip/UnRarX

  • Linux 的 7-Zip/PeaZip

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Unity-2020-Game-Development。如果代码有更新,将会在现有的 GitHub 存储库中更新。

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

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:

static.packt-cdn.com/downloads/9781838642006_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

文本中的代码:指示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:“将其着色器设置为Universal Render Pipeline/Particles/Unlit。”

粗体:表示一个新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“创建一个新的空 GameObject(使用GameObject | Create Empty)。”

提示或重要说明

像这样出现。

第一章:从零开始设计游戏

欢迎来到本书的第一章!我相信你和我一样对使用 Unity 进行游戏开发的惊人旅程充满期待。我们将分四部分来接触游戏开发。首先,我们将讨论游戏开发的基础知识,涉及如何在编码之前设计游戏等主题,然后我们将使用 Unity 原型制作一个简单的第一关。然后,我们将深入图形学,找出一个好游戏的外观和感觉。之后,我们将学习如何通过脚本使一切运动起来;最后,我们将看到你如何完成并发布你的游戏。在阅读各章节时,你将把每个概念应用到一个完整的游戏项目中,因此你将以一个完全功能的射击游戏结束本书。

在这一章中,我们将设计我们的游戏《超级射手》。这个阶段被称为前期制作,我们将创建一个开发计划。我们的游戏设计将包括我们想要在游戏中拥有的所有功能:玩家角色、非玩家角色、游戏资产、动画等等。我们将使用屏幕模拟以及叙述来记录我们游戏的设计。我们将在这个过程中讨论有关使用 Unity 开发我们的游戏所需的所有设计工作的相关概念。我们将讨论在整个本章中我们将要做的所有设计工作所需的文档。

具体来说,我们将在本章中研究以下概念:

  • 游戏概念

  • 游戏角色

  • 游戏玩法

  • 难度平衡

  • 文档

游戏概念

为什么不直接开始开发我们的游戏而不是先设计它呢?这个问题源自于开发游戏的兴奋,特别是使用 Unity 游戏引擎。所有的游戏都始于一个想法。这个想法被转化为设计,而这个设计是开发和最终游戏的基础。

游戏的设计就像房子的蓝图。你不会考虑在没有蓝图的情况下建造房子,同样,开发游戏而不先设计它也是一个同样糟糕的主意。这样做的原因是为了节省时间和挫折。对于更大的项目,浪费的时间也意味着不必要的资金支出。

想象一下,你雇佣了一个由 12 名开发人员、动画师和艺术家组成的项目团队。如果你分享了你的游戏想法,他们会有足够的信息吗?他们会做出很棒的东西,但没有一个统一的游戏组件吗?我们在游戏设计中所做的一切就是在一开始尽可能多地记录,以便开发过程是有目的的。毫无疑问,在开发过程中,你将不断修改你的游戏设计,因此拥有一个坚实的起点对你的成功至关重要。

我们的游戏设计将作为我们游戏外观的基础,玩家的目标是什么,游戏玩法是什么,以及支持用户操作、动画、音频、人工智能和胜利条件。这是需要考虑的很多内容,也强调了将游戏想法转化为游戏设计的重要性。

在整本书中,我们将涵盖一系列组件。然而,在本节中,我们将涵盖以下列表中出现的组件:

  • 游戏想法

  • 输入控制

  • 胜利和失败

所以,让我们更详细地看看每个组件。

游戏想法

我们《超级射手》游戏的基本概念是,它将是一个 3D 游戏,以未来英雄士兵为玩家角色。角色必须与敌方士兵作战。这些敌人打算摧毁我们英雄的基地,以及任何阻挡他们的人,包括我们的英雄。他将有一定数量的子弹需要跟踪。

既然我们已经大致知道游戏将会是什么样子,让我们来谈谈玩家将如何控制角色。

输入控制

考虑玩家如何与我们的游戏互动是很重要的。玩家将使用标准的控制方式来控制我们的英雄。玩家期望游戏中实施用户控制的行业规范。因此,我们默认的用户输入控制组合,如下截图所示,将包括键盘和鼠标:

图 1.1 - 控制方案

图 1.1 - 控制方案

我们将配置和编程我们的游戏,以便键盘的用户输入与下表中显示的键和动作配对相匹配:

图 1.2 - 键映射

图 1.2 - 键映射

鼠标也将是用户输入的重要来源。我们将使用鼠标实现两个组件,如下表所示:

图 1.3 - 鼠标映射

图 1.3 - 鼠标映射

左鼠标按钮将是我们的动作按钮。我们需要确保只有当玩家剩下一个或多个子弹时才会射击子弹。

这就是我们处理输入的方式,但有时我们需要结束游戏会话!让我们谈谈玩家如何获胜和失败。

获胜和失败

我们的获胜条件是当所有敌人波都被消灭时。玩家可以输掉游戏的两种不同方式。第一个失败条件是当基地生命值变为0时。第二个失败条件是如果英雄的生命值变为0

通过这个简短的描述,你可以知道有几件事情需要跟踪,包括以下内容:

  • 剩余波数

  • 我们基地的生命值

  • 我们英雄的生命值

现在,我们已经定义了所谓的游戏“核心循环”(开始一个关卡,玩它,赢/输它,然后重复)。让我们深入探讨具体细节,从我们的角色开始。

游戏角色

我们的游戏将包含几个对象,但只有两种游戏角色。第一个游戏角色是我们的英雄,将由玩家控制。第二种游戏角色是敌人。他们是由人工智能控制的非玩家角色。让我们更仔细地看看这两个角色。

英雄

玩家将扮演我们游戏的主角英雄。这是一个我们将导入并在游戏中使用的角色。那么,我们的英雄玩家角色能做什么呢?我们已经知道我们将能够使用键盘和鼠标输入的组合在游戏环境中移动他们。我们也知道左鼠标按钮 - 我们的动作按钮 - 会让他射击子弹。

重要提示:

因为英雄是由人类玩家控制,所以被称为玩家角色。

我们将为英雄实现以下动画:

  • 空闲:当角色没有被玩家移动时,将播放此动画。

  • 奔跑:当角色被玩家移动时,将播放此动画。

  • 射击:这是一个会让英雄射击子弹的动画。

这是我们的玩家。现在,让我们讨论我们的敌人角色。

敌人

我们游戏的反派将是敌人士兵。我们将控制他们在游戏中的数量和位置。我们还将通过人工智能控制他们的行为。敌人将直奔基地,一旦到达那里,他们将开始破坏它。我们将确定我们的基地被完全摧毁需要多长时间。

信息框:

因为敌人是由人工智能控制而不是人类玩家,所以被称为非玩家角色。

我们将为敌人士兵实现以下动画:

  • 奔跑:当敌人看到英雄时,他们将向英雄奔跑,并在靠近玩家足够近时停下来开始攻击。

  • 攻击:当敌人靠近攻击基地或玩家时,将播放此动画。

  • 死亡:当敌人被玩家击败时,将播放此动画。

我们需要仔细的规划和脚本编写来创建所需的敌人行为。敌人的数量和位置是我们需要做出的决定。

这定义了我们角色的细节。现在,让我们讨论游戏将如何进行,看看具体的细节。

游戏玩法

游戏将从玩家位于游戏世界中心开始。由玩家控制的英雄需要保卫基地免受敌人的攻击。为了击退敌人,英雄将拥有一定数量的子弹。目标是在基地被完全摧毁之前击败所有的敌人。

让我们看看我们将如何实现所有这些。本节涵盖了以下游戏组件:

  • 游戏世界布局

  • 起始条件

  • 结束条件

  • 积分系统

  • 头顶显示HUD

我们将涵盖每个前面的组件,并讨论它们如何改变游戏体验。让我们从游戏世界的设计开始谈起。

游戏世界布局

我们将创建我们的基地环境,其中包括大型金属地板砖、墙壁、敌人生成的门,以及屏幕底部的基地建筑,敌人需要到达并开始攻击它。

这是我们游戏世界的形状的模拟:

图 1.4 - 基地布局

图 1.4 - 基地布局

在前面的模拟中有四个基本的事物,列举如下:

  • :无法穿透的屏障,防止玩家走出游戏区域。

  • :与墙壁一样坚固,也将作为敌人的生成位置。敌人将在它们的后面生成,并可以穿过它们进入我们的基地区域。

  • 玩家起始:英雄的起始位置。

  • 基地建设:我们的基地。敌人必须足够接近才能攻击它。

随着我们基地级别设计的完成,让我们讨论玩家如何以温和的方式进入这个世界。

起始条件

当我们的游戏首次启动时,我们将设置几个起始条件。以下是这些条件的列表:

  • 敌人生成点(门)的数量和位置

  • 英雄持有的子弹数量

  • 基地的位置

让我们看看每个这些起始条件:

  • 第一个起始条件是敌人的生成位置。正如您在我们之前的模拟中看到的,游戏中将有几个可能的生成点(门)。我们将编写一个脚本,以便每次在随机位置生成敌人的波数。每一波都会增加敌人的数量,增加难度。

  • 玩家持有的子弹数量必须经过仔细考虑。它需要足够大,以在敌人耗尽之前击败敌人。

  • 我们的第三个和最后一个起始条件是基地的位置。正如您从前面的截图中看到的,它位于门的对面 - 因此,敌人必须穿过它们之间的整个空间,给玩家一个攻击他们的机会。

我们已经定义了敌人生成规则以及玩家如何玩游戏。现在,让我们谈谈游戏将如何结束,看看具体的实现方式。

结束条件

到目前为止,我们已经确定了游戏中要跟踪的几个组件。让我们记住它们 - 它们如下:

  • 剩余波数

  • 基地健康

  • 玩家健康

根据我们之前对游戏结束条件的决定,我们可以应用以下数学检查来确定游戏是否结束以及结果是什么。每个游戏结束条件都列在下表中,以及结果:

图 1.5 - 游戏结束条件

图 1.5 - 游戏结束条件

为了实现这三个游戏结束条件,我们知道我们必须跟踪波数、玩家健康和基地健康。这是必须的。

现在我们有了一个完整的游戏,让我们考虑如何通过实现一个经典的积分系统使其更有价值。

积分系统

由于我们正在跟踪涉及数字的关键信息,这使得我们可以轻松实现一个积分系统。例如,每次消灭一个敌人,我们可以给玩家 50 分,或者每次敌人损害基地时我们也可以扣分。在我们的情况下,我们将只在敌人被杀时给分,但如果您愿意,可以随意扩展这个领域。

现在,我们有几个玩家需要了解的系统,但是目前玩家没有任何方法可以对其做出知情决策。因此,让我们看看如何使用 HUD 来改进。

HUD

我们决定在游戏过程中跟踪信息,这些信息不仅在计算得分和游戏结束时有价值。玩家会希望看到这些信息,因为它往往会提供动力,并增加游戏的乐趣。因此,我们将为玩家创建一个 HUD,并动态更新游戏中的数据。

信息框:

HUD 是屏幕上始终存在的信息视觉层。

这是我们超级射手游戏中 HUD 的模拟图:

图 1.6 – UI 布局

图 1.6 – UI 布局

正如您所看到的,我们的 HUD 有几个组件,如下:

  • 英雄生命值:经典的生命值条,让我们可以直观地看到剩余的生命值。我们选择条形图而不是数字,因为在激烈战斗中更容易看到,而不是读取数字。

  • 英雄头像:生命值条旁边的图像,只是为了展示我们英雄的面孔。

  • 得分:我们已经获得的分数。

  • 子弹:剩余的子弹数量。玩家必须经常检查这个数字,以避免用尽子弹。

  • 剩余波次/敌人:关于当前波次和游戏状态的信息,只是让玩家知道游戏何时结束,在此过程中给他们施加一些压力。

  • 基地生命值:另一个重要的信息。足够大,让玩家注意到基地正在受到攻击,并在这种情况下采取行动。

最后,我们有一个完全成熟的游戏,有很多关于它将如何行为的规则和规范,我们现在可以开始创建我们的游戏。然而,有一个好的实践是:平衡游戏永远不会太早。

难度平衡

确定游戏难度时需要考虑很多因素。如果太难,玩家会失去兴趣,如果游戏太容易,可能不会吸引目标受众。一些游戏包括供用户选择的难度级别。其他游戏有多个级别,每个级别难度逐渐增加。为了实现我们期望的难度平衡,我们必须应对几个问题。

在本节中,我们首先将看一些与难度平衡相关的问题,然后是我们的实施计划。

难度平衡问题

我们需要在游戏设计中考虑很多关于我们游戏的问题。回顾本节中的问题将帮助我们了解即使是像我们这样简单的游戏也必须应对的问题,以实现期望的难度平衡。

以下是第一组问题,与我们游戏中难度的整体实施有关:

  • 我们是否应该有玩家可选择的不同难度级别?

  • 每个难度级别会有什么具体不同?

  • 我们是否应该有多个游戏级别,每个级别难度增加?

  • 每个游戏级别会有什么具体不同?

考虑以下关于我们游戏中敌人的问题:

  • 每个波次应该生成多少敌人?

  • 敌人应在多远的距离内意识到英雄?

  • 敌人每次攻击应对玩家造成多少伤害?

  • 敌人可以忍受多少伤害才会死亡?

以下列出的下一组问题涉及我们的可玩角色,英雄:

  • 角色应该有多少生命?

  • 角色应该从单个敌人的攻击中受到多少伤害?

  • 角色应该能够超越敌人吗?

我们还需要考虑基地和子弹在我们的游戏中。以下是我们将在游戏中实施的这些游戏资产的一些问题。对于基地,问题如下:

  • 敌人摧毁基地需要多少次攻击?

  • 一波中产生的敌人的理想最大数量是多少?

  • 门和基地应该在游戏环境中的哪里?

现在,让我们讨论子弹的情况下的问题,如下所示:

  • 玩家应该以什么速度用尽子弹?

  • 玩家可以拥有的子弹的最大数量是多少?

  • 子弹会对敌人造成多少伤害?

正如你所看到的,我们需要回答几个问题作为我们设计的一部分。一些问题可能看起来多余,因为它们涉及游戏中的多个组件。现在,让我们回答其中一些。

实施计划

根据上一节中提出的问题,我们必须想出一些答案。所以,让我们在这里做到这一点。我们的第一组决策集中在整体游戏概念上,如下所示:

  • 实施一个游戏级别。

  • 为用户提供三种游戏难度设置:简单普通困难

现在我们决定创建三个游戏级别,我们必须确定它们将如何不同。这可以通过使用矩阵轻松管理。当我们填写矩阵时,我们将能够记录大多数先前列出的问题的答案。这就是我们将称之为难度实施矩阵的东西。

图 1.7 - 每个级别的难度通过

图 1.7 - 每个级别的难度通过

还将有一组决策,不会根据用户选择的难度级别而改变。以下是这些决策的列表:

  • 敌人的攻击性不会改变。我们将对它们进行脚本编写,以便如果它们意识到英雄,它们将攻击他。

  • 我们将为敌人建立一个相当小的视野范围,这样英雄可以轻松地溜过它们,也许更重要的是,超过它们。

  • 重生将在游戏模型中先前确定的生成点之间随机进行。

重要的是要考虑到这是第一次平衡调整,我们肯定会根据我们进行游戏实施时进行的测试进行更改。

现在,我们可以说游戏设计已经完成了,还是可以吗?实际上,游戏设计永远不会结束;随着游戏的开发,它将不断发展,但让我们把这个留到以后。现在,让我们谈谈如何用文档与团队中的每个人沟通我们伟大的想法。

文档

现在我们已经涵盖了游戏的所有主要方面,重要的是要准备好与他人分享。通过这本书,你可能会独自工作,但在现实生产中,你可能会与他人合作,因此分享你的愿景是你需要获得的关键技能,以创造成功的游戏。你不仅会与队友分享你的愿景,还会与希望投资你的游戏项目的潜在投资者分享。在本节中,我们将提供关于如何将游戏信息正确格式化为可理解文件的建议。

游戏设计文件(GDD)

这个文件基本上是你的游戏圣经。它包含了游戏的所有方面的分解,每个方面都有关于不同游戏系统应该如何工作的详细解释。在这里,你将把我们之前在实施计划中回答的问题和答案放在一起,并深入研究它们。记住,你脑海中有一个想法,确保其他人理解那个想法是复杂的,所以不要低估这一重要任务。

也许你正在独自制作一款游戏,并且说你不需要 GDD,因为所有的想法都可以在你的脑海中得到体现,对于非常小的游戏来说可能是真的,但任何规模的游戏和团队都可以从 GDD 中受益。它将作为你的笔记本,记录下你自己的想法并阅读它们。这很重要,因为在你的脑海中一切都是有意义的,但一旦你阅读自己的想法并审查它们,你会发现很多盲点可以很容易地修复,而不是在编写整个游戏时才发现它们。

让我们开始谈论 GDD 的结构可以是什么样的。

GDD 格式

遗憾的是,没有一种标准或独特的创建 GDD 的方式。每家公司和团队都有自己的做法,不仅在创建它时使用的工具方面,还有文件的内容。这在很大程度上取决于团队的规模(或团队)、游戏类型和公司背后的一般文化。事实上,一些公司实际上认为没有必要创建 GDD。

开始创建它们的一个好主意是查看几款游戏的已发布 GDD。有很多这样的游戏,包括大型知名游戏(如《毁灭战士》)。它们大多是 Word 文档,其中有解释游戏系统(如武器、库存等)的部分,以及所有角色的列表,而有些可能只是一个列出游戏不同部分的事实的项目列表。之后,你可以开始尝试不同的 GDD 格式,以适应你的项目和团队。

一旦你决定了一个好的格式,你必须实际决定你将如何写这个格式,除了使用纸和笔,一个更好的主意是利用所有那些出色的数字工具。让我们看看其中一些。

GDD 创建工具

在审查现有的 GDD 之后,下一步是选择一个合适的工具来编写它。你需要考虑的第一件事是,GDD 会经常发生很多变化。在创建游戏的过程中,你将验证或放弃你在 GDD 中写的想法,所以使用一个动态工具是个好主意。这可以通过你熟悉的任何文本处理器来实现,但你还需要解决其他问题,也许文本处理器不够用。

你的 GDD 会很大,我的意思是,非常大,即使对于简单的游戏来说也是如此,所以它会有很多部分,你会发现整个部分都会引用其他部分,生成一个大的链接网络连接文档的几个部分。在这种情况下,使用任何一种维基而不是文本处理器来管理这一点是个好主意,维基是极其有用的工具,我强烈建议在这些情况下使用。它们允许你将整个 GDD 分解为可以轻松编辑和链接到其他文章的文章,而且许多维基还允许你协作编辑文章。还有其他附加功能,比如评论,允许在 GDD 内部对一个功能进行整个对话,并记录下来以供将来参考。与 GDD 相关的维基百科页面可以在以下截图中看到:

图 1.8 - 维基百科网站

图 1.8 - 维基百科网站

此外,你还可以使用其他工具,比如 Google Drive,它允许你混合不同类型的文档 - 从常规文本文档到动态幻灯片 - 来创建演示文稿,用简单而强大的媒体传达复杂的方面。此外,Google Drive 还有许多出色的协作工具,可以改善多人共同参与 GDD 的工作方式。

我们描述的所有工具都是通用的解决方案,用于撰写一般文件,它们可以像魅力一样工作,但也有其他专门为游戏设计的工具。

现在,让我们开始写我们的 GDD。我知道我说没有标准格式,但至少让我们看看每个 GDD 应该有什么,从电梯演讲开始。

电梯演讲

想象一下,你在一栋大楼里乘坐电梯,下一层,一个重要的游戏投资者刚好进来了。他们按了十楼的按钮,所以你有八层的时间来说服他们把钱投入你的口袋,帮助你创造游戏。我知道这是一个不太可能的情况,但在现实生活中,当你站在一个圆桌前面的投资者面前时,你没有太多时间说服他们。记住,在你身后可能有成千上万的开发者想要做同样的事情,所以你必须快速而直接,这就是为什么拥有一个好的电梯推销如此重要。

电梯推销可能是你在 GDD 中找到的第一句话,也是最重要的一句。它需要用不超过两行来描述你的游戏,并说服阅读 GDD 的人你的游戏是一个好主意——你需要让他们立刻想要玩你的游戏。是的——听起来非常雄心勃勃,而且确实如此,但这可以让你脱颖而出,与众多想为他们的游戏获得资金的开发者区分开来。

再次强调,没有标准的公式可以创造成功的电梯推销(如果有这样的东西,我们都会变得富有),但以下是一些需要考虑的建议:

  • 你必须在不超过 10 秒的时间内做出你的推销。再长一点,你就会失去你试图说服的人的兴趣。

  • 你必须对自己的想法有信心;没有人会投资于你自己都不确定的游戏是否是下一个大作。

  • 不要使用任何技术性词汇(我在看你,程序员)。

  • 包括你的游戏与所有其他游戏的区别。

  • 反复修改,直到你能说服身边的任何人来玩这个游戏,试着找一个最诚实的人来测试——一个不会介意把你的想法击得粉碎的人(如果你的想法真的值得的话)。

  • 反复练习,站在镜子前,直到你能够清晰、明确地一次说出来。

以下是一些电梯推销的例子:

  • 想象一下用你的手臂和力量屠杀巨大的希腊神祗,直到你成为奥林匹斯之王。你将在[在这里插入一个完全不存在的游戏名称]中感受到那种力量。

  • 文明已经崩溃。可怕的感染让人们变成了僵尸。你拥有唯一的解药,必须穿越整个国家去交付它,否则人类将崩溃。

好吧——现在这些推销可能不是很原创,但几年前它们是的。想象一下那时这些推销的力量;你必须找到类似的东西。我不是说这很容易,但看看只有两行可以成为惊人体验的开始,所以首先专注于写下这两行,然后再写游戏的其余部分。

现在你已经吸引了投资者的注意,是时候向他们展示所有的游戏系统和一些细节来进一步激发他们的热情,好吧,现在不是时候。你刚刚吸引了他们的注意,你还没有说服他们。现在是时候开始谈谈你的游戏,一个高概念是一个很好的方式。

一个高概念

一个高概念是一系列陈述,进一步简洁地描述你的游戏,即使你不是在说服投资者,这些陈述也会概述你的游戏将如何定义。

一个好的高概念可以包括以下部分:

  • 电梯推销:正如我们在前一节中所解释的。

  • 类型:也许您正在创造以前从未见过的新东西,但它可能会受到其他几款游戏的启发。在这里,您将指定您的想法基于哪种类型的游戏,以便文档的读者可以开始想象游戏的玩法。稍后,您将指定区别,但最好先提出一个众所周知的想法,以便开始构建读者的概念。此外,您还可以在此处指定玩家在游戏中的视角和设置,例如:顶部中世纪 Roguelike 角色扮演游戏(RPG)

  • 平台和人口统计:您需要非常清楚地知道谁会玩您的游戏。为北美成年人创建游戏与为中国青少年创建游戏或为那些想在回家的路上或公交车上分散几分钟注意力的商务人士创建游戏并不相同。这些人群会希望不同的体验,不同挑战的水平和游戏时长。他们甚至会使用不同的设备来玩游戏。考虑到这一点,将有助于您找到最适合目标受众的游戏机制和平衡。通常人们会说他们正在为自己创建游戏,但请记住您不会购买那款游戏,因此在创建游戏时也要考虑您的钱包,例如:休闲手机平台玩家

  • 特点:列出您的游戏将具有的不超过三到五个特点。列出您将从所选择的游戏类型中使用的特点,例如:您将用各种武器击败敌人的波浪。您将升级您的飞船以提高其属性

  • 独特卖点(USP):这与特点列表类似,但在这里,您将包括区分您的游戏与其他游戏的特点(不超过三到五个),例如:您可以使用跑酷风格的动作穿越场景。您可以使用掠夺的材料制作全新的武器。想想这些特点在多年前是多么独特。

再次强调,没有理想的高概念。也许您会发现游戏的其他方面可以在此处突出并添加到文档中,但请尽量将所有内容放在一页上。

现在我们已经讨论了每个 GDD应该具备的内容,让我们谈谈 GDD可能具备的内容。

创建 GDD 的提示

现在,是时候定义整个游戏了。我们说 GDD 没有标准格式,但至少在创建时可以考虑几种良好的做法。以下列表突出了其中一些:

  • 可读性:您的 GDD 必须准备好供任何人阅读,包括没有游戏开发知识的人。不要使用任何技术术语(猜猜我现在在看谁),并尽量保持简单。测试您的 GDD 可读性的一个好方法是将其交给您的祖母或任何您认为与游戏毫不相干的人,看看他们是否能够阅读。

  • 背景和介绍:在开始描述游戏机制之前,让读者置身于游戏中。描述世界、玩家角色、他们的背景故事、动机以及玩家需要努力解决的主要问题。让 GDD 的读者对游戏的背景感兴趣,并希望继续阅读,看看他们将如何玩游戏并应对游戏中玩家将面临的所有任务。

  • 游戏玩法部分:这些部分将游戏分解为相互关联的几个系统和子系统。一些示例可以是库存、任务、制作、战斗、移动、商店等等。您将需要非常具体地说明这些系统的每个方面,因为请记住,这份文件将被团队用来制作游戏的代码和资产。我们在本章的前几节中进行的所有先前分析都将在这里,并将进一步解释和分析。

  • 内容部分:你还会想创建内容部分,比如我们之前设计的部分。这些可以是——但不限于——角色、故事、世界、关卡、美学、艺术资产、声音和音乐资产、经济和输入。

  • 分享你的想法:在将你的想法永恒化在 GDD 上并让每个人开始制作它们之前,讨论不同的 GDD 部分然后再标记它们为完成。与你的团队、互联网上的人、朋友讨论——每个人都可以给你有价值的反馈关于你的想法。我很确定你在想你的想法会被互联网上的某个随机人偷走然后在你之前发布相同的游戏——这种情况可能会发生——但我并不是说分享整个 GDD,只是一些你对某些实施细节不确定的细节。

  • 保持控制:团队中的每个人都是游戏设计师——有些人比其他人更多。每个人都会有自己的想法和他们会做不同的事情。倾听他们——这样做会很有用,但记住你是负责人,你将有最后的决定权。你需要开放,但设定一些限制,不要偏离你最初的想法和概念。防止著名的功能蔓延:知道什么是足够。再次强调,这并不是一项容易的任务——你会以艰难的方式学到这一点,相信我,但当发生这种情况时,请记住这一点:我告诉过你!

  • 游戏会改变:我已经说过了,但我想尽可能强调这一点。游戏会因为你在创建过程中发现的许多原因而发生很大的变化。你可能会发现X机制并不那么有趣,你创造了更好的处理Y系统的方法,你认为值得花时间改变已经存在的游戏部分,因为测试表明玩家不理解如何使用你游戏中全新的关键功能,等等。要对变化持开放态度,并调整你的游戏想法。如果你以正确的方式做到这一点,你的游戏不会像你最初想象的那样,但会是它的更好版本。

  • 图形:使用图形、图表等。尽量避免大段文字。记住一张图片胜过千言万语。你正在沟通,没有人想要花费宝贵的时间来理解你想要表达的内容。提高你的视觉沟通能力,你将拥有一个专注的团队。

  • 纸上原型:在将想法写入 GDD 之前,你可以在纸上测试一些想法。即使你的游戏是一个狂热的“打击他们”,你也可以让小纸人物在桌子上移动,看看他们如何攻击玩家,以及他们将会有哪些移动模式。做一些数学来看看完美的时间、伤害和生命值等。

  • 定期原型:当你的游戏正在开发时,GDD 将会根据玩家的反馈不断变化。你必须测试你的游戏,即使它还没有完成,并尽早从玩家那里得到反馈。当然,他们会告诉你很多你已经知道的事情,但他们会看到你没有看到的很多问题,因为你每天都在创作和玩你的游戏。他们有第一次玩游戏的优势,这是一个真正的改变。

游戏设计和 GDD 的创建是一个复杂的主题,可以在几章中探讨,但有很多书籍正是这样做的,游戏设计并不是本书的主要内容。

在这之后,我们可以开始创建我们的 GDD,并记住:你需要找出对你最有效的格式是什么。

总结

在这一章中,我们完全设计了我们的超级射手游戏,并计划利用我们的设计来推动我们的开发工作。我们的游戏设计包括功能、玩家角色、非玩家角色、游戏资产、动画等等。我们使用屏幕模拟来帮助记录我们游戏的设计。此外,我们计划了游戏的难度平衡,以确保游戏根据用户选择具有适当的难度。我们讨论了什么是 GDD,我们如何创建它,以及它和游戏设计在游戏制作过程中会发生什么变化。

请记住,这很重要,因为您希望在编写游戏代码之前回答所有可能的问题。如果您不这样做,您将不得不一遍又一遍地重新编写游戏的部分,以解决每一个意想不到的问题。您无法预防所有可能的复杂情况,但至少通过这种分析,可以解决大部分问题。

在下一章中,您将学习如何开始使用 Unity。您将了解为什么 Unity 是一个很好的选择来开始创建游戏。您将创建您的第一个游戏项目,并分析它是如何组成的。

第二章:设置 Unity

在本章中,我们将学习为什么 Unity 是一个很好的游戏引擎入门工具。开始游戏开发职业有很多种方式,因此选择合适的工具是迈出的重要第一步。然后,我们将看到如何安装 Unity 并使用 Unity Hub 创建项目,这是一个管理不同 Unity 安装和项目的工具,帮助我们处理大量项目。

具体来说,在本章中我们将研究以下概念:

  • 为什么要使用 Unity 这样的游戏引擎?

  • 安装 Unity

  • 创建项目

让我们首先讨论为什么选择 Unity 来开始你的游戏开发职业。

为什么要使用 Unity 这样的游戏引擎?

当你想创建一个游戏时,有几种方法可以选择,每种方法都有其利弊。那么,为什么选择 Unity 呢?在本节中,我们将讨论这些原因,概述先前和当前行业状态,并具体看到以下概念:

  • 过去和现在的行业洞察

  • 游戏引擎

  • Unity 的优势

过去和现在的行业洞察

在开始阶段,用户们在设备资源有限但游戏设计简单的情况下挣扎。随着行业的发展,硬件变得更加强大,游戏也比以前更加复杂。一个大型 AAA 游戏标题需要近 200 名开发人员,负责游戏的不同领域。这些角色中的每一个都需要多年的经验,使得游戏成为一项昂贵且风险高的任务:你永远不知道一个游戏是成功还是浪费了大量资金。因此,一个人很难制作整个游戏。

重要提示:

AAA 游戏是由许多在大公司工作的人创建的,通常成本数百万美元。还有 AA 游戏,这意味着团队规模和预算的差异。

过去,程序员需要学习如何使用许多工具来解决不同的游戏开发问题。一些工具停止了来自其创建者的支持,留下了未解决的错误和功能。因此,大公司开始雇佣高技能的开发人员来创建所有这些工具,结果就是所谓的游戏引擎。让我们来回顾一下这是什么。

游戏引擎

游戏引擎是一组不同的软件,解决游戏开发中的问题,如音频、图形和物理问题,但它们被设计为一起工作,都遵循相同的理念。这很重要,因为每个团队和公司都有自己的工作方式。从零开始创建游戏引擎是一项艰巨的任务,只有少数大公司能够做到这一点。公司创建的游戏引擎通常是私有的,所以只有公司被允许使用它们。一些公司出售他们的引擎,但成本太高。但几年前出现了另一种获取游戏引擎的方式。

你可能听说过由 1 至 10 名开发人员创建的独立游戏,但是这样一个小团队如何创建游戏呢?答案是通用游戏引擎。这些游戏引擎就像公司创建的那些一样,但它们被设计为每个游戏的良好基础,并提供一个准备好供任何游戏开发者使用的工具集。这些引擎创造了一整代的热衷开发者,他们现在能够比以前更容易地开发自己的游戏。过去有很多游戏引擎公司,但只有少数公司今天还在生存,Unity 是其中最有影响力的之一。但为什么呢?让我们进一步讨论这个问题。

重要提示:

其他通用引擎的例子包括虚幻引擎、Godot、扭矩和 CryEngine。

Unity 的优势

好吧,有很多潜在原因解释了为什么 Unity 如此受欢迎。让我们列举其中一些,如下:

  • Unity 的设计初衷是简单易用,具有非常简单和精致的界面,以及少量但功能强大的工具设置。这有助于新手在启动引擎的第一秒就不会立即感到迷失。

  • Unity 的编程语言 C#对于初学者和高级程序员都非常熟悉,使用 C#进行编码的 Unity 方式简洁易懂。Unity 和 C#处理了您在其他语言中可能遇到的大部分编程问题,大大减少了您的生产时间。

  • 当移动游戏市场时代开始时,Unity 就已经存在了,它的创作者们全力以赴地创造了任何移动引擎所需的所有功能。在我看来,这是 Unity 成为今天的样子的最重要原因之一。

  • 随着其他新技术,如增强现实(AR)和虚拟现实(VR),Unity 不仅将其用于游戏,还用于应用程序、培训模拟、建筑可视化、汽车行业、电影等等。使用 Unity,您可以为广泛的行业创建应用程序,它们的使用范围每年都在增加。

  • Unity 拥有庞大的开发者社区,他们创建了参考书目和教程,提出并回答问题,并为引擎创建插件。所有这些在您开始使用 Unity 时都会有很大帮助,因为解决问题的答案只是一个谷歌搜索(有时只是几美元)之遥。

  • 由于其增长,全球范围内有大量的 Unity 工作机会,比其他游戏引擎的工作机会更多,其中一些工作机会正在寻找初级开发人员,因此新人进入这个行业的机会很大。

Unity 并非完美无缺,它也有缺点,还有其他引擎(如 Unreal Engine 4 或 Godot)与 Unity 竞争,它们在一些方面比 Unity 更好,但也有自己的缺点。在我看来,选择 Unity 还是其他引擎取决于您的意图以及您习惯使用的技术,但归根结底,您可以使用 Unity 做任何您需要做的事情,并借助其庞大的社区来解决任何弱点。现在我们了解了 Unity,让我们看看如何安装引擎。

安装 Unity

好的,经过所有这些,您已经决定选择 Unity——很好的决定!现在,我们从哪里开始呢?让我们从一个简单但必要的第一步开始:安装 Unity。这似乎是一个直接的第一步,但我们可以讨论一下正确的安装方式。在本节中,我们将讨论以下概念:

  • Unity 技术要求

  • Unity 版本

  • 安装 Unity

要运行 Unity 2020,您的计算机需要满足以下要求:

  • 如果您使用 Windows,您需要 Windows 7 SP1 或更高版本,8 或 10。Unity 只能在这些系统的 64 位版本上运行;不支持 32 位。

  • 对于 Mac,您需要 macOS High Sierra 10.13 或更高版本。

  • 对于 Linux,您需要 Ubuntu 16.04 或 18.04 或 CentOS 7。

  • 您的 CPU 需要支持 64 位和 SSE2(大多数 CPU 都支持)。

  • 具有 DirectX 10 支持的显卡(大多数现代 GPU 都支持)在 Windows 上,Mac 上支持 Metal,在 Linux 上支持 Open GL 3.2+或 Vulkan 支持。

现在我们知道了要求,让我们讨论一下 Unity 的版本控制系统。

Unity 版本

在以前的 Unity 版本中,我们通常只需下载特定 Unity 版本的安装程序,然后一直点击“下一步”直到安装完成。但是当您专业使用 Unity 时,您需要安装几个不同版本的 Unity,因为您将使用不同版本制作的不同项目。您可能会想为什么不能只使用最新的 Unity 版本来处理每个项目,但这会带来一些问题。

在 Unity 的新版本中,通常会有很多关于引擎如何工作的变化,因此您可能需要重新调整游戏的许多部分以进行升级。此外,您可能正在使用尚未适应更新的插件,因此这些插件将停止工作。在我的个人项目中,我习惯于进行项目升级;但仅仅出于学习目的,在具有特定发布日期的项目中,可能需要花费大量时间来升级整个项目,这可能会大大推迟发布日期。也许您需要一个特定的功能,该功能随着更新而带来了很大帮助。在这种情况下,升级的成本可能是值得的,但请注意,大多数情况下,这种情况并不会发生。

管理使用不同 Unity 版本制作的不同项目,安装和更新新的 Unity 版本等过去是一件很麻烦的事情,但 Unity Hub 就是为了帮助我们解决这个问题而创建的,并且已经成为安装 Unity 的默认方式。让我们更多地了解一下。

使用 Unity Hub 安装 Unity

Unity Hub 是您在安装 Unity 之前安装的一小部分软件。它集中管理所有您的 Unity 项目和安装。您可以从 Unity 官方网站获取它。下载它的步骤经常会发生变化,但在撰写本书时,您需要执行以下操作:

  1. 前往unity.com

  2. 点击开始蓝色按钮,如下截图所示:图 2.1-Unity 网站上的开始按钮

图 2.1-Unity 网站上的开始按钮

  1. 点击个人选项卡,然后点击个人部分下的开始按钮,如下截图所示:图 2.2-选择个人/免费许可证

图 2.2-选择个人/免费许可证

  1. 点击新用户部分的从这里开始按钮,如下截图所示:图 2.3-开始下载

图 2.3-开始下载

  1. 接受条款和条件,如下截图所示:图 2.4-同意隐私政策

图 2.4-同意隐私政策

  1. 执行下载的安装程序。

请注意,我们使用的是 Unity Hub 2.3.2,这是撰写本书时的最新版本。如果您使用更新版本,某些步骤可能会发生变化,但主要概念通常保持不变。现在我们已经安装了 Unity Hub,我们必须使用它来安装特定的 Unity 版本。您可以按照以下步骤执行:

  1. 启动 Unity Hub。

  2. 它可能会要求您创建一个 Unity 账户。如果是这样,请创建一个账户并用该账户登录。如果没有,请点击窗口右上角的人物图标,然后选择登录以有登录或创建账户的选项,如下截图所示:图 2.5-登录到 Unity Hub

图 2.5-登录到 Unity Hub

  1. 按照安装程序上的步骤,然后您应该会看到以下屏幕:图 2.6-Unity Hub 窗口

图 2.6-Unity Hub 窗口

  1. 新版本的 Unity 会指导您完成 Unity 安装和第一个项目的创建。在这种情况下,请跳过下一步,但如果您已经安装了 Unity,请按照它们的步骤进行。

  2. 点击安装按钮,检查您是否在那里列出了 Unity 2020。如果没有,请按添加按钮。确保选择了最新的 Unity 2020 版本(在我的情况下是 Unity 2020.1.0f1),然后点击下一步按钮。您的屏幕可能显示比我的更新版本,所以不用担心。该过程如下截图所示:图 2.7-选择要安装的 Unity 版本

图 2.7-选择要安装的 Unity 版本

重要提示:

这是我们将在第十三章**,使用 C#进行脚本编写的介绍中使用的程序。我们现在不需要其他 Unity 功能,但如果您需要,可以随时返回并安装它们。

  1. 将会出现一个功能选择窗口。确保Microsoft Visual Studio Community被选中。在撰写本书时,最新版本是 2019,但更新版本也同样适用。现在,点击下一步按钮。该过程如下截图所示:图 2.8 - 选择 Visual Studio

图 2.8 - 选择 Visual Studio

  1. 接受 Visual Studios 的条款和条件,如下截图所示:图 2.9 - 接受 Visual Studio 的条款和条件

图 2.9 - 接受 Visual Studio 的条款和条件

  1. 您将看到所选的 Unity 版本正在下载并安装。等待此过程完成。在下图中,您将看到我安装了其他 Unity 版本,但您只会看到一个版本,这是正常的:图 2.10 - 我目前在计算机上安装的所有 Unity 版本

图 2.10 - 我目前在计算机上安装的所有 Unity 版本

  1. 当 Unity 安装完成后,Visual Studio Installer 将自动执行。它将下载一个安装程序,该安装程序将下载并安装 Visual Studio Community 2019,如下截图所示:

图 2.11 - 安装 Visual Studio

图 2.11 - 安装 Visual Studio

请记住,新的 Unity 版本可能会有不同的步骤,所以尽量按照 Unity 设计的流程进行操作——大多数情况下,这是直观的。现在是使用 Unity 创建项目的时候了。

创建项目

现在我们已经安装了 Unity,可以开始创建我们的游戏了。为此,我们首先需要创建一个项目,这基本上是一个包含游戏所有文件的文件夹。这些文件称为资源,有不同类型,如图像、音频、3D 模型、脚本文件等。在本节中,我们将看到如何管理项目,涉及以下概念:

  • 创建项目

  • 项目结构

创建项目

与 Unity 安装一样,我们将使用 Unity Hub 来管理项目。我们需要按照以下步骤创建一个项目:

  1. 打开 Unity Hub,点击项目按钮,然后点击新建,如下截图所示:图 2.12 - 在 Unity Hub 中创建新项目

图 2.12 - 在 Unity Hub 中创建新项目

  1. 选择通用渲染管线模板,然后输入项目名称和位置,然后点击创建。我们将创建一个简单图形的 3D 游戏,准备在 Unity 可以执行的每个设备上运行,因此通用渲染管线(或URP)是更好的选择。在第六章**,使用 URP 和 Shader Graph 进行材质和效果中,我们将详细讨论原因。该过程如下截图所示:图 2.13 - 选择通用渲染管线模板

图 2.13 - 选择通用渲染管线模板

重要提示:

尝试将项目放在 Dropbox、Google Drive 或任何云同步文件夹中,以确保您随时可以使用该项目。这个项目会不断增长,所以确保您在该文件夹中有足够的空间。如果您的托管服务空间不足,可以跳过这一步。如果您知道如何使用 Git,那将是一个更好的选择。

  1. Unity 将创建并自动打开项目。这可能需要一些时间,但之后,您将看到一个类似于下图所示的屏幕:图 2.14 - Unity 编辑器窗口

图 2.14 – Unity 编辑器窗口

  1. 尝试关闭窗口,然后再次打开它,然后返回 Unity Hub 并从列表中选择项目,如下所示:

图 2.15 – 重新打开项目

图 2.15 – 重新打开项目

现在我们已经创建了项目,让我们来探索它的结构。

项目结构

我们刚刚打开了 Unity,但在下一章之前我们不会开始使用它。现在是时候看看项目文件夹结构是如何组成的了。为此,我们需要打开创建项目的文件夹。如果您不记得这是在哪里,可以这样做:

  1. 右键单击编辑器底部的项目面板中的Assets文件夹。

  2. 单击在资源管理器中显示选项。如果您使用的是 Mac,该选项称为在 Finder 中显示。以下截图说明了这一点:

图 2.16 – 在资源管理器中打开项目文件夹

图 2.16 – 在资源管理器中打开项目文件夹

然后,您将看到以下文件夹结构:

图 2.17 – Unity 项目文件夹结构

图 2.17 – Unity 项目文件夹结构

如果您想随时将此项目移动到另一台 PC 或发送给同事,只需压缩所有这些文件并将其作为 ZIP 文件发送给他们,但并非所有文件夹都是必需的。重要的文件夹是AssetsPackagesProjectSettingsAssets 文件夹将保存我们为游戏创建和使用的所有文件,因此这是必须的。我们还将配置不同的 Unity 系统以定制引擎以适应我们的游戏。所有与此相关的设置都在ProjectSettings文件夹中。最后,我们将安装不同的 Unity 模块或包以扩展其功能,因此Packages文件夹将保存我们正在使用的模块,以便 Unity 知道。如果您需要将项目移动到其他地方,则无需复制其余文件夹,但让我们至少讨论一下Library文件夹是什么。

Unity 需要将我们将要使用的文件转换为自己的格式才能运行,一个例子就是音频和图形。Unity 支持Assets文件夹并完全重新创建Library文件夹。这个过程可能需要时间,项目越大,所需的时间就越多。

请注意,您在项目上工作时希望保留 Unity 创建的所有文件夹,因此在这期间不要删除任何文件夹,但如果您需要移动整个项目,现在您知道需要携带哪些文件。

摘要

在本章中,我们讨论了为什么 Unity 是一个用于创建游戏的好工具,并将其与市场上的其他引擎进行了比较。这个分析旨在帮助您选择 Unity 作为您的第一个游戏开发工具。之后,我们还回顾了如何使用 Unity Hub 安装和管理不同的 Unity 版本,最后,我们看到了如何使用相同的工具创建和管理多个项目。我们将经常使用 Unity Hub,因此最初了解如何使用它是很重要的。现在,我们已经准备好进入 Unity 编辑器。

在下一章中,我们将开始学习基本的 Unity 工具,以制作我们的第一级原型。

第三章:使用场景和游戏对象

欢迎来到本书的第三章—这里是艰苦工作开始的地方!在本章中,我们将开发一些关于 Unity 的基础知识,以便编辑项目。我们将看到如何使用几个 Unity 编辑器窗口来操作我们的第一个场景及其对象。此外,我们将学习如何创建和组合对象或游戏对象,以及如何使用层次结构和预制件来管理具有多个对象的复杂场景。最后,我们将回顾如何正确保存所有工作,以便以后继续工作。

具体来说,在本章中,我们将研究以下概念:

  • 操作场景

  • 游戏对象和组件

  • 对象层次结构

  • 预制件

  • 保存场景和项目

操作场景

场景是我们项目中几种文件(也称为资源)之一。根据项目类型或公司习惯的工作方式,场景可能意味着不同的事情,但最常见的用例是将游戏分成整个部分,最常见的部分包括以下内容:

  • 主菜单

  • 第 1 关,第 2 关,第 3 关,…,第 N 关

  • 胜利画面,失败画面

  • 启动画面,加载画面

在本节中,我们将涵盖与场景相关的以下概念:

  • 场景的目的

  • 场景视图

  • 我们的第一个游戏对象

  • 导航场景视图

  • 操作游戏对象

场景的目的

将游戏分成场景的想法是,您将处理并加载场景所需的数据;因此,如果您在主菜单中,您将只有该特定场景需要的纹理、音乐和对象—如果您现在不需要,就没有必要在随机存取内存RAM)中加载第 10 关的 Boss。这就是加载画面存在的原因,只是为了填补在卸载一个场景中需要的资源和加载另一个场景中需要的资源之间的时间。也许您会认为像侠盗猎车手GTA)这样的开放世界游戏在您漫游世界时没有加载画面,但实际上,它们在您移动时实际上在后台加载和卸载世界的块,而这些块是设计为彼此连接的不同场景。

主菜单和常规关卡场景之间的区别在于它们拥有的对象(也称为游戏对象)。在菜单中,您会找到背景、音乐、按钮和标志等对象,在关卡中,您将拥有玩家、敌人、平台、生命值盒等。因此,取决于您和放置在场景中的游戏对象来决定该场景对于您的游戏意味着什么。

但是我们如何创建一个场景呢?让我们从场景视图开始。

场景视图

当您打开一个 Unity 项目时,您将看到 Unity 编辑器。它将由几个窗口或面板组成,每个面板都可以帮助您更改游戏的不同方面。在本章中,我们将看看帮助您编写场景的窗口。Unity 编辑器如下截图所示:

图 3.1 – Unity 编辑器

图 3.1 – Unity 编辑器

如果您以前曾经编写过任何类型的应用程序,您可能习惯于拥有一个起始函数,比如Main,在那里您开始编写代码来创建应用程序所需的多个对象,如果我们谈论游戏,您可能会在这里创建场景中的所有对象。这种方法的问题在于,为了确保所有对象都被正确创建,您需要运行程序以查看结果,如果有什么错位,您将需要手动更改对象的坐标,这是一个缓慢而痛苦的过程。幸运的是,在 Unity 中,我们有场景视图,以下是其示例截图:

图 3.2 – 场景视图

图 3.2 – 场景视图

这个窗口是经典WYSIWYG(所见即所得)概念的实现。在这里,您可以创建对象并将它们放置在整个场景中,通过场景预览,您可以看到当您点击播放时场景将会呈现什么样子。但在学习如何使用这个场景之前,我们需要在场景中有一个对象,所以让我们创建我们的第一个对象。

我们的第一个游戏对象

Unity 通用渲染管线URP)模板带有一个建筑工地测试场景,但让我们创建一个空的场景来开始探索这个新概念。为此,您可以简单地使用文件 | 新建场景菜单选项来创建一个空的新场景,如下截图所示:

图 3.3 – 创建一个新场景

图 3.3 – 创建一个新场景

本书将学习创建游戏对象的几种方法,但现在,让我们开始使用 Unity 提供的一些基本模板。为了创建它们,我们需要在 Unity 窗口顶部打开GameObject菜单,它将显示我们几个模板类别,如3D 对象2D 对象特效等,如下截图所示:

图 3.4 – 创建一个立方体

图 3.4 – 创建一个立方体

3D 对象类别下,我们将看到几个 3D 基本形状,如立方体球体圆柱体等,虽然使用它们不如使用精美的下载的 3D 模型令人兴奋,但请记住,我们正在原型化我们的关卡,也就是灰盒模型。这意味着我们将使用大量的原型形状来模拟我们的关卡,以便我们可以快速测试它,并查看我们的想法是否足够好,可以开始将其转换为最终版本的复杂工作。

我建议您选择立方体对象开始,因为它是一个多功能的形状,可以代表许多对象。因此,现在我们有一个带有要编辑的对象的场景,我们需要学习使用场景视图的第一件事就是浏览场景。

浏览场景视图

为了操作一个场景,我们需要学习如何在其中移动以从不同的角度查看结果。有几种导航方式,让我们从最常见的一种开始:第一人称视角。这个视图允许您使用类似第一人称射击游戏的导航方式在场景中移动,使用鼠标和WASD键。要像这样导航,您需要按住鼠标右键,然后在这样做的同时,您可以移动鼠标来旋转相机,并按下WASD键来移动它。您还可以按下Shift键以加快移动速度,并按下QE键来上下移动。

另一种常见的移动方式是单击对象以选择它(所选对象将有橙色轮廓),然后按下F键将其聚焦,使场景视图相机立即移动到一个位置,我们可以更仔细地查看该对象。之后,我们可以按住左侧Alt键和鼠标左键,开始移动鼠标以围绕对象“轨道”,看到不同的角度来检查它的每个部分是否放置正确,如下截图所示:

图 3.5 – 选择一个对象

图 3.5 – 选择一个对象

现在我们可以自由地在场景中移动,我们可以开始使用场景视图来操作游戏对象。

操作游戏对象

场景视图的另一个用途是操作对象的位置。为了这样做,我们首先需要选择一个对象,然后按键盘上的Y键或 Unity 编辑器左上角的第六个按钮,如下截图所示:

图 3.6 – 更改变换工具

图 3.6 – 更改变换工具

这将显示所选对象上的变换标尺,它允许我们改变对象的位置、旋转和缩放,如下截图所示:

图 3.7 – 变换标尺

图 3.7 – 变换标尺

让我们开始翻译物体,通过在标尺球内拖动红色、绿色和蓝色箭头来完成。在你这样做的时候,你会看到物体沿着选定的轴移动。这里有一个有趣的概念需要探索,那就是这些箭头颜色的含义。如果你注意场景视图右上角的区域,你会看到一个轴标尺,它作为这些颜色含义的提醒,如下截图所示:

图 3.8 – 轴标尺

图 3.8 – 轴标尺

计算机图形学使用经典的 3D 笛卡尔坐标系来表示物体的位置。红色与物体的 x 轴相关联,绿色与 y 轴相关联,蓝色与 z 轴相关联。但每个轴代表什么意思呢?如果你来自另一个 3D 制作程序,这可能会有所不同,但在 Unity 中,z 轴(蓝色)代表着前向矢量,这意味着箭头指向物体的前方;x 轴是右向矢量,y 轴代表上向矢量。需要考虑这些轴是本地的,这意味着如果你旋转物体,它们会改变它们面对的方向,因为物体的方向改变了物体面对的方式。Unity 可以在必要时显示这些轴的全局坐标,但现在让我们坚持使用本地坐标。

为了确保我们正在使用本地坐标,确保本地模式已激活,如下截图所示:

图 3.9 – 在枢轴和本地坐标之间切换

图 3.9 – 在枢轴和本地坐标之间切换

如果你看到全局而不是本地作为右侧按钮,只需点击它就会改变。顺便说一下,尽量保持左侧按钮为枢轴。如果它显示中心,只需点击它进行更改。

我知道——我们正在编辑一个立方体,所以没有明显的前面或右侧,但当你使用真实的 3D 模型,比如汽车和角色时,它们肯定会有这些面,而且它们必须与这些轴正确对齐。如果将来你在 Unity 中导入一辆汽车,汽车的前面指向红轴(X 轴),你需要修正它,因为我们未来的移动代码将依赖于这个约定,但让我们留到以后再说。

现在,让我们使用这个变换标尺来旋转物体,使用它周围的三个彩色圆圈。例如,如果你点击并拖动红色圆圈,你会沿着 x 旋转轴旋转物体。这里还有一个有趣的提示需要考虑。如果你想根据我们之前讨论的颜色编码来水平旋转物体,你可能会选择 x 轴——用于水平移动的轴——但很遗憾,那是错误的。

旋转的一个好方法就像自行车的加速器:你需要拿着它并转动。如果你像这样旋转 x 轴,你会使物体上下旋转。所以,为了水平旋转,你需要使用绿色圆圈或 y 轴。这个过程如下截图所示:

图 3.10 – 旋转物体

图 3.10 – 旋转物体

最后,我们有缩放,通过变换标尺球外侧的彩色立方体来完成。如果你点击并拖动它们,你会看到我们的立方体沿着这些轴被拉伸,允许你改变物体的大小。此外,你会看到标尺中心有一个灰色立方体,它允许你统一沿着所有轴改变物体的大小。这个过程如下截图所示:

图 3.11 – 缩放对象

图 3.11 – 缩放对象

记住,在许多情况下,缩放对象通常是一个不好的做法。在你场景的最终版本中,你将使用适当大小和比例的模型,并且它们将以模块化的方式设计,这样你可以将它们一个接一个地连接起来。如果你对它们进行缩放,可能会发生一些不好的事情,比如纹理被拉伸并变得像素化,以及不再正确连接的模块。当然也有一些例外,比如在森林中放置大量相同树木的实例,并稍微改变它的比例以模拟变化,以及在灰盒阶段,将立方体改变比例以创建地板、墙壁、天花板、柱子等是完全可以的,因为最终,这些立方体将被真正的 3D 模型替换。

挑战

创建一个由地板、三面普通墙和一个带门洞的第四面墙(三个立方体)组成的房间。在下面的截图中,你可以看到它应该是什么样子的:

图 3.12 – 房间任务完成

图 3.12 – 房间任务完成

现在我们可以编辑对象的位置,让我们看看如何编辑它的其他方面。

游戏对象和组件

我们谈到我们的项目由资源组成,场景(一种特定类型的资源)由游戏对象组成;那么,我们如何创建一个对象呢?通过组件的组合。

在本节中,我们将涵盖与组件相关的以下概念:

  • 组件

  • 操作组件

组件

组件是游戏对象可以由多个部分组成的其中之一;每个组件负责对象的不同特性。有几个组件可以解决不同的任务,比如播放声音,渲染网格,应用物理等等,即使 Unity 有大量的组件,我们最终还是需要创建自定义组件,迟早会用到。在下面的截图中,你可以看到当我们选择一个游戏对象时 Unity 向我们展示的内容:

图 3.13 – 检视面板

图 3.13 – 检视面板

在前面的截图中,我们可以看到检视面板,如果我们需要猜测它的作用,我们现在可以说它显示了所选对象的所有属性,并且我们可以配置它们来改变对象的行为,比如位置和旋转,是否投射阴影等等。这是正确的,但我们缺少一个关键元素:这些属性不属于对象;它们属于对象的组件。我们可以在一组属性之前看到一些标题加粗,比如变换盒碰撞器等等。这些是对象的组件。

在这种情况下,我们的对象有一个变换、一个网格过滤器、一个网格渲染器和一个盒碰撞器组件,所以让我们逐个审查这些。变换只有位置信息,比如对象的位置、旋转和比例,它本身什么也不做——它只是我们游戏中的一个点——但当我们向对象添加组件时,那个位置开始变得更有意义。这是因为一些组件将与变换和其他组件互动,每一个都会影响其他。

一个例子是网格过滤器网格渲染器,它们都负责渲染 3D 模型。网格渲染器将在网格过滤器中指定的网格在变换组件中指定的位置上渲染,所以网格渲染器需要从这些其他组件获取数据,没有它们就无法工作。另一个例子是盒碰撞器。它代表了对象的物理形状,所以当物理计算对象之间的碰撞时,它会检查该形状是否与其他形状基于变换中指定的位置发生碰撞。

我们现在不想探索物理和渲染。本节的要点是 GameObject 是一组组件,每个组件为我们的对象添加特定的行为,并且每个组件与其他组件互动以完成所需的任务。为了进一步加强这一点,让我们看看如何将一个立方体转换成一个使用物理下落的球体。

操作组件

编辑对象组件的工具是检视器。它不仅允许我们更改组件的属性,还允许我们添加和移除组件。在这种情况下,我们想要将一个立方体转换成一个球体,因此我们需要改变这些组件的几个方面。我们可以从改变对象的视觉形状开始,因此我们需要改变渲染模型或网格。指定要渲染的网格的组件是MeshFilter组件。如果我们看一下,我们可以看到一个说CubeMesh属性,右边有一个带点的小圆圈。

重要提示

如果您没有看到任何像我们刚提到的网格的属性,请尝试点击组件名称左侧的三角形。这样做将展开和折叠所有组件的属性。这在下面的截图中有所说明:

图 3.14 – 禁用组件

图 3.14 – 禁用组件

如果我们点击它,选择网格窗口将弹出,允许我们选择几个网格选项;因此,在这种情况下,选择Sphere组件。在将来,我们将向我们的项目添加更多的 3D 模型,以便该窗口将有更多的选项。网格选择器显示在下面的截图中:

图 3.15 – 网格选择器

图 3.15 – 网格选择器

好了,看起来像一个球体,但它会像一个球体一样行为吗?让我们找出来。为了这样做,我们可以在窗口顶部的搜索框中添加一个Rigidbody。下面的截图说明了如何添加一个组件:

图 3.16 – 添加组件

图 3.16 – 添加组件

如果您点击编辑器顶部中间的播放按钮,您可以使用游戏面板测试您的球体物理。当您点击播放时,该面板将自动聚焦,并向您展示玩家将如何看到游戏。这里可能出现的一个问题是,也许您看不到任何东西,如果游戏摄像机没有指向我们的球体所在的位置,那就可能会发生这种情况。播放控制显示在下面的截图中:

图 3.17 – 播放控制

图 3.17 – 播放控制

在这里,您可以使用变换图标来旋转和定位摄像机,使其以某种方式看向我们的球体。在移动时,您可以在场景窗口的右下部分检查小预览,以查看新的摄像机视角。另一种选择是在层次结构中选择摄像机,然后使用快捷键Ctrl + Shift + F(或 Mac 上的command + Shift + F)。摄像机预览显示在下面的截图中:

图 3.18 – 摄像机预览

图 3.18 – 摄像机预览

现在,为了测试物理碰撞是否执行正常,让我们创建一个立方体,将其缩放直到它呈坡道形状,并将该坡道放在我们的球体下方,如下所示:

图 3.19 – 球和坡道对象

图 3.19 – 球和坡道对象

如果您现在点击播放,您将看到球体与我们的坡道发生碰撞,但方式很奇怪。看起来像是在弹跳,但实际并非如此。如果您展开球体的Box Collider组件,您将看到即使我们的物体看起来像一个球体,绿色的框图标显示我们的球体实际上是一个盒子在物理世界中,如下面的截图所示:

图 3.20 – 具有球体图形和盒碰撞器的对象

图 3.20 - 具有球形图形和盒碰撞器的对象

如今,视频卡可以处理渲染高度详细的模型(具有高多边形数量),但是物理系统是在中央处理单元CPU)中执行的,它需要进行复杂的计算以侦测碰撞。为了在我们的游戏中获得良好的性能(至少 30帧每秒FPS)),物理系统使用简化的碰撞形状来工作,这些形状可能与玩家在屏幕上看到的实际形状不同。这就是为什么我们有Mesh Filter和不同类型的碰撞器组件分开——一个处理视觉形状,另一个处理物理形状。

再次强调,本节的目的不是深入研究这些 Unity 系统,所以让我们继续前进。我们如何解决这个问题呢?简单:通过修改我们的组件!在这种情况下,BoxCollider可以表示一个盒子形状,不像MeshFilter,它支持任何形状。因此,首先我们需要通过右键单击组件的标题并选择移除组件选项来移除它,如下面的屏幕截图所示:

图 3.21 - 移除组件

图 3.21 - 移除组件

现在,我们可以再次使用添加组件菜单来选择一个物理组件,这次选择球体碰撞器组件。如果您查看物理组件,您将看到其他类型的碰撞器,可以用来表示其他形状,但我们将在第十五章**,物理碰撞和健康系统中进行讨论。球体碰撞器组件可以在下面的屏幕截图中看到:

图 3.22 - 添加一个球体碰撞器组件

图 3.22 - 添加一个球体碰撞器组件

因此,如果您现在点击播放,您将看到我们的球体不仅外观像一个球体,而且行为也像一个球体。记住:本书本节的主要思想是理解在 Unity 中,您可以通过添加、移除和修改组件来创建任何您想要的对象,我们将在整本书中大量使用这种方法。

现在,组件不是创建对象所需的唯一内容。复杂的对象可能由多个子对象组成,因此让我们看看它是如何工作的。

对象层次结构

一些复杂的对象可能需要分成子对象,每个子对象都有自己的组件。这些子对象需要以某种方式附加到主对象,并共同工作以创建必要的对象行为。

在本节中,我们将涵盖与组件相关的以下概念:

  • 对象的父子关系

  • 可能的用途

对象的父子关系

父子关系包括使一个对象成为另一个对象的子对象,这意味着这些对象将彼此相关联。发生的一种关系类型是变换关系,意味着子对象将受到父对象变换的影响。简单来说,子对象将跟随父对象,就像它附加在上面一样。举个例子,想象一个戴着帽子的玩家。帽子可以是玩家头部的子对象,使帽子在附着时跟随头部移动。

为了尝试这个,让我们创建一个代表敌人的胶囊体和一个代表敌人武器的立方体。请记住,为了这样做,您可以使用GameObject | 3D Object | CapsuleBox选项。一个示例胶囊体可以在下面的屏幕截图中看到:

图 3.23 - 代表人类的胶囊体和代表武器的盒子

图 3.23 - 代表人类的胶囊体和代表武器的盒子

如果您移动敌人对象(胶囊体),武器(盒子)将保持其位置,而不会跟随我们的敌人。因此,为了防止这种情况发生,我们可以简单地将武器拖到层次结构窗口中的敌人对象上,如下面的屏幕截图所示:

图 3.24 - 将立方体武器作为胶囊角色的父对象

图 3.24 - 将立方体武器作为胶囊角色的父对象

现在,如果你移动敌人,你会看到枪也随之移动、旋转和缩放。所以,基本上,枪的变换也受到敌人变换组件的影响。

现在我们已经做了一些基本的父子关系,让我们继续探索其他可能的用途。

可能的用途

除了创建复杂对象之外,父子关系还有一些其他常见的用途。其中一个常见的用途是组织项目的层次结构。现在,我们的场景很简单,但随着时间的推移,它会变得更加复杂,因此跟踪所有对象将变得困难。为了防止这种情况发生,我们可以创建空的游戏对象(在GameObject | Create Empty中),充当容器,将对象放入其中,以便组织我们的场景。请谨慎使用这个功能,因为如果滥用,会有性能成本。通常情况下,在组织场景时,有一到两级的父子关系是可以接受的,但超过这个数量可能会影响性能。请考虑,你可以并且将会在创建复杂对象时有更深层次的父子关系;所提出的限制只是用于场景组织。

为了不断改进我们之前的示例,在整个场景中复制敌人几次,创建一个名为敌人的空游戏对象,并将所有敌人拖放到其中,使其充当容器。如下截图所示:

图 3.25 - 将敌人分组在父对象中

图 3.25 - 将敌人分组在父对象中

父子关系的另一个常见用途是改变对象的枢轴或中心。现在,如果我们尝试用变换控件旋转我们的枪,它会围绕它的中心旋转,因为立方体的创建者决定将中心放在那里。通常情况下,这是可以接受的,但让我们考虑这样一种情况,即我们需要让武器瞄准敌人所看的点。在这种情况下,我们需要围绕武器手柄旋转武器;所以,在这个"盒子"武器的情况下,它将是最接近敌人的一端。问题在于我们无法改变对象的中心,因此一个解决方案是创建另一个具有不同中心的"武器"3D 模型或网格,这将导致大量重复的武器版本,如果我们考虑到其他可能的游戏需求,比如旋转武器拾取物品。我们可以很容易地使用父子关系来解决这个问题。

这个想法是创建一个空的游戏对象,并将其放在我们希望对象的新枢轴的位置。之后,我们可以简单地将我们的武器拖放到这个空的游戏对象中,并且从现在开始,将空对象视为实际的武器。如果你旋转或缩放这个武器容器,你会发现武器网格会围绕这个容器应用这些变换,所以我们可以说武器的枢轴已经改变了(实际上并没有,但我们的容器模拟了这种变化)。这个过程如下截图所示:

图 3.26 - 改变武器的枢轴

图 3.26 - 改变武器的枢轴

现在,让我们继续看看不同的管理游戏对象的方法,这次使用预制体。

预制体

在前面的例子中,我们在场景中创建了许多敌人的副本,但在这样做的过程中,我们创建了一个新问题。让我们想象一下,我们需要改变我们的敌人并给它添加一个Rigidbody组件,但因为我们有几个相同对象的副本,我们需要逐个将它们添加相同的组件。也许以后,我们需要改变每个敌人的质量,所以再一次,我们需要逐个地去改变每一个敌人,这里我们可以开始看到一个模式。

一个解决方案可能是使用Ctrl键(Mac 上的option键)选择所有的敌人并一次修改它们,但如果我们在其他场景中有敌人的副本,这个解决方案就没有用了。这就是预制体的用武之地。

在本节中,我们将涵盖与预制体相关的以下概念:

  • 创建预制体

  • 预制体实例关系

  • 预制体变体

创建预制体

预制体是 Unity 工具,允许我们将自定义对象,比如我们的敌人,转换为定义它们如何创建的资源。我们可以使用它们轻松地创建自定义对象的新副本,而不需要再次创建其组件和子对象。

为了创建一个预制体,我们可以简单地将自定义对象从层次结构窗口拖动到项目窗口,这样做之后,你会在项目文件中看到一个新的资源。项目窗口是你可以浏览和探索所有项目文件的地方;所以,在这种情况下,我们的预制体是我们创建的第一个资源。现在,你可以简单地将预制体从项目窗口拖到场景中,轻松地创建新的预制体副本,如下面的截图所示:

图 3.27 - 创建预制体

图 3.27 - 创建预制体

现在,我们有一个小问题。如果你注意层次结构窗口,你会看到原始预制体对象和所有新副本,它们的名称是蓝色的,而在预制体之前创建的敌人的名称是黑色的。名称中的蓝色表示该对象是预制体的实例,意味着该对象是基于预制体创建的。我们可以选择这些蓝色命名的对象,并在检视器中点击选择按钮,选择创建该对象的原始预制体。这在下面的截图中有所说明:

图 3.28 - 在层次结构中检测预制体

图 3.28 - 在层次结构中检测预制体

所以,问题在于之前的预制体副本不是原始预制体的实例,遗憾的是没有办法使它们与预制体连接起来。因此,为了实现这一点,我们需要简单地销毁旧的副本,并用预制体创建的副本替换它们。起初,没有所有副本都作为实例似乎不是一个问题,但在本章的下一节中,我们将探讨预制体和它们的实例之间的关系。

预制体实例关系

预制体的实例具有与预制体的绑定,有助于在它们之间轻松地恢复和应用更改。如果你拿一个预制体并对其进行一些修改,这些更改将自动应用到项目中所有场景中的所有实例,因此我们可以轻松地创建预制体的第一个版本,在整个项目中使用它,然后尝试进行更改。

为了练习这一点,假设我们想要给敌人添加一个Rigidbody组件,以便它们可以下落。为了做到这一点,我们可以简单地双击预制体文件,进入预制体编辑模式,在这里,我们可以编辑预制体,与场景中的其余部分隔离开来。在这里,我们可以简单地取出预制体根对象,并向其添加Rigidbody组件。之后,我们可以简单地点击场景窗口左上角的场景按钮,回到我们正在编辑的场景,现在,我们可以看到所有敌人的预制体实例都有Rigidbody组件,如下面的截图所示:

图 3.29 - 预制体编辑模式

图 3.29 - 预制体编辑模式

现在,如果我们改变了一个预制体实例会发生什么呢?比如说我们想让一个特定的敌人飞行,这样他们就不会受到重力的影响。我们只需选择特定的预制体,取消刚体组件中的使用重力复选框。这样做后,如果我们玩游戏,我们会看到只有那个特定实例会飘浮。这是因为预制体实例的更改变成了覆盖,如果你看检视器中的该实例的使用重力属性变成了粗体,你就可以清楚地看到这一点。让我们拿另一个对象,改变它的缩放属性使其变大。同样,我们会看到缩放属性变成了粗体,并且左侧有一个小条。使用重力复选框可以在下面的截图中看到:

图 3.30 – 使用重力被突出显示为覆盖

图 3.30 – 使用重力被突出显示为覆盖

覆盖优先于预制体,因此如果我们改变原始预制体的缩放,具有缩放覆盖的预制体不会改变,保持其自己的缩放版本,如下截图所示:

图 3.31 – 一个带有缩放覆盖的预制体实例

图 3.31 – 一个带有缩放覆盖的预制体实例

我们可以使用检视器中的覆盖下拉菜单轻松定位实例的所有覆盖,找到我们对象的所有更改。它不仅允许我们查看所有覆盖,还可以撤销我们不想要的任何覆盖并应用我们想要的覆盖。比如说,我们后悔了特定预制体缺乏重力,没问题!我们只需找到覆盖并撤销它。该过程如下截图所示:

图 3.32 – 撤销单个覆盖

图 3.32 – 撤销单个覆盖

另外,假设我们真的喜欢那个实例的新缩放,所以我们希望所有实例都具有那个缩放—太棒了!我们只需点击应用按钮,选择特定的更改,所有实例都会具有那个缩放(除了具有覆盖的实例),如下截图所示:

图 3.33 – 应用按钮

图 3.33 – 应用按钮

此外,我们还有撤销所有应用所有按钮,但要谨慎使用,因为你可能会轻易撤销和应用你不知道的更改。

因此,正如你所看到的,预制体是一个非常有用的 Unity 工具,可以跟踪所有相似的对象并对它们应用更改,并且还可以具有具有少量变化的特定实例。谈到变化,还有其他情况,你会希望有几个具有相同变化集的预制体实例,比如飞行敌人和地面敌人,但是如果你考虑一下,我们在不使用预制体时遇到的问题是一样的,所以我们需要手动逐个更新这些不同版本。

在这里,我们有两个选择:一个是创建一个全新的预制体,只是为了有另一个带有那种变化的版本。这会导致一个问题,如果我们希望所有类型的敌人都受到更改,我们需要手动将更改应用到每个可能的预制体。第二个选择是创建一个预制体变体。让我们来看看后者。

预制体变体

预制体变体是创建一个新的预制体,但基于现有预制体的行为,因此新的预制体继承了基础预制体的特性。这意味着我们的新预制体可以与基础预制体有所不同,但它们共有的特性仍然是相连的。

为了说明这一点,让我们创建一个可以飞行的敌人预制件的变体:飞行敌人预制件。为此,我们可以在“层次结构”窗口中选择现有的敌人预制件实例,将其命名为“飞行敌人”,然后再次将其拖到“项目”窗口中,这次会看到一个提示,询问我们要创建哪种预制件。这次,我们需要选择“预制件变体”,如下截图所示:

图 3.34 - 创建预制件变体

](https://gitee.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_3.34_B14199.jpg)

图 3.34 - 创建预制件变体

现在,我们可以通过双击进入变体的预制件编辑模式,然后添加一个立方体作为我们敌人的喷气背包,并取消敌人的“使用重力”属性。如果我们返回到场景,我们将看到变体实例已经更改,而基本敌人没有改变。您可以在下面的截图中看到这一点:

图 3.35 - 预制件变体实例

](https://gitee.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_3.35_B14199.jpg)

图 3.35 - 预制件变体实例

现在,想象一下您想要为我们所有类型的敌人添加一个帽子。我们只需双击基本敌人预制件进入预制件编辑模式,然后添加一个立方体作为帽子。现在,我们将看到该更改应用于所有敌人,因为请记住:“飞行敌人”预制件是基本敌人预制件的变体,这意味着它将继承所有更改。

到目前为止,我们已经创建了大量内容,但是如果我们的电脑因某种原因关闭,我们肯定会丢失所有内容,所以让我们看看如何保存我们的进度。

保存场景和项目

与任何其他程序一样,我们需要保存我们的进度。不同之处在于,我们不只有一个包含所有项目资产的巨大文件,而是每个资产都有几个文件。

在本节中,我们将涵盖以下与保存相关的概念:

  • 保存我们的更改

  • 项目结构

保存我们的更改

让我们开始通过保存场景来保存我们的进度,这非常简单。我们只需转到项目的“资产”文件夹,但永远不要超出该文件夹。这将在“项目”窗口中生成一个新的资产:一个场景文件,如下截图所示:

图 3.36 - 场景文件

](https://gitee.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_3.36_B14199.jpg)

图 3.36 - 场景文件

我们可以在对话框中创建一个文件夹来保存我们的场景,或者,如果您已经保存了场景,可以在“项目”窗口中使用“创建”|“文件夹”选项创建一个文件夹,并将创建的场景拖到该文件夹中。现在,如果您使用“文件”|“新建场景”菜单选项创建另一个场景,您可以通过在“项目”窗口中双击资产来返回到先前的场景。

这只保存了场景,但是对预制件和其他类型的资产的任何更改都不会使用该选项保存。相反,如果您想保存除场景之外的所有资产的每个更改,可以使用“文件”|“保存项目”选项。这可能有点令人困惑,但是如果您想保存所有更改,您需要同时保存场景和项目,因为仅保存项目不会保存场景上的更改。有时,确保一切都已保存的最佳方法就是关闭 Unity,这在尝试在计算机或文件夹之间移动项目时是推荐的。让我们在下一节中讨论这个问题。

项目结构

现在我们已经保存了所有更改,我们准备好将项目在计算机之间或到另一个文件夹中移动(如果有需要的话)。您可以关闭 Unity 以确保一切都已保存,并只需复制整个项目文件夹。如果您不记得在哪里保存了项目,您可以右键单击“项目”窗口中的“资产”文件夹,然后选择“在资源管理器中显示”(Mac 上的“显示在 Finder 中”),如下截图所示:

图 3.37 - 定位项目文件夹

](https://gitee.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_3.37_B14199.jpg)

图 3.37 - 定位项目文件夹

现在我们在项目文件夹中,让我们抓住机会稍微探索一下。在一个完整的项目中,我们会发现几个文件夹和文件,但并非所有文件都需要复制以便将项目移动到其他地方。最重要的文件夹是AssetsProjectSettingsPackages。这些文件夹可以在以下截图中看到:

图 3.38 - 项目文件夹结构

图 3.38 - 项目文件夹结构

ProjectSettings文件夹包含了我们将在本书后面玩弄的不同 Unity 系统的几个配置,但即使我们没有更改任何设置,带上这个文件夹总是一个好主意。.unitypackage文件曾经是,但让我们稍后讨论。到目前为止,重要的是要注意,该文件夹将包含关于我们的项目正在使用哪些包的设置,因此也记得带上它。

其他文件夹/文件都不是必需的,因为其中一些是临时的,另一些可以重新生成,比如Library,那里存放着我们资产的转换版本。通过转换,我们指的是外部生成的文件,比如 3D 模型、图像、声音等。Unity 需要将这些文件转换为 Unity 兼容的格式。原始文件将存放在Assets中,转换后的文件将存放在Library中,以便在必要时可以轻松重新生成。稍后,在第五章**,导入和集成资产中,我们将讨论如何集成外部生成的内容。

现在,让我们想象一下,你已经压缩了这三个文件夹,将它们复制到 U 盘中,然后在另一台电脑上解压这些文件夹。我们如何再次打开项目呢?正如你所看到的,项目没有项目文件或类似的东西 - 它只是一堆文件夹。为了打开一个项目,最简单的方法是在Assets文件夹中找到一个场景文件,然后双击它,这样 Unity 就会在那个场景中打开项目。另一个选项是使用Assets文件夹。因此,我们将把该项目添加到我们计算机项目的列表中,稍后,我们只需点击列表中的名称即可打开它。以下截图说明了这一点:

图 3.39 - 重新打开项目

图 3.39 - 重新打开项目

现在,我们已经掌握了开始学习如何使用不同的 Unity 系统所需的所有基本 Unity 知识,以便开始创建一个真正的游戏!让我们在下一章中做到这一点!

总结

在本章中,我们简要介绍了基本的 Unity 概念。我们回顾了所有 Unity 窗口以及如何使用它们来编辑一个完整的场景,从导航和创建预制对象,到操纵它们以创建我们自己类型的对象,使用游戏对象和组件。我们还讨论了如何使用Hierarchy窗口将游戏对象进行父子关系,以创建复杂的对象层次结构,以及创建预制件以重用和操纵大量相同类型的对象。最后,我们讨论了如何保存我们的进度并移动项目,回顾了项目的结构以及哪些文件夹是必不可少的。

在下一章中,我们将学习使用不同的工具来创建游戏关卡的第一个原型。

第四章:使用地形和 ProBuilder 进行灰盒设计

现在我们已经掌握了使用 Unity 所需的所有概念,让我们开始设计我们的第一个关卡。本章的想法是学习如何使用地形工具来创建游戏的景观,然后使用 ProBuilder 来创建基础的 3D 网格,比使用立方体具有更多的细节。使用这些工具,您将能够创建任何类型的场景原型,并在实际实现最终图形之前尝试您的想法。

具体来说,在本章中,我们将讨论以下概念:

  • 使用地形创建景观

  • 使用 ProBuilder 创建形状

使用地形创建景观

到目前为止,我们已经使用立方体生成了我们的关卡原型,但我们也了解到,有时这些形状无法代表我们可能需要的所有对象。想象一下不规则的东西,比如丘陵、峡谷和河流的完整地形。使用立方体创建这将是一场噩梦。另一个选择是使用 3D 建模软件,但问题是生成的模型将如此庞大和详细,以至于即使在高端 PC 上也无法良好运行。在这种情况下,我们需要学习如何使用地形,这将是本章的第一节中我们要做的事情。

在本节中,我们将涵盖与地形相关的以下概念:

  • 讨论高度图

  • 创建和配置高度图

  • 编写高度图

  • 添加高度图细节

让我们开始谈论高度图,其纹理帮助我们定义地形的高度。

讨论高度图

如果我们使用常规的 3D 建模工具创建一个巨大的游戏区域,包括丘陵、峡谷、陨石坑、山谷和河流,我们将面临一个问题,即我们将在所有可能的距离上使用完整详细的模型,从而浪费资源在远处看不到的细节上。我们将从很远的地方看到很多地形部分,比如山脉和河流,所以这是一个严重的问题。

Unity 地形工具使用一种称为高度图的技术以高效和动态的方式生成地形。它不是为整个地形生成大型 3D 模型,而是使用一种称为高度图的图像,看起来像地形的俯视黑白照片。

在下图中,您可以看到苏格兰地形高度的黑白俯视图,白色表示较高的高度,黑色表示较低的高度:

图 4.1 - 苏格兰的高度图

图 4.1 - 苏格兰的高度图

在上图中,您可以很容易地在寻找图像中最白的区域时发现山峰。海平面以下是黑色的,而中间使用灰度渐变的任何东西代表了最小和最大高度之间的不同高度。想法是图像的每个像素确定了地形特定区域的高度。

Unity 地形工具可以自动生成地形的 3D 网格,从而节省了完整地形的 3D 模型的硬盘空间。此外,Unity 将随着我们移动创建地形,为附近区域生成高详细度模型,并为远处区域生成低详细度模型,使其成为一种高效的解决方案。

在下图中,您可以看到为地形生成的网格。您可以看到地形的近处部分比远处部分有更多的多边形:

图 4.2 - 高度图生成的网格

图 4.2 - 高度图生成的网格

请注意,这项技术也有其缺点,比如 Unity 在我们玩游戏时生成这些 3D 模型所需的时间,以及无法创建洞穴,但目前对我们来说这不是问题。

现在我们知道了什么是高度图,让我们看看如何使用 Unity 地形工具来创建我们自己的高度图。

创建和配置高度图

如果单击GameObject | 3D Object | Terrain,您将看到一个巨大的平面出现在场景中,并且Hierarchy窗口中出现了一个 Terrain 对象。这就是我们的地形,它是平的,因为它的高度图一开始就是全黑的,因此在其初始状态下没有任何高度。在下面的截图中,您可以看到全新的Terrain是什么样子的:

图 4.3 - 尚未绘制高度的地形

图 4.3 - 尚未绘制高度的地形

在开始编辑地形之前,您必须配置不同的设置,例如地形的高度图的大小和分辨率,这取决于您打算如何使用它。这与生成整个世界不同。请记住,我们的游戏将发生在玩家的基地,因此地形将很小。在这种情况下,200 x 200 米大小的区域被山脉包围将足够。

为了根据这些要求配置我们的地形,我们需要执行以下操作:

  1. HierarchyScene窗口中选择Terrain

  2. 查看Terrain组件的Inspector,如果折叠了,请展开它。

  3. 单击轮子图标以切换到配置模式。在下面的截图中,您可以看到该按钮的位置:图 4.4 - 地形设置按钮

图 4.4 - 地形设置按钮

  1. 查找Mesh Resolution部分。

  2. 更改200。这将说明我们的地形尺寸为 200 x 200 米。

  3. 500只是为了限制我们山的最高峰:图 4.5 - 地形分辨率设置

图 4.5 - 地形分辨率设置

  1. 查找Texture Resolutions部分。

  2. Heightmap Resolut更改为257 x 257

图 4.6 - 高度图分辨率设置

图 4.6 - 高度图分辨率设置

重要提示

高度图分辨率是将保存地形不同部分的高度的高度图图像的大小。在我们的 200 x 200 米地形中使用 257 x 257 的分辨率意味着地形中的每个平方米将被高度图的略多于 1 像素覆盖。每平方米的分辨率越高,您可以在该区域大小中绘制的细节就越多。通常,地形特征很大,因此每平方米多于 1 像素通常是资源浪费。找到允许您创建所需细节的最小分辨率。

您还需要设置的初始设置是初始地形高度。默认情况下,这是 0,因此您可以从底部开始绘制高度,但这样做,您无法在地形中开洞,因为它已经处于最低点。设置一点初始高度允许您绘制河流路径和必要时开洞。为此,请执行以下操作:

  1. 选择Terrain

  2. 单击Brush按钮(第二个按钮)。

  3. 将下拉菜单设置为Set Height

  4. 50。这将说明我们希望所有地形的起始高度为50米,从而允许我们最大深度为50米的洞:图 4.7 - 设置高度地形工具位置

图 4.7 - 设置高度地形工具位置

  1. 单击我们之前指定的50米。

现在我们已经正确配置了我们的高度图,让我们开始编辑它。

编写高度图

请记住,高度图只是一个带有高度的图像,因此为了编辑它,我们需要在该图像中绘制高度。幸运的是,Unity 有工具可以让我们直接在编辑器中编辑地形并直接查看修改后的高度结果。为此,我们必须按照以下步骤进行:

  1. 选择Terrain

  2. 单击Brush按钮。

  3. 将下拉菜单设置为提高或降低地形模式:图 4.8 - 提高或降低地形工具位置

图 4.8 - 提高或降低地形工具位置

  1. 选择画笔选择器中的第二个画笔。这个画笔的边缘模糊,可以让我们创建更柔和的高度。

  2. 画笔大小设置为30,这样我们就可以创建跨越30米区域的高度。如果你想创建更细微的细节,你可以减小这个数字。

  3. 不透明度设置为10,以减少每秒或每次点击绘制的高度量:图 4.9 - 光滑边缘画笔

图 4.9 - 光滑边缘画笔

  1. 现在,如果你在场景视图中移动鼠标,你会看到你点击该区域时将绘制的高度的一个小预览。也许你需要靠近地形才能看清楚:图 4.10 - 提升地形区域的预览

图 4.10 - 提升地形区域的预览

重要提示

你在地形附近看到的方格图案可以让你看到你正在编辑的对象的实际大小。每个单元格代表一个平方米。记住,有一个参考来看到你正在编辑的对象的实际大小是有用的,可以防止创建太大或太小的地形特征。也许你可以放入其他类型的参考,比如一个具有准确尺寸的大立方体,代表一个建筑的大小,以便了解你正在创建的山或湖的大小。记住,立方体的默认尺寸是 1 x 1 x 1 米,所以缩放到(10,10,10)将给你一个 10 x 10 x 10 米的立方体。

  1. 按住左键并拖动鼠标在地形上开始绘制地形高度。记住,你可以按下Ctrl + Z(Mac 上为command+ Z)来撤销任何不需要的更改。

  2. 尝试绘制围绕我们区域边界的山脉,这将代表我们基地的背景山丘:

图 4.11 - 绘制在地形边缘的山脉

图 4.11 - 绘制在地形边缘的山脉

现在,我们在未来基地周围有了不错的起始山丘。我们还可以在未来基地区域周围绘制一个河流流域。要这样做,请按照以下步骤进行:

  1. 在地形的中间放置一个缩放为(50,10,50)的立方体。这将作为我们即将创建的基地的占位符:图 4.12 - 基地区域的占位立方体

图 4.12 - 基地区域的占位立方体

  1. 再次选择地形画笔按钮。

  2. 画笔大小减小到10

  3. 按住Shift键,左键单击并拖动鼠标在地形上绘制我们基地占位符周围的流域。这样做会降低地形而不是提升它:图 4.13 - 我们占位基地周围的河流流域

图 4.13 - 我们占位基地周围的河流流域

现在,我们有了一个简单但不错的起始地形,让我们对从基地的角度看它的外观有了基本的了解。在继续之前,我们将应用一些更精细的细节,使我们的地形看起来更好一些。在下一节中,我们将讨论如何使用不同的工具模拟地形侵蚀。

添加高度图细节

在前一节中,我们创建了地形的粗略轮廓。如果你想让它看起来更真实一些,那么你需要开始在各处绘制许多微小的细节。通常,这是在关卡设计过程的后期完成的,但是让我们现在来看看,因为我们正在探索地形工具。现在,我们的山看起来非常平滑。在现实生活中,它们更加陡峭,所以让我们改进一下:

  1. 选择地形并启用画笔按钮。

  2. 设置下拉菜单为提升或降低地形模式。

  3. 选择第五个画笔。这个画笔的布局不规则,这样我们就可以在各处绘制一些噪音。

  4. 画笔大小设置为50,这样我们就可以覆盖更大的区域:图 4.14 - 云图案画笔用于随机性

图 4.14 - 云图案画笔用于随机性

  1. 按住Shift键,对地形的山丘进行小幅点击,不要拖动鼠标。记得放大到你正在应用细节的区域,因为这些在远处看不到:

图 4.15 – 用之前的刷子生成的侵蚀

图 4.15 – 用之前的刷子生成的侵蚀

这给我们的山丘增加了一些不规则性。现在,让我们想象我们想在山丘上有一个平坦的区域,可以放置装饰性的天文台或天线。按照以下步骤操作:

  1. 选择地形刷工具,并从下拉菜单中选择设置高度

  2. 高度设置为60

  3. 在山丘上涂抹一个区域。你会看到如果低于60米,地形会升高,或者在大于60米的区域变得更低:图 4.16 – 平坦的山丘

图 4.16 – 平坦的山丘

  1. 你可以看到边界有一些粗糙的角落需要平滑:图 4.17 – 未平滑的地形边缘

图 4.17 – 未平滑的地形边缘

  1. 将下拉菜单更改为平滑高度模式。

  2. 选择尺寸为5,不透明度为10的第二个刷子:图 4.18 – 平滑高度刷

图 4.18 – 平滑高度刷

  1. 点击并拖动我们平坦区域的边界,使它们更加平滑:

图 4.19 – 平滑地形边缘

图 4.19 – 平滑的地形边缘

我们可以在这里和那里不断添加细节,但现在我们可以就此结束。下一步是创建我们的玩家基地,但这次,让我们探索 ProBuilder 以生成我们的几何图形。

使用 ProBuilder 创建形状

到目前为止,我们使用立方体和基本形状创建了简单的场景,对于大多数原型来说已经足够了,但有时,你可能会遇到游戏中难以用普通立方体建模的棘手区域,或者你可能想在游戏的某些部分添加更深层次的细节,以便让玩家感受到该区域的视觉效果。在这种情况下,我们可以使用任何 3D 建模工具,比如 3D Studio、Maya 或 Blender,但这些工具可能难以学习,而且在开发的这个阶段你可能不需要它们的全部功能。幸运的是,Unity 有一个名为 ProBuilder 的简单 3D 模型创建工具,让我们来探索一下。

在本节中,我们将涵盖与 ProBuilder 相关的以下概念:

  • 安装 ProBuilder

  • 创建一个形状

  • 操作网格

  • 添加细节

ProBuilder 不是默认包含在我们的 Unity 项目中的,所以让我们从学习如何安装它开始。

安装 ProBuilder

Unity 是一个功能强大的引擎,但如果我们没有使用所有这些工具,会使引擎运行变慢,因此我们需要手动指定我们正在使用哪些 Unity 工具。为此,我们将使用包管理器,这是一个工具,我们可以用它来查看和选择我们将需要哪些 Unity 包。你可能还记得,之前我们谈到过Packages文件夹。这基本上就是包管理器在修改的内容。

为了使用这个工具安装 ProBuilder,我们需要执行以下操作:

  1. 点击窗口 | 包管理器选项:图 4.20 – 包管理器选项

图 4.20 – 包管理器选项

  1. 在刚刚打开的窗口中,确保Packages处于Unity Registry模式,方法是点击窗口左上角的Packages按钮,然后选择Unity Registry图 4.21 – 显示所有包

图 4.21 – 显示所有包

  1. 等待一会儿,直到左侧的包列表填满。确保连接到互联网以下载和安装包。

  2. 看看那个列表中的ProBuilder包,并选择它。

重要提示

我正在使用 ProBuilder 4.2.3 版本,这是我写这本书时可用的最新版本。虽然您可以使用更新的版本,但请考虑使用它的步骤可能会有所不同。您可以使用标题左侧的箭头查看旧版本。

图 4.22 - 在包列表中的 ProBuilder

图 4.22 - 在包列表中的 ProBuilder

  1. 单击Package Manager右下角的Install按钮:图 4.23 - 安装按钮

图 4.23 - 安装按钮

  1. 等待一会儿,直到包安装完成。当Install按钮被替换为 Up to Date 标签时,您会注意到过程已经结束。

既然我们已经在项目中安装了 ProBuilder,让我们来使用它吧!

创建形状

我们将通过创建一个地板平面来开始我们的基础。我们将通过以下方式来做到这一点:

  1. 打开 ProBuilder 并转到工具 | ProBuilder | ProBuilder窗口:图 4.24 - ProBuilder 窗口选项

图 4.24 - ProBuilder 窗口选项

  1. 在打开的窗口中,单击New Shape按钮右侧的加号图标(+):图 4.25 - 新形状选项

图 4.25 - 新形状选项

  1. Shape Selector中,选择Plane

  2. 设置50

  3. 设置2。我们稍后会需要这些细分:图 4.26 - 新形状设置

图 4.26 - 新形状设置

  1. 单击Build按钮以确认Plane

  2. 单击场景视图中ProBuilder按钮中的四个按钮中的第一个按钮,以启用整个平面的移动:

图 4.27 - 选择对象工具

图 4.27 - 选择对象工具

  1. 用这个地板替换占位符立方体:

图 4.28 - 平面被细分为 3 x 3 网格

图 4.28 - 平面被细分为 3 x 3 网格

既然我们已经创建了地板,让我们学习如何操纵其顶点以改变其形状。

操纵网格

如果您选择平面,您会看到它被细分为 3 x 3 网格,因为我们将宽度和高度分段设置为 2(2 切割)。我们这样做是因为我们将使用外部单元来创建我们的墙,从而将它们升起。我们的想法是修改这些单元的大小,以勾勒出墙的长度和宽度,然后再创建墙。为了做到这一点,我们将执行以下操作:

  1. 选择平面。

  2. 单击场景视图中出现的四个新按钮中的第二个按钮(显示顶点):图 4.29 - 选择顶点工具

图 4.29 - 选择顶点工具

  1. 单击并拖动鼠标以创建一个选择框,选择第二行顶点的四个顶点:图 4.30 - 顶点选择

图 4.30 - 顶点选择

  1. 单击 Unity 编辑器顶部左侧的第二个按钮,以启用移动工具:图 4.31 - 移动工具

图 4.31 - 移动工具

  1. 移动顶点行,使平面的这个细分变得更薄。您可以使用地形上的方格图案来了解墙的尺寸:图 4.32 - 移动顶点

图 4.32 - 移动顶点

  1. 重复步骤 35,直到您获得具有相似大小的墙轮廓:

图 4.33 - 移动顶点以减少边缘单元宽度

图 4.33 - 移动顶点以减少边缘单元宽度

重要提示

如果您希望顶点具有精确的位置,我建议您安装并探索 ProGrids 包。这是一个与常规 Unity 和 ProBuilder 一起使用的位置捕捉系统。

现在我们已经为墙壁创建了轮廓,让我们向网格添加新的面来创建它们。为了使用我们创建的细分或“面”来制作墙壁,我们必须选择并拉伸它们。按照以下步骤进行:

  1. 选择平面。

  2. 在场景视图中选择ProBuilder按钮的第四个按钮:图 4.34 – 选择面工具

图 4.34 – 选择面工具

  1. 在按住Ctrl(Mac 上为command)的同时,单击墙壁轮廓的每个面:图 4.35 – 选择边缘面

图 4.35 – 选择边缘面

  1. ProBuilder窗口中,找到Extrude Faces按钮右侧的加号图标(+)。它将位于窗口的红色部分:图 4.36 – 拉伸面选项

图 4.36 – 拉伸面选项

  1. 在点击加号按钮后出现的窗口中设置5

  2. 单击该窗口中的Extrude Faces按钮:图 4.37 – 拉伸距离选项

图 4.37 – 拉伸距离选项

  1. 现在,你应该看到墙壁的轮廓刚从地面上升起:

图 4.38 – 拉伸的网格边缘

图 4.38 – 拉伸的网格边缘

现在,如果你注意一下基础地板和墙壁与地形的接触,会发现有一点间隙。我们可以尝试将基础向下移动,但地板可能会消失,因为它将被埋在地形下面。我们可以在这里做一个小技巧,只是将墙壁向下推,而不移动地板,这样墙壁将被埋在地形中,但我们的地板将保持与地形的一点距离。你可以在以下图中看到它看起来的样子:

图 4.39 – 预期结果的切片

图 4.39 – 预期结果的切片

为了这样做,我们需要执行以下操作:

  1. 在场景视图中选择第三个ProBuilder按钮以启用边缘选择:图 4.40 – 选择边缘工具

图 4.40 – 选择边缘工具

  1. 在按住Ctrl(Mac 上为command)的同时,选择墙壁的所有底部边缘。

如果选择了不想要的边缘,只需再次单击它们,同时按住Ctrl(Mac 上为command)取消选择,同时保持当前选择:

图 4.41 – 选择地板边缘

图 4.41 – 选择地板边缘

信息框

如果你想在上一个截图中使用线框模式,点击场景视图左上部的着色按钮,并从下拉菜单中选择线框选项。

  1. 启用移动工具,按下 Unity 编辑器左上部的第二个按钮:图 4.42 – 移动工具

图 4.42 – 移动工具

  1. 将边缘向下移动,直到完全埋在地形下:

图 4.43 – 重叠的面

图 4.43 – 重叠的面

现在我们有了一个基础网格,我们可以开始使用其他几个 ProBuilder 工具向其添加细节。

添加细节

让我们开始通过对墙壁施加一点倒角来向基础添加细节。按照以下步骤进行:

  1. 使用边缘选择模式(ProBuilder按钮的第三个按钮),选择我们模型的顶部边缘:图 4.44 – 选择顶部墙壁边缘

图 4.44 – 选择顶部墙壁边缘

  1. ProBuilder窗口中,点击Bevel按钮右侧的加号图标。

  2. 设置距离为0.5图 4.45 – 生成倒角距离

图 4.45 – 生成倒角距离

  1. 点击Bevel Edges。现在,你可以看到我们墙壁的顶部有一点倒角:图 4.46 – 倒角过程的结果

图 4.46 – 倒角过程的结果

  1. 可选地,你也可以对内墙的底部进行操作:

图 4.47 - 地板-墙壁边缘应用斜角

图 4.47 - 地板-墙壁边缘应用斜角

另一个要添加的细节可能是地面中央的坑,作为我们需要避免掉入的危险,以及使敌人避开它使用人工智能。为了做到这一点,请按照以下步骤操作:

  1. 点击第四个 ProBuilder 场景视图按钮,启用 FACE 选择模式。

  2. 选择地板。

  3. 在 ProBuilder 窗口中点击Subdivide面选项。你将得到地板分成四块。

  4. 再次点击该按钮,以得到一个 4x4 的网格地板:图 4.48 - 细分地板

图 4.48 - 细分地板

  1. 选择四个内部地板砖。

  2. 通过点击 Unity 编辑器左上角的第四个按钮,启用缩放工具:图 4.49 - 缩放工具

图 4.49 - 缩放工具

  1. 使用标尺中心的灰色立方体,缩小中心的瓷砖:图 4.50 - 内部单元格被缩小

图 4.50 - 内部单元格被缩小

  1. ProBuilder窗口中点击 Extrude Faces 按钮。

  2. 将挤出的面向下推。

  3. 右键单击ProBuilder窗口标签,然后选择Close Tab。我们需要回到地形编辑,并且ProBuilder打开会让我们无法舒适地进行编辑:图 4.51 - 关闭标签选项

图 4.51 - 关闭标签选项

  1. 选择地形并降低该地区的地形,以便我们可以看到坑:

图 4.52 - 为了使坑可见而降低地形

图 4.52 - 为了使坑可见而降低地形

我知道我们在原始关卡布局中没有计划这个坑,但请记住,定义首字母缩写是一个在游戏开发中会不断变化的文件,所以有时候,我们可以大胆地进行更改,以改进游戏。只是要注意不要过度进行永无止境的更改,这是一门难以掌握的艺术。

总结

在本章中,我们学习了如何使用高度图和 Unity 地形工具创建大型地形网格,例如使用绘制高度和设置高度来创建山丘和河流盆地。此外,我们还学习了如何使用 ProBuilder 创建自己的 3D 网格,以及如何操纵模型的顶点、边和面,创建游戏原型基础模型。我们没有讨论一些可以应用于网格的性能优化,或者一些高级的 3D 建模概念,因为那需要整整的章节,而这超出了本书的范围。现在,我们的主要重点是原型设计,所以我们对我们关卡的当前状态感到满意。

在下一章中,我们将学习如何使用外部工具创建的资产(文件)来下载和替换这些原型模型。这是提高游戏图形质量的第一步,以便达到最终的外观,这将在第二部分结束时完成。

第五章:导入和整合资产

在上一章中,我们创建了我们的关卡的原型。现在,假设我们已经编写了游戏并对其进行了测试,验证了这个想法。因此,现在是时候将原型艺术改为真正的完成艺术了。实际上,我们将在第三部分编写游戏,但出于学习目的,让我们暂时跳过这部分。为了使用最终资产,我们需要学习如何获取它们(图像、3D 模型等),如何将它们导入 Unity,并如何在我们的场景中使用它们。

在本章中,我们将研究以下主题:

  • 导入资产

  • 整合资产

  • 配置资产

导入资产

我们可以在项目中使用不同的资产来源。我们可以简单地从我们的艺术家那里获取文件,从不同的免费和付费资产网站下载它们,或者我们可以使用资产商店,Unity 的官方虚拟资产商店,在那里我们可以获取准备在 Unity 中使用的免费和付费资产。我们将使用从互联网和资产商店下载资产的混合方式,以获取所有可能的资源。

在本节中,我们将涵盖与导入资产相关的以下概念:

  • 从互联网导入资产

  • 从资产商店导入资产

  • 从互联网下载并导入我们项目中的资产

从互联网导入资产

在获取项目的艺术资产方面,让我们从地形纹理开始。请记住,我们的地形上有一个网格图案,所以想法是用草、泥土、岩石和其他类型的纹理替换它。为此,我们必须获取图像。在这种情况下,这些图像通常是不同地形图案的俯视图,并且它们必须是“可平铺的”。您可以在以下截图中看到一个示例:

图 5.1 – 左:草块;右:将同一草块分开以突出纹理平铺

图 5.1 – 左:草块;右:将同一草块分开以突出纹理平铺

左边的草看起来是一个单一的大图像,但如果您留意,应该能够看到一些重复出现的图案。在这种情况下,这种草只是一个在网格中重复四次的单个图像,就像右边的那个。这样,您可以通过重复单个小图像来覆盖大面积,从而节省计算机内存。

想法是获取这些类型的图像来绘制我们的地形。你可以从几个地方获取它们,但最简单的方法是使用 Google 图片或任何图片搜索引擎。要做到这一点,请按照以下步骤操作:

  1. 打开您的浏览器(Chrome、Safari、Edge 等)。

  2. 转到您喜欢的搜索引擎。在这种情况下,我将使用 Google。

  3. 使用关键词PATTERN tileable texture,将PATTERN替换为您正在寻找的地形类型,例如grass tileable texturemud tileable texture。在这种情况下,我将输入grass tileable texture,然后按Enter进行搜索。

  4. 切换到图像搜索模式:图 5.2 – Google 图像搜索

图 5.2 – Google 图像搜索

  1. 找到任何您认为适合所需草坪的纹理并单击它。请记住,纹理必须是草的俯视图,并且必须重复。

重要提示

在选择图像之前,请尝试检查图像的分辨率。现在,请尝试选择分辨率小于 1024 x 1024 的正方形图像。

  1. 右键单击打开的图像,然后选择另存为图像…图 5.3 – 另存为图像…选项

图 5.3 – 另存为图像…选项

  1. 将图像保存在您记得的任何文件夹中。

现在您已经下载了图像,可以通过几种方式将其添加到您的项目中。最简单的方法是执行以下操作:

  1. 使用文件资源管理器(Mac 中的Finder)找到您的图像。

  2. 在 Unity 的项目窗口中找到或创建Textures文件夹。

  3. 将文件资源管理器和 Unity 项目窗口并排放置。

  4. 将文件从文件资源管理器拖到 Unity 项目窗口的Textures文件夹中:

图 5.4 - 从文件资源管理器拖动纹理到 Unity 项目视图

图 5.4 - 从文件资源管理器拖动纹理到 Unity 项目视图

对于这些简单的纹理,任何搜索引擎都可以帮助,但如果你想用详细的墙壁和门替换玩家的基础几何图形,或者在场景中放置敌人,你需要获取 3D 模型。如果你使用关键词如“免费僵尸 3D 模型”在任何搜索引擎中搜索,你会找到无数的免费和付费 3D 模型网站,如 TurboSquid 和 Mixamo,但这些网站可能存在问题,因为这些网格通常没有准备好在 Unity 中使用,甚至在游戏中使用。你会发现模型的多边形数量非常高,尺寸或方向不正确,纹理未经优化等等。为了避免这些问题,我们需要使用更好的来源,在这种情况下,我们将使用 Unity 的资产商店,所以让我们来探索一下。

从资产商店导入资产

资产商店是 Unity 的官方资产市场,你可以在那里找到很多模型、纹理、声音,甚至整个 Unity 插件,以扩展引擎的功能。在这种情况下,我们将限制自己下载 3D 模型来替换玩家的基础原型。你会想要获取模块化设计的 3D 模型,意味着你会得到几个部分,如墙壁、地板、角落等等。你可以连接它们来创建任何类型的场景。

为了做到这一点,你必须按照以下步骤进行:

  1. 在 Unity 中点击窗口|资产商店,这将打开一个新窗口,显示资产商店已经移动。在之前的 Unity 版本中,你可以直接在编辑器中看到资产商店,但现在建议在常规网络浏览器中打开它,所以只需点击在线搜索按钮:图 5.5 - 资产商店已移动消息

图 5.5 - 资产商店已移动消息

  1. 你的浏览器将打开一个类似于以下截图的网站:图 5.6 - 资产商店首页

图 5.6 - 资产商店首页

  1. 在右侧面板中,点击其右侧的箭头打开 3D 类别。然后,打开环境并勾选科幻标记。如你所见,有几个类别可以找到不同类型的资产,如果你愿意,你可以选择另一个。在环境中,你会找到可以用来生成游戏场景的 3D 模型。

  2. 如果需要的话,你可以为资产付费,但现在让我们先隐藏付费的资产。你可以通过侧边栏的定价部分进行搜索,使用其右侧的加号(+)符号打开它,然后勾选免费资产复选框:图 5.7 - 免费资产选项

图 5.7 - 免费资产选项

  1. 在搜索区域,找到任何看起来具有你所寻找美学的资产,并点击它。记得留意户外资产,因为大多数环境包通常只有室内。在我的情况下,我选择了一个名为科幻风格模块化包的资产,它适用于室内和室外。请注意,到你阅读本书时,该包可能已经不存在,所以你可能需要选择另一个。如果你找不到合适的包,你可以下载我们在 GitHub 存储库中使用的资产文件。请参考如何访问它的前言说明。图 5.8 - 资产商店搜索包的预览

图 5.8 - 资产商店搜索包的预览

重要提示

在撰写本书时,Unity 正在发布“Snaps”包,这是一组官方 Unity 3D 模型,可用于不同类型环境的模块化设计。其中一些是付费的,而另一些是免费的 - 我建议你试试看。

  1. 现在,你将在资产商店窗口中看到包的详细信息。在这里,你可以找到关于包的描述、视频/图片、包的内容,以及最重要的部分,评论,你可以查看包是否值得购买,如果是付费的话:图 5.9 – 资产商店包详情

图 5.9 – 资产商店包详情

  1. 如果你对这个包满意,点击添加到我的资产按钮,如果需要的话登录 Unity,然后点击在 Unity 中打开按钮。你可能会被提示接受切换到 Unity 应用程序 – 只需接受:图 5.10 – 切换应用程序

图 5.10 – 切换应用程序

  1. 这将再次打开包管理器,但这次是以我的资产模式,显示你从资产商店下载过的所有资产的列表,以及你刚刚在列表中选择的那个被突出显示的资产。如果你没有在 Unity Hub 中登录,你可能需要点击登录按钮重新登录 Unity。图 5.11 – 包管理器显示资产

图 5.11 – 包管理器显示资产

  1. 点击窗口右下角的下载,等待下载完成。然后点击导入。双重检查你是否从列表中选择了正确的资产包。

  2. 过了一会儿,包内容 窗口将会出现,允许你选择包中的哪些资产要放入你的项目中。现在,保持原样,点击导入图 5.12 – 要导入的资产

图 5.12 – 要导入的资产

  1. 一段时间后导入,你将在项目窗口中看到所有的包文件。

请注意,导入大量完整的包将大大增加你项目的大小,而且以后你可能想要删除你没有使用的资产。另外,如果导入的资产生成了错误,阻止你播放场景,只需删除包中附带的所有.cs文件。它们通常在名为Scripts的文件夹中。这些是可能与你的 Unity 版本不兼容的代码文件。在第三部分中,我们将学习如何制作我们自己的,这样就不需要这些了:

图 5.13 – 点击播放时的代码错误警告

图 5.13 – 点击播放时的代码错误警告

重要提示

资产商店容易发生变化,即使你使用的是和我一样的 Unity 版本,所以之前的步骤可能会被 Unity 改变而没有通知。而且,它的内容经常变化,你可能找不到本书中使用的相同的包。在这种情况下,你可以找到另一个类似的包,或者从 GitHub 仓库中获取我使用的文件(在前言中有链接和说明)。

在继续本章之前,尝试使用资产商店下载一个敌人角色,按照之前的步骤。为了解决这个练习,你必须完成之前的相同步骤,但是在资产商店的3D | 人物 | 人形类别中查找。

现在我们已经导入了大量的艺术资产,让我们学习如何在场景中使用它们。

整合资产

我们刚刚导入了许多可以以多种方式使用的文件,所以这一部分的目的是看看 Unity 如何将这些资产与需要它们的游戏对象和组件整合在一起。

在本节中,我们将涵盖与导入资产相关的以下概念:

  • 整合地形纹理

  • 整合网格

  • 整合材质

让我们开始使用平铺纹理来覆盖地形。

整合地形纹理

为了将纹理应用到我们的地形,做如下操作:

  1. 选择地形对象。

  2. 检视器中,点击地形组件的刷子图标(第二个按钮)。

  3. 从下拉菜单中选择油漆纹理图 5.14 – 油漆纹理选项

图 5.14 – 油漆纹理选项

  1. 点击编辑地形层… | 创建层选项。

  2. 在纹理选择器窗口中查找之前下载的地形纹理:图 5.15-纹理选择器

图 5.15-纹理选择器

  1. 您将看到纹理将立即应用于整个地形。

  2. 重复步骤 45来添加其他纹理。这一次,您会发现该纹理不会立即应用。

  3. 地形层部分,选择您创建的新纹理开始绘制。在我的案例中,我使用了泥浆纹理。

  4. 就像编辑地形时,在画笔部分,您可以选择并配置一个画笔来绘制地形。

  5. 在场景视图中,绘制您希望应用该纹理的区域。

  6. 如果您的纹理图案太明显,选择地形层框中的纹理,然后打开NewLayer N,其中 N 是您创建的层数所依赖的数字。

重要提示

每次向地形添加纹理,您会看到在项目视图中创建了一个名为“NewLayer N”的新资产。它保存了您创建的地形层的数据,如果需要,您可以在其他地形中使用它。您还可以重命名该资产以赋予其有意义的名称。此外,您还可以将这些资产重新组织到它们自己的文件夹中。

  1. 使用左侧的三角形打开该部分,并在平铺设置部分中增加大小属性,直到找到一个合适的大小,其中图案不那么明显:图 5.16-绘画纹理选项

图 5.16-绘画纹理选项

  1. 重复步骤 412,直到您应用了所有想要添加到地形的纹理。在我的案例中,我将泥浆纹理应用于河流盆地,并在山丘上使用了岩石纹理。对于岩石的纹理,我减少了画笔的不透明度属性,以更好地与山上的草地融合。您可以尝试在顶部添加一层雪,只是为了好玩:

图 5.17-用三种不同的纹理绘制我们的地形的结果

图 5.17-用三种不同的纹理绘制我们的地形的结果

当然,我们可以使用系统的许多高级工具来大大改进这一点,但让我们现在保持简单。现在,让我们看看如何集成 3D 模型。

集成网格

如果您选择我们之前配置过的 3D 资产之一并单击其右侧的箭头,则一个或多个子资产将出现在项目窗口中。这意味着 FBX 不是一个 3D 模型,而是定义 3D 模型的资产容器:

图 5.18-网格选择器

图 5.18-网格选择器

其中一些子资产是网格,这是定义模型几何形状的三角形集合。您可以在文件中找到至少一个这样的网格,但也可以找到多个,如果您的模型由许多部分组成,那就可能会发生。例如,汽车可以是一个单一的刚性网格,但这不允许您旋转其车轮或打开其车门;它只是一个静态的汽车,如果汽车只是场景中的道具,那可能足够了,但如果玩家能够控制它,您可能需要对其进行修改。这个想法是您的汽车的所有部分都是不同的游戏对象,彼此之间是父子关系,这样如果您移动一个,所有的都会移动,但您仍然可以独立旋转这些部分。

当您将 3D 模型文件拖到场景中时,Unity 将根据艺术家的创建方式自动为每个部分创建所有对象及其适当的父子关系。您可以在层次结构中选择对象并探索其所有子对象来查看这一点:

图 5.19-子对象选择

图 5.19-子对象选择

此外,您会发现每个对象都有自己的MeshFilterMeshRenderer组件,每个组件只渲染汽车的一部分。请记住,网格过滤器是一个具有对要渲染的网格资产的引用的组件,因此网格过滤器是使用我们之前讨论过的那些网格子资产的组件:

图 5.20 – 网格过滤器当前网格选择

图 5.20 – 网格过滤器当前网格选择

现在,如果您将 3D 模型文件拖入场景,您将获得与模型为 Prefab 且您正在实例化它时类似的结果。但是 3D 模型文件比 Prefab 更有限,因为您无法对模型应用更改,因此在将对象拖入场景并编辑以获得所需的行为后,我建议您创建一个 Prefab,以获得我们在第三章中讨论的所有好处,使用场景和游戏对象,例如对所有 Prefab 实例应用更改等。永远不要从模型文件创建大量实例 – 总是从基于该文件创建的 Prefab 创建它们。

这是 3D 网格的基本用法。现在,让我们来探讨纹理集成过程,这将使我们的 3D 模型更加详细。

集成纹理

也许您的模型已经应用了纹理,或者它已经应用了品红色。在后一种情况下,这意味着该资产未准备好与您在创建项目时选择的 URP 模板一起使用。Asset Store 中的一些资产是为旧版本的 Unity 而设计的:

图 5.21 – 使用错误或没有材质渲染的网格

图 5.21 – 使用错误或没有材质渲染的网格

修复的一个选项是使用编辑 | 渲染管线 | 通用渲染管线 | 将项目材质升级到 UniversalRP 材质。这将尝试将所有材质升级到 Unity 的当前版本:

图 5.22 – 将材质升级到 URP

图 5.22 – 将材质升级到 URP

这种方法的缺点是,有时它无法正确升级材质。幸运的是,我们可以通过以这种新方式重新应用对象的纹理来解决这个问题。即使您的资产工作正常,我建议您无论如何重新应用纹理,只是为了更多地了解材质的概念。

纹理直接应用于对象。这是因为纹理只是控制模型外观的所有配置中的一个。为了改变模型的外观,您必须创建一个材质。材质是一个单独的资产,包含有关 Unity 应该如何渲染您的对象的许多设置。您可以将该资产应用于共享相同图形设置的多个对象,如果更改材质的设置,它将影响使用它的所有对象。它的工作原理类似于图形配置文件。

为了创建一个可以应用纹理的材质,您需要按照以下步骤进行:

  1. 在项目窗口中,单击窗口左上角的加号(+)按钮。

  2. 在菜单中找到Material选项并单击它。

  3. 给您的材质命名。这通常是您正在创建的资产的名称(例如,汽车,船,角色等)。

  4. 将您创建的材质资产拖到场景中的模型实例上。目前,如果您将鼠标与拖动的资产一起移动到对象上,您将能够预览使用该材质的外观。释放鼠标即可确认。

  5. 也许您的对象有几个部分。在这种情况下,您需要将材质拖到对象的每个部分。

重要提示

拖动材质只会改变您拖动的对象的MeshRenderer组件的材质属性。

  1. 选择材质并单击Base Map属性左侧的圆圈。

  2. 纹理选择器中,点击模型的纹理。定位纹理可能有些复杂。通常,纹理的名称将与模型名称匹配。如果不匹配,您将需要尝试不同的纹理,直到找到适合您对象的纹理。此外,您可能会发现几个与您的模型同名的纹理。只需选择看起来颜色合适的纹理,而不是看起来是黑白或浅蓝色的纹理;我们稍后会用到它们:

图 5.23 – URP 材质的基本贴图属性

图 5.23 – URP 材质的基本贴图属性

通过这样,您已成功通过材质将纹理应用到对象上。对于每个使用相同纹理的对象,只需拖动相同的材质。现在我们已经基本了解了如何应用模型纹理,让我们学习如何在将模型传播到整个场景之前正确配置导入设置。

配置资产

正如我们之前提到的,艺术家习惯于在 Unity 之外创建艺术资产,这可能导致从该工具中看到的资产与 Unity 导入的方式之间存在差异。例如,3D Studio 可以使用厘米、英寸等单位,而 Unity 使用米。我们刚刚下载并使用了大量资产,但我们跳过了解决这些差异的配置步骤,所以现在让我们来看看这个。

在本节中,我们将涵盖与导入资产相关的以下概念:

  • 配置网格

  • 配置纹理

让我们开始讨论如何配置 3D 网格。

配置网格

为了更改模型的导入设置,您需要定位您下载的模型文件。包含 3D 模型的文件扩展名有几种,最常见的是.fbx文件,但您可能会遇到其他扩展名,如.obj.3ds.blender.mb等。您可以通过其扩展名来识别文件是否为 3D 网格:

图 5.24 – 选择的资产路径扩展

图 5.24 – 选择的资产路径扩展

此外,您可以点击资产,并在检查器中查看以下截图中可以看到的选项卡:

图 5.25 – 网格材质设置

图 5.25 – 网格材质设置

现在您已经找到了 3D 网格文件,可以正确配置它们。现在,我们应该考虑的唯一事情是模型的适当比例。艺术家习惯于使用不同设置的不同软件工作;也许一个艺术家使用米作为其度量单位创建了模型,而其他艺术家使用英寸、英尺等。当导入使用不同单位创建的资产时,它们可能会不成比例,这意味着我们将得到诸如人类比建筑物更大等结果。

最好的解决方案是询问艺术家来解决这个问题。如果所有的资产都是由您公司创建的,或者您使用了外部资产,您可以要求艺术家将其修复为您公司的工作方式,但现在,您可能是一个独立学习 Unity 的开发人员。幸运的是,Unity 有一个设置,允许您在在 Unity 中使用原始资产之前重新调整其比例。为了改变对象的比例因子,您必须执行以下操作:

  1. 在项目窗口中找到 3D 网格。

  2. 将其拖到场景中。您会看到一个对象出现在您的场景中。

  3. 使用GameObject | 3D Object | Capsule选项创建一个胶囊。

  4. 将胶囊放在您拖入编辑器的模型旁边。看看比例是否合理。这样做的目的是让胶囊代表一个人(2 米高),这样您就有了比例的参考:图 5.26 – 不成比例的资产

图 5.26 – 不成比例的资产

  1. 如果模型比预期的要大或小,请在项目窗口中再次选择网格(而不是您拖到编辑器中的 GameObject 实例),您将在检查器中看到一些导入设置。

  2. 查找比例因子属性并修改它,如果您的模型比预期的要小,则增加它,或者在相反的情况下减小它:图 5.27-模型网格选项

图 5.27-模型网格选项

  1. 单击检查器底部的应用按钮。

  2. 重复步骤 67,直到获得所需的结果。

还有很多其他选项要配置,但现在让我们暂停一下。现在,让我们讨论如何正确配置我们模型的纹理。

配置纹理

同样,这里有几个设置要配置,但让我们现在专注于纹理大小。想法是使用最适合该纹理使用的大小,这取决于许多因素。要考虑的第一个因素是从对象将被看到的距离。如果您正在创建第一人称游戏,您可能会看到足够接近的许多对象,以证明大纹理的必要性,但也许您有许多远处的对象,例如建筑物顶部的广告牌,您永远不会靠近到足以看到细节,因此您可以为此使用较小的纹理。还要考虑的是对象的重要性。如果您正在创建赛车游戏,您可能会有许多只会在屏幕上出现几秒钟且玩家永远不会关注的 3D 模型;他们将会注意道路和其他汽车。在这种情况下,街上的垃圾箱之类的对象可以有一个小纹理和低多边形模型,用户永远不会注意到(除非他们停下来欣赏风景,但这是可以接受的)。最后,您可以有一个从顶部向下查看的游戏,永远不会放大场景,因此在第一人称游戏中具有大纹理的相同对象在这里会有一个不太详细的纹理。在下一张图中,您可以看到较小的船可以使用较小的纹理:

图 5.28-在不同距离下看到相同的模型

图 5.28-在不同距离下看到相同的模型

纹理的理想尺寸是相对的。找到它的常规方法是通过改变其大小,直到在游戏中以可能的最近位置看到对象时找到最小可能的尺寸并保持良好的质量。这是一种反复试验的方法。为了做到这一点,您可以执行以下操作:

  1. 找到 3D 模型并将其放入场景中。

  2. 将场景视图相机放置在一个位置,以便以游戏中最大的尺寸显示对象。例如,在 FPS 游戏中,它几乎会在对象旁边,而在俯视游戏中,它会在对象上方几米处。同样,这取决于您的游戏。

  3. 在导入包或之前创建的材质中的文件夹中找到并选择对象使用的纹理。它们通常具有.png.jpg.tif扩展名。

  4. 在检查器中,查看最大尺寸属性并减小它,尝试下一个更小的值。例如,如果纹理为 2,048,请尝试 1,024。

  5. 单击应用并检查场景视图,看看质量是否急剧下降或更改是否不明显。您会感到惊讶。

  6. 重复步骤 45,直到获得低质量的结果。在这种情况下,只需增加先前的分辨率以获得可接受的质量。当然,如果您的目标是 PC 游戏,您可以期望比移动游戏更高的分辨率。

现在您已经导入,集成和配置了您的对象,让我们用这些资产创建我们的玩家基地。

组装场景

让我们开始使用我们下载的环境包替换我们的原型基地。为此,您必须执行以下操作:

  1. corner关键字中:图 5.29-网格选择器

图 5.29 - 网格选择器

  1. 在我的特定情况下,我有角落的外侧和内侧作为单独的模型,所以我需要将它们放在一起。

  2. 将其放置在原型基地的任何一个角落的相同位置:图 5.30 - 在替换占位符上放置网格

图 5.30 - 在替换占位符上放置网格

  1. 找到适合与角落连接以创建墙壁的合适模型。同样,你可以尝试在项目窗口中搜索wall关键字。

  2. 实例化它并将其定位,使其与角落相连。如果不完全匹配,不要担心;以后可以随时调整场景。

重要提示

你可以选择一个对象,按下V键选择所选对象的顶点,将其拖动到另一个对象的顶点。这被称为顶点捕捉。它允许你精确地连接场景中的两个部分:

图 5.31 - 连接两个模块

图 5.31 - 连接两个模块

  1. 重复墙壁,直到达到玩家基地的另一端,并放置另一个角落。你可能会得到一个比原型稍大或稍小的墙壁,但没关系:图 5.32 - 连接模块的链

图 5.32 - 连接模块的链

重要提示

记住,你可以按住Ctrl键(Mac 上的Control)移动一个对象,以便将对象的位置捕捉到墙壁的克隆物可以轻松地定位在其他墙壁旁边。

  1. 完成其余的墙壁并摧毁原型。记住这个过程很慢,你需要耐心。

  2. 通过寻找地板砖并在整个表面重复它们来添加地板:!图 5.33 - 带有坑洞的地板模块

图 5.33 - 带有坑洞的地板模块

  1. 使用包中的其他模块化部件添加任何你想要添加的细节。

  2. 将所有这些部件放入一个名为Base的容器对象中。记得创建一个空对象,然后将基础部件拖放到其中:

图 5.34 - 网格子资产

图 5.34 - 网格子资产

经过多次练习,你将慢慢积累关于模块化场景设计的常见问题和良好实践的经验。所有的包都有不同的模块化设计,所以你需要适应它们。

总结

在本章中,我们学习了如何导入模型和纹理,并将它们整合到我们的场景中。我们讨论了如何将纹理应用到地形上,如何用模块化模型替换我们的原型网格,如何将纹理应用到这些模型上,以及如何根据对象的使用情况考虑多个标准来正确配置资产。

通过这样,我们已经完成了本书的第一部分,并讨论了几个基本的 Unity 概念。在第二部分中,我们将开始深入研究几个 Unity 系统,这些系统可以帮助我们改善游戏的图形和声音质量。我们将开始学习如何创建自定义材质类型,以创建有趣的视觉效果和动画。

第六章:使用 URP 和着色器图的材质和效果

欢迎来到第二部分的第一章!我非常激动,因为你已经到达了这本书的这一部分,因为在这里,我们将深入研究 Unity 的不同图形和音频系统,以显着改善游戏的外观和感觉。我们将从这一部分开始,本章将讨论材质的着色器是什么,以及如何创建我们自己的着色器来实现一些无法使用默认 Unity 着色器实现的自定义效果。我们将创建一个简单的水动画效果来学习这个新概念。

在本章中,我们将研究以下着色器概念:

  • 着色器介绍

  • 使用着色器图创建着色器

介绍着色器

在上一章中,我们创建了材质,但我们从未讨论过它们内部是如何工作的,以及为什么着色器属性非常重要。在本章的第一部分,我们将探讨着色器的概念,作为编程视频卡以实现自定义视觉效果的一种方式。

在这一部分,我们将涵盖与着色器相关的以下概念:

  • 着色器管道

  • 渲染管道和 URP

  • URP 内置着色器

让我们从讨论着色器如何修改着色器管道以实现效果开始。

着色器管道

每当显卡渲染 3D 模型时,它需要输入数据进行处理,例如网格、纹理、对象的变换(位置、旋转和缩放)以及影响该对象的光源。有了这些数据,显卡必须将对象的像素输出到后备缓冲区,即视频卡将绘制我们对象的图像的地方。当 Unity 完成渲染所有对象(和一些效果)以显示完成的场景时,将显示该图像。基本上,后备缓冲区是显卡逐步渲染的图像,在绘制完成时显示出来(此时,它变成前置缓冲区,与之前的缓冲区交换)。

这是渲染对象的常规方式,但在输入数据和像素输出之间发生的事情可以通过许多不同的方式和技术来处理,这取决于您希望对象的外观如何;也许您希望它看起来很逼真或看起来像全息图,也许对象需要一个解体效果或卡通效果——可能有无尽的可能性。指定我们的显卡将如何处理对象的渲染的方式是通过着色器。

着色器是用特定的显卡语言编写的程序,例如 CG、HLSL 或 GLSL,它配置渲染过程的不同阶段,有时不仅配置它们,还用完全自定义的代码替换它们,以实现我们想要的精确效果。渲染的所有阶段形成了我们所说的着色器管道,一系列应用于输入数据的修改,直到它被转换为像素。

重要说明

有时,在本书中我们所说的着色器管道也可以在其他文献中被称为渲染管道,而后者也是正确的,在 Unity 中,渲染管道这个术语指的是不同的东西,所以让我们坚持这个名字。

管道的每个阶段负责不同的修改,根据显卡着色器模型的不同,这个管道可能会有很大的变化。在下一个图表中,您可以找到一个简化的渲染管道,跳过了现在不重要的高级/可选阶段:

图 6.1 – 常见着色器管道

图 6.1 – 常见着色器管道

让我们讨论每个阶段:

  • 输入组装器:这里是所有网格数据的组装地方,例如顶点位置、UV 和法线,准备好进行下一阶段。在这里你不能做太多事情;这个过程几乎总是一样的。

  • 顶点着色器:过去,这个阶段仅限于应用对象的变换、相机的位置和透视以及一些简单但有限的光照计算。使用现代 GPU,您可以自行决定。这个阶段接收要渲染的对象的每一个顶点,并输出一个修改过的顶点,因此基本上您有机会在这里修改对象的几何形状。这里的通常代码基本上与旧视频卡的代码相同,应用对象的变换,但您可以进行多种效果,比如沿着法线膨胀对象以应用旧的卡通效果技术,或者应用一些扭曲效果以制作全息效果(看看死亡搁浅中的全息效果)。还有机会计算下一个阶段的数据,但我们暂时不会深入讨论。

  • 裁剪:对于大多数要渲染的模型,您永远不会看到模型面的背面。以立方体为例;无论如何都无法看到任何一面的背面或内侧,因为它们会被其他面自动遮挡。因此,渲染立方体每个面的两面,即使看不到背面,也是没有意义的,幸运的是,这个阶段会处理这个问题。裁剪将根据面的方向确定是否需要渲染面,从而节省了遮挡面的大量像素计算。您可以根据特定情况更改这一行为;例如,我们可以创建一个需要透明的玻璃箱,以便看到箱子的所有侧面。

  • 光栅化器:现在我们已经计算出了修改过的可见几何模型,是时候将其转换为像素了。光栅化器为我们的网格三角形创建所有像素。这里发生了很多事情,但我们对此几乎没有控制权;通常的光栅化方式是在网格三角形的边缘内创建所有像素。我们还有其他模式,只渲染边缘上的像素以实现线框效果,但这通常用于调试目的:

图 6.2 - 光栅化的图例

图 6.2 - 光栅化的图例

  • 片段着色器:这是所有阶段中最可定制的阶段之一。它的目的很简单:确定光栅化器生成的每个片段(像素)的颜色。在这里,可以发生很多事情,从简单地输出纯色或对纹理进行采样到应用复杂的光照计算,比如法线贴图和 PBR。此外,您还可以使用这个阶段创建特殊效果,比如水动画、全息图、扭曲、解体和其他需要修改像素外观的特殊效果。我们将在本章的后续部分探讨如何使用这个阶段。

  • 深度测试:在将像素视为完成之前,我们需要检查像素是否可见。这个阶段检查像素的深度是在之前渲染的像素的后面还是前面,确保无论对象的渲染顺序如何,相机最近的像素始终位于其他像素的顶部。同样,通常情况下,这个阶段保持默认状态,优先考虑靠近相机的像素,但有些效果需要不同的行为。例如,在下一个截图中,您可以看到一种效果,它允许您看到其他对象后面的对象,比如帝国时代中的单位和建筑:

图 6.3 - 渲染角色的遮挡部分

图 6.3 - 渲染角色的遮挡部分

  • 混合:一旦确定了像素的颜色,并且我们确定像素没有被前一个像素遮挡,最后一步就是将其放入后备缓冲区(正在绘制的帧或图像)。通常的做法是覆盖该位置的任何像素(因为我们的像素更接近相机),但是如果考虑透明物体,我们需要将我们的像素与前一个像素结合起来,以产生透明效果。透明度除了混合之外还有其他要考虑的事情,但主要思想是混合控制像素将如何与后备缓冲区中先前渲染的像素结合。

着色器管线是一个需要整本书来讨论的主题,但在本书的范围内,前面的描述将让您对着色器的功能以及可能实现的效果有一个很好的了解。现在我们已经讨论了着色器如何渲染单个对象,值得讨论的是 Unity 如何使用渲染管线渲染所有对象。

渲染管线和 URP

我们已经介绍了视频卡如何渲染对象,但 Unity 负责要求视频卡对每个对象执行其着色器管线。为此,Unity 需要进行大量的准备和计算,以确定每个着色器需要何时以及如何执行。负责执行此操作的是 Unity 所谓的渲染管线。

渲染管线是绘制场景中对象的一种方式。起初,听起来似乎应该只有一种简单的方法来做到这一点,例如只需迭代场景中的所有对象,并使用每个对象材质中指定的着色器执行着色器管线,但实际上可能比这更复杂。通常,一个渲染管线与另一个之间的主要区别在于光照和一些高级效果的计算方式,但它们也可能在其他方面有所不同。

在以前的 Unity 版本中,只有一个单一的渲染管线,现在称为内置渲染管线。它是一个具有您在各种项目中所需的所有可能功能的管线,从移动 2D 图形和简单 3D 图形到主机或高端 PC 上可以找到的尖端 3D 图形。这听起来很理想,但实际上并非如此;拥有一个单一的巨大渲染器,需要高度可定制以适应所有可能情况,会产生大量的开销和限制,导致比创建自定义渲染管线更头疼。幸运的是,Unity 的最新版本引入了可编程渲染管线(SRP),一种为您的项目创建适用的渲染管线的方法。

幸运的是,Unity 不希望您为每个项目创建自己的渲染管线(这是一项复杂的任务),因此它为您创建了两个定制的管线,可以立即使用:URP(以前称为 LWRP),代表通用渲染管线,以及 HDRP,代表高清晰度渲染管线。其想法是您必须根据项目要求选择其中一个(除非您真的需要创建自己的)。URP 是我们为游戏创建项目时选择的一个渲染管线,适用于大多数不需要大量高级图形功能的游戏,例如移动游戏或简单的 PC 游戏,而 HDRP 则具有许多高级渲染功能,适用于高质量游戏。后者需要高端硬件才能运行,而 URP 可以在几乎所有相关目标设备上运行。值得一提的是,您可以随时在内置渲染器、HDRP 和 URP 之间切换,包括在创建项目后(不建议):

图 6.4 – 项目向导显示 HDRP 和 URP 模板

图 6.4 – 项目向导显示 HDRP 和 URP 模板

我们可以讨论每个着色器是如何实现的以及它们之间的区别,但是这可能会填满整整一章;现在,这一部分的想法是让你知道为什么我们在创建项目时选择了 URP,因为它有一些限制,我们将在本书中遇到这些限制,所以了解为什么我们接受了这些限制是很重要的(为了在所有相关的硬件上运行我们的游戏)。此外,我们需要知道我们选择了 URP 是因为它支持 Shader Graph,这是 Unity 工具,我们将在本章中使用它来创建自定义效果。以前的 Unity 内置管线没有为我们提供这样的工具(除了第三方插件)。最后,介绍 URP 的概念的另一个原因是它带有许多内置的着色器,我们需要在创建自己的着色器之前了解这些着色器,以避免重复造轮子,并且要适应这些着色器,因为如果你来自以前的 Unity 版本,你所了解的着色器在这里不起作用,实际上这正是我们将在本书的下一部分讨论的内容:不同 URP 内置着色器之间的区别。

URP 内置着色器

现在我们知道了 URP 和其他管线之间的区别,让我们讨论一下哪些着色器集成到了 URP 中。让我们简要描述一下这个管线中最重要的三个着色器:

  • Lit:这是旧的 Standard Shader 的替代品。当创建各种真实的物理材料时,比如木头、橡胶、金属、皮肤以及它们的组合(比如皮肤和金属盔甲的角色)时,这个着色器非常有用。它支持法线贴图、遮挡、金属和高光工作流程以及透明度。

  • Simple Lit:这是旧的 Mobile/Diffuse Shader 的替代品。顾名思义,这个着色器是 Lit 的简化版本,意味着它的光照计算是光照工作的简化近似,比其对应物少了一些功能。基本上,当你有简单的图形而没有真实的光照效果时,这是最好的选择。

  • Unlit:这是旧的 Unlit/Texture Shader 的替代品。有时,你需要没有任何光照的对象,在这种情况下,这就是适合你的着色器。没有光照并不意味着没有光或完全黑暗;实际上,这意味着对象根本没有阴影,并且完全可见而没有任何阴影。一些简单的图形可以使用这个,依赖于阴影被烘焙在纹理中,这意味着纹理带有阴影。这是非常高效的,特别是对于移动电话等低端设备。此外,你还有其他情况,比如光管或屏幕,这些对象不能接收阴影,因为它们发出光,所以即使在完全黑暗中也会以全彩色显示。在下面的截图中,你可以看到一个使用 Unlit Shader 的 3D 模型。它看起来像是被照亮了,但实际上只是模型的纹理在对象的不同部分应用了较浅和较深的颜色:

图 6.5 - 使用无光效果模拟廉价照明的 Pod

图 6.5 - 使用无光效果模拟廉价照明的 Pod

让我们使用 Simple Lit Shader 做一个有趣的分解效果来展示它的能力。你必须做以下操作:

  1. 从任何搜索引擎下载并导入Cloud Noise纹理:图 6.6 - 噪音纹理

图 6.6 - 噪音纹理

  1. 在项目面板中选择最近导入的纹理。

  2. 在检查器中,将Alpha Source属性设置为From Gray Scale。这意味着纹理的 alpha 通道将根据图像的灰度计算:图 6.7 - 从灰度纹理生成 Alpha 纹理设置

图 6.7 - 从灰度纹理生成 Alpha 纹理设置

重要提示

颜色的 Alpha 通道通常与透明度相关联,但您会注意到我们的物体不会是透明的。Alpha 通道是额外的颜色数据,可以在进行效果时用于多种目的。在这种情况下,我们将使用它来确定哪些像素首先被解体。

  1. 通过单击项目视图中的**+图标并选择Material**来创建一个材质:图 6.8 – 材质创建按钮

图 6.8 – 材质创建按钮

  1. 使用 Unity 顶部菜单中的GameObject | 3d Object | Cube选项创建一个立方体:图 6.9 – 创建立方体原语

图 6.9 – 创建立方体原语

  1. 从项目窗口将创建的材质拖动到立方体上应用材质。

  2. 单击检查器中 Shader 属性右侧的下拉菜单,并搜索Universal Render Pipeline | Simple Lit选项:图 6.10 – 简单光照着色器选择

图 6.10 – 简单光照着色器选择

  1. 选择Material,在Base Map中设置最近下载的 Cloud Noise Texture。

  2. 检查0.5图 6.11 阿尔法剪裁阈值材质滑块

图 6.11 Alpha Clipping 阈值材质滑块

  1. 当您移动 Alpha Clipping 滑块时,您会看到物体开始崩解。Alpha Clipping 会丢弃比样式值具有更低 Alpha 强度的像素:图 6.12 带有 Alpha Clipping 的崩解效果

图 6.12 带有 Alpha Clipping 的崩解效果

  1. 最后,将Render Face设置为Both以关闭Culling Shader Stage并查看立方体面的两侧:图 6.13 双面 Alpha Clipping

图 6.13 双面 Alpha Clipping

  1. 请注意,创建纹理的艺术家可以手动配置 Alpha 通道,而不是从灰度计算,只是为了精确控制崩解效果的外观,而不考虑纹理的颜色分布。

本节的目的不是全面介绍所有 URP Shader 的所有属性,而是让您了解当正确配置 Shader 时 Shader 可以做什么,以及何时使用每个集成 Shader。有时,您可以通过使用现有的 Shader 来实现所需的效果。实际上,在简单的游戏中,您可能可以在 99%的情况下使用现有的 Shader。因此,请尽量坚持使用它们。但是,如果确实需要创建自定义 Shader 来创建非常特定的效果,下一节将教您如何使用名为 Shader Graph 的 URP 工具。

使用 Shader Graph 创建 Shader

现在我们知道了 Shader 的工作原理以及 URP 中现有的 Shader,我们对何时需要创建自定义 Shader 以及何时不需要有了基本概念。如果确实需要创建一个,本节将介绍使用 Shader Graph 创建效果的基础知识,Shader Graph 是一种使用可视化节点编辑器创建效果的工具,在您不习惯编码时使用起来非常方便。

在本节中,我们将讨论 Shader Graph 的以下概念:

  • 创建我们的第一个 Shader Graph

  • 使用纹理

  • 组合纹理

  • 应用透明度

让我们开始看看如何创建和使用 Shader Graph。

创建我们的第一个 Shader Graph 资产

Shader Graph 是一种工具,允许我们使用基于节点的系统创建自定义效果。Shader Graph 中的效果可能看起来像以下截图,您可以看到创建全息效果所需的节点:

图 6.14 带有节点的 Shader Graph 以创建自定义效果

图 6.14 Shader Graph 带有节点以创建自定义效果

我们稍后将讨论这些节点的作用,并进行逐步的效果示例,但在屏幕截图中,您可以看到作者创建并连接了几个节点,这些节点是相互连接的框,每个节点都执行特定的过程以实现效果。使用 Shader Graph 创建效果的想法是学习您需要哪些特定节点以及如何正确连接它们,以创建一个“算法”或一系列有序的步骤来实现特定的结果。这类似于我们编写游戏玩法的方式,但这个图表是专门为效果目的而调整和简化的。

要创建和编辑我们的第一个 Shader Graph 资产,请执行以下操作:

  1. 在项目窗口中,单击**+图标,然后找到Shader | PBR Graph**选项。这将使用 PBR 模式创建一个 Shader Graph,这意味着这个 Shader 将支持照明效果(不像 Unlit Graphs):图 6.15 PBR Shader Graph 创建

图 6.15 PBR Shader Graph 创建

  1. 将其命名为WaterGraph。如果您错过了重命名资产的机会,请记住您可以选择资产,右键单击,然后选择重命名图 6.16 Shader Graph 资产

图 6.16 Shader Graph 资产

  1. 创建一个名为WaterMaterial的新材质,并将Shader设置为Shader Graphs/Water。如果由于某种原因 Unity 不允许您这样做,请尝试右键单击WaterGraph,然后单击Reimport。正如您所看到的,创建的 Shader Graph 资产现在显示为材质中的 Shader,这意味着我们已经创建了一个自定义 Shader:图 6.17 将 Shader Graph 设置为材质 Shader

图 6.17 将 Shader Graph 设置为材质 Shader

  1. 使用GameObject | 3d Object | Plane选项创建一个平面。

  2. 材质拖动到平面上应用它。

现在,您已经创建了您的第一个自定义 Shader 并将其应用于材质。到目前为止,它看起来一点也不有趣——它只是一个灰色的效果,但现在是时候编辑图表以释放其全部潜力了。正如图表的名称所暗示的,本章中我们将创建一个水效果,以说明 Shader Graph 工具集的几个节点以及如何连接它们,因此让我们从讨论主节点开始。当您双击打开图表时,您将看到以下内容:

图 6.18 具有计算对象外观所需的所有属性的主节点

图 6.18 具有计算对象外观所需的所有属性的主节点

所有节点都有输入引脚,它们需要的数据,以及输出引脚,这是其过程的结果。例如,在求和运算中,我们将有两个输入数字和一个输出数字,即求和的结果。在这种情况下,您可以看到主节点只有输入,这是因为进入主节点的所有数据将被 Unity 用于计算对象的渲染和照明,诸如所需的对象颜色或纹理(反照率输入引脚),它有多光滑(光滑度输入引脚),或者它含有多少金属(金属输入引脚),因此它们都是将影响照明如何应用于对象的属性。在某种意义上,这个节点的输入是整个图的输出数据,也是我们需要填充的数据。

让我们开始探索如何通过以下方式更改输出数据:

  1. 双击Shader Graph以打开其编辑窗口。

  2. 单击Albedo输入引脚左侧的灰色矩形:图 6.19 反照率主节点输入引脚

图 6.19 反照率主节点输入引脚

  1. 在颜色选择器中,选择浅蓝色,就像水一样。选择选择器周围的蓝色部分,然后在中间矩形中选择该颜色的一种色调:图 6.20 颜色选择器

图 6.20 颜色选择器

  1. 设置0.9图 6.21 光滑度 PBR 主节点输入引脚

图 6.21 光滑度 PBR 主节点输入引脚

  1. 单击窗口左上角的保存资源按钮:图 6.22 Shader Graph 保存选项

图 6.22 Shader Graph 保存选项

  1. 返回到场景视图,检查平面是否为浅蓝色,并且有太阳的反射:

图 6.23 初始 Shader Graph 结果

图 6.23 初始 Shader Graph 结果

如您所见,着色器的行为根据您在主节点中设置的属性而变化,但到目前为止,这与创建无光着色器并设置其属性没有什么不同;Shader Graph 的真正威力在于当您使用执行特定计算的节点作为主节点的输入时。我们将开始看到纹理节点,它们允许我们将纹理应用到我们的模型上。

使用纹理

使用纹理的想法是以一种方式将图像应用于模型,这意味着我们可以用不同的颜色涂抹模型的不同部分。请记住,模型有 UV 映射,这使得 Unity 知道纹理的哪个部分将应用于模型的哪个部分:

图 6.24 左侧是面部纹理;右侧是应用于面部网格的相同纹理

图 6.24 左侧是面部纹理;右侧是应用于面部网格的相同纹理

我们有几个节点来执行此任务,其中之一是 Sample Texture 2D,这是一个具有两个主要输入的节点。首先,它要求我们提供要对模型进行采样或应用的纹理,然后是 UV。您可以在以下截图中看到它:

图 6.25 Sample Texture 节点

图 6.25 Sample Texture 节点

如您所见,纹理输入节点的默认值为None,因此默认情况下没有纹理,我们需要手动指定。对于 UV,默认值为 UV0,这意味着默认情况下,节点将使用模型的主 UV 通道,是的,一个模型可以设置多个 UV,但现在我们将坚持使用主要的 UV。让我们尝试这个节点,执行以下操作:

  1. 从互联网上下载并导入可平铺的水纹理图 6.26 可平铺的水纹理

图 6.26 可平铺的水纹理

  1. 选择纹理,并确保纹理的包裹模式属性为重复,这将允许我们像在地形中那样重复纹理,因为想法是使用此着色器覆盖大水域:图 6.27 纹理重复模式

图 6.27 纹理重复模式

  1. 水着色器图中,在Shader Graph的空白区域右键单击并选择创建节点图 6.28 Shader Graph 创建节点选项

图 6.28 Shader Graph 创建节点选项

  1. 在搜索框中,写入Sample texture,所有的示例节点都会显示出来。双击选择Sample Texture 2D图 6.29 Sample texture 节点搜索

图 6.29 Sample texture 节点搜索

  1. 单击 Sample Texture 2D 节点的纹理输入引脚左侧的圆圈。这将允许我们选择要采样的纹理—只需选择水纹理。您可以看到纹理可以在节点的底部部分预览:图 6.30 带有输入引脚中纹理的 Sample Texture 节点

图 6.30 带有输入引脚中纹理的 Sample Texture 节点

  1. Sample Texture 2D节点的RGBA输出引脚拖动到主节点的Albedo输入引脚:图 6.31 连接纹理采样的结果与主节点的反照率引脚

图 6.31 连接纹理采样的结果与主节点的反照率引脚

  1. 单击 Shader Graph 编辑器左上角的保存资源按钮,查看场景视图中的更改:

图 6.32 应用纹理在我们的 Shader Graph 中的结果

图 6.32 应用纹理在我们的着色器图中的结果

如你所见,纹理已经正确应用到了模型,但是如果考虑到默认平面的大小是 10x10 米,水波似乎太大了,所以让我们平铺纹理!为此,我们需要改变模型的 UV,使它们变大。更大的 UV 听起来意味着纹理也应该变大,但要考虑到我们并没有使物体变大;我们只是修改了 UV,所以相同的物体大小将读取更多的纹理,这意味着更大的纹理采样区域将使纹理重复,并将它们放在相同的物体大小内,因此将被压缩在模型区域内。为此,请按照以下步骤进行:

  1. 右键单击任何空白区域,然后单击新建节点来搜索 UV 节点:图 6.33 寻找 UV 节点

图 6.33 寻找 UV 节点

  1. 使用相同的方法创建一个乘以节点。

  2. 设置4,4,4,4):图 6.34 将 UV 乘以 4

图 6.34 将 UV 乘以 4

  1. 将 UV 节点的Out引脚拖动到乘以节点的A引脚上连接它们。

  2. 乘以节点的Out引脚拖动到采样纹理 2D节点的UV引脚上连接它们:图 6.35 使用乘以后的 UV 来采样纹理

图 6.35 使用乘以后的 UV 来采样纹理

  1. 如果你保存了图表并返回到场景视图,你会看到现在涟漪变小了,因为我们已经平铺了模型的 UV。你还可以在采样纹理 2D节点的预览中看到:

图 6.36 模型 UV 乘法的结果

图 6.36 模型 UV 乘法的结果

现在我们可以做另一个有趣的效果,就是对纹理应用偏移来移动它。即使平面实际上并没有移动,我们也会通过移动纹理来模拟水流动,只是移动纹理。记住,确定纹理的哪一部分应用到模型的哪一部分的责任属于 UV,所以如果我们给 UV 坐标添加值,我们将移动它们,产生纹理滑动效果。为此,让我们按照以下步骤进行:

  1. 乘以节点的右侧创建一个Add节点。

  2. 将 UV 的Out引脚连接到Add节点的A引脚:图 6.37 给 UV 添加值

图 6.37 给 UV 添加值

  1. Add节点的左侧创建一个Time节点。

  2. Time节点连接到Add节点的B引脚:图 6.38 给 UV 添加时间

图 6.38 给 UV 添加时间

  1. Add节点的Out引脚连接到乘以节点的A输入引脚:图 6.39 添加和乘以 UV 作为采样纹理的输入

图 6.39 添加和乘以 UV 作为采样纹理的输入

  1. 保存并在场景视图中看到水流动。

  2. 如果你觉得水流动得太快,尝试使用乘法节点使时间变小。我建议你在查看下一个屏幕截图之前自己尝试一下,那里有答案:图 6.40 时间乘法以加快移动速度

图 6.40 时间乘法以加快移动速度

  1. 如果你觉得图表开始变得更大,尝试通过点击预览上出现的上箭头来隐藏一些节点的预览:

图 6.41 隐藏图表节点的预览和未使用的引脚

图 6.41 隐藏图表节点的预览和未使用的引脚

因此,简而言之,首先我们将时间添加到 UV 中以移动它,然后将移动后的 UV 的结果乘以使其变大以平铺纹理。值得一提的是,有一个平铺和偏移节点可以为我们完成所有这些工作,但我想向您展示一个简单的乘法来缩放 UV 和一个加法操作来移动它是如何产生一个不错的效果的;您无法想象使用其他简单数学节点可以实现的所有可能效果!实际上,让我们在下一节中探索数学节点的其他用途,以组合纹理。

组合纹理

尽管我们使用了节点,但我们并没有创建任何不能使用常规着色器创建的东西,但这将发生改变。到目前为止,我们可以看到水在移动,但它看起来仍然是静态的,这是因为涟漪总是相同的。我们有几种生成涟漪的技术,最简单的一种是将两个以不同方向移动的水纹理组合在一起以混合它们的涟漪,实际上,我们可以简单地使用相同的纹理,只是翻转了一下,以节省一些内存。为了组合这些纹理,我们将它们相加,然后除以 2,所以基本上,我们正在计算纹理的平均值!让我们通过以下方式来做到这一点:

  1. 选择时间采样器 2D之间的所有节点(包括它们),通过单击图表中的任何空白处创建一个选择矩形,按住并拖动单击,然后在所有目标节点都被覆盖时释放:图 6.42 选择多个节点

图 6.42 选择多个节点

  1. 右键单击并选择复制,然后再次右键单击并选择粘贴,或使用经典的Ctrl + CCtrl + V命令(Mac 中为command + Ccommand + V),或只需Ctrl + Dcommand + D)。

  2. 将复制的节点移动到原始节点下方:图 6.43 节点的复制

图 6.43 节点的复制

  1. 对于复制的节点,设置为-4,-4,-4,-4)。您可以看到纹理已经翻转了。

  2. 还要设置为-0.1:图 6.44 值的乘法

图 6.44 值的乘法

  1. 在两个采样器纹理 2D 节点的右侧创建一个加法节点,并将这些节点的输出连接到加法节点的AB输入引脚:图 6.45 添加两个纹理

图 6.45 添加两个纹理

  1. 您可以看到,由于我们对两种纹理的强度进行了求和,所以得到的组合太亮了,让我们通过乘以0.5,0.5,0.5,0.5来修复这个问题,这将把每个结果颜色通道除以 2,从而平均颜色:图 6.46 将两个纹理的总和除以得到平均值

图 6.46 将两个纹理的总和除以得到平均值

  1. 乘法节点的输出引脚连接到主节点的反照率引脚,以将所有这些计算应用为对象的颜色。

  2. 保存资产并在场景视图中查看结果:

图 6.47 纹理混合的结果

图 6.47 纹理混合的结果

您可以继续添加节点以使效果更加多样化,例如使用正弦节点应用非线性运动等,但我会让您自己通过实验来学习。现在,我们就到这里。一如既往,这个主题值得一本完整的书,本章的目的是让您对这个强大的 Unity 工具有一个初步了解。我建议您在互联网上寻找其他 Shader Graph 示例,以了解相同节点的其他用法,当然还有新节点。需要考虑的一件事是,我们刚刚做的一切基本上都应用于我们之前讨论的 Shader Pipeline 的片段着色器阶段。现在,让我们使用混合着色器阶段为水应用一些透明度。

应用透明度

在宣布我们的效果完成之前,我们可以做一个小小的添加,让水变得稍微透明一点。记住,Shader Pipeline 有一个混合阶段,负责将我们模型的每个像素混合到当前帧渲染的图像中。我们的 Shader Graph 的想法是修改这个阶段,应用 Alpha 混合,根据我们模型的 Alpha 值将我们的模型与先前渲染的模型进行混合。为了实现这个效果,执行以下步骤:

  1. 点击主节点右上角的轮子。

  2. 表面属性设置为透明

  3. 如果Blend属性不是 Alpha,请将其设置为Alpha图 6.48 PBR 主节点设置

图 6.48 PBR 主节点设置

  1. 0.5设置为:图 6.49 设置主节点的 Alpha

图 6.49 设置主节点的 Alpha

  1. 保存图表,查看透明度在场景视图中的应用。如果你看不到效果,只需在水中放一个立方体,使效果更加明显:图 6.50 水的阴影应用到立方体上

图 6.50 水的阴影应用到立方体上

  1. 你可以看到水投射在我们立方体上的阴影。这是因为 Unity 没有检测到对象是透明的,所以它认为必须投射阴影,所以让我们禁用它们。点击水平面,在检视器中查找 Mesh Renderer 组件。

  2. 照明部分,将投射阴影设置为关闭;这将禁用平面的阴影投射:

图 6.51 禁用投射阴影

图 6.51 禁用投射阴影

添加透明度是一个简单的过程,但也有其注意事项,比如阴影问题,在更复杂的场景中可能会有其他问题,所以我建议除非必要,否则避免使用透明度。实际上,我们的水可以不透明,特别是当我们将这种水应用到基地周围的河盆时,因为我们不需要看到水下的东西,但是我希望你知道所有的选择。在下一个截图中,你可以看到我们在基地下方放了一个巨大的平面,足够大以覆盖整个盆地:

图 6.52 在主场景中使用我们的水

图 6.52 在主场景中使用我们的水

总结

在本章中,我们讨论了 Shader 如何利用 GPU 工作,以及如何创建我们的第一个简单 Shader 来实现一个漂亮的水效果。使用 Shader 是一项复杂而有趣的工作,在团队中通常有一名或多名负责创建所有这些效果的人,这个职位被称为技术艺术家;所以,你可以看到,这个话题可以扩展成一个完整的职业。请记住,本书的目的是让你对行业中可能承担的各种角色有一点点了解,所以如果你真的喜欢这个角色,我建议你开始阅读专门讨论 Shader 的书籍。你面前有一条漫长但非常有趣的道路。

但现在先不谈 Shader 了,让我们转到下一个话题,讨论如何通过粒子系统改善图形并创建视觉效果!

第七章:使用粒子系统和 VFX 图进行视觉效果

在这里,我们将继续学习关于我们游戏的视觉效果。我们将讨论粒子系统,一种模拟火、瀑布、烟雾和各种流体的方法。此外,我们将看到两种 Unity 粒子系统来创建这些效果,ShurikenVFX Graph,后者比前者更强大,但需要更多的硬件。

在本章中,我们将讨论以下与粒子相关的概念:

  • 粒子系统简介

  • 创建流体模拟

  • 使用 VFX 图创建复杂模拟

粒子系统简介

到目前为止,我们创建的所有图形和效果都使用静态网格,即无法扭曲、弯曲或以任何方式变形的 3D 模型。火和烟等流体显然不能用这种网格来表示,但实际上,我们可以通过静态网格的组合来模拟这些效果,这就是粒子系统有用的地方。

粒子系统是发射和动画大量粒子广告牌的对象,这些广告牌是朝向摄像机的简单四边形网格。每个粒子都是一个静态网格,但渲染、动画和组合大量粒子可以产生流体的错觉。在下图中,您可以在左侧看到使用粒子系统的烟雾效果,右侧是相同粒子的线框视图。在那里,您可以看到创建烟雾错觉的四边形,这是通过将烟雾纹理应用到每个粒子并使它们在底部生成并朝着随机方向移动来实现的:

图 7.1-左侧,烟雾粒子系统;右侧,相同系统的线框图

图 7.1-左侧,烟雾粒子系统;右侧,相同系统的线框图

在本节中,我们将涵盖与粒子相关的以下概念:

  • 创建基本粒子系统

  • 使用高级模块

让我们开始讨论如何创建我们的第一个粒子系统。

创建基本粒子系统

为了说明粒子系统的创建,让我们创建一个爆炸效果。想法是一次产生大量粒子并将它们朝各个方向扩散。让我们开始创建粒子系统并配置它提供的基本设置以更改其默认行为。为此,请按照以下步骤操作:

  1. 选择GameObject | Effects | Particle System选项:图 7.2-粒子系统创建按钮

图 7.2-粒子系统创建按钮

  1. 您应该在以下截图中看到效果。默认行为是一列粒子向上移动,就像之前显示的烟雾效果一样。让我们改变一下:图 7.3-默认粒子系统外观

图 7.3-默认粒子系统外观

  1. 单击场景中创建的对象,查看检查器。

  2. 通过单击标题打开形状部分。

  3. 形状属性更改为球体。现在粒子应该在所有可能的方向上移动,而不是遵循默认的锥形:图 7.4-形状属性

图 7.4-形状属性

  1. 在粒子系统10中。这将使粒子移动得更快。

  2. 在相同的模块中,设置0.5。这指定了粒子的寿命。在这种情况下,我们给了半秒的寿命。结合速度(每秒 10 米),这使得粒子在移动 5 米后消失:图 7.5-主粒子系统模块

图 7.5-主粒子系统模块

  1. 打开0。这个属性指定每秒将发射多少粒子,但对于爆炸,实际上我们需要一团粒子,所以在这种情况下我们不会持续不断地发射粒子。

  2. 100中:图 7.6-发射模块

图 7.6-发射模块

  1. 在主模块(标题为1)中取消选中循环。在我们的情况下,爆炸不会不断重复;我们只需要一个爆炸:图 7.7 – 循环复选框

图 7.7 – 循环复选框

  1. 现在粒子不再循环,您需要手动点击粒子效果窗口右下角的播放按钮来查看系统:图 7.8 – 粒子系统播放控件

图 7.8 – 粒子系统播放控件

  1. 停止动作设置为销毁。当持续时间过去时,这将销毁对象。这只在游戏运行时有效,因此您可以在编辑场景时安全地使用此配置:图 7.9 – 停止动作设置为销毁

图 7.9 – 停止动作设置为销毁

  1. 设置3。这将使粒子变大,看起来更密集:图 7.10 – 粒子系统开始大小

图 7.10 – 粒子系统开始大小

  1. 单击主模块的开始旋转属性右侧的向下箭头,并选择两个常数之间的随机值

  2. 在上一步骤之后出现的两个输入值中设置0360。这样可以使粒子在生成时具有随机旋转,使它们看起来略有不同:图 7.11 – 随机开始旋转

图 7.11 – 随机开始旋转

  1. 现在粒子的行为符合预期,但外观不符合预期。让我们改变一下。通过点击爆炸创建一个新材质。

  2. 将其着色器设置为Universal Render Pipeline/Particles/Unlit。这是一种特殊的着色器,用于将纹理应用到 Shuriken 粒子系统:图 7.12 – 粒子系统材质着色器

图 7.12 – 粒子系统材质着色器

  1. 从互联网或资产商店下载烟雾粒子纹理。在这种情况下,重要的是下载带有黑色背景的纹理;忽略其他的:图 7.13 – 烟雾粒子纹理

图 7.13 – 烟雾粒子纹理

  1. 将此纹理设置为材质的基本贴图

  2. 表面类型设置为透明混合模式设置为加法。这样做将使粒子相互混合,而不是相互绘制,以模拟一大团烟雾而不是单个烟雾。我们使用加法模式,因为我们的纹理有黑色背景,而且我们想要创建一种光照效果(爆炸会照亮场景):图 7.14 – 粒子的表面选项

图 7.14 – 粒子的表面选项

  1. 将您的材质拖到渲染器模块的材质属性中:图 7.15 – 粒子材质设置

图 7.15 – 粒子材质设置

  1. 现在您的系统应该是这样的:

图 7.16 – 前面设置的结果

图 7.16 – 前面设置的结果

在前面的步骤中,我们已经改变了粒子或广告牌的生成方式(使用发射模块),它们将朝向哪个方向移动(使用形状模块),它们将以多快的速度移动,它们将持续多久,它们将有多大(使用主模块),以及它们将看起来像什么(使用渲染器模块)。创建粒子系统就是正确配置它们不同设置的简单情况。当然,正确地做这件事本身就是一门艺术;它需要创造力和对如何使用它们提供的所有设置和配置的知识。因此,为了增加我们的配置工具箱,让我们讨论一些高级模块。

使用高级模块

我们的系统看起来不错,但我们可以大大改进它,所以让我们启用一些新模块来提高其质量:

  1. 点击颜色随生命周期模块左侧的复选框以启用它:图 7.17 - 启用颜色随生命周期模块

图 7.17 - 启用颜色随生命周期模块

  1. 通过点击标题打开模块,并点击颜色属性右侧的白色条。这将打开渐变编辑器。

  2. 点击白色标记栏的左上方略微向右侧,创建一个新的标记。同时,点击白色标记的右上方略微向左侧,创建第四个标记。这些标记将允许我们在粒子生命周期中指定透明度:图 7.18 - 颜色随生命周期渐变编辑器

图 7.18 - 颜色随生命周期渐变编辑器

  1. 如果创建了不需要的标记,只需将它们拖到窗口外即可删除。

  2. 点击左上角的标记(不是我们创建的那个,而是已经存在的那个)并设置为0。对右上角的标记也做同样的操作,如下图所示。现在你应该看到粒子在爆炸结束时淡出而不是突然消失:图 7.19 - 渐变淡入和淡出

图 7.19 - 渐变淡入和淡出

  1. 通过点击其复选框启用限制生命周期内的速度模块。

  2. 设置为0.1。这将使粒子慢慢停止而不是继续移动:图 7.20 - 减弱速度以使粒子停止

图 7.20 - 减弱速度以使粒子停止

  1. 启用-9090。记住,你应该通过点击属性右侧的向下箭头来设置两个常数之间的随机值。现在粒子在它们的生命周期中应该稍微旋转,以模拟更多的运动:

图 7.21 - 随机旋转速度

图 7.21 - 随机旋转速度

正如你所看到的,有许多额外的模块可以启用和禁用,以在现有模块之上添加行为层,因此,再次创造性地使用它们来创建各种效果。记住,你可以创建这些系统的预制件,以在整个场景中复制它们。我还建议在资产商店搜索和下载粒子效果,看看其他人如何使用相同的系统来创建惊人的效果。这是学习如何创建它们的最佳方式,看到各种不同的系统,这实际上也是我们将在下一节中要做的事情,创建更多的系统!

创建流体模拟

正如我们所说,学习如何创建粒子系统的最佳方式是继续寻找已经创建的粒子系统,并探索人们如何使用各种系统设置来创建完全不同的模拟。

在本节中,我们将看到如何使用粒子系统创建以下效果:

  • 瀑布效果

  • 篝火效果

让我们从最简单的瀑布效果开始。

创建瀑布效果

为了做到这一点,请按照以下步骤进行:

  1. 创建一个新的粒子系统(GameObject | Effects | Particle System)。

  2. 形状设置为边缘,并将半径设置为5形状模块中。这将使粒子沿着一个发射线产生:图 7.22 - 边缘形状

图 7.22 - 边缘形状

  1. 设置为50

  2. 设置为33图 7.23 - 主模块设置

图 7.23 - 主模块设置

  1. 设置为0.5。这将使粒子下落:图 7.24 - 主模块中的重力修饰器

图 7.24 - 主模块中的重力修饰器

  1. 使用我们之前为这个系统创建的爆炸材质:图 7.25 - 爆炸粒子材质

图 7.25 - 爆炸粒子材质

  1. 启用颜色随生命周期并打开渐变编辑器。

  2. 单击右下角的标记,这次你应该看到一个颜色选择器,而不是一个透明度滑块。顶部的标记允许您随时间改变透明度,而底部的标记则随时间改变粒子的颜色。在这个标记中设置浅蓝色:

图 7.26 – 从白色到浅蓝色的渐变

图 7.26 – 从白色到浅蓝色的渐变

作为挑战,我建议您在这个结束的地方添加一个小的粒子系统,以创建一些水花,模拟水与湖底碰撞。现在我们可以将这个粒子系统添加到我们场景中的一个山丘上进行装饰,就像下面的截图一样。我已经调整了系统,使其在这种情况下看起来更好。我挑战你自己调整它,使它看起来像这样:

图 7.27 – 应用到我们当前场景中的瀑布粒子系统

图 7.27 – 应用到我们当前场景中的瀑布粒子系统

现在,让我们创建另一个效果,一个篝火。

创建篝火效果

为了创建它,做以下操作:

  1. 创建一个粒子系统。

  2. 在互联网或资产商店上寻找火焰粒子纹理表纹理。这种纹理应该看起来像一个不同火焰纹理的网格。想法是将火焰动画应用到我们的粒子上,交换所有这些小纹理:图 7.28 – 粒子纹理精灵表

图 7.28 – 粒子纹理精灵表

  1. 创建一个粒子材质,并将此纹理设置为基本贴图。将基本贴图右侧的颜色设置为白色。然后将此材质设置为粒子材质。记得将表面类型设置为透明混合模式设置为叠加图 7.29 – 带有粒子精灵表的材质

图 7.29 – 带有粒子精灵表的材质

  1. Y中启用4中的4。之后,您应该看到粒子交换纹理:图 7.30 – 启用纹理表动画

图 7.30 – 启用纹理表动画

  1. 在主模块中设置01.5

  2. 形状中设置0.5

  3. 创建第二个粒子系统,并将其设置为火系统的子对象:图 7.31 – 粒子系统的父子关系

图 7.31 – 粒子系统的父子关系

  1. 应用爆炸示例中的烟雾材质。

  2. 形状中设置00.5

  3. 系统应该看起来像这样:

图 7.32 – 结合火和烟粒子系统的结果

图 7.32 – 结合火和烟粒子系统的结果

正如您所看到的,您可以组合多个粒子系统来创建单个效果。在这样做时要小心,因为很容易发射太多的粒子并影响游戏的性能。粒子并不便宜,如果不小心使用,可能会导致游戏的**FPS(每秒帧数)**下降。

到目前为止,我们已经探索了 Unity 系统中的一个用于创建这种效果的系统,虽然这个系统对于大多数情况来说已经足够了,但 Unity 最近发布了一个新的系统,可以生成更复杂的效果,称为VFX Graph。让我们看看如何使用它,以及它与 Shuriken 有何不同。

使用 VFX Graph 创建复杂的模拟

到目前为止,我们使用的粒子系统称为 Shuriken,它在 CPU 中处理所有计算。这既有优点也有缺点。优点是它可以在 Unity 支持的所有设备上运行,而不受它们的能力限制(它们都有 CPU),但缺点是如果我们不小心发射太多粒子,很容易超出 CPU 的能力。现代游戏需要更复杂的粒子系统来生成可信的效果,而这种基于 CPU 的粒子系统解决方案已经开始达到极限。这就是 VFX Graph 的用武之地:

图 7.33 – 左侧是一个大型粒子系统,右侧是 VFX 图的示例

图 7.33 – 左侧是一个大型粒子系统,右侧是 VFX 图的示例

VFX 图(视觉效果图)是基于 GPU 的粒子系统解决方案,这意味着系统在视频卡上执行,而不是在 CPU 上执行。这是因为视频卡在执行许多小模拟方面要高效得多,就像系统的每个粒子所需的模拟一样,因此我们可以使用 GPU 比使用 CPU 实现更高数量级的粒子。这里的缺点是我们需要一个具有计算着色器功能的相当现代的 GPU 来支持此系统,因此我们将使用此系统排除某些目标平台(忘记大多数手机),因此只有在您的目标平台支持它时才使用它(中高端 PC、游戏机和一些高端手机)。

在本节中,我们将讨论 VFX 图的以下概念:

  • 安装 VFX 图

  • 创建和分析 VFX 图

  • 创建雨效果

让我们开始看看如何在我们的项目中添加对 VFX 图的支持。

安装 VFX 图

到目前为止,我们已经使用了许多 Unity 功能,这些功能已经安装在我们的项目中,但是 Unity 可以通过各种官方和第三方插件进行扩展。VFX 图是其中之一,如果您使用通用渲染管线(URP),则需要单独安装该功能。我们可以使用包管理器来完成这一点,包管理器是一个专门用于管理官方 Unity 插件的 Unity 窗口。

在安装这些软件包时需要考虑的一点是,每个软件包或插件都有自己的版本,与 Unity 版本无关。这意味着您可以安装 Unity 2020.1,但 VFX 图可以是 7.1.5 或 7.1.2 或任何您想要的版本,并且您实际上可以将软件包更新到新版本,而无需升级 Unity。这很重要,因为这些软件包的某些版本需要 Unity 的最低版本。此外,某些软件包依赖于其他软件包,实际上是这些软件包的特定版本,因此我们需要确保我们拥有每个软件包的正确版本以确保兼容性。需要明确的是,软件包的依赖关系会自动安装,但有时我们可以单独安装它们,因此在这种情况下,我们需要检查所需的版本。听起来很复杂,但实际上比听起来简单。

在撰写本书时,我正在使用 VFX 图版本 8.2.0,与 URP 相同的版本。是的,URP 是另一个您需要使用包管理器安装的功能,但是由于我们使用了 URP 模板创建项目,它已经为我们安装好了。关于版本,一个建议:在制作游戏期间,除非确有必要,否则永远不要更新 Unity 版本或软件包版本。升级通常会带来许多兼容性版本,这意味着在升级后,您的游戏的某些部分可能需要修复以符合这些软件包的新版本的工作方式。此外,请考虑一些软件包具有已验证标签,这意味着它已在我们的 Unity 版本中进行了测试,因此建议使用它。

现在,让我们按照以下步骤安装 VFX 图:

  1. 在 Unity 的顶部菜单中,转到窗口 | 包管理器图 7.34 – 包管理器位置

图 7.34 – 包管理器位置

  1. 在窗口左侧查找视觉效果图软件包。确保选择 8.2.0 或更高版本:图 7.35 – 视觉效果图软件包

图 7.35 – 视觉效果图软件包

  1. 点击窗口右下角的安装按钮,等待软件包安装:图 7.36 – 安装软件包按钮

图 7.36 – 安装软件包按钮

  1. 建议在安装包后重新启动 Unity,所以保存你的更改并重新启动 Unity。

现在我们已经安装了 VFX 图形,让我们使用它来创建我们的第一个粒子系统。

创建和分析 VFX 图形

使用 VFX 图形创建粒子系统的理念与常规粒子系统类似。我们将链接和配置模块作为粒子行为的一部分,每个模块都添加一些特定的行为,但我们的做法与 Shuriken 有很大不同。首先,我们需要创建一个视觉效果图形,这是一个包含所有模块和配置的资产,然后让一个游戏对象播放这个图形。让我们按照以下步骤来做:

  1. 在项目窗口中,点击**+按钮,查找视觉效果** | 视觉效果图形图 7.37 - 视觉效果图形

图 7.37 - 视觉效果图形

  1. 使用游戏对象 | 创建空选项创建一个空游戏对象:图 7.38 - 创建空游戏对象

图 7.38 - 创建空游戏对象

  1. 选择创建的对象并查看检查器。

  2. 使用添加组件搜索栏,查找可视效果组件并点击它以将其添加到对象中:图 7.39 - 向视觉效果图形添加组件

图 7.39 - 向视觉效果图形添加组件

  1. 将我们创建的 VFX 资产拖到我们游戏对象的可视效果组件的资产模板属性中:图 7.40 - 使用先前创建的 VFX 资产的可视效果

图 7.40 - 使用先前创建的 VFX 资产的可视效果

  1. 你应该看到时钟粒子从我们的对象中发射出来:

图 7.41 - 默认 VFX 资产结果

图 7.41 - 默认 VFX 资产结果

现在我们有了一个基本效果,让我们创建一些需要大量粒子的东西,比如密集的雨。在这样做之前,让我们探索一些 VFX 图形的核心概念。如果你双击可视效果资产,你会看到以下编辑器:

图 7.42 - 可视效果图形编辑器窗口

图 7.42 - 可视效果图形编辑器窗口

这个窗口由几个相互连接的节点组成,生成要执行的操作流。起初,它似乎类似于着色器图,但它的工作方式有点不同,所以让我们研究一下默认图的每个部分。

要探索的第一个区域是包含三个节点的虚线区域。这就是 Unity 所谓的系统。系统是一组定义粒子行为的节点,你可以有任意多个,这相当于有几个粒子系统对象。每个系统由上下文组成,即虚线区域内的节点,在这种情况下,我们有初始化粒子更新粒子输出粒子四边形。每个上下文代表粒子系统逻辑流的不同阶段,所以让我们定义一下我们图中的每个上下文做什么:

  • 初始化粒子:这定义了每个发射粒子的初始数据,如位置、颜色、速度和大小。这类似于本章开头看到的粒子系统的主模块中的起始属性。这个节点中的逻辑只有在发射新粒子时才会执行。

  • 更新粒子:在这里,我们可以对活动粒子的数据应用修改。我们可以改变粒子数据,比如当前速度或大小,所有帧都可以。这类似于先前粒子系统的随时间节点。

  • 输出粒子四边形:这个上下文将在需要渲染粒子时执行。它将读取粒子数据,看到在哪里渲染,如何渲染,使用哪个纹理和颜色,以及不同的视觉设置。这类似于先前粒子系统的渲染器模块。

除了一些基本配置外,我们可以在每个上下文中添加。每个块都是在上下文中执行的操作。我们有一些可以在任何上下文中执行的操作,然后是一些特定的上下文操作。例如,我们可以在初始化粒子上下文中使用添加位置块来移动初始粒子位置,但如果我们在更新粒子上下文中使用相同的块,它将不断地移动粒子。因此,上下文是粒子生命周期中发生的不同情况,而块是在这些情况下执行的操作:

图 7.43 – 在初始化粒子上下文中的设置速度随机块。这将设置粒子的初始速度

图 7.43 – 在初始化粒子上下文中的设置速度随机块。这将设置粒子的初始速度

此外,我们可以有独立上下文,即系统之外的上下文,例如生成。这个上下文负责告诉系统需要创建一个新粒子。我们可以添加块来指定上下文何时告诉系统创建粒子,例如在固定时间内以固定速率、突发等。生成将根据其块创建粒子,而系统负责根据我们在每个上下文中设置的块来初始化、更新和渲染每个粒子。

因此,我们可以看到与 Shuriken 有很多相似之处,但在这里创建系统的方式是完全不同的。让我们通过创建一个雨效果来加强这一点,这将需要大量粒子,这是 VFX 图形的一个很好的使用案例。

创建雨效果

为了创建这种效果,执行以下操作:

  1. 设置10000图 7.44 – 初始化粒子上下文

图 7.44 – 初始化粒子上下文

  1. 设置10000图 7.45 – 常量生成率块

图 7.45 – 常量生成率块

  1. 初始化粒子上下文中的设置速度随机块中分别设置0-500)和(0-750)。这将为我们的粒子设置一个指向下方的随机速度:图 7.46 – 设置速度随机块

图 7.46 – 设置速度随机块

  1. 单击初始化粒子标题以选择上下文,一旦突出显示,按空格键显示添加块窗口。

  2. 搜索设置位置随机块并单击它:图 7.47 – 添加块

图 7.47 – 添加块

  1. -500-50)和(50050)分别设置。这将定义一个初始区域,以在其中随机生成粒子。

  2. 单击0-12.50)和(10025100)左侧的箭头。这将定义粒子应该存在的区域。粒子实际上可以移出这个区域,但这对系统正常工作很重要(在互联网上搜索视锥体剔除以获取更多信息)。

  3. 选择执行系统的 GameObject,并在场景视图的右下窗口中选中显示边界复选框,以查看先前定义的边界:图 7.48 – 视觉效果播放控制

图 7.48 – 视觉效果播放控制

  1. 将对象位置设置为覆盖整个基础区域。在我的案例中,位置是(10037100)。请记住,您需要更改变换组件的位置图 7.49 – 设置变换位置

图 7.49 – 设置变换位置

  1. 设置0.5。这将使粒子的寿命更短,确保它们始终在边界内:图 7.50 – 设置寿命随机块

图 7.50 – 设置寿命随机块

  1. 输出粒子四边形上下文的主纹理属性更改为另一个纹理。在这种情况下,之前下载的烟雾纹理可以在这里使用,即使它不是水,因为我们将在一会儿修改它的外观。另外,如果你愿意,你也可以尝试下载水滴纹理:图 7.51 - VFX 图主纹理

图 7.51 - VFX 图主纹理

  1. 输出粒子四边形上下文的混合模式设置为附加图 7.52 - VFX 图的附加模式

图 7.52 - VFX 图的附加模式

  1. 如果你看不到最后的更改被应用,点击窗口左上角的编译按钮。另外,你可以使用Ctrl + S(Mac 上为Command + S)保存你的更改:图 7.53 - VFX 资产保存控制

图 7.53 - VFX 资产保存控制

  1. 现在我们需要稍微拉伸我们的粒子,使其看起来像真正的雨滴而不是下落的球。为此,首先我们需要改变粒子的方向,使它们不总是指向摄像机。为了做到这一点,右键单击输出粒子四边形上下文中的定向块,然后选择删除(或在 PC 上按Delete,在 Mac 上按Command + Backspace):图 7.54 - 删除块

图 7.54 - 删除块

  1. 我们想根据它们的速度方向拉伸我们的粒子。为此,选择输出粒子四边形上下文的标题,然后按空格键查找要添加的块。在这种情况下,我们需要搜索沿速度定向块。

  2. 添加0.251.50.25)。这将拉伸粒子,使其看起来像落下的水滴:图 7.55 - 设置比例块

图 7.55 - 设置比例块

  1. 再次点击窗口左上角的编译按钮,以查看更改。你的系统应该看起来像这样:

图 7.56 - 雨结果

图 7.56 - 雨结果

从这里开始,你可以根据自己的意愿向上下文中添加和删除块进行实验,我再次建议你寻找已经创建的视觉效果图,以找到其他系统的创意。实际上,你可以通过查看 Shuriken 中制作的效果并使用类似的块来获得 VFX 图的创意。另外,我建议你查看 VFX 图文档docs.unity3d.com/Packages/com.unity.visualeffectgraph@7.1/manual/index.html以了解更多关于这个系统的信息。

总结

在本章中,我们讨论了使用 Shuriken 和 VFX 图创建粒子系统的两种不同方法。我们用它们来模拟不同的流体现象,如火、瀑布、烟雾和雨。这个想法是将粒子系统与网格相结合,生成场景所需的所有可能道具。另外,正如你可以想象的,专业地创建这些效果需要你深入了解。如果你想专注于这一点(技术艺术家的另一部分工作),你需要学会如何创建自己的粒子纹理,以获得你想要的精确外观和感觉,编写控制系统某些方面的代码脚本,以及粒子创建的其他几个方面。再次强调,这超出了本书的范围。

现在我们的场景中有了一些雨,我们可以看到天空和场景中的光线并不真正反映出雨天,所以让我们在下一章中解决这个问题!

第八章:使用通用渲染管线进行照明

照明是一个复杂的主题,有几种可能的处理方式,每种方式都有其优缺点。为了在最佳性能下获得最佳质量,您需要确切了解您的渲染器如何处理它,这正是我们将在本章中要做的。我们将讨论 Unity 的通用渲染管线URP)中如何处理照明,以及如何正确配置它以适应我们场景的氛围和适当的照明效果。

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

  • 应用照明

  • 应用阴影

  • 优化照明

应用照明

在讨论游戏中处理照明的方式时,我们可以使用两种主要方式,称为前向渲染延迟渲染。两者以不同的顺序处理照明,具有不同的技术、要求、优缺点。前向渲染通常推荐用于性能,而延迟渲染通常推荐用于质量。后者被 Unity 的高清晰度渲染管线使用,这是用于高端设备高质量图形的渲染器。在撰写本书时,Unity 正在为 URP 开发一个高性能版本。此外,在 Unity 中,前向渲染有两种类型:多通道前向,用于内置渲染器(旧的 Unity 渲染器),以及单通道前向,用于 URP。同样,每种方法都有其优缺点。

重要信息

实际上,还有其他可用的选项,包括官方和第三方的选项,比如顶点光照,但暂时我们将专注于三种主要的选项 - 您 95%的时间使用的选项。

选择其中一种取决于您正在创建的游戏类型以及您需要在哪个目标平台上运行游戏。由于您应用照明到场景的方式,您选择的选项将发生很大变化,因此您必须了解您正在处理的系统。

在本节中,我们将讨论以下实时照明概念:

  • 讨论照明方法

  • 使用天空盒配置环境光照

  • 在 URP 中配置照明

让我们开始比较先前提到的照明方法。

讨论照明方法

总之,我们提到了三种主要的处理照明的方式:

  • 前向渲染(单通道)

  • 前向渲染(多通道)

  • 延迟渲染

在我们讨论它们之间的差异之前,让我们谈谈它们共同的特点。这三种渲染器都通过确定相机可以看到哪些对象来开始绘制场景;也就是说,那些落在相机截锥体内的对象,并在选择相机时提供一个巨大的金字塔。

图 8.1 - 相机的截锥体只显示可以看到的对象

图 8.1 - 相机的截锥体只显示可以看到的对象

之后,Unity 将按照距离相机最近到最远的顺序对它们进行排序(透明对象处理方式略有不同,但暂时忽略)。这样做是因为更有可能靠近相机的对象将覆盖大部分相机,因此它们将遮挡其他对象,防止我们浪费资源计算被遮挡的像素。

最后,Unity 将尝试按照这个顺序渲染对象。这就是光照方法之间开始出现差异的地方,所以让我们开始比较这两种前向渲染变体。对于每个对象,单次渲染将在一个绘制调用中计算对象的外观,包括所有影响对象的光源,或者我们称之为绘制调用。绘制调用是 Unity 要求显卡实际渲染指定对象的确切时刻。之前的所有工作只是为了这一刻做准备。在多次渲染前向渲染器的情况下,简化一点实际逻辑,Unity 将为影响对象的每个光源渲染一次对象。因此,如果对象受到三个光源的照明,Unity 将渲染对象三次,这意味着将发出三个绘制调用,并将执行渲染过程的 GPU 进行三次调用:

图 8.2 – 左图,多次渲染中受两个光源影响的球体的第一个绘制调用;中间图,球体的第二个绘制调用;右图,两个绘制调用的组合

图 8.2 – 左图,多次渲染中受两个光源影响的球体的第一个绘制调用;中间图,球体的第二个绘制调用;右图,两个绘制调用的组合

现在你可能在想,“为什么我要使用多次渲染?单次渲染更高效!”是的,你是对的!单次渲染比多次渲染更高效,这就是其中的好处。GPU 中的绘制调用有一定数量的操作可以执行,因此绘制调用的复杂度有限。计算对象的外观和所有影响它的光源是非常复杂的,为了使其适应一个绘制调用,单次渲染执行了简化版本的光照计算,这意味着光照质量和功能较少。它们还有一个限制,即一次只能处理多少个光源,目前写作本书时,每个对象的限制是八个(低端设备为四个)。这听起来像是一个小数字,但通常足够了。

另一方面,多次渲染可以应用任意数量的光源,并且可以为每个光源执行不同的逻辑。假设我们的物体受到四个光源的影响,但有两个光源对其影响很大,因为它们更近或强度更高,而其余的光源对物体的影响只是足够让人注意到。在这种情况下,我们可以使用更高质量的方式渲染前两个光源,而用廉价的计算渲染其余的光源——没有人能够察觉到区别。在这种情况下,多次渲染可以使用像素光照计算前两个光源,而使用顶点光照计算其余的光源。它们的区别在于它们的名称;像素光照按对象像素计算光照,而顶点光照按对象顶点计算光照,并填充这些顶点之间的像素,从而在顶点之间插值信息。您可以清楚地看到以下图像中的区别:

图 8.3 – 左图,使用顶点光照渲染的球体;右图,使用像素光照渲染的球体)

图 8.3 – 左图,使用顶点光照渲染的球体;右图,使用像素光照渲染的球体)

在单次渲染中,将所有内容都计算在一个绘制调用中会迫使你使用顶点光照或像素光照;你不能将它们结合起来。

因此,总结一下单通道和多通道之间的区别,在单通道中,性能更好,因为每个对象只绘制一次,但你只能应用有限数量的光照,而在多通道中,你需要多次渲染对象,但没有光照数量的限制,并且你可以为每个光源指定精确的质量。还有其他需要考虑的事情,比如绘制调用的实际成本(一个绘制调用可能比两个简单的绘制更昂贵),以及特殊的光照效果,比如卡通着色,但让我们保持简单。

最后,让我们简要讨论一下延迟渲染。尽管我们不打算使用它,但了解为什么我们不这样做是很有趣的。在确定哪些对象落在视锥体内并对它们进行排序之后,延迟将渲染对象而不进行任何光照,生成所谓的G-Buffer。G-Buffer 是一组包含有关场景对象的不同信息的图像,例如其像素的颜色(不带光照),每个像素的方向(称为法线),以及离摄像机的距离。你可以在以下图中看到 G-Buffer 的典型示例:

图 8.4 - 左图,对象的纯色;中图,每个像素的深度;右图,像素的法线

图 8.4 - 左图,对象的纯色;中图,每个像素的深度;右图,像素的法线

重要信息

法线是方向,方向的(X,Y,Z)分量被编码在颜色的 RGB 分量中。

在渲染场景中的所有对象之后,Unity 将迭代所有可以在相机中看到的光源,从而在 G-Buffer 上应用一层光照,从中获取信息来计算特定的光照。在所有光源都被处理之后,你将得到以下结果:

图 8.5 - 应用于上一图像中的 G-Buffer 的三种光的组合

图 8.5 - 应用于上一图像中的 G-Buffer 的三种光的组合

正如你所看到的,这种方法的延迟部分来自于将照明计算作为渲染过程的最后阶段的想法。这样做更好,因为你不会浪费资源计算可能被遮挡的物体的照明。如果在前向渲染中首先渲染图像的底部,那么其余物体将遮挡的像素就是徒劳的计算。此外,延迟只计算光照能够到达的确切像素。例如,如果你使用手电筒,Unity 只会在手电筒锥体内的像素中计算光照。缺点是,一些相对较旧的显卡不支持延迟,而且你无法使用顶点光照质量来计算照明,因此你将需要付出像素光照的代价,这在低端设备上不被推荐(甚至在简单的图形游戏中也不需要)。

那么,为什么我们要使用 URP 和单通道前向渲染?因为它在性能、质量和简单性之间提供了最佳平衡。在这个游戏中,我们不会使用太多的光源,所以我们不会担心单通道的光源数量限制,而且我们也不会充分利用延迟的好处,所以使用更多的硬件来运行游戏是没有意义的。

现在我们对 URP 处理光照的基本概念有了一个非常基本的了解,让我们开始使用它吧!

配置天空盒的环境光照

有不同的光源可以影响场景,如太阳、火炬、灯泡等。这些被称为直接光;也就是说,发射光线的物体。然后,我们有间接光,通常代表直接光的反射。然而,如果要让游戏以至少 30 FPS(或者只是运行)运行,计算所有光线发射的所有光线的所有反射是不可能的。问题在于没有间接光会产生不真实的结果,因为我们当前的场景照明中,你可以观察到阳光无法到达的地方完全黑暗,因为没有光从其他阳光照射的地方反射过来:

图 8.6 – 没有环境光的山上投影的阴影

图 8.6 – 没有环境光的山上投影的阴影

为了解决这个问题,我们可以使用这些反弹的近似值。这就是我们所说的环境光。这代表了通常根据天空的颜色施加一点点光的基础光照层,但你可以选择任何你想要的颜色。例如,在晴朗的夜晚,我们可以选择深蓝色来代表月光的色调。

默认情况下,Unity 不会从天空计算环境光,因此我们需要手动进行以下操作:

  1. 在层次结构中选择地形,并在检查器的右上角取消选择“静态”。稍后我们会解释为什么要这样做:图 8.7 – 层次结构中的地形

图 8.7 – 层次结构中的地形

  1. 点击窗口 | 渲染 | 灯光设置。这将打开场景灯光设置窗口:图 8.8 – 灯光设置位置

图 8.8 – 灯光设置位置

  1. 点击窗口底部的生成灯光按钮。如果到目前为止你还没有保存场景,会提示你保存,这是必要的:图 8.9 – 生成灯光按钮

图 8.9 – 生成灯光按钮

  1. 查看 Unity 窗口右下角的进度计算栏,以检查进程何时完成:图 8.10 – 灯光生成进度条

图 8.10 – 灯光生成进度条

  1. 现在你可以看到完全黑暗的区域现在有了一点光的效果:

图 8.11 – 带环境光的阴影

图 8.11 – 带环境光的阴影

现在,通过这样做,我们有了更好的照明,但它看起来仍然像是一个晴天。记住,我们想要有雨天的天气。为了做到这一点,我们需要改变默认的天空,使其多云。你可以通过下载一个天空盒来实现。你可以在场景周围看到的当前天空只是一个包含每一面纹理的大立方体,这些纹理具有特殊的投影,以防止我们检测到立方体的边缘。我们可以为立方体的每一面下载六张图像并应用它们,以获得任何我们想要的天空,所以让我们这样做:

  1. 你可以从任何你想要的地方下载天空盒纹理,但在这里,我会选择资产商店。通过窗口 | 资产商店 打开它,并转到资产商店网站。

  2. 在右侧的类别列表中查找2D | 纹理和材质 | 天空。请记住,如果看不到类别列表,需要扩大窗口宽度:图 8.12 – 天空盒类别

图 8.12 – 天空盒类别

  1. 记得在定价部分勾选免费资产复选框:图 8.13 – 免费资产过滤

图 8.13 – 免费资产过滤

  1. 选择任何你喜欢的天空盒来模拟雨天。请注意,天空盒有不同的格式。我们使用的是六图格式,所以在下载之前要检查一下。在我的例子中,我选择了下图中显示的天空盒包。下载并导入它,就像我们在第五章**,导入和整合资源中所做的那样:图 8.14 – 为本书选择的天空盒套装

图 8.14 – 为本书选择的天空盒套装

  1. 通过在Project窗口中使用**+图标并选择Material**来创建一个新的材质。

  2. 将该材质的Shader选项设置为Skybox/6 sided。记住,天空盒只是一个立方体,所以我们可以应用一个材质来改变它的外观。天空盒着色器已经准备好应用这六个纹理。

  3. 将六个纹理拖到材质的FrontBackLeftRightUpDown属性中。这六个下载的纹理将有描述性的名称,这样你就知道哪些纹理应该放在哪里:图 8.15 – 天空盒材质设置

图 8.15 – 天空盒材质设置

  1. 将材质直接拖到场景视图中的天空中。确保你不要把材质拖到一个物体上,因为材质会被应用到它上面。

  2. 重复环境光计算的步骤 14Lighting Settings | Generate Lighting)以根据新的天空盒重新计算。在下图中,你可以看到目前我的项目的结果:

图 8.16 – 应用的天空盒

图 8.16 – 应用的天空盒

现在我们有了一个良好的基础光照层,我们可以开始添加光源对象了。

在 URP 中配置光照

我们可以在场景中添加三种主要类型的直射光:

  • Directional Light:这是代表太阳的光。这个对象会向着它所面对的方向发出光线,而不受位置的影响;太阳向右移动 100 米不会有太大的影响。举个例子,如果你慢慢旋转这个对象,你可以生成一个昼夜循环:

图 8.17 – 定向光结果

图 8.17 – 定向光结果

  • 点光源:这种光代表了一个发射光线的灯泡,以全向方式发出光线。它对太阳的影响与太阳不同,因为它的位置很重要,因为它更接近。此外,因为它是一个较弱的光源,这种光的强度会根据距离而变化,所以它的效果有一个范围 – 距离光源越远,接收到的强度就越弱:

图 8.18 – 点光结果

图 8.18 – 点光结果

  • Spotlight:这种光代表了光锥,就像手电筒发出的光一样。它的行为类似于点光源,其位置和方向很重要,光强度会随着一定距离的衰减:

图 8.19 – 聚光灯结果

图 8.19 – 聚光灯结果

到目前为止,我们有了一个不错的、多雨的环境光照,但是我们场景中唯一的直射光,定向光,看起来不像这样,所以让我们改变一下:

  1. Hierarchy窗口中选择Directional Light对象,然后查看Inspector窗口。

  2. 点击Colour属性以打开颜色选择器。

  3. 选择深灰色来实现部分被云层遮挡的阳光。

  4. Shadow Type设置为No Shadows。现在我们有了多云的天气,太阳不会投射清晰的阴影,但我们稍后会更多地讨论阴影:

图 8.20 – 没有阴影的柔和定向光

图 8.20 – 没有阴影的柔和定向光

现在场景变暗了,我们可以添加一些灯光来照亮场景,如下所示:

  1. 通过转到GameObject | Light | Spotlight创建一个聚光灯:图 8.21 – 创建聚光灯

图 8.21 – 创建聚光灯

  1. 选择它。然后,在90120中,这将增加锥体的角度。

  2. 设置为50,表示光可以达到 50 米,沿途衰减。

  3. 设置为1000图 8.22 – 聚光灯设置

图 8.22 – 聚光灯设置

  1. 将光源放在基座的一个角落,指向中心:图 8.23 – 聚光灯放置

图 8.23 – 聚光灯放置

  1. 通过选择光源并按下Ctrl + D(Mac 上为command + D)来复制该光源。

  2. 将其放在基座的对角线上:

图 8.24 – 两个聚光灯的效果

图 8.24 – 两个聚光灯的效果

你可以继续向场景中添加光源,但要注意不要走得太远——记住光源的限制。此外,你可以下载一些灯柱放在光源位置,以视觉上证明光的来源。现在我们已经实现了适当的照明,我们可以谈论阴影了。

应用阴影

也许你会认为我们的场景中已经有阴影了,但实际上并没有。物体的较暗区域,也就是不面向光源的区域,没有阴影——它们没有被照亮,这与阴影是完全不同的。在这种情况下,我们指的是从一个物体投射到另一个物体的阴影;例如,玩家在地板上投射的阴影,或者从山上到其他物体的阴影。阴影可以提高我们场景的质量,但计算成本也很高,因此我们有两个选择:不使用阴影(建议用于移动设备等低端设备)或根据我们的游戏和目标设备在性能和质量之间找到平衡。在第一种情况下,你可以跳过整个部分,但如果你想要实现高性能的阴影,尽可能地继续阅读。

在本节中,我们将讨论有关阴影的以下主题:

  • 理解阴影计算

  • 配置高性能阴影

让我们先讨论 Unity 如何计算阴影。

理解阴影计算

在游戏开发中,众所周知,阴影在性能方面是昂贵的,但为什么呢?当光线射到另一个物体后再到达物体时,物体会产生阴影。在这种情况下,该像素不会受到来自该光源的照明。问题在于,这与环境光模拟的光照存在相同的问题——计算所有可能的光线及其碰撞将成本过高。因此,我们需要一个近似值,这就是阴影贴图发挥作用的地方。

阴影贴图是从光的视角渲染的图像,但不是绘制带有所有颜色和光照计算的完整场景,而是以灰度渲染所有物体,其中黑色表示像素距离摄像机很远,白色表示像素距离摄像机较近。如果你仔细想一想,每个像素都包含了光线的碰撞信息。通过了解光的位置和方向,你可以使用阴影贴图计算出每个“光线”碰撞的位置。在下图中,你可以看到我们定向光的阴影贴图:

图 8.25 – 我们场景中定向光生成的阴影贴图

图 8.25 – 我们场景中定向光生成的阴影贴图

每种类型的光都会稍微不同地计算阴影贴图,尤其是点光源。由于它是全向的,它需要在所有方向(前、后、上、下、左、右)渲染场景多次,以收集关于它发射的所有光线的信息。不过,我们不会在这里详细讨论这个问题,因为我们可能会谈论一整天。

现在,这里需要强调的一点是,阴影图是纹理,因此它们有分辨率。分辨率越高,我们的阴影图计算的“光线”就越多。您可能想知道当低分辨率阴影图中只有少量光线时会是什么样子。看看下图,看看一个低分辨率阴影图是什么样子:

图 8.26 - 使用低分辨率阴影图渲染的硬阴影

图 8.26 - 使用低分辨率阴影图渲染的硬阴影

问题在于光线数量较少会生成更大的阴影像素,导致像素化阴影。在这里,我们有第一个要考虑的配置:我们的阴影的理想分辨率是多少?您可能会诱惑地增加它,直到阴影看起来平滑,但当然,这将增加计算所需的时间,因此除非您的目标平台可以处理它(移动设备肯定不能),否则它将大大影响性能。在这里,我们可以使用软阴影技巧,在阴影上应用模糊效果以隐藏像素化的边缘,如下图所示:

图 8.27 - 使用低分辨率阴影图渲染的软阴影

图 8.27 - 使用低分辨率阴影图渲染的软阴影

当然,模糊效果并不是免费的,但是如果您接受其模糊结果,并将其与低分辨率阴影图结合使用,可以在质量和性能之间达到良好的平衡。

现在,低分辨率阴影图还有另一个问题,称为阴影痤疮。这是您可以在下图中看到的照明错误:

图 8.28 - 低分辨率阴影图的阴影痤疮

图 8.28 - 低分辨率阴影图的阴影痤疮

低分辨率阴影图会产生假阳性,因为计算的“光线”较少。需要在光线之间进行插值以对光线进行着色的像素需要从最近的光线中插值信息。阴影图的分辨率越低,光线之间的间隔就越大,这意味着精度越低,假阳性就越多。一种解决方法是增加分辨率,但同样会出现性能问题(一如既往)。我们有一些聪明的解决方案,比如使用深度偏差。可以在下图中看到这种情况的一个例子:

图 8.29 - 两个远处的“光线”之间的假阳性。突出显示的区域认为光线在到达之前就击中了物体。

图 8.29 - 两个远处的“光线”之间的假阳性。突出显示的区域认为光线在到达之前就击中了物体。

深度偏差的概念很简单 - 如此简单,以至于看起来像是一个大的欺骗,实际上确实如此,但游戏开发中充满了这些欺骗!为了防止假阳性,我们“推”光线再多一点,足以使插值光线达到击中表面:

图 8.30 - 具有深度偏差的光线以消除假阳性

图 8.30 - 具有深度偏差的光线以消除假阳性

当然,正如您可能期望的那样,它们不能轻松解决这个问题而没有任何警告。推动深度会在其他区域产生假阴性,如下图所示。看起来立方体在漂浮,但实际上它是与地面接触的 - 假阴性产生了它漂浮的错觉:

图 8.31 - 由于深度偏差导致的假阴性

图 8.31 - 由于深度偏差导致的假阴性

当然,我们有一个对这种情况的反对技巧,称为法线偏差。它仍然推动物体,但沿着它们面对的方向。这有点棘手,所以我们不会在这里详细介绍,但是想法是结合一点深度偏差和另一点法线偏差将减少错误的阳性,但不会完全消除它们。因此,我们需要学会如何与之共存,并通过巧妙地定位物体来隐藏它:

图 8.32-减少假阴性,这是深度和法线偏差相结合的结果

图 8.32-减少假阴性,这是深度和法线偏差相结合的结果

还有其他几个影响阴影图工作方式的方面,其中之一是光范围。光范围越小,阴影覆盖的区域就越小。相同的阴影图分辨率可以为该区域添加更多细节,因此尽量减少光范围。

我可以想象你现在的表情,是的,照明很复杂,我们只是刚刚触及到表面!但保持你的精神!在稍微调整设置后,你会更好地理解它。我们将在下一节中做到这一点。

重要信息

如果您真的对学习阴影系统的内部更多信息感兴趣,我建议您查看阴影级联的概念,这是有关定向光和阴影图生成的高级主题。

配置高性能阴影

因为我们的目标是中端设备,所以我们将尝试在这里实现质量和性能的良好平衡,因此让我们开始仅为聚光灯启用阴影。定向光的阴影不会那么明显,实际上,雨天的天空不会产生清晰的阴影,因此我们将借此借口不计算那些阴影。为了做到这一点,请执行以下操作:

  1. 通过在层次结构中单击它们并同时按下Ctrl(Mac 上的Command)来选择两个点光源。这将确保检查器窗口中所做的任何更改都将应用于两者:图 8.33-选择多个对象

图 8.33-选择多个对象

  1. 检查器窗口中,将阴影类型设置为软阴影。我们将在这里使用低分辨率阴影图:图 8.34-软阴影设置

图 8.34-软阴影设置

  1. 选择定向光并将阴影类型设置为无阴影以防止其投射阴影:图 8.35-无阴影设置

图 8.35-无阴影设置

  1. 创建一个立方体(GameObject | 3D Object | Cube)并将其放在灯光附近,以便我们可以在其上投射阴影进行测试。

现在我们有了一个基本的测试场景,让我们调整阴影图分辨率设置,同时防止阴影痤疮:

  1. 转到编辑 | 项目设置

  2. 在左侧列表中,查找图形并单击它:图 8.36-图形设置

图 8.36-图形设置

在选择此选项后出现的属性中,单击下面的可编写渲染管线设置框中的一个名称。在我的情况下,这是LWRP-HighQuality,但由于您使用的 Unity 版本不同,您的情况可能不同:

图 8.37-当前渲染管线设置

图 8.37-当前渲染管线设置

  1. 这样做将在项目窗口中突出显示一个资产,因此在选择之前,请确保该窗口可见。选择突出显示的资产:图 8.38-突出显示当前管道

图 8.38-突出显示当前管道

  1. 这个资产有几个与 URP 如何处理其渲染相关的图形设置,包括照明和阴影。展开照明部分以显示其设置:图 8.39 – 管道照明设置

图 8.39 – 管道照明设置

  1. Main Light)。如果它的值还不是1024,将其设置为1024

  2. 0.25下,为了尽量减少它们,我们需要在移除阴影痤疮之前尽可能减少它们:图 8.40 – 光影设置

图 8.40 – 光影设置

  1. 这与阴影没有直接关联,但在这里,你可以更改每个对象光限制,以增加或减少可以影响对象的光的数量(不超过八个)。

  2. 如果你之前遵循了阴影级联提示,可以稍微调整级联值,以启用定向光的阴影以观察效果。请记住,这些阴影设置仅适用于定向光。

  3. 将两个灯光的范围设置为 40 米。看看在更改前后阴影的质量如何改善:

图 8.41 – 偏差设置

图 8.41 – 偏差设置

请记住,这些值只适用于我的情况,所以尝试稍微调整这些值,看看结果如何改变 – 你可能会找到更适合你的 PC 的设置。同时,请记住,不使用阴影始终是一个选择,所以在你的游戏运行时低于 FPS 时(并且没有其他性能问题潜伏)时,始终要考虑这一点。

你可能认为这就是我们在照明性能方面所能做的一切,但幸运的是,情况并非如此!我们还有另一个资源可以用来进一步改善,即静态照明。

优化照明

我们之前提到不计算照明对性能有好处,但是不计算灯光,但仍然拥有它们呢?是的,这听起来太美好了,但实际上是可能的(当然,也很棘手)。我们可以使用一种称为静态照明或烘焙的技术,它允许我们计算一次照明并使用缓存的结果。

在本节中,我们将涵盖与静态照明相关的以下概念:

  • 理解静态照明

  • 烘焙光照图

  • 将静态照明应用于动态对象

理解静态照明

这个想法非常简单:只需进行一次照明计算,保存结果,然后使用这些结果,而不是一直计算照明。你可能会想为什么这不是默认的技术。这是因为它有一些限制,其中最大的限制是动态对象。预计算阴影意味着一旦计算出来就不能改变,但如果投射阴影的对象移动了,阴影仍然会在那里,因此需要考虑的主要事情是你不能在移动对象上使用这种技术。相反,你需要为静态对象混合静态烘焙照明,对于动态(移动)对象使用实时照明。此外,需要考虑的是,除了这种技术只适用于静态对象,它也只适用于静态光源。同样,如果光源移动,预先计算的数据就会变得无效。

你需要考虑的另一个限制是,预先计算的数据可能会对内存产生巨大影响。这些数据占用了 RAM 的空间,也许有数百 MB,因此你需要考虑你的目标平台是否有足够的空间。当然,你可以降低预先计算的照明质量以减小数据的大小,但你需要考虑失去的质量是否会过分恶化你的游戏的外观和感觉。就像所有关于优化的选项一样,你需要平衡两个因素:性能和质量。

在我们的过程中有几种预先计算的数据,但最重要的是我们所谓的光照贴图。光照贴图是一种纹理,其中包含场景中所有对象的阴影和光照,因此当 Unity 应用预先计算或烘焙的数据时,它将查看此纹理,以了解静态对象的哪些部分受到照明,哪些部分没有。您可以在以下图中看到光照贴图的示例:

图 8.42 - 左边是没有光照的场景;中间是包含来自该场景的预先计算数据的光照贴图;右边是将光照贴图应用到场景中

图 8.42 - 左边是没有光照的场景;中间是包含来自该场景的预先计算数据的光照贴图;右边是将光照贴图应用到场景中

无论如何,光照贴图也有其自身的好处。烘焙过程在 Unity 中执行,游戏发货给用户之前,因此您可以花费大量时间计算无法在运行时执行的内容,例如改进的准确性、光线反射、角落中的光遮挡以及来自发光对象的光线。然而,这也可能是一个问题。请记住,动态对象仍然需要依赖实时光照,而该光照看起来与静态光照非常不同,因此我们需要对其进行大量调整,以使用户注意不到差异。

现在我们对静态光照有了基本概念,让我们深入了解如何使用它。

烘焙光照贴图

要使用光照贴图,我们需要对 3D 模型进行一些准备工作。记住,网格有UV,其中包含有关将纹理的哪个部分应用于模型的每个部分的信息。有时,为了节省纹理内存,您可以将相同的纹理片段应用于不同的部分。例如,在汽车的纹理中,您不会有四个车轮,只会有一个,您可以将相同的纹理片段应用于所有车轮。问题在于静态光照以相同的方式使用纹理,但在这里,它将应用光照贴图来照亮对象。在车轮的情况下,问题在于如果一个车轮接收阴影,所有车轮都会有阴影,因为所有车轮共享相同的纹理空间。通常的解决方案是在模型中有第二组 UV,其中没有共享纹理空间,仅用于光照贴图。

有时,下载的模型已经准备好进行光照贴图,有时没有,但幸运的是,Unity 在这些情况下为我们提供了帮助。为了确保模型能够正确计算光照贴图,让我们通过以下步骤让 Unity 自动生成光照贴图 UV集:

  1. 项目窗口中选择网格资产(FBX)。

  2. 模型选项卡中,查找底部的生成光照贴图复选框并选中它。

  3. 单击底部的应用按钮:图 8.43 - 生成光照贴图设置

图 8.43 - 生成光照贴图设置

  1. 对每个模型重复此过程。从技术上讲,您只能在烘焙光照贴图后在模型中出现伪影和奇怪结果时才能这样做,但现在,让我们在所有模型中都这样做以防万一。

准备好模型进行光照贴图后,下一步是告诉 Unity 哪些对象不会移动。要做到这一点,按照以下步骤进行:

  1. 选择不会移动的对象。

  2. 检视器窗口的右上角选中静态复选框:图 8.44 - 静态复选框

图 8.44 - 静态复选框

  1. 对每个静态对象重复此过程(对灯光不需要这样做;我们稍后会处理)。

请注意,您可能不希望每个对象,即使是静态的,都被烘焙,因为您烘焙的对象越多,您就需要更多的纹理大小。例如,地形太大,将占用大部分烘焙的大小。通常情况下,这是必要的,但在我们的情况下,聚光灯几乎没有触及地形。在这里,我们有两个选择:将地形保留为动态,或者更好地直接告诉聚光灯不要影响地形,因为一个只受环境光和定向光(不投射阴影)照亮。请记住,这是我们可以做的事情,因为我们的场景类型;然而,在其他情况下,您可能需要在其他情景中使用其他设置。您可以通过以下方式从实时和静态照明计算中排除对象:

  1. 选择要排除的对象。

  2. 检视器窗口中,单击图层下拉菜单,然后单击添加图层图 8.45 – 图层创建按钮

图 8.45 – 图层创建按钮

  1. 在这里,您可以创建一个图层,这是一个用于识别哪些对象不会受到照明影响的对象组。在图层列表中,查找一个空白空间,并键入这些类型对象的任何名称。在我的情况下,我只会排除地形,所以我只是将其命名为地形图 8.46 – 图层列表

图 8.46 – 图层列表

  1. 再次选择地形,转到图层下拉菜单,并选择在上一步中创建的图层。这样,您可以指定该对象属于该组对象:图 8.47 – 更改游戏对象的图层

图 8.47 – 更改游戏对象的图层

  1. 选择所有聚光灯,查找检视器窗口中的剔除蒙版,单击它,并取消选中之前创建的图层。这样,您可以指定这些灯不会影响该组对象:图 8.48 – 光照剔除蒙版

图 8.48 – 光照剔除蒙版

  1. 现在,您可以看到那些选定的灯不会照亮或对地形应用阴影。

现在,是时候处理灯光了,因为静态复选框对它们不起作用。对于它们,我们有以下三种模式:

  • 实时:实时模式下的光会影响所有对象,包括静态和动态对象,使用实时照明,这意味着没有预先计算。这对于不是静态的灯光非常有用,比如玩家的手电筒,因为风而移动的灯等等。

  • 烘焙:与实时相反,这种类型的光只会影响具有光照贴图的静态对象。这意味着如果玩家(动态)在街道上的烘焙光下移动,街道看起来会被照亮,但玩家仍然会很暗,并且不会在街道上投下任何阴影。这个想法是在不影响任何动态对象的灯光上使用它,或者在它们上几乎不可察觉的灯光上使用它,这样我们就可以通过不计算它们来提高性能。

  • 混合:如果不确定要使用哪种模式,则这是首选模式。这种类型的光会为静态对象计算光照贴图,但也会影响动态对象,将其实时照明与烘焙照明结合在一起(就像实时光也会做的那样)。

在我们的情况下,我们的定向光只会影响地形,而且因为我们没有阴影,在 URP 中应用照明相对便宜,所以我们可以将定向光保留在实时模式,这样它就不会占用任何光照贴图区域。我们的聚光灯影响了基地,但实际上,它们只是对其应用照明 - 我们没有阴影,因为我们的基地是空的。在这种情况下,最好根本不计算光照贴图,但出于学习目的,我将添加一些障碍物作为基地的阴影,并证明使用光照贴图,如下图所示:

图 8.49 – 添加对象以投射光线

图 8.49 – 向项目添加对象以投射光线

在这里,您可以看到我们的关卡原始设计在游戏开发过程中不断变化,这是您无法避免的事情 - 游戏的更大部分将随时间改变。现在,我们已经准备好设置光照模式并执行烘焙过程,如下所示:

  1. 选择定向光

  2. 检视器窗口设置为实时(如果尚未处于该模式)。

  3. 选择两个聚光灯。

  4. 将它们的渲染模式设置为混合图 8.50 – 混合光照设置

图 8.50 – 混合光照设置

  1. 打开照明设置窗口(窗口 | 渲染 | 照明设置)。

  2. 单击生成照明,这是我们之前用来生成环境光照的相同按钮。

  3. 等待进程完成。您可以通过检查 Unity 编辑器右下角的进度条来完成此操作。请注意,这个过程可能需要几个小时才能完成,所以请耐心等待:图 8.51 – 烘焙进度条

图 8.51 – 烘焙进度条

  1. 我们想要更改烘焙过程的一些设置。为了启用此控件,单击新照明设置按钮。这将创建一个具有光照设置的资源,可以应用于多个场景,以便我们多次共享相同的设置:图 8.52 – 创建照明设置

图 8.52 – 创建照明设置

  1. 降低光照质量,以加快进程。只需迭代,通过使用光照贴图分辨率直接间接环境样本等设置,可以轻松降低照明。在我的情况下,我已经应用了这些设置,如下图所示。请注意,即使减少这些设置也需要时间;由于模块化关卡设计,我们的场景中有太多对象:图 8.53 – 场景光照设置

图 8.53 – 场景光照设置

  1. 进程完成后,您可以检查照明设置窗口的底部,您可以看到需要生成多少个光照贴图。我们有最大光照贴图分辨率,所以我们可能需要生成几个光照贴图来覆盖整个场景。此外,它还告诉我们它们的大小,以便我们可以考虑它们对 RAM 的影响。最后,您可以查看烘焙光照贴图部分来查看它们:图 8.54 – 生成的光照贴图

图 8.54 – 生成的光照贴图

  1. 现在,根据结果,您可以移动对象,修改光强度,或者进行任何您需要的修正,以使场景看起来符合您的要求,并在需要时重新计算照明。在我的情况下,这些设置给我带来了足够好的结果,您可以在下图中看到:

图 8.55 – 光照贴图结果

图 8.55 – 光照贴图结果

我们仍有许多小设置要处理,但我会让您通过试错或阅读 Unity 关于光照贴图的文档来发现这些设置。阅读 Unity 手册是获取知识的好途径,我建议您开始使用它 - 任何经验丰富的好开发人员都应该阅读手册。

将静态光照应用于静态对象

当在场景中将对象标记为静态时,您可能已经发现场景中的所有对象都不会移动,因此您可能已经为每个对象都勾选了静态复选框。这没问题,但您应该始终将一个动态对象放入场景中,以确保一切正常 - 没有游戏完全静态。尝试添加一个胶囊体并将其移动以模拟我们的玩家,如下图所示。如果您留意,您会注意到一些奇怪的事情 - 光照贴图过程生成的阴影未应用于我们的动态对象:

图 8.56 - 动态物体在烘焙阴影下

图 8.56 - 动态物体在烘焙阴影下

你可能会认为混合光模式应该影响动态和静态物体,这正是它所做的。问题在于,与静态物体相关的所有内容都预先计算到那些光照图纹理中,包括它们投射的阴影,因为我们的胶囊是动态的,在预计算过程执行时并不存在。所以,在这种情况下,因为投射阴影的对象是静态的,它的阴影不会影响任何动态物体。

在这里,我们有几种解决方案。第一种是改变静态和实时混合算法,使相机附近的所有东西都使用实时照明,并防止这个问题(至少在玩家的注意焦点附近),这对性能会有很大影响。另一种选择是使用光探头。当我们烘焙信息时,我们只在光照图上做了这个,这意味着我们只有表面上的光照信息,而不是空白空间中的光照信息。因为我们的玩家正在穿越这些表面之间的空白空间,我们不知道这些空间的光照会是什么样子,比如走廊中间。光探头是在这些空白空间中的一组点,Unity 也会预先计算信息,所以当一些动态物体经过时,它会从中采样信息。在下图中,你可以看到一些应用到我们场景中的光探头。你会注意到那些在阴影中的光探头会变暗,而那些暴露在光线下的光探头会有更大的强度。这种效果将应用到我们的动态物体上:

图 8.57 - 代表光探头的球体

图 8.57 - 代表光探头的球体

如果你现在在场景中移动你的物体,它将对阴影做出反应,就像下面两张图片中所示,你可以看到一个动态物体在烘焙阴影外被照亮,而在内部变暗:

图 8.58 - 动态物体接收来自光探头的烘焙照明

图 8.58 - 动态物体接收来自光探头的烘焙照明

为了创建光探头,进行以下操作:

  1. 通过转到GameObject | Light | Light Probe Group来创建一组探头:图 8.59 - 创建光探头组

图 8.59 - 创建光探头组

  1. 幸运的是,我们有一些关于如何定位它们的指导方针。建议将它们放在光照变化的地方,比如在内部和外部阴影边界。然而,这相当复杂。最简单和推荐的方法是在可玩区域上放置一个光探头网格。为此,你可以简单地多次复制和粘贴光网格组,以覆盖整个基地:图 8.60 - 光探头网格

图 8.60 - 光探头网格

  1. 另一种方法是选择一组并点击编辑光探头按钮进入光探头编辑模式:图 8.61 - 光探头组编辑按钮

图 8.61 - 光探头组编辑按钮

  1. 点击全选按钮,然后点击复制所选按钮,复制所有先前存在的探针。

  2. 使用平移小工具,将它们移动到先前的位置旁边,同时扩展网格。请考虑到探针越近,你需要更多的探针来覆盖地形,这将产生更多的数据。然而,光探头数据相对便宜,所以你可以有很多。

  3. 重复步骤 45,直到覆盖整个区域。

  4. 使用光照设置中的生成照明按钮重新生成照明。

有了这个,你就预先计算了影响我们动态物体的光探头上的照明,将两个世界结合起来,获得了连贯的照明。

总结

在本章中,我们讨论了几个照明主题,比如 Unity 如何计算光线、阴影,如何处理不同的光源,比如直接和间接照明,如何配置阴影,如何烘焙照明以优化性能,以及如何结合动态和静态照明,使光线不脱离影响其所在世界的环境。这是一个很长的章节,但照明值得如此。这是一个复杂的主题,可以显著改善场景的外观和感觉,同时大大降低性能。这需要大量的实践,我们在这里试图总结出你开始尝试的所有重要知识。对这个主题要有耐心;很容易得到不正确的结果,但你可能只差一个复选框就能解决问题。

现在我们已经在场景设置中做了所有可以改进的事情,在下一章中,我们将使用 Unity 后期处理堆栈应用最终的图形效果,这将应用全屏图像效果-这些效果将给我们带来当今所有游戏都具有的电影般的外观和感觉。

第九章:使用后期处理的全屏效果

到目前为止,我们已经创建了不同的对象来改变场景的视觉效果,例如网格、粒子和灯光。我们可以在这里和那里调整这些对象的设置,以改善我们的场景质量,但是当与现代游戏场景进行比较时,您总会感到缺少某些东西,即全屏或后期处理效果。在本章中,您将学习如何将效果应用于最终渲染的帧,这将改变整个场景的外观。

使用后期处理

后期处理是 Unity 的一个功能,允许我们应用多种效果(一堆效果)叠加在一起,这将改变图像的最终外观。每个效果都会影响完成的帧,根据不同的标准改变其中的颜色。在以下截图中,您可以看到应用图像效果之前和之后的场景。您会注意到明显的差异,但是该场景的对象,包括灯光、粒子或网格,都没有任何变化。应用的效果是基于像素分析的。在这里看看两个场景:

图 9.1 没有图像效果的场景(左)和具有效果的相同场景(右)

图 9.1-没有图像效果的场景(左)和具有效果的相同场景(右)

需要考虑的是,以前的后期处理解决方案postprocessing Stack version 2PPv2)在Universal Render PipelineURP)上无法使用;它有自己的后期处理实现,因此我们将在本章中看到它。无论如何,它们非常相似,因此即使您使用 PPv2,您仍然可以从本章中获得一些东西。

在本节中,我们将讨论以下 URP 后期处理概念:

  • 设置配置文件

  • 使用基本效果

让我们开始准备我们的场景应用效果。

设置配置文件

要开始应用效果,我们需要创建一个Profile,它是一个包含我们想要应用的所有效果和设置的资产。出于与材质相同的原因,这是一个单独的资产,因为我们可以在不同的场景和场景部分之间共享相同的后期处理配置文件。当我们提到场景的部分时,我们指的是应用了某些效果的体积或游戏区域。我们可以定义一个全局区域,无论玩家的位置如何都会应用效果,或者我们可以应用不同的效果-例如,当我们在室外或室内时。

在这种情况下,我们将使用全局体积,我们将使用它来应用我们的第一个效果配置文件,方法如下:

  1. 创建一个新的空游戏对象(GameObject | Create Empty)。

  2. 将其命名为PP Volume(表示后期处理体积)。

  3. Volume组件添加到其中。

  4. 确保Mode设置为Global

  5. 单击Profile设置右侧的New按钮,这将生成一个名为我们对象的新配置文件资产(PPVolume Profile)。您可以稍后将其移动到自己的文件夹中,这是为了资产组织目的而推荐的。该过程如下截图所示:图 9.2 体积组件

图 9.2-体积组件

  1. 要测试体积是否起作用,让我们添加一个效果。单击Add Override按钮,然后选择postprocessing | Chromatic Aberration选项。

  2. 检查0.5,如下截图所示:![图 9.3 色差效果使用后期处理图 9.3-色差效果 1. 现在,您将看到图像的角落应用了一种像差效果。请记住在场景面板中查看这一点;我们将在下一步中将效果应用于游戏视图。这在以下截图中有所说明:图 9.4 应用于场景的色差

图 9.4 - 应用到场景中的色差

  1. 现在,如果你点击Main Camera,你会发现效果没有被应用,这是因为我们需要勾选Main Camera,如下面的截图所示:

图 9.5 启用后期处理

图 9.5 - 启用后期处理

因此,我们创建了一个全局体积,它将将指定的效果作为覆盖应用到整个场景,而不管玩家的位置如何。

现在我们已经准备好使用后期处理来准备我们的场景,我们可以开始尝试不同的效果。让我们从下一节中最简单的效果开始。

使用基本效果

现在我们在场景中有了后期处理,唯一需要做的就是开始添加效果并设置它们,直到我们得到期望的外观和感觉。为了做到这一点,让我们探索系统中包含的几种简单效果。

让我们从色差开始,这是我们刚刚使用的效果,与大多数图像效果一样,它试图复制特定的真实效果。所有游戏引擎渲染系统都使用了眼睛视觉真实工作的简单数学近似,因此我们没有一些发生在人眼或相机镜头中的效果。真实的相机镜头通过弯曲光线来将其指向相机传感器,但在一些镜头中(有时是故意的),这种弯曲并不完美,因此你会看到一些失真,如下面的截图所示:

图 9.6 没有色差的图像(左)和有色差的相同图像(右)

图 9.6 - 没有色差的图像(左)和有色差的相同图像(右)

这个效果将是我们添加的几个效果之一,以在游戏中产生一种电影感,模拟真实相机的使用。当然,这种效果并不适合每种类型的游戏;也许简单的卡通风格不会从中受益,但你永远不知道:艺术是主观的,所以这是一个试错的过程。

此外,我们在上一个例子中夸大了强度,以使效果更加明显,但我建议在这种情况下使用强度为 0.25。通常建议对效果的强度要温和;强烈的效果很诱人,但当你添加了很多效果之后,图像会变得臃肿,扭曲太多。因此,尽量添加一些微妙的效果,而不是少量强烈的效果。但是,这取决于你所追求的目标风格;在这里没有绝对的真理(但常识仍然适用)。

最后,在讨论其他效果之前,如果你习惯使用其他类型的后期处理效果框架,你会注意到这个版本的色差设置较少,这是因为 URP 版本追求性能,所以尽可能简单。

接下来我们要讨论的效果是晕影。这是另一个相机镜头的缺陷,图像强度在镜头边缘会丢失。这不仅可以用来模拟旧相机,还可以吸引用户的注意力集中在相机的中心,比如在电影中。此外,如果你正在开发虚拟现实VR)应用程序,这可以通过减少玩家的外围视觉来减轻晕动病。在下面的截图中,你可以看到一个旧相机上晕影的例子:

图 9.7 使用旧相机拍摄的照片,边缘有晕影

图 9.7 - 使用旧相机拍摄的照片,边缘有晕影

只是试试,让我们通过以下方式向我们的场景应用一些晕影:

  1. 选择PP Volume游戏对象。

  2. 通过点击添加覆盖按钮添加后期处理 | 晕影效果。

  3. 检查0.3,增加效果。

  4. 检查0.5;这将增加效果的扩散。您可以在下面的截图中看到结果:

图 9.8 晕影效果

图 9.8 – 晕影效果

如果您愿意,您可以通过勾选CenterRounded,以Particles的方式工作。您可以通过调整数值来创建漂亮的效果。

我们将在这个基础部分中审查的另一个效果是运动模糊,再次模拟相机的工作方式。相机有一个曝光时间,它需要捕捉光子以获得每一帧。当一个物体移动得足够快时,在那短暂的曝光时间内,同一个物体会处于不同的位置,因此它会显得模糊不清。在下面的截图中,您可以看到该效果应用到我们的场景中。在这张图片中,我们快速上下旋转相机,得到以下结果:

图 9.9 将运动模糊应用到我们的场景中

图 9.9 将运动模糊应用到我们的场景中

需要考虑的一件事是,这种模糊只会应用于相机的移动,而不是物体的移动(静止相机,移动物体),因为 URP 目前不支持运动矢量。

要使用此效果,请按照以下步骤进行:

  1. 使用Post-processing | Motion Blur覆盖,点击Add override按钮。

  2. 检查0.5

  3. 在查看游戏视图时旋转相机(而不是场景视图)。您可以单击并拖动相机的TransformX属性(不是值,而是X标签),如下面的截图所示:

图 9.10 改变旋转

图 9.10 – 改变旋转

正如您所看到的,这种效果在场景视图中是看不到的,其他效果也是如此,因此在得出效果不起作用的结论之前,请考虑这一点。Unity 之所以这样做,是因为在场景中工作时,拥有这种效果会非常恼人。

最后,我们将简要讨论两个最终简单的效果,胶片颗粒白平衡。第一个非常简单:添加它,将强度设置为 1,您将得到老电影中著名的颗粒效果。您可以通过不同大小的Type来使其更加微妙或明显。白平衡允许您改变色温,根据您的配置使颜色变得更温暖或更凉爽。在我们的情况下,我们正在处理一个寒冷的黑暗场景,因此您可以添加它并将温度设置为-20,稍微调整外观,改善这种场景的外观和感觉。

既然我们已经看到了一些简单的效果,让我们来看看剩下的一些受一些高级渲染特性影响的效果。

使用高级效果

我们将在本节中看到的效果与之前的效果并没有太大的区别;它们只是有点棘手,需要一些背景知识才能正确使用它们。所以,让我们深入了解它们!

在本节中,我们将看到高级效果概念

高动态范围HDR)和深度图。

高级效果

让我们首先讨论一些这些效果正常工作所需的要求。

HDR 和深度图

有些效果不仅适用于渲染图像,还需要额外的数据。我们首先讨论深度图,这是我们在上一章中已经讨论过的概念。简而言之,深度图是从相机的视角渲染的图像,但它不是生成场景的最终图像,而是渲染场景对象的深度,以灰度渲染对象。颜色越深,像素距离相机越远,反之亦然。在下面的截图中,您可以看到深度图的一个示例:

图 9.11 – 几何图形的深度图

图 9.11 – 几何图形的深度图

我们将看到一些效果,比如景深,它会根据相机的距离模糊图像的某些部分,但它可以用于自定义效果的几个目的(不在基本 URP 包中)。

这里要讨论的另一个概念会改变颜色的处理方式,因此也会改变一些效果的工作方式,那就是 HDR。在旧的硬件中,颜色通道(红色、绿色和蓝色)被编码在 0 到 1 的范围内,0 表示没有强度,1 表示完全强度(每个通道),因此所有照明和颜色计算都是在该范围内进行的。这似乎没问题,但并不反映光的实际工作方式。您可以看到一张纸被阳光照射时呈现全白(所有通道设置为 1),当您直接看灯泡时也会看到全白,但即使光和纸都是相同的颜色,后者首先会在一段时间后刺激眼睛,其次,由于光线过多,会有一些过亮。问题在于最大值(1)不足以表示最强烈的颜色,因此,如果您有一个高强度的光和另一个甚至更高强度的光,由于计算无法超过 1,两者都将生成相同的颜色(每个通道中的 1)。这就是为什么创建了HDR 渲染

HDR 是一种使颜色超出 0.1 范围的方式,因此基于颜色强度工作的照明和效果在此模式下具有更好的准确性。这与具有相同名称的新电视功能的想法相同,尽管在这种情况下,Unity 将以 HDR 进行计算,但最终图像仍将使用先前的颜色空间(0 到 1,或低动态范围(LDR)),因此不要将 Unity 的HDR 渲染显示的 HDR混淆。要将 HDR 计算转换回 LDR,Unity(以及电视)使用了一个称为色调映射的概念。您可以在以下屏幕截图中看到一个 LDR 渲染的场景和色调映射在 HDR 场景中的应用示例:

图 9.12 左边是 LDR 渲染的场景,右边是使用色调映射校正过亮的 HDR 场景

图 9.12-左边是 LDR 渲染的场景,右边是使用色调映射校正过亮的 HDR 场景

色调映射是一种将颜色从 0.1 范围之外带回到其中的方法。它基本上使用一些公式和曲线来确定如何映射每个颜色通道。您可以在典型的从暗到亮的场景转换中清楚地看到这一点,比如当您走出没有窗户的建筑物,走到明亮的一天。有一段时间,您会看到一切变得更亮,直到一切恢复正常。这里的想法是,当您在建筑物内外时,计算并不不同;建筑物内的白墙将具有接近 1 强度的颜色,而外面的同样白墙将具有更高的值(由于阳光)。不同之处在于,当您在建筑物外时,色调映射将把高于 1 的颜色带回到 1,并且根据您的设置,如果整个场景较暗,可能会增加建筑物内墙壁的照明。

即使 HDR 默认启用,让我们看看如何通过以下方式检查:

  1. 转到编辑 | 项目设置

  2. 单击左侧面板中的图形设置部分。

  3. 单击脚本渲染管线设置属性下引用的资产。

  4. 单击项目面板中突出显示的资产。在单击图形设置中的属性之前,请确保此面板可见。

  5. 质量部分,确保HDR已被选中,如下面的屏幕截图所示:

图 9.13 启用 HDR

图 9.13-启用 HDR

当然,HDR 是可切换的,这意味着有些情况下您可能不想使用它。正如您可以猜到的,不是所有的硬件都支持 HDR,并且使用它会带来性能开销,所以请考虑这一点。幸运的是,大多数效果都适用于 HDR 和 LDR 颜色范围,因此如果您启用了 HDR 但用户设备不支持它,您不会遇到任何错误,只是会得到不同的结果。

既然我们确定已启用 HDR,让我们探索一些使用这个和深度映射的高级效果。

让我们看看一些使用先前描述的技术的特定效果,首先是常用的 Bloom。这种效果通常模拟相机镜头或甚至人眼周围发生的强烈照明物体的过度发光。在下面的截图中,您可以看到我们场景的默认版本和夸张的 Bloom 版本之间的差异。您可以观察到效果只应用于我们场景最明亮的区域。在这里看看这两种效果:

图 9.14 默认场景(左)和相同场景的高强度 Bloom(右)

图 9.14 - 默认场景(左)和相同场景的高强度 Bloom(右)

这种效果实际上非常普遍和简单,但我认为它是高级的,因为结果受到 HDR 的影响很大。这种效果依赖于计算每个像素的颜色强度,以便检测可以应用它的区域。在 LDR 中,我们可能有一个白色的物体,实际上并不是过亮的,但由于这种颜色范围的限制,Bloom 可能会在其上产生过度发光。在 HDR 中,由于其增加的颜色范围,我们可以检测物体是否是白色,或者物体可能是浅蓝色但只是过亮,产生了它是白色的错觉(比如在高强度灯附近的物体)。在下面的截图中,您可以看到我们的场景在启用 HDR 和未启用 HDR 时的区别。您会注意到 LDR 版本会在不一定是过亮的区域产生过度发光。差异可能非常微妙,但请注意细节以注意到差异。记住,我在这里夸大了效果。在这里看看两个场景:

图 9.15 - LDR 场景中的 Bloom(左)和 HDR 场景中的 Bloom(右)。

图 9.15 - LDR 场景中的 Bloom(左)和 HDR 场景中的 Bloom(右)。请注意,Bloom 设置已更改,以尽量接近它们

现在,让我们继续使用场景的 HDR 版本。为了启用 Bloom,执行以下操作:

  1. 像往常一样,将Bloom覆盖添加到配置文件中。

  2. 启用1.5。这控制着将应用多少过度发光。

  3. 启用0.7。这个值表示颜色需要具有的最小强度,才能被认为是过度发光。在我们的情况下,我们的场景有点暗,所以我们需要在 Bloom 效果设置中减少这个值,以包括更多的像素。通常情况下,这些值需要根据您的具体情况进行调整。

  4. 您会注意到差异非常微妙,但再次记住,您将有几种效果,所以所有这些小差异将累积起来。您可以在以下截图中看到这两种效果:

图 9.16 - Bloom 效果

图 9.16 - Bloom 效果

像往常一样,建议您调整其他值。我建议您测试一些有趣的设置,比如Dirt TextureDirt Intensity值。

现在,让我们转移到另一个常见的效果,景深。这个效果依赖于我们之前讨论过的深度图。肉眼并不那么明显,但当你专注于视野内的一个物体时,周围的物体会变得模糊,因为它们失焦了。我们可以利用这一点来在游戏玩法的关键时刻引起玩家的注意。这个效果将对深度图进行采样,以查看物体是否在焦点范围内;如果是,就不会应用模糊效果,反之亦然。为了使用它,做如下操作:

  1. 这个效果取决于你的游戏摄像机定位。在这种情况下,我们将把摄像机放在柱子附近,以尝试专注于特定物体,如下截图所示:图 9.17 – 摄像机定位

图 9.17 – 摄像机定位

  1. 添加景深覆盖。

  2. 启用并将模式设置为高斯:这是最简单的模式。

  3. 在我的情况下,我设置了1020,这将使效果从目标物体后面的一定距离开始。结束设置将控制模糊的强度增加,达到最大值时距离为 20 米。记得根据你的情况调整这些值。

  4. 如果你想稍微夸张效果,设置为1.5。结果如下截图所示:

图 9.18 夸张效果

图 9.18 – 夸张效果

这里需要考虑的一点是,我们的特定游戏将采用俯视视角,与第一人称摄像机不同,你可以看到远处的物体,而在这里,物体足够近以至于不会注意到效果,所以我们可以将这个效果限制在剧情场景中使用。

现在,剩下的大部分效果都是改变场景实际颜色的不同方式。思路是,真实的颜色有时并不能给你想要的精确外观和感觉。也许你需要让暗区域更暗,以加强恐怖氛围的感觉,或者你想做相反的事情:增加暗区域的亮度,以代表一个开放的场景。也许你想给高光着色一点,以获得霓虹效果,如果你正在创建一个未来主义游戏,或者也许你想暂时使用棕褐色效果,进行一个回忆。我们有无数种方法可以做到这一点,在这种情况下,我将使用一个简单但强大的效果,叫做阴影、中间色调、高光

这个效果将对阴影中间色调高光应用不同的颜色校正,这意味着我们可以分别修改较暗、较亮和中等区域。让我们尝试一下:

  1. 添加阴影、中间色调、高光覆盖。

  2. 让我们开始做一些测试。勾选三个阴影中间色调高光复选框。

  3. 阴影中间色调滑块全部向左移动,将高光的滑块向右移动。这将减少阴影和中间色调的强度,并增加高光的强度。我们这样做是为了让你看到高光会根据其强度改变的区域(这在恐怖游戏中也可能是一个有趣的效果)。你可以用其他滑块做同样的操作来检查其他两个区域。你可以在下面的截图中看到结果:图 9.19 – 高光隔离

图 9.19 – 高光隔离

  1. 此外,你可以尝试移动彩色圆圈中心的白色圆圈,对这些区域进行轻微着色。将滑块稍微向左移动以减少高光的强度,使着色效果更加明显。你可以在下面的截图中看到结果:图 9.20 – 高光着色

图 9.20 – 高光着色

  1. 通过这样做,您可以探索这些控件的工作方式,但当然,这些极端值对于某些边缘情况是有用的。在我们的场景中,您可以在下面的屏幕截图中看到的设置对我来说效果最好。一如既往,最好使用更微妙的值,以不要过度扭曲原始结果,如下所示:图 9.21 – 微妙的变化

图 9.21 – 微妙的变化

  1. 以下是屏幕截图中的前后效果:

图 9.22 – 前后效果

图 9.22 – 前后效果

您还有其他更简单的选项,比如分割调色,它做的事情类似,但只涉及阴影和高光,或者颜色曲线,它可以让您更高级地控制场景的每个颜色通道将如何映射,但其思想是相同的——即改变结果场景的实际颜色,以赋予您的场景特定的色彩氛围。如果您还记得电影系列黑客帝国,当角色在矩阵中时,一切都带有微妙的绿色色调,而在外面时,色调是蓝色的。

请记住,使用 HDR 和不使用它对于这些效果的结果是重要的,因此最好尽早决定是否使用 HDR,排除某些目标平台(这可能对您的目标受众不重要),或者不使用它(使用 LDR)并且对场景的光照水平控制较少。

还要考虑到,也许您需要调整一些对象的设置,比如光强度和材质属性,因为有时我们使用后期处理来修复可能由错误设置的对象引起的图形错误,这是不好的。例如,增加场景中的环境光照会大大改变效果的输出,我们可以利用这一点来增加整体亮度,而不是使用效果,如果我们发现场景太暗。

这涵盖了要使用的主要图像效果。请记住,不是使用每一个效果,而是使用您认为对您的场景有贡献的效果;它们在性能方面并不是免费的(尽管不是那么资源密集),所以要明智地使用它们。此外,您可以查看已创建的配置文件,将它们应用到您的游戏中,看看微小的变化如何产生巨大的影响。

总结

在本章中,我们讨论了在我们的场景中应用的基本和高级全屏效果,使其在相机镜头效果方面看起来更真实,在颜色扭曲方面更时尚。我们还讨论了 HDR 和深度图的内部结构,以及在使用这些效果时它们的重要性,这可以立即提高您游戏的图形质量,而付出的努力却很少。

现在我们已经涵盖了 Unity 系统中常见的大部分图形,让我们开始看看如何通过声音增强我们场景的沉浸感。

第十章:声音和音乐集成

我们刚刚达到了足够好的图形质量,但我们缺少游戏美学的一个重要部分:声音。声音经常被排在游戏开发的最后一步,但它是那种如果存在,你不会注意到它的存在,但如果你没有它,你会感觉到缺少了什么。它将帮助你加强你在游戏中想要的氛围,并且必须与图形设置相匹配。

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

  • 导入音频

  • 集成和混合音频

导入音频

与图形资产一样,正确设置音频资产的导入设置非常重要,如果不正确的话可能会消耗大量资源。

在本节中,我们将讨论以下音频导入概念:

  • 音频类型

  • 配置导入设置

让我们开始讨论我们可以使用的不同类型的音频。

音频类型

视频游戏中存在不同类型的音频,包括以下内容:

  • 音乐:用于根据情况增强玩家体验的音乐。

  • 音效(SFX):作为对玩家或 NPC 行为的反应发生的声音,例如点击按钮、行走、打开门和开枪。

  • 环境声音:一个只有作为事件反应的声音的游戏会感觉空荡。如果你正在重建城市中的公寓,即使玩家只是闲置在房间中什么也不做,应该听到很多声音,大部分声音的来源都在房间外,比如飞机在头顶飞过,两个街区外的建筑工地,和街上的汽车。创建看不见的对象是资源的浪费。相反,我们可以在整个场景中放置单独的声音来重新创建所需的氛围,但这将消耗大量的 CPU 和 RAM 来实现可信的结果。考虑到这些声音通常是用户注意力的第二个平面,我们可以将它们全部合并成一个循环轨道,只播放一个音频,这正是环境声音。如果你想创建一个咖啡馆场景,你可以简单地去一个真正的咖啡馆录制几分钟的音频,将其用作你的环境声音。

对于几乎所有的游戏,我们至少需要一条音乐曲目,一条环境曲目和几个 SFX 来开始音频的制作。和往常一样,我们有不同的音频资产来源,但我们将使用资产商店。它有三个音频类别,可以搜索到我们需要的资产:

图 10.1 - 资产商店中的音频类别

图 10.1 - 资产商店中的音频类别

在我的情况下,我还使用了搜索栏来进一步过滤类别,搜索天气以找到雨的效果。有时,你无法单独找到确切的音频;在这种情况下,你需要深入包和库,所以在这里要有耐心。在我的情况下,我选择了你可以在下图中看到的三个包,但是导入其中一些包含的声音,所有这些声音在项目中都会占用很大的空间。对于环境声音,我选择了雨。然后,我选择了音乐 - 伤感希望作为音乐,对于 SFX,我选择了一个枪声音效包,用于我们未来的玩家英雄角色。当然,你可以选择其他包以更好地满足你的游戏需求:

图 10.2 - 我们游戏的包

图 10.2 - 我们游戏的包

请记住,当你阅读这篇文章时,这些确切的包可能不可用。在这种情况下,你可以下载其他包,或者从 GitHub 仓库中选择我使用的文件。现在我们有了必要的音频包,让我们讨论如何导入它们。

配置导入设置

我们有几个可以调整的导入设置,但问题是我们需要考虑音频的使用情况才能正确设置它,所以让我们看看每种情况的理想设置。要查看导入设置,像往常一样,您可以选择资产并在检查器面板中查看它,如下面的截图所示:

图 10.3 - 音频导入设置

图 10.3 - 音频导入设置

让我们讨论最重要的几个,首先是强制转换为单声道。一些音频可能带有立体声声道,这意味着我们左耳和右耳分别有一个声音。这意味着一段音频实际上可以包含两个不同的音轨。立体声音对于不同的效果和乐器空间化在音乐的情况下是有用的,所以在这些情况下我们希望有这种效果,但也有其他情况下立体声并不有用。考虑 3D 音效,比如射击枪声或步行声音。在这些情况下,我们需要听到声音来自源头的方向。如果枪声发生在我的左边,我需要听到它来自我的左边。在这些情况下,我们可以通过在音频导入设置中勾选强制转换为单声道复选框来将立体声音转换为单声道音频。这将使 Unity 将两个声道合并为一个声道,将音频的大小通常减少到几乎一半(有时更多,有时更少,这取决于各种因素)。

您可以在音频资产检查器底部验证该设置和其他设置的影响,您可以在那里看到导入的音频大小:

图 10.4 - 左:未强制转换为单声道的音频。右:相同的音频强制转换为单声道

图 10.4 - 左:未强制转换为单声道的音频。右:相同的音频强制转换为单声道

接下来要讨论的设置是加载类型,这是一个重要的设置。为了播放一些音频,Unity 需要从磁盘读取音频,解压缩,然后播放。加载类型改变了这三个过程的处理方式。我们在这里有以下三个选项:

  • 加载时解压缩:最占用内存的选项。这种模式将使 Unity 在场景加载时将音频解压缩到内存中。这意味着音频将占用大量 RAM 空间,因为我们已经加载了未压缩的版本。使用这种模式的优势在于播放音频更容易,因为我们已经准备好在 RAM 中播放原始音频数据。

  • 流式传输:与加载时解压缩完全相反。这种模式从不在 RAM 中加载音频。相反,当音频播放时,Unity 会从磁盘读取音频资产的一部分,解压缩它,播放它,然后重复这个过程,对于每个正在流式传输播放的音频部分运行一次。这意味着这种模式将会占用大量 CPU 资源,但几乎不会消耗 RAM 字节。

  • 内存中的压缩:中间地带。这种模式将在场景加载时从磁盘加载音频,但将保持其在内存中的压缩状态。当 Unity 需要播放音频时,它只会从 RAM 中取一部分,解压缩并播放。请记住,从 RAM 中读取音频资产的部分比从磁盘读取要快得多。

也许如果您是一位经验丰富的开发者,您可以轻松确定哪种模式更适合哪种类型的音频,但如果这是您第一次接触视频游戏,可能会感到困惑,所以让我们讨论不同情况下的最佳模式:

  • 频繁的短音频:这可能是射击枪声或脚步声等持续时间不到 1 秒的声音,但可能会在多个实例中发生并同时播放。在这种情况下,我们可以使用加载时解压缩。未压缩的短音频与其压缩版本的大小差异不大。而且,由于这是性能最佳的 CPU 选项,有多个实例不会对性能产生巨大影响。

  • 不经常的大型音频:这包括音乐、环境声音和对话。这些类型的音频通常只有一个实例在播放,而且它们通常很大。这些情况更适合于流媒体模式,因为在低端设备(如移动设备)中对它们进行压缩或解压缩可能会产生巨大影响(在 PC 上,有时我们可以使用内存中的压缩)。CPU 可以处理两三个音频位在流媒体中播放,但尽量不要超过这个数量。

  • 频繁的中等音频:这包括多人游戏中预制的语音对话、角色表情、长时间的爆炸声音,或者任何超过 500KB 的音频(这不是一个严格的规则——这个数字在很大程度上取决于目标设备)。将这种类型的音频解压缩到 RAM 中可能会对性能产生明显影响,但由于这种音频相对较小,通常不会对我们的游戏产生巨大影响,并且我们将避免浪费 CPU 资源从磁盘读取。

还有其他情况需要考虑,但这些可以根据前面的情况进行推断。请记住,前面的分析是根据标准游戏的要求进行的,但这可能会根据您的游戏和目标设备而有很大不同。也许你正在制作一个不会消耗大量 RAM 但在 CPU 资源方面非常密集的游戏,在这种情况下,你可以将所有内容都放在加载时解压缩。重要的是要考虑游戏的所有方面,并根据资源进行平衡。

最后,还有一件要考虑的事情是压缩格式,这将改变 Unity 在发布游戏时对音频进行编码的方式。不同的压缩格式将以不同的压缩比率换取与原始音频的保真度较低或更高的解压缩时间,所有这些都根据音频模式和长度而有很大不同。我们有三种压缩格式:

  • PCM:无压缩格式将为您提供最高的音频质量,没有噪音伪影,但会导致更大的资产文件大小。

  • ADPCM:以这种方式压缩音频可以减小文件大小并产生快速的解压缩过程,但这可能会引入在某些类型的音频中会明显的噪音伪影。

  • Vorbis:一种高质量的压缩格式,几乎不会产生任何伪影,但解压时间较长,因此播放 Vorbis 音频会比其他格式稍微更加密集。它还提供了一个质量滑块,可以选择精确的压缩程度。

你应该使用哪一个?同样,这取决于你的音频特性。短平滑的音频可以使用 PCM,而长嘈杂的音频可以使用 ADPCM;这种格式引入的伪影将被隐藏在音频本身中。也许长平滑的音频在压缩伪影明显时使用 Vorbis 会更好。有时,这只是一个试错的问题。也许默认使用 Vorbis,当性能降低时,尝试切换到 ADPCM,如果那导致故障,就切换到 PCM。当然,问题在于确保音频处理确实是导致性能问题的原因——也许将所有音频切换到 ADPCM 并检查是否有所不同是检测的一个好方法,但更好的方法是使用 Profiler,这是一个性能测量工具,我们将在本书后面看到。

我们还有其他设置,比如采样率设置,再次,通过一些试错,你可以找到最佳设置。

我已经设置了从资产商店下载的音频,如下截图所示。第一张截图显示了我是如何设置音乐和环境音频文件的(大文件):

图 10.5 – 音乐和环境设置

图 10.5 – 音乐和环境设置

应该是立体声(未选中强制转换为单声道),使用流式加载类型,因为它们很大,只会有一个实例播放,并且使用ADPCM 压缩格式,因为 Vorbis 并没有产生巨大的大小差异。

第二个截图显示了我如何设置 SFX 文件(小文件):

图 10.6–射击 SFX 设置

图 10.6–射击 SFX 设置

将是 3D 声音,因此应该选中强制转换为单声道。将是短暂的,因此在加载时解压缩加载类型效果更好。Vorbis 压缩格式将 ADPCM 大小减少了一半以上

现在我们的音频片段已经正确配置,我们可以开始在场景中使用它们了。

集成和混音音频

我们可以简单地将我们的音频片段拖到场景中开始使用它,但是我们可以深入挖掘一下,探索将它们配置到每种可能的场景中的最佳方法。

在本节中,我们将研究以下音频集成概念:

  • 使用 2D 和 3D AudioSources

  • 使用音频混音器

让我们开始探索 AudioSources,这些对象负责音频播放。

使用 2D 和 3D AudioSources

AudioSources是可以附加到 GameObject 的组件。它们负责根据AudioClips发出游戏中的声音,这些将是我们之前下载的音频资产。重要的是要区分AudioClipAudioSource:我们可以有一个单一的爆炸AudioClip,但有很多AudioSources播放它,模拟多个爆炸。这样,AudioSource可以被视为AudioClip的一个实例。

创建AudioSource的最简单方法是选择一个AudioClip(音频资产)并将其拖到Hierarchy窗口中。尽量避免将音频拖到现有对象中;相反,将其拖动到对象之间,这样 Unity 将创建一个带有AudioSource的新对象,而不是将其添加到现有对象中(有时,您可能希望现有对象具有AudioSource,但现在让我们保持简单):

图 10.7–将音频剪辑拖到层次结构窗口之间的对象

图 10.7–将音频剪辑拖到层次结构窗口之间的对象

下面的截图显示了通过将音乐资产拖到场景中生成的AudioSource。您可以看到AudioClip字段引用了拖动的音频:

图 10.8–配置为播放我们的音乐资产的 AudioSource

图 10.8–配置为播放我们的音乐资产的 AudioSource

正如您所看到的,AudioSource有几个设置,让我们在以下列表中回顾常见的设置:

  • 播放时唤醒:确定游戏启动时音频是否自动开始播放。我们可以取消选中该选项,并通过脚本播放音频,也许是玩家射击或跳跃时(有关详细信息,请参阅本书的第三部分)。

  • 循环:当音频播放完毕时会自动重复。请记住始终在音乐和环境音频剪辑上检查此设置。很容易忘记这一点,因为这些曲目很长,我们可能永远不会在测试中达到它们的结尾。

  • 音量:控制音频强度。

  • 音调:控制音频速度。这对于模拟慢动作或引擎转速增加等效果非常有用。

  • 空间混合:控制我们的音频是 2D 还是 3D。在 2D 模式下,音频将在所有距离上以相同的音量听到,而 3D 将使音频音量随着距离增加而减小。

在我们的音乐曲目的情况下,我已经按照下面的截图所示进行了配置。您可以拖动环境雨声以将其添加到场景中,并使用与这些相同的设置,因为我们希望所有场景中都具有相同的环境效果。但是,在复杂的场景中,您可以在整个场景中散布不同的 3D 环境声音,以根据当前环境改变声音:

图 10.9 – 音乐和环境设置。这将循环播放,设置为唤醒时播放,是 2D

图 10.9 – 音乐和环境设置。这将循环播放,设置为唤醒时播放,是 2D

现在,您可以拖动射击效果并按照以下截图所示进行配置。正如您所看到的,这种情况下音频不会循环,因为我们希望射击效果每发一颗子弹就播放一次。请记住,在我们的情况下,子弹将是一个预制件,每次按下射击键时都会生成一个子弹,因此每颗子弹都将有自己的AudioSource在创建子弹时播放。此外,子弹设置为 3D 空间混合,这意味着效果将根据音频源相对于摄像机位置的位置而通过不同的扬声器传输:

图 10.10 – 音效设置。这不会循环,是一个 3D 声音

图 10.10 – 音效设置。这不会循环,是一个 3D 声音

在处理 3D 声音时需要考虑的一点是音量衰减设置,它位于 3D 声音设置部分。此设置控制声音随着到相机的距离而衰减的方式。默认情况下,您可以看到此设置设置为对数衰减,这是现实生活中声音的工作方式,但有时您不希望现实生活中的声音衰减,因为现实生活中的声音通常即使源头非常遥远也会被轻微听到。

一个选项是切换到线性衰减并使用最大距离设置来配置确切的最大距离:

图 10.11 – 最大距离为 10 米的 3D 声音,使用线性衰减

图 10.11 – 最大距离为 10 米的 3D 声音,使用线性衰减

现在我们可以配置单独的音频片段,让我们看看如何使用音频混音器对音频实例组应用效果。

使用音频混音器

我们将在整个游戏中播放几个音频实例:角色的脚步声,射击声,篝火声,爆炸声,雨声等等。根据情况精确控制哪些声音应该更响或更轻,并应用效果来加强某些情况,比如因附近爆炸而受到震惊,这就是音频混音 - 将几种声音以一种连贯和受控的方式混合在一起的过程。

在 Unity 中,我们可以创建一个音频混音器,这是一个我们可以用来定义声音组的资产。对组的所有更改都将影响其中的所有声音,可能是通过提高或降低音量,或者应用效果。您可以拥有 SFX 和音乐组来分别控制声音 - 例如,您可以在暂停菜单中降低 SFX 音量,但不降低音乐音量。此外,组是以层次结构组织的,其中一个组还可以包含其他组,因此对组的更改也将应用于其子组。事实上,您创建的每个组都将始终是主组的子组,这个组将影响游戏中使用该混音器的每一个声音。

让我们创建一个带有 SFX 和音乐组的混音器:

  1. 在项目窗口中,使用主混音器

  2. 双击创建的资产以打开音频混音器窗口:图 10.12 – 音频混音器窗口

图 10.12 – 音频混音器窗口

  1. 点击SFX图 10.13 – 组创建

图 10.13 – 组创建

  1. 点击音乐。记得在点击**+按钮之前选择组,因为如果选择了其他组,新组将成为该组的子组。无论如何,您可以通过在层次结构**窗口中拖动来重新排列组的子父关系:图 10.14 – 主、SFX 和音乐组

图 10.14 – 主、SFX 和音乐组

  1. 层次窗口中选择音乐GameObject,并在检视器窗口中查找AudioSource组件。

  2. 单击输出属性右侧的圆圈,并在音频混音器组选择器中选择音乐组。这将使该AudioSource受到指定混音器组的设置的影响:图 10.15 - 使一个 AudioSource 属于一个音频混音器组

图 10.15 - 使一个 AudioSource 属于一个音频混音器组

  1. 如果您现在玩游戏,您会看到音频混音器中的音量表开始移动,表明音乐正在通过音乐组。您还会看到组音量表也在移动,表明通过音乐组传递的声音也会通过组(音乐组的父级)传递到计算机的声卡:图 10.16 - 组音量级别

图 10.16 - 组音量级别

  1. 重复步骤 56,使环境和射击声音属于SFX组。

现在我们已经将我们的声音分成组,我们可以开始调整组的设置。但是,在这样做之前,我们需要考虑这样一个事实,即我们不希望始终使用相同的设置,就像之前提到的暂停菜单情况一样,其中 SFX 音量应该更低。为了处理这些情况,我们可以创建快照,这些快照是我们混音器的预设,可以在游戏过程中通过脚本激活。我们将在本书的第三部分处理脚本步骤,但是我们可以为游戏设置创建一个正常快照和一个暂停快照。

如果您检查快照列表,您会看到已经创建了一个快照 - 那可以是我们的正常快照。因此,让我们通过以下方式创建一个暂停快照:

  1. 单击暂停。记得停止游戏以编辑混音器,或者单击在 Playmode 中编辑选项允许 Unity 在播放过程中更改混音器。如果选择后者,请记住更改将在停止游戏时保留,不像对游戏对象的更改。实际上,如果您在播放模式下更改其他资产,这些更改也将保留 - 只有游戏对象的更改会被还原(以及我们现在不讨论的一些其他特定情况):图 10.17 - 快照创建

图 10.17 - 快照创建

  1. 选择暂停快照并降低SFX组的音量滑块:图 10.18 - 降低暂停快照的音量

图 10.18 - 降低暂停快照的音量

  1. 玩游戏,听听声音是否仍然保持正常音量。这是因为原始快照是默认的 - 您可以通过检查其右侧的星号来看到。您可以右键单击任何快照,并使用设置为起始快照选项将其设置为默认快照。

  2. 单击在 Playmode 中编辑以在运行时启用音频混音器修改。

  3. 单击暂停快照以启用它,并听听射击环境声音的音量是否已经减小。

正如您所看到的,混音器的主要用途之一是控制组音量,特别是当您看到组音量超过 0 标记时,表明该组太响了。无论如何,混音器还有其他用途,比如应用效果。如果您玩过任何战争游戏,您会注意到每当附近有炸弹爆炸时,您会在一段时间内以不同的方式听到声音,就好像声音在另一个房间里一样。这可以通过一种称为低通的效果来实现,它会阻止高频声音,这正是在这些情景中发生的:爆炸产生的高音量声音刺激了我们的耳朵,使它们在一段时间内对高频率的声音变得不那么敏感。

我们可以向任何通道添加效果,并根据当前快照进行配置,就像我们为音量所做的那样,方法如下:

  1. 点击组底部的添加...按钮,并选择低通简单图 10.19 - 通道的效果列表

图 10.19 - 通道的效果列表

  1. 选择正常快照(名为快照)进行修改。

  2. 选择组并查看检查器面板,在那里您将看到通道及其效果的设置。

  3. 设置22000);这将禁用该效果。

  4. 暂停快照重复步骤 34;我们不希望在该快照中出现这种效果。

  5. 创建一个名为炸弹震慑的新快照并选择它进行编辑。

  6. 设置1000图 10.20 - 设置低通简单效果的截止频率

图 10.20 - 设置低通简单效果的截止频率

  1. 玩游戏并在快照之间切换以检查差异。

除了低通滤波器,您还可以应用其他几种滤波器,比如回声,以创建一种近乎梦幻的效果,或者使用发送、接收和减弱的组合来根据另一个组的强度降低其音量(例如,您可能希望在对话发生时降低 SFX 音量)。我邀请您尝试这些和其他效果,并检查结果以确定潜在用途。

摘要

在本章中,我们讨论了如何导入和集成声音,考虑它们的内存影响并应用效果以生成不同的场景。声音是实现所需游戏体验的重要组成部分,因此请花足够的时间来做好它。

现在我们已经涵盖了游戏中几乎所有重要的美学方面,让我们创建另一种形式的视觉沟通,用户界面。

第十一章:用户界面设计

在屏幕上显示的一切并通过计算机的扬声器传达的都是一种形式的沟通。在之前的章节中,我们使用三维模型让用户知道他们在山中的基地,并通过适当的声音和音乐加强了这个想法。但对于我们的游戏,我们需要传达其他信息,比如用户剩余的生命值、当前得分等,有时很难使用游戏内图形来表达这些信息(有一些成功的案例可以做到这一点,比如死亡空间,但让我们保持简单)。为了传达这些信息,我们将在我们的场景顶部添加另一层图形,通常称为用户界面UI)或抬头显示HUD)。

这将包含不同的视觉元素,如文本字段、条形图和按钮,以便用户可以根据诸如生命值低时逃到安全地方等情况做出知情决策:

图 11.1 – 角色创建 UI 显示有关角色统计信息的数字

图 11.1 – 角色创建 UI 显示有关角色统计信息的数字

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

  • 理解CanvasRectTransform

  • Canvas 对象类型

  • 创建响应式 UI

在本章结束时,您将能够使用 Unity UI 系统创建能够通知用户游戏状态并允许他们通过按按钮来采取行动的界面。让我们开始讨论 Unity UI 系统的基本概念之一——RectTransform。

理解 Canvas 和 RectTransform

目前,Unity 中有三种不同用途的 UI 系统:

  • UI 元素:用于扩展 Unity 编辑器的系统,具有自定义窗口和工具。它使用了一些 Web 概念,如样式表和基于 XML 的语言来布局您的 UI。将来,它将可用于游戏中使用。

  • Unity UI:基于 GameObject 的 UI 仅适用于游戏内 UI(不是编辑器扩展)。您可以像编辑其他对象一样使用 GameObject 和组件来创建它。

  • IMGUI:一种完全使用脚本创建的遗留代码 UI。很久以前,这是编辑器和游戏内 UI 中唯一使用的 UI 系统。如今,它只用于扩展编辑器,并很快将被 UI 元素完全取代。

在本章中,我们只关注游戏内 UI,以向玩家传达有关游戏状态的不同信息,因此我们将使用 Unity UI。在撰写本书时,有计划用 UI 元素替换 Unity UI,但尚无预计的时间。无论如何,即使 Unity 很快发布 UI 元素作为游戏内 UI 系统,Unity UI 仍将存在一段时间,并且完全能够处理您需要创建的所有类型的 UI。

如果您要使用 Unity UI,首先需要了解它的两个主要概念——Canvas 和RectTransform。Canvas 是将包含和渲染我们的 UI 的主对象,而 RectTransform 是负责在屏幕上定位和调整每个 UI 元素的功能。

在这一部分,我们将研究以下 Unity UI 概念:

  • 使用 Canvas 创建 UI

  • 使用 RectTransform 定位元素

让我们开始使用 Canvas 组件来创建我们的 UI。

使用 Canvas 创建 UI

在 Unity UI 中,你在 UI 中看到的每个图像、文本和元素都是一个 GameObject,具有一组适当的组件,但为了让它们工作,它们必须是带有 Canvas 组件的主 GameObject 的子对象。这个组件负责触发 UI 生成并在每个子对象上进行绘制迭代。我们可以配置这个组件来指定这个过程的工作方式,并使其适应不同的可能要求。

首先,您可以通过GameObject | UI | Canvas选项简单地创建一个画布。这样做后,您将在场景中看到一个矩形,代表用户屏幕,因此您可以在其中放置元素,并预览它们相对于用户监视器的位置。您可以在以下截图中看到这个矩形的示例:

图 11.2 - 画布屏幕矩形

图 11.2 - 画布屏幕矩形

您可能会想到两件事。首先,“为什么矩形在场景中间?我希望它始终显示在屏幕上!”。不用担心,因为情况确实如此。当您编辑 UI 时,您将把它视为级别的一部分,作为其中的一个对象,但当您玩游戏时,它将始终投影到屏幕上,覆盖在每个对象的顶部。此外,您可能会想知道为什么矩形如此巨大,这是因为屏幕上的一个像素在场景上对应一米。所以不用担心这一点;当您在游戏视图中看到游戏时,您将看到所有 UI 元素在用户屏幕上的正确大小和位置。

在向 UI 添加元素之前,值得注意的是,当您创建 UI 时,会在画布旁边创建第二个对象,称为事件系统。这个对象对于渲染 UI 并不是必要的,但如果您希望 UI 可以交互,也就是包括点击按钮、在字段中输入文本或使用摇杆导航 UI 等操作,那么它就是必要的。EventSystem组件负责对用户输入进行采样,比如键盘、鼠标或摇杆,并将数据发送给 UI 以做出相应反应。我们可以更改与 UI 交互的确切按钮,但默认值现在可以接受,所以只需知道如果要与 UI 交互,就需要这个对象。如果出于某种原因删除了该对象,可以在GameObject | UI | Event System中重新创建它。

现在我们有了创建 UI 的基本对象,让我们向其中添加元素。

使用 RectTransform 定位元素

在 Unity UI 中,您在 UI 中看到的每个图像、文本和元素都是一个 GameObject,具有一组适合其用途的组件,但您会发现它们大多数都有一个共同的组件-RectTransform。UI 的每个部分本质上都是一个填充有文本或图像的矩形,并且具有不同的行为,因此了解RectTransform组件的工作原理以及如何编辑它是很重要的。

为了尝试这个组件,让我们通过以下步骤创建和编辑 UI 的一个简单白色矩形元素的位置:

  1. 转到GameObject | UI | Image。之后,您将看到在Canvas元素内创建了一个新的 GameObject。Unity 会负责将任何新的 UI 元素设置为Canvas的子元素;在外面,该元素将不可见:图 11.3 - 默认图像 UI 元素-白色框

图 11.3 - 默认图像 UI 元素-白色框

  1. 单击场景视图顶部栏中的 2D 按钮。这将只是改变场景视图的透视,以更适合编辑 UI(以及二维游戏):图 11.4 - 2D 按钮位置

图 11.4 - 2D 按钮位置

  1. 双击层次结构窗口中的画布,使 UI 完全适应场景视图。这将使我们能够清楚地编辑 UI。您还可以使用鼠标滚轮导航 UI 进行缩放,并单击并拖动滚轮以平移相机:图 11.5 - 2D 编辑模式下的场景视图

图 11.5 - 2D 编辑模式下的场景视图

  1. 禁用PPVolume对象以禁用后期处理。最终的 UI 不会有后期处理,但编辑器视图仍然会应用它。记得稍后重新启用它:图 11.6 - 禁用游戏对象-在这种情况下是后期处理体积

图 11.6 – 禁用游戏对象—在这种情况下是后期处理体积

  1. 启用(如果尚未启用)RectTrasform工具,这是 Unity 编辑器左上部的第五个按钮(或按T键)。这将启用矩形标尺,允许您移动、旋转和缩放二维元素。您可以使用通常的变换、旋转和缩放标尺,这些是我们在 3D 模式下使用的标尺,但矩形标尺会带来更少的麻烦,特别是在缩放方面:图 11.7 – 矩形标尺按钮

图 11.7 – 矩形标尺按钮

  1. 使用矩形标尺,拖动对象以移动它,使用蓝点改变其大小,或者将鼠标放在靠近蓝点的棘手位置以旋转它。请注意,使用这个标尺调整对象的大小并不等同于缩放对象,但稍后会详细介绍:图 11.8 – 用于编辑二维元素的矩形标尺

图 11.8 – 用于编辑二维元素的矩形标尺

  1. 在检视器窗口中,注意在更改 UI 元素的大小后,111),但是您可以看到宽度高度属性已经改变。Rect Transform本质上是一个经典的变换,但增加了宽度高度(以及其他稍后要探索的属性)。您可以在这里设置以像素表示的确切值:

图 11.9 – 矩形变换属性

图 11.9 – 矩形变换属性

现在我们知道了如何定位任何 UI 对象的基础知识,让我们来探索可以添加到画布中的不同类型的元素。

画布对象类型

到目前为止,我们已经使用了最简单的画布对象类型—白色框,但是还有很多其他对象类型可以使用,比如图像、按钮、文本等等。它们都使用RectTransform来定义它们的显示区域,但每种对象都有自己的概念和配置需要理解。

在本节中,我们将探索以下画布对象的概念:

  • 集成 UI 资产

  • 创建 UI 控件

让我们首先开始探索如何集成图像和字体,以便在画布中使用它们,这样我们就可以使用图像和文本 UI 对象类型将它们集成到我们的 UI 中。

集成 UI 资产

在使我们的 UI 使用漂亮的图形资产之前,我们需要像往常一样将它们正确地集成到 Unity 中。在下面的截图中,您将找到我们在第一章中提出的 UI 设计,从头开始设计游戏:

图 11.10 – 第一章的 UI 设计

图 11.10 – 第一章的 UI 设计

除此之外,我们还将添加一个暂停菜单,当用户按下Esc键时将被激活。它将如下截图所示:

图 11.11 – 暂停菜单设计

图 11.11 – 暂停菜单设计

基于这些设计,我们可以确定我们将需要以下资产:

  • 英雄的头像图像

  • 生命值条图像

  • 暂停菜单背景图像

  • 暂停菜单按钮图像

  • 文本的字体

像往常一样,我们可以在互联网上或者资产商店上找到所需的资产。在我的情况下,我会混合使用两者。让我们从最简单的一个开始—头像。采取以下步骤:

  1. 从互联网上下载你想要的头像:图 11.12 – 下载的头像资产

图 11.12 – 下载的头像资产

  1. 将其添加到你的项目中,可以通过将其拖放到项目窗口中,或者使用Sprites文件夹。

  2. 选择纹理,在检视器窗口中,将纹理类型设置为精灵(2D 和 UI)。所有纹理默认都准备用于 3D。此选项准备好所有用于 2D 的内容。

对于条形、按钮和窗口背景,我将使用资产商店寻找 UI 包。在我的情况下,我发现以下截图中的包是一个很好的开始我的 UI。通常情况下,请记住这个确切的包现在可能不可用。在这种情况下,请记住寻找另一个类似的包,或者从 GitHub 存储库中选择精灵:

图 11.13 - 选择的 UI 包

图 11.13 - 选择的 UI 包

首先,包含许多以精灵形式配置的图像,但我们可以进一步修改导入设置以实现高级行为,这是我们在按钮中需要的。按钮资产具有固定大小,但如果需要更大的按钮怎么办?一种选择是使用不同尺寸的其他按钮资产,但这将导致大量重复的按钮和其他资产,例如不同大小的背景用于不同的窗口,这将消耗不必要的 RAM。另一种选择是使用九片方法,这种方法包括将图像分割,使角落与其他部分分离。这允许 Unity 拉伸图像的中间部分以适应不同的大小,保持角落的原始大小,当与巧妙的图像结合时,可以用来创建几乎任何所需的大小。在下图中,您可以看到左下角有九片的形状,在同一图中的右下角,您可以看到形状被拉伸但保持其角落的原始大小。右上角显示了拉伸的形状没有片。您可以看到非切片版本被扭曲,而切片版本没有被扭曲:

图 11.14 - 切片与非切片图像拉伸

图 11.14 - 切片与非切片图像拉伸

在这种情况下,我们可以将九片应用于按钮和面板背景图像,以在游戏的不同部分使用它们。为了做到这一点,请执行以下操作:

  1. 使用窗口 | 包管理器选项打开包管理器。

  2. 验证Unity Registry图 11.15 - 包管理器中显示所有包

图 11.15 - 包管理器中显示所有包

  1. 安装2D Sprite包以启用精灵编辑工具(如果尚未安装):图 11.16 - 包管理器中的 2D Sprite 包

图 11.16 - 包管理器中的 2D Sprite 包

  1. 项目窗口中选择按钮精灵,然后单击检查器窗口中的精灵编辑器按钮:图 11.17 - 检查器窗口中的精灵编辑器按钮

图 11.17 - 检查器窗口中的精灵编辑器按钮

  1. 精灵编辑器窗口中,找到并拖动图像边缘的绿点以移动切片标尺。尝试确保切片不位于按钮边缘的中间。需要注意的一件事是,在我们的情况下,我们将使用三个切片而不是九个,因为我们的按钮不会在垂直方向上拉伸。

  2. 单击窗口右上角的应用按钮,然后关闭它:图 11.18 - 精灵编辑器窗口中的九片

图 11.18 - 精灵编辑器窗口中的九片

  1. 背景面板重复相同的步骤。在我的情况下,您可以在以下截图中看到,这个背景并没有考虑到九片,因为图像的所有中间区域都可以变小,如果使用九片方法来拉伸它们,它们看起来会一样。因此,我们可以使用任何图像编辑工具对其进行编辑,或者暂时使用它:

图 11.19 - 精灵编辑器窗口中的九片

图 11.19 - 精灵编辑器窗口中的九片

现在我们已经准备好我们的精灵,我们可以找到一个字体,这是一个非常简单的任务。只需下载任何.ttf.otf格式的字体并将其导入 Unity,就可以了,无需进一步配置。您可以在互联网上找到许多好的免费字体网站。我习惯于使用经典的DaFont.com网站,但还有很多其他网站可以使用。在我的情况下,我将使用以下字体:

图 11.20 - 我从 DaFont.com 选择的用于项目的字体

图 11.20 - 我从 DaFont.com 选择的用于项目的字体

如果压缩文件包含多个字体文件,您可以将它们全部拖入 Unity,然后使用您最喜欢的字体。同样,尝试将字体放在名为“字体”的文件夹中。

现在我们已经准备好创建 UI 所需的所有资产,让我们探索不同类型的组件以创建所有所需的 UI 元素。

创建 UI 控件

几乎 UI 的每个部分都将是巧妙配置的图像和文本的组合。在本节中,我们将探索以下组件:

  • 图像

  • 文本

  • 按钮

让我们开始探索图像。实际上,我们的 UI 中已经有一个图像 - 我们在本章前面创建的白色矩形。如果选择它并查看检查器窗口,您会注意到它有一个图像组件,就像以下截图中的一个:

图 11.21 - 图像组件的检查器窗口

图 11.21 - 图像组件的检查器窗口

让我们开始探索该组件的不同设置,从我们的英雄头像开始。采取以下步骤:

  1. 使用矩形图标,将白色矩形定位在 UI 的左上角:图 11.22 - 位于 UI 左上角的白色矩形

图 11.22 - 位于 UI 左上角的白色矩形

  1. 在“源图像”属性中选择并拾取下载的英雄头像精灵:图 11.23 - 设置我们的图像组件的精灵

图 11.23 - 设置我们的图像组件的精灵

  1. 我们需要校正图像的纵横比以防止失真。做到这一点的一种方法是单击“图像”组件,使图像使用与原始精灵相同的大小。但是,通过这样做,图像可能会变得太大,因此您可以按Shift减小图像大小以修改“宽度”和“高度”值。另一种选择是选中“保持纵横比”复选框,以确保图像适合矩形而不会拉伸。在我的情况下,我将两者都使用:

图 11.24 - 保持纵横比和设置原生大小图像选项

图 11.24 - 保持纵横比和设置原生大小图像选项

现在,让我们通过以下步骤创建生命条:

  1. 使用“GameObject”|“UI”|“图像”选项创建另一个“图像”组件。

  2. 将“源图像”属性设置为您下载的生命条图像:图 11.25 - 头像和生命条

图 11.25 - 头像和生命条

  1. 将“图像类型”属性设置为“填充”。

  2. 将“填充方法”属性设置为“水平”。

  3. 拖动“填充量”滑块,查看根据滑块值切割条的方式。当我们在书的第三部分编写生命系统时,我们将通过脚本更改该值,那里我们将编写自己的脚本:图 11.26 - 填充量滑块,将图像宽度切割为其大小的 73%

图 11.26 - 填充量滑块,将图像宽度切割为其大小的 73%

  1. 在我的情况下,条图像也带有条框,因此我将创建另一个图像,设置精灵,并将其定位在生命条顶部以形成框架。请记住,层次结构窗口中对象的顺序决定了它们绘制的顺序。因此,在我的情况下,我需要确保框架游戏对象在生命条图像下方:图 11.27 – 将一个图像放在另一个图像上创建框架效果

图 11.27 – 将一个图像放在另一个图像上创建框架效果

  1. 重复步骤 1 至 6,创建底部的基本条,或者只需复制并粘贴条和框架,并将其定位在屏幕底部:图 11.28 – 两个条

图 11.28 – 两个条

  1. 项目窗口中单击“+”按钮,然后选择Sprites | Square选项。这将创建一个简单的方形精灵。这与下载一个4 x 4分辨率的全白图像并将其导入 Unity 相同。

  2. 将精灵设置为基本条,而不是下载的条精灵。这一次,我们将使用一个纯白色的图像作为条的背景,因为在我的情况下,原始图像是红色的,将红色图像改为绿色是不可能的。然而,白色图像可以很容易地着色。考虑原始条的细节,例如,我的原始条中的小阴影在这里不会出现,但如果您想保留它,您应该获得一个带有该细节的白色条。

  3. 选择基本生命条并将“颜色”属性设置为绿色:图 11.29 – 带有方形精灵和绿色色调的条

图 11.29 – 带有方形精灵和绿色色调的条

  1. 一个可选的步骤是将条框图像转换为九切片图像,以便我们可以更改原始宽度以适应屏幕。

现在,让我们通过以下方式为得分、子弹、剩余波数和剩余敌人标签添加文本字段:

  1. 使用GameObject | UI | Text选项创建一个文本标签。这将是得分标签。

  2. 将标签定位在屏幕的右上角。

  3. 在“得分:0”中。

  4. 设置为20

  5. 通过单击Font属性右侧的圆圈并选择所需的字体来应用下载的字体。

  6. 检查“对齐”属性的水平对齐选项(最右边的选项)和垂直选项的中心选项:图 11.30 – 文本标签的设置

图 11.30 – 文本标签的设置

  1. 重复步骤 1 至 6,创建其他三个标签(或者只需将得分复制并粘贴三次)。对于“剩余波数”标签,您可以使用左对齐选项来更好地匹配原始设计:图 11.31 – 我们 UI 的所有标签

图 11.31 – 我们 UI 的所有标签

  1. 将所有标签的颜色设置为白色,因为我们的场景主要是黑暗的。

现在我们已经完成了原始的 UI 设计,让我们通过以下方式创建暂停菜单:

  1. 为菜单的背景创建一个“图像”组件(GameObject | UI | Image)。

  2. 使用我们之前制作的九切片设置Background面板精灵。

  3. 如果尚未这样做,请将“图像类型”属性设置为“切片”。此模式将应用九切片方法以防止角落拉伸。

  4. 有可能图像会在任何情况下拉伸角落,这是因为有时角落相对于精灵的“每单位像素”值来说相当大,这将减小原始图像的比例,同时保留其分辨率。

在接下来的两个屏幕截图中,您可以看到背景图像的“每单位像素”值为100,然后再次为700。请记住,只有对于九切片或平铺图像类型,或者如果您没有艺术家为您调整它时,才能这样做:

图 11.32 - 顶部是一个小的 RectTransform 组件中的大九宫格图像,足够小以缩小角落,底部是将每单位像素设置为 700 的相同图像

图 11.32 - 顶部是一个小的 RectTransform 组件中的大九宫格图像,足够小以缩小角落,底部是将每单位像素设置为 700 的相同图像

  1. 创建一个文本字段,将其放置在您的图表中想要暂停标签的位置,将其设置为显示暂停文本,并设置字体。请记住,您可以使用Color属性更改文本颜色。

  2. 将文本字段拖放到背景图像上。Canvas中的父子关系系统工作原理相同 - 如果移动父级,则子级将随之移动。这样做的想法是,如果我们禁用面板,它也将禁用按钮和所有其内容:图 11.33 - 暂停标签

图 11.33 - 暂停标签

  1. 通过转到GameObject | UI | Button来创建两个按钮。将它们放置在背景图像上的所需位置。

  2. 通过将它们在层次结构窗口中拖动到暂停背景图像中,将它们设置为暂停背景图像的子级。

  3. 选择按钮,并将它们的图像组件的Source Image属性设置为我们之前下载的按钮精灵。如果您遇到与之前相同的问题,请记住我们之前的每单位像素修复。

  4. 您会注意到按钮本质上是一个带有子文本对象的图像。将两个按钮的文本分别更改为恢复退出图 11.34 - 暂停菜单实现

图 11.34 - 暂停菜单实现

  1. 请记住,您可以通过取消顶部检查器窗口对象名称右侧复选框旁边的复选框来隐藏面板:

图 11.35 - 禁用游戏对象

图 11.35 - 禁用游戏对象

正如您所看到的,您可以通过使用图像和文本组件来创建几乎任何类型的 UI。当然,还有更高级的组件可以让您创建按钮、文本字段、复选框、列表等,但让我们先从基础知识开始。需要注意的一点是,我们已经创建了按钮,但到目前为止它们什么也没做。在本书的第三部分中,我们将看到如何编写脚本使它们具有功能。

在本节中,我们讨论了如何导入图像和字体,通过图像、文本和按钮组件进行集成,以创建丰富和信息丰富的 UI。做到这一点后,让我们讨论如何使它们适应不同的设备。

创建响应式 UI

如今,几乎不可能在单一分辨率下设计 UI,我们的目标受众显示设备可能差异很大。PC 具有各种不同分辨率的显示器(如 1080p、4k 等)和不同的宽高比(如 16:9、16:10、超宽等),移动设备也是如此。我们需要准备我们的 UI 以适应最常见的显示器,Unity UI 具有所需的工具来实现这一点。

在本节中,我们将探讨以下 UI 响应性概念:

  • 调整对象的位置

  • 调整对象的大小

我们将探讨如何使用 Canvas 和RectTransform组件的高级功能(如锚点和缩放器)使 UI 元素能够适应不同的屏幕尺寸和位置。

调整对象的位置

现在,如果我们玩我们的游戏,我们会看到 UI 如何很好地适应我们的屏幕。但是,如果由于某种原因我们改变了游戏视图大小,我们会看到对象开始从屏幕上消失。在以下截图中,您可以看到不同大小的游戏窗口以及 UI 在一个窗口中看起来很好,但在其他窗口中看起来很糟糕:

图 11.36 - 相同的 UI 但在不同的屏幕尺寸上

图 11.36 - 相同的 UI 但在不同的屏幕尺寸上

问题在于我们使用编辑器中的任何分辨率创建了 UI,但一旦我们稍微改变它,UI 就会保留先前分辨率的设计。此外,如果你仔细观察,你会注意到 UI 总是居中,比如在中间的图像中,UI 在两侧被裁剪,或者第三个图像中,屏幕边缘可见额外空间。这是因为 UI 中的每个元素都有自己的锚点,当你选择一个对象时,你可以看到一个小交叉点,就像下面的截图中所示:

图 11.37 - 位于屏幕右下部分的锚点交叉属于到屏幕的左上部分的英雄角色

图 11.37 - 位于屏幕右下部分的英雄角色的锚点交叉

对象的 X 和 Y 位置是相对于该锚点的距离,而锚点相对于屏幕有一个位置,其默认位置是在屏幕的中心。这意味着在 800 x 600 的屏幕上,锚点将放置在 400 x 300 的位置,在 1920 x 1080 的屏幕上,锚点将位于 960 x 540 的位置。如果元素(RectTransform 中的元素)的 X 和 Y 位置为 0,则对象将始终与中心的距离为 0。在前三个示例的中间截图中,英雄角色超出了屏幕,因为它与中心的距离大于屏幕的一半,并且当前距离是基于先前更大的屏幕尺寸计算的。那么,我们能做些什么呢?移动锚点!

通过设置相对位置,我们可以将锚点放在屏幕的不同部分,并使屏幕的该部分成为我们的参考位置。对于我们的英雄角色,我们可以将锚点放在屏幕的左上角,以确保我们的角色与该角落的距离固定。我们可以通过以下方式实现:

  1. 选择你的英雄角色。

  2. 用鼠标将锚点交叉拖动到屏幕的左上角。如果由于某种原因,当你拖动它时锚点会分裂成几部分,撤消更改(按Ctrl + Z,或者在 macOS 上按Command + Z)并尝试通过点击中心来拖动它。我们稍后会打破锚点:图 11.38 - 一个带有锚点的图像,位于屏幕的左上角

图 11.38 - 一个带有锚点的图像,位于屏幕的左上角

  1. 生命条对象和其框架的锚点放在同一位置。我们希望该条始终与该角落保持相同的距离,以便在屏幕大小改变时,它将随着英雄角色一起移动。

  2. 对于Boss Bar对象,将锚点放在屏幕底部中心位置,这样它将始终居中。稍后,我们将调整其大小。

  3. 剩余波数标签放在左下角,剩余敌人放在右下角:图 11.39 - 生命条和标签的锚点

图 11.39 - 生命条和标签的锚点

  1. 得分子弹锚点放在右上角:图 11.40 - 得分和子弹标签的锚点

图 11.40 - 得分和子弹标签的锚点

  1. 选择任何元素,并用鼠标拖动 Canvas 矩形的边缘,以预览元素将如何适应它们的位置。请注意,你必须选择 Canvas 的直接子对象;按钮内的文本将没有这个选项:

图 11.41 - 预览画布调整大小

图 11.41 - 预览画布调整大小

现在我们的 UI 元素已经适应了它们的位置,让我们考虑对象大小必须适应的情况。

调整对象的大小

处理不同宽高比的第一件事是,我们的屏幕元素可能不仅会从它们的原始设计位置移动(我们在上一节中固定了),而且它们可能不适合原始设计。在我们的 UI 中,我们有生命条的情况,当我们在更宽的屏幕上预览时,条明显不适应屏幕宽度。我们可以通过打破我们的锚点来解决这个问题。

当我们打破我们的锚点时,对象的位置和大小被计算为相对于不同锚点部分的距离。如果我们水平分割锚点,我们将有左和右属性,而不是 X 和宽度属性,它们代表到左和右锚点的距离。我们可以这样使用:

  1. 选择生命条,将锚点的左部分拖到屏幕的左部分,右部分拖到屏幕的右部分。

  2. 对于生命条框架也是一样的:图 11.42 - 生命条中的分隔锚点

图 11.42 - 生命条中的分隔锚点

  1. 在检视器窗口中检查Rect Transform设置的属性,它们代表当前到各自锚点的距离。如果你愿意,你可以添加一个特定的值,特别是如果你的生命条显示在屏幕外:

图 11.43 - 分隔锚点的左右属性

图 11.43 - 分隔锚点的左右属性

这样,对象将始终保持相对于屏幕的固定距离,即屏幕的两侧。如果你正在处理一个子对象,比如按钮的文本和图像组件,锚点是相对于父对象的。如果你注意到文本的锚点,它们不仅在水平方向上分割,而且在垂直方向上也分割。这允许文本根据按钮的大小调整位置,这样你就不必手动更改它:

图 11.44 - 按钮文本的分隔锚点

图 11.44 - 按钮文本的分隔锚点

现在,这种解决方案并不适用于所有情况。让我们考虑一个情况,英雄头像显示的分辨率比它设计的要高。即使头像被正确放置,它也会显示得更小,因为屏幕的像素密度比其他分辨率更高。你可以考虑使用分隔锚点,但是在不同宽高比的屏幕上,宽度和高度锚点可能会以不同的比例进行缩放,所以原始图像会变形。相反,我们可以使用画布缩放器组件。

画布缩放器组件定义了在我们的场景中 1 像素的含义。如果我们的 UI 设计分辨率是 1080p,但我们在 4k 显示器上看到它(这是 1080p 分辨率的两倍),我们可以缩放 UI,使得一个像素变为 2,调整其大小以保持与原始设计相同的比例大小。基本上,这个想法是,如果屏幕更大,我们的元素也应该更大。

我们可以通过以下方式使用这个组件:

  1. 选择Canvas对象,并在检视器窗口中找到Canvas Scaler组件。

  2. UI Scale Mode属性设置为Scale with Screen Size

  3. 这对我们来说并不是问题,但是如果将来你和一个艺术家合作,将参考分辨率设置为艺术家创建 UI 的分辨率,记住它必须是最高目标设备分辨率。在我们的情况下,我们不确定下载资产的艺术家有没有考虑过分辨率,所以我们可以设置为1920 x 1080,这是全高清分辨率大小,现在非常常见。

  4. 在这种情况下设置“宽度”值,因为屏幕的宽度可能会非常宽,比如超宽屏,如果我们选择了那个选项,那些屏幕会不必要地缩放 UI。另一个选项是将此值设置为0.5以考虑这两个值,但在 PC 上,这并没有太多意义。在移动设备上,您应该根据游戏的方向选择这个值,为横向模式设置高度,为纵向模式设置宽度。尝试预览更宽和更高的屏幕,看看这个设置是如何工作的:

图 11.45 - 带有标准 PC 游戏正确设置的画布缩放器

图 11.45 - 带有标准 PC 游戏正确设置的画布缩放器

您会发现您的 UI 比原始设计要小,这是因为我们应该在之前设置这些属性。现在,唯一的解决办法是重新调整大小。下次尝试这个练习时要考虑到这一点;我们只是按照这个顺序进行学习。

在继续之前,请记得重新激活后期处理体积对象以再次显示这些效果。您会注意到 UI 在游戏视图中不受它们的影响。

重要提示:

如果您希望您的 UI 受到后期处理效果的影响,您可以设置为“-相机”。将主摄像机拖动到“渲染相机”属性,并将“平面距离”设置为5。这将使 UI 与其他对象一起放置在世界中,与相机视图对齐,距离为 5 米。

图 11.46 - 画布渲染模式设置为相机模式以接收后期处理效果

图 11.46 - 画布渲染模式设置为相机模式以接收后期处理效果

有了这些知识,现在您已经准备好开始自己创建您的第一个 UI 了。

总结

在本章中,我们介绍了 UI 的基础知识,理解了“图像”和“文本”,为我们的 UI 布局赋予生命,并使其对用户具有吸引力。最后,我们讨论了如何使 UI 对象适应不同的分辨率和宽高比,使我们的 UI 适应不同的屏幕尺寸,即使我们无法预测用户将在哪种显示器上玩游戏。

在下一章中,我们将开始看如何向我们的游戏中添加动画角色。

第十二章:使用动画师、电影机和时间轴创建动画

在我们当前的游戏状态下,除了考虑着色器和粒子动画外,我们大部分时间都处于静态场景中。在下一章中,当我们为游戏添加脚本时,一切都将根据我们想要的行为开始移动。但有时,我们需要以预定的方式移动对象,例如通过过场动画,或者特定的角色动画,例如跳跃、奔跑等。本章的目的是介绍几种 Unity 动画系统,以创建所有可能的对象运动,而无需脚本。

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

  • 使用动画师进行骨骼动画

  • 使用电影机创建动态摄像机

  • 使用时间轴创建过场动画

通过本章结束时,您将能够创建过场动画来讲述游戏的故事或突出显示级别的特定区域,以及创建能够准确展示游戏外观的动态摄像机,无论情况如何。

使用动画师进行骨骼动画

到目前为止,我们使用的是静态网格,这些是实心的三维模型,不应该以任何方式弯曲或动画化(除了单独移动,如汽车的门)。我们还有另一种网格,称为蒙皮网格,它们具有根据骨骼弯曲的能力,因此可以模拟人体肌肉的运动。我们将探讨如何将动画人形角色整合到我们的项目中,以创建敌人和玩家的动作。

在本节中,我们将研究以下骨骼网格概念:

  • 了解蒙皮

  • 导入蒙皮网格

  • 使用动画师控制器进行整合

我们将探讨蒙皮的概念以及它如何使您能够为角色添加动画。然后,我们将把动画网格引入我们的项目,最终对其应用动画。让我们从讨论如何将骨骼动画引入我们的项目开始。

了解蒙皮

为了获得动画网格,我们需要四个部分,从网格本身和将要进行动画的模型开始,这与任何其他网格的创建方式相同。然后,我们需要骨骼,这是一组骨骼,将与所需的网格拓扑匹配,例如手臂、手指、脚等。在图 12.1中,您可以看到一组骨骼与我们的目标网格对齐的示例。您会注意到这类网格通常是用T姿势建模的,这将有助于动画制作过程:

图 12.1 – 忍者网格与其默认姿势匹配的骨骼

图 12.1 – 忍者网格与其默认姿势匹配的骨骼

一旦艺术家创建了模型及其骨骼,下一步就是进行蒙皮,即将模型的每个顶点与一个或多个骨骼相关联的过程。这样,当您移动骨骼时,相关的顶点也会随之移动。这样做是因为动画化少量骨骼比动画化模型的每个单独顶点更容易。在下一个截图中,您将看到网格的三角形根据受其影响的骨骼的颜色进行着色,以可视化骨骼的影响。您将注意到颜色之间的混合,这意味着这些顶点受不同骨骼的不同影响,以使关节附近的顶点能够很好地弯曲。此外,截图还说明了用于二维游戏的二维网格的示例,但概念是相同的:

图 12.2 – 网格蒙皮权重以颜色形式可视化表示

图 12.2 – 网格蒙皮权重以颜色形式可视化表示

最后,你需要的最后一部分是实际的动画,它将简单地由网格的不同姿势混合而成。艺术家将在动画中创建关键帧,确定模型在不同时刻需要采取哪种姿势,然后动画系统将简单地在它们之间进行插值。基本上,艺术家将对骨骼进行动画处理,而蒙皮系统将把这个动画应用到整个网格上。你可以有一个或多个动画,之后你可以根据你想要匹配角色动作的动画来在它们之间切换(比如站立、行走、跌倒等)。

为了获得这四个部分,我们需要获取包含它们的适当资产。在这种情况下,通常的格式是FilmboxFBX),这与我们迄今为止用来导入 3D 模型的格式相同。这种格式可以包含我们需要的每一部分——模型、带有蒙皮的骨骼和动画——但通常,我们会将部分拆分成多个文件以重复利用这些部分。

想象一个城市模拟游戏,我们有几个市民网格,外观各异,所有这些网格都必须进行动画处理。如果每个市民的单个 FBX 包含网格、蒙皮和动画,那么每个模型都会有自己的动画,或者至少是相同动画的克隆,重复出现。当我们需要更改动画时,我们需要更新所有网格市民,这是一个耗时的过程。与此相反,我们可以为每个市民准备一个 FBX,其中包含网格和骨骼,以及一个单独的 FBX 文件用于每个动画,其中包含所有市民都具有的相同骨骼和适当动画,但不包含网格。这将允许我们混合和匹配市民 FBX 和动画的 FBX 文件。也许你会想为什么模型 FBX 和动画 FBX 都必须有网格。这是因为它们需要匹配才能使两个文件兼容。在下一个截图中,你可以看到文件应该是什么样子的:

图 12.3 – 我们将在项目中使用的包的动画和模型 FBX 文件

图 12.3 – 我们将在项目中使用的包的动画和模型 FBX 文件

另外,值得一提的是一个叫做重定向的概念。正如我们之前所说,为了混合模型和动画文件,我们需要它们具有相同的骨骼结构,这意味着相同数量的骨骼、层次结构和名称。有时,这是不可能的,特别是当我们混合我们的艺术家创建的自定义模型与使用动作捕捉技术从演员那里记录下来的外部动画文件,或者只是购买一个 Mocap 库。在这种情况下,很可能会遇到 Mocap 库中的骨骼结构与您的角色模型不同,这就是重定向发挥作用的地方。这种技术允许 Unity 创建两种不同的仅限于人形的骨骼结构之间的通用映射,使它们兼容。一会儿,我们将看到如何启用这个功能。

现在我们了解了有关蒙皮网格的基础知识,让我们看看如何获取带有骨骼和动画的模型资产。

导入骨骼动画

让我们从如何从资产商店导入一些带有动画的模型开始,在3D | Characters | Humanoids部分。你也可以使用外部网站,比如 Mixamo,来下载它们。但现在,我会坚持使用资产商店,因为你在使资产工作时会遇到更少的麻烦。在我的情况下,我已经下载了一个包,正如你在下面的截图中所看到的,其中包含了模型和动画。

请注意,有时您需要单独下载它们,因为某些资产将仅为模型或动画。另外,请注意,本书中使用的软件包可能在您阅读时不可用;在这种情况下,您可以寻找另一个具有类似资产(角色和动画)的软件包,或者从书的 GitHub 存储库中下载项目文件,并从那里复制所需的文件:

图 12.4 - 我们游戏的士兵模型

图 12.4 - 我们游戏的士兵模型

在我的包内容中,我可以在Animations文件夹中找到动画的 FBX 文件,而在Model中找到单个模型的 FBX 文件。请记住,有时您不会将它们分开,动画可能位于与模型相同的 FBX 中,如果有任何动画的话。现在我们有了所需的文件,让我们讨论如何正确配置它们。

让我们开始选择模型文件并检查骨骼选项卡。在此选项卡中,您将找到一个名为动画类型的设置,如下图所示:

图 12.5 - 骨骼属性

图 12.5 - 骨骼属性

此属性包含以下选项:

  • :非动画模型的模式;您游戏中的每个静态网格将使用此模式。

  • 传统:用于旧 Unity 项目和模型的模式;不要在新项目中使用此模式。

  • 通用:一种新的动画系统,可以用于各种模型,但通常用于非人形模型,如马、章鱼等。如果使用此模式,模型和动画 FBX 文件必须具有完全相同的骨骼名称和结构,从而减少了来自外部来源的动画组合的可能性。

  • 人形:设计用于人形模型的新动画系统。它启用了重新定位和反向运动学IK)等功能。这使您能够将具有不同骨骼的模型与动画结合,因为 Unity 将在这些结构和一个通用结构之间创建映射,称为阿凡达。请注意,有时自动映射可能会失败,您将需要手动更正;因此,如果您的通用模型具有您需要的一切,我建议您坚持使用通用,如果那是 FBX 的默认配置。

在我的情况下,我的软件包中的 FBX 文件的模式设置为Humanoid,所以很好,但请记住,只有在绝对必要时才切换到其他模式(例如,如果您需要组合不同的模型和动画)。现在我们已经讨论了骨骼设置,让我们谈谈动画设置。

为此,请选择任何动画 FBX 文件,并查找检视器窗口中的动画部分。您会发现几个设置,例如导入动画复选框,如果文件有动画(而不是模型文件),必须标记该复选框,以及剪辑列表,您将在其中找到文件中的所有动画。在下面的截图中,您可以看到我们一个动画文件的剪辑列表:

图 12.6 - 动画设置中的剪辑列表

图 12.6 - 动画设置中的剪辑列表

带有动画的 FBX 文件通常包含单个大动画轨道,其中可以包含一个或多个动画。无论如何,默认情况下,Unity 将基于该轨道创建单个动画,但如果该轨道包含多个动画,则您需要手动拆分它们。在我们的情况下,我们的 FBX 已经由软件包创建者拆分为多个动画,但为了学习如何手动拆分,请执行以下操作:

  1. HumanoidCrouchIdle

  2. 看一下动画时间轴下方的开始结束值,并记住它们;我们将使用它们来重新创建此剪辑:图 12.7 - 剪辑设置

图 12.7 - 剪辑设置

  1. 单击片段列表底部右侧的减号按钮以删除所选的片段。

  2. 使用加号按钮创建一个新的片段并选择它。

  3. 使用Take 001输入字段将其重命名为与原始名称类似的内容。在我的例子中,我会将其命名为空闲

  4. 开始设置为319,将结束设置为264。这些信息通常来自艺术家,但您可以尝试最适合的数字,或者简单地在时间轴上拖动蓝色标记到这些属性上。

  5. 您可以通过单击检视器窗口底部的标题栏上的条形图来预览片段,然后单击播放按钮来预览您的动画(在我的例子中是HumanoidIdle)。您将看到默认的 Unity 模型,但是您可以通过将模型文件拖放到预览窗口中来查看自己的模型,因为检查我们的模型是否正确配置是很重要的。如果动画没有播放,您需要检查动画类型设置是否与动画文件匹配:

图 12.8 - 动画预览

图 12.8 - 动画预览

现在,打开动画文件,单击箭头,然后检查子资产。您会看到这里有一个与您的动画标题相对应的文件,以及剪辑列表中的其他动画,其中包含了剪辑。一会儿,我们将播放它们。在下面的截图中,您可以看到我们.fbx文件中的动画:

图 12.9 - 生成的动画片段

图 12.9 - 生成的动画片段

现在我们已经介绍了基本配置,让我们看看如何集成动画。

使用动画控制器进行集成

在为角色添加动画时,我们需要考虑动画的流程,这意味着考虑必须播放哪些动画,每个动画何时处于活动状态,以及动画之间的过渡应该如何发生。在以前的 Unity 版本中,您需要手动编写复杂的 C#代码脚本来处理复杂的情景;但现在,我们有了动画控制器。

动画控制器是基于状态机的资产,我们可以使用名为动画师的可视编辑器来绘制动画之间的转换逻辑。其思想是每个动画都是一个状态,我们的模型将有多个状态。一次只能激活一个状态,因此我们需要创建转换来改变它们,这些转换将具有必须满足的条件才能触发转换过程。条件是关于要进行动画的角色的数据的比较,例如其速度、是否在射击或蹲下等。

因此,动画控制器或状态机基本上是一组带有转换规则的动画,它将决定哪个动画应处于活动状态。让我们通过以下步骤开始创建一个简单的动画控制器:

  1. 点击“播放器”。记得将您的资产放在一个文件夹中以便进行适当的组织;我会把我的称为“动画师”。

  2. 双击资产以打开动画师窗口。不要将此窗口与动画窗口混淆;动画窗口有不同的功能。

  3. 将您角色的空闲动画片段拖放到动画师窗口中。这将在控制器中创建一个框,表示将连接到控制器的默认动画,因为这是我们拖动的第一个动画。如果您没有空闲动画,我建议您找一个。我们至少需要一个空闲和一个行走/奔跑的动画片段:图 12.10 - 从 FBX 资产中拖动动画片段到动画控制器

图 12.10 - 从 FBX 资产中拖动动画片段到动画控制器

  1. 以相同的方式拖动奔跑动画。

  2. 右键点击Idle动画,选择Create Transition,然后左键点击Run动画。这将在IdleRun之间创建一个过渡。

  3. 以相同的方式从RunIdle创建另一个过渡:图 12.11 – 两个动画之间的过渡

图 12.11 – 两个动画之间的过渡

过渡必须有条件,以防止动画不断切换,但为了创建条件,我们需要数据进行比较。我们将向我们的 Controller 添加属性,这些属性将代表过渡所使用的数据。稍后在第三部分中,我们将设置这些数据以匹配对象的当前状态。但现在,让我们创建数据并测试 Controller 对不同值的反应。为了基于属性创建条件,做如下操作:

  1. 点击Animator窗口左上角的Parameters选项卡。如果你没有看到它,点击交叉眼按钮显示选项卡。

  2. 点击Velocity。如果你错过了重命名部分,只需左键点击变量并重命名:图 12.12 – 具有浮点速度属性的参数选项卡

图 12.12 – 具有浮点速度属性的参数选项卡

  1. 在检查器窗口中点击Conditions属性。

  2. 点击0。这告诉我们过渡将从0执行。我建议你设置一个稍高一点的值,比如0.01,以防止任何浮点舍入错误(常见的 CPU 问题)。还要记住,Velocity的实际值需要通过脚本手动设置,这将在第三部分中进行:图 12.13 – 检查速度是否大于 0.01 的条件

图 12.13 – 检查速度是否大于 0.01 的条件

  1. 0.01做同样的操作:

图 12.14 – 检查值是否小于 0.01 的条件

图 12.14 – 检查值是否小于 0.01 的条件

现在我们已经设置好了第一个 Animator Controller,是时候将它应用到一个对象上了。为了做到这一点,我们需要一系列的组件。首先,当我们有一个动画角色时,我们使用蒙皮网格渲染器而不是普通的网格渲染器。如果你将角色模型拖到场景中并探索它的子级,你会看到一个组件,如下所示:

图 12.15 – 一个蒙皮网格渲染器组件

图 12.15 – 一个蒙皮网格渲染器组件

这个组件将负责将骨骼的移动应用到网格上。如果你搜索模型的子级,你会发现一些骨骼;你可以尝试旋转、移动和缩放它们,以查看效果,如下面的截图所示。请注意,如果你从资产商店下载了另一个包,你的骨骼层次结构可能与我的不同:

图 12.16 – 旋转颈骨

图 12.16 – 旋转颈骨

我们需要的另一个组件是Animator,它会自动添加到其根 GameObject 的蒙皮网格上。这个组件将负责应用我们在 Animator Controller 中创建的状态机,如果动画 FBX 文件按照我们之前提到的方式正确配置的话。为了应用 Animator Controller,做如下操作:

  1. 如果场景中还没有角色模型,将角色模型拖到场景中。

  2. 选择它并定位根 GameObject 中的Animator组件。

  3. 点击Controller属性右侧的圆圈,选择之前创建的Player控制器。你也可以直接从项目窗口拖动它。

  4. 确保Avatar属性设置为 FBX 模型内的 avatar;这将告诉动画师我们将使用该骨架。您可以通过其人物图标来识别 avatar 资源,如下面的屏幕截图所示。通常,当您将 FBX 模型拖到场景中时,此属性会自动正确设置:图 12.17 - 动画师使用玩家控制器和机器人 avatar

图 12.17 - 动画师使用玩家控制器和机器人 avatar

  1. Camera游戏对象设置为朝向玩家并播放游戏,您将看到角色执行其Idle动画。

  2. 在不停止游戏的情况下,再次通过双击打开动画控制器资源,并在Hierarchy窗格中选择角色。通过这样做,您应该看到该角色正在播放的动画的当前状态,使用条形图表示动画的当前部分:图 12.18 - 在选择对象时播放模式下的动画控制器,显示当前动画及其进度

图 12.18 - 在选择对象时播放模式下的动画控制器,显示当前动画及其进度

  1. 使用1.0并查看转换的执行方式:图 12.19 - 设置控制器的速度以触发转换

图 12.19 - 设置控制器的速度以触发转换

根据Run动画的设置方式,您的角色可能会开始移动。这是由根动作引起的,这是一个根据动画移动角色的功能。有时这是有用的,但由于我们将完全使用脚本移动角色,我们希望关闭该功能。您可以通过取消Character对象的Animator组件中的Apply Root Motion复选框来实现。:

图 12.20 - 禁用根动作

图 12.20 - 禁用根动作

  1. 您还会注意到更改Velocity值和动画转换开始之间存在延迟。这是因为默认情况下,Unity 会等待原始动画结束后再执行转换,但在这种情况下,我们不希望如此。我们需要立即开始转换。为了做到这一点,选择控制器的每个转换,并在检查器窗口中取消选中Has Exit Time复选框:

图 12.21 - 禁用“具有退出时间”复选框以立即执行转换

图 12.21 - 取消“具有退出时间”复选框以立即执行转换

您可以开始将其他动画拖入控制器并创建复杂的动画逻辑,例如添加跳跃、下落或蹲伏动画。我邀请您尝试其他参数类型,例如布尔值,它使用复选框而不是数字。此外,随着游戏的进一步开发,您的控制器将增加其动画数量。为了管理它,还有其他值得研究的功能,例如混合树和子状态机,但这超出了本书的范围。

现在我们了解了 Unity 中角色动画的基础知识,让我们讨论如何创建动态摄像机动画来跟随我们的玩家。

使用 Cinemachine 创建动态摄像机

摄像机在视频游戏中是一个非常重要的主题。它们允许玩家看到周围的环境,以便根据所见做出决策。游戏设计师通常定义其行为方式,以获得他们想要的确切游戏体验,这并不容易。必须层叠许多行为才能获得确切的感觉。此外,在过场动画期间,控制摄像机将要穿越的路径以及摄像机的焦点是重要的,以便在这些不断移动的场景中聚焦动作。

在本章中,我们将使用 Cinemachine 软件包创建两个动态摄像机,这些摄像机将跟随玩家的动作,我们将在第三部分中编写,并且还将用于过场动画中使用的摄像机。

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

  • 创建摄像机行为

  • 创建摄影机轨道

让我们首先讨论如何创建一个 Cinemachine 控制的摄像机,并在其中配置行为。

创建摄像机行为

Cinemachine 是一组不同的行为,可以用于摄像机中,当正确组合时可以生成各种常见的视频游戏摄像机类型,包括从后面跟随玩家,第一人称摄像机,俯视摄像机等。为了使用这些行为,我们需要了解大脑和虚拟摄像机的概念。

在 Cinemachine 中,我们将只保留一个主摄像机,就像我们迄今为止所做的那样,该摄像机将由虚拟摄像机控制,这些虚拟摄像机是分开的游戏对象,具有先前提到的行为。我们可以有几个虚拟摄像机,并且可以随意在它们之间切换,但是活动虚拟摄像机将是唯一控制我们主摄像机的摄像机。这对于在游戏的不同点之间切换摄像机非常有用,例如在我们玩家的第一人称摄像机之间切换。为了使用虚拟摄像机控制主摄像机,它必须具有Brain组件。

要开始使用 Cinemachine,首先我们需要从软件包管理器中安装它,就像我们之前安装其他软件包一样。如果您不记得如何做到这一点,只需执行以下操作:

  1. 转到窗口 | 软件包管理器

  2. 确保窗口左上角的软件包选项设置为Unity Registry图 12.22 – 软件包过滤模式

图 12.22 – 软件包过滤模式

  1. 等待左侧面板从服务器中填充所有软件包(需要互联网)。

  2. 查找列表中的Cinemachine软件包并选择它。在撰写本书时,我们使用的是 Cinemachine 2.6.0。

  3. 单击屏幕右下角的安装按钮。

让我们开始创建一个虚拟摄像机来跟随我们之前制作的角色,这将是我们的玩家英雄。执行以下操作:

  1. 单击CM vcam1图 12.23 – 虚拟摄像机创建

图 12.23 – 虚拟摄像机创建

  1. 如果您从CinemachineBrain组件中选择了主摄像机,那么我们的主摄像机将自动添加到其中,使我们的主摄像机跟随虚拟摄像机。尝试移动创建的虚拟摄像机,您将看到主摄像机如何跟随它:图 12.24 – CinemachineBrain 组件

图 12.24 – CinemachineBrain 组件

  1. 选择虚拟摄像机,并将角色拖动到 Cinemachine 虚拟摄像机组件的跟随看向属性中。这将使移动和观察行为使用该对象来完成它们的工作:图 12.25 – 设置我们摄像机的目标

图 12.25 – 设置我们摄像机的目标

  1. 您可以看到03-3)值:图 12.26 – 摄像机从后面跟随角色

图 12.26 – 摄像机从后面跟随角色

  1. 图 12.26 显示01.50很好地使摄像机看向胸部:

图 12.27 – 改变瞄准偏移

图 12.27 – 改变瞄准偏移

正如您所看到的,使用 Cinemachine 非常简单,在我们的情况下,默认设置大多已经足够满足我们需要的行为。但是,如果您探索其他BodyAim模式,您会发现您可以为任何类型的游戏创建任何类型的摄像机。我们不会在本书中涵盖其他模式,但我强烈建议您查看 Cinemachine 的文档,以了解其他模式的功能。要打开文档,请执行以下操作:

  1. 通过转到窗口 | 包管理器来打开包管理器。

  2. 在左侧列表中找到Cinemachine。如果没有显示,请稍等一会。请记住,您需要互联网连接才能使用它。

  3. 一旦选择了Cinemachine,请查找蓝色的查看文档链接。单击它:图 12.28 - Cinemachine 文档链接

图 12.28 - Cinemachine 文档链接

  1. 您可以使用左侧的导航菜单来探索文档:

图 12.29 - Cinemachine 文档

图 12.29 - Cinemachine 文档

就像您在 Cinemachine 中所做的那样,您也可以以同样的方式找到其他软件包的文档。现在我们已经实现了我们需要的基本摄像机行为,让我们探索如何使用 Cinemachine 为我们的开场动画创建摄像机。

创建推车轨道

当玩家开始关卡时,我们希望有一个小的过场动画,展示我们的场景和战斗之前的基地。这将需要摄像机沿着固定路径移动,这正是 Cinemachine 的推车摄像机所做的。它创建了一个我们可以附加虚拟摄像机的路径,以便它会跟随它。我们可以设置 Cinemachine 自动沿着轨道移动或者跟随目标到轨道最近的点;在我们的情况下,我们将使用第一个选项。

为了创建推车摄像机,请执行以下操作:

  1. 让我们开始用一个推车创建轨道,这是一个小物体,将沿着轨道移动,这将是摄像机跟随的目标。要做到这一点,请单击Cinemachine | 创建带有推车的推车轨道图 12.30 - 默认直线路径的推车摄像机

图 12.30 - 默认直线路径的推车摄像机

  1. 如果选择DollyTrack1对象,您可以看到两个带有数字01的圆圈。这些是轨道的控制点。选择其中一个并像移动其他对象一样移动它,使用平移图标的箭头。

  2. 您可以通过单击DollyTrack1对象的CinemachineSmoothPath组件来创建更多的控制点:图 12.31 - 添加路径控制点

图 12.31 - 添加路径控制点

  1. 创建尽可能多的航点,以创建一个将在开场动画中遍历您希望摄像机监视的区域的路径。请记住,您可以通过单击它们并使用平移图标来移动航点:图 12.32 - 我们场景中的推车轨道。它在角色的后面结束

图 12.32 - 我们场景中的推车轨道。它在角色的后面结束

  1. 创建一个新的虚拟摄像机。创建后,如果您转到游戏视图,您会注意到角色摄像机将处于活动状态。为了测试新摄像机的外观,选择它并在检查器窗口中单击独奏按钮:图 12.33 - 在编辑时临时启用虚拟摄像机的“独奏”按钮

图 12.33 - 在编辑时临时启用虚拟摄像机的“独奏”按钮

  1. 设置我们之前使用轨道创建的DollyCart1对象。

  2. 000设置为使摄像机保持在与推车相同的位置。

  3. Aim设置为与跟随目标相同,使摄像机朝着相同的方向看,这将跟随轨道曲线:图 12.34 - 配置以使虚拟摄像机跟随推车轨道

图 12.34 – 配置使虚拟相机跟随推车轨道

  1. 选择DollyCart1对象,并更改位置值,以查看推车沿着轨道移动的情况。在游戏窗口聚焦且CM vcam2处于独立模式时执行此操作,以查看相机的外观:

图 12.35 – 推车组件

图 12.35 – 推车组件

有了正确设置的推车轨道,我们可以使用时间轴来创建我们的剧情场景。

使用时间轴创建剧情场景

我们有我们的开场相机,但这还不足以创建一个剧情场景。一个合适的剧情场景是一系列在应该发生的确切时刻发生的动作,协调多个对象以按预期方式行动。我们可以有启用和禁用对象、切换相机、播放声音、移动对象等动作。为此,Unity 提供了时间轴,这是一个协调这种类型剧情场景的动作的序列器。我们将使用时间轴为我们的场景创建一个开场剧情,展示游戏开始前的关卡。

在本节中,我们将研究以下时间轴概念:

  • 创建动画剪辑

  • 安排我们的开场剧情

我们将看到如何在 Unity 中创建自己的动画剪辑,以动画我们的游戏对象,然后将它们放入一个剧情场景中,使用时间轴序列工具协调它们的激活。让我们开始创建一个相机动画,以便稍后在时间轴中使用。

创建动画剪辑

这实际上不是时间轴特定的功能,而是一个与时间轴很好配合的 Unity 功能。当我们下载角色时,它带有使用外部软件创建的动画剪辑,但您可以使用 Unity 的动画窗口创建自定义动画剪辑。不要将其与动画师窗口混淆,后者允许我们创建根据游戏情况做出反应的动画过渡。这对于创建您稍后将在时间轴中与其他对象的动画协调的小对象特定动画非常有用。

这些动画可以控制对象组件属性的任何值,例如位置、颜色等。在我们的情况下,我们想要动画推车轨道的位置属性,使其在给定时间内从起点到终点。为了做到这一点,请执行以下操作:

  1. 选择DollyCart1对象。

  2. 打开动画(而不是动画师)窗口,方法是转到窗口 | 动画 | 动画

  3. 单击动画窗口中心的创建按钮。记住在选择推车(而不是轨道)时执行此操作:图 12.36 – 创建自定义动画剪辑

图 12.36 – 创建自定义动画剪辑

  1. 完成此操作后,系统将提示您在某个位置保存动画剪辑。我建议您在项目中(在Assets文件夹内)创建一个Animations文件夹,并将其命名为IntroDollyTrack

如果你注意到,推车现在有一个带有创建的动画控制器的动画师组件,其中包含我们刚刚创建的动画。与任何动画剪辑一样,您需要将其应用到具有动画控制器的对象上;自定义动画也不例外。所以,动画窗口为您创建了它们。

在此窗口中进行动画操作包括在给定时刻指定其属性的值。在我们的情况下,我们希望在动画的开始时在时间轴的第 0 秒处为0,并在动画结束时在第5秒处为240。我选择了240,因为这是我的手推车的最后可能位置,但这取决于您的手推车轨道的长度。只需测试一下您的最后可能位置是什么。此外,我选择第5秒,因为我觉得这是动画的正确长度,但随时可以根据需要进行更改。现在,在动画的05秒之间发生的任何事情都是0240值的插值,这意味着在2.5秒时,值为120。动画始终包括在不同时刻对对象的不同状态进行插值。

为了做到这一点,执行以下操作:

  1. 动画窗口中,单击记录按钮(位于左上角的红色圆圈)。这将使 Unity 检测对象的任何更改并将其保存到动画中。记得在选择手推车时进行此操作。

  2. 设置1,然后设置为0。将其更改为任何值,然后再次更改为0将创建一个关键帧,这是动画中的一个点,表示在0秒时,我们希望0。如果值已经为0,则首先将其设置为任何其他值。您会注意到位置属性已添加到动画中:图 12.37 - 在将位置值更改为 0 后,记录模式下的动画

图 12.37 - 在将位置值更改为 0 后,记录模式下的动画

  1. 使用鼠标滚轮,将时间轴向右缩小到顶部栏的5秒:图 12.38 - 显示 5 秒的动画窗口时间轴

图 12.38 - 显示 5 秒的动画窗口时间轴

  1. 单击时间轴顶部的5秒标签,将播放头定位到该时刻。这将定位我们在该时刻进行的下一个更改。

  2. 设置240。记得将动画窗口设置为记录模式:图 12.39 - 在动画的第 5 秒创建一个值为 240 的关键帧

图 12.39 - 在动画的第 5 秒创建一个值为 240 的关键帧

  1. 点击CM vcam2左上角的播放按钮,它处于独奏模式。

现在,如果我们点击播放,动画将开始播放,但这并不是我们想要的。在这种情况下,想法是将过场动画的控制权交给过场动画系统 Timeline,因为这个动画不是我们需要在过场动画中进行排序的唯一内容。防止Animator组件自动播放我们创建的动画的一种方法是在控制器中创建一个空动画状态,并通过以下方式将其设置为默认状态:

  1. 搜索我们创建动画时创建的动画控制器并打开它。如果找不到它,只需选择手推车,然后双击我们游戏对象的Animator组件的Controller属性以打开资产。

  2. 在控制器中的空状态上右键单击,然后选择创建状态 | 。这将在状态机中创建一个新状态,就好像我们创建了一个新动画,但这次是空的:图 12.40 - 在动画控制器中创建一个空状态

图 12.40 - 在动画控制器中创建一个空状态

  1. 右键单击新状态,然后单击设置为层默认状态。状态应变为橙色:图 12.41 - 将控制器的默认动画更改为空状态

图 12.41 - 将控制器的默认动画更改为空状态

  1. 现在,如果点击播放,由于我们手推车的默认状态为空,不会播放任何动画。

现在我们已经创建了我们的摄像机动画,让我们开始创建一个通过时间轴从 intro 片段摄像机切换到玩家摄像机的片段。

对我们的 intro 片段进行排序

时间轴已经安装在您的项目中,但是如果您转到时间轴的包管理器,您可能会看到一个“更新”按钮,以获取最新版本,如果您需要一些新功能。在我们的情况下,我们将保留包含在我们项目中的默认版本(在撰写本书时为 1.3.4)。

我们要做的第一件事是创建一个片段资产和一个负责播放它的场景中的对象。要做到这一点,请按照以下步骤进行:

  1. 使用“GameObject” | “Create Empty”选项创建一个空的 GameObject。

  2. 选择空对象并将其命名为“导演”。

  3. 转到“窗口” | “排序” | “时间轴”以打开“时间轴”编辑器。

  4. 在“导演”对象被选中时,单击“时间轴”窗口中间的“创建”按钮,将该对象转换为片段播放器(或导演)。

  5. 完成此操作后,将弹出一个窗口询问您保存文件。这个文件将是片段或时间轴;每个片段将保存在自己的文件中。将其保存在项目中的Cutscenes文件夹中(Assets文件夹)。

  6. 现在,您可以看到“导演”对象具有“可播放导演”组件,并且在“可播放”属性中设置了上一步保存的“Intro”片段资产,这意味着这个片段将由导演播放:

图 12.42 - 可播放导演准备播放 Intro 时间轴资产

图 12.42 - 准备播放 Intro 时间轴资产的可播放导演

现在我们已经准备好使用时间轴资产进行工作,让我们让它排序动作。首先,我们需要排序两件事 - 首先是我们在上一步中做的 cart 位置动画,然后是 dolly 轨道摄像机(CM vcam2)和玩家摄像机(CM vcam1)之间的摄像机切换。正如我们之前所说,片段是在给定时刻执行的一系列动作,为了安排动作,您需要轨道。在时间轴中,我们有不同类型的轨道,每种轨道都允许您在特定对象上执行某些动作。我们将从动画轨道开始。

动画轨道将控制特定对象播放哪个动画;我们需要为每个要进行动画处理的对象创建一个轨道。在我们的情况下,我们希望 dolly 轨道播放我们创建的“Intro”动画,所以让我们这样做:

  1. 右键单击时间轴编辑器的左侧并单击“动画轨道”创建动画轨道:图 12.43 - 创建动画轨道

图 12.43 - 创建动画轨道

  1. 选择“导演”对象并检查检查器窗口中“可播放导演”组件的“绑定”列表。

  2. 拖动“Cart”对象以指定我们希望动画轨道控制其动画:图 12.44 - 使动画轨道控制 dolly cart 动画

图 12.44 - 使动画轨道控制 dolly cart 动画

重要提示:

时间轴是一个通用资产,可以应用到任何场景,但是由于轨道控制特定对象,您需要在每个场景中手动绑定它们。在我们的情况下,我们有一个期望控制单个动画师的动画轨道,因此在每个场景中,如果我们想应用这个片段,我们需要将特定的动画师拖放到“绑定”列表中。

  1. 将我们创建的“Intro”动画资产拖放到“时间轴”窗口中的动画轨道中。这将在轨道中创建一个剪辑,显示动画将播放的时间和持续时间。您可以将许多动画拖放到轨道中,以便在不同时刻对不同动画进行排序;但是现在,我们只需要这一个:图 12.45 - 使动画轨道播放 intro 剪辑

图 12.45 – 使动画师轨道播放介绍剪辑

  1. 你可以拖动动画来改变你想要它播放的确切时刻。将它拖到轨道的开头。

  2. 点击时间轴窗口左上角的播放按钮来查看它的运行情况。你也可以手动拖动时间轴窗口中的白色箭头来查看不同时刻的过场动画:

图 12.46 – 播放时间轴并拖动播放头

图 12.46 – 播放时间轴并拖动播放头

重要提示:

请记住,你不需要使用时间轴来播放动画。在这种情况下,我们是这样做的,以便精确控制我们希望动画播放的时刻。你也可以使用脚本来控制动画师。

现在,我们将使我们的介绍时间轴资产告诉CinemachineBrain组件(主摄像头)在过场动画的每个部分时使用哪个摄像头,一旦摄像头动画结束就切换到玩家摄像头。我们将创建第二个轨道—Cinemachine 轨道—专门用于使特定的CinemachineBrain组件在不同的虚拟摄像头之间切换。要做到这一点,请按照以下步骤进行:

  1. 右键单击动画轨道下方的空白处,然后单击Cinemachine 轨道。请注意,你可以安装不带 Cinemachine 的时间轴,但在这种情况下,这种轨道不会显示出来:图 12.47 – 创建新的 Cinemachine 轨道

图 12.47 – 创建新的 Cinemachine 轨道

  1. Playable Director组件的Bindings列表中,将主摄像头拖到Cinemachine 轨道,以使该轨道控制在过场动画的不同时刻哪个虚拟摄像头将控制主摄像头:图 12.48 – 使 Cinemachine 轨道控制我们场景的主摄像头

图 12.48 – 使 Cinemachine 轨道控制我们场景的主摄像头

  1. 下一步指示了时间轴的特定时刻将使用哪个虚拟摄像头。为此,我们的 Cinemachine 轨道允许我们将虚拟摄像头拖到其中,这将创建虚拟摄像头剪辑。按顺序将CM vcam2CM vcam1拖到 Cinemachine 轨道中:图 12.49 – 拖动虚拟摄像头到 Cinemachine 轨道

图 12.49 – 拖动虚拟摄像头到 Cinemachine 轨道

  1. 如果你点击播放按钮或者只是拖动时间轴播放头,你可以看到当播放头到达第二个虚拟摄像头剪辑时,活动虚拟摄像头是如何改变的。记得在游戏视图中查看。

  2. 如果你将鼠标放在剪辑的末端附近,会出现一个调整大小的光标。如果你拖动它们,你可以调整剪辑的持续时间。在我们的情况下,我们需要将CM vcam2剪辑的长度与Cart动画剪辑匹配,然后通过拖动将CM vcam1放在其末端,这样当手推车动画结束时摄像头就会激活。在我的情况下,它们已经是相同的长度,但是尝试改变一下也是练习。另外,你可以使CM vcam1剪辑变短;我们只需要它播放几个时刻来执行摄像头切换。

  3. 你也可以让剪辑有一点重叠,以使两个摄像头之间有一个平滑的过渡,而不是一个突然的切换,这看起来会很奇怪:

图 12.50 – 调整大小和重叠剪辑以插值它们

图 12.50 – 调整大小和重叠剪辑以插值它们

如果你等待完整的过场动画结束,你会注意到在最后,CinemachineBrain组件会选择具有最高优先级值的虚拟摄像机。我们可以更改虚拟摄像机的优先级属性,以确保CM vcam1(玩家摄像机)始终是最重要的,或者将Playable Director组件的包裹模式设置为保持,这将保持一切,就像时间轴的最后一帧指定的那样。

在我们的案例中,我们将使用后一种选项来测试时间轴特定的功能:

图 12.51 - 包裹模式设置为保持模式

图 12.51 - 包裹模式设置为保持模式

大多数不同类型的轨道都遵循相同的逻辑;每个轨道将控制特定对象的特定方面,使用剪辑在设定的时间内执行。我鼓励你测试不同的轨道,看看它们的作用,比如激活,它可以在过场动画期间启用和禁用对象。记住,你可以在包管理器中查看时间轴包的文档。

总结

在本章中,我们介绍了 Unity 提供的不同动画系统,以满足不同的需求。我们讨论了导入角色动画并使用动画控制器控制它们的方法。我们还看到了如何制作可以根据游戏当前情况(如玩家位置)做出反应的摄像机,或者在过场动画中使用的摄像机。最后,我们看了时间轴和动画系统如何为游戏创建开场过场动画。这些工具对于让我们团队中的动画师直接在 Unity 中工作非常有用,而无需整合外部资产(除了角色动画),也可以避免程序员创建重复的脚本来创建动画,从而节省时间。

现在,你可以在 Unity 中导入和创建动画剪辑,并将它们应用到游戏对象上,使它们根据剪辑移动。此外,你还可以将它们放置在时间轴序列中进行协调,并为游戏创建过场动画。最后,你可以创建动态摄像机在游戏中或过场动画中使用。

到目前为止,我们已经讨论了许多 Unity 系统,允许我们在不编码的情况下开发游戏的不同方面,但迟早需要编写脚本。Unity 提供了通用工具来处理通用情况,但我们游戏独特的玩法通常需要手动编码。在下一章中,也就是第三部分的第一章,我们将开始学习如何使用 C#在 Unity 中编码。

第十三章:使用 C#介绍 Unity 脚本

Unity 有很多内置工具来解决游戏开发中最常见的问题,就像我们迄今所见过的那些问题。即使是同一类型的两个游戏也有各自的小差异,使得游戏独一无二,而 Unity 无法预见到这一点,这就是为什么我们需要脚本。通过编码,我们可以以多种方式扩展 Unity 的功能,以实现我们需要的确切行为,而这一切都是通过一种众所周知的语言——C#。我们将介绍如何使用 C#脚本创建自定义组件。

这里我要指出的一件事是,本章主要是对 Unity 的 C#脚本基础知识的回顾,但在其中的某一节中,我将解释一些针对有经验的程序员的高级技巧。因此,如果你有编程经验但不熟悉 Unity,请尽量不要跳过本章。

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

  • 创建 C#脚本

  • 使用事件和指令

我们将创建我们自己的 Unity 组件,学习类的基本结构以及我们可以执行操作和暴露属性以进行配置的方式。让我们从讨论脚本创建的基础知识开始。

创建 C#脚本

本书面向具有一定编程知识的读者,但在本节中,我们将讨论 C#脚本结构,以确保你对我们将在接下来的章节中编写的行为有坚实的基础。

在本节中,我们将讨论以下脚本创建概念:

  • 初始设置

  • 创建一个基于 MonoBehaviour 的类

  • 添加字段

我们将创建我们的第一个 Unity 脚本,这将用于创建我们的组件,讨论所需的工具,并探讨如何将我们的类字段暴露给编辑器。让我们从脚本创建的基础知识开始。

初始设置

在创建我们的第一个脚本之前,有一件事需要考虑,那就是 Unity 如何编译代码。在编码时,我们习惯于使用集成开发环境IDE),这是一个用于创建我们的代码并编译或执行它的程序。在 Unity 中,我们只会将 IDE 作为一个工具来轻松创建带有着色和自动补全的脚本,因为 Unity 没有自定义的代码编辑器,如果你以前从未编写过代码,这些对初学者来说是宝贵的工具。脚本将被创建在 Unity 项目中,如果进行了任何更改,Unity 将检测并编译它们,因此你不需要在 IDE 中进行编译。不用担心——你仍然可以在这种方法中使用断点。

我们可以使用 Visual Studio、Visual Studio Code、Rider 或者你喜欢使用的任何 C# IDE,但当你安装 Unity 时,你可能会看到一个选项自动安装 Visual Studio,这样你就可以拥有一个默认的 IDE。这将安装 Visual Studio 的免费版本,所以不用担心许可证问题。如果你的电脑上没有 IDE,并且在安装 Unity 时没有勾选 Visual Studio 选项,你可以这样做:

  1. 打开Unity Hub

  2. 转到安装部分。

  3. 点击 Unity 版本右上角的三个点,然后点击添加模块图 13.1 – 将模块添加到 Unity 安装中

图 13.1 – 将模块添加到 Unity 安装中

  1. 勾选Visual Studio选项;选项的描述将根据你使用的 Unity 版本而有所不同。

  2. 点击右下角的下一步按钮:图 13.2 – 安装 Visual Studio

图 13.2 – 安装 Visual Studio

  1. 等待操作结束。这可能需要几分钟的时间。

如果你有自己喜欢的 IDE,你可以自行安装并配置 Unity 来使用它。如果你有能力支付或者你是一名教师或学生(在这些情况下是免费的),我推荐 Rider。它是一个功能强大的 IDE,拥有许多你会喜欢的 C#和 Unity 功能;然而,对于这个练习来说并不是必不可少的。为了设置 Unity 使用自定义 IDE,你可以这样做:

  1. 打开项目。

  2. 转到编辑器的顶部菜单中的编辑 | 首选项

  3. 从左侧面板中选择外部工具菜单。

  4. 从外部脚本编辑器中选择您喜欢的 IDE;Unity 将自动检测到支持的 IDE:图 13.3 – 选择自定义 IDE

图 13.3 – 选择自定义 IDE

  1. 如果在列表中找不到您的 IDE,可以使用**浏览...**选项,但通常需要使用此选项的 IDE 支持不是很好——但值得一试。

最后,一些 IDE,如 Visual Studio、Visual Studio Code 和 Rider,具有 Unity 集成工具,您需要在项目中安装这些工具,这是可选的,但可能很有用。通常,Unity 会自动安装这些工具,但如果您想确保它们已安装,请执行以下操作:

  1. 打开包管理器窗口 | 包管理器)。

  2. 搜索列表中的您的 IDE,或者使用搜索栏过滤列表。在我的情况下,我使用了 Rider,并且我可以找到一个名为JetBrains Rider Editor的包:图 13.4 – 自定义 IDE 编辑器扩展安装——在这种情况下是 Rider

图 13.4 – 自定义 IDE 编辑器扩展安装——在这种情况下是 Rider

  1. 通过查看包管理器底部右侧的按钮来检查是否安装了 IDE 集成包。如果看到安装更新按钮,请单击它,但如果显示已安装,则一切都设置好了。

现在我们已经配置了 IDE,让我们创建我们的第一个脚本。

创建基于 MonoBehaviour 的类

C#是一种面向对象的语言,在 Unity 中也是如此。每当我们想要扩展 Unity 时,我们都需要创建自己的类——一个包含我们想要添加到 Unity 的指令的脚本。如果我们想要创建自定义组件,我们需要创建一个从MonoBehaviour继承的类,这是每个自定义组件的基类。

我们可以直接在 Unity 项目中使用编辑器创建 C#脚本文件,并且可以将它们排列在其他资产文件夹旁边的文件夹中。创建脚本的最简单方法是按照以下步骤进行:

  1. 选择要创建组件的任何游戏对象。由于我们只是在测试这个功能,所以选择任何对象。

  2. 单击检查器底部的添加组件按钮,并查找新脚本选项,该选项显示在单击添加组件后的列表底部:图 13.5 – 新脚本选项

图 13.5 – 新脚本选项

  1. MyFirstScript中,但是对于您将用于游戏的脚本,请尝试输入描述性名称,而不管长度如何:图 13.6 – 命名脚本

图 13.6 – 命名脚本

重要提示:

建议您使用帕斯卡命名法来命名脚本。在帕斯卡命名法中,玩家射击功能的脚本将被称为PlayerShoot。名称的每个单词的第一个字母都是大写的,而且不能使用空格。

  1. 您可以看到在项目视图中创建了一个名为脚本的新资产。请记住,每个组件都有自己的资产,我建议您将每个组件放在Scripts文件夹中:图 13.7 – 脚本资产

图 13.7 – 脚本资产

  1. 现在,您还会看到您的游戏对象在检查器窗口中有一个新的组件,该组件的名称与您的脚本相同。因此,您现在已经创建了您的第一个组件类:

图 13.8 – 我们的脚本添加到游戏对象

图 13.8 – 我们的脚本添加到游戏对象

现在我们已经创建了一个component类,请记住类不是组件本身。它是组件应该是什么的描述 - 组件应该如何工作的蓝图。要实际使用组件,我们需要通过创建基于该类的组件来实例化它。每次我们使用编辑器向对象添加组件时,我们都在实例化它。通常,我们不使用 new 来实例化,而是使用编辑器或专门的函数。现在,您可以像使用Add Component按钮一样添加您的组件到任何其他组件中,并在检视器窗口中的Scripts类别中查找它或通过名称搜索它:

图 13.9 - 在 Scripts 类别中添加自定义组件

图 13.9 - 在 Scripts 类别中添加自定义组件

这里需要考虑的一点是,我们可以将相同的组件添加到多个游戏对象中。我们不需要为每个使用该组件的游戏对象创建一个类。我知道这是基本的程序员知识,但请记住我们正在尝试回顾基础知识。在下一章中,我们将研究更有趣的主题。

现在我们有了我们的组件,让我们探索它的外观,并通过以下方式进行类结构回顾:

  1. 在 Project View 中找到脚本资源并双击打开。记住它应该位于您之前创建的Scripts文件夹中。

  2. 等待 IDE 打开;这可能需要一段时间。当您看到您的脚本代码及其关键字正确着色时,您将知道 IDE 已完成初始化,这取决于所需的 IDE。在 Rider 中,它看起来如下截图。在我的情况下,我知道 Rider 已经完成初始化,因为 MonoBehaviour 类型和脚本名称都着色相同:图 13.10 - 在 Rider IDE 中打开的新脚本

图 13.10 - 在 Rider IDE 中打开的新脚本

  1. 前三行 - 以using关键字开头的行 - 包括常见的命名空间。命名空间就像代码容器,也就是在这种情况下,由他人创建的代码(如 Unity,C#创建者等)。我们将经常使用命名空间来简化我们的任务;它们已经包含了我们将使用的解决算法。我们将根据需要添加和删除using组件;在我的情况下,Rider 建议前两个using组件是不必要的,因为我没有在其中使用任何代码,所以它们是灰色的。但是现在,保留它们,因为您将在本书的后面章节中使用它们。记住,它们应该始终位于类的开头:图 13.11 - using 部分

图 13.11 - using 部分

  1. 下一行,以public class开头的行,是我们声明正在创建一个继承自MonoBehaviour的新类的地方,这是每个自定义组件的基类。我们知道这是因为它以:MonoBehaviour结尾。您可以看到代码的其余部分位于该行的下方括号内,这意味着括号内的代码属于该组件:

图 13.12 - MyFirstScript 类定义继承自 MonoBehaviour

图 13.12 - MyFirstScript 类定义继承自 MonoBehaviour

现在我们有了第一个组件,让我们编辑它,从字段开始。

添加字段

当我们添加Rigidbody或不同类型的碰撞体作为组件时,仅仅添加组件是不够的。我们需要正确配置它们以实现我们需要的确切行为。例如,RigidbodyMass属性来控制物体的重量,碰撞体有Size属性来控制它们的形状。这样,我们可以在不同的场景中重复使用相同的组件,避免重复相似组件。使用Box碰撞体,我们可以通过更改大小属性来表示正方形或矩形框。我们的组件也不例外;如果我们有一个移动物体的组件,并且我们希望两个物体以不同的速度移动,我们可以使用相同的组件进行不同的配置。

每个配置都是一个类字段,一个特定类型的变量,我们可以在其中保存参数的值。我们可以创建可以在编辑器中编辑的类字段的两种方式——通过将字段标记为public,但违反封装原则,或者通过创建一个私有字段并使用属性公开它。现在,我们将涵盖这两种方法,但如果您不熟悉面向对象编程(OOP)概念,比如封装,我建议您使用第一种方法。

假设我们正在创建一个移动脚本。我们将使用第一种方法添加一个可编辑的数字字段,表示速度,即通过添加public字段。我们将按照以下步骤进行操作:

  1. 双击打开脚本,就像之前一样。

  2. 在类括号内,但在其中的任何括号之外,添加以下代码:图 13.13 - 在我们的组件中创建一个速度字段

图 13.13 - 在我们的组件中创建一个速度字段

重要提示:

public关键字指定变量可以在类的范围之外被看到和编辑。代码中的float部分表示变量使用十进制数类型,speed是我们为字段选择的名称——这可以是任何您想要的。您可以使用其他值类型来表示其他类型的数据,比如bool表示复选框或布尔值,string表示文本。

  1. 要应用更改,只需在 IDE 中保存文件(通常通过按下Ctrl + Scommand + S),然后再返回 Unity。当您这样做时,您会注意到编辑器底部右侧有一个小加载轮,表示 Unity 正在编译代码。直到加载轮完成,您才能测试更改。请记住,Unity 将编译代码;不要在 IDE 中编译:图 13.14 - 加载轮

图 13.14 - 加载轮

  1. 编译完成后,您可以在检视器窗口中看到您的组件,Speed变量应该在那里,允许您设置您想要的速度。当然,现在这些变量什么都不做。Unity 不会根据变量的名称识别您的意图;我们需要以某种方式设置它以供后续使用,但我们稍后会这样做:图 13.15 - 一个用于编辑组件稍后将使用的公共字段

图 13.15 - 一个用于编辑组件稍后将使用的公共字段

  1. 尝试将相同的组件添加到其他对象,并设置不同的速度。这将向您展示不同游戏对象中的组件是独立的,允许您通过不同的设置更改它们的一些行为。

定义属性的第二种方法类似,但是我们创建一个私有字段,鼓励封装,并使用SerializeField属性公开它,如下面的屏幕截图所示。这些屏幕截图展示了两种方法——两种方法都会产生相同的结果;唯一的区别是样式。使用最符合您编码标准的方法:

图 13.16 - 在检视器窗口中公开私有属性的两种方法

图 13.16 - 在检视器窗口中公开私有属性的两种方法

如果你不熟悉面向对象编程的封装概念,只需使用第一种方法,对初学者来说更灵活。如果你创建了一个private字段,它将不可被其他脚本访问,因为SerializeField属性只会将变量暴露给编辑器。记住,Unity 不允许你使用构造函数,所以设置初始数据和注入依赖的唯一方法是通过序列化的私有字段或公共字段,并在编辑器中设置它们(或使用依赖注入框架,但这超出了本书的范围)。为简单起见,我们将在本书的大部分练习中使用第一种方法。

如果你愿意,尝试创建其他类型的变量,并查看它们在检视器中的样子。尝试用boolstring替换float,就像之前建议的那样。现在我们知道如何通过数据配置我们的组件,让我们使用这些数据来创建一些行为。

使用事件和指令

现在我们有了一个脚本,我们准备对其进行一些操作。在本章中,我们不会实现任何有用的东西,但我们会解决一些概念,以便在接下来的章节中为我们即将创建的脚本添加一些类型的行为。

在本节中,我们将涵盖以下概念:

  • 事件和指令

  • 在指令中使用字段

  • 常见的初学者错误

我们将探索 Unity 事件系统,它将允许我们通过执行 Unity 函数来响应这些情况。这些函数也会受到编辑器的值的影响,我们脚本中暴露的字段将是可配置的。最后,我们将讨论常见的脚本错误以及如何解决它们。让我们先介绍 Unity 事件的概念。

事件和指令

Unity 允许我们以因果关系的方式创建行为,通常称为事件系统。事件是 Unity 正在监视的情况,例如,当两个对象发生碰撞或被销毁时,Unity 会告诉我们这种情况,从而允许我们根据我们的需求做出反应。例如,当玩家与子弹发生碰撞时,我们可以减少玩家的生命。在这里,我们将探索如何监听这些事件并通过使用一些简单的操作来测试它们。

如果你习惯于事件系统,你会知道它们通常要求我们订阅某种监听器或委托,但在 Unity 中,有一种更简单的方法可用。我们只需要为我们正在寻找的事件编写确切的函数——我是说确切的。如果名称中的一个字母大小写不正确,它将不会执行,也不会引发任何警告。这是最常见的初学者错误,所以要注意。

在 Unity 中有很多事件或消息可以监听,所以让我们从最常见的一个开始——Update。这个事件会告诉你当 Unity 希望你更新你的对象时,根据你的行为目的而定;有些行为不需要它们。Update逻辑通常是需要不断执行的东西;更准确地说,是在每一帧中。记住,每个游戏就像一部电影——屏幕快速切换的一系列图像,看起来就像我们有连续的运动。在Update事件中常见的操作是让对象移动一点,通过这样做,每一帧都会让你的对象不断移动。

我们将在以后学习关于Update和其他事件或消息可以做的事情。现在,让我们专注于如何使我们的组件至少监听这个事件。实际上,基本组件已经带有两个准备好使用的事件函数,一个是Update,另一个在脚本中。如果你不熟悉 C#中函数的概念,我们指的是下面截图中已经包含在我们脚本中的代码片段。试着在你的脚本中找到它。

图 13.17 - 一个名为 Update 的函数,将在每一帧中执行

图 13.17 - 一个名为 Update 的函数,将在每一帧中执行

您会注意到void Update()行上方通常有一行绿色的文本(取决于 IDE)-这称为注释。这些基本上被 Unity 忽略。它们只是您可以留给自己的注释,必须始终以//开头,以防止 Unity 尝试执行它们并失败。我们将在以后使用这个来临时禁用代码行。

现在,为了测试这是否真的有效,让我们添加一个将一直执行的指令。没有比print更好的测试函数了。这是一个简单的指令,告诉 Unity 在控制台中打印一条消息,开发人员可以在其中看到各种消息,以检查一切是否正常工作。用户永远不会看到这些消息。它们类似于经典的日志文件,有时当游戏出现问题并且您正在报告问题时,开发人员会要求您提供这些日志文件。

为了使用函数测试事件,请执行以下操作:

  1. 通过双击打开脚本。

  2. 为了测试,添加print("test");到事件函数中。在下面的屏幕截图中,您可以看到如何在Update事件中执行此操作的示例。记得精确写出指令,包括正确的大小写,空格和引号符号:图 13.18 - 在所有帧中打印消息

图 13.18 - 在所有帧中打印消息

  1. 保存文件,转到 Unity,并播放游戏。

重要提示:

在从 IDE 切换回 Unity 之前记得保存文件。这是 Unity 知道您的文件已更改的唯一方式。一些 IDE,如 Rider,会自动为您保存文件,但我不建议您使用自动保存,至少在大型项目中不要这样做(您不希望在有很多脚本的项目中意外重新编译未完成的工作;这需要太长时间)。

  1. 查找控制台选项卡并选择它。这通常可以在项目视图选项卡旁边找到。如果找不到,请转到窗口 | 常规 | 控制台,或按下Ctrl + Shift + C(macOS 上为command + shift + C)。

  2. 您会看到在控制台选项卡的每一帧中都打印出"test"的许多消息。如果您没有看到这个,请记得在播放游戏之前保存脚本文件。

  3. 让我们也测试Start函数。在其中添加print("test Start");,保存文件,并播放游戏。完整的脚本应如下所示:

图 13.19 - 测试开始和更新函数的脚本

图 13.19 - 测试开始和更新函数的脚本

如果现在检查控制台并向上滚动,您会看到一个单独的"test Start"消息和许多随后的"test"消息。您可以猜到,Start事件告诉您游戏已经开始,并允许您执行需要在游戏开始时仅执行一次的代码。我们将在本书的后面使用这个。

对于void Update()语法,我们将告诉 Unity,该行下方括号中包含的内容是一个将在所有帧中执行的函数。重要的是将打印指令放在Update括号内(类的括号内)。此外,print函数期望在其括号内接收文本,称为参数或参数,并且 C#中的文本必须用引号括起来。最后,UpdateStart等函数内的所有指令必须以分号结束。

在这里,我挑战你尝试添加另一个名为OnDestroy的事件,使用print 函数来发现它何时执行。一个小建议是播放并停止游戏,然后查看控制台底部以测试这个。

对于高级用户,如果您的 IDE 允许,您还可以使用断点。断点允许您在执行特定代码行之前完全冻结 Unity,以查看我们的字段数据随时间如何变化并检测错误。在这里,我将向您展示在 Rider 中使用断点的步骤,但 Visual Studio 版本应该类似:

  1. 单击要添加断点的行左侧的垂直条:图 13.20 - 打印指令中的断点

图 13.20 - 打印指令中的断点

  1. 转到运行 | 附加到 Unity 进程(在 Visual Studio 中,转到调试 | 附加 Unity 调试器。请记住,您需要 Visual Studio Unity 插件和包管理器的 Visual Studio 集成包):图 13.21 - 攻击我们的 IDE 与 Unity 进程

图 13.21 - 攻击我们的 IDE 与 Unity 进程

  1. 从列表中查找您想要测试的特定 Unity 实例。列表将显示其他已打开的编辑器或正在执行调试构建。

停止调试过程不会关闭 Unity。它只会将 IDE 与编辑器分离。

现在我们已经创建了字段和指令,让我们将它们结合起来制作可配置的组件。

在指令中使用字段

我们已经创建了字段来配置组件的行为,但到目前为止我们还没有使用它们。我们将在下一章中创建有意义的组件,但我们经常需要做的一件事是使用我们创建的字段来改变对象的行为。到目前为止,我们还没有真正使用我们创建的speed字段。然而,遵循测试代码是否工作的想法(也称为调试),我们可以学习如何使用字段内的数据与函数一起测试值是否符合预期,并根据字段的值改变控制台中print的输出。

在我们当前的脚本中,我们的speed值在运行时不会改变。然而,举个例子,如果您正在创建一个具有护盾伤害吸收的生命系统,并且您想要测试减少的伤害计算是否正常工作,您可能希望将计算值打印到控制台并检查它们是否正确。这里的想法是用字段替换print函数内的固定消息。当您这样做时,print将在控制台中显示字段的值。因此,如果您在speed中设置了5的值并将其打印出来,您将在控制台中看到大量显示5的消息,并且print函数的输出由字段控制。为了测试这一点,您Update函数中的print消息应该如下所示:

图 13.22 - 使用字段作为打印函数参数

图 13.22 - 使用字段作为打印函数参数

如您所见,我们只是将字段的名称放在引号中。如果您使用引号,将打印一个"speed"消息。在其他情况下,您可以在一些移动函数中使用speed值来控制移动速度,或者您可以创建一个名为"fireRate"的字段(字段使用驼峰命名法而不是帕斯卡命名法,第一个字母小写)来控制一颗子弹和下一颗子弹之间的冷却时间:

图 13.23 - 打印当前速度

图 13.23 - 打印当前速度

重要提示:

您可以看到我的编辑器被涂成红色,这是因为我配置它在游戏中播放时变成红色,以便轻松检测到。您可以通过转到编辑 > 首选项 > 颜色并更改播放模式色调来实现这一点。

有了所有这些,我们现在有了开始创建实际组件所需的工具。在继续之前,让我们回顾一些常见的错误,如果这是您第一次在 C#中创建脚本,您可能会遇到这些错误。

常见初学者错误

如果您是一名经验丰富的程序员,我敢打赌您对这些非常熟悉,但让我们回顾一下在开始脚本编写时会让您浪费大量时间的常见错误。其中大部分是由于未精确复制所示代码引起的。如果代码中有错误,Unity 将在控制台中显示红色消息,并且不允许您运行游戏,即使您没有使用该脚本。因此,永远不要留下任何未完成的事情。

让我们从一个经典错误开始,即缺少分号,这导致了许多程序员的笑话和段子。所有字段和大多数函数内的指令(如print)在调用时都需要在末尾加上分号。如果不加分号,Unity 将显示错误,例如下图左侧截图中的控制台中的错误。您还会注意到下图右侧的截图中还有一个糟糕的代码示例,IDE 显示了一个红色图标,表明该位置有问题:

图 13.24 - IDE 和 Unity 控制台提示的打印行错误

图 13.24 - IDE 和 Unity 控制台提示的打印行错误

您会注意到错误显示了确切的脚本(MyFirstScript.cs),代码的确切行号(在本例中为18),通常还有一个描述性消息 - 在本例中为;[分号]预期。您只需双击错误,Unity 将打开 IDE 并突出显示有问题的行。您甚至可以单击堆栈中的链接,跳转到您想要的堆栈行。

我已经提到了为每个指令使用确切大小写非常重要的原因。然而,根据我教授初学者的经验,我需要更加强调这一特定方面。这种情况可能发生的第一个场景是在指令中。在下面的截图中,您可以看到一个糟糕编写的print函数的样子 - 也就是说,您可以看到控制台将显示的错误以及 IDE 将建议存在问题的方式。首先,在 Rider 的情况下,指令被标记为红色,表示该指令未被识别(在 Visual Studio 中,它将显示为红色线)。然后,错误消息表示Print在当前上下文中不存在,这意味着 Unity(或实际上是 C#)不认识任何名为Print的指令。在另一种类型的脚本中,大写的Print可能是有效的,但在常规组件中不是有效的,这就是为什么当前上下文澄清存在的原因:

图 13.25 - 写指令错误时的错误提示

图 13.25 - 写指令错误时的错误提示

现在,如果您使用错误的大小写编写事件,情况就更糟了。您可以创建诸如StartUpdate之类的函数,并为其他目的使用任何名称。编写updatestart是完全有效的,因为 C#会认为您将使用这些函数而不是事件作为常规函数。因此,不会显示任何错误,并且您的代码将无法正常工作。尝试编写update而不是Update,看看会发生什么:

图 13.26 - Update 函数中的错误大小写将编译函数但不会执行

图 13.26 - Update 函数中的错误大小写将编译函数但不会执行

另一个错误是将指令放在函数括号外,比如在类的括号内或外部。这样做将不会给函数提示,告诉它何时需要执行。因此,在Event函数外部的print函数是没有意义的,它会显示类似以下截图中的错误。这次,错误并不是非常描述性的。标识符预期表示 C#希望您创建一个函数或字段 - 可以直接放在类中的结构类型:

图 13.27 - 指令或函数调用放错位置

图 13.27 – 指令或函数调用放错位置

最后,另一个经典错误是忘记关闭开放的括号。如果你不关闭一个括号,C#就不知道一个函数在哪里结束,另一个函数在哪里开始,或者类函数在哪里结束。这可能听起来有些多余,但 C#需要完全定义。在下面的截图中,你可以看到这会是什么样子:

图 13.28 – 缺少闭合括号

图 13.28 – 缺少闭合括号

这个有点难以捕捉,因为代码中的错误显示在实际错误之后很久。这是由于 C#允许你在函数内部放置函数(不经常使用),所以 C#会在后面检测到错误,并要求你添加一个闭合括号。然而,由于我们不想把update放在Start内部,我们需要在Start的末尾修复错误。控制台中的错误消息会很具体,但是不要按照消息建议的位置放置闭合括号,除非你 100%确定该位置是正确的。

除了这些错误,你可能会遇到很多其他错误,但它们都是一样的。IDE 会显示提示,控制台会显示消息;你会随着时间学会它们。只要有耐心,因为每个程序员都会经历这个过程。还有其他类型的错误,比如运行时错误,编译时代码出现错误,由于某些错误配置而在执行时失败,或者最糟糕的是逻辑错误,你的代码编译和执行都没有错误,但却没有达到你的意图。

总结

在本章中,我们探讨了创建脚本时会用到的基本概念。我们讨论了类和实例的概念,以及它们必须继承自 MonoBehaviour 才能被 Unity 接受来创建我们自己的脚本。我们还看到了如何混合事件和指令来为对象添加行为,以及如何在指令中使用字段来自定义它们的功能。

我们刚刚探讨了 C#脚本的基础知识,以确保每个人都在同一起跑线上。然而,从现在开始,我们将假设你在某种编程语言中具有基本的编码经验,并且知道如何使用诸如ifforarray等结构。如果没有,你仍然可以阅读本书,并尝试用 C#入门书籍来补充你不理解的部分。

在下一章中,我们将开始看如何利用我们所学到的知识来创建移动和生成脚本。

第十四章:实现移动和生成

现在我们已经准备好开始编码了,让我们创建我们的第一个行为。我们将看到如何通过使用Transform组件来移动对象的基础知识,这将应用于我们的玩家的移动,子弹的恒定移动以及其他对象的移动。此外,我们还将看到如何在游戏过程中创建和销毁对象,例如玩家和敌人射击的子弹以及敌人波次生成器。这些操作可以在其他场景中使用,所以我们将探索一些来加强这个想法。

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

  • 实现移动

  • 实现生成

我们将开始编写脚本来执行先前提到的移动行为,然后我们将继续进行对象的创建和销毁。

实现移动

几乎游戏中的每个对象都以某种方式移动,玩家角色通过键盘移动,敌人通过 AI 移动,子弹简单地向前移动,等等。在 Unity 中有几种移动对象的方式,所以我们将从最简单的方式开始,即通过Transform组件。

在本节中,我们将探讨以下移动概念:

  • 通过 Transform 移动对象

  • 使用输入

  • 理解 Delta Time

首先,我们将探索如何在我们的脚本中访问 Transform 组件来驱动玩家的移动,然后根据玩家的键盘输入应用移动。最后,我们将探索 Delta Time 的概念,以确保在每台电脑上移动速度保持一致。我们将开始学习 Transform API 来掌握简单的移动。

通过 Transform 移动对象

Transform是一个持有对象的平移、旋转和缩放的组件,因此每个移动系统,如物理或路径查找,都会影响这个组件。无论如何,有时我们想以特定的方式移动一个对象,根据我们的游戏创建我们自己的脚本,它将处理我们需要的移动计算并修改 Transform 来应用它们。

这里暗示的一个概念是组件改变其他组件。在 Unity 中编码的主要方式是创建与其他组件交互的组件。在这里,想法是创建一个访问另一个组件并告诉它做某事的组件,这种情况下是移动。要创建一个告诉Transform移动的脚本,做如下操作:

  1. 创建并添加一个名为Player Movement的脚本到我们的角色。在这种情况下,它将是我们之前创建的动画机器人对象。记得在创建后将脚本移动到Scripts文件夹中:图 14.1 - 为角色创建一个玩家移动脚本

图 14.1 - 为角色创建一个玩家移动脚本

  1. 双击创建的脚本资源以打开 IDE 编辑代码。

  2. 我们正在移动,移动是每帧应用的,所以这个脚本只会使用update函数或方法,我们可以移除Start(移除未使用的函数是一个好习惯):图 14.2 - 一个只有 update 事件函数的组件

图 14.2 - 一个只有 update 事件函数的组件

  1. 要沿着对象的前向轴(Z 轴)移动我们的对象,将transform.Translate(0,0,1);行添加到update函数中,如下图所示。

重要提示

每个组件都继承了一个transform字段(具体来说是一个 getter),它是对放置组件的游戏对象的 Transform 的引用,它代表我们组件的兄弟 Transform。通过这个字段,我们可以访问 Transform 的Translate函数,它将接收要在 X、Y、Z 本地坐标中应用的偏移量:

图 14.3 - 一个简单的向前移动脚本

图 14.3 - 一个简单的向前移动脚本

  1. 保存文件并播放游戏以查看移动。

图 14.4 - 暂时禁用导演并增加玩家摄像机优先级

图 14.4 - 暂时禁用导演并增加玩家摄像机优先级

重要提示

我建议您暂时禁用可播放导演对象并增加 CM vcam1 的优先级,这将禁用引入过场动画并使角色跟随摄像机默认激活,减少测试游戏所需的时间。另一个选项是创建一个用于测试玩家移动的辅助场景,这实际上在真实项目中是做的,但现在,让我们保持简单。

您会注意到玩家移动得太快了,这是因为我们使用了固定的 1 米速度,而且因为update正在执行所有帧,所以我们每帧移动 1 米。在标准的 30 FPS 游戏中,玩家每秒移动 30 米,这太多了。我们可以通过添加一个“速度”字段并使用编辑器中设置的值来控制玩家速度,而不是固定的 1 的值。您可以在下一个截图中看到如何做到这一点,但请记住我们在上一章讨论的其他选项(使用 Serialize Field 属性):

图 14.5 - 创建速度字段并将其用作移动脚本的 Z 速度

图 14.5 - 创建速度字段并将其用作移动脚本的 Z 速度

现在,如果您保存脚本以应用更改并设置为0.1,但您可能需要另一个值(稍后会详细介绍):

图 14.6 - 设置每帧 0.1 米的速度

图 14.6 - 设置每帧 0.1 米的速度

您会注意到玩家会自动移动。现在让我们看看如何基于玩家输入(如键盘和鼠标输入)执行移动。

使用输入

与 NPC 不同,我们希望玩家的移动是由玩家的输入驱动的,基于他们按下的键,鼠标移动等。我们可以回想我们在第一章**从零开始设计游戏中设计的原始键映射,从下面的两个表中:

表 14.1 - 键盘映射

表 14.1 - 键盘映射

请查看以下表格中的鼠标映射:

表 14.2 - 鼠标映射

表 14.2 - 鼠标映射

重要提示

最新的 Unity 版本有一个新的输入系统,但在使用之前需要进行一些设置。现在我们将使用默认的输入系统来简化我们的脚本

要知道是否按下某个特定键,比如上箭头,我们可以使用Input.GetKey(KeyCode.W)这一行,它将返回一个布尔值,指示是否按下了KeyCode枚举中指定的键。我们可以更改键以检查KeyCode枚举值的更改,并将GetKey函数与“if”语句结合使用,使翻译仅在满足该条件时执行(当前按下该键时)。

重要提示

最新的 Unity 版本有一个新的输入系统,但在使用之前需要进行一些设置。现在我们将使用默认的输入系统来简化我们的脚本。

让我们通过以下方式开始实现键盘移动:

  1. 使前进运动仅在按下W键时执行,如下截图所示:图 14.7 - 仅在按下 W 键时执行移动

图 14.7 - 仅在按下 W 键时执行移动

  1. 我们可以通过更多的If语句添加其他移动方向。我们可以使用S向后移动,AD向左和向右移动,如下截图所示。请注意,当需要沿相反轴方向移动时,我们使用减号来反转速度:图 14.8 - 检查 W、A、S 和 D 键的压力

图 14.8 - 检查 W、A、S 和 D 键的压力

重要提示

记住,如果不使用括号的if语句,意味着只有if语句内部的一行将紧跟在if语句后面,也就是说,transform.Translate的调用。无论如何,在最终的代码中,我建议保留括号。

  1. 如果你还想考虑箭头键,可以在if语句中使用 OR,如下面的截图所示:图 14.9 - 检查 W、A、S、D 和箭头键的压力

图 14.9 - 检查 W、A、S、D 和箭头键的压力

  1. 保存更改并在播放模式下测试移动。

需要考虑的一点是,首先,我们可以通过配置输入管理器来将多个键映射到单个操作的另一种方式,输入管理器是可以创建动作映射的地方;其次,在撰写本文时,Unity 发布了一个实验性的新输入系统,将取代这个输入管理器。目前,我们将使用这个输入管理器,因为它足够简单,可以启动一个基本的游戏,而且实验性的 Unity 软件包可能存在错误或工作方式的变化。在复杂输入的游戏中,建议使用更高级的工具来进行控制。

现在,让我们实现鼠标控制。在这一部分,我们只会涵盖鼠标移动的旋转;下一部分我们会讨论射击子弹。在鼠标移动的情况下,我们可以得到一个值,表示鼠标水平或垂直移动的程度。这个值不是布尔值,而是一个数字,通常被称为轴的输入类型,这个数字将表示移动的强度和数字的符号表示方向。例如,如果 Unity 的"Mouse X"轴的值为 0.5,意味着鼠标以适度的速度向右移动,但如果值为-1,表示鼠标向左快速移动,如果没有移动,值为 0。游戏手柄的摇杆也是一样;Horizontal轴表示常见游戏手柄左摇杆的水平移动,所以如果玩家将摇杆完全向左拉,值将为-1。

我们可以创建自己的轴来映射其他常见游戏手柄的压力控制,但对于我们的游戏来说,默认的足够了。要检测鼠标移动,做如下操作:

  1. update中使用Input.GetAxis函数,紧挨着移动的if语句,如下面的截图所示,将这一帧的鼠标移动值存储到一个变量中:图 14.10 获取鼠标的水平移动

图 14.10 获取鼠标的水平移动

  1. 使用transform.Rotate函数来旋转角色。这个函数按 X、Y、Z 轴的顺序接收旋转的度数。在这种情况下,我们需要水平旋转,所以我们将使用鼠标移动值作为 Y 轴的旋转,如下面的截图所示:图 14.11 - 根据鼠标移动水平旋转对象

图 14.11 - 根据鼠标移动水平旋转对象

  1. 如果你保存并测试这个,你会注意到玩家会旋转,但速度很快或很慢,这取决于你的电脑。记住,这种值需要可配置,所以让我们在编辑器中创建一个rotationSpeed字段来配置玩家的速度:图 14.12 - 速度和旋转速度字段

图 14.12 - 速度和旋转速度字段

  1. 现在我们需要将鼠标移动值乘以速度,这样,根据rotationSpeed,我们可以增加或减少旋转的量。例如,如果我们将旋转速度设置为 0.5,将这个值乘以鼠标移动值将使对象以之前速度的一半旋转,如下面的截图所示:图 14.13 - 将鼠标移动乘以旋转速度

图 14.13 - 将鼠标移动乘以旋转速度

  1. 保存代码,回到编辑器设置旋转速度值。如果不这样做,对象就不会旋转,因为浮点类型字段的默认值是 0:图 14.14 – 设置旋转速度

图 14.14 – 设置旋转速度

  1. 您可能还注意到,由 Cinemachine 控制的摄像机可能需要延迟来适应新的玩家位置。您可以像我在下一个截图中所做的那样调整插值速度,以获得更灵敏的行为:

图 14.15 – 减少角色虚拟摄像机身体和瞄准部分的阻尼

图 14.15 – 减少角色虚拟摄像机身体和瞄准部分的阻尼

现在我们已经完成了我们的移动脚本,我们需要通过探索 Delta Time 的概念来完善它,使其在每台机器上都能工作。

理解 Delta Time

Unity 的更新循环以计算机的速度执行。您可以在 Unity 中指定所需的帧率,但实现这一点完全取决于您的计算机是否能达到这一点,这取决于许多因素,不仅仅是硬件,因此您不能期望始终具有一致的 FPS。您必须编写脚本来处理每种可能的情况。我们当前的脚本是以每帧一定的速度移动的,这里的“每帧”部分很重要。

我们已经将移动速度设置为 0.1,所以如果我的计算机以 120 FPS 运行游戏,玩家将每秒移动 12 米。那么在游戏以 60 FPS 运行的计算机上会发生什么呢?您可能会猜到,它只会每秒移动 6 米,使我们的游戏在不同的计算机上具有不一致的行为。这就是 Delta Time 拯救了我们的地方。

Delta Time 是一个告诉我们自上一帧以来经过了多少时间的值。这个时间很大程度上取决于我们游戏的图形、实体数量、物理体、音频和无数方面,这些将决定您的计算机可以处理一帧的速度有多快。例如,如果您的游戏以 10 FPS 运行,这意味着在一秒内,您的计算机可以处理更新循环 10 次,这意味着每个循环大约需要 0.1 秒;在那一帧中,Delta Time 将提供该值。在下一个图表中,您可以看到 4 帧需要不同的时间来处理的示例,这在现实情况下可能会发生:

图 14.16 – 游戏不同帧的 Delta Time 值变化

图 14.16 – 游戏不同帧的 Delta Time 值变化

在这里,我们需要以一种方式编码,将移动的“每帧”部分改为“每秒”; 我们需要在不同的计算机上每秒有一致的移动。一种方法是与 Delta Time 成比例地移动:Delta Time 值越高,那一帧就越长,移动量应该越大,以匹配自上次更新以来经过的真实时间。我们可以根据每秒 0.1 米的速度字段当前值来思考;我们的 Delta Time 为 0.5,意味着已经过去了半秒,所以我们应该移动一半的速度,0.05。两帧后,一秒已经过去,帧的移动总和(2 x 0.05)与目标速度 0.1 相匹配。Delta Time 可以被解释为已经过去的秒数的百分比。

为了使 Delta Time 影响我们的移动,我们应该在每一帧简单地将我们的速度乘以 Delta Time,因为 Delta Time 每一帧都可能不同,所以让我们这样做:

  1. 我们使用 Time.deltaTime 访问 Delta Time。我们可以通过在每个 Translate 中乘以 Delta Time 来开始影响移动:图 14.17 – 通过 Delta Time 乘以速度

图 14.17 – 通过 Delta Time 乘以速度

  1. 我们可以对旋转速度做同样的操作,将鼠标和速度相乘:图 14.18 – 将 Delta Time 应用于旋转代码

图 14.18 – 将 Delta Time 应用于旋转代码

  1. 如果你保存并播放游戏,你会注意到移动速度比以前慢了,这是因为现在每秒移动 0.1,意味着每秒 10 厘米,这相当慢;尝试提高这些值。在我的情况下,速度为 10,旋转速度为 180 就足够了,但旋转速度取决于玩家的首选灵敏度,这是可以配置的,但让我们留到另一个时间。

我们刚学会了如何将 Unity 的输入系统(告诉我们键盘、鼠标和其他输入设备的状态)与基本的变换移动函数相结合。这样,我们可以开始让我们的游戏感觉更加动态。

现在我们已经完成了玩家的移动,让我们讨论如何使用 Instantiate 函数让玩家发射子弹。

实现生成

我们在编辑器中创建了许多定义我们级别的对象,但一旦游戏开始,并根据玩家的操作,必须创建新的对象以更好地适应玩家交互生成的场景。敌人可能需要在一段时间后出现,或者根据玩家的输入创建子弹;即使敌人死亡,也有可能生成一些增益道具。这意味着我们不能预先创建所有必要的对象,而应该动态创建它们,这是通过脚本完成的。

在本节中,我们将研究以下生成概念:

  • 生成对象

  • 计时动作

  • 销毁对象

我们将开始看到 Unity 的Instantiate函数,它允许我们在运行时创建预制体的实例,例如按下键时,或者按时间安排,例如使我们的敌人每隔一段时间生成子弹。此外,我们将学习如何销毁这些对象,以防止场景由于处理太多对象而开始表现不佳。

让我们从如何根据玩家的输入射击子弹开始。

生成对象

要在运行时或播放模式下生成一个对象,我们需要一个对象的描述,它有哪些组件,它的设置以及可能的子对象。你可能会在这里考虑到预制体,你是对的,我们将使用一条指令告诉 Unity 通过脚本创建一个预制体的实例。记住,预制体的实例是基于预制体创建的对象,基本上是原始对象的克隆。

我们将开始射击玩家的子弹,所以首先让我们通过以下步骤创建子弹预制:

  1. GameObject | 3D Object | Sphere中创建一个球体。如果你愿意,你可以用另一个子弹模型替换球体网格,但在这个例子中我们暂时保留球体。

  2. 将球体重命名为Bullet

  3. 通过单击Bullet来创建一个材质。记得将它放在Materials文件夹中。

  4. 在材质中勾选Emission复选框,并将emission MapBase Map颜色设置为红色。记住,发射颜色会使子弹发光,特别是在我们的后期处理体积中的泛光效果下:图 14.19 – 创建一个带发光颜色的红色子弹材质

图 14.19 – 创建一个带发光颜色的红色子弹材质

  1. 通过将材质拖放到球体上,将材质应用到球体上。

  2. 将比例设置为较小的值—(0.3, 0.3, 0.3)在我的情况下有效:图 14.20 – 小红色子弹

图 14.20 – 小红色子弹

  1. 创建一个名为ForwardMovement的脚本,以使子弹以固定速度不断向前移动。

我建议你先自己尝试解决这个问题,然后在下一步中查看屏幕截图以获取解决方案,这是一个小挑战,可以回顾我们之前看到的运动概念。如果你不记得如何创建脚本,请阅读第十三章**,使用 C#介绍 Unity 脚本编写,并检查前一节以了解如何移动对象。

  1. 下一张截图向你展示了脚本应该是什么样子的:图 14.21 – 简单的向前移动脚本

图 14.21 – 简单的向前移动脚本

  1. 将脚本(如果尚未存在)添加到子弹上,并将速度设置为您认为合适的值。通常,子弹比玩家更快,但这取决于您想要获得的玩家体验(记住第一章**中的问题,从零开始设计游戏)。在我的情况下,20 效果很好。通过将子弹放在玩家附近并播放游戏来进行测试:图 14.22 – 子弹中的前进运动脚本

图 14.22 – 子弹中的前进运动脚本

  1. 将子弹GameObject实例拖到Prefabs文件夹中创建一个子弹Prefab。记住,Prefab 是一个描述创建的子弹的资产,就像创建子弹的蓝图:图 14.23 – 创建一个 Prefab

图 14.23 – 创建一个 Prefab

  1. 从场景中移除原始子弹;当玩家按下按键时,我们将使用 Prefab 来创建子弹(如果需要的话)。

现在我们有了子弹 Prefab,是时候在玩家按下按键时实例化它(克隆它)了。为此,请执行以下操作:

  1. 创建并添加一个脚本到玩家的GameObject(机器人)上,名为PlayerShooting,然后打开它。

我们需要一种方式让脚本访问 Prefab,以了解从我们项目中可能有的几十个 Prefab 中使用哪一个。我们脚本所需的所有数据都取决于所需的游戏体验,都以字段的形式存在,比如到目前为止使用的速度字段,因此在这种情况下,我们需要一个GameObject类型的字段,一个可以引用或指向特定 Prefab 的字段,可以使用编辑器进行设置。

  1. 添加字段代码将如下所示:图 14.24 – Prefab 引用字段

图 14.24 – Prefab 引用字段

重要提示

你可能会猜到,我们可以使用GameObject类型来引用 Prefab,也可以引用其他对象。想象一下,敌人 AI 需要引用玩家对象来获取其位置,使用一个 GameObject 来链接这两个对象。关键在于考虑 Prefab 只是场景之外的常规 GameObject;你看不到它们,但它们存在于内存中,准备好被复制或实例化。你只能通过脚本或通过编辑器放置在场景中的副本或实例来看到它们,就像我们到目前为止所做的那样。

  1. 在编辑器中,单击属性右侧的圆圈,并选择BulletPrefab。另一个选项是将BulletPrefab 直接拖到属性中:

图 14.25 – 设置 Prefab 引用指向子弹

图 14.25 – 设置 Prefab 引用指向子弹

这样,我们告诉我们的脚本要射击的子弹就是这个。记得拖动 Prefab 而不是场景中的子弹(那应该已经被删除了)。

按照设计文档中指定的方式,当玩家按下鼠标左键时,我们将射击子弹,因此让我们在update事件函数中放置适当的if语句来处理,就像下一张截图中所示的那样:

图 14.26 – 检测鼠标左键的压力

图 14.26 – 检测鼠标左键的压力

你会注意到这次我们使用了GetKeyDown而不是GetKey,前者是一种检测按键开始的确切帧的方法;这个if语句只会在那一帧执行它的代码,并且直到按键释放并重新按下,它才会再次进入。这是防止子弹在每一帧生成的一种方法,但只是为了好玩,你可以尝试使用GetKey来看看它会如何表现。另外,零是属于左键点击的鼠标按钮编号,一是右键点击,二是中键点击。

我们可以使用Instantiate函数来克隆预制品,将其引用作为第一个参数传递。这将在场景中创建一个所述预制品的克隆:

图 14.27 – 实例化预制品

](https://gitee.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_14.27_B14199.jpg)

图 14.27 – 实例化预制品

如果你保存脚本并播放游戏,你会注意到当你按鼠标时,子弹会生成,但可能不是在你期望的位置,如果你没有看到它,尝试在层次结构中查找新对象;它会在那里。问题在于我们没有指定期望的生成位置,我们有两种设置的方法,我们将在接下来的步骤中看到。

第一种方法是使用从 MonoBehaviour 继承的transform.positiontransform.rotation字段,它们会告诉我们当前的位置和旋转。我们可以将它们作为Instantiate函数的第二个和第三个参数传递,函数会理解这是我们希望子弹出现的地方。记住,设置旋转是很重要的,让子弹面向与玩家相同的方向,这样它就会朝着那个方向移动:

图 14.28 – 在我们的位置和旋转实例化预制品

](https://gitee.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_14.28_B14199.jpg)

图 14.28 – 在我们的位置和旋转实例化预制品

第二种方式会更长,但会让我们有更多的灵活性来改变对象的其他方面,就是使用之前版本的 Instantiate,但保存函数返回的引用,这个引用将指向预制品的克隆。拥有实例化子弹的引用允许我们改变任何我们想要的东西,不仅仅是位置,还有旋转,但现在,让我们限制在位置和旋转上。在这种情况下,我们将需要以下三行;第一行将实例化并捕获克隆引用,第二行将设置克隆的位置,第三行将设置旋转。你会注意到我们还将使用克隆的transform.position字段,但这次是通过使用=(赋值)运算符来改变它的值:

图 14.29 – 在特定位置实例化预制品的较长版本

](https://gitee.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/hsn-unity20-gm-dev/img/Figure_14.29_B14199.jpg)

图 14.29 – 在特定位置实例化预制品的较长版本

使用你喜欢的版本——两者都是一样的。记住,你可以检查项目存储库以查看完整的脚本。现在你可以用其中一个版本保存文件并尝试射击。

如果你尝试到目前为止的脚本,你应该会看到子弹在玩家的位置生成,但在我们的情况下,它可能是在地板上。问题在于机器人的枢轴在那里,通常每个人形角色的枢轴都在那里。我们有几种方法来解决这个问题,最灵活的方法是创建一个射击点,一个空的玩家子对象,放在我们希望子弹生成的位置。我们可以使用该对象的位置而不是玩家的位置,方法如下:

  1. ShootPoint中创建一个空的GameObject

  2. 将其作为玩家机器人角色对象的子对象,并将其放在你希望子弹出现的位置,可能比原始生成位置稍高和稍向前:图 14.30 – 放置在角色内部的空 ShootPoint 对象

图 14.30 – 放置在角色内部的空 ShootPoint 对象

  1. 像往常一样,要访问另一个对象的数据,我们需要一个对它的引用,比如 Prefab 引用,但这次需要指向我们的ShootPoint。我们可以创建另一个GameObject类型的字段,但这次拖动ShootPoint而不是 Prefab。脚本和对象设置如下截图所示:图 14.31 - Prefab 和 Shoot Point 字段以及它们在编辑器中的设置

图 14.31 - Prefab 和 Shoot Point 字段以及它们在编辑器中的设置

  1. 我们可以再次使用transform.position字段访问shootPoint的位置,如下截图所示:

图 14.32 - Prefab 和 ShootPoint 字段以及它们在编辑器中的设置

图 14.32 - Prefab 和 ShootPoint 字段以及它们在编辑器中的设置

您会注意到现在用鼠标射击和旋转有一个问题;当移动鼠标进行旋转时,指针会落在游戏视图之外,当点击时,您会意外地点击到编辑器,失去了对游戏视图的焦点,因此您需要再次点击游戏视图以恢复焦点并再次使用输入。防止这种情况发生的方法是在游戏进行时禁用鼠标。要做到这一点,请按照以下步骤操作:

  1. 为我们的 Player Movement Script 添加一个Start事件函数。

  2. 将您在脚本中看到的两行添加到您的脚本中。第一行将使光标可见,第二行将锁定光标在屏幕中央,因此它永远不会离开游戏视图。请考虑后者;当您切换回主菜单或暂停菜单时,您将需要重新启用光标,以允许鼠标点击 UI 按钮:图 14.33 - 禁用鼠标光标

图 14.33 - 禁用鼠标光标

  1. 保存并测试。如果要停止游戏,您可以按Ctrl + Shift + P(Mac 上为command + Shift + P)或按Esc键重新启用鼠标。这两种方法只在编辑器中有效;在真实游戏中,您将需要手动重新启用。

现在我们已经介绍了对象生成的基础知识,让我们通过将其与定时器结合来看一个高级示例。

定时动作

与生成不完全相关,但通常一起使用,定时动作是游戏中的常见任务。其思想是安排某些事情在以后发生;也许我们希望子弹在一段时间后被销毁以防止内存溢出,或者我们想控制敌人的生成速率或它们应该何时生成,这正是我们将在本节中要做的事情,从第二个开始,敌人波次。

我们的想法是,我们希望在游戏的不同时刻以一定的速率生成敌人;也许我们想在第 1 到 5 秒生成敌人,每秒 2 个,得到 10 个敌人,然后给玩家 20 秒的时间来完成它们,并编程另一个波次在第 25 秒开始。当然,这在很大程度上取决于您想要的确切游戏,您可以从这样的想法开始,并在一些测试后修改它,找到您想要波次系统工作的确切方式。在我们的案例中,我们将用先前提到的逻辑来说明定时。

首先,我们需要一个敌人,目前我们将简单地使用与玩家相同的机器人角色,但添加一个前进运动脚本来使其向前移动;稍后在本书中,我们将为我们的敌人添加 AI 行为。我建议您尝试自己创建这个 Prefab,并在尝试后查看下一步,以查看正确答案:

  1. 将 Robot FBX 模型拖到场景中以创建另一个机器人角色,但这次将其重命名为Enemy

  2. 将为子弹创建的ForwardMovement脚本添加到Enemy,并将其速度设置为 10。

  3. Enemy游戏对象拖到项目中,以创建基于该对象的预制件;我们稍后需要生成它。记得选择预制件变体,这样将保持预制件与原始模型链接,使对模型的更改自动应用到预制件。还记得销毁场景中的原始敌人。

现在,为了安排行动,我们将使用Invoke函数套件,一组用于创建定时器的函数,这些函数基本但足够满足我们的要求。让我们通过以下方式使用它:

  1. 在基地的一端创建一个空游戏对象,并将其命名为Wave1a

  2. 创建并添加一个名为WaveSpawner的脚本。

  3. 我们的生成器将需要四个字段:要生成的敌人预制件,开始波浪的游戏时间,结束波浪生成的endTime,以及敌人的生成速率 - 基本上,在给定生成期间每次生成之间应该经过多长时间。脚本和设置将如下截图所示:图 14.34 - 波浪生成器脚本的字段

图 14.34 - 波浪生成器脚本的字段

我们将使用InvokeRepeating函数来安排一个自定义函数定期重复。您只需要安排重复一次;Unity 会记住这一点,所以不要每帧都这样做。这是使用Start事件函数的好时机。函数的第一个参数是一个字符串(引号之间的文本),其中包含要定期执行的其他函数的名称,与 Start 或 update 不同,您可以随意命名函数。第二个参数是开始重复的时间,我们的startTime字段,在这种情况下。最后,函数的第三个参数是函数的重复率,每次重复之间需要经过多长时间,这是spawnRate字段。您可以在下一个截图中找到如何调用该函数,以及自定义的Spawn函数:

图 14.35 - 安排生成函数重复

图 14.35 - 安排生成函数重复

  1. Spawn函数内部,我们可以像我们知道的那样放置生成代码,使用Instantiate函数。想法是以一定的速率调用这个函数,每次调用生成一个敌人。这次,生成位置将与生成器的位置相同,所以要小心放置:图 14.36 - 在生成函数中实例化

图 14.36 - 在生成函数中实例化

如果您测试此脚本,将startTimespawnRate字段设置为一些测试值,您会注意到敌人将开始生成但永远不会停止,并且您会看到我们到目前为止还没有使用endTime字段。想法是调用CancelInvoke函数,一个函数,将取消我们所做的所有InvokeRepeating调用,但在一段时间后使用Invoke函数,这个函数与InvokeRepeating类似,但这个函数只执行一次。在下一个截图中,您可以看到我们如何在Start中添加了一个Invoke调用到CancelInvoke函数,使用endTime字段作为执行CancelInvoke的时间。这将在一段时间后执行CancelInvoke,取消生成预制件的第一个InvokeRepeating调用:

图 14.37 - 安排生成重复但在一段时间后取消

图 14.37 - 安排生成重复但在一段时间后取消

重要提示

这次,我们使用了CancelInvoke。我们没有使用自定义函数,因为CancelInvoke不接收参数。如果您需要安排带参数的函数,您需要创建一个无参数的包装函数,调用所需的函数并安排那个函数,就像我们在Spawn中所做的那样,那里的唯一目的是使用特定的参数调用Instantiate

  1. 现在您可以保存并为我们的生成器设置一些真实值。在我的情况下,我使用了以下截图中显示的值:

图 14.38 – 在游戏进行的 1 到 5 秒内每 0.5 秒生成一次敌人,每秒 2 个

图 14.38 – 在游戏进行的 1 到 5 秒内每 0.5 秒生成一次敌人,每秒 2 个

您应该看到敌人一个接一个地生成,因为它们向前移动,它们将形成一排敌人。这种行为稍后将随 AI 而改变:

图 14.39 – 生成敌人

图 14.39 – 生成敌人

如果您愿意,可以创建几个 Wave Spawner 对象,安排后期游戏的波次。记住我们在[第一章](B14199_01_Final_SK_ePub.xhtml#_idTextAnchor015)中讨论的难度平衡,从零开始设计游戏;您需要尝试使用最终的敌人 AI,但波次的数量、时间和生成速率将决定游戏的难度,这就是为什么设置这些值很重要。此外,有很多方法可以创建敌人的波次;这只是我能找到的最简单的方法。您可能需要根据您的游戏进行更改。

现在我们已经讨论了定时和生成,让我们讨论定时和销毁对象,以防止我们的子弹永远存在于内存中。

销毁对象

这将非常简短,但是这是一个广泛使用的功能,因此它值得有自己的部分。我们可以使用Destroy函数来销毁对象实例。这个想法是让子弹有一个脚本,在一段时间后安排它们自动销毁,以防止它们永远存在。我们将通过以下步骤创建脚本:

  1. 选择Bullet的预制件,并像使用添加组件 | 新脚本选项一样,为其添加一个名为Autodestroy的脚本。这次,脚本将被添加到预制件中,并且您生成的每个预制件实例都将拥有它。

  2. 您可以使用Destroy函数如下一张截图所示,在Start中仅一次销毁对象。

Destroy函数期望将要销毁的对象作为第一个参数,这里,我们使用gameObject引用,一种指向我们要销毁的 GameObject 的方式。如果您使用this指针,我们将只销毁Autodestroy组件;请记住,在 Unity 中,您永远不会创建 Gameobjects,而是创建要添加到它们的组件:

图 14.40 – 当对象启动时销毁对象

图 14.40 – 当对象启动时销毁对象

当然,我们不希望子弹在生成后立即被销毁,因此我们需要延迟销毁。您可能会考虑使用Invoke,但与 Unity 中的大多数函数不同,Destroy可以接收第二个参数,即等待销毁的时间。

  1. 创建一个delay字段,用作Destroy的第二个参数,如下一张截图所示:图 14.41 – 使用字段配置延迟销毁对象

图 14.41 – 使用字段配置延迟销毁对象

  1. delay字段设置为适当的值;在我的情况下,5 就足够了。现在通过查看它们从层次结构中被移除来检查子弹在一段时间后消失。

现在,我们可以随意创建和销毁对象,这在 Unity 脚本中非常常见。

重要提示

研究对象池的概念;您会发现有时创建和销毁对象并不那么高效。

总结

我们已经创建了我们的第一个真正的脚本,它提供了有用的行为。我们讨论了如何通过脚本实例化预制件,根据游戏情况随意创建对象。此外,我们还看到了如何安排动作,这种情况下是生成,但这可以用于安排任何事情。最后,我们看到了如何销毁创建的对象,以防止对象数量增加到无法管理的水平。我们将使用这些操作来创建本书后面的其他类型的对象,例如声音和效果。

现在,您可以创建任何类型的运动或生成逻辑,您的对象将需要确保这些对象在需要时被销毁。您可能会认为所有游戏以相同的方式移动和创建射击系统,虽然它们相似,但能够创建自己的运动和射击脚本使您能够定制游戏的这些方面,使其行为如预期,并创造您所寻找的确切体验。

在下一章中,我们将讨论如何检测碰撞,以防止玩家和子弹穿过墙壁等等。

第十五章:物理碰撞和健康系统

由于游戏试图模拟现实世界的行为,模拟物理是一个重要的方面,它决定了对象如何移动以及它们如何相互碰撞,比如玩家和墙壁的碰撞或子弹和敌人的碰撞。由于碰撞后可能发生的各种反应,物理可能很难控制,因此我们将学习如何正确配置它以获得半准确的物理效果,这将产生期望的街机运动感觉,但会使碰撞生效——毕竟,有时候现实生活并不像视频游戏那样有趣。

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

  • 配置物理

  • 检测碰撞

  • 使用物理移动

首先,我们将学习如何正确配置物理,这是检测对象之间碰撞的必要步骤,我们将使用新的事件来学习。然后,我们将讨论使用Transform移动和使用 Rigidbody 移动之间的区别,以及每个版本的优缺点。让我们开始讨论物理设置。

配置物理

Unity 的物理系统准备好覆盖各种可能的游戏应用,因此正确配置它对于获得期望的结果非常重要。

在本节中,我们将讨论以下物理设置概念:

  • 设置形状

  • 物理对象类型

  • 过滤碰撞

我们将开始学习 Unity 提供的不同类型的碰撞器,然后学习不同的配置方式来检测不同类型的物理反应(碰撞和触发)。最后,我们将讨论如何忽略特定对象之间的碰撞,以防止玩家的子弹伤害玩家等情况发生。

设置形状

在本书的开头,我们学到对象通常有两种形状,一种是视觉形状,基本上就是 3D 网格,另一种是物理形状,也就是碰撞器,物理系统将使用它来计算碰撞。请记住,这样做的目的是让你拥有高度详细的视觉模型,同时拥有简化的物理形状以提高性能。

Unity 有几种类型的碰撞器,因此我们将回顾常见的碰撞器,从基本类型开始,即盒子、球体和胶囊体。这些形状是最便宜的(性能方面)来检测碰撞,因为它们之间的碰撞是通过数学公式进行的,不像其他碰撞器,比如 Mesh Collider,它允许你使用任何网格作为对象的物理主体,但代价更高且有一些限制。理念是你应该使用基本类型来表示你的对象,或者它们的组合,例如,一个平面可以用两个盒子碰撞器来做,一个用于主体,另一个用于翅膀。你可以在下面的截图中看到一个例子,其中你可以看到由基本形状制作的武器碰撞器:

图 15.1 - 复合碰撞器

图 15.1 - 复合碰撞器

无论如何,尽量避免这样做;如果我们只是希望武器掉落到地面上,也许一个覆盖整个武器的盒子碰撞器就足够了,考虑到这些类型的碰撞不需要精确,从而提高性能。此外,有些形状甚至不能用基本形状的组合来表示,比如坡道或金字塔,你唯一的解决方案就是使用 Mesh Collider,它需要一个 3D 网格用于碰撞,但我们不会在本书中使用它们;我们将用基本形状来解决所有的物理碰撞器。

现在,让我们为场景添加必要的碰撞器,以便正确计算碰撞。请注意,如果您使用的是除了我的之外的 Asset Store 环境包,您可能已经具有带有碰撞器的场景模块;我将展示我需要在我的情况下做的工作,但请尝试将这里的主要思想推广到您的场景中。要添加碰撞器,请按照以下步骤操作:

  1. 在基础中选择一面墙,并检查对象和可能的子对象是否有碰撞器组件;在我的情况下,我没有碰撞器。如果检测到任何网格碰撞器,可以保留它,但我建议您删除它,并在下一步中用另一个选项替换它。想法是给它添加碰撞器,但我在这里检测到的问题是,由于我的墙不是预制体的实例,我需要给每面墙都添加碰撞器。

  2. 一种选择是创建一个预制体,并将所有墙替换为预制体的实例(推荐的解决方案),或者只需在层次结构中选择所有墙(按住Ctrl或 Mac 上的Cmd并单击它们),然后在选择它们时使用Box Collider组件,该组件将使碰撞器的大小适应网格。如果它不适应,您可以只需更改 Box Collider 的 Size 和 Center 属性以覆盖整个墙:图 15.2 - 添加到墙上的盒子碰撞器

图 15.2 - 添加到墙上的盒子碰撞器

  1. 对于角落、地板瓷砖和任何其他会阻碍玩家和敌人移动的障碍,重复步骤 1 和 2

对于我们的敌人和玩家,我们将添加胶囊碰撞器,这是可移动角色中常用的碰撞器,因为其圆形底部将允许对象顺利爬坡,并且横向圆形允许对象在拐角处轻松旋转而不会卡住,还有其他这种形状的便利之处。请记住,敌人是一个预制体,所以您需要将碰撞器添加到预制体中,而我们的玩家是场景中的一个简单对象,所以您需要将碰撞器添加到该对象中。

重要提示

您可能会诱惑地在角色的骨骼上添加几个盒子碰撞器,以创建对象的真实形状,虽然我们可以这样做,根据敌人被击中的身体部位应用不同的伤害,但我们只是创建了移动碰撞器;胶囊足够了。在高级伤害系统中,胶囊和骨骼碰撞器将共存,一个用于移动,另一个用于伤害检测;但在我们的游戏中,我们将简化这一过程。

此外,有时碰撞器无法很好地适应对象的视觉形状,在我的情况下,胶囊碰撞器对角色来说形状不好。我需要通过设置其值来修复其形状,如下面的截图所示:

图 15.3 - 角色碰撞器

图 15.3 - 角色碰撞器

我们用球体创建的子弹已经有了一个球体碰撞器,但如果您用另一个替换了子弹的网格,您可能需要更改碰撞器。目前,我们的游戏不需要其他对象,所以现在每个对象都有了适当的碰撞器,让我们看看如何为每个对象设置不同的物理设置以启用适当的碰撞检测。

物理对象类型

现在,通过使对象在物理模拟中存在,我们已经为每个对象添加了碰撞器,是时候配置它们以获得我们想要的确切物理行为了。我们有许多可能的设置组合,但我们将讨论一组常见的配置文件,涵盖大多数情况。请记住,除了碰撞器,我们在本书的开头看到了 Rigidbody 组件,它是将物理应用于对象的组件。以下配置文件是通过碰撞器和 Rigidbody 设置的组合完成的:

  • “刚体”组件,因此它们在物理模拟中存在,但没有任何物理作用;它们不能被其他物体移动,它们不会有物理效应,无论如何它们都会固定在它们的位置。需要注意的是,这与编辑器右上角的静态复选框无关;那些是用于之前看到的系统(比如照明和其他系统),所以如果需要的话,你可以有一个未选中该复选框的静态碰撞体。

重要提示

需要考虑的是,这些物体可以通过脚本移动,但你不应该这样做。Unity 对它们应用了一种优化技术,每当静态碰撞体移动时,优化就会失效,需要进一步计算来更新它,而且每一帧都这样做是很昂贵的。

我们刚提到地形作为一个例子,如果你检查地形的组件,你会发现它有自己的一种碰撞体,地形碰撞体。对于地形来说,这是唯一要使用的碰撞体。

  • “刚体”组件,就像我们在本书的第一部分中所做的掉落球的例子。这些是完全由物理驱动的物体,具有重力,并且可以通过力移动;其他物体可以推动它们,并且它们会执行你可以期望的每一个物理反应。你可以用它来控制玩家、手榴弹移动,或者掉落的板条箱,或者在像“不可思议的机器”这样的重度物理游戏中的所有物体。

  • “刚体”组件但有transform.Translate)而没有性能损失。需要考虑的是,由于它们没有物理效应,它们也不会有碰撞,所以它们可以穿过墙壁。这些可以用于需要使用动画或自定义脚本移动的物体,比如移动平台,考虑到在这种情况下,平台不会与其他物体发生碰撞,但是玩家通常会与它们发生碰撞,因为玩家通常会有一个物理碰撞体,实际上,物理碰撞体是会与各种碰撞体发生碰撞的。

  • “触发器”事件,这是可以通过脚本捕获的事件,告诉我们有东西在碰撞体内。这可以用来创建按钮或触发物体,在游戏中当玩家通过某些事件发生的区域时,比如生成一波敌人、打开门,或者在玩家到达目标位置时赢得游戏。需要考虑的是,普通的静态碰撞体在通过这种类型的碰撞体时不会生成触发事件,因为它们不应该移动。

  • “触发器运动碰撞体”:运动碰撞体不会生成碰撞,所以它们会穿过任何其他物体,但它们会生成触发事件,所以我们可以通过脚本做出反应。这可以用来创建可移动的能量增强道具,当触碰时消失并给我们分数,或者子弹通过自定义脚本移动而没有物理效应,就像我们的子弹一样直线前进,但在接触时会对其他物体造成伤害。

  • 我们可以有一个触发器物理碰撞体,一个带有刚体但勾选了“是触发器”的碰撞体,通常它没有真正的用途;它将是一个永远下落的物体,在世界中生成触发事件,但通过一切。当然,除了指定的这些配置外,还可以存在其他配置,用于一些具有特定游戏玩法要求的游戏,但是考虑到所有可能的物理设置组合是由你来实验的,看看哪些对你的情况有用,描述的配置将涵盖 99%的情况。

  • 为了总结之前的情景,我给你留下以下表格,显示了所有类型的碰撞体之间的接触反应。你会发现每个可以移动的配置文件都有一行;记住静态配置文件不应该移动。每一列代表了它们与其他类型碰撞时的反应,“Nothing”表示物体会毫无影响地穿过,“Trigger”表示物体会穿过但会触发触发事件,“Collision”表示物体无法穿过物体:

表 15.4 碰撞反应矩阵

表 15.4 碰撞反应矩阵

考虑到这一点,让我们开始配置场景物体的物理。

墙壁、角落、地板砖和障碍物应该使用静态碰撞体配置文件,所以它们上面没有Rigidbody组件,它们的碰撞体将不勾选Is Trigger复选框:

图 15.5 - 地板砖的配置;记住静态复选框只是用于照明

图 15.5 - 地板砖的配置;记住静态复选框只是用于照明

玩家应该移动并与物体发生碰撞,所以我们需要它具有动态配置文件。这个配置文件将会生成一个有趣的行为与我们当前的移动脚本(我鼓励你去测试),特别是当与墙壁碰撞时,它不会像你期望的那样行为。我们将在本章后面处理这个问题:

图 15.6 - 玩家的动态设置

图 15.6 - 玩家的动态设置

对于Enemy Prefab,我们将在这里使用 Kinematic 配置文件,因为我们稍后将使用 Unity 的 AI 系统移动这个物体,所以我们这里不需要物理,而且我们希望玩家与它们发生碰撞,所以这里需要一个碰撞反应,所以这里没有Trigger

图 15.7 - 敌人的运动设置

图 15.7 - 敌人的运动设置

对于Bullet Prefab,它会移动,但是通过脚本进行简单的移动(只是向前移动),而不是物理。我们不需要碰撞;我们将编写代码,使子弹在触碰到物体时立即销毁,并且会对碰撞到的物体造成伤害(如果可能的话),所以对于这个物体来说,Kinematic Trigger 配置文件就足够了;我们将使用Trigger事件来编写接触反应:

图 15.8 - 我们子弹的运动触发器设置;Is Trigger 和 Is Kinematic 都被勾选了

图 15.8 - 我们子弹的运动触发器设置;Is Trigger 和 Is Kinematic 都被勾选了

现在我们已经正确配置了物体,让我们来看看如何过滤掉某些物体类型之间不需要的碰撞。

过滤碰撞

在配置物体的所有麻烦之后,我们是否想要阻止碰撞?实际上,有时我们希望某些物体忽略彼此。例如,玩家射出的子弹不应该与玩家自身发生碰撞,敌人的子弹也不应该击中它们。我们可以在 C#脚本中使用If语句来过滤,检查击中的物体是否来自对立的队伍或者其他你想要的过滤逻辑,但那时已经太迟了,物理系统已经浪费了资源来检查本来不应该碰撞的物体之间的碰撞。这就是图层碰撞矩阵可以帮助我们的地方。

图层碰撞矩阵听起来很可怕,但它是物理系统的一个简单设置,允许我们指定哪些对象组应该与其他组发生碰撞,例如,玩家的子弹应该与敌人发生碰撞,敌人的子弹应该与玩家发生碰撞。这个想法是创建这些组并将我们的对象放在其中,在 Unity 中,这些组被称为图层。我们可以创建图层并设置 GameObject 的图层属性(检查器的顶部部分)以将对象分配到该组或图层。请注意,您拥有有限数量的图层,因此请明智地使用它们。

创建图层并分配对象后,我们可以转到物理设置并指定哪些图层将与其他图层发生碰撞。我们可以通过以下方式实现这一点:

  1. 转到编辑 | 项目设置,在其中,从左侧窗格中查找标签和图层选项:图 15.9 - 标签和图层设置

图 15.9 - 标签和图层设置

  1. PlayerEnemyPlayerBulletPlayerEnemy图 15.10 - 创建图层

图 15.10 - 创建图层

  1. 选择Player,并从检查器的顶部部分将图层属性更改为Player。还要将Enemy预制件更改为Enemy图层。将显示一个窗口询问您是否要更改子对象;选择该选项:图 15.11 - 更改玩家和敌人预制件的图层

图 15.11 - 更改玩家和敌人预制件的图层

对于子弹,我们有一个问题;我们有一个预制件,但有两个图层,而预制件只能有一个图层。我们有两个选择,即根据射手通过脚本更改图层或具有两个具有不同图层的子弹预制件。为简单起见,我将选择后者,同时也有机会将另一个材料应用于敌人子弹,使其看起来不同。

我们将创建玩家子弹的预制件变体。请记住,变体是基于原始预制件的预制件,就像类继承一样。当原始预制件更改时,变体将更改,但变体可以有差异,这将使其成为独特的:

  1. 将子弹放入场景中创建一个实例。

  2. 再次将实例拖放到预制件文件夹中,这次选择预制件变体选项。将其命名为敌人子弹。记得销毁场景中的预制件实例。

  3. 创建第二种类似于玩家子弹的材质,但是黄色或您喜欢的任何颜色,并将其放在敌人子弹预制件变体上。

  4. 选择敌人子弹的变体,设置其图层(EnemyBullet),并对原始预制件(PlayerBullet)执行相同操作。即使您更改了原始预制件的图层,由于变体修改了它,修改后的版本(或覆盖)将占上风,从而使每个预制件都有自己的图层。

  5. 转到编辑 | 项目设置,查找物理设置(不是物理 2D)。

  6. 向下滚动,直到看到图层碰撞矩阵,一个半复选框网格。您会注意到每一列和行都标有图层的名称,因此在行和列的交叉处的每个复选框都允许我们指定这两个是否应该发生碰撞。在我们的情况下,我们将其配置如下截图所示:

图 15.12 - 使玩家子弹与敌人发生碰撞,敌人子弹与玩家发生碰撞

图 15.12 - 使玩家子弹与敌人发生碰撞,敌人子弹与玩家发生碰撞

值得注意的是,有时过滤逻辑可能不那么固定或可预测,例如,我们的子弹可能只会击中具有一定生命值的对象,或者不具有临时隐身增益的对象,或者在游戏过程中可能会发生变化且难以为所有可能的组生成所有可能的层。因此,在这些情况下,我们应该依靠触发或碰撞事件后的手动过滤。

现在我们已经过滤了碰撞,让我们通过在下一节对碰撞做出反应来检查我们的设置是否正常工作。

检测碰撞

正如您所看到的,正确的物理设置可能会很复杂且非常重要,但现在我们已经解决了这个问题,让我们通过以不同的方式对接触做出反应并在此过程中创建一个健康系统来利用这些设置。

在本节中,我们将研究以下碰撞概念:

  • 检测触发事件

  • 修改其他对象

首先,我们将探索 Unity 提供的不同碰撞和触发事件,以对两个对象之间的接触做出反应。这使我们能够执行任何我们想要放置的反应代码,但我们将探索如何使用GetComponent函数修改接触对象的组件。

检测触发事件

如果对象被正确配置,就像之前讨论的那样,我们可以得到两种反应,触发和碰撞。碰撞反应有一个默认效果,即阻止对象的移动,但我们可以使用脚本添加自定义行为,但是触发器,除非我们添加自定义行为,否则不会产生任何明显的效果。无论哪种方式,我们都可以对两种可能的情况进行脚本反应,比如添加得分、减少生命和输掉游戏。为此,我们可以使用物理事件套件。

这些事件分为两组,碰撞事件和触发事件,因此根据您的对象设置,您将需要选择适当的组。两个组都有三个主要事件,进入停留退出,告诉我们碰撞或触发何时开始(进入),它们是否仍在发生或仍在接触(停留),以及何时停止接触(退出)。例如,我们可以在进入事件中编写一个行为,比如在两个对象开始接触时播放声音,比如摩擦声音,并在退出事件中停止它。

通过创建我们的第一个接触行为来测试这一点,也就是说,当子弹接触到某物时被销毁。请记住,子弹被配置为触发器,因此它们在接触任何物体时都会生成触发事件。您可以按照以下步骤进行操作:

  1. 在子弹玩家预制件上创建并添加一个名为ContactDestroyer的脚本;因为子弹敌人预制件是它的变体,它也会有相同的脚本。

  2. 要检测触发发生的时候,就像使用 Start 和 Update 一样,创建一个名为OnTriggerEnter的事件函数。

  3. 在事件中,使用Destroy(gameObject);行使子弹在接触到物体时自我销毁:图 15.13 - 与某物接触时自动销毁

图 15.13 - 与某物接触时自动销毁

  1. 保存并射击子弹,看看它们在接触墙壁时如何消失而不是穿过它。同样,在这里,我们没有碰撞,而是触发了接触时销毁子弹。因此,通过这种方式,我们可以确保子弹永远不会穿过任何东西,但我们仍然没有使用物理运动。

目前,我们不需要其他碰撞事件,但如果您需要它们,它们将类似工作;只需使用OnCollisionEnter即可。现在,让我们探索相同函数的另一个版本。它不仅告诉我们我们击中了什么,还告诉我们我们接触了什么。我们将使用这个来使我们的接触销毁器也销毁其他对象。要做到这一点,请按照以下步骤进行:

  1. 用以下截图中的方法签名替换OnTriggerEnter方法签名。这个方法接收Collider类型的参数,指示精确撞击我们的碰撞体:图 15.14 - 告诉我们与之发生碰撞的对象的触发事件的版本

图 15.14 - 告诉我们与之发生碰撞的对象的触发事件的版本

  1. 我们可以使用gameObject setter 来访问碰撞体的整个对象,因此我们也可以使用它来摧毁另一个对象,如下截图所示。如果我们只是通过传递other引用来使用Destroy,那么它只会摧毁Collider组件:图 15.15 - 摧毁两个对象

图 15.15 - 摧毁两个对象

  1. 保存并测试脚本。您会注意到子弹会摧毁它接触到的一切。

当然,我们不希望子弹在接触时摧毁一切,只摧毁自身和其他对象,如果符合某些条件,比如在对立队伍或其他情况下,根据我们的游戏。在我们的情况下,我们将向前迈进一步,而不是直接在接触时摧毁对象,而是使敌人和玩家具有生命值,因此子弹将减少生命值直到达到 0。

修改其他对象

到目前为止,我们使用transform字段来访问对象的特定组件,但是当我们需要访问其他组件时会发生什么?在我们的场景中,为了使子弹损坏碰撞的对象,它将需要访问其Life组件以改变生命值。请记住,Unity 并没有游戏的所有可能行为。因此,在我们的情况下,Life组件就是我们要创建的组件,只是用来保存一个带有生命值的浮点字段。拥有此组件的每个对象都将被视为可损坏对象。这就是GetComponent函数将帮助我们的地方。

如果您有一个对 GameObject 或 Component 的引用,您可以使用GetComponent来访问目标组件的引用,如果对象包含它(如果没有,它将返回 null)。让我们看看如何使用该函数来使子弹降低其他对象的生命值,如果它受到损坏,按照以下步骤进行:

  1. 在玩家和敌人上创建并添加一个Life组件,其中包含一个名为amountpublic float字段。记得在检查器中为两个对象的 amount 字段设置值:图 15.16 - 生命组件

图 15.16 - 生命组件

  1. 从玩家子弹中删除ContactDestroyer组件,这也会将其从敌人子弹变体中删除,并添加一个名为ContactDamager的新组件;您可能稍后需要ContactDestroyer行为。因此,我们正在创建另一个组件。

  2. 添加一个OnTriggerEnter事件,接收其他碰撞体,并只添加Destroy函数调用,自动摧毁自身,而不是摧毁其他对象的那个;我们的脚本不会负责摧毁它,只是减少它的生命值。

  3. 添加一个名为 damage 的浮点字段,这样我们就可以配置对其他对象造成的伤害量。在继续之前,请记得保存文件并设置一个值。

  4. 在对其他碰撞体的引用上使用GetComponent来获取其life组件的引用并将其保存在一个变量中:图 15.17 - 访问碰撞对象的生命组件

图 15.17 - 访问碰撞对象的生命组件

  1. 在减少对象的生命之前,我们必须检查生命引用是否不为空,如果其他对象没有Life组件,就会发生这种情况,比如墙壁和障碍物。子弹将在任何碰撞时摧毁自身,并减少其他对象的生命,如果它是包含Life组件的可损坏对象。

在下面的截图中,您将找到完整的脚本完成:

图 15.18 - 减少碰撞对象的生命

图 15.18 - 减少碰撞对象的生命值

  1. 在场景中放置一个基于预制件的敌人,并将实例速度(场景中的速度)设置为0,以防止其移动。

  2. 在点击播放之前选择它并开始向其射击。

您可以在检查器中看到生命值的减少。您还可以在播放模式下按Esc键重新获得鼠标控制权,并在编辑器中查看运行时生命字段的变化。

现在,您会注意到生命值正在减少,但它将变为负数;我们希望对象在生命值低于 0 时自行销毁。我们可以通过两种方式实现这一点,一种是向Life组件添加Update,它将检查所有帧是否生命值低于 0,并在发生时销毁自身。第二种方法是通过封装life字段,并在 setter 内部进行检查,以防止检查所有帧。我更喜欢第二种方式,但我们将实现第一种方式,以使我们的脚本对初学者尽可能简单。要做到这一点,请按照以下步骤操作:

  1. Life组件添加Update

  2. 将“如果”添加到检查amount字段是否低于0

  3. if条件为真的情况下添加Destroy

  4. 完整的Life脚本将如下截图所示:图 15.19 - 生命组件

图 15.19 - 生命组件

  1. 保存并查看对象在其生命值变为 0 时被销毁。

可选地,您可以在发生这种情况时实例化一个对象,比如声音、粒子或者一个道具。我将把这留给您作为一个挑战。

通过使用类似的脚本,您可以制作增加生命值的生命力道具,或者访问PlayerMovement脚本并增加速度字段的速度道具;从现在开始,尽情发挥想象力,创造出令人兴奋的行为。

现在我们已经探讨了如何检测碰撞并对其做出反应,让我们探索一下当玩家撞到墙壁时如何修复玩家下落的问题。

使用物理移动

到目前为止,唯一使用动态碰撞器配置文件移动的对象是玩家,也是将使用物理移动的对象,实际上是通过使用 Transform API 进行自定义脚本编写移动。每个动态对象都应该使用 Rigidbody API 函数移动,以便物理系统更好地理解,因此在这里我们将探讨如何移动对象,这次是通过 Rigidbody 组件。

在本节中,我们将研究以下物理运动概念:

  • 应用力量

  • 调整物理

我们将开始看到如何以正确的物理方式移动对象,通过力量,并将这个概念应用到我们玩家的移动上。然后,我们将探讨为什么真实的物理并不总是有趣,以及如何调整我们对象的物理属性以获得更具响应性和吸引力的行为。

应用力量

通过力量的物理准确移动对象的方式是通过影响对象的速度。要应用力量,我们需要访问Rigidbody而不是Transform,并使用AddForceAddTorque函数分别移动和旋转。这些是函数,您可以在其中指定要应用到位置和旋转的每个轴上的力量量。这种移动技术将产生完整的物理反应;力量将累积到速度上开始移动,并且将遭受减速效果,使速度缓慢减小,这里最重要的一点是它将与墙壁发生碰撞,阻挡对象的路径。

要获得这种移动方式,我们可以这样做:

  1. PlayerMovement脚本中创建一个Rigidbody字段,但这次将其设置为private,意思是在字段中不写public关键字,这将使其在编辑器中消失;我们将以另一种方式获取引用。

某些编码标准规定您需要明确用private关键字替换public关键字,但在 C#中,使用private和不使用它具有相同的效果,所以这取决于您的偏好:

![图 15.20–私有刚体引用字段

![图 15.20_B14199.jpg]

图 15.20–私有刚体引用字段

  1. Start事件函数中使用GetComponent,获取我们的Rigidbody并将其保存在字段中。我们将使用此字段来缓存GetComponent函数的结果;每帧调用该函数以访问刚体的性能不佳。此外,您还可以注意到GetComponent函数不仅可用于从其他对象(如碰撞示例)检索组件,还可以用于检索自己的组件:![图 15.21–缓存刚体引用以供将来使用

![图 15.21_B14199.jpg]

图 15.21–缓存刚体引用以供将来使用

  1. rb.AddRelativeForce替换transform.Translate调用。这将调用刚体的添加力函数,具体来说是相对的力函数,它将考虑对象的当前旋转。例如,如果您在 z 轴(第三个参数)上指定一个力,对象将沿着它的前向矢量施加力。

  2. rb.AddRelativeTorque替换transform.Rotate调用,这将应用旋转力:

![图 15.22–使用刚体力 API

![图 15.22_B14199.jpg]

图 15.22–使用刚体力 API

重要提示

如果您熟悉 Unity,您可能会认为我需要在 Fixed Update 中执行此操作,虽然这是正确的,但在 Update 中执行此操作不会产生任何显着效果。我更喜欢在初学者脚本中使用Update来防止在FixedUpdate中使用GetKeyDownGetKeyUp时可能发生的问题。

现在,如果您保存并测试结果,您可能会发现玩家正在下落,这是因为现在我们正在使用真正的物理,其中包含地板摩擦力,并且由于力被施加在重心上,它将使对象下落。请记住,在物理学上,您是一个胶囊;您没有腿来移动,这就是标准物理学不适合我们的游戏的地方。解决方案是调整物理以模拟我们需要的行为。

调整物理

为了使我们的玩家像常规平台游戏中一样移动,我们需要冻结某些轴以防止对象下落。去除地面摩擦力,并增加空气摩擦力(阻力),以使玩家在释放按键时自动减速。要做到这一点,请按照以下步骤进行:

  1. Rigidbody组件中,查看底部的Constraints部分,并检查Freeze Rotation属性的XZ轴:![图 15.23–冻结旋转轴

![图 15.23_B14199.jpg]

图 15.23–冻结旋转轴

这将防止对象侧倾,但允许对象水平旋转。如果您不希望玩家跳跃,可以冻结Freeze Position属性的 y 轴,以防止在碰撞时发生一些不希望的垂直移动。

  1. 您可能需要更改速度值,因为您从每秒米的值更改为每秒牛顿的值,旋转速度中的45预期值对我来说已经足够了。

  2. 现在,你可能会注意到速度和旋转会随着时间的推移而大幅增加。记住,你正在使用力量,这会影响你的速度。当你停止施加力时,速度会保持不变,这就是为什么即使你不移动鼠标,玩家仍然会保持旋转。解决这个问题的方法是增加阻力角阻力值,这模拟了空气摩擦,当不施加力时,将分别减少移动和旋转。尝试适合你的值;在我的情况下,我使用了2作为阻力10作为角阻力,需要将旋转速度增加到150来补偿阻力的增加:图 15.24 – 设置旋转和移动的空气摩擦

图 15.24 – 设置旋转和移动的空气摩擦

  1. 现在,如果你在触摸墙壁时移动,你的玩家不会像大多数游戏那样滑动,而是会因为接触摩擦而粘在障碍物上。我们可以通过创建一个物理材质来消除这种情况,这是一个可以分配给碰撞体以控制它们在这些情况下如何反应的资源。通过点击物理材质(不是 2D 版本)来开始创建一个。将其命名为玩家,并记得将其放在专门的资源文件夹中。

  2. 选择它并设置为0最小,这将使物理系统选择两个碰撞物体的最小摩擦,始终是最小的—在我们的情况下是零:图 15.25 – 创建物理材质

图 15.25 – 创建物理材质

  1. 选择玩家并将此资源拖到胶囊碰撞体材质属性中:图 15.26 – 设置玩家的物理材质

图 15.26 – 设置玩家的物理材质

  1. 如果现在玩游戏,你可能会注意到玩家移动得比以前快,因为现在地板上没有任何摩擦,所以你可能需要减少移动力。

  2. 你可能会发现一个小错误,就是相机后期处理对玩家应用的运动模糊效果有些小问题,有些帧是物体在移动,有些帧是不移动的。问题在于由于性能和确定性,物理不是在每一帧都执行的(默认情况下是每帧 50 次),但渲染是执行的,这影响了后期处理。你可以将刚体的插值属性设置为插值值,使刚体以自己的速率计算物理,但每帧插值位置以模拟流畅度:

图 15.27 – 使刚体插值其位置

图 15.27 – 使刚体插值其位置

正如你所看到的,我们需要弯曲物理规则来允许玩家的灵活移动。通过增加阻力和力量,可以获得更高的灵敏度,使速度更快地应用和减少,但这取决于你希望游戏拥有的体验。有些游戏希望立即响应,没有速度插值,从 0 到全速度,反之亦然,而在这种情况下,你可以直接覆盖玩家的速度和旋转向量,甚至在物理系统之外使用其他系统,比如角色控制器组件,它对平台角色有特殊的物理效果;但现在让我们保持简单。

总结

每个游戏都以某种方式具有物理特性,用于移动、碰撞检测或两者兼而有之。在本章中,我们学习了如何使用物理系统来处理这些情况,了解适当的设置以使系统正常工作,对碰撞做出反应以生成游戏系统,并以使玩家与障碍物发生碰撞,保持其物理上不准确的移动方式。我们利用这些概念来创建我们的玩家和子弹移动,并使我们的子弹对敌人造成伤害,但我们可以重复利用这些知识来满足各种其他可能的游戏需求,因此我建议你在这里玩一下物理概念;你可以发现许多有趣的用例。

在下一章中,我们将讨论如何编程游戏的视觉方面,例如效果,并使用户界面对输入做出反应。

第十六章:赢和输的条件

既然我们已经有了基本的游戏体验,现在是时候让游戏在某个时候结束,无论是赢还是输。一种常见的实现方法是通过分离的组件来监视一组对象,以检测需要发生的特定情况,比如玩家生命值变为 0 或者所有波次都被清除。我们将通过管理者的概念来实现这一点,管理者组件将管理多个对象并监控它们。

在本章中,我们将研究以下管理器概念:

  • 创建对象管理器

  • 创建游戏模式

  • 通过事件改进我们的代码

有了这些知识,你不仅能够创建游戏的胜利和失败条件,还能以正确的结构方式使用设计模式,比如单例和事件监听器。这些技能不仅对创建游戏的胜利和失败功能的代码有用,对任何代码都有用。

创建对象管理器

场景中并非每个对象都是可以看到、听到或碰撞的。有些对象也可以存在于概念上,而不是实体的东西。想象一下,你需要记录敌人的数量,你会把它保存在哪里?你还需要一个地方来保存玩家的当前分数,你可能会认为它可以保存在玩家身上,但如果玩家死亡并重生会发生什么?数据会丢失!在这种情况下,管理者的概念可以是解决我们的第一个游戏中的有用方式,所以让我们来探索一下。

在本章中,我们将看到以下对象管理器的概念:

  • 实现单例设计模式

  • 使用单例创建管理器

我们将从讨论单例设计模式是什么以及它如何帮助我们简化对象之间的通信开始。通过它,我们将创建管理者对象,这将允许我们集中一组对象的信息,等等。让我们开始讨论单例设计模式。

实现单例设计模式

设计模式通常被描述为常见问题的常见解决方案。在编写游戏代码时,你将不得不做出许多编码设计决策,但幸运的是,解决最常见情况的方法是众所周知和有文档记录的。在本节中,我们将讨论最常见的设计模式之一,即单例模式,这是一个非常有争议但在简单项目中实现起来非常方便的设计模式。

当我们需要一个对象的单个实例时,就会使用单例模式,这意味着一个类不应该有多个实例,并且我们希望它易于访问(不一定,但在我们的场景中很有用)。在我们的游戏中有很多情况可以应用这个模式,例如ScoreManager,一个将保存当前分数的组件。在这种情况下,我们永远不会有多个分数,所以我们可以利用单例管理器的好处。

一个好处是确保我们不会有重复的分数,这使我们的代码更不容易出错。此外,到目前为止,我们需要创建公共引用并通过编辑器拖动对象来连接两个对象或使用GetComponent来查找它们,但是通过这种模式,我们将全局访问我们的单例组件,这意味着你只需写组件的名称,就可以访问它。最后,只有一个ScoreManager组件,因此通过编辑器指定它是多余的。这类似于Time.deltaTime,负责管理时间的类——我们只有一个时间。

重要提示

如果你是一个高级程序员,现在可能会考虑代码测试和依赖注入,你是对的,但请记住,我们试图写简单的代码,所以我们将坚持这个简单的解决方案。

让我们创建一个 Score Manager 对象,负责处理分数,以示例展示单例模式,具体操作如下:

  1. 创建一个空的游戏对象(ScoreManager;通常,管理器会放在空对象中,与场景中的其他对象分开。

  2. 在这个对象上添加一个名为ScoreManager的脚本,其中包含一个名为amountint字段,用于保存当前分数。

  3. 添加一个名为instanceScoreManager类型字段,但在其前面加上static关键字;这将使变量成为全局变量,意味着可以通过简单地写出其名称在任何地方访问它:图 16.1 – 可以在代码中任何地方访问的静态字段

图 16.1 – 可以在代码中任何地方访问的静态字段

  1. Awake中,检查instance字段是否不为空,如果是,则使用this引用将自己设置为实例引用。

  2. 在空检查if语句的else子句中,打印一条消息,指示存在第二个ScoreManager实例必须被销毁:图 16.2 – 检查是否只有一个单例实例

图 16.2 – 检查是否只有一个单例实例

这个想法是将唯一的ScoreManager实例的引用保存在静态字段instance中,但如果用户错误地创建了两个带有ScoreManager组件的对象,这个if语句将会检测到并通知用户错误,并要求他们采取行动。在这种情况下,第一个ScoreManager实例执行Awake时会发现没有设置实例(字段为空),所以它会将自己设置为当前实例,而第二个ScoreManager实例会发现实例已经设置,并会打印消息。请记住,instance是一个静态字段,是所有类之间共享的字段,不同于常规引用字段,其中每个组件都有自己的引用,所以在这种情况下,我们在场景中添加了两个ScoreManagers,它们都将共享相同的实例字段。

为了稍微改进示例,最好有一种简单的方法来找到游戏中的第二个ScoreManager。它将被隐藏在层次结构的某个地方,很难找到。我们可以用Debug.Log替换print,基本上是一样的,但允许我们向函数传递第二个参数,即一个对象,在控制台中点击消息时可以突出显示。在这种情况下,我们将传递gameObject引用,以允许控制台突出显示重复的对象:

图 16.3 – 使用 Debug.Log 在控制台中打印消息

图 16.3 – 使用 Debug.Log 在控制台中打印消息

  1. 点击日志消息后,此游戏对象将在层次结构中突出显示:图 16.4 – 点击消息后突出显示的对象

图 16.4 – 点击消息后突出显示的对象

  1. 最后,可以通过将Debug.Log替换为Debug.LogError来进行一些改进,这样也会打印消息,但会带有错误图标。在真实的游戏中,控制台中会有大量的消息,将错误消息突出显示在信息消息之上将有助于我们快速识别它们:图 16.5 – 使用 LogError 打印错误消息

图 16.5 – 使用 LogError 打印错误消息

  1. 尝试运行代码并观察控制台中的错误消息:

图 16.6 – 控制台中的错误消息

图 16.6 – 控制台中的错误消息

下一步将是在某个地方使用这个单例模式,所以在这种情况下,我们将使敌人在被杀死时给予分数,具体操作如下:

  1. Enemy预制体添加一个名为ScoreOnDeath的脚本,其中包含一个名为amountint字段,它将指示敌人被杀时将给出的积分数。记得在预制体的编辑器中将值设置为非 0 的值。

  2. 创建OnDestroy事件函数,当这个对象被销毁时,Unity 将自动调用它;在我们的情况下,是敌人:图 16.7 – OnDestroy 事件函数

图 16.7 – OnDestroy 事件函数

重要提示

考虑到OnDestroy函数在我们切换场景或游戏退出时也会被调用,所以在这种情况下,也许我们会在切换场景时得到积分,这是不正确的。到目前为止,在我们的情况下这不是问题,但是在本章的后面,我们将看到一种防止这种情况发生的方法。

  1. 通过编写ScoreManager.instanceOnDestroy函数中访问单例引用,并将我们脚本的amount字段添加到单例的amount字段中,以增加在杀死敌人时的得分:图 16.8 – 完整的 ScoreOnDeath 组件类内容

图 16.8 – 完整的 ScoreOnDeath 组件类内容

  1. 在层次结构中选择ScoreManager,点击播放,杀死一些敌人,看到得分随着每次杀敌而增加。记得设置预制体的ScoreOnDeath组件的amount字段。

正如你所看到的,单例简化了访问ScoreManager的方式,并防止我们拥有两个相同对象的版本,这将有助于减少我们代码中的错误。需要注意的是,现在你可能会诱惑只是把所有东西都变成单例,比如玩家生命或玩家子弹,并且只是为了让你的生活更容易地创建游戏玩法,比如道具,虽然这样完全可行,但要记住你的游戏会改变,我的意思是,会有很多变化;任何真正的项目都会经历这些。也许今天,游戏只有一个玩家,但也许在未来,你会想要添加第二个玩家或一个 AI 伙伴,并且你希望道具也能影响到他们,所以如果你滥用单例模式,你将很难处理这些情况。也许伙伴会试图拿到道具,但主要玩家会被治愈!

关键是尽量少地使用这种模式,只有在没有其他解决问题的办法时才使用。老实说,总是有办法可以解决问题而不使用单例,但对于初学者来说,这些方法实现起来可能会更加困难,所以我更倾向于简化一下你的生活,让你保持动力。通过足够的练习,你将达到一个可以提高编码标准的水平。

现在我们知道如何创建单例了,让我们完成一些游戏后期需要的其他管理器。

使用单例创建管理器

有时,我们需要一个地方来整合一组类似的对象的信息,例如,一个敌人管理器,用来检查敌人的数量并可能访问它们的数组来迭代它们并执行某些操作,或者MissionManager,用来访问游戏中所有的活动任务。同样,这些情况可以被视为单例,即不会重复出现的单个对象(在我们当前的游戏设计中),所以让我们创建我们游戏中需要的那些,即EnemyManagerWaveManager

在我们的游戏中,EnemyManagerWaveManager只是保存游戏中现有敌人和波的引用数组的地方,只是一种了解它们当前数量的方式。有一些方法可以搜索特定类型的所有对象来计算它们的数量,但这些函数很昂贵,不建议使用,除非你真的知道自己在做什么。因此,具有一个单独更新的引用列表的单例,将需要更多的代码,但性能会更好。此外,随着游戏功能的增加,这些管理器将具有更多的功能和辅助函数来与这些对象交互。

让我们从敌人管理器开始,做以下操作:

  1. 将名为Enemy的脚本添加到敌人预制件中;这将是将此对象与EnemyManager连接的脚本。

  2. 创建一个名为EnemyManager的空GameObject,并向其添加名为EnemiesManager的脚本。

  3. 在脚本内创建一个名为instanceEnemiesManager类型的公共静态字段,并在Awake中添加与ScoreManager中相同的单例重复检查。

  4. 创建一个名为enemiesList<Enemy>类型的公共字段:!图 16.9-敌人组件列表

图 16.9-敌人组件列表

C#中的列表表示动态数组,可以添加和删除对象的数组。您会发现您可以在编辑器中向此列表添加和删除元素,但保持列表为空;我们将以另一种方式添加敌人。请注意,ListSystem.Collections.Generic命名空间中;您将在我们的脚本开头找到using语句。此外,请考虑您可以将列表设置为私有,并通过 getter 将其暴露给代码,而不是将其设置为公共字段;但通常情况下,我们将尽可能简化我们的代码。

重要提示

请记住,List是一个类类型,因此必须实例化,但由于此类型在编辑器中具有暴露支持,Unity 将自动实例化它。在您想要一个非编辑器暴露的列表,例如私有列表或常规非组件 C#类中的列表的情况下,您必须使用 new 关键字进行实例化。

C#列表在内部实际上是作为数组实现的。如果需要链表,请查看LinkedList集合类型。

  1. Enemy脚本的Start函数中,访问EnemyManager单例,并使用敌人列表的Add函数,将此对象添加到列表中。这将在管理器中“注册”此敌人为活动状态,以便其他对象可以访问管理器并检查当前的敌人。 Start函数在所有Awake函数调用之后调用,这很重要,因为我们需要确保在敌人的Start函数之前执行管理器的Awake函数,以确保有一个管理器设置为实例。

重要提示

我们通过Start函数解决的问题称为竞争条件,即两段代码不能保证以相同的顺序执行,而Awake执行顺序可能会因不同原因而改变。代码中有很多情况会发生这种情况,因此请注意代码中可能出现的竞争条件。此外,您可能考虑在这里使用更高级的解决方案,例如延迟初始化,这可以为您提供更好的稳定性,但出于简单起见并探索 Unity API,我们现在将使用Start函数方法。

  1. OnDestroy函数中,从列表中移除敌人,以保持列表中只有活动的敌人:

!图 16.10-注册自己为活动敌人的敌人脚本

图 16.10-注册自己为活动敌人的敌人脚本

有了这个,现在我们有了一个集中的地方以简单而有效的方式访问所有活动的敌人。我向你挑战,用WaveManager做同样的事情,它将拥有所有活动波的集合,以后检查所有波是否完成工作以考虑游戏是否获胜。花点时间解决这个问题;你将在以下截图中找到解决方案,从WavesManager开始:

图 16.11 - 完整的 WavesManager 脚本

图 16.11 - 完整的 WavesManager 脚本

你还需要WavesSpawner脚本:

图 16.12 - 修改后的 WaveSpawner 脚本以支持 WavesManager

图 16.12 - 修改后的 WaveSpawner 脚本以支持 WavesManager

正如你所看到的,WaveManager的创建方式与EnemyManager相同,只是一个具有WaveSpawner引用列表的单例,但WaveSpawner是不同的。我们在WaveSpawnerStart事件中执行列表的Add函数,将波注册为活动波,但Remove函数需要更多的工作。

这个想法是在生成器完成其工作后,当波完成生成所有敌人时,从活动波列表中注销该波。在进行这种修改之前,我们使用Invoke在一段时间后调用CancelInvoke函数来停止生成,但现在在结束时间之后我们需要做更多的事情。我们将在指定的波结束时间后调用CancelInvoke,而是调用一个名为EndSpawner的自定义函数,它将调用CancelInvoke来停止生成器,Invoke Repeating,但也将调用WavesManager列表中的Remove函数,以确保在WaveSpawner完成其工作时确切地调用从列表中移除。

使用对象管理器,我们现在集中了关于一组对象的信息,并且可以在这里添加各种对象组逻辑,但除了拥有这些信息以更新 UI(我们将在下一章中进行),我们还可以使用这些信息来检测我们游戏的胜利和失败条件是否满足,创建一个游戏模式对象来检测这一点。

创建游戏模式

我们已经创建了对象来模拟游戏中许多方面的玩法,但游戏总归需要在某个时候结束,无论是赢还是输。就像往常一样,问题是在哪里放置这个逻辑,这导致了更多的问题。主要问题是,我们是否总是以相同的方式赢得或输掉游戏?我们是否会有一个特殊的级别,其标准不同于杀死所有波,比如定时生存?只有你知道这些问题的答案,但如果现在的答案是否定的,这并不意味着以后不会改变,因此最好是准备我们的代码以无缝适应变化。

重要提示

老实说,让我们的代码无缝适应变化几乎是不可能的;没有办法编写完美的代码来考虑每种可能的情况,我们总是需要迟早重写一些代码。我们将尽量使代码尽可能适应变化;总是这样做并不会消耗大量的开发时间,有时快速编写简单的代码比缓慢编写复杂的代码更可取,而且可能并不必要,因此明智地平衡你的时间预算。

为此,我们将胜利和失败条件的逻辑分离到自己的对象中,我喜欢称之为“游戏模式”(不一定是行业标准)。这将是一个组件,将监督游戏,检查需要满足的条件以考虑游戏结束。它将像我们游戏的裁判一样。游戏模式将不断检查对象管理器中的信息,也许还有其他信息来源,以检测所需的条件。将这个对象与其他对象分离允许我们创建具有不同游戏模式的不同级别;只需在该级别中使用另一个游戏模式脚本,就可以了。

在我们的情况下,目前我们将只有一个游戏模式,它将检查波次和敌人的数量是否变为 0,这意味着我们已经杀死了所有可能的敌人并且游戏获胜。此外,它还将检查玩家的生命值是否达到 0,在这种情况下认为游戏失败。让我们通过以下方式创建它:

  1. 创建一个GameMode空对象,并向其添加一个WavesGameMode脚本。正如您所看到的,我们使用了一个描述性的名称来命名脚本,考虑到我们可以添加其他游戏模式。

  2. 在其Update函数中,使用敌人和波次管理器检查敌人和波次的数量是否达到了0;在这种情况下,目前只需在控制台中print一条消息。所有列表都有一个Count属性,它将告诉您存储在其中的元素数量。

  3. 添加一个名为PlayerLifeLife类型的public字段,并将玩家拖放到其中;这样也可以检测失败条件。

  4. Update中,添加另一个检查,以检测PlayerLife引用的生命值是否达到了0,如果是,就在控制台中print一个失败消息:图 16.13 - 在 WavesGameMode 中检查胜利和失败条件

图 16.13 - 在 WavesGameMode 中检查胜利和失败条件

  1. 玩游戏并测试两种情况,即玩家生命值是否达到 0 或者您是否已经杀死了所有敌人和波次。

重要提示

请记住,我们不希望有两个此对象的实例,因此我们也可以将其设置为单例,但由于其他对象不会访问此对象,这可能是多余的;我会把这个决定留给您。无论如何,请记住,这不会阻止您实例化两个不同的GameModes;为此,您可以创建一个GameMode基类,其中包含单例功能,以防止在同一场景中出现两个GameModes

现在,是时候用更有趣的东西替换这些消息了。目前,我们只会将当前场景更改为一个胜利场景和失败场景,它们只会有一个带有胜利和失败消息以及一个再玩一次按钮的 UI。将来,您可以添加一个主菜单场景,并提供返回选项。让我们通过以下方式做到这一点:

  1. 创建一个新场景(WinScreen)。

  2. 添加一个 UI 文本,并将其与文本居中,写上“你赢了!”。

  3. 在文本下方添加一个 UI 按钮,并将其文本更改为“再玩一次”:图 16.14 - WinScreen

图 16.14 - WinScreen

  1. 在项目视图中选择场景,按Ctrl + D(Mac 上为Cmd + D)来复制场景。将其重命名为LoseScreen

  2. 双击LoseScreen场景以打开它,然后将“你赢了!”文本更改为“你输了!”文本。

  3. 进入WinScreenLoseScreen,以及我们迄今为止创建的游戏场景,我称之为Game,所以只需将这些场景从项目视图拖动到构建设置窗口的列表中;我们需要这样做来确保游戏模式脚本能够正确地改变场景。另外,请注意,这个列表中的第一个场景将是在最终版本(即构建版本)中打开的第一个场景,因此您可能希望根据这一点重新排列列表:图 16.15 - 注册要包含在游戏构建中的场景

图 16.15 - 注册要包含在游戏构建中的场景

  1. WavesGameMode中,添加一个using语句,用于启用此脚本中的场景更改功能的UnityEngine.SceneManagement命名空间。

  2. SceneManager.LoadScene函数替换控制台print消息,该函数将接收一个字符串,其中包含要加载的场景的名称;在这种情况下,它将是WinScreenLoseScreen。您只需要场景名称,而不是整个文件路径。

如果你想链接不同的关卡,可以创建一个public字符串字段,允许你通过编辑器指定要加载哪些场景。记得将场景添加到构建设置中,否则当你尝试更改场景时,控制台会收到错误消息:

图 16.16 – 使用 SceneManager 更改场景

图 16.16 – 使用SceneManager更改场景

  1. 玩游戏,检查场景是否正确更改。

重要提示

现在,我们选择了最简单的方式来显示我们是输了还是赢了,但也许在未来,你会希望有比突然改变场景更温和的方式,比如可能使用Invoke等待一段时间来延迟改变,或者直接在游戏中显示获胜消息而不改变场景。在测试游戏时,考虑到玩家在玩游戏时是否理解发生了什么,游戏反馈对于让玩家知晓正在发生的事情是很重要的,这并不是一件容易的事情。

现在我们有一个完全功能的简单游戏,具有机制和胜利和失败条件,虽然这已经足够开始开发游戏的其他方面,但我想讨论一下我们当前的管理器方法存在的一些问题,以及如何通过事件解决这些问题。

通过事件改进我们的代码

到目前为止,我们使用了 Unity 事件函数来检测游戏中可能发生的情况,比如AwakeUpdate。这些函数是 Unity 用来让两个组件进行通信的方式,比如OnTriggerEnter,这是刚体通知游戏对象中的其他组件发生了碰撞的一种方式。在我们的情况下,我们在 Update 中使用if来检测其他组件的变化,比如GameMode检查敌人数量是否达到 0。但是,如果敌人管理器在发生变化时通知我们,我们可以在那一刻进行检查,就像刚体告诉我们碰撞发生的情况一样,而不是每帧都检查碰撞。

有时,我们依赖 Unity 事件来执行逻辑,比如在OnDestroy事件中给予分数,该事件通知我们对象被销毁时,但由于事件的性质,它可能在我们不希望加分的情况下被调用,比如场景改变或游戏关闭时。在这些情况下对象被销毁,但不是因为玩家杀死了敌人,导致分数被提高,这时候就需要一个事件告诉我们玩家的生命值已经达到 0,以执行这个逻辑,而不是依赖通用的销毁事件。

事件的理念是改进我们对象之间的通信模型,确保在某个情况发生时,对该情况感兴趣的部分被通知以做出相应反应。Unity 有很多事件,但我们可以创建特定于我们游戏逻辑的事件。让我们从之前讨论的分数场景中开始看到这个应用;想法是让Life组件有一个事件来通知其他组件,对象被销毁是因为它的生命值达到了 0。

有几种方法可以实现这一点,我们将使用与AwakeUpdate方法略有不同的方法;我们将使用UnityEvent字段类型。这是一种能够保存引用函数的字段类型,当我们想要执行时,就像 C#委托一样,但具有其他好处,比如更好的 Unity 编辑器集成。要实现这一点,按照以下步骤进行:

  1. Life组件中,创建一个名为onDeathUnityEvent类型的public字段。这个字段将代表一个事件,其他类可以订阅它以便在Life达到 0 时知晓:图 16.17 – 创建自定义事件字段

图 16.17 – 创建自定义事件字段

  1. 如果你保存脚本并进入编辑器,你可以在检视器中看到事件。Unity 事件支持在编辑器中订阅方法,这样我们可以连接两个对象。我们将在 UI 脚本章节中使用这个功能,所以现在就忽略它吧:图 16.18 – UnityEvents 显示在检视器中

图 16.18 – UnityEvents 显示在检视器中

重要提示

你可以使用通用委托动作或自定义委托来创建事件,而不是使用UnityEvent,除了某些性能方面的差异之外,唯一显著的区别是UnityEvent会显示在编辑器中,就像步骤 2中演示的那样。

  1. 当生命值达到0时,调用事件的Invoke函数,这样我们就告诉任何对该事件感兴趣的人,事件已经发生:图 16.19 – 执行事件

图 16.19 – 执行事件

  1. ScoreOnDeath中,将OnDestroy函数重命名为GivePoints或者你喜欢的其他名称;这里的想法是停止在OnDestroy事件中给分。

  2. ScoreOnDeath脚本的Awake函数中,使用GetComponent获取Life组件并将其保存在一个局部变量中。

  3. 调用Life引用的onDeath字段的AddListener函数,并将GivePoints函数作为第一个参数传递。这样做的想法是告诉LifeonDeath事件被调用时执行GivePoints。这样,Life会通知我们发生了什么情况。记住,你不需要调用GivePoints,只需要将函数作为字段传递即可:图 16.20 – 订阅 OnDeath 事件以在该场景中给分

图 16.20 – 订阅 OnDeath 事件以在该场景中给分

重要提示

考虑在OnDestroy中调用RemoveListener;通常情况下,尽可能取消订阅监听器是方便的,以防止内存泄漏(引用阻止 GC 释放内存)。在这种情况下,这并不是完全必要的,因为LifeScoreOnDeath组件将同时被销毁,但尽量养成这种良好的习惯。

  1. 保存,在编辑器中选择ScoreManager,然后点击播放进行测试。尝试在播放模式下从层次结构中删除一个敌人,以检查分数不会上升,因为敌人被摧毁不是因为生命值变为 0;你必须通过射击摧毁敌人才能看到分数上升。

现在Life有了onDeath事件,我们也可以将WavesGameMode中对玩家Life的检查替换为使用事件,方法如下:

  1. WavesGameMode脚本中创建一个OnLifeChanged函数,并将生命检查条件从Update移动到这个函数中。

  2. Awake中,订阅玩家Life组件引用的onDeath事件到这个新函数:

图 16.21 – 使用事件检查失败条件

图 16.21 – 使用事件检查失败条件

如你所见,创建自定义事件可以让你检测到 Unity 中默认情况之外的更具体的情况,并且保持你的代码清晰,而不需要在Update函数中不断询问条件,这并不一定是坏事,但事件方法可以生成更清晰的代码。

记住,我们也可以通过玩家的基础生命值达到 0 来输掉游戏,我们将在本书的后面探讨玩家基础的概念,但现在,让我们创建一个立方体,代表敌人将攻击以减少基础生命值的对象,就像基础核心一样。考虑到这一点,我挑战你将这个额外的失败条件添加到我们的脚本中。完成后,你可以在以下截图中检查解决方案:

图 16.22 – 完整的 WavesGameMode 失败条件

图 16.22 – 完整的 WavesGameMode 失败条件

正如你所看到的,我们只是重复了生活事件的订阅:记得创建一个对象来代表玩家基地的伤害点,给它添加一个Life脚本,并将其作为WavesGameMode的玩家基地生命引用拖放进去。

现在,让我们通过将其应用于管理器来继续说明这个概念,以防止游戏模式每帧检查条件:

  1. EnemyManager中添加一个UnityEvent字段,称为onChanged。每当敌人被添加或从列表中移除时,将执行此事件。

  2. 创建两个函数,AddEnemyRemoveEnemy,都接收Enemy类型的参数。想法是,Enemy不直接向列表中添加和移除自己,而是应该使用这些函数。

  3. 在这两个函数中,调用onChanged事件通知其他人敌人列表已经更新。想法是任何想要向列表中添加或移除敌人的人都需要使用这些函数:图 16.23 – 当敌人被添加或移除时调用事件

图 16.23 – 当敌人被添加或移除时调用事件

重要提示

在这里,我们的问题是没有什么能阻止我们绕过这两个函数直接使用列表。你可以通过将列表设置为私有,并使用IReadOnlyList接口来公开它来解决这个问题。请记住,这种方式,列表不会出现在编辑器中以进行调试。

  1. 更改Enemy脚本以使用这些函数:

图 16.24 – 使敌人使用 Add 和 Remove 函数

图 16.24 – 使敌人使用 Add 和 Remove 函数

  1. WaveManagerWaveSpawner执行相同的过程,创建一个onChanged事件,并创建AddWaveRemoveWave函数,并在WaveSpawner中调用它们,而不是直接访问列表。这样,我们可以确保在必要时调用事件,就像我们在EnemyManager中所做的那样。尝试自己解决这一步,然后在下面的屏幕截图中检查解决方案,从WavesManager开始:图 16.25 – Wave Manager On Changed 事件实现

图 16.25 – Wave Manager On Changed 事件实现

此外,WavesSpawner 需要更改:

图 16.26 – 实现 AddWave 和 RemoveWave 函数

图 16.26 – 实现 AddWave 和 RemoveWave 函数

  1. WavesGameMode中,将Update重命名为CheckWinCondition,并订阅此函数到EnemyManageronChanged事件和WavesManageronChanged事件。想法是只在必要时检查敌人和波数的变化。请记住在Start函数中订阅事件,因为单例在Awake中初始化:

图 16.27 – 当敌人或波数发生变化时检查胜利条件

图 16.27 – 当敌人或波数发生变化时检查胜利条件

是的,这种方式,我们需要比以前写更多的代码,而且在功能上,我们并没有得到任何新的东西,但在更大的项目中,通过Update检查来管理条件会导致之前讨论过的不同种类的问题,比如竞争条件和性能问题。有一个可扩展的代码库有时需要更多的代码,这就是这种情况之一。

在我们完成之前,需要考虑的一点是,Unity 事件并不是在 Unity 中创建这种事件通信的唯一方式;你会发现一种类似的方法叫做Action,这是 Unity 事件的本地 C#版本,我建议你去寻找一下,如果你想看看所有的选择。

总结

在本章中,我们完成了游戏的重要部分,结局,无论是胜利还是失败。我们讨论了一种简单但强大的方法,通过使用通过单例创建的管理器来分离不同的责任层,以确保每种类型的管理器都不会有多个实例,并通过静态访问简化它们之间的连接(这是在发现代码测试的那一天要考虑的事情)。此外,我们讨论了事件的概念,以简化对象之间的通信,以防止问题并创建更有意义的对象之间的通信。

有了这些知识,你现在不仅能够检测游戏的胜利和失败条件,还能以更好的结构方式来做到这一点。这些模式可以用来改进我们的游戏代码,我建议你尝试将其应用到其他相关场景中。

在下一章中,我们将探讨如何创建视觉和音频反馈以响应我们的游戏玩法,结合本书第二部分中集成的脚本和资产。

第十七章:UI、声音和图形脚本

在游戏中,即使玩家通过摄像机看到游戏,也有一些重要信息是肉眼不可见的,比如剩余子弹的确切数量、他们的生命、敌人、是否有敌人在他们身后等等。我们已经讨论过如何通过 UI、声音和视觉效果(VFX)来解决这些问题,但随着我们在游戏中开始进行脚本编写,这些元素也需要适应游戏。本章的理念是通过脚本使我们的 UI、声音和 VFX 对游戏情况做出反应,反映世界上正在发生的事情。

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

  • UI 脚本

  • 脚本反馈

在本章结束时,您将能够使 UI 对游戏情况做出反应,以文本和条形图的形式显示相关信息,并且还能够使游戏对与 UI 的交互做出反应,比如按钮。此外,您还将能够使游戏通过其他媒介向用户传达这些信息,比如声音和粒子图形,这些可以和 UI 一样有效,但更具吸引力。

UI 脚本

我们之前创建了一个包含条形、文本和按钮等元素的 UI 布局,但到目前为止,它们都是静态的。我们需要使它们适应游戏的实际状态。在本章中,我们将讨论以下 UI 脚本概念:

  • 在 UI 中显示信息

  • 编写暂停菜单的程序

我们将首先看看如何使用脚本在我们的 UI 上显示信息,这些脚本修改了与 Canvas 元素一起显示的文本和图像。之后,我们将创建暂停功能,该功能将在整个 UI 中使用。

在 UI 中显示信息

如前所述,我们将使用 UI 向用户显示信息,以便他们做出明智的决定,因此让我们从看看如何使玩家的生命条对我们之前创建的Life脚本中剩余的生命做出反应开始:

  1. 添加一个名为Image的新脚本,用于表示生命条:图 17.1 – 玩家 HealthBar 画布中的生命条组件

图 17.1 – 玩家 HealthBar 画布中的生命条组件

  1. Life Bar脚本中添加一个Life类型字段。这样,我们的脚本将询问编辑器我们将监视哪个Life组件。保存脚本:图 17.2 – 可在编辑器中配置的对生命组件的引用

图 17.2 – 可在编辑器中配置的对生命组件的引用

  1. 在编辑器中,将Player游戏对象从targetlife属性拖动到生命条引用玩家的Life组件,并记得在拖动LifeBar脚本之前选择HealthBar对象,以检查玩家剩余的生命。有趣的是,敌人也有相同的Life组件,所以我们可以轻松地使用这个组件为游戏中具有生命的其他对象创建生命条:图 17.3 – 拖动 Player 以引用其生命组件

图 17.3 – 拖动 Player 以引用其生命组件

  1. 在脚本的前几行的using语句之后添加using UnityEngine.UI;行。这将告诉 C#我们将与 UI 脚本进行交互:图 17.4 – 我们脚本中的所有 using 语句。我们现在不会使用它们,但让我们暂时保留它们现在不使用它们,但让我们暂时保留它们

图 17.4 – 我们脚本中的所有 using 语句。我们现在不会使用它们,但让我们暂时保留它们

  1. 创建一个private字段(不带public关键字),类型为Image。我们将在这里保存对组件的引用:图 17.5 – 对图像的私有引用

图 17.5 – 对图像的私有引用

  1. Awake中使用GetComponent,访问我们游戏对象(HealthBar)中Image组件的引用,并将其保存在image字段中。通常情况下,想法是只获取一次这个引用,并在Update函数中保存以供以后使用。当然,当你将这个组件放在一个带有Image组件的对象中时,这将总是有效。如果不是的话,另一个选择就是创建一个Image类型的公共字段,并将图像组件拖放到其中:图 17.6 – 在此对象中保存对 Image 组件的引用

图 17.6 – 在此对象中保存对 Image 组件的引用

  1. LifeBar脚本中创建一个Update事件函数。我们将使用这个函数来根据玩家的生命不断更新生命条。

  2. Update事件中,将生命值除以100,以便在01范围内表示我们当前的生命百分比(假设我们的最大生命是100),并将结果设置在Image组件的fillAmount字段中,如下面的截图所示。请记住,fillAmount期望一个在01之间的值,0表示条是空的,1表示条是满的:图 17.7 – 根据 Life 组件更新 LifeBar 脚本的 Image 组件的填充量

图 17.7 – 根据 Life 组件更新 LifeBar 脚本的 Image 组件的填充量

重要提示:

记住,在代码中放入100被认为是硬编码(也被称为魔术数字),这意味着以后更改该值需要我们在代码中查找该值,这在大型项目中是一项复杂的任务。这就是为什么它被认为是不好的实践。最好在Life组件中有一个Maximum Life字段,或者至少有一个包含这个值的常量。

  1. 保存脚本,并在编辑器中选择玩家并开始游戏。在播放模式下,按下Esc键以重新获得鼠标访问权限,并在检查器窗口中更改玩家的生命值,以查看生命条如何相应更新。你也可以通过让玩家受到伤害来测试这一点,比如让敌人生成子弹(稍后会详细介绍敌人):

图 17.8 – 完整的 LifeBar 脚本

图 17.8 – 完整的 LifeBar 脚本

重要提示:

在上一章中,我们探讨了事件的概念,以便检测其他对象状态的变化。生命条是另一个使用事件的例子,因为我们可以在生命实际改变时改变图像的填充量。我向你挑战,尝试创建一个生命改变时触发事件,并使用我们在上一章中看到的脚本来实现这个脚本。

你可能会认为这个 UI 行为可以直接编码在Life组件中,这是完全可能的,但这里的想法是创建简单的脚本,减少对我们代码的压力。每个脚本应该只有一个修改的原因,将 UI 行为和游戏行为混合在一个脚本中会使脚本具有两个责任,这将导致脚本有两个可能的修改原因。通过这种方法,我们还可以通过将相同的脚本添加到其生命条中并将我们在上一章中创建的基础伤害对象拖放为目标生命,来设置玩家的基础生命条。

重要提示:

我们刚提到的单一对象责任原则是作为 SOLID 的五个面向对象编程原则之一。如果你不知道 SOLID 是什么,我强烈建议你查一下,以改进你的编程最佳实践。

现在我们已经解决了玩家的生命条,让我们根据玩家剩余的子弹数量更新Bullets标签。这里需要考虑的是,我们当前的玩家射击脚本有无限的子弹,所以让我们通过以下步骤来改变这一点:

  1. 在 Player Shooting 脚本中添加一个名为bulletsAmount的公共int类型字段。

  2. 在检查左鼠标按钮的压力的if语句中,添加一个条件来检查子弹数量是否大于0

  3. if语句中,减少子弹数量1图 17.9 - 限制射击的子弹数量

图 17.9 - 限制射击的子弹数量

现在我们有一个字段指示剩余子弹的数量,我们可以创建一个脚本来显示该数字在 UI 中,方法如下:

  1. PlayerBulletsUI脚本添加到子弹的Text游戏对象中。在我的案例中,我将其称为Bullets Label

  2. 添加using UnityEngine.UI语句,并在Awake中添加一个Text类型的私有字段,将其保存在我们自己的Text组件的引用中:图 17.10 - 缓存对我们自己的文本组件的引用

图 17.10 - 缓存对我们自己的文本组件的引用

  1. 创建一个名为targetShootingPlayerShooting类型的公共字段,并将Player拖放到编辑器中的此属性中。就像生命条组件一样,我们的 UI 脚本将访问具有剩余子弹的脚本以更新文本,以保持两个脚本(TextPlayerShooting)的责任分离。

  2. 创建一个Update语句,在其中,使用文本引用的text字段(我知道,令人困惑)与targetShooting引用的bulletsAmount字段的连接来设置它。这样,我们将根据当前的子弹数量替换标签的文本:

图 17.11 - 更新子弹文本标签

图 17.11 - 更新子弹文本标签

重要提示:

请记住,连接字符串会分配内存,所以我再次敦促您只在必要时使用事件来做这件事。

如果您查看这两个脚本,您会发现一个模式。您可以访问UIGameplay组件,并相应地更新UI组件,大多数 UI 脚本都会以相同的方式运行。牢记这一点,我挑战您创建必要的脚本来使用using UnityEngine.UI来使用Text组件。完成后,您可以将您的解决方案与以下截图中的解决方案进行比较,从ScoreUI开始:

图 17.12 - ScoreUI 脚本

图 17.12 - ScoreUI 脚本

此外,我们还需要WavesUI组件:

图 17.13 - WavesUI 脚本

图 17.13 - WavesUI 脚本

最后,我们需要EnemiesUI

图 17.14 - EnemiesUI 脚本

图 17.14 - EnemiesUI 脚本

正如您所看到的,我们已经使用了管理器中已编码的事件来仅在必要时更改 UI。现在我们已经编写了 UI 标签和条,让我们编写Pause菜单。

编写暂停菜单

回想一下我们在上一章中创建了一个暂停菜单,但它目前被禁用了,所以让我们让它工作起来。首先,我们需要编写Pause,这可能会相当复杂。因此,我们将再次使用一个简单的方法来暂停大多数行为,即停止时间!请记住,我们的大多数移动脚本都使用时间功能,比如timeScale

这个字段将影响 Unity 的时间系统的速度,我们可以将其设置为0来模拟时间已经停止,这将暂停动画,停止粒子,并减少0,使我们的移动停止。所以,让我们来做吧:

  1. 创建一个名为Pause的脚本,并将其添加到场景中的一个新对象中,也称为Pause

  2. Update中,检测当按下Esc键时,然后在这种情况下,将Time.timeScale设置为0图 17.15 - 停止时间以模拟暂停

图 17.15 - 停止时间以模拟暂停

  1. 保存并测试这个。

您会注意到几乎所有东西都会停止,但您可以看到射击功能仍在工作。这是因为玩家射击脚本不依赖于时间。这里的一个解决方案可能是简单地检查Time.timeScale是否大于0以防止这种情况发生:

图 17.16 - 在 PSlayer 射击脚本中检查暂停

图 17.16 - 在 PSlayer 射击脚本中检查暂停

重要提示:

像往常一样,我们在这里追求了最简单的方法,但有更好的方法。我挑战您尝试创建PauseManager,其中包含一个布尔值,指示游戏是否暂停,从而改变timeScale

现在我们有了一个简单但有效的暂停游戏的方法,让我们通过以下方式使暂停菜单可见以取消暂停游戏:

  1. Pause脚本中添加一个名为pauseMenuGameObject类型字段。想法是将暂停菜单拖放到这里,以便我们有一个启用和禁用它的引用。

  2. Awake中,添加pauseMenu.SetActive(false);以在游戏开始时禁用暂停菜单。即使我们在编辑器中禁用了暂停菜单,我们也添加了这个以防我们错误地重新启用它。它必须始终处于禁用状态。

  3. 使用相同的函数,但将true作为第一个参数传递,启用UnityEventsButton脚本。我们的OnClick事件,这是一个通知我们特定按钮已被按下的事件。按下这些按钮时让游戏恢复,做如下操作:

  4. 在我们的Pause脚本中创建一个Button类型的字段,名为resumeButton,并将resumeButton拖放到其中;这样,我们的Pause脚本就有了对按钮的引用。

  5. Awake中,为resumeButtononClick事件添加名为OnResumePressed的监听函数。

  6. 使OnResumePressed函数将timeScale设置为1并禁用Awake

图 17.18 - 取消暂停游戏

图 17.18 - 取消暂停游戏

如果您保存并测试此代码,您会注意到当您恢复时无法单击“暂停”并禁用它:

图 17.19 - 在暂停时显示和隐藏光标

图 17.19 - 在暂停时显示和隐藏光标

现在您知道如何编写按钮,我挑战您编写“退出”按钮的行为。同样,记得添加using UnityEngine.UI。此外,您需要调用Application.Quit();来退出游戏,但请注意这在编辑器中不起作用;我们不希望在创建游戏时关闭编辑器。此函数仅在构建游戏时起作用。因此,现在只需调用它,如果您想要打印一条消息以确保按钮正常工作,解决方案在以下截图中提供:

图 17.20 - 退出按钮脚本

图 17.20 - 退出按钮脚本

此解决方案建议您直接将此脚本添加到其Button兄弟组件的onClick事件上,并在这种情况下执行Quit函数。您还可以将此行为添加到Pause脚本中,虽然这样也可以工作,但请记住,如果一个脚本可以分成两个因为它执行两个不相关的任务,最好将其拆分为两个不相关的行为。在这里,暂停行为与退出行为无关。

现在我们已经使用 UI 和按钮设置了暂停系统,让我们继续探讨其他视觉和听觉方式,让玩家意识到发生了什么。

脚本化反馈

我们只是使用 UI 向用户传递数据,以便他们知道发生了什么,但有时这还不够。我们可以使用其他类型的反馈来加强游戏事件,例如声音和爆炸,这些在之前的章节中已经集成了。

在本节中,我们将探讨以下反馈概念:

  • 脚本化视觉反馈

  • 脚本化音频反馈

  • 脚本化动画

我们将开始看到如何使我们的游戏玩法具有更多的反馈,使用在正确时刻使用的不同视觉效果,比如音频和粒子系统。然后,我们将使我们角色的动画与这些时刻相匹配,例如,我们将创造他们实际上正在行走的幻觉。

脚本化视觉反馈

视觉反馈是使用不同的 VFX 概念,比如粒子和 VFX 图表,来加强发生的事情。例如,比如现在我们正在射击,我们知道这是发生的,因为我们可以看到子弹。但这并不完全感觉像真正的射击,因为一个合适的射击模拟需要我们的枪显示枪口闪光效果。另一个例子是敌人死亡——它只是消失了!这并不像应该的那样令人满意。我们可以改为添加一点爆炸效果(考虑到它们是机器人)。

让我们开始使我们的敌人在死亡时生成爆炸,方法如下:

  1. 创建一个爆炸效果或从资产商店下载一个。它不应该循环,并且在爆炸结束时需要自动销毁(确保在主模块中销毁)。

  2. 资产商店中的一些爆炸可能使用不兼容 URP 的着色器。您可以通过将“编辑” | “渲染管线” | “通用渲染管线” | “升级所选材料”选项设置为“UniversalRP 材料”来修复它们,同时保持所选材料。

  3. 手动升级未自动升级的材料。

  4. Enemy预制体中添加一个名为ExplosionOnDeath的脚本。这将负责在敌人死亡时生成粒子预制体。

  5. 添加一个名为particlePrefab的 GameObject 类型字段,并将爆炸预制体拖放到其中。

重要提示:

您可能希望将爆炸生成添加到“生命”组件中。在这种情况下,您假设任何与生命有关的东西在死亡时都会生成一个粒子,但请考虑角色以下落动画死亡的情况,或者可能是一个物体在没有任何效果的情况下消失。如果某种行为在大多数情况下都没有使用,最好将其编码为一个单独的可选脚本,以允许我们混合和匹配不同的组件,并获得我们想要的确切行为。

  1. 使脚本访问“生命”组件并订阅其onDeath事件。

  2. listener函数中,在相同位置生成粒子系统:图 17.21 – 爆炸生成器脚本

图 17.21 – 爆炸生成器脚本

正如你所看到的,我们只是在以前的章节中学到的概念中,以新的方式进行组合。这就是编程的全部内容。让我们继续进行枪口效果,这也将是一个粒子系统,但这次我们将采取另一种方法。

  1. 从资产商店下载一个武器模型并将其实例化,使其成为玩家手的父级。记住我们的角色是绑定的,并且有一个手骨,所以你应该把武器放在那里:图 17.22 – 将武器放在手骨中

图 17.22 – 将武器放在手骨中

  1. 创建或获取一个枪口粒子系统。在这种情况下,我的枪口粒子系统是作为一个短粒子系统创建的,它有一阵粒子然后自动停止。尝试获取一个具有这种行为的粒子系统,因为还有其他的粒子系统会循环,处理这种情况的脚本会有所不同。

  2. 在编辑器中创建一个粒子系统预制体的实例,并将其放置在武器内,位于枪管的前方。确保粒子系统的主模块的“自动播放”属性未选中;我们不希望枪口在我们按下开火键之前就发射:图 17.23 – 枪口与武器相连

图 17.23 – 枪口与武器相连

  1. PlayerShooting中创建ParticleSystem类型的字段,命名为muzzleEffect,并将父级为枪的枪口效果拖动到其中。现在,我们有了对枪口的ParticleSystem组件的引用来管理它。

  2. 在检查是否正在射击的if语句中,执行muzzleEffect.Play();以播放粒子系统。它将自动停止,并且足够短,可以在按键压力之间完成:

图 17.24 - 枪口与武器相连

图 17.24 - 枪口与武器相连

重要提示:

在这里,我们再次面临同样的问题:所有武器在射击时都会有枪口吗?在这种情况下,由于我们项目的范围,我会说是的,所以我会保持代码不变。但是,在将来,如果您需要其他组件知道此脚本是否在射击,可以创建一个onShoot事件。这样,您可以扩展射击行为。考虑使用事件作为启用脚本中的插件的一种方式。

现在我们已经有了一些 VFX,让我们添加音效。

脚本音频反馈

VFX 为游戏中发生的事情增加了很好的沉浸感,但我们可以通过声音进一步改进。让我们开始通过以下方式向爆炸效果添加声音:

  1. 下载爆炸音效。

  2. 选择爆炸预制件并向其添加Audio Source

  3. 将下载的爆炸音频剪辑设置为音频源的AudioClip属性。

  4. 确保Play On Awake已选中,并且Loop未选中在Audio Source下。

  5. Spatial Blend滑块设置为3D并测试声音,根据需要配置3D 声音设置:图 17.25 - 爆炸时添加声音

图 17.25 - 爆炸时添加声音

正如您在这里所看到的,我们不需要使用任何脚本。由于声音已添加到预制件中,它将在实例化预制件的那一刻自动播放。现在,让我们通过以下方式集成射击声音:

  1. 下载射击声音,并通过音频源添加到玩家的武器中,这次取消Play On Awake复选框,并再次将Spatial Blend设置为3D

  2. PlayerShooting脚本中,创建AudioSource类型的字段,命名为shootSound,并将武器拖动到此属性中,以将脚本与武器中的AudioSource变量连接起来。

  3. 在检查是否可以射击的if语句中,添加shootSound.Play();行以执行射击时的声音,使用相同的逻辑应用于粒子系统:

图 17.26 - 射击时添加声音

图 17.26 - 射击时添加声音

另一种方法是与我们处理爆炸时的方法相同;只是将射击声音添加到子弹中,但如果子弹与墙壁碰撞,很快声音就会被切断。或者,如果将来我们想要自动武器声音,就需要将其实现为一个单一的循环声音,当我们按下相关按键时开始,松开按键时停止。这样,当我们射出太多子弹时,可以防止太多声音实例重叠。在选择脚本反馈的方法时,请考虑这些情景。

现在我们已经完成了音频反馈,让我们完成集成我们在第十二章中准备的动画资产,使用 Animator、Cinemachine 和 Timeline 创建动画

脚本动画

第十二章使用 Animator、Cinemachine 和 Timeline 创建动画,我们创建了一个动画控制器,作为整合多个动画的一种方式,并为其添加了参数,以控制动画之间的过渡何时执行。现在,是时候做一些脚本,使这些参数受到玩家实际行为的影响,并通过以下方式匹配玩家当前状态:

  1. PlayerShooting脚本中,使用Awake中的GetComponent添加对Animator的引用,并将其缓存在字段中:图 17.27 - 缓存 Animator 引用

图 17.27 - 缓存 Animator 引用

  1. 在检查我们是否在射击的if语句中调用animator.SetBool("Shooting", true);函数,并在if语句的else子句中添加相同的函数,但将false作为第二个参数传递。此函数将修改动画控制器的"Shooting"参数:图 17.28 - 根据我们是否在射击来设置 Shooting 布尔值

图 17.28 - 根据我们是否在射击来设置 Shooting 布尔值

如果您测试此功能,可能会注意到一个错误-动画没有播放。如果您检查脚本,您会注意到它只会在一个帧中为true,因为我们使用GetKeyDown,所以 Shooting 布尔值将立即在下一帧被设置为false。我们可以在这里实现的几种解决方案之一是,使我们的射击脚本在按住键时重复射击动作,而不是释放并再次点击以射出另一颗子弹。

  1. 查看以下截图以获取解决方案,并尝试理解逻辑:

图 17.29 - 重复射击脚本

图 17.29 - 重复射击脚本

如您所见,我们的脚本现在使用GetKey来保持按住射击按钮时持续射击,并且为了防止在每一帧中射击,我们将当前时间与上次射击时间进行比较,以检查自上次射击以来经过了多少时间。我们创建了fireRate字段来控制射击之间的时间。

对于动画控制器的Velocity参数,我们可以检测Rigidbody的速度矢量的大小(以米/秒为单位),并将其设置为当前值。这可以完全与PlayerMovement脚本分离,因此在其他情况下我们可以重复使用这个。因此,我们需要一个脚本,如下所示,它只是将Rigidbody组件的速度与animatorVelocity参数连接起来:

图 17.30 - 重复射击脚本

图 17.30 - 重复射击脚本

您可能需要稍微增加动画控制器的转换条件中使用的0.01过渡阈值,因为Rigidbody在释放键后仍在移动。对我来说,使用1效果非常好。另一个选择是增加玩家的阻力和速度,使角色更快地停下来。选择最适合您的方法。

如您所见,我们可以收集有关玩家实际移动和射击动作的数据,以通知动画控制器其状态,以便它可以做出相应的反应。

总结

反馈是视频游戏中的一个重要主题。它为玩家提供宝贵的信息,例如敌人的位置(如果有 3D 声音设置)、远处射击的枪口火光、生命条指示玩家即将死亡、根据玩家动作反应的动画等。在本章中,我们看到了不同形式的反馈,声音、VFX、动画和 UI,这些都是我们在本书的第二部分中已经创建的。在这里,我们学习了如何使用脚本将 UI 连接到游戏中。

现在,您可以编写脚本来让界面、粒子系统和声音根据游戏状态做出反应,包括更改界面上的得分文本或生命条,或在角色射击时播放粒子和声音效果。这将提高玩家在游戏中的沉浸体验。

在下一章中,我们将讨论如何为我们的敌人创建具有挑战性的人工智能。

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

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

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

  • 使用传感器收集信息

  • 使用 FSM 做出决策

  • 执行 FSM 动作

使用传感器收集信息

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

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

  • 创建三过滤器传感器

  • 使用 Gizmos 进行调试

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

创建三过滤器传感器

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

  1. 创建一个名为0,0,0``0,0,0)1,1,1的空GameObject,这样它就会与敌人对齐。虽然我们当然可以直接将所有 AI 脚本放在敌人身上,但我们之所以这样做只是为了分离和组织:图 18.1 - AI 脚本容器

图 18.1 - AI 脚本容器

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

  2. 创建两个float类型的字段,分别命名为distanceangle,另外创建两个LayerMask类型的字段,分别命名为obstaclesLayersObjectsLayersdistance将用作视觉距离,angle将确定视野锥的幅度,ObstacleLayers将被我们的障碍物检查使用,以确定哪些对象被视为障碍物,ObjectsLayers将用于确定我们希望视线检测到的对象类型。我们只希望视线看到敌人;我们对墙壁或道具等对象不感兴趣。LayerMask是一种属性类型,允许我们在代码中选择一个或多个层,因此我们将通过层来过滤对象。稍后你将看到我们如何使用它:图 18.2 - 用于参数化我们视线检查的字段

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

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

重要说明

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

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

  1. 遍历函数返回的对象数组:图 18.3 获取特定距离处的所有对象

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

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

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

  1. 我们可以使用Vector3.Angle函数来计算两个方向之间的角度。在我们的情况下,我们可以计算朝向敌人的方向和我们的前向量之间的角度:图 18.5 计算两个方向之间的角度

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

重要信息

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

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

  2. 使用Physics.Line函数在我们的位置和碰撞体位置之间创建一条虚拟线,以检测在第三个参数中指定的层(obstacles层)中的对象,并返回一个boolean,指示该射线是否击中了某物体。这个想法是使用这条线来检测我们和检测到的碰撞体之间是否有障碍物,如果没有障碍物,这意味着我们对该对象有直线视线。再次提醒,这个函数依赖于障碍物对象有碰撞体,而在我们的情况下,我们有(墙壁、地板等):图 18.6 - 使用线性投射检查传感器和目标对象之间的障碍物

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

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

图 18.7 - 完整的传感器脚本

重要信息

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

  1. 在编辑器中,根据需要配置传感器。在这种情况下,我们将ObjectsLayer设置为Player,这样我们的传感器将专注于具有该层的对象,并将obstaclesLayer设置为Default,这是我们用于墙壁和地板的层:图 18.8 传感器设置

图 18.8 - 传感器设置

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

图 18.9 传感器捕捉玩家

图 18.9 - 传感器捕捉玩家

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

使用 Gizmos 进行调试

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

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

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

  2. 使用Gizmos.DrawWireSphere函数,将我们的位置作为第一个参数,距离作为第二个参数,以在我们的位置绘制一个半径为我们距离的球体。您可以检查随着更改距离字段而 Gizmo 大小的变化:图 18.10 球体 Gizmo

图 18.10 - 球体 Gizmo

  1. 可选地,您可以更改 Gizmo 的颜色,设置Gizmos.color然后调用绘图函数:图 18.11 Gizmos 绘图代码

图 18.11 - Gizmos 绘图代码

重要信息

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

  1. 我们可以使用Gizmos.DrawRay来绘制代表锥体的线,它接收要绘制的线的起点和线的方向,可以乘以某个值来指定线的长度,如下面的屏幕截图所示:图 18.12 绘制旋转线

图 18.12 - 绘制旋转线

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

图 18.13 视觉角线

图 18.13 - 视觉角线

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

  1. 首先,让我们调试LineCast未检测到任何障碍物的情况,因此我们需要在我们的传感器和对象之间绘制一条线。我们可以在调用LineCastif语句中调用Debug.DrawLine,如下面的屏幕截图所示:图 18.14 在 Update 中绘制一条线

图 18.14 - 在 Update 中绘制一条线

  1. 在下一个屏幕截图中,您可以看到DrawLine的效果:图 18.15 指向检测到的对象的线

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

  1. 当视线被对象遮挡时,我们还希望以红色绘制一条线。在这种情况下,我们需要知道 Line Cast 的命中位置,因此我们可以使用函数的一个重载,它提供了一个out参数,可以提供有关线碰撞的更多信息,例如命中的位置、法线和碰撞的对象,如下面的屏幕截图所示:图 18.16 获取有关 LineCast 的信息

图 18.16 - 获取有关 Linecast 的信息

重要信息

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

  1. 我们可以使用这些信息在else子句中绘制从我们的位置到命中点的线,当线与某物发生碰撞时:图 18.17 在我们遇到障碍物时绘制一条线

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

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

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

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

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

使用 FSM 做出决策

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

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

  • 创建 FSM

  • 创建转换

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

创建 FSM

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

图 18.19 FSM 示例

图 18.19 - FSM 示例

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

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

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

  3. 创建一个名为currentStateEnemyState类型字段,它将保存我们的 Enemy 的当前状态:图 18.20 EnemyFSM 状态定义

图 18.20 - EnemyFSM 状态定义

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

  2. 根据当前状态在Update中调用这些函数:图 18.21 基于 If 的 FSM

图 18.21 - 基于 If 的 FSM

重要信息

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

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

图 18.22 状态测试

图 18.22 - 状态测试

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

创建转换

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

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

  2. GoToBase函数中,检查Sight组件检测到的对象是否不为null,这意味着我们的视线内有东西。如果我们的 AI 正在前往基地,但在路上检测到一个对象,我们必须切换到Chase状态以追击玩家,所以我们改变状态,如下面的屏幕截图所示:图 18.23 创建转换

图 18.23 - 创建转换

  1. 此外,我们必须在靠近必须受损的对象时切换到AttackBase。我们可以创建一个Transform类型的字段,称为baseTransform,并将基地生命对象拖放到那里,以便我们可以检查距离。记得添加一个名为baseAttackDistancefloat字段,以使该距离可配置:图 18.24 前往基地转换

图 18.24 - 前往基地转换

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

图 18.25 - 追击玩家转换

  1. 对于AttackPlayer,我们需要检查Player是否不在视线内,以返回到GoToBase,或者它是否足够远,以返回追击它。您可以注意到我们将PlayerAttackDistance乘以1.1,使停止攻击的距离比开始攻击的距离大一点;这将防止在玩家接近该距离时快速在攻击和追击之间切换。您可以使其可配置,而不是硬编码1.1图 18.26 - 攻击玩家转换

图 18.26 - 攻击玩家转换

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

  2. 记得你可以使用Gizmos来绘制距离:图 18.27 FSM 小工具

图 18.27 - FSM 小工具

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

图 18.28 敌人 FSM 设置

图 18.28 - 敌人 FSM 设置

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

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

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

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

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

执行 FSM 动作

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

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

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

  • 使用路径规划

  • 添加最后的细节

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

计算我们场景的路径规划

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

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

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

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

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

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

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

  3. 选择NavMesh

图 18.31 生成 NavMesh

图 18.31 - 生成 NavMesh

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

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

使用路径规划

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

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

  1. 选择敌人 Prefab 并向其添加 NavMeshAgent 组件。将其添加到根对象,称为 Enemy,而不是 AI 子对象 - 我们希望整个对象移动。你会看到对象周围有一个圆柱体,表示对象在 NavMesh 中所占据的区域。请记住,这不是一个碰撞体,所以它不会用于物理碰撞:图 18.32 NavMeshAgent 组件

图 18.32 - NavMeshAgent 组件

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

  2. 在 EnemyFSM 脚本的 Awake 事件函数中,使用 GetComponentInParent 函数来缓存 NavMeshAgent 的引用。这将类似于 GetComponent - 它将在我们的 GameObject 中查找组件,但如果组件不存在,这个版本将尝试在所有父级中查找该组件。记得添加 using UnityEngine.AI 行来在这个脚本中使用 NavMeshAgent 类:图 18.33 缓存父级组件引用

图 18.33 - 缓存父级组件引用

重要信息

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

  1. 在 GoToBase 状态函数中,调用 NavMeshAgent 引用的 SetDestination 函数,传递基本对象的位置作为目标:图 18.34 设置我们的 AI 的目的地

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

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

图 18.35 - 停止代理移动

  1. 我们可以对 ChasePlayer 和 AttackPlayer 做同样的操作。在 ChasePlayer 中,我们可以将代理的目的地设置为玩家的位置,在 AttackPlayer 中,我们可以停止移动。在这种情况下,AttackPlayer 可以再次返回到 GoToBase 或 ChasePlayer,所以你需要在这些状态或在进行转换之前将 isStopped 代理字段设置为 false。我们将选择前者,因为这个版本将覆盖其他也会停止代理的状态而不需要额外的代码。我们将从 GoToBase 状态开始:图 18.36 重新激活代理

图 18.36 - 重新激活代理

  1. 然后,继续进行 Chase Player:图 18.37 重新激活代理并追逐玩家

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

  1. 最后,继续进行攻击玩家:图 18.38 停止移动

图 18.38 - 停止移动

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

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

添加最后的细节

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

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

  2. 创建一个名为Shoot的函数,并在AttackBaseAttackPlayer中调用它:图 18.39 射击函数调用

图 18.39 - 射击函数调用

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

图 18.40 - 射击函数代码

重要信息

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

  1. 当代理停止时,不仅移动停止,而且旋转也停止。如果玩家在敌人受到攻击时移动,我们仍然需要敌人面对它以向其方向射击子弹。我们可以创建一个LookTo函数,该函数接收要查看的目标位置,并在AttackPlayerAttackBase中调用它,传递要射击的目标:图 18.41 LookTo 函数调用

图 18.41 - LookTo 函数调用

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

图 18.42 - 面向目标

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

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

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

总结

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

  1. 图 18.45 - 打开射击动画

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

图 18.44 - 访问父级的 Animator 引用

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

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

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

  1. ![图 18.43 - 将 NavMeshAgent 连接到我们的 Animator 控制器我相当确定 AI 不是你想象的那样;你并没有在这里创建任何 SkyNet,但我们已经为挑战我们的玩家完成了一个简单但有趣的 AI,我们可以迭代和调整以适应游戏的预期行为。我们看到了如何通过传感器收集周围的信息,使用 FSM 做出决策并使用不同的 Unity 系统(如寻路和 Animator)来执行这些动作。![图 18.46 关闭射击动画在EnemyFSM脚本中缓存父级Animator的引用。做与访问NavMeshAgent相同的事情:图 18.44 访问父级的 Animator 引用图 18.46 - 关闭射击动画#

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

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

第十九章:场景性能优化

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

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

  • 优化图形

  • 优化处理

  • 优化内存

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

优化图形

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

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

  • 图形引擎简介

  • 使用帧调试器

  • 使用批处理

  • 其他优化

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

图形引擎简介

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

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

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

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

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

重要说明

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

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

使用帧调试器

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

重要提示

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

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

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

  2. 播放游戏,如果要分析性能,请单击窗口左上角的启用按钮(在播放时按Esc重新获得鼠标控制):图 19.2 - 启用帧调试器

图 19.2 - 启用帧调试器

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

重要提示

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

  1. 将滑块从禁用按钮右侧缓慢向右拖动,以查看场景是如何渲染的。每一步都是在 CPU 中执行的给定游戏帧的绘制调用。您还可以观察窗口左侧的列表如何在那一刻突出显示执行的绘制调用的名称:图 19.3 - 分析我们帧的绘制调用

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

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

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

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

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

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

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

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

使用批处理

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

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

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

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

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

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

  1. 搜索我们当前想要使用的渲染管线资产(编辑 | 项目设置 | 图形 | 可编程渲染设置):图 19.6 - 可编程渲染管线设置

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

  1. 高级部分取消选择SRP 批处理器。我们稍后会讨论这个:图 19.7 - 禁用 SRP 批处理器

图 19.7 - 禁用 SRP 批处理器

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

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

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

  4. 打开 Frame Debugger 并单击启用以查看我们立方体的绘制调用列表:图 19.8 - 立方体的绘制调用

图 19.8 - 立方体的绘制调用

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

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

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

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

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

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

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

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

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

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

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

其他优化

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

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

  1. 创建一个空对象并将模型的两个版本作为子对象。您需要使用具有不同细节级别的多个版本的模型,但现在,我们只是要使用一个立方体和一个球来测试这个功能:图 19.11 – 一个带有两个 LOD 网格的单个对象

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

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

  2. 默认的 LOD 组准备支持三个 LOD 网格组,但由于我们只有两个,右键单击一个并单击删除。您还可以选择在之前插入以添加更多 LOD 组:图 19.12 – 移除 LOD 组

图 19.12 – 移除 LOD 组

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

  2. 选择LOD 1并添加立方体:图 19.13 – 将渲染器添加到 LOD 组

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

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

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

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

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

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

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

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

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

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

  5. 遮挡裁剪窗口可见时,选择摄像机并拖动它,看看随着摄像机移动对象是如何被遮挡的:图 19.14 – 左边是正常场景,右边是带有遮挡裁剪的场景

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

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

图 19.15 – 遮挡区域

图 19.15 – 遮挡区域

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

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

优化处理

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

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

  • 检测 CPU 和 GPU 负载

  • 使用 CPU 使用率分析器

  • 一般的 CPU 优化技术

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

检测 CPU 和 GPU 负载

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

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

重要提示

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

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

  1. 打开ProfilerWindow | Analysis | Profiler)。

  2. 在左上角的Profiler Modules下拉菜单中,勾选GPU以启用 GPU 分析器:图 19.16 – 启用 GPU 分析器

图 19.16 – 启用 GPU 分析器

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

  2. 观察窗口中间带有CPUGPU标签的条形图。它应该显示 CPU 和 GPU 消耗了多少毫秒。数字较高的那个将限制我们的帧率,并确定我们是受 GPU 限制还是受 CPU 限制:图 19.17 – 确定我们是受 CPU 限制还是受 GPU 限制

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

  1. 点击标有 Timeline 的按钮,选择 Hierarchy:图 19.18 – 选择 Hierarchy

图 19.18 – 选择 Hierarchy

  1. 当您尝试打开 GPU 分析器时,有可能会看到不支持的消息,在某些情况下会发生这种情况(例如在某些 Mac 设备上)。在这种情况下,另一种查看我们是否受 GPU 限制的方法是在选择 CPU 使用率分析器时,在 CPU/GPU 标签旁边的搜索栏中搜索waitforpresent图 19.19 – 搜索 waitforpresent

图 19.19 – 搜索 waitforpresent

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

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

使用 CPU 使用率分析器

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

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

图 19.20 – 分析 CPU 使用率图

图 19.20 – 分析 CPU 使用率图

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

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

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

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

重要提示

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

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

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

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

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

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

重要提示

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

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

  2. 点击每个功能以在图表中突出显示。处理时间较长的功能将以较粗的条形突出显示,这些是我们将要关注的功能:图 19.22 - 在图表中突出显示的渲染相机功能

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

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

图 19.23 - 启用深度分析

图 19.23 - 启用深度分析

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

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

CPU 优化的一般技术

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

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

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

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

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

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

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

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

图 19.26 – 粒子处理

图 19.26 – 粒子处理

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

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

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

图 19.27 – 禁用代码

图 19.27 – 禁用代码

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

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

优化内存

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

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

  • 内存分配和垃圾收集器

  • 使用内存分析器

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

内存分配和垃圾收集器

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

图 19.32 – 一个简单的对象池

图 19.32 – 一个简单的对象池

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

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

使用内存分析器

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

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

  1. 安装包管理器窗口 | 包管理器)。记得将包模式设置为Unity 注册表并启用预览包(齿轮图标 | 高级项目设置 | 启用预览包)。图 19.33 – 启用预览包

图 19.33 – 启用预览包

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

  2. 玩游戏并在内存分析器窗口中点击捕获玩家按钮:图 19.34 – 启用预览包

图 19.34 – 启用预览包

  1. 点击快照旁边的打开按钮以打开树视图,在这里你可以看到内存按类型分成块:图 19.35 – 内存块

图 19.35 – 内存块

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

  2. 在我的情况下,这减少了 130 MB。还有其他用于其他效果的纹理,例如 HDR。如果您想探索剩余 MB 的来源,请单击块以将其细分为其对象,并根据纹理的名称进行猜测:图 19.36 - 内存块详细信息

图 19.36 - 内存块详细信息

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

重要提示

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

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

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

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

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

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

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

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

图 19.37 - 参考列表

图 19.37 - 参考列表

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

总结

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

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

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

第二十章:构建项目

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

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

  • 构建项目

  • 调试构建

构建项目

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

重要提示

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

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

图 20.1 场景列表顺序

图 20.1 - 场景列表顺序

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

图 20.2 - 目标平台

图 20.2 - 目标平台

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

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

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

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

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

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

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

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

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

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

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

图 20.3 玩家设置

图 20.3 – 玩家设置

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

图 20.4 – 构建游戏

图 20.4 – 构建游戏

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

图 20.5 – 构建错误

图 20.5 – 构建错误

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

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

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

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

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

调试构建

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

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

  • 调试代码

  • 性能分析

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

调试代码

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

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

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

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

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

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

图 20.7 - 显示隐藏文件

图 20.7 - 显示隐藏文件

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

图 20.8 调试目录

图 20.8 - 调试目录

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

图 20.9 开发构建复选框

图 20.9 - 开发构建复选框

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

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

图 20.10 调试错误消息

图 20.10 - 调试错误消息

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

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

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

图 20.12 – 启用脚本调试

图 20.12 – 启用脚本调试

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

性能分析

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

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

  1. 通过构建来玩游戏。

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

  3. 打开性能分析器。

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

图 20.13 对玩家进行性能分析

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

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

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

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

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

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

图 20.15 – 调试游戏玩家的帧

图 20.15 – 调试游戏玩家的帧

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

图 20.16 – 启用窗口模式

图 20.16 – 启用窗口模式

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

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

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

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

总结

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

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

第二十一章:润色

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

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

  • 迭代你的游戏

  • 发布你的游戏

迭代你的游戏

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

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

  • 测试和反馈

  • 解决反馈

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

测试和反馈

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

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

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

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

图 21.1 – 盖塞尔室

图 21.1 – 盖塞尔室

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

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

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

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

解释反馈

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

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

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

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

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

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

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

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

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

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

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

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

发布你的游戏

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

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

  • 预发布

  • 发布

  • 发布后

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

预发布

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

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

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

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

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

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

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

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

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

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

发布

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

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

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

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

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

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

发布后

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

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

总结

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

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

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

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

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

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

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

  • 使用 AR Foundation

  • 为移动设备构建

  • 创建一个简单的 AR 游戏

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

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

使用 AR Foundation

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

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

  • 创建一个 AR Foundation 项目

  • 使用跟踪功能

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

创建一个 AR Foundation 项目

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

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

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

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

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

图 22.2 - 安装 AR Foundation

图 22.2 - 安装 AR Foundation

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

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

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

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

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

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

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

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

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

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

  4. 在同一个菜单中,创建一个AR 会话起源对象,其中包含一个相机图 22.4 - 创建会话对象

图 22.4 - 创建会话对象

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

图 22.5 - 起始 ARSCcene

图 22.5 - 起始 ARSCcene

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

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

图 22.6 - 添加对 URP 的支持

图 22.6 - 添加对 URP 的支持

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

使用跟踪功能

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

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

  1. 向项目添加一个图像,您可以打印或在手机上显示。有一种在现实世界中显示图像的方式是必要的来测试这一点。在这种情况下,我将使用以下图像:图 22.7 - 要跟踪的图像

图 22.7 - 要跟踪的图像

重要提示

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

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

  1. 通过单击Project Panel中的**+按钮并选择XR** | Reference Image Library来创建一个包含我们希望应用程序识别的所有图像的资产,创建一个参考图像库:图 22.8 – 创建参考图像库

图 22.8 – 创建参考图像库

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

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

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

图 22.9 – 添加要识别的图像

图 22.9 – 添加要识别的图像

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

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

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

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

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

图 22.10 – 设置 Tracked Image Manager

图 22.10 – 设置 Tracked Image Manager

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

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

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

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

图 22.12 – 添加 AR Plane Manager 组件

图 22.12 – 添加 AR Plane Manager 组件

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

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

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

  3. 0.01Color属性设置为黑色,并取消选中Use World Space图 22.13 – 设置 Line Renderer

图 22.13 – 设置 Line Renderer

  1. 记得为Line Renderer创建一个合适的着色器材质,并将其设置为渲染器的材质:图 22.14 – 创建 Line Renderer 材质

图 22.14 – 创建 Line Renderer 材质

  1. 另外,创建一个透明材质并在MeshRenderer平面中使用。我们希望能透过它看到真实表面,以便轻松地看到下面的真实表面:图 22.15 – 用于检测平面的材质

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

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

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

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

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

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

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

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

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

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

  4. 创建一个List<ARRaycastHit>类型的私有字段并实例化它;Raycast 将检测我们的射线击中的每个平面,而不仅仅是第一个:图 22.17 – 存储射线击中的列表

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

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

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

  3. 这将向玩家触摸屏幕的方向投射射线,并将击中的结果存储在我们提供的列表中。如果有东西被击中,它将返回true,否则返回false图 22.18 – 发射 AR 射线

图 22.18 – 发射 AR 射线

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

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

图 22.19 – 射线投射器组件

图 22.19 – 射线投射器组件

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

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

为移动设备构建

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

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

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

  • 为 Android 构建

  • 为 iOS 构建

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

为 Android 构建

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

  1. 关闭 Unity 并打开Unity Hub

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

  3. 单击 Unity 版本右上角的三个点按钮,然后单击Add Modules图 22.20 – 向 Unity 版本添加模块

图 22.20 – 向 Unity 版本添加模块

  1. 确保勾选Android Build Support以及单击左侧箭头时显示的子选项。如果没有,请勾选它们,然后单击右下角的Done按钮进行安装:图 22.21 – 向 Unity 添加 Android 支持

图 22.21 – 向 Unity 添加 Android 支持

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

  2. 进入Build SettingsFile | Build Settings)。

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

图 22.22 – 切换到 Android 构建

图 22.22 – 切换到 Android 构建

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

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

  2. 单击External Tools

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

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

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

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

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

  2. 取消选中Multithreaded RenderingAuto Graphics API

  3. Graphics APIs列表中删除Vulkan

  4. Minimum API Level设置为Android 7.0

图 22.24 – AR Core 设置

图 22.24 – AR Core 设置

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

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

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

图 22.25 – 查找构建号

图 22.25 – 查找构建号

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

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

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

  4. 连接您的设备(如果已连接,请重新连接)。设备上将出现允许 USB 调试的选项。选择始终允许并点击确定图 22.26 – 允许 USB 调试

图 22.26 – 允许 USB 调试

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

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

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

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

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

为 iOS 构建

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

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

  1. 获取一台 Mac 电脑。

  2. 获取一个 iOS 设备。

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

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

  5. 检查 Unity Hub 中是否安装了 iOS 构建支持。有关此步骤的更多信息,请参考在 Android 上构建部分:图 22.27 - 启用 iOS 构建支持

图 22.27 - 启用 iOS 构建支持

  1. 构建设置下切换到 iOS 平台,选择 iOS 并点击切换平台按钮:图 22.28 - 切换到 iOS 构建

图 22.28 - 切换到 iOS 构建

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

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

  1. 双击生成的文件夹中的.xcproject文件:图 22.29 - Xcode 项目文件

图 22.29 - Xcode 项目文件

  1. 转到Xcode | 首选项

  2. 帐户选项卡中,点击窗口左下角的**+**按钮,并使用您注册为苹果开发者的苹果帐户登录:图 22.30 - 帐户设置

图 22.30 - 帐户设置

  1. 连接您的设备,并从窗口左上角选择它,现在应该显示通用 iOS 设备图 22.31 - 选择设备

图 22.31 - 选择设备

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

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

  3. 个人团队中:图 22.32 - 选择团队

图 22.32 - 选择团队

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

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

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

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

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

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

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

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

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

创建一个简单的 AR 游戏

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

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

  • 生成玩家和敌人

  • 编写玩家和敌人的行为

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

生成玩家和敌人

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

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

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

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

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

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

  3. 您将注意到使用Physics.IgnoreCollision来防止生成器与Spawner对象发生碰撞,获取两个对象的碰撞体并将它们传递给函数。您也可以使用层碰撞矩阵来防止碰撞,就像我们在本书的主要项目中所做的那样,如果您愿意的话:图 22.34 – 生成器脚本

图 22.34 – 生成器脚本

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

  2. 将生成器的Prefab属性设置为在所需的时间频率生成我们的敌人:图 22.35 – 配置生成器

图 22.35 – 配置生成器

  1. AR Session Origin对象中添加一个新的SpawnerPlacer自定义脚本,使用 AR 射线系统在玩家点击的地方实例化一个预制体,如下截图所示:图 22.36 – 放置生成器

图 22.36 – 放置生成器

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

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

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

编写玩家和敌人的行为

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

  1. 创建一个类似于下图所示的PlayerManager脚本,并将其添加到玩家:图 22.37 – 创建 PlayerManager 脚本

图 22.37 – 创建 PlayerManager 脚本

  1. 现在敌人已经有了对玩家的引用,让我们通过添加一个LookAtPlayer脚本使它们朝向玩家,如下所示:图 22.38 – 创建 LookAtPlayer 脚本

图 22.38 – 创建 LookAtPlayer 脚本

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

图 22.39 – 创建 MoveForward 脚本

图 22.39 – 创建 MoveForward 脚本

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

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

  2. 创建一个像下面截图中所示的EnemyManager脚本,并将其添加到场景中的一个空的EnemyManager对象中:图 22.40 – 创建 EnemyManager 脚本

图 22.40 – 创建 EnemyManager 脚本

  1. Enemy脚本中,确保在EnemyManager中注册对象,就像我们之前在本书的主项目中使用WavesManager一样:图 22.41 – 创建 Enemy 脚本

图 22.41 – 创建 Enemy 脚本

  1. 创建一个像下面截图中所示的LookAtNearestEnemy脚本,并将其添加到Player预制件中,使其朝向最近的敌人:图 22.42 – 瞄准最近的敌人

图 22.42 – 瞄准最近的敌人

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

  1. 创建一个像下面截图中所示的Life脚本,并将其添加到Life中,而不需要每帧检查生命是否已经降至零。我们创建了一个Damage函数来检查是否造成了伤害(执行了Damage函数),但本书项目的另一个版本也可以工作:图 22.43 – 创建 Life 组件

图 22.43 – 创建 Life 组件

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

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

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

  4. Bullet预制件添加一个像下面截图中所示的Damager脚本,使子弹对其触及的物体造成伤害。记得设置伤害:图 22.44 – 创建 Damager 脚本 – 第一部分

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

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

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

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

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

总结

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

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

posted @ 2024-05-17 17:50  绝不原创的飞龙  阅读(106)  评论(0编辑  收藏  举报