Unity-5-x-秘籍-全-
Unity 5.x 秘籍(全)
原文:
zh.annas-archive.org/md5/6f407f8e8e443d908d4bdd7209db6b26译者:飞龙
前言
游戏开发是一项广泛而复杂的任务。它是一个跨学科领域,涵盖了从人工智能、角色动画、数字绘画到声音编辑等各种各样的主题。所有这些知识领域都可以转化为数百(或数千!)多媒体和数据资产的生产。需要一个特殊的软件应用——游戏引擎——来将这些资产整合成一个单一的产品。
游戏引擎是专门的软件组件,过去它们属于一个神秘的领域。它们价格昂贵、缺乏灵活性,且使用起来极其复杂。它们只为大型工作室或硬核程序员所使用。然后 Unity 出现了。
Unity 代表了游戏开发的真正民主化。它是一个用户友好且多功能的引擎和多媒体编辑环境。它有免费和 Pro 版本;后者包括更多功能。在我们撰写这篇前言时,Unity 提供了以下部署选项:
-
移动: Android、iOS、Windows Phone 和 BlackBerry
-
Web: WebGL
-
桌面: PC、Mac 和 Linux 平台
-
游戏机: PS4、PS3、Xbox One、XBox 360、PlayStation Mobile、PlayStation Vita 和 Wii U
-
虚拟现实(VR)/增强现实(AR): Oculus Rift 和 Gear VR
现在,Unity 被全球各地的各种开发者社区所使用。其中一些是学生和爱好者,但许多是商业组织,从车库开发者到国际工作室,他们使用 Unity 制作了大量的游戏——其中一些你可能已经在某个平台上玩过了。
本书提供了超过 100 个 Unity 游戏开发食谱。一些食谱展示了 Unity 应用程序的多媒体功能技术,包括与动画的工作以及使用预安装的包系统。其他食谱使用 C#脚本开发游戏组件,从处理数据结构和数据文件操作,到为计算机控制的角色的人工智能算法。
如果你想要以有组织且直接的方式开发高质量的游戏,并想学习如何创建有用的游戏组件和解决常见问题,那么 Unity 和这本书都是为你准备的。
本书涵盖的内容
第一章,核心用户界面 – 消息、菜单、得分和计时器,充满了用户界面(UI)食谱,帮助你通过交互视觉元素的质量来提高游戏的娱乐性和享受价值。你将学习到广泛的用户界面技术,包括可更新的文本和图像、方向雷达、倒计时计时器和自定义鼠标光标。
第二章, 库存 GUI,展示了众多游戏如何涉及玩家收集物品,例如开门的钥匙、武器的弹药,或从一系列物品中选择,如从施法咒语的集合中选择。本章提供的配方提供了一系列文本和图形解决方案,用于向玩家显示库存状态,包括他们是否携带物品,或他们能收集的最大物品数量。
第三章, 2D 动画,包括强大的 2D 动画和物理功能。在本章中,我们提供了配方,帮助你理解 Unity 中不同动画元素之间的关系,探讨身体不同部位的移动以及使用包含一系列精灵帧图片的精灵图集图像文件。
第四章, 创建地图和材质,包含的配方将帮助你更好地理解如何使用 Unity 5 的新物理着色器来使用地图和材质,无论你是否是游戏艺术家。它是一个锻炼图像编辑技能的绝佳资源。
第五章, 使用摄像机,解释了涵盖控制并增强游戏摄像机技术的配方。本章将展示如何与单个和多个摄像机一起工作的有趣解决方案。
第六章, 灯光和效果,提供了一种实用的方法来了解 Unity 的照明系统功能,如饼干纹理、反射贴图、光照贴图、光照和反射探针以及过程天空盒。此外,它还演示了投影仪的使用。
第七章, 控制 3D 动画,专注于角色动画,并演示如何利用 Unity 的动画系统——Mecanim。它涵盖了从基本角色设置到过程动画和 ragdoll 物理的广泛主题。
第八章, 角色 GameObject 的位置、移动和导航,提供了一系列用于计算机控制对象和角色的方向性配方,这些配方可以使游戏拥有更丰富和更令人兴奋的用户体验。这些配方的例子包括出生点、检查点和航标点。它还包括使一组对象聚集在一起的例子,以及使用 Unity NavMeshes 在地面和障碍物周围进行自动路径查找。
第九章, 播放和操作声音,致力于使你的游戏中的声音效果和配乐音乐更加有趣。本章演示了如何通过脚本、混响区域和 Unity 的新音频混音器在运行时操作声音。
第十章,处理外部资源文件和设备,阐述了外部数据如何通过添加可更新内容、与网站通信等方式增强你的游戏。该章节还包括使用 Unity Cloud 自动化构建的食谱,以及如何构建项目以便使用 GitHub 等在线版本控制系统轻松备份。
第十一章,使用额外功能和优化改进游戏,提供了几个添加额外功能到你的游戏的食谱想法(例如添加慢动作和确保在线游戏的安全性)。该章节中的许多其他食谱提供了如何调查和可能改进游戏代码的效率和性能的示例。
第十二章,编辑器扩展,提供了几个增强 Unity 编辑器设计时工作的食谱。编辑器扩展是脚本和多媒体组件,允许使用自定义文本、游戏参数的 UI 展示、检查器和场景面板中的数据,以及自定义菜单和菜单项。这些可以促进工作流程改进,从而使游戏开发者能够更快、更轻松地实现目标。
你需要这本书的物品
你只需要一份 Unity 5.x 的副本,可以从www.unity3d.com免费下载。
如果你希望为第四章中的食谱创建自己的图像文件,创建地图和材质,你还需要一个图像编辑器,例如 Adobe Photoshop,可以在www.photoshop.com找到,或者 GIMP,它是免费的,可以在www.gimp.org找到。
这本书适合谁
这本书是为任何想要探索 Unity 脚本和多媒体功能的广泛范围,并找到许多游戏功能现成解决方案的人而写的。程序员可以探索多媒体功能,多媒体开发者可以尝试脚本编写。
从中级到高级用户,从艺术家到程序员,这本书是为你和你团队中的每个人而写的!
它是为那些有 Unity 使用基础和一点 C#编程知识的人而设计的。
部分
在这本书中,你会发现几个经常出现的标题。
为了清楚地说明如何完成食谱,我们按照以下方式使用这些部分:
准备工作
这一部分告诉你可以在食谱中期待什么,并描述如何设置任何软件或任何为食谱所需的初步设置。
如何做…
这一部分包含遵循食谱所需的步骤。
它是如何工作的…
这一部分通常包含对上一部分发生情况的详细解释。
还有更多…
本节包含有关食谱的附加信息,以便使读者对食谱有更深入的了解。
参见
本节提供了对其他有用信息的链接,以帮助读者更好地了解食谱。
惯例
在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、文件夹名称、文件名、文件扩展名、路径名和用户输入如下所示:"对于这个食谱,我们在1362_01_01文件夹中的Fonts文件夹中准备了您需要的字体。”
网址如下所示:在他们的手册页面上了解更多关于 Unity UI 的信息,网址为 docs.unity3d.com/Manual/UISystem.html。
代码块设置如下:
void Start (){
textClock = GetComponent<Text>();
}
void Update (){
DateTime time = DateTime.Now;
string hour = LeadingZero( time.Hour );
string minute = LeadingZero( time.Minute );
string second = LeadingZero( time.Second );
textClock.text = hour + ":" + minute + ":" + second;
}
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:"在层次结构面板中,将一个UI | 文本游戏对象添加到场景中 - 选择菜单:GameObject | UI | Text。”
注意
警告或重要注意事项以如下框的形式出现。
小贴士
小贴士和技巧看起来像这样。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。
要向我们发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍标题。
如果你在某个主题上有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,网址为 www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲拥有者,我们有许多事情可以帮助你从购买中获得最大收益。
下载示例代码和彩色图像
完成书中食谱所需的所有文件都可以从以下网址下载:github.com/dr-matt-smith/unity-5-cookbook-codes。
可下载的代码已完全注释,并且每个食谱都提供了完成的 Unity 项目。此外,你还可以在这个存储库中找到一个包含每章彩色图像的文件夹。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将在勘误部分显示。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。
问题和建议
如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章. 核心 UI – 消息、菜单、得分和计时器
在本章中,我们将涵盖:
-
显示 "Hello World" UI 文本消息
-
显示数字时钟
-
显示数字倒计时计时器
-
创建渐隐的消息
-
显示透视 3D 文本消息
-
显示图像
-
创建 UI 按钮,在场景间切换
-
在面板内组织图像并通过按钮更改面板深度
-
显示交互式 UI 滑块的值
-
使用 UI 滑块图形化显示倒计时计时器
-
显示雷达以指示对象的相对位置
-
使用 Fungus 开源对话框系统创建 UI
-
设置自定义鼠标光标图像
-
文本输入字段组件
-
通过切换组使用切换和单选按钮
简介
对大多数游戏娱乐和享受贡献关键要素的是视觉体验的质量,而这其中重要的部分是用户界面(UI)。UI 元素包括用户与游戏交互的方式(例如按钮、光标、文本框等),以及游戏向用户展示最新信息的方式(例如剩余时间、当前生命值、得分、剩余生命或敌人位置)。本章充满了 UI 烹饪配方,为你提供一系列创建游戏 UI 的示例和想法。
整体概念
每个游戏都不同,因此本章试图完成两个关键角色。第一个目标是提供创建各种 Unity 5 UI 元素的逐步说明,并在适当的地方将它们与代码中的游戏变量关联。第二个目标是提供丰富的说明,说明 UI 元素可以用于各种目的,以便你可以获得关于如何使 Unity 5 UI 控件集为正在开发的游戏提供特定视觉体验和交互的灵感。
基本 UI 元素可以提供静态图像和文本,仅使屏幕看起来更有趣。通过使用脚本,我们可以更改这些图像和文本对象的内容,以便更新玩家的分数,或者我们可以显示棍人图像来指示玩家剩余的生命,等等。其他 UI 元素是交互式的,允许用户点击按钮、选择选项、输入文本等。更复杂的 UI 类型可能涉及收集和计算有关游戏的数据(例如剩余时间的百分比或敌人击中伤害;或场景中关键 GameObjects 的位置和类型,以及它们与玩家位置和朝向的关系),然后以自然、图形化的方式显示这些值(例如进度条或雷达屏幕)。
与 Unity UI 开发相关的核心 GameObjects、组件和概念包括:
-
Canvas:每个 UI 元素都是Canvas的子对象。一个场景中可以有多个Canvas GameObjects。如果还没有Canvas,则在创建新的 UI GameObject 时,会自动创建一个,并且该 UI 对象将成为新Canvas GameObject 的子对象。
-
EventSystem:需要一个EventSystem GameObject 来管理 UI 控件的交互事件。第一个 UI 元素创建时,会自动创建一个。
-
面板:UI 对象可以通过 UI 面板(逻辑上和物理上)组合在一起。面板可以扮演多个角色,包括为相关控制组在层次结构中提供一个 GameObject 父对象。它们可以提供视觉背景图像,以图形化地关联屏幕上的控件,并且如果需要,还可以添加脚本化的调整大小和拖动交互。
-
视觉 UI控件:可见的 UI 控件包括按钮、图像、文本、切换等。
-
交互 UI控件:这些是非可见组件,被添加到 GameObject 中;例如包括输入字段和切换组。
-
Rect Transform组件:UI GameObjects 可以存在于与 2D 和 3D 场景渲染的相机不同的空间中。因此,UI GameObjects 都具有特殊的Rect Transform组件,它具有与场景的 GameObject Transform组件(具有直接的 X/Y/Z 位置、旋转和缩放属性)不同的属性。与Rect Transforms相关联的是枢轴点(缩放、调整大小和旋转的参考点)和锚点。下面将详细介绍这些核心功能。
-
兄弟深度:UI 元素的从下到上的显示顺序(什么在什么上面显示)最初由它们在层次结构中的顺序决定。在设计时,可以通过将 GameObject 拖动到层次结构中所需的顺序来手动设置。在运行时,我们可以向 GameObject 的Rect Transforms发送消息以动态更改它们的层次结构位置(因此,显示顺序),以满足游戏或用户交互的需求。这在本章的在面板内组织图像并通过按钮更改面板深度食谱中得到了说明。
下图显示了有四个主要的 UI 控件类别,每个类别都在一个画布GameObject 中,并通过一个事件系统GameObject 进行交互。UI 控件可以有自己的画布,或者多个 UI 控件可以位于同一个画布中。这四个类别是:静态(仅显示)和交互式 UI 控件、不可见组件(例如用于分组一组互斥的单选按钮)以及 C#脚本类,通过程序代码中编写的逻辑来管理 UI 控件的行为。请注意,不是画布的子控件或后代控件将无法正常工作,如果缺少事件系统,交互式 UI 控件也无法正常工作。画布和事件系统GameObject 都会在将第一个 UIGameObject 添加到场景时自动添加到层次结构中。

UI 矩形变换表示一个矩形区域,而不是一个单独的点,这与场景 GameObject 变换的情况不同。矩形变换描述了 UI 元素相对于其父元素应该如何定位和调整大小。矩形变换具有可以更改的宽度和高度,而不会影响组件的局部缩放。当对 UI 元素的矩形变换进行缩放时,这也会缩放切片图像上的字体大小和边框等。如果所有四个锚点都位于同一点,那么调整画布大小将不会拉伸矩形变换。它只会影响其位置。在这种情况下,我们将看到X 位置和Y 位置属性,以及矩形的宽度和高度。然而,如果锚点不在同一点,那么画布大小的调整将导致元素矩形的拉伸。因此,我们将看到左和右的值——矩形的水平边相对于画布边的位置,其中宽度将取决于实际的画布宽度(对于上/下/高度也是如此)。
Unity 提供了一套预设值用于旋转中心和锚点,使得最常见的值可以非常快速和容易地分配给元素的矩形变换。以下截图显示了 3 x 3 网格,它允许你快速选择左、右、上、下、中间、水平和垂直值。此外,右侧的额外列提供了水平拉伸预设,底部额外的行提供了垂直拉伸预设。使用SHIFT和ALT键在点击预设时设置旋转中心和锚点。

Unity 手册提供了对矩形变换的非常好的介绍。此外,Ray Wenderlich 的两个部分 Unity UI 网络教程也展示了矩形变换、旋转中心和锚点的概述。Wenderlich 教程的两个部分都很好地使用了动画 GIF 来展示旋转中心和锚点不同值的效应:
有三种画布渲染模式:
-
屏幕空间 - 覆盖:在此模式下,UI 元素显示时不参考任何摄像机(场景中不需要任何摄像机)。UI 元素在场景内容的任何类型的摄像机显示之前(覆盖)呈现。
-
屏幕空间 - 摄像机:在此模式下,画布被视为摄像机场景视锥体(观察空间)中的一个平面——其中此平面始终面向摄像机。因此,位于此平面之前的所有场景对象都将渲染在画布上的 UI 元素之前。如果屏幕大小、分辨率或摄像机设置发生变化,画布将自动调整大小。
-
世界空间:在此模式下,画布作为摄像机场景视锥体(观察空间)中的一个平面——但该平面不是始终面向摄像机。画布的显示方式与场景中的任何其他对象一样,相对于摄像机观察视锥体中(如果有的话)画布平面的位置和方向。
在本章中,我们专注于屏幕空间 - 覆盖模式。但所有这些配方同样可以用于其他两种模式。
发挥创意!本章旨在作为想法、技术和可重用C#脚本的发射台,用于您的项目。了解 Unity UI 元素的范畴,并尝试聪明地工作。通常,一个 UI 元素具有您可能需要的几乎所有组件,但您可能需要对其进行某种调整。一个例子可以在使 UI 滑块不可交互的配方中看到,而不是使用它来显示倒计时计时器的红色-绿色进度条。请参阅使用 UI 滑块图形显示倒计时计时器配方。
显示“Hello World”UI 文本消息
使用一种新的计算技术要解决的第一个传统问题通常是显示Hello World消息。在本配方中,你将学习如何创建一个简单的 UI Text 对象,其中包含此消息,以大号白色文本和选定的字体显示,并在屏幕中央。

准备工作
对于这个配方,我们在1362_01_01文件夹中的Fonts文件夹中准备了您需要的字体。
如何操作...
要显示Hello World文本消息,请按照以下步骤操作:
-
创建一个新的 Unity 2D 项目。
-
导入提供的
Fonts文件夹。 -
在层次面板中,将一个UI | 文本GameObject 添加到场景中 - 选择菜单:GameObject | UI | 文本。将此 GameObject 命名为Text-hello。
注意
或者,使用位于层次标签下面的创建菜单,选择菜单:创建 | UI | 文本。
-
确保在层次结构面板中选中您的新Text-helloGameObject。现在,在检查器中,确保以下属性已设置:
-
文本设置为读取
Hello World -
字体设置为
Xolonium-Bold -
字体大小根据您的需求设置(大——这取决于您的屏幕——尝试
50或100) -
对齐设置为水平和垂直居中
-
水平和垂直溢出设置为
Overflow -
颜色设置为白色
以下截图显示了具有这些设置的检查器面板:
![如何操作...]()
-
-
现在,在Rect Transform中,单击锚点预设方框图标,这将导致出现几行几列的预设位置方块。按住SHIFT和ALT并单击中心一个(行中间和列中心)。
![如何操作...]()
-
您的Hello World文本现在将出现在游戏面板中,居中显示。
工作原理...
您已将一个新的Text-helloGameObject 添加到场景中。还会自动创建一个父Canvas和 UI EventSystem。
您设置文本内容和展示属性,并使用Rect Transform锚点预设来确保无论屏幕如何调整大小,文本都将保持水平和垂直居中。
更多内容...
这里有一些您不想错过的更多细节。
使用富文本样式子字符串
每个单独的 UI 文本组件都可以有自己的颜色、大小、粗体样式等。然而,如果您希望快速向要显示给用户的字符串的一部分添加一些突出显示样式,以下是一些不需要创建单独 UI 文本对象的 HTML 样式标记示例:
-
使用"
b"标记加粗文本:I am <b>bold</b> -
使用"
i"标记斜体文本:I am <i>italic</i> -
使用十六进制值或颜色名称设置文本颜色:
I am <color=green>green text</color>,但我现在是 <color=#FF0000>红色</color>注意
在 Unity 在线手册的富文本页面了解更多信息:
docs.unity3d.com/Manual/StyledText.html。
显示数字时钟
不论是现实世界的时间,还是游戏中的倒计时时钟,许多游戏都通过某种形式的时钟或计时器显示得到了增强。要显示的最直接的时钟类型是由小时、分钟和秒的整数组成的字符串,这就是我们在本菜谱中要创建的内容。
以下截图显示了我们将在这个菜谱中创建的时钟类型:

准备中
对于这个菜谱,我们在1362_01_01文件夹中的Fonts文件夹中准备了您需要的字体。
如何操作...
要创建一个数字时钟,请按照以下步骤操作:
-
创建一个新的 Unity 2D 项目。
-
导入提供的
Fonts文件夹。 -
在层次结构面板中,将一个UI | 文本游戏对象添加到场景中,命名为Text-clock。
-
确保在层次结构面板中选择 GameObject Text-clock。现在,在检查器中,确保以下属性已设置:
-
文本设置为读取为
time goes here(此占位文本将在场景运行时被时间替换。) -
字体类型设置为
Xolonium Bold -
字体大小设置为
20 -
对齐设置为水平和垂直居中
-
水平和垂直溢出设置设置为溢出
-
颜色设置为白色
-
-
现在,在矩形变换中,单击锚点预设方图标,这将导致出现几行几列的预设位置方块。按住SHIFT和ALT,然后单击顶部和列中心行。
-
创建一个名为
Scripts的文件夹,并在该新文件夹中创建一个名为ClockDigital的 C#脚本类:using UnityEngine; using System.Collections; using UnityEngine.UI; using System; public class ClockDigital : MonoBehaviour { private Text textClock; void Start (){ textClock = GetComponent<Text>(); } void Update (){ DateTime time = DateTime.Now; string hour = LeadingZero( time.Hour ); string minute = LeadingZero( time.Minute ); string second = LeadingZero( time.Second ); textClock.text = hour + ":" + minute + ":" + second; } string LeadingZero (int n){ return n.ToString().PadLeft(2, '0'); } } -
在层次结构面板中选择 GameObject Text-clock,将您的
ClockDigital脚本拖放到它上面,以将此脚本类的实例作为组件添加到 GameObject Text-clock中,如图所示:![如何操作...]()
-
当您运行场景时,您现在将看到一个数字时钟,显示在屏幕的顶部中央部分,显示小时、分钟和秒。
它是如何工作的...
您已将一个文本GameObject 添加到场景中。您已将该 GameObject 添加了ClockDigital C#脚本类的一个实例。
注意,除了默认为每个新脚本编写的标准两个 C#包(UnityEngine和System.Collections)之外,您还添加了两个额外的 C#脚本包的using语句,即UnityEngine.UI和System。UI 包是必需的,因为我们的代码使用了 UI Text对象;而System包也是必需的,因为它包含我们需要的DateTime类,以便访问运行游戏的计算机上的时钟。
有一个变量textClock,它将是一个对Text组件的引用,我们希望在每个帧中用当前的小时、分钟和秒更新其文本内容。
当场景开始时执行的Start()方法将textClock变量设置为对 GameObject 中Text组件的引用,该组件已添加到我们的脚本对象中。
注意
注意,另一种方法是将textClock设置为public变量。这将允许我们在检查器面板中通过拖放来分配它。
Update()方法在每一帧执行。当前时间存储在time变量中,并通过为变量time的小时、分钟和秒属性添加前导零来创建字符串。
此方法最终更新text属性(即用户看到的字母和数字)为字符串,通过冒号分隔符连接小时、分钟和秒。
LeadingZero(…)方法接受一个整数作为输入,并返回一个字符串,如果该值小于 10,则在左侧添加前导零。
还有更多...
有一些细节你不希望错过。
Unity 教程:如何为模拟时钟动画
Unity 发布了一个关于如何创建 3D 对象并通过 C#脚本动画它们以显示模拟时钟的精彩教程,请参阅unity3d.com/learn/tutorials/modules/beginner/scripting/simple-clock。
显示数字倒计时计时器
这个食谱将向你展示如何显示这里所示的数字倒计时时钟:

准备工作
这个食谱是对之前的食谱的改编。因此,请复制之前食谱的项目,并在此基础上进行工作。
对于这个食谱,我们在1362_01_03文件夹下的Scripts文件夹中准备了你需要的脚本。
如何操作...
要创建一个数字倒计时计时器,请按照以下步骤操作:
-
在检查器面板中,从 GameObject Text-clock中移除脚本组件
ClockDigital。 -
创建一个包含以下代码的
DigitalCountdownC#脚本类,并将其作为脚本组件添加到 GameObject Text-clock中:using UnityEngine; using System.Collections; using UnityEngine.UI; using System; public class DigitalCountdown : MonoBehaviour { private Text textClock; private float countdownTimerDuration; private float countdownTimerStartTime; void Start (){ textClock = GetComponent<Text>(); CountdownTimerReset(30); } void Update (){ // default - timer finished string timerMessage = "countdown has finished"; int timeLeft = (int)CountdownTimerSecondsRemaining(); if(timeLeft > 0) timerMessage = "Countdown seconds remaining = " + LeadingZero( timeLeft ); textClock.text = timerMessage; } private void CountdownTimerReset (float delayInSeconds){ countdownTimerDuration = delayInSeconds; countdownTimerStartTime = Time.time; } private float CountdownTimerSecondsRemaining (){ float elapsedSeconds = Time.time - countdownTimerStartTime; float timeLeft = countdownTimerDuration - elapsedSeconds; return timeLeft; } private string LeadingZero (int n){ return n.ToString().PadLeft(2, '0'); } } -
当你运行场景时,你现在将看到一个从 30 开始的数字时钟倒计时。当倒计时达到零时,将显示消息倒计时已完成。
它是如何工作的...
你已经向场景中添加了一个Text GameObject。你已向该 GameObject 添加了DigitalCountdown C#脚本类的实例。
有一个变量textClock,它将是一个对Text组件的引用,我们希望在每个帧中用剩余时间消息(或计时器完成消息)更新其文本内容。然后调用CountdownTimerReset(…)方法,传递一个初始值为 30 秒的值。
Start()方法(在场景开始时执行)将textClock变量设置为在添加脚本对象的游戏对象中查找Text组件。
Update()方法在每一帧执行。该方法最初将timerMessage变量设置为一条消息,表明计时器已完成(默认要显示的消息)。然后检查剩余秒数是否大于零。如果是这样,则将消息变量的内容更改为显示倒计时剩余的整数(整个)秒数——从CountdownTimerSecondsRemaining()方法中检索。该方法最后更新text属性(即用户看到的字母和数字)为包含关于剩余秒数消息的字符串。
CountdownTimerReset(…)方法记录提供的秒数和调用该方法的时间。
CountdownTimerSecondsRemaining()方法返回剩余秒数的整数值。
创建一个渐隐的消息
有时,我们希望消息只显示一段时间,然后渐隐并消失,这将在下面的屏幕截图中显示:

准备工作
这个食谱改编了本章的第一个食谱,所以请复制那个项目来为这个食谱工作。
对于这个食谱,我们在1362_01_04文件夹中的Scripts文件夹里准备了一个你需要使用的脚本。
如何操作...
要显示渐隐的文本消息,请按照以下步骤操作:
-
导入提供的名为
CountdownTimer的 C#脚本类。 -
确保在层次结构选项卡中选择 GameObject Text-hello。然后,将
CountdownTimerC#脚本类的实例作为此 GameObject 的组件附加。 -
创建一个包含以下代码的 C#脚本类
FadeAway,并将其作为脚本组件添加到 GameObject Text-hello中:using UnityEngine; using System.Collections; using UnityEngine.UI; public class FadeAway : MonoBehaviour { private CountdownTimer countdownTimer; private Text textUI; private int fadeDuration = 5; private bool fading = false; void Start (){ textUI = GetComponent<Text>(); countdownTimer = GetComponent<CountdownTimer>(); StartFading(fadeDuration); } void Update () { if(fading){ float alphaRemaining = countdownTimer.GetProportionTimeRemaining(); print (alphaRemaining); Color c = textUI.material.color; c.a = alphaRemaining; textUI.material.color = c; // stop fading when very small number if(alphaRemaining < 0.01) fading = false; } } public void StartFading (int timerTotal){ countdownTimer.ResetTimer(timerTotal); fading = true; } } -
当你运行场景时,你现在会看到屏幕上的消息会慢慢渐隐,5 秒后消失。
工作原理...
将提供的CountdownTimer脚本类的实例添加为Text-hello GameObject 的组件。
你向 GameObject Text-hello添加了FadeAway脚本类的实例。Start()方法将Text和CountdownTimer组件的引用缓存到countdownTimer和textUI变量中。然后,它调用StartFading(…)方法,传入数字 5,这样消息将在 5 秒后变为不可见。
StartFading(…)方法启动这个计时器脚本组件,倒计时到指定的秒数。它还设置fading布尔标志变量为true。
Update()方法在每个帧中测试fading变量是否为true。如果是,则将Text-hello对象的颜色的 alpha(透明度)组件设置为介于 0.0 和 1.0 之间的值,基于CountdownTimer对象剩余时间的比例。最后,如果剩余时间的比例小于一个非常小的值(0.01),则将fading变量设置为false(以节省处理工作,因为文本现在不可见)。
显示透视 3D 文本消息
Unity 提供了一个通过TextMesh组件显示 3D 文本的替代方法。虽然这非常适合场景中的文本(如广告牌、路标以及通常在可能近距离看到的 3D 物体旁边的文字),但它创建起来很快,是创建有趣菜单或指令场景等的一种方式。
在这个食谱中,你将学习如何创建一个滚动的 3D 文字,模拟电影星球大战著名的开场字幕,看起来就像这样:

准备工作
对于这个食谱,我们在1362_01_04文件夹中的Fonts文件夹里准备了你需要使用的字体,以及Text文件夹里你需要使用的文本文件。
如何操作...
要显示透视 3D 文本,请按照以下步骤操作:
-
创建一个新的 Unity 3D 项目(这确保我们从一个透视摄像机开始,适合我们想要创建的 3D 效果)。
注意
如果你需要在项目中混合 2D 和 3D 场景,你总是可以通过检查器面板手动设置任何摄像机的相机投影属性为透视或正交。
-
在层次结构面板中,选择主摄像机项,并在检查器面板中设置其属性如下:相机清除标志为纯色,视野为150。还将背景颜色设置为黑色。
-
导入提供的
Fonts文件夹。 -
在层次结构面板中,将一个UI | 文本游戏对象添加到场景中 - 选择菜单:GameObject | UI | Text。将此 GameObject 命名为
Text-star-wars。将其文本内容设置为星球大战(每个单词占一行)。然后,将其字体设置为Xolonium Bold,其字体大小为50。使用Rect Transform中的预设锚点将此 UI 文本对象定位在屏幕顶部中央。 -
在层次结构面板中,将一个3D 文本游戏对象添加到场景中 - 选择菜单:GameObject | 3D Object | 3D Text。将此 GameObject 命名为
Text-crawler。 -
在检查器面板中,设置 GameObject Text-crawler的变换属性如下:位置 (
0,-300,-20), 旋转 (15,0,0)。 -
在检查器面板中,将 GameObject Text-crawler的文本网格属性设置如下:
-
将提供的文本文件
star_wars.txt的内容粘贴到文本中。 -
设置偏移 Z =
20,行间距 =0.8,和锚点 = 中间中心 -
设置字体大小 =
200,字体 =SourceSansPro-BoldIt
-
-
当场景运行时,星球大战的故事文本现在将很好地以 3D 透视形式出现在屏幕上。
它是如何工作的...
你已经模拟了电影《星球大战》的开场屏幕,屏幕顶部有一个平面的 UI 文本对象标题,以及 3D 文本网格,其设置看起来像是随着 3D 透视而消失在地平线上。
更多内容...
有一些细节你不应该错过。
我们必须让这段文字像电影中那样滚动
通过几行代码,我们可以让这段文字在水平方向上滚动,就像电影中那样。将以下 C#脚本类ScrollZ作为组件添加到 GameObject Text-crawler:
using UnityEngine;
using System.Collections;
public class ScrollZ : MonoBehaviour {
public float scrollSpeed = 20;
void Update () {
Vector3 pos = transform.position;
Vector3 localVectorUp = transform.TransformDirection(0,1,0);
pos += localVectorUp * scrollSpeed * Time.deltaTime;
transform.position = pos;
}
}
在每一帧通过Update()方法,将 3D 文本对象的位置移动到该 GameObject 的局部向上方向。
更多信息在哪里
在 Unity 在线手册中了解更多关于 3D 文本和文本网格的信息,请访问docs.unity3d.com/Manual/class-TextMesh.html。
注意
注意:实现类似这种透视文本的另一种方法是将 Canvas 与世界空间渲染模式一起使用。
显示图像
有许多情况我们希望在屏幕上显示图像,包括标志、地图、图标、启动图形等。在这个菜谱中,我们将在屏幕顶部显示一个图像,并使其拉伸以适应屏幕调整后的任何宽度。
以下截图显示了 Unity 显示的图像:

准备工作
对于这个菜谱,我们在1362_01_06文件夹中的Images文件夹中准备了您需要的图像。
如何操作...
要显示拉伸的图像,请按照以下步骤操作:
-
创建一个新的 Unity 3D 项目。
注意
默认情况下,3D 项目将图像导入为纹理,2D 项目将图像导入为精灵(2D 和 UI)。由于我们将使用原始图像UI 组件,我们需要将图像导入为纹理。
-
将游戏面板设置为 400 x 300 的大小。通过菜单:编辑 | 项目设置 | 玩家。确保分辨率 | 默认为全屏设置未勾选,宽度和高度设置为 400 x 300。然后,在游戏面板中,选择独立(400 x 300)。这将允许我们测试图像拉伸至 400 像素的宽度。
-
导入提供的文件夹,该文件夹名为
Images。在检查器选项卡中,确保unity5_learn图像的纹理类型设置为纹理。如果没有,则从下拉列表中选择纹理,并单击应用按钮。以下截图显示了带有纹理设置的检查器选项卡:![如何操作...]()
-
在层次结构面板中,向场景添加一个名为RawImage-unity5的UI | 原始图像游戏对象。
注意
如果您希望防止图像的扭曲和拉伸,则使用 UI 精灵游戏对象,并确保在检查器面板中其图像(脚本)组件中勾选保留纵横比选项。
-
确保在层次结构面板中选择RawImage-unity5游戏对象。从您的项目文件夹(
Images),将unity5_learn图像拖动到原始图像(脚本)公共属性纹理。单击设置原生大小按钮,在拉伸之前预览图像,如图所示:![如何操作...]()
-
现在,在矩形变换中,单击锚点预设方形图标,这将导致出现几行几列的预设位置方块。按住SHIFT和ALT,然后单击顶部行和拉伸列。
-
现在图像将被整齐地放置在游戏面板的顶部,并拉伸至 400 像素的全宽。
工作原理...
您已确保图像的纹理类型设置为纹理。您已向场景添加了一个UI 原始图像控件。原始图像控件被设置为显示unity5_learn图像文件。
图像已定位在游戏面板的顶部,并使用锚点和预设的枢轴,使图像拉伸以填充整个宽度,我们通过玩家设置将其设置为 400 像素。
还有更多...
有一些细节你不希望错过:
与 Sprite 和 UI Image 组件一起工作
如果你只想显示非动画图像,那么纹理图像和 UI RawImage控件就是你的选择。然而,如果你想有更多关于如何显示图像的选项(例如平铺和动画),那么应该使用 UI Sprite控件。这个控件需要将图像文件导入为Sprite(2D 和 UI)类型。
一旦图像文件被拖动到 UI Image控件的Sprite属性中,将提供额外的属性,例如图像类型、保留宽高比等选项。

参见
在第二章中,通过改变平铺图像的大小来显示多个对象拾取的图标的配方中可以找到一个平铺 Sprite 图像的例子,库存 GUI。
创建 UI 按钮在场景间切换
除了玩家玩游戏时的场景,大多数游戏都会有菜单屏幕,这些屏幕向用户显示有关说明、高分、他们已经达到的水平等信息。Unity 提供了 UI 按钮,以便用户可以轻松地在这些屏幕上表示他们的选择。
在这个配方中,我们将创建一个非常简单的游戏,包含两个屏幕,每个屏幕都有一个按钮来加载另一个屏幕,类似于以下截图:

如何操作...
要创建一个按钮可导航的多场景游戏,请按照以下步骤操作:
-
创建一个新的 Unity 2D 项目。
-
保存当前(空)场景,命名为page1。
-
在场景顶部中心位置添加一个 UI 文本对象,包含文本
主菜单 / (page 1),字体大小较大。 -
在屏幕中间中心位置添加一个 UI 按钮。在层次结构面板中,点击显示子项三角形以显示此按钮 GameObject 的 UI 文本子项。选择文本按钮子项 GameObject,并在检查器面板中,对于文本(脚本)组件的文本属性,输入按钮文本
goto page 2,如图所示:![如何操作...]()
-
将当前场景添加到构建中,选择菜单:文件 | 构建设置…。然后,点击添加当前按钮,使page1场景成为构建中的场景列表中的第一个场景。
注意
我们不能告诉 Unity 加载尚未添加到构建场景列表中的场景。我们使用
Application.LoadLevel(…)代码告诉 Unity 加载提供的场景名称(或数字索引)。 -
创建一个包含以下代码的 C#脚本类
MenuActions,并将其作为脚本组件添加到主相机:using UnityEngine; using System.Collections; public class MenuActions : MonoBehaviour { public void MENU_ACTION_GotoPage(string sceneName){ Application.LoadLevel(sceneName); } } -
确保在层次结构中选择了按钮,然后在Inspector 视图中点击按钮(脚本)组件底部的加号“+”按钮,为该按钮创建一个新的OnClick事件处理器。
-
将主相机从层次结构拖到对象槽中——位于说仅运行时的菜单下方。这意味着当按钮接收到OnClick事件时,我们可以从主相机内部的脚本对象中调用一个公共方法。
-
现在,从MenuActions下拉列表中选择MENU_ACTION_GotoPage()方法(最初显示No Function)。在方法下拉菜单下面的文本框中输入
page2(当此按钮被点击时我们想要加载的场景的名称)。当按钮接收到OnClick事件消息时,此page2字符串将被传递给方法,如图所示:![如何操作...]()
-
保存当前场景,创建一个新的空场景,然后将这个新场景保存为page2。
-
按照类似的步骤进行此场景。添加一个显示Instructions / (page 2)文本的UI Text游戏对象,字体大小要大。添加一个显示goto page 1 text的 UI 按钮。
-
将当前场景添加到构建中(因此现在,page1和page2都将列在构建中)。
-
将
MenuActions脚本类的一个实例添加到主相机。 -
在层次结构面板中选择按钮,并添加一个On Click事件处理器,该处理器将传递字符串page1(当此按钮被点击时我们想要加载的场景的名称)给`MENU_ACTION_GotoPage()**方法。
-
保存场景。
-
当你运行page1 场景时,你会看到你的主菜单文本和一个按钮,当点击此按钮时,游戏将加载page2 场景。在场景page2中,你将有一个按钮可以带你回到page1。
它是如何工作的...
你已经创建了两个场景,并将它们都添加到了游戏构建中。每个场景都有一个按钮,当点击(当游戏正在播放时),Unity 将加载(命名)其他场景。这是可能的,因为当每个按钮被点击时,它会运行位于主相机内部的脚本MenuActions组件中的MENU_ACTION_GotoPage(…)方法。此方法输入要加载的场景的文本字符串名称,因此page1 场景中的按钮给出page2的字符串名称作为要加载的场景,反之亦然。
当一个 UI 按钮被添加到层次结构面板时,一个子 UI 文本对象也会自动创建,并且这个 UI 文本子对象的文本属性内容是用户在按钮上看到的文本。
还有更多...
有些细节你不希望错过。
按钮鼠标悬停的视觉动画
有几种方式可以让我们在用户将鼠标光标移到按钮上时,从视觉上告知按钮是交互式的。最简单的是添加一个颜色渐变,当鼠标悬停在按钮上时会出现——这是默认的Transition。在Hierarchy中选择Button后,在Inspector选项卡中为Button (Script)组件的Highlighted Color属性选择一个渐变颜色(例如,红色),如图所示:

另一种通知用户按钮处于活动状态的可视Transition形式是Sprite Swap。在这种情况下,Inspector选项卡中提供了Targeted/Highlighted/Pressed/Disabled不同图像的属性。默认的Targeted Graphic是内置的 Unity Button (image) – 这是当 GameObject 按钮创建时的灰色圆角矩形默认值。将一个看起来非常不同的图像拖入Highlighted Sprite是一个有效的替代方案来设置颜色提示。我们为这个配方提供了项目中的rainbow.png图像,可用于Button鼠标悬停时的Highlighted Sprite。以下截图显示了具有这种彩虹背景图像的按钮:

在鼠标悬停时动画化按钮属性
最后,可以为动态突出显示按钮给用户创建动画,例如,当鼠标悬停在按钮上时,按钮可能会变大,然后当鼠标指针移开时,它可能会缩小回原始大小。这些效果是通过为Transition属性的Animation选项选择,并创建一个具有Normal、Highlighted、Pressed和Disabled状态触发器的动画控制器来实现的。要为鼠标悬停时(高亮状态)的按钮动画放大,请执行以下操作:
-
创建一个新的 Unity 2D 项目。
-
创建一个按钮。
-
在Inspector Button (Script)组件中,将Transition属性设置为Animation。
-
点击Button (Script)组件中(位于Disabled Trigger属性下方)的Auto Generate Animation按钮,如图所示:
![在鼠标悬停时动画化按钮属性]()
-
通过命名为button-animation-controller保存新的控制器。
-
确保在Hierarchy中选择了ButtonGameObject。然后,在Animation面板中,从下拉菜单中选择Highlighted剪辑,如图所示:
![在鼠标悬停时动画化按钮属性]()
-
在Animation面板中,点击红色的record圆形按钮,然后点击Add Property按钮,选择记录对Rect Transform | Scale属性的更改。
-
将会创建两个关键帧,删除第二个在1:00(因为我们不希望有一个“弹跳”按钮),如下面的截图所示。
![鼠标悬停时动画按钮属性]()
-
选择0:00的第一个关键帧(现在只有一个!)然后,在检查器视图中,将Rect Transform组件的X和Y缩放属性设置为(
1.2,1.2)。 -
最后,再次点击红色记录圆形按钮以结束动画更改的录制。
-
保存并运行你的场景,你会看到当鼠标悬停在按钮上时,按钮会平滑地放大,当鼠标移开时,它会平滑地恢复到原始大小。
以下网页提供了关于 UI 动画的视频和基于网页的教程:
-
Unity 按钮过渡教程可在以下位置找到:
-
Ray Wenderlich 的教程(第二部分),包括按钮动画,可在以下位置找到:
通过按钮在面板内组织图像并更改面板深度
Unity 提供了 UI 面板,允许将 UI 控件分组并一起移动,并且还可以(如果需要)通过图像背景视觉分组。兄弟深度决定了哪些 UI 元素将出现在其他元素之上或之下。我们可以在层次结构中明确看到兄弟深度,因为层次结构中 UI GameObjects 的从上到下的顺序设置了兄弟深度。因此,第一个项目的深度为 1,第二个项目的深度为 2,依此类推。具有较大兄弟深度(在层次结构中更靠下)的 UI GameObjects 会出现在具有较小兄弟深度(在层次结构中更靠上)的 UI GameObjects 之上。
在这个菜谱中,我们将创建三个 UI 面板,每个面板显示不同的扑克牌图像。我们还将添加四个三角形排列按钮来更改显示顺序(移到底部,移到顶部,上移一个,下移一个)。

准备工作
对于这个菜谱,我们在 1362_01_08 文件夹中的 Images 文件夹中准备了所需的图像。
如何做到这一点...
要创建用户可以通过点击按钮更改层级的 UI 面板,请按照以下步骤操作:
-
创建一个新的 Unity 2D 项目。
-
创建一个新的 UI 面板,命名为
Panel-jack-diamonds。将其放置在屏幕中间部分,宽度为 200 像素,高度为 300 像素。取消选中此面板的图像(脚本)组件(因为我们不想看到面板的默认半透明灰色矩形背景图像)。 -
创建一个新的 UI 图像,并将此图像作为子项添加到 Panel-jack-diamonds。
-
将Panel-jack-diamonds图像定位在中心中间,并调整其大小为 200 x 300。将Jack-of-diamonds扑克牌图像拖入源图像属性,用于图像(脚本)组件在检查器选项卡中。
-
创建一个名为Button-move-to-front的 UI按钮。将此按钮作为子项添加到Panel-jack-diamonds。删除此按钮的Text子 GameObject(因为我们将使用图标来指示此按钮的功能)。
-
将Button-move-to-front按钮的大小调整为 16 x 16,并将其定位在玩家卡片图像的顶部中心,以便在扑克牌的顶部可以看到它。将
icon_move_to_front排列三角形图标图像拖入源图像属性,用于图像(脚本)组件,在检查器视图中。 -
确保在层级中选择Button-move-to-front按钮。然后,在检查器视图中的Button (Script)组件底部点击加号(+)以为此按钮创建一个新的OnClick事件处理程序。
-
将Panel-jack-diamonds从层级拖到对象槽位(位于说仅限运行时的菜单下方)。
-
现在,从下拉函数列表中选择RectTransform.SetAsLastSibling方法(最初显示为无函数)。
注意
这意味着当按钮接收到OnClick事件时,Panel的RectTransform将收到SetAsLastSibling消息——这将把Panel移到Canvas中 GameObject 的底部,因此将这个Panel移到Canvas中所有其他 GameObject 的前面。
![如何做...]()
-
重复步骤 2;创建一个带有移至最前按钮的第二个面板。将这个第二个面板命名为Panel-2-diamonds,然后将其移动并稍微向Panel-jack-diamonds的右侧定位,以便两个移至最前按钮都能被看到。
-
保存你的场景并运行游戏。你将能够点击任意一张卡片上的“移至最前”按钮,将那张卡片的面板移至最前。如果你以未最大化的游戏面板运行游戏,你实际上会看到面板在层级中Canvas的子项列表中的顺序发生变化。
它是如何工作的...
你已经创建了两个 UI面板,每个面板都包含一张扑克牌的图片和一个按钮,该按钮的动作会使其父面板移至最前。按钮的动作说明了OnClick函数不必是调用对象脚本组件的公共方法,但它可以向目标 GameObject 的组件发送消息——在这个例子中,我们向Panel中的RectTransform发送了SetAsLastSibling消息。
还有更多...
有一些细节你不希望错过。
使用脚本方法仅通过上下移动一个位置
虽然 Rect Transform 提供了有用的 SetAsLastSibling(移动到前面)和 SetAsFirstSibling(移动到后面),以及 SetSiblingIndex(如果我们知道确切的位置),但在 层次 面板中并没有内置的方法来使元素上下移动,只有一个在 GameObject 序列中的单个位置。然而,我们可以用 C# 编写两个简单的方法来实现这一点,并且我们可以添加按钮来调用这些方法,从而提供对屏幕上 UI 控件从上到下排列的完全控制。要实现四个按钮(移动到前面/移动到后面/上移一个/下移一个),请执行以下操作:
-
创建一个名为
ArrangeActions的 C# 脚本类,包含以下代码,并将其作为脚本组件添加到每个 面板 上:using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using System.Collections; public class ArrangeActions : MonoBehaviour { private RectTransform panelRectTransform; void Start(){ panelRectTransform = GetComponent<RectTransform>(); } public void MoveDownOne(){ print ("(before change) " + GameObject.name + " sibling index = " + panelRectTransform.GetSiblingIndex()); int currentSiblingIndex = panelRectTransform.GetSiblingIndex(); panelRectTransform.SetSiblingIndex( currentSiblingIndex - 1 ); print ("(after change) " + GameObject.name + " sibling index = " + panelRectTransform.GetSiblingIndex()); } public void MoveUpOne(){ print ("(before change) " + GameObject.name + " sibling index = " + panelRectTransform.GetSiblingIndex()); int currentSiblingIndex = panelRectTransform.GetSiblingIndex(); panelRectTransform.SetSiblingIndex( currentSiblingIndex + 1 ); print ("(after change) " + GameObject.name + " sibling index = " + panelRectTransform.GetSiblingIndex()); } } -
在每个卡片面板上添加第二个按钮,这次使用名为
icon_move_to_front的排列三角形图标图像,并将这些按钮的 OnClick 事件函数设置为 SetAsFirstSibling。 -
在每个卡片面板上添加两个带有上下三角形图标图像的按钮:
icon_down_one和icon_up_one。将下移一个按钮的 OnClick 事件处理函数设置为调用MoveDownOne()方法,并将上移一个按钮的函数设置为调用MoveUpOne()方法。 -
复制一个面板以创建第三个卡片(这次显示方块 A)。排列三个卡片,以便至少可以看到两张卡片的所有四个按钮,即使这些卡片在底部(参见本食谱开头的截图)。
-
保存场景并运行你的游戏。现在你将完全控制三个卡片面板的分层。
显示交互式 UI 滑块的值
本示例说明如何创建一个交互式 UI 滑块,并在用户更改 滑块值 时执行 C# 方法。

如何操作...
要创建一个 UI 滑块并在屏幕上显示其值,请按照以下步骤操作:
-
创建一个新的 2D 项目。
-
在场景中添加一个 UI 文本 GameObject,字体大小为
30,占位文本为slider value here(当场景开始时,此文本将被滑块值替换)。 -
在 层次 面板中,将一个 UI | 滑块 游戏对象添加到场景中——选择菜单:GameObject | UI | 滑块。
-
在 检查器 选项卡中,修改 Rect Transform 的设置以将滑块放置在屏幕的顶部中间部分,并将文本放置在其下方。
-
在 检查器 选项卡中,将滑块的 最小值 设置为
0,最大值 设置为20,并勾选 整数 复选框,如图所示:![如何操作...]()
-
创建一个名为
SliderValueToText的 C# 脚本类,包含以下代码,并将其作为脚本组件添加到名为 Text 的 GameObject 上:using UnityEngine; using System.Collections; using UnityEngine.UI; public class SliderValueToText : MonoBehaviour { public Slider sliderUI; private Text textSliderValue; void Start (){ textSliderValue = GetComponent<Text>(); ShowSliderValue(); } public void ShowSliderValue () { string sliderMessage = "Slider value = " + sliderUI.value; textSliderValue.text = sliderMessage; } } -
确保在层次结构中选择了文本GameObject。然后,在检查器视图中,将滑块GameObject 拖动到
Slider Value To Text (Script)脚本组件的公共Slider UI变量槽中,如图所示:![如何操作...]()
-
确保在层次结构中选择了滑块GameObject。然后,在检查器视图中,将文本GameObject 拖动到滑块(脚本)脚本组件的公共None (Object)槽中,位于On Value Changed (Single)部分。
注意
你现在已经告诉 Unity 每次滑块改变时应该向哪个对象发送消息。
![如何操作...]()
-
从下拉菜单中选择SliderValueToText和
ShowSliderValue()方法,如图所示。这意味着每次滑块更新时,脚本对象中的ShowSliderValue()方法,在 GameObject 文本中将被执行。![如何操作...]()
-
当你运行场景时,你现在会看到一个滑块。在其下方,你会看到一个
Slider value = <n>形式的文本消息。 -
每次移动滑块时,显示的文本值将(几乎)立即更新。值应从
0(滑块的左侧)到20(滑块的右侧)。注意
屏幕上文本值的更新可能不会立即发生,就像滑块值移动时发生的那样,因为滑块在决定需要触发On Value Changed事件消息时涉及一些计算,然后查找已注册为该事件处理程序的对象的任何方法。然后,需要按顺序执行对象方法中的语句。然而,这一切都应该在几毫秒内完成,并且应该足够快,以便为用户界面操作如改变和移动此滑块提供令人满意的响应性 UI。
工作原理...
你已经为文本GameObject 添加了SliderValueToText类的脚本实例。
当场景首次运行时,会执行Start()方法,将变量设置为对滑块项内文本组件的引用。接下来,会调用ShowSliderValue()方法,以确保场景开始时显示正确(显示初始滑块值)。
这包含ShowSliderValue()方法,它获取滑块的值。它更新显示的文本,使其成为以下形式的消息:Slider value = <n>。
你创建了一个UI 滑块GameObject,并将其设置为 0-20 范围内的整数。
你已经将SliderValueToText脚本组件的ShowSliderValue()方法添加到UI 滑块GameObject 的On Value Changed事件监听器列表中。因此,每次滑块值改变时,它会发送一个消息来调用ShowSliderValue()方法,因此新的值会在屏幕上更新。
使用 UI 滑块图形显示倒计时器
有许多情况,我们希望通知玩家剩余时间的比例,或在某个时间点的某些值完成时,例如,加载进度条,剩余时间或健康与起始最大值的比较,玩家从青春之泉中填充了多少水壶,等等。在此配方中,我们将说明如何移除UI 滑块的交互式'手柄',并更改其组件的大小和颜色,以便我们有一个易于使用的通用进度/比例条。在此配方中,我们将使用修改后的滑块以图形方式向用户展示倒计时器剩余的时间。

准备工作
此配方基于之前的配方进行修改。因此,请复制之前配方中的项目,并在此基础上工作以遵循此配方。
对于此配方,我们在1362_01_10文件夹中的Scripts和Images文件夹中准备了所需的脚本和图像。
如何操作...
要创建一个带有图形显示的数字倒计时器,请按照以下步骤操作:
-
删除文本GameObject。
-
将
CountdownTimer脚本和red_square、green_square图像导入到项目中。 -
确保在层次结构标签中选择了滑块GameObject。
-
禁用手柄滑动区域子 GameObject(通过取消选中它)
-
你会在游戏面板中看到“拖动圆圈”消失(用户不会拖动滑块,因为我们希望这个滑块仅用于显示),如下面的截图所示:
![如何操作...]()
-
选择背景子项:
- 将
red_square图像拖动到图像(脚本)组件的源图像属性中,该组件位于检查器视图中
- 将
-
选择填充子项:
- 将
green_square图像拖动到图像(脚本)组件的源图像属性中,该组件位于检查器标签页
- 将
-
选择填充区域子项:
-
在矩形变换组件中,使用锚点预设位置为左中
-
将宽度设置为 155,将高度设置为 12,如图所示:
![如何操作...]()
-
-
确保在层次结构中选择了滑块GameObject。然后,将名为
CountdownTimer的 C#脚本类实例作为组件附加到此 GameObject。 -
创建一个名为
SliderTimerDisplay的 C#脚本类,包含以下代码,并将其作为脚本组件添加到滑块GameObject 中:using UnityEngine; using System.Collections; using UnityEngine.UI; public class SliderTimerDisplay : MonoBehaviour { private CountdownTimer countdownTimer; private Slider sliderUI; private int startSeconds = 30; void Start (){ SetupSlider(); SetupTimer(); } void Update () { sliderUI.value = countdownTimer.GetProportionTimeRemaining(); print (countdownTimer.GetProportionTimeRemaining()); } private void SetupSlider (){ sliderUI = GetComponent<Slider>(); sliderUI.minValue = 0; sliderUI.maxValue = 1; sliderUI.wholeNumbers = false; } private void SetupTimer (){ countdownTimer = GetComponent<CountdownTimer>(); countdownTimer.ResetTimer(startSeconds); } } -
运行你的游戏,你会看到滑块每秒移动,逐渐显示更多的红色背景,以此表示剩余时间。
工作原理...
你隐藏了Handle Slide Area子组件,使得Slider仅用于显示,用户无法与之交互。Slider的背景颜色设置为红色,因此,随着计数器的下降,越来越多的红色被揭示——警告用户时间正在流逝。Slider的填充设置为绿色,以便剩余的比例以绿色显示(绿色越多,滑块/计时器的值就越大)。
将提供的CountdownTimer脚本类的一个实例添加为滑块的组件。ResetTimer(…)方法记录提供的秒数和方法被调用的时间。GetProportionRemaining()方法返回一个 0.0-1.0 之间的值,表示剩余秒数的比例(1.0 代表所有秒数,0.5 代表一半秒数,0.0 表示没有剩余秒数)。
你在SliderGameObject 中添加了SliderTimerDisplay脚本类的一个实例。Start()方法调用SetupSlider()和SetupTimer()方法。
SetupSlider()方法将sliderUI变量设置为对Slider组件的引用,并设置此滑块映射到 0.0 和 1.0 之间的浮点(小数)值。
SetupTimer()方法将countdownTimer变量设置为CountdownTimer组件的引用,并启动此计时器脚本组件从 30 秒开始倒计时。
在每一帧中,Update()方法将滑块值设置为从运行计时器中调用GetProportionRemaining()方法返回的浮点数。
注意
尽可能在 0.0-1.0 之间使用浮点数。
整数也可以使用,将滑块的最低值设置为 0,最高值设置为 30(代表 30 秒)。然而,改变总秒数时,也需要相应地更改滑块的设置。在大多数情况下,使用 0.0 到 1.0 之间的浮点比例是更通用和可重用的方法。
显示雷达以指示物体的相对位置
雷达显示其他物体相对于玩家的位置,通常基于圆形显示,其中中心代表玩家,每个图形“亮点”表示物体距离玩家有多远以及相对方向。复杂的雷达显示将以不同颜色或形状的“亮点”图标显示不同类别的物体。
在屏幕截图中,我们可以看到 2 个红色的正方形“亮点”,指示玩家附近的 2 个标记为Cube的红色立方体 GameObject 的相对位置,以及一个黄色的圆形“亮点”指示标记为Sphere的黄色球体 GameObject 的相对位置。绿色的圆形雷达背景图像给人一种飞机控制塔雷达或类似的感觉。

准备工作
对于这个食谱,我们在1362_01_11目录下的Images文件夹中准备了所需的图像。
如何操作...
要创建一个雷达以显示对象的相对位置,请按照以下步骤操作:
-
通过导入以下标准资产创建一个新的 3D 项目:
-
环境
-
角色
-
相机
-
-
通过导航到创建 | 3D 对象 | 地形菜单创建地形。
-
将地形设置为 20 x 20,放置在(-10, 0, -10)的位置——这样其中心就在(0, 0, 0),如图所示:
![如何操作...]()
-
使用SandAlbedo选项对地形进行纹理绘制,如图所示:
![如何操作...]()
-
从项目面板中的标准资产文件夹,将预制件ThirdPersonController拖动到场景中,并将其放置在(0, 1, 0)的位置。
-
标记名为Player的ThirdPersonController GameObject。
-
删除主相机 GameObject。
-
从项目面板中的标准资产文件夹,将预制件Multi-PurposeCameraRig拖动到场景中。
-
在层次面板中选择Multi-PurposeCameraRig,将ThirdPersonController GameObject 拖动到检查器选项卡中Auto Cam (Script)公共变量的目标属性中,如图所示:
![如何操作...]()
-
导入提供的名为
Images的文件夹。 -
在层次面板中,将名为RawImage-radar的UI | 原始图像 GameObject 添加到场景中。
-
确保在层次面板中选择了RawImage-radar GameObject。从你的项目
Images文件夹中,将radarBackground图像拖动到原始图像(脚本)公共属性纹理中。 -
现在,使用锚点预设项将Rect Transform中的RawImage-radar放置在左上角。然后设置宽度和高度为 200 像素。
-
创建另一个新的 UI 原始图像,命名为RawImage-blip。分配
yellowCircleBlackBorder纹理。标记Blip GameObject。 -
在项目面板中,创建一个名为blip-sphere的新空预制件,并将RawImage-blip GameObject 拖动到这个预制件中以存储所有其属性。
-
现在,将RawImage-blip的纹理更改为
redSquareBlackBorder。 -
在项目面板中,创建一个名为blip-cube的新空预制件,并将RawImage-blip GameObject 拖动到这个预制件中以存储所有其属性。
-
从层次面板中删除RawImage-blip GameObject。
-
创建一个名为
Radar的 C#脚本类,包含以下代码,并将其作为脚本组件添加到RawImage-radar GameObject 中:using UnityEngine; using System.Collections; using UnityEngine.UI; public class Radar : MonoBehaviour{ public float insideRadarDistance = 20; public float blipSizePercentage = 5; public GameObject rawImageBlipCube; public GameObject rawImageBlipSphere; private RawImage rawImageRadarBackground; private Transform playerTransform; private float radarWidth; private float radarHeight; private float blipHeight; private float blipWidth; void Start (){ playerTransform = GameObject.FindGameObjectWithTag("Player").transform; rawImageRadarBackground = GetComponent<RawImage>(); radarWidth = rawImageRadarBackground.rectTransform.rect.width; radarHeight = rawImageRadarBackground.rectTransform.rect.height; blipHeight = radarHeight * blipSizePercentage/100; blipWidth = radarWidth * blipSizePercentage/100; } void Update (){ RemoveAllBlips(); FindAndDisplayBlipsForTag("Cube", rawImageBlipCube); FindAndDisplayBlipsForTag("Sphere", rawImageBlipSphere); } private void FindAndDisplayBlipsForTag(string tag, GameObject prefabBlip){ Vector3 playerPos = playerTransform.position; GameObject[] targets = GameObject.FindGameObjectsWithTag(tag); foreach (GameObject target in targets) { Vector3 targetPos = target.transform.position; float distanceToTarget = Vector3.Distance(targetPos, playerPos); if( (distanceToTarget <= insideRadarDistance) ){ Vector3 normalisedTargetPosiiton = NormalisedPosition(playerPos, targetPos); Vector2 blipPosition = CalculateBlipPosition(normalisedTargetPosiiton); DrawBlip(blipPosition, prefabBlip); } } } private void RemoveAllBlips(){ GameObject[] blips = GameObject.FindGameObjectsWithTag("Blip"); foreach (GameObject blip in blips) Destroy(blip); } private Vector3 NormalisedPosition(Vector3 playerPos, Vector3 targetPos){ float normalisedyTargetX = (targetPos.x - playerPos.x)/insideRadarDistance; float normalisedyTargetZ = (targetPos.z - playerPos.z)/insideRadarDistance; return new Vector3(normalisedyTargetX, 0, normalisedyTargetZ); } private Vector2 CalculateBlipPosition(Vector3 targetPos){ // find angle from player to target float angleToTarget = Mathf.Atan2(targetPos.x, targetPos.z) * Mathf.Rad2Deg; // direction player facing float anglePlayer = playerTransform.eulerAngles.y; // subtract player angle, to get relative angle to object // subtract 90 // (so 0 degrees (same direction as player) is UP) float angleRadarDegrees = angleToTarget - anglePlayer - 90; // calculate (x,y) position given angle and distance float normalisedDistanceToTarget = targetPos.magnitude; float angleRadians = angleRadarDegrees * Mathf.Deg2Rad; float blipX = normalisedDistanceToTarget * Mathf.Cos(angleRadians); float blipY = normalisedDistanceToTarget * Mathf.Sin(angleRadians); // scale blip position according to radar size blipX *= radarWidth/2; blipY *= radarHeight/2; // offset blip position relative to radar center blipX += radarWidth/2; blipY += radarHeight/2; return new Vector2(blipX, blipY); } private void DrawBlip(Vector2 pos, GameObject blipPrefab){ GameObject blipGO = (GameObject)Instantiate(blipPrefab); blipGO.transform.SetParent(transform.parent); RectTransform rt = blipGO.GetComponent<RectTransform>(); rt.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, pos.x, blipWidth); rt.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, pos.y, blipHeight); } } -
创建两个标记为Cube的立方体,使用名为icon32_square_red的红色图像进行纹理处理。将每个立方体放置在玩家角色的远离位置。
-
创建一个标记为Sphere的球体,使用名为icon32_square_yellow的红色图像进行纹理处理。将这个球体放置在立方体和玩家角色的远离位置。
-
运行你的游戏。你将在雷达上看到两个红色方块和一个黄色圆圈,显示红色立方体和黄色球体的相对位置。如果你移动得太远,那么这些标记将会消失。
注意
这个雷达脚本扫描玩家周围 360 度的所有区域,并且只考虑 X-Z 平面的直线距离。因此,这个雷达中的距离不受玩家和目标 GameObject 之间任何高度差异的影响。该脚本可以被修改以忽略高度超过玩家高度阈值的目标。此外,正如所展示的,这个雷达配方可以看到一切,即使玩家和目标之间存在障碍。该配方可以通过使用射线投射技术来扩展,以不显示被遮挡的目标。有关射线投射的更多详细信息,请参阅 Unity 脚本参考
docs.unity3d.com/ScriptReference/Physics.Raycast.html。
它是如何工作的...
在屏幕上显示雷达背景。这个圆形图像的中心代表玩家角色的位置。你已经创建了两个预制体;一个用于代表雷达距离内找到的每个红色立方体的红色方形图像,另一个用于代表黄色球体的黄色圆形 GameObject。
Radar C# 脚本类已被添加到雷达 UI Image GameObject 中。这个类定义了四个公共变量:
-
insideRadarDistance:此值定义了一个对象可能离玩家的最大距离,仍然可以包含在雷达上(距离超过此距离的对象将不会在雷达上显示)。 -
blipSizePercentage:这个公共变量允许开发者决定每个'标记'的大小,作为雷达图像的比例。 -
rawImageBlipCube和rawImageBlipSphere:这些是对要用于在雷达上视觉表示立方体和球体相对距离和位置的预制 UI RawImages 的引用。
由于这个配方中的代码有很多操作,每个方法将在自己的部分中进行描述。
Start() 方法
Start() 方法缓存了玩家角色(标记为 Player)的 Transform 组件的引用。这允许脚本对象知道玩家角色在每个帧中的位置。接下来,缓存雷达图像的宽度和高度——因此,可以根据这个背景雷达图像的大小计算'标记'的相对位置。最后,使用 blipSizePercentage 公共变量计算每个标记的大小(宽度和高度)。
Update() 方法
Update() 方法调用 RemoveAllBlips() 方法,该方法移除任何可能当前显示的立方体和球体的旧 RawImage UI GameObjects。
接下来,调用FindAndDisplayBlipsForTag(…)方法两次。首先,为了在雷达上用rawImageBlipCube预制体表示的标签为Cube的对象,然后再次为标签为Sphere的对象,用rawImageBlipSphere预制体在雷达上表示。正如你所预期的那样,雷达的大部分工作将由FindAndDisplayBlipsForTag(…)方法来完成。
FindAndDisplayBlipsForTag(…)方法
此方法输入两个参数:要搜索的对象的字符串标签;以及要在雷达上显示的RawImage预制体的引用,用于任何此类标签对象在范围内。
首先,从缓存的玩家变换变量中检索玩家角色的当前位置。接下来,构建一个数组,引用场景中所有具有提供标签的 GameObject。这个 GameObject 数组被遍历,并对每个 GameObject 执行以下操作:
-
获取目标 GameObject 的位置
-
计算从目标位置到玩家位置的距离,如果这个距离在范围内(小于或等于
insideRadarDistance),那么现在需要三个步骤来使此对象的 blip 出现在雷达上:-
通过调用
NormalisedPosition(…)计算目标的标准化位置 -
然后通过调用
CalculateBlipPosition(…)从这个标准化位置计算出雷达上 blip 的位置 -
最后,通过调用
DrawBlip(…)并传递 blip 位置以及要创建的RawImage预制体的引用来显示RawImage blip
-
NormalisedPosition(…)方法
NormalisedPosition(…)方法输入玩家的角色位置和目标 GameObject 的位置。它的目标是输出目标相对于玩家的相对位置,返回一个包含X、Y和Z值的 Vector3 对象。请注意,由于雷达是 2D 的,我们忽略目标 GameObject 的Y值。因此,此方法返回的 Vector3 对象的Y值始终为 0。例如,如果目标与玩家位于完全相同的位置,返回的X、Y、Z Vector3 对象将是(0, 0, 0)。
由于我们知道目标 GameObject 距离玩家角色不超过insideRadarDistance,我们可以通过找到每个轴上从目标到玩家的距离,然后除以insideRadarDistance来计算X和Z轴的-1…0…+1 范围内的值。X值为-1 表示目标完全在玩家的左侧(距离等于insideRadarDistance),+1 表示它完全在右侧。0 值表示目标具有与玩家角色相同的X位置。同样,对于Z 轴的-1…0…+1 值(这个轴表示一个物体在我们前方或后方有多远,这将在我们的雷达中映射到垂直轴)。
最后,这个方法构建并返回一个新的 Vector3 对象,包含计算出的X和Z归一化值,以及Y值为零。
注意
归一化位置
一个归一化的值是指以某种方式简化过的值,因此上下文已经被抽象化。在这个菜谱中,我们感兴趣的是对象相对于玩家的位置。因此,我们的常规做法是获取目标在-1 到+1 范围内的每个轴的X和Z位置值。由于我们只考虑insideRadarDistance值内的 GameObject,我们可以直接将这些归一化的目标位置映射到 UI 中雷达图像的位置。
CalculateBlipPosition(…)方法
首先,我们计算angleToTarget:从(0, 0, 0)到我们的归一化目标位置的角度。
接下来,我们计算anglePlayer:玩家角色面向的角度。这个菜谱使用了旋转的偏航角度,这是围绕Y 轴的旋转——也就是说,角色控制器面向的方向。这可以在 GameObject 的 transform 组件的eulerAngles组件的Y分量中找到。你可以想象从上方向下看角色控制器,看看他们面向的方向——这正是我们试图用指南针图形显示的内容。
我们期望的雷达角度(angleRadarDegrees变量)是通过从目标与玩家之间的角度中减去玩家的方向角度来计算的,因为雷达显示的是玩家面向的方向与目标对象之间的相对角度。在数学上,零角度表示东方方向。为了纠正这一点,我们还需要从角度中减去 90 度。
角度随后被转换为弧度,因为这是 Unity 三角函数方法所必需的。然后我们将Sin()和Cos()的结果乘以我们的归一化距离,分别计算出X和Y值(见下图):

我们最终的定位值需要以像素长度表示,相对于雷达的中心。因此,我们将 blipX 和 blipY 值乘以雷达宽度的一半和高度的一半;注意我们只乘以宽度的一半,因为这些值是相对于雷达中心的。
注意
注意:在此图中,alpha 是玩家与目标对象之间的角度,'a' 是邻边,'h' 是斜边,'o' 是对边。
然后,我们将雷达图像宽度的一半和高度的一半添加到 blipX/Y 值中。因此,这些值现在是相对于中心的定位。
最后创建了一个新的 Vector2 对象并返回,传递回这些最终计算出的 X 和 Y 像素值,用于我们 blip 图标的定位。
DrawBlip() 方法
DrawBlip() 方法接受 blip 定位(作为一个 Vector2 X,Y 对)的输入参数,以及要在雷达上该位置创建的 RawImage 预制件的引用。
从预制件创建一个新的 GameObject,并将其作为父对象添加到雷达 GameObject(其中脚本对象也是一个组件)。检索到为 'blip' 创建的新 RawImage GameObject 的 Rect Transform 引用。调用 Unity RectTransform 方法 SetInsetAndSizeFromParentEdge(…),结果是将 blip GameObject 定位在雷达图像上提供的水平和垂直位置,无论背景雷达图像在 游戏 面板中的位置如何。
使用 Fungus 开源对话框系统创建 UI
而不是每次都从头开始构建自己的 UI 和交互,Unity 中有很多 UI 和对话框系统可供选择。一个强大、免费且开源的对话框系统叫做 Fungus,它使用可视流程图方法进行对话框设计。
在这个菜谱中,我们将创建一个非常简单的、两句话的对话,以说明 Fungus 的基本原理。以下截图显示了 Fungus 生成的第一句话('你好,你好吗')和用户点击以进入下一部分对话的交互按钮(一个圆圈内的三角形)(位于矩形的右下角)。

如何操作...
要使用 Fungus 创建两句话的对话,请按照以下步骤操作:
-
从 FungusGames 网站下载 Fungus 的最新版本 unitypackage
www.fungusgames.com/。 -
创建一个新的 Unity 2D 项目。
-
通过导航到 资产 | 导入包 | 自定义包... 导入 Fungus unitypackage,然后导航到您下载的文件位置。
-
通过选择菜单:工具 | Fungus | 创建 | 流程图来创建一个新的 Fungus 流程图GameObject。
-
通过选择菜单:工具 | Fungus | 流程图窗口来显示和停靠 Fungus 流程图窗口面板。
-
在流程图窗口中会有一个块。点击此块以选择它(块周围出现绿色边框以指示已选择),然后在检查器面板中,将此块的名字改为Start,如图所示:
![如何操作...]()
-
流程图中的每个块都遵循一系列命令。因此,我们现在将创建一系列命令,以便在游戏运行时向用户显示两句话。
注意
块中的命令序列
流程图中的每个块都遵循一系列命令,所以当游戏运行时向用户显示两句话,我们需要在检查器面板的块属性中创建两个说命令的序列。
-
确保在流程图面板中仍然选择了Start块。现在,点击检查器面板底部部分的加号+按钮以显示命令菜单,并选择叙事|说命令,如图所示:
![如何操作...]()
-
由于我们只为这个块有一个命令,因此该命令将自动在检查器视图的上半部分选中(高亮绿色)。检查器视图的下半部分显示当前选定的命令的属性,如图所示。在检查器视图的下半部分,对于故事文本属性,输入你想展示给用户的文本:
你今天怎么样?![如何操作...]()
-
现在,创建另一个说命令,并在其故事文本属性中输入以下内容:
非常好,谢谢。 -
当你运行游戏时,用户首先会看到你今天怎么样?的文本(随着每个字母在屏幕上输入时,会听到点击声)。在用户点击对话框窗口右下角的'继续'三角形按钮后,他们将会看到第二句话:非常好,谢谢。
它是如何工作的...
你已经创建了一个新的 Unity 项目,并导入了 Fungus 资产包,其中包含 Fungus Unity 菜单、窗口和命令,以及示例项目。
你已经将一个名为Start的单个Fungus 流程图添加到场景中。当游戏开始时,你的块开始执行(因为第一个块的默认行为是在接收到游戏开始事件时执行)。
在Start块中,你添加了一系列两个说命令。每个命令向用户展示一句话,然后等待用户点击继续按钮,然后再进行到下一个命令。
如所示,Fungus 系统处理创建一个用户界面良好的面板的工作,显示所需的文本和继续按钮。Fungus 提供许多其他功能,包括菜单、动画、声音和音乐的控件等,更多详细信息可以通过探索他们提供的示例项目和网站找到:
设置自定义鼠标光标图像
光标图标通常用于指示可以使用鼠标进行的交互性质。例如,缩放可能通过放大镜来表示。另一方面,射击通常由一个风格化的靶子表示。在本教程中,我们将学习如何实现自定义鼠标光标图标,以更好地说明您的游戏玩法——或者只是逃离 Windows、OSX 和 Linux 的默认 GUI。以下截图显示了当用户的鼠标指针悬停在按钮上时,自定义放大镜鼠标光标:

准备工作
对于这个教程,我们在1362_01_13文件夹中的IconsCursors文件夹中准备了您需要的图像。
如何操作...
要在鼠标悬停在 GameObject 上时显示自定义光标,请按照以下步骤操作:
-
创建一个新的 Unity 2D 项目。
-
通过导航到创建 | 灯光 | 方向光,向场景中添加一个方向光项目。
-
向场景中添加一个缩放为(5, 5, 5)的 3DCube。由于它是一个 2D 项目创建的,立方体将在游戏面板中显示为一个灰色正方形(2D 项目具有正交相机,因此我们不会看到透视效果)。
-
导入名为
IconsCursors的提供文件夹。小贴士
确保这个文件夹中的每个图像都已导入为纹理类型光标。如果不是,则为每个图像选择此类型,并在检查器视图中单击应用按钮。
-
创建一个名为
CustomCursorPointer的 C#脚本类,包含以下代码,并将其作为脚本组件添加到CubeGameObject 实例中:using UnityEngine; using System.Collections; public class CustomCursorPointer : MonoBehaviour { public Texture2D cursorTexture2D; private CursorMode cursorMode = CursorMode.Auto; private Vector2 hotSpot = Vector2.zero; public void OnMouseEnter() { SetCustomCursor(cursorTexture2D); } public void OnMouseExit() { SetCustomCursor(null); } private void SetCustomCursor(Texture2D curText){ Cursor.SetCursor(curText, hotSpot, cursorMode); } }注意
故事方法
OnMouseEnter()和OnMouseExit()已被故意声明为public。这将允许这些方法在它们接收到OnPointerEnterExit事件时也能从 UI GameObject 中被调用。 -
在层次面板中选择Cube项目,将
CursorTarget图像拖动到检查器面板中Customer Cursor Pointer (Script)组件的公共Cursor Texture 2D变量槽中。 -
保存当前场景,并将其添加到构建中。
小贴士
您在 Unity 编辑器中看不到自定义光标。您必须构建您的游戏应用程序,当您运行构建的应用程序时,您将看到自定义光标。
-
构建您的项目。现在,运行您构建的应用程序,当鼠标指针移动到Cube的灰色正方形上时,它将更改为您选择的自定义
CursorTarget图像。
它是如何工作的...
您已将一个脚本对象添加到一个立方体上,这将告诉 Unity 在接收到OnMouseEnter消息时更改鼠标指针——也就是说,当用户的鼠标指针移动到正在渲染立方体的屏幕部分时。当接收到OnMouseExit事件(用户的鼠标指针不再位于屏幕的立方体部分)时,系统会被告知返回到操作系统默认光标。此事件应在用户鼠标从碰撞器退出后的几毫秒内接收到。
还有更多...
有些细节您不容错过。
鼠标悬停在 UI 控件上的自定义光标
Unity 5 UI 控件不接收OnMouseEnter和OnMouseExit事件。它们可以响应PointerEnter/Exit事件,但这需要添加事件触发器组件。要更改鼠标指针在鼠标悬停在 UI 元素上时,请执行以下操作:
-
将一个 UI按钮添加到场景中。
-
将名为
CustomCursorPointer的 C#脚本类的一个实例添加到按钮中。 -
在层次结构面板中选择按钮,将
CursorZoom图像拖放到检查器面板中Customer Cursor Pointer (Script)组件的公共Cursor Texture 2D变量槽中。 -
在检查器视图中,将一个事件触发器组件添加到按钮上。选择菜单:添加组件 | 事件 | 事件触发器。
-
在您的事件触发器组件中添加一个指针进入事件,点击加号(+)按钮以添加事件处理器槽,并将按钮游戏对象拖放到对象槽中。
-
从函数下拉菜单中选择CustomCursorPointer,然后选择OnMouseEnter方法。
注意
我们已添加一个事件处理器,以便当按钮接收到指针进入(鼠标悬停)事件时,它将执行按钮内部CustomCursorPointer脚本对象的OnMouseEnter()方法。
-
在您的事件触发器组件中添加一个指针退出事件,并在接收到此事件时调用CustomCursorPointer中的
OnMouseExit()方法。 -
保存当前场景。
-
构建您的项目。现在,运行您构建的应用程序,当鼠标指针移动到按钮上时,它将更改为您选择的自定义
CursorZoom图像。
文本输入字段组件
虽然很多时候我们只想向用户显示非交互式文本消息,但有时(如输入高分时的名字)我们希望用户能够将文本或数字输入到我们的游戏中。Unity 提供了输入字段UI 组件来实现此目的。在本配方中,我们将通过使用默认按钮图像和文本游戏对象来创建一个简单的文本输入 UI,并将添加一个脚本以响应输入字段的每个新值。
注意
当然,您可以通过选择菜单:创建 | UI | 输入字段来更快地创建一个工作的文本输入,这将创建一个包含输入字段组件、子文本和占位符游戏对象的 GameObject,如图下截图所示。然而,通过遵循本食谱中的步骤,您将了解不同界面元素之间的相互关系,因为您将手动从 UI 按钮游戏对象的拆解部分创建这些连接。

如何操作...
要为用户创建一个带有淡色占位符文本的推广文本输入框,请按照以下步骤操作:
-
创建一个新的 Unity 2D 项目。
-
在检查器视图中,将主摄像机的背景更改为纯白色。
-
向场景中添加一个UI 按钮。删除按钮GameObject 的按钮(脚本)组件(因为它将不是一个按钮,在我们完成它的时候,它将是一个交互式文本输入!)。
-
将按钮组件的文本子 GameObject 重命名为文本占位符。取消选择富文本选项,将文本更改为输入名称…,在左和顶中更改对齐,并在矩形变换中设置左为
4和顶为7。![如何操作...]()
-
通过命名副本为文本提示来复制文本占位符。将此 GameObject 的文本更改为名称,并将其左位置设置为
-50。 -
再次复制文本占位符,将新副本命名为文本输入。删除此新 GameObject 的文本属性中的所有内容。
-
在层次结构中选择文本占位符,现在我们将占位符文本设置为大部分透明。将此 GameObject 的文本(脚本)组件的A(alpha)颜色值设置为最大值的约四分之一(例如,64)。
-
在层次结构中选择文本输入,并通过选择菜单:添加组件 | UI | 输入字段来添加一个输入字段组件。
-
将文本输入GameObject 拖动到输入字段的文本组件属性中,并将文本占位符GameObject 拖动到占位符属性中。
-
保存并运行您的场景。现在您有一个为用户工作的文本输入 UI。当没有文本内容时,将显示淡色占位符文本。一旦输入了任何字符,占位符将被隐藏,输入的字符将以黑色文本显示。然后,如果删除所有字符,占位符将再次出现。
它是如何工作的...
Unity 中交互式文本输入的核心责任在于 Input Field 组件。这需要一个对 UI Text GameObject 的引用。为了更容易地看到可以输入文本的位置,我们使用了 Unity 在创建 Button GameObject 时提供的默认圆角矩形图像。Buttons 具有图像组件和 Text 子 GameObject。因此,通过创建一个新的 Button 并简单地移除 Button (Script) 组件,我们可以非常容易地获得所需的两个项目。
通常涉及三个 Text GameObject 与用户文本输入相关:静态提示文本(例如,在我们的配方中,Name: 文本);然后是微弱的占位符文本,提醒用户在哪里以及应该输入什么;最后是实际显示给用户的文本对象(具有字体和颜色设置等),显示用户输入的字符。
在运行时,会创建一个 Text-Input Input Caret GameObject——显示闪烁的垂直线以告知用户下一个字母将被输入的位置。请注意,在 Inspector 中的 Input Field (Script) 的 Content Type 可以设置为几种特定的文本输入类型,包括电子邮件地址、仅整数或小数,或密码文本(每个输入字符都会显示一个星号)。
还有更多...
有一些细节你不希望错过。
执行 C# 方法以响应用户更改输入文本内容
除非我们能检索到用户输入的文本以用于游戏逻辑,否则屏幕上的交互式文本并没有太大的用处,我们可能还需要知道每次用户更改文本内容时的情况,并据此采取行动。
要添加代码和事件以响应用户每次更改文本内容的情况,请执行以下操作:
-
将名为
DisplayChangedTextContent的 C# 脚本类实例添加到 Text-input GameObject 中:using UnityEngine; using System.Collections; using UnityEngine.UI; public class DisplayChangedTextContent : MonoBehaviour { private InputField inputField; void Start(){ inputField = GetComponent<InputField>(); } public void PrintNewValue (){ string msg = "new content = '" + inputField.text + "'"; print (msg); } } -
将 End Edit (String) 事件添加到 Input Field (Script) 组件的事件处理程序列表中。点击加号 (+) 按钮以添加事件处理程序槽位,并将 Text-input GameObject 拖动到 Object 槽位中。
-
从 Function 下拉菜单中选择 DisplayChangedTextContent,然后选择 PrintNewValue 方法。
-
保存并运行场景。每次用户输入新文本然后按下 Tab 或 Enter 键时,End Edit 事件将被触发,你将看到我们的脚本在 Console 窗口中打印出新的内容文本消息,如下面的截图所示:
![执行 C# 方法以响应用户更改输入文本内容]()
通过 Toggle Groups 使用开关和单选按钮
用户做出选择,通常这些选择要么是两个可用选项之一(例如,声音开或关),有时则是从几个可能性中选择一个(例如,难度级别简单/中等/困难)。Unity UI Toggle允许用户开启或关闭选项;当与Toggle Groups结合使用时,它们将选择限制在项目组中的一个。在本食谱中,我们将首先探索基本的Toggle,以及一个响应值变化的脚本。然后在更多内容部分,我们将扩展示例以说明Toggle Groups,并使用圆形图像进行样式化,使其看起来更像传统的单选按钮。
以下截图显示了当场景运行时,按钮状态变化在控制台面板中的记录情况:

准备工作
对于这个食谱,我们在1362_01_15文件夹中的UI Demo Textures文件夹中准备了你需要使用的图像。
如何操作...
要向用户显示开/关 UI Toggle,请按照以下步骤操作:
-
创建一个新的 Unity 2D 项目。
-
在检查器面板中,将主摄像机的背景颜色更改为白色。
-
将UI Toggle添加到场景中。
-
将Toggle GameObject 的Label子 GameObject 的文本设置为
First Class。 -
将名为
ToggleChangeManager的 C#脚本类实例添加到Toggle GameObject 中:using UnityEngine; using System.Collections; using UnityEngine.UI; public class ToggleChangeManager : MonoBehaviour { private Toggle toggle; void Start () { toggle = GetComponent<Toggle>(); } public void PrintNewToggleValue(){ bool status = toggle.isOn; print ("toggle status = " + status); } } -
选择Toggle GameObject 后,将On Value Changed事件添加到Toggle (Script)组件的事件处理器列表中,点击加号(+)按钮添加事件处理器槽位,并将Toggle拖入对象槽位。
-
从功能下拉菜单中选择ToggleChangeManager,然后选择PrintNewToggleValue方法。
-
保存并运行场景。每次检查或取消检查Toggle GameObject 时,On Value Changed事件都会触发,你将看到我们的脚本在控制台窗口中打印出新的布尔值(true/false)的文本消息。
工作原理...
当你创建一个 Unity UI Toggle GameObject 时,它将自动包含几个子 GameObject——背景、勾选标记和文本标签。除非我们需要以特殊方式样式化Toggle的外观,否则只需要简单地编辑文本标签,以便用户知道这个Toggle将要开启/关闭哪个选项或功能。
被称为ToggleChangeManager的 C#脚本类的方法Start()获取位于脚本实例所在游戏对象的Toggle组件的引用。当游戏运行时,每次用户点击Toggle以更改其值时,都会触发一个On Value Changed事件。我们随后注册PrintNewToggleValue()方法,该方法将在此类事件发生时执行。此方法检索,然后打印到Console面板的Toggle的新布尔值(真/假)。
还有更多...
有些细节你不应该错过。
添加更多 Toggle 和一个 Toggle Group 以实现互斥单选按钮
Unity UI Toggle也是基础组件,如果我们希望以单选按钮的风格实现一组互斥选项,可以这样做:
-
将
UI Demo Textures文件夹导入到项目中。 -
从Toggle游戏对象中移除 C#脚本类
ToggleChangeManager组件。 -
将Toggle游戏对象重命名为Toggle-easy。
-
将Label文本更改为Easy,并给这个游戏对象添加一个名为Easy的新标签。
-
选择Toggle-easy的Background子游戏对象,在Image (Script)组件中,将
UIToggleBG图像拖动到Source Image属性。 -
确保Toggle (Script)组件的Is On属性被勾选,然后选择Toggle-easy的Checkmark子游戏对象。在Image (Script)组件中,将
UIToggleButton图像拖动到Source Image属性。注意
在我们提供给用户的三个选项(简单、中等和困难)中,我们将简单选项设置为默认选中项。因此,我们需要将其Is On属性勾选,这将导致其“勾选”图像显示。
为了使这些Toggle看起来更像单选按钮,每个的背景都设置为
UIToggleBG的圆形图像,而勾选标记(显示开启的Toggle)则填充了名为UIToggleButton的圆形图像。 -
复制Toggle-easy游戏对象,将副本命名为Toggle-medium。将其Rect Transform属性的Pos Y设置为
-25(这样副本就会位于简单选项下方),并取消勾选Toggle (Script)组件的Is On属性。给这个副本添加一个名为Medium的新标签。 -
复制Toggle-medium游戏对象,将副本命名为Toggle-hard。将其Rect Transform属性的Pos Y设置为
-50(这样副本就会位于中等选项下方)。给这个副本添加一个名为Hard的新标签。 -
将名为
RadioButtonManager的 C#脚本类实例添加到Canvas游戏对象:using UnityEngine; using System.Collections; using UnityEngine.UI; public class RadioButtonManager : MonoBehaviour { private string currentDifficulty = "Easy"; public void PrintNewGroupValue(Toggle sender){ // only take notice from Toggle just swtiched to On if(sender.isOn){ currentDifficulty = sender.tag; print ("option changed to = " + currentDifficulty); } } } -
在选择Toggle-easy GameObject 后,将On Value Changed事件添加到Toggle (Script)组件的事件处理器列表中。点击加号(+)按钮添加事件处理器槽,并将Canvas GameObject 拖入对象槽中。
-
从功能下拉菜单中选择RadioButtonManager,然后选择PrintNewGroupValue方法。在Toggle参数槽中,最初为
None (Toggle),拖入Toggle-easy GameObject。 -
对Toggle-medium和Toggle-hard GameObject 执行相同的操作——这样每个Toggle对象都会调用Canvas GameObject 中名为
RadioButtonManager的 C#脚本组件的PrintNewGroupValue(…)方法,并将自身作为参数传递。 -
保存并运行场景。每次您检查三个单选按钮中的任何一个时,On Value Changed事件都会触发,您会看到我们的脚本在控制台窗口中打印出一条新的文本消息,指出刚刚设置为 true(开启)的Toggle(单选按钮)的标签。
-
以下截图显示了当场景运行时,对应于所选单选按钮的值如何记录到控制台面板中:
![添加更多切换和切换组以实现互斥的单选按钮]()
结论
在本章中,我们介绍了各种 Unity 5 UI 组件的食谱,并说明了相同的组件可以以不同的方式使用(例如,使用交互式滑块来显示倒计时计时器的状态)。许多游戏中的一组 UI 组件是那些向用户传达他们所携带的内容(或尚未拾取的内容)的组件。我们在本书中专门用另一章介绍了第二章中的库存,库存 GUI,它提供了许多库存食谱和额外的 UI 控件,例如添加交互式滚动条。
这里有一些进一步阅读、教程和资源的建议,以帮助您继续在 Unity 中学习 UI 开发:
-
在
docs.unity3d.com/Manual/UISystem.html的手册页面上了解更多关于 Unity UI 的信息。 -
在
unity3d.com/learn/tutorials/topics/user-interface-ui上观看 Unity UI 教程视频。 -
Ray Wenderlich 关于 Unity UI 开发的优秀教程,请访问
www.raywenderlich.com/78675/unity-new-gui-part-1。 -
Unity 关于为多分辨率设计 UI 的文档页面:
docs.unity3d.com/Manual/HOWTO-UIMultiResolution.html。
游戏需要与游戏玩法和主题相匹配的字体。以下是一些适合许多游戏的免费个人/商业字体的来源:
-
FontSquirrel 上的所有字体均可 100%免费用于商业用途。它们可在
www.fontsquirrel.com/找到。 -
在 DaFont 网站上查看每个字体的单独许可。如果用于商业目的,许多人会要求捐赠。更多信息,请查看
www.dafont.com/xolonium.font。 -
在
naldzgraphics.net/textures/的 Naldz Graphics 博客上查看每个字体的单独许可。 -
1001 Free Fonts (for personal use) are available at
www.1001freefonts.com/index.php.
第二章. 库存 GUI
在本章中,我们将涵盖以下主题:
-
创建一个简单的 2D 小游戏 – SpaceGirl
-
显示带有携带和不携带文本的单个物品拾取
-
显示带有携带和不携带图标的单个物品拾取
-
以文本总数的形式显示相同对象的多个拾取
-
以多个状态图标显示相同对象的多个拾取
-
通过改变瓦片图像的大小来揭示多个对象拾取的图标
-
通过动态的 List<> 的 PickUp 对象以文本列表的形式显示不同对象的多个拾取
-
通过动态的 PickUp 对象的 Dictionary<> 和 "enum" 拾取类型,以文本总数的形式显示不同对象的多个拾取
-
使用 UI 网格布局组(带滚动条!)泛化多个图标显示
简介
许多游戏涉及玩家收集物品或从物品选择中做出选择。例如,收集钥匙打开门,收集武器弹药,从一系列法术中选择施法,等等。
本章中的食谱提供了一系列解决方案,用于向玩家显示他们是否携带了物品,是否允许携带多个物品,以及他们有多少个。
整体情况
实现库存的软件设计的两部分与以下两个方面相关,首先是我们如何选择表示库存物品数据的方式(即存储数据的类型和结构),其次是我们如何选择向玩家显示库存物品信息的方式(UI:用户界面)。
此外,虽然不是严格的库存物品,但玩家的属性,如剩余生命值、健康值或剩余时间,也可以围绕本章中提出的相同概念进行设计。
我们需要首先考虑任何特定游戏中不同库存物品的性质:
-
单个物品:
-
示例:一个级别的唯一钥匙,我们的魔法盔甲套装
-
数据类型:bool(真/假)
-
UI:如果没有携带,则无内容或显示携带的文本/图像
-
或者可能是“没有钥匙”/“钥匙”的文本,或者两个图像,一个显示空钥匙轮廓,另一个显示全色钥匙
-
如果我们希望向玩家突出显示有携带此物品的选项
-
-
-
持续物品:
-
示例:剩余时间,健康值,护盾强度
-
数据类型:float(例如,0.00–1.00)或整数比例(例如,0% .. 100%)
-
UI:文本数字或图像进度条/饼图
-
-
相同物品的两个或更多个
-
示例:剩余生命值,或剩余箭矢或子弹的数量
-
数据类型:int(整数)
-
UI:文本计数或图像
-
-
相关物品的集合
-
示例:不同颜色的钥匙打开相应颜色的门,不同强度的药水具有不同的名称
-
数据结构:用于通用物品类型的结构体或类(例如,
Key类(颜色/成本/门打开标签字符串),存储为数组或 List<> 类型 -
UI:文本列表或图标列表/网格排列
-
-
不同物品的集合
-
示例:键、药水、武器、工具—all 在同一个库存系统中
-
数据结构:List<>或 Dictionary<>或对象数组,每个项目类型可以是不同类的实例
-
上面的每种表示和 UI 显示方法都由本章中的食谱进行说明。
创建一个简单的 2D 迷你游戏 – SpaceGirl
本食谱展示了创建 2DSpaceGirl 迷你游戏的步骤,本章的所有食谱都是基于此。
准备工作
对于这个食谱,我们在1362_02_01文件夹中名为Sprites的文件夹中准备了您需要的图像。我们还在这个文件夹中提供了一个名为Simple2DGame_SpaceGirl的 Unity 包,其中包含了完成的游戏。
如何操作...
要创建简单的 2D 迷你游戏Space Girl,请按照以下步骤操作:
-
创建一个新的空 2D 项目。
-
将提供的文件夹
Sprites导入到您的项目中。 -
将每个精灵图像转换为精灵(2D 和 UI)类型。为此,在项目面板中选择精灵,然后在检查器中,从下拉菜单纹理类型中选择精灵(2D 和 UI),并点击应用按钮,如图所示:
![如何操作...]()
-
将 Unity Player 屏幕大小设置为 800 x 600:选择编辑 | 项目设置 | 玩家菜单,然后对于选项分辨率和显示取消勾选
默认为全屏,并将宽度设置为800,高度设置为600,如图所示:![如何操作...]()
-
选择游戏面板;如果尚未选择,则从下拉菜单中选择独立(800 x 600),如图所示:
![如何操作...]()
-
显示当前 Unity 项目的标签和层属性。选择菜单编辑 | 项目设置 | 标签和层。或者,如果您已经在编辑一个 GameObject,那么您可以从检查器面板顶部的层下拉菜单中选择添加层…菜单,如图所示:
![如何操作...]()
-
现在检查器应该正在显示当前 Unity 项目的标签和层属性。使用展开/收缩三角形工具收缩标签和层,并展开排序层。
-
使用加号+按钮添加两个新的排序层,如图所示:首先,添加一个名为背景的层,然后添加一个名为前景的层。顺序很重要,因为 Unity 将在列表中较后的层上绘制项目,覆盖列表中较早的项目。
![如何操作...]()
-
将精灵
background-blue从项目面板(文件夹Sprites)拖动到游戏或层次结构面板中,以创建当前场景的 GameObject。 -
将 GameObject
background-blue的排序层设置为背景(在精灵渲染器组件中)。 -
从项目面板(文件夹精灵)中将精灵
star拖动到游戏或层次面板中,为当前场景创建一个 GameObject。 -
在检查器面板中,通过选择检查器面板顶部的标签下拉菜单中的添加标签…选项,添加一个新的标签Star,如图所示:
![如何操作…]()
-
将Star标签应用到层次场景中的 GameObject
star。 -
将 GameObject
star的排序层设置为前景(在精灵渲染器组件中)。 -
向 GameObject
star添加一个 Box Collider 2D(添加组件 | 物理 2D | 盒子碰撞器 2D)并勾选其Is Trigger,如图所示:![如何操作…]()
-
从项目面板(文件夹精灵)中将精灵
girl1拖动到游戏或层次面板中,为当前场景中的玩家角色创建一个 GameObject。将此 GameObject 重命名为player-SpaceGirl。 -
将 GameObject
player-SpaceGirl的排序层设置为前景(在精灵渲染器组件中)。 -
向 GameObject
player-SpaceGirl添加一个 Box Collider 2D(添加组件 | 物理 2D | 盒子碰撞器 2D)。 -
向 GameObject
player-SpaceGirl添加一个 RigidBody 2D(添加组件 | 物理 2D | 刚体 2D)。将其重力缩放设置为零(这样它就不会因为模拟重力而掉落屏幕),如图所示:![如何操作…]()
-
为你的脚本创建一个新的文件夹,命名为
Scripts。 -
创建以下 C#脚本
PlayerMove(在文件夹Scripts中),并将其作为一个组件添加到层次中的 GameObjectplayer-SpaceGirl:using UnityEngine; using System.Collections; public class PlayerMove : MonoBehaviour { public float speed = 10; private Rigidbody2D rigidBody2D; void Awake(){ rigidBody2D = GetComponent<Rigidbody2D>(); } void FixedUpdate(){ float xMove = Input.GetAxis("Horizontal"); float yMove = Input.GetAxis("Vertical"); float xSpeed = xMove * speed; float ySpeed = yMove * speed; Vector2 newVelocity = new Vector2(xSpeed, ySpeed); rigidBody2D.velocity = newVelocity; } } -
保存场景(命名为主场景并将其保存到一个名为
Scenes的新文件夹中)。
工作原理...
你已经在场景中创建了一个玩家角色,并为其运动脚本组件PlayerMove进行了脚本编写。你还创建了一个星星 GameObject(一个拾取物),标记为Star,并具有一个 2D 盒子碰撞器,当玩家角色碰到它时将触发碰撞。当你运行游戏时,player-SpaceGirl角色应该使用W A S D,箭头键或摇杆移动。目前,如果player-SpaceGirl角色碰到星星,由于尚未编写脚本,所以不会发生任何操作。
你已经为场景添加了一个背景(GameObject background-blue),由于它位于最远的排序层背景,所以它将位于所有内容的后面。你希望出现在这个背景之前(玩家角色和到目前为止的星星)的项目放置在前景排序层。了解更多关于 Unity 标签和层的信息,请访问docs.unity3d.com/Manual/class-TagManager.html。
显示带有携带和不携带文本的单个对象拾取
通常,最简单的库存情况是显示文本来告诉玩家他们是否携带一个物品(或不是)。
准备工作
此配方假设您是从本章的第一个配方中设置的Simple2Dgame_SpaceGirl项目开始的。因此,要么复制该项目,要么执行以下操作:
-
创建一个新的空 2D 项目。
-
导入
Simple2Dgame_SpaceGirl包。 -
打开场景Scene1(在
Scenes文件夹中)。 -
将 Unity 玩家屏幕大小设置为 800 x 600(参见之前的配方了解如何操作)并在游戏面板的下拉菜单中选择此分辨率。
-
将每个精灵图像转换为Sprite(2D 和 UI)类型。在检查器中,从下拉菜单纹理类型中选择Sprite(2D 和 UI),并点击应用按钮。
对于这个配方,我们在1362_02_02文件夹中的Fonts文件夹中准备了您需要的字体。
如何操作...
要显示文本以通知用户携带单个拾取物品的状态,请按照以下步骤操作:
-
从 mini 游戏
Simple2Dgame_SpaceGirl的新副本开始。 -
添加一个 UI 文本对象(创建 | UI | 文本)。重命名为
Text-carrying-star。将其文本更改为Carrying star: false。 -
将提供的
Fonts文件夹导入到您的项目中。 -
在检查器面板中,将
Text-carrying-star的字体设置为Xolonium-Bold(文件夹Fonts),并将其颜色设置为黄色。水平垂直居中文本,并将高度设置为50,将字体大小设置为32,如图下所示:![如何操作...]()
-
在其矩形变换组件中,将其高度设置为
50,如图下所示:![如何操作...]()
-
编辑其矩形变换,在按住SHIFT和ALT(以设置枢轴和位置)的同时,选择顶部扩展框,如图下所示:
![如何操作...]()
-
您的文本现在应位于游戏面板的中间顶部,其宽度应扩展以匹配整个面板,如图下所示:
![如何操作...]()
-
将以下 C#脚本
Player添加到层次结构中的 GameObjectplayer-SpaceGirl:using UnityEngine; using System.Collections; using UnityEngine.UI; public class Player : MonoBehaviour { public Text starText; private bool carryingStar = false; void Start(){ UpdateStarText(); } void OnTriggerEnter2D(Collider2D hit){ if(hit.CompareTag("Star")){ carryingStar = true; UpdateStarText(); Destroy(hit.gameObject); } } private void UpdateStarText(){ string starMessage = "no star :-("; if(carryingStar) starMessage = "Carrying star :-)"; starText.text = starMessage; } } -
从层次结构视图中选择 GameObject
player-SpaceGirl。然后,从检查器中访问玩家(脚本)组件,并将星文公共字段填充为 UI 文本对象Text-carrying-star,如图下所示:![如何操作...]()
-
当您播放场景时,在将角色移动到星星后,星星应该消失,屏幕上的 UI 文本消息应更改为Carrying star 😃,如图下所示:
![如何操作...]()
它是如何工作的...
Text变量starText是对 UI 文本对象Text-carrying-star的引用。bool变量carryingStar表示玩家在任何时候是否携带星星;它被初始化为 false。
UpdateStarText()方法将starMessage字符串的内容复制到starText的文本属性中。此字符串的默认值告诉用户玩家没有携带星星,但一个if语句测试carryingKey的值,如果为真,则消息会更改以告知玩家他们正在携带星星。
每当玩家的角色与任何将其Is Trigger设置为true的对象发生碰撞时,都会向参与碰撞的两个对象发送一个OnTriggerEnter2D()事件消息。OnTriggerEnter2D()消息传递一个参数,即刚刚碰撞的对象内部的Collider2D组件。
我们的玩家的OnTriggerEnter2D()方法测试与对象碰撞的tag字符串,以查看它是否有值Star。由于我们创建的 GameObject star具有触发器设置,并且具有标签Star,因此此方法内部的if语句将检测与star的碰撞并完成三个动作:它将布尔变量carryingStar设置为true,它调用UpdateStarText()方法,并且它销毁它刚刚碰撞的 GameObject(在这种情况下,star)。
注意
NOTE: 布尔变量通常被称为标志。
使用布尔(true/false)变量来表示游戏状态的一些功能是真是假是非常常见的。程序员通常将这些变量称为标志。因此,程序员可能会将carryingStar变量称为携带星标志。
当场景开始时,通过Start()方法,我们调用UpdateStarText()方法;这确保我们不是依赖于设计时输入到 UI Text对象Text-carrying-star中的文本,而是用户看到的 UI 总是由我们的运行时方法设置的。这避免了问题,即要显示给用户的单词在代码中更改,而不是在Inspector面板中更改——这导致场景首次运行时和从脚本更新后屏幕文本不匹配。
注意
在 Unity 游戏设计中,一个黄金法则是在多个地方避免重复内容,因此我们避免需要维护两个或更多相同内容的副本。每个重复的内容都是当某些但不是所有副本的值发生变化时,维护问题的一个机会。
最大化使用预制件是这一原则在行动中的另一个例子。这也被称为 DRY 原则 - 不要重复自己。
还有更多...
一些你不希望错过的细节:
视图逻辑的分离
一种称为模型-视图-控制器模式(MVC)的游戏设计模式(最佳实践方法)旨在将更新 UI 的代码与更改玩家和游戏变量(如得分和库存物品列表)的代码分离。尽管这个方法只有一个变量和一个更新 UI 的方法,但结构良好的游戏架构可以扩展以应对更复杂的游戏,因此在这个游戏开始阶段,如果我们要确保最终的游戏架构结构良好且易于维护,那么多写一些代码和额外的脚本类通常是值得的。
为了实现这个方法的视图模式分离,我们需要做以下几步:
-
将以下 C#脚本
PlayerInventoryDisplay添加到层次结构中的 GameObjectplayer-SpaceGirl:using UnityEngine; using System.Collections; using UnityEngine.UI; public class PlayerInventoryDisplay : MonoBehaviour { public Text starText; public void OnChangeCarryingStar(bool carryingStar){ string starMessage = "no star :-("; if(carryingStar) starMessage = "Carrying star :-)"; starText.text = starMessage; } } -
从层次结构视图中选择 GameObject
player-SpaceGirl。然后,从检查器中访问PlayerInventoryDisplay(脚本)组件,并将得分文本公共字段填充为 UI Text对象Text-carrying-star。 -
删除现有的 C#脚本组件
Player,并用包含以下(简化)代码的 C#脚本PlayerInventory替换它:using UnityEngine; using System.Collections; public class PlayerInventory : MonoBehaviour { private PlayerInventoryDisplay playerInventoryDisplay; private bool carryingStar = false; void Start(){ playerInventoryDisplay = GetComponent<PlayerInventoryDisplay>(); playerInventoryDisplay.OnChangeCarryingStar(carryingStar); } void OnTriggerEnter2D(Collider2D hit){ if(hit.CompareTag("Star")){ carryingStar = true; playerInventoryDisplay.OnChangeCarryingStar(carryingStar); Destroy(hit.gameObject); } } }
如所示,脚本类PlayerInventory不再需要维护与 UI Text的链接或担心更改该 UI 组件的文本属性——所有这些工作现在都是脚本PlayerInventoryDisplay的责任。当玩家实例组件检测到与星星的碰撞后,在将carryingStar布尔标志的值更改为true后,它只需调用PlayerInventoryDisplay组件的OnChangeCarryingStar()方法。
结果是,脚本类PlayerInventory的代码专注于玩家的碰撞和状态变量,而脚本类PlayerInventoryDisplay的代码处理与用户的通信。这种设计模式的另一个优点是,通过 UI 将信息传达给用户的方法可以更改(例如,从文本到图标),而无需对脚本类Player中的代码进行任何更改。
注意
注意:玩家的体验没有差异,所有更改都是为了改善我们游戏代码的架构结构。
显示携带和不携带图标的单个对象拾取
图形图标是通知玩家他们携带物品的有效方式。在这个方法中,如果没有携带星星,则显示一个灰色填充的图标在封闭的圆圈中;然后,在捡起星星后,显示一个黄色填充的图标,如以下截图所示。

在许多情况下,图标比文本消息更清晰(它们不需要阅读和思考),并且在表示玩家状态和库存物品时,屏幕上的图标也可以更小。
准备中
此配方假设您是从本章第一道菜谱中设置的Simple2Dgame_SpaceGirl项目开始的。
如何操作...
要切换单个对象拾取的携带和不携带图标,请按照以下步骤操作:
-
从
Simple2Dgame_SpaceGirl迷你游戏的新副本开始。 -
在层次面板中,添加一个新的 UI图像对象(创建 | UI | 图像)。将其重命名为
Image-star-icon。 -
在层次面板中选择
Image-star-icon。 -
从项目面板中,将精灵icon_nostar_100(文件夹
Sprites)拖动到检查器(在图像(脚本)组件)中的源图像字段。 -
点击设置原生大小按钮为图像组件。这将调整 UI图像的大小以适应精灵文件icon_nostar_100的物理像素宽度和高度,如图下所示:
![如何操作...]()
-
现在,我们将图标放置在游戏面板的顶部和左侧。编辑 UI图像的矩形变换组件,在按住SHIFT和ALT(以设置枢轴和位置)的同时,选择左上角的框。UI图像现在应位于游戏面板的左上角,如图下所示:
![如何操作...]()
-
将以下 C#脚本
Player添加到层次中的 GameObjectplayer-SpaceGirl:using UnityEngine; using System.Collections; using UnityEngine.UI; public class Player : MonoBehaviour { public Image starImage; public Sprite iconStar; public Sprite iconNoStar; private bool carryingStar = false; void OnTriggerEnter2D(Collider2D hit){ if(hit.CompareTag("Star")){ carryingStar = true; UpdateStarImage(); Destroy(hit.gameObject); } } private void UpdateStarImage(){ if(carryingStar) starImage.sprite = iconStar; else starImage.sprite = iconNoStar; } } -
从层次视图中选择 GameObject
player-SpaceGirl。然后,从检查器中访问Player(脚本)组件,并将星图像公共字段填充为 UI图像对象Image-star-icon。 -
现在,从项目面板中用精灵
icon_star_100填充图标星公共字段,并从项目面板中用精灵icon_nostar_100填充图标无星公共字段,如图下所示:![如何操作...]()
-
现在当你播放场景时,你应该看到无星图标(一个在封闭圆圈中的灰色填充图标)在左上角,直到你捡起星星,此时它将变为携带星星图标(黄色填充的星星)。
它是如何工作的...
Image变量starImage是对 UI图像对象Image-star-icon的引用。Sprite变量iconStar和iconNoStar是对项目面板中Sprite文件的引用——这些精灵用来告诉玩家是否正在携带星星。bool变量carryingStar表示程序数据中玩家在任何时间点是否携带星星;它被初始化为false。
此配方的许多逻辑与上一个配方相同。每次调用UpdateStarImage()方法时,它都会将 UI图像设置为与 bool 变量carryingsStar的值相对应的精灵。
显示相同对象的多个拾取与文本总数
当收集到多个同类型的物品时,通常向用户传达所携带物品的最简单方法是通过显示一个文本消息,显示每种物品类型的数量总和,如下面的截图所示。在这个菜谱中,使用 UI 文本对象显示了收集到的星星总数。

准备工作
此菜谱假设您是从本章的第一个菜谱中设置的Simple2Dgame_SpaceGirl项目开始的。所需的字体可以在文件夹1362_02_02中找到。
如何操作...
要显示同一类型对象的多个拾取的库存总数文本,请按照以下步骤操作:
-
从迷你游戏
Simple2Dgame_SpaceGirl的新副本开始。 -
添加一个 UI 文本对象(创建 | UI | 文本)。重命名为
Text-carrying-star。将其文本更改为stars = 0。 -
将提供的
Fonts文件夹导入到您的项目中。 -
在检查器面板中,将
Text-carrying-star的字体设置为Xolonium-Bold(文件夹Fonts),并将其颜色设置为黄色。水平垂直居中文本,并将字体大小设置为32。 -
在其矩形变换组件中,将高度设置为
50。编辑其矩形变换,在按住SHIFT和ALT(以设置枢轴和位置)的同时,选择顶部拉伸框。现在,您的文本应位于游戏面板的中间顶部,其宽度应扩展以匹配整个面板的宽度。 -
将以下 C#脚本
Player添加到层次结构中的 GameObjectplayer-SpaceGirl:using UnityEngine; using System.Collections; using UnityEngine.UI; public class Player : MonoBehaviour { public Text starText; private int totalStars = 0; void Start(){ UpdateStarText(); } void OnTriggerEnter2D(Collider2D hit){ if(hit.CompareTag("Star")){ totalStars++; UpdateStarText(); Destroy(hit.gameObject); } } private void UpdateStarText(){ string starMessage = "stars = " + totalStars; starText.text = starMessage; } } -
从层次结构视图中选择 GameObject
player-SpaceGirl。然后,从检查器中访问Player (Script)组件,并将 UI 文本对象Text-carrying-star填充到星文公共字段中。 -
在层次结构面板中选择 GameObject
star,并复制此 GameObject 三次。注意
注意:使用键盘快捷键CTRL + D(Windows)或CMD + D(Mac)快速复制 GameObject。
-
将这些新 GameObject 移动到屏幕的不同部分。
-
开始游戏——每次捡起星星时,总数应以stars = 2的形式显示。
工作原理...
Text变量starText是对 UI 文本对象Text-carrying-star的引用。int变量totalStars表示到目前为止收集了多少星星;它初始化为零。
在OnTriggerEnter2D()方法中,每当玩家的角色击中一个标记为Star的对象时,totalStars计数器增加 1。碰撞的星星 GameObject 被销毁,并调用UpdateStarText()方法。
UpdateStarText()方法通过将文本字符串stars =与变量totalStars内部的整数值连接起来,更新 UI 文本对象Text-carrying-star的文本内容,向用户显示更新的星星总数。
显示具有多个状态图标的多重拾取同一物体
如果要收集的物品数量是一个小的、固定的总数而不是文本总数,那么一个有效的替代 UI 方法是显示占位符图标(空或灰色图片)来显示用户还有多少个物品需要收集,并且每次拾取一个物品时,占位符图标将被一个全色的收集图标所替换。
在本食谱中,我们使用灰色填充的五角星图标作为占位符,并使用黄色填充的五角星图标来指示每个收集到的星星,如下截图所示。
由于我们的 UI 代码变得越来越复杂,本食谱将实现 MVC 设计模式来分离视图代码和核心玩家逻辑(如 Displaying single object pickups with carrying and not-carrying text 食谱末尾所述)。

准备工作
本食谱假设您是从本章第一道食谱中设置的 Simple2Dgame_SpaceGirl 项目开始的。
如何操作...
要显示同一类型物体的多重拾取的多个库存图标,请按照以下步骤操作:
-
从一个新的
Simple2Dgame_SpaceGirl小游戏副本开始。 -
将以下 C# 脚本
Player添加到 层次结构 中的player-SpaceGirlGameObject:using UnityEngine; using System.Collections; using UnityEngine.UI; public class Player : MonoBehaviour { private PlayerInventoryDisplay playerInventoryDisplay; private int totalStars = 0; void Start(){ playerInventoryDisplay = GetComponent<PlayerInventoryDisplay>(); } void OnTriggerEnter2D(Collider2D hit){ if(hit.CompareTag("Star")){ totalStars++; playerInventoryDisplay.OnChangeStarTotal(totalStars); Destroy(hit.gameObject); } } } -
在 层次结构 面板中选择 GameObject
star,并复制三次此 GameObject(Windows CTRL + D / Mac CMD + D)。 -
将这些新的 GameObject 移动到屏幕的不同部分。
-
将以下 C# 脚本
PlayerInventoryDisplay添加到player-SpaceGirlGameObject 中,在 层次结构 中:using UnityEngine; using System.Collections; using UnityEngine.UI; public class PlayerInventoryDisplay : MonoBehaviour { public Image[] starPlaceholders; public Sprite iconStarYellow; public Sprite iconStarGrey; public void OnChangeStarTotal(int starTotal){ for (int i = 0;i < starPlaceholders.Length; ++i){ if (i < starTotal) starPlaceholders[i].sprite = iconStarYellow; else starPlaceholders[i].sprite = iconStarGrey; } } } -
在 层次结构 面板中选择 Canvas,并添加一个新的 UI Image 对象(创建 | UI | Image)。将其重命名为
Image-star0。 -
在 层次结构 面板中选择
Image-star0。 -
从 项目 面板中,将精灵
icon_star_grey_100(文件夹Sprites)拖动到 检查器 中 Image 组件的 源图像 字段。 -
点击 设置原生大小 按钮以调整 Image 组件的大小。这将使 UI Image 的大小与精灵文件
icon_star_grey_100的物理像素宽度和高度相匹配。 -
现在,我们将图标放置在 游戏 面板的 顶部 和 左侧。编辑 UI Image 的 Rect Transform 组件,在按住 SHIFT 和 ALT(以设置枢轴和位置)的同时,选择左上角的框。UI Image 应该现在位于 游戏 面板的左上角。
-
在 层次结构 面板中复制三次
Image-star0,分别命名为Image-star1、Image-star2和Image-star3。 -
在 检查器 面板中,将
Image-star1的 Pos X 位置(在 Rect Transform 组件中)更改为100,Image-star2更改为200,Image-star3更改为100,如下截图所示:![如何操作...]()
-
从层次视图中,选择 GameObject
player-SpaceGirl。然后,从检查器中,访问玩家库存显示(脚本)组件,并将公共字段星形占位符的大小属性设置为4。 -
接下来,用 UI Image对象
Image-star0/1/2/3填充公共字段星形占位符的元素 0/1/2/3数组值。 -
现在,从项目面板中用精灵
icon_star_100和icon_star_grey_100填充图标星黄色和图标星灰色公共字段,如图下截图所示:![如何做...]()
-
现在,当你播放场景时,你应该最初看到四个灰色的占位符星形图标,每次你撞击到一个星形时,顶部的下一个图标应该变为黄色。
它是如何工作的...
在屏幕顶部创建了四个 UI Image对象Image-star0/1/2/3,初始化为灰色占位符图标。灰色和黄色图标精灵文件已调整大小为 100 x 100 像素,这使得在设计时更容易进行水平定位,因为它们的坐标是(0,0),(100, 0),(200, 0),和(300,0)。在一个更复杂的游戏屏幕,或者一个空间宝贵的屏幕中,实际的图标大小可能会更小,具体取决于游戏图形设计师的决定。
int变量totalStars代表到目前为止收集了多少颗星星;它被初始化为零。PlayerInventoryDisplay变量playerInventory是管理我们的库存显示的脚本组件的引用——此变量在场景开始运行时的Start()方法中被设置。
在OnTriggerEnter2D()方法中,每当玩家的角色撞击带有标签Star的对象时,totalStars计数器增加 1。除了销毁被击中的 GameObject 外,PlayerInventoryDisplay组件的OnChangeStarTotal(…)方法也被调用,传递新的星星总数整数。
脚本类PlayerInventoryDisplay的OnChangeStarTotal(…)方法引用了四个 UI Images,并遍历图像引用数组中的每个项目,将指定的图像设置为黄色,其余设置为灰色。此方法是公开的,允许从脚本类Player的实例中调用它。
如所见,脚本类Player中的代码仍然相当简单,因为我们已经将所有库存 UI 逻辑移动到了它自己的类,PlayerInventory。
通过改变瓦片图像的大小来揭示多个对象拾取的图标
另一种可以用来显示增加的图像数量的方法是使用瓦片图像。通过使用宽度为 400 的瓦片灰色星形图像(显示四个灰色星形图标),在瓦片黄色星形图像后面,其宽度是收集的星星数量的 100 倍,也可以实现与上一个配方中相同的视觉效果。我们将调整前面的配方来展示这种技术。
准备工作
此配方基于本章前面的配方。
如何操作...
要使用平铺图像显示多个对象拾取的灰色和黄色星星图标,请按照以下步骤操作:
-
为前面的配方复制你的工作。
-
在层次结构面板中,从画布中移除四个 Image-star0/1/2/3 UI 图像。
-
在层次结构面板中选择画布,并添加一个新的 UI 图像对象(创建 | UI | 图像)。将其重命名为
Image-stars-grey。 -
在层次结构面板中选择
Image-stars-grey。 -
从项目面板中,将精灵
icon_star_grey_100(文件夹Sprites)拖动到检查器(在图像(脚本)组件中)的源图像字段。 -
点击设置原始大小按钮以调整图像组件的大小。这将使 UI 图像的大小与精灵文件star_empty_icon的物理像素宽度和高度相匹配。
-
现在,我们将图标放置在游戏面板的顶部和左侧。编辑 UI 图像的矩形变换组件,在按住SHIFT和ALT(以设置枢轴和位置)的同时,选择左上角框。现在 UI 图像应位于游戏面板的左上角。
-
在检查器面板中,将
Image-stars-grey的宽度(在矩形变换组件中)更改为 400。同时,将图像类型(在图像(脚本)组件中)设置为平铺,如图所示:![如何操作...]()
-
在层次结构面板中复制
Image-stars-grey,并将其副本命名为Image-stars-yellow。 -
在层次结构面板中选择
Image-stars-yellow,从项目面板中,将精灵icon_star_100(文件夹Sprites)拖动到检查器(在图像(脚本)组件中)的源图像字段。 -
将
Image-stars-yellow的宽度设置为 0(在矩形变换组件中)。因此,现在我们有了位于灰色平铺图像之上的黄色星星平铺图像,但由于其宽度为零,我们目前看不到任何黄色星星。 -
用以下代码替换现有的 C#脚本
PlayerInventoryDisplay:using UnityEngine; using System.Collections; using UnityEngine.UI; public class PlayerInventoryDisplay : MonoBehaviour { public Image iconStarsYellow; public void OnChangeStarTotal(int starTotal){ float newWidth = 100 * starTotal; iconStarsYellow.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, newWidth); } } -
从层次结构视图中选择 GameObject
player-SpaceGirl。然后,从检查器中访问玩家库存显示(脚本)组件,并将公共字段图标星星黄色填充为 UI 图像对象Image-stars-yellow。
它是如何工作的...
UI 图像 Image-stars-grey是一个平铺图像,宽度足够(400px),可以显示灰色精灵icon_star_grey_100四次。UI 图像 Image-stars-yellow是一个平铺图像,位于灰色图像之上,初始宽度设置为零,因此看不到任何黄色星星。
每次拾取一颗星星时,都会调用脚本类PlayerInventoryDisplay的OnChangeStarTotal(…)方法,传递收集到的新的整数星星数量。通过将这个数乘以黄色精灵图像的宽度(100px),我们得到设置 UI Image Image-stars-yellow的正确宽度,以便用户现在可以看到相应数量的黄色星星。任何尚未收集的星星仍然以灰色星星的形式显示,这些星星尚未被覆盖。
实际上,通过调用SetSizeWithCurrentAnchors(…)方法来更改 UI Image Image-stars-yellow的宽度。第一个参数是轴,因此我们传递常量RectTransform.Axis.Horizontal,以便它将更改宽度。第二个参数是那个轴的新大小——因此我们传递一个值,它是迄今为止收集的星星数量的 100 倍(变量newWidth)。
通过动态List<>的 PickUp 对象以文本列表的形式显示不同对象的多个拾取器
当与不同类型的拾取器一起工作时,一种方法是在 C# List中使用,以维护当前库存中项目的一个灵活长度的数据结构。在这个菜谱中,我们将向您展示,每次拾取一个项目时,都会向这样的List集合中添加一个新的对象。通过遍历List,每次库存更改时都会生成物品的文本显示。我们引入一个非常简单的PickUp脚本类,演示如何将拾取的信息存储在脚本组件中,在碰撞时提取,并存储在我们的List中。

准备工作
本菜谱假设您是从本章第一道菜谱中设置的Simple2Dgame_SpaceGirl项目开始的。您需要的字体可以在1362_02_02文件夹中找到。
如何操作...
要显示多个不同类型拾取器的库存总数文本,请按照以下步骤操作:
-
从
Simple2Dgame_SpaceGirl迷你游戏的副本开始。 -
编辑标签,将标签星更改为拾取。确保
starGameObject 现在具有标签拾取。 -
将以下 C#脚本
PickUp添加到层次结构中的 GameObjectstar:using UnityEngine; using System.Collections; public class PickUp : MonoBehaviour { public string description; } -
在检查器中,将 GameObject
star的组件拾取(脚本)的描述属性更改为文本star,如图所示:![如何操作...]()
-
在层次结构面板中选择 GameObject
star,并复制此 GameObject,将其重命名为heart。 -
在检查器中,将 GameObject
heart的组件拾取(脚本)的描述属性更改为文本heart。然后,从项目面板(文件夹Sprites)中将图像healthheart拖动到 GameObjectheart的 Sprite 属性中。现在,玩家应该能在屏幕上看到这个拾取物品的心形图像。 -
在 Hierarchy 面板中选择 GameObject
star,并复制此 GameObject,将其重命名为key。 -
在 Inspector 中,将 GameObject
key的组件 Pick Up (Script) 的描述属性更改为文本key.。也将从 Project 面板(文件夹 Sprites)中的图像 icon_key_green_100 拖动到 GameObjectkey的 Sprite 属性中。现在玩家应该在屏幕上看到这个拾取物品的钥匙图像。 -
为每个拾取 GameObject 制作一个或两个副本,并将它们排列在屏幕周围,以便星形、心形和钥匙拾取 GameObject 各有两个或三个。
-
将以下 C# 脚本
Player添加到 Hierarchy 中的 GameObjectplayer-SpaceGirl:using UnityEngine; using System.Collections; using UnityEngine.UI; using System.Collections.Generic; public class Player : MonoBehaviour { private PlayerInventoryDisplay playerInventoryDisplay; private List<PickUp> inventory = new List<PickUp>(); void Start(){ playerInventoryDisplay = GetComponent<PlayerInventoryDisplay>(); playerInventoryDisplay.OnChangeInventory(inventory); } void OnTriggerEnter2D(Collider2D hit){ if(hit.CompareTag("Pickup")){ PickUp item = hit.GetComponent<PickUp>(); inventory.Add( item ); playerInventoryDisplay.OnChangeInventory(inventory); Destroy(hit.gameObject); } } } -
添加一个 UI Text 对象(Create | UI | Text)。将其重命名为
Text-inventory-list。将其文本更改为 the quick brown fox jumped over the lazy dog the quick brown fox jumped over the lazy dog,或另一个长列表的胡言乱语,以测试您在下一步中更改的溢出设置。 -
在 Text (Script) 组件中,确保 Horizontal Overflow 设置为 Wrap,并将 Vertical Overflow 设置为 Overflow——这将确保文本将换行到第二行或第三行(如果需要),并且当有很多拾取物时不会被隐藏。
-
在 Inspector 面板中,将其字体设置为 Xolonium-Bold(文件夹
Fonts),并将颜色设置为黄色。对于 Alignment 属性,水平居中文本,并确保文本垂直对齐,将 Font Size 设置为 28 并选择黄色文本 Color。 -
编辑其 Rect Transform 并将其 Height 设置为
50。然后,在按住 SHIFT 和 ALT(以设置轴点和位置)的同时,选择顶部拉伸框。现在文本应位于 Game 面板的中间顶部,其宽度应拉伸以匹配整个面板。 -
您的文本现在应出现在游戏面板的顶部。
-
将以下 C# 脚本
PlayerInventoryDisplay添加到 Hierarchy 中的 GameObjectplayer-SpaceGirl:using UnityEngine; using System.Collections; using UnityEngine.UI; using System.Collections.Generic; public class PlayerInventoryDisplay : MonoBehaviour { public Text inventoryText; public void OnChangeInventory(List<PickUp> inventory){ // (1) clear existing display inventoryText.text = ""; // (2) build up new set of items string newInventoryText = "carrying: "; int numItems = inventory.Count; for(int i = 0; i < numItems; i++){ string description = inventory[i].description; newInventoryText += " [" + description+ "]"; } if(numItems < 1) newInventoryText = "(empty inventory)"; // (3) update screen display inventoryText.text = newInventoryText; } } -
从 Hierarchy 视图中选择 GameObject
player-SpaceGirl。然后,从 Inspector 中访问 Player Inventory Display (Script) 组件,并将 Inventory Text 公共字段填充为 UI Text 对象Text-inventory-list。 -
开始游戏——每次拾取星星、钥匙或心形时,您所携带的更新列表应以 carrying: [key] [heart] 的形式显示。
它是如何工作的...
在脚本类 Player 中,变量 inventory 是一个 C# List<>。这是一个灵活的数据结构,可以进行排序、搜索,并在游戏运行时动态(在游戏进行时)向其中添加和删除项目。尖括号中的 <PickUp> 表示变量 inventory 将包含 PickUp 对象的列表。对于这个配方,我们的 PickUp 类只有一个字段,即一个字符串描述,但我们在后面的配方中将在 PickUp 类中添加更复杂的数据项。
当场景开始时,脚本类 Player 的 Start() 方法获取 PlayerInventoryDisplay 脚本组件的引用,并将变量 inventory 初始化为一个新的空 C# PickUp 对象列表。当 OnColliderEnter2D(…) 方法检测到与标记为 Pickup 的项目发生碰撞时,被击中的项目的 PickUp 对象组件将被添加到我们的 inventory 列表中。同时还会调用 playerInventoryDisplay 的 OnChangeInventory(…) 方法来更新玩家的库存显示,并将更新的 inventory List 作为参数传递。
脚本类 playerInventoryDisplay 有一个公共变量,与 UI Text 对象 Text-inventory-list 相关联。OnChangeInventory(…) 方法首先将 UI 文本设置为空,然后遍历库存列表,构建一个包含每个项目描述的字符串([key],[heart],等等)。如果没有项目在列表中,则字符串设置为文本 (empty inventory)。最后,将 UI Text 对象 Text-inventory-list 的文本属性设置为变量 inventory 内部表示的字符串值。
还有更多...
一些你不希望错过的细节:
按字母顺序排列库存列表中的项目
如果能按字母顺序对 inventory 列表中的单词进行排序那就太好了——这不仅为了整洁和一致性(因此,在游戏中,如果我们捡起一个钥匙和一个心形,无论顺序如何,看起来都一样),而且还因为相同类型的物品将一起列出,这样我们可以轻松地看到我们携带了多少每种物品。

要实现 inventory 列表中项目的字母排序,我们需要做以下几步:
-
将以下 C# 代码添加到脚本类
PlayerInventoryDisplay中OnChangeInventory(...)方法的开头:public void OnChangeInventory(List<PickUp> inventory){ inventory.Sort( delegate(PickUp p1, PickUp p2){ return p1.description.CompareTo(p2.description); } ); // rest of the method as before … } -
你现在应该能看到所有项目按字母顺序列出。这段 C# 代码利用了
List.Sort(…)方法,这是集合的一个特性,其中每个项目都可以与下一个项目进行比较,如果顺序错误(如果CompareTo(…)方法返回 false),则它们会被交换。
通过动态字典<>显示不同对象的多个拾取项作为文本总计,字典包含 PickUp 对象和 "enum" 拾取类型
尽管之前的菜谱工作得很好,但描述中可能已经输入了任何旧文本,或者可能是误输入(star、Sstar、starr等等)。一个更好的方法是将游戏属性限制为预定义(枚举)列表中的可能值之一,是使用 C# 枚举。除了消除输入字符串错误的机会外,这也意味着我们可以编写代码来适当地处理预定义的可能值集合。在这个菜谱中,我们将通过引入三种可能的拾取类型(星、心形和钥匙)来改进我们的通用 PickUp 类,并编写库存显示代码,该代码计算携带的每种类型拾取的数量,并通过屏幕上的 UI Text 对象显示这些总数。我们还从使用 List 切换到使用 Dictionary,因为 Dictionary 数据结构是专门为键值对设计的,非常适合将数值总数与枚举拾取类型关联起来。

准备工作
这个菜谱是本章中之前菜谱的延续。
如何做...
要通过动态的 Dictionary 显示不同对象的多个拾取作为文本总数,请按照以下步骤操作:
-
复制你之前菜谱的工作。
-
将脚本类
PickUp的内容替换为以下代码:using UnityEngine; using System.Collections; public class PickUp : MonoBehaviour { public enum PickUpType { Star, Key, Heart } public PickUpType type; } -
将脚本类
Player的内容替换为以下代码:using UnityEngine; using System.Collections; using UnityEngine.UI; using System.Collections.Generic; public class Player : MonoBehaviour { private InventoryManager inventoryManager; void Start(){ inventoryManager = GetComponent<InventoryManager>(); } void OnTriggerEnter2D(Collider2D hit){ if(hit.CompareTag("Pickup")){ PickUp item = hit.GetComponent<PickUp>(); inventoryManager.Add( item ); Destroy(hit.gameObject); } } } -
将脚本类
PlayerInventoryDisplay的内容替换为以下代码:using UnityEngine; using System.Collections; using UnityEngine.UI; using System.Collections.Generic; public class PlayerInventoryDisplay : MonoBehaviour { public Text inventoryText; private string newInventoryText; public void OnChangeInventory(Dictionary<PickUp.PickUpType, int> inventory){ inventoryText.text = ""; newInventoryText = "carrying: "; int numItems = inventory.Count; foreach(var item in inventory){ int itemTotal = item.Value; string description = item.Key.ToString(); newInventoryText += " [ " + description + " " + itemTotal + " ]"; } if(numItems < 1) newInventoryText = "(empty inventory)"; inventoryText.text = newInventoryText; } } -
将以下 C# 脚本
InventoryManager添加到 Hierarchy 中的player-SpaceGirlGameObject:using UnityEngine; using System.Collections; using System.Collections.Generic; public class InventoryManager : MonoBehaviour { private PlayerInventoryDisplay playerInventoryDisplay; private Dictionary<PickUp.PickUpType, int> items = new Dictionary<PickUp.PickUpType, int>(); void Start(){ playerInventoryDisplay = GetComponent<PlayerInventoryDisplay>(); playerInventoryDisplay.OnChangeInventory(items); } public void Add(PickUp pickup){ PickUp.PickUpType type = pickup.type; int oldTotal = 0; if(items.TryGetValue(type, out oldTotal)) items[type] = oldTotal + 1; else items.Add (type, 1); playerInventoryDisplay.OnChangeInventory(items); } } -
在 Hierarchy(或 Scene)面板中,依次选择每个拾取 GameObject,并在 Inspector 面板中的下拉菜单中选择其对应的 Type。正如你所看到的,公共变量如果是
enum类型,它们会自动限制为可能的值集合,作为 Inspector 面板中的组合框下拉菜单。![如何做...]()
-
玩游戏。首先,你应该在屏幕上看到一个消息,说明库存为空,然后当你拾取每种类型的单个或多个物品时,你会看到你收集的每种类型的文本总数。
它是如何工作的...
场景中的每个拾取 GameObject 都有一个名为 PickUp 类的脚本组件。每个 PickUp GameObject 的 PickUp 对象有一个单一属性,即拾取类型,它必须是 Star、Key、Heart 等枚举集合中的一个。Player 脚本类通过其 Start() 方法获取对 InventoryManager 组件的引用,并且每当玩家的角色与拾取 GameObject 发生碰撞时,它都会调用库存管理器的 Add(…) 方法,并将碰撞对象的 PickUp 对象传递给它。
在这个菜谱中,玩家携带的库存由 C# 的 Dictionary 表示。在这种情况下,我们在脚本类 InventoryManager 中有一个键值对字典,其中键是可能的 PickUp.PickUpType 枚举值之一,而值是携带该类型拾取物的整数总数。每个 InventoryItemTotal 对象只有两个属性:一个 PickUp 类型和一个整数总数。在脚本类 Player 和 PlayerInventoryDisplay 之间添加了 InventoryManager 的这一额外层,目的是为了将 Player 的行为与库存的内部存储方式分开,并防止 Player 脚本类变得过大,试图处理过多的不同职责。
C# 字典提供了一个 TryGetValue(…) 方法,它接收键的参数,并传递一个与 Dictionary 的值相同数据类型的变量的引用。当调用库存管理器的 Add(…) 方法时,会测试 PickUp 对象的类型,以查看是否在 Dictionary items 中已经存在该类型的总数。如果找到给定类型的项总数在 Dictionary 中,则该项目的 Dictionary 中的值会增加。如果没有找到给定类型的条目,则会在 Dictionary 中添加一个新的元素,其总数为 1。
Add(…) 方法的最后一个动作是调用玩家 GameObject 的 PlayerInventoryDisplay 脚本组件的 OnChangeInventory(…) 方法,以更新屏幕上显示的文本总数。在 PlayerInventoryDisplay 中,此方法遍历 Dictionary,构建类型名称和总数的字符串,然后更新 UI Text 对象的文本属性,向玩家显示库存总数。
在 Unity Technologies 的教程中了解更多关于在 Unity 中使用 C# 列表和字典的信息,请访问 unity3d.com/learn/tutorials/modules/intermediate/scripting/lists-and-dictionaries。
使用 UI Grid Layout Groups 通用多个图标显示(带滚动条!)
到目前为止,本章中的菜谱都是为每种情况手工制作的。虽然这样做是可以的,但更通用和自动化的库存 UI 方法有时可以节省时间和精力,同时仍然达到相同质量和可用性的视觉效果。在下一道菜谱中,我们将开始探索一种更工程化的库存 UI 方法,通过利用 Unity 5 的 Grid Layout Group 组件提供的自动尺寸和布局。

准备工作
本菜谱假设您是从本章第一道菜谱中设置的 Simple2Dgame_SpaceGirl 项目开始的。所需的字体可以在 1362_02_02 文件夹中找到。
如何操作...
要使用 UI 网格布局组显示灰色和黄色星形图标以表示多个对象拾取,请按照以下步骤操作:
-
从一个新的
Simple2Dgame_SpaceGirl迷你游戏副本开始。 -
在层次面板中,创建一个 UI 面板
Panel–background(创建 | UI | 面板)。 -
现在将
Panel–background放置在游戏面板的顶部,拉伸画布的水平宽度。编辑 UI 图像的矩形变换组件,在按住SHIFT和ALT(以设置支点和位置)的同时,选择顶部拉伸框。 -
面板仍然会占据整个游戏窗口。因此,现在在检查器面板中,将
Panel–background的高度(在矩形变换组件中)更改为 100,如图所示:![如何操作...]()
-
添加一个 UI 文本对象(创建 | UI | 文本),将其重命名为
Text-inventory,并将其文本更改为库存。 -
在层次面板中,将此 UI 文本对象作为子对象添加到面板
Panel–background中。 -
在检查器面板中,还将
Text-inventory的字体设置为Xolonium-Bold(在Fonts文件夹中)。水平居中文本,垂直顶部对齐文本,将其高度设置为50,并将字体大小设置为23。 -
编辑
Text-inventory的矩形变换,在按住SHIFT和ALT(以设置支点和位置)的同时,选择顶部拉伸框。现在文本应位于UI 面板Panel–background的中间顶部,并且其宽度应拉伸以匹配整个面板的宽度。 -
在层次面板中选择画布,并添加一个新的 UI 面板对象(创建 | UI | 图像)。将其重命名为
Panel-slot-grid。 -
将
Panel-slot-grid放置在游戏面板的顶部,拉伸画布的水平宽度。编辑 UI 图像的矩形变换组件,在按住SHIFT和ALT(以设置支点和位置)的同时,选择顶部拉伸框。 -
在检查器面板中,将
Panel-slot-grid的高度(在矩形变换组件中)更改为80,并设置其顶部为20(这样它就在 UI 文本游戏对象Text-inventory下方)。 -
在层次面板中选择
Panel-slot-grid面板,添加一个网格布局组组件(添加组件 | 布局 | 网格布局组)。将单元格大小设置为70x70,将间距设置为5x5。同时,将子对齐设置为居中(这样我们的图标在左侧和右侧将有均匀的间距),如图所示:![如何操作...]()
-
在层次面板中选择
Panel-slot-grid面板,添加一个遮罩(脚本)组件(添加组件 | UI | 遮罩)。取消勾选显示遮罩图形选项。拥有这个遮罩组件意味着用户将看不到我们网格的任何溢出部分——只有面板Panel-slot-grid的图像区域内的内容才会可见。 -
在您的Canvas中添加一个 UI Image 对象(创建 | UI | Image)。将其重命名为
Image-slot。 -
在层次结构面板中,将子 UI Image 对象
Image-slot添加到面板Panel–slot-grid。 -
将
Image-slot的源图像设置为 Unity 提供的旋钮(圆形)图像,如图所示:![如何操作...]()
-
由于
Image-slot是Panel-slot-grid内部唯一的 UI 对象,它将在该面板中居中显示(尺寸为 70 x 70),如图所示:![如何操作...]()
-
每个图像槽位将有一个黄色星形子图像和一个灰色星形子图像。现在让我们创建这些图像。
-
在您的Canvas中添加一个 UI Image 对象(创建 | UI | Image)。将其重命名为
Image-star-yellow。 -
在层次结构面板中,将子 UI Image 对象
Image-star-yellow添加到图像Image–slot。 -
将
Image-star-yellow的源图像设置为icon_star_100图像(在Sprites文件夹中)。 -
现在,我们将设置黄色星形图标图像以完全填充其父
Image-slot,通过水平和垂直拉伸。编辑 UI Image 的 Rect Transform组件,并在按住SHIFT和ALT(以设置枢轴和位置)的同时,选择底部右侧选项以完全拉伸。UI ImageImage-star-yellow现在应该位于Image-slot圆形旋钮图像的中间,如图所示:![如何操作...]()
-
在层次结构面板中复制
Image-star-yellow,将其命名为Image-star-grey。这个新的 GameObject 也应该成为Image-slot的子项。 -
将
Image-star-grey的源图像更改为icon_star_grey_100图像(在Sprites文件夹中)。在任何时候,我们的库存槽位都可以显示无内容、黄色星形图标或灰色星形图标,具体取决于Image-star-yellow和Image-star-grey是否启用:我们将在本食谱后面的库存显示代码中控制这一点。 -
在层次结构面板中,确保已选择
Image-slot,并添加以下代码的 C#脚本PickupUI:using UnityEngine; using System.Collections; public class PickupUI : MonoBehaviour { public GameObject starYellow; public GameObject starGrey; void Awake(){ DisplayEmpty(); } public void DisplayYellow(){ starYellow.SetActive(true); starGrey.SetActive(false); } public void DisplayGrey(){ starYellow.SetActive(false); starGrey.SetActive(true); } public void DisplayEmpty(){ starYellow.SetActive(false); starGrey.SetActive(false); } } -
在层次结构面板中选择 GameObject
Image-slot,将其两个子项Image-star-yellow和Image-star-grey拖动到相应的检查器面板拾取 UI槽位星黄色和星灰色,如图所示:![如何操作...]()
-
在层次结构面板中,复制
Image-slot九次;它们应该自动命名为Image-slot 1 .. 9。查看以下截图以确保您的 Canvas 层次结构正确——Image-slot作为Image-slot-grid的子项,以及Image-star-yellow和Image-star-grey作为每个Image-slot的子项的父子关系非常重要。![如何操作...]()
-
在层次面板中,确保选择
player-SpaceGirl,并添加以下代码的 C#脚本Player:using UnityEngine; using System.Collections; using UnityEngine.UI; public class Player : MonoBehaviour { private PlayerInventoryModel playerInventoryModel; void Start(){ playerInventoryModel = GetComponent<PlayerInventoryModel>(); } void OnTriggerEnter2D(Collider2D hit){ if(hit.CompareTag("Star")){ playerInventoryModel.AddStar(); Destroy(hit.gameObject); } } } -
在层次面板中,确保选择
player-SpaceGirl,并添加以下代码的 C#脚本PlayerInventoryModel:using UnityEngine; using System.Collections; public class PlayerInventoryModel : MonoBehaviour { private int starTotal = 0; private PlayerInventoryDisplay playerInventoryDisplay; void Start(){ playerInventoryDisplay = GetComponent<PlayerInventoryDisplay>(); playerInventoryDisplay.OnChangeStarTotal(starTotal); } public void AddStar(){ starTotal++; playerInventoryDisplay.OnChangeStarTotal(starTotal); } } -
在层次面板中,确保选择
player-SpaceGirl,并添加以下代码的 C#脚本PlayerInventoryDisplay:using UnityEngine; using System.Collections; using UnityEngine.UI; public class PlayerInventoryDisplay : MonoBehaviour { const int NUM_INVENTORY_SLOTS = 10; public PickupUI[] slots = new PickupUI[NUM_INVENTORY_SLOTS]; public void OnChangeStarTotal(int starTotal){ for(int i = 0; i < NUM_INVENTORY_SLOTS; i++){ PickupUI slot = slots[i]; if(i < starTotal) slot.DisplayYellow(); else slot.DisplayGrey(); } } } -
在层次面板中选中 GameObject
player-SpaceGirl后,将十个Image-slotGameObjects 拖动到玩家库存显示(脚本)组件数组槽位中,在检查器面板中,如图所示:![如何操作...]()
-
保存场景并开始游戏。当你收集星星时,你应该会看到库存显示中更多的灰色星星变为黄色。
它是如何工作的...
我们在游戏画布的顶部创建了一个简单的面板(Panel-background)和文本——“库存”。在这个区域内部(Panel-slot-grid)创建了一个小面板,其中包含一个网格布局组件,它会自动调整大小并排列我们使用旋钮(圆形)源图像创建的 10 个Image-slot GameObjects。通过向Panel-slot-grid添加一个遮罩组件,我们确保内容不会超出该面板源图像的矩形范围。
Panel-slot-grid的 10 个Image-slot GameObjects 子项中每个都包含一个黄色星星图像和一个灰色星星图像。此外,每个Image-slot GameObjects 都有一个脚本组件PickupUI。PickupUI脚本提供了三个公共方法,可以显示仅黄色星星图像、仅灰色星星图像或两者都不显示(因此,将看到一个空旋钮圆形图像)。
我们玩家的角色 GameObject player-SpaceGirl有一个非常简单的Player脚本——这个脚本仅检测与标记为Star的对象的碰撞,当发生碰撞时,它会移除与之碰撞的星星 GameObject,并调用其playerInventoryModel脚本组件的AddStar()方法。PlayerInventoryModel C#脚本类维护一个累计整数,记录添加到库存中的星星数量。每次调用AddStar()方法时,它会增加(加 1)这个总数,然后调用脚本组件playerInventoryDisplay的OnChangeStarTotal(…)方法。此外,当场景开始时,会调用OnChangeStarTotal(…)方法,以便设置 UI 显示以显示我们最初没有携带任何星星。
C#脚本类PlayerInventoryDisplay有两个属性:一个是定义我们库存中槽位数量的常量整数,对于这个游戏,我们将其设置为 10,另一个是变量是一个指向PickupUI脚本组件的引用数组——这些中的每一个都是指向我们Panel-slot-grid中的 10 个Image-slot GameObject 中的脚本组件的引用。当OnChangeStarTotal(…)方法传递我们携带的星星数量时,它遍历这 10 个槽位。当当前槽位小于我们的星星总数时,通过调用当前槽位的DisplayYellow()方法(PickupUI脚本组件)显示一个黄色星星。一旦循环计数器等于或大于我们的星星总数,那么所有剩余的槽位都通过调用方法DisplayGrey()显示为灰色星星。
这个配方是 MVC 设计模式低耦合的一个例子。我们设计我们的代码不依赖于或对游戏的其它部分做出太多假设,这样其他部分游戏中的变化破坏我们的库存显示代码的可能性就小得多。显示(视图)与我们所携带的逻辑表示(模型)是分离的,对模型的更改是通过玩家(控制器)调用的公共方法来实现的。
注意
注意:看起来我们可能可以通过假设槽位总是显示灰色(没有星星)并且每次捡到黄色星星时只改变一个槽位为黄色来使我们的代码更简单。但如果游戏中发生某些情况(例如,撞到黑洞或被外星人射击)导致我们掉落一个或多个星星,这就会导致问题。C#脚本类PlayerInventoryDisplay对哪些槽位可能或可能没有以前显示为灰色、黄色或空没有做出任何假设——每次调用它时,它都会确保显示适当数量的黄色星星,并且所有其他槽位都显示为灰色星星。
还有更多...
一些你不希望错过的细节:
将水平滚动条添加到库存槽位显示中
我们现在可以看到 10 个库存槽位——但如果有很多更多呢?一个解决方案是添加一个滚动条,这样用户就可以左右滚动,每次查看 10 个,如下面的截图所示。让我们在我们的游戏中添加一个水平滚动条。这可以通过不进行任何 C#代码更改,完全通过 Unity 5 UI 系统来实现。

要实现库存显示的水平滚动条,我们需要做以下几步:
-
将
Panel-background的高度增加到 130 像素。 -
在检查器面板中,将
Panel-slot-grid组件的Child Alignment属性设置为Upper Left。然后,将此面板稍微向右移动,以便 10 个库存图标在屏幕上居中。 -
在层次结构面板中,将 Image-slot 9 复制三次,现在
Panel-slot-grid中有 13 个库存图标。 -
在场景面板中,将
Panel-slot-grid面板的右侧边缘拖动,使其足够宽,以便所有 13 个库存图标都能水平放置——当然,最后三个将超出屏幕,如图所示:![向库存槽位显示添加水平滚动条]()
-
在画布上添加一个 UI 面板,命名为
Panel-scroll-container,并通过将其图像(脚本)组件的颜色属性设置为红色来着色。 -
调整
Panel-scroll-container的大小和位置,使其刚好位于我们的Panel-slot-grid之后。因此,你现在应该看到 10 个库存圆圈槽位后面的红色矩形。 -
在层次结构面板中,将
Panel-slot-grid拖动,使其成为Panel-scroll-container的子项。 -
向
Panel-scroll-container添加一个 UI 遮罩,现在你应该只能看到这个红色着色面板内的 10 个库存图标。注意
注意:你可能希望暂时将此遮罩组件设置为非活动状态,以便在需要时可以看到并处理
Panel-slot-grid的未显示部分。 -
在画布上添加一个 UI 滚动条,命名为
Scrollbar-horizontal。将其移动到 10 个库存图标下方,并调整大小以与着色为红色的Panel-scroll-container具有相同的宽度,如图所示:![向库存槽位显示添加水平滚动条]()
-
将 UI 滚动矩形组件添加到
Panel-scroll-container。 -
在检查器面板中,将
Scrolbar-horizontal拖动到Panel-scroll-container组件的水平滚动条属性。 -
在检查器面板中,将
Panel-slot-grid拖动到Panel-scroll-container组件的内容属性,如图所示:![向库存槽位显示添加水平滚动条]()
-
现在,确保
Panel-scroll-container的遮罩组件设置为活动状态,这样我们就不会看到Panel-slot-grid的溢出部分,并取消选中此遮罩组件的显示遮罩图形选项(这样我们就不会再看到红色矩形)。
你现在应该有一个可工作的可滚动库存系统。请注意,最后三个新图标将只是空圆圈,因为库存显示脚本没有对这些额外三个槽位进行引用或尝试进行更改;因此,需要更改脚本代码以反映我们添加到Panel-slot-grid的每个额外槽位。
自动化 PlayerInventoryDisplay 获取所有槽位的引用
从 Hierarchy 面板中将槽位拖动到脚本组件 PlayerInventoryDisplay 的数组中,这个过程需要一些工作(如果在错误顺序或重复拖动相同的项目时,可能会出错)。此外,如果我们更改槽位数量,我们可能需要重新做所有这些工作,或者如果我们增加数量,记得拖动更多槽位,等等。更好的方法是,在场景开始时,脚本类 PlayerInventoryDisplay 的第一个任务是创建每个 Image-slot GameObject 作为 Panel-slot-grid 的子对象,并同时在数组中填充。
为了实现此配方中脚本数组 PickupUI 对象的自动化填充,我们需要执行以下操作:
-
创建一个名为
Prefabs的新文件夹。在这个文件夹中,创建一个名为starUI的新空预制件。 -
从 Hierarchy 面板中,将 GameObject
Image-slot拖动到您新创建的空预制件starUI中。现在,这个预制件应该变成蓝色,表示它已被填充。 -
在 Hierarchy 面板中,删除 GameObject
Image-slot及其所有副本Image-slot 1 – 9。 -
将 GameObject
player-SpaceGirl中的 C# 脚本PlayerInventoryDisplay替换为以下代码:using UnityEngine; using System.Collections; using UnityEngine.UI; public class PlayerInventoryDisplay : MonoBehaviour { const int NUM_INVENTORY_SLOTS = 10; private PickupUI[] slots = new PickupUI[NUM_INVENTORY_SLOTS]; public GameObject slotGrid; public GameObject starSlotPrefab; void Awake(){ for(int i=0; i < NUM_INVENTORY_SLOTS; i++){ GameObject starSlotGO = (GameObject) Instantiate(starSlotPrefab); starSlotGO.transform.SetParent(slotGrid.transform); starSlotGO.transform.localScale = new Vector3(1,1,1); slots[i] = starSlotGO.GetComponent<PickupUI>(); } } public void OnChangeStarTotal(int starTotal){ for(int i = 0; i < NUM_INVENTORY_SLOTS; i++){ PickupUI slot = slots[i]; if(i < starTotal) slot.DisplayYellow(); else slot.DisplayGrey(); } } } -
在 Hierarchy 面板中选择 GameObject
player-SpaceGirl,将 GameObjectPanel-slot-grid拖动到 Inspector 面板中的 Player Inventory Display (Script) 变量的 Slot grid。 -
在 Hierarchy 面板中选择 GameObject
player-SpaceGirl,从 Project 面板的预制件starUI拖动到 Inspector 面板中的 Player Inventory Display (Script) 变量的 Star Slot Prefab,如图所示:![PlayerInventoryDisplay 获取所有槽位的自动化]()
公共数组已被改为私有,不再需要通过手动拖放来填充。当你运行游戏时,它将和以前一样运行,现在我们的库存网格面板中的图像数组填充现在是自动化的。Awake() 方法创建预制件的新实例(根据常量 NUM_INVENTORY_SLOTS 定义的数量),并立即将其作为子对象附加到 Panel-slot-grid。由于我们有一个网格布局组组件,它们在我们的面板中的放置将自动整齐有序。
小贴士
注意:当 GameObject 改变其父对象时,其变换组件的缩放属性会被重置(以保持相对子对象大小与父对象大小之间的比例)。因此,在 GameObject 成为另一个 GameObject 的子对象后,立即将其局部缩放重置为 (1,1,1) 是一个好主意。我们在 SetParent(…) 语句之后的 for 循环中这样做。
注意,我们使用Awake()方法在PlayerInventoryDispay中创建预制实例,这样我们就可以知道这将在PlayerInventoryModel中的Start()方法之前执行——因为只有在场景中所有 GameObject 的Awake()方法都完成后才会执行Start()方法——由于没有执行Start()方法。
根据库存中的槽位数量自动更改网格单元格大小
考虑这样一种情况,我们希望更改槽位数量。另一种不使用滚动条的方法是更改网格布局组组件中的单元格大小。我们可以通过代码来自动化这个过程,以确保NUM_INVENTORY_SLOTS将适合画布顶部面板的宽度。
要实现此菜谱中网格布局组单元格大小的自动调整,我们需要执行以下操作:
-
在
player-SpaceGirl的PlayerInventoryDisplayGameObject 中的 C#脚本PlayerInventoryDisplay中添加以下Start()方法:void Start(){ float panelWidth = slotGrid.GetComponent<RectTransform>().rect.width; print ("slotGrid.GetComponent<RectTransform>().rect = " + slotGrid.GetComponent<RectTransform>().rect); GridLayoutGroup gridLayoutGroup = slotGrid.GetComponent<GridLayoutGroup>(); float xCellSize = panelWidth / NUM_INVENTORY_SLOTS; xCellSize -= gridLayoutGroup.spacing.x; gridLayoutGroup.cellSize = new Vector2(xCellSize, xCellSize); }![根据库存中的槽位数量自动更改网格单元格大小]()
我们在Start()方法中编写代码,而不是在Awake()方法中添加代码,以确保 GameObject Panel-slot-grid的RectTransform已经完成尺寸调整(在这个菜谱中,它根据游戏面板的宽度拉伸)。虽然我们无法知道场景开始时层次结构GameObject 的创建顺序,但我们可以依赖 Unity 的行为,即每个 GameObject 都会发送Awake()消息,并且只有当所有相应的Awake()方法都执行完毕后,才会发送Start()消息。因此,Start()方法中的任何代码都可以安全地假设每个 GameObject 都已初始化。
上述截图显示了NUM_INVENTORY_SLOTS的值已更改为 15,并且单元格大小相应地更改,以便所有 15 个单元格现在都水平地适合我们的面板。请注意,单元格之间的间距从计算出的可用宽度中减去,因为每个显示的项目之间也需要这个间距(xCellSize -= gridLayoutGroup.spacing.x)。
向 Rect Transform 脚本类添加一些帮助方法
如果我们希望进一步使用代码更改,例如RectTransform属性,我们可以通过创建包含特殊静态方法的文件并使用特殊的“this”关键字来添加扩展方法。以下代码向RectTransform脚本组件添加了SetWidth(…), SetHeight(…), 和 SetSize(…)方法:
using UnityEngine;
using System;
using System.Collections;
public static class RectTransformExtensions
{
public static void SetSize(this RectTransform trans, Vector2 newSize) {
Vector2 oldSize = trans.rect.size;
Vector2 deltaSize = newSize - oldSize;
trans.offsetMin = trans.offsetMin - new Vector2(deltaSize.x * trans.pivot.x, deltaSize.y * trans.pivot.y);
trans.offsetMax = trans.offsetMax + new Vector2(deltaSize.x * (1f - trans.pivot.x), deltaSize.y * (1f - trans.pivot.y));
}
public static void SetWidth(this RectTransform trans, float newSize) {
SetSize(trans, new Vector2(newSize, trans.rect.size.y));
}
public static void SetHeight(this RectTransform trans, float newSize) {
SetSize(trans, new Vector2(trans.rect.size.x, newSize));
}
}
Unity C#允许我们通过声明static void方法来添加这些扩展方法,其中第一个参数的形式为this
我们需要做的所有事情就是在项目面板中的脚本文件夹中创建一个新的 C#脚本类文件RectTransformExtensions,包含上述代码。实际上,你可以找到由 OrbcreationBV 创建的整个有用的额外RectTransform方法集(上述代码只是其中的一部分),并且这些方法在网上www.orbcreation.com/orbcreation/page.orb?1099可用。
结论
在本章中,我们介绍了各种 C#数据表示方法,用于表示库存物品,以及一系列 Unity UI 界面组件,用于在运行时显示玩家库存的状态和内容。
库存用户界面需要高质量的图形资源以获得高质量的结果。以下是一些你可能希望探索的资源来源网站:
-
我们 SpaceGirl 迷你游戏的图形来自 Daniel Cook 的 Space Cute 艺术作品;他慷慨地发布了大量 2D 艺术作品,供游戏开发者使用:
-
Sethbyrd——许多有趣的 2D 图形:
-
适用于 2D 游戏的免版税艺术作品:
第三章。2D 动画
在本章中,我们将涵盖:
-
水平翻转精灵
-
为角色移动事件动画身体部分
-
创建一个 3 帧动画剪辑,使平台持续动画
-
使用触发器使平台一旦被踩到就开始下落,从而将动画从一个状态移动到另一个状态
-
从精灵表序列创建动画剪辑
简介
Unity 5 建立在 2014 年晚些时候在 Unity 4.6 中引入的强大 2D 功能 Mecanim 动画系统和 2D 物理系统的基础上。在本章中,我们提供了一系列菜谱来介绍 Unity 5 中 2D 动画的基础知识,并帮助你理解不同动画元素之间的关系。
整体概念
在 Unity 2D 中,可以通过几种不同的方式创建动画 - 一种方式是创建许多略有不同的图像,每个图像逐帧给出运动的外观。创建动画的第二种方式是定义对象各个部分的键帧位置(例如,手臂、腿部、脚、头部、眼睛等),并在游戏运行时让 Unity 计算所有中间位置。

两种动画来源都成为动画面板中的动画剪辑。然后,每个动画剪辑成为动画控制器状态机中的状态。然后我们定义在什么条件下 GameObject 将过渡从一个动画状态(剪辑)到另一个状态。
水平翻转精灵
可能最简单的 2D 动画就是简单的翻转,从面向左到面向右,或者从面向上到面向下,等等。在这个菜谱中,我们将向场景添加一个可爱的虫子精灵,并编写一个简短的脚本,当按下左和右箭头键时翻转其水平方向。

准备工作
对于这个菜谱,我们在文件夹1362_03_01中的Sprites文件夹中准备了你需要的图像。
如何做到这一点...
要通过箭头键按下来水平翻转一个对象,请按照以下步骤操作:
-
创建一个新的 Unity 2D 项目。
-
导入提供的图像
EnemyBug.png。 -
将敌人虫子图像的一个实例从项目 | 精灵文件夹拖到场景中。将此 GameObject 定位在(
0,0,0),并缩放到(2,2,2)。 -
将 C#脚本类
BugFlip的一个实例作为组件添加到敌人虫子GameObject:using UnityEngine; using System.Collections; public class BugFlip : MonoBehaviour { private bool facingRight = true; void Update() { if (Input.GetKeyDown(KeyCode.LeftArrow) && facingRight) Flip (); if (Input.GetKeyDown(KeyCode.RightArrow) && !facingRight) Flip(); } void Flip (){ // Switch the way the player is labelled as facing. facingRight = !facingRight; // Multiply the player's x local scale by -1. Vector3 theScale = transform.localScale; theScale.x *= -1; transform.localScale = theScale; } } -
当你运行场景时,按下左和右箭头键应该使虫子面向左或右。
它是如何工作的...
C#类定义了一个布尔变量facingRight,它存储一个true/false值,对应于虫子是否面向右。由于我们的虫子精灵最初面向右,因此我们将facingRight的初始值设置为 true 以匹配这一点。
方法 Update(),在每一帧都会检查是否按下了左或右箭头键。如果按下左箭头键且虫子面向右,则调用方法 Flip(),同样,如果按下右箭头键且虫子面向左(即面向右为假),则再次调用方法 Flip()。
方法 Flip() 执行两个动作,第一个动作简单地反转变量 facingRight 中的真/假值。第二个动作改变变换的 localScale 属性的 X 值的 +/- 符号。反转 localScale 的符号会产生我们想要的 2D 翻转效果。在下一道菜谱中查看 PlayerControl 脚本中的 BeanMan 角色内部 – 你会看到完全相同的 Flip() 方法被使用。
为角色移动事件动画化身体部位
在这个菜谱中,我们将学习如何根据跳跃事件来动画化 Unity 豆人角色的帽子。
准备工作
对于这个食谱,我们在文件夹 1362_03_02 中准备了所需的文件。
如何操作...
要为角色移动事件动画化身体部位,请按照以下步骤操作:
-
创建一个新的 Unity 2D 项目。
-
通过选择菜单:资产 | 导入包 | 自定义包…,导入提供的包
BeanManAssets,然后点击导入按钮将所有这些资产导入到你的项目面板中。 -
将主摄像机的大小增加到
10。 -
让我们为这个项目设置 2D 重力设置 – 我们将使用与 Unity 2D 平台教程相同的设置,Y=
-30。通过选择菜单:编辑 | 项目设置 | 物理 2D,然后在顶部将 Y 值更改为-30。 -
从项目 | 预制体文件夹中拖拽一个 BeanMan character2D精灵实例到场景中。将此 GameObject 定位在(
0,3,0)。 -
从项目 | 精灵文件夹中拖拽一个platformWallBlocks精灵实例到场景中。将此 GameObject 定位在(
0,-4,0)。 -
通过选择菜单:添加组件 | 物理 2D | Box Collider 2D,为 GameObject platformWallBlocks添加一个Box Collider 2D组件。
-
现在我们有一个静止的平台,玩家可以站在上面,并在左右方向行走。创建一个新的层,命名为地面,并将 GameObject platformWallBlocks分配到这个新层,如下截图所示。当角色站在平台上时按下空格键,现在他会跳起。
![如何操作...]()
-
目前,当 BeanMan 角色跳跃时,他会进行动画(手臂和腿部移动)。让我们删除动画剪辑和动画控制器,从头开始创建。如截图所示,从项目 | 资产 | PotatoMan2DAssets | Character2D | Animation中删除文件夹 Clips 和 Controllers:
![如何操作...]()
-
让我们为我们的英雄角色创建一个动画剪辑(及其相关的动画控制器)。在层次结构面板中选择 GameObject hero。确保在层次结构中选择 GameObject character2D,打开动画面板,并确保它处于Dope Sheet视图(这是默认视图)。
-
点击动画面板中的空白下拉菜单(位于灰色文字
样本旁边),选择菜单项[创建新剪辑]:![如何操作...]()
-
将新剪辑保存在Character2D | 动画文件夹中,命名为character-beanman-idle。您现在已创建了一个用于“空闲”角色状态的动画剪辑(该状态未进行动画处理)。
注意
您最终的游戏可能包含数十个甚至数百个动画剪辑。通过在剪辑名称前加上对象类型、名称以及动画剪辑的描述,使搜索变得容易。
-
在项目面板中查看Character2D | 动画文件夹,您现在应该看到您刚刚创建的动画剪辑(character-beanman-idle)以及一个新的动画控制器,该控制器默认为您的 GameObject character2D的名称:
![如何操作...]()
-
确保在层次结构中选择 GameObject character2D,打开动画面板,您将看到控制我们角色动画的状态机。由于我们只有一个动画剪辑(character-beanman-idle),因此状态机在进入时立即进入此状态。
![如何操作...]()
-
运行您的场景——由于角色始终处于“空闲”状态,当我们让它跳跃时,我们还没有看到任何动画。
-
现在我们将创建一个“跳跃”动画剪辑,用于动画化帽子。点击动画面板中的空白下拉菜单(位于灰色文字
样本旁边),并在您的动画文件夹中创建一个新的剪辑,命名为character-beanman-jump。 -
点击添加属性按钮,并通过点击其“+”加号按钮选择帽子子对象的变换 | 位置。我们现在正在记录在此动画剪辑中 GameObject 帽子的(X, Y, Z)位置的变化:
![如何操作...]()
-
您现在应该看到在 0.0 和 1.0 处有 2 个“关键帧”。这些在动画面板右侧部分的时间轴区域用菱形表示。
-
点击选择第一个关键帧(在时间 0.0 处)。现在在场景面板中移动帽子向上并向左稍微移动一点,远离头部。您应该看到在检查器中所有三个 X, Y, Z 值都有红色背景——这是为了通知您,变换组件的值正在被记录在动画剪辑中:
![如何操作...]()
-
由于 1 秒可能对于我们跳跃动画来说太长了,将第二个关键帧的菱形拖动到左侧,时间设置为 0.5。
-
我们现在需要定义角色应该从 '空闲' 状态转换到 '跳跃' 状态的时间。在 动画器 面板中选择状态 character-beanman-idle,通过右键单击并选择菜单 Make Transition,然后拖动转换箭头到状态 character-beanman-jump,如图所示:
![如何操作...]()
-
现在,让我们通过点击 动画器 面板左上角的添加参数加号按钮 "+",选择 触发器,并输入名称 Jump 来添加一个名为 'Jump' 的触发器参数:
![如何操作...]()
-
我们现在可以定义当我们的角色应该从空闲状态转换到跳跃状态时的属性。点击转换箭头以选择它,并在 检查器 面板中设置以下 4 个属性:
-
具有退出时间: 取消勾选
-
转换持续时间:
0.01 -
中断状态:
Current State -
条件: 添加
Jump(点击底部的加号按钮+)
![如何操作...]()
-
-
保存并运行你的场景。一旦角色落在平台上并且你按下 空格 键跳跃,你现在会看到角色的帽子从头上跳开,并慢慢移动回来。由于我们没有添加任何离开跳跃状态的转换,这个动画剪辑将循环,所以帽子即使在跳跃完成后也会继续移动。
-
在动画器面板中选择状态 character-beanman-jump 并添加一个返回到状态 character-beanman-idle 的新转换。选择这个转换箭头,并在 检查器 面板中设置其属性如下:
-
具有退出时间: (保持勾选)
-
退出时间:
0.5(这需要与我们的跳跃动画剪辑的第二个关键帧相同的时间值) -
转换持续时间:
0.01 -
中断状态:
Current State
-
-
保存并运行你的场景。现在当你跳跃时,帽子应该会动画一次,之后角色立即回到空闲状态。
它是如何工作的...
你已经为 GameObject character2D 添加了一个动画控制器状态机。你创建的两个动画剪辑(空闲和跳跃)在动画器面板中显示为状态。当状态机接收到 'Jump' 触发器参数时,你创建了一个从空闲到跳跃的转换。你创建了一个第二个转换,在等待 0.5 秒后(与我们的跳跃动画剪辑中的两个关键帧之间的相同持续时间)返回空闲状态。
注意,使豆人角色一切正常的关键在于,当我们使用 空格 键使角色跳跃时,GameObject character2D 的 PlayerControl C# 脚本组件中的代码,以及使精灵在屏幕上向上移动,同时也向动画控制器组件发送一个 SetTrigger(…) 消息,用于名为 Jump 的 触发器。
注意
布尔参数和触发器之间的区别在于,触发器是临时设置为True,一旦SetTrigger(…)事件被状态转换'消耗',它将自动返回到False。因此,触发器对于我们希望执行一次然后返回到先前状态的操作很有用。布尔参数是一个变量,可以在游戏的不同时间将其值设置为True或False,因此可以创建不同的转换来根据变量的值在任何时间触发。请注意,布尔参数必须使用SetBool(…)显式地将它们的值设置回False。
以下截图突出了发送SetTrigger(…)消息的代码行:

用于一系列动作(跑步/行走/跳跃/坠落/死亡等)的状态机将具有更多状态和转换。Unity 提供的 bean-man 角色有一个更复杂的状态机,以及更复杂的动画(每个动画片段的手和脚、眼睛和帽子等),这可能对您探索这些内容很有用。
在 Unity 手册网页上了解更多关于动画视图的信息:docs.unity3d.com/Manual/AnimationEditorGuide.html。
创建一个 3 帧的动画片段以使平台不断动画化
在这个菜谱中,我们将制作一个看起来像木头的平台,使其不断动画化,上下移动。这可以通过一个单一的、3 帧的动画片段(从顶部开始,位置在底部,然后再次回到顶部位置)来实现。

准备工作
这个菜谱基于之前的菜谱,所以请复制那个项目,并为此菜谱在该副本上工作。
如何操作...
要创建一个持续移动的动画平台,请按照以下步骤操作:
-
将platformWoodBlocks精灵实例从项目 | 精灵文件夹拖动到场景中。将此 GameObject 定位在(
-4,-5,0),这样这些木块就整齐地位于墙块平台的左侧,并略微低于它。 -
将 Box Collider 2D 组件添加到 GameObject platformWoodBlocks,以便玩家的角色也能站在这个平台上。选择菜单:添加组件 | 物理 2D | Box Collider 2D。
-
创建一个名为
Animations的新文件夹,用于存储我们接下来要创建的动画片段和控制器。 -
确保在层次结构中仍然选中 GameObject platformWoodBlocks,打开动画面板,并确保它处于Dope Sheet视图(这是默认视图)。
-
点击动画面板中的空下拉菜单(位于灰色文字'样本'旁边),并选择菜单项[创建新剪辑]。
-
将新剪辑保存在您的Animations文件夹中,命名为 'platform-wood-moving-up-down'。
-
点击按钮 添加曲线,选择 变换 并点击 位置 旁边的 '+' 加号。我们现在正在记录这个动画剪辑中 GameObject platformWoodBlocks 的 (X, Y, Z) 位置的变化。
-
你现在应该能看到在 0.0 和 1.0 处的 2 个 '关键帧'。这些在 时间轴 区域的右侧 动画 面板的钻石中指示。
-
我们需要 3 个关键帧,新关键帧在 2:00 秒。在 动画 面板的顶部沿时间轴点击 2:00,以便当前播放头时间的红色线位于时间 2:00。然后点击钻石加号按钮在当前播放头时间创建一个新的关键帧:
![如何操作...]()
-
第一和第三个关键帧是好的 – 它们记录了木平台在 Y=
-5处的当前高度。我们需要让中间的关键帧记录平台在运动顶部的位置,Unity 会在中间进行插值,为我们完成剩余的动画工作。通过点击时间 1:00 处的任意一个钻石(它们应该都变成蓝色,红色播放头垂直线应该移动到 1:00,以指示中间关键帧正在被编辑)来选择中间的关键帧(在时间 1:00)。 -
现在,在 检查器 中将平台的 Y 位置更改为 0。你应该会看到所有三个 X, Y, Z 值在 检查器 中都有红色背景 – 这是为了通知你 变换 组件的值正在被记录在动画剪辑中。
-
保存并运行你的场景。现在木平台应该正在连续动画,平滑地上下移动到我们设置的位置。
它是如何工作的...
你已经为 GameObject platformWoodBlocks 添加了一个动画。这个动画包含三个关键帧。关键帧表示对象在某个时间点的属性值。第一个关键帧存储了 Y 值为 -4,第二个关键帧 Y 值为 0,最后一个关键帧再次为 -4。Unity 为我们计算所有中间值,结果是平台 Y 位置的平滑动画。
注意
注意:如果我们想要复制移动平台,首先我们需要创建一个新的、空的 GameObject,命名为 movingBlockParent,然后将 platformWoodBlocks 作为子对象添加到这个 GameObject 上。复制 GameObject movingBlockParent 将允许我们在场景中创建更多移动的方块。如果我们直接复制 platformWoodBlocks,那么当场景运行时,每个副本都会被动画回原始动画帧的位置(也就是说,所有副本都会定位并移动到原始位置)。
使用触发器将平台开始下落,一旦被踩到,通过移动动画从一个状态转换到另一个状态
在许多情况下,我们希望动画在满足某些条件或发生某些事件后才开始播放。在这些情况下,组织 Animator Controller 的一个好方法是在剪辑之间有一个触发器,并有两个动画状态(剪辑)。我们使用代码来检测何时希望动画开始播放,并在那时向动画控制器发送触发器消息,从而开始过渡。
在这个菜谱中,我们将在我们的 2D 平台游戏中创建一个水平台方块;一旦被踩到,这样的方块就会开始慢慢从屏幕上掉落,因此玩家必须不断移动,否则他们也会和方块一起掉落屏幕!如下截图所示:

准备工作
这个菜谱基于之前的菜谱,所以复制那个项目,并在这个菜谱上工作。
如何操作...
要构建一个只有在接收到触发器后才会播放的动画,请按照以下步骤操作:
-
在层次结构中创建一个名为water-block-container的空GameObject,位置在(
2.5,-4, 0)。这个空 GameObject 将允许我们制作动画水方块的多份副本,这些副本将相对于其父 GameObject 的位置进行动画。 -
将Water Block精灵实例从项目 | 精灵文件夹拖动到场景中,并将其子对象设置为water-block-container。确保您的新子 GameObject Water Block的位置是(
0,0,0),这样它就会整洁地出现在墙块平台右侧,如下截图所示:![如何操作...]()
-
向子 GameObject Water Block添加一个Box Collider 2D组件,并将此 GameObject 的层设置为地面,这样玩家的角色就可以站在这个水块平台上跳跃。
-
确保在层次结构中选择了子 GameObject Water Block,打开动画面板,然后创建一个名为platform-water-up的新剪辑。将其保存在您的
Animations文件夹中。 -
点击添加曲线按钮,选择变换和位置。
-
删除时间1:00的第二关键帧。您现在已经完成了水块上动画剪辑的创建。
-
创建第二个动画剪辑,命名为platform-water-down。再次点击添加曲线按钮,选择变换和位置,并删除时间1:00的第二关键帧。
-
选择时间0:00的第一个关键帧,将 GameObject 的变换位置 Y 值设置为
-5。您现在已经完成了水块下动画剪辑的创建,因此可以点击红色录音按钮停止录制。 -
你可能已经注意到,除了你创建的上下动画剪辑之外,在你的
动画文件夹中还有一个名为Water Block的动画师控制器文件。选择此文件并打开动画师面板,以查看和编辑状态机图:![如何操作...]()
-
目前,尽管我们创建了 2 个动画剪辑(状态),但只有Up状态始终处于活动状态。这是因为当场景开始(进入)时,对象将立即进入状态platform-water-up,但由于没有从该状态到platform-water-down的过渡箭头,因此目前Water Block游戏对象将始终处于其Up状态。
-
确保选择状态platform-water-up(它周围将有一个蓝色边框),然后通过从鼠标右键点击菜单中选择创建过渡来创建一个过渡(箭头)到状态platform-water-down。
-
如果你现在运行场景,默认的过渡设置是在 0.9 秒后水块将过渡到其Down状态。我们不想这样——我们只想在玩家走上它们之后让它们向下动画。因此,通过在动画师面板中选择参数选项卡,点击加号 '+' 按钮,选择触发器,然后选择Fall来创建一个名为Fall的触发器。
-
按以下步骤创建我们的触发器:
-
在动画师面板中选择过渡
-
在检查器面板中取消选中具有退出时间选项
-
在检查器面板中将过渡结束时间拖动到2:00秒(这样水块将在 2 秒内缓慢过渡到其 Down 状态)
-
在检查器面板中点击加号 '+' 按钮添加一个条件,它应该自动建议唯一可能的条件参数,即我们的Trigger Fall。
![如何操作...]()
-
-
我们现在需要在 Water Block 上方添加一个碰撞触发器,并添加 C#脚本行为以在玩家进入碰撞器时发送动画师控制器触发器。确保选择子 GameObject Water Block,添加一个(第二个)2D Box Collider,Y 偏移为1,并勾选其是触发器复选框:
![如何操作...]()
-
将 C#脚本类
WaterBlock的一个实例作为组件添加到你的Water Block子 GameObject:using UnityEngine; using System.Collections; public class WaterBlock : MonoBehaviour { private Animator animatorController; void Start(){ animatorController = GetComponent<Animator>(); } void OnTriggerEnter2D(Collider2D hit){ if(hit.CompareTag("Player")){ animatorController.SetTrigger("Fall"); } } } -
将 GameObject water-block-container复制 6 次,每次 X 位置增加 1,即
3.5,4.5,5.5,依此类推。 -
运行场景,当玩家的角色跑过每个水块时,它们将开始下落,所以他最好继续跑!
它是如何工作的...
您创建了一个两状态的 动画控制器 状态机。每个状态都是一个 动画片段。您从 水块 上状态创建了一个 转换 到其下状态,当动画控制器接收到 Fall 触发消息时将发生转换。您创建了一个带有 触发器 的 Box Collider 2D,以便当玩家(标记为 Player)进入其碰撞器时,可以检测到脚本组件 WaterBlock,并在此时发送 Fall 触发消息,使 水块 GameObject 开始逐渐过渡到屏幕下方的下状态。
在 Unity 手册网页上了解更多关于动画控制器的内容,请访问 docs.unity3d.com/Manual/class-AnimatorController.html。
从精灵表序列创建动画片段
传统的动画方法涉及手工绘制许多图像,每个图像略有不同,它们快速逐帧显示,以产生运动的外观。对于计算机游戏动画,将包含一个或多个精灵帧序列的图像文件称为精灵表。Unity 提供了工具,可以将大型精灵表文件中的单个精灵图像拆分,以便可以使用单个帧或帧的子序列来创建动画片段,这些动画片段可以成为动画控制器状态机中的状态。在本食谱中,我们将导入并拆分一个开源怪物精灵表,将其导入为三个动画片段,用于空闲、攻击和死亡,如图所示:

准备工作
对于本章中的所有食谱,我们已在文件夹 1362_03_05 中准备了所需的精灵图像。感谢 Rosswet Mobile 使这些精灵作为开源资源可用:www.rosswet.com/wp/?p=156。
如何操作...
要从帧动画的精灵表中创建动画,请按照以下步骤操作:
-
创建一个新的 Unity 2D 项目。
-
导入提供的图像
monster1。 -
在 项目 面板中选择图像
monster1,然后在 检查器 中将其 精灵 模式更改为 多个,然后通过点击按钮 精灵编辑器 打开 精灵编辑器 面板。 -
在 精灵编辑器 中打开 切片 下拉对话框,将 类型 设置为 网格,将网格 像素大小 设置为 64x64,然后点击 切片 按钮。最后,点击 精灵编辑器 面板右上角的栏中的 应用 按钮):
![如何操作...]()
-
在 项目 面板中,您现在可以点击精灵中心右边的展开三角形按钮,您将看到该精灵的所有不同子帧。
![如何操作...]()
-
创建一个名为
Animations的文件夹。 -
在您的新的文件夹中,创建一个名为 monster-animator 的 动画控制器。
-
在场景中创建一个新的空GameObject,命名为monster1(位置为
0,0,0),并将你的monster-animator拖动到这个 GameObject 中。 -
在层次结构中选择 GameObject monster1,打开动画面板,并创建一个名为Idle的新动画剪辑。
-
在项目面板中选择图像
monster1(在其展开视图中),选择并拖动前 5 帧(帧 0-4)到动画面板。将采样率更改为 12(因为此动画是为每秒 12 帧而创建的)。![如何操作...]()
-
如果你查看monster-animator的状态图,你会看到它有一个默认状态(剪辑)名为 monster-idle。
-
当你运行场景时,你现在应该看到monster1GameObject 正在其 monster-idle 状态下动画化。你可能希望将主相机的尺寸稍微缩小一点,因为这些精灵相当小。
它是如何工作的...
Unity 的精灵编辑器了解精灵图集,一旦输入了正确的网格大小,它就会将精灵图集图像中每个网格方格内的项目视为动画的单独图像或帧。你选择了精灵动画帧的子序列,并将它们添加到几个动画剪辑中。你已经在你的 GameObject 中添加了一个动画控制器,因此每个动画剪辑都作为动画控制器状态机中的一个状态出现。
现在,你可以重复这个过程,创建一个名为 monster-attack 的动画剪辑,包含帧 8-12,以及一个名为 monster-death 的第三个剪辑,包含帧 15-21。然后,创建触发器和过渡,使怪物 GameObject 在游戏进行时过渡到适当的状态。
从 Unity 视频教程中了解更多关于 Unity 精灵编辑器的信息,请访问unity3d.com/learn/tutorials/modules/beginner/2d/sprite-editor。
结论
在本章中,我们介绍了展示 2D 游戏元素动画系统的食谱。bean-man 2D 角色来自 Unity 2D 平台游戏,你可以从 Unity 资源商店自行下载。该项目是查看更多 2D 游戏和动画技术示例的好地方(www.assetstore.unity3d.com/en/#!/content/11228)。

这里有一些链接,提供了有用的资源和信息来源,以进一步探索这些主题:
-
Unity 2D 平台游戏(BeanMan 角色来源):
-
平台精灵来自 Daniel Cook 的 Planet Cute 游戏资源:
www.lostgarden.com/2007/05/dancs-miraculously-flexible-game.html -
创建一个基本的 2D 平台游戏:
-
Hat Catch 2D 游戏教程:
www.unity3d.com/learn/tutorials/modules/beginner/live-training-archive/2d-catch-game-pt1 -
从 2D 视角看 Unity 游戏视频:
www.unity3d.com/learn/tutorials/modules/beginner/live-training-archive/introduction-to-unity-via-2d -
来自“Kenny”的免费 Creative Commons 许可的出色 2D 模块化角色集。这些资源非常适合以类似本章中豆人示例和 Unity 2D 平台游戏演示中的方式动画化身体部位:
第四章:创建地图和材质
在本章中,我们将涵盖以下主题:
-
使用标准着色器(镜面设置)创建基本材质
-
将基本材质从镜面设置调整为金属
-
将法线图应用于材质
-
将透明度和发射地图添加到材质中
-
鼠标悬停时突出显示材质
-
将细节地图添加到材质中
-
渐变材质的透明度
-
在场景内播放视频
简介
Unity 5 引入了新的基于物理的着色器。基于物理的渲染是一种技术,它根据光线与材质(更具体地说,材质由其制成的物质)在现实世界中的反应来模拟材质的外观。这种技术可以实现更真实和一致的材质。因此,您在 Unity 中的创作应该比以往任何时候都要好。在 Unity 中创建材质也变得更加高效。一旦您在可用的工作流程(金属或镜面设置;我们稍后会回到这一点)之间做出选择,就不再需要浏览下拉菜单以查找特定功能,因为 Unity 会为创建的材质优化着色器,一旦材质设置完成并且纹理图已分配,就会删除未使用的属性的冗余代码。
对于对基于物理的渲染的深入了解,我们建议您查看由 Allegorithmic 的 Wes McDermott 撰写的《全面 PBR 指南》,可在www.allegorithmic.com/pbr-guide免费获取两卷。Allegorithmic 的指南包含有关 PBR 理论和技术的宝贵信息,是本章的基本参考。我们还推荐您查看由 Renaldas Zioma(Unity)、Erland Körner(Unity)和 Wes McDermott(Allegorithmic)合著的《Unity 5 精通基于物理的着色》,可在www.slideshare.net/RenaldasZioma/unite2014-mastering-physically-based-shading-in-unity-5找到。
另一个资源是 Aras Pranckevičius(Unity)所著的《Unity 中的基于物理的着色》,可在aras-p.info/texts/files/201403-GDC_UnityPhysicallyBasedShading_notes.pdf找到。
创建和保存纹理图
材质的可视方面可以通过使用纹理进行修改。为了创建和编辑图像文件,您需要一个图像编辑器,例如 Adobe Photoshop(行业标准,并且其原生格式由 Unity 支持),GIMP 等。为了遵循本章中的食谱,强烈建议您能够访问这些软件中的一两件。
当保存纹理图时,尤其是带有 Alpha 通道的纹理图,你可能需要选择一个合适的文件格式。PSD,Photoshop 的本地格式,对于保留多层原始艺术品来说非常实用。PNG 格式也是一个很好的选择,但请注意,Photoshop 不会独立于透明度处理 PNG 的 Alpha 通道,这可能会影响材质的外观。此外,PNG 文件不支持图层。对于本章,我们将经常使用 TIF 格式,主要有三个原因:(a) 它对不使用 Photoshop 的用户开放;(b) 它使用图层;(c) 它保留了 Alpha 通道信息。文件大小显著大于 PSD 和 PNG,所以如果你有 Photoshop,可以随意保存你的工作为 PSD 格式;如果你不需要图层,或者在使用 Photoshop 时不需要 Alpha 通道,可以保存为 PNG 格式。
最后,一些建议——虽然我们可以通过使用传统的图像编辑软件手动创建材质的纹理图,但新的工具,如 Allegorthmic 的 Substance Painter 和 Bitmap2Material,使这项工作变得更加高效、完整和直观,补充了传统的纹理制作过程,甚至完全取代了它——类似于 zBrush 和 Mudbox 对 3D 建模所做的那样。对于设计专业人士,我们强烈建议至少尝试这样的工具。然而,请注意,Allegorithmic 的产品不会使用 Unity 的标准着色器,而是依赖于substance文件(这些文件由 Unity 原生支持)。
整体情况
要理解新的标准着色器,了解工作流程、它们的属性以及它们如何影响材质的外观是很好的主意。然而,有许多可能的方法可以与材质一起工作——例如,纹理图要求可能从引擎到引擎,或从一种工具到另一种工具而有所不同。目前,Unity 支持两种不同的工作流程:一种基于镜面,另一种基于金属值。尽管两种工作流程具有相似的性质(如法线、高度、遮挡和发射),但它们在设置漫反射颜色和反射特性方面有所不同。
镜面工作流程
Unity 的标准着色器(镜面设置)使用 Albedo 和 Specular/Smoothness 图,将它们结合起来创建材质的一些特性——主要是其颜色和反射特性。以下展示了 Albedo 和 Smoothness 图之间的区别:
-
反照率: 这是指材料的漫反射颜色。简单来说,这就是你通常描述材料外观的方式(英国的国旗是红、白、蓝;法拉利的标志是一个黑色马匹在黄色背景上;一些太阳镜的镜片是半透明的渐变,等等)。然而,这种描述可能会误导。纯金属物体(如铝、铬、金等)的漫反射颜色应该是黑色。我们感知到的颜色实际上来源于它们的镜面通道。另一方面,非金属物体(如塑料、木材,甚至是涂漆或生锈的金属)确实具有非常明显的漫反射颜色。Albedo 属性纹理图具有 RGB 通道用于颜色,以及(可选)用于透明度的 Alpha 通道。
-
镜面/光滑度: 这指的是材料的亮度。纹理图使用 RGB 通道表示镜面颜色(提供色调和强度信息),以及 Alpha 通道表示光滑度/光泽(暗值用于较不光滑的表面和模糊的反射;亮/白色值用于光滑、镜面般的外观)。重要的是要注意,非金属物体具有中性、非常暗的镜面颜色(例如,对于塑料,你应该使用大约 59 的灰色值)。另一方面,金属物体具有非常亮的值,并且在色调上也有点黄色。
为了说明这些概念,我们创建了一个电池对象(如下所示),具有磨砂金属盖和塑料外壳。观察每个纹理图如何贡献最终结果:

金属工作流程
Unity 的默认标准着色器将 Albedo 和 Metallic/Glossiness 纹理图结合以创建材料的颜色和反射特性。以下是一些区别:
-
反照率: 与镜面工作流程一样,这是材料的漫反射颜色;你如何描述材料。然而,金属工作流程的反照率纹理图应该以略不同于镜面工作流程的方式配置。这次,金属材料的感知漫反射颜色(铁为灰色,金色为黄色/橙色等)必须出现在反照率纹理图中。同样,反照率纹理图具有 RGB 通道用于颜色,以及(可选)用于透明度的 Alpha 通道。
-
金属/光滑度: 这指的是材料看起来有多金属。金属纹理图使用红色通道表示金属值(非金属为黑色,未涂漆或未生锈的金属为白色)和 Alpha 通道表示光滑度(与镜面工作流程类似)。请注意,金属纹理图不包含任何关于色调的信息,在这些情况下,金属光泽的黄色性质应该应用于反照率纹理图中。
要使用金属工作流程重现说明镜面工作流程的电池,需要按照以下方式重新创建纹理:

注意
你可能已经注意到我们使用了白色来传达金属物体。技术上,由于只有红色通道相关,我们也可以使用红色(R: 255, G: 0, B: 0)、黄色(R: 255, G: 255, B: 0)或任何具有 255 红色值的颜色。
其他材质属性
值得注意的是,Unity 的标准着色器支持其他图,例如:
-
法线图(Normal maps):法线图将详细的凹凸效果添加到材质中,模拟更复杂的几何形状。例如,电池正极(顶部)节点上的内部环在 3D 对象的几何形状中未建模,而是通过一个简单的法线图创建的。
-
遮挡图(Occlusion maps):使用灰度图来模拟在环境光下的物体暗部区域。通常,它用于强调关节、褶皱和其他几何形状的细节。
-
高度图(Height maps):这些图添加了一个位移效果,给人一种深度感,而不需要复杂的几何形状。
-
发射图(Emission maps):这些图增加了材质发出的颜色,就像自发光一样,例如荧光表面或液晶显示屏。发射图的纹理具有 RGB 通道用于颜色。
Unity 样本和文档
在开始之前,阅读 Unity 关于纹理的文档可能是个好主意。它可以在网上找到,地址是unity3d.com/support/documentation/Manual/Textures.html。
最后,Unity 为那些寻找如何为各种材质设置图的指南的人准备了一个很好的资源:着色器校准场景(Shader Calibration Scene),可以从 Unity Asset Store 免费下载。这是一个出色的集合,包括木材、金属、橡胶、塑料、玻璃、皮肤、泥土等样本材质(金属和镜面设置)。
使用标准着色器(镜面设置)创建基本材质
在这个配方中,我们将学习如何使用新的标准着色器(镜面设置)、一个阿尔贝多图(Albedo map)和一个镜面/光滑度图(Specular/Smoothness map)来创建一个基本材质。这个材质将包含金属和非金属部分,以及不同的光滑度级别。
准备工作
已经准备了两个文件来支持这个配方:一个电池的 3D 模型(FBX 格式),以及一个 UVW 模板纹理(PNG 格式),用于在创建漫反射纹理图时指导我们。3D 模型和 UVW 模板可以使用 3D 建模软件创建,例如 3DS MAX、Maya 或 Blender。所有必要的文件都可在1362_04_01文件夹中找到。
如何操作...
要创建一个基本材质,请按照以下步骤操作:
-
将
battery.FBX和uvw_template.png文件导入到你的项目中。 -
通过从项目视图中的资产文件夹拖动到场景视图,将电池模型放置在场景中。在场景视图中选择它,并通过检查器视图中的变换组件确保它位于X: 0、Y: 0、Z: 0。
-
现在,让我们为我们的对象创建一个高光/平滑度图。在您的图像编辑器中打开名为
uvw_template.png的图像文件(我们将使用 Adobe Photoshop 来说明下一步)。请注意,该图像文件只有一个图层,大部分是透明的,包含我们将用作高光图指南的 UVW 映射模板。 -
创建一个新图层并将其放置在带有辅助线的图层下方。用深灰色(R: 56,G: 56,B: 56)填充新图层。辅助线将在实心黑色填充的顶部可见:
![如何操作...]()
-
创建一个新图层并选择图像的上部区域(带有圆圈的区域)。然后,用略带色调的浅灰色(R: 196,G: 199,B: 199)填充该区域:
注意
我们的高光图 RGB 值不是随意的:基于物理的着色从映射过程中去除了大部分猜测,用参考研究取而代之。在我们的例子中,我们使用了基于铁(略带色调的浅灰色)和塑料(深灰色)的反射值。查看本章结论以获取参考列表。
-
使用白色文本元素为电池主体添加品牌、尺寸和正负指示器。然后,隐藏辅助线图层:
![如何操作...]()
-
选择所有图层并将它们组织到一个组中(在 Photoshop 中,可以通过点击图层窗口中的下拉菜单并导航到窗口 | 从图层新建组…)来做到这一点。将新组命名为`高光度**:
![如何操作...]()
-
复制
高光度**组**(在**图层**窗口中,右键单击组名并选择**复制组…**)。将复制的组命名为平滑度**。 -
隐藏
平滑度组。然后,展开`高光度组并隐藏所有文本图层:![如何操作...]()
-
显示
平滑度组,并隐藏`高光度组。选择深灰色图层。然后,在电池主体的上部区域周围创建一个区域选择,并用浅灰色(R: 220,G: 220,B: 220)填充。如有必要,重新调整和排列文本图层:![如何操作...]()
-
复制包含图像上部区域灰色填充的图层(覆盖圆圈的图层)。
-
要给这个材质添加刷过的质感,向复制的图层添加噪声滤镜(在 Photoshop 中,可以通过导航到滤镜 | 噪声 | 添加噪声...)来实现)。将数量设置为50%,并将单色设置为
true。然后,使用30 像素作为距离应用运动模糊滤镜(滤镜 | 模糊 | 运动模糊...)。 -
复制
平滑度组。然后,选择复制的组并将其合并为单个图层(在图层窗口中,右键单击组名并选择合并组)。 -
选择合并层,使用CTRL + A键组合选择整个图像,并使用CTRL + C键复制它:
![如何操作...]()
-
隐藏合并层和
平滑度组。然后,取消隐藏镜面反射组。 -
在您的图像编辑器中,访问图像通道窗口(在 Photoshop 中,可以通过导航到窗口 | 通道)。创建一个新通道。这将是我们Alpha通道。
-
将您之前复制的图像(从合并层)粘贴到Alpha通道中。然后,将所有通道设置为
可见:![如何操作...]()
-
将您的图像保存到项目的资产文件夹中,命名为
Battery_specular,可以是 Photoshop 格式(PSD)或 TIF 格式。 -
现在,让我们处理反照率图。将
Battery_specular的副本保存为Battery_albedo。然后,从通道窗口,删除Alpha通道。 -
从图层窗口,隐藏
平滑度副本合并层,取消隐藏平滑度组。最后,展开平滑度组,并隐藏应用了噪点滤镜的图层:![如何操作...]()
-
将上方的矩形颜色更改为黑色。然后,将浅灰色区域更改为深红色(R: 204, G: 0, B: 0),将深灰色更改为红色(R: 255, G: 0, B: 0)。将组重命名为
Albedo并保存文件:![如何操作...]()
-
返回 Unity,确保两个文件都已导入。然后,从项目视图,创建一个新的材质。将其命名为
Battery_MAT。小贴士
创建新材质的一个简单方法是访问项目视图,点击创建下拉菜单,然后选择材质。
-
选择
Battery_MAT。从检查器视图,将着色器更改为标准(镜面反射设置),并确保渲染模式设置为不透明。 -
将
Battery_specular设置为镜面反射图,将Battery_albedo设置为Battery_MAT的反照率图。 -
将
Battery_MAT材质从项目视图拖动到层次结构视图中的电池对象:![如何操作...]()
-
将
Battery_MAT材质从项目视图拖动,在层次结构视图中将其拖放到电池对象:![如何操作...]()
它是如何工作的...
最终,电池的视觉外观是其材质的三个属性的组合:镜面反射、平滑度和反照率。
例如,为了组成塑料体的深红色部分,我们混合了以下内容:
-
镜面反射图(RGB):非常暗的灰色镜面反射(表示非金属外观)
-
平滑度(镜面反射图的 Alpha 通道):浅灰色(表示光泽外观)
-
反照率图:深红色(表示深红色)
另一方面,浅红色部分结合了以下内容:
-
镜面反射图(RGB):相同的暗灰色镜面反射
-
平滑度(镜面反射图的 Alpha 通道):深灰色(表示哑光外观)
-
Albedo图:红色(用于红色)
最后,用于顶部和底部盖子的刷漆金属结合了以下特点:
-
Specular图(RGB):浅灰色(用于金属效果)
-
平滑度(镜面图的 Alpha 通道):一种模糊的灰色噪声图案(用于刷漆效果)
-
Albedo图:黑色(用于红色)
关于图像层的结构,将层组织成以它们相关的属性命名的组是个好习惯。随着纹理图的多样化,保留一个包含所有图的文件以供快速参考和一致性是个好主意。
更多内容...
在使用 Albedo 图时,您应该考虑以下几点。
设置图像文件的纹理类型
由于图像文件可以在 Unity 中用于多种目的(纹理图、GUI 纹理、光标等),检查是否已为您的文件分配了正确的纹理类型是个好主意。这可以通过在项目视图中选择图像文件,并在检查器视图中使用下拉菜单选择正确的纹理类型(在这种情况下,Texture)来完成。请注意,还可以调整其他设置,例如包裹模式、过滤模式和最大尺寸。最后一个参数如果您想保持纹理图在游戏中的尺寸较小,同时仍然能够以全尺寸编辑它们,非常有用。
将图与颜色结合
在编辑材质时,如果Albedo图槽右侧的检查器视图中没有纹理图,可以使用颜色选择器选择材质的颜色。如果正在使用纹理图,所选颜色将乘以图像,从而在材质的颜色色调上产生变化。
将从镜面设置到金属的基本材料进行适配
为了更好地理解金属和非金属工作流程之间的差异,我们将修改在镜面设置材质上使用的 Albedo 和 Specular/Smoothness 图,以便将它们适配到金属工作流程。要生成的材质将具有金属和非金属部分,以及不同的平滑度级别。
准备工作
对于这个配方,我们准备了一个包含电池模型及其原始材质(使用标准着色器——镜面设置)的 Unity 包。该包包括两个图像文件,用于原始的 Albedo 和 Specular/Smoothness 图,在整个配方中,应将它们适配以用于金属设置。该包位于1362_04_02文件夹中。
如何操作...
要创建基本材质,请按照以下步骤操作:
-
将
battery_prefabUnity 包导入到新项目中。 -
从项目视图中选择battery_prefab元素。然后,从检查器中访问其材质(命名为Battery_MAT)并将其着色器更改为标准(与当前着色器——标准(镜面设置)相反)。
![如何操作...]()
-
从项目视图,找到
Battery_specular贴图并将其重命名为Battery_metallic。在您的图像编辑器中打开它(以下步骤我们将使用 Adobe Photoshop 进行说明)。 -
找到名为Specular的图层组并将其重命名为Metallic。然后,将浅灰色层(在Metallic组中命名为层 2)填充为白色(R:255,G:255,B:255),并将深灰色层(在Metallic组中命名为层 1)填充为黑色(R:0,G:0,B:0)。保存文件:
![如何操作...]()
-
返回 Unity。从检查器视图,将修改后的Battery_metallic贴图设置为Battery_MAT材料的金属贴图。同时,将那个材料的反照率贴图设置为无。这将给您一个关于材料进展的思路:
![如何操作...]()
-
现在,让我们调整反照率纹理贴图。从项目视图,定位
Battery_albedo贴图并在您的图像编辑器中打开它。然后,使用油漆桶工具将反照率组中层 2的黑色区域填充为浅灰色(R:196,G:199,B:199)。保存文件:![如何操作...]()
-
返回 Unity。从检查器视图,将修改后的Battery_albedo贴图设置为Battery_MAT材料的反照率贴图。
您的材料已经准备好,结合了您编辑和分配的不同贴图所基于的视觉属性。

它是如何工作的...
电池的视觉外观是其材料三个属性的组合:金属、光滑度和反照率。
例如,要组成塑料体的深红色部分,我们混合了以下成分:
-
金属贴图(RGB):黑色(用于非金属外观)
-
光滑度(金属贴图的 Alpha 通道):浅灰色(用于光泽外观)
-
反照率贴图:深红色(用于深红色)
另一方面,浅红色部分结合了以下成分:
-
金属贴图(RGB):黑色
-
光滑度(金属贴图的 Alpha 通道):深灰色(用于哑光外观)
-
反照率贴图:红色(用于红色)
最后,用于顶部和底部盖子的刷漆金属结合了以下成分:
-
金属贴图(RGB):白色(用于金属外观)
-
光滑度(金属贴图的 Alpha 通道):模糊的灰色噪点图案(用于刷漆外观)
-
反照率贴图:浅灰色(用于类似铁的外观)
记得将图层组织成以它们相关的属性命名的组。
将法线贴图应用于材料
法线图通常用于模拟在游戏运行时用 3D 多边形实际表示过于昂贵的复杂几何形状。简单来说,法线图在低分辨率 3D 网格上伪造复杂几何形状。这些图可以通过将高分辨率 3D 网格投影到低多边形网格上(通常称为烘焙技术)生成,或者,如本菜谱所示,从另一个纹理图中生成。
准备工作
对于这道菜谱,我们将准备两个纹理贴图:高度图和法线图。前者将使用图像编辑器中的简单形状制作。后者将自动从高度图中处理。尽管有许多工具可以用来生成法线图(参见本章的更多内容部分以获取资源列表),但我们将使用一个免费在线工具,兼容 Windows 和 Mac,来生成我们的纹理。由 Christian Petry 开发,NormalMap Online功能可以在cpetry.github.io/NormalMap-Online/访问。
为了帮助您制作这道菜谱,已经提供了一个 Unity 包,其中包含由 3D 对象及其材质组成的预制件;还有一个 UVW 模板纹理(PNG 格式),用于指导您在创建漫反射纹理贴图时的操作。所有文件都位于1362_04_03文件夹中。
如何操作...
要将法线图应用于材质,请按照以下步骤操作:
-
将
1362_04_03.unitypackage文件导入到您的项目中。在项目视图中,从资产 |1362_04_03文件夹中选择batteryPrefab对象。在比较了一些参考照片后,了解应该通过法线图复制的特征:(A)顶部有一个凹凸的环;以及(B)底部有一些圆形的褶皱,如图所示:![如何操作...]()
-
在图像编辑器中打开
uvw_template.png。创建一个新图层,用灰色(RGB:128)填充,并将其放置在现有图层下方,如图所示:![如何操作...]()
-
在一个单独的图层上,以电池顶部为中心绘制一个白色圆圈。然后,在另一个图层上,以电池底部为中心绘制一个黑色圆圈,如下所示:
![如何操作...]()
-
如果您已经使用矢量形状制作了圆圈,请将它们的图层进行光栅化(在 Adobe Photoshop 中,右键单击图层名称,从上下文菜单中选择光栅化图层选项)。
-
在 Photoshop 中,通过导航到滤镜 | 模糊 | 高斯模糊...来模糊白色圆圈。使用4,0像素作为半径。
-
隐藏 UVW 模板图层,并将图像保存为
Battery_height.png。 -
如果您想直接从 Unity 转换 Heightmap,将其导入到您的项目中。从项目视图中选择它,然后从检查器视图中,将其纹理类型更改为法线图。勾选从灰度创建选项,根据需要调整凹凸度和过滤,然后点击应用以保存更改:
![如何操作...]()
-
要外部转换您的 Heightmap,访问网站
cpetry.github.io/NormalMap-Online/。然后,将HEIGHT_battery.png文件拖到相应的图像槽中。您可以随意调整强度、级别和模糊/锐化参数:![如何操作...]()
-
将生成的法线图保存为
Battery_normal.jpg并将其添加到您的 Unity 项目中。 -
在 Unity 中,从项目视图中选择
Battery_normal。然后,从检查器视图中,将其纹理类型更改为正常,并确保从灰度创建框未被勾选。点击应用以保存更改:![如何操作...]()
-
在项目视图中选择
batteryPrefab。然后,在检查器视图中,滚动到材质组件,并将Battery_normal分配给法线图槽。要调整其强度和方向,将其值更改为-0.35:![如何操作...]()
它是如何工作的...
法线图是从高度图上的灰度值计算得出的,其中较亮的色调被解释为凹槽(应用于电池顶部),较暗的色调被解释为凸起(应用于电池底部)。由于所需的输出实际上是相反的,因此需要将法线图调整到负值(-0.35)。解决此问题的另一个可能方案是重新绘制高度图并交换白色和黑色圆圈的颜色。
还有更多...
如果您想探索 Normal mapping 超出 NormalMap Online 的限制,有一个不断增长的软件列表,可以生成法线图(以及更多)。以下是一些您可能想要查看的资源:
-
CrazyBump 是适用于 Windows 和 Mac 的独立工具,可在
www.crazybump.com获取。 -
nDo 是 Quixel 的 Photoshop 插件(仅限 Windows),可在
quixel.se/ndo获取。 -
GIMP normalmap 插件,仅适用于 Windows,可在
code.google.com/p/gimp-normalmap/获取。 -
NVIDIA Texture Tools for Adobe Photoshop,仅适用于 Windows,可在
developer.nvidia.com/nvidia-texture-tools-adobe-photoshop获取。 -
Bitmap2Material 是来自 Allegorithmic 的一个惊人的纹理生成工具,可在
www.allegorithmic.com/获取。
向材质添加透明度和发射图
Emission 属性可以用来模拟各种自发光物体,从移动显示器的 LED 到未来派的 Tron 服装。另一方面,透明度可以使材料的漫反射颜色更明显或更不明显。在这个配方中,你将学习如何配置这些属性来制作一个具有塑料外壳和发光文字的玩具纸盒包装。
准备工作
对于这个配方,我们准备了一个包含由 3D 对象、其材质及其相应的漫反射纹理图(PNG 格式)组成的预制件的 Unity 包。所有文件都在 1362_04_04 文件夹中。
如何操作...
要向材质添加透明度和颜色发射,请按照以下步骤操作:
-
将
TransparencyEmission.unitypackage导入到你的项目中。从 Assets 文件夹中,在 Project 视图中选择DIFF_package纹理。然后,在您的图像编辑器中打开它。 -
首先,我们将通过删除包装(和悬挂孔)周围的白色区域来为图像添加透明度。选择这些区域(在 Photoshop 中,可以使用 Magic Wand 工具完成)。
-
确保通过单击图层名称左侧的锁图标解锁 Background 层,如下所示:
![如何操作...]()
-
删除之前所做的选择(在 Photoshop 中可以通过按 Delete 键完成)。图像的背景应该是透明的,如下所示。保存文件:
![如何操作...]()
-
回到 Unity,在 Assets 文件夹中,展开 packagePrefab 列表并选择 PackageCard 对象。现在,在 Inspector 视图中,向下滚动到 Material 组件,将其 Rendering Mode 更改为 Cutout,并将其 Alpha Cutoff 调整为
0.9:![如何操作...]()
小贴士
选择 Cutout 意味着你的材质可以是完全不可见或完全可见的,不允许半透明。Alpha Cutoff 用于去除透明边缘周围的不想要的像素。
-
从扩展的 packagePrefab 中选择 PackagePlastic 对象。在 Inspector 视图中,向下滚动到 Material 组件,将其 Rendering Mode 更改为 Transparent。然后,使用 Diffuse 颜色选择器将颜色的 RGB 值更改为
56,并将 Alpha 更改为25。此外,将 Smoothness 级别更改为0.9:![如何操作...]()
-
现在我们已经处理好了透明度需求,我们需要处理 Emission 纹理。从 Assets 文件夹中,复制
DIFF_package纹理,重命名为EMI_package,并在您的图像编辑器中打开它。 -
选择 Ms. Laser 标记和绿色星星上的所有字符(在 Photoshop 中,可以通过使用带有按住 Shift 键的多区域选择的魔棒工具来完成)。
-
将你的选择复制并粘贴到一个新图层中。然后,选择它并应用一个噪声滤镜(在 Photoshop 中,可以通过导航到滤镜 | 噪声 | 添加噪声...)来执行此操作。使用50%作为值。
-
创建一个新图层,并使用如油漆桶工具之类的工具将其填充为黑色(R:
0,G:0,B:0)。将这个黑色图层放置在带有彩色元素的图层下方。 -
扁平化你的图像(在 Photoshop 中,可以通过导航到图层 | 合并图层)并保存你的文件:
![如何操作...]()
-
回到 Unity 中,在资产文件夹中,展开packagePrefab并选择PackageCard对象。现在,在检查器视图中,向下滚动到材质组件并将
EMI_package纹理分配给其发射槽。然后,将发射颜色槽更改为白色(R:255,G:255,B:255),并将强度降低到0.25,如以下截图所示。同时,将其全局光照选项更改为无,这样它的光芒就不会添加到光照贴图中或影响实时照明:![如何操作...]()
-
在你的场景中放置一个packagePrefab实例,并查看结果。你的材质已经准备好了:
![如何操作...]()
它是如何工作的...
Unity 能够读取纹理贴图的四个通道:R(红色)、G(绿色)、B(蓝色)和 A(Alpha)。当设置为透明或剪裁时,漫反射纹理贴图的 Alpha 通道根据每个像素的亮度级别设置材质的透明度(剪裁模式不会渲染半透明——只有完全可见或不可见的像素)。你可能已经注意到我们没有添加 Alpha 通道——这是因为 Photoshop 根据其透明度导出 PNG 的 Alpha 贴图。为了帮助你可视化 Alpha 贴图,1362_04_04文件夹中包含一个DIFF_packageFinal.TIF文件,它包含一个 Alpha 贴图,其工作方式与我们所生成的 PNG 文件完全相同:

关于发射纹理贴图,Unity 将其 RGB 颜色分配给材质,与适当的选择颜色槽结合,并允许调整该发射的强度。
更多内容...
让我们看看关于透明度和发射的更多信息。
使用透明模式的纹理贴图
请注意,你可以在透明渲染模式下使用位图纹理作为漫反射贴图。在这种情况下,RGB 值将被解释为漫反射颜色,而 Alpha 值将用于确定该像素的透明度(在这种情况下,允许半透明材质)。
避免半透明对象的问题
您可能已经注意到塑料外壳是由两个对象(PackagePlastic和innerPlastic)组成的。这样做是为了避免 z 排序问题,即当面应该在后面时,却渲染在了其他几何体前面。使用多个网格而不是单个网格可以正确排序这些面以便渲染。在Cutout模式下的材质不受此问题影响。
在其他物体上发射光线
当使用 Lightmaps 时,可以使用发射值来计算材料在其它物体上的光照投影。
鼠标悬停时突出显示材料
在运行时更改物体的颜色可以是一个非常有效的方法,让玩家知道他们可以与之交互。这在许多游戏类型中非常有用,例如益智游戏和点击冒险游戏,也可以用来创建 3D 用户界面。
准备工作
对于这个配方,我们将使用直接在 Unity 中创建的对象。或者,您可以使用您喜欢的任何 3D 模型。
如何操作...
要在鼠标悬停时突出显示材料,请按照以下步骤操作:
-
创建一个新的 3D 项目,并将一个Cube添加到场景中(从层次结构视图,导航到创建 | 3D 对象 | Cube)。
-
从项目视图,点击创建下拉菜单并选择材质。将其命名为
HighlightMaterial。 -
选择HighlightMaterial,从检查器视图中将其Albedo颜色更改为灰色(R:
135,G:135,B:135),其发射强度设置为1,如以下截图所示,并将发射颜色设置为 R:1,G:1,B:1:![如何操作...]()
-
将HighlightMaterial分配给您之前创建的Cube。
-
从项目视图,点击创建下拉菜单并选择C# 脚本。将其重命名为
HighlightObject并在您的编辑器中打开它。 -
将以下代码替换所有内容:
using UnityEngine; using System.Collections; public class HighlightObject : MonoBehaviour{ private Color initialColor; public bool noEmissionAtStart = true; public Color highlightColor = Color.red; public Color mousedownColor = Color.green; private bool mouseon = false; private Renderer myRenderer; void Start() { myRenderer = GetComponent<Renderer>(); if (noEmissionAtStart) initialColor = Color.black; else initialColor = myRenderer.material.GetColor("_EmissionColor"); } void OnMouseEnter(){ mouseon = true; myRenderer.material.SetColor("_EmissionColor", highlightColor); } void OnMouseExit(){ mouseon = false; myRenderer.material.SetColor("_EmissionColor",initialColor); } void OnMouseDown(){ myRenderer.material.SetColor("_EmissionColor", mousedownColor); } void OnMouseUp(){ if (mouseon) myRenderer.material.SetColor("_EmissionColor", highlightColor); else myRenderer.material.SetColor("_EmissionColor", initialColor); } } -
保存您的脚本并将其附加到Cube。
-
选择Cube,在检查器视图中设置您想要的突出显示颜色和鼠标按下颜色值:
![如何操作...]()
-
如果您使用的是自己的导入的 3D 网格,请确保为您的对象添加一个Collider组件。
-
测试场景。当鼠标悬停在Cube上时,它将被突出显示为红色(点击时为绿色)。
它是如何工作的...
当用户将鼠标指针移动到屏幕上立方体可见的部分上方和移开时,立方体会自动接收到鼠标的进入/退出/按下/抬起事件。当检测到这些事件时,我们的脚本会给立方体添加一个行为。Start() 方法获取脚本已添加到 GameObject 的 Renderer 组件的引用,并将其存储在变量 myRenderer 中(注意,在 Unity 中 'renderer' 已经有了一个含义,因此不适合作为此脚本的私有变量名)。布尔变量 mouseon 记录鼠标指针是否当前位于对象上。当鼠标按钮释放时,我们使用 mouseon 变量来决定是否将立方体变回其初始颜色(mouseon 为 FALSE,表示鼠标指针远离立方体),或者变回其高亮颜色(mouseon 为 TRUE,表示鼠标指针位于立方体上)。
我们需要将材质的原始 Emission 颜色更改为超深灰色,原因在于如果保持黑色,Unity 会通过从材质中移除 Emission 属性来优化 Shader。如果发生这种情况,我们的脚本将无法工作。
更多内容...
通过更改材质的其他属性(例如,将 _EmissionColor 脚本更改为 _Color 或 `"_SpecularColor"),你可以实现其他有趣的结果。要查看属性的全列表,选择你的材质,然后在 Inspector 视图中,点击 Shader 下拉菜单旁边的 Edit 按钮。
将细节贴图添加到材质中
当创建一个大型对象时,不仅希望整体对其进行纹理处理,还希望添加可以在近距离看起来更好的细节。为了克服对巨大纹理贴图的需求,使用细节贴图可以真正地产生影响。在这个配方中,我们将通过应用细节蒙版和细节法线贴图来为火箭玩具添加细节贴图。在我们的例子中,我们希望为绿色塑料添加纹理质感(和条纹图案),除了电池仓和玩具标志的区域:

准备工作
对于这个配方,我们准备了一个 Unity 包,其中包含一个火箭玩具的预制件。预制件包括 3D 模型和材质,具有漫反射贴图和法线贴图(由高度图生成)。该文件位于 1362_04_06 文件夹中。
如何操作...
要将细节贴图添加到你的对象中,请按照以下步骤操作:
-
将
rocket.unitypackage文件导入到你的项目中。然后,在 Project 视图中的 Assets 文件夹中,选择名为rocketToy的预制件,并将其放置到你的场景中。 -
从层次视图中展开rocketToy GameObject,并选择其名为rocketLevel1的子项。然后,在检查器视图中向下滚动到材质组件。观察它使用
DIFF_ship纹理作为漫反射贴图。复制此文件,并将新副本重命名为COPY_ship。 -
在你的图像编辑器中打开
COPY_ship。选择围绕标志和电池仓周围的实心绿色像素(在 Photoshop 中,这可以通过按下Shift键同时选择多个区域使用魔棒工具完成):![如何操作...]()
-
保持选择活动状态,访问图像通道窗口(在 Photoshop 中,这可以通过导航到窗口 | 通道完成)。点击新建通道。这将是我们Alpha通道:
![如何操作...]()
-
隐藏红色、绿色和蓝色通道。选择Alpha通道,并将选择区域涂成白色。然后,选择电池仓区域,将其涂成灰色(R、G 和 B:
100):![如何操作...]()
-
将其保存为 TIFF 格式,文件名为
MASK_ship.TIF,在Assets文件夹中。确保包含Alpha 通道:![如何操作...]()
-
现在我们有了面具,让我们为我们的细节创建一个漫反射图。在你的图像编辑器中,创建一个具有以下尺寸的新图像:宽度:
64,和高度:64。 -
将新图像填充为灰色(R、G 和 B:
128)。然后,使用形状或矩形填充创建一个深灰色(R、G 和 B:100)的水平线,高度约为 16 像素:![如何操作...]()
-
将图像保存为
DIFF_detail.PNG在Assets文件夹中。 -
创建一个新的 64 x 64 图像。使用渐变工具创建一个黑白径向渐变(在 Photoshop 中,这可以通过在径向模式下使用渐变工具完成):
![如何操作...]()
-
将图像保存为
HEIGHT_detail.PNG在Assets文件夹中。 -
返回 Unity。从
Assets文件夹中,选择HEIGHT_detail。然后,从检查器视图中,更改其纹理类型为法线贴图,勾选从灰度创建选项,调整凹凸度到0.25,并将过滤设置为平滑。点击应用以保存更改:![如何操作...]()
-
从层次视图中展开rocketToy GameObject,并选择其名为rocketLevel1的子项。然后,在检查器视图中向下滚动到材质组件。将
MASK_ship分配给细节遮罩槽位;将DIFF_detail作为次级贴图 | 细节漫反射 x 2;将HEIGHT_detail作为次级贴图 | 法线贴图。同时,将法线贴图强度降低到0.6。 -
在次级贴图部分,将平铺值更改为X:
200,和Y:50。您可能会注意到图案不是无缝的。这是因为我们正在使用与漫反射纹理相同的UV 集。然而,物体已被分配到两个不同的UV 通道(在建模时)。虽然 UV 通道 1 包含我们的漫反射贴图的映射,但 UV 通道 2 使用基本的圆柱映射。我们需要将次级贴图部分的UV 集从UV0更改为UV1。您的材质的细节贴图已准备好:![如何操作...]()
它是如何工作的...
在使用时,次级贴图会被混合到材质的漫反射和法线主贴图上(这就是为什么即使应用了细节漫反射,我们的物体仍然是绿色的:灰色色调叠加在原始的漫反射纹理上)。通过使用细节遮罩,艺术家可以定义物体的哪些区域应该受到次级贴图的影响。这对于定制来说非常好,也可以用来创建细微差别(如我们示例中的半凸电池仓)。
另一个有用的功能是使用单独的 UV 通道来处理细节贴图和平铺。除了为纹理映射增加变化外,这还允许我们绘制即使在非常近的距离也能感知到的细节,从而显著提高我们物体的视觉质量。
淡入材质的透明度
在这个菜谱中,我们将创建一个对象,一旦点击,就会淡出并消失。但是,脚本将足够灵活,允许我们调整初始和最终的不透明度值。此外,我们还将有选项使物体在不可见时自我销毁。
如何操作...
按照以下步骤操作:
-
通过访问游戏对象 | 3D 对象 | 球体菜单将球体添加到您的场景中。
-
选择球体并确保它有一个碰撞器(如果您正在使用自定义 3D 对象,您可能需要通过组件 | 物理菜单添加碰撞器)。
-
创建一个新的材质。最简单的方法是访问项目视图,点击创建下拉菜单,然后选择材质。
-
重命名您的新材质。在这个例子中,让我们称它为
Fade_MAT。 -
选择您的材质。从检查器视图,使用下拉菜单将其渲染模式更改为淡入:
![如何操作...]()
小贴士
淡入渲染模式专门设计用于这种情况。其他渲染模式,如透明,会淡入使 Albedo 颜色透明,但不会影响高光或反射,在这种情况下,物体仍然可见。
-
将FadeMaterial应用到球体上,通过从项目视图将其拖动到层次结构视图中的球体游戏对象名称。
-
从项目视图,点击创建下拉菜单并选择C# 脚本。将其重命名为
FadeMaterial并在您的编辑器中打开它。 -
用以下代码替换你的脚本:
using UnityEngine; using System.Collections; public class FadeMaterial : MonoBehaviour { public float fadeDuration = 1.0f; public bool useMaterialAlpha = false; public float alphaStart = 1.0f; public float alphaEnd = 0.0f; public bool destroyInvisibleObject = true; private bool isFading = false; private float alphaDiff; private float startTime; private Renderer rend; private Color fadeColor; void Start () { rend = GetComponent<Renderer>(); fadeColor = rend.material.color; if (!useMaterialAlpha) { fadeColor.a = alphaStart; } else { alphaStart = fadeColor.a; } rend.material.color = fadeColor; alphaDiff = alphaStart - alphaEnd; } void Update () { if(isFading){ var elapsedTime = Time.time - startTime; if(elapsedTime <= fadeDuration){ var fadeProgress = elapsedTime / fadeDuration; var alphaChange = fadeProgress * alphaDiff; fadeColor.a = alphaStart - alphaChange; rend.material.color = fadeColor; } else { fadeColor.a = alphaEnd; rend.material.color = fadeColor; if(destroyInvisibleObject) Destroy (gameObject); isFading = false; } } } void OnMouseUp(){ FadeAlpha(); } public void FadeAlpha(){ isFading = true; startTime = Time.time; } } -
保存你的脚本并将其应用到球体游戏上。
-
播放你的场景并点击球体以查看它淡出并自我销毁。
它是如何工作的...
由于使用透明着色器时材料的透明度由其主要颜色的 alpha 值决定,因此我们为了淡入淡出只需要改变这个值在一定时间内。这种转换在我们的脚本中以下代码行中表示:
var fadeProgress = elapsedTime / fadeDuration;
var alphaChange = fadeProgress * alphaDiff;
fadeColor.a = alphaStart - alphaChange;
rend.material.color = fadeColor;
还有更多...
你可以在其他情况下调用FadeAlpha函数(例如,在Rigidbody碰撞时)。实际上,你甚至可以通过使用GetComponent命令从另一个游戏对象的脚本中调用它。脚本可能如下所示:
GameObject.Find("Sphere").GetComponent<FadeMaterial>().FadeAlpha();
在场景中播放视频
电视、投影仪、显示器……如果你想在你的级别中添加复杂动画材质,你可以播放视频文件作为纹理图。在本教程中,我们将学习如何将视频纹理应用到立方体上。我们还将实现一个简单的控制方案,当点击该立方体时播放或暂停视频。
准备工作
Unity 通过 Apple Quicktime 导入视频文件。如果你没有安装在你的机器上,请从www.apple.com/quicktime/download/下载它。
此外,如果你需要视频文件来遵循此教程,请使用文件夹1632_04_08中包含的videoTexture.mov。
如何操作...
按照以下步骤操作:
-
通过游戏对象 | 3D 对象 | 立方体菜单将一个立方体添加到场景中。
-
导入提供的
videoTexture.mov文件。 -
从项目视图,使用创建下拉菜单创建一个新的材质。将其重命名为
Video_MAT,并在检查器视图中将其着色器更改为Unlit/Texture:![如何操作...]()
-
通过从项目视图拖动到适当的槽位,将
videoTexture应用到Video_MAT的纹理槽位。 -
将
Video_MAT应用到之前创建的立方体上。 -
在项目视图中展开
videoTexture以显示其对应的音频剪辑。然后,将那个音频剪辑应用到立方体上(你可以通过从项目视图拖动它到层次结构视图中的立方体,或者场景视图中来完成)。![如何操作...]()
-
选择立方体。确保从检查器视图中可以看到一个可见的Collider组件。如果没有,可以通过组件 | 物理 | 箱体碰撞器菜单添加它。碰撞器用于鼠标碰撞检测。
-
现在我们需要创建一个控制电影纹理和相关音频剪辑的脚本。从项目视图,使用创建下拉菜单添加一个C#脚本。将其命名为
PlayVideo。 -
打开脚本并将其替换为以下代码:
using UnityEngine; using System.Collections; [RequireComponent(typeof(AudioSource))] public class PlayVideo : MonoBehaviour { public bool loop = true; public bool playFromStart = true; public MovieTexture video; public AudioClip audioClip; private AudioSource audio; void Start () { audio = GetComponent<AudioSource> (); if (!video) video = GetComponent<Renderer>().material.mainTexture as MovieTexture; if (!audioClip) audioClip = audio.clip; video.Stop (); audio.Stop (); video.loop = loop; audio.loop = loop; if(playFromStart) ControlMovie(); } void OnMouseUp(){ ControlMovie(); } public void ControlMovie(){ if(video.isPlaying){ video.Pause(); audio.Pause(); } else { video.Play(); audio.Play(); } } } -
保存你的脚本并将其附加到立方体上。
-
测试你的场景。你应该能够看到在立方体面上播放的电影,并且通过点击它来暂停/播放。
它是如何工作的...
默认情况下,我们的脚本使电影纹理以循环模式播放。然而,有一个布尔变量可以通过检查器面板进行更改,在那里它由一个复选框表示。同样,还有一个复选框可以用来防止在关卡开始时播放电影。
还有更多...
还有一些其他电影纹理命令和参数可以进行调整。别忘了查看 Unity 的脚本指南:docs.unity3d.com/Documentation/ScriptReference/MovieTexture.html。
结论
本章介绍了用于创建纹理图的技术,这些纹理图通常手动创建,有时自动创建,能够为材料提供独特的特征。希望您现在对使用 Unity 的新基于物理着色更有信心,它能够理解不同工作流程之间的差异,了解每个材料属性的作用,并准备好为您的游戏制作更美观的材料。我们还探讨了通过脚本访问对象的材质来在运行时更改材料属性的方法。
资源
基于物理渲染是一个复杂(且当前)的话题,因此熟悉其背后的工具和概念,对其进行一些研究是个好主意。为了帮助您完成这项任务,我们下面提供了一份非详尽的资源列表,您应该查看一下。
参考文献
这里有一份关于基于物理渲染(Unity 内部和外部)的有趣、详细的材料列表:
-
《全面 PBR 指南第 1 卷和第 2 卷》,由 Wes McDermott(Allegorithmic)撰写,可在
www.allegorithmic.com/pbr-guide找到。本指南深入探讨了 PBR 的实践和理论方面,包括对可能的工作流程的精彩分析。 -
Unity 5 中掌握基于物理着色,由 Renaldas Zioma(Unity)、Erland Körner(Unity)和 Wes McDermott(Allegorithmic)合著,可在
www.slideshare.net/RenaldasZioma/unite2014-mastering-physically-based-shading-in-unity-5找到。这是一份关于在 Unity 中使用 PBS 的详细演示文稿。最初在 Unite 2014 会议上展示,其中包含一些过时的信息,但无论如何,它仍然值得一看。 -
Unity 5 中的基于物理着色,由 Unity 的 Aras Pranckevičius 撰写,可在
aras-p.info/texts/talks.html找到。关于该主题的演示文稿的幻灯片和笔记在 GDC 上提供。 -
Joe "EarthQuake" Wilson 的教程《基于物理渲染,你也可以做到!》可在
www.marmoset.co/toolbag/learn/pbr-practice找到。这是来自Marmoset Toolbag和Skyshop制作团队的精彩概述。 -
Polycount PBR Wiki,可在
wiki.polycount.com/wiki/PBR找到,是由 Polycount 社区整理的资源列表。
工具
这是一代新的纹理软件,供您查看,以防您还没有看过:
-
Substance Painter 是来自 Allegorithmic 的 3D 绘画应用程序。它可在
www.allegorithmic.com/products/substance-painter找到。再次值得一提的是,Allegorithmic 的产品不会使用 Unity 的标准着色器,而是依赖 Unity 原生支持的 substance 文件。 -
Bitmap2Material 可以从单个位图图像创建全功能的材质(包括法线图、高光图等)。此外,它也来自 Allegorithmic,可在
www.allegorithmic.com/products/bitmap2material找到。 -
Quixel DDO 是一个用于在 Adobe Photoshop 中创建 PBR 准备纹理的插件。从 Quixel 获取,可在
www.quixel.se/ddo找到。 -
Quixel NDO 是一个用于在 Adobe Photoshop 中创建法线图的插件。从 Quixel 获取,可在
www.quixel.se/ndo找到。 -
Mari 是来自 The Foundry 的 3D 绘画工具。它可在
www.thefoundry.co.uk/products/mari/找到。
第五章。使用相机
在本章中,我们将涵盖:
-
创建画中画效果
-
在多个相机之间切换
-
从屏幕内容创建纹理
-
调整望远镜相机的缩放
-
显示迷你地图
-
创建游戏中的监控相机
简介
作为开发者,我们永远不应该忘记关注相机。毕竟,它们是我们玩家看到我们游戏窗口。在本章中,我们将探讨一些有趣的使用相机的方法,这些方法可以增强玩家的体验。
整体情况
相机可以通过多种方式自定义:
-
它们可以排除特定图层上的对象以进行渲染
-
它们可以被设置为以正交模式(即没有透视)进行渲染
-
它们的视野(FOV)可以被操作以模拟广角镜头
-
它们可以渲染在其他相机之上或屏幕的特定区域
-
它们可以被渲染到纹理上
列表还在继续。

两个同时的相机视图
注意,在本章中,您将注意到一些配方具有一个跟随玩家第三人称角色的相机装置。这个装置是多功能相机装置,最初来自 Unity 的示例资产,可以通过导航到资产 | 导入包 | 相机将其导入到项目中。为了使事情更容易,我们将包含它的 MultipurposeCamera Unity 包作为一个预制件组织起来,该预制件可以在 1362_05_codes 文件夹中找到。
创建画中画效果
在许多情况下,显示多个视口可能很有用。例如,您可能希望显示在不同位置同时发生的事件,或者您可能希望为热座多人游戏设置一个单独的窗口。虽然您可以通过调整相机上的归一化视口矩形参数手动完成此操作,但此配方包含一系列额外的首选项,使其更多地独立于用户的显示配置。
准备工作
对于这个配方,我们已准备了 BasicScene Unity 包,其中包含一个名为 BasicScene 的场景。该包位于 1362_05_codes 文件夹中。
如何操作...
要创建画中画显示,只需按照以下步骤操作:
-
将
BasicScene包导入到您的 Unity 项目中。 -
从项目视图打开BasicScene级别。这是一个包含动画角色和一些额外几何形状的基本场景。
-
通过在层次视图顶部的创建下拉菜单中添加一个新的相机(创建 | 相机)来将场景中的新相机。
-
选择您创建的相机,从检查器视图,将其深度更改为1,如图下截图所示:
![如何操作...]()
-
从项目视图创建一个新的C# 脚本文件,并将其重命名为
PictureInPicture。 -
打开您的脚本,并用以下代码替换所有内容:
using UnityEngine; public class PictureInPicture: MonoBehaviour { public enum hAlignment{left, center, right}; public enum vAlignment{top, middle, bottom}; public hAlignment horAlign = hAlignment.left; public vAlignment verAlign = vAlignment.top; public enum UnitsIn{pixels, screen_percentage}; public UnitsIn unit = UnitsIn.pixels; public int width = 50; public int height= 50; public int xOffset = 0; public int yOffset = 0; public bool update = true; private int hsize, vsize, hloc, vloc; void Start (){ AdjustCamera (); } void Update (){ if(update) AdjustCamera (); } void AdjustCamera(){ int sw = Screen.width; int sh = Screen.height; float swPercent = sw * 0.01f; float shPercent = sh * 0.01f; float xOffPercent = xOffset * swPercent; float yOffPercent = yOffset * shPercent; int xOff; int yOff; if(unit == UnitsIn.screen_percentage){ hsize = width * (int)swPercent; vsize = height * (int)shPercent; xOff = (int)xOffPercent; yOff = (int)yOffPercent; } else { hsize = width; vsize = height; xOff = xOffset; yOff = yOffset; } switch (horAlign) { case hAlignment.left: hloc = xOff; break; case hAlignment.right: int justfiedRight = (sw - hsize); hloc = (justfiedRight - xOff); break; case hAlignment.center: float justifiedCenter = (sw * 0.5f) - (hsize * 0.5f); hloc = (int)(justifiedCenter - xOff); break; } switch (verAlign) { case vAlignment.top: int justifiedTop = sh - vsize; vloc = (justifiedTop - (yOff)); break; case vAlignment.bottom: vloc = yOff; break; case vAlignment.middle: float justifiedMiddle = (sh * 0.5f) - (vsize * 0.5f); vloc = (int)(justifiedMiddle - yOff); break; } GetComponent<Camera>().pixelRect = new Rect(hloc,vloc,hsize,vsize); } }注意
如果你没有注意到,我们不是通过将数字除以 100 来获得百分比,而是将它们乘以 0.01。这样做的原因是计算机处理器在乘法上比除法更快。
-
保存你的脚本并将其附加到之前创建的相机上。
-
取消选中新相机的音频监听器组件,并更改一些画中画参数:将水平对齐改为
right,垂直对齐改为top,并将单位改为pixels。将X 偏移和Y 偏移保留为0,将宽度改为400,将高度改为200,如下所示:![如何操作...]()
-
播放你的场景。新的相机的视口应该显示在屏幕的右上角,如下所示:
![如何操作...]()
它是如何工作的...
在这个例子中,我们添加了第二个相机,以便从不同的视角显示场景。第二个相机的相对视口最初放置在主相机视口的上方,因此占据了整个屏幕空间。
PictureInPicture脚本更改相机的标准化视口矩形,从而根据用户的偏好调整视口的大小和位置。
首先,它读取组件的用户偏好设置(PiP 视口的尺寸、对齐和偏移)并将屏幕百分比的尺寸转换为像素。
后来,从if(unit == UnitsIn.screen_percentage){条件语句中,脚本根据用户的选取计算两个视口矩形参数(宽度和高度)。
之后,使用switch语句调整其他两个视口矩形参数(水平和垂直位置)根据总屏幕尺寸、PiP 视口尺寸、垂直/水平对齐和偏移。
最后,一行代码告诉相机更改相机的视口矩形的位置和尺寸:
GetComponent<Camera>().pixelRect = new Rect(hloc,vloc,hsize,vsize);
还有更多...
以下是你可能想要更改的画中画的一些方面:
使画中画与屏幕大小成比例
如果你将单位选项更改为screen_percentage,视口大小将基于实际屏幕的尺寸而不是像素。
更改画中画的位置
垂直对齐和水平对齐选项可以用来更改视口的垂直和水平对齐。使用它们将其放置到你希望的位置。
防止画中画在每一帧更新
如果你计划在运行模式下不更改视口位置,请取消选中更新选项。同时,在测试时保留选中状态,一旦位置确定并设置好,再取消选中。
参见
- 本章中显示迷你地图的配方
在多个相机之间切换
在多种摄像机中选择是许多流派中常见的功能:赛车、体育、大亨/策略,等等。在这个菜谱中,你将学习如何通过使用键盘让玩家能够从许多摄像机中选择。
准备工作
对于这个菜谱,我们准备了一个包含名为BasicScene的场景的BasicScene Unity 包。该包位于1362_05_codes文件夹中。
如何做到这一点...
要实现可切换的摄像机,请按照以下步骤操作:
-
将
BasicScene包导入新的项目。 -
从项目视图,打开BasicScene级别。这是一个包含动画角色和一些额外几何形状的基本场景。
-
通过层次视图顶部的创建下拉菜单(创建 | 摄像机)向场景添加两个更多摄像机。将它们重命名为
cam1和cam2。 -
修改
cam2摄像机的位置和旋转,使其不会与cam1完全相同。 -
通过导航到层次视图顶部的创建下拉菜单(创建 | 创建空对象)创建一个空对象。然后,将其重命名为
Switchboard。 -
从检查器视图,禁用
cam1和cam2的摄像机和音频监听器组件。同时,将它们的标签设置为主摄像机,如图所示:![如何做到这一点...]()
-
从项目视图,创建一个新的C# 脚本文件。将其重命名为
CameraSwitch并在你的编辑器中打开它。 -
打开你的脚本,将所有内容替换为以下代码:
using UnityEngine; public class CameraSwitch : MonoBehaviour { public GameObject[] cameras; public string[] shortcuts; public bool changeAudioListener = true; void Update (){ if (Input.anyKeyDown) { for (int i=0; i<cameras.Length; i++) { if (Input.GetKeyDown (shortcuts [i])) SwitchCamera (i); } } } void SwitchCamera (int indexToSelect){ for (int i = 0; i<cameras.Length; i++){ // test whether current array index matches camera to make active bool cameraActive = (i == indexToSelect); cameras[i].GetComponent<Camera>().enabled = cameraActive; if (changeAudioListener) cameras[i].GetComponent<AudioListener>().enabled = cameraActive; } } } -
将
CameraSwitch附加到Switchboard游戏对象。 -
从检查器视图,将摄像机和快捷方式的大小设置为
3。然后,将场景中的摄像机(包括多功能摄像机装置中的主摄像机 | 枢轴游戏对象)拖放到摄像机插槽中。然后,在快捷方式文本字段中输入1、2和3,如图所示:![如何做到这一点...]()
-
播放你的场景,并通过按键盘上的1、2和3来测试你的摄像机。
它是如何工作的...
脚本非常简单。首先,它将按下的键与快捷方式列表进行比较。如果键确实包含在快捷方式列表中,它将被传递到SwitchCamera函数,该函数随后遍历摄像机列表,启用与接收到的快捷方式关联的摄像机,并启用其音频监听器,如果已勾选更改音频监听器选项。
还有更多...
这里有一些关于你可以如何尝试稍微改变这个菜谱的想法。
使用单启用摄像机
解决这个问题的另一种方法是将所有辅助摄像机保持禁用状态,并通过脚本将它们的位置和旋转分配给主摄像机(如果你想要保存其变换设置,你需要复制主摄像机并将其添加到列表中)。
通过其他事件触发开关
此外,您可以通过使用如下所示的代码行从其他 GameObject 的脚本中更改您的相机:
GameObject.Find("Switchboard").GetComponent("CameraSwitch").SwitchCamera(1);
参见
- 本章中关于创建游戏内监控摄像头的配方
从屏幕内容创建纹理
如果您想让您的游戏或玩家在游戏中捕捉快照并将其作为纹理应用,这个配方将向您展示如何操作。如果您计划实现游戏中的照片库或在关卡结束时显示过去关键时刻的快照(赛车游戏和特技模拟大量使用此功能),这将非常有用。对于这个特定的例子,我们将从屏幕的框架区域内捕捉快照,并将其打印在显示器的右上角。
准备工作
对于这个配方,我们已准备了BasicSceneUnity 包,其中包含一个名为BasicScene的场景。该包位于1362_05_codes文件夹中。
如何操作...
要从屏幕内容创建纹理,请按照以下步骤操作:
-
将
BasicScene包导入一个新的项目。 -
从项目视图,打开BasicScene关卡。这是一个包含动画角色和一些额外几何形状的基本场景。它还包含用于 UI 元素的Canvas。
-
从创建下拉菜单中的层次结构视图(创建 | UI | 图像)创建一个UI 图像GameObject。请注意,它将作为CanvasGameObject 的子项创建。然后,将其重命名为
frame。 -
从检查器视图,找到frameGameObject 的图像(脚本)组件,并将其源图像设置为
InputFieldBackground。这是一个与 Unity 捆绑的精灵,并且已经切片以进行缩放。 -
现在,从检查器视图,将矩形变换更改为以下值:锚点 | 最小值 | X:
0.25,Y:0.25;锚点 | 最大值 | X:0.75,Y:0.75;锚点 | X:0.5,Y:0.5;左:0;上:0;位置 Z:0;右:0;下:0。 -
从图像(脚本)组件中,取消选中填充中心选项,如下所示:
![如何操作...]()
-
从创建下拉菜单中的层次结构视图(创建 | UI | 原始图像)创建一个UI 原始图像GameObject。请注意,它将作为CanvasGameObject 的子项创建。然后,将其重命名为
Photo。 -
从检查器视图,找到PhotoGameObject 的原始图像(脚本)组件,并将其纹理设置为
None。同时,从检查器视图的顶部,通过取消选中其名称旁边的框来禁用PhotoGameObject。 -
现在,从检查器视图,将矩形变换更改为以下值:宽度:
1;高度:1;锚点 | 最小值 | X:0,Y:1;锚点 | 最大值 | X:0,Y:1;锚点 | X:0,Y:1;Pivot | X:0,Y:1;位置 X:0;位置 Y:0;位置 Z:0,如下截图所示:![如何操作...]()
-
我们需要创建一个脚本。在项目视图中,点击创建下拉菜单并选择C# 脚本。将其重命名为
ScreenTexture并在您的编辑器中打开它。 -
打开您的脚本并替换以下代码:
using UnityEngine; using UnityEngine.UI; using System.Collections; public class ScreenTexture : MonoBehaviour { public GameObject photoGUI; public GameObject frameGUI; public float ratio = 0.25f; void Update (){ if (Input.GetKeyUp (KeyCode.Mouse0)) StartCoroutine(CaptureScreen()); } IEnumerator CaptureScreen (){ photoGUI.SetActive (false); int sw = Screen.width; int sh = Screen.height; RectTransform frameTransform = frameGUI.GetComponent<RectTransform> (); Rect framing = frameTransform.rect; Vector2 pivot = frameTransform.pivot; Vector2 origin = frameTransform.anchorMin; origin.x *= sw; origin.y *= sh; float xOffset = pivot.x * framing.width; origin.x += xOffset; float yOffset = pivot.y * framing.height; origin.y += yOffset; framing.x += origin.x; framing.y += origin.y; int textWidth = (int)framing.width; int textHeight = (int)framing.height; Texture2D texture = new Texture2D(textWidth,textHeight); yield return new WaitForEndOfFrame(); texture.ReadPixels(framing, 0, 0); texture.Apply(); photoGUI.SetActive (true); Vector3 photoScale = new Vector3 (framing.width * ratio, framing.height * ratio, 1); photoGUI.GetComponent<RectTransform> ().localScale = photoScale; photoGUI.GetComponent<RawImage>().texture = texture; } } -
保存您的脚本并将其应用到主相机GameObject 中的多功能相机装置 | 枢轴GameObject。
-
在检查器视图中,找到屏幕纹理组件,并将Photo GUI和Frame GUI字段分别填充为Photo和frame GameObject:
![如何操作...]()
-
播放场景。您可以通过单击鼠标按钮来捕获屏幕截图(并将其显示在左上角,大小为原始大小的四分之一),如图下所示:
![如何操作...]()
它是如何工作的...
首先,我们创建了一个 GUI 框架来截图,以及一个 GUI 元素来应用纹理。然后,我们将脚本应用于主相机以捕获屏幕内容并应用新的纹理。
脚本创建了一个新的纹理,并捕获了左鼠标按钮被按下,随后它启动一个协程来计算一个 Rect 区域,从该区域复制屏幕像素,并将它们应用到要由photo GUI 元素显示的纹理上,该元素的大小也调整为适合纹理。
Rect 的大小是根据屏幕的尺寸和框架的Rect Transform设置计算的,特别是其枢轴、锚点、宽度和高度。然后,通过ReadPixels()命令捕获屏幕像素,并将其应用到纹理上,然后将该纹理应用到原始图像photo 上,该 photo 的大小调整为满足照片大小与原始像素之间的期望比例。
更多...
除了将纹理作为 GUI 元素显示外,您还可以以其他方式使用它。
将您的纹理应用到材质上
您可以通过在CaptureScreen函数末尾添加类似GameObject.Find("MyObject").renderer.material.mainTexture = texture;的行,将您的纹理应用到现有对象的材质上。
使用您的纹理作为截图
您可以将您的纹理编码为 PNG 图像文件并保存。请查看 Unity 关于此功能的文档docs.unity3d.com/Documentation/ScriptReference/Texture2D.EncodeToPNG.html。
参见
- 第十章,使用外部资源文件和设备中的游戏截图配方
缩放望远镜相机
在本配方中,我们将创建一个在按下左鼠标按钮时缩放的望远镜相机。这非常有用,例如,如果我们游戏中有一个狙击手。
准备工作...
对于本配方,我们已准备了BasicScene Unity 包,其中包含一个名为BasicScene的场景。该包位于1362_05_codes文件夹中。
如何操作...
要创建一个望远镜相机,请按照以下步骤操作:
-
将
BasicScene包导入一个新的项目。 -
从项目视图中打开BasicScene级别。这是一个包含动画角色和一些额外几何形状的基本场景。
-
通过导航到资产 | 导入包 | Effects导入 Unity 的效果包。
-
在多功能相机装置 | 枢轴游戏对象内选择主摄像机游戏对象,并应用Vignette图像效果(通过导航到组件 | 图像效果 | 摄像机 | Vignette and Chromatic Aberration)。
-
我们需要创建一个脚本。在项目视图中,点击创建下拉菜单并选择C# 脚本。将其重命名为
TelescopicView并在您的编辑器中打开它。 -
打开您的脚本,并用以下代码替换所有内容:
using UnityEngine; using System.Collections; using UnityStandardAssets.ImageEffects; public class TelescopicView : MonoBehaviour{ public float zoom = 2.0f; public float speedIn = 100.0f; public float speedOut = 100.0f; private float initFov; private float currFov; private float minFov; private float addFov; private VignetteAndChromaticAberration v; public float vMax = 10.0f; void Start(){ initFov = Camera.main.fieldOfView; minFov = initFov / zoom; v = this.GetComponent<VignetteAndChromaticAberration>() as VignetteAndChromaticAberration; } void Update(){ if (Input.GetKey(KeyCode.Mouse0)) ZoomView(); else ZoomOut(); float currDistance = currFov - initFov; float totalDistance = minFov - initFov; float vMultiplier = currDistance / totalDistance; float vAmount = vMax * vMultiplier; vAmount = Mathf.Clamp (vAmount,0,vMax); v.intensity = vAmount; } void ZoomView(){ currFov = Camera.main.fieldOfView; addFov = speedIn * Time.deltaTime; if (Mathf.Abs(currFov - minFov) < 0.5f) currFov = minFov; else if (currFov - addFov >= minFov) currFov -= addFov; Camera.main.fieldOfView = currFov; } void ZoomOut(){ currFov = Camera.main.fieldOfView; addFov = speedOut * Time.deltaTime; if (Mathf.Abs(currFov - initFov) < 0.5f) currFov = initFov; else if (currFov + addFov <= initFov) currFov += addFov; Camera.main.fieldOfView = currFov; } } -
保存您的脚本并将其应用到多功能相机装置 | 枢轴游戏对象内的主摄像机游戏对象。
-
播放级别。您应该会看到一个动画的晕影效果以及缩放效果:
![如何做...]()
它是如何工作的...
缩放效果实际上是由摄像机视野(FOV)属性值的改变引起的;较小的值会导致较小区域的更近距离视图,而较大的值会扩大 FOV。
当按下鼠标左键时,TelescopicView脚本会通过从它减去值来改变摄像机的视野。当鼠标按钮没有被按下时,它会将值添加到 FOV 中,直到达到原始值。
FOV 的缩放限制可以从代码minFov = initFov / zoom;中推断出来。这意味着 FOV 的最小值等于其原始值除以缩放量。例如,如果我们的摄像机原本的 FOV 为60,我们将望远镜视图缩放设置为2.0,则允许的最小 FOV 将是60/2 = 30。差异在以下两个屏幕截图中显示:

更多内容...
您还可以添加一个变量来控制Vignette图像效果的模糊晕影级别。
显示迷你地图
在许多游戏中,更宽的视野对于导航和信息非常有价值。迷你地图对于在第一人称或第三人称模式下提供玩家所需的额外视角非常有用。
准备工作...
对于这个食谱,我们准备了BasicScene Unity 包,其中包含一个名为BasicScene的场景。您还需要导入三个名为Compass.png, compassMarker.png和compassMask.png的图像文件。所有文件都位于1362_05_05文件夹中。
如何做...
要创建迷你地图,请按照以下步骤操作:
-
将
BasicScene包导入一个新的项目。同时,导入提供的png文件。打开BasicScene级别。 -
从项目视图中,选择
Compass、compassMarker和compassMask纹理文件。然后,从检查器中,将它们的纹理类型更改为精灵(2D 和 UI),保留精灵模式为单个,并将锚点设置为中心。单击应用以确认更改,如图所示:![如何操作...]()
-
从层次视图中创建一个新的UI 面板对象(创建 | UI | 面板)。它将作为 UI Canvas游戏对象的子对象创建。将其重命名为
MiniMap。然后,从检查器视图中,将其对齐设置为顶部/右侧,将宽度和高度都更改为256,并将其X 位置和Y 位置字段设置为-128。此外,在图像组件中,将compassMask精灵填充到源图像字段中,通过将Alpha值提高到255来调整颜色字段,如图所示:![如何操作...]()
-
向MiniMap添加一个遮罩组件(从主菜单中选择组件 | UI | 遮罩)。然后,从检查器视图中,找到遮罩组件并取消选中显示遮罩图形(它将变为不可见,作为迷你地图的遮罩使用)。
-
选择MsLaser游戏对象(即玩家的角色),然后从检查器视图的顶部访问层下拉菜单。选择添加层…然后命名一个用户层
Player,如图所示:![如何操作...]()
-
再次选择MsLaser角色,然后从层下拉菜单中选择玩家:
![如何操作...]()
-
从项目视图中创建一个新的渲染纹理并将其命名为
Map_Render。然后,从检查器中,将其大小更改为256 x 256。 -
从层次视图中创建一个新的相机(创建 | 相机)并将其重命名为
MapCamera。从检查器视图中,按照以下参数更改其设置(如图所示):-
清除标志:
仅深度 -
剔除遮罩:
混合…(取消选择玩家) -
投影:
正交 -
深度:
1(或更高) -
目标纹理:
Map_Render -
此外,取消选中相机的音频监听器组件
![如何操作...]()
-
-
在层次视图中,右键单击MiniMap并导航到UI | 原始图像以创建一个子 UI 元素。将其命名为
MapTexture。然后,从检查器视图中,将Map_Render纹理填充到纹理字段中,并单击设置原生大小按钮,如图所示:![如何操作...]()
-
现在,右键单击MiniMap并导航到UI | 图像以创建另一个子元素。将其命名为
Compass。然后,从检查器视图中,将Compass图像填充到源图像字段中,并单击设置原生大小按钮。 -
再次右键单击迷你地图并导航到UI | 图像以添加另一个子元素。将其命名为
Marker。然后,从检查器视图,将源图像字段填充为compassMarker图像,并单击设置原生大小按钮。 -
从项目视图,创建一个新的C# 脚本并将其命名为
MiniMap。打开它并将所有内容替换为以下代码:using UnityEngine; using UnityEngine.UI; using System.Collections; public class MiniMap : MonoBehaviour { public Transform target; public GameObject marker; public GameObject mapGUI; public float height = 10.0f; public float distance = 10.0f; public bool rotate = true; private Vector3 camAngle; private Vector3 camPos; private Vector3 targetAngle; private Vector3 targetPos; private Camera cam; void Start(){ cam = GetComponent<Camera> (); camAngle = transform.eulerAngles; targetAngle = target.transform.eulerAngles; camAngle.x = 90; camAngle.y = targetAngle.y; transform.eulerAngles = camAngle; } void Update(){ targetPos = target.transform.position; camPos = targetPos; camPos.y += height; transform.position = camPos; cam.orthographicSize = distance; Vector3 compassAngle = new Vector3(); compassAngle.z = target.transform.eulerAngles.y; if (rotate) { mapGUI.transform.eulerAngles = compassAngle; marker.transform.eulerAngles = new Vector3(); } else { marker.transform.eulerAngles = -compassAngle; } } } -
保存脚本并将其附加到MapCamera。然后,从检查器视图,按照以下方式更改迷你地图组件的参数(如随后的截图所示):
-
目标:
MsLaser -
标记:
Marker(之前创建的 UI 元素) -
地图 GUI:
MiniMap(之前创建的 UI 面板) -
高度:
10 -
距离:
10 -
旋转:勾选
![如何操作...]()
-
-
播放场景。您应该能够在屏幕右上角看到迷你地图正在工作:
![如何操作...]()
它是如何工作的...
迷你地图的主要元素是一个纹理,用作 GUI 元素,从正交相机渲染,从俯视角度跟随玩家。对MapCamera进行了一些必要的调整:
-
将其投影模式更改为正交(以使其成为二维)
-
从其剔除遮罩中排除玩家标签(以使角色模型对相机不可见)
-
禁用其音频监听器(以免与主相机冲突)
迷你地图装饰了一个指南针框架和一个指示玩家位置的标记。所有这些 GUI 元素都由一个面板作为父元素,该面板还充当视觉元素的遮罩。最后,创建了一个脚本,具有三个作用:配置相机(如覆盖区域)的偏好设置,在运行时根据玩家的变换设置重新定位相机,并旋转适当的 UI 元素。
还有更多...
如果您想对您的迷你地图进行更多实验,请继续阅读。
覆盖更宽或更窄的区域
迷你地图的范围由距离参数给出。更高的值将导致更广泛的区域覆盖,因为迷你地图类使用与正交相机视口大小相同的值。
改变地图的方向
默认情况下,迷你地图设置为随着玩家改变方向而旋转。如果您希望它保持静态,取消勾选旋转选项,使标记旋转。
将您的迷你地图适配到其他风格
您可以轻松修改此配方以制作赛车游戏电路地图的俯视图或等距视图。只需手动定位相机并防止其跟随角色。
创建游戏中的监控摄像头
虽然在许多情况下使用第二个视口可能很有用,但有时您需要在运行时将摄像头渲染的图像输出到纹理。为了说明这一点,在这个食谱中,我们将使用渲染纹理创建一个将视频传输到监视器的游戏内监控摄像头。

游戏内监控摄像头
准备工作
对于这个食谱,我们已准备好BasicSceneUnity 包,其中包含一个名为BasicScene的场景,以及两个用于监视器和摄像头对象的 FBX 3D 模型。该包位于1362_05_codes文件夹中,3D 模型位于1362_05_06文件夹中。
如何操作...
要创建画中画显示,只需按照以下步骤操作:
-
将
BasicScene包和monitor以及camera模型导入到您的 Unity 项目中。 -
从项目视图,打开BasicScene级别。这是一个包含动画角色和一些额外几何形状的基本场景。
-
从项目视图,通过将它们拖入层次结构面板将监视器和摄像头对象放置到场景中。它们的变换设置应如下(如图下截图所示):监视器:位置:X:
0;Y:0.09;Z:4。旋转:X:0;Y:180;Z:0。摄像头:位置:X:-3;Y:0.06;Z:4。旋转:X:0;Y:90;Z:0:![如何操作...]()
-
从项目视图创建一个新的渲染纹理,并将其重命名为
screen。然后,从检查器视图,将其大小更改为512x512。 -
通过层次结构视图顶部的创建下拉菜单(创建 | 摄像头)向场景中添加一个新的摄像头。然后,从检查器视图,将其命名为
Surveillance并使其成为摄像头GameObject 的子对象。然后,将其变换设置更改为以下内容:位置:X:0;Y:2;Z:0,和旋转:X:0;Y:0;Z:0。 -
选择您创建的
Surveillance摄像头,并从检查器视图,将其裁剪平面 | 近面更改为0.6。同时,将目标纹理槽位填充为渲染纹理屏幕,并禁用摄像头的音频监听器组件,如图下截图所示:![如何操作...]()
-
从层次结构视图,展开监视器对象并选择其屏幕子对象。然后,从检查器中找到其材质(命名为Desert),并在着色器下拉菜单中将其更改为Unlit/Texture。最后,将屏幕纹理设置为基本纹理,如图下截图所示:
![如何操作...]()
-
现在是时候向纹理添加一些后期处理效果了。从主菜单,导入效果包(资产 | 导入包 | 效果)。
-
从层次结构视图中选择
Surveillance摄像头。然后,从主菜单中,添加灰度图像效果组件(组件 | 图像效果 | 颜色调整 | 灰度)。同时,添加噪点和颗粒图像效果(组件 | 图像效果 | 噪声 | 噪点和颗粒(电影风格))。最后,从检查器视图中,将噪点和颗粒的强度乘数设置为4。 -
播放你的场景。你应该能够在监视器的屏幕上实时看到你的操作,如图所示:
![如何操作...]()
它是如何工作的...
我们通过将监控摄像头作为应用到屏幕上的渲染纹理的来源,实现了最终效果。为了便于重新定位,摄像头被设置为摄像头的 3D 模型的子对象。此外,其近裁剪面被重新调整,以避免显示摄像头 3D 模型几何形状的一部分,并且禁用了其音频源组件,以免与主摄像头的组件冲突。
除了设置监控摄像头外,还向其中添加了两个图像效果:噪点和颗粒和灰度。这些效果共同作用,应该使渲染纹理看起来更像廉价显示器的屏幕。
最后,我们的屏幕渲染纹理被应用到屏幕的 3D 对象的材质上(其着色器被更改为Unlit/texture,以便在低/无光条件下可见,就像真实的显示器一样)。
第六章. 灯光与效果
本章将涵盖:
-
使用灯光和 cookie 纹理模拟多云天气
-
向场景添加自定义反射贴图
-
使用投影仪和线条渲染器创建激光瞄准
-
使用反射探针反射周围对象
-
使用程序化天空盒和方向光设置环境
-
使用光照贴图和光照探针照亮简单场景
简介
无论你是愿意制作一个更美观的游戏,还是添加有趣的功能,灯光和效果都可以提升你的项目,帮助你交付更高品质的产品。在本章中,我们将探讨使用灯光和效果的创新方法,同时也会看看 Unity 的一些新特性,例如程序化天空盒、反射探针、光照探针和自定义的反射源。
灯光无疑是 Unity 关注的一个领域,现在它提供了由Enlighten提供的实时全局光照技术。这项新技术为实时和烘焙光照提供了更好、更逼真的结果。有关 Unity 全局光照系统的更多信息,请查看其文档docs.unity3d.com/Manual/GIIntro.html。
整体概念
在 Unity 中创建光源有许多方法。以下是对最常见方法的快速概述。
灯光
灯光作为游戏对象放置到场景中,具有一个灯光组件。它们可以在实时、烘焙或混合模式下工作。在其他属性中,用户可以设置它们的范围、颜色、强度和阴影类型。有四种类型的灯光:
-
方向光: 这通常用于模拟阳光
-
聚光灯: 这就像一个锥形聚光灯
-
点光源: 这是一种类似灯泡的全向光
-
区域光: 这种仅烘焙的光源类型从矩形实体向所有方向发射,允许平滑、逼真的着色
要了解灯光类型概述,请查看 Unity 的文档docs.unity3d.com/Manual/Lighting.html。

不同类型的灯光
环境光照
Unity 的环境光照通常是通过结合天空盒材质和由场景的方向光定义的阳光来实现的。这种组合创建了一种环境光,它被整合到场景环境中,并且可以设置为实时或烘焙到光照贴图中。

发光材质
当应用于静态对象时,具有发射颜色或贴图的材质将在实时和烘焙模式下向附近的表面投射光线,如图下所示:

投影仪
正如其名所示,投影仪可以用来模拟投影的光线和阴影,基本上是通过将材质及其纹理映射投影到其他对象上。

光照贴图和光照探针
光照贴图基本上是从场景的照明信息生成的纹理映射,并将其应用于场景的静态对象,以避免使用处理密集型的实时照明。
光照探针是一种在场景的特定点采样照明的方法,以便在没有使用实时照明的情况下将其应用于动态对象。
照明窗口
可以通过导航到窗口 | 照明菜单找到的照明窗口,是设置和调整场景照明功能(如光照贴图、全局照明、雾等)的中心。强烈建议你查看 Unity 关于此主题的文档,该文档可以在docs.unity3d.com/Manual/GlobalIllumination.html找到。

使用灯光和饼干纹理模拟多云的一天
正如在许多第一人称射击游戏和生存恐怖游戏中可以看到的那样,光线和阴影可以为场景增添大量的真实感,极大地帮助创造适合游戏的氛围。在这个配方中,我们将使用饼干纹理来创建多云的户外环境。饼干纹理充当光线的遮罩。它通过调整光线投影的强度到饼干纹理的 alpha 通道来实现。这允许产生轮廓效果(只需想想蝙蝠信号),或者在这个特定案例中,产生细微的变化,使光线具有过滤效果。
准备工作
如果你没有访问图像编辑器的权限,或者希望跳过纹理映射的详细说明,以便专注于实现,请使用名为cloudCookie.tga的图像文件,该文件位于1362_06_01文件夹内。
如何操作...
要模拟多云的户外环境,请按照以下步骤操作:
-
在你的图像编辑器中,创建一个新的 512 x 512 像素的图像。
-
使用黑色作为前景色和白色作为背景色,应用云彩滤镜(在 Photoshop 中,这是通过导航到滤镜 | 渲染 | 云彩菜单来完成的)。
![如何操作...]()
注意
了解 alpha 通道很有用,但即使没有它,你也可以得到相同的结果。跳过步骤 3 到 7,将你的图像保存为
cloudCookie.png,并在步骤 9 更改纹理类型时,保留从灰度到 Alpha 的勾选。 -
选择整个图像并复制它。
-
打开通道窗口(在 Photoshop 中,可以通过导航到窗口 | 通道菜单来完成)。
-
应该有三个通道:红色、绿色和蓝色。创建一个新的通道。这将是一个alpha通道。
-
在通道窗口中,选择Alpha 1通道,并将您的图像粘贴到其中。
![如何操作...]()
-
将您的图像文件保存为
cloudCookie.PSD或TGA。 -
将您的图像文件导入 Unity 并在项目视图中选择它。
-
在检查器视图中,将其纹理类型更改为Cookie,并将其光照类型更改为方向。然后,点击应用,如图所示:
![如何操作...]()
-
我们需要一个表面来真正看到光照效果。您可以通过导航到GameObject | 3D Object | Plane菜单向场景中添加一个平面,或者创建一个地形(菜单选项GameObject | 3D Object | Terrain),如果您愿意的话可以编辑它。
-
让我们在场景中添加一个光源。由于我们想要模拟阳光,最佳选项是创建一个方向光。您可以通过层次视图中的创建 | Light | Directional Light下拉菜单来完成此操作。
-
使用检查器视图中的变换组件,将光线的位置重置为X:
0,Y:0,Z:0,并将其旋转重置为X:90;Y:0;Z:0。 -
在Cookie字段中,选择您之前导入的cloudCookie纹理。将Cookie 大小字段更改为
80,或您认为更适合场景尺寸的值。请将阴影类型保留为无阴影。![如何操作...]()
-
现在,我们需要一个脚本来转换我们的光,从而相应地转换Cookie投影。在项目视图中的创建下拉菜单中,创建一个新的 C# 脚本,命名为
MovingShadows.cs。 -
打开您的脚本,将所有内容替换为以下代码:
using UnityEngine; using System.Collections; public class MovingShadows : MonoBehaviour{ public float windSpeedX; public float windSpeedZ; private float lightCookieSize; private Vector3 initPos; void Start(){ initPos = transform.position; lightCookieSize = GetComponent<Light>().cookieSize; } void Update(){ Vector3 pos = transform.position; float xPos= Mathf.Abs (pos.x); float zPos= Mathf.Abs (pos.z); float xLimit = Mathf.Abs(initPos.x) + lightCookieSize; float zLimit = Mathf.Abs(initPos.z) + lightCookieSize; if (xPos >= xLimit) pos.x = initPos.x; if (zPos >= zLimit) pos.z = initPos.z; transform.position = pos; float windX = Time.deltaTime * windSpeedX; float windZ = Time.deltaTime * windSpeedZ; transform.Translate(windX, 0, windZ, Space.World); } } -
保存您的脚本并将其应用到方向光上。
-
选择方向光。在检查器视图中,将风速 X和风速 Z参数更改为
20(您可以更改这些值,如图所示)。![如何操作...]()
-
播放您的场景。阴影将会移动。
它是如何工作的...
使用我们的脚本,我们正在告诉方向光在 X 和 Z 轴上移动,导致光 Cookie纹理也发生位移。此外,每当它移动的距离等于或大于光 Cookie 大小时,我们都会将光对象重置到其原始位置。必须重置光位置以防止它移动得太远,从而在实时渲染和光照中引起问题。光 Cookie 大小参数用于确保平滑过渡。
我们没有启用阴影的原因是因为 X 轴的光线角度必须是 90 度(否则当光线重置到原始位置时会出现明显的间隙)。如果您想在场景中实现动态阴影,请添加第二个方向光。
更多...
在这个菜谱中,我们已经将一个 cookie 纹理应用到方向****光上。但如果我们使用聚光灯或点光源会怎样呢?
创建聚光灯 Cookie
Unity 文档中有一个关于如何制作 聚光灯 Cookie 的优秀教程。这对于模拟来自投影仪、窗户等处的阴影非常有用。你可以在 docs.unity3d.com/Manual/HOWTO-LightCookie.html 查看它。
创建点光源 Cookie
如果你想使用与点光源配合的 Cookie 纹理,你需要在 检查器 的 纹理导入器 部分更改 灯光类型。
向场景添加自定义反射贴图
虽然 Unity 旧式着色器 为每个材质使用单独的 反射立方体贴图,但新的 标准着色器 从场景的 反射源 获取反射,这已在 灯光 窗口的 场景 部分配置。每个材质的反射程度现在由其 金属 值或 光泽 值(对于使用光泽设置的材质)给出。这种方法可以节省大量时间,允许你快速将相同的反射贴图分配给场景中的每个对象。此外,正如你可以想象的那样,它有助于保持场景的整体外观协调一致。在这个配方中,我们将学习如何利用 反射源 功能。
准备工作
对于这个配方,我们将准备一个 反射立方体贴图,这基本上是要投影到材质上的环境。它可以由六个或,如本配方所示,单个图像文件制作而成。
为了帮助我们完成这个配方,提供了一个 Unity 包,其中包含一个由 3D 对象和基本材质(使用 TIFF 作为漫反射贴图)组成的预制件,以及一个用于反射贴图的 JPG 文件。所有这些文件都位于 1362_06_02 文件夹中。
如何操作...
要向材质添加反射性和光泽度,请按照以下步骤操作:
-
将
batteryPrefab.unitypackage导入到一个新项目中。然后,在 项目 视图中从 资产 文件夹中选择battery_prefab对象。 -
从 检查器 视图中展开 材质 组件,观察资产预览窗口。多亏了 高光 贴图,材质已经具有了反射的外观。然而,它看起来像是在反射场景的默认 天空盒,如下所示:
![如何操作...]()
-
导入
CustomReflection.jpg图像文件。从 检查器 视图中,将其 纹理类型 更改为 立方体贴图,其 映射 更改为 纬度-经度布局(圆柱形),并勾选 光泽反射 和 修复边缘缝隙 复选框。最后,将其 过滤模式 更改为 三线性 并点击 应用 按钮,如下所示:![如何操作...]()
-
让我们将场景的天空盒替换为我们新创建的立方体贴图,作为场景的反射贴图。为了做到这一点,通过导航到窗口 | 照明菜单打开照明窗口。选择场景部分,并使用下拉菜单将反射源更改为自定义。最后,将新创建的
CustomReflection纹理分配为立方体贴图,如下所示:![如何操作...]()
-
检查
battery_prefab对象上的新反射。如何操作...
它是如何工作的...
虽然是材质的镜面反射贴图允许反射外观,包括反射的强度和光滑度,但反射本身(即你在反射中看到的图像)是由我们从图像文件创建的立方体贴图提供的。
更多...
反射立方体贴图可以通过多种方式实现,并且具有不同的映射属性。
映射坐标
我们应用的圆柱形贴图非常适合我们使用的照片。然而,根据反射图像的生成方式,基于立方体或球体贴图的贴图可能更合适。此外,请注意,修复边缘接缝选项将尝试使图像无缝。
锐利反射
你可能已经注意到,与原始图像相比,反射有些模糊;这是因为我们勾选了光泽反射框。为了得到更清晰的反射效果,取消选中此选项;在这种情况下,你还可以将滤波模式选项保留为默认(双线性)。
最大尺寸
在 512 x 512 像素的情况下,我们的反射贴图在低端机器上可能运行良好。然而,如果你的游戏中反射贴图的质量不是那么重要,并且原始图像的尺寸很大(比如,4096 x 4096),你可能想在导入设置中将纹理的最大尺寸更改为一个较小的数字。
使用投影仪和线渲染器创建激光瞄准
虽然使用 GUI 元素,如准星,是允许玩家瞄准的有效方法,但用投影激光点替换(或结合)它可能是一个更有趣的方法。在这个菜谱中,我们将使用投影仪和线组件来实现这个概念。
准备工作
为了帮助我们完成这个菜谱,提供了一个包含一个带有激光指针的角色和名为LineTexture的纹理图的 Unity 包。所有文件都在1362_06_03文件夹中。此外,我们还将使用 Unity 提供的效果资产包(你应该在安装 Unity 时安装过)。
如何操作...
要使用投影仪创建激光点瞄准,请按照以下步骤操作:
-
将
BasicScene.unitypackage导入到新项目中。然后,打开名为BasicScene的场景。这是一个基本场景,其中包含一个玩家角色,其瞄准是通过鼠标控制的。 -
通过导航到Assets | Import Package | Effects菜单导入Effects包。如果您只想导入包内的必要文件,请通过点击None按钮取消Importing package窗口中的所有选择,然后仅选择Projectors文件夹。然后,点击Import,如图所示:
![如何操作...]()
-
从Inspector视图,找到
ProjectorLight着色器(位于Assets | Standard Assets | Effects | Projectors | Shaders文件夹中)。复制文件并将新副本命名为ProjectorLaser。 -
打开
ProjectorLaser。从代码的第一行开始,将Shader "Projector/Light"更改为Shader "Projector/Laser"。然后,找到代码行Blend DstColor One并将其更改为Blend One One。保存并关闭文件。注意
编辑激光着色器的目的是通过将其混合类型更改为Additive来增强其效果。着色器编程是一个复杂的话题,超出了本书的范围。然而,如果您想了解更多,请查看 Unity 关于该主题的文档,可在
docs.unity3d.com/Manual/SL-Reference.html找到,以及由 Packt 出版的名为Unity Shaders and Effects Cookbook的书籍。 -
现在我们已经固定了着色器,我们需要一个材质。从Project视图,使用Create下拉菜单创建一个新的Material。将其命名为
LaserMaterial。然后,从Project视图选择它,并从Inspector视图,将其Shader更改为Projector/Laser。 -
从Project视图,找到Falloff纹理。在您的图像编辑器中打开它,除了应该为黑色的第一列和最后一列像素外,将其他所有内容涂成白色。保存文件并返回 Unity。
![如何操作...]()
-
将LaserMaterial的Main Color更改为红色(RGB:
255,0,0)。然后,从纹理槽中,选择Light纹理作为Cookie和Falloff纹理。![如何操作...]()
-
从Hierarchy视图,找到并选择pointerPrefab对象(MsLaser | mixamorig:Hips | mixamorig:Spine | mixamorig:Spine1 | mixamorig:Spine2 | mixamorig:RightShoulder | mixamorig:RightArm | mixamorig:RightForeArm | mixamorig:RightHand | pointerPrefab)。然后,从Create下拉菜单中选择Create Empty Child。将pointerPrefab的新子项重命名为LaserProjector。
-
选择LaserProjector对象。然后,从Inspector视图,点击Add Component按钮并导航到Effects | Projector。然后,从Projector组件,将Orthographic选项设置为 true 并将Orthographic Size设置为
0.1。最后,从Material槽中选择LaserMaterial。 -
测试场景。您将能够看到激光瞄准点,如图所示:
![如何操作...]()
-
现在,让我们为即将添加的线渲染器组件创建一个材质。从项目视图,使用创建下拉菜单添加一个新的材质。将其命名为Line_Mat。
-
从检查器视图,将Line_Mat的着色器更改为粒子/添加。然后,将其着色颜色设置为红色(RGB:
255;0;0)。 -
导入
LineTexture图像文件。然后,将其设置为Line_Mat的粒子纹理,如图所示:![如何操作...]()
-
从项目视图的创建下拉菜单添加一个名为
LaserAim的 C#脚本。然后,在您的编辑器中打开它。 -
将以下代码替换为所有内容:
using UnityEngine; using System.Collections; public class LaserAim : MonoBehaviour { public float lineWidth = 0.2f; public Color regularColor = new Color (0.15f, 0, 0, 1); public Color firingColor = new Color (0.31f, 0, 0, 1); public Material lineMat; private Vector3 lineEnd; private Projector proj; private LineRenderer line; void Start () { line = gameObject.AddComponent<LineRenderer>(); line.material = lineMat; line.material.SetColor("_TintColor", regularColor); line.SetVertexCount(2); line.SetWidth(lineWidth, lineWidth); proj = GetComponent<Projector> (); } void Update () { RaycastHit hit; Vector3 fwd = transform.TransformDirection(Vector3.forward); if (Physics.Raycast (transform.position, fwd, out hit)) { lineEnd = hit.point; float margin = 0.5f; proj.farClipPlane = hit.distance + margin; } else { lineEnd = transform.position + fwd * 10f; } line.SetPosition(0, transform.position); line.SetPosition(1, lineEnd); if(Input.GetButton("Fire1")){ float lerpSpeed = Mathf.Sin (Time.time * 10f); lerpSpeed = Mathf.Abs(lerpSpeed); Color lerpColor = Color.Lerp(regularColor, firingColor, lerpSpeed); line.material.SetColor("_TintColor", lerpColor); } if(Input.GetButtonUp("Fire1")){ line.material.SetColor("_TintColor", regularColor); } } } -
保存你的脚本并将其附加到激光投影仪游戏对象。
-
选择激光投影仪游戏对象。从检查器视图,找到激光瞄准组件,并将线材质槽填满
Line_Mat材质,如图所示:![如何操作...]()
-
播放场景。激光瞄准已准备就绪,看起来如图所示:
![如何操作...]()
注意
在这个菜谱中,激光束的宽度和其瞄准点已被夸张。如果您需要更真实的厚度,请将激光瞄准组件的线宽字段更改为
0.05,并将投影仪组件的正交大小更改为0.025。此外,请记住通过将激光瞄准组件的常规颜色设置得更亮来使光束更不透明。
工作原理...
激光瞄准效果是通过结合两种不同的效果实现的:投影仪和线渲染器。
投影仪,可以用来模拟光、阴影等,是一个将材质(及其纹理)投影到其他游戏对象的组件。通过将投影仪附加到激光指针对象,我们确保它始终面向正确的方向。为了获得正确的、生动的效果,我们编辑了投影仪材质的着色器,使其更亮。此外,我们编写了一个脚本,通过将其远裁剪平面设置在接收投影的第一个对象的大致相同水平,来防止投影穿过对象。负责此操作的代码行是—proj.farClipPlane = hit.distance + margin;。
关于线渲染器,我们选择通过代码动态创建它,而不是手动将组件添加到游戏对象。代码还负责设置其外观,更新线顶点位置,并在按下射击按钮时更改其颜色,使其具有发光/脉冲的外观。
要了解脚本的工作原理的更多细节,别忘了查看1362_06_03 | End文件夹中可用的注释代码。
使用反射探针反射周围对象
如果你想让你的场景环境通过具有反射材料(如具有高金属或高光泽度级别的材料)的游戏对象反射,那么你可以使用 Reflection Probes 来实现这种效果。它们允许通过使用立方体贴图实现实时、烘焙或自定义反射。
实时反射在处理方面可能会很昂贵;在这种情况下,你应该优先考虑烘焙反射,除非确实有必要显示动态反射对象(例如镜子一样的对象)。尽管如此,还有一些方法可以优化实时反射。在这个配方中,我们将测试三种不同的反射探针配置:
-
实时反射(持续更新)
-
实时反射(按需更新)通过脚本
-
烘焙反射(来自编辑器)
准备工作
对于这个配方,我们准备了一个基本场景,包含三组反射对象:一组是持续移动的,一组是静态的,还有一组在交互时移动。包含场景的 Probes.unitypackage 包位于 1362_06_04 文件夹中。
如何操作...
要使用反射探针反射周围的对象,请按照以下步骤操作:
-
将
Probes.unitypackage导入到一个新项目中。然后,打开名为 Probes 的场景。这是一个包含三组反射对象的基本场景。 -
播放场景。观察到一个系统是动态的,一个是静态的,还有一个在按下键时随机旋转。
-
停止场景。
-
首先,让我们创建一个持续更新的实时反射探针。从 Hierarchy 视图的 Create 下拉按钮中,向场景中添加一个 Reflection Probe(Create | Light | Reflection Probe)。将其命名为
RealtimeProbe并使其成为 System 1 Realtime | MainSphere 游戏对象的子对象。然后,从 Inspector 视图中,更改 Transform 组件的 Position 为 X:0; Y:0; Z:0,如图所示:![如何操作...]()
-
现在,转到 Reflection Probe 组件。将 Type 设置为 Realtime;Refresh Mode 设置为 Every Frame 和 Time Slicing 设置为 No time slicing,如图所示:
![如何操作...]()
-
播放场景。现在,反射将实时更新。停止场景。
-
注意,唯一显示实时反射的对象是 System 1 Realtime | MainSphere。这是因为反射探针的 Size。从 Reflection Probe 组件中,将 Size 更改为 X:
25; Y:10; Z:25。请注意,现在小红色球体也会受到影响。然而,重要的是要注意所有对象都显示相同的反射。由于我们的反射探针的起点与 MainSphere 的位置相同,所有反射对象都将从这个角度显示反射。![如何操作...]()
-
如果您想从反射探针内的反射对象中消除反射,例如小红色球体,请选择这些对象,并从网格渲染器组件中,将反射探针设置为关闭,如图所示:
![如何操作...]()
-
向场景中添加一个新的反射探针。这次,将其命名为
OnDemandProbe,并使其成为系统 2 按需|主球体游戏对象的子对象。然后,从检查器视图,变换组件,将位置设置为X:0;Y:0;Z:0。 -
现在,前往反射探针组件。将类型设置为实时,刷新模式设置为通过脚本,并将时间切片设置为单个面,如图所示:
![如何操作...]()
-
在项目视图中的创建下拉菜单中,创建一个名为
UpdateProbe的新 C#脚本。 -
打开您的脚本,并将所有内容替换为以下代码:
using UnityEngine; using System.Collections; public class UpdateProbe : MonoBehaviour { private ReflectionProbe probe; void Awake () { probe = GetComponent<ReflectionProbe> (); probe.RenderProbe(); } public void RefreshProbe(){ probe.RenderProbe(); } } -
保存您的脚本并将其附加到按需探针。
-
现在,找到名为
RandomRotation的脚本,它附加到系统 2 按需|球体对象上,并在代码编辑器中打开它。 -
在
Update()函数之前,添加以下行:private GameObject probe; private UpdateProbe up; void Awake(){ probe = GameObject.Find("OnDemandProbe"); up = probe.GetComponent<UpdateProbe>(); } -
现在,找到名为
transform.eulerAngles = newRotation;的代码行,并在其后立即添加以下行:up.RefreshProbe(); -
保存脚本并测试您的场景。观察每当按下键时,反射探针是如何更新的。
-
停止场景。向场景中添加第三个反射探针。将其命名为
CustomProbe,并使其成为系统 3 自定义|主球体游戏对象的子对象。然后,从检查器视图,变换组件,将位置设置为X:0;Y:0;Z:0。 -
前往反射探针组件。将类型设置为自定义,并单击烘焙按钮,如图所示:
![如何操作...]()
-
将会弹出一个保存文件对话框。将文件保存为
CustomProbe-reflectionHDR.exr。 -
观察到反射贴图不包括红色球体的反射。要更改此,您有两个选项:将系统 3 自定义|球体游戏对象(及其所有子对象)设置为反射探针静态,或者从CustomProbe游戏对象的反射探针组件中,勾选动态对象选项,如图所示,并再次烘焙贴图(通过单击烘焙按钮)。
![如何操作...]()
-
如果你希望在编辑场景时动态烘焙你的反射立方体贴图,你可以将反射探针类型设置为烘焙,打开光照窗口(资产 | 光照菜单),访问场景部分,并检查如图所示的连续烘焙选项。请注意,此模式不会包括动态对象在反射中,所以请确保将系统 3 自定义 | 球体和系统 3 自定义 | 主球体设置为反射探针静态。
![如何操作...]()
它是如何工作的...
反射探针元素像全向相机一样工作,渲染立方体贴图并将它们应用到其约束内的对象上。在创建反射探针时,重要的是要了解不同类型的工作方式:
-
实时反射探针:立方体贴图在运行时更新。实时反射探针有三种不同的刷新模式:在唤醒时(立方体贴图在场景开始前烘焙一次);每帧(立方体贴图不断更新);通过脚本(每次使用RenderProbe函数时更新立方体贴图)。
由于立方体贴图有六个面,反射探针具有时间切片功能,因此每个面可以独立更新。有时间切片的三种不同类型:一次性更新所有面(一次性渲染所有面,并在 6 帧内计算米普贴图。在 9 帧内更新探针);单独更新每个面(每个面在多帧内渲染。在 14 帧内更新探针。结果可能有点不准确,但就帧率影响而言是最经济的解决方案);无时间切片(探针在一帧内渲染,米普贴图在一帧内计算。它提供高精度,但也是帧率中最昂贵的)。
-
烘焙:立方体贴图在编辑屏幕时进行烘焙。立方体贴图可以是手动或自动更新,这取决于是否选中了连续烘焙选项(它可以在光照窗口的场景部分找到)。
-
自定义:自定义反射探针可以是手动从场景中烘焙(甚至包括动态对象),或者从预制的立方体贴图中创建。
还有更多...
有许多其他设置可以调整,例如重要性、强度、盒投影、分辨率、HDR等等。为了全面了解这些设置,我们强烈建议您阅读 Unity 关于此主题的文档,该文档可在docs.unity3d.com/Manual/class-ReflectionProbe.html找到。
设置具有程序化天空盒和方向光的场景
除了传统的六面体和立方体贴图,Unity 现在还提供第三种天空盒类型:程序化天空盒。它易于创建和设置,程序化天空盒可以与方向光结合使用,为你的场景提供环境光照。在本教程中,我们将学习程序化天空盒的不同参数。
准备工作
对于这个教程,你需要导入 Unity 的标准资产效果包,当你安装 Unity 时应该已经安装了。
如何操作...
要使用程序化天空盒和方向光设置环境光照,请按照以下步骤操作:
-
在 Unity 项目中创建一个新的场景。观察到一个新的场景已经包括两个对象:主摄像机和一个方向光。
-
在你的场景中添加一些立方体,包括一个在位置 X:
0;Y:0;Z:0,缩放为X:20;Y:1;Z:20,用作地面,如下所示:![如何操作...]()
-
从项目视图的创建下拉菜单中,创建一个新的材质,并将其命名为
MySkybox。从检查器视图,使用适当的下拉菜单将MySkybox的着色器从标准更改为Skybox/程序化。 -
打开光照窗口(菜单窗口 | 光照),访问场景部分。在环境光照子部分,将天空盒槽位填充为MySkybox材质,将太阳槽位填充为场景中的方向光。
-
从项目视图,选择MySkybox。然后,从检查器视图,设置太阳大小为
0.05和大气厚度为1.4。通过将天空色调颜色更改为 RGB:148;128;128,并将地面颜色设置为类似于场景立方体地板的颜色(例如 RGB:202;202;202)进行实验。如果你觉得场景太亮,尝试将曝光级别降低到0.85,如下所示:![如何操作...]()
-
选择方向光并更改其旋转为X:
5;Y:170;Z:0。注意,场景应该类似于黎明环境,如下面的场景:![如何操作...]()
-
让我们使事情更有趣。在项目视图中的创建下拉菜单中,创建一个新的 C# 脚本,命名为
RotateLight。打开你的脚本,将所有内容替换为以下代码:using UnityEngine; using System.Collections; public class RotateLight : MonoBehaviour { public float speed = -1.0f; void Update () { transform.Rotate(Vector3.right * speed * Time.deltaTime); } } -
保存它并将其作为组件添加到方向光。
-
将效果资产包导入到你的项目中(通过资产 | 导入包 | 效果菜单)。
-
选择方向光。然后,从检查器视图,光照组件,将光晕槽位填充为
Sun光晕。 -
在光照窗口的场景部分,找到其他设置子部分。然后,将光晕淡入速度设置为
3,将光晕强度设置为0.5,如下所示:![如何操作...]()
-
播放场景。你会看到太阳升起,并且 Skybox 颜色相应地改变。
它是如何工作的...
最终,Unity 原生程序化 Skybox 的外观取决于构成它们的五个参数:
-
太阳大小:绘制到 Skybox 上的明亮黄色太阳的大小,根据方向光的X和Y轴上的旋转定位。
-
大气厚度:这模拟了此 Skybox 的大气密度。较低的值(小于
1.0)适合模拟外太空设置。适中的值(大约1.0)适合基于地球的环境。略高于1.0的值在模拟空气污染和其他戏剧性设置时可能很有用。夸张的值(如大于2.0)有助于说明极端条件或甚至外星环境。 -
天空着色:这是用于着色 Skybox 的颜色。它对于微调或创建风格化环境非常有用。
-
地面:这是地面的颜色。它确实会影响场景的全局光照。因此,选择一个接近关卡地形和/或几何形状(或中性)的值。
-
曝光:这决定了有多少光线进入 Skybox。较高的级别模拟过曝,而较低的值模拟欠曝。
重要的是要注意,Skybox的外观将响应场景的方向光,扮演太阳的角色。在这种情况下,围绕其X轴旋转光线可以创建黎明和日落场景,而围绕其Y轴旋转则会改变太阳的位置,改变场景的方位点。
此外,关于环境光照,请注意,尽管我们已使用Skybox作为环境光源,但我们本可以选择渐变或单一颜色——在这种情况下,场景的照明就不会与 Skybox 外观相关联。
最后,关于环境光照,请注意,我们已经将环境全局光照设置为实时。这样做的原因是允许由旋转的方向光促进的 GI 实时变化。如果我们不需要在运行时进行这些变化,我们可以选择烘焙选项。
使用光照贴图和光照探针照亮简单场景
光图是实时光照的一个很好的替代方案,因为它们可以在不占用处理器资源的情况下为环境提供所需的外观。然而,有一个缺点——由于无法将光图烘焙到动态对象上,游戏中的重要元素(如玩家角色本身)的光照可能会显得不自然,无法与周围区域的光照强度相匹配。解决方案?光照探针。
光照探针通过在它们放置的位置采样光照强度来工作。一旦启用光照探针,动态对象的光照将根据它们周围最近的探针进行插值。

准备工作
对于这个配方,我们已经准备了一个基本场景,包括一个简单的游戏环境和 Unity 的 Rollerball 示例资产的一个实例,该实例将被用作玩家角色。场景的几何形状是使用ProBuilder 2.0创建的,这是一个由 ProCore 开发的扩展,可在 Unity 的 Asset Store 和 ProCore 的网站上购买(www.protoolsforunity3d.com)。ProBuilder 是一个出色的关卡设计工具,可以显著加快简单和复杂关卡设计的过程。
包含场景和所有必要文件的LightProbes.unitypackage包可以在1362_06_06文件夹中找到。
如何操作...
要使用反射****探针反射周围的对象,请按照以下步骤操作:
-
将
LightProbes.unitypackage导入到新项目中。然后,打开名为LightProbes的场景。该场景包含一个基本环境和可玩 Rollerball 游戏序列。 -
首先,让我们设置场景中的光照。从层次结构视图中选择方向光。然后,从检查器视图,将烘焙设置为烘焙。此外,在检查器的顶部,在对象名称的右侧,勾选静态框,如下所示:
![如何操作...]()
-
现在,让我们设置场景的全局光照。打开光照窗口(通过菜单窗口 | 光照)并选择场景部分。然后,从环境光照子部分,将
SkyboxProbes(从资产中可用)设置为天空盒,并将场景的方向光设置为太阳。最后,将环境 GI选项从实时更改为烘焙,如下截图所示:![如何操作...]()
-
光图只能应用于静态对象。从层次结构视图展开关卡游戏对象以显示子对象列表。然后,选择每个子对象并将它们设置为静态,如下所示:
![如何操作...]()
-
导入的 3D 网格必须具有光照贴图 UV 坐标。从项目视图,找到并选择
lamp网格。然后,从检查器视图,在导入设置的模型部分中勾选生成光照贴图 UVs选项,并点击应用按钮以确认更改,如下所示:![如何操作...]()
-
滚动到导入设置视图,展开灯的材质组件。然后,在资产文件夹中找到名为
lamp_EMI的纹理,将其填充到发射字段中。最后,将全局照明选项更改为烘焙。这将使灯对象发出绿色光,并将其烘焙到光照贴图中。![如何操作...]()
-
打开照明窗口。默认情况下,连续烘焙选项将被勾选。取消勾选它,如下所示,这样我们就可以按需烘焙光照贴图。
![如何操作...]()
-
点击构建按钮,等待光照贴图生成。
-
从层次结构视图,选择RollerBall。然后,从检查器视图,找到网格渲染器组件并勾选使用光照探头选项,如下所示:
![如何操作...]()
-
现在,我们需要为场景创建光照探头。从层次结构视图,点击创建下拉菜单,并将光照探头组添加到场景中(创建 | 光 | 光照探头组)。
-
为了便于操作探头,在层次结构视图的搜索字段中输入
Probe。这将隔离新创建的光照探头组,使其成为场景中唯一的可编辑对象。![如何操作...]()
-
将您的视口布局更改为4 分割,方法是导航到窗口 | 布局 | 4 分割。然后,设置视口为顶部、前部、右侧和透视。可选地,将顶部、前部和右侧视图更改为线框模式。最后,确保它们设置为正交视图,如下面的截图所示。这将使您更容易定位光照探头。
![如何操作...]()
-
将初始光照探头放置在关卡顶部房间的角落。要移动探头,只需点击并拖动它们,如下所示:
![如何操作...]()
-
选择隧道入口左侧的四个探头。然后,通过在检查器视图中点击相应的按钮或,作为替代,使用Ctrl/Cmd + D键来复制它们。最后,将新探头稍微向右拖动,直到它们不再位于由墙壁投射的阴影上,如下所示:
![如何操作...]()
-
重复上一步,这次复制隧道入口附近的探针并将它们向内移动到组中。要删除选定的探针,请使用光探针组组件上的相应按钮,或使用Ctrl/Cmd + Backspace键。
![如何操作...]()
-
复制并重新定位距离隧道最近的四个探针,重复操作五次,并将每个副本集与隧道投射的阴影相匹配。
![如何操作...]()
-
使用添加 探针按钮将三个探针放置在场景中光照良好的区域。
![如何操作...]()
-
现在,在 L 形墙投射的阴影内添加光探针。
![如何操作...]()
-
由于 Rollerball 能够跳跃,因此将较高的探针放置得更高,以便它们可以采样场景中阴影区域上方的光照。
![如何操作...]()
-
在场景上放置过多的光探针可能会占用大量内存。尝试通过从玩家无法访问的区域移除探针来优化光探针组。此外,通过移除与同一光照条件下其他探针过于接近的探针,避免过度拥挤连续光照条件区域。
![如何操作...]()
-
要检查哪些光探针正在影响场景中任何位置的Rollerball,请将Rollerball游戏对象在场景中移动。一个多面体会指示在该位置正在插值的探针,如图所示:
![如何操作...]()
-
在光照窗口底部,单击构建按钮,等待光照贴图烘焙完成。
![如何操作...]()
-
测试场景。Rollerball 将根据光探针进行照明。
![如何操作...]()
-
继续添加探针,直到关卡完全被覆盖。
它是如何工作的...
光照贴图基本上是包含场景灯光/阴影、全局光照、间接光照以及具有发射材料的对象的纹理图。它们可以由 Unity 的光照引擎自动生成或在需要时生成。然而,有一些需要注意的点,例如:
-
将所有非移动对象和灯光设置为静态烘焙
-
将游戏灯光设置为烘焙
-
将场景的环境全局光照设置为烘焙
-
将发射材料的全局光照选项设置为烘焙
-
为所有 3D 网格(特别是导入的网格)生成光照 UV
-
要么从光照窗口手动构建光照贴图,要么设置连续烘焙选项为勾选
光探针通过在它们放置的点采样场景的照明来工作。一个启用了使用光探针的动态对象,其照明由围绕它的四个定义体积的光探针的照明值之间的插值确定(或者,如果没有适合定义动态对象周围体积的探针,则使用最近探针之间的三角剖分)。
重要的是要注意,即使您正在处理一个平坦的水平,您也不应该将所有探针放置在同一水平面上,因为光探针组将形成一个体积,以便正确计算插值。关于这个主题的更多信息可以在 Unity 的文档中找到,网址为docs.unity3d.com/Manual/LightProbes.html。
还有更多...
如果您能节省一些处理能力,您可以将光探针的使用与混合光交换。只需从您的场景中删除光探针组,选择方向光,然后从灯光组件中,将烘焙更改为混合。然后,将阴影类型设置为软阴影,并将强度设置为0.5,如以下屏幕所示。最后,点击构建按钮,等待光照图烘焙完成。实时光/阴影将被投射到/从动态对象,如Rollerball。

结论
本章旨在向您介绍 Unity 在照明方面的一些新功能,并偶尔教您一些关于灯光和效果的技巧。到目前为止,您应该已经熟悉了 Unity 5 引入的一些概念,对各种技术感到舒适,并且,希望您愿意更深入地探索这些菜谱中讨论的功能。
和往常一样,Unity 关于这个主题的文档非常出色,所以我们鼓励您回到菜谱,并遵循提供的 URL。
第七章。控制 3D 动画
在本章中,我们将介绍:
-
配置角色的 Avatar 和空闲动画
-
使用根运动和混合树移动你的角色
-
使用层和遮罩混合动画
-
将状态组织到子状态机中
-
通过脚本转换角色控制器
-
将刚体道具添加到动画角色上
-
使用动画事件抛掷一个对象
-
将 Ragdoll 物理应用到角色上
-
旋转角色的躯干以瞄准武器
简介
Mecanim 动画系统彻底改变了在 Unity 中对角色进行动画和控制的模式。在本章中,我们将学习如何利用其灵活性、强大功能和友好且高度可视化的界面。
整体图景
使用 Mecanim 系统控制可玩角色可能看起来是一个复杂的任务,但实际上非常直接。

希望到本章结束时,你至少能对 Mecanim 系统有一个基本的了解。为了更全面地了解这个主题,可以考虑阅读 Jamie Dean 的 Unity Character Animation with Mecanim,这本书也由 Packt Publishing 出版。
附加说明——所有的配方都将使用 Mixamo 动作包。Mixamo 是一个完整的角色制作、绑定和动画解决方案。实际上,正在使用的角色是用 Mixamo 的角色创建软件 Fuse 设计的,并使用 Mixamo 的 Auto-rigger 进行绑定。你可以在 Unity 的 Asset Store (www.assetstore.unity3d.com/en/#!/publisher/150) 或他们的网站 www.mixamo.com/ 上了解更多关于 Mixamo 和他们的产品。
请注意,尽管 Mixamo 提供了 Mecanim 准备好的角色和动画片段,但我们在本章的配方中将使用未准备的动画片段。这样做的原因是让你在处理通过其他方法和来源获得的资产时更有信心。
配置角色的 Avatar 和空闲动画
使 Mecanim 非常灵活和强大的一个特性是能够快速将动画片段从一个角色重新分配到另一个角色。这是通过使用 Avatar 实现的,它基本上是在你的角色的原始骨架和 Unity 的 Animator 系统之间的一层。
在这个配方中,我们将学习如何在绑定的角色上配置 Avatar 骨架。
准备工作
对于这个配方,你需要 MsLaser@T-Pose.fbx 和 Swat@rifle_aiming_idle.fbx 文件,这些文件包含在 1362_07_code/character_and_clips/ 文件夹中。
如何做到这一点...
要配置一个 Avatar 骨架,请按照以下步骤操作:
-
将
MsLaser@T-Pose.fbx和Swat@rifle_aiming_idle.fbx文件导入到你的项目中。 -
从 项目 视图中选择
MsLaser@T-Pose模型。 -
在检查器视图中,在MsLaser@T-Pose 导入设置下,激活骨架部分。将动画类型更改为人类。然后,将Avatar 定义保留为从此模型创建。最后,点击配置…按钮。
![如何操作...]()
-
检查器视图将显示新创建的 Avatar。观察 Unity 如何正确地将我们角色的骨骼映射到其结构中,例如,将mixamoRig:LeftForeArm骨骼分配为 Avatar 的下臂。当然,如果需要,我们可以重新分配骨骼。现在,只需点击完成按钮关闭视图。
![如何操作...]()
-
现在我们已经准备好了我们的 Avatar,让我们为空闲状态配置动画。从项目视图中,选择Swat@rifle_aiming_idle文件。
-
激活骨架部分,将动画类型更改为人类,并将Avatar 定义更改为从此模型创建。通过点击应用来确认更改。
![如何操作...]()
-
激活动画部分(位于骨架右侧)。从剪辑列表中选择rifle_aiming_idle剪辑。预览区域(检查器底部的底部)将显示消息没有模型可用于预览。请 将模型拖动到此预览区域。将MsLaser@T-Pose拖动到预览区域以纠正此问题。
![如何操作...]()
-
从剪辑列表中选择rifle_aiming_idle,勾选循环时间和循环姿态选项。同时,点击限制范围按钮调整时间线到动画剪辑的实际时间。然后,在根变换旋转下,勾选烘焙到姿态,并选择基于 | 原始。在根变换位置(Y)下,勾选烘焙到姿态,并选择基于(在开始时) | 原始。在根变换位置(XZ)下,不勾选烘焙到姿态,并选择基于(在开始时) | 质心。最后,点击应用以确认更改。
![如何操作...]()
-
为了访问动画剪辑并播放它们,我们需要创建一个控制器。通过从项目视图点击创建按钮,然后选择动画控制器选项来完成此操作。将其命名为
MainCharacter。 -
双击动画控制器以打开动画器视图。
-
从动画器视图,在网格上右键单击以打开上下文菜单。然后,选择创建状态 | 空选项。一个名为新状态的新框将出现。它将呈橙色,表示它是默认状态。
![如何操作...]()
-
选择新状态,在检查器视图中,将其名称更改为
Idle。此外,在运动字段中,通过从列表中选择或从项目视图拖动来选择rifle_aiming_idle。![如何操作...]()
-
将
MsLaser@T-Pose模型从项目视图拖到层次结构视图,并将其放置在场景中。 -
从层次结构视图中选择MsLaser@T-Pose,并在检查器视图中观察其动画器组件。然后,将新创建的MainCharacter 控制器分配给其控制器字段。
![如何操作...]()
-
播放你的场景以查看角色正确地进行了动画处理。
它是如何工作的...
为角色制作动画需要许多步骤。首先,我们根据角色模型的原始骨骼结构创建了它的虚拟形象。然后,我们使用它自己的虚拟形象设置了动画片段(作为角色网格,存储在.fbx文件中),调整了动画片段,固定其大小并使其循环。我们还烘焙了其根变换旋转以遵守原始文件的方向。最后,创建了一个动画控制器,并将编辑后的动画片段设置为默认的动画状态。
虚拟形象的概念使得 Mecanim 非常灵活。一旦你有了控制器,你就可以将其应用于其他类人角色,只要它们有虚拟形象身体蒙版。如果你想亲自尝试,导入mascot.fbx文件,它也位于charater_and_clips文件夹内,将步骤 3 和 4 应用到这个角色上,将其放置在场景中,并在动画器组件中将MainCharacter设置为它的控制器。然后,播放场景以查看吉祥物正在播放rifle_aiming_idle动画片段。
更多内容...
要了解更多关于动画控制器的信息,请查看 Unity 的文档docs.unity3d.com/Manual/class-AnimatorController.html。
使用根运动和混合树移动你的角色
Mecanim 动画系统能够将根运动应用于角色。换句话说,它实际上根据动画片段移动角色,而不是在播放原地动画循环时任意转换角色模型。这使得大多数 Mixamo 动画片段非常适合与 Mecanim 一起使用。
动画系统的另一个功能是混合树,它可以平滑且容易地混合动画片段。在这个配方中,我们将利用这些功能使我们的角色向前和向后行走/奔跑,并在不同速度下向右和向左横移。
准备工作
对于这个配方,我们准备了一个名为Character_02的 Unity 包,其中包含一个角色和一个基本的动画控制器。该包位于1362_07_02文件夹中,包括必要的动画片段的.fbx文件。
如何操作...
要使用混合树将根运动应用于你的角色,请按照以下步骤操作:
-
将
Character_02.unityPackage导入到新项目中。同时导入Swat@rifle_run, Swat@run_backwards, Swat@strafe, Swat@strafe_2, Swat@strafe_left, Swat@strafe_right, Swat@walking,和 Swat@walking_backwards .fbx文件。 -
我们需要配置我们的动画剪辑。从Project视图下,选择Swat@rifle_run。
-
激活Rig部分。将Animation Type更改为Humanoid,将Avatar Definition更改为Create From this Model。通过点击Apply进行确认。
![如何操作...]()
-
现在,激活Animations部分(位于Rig右侧)。从Clips列表中选择rifle_run剪辑。在Inspector视图底部的Preview area将显示消息No model is available for preview. Please drag a model into this Preview area。将MsLaser@T-Pose拖动到Preview区域以纠正此问题。
-
在Clips列表中选择rifle_run后,选择rifle_run剪辑(从Clips列表中),并勾选Loop Time和Loop Pose选项。同时,点击Clamp Range按钮以调整时间线到动画剪辑的实际时间。
-
然后,在Root Transform Rotation下,勾选Bake Into Pose,并选择Baked Upon (at Start) | Original。在Root Transform Position (Y)下,勾选Bake Into Pose,并选择Baked Upon | Original。在Root Transform Position (XZ)下,不勾选Bake Into Pose,并选择Baked Upon (at Start) | Center of Mass。最后,点击Apply以确认更改。
![如何操作...]()
-
对以下每个动画剪辑重复步骤 3 到 6:Swat@run_backwards,Swat@strafe,Swat@strafe_2,Swat@strafe_left,Swat@strafe_right,Swat@walking,和Swat@walking_backwards。
-
从Project视图下,选择MsLaser预制体并将其拖动到Hierarchy视图,放置到场景中。
-
从Hierarchy视图下,选择MsLaserGameObject,并为其附加一个Character Controller组件(menu Component | Physics | Character Controller)。然后,将其Skin Width设置为
0.0001,其Center设置为X: 0,Y: 0.9,Z: 0;同时将其Radius更改为0.34,其Height更改为1.79。![如何操作...]()
-
在Project视图下,打开MainCharacter控制器。
-
在Animator视图的右上角,激活Parameters部分,并使用+符号创建三个新的Parameters (Float),分别命名为
xSpeed,zSpeed和Speed。 -
我们确实为我们的角色提供了一个Idle状态,但我们需要新的状态。在网格区域上右键单击,从上下文菜单中导航到Create State | From New Blend Tree。从Inspector视图更改其名称为
Move。![如何操作...]()
-
双击移动状态。你会看到你创建的空混合树。选择它,在检查器视图中将其重命名为
Move。然后,将其混合类型更改为2D 自由形式方向,也在参数选项卡中设置xSpeed和zSpeed。最后,使用Motion列表底部的+号添加九个新的运动字段。![如何做...]()
-
现在,将以下运动剪辑及其相应的Pos X和Pos Y值填充到Motion列表中:run_backwards,
0,-1;walking_backwards,0,-0.5;rifle_aiming_idle,0,0;walking,0,0.5;rifle_run,0,1;strafe,-1,0;strafe_left,-0.5,0;strafe_right,0.5,0;strafe_2,1,0。你可以通过从列表中选择它来填充Motion列表,或者如果有多个具有相同名称的剪辑,你可以将其从项目视图拖动到槽位(通过展开相应的模型图标)上。![如何做...]()
-
双击网格区域,从移动混合树回到基础层。
-
由于我们在移动混合树中有
rifle_aiming_idle运动剪辑,我们可以删除原始的空闲状态。右键单击空闲状态框,从菜单中选择删除。移动混合状态将成为新的默认状态,变为橙色。![如何做...]()
-
现在,我们必须创建一个脚本,将玩家的输入转换为创建来控制动画的变量。
-
从项目视图创建一个新的C# 脚本,并将其命名为
BasicController。 -
打开你的脚本,将其替换为以下代码:
using UnityEngine; using System.Collections; public class BasicController: MonoBehaviour { private Animator anim; private CharacterController controller; public float transitionTime = .25f; private float speedLimit = 1.0f; public bool moveDiagonally = true; public bool mouseRotate = true; public bool keyboardRotate = false; void Start () { controller = GetComponent<CharacterController>(); anim = GetComponent<Animator>(); } void Update () { if(controller.isGrounded){ if (Input.GetKey (KeyCode.RightShift) ||Input.GetKey (KeyCode.LeftShift)) speedLimit = 0.5f; else speedLimit = 1.0f; float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); float xSpeed = h * speedLimit; float zSpeed = v * speedLimit; float speed = Mathf.Sqrt(h*h+v*v); if(v!=0 && !moveDiagonally) xSpeed = 0; if(v!=0 && keyboardRotate) this.transform.Rotate(Vector3.up * h, Space.World); if(mouseRotate) this.transform.Rotate(Vector3.up * (Input.GetAxis("Mouse X")) * Mathf.Sign(v), Space.World); anim.SetFloat("zSpeed", zSpeed, transitionTime, Time.deltaTime); anim.SetFloat("xSpeed", xSpeed, transitionTime, Time.deltaTime); anim.SetFloat("Speed", speed, transitionTime, Time.deltaTime); } } } -
保存你的脚本并将其附加到MsLaser游戏对象上,在层次结构视图中。然后,添加平面(菜单选项GameObject | 3D Object | Plane),并将其放置在角色下方。
-
演示你的场景并测试游戏。你可以使用箭头键(或WASD键)来控制你的角色。按住Shift键会减慢速度。
它是如何工作的...
当BasicController脚本检测到任何方向键被使用时,它会将Animator状态的Speed变量设置为大于 0 的值,将Animator状态从空闲变为移动。移动状态反过来会根据xSpeed(从水平轴输入获取,通常是A和D键)和zSpeed(从垂直轴输入获取,通常是W和S键)的输入值混合它所填充的运动剪辑。由于 Mecanim 能够将根运动应用于角色,我们的角色实际上会沿着结果方向移动。
例如,如果按下W和D键,xSpeed和zSpeed值将增加到 1.0。从检查器视图可以看到,这种组合将导致名为rifle_run和strafe_2的运动剪辑之间的混合,使角色以对角线(前+右)的方式奔跑。

我们的BasicController包括三个复选框以提供更多选项:对角移动—默认设置为true,允许在向前/向后和向左/向右剪辑之间进行混合;鼠标旋转—默认设置为true,允许使用鼠标旋转角色,在移动时改变方向;键盘旋转—默认设置为false,允许通过同时使用左右和向前/向后方向键来旋转角色。
更多内容...
我们使用的混合树使用了2D 自由形式方向混合类型。然而,如果我们只有四个动画剪辑(向前、向后、向左和向右),2D 简单方向将是一个更好的选择。更多信息请查看以下链接:
-
在 Unity 文档中了解更多关于混合树和 2D 混合的信息,请访问:
docs.unity3d.com/Manual/BlendTree-2DBlending.html。 -
此外,如果您想了解更多关于 Mecanim 动画系统,有一些链接您可以查看,例如 Unity 的文档:
docs.unity3d.com/Manual/AnimationOverview.html。 -
Mecanim 示例场景可在 Unity Asset Store 中找到:
www.assetstore.unity3d.com/en/#!/content/5328。 -
Mecanim 视频教程可在:
unity3d.com/pt/learn/tutorials/topics/animation找到。
使用层和遮罩混合动画
混合动画是向您的动画角色添加复杂性的好方法,而无需大量动画剪辑。使用层和遮罩,我们可以通过播放特定身体部分的特定剪辑来组合不同的动画。在这个菜谱中,我们将应用这种技术到我们的动画角色上,触发发射步枪和投掷手榴弹的动画剪辑,同时根据玩家的输入保持下半身移动或空闲。
准备工作
对于这个菜谱,我们准备了一个名为Mixing的 Unity 包,其中包含一个具有动画角色的基本场景。该包位于1362_07_03文件夹中,包括名为Swat@firing_rifle.fbx和Swat@toss_grenade.fbx的动画剪辑。
如何操作...
要使用层和遮罩混合动画,请按照以下步骤操作:
-
创建一个新的项目并导入
MixingUnity 包。然后,从项目视图中打开mecanimPlayground级别。 -
将
Swat@firing_rifle.fbx和Swat@toss_grenade.fbx文件导入到项目中。 -
我们需要配置动画剪辑。从项目视图,选择Swat@firing_rifle动画剪辑。
-
激活绑定部分。将动画类型更改为人类,并将头像定义更改为从此模型创建。通过点击应用来确认更改。
![如何操作...]()
-
现在,激活动画部分。从剪辑列表中选择firing_rifle剪辑,点击限制范围按钮调整时间轴,并勾选循环时间和循环姿态选项。在根变换旋转下,勾选烘焙到姿态,并选择烘焙于|原始。在根变换位置(Y)下,勾选烘焙到姿态,并选择烘焙于(起始处)|原始。在根变换位置(XZ)下,取消勾选烘焙到姿态。点击应用以确认更改。
![如何操作...]()
-
选择Swat@toss_grenade动画剪辑。激活绑定部分。然后,将动画类型更改为人类,并将头像定义更改为从此模型创建。通过点击应用来确认更改。
-
现在,激活动画部分。从剪辑列表中选择toss_grenade剪辑,点击限制范围按钮调整时间轴,并取消选中循环时间和循环姿态选项。在根变换旋转下,勾选烘焙到姿态,并选择烘焙于(起始处)|原始。在根变换位置(Y)下,勾选烘焙到姿态,并选择烘焙于(起始处)|原始)。在根变换位置(XZ)下,取消勾选烘焙到姿态。点击应用以确认更改。
-
让我们创建一个面具。从项目视图,点击创建按钮,并将头像面具添加到项目中。将其命名为BodyMask。
-
选择BodyMask选项卡,并在检查器视图中展开人类部分以取消选择角色的腿部、基础和IK点,使它们的轮廓变红。
![如何操作...]()
-
从层次结构视图,选择MsLaser角色。然后,从检查器视图中的动画器组件,双击MainCharacter控制器以打开它。
-
在动画器视图中,通过点击左上角的+号在图层选项卡上创建一个新图层,位于基础图层上方。
-
将新图层命名为UpperBody,并点击齿轮图标进行设置。然后,将其权重更改为
1,并在遮罩槽中选择BodyMask。此外,将混合模式更改为加法。![如何操作...]()
-
现在,在动画器视图中,选择UpperBody层,创建三个新的空状态(通过右键点击网格区域并从菜单中选择创建状态 | 空)。将默认(橙色)状态命名为null,其他两个命名为Fire和Grenade。
-
现在,访问参数选项卡并添加两个布尔类型的新的参数:
Fire和Grenade。![如何操作...]()
-
选择Fire状态,并在检查器视图中将firing_rifle动画片段添加到运动字段。
![如何操作...]()
-
现在,选择Grenade状态,并在检查器视图中,将toss_grenade动画片段添加到运动字段。
-
右键点击null状态框,然后从菜单中选择创建转换。接着,将白色箭头拖动到Fire框上。
-
选择箭头(它将变为蓝色)。从检查器视图中取消勾选Has Exit Time选项。然后,访问条件列表,点击加号+添加一个新条件,并将其设置为Fire和true。
![如何操作...]()
-
现在,从null到Grenade创建转换。选择箭头(它将变为蓝色)。从检查器视图中取消勾选Has Exit Time选项。然后,访问条件列表,点击加号+添加一个新条件,并将其设置为Grenade和true。
-
现在,从Fire到null,以及从Grenade到null创建转换。然后,选择从Fire到null的箭头,并在条件框中选择Fire和false选项。保留Has Exit Time选项的勾选状态。
-
最后,选择从Grenade到null的箭头。在条件框中,选择Grenade和false选项。保留Has Exit Time选项的勾选状态。
![如何操作...]()
-
从层次视图中,选择MsLaser角色。在检查器视图中找到基本控制器组件并打开其脚本。
-
在
Update()函数结束前立即添加以下代码:if(Input.GetKeyDown(KeyCode.F)){ anim.SetBool("Grenade", true); } else { anim.SetBool("Grenade", false); } if(Input.GetButtonDown("Fire1")){ anim.SetBool("Fire", true); } if(Input.GetButtonUp("Fire1")){ anim.SetBool("Fire", false); } -
保存脚本并播放场景。您可以通过点击fire按钮并按F键来触发firing_rifle和toss_grenade动画。观察角色的腿部仍然对Move动画状态做出反应。
它是如何工作的...
一旦创建了 Avatar 面具,它可以用作过滤实际播放特定层动画状态的身体部分的方式。在我们的例子中,我们将fire_rifle和toss_grenade动画片段限制在角色的上半身,使下半身可以自由播放与运动相关的动画片段,例如行走、跑步和侧滑。
还有更多...
你可能已经注意到 UpperBody 层有一个名为 Blending 的参数,我们将其设置为 Additive。这意味着该层的动画状态将添加到下层的状态中。如果将其更改为 Override,则当播放时,该层的动画将覆盖下层的动画状态。在我们的例子中,Additive 有助于在跑步时保持瞄准稳定。
更多关于 Animation Layers 和 Avatar Body Masks 的信息,请查看 Unity 的文档,链接为 docs.unity3d.com/Manual/AnimationLayers.html 和 docs.unity3d.com/Manual/class-AvatarMask.html。
将状态组织到子状态机中
当动画区域变得过于杂乱时,你总是可以考虑将你的动画状态组织到子状态机中。在这个食谱中,我们将使用这种技术来组织角色的转向动画状态。此外,由于提供的动画剪辑不包括根运动,我们将利用这个机会通过脚本说明如何克服根运动的不足,使用它使角色向左和向右转动 45 度。

准备工作
对于这个食谱,我们准备了一个名为 Turning 的 Unity 包,其中包含一个具有动画角色的基本场景。该包位于 1362_07_04 文件夹中,还包括名为 Swat@turn_right_45_degrees.fbx 和 Swat@turn_left.fbx 的动画剪辑。
如何操作...
要通过脚本应用根运动,请按照以下步骤操作:
-
创建一个新的项目并导入
TurningUnity 包。然后,从 Project 视图打开 mecanimPlayground 级别。 -
在项目中导入
Swat@turn_right_45_degrees.fbx和Swat@turn_left.fbx文件。 -
我们需要配置我们的动画剪辑。从 Project 视图中选择 Swat@turn_left 文件。
-
激活 Rig 部分。将 Animation Type 更改为 Humanoid,并将 Avatar Definition 更改为 Create From this Model。通过点击 Apply 来确认。
-
现在,激活 Animations 部分。从 Clips 列表中选择 turn_left 剪辑,点击 Clamp Range 按钮调整时间线,并勾选 Loop Time 选项。在 Root Transform Rotation 下,勾选 Bake Into Pose,并导航到 Baked Upon (at Start) | Original。在 Root Transform Position (Y) 下,勾选 Bake Into Pose,并选择 Baked Upon (at Start) | Original。在 Root Transform Position (XZ) 下,不勾选 Bake Into Pose。点击 Apply 以确认更改。
![如何操作...]()
-
对于 Swat@turning_right_45_degrees,重复步骤 4 和 5。
-
从 Hierarchy 视图中选择 MsLaser 角色。然后,从 Inspector 视图中的 Animator 组件打开 MainCharacter 控制器。
-
从动画器视图的左上角激活参数部分,并使用+符号创建两个新的参数(布尔值)命名为
TurnLeft和TurnRight。 -
右键单击网格区域。从上下文菜单中选择创建子状态机。在检查器视图中,将其重命名为
Turn。![如何操作...]()
-
双击向右转子状态机。在网格区域上右键单击,选择创建状态 | 空,并添加一个新状态。将其重命名为
向左转。然后,添加另一个名为向右转的状态。 -
在检查器视图中,将
Turn Left填充为turn_left动作剪辑。然后,将Turn Right填充为turning_right_45_degrees。![如何操作...]()
-
从向右转子状态机退出回到基础层。通过在每个状态上右键单击并选择创建转换选项,在移动和向左转,以及移动和向右转之间创建转换。
![如何操作...]()
-
进入向右转子状态机。然后,从向左转和向右转创建到移动状态的转换。
![如何操作...]()
-
选择从向右转到(向上)基础层的箭头。它将变为蓝色。从检查器视图中取消选中具有退出时间选项。然后,访问条件列表,点击+符号添加一个新条件,并将其设置为向右转和false。
![如何操作...]()
-
选择从(向上)基础层到向右转的箭头。在检查器视图中,取消选中具有退出时间选项。然后,访问条件列表,点击+符号添加一个新条件,并将其设置为向右转和true。
-
使用向左转作为条件,重复步骤 14 和 15,使用(向上)基础层和向左转之间的箭头。
-
在层次结构视图中,选择MsLaser角色。然后,从检查器视图中打开BasicController组件的脚本。
-
在
if(controller.isGrounded){行之后立即添加:if(Input.GetKey(KeyCode.Q)){ anim.SetBool("TurnLeft", true); transform.Rotate(Vector3.up * (Time.deltaTime * -45.0f), Space.World); } else { anim.SetBool("TurnLeft", false); } if(Input.GetKey(KeyCode.E)){ anim.SetBool("TurnRight", true); transform.Rotate(Vector3.up * (Time.deltaTime * 45.0f), Space.World); } else { anim.SetBool("TurnRight", false); } -
保存您的脚本。然后,选择MsLaser角色,并从检查器视图中访问基本控制器组件。取消选中对角移动和鼠标旋转选项。同时,保留键盘旋转选项选中。最后,播放场景。您可以使用Q键向左转,使用E键向右转。
它是如何工作的...
如从配方中可以清楚地看出,子状态机的工作方式与组或文件夹类似,允许您将一系列状态机封装成一个单一实体,以便更容易引用。子状态机中的状态可以从外部状态转换,在我们的例子中,是移动状态,甚至可以从不同的子状态机转换。
关于角色的旋转,我们通过使用transform.Rotate(Vector3.up * (Time.deltaTime * -45.0f), Space.World);命令来克服根运动的不足,使角色在按下Q和E键时实际上能够转身。这个命令与animator.SetBool("TurnLeft", true);一起使用,触发正确的动画剪辑。
通过脚本转换角色控制器
将Root Motion应用于你的角色可能是一种非常实用且准确的方式来动画化它。然而,时不时地,你可能需要手动控制角色运动的一两个方面。也许你只有原地动画可以操作,或者你可能想让角色的运动受到其他变量的影响。在这些情况下,你需要通过脚本覆盖根运动。
为了说明这个问题,这个配方使用了一个跳跃动画剪辑,它最初只沿 Y 轴移动角色。为了让她在跳跃时向前或向后移动,我们将学习如何通过脚本访问角色的速度来通知跳跃的方向。

准备工作
对于这个配方,我们准备了一个名为Jumping的 Unity 包,其中包含一个具有动画角色的基本场景。该包位于1362_07_05文件夹中,包括名为Swat@rifle_jump的动画剪辑。
如何做到这一点...
要通过脚本应用根运动,请按照以下步骤操作:
-
创建一个新的项目并导入
JumpingUnity 包。然后,从Project视图中打开mecanimPlayground级别。 -
将
Swat@rifle_jump.fbx文件导入到项目中。 -
我们需要配置我们的动画剪辑。从Project视图中选择Swat@rifle_jump文件。
-
激活Rig部分。将Animation Type更改为Humanoid,并将Avatar Definition设置为Create From this Model。通过点击Apply来确认这一设置。
-
现在,激活Animations部分。从Clips列表中选择rifle_jump剪辑,点击Clamp Range按钮调整时间轴,并检查Loop Time和Loop Pose选项。在Root Transform Rotation下,检查Bake Into Pose,并选择Baked Upon (at Start) | Original。在Root Transform Position (Y)下,不勾选Bake into Pose,并选择Baked Upon (at Start) | Original。在Root Transform Position (XZ)下,不勾选Bake Into Pose。点击Apply以确认更改。
![如何做到这一点...]()
-
从Hierarchy视图中选择MsLaser角色。然后,从Inspector视图中的Animator组件,打开MainCharacter控制器。
-
从Animator视图的左上角,激活Parameters部分,并使用+符号创建一个新的Parameters (Boolean)名为
Jump。 -
右键单击网格区域,从上下文菜单中选择创建状态 | 空。从检查器视图更改其名称为
跳跃。![如何操作...]()
-
选择跳跃状态。然后,从检查器视图填充它,使用rifle_jump运动剪辑。
![如何操作...]()
-
找到并右键单击任何状态。然后,从检查器视图中选择创建转换选项,从任何状态到跳跃创建一个转换。选择转换,取消选中有退出时间,并使用跳跃变量作为条件(true)。
-
现在,从跳跃到移动创建一个转换。
![如何操作...]()
-
配置跳跃和移动之间的转换,保留有退出时间选中,并使用跳跃变量作为条件(false)。
![如何操作...]()
-
从层次结构视图中选择MsLaser角色。然后,从检查器视图打开BasicController组件的脚本。
-
在
Start()函数之前立即添加以下代码:public float jumpHeight = 3f; private float verticalSpeed = 0f; private float xVelocity = 0f; private float zVelocity = 0f; -
在
Update()函数内部,找到包含以下代码的行:if(controller.isGrounded){然后在它之后立即添加以下行:
if (Input.GetKey (KeyCode.Space)) { anim.SetBool ("Jump", true); verticalSpeed = jumpHeight; } -
最后,在代码的最后一个
}之前立即添加一个新函数:void OnAnimatorMove(){ Vector3 deltaPosition = anim.deltaPosition; if (controller.isGrounded) { xVelocity = controller.velocity.x; zVelocity = controller.velocity.z; } else { deltaPosition.x = xVelocity * Time.deltaTime; deltaPosition.z = zVelocity * Time.deltaTime; anim.SetBool ("Jump", false); } deltaPosition.y = verticalSpeed * Time.deltaTime; controller.Move (deltaPosition); verticalSpeed += Physics.gravity.y * Time.deltaTime; if ((controller.collisionFlags & CollisionFlags.Below) != 0) { verticalSpeed = 0; } } -
保存你的脚本并播放场景。你将能够使用空格键跳跃。观察角色的速度如何影响跳跃的方向。
工作原理...
注意,一旦将此函数添加到脚本中,Animator组件中的应用根运动字段将从勾选框变为由脚本处理。原因是,为了覆盖动画剪辑的原始运动,我们在 Unity 的OnAnimatorMove()函数中放置了一系列命令来移动我们的角色控制器,使其在跳跃时移动。代码行:controller.Move (deltaPosition);基本上用deltaPosition 3D 向量替换了跳跃的方向,该向量由跳跃前的瞬间角色的速度(x和z-轴)以及jumpHeight变量和重力力的计算(y-轴)组成。
将刚体属性添加到动画角色中
如果你在建模和动画化角色时没有包含足够多的属性,你可能希望给她在运行时收集新属性的机会。在这个菜谱中,我们将学习如何实例化一个 GameObject 并将其分配给一个角色,同时尊重动画层次结构。
准备工作
对于这个菜谱,我们准备了一个名为Props的 Unity 包,其中包含一个基本场景,该场景包含一个动画角色和一个名为徽章的预制件。该包位于1362_07_06文件夹中。
如何操作...
要在运行时将刚体属性添加到动画角色中,请按照以下步骤操作:
-
创建一个新的项目并导入
PropsUnity 包。然后,从项目视图打开mecanimPlayground级别。 -
从项目视图,通过将其拖放到层次结构视图来将徽章道具添加到场景中。然后,将其设置为mixamorig:Spine2变换的子对象(使用层次结构树导航到MsLaser | mixamorig:Hips | mixamorig:Spine | mixamorig:Spine1 | mixamorig:Spine2)。然后,通过将变换位置更改为X:
-0.08,Y:0,Z:0.15;以及旋转更改为X:0.29,Y:0.14,Z:-13.29,使徽章对象在角色的胸部上方可见。![如何操作...]()
-
记录位置和旋转值,并从场景中删除徽章对象。
-
在场景中添加一个新的立方体(下拉创建 | 3D 对象 | 立方体),重命名为PropTrigger,并将其位置更改为X:
0,Y:0.5,Z:2。 -
从检查器视图的盒子碰撞器组件中,勾选是触发器选项。
-
从项目视图创建一个新的C# 脚本,命名为
AddProp.cs。 -
打开脚本并添加以下代码:
using UnityEngine; using System.Collections; public class AddProp : MonoBehaviour { public GameObject prop; public Transform targetBone; public Vector3 positionOffset; public Vector3 rotationOffset; public bool destroyTrigger = true; void OnTriggerEnter ( Collider collision ){ if (targetBone.IsChildOf(collision.transform)){ bool checkProp = false; foreach(Transform child in targetBone){ if (child.name == prop.name) checkProp = true; } if(!checkProp){ GameObject newprop; newprop = Instantiate(prop, targetBone.position, targetBone.rotation) as GameObject; newprop.name = prop.name; newprop.transform.parent = targetBone; newprop.transform.localPosition += positionOffset; newprop.transform.localEulerAngles += rotationOffset; if(destroyTrigger) Destroy(gameObject); } } } } -
保存并关闭脚本。
-
将AddProp.cs脚本附加到PropTrigger游戏对象。
-
选择PropTrigger文本框并查看其添加道具组件。首先,将道具字段填充为徽章预制体。然后,将目标骨骼填充为mixamorig:Spine2变换。最后,将我们之前记录的位置和旋转值分别分配给位置偏移和旋转偏移字段(位置偏移:X:
-0.08,Y:0,Z:0.15;旋转偏移:X:0.29,Y:0.14,Z:-13.29)。![如何操作...]()
-
演示场景。使用“WASD”键盘控制方案,将角色引导到PropTrigger文本框。与之碰撞将为角色添加徽章。
![如何操作...]()
它是如何工作的...
一旦被角色触发,附加到PropTrigger的脚本将实例化指定的预制体,使其成为放置其中的骨骼的子对象。位置偏移和旋转偏移可用于微调道具的确切位置(相对于其父变换)。当道具成为动画角色的骨骼的父对象时,它们将跟随并尊重其层次结构和动画。请注意,脚本在实例化新对象之前会检查是否存在同名的前置道具。
更多...
你可以创建一个类似的脚本以移除道具。在这种情况下,OnTriggerEnter函数将只包含以下代码:
if (targetBone.IsChildOf(collision.transform)){
foreach(Transform child in targetBone){
if (child.name == prop.name)
Destroy (child.gameObject);
}
}
使用动画事件抛出对象
现在动画角色已经准备好了,你可能想协调她的一些动作与她的动画状态。在这个配方中,我们将通过使角色在适当的动画剪辑达到正确的时间时扔一个对象来举例说明这一点。为此,我们将利用动画事件,它基本上会从动画剪辑的时间线中触发一个函数。这个功能是最近添加到Mecanim系统中的,对于那些熟悉经典动画面板的添加事件功能的用户来说应该很熟悉。

准备工作
对于这个配方,我们准备了一个名为Throwing的 Unity 包,其中包含一个基本场景,包含一个动画角色和一个名为EasterEgg的预制件。该包位于1362_07_07文件夹中。
如何操作...
要使动画角色扔一个复活节彩蛋(!),请按照以下步骤操作:
-
创建一个新的项目并导入
ThrowingUnity 包。然后,从项目视图中,打开mecanimPlayground关卡。 -
播放关卡并按键盘上的F键。角色将像用右手扔东西一样移动。
-
从项目视图中,创建一个新的C#脚本,命名为
ThrowObject.cs。 -
打开脚本并添加以下代码:
using UnityEngine; using System.Collections; public class ThrowObject : MonoBehaviour { public GameObject prop; private GameObject proj; public Vector3 posOffset; public Vector3 force; public Transform hand; public float compensationYAngle = 0f; public void Prepare () { proj = Instantiate(prop, hand.position, hand.rotation) as GameObject; if(proj.GetComponent<Rigidbody>()) Destroy(proj.GetComponent<Rigidbody>()); proj.GetComponent<SphereCollider>().enabled = false; proj.name = "projectile"; proj.transform.parent = hand; proj.transform.localPosition = posOffset; proj.transform.localEulerAngles = Vector3.zero; } public void Throw () { Vector3 dir = transform.rotation.eulerAngles; dir.y += compensationYAngle; proj.transform.rotation = Quaternion.Euler(dir); proj.transform.parent = null; proj.GetComponent<SphereCollider>().enabled = true; Rigidbody rig = proj.AddComponent<Rigidbody>(); Collider projCollider = proj.GetComponent<Collider> (); Collider col = GetComponent<Collider> (); Physics.IgnoreCollision(projCollider, col); rig.AddRelativeForce(force); } } -
保存并关闭脚本。
-
将ThrowObject.cs脚本附加到名为MsLaser的角色 GameObject 上。
-
选择MsLaser对象。从检查器视图中,检查其投掷对象组件。然后,将名为EasterEgg的预制件填充到道具字段中。将手设置为mixamorig:RightHand。此外,将位置偏移更改为X:
0;Y:0.07;Z:0.04。最后,将力更改为X:0;Y:200;Z:500。![如何操作...]()
-
从项目视图中,选择Swat@toss_grenade文件。然后,从检查器视图中,访问动画部分并滚动到事件部分。
-
展开事件部分。将播放头拖动到大约0:17 (017.9%)的动画时间轴上。然后,点击带有marker + 图标的按钮以添加一个动画事件。从编辑动画事件窗口,将函数设置为
Prepare。关闭窗口。![如何操作...]()
-
在动画时间轴的大约1:24 (057.1%)处添加一个新的动画事件。这次,从编辑动画事件窗口,将函数设置为
Throw。关闭窗口。 -
点击应用按钮以保存更改。
-
播放你的场景。现在,当你按下F键时,角色将能够扔一个复活节彩蛋。
它是如何工作的...
当toss_grenade动画达到我们设置的事件时刻时,将调用Prepare()和throw()函数。前者将一个预制体实例化,现在命名为projectile,放置到角色的手中(使用Projectile Offset值来微调其位置),同时也使其尊重角色的层次结构。此外,它禁用了预制体的碰撞器并销毁了其Rigidbody组件(如果有的话)。后者函数启用了 projectile 的碰撞器,并为其添加了一个Rigidbody组件,使其独立于角色的手。最后,它向 projectile 的Rigidbody组件添加了一个相对力,使其表现得像被角色投掷出去。Compensation YAngle可以用来调整手榴弹的方向,如果需要的话。
将 Ragdoll 物理应用于角色
动作游戏通常利用ragdoll 物理来模拟角色在受到打击或爆炸无意识影响下的身体反应。在本教程中,我们将学习如何设置并激活 ragdoll 物理,以便角色在踏入地雷物体时触发。我们还将利用这个机会在事件发生后几秒钟重置角色的位置和动画。
准备工作
对于这个教程,我们准备了一个名为Ragdoll的 Unity 包,其中包含一个基本场景,包含一个动画角色和两个预制体,已经放置在场景中,分别命名为Landmine和Spawnpoint。该包位于1362_07_08文件夹中。
如何操作...
要将 Ragdoll 物理应用于你的角色,请按照以下步骤操作:
-
创建一个新的项目并导入
RagdollUnity 包。然后,从项目视图中打开mecanimPlayground级别。 -
你将看到动画的 MsLaser 角色和两个圆盘:Landmine和Spawnpoint。
-
首先,让我们设置我们的Ragdoll。访问游戏对象 | 3D 对象 | Ragdoll...菜单,Ragdoll 向导将弹出。
-
按以下方式分配变换:
-
骨盆:mixamorig:Hips
-
左臀:mixamorig:LeftUpLeg
-
左膝:mixamorig:LeftLeg
-
左脚:mixamorig:LeftFoot
-
右臀:mixamorig:RightUpLeg
-
右膝:mixamorig:RightLeg
-
右脚:mixamorig:RightFoot
-
左臂:mixamorig:LeftArm
-
左肘:mixamorig:LeftForeArm
-
右臂:mixamorig:RightArm
-
右肘:mixamorig:RightForeArm
-
中脊:mixamorig:Spine1
-
头部:mixamorig:Head
-
总质量:20
-
力量:50
![如何操作...]()
-
-
从项目视图中,创建一个新的C# 脚本,命名为
RagdollCharacter.cs。 -
打开脚本并添加以下代码:
using UnityEngine; using System.Collections; public class RagdollCharacter : MonoBehaviour { void Start () { DeactivateRagdoll(); } public void ActivateRagdoll(){ gameObject.GetComponent<CharacterController> ().enabled = false; gameObject.GetComponent<BasicController> ().enabled = false; gameObject.GetComponent<Animator> ().enabled = false; foreach (Rigidbody bone in GetComponentsInChildren<Rigidbody>()) { bone.isKinematic = false; bone.detectCollisions = true; } foreach (Collider col in GetComponentsInChildren<Collider>()) { col.enabled = true; } StartCoroutine (Restore ()); } public void DeactivateRagdoll(){ gameObject.GetComponent<BasicController>().enabled = true; gameObject.GetComponent<Animator>().enabled = true; transform.position = GameObject.Find("Spawnpoint").transform.position; transform.rotation = GameObject.Find("Spawnpoint").transform.rotation; foreach(Rigidbody bone in GetComponentsInChildren<Rigidbody>()){ bone.isKinematic = true; bone.detectCollisions = false; } foreach (CharacterJoint joint in GetComponentsInChildren<CharacterJoint>()) { joint.enableProjection = true; } foreach(Collider col in GetComponentsInChildren<Collider>()){ col.enabled = false; } gameObject.GetComponent<CharacterController>().enabled= true; } IEnumerator Restore(){ yield return new WaitForSeconds(5); DeactivateRagdoll(); } } -
保存并关闭脚本。
-
将RagdollCharacter.cs脚本附加到MsLaser游戏对象上。然后,选择MsLaser角色,从检查器视图的顶部,将其标签更改为Player。
-
从项目视图中,创建一个新的C# 脚本,命名为
Landmine.cs。 -
打开脚本并添加以下代码:
using UnityEngine; using System.Collections; public class Landmine : MonoBehaviour { public float range = 2f; public float force = 2f; public float up = 4f; private bool active = true; void OnTriggerEnter ( Collider collision ){ if(collision.gameObject.tag == "Player" && active){ active = false; StartCoroutine(Reactivate()); collision.gameObject.GetComponent<RagdollCharacter>().ActivateRagdoll(); Vector3 explosionPos = transform.position; Collider[] colliders = Physics.OverlapSphere(explosionPos, range); foreach (Collider hit in colliders) { if (hit.GetComponent<Rigidbody>()) hit.GetComponent<Rigidbody>().AddExplosionForce(force, explosionPos, range, up); } } } IEnumerator Reactivate(){ yield return new WaitForSeconds(2); active = true; } } -
保存并关闭脚本。
-
将脚本附加到 Landmine GameObject。
-
演示场景。使用 WASD 键盘控制方案,将角色引导至 Landmine GameObject。与之碰撞将激活角色的 Ragdoll 物理效果,并对其施加爆炸力。因此,角色将被抛出相当远的距离,并且将不再受其身体运动的控制,就像一个 Ragdoll。
它是如何工作的...
Unity 的 Ragdoll Wizard 将 Collider、Rigidbody 和 Character Joint 组件分配给选定的变换。这些组件共同作用,使得 Ragdoll 物理成为可能。然而,当我们想要我们的角色被动画化和由玩家控制时,这些组件必须被禁用。在我们的例子中,我们使用 RagdollCharacter 脚本及其两个函数:ActivateRagdoll() 和 DeactivateRagdoll() 来开关这些组件,后者包括将我们的角色重新生成到适当位置的指令。
为了测试目的,我们还创建了一个 Landmine 脚本,该脚本调用 RagdollCharacter 脚本中的 ActivateRagdoll() 函数。它还将爆炸力应用到我们的 Ragdoll 角色上,将其抛出爆炸现场。
还有更多...
而不是重置角色的变换设置,你可以销毁其 GameObject,并在重生点使用 Tags 实例化一个新的 GameObject。有关此主题的更多信息,请参阅 Unity 的文档:docs.unity3d.com/ScriptReference/GameObject.FindGameObjectsWithTag.html。
将角色的躯干旋转以瞄准武器
当扮演第三人称角色时,你可能希望她瞄准她前方不直接的目标,而不改变她的方向。在这些情况下,你需要应用所谓的 过程动画,它不依赖于预制的动画剪辑,而是依赖于对其他数据的处理,例如玩家输入,以动画化角色。在这个菜谱中,我们将使用这种技术通过移动鼠标来旋转角色的脊柱,从而调整角色的瞄准。我们还将利用这个机会从角色的武器发射一条射线,并在目标上最近的对象上显示一个准星。请注意,这种方法适用于站在第三人称控制角色背后的摄像机。
准备中
对于这个菜谱,我们准备了一个名为 AimPointer 的 Unity 包,其中包含一个基本场景,场景中有一个装备激光指示器的角色。该包还包括用作瞄准准星的 crossAim 精灵,可以在 1362_07_09 文件夹中找到。
如何实现...
-
创建一个新的项目并导入
AimPointerUnity 包。然后,从项目视图打开mecanimPlayground级别。你会看到一个名为MsLaser的动画角色,它手持pointerPrefab对象。 -
从项目视图创建一个新的C# 脚本,命名为
MouseAim.cs。 -
打开脚本并添加以下代码:
using UnityEngine; using System.Collections; public class MouseAim : MonoBehaviour { public Transform spine; private float xAxis = 0f; private float yAxis = 0f; public Vector2 xLimit = new Vector2(-30f,30f); public Vector2 yLimit= new Vector2(-30f,30f); public Transform weapon; public GameObject crosshair; private Vector2 aimLoc; public void LateUpdate(){ yAxis += Input.GetAxis ("Mouse X"); yAxis = Mathf.Clamp (yAxis, yLimit.x, yLimit.y); xAxis -= Input.GetAxis ("Mouse Y"); xAxis = Mathf.Clamp (xAxis, xLimit.x, xLimit.y); Vector3 corr = new Vector3(xAxis,yAxis, spine.localEulerAngles.z); spine.localEulerAngles = corr; RaycastHit hit; Vector3 fwd = weapon.TransformDirection(Vector3.forward); if (Physics.Raycast (weapon.position, fwd, out hit)) { print (hit.transform.gameObject.name); aimLoc = Camera.main.WorldToScreenPoint(hit.point); crosshair.SetActive(true); crosshair.transform.position = aimLoc; } else { crosshair.SetActive(false); } Debug.DrawRay (weapon.position, fwd, Color.red); } } -
保存并关闭脚本。
-
从层次结构视图创建一个新的UI | Image游戏对象。然后,从检查器视图,将其名称更改为
crosshair。在矩形 变换中,将宽度和高度设置为16,并在源图像字段中填充crossAim精灵。![如何操作...]()
-
将
MouseAim.cs脚本附加到MsLaser游戏对象上。 -
选择MsLaser游戏对象,并从检查器视图的鼠标瞄准组件中,将脊柱字段填充为mixamorig:Spine;将武器字段填充为pointerPrefab;将十字准线字段填充为crosshair UI 游戏对象。
![如何操作...]()
-
播放场景。现在,你可以通过移动鼠标来旋转角色的躯干。更好的是,十字准线 GUI 纹理将显示在指针所指向的对象顶部。
![如何操作...]()
它是如何工作的...
你可能已经注意到,所有旋转角色脊柱的代码都在LateUpdate函数内部,而不是更常见的Update函数中。这样做的原因是为了确保所有的变换操作都会在原始动画剪辑播放之后执行,从而覆盖它。
关于脊柱旋转,我们的脚本将鼠标的水平速度和垂直速度添加到xAxis和yAxis浮点变量中。然后,这些变量被限制在指定的范围内,避免对角色模型的扭曲。最后,将spine对象变换旋转的x和y轴分别设置为xAxis和yAxis。z轴保留原始动画剪辑中的原始旋转。
此外,我们的脚本使用Raycast命令来检测武器瞄准范围内是否有任何对象的碰撞器,在这种情况下,屏幕上会绘制一个十字准线。
还有更多...
由于这个菜谱的脚本是为站在第三人称控制角色背后的摄像机定制的,因此我们为这个问题提供了一个更通用的解决方案——实际上,与Unity 4.x Cookbook,Packt Publishing中提出的方法类似。一个名为MouseAimLokkAt的替代脚本,可以在1362_07_09文件夹中找到,它首先将我们的二维鼠标光标屏幕坐标转换为三维世界空间坐标(存储在一个point变量中)。然后,它使用LookAt()命令将角色的躯干旋转到point位置。此外,它确保脊柱不会外推minY和maxY角度,否则会导致角色模型变形。另外,我们还包含了一个Compensation YAngle变量,使我们能够微调角色与鼠标光标的对齐。另一个新增功能是冻结 X 轴旋转,以防你只想让角色横向旋转躯干,而不向上或向下看。同样,这个脚本使用Raycast命令来检测武器瞄准前的物体,当它们存在时在屏幕上绘制一个十字准星。
第八章.角色 GameObject 的位置、移动和导航
在本章中,我们将涵盖:
-
玩家对 2D GameObject 的控制(以及限制在矩形内的移动)
-
玩家对 3D GameObject 的控制(以及限制在矩形内的移动)
-
选择目的地——找到最近的(或随机的)出生点
-
选择目的地——重生到最近通过的检查点
-
NPC NavMeshAgent 在寻找或逃离目的地的同时避开障碍物
-
NPC NavMeshAgent 按顺序跟随航点
-
通过集群控制对象组移动
简介
游戏中的许多 GameObject 都在移动!移动可以由玩家控制,由环境中的(模拟)物理定律控制,或者由非玩家角色(NPC)逻辑控制;例如,跟随路径上的航点、朝向(移动到)或逃离(远离)角色的当前位置的对象。Unity 提供了多个控制器,用于第一人称和第三人称角色,以及汽车和飞机等车辆。GameObject 的移动也可以通过 Unity Mecanim 动画系统的状态机来控制。
然而,可能有时你希望调整 Unity 中的玩家角色控制器,或者编写自己的控制器。你可能希望编写方向逻辑——简单或复杂的人工智能(AI)来控制游戏中的 NPC 和敌人角色。这种 AI 可能涉及你的计算机程序使对象朝向或远离角色或其他游戏对象。
本章介绍了一系列这样的方向性食谱,许多游戏可以从更丰富和更令人兴奋的用户体验中获得益处。
Unity 提供了包括 Vector3 类和刚体物理在内的复杂类和组件,用于在游戏中建模真实的移动、力和碰撞。我们利用这些游戏引擎功能来实现本章食谱中的一些复杂的 NPC 和敌人角色移动。
整体概念
对于 3D 游戏(以及在一定程度上,2D 游戏),一个基本的对象类是 Vector3 类——存储和操作代表 3D 空间中位置的(x,y,z)值的对象。如果我们从一个假想的箭头从原点(0,0,0)到空间中的一个点,那么这个箭头的方向和长度(向量)可以表示速度或力(即,在某个方向上的一定量的幅度)。
如果我们忽略 Unity 中的所有角色控制器组件、碰撞器和物理系统,我们可以编写代码将对象直接传送到场景中的特定(x, y, z)位置。有时这正是我们想要的;例如,我们可能希望在一个位置生成一个对象。然而,在大多数情况下,如果我们想让对象以更物理现实的方式移动,那么我们要么对对象施加力,要么改变其速度分量。或者如果它有一个角色控制器组件,那么我们可以发送一个Move()消息给它。随着 Unity NavMeshAgents(以及相关的导航网格)的引入,我们现在可以为具有 NavMeshAgent 的对象设置目的地,然后内置的路径查找逻辑将完成将我们的 NPC 对象沿着给定(x, y, z)目的地位置的路径移动的工作。
除了决定使用哪种技术来移动对象外,我们的游戏还必须决定如何选择目的地位置,或者移动方向和大小的变化。这可能涉及到逻辑,告诉 NPC 或敌人对象玩家的角色目的地(要移动到,然后在足够接近时攻击)。或者,可能害羞的 NPC 对象会被给予指向玩家角色的方向,这样它们就可以朝相反方向逃跑,直到它们到达一个安全距离。
NPC 对象移动和创建(实例化)的其他核心概念包括:
-
生成点
- 场景中对象要创建或移动到的特定位置
-
路标
- 定义 NPC 或玩家角色要遵循的路径的位置序列
-
检查点
- 位置(或碰撞器),一旦通过,就会改变游戏中的事件(例如,额外时间,或者如果玩家的角色被杀死,他们将在最后一个通过的检查点重生,等等)
玩家控制 2D 游戏对象(以及限制其在矩形内的移动)
虽然本章中的其余食谱都是在 3D 项目中演示的,但 2D 中的基本角色移动以及限制移动到边界矩形是许多 2D 游戏的核心技能,因此这个第一个食谱说明了如何为 2D 游戏实现这些功能。
由于在第三章
准备工作
这个食谱基于一个简单的 2D 游戏,称为Creating the Simple2DGame_SpaceGirl迷你游戏,来自第三章
-
复制corner_maxGameObject,并将其克隆命名为corner_min,然后将此克隆放置在player-spaceGirl1GameObject 下方和左侧的位置。这两个 GameObject 的坐标将确定玩家角色允许的最大和最小移动边界。
-
修改名为
PlayerMove的 C#脚本,在类开始处声明一些新的变量:public Transform corner_max; public Transform corner_min; private float x_min; private float y_min; private float x_max; private float y_max; -
修改名为
PlayerMove的 C#脚本,使Awake()方法现在获取 SpriteRenderer 的引用,并使用此对象来帮助设置最大和最小 X 和 Y 移动限制:void Awake(){ rigidBody2D = GetComponent<Rigidbody2D>(); x_max = corner_max.position.x; x_min = corner_min.position.x; y_max = corner_max.position.y; y_min = corner_min.position.y; } -
修改名为
PlayerMove的 C#脚本,声明一个新的方法,名为KeepWithinMinMaxRectangle():private void KeepWithinMinMaxRectangle(){ float x = transform.position.x; float y = transform.position.y; float z = transform.position.z; float clampedX = Mathf.Clamp(x, x_min, x_max); float clampedY = Mathf.Clamp(y, y_min, y_max); transform.position = new Vector3(clampedX, clampedY, z); } -
修改名为
PlayerMove的 C#脚本,以便在FixedUpdate()方法中的所有其他操作完成后,最终调用KeepWithinMinMaxRectangle()方法:void FixedUpdate(){ float xMove = Input.GetAxis("Horizontal"); float yMove = Input.GetAxis("Vertical"); float xSpeed = xMove * speed; float ySpeed = yMove * speed; Vector2 newVelocity = new Vector2(xSpeed, ySpeed); rigidBody2D.velocity = newVelocity; // restrict player movement KeepWithinMinMaxRectangle(); } -
在层次结构视图中选择player-SpaceGirl1GameObject,将corner_max和corner_minGameObject 拖放到检查器中的公共变量
corner_max和corner_min上。 -
在场景面板中运行场景之前,尝试重新定位corner_max和corner_minGameObject。当运行场景时,这两个 GameObject(最大和最小,以及 X 和 Y)的位置将被用作玩家player-SpaceGirl1角色的移动限制。
-
虽然这一切都运行得很好,但让我们通过在场景面板中绘制一个黄色的“小工具”矩形来使运动的矩形边界在视觉上更加明确。向名为
PlayerMove的 C#脚本类中添加以下方法:void OnDrawGizmos(){ Vector3 top_right = Vector3.zero; Vector3 bottom_right = Vector3.zero; Vector3 bottom_left = Vector3.zero; Vector3 top_left = Vector3.zero; if(corner_max && corner_min){ top_right = corner_max.position; bottom_left = corner_min.position; bottom_right = top_right; bottom_right.y = bottom_left.y; top_left = top_right; top_left.x = bottom_left.x; } //Set the following gizmo colors to YELLOW Gizmos.color = Color.yellow; //Draw 4 lines making a rectangle Gizmos.DrawLine(top_right, bottom_right); Gizmos.DrawLine(bottom_right, bottom_left); Gizmos.DrawLine(bottom_left, top_left); Gizmos.DrawLine(top_left, top_right); }
它是如何工作的...
您已将名为corner_max和corner_min的空 GameObject 添加到场景中。这些 GameObject 的 X 和 Y 坐标将用于确定允许名为player-SpaceGirl1的角色进行的移动边界。由于这些是空 GameObject,因此在播放模式中玩家将看不到它们。然而,我们可以在场景面板中看到并移动它们,并且添加了黄色的椭圆形图标后,我们可以很容易地看到它们的位置和名称。
在调用 Awake() 方法激活 PlayerMoveWithLimits 对象后,位于 player-SpaceGirl1 GameObject 内部记录了被称作 corner_max 和 corner_min 的 GameObject 的最大和最小 X- 和 Y- 值。每次通过 FixedUpdate() 方法调用物理系统时,player-SpaceGirl1 角色的速度会根据水平方向和垂直方向的键盘/摇杆输入进行调整。然而,FixedUpdate() 方法的最终操作是调用 KeepWithinMinMaxRectangle() 方法,该方法使用 Math.Clamp(…) 函数将角色移动回 X- 和 Y- 的限制范围内。这样做是为了确保玩家的角色不被允许移动到由 corner_max 和 corner_min GameObjects 定义的区域之外。
OnDrawGizmos() 方法会检查对 corner_max 和 corner_min GameObjects 的引用是否不为空,然后设置代表由 corner_max 和 corner_min 在对角处定义的矩形的四个角的四个 Vector3 对象的位置。然后设置 Gizmo 颜色为黄色,并在 场景 面板中绘制连接四个角的线条。
参见
参考下一菜谱以获取有关限制玩家控制的字符移动的更多信息。
3D GameObject 的玩家控制(以及限制矩形内的移动)
本章中的许多 3D 菜谱都是基于这个基本项目构建的,该项目构建了一个包含纹理地形、主相机和可以由用户使用四个方向箭头键移动的红色立方体的场景。立方体的移动范围使用与上一个 2D 菜谱中相同的技术进行限制。

如何操作...
要创建一个基本的 3D 立方体控制游戏,请按照以下步骤操作:
-
创建一个新的空 3D 项目。
-
一旦创建项目,导入名为
SandAlbedo的单个地形纹理(在 Unity 4 中名为GoodDirt)。选择菜单:资产 | 导入包 | 环境,取消选择所有选项,然后定位并勾选资产:Assets/Environment/TerrainAssets/SurfaceTextures/SandAlbedo.psd。小贴士
你本可以在创建项目时直接添加环境资产包——但这会导入 100 多个文件,而我们只需要这一个。如果你想要保持项目资产文件夹的大小尽可能小,那么在 Unity 中开始项目然后只选择性地导入所需内容是最佳做法。
-
创建一个位于 (-15, 0, -10) 且大小为 30x20 的地形。
注意
地形的变换位置与其角落相关,而不是中心。
由于地形的变换位置与对象角落相关,我们通过将 X 坐标设置为(-1width/2)和 Z 坐标设置为(-1length/2)来将此类对象居中在(0,0,0)。换句话说,我们通过对象宽度的一半和高度的一半滑动对象,以确保其中心正好在我们想要的位置。
在这种情况下,宽度为 30,长度为 20,因此我们得到 X 轴为-15(-1 * 30/2),Z 轴为-10(-1 * 20/2)。
-
使用您的纹理
SandAlbedo对地形进行纹理绘制。 -
创建一个方向光(它应该向下面对地形,使用默认设置——但如果由于某种原因没有这样做,则旋转它,以便地形得到良好的照明)。
-
对主摄像机进行以下修改:
-
位置 = (0, 20, -15)
-
旋转 = (60, 0, 0)
-
-
将游戏面板的纵横比从自由纵横比更改为4:3。现在您将在游戏面板中看到整个地形。
-
创建一个新的空 GameObject,命名为corner_max,并将其放置在(14, 0, 9)。在层次结构中选择此 GameObject,然后在检查器面板中选择大型的黄色椭圆形图标。
-
复制corner_max GameObject,将其克隆命名为corner_min,并将此克隆放置在(-14, 0, -9)。这两个 GameObject 的坐标将确定玩家角色允许的最大和最小移动范围。
-
在位置(0, 0.5, 0)创建一个新的名为Cube-player的Cube GameObject,并调整其大小为(1,1,1)。
-
将名为Physics | RigidBody的组件添加到Cube-player GameObject 中,并取消选中RigidBody属性的Use Gravity选项。
-
创建一个名为m_red的红色材质,并将其应用到Cube-player上。
-
将以下名为
PlayerControl的 C#脚本类添加到Cube-player:using UnityEngine; using System.Collections; public class PlayerControl : MonoBehaviour { public Transform corner_max; public Transform corner_min; public float speed = 40; private Rigidbody rigidBody; private float x_min; private float x_max; private float z_min; private float z_max; void Awake (){ rigidBody = GetComponent<Rigidbody>(); x_max = corner_max.position.x; x_min = corner_min.position.x; z_max = corner_max.position.z; z_min = corner_min.position.z; } void FixedUpdate() { KeyboardMovement(); KeepWithinMinMaxRectangle(); } private void KeyboardMovement (){ float xMove = Input.GetAxis("Horizontal") * speed * Time.deltaTime; float zMove = Input.GetAxis("Vertical") * speed * Time.deltaTime; float xSpeed = xMove * speed; float zSpeed = zMove * speed; Vector3 newVelocity = new Vector3(xSpeed, 0, zSpeed); rigidBody.velocity = newVelocity; // restrict player movement KeepWithinMinMaxRectangle (); } private void KeepWithinMinMaxRectangle (){ float x = transform.position.x; float y = transform.position.y; float z = transform.position.z; float clampedX = Mathf.Clamp(x, x_min, x_max); float clampedZ = Mathf.Clamp(z, z_min, z_max); transform.position = new Vector3(clampedX, y, clampedZ); } void OnDrawGizmos (){ Vector3 top_right = Vector3.zero; Vector3 bottom_right = Vector3.zero; Vector3 bottom_left = Vector3.zero; Vector3 top_left = Vector3.zero; if(corner_max && corner_min){ top_right = corner_max.position; bottom_left = corner_min.position; bottom_right = top_right; bottom_right.z = bottom_left.z; top_left = bottom_left; top_left.z = top_right.z; } //Set the following gizmo colors to YELLOW Gizmos.color = Color.yellow; //Draw 4 lines making a rectangle Gizmos.DrawLine(top_right, bottom_right); Gizmos.DrawLine(bottom_right, bottom_left); Gizmos.DrawLine(bottom_left, top_left); Gizmos.DrawLine(top_left, top_right); } } -
在层次结构中选择Cube-player GameObject,将名为corner_max和corner_min的 GameObject 拖动到检查器面板中名为
corner_max和corner_min的公共变量上。 -
当运行场景时,corner_max和corner_min GameObject 的位置将定义玩家Cube-player角色的移动范围。
它是如何工作的...
场景包含一个定位的地形,使其中心为(0,0,0)。红色立方体通过PlayerControl脚本来控制用户的箭头键。
就像之前的 2D 配方一样,当Awake()方法执行时,会存储(3D) RigidBody 组件的引用,并从两个角落 GameObject 中检索最大和最小的 X-和 Z-值,存储在x_min、x_max、z_min和z_max变量中。请注意,对于这个基本的 3D 游戏,我们不会允许任何 Y 轴移动,尽管可以通过扩展这个配方中的代码轻松添加这种移动(以及通过添加第三个“最大高度”角落 GameObject 来设置边界限制)。
KeyboardMovement()方法读取水平和垂直输入值(Unity 默认设置从四个方向箭头键读取)。根据这些左右和上下值,更新立方体的速度。移动的量取决于速度变量。
KeepWithinMinMaxRectangle()方法使用Math.Clamp(…)函数将角色移动回 X 和 Z 限制内,这样玩家角色就不被允许移动到由corner_max和corner_minGameObject 定义的区域之外。
OnDrawGizmos()方法检查对corner_max和corner_minGameObject 的引用是否不为 null,然后设置四个 Vector3 对象的位置,这些对象代表由corner_max和corner_minGameObject 在相对角落定义的矩形的四个角落。然后,将 Gizmo 颜色设置为黄色,并在场景面板中绘制连接四个角落的线条。
选择目的地 – 找到最近的(或随机的)出生点
许多游戏都使用出生点和航点。本食谱演示了两种非常常见的出生示例——选择随机出生点或选择一个感兴趣对象(如玩家角色)最近的出生点,然后在所选位置实例化一个对象。
准备工作
本食谱基于之前的食谱。因此,复制此项目,打开它,然后按照以下步骤操作。
如何操作...
要找到随机的出生点,请按照以下步骤操作:
-
在(2,2,2)位置创建一个大小为(1,1,1)的Sphere,并应用
m_red材质。 -
创建一个新的 Prefab,命名为
Prefab-ball,并将你的Sphere拖放到其中(然后从Hierarchy面板中删除Sphere)。 -
在(3, 0.5, 3)位置创建一个新的胶囊对象,命名为
Capsule-spawnPoint,将其标签设置为Respawn(这是 Unity 提供的默认标签之一)。注意
为了测试,我们将保留这些重生点可见。对于最终游戏,我们将取消选中每个重生 GameObject 的 Mesh Rendered,这样它们就不会对玩家可见。
-
将你的胶囊-spawnPoint复制到地形上的不同位置,以创建多个副本。
-
将名为
SpawnBall的以下 C#脚本类的一个实例添加到立方体-玩家GameObject 中:using UnityEngine; using System.Collections; public class SpawnBall : MonoBehaviour { public GameObject prefabBall; private SpawnPointManager spawnPointManager; private float destroyAfterDelay = 1; private float testFireKeyDelay = 0; void Start (){ spawnPointManager = GetComponent<SpawnPointManager> (); StartCoroutine("CheckFireKeyAfterShortDelay"); } IEnumerator CheckFireKeyAfterShortDelay () { while(true){ yield return new WaitForSeconds(testFireKeyDelay); // having waited, now we check every frame testFireKeyDelay = 0; CheckFireKey(); } } private void CheckFireKey() { if(Input.GetButton("Fire1")){ CreateSphere(); // wait half-second before alling next spawn testFireKeyDelay = 0.5f; } } private void CreateSphere(){ GameObject spawnPoint = spawnPointManager.RandomSpawnPoint (); GameObject newBall = (GameObject)Instantiate (prefabBall, spawnPoint.transform.position, Quaternion.identity); Destroy(newBall, destroyAfterDelay); } } -
将名为
SpawnPointManager的以下 C#脚本类的一个实例添加到Cube-playerGameObject 中:using UnityEngine; using System.Collections; public class SpawnPointManager : MonoBehaviour { private GameObject[] spawnPoints; void Start() { spawnPoints = GameObject.FindGameObjectsWithTag("Respawn"); } public GameObject RandomSpawnPoint (){ int r = Random.Range(0, spawnPoints.Length); return spawnPoints[r]; } } -
确保在
SpawnBall脚本组件中选择了Cube-player。然后,将Prefab-ball拖放到公共变量 projectile 的Prefab Ball上。 -
现在,运行你的游戏。当你点击鼠标(射击)按钮时,一个球体将以随机方式实例化到胶囊位置之一。
![如何操作...]()
工作原理...
Capsule-spawnPoint 对象代表可能创建我们的球体 Prefab 的候选位置。当我们的 SpawnPointManager 对象在 Cube-player GameObject 内接收到 Start() 消息时,respawns GameObject 数组被设置为从调用 FindGameObjectsWithTag("Respawn") 返回的数组。这创建了一个包含场景中所有带有标签 Respawn 的对象的数组——即,所有我们的 Capsule-spawnPoint 对象。
当我们的 SpawnBall 对象 GameObject Cube-player 接收到 Start() 消息时,它将 spawnPointManager 变量设置为对其兄弟 SpawnPointManager 脚本组件的引用。接下来,我们开始名为 CheckFireKeyAfterShortDelay() 的 协程 方法。
CheckFireKeyAfterShortDelay() 方法使用典型的 Unity 协程技术,通过由 testFireKeyDelay 变量控制的延迟进入无限循环。这个延迟是为了让 Unity 等待,然后再调用 CheckFireKey() 来测试用户是否想要生成一个新的球体。
提示
协程是一种高级技术,其中方法内的执行可以暂停,并从相同点恢复。Yield 命令暂时暂停方法中的代码执行,允许 Unity 去执行其他 GameObject 中的代码,以及执行物理和渲染工作等。它们非常适合在固定间隔检查是否发生了某些情况(例如测试 Fire 键,或者是否收到了来自互联网请求的响应等)的情况。
在 docs.unity3d.com/Manual/Coroutines.html 了解更多关于 Unity 协程的信息。
SpawnBall 方法 CheckFireKey() 测试在那个瞬间用户是否按下了 Fire 按钮。如果按下了 Fire 按钮,则调用 CreateSphere() 方法。同时,将 testFireKeyDelay 变量设置为 0.5。这确保了我们在半秒内不会再次测试 Fire 按钮。
SpawnBall 方法 CreateSphere() 将变量 spawnPoint 赋值给由 spawnPointManager 的 RandomSpawnpoint(…) 方法返回的 GameObject。然后,它在 spawnPoint GameObject 的相同位置创建 prefab_Ball 的新实例(通过公共变量)。
还有更多...
有一些细节你不希望错过。
选择最近的出生点
而不是仅仅选择一个随机的出生点,让我们遍历数组中的出生点,并选择离玩家最近的那个。
要找到最近的出生点,我们需要做以下几步:
-
将以下方法添加到名为
SpawnPointManager的 C# 脚本类中:public GameObject NearestSpawnpoint (Vector3 source){ GameObject nearestSpawnPoint = spawnPoints[0]; Vector3 spawnPointPos = spawnPoints[0].transform.position; float shortestDistance = Vector3.Distance(source, spawnPointPos); for (int i = 1; i < spawnPoints.Length; i++){ spawnPointPos = spawnPoints[i].transform.position; float newDist = Vector3.Distance(source, spawnPointPos); if (newDist < shortestDistance){ shortestDistance = newDist; nearestSpawnPoint = spawnPoints[i]; } } return nearestSpawnPoint; } -
现在,我们需要更改名为
SpawnBall的 C# 类中的第一行,以便通过调用我们新方法NearestSpawnpoint(…)来设置spawnPoint变量:private void CreateSphere(){ GameObject spawnPoint = spawnPointManager.NearestSpawnpoint(transform.position); GameObject newBall = (GameObject)Instantiate (prefabBall, spawnPoint.transform.position, Quaternion.identity); Destroy(newBall, lifeDuration); }
在NearestSpawnpoint(…)方法中,我们将nearestSpawnpoint设置为数组中的第一个(数组索引 0)游戏对象作为我们的默认值。然后我们遍历数组的其余部分(数组索引 1 到spawnPoints.Length)。对于数组中的每个游戏对象,我们测试其距离是否小于迄今为止的最短距离,如果是,则更新最短距离,并将nearestSpawnpoint设置为当前元素。当数组搜索完毕后,我们返回nearestSpawnpoint变量引用的游戏对象。
避免因空数组导致的错误
让我们的代码更加健壮,以便它可以应对空spawnPoints数组的问题——即场景中没有标记为Respawn的对象。
为了应对没有标记为Respawn的对象,我们需要做以下操作:
-
在名为
SpawnPointManager的 C#脚本类中改进我们的Start()方法,以便如果标记为Respawn的对象数组为空,则记录一个 ERROR:public GameObject NearestSpawnpoint (Vector3 source){ void Start() { spawnPoints = GameObject.FindGameObjectsWithTag("Respawn"); // logError if array empty if(spawnPoints.Length < 1) Debug.LogError ("SpawnPointManagaer - cannot find any objects tagged 'Respawn'!"); } -
在名为
SpawnPointManager的 C#脚本类中改进RandomSpawnPoint()和NearestSpawnpoint()方法,以便即使在数组为空的情况下也能返回一个游戏对象:public GameObject RandomSpawnPoint (){ // return current GameObject if array empty if(spawnPoints.Length < 1) return null; // the rest as before ... -
在名为
SpawnBall的 C#类中改进CreateSphere()方法,以便只有在RandomSpawnPoint()和NearestSpawnpoint()方法返回非空对象引用时,我们才尝试实例化一个新的游戏对象:private void CreateSphere(){ GameObject spawnPoint = spawnPointManager.RandomSpawnPoint (); if(spawnPoint){ GameObject newBall = (GameObject)Instantiate (prefabBall, spawnPoint.transform.position, Quaternion.identity); Destroy(newBall, destroyAfterDelay); } }
参见
- 同样的技术和代码可以用于选择重生点或航点。有关航点的更多信息,请参阅本章中的NPC NavMeshAgent 控制按顺序跟随航点配方。
选择目的地 – 重生到最近通过的检查点
检查点通常表示游戏中一定距离(或可能是一条轨道),其中代理(用户或 NPC)成功到达。达到(或通过)检查点通常会带来奖励,如额外时间、分数、弹药等。此外,如果玩家有多个生命,那么通常玩家只会重生到最近通过的检查点,而不是直接回到关卡开始处。
此配方演示了一种简单的检查点方法,即一旦玩家的角色通过检查点,如果他们死亡,他们将被移回最近通过的检查点。

准备工作
此配方基于您在本章开头创建的玩家控制的 3D 立方体 Unity 项目。因此,复制此项目,打开它,然后按照此配方的步骤进行操作。
如何操作...
要使失去生命后的重生位置根据通过的检查点而变化,请按照以下步骤操作:
-
将Cube-player游戏对象移动到(12, 0.5, 0)位置。
-
在Inspector面板中选择Cube-player,通过点击Add Component | Physics | Character Controller添加一个Character Controller组件(这是为了启用接收
OnTriggerEnter碰撞消息)。 -
在(5, 0, 0)位置创建一个名为Cube-checkpoint-1的立方体,缩放为(1, 1, 20)。
-
选择Cube-checkpoint-1,在Inspector面板中检查其Box Collider组件的Is Trigger属性。
-
创建一个CheckPoint标签,并将此标签分配给Cube-checkpoint-1。
-
通过命名Cube-checkpoint-2克隆体并放置在(-5, 0, 0)的位置来复制Cube-checkpoint-1。
-
在(7, 0.5, 0)位置创建一个名为Sphere-Death的球体。将m_red材质分配给这个球体,使其变为红色。
-
选择Sphere-Death,在Inspector面板中检查其Sphere Collider组件的Is Trigger属性。
-
创建一个Death标签,并将此标签分配给Sphere-Death。
-
复制Sphere-Death,并将这个克隆体放置在(0, 0.5, 0)的位置。
-
第二次复制Sphere-Death,并将这个第二个克隆体放置在(-10, 0.5, 0)的位置。
-
将以下名为
CheckPoints的 C#脚本类的一个实例添加到Cube-playerGameObject 中:using UnityEngine; using System.Collections; public class CheckPoints : MonoBehaviour { private Vector3 respawnPosition; void Start (){ respawnPosition = transform.position; } void OnTriggerEnter (Collider hit){ if(hit.CompareTag("CheckPoint")){ respawnPosition = transform.position; } if(hit.CompareTag("Death")){ transform.position = respawnPosition; } } } -
运行场景。如果方块在通过检查点之前撞到了红色球体,它将被重新生成到其起始位置。一旦红色方块通过检查点,如果击中了红色球体,则方块将被移动到它最近通过的检查点的位置。
它是如何工作的...
CheckPoints这个 C#脚本类有一个名为respawnPosition的变量,它是一个 Vector3,指向玩家方块要移动到(重新生成)的位置,如果它与标记为Death的对象发生碰撞。此变量的默认设置是场景开始时玩家方块的位置——因此,在Start()方法中,我们将其设置为玩家的位置。
每当与标记为CheckPoint的对象发生碰撞时,respawnPosition的值将更新为玩家红色方块在此时刻的位置(即它接触标记为CheckPoint的拉伸方块时的位置)。这样,下次标记为Death的对象被击中时,方块将被重新生成到它上次接触标记为CheckPoint的对象的位置。
NPC NavMeshAgent 在寻找或逃离目的地的同时避开障碍物
Unity 的 NavMeshAgent 的引入大大简化了 NPC 和敌人代理行为的编码。在这个菜谱中,我们将添加一些墙壁(缩放立方体)障碍物,并生成一个 NavMesh,这样 Unity 就知道不要试图穿过墙壁。然后我们向 NPC GameObject 添加一个 NavMeshAgent 组件,并告诉它通过智能规划和跟随路径前往一个指定的目的地位置,同时避开墙壁障碍物。
在下一张截图的场景面板中,我们可以看到代表路径上潜在点的正方形。我们还可以看到显示当前临时方向和目的地以及当前障碍物周围的线条。
当导航面板可见时,场景面板显示蓝色阴影的可通行区域,以及在地形边缘和每个两个墙对象周围的未着色、不可通行区域。

准备工作
此配方基于您在本章开头创建的玩家控制的 3D 立方体 Unity 项目。因此,复制此项目,打开它,然后按照此配方的步骤进行。
如何操作...
要使一个对象从一个位置寻求或逃离,请遵循以下步骤:
-
删除Cube-player GameObject,因为我们将要创建一个 NPC 电脑控制代理。
-
创建一个名为Sphere-arrow的球体,其位置在(2, 0.5, 2)。将其缩放为(1,1,1)。
-
创建第二个名为Sphere-small的球体。将其缩放为(0.5, 0.5, 0.5)。
-
将Sphere-small子对象化到Sphere-arrow,并将其位置设置为(0, 0, 0.5)。
注意
子对象化指的是在层次结构面板中,将一个 GameObject 设置为另一个 GameObject 的子对象。这是通过将作为子对象的物体拖动到作为父对象的物体上完成的。一旦完成,父-子关系将通过所有子对象在层次结构面板中右缩进并立即位于其父对象下方来视觉上表示。如果一个父对象被变换(移动/缩放/旋转),那么所有其子对象也将相应地变换。
-
在检查器面板中,为Sphere-arrow添加一个新的 NavMeshAgent;选择添加组件 | 导航 | Nav Mesh Agent。
-
将NavMeshAgent组件的停止距离属性设置为
2。 -
将以下名为
ArrowNPCMovement的 C#脚本类添加到Sphere-arrow GameObject:using UnityEngine; using System.Collections; public class ArrowNPCMovement : MonoBehaviour { public GameObject targetGO; private NavMeshAgent navMeshAgent; void Start (){ navMeshAgent = GetComponent<NavMeshAgent>(); HeadForDestintation(); } private void HeadForDestintation (){ Vector3 destinaton = targetGO.transform.position; navMeshAgent.SetDestination (destinaton); } } -
确保在检查器面板中选择Sphere-arrow的
ArrowNPCMovement脚本组件。将Capsule-destination拖动到名为Target GO的变量投射物上。 -
在(-6, 0, 0)处创建一个名为Cube-wall的 3D 立方体,并将其缩放为(1, 2, 10)。
-
在(-2, 0, 6)处创建另一个名为Cube-wall的 3D 立方体,并将其缩放为(1, 2, 7)。
-
通过选择窗口 | 导航来显示导航面板。
注意
将导航面板放置在检查器面板旁边是一个很好的地方,因为您永远不会同时使用检查和导航面板。
-
在 Hierarchy 选项卡中,选择两个 Cube-wall 对象(我们选择那些不应该成为场景可走部分的对象),然后在 Navigation 面板中,勾选 Navigation Static 复选框。然后,点击 Navigation 面板底部的 Bake 按钮。当 Navigation 面板显示时,你会在 Scene 面板中可走的区域看到蓝色 tint。NavMeshAgent 的候选区域应该被视为通往目的地的路径的一部分。
![如何操作...]()
-
现在运行你的游戏。你会看到 Sphere-arrow GameObject 自动移动到 Capsule-destination GameObject,沿着一条避开两个墙壁对象的路径。
它是如何工作的...
我们添加到 GameObject Sphere-arrow 的 NavMeshAgent 组件为我们做了大部分工作。NavMeshAgents 需要 2 件事情:一个要前往的目标位置,以及具有可走/不可走区域的地面 NavMesh 组件,这样它就可以规划路径,避开障碍物。我们创建了两个障碍物(Cube-wall 对象),并在创建此场景的 Navigation 面板中创建 NavMesh 时选择了这些对象。
我们 NPC 对象要前往的位置是 Capsule-destination GameObject 在 (-12, 0, 8) 的位置;但当然,我们可以在 Scene 面板中的 Design-time 移动这个对象,并且当运行游戏时,它的新位置将是目的地。
被称为 ArrowNPCMovement 的 C# 脚本类有两个变量:一个是目标 GameObject 的引用,另一个是我们 ArrowNPCMovement 类实例所在的 GameObject 的 NavMeshAgent 组件的引用。当场景开始时,通过 Start() 方法,找到 NavMeshAgent 同级组件,并调用 HeadForDestination() 方法,将 NavMeshAgent 的目标位置设置为目标 GameObject 的位置。
一旦 NavMeshAgent 有了一个要前往的目标,它将规划一条路径并持续移动,直到到达(或者如果设置了大于零的停止距离参数,则到达停止距离内)。
小贴士
确保在运行时选择带有 NavMeshAgent 组件的对象在 Hierarchy 面板中,以便能够在 Scene 面板中看到这些导航数据。
还有更多...
有一些细节你不应该错过。
持续更新 NavMeshAgent 的目标位置到玩家角色的当前位置
而不是在场景开始时固定的目标,让我们允许 Capsule-destination 对象在场景运行时被玩家移动。在每一帧,我们将重置 NPC 箭头的 NavMeshAgent 的目标位置到 Capsule-destination 被移动到的位置。
要允许用户移动目标对象并逐帧更新 NavMeshAgent 目标,我们需要做以下事情:
-
将名为
PlayerControl的 C# 脚本类实例添加为 胶囊目标 的组件。 -
更新名为
ArrowNPCMovement的 C# 脚本类,以便我们每帧调用HeadForDestintation()方法,即从Update()而不是仅在Start()中调用一次:void Start (){ navMeshAgent = GetComponent<NavMeshAgent>(); } void Update (){ HeadForDestintation(); }
现在,当你运行游戏时,你可以使用箭头键来移动目标位置,NavMeshAgent 将根据 胶囊目标 GameObject 的更新位置在每一帧更新其路径。
不断更新 NavMeshAgent 目标以避开玩家角色的当前位置
而不是朝向玩家的当前位置寻求,让我们让我们的 NPC 代理始终尝试避开玩家的位置。例如,一个健康值非常低的敌人可能会逃跑,从而在再次战斗之前获得恢复健康的时间。

要指示我们的 NavMeshAgent 避开玩家的位置逃跑,我们需要用以下内容替换名为 ArrowNPCMovement 的 C# 脚本类:
using UnityEngine;
using System.Collections;
public class ArrowNPCMovement : MonoBehaviour {
public GameObject targetGO;
private NavMeshAgent navMeshAgent;
private float runAwayMultiplier = 2;
private float runAwayDistance;
void Start(){
navMeshAgent = GetComponent<NavMeshAgent>();
runAwayDistance = navMeshAgent.stoppingDistance * runAwayMultiplier;
}
void Update () {
Vector3 enemyPosition = targetGO.transform.position;
float distanceFromEnemy = Vector3.Distance(transform.position, enemyPosition);
if (distanceFromEnemy < runAwayDistance)
FleeFromTarget (enemyPosition);
}
private void FleeFromTarget(Vector3 enemyPosition){
Vector3 fleeToPosition = Vector3.Normalize(transform.position - enemyPosition) * runAwayDistance;
HeadForDestintation(fleeToPosition);
}
private void HeadForDestintation (Vector3 destinationPosition){
navMeshAgent.SetDestination (destinationPosition);
}
}
Start() 方法缓存了 NavMeshAgent 组件的引用,并计算 runAwayDistance 变量,使其为 NavMeshAgent 停止距离的两倍(尽管可以通过相应地更改 runAwayMultiplier 变量的值来改变这个值)。当到敌人的距离小于这个变量的值时,我们将指示由计算机控制的对象向相反方向逃跑。
Update() 方法计算到敌人的距离是否在 runAwayDistance 范围内,如果是,则调用传递敌人位置作为参数的 FleeFromTarget(…) 方法。
FleeFromTarget(…) 方法计算一个点,该点距离玩家的立方体 runAwayDistance Unity 单位,方向是直接远离由计算机控制的对象。这是通过从当前变换的位置减去敌人位置向量来实现的。最后,调用 HeadForDestintation(…) 方法,传递逃跑到的位置,这将导致 NavMeshAgent 被指示将位置设置为新的目标。
注意
Unity 的单位是任意的,因为它们只是计算机中的数字。然而,在大多数情况下,将距离视为米(1 Unity 单位 = 1 米)和将质量视为千克(1 Unity 单位 = 1 千克)可以简化事情。当然,如果您的游戏基于微观世界或全银河系太空旅行等,那么您需要决定每个 Unity 单位在您的游戏上下文中对应什么。有关 Unity 中单位的更多讨论,请参阅forum.unity3d.com/threads/best-units-of-measurement-in-unity.284133/#post-1875487链接。
如以下截图所示,NavMeshAgent 规划了一条逃向位置的路径:

创建一个迷你点选游戏
另一种为我们的Sphere-arrow游戏对象选择目标的方法是通过用户点击屏幕上的一个对象,然后Sphere-arrow游戏对象移动到被点击对象的当前位置。
要允许用户通过点选选择目标对象,我们需要做以下操作:
-
从Sphere-arrow游戏对象中移除
ArrowNPCMovement组件。 -
创建一些目标对象,例如一个黑色立方体、一个蓝色球体和一个绿色圆柱体。请注意,为了成为目标,每个对象都需要一个碰撞器组件以便接收
OnMouseOver事件消息(从 Unity 菜单创建 | 3D 对象创建原生物体时,会自动创建碰撞器)。 -
将以下名为
ClickMeToSetDestination的 C#脚本类实例添加到您希望成为可点击目标的每个 GameObject 中:using UnityEngine; using System.Collections; public class ClickMeToSetDestination : MonoBehaviour { private NavMeshAgent playerNavMeshAgent; private MeshRenderer meshRenderer; private bool mouseOver = false; private Color unselectedColor; void Start (){ meshRenderer = GetComponent<MeshRenderer>(); unselectedColor = meshRenderer.sharedMaterial.color; GameObject playerGO = GameObject.FindGameObjectWithTag("Player"); playerNavMeshAgent = playerGO.GetComponent<NavMeshAgent>(); } void Update (){ if (Input.GetButtonDown("Fire1") && mouseOver) playerNavMeshAgent.SetDestination(transform.position); } void OnMouseOver (){ mouseOver = true; meshRenderer.sharedMaterial.color = Color.yellow; } void OnMouseExit (){ mouseOver = false; meshRenderer.sharedMaterial.color = unselectedColor; } }
现在,当运行游戏时,当您的鼠标悬停在三个对象之一上时,该对象将被突出显示为黄色。如果您在对象突出显示时点击鼠标按钮,Sphere-arrow游戏对象将前往(但停在点击对象之前)。
NPC NavMeshAgent 按顺序跟随航点
航点通常用作引导,使自主移动的 NPC 和敌人以一般方式跟随路径(但如果附近检测到朋友/捕食者/猎物,则能够响应其他方向行为,如逃跑或寻找)。航点按顺序排列,因此当角色到达或接近航点时,它将选择序列中的下一个航点作为移动的目标位置。本配方演示了一个箭头对象向航点移动,然后,当它足够接近时,它将选择序列中的下一个航点作为新的目标目的地。当到达最后一个航点后,它再次开始向第一个航点前进。
由于 Unity 的 NavMeshAgent 简化了 NPC 行为的编码,我们在这个配方中的工作基本上是找到下一个航点的位置,然后告诉 NavMeshAgent 这个航点是其新的目的地。

准备工作
此配方基于您在本章开头创建的玩家控制 3D 立方体 Unity 项目。因此,复制此项目,打开它,然后按照此配方的步骤进行操作。
对于这个配方,我们在1362_08_06文件夹中的Textures文件夹中准备了您需要的黄色砖块纹理图像。
如何操作...
要指示一个对象按照航点的顺序进行跟随,请按照以下步骤操作:
-
删除Cube-player游戏对象,因为我们将要创建一个 NPC 电脑控制代理。
-
创建一个名为Sphere-arrow的球体,定位在(2, 0.5, 2),并按(1,1,1)的比例缩放。
-
创建第二个名为Sphere-small的球体,并按(0.5, 0.5, 0.5)的比例缩放。
-
将Sphere-small到Sphere-arrow,然后将其定位在(0, 0, 0.5)。
-
在检查器中,向Sphere-arrow添加一个新的 NavMeshAgent,然后选择添加组件 | 导航 | NavMeshAgent。
-
将NavMeshAgent组件的停止距离属性设置为
2。 -
通过选择窗口 | 导航来显示导航面板。
-
点击导航面板底部的烘焙按钮。当导航面板显示时,您将在场景面板的可行走部分看到蓝色色调,这将是地形的所有部分,除了边缘附近。
-
将名为
ArrowNPCMovement的以下 C#脚本类实例添加到Sphere-arrow游戏对象中:using UnityEngine; using System.Collections; public class ArrowNPCMovement : MonoBehaviour { private GameObject targetGO = null; private WaypointManager waypointManager; private NavMeshAgent navMeshAgent; void Start (){ navMeshAgent = GetComponent<NavMeshAgent>(); waypointManager = GetComponent<WaypointManager>(); HeadForNextWayPoint(); } void Update (){ float closeToDestinaton = navMeshAgent.stoppingDistance * 2; if (navMeshAgent.remainingDistance < closeToDestinaton){ HeadForNextWayPoint (); } } private void HeadForNextWayPoint (){ targetGO = waypointManager.NextWaypoint (targetGO); navMeshAgent.SetDestination (targetGO.transform.position); } } -
在(-12, 0, 8)位置创建一个新的胶囊对象Capsule-waypoint-0,并赋予它航点标签。
-
复制胶囊-航点 -0,将其命名为胶囊-航点 -3,并将此副本定位在(8, 0, -8)。
注意
我们稍后将要添加一些中间的航点,编号为 1 和 2。这就是为什么我们的第二个航点编号为 3,以防您有所疑问。
-
将名为
WaypointManager的以下 C#脚本类添加到Sphere-arrow游戏对象中:using UnityEngine; public class WaypointManager : MonoBehaviour { public GameObject wayPoint0; public GameObject wayPoint3; public GameObject NextWaypoint(GameObject current){ if(current == wayPoint0) return wayPoint3; else return wayPoint0; } } -
确保在检查器中选择了
WaypointManager脚本组件的Sphere-arrow。将Capsule-waypoint-0和Capsule-waypoint-3拖到名为航点 0和航点 3的公共变量 projectile 上。![如何操作...]()
-
通过选择窗口 | 导航来显示导航面板。
-
再次点击导航面板底部的烘焙按钮。当导航面板显示时,您将在场景的可行走部分看到蓝色色调,这将是地形的所有部分,除了边缘附近。
-
现在,运行你的游戏。箭头对象将首先移动到其中一个 waypoint 胶囊,然后当它靠近时,它会减速,转身,朝向另一个 waypoint 胶囊前进,并持续这样做。
它是如何工作的...
我们添加到Sphere-arrow GameObject 中的NavMeshAgent组件为我们做了大部分工作。NavMeshAgents需要两样东西:一个目的地位置来前往,以及一个 NavMesh,以便它可以规划路径,避开障碍物。
我们创建了两个可能的 waypoint 作为 NPC 移动的位置:Capsule-waypoint-0和Capsule-waypoint-3。
C#脚本类WaypointManager有一个任务——返回我们的 NPC 应该前往的下一个 waypoint 的引用。有两个变量:wayPoint0和wayPoint3,它们分别引用场景中的两个 waypoint GameObject。NextWaypoint(…)方法接受一个名为current的单个参数,它是对象移动方向上的当前 waypoint 的引用(或 null)。此方法的任务是返回 NPC 应该前往的下一个waypoint 的引用。此方法的逻辑很简单——如果current指向waypoint0,则我们将返回waypoint3,否则返回waypoint0。注意,如果我们传递此null方法,则将返回waypoint0(因此,它是我们的默认第一个 waypoint)。
C#脚本类ArrowNPCMovement有三个变量:一个是名为targetGO的指向目标 GameObject 的引用。第二个是NavMeshAgent组件的引用,该组件位于我们的ArrowNPCMovement类实例所在的 GameObject 中。第三个变量名为WaypointManager,是引用同级的脚本组件,即我们的WaypointManager脚本类的实例。
当场景开始时,通过Start()方法,找到NavMeshAgent和WaypointManager同级组件,并调用HeadForDestination()方法。
HeadForDestination()方法首先将变量targetGO设置为指向由名为WaypointManager的脚本组件调用的NextWaypoint(…)方法返回的 GameObject(即targetGO设置为指向Capsule-waypoint-0或Capsule-waypoint-3)。接下来,它指示NavMeshAgent将其目的地设置为targetGO GameObject 的位置。
每一帧都会调用名为Update()的方法。进行一个测试,查看 NPC 箭头对象与目的地 waypoint 的距离是否接近。如果距离小于我们在NavMeshAgent中设置的停止距离的两倍,则调用WaypointManager.NextWaypoint(…)来更新我们的目标目的地为序列中的下一个 waypoint。
还有更多...
有一些细节你不希望错过。
更高效地避免使用 NavMeshes 作为 waypoint
NavMeshes 比航点优越得多,因为可以使用一个大致区域的位置(而不是一个特定点),路径查找算法将自动找到最短路径。对于简短的菜谱(如上述),我们可以简化使用 NavMeshes 计算移动的实现。然而,对于优化后的现实游戏,从一点移动到下一点的最常见方式是通过线性插值,或者通过实现 Craig Reynold 的 Seek 算法(有关详细信息,请参阅结论部分列出的链接,在本章末尾)。
使用航点数组进行工作
有一个名为 WaypointManager 的独立 C# 脚本类,用于简单地切换 Capsule-waypoint-0 和 Capsule-waypoint-3,这似乎是一个繁重且过度工程化的任务,但实际上这是一个非常好的举措。WaypointManager 脚本类的一个对象负责返回 下一个 航点。现在,添加一个更复杂的具有航点数组的方法变得非常简单,而无需修改名为 ArrowNPCMovement 的脚本类中的任何代码。我们可以选择一个随机航点作为下一个目的地(参见 选择目的地 – 找到最近的(或随机的)出生点 菜谱)。或者,我们可以有一个航点数组,并按顺序选择下一个。
为了改进我们的游戏,使其能够按照航点序列工作,我们需要做以下事情:
-
复制 Capsule-waypoint-0,将其命名为 Capsule-waypoint-1,并将此副本放置在 (0, 0, 8)。
-
再复制四个(命名为 Capsule-waypoint-1、2、4、5),并按照以下方式定位:
-
Capsule-waypoint-1: 位置 = (-2, 0, 8)
-
Capsule-waypoint-2: 位置 = (8, 0, 8)
-
Capsule-waypoint-4: 位置 = (-2, 0, -8)
-
Capsule-waypoint-5: 位置 = (-12, 0, -8)
-
-
用以下代码替换名为
WaypointManager的 C# 脚本类:using UnityEngine; using System.Collections; using System; public class WaypointManager : MonoBehaviour { public GameObject[] waypoints; public GameObject NextWaypoint (GameObject current) { if( waypoints.Length < 1) Debug.LogError ("WaypointManager:: ERROR - no waypoints have been added to array!"); int currentIndex = Array.IndexOf(waypoints, current); int nextIndex = ((currentIndex + 1) % waypoints.Length); return waypoints[nextIndex]; } } -
确保选择 Sphere-arrow。在
WaypointManager脚本组件的 Inspector 面板中设置Waypoints数组的大小为6。现在,拖入所有六个名为Capsule-waypoint-0/1/2/3/4/5的胶囊航点对象。 -
运行游戏。现在,Sphere-arrow GameObject 将首先移动到航点 0(左上角),然后沿着地形周围的序列移动。
-
最后,你可以让它看起来像球体正沿着一条黄色的砖路行走。导入提供的黄色砖纹理,将其添加到你的地形中,并在航点之间绘制一个椭圆形路径的纹理。你也可以取消选中每个航点胶囊的“网格渲染”组件,这样用户就看不到任何航点,只能看到跟随黄色砖路的箭头对象。
在NextWaypoint(…)方法中,首先检查数组是否为空,如果是,则记录错误。接下来,找到当前 waypoint GameObject 的数组索引(如果存在于数组中)。最后,使用模运算符计算下一个 waypoint 的数组索引,以支持循环序列,在访问最后一个元素后返回数组的开头。
使用 WayPoint 类提高灵活性
而不是强制一个 GameObject 跟随一个单一的刚体序列位置,我们可以通过定义一个WayPoint类来使事情更加灵活,其中每个 waypoint GameObject 都有一个可能的终点数组,每个这样的终点都有一个自己的数组,以此类推。这样就可以实现一个有向图(directed graph),其中线性序列只是可能的一个实例。
为了提高我们的游戏以使用 waypoint 的有向图,请执行以下操作:
-
从Sphere-arrow GameObject 中移除脚本组件
WayPointManager。 -
用以下代码替换名为
ArrowNPCMovement的 C#脚本类:using UnityEngine; using System.Collections; public class ArrowNPCMovement : MonoBehaviour { public Waypoint waypoint; private bool firstWayPoint = true; private NavMeshAgent navMeshAgent; void Start (){ navMeshAgent = GetComponent<NavMeshAgent>(); HeadForNextWayPoint(); } void Update () { float closeToDestinaton = navMeshAgent.stoppingDistance * 2; if (navMeshAgent.remainingDistance < closeToDestinaton){ HeadForNextWayPoint (); } } private void HeadForNextWayPoint (){ if(firstWayPoint) firstWayPoint = false; else waypoint = waypoint.GetNextWaypoint(); Vector3 target = waypoint.transform.position; navMeshAgent.SetDestination (target); } } -
创建一个新的 C#脚本类名为
WayPoint,代码如下:using UnityEngine; using System.Collections; public class Waypoint: MonoBehaviour { public Waypoint[] waypoints; public Waypoint GetNextWaypoint () { return waypoints[ Random.Range(0, waypoints.Length) ]; } } -
选择所有六个名为Capsule-waypoint -0/1/2/3/4/5的 GameObject,并添加一个名为
WayPoint的 C#类的脚本实例。 -
选择Sphere-arrow GameObject,并添加一个名为
WayPoint的 C#类的脚本实例。 -
确保已选择Sphere-arrow GameObject:在
ArrowNPCMovement脚本组件的Inspector面板中,将Capsule-waypoint-0拖入Waypoint公共变量槽中。 -
现在,我们需要将Capsule-waypoint-0链接到Capsule-waypoint-1,Capsule-waypoint-1链接到Capsule-waypoint -2,依此类推。选择Capsule-waypoint-0,将其
Waypoints数组大小设置为1,并将Capsule-waypoint-1拖入。接下来,选择Capsule-waypoint-1,将其Waypoints数组大小设置为 1,并将Capsule-waypoint-2拖入。重复此操作,直到最终将Capsule-waypoint-5链接回Capsule-waypoint-0。
现在你有一个更加灵活的游戏架构,允许 GameObject 在到达每个 waypoint 时随机选择几条不同的路径。在这个最终的食谱变体中,我们实现了一个 waypoint 序列,因为每个 waypoint 都有一个仅包含一个链接 waypoint 的数组。然而,如果你将数组大小更改为 2 或更多,那么你将创建一个链接 waypoint 的图,为计算机控制的字符在游戏运行中的任何给定运行添加随机变化。
通过群聚控制对象组的移动
通过创建具有以下四个简单规则的对象集合,可以创建一个逼真、自然的外观群聚行为(例如鸟类、羚羊或蝙蝠):
-
分离:避免与邻居过于接近
-
避开障碍物:立即转向避开前方障碍物
-
对齐:向群移动的一般方向
-
凝聚力:向群中间的位置移动
群中的每个成员都独立行动,但需要了解其群成员的当前航向和位置。此配方向您展示如何创建一个场景,其中包含两群立方体:一群绿色立方体和一群黄色立方体。为了简化,我们不会在配方中考虑分离。

准备工作
此配方基于你在第一个配方中创建的玩家控制立方体 Unity 项目。因此,复制此项目,打开它,然后按照此配方的步骤进行操作。
如何操作...
要使一组对象聚集在一起,请按照以下步骤操作:
-
在项目面板中创建一个材质,并将其命名为
m_green,主色调为绿色。 -
在项目面板中创建一个材质,并将其命名为
m_yellow,主色调为黄色。 -
在(0,0,0)处创建一个名为
Cube-drone的 3D 立方体游戏对象。将m_yellow材质拖动到这个对象中。 -
向
Cube-drone添加一个导航 | NavMeshAgent组件。将停止距离属性设置为2。 -
将以下属性的物理刚体组件添加到
Cube-drone中:-
质量为
1 -
拖动为
0 -
角动量阻力为
0.05 -
使用重力和是运动学都未勾选
-
在约束冻结位置的Y轴上勾选
-
-
你将看到以下检查器值用于你的立方体刚体组件:
![如何操作...]()
-
创建以下名为
Drone的 C#脚本类,并将其作为组件添加到Cube-drone游戏对象中:using UnityEngine; using System.Collections; public class Drone : MonoBehaviour { private NavMeshAgent navMeshAgent; void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); } public void SetTargetPosition(Vector3 swarmCenterAverage, Vector3 swarmMovementAverage) { Vector3 destination = swarmCenterAverage + swarmMovementAverage; navMeshAgent.SetDestination(destination); } } -
创建一个新的空 Prefab,命名为
dronePrefabYellow,然后从层次面板中,将你的Cube-boid游戏对象拖动到这个 Prefab 中。 -
现在,将
m_green材质拖动到Cube-boid游戏对象中。 -
创建一个新的空 Prefab,命名为
dronePrefabGreen,然后从层次面板中,将你的Cube-drone游戏对象拖动到这个 Prefab 中。 -
从场景面板中删除
Cube-drone游戏对象。 -
将以下 C#脚本
Swarm类添加到主相机:using UnityEngine; using System.Collections; using System.Collections.Generic; public class Swarm : MonoBehaviour { public int droneCount = 20; public GameObject dronePrefab; private List<Drone> drones = new List<Drone>(); void Awake() { for (int i = 0; i < droneCount; i++) AddDrone(); } void FixedUpdate() { Vector3 swarmCenter = SwarmCenterAverage(); Vector3 swarmMovement = SwarmMovementAverage(); foreach(Drone drone in drones) drone.SetTargetPosition(swarmCenter, swarmMovement); } private void AddDrone() { GameObject newDroneGO = (GameObject)Instantiate(dronePrefab); Drone newDrone = newDroneGO.GetComponent<Drone>(); drones.Add(newDrone); } private Vector3 SwarmCenterAverage() { // cohesion (swarm center point) Vector3 locationTotal = Vector3.zero; foreach(Drone drone in drones) locationTotal += drone.transform.position; return (locationTotal / drones.Count); } private Vector3 SwarmMovementAverage() { // alignment (swarm direction average) Vector3 velocityTotal = Vector3.zero; foreach(Drone drone in drones) velocityTotal += drone.rigidbody.velocity; return (velocityTotal / drones.Count); } } -
在层次面板中选择主相机,从项目面板中,将
prefab_boid_yellow拖动到DronePrefab 的公共变量上。 -
在层次面板中选择主相机,向此游戏对象添加第二个名为
Swarm的脚本类实例,然后从项目面板中,将prefab_boid_green拖动到DronePrefab 的公共变量上。 -
创建一个名为
wall-left的新立方体,具有以下属性:-
位置 = (-15, 0.5, 0)
-
尺寸 = (1, 1, 20)
-
-
通过将新对象命名为
wall-right来复制wall-left对象,并将wall-right的位置更改为(15, 0.5, 0)。 -
创建一个名为
wall-top的新立方体,具有以下属性:-
Position = (0, 0.5, 10)
-
Scale = (31, 1, 1)
-
-
通过将新对象命名为
wall-bottom来复制wall-top对象,并将wall-bottom的位置更改为(0,0.5,-10)。 -
创建一个新的名为
Sphere-obstacle的球体,具有以下属性:-
Position = (5, 0, 3)
-
Scale = (10, 3, 3)
-
-
在层次面板中,选择
Sphere-obstacle游戏对象。然后在导航面板中,勾选导航静态复选框。然后,点击导航面板底部的烘焙按钮。 -
最后,通过将玩家的红色立方体的比例设置为(3,3,3)来使其更大。
它是如何工作的...
Swarm类包含三个变量:
-
droneCount:它是一个整数,引用创建的Swarm类成员的数量 -
dronePrefab:它引用要克隆以创建蜂群成员的 Prefab -
Drone:一个对象列表,引用drones,它是一个列表,包含所有已创建的Swarm对象内部的所有脚本Drone组件
在创建时,随着场景的开始,Swarm脚本类的Awake()方法循环创建droneCount个蜂群成员,通过重复调用AddDrone()方法。该方法从预制体实例化一个新的GameObject,然后将newDrone变量设置为对新的Swarm类成员中 Drone 脚本对象的引用。在每一帧中,FixedUpdate()方法通过调用它们的SetTargetPosition(…)方法遍历Drone对象列表,并传入蜂群中心位置和所有蜂群成员速度的平均值。
这个Swarm类的其余部分由两个方法组成:一个(SwarmCenterAverage)返回一个表示所有Drone对象平均位置的 Vector3 对象,另一个(SwarmMovementAverage)返回一个表示所有Drone对象平均速度(运动力)的 Vector3 对象,如下列表所述。
-
SwarmMovementAverage():-
蜂群移动的一般方向是什么?
-
这被称为对齐——一个蜂群成员试图移动到蜂群平均方向
-
-
SwarmCenterAverage():-
蜂群的中心位置是什么?
-
这被称为凝聚力——一个蜂群成员试图移动到蜂群中心
-
核心工作由Drone类承担。每个无人机的Start(…)方法找到并缓存其 NavMeshAgent 组件的引用。
每个无人机的UpdateVelocity(…)方法接受两个 Vector3 参数:swarmCenterAverage和swarmMovementAverage。然后该方法计算这个无人机期望的新速度(通过简单地添加两个向量),然后使用结果(一个 Vector3 位置)来更新 NavMeshAgent 的目标位置。
还有更多...
有一些细节你不希望错过。
了解更多关于集群人工智能的信息
现代计算中的大多数群集模型都归功于克雷格·雷诺兹在 20 世纪 80 年代的工作。在en.wikipedia.org/wiki/Craig_Reynolds_(computer_graphics)了解更多关于克雷格和他的 boids 程序的信息
结论
在本章中,我们介绍了各种玩家和计算机控制的角色、车辆和对象的配方。玩家角色控制器是每个游戏可用体验的基础,而 NPC 对象和角色则为许多游戏增添了丰富的交互:
-
从这个 Unity 教程中了解更多关于 Unity NavMeshes 的信息,该教程可在
unity3d.com/learn/tutorials/modules/beginner/live-training-archive/navmeshes找到 -
在
unity3d.com/learn/tutorials/modules/beginner/2d/2d-controllers了解更多关于 Unity 2D 角色控制器的信息 -
从克雷格·雷诺兹的经典论文《Steering Behaviors For Autonomous Characters》中学习大量关于计算机控制移动 GameObject 的知识,这篇论文在 1999 年 GDC-99(游戏开发者大会)上展出,可在
www.red3d.com/cwr/steer/gdc99/找到 -
在以下链接了解 Unity 3D 角色组件和控制:
每个游戏都需要纹理——以下是一些适合许多游戏的免费纹理来源:
-
CG Textures 可在
www.cgtextures.com/找到 -
Naldz Graphics 博客可在
naldzgraphics.net/textures/找到
第九章.播放和操作声音
在本章中,我们将涵盖:
-
将音频音调与动画速度匹配
-
使用 Reverb Zones 模拟声学环境
-
防止音频剪辑在播放时重新启动
-
在对象自动销毁前等待音频播放完成
-
使用音频混音器添加音量控制
-
使用快照制作动态配乐
-
在游戏中平衡音频与 Ducking
简介
声音是游戏体验中非常重要的一个部分。实际上,我们无法强调其对玩家沉浸于虚拟环境中的重要性。只需想想你在最喜欢的赛车游戏中引擎的轰鸣声,模拟游戏中的遥远城市嘈杂声,或者恐怖游戏中的缓慢声音。想想这些声音是如何将你带入游戏的。
整体概念
在继续介绍食谱之前,让我们退后一步,快速回顾一下在 Unity 5 中声音是如何工作的。
音频文件可以通过音频源组件嵌入到 GameObject 中。Unity 支持3D 声音,这意味着音频源和音频监听器之间的位置和距离会影响声音在响度和左右平衡方面的感知。这除非音频源被指定为2D 声音(这通常是背景配乐音乐的情况)。
尽管所有声音都发送到场景的音频监听器(通常附加到主相机上,并且不应同时附加到多个对象上),Unity 5 为音频场景带来了一个新的玩家:音频混音器。音频混音器彻底改变了声音元素可以体验和操作的方式。它允许开发者以与音乐家和制作人他们在他们的数字音频工作站(D.A.W)中(如GarageBand或ProTools)相同的方式混合和排列音频。它允许你将音频源剪辑路由到特定的通道,这些通道可以单独调整音量并经过定制效果和过滤器的处理。你可以使用多个音频混音器,将混音器的输出发送到父混音器,并将混音器偏好保存为快照。此外,你还可以从脚本中访问混音器参数。以下图表示了 Unity 5 主要音频混音概念及其关系:

利用许多示例项目中的新音频混音器功能,本章充满了希望帮助你实现更好的、更有效的声音设计,增强玩家的沉浸感,将他或她带入游戏环境,甚至改善游戏玩法。
将音频音调与动画速度匹配
许多在加速时音调更高,在减速时音调更低的文物。汽车引擎、风扇冷却器、黑胶唱片机……等等。如果您想在可以动态改变速度的动画对象中模拟这种音效,请遵循此食谱。
准备工作
对于这个食谱,您需要一个动画 3D 对象和一个音频剪辑。请使用代码包中1362_09_01文件夹中可用的文件animatedRocket.fbx和engineSound.wav。
如何操作...
要根据动画对象的速率更改音频剪辑的音调,请按照以下步骤操作:
-
将
animatedRocket.fbx文件导入到您的项目中。 -
在项目视图中选择
animatedRocket文件。然后,从检查器视图中检查其导入设置。选择动画,然后选择剪辑Take 001,并确保勾选循环时间选项。单击以下所示的应用按钮以保存更改:![如何操作...]()
小贴士
我们不需要勾选循环姿态选项的原因是因为我们的动画已经无缝循环。如果它没有,我们可以勾选该选项以自动创建从动画最后一帧到第一帧的无缝过渡。
-
通过从项目视图拖动到层次结构视图,将animatedRocket GameObject 添加到场景中。
-
导入
engineSound.wav音频剪辑。 -
选择animatedRocket GameObject。然后,从项目视图拖动engineSound到检查器视图,将其添加为该对象的音频源。
-
在animatedRocket的音频源组件中,勾选循环选项,如图以下截图所示:
![如何操作...]()
-
我们需要为我们对象创建一个控制器。在项目视图中,点击创建并选择动画控制器。将其命名为
rocketlController。 -
双击rocketController对象以打开动画器窗口,如图所示。然后,在网格区域右键单击并从上下文菜单中选择创建状态 | 空选项:
![如何操作...]()
-
将新状态命名为
spin,并在运动字段中将Take 001设置为它的动作:![如何操作...]()
-
从层次结构视图中选择animatedRocket。然后,在动画器组件(在检查器视图中),将rocketController设置为它的控制器,并确保应用根运动选项未勾选,如图所示:
![如何操作...]()
-
在项目视图中创建一个新的C# 脚本,并将其重命名为
ChangePitch。 -
在您的编辑器中打开脚本,并将所有内容替换为以下代码:
using UnityEngine; public class ChangePitch : MonoBehaviour{ public float accel = 0.05f; public float minSpeed = 0.0f; public float maxSpeed = 2.0f; public float animationSoundRatio = 1.0f; private float speed = 0.0f; private Animator animator; private AudioSource audioSource; void Start(){ animator = GetComponent<Animator>(); audioSource = GetComponent<AudioSource>(); speed = animator.speed; AccelRocket (0f); } void Update(){ if (Input.GetKey (KeyCode.Alpha1)) AccelRocket(accel); if (Input.GetKey (KeyCode.Alpha2)) AccelRocket(-accel); } public void AccelRocket(float accel){ speed += accel; speed = Mathf.Clamp(speed,minSpeed,maxSpeed); animator.speed = speed = Mathf.Clamp (speed, 0, maxSpeed); float soundPitch = animator.speed * animationSoundRatio; audioSource.pitch = soundPitch; } } -
保存您的脚本并将其作为组件添加到animatedRocket GameObject。
-
播放场景,并通过按数字键盘上的 1 键(加速)和 2 键(减速)来更改动画速度。音频音高将相应地改变。
它是如何工作的...
在 Start() 方法中,除了将 Animator 和 AudioSource 组件存储在变量中,我们还会从 Animator 中获取初始的 speed,并通过传递 0 作为参数调用 AccelRocket() 函数,仅为此函数计算 Audio Source 的结果音高。在 Update() 函数中,if(Input.GetKey(KeyCode.Alpha1)) 和 if(Input.GetKey(KeyCode.Alpha2)) 代码行检测数字键盘上的 1 或 2 键是否被按下,以调用 AccelRocket() 函数,并传递一个 accel 浮点变量作为参数。AccelRocket() 函数反过来,通过接收到的参数(accel 浮点变量)增加 speed。然而,它使用 Mathf.Clamp() 命令将新的速度值限制在用户设置的最低和最高速度之间。然后,它根据新的 speed 绝对值更改 Animator 速度和 Audio Source 音高。该值被第二次限制以避免出现负数。如果您想反转动画,请查看代码文件中包含的完成项目的解决方案。另外,请注意,将动画速度以及因此声音音高设置为 0 将导致声音停止,这清楚地表明停止对象的动画也会阻止引擎声音播放。
还有更多...
这里有一些关于如何微调和自定义这个菜谱的信息。
改变动画/声音比率
如果您想使音频剪辑的音高更多地或更少地受动画速度的影响,请更改 动画/声音比率 参数的值。
从其他脚本访问函数
AccelRocket() 函数被设置为公共的,以便可以从其他脚本中访问。例如,我们在 1362_09_01 文件夹中包含了 ExtChangePitch.cs 脚本。尝试将此脚本附加到 主摄像机 对象上,并通过点击左右鼠标按钮来控制速度。
使用混响区域模拟声学环境
一旦您创建了您级别的几何形状,并且场景看起来正是您想要的样子,您可能希望您的声音效果与这种外观相匹配。声音的行为取决于其被投射的环境,因此使其产生混响可能是一个好主意。在这个菜谱中,我们将通过使用 混响区域 来处理这种声学效果。
准备工作
对于这个菜谱,我们准备了 ReverbZone.unitypackage 文件,其中包含一个名为 reverbScene 的基本级别和 Signal 预制体。该包位于代码包中的 1362_09_02 文件夹。
如何做到...
按照以下步骤模拟隧道的声学景观:
-
将
ReverbZone包导入到您的 Unity 项目中。 -
在项目视图中,打开reverbScene级别,位于
ReverbZones文件夹内。这是一个基本场景,包括一个可控制的角色和一个隧道。 -
现在,将信号预制体从项目视图拖动到层次结构中,如图所示。这将向场景添加一个发声对象。将其放置在隧道的中心。
![如何操作...]()
-
将信号GameObject 复制五次,并将它们分布在隧道中(在每个入口外留一个副本):
![如何操作...]()
-
在层次结构视图中,导航到创建 | 音频 | 音频混响区域以向场景添加混响区域。然后,将其放置在隧道的中心。
-
选择混响区域GameObject。在检查器视图中,将混响区域组件参数更改为以下值:最小距离:
6;最大距离:18;以及预设:StoneCorridor,如图所示:![如何操作...]()
-
播放场景,并使用W A S D键(按住Shift键以跑步)穿过隧道。当您在混响区域区域内时,您将听到音频混响。
它是如何工作的...
一旦定位,音频混响区域将对其半径内的所有音频源应用音频过滤器。
更多内容...
这里有一些选项供您尝试。
将音频混响区域组件附加到音频源
您不必创建音频混响区域GameObject,您可以通过组件 | 音频 | 音频混响区域菜单将其附加到声音发出对象(在我们的例子中,信号)作为组件。在这种情况下,混响区域将围绕对象单独设置。
自定义混响设置
Unity 自带几个混响预设。我们使用了StoneCorridor,但您的场景可以要求更不强烈(例如Room)或更激进(例如Psychotic)的东西。如果这些预设仍然无法重现您心中的效果,将其更改为用户并按您希望的参数进行编辑。
防止正在播放的音频剪辑重新开始
在游戏中,可能有几个不同的事件会导致声音开始播放。如果声音已经在播放,那么在几乎所有情况下,我们都不希望重新开始播放声音。这个配方包括一个测试,以确保只有当音频源组件当前未播放时,才会发送Play()消息。
准备工作
尝试任何持续一秒或更长的音频剪辑。我们在1362_09_03文件夹中包含了engineSound音频剪辑。
如何操作...
要防止音频剪辑重新开始,请按照以下步骤操作:
-
创建一个空GameObject 并将其重命名为音频对象。然后,向此对象添加音频源组件(在组件 | 音频 | 音频源菜单中)。
-
导入
engineSound音频片段,并将其从项目视图拖动到音频对象组件的音频片段参数中:![如何操作...]()
-
在屏幕上创建一个名为PlaySoundButton的 UI 按钮,并将以下脚本附加到该按钮:
using UnityEngine; using System.Collections; using UnityEngine.UI; public class AvoidEarlySoundRestart : MonoBehaviour { public AudioSource audioSource; public Text message; void Update(){ string statusMessage = "Play sound"; if(audioSource.isPlaying) statusMessage = "(sound playing)"; message.text = statusMessage; } // button click handler public void PlaySoundIfNotPlaying(){ if( !audioSource.isPlaying) audioSource.Play(); } } -
在层次结构面板中选择PlaySoundButton,将其拖动到检查器视图中公共音频源变量,并将PlaySoundButton的Text子项拖动到公共ButtonText:
![如何操作...]()
-
在层次结构面板中选择PlaySoundButton,创建一个新的点击事件处理程序,将PlaySoundButton拖入对象槽,并选择PlaySoundIfNotPlaying()函数。
工作原理...
音频源组件具有一个公共可读属性isPlaying,它是一个布尔值,表示声音是否正在播放。当声音未播放时,按钮文本设置为显示播放声音,当正在播放时显示(声音播放)。当按钮被点击时,会调用PlaySoundIfNotPlaying()方法。此方法使用if语句,确保只有当isPlaying为false时,才会向音频源组件发送Play()消息。
参见
- 本章中关于等待音频播放完成后再自动销毁对象的配方。
在对象自动销毁前等待音频播放完成
可能会发生某些事件(例如拾取对象或击败敌人),我们希望通过播放音频片段并关联一个视觉对象(例如爆炸粒子系统或事件位置的临时对象)来通知玩家。然而,一旦音频片段播放完毕,我们希望将视觉对象从场景中移除。本配方提供了一种简单的方法,将播放音频片段的结束与包含对象的自动销毁联系起来。
准备工作
尝试使用任何时长为一秒或更长的音频片段。我们已在1362_09_04文件夹中包含了engineSound音频片段。
如何操作...
要在销毁 GameObject 之前等待音频播放完成,请按照以下步骤操作:
-
创建一个空GameObject,并将其重命名为音频对象。然后,向该对象添加一个音频源组件(在组件 | 音频 | 音频源菜单中)。
-
导入
engineSound音频片段,并将其从项目视图拖动到音频对象组件的音频片段参数中,并取消选中组件的播放于唤醒复选框:![如何操作...]()
-
将以下脚本类添加到音频对象:
using UnityEngine; using System.Collections; public class AudioDestructBehaviour : MonoBehaviour { private AudioSource audioSource; void Start(){ audioSource = GetComponent<AudioSource>(); } private void Update(){ if( !audioSource.isPlaying ) Destroy(gameObject); } } -
在检查器视图中,禁用(取消选中)音频对象的
AudioDestructBehaviour脚本组件(当需要时,将通过 C#代码重新启用):![如何操作...]()
-
创建一个名为ButtonActions的新 C#文件,包含以下代码:
using UnityEngine; using System.Collections; public class ButtonActions : MonoBehaviour{ public AudioSource audioSource; public AudioDestructBehaviour audioDestructScriptedObject; public void PlaySound(){ if( !audioSource.isPlaying ) audioSource.Play(); } public void DestroyAfterSoundStops(){ audioDestructScriptedObject.enabled = true; } } -
在屏幕上创建一个带有按钮文本
Play Sound的 UI 按钮PlaySoundButton,并将ButtonActions脚本附加到该按钮。 -
在层次中选择PlaySoundButton,创建一个新的点击事件处理程序,将PlaySoundButton拖动到对象槽中,并选择PlaySound()函数。
-
在层次面板中选择PlaySoundButton,将AudioObject拖动到检查器视图中,以设置公共音频源变量AudioObject。同时,将AudioObject拖动到检查器视图中,以设置公共脚本变量AudioDestructScriptedObject,如下所示:
![如何操作...]()
-
在屏幕上创建一个名为DestoryWhenSoundFinishedButton的第二个 UI 按钮,按钮文本为
Destroy When Sound Finished,并将ButtonActions脚本附加到该按钮。 -
在层次面板中选择DestoryWhenSoundFinishedButton,创建一个新的点击事件处理程序,将PlaySoundButton拖动到GO槽中,然后选择DestroyAfterSoundStops()函数。
-
就像处理其他按钮一样,现在在层次面板中选择DestoryWhenSoundFinishedButton,将AudioObject拖动到检查器视图中,以设置公共脚本变量MyAudioDestructObect。
它是如何工作的...
名称称为AudioObject的 GameObject 包含一个音频源组件,用于存储和管理音频剪辑的播放。AudioObject还包含一个脚本组件,它是AudioDestructBehaviour类的实例。此脚本最初是禁用的。当启用时,每帧此对象(通过其Update()方法)都会测试音频源是否未播放(!audio.isPlaying)。一旦发现音频未播放,GameObject 将被销毁。
已创建了两个 UI 按钮。按钮PlaySoundButton调用PlaySound()方法。如果音频剪辑尚未播放,此方法将开始播放音频剪辑。
第二个按钮DestoryWhenSoundFinishedButton调用DestoryAfterSoundStops()方法。此方法在 GameObject AudioObject中启用脚本组件AudioDestructBehaviour——这样,当声音播放完毕后,该 GameObject 将被销毁。
相关内容
- 本章中防止音频剪辑在已播放时重新启动的菜谱
使用音频混音器添加音量控制
音量调整可能是一个非常重要的功能,尤其是如果你的游戏是独立的。毕竟,访问操作系统的音量控制可能会非常令人沮丧。在这个菜谱中,我们将使用新的音频混音器功能为音乐和声音效果创建独立的音量控制。
准备工作
对于这个配方,我们提供了一个名为Volume.unitypackage的 Unity 包,其中包含一个初始场景,包含配乐音乐和音效。文件位于1362_09_05文件夹中。
如何操作...
要将音量控制滑块添加到您的场景中,请按照以下步骤操作:
-
将
Volume.unitypackage导入到您的项目中。 -
打开Volume场景(位于资产 | Volume文件夹中)。播放场景,并使用W A S D键(按住Shift键以跑步)走向隧道中的半透明绿色墙。您将能够听到:
-
一个循环的配乐音乐
-
钟声响起
-
当角色与墙碰撞时,机器人语音
-
-
从项目视图中,使用创建下拉菜单将音频混音器添加到项目中。将其命名为MainMixer。双击它以打开音频混音器窗口。
-
从组视图中,突出显示主并单击+号以向主组添加一个子项。将其命名为音乐。然后,再次突出显示主并添加一个新的子组FX,如图所示:
![如何操作...]()
-
从混音器视图中,突出显示MainMixer并单击+号以向项目中添加一个新的混音器。将其命名为MusicMixer。然后,将其拖入MainMixer并选择音乐组作为其输出。重复此操作,通过选择效果组作为输出,将一个名为FxMixer的混音器添加到项目中:
![如何操作...]()
-
现在,选择MusicMixer。选择其主组并添加一个名为Soundtrack的子项。然后,选择FxMixer并为其主组添加两个子项:一个名为Speech,另一个名为Bells,如图所示:
![如何操作...]()
-
从层次视图中,选择DialogueTrigger对象。然后,在检查器视图中,将输出轨道更改为Audio Source组件中的FxMixer | Speech:
![如何操作...]()
-
现在,选择Soundtrack游戏对象。从检查器视图中,找到Audio Source组件并将其输出轨道更改为MusicMixer | Soundtrack:
![如何操作...]()
-
最后,从项目视图中的资产文件夹,选择信号预制体。从检查器视图中,访问其Audio Source组件并将其输出更改为FxMixer | Bells:
![如何操作...]()
-
从音频混音器窗口,选择MainMixer并选择其主轨道。然后,从检查器视图中,在衰减组件中的音量上右键单击。从上下文菜单中,选择如图所示的将“音量(主音量)暴露给脚本”。为音乐和效果轨道重复此操作:
![如何操作...]()
-
从顶部选择主混音器的音频混音器,访问暴露参数下拉菜单。然后,右键单击我的暴露参数并重命名为
总体音量。接着,将我的暴露参数 1重命名为音乐音量,将我的暴露参数 2重命名为效果音量。 -
在项目视图中,创建一个新的C# 脚本并将其重命名为
音量控制。 -
在您的编辑器中打开脚本,并用以下代码替换所有内容:
using UnityEngine; using UnityEngine.Audio; using System.Collections; public class VolumeControl : MonoBehaviour{ public AudioMixer myMixer; private GameObject panel; private bool isPaused = false; void Start(){ panel = GameObject.Find("Panel"); panel.SetActive(false); } void Update() { if (Input.GetKeyUp (KeyCode.Escape)) { panel.SetActive(!panel.activeInHierarchy); if(isPaused) Time.timeScale = 1.0f; else Time.timeScale = 0.0f; isPaused = !isPaused; } } public void ChangeMusicVol(float vol){ myMixer.SetFloat ("MusicVolume", Mathf.Log10(vol) * 20f); } public void ChangeFxVol(float vol){ myMixer.SetFloat ("FxVolume", Mathf.Log10(vol) * 20f); } public void ChangeOverallVol(float vol){ myMixer.SetFloat ("OverallVolume", Mathf.Log10(vol) * 20f); } } -
在层次结构视图中,使用创建下拉菜单向场景添加一个面板(创建 | UI | 面板)。请注意,它将自动向场景添加一个Canvas。
-
在层次结构视图中,使用创建下拉菜单向场景添加一个滑块(创建 | UI | 滑块)。将其设置为面板对象的子对象。
-
将滑块重命名为总体滑块。复制它,并将新副本重命名为音乐滑块。然后在检查器视图中,矩形变换组件,将Pos Y参数更改为
-40。 -
复制音乐滑块,并将新副本重命名为效果滑块。然后,将其Pos Y参数更改为
-70:![如何操作...]()
-
选择Canvas游戏对象,并向其添加音量控制脚本。然后,将音量控制的MyMixer字段填充为MainMixer:
![如何操作...]()
-
选择总体滑块组件。在滑块组件的检查器视图中,将最小值更改为
0.000025(或2.5e-05)。然后,在值改变时列表下方,点击+符号添加一个动作。从层次结构面板中,将Canvas拖入对象槽,并使用下拉菜单选择VolumeControl | ChangeOverallVol选项,如图所示。为了测试目的,将适当的选择器从仅运行时更改为编辑器和运行时。![如何操作...]()
-
重复前面的步骤,使用音乐滑块和效果滑块,但这次分别从下拉菜单中选择更改音乐音量和更改效果音量选项。
-
播放场景。您可以在按下键盘上的Escape键时访问滑块,并从那里调整音量设置。
它是如何工作的...
新的音频混音器功能与数字音频工作站(如 Logic 和 Sonar)类似工作。通过音频混音器,您可以通过将它们路由到具有单独音频轨道的特定组来组织和管理工作音频元素,从而调整音量级别和音效。
通过将我们的音频剪辑组织到两个组(音乐和效果)中,我们将主混音器建立为一个统一的音量控制器。然后,我们使用音频混音器暴露了主混音器每个轨道的音量级别,使它们可以通过我们的脚本访问。
此外,我们设置了一个基本的 GUI,包含三个滑块,当使用时,将它们的浮点值(在0.000025和1之间)作为参数传递给脚本中的三个特定函数:ChangeMusicVol、ChangeFxVol和ChangeOverallVol。这些函数反过来使用SetFloat命令在运行时有效地更改音量级别。然而,在传递新的音量级别之前,脚本将线性值(在0.000025和1之间)转换为音频混音器使用的分贝级别。这种转换是通过log(x) * 20数学函数计算的。
注意
对于有关将线性值转换为分贝级别以及相反的问题的完整解释,请查看 Aaron Brown 在www.playdotsound.com/portfolio-item/decibel-db-to-float-value-calculator-making-sense-of-linear-values-in-audio-tools/的优秀文章。
值得注意的是,VolumeControl脚本还包括代码来启用和禁用GUI和EventSystem,这取决于玩家是否按下 Escape 键来激活/禁用音量控制滑块。
一个非常重要的注意事项——不要更改任何MainMixer的音轨音量;请将它们保持在0 dB。原因是我们的VolumeControl脚本设置了它们的最大音量级别。对于一般调整,请使用辅助混音器MusicMixer和FxMixer。
还有更多...
这里有一些关于音频混音器的额外信息。
音频制作中的玩耍
对于暴露的参数有许多创意用途。例如,我们可以向音频通道添加如失真、镶边和合唱等效果,使用户能够操作虚拟音表/混音板。
参见
-
本章中的使用快照制作动态配乐配方
-
本章中关于平衡游戏内音频与 ducking的内容
使用快照制作动态配乐
动态配乐是根据游戏中玩家的行为而变化的,音乐上反映了角色的冒险地点或时刻。在本配方中,我们将实现一个配乐,它将改变两次;第一次是在进入隧道时,第二次是在从隧道末端出来时。为了实现这一点,我们将使用音频混音器的新快照功能。
快照是一种保存音频混音器状态的方法,保留音量级别、音频效果等偏好。我们可以通过脚本访问这些状态,创建混音之间的过渡,并为玩家旅程的每个时刻提供所需的音效氛围。
准备工作
对于这个食谱,我们准备了一个基本的游戏关卡,包含在名为DynamicSoundtrack的 Unity 包中,以及两个.ogg格式的音轨音频剪辑:Theme01_Percussion和Theme01_Synths。所有这些文件都可以在1362_09_06文件夹中找到。
如何操作...
要制作动态音轨,请按照以下步骤操作:
-
将
DynamicSoundtrack包和两个.ogg文件导入到您的 Unity 项目中。 -
打开名为动态的场景。
-
从项目视图,使用创建下拉菜单将音频混音器添加到项目中。将其命名为MusicMixer。双击它以打开音频混音器窗口。
-
从组视图,高亮显示主并点击+符号向主组添加一个子组。将其命名为音乐。然后,向音乐添加两个子组:打击乐器和合成器:
![如何操作...]()
-
从层次结构视图,创建一个新的空GameObject。将其命名为音乐。然后,向其添加两个空子 GameObject。将它们命名为打击乐器和合成器。
-
从项目视图,将名为Theme01_Percussion的音频剪辑拖动到层次结构中的打击乐器GameObject。选择打击乐器,在检查器视图中,访问音频源组件。将其输出更改为打击乐器 (MusicMixer),确保播放于唤醒选项被勾选,勾选循环选项,并确保其空间混合设置为2D,如图所示:
![如何操作...]()
-
现在,将Theme01_Synths音频文件拖动到合成器GameObject。从检查器视图,将其输出更改为合成器 (MusicMixer),确保播放于唤醒选项被勾选,勾选循环选项,并确保其空间混合设置为2D,如图所示:
![如何操作...]()
-
打开音频混音器并播放场景。我们现在将使用混音器为场景的开始设置音轨。当场景播放时,点击音频混音器顶部的在播放模式下编辑按钮,如图所示。然后,将合成器轨道的音量降低到-30 dB:
![如何操作...]()
-
现在,选择打击乐器轨道。右键点击衰减并在其之前添加高通效果:
![如何操作...]()
-
从检查器视图,将高通效果的截止频率更改为544.00 Hz:
![如何操作...]()
-
到目前为止,每一次变化都分配给了当前的快照。从快照视图,右键点击当前的快照并将其重命名为开始。然后,右键点击开始并选择复制选项。将新的快照重命名为隧道,如图所示:
![如何操作...]()
-
选择隧道快照。然后,从检查器视图中,将高通效果的截止频率更改为10.00 Hz:
![如何操作...]()
-
在隧道和开始快照之间切换。你将能够听到区别。
-
复制隧道快照,将其重命名为另一侧并选择它。
-
将合成器音轨的音量提升到0 dB:
![如何操作...]()
-
现在我们有了三个快照,是时候创建触发器以在它们之间进行转换了。从层次结构视图中,使用创建下拉菜单向场景添加一个立方体(创建 | 3D 对象 | 立方体)。
-
选择新的立方体并将其重命名为
SnapshotTriggerTunnel。然后,从检查器视图中访问盒子碰撞器组件并勾选是触发器选项,如图所示。同时,取消勾选其网格渲染器组件。最后,调整其大小和位置以适应场景隧道内部:![如何操作...]()
-
复制
SnapshotTriggerTunnel两次,并将它们重命名为SnapshotTriggerStart和SnapshotTriggerOtherSide。然后调整它们的大小和位置,使它们占据隧道入口前(角色所在的位置)和另一端之后,如图所示:![如何操作...]()
-
在项目视图中创建一个新的C# 脚本文件并将其重命名为
SnapshotTrigger。 -
在您的编辑器中打开脚本并将所有内容替换为以下代码:
using UnityEngine; using UnityEngine.Audio; using System.Collections; public class SnapshotTrigger : MonoBehaviour{ public AudioMixerSnapshot snapshot; public float crossfade; private void OnTriggerEnter(Collider other){ snapshot.TransitionTo (crossfade); } } -
保存您的脚本并将其附加到
SnapshotTriggerTunnel、SnapshotTriggerStart和SnapshotTriggerOtherSide对象。 -
选择
SnapshotTriggerTunnel。然后,从检查器视图中访问快照触发器组件,将快照设置为隧道,并将淡入淡出设置为2,如图所示:![如何操作...]()
-
通过将它们的快照分别设置为开始和另一侧来修改
SnapshotTriggerStart和SnapshotTriggerOtherSide。 -
测试场景。背景音乐将在角色从起点移动到隧道,然后进入另一侧时改变。
它是如何工作的...
快照功能允许您保存音频混合器状态(包括所有音量级别、每个过滤器设置等),以便您可以在运行时更改这些混音偏好,使音频设计更适合特定位置或游戏设置。对于这个配方,我们为玩家旅程中的不同时刻创建了三个快照:进入隧道前、隧道内和隧道外。我们使用了高通过滤器来使初始快照不那么强烈。我们还提高了合成器音轨的音量,以强调隧道外的开阔环境。希望音频混音的变化将与设置游戏正确氛围相协作。
为了激活我们的快照,我们在其中放置了触发碰撞体,并配备了我们的快照触发器组件,在其中我们设置了所需的快照和过渡(交叉淡入)到下一个快照所需的时间(秒)。实际上,我们脚本中的功能非常直接——snapshot.TransitionTo(crossfade)代码行简单地开始一个持续crossfade秒的过渡到所需的Snapshot。
还有更多...
这里有一些关于如何微调和自定义此配方的信息。
减少对多个音频剪辑的需求
您可能已经注意到,当高通滤波器的截止频率设置为10.00 Hz时,Theme01_Percussion音频剪辑听起来有多么不同。这是因为高通滤波器,正如其名称所暗示的,切断了音频信号的较低频率。在这种情况下,它将低音鼓衰减到听不见的水平,同时保持摇铃的声音。通过低通滤波器可以达到相反的效果。一个主要的好处是几乎可以在同一个音频剪辑中拥有两个独立的轨道。
处理音频文件格式和压缩率
为了避免音频质量损失,您应该根据目标平台使用适当的文件格式导入您的音频剪辑。如果您不确定使用哪种格式,请查看 Unity 关于此主题的文档,链接为docs.unity3d.com/Manual/AudioFiles.html。
将快照应用于背景噪音
尽管我们已经将快照应用于我们的音乐音轨,背景噪音也可以受益匪浅。如果您的角色穿越了显著不同的地方,从开阔空间过渡到室内环境,您应该考虑将快照应用于您的环境音频混合。但是,请注意为音乐和环境创建单独的音频混合器——除非您不介意音乐和环绕声音绑定到同一个快照。
用效果来发挥创意
在这个配方中,我们提到了高通和低通滤波器。然而,有许多效果可以使音频剪辑听起来截然不同。尝试一下!尝试应用如失真、镶边和合唱等效果。实际上,我们鼓励您尝试每一个效果,并玩转它们的设置。这些效果的创意使用可以为单个音频剪辑带来不同的表现。
参见
-
本章中关于使用音频混合器添加音量控制的配方
-
本章中关于平衡音轨音量与 ducking的配方
在游戏中平衡音频与 ducking
背景音乐在建立正确氛围方面可能很重要,但有时其他音频剪辑应该被强调,并且在该剪辑期间降低音乐音量。这种效果被称为** ducking。你可能需要它来产生戏剧效果(模拟爆炸发生后听力受损),或者你可能想确保玩家听到特定的信息。在这个食谱中,我们将学习如何在播放特定声音消息时通过降低音频来强调一段对话。为此效果,我们将使用新的音频混音器**在轨道之间传递信息。
准备工作
对于这个食谱,我们提供了soundtrack.mp3音频剪辑和一个名为Ducking.unitypackage的 Unity 包,其中包含一个初始场景。所有这些文件都位于1362_09_07文件夹内。
如何操作...
要将音频 ducking 应用到你的配乐中,请按照以下步骤操作:
-
将
Ducking.unitypackage和soundtrack.mp3导入到你的项目中。 -
打开Ducking场景(位于资产 | Ducking文件夹)。播放场景,并使用W A S D键(按Shift键跑步)走向隧道中的半透明绿色墙。你会听到robotDucking音频剪辑在角色与墙碰撞时播放。
-
从层次结构视图顶部的创建下拉菜单,选择创建空对象将新 GameObject 添加到场景中。命名为Soundtrack。
-
将你导入的soundtrack音频剪辑拖放到SoundtrackGameObject 中。然后,选择Soundtrack对象,从检查器视图,音频源组件,勾选循环选项。确保唤醒时播放选项被勾选,并将空间混合设置为2D,如图所示:
![如何操作...]()
-
再次测试场景。配乐音乐应该正在播放。
-
从项目视图,使用创建下拉菜单将音频混音器添加到项目中。命名为MainMixer。双击它以打开音频混音器窗口。
-
从组视图,高亮显示主并点击+号向主组添加一个子项。命名为音乐。然后,再次高亮显示主并添加一个名为效果的新子组,如图所示。最后,向主组添加第三个子项,命名为输入:
![如何操作...]()
-
从混音器视图,高亮显示MainMixer并点击+号向项目中添加一个新的混音器。命名为MusicMixer。然后,将其拖入MainMixer并将组音乐作为其输出。重复此操作,将名为FxMixer的混音器添加到项目中,选择效果组作为输出:
![如何操作...]()
-
现在,选择MusicMixer。选择其主(Master)组,并添加一个名为Soundtrack的子组。然后,选择FxMixer并添加一个名为Bells的子组,如图所示:
![如何操作...]()
-
从层次结构(Hierarchy)视图,选择DialogueTrigger对象。然后,在检查器(Inspector)视图,音频源(Audio Source)组件中,将输出(Output)轨道更改为MainMixer | Input:
![如何操作...]()
-
现在,选择Soundtrack游戏对象,并在检查器(Inspector)视图中,在音频源(Audio Source)组件中,将输出(Output)轨道更改为MusicMixer | Soundtrack:
![如何操作...]()
-
最后,从项目(Project)视图中的资产(Assets)文件夹,选择信号(Signal)预制体。从检查器(Inspector)视图,访问其音频源(Audio Source)组件,并将输出(Output)更改为FxMixer | Bells:
![如何操作...]()
-
打开音频混音器(Audio Mixer)窗口。选择MainMixer,选择音乐(Music)轨道控制器,右键单击衰减,并使用上下文菜单,在衰减前添加Duck Volume效果:
![如何操作...]()
-
现在,选择输入轨道,右键单击衰减(Attenuation),并使用上下文菜单,在衰减后添加发送(Send)
![如何操作...]()
-
在仍然选择输入轨道的情况下,转到检查器视图,并将发送中的接收(Receive)设置更改为音乐\Duck Volume,并设置其发送级别为
0.00 db,如图所示:![如何操作...]()
-
选择音乐(Music)轨道。从检查器(Inspector)视图,按照以下方式更改Duck Volume的设置:阈值:-
40.00 db;比率:300.00 %;攻击时间:100.00 ms;释放时间:2000.00 ms,如图所示:![如何操作...]()
-
再次测试场景。进入触发对象将导致音轨音量显著降低,2 秒后恢复到原始音量。
工作原理...
在这个配方中,除了音乐和声音效果(Sound FX),我们还创建了一个名为输入(Input)的组,我们将触发我们音乐轨道上附加的Duck Volume效果的音频剪辑路由到这个组。Duck Volume效果会在接收到比其阈值设置中指示的更响亮的输入时改变轨道的音量。在我们的例子中,我们将输入轨道作为输入发送,并调整设置,使得在接收到输入后的 0.1 秒内音量降低,输入停止后 2 秒恢复到原始值。音量降低的量由我们的比率(Ratio)300.00 %决定。调整设置值将更好地了解每个参数如何影响最终结果。此外,确保在触发声音播放时可视化图形。您将能够看到输入声音如何通过阈值,触发效果。

Duck Volume
此外,请注意我们已经组织了我们的音轨,以便其他声音剪辑(除了语音)不会影响音乐的音量——但是每个音乐剪辑都会受到发送到输入音轨的音频剪辑的影响。
参见
-
本章中的 使用音频混音器添加音量控制 菜单
-
本章中的 使用快照制作动态音轨 菜单
第十章. 与外部资源文件和设备协同工作
在本章中,我们将涵盖:
-
加载外部资源文件 - 使用 Unity 默认资源
-
加载外部资源文件 - 通过从互联网下载文件
-
加载外部资源文件 - 通过手动将文件存储在 Unity 资源文件夹中
-
保存和加载玩家数据 - 使用静态属性
-
保存和加载玩家数据 - 使用 PlayerPrefs
-
从游戏中保存截图
-
使用 PHP/MySQL 设置排行榜
-
从文本文件地图加载游戏数据
-
使用 Git 版本控制和 GitHub 托管管理 Unity 项目代码
-
通过 Unity Cloud 发布到多个设备
简介
对于某些项目,使用检查器窗口手动将导入的资产分配到组件槽位,然后构建并播放游戏,无需进一步更改即可正常工作。然而,也有很多时候,某种外部数据可以为游戏增加灵活性和功能。例如,它可能添加可更新或用户可编辑的内容;它可以使用户偏好和成就之间的场景,甚至游戏会话的记忆;使用代码在运行时读取本地或互联网文件内容可以帮助文件组织和分离游戏程序员和内容设计师之间的任务。拥有不同类型的资产和长期游戏记忆技术意味着为玩家和开发者提供广泛的机会,以提供丰富的体验。
整体概念
在继续介绍食谱之前,让我们退后一步,快速回顾一下资产文件和 Unity 游戏构建及运行过程的作用。与资产最直接的工作方式是将它们导入 Unity 项目,使用检查器窗口将资产分配给检查器中的组件,然后构建并播放游戏。

独立的可执行文件提供另一种可能的流程,即在游戏构建完成后,将文件添加到游戏的Resources文件夹中。这将支持游戏媒体资产开发者能够在开发和构建完成后提供资产的最终版本。
然而,另一种选择是使用WWW类在运行时从网络上动态读取资产;或者,也许是为了与高分或多人游戏服务器通信,发送和接收信息和文件。

当在本地或通过网页界面加载数据或保存数据时,记住可以使用的数据类型是很重要的。在编写 C#代码时,我们的变量可以是语言允许的任何类型,但当通过网页界面或使用 Unity 的PlayerPrefs类与本地存储通信时,我们在可以处理的数据类型上受到限制。Unity 的WWW类允许三种文件类型(文本文件、二进制音频剪辑和二进制图像纹理),但例如,对于 2D 用户界面,我们有时需要 Sprite 图像而不是纹理,因此我们在本章中提供了一个 C#方法,可以从纹理创建 Sprite。当使用PlayerPrefs类时,我们只能保存和加载整数、浮点数和字符串。同样,当使用 URL 编码数据与网络服务器通信时,我们被限制在可以放入字符串中的任何内容(我们包括一个基于 PHP 的在线高分食谱,用户分数可以通过这种方法加载和保存)。
最后,使用 Git 和 GitHub 这样的分布式版本控制系统(DVCS)管理 Unity 项目源代码,为代码更新的持续集成到工作构建中打开了新的工作流程。Unity Cloud 将从您的在线仓库中拉取更新的源代码项目,然后为指定的 Unity 版本和部署设备构建游戏。开发者将收到确认构建成功的电子邮件,或者列出任何构建失败的原因。本章的最后两个食谱展示了如何使用 Git 和 GitHub 管理代码,并使用 Unity Cloud 为多台设备构建项目。
注意
致谢:感谢以下机构发布Creative Commons (BY 3.0)许可的图标:Elegant Themes、Picol、Freepik、Yannick、Google、www.flaticon.com。
加载外部资源文件 – 使用 Unity 默认资源
在这个食谱中,我们将加载一个外部图像文件,并使用Unity 默认资源文件(在游戏编译时创建的库)将其显示在屏幕上。
注意
这种方法可能是存储和读取外部资源文件的最简单方法。然而,它只适用于资源文件的内容在编译后不会改变的情况,因为这些文件的内容被合并并编译到resources.assets文件中。
resources.assets文件可以在编译后的游戏的Data文件夹中找到。

准备工作
在1362_10_01文件夹中,我们为这个食谱提供了一个图像文件、一个文本文件和一个.ogg格式的音频文件:
-
externalTexture.jpg -
cities.txt -
soundtrack.ogg
如何操作...
要通过 Unity 默认资源加载数据,请执行以下操作:
-
创建一个新的 3D Unity 项目。
-
在项目窗口中,创建一个新的文件夹并将其重命名为
Resources。 -
导入
externalTexture.jpg文件并将其放置在Resources文件夹中。 -
创建一个 3D 立方体。
-
将以下 C# 脚本添加到您的立方体中:
using UnityEngine; using System.Collections; public class ReadDefaultResources : MonoBehaviour { public string fileName = "externalTexture"; private Texture2D externalImage; void Start () { externalImage = (Texture2D)Resources.Load(fileName); Renderer myRenderer = GetComponent<Renderer>(); myRenderer.material.SetTexture("_MainTex", externalImage); } } -
播放场景。纹理将被加载并在屏幕上显示。
-
如果您有另一个图像文件,将其副本放入
Resources文件夹。然后,在 检查器 窗口中,将公共文件名更改为您的图像文件名并再次播放场景。现在将显示新的图像。
它是如何工作的...
Resources.Load(fileName) 语句使 Unity 在其编译的项目数据文件 resources.assets 中查找名为 externalTexture 的文件内容。这些内容以纹理图像的形式返回,并存储在 externalImage 变量中。Start() 方法中的最后一个语句将脚本附加到的 GameObject 的纹理设置为 externalImage 变量。
注意
注意:传递给 Resources.Load() 的文件名字符串不包含文件扩展名(如 .jpg 或 .txt)。
还有更多...
有一些细节您不想错过。
使用此方法加载文本文件
您可以使用相同的方法加载外部文本文件。私有变量需要是字符串(用于存储文本文件内容)。Start() 方法使用一个临时的 TextAsset 对象来接收文本文件内容,而这个对象的文本属性包含要存储在私有变量 textFileContents 中的字符串内容:
public class ReadDefaultResourcesText : MonoBehaviour {
public string fileName = "textFileName";
private string textFileContents;
void Start () {
TextAsset textAsset = (TextAsset)Resources.Load(fileName);
textFileContents = textAsset.text;
Debug.Log(textFileContents);
}
}
最后,此字符串将在控制台上显示。

使用此方法加载和播放音频文件
您可以使用相同的方法加载外部音频文件。私有变量需要是 AudioClip 类型:
using UnityEngine;
using System.Collections;
[RequireComponent (typeof (AudioSource))]
public class ReadDefaultResourcesAudio : MonoBehaviour {
public string fileName = "soundtrack";
private AudioClip audioFile;
void Start (){
AudioSource audioSource = GetComponent<AudioSource>();
audioSource.clip = (AudioClip)Resources.Load(fileName);
if(!audioSource.isPlaying && audioSource.clip.isReadyToPlay)
audioSource.Play();
}
}
相关内容
参考本章中的以下食谱以获取更多信息:
-
通过手动将文件存储在 Unity 资源文件夹中加载外部资源文件
-
通过从互联网下载文件加载外部资源文件
通过从互联网下载文件加载外部资源文件
存储和读取文本文件数据的一种方法是将文本文件存储在网络上。在这个食谱中,下载、读取给定 URL 的文本文件内容,然后显示。
准备工作
对于这个食谱,您需要能够访问网络服务器上的文件。如果您运行一个本地的网络服务器,如 Apache,或者拥有自己的网络托管,那么您可以使用 1362_10_01 文件夹中的文件和相应的 URL。
否则,您可能发现以下网址很有用;因为它们是图像文件(Packt Publishing 标志)和文本文件(ASCII 艺术獾图片)的网络位置:
如何操作...
要通过从互联网下载它们来加载外部资源,请执行以下操作:
-
在 2D 项目中,创建一个新的
RawImageUI GameObject。 -
将以下 C#脚本类作为图像对象的组件添加:
using UnityEngine; using UnityEngine.UI; using System.Collections; public class ReadImageFromWeb : MonoBehaviour { public string url = "http://www.packtpub.com/sites/default/files/packt_logo.png"; IEnumerator Start() { WWW www = new WWW(url); yield return www; Texture2D texture = www.texture; GetComponent<RawImage>().texture = texture; } } -
播放场景。一旦下载完成,图像文件的内容将显示出来:
![如何操作...]()
它是如何工作的...
注意需要使用UnityEngine.UI包来执行此食谱。
当游戏开始时,我们的Start()方法启动名为LoadWWW()的协程方法。协程是一种可以在后台持续运行而不会停止或减慢游戏其他部分和帧率的函数。yield语句表示一旦可以为imageFile返回一个值,方法的其他部分就可以执行——也就是说,直到文件下载完成,不应尝试提取WWW对象变量的纹理属性。
一旦图像数据被加载,执行将超过yield语句。最后,附加脚本的RawImage GameObject 的texture属性将更改为从 Web 下载的图像数据(在www对象的texture变量中)。
还有更多...
有一些细节你不希望错过。
从纹理转换为精灵
在食谱中,我们使用了 UI RawImage,因此可以直接使用下载的Texture,但在某些情况下,我们可能希望使用Sprite而不是Texture。使用此方法从Texture创建Sprite对象:
private Sprite TextureToSprite(Texture2D texture){
Rect rect = new Rect(0, 0, texture.width, texture.height);
Vector2 pivot = new Vector2(0.5f, 0.5f);
Sprite sprite = Sprite.Create(texture, rect, pivot);
return sprite;
}
从 Web 下载文本文件
使用此技术下载文本文件:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class ReadTextFromWeb : MonoBehaviour {
public string url = "http://www.ascii-art.de/ascii/ab/badger.txt";
IEnumerator Start(){
Text textUI = GetComponent<Text>();
textUI.text = "(loading file ...)";
WWW www = new WWW(url);
yield return www;
string textFileContents = www.text;
Debug.Log(textFileContents);
textUI.text = textFileContents;
}
}
WWW 类和资源内容
WWW类定义了几个不同的属性和方法,允许下载的媒体资源文件数据被提取到适当的变量中,以便在游戏中使用。其中最有用的包括:
-
.text:一个只读属性,返回作为字符串的 Web 数据 -
.texture:一个只读属性,返回作为 Texture2D 图像的 Web 数据 -
.GetAudioClip():一个返回作为 AudioClip 的 Web 数据的方法
注意
更多关于 Unity WWW类的信息,请访问docs.unity3d.com/ScriptReference/WWW.html。
相关链接
参考本章中的以下食谱以获取更多信息:
-
通过 Unity 默认资源加载外部资源文件
-
加载外部资源文件 – 通过手动将文件存储在 Unity 资源文件夹中
加载外部资源文件 – 通过手动将文件存储在 Unity 资源文件夹中
有时,外部资源文件的内容可能需要在游戏编译后进行更改。在 Web 上托管资源文件可能不是一个选项。有一种手动存储和从编译后的游戏Resources文件夹中读取文件的方法,这使得这些文件在游戏编译后可以更改。
注意
此技术仅在将项目编译为 Windows 或 Mac 独立可执行文件时才有效——例如,对于 Web Player 构建,它将不起作用。
准备工作
1362_10_01文件夹提供了可用于此食谱的纹理图像:
externalTexture.jpg
如何操作...
要通过手动将文件存储在Resources文件夹中来加载外部资源,请执行以下操作:
-
在 2D 项目中创建一个新的 Image UI GameObject。
-
将以下 C#脚本类作为 Image 对象的组件添加:
using UnityEngine; using System.Collections; using UnityEngine.UI; using System.IO; public class ReadManualResourceImageFile : MonoBehaviour { private string fileName = "externalTexture.jpg"; private string url; private Texture2D externalImage; IEnumerator Start () { url = "file:" + Application.dataPath; url = Path.Combine(url, "Resources"); url = Path.Combine(url, fileName); WWW www = new WWW (url); yield return www; Texture2D texture = www.texture; GetComponent<Image>().sprite = TextureToSprite(texture); } private Sprite TextureToSprite(Texture2D texture){ Rect rect = new Rect(0, 0, texture.width, texture.height); Vector2 pivot = new Vector2(0.5f, 0.5f); Sprite sprite = Sprite.Create(texture, rect, pivot); return sprite; } } -
构建您的(Windows 或 Mac)独立可执行文件。
-
复制
externalTexture.jpg图像到您的独立应用程序的Resources文件夹。注意
每次编译后,你都需要手动将文件放置在
Resources文件夹中。当你创建 Windows 或 Linux 独立可执行文件时,还有一个与可执行应用程序文件一起创建的
_Data文件夹。Resources文件夹位于此Data`文件夹内。一个 Mac 独立应用程序的可执行文件看起来像一个单独的文件,但实际上是一个 MacOS
package文件夹。右键单击可执行文件并选择显示包内容。然后你将在Contents文件夹内找到独立应用程序的Resources文件夹。 -
运行您的独立游戏应用程序,图像将显示:
![如何操作...]()
工作原理...
注意,为了遵循这个食谱,需要使用System.IO和UnityEngine.UI包。
当可执行文件运行时,WWW对象会注意到 URL 以单词 file 开头,然后 Unity 会尝试在其Resources文件夹中找到外部资源文件,并加载其内容。
更多...
有些细节你不希望错过。
使用 Path.Combine()而不是"/"或"\"避免跨平台问题
Windows 和 Mac 文件系统中的文件路径分隔符字符不同(Windows 为反斜杠(\),Mac 为正斜杠(/))。然而,Unity 知道你正在将项目编译成哪种类型的独立应用程序,因此Path.Combine()方法将插入所需的文件 URL 中的适当分隔符斜杠字符。
参见
参考本章中的以下食谱以获取更多信息:
-
通过 Unity 默认资源加载外部资源文件
-
通过从互联网下载文件加载外部资源文件
保存和加载玩家数据 – 使用静态属性
在游戏过程中跟踪玩家的进度和用户设置对于给你的游戏带来更深的感受和内容至关重要。在这个食谱中,我们将学习如何使我们的游戏记住玩家在不同级别(场景)之间的得分。
准备工作
我们在1362_10_04文件夹中包含了一个名为game_HigherOrLower的 Unity 包中的完整项目。为了遵循这个食谱,我们将把这个包作为起点导入。
如何操作...
要保存和加载玩家数据,请按照以下步骤操作:
-
创建一个新的 2D 项目并导入
game_HigherOrLower包。 -
按顺序将每个场景添加到构建中(
scene0_mainMenu,然后scene1_gamePlaying,依此类推)。 -
通过多次玩游戏并检查场景内容,让自己熟悉游戏。游戏从
scene0_mainMenu场景开始,位于Scenes文件夹内。 -
让我们创建一个类来存储用户做出的正确和错误猜测的数量。创建一个新的 C#脚本名为
Player,代码如下:using UnityEngine; public class Player : MonoBehaviour { public static int scoreCorrect = 0; public static int scoreIncorrect = 0; } -
在
scene0_mainMenu场景的左下角,创建一个名为Text – score的 UI Text GameObject,包含占位文本Score: 99 / 99。![如何操作...]()
-
接下来,将以下 C#脚本附加到 UI GameObject Text – score:
using UnityEngine; using System.Collections; using UnityEngine.UI; public class UpdateScoreText : MonoBehaviour { void Start(){ Text scoreText = GetComponent<Text>(); int totalAttempts = Player.scoreCorrect + Player.scoreIncorrect; string scoreMessage = "Score = "; scoreMessage += Player.scoreCorrect + " / " + totalAttempts; scoreText.text = scoreMessage; } } -
在
scene2_gameWon场景中,将以下 C#脚本附加到主相机:using UnityEngine; public class IncrementCorrectScore : MonoBehaviour { void Start () { Player.scoreCorrect++; } } -
在
scene3_gameLost场景中,将以下 C#脚本附加到主相机:using UnityEngine; public class IncrementIncorrectScore : MonoBehaviour { void Start () { Player.scoreIncorrect++; } } -
保存你的脚本并玩游戏。当你从关卡(场景)进步到下一个关卡时,你会发现分数和玩家名称被记住,直到你退出应用程序。
它是如何工作的...
Player类使用静态(类)属性scoreCorrect和scoreIncorrect来存储当前正确和错误猜测的总数。由于这些是公共静态属性,任何场景中的任何对象都可以访问(设置或获取)这些值,因为静态属性在场景之间是记忆的。此类还提供了一个名为ZeroTotals()的公共静态方法,可以将这两个值重置为零。
当加载scene0_mainMenu场景时,所有带有脚本的 GameObject 将执行它们的Start()方法。名为Text – score的 UI Text GameObject 有一个UpdateScoreText类的实例作为脚本组件,因此脚本Start()方法将被执行,它从Player类检索正确和错误总数,创建关于当前分数的scoreMessage字符串,并更新文本属性,以便用户看到当前分数。
当游戏运行且用户猜对(高于)时,则加载场景scene2_gameWon。因此,在此场景中主相机的IncrementCorrectScore脚本组件的Start()方法被执行,它将Player类的scoreCorrect变量的值增加1。
当游戏运行且用户猜错(低于)时,则加载场景scene3_gameLost。因此,在此场景中主相机的IncrementIncorrectScore脚本组件的Start()方法被执行,它将Player类的scoreIncorrect变量的值增加1。
下次用户访问主菜单场景时,将从Player类读取正确和错误总数的最新值,并且屏幕上的 UI Text 将通知用户他们的游戏更新总分。
更多内容...
有一些细节你不希望错过。
在第一次尝试完成之前隐藏分数
显示零分并不太专业。让我们添加一些逻辑,以便只有在总尝试次数大于零时才显示分数(非空字符串):
void Start(){
Text scoreText = GetComponent<Text>();
int totalAttempts = Player.scoreCorrect + Player.scoreIncorrect;
// default is empty string
string scoreMessage = "";
if( totalAttempts > 0){
scoreMessage = "Score = ";
scoreMessage += Player.scoreCorrect + " / " + totalAttempts;
}
scoreText.text = scoreMessage;
}
参见
参考本章中的以下配方以获取更多信息:
- 使用 PlayerPrefs 保存和加载玩家数据
使用 PlayerPrefs 保存和加载玩家数据
虽然前面的配方说明了静态属性如何允许游戏在不同的场景之间记住值,但这些值一旦游戏应用程序退出就会被遗忘。Unity 提供了PlayerPrefs功能,允许游戏在不同的游戏会话之间存储和检索数据。

准备工作
这个配方基于前面的配方。如果你还没有完成前面的配方,我们在1362_10_05文件夹中包含了一个名为game_scoreStaticVariables的Unity包。为了使用这个包来遵循这个配方,你必须执行以下操作:
-
创建一个新的 2D 项目并导入
game_HigherOrLower包。 -
按顺序将每个场景添加到构建中(
scene0_mainMenu,然后scene1_gamePlaying,依此类推)。
如何做...
要使用PlayerPrefs保存和加载玩家数据,请按照以下步骤操作:
-
删除名为
Player的 C#脚本。 -
编辑名为
UpdateScoreText的 C#脚本,用以下代码替换Start()方法:void Start(){ Text scoreText = GetComponent<Text>(); int scoreCorrect = PlayerPrefs.GetInt("scoreCorrect"); int scoreIncorrect = PlayerPrefs.GetInt("scoreIncorrect"); int totalAttempts = scoreCorrect + scoreIncorrect; string scoreMessage = "Score = "; scoreMessage += scoreCorrect + " / " + totalAttempts; scoreText.text = scoreMessage; } -
现在,编辑名为
IncrementCorrectScore的 C#脚本,用以下代码替换Start()方法:void Start () { int newScoreCorrect = 1 + PlayerPrefs.GetInt("scoreCorrect"); PlayerPrefs.SetInt("scoreCorrect", newScoreCorrect); } -
现在,编辑名为
IncrementIncorrectScore的 C#脚本,用以下代码替换Start()方法:void Start () { int newScoreIncorrect = 1 + PlayerPrefs.GetInt("scoreIncorrect"); PlayerPrefs.SetInt("scoreIncorrect", newScoreIncorrect); } -
保存你的脚本并玩游戏。退出 Unity 然后重新启动应用程序。你会发现玩家的名字、等级和分数现在可以在游戏会话之间保持。
工作原理...
我们不需要Player类,因为这个配方使用了 Unity 提供的内置运行时类PlayerPrefs。
Unity 的PlayerPrefs运行时类能够在用户的机器上存储和访问信息(字符串、整数和浮点变量)。值存储在plist文件(Mac)或注册表(Windows)中,类似于网络浏览器的 cookies,因此可以在游戏应用会话之间记住这些值。
总正确和错误得分的值由IncrementCorrectScore和IncrementIncorrectScore类中的Start()方法存储。这些方法使用PlayerPrefs.GetInt("<variableName>")方法检索旧的总数,将其加 1,然后使用PlayerPrefs.SetInt("<variableName>")方法存储增加后的总数。
这些正确和错误的总数将在每次加载scene0_mainMenu场景时读取,并通过屏幕上的 UI Text 对象显示分数总数。
注意
更多关于PlayerPrefs的信息,请参阅 Unity 的在线文档:
docs.unity3d.com/ScriptReference/PlayerPrefs.html.
相关内容
参考本章中的以下配方以获取更多信息:
- 保存和加载玩家数据 – 使用静态属性
从游戏中保存截图
在此配方中,我们将学习如何捕获游戏内的快照,并将它们保存到外部文件。更好的是,我们将使其能够选择三种不同的方法。
注意
此技术仅在编译为 Windows 或 Mac 独立可执行文件时才有效——例如,它不适用于 Web Player 构建。
准备工作
为了遵循此配方,请将 screenshots 包导入到您的项目中,该包位于 1362_10_06 文件夹中。该包包括一个基本的地形和一个可以通过鼠标旋转的摄像机。
如何操作...
要从游戏中保存截图,请按照以下步骤操作:
-
导入
screenshots包并打开screenshotLevel场景。 -
将以下 C# 脚本添加到主摄像机:
using UnityEngine; using System.Collections; using System; using System.IO; public class TakeScreenshot : MonoBehaviour { public string prefix = "Screenshot"; public enum method{captureScreenshotPng, ReadPixelsPng, ReadPixelsJpg}; public method captMethod = method.captureScreenshotPng; public int captureScreenshotScale = 1; [Range(0, 100)] public int jpgQuality = 75; private Texture2D texture; private int sw; private int sh; private Rect sRect; string date; void Start(){ sw = Screen.width; sh = Screen.height; sRect = new Rect(0,0,sw,sh); } void Update (){ if (Input.GetKeyDown (KeyCode.P)){ TakeShot(); } } private void TakeShot(){ date = System.DateTime.Now.ToString("_d-MMM-yyyy-HH-mm-ss-f"); if (captMethod == method.captureScreenshotPng){ Application.CaptureScreenshot(prefix + date + ".png", captureScreenshotScale); } else { StartCoroutine(ReadPixels()); } } IEnumerator ReadPixels (){ yield return new WaitForEndOfFrame(); byte[] bytes; texture = new Texture2D (sw,sh,TextureFormat.RGB24,false); texture.ReadPixels(sRect,0,0); texture.Apply(); if (captMethod == method.ReadPixelsJpg){ bytes = texture.EncodeToJPG(jpgQuality); WriteBytesToFile(bytes, ".jpg"); } else if (captMethod == method.ReadPixelsPng){ bytes = texture.EncodeToPNG(); WriteBytesToFile(bytes, ".png"); } } private void WriteBytesToFile(byte[] bytes, string format){ Destroy (texture); File.WriteAllBytes(Application.dataPath + "/../"+prefix + date + format, bytes); } } -
保存脚本并将其附加到主摄像机游戏对象,通过从项目视图拖动到层次结构视图中的主摄像机游戏对象。
-
访问 截图 组件。将 捕获方法 设置为 捕获 Png 截图。将 捕获截图缩放 更改为 2。
注意
如果您希望图像文件的名称不同于
Screenshot,则可以在 前缀 字段中更改它。![如何操作...]()
-
播放场景。每次按下 P 键时,都会在项目文件夹中保存一个大小为原始尺寸两倍的新截图。
工作原理...
Start() 方法创建一个具有屏幕宽度和高度的 Rect 对象。每一帧 Update() 方法都会测试是否按下了 P 键。
一旦脚本检测到按下了 P 键,屏幕就会被捕获并作为图像文件存储在可执行文件相同的文件夹中。如果选择了 捕获截图 Png 选项,脚本将调用一个名为 CaptureScreenshot() 的内置 Unity 函数,该函数能够放大原始屏幕尺寸(在我们的例子中,基于我们脚本的 Scale 变量)。如果没有,图像将通过 ReadPixels 函数捕获,编码为 PNG 或 JPG,并通过 WriteAllBytes 函数写入。
在所有情况下,创建的文件都将具有适当的 ".png" 或 ".jpg" 文件扩展名,以匹配其图像文件格式。
更多内容...
我们包括使用 ReadPixel 函数的选项,以展示如何在不使用 Unity 的 CaptureScreenshot() 函数的情况下将图像保存到磁盘。此方法的一个优点是它可以适应只捕获和保存屏幕的一部分。不过,我们脚本中的 captureScreenshotScale 变量不会影响使用 ReadPixel 函数创建的截图。
使用 PHP/MySQL 设置排行榜
当玩家达到高分时,排行榜会使游戏更有趣。即使是单人游戏也可以与共享的基于网络的排行榜进行通信。本食谱包括客户端(Unity)代码以及设置和从 MySQL 数据库获取玩家分数的 Web 服务器端(PHP)脚本。

准备工作
本食谱假设您要么有自己的网络托管,要么正在运行本地网络服务器和数据库服务器,例如 XAMPP 或 MAMP。您的网络服务器需要支持 PHP,并且您还需要能够创建 MySQL 数据库。
本食谱的所有 SQL、PHP 和 C#脚本都可以在1362_10_07文件夹中找到。
由于场景包含多个 UI 元素,而食谱的代码是与 PHP 脚本和 SQL 数据库的通信,在1362_10_07文件夹中,我们提供了一个名为PHPMySQLeaderboard的Unity包,其中包含一个为 Unity 项目设置好的场景。
注意
如果您在公共网站上托管排行榜,出于安全原因,您将更改数据库、数据库用户和密码的名称。您还应该实现某种形式的秘密游戏代码,如更多内容部分所述。
如何操作...
要使用 PHP 和 MySQL 设置排行榜,请执行以下操作:
-
在您的服务器上,创建一个新的 MySQL 数据库,命名为
cookbook_highscores。 -
在您的服务器上,创建一个新的数据库用户(用户名=
cookbook,密码=cookbook),并授予对该数据库的完全访问权限。 -
在您的服务器上,执行以下 SQL 语句以创建名为
score_list的数据库表:CREATE TABLE `score_list` ( `id` int(11) NOT NULL AUTO_INCREMENT, `player` varchar(25) NOT NULL, `score` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1; -
将提供的 PHP 脚本文件复制到您的网络服务器:
-
index.php -
scoreFunctions.php -
htmlMenu.php
-
-
创建一个新的 2D Unity 项目,并解压名为
PHPMySQLeaderboard的 Unity 包。 -
运行提供的场景,并点击按钮以使 Unity 与可以访问高分数据库的 PHP 脚本进行通信。
工作原理...
玩家的分数存储在 MySQL 数据库中。通过提供的 PHP 脚本可以方便地访问数据库。在我们的示例中,所有 PHP 脚本都放置在本地 Apache 网络服务器的根目录中。因此,脚本通过http://localhost:8888/进行访问。然而,由于 URL 是一个公开的字符串变量,这可以在运行之前设置为服务器和网站代码的位置。
所有访问都通过名为index.php的 PHP 文件进行。实现了五个操作,每个操作都通过在 URL 末尾添加操作名称来指示(这是GET HTTP方法,有时用于网页表单。例如,下次您在 Google 上搜索时,请查看浏览器的地址栏)。操作及其参数(如果有)如下:
-
action = html:此操作请求返回列出所有玩家分数的 HTML 文本。此操作不接受任何参数。它返回:HTML 文本。 -
action = xml: 这个操作请求返回列出所有玩家得分的 XML 文本。此操作不接收任何参数。它返回:XML 文本。 -
action = reset: 这个操作请求用一组默认的玩家名称和得分值替换数据库表中的当前内容。此操作不接收任何参数。它返回:字符串reset。 -
action = get: 这个操作请求找到指定玩家的整数得分。它接收的参数形式为player = matt。它返回:整数得分。 -
action = set: 这个操作请求将指定玩家的得分存储到数据库中(但仅当这个新得分高于当前存储的得分时)。它接收的参数形式为player = matt, score = 101。它返回:整数得分(如果数据库更新成功),否则返回负值(表示没有发生更新)。
Unity 场景中有五个按钮(对应五个操作),这些按钮设置了要添加到 URL 中的相应操作和参数,以便通过LoadWWW()方法对网络服务器进行下一次调用。每个按钮的OnClick操作都已设置,以调用主相机的WebLeaderBoard C#脚本的相应方法。
同时也有三个 UI 文本对象。第一个显示发送到服务器的最新 URL 字符串。第二个显示从服务器接收到的响应消息中提取的整数值(如果收到其他数据,则显示消息“not an integer”)。第三个 UI 文本对象位于一个面板中,已经足够大,可以显示从服务器接收到的完整、多行文本字符串(该字符串存储在textFileContents变量中)。
三个 UI 文本对象已被分配给主相机的WebLeaderBoard C#脚本的公共变量。当场景首次启动时,Start()方法调用UpdateUI()方法来更新三个文本 UI 元素。当点击任何按钮时,将调用WebLeaderBoard方法的相应方法,该方法构建带有参数的 URL 字符串,然后调用LoadWWW()方法。此方法向 URL 发送请求,并通过作为协程的特性等待接收响应。然后它将接收到的内容存储在textFileContents变量中,并调用UpdateUI()方法。
还有更多...
以下章节将为您调整和定制这个食谱:
将完整的排行榜数据作为 XML 提取出来,用于在 Unity 中显示
可以从 PHP 网络服务器检索到的 XML 文本为 Unity 游戏提供了一个有用的方法,允许游戏从数据库中检索完整的排行榜数据集。然后,排行榜可以在 Unity 游戏中显示给用户(可能以某种漂亮的 3D 方式或通过游戏一致的 GUI 显示)。
使用秘密游戏代码来保护您的排行榜脚本
展示的 Unity 和 PHP 代码演示了一个简单、不安全的基于网络的排行榜。为了防止玩家通过虚假分数黑客攻击排行榜,通常会在通信中编码某种形式的秘密游戏代码(或密钥)。只有包含正确代码的更新请求才会实际导致数据库发生变化。
Unity 代码将秘密密钥(在这个例子中,是名为 harrypotter 的字符串)与通信相关的内容结合起来——例如,同一个 MySQL/PHP 排行榜可能对不同游戏有不同的数据库记录,这些游戏通过游戏 ID 进行识别:
// Unity Csharp code
string key = "harrypotter"
string gameId = 21;
string gameCode = Utility.Md5Sum(key + gameId);
服务器端 PHP 代码将接收加密的游戏代码,以及用于创建该加密代码的游戏数据片段(在这个例子中,是游戏 ID 和 MD5 散列函数,这两个函数在 Unity 和 PHP 中都是可用的)。秘密密钥(harrypotter)与游戏 ID 结合使用,创建一个可以与从 Unity 游戏(或任何尝试与排行榜服务器脚本通信的用户代理或浏览器)接收的代码进行比较的加密代码。只有当服务器上创建的游戏代码与随数据库操作请求发送的代码匹配时,才会执行数据库操作。
// PHP – security code
$key = "harrypotter"
$game_id = $_GET['game_id'];
$provided_game_code = $_GET['game_code'];
$server_game_code = md5($key.$game_id);
if( $server_game_code == $provided_game_code ) {
// codes match - do processing here
}
参见
参考以下配方以获取更多信息:
- 在第十一章防止游戏在未知服务器上运行中,通过额外功能和优化改进游戏,而不是为每个游戏等级手动创建和放置每个 GameObject。
从文本文件地图加载游戏数据
在这个配方中,我们可以创建文本文件,其中包含行和列的字符,每个字符对应于要在相应位置创建的 GameObject 的类型。在这个配方中,我们将使用文本文件和一组预制精灵来显示经典游戏 NetHack 的屏幕文本数据的图形版本。

准备工作
在 1362_10_08 文件夹中,我们为这个配方提供了以下两个文件:
-
level1.txt(一个文本文件,代表一个等级) -
absurd128.png(Nethack 的 128 x 128 精灵图集)。
等级数据来自 Nethack 维基百科页面,精灵图集来自 SourceForge:
注意,我们还包含了一个包含所有预制件设置的 Unity 包,因为这是一项费力的任务。
如何做到这一点...
要从文本文件地图加载游戏数据,请执行以下操作:
-
导入名为
level1.txt的文本文件和名为absurd128.png的图像文件。 -
在检查器中选择
absurd128.png,并将纹理类型设置为精灵(2D/uGUI),将精灵模式设置为多个。 -
在精灵编辑器中编辑这个精灵,选择类型为网格,并将像素大小设置为
128x128,然后应用这些设置。![如何操作...]()
-
在项目面板中,点击指向右侧的白三角以展开图标,以单独显示这个精灵图集中的所有精灵。
![如何操作...]()
-
将名为
absurd128_175的精灵拖放到场景中。 -
在项目面板中创建一个新的预制体,命名为
corpse_175,并将其从场景拖动到这个空白预制体精灵absurd128_175上。现在,从场景中删除精灵实例。你现在创建了一个包含精灵175的预制体。 -
对以下精灵重复此过程(即为每个精灵创建预制体):
-
floor_848 -
corridor_849 -
horiz_1034 -
vert_1025 -
door_844 -
potion_675
-
chest_586 -
alter_583 -
stairs_up_994 -
stairs_down_993 -
wizard_287
-
-
在检查器中选择主相机,并确保它设置为正交相机,大小为20,清除标志为实色,背景为黑色。
-
将以下 C#代码附加到主相机上,作为名为
LoadMapFromTextfile的脚本类:using UnityEngine; using System.Collections; using System.Collections.Generic; public class LoadMapFromTextfile : MonoBehaviour { public TextAsset levelDataTextFile; public GameObject floor_848; public GameObject corridor_849; public GameObject horiz_1034; public GameObject vert_1025; public GameObject corpse_175; public GameObject door_844; public GameObject potion_675; public GameObject chest_586; public GameObject alter_583; public GameObject stairs_up_994; public GameObject stairs_down_993; public GameObject wizard_287; public Dictionary<char, GameObject> dictionary = new Dictionary<char, GameObject>(); void Awake(){ char newlineChar = '\n'; dictionary['.'] = floor_848; dictionary['#'] = corridor_849; dictionary['('] = chest_586; dictionary['!'] = potion_675; dictionary['_'] = alter_583; dictionary['>'] = stairs_down_993; dictionary['<'] = stairs_up_994; dictionary['-'] = horiz_1034; dictionary['|'] = vert_1025; dictionary['+'] = door_844; dictionary['%'] = corpse_175; dictionary['@'] = wizard_287; string[] stringArray = levelDataTextFile.text.Split(newlineChar); BuildMaze( stringArray ); } private void BuildMaze(string[] stringArray){ int numRows = stringArray.Length; float yOffset = (numRows / 2); for(int row=0; row < numRows; row++){ string currentRowString = stringArray[row]; float y = -1 * (row - yOffset); CreateRow(currentRowString, y); } } private void CreateRow(string currentRowString, float y) { int numChars = currentRowString.Length; float xOffset = (numChars/2); for(int charPos = 0; charPos < numChars; charPos++){ float x = (charPos - xOffset); char prefabCharacter = currentRowString[charPos]; if (dictionary.ContainsKey(prefabCharacter)){ CreatePrefabInstance( dictionary[prefabCharacter], x, y); } } } private void CreatePrefabInstance(GameObject objectPrefab, float x, float y){ float z = 0; Vector3 position = new Vector3(x, y, z); Quaternion noRotation = Quaternion.identity; Instantiate (objectPrefab, position, noRotation); } } -
选择主相机,将适当的预制体拖放到检查器中的预制体槽位,用于
LoadMapFromTextfile脚本组件。![如何操作...]()
-
当你运行场景时,你会看到一个基于精灵的 Nethack 地图出现,使用你的预制体。
它是如何工作的...
精灵图集被自动切割成数百个 128 x 128 像素的精灵方块。我们从这些精灵中创建了一些预制体对象,以便在需要时在运行时创建副本。
文件名为level1.txt的文本文件包含文本字符的行。每个非空格字符表示精灵预制体应该实例化的位置(列 = X,行 = Y)。在Start()方法中声明并初始化了一个名为dictionary的 C#字典变量,用于将特定的预制体 GameObject 与文本文件中的某些特定字符关联起来。
Awake()方法使用换行符作为分隔符将字符串拆分为一个数组。因此,现在我们有了stringArray,其中包含文本数据的每一行的条目。BuildMase(…)方法使用stringArray调用。
BuildMaze(…)方法查询数组以找到其长度(此级别的数据行数),并将yOffSet设置为这个值的一半。这样做是为了允许将预制体放置在Y = 0上方和下方的一半,因此(0,0,0)是级别地图的中心。使用for循环从数组中读取每一行的字符串。它将字符串传递给CreateRow(…)方法,并附带与当前行对应的 Y 值。
CreateRow(…)方法提取字符串的长度,并将xOffSet设置为这个值的一半。这样做是为了允许将预制体放置在X = 0的左侧和右侧各一半,因此(0,0,0)是关卡地图的中心。使用for循环从当前行的字符串中读取每个字符,并且(如果我们的字典中有该字符的条目)则调用CreatePrefabIInstance (…)方法,传递字典中该字符的预制体引用以及x和y值。
CreatePrefabInstance(…)方法在(x, y, z)位置实例化给定的预制体,其中z始终为零,并且没有旋转(Quarternion.identity)。
使用 Git 版本控制和 GitHub 托管管理 Unity 项目代码
分布式版本控制系统(DVCS)正成为软件开发人员日常使用的必备工具。Unity 项目的问题可能在于每个项目中都有许多二进制文件。在本地系统的 Unity 项目目录中,也有许多文件对于存档/共享不是必需的,例如特定操作系统的缩略图文件、垃圾文件等。最后,一些 Unity 项目文件夹本身也不需要存档,例如 Temp 和 Library。
虽然 Unity 提供了自己的“资产服务器”,但许多小型游戏开发者选择不为此额外功能付费。此外,Git 和 Mercurial(最常见的 DVCS)是免费的,并且可以与任何需要维护的文档集(任何编程语言中的程序、文本文件等)一起使用。因此,学习如何使用第三方、行业标准 DVCS 来处理 Unity 项目是有意义的。实际上,这本书的文档都是使用私人 GitHub 仓库存档和版本控制的!
在这个菜谱中,我们将通过 Unity 应用程序设置和使用 GitHub GUI 客户端应用程序的组合来设置一个用于 GIT DVCS 的 Unity 项目。
注意
我们就是这样创建了一个真实的项目——一个吃豆人风格的游戏,你可以通过公共 GitHub 的 URL 进行探索和下载/拉取,该 URL 可在 github.com/dr-matt-smith/matt-mac-man 找到。
准备工作
这个菜谱可以用于任何 Unity 项目。在1362_10_09文件夹中,我们提供了一个matt-mac-man游戏的 Unity 包,如果你希望使用它——在这种情况下,在 Unity 中创建一个新的 2D 项目,并导入此包。
由于这个菜谱展示了在 GitHub 上托管代码,如果你还没有,你需要创建一个(免费)GitHub 账户在 github.com。
在开始这个菜谱之前,你需要安装 Git 和 GitHub 客户端应用程序。
了解如何操作,并从以下链接下载客户端:
如何操作...
要通过 Unity 默认资源加载外部资源,请执行以下操作:
-
在您的 Unity 项目根目录中,将以下代码添加到名为
.gitignore的文件中(确保文件名以点开头):# =============== # # Unity generated # # =============== # Temp/ Library/ # ===================================== # # Visual Studio / MonoDevelop generated # # ===================================== # ExportedObj/ obj/ *.svd *.userprefs /*.csproj *.pidb *.suo /*.sln *.user *.unityproj *.booproj # ============ # # OS generated # # ============ # .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db小贴士
这个特殊的文件(
.gitignore)告诉版本控制系统哪些文件不需要存档。例如,我们不需要记录 Windows 或 Mac 的图片缩略图文件(DS_STORE或Thumbs.db)。 -
通过导航到编辑 | 项目设置 | 编辑器,在检查器中打开编辑设置。
-
在编辑设置中,将版本控制模式设置为可见元文件。
-
在编辑设置中,将资产序列化模式设置为强制文本。
![如何操作...]()
-
保存您的项目,以便这些新设置被存储。然后,关闭 Unity 应用程序。
-
登录您的 GitHub 账户。
-
在您的 GitHub 主页上,点击绿色的新建按钮以开始创建一个新的仓库。
![如何操作...]()
-
给您的新的仓库起一个名字(我们选择了matt-mac-man)并勾选使用 README 初始化此仓库选项。
![如何操作...]()
-
在您的计算机上启动 GitHub 客户端应用程序,通过导航到文件 | 克隆仓库 ... 从提供的列表中选择您的新仓库(对我们来说,是 matt-mac-man),然后点击克隆按钮到该仓库。
![如何操作...]()
-
您将被询问在哪里存储此仓库在您的本地计算机上(我们简单地选择了桌面)。现在,您将在您的计算机磁盘上看到一个带有仓库名称的文件夹,其中包含一个隐藏的
.git文件夹和一个名为README.md的单个文件。![如何操作...]()
-
现在,将以下文件和文件夹从您的 Unity 项目中复制到这个本地仓库文件夹中:
-
.gitignore -
/Assets -
/Library -
/ProjectSettings
![如何操作...]()
-
-
在您的 GitHub 客户端应用程序中,您现在将看到许多未提交的更改。为您的第一次提交输入一个简短的注释(我们输入了标准注释—
v0.1 – first commit),然后点击提交 & 同步以将此 Unity 项目文件夹的内容推送到您的 GitHub 账户仓库。![如何操作...]()
-
现在,如果您访问您的 GitHub 项目页面,您将看到所有这些 Unity 项目文件都可供人们下载到他们的电脑上,无论是作为 ZIP 存档,还是使用 Git 客户端进行克隆。
![如何操作...]()
工作原理...
被称为.gitngnore的特殊文件列出了所有不应存档的文件和目录。
将 Unity 的编辑器设置中的版本控制模式改为元文件确保 Unity 将其关联的元文件中每个资产所需的管理数据存储起来。选择可见而不是隐藏只是避免任何混淆,即是否 GIT 会记录元文件——无论可见与否,GIT 都会记录它们。因此,通过使它们可见,对于与文件一起工作的开发者来说,很明显它们将被包括在内。
将 Unity 的编辑器设置中的资产序列化模式改为强制文本尝试解决使用大型二进制文件管理更改的一些困难。Unity 项目通常有很多二进制文件,例如.unity场景文件、预制件等。似乎有一些关于最佳设置的争论;我们发现强制文本效果很好,因此,我们现在将使用这个设置。你会在 GitHub 上看到两个提交,因为第一个是在我们创建新仓库时,第二个是我们使用 GitHub 客户端对仓库进行第一次提交,当时我们将所有代码添加到本地仓库并推送到远程服务器。
还有更多...
有些细节你不希望错过。
了解更多关于分布式版本控制系统(DVCS)
以下视频链接是关于分布式版本控制系统(DVCS)的简要介绍:
注意,Fogcreek Kiln 的“和谐”功能现在允许使用相同的 Kiln 仓库在 GIT 和 Mercurial 之间无缝工作:
使用 Bitbucket 和 SourceTree
如果你更喜欢在 Unity 项目中使用 Bitbucket 和 SourceTree,你可以在以下 URL 找到一个很好的教程:
使用命令行而不是 Git 客户端应用程序
对于许多人来说,使用 GUI 客户端,如 GitHub 应用程序,是使用 DVCS 的温和介绍,但最终,你将想要了解更多并掌握在命令行中工作。
由于 Git 和 Mercurial 都是开源的,因此有很多优秀的免费在线资源可用。以下是一些入门的好资源:
-
了解 Git 的所有内容,下载免费的 GUI 客户端,甚至可以通过以下 URL 免费在线访问《Pro Git》一书(由 Scott Chacon 编写),该书通过创意共享许可提供:
-
你将找到一个在线交互式的 Git 命令行来练习:
-
主 Mercurial 网站,包括免费在线访问Bryan O'Sullivan 编写的《Mercurial: The Definitive Guide》(通过开放出版许可),可在以下网址获得:
-
SourceTree 是一个免费的 Mercurial 和 Git 图形用户界面客户端,可在以下位置获取:
参见
参考以下配方以获取更多信息:
- 通过 Unity Cloud 发布到多个设备
通过 Unity Cloud 发布到多个设备
本章中 Git 配方的一个原因是为了让您为近年来 Unity 开发者提供的一项最激动人心的新服务做好准备——Unity Cloud!Unity Cloud 会为您构建不同设备上不同版本的项目的工作全部自动化——您将更新的 Unity 项目 PUSH 到您的在线分布式版本控制系统(如 GitHub)。然后,Unity Cloud 将看到更新并 PULL 您的新代码,并为您设置的设备/部署平台范围构建您的游戏。
准备工作
首先,登录 Unity Cloud Build 网站并创建账户:
对于这个配方,您需要访问项目源代码。如果您没有自己的(例如,您还没有完成本章中的 Git 配方),那么您可以自由使用在公共 GitHub URL 上可用的 matt-mac-man 项目:
注意
一个测试项目最初构建失败的共同原因是忘记至少将一个场景添加到项目的构建设置中。
如何操作...
要通过 Unity Default Resources 加载外部资源,请执行以下操作:
-
登录您的 Unity Cloud Build 账户。
-
在项目页面,点击添加新项目按钮。
![如何操作...]()
-
接下来,您需要添加源代码的 URL 和源代码管理方法(SCM)。对于我们的项目,我们输入了matt-mac-man URL,并将 SCM 设置为GIT。
![如何操作...]()
-
接下来,您需要输入一些设置。Unity Cloud Build 会默认选择您的源代码项目名称作为应用程序名称(大多数情况下,这是可以的)。您需要输入一个Bundle ID——通常,这里使用您网站 URL 的反向来确保应用程序名称加上Bundle ID是唯一的。因此,我们输入了
com.mattsmithdev。除非测试代码的分支,默认的 master 分支就足够了,同样,除非测试子文件夹,默认(无子文件夹)就足够了。除非您正在使用最新的“beta”版本,否则应该将Unity 版本选项保留为默认的始终使用最新版本。最后,检查您希望创建的构建选项。请注意,如果您为 iOS 构建,则需要设置 Apple 代码;但您将能够立即为 Unity Web Player 和 Android 进行构建。![如何操作...]()
-
接下来是应用的“凭证”。除非您有 Android 凭证,否则您可以选择默认的“开发”凭证。但这意味着当用户安装应用程序时,会收到警告。
-
Unity Cloud 将开始构建您的应用程序——这需要几分钟时间(取决于他们服务器上的负载)。
-
构建完成后,您会收到一封电子邮件(针对每个部署目标——因此,我们为 Web Player 和 Android 分别收到了一封)。如果构建失败,您仍然会收到电子邮件,并且可以查看日志以了解构建失败的原因。
![如何操作...]()
-
您可以立即播放 Web Player 版本:
![如何操作...]()
-
要测试 Android 或 iOS,您需要将其下载到设备上(从 Unity Cloud 网络服务器),然后玩游戏:
![如何操作...]()
它是如何工作的...
Unity Cloud 从 DVCS 系统(如 GitHub)拉取您的项目源代码。然后,它使用为 Unity 版本和部署平台选择的设置编译您的代码(在本配方中我们选择了 Web Player 和 Android)。如果构建成功,Unity Cloud 将构建的应用程序可供下载和运行。
还有更多...
有一些细节您不想错过。
了解更多关于 Unity Cloud 的信息
在 Unity Cloud 网站的“支持”部分(登录后)和 Unity 主网站的“云构建”信息页面了解更多信息:
另请参阅
- 更多信息请参阅 使用 Git 版本控制和 GitHub 托管管理 Unity 项目代码 的配方
第十一章.通过额外功能和优化改进游戏
在本章中,我们将涵盖以下主题:
-
暂停游戏
-
实现慢动作
-
防止你的游戏在未知服务器上运行
-
状态驱动行为 Do-It-Yourself 状态
-
使用状态设计模式实现状态驱动行为
-
通过在死亡时间销毁对象来减少对象数量
-
通过尽可能禁用对象来减少启用对象的数量
-
通过尽可能使对象无效来减少活动对象的数量
-
通过使用委托和事件以及避免 SendMessage!来提高效率
-
使用协程定期执行方法,但与帧率无关
-
使用协程将长时间计算分散到几个帧上
-
通过测量最大和最小帧率(FPS)来评估性能
-
使用 Unity 性能分析器识别性能瓶颈
-
使用 Do-It-Yourself 性能分析识别性能瓶颈
-
缓存 GameObject 和组件引用以避免昂贵的查找
-
使用 LOD 组提高性能
-
通过设计批处理绘制调用以减少绘制调用次数来提高性能
简介
本章前三个食谱提供了一些为你的游戏添加额外功能(暂停、慢动作和确保在线游戏安全)的想法。接下来的两个食谱然后展示了通过管理状态及其转换来管理游戏复杂性的方法。
本章剩余的食谱提供了如何调查和改进游戏效率和性能的示例。每个优化食谱都首先陈述一个它所体现的优化原则。
整体情况
在继续食谱之前,让我们退后一步,思考 Unity 游戏的不同部分以及它们的构建和运行时行为如何影响游戏性能。
游戏由几种不同类型的组件组成:
-
音频资源
-
2D 和 3D 图形资源
-
文本和其他文件资源
-
脚本
当游戏运行时,CPU 和 GPU 有许多相互竞争的处理需求,包括:
-
音频处理
-
脚本处理
-
2D 物理处理
-
3D 物理处理
-
图形渲染
-
GPU 处理
减少图形计算复杂度并提高帧率的办法之一是在可能的情况下使用更简单的模型——这就是细节级别(LOD)的降低。一般策略是识别那些使用更简单模型不会降低用户体验的情况。通常,这些情况包括模型只占据屏幕的一小部分(因此模型中的细节较少不会改变用户所看到的内容),当物体在屏幕上快速移动时(因此用户不太可能有时间注意到细节较少),或者我们确信用户的视觉焦点在其他地方(例如,在赛车游戏中,用户不会关注树木的质量,而是关注前方的道路)。我们在本章中提供了一个 LOD 食谱,使用 LOD 组提高性能。
Unity 的绘制调用批处理可能实际上比您或您的团队的三维建模师在减少三角形/顶点几何形状方面更有效率。因此,可能的情况是,通过手动简化三维模型,您已经移除了 Unity 应用其高度有效的顶点减少算法的机会;然后,对于小模型,几何复杂性可能比大模型更大,因此小模型可能导致游戏性能降低!一个食谱提供了从多个来源收集的建议和辅助不同策略的工具位置,以尝试减少绘制调用并提高图形性能。
我们将介绍几个食谱,让您能够分析实际的处理时间和帧率,以便您可以收集数据以确认您的设计决策是否带来了预期的效率提升。
| "你的 CPU 预算有限,你必须接受这一点" | ||
|---|---|---|
| --约阿希姆·安特,Unite-07 |
最后,您特定的游戏项目的最佳启发式策略平衡只能通过投入时间和辛勤工作,以及某种形式的性能分析调查来发现。某些策略(如缓存以减少组件反射查找)可能应该是所有项目的标准做法,而其他策略可能需要针对每个独特的游戏和级别进行调整,以找到哪些方法有效地提高了效率、帧率和,最重要的是,游戏时的用户体验。
"过早优化是万恶之源"
唐纳德·克努特,《使用 goto 语句的结构化编程》。计算机调查,第 6 卷,第 4 期,1974 年 12 月
也许,从本章中可以吸取的核心策略是,游戏中的许多部分都是可能的优化候选者,您应该根据对特定游戏性能的分析所获得的数据来驱动您最终实施的优化。
暂停游戏
尽管你的下一款游戏将非常吸引人,但你应该始终允许玩家短暂暂停游戏。在这个菜谱中,我们将实现一个简单而有效的暂停屏幕,包括用于更改显示质量设置的控件。
准备工作
对于这个菜谱,我们准备了一个名为BallGame的包,其中包含一个可玩场景。该包位于1362_11_01文件夹中。
如何操作...
要在按下Esc键时暂停游戏,请按照以下步骤操作:
-
将
BallGame包导入到你的项目中,并从项目视图打开名为BallGame_01的关卡。 -
在检查器中,创建一个新的标签球,将此标签应用到“预制体”文件夹中的
ball预制体上,并保存场景。 -
从层次结构视图,使用创建下拉菜单向 UI 添加一个面板(创建 | UI | 面板)。注意,它将自动将其添加到场景中的当前Canvas。将面板重命名为
QualityPanel。 -
现在使用创建下拉菜单向 UI 添加一个滑动条(创建 | UI | 滑动条)。将其重命名为
QualitySlider。 -
最后,使用创建下拉菜单向 UI 添加一个文本(创建 | UI | 文本)。将其重命名为
QualityLabel。此外,从检查器视图中的Rect Transform,将Pos Y更改为-25。 -
将以下 C#脚本PauseGame添加到第一人称控制器:
using UnityEngine; using UnityEngine.UI; using System.Collections; public class PauseGame : MonoBehaviour { public GameObject qPanel; public GameObject qSlider; public GameObject qLabel; public bool expensiveQualitySettings = true; private bool isPaused = false; void Start () { Cursor.visible = isPaused; Slider slider = qSlider.GetComponent<Slider> (); slider.maxValue = QualitySettings.names.Length; slider.value = QualitySettings.GetQualityLevel (); qPanel.SetActive(false); } void Update () { if (Input.GetKeyDown(KeyCode.Escape)) { isPaused = !isPaused; SetPause (); } } private void SetPause(){ float timeScale = !isPaused ? 1f : 0f; Time.timeScale = timeScale; Cursor.visible = isPaused; GetComponent<MouseLook> ().enabled = !isPaused; qPanel.SetActive (isPaused); } public void SetQuality(float qs){ int qsi = Mathf.RoundToInt (qs); QualitySettings.SetQualityLevel (qsi); Text label = qLabel.GetComponent<Text> (); label.text = QualitySettings.names [qsi]; } } -
从层次结构视图中选择第一人称控制器。然后,从检查器中访问暂停游戏组件,并将游戏对象QualityPanel、QualitySlider和QualityLabel分别填充到QPanel、QSlider和QLabel字段中,如图所示:
![如何操作...]()
-
从层次结构视图中选择QualitySlider。然后,从检查器视图中的滑动条组件,找到名为On Value Changed (Single)的列表,并点击+符号添加一个命令。
-
将第一人称控制器从层次结构视图拖动到新命令的游戏对象字段中。然后,使用函数选择器在Dynamic float下的SetQuality函数(无函数 | PauseGame | Dynamic float | SetQuality)中找到,如图所示:
![如何操作...]()
-
当你播放场景时,你应该能够通过按下Esc键暂停/恢复游戏,同时激活一个控制游戏质量设置的滑动条。
![如何操作...]()
它是如何工作的...
在 Unity 中暂停游戏实际上是一个简单直接的任务:我们只需要将游戏的时间缩放设置为0(并将它设置回1以继续)。在我们的代码中,我们已经在SetPause()函数中包含了这样的命令,该函数在玩家按下Esc键时被调用,同时也切换isPaused变量。为了使功能更加完善,我们还包括了一个GUI 面板,其中包含一个质量设置滑块,当游戏暂停时被激活。

关于质量设置滑块和文本的行为,它们的参数在游戏开始时根据游戏的各种质量设置、它们的名称及其当前状态进行调整。然后,滑块值的更改重新定义了质量设置,并相应地更新标签文本。
更多内容...
你总是可以通过显示音量控制、保存/加载按钮等来为暂停屏幕添加更多功能。
了解更多关于质量设置的信息
我们更改质量设置的代码是对 Unity 文档中给出的示例的轻微修改。如果你想了解更多关于这个主题的信息,请查看docs.unity3d.com/ScriptReference/QualitySettings.html。
相关内容
参考本章中的实现慢动作配方以获取更多信息。
实现慢动作
自从 Remedy Entertainment 的Max Payne以来,慢动作或子弹时间成为游戏中的一个流行特性。例如, Criterion 的Burnout系列成功探索了赛车类别中的慢动作效果。在这个配方中,我们将实现一个通过按下鼠标右键触发的慢动作效果。
准备工作
对于这个配方,我们将使用与上一个配方相同的包,即1362_11_02文件夹中的BallGame。
如何做到这一点...
要实现慢动作,请按照以下步骤操作:
-
将
BallGame包导入到你的项目中,并从项目视图打开名为BallGame_01的场景。 -
在检查器中,创建一个新的标签球,将此标签应用于“预制体”文件夹中的
ball预制体,并保存场景。 -
将以下 C#脚本BulletTime添加到第一人称控制器:
using UnityEngine; using UnityEngine.UI; using System.Collections; public class BulletTime : MonoBehaviour { public float sloSpeed = 0.1f; public float totalTime = 10f; public float recoveryRate = 0.5f; public Slider EnergyBar; private float elapsed = 0f; private bool isSlow = false; void Update () { if (Input.GetButtonDown ("Fire2") && elapsed < totalTime) SetSpeed (sloSpeed); if (Input.GetButtonUp ("Fire2")) SetSpeed (1f); if (isSlow) { elapsed += Time.deltaTime / sloSpeed; if (elapsed >= totalTime) { SetSpeed (1f); } } else { elapsed -= Time.deltaTime * recoveryRate; elapsed = Mathf.Clamp (elapsed, 0, totalTime); } float remainingTime = (totalTime - elapsed) / totalTime; EnergyBar.value = remainingTime; } private void SetSpeed (float speed) { Time.timeScale = speed; Time.fixedDeltaTime = 0.02f * speed; isSlow = !(speed >= 1.0f); } } -
从层次结构视图,使用创建下拉菜单向 UI 添加一个滑块(创建 | UI | 滑块)。请注意,它将被创建为现有画布对象的子对象。将其重命名为
EnergySlider。 -
选择EnergySlider,从检查器视图中的矩形变换组件,设置其位置如下:左:0;Y 位置:0;Z 位置:0;右:0;高度:50。然后,展开锚点设置,并将其更改为:最小 X:0;Y:1;最大 X:0.5;Y:1;中心 X:0;Y:1,如以下截图所示:
![如何做到这一点...]()
-
还选择滑块手柄区域子项,并在检查器视图中将其禁用,如图所示:
![如何操作...]()
-
最后,从层级视图中选择第一人称控制器,找到子弹时间组件,并将能量滑块从层级视图拖动到其能量栏槽中,如图所示:
![如何操作...]()
-
播放您的游戏。您应该能够通过按住右鼠标按钮(或您为输入轴Fire2设置的任何替代选项)来激活慢动作。滑块将充当进度条,缓慢缩小,指示您剩余的子弹时间。
它是如何工作的...
基本上,要实现慢动作效果,我们只需要降低Time.timeScale变量。在我们的脚本中,我们通过使用sloSpeed变量来实现这一点。请注意,我们还需要调整Time.fixedDeltaTime变量,更新我们游戏的物理模拟。
为了使体验更具挑战性,我们还实现了一种能量栏,以指示玩家剩余的子弹时间(初始值由totalTime变量给出,以秒为单位)。当玩家不使用子弹时间时,他的配额根据recoveryRate变量填充。
关于GUI 滑块,我们使用了矩形变换设置将其放置在屏幕左上角,并设置其尺寸为屏幕宽度的一半和 50 像素高。此外,我们还隐藏了滑块手柄区域,使其更类似于传统的能量栏。最后,我们不是允许玩家直接与滑块交互,而是使用了BulletTime脚本来改变滑块的值。
还有更多...
以下是一些建议,帮助您进一步提升慢动作效果。
自定义滑块
不要忘记,您可以通过创建自己的精灵或根据滑块的值更改滑块的填充颜色来自定义滑块的外观。尝试将以下代码行添加到Update函数的末尾:
GameObject fill = GameObject.Find("Fill").gameObject;
Color sliderColor = Color.Lerp(Color.red, Color.green, remainingTime);
fill.GetComponent<Image> ().color = sliderColor;
添加运动模糊
运动模糊是一种常与慢动作相关的图像效果。一旦附加到相机,它可以根据speed浮点值启用或禁用。有关运动模糊图像效果的更多信息,请参阅docs.unity3d.com/Manual/script-MotionBlur.html。
创建音效氛围
《马克斯·佩恩》著名地使用了强烈、沉重的心跳声作为音效氛围。您也可以尝试降低音效音量,以传达在慢动作时角色的专注。此外,在相机上使用音频过滤器可能是一个有趣的选择。
参见
请参阅本章中的“暂停游戏”配方以获取更多信息。
防止您的游戏在未知服务器上运行
在您完成所有艰苦的工作以完成您的网络游戏项目之后,如果它最终在别人的网站上产生流量和收入,那就太不公平了。在这个食谱中,我们将创建一个脚本,以防止除非它由授权服务器托管,否则主游戏菜单显示。
准备工作
要测试这个食谱,您需要访问一个可以托管游戏的网络空间提供商。
如何操作...
为了防止您的网络游戏被盗版,请按照以下步骤操作:
-
从层次结构视图,使用创建下拉菜单创建一个UI Text游戏对象(创建 | UI | 文本)。将其命名为
Text – warning。然后,从检查器中的文本组件,将文本字段更改为Getting Info. Please wait。 -
将以下 C#脚本添加到
Text – warning游戏对象:using UnityEngine; using System.Collections; using UnityEngine.UI; public class BlockAccess : MonoBehaviour { public bool checkDomain = true; public bool fullURL = true; public string[] domainList; public string warning; private void Start(){ Text scoreText = GetComponent<Text>(); bool illegalCopy = true; if (Application.isEditor) illegalCopy = false; if (Application.isWebPlayer && checkDomain){ for (int i = 0; i < domainList.Length; i++){ if (Application.absoluteURL == domainList[i]){ illegalCopy = false; }else if (Application.absoluteURL.Contains(domainList[i]) && !fullURL){ illegalCopy = false; } } } if (illegalCopy) scoreText.text = warning; else Application.LoadLevel(Application.loadedLevel + 1); } } -
从检查器视图,保留检查域名和完整 URL选项的勾选,并将域名列表的大小增加到
1,并在元素 0中填写您游戏的完整 URL。在消息字段中输入句子This is not a valid copy of the game,如以下截图所示。您可能需要将段落的水平溢出更改为溢出。注意
注意:请记住在 URL 中包含 Unity 3D 文件名和扩展名,而不是它嵌入的 HTML。
![如何操作...]()
-
将场景保存为
menu。 -
创建一个新的场景,并将其主摄像机的背景颜色更改为黑色。将此场景保存为
nextLevel。 -
让我们构建游戏。转到文件 | 构建设置…菜单,并按顺序将场景menu和nextLevel包含在构建列表中(构建中的场景)。此外,选择Web Player作为您的平台,然后点击构建。
它是如何工作的...
一旦场景开始,脚本就会将.unity3d文件的实际 URL 与Block Access组件中列出的 URL 进行比较。如果不匹配,则构建中的下一级不会加载,并在屏幕上显示消息。如果它们匹配,则代码行Application.LoadLevel(Application.loadedLevel + 1)将加载构建列表中的下一场景。
更多...
这里有一些关于如何微调和自定义这个食谱的信息。
通过在域名列表中使用完整 URL 提高安全性
如果您在域名列表中填写完整的 URL(例如 www.myDomain.com/unitygame/game.unity3d),您的游戏将更加安全。实际上,建议您选择完整 URL选项,这样您的游戏就不会被盗版并在类似www.stolenGames.com/yourgame.html?www.myDomain.com的 URL 下发布。
允许更多域的重新分发
如果您想让游戏从多个不同的域运行,请增加大小并填写更多 URL。此外,您可以通过不勾选检查域名选项来完全取消保护您的游戏。
状态驱动行为 DIY 状态
整个游戏以及单个对象或角色通常可以被视为(或建模为)通过不同的状态或模式。建模状态和状态的变化(由于事件或游戏条件)是管理游戏和游戏组件复杂性的非常常见方式。在这个菜谱中,我们使用单个GameManager类创建一个简单的三状态游戏(游戏进行/游戏胜利/游戏失败)。
如何操作...
要使用状态来管理对象行为,请按照以下步骤操作:
-
在屏幕顶部中间创建两个 UI 按钮。将一个命名为Button-win,并编辑其文本为Win Game。将第二个命名为Button-lose,并编辑其文本为Lose Game。
-
在屏幕顶部左侧创建一个 UI 文本对象。将此命名为Text-state-messages,并将其Rect Transform高度属性设置为300,其Text (Script) Paragraph Vertical Overflow属性设置为Overflow。
![如何操作...]()
-
将以下 C# 脚本类
GameManager添加到Main Camera:using UnityEngine; using System.Collections; using System; using UnityEngine.UI; public class GameManager : MonoBehaviour { public Text textStateMessages; public Button buttonWinGame; public Button buttonLoseGame; private enum GameStateType { Other, GamePlaying, GameWon, GameLost, } private GameStateType currentState = GameStateType.Other; private float timeGamePlayingStarted; private float timeToPressAButton = 5; void Start () { NewGameState( GameStateType.GamePlaying ); } private void NewGameState(GameStateType newState) { // (1) state EXIT actions OnMyStateExit(currentState); // (2) change current state currentState = newState; // (3) state ENTER actions OnMyStateEnter(currentState); PostMessageDivider(); } public void PostMessageDivider(){ string newLine = "\n"; string divider = "--------------------------------"; textStateMessages.text += newLine + divider; } public void PostMessage(string message){ string newLine = "\n"; string timeTo2DecimalPlaces = String.Format("{0:0.00}", Time.time); textStateMessages.text += newLine + timeTo2DecimalPlaces + " :: " + message; } public void BUTTON_CLICK_ACTION_WIN_GAME(){ string message = "Win Game BUTTON clicked"; PostMessage(message); NewGameState( GameStateType.GameWon ); } public void BUTTON_CLICK_ACTION_LOSE_GAME(){ string message = "Lose Game BUTTON clicked"; PostMessage(message); NewGameState( GameStateType.GameLost ); } private void DestroyButtons(){ Destroy (buttonWinGame.gameObject); Destroy (buttonLoseGame.gameObject); } //--------- OnMyStateEnter[ S ] - state specific actions private void OnMyStateEnter(GameStateType state){ string enterMessage = "ENTER state: " + state.ToString(); PostMessage(enterMessage); switch (state){ case GameStateType.GamePlaying: OnMyStateEnterGamePlaying(); break; case GameStateType.GameWon: // do nothing break; case GameStateType.GameLost: // do nothing break; } } private void OnMyStateEnterGamePlaying(){ // record time we enter state timeGamePlayingStarted = Time.time; } //--------- OnMyStateExit[ S ] - state specific actions private void OnMyStateExit(GameStateType state){ string exitMessage = "EXIT state: " + state.ToString(); PostMessage(exitMessage); switch (state){ case GameStateType.GamePlaying: OnMyStateExitGamePlaying(); break; case GameStateType.GameWon: // do nothing break; case GameStateType.GameLost: // do nothing break; case GameStateType.Other: // cope with game starting in state 'Other' // do nothing break; } } private void OnMyStateExitGamePlaying(){ // if leaving gamePlaying state then destroy the 2 buttons DestroyButtons(); } //--------- Update[ S ] - state specific actions void Update () { switch (currentState){ case GameStateType.GamePlaying: UpdateStateGamePlaying(); break; case GameStateType.GameWon: // do nothing break; case GameStateType.GameLost: // do nothing break; } } private void UpdateStateGamePlaying(){ float timeSinceGamePlayingStarted = Time.time - timeGamePlayingStarted; if(timeSinceGamePlayingStarted > timeToPressAButton){ string message = "User waited too long - automatically going to Game LOST state"; PostMessage(message); NewGameState(GameStateType.GameLost); } } } -
在层次结构中,选择Button-win按钮,并为它的Button (Script)组件添加一个
OnClick动作,以从Main Camera GameObject 中的GameManager组件调用BUTTON_CLICK_ACTION_WIN_GAME()方法。 -
在层次结构中,选择Button-lose按钮,并为它的Button (Script)组件添加一个
OnClick动作,以从Main Camera GameObject 中的GameManager组件调用BUTTON_CLICK_ACTION_LOSE_GAME()方法。 -
在层次结构中,选择Main Camera GameObject。接下来,将其拖入Inspector以确保所有三个GameManager (Script)公共变量、Text State Messages、Button Win Game和Button Lose Game都有相应的 Canvas GameObjects 拖入其中(两个按钮和 UI 文本 GameObject)。
如何工作...
如以下状态图所示,这个菜谱模拟了一个简单的游戏,它从GAME PLAYING状态开始;然后,根据用户点击的按钮,游戏要么移动到GAME WON状态或GAME LOST状态。此外,如果用户等待太长时间才点击按钮,游戏将移动到GAME LOST状态。
系统的可能状态由枚举类型GameStateType定义,而系统在任何时间点的当前状态存储在currentState变量中。

定义了第四个状态(Other),以便我们可以在Start()方法中显式设置所需的GamePlaying状态。当我们希望游戏状态改变时,我们调用NewGameState(…)方法,传递游戏要改变到的新的状态。NewGameState(…)方法首先调用带有当前状态的OnMyStateExit(…)方法,因为当退出特定状态时可能需要执行某些操作;例如,当退出GamePlaying状态时,它销毁两个按钮。接下来,NewGameState(…)方法将currentState变量设置为分配给新状态。然后,调用OnMyStateEnter(…)方法,因为当进入新状态时可能需要立即执行某些操作。最后,向 UI 文本框发布一个消息分隔符,调用PostMessageDivider()方法。
当GameManager对象接收到消息(例如,每帧的Update())时,其行为必须适合当前状态。因此,我们在这个方法中看到一个Switch语句,它调用特定状态的方法。例如,如果当前状态是GamePlaying,那么当接收到Update()消息时,将调用UpdateStateGamePlaying()方法。
如果对应的按钮已被点击,将执行BUTTON_CLICK_ACTION_WIN_GAME()和BUTTON_CLICK_ACTION_LOSE_GAME()方法。它们将游戏移动到相应的WIN或LOSE状态。
逻辑已编写在UpdateStateGamePlaying()方法中,因此一旦GameManager处于GamePlaying状态超过一定时间(由变量timeToPressAButton定义),游戏将自动切换到GameLost状态。
因此,对于每个状态,我们可能需要编写状态退出、状态进入和更新事件的相应方法,以及每个事件的主方法,其中包含一个Switch语句来决定应该调用哪个状态方法(或不应调用)。正如可以想象的那样,随着更多状态和更复杂的游戏逻辑的需求增加,我们的方法和GameManager类中的方法数量将显著增加。下一节将采用更复杂的方法来处理状态驱动游戏,其中每个状态都有自己的类。
参见
参考本章下一节以获取更多关于如何管理具有类继承和状态设计模式的状态复杂性的信息。
使用状态设计模式实现状态驱动行为
之前的模式不仅说明了建模游戏状态的有用性,还说明了游戏管理类如何增长并变得难以管理。为了管理许多状态和状态的复杂行为的复杂性,软件开发社区提出了状态模式。设计模式是一般目的的软件组件架构,经过尝试和测试,被证明是解决常见软件系统特征的优秀解决方案。状态模式的关键特性是每个状态都由其自己的类进行建模,并且所有状态都继承(子类化)自单个父状态类。为了告诉游戏管理器更改当前状态,状态之间需要相互了解。这是为了将整体游戏行为的复杂性分解为单独的状态类而付出的微小代价。
注意
注意:非常感谢 Bryan Griffiths 的贡献,这有助于改进这个配方。
准备工作
这个配方基于之前的配方。因此,复制那个项目,打开它,然后按照这个配方的步骤进行。
如何操作...
使用状态模式架构来管理对象的行为,请执行以下步骤:
-
用以下内容替换 C#脚本类
GameManager的内容:using UnityEngine; using System.Collections; using UnityEngine.UI; public class GameManager : MonoBehaviour { public Text textGameStateName; public Button buttonWinGame; public Button buttonLoseGame; public StateGamePlaying stateGamePlaying{get; set;} public StateGameWon stateGameWon{get; set;} public StateGameLost stateGameLost{get; set;} private GameState currentState; private void Awake () { stateGamePlaying = new StateGamePlaying(this); stateGameWon = new StateGameWon(this); stateGameLost = new StateGameLost(this); } private void Start () { NewGameState( stateGamePlaying ); } private void Update () { if (currentState != null) currentState.StateUpdate(); } public void NewGameState(GameState newState) { if( null != currentState) currentState.OnMyStateExit(); currentState = newState; currentState.OnMyStateEntered(); } public void DisplayStateEnteredMessage(string stateEnteredMessage){ textGameStateName.text = stateEnteredMessage; } public void BUTTON_CLICK_ACTION_WIN_GAME(){ if( null != currentState){ currentState.OnButtonClick(GameState.ButtonType.ButtonWinGame); DestroyButtons(); } } public void BUTTON_CLICK_ACTION_LOSE_GAME(){ if( null != currentState){ currentState.OnButtonClick(GameState.ButtonType.ButtonLoseGame); DestroyButtons(); } } private void DestroyButtons(){ Destroy (buttonWinGame.gameObject); Destroy (buttonLoseGame.gameObject); } } -
创建一个新的 C#脚本类,命名为
GameState:using UnityEngine; using System.Collections; public abstract class GameState { public enum ButtonType { ButtonWinGame, ButtonLoseGame } protected GameManager gameManager; public GameState(GameManager manager) { gameManager = manager; } public abstract void OnMyStateEntered(); public abstract void OnMyStateExit(); public abstract void StateUpdate(); public abstract void OnButtonClick(ButtonType button); } -
创建一个新的 C#脚本类,命名为
StateGamePlaying:using UnityEngine; using System.Collections; public class StateGamePlaying : GameState { public StateGamePlaying(GameManager manager):base(manager){} public override void OnMyStateEntered(){ string stateEnteredMessage = "ENTER state: StateGamePlaying"; gameManager.DisplayStateEnteredMessage(stateEnteredMessage); Debug.Log(stateEnteredMessage); } public override void OnMyStateExit(){} public override void StateUpdate() {} public override void OnButtonClick(ButtonType button){ if( ButtonType.ButtonWinGame == button ) gameManager.NewGameState(gameManager.stateGameWon); if( ButtonType.ButtonLoseGame == button ) gameManager.NewGameState(gameManager.stateGameLost); } } -
创建一个新的 C#脚本类,命名为
StateGameWon:using UnityEngine; using System.Collections; public class StateGameWon : GameState { public StateGameWon(GameManager manager):base(manager){} public override void OnMyStateEntered(){ string stateEnteredMessage = "ENTER state: StateGameWon"; gameManager.DisplayStateEnteredMessage(stateEnteredMessage); Debug.Log(stateEnteredMessage); } public override void OnMyStateExit(){} public override void StateUpdate() {} public override void OnButtonClick(ButtonType button){} } -
创建一个新的 C#脚本类,命名为
StateGameLost:using UnityEngine; using System.Collections; public class StateGameLost : GameState { public StateGameLost(GameManager manager):base(manager){} public override void OnMyStateEntered(){ string stateEnteredMessage = "ENTER state: StateGameLost"; gameManager.DisplayStateEnteredMessage(stateEnteredMessage); Debug.Log(stateEnteredMessage); } public override void OnMyStateExit(){} public override void StateUpdate() {} public override void OnButtonClick(ButtonType button){} } -
在层次结构中,选择Button-win按钮,并为它的Button (Script)组件添加一个
OnClick动作,调用主摄像机GameObject 中的GameManager组件的BUTTON_CLICK_ACTION_WIN_GAME()方法。 -
在层次结构中,选择Button-lose按钮,并为它的Button (Script)组件添加一个
OnClick动作,调用主摄像机GameObject 中的GameManager组件的BUTTON_CLICK_ACTION_LOSE_GAME()方法。 -
在层次结构中,选择主摄像机GameObject。接下来,将其拖入检查器中,以确保所有三个GameManager (Script)公共变量、文本状态消息、胜利游戏按钮和失败游戏按钮都有相应的 Canvas GameObject 拖入其中(两个按钮和 UI 文本 GameObject)。
工作原理...
对于这个配方来说,场景非常直接。有一个单独的主摄像机GameObject,它附加了GameManager脚本对象组件。
为游戏需要管理的每个状态定义一个 C#脚本类——对于这个例子,有三个状态StateGamePlaying、StateGameWon和StateGameLost。这些状态类都是GameState的子类。GameState定义了所有子类状态将拥有的属性和方法:
-
一个枚举类型
ButtonType,它定义了游戏可能生成的两种可能的按钮点击:ButtonWinGame和ButtonLoseGame。 -
gameManager变量:以便每个状态对象都有一个指向游戏管理器的链接。 -
接受
GameManager引用的构造函数方法:这将自动使gameManager变量指向传入的GameManager对象。 -
四个抽象方法
OnMyStateEntered()、OnMyStateExit()、OnButtonClick(…)和StateUpdate()。请注意,抽象方法必须为每个子类有自己的实现。
当 GameManager 类的 Awake() 方法执行时,将创建三个状态对象,每个对应于 playing/win/lose 类。这些状态对象存储在其相应的变量中:stateGamePlaying、stateGameWon 和 stateGameLost。
GameManager 类有一个名为 currentState 的变量,它是对当前状态对象的引用,在游戏运行期间任何时刻都有效(最初,它将是 null)。由于它是 GameState 类(所有状态类的父类)的实例,它可以引用任何不同的状态对象。
在 Awake() 之后,GameManager 将接收到一个 Start() 消息。此方法将 currentState 初始化为 stateGamePlaying 对象。
对于每一帧,GameManager 将接收到 Update() 消息。在接收到这些消息后,GameManager 将向 currentState 对象发送 StateUpdate() 消息。因此,对于每一帧,当前游戏状态的对象将执行这些方法。例如,当 currentState 设置为游戏进行时,对于每一帧,gamePlayingObject 将调用其(在这种情况下,为空的)StateUpdate() 方法。
StateGamePlaying 类在其 OnButtonClick() 方法中实现了语句,以便当用户点击按钮时,gamePlayingObject 将调用 GameManager 实例的 NewState() 方法,并传递对应新状态的对象。因此,如果用户点击 Button-win 按钮,NewState() 方法将传递给 gameManager.stateGameWon。
通过在死亡时销毁对象来减少对象数量
优化原则 1:最小化场景中活跃和启用的对象数量。
减少活跃对象数量的方法之一是在它们不再需要时销毁对象。一旦对象不再需要,就应该销毁它;这样可以节省内存和处理资源,因为 Unity 不再需要发送对象如 Update() 和 FixedUpdate() 等消息,或者考虑对象碰撞或物理等。
然而,有时我们可能希望不是立即销毁一个对象,而是在未来的某个已知点。例如,在声音播放完毕后(参见第九章中的配方 在自动销毁对象前等待音频播放结束,播放和操作声音),玩家只有一定时间收集奖励对象,或者可能显示给玩家的对象在一段时间后应该消失。
本配方演示了如何告诉对象 开始死亡,然后在给定延迟过后自动销毁它们。

如何操作...
要在指定时间后销毁对象,请按照以下步骤操作:
-
创建一个新的 2D 项目。
-
创建一个名为 Click Me 的 UI 按钮,并使其填充整个窗口。
-
在 Inspector 中,将按钮的 Text child 设置为左对齐和大号文本。
-
将以下脚本类
DeathTimeExample.cs添加到 Button Click Me:using UnityEngine; using System.Collections; using UnityEngine.UI; public class DeathTimeExample : MonoBehaviour { public void BUTTON_ACTION_StartDying() { deathTime = Time.time + deathDelay; } public float deathDelay = 4f; private float deathTime = -1; public Text buttonText; void Update(){ if(deathTime > 0){ UpdateTimeDisplay(); CheckDeath(); } } private void UpdateTimeDisplay(){ float timeLeft = deathTime - Time.time; string timeMessage = "time left: " + timeLeft; buttonText.text = timeMessage; } private void CheckDeath(){ if(Time.time > deathTime) Destroy( gameObject ); } } -
将 Button Click Me 的 Text 子对象拖动到脚本的公共变量 Button Text 中,这样脚本就能更改按钮文本以显示倒计时。
-
在 Hierarchy 中选择 Button Click Me,为此按钮添加一个新的 On Click() 事件,将按钮本身作为目标 GameObject,并选择公共函数
BUTTON_ACTION_StartDying(),如以下截图所示:![如何操作...]()
-
现在,运行场景;一旦点击按钮,按钮的文本应该显示倒计时。一旦倒计时到达零,Button Click Me 将被销毁(包括所有子对象,在这种情况下,只是 GameObject Text)。
它是如何工作的...
浮点变量 deathDelay 存储对象在决定开始死亡后等待销毁自己的秒数。浮点变量 deathTime 要么是 -1(尚未设置死亡时间),要么是一个非负值,这是我们希望对象销毁自己的时间。
当按钮被点击时,会调用 BUTTON_ACTION_StartDying() 方法。此方法将 deathTime 变量设置为当前时间加上 deathDelay 中设置的任何值。这个新的 deathTime 值将是一个正数,这意味着从这一点开始,Update() 方法中的 IF 语句将会触发。
每一帧方法 Update() 检查 deathTime 是否大于零(即已设置死亡时间),如果是,则调用 UpdateTimeDisplay() 和 CheckDeath() 方法。
UpdateTimeDisplay() 方法创建一个字符串消息,说明还剩下多少秒,并将 Button Text 更新以显示此消息。
CheckDeath() 方法测试当前时间是否已经超过了 deathTime。如果死亡时间已过,则父 gameObject 将立即被销毁。
当您运行场景时,您将看到一旦达到其死亡时间,Button 就会从 Hierarchy 中移除。
参见
有关更多信息,请参阅本章中的以下配方:
-
通过在可能的情况下禁用对象来减少启用对象的数量
-
通过在可能的情况下使对象不活跃来减少活动对象的数量
通过在可能的情况下禁用对象来减少启用对象的数量
优化原则 1:最小化场景中活动对象和启用对象的数量。
有时,我们可能不想完全删除一个对象,但我们可以确定何时可以安全地禁用对象的脚本组件。如果一个 MonoBehaviour 脚本被禁用,那么 Unity 就不再需要为每个帧发送对象消息,例如 Update() 和 FixedUpdate()。
例如,如果一个非玩家角色(NPC)只有在玩家可以看到该角色时才应该展示某些行为,那么我们只需要在 NPC 可见时执行行为逻辑——其余时间,我们可以安全地禁用脚本组件。
Unity 提供了非常有用的事件 OnBecameInvisible() 和 OnBecameVisible(),它们会在对象从场景中的一个或多个摄像机的可见区域移出和进入时通知对象。
这个配方说明了以下经验法则:如果一个对象在不可见时没有理由执行动作,那么我们应该在它不可见时禁用该对象。

准备工作
对于这个配方,我们准备了一个名为 unity4_assets_handyman_goodDirt 的包,其中包含 3rdPersonController handyman 和地形材质 goodDirt。该包位于 1362_11_07 文件夹中。
如何操作...
要禁用对象以减少计算机处理工作负载的要求,请按照以下步骤操作:
-
创建一个新的 Unity 项目,导入提供的 Unity 包
unity4_assets_handyman_goodDirt。 -
创建一个新的 Terrain(大小 20 x 20,位于 -10, 0, -10),并用 GoodDirt(您可以在从 Terrain Assets 包导入的 Standard Assets 文件夹中找到)进行纹理绘制。
-
在(0, 1, 0)位置添加一个3rdPersonController。
-
在您的 3rdPersonController 前创建一个新的 Cube(这样当您开始运行游戏时,它在 Game 面板中是可见的)。
-
将以下 C# 脚本类
DisableWhenNotVisible添加到您的 Cube:using UnityEngine; using System.Collections; public class DisableWhenNotVisible : MonoBehaviour { private GameObject player; void Start(){ player = GameObject.FindGameObjectWithTag("Player"); } void OnBecameVisible() { enabled = true; print ("cube became visible again"); } void OnBecameInvisible() { enabled = false; print ("cube became invisible"); } void Update(){ //do something, so we know when this script is NOT doing something! float d = Vector3.Distance( transform.position, player.transform.position); print(Time.time + ": distance from player to cube = " + d); } }
它是如何工作的...
当可见时,立方体的脚本DisableWhenNotVisible组件会重新计算并通过Update()方法中的player变量显示其自身到3rdPersonController对象变换的距离,每帧进行一次。然而,当此对象接收到OnBecameInvisible()消息时,对象将其enabled属性设置为false。这导致 Unity 不再向GameObject发送Update()消息,因此Update()中的距离计算不再执行;从而减少了游戏的处理工作量。接收到OnBecameVisible()消息后,enabled属性被设置回true,并且对象将接收每帧的Update()消息。请注意,如果你在运行游戏时在层次结构中选择立方体,你可以通过看到其检查器中的蓝色勾选消失来看到脚本组件被禁用。

上一张截图显示了我们的控制台文本输出,记录了用户在游戏开始后 6.9 秒转向立方体的情况(因此立方体不再可见);然后,在 9.4 秒时,用户转向以便再次看到立方体,导致它被重新启用。
还有更多...
一些你不希望错过的细节:
注意 – 在场景面板中可见仍然算作可见!
注意,即使游戏面板没有显示(渲染)对象,如果对象在场景面板中可见,那么它仍然会被视为可见。因此,建议你在测试此菜谱时隐藏/关闭场景面板,否则可能的情况是对象仅在游戏停止运行时才变为不可见。
另一种常见情况 – 仅在 OnTrigger()之后启用
另一种常见情况是我们只想在玩家角色附近(在某个最小距离内)激活脚本组件。在这些情况下,可以在要禁用/启用的对象上设置一个球体碰撞器(勾选Is Trigger),例如在我们的立方体上。脚本组件只能在玩家角色进入该球体时启用。这可以通过将OnBecameInvisible()和OnBecameVisible()方法替换为OnTriggerEnter()和OnTriggerExit()方法来实现,如下所示:
void OnTriggerEnter(Collider hitObjectCollider) {
if (hitObjectCollider.CompareTag("Player")){
print ("cube close to Player again");
enabled = true;
}
}
void OnTriggerExit(Collider hitObjectCollider) {
if (hitObjectCollider.CompareTag("Player")){
print ("cube away from Player");
enabled = false;
}
}
以下截图说明了在立方体周围创建了一个大球体碰撞器,并且其触发器已启用:

许多计算机游戏(如半条命)使用环境设计,如走廊,通过加载和卸载环境的不同部分来优化内存使用。例如,当玩家触发走廊触发器时,环境对象会加载和卸载。有关此类技术的更多信息,请参阅以下内容:
参见
参考本章中的以下配方以获取更多信息:
-
在死亡时间销毁对象以减少对象数量
-
尽可能使对象不活跃以减少活跃对象的数量
通过尽可能使对象不活跃以减少活跃对象的数量
优化原则 1:最小化场景中活跃和启用的对象数量。
有时,我们可能不想完全删除一个对象,但可以通过使包含脚本组件的父GameObject不活跃来比禁用脚本组件更进一步。这就像在检查器中取消选中GameObject旁边的复选框一样,如图所示:

如何操作...
要通过在对象变得不可见时使其不活跃来减少计算机处理工作量要求,请按照以下步骤操作:
-
复制上一个配方。
-
从您的立方体中移除脚本组件
DisableWhenNotVisible,并改为向立方体添加以下 C#脚本类InactiveWhenNotVisible:using UnityEngine; using System.Collections; using UnityEngine.UI; public class InactiveWhenNotVisible : MonoBehaviour { // button action public void BUTTON_ACTION_MakeActive(){ gameObject.SetActive(true); makeActiveAgainButton.SetActive(false); } public GameObject makeActiveAgainButton; private GameObject player; void Start(){ player = GameObject.FindGameObjectWithTag("Player"); } void OnBecameInvisible() { makeActiveAgainButton.SetActive(true); print ("cube became invisible"); gameObject.SetActive(false); } void Update(){ float d = Vector3.Distance( transform.position, player.transform.position); print(Time.time + ": distance from player to cube = " + d); } } -
创建一个新的按钮,包含文本
Make Cube Active Again,并将按钮放置在游戏面板的顶部,使其占据游戏面板的整个宽度,如图所示:![如何操作...]()
-
在层次结构中选择按钮后,为此按钮添加一个新的On Click()事件,将立方体作为目标 GameObject,并选择公共函数
BUTTON_ACTION_makeCubeActiveAgain()。 -
在检查器中取消选中按钮名称旁边的活动复选框(换句话说,手动停用此按钮,这样在场景首次运行时我们就看不到按钮)。
-
在检查器中选择立方体,并将按钮拖动到其脚本组件
InactiveWhenNotVisible的MakeActiveAgainButton变量槽中,如图所示:![如何操作...]()
它是如何工作的...
初始时,立方体是可见的,而按钮是不可用的(因此对用户不可见)。当立方体接收到OnBecameInvisible事件消息时,其OnBecameInvisible()方法将执行。此方法执行两个动作:
-
它首先启用(因此使其可见)
按钮。 -
然后使脚本的父级
gameObject(即CubeGameObject)失效。
当 按钮 被点击时,它会再次激活 Cube 对象,并再次使 按钮 失效。因此,在任何时候,只有 Cube 和 按钮 对象中的一个处于活动状态,并且当另一个对象处于活动状态时,每个对象都会使自身失效。
注意,一个失效的 GameObject 不会接收任何消息,因此它不会接收到 OnBecameVisible() 消息,这可能不适合每个在摄像机视线之外的物体。然而,当使对象失效是合适的时候,与简单地禁用 GameObject 的单个脚本 Monobehaviour 组件相比,可以节省更多的性能。
激活失效对象的唯一方法是将 GameObject 组件的激活属性设置回 true。在本食谱中,是 Button GameObject,当点击时,运行 BUTTON_ACTION_makeCubeActiveAgain() 方法,允许我们的游戏再次激活 Cube。
参考内容
有关更多信息,请参阅本章中的以下食谱:
-
通过在死亡时间销毁对象来减少对象数量
-
通过在可能的情况下禁用对象来减少启用对象的数量
使用委托和事件提高效率并避免使用 SendMessage!
优化原则 2:最小化需要 Unity 对对象进行“反射”操作和搜索所有当前场景对象的动作。
当事件可以根据可见性、距离或碰撞来触发时,我们可以使用如 OnTriggerExit 和 OnBecomeInvisible 这样的事件,正如在前面的一些食谱中所描述的。当事件可以根据时间段来触发时,我们可以使用协程,正如本章其他食谱中所描述的。然而,某些事件是每个游戏情况独有的,C# 提供了多种方法将用户定义的事件消息广播到脚本对象。一种方法是 SendMessage(…) 方法,当发送到 GameObject 时,将检查每个 Monobehaviour 脚本组件,如果其参数匹配,则执行命名方法。然而,这涉及一种称为 反射 的低效技术。如果性能很重要,应避免使用 SendMessage(…),因为这意味着 Unity 必须分析每个脚本对象(对对象进行 反射)以查看是否存在与发送的消息相对应的公共方法;这比使用委托和事件慢得多。
委托和事件实现了发布-订阅设计模式(pubsub)。这也被称为观察者设计模式。对象可以将它们的方法之一订阅到接收特定发布者特定类型的事件消息。在这个菜谱中,我们将有一个管理类,当 UI 按钮被点击时,它将发布新的事件。我们将创建一些 UI 对象,其中一些对象订阅颜色更改事件,以便每次发布颜色更改事件时,订阅的 UI 对象都会接收到事件消息并相应地更改它们的颜色。C#发布者对象不必担心在任何时候有多少对象订阅它们(可能是没有或 1000 个!);这被称为松耦合,因为它允许独立编写(和维护)不同的代码组件,并且是面向对象代码的一个理想特性。
如何操作...
要实现委托和事件,请按照以下步骤操作:
-
创建一个新的 2D 项目。
-
将以下 C#脚本类ColorManager添加到主相机:
using UnityEngine; using System.Collections; public class ColorManager : MonoBehaviour { public void BUTTON_ACTION_make_green(){ PublishColorEvent(Color.green); } public void BUTTON_ACTION_make_blue(){ PublishColorEvent(Color.blue); } public void BUTTON_ACTION_make_red(){ PublishColorEvent(Color.red); } public delegate void ColorChangeHandler(Color newColor); public static event ColorChangeHandler onChangeColor; private void PublishColorEvent(Color newColor){ // if there is at least one listener to this delegate if(onChangeColor != null){ // broadcast change color event onChangeColor(newColor); } } } -
创建两个 UI图像对象和两个 UI文本对象。将一个图像和文本对象定位在屏幕的左下角,将另一个定位在屏幕的右下角。使左下角的文本读作未监听,使屏幕右边的文本读作我在监听。为了保险起见,在屏幕右上角添加一个滑块UI 对象。
-
在屏幕左上角创建三个 UI 按钮,分别命名为Button-GREEN、Button-BLUE和Button-RED,对应的文本分别为
make things <color=green>GREEN</color>、make things <color=blue>BLUE</color>和make things <color=red>RED</color>。![如何操作...]()
-
将以下 C#脚本类
ColorChangeListenerImage附加到右下角的图像和滑块:using UnityEngine; using System.Collections; using UnityEngine.UI; public class ColorChangeListenerImage : MonoBehaviour { void OnEnable() { ColorManager.onChangeColor += ChangeColorEvent; } private void OnDisable(){ ColorManager.onChangeColor -= ChangeColorEvent; } void ChangeColorEvent(Color newColor){ GetComponent<Image>().color = newColor; } } -
将以下 C#脚本类
ColorChangeListenerText附加到我在监听 Text UI 对象:using UnityEngine; using System.Collections; using UnityEngine.UI; public class ColorChangeListenerText : MonoBehaviour { void OnEnable() { ColorManager.onChangeColor += ChangeColorEvent; } private void OnDisable(){ ColorManager.onChangeColor -= ChangeColorEvent; } void ChangeColorEvent(Color newColor){ GetComponent<Text>().color = newColor; } } -
在层次结构中选择绿色按钮,为该按钮添加一个新的点击()事件,将主相机作为目标 GameObject,并选择公共函数
BUTTON_ACTION_make_green()。对蓝色和红色按钮分别使用函数BUTTON_ACTION_make_blue()和BUTTON_ACTION_make_red()执行相同的操作。 -
运行游戏。当你点击更改颜色按钮时,屏幕右侧的三个 UI 对象会显示所有对应颜色的更改,而屏幕左下角的两个 UI 对象将保持默认的白色颜色。
它是如何工作的...
首先,让我们考虑我们想要发生的事情——我们希望右侧的图像、滑块和文本对象在接收到带有新颜色参数的事件消息OnChangeColor()时改变它们的颜色。
这是通过每个对象都有一个适当的ColorChangeListener类的实例来实现的,该实例将它们的OnChangeColor()方法订阅到监听从ColorManager类发布的颜色更改事件。由于Image和Slider对象都有一个颜色会改变的形象组件,因此它们有我们 C#类的脚本组件ColorChangeListenerImage,而Text对象需要一个不同的类,因为要改变颜色的是文本组件的颜色(因此我们在Text UI 对象中添加了一个 C#脚本组件ColorChangeListenerText)。所以,正如我们所看到的,不同的对象可能会以适合每个不同对象的方式响应接收相同的事件消息。
由于我们的脚本对象可能在不同的时间启用和禁用,因此每次脚本ColorChangeListener对象被启用(例如,当其 GameObject 父对象被实例化时),其OnChangeColor()方法就会被添加到订阅监听颜色更改事件的列表中(同样,每次ColorChangeListenerImage/Text对象被禁用时,这些方法就会从事件订阅者列表中移除)。
当ColorChangeListenerImage/Text对象接收到颜色更改消息时,其订阅的OnChangeColor()方法将被执行,并且相应组件的颜色将更改为接收到的Color值(green/red/blue)。
ColorManager类有一个公共类(静态)变量changeColorEvent,它定义了一个事件,Unity 维护一个动态列表,其中包含所有订阅的对象方法。ColorChangeListenerImage/Text对象就是向这个事件注册或注销它们的方法的。
ColorManager类向用户显示三个按钮,以将所有监听对象更改为特定颜色:绿色、红色和蓝色。当点击按钮时,changeColorEvent会被告知发布一个新的事件,并将相应的Color参数传递给所有订阅的对象方法。
ColorManager类声明了一个名为ColorChangeHandler的委托。委托定义了可以委托(订阅)到事件的方法的返回类型(在这种情况下,void)和参数签名。在这种情况下,方法必须具有单个参数类型为Color的参数签名。我们类ColorChangeListenerImage/Text中的OnChangeColor()方法与这个参数签名相匹配,因此可以订阅ColorManager类中的changeColorEvent。
注意
注意:您可以在www.youtube.com/watch?v=N2zdwKIsXJs找到关于 Unity 委托和事件的简单易懂的视频。
参见
在本章中,有关更多信息,请参阅缓存 GameObject 和组件引用以避免昂贵的查找配方。
使用协程定期执行方法,但与帧率无关
优化原则 3:尽可能少地调用方法。
虽然将逻辑放入Update()并使其为每个帧定期执行非常简单,但我们可以通过尽可能少地执行逻辑来提高游戏性能。所以,如果我们能够每 5 秒只检查一次情况,那么就可以通过将逻辑移出Update()来节省大量性能。
协程是一个可以暂停其执行直到yield动作完成的函数。一种 yield 动作简单地等待给定数量的秒数。在这个菜谱中,我们使用协程和 yield 来展示一个方法如何每 5 秒只执行一次;这可能对 NPC 决定是否随机醒来或选择一个新的位置开始移动很有用。

如何做...
要在帧率独立的情况下定期执行方法,请遵循以下步骤:
-
将以下 C#脚本类
TimedMethod添加到主相机:using UnityEngine; using System.Collections; public class TimedMethod : MonoBehaviour { private void Start() { StartCoroutine(Tick()); } private IEnumerator Tick() { float delaySeconds = 5.0F; while (true) { print("tick " + Time.time); yield return new WaitForSeconds(delaySeconds); } } }
它是如何工作的...
当接收到Start()消息时,Tick()方法以协程的形式启动。Tick()方法将执行之间的延迟(变量delaySeconds)设置为 5 秒。然后启动一个无限循环,其中方法执行其操作(在这种情况下,只是打印出时间);最后,执行一个yield指令,这会导致方法暂停执行 5 秒。在yield指令完成后,循环将再次继续执行,依此类推。在处理协程时,重要的是要理解方法将从它 yield 的相同状态恢复执行。
你可能已经注意到根本没有任何Update()或FixedUpdate()方法。所以,尽管我们的游戏有定期执行的逻辑,但在本例中,没有必须每帧执行的逻辑——太棒了!
还有更多...
一些你不希望错过的细节:
让不同的动作在不同的间隔发生
协程可用于在不同的常规间隔执行不同类型的逻辑。因此,需要逐帧执行的逻辑放入Update(),而每秒或每两秒执行一次即可的逻辑可能放入一个延迟为 0.5 秒的协程中;可以更少地偶尔更新的逻辑可以放入另一个延迟为 2 秒或 5 秒的协程中,依此类推。通过仔细分析(并测试)不同的游戏逻辑,可以找到仍然可接受的最不频繁执行,从而找到有效和明显的性能提升。
参见
参考下一道菜谱以获取更多信息。
使用协程在多个帧上分散长计算
优化原则 3:尽可能少地调用方法。
协程允许我们编写异步代码——我们可以要求一个方法去计算某件事,而游戏的其他部分可以继续运行,无需等待该计算结束。或者,我们可以在Update()方法中调用协程方法,并组织每次调用时完成复杂计算的一部分。
注意,协程不是线程,但它们非常方便,因为每个协程都可以在每个帧上进一步推进。它还允许我们编写不需要等待某些方法完成就可以开始另一个方法的代码。
当游戏开始需要复杂的计算,例如人工智能推理时,在尝试在一个帧内完成所有计算时,可能无法维持可接受的游戏性能——这就是协程可以成为优秀解决方案的地方。
这个示例说明了一个复杂计算如何被结构化为几个部分,每个部分一次完成一个帧。
注意
注意:关于协程(以及其他 Unity 主题)的出色描述可以在 Ray Pendergraph 的 wikidot 网站上找到 raypendergraph.wikidot.com/unity-developer-s-notes#toc6。
如何做...
要将计算分散到几个帧上,请按照以下步骤操作:
-
将以下脚本类
SegmentedCalculation添加到主摄像机:using UnityEngine; using System.Collections; public class SegmentedCalculation : MonoBehaviour { private const int ARRAY_SIZE = 50; private const int SEGMENT_SIZE = 10; private int[] randomNumbers; private void Awake(){ randomNumbers = new int[ARRAY_SIZE]; for(int i=0; i<ARRAY_SIZE; i++){ randomNumbers[i] = Random.Range(0, 1000); } StartCoroutine( FindMinMax() ); } private IEnumerator FindMinMax() { int min = int.MaxValue; int max = int.MinValue for(int i=0; i<ARRAY_SIZE; i++){ if(i % SEGMENT_SIZE == 0){ print("frame: " + Time.frameCount + ", i:" + i + ", min:" + min + ", max:" + max); // suspend for 1 frame since we've completed another segment yield return null; } if(randomNumbers[i] > max){ max = randomNumbers[i]; } else if(randomNumbers[i] < min){ min = randomNumbers[i]; } } // disable this scripted component print("** completed - disabling scripted component"); enabled = false; } } -
运行游戏,你会看到数组中最高和最低值的搜索是如何逐步进行的,避免了每个新帧之间的不希望出现的延迟。
![如何做...]()
它是如何工作的...
随机整数数组randomNumbers在Awake()中创建。然后,FindMinMax()方法以协程的方式启动。数组的大小由常量ARRAY_SIZE定义,每帧要处理的元素数量由SEGMENT_SIZE定义。
FindMinMax()方法为min和max设置初始值,并开始遍历数组。如果当前索引可以被SEGMENT_SIZE整除(余数为 0),则我们让该方法显示当前帧号和变量值,并使用yield null语句暂停执行一个帧。对于每个循环,当前数组索引的值与min和max进行比较,如果找到新的最小值或最大值,则更新这些值。当循环完成后,脚本组件会禁用自己。
还有更多...
一些您不想错过的细节:
从您的系统中检索完整的 Unity 日志文本文件
除了在控制台面板中查看日志文本外,您还可以按照以下方式访问 Unity 编辑器日志文本文件:
-
Mac:
-
~/Library/Logs/Unity/Editor.log -
通过标准控制台应用程序访问
-
-
Windows:
C:\Users\username\AppData\Local\Unity\Editor\Editor.log
-
移动设备(请参阅 Unity 文档以访问设备日志数据)
![从您的系统中检索完整的 Unity 日志文本文件]()
有关 Unity 日志文件的更多信息,请参阅在线手册 docs.unity3d.com/Manual/LogFiles.html。
相关内容
有关本章中 定期执行但与帧率无关的方法 菜谱的更多信息,请参阅。
通过测量最大和最小帧率(FPS)来评估性能
优化原则 4:使用性能数据来驱动设计和编码决策。
游戏性能的一个有用的原始测量是游戏某部分的最高和最低帧率。在这个菜谱中,我们使用 Creative Commons 的 每秒帧数(FPS)计算脚本来记录进行每帧数学计算的游戏的最高和最低帧率。

准备工作
对于这个菜谱,我们在 1362_11_12 文件夹中提供了 C# 脚本 FPSCounter.cs。这是我们修改过的文件,它包括了基于 Annop "Nargus" Prapasapong 的 Do-It-Yourself(DIY)帧率计算脚本的最高和最低值,该脚本已友好地发布在 Creative Commons 下的 Unify wiki 上 wiki.unity3d.com/index.php?title=FramesPerSecond。
如何操作...
要计算和记录最大和最小 FPS,请按照以下步骤操作:
-
开始一个新的项目,并导入
FPSCounter.cs脚本。 -
将
FPSCounter脚本类添加到 主相机。 -
将以下 C# 脚本类
SomeCalculations添加到 主相机:using UnityEngine; using System.Collections; public class SomeCalculations : MonoBehaviour { public int outerLoopIterations = 20; public int innerLoopMaxIterations = 100; void Update(){ for(int i = 0; i < outerLoopIterations; i++){ int innerLoopIterations = Random.Range(2,innerLoopMaxIterations); for(int j = 0; j < innerLoopIterations; j++){ float n = Random.Range(-1000f, 1000f); } } } } -
运行游戏 20 到 30 秒。在屏幕上,你应该能看到当前的平均帧率以及最大和最小帧率显示。
-
停止游戏运行。你现在应该在 控制台 中看到一个总结信息,显示每秒最大和最小帧数,如下截图所示:
![如何操作...]()
它是如何工作的...
SomeCalculations 脚本确保我们让 Unity 为每一帧做些事情,它在每一帧调用 Update() 方法时执行大量计算。有一个外循环(循环计数器 i)是公共变量 outerLoopIterations 的迭代次数(我们将其设置为 20),还有一个内循环(循环计数器 j),它是介于 2 和公共变量 innerLoopMaxIterations(我们将其设置为 100)之间的随机迭代次数。
计算平均 每秒帧数(FPS)的工作由 FPSCounter 脚本执行,该脚本在选择的频率下运行协程方法 FPS()(我们可以在 检查器 中更改)。每次 FPS() 方法执行时,它都会重新计算平均每秒帧数,如果适当的话,更新最大和最小值,如果勾选了 运行时显示 复选框,那么屏幕上的 GUIText 对象会更新为平均、最大和最小 FPS 的消息。
最后,当游戏结束时,脚本类FPSCounter中的OnApplicationQuit()方法会被执行,并将最大/最小 FPS 摘要信息打印到控制台。
更多内容...
一些你不希望错过的细节:
关闭运行时显示以减少 FPS 处理
我们添加了一个选项,允许你关闭运行时显示,这将减少 FPS 计算所需的处理。你只需在检查器中取消选中运行时显示复选框即可。

参见
参考本章中的以下食谱以获取更多信息:
-
使用 Unity 性能分析器识别性能瓶颈
-
使用 Do-It-Yourself 性能分析器识别性能瓶颈
使用 Unity 性能分析器识别性能瓶颈
优化原则 4:使用性能数据来驱动设计和编码决策。
除了遵循一般的资源和代码设计原则,我们知道这些原则应该能提高性能之外,我们还应该意识到每个游戏都是不同的,而在现实中,唯一知道哪些设计决策对性能影响最大的是收集和分析运行时性能数据。虽然原始的每秒帧数(FPS)测量是有用的,但在选择不同的决策时,了解每个帧的渲染和代码执行的处理需求是极其宝贵的。
Unity 5 性能分析器提供了代码和渲染处理需求的详细分解,以及 GPU、音频以及 2D 和 3D 物理所需的处理。也许最有用的一点是,它允许程序员明确记录命名代码段的数据。我们将命名我们的配置文件为MATT_SomeCalculations,并记录和检查我们计算每帧的处理需求。

如何操作...
要使用 Unity 性能分析器记录处理需求,请按照以下步骤操作:
-
开始一个新的 2D 项目。
-
从窗口菜单打开性能分析器窗口,确保已选中记录选项,并且正在收集脚本性能数据,如下截图所示:
![如何操作...]()
-
将以下 C#脚本类
ProfileCalculations添加到主摄像机:using UnityEngine; using System.Collections; public class ProfileCalculations : MonoBehaviour { public int outerLoopIterations = 20; public int innerLoopMaxIterations = 100; void Update(){ Profiler.BeginSample("MATT_calculations"); for(int i = 0; i < outerLoopIterations; i++){ int innerLoopIterations = Random.Range(2,innerLoopMaxIterations); for(int j = 0; j < innerLoopIterations; j++){ float n = Random.Range(-1000f, 1000f); } } Profiler.EndSample(); } } -
运行游戏 20 到 30 秒。
-
停止运行的游戏。你现在应该在性能分析器面板中看到所选帧所需处理细节的分解——性能分析器面板右上角的每一条锯齿形线条代表一个帧收集的数据。
-
通过拖动白色线条到不同的水平位置来查看不同帧的数据——当前帧和总帧数显示在右上角,形式为帧:frame / totalFrames。
-
由于我们给代码配置样本命名,并以MATT为前缀,我们可以限制只显示包含该单词的样本数据。在搜索文本框(位于小放大镜旁边)中输入
MATT,你现在应该只看到一行关于我们的样本MATT_calculations的配置数据。我们可以看到,在第 83 帧,我们的代码占用了该帧处理时间的 1.2%。![如何操作...]()
它是如何工作的...
ProfileCalculations脚本确保 Unity 为每一帧执行一些操作;它使用内循环和外循环进行大量计算,就像在之前的 FPS 食谱中一样。
两个重要的语句是标记要记录和展示在Profiler中的命名代码样本的开始和结束。Profiler.BeginSample("MATT_calculations")语句开始我们的命名配置,它通过EndSample()语句结束。
使用引人注目的前缀,我们可以轻松地隔离我们的命名代码配置以进行分析,使用Profiler面板中的搜索文本框。
参考信息
参考本章中的以下食谱以获取更多信息:
-
通过测量最大和最小帧率(FPS)来评估性能
-
使用 Do-It-Yourself 性能配置识别性能瓶颈
使用 Do-It-Yourself 性能配置识别性能“瓶颈”
优化原则 4:使用性能数据来驱动设计和编码决策。
Unity 5 性能配置器很棒,但有时我们可能希望对正在运行的代码以及它如何显示或记录数据有完全的控制。在这个食谱中,我们探讨了如何使用一个免费可用的脚本进行 DIY 性能配置。虽然它不像 Unity 性能配置器的图形和详细配置那样花哨,但它仍然提供了关于脚本命名部分所需时间的低级数据,这对于做出改进游戏性能的代码设计决策是足够的。

准备工作
对于这个食谱,我们在1362_11_14文件夹中提供了 C#脚本Profile.cs。这是 Michael Garforth 的 DIY 配置脚本,他友好地将其发布在 Unify Wiki 上的Creative Commons下,网址为wiki.unity3d.com/index.php/Profiler。
如何操作...
要使用 Do-It-Yourself 代码配置来记录处理需求,请按照以下步骤操作:
-
开始一个新项目,并导入
Profile.cs脚本。 -
将以下 C#脚本类
DIYProfiling添加到主摄像机:using UnityEngine; using System.Collections; public class DIYProfiling : MonoBehaviour { public int outerLoopIterations = 20; public int innerLoopMaxIterations = 100; void Update(){ string profileName = "MATT_calculations"; Profile.StartProfile(profileName); for (int i = 0; i < outerLoopIterations; i++){ int innerLoopIterations = Random.Range(2,innerLoopMaxIterations); for (int j = 0; j < innerLoopIterations; j++){ float n = Random.Range(-1000f, 1000f); } } Profile.EndProfile(profileName); } private void OnApplicationQuit() { Profile.PrintResults(); } } -
运行游戏几秒钟。
-
停止游戏运行。你现在应该在控制台中看到一个总结消息,说明我们命名配置的总处理时间、平均时间和迭代次数,以及游戏运行的总时间。
它是如何工作的...
如您所见,脚本几乎与之前配方中使用的 Unity 性能分析脚本相同。我们不是调用 Unity 的 Profiler,而是调用 Michael Garforth 的 Profile 类的静态(类)方法。
我们使用 Profile 类方法 StartProfile(…) 和 EndProfile(…),并传入要分析内容的字符串名称(在本例中为 MATT_calculations)。
最后,当游戏终止时,会执行 OnApplicationQuit() 方法,调用 Profile 类的 PrintResults() 方法,该方法将性能摘要信息打印到控制台。
Profile 类记录每个命名配置文件被调用的次数以及从开始到结束之间的持续时间,当调用 PrintResults() 时,输出关于这些执行的摘要信息。
参见
有关更多信息,请参阅本章以下配方:
-
通过测量最大和最小帧率(FPS)来评估性能
-
使用 Unity 性能分析器识别性能瓶颈
缓存 GameObject 和组件引用以避免昂贵的查找
优化原则 2:最小化需要 Unity 对对象进行“反射”操作和搜索所有当前场景对象的动作。
反射是指在运行时,Unity 必须分析对象以查看它们是否包含与对象接收到的“消息”相对应的方法 - 例如 SendMessage()。Unity 在场景中搜索所有活动对象的简单而有用但速度较慢的 FindObjectsByTag() 是一个示例。每次我们使用 GetComponent() 查找对象的组件时,都会减慢 Unity 的速度。

在过去,对于许多组件,Unity 提供了诸如 .audio 这样的 快速组件属性获取器,以引用脚本父 GameObject 的 AudioSource 组件,rigidbody 以引用 RigidBody 组件,等等。然而,这并不是一个一致的规则,在其他情况下,您必须使用 GetComponent()。在 Unity 5 中,所有这些 快速组件属性获取器 都已被移除(除了 .transform,它被自动缓存,因此使用时没有性能成本)。为了帮助游戏开发者更新他们的脚本以与 Unity 5 兼容,他们引入了 自动脚本更新,其中(在适当警告并在继续之前备份文件之后!)Unity 会遍历脚本,将 快速组件属性获取器 代码替换为标准化的 GetComponent<ComponentType>() 代码模式,例如 GetComponent<Rigidbody>() 和 GetComponent<AudioSource>()。然而,尽管脚本更新使事物保持一致,并明确所有这些 GetComponent() 反射语句,但每次 GetComponent() 执行都会消耗宝贵的处理资源。
注意
您可以在以下 2014 年 6 月的博客文章和手册页面中了解更多关于 Unity 这样做的原因(以及他们拒绝的替代方案Extension Methods;真遗憾——我认为我们将在 Unity 的后续版本中看到它们,因为这是一种优雅地解决这种编码情况的方法):
-
blogs.unity3d.com/2014/06/23/unity5-api-changes-automatic-script-updating/ -
unity3d.com/learn/tutorials/modules/intermediate/scripting/extension-methods
在这个菜谱中,我们将逐步重构一个方法,通过移除反射和组件查找操作,使其在每一步都变得更加高效。我们将改进的方法是找到场景中标记为Player(一个3rd Person Controller)的GameObject和场景中标记为Respawn的 1,000 个其他GameObject之间的一半距离。
准备工作
对于这个菜谱,我们准备了一个名为unity4_assets_handyman_goodDirt的包,包含 3rdPersonController handyman 和地形材质goodDirt。该包位于文件夹1362_11_15。
如何操作...
为了通过缓存组件查找来提高代码性能,请按照以下步骤操作:
-
创建一个新的 3D 项目,导入提供的 Unity 包
unity4_assets_handyman_goodDirt。 -
创建一个新的地形(大小为200 x 200,位于-100, 0, -100),并使用GoodDirt进行纹理绘制。
-
在地形的中心添加一个3rdPersonController(即0, 1, 0)。请注意,这已经标记为Player。
-
创建一个新的球体,并给它添加Respawn标签。
-
在Project面板中,创建一个新的空预制件,命名为prefab_sphere,并将球体从Hierarchy面板拖动到Project面板中的预制件中。
-
现在,从Hierarchy面板中删除球体(因为所有属性都已复制到我们的预制件中)。
-
将以下 C#脚本类
SphereBuilder添加到主相机:using UnityEngine; using System.Collections; public class SphereBuilder : MonoBehaviour { public const int NUM_SPHERES = 1000; public GameObject spherePrefab; void Awake(){ List<Vector3> randomPositions = BuildVector3Collection(NUM_SPHERES); for(int i=0; i < NUM_SPHERES; i++){ Vector3 pos = randomPositions[i]; Instantiate(spherePrefab, pos, Quaternion.identity); } } public List<Vector3> BuildVector3Collection(int numPositions){ List<Vector3> positionArrayList = new List<Vector3>(); for(int i=0; i < numPositions; i++) { float x = Random.Range(-100, 100); float y = Random.Range(1, 100); float z = Random.Range(-100, 100); Vector3 pos = new Vector3(x,y,z); positionArrayList.Add (pos); } return positionArrayList; } } -
在Hierarchy中选择主相机,从Inspector中的Project面板拖动prefab_sphere到公共变量
Sphere Prefab,如以下截图所示:![如何操作...]()
-
将以下 C#脚本类
SimpleMath添加到主相机:using UnityEngine; using System.Collections; public class SimpleMath : MonoBehaviour { public float Halve(float n){ return n / 2; } }
方法 1 – 平均距离计算
按照以下步骤操作:
-
将以下 C#脚本类
AverageDistance添加到主相机:using UnityEngine; using System.Collections; using System; public class AverageDistance : MonoBehaviour { void Update(){ // method1 - basic Profiler.BeginSample("TESTING_method1"); GameObject[] sphereArray = GameObject.FindGameObjectsWithTag("Respawn"); for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){ HalfDistanceBasic(sphereArray[i].transform); } Profiler.EndSample(); } // basic private void HalfDistanceBasic(Transform sphereGOTransform){ Transform playerTransform = GameObject.FindGameObjectWithTag("Player").transform; Vector3 pos1 = playerTransform.position; Vector3 pos2 = sphereGOTransform.position; float distance = Vector3.Distance(pos1, pos2); SimpleMath mathObject = GetComponent<SimpleMath>(); float halfDistance = mathObject.Halve(distance); } } -
打开Profiler面板,确保已选择record,并且正在记录脚本处理负载。
-
运行游戏 10 到 20 秒。
-
在Profiler面板中,仅限制列出的结果为以
TEST开头的样本。对于您选择的任何帧,您应该看到TESTING_method1的 CPU 负载百分比和所需的毫秒数。
方法 2 – 缓存 Respawn 对象变换数组
按照以下步骤操作:
-
FindGameObjectWithTag()很慢,所以让我们修复搜索带有Respawn标记的对象的问题。首先,在 C#脚本类AverageDistance中添加一个名为sphereTransformArrayCache的私有Transform数组变量:private Transform[] sphereTransformArrayCache; -
现在,添加
Start()方法,该语句将存储在这个数组中,指向所有带有Respawn标记对象的Transform组件的引用:private void Start(){ GameObject[] sphereGOArray = GameObject.FindGameObjectsWithTag("Respawn"); sphereTransformArrayCache = new Transform[SphereBuilder.NUM_SPHERES]; for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){ sphereTransformArrayCache[i] = sphereGOArray[i].transform; } } -
现在,在
Update()方法中,开始一个新的名为TESTING_method2的Profiler样本,它使用我们缓存的带有Respawn标记的游戏对象数组:// method2 - use cached sphere ('Respawn' array) Profiler.BeginSample("TESTING_method2"); for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){ HalfDistanceBasic(sphereTransformArrayCache[i]); } Profiler.EndSample(); -
再次运行游戏 10 到 20 秒,并将Profiler面板设置为仅列出以
TEST开头的样本。对于你选择的任何帧,你应该看到TESTING_method1和TESTING_method2的 CPU 负载百分比和所需的毫秒数。
方法 3 – 缓存玩家变换引用
这样应该会更快。但是等等!让我们再进一步改进。让我们利用缓存的Cube-Player组件的变换引用,完全避免缓慢的对象标记反射查找。按照以下步骤操作:
-
首先,在
Start()方法中添加一个新的私有变量和一个语句,将Player对象的变换分配到这个变量playerTransformCache中:private Transform playerTransformCache; private Transform[] sphereTransformArrayCache; private void Start(){ GameObject[] sphereGOArray = GameObject.FindGameObjectsWithTag("Respawn"); sphereTransformArrayCache = new Transform[SphereBuilder.NUM_SPHERES]; for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){ sphereTransformArrayCache[i] = sphereGOArray[i].transform; } playerTransformCache = GameObject.FindGameObjectWithTag("Player").transform; } -
现在,在
Update()中添加以下代码以启动一个新的名为TESTING_method3的Profiler样本:// method3 - use cached playerTransform Profiler.BeginSample("TESTING_method3"); for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){ HalfDistanceCachePlayerTransform(sphereTransformArrayCache[i]); } Profiler.EndSample(); -
最后,我们需要编写一个新的方法,该方法利用我们设置的缓存的玩家变换变量来计算半距离。所以,添加这个新方法,
HalfDistanceCachePlayerTransform(sphereTransformArrayCache[i]):// playerTransform cached private void HalfDistanceCachePlayerTransform(Transform sphereGOTransform){ Vector3 pos1 = playerTransformCache.position; Vector3 pos2 = sphereGOTransform.position; float distance = Vector3.Distance(pos1, pos2); SimpleMath mathObject = GetComponent<SimpleMath>(); float halfDistance = mathObject.Halve(distance); }
方法 4 – 缓存玩家的 Vector3 位置
让我们再进一步改进。如果我们假设对于我们的特定游戏,玩家角色不会移动,那么我们可以缓存玩家的位置一次,而不是每次帧都检索它。
按照以下步骤操作:
-
目前,为了找到
pos1,我们每次在Update()方法调用时都让 Unity 找到playerTransform中的Vector3位置值。让我们使用Start()中的变量和语句将这个Vector3位置缓存起来,如下所示:private Vector3 pos1Cache; private void Start(){ ... pos1Cache = playerTransformCache.position; } -
现在,编写一个新的半距离方法,利用这个缓存的位置:
// player position cached private void HalfDistanceCachePlayer1Position(Transform sphereGOTransform){ Vector3 pos1 = pos1Cache; Vector3 pos2 = sphereGOTransform.position; float distance = Vector3.Distance(pos1, pos2); SimpleMath mathObject = GetComponent<SimpleMath>(); float halfDistance = mathObject.Halve(distance); } -
现在,在
Update()方法中添加以下代码,以便为我们创建一个新的样本,并调用我们新的半距离方法:// method4 - use cached playerTransform.position Profiler.BeginSample("TESTING_method4"); for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){ HalfDistanceCachePlayer1Position(sphereTransformArrayCache[i]); } Profiler.EndSample();
方法 5 – 缓存 SimpleMath 组件引用
这样应该再次改进。但我们还可以进一步改进——你会在我们最新的半距离方法中注意到,我们有一个显式的GetComponent()调用,用于获取我们的mathObject的引用;这将每次方法被调用时执行。按照以下步骤操作:
-
让我们将这个脚本组件引用也缓存起来,以节省每次迭代的
GetComponent()反射。我们将声明一个变量mathObjectCache,并在Awake()中将其设置为指向我们的SimpleMath脚本组件:private SimpleMath mathObjectCache; private void Awake(){ mathObjectCache = GetComponent<SimpleMath>(); } -
让我们编写一个新的半距离方法,该方法使用这个缓存的数学组件引用
HalfDistanceCacheMathComponent(i):// math Component cache private void HalfDistanceCacheMathComponent(Transform sphereGOTransform){ Vector3 pos1 = pos1Cache; Vector3 pos2 = sphereGOTransform.position; float distance = Vector3.Distance(pos1, pos2); SimpleMath mathObject = mathObjectCache; float halfDistance = mathObject.Halve(distance); } -
现在,在
Update()方法中添加以下代码,以便为我们的method5创建一个新的样本并调用我们新的半距离方法:// method5 - use cached math component Profiler.BeginSample("TESTING_method5"); for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){ HalfDistanceCacheMathComponent(sphereTransformArrayCache[i]); } Profiler.EndSample();
方法 6 – 缓存球体 Vector3 位置数组
我们已经改进了很多,但仍然有一个明显的机会可以通过缓存来改进我们的代码(如果我们假设球体不会移动,这在示例中似乎是合理的)。目前,在半距离计算方法中的每一帧和每个球体,我们都要求 Unity 检索当前球体变换中的Vector3位置属性值(这是我们变量pos2),这个位置用于计算当前球体与Player的距离。让我们创建一个包含所有这些Vector3位置的数组,这样我们就可以将当前的值传递给我们的半距离计算方法,并节省多次检索它的工作。
按照以下步骤操作:
-
首先,在
Start()方法中添加一个新的私有变量和一条语句,将每个球体的Vector3变换位置分配到数组spherePositionArrayCache中:private Vector3[] spherePositionArrayCache = new Vector3[SphereBuilder.NUM_SPHERES]; private void Start(){ GameObject[] sphereGOArray = GameObject.FindGameObjectsWithTag("Respawn"); sphereTransformArrayCache = new Transform[SphereBuilder.NUM_SPHERES]; for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){ sphereTransformArrayCache[i] = sphereGOArray[i].transform; spherePositionArrayCache[i] = sphereGOArray[i].transform.position; } playerTransformCache = GameObject.FindGameObjectWithTag("Player").transform; pos1Cache = playerTransformCache.position; } -
让我们编写一个新的半距离方法,该方法使用这个缓存的位位置数组:
// sphere position cache private void HalfDistanceCacheSpherePositions(Transform sphereGOTransform, Vector3 pos2){ Vector3 pos1 = pos1Cache; float distance = Vector3.Distance(pos1, pos2); SimpleMath mathObject = mathObjectCache; float halfDistance = mathObject.Halve(distance); } -
现在,在
Update()方法中添加以下代码,以便为我们的method6创建一个新的样本并调用我们新的半距离方法:// method6 - use cached array of sphere positions Profiler.BeginSample("TESTING_method6"); for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){ HalfDistanceCacheSpherePositions(sphereTransformArrayCache[i], spherePositionArrayCache[i]); } Profiler.EndSample(); -
打开Profiler面板并确保record被选中,正在记录脚本处理负载。
-
运行游戏 10 到 20 秒。
-
在Profiler面板中,仅限制列出的结果为以
TEST开头的样本。对于您选择的任何帧,您应该看到每个方法的 CPU 负载百分比和所需毫秒数(这两个值都越低越好!)。对于几乎每一帧,您都应该看到通过缓存改进的每个方法是如何/是否减少了 CPU 负载。![方法 6 – 缓存球体 Vector3 位置数组]()
它是如何工作的...
这个配方说明了我们如何尝试在迭代之前一次性缓存不会改变的变量的引用,例如GameObjects及其组件的引用,以及在这个示例中,标记为Player和Respawn的对象的Transform组件和Vector3位置。当然,与所有事情一样,缓存也有“成本”,这个成本就是存储所有这些引用所需的内存需求。这被称为时空权衡。您可以在en.wikipedia.org/wiki/Space%E2%80%93time_tradeoff了解更多关于这个经典的计算机科学速度与内存权衡。
在需要多次执行的方法中,这种移除隐式和显式组件和对象查找可能提供可测量的性能改进。
注意
注意:了解 Unity 性能优化技术的两个好地方是 Unity 脚本参考中的性能优化网页以及 Unity 的 Jonas Echterhoff 和 Kim Steen Riber Unite2012 演示Unity 性能优化技巧和窍门。本章中的许多配方都源于以下来源的建议:
相关内容
参考本章中的以下配方以获取更多信息:
-
使用委托和事件提高效率并避免 SendMessage!
-
使用 Unity 性能分析器识别性能瓶颈
-
使用 Do-It-Yourself 性能分析识别性能瓶颈
使用 LOD 组提高性能
优化原则 5:最小化绘制调用次数。
精细几何和高分辨率纹理图可以是一把双刃剑:它们可以提供更好的视觉体验,但它们可能会对游戏性能产生负面影响。LOD 组通过在对象占据屏幕的必要部分小于高质量版本能产生显著差异时,用简化版本替换高质量对象来解决这个问题。
在这个配方中,我们将使用LOD 组来创建一个具有两个不同细节级别的游戏对象:当对象占据屏幕超过 50%时的高质量版本,以及占据少于该数量的低质量版本。我们想感谢 Unity 的 Carl Callewaert,他为 LOD Group 功能进行了演示,这在许多方面为这个配方提供了信息。

准备工作
对于这个配方,我们为游戏对象的高质量和低质量版本准备了两个预制件。它们具有相同的尺寸和变换设置(位置、旋转和缩放),以便它们可以无缝替换。这两个预制件都包含在名为LODGroup的包中,位于1362_11_16文件夹内。
如何操作...
要创建一个 LOD 组,请按照以下步骤操作:
-
将LODGroup包导入到您的项目中。
-
从Project视图,在LOD文件夹内,将batt-high预制件拖放到Hierarchy视图。然后,对batt-low预制件做同样的操作。确保它们放置在相同的位置(X:0;Y:0;Z:0)。
-
从Hierarchy视图中的Create下拉菜单创建一个新的空游戏对象(Create | Create Empty)。将其重命名为
battLOD。 -
将LODGroup组件添加到battLOD(菜单Component | Rendering | LODGroup)。
-
选择battLOD对象,并在检查器视图中的LODGroup组件上,右键点击LOD 2并删除它(因为我们将只有两个不同的 LOD:LOD 0和LOD 1),如下截图所示:
![如何操作...]()
-
选择LOD 0区域,点击添加按钮,并从列表中选择batt-high游戏对象。会出现关于重新父化对象的消息。选择是,重新父化。
![如何操作...]()
-
选择LOD 1部分,点击添加,并选择batt-low对象。再次,当提示时选择是,重新父化。
![如何操作...]()
-
拖动 LOD 渲染器的限制以将其设置为:LOD 0:100%,LOD 1:50%,裁剪:1%。这样,当bat-high占据屏幕空间的 51%到 100%时,Unity 将渲染它;当占据 2%到 50%时,将渲染batt-low;如果占据 1%或更少,则不会渲染任何内容。
![如何操作...]()
-
将场景的摄像机移向battLOD对象并返回。你会注意到 Unity 如何根据对象占据屏幕空间的多少在高清和低清 LOD 渲染器之间切换。
它是如何工作的...
一旦我们用适当的模型填充了 LOD 渲染器,LODGroup组件将根据对象占据屏幕百分比的大小选择并显示正确的渲染器,甚至可以完全不显示。
还有更多...
一些你不应该错过的细节:
添加更多 LOD 渲染器
你可以通过在现有的 LOD 渲染器上右键点击并从上下文菜单中选择插入之前来添加更多 LOD 渲染器。
LOD 过渡淡入
如果你想最小化渲染器交换时发生的闪烁,你可以尝试将参数淡入模式从无更改为百分比或交叉淡入。
相关内容
参考本章下一节以获取更多信息
通过设计绘制调用批处理来提高性能
优化原则 5:最小化绘制调用次数。
减少绘制调用的一种方法是通过优先考虑设计决策,使对象符合 Unity 的静态和动态绘制调用批处理资格。
更 CPU 高效的批处理方法是 Unity 的静态批处理。它允许减少任何尺寸几何形状的绘制调用次数。如果这不可能,那么最好的选择是动态批处理,它再次允许 Unity 在单个绘制调用中处理多个移动对象。
注意,这里有一个成本——批处理使用内存,静态批处理比动态内存使用更多。因此,你可以通过批处理来提高性能,但你会增加场景的内存“足迹”。像往常一样,使用内存和性能分析来评估哪种技术最适合你的游戏及其目标部署设备。
如何操作...
在本节中,我们将学习如何实现 静态批处理 和 动态批处理。
静态批处理
要使 Unity 的 静态批处理 成为可能,你需要执行以下操作:
-
确保模型共享相同的材质。
-
标记模型为 静态,如图所示:
![静态批处理]()
可以安全标记为 静态 的对象包括那些不会移动或缩放的环境对象。
可以使用许多技术来确保模型共享相同的材质,包括:
-
避免通过直接绘制模型的顶点来使用纹理(有关此内容的链接在 还有更多… 部分提供)
-
增加使用完全相同纹理的纹理对象的数量
-
通过将多个纹理组合成一个来人工启用对象共享相同的纹理(纹理图集)
-
最大化使用
Renderer.sharedMaterial而不是Renderer.material(因为使用Render.material会涉及复制材质,因此该 GameObejct 将无法进行批处理)
事实上,静态和动态批处理都仅适用于使用相同材质的对象,因此上述所有方法同样适用于使动态批处理成为可能。
动态批处理
要使 Unity 的 动态批处理 成为可能,你需要执行以下操作:
-
确保模型共享相同的材质。
-
将每个网格的 顶点属性 数量保持在 900 以下。
-
要使对象组符合动态批处理条件,需要使用相同的变换缩放(尽管非均匀缩放的模型仍然可以批处理)。
-
如果可能,让动态光照映射对象指向相同的光照贴图位置,以方便动态批处理。
-
如果可能,避免使用多通道着色器和实时阴影,因为这两者都会阻止动态批处理。
要计算 顶点属性 的数量,你需要将顶点数乘以 Shader 使用的属性数。例如,对于使用三个属性(顶点位置、法线和 UV)的 Shader,这意味着模型必须少于 300 个顶点,以保持属性总数低于 900,从而符合动态批处理的条件。
还有更多...
一些你不希望错过的细节:
通过顶点绘制减少对纹理的需求
有关此主题的更多信息,请参阅以下内容:
-
Blender:
wiki.blender.org/index.php/Doc:2.6/Manual/Materials/Special_Effects/Vertex_Paint -
3D Studio Max:
-
Maya:免费的 Vertex Chameleon 插件
关于减少纹理和材质的信息来源
有关此主题的更多信息,请参阅以下内容:
-
Unity 手册页面关于 Draw Call Batching:
-
Paladin Studios:
www.paladinstudios.com/2012/07/30/4-ways-to-increase-performance-of-your-unity-game/ -
Nvidia 关于纹理图集以增加绘制调用批处理机会的白色论文:
http.download.nvidia.com/developer/NVTextureSuite/Atlas_Tools/Texture_Atlas_Whitepaper.pdf -
Nvidia 免费纹理工具和 Photoshop 插件:
参考以下内容
参考本章中的“使用 LOD 组提高性能”配方以获取更多信息
结论
在本章中,我们介绍了一些额外功能以及一系列提高游戏性能和收集性能数据进行分析的方法。
本章的前三个配方提供了一些为您的游戏添加额外功能(暂停、慢动作和确保在线游戏安全)的想法。本章的其余配方提供了如何调查和改进游戏效率与性能的示例。
更多内容...
正如游戏中有很多组件一样,游戏中有许多部分可能会发现处理瓶颈并需要解决以提升整体游戏性能。现在提供了一些额外的建议和进一步参考资料,以提供一个起点,以便您进一步探索优化和性能问题,因为这些问题可能需要整本书来讨论,而不仅仅是一章。
游戏音频优化
移动设备相较于游戏机、桌面或笔记本电脑,内存和处理资源都相对较少,因此在游戏音频方面常常面临最大的挑战。例如,iPhone 一次只能解压缩一个音频剪辑,因此游戏可能会因为音频解压缩问题而出现处理峰值(即降低游戏帧率)。
Paladin Studios 推荐以下音频文件压缩策略用于移动游戏:
-
短剪辑:原生(无压缩)
-
长剪辑(或循环的剪辑):在内存中压缩
-
音乐:从光盘流式传输
-
持续引起 CPU 峰值波动的文件:加载时解压缩
关于此主题的更多信息,请参阅以下链接:
-
Unity 手册音频:
-
Paladin Studios:
www.paladinstudios.com/2012/07/30/4-ways-to-increase-performance-of-your-unity-game/ -
Apple 开发者音频页面:
物理引擎优化
对于一些与物理相关的策略,你可能需要考虑以下方法来提高性能:
-
如果可能,使用几何原语碰撞体(2D 盒子/2D 圆形/3D 盒子/3D 球体/3D 圆柱体):
- 你可以有多个原语碰撞体
-
你也可以在子对象上使用原语碰撞体:
- 只要对象层次结构中的根对象上有刚体
-
避免使用2D 多边形和3D 网格碰撞体:
- 这些操作对处理器的需求更大
-
尝试增加每次
FixedUpdate()方法调用之间的延迟,以减少物理计算:- 虽然不会降低用户体验或游戏行为到不可接受的质量水平!
-
在可能的情况下,将刚体设置为睡眠模式(这样它们在代码唤醒或碰撞之前不需要物理处理)。有关使对象进入睡眠和唤醒的 Unity 脚本参考页面如下:
![物理引擎优化]()
提高脚本效率的更多技巧
你可以考虑以下一些代码策略来提高性能:
-
使用结构体而不是类来提高处理速度。
-
在可能的情况下,使用简单的原始类型数组而不是ArrayLists、Dictionaries或更复杂的集合类。关于在 Unity 中选择最合适的集合类型的好文章可以在
wiki.unity3d.com/index.php/Choosing_the_right_collection_type找到。 -
光线投射较慢,因此避免每帧都进行光线投射,例如,可以使用协程仅在每 3 帧或第 10 帧进行光线投射。
-
查找对象较慢,因此避免在
Update()或内部循环中查找对象,并且你可以让对象设置一个public static变量以允许快速实例检索,而不是使用Find(…)方法。或者你可以使用 Singleton 设计模式。 -
避免使用
OnGUI(),因为它与Update()一样,每帧都会被调用;现在使用新的 Unity 5 UI 系统更容易避免。
更多关于优化的智慧来源
这里有一些其他来源,你可能想要探索以了解更多关于游戏优化主题的信息:
-
Unity 通用移动优化页面:
-
X-team Unity 最佳实践:
x-team.com/2014/03/unity-3d-optimisation-and-best-practices-part-1/ -
代码项目:
www.codeproject.com/Articles/804021/Unity-and-Csharp-Performance-Optimisation-tips -
通用图形优化:
-
在 Unity 的 iPhone 优化物理页面了解更多关于移动物理的信息:
讨论过早优化的文章
这里有一些讨论唐纳德·克努特关于过早优化是“邪恶”的著名引言的文章:
-
乔·达菲的博客:
joeduffyblog.com/2010/09/06/the-premature-optimization-is-evil-myth/ -
"何时优化是过早的?" Stack Overflow:
stackoverflow.com/questions/385506/when-is-optimisation-premature -
过早优化的谬误,兰德尔·海德(由 ACM 出版),来源:Ubiquity 第 10 卷,第 3 期,2009:
关于游戏管理器和状态模式的更多信息来源
从以下网站了解更多关于在 Unity 中实现状态模式和游戏管理器的信息:
-
rusticode.com/2013/12/11/creating-game-manager-using-state-machine-and-singleton-pattern-in-unity3d/
第十二章。编辑器扩展
在本章中,我们将涵盖以下主题:
-
一个编辑器扩展,允许在设计时通过自定义检查器 UI 更改拾取类型(和参数)
-
一个编辑器扩展,通过一个菜单点击添加 100 个随机位置的预制体副本
-
一个进度条来显示编辑器扩展处理完成的百分比
-
一个编辑器扩展,拥有一个对象创建器 GameObject,带有按钮在场景中十字准星对象位置实例化不同的拾取
简介
游戏开发的一般方面(以及在本章中作为特定例子的库存)的一个方面是关于何时我们进行活动的区分。运行时是游戏运行时(以及我们所有的软件和 UI 选择生效时)。然而,设计时是我们游戏设计团队的不同成员在构建各种游戏组件的时间,包括脚本、音频和视觉资产,以及构建每个游戏关卡(或 Unity 中的“场景”)的过程。
在本章中,我们将介绍几个使用 Unity 编辑器扩展的食谱;这些是脚本和多媒体组件,使游戏软件工程师能够使设计时工作更容易,更不容易出错。编辑器扩展允许工作流程改进,从而让设计师更快、更轻松地实现目标;例如,通过菜单选择在场景中生成许多随机位置的库存拾取时,无需任何脚本知识,或者编辑在关卡中不同位置手动放置的拾取的类型或属性。
虽然编辑器扩展是一个相当高级的话题,但如果你团队中有能够编写自定义编辑器组件的人,例如我们展示的那些,可以大大提高只有一两个成员且对脚本有信心的小型团队的效率。
一个编辑器扩展,允许在设计时通过自定义检查器 UI 更改拾取类型(和参数)
在检查器面板中使用枚举和相应的下拉菜单来限制更改到有限集中的一个,通常效果不错(例如,拾取对象的拾取类型)。然而,这种方法的麻烦在于,当两个或多个属性相关且需要一起更改时,存在更改一个属性的风险,例如,将拾取类型从心形更改为钥匙,但忘记更改相应的属性;例如,让精灵渲染器组件仍然显示心形精灵。这种不匹配既会搞乱预期的关卡设计,当然,当玩家与显示一个拾取图像但实际添加到库存中的拾取类型不同时,也会让玩家感到沮丧!
如果 GameObject 有一个或多个相关的属性或组件,并且所有这些都需要一起更改,那么一个很好的策略是使用 Unity 编辑器扩展,每次从显示定义的枚举选择集的下拉菜单中选择不同的选项时,都执行所有相关的更改。
在这个菜谱中,我们介绍了一个用于 GameObject 的 PickUp 组件的编辑器扩展。

准备工作
本菜谱假设您从第二章中第一个菜谱设置的Simple2Dgame_SpaceGirl项目开始,该项目的设置在库存 GUI中。在1362_12_01文件夹中提供了一个 Unity 项目的副本,文件夹名为unityProject_spaceGirlMiniGame。
如何操作...
要创建一个编辑器扩展,允许在设计时通过自定义检查器 UI 更改拾取类型(和参数),请按照以下步骤操作:
-
从迷你游戏
Simple2Dgame_SpaceGirl的新副本开始。 -
在项目面板中,创建一个名为
EditorSprites的新文件夹。将以下图像从Sprites文件夹移动到这个新文件夹:star、healthheart、icon_key_green_100、icon_key_green_32、icon_star_32和icon_heart_32。![如何操作...]()
-
在层次结构面板中,将 GameObject
star重命名为pickup。 -
编辑标签,将标签Star更改为Pickup。确保
pickupGameObject 现在具有标签Pickup。 -
将以下 C#脚本
PickUp添加到pickupGameObject 的层次结构中:using UnityEngine; using System; using System.Collections; public class PickUp : MonoBehaviour { public enum PickUpType { Star, Health, Key } [SerializeField] public PickUpType type; public void SetSprite(Sprite newSprite){ SpriteRenderer spriteRenderer = GetComponent<SpriteRenderer>(); spriteRenderer.sprite = newSprite; } } -
在项目面板中,创建一个名为
Editor的新文件夹。在此新文件夹中,创建一个名为PickUpEditor的新 C#脚本类,代码如下:using UnityEngine; using System.Collections; using System; using UnityEditor; using System.Collections.Generic; [CanEditMultipleObjects] [CustomEditor(typeof(PickUp))] public class PickUpEditor : Editor { public Texture iconHealth; public Texture iconKey; public Texture iconStar; public Sprite spriteHealth100; public Sprite spriteKey100; public Sprite spriteStar100; UnityEditor.SerializedProperty pickUpType; private Sprite sprite; private PickUp pickupObject; void OnEnable () { iconHealth = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/icon_heart_32.png", typeof(Texture)) as Texture; iconKey = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/icon_key_32.png", typeof(Texture)) as Texture; iconStar = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/icon_star_32.png", typeof(Texture)) as Texture; spriteHealth100 = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/healthheart.png", typeof(Sprite)) as Sprite; spriteKey100 = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/icon_key_100.png", typeof(Sprite)) as Sprite; spriteStar100 = AssetDatabase.LoadAssetAtPath("Assets/EditorSprites/star.png", typeof(Sprite)) as Sprite; pickupObject = (PickUp)target; pickUpType = serializedObject.FindProperty ("type"); } public override void OnInspectorGUI() { serializedObject.Update (); string[] pickUpCategories = TypesToStringArray(); pickUpType.enumValueIndex = EditorGUILayout.Popup("PickUp TYPE: ", pickUpType.enumValueIndex, pickUpCategories); PickUp.PickUpType type = (PickUp.PickUpType)pickUpType.enumValueIndex; switch(type) { case PickUp.PickUpType.Health: InspectorGUI_HEALTH(); break; case PickUp.PickUpType.Key: InspectorGUI_KEY(); break; case PickUp.PickUpType.Star: default: InspectorGUI_STAR(); break; } serializedObject.ApplyModifiedProperties (); } private void InspectorGUI_HEALTH() { GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUILayout.Label(iconHealth); GUILayout.Label("HEALTH"); GUILayout.Label(iconHealth); GUILayout.Label("HEALTH"); GUILayout.Label(iconHealth); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); pickupObject.SetSprite(spriteHealth100); } private void InspectorGUI_KEY() { GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUILayout.Label(iconKey); GUILayout.Label("KEY"); GUILayout.Label(iconKey); GUILayout.Label("KEY"); GUILayout.Label(iconKey); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); pickupObject.SetSprite(spriteKey100); } private void InspectorGUI_STAR() { GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUILayout.Label(iconStar); GUILayout.Label("STAR"); GUILayout.Label(iconStar); GUILayout.Label("STAR"); GUILayout.Label(iconStar); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); pickupObject.SetSprite(spriteStar100); } private string[] TypesToStringArray(){ var pickupValues = (PickUp.PickUpType[])Enum.GetValues(typeof(PickUp.PickUpType)); List<string> stringList = new List<string>(); foreach(PickUp.PickUpType pickupValue in pickupValues){ string stringName = pickupValue.ToString(); stringList.Add(stringName); } return stringList.ToArray(); } } -
在检查器面板中,选择 GameObject
pickup并选择拾取类型下拉菜单的不同值。你应该在检查器的拾取(脚本)组件中的图像和图标中看到相应的更改(三个带有类型名称的图标)。此 GameObject 的Sprite Renderer组件的Sprite属性应该更改。此外,在场景面板中,你会看到场景中的图像更改为你选择的拾取类型的适当图像。![如何操作...]()
工作原理...
我们的脚本类PickUp有一个枚举PickUpType,包含三个值:Star、Health和Key。此外,还有一个变量type,用于存储父 GameObject 的类型。最后,有一个SetSprite(…)方法,用于将父 GameObject 的Sprite Renderer组件设置为提供的Sprite参数。每次从编辑器脚本中调用此方法时,都会更改拾取类型,并传递对应新类型的相应精灵。
这个菜谱的大部分工作都由脚本类 PickUpEditor 负责。虽然这个脚本中有很多内容,但其工作相对简单:对于每一帧,通过 OnInspectorGUI() 方法,向用户展示一个 PickUpType 值的下拉列表。根据从下拉列表中选择的值,执行三种方法之一:InspectorGUI_HEALTH()、InspectorGUI_KEY()、InspectorGUI_STAR()。这些方法在每个方法中都在下拉菜单下方显示三个图标和类型名称,并在最后调用正在编辑的 GameObject 的 SetSprite(…) 方法来更新父 GameObject 的 Sprite Renderer 组件。
在我们的类声明之前出现的 C# 属性 [CustomEditor(typeof(PickUp))] 告诉 Unity 使用这个特殊的编辑脚本来显示 GameObject 的 Inspector 面板中的组件属性,而不是 Unity 的默认 Inspector,后者显示此类脚本组件的公共变量。
在 OnInspectorGUI() 方法的主要工作之前和之后,首先确保与检查器中正在编辑的对象相关的任何变量都已更新——serializedObject.Update()。这个方法中的最后一条语句相应地确保将编辑脚本中变量的任何更改复制回正在编辑的 GameObject——serializedObject.ApplyModifiedProperties()。
脚本类 PickUpEditor 的 OnEnable() 方法加载三个小图标(用于在 Inspector 中显示)和三个较大的精灵图像(用于更新在 Scene/Game 面板中显示的 Sprite Renderer)。pickupObject 变量被设置为对 PickUp 脚本组件的引用,允许我们调用 SetSprite(…) 方法。pickUpType 变量被设置为与 PickUp 脚本组件的类型变量链接,该组件的特殊 Inspector 编辑器视图使此脚本成为可能——serializedObject.FindProperty ("type")。
还有更多...
这里有一些细节你不希望错过。
通过检查器提供拾取参数的自定义编辑
许多拾取项具有额外的属性,而不仅仅是携带的物品。例如,一个健康拾取项可能向玩家的角色添加“点数”,一个硬币拾取项可能向角色的银行余额添加“点数”,等等。因此,让我们在我们的 PickUp 类中添加一个整数 points 变量,并允许用户通过我们的自定义检查器编辑器中的 GUI 滑块轻松编辑此点数值。

要为我们的 PickUp 对象添加可编辑的点属性,请按照以下步骤操作:
-
将以下额外行添加到 C# 脚本
PickUp中,以创建我们新的整数points变量:public int points; -
在 C#脚本
PickUpEditor中添加以下额外行,以处理我们新的整数points变量:UnityEditor.SerializedProperty points; -
在 C#脚本
PickUpEditor中的OnEnable()方法中添加以下额外行,以将我们的新points变量与其对应的 GameObject 的PickUp脚本组件中的值关联起来:void OnEnable () { points = serializedObject.FindProperty ("points"); pickUpType = serializedObject.FindProperty ("type"); // rest of method as before… -
现在我们可以为不同类型的 PickUp 添加额外的行到每个 GUI 方法中。例如,我们可以添加一个语句向用户显示一个IntSlider,以便他们能够查看和修改Health PickUp对象的点值。我们在 C#脚本
PickUpEditor中的InspectorGUI_HEALTH()方法末尾添加了一个新的语句,如下所示,以显示一个可修改的IntSlider,代表我们新的points变量:private void InspectorGUI_HEALTH(){ // beginning of method just as before… pickupObject.SetSprite(spriteHealth100); // now display Int Slider for points points.intValue = EditorGUILayout.IntSlider ("Health points", points.intValue, 0, 100); }
我们向IntSlider(…)方法提供了四个参数。第一个是用户将在滑块旁边看到的文本标签。第二个是滑块显示的初始值。最后两个是最大值和最小值。在我们的例子中,我们允许从 0 到 100 的值,但如果健康拾取只提供一、二或三个健康点,那么我们只需调用EditorGUILayout.IntSlider ("Health points", points.intValue, 1, 5)。此方法返回一个基于滑块位置的新整数值,并将此新值存储回我们的SerializedProperty变量points的整数部分。
注意,从 GameObject 中的脚本组件和我们的编辑器脚本中加载和保存值,都是我们调用OnInspectorGUI()方法中的Update()方法和ApplyModifiedProperties()方法在序列化对象上执行的工作的一部分。
注意,由于对于某些拾取(例如钥匙)点数可能没有任何意义,因此当用户正在编辑该类型的PickUp对象时,我们不会在 GUI Inspector 编辑器中显示任何滑块。
为关键拾取通过 Inspector 提供标签下拉列表
虽然对于钥匙拾取,“点数”可能没有意义,但给定钥匙适合的锁的类型肯定是我们希望在游戏中实现的东西。由于 Unity 为我们提供了任何 GameObject 的字符串标签的(定义和可编辑)列表,通常通过标签表示与钥匙对应的锁或门类型是足够简单和直接的。例如,绿色钥匙可能适合所有标记为LockGreen的对象等等。

因此,能够为关键拾取的字符串属性提供一个自定义的 Inspector 编辑器,以存储可以打开的锁的标签,是非常有用的。这个任务结合了几个动作,包括使用 C#从 Unity 编辑器检索一个标签数组,然后构建并提供一个包含这些标签的下拉列表给用户,当前值已在此列表中选中。
要为适合锁的标记添加一个可选择的字符串列表,请按照以下步骤操作:
-
在 C#脚本
PickUp中添加以下额外行以创建我们的新整数fitsLockTag变量:public string fitsLockTag; -
在 C#脚本
PickUpEditor中添加以下额外行以处理我们的新整数fitsLockTag变量:UnityEditor.SerializedProperty fitsLockTag; -
在 C#脚本
PickUpEditor的OnEnable()方法中添加以下额外行,以将我们的新fitsLockTag变量与其对应的 GameObject 正在编辑的拾取脚本组件中的值关联起来:void OnEnable () { fitsLockTag = serializedObject.FindProperty ("fitsLockTag"); points = serializedObject.FindProperty ("points"); pickUpType = serializedObject.FindProperty ("type"); // rest of method as before… -
现在我们需要在用于关键拾取的 GUI 方法中添加一些额外的代码行。我们需要在 C#脚本
PickUpEditor中的InspectorGUI_KEY()方法末尾添加几个语句来设置并显示一个可选择的弹出下拉列表,表示我们的新fitsLockTag变量,如下所示。用以下代码替换InspectorGUI_KEY()方法:private void InspectorGUI_KEY() { GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUILayout.Label(iconKey); GUILayout.Label("KEY"); GUILayout.Label(iconKey); GUILayout.Label("KEY"); GUILayout.Label(iconKey); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); pickupObject.SetSprite(spriteKey100); string[] tags = UnityEditorInternal.InternalEditorUtility.tags; Array.Sort(tags); int selectedTagIndex = Array.BinarySearch(tags, fitsLockTag.stringValue); if(selectedTagIndex < 0) selectedTagIndex = 0; selectedTagIndex = EditorGUILayout.Popup("Tag of door key fits: ", selectedTagIndex, tags); fitsLockTag.stringValue = tags[selectedTagIndex]; }
我们已经在这个方法的末尾添加了几个语句。首先创建了一个字符串数组tags(并排序),包含当前游戏在 Unity 编辑器中可用的标记列表。然后我们尝试在数组中找到fitsLockTag当前值的定位——由于我们已经按字母顺序排序了数组(这也使得用户更容易导航),我们可以使用内置脚本类Array的BinarySearch(…)方法。如果fitsLockTag中的字符串在tags数组中找不到,则默认选择第一个项目(索引 0)。
然后用户通过GUILayout方法的EditorGUILayout.Popup(…)看到下拉列表,此方法返回所选项目的索引。所选索引存储到selectedTagIndex中,方法中的最后一个语句提取相应的字符串并将其存储到fitsLockTag变量中。
注意
注意:而不是显示所有可能的标记,进一步的改进可能移除数组'tags'中所有没有前缀'Lock'的项目。因此,用户只会看到像'LockBlue'和'LockGreen'这样的标记,等等。
基于 fitsLockTag 打开门的逻辑
在我们的玩家碰撞逻辑中,我们现在可以搜索我们的库存,看看是否有任何钥匙物品适合我们与之碰撞的锁。例如,如果与绿色门发生碰撞,并且玩家携带的钥匙可以打开这样的门,那么该物品应该从库存List<>中移除,并且门应该被打开。
为了实现这一点,你需要在OnTriggerEnter()方法中添加一个if测试来检测与标记为Door的物品的碰撞,然后添加逻辑来尝试打开门,如果失败,则执行适当的动作(例如,播放声音)来通知玩家他们现在还不能打开门(我们假设我们已经编写了一个门动画控制器,当门要打开时,它会播放适当的动画和声音):
if("Door" == hitCollider.tag){
if(!OpenDoor(hitCollider.gameObject))
DoorNotOpenedAction();
}
OpenDoor() 方法需要识别库存中哪些项目(如果有的话)可以打开这样的门,如果找到,则应该从 List<> 中移除该物品,并通过适当的方法打开门:
private bool OpenDoor(GameObject doorGO){
// search for key to open the tag of doorGO
int colorKeyIndex = FindItemIndex(doorGO.tag);
if( colorKeyIndex > -1 ){
// remove key item from inventory List<>
inventory.RemoveAt( colorKeyIndex );
// now open the door...
DoorAnimationController doorAnimationController = doorGO.GetComponent<>(DoorAnimationController);
doorAnimationController.OpenDoor();
return true;
}
return false;
}
以下是一个查找适合门标签的库存列表关键项目的代码方法:
private int FindItemIndex(string doorTag){
for (int i = 0; i < inventory.Count; i++){
PickUp item = inventory[i];
if( (PickUp.PickUpType.Key == item.type) && (item.fitsLockTag == doorTag))
return i;
}
// not found
return -1;
}
需要在私有属性中添加 [SerializeField]
注意,如果我们希望创建用于处理私有变量的编辑器扩展,那么我们需要在编辑器脚本将要更改的变量所在行的前面显式添加 [SerializeField]。在 Unity 中,公共变量默认会进行序列化,因此对于脚本类 PickUp 中的公共 type 变量来说,这并不是必需的,尽管将所有可以通过编辑器扩展更改的变量以这种方式标记为良好实践。
从 Unity 文档中了解更多信息
Unity 提供了有关编辑器脚本的文档页面,位于 docs.unity3d.com/ScriptReference/Editor.html。
通过一次菜单点击添加 100 个随机位置预制件的编辑器扩展
有时我们希望在场景中随机创建“大量”拾取物品。而不是手动这样做,我们可以在 Unity 编辑器中添加一个自定义菜单和项目,当选择它时,将执行一个脚本。在这个菜谱中,我们创建了一个调用脚本的菜单项,该脚本在场景中创建 100 个随机位置的天星拾取预制件。

准备工作
此配方假设您是从本章第一道菜谱中设置的 Simple2Dgame_SpaceGirl 项目开始的。
如何做到这一点...
要创建一个通过一次菜单点击添加 100 个随机位置预制件的编辑器扩展,请按照以下步骤操作:
-
从新的副本开始 mini-game
Simple2Dgame_SpaceGirl。 -
在 项目 面板中,创建一个名为
Prefabs的新文件夹。在这个新文件夹内,创建一个名为prefabstar的新空预制件。通过将 层次结构 面板中的 GameObjectstar拖动到 项目 面板中的prefabstar上来填充这个预制件。现在预制件应该变成蓝色,并包含 GameObject star 的所有属性和组件的副本。 -
从 层次结构 中删除 GameObject
star。 -
在 项目 面板中,创建一个名为
Editor的新文件夹。在这个新文件夹内,创建一个名为MyGreatGameEditor的新 C# 脚本类,代码如下:using UnityEngine; using UnityEditor; using System.Collections; using System; public class MyGreatGameEditor : MonoBehaviour { const float X_MAX = 10f; const float Y_MAX = 10f; static GameObject starPrefab; [MenuItem("My-Great-Game/Make 100 stars")] static void PlacePrefabs(){ string assetPath = "Assets/Prefabs/prefab_star.prefab"; starPrefab = (GameObject)AssetDatabase.LoadMainAssetAtPath(assetPath); int total = 100; for(int i = 0; i < total; i++){ CreateRandomInstance(); } } static void CreateRandomInstance(){ float x = UnityEngine.Random.Range(-X_MAX, X_MAX); float y = UnityEngine.Random.Range(-Y_MAX, Y_MAX); float z = 0; Vector3 randomPosition = new Vector3(x,y,z); Instantiate(starPrefab, randomPosition, Quaternion.identity); } } -
根据您电脑的速度,大约 20 到 30 秒后,您应该会看到一个新菜单出现,我的伟大游戏,其中有一个菜单项,制作 100 颗星星。选择此菜单项,就像魔法一样,您现在应该会看到 100 个新的 prefab_star(Clone) GameObjects 出现在场景中!!如何做到这一点...
它是如何工作的...
这个菜谱的核心目的是添加一个新的菜单,其中包含一个将执行我们所需操作的菜单项。C# 属性 [MenuItem("<menuName>/<menuItemName>")] 声明了菜单名称和菜单项名称,Unity 将在代码列表中执行每次用户选择菜单项时跟在后面的静态方法。
在这个菜谱中,[MenuItem("My-Great-Game/Make 100 stars")] 语句声明了菜单名称为 My-Great-Game,菜单项为 Make 100 stars。紧随此属性之后的方法是 PlacePrefabs() 方法。当此方法执行时,它使 starPrefab 变量成为通过 Assets/Prefabs/prefab_star.prefab 路径找到的预制体的引用。然后,执行一个循环 100 次,每次调用 CreateRandomInstance() 方法。
CreateRandomInstance() 方法创建一个 Vector3 randomPosition 变量,使用 X_MAX 和 Y_MAX 常量。然后使用内置方法 Instantiate(...) 在场景中创建一个新的 GameObject,创建预制体的副本并将其定位在由 randomPosition 定义的位
还有更多...
一些你不希望错过的细节:
将每个新的 GameObject 子对象链接到单个父对象,以避免在 Hierarchy 中填充 100 多个新对象
为了避免让我们的 Hierarchy 面板充满数百个新对象克隆,一个好的方法是有一个空的 "父" GameObject,并将相关的一组 GameObjects 作为其子对象。让我们在 Hierarchy 中有一个名为 Star-container 的 GameObject,并将所有新的星星子对象链接到这个对象。

我们需要一个变量,它将是我们容器对象的引用,starContainerGO。我们还需要一个新的方法,CreateStarContainerGO(),它将找到 GameObject star-container 的引用,如果已经存在这样的对象,它将被删除,然后该方法将创建一个新的空 GameObject 并给它这个名称。将以下变量和方法添加到我们的脚本类中:
static GameObject starContainerGO;
static void CreateStarContainerGO() {
string containerName = "Star-container";
starContainerGO = GameObject.Find(containerName);
if (null != starContainerGO)
DestroyImmediate(starContainerGO);
starContainerGO = new GameObject(containerName);
}
在我们创建预制体克隆之前,我们首先需要确保我们已经创建了我们的星容器 GameObject。因此,我们需要在执行 PlacePrefabs() 方法时首先调用我们的新方法,所以请在 PlacePrefabs() 方法的开始处添加一个调用此方法的语句:
static void PlacePrefabs(){
CreateStarContainerGO();
// rest of method as before ...
}
现在我们需要修改 CreateRandomInstance() 方法,使其能够获取它刚刚创建的新 GameObject 的引用,然后可以将这个新对象作为我们的 star-container GameObject 变量 starContainerGO 的子对象。修改 CreateRandomInstance() 方法,使其看起来如下所示:
static void CreateRandomInstance() {
float x = UnityEngine.Random.Range(-X_MAX, X_MAX);
float y = UnityEngine.Random.Range(-Y_MAX, Y_MAX);
float z = 0;
Vector3 randomPosition = new Vector3(x,y,z);
GameObject newStarGO = (GameObject)Instantiate(starPrefab, randomPosition, Quaternion.identity);
newStarGO.transform.parent = starContainerGO.transform;
}
一个进度条来显示编辑器扩展处理的完成比例
如果编辑器任务需要超过半秒或更长时间,那么我们应该通过进度条向用户显示进度完成/剩余情况,以便他们了解实际上正在发生某些事情,并且应用程序没有崩溃和冻结。

准备工作
这个菜谱在先前的菜谱基础上进行了扩展,所以请复制那个项目文件夹,并使用该副本来完成这个菜谱的工作。
如何操作...
要在循环期间添加进度条(然后在循环完成后将其移除),将PlacePrefabs()方法替换为以下代码:
static void PlacePrefabs(){
string assetPath = "Assets/Prefabs/prefab_star.prefab";
starPrefab = (GameObject)AssetDatabase.LoadMainAssetAtPath(assetPath);
int total = 100;
for(int i = 0; i < total; i++){
CreateRandomInstance();
EditorUtility.DisplayProgressBar("Creating your starfield", i + "%", i/100f);
}
EditorUtility.ClearProgressBar();
}
它是如何工作的...
如所示,在for循环内部,我们调用EditorUtility.DisplayProgressBar(...)方法,传递三个参数。第一个是进度条对话框窗口的字符串标题,第二个是在条形本身下方显示的字符串(通常百分比就足够了),最后一个参数是介于 0.0 和 1.0 之间的值,表示要显示的完成百分比。
由于我们有一个从 1 到 100 的循环变量i,我们可以显示这个整数,然后跟一个百分号作为第二个参数,并将这个数字除以 100 以得到所需的十进制值,以指定进度条应该显示多少已完成的进度。如果循环运行的是其他数字,我们只需将循环计数器除以循环总数以获得我们的十进制进度值。最后,在循环完成后,我们使用语句EditorUtility.ClearProgressBar()移除进度条。
一个编辑器扩展,用于创建一个对象创建器 GameObject,带有按钮,可以在场景中十字准星对象位置实例化不同的拾取物
如果关卡设计师希望手动“逐个”放置每个拾取物,我们仍然可以使这个过程比手动从项目面板拖动预制件副本更容易。在这个菜谱中,我们提供了一个“十字准星”GameObject,在检查器中有按钮,允许游戏设计师通过点击适当的按钮,当十字准星的中心位于所需位置时,在精确位置创建三种不同类型的预制件的实例。
Unity 编辑器扩展是这个菜谱的核心,展示了这样的扩展如何允许游戏开发团队中不太技术的人员在 Unity 编辑器中积极参与关卡创建。

准备工作
这个菜谱假设你是从第二章中第一个菜谱设置的Simple2Dgame_SpaceGirl项目开始的,库存 GUI。
对于这个菜谱,我们在1362_12_04文件夹中的Sprites文件夹中准备了所需的十字准星图像。
如何操作...
要创建一个对象创建器 GameObject,请按照以下步骤操作:
-
从新的迷你游戏
Simple2Dgame_SpaceGirl的副本开始。 -
在 Project 面板中,将 GameObject
star重命名为pickup。 -
在 Project 面板中,创建一个名为
Prefabs的新文件夹。在这个新文件夹中,创建三个新的空预制件,分别命名为star、heart和key。 -
通过将 Hierarchy 面板中的 GameObject
pickup拖动到 Project 面板中的star上,来填充star预制件。现在,预制件应该变成蓝色,并复制了星形 GameObject 的所有属性和组件。 -
在检查器中添加一个新的标签
Heart。在 Hierarchy 面板中选择 GameObjectpickup,并分配标签Heart。此外,从 Project 面板(文件夹Sprites)将 healthheart 图像拖动到 GameObjectpickup的 Sprite 属性中,以便玩家在屏幕上看到这个拾取物品的心形图像。 -
通过将 Hierarchy 面板中的 GameObject
pickup拖动到 Project 面板中的Prefabs文件夹下的heart上,来填充heart预制件。现在,预制件应该变成蓝色,并复制了拾取 GameObject 的所有属性和组件。 -
在检查器中添加一个新的标签
Key。在 Hierarchy 面板中选择 GameObject 的pickup,并分配这个标签Key。此外,从 Project 面板(文件夹Sprites)将图像 icon_key_green_100 拖动到 GameObject 的pickup的 Sprite 属性中,以便玩家在屏幕上看到这个拾取物品的钥匙图像。 -
通过将 Hierarchy 面板中的 GameObject
pickup拖动到 Project 面板中的 Prefabs 文件夹下的key上,来填充key预制件。现在,预制件应该变成蓝色,并复制了拾取 GameObject 的所有属性和组件。 -
从 Hierarchy 中删除 GameObject 的
pickup。 -
在 Project 面板中,创建一个名为
Editor的新文件夹。在这个新文件夹中,创建一个名为ObjectBuilderEditor的新 C# 脚本类,代码如下:using UnityEngine; using System.Collections; using UnityEditor; [CustomEditor(typeof(ObjectBuilderScript))] public class ObjectBuilderEditor : Editor{ private Texture iconStar; private Texture iconHeart; private Texture iconKey; private GameObject prefabHeart; private GameObject prefabStar; private GameObject prefabKey; void OnEnable () { iconStar = Resources.LoadAssetAtPath("Assets/EditorSprites/icon_star_32.png", typeof(Texture)) as Texture; iconHeart = Resources.LoadAssetAtPath("Assets/EditorSprites/icon_heart_32.png", typeof(Texture)) as Texture; iconKey = Resources.LoadAssetAtPath("Assets/EditorSprites/icon_key_green_32.png", typeof(Texture)) as Texture; prefabStar = Resources.LoadAssetAtPath("Assets/Prefabs/star.prefab", typeof(GameObject)) as GameObject; prefabHeart = Resources.LoadAssetAtPath("Assets/Prefabs/heart.prefab", typeof(GameObject)) as GameObject; prefabKey = Resources.LoadAssetAtPath("Assets/Prefabs/key.prefab", typeof(GameObject)) as GameObject; } public override void OnInspectorGUI(){ ObjectBuilderScript myScript = (ObjectBuilderScript)target; GUILayout.Label(""); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUILayout.Label("Click button to create instance of prefab"); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); GUILayout.Label(""); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if(GUILayout.Button(iconStar)) myScript.AddObjectToScene(prefabStar); GUILayout.FlexibleSpace(); if(GUILayout.Button(iconHeart)) myScript.AddObjectToScene(prefabHeart); GUILayout.FlexibleSpace(); if(GUILayout.Button(iconKey)) myScript.AddObjectToScene(prefabKey); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); } } -
我们的编辑器脚本期望在名为
EditorSprites的文件夹中找到三个图标,所以让我们这样做。首先创建一个名为EditorSprites的新文件夹。接下来,将三个 32 x 32 像素的图标从Sprites文件夹拖动到这个新的EditorSprites文件夹中。现在,我们的编辑器脚本应该能够加载这些图标,用于在检查器中绘制的基于图像的按钮,用户可以通过这些按钮选择要克隆到场景中的拾取预制对象。![如何操作...]()
-
从 Project 面板中,将精灵 cross_hairs.fw 拖动到 Scene 中。将这个 gameObject 重命名为
object-creator-cross-hairs,并在 Inspector 中的其 Sprite Renderer 组件中,将 Sorting Layer 设置为 Foreground。 -
将以下 C# 脚本附加到 GameObject
object-creator-cross-hairs上:using UnityEngine; using System.Collections; public class ObjectBuilderScript : MonoBehaviour { void Awake(){ gameObject.SetActive(false); } public void AddObjectToScene(GameObject prefabToCreateInScene){ GameObject newGO = (GameObject)Instantiate(prefabToCreateInScene, transform.position, Quaternion.identity); newGO.name = prefabToCreateInScene.name; } } -
选择 Rect Tool(快捷键 T),当你拖动 gameObject
object-creator-cross-hairs并在 Inspector 中点击所需的图标时,新的拾取 GameObject 将被添加到场景的 Hierarchy 中。
它是如何工作的...
脚本类ObjectBuilderScript只有两个方法,其中一个方法只有一个语句——Awake()方法简单地将这个 GameObject 在游戏运行时变为非活动状态(因为我们不希望用户在游戏过程中看到我们创建的工具十字准星)。AddObjectToScene(…)方法接收一个预制体的引用作为参数,并在该时刻在 GameObject object-creator-cross-hairs的位置在场景中实例化预制体的新克隆。
脚本类ObjectBuilderEditor在类声明之前立即有一个 C#属性[CustomEditor(typeof(ObjectBuilderScript))],告诉 Unity 使用这个类来控制ObjectBuilderScript GameObject 的属性和组件在检查器中如何显示给用户。
有六个变量,三个用于在检查器中形成按钮的图标纹理,以及三个 GameObject 引用,这些引用将创建实例的预制体。OnEnable()方法使用内置方法Resources.LoadAssetAtPath()将这些六个变量赋值,从项目文件夹EditorSprites中检索图标,并从项目文件夹Prefabs中获取预制体的引用。
OnInspectorGUI()方法有一个变量myScript,它被设置为指向 GameObject object-creator-cross-hairs中脚本组件ObjectBuilderScript的实例(这样我们就可以在选择了预制体时调用其方法)。然后,该方法显示了一些空文本Labels(为了获得一些垂直间距)和FlexibleSpace(为了获得一些水平间距和居中对齐),并向用户显示三个带有星星、心形和钥匙图标的按钮。Unity 自定义检查器GUI 的脚本 GUI 技术将一个if语句包裹在每个按钮周围,当用户点击按钮时,if语句的代码块将被执行。当点击任意一个按钮时,都会调用脚本组件ObjectBuilderScript的AddObjectToScene(…)方法,传递与被点击按钮对应的预制体。
结论
在本章中,我们介绍了展示一些 Unity 编辑器扩展脚本的食谱,说明了我们如何通过限制和控制对象的属性以及它们如何通过检查器被选择或更改,使事情变得更简单、更少依赖于脚本,并减少出错的可能性。
在编辑器扩展食谱中提出了序列化的概念,我们需要记住,当我们正在检查器中编辑项目属性时,每次更改都需要保存到磁盘,以确保下次使用或编辑该项目时更新的属性是正确的。这是通过在OnInspectorGUI()方法中首先调用serializedObject.Update()方法,然后在检查器中完成所有更改后,最终调用serializedObject.ApplyModifiedProperties()方法来实现的。有关更多信息和示例,以下是一些自定义编辑器扩展的资源:
-
更多关于 Ryan Meier 博客中自定义 Unity 编辑器的信息,请参阅
www.ryan-meier.com/blog/?p=72 -
更多自定义 Unity 编辑器脚本/教程,包括网格和颜色选择器,请参阅
code.tutsplus.com/tutorials/how-to-add-your-own-tools-to-unitys-editor--active-10047












































































































































































































































































































































浙公网安备 33010602011771号