精通-Unity-UI-开发第二版-全-
精通 Unity UI 开发第二版(全)
原文:
zh.annas-archive.org/md5/34578a8c5f7fe7c292f7bcd0369e38eb译者:飞龙
前言
Unity 中可以集成许多内置 UI 元素,本书将帮助你通过深入描述各种 UI 对象、功能和属性,并提供它们实现的逐步示例来掌握 Unity 的 UI 系统。
这本书面向的对象
本书旨在为使用 Unity 并希望提高对 Unity 内提供 UI 系统知识了解的游戏开发者编写。那些寻求对特定 UI 元素进行深入解释的个人,以及那些寻求在多个游戏类型中实现 UI 项目的逐步指导的个人,也会发现本书很有帮助。需要具备 Unity 和 C#编程的基础知识。
这本书涵盖的内容
第一章,设计用户界面,涵盖了与设计用户界面相关的基本信息。定义了图形用户界面和用户界面的区别。讨论了四种不同的游戏界面类型,如何根据设计原则创建美观的 UI,以及界面隐喻的概念。此外,还详细解释了设置 Unity 项目纵横比和分辨率的方法。
第二章,设计移动用户界面,涵盖了 UI 设计师在为移动设备开发时必须考虑的审美和机械方面的考虑因素。此外,它还讨论了可供开发者使用的资源,以帮助他们设计移动用户界面。
第三章,设计 VR、MR 和 AR 用户界面,涵盖了为 VR、MR 和 AR 应用程序设计用户界面的基本概念。它探讨了这些应用程序中的交互与其他应用程序的不同之处,并讨论了设计它们的最佳实践。
第四章,UI 的通用设计和可访问性,涵盖了设计用户界面以便尽可能多的人使用的相关基本概念。本章将探讨通用设计和为可访问性设计的话题,同时讨论 UI 设计师可以采取的步骤以确保他们的用户界面尽可能无障碍。
第五章,Unity 中的用户界面和输入系统,回顾了 Unity 提供的用于与 UI 一起工作的各种系统。Unity 提供了三个用于设计用户界面的系统,以及两个用于控制输入的系统。本章探讨了这些系统,比较了它们的优点,并讨论了何时使用其中的哪一个。
第六章,画布、面板和基本布局,探讨了通过在画布内适当地布局 UI 元素来开发用户界面。本章使用了面板,并提供了文本和图像的简介。本章包含的示例展示了如何布局基本抬头显示,创建永久背景图像,以及开发基本弹出菜单。
第七章,探索自动布局,讨论了如何实现各种自动布局组件以简化 UI 构建过程。本章包含的示例利用自动布局功能在抬头显示中创建选择菜单和网格式库存。
第八章,事件系统和 UI 编程,涵盖了事件系统及其与 UI 的关联。讨论了如何将事件触发器添加到 UI 元素上。本章涵盖了编程 UI 系统所需的必要关键字,如何通过代码访问 UI 组件,以及如何编写可以通过事件触发器访问的函数。
第九章,UI 按钮组件,探讨了按钮的各种属性。本章中的示例介绍了如何设置按钮的键盘和控制器导航,如何在按钮按下时加载场景,如何创建动画按钮过渡,以及如何使按钮交换它们的图像。
第十章,UI 文本和 Text-TextMeshPro,更详细地讨论了文本属性,并演示了如何通过代码影响它们的属性。本章末尾的示例展示了如何创建一个文本动画对话框,如何创建自定义字体,以及如何创建带有渐变的文本。
第十一章,UI 图像和效果,展示了更多 UI 图像的使用和操作方式。此外,它还演示了如何将各种效果应用于 UI 元素。
第十二章,使用遮罩、滚动条和滚动视图,介绍了如何使用遮罩创建可滚动窗口,以便您的 UI 可以容纳比立即可见的更多项目。
第十三章,其他可交互 UI 组件,涵盖了众多其他 UI 输入。本章末尾介绍了如何使用各种输入以及如何创建带有图像的下拉菜单。
第十四章,动画 UI 元素,全部关于动画 UI。本章中的示例展示了如何使菜单淡入淡出,以及如何使用 Unity 状态机创建宝箱动画。
第十五章,UI 中的粒子,在上一章的动画示例的基础上进行了扩展,并提供了更多使用粒子效果美化 UI 的方法。
第十六章,利用世界空间 UI,展示了如何创建存在于游戏场景中而不是在所有游戏项目“屏幕”前的 UI 元素。示例涵盖了如何为 2D 场景创建交互式 UI,以及为 3D 场景创建交互式、悬停生命值条。
第十七章,优化 Unity UI,涵盖了创建优化用户界面的基本概念。它定义了关键术语,概述了 Unity 中包含的可以帮助您确定游戏性能的工具,并涵盖了与 Unity UI 系统一起工作的各种优化策略。
第十八章,开始使用 UI 工具包,介绍了新的 Unity UI 工具包,并解释了如何使用它创建基本布局。它涵盖了使用此不同 UI 系统的关键概念,同时展示了如何使用它创建编辑器和运行时 UI。
第十九章,使用 IMGUI,讨论了如何使用 IMGUI 系统构建用户界面。在探索了 IMGUI 的基本概念之后,本章涵盖了如何使用该系统创建在编辑器和运行时出现的开发者工具。
第二十章,新的输入系统,介绍了使用新的输入系统进行简单输入设置的技巧。它涵盖了发布者-订阅者架构模式,同时介绍了输入系统的基本概念和原则。此外,它还涵盖了如何将使用输入管理器的项目更新为使用新的输入系统,以及如何以两种不同的方式将您的代码连接到输入系统。
要充分利用本书
本书假设您已经对在 Unity 编辑器内导航和工作有良好的理解。此外,它还假设您对使用 C# 编程进行 Unity 开发有基本的了解 知识。
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Unity 2020 或更高版本 | Windows、macOS 或 Linux |
除了安装 Unity 之外,您还需要一个代码编辑器(IDE)。虽然本书没有指定偏好,但支持的 IDE 示例包括 Visual Studio 和 JetBrains Rider。*
如果您使用的是本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误 。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件 github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包可供在 github.com/PacktPublishing/ 上获取。查看它们吧!
使用的约定
本书中使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“目前,AnimationComplete 触发器有点问题。”
代码块设置如下:
[System.Serializable]public class Translation { public string languageKey; public string translatedString; public Font font; public FontStyle fontStyle;}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
Vector2[] newPositions = new Vector2[]{Input.GetTouch(0).position, Input.GetTouch(1).position};
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“当选择永久作为可见性属性时,相应的滚动条将保持可见,即使不需要,如果允许其相应的移动。”
小贴士或重要注意事项
看起来是这样的。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问 www.packtpub.com/support/errata 并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送至 copyright@packt.com 并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问 authors.packtpub.com。
分享您的想法
读完 Mastering UI Development with Unity 后,我们很乐意听听您的想法!请 点击此处直接访问此书的 Amazon 评论页面 并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢随时随地阅读,但又无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
别担心!现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何地点、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制并粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、新闻通讯以及每天收件箱中的精彩免费内容。
按照以下简单步骤获取福利:
- 扫描二维码或访问以下链接:

packt.link/free-ebook/9781803235394
-
提交您的购买证明。
-
就这些!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱。
第一部分:设计用户界面
在本部分中,您将获得在设计各种平台用户界面时需要考虑的设计原则的概述。如何为移动游戏和 XR 体验的特殊情况设计 UI 被探讨。此外,还涵盖了如何考虑通用设计和无障碍性来设计适用于所有平台的 UI。最后,比较和讨论了 Unity 中促进 UI 开发的各个系统。
本部分包含以下章节:
-
第一章, 设计用户界面
-
第二章, 设计移动用户界面
-
第三章, 设计 VR、MR 和 AR 用户界面
-
第四章, UI 的通用设计和无障碍性
-
第五章, Unity 中的用户界面和输入系统
第一章:设计用户界面
当与用户界面(UI)打交道时,理解一些设计基础是很重要的。本章将涵盖设计 UI 的基础和一些关键概念,以帮助您正确起步。
在本章中,我们将讨论以下主题:
-
定义 UI 和 GUI
-
描述四种类型的界面
-
布局用户界面
-
区分和设置分辨率和宽高比
本书不是关于设计 UI 的艺术。它是一篇技术性文章,讨论 UI 功能的实现。然而,我确实想讨论一些 UI 设计的基本设计原则。我不期望你在阅读本章后成为一个出色的 UI 设计师。但我确实希望你能从本章中获得一些关于布局和设计原则的基本理解,这样也许你的艺术家朋友们就不会太取笑你了。
技术要求
对于本章,您需要以下内容:
Unity 2020.3.26f1 或更高版本
定义 UI 和 GUI
那么,UI 和 GUI 究竟代表什么,它们之间有什么区别?UI代表用户界面,而GUI(发音为“gooey”)代表图形用户界面。界面意味着交互,因此 UI 是让玩家与游戏交互的一组设备。鼠标、键盘、游戏控制器、触摸屏等等都是 UI 的一部分。GUI 是 UI 的图形子集。因此,屏幕上的按钮、下拉菜单和图标都是游戏 GUI 的一部分。由于 GUI 是 UI 的子集,许多人(包括我自己)倾向于只把 GUI 称为 UI。Unity 也将他们提供的所有 GUI 项目模板称为 UI。
本书将主要关注 GUI 设计,但也会讨论 UI 控件的一些非图形方面,例如从鼠标、屏幕点击、键盘或控制器访问数据。本章将特别探讨不同界面类型的一些基本设计考虑因素。
四种游戏界面类型
当你说“游戏 UI”时,大多数人会想到出现在所有游戏项目前面的抬头显示(HUD)。然而,实际上有四种不同类型的游戏界面:非叙事的、叙事的、元和空间的。
Fagerholt 和 Lorentzon 首先在 2009 年的论文《超越 HUD:FPS 游戏中的用户界面以提高玩家沉浸感:硕士学位论文》中描述了这四种不同的界面类型。从那时起,这个术语在整个 UI 游戏设计领域得到了广泛使用。您可以在publications.lib.chalmers.se/records/fulltext/111921.pdf找到原始出版物。
这四种类型的区别是由以下两个维度的交叉决定的:
-
叙事性:它是故事的一部分吗?
-
空间性:它是否在游戏环境之中?
下面的图表展示了这两个问题之间的交叉关系以及它们如何定义四种类型的界面:

图 1.1:四种界面类型
一款游戏的 HUD 属于非叙事性类别。这种信息纯粹是为了玩家查看,而游戏中的角色并不知道它的存在。它存在于游戏视图的第四面墙上,看起来像是屏幕上所有内容的前面。这种类型 UI 的例子无穷无尽,因为几乎每款游戏都有一些非叙事性 UI 元素。
另一方面,叙事性界面是指存在于游戏世界中,而游戏中的角色知道它的存在。常见的例子包括角色查看库存或地图。最常提到的叙事性 UI 例子是《死亡空间》中的库存和健康显示。库存显示在一个弹出在可玩角色前面的全息显示窗口上,而你选择他的武器时与之交互。他的健康状态也通过他背上的仪表来指示。《黑暗料理传说》(2008)的库存也是以叙事方式显示的。虽然有一些 UI 元素只有玩家可以看到,但主要角色在夹克口袋中查看库存并与物品交互。无主之地:失落遗产和孤岛惊魂 2都使用角色在场景中实际持有的地图,并与地图交互。《辐射 3》和《辐射 4》使用叙事性界面在角色的 Pip-Boy 上显示库存和地图,Pip-Boy 永久固定在他们的手臂上。当角色在车辆或服装中时,游戏也会使用这种类型的显示,各种显示出现在盾牌、窗户或驾驶舱上。
元界面是指游戏中的角色知道但不在场景中物理显示的界面。常见的例子是赛车游戏的速度显示。Forza 7实际上使用了元和叙事显示的组合来显示速度表。元速度指示器持续显示在屏幕的右下角,供玩家查看。由于角色始终知道他们开得有多快,因此他们会知道这个速度指示器,因此它是一个元界面。还有一种叙事速度表在汽车的仪表板上显示,当以第一人称视角玩游戏时会出现。这种类型显示的另一种常见用途是在屏幕上出现的手机,暗示可玩角色正在与之交互。女神异闻录 5、Catherine和侠盗猎车手 5都使用这种界面类型进行手机交互。
最后一种类型的界面,空间界面存在于场景中,但游戏中的角色对此并不知情。存在于场景中但角色并不知情的界面非常常见。这通常用于让玩家知道场景中可交互物品的位置、游戏角色正在做什么,或者场景中角色和物品的信息。例如,在《塞尔达传说:荒野之息》中,箭头出现在敌人的头顶上,指示林克将要攻击谁。林克实际上并不意识到这些箭头图标;它们的存在是为了让玩家知道他正在关注谁。“异度之刃 2”使用空间界面,通过在可挖掘区域上方显示铲子图标来指示玩家可以挖掘的位置。
布局 UI 元素
当布局您游戏的 UI 时,我强烈建议检查同一类型的其他游戏,看看它们是如何实现它们的 UI 的。玩这些游戏,看看它们是否让您感觉良好。
如果您不确定如何布局您游戏的 UI,我建议将游戏屏幕划分为一个带排水沟的网格,就像以下图中所示,并将物品放置在非排水沟区域:

图 1.2:带排水沟的网格
您可以使用尽可能多的网格,但根据网格布局项目将有助于确保 UI 以平衡的方式排列。
在大多数情况下,HUD 项目应保持在网格的外边缘。任何显示在中心网格中的 UI 都会限制玩家的视野。因此,这个区域适合弹出窗口,暂停游戏。
您的游戏将在哪种设备上运行对于确定布局很重要。如果您的游戏是为移动设备设计的,并且有很多玩家将与之交互的按钮,那么按钮通常最适合屏幕的底部或侧面部分。这是因为玩家握手机的方式,以及屏幕的顶部中央部分是玩家用拇指最难触及的区域。此外,伸手到这个区域会让他们用手挡住大部分游戏视图。我们将在第二章中更详细地讨论为移动设备设计 UI。
您会注意到,当您玩电脑游戏时,它们往往比移动和主机游戏有更小、更杂乱的 UI。这是由于可见性和交互。用鼠标点击小对象比用手指轻触或用 D-pad 选择它们要容易得多。此外,屏幕分辨率要大得多,这允许 UI 占用更多空间。
当试图确定 UI 项的大小和相对位置时,你可以参考菲茨定律。菲茨定律可以基于 UI 项的大小和距离用户起始位置的距离,从数学上计算出用户导航到 UI 项所需的时间。在这里我不会讲解数学(尽管我内心渴望这样做),但可以从菲茨定律中汲取的教训如下:
-
不要使可交互的 UI 元素太小且间隔太远
-
将最重要的可交互元素设计得最大,并且彼此靠近
接下来,我们将探讨分辨率和宽高比。
分辨率和宽高比
游戏的分辨率是游戏运行的屏幕的像素维度。例如,一个游戏可以运行在 1,024x768 上。这意味着游戏宽度为 1,024 像素,高度为 768 像素。游戏的宽高比是宽度和高度的比率(表示为宽度:高度)。这个宽高比是通过将分辨率宽度除以分辨率高度然后简化分数来确定的。例如,如果你的游戏分辨率为 1024x768,那么宽高比如下:
1024px/768px=4/3
在这里,分数 4/3 代表宽高比 4:3。
下表提供了一组常见的宽高比和相关分辨率:

图 1.3:常见的宽高比和分辨率
在设计你的 UI 时,分辨率和宽高比将在你的 UI 外观中扮演重要角色。了解你的目标设备的分辨率和宽高比将是设计你的 UI 的重要第一步,原因如下:
-
它将决定你的 UI 布局
-
你在 Unity 中构建 UI 的方式将由你计划支持多少分辨率和宽高比来决定
如果你只为单一分辨率/宽高比构建,UI 的构建将会更加简单,因为你不需要确保所有元素在多个宽高比下都保持其相对位置。然而,如果你构建的游戏将在多个分辨率/宽高比下运行(例如,一个移动项目或一个在窗口内缩放的网页游戏),你希望你的 UI 能够适当地缩放和移动。你还将希望能够在测试期间轻松地更改分辨率,以确保 UI 在显示窗口变化时能够适当地定位。
即使你将允许分辨率和宽高比变化,你也应该决定一个默认分辨率。这个默认分辨率代表了你理想设计的分辨率。这将是你初始设计和 UI 布局的基础,因此如果分辨率或宽高比发生变化,UI 将尽可能保持相同的设计。
注意
由于今天所有销售的电视都具有 16:9 的宽高比,因此为控制台游戏制作的任何 UI 都应该考虑到 16:9 的宽高比。
改变游戏视图的宽高比和分辨率
您可以在游戏标签页中轻松地在不同的分辨率和纵横比之间切换。这将允许您查看您的 UI 在不同的分辨率和纵横比下的缩放情况:
-
如果您导航到您的游戏标签页,您将看到文字自由纵横比。点击自由纵横比将显示一个菜单,显示各种纵横比和分辨率:
![图 1.4:从游戏视图中选择自由纵横比模式]()
图 1.4:从游戏视图中选择自由纵横比模式
此列表中显示的项目是您当前所选构建目标最常见的纵横比和分辨率。在前面的屏幕截图中,我的构建目标是PC、Mac 和 Linux 独立版,因此显示了最常见的监视器设置。如果我将构建目标更改为 iOS,我会看到一组流行的 iPhone 和 iPad 屏幕尺寸。
自由纵横比意味着游戏的纵横比将相对于游戏视图的窗口进行缩放。因此,通过在游戏窗口上移动框架,您将改变纵横比。
-
您可以通过将编辑器的布局设置为同时显示屏幕和游戏标签页的布局,轻松地看到自由纵横比对游戏纵横比的影响。例如,将布局设置为2x3将做到这一点。在 Unity 编辑器的右上角选择布局下拉菜单以更改布局。
![图 1.5:更改编辑器布局]()
图 1.5:更改编辑器布局
现在,游戏和场景标签页都将显示在您的屏幕左侧。
![图 1.6:2x3 布局的成果]()
图 1.6:2x3 布局的成果
-
现在,减小游戏标签页的大小,使其成为一个非常小的细长矩形。您会看到,场景视图中的主摄像机现在也显示为一个非常小的细长矩形:

图 1.7:在自由纵横比模式下调整游戏视图大小的成果
-
您可以从下拉菜单中选择一个纵横比,并看到,当您调整游戏窗口的大小时,代表实际游戏的蓝色区域将保持您选择的纵横比,黑色条带将填充任何额外的空间。摄像机也将保持该比例。
-
全高清 (1920x1080) 将尝试模拟 1920x1080 的分辨率。您设置的游戏标签页的窗口很可能不足以支持 1920x1080 像素;如果是这样,它将按以下截图所示进行缩放:

图 1.8:游戏视图缩放
-
如果您想使用的分辨率或纵横比在分辨率下拉菜单中不可用,您可以通过在下拉菜单底部选择加号来添加自己的项目到该菜单。如果您想创建一个固定分辨率项,请将 类型 设置为 固定分辨率。如果您想创建一个固定纵横比项,请将 类型 设置为 纵横比。
![图 1.9:添加新的分辨率或纵横比预设]()
图 1.9:添加新的分辨率或纵横比预设
例如,如果您想制作一个让人联想到旧 Game Boy 游戏的游戏,您可以添加一个 160x144 像素的预设:
![图 1.10:创建固定分辨率预设]()
图 1.10:创建固定分辨率预设
-
一旦您点击 确定,新的预设项将显示在列表底部。当您选择它时,游戏选项卡的相机和可见区域将保持由 160x144 分辨率创建的纵横比:

图 1.11:选择自定义预设
为单一分辨率构建
如果您正在创建计划在 PC、Mac 和 Linux 独立目标平台上构建的游戏,您可以强制分辨率始终相同。为此,请转到 编辑 | 项目设置 | 玩家。此时,您的检查器应显示以下内容:

图 1.12:PC、Mac 和 Linux 独立玩家分辨率设置
这里可能显示的平台数量可能更多或更少;这取决于您安装的 Unity 模块。
要在 PC、Mac 和 Linux 独立游戏中强制特定分辨率,请取消选择 默认为原生分辨率。将提供输入 默认屏幕宽度 和 默认屏幕高度 的选项,您可以输入所需的分辨率值。然后,当您构建游戏时,它将以您指定的尺寸播放。
以下截图显示了强制 PC 游戏以 Game Boy Color 尺寸的窗口播放的设置:

图 1.13:设置特定的 PC、Mac 和 Linux 独立玩家分辨率
您也可以通过 WebGL 构建强制设置特定的分辨率。需要考虑的选项较少,但基本概念是相同的。以下截图显示了在 WebGL 的 Player Settings 中强制游戏以 160x140 分辨率显示的设置:

图 1.14:设置特定的 WebGL 分辨率
在 第二章 中,我们将讨论如何为具有可变分辨率且无法预先定义的移动游戏设置分辨率属性。
摘要
本章讨论了一些与 UI 相关的基本设计原则和术语。你现在应该能够区分 GUI 和 UI,并定义四种类型的界面:叙事性界面、空间界面、元界面和非叙事性界面。此外,你还应该了解一些布局 UI 的基本规则以及如何在不同的分辨率和宽高比下工作。
下一章将扩展这些设计原则,并探讨为移动游戏设计 UI 时的一些重要考虑因素。
第二章:设计移动用户界面
设计移动界面最具挑战性的方面之一是移动设备可能具有的纵横比数量。移动设备包括手机和平板电脑。手机通常比平板电脑长得多,一个在手机上完美适配的 UI,当放在平板电脑上时可能会重叠或看起来挤压。除了尴尬的分辨率之外,还有一些怪癖会影响您的 UI,例如 iPhone 的刘海和三星 Galaxy 设备的各种折叠屏幕。
移动设备还有一套不同的输入和交互。例如,在移动设备上,您可以在多个位置同时触摸屏幕,但在 PC 上,您一次只能用鼠标在一个地方点击。移动设备需要屏幕键盘和其他外围设备来执行您可能在控制台或 PC 游戏中执行的动作。
在本章中,我将讨论围绕移动设备各种怪癖的设计考虑因素,并涵盖以下主题:
-
如何在多种移动分辨率下模拟您的游戏并在特定方向上构建
-
移动游戏的推荐按钮大小
-
利用不可见按钮创建触摸区域
-
根据拇指区域布局交互
-
多点触控输入在移动 UI 中的作用
-
何时使用加速度计和陀螺仪
技术要求
对于本章,您需要 Unity 2020.3.26f1 或更高版本。
注意
在描述移动界面时,我将主要关注 iOS 和 Android 操作系统的手机和平板电脑。然而,偶尔我也会提到微软,因为他们确实创建了一系列平板电脑,尽管它们运行 Windows 操作系统,但它们确实具有触摸功能。
设置分辨率、纵横比和方向
当您设计移动 UI 时,您希望确保它在各种分辨率大小和纵横比下都有意义且可见。您还可能希望允许不同的屏幕方向。在第六章中,我将讨论您如何开发能够适应多个分辨率和布局的用户界面。但就目前而言,让我们先回顾一下分辨率、纵横比和方向如何影响您的设计,以及如何使用各种屏幕设置查看您的游戏。
在游戏视图中设置分辨率
在第一章中,我们回顾了如何更改游戏视图的分辨率和纵横比。当您将游戏构建设置更改为 iOS 或 Android 时,将为您提供一组新的预设。例如,在以下图像中,您可以看到当构建设置为 iOS 时的可能性列表:

图 2.1:游戏视图中的 iOS 分辨率
它不会显示所有可能的 iOS 分辨率,但许多常见的较新分辨率都会显示。你可以在www.ios-resolution.com/找到更完整的列表。类似地,当切换到 Android 构建时,不会列出所有 Android 分辨率,特别是考虑到 Android 分辨率数量显著更多。然而,你可以在developer.android.com/guide/practices/screens_support.xhtml#testing找到有关 Android 屏幕分辨率的更多信息。
设备模拟器
有时候,仅设置游戏视图的纵横比并不足以完全看到你的 UI 在设备上的显示效果。例如,iPhone X 中引入的具有争议性的刘海在游戏视图中不会显示,可能会对你的精心设计的 UI 造成很大的影响。然而,你可以使用设备模拟器来查看这个刘海以及它如何与你的 UI 重叠。
要启用设备模拟器,请完成以下步骤:
-
从包管理器下载设备模拟器包。
要这样做,请转到窗口 | 包管理器,然后在列表中搜索设备模拟器。最初你很可能看不到它。如果你看不到它,请确保显示的包来自Unity 注册表,如下面的截图所示:

图 2.2:Unity 注册表包
- 如果你仍然在列表中看不到它,你必须启用预览包。点击右上角的设置齿轮,选择高级项目设置,然后选择启用预览包,如图图 2**.3所示:

图 2.3:启用预览包
- 一旦你在列表中找到设备模拟器,请安装它:

图 2.4:安装设备模拟器
- 现在设备模拟器已下载,你可以通过选择游戏下拉菜单,然后选择模拟器来更改你的游戏视图以反映模拟器,如下面的截图所示:

图 2.5:选择模拟器视图
- 现在你可以从下拉菜单中选择多个设备进行模拟。例如,我可以选择第五代 iPad 或 iPhone X,其中 iPhone X 显示了令人讨厌的刘海:

图 2.6:模拟器上的不同设备
- 此外,你可以选择安全区域来查看放置 UI 的最佳位置:

图 2.7:安全区域
构建特定方向
你可能已经注意到在 图 2**.1 中,每个分辨率都有横屏和竖屏选项。这允许你在开发过程中根据你想要支持的方向来查看你的游戏。
当为移动设备构建时,你不能指定分辨率和纵横比,而必须支持所有分辨率和纵横比。然而,你可以在移动设备上选择屏幕方向。有两种不同的方向:横屏和竖屏。
建造得比高更宽的游戏被称为横屏分辨率。建造得比宽更高的游戏被称为竖屏分辨率。例如,16:9的纵横比将是横屏分辨率,而9:16的纵横比将是竖屏分辨率,如下所示:

图 2.8:横屏与竖屏方向
因此,虽然你不能选择移动游戏构建的确切纵横比,但你可以选择方向,这将迫使纵横比要么更宽,要么更高。你可以通过导航到 编辑 | 项目设置 | 玩家设置 并选择移动设备来设置方向。如果你同时为 iOS 和 Android 构建游戏,你不需要为两者设置这些属性。如以下截图所示,默认方向属性旁边的星号表示这些设置在多个平台之间是共享的:

图 2.9:移动设备的分辨率和方向设置
你可以将默认方向设置为自动旋转或其他旋转之一,如下所示:

图 2.10:移动设备的方向选项
Unity 将以下方向定义为以下旋转:

图 2.11:移动设备方向旋转
当你选择除自动旋转之外的默认方向时,游戏将只在设备上以该方向播放。如果你选择自动旋转,你将可以选择多个方向:

图 2.12:自动旋转选项
在大多数情况下,最好只选择横屏方向或只选择竖屏方向,而不是所有四个方向。通常,允许所有四个方向会导致游戏 UI 的缩放出现问题。
玩家往往更喜欢能够旋转他们的游戏(尤其是像我这样喜欢在手机充电时躺在床上玩游戏的人),所以除非你有充分的理由停止旋转,否则启用纵向和纵向颠倒或横向右和横向左是一个好主意。
推荐按钮大小
在创建移动游戏时,几乎所有的交互都是由按钮和屏幕点击控制的。在 PC 或游戏机上的合理大小的按钮可能对于移动游戏来说太小了。因此,你需要确保设计你的按钮,以便它们在小屏幕上仍然可见,并且足够大,可以用手指触摸。
苹果、谷歌和微软在设计设备时,都对按钮的可触摸区域大小有特定的推荐。苹果建议按钮大小为 44 点 x 44 点。谷歌推荐 48 dp x 48 dp,两个按钮之间间隔 8 dp。微软推荐 9 mm x 9 mm,两个按钮之间填充 2 mm。
你可以在以下位置找到有关为每个移动平台设计触摸/击打区域的信息:
-
苹果:
developer.apple.com/design/human-interface-guidelines/layout -
谷歌:
material.io/design/usability/accessibility.xhtml#layout-and-typography -
微软:
docs.microsoft.com/en-us/windows/uwp/design/input/guidelines-for-targeting
令人烦恼的是,所有这些推荐都使用不同的度量单位。那么,这些数字在设计方面究竟意味着什么?你如何确保你的按钮是 9 mm x 9 mm 或 44 点 x 44 点?他们为什么用不同的单位来谈论这些测量值?这几乎就像他们都是竞争对手,不想好好合作!为了回答这些问题,让我们首先看看各种度量单位代表什么:
-
点(pt)用于测量屏幕上的物理尺寸。1 点等于国际英寸的 1/72 或 0.3528 毫米。它主要用于排版和印刷媒体。当使用像Illustrator这样的程序时,在点中创建对象,然后以 72 ppi 导出你的图像,像素和点的大小就相同。点和像素并不相同,除非以 72 ppi 导出。
-
密度无关像素(dp),发音为“dips”,是一个用于在具有不同 dpi(每英寸点数)值的屏幕上保持项目大小一致的测量单位。密度无关像素测量的是在 160 dpi 屏幕上 1 像素的大小。使用这种转换就像说它会在 160 dpi 屏幕上以这个大小显示,并且应该在任何其他屏幕上以相同的物理大小显示。你可以在
developer.android.com/training/multiscreen/screendensities上了解更多关于密度无关像素的信息。 -
当使用 毫米(mm)来描述按钮大小时,这个大小是屏幕上按钮的物理表示。因此,如果你用尺子对着屏幕,它将与这个测量单位一致。
好的,所以它们都代表了屏幕上的一些物理测量单位。这使得事情变得简单一些。让我们将这些值都转换为毫米,这样我们就可以在一个更容易概念化的测量单位中比较它们。我还会将它们转换为点,因为你可以使用点在程序(如 Illustrator)中创建你的按钮艺术。
如果你需要转换这些测量单位之一,并且不太喜欢数学,搜索 convert points to mm 将会为你提供一个不错的转换计算器。
你还可以使用以下转换工具——它对于在所有不同的测量单位之间转换来说非常方便:angrytools.com/android/pixelcalc/
在下面的图表中,我将像素的测量值四舍五入到最接近的整数,将毫米的测量值四舍五入到最接近的十分之一,以便于比较。我们可以使用这张图片来比较不同的尺寸(图片已被缩放,尺寸可能不会转换为实际的测量值):

图 2.13:推荐的最低可触摸按钮大小
那么,你应该使用多大呢?这取决于你。你不必使用他们的推荐,但我个人倾向于使用苹果的推荐,因为它最大,因此也符合其他两个的推荐。此外,按钮越大,越多人能够轻松触摸它。在第四章中,我们将讨论如何设计我们的用户界面,以便尽可能多的人能够与之交互。
另一个考虑因素是,你的游戏是用拇指还是用手指来玩。如果游戏是用拇指来玩,你将需要更大的按钮,因为拇指更大!之前描述的数字是最低推荐值,因此它们适用于手指触摸,而不是拇指触摸。
那么,你如何确保你的按钮在游戏中始终保持你想要的尺寸?答案是画布缩放器组件!在第六章中,我们将讨论如何通过将画布缩放器组件的UI 缩放模式设置为常量物理尺寸来确保按钮的指定尺寸,无论分辨率如何。你可以选择将画布的测量单位设置为毫米或点(以及一些其他单位)。
当为移动设备设计时,我的建议是准备多台设备以在各种分辨率下进行测试。玩玩游戏,看看感觉如何。请比你手小和手大的人来玩。即使遵循了各种移动平台指定的最低指南,你仍然可能会发现你的按钮对你所需的操作来说太小了。
谷歌和微软也指定了他们推荐的可见尺寸,因此你可以有一个更小的按钮图像,只要按钮的点击区域是推荐的尺寸。如果你想有一个视觉上更小但点击区域更大的按钮,而不是将按钮组件附加到小块艺术上,而是将其附加到一个更大的父级点击区域,并更改按钮的目标图像为小块艺术。
全屏/屏幕部分点击
许多移动游戏只有一个输入,你可以点击屏幕上的任何位置来执行动作。例如,无尽跑酷游戏通常允许玩家点击或按住屏幕上的任何位置来跳跃。为了实现这一点,你只需要添加一个覆盖整个屏幕的不可见按钮。如果你有其他接收输入的 UI,它需要位于全屏按钮之前,以确保按钮不会阻挡其他 UI 元素的输入。
一些游戏要求你点击屏幕上的特定区域来执行特定动作。例如,我为我的博士论文创建了一个名为 Sequence Seekers 的游戏。这个游戏包括一个下山模式,玩家必须点击屏幕的左侧或右侧来在游戏中左右移动。我是通过添加覆盖屏幕两半的不可见按钮来实现这一点的,如下所示:

图 2.14:使用不可见按钮创建标签区域
在第九章和第十一章中,我们将讨论如何实现这样的按钮,以及如何实现浮动 D-Pad 和摇杆。
拇指区域
在设计移动游戏时,考虑玩家如何握持设备非常重要。您不希望将 UI 放在玩家难以触及的区域。玩家倾向于喜欢单手握持和玩游戏。并非所有游戏都允许这样做,但如果可能的话,您希望允许玩家这样做。您如何知道您的 UI 是否在拇指可触及的区域?将 UI 放在拇指区域!本质上,拇指区域是玩家单手握持手机时感到舒适可触及的区域。您可以通过握持手机并轻松移动拇指来找到您手机的拇指区域,而无需移动您的手。
以下博客文章提供了一个非常好的拇指区域解释,以及一个实用的(无意中打趣)模板,用于在各种设备上找到拇指区域:www.scotthurff.com/posts/how-to-design-for-thumbs-in-the-era-of-huge-screens/
链接中提到的手机可能有些过时,但它在互联网上仍然是关于拇指区域的最佳资源之一。
作为左撇子,我恳请您在设计时考虑使游戏在拇指区域设计下左手的操作与右手一样容易。
其他移动输入
在为移动设备设计时,重要的是要记住输入的工作方式与电脑或游戏机游戏略有不同。在移动设备上,大部分输入由触摸屏、加速度计或陀螺仪控制。这为您在创建移动游戏时提供了不同的设计选择。
触摸屏设备通常可以访问多个触摸点。您可以使用多点触控进行不同类型的交互,但最常见的多点触控用法是允许玩家进行捏合缩放。在第八章中,我们将讨论如何使用多点触控输入在游戏中创建平移和捏合缩放功能。
大多数移动设备都内置了加速度计,许多设备还内置了陀螺仪。在这里我们不会过于技术性地描述它们是如何实际工作的,加速度计和陀螺仪的区别在于它们所测量的内容。加速度计测量 3D 坐标系内的加速度,而陀螺仪测量旋转。我们将在第八章中回顾如何使用加速度计和陀螺仪的示例。
设备特定资源
如果您正在为移动设备制作用户界面,您可能希望使用设备特定的 UI 元素以保持一致的风格。您可以在以下位置找到为每个移动平台设计 UI 的各种艺术资源和模板:
摘要
为移动设备创建 UI 与为控制台或计算机创建 UI 并没有太大区别,但不同之处在于你可以接受多个屏幕输入,并且还可以访问设备加速度计和陀螺仪的信息。此外,分辨率在游戏开发中起着重要作用,因为移动设备具有广泛的分辨率和纵横比。
在下一章中,我们将讨论为 XR 应用程序开发 UI 的设计考虑因素,包括 VR、MR 和 AR。
第三章:设计 VR、MR 和 AR 用户界面
对虚拟现实(VR)、混合现实(MR)和增强现实(AR)的用户界面(UI)设计的研究是广泛的,并且不断增长。它是一种新兴技术,最佳实践尚未完全定义;然而,有许多研究人员和开发者正在勤奋地确定这些最佳实践。在本章中,我将总结一些普遍认同的最佳实践,用于设计扩展现实(XR)体验的用户界面。
在本章中,我将讨论以下内容:
-
区分 XR、VR、MR 和 AR 应用
-
开发 VR 用户界面的考虑因素和最佳实践
-
开发 MR 用户界面的考虑因素和最佳实践
-
设计 AR 用户界面的考虑因素和最佳实践
XR、VR、MR 和 AR 是什么?
在我们开始讨论 VR、MR 和 AR 的最佳实践之前,我们应该明确 XR、VR、MR 和 AR 的定义。
如果你提前浏览本章,可能会注意到没有名为 设计 XR 用户界面 的章节。这是因为 XR 包括了 VR、MR 和 AR!它是一个总称。事实上,本章的一个更简洁的标题可以是 设计 XR 用户界面!XR 包括 VR、MR 和 AR 技术:

图 3.1:XR 技术的表示
VR体验可能最容易描述。它们完全存在于虚拟世界中。整个物理世界被遮挡,你所能看到的就是虚拟空间。这种空间往往可以如此沉浸,以至于大脑难以区分它与现实。便于 VR 的设备会覆盖用户的眼睛,以完全遮挡现实世界的视觉。今天支持 VR 的流行设备包括 Meta(前身为 Oculus)Quest 系列、索尼 PlayStation VR 系列和 HTC Vive 系列。一些流行的 VR 游戏包括 半条命:艾莉克斯 和 节奏大师。
MR在现实世界中叠加虚拟物品,并允许你与之交互。用户可以看到现实世界,并与现实世界物品和虚拟物品进行交互。MR 设备也以眼镜和头戴式设备的形式佩戴在用户的脸上。它们要么不完全遮挡现实世界,要么允许视频透过来允许用户通过镜头看到现实世界。今天便于 MR 练习的流行设备包括 Meta Quest 2(及以上)、微软 HoloLens 系列和 Magic Leap。一个流行的 MR 体验示例是 我期待你死:家 甜蜜的家。
AR与MR类似,因为它结合了现实世界和虚拟世界;然而,它不同之处在于虚拟物品不会与真实世界互动。在 AR 中,虚拟物品只是简单地叠加在真实世界之上,并不出现在真实世界的同一空间内。此外,它也不允许用户以感觉像是在他们的世界中发生的方式与虚拟物品互动。AR 交互通常发生在屏幕上,而 MR 交互发生在物理空间内。这种区别很微妙,但你可以将 AR 视为与屏幕上出现的虚拟物品互动,而 MR 则是与看起来像在你空间中出现的物品互动。市场上可用的 AR 设备数量显著更多。智能手机和平板电脑是最受欢迎的 AR 促进者;然而,一些智能眼镜也能实现这一点。此外,世界各地都有 AR 屏幕、自助服务亭和安装。AR 游戏的流行例子包括宝可梦 GO和入侵者。
注意
关于 XR 及其区别的想法,我在本节中并未完全深入探讨。如果您想了解更多,我建议研究虚拟性连续体:www.interaction-design.org/literature/topics/virtuality-continuum。
现在我们已经了解了 VR、MR 和 AR 之间的区别,让我们来看看为这些体验设计 UI 的一些最佳实践。
设计 VR 的 UI
通常,当你想到 UI 时,你会想到一个抬头显示(HUD)——UI 位于所有游戏玩法前面的屏幕上。但在 VR 中,并没有像在游戏机上玩游戏那样的屏幕。玩家感觉就像完全沉浸在世界中,如果在他们体验 VR 游戏时所看到的屏幕上放置一些东西(即镜头),由于它正位于他们的眼睛上方,这会导致模糊、无法观看的模糊。因此,VR UI 往往放置在玩家将要沉浸其中的世界中。
注意
在第十六章中,我们将讨论在世界上创建具有物理表示的 UI 的步骤。
虽然可能有例外,但在 VR 游戏中,UI 通常放置在三个位置:
-
静态地嵌入在世界中。玩家必须通过将他们的化身移动到其虚拟位置来接近它。
-
嵌入在世界中,距离玩家脸部一定距离。UI 随着玩家移动,并且无论玩家转向哪里,总是处于相同的相对位置。或者玩家的化身在空间中不移动,UI 也不移动。
-
附在玩家的手上——UI 以某种外围设备的形式出现在玩家的化身虚拟手/臂上。
你选择哪种放置方式取决于你的游戏设计,但也可能受到你是否以及如何想要玩家与 UI 交互的影响。本质上,你想要确保玩家可以看到 UI 并且可以与之交互(如果需要)。
让我们从设计视觉用户界面的考虑因素开始我们的 VR 用户界面设计探索。
视觉用户界面放置和考虑因素
正如我之前所说,由于这项技术仍被视为新兴技术,研究人员和开发者仍在学习最佳实践,因此没有一套明确的设计 VR 游戏体验的规则。
当创建一个始终与玩家保持特定距离的用户界面时,重要的是将其放置在玩家可以看到的位置。仅仅因为玩家可以看到一个物品,并不意味着看到它是舒适的。例如,虽然玩家可以看到他们视野边缘的物品,但这并不是放置用户界面的舒适位置。以下图表显示了人的视野(FOV)的一般角度,其中灰色区域是最大观看角度:

图 3.2:人类视野的概括
注意
前一图中描述的角度是一般性指南,并非固定不变。它们将根据用户的视力、是否需要他们旋转眼睛(而不是头部),以及你使用的设备而变化。
请记住,当玩家移动他们的头部时,摄像头会随之移动。因此,如果你的用户界面放置在一个鼓励他们旋转头部并且固定在头部位置的地方,用户界面将会随着他们的头部移动。所以,尽量将任何固定的用户界面放置在图 3.2中描述的区域。2*。
在相对于玩家眼睛的角度之上,你还必须考虑从玩家眼睛的距离。如果太近或太远,都会造成眼睛疲劳。尽量将任何视觉 UI,特别是必须阅读的文字,保持在玩家眼睛 1.3 到 3 个游戏米内。
如果玩家处于静止状态,创建一个距离他们一定距离的静态弯曲用户界面总是有助于玩家舒适地移动头部以查看用户界面。
我想要讨论的最后一个视觉用户界面考虑因素是文字大小。虽然建议可能因来源而异,但我看到的一致引用(无意中打趣)是,UI 应该以每米 2.32 厘米的高度出现。所以,如果它显示在 1 米远的地方,它应该有 2.32 厘米高。如果它显示在 2 米远的地方,它应该看起来是 4.64 厘米高。我个人视力非常差,我倾向于使我的 UI 文字比这大一点,这样我就可以轻松地看到它。我的建议是让不同视力的人玩你的游戏,并告诉你用户界面是否舒适。
现在我们已经回顾了在 VR 空间中设计视觉 UI 的注意事项,让我们来回顾一下交互式 UI 的注意事项。
交互式 UI 的位置和考虑因素
在 VR 中,交互式 UI 的实现方式有几种,这取决于体验是否使用控制器或手势追踪。
当使用控制器时,以下是 UI 交互的常见方式:
-
玩家只需按下按钮即可执行交互——例如,按下菜单按钮以使菜单按钮出现和消失。
-
玩家使用射线选择 UI 项目。玩家指向 UI 项目,从他们的手中出现一条射线,然后他们按下按钮与项目进行交互。
-
玩家的化身在虚拟空间中接近 UI 项目,并与它进行交互。这包括用虚拟手在虚拟空间中按下按钮或抓取虚拟物品。
当使用手势追踪时,以下是 UI 交互的常见方式:
-
玩家执行一些手势,完成 UI 交互。例如,做一个点赞手势可能会使菜单出现或消失。
-
玩家用手指指向一个项目,从他们的手指中出现一条射线。然后他们完成其他手势以确认选择。
-
玩家的化身在虚拟空间中接近 UI 项目,并与它进行交互。这包括用虚拟手在虚拟空间中按下按钮或抓取虚拟物品。
当通过射线与 UI 交互时,玩家不需要物理上能够触及它。但如果你想要创建玩家化身必须虚拟交互的 UI,你需要确保它在玩家水平方向和垂直方向上都能触及。可达性的因素包括以下:
-
游戏是否以静止模式进行或玩家是否需要四处移动
-
玩家的化身是否可以移动
-
玩家的高度
-
如果玩家是坐着还是站着
-
玩家的总体移动性和残疾
一个好的经验法则是确保玩家可以通过将项目放置在距离他们静止位置 0.5 到 0.75 米之间来触及它们。就像你想要用不同视力水平的人测试你的视觉 UI 一样,你也会想用不同身高和能力的人测试你的 UI。
在使用控制器和手势追踪的情况下,你想要确保你选择的任何交互都不会让玩家感到疲劳。要求他们执行多个手势或长时间举起手可能会引起疲劳和压力,这是你不想施加在玩家身上的。
这就结束了我想讨论的关于为 VR 设计 UI 的主要注意事项。然而,还有许多研究和信息等待你去探索!有关为 VR 设计 UI 的更多信息,我建议查看本章末尾提供的资源。
现在我们已经了解了为 VR 设计 UI,接下来让我们看看如何为 MR 设计 UI。
为 MR 设计 UI
为 MR 设计 UI 与为 VR 设计 UI 极为相似,所以前一部分讨论的大部分内容都适用。然而,为 MR 设计 UI 确实有几个额外的注意事项。
设计 VR UI 和 MR UI 之间的主要区别在于 MR 结合了玩家的物理空间和周围物品。当玩 VR 时,人们通常被认为周围有宽敞的空间——这样当他们鲁莽地挥舞时,不会碰到任何东西伤害到自己。相反,MR 体验往往鼓励玩家在他们现实世界的家具、墙壁和其他物品周围活动。
虚拟放置的 UI 不仅必须通过玩家访问,而且不能与玩家世界中存在的事物相交。你不能让玩家按下他们桌子里的按钮或观看他们墙壁后面的屏幕。因此,在设计 MR 体验的 UI 时,你必须考虑玩家的空间,而不仅仅是玩家的身体来放置你的 UI。
解决此问题的方法包括允许玩家选择他们的 UI 显示位置,将其显示在技术检测到的表面之上(检测方式高度依赖于玩家所使用的设备),或者将其附加到玩家身上。
与 VR 相比,MR 是一个更加新兴的空间,尤其是在视频游戏领域。到目前为止,MR 主要用于工业和医疗目的,并且通常在高度受控的空间内进行。对于在家中的玩家来说,MR 具有无限可能的家具和物体放置方式,仍处于起步阶段,直到最近才可以通过 Microsoft HoloLens 在 Unity 中实现。然而,Meta Quest 2 增加了在 Unity 中创建 MR 游戏的能力,而 Meta Quest 3 计划提供更多精选和完善的 MR 支持。此外,Magic Leap 与 Unity 合作,允许进行 MR 开发。因此,你可能会在未来几年看到更多关于 MR UI 的最佳实践和技术。
注意
有关使用 Unity 和 HoloLens 进行开发的更多信息,请参阅learn.microsoft.com/en-us/windows/mixed-reality/develop/unity/unity-development-overview?tabs=arr%2CD365%2Chl2。
有关 Quest 3 计划中的 MR 功能的信息,请参阅developer.oculus.com/blog/build-the-next-generation-of-vr-mr-with-meta-quest-3/。
有关使用 MagicLeap 进行开发的更多信息,请参阅ml1-developer.magicleap.com/en-us/learn/guides/unity-overview。
很遗憾,由于 MR 在 Unity 引擎中是一个如此新且新兴的空间,我无法就设计 MR 的用户界面说更多。然而,AR 已经存在了一段时间,所以现在让我们来探讨它!
设计 AR 的用户界面
记住,区分 AR 和 MR 的是 AR 倾向于在覆盖真实世界的屏幕上,而 MR 则显得更加沉浸和融入世界。因此,AR 体验的交互项目将显示在屏幕上,而不是在世界上。
设计增强现实(AR)的用户界面高度依赖于用于增强现实所使用的设备。为了清晰起见,我将专注于讨论为手机开发的 AR 游戏,而不是试图普遍性地讨论包括分期付款和自助服务亭等内容。
当在移动设备或触摸屏上设计 AR 应用程序的用户界面时,您将希望遵循第二章中概述的规则。最佳实践是将大多数非增强数字项目(即,抬头显示 UI)放置在屏幕的外边缘。然后,增强数字项目将出现在屏幕的中间。这将导致增强世界的项目成为焦点。
摘要
在本章中,我们讨论了开发 XR 体验的一些基本设计考虑因素。由于我们体验 VR、MR 和 AR 的方式都不同,我们回顾了在设计每种类型体验时应考虑的因素。
XR 仍然是一个新兴空间,因此还没有许多广泛接受的标准。这一点在 MR 中尤其如此,因为最近才开始有面向消费者的 MR 设备。然而,本章提供的信息应该有助于您开始 XR 用户界面设计,并开始思考 XR 空间与屏幕空间的不同之处。
在下一章中,我们将讨论通用设计和可访问性的概念,以及您如何使您的用户界面更具包容性和用户友好性。
进一步阅读
关于学习设计 XR 用户界面的更多资源,请在此处找到:
-
medium.com/@oneStaci/https-medium-com-ux-vr-18-guidelines-51ef667c2c49 -
developer.qualcomm.com/blog/xr-user-interfaces-and-perception-technologies -
uxplanet.org/ux-101-for-virtual-and-mixed-reality-part-1-physicality-3fed072f371
第四章:通用设计及 UI 的可访问性
当您设计界面时,您需要考虑所有可能与您的游戏互动的不同类型的玩家。您需要考虑他们的体型、年龄、当前情况、环境位置、输出设备、输入设备、认知能力、偏好、移动能力水平等。您希望您的界面尽可能适用于各种类型的人。
在设计过程的开始就考虑这些因素很重要,因为最初在设计 UI 时就考虑到这些因素,要比在 UI 已经设计、构建和实施之后再添加它们容易得多。
在本章中,我们将讨论以下主题:
-
什么是通用设计和可访问设计?
-
通用设计原则
-
可访问性设计
什么是通用设计和可访问设计?
罗纳德·马西提出了“通用设计”这个术语,并定义为如下:
“将产品和环境设计得尽可能适用于所有人,无需适应或专门设计。”
通用设计是一种设计理念,旨在设计出所有人都可以使用的、不受年龄、体型、偏好、能力、残疾、状况或情况限制的产品。它不专注于为特定群体设计,而是专注于设计出最广泛人群都能使用的产品。虽然最初关注的是建筑,但通用设计的概念已经扩展到包括所有类型产品的设计,包括视频游戏。“可访问设计”是一种专注于为有障碍和残疾的人设计的产品,它是通用设计的一个子集。
为了使设计有效,必须做出有目的的决定,以确保设计对所有人来说既实用又有用。在设计用户界面时,您应该从设计过程的开始就考虑如何使其对所有人可用和可访问。如果从这些考虑开始设计,那么设计一个普遍可用和可访问的 UI 要比在现有的界面之上添加功能容易得多。
在以下各节中,我将讨论使用通用设计原则设计用户界面时应考虑的因素,以及您应采取的特殊考虑因素,以确保您的界面可访问。
由于本书主要关注 Unity 中的 UI 开发,而 Unity 主要是一个视频游戏引擎,因此我将描述的大多数示例和用例将与视频游戏相关。
通用设计原则
1997 年,北卡罗来纳州立大学的一个工作组开发了 7 个通用设计原则。这些原则旨在帮助指导产品的设计,以便它们可以普遍使用。这些原则在正式定义时,以不同类型的设计为依据,并侧重于建筑。我将用其应用于数字用户界面的方式来概述每个原则,并提供每个原则如何在视频游戏界面设计中应用的示例。如果您想查看原则及其具体指南的列表,可以访问universaldesign.ie/what-is-universal-design/the-7-principles/。
许多这些原则相互重叠,我将提供的某些示例可能适用于多个原则。
公平使用
公平使用原则指出,设计应该对所有人同等可用和吸引人。如果所有用户都无法获得相同的使用体验,他们应该获得等效的体验。界面应该设计成对所有用户都有吸引力,并且不应该使任何用户感到污名化或隔离。这是第一个原则,因为它最终驱动了所有其他原则。
例如,在界面中应使用高对比度的颜色。高对比度的界面不仅对所有用户都有吸引力,而且有助于在直接阳光下使用的移动用户看到您的界面,并且它们避免了给视力受损的用户(如视力低下和色盲)带来污名化。
记住,通用设计的目的是不是明确地关于设计可访问性(即,为有残疾的群体设计)。利用类似高对比度颜色方案这样的东西对所有人都有益,而不仅仅是视力受损的人。这也不是一个需要明确开启的设置,例如开启色盲模式,这将是一种为可访问性设计。我将在本章后面讨论为可访问性设计。
如果您正在制作 PC 游戏,您不希望隐藏信息,使其只能通过鼠标访问。允许通过控制器的方向键或通过键盘的标签导航到信息,将使用不同输入设备(由于个人偏好或视力障碍)的用户能够访问与鼠标用户相同的信息。
使用灵活性
使用灵活性原则指出,设计应适应具有广泛偏好和能力的人群,并且人们在使用设计时应被提供选择。这一原则的关键在于,当用户与您的游戏互动时,为他们提供选择。
如果你正在制作移动游戏,你可以提供左右手模式,以交换按钮在屏幕上的位置,就像我在我的游戏 Barkeology 中所做的那样。这允许玩家轻松地与界面交互,而不会用他们的优势手阻挡游戏。

图 4.1:iOS 版 Barkeology 中的左手模式与右手模式
《幻想生活在线》允许你在三个位置中选择放置“愤怒按钮”的位置——这是一个当玩家可以使用特殊攻击时出现的按钮。这允许玩家根据他们的游戏风格和握手机的方式选择最舒适的位置。
对于 PC 游戏,你可以允许玩家选择不同的输入设备。例如,你可以提供使用键盘和鼠标或控制器进行游戏的选择。你可以允许玩家按他们希望的任何方式映射键盘或控制器,或者给他们提供预定义的方案进行选择。允许玩家选择控制器按钮方案也适用于游戏机。
如果你的游戏中有文本,允许用户更改文本的大小或展示给他们的速度。
你可以在界面上允许灵活性的方式还有很多。在设计你的界面时,只需确保考虑到玩家可能的不同偏好,这样你就可以相应地布局你的界面并映射你的输入。
简单直观的使用
简单直观的使用原则指出,设计应该易于人们理解,无论他们的过去知识、经验、技能或语言水平如何。
不要让你的界面过于复杂。如果你需要解释它,可能需要重新设计。从未玩过视频游戏的玩家应该能够像老手一样轻松理解你的界面。
将最重要的信息放置在最显眼的位置,并使它们最容易找到。不要将常见功能隐藏在多次按钮点击和菜单之后。如果你的菜单系统是嵌套的,并且某些菜单比其他菜单访问得更频繁,考虑将它们映射到快捷键或按钮。
向用户提供反馈,让他们知道何时在与你的界面交互。如果你有屏幕上的按钮,旨在通过触摸或鼠标点击进行交互,确保它们向用户提供反馈,让他们知道何时突出显示或点击它们。
向用户提供提示,让玩家知道你的界面的哪些部分是可交互的。你可以通过设计看起来可以物理按压的按钮或通过动画或配色方案来吸引注意力来实现这一点。
重要的是要记住,你的用户会有不同的阅读水平,可能说不同的语言。在可能的情况下,通过使用更多的图标隐喻来减少对文本的依赖。隐喻是意义被广泛认可的符号。例如,大多数人都会认识到以下图中显示的按钮的意义是播放、暂停、菜单、设置、关闭/取消、确认、静音和保存:

图 4.2:界面隐喻示例
伴随图标隐喻的文本,以便用户有多种方式感知特定图标的意义。
并非总是有可能完全从你的游戏 UI 中移除所有文本,并用图标和图像替换。因此,将你的游戏翻译成多种语言可以增加 UI 的通用可感知性。在第十一章中,我将讨论在构建 UI 时可以做的事情,以使翻译过程更加顺利,以及创建 UI 翻译系统的示例。
可感知信息
可感知信息原则指出,无论用户身处何种环境或感官能力如何,信息都应以可感知的方式进行传达。在考虑这一原则时,你想要思考信息可以传达的替代方式,以及如何在多种场景中清晰地传达信息。记住,界面是玩家通过它感知和与你的游戏互动的透镜。因此,他们必须能够理解它。
UI 的颜色应与背景形成鲜明对比,但也不应过于突出,以免造成视觉疲劳。你的游戏没有特定的颜色方案必须使用,但一般来说,互补色方案是减少视觉疲劳同时产生足够对比度以区分项目最佳的选择。
高对比度的文本在不同光照条件下更容易看清。确保文本大小适中,以便可见,并允许用户根据自己的偏好调整文本大小。此外,选择清晰易读的字体。
旁白不仅仅是用于对话!你也可以为你的菜单提供旁白。例如,在《孤岛惊魂 6》中,自动语音会在启动屏幕上读出各种菜单选项,以便那些有视觉障碍的人仍然能够感知屏幕上的项目。此选项默认开启,用户无需与菜单交互即可访问。
如果您正在制作控制台游戏,请考虑您的玩家将拥有不同类型、不同分辨率和亮度设置的电视。在启动 PlayStation 或 Xbox 游戏时,您可能看到调整亮度的提示,直到图像可见,或者可能看到移动框角直到它们达到屏幕边缘的提示。这些提示是安全措施,以确保游戏无论玩家的电视如何都能正确显示。
错误容忍
错误容忍原则指出,与设计交互的不利后果应尽量减少。我们都在某个时候无意中覆盖保存或删除保存文件,因为我们按错了按钮或看错了说明。错误容忍原则旨在最大限度地减少这类事件的发生。
一个设计良好的界面不一定是方便或易于使用的。人们喜欢快速点击或轻触事物。有时,我们希望让他们更难做到这一点。设计不便的概念涉及使界面不那么方便或更难交互。这听起来可能像是一种反直觉的设计,但如果一个界面让用户删除不小心删除的最后一个保存文件变得不那么方便,那么也许您的保存文件仍然在我们这里。
要求用户双倍确认删除的警告弹出窗口、切换确认按钮的位置以便用户不会快速点击、在点击之间添加计时器以及要求按下并保持都是我们可以使我们的界面在交互可能产生有害后果时对用户稍微不那么方便的方法。
低体力劳动
低体力劳动原则指出,设计应该舒适易用,并应尽量减少疲劳和不适。该原则的一个指导方针是尽量减少重复操作。在视频游戏中,这看起来可能是不可能做到的,因为坦白说,它们往往需要大量的重复操作,但以减少点击和鼠标拖动的方式组织您的界面可以提高您 UI 的质量。
在您的界面上减少体力劳动的一种方法是将类似操作分组在屏幕上。不要让用户在屏幕上来回跳动。当用户在一个视图菜单中完成所有操作会更方便时,不要要求用户跳转到多个菜单。您可以通过分配操作到快捷键或创建快捷方式来减少鼠标使用/拖动。此外,允许用户使用箭头键或控制器按钮而不是仅使用鼠标在菜单中导航,可能对某些用户来说更舒适。
当在 VR 中设计用户界面时,与在传统 2D 屏幕上的游戏相比,与界面交互所需的物理努力会呈指数级增长。将相似的项目分组,并允许用户使用控制器而不是指向来导航菜单,会使与界面交互更加舒适。
震动功能可以是一种为您的界面和游戏玩法提供反馈的极好方式,但它也可能被认为对某些玩家来说不舒服,并且据说与手臂振动综合征有关。如果您包括它,请确保允许用户将其关闭。
在控制台游戏中,一个令人沮丧的交互是输入表单中的信息。如果您让玩家使用屏幕键盘输入长文本字符串,需要导航到每个字母,这会非常繁琐,非常快。考虑允许玩家在手机或电脑上输入这些长字符串,并将数据发送到游戏,而不是在控制台上。
接近和使用的大小和空间
接近和使用的大小和空间原则指出,界面应允许用户无论其移动性、尺寸、姿势或位置如何都能与之交互。这一原则与先前的低物理努力原则紧密相关,即您希望他们不仅能够物理上与您的界面交互,而且还要舒适地这样做。
在确定游戏的键盘布局时,确保玩家不需要以对他们来说不可能或不适的方式伸展他们的手。如果您在控制器上有两个经常一起使用的按钮,您想确保按钮组合是可行的。例如,您不希望他们同时按下 Xbox 控制器上的 X 按钮和 B 按钮。
允许使用多种类型的输入设备是这一原则的关键,因为它将使人们能够使用为特定尺寸和移动性设计的输入设备。例如,在 PC 游戏中,有些人可能会发现使用控制器与键盘交互更容易。
此外,允许调整输入的灵敏度可以帮助那些有不同移动性问题的用户。例如,我手痛和颤抖问题。当我可以调整控制器和鼠标的灵敏度时,这使我以不影响游戏玩法的方式与游戏交互变得更容易,并且可以减少我体验到的疼痛。
在设计 VR 界面时,不要将 UI 项目放置得太高或太远,以至于玩家无法触及。这可能会使体型较小的玩家、坐着玩的玩家或移动性有问题的玩家无法与您的 UI 交互。
在第二章中,我们讨论了拇指区域。这是屏幕上玩家在握住手机时可以轻松触及的位置,并且大多数可交互的用户界面应该放置在这里。我们还希望确保在移动屏幕上按钮尽可能大。你不希望按钮太小且间距太近,以至于大手用户无法与之交互。
现在我们已经回顾了各种通用设计原则,让我们看看我们如何可以为有障碍和残疾的人专门设计。
无障碍设计
记住,通用设计涉及设计适用于所有人的通用用户界面。另一方面,无障碍设计涉及在设计时考虑到特定的障碍和残疾。在本节中,我将讨论几种非常具体的障碍和残疾类型,以及你如何设计你的用户界面,使其对具有这些障碍和残疾的个人可访问。其中一些例子将与我在通用设计部分讨论的例子重叠。
视觉
在设计你的界面时,你应该考虑不同类型的视觉障碍和残疾,包括(但不限于)色盲、低视力和失明。
重要的信息绝不能仅通过颜色传达,你应该始终包括另一种传达信息的方式。例如,假设一个游戏有一个数字,当为负数时是红色,当为正数时是绿色。这个数字也可以有负号和正号来表示符号。如果这个数字代表某种变化,也许当为正数时可以飞上去,当为负数时可以飞下来。这将确保色盲用户仍然能够看到这些重要的信息。
在可能的情况下,你应该避免对色盲用户不可区分的颜色组合;如果不可能,使用其他指示器使项目易于区分。例如,如果你有一个使用颜色来表示可匹配部件的三合一游戏,也可以在部件上包含符号,以便更容易区分。
以下网站提供了关于为色盲用户设计无障碍用户界面的非常有益的信息:
如本章前面所述,你应该确保你的文本和其他 UI 元素具有非常高的对比度。这使色盲和视力低下的人能够更清楚地感知你的 UI。以下网站是一个检查两种颜色之间对比度的优秀资源:webaim.org/resources/contrastchecker/。
虽然前面的网站展示了颜色是否符合网络规定的对比度要求,但这些信息同样适用于视频游戏。
如果你有一些重要信息,这些信息以弹出窗口的形式临时出现在屏幕上,请确保它出现在玩家的视线范围内。对于视野较低的人来说,将其放置在视线之外可能很难看到。更好的做法是允许玩家选择重要信息在屏幕上的弹出位置。
记住,根据无障碍设计的原则,选择非常重要。允许玩家尽可能多地更改视觉元素设置。如果你的游戏使用准星或光标,允许玩家更改准星或光标的样式。允许玩家更改界面或文本的大小。允许玩家更改界面的字体,使其具有更高的字距或间距,或者不那么花哨。
同样重要的是,以非纯视觉的方式指示信息。例如,假设你的 UI 变红以指示玩家健康值低,并且屏幕上有一个红色的健康计。你还可以包括蜂鸣声和控制器振动来指示低健康状态。这将帮助那些视力低下和色盲的人能够感知低健康状态。
如果你设有输入框,你可以使用语音作为输入文本的方式,而不仅仅是视觉键盘。
记住,你还可以使用描述轨道来描述游戏中的所有用户界面元素,从而让视力低下和盲人仍然能够感知屏幕上的内容。
听力和言语
有多种方式可以使你的界面对有听力和言语障碍的残疾人友好。本质上,你不想任何重要信息仅通过声音传达,也不希望你的输入需要语音。此外,如果你的 UI 产生任何类型的噪音,请尽量减少重叠噪音。
在我之前描述的低健康状态示例中,除了蜂鸣声之外,还有多种方式让玩家意识到健康值低。如果你的 UI 包括语音输入,请确保它不是唯一的输入方式,并允许用户通过其他方式输入信息。
虽然这可能看起来不是界面设计,但为所有对话添加字幕需要设计为一个 UI 元素。你的字幕 UI 需要清晰易读。通常,你应该将它们放在某种框中,以便它们与背景容易形成对比。你还想确保它们与游戏中的对话同步。
移动性
在设计你的界面时,你想要确保它们对所有移动能力水平的人都是可访问的。使你的游戏对移动能力水平可访问的最好方法是提供尽可能多的输入设备、输入映射和配置选项。允许玩家自己选择如何与你的界面交互,这可能会确保它符合他们的特定移动需求。
你希望你的控制尽可能简单,并且尽可能少使用按钮。如果你的控制特别复杂,提供简化控制方案的替代方案。
此外,不要要求玩家使用与游戏其他部分不同的输入设备来操作一小部分游戏。例如,如果大部分游戏是通过键盘控制的,那么不要要求玩家使用鼠标与起始屏幕交互。允许玩家使用相同的输入设备来操作游戏的各个方面。
认知和情感
在设计用户界面时,考虑玩家的认知和情感状态以及他们的语言技能是非常重要的。
不要假设玩家能够阅读你创建 UI 所使用的语言,或者他们能够快速阅读。如果你的游戏包含文本,请使用其他上下文元素和图像来传达文本的含义。如果你在 UI 中显示文本然后让其消失,允许玩家选择何时消失,而不是让其以自己的速度消失。
使用你的 UI 提醒玩家重要的游戏元素以及如何与你的界面交互的重要信息。通过在你的 UI 中突出显示控制,提醒玩家控制是什么。使用你的 UI 清楚地指示交互元素和目标。
不要在用户界面(UI)中放置闪烁的图像或重复的图案,因为这可能会触发易患癫痫的人。
虽然这可能更多地与你的游戏玩法而不是用户界面有关,但我想要指出,在游戏中包含特别可能引起不适的内容的警告(例如,如果游戏包含令人不安的内容,如《Doki Doki Literature Club》),可以帮助为玩家准备可能引起痛苦的事情。例如,现在许多游戏在包含令人不安的内容时都会附带内容警告。
其他资源
我本可以就用户界面的通用设计和可访问性设计方面说很多,但遗憾的是,这不是一本关于设计 UI 的书,而是关于 UI 开发的书。我希望这一章至少让你认识到考虑这些元素的重要性,以及它们在用户界面开发初期考虑的重要性。
如果你想要了解更多关于可访问性设计的知识,我强烈建议你查看以下网站提供的信息:gameaccessibilityguidelines.com/full-list/。它提供了一系列关于视频游戏可访问性的最佳实践。此外,它还提供了多个最佳实践示例,你可以作为参考使用。
你还可以访问caniplaythat.com/,这是一个提供游戏行业可访问性信息的网站。
摘要
在本章中,我们讨论了一些设计 UI 的关键考虑因素,以便使其尽可能普遍可感知。我们讨论了通用设计的原则,并回顾了一些你可以实施在设计中的考虑因素,以改善其可访问性。
在下一章中,我们将结束设计讨论,开始探讨如何实现 UI。我们将回顾 Unity 中可用的不同界面系统,这样你就可以开始应用一些设计知识了!
第五章:Unity 中的用户界面和输入系统
既然我们已经讨论了开发用户界面的各种设计考虑因素,我们可以开始讨论如何在 Unity 中实现它们。Unity 提供了各种用于创建 UI 的系统。它有现成的系统,允许你创建将在你的游戏中显示的 UI,或者只会在编辑器中显示的 UI。此外,它还提供了多个系统用于接收玩家的输入。
在撰写本文时,这两个系统仍在积极开发中,并且默认情况下不包含在 Unity 中。本书将主要关注使用完整系统进行开发,但由于 Unity 有意向在某个时候将这些系统作为标准功能,因此我必须讨论它们,即使它们目前仍处于 预览 状态。
在本章中,我将讨论以下主题:
-
识别 UI Toolkit、Unity UI 和 IMGUI
-
在三个 UI 系统之间进行选择
-
识别输入管理器和输入系统
-
在输入管理器和输入系统之间进行选择
让我们先看看 Unity 中的三个 UI 系统。
三个 UI 系统
Unity 有三个系统可以用来构建 UI。你选择哪个将取决于你的 UI 将在哪里显示,你试图实现什么,你是否在处理一个现有项目,以及你对编码的熟悉程度。
你构建的 UI 可以是游戏内或编辑器内。游戏内 UI 是玩家可以访问的 UI。编辑器内 UI 是在 Unity 编辑器中显示并辅助开发的 UI。
如果你想要为你的游戏或应用程序构建 UI,你可以选择 Unity UI(uGUI)系统或 UI Toolkit。如果你想要构建只会在 Unity 编辑器中显示的 UI,你可以使用 UI Toolkit 或 IMGUI。以下维恩图总结了不同 UI 系统的用途:

图 5.1:游戏内和编辑器 UI 的比较
看看 图 5**.1,你可能会有这样的想法:“嗯,UI Toolkit 适用于所有东西!我只需要学习它,就能结束我的 UI 学习之旅!这是一个简单的选择!”不幸的是,事情并不那么简单。让我们更深入地了解不同的系统,以便你可以决定哪个适合你。
Unity UI(或 uGUI)
Unity UI 系统也称为 uGUI,是 Unity 内置的系统,无需任何额外下载。它基于 GameObject 和 Component,并包含多种 UI 元素以供选择。当涉及到为游戏或应用程序开发 UI 时,这是最稳健和最稳定的选项。由于这是唯一一个不在预览模式中且包含在 Unity 中的游戏内 UI 构建系统,因此本文的大部分内容将专注于如何使用此系统开发 UI。
IMGUI
IMGUI,或即时模式 GUI系统,是一个基于代码的 GUI 系统,用于在编辑器中创建界面。其主要功能是帮助程序员进行开发,由于性能问题,不建议用于游戏中的 UI 开发。由于这个系统不是为了在游戏中使用而设计的,并且本书将主要关注游戏中的 UI 开发,因此我不会花费大量时间介绍它,但我会讨论一些其基本功能和用法,在第十九章中。
UI Toolkit
UI Toolkit是一个正在积极开发中的新 UI 系统。它默认不包含在引擎中,必须通过包管理器下载。此外,它是一个预览包,这意味着您必须选择才能在 Unity 提供的可用包列表中看到它。Unity 计划最终用 UI Toolkit 替换 uGUI 和 IMGUI。UI Toolkit 的设计采用了传统的 Web 开发概念,并且与基于 GameObject 的 uGUI 结构完全不同。在第十八章中,我将介绍如何下载 UI Toolkit 包以及如何使用它。
选择 UI 系统
您选择使用哪个系统将取决于几个因素。如前所述,您是为编辑器还是为游戏制作 UI 将决定您选择哪个系统。如果您正在为游戏制作 UI,您可以使用 Unity UI 或 UI Toolkit。如果您正在为编辑器制作 UI,您可以使用 IMGUI 或 UI Toolkit。
UI Toolkit 是一个尚未完全实现的系统,因此如果您正在为已经包含 UI 的现有游戏工作,您可能不会使用 UI Toolkit。它可能也不具备您想要使用的所有功能。由于 UI Toolkit 处于开发中,它可能无法保证稳定性,并且在开发过程中更新它可能会对您的项目产生不利影响。
您对编码的舒适度也可能影响您的决定。一般来说,使用 Unity UI 所需的编码强度低于 IMGUI,并且可能比 UI Toolkit 更熟悉,因为它是基于 GameObject 的。然而,如果您熟悉基于 Web 的 UI 创建,UI Toolkit 可能对您来说非常熟悉。
如果您正在考虑使用 UI Toolkit,我建议在决定使用哪个系统之前,先查看本书中的示例以及以下 Unity 文档页面:docs.unity3d.com/Manual/UI-system-compare.xhtml。
现在我们已经回顾了 Unity 提供的三个 UI 系统,我们可以回顾两个输入系统。
两个输入系统
如第一章中定义,UI代表用户界面,包括用户和游戏之间传达信息的所有机制。在讨论三个 UI 系统时,我们提到了游戏与用户沟通的三个方式——具体来说是通过输出设备(即屏幕)上的 GUI。然而,如果用户想要与游戏沟通,用户将需要某种方式来提供输入。然后游戏需要处理这些输入。
Unity 可以以两种核心方式处理输入。输入管理器或输入系统。正如有各种因素会影响您可能使用的 UI 系统一样,也有各种因素可以帮助您确定使用哪个输入系统。这两个系统都允许您轻松处理多种类型的输入,就像它们是同一种输入一样。例如,每个系统都会让您处理键盘空格键和 Xbox 控制器 A 按钮,就像它们是同一种类型的输入一样。它们是如何做到这一点的将在后面的章节中更详细地讨论,但就目前而言,让我们先看看这两个系统之间的一般区别。
输入管理器
输入管理器是 Unity 默认提供的输入系统,无需下载任何额外的包。无需配置任何设置,您就可以轻松地接受来自键盘、鼠标、游戏手柄或触摸屏等设备的输入。它是通过提供预定义的输入轴来实现的,这些轴本质上指定了关键词和与之绑定的按钮。我们将在第八章中更详细地回顾这一功能。
新输入系统
输入系统(俗称新输入系统)是一个目前正在开发中的包,正如 Unity 在其文档中所述,它旨在比输入管理器更强大、更灵活、更可配置:docs.unity3d.com/Packages/com.unity.inputsystem@1.3/manual/index.xhtml。
如果您一直在使用输入管理器进行项目开发,您可以将项目转换为使用新输入系统的项目。我们将在第二十章中讨论如何实现输入系统。
在输入系统和新输入系统之间进行选择
让我先说,关于选择哪个系统并没有必然的正确答案。理论上,您可以使用任何一个系统来处理任何类型的输入。您选择哪个系统将主要基于个人偏好、您项目输入集的复杂程度以及您是否在为多个平台开发。
如果你没有计划允许玩家重新映射控制(如第四章中所述)或者没有计划进行跨平台开发,那么使用输入管理器可能就足够了,你不需要经历下载输入系统的过程。然而,如果你想要拥有超可配置的控制方案,接受来自各种设备的输入,以及接受复杂的输入动作,你可能会发现使用输入系统来编写处理这些输入的代码比使用输入管理器要容易得多。
由于输入管理器被用于许多正在开发的项目中,如果仅仅因为它不是“新热点”就完全忽略它,那将是对你的一种不公平。此外,新的输入系统仍然处于开发阶段,并且随着每次更新都会发生重大变化。然而,它确实使某些事情的建设变得更加容易,并且在开发者中越来越受欢迎。因此,我不会在这本书中只关注这些系统中的任何一个。
摘要
Unity 提供了多种方式,你可以通过使用三个 UI 系统向用户显示信息。你选择哪种方式取决于你的需求和是否在为游戏或编辑器开发 UI。这本书将主要关注 uGUI,因为它是用于游戏开发中最稳定的 UI 版本,并且由 Unity 提供,无需额外下载。然而,如何使用 IMGUI 开发编辑器 UI 以及如何使用 UI Toolkit 同时使用编辑器 UI 和游戏 UI 将在本书的后续章节中讨论。
Unity 还提供了多种方式来处理用户输入。虽然新的输入系统仍在开发中,并且默认情况下不包含在 Unity 中,但我将确保给你足够的信息,以便你在项目中使用它。
在下一章中,我们将开始使用 uGUI 系统开发 UI,通过探索 UI 画布、面板和布局。
第二部分:Unity UI 基础知识
在本部分,你将学习与Unity UI(uGUI)系统一起工作的基础知识。你将了解如何使用画布和面板及其 Rect Transform 组件来布局 UI。你还将了解如何使用 Unity 的各种自动布局组件来帮助你更轻松地设计 UI。最后,你将了解如何为 uGUI 系统编写代码并为你的 UI 编程交互。
本部分包含以下章节:
-
第六章, 画布、面板和基本布局
-
第七章, 探索自动布局
-
第八章, 事件系统和 UI 编程
第六章:画布、面板和基本布局
如前一章所述,本文的大部分内容将侧重于 Unity UI 系统,即 uGUI。画布是所有使用 Unity UI 制作的 UI 的核心。每个 uGUI 元素都必须包含在一个画布中,才能在场景中进行渲染。它的工作方式类似于艺术家在上面作画的画布,但不同的是,我们在上面布局 UI 元素,而不是在上面作画。因此,我们将从 uGUI 系统提供的各种 UI 元素开始,以画布为起点进行探索。
画布不仅用于包含它们内部的所有 UI 元素,还决定了元素如何渲染以及如何缩放。在早期就关注设置能够在多个分辨率和宽高比下缩放的 UI 非常重要,因为稍后尝试这样做将会带来很多麻烦和额外的工作。因此,我们还将讨论如何确保我们的 UI 能够适当地缩放。
在本章中,我们将讨论以下主题:
-
创建 UI 画布并设置其属性
-
创建 UI 面板并设置其属性
-
使用矩形工具和矩形变换组件
-
正确设置锚点和中心点
-
如何创建和布局基本 HUD
-
如何创建背景图片
-
如何设置基本弹出菜单
技术要求
你可以在此处找到本章的相关代码和资产文件:
github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2006
UI Canvas
你创建的每个 UI 元素都必须是UI Canvas的子元素。要查看在 Unity 中可以创建的所有 UI 元素列表,请从Hierarchy窗口中选择+ | UI,如下截图所示:

图 6.1:Unity UI(uGUI)系统中的可渲染 UI 元素
在前面的截图中所突出的每个 UI 项目都是一个可渲染的 UI 项目,并且必须包含在一个画布中才能进行渲染。如果你尝试将任何这些 UI 元素添加到一个不包含画布的场景中,系统会自动添加一个画布到场景中,并且你尝试创建的项目将成为新添加的画布的子元素。为了演示这一点,尝试向一个空场景添加一个新的UI Text元素。你可以通过选择+ | UI | Text来实现。
这将在 Hierarchy 列表中产生三个新项目:Canvas、Text和EventSystem,其中 Text 是 Canvas 的子元素。

图 6.2:将 UI 文本元素添加到场景后的结果
现在你场景中已经有了画布,任何添加到场景中的新 UI 元素都将自动添加到这个画布中。
注意
如果你尝试将可渲染的 UI 元素从 Canvas 中移除,它将不会绘制到场景中。
你也可以通过选择EventSystem GameObject(场景中尚未存在)来创建一个空的 Canvas,一个将自动为你创建(正如你在前面的截图中所看到的)。我们将在第八章中进一步讨论EventSystem GameObject,但就目前而言,你真正需要知道的是EventSystem允许你与 UI 项目进行交互。
注意
你可以在场景中拥有多个 Canvas,每个都有自己的子组件。
当你创建一个 Canvas 时,它将作为场景中的一个大型矩形出现。它将比代表摄像机视图的矩形大得多:

图 6.3:Unity UI(uGUI)系统中的可渲染 UI 元素
Canvas 比摄像机大,因为 Canvas 组件上有一个缩放模式。默认情况下,缩放模式将 UI 中的每个像素等同于一个 Unity 单位,所以它要大得多。这个大尺寸的一个好结果是,它真的很容易将 UI 项目作为一个相对独立的实体看到,这有助于避免它弄乱你的摄像机视图。
每个新创建的 Canvas 都自动包含四个组件:Rect Transform、Canvas、Canvas Scaler和Graphic Raycaster,如下面的截图所示:

图 6.4:Canvas GameObject 的组件
让我们探索这些组件中的每一个。
Rect Transform 组件
每个 Unity UI GameObject 都包含一个作为其第一个组件的Rect Transform组件。这个组件与非 UI GameObject 上的Transform组件非常相似,因为它允许你在场景中放置对象。
你会注意到,当你第一次将 Canvas 放置在场景中时,你无法调整Rect Transform中的值,并且会看到一个消息,“某些值由 Canvas 驱动”,如前面的截图所示。这个消息意味着你不能控制 Canvas 的位置,因为Canvas组件中选择的属性决定了它们。
当 Canvas 组件的Render Mode设置为Screen Space-Overlay或Screen Space-Camera时,Rect Transform组件值的调整被禁用。在这两种模式下,值由游戏显示的分辨率确定,因为 Canvas 填充了全屏区域。当 Canvas 的Render Mode设置为World Space时,你可以根据需要调整值,因为该组件将确定其在场景中的位置。我们将在本章后面更详细地讨论如何使用Rect Transform组件,但就目前而言,让我们更深入地回顾 Canvas 组件和不同的渲染模式。
Canvas 组件
画布组件允许你从下拉菜单中选择画布渲染模式。有三种渲染模式:屏幕空间叠加、屏幕空间-相机和世界空间。不同的渲染模式决定了场景中的 UI 元素将在哪里绘制以及如何使用矩形变换组件。
在开发你的 UI 时,你应该首先始终适当地根据你的需求设置画布的渲染模式。如果你的场景中有多个画布,它们可以各自有不同的渲染模式。更改渲染模式将更改画布组件上的属性。让我们回顾每种渲染模式的目的以及与之相关的属性。
屏幕空间叠加
屏幕空间叠加是默认的渲染模式。如果你被要求思考一个视频游戏 UI,你很可能想象的是一个在屏幕空间叠加中渲染的 UI。这种渲染模式将画布内的所有 UI 元素叠加在场景中的所有元素之前,就像它是在屏幕上绘制的一样。因此,像抬头显示(HUDs)和出现在屏幕同一平面的弹出窗口这样的 UI 项目将被包含在屏幕空间叠加画布中。
记住,当画布使用屏幕空间叠加渲染模式时,你无法调整画布的矩形变换组件。这是因为画布将根据屏幕的大小(而不是相机)自动调整大小。

图 6.5:屏幕空间 – 堆叠渲染模式属性
当你选择了屏幕空间叠加时,以下属性将变得可用:
-
像素完美:选择此复选框将使画布中渲染的元素与像素对齐。它可以使 UI 元素看起来更清晰,模糊度更低。这可能会引起性能问题,因此只有在绝对必要时才使用它。
-
排序顺序:此选项将确定场景中所有屏幕空间叠加画布渲染的顺序。你可以将其视为堆叠顺序。数字越高,画布内项目对观看场景的人看起来就越靠近。换句话说,排序顺序编号较高的画布将出现在排序顺序编号较低的画布之上。
-
目标显示:如果你正在创建 PC、Mac、Linux 独立游戏,你可以让不同的相机显示在多达八个不同的显示器上。你也可以为每个显示器设置不同的 UI。这就是你将告诉画布它将在哪个显示上渲染的地方。
-
附加着色器通道: 着色器本质上是一种算法,它根据光线和材质描述 GameObject 的颜色。每个画布自动包含以下着色器通道:位置、颜色和 Uv0。然而,这个属性允许你添加额外的通道。着色器是一个相当复杂的话题,所以我们不会在本文本中花费太多时间讨论它们。在屏幕空间-叠加模式下,下拉菜单中并非所有可用的通道都有效,你会在组件中看到一个消息,说明哪些通道不起作用。
接下来,让我们看看屏幕空间相机。
屏幕空间-相机
屏幕空间-相机的表现与屏幕空间-叠加类似,但它将所有 UI 元素渲染成好像它们与相机有一定距离。正如你从下面的截图中所见,如果没有选择渲染相机,这种渲染模式的工作方式与屏幕空间-叠加模式完全相同(如警告信息所示):

图 6.6:屏幕空间 – 相机渲染模式的警告信息
你可以将场景中的任何相机分配到屏幕空间-相机渲染模式中的渲染相机。这是画布将要绘制的相机。一旦你将一个相机添加到渲染相机槽中,警告信息就会消失,并且会提供新的选项,如下面的截图所示:

图 6.7:屏幕空间 – 相机渲染模式属性
当你选择了屏幕空间-相机时,以下属性将变得可用:
-
像素完美: 这与屏幕空间-叠加中的选项相同。
-
渲染相机: 如前所述,这是画布将要绘制的相机。
-
平面距离: 这个属性告诉画布它应该显示在相机多远的位置。
-
排序层: 这个属性允许你选择要使用哪个精灵排序层来显示画布。
-
层内顺序: 这个属性决定了画布在之前选择的精灵排序层中的显示顺序。这个顺序与排序顺序的工作方式类似,数值较高的元素会显示在数值较低的元素之上。
-
附加着色器通道: 这个属性在屏幕空间-叠加中的工作方式相同。
这种渲染模式在你想要画布从与主相机不同的视角渲染时很有用。它还有助于创建一个会与相机一起一致缩放的静态背景,这在 2D 游戏中很有用。由于你可以使用精灵排序层与这种渲染模式一起使用,你可以确保包含背景的画布始终在场景中的所有其他对象之后渲染。
记住,当画布使用屏幕空间-相机渲染模式时,你不能调整画布的矩形变换组件。这是因为画布将根据相机的尺寸(而不是屏幕)自动调整大小。
世界空间
最后的渲染模式是世界空间。此模式允许你将 UI 元素渲染成仿佛它们在物理世界中定位。
在屏幕空间-叠加和屏幕空间-相机中,你不能调整矩形变换组件的属性。具有这两种渲染模式的画布中 UI 元素的定位不转换为世界空间坐标,而是相对于屏幕和相机。然而,当画布处于世界空间渲染模式时,可以调整矩形变换的值,因为 UI 元素的坐标基于场景中的实际位置。这些画布不需要像其他两种画布类型那样面对指定的相机。

图 6.8:渲染模式 – 世界空间属性
此模式请求一个事件相机,而不是像屏幕空间-相机模式请求的渲染相机。事件相机与渲染相机不同。由于此画布位于世界空间,它将与主相机一起渲染,就像场景中存在的所有其他对象一样。事件相机是接收来自 EventSystem 事件的相机。因此,如果画布上的项目需要交互,你必须包含一个事件相机。如果玩家不会与画布上的项目交互,你可以将其留空。
你需要指定一个事件相机的原因是光线投射。当用户点击或触摸屏幕时,从点击(或触摸)点向场景无限延伸一条射线(单向线)。射线的方向由相机的朝向决定。大多数情况下,你会将其设置为主相机,因为玩家会期望事件发生在这个方向上。
当你选择世界空间时,以下属性变为可用:
-
事件相机:如前所述,分配给此槽位的相机决定了哪个相机将接收画布的事件
-
排序层:此属性与屏幕空间-相机相同
-
层顺序:此属性与屏幕空间-相机相同
-
附加着色器通道:此属性与屏幕空间-叠加中的工作方式相同
接下来,让我们看看画布 标量组件。
画布标量组件
画布标量组件决定了画布内项目如何缩放。它还决定了 UI 画布内项目的像素密度。
在第一章中,我们讨论了如何在单个分辨率或单个纵横比下构建您的游戏。然而,大多数时候,您不会有选择游戏分辨率或纵横比的自由。请注意,我只提到了为 PC、Mac 和 Linux 独立构建以及 WebGL 构建指定纵横比和分辨率。当您构建将在手持屏幕或电视屏幕上播放的内容时,您无法保证屏幕的大小。
由于您无法保证游戏的分辨率或纵横比,因此您的 UI 需要调整以适应各种分辨率和缩放,这一点非常重要;这就是为什么存在这个画布标量组件的原因。
画布标量组件有四个UI缩放模式:
-
固定 像素大小
-
根据屏幕大小缩放
-
固定 物理大小
-
世界
当画布渲染模式设置为屏幕空间-叠加或屏幕空间-相机时,前三个UI 缩放模式可用。当画布渲染模式设置为世界空间时,自动分配第四个UI 缩放模式(设置后不能更改)。
固定像素大小
当画布的UI 缩放模式设置为固定像素大小时,UI 中的每个项目都将保持其原始像素大小,无论屏幕大小如何。您会注意到这是默认设置,因此默认情况下 UI 不会缩放;您必须通过更改屏幕分辨率来启用设置才能使其缩放。

图 6.9:固定像素大小 UI 缩放模式属性
当您将UI 缩放模式更改为固定像素大小时,您将在检查器中看到以下属性:
-
2,UI 中的所有内容将加倍。如果您将此数字设置为0.5,所有项目的大小将减半。 -
100,这意味着两个相隔一个游戏单位的对象将相距 100 像素。换句话说,如果两个对象在相同的y-坐标上,但一个对象的x-坐标为1,另一个对象的x-坐标为2,它们正好相距 100 像素。这个属性在所有其他模式中都以相同的方式工作,因此我将在下一节中不讨论它。
根据屏幕大小缩放
当您将画布标量组件设置为根据屏幕大小缩放时,画布上的 UI 元素将根据参考分辨率进行缩放。如果屏幕比参考分辨率值大或小,画布上的项目将相应地放大或缩小。在第一章中,我告诉您应该决定一个代表您 UI 设计理想分辨率的默认分辨率。这个默认分辨率将是参考分辨率。

图 6.10:根据屏幕尺寸缩放的 UI 缩放模式属性
如果你的屏幕纵横比与参考分辨率值匹配,那么事物将无问题地缩放和缩小。如果不匹配,那么你需要使用画布缩放器组件来定义如果纵横比发生变化,项目将如何缩放。
这可以通过使用屏幕匹配模式设置来实现。以下列出了三种不同的屏幕匹配模式,它们决定了如果游戏的纵横比与参考分辨率的纵横比不匹配时,画布将如何缩放:
-
匹配宽度和高度: 这将根据参考高度或参考宽度缩放 UI。它也可以根据两者的组合进行缩放。
-
扩展: 如果屏幕尺寸小于参考分辨率,画布将被扩展以匹配参考分辨率。
-
缩小: 如果屏幕尺寸大于参考分辨率,画布将被缩小以匹配参考分辨率。
0 (1 (高度).

图 6.11:根据屏幕尺寸缩放的 UI 缩放模式属性
当0的值为时,画布缩放器将强制画布始终具有由参考分辨率指定的相同宽度。这将保持画布宽度方向上对象的相对缩放和位置。因此,对象在水平方向上不会彼此远离或靠近。然而,它将完全忽略高度。因此,对象可以在垂直方向上彼此远离或靠近。
设置1将完成相同的事情,但将保持对象沿高度而不是宽度的位置和缩放。
设置0.5将比较游戏宽度和高度与参考分辨率,并且它将尝试保持水平和垂直方向上对象之间的距离。
0和1。如果数字接近1,缩放将优先考虑高度,如果接近0,则优先考虑宽度。
这些1)。如果你想保持相对水平位置不变,请使用0)。这实际上完全取决于你最关心的哪种间距。
我建议根据你游戏的朝向使用以下设置:
| 方向 | 匹配值 |
|---|---|
| 肖像 | 0 (宽度) |
| 横幅 | 1 (高度) |
| 变化 | 0.5 (宽度和高度) |
表 6.1:方向和匹配值
我选择这些设置是基于参考分辨率中两个数字中较小的一个。在肖像模式下,宽度将是最小的,因此我认为保持项目在宽度上的相对位置很重要。这是一个个人偏好,仅作为一个建议,并且不一定适用于所有游戏。然而,我发现这对于大多数游戏来说是一个很好的经验法则。
最好避免制作在肖像模式和横幅模式之间变化的游戏,除非你有最小的 UI 或者非常熟悉创建可缩放 UI。
常数物理大小
当画布的UI 缩放模式设置为常数物理大小时,UI 中的每个项目都将保持其原始的物理大小,无论屏幕的大小如何。物理大小是指用户如果拿出尺子并在屏幕上测量它时将看到的尺寸。与常数像素大小类似,具有此UI 缩放模式设置的画布上的项目将不会缩放。
如果你有一个希望始终具有特定宽度和高度的 UI 项目,你可以在具有此UI 缩放模式的画布上放置它。例如,如果你希望一个按钮始终宽 2 英寸、高 1 英寸,你会使用此模式。这在移动游戏中尤其有用,可以确保按钮的大小足够大,以便人类手指根据我们在第四章中讨论的建议进行操作。

图 6.12:使用常数物理大小 UI 缩放模式属性进行缩放
当你将UI 缩放模式更改为常数物理大小时,你将在检查器中看到以下属性:
-
物理单位:计量单位。您可以选择厘米、毫米、英寸、点和皮卡。
-
回退屏幕 DPI:如果 DPI 未知,则假定此 DPI。
-
默认精灵 DPI:所有具有与参考每单位像素值相等的每单位像素的精灵的 DPI。
世界
世界是唯一适用于设置为世界空间的画布的UI 缩放模式。从以下截图可以看出,模式不能更改:

图 6.13:使用世界 UI 缩放模式属性进行缩放
当你将UI 缩放模式更改为世界时,你将在检查器中看到以下属性:
- 动态每单位像素:这是所有动态 UI 项目(如文本)的每单位像素设置。
图形射线组件
图形射线投射器组件允许您使用事件系统检查画布上的对象是否被用户输入击中。正如在查看世界空间画布渲染模式时讨论的那样,当用户触摸屏幕时,从玩家触摸的屏幕上的点向前发射一条射线。图形射线投射器检查这些射线,看它们是否击中了画布上的某个对象。

图 6.14:图形射线投射器组件
您可以在图形 射线投射器组件上调整以下属性:
-
忽略反转图形:如果 UI 元素背对玩家,选中此选项将阻止击中事件注册。如果没有选中,则击中事件将注册在背面的 UI 对象上。
-
遮挡对象:此设置指定了哪些类型的物品在其前方会阻止它被击中。因此,如果您选择二维,则此画布前方任何二维对象都会阻止这些对象被交互。然而,3D 对象不会阻止交互。可能的选项如下所示:

图 6.15:图形射线投射器遮挡对象选项
- 遮挡掩码:在此属性上选择项目的工作方式类似于遮挡对象属性。这允许您根据它们的渲染层选择项目,因此您可以更具体一些。可能的选项如下所示:

图 6.16:图形射线投射器遮挡掩码选项
我们将在第八章中更详细地讨论射线投射和事件系统。
画布渲染器组件
画布渲染器组件不在画布 GameObject 上,而是在所有可渲染 UI 对象上。

图 6.17:画布渲染器组件
为了使 UI 元素能够渲染,它必须在其上有一个画布渲染器组件。您通过+ | UI菜单创建的所有可渲染 UI 元素都将自动附加此组件。如果您尝试移除此组件,您将看到类似于以下警告的信息:

图 6.18:尝试移除画布渲染器组件时的警告信息
在前面的屏幕截图中,我尝试从一个文本 UI 对象中移除画布渲染器组件。如您所见,它不允许我移除画布渲染器组件,因为文本组件依赖于它。如果我返回并移除文本组件,然后我就能移除画布渲染器组件。
画布渲染器组件上唯一的属性是裁剪透明网格切换。 “裁剪”意味着不绘制。因此,此属性表示渲染器将不会绘制任何顶点颜色 alpha 值为 0 或接近 0 的几何形状。
UI 面板
UI 面板的主要功能是包含其他 UI 元素。您可以通过选择+ | UI | 面板来创建面板。需要注意的是,没有面板组件。面板实际上只是具有矩形变换、画布渲染器和图像组件的 GameObject。因此,实际上,UI 面板只是一个具有一些预定义属性的 UI 图像。

图 6.19:面板 GameObject 上的组件
默认情况下,面板以背景图像(只是一个灰色圆角矩形)作为源图像,具有中等不透明度。您可以替换源图像为另一个图像或完全删除图像。
当您试图确保项目按比例缩放并相对于彼此适当地定位时,面板非常有用。包含在同一个面板中的项目将相对于面板缩放,并在过程中保持彼此的相对位置。
我们很快将更详细地查看图像组件,但现在我们正在查看一个允许我们编辑其矩形变换组件的对象,让我们来探索这个组件。
矩形变换
每个 UI 元素都有一个矩形变换组件。矩形变换组件与变换组件非常相似,用于确定其附加对象的位置。
矩形工具
可以使用任何变换工具来操纵 UI 对象。然而,矩形工具允许您通过操纵包含对象的矩形来缩放、移动和旋转任何对象。虽然这个工具可以用于 3D 对象,但它对 2D 和 UI 对象最有用。

图 6.20:矩形工具
-
要使用矩形工具移动对象,选择对象然后点击并拖动在矩形内。
-
要调整对象的大小,将光标悬停在对象的边缘或角落。当光标变为箭头时,点击并拖动以调整对象的大小。在拖动时按住shift键可以均匀缩放。
-
要旋转对象,将光标悬停在对象的角落——稍微在矩形外面,直到光标在角落显示一个旋转的圆圈。然后可以通过点击和拖动来旋转。
定位模式
当使用矩形工具时,选择正确的定位模式非常重要。您可以选择居中或锚点以及全局或本地。模式可以通过点击按钮切换:

图 6.21:各种定位模式
-
当处于中心模式时,对象将根据其中心点移动并围绕其中心点旋转。
-
当处于旋转模式时,对象将围绕其旋转中心点旋转,而不是中心点。您还可以在此模式下通过悬停在旋转中心点上并点击拖动来改变旋转中心点的位置。
-
当处于全局模式时,矩形变换的边界框将是一个非旋转的框,包围整个对象。
-
当处于本地模式时,矩形变换的边界框将是一个旋转的框,紧密地适合对象。
以下插图显示了全局和本地模式下面板的矩形变换的边界框。空心的蓝色圆圈代表对象的旋转中心点:

图 6.22:全局模式与本地模式
接下来,让我们看看矩形 变换组件。
矩形变换组件
如前所述,UI 元素没有标准的变换组件;它们有矩形变换组件。如果您将其与标准变换组件进行比较,您会发现它有相当多的属性:

图 6.23:变换与矩形变换
您可以使用它来改变位置、旋转和缩放,就像使用1一样。
您可能已经注意到,前一个插图中的位置和尺寸值的标签与UI 面板属性部分提供的标签不同。这是因为表示位置和尺寸的标签会根据选择的锚点预设而改变。我们将稍后讨论如何使用这些锚点预设,但让我们看看位置和尺寸值可以持有的不同标签示例。

图 6.24:矩形变换属性的变化
如果矩形变换的锚点预设设置为不包含拉伸,就像前一个屏幕截图中的左上角示例一样,位置值将由Pos X、Pos Y、Pos Z确定,尺寸由宽度和高度确定。
如果矩形变换的锚点预设设置为包含拉伸,就像其他三个示例一样,垂直于拉伸的位置和与拉伸平行的尺寸将用左、右、上和下标记。这些值代表从父级矩形变换边界的偏移量。
对象的锚点决定了所有相对位置测量的起点。旋转中心点决定了缩放和旋转修改发生的起点。它将围绕这个点旋转并朝向这个点缩放。我们将在“锚点和旋转中心点”部分更详细地探讨锚点和旋转中心点。
矩形变换编辑模式
在 矩形变换 组件中,你可以使用两种不同的编辑模式——蓝图模式和原始编辑模式,分别由以下图标表示:

图 6.25:矩形变换编辑模式
蓝图模式将忽略对其应用的任何本地旋转或缩放,并将矩形变换边界框显示为非旋转、非缩放的框。以下截图显示了在关闭和开启蓝图模式时旋转和缩放的 Panel 的边界框:

图 6.26:开启与关闭蓝图模式
原始编辑模式将允许你更改 UI 对象的锚点和枢轴点,而不会根据你所做的更改移动或缩放对象。
锚点和枢轴点
每个 UI 对象都有锚点手柄和枢轴点。当它们一起使用时,将有助于确保你的 UI 位置适当,并在游戏分辨率或纵横比发生变化时适当缩放。
锚点手柄以 X 形状的四个三角形表示,如下所示:

图 6.27:锚点手柄和枢轴点
锚点可以组合在一起形成一个单独的锚点,如图中所示,或者它们可以被分割成多个锚点,如下所示:

图 6.28:分割锚点手柄
锚点将始终形成一个矩形。因此,边总是对齐的。
在 x 值中的 0 将手柄移动到最左边,而 1 将手柄移动到最右边。你可以从以下屏幕截图看到调整 x 值如何相对于父元素移动锚点:

图 6.29:调整锚点最小和最大值
矩形变换具有锚点预设和最小和最大锚点属性。锚点代表 UI 元素连接到其父矩形变换的点:

图 6.30:访问锚点预设
由于画布没有父元素,你会看到锚点预设区域是空的。这无论选择哪种画布渲染模式都是正确的:

图 6.31:画布游戏对象上缺少锚点预设
点击锚点预设框将显示所有可能的 锚点预设 列表,如下所示截图:

图 6.32:所有可用的锚点预设
如果你点击其中一个预设,它将移动锚点到截图显示的位置。你也可以使用锚点预设调整位置和轴心点。
如果按住 Shift 和/或 Alt.,表示预设的图像将改变。按住 Shift 将显示由蓝色点表示的轴心点的位置,按住 Alt 将显示位置如何变化,同时按住两者将显示轴心点和位置变化。

图 6.33:设置轴心和位置
注意
如果使用 Mac,由于没有 Alt 键,你将使用 Option 键代替。然而,编辑器中的说明仍然会提到 Alt,并且这对于 Mac 版本没有变化。之前的截图是在 Mac 上拍摄的,尽管它没有 Alt 键。
现在,让我们看看 Canvas 组 组件。
Canvas 组组件
你可以将 Canvas 组 组件添加到任何 UI 对象中。将其附加到 UI 对象将允许你使用单个组件调整对象的特定属性以及其所有子对象的属性,而不是必须为每个 UI 元素调整这些属性。
你可以通过选择 UI 对象的检查器中的 添加组件 | 布局 | Canvas 组(你也可以直接搜索 Canvas 组)来将 Canvas 组组件添加到任何 UI 对象中。

图 6.34:Canvas 组组件
你可以使用 Canvas 组 组件调整以下属性:
-
0和1代表不透明度的百分比;0是完全透明的,而1是完全不透明的。 -
交互性:此设置确定组内的对象是否可以接受输入。
-
阻挡射线投射:此设置确定组内的对象是否会阻挡射线投射到其后面的物体。
-
忽略父组:如果此 Canvas 组 组件位于具有 Canvas 组组件的另一个 UI 元素的子 UI 元素上,此属性确定此 Canvas 组是否会覆盖上面的一个。如果选中,它将覆盖父级的 Canvas 组属性。
介绍 UI 文本和图像
在不使用文本或图像的情况下创建任何 UI 示例都有点困难。因此,在我们介绍布局示例之前,让我们首先看看 UI 文本和 UI 图像 GameObject 的基本属性。UI 文本和 UI 图像在 第十一章 和 第十二章 中有更详细的讨论。
当你使用 + | UI | 文本 创建一个新的文本对象时,你会看到它有一个 文本 组件。

图 6.35:文本组件
你可以通过更改文本框中的文字来更改显示的文本。在第十一章中,我们将更详细地查看文本组件的各个属性,但到目前为止,大多数属性的功能应该是相当明显的。
当你使用+ | UI | 图像创建一个新的 Image 对象时,你会看到它有一个图像组件。

图 6.36:图像组件
记住,面板本质上是一个图像,但有一些预填充的属性。然而,当你创建一个图像时,没有预填充的属性。
在本章中,我们将使用源图像属性,该属性允许你更改显示的精灵。我们将在第十一章中查看其他属性。
示例
现在让我们来看一些示例!我们将创建一个基本抬头显示(HUD)的布局以及一个随屏幕拉伸并在多个分辨率下缩放的背景图像。
在我们开始构建 UI 之前,让我们设置我们的项目并引入我们将需要的艺术资产。
我们将首先设置我们的项目:
- 创建一个新的 Unity 项目,并将其命名为
Mastering Unity UI Project。以 2D 模式创建它。
注意
我们选择 2D 模式,因为它将使导入我们的 UI 精灵变得容易得多。在 2D 模式下,所有图像都导入为精灵(2D 和 UI)图像,而不是纹理图像,就像在 3D 模式中那样。你可以通过导航到编辑 | 项目设置 | 编辑器并将模式更改为3D来随时切换到 3D 模式。
-
在
Assets文件夹内创建两个新的文件夹,分别命名为Scripts和Sprites。 -
创建一个新的场景,并将其命名为
Chapter6.unity;确保将其保存在Scenes文件夹中。你也可以将SampleScene.unity重命名为Chapter6.unity。我们将使用我在以下网站找到的免费艺术资产修改的艺术资产:
在文本源文件的
Chapter2/Sprites文件夹中,找到catSprites.png、pinkBackground.png和uiElements.png图像,并将它们拖动到Sprites文件夹中导入到你的项目中。

图 6.37:导入精灵
-
现在,我们需要将精灵图分成单个精灵。如果你已经知道如何分割精灵图,请现在对
catSprites图像和uiElements图像进行分割,然后继续到布局基本 HUD部分。如果你不熟悉这个过程,请按照以下步骤操作,然后继续到第 5 步。 -
选择
catSprites图像,按住Ctrl,然后点击uiElements图像,以便两者都被选中。

图 6.38:选择两个精灵表
-
现在,在
catSprites和uiElements精灵中,将它们视为精灵表。![图 6.39:将精灵转换为多精灵模式]()
图 6.39:将精灵转换为多精灵模式
注意,检查器显示2 Texture 2Ds 导入设置,因为我们选择了两个图像。
-
现在选择
catSprites图像,并使用导入 设置面板中的按钮打开精灵编辑器。 -
在精灵编辑器打开的情况下,选择切片。
-
现在更改切片属性,使切片类型为自动,并将精灵支点应用到底部。完成后,点击切片。
-
你现在应该看到精灵被分割成三个独立的区域。点击应用以保存更改。
-
现在,如果你在项目文件夹视图中点击
catSprites图像上的箭头,你应该能看到单个图像:

图 6.40:分割精灵表
- 完成 8 到 12 步的
uiElements图像,但将支点设置为中心。
现在我们已经设置了项目和精灵,我们可以开始 UI 示例。
布局基本 HUD
我们将制作一个看起来像以下的 HUD:

图 6.41:我们将开发的 HUD
这将在接下来的章节中进一步说明,但到目前为止,它将有一个相当简单的布局,重点关注父子关系和锚点/支点位置。
要创建前面图像中显示的 HUD,请完成以下步骤:
-
使用+ | UI | Canvas创建一个新的 Canvas。
-
在
Canvas HUD Canvas中。 -
最好设置所有
1024x768。如果你看pinkBackground图像(我们将在下一个示例中应用),它的分辨率为2048x1536;1024x768与背景图像具有相同的宽高比。因此,将你的Canvas 缩放器组件设置为以下设置:![图 6.42:Canvas 缩放器属性]()
图 6.42:Canvas 缩放器属性
我们将
1设置为保持垂直方向上的比例。如果你还记得根据屏幕大小缩放部分,我发现这对于大多数使用横向分辨率制作的游戏来说效果最好。 -
将你的游戏视图设置为
1024x768,这样你将看到所有内容都适当缩放(有关添加自己的游戏视图分辨率的说明,请参阅第一章中的更改游戏视图的宽高比和分辨率部分)。

图 6.43:1024 x 768 游戏视图分辨率
-
由于我们只有一个画布,当我们向场景添加任何新的 UI 元素时,它们将自动成为我们的
HUD Canvas的子元素。使用HUDPanel创建一个新的面板。你会看到它是一个HUD Canvas的子元素。这个面板将代表包含所有 HUD 元素的矩形。 -
点击锚点预设图标以打开 锚点预设。在按住 Shift + Alt 的同时选择左上角的锚点预设。

图 6.44:设置 HUD 面板的锚点预设
- 通过选择其右侧的箭头来展开
uiElements图像。定位uiElements_1子图像:

图 6.45:设置 HUD 面板的锚点预设
- 将
uiElements_1拖动到HUD 面板:

图 6.46:已分配 uiElements_1 的 Image 组件
- 目前,面板非常淡,横跨整个屏幕。让我们通过增加不透明度使其更容易看到。点击
Alpha滑块中的白色矩形,将其完全滑到最右边,或者在 alpha 值槽中输入值255:

图 6.47:在颜色选择器上调整 alpha 值至全透明
-
点击 Image 组件旁边 Preserve Aspect 设置旁边的复选框。此属性将使图像始终保持原始图像的纵横比,即使你将图像的宽度和高度设置为不具有相同纵横比的大小。
面板现在将仅占用场景的顶部部分:

图 6.48:保持纵横比后的面板
- 从
HUD 面板,你会注意到1024和768,分别。你也可以从 场景 视图中更容易地看到矩形变换超出了精灵的可视区域。因此,对象的实际大小远大于其看起来的大小。

图 6.49:矩形变换超出可见图像区域
- 让我们调整面板的矩形变换大小,使其与我们要找的大小相匹配,并更好地贴合可视图像。更改
300和102。在垂直方向上,矩形变换可能不会完全贴合,但会非常接近。

图 6.50:调整 HUD 面板的矩形变换大小
-
我们现在已经设置了主要面板。由于所有其他图像都将包含在
HUD 面板中,我们希望将它们设置为HUD面板的子项。这样,当屏幕缩放时,其他图像将保持在“内部”HUD面板中,并保持相对于HUD面板的大小。让我们从包含猫角色头部的图像开始。在
HUD 面板上右键单击并选择HUD 面板。将其重命名为角色容器。 -
将
uiElement_6精灵放入源图像槽中,并选择保持纵横比。 -
由于
角色容器图像是HUD面板的子项,我们设置的任何锚定都将相对于HUD面板。在按住 Shift + Alt 的同时选择左拉伸锚定预设。此外,设置位置和尺寸变量,如下所示:

图 6.51:调整角色容器的矩形变换
-
让我们添加猫头部的图像。我们希望它完全填充由
角色容器图像表示的槽。因此,我们将使其成为角色容器图像的子项。在角色容器上右键单击并选择角色图像。 -
现在将
catSprites_0子图像添加到图像组件的源图像中,并选择保持纵横比。 -
在按住 Shift + Alt 的同时将锚定预设设置为拉伸-拉伸:
![图 6.52:调整角色图像的矩形变换]()
图 6.52:调整角色图像的矩形变换
由于我们确保
角色容器图像紧密围绕容器图像,因此它应该使猫头完美地适应容器图像,而无需调整任何设置!![图 6.53:角色图像在角色容器内适配]()
图 6.53:角色图像在角色容器内适配
-
现在,我们已经准备好开始制作生命值条。我们将以创建
角色容器和角色相同的方式创建它。在HUD 面板上右键单击并选择生命值容器。 -
将
uiElement_20精灵放入源图像槽中,并选择保持纵横比。 -
设置矩形变换属性,如下面的图像所示,并确保在选择锚定预设时按住 Shift + Alt:

图 6.54:生命值容器的属性
-
现在,我们只剩下生命值条了!就像我们将猫头图像设置为
角色容器的子项一样,我们还需要将生命值条的图像设置为生命值容器的子项。在生命值容器上右键单击并选择生命值条。 -
将
uiElement_23图像放入源图像槽中。这次,我们不会选择保持纵横比,因为我们希望图像水平缩放此图像。 -
将矩形变换属性设置为以下截图所示,并在选择锚点预设时确保按住Shift + Alt:
![图 6.55:健康持有者的属性]()
图 6.55:健康持有者的属性
注意,增加了一些填充,以便你可以看到
Health Holder的边缘。 -
在继续之前,确保你的矩形变换定位模式设置为中心点。否则,你将无法在下一步中移动中心点。

图 6.56:矩形变换定位模式
- 我们几乎完成了!目前,图像的中心点正好位于中心。这意味着如果我们尝试缩放它,它将向中心缩放。然而,我们希望它能够向左缩放。因此,打开锚点预设,并且只按住Shift,选择中左。这将只移动中心点。

图 6.57:移动中心点
- 现在,当我们调整矩形变换中的X 轴缩放值时,生命值条将向左缩放:

图 6.58:调整生命值条的比例
我们的 HUD 示例就到这里!尝试更改你的游戏视图的纵横比到不同的设置,这样你可以看到面板适当地缩放,并看到所有对象相对位置保持不变。
如果你更改游戏的纵横比时,你的 HUD 出现了一些奇怪的问题,确保你的对象具有正确的父子关系。你的父子关系应该是这样的:

图 6.59:层级结构的父子关系
此外,检查以确保锚点和中心点设置正确。
放置 2D 游戏背景图像
只要使用适当的画布属性,放置一个随屏幕缩放的背景图像并不太难。我们将扩展我们的 HUD 示例,并在场景中放置一个背景图像。

图 6.60:背景图像的结果
我们需要确保这个背景图像不仅显示在其他 UI 元素之后,还显示在我们场景中可能放置的任何游戏对象之后。
要创建一个显示在所有 UI 元素和所有游戏元素之后的背景图像,请完成以下步骤:
-
使用+ | UI | 画布创建一个新的画布。我喜欢使用不同的画布来整理我的不同 UI 元素,但在这个案例中,需要一个新的画布不仅仅是因为个人偏好。我们需要一个新的画布,因为我们需要一个具有不同渲染模式的画布。这个画布将使用屏幕空间-相机渲染模式。
-
在画布
背景画布中。 -
将
主摄像机切换到渲染 摄像机槽:

图 6.61:背景画布的 Canvas 组件
- 为了确保这个 Canvas 出现在所有其他 UI 元素和游戏中的所有 2D 精灵之后,我们需要使用排序层。在 Unity 编辑器的右上角,你会看到一个标签为层的下拉菜单。选择它并选择编辑层:

图 6.62:编辑层
-
展开
背景。图 6.63:添加背景排序层图 6.63:添加背景排序层
排序层的工作原理是,列表顶部的任何内容都将渲染在场景中最后。所以,如果你想添加一个前景层,你需要在
背景层之下添加它,这样新创建的精灵就会在背景层之后。如果你创建了新层,确保背景层保持在列表的顶部。 -
重新选择
背景画布,现在将排序层更改为背景:

图 6.64:设置背景排序层
-
现在,我们只需要添加背景图像。在
背景画布上右键单击并选择背景图像。 -
将
pinkBackground精灵放入源图像槽中。这次,我们不会选择保留宽高比,因为我们希望图像能够在游戏屏幕调整大小时能够挤压和拉伸,并且始终充满场景。 -
将矩形变换属性设置为所示,并确保在选择锚点预设时按住Shift + Alt:

图 6.65:背景图像设置
因为背景画布设置为屏幕空间 - 摄像机,你可能需要更改视图以便可以看到它。它将在主摄像机视图的位置。
就这样!尝试调整游戏视图的宽高比,并在自由宽高比模式下调整屏幕大小,以便你可以看到背景图像总是充满屏幕。此外,尝试向场景中添加一些非 UI 2D 精灵,看看它们是如何在背景之上渲染的。
这个例子中不理想的一点是,背景图像被允许改变其宽高比。你会看到由于这个原因,在某些宽高比下图像看起来相当糟糕。这个背景图像不适合在多个宽高比上发布的游戏。我选择这个图像有两个原因:
-
你可以看到选择一个不高度依赖宽高比的图片是多么重要。
-
这是免费的!
我强烈建议如果你使用这种方法创建背景图像,使用一个图案不那么明显显示扭曲的图像。
设置基本弹出菜单
本章我们将要讨论的最后一个例子将使用画布组组件。我们直到开始编程在第第八章中才能真正看到这个组件的作用,但现在我们可以打下基础。我们也将通过这个例子获得更多关于布局 UI 的实践。

图 6.66:我们将布局的弹出面板
要创建前面图像中显示的弹出菜单,请完成以下步骤:
-
使用+ | UI | 画布创建一个新的画布。
-
在
弹出画布中。 -
我想为
HUD 画布使用相同的属性。而不是再次设置所有这些,我将使用快捷键并复制HUD 画布。要做到这一点,请选择HUD 画布右上角的三个点(“串烧”菜单)并选择复制组件。 -
现在请选择
弹出画布右上角的“串烧”菜单并选择粘贴 组件值。 -
我们将添加一个面板来存放所有项目,类似于我们处理 HUD 的方式。这将确保所有项目都保持在一起。在
弹出画布上右键单击并选择暂停面板。 -
将
uiElement_32图像放入源图像槽中,赋予它全透明度,并选择保持纵横比。 -
设置矩形变换属性,如以下截图所示,并确保在选择锚点预设时按住Shift + Alt:

图 6.67:暂停面板的属性
-
现在,让我们给面板顶部添加一个漂亮的横幅。在
弹出面板上右键单击并选择暂停横幅。 -
将
uiElement_27图像放入源图像槽中并选择保持纵横比。 -
设置矩形变换属性,如以下截图所示,并确保在选择锚点预设时按住Shift + Alt:
![图 6.68:暂停横幅的属性]()
图 6.68:暂停横幅的属性
我们将在后面的章节中添加文本到这个横幅。
-
本例的主要目的是演示
暂停面板的使用。选择
暂停面板,然后选择添加组件 | 布局 | 画布组(你也可以直接搜索画布组)。
到此为止!更改暂停面板和暂停横幅的透明度值。这对于想要隐藏和显示弹出菜单而不必为每个项目单独编程非常有用。一旦我们花更多时间与暂停面板打交道,它上面将会有更多项目,我们也会很高兴我们不必为每个部分单独编程。
概述
哇!这一章内容丰富!有很多内容需要涵盖,因为这一章为本书剩余部分奠定了基础。我们讨论了画布的概念以及如何在场景中正确放置它。此外,我们还讨论了基本的 UI 面板,以便我们探索在场景中定位 UI 元素的概念。正确设置画布及其标量是开发 UI 的重要第一步。
下一章将介绍如何创建不同的自动布局,这将使我们能够以网格的形式排列我们的用户界面。
第七章:探索自动布局
现在我们已经掌握了使用 Rect Transform 和锚点手动定位、缩放和对齐 UI 元素的基础,我们可以探索如何使用自动布局。自动布局允许你将 UI 元素分组,以便它们相对于彼此自动定位。
在许多场景中,你可能希望 Unity 自动控制你的 UI 对象的布局。如果你通过代码生成 UI 项目,并且项目数量可能会变化,但你仍然希望它们正确对齐、缩放和定位,你可以使用自动布局。此外,如果你想创建完美间隔的 UI 对象,自动布局将帮助你创建这种完美间隔,而无需自己进行任何位置计算。这些自动布局非常适合像网格或列表对齐的库存系统等。
在本章中,我们将讨论以下主题:
-
使用布局组组件自动间隔、定位和对齐一组 UI 对象
-
使用布局元素组件、内容大小适配器组件和纵横比适配器组件来调整 UI 元素的大小
-
如何设置水平 HUD 选择菜单
-
如何设置网格库存
注意
本节中展示的所有示例都可以在名为Chapter 07.unitypackage 的 Unity 包中找到,在代码包中。每个示例图像都有一个说明示例编号的场景。在场景中,每个示例都在自己的 Canvas 上,其中一些 Canvas 已被禁用。要查看已禁用的 Canvas 上的示例,只需在检查器中选中 Canvas 名称旁边的复选框即可。

图 7.1:启用或禁用 Canvas 示例的复选框
让我们探索不同类型的自动布局组。
技术要求
你可以在此处找到本章的相关代码和资产文件:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2007
自动布局组的类型
当一个 UI 对象附加了自动布局组组件时,其所有子对象都将根据布局组件的参数进行对齐、调整大小和定位。自动布局组有三个选项:水平布局组、垂直布局组和网格布局组。
以下截图显示了三个面板(用灰色矩形表示),每个面板有六个 UI 图像子对象(用黑色矩形表示);第一个面板有一个水平布局组组件,第二个面板有一个垂直布局组组件,第三个面板有一个网格布局组组件:

图 7.2:第七章场景中的自动布局组示例 1
从前面的截图,你可以清楚地看到三种自动布局组能完成什么。你可以使用这三种类型的任意组合来创建嵌套、完美间距的布局,如下所示:

图 7.3:第七章场景中的自动布局组示例 2
让我们分别查看这些布局组,并探索它们的各个属性。
水平布局组
带有水平布局组组件的 UI 对象的全部子元素都将自动并排放置。如果你允许水平布局组调整子元素的大小,它们将被定位和缩放,以确保它们完全位于父对象矩形变换的范围内。然而,如果你希望它们超出父对象矩形变换的范围,可以调整填充属性。
子元素在层次结构中出现的顺序决定了水平布局组将按照什么顺序进行布局。子元素将从左到右进行布局。层次结构中最顶层的子元素将被放置在最左侧的位置,而层次结构中最底层的子元素将被放置在最右侧的位置:

图 7.4:第七章场景中的水平布局组示例 1
要将水平布局组组件添加到 UI 对象中,请从对象的检查器中选择添加组件 | 布局 | 水平布局组。如果你点击填充属性旁边的箭头,你应该看到以下内容:

图 7.5:水平布局组组件
让我们进一步探讨水平布局组组件的每个属性。
填充
填充属性表示父对象矩形变换边缘的填充。正数将使子对象向内移动,而负数将使子对象向外移动。

图 7.6:第七章场景中的水平布局组示例 2
例如,前面的截图显示了三个应用了不同填充值的面板。第一个面板没有填充,第二个面板在所有四边都有正填充,而第三个面板在左侧、右侧和底部有正填充,但在顶部有负填充。
间距
间距属性确定子对象之间的水平间距。如果你在未使用控制子元素大小属性的情况下使用子元素强制扩展属性,则可能会覆盖此间距,子元素可能会有更大的间距。
子元素对齐
Child Alignment属性决定了子元素组将如何对齐。此属性有九个选项,如下所示:

图 7.7:水平布局组的子元素对齐选项
例如,以下图显示了三个重叠的 Panel,它们填充了屏幕。这些父 Panel 的 Rect Transform 区域由选定的 Rect Transform 表示。第一个 Panel 具有Upper Left子元素对齐。其子元素由白色方块表示。第二个 Panel 具有Middle Center子元素对齐,其子元素由灰色方块表示。第三个 Panel 具有Lower Right子元素对齐,其子元素由黑色方块表示:

图 7.8:第七章场景中的水平布局组示例 3
需要注意的是,Child Alignment属性只有在子元素(包括间距)没有完全填满 Rect Transform 时才会显示效果,如图所示。
反向排列
Reverse Arrangement属性是一个切换按钮。选择此切换按钮将导致元素以与它们在层次结构中出现的顺序相反的顺序排列。
控制子元素大小
Control Child Size选项允许自动布局覆盖子对象的当前宽度或高度。如果您选择这些复选框而没有选择相应的Child Force Expand复选框,您的子对象将不再可见(除非子元素具有具有首选****宽度指定的 Layout Element 组件)。
如果您没有设置此属性,子元素可能会绘制在父元素的 Rect Transform 之外——也就是说,如果存在太多的子元素。
注意
此属性更改子对象 Rect Transforms 的宽度和高度属性。因此,如果您选择然后取消选择它,子元素将不会回到它们原来的大小。您必须使用编辑 | 撤销 (Ctrl + Z)或通过它们的 Rect Transform 组件手动重置子元素的大小。
由于此属性依赖于Child Force Expand属性,因此下一节将展示Control Child Size属性的示例。
子元素强制扩展
Child Force Expand属性将导致子元素填充可用空间。如果未选择相应的Control Child Size,则此属性将移动子元素,使它们及其间距填充空间。这可能会覆盖Spacing属性。如果选择了相应的Control Child Size,它将在所选方向上拉伸子元素,使它们及其间距完全填充空间。这将保持Spacing属性。
在以下屏幕截图中,所有三个面板都有一个水平布局组组件,具有中间左 子级对齐,并选择了不同的子级控制大小和子级强制扩展组合。顶部面板只选择了子级强制扩展宽度,中间面板选择了控制子级大小宽度和子级强制扩展宽度,最后一个面板选择了控制子级大小属性和子级强制扩展属性:

图 7.9:第七章场景中水平布局组示例 4
接下来,让我们看看使用子级 缩放属性。
使用子级缩放
使用子级缩放属性仅在 Unity 的最新版本中可用。勾选此属性将告诉布局组是否在自动化布局时考虑子级的缩放。
垂直布局组
垂直布局组组件与水平布局组非常相似,具有所有相同的属性,但具有垂直布局组组件的 UI 对象的子级将自动堆叠在一起,而不是并排排列。
与水平布局组一样,子级在层次结构中出现的顺序决定了垂直布局组将按照什么顺序进行布局。子级将按照它们在层次结构中出现的顺序从上到下进行布局:

图 7.10:第七章场景中垂直布局组示例
要将垂直布局组组件添加到 UI 对象中,请从对象的检查器中选择添加组件 | 布局 | 垂直布局组。如果您单击填充属性旁边的箭头,您应该看到以下内容:

图 7.11:垂直布局组组件
由于垂直布局组组件的属性与水平布局组相同,我们不会进一步探讨每个属性的细节。有关每个属性的说明,请参阅水平布局 组部分。
网格布局组
网格布局组组件允许您在(你猜对了)网格布局中按列和行组织子对象。它的工作方式与水平和垂直布局组类似,但有一些可以操作的额外属性。
要将网格布局组组件添加到 UI 对象中,请从对象的检查器中选择添加组件 | 布局 | 网格布局组。如果您单击填充属性旁边的箭头,您应该看到以下内容:

图 7.12:网格布局组组件
网格布局组的某些属性与其他两个布局组相同,但让我们更仔细地看看网格布局组组件特有的属性。
单元大小
与水平和垂直布局组不同,它们通过其矩形变换组件确定子元素的大小,或者通过缩放使其适合父级的矩形变换,网格布局组要求您指定子对象的高度和宽度。您通过设置单元大小属性的X和Y属性来完成此操作。这将自动将指定的X和Y大小应用到每个子元素的矩形变换的宽度和高度属性。
由于单元大小属性和缺少控制子元素大小属性,子元素不一定能适合父级的矩形变换。如果存在太多的子元素,它们可能会被绘制在父级矩形变换之外。因此,如果您有一个动态填充的网格,在游戏过程中可能会改变,并且希望网格始终适合特定区域,您将必须为该溢出场景做好准备。
网格布局组允许您指定X 间距和Y 间距。X 间距是水平间距,Y 间距是垂直间距。这些值不会被进一步的属性选择覆盖,就像在水平和垂直布局组中那样。
起始角和起始轴
起始角属性决定了层次结构中第一个子元素的位置。起始角属性有四个选择,如下所示:

图 7.13:网格布局组组件的起始角选项
起始轴属性决定了所有其他子元素相对于第一个子元素的位置。有两个选项,如下所示:

图 7.14:网格布局组组件的起始轴选项
将起始轴属性设置为水平意味着子元素将从第一个子元素开始,以水平方式排列。如果将起始角分配给左选项之一,子元素将从左到右排列。如果将起始角分配给右选项之一,子元素将从右到左排列。一旦新行被填满,它将继续到下一行,并将在与起始角相同的侧重新开始。如果起始角是上选项之一,行将继续向下。如果起始角是下选项之一,行将继续向上。
以下截图演示了基于不同的起始****角选项的子对象流动,基于水平起始轴:

图 7.15:第七章场景中的网格布局组示例 1
将起始轴属性设置为垂直意味着子对象将从第一个子对象开始布局,然后以垂直方式排列。子对象是自上而下还是自下而上放置,与将此属性设置为水平时的方式相同,取决于起始角的位置。然后,当一列被填满时,子对象将根据起始角的位置从左到右或从右到左放置。
以下截图演示了基于不同的起始****角选项的子对象流动,基于垂直起始轴:

图 7.16:第七章场景中的网格布局组示例 2
如您所见,起始角和起始轴选项可以大大改变您的子对象显示的顺序。
约束
约束属性允许您指定网格将有多少行或列。这里有三种选项,如下所示:

图 7.17:网格布局组组件的约束选项
固定列数和固定行数属性允许您分别指定列数或行数。如果您选择这两个选项中的任何一个,将出现一个新的属性,约束计数。然后您指定您想要的列数或行数。当您选择固定列数时,行数将是可变的。当您选择固定行数时,列数将是可变的。
灵活选项会根据您选择的单元格大小和起始轴选项自动计算行数和列数。它将开始以定义的模式布局子对象,直到所选轴上没有空间为止。然后它将继续。起始轴中指定的轴将有固定数量的子对象,而另一个轴将是可变的。例如,如果起始轴设置为水平,并且三个子对象可以在定义的空间内水平放置,则将有三列,行数将由总子对象的数量决定。
现在我们已经探讨了三个自动布局组,让我们看看一个组件,它将允许我们改变这些布局组内子对象的大小或位置。
布局元素
布局元素组件允许我们指定对象在自动调整大小时的尺寸值范围。如果父对象尝试超出这些偏好设置调整其大小,布局元素将覆盖来自父对象的所有尺寸信息。
要将布局元素组件添加到 UI 对象,请从对象的检查器中选择添加组件 | 布局 | 布局元素。布局元素具有以下属性:

图 7.18:布局元素组件
要使用这些属性,您首先选择它们的复选框以启用它们;将出现框供您输入所需值:

图 7.19:设置布局元素组件的属性
让我们回顾一下布局元素组件的各个属性将如何影响它们所添加的元素。
忽略布局
忽略布局属性可用于使子对象忽略其父对象的任何自动布局组件。具有此属性选中的子对象可以自由移动和调整大小,而其他所有子对象都将不考虑被忽略的子对象进行布局。
在以下示例中,面板具有水平布局组组件和五个子对象。第一个子对象,标记为 1,具有具有忽略布局属性选中的布局元素组件:

图 7.20:第七章场景中的布局元素示例 1
您可以看到,由于第一个子对象选择了忽略布局属性,它可以被移动到父面板之外,并且在确定其他子对象的位置和缩放时被忽略。它还保持了其原始的矩形变换缩放。
如果取消选择忽略布局属性,第一个子对象将与其他子对象一起添加到水平布局组中。
宽度和高度属性
布局元素组件有三组属性,可用于指定对象如何调整大小。如果分配的大小超出了提供的值,这些属性将覆盖父对象分配给子对象的大小。
注意
重要的是要注意,这些属性不会覆盖网格布局组组件的单元格大小设置。它们对网格布局组内的子对象没有任何影响。
最小宽度和高度
最小宽度和最小高度属性是子对象可以达到的最小宽度和高度。如果父对象缩小,子对象将缩小,直到达到其最小宽度或最小高度。一旦达到,它将不再在该方向上缩放。
在下面的图中,面板包含一个水平布局组组件和五个子对象。第一个子对象,标记为 1,有一个设置了最小宽度和最小高度属性的布局元素组件:

图 7.21:第七章场景中的布局元素示例 2
您可以看到,当父对象的自定义布局组缩放时,它试图将所有子对象与其一起缩放。其他四个子对象也进行了缩放,但由于第一个子对象设置了最小宽度和最小高度属性,它拒绝进一步缩放。
优先宽度和优先高度
优先宽度和优先高度属性有些令人困惑,因为它们的性能取决于您为父布局组设置的配置。尽管存在最小宽度和最小高度设置,但没有官方的最大宽度和最大高度设置。然而,优先宽度和优先高度属性可以用来指定子对象将达到的最大尺寸,但前提是在父布局组上选择了正确的设置。
下面的图中包含三个带有垂直布局组组件的面板和不同的设置。它们的子元素在布局 元素组件内也有不同的优先高度设置:

图 7.22:第七章场景中的布局元素示例 3
第一个父面板有一个带有控制子大小宽度和高度选择的垂直布局组组件,以及子强制扩展的宽度和高度。它没有任何子对象在布局元素组件内有优先宽度或优先高度设置。第一个面板将在比较其他面板时作为默认参考。
第二个父面板具有与第一个相同的属性——在布局 元素组件中有一个100。
您可以看到,由于第二个父面板比其他四个子对象高100单位,所以当在父上选择子强制扩展属性时,具有优先高度的子对象不会使用优先高度作为其最大可能高度;它将此值添加到由父布局组组件分配的高度。
第三个面板在布局 元素组件中包含一个100。
如果您将第三面板的子组件与第一面板(默认)的子组件进行比较,您会发现子组件更短。这是因为它们的首选高度设置比垂直布局组组件尝试分配给它们的高度小。因此,当取消选中子强制扩展的高度属性时,子组件将按预期使用它们的首选高度设置,使其成为子组件应达到的最大尺寸。
因此,如果您希望首选宽度或首选高度设置作为可达到的最大宽度和高度,您需要在父对象上取消选中相应的子强制扩展属性。
弹性宽度和弹性高度
弹性宽度和弹性高度属性表示一个百分比,其中百分比是子组件相对于其他子组件的大小。由于这些值是百分比,因此值为 0 表示 0%,值为 1 表示 100%。
与首选宽度和首选高度类似,除非取消选中子强制扩展属性,否则此设置不会按预期工作。在以下示例中,两个面板及其子组件的设置几乎完全相同。两者之间的唯一区别是,顶部父面板选中了子强制扩展的宽度属性,而底部父面板则没有。因此,如果父组件上选中了子强制扩展的宽度,则子组件的弹性宽度设置将被忽略:

图 7.23:第七章场景中的布局元素示例 4
前一图第二行的子组件具有以下0、0.5、0.75、1和1.5。您可以看到,子组件是相对于彼此按百分比缩放的。第一个子组件不可见,因为它设置了0。
布局元素组件基本上允许我们覆盖元素的自动大小和位置。现在,让我们回顾一些组件,这些组件将允许我们自动调整 UI 元素的大小。
适配器
有两个适配器布局组件。这些组件使它们附加的对象的矩形变换适应指定的区域。
内容大小适配器
内容大小适配器组件允许您强制父组件的大小适应其子组件的大小。这种适配可以基于子组件的最小或首选大小。
要将内容大小适配器组件添加到 UI 对象中,请从对象的检查器中选择添加组件 | 布局 | 内容大小适配器。内容大小适配器组件具有以下属性:

图 7.24:内容大小适配器组件
您可以为水平适配和垂直适配选择以下属性:

图 7.25:内容大小适配器组件的可能适配选项
如果选择无约束属性,则内容大小适配器将不会调整对象沿该轴的大小。
如果选择最小大小属性,则内容大小适配器将根据子组件的最小大小调整对象的大小。此最小大小由子组件的布局元素组件的最小宽度和最小高度属性确定。
如果父组件具有用于此属性的网格布局组组件,则子组件不需要有布局元素组件。如果为此具有网格布局组组件的对象选择此属性,则父组件的矩形变换将根据单元格大小和填充属性来适应子组件,如下所示:

图 7.26:第七章场景中的内容大小适配器示例
如果选择首选大小属性,则内容大小适配器将根据子组件的首选大小调整对象的大小。此首选大小由子组件的布局元素组件的首选宽度和首选高度属性确定。如果对象具有网格布局组组件,此设置将按与最小大小完全相同的方式执行。
纵横比适配器
纵横比适配器组件与布局元素组件类似工作,因为它允许您覆盖发送给它的尺寸约束。它将强制附加到其上的 UI 对象根据纵横比进行缩放。
要将纵横比适配器组件添加到 UI 对象中,请从对象的检查器中选择添加组件 | 布局 | 纵横比适配器(脚本)。纵横比适配器组件具有以下属性:

图 7.27:纵横比适配器组件
一旦选择了一个纵横比模式选项,纵横比属性将是可编辑的。纵横比属性定义了矩形变换将保持的纵横比。例如,如果您想要 4:3 的纵横比,您只需在框中输入 4/3,它将转换为小数值:

图 7.28:将分数输入到纵横比适配器组件中
您可以为纵横比模式属性选择以下属性:

图 7.29:宽高比适配器组件的宽高比模式选项
如果选择了无属性,宽高比适配器将不会调整尺寸以适应宽高比。
如果选择了宽度控制高度属性,宽高比适配器将根据对象的宽度调整高度的尺寸。
如果选择了高度控制宽度属性,宽高比适配器将根据对象的高度调整宽度的尺寸。
如果选择了适应父对象属性,宽高比适配器将调整对象的尺寸以适应其父对象,但将保持宽高比。这将使子对象保持在父对象的边界内。
如果选择了包裹父对象属性,宽高比适配器将调整对象的尺寸以覆盖其父对象,但将保持宽高比。这与适应父对象属性类似,但不同之处在于,它不仅可以保持在父对象的边界内,还可以超出边界。
如果您尝试将一个宽高比适配器组件添加到一个父组件包含布局组组件的子组件中,您将在子组件上看到以下消息:

图 7.30:宽高比适配器警告消息
虽然您可以忽略此消息并继续操作,但它并不完全按预期工作。推荐的解决方案是在组内的子组件的子组件中添加宽高比适配器组件。例如,在以下图中,一个面板被添加为水平布局组的子组件。然后,一个带有宽高比适配器组件的子组件被添加到面板中,以便子组件可以具有 4:3 的宽高比:

图 7.31:第七章场景中的宽高比适配器示例
现在我们已经查看了一些自动布局组件的所有属性,让我们看看如何使用它们的示例!
示例
我们将继续工作于在第六章中创建的场景,并使用为它们导入的艺术资产。
注意
如果您没有跟随第六章中的示例,但想跟随这些示例,您可以从代码包中下载名为第七章 - 示例 - 开始.unitypackage的 Unity 包。
除了已经添加到我们项目中的艺术作品外,我们还将使用我从opengameart.org/content/platformer-pickups-pack找到的免费艺术资产中修改的艺术资产。
从上一个链接下载提供了许多单独的图像。我本可以使用那些图像,但出于性能原因,尽可能使用精灵表是更好的选择。因此,你可以在代码包中找到标记为 foodSpriteSheet.png 的精灵表。为了将所有图像组合成精灵表,我使用了 Texture Packer 程序,该程序可在 www.codeandweb.com/texturepacker 找到。
在开始以下示例之前,请完成以下步骤:
-
将
foodSpriteSheet.png精灵表导入到项目中的Asset/Sprites文件夹。 -
将
foodSpriteSheet.png改为 Multiple。使用 Sprite Editor 自动切片精灵表。 -
自动切片会在精灵表中创建一个空白图像。在 Sprite Editor 中找到以下屏幕截图所示的矩形,然后选择并删除它:

图 7.32:需要删除的空精灵
- 应用你的更改后,你应该在你的
Sprites文件夹中有以下内容:

图 7.33:项目中当前所有的精灵
- 通过按 Ctrl + D 复制名为
Chapter6的场景,并将其命名为Chapter7。打开Chapter7场景并在其中完成以下示例。
现在场景已复制且艺术资源已导入,让我们来看看应用一些自动布局。
布局 HUD 选择菜单
本章我们将介绍的是屏幕右下角使用 Horizontal Layout Group 组件的 HUD 选择菜单。完成之后,它将看起来像以下图示:

图 7.34:本例中将构建的 HUD 选择菜单
要创建前一个屏幕截图所示的 HUD 组,请完成以下步骤:
-
目前,屏幕左上角有一个名为
HUD Panel的面板。为了清晰起见,将此面板重命名为Top Left Panel。 -
我们将创建一个新的 HUD Panel 来存放我们的水果库存。我们希望将新的 HUD Panel 放在 HUD Canvas 上。在 Hierarchy 中右键单击名为
HUD Canvas的 Canvas,然后选择 UI | Panel。 -
将新面板重命名为
Bottom Right Panel:

图 7.35:层次结构中的面板
-
修改
Bottom Right Panel的 Rect Transform 属性,使面板锚定在右下角,具有500的宽度和100的高度。记得在选择右下角锚定预设时按住 Shift + Alt:![图 7.36:Bottom Right Panel 的 Rect Transform]()
图 7.36:Bottom Right Panel 的 Rect Transform
你应该在游戏视图中看到以下内容:
![图 7.37:结果面板]()
图 7.37:结果面板
-
现在,我们将用
uiElements.png精灵中的一个替换图片。将uiElements_1拖动到Image组件的Source Image属性中。更改Color属性,使其具有全不透明度:![图 7.38:面板的 Image 组件属性]()
图 7.38:面板的 Image 组件属性
你现在应该在你的Game 视图中看到以下内容:
![图 7.39:结果面板]()
图 7.39:结果面板
-
为了创建我们想要的布局,我们需要添加一个
Bottom Right Panel。选择添加组件 | 布局 | 水平布局组。我们将稍后调整其属性。首先,让我们给这个面板添加一些子元素,这样我们就可以看到属性的效果。 -
在层次结构中右键点击
Bottom Right Panel并选择Item Holder。我们不会更改这个 Image 的Rect Transform组件,因为我们允许其父元素的水平布局组控制其大小、位置和锚点。 -
这张图片将是物品的背景持有者。因此,将
uiElement_6拖动到其Image组件的Source Image中。 -
现在,让我们为水果添加图片。在
Food中的Item Holder上右键点击:

图 7.40:UI 元素的层次结构
-
为了确保我们看到的不是一个白色方块,让我们替换
foodSpriteSheet.png。我使用了foodSpriteSheet_18,这是一个完整的橙子:![图 7.41:食物元素的 Image 组件]()
图 7.41:食物元素的 Image 组件
你应该看到类似这样的东西:
![图 7.42:包含橙子的结果面板]()
图 7.42:包含橙子的结果面板
如果你的橙子和它的持有者不在我的位置,不要担心。当我们开始添加更多子元素并调整水平组布局时,一切都应该正确地弹出。
-
我们不希望我们的橙子图片的宽高比被扭曲,同时我们也想确保它总是填充
Item Holder图片,而不会超出它。因此,让我们在5上调整一些属性。 -
现在,选择
FoodImage 应该具有以下属性:

图 7.43:食物 UI 图片的 Rect Transform 和 Image 组件
-
现在,我们准备开始添加更多的子元素。在
Item HolderGameObjects 中的Item上选择Item HolderImage:![图 7.44:UI 元素的层次结构]()
图 7.44:UI 元素的层次结构
你应该在Game 视图中看到以下内容:
![图 7.45:包含五个橙子的结果面板]()
图 7.45:包含五个橙子的最终面板
-
我不喜欢我的对象名称中包含带数字括号的名称,所以我将所有重复的图像重命名为
Item Holder,不带数字。选择Item Holder (1),按住Shift,然后选择Item Holder (4),以便选中所有这些。现在,在名称槽中的Item Holder处按下Enter。它们现在都应该被重命名为ItemHolder:

图 7.46:重命名元素的结果层次结构
-
现在,让我们调整
Bottom Right Panel上的属性。为此,选择Bottom Right Panel,并在其水平布局组组件中,给它以下属性:![图 7.47:Bottom Right Panel 的水平布局组组件]()
图 7.47:Bottom Right Panel 的水平布局组组件
你现在将能够看到以下内容:
![图 7.48:最终面板中的橙子]()
图 7.48:最终面板中的橙子
通过调整
Item Holder图像,通过启用控制子大小宽度和高度,使它们适合父面板。 -
现在,剩下的只是将橙子图像替换为其他四种物品。选择第二到第四个
Item HolderGameObjects 的Food图像,并将它们的foodSpriteSheet_13、foodSpriteSheet_22、foodSpriteSheet_34和foodSpriteSheet_45更改为以下结果:

图 7.49:各种水果的最终面板
如此例所示,水平布局组组件和(类似地)垂直布局组组件设置起来并不困难,并且对于创建有组织的列表非常有用。
布局网格库存
本章我们将讨论的最后一个示例是使用网格布局组组件和内容适配器组件创建网格库存系统。我们将在后续章节继续在此面板上工作:

图 7.50:我们将在此示例中构建的网格库存
要创建前面截图所示的网格库存系统,请完成以下步骤:
- 包含此库存系统的外壳看起来与我们之前的
Pause Panel(见图 7**.49)非常相似。由于它们如此相似,而且没有必要重新发明轮子,我们将复制在第六章中创建的Pause Panel,并调整其一些设置以获得正方形形状。在层次结构中选择Pause Panel,然后按Ctrl + D复制它。现在,重命名副本Inventory Panel。重命名其子图像Inventory Banner:

图 7.51:复制和重命名后的结果层次结构
注意
记住我们在第六章中向暂停面板添加了一个 Canvas Group 组件。通过复制它来创建库存面板,库存面板也有一个Canvas Group组件。此组件将使我们能够轻松地隐藏和显示两个面板,我们将在下一章中这样做。
-
要获得示例截图中的
Inventory Panel的方形外观,我们需要取消选择500:![图 7.52:库存面板的矩形变换和图像组件]()
图 7.52:库存面板的矩形变换和图像组件
你现在应该在游戏视图中看到以下内容:
![图 7.53:生成的库存面板]()
图 7.53:生成的库存面板
-
如果你查看图 7**.50,你会看到库存项目组有一个轮廓的精灵。这将作为我们网格的父对象。通过在
Inventory Holder上右键单击来创建此父对象:

图 7.54:项目层次结构
-
更改
uiElement_38并使用颜色属性使图像完全不透明。 -
目前,
Inventory Holder完全覆盖了Inventory Panel。然而,我们不需要通过选择Inventory Holder没有子项来更改任何Inventory Holder,现在调整水平适配和垂直适配设置将导致它“消失”。 -
通过选择添加组件 | 布局 | 网格布局组来添加一个
Inventory Holder。再次提醒,现在不要调整设置。我们将在添加子项后进行此操作。 -
从图 7**.50中注意,库存的子项设置与我们在上一个示例中创建的水平 HUD 中的子项设置一样。因此,我们将复制
Bottom Right Panel的子项并将副本移动到Inventory Holder的子项。选择Bottom Right Panel的第一个Item Holder子项,按住Shift并选择Bottom Right Panel的最后一个Item Holder子项。这将选择所有子项。现在,所有子项都被选中,按Ctrl + D来复制它们。 -
将
Bottom Right Panel中的复制Item HolderGameObject 拖放到Inventory Holder中,使它们成为Inventory Holder的子项:

图 7.55:项目层次结构
-
选择一个
Item HolderGameObject 并复制四次,以便总共有九个Item Holder子项。选择所有Item Holder子项并将它们重命名为Item Holder,以便它们的名字中不再有数字:![图 7.56:项目层次结构]()
图 7.56:项目层次结构
你现在应该在游戏视图中看到类似于 图 7.57 的内容。根据你复制的顺序或你执行操作的顺序,水果的顺序可能略有不同。不过,这没关系!
![图 7.57:水果的网格]()
图 7.57:水果的网格
-
现在,让我们调整
Inventory Holder Panel上的属性,以便子项将以 3x3 网格排列。调整属性以匹配以下截图中的属性:

图 7.58:库存持有者的网格布局组组件
-
你现在应该在游戏视图中看到以下内容:
![图 7.59:库存持有者的网格布局组组件]()
图 7.59:库存持有者的网格布局组组件
我们使用
3在每个单元格之间添加间距。 -
现在,由于
Inventory Holder有子项,我们可以更改其 Content Size Fitter 的设置。将 Horizontal Fit 和 Vertical Fit 设置为 Min Size:![图 7.60:库存持有者的内容大小适配器组件]()
图 7.60:库存持有者的内容大小适配器组件
你现在应该在游戏视图中看到以下内容:
![图 7.61:水果的适配网格]()
图 7.61:水果的适配网格
这有点难以看清,但
Inventory Holder的图像现在紧紧地围绕Item Holder图像的网格。不过,我们想要一点填充。 -
通过调整 Grid Layout Group 中的 Padding 属性,为
Item HolderGameObject 的两侧添加填充,如图所示:

图 7.62:水果的填充网格
-
现在一切都已排列并正确定位,剩下要做的就是更改图像的顺序并更改最后四个槽位的图像。要更改图像的顺序,只需在
Food项目中将它们的顺序更改为foodSpriteSheet_41、foodSpriteSheet_52、foodSpriteSheet_55和foodSpriteSheet_53。这些更改导致以下完成的库存面板:![图 7.63:各种食物的网格]()
图 7.63:各种食物的网格
就这样。你现在应该有一个完美排列的库存网格。
随着
Inventory Holder的自动调整大小以适应所有项目,正如你所看到的:![图 7.64:各种食物的小网格]()
图 7.64:各种食物的小网格
实际上,这种方法效果非常好,直到我们尝试向库存中添加更多项目。你会发现一旦我们有 10 个项目,一切看起来都很糟糕:
![图 7.65:各种食物的扩展网格]()
图 7.65:各种食物的扩展网格
我们可以做一些事情来处理这个问题,包括更改单元格大小和与滚动矩形一起使用遮罩。我们将在后面的章节中讨论如何进行这些更改。不过,现在请将你的库存保持在九个项目,这样一切看起来都很漂亮。
完成前两章的所有示例后,你应该具备以下能力:
![图 7.66:第六章和第七章所有示例的结果]()
图 7.66:第六章和第七章所有示例的结果
这就是为我们的场景添加自动布局的原因。我们将在未来的章节中继续对其进行改进。
摘要
现在,我们已经掌握了各种布局 UI 元素的技术。本章和上一章所涵盖的信息,已经提供了足够多的工具来创建几乎任何你可以想象到的 UI 布局。
本章讨论的自动布局不仅在你想要手动添加 UI 项目时有用,就像我们在本章中所做的那样。如果你想要根据特定条件动态创建和添加 UI 项目,这些自动布局尤其有用。
在下一章中,我们将学习如何通过代码访问 UI 组件,以及如何使用事件系统允许玩家与 UI 对象进行交互。
第八章:事件系统和 UI 编程
Unity UI 系统的关键特性之一是能够通过事件轻松编程 UI 元素如何接收玩家的交互。事件系统是一个强大的系统,允许你创建和管理事件。
一旦你学会了如何利用事件系统,你将能够创建可交互的 UI 以及响应游戏事件的事件 UI。
在本章中,我们将讨论以下主题:
-
如何通过代码访问 UI 元素及其属性
-
事件系统是什么以及如何与之协同工作
-
如何使用输入管理器自定义输入轴
-
输入模块是什么,以及 Unity 提供了哪些输入模块
-
如何使用事件触发组件接收 UI 对象上的事件
-
射线投射器是什么以及 Unity 提供了哪些类型的射线投射器
-
如何使用键盘输入显示和隐藏弹出面板
-
如何暂停游戏
-
如何创建拖放库存系统
-
如何使用鼠标或多点触控输入平移和缩放相机
技术要求
你可以在这里找到本章的相关代码和资产文件:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2008
在代码中访问 UI 元素
所有 UI 元素都可以像其他 GameObject 一样在代码中进行访问和操作。要访问代码中的 UI 元素,你必须包含UnityEngine.UI命名空间和正确的变量类型。让我们看看UnityEngine.UI命名空间。
UnityEngine.UI命名空间
using关键字。
默认情况下,所有新的 C#脚本都包含了System.Collections、System.Collections.Generic和UnityEngine命名空间。要通过代码访问 UI 元素的属性,你必须首先使用UnityEngine.UI命名空间。
因此,在你的 C#脚本顶部,你需要包含以下行来表示你想要使用UnityEngine.UI命名空间:
using UnityEngine.UI;
在不使用命名空间的情况下,与 UI 元素相关的任何变量类型在你的代码编辑器中都会被标记为红色,并且你会收到一个编译错误。一旦你包含了命名空间,变量类型将变为蓝色文本,表示这是一个可用的变量类型,编译错误将消失。
UI 变量类型
每个变量类型都是UnityEngine.UI命名空间中的一个类。因此,这些变量类型中的每一个都有自己的变量和函数集,可以访问。我们将在未来的章节中更详细地讨论每个变量类型,但就目前而言,让我们先看看在代码中访问 UI 元素属性的标准化模板。
你可以在源文件中找到一个名为Chapter 08``.unitypackage的 Unity 包。导入它将引入一个名为Chapter8.unity的场景和多个代码文件。导入包中的项目并打开场景。在Chapter8场景中,你会看到一个名为UI Variables Example的 UI 图像。它没有分配任何精灵,显示为一个白色方块。以下名为AddSprite.cs的脚本附加到 UI 图像上:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class AddSprite : MonoBehaviour {
Image theImage;
public Sprite theSprite;
void Awake(){
theImage = GetComponent<Image>();
}
void Start () {
theImage.sprite = theSprite;
theImage.preserveAspect = true;
}
}
上一段代码中突出显示了 UI 特定的代码。请注意,UnityEngine.UI命名空间包含在类的顶部。
在类中定义了两个公共变量:theImage,它是一个Image类型,和theSprite,它是一个Sprite类型。theImage变量引用场景中的 UI 图像,而theSprite变量引用将成为 UI 图像源图像的精灵。
Image变量类型位于UnityEngine.UI命名空间中,表示 UI 图像 GameObject。Sprite变量类型不是一个 UI 元素,它包含在UnityEngine命名空间中。
在Start()函数中,通过在变量名后输入一个点然后输入属性来引用theImage上的Image组件的属性。你可以以这种方式访问 UI 元素对应组件中出现的任何属性。你也可以以这种方式访问组件中未列出的属性。
附加到UI Variables Example (Image)的AddSprite脚本在检查器中显示,如下面的截图所示:

图 8.1:AddSprite 脚本及其属性
现在,当场景播放时,精灵将从空白白色方块变为香蕉图像,并保持其宽高比。
让我们探索事件系统,这将使我们能够与我们的 UI 进行交互。
事件系统
在第六章中,我们了解到当第一个 Canvas 被添加到场景中时,会自动在层次结构中添加一个名为EventSystem的 GameObject。事件系统允许你轻松接收玩家交互并将这些交互通过事件发送到场景中的对象。请注意,我说的是场景中的对象,而不是UI 对象。事件系统还可以将事件发送到非 UI 元素!
在我们继续之前,我想指出我使用EventSystem(一个单词)和 Event System(两个单词),因为我将在两者之间来回切换。我想让你知道我这样做是有意为之,并不是随机决定有时候我讨厌空格键。
我将使用EventSystem(一个单词)来引用场景层次结构中实际出现的 GameObject,并使用 Event System(两个单词)来引用处理事件的系统。
除了向对象发送事件之外,事件系统还为你做了很多事情。它还跟踪当前选中的 GameObject、输入模块和射线投射。
EventSystem GameObject 默认初始化时包含三个组件:Transform、Event System 管理器和 Standalone Input Module,如下面的截图所示:

图 8.2:EventSystem GameObject 及其组件
由于 EventSystem 是一个 GameObject,它实际上存在于场景中(尽管它没有可渲染的组件使其可见),因此它具有与其他所有 GameObject 一样的 Transform 组件。你现在应该熟悉 Transform 组件了,所以我们不会进一步讨论它。然而,其他两个组件确实值得进一步讨论。现在让我们更仔细地看看 Event System 组件。我们还会在本章的 Input Modules 部分讨论 Standalone Input Module 组件。
你的场景中不能有超过一个 EventSystem GameObject。如果你尝试通过 + | UI | Event System 在场景中添加一个新的,则不会添加新的,而是会为你选择场景中当前存在的那个。
注意
如果你设法在你的场景中添加第二个 EventSystem(例如通过使用 Ctrl + D 复制现有的一个),你将在你的 Console 上看到一个警告信息。
如果你的场景中有多于一个 EventSystem GameObject,只有第一个添加的会实际执行任何操作。任何额外的 EventSystems 都将无法使用。
让我们接下来看看 Event System 管理器。
Event System 管理器
Event System 管理器是负责跟踪和管理各种 Event System 元素的组件。
如果你想要与 EventSystem GameObject 一起工作,它不会自动为你创建。你可以通过在对象的检查器中选择 Add Component | Event | Event System 来将 Event System 管理器添加到 GameObject。让我们来谈谈 Event System 管理器下的属性。
首次选中
当你启动游戏时,你知道 Start Game 按钮会为你高亮显示,这样按下 Enter 就可以开始游戏,而无需使用鼠标?这就是 First Selected 属性为你做的事情。它在启动时自动为你选择场景中的 UI 元素。
你可以将任何不可交互的 UI 元素拖放到这个槽位,使其成为场景中第一个选中的 UI 项目。这对于不使用鼠标或触摸屏,而完全依赖游戏手柄、摇杆或键盘的游戏尤其有帮助。
发送导航事件
Send Navigation Events 属性可以开启或关闭。当此属性启用时,你可以通过游戏手柄、摇杆或键盘在 UI 元素之间导航。以下导航事件可以使用:
-
移动: 您可以通过键盘上的箭头键或游戏手柄上的控制杆(或您指定的任何移动键/按钮)选择各种 UI 元素。移动将从指定的第一个选择的 UI 项目开始。我们将在第十章中讨论如何使用移动来指定 UI 项目的选择顺序。
-
提交: 承诺选择 UI 项目。
-
取消: 取消选择。
拖动阈值
拖动阈值属性表示 UI 对象在被视为拖动之前可以移动的像素数。人们的手并不完全稳定,所以当他们试图点击或轻触 UI 项目时,他们的鼠标或手指可能会稍微移动。这个拖动阈值允许玩家在所选项目被拖动而不是点击之前稍微移动他们的输入(如果你将这个数字设置得高,那么移动可能会很多)。
输入管理器
在我们讨论事件系统管理器的下一个组件之前,我想讨论输入管理器。输入管理器是您通过将它们分配到鼠标、键盘或游戏手柄(游戏手柄)上的按钮来定义游戏中轴的地方。这也允许您在编码时使用轴名称,以便轻松引用您想要在动作中执行的所有输入。
注意
记住,正如我们在第五章中讨论的,实际上有两个系统可以允许您在游戏中处理输入:输入管理器和新的输入系统。本章将重点介绍输入管理器。我们将在未来的章节中讨论新的输入系统。
要打开输入管理器,请选择编辑 | 项目设置 | 输入管理器。
如果您选择轴旁边的箭头,您将看到默认的轴列表:

图 8.3:输入管理器和所有预定义的轴
默认情况下有 30 个总轴。更改大小旁边的数字将为您提供更多或更少的轴。展开单个轴将显示以下内容:

图 8.4:第一个水平输入轴
在名称槽中输入的单词将出现在可展开箭头旁边。在上面的截图中,所有允许水平移动的键都已定义。
注意,左箭头和右箭头,以及键盘上的A和D键,默认为水平移动。
列表中还有一个第二个水平轴。它配置为与游戏手柄一起工作。

图 8.5:第二个水平输入轴
由于有两个"``Horizontal"标签。
注意
要查看每个键盘键的关键字列表以及轴输入的每个属性的描述,请访问docs.unity3d.com/Manual/class-InputManager.xhtml。
这将允许你将所有这些按钮和摇杆作为一个组来引用。这比编写代码来获取每个单独摇杆上的单独键盘键要简单得多。
你可以通过右键单击并选择删除 数组元素来删除这些 30 个默认轴中的任何一个。
然而,当你删除它们时要小心。你需要至少一个提交轴和一个取消轴才能使用独立输入管理器(除非你更改独立输入管理器中的提交按钮和取消按钮)。有关更多信息,请参阅本章的独立输入管理器部分。
现在我们已经探讨了输入管理器,我们可以回顾按钮和按键的各种输入函数。
按键和按键的输入函数
有很多种方法可以通过代码访问按键和按钮的按下。你如何做这取决于你是否在输入管理器中将键指定为轴,以及你是否希望键只注册一次或持续注册。我将在本文中讨论几个,但你可以在docs.unity3d.com/ScriptReference/Input.xhtml找到完整的函数列表。
在本章前面我们回顾的Chapter8示例场景中,有一个名为KeyPresses.cs的脚本附加到了Main Camera上。如果你想要尝试按键,KeyPresses.cs脚本包含了本节中展示的所有代码。
GetButton
如果你将按钮定义为GetButton()、GetButtonDown()和GetButtonUp()中的轴,以确定何时按下按钮。
GetButton()在按钮被按下时返回true,GetButtonDown()只在按钮最初按下的一帧返回true,而GetButtonUp()只在按钮释放的一帧返回true。
在每个函数中,你放置从脚本的Update()函数中获取的轴名称,这样它们就可以在任何时候被触发。
例如,如果你想检查Enter键是否被按下,因为它被分配给提交轴的正按钮,你可以在Enter键按下时触发以下代码:
void Update () {
if (Input.GetButtonDown("Submit")){
Debug.Log("You pressed a submit key/button!");
}
}
请记住,这不仅仅会与Enter键触发,因为提交轴分配了一些键给正按钮和Alt 正按钮。
注意
重要的是要注意,如果你要播放Chapter8场景并想观察这些按钮和按键触发控制台日志消息,你必须首先在游戏视图内点击,这样输入才会注册到游戏中。
GetAxis
如果你需要一个在触发之间没有任何中断的连续触发的函数,你应该使用GetAxis()而不是GetButton()。GetButton()对于你想要按下但希望在事件触发之间有轻微暂停的按钮很好(想想按下射击按钮,枪在子弹之间有中断地射击)。GetAxis()由于这种连续的、帧率无关的执行,更适合涉及移动的事件。
GetAxis()的工作方式略有不同,因为它返回一个float值,而不是像GetButton()那样的bool值。它也最适合在Update()函数中使用。例如,你可以检查水平移动是否发生,如下所示:
void Update () {
float horizontalValue = Input.GetAxis("Horizontal");
if (horizontalValue != 0){
Debug.Log("You're holding down a horizontal button!");
}
}
GetKey
如果你想要获取未分配给轴的键盘按键,你可以使用GetKey()、GetKeyDown()或GetKeyUp()通过它们的KeyCode引用键盘按键。
GetKey()函数的工作方式与GetButton()函数非常相似。GetKey()在按键被按下时返回true;GetKeyDown()只在按键首次按下时返回true;GetKeyUp()只在按键释放时返回true。
每个键都有自己的KeyCode,需要在GetKey()函数的括号中引用。你可以在docs.unity3d.com/ScriptReference/KeyCode.xhtml找到所有键盘KeyCode值的列表。
例如,如果你想检查字母数字键盘上的*8*键是否被按下,你可以在按下*8*键时触发以下代码:
void Update () {
if (Input.GetKeyDown(KeyCode.Alpha8)){
Debug.Log("You pressed the 8 key for some reason!");
}
}
GetMouseButton
就像GetButton()和GetKey()一样,有三个函数用于检查鼠标按钮是否被按下:GetMouseButton()、GetMouseButtonDown()和GetMouseButtonUp()。它们返回true的方式与GetButton()和GetKey()函数相同。
你可以将这些函数放在Update()函数中。在括号内,你检查哪个按钮被按下;0代表左键点击,1代表右键点击,2代表中间点击。
例如,如果你想检查中间鼠标按钮是否被点击,你可以在鼠标按钮按下时触发以下代码:
void Update () {
if (Input.GetMouseButtonDown(2)){
Debug.Log("You pressed the middle mouse button!");
}
}
现在我们已经回顾了按钮和按键的输入函数,让我们回顾一下输入模块。
输入模块
输入模块描述了事件系统如何通过鼠标、键盘、触摸屏、游戏手柄等处理游戏的输入。你可以把它们看作是硬件和事件之间的桥梁。
Unity 提供了三个输入模块:
-
独立输入模块
-
基础输入模块
-
指针输入模块
要使用这些输入模块,你需要将它们作为组件附加到你的EventSystem游戏对象上。
你不仅限于使用这三个输入模块,还可以创建自己的模块,所以如果你有一个未被这三个模块覆盖的输入设备,你将创建自己的输入模块脚本,然后将其附加到事件系统。
另有一个名为触摸输入模块的输入模块,曾经是触摸屏输入所必需的。然而,此模块已被弃用,其功能现在被整合到独立输入模块中。由于此输入模块已被弃用,它将不会在本文本中讨论。
让我们深入探讨 Unity 提供的三个输入模块。
独立输入模块
独立输入模块是一个相当健壮的输入模块,可以与大多数输入设备一起工作。它支持鼠标、键盘、触摸屏和游戏手柄。
当创建EventSystem游戏对象时。然而,你可以使用添加组件 | 事件 | 独立输入模块在对象的检查器上将其作为组件附加。如果你想要添加第二个,之前已经删除了它,并想要重新附加,或者想要将独立输入模块添加到另一个游戏对象。

图 8.6:独立输入模块组件
你会看到独立输入模块的前四个属性是水平轴、垂直轴、提交按钮和取消按钮。这些属性是我想要在讨论输入模块之前先讨论输入管理器的原因。这些槽位分配的默认属性是水平、垂直、提交和取消。这些分配引用了输入管理器的轴分配。
10。这意味着在下一个输入动作被注册之前,将有一个十分之一秒的延迟。重复延迟属性是在每秒输入动作数发生之前的时间,以秒为单位。
将Force Module Active属性设置为 true 将使此独立输入****模块生效。
注意
你可以在以下位置了解更多关于独立输入模块的信息:
docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/script-StandaloneInputModule.xhtml
docs.unity3d.com/2019.1/Documentation/ScriptReference/EventSystems.StandaloneInputModule.xhtml
BaseInputModule/PointerInputModule
BaseInputModule和PointerInputModule是只能通过代码访问的模块。
如果您需要创建自己的输入模块,您将通过从BaseInputModule扩展来创建它。您可以在docs.unity3d.com/Packages/com.unity.ugui@1.0/api/UnityEngine.EventSystems.BaseInputModule.xhtml查看通过扩展BaseInputModule可以利用的变量、函数和消息的完整列表。
PointerInputModule是一个BaseInputModule,它被前面描述的独立输入模块使用。它也可以用来编写自定义输入模块。您可以在docs.unity3d.com/Packages/com.unity.ugui@1.0/api/UnityEngine.EventSystems.PointerInputModule.xhtml查看通过扩展PointerInputModule可以利用的变量、函数和消息的完整列表。
现在,让我们看看我们如何访问移动设备和触摸屏设备上的多点触控输入。
多点触控输入
访问多点触控非常简单。您可以使用Input.GetTouch(index)访问触摸,其中 index 代表触摸的索引,第一个触摸发生在索引0。从那里,您可以以几乎与访问鼠标信息相同的方式访问信息。您还可以使用Input.touchCount找出正在发生的总触摸数。请参阅本章的示例部分,了解如何访问多点触控输入的示例。
移动设备还具有加速度计和陀螺仪,为设备提供输入。让我们看看您如何访问这些输入。
加速度计和陀螺仪输入
您可以使用Vector3 Input.acceleration属性从设备的加速度计访问数据。Input.acceleration的坐标与基于设备旋转的场景对齐,如下所示:

图 8.7:基于屏幕旋转的世界坐标轴
这种情况的简单例子包括在设备移动时在场景中移动一个对象,可以在对象的Update()函数中使用以下内容:
transform.Translate(Input.acceleration.x, 0, -Input.acceleration.y);
陀螺仪使用更复杂的数学来使用Gyroscope类获取更精确的屏幕移动。记住,陀螺仪在许多设备上不受支持,因此当可能时最好使用加速度计。
注意
如何在 iOS 设备上使用陀螺仪的示例可以在这里找到:docs.unity3d.com/ScriptReference/Gyroscope.xhtml。
现在我们已经回顾了各种输入模块,让我们来回顾一下事件触发器。
事件触发器
onClick事件。然而,如果您想向一个尚未设置接收事件的对象或希望它接收不同事件的对象添加事件,您可以将事件触发器组件附加到 GameObject 上。
你可以通过选择添加组件 | 事件 | 事件触发器来附加一个事件触发器组件。
使用事件触发器组件的一个注意事项是,附加到该组件的对象会接收到所有事件,而不仅仅是添加的事件。所以,即使你没有告诉对象如何处理指定的事件,它也会接收到该事件并确认事件已发生——它只是不会做出任何响应。这可能会降低你游戏的表现。如果你担心性能,你将需要编写自己的脚本,只将你想要使用的那些事件附加到组件上。下一节事件输入将讨论如何实现这一点。
如果你在一个非 UI 元素上使用事件触发器组件,该对象也必须有一个碰撞器组件,并且你必须在场景中包含摄像机的射线投射器。
你使用的碰撞器和射线投射器取决于你是否在 2D 或 3D 环境中工作。
如果你正在 2D 环境中工作,你可以通过选择添加组件 | 物理 2D来为对象添加一个 2D 碰撞器,然后从对象的检查器中选择合适的 2D 碰撞器。然后,你可以通过在摄像机的检查器中选择添加组件 | 事件 | 物理 2D 射线投射器来为摄像机添加一个射线投射器。
如果你正在 3D 环境中工作,你可以通过选择添加组件 | 物理来为对象添加一个 3D 碰撞器,然后从对象的检查器中选择合适的 3D 碰撞器。然后,你可以通过在摄像机的检查器中选择添加组件 | 事件 | 物理射线投射器来为摄像机添加一个射线投射器。
让我们看看事件触发器可以接收的各种事件类型。
事件类型
你可以通过选择添加新事件类型来告诉对象你希望接收哪种类型的输入事件。
许多这些事件都与对象的边界区域相关。UI 对象的边界区域由 Rect Transform 的面积表示。对于非 UI 对象,边界区域由 2D 或 3D 碰撞器表示。
指针事件
指针事件可以通过独立输入模块中的指针调用。记住,指针不仅仅是鼠标。在独立输入模块中的指针可以是鼠标、手指触摸或与游戏手柄移动相关的十字准星。
两种事件类型与指针相对于对象边界框区域的位置相关。当指针进入对象的边界框时,会调用PointerEnter事件,而当指针退出边界框区域时,会调用PointerExit。
与点击对象相关的事件有三个。当指针在对象的边界区域内按下时,会调用PointerDown事件,而当指针在对象的边界区域内释放时,会调用PointerUp。需要注意的是,在PointerUp事件中,指针可以在对象外部按下,然后保持按下状态,并在对象内部释放,以触发PointerUp事件。当指针在对象的边界区域内按下并释放时,会调用PointerClick事件。
拖放事件
当处理各种拖放事件时,区分被拖动的对象和拖动对象被放置的对象非常重要。
当发现拖动对象时,会调用InitializePotentialDrag事件,但在实际拖动对象之前。
当对象被拖动时,会调用Drag事件。当指针在对象的边界框内按下并移动而不释放时,会发生Drag事件。通过释放指针来结束。当拖动开始时,从被拖动的对象中调用BeginDrag事件,当拖动结束时,调用EndDrag事件。
Drop事件与EndDrag事件不同。EndDrag事件是在刚刚被拖动的对象上调用。Drop事件是由拖动对象被放置的对象调用的。因此,当拖动对象停止拖动时,Drop事件是由接触拖动对象的那个对象调用的。所以,如果您正在制作拖放菜单,您会将Drag事件添加到您想要拖动的对象上,并将Drop事件添加到它们将被拖放到其中的槽位。
选择事件
当对象被认为是选中的对象时,会调用Select事件,而当对象不再被认为是选中时,会调用Deselect。这些事件中的每一个都只触发一次——即对象被认为是选中或取消选中的那一刻。如果您想要一个在对象选中时持续触发的事件,您可以使用UpdateSelected事件。UpdateSelected事件会每帧调用一次。
其他事件
其他事件是基于输入管理器中的分配来触发的。请记住,您可以将按钮、键等分配给定义移动、提交和取消的轴。让我们来谈谈这些事件中的几个。
当鼠标滚轮滚动时,会调用Scroll事件,当发生移动时,会调用Move事件。当分配给Submit轴的按钮被按下时,会调用Submit事件,而当分配给Cancel轴的按钮被按下时,会调用Cancel事件。
将动作添加到事件中
一旦你实际选择了事件类型,你必须指定当该事件类型触发时会发生什么。以下截图显示了选择指针进入作为事件类型的结果:

图 8.8:事件触发器组件
上述截图显示已选择指针进入事件类型,但指针进入对象的边界区域时会发生什么尚未定义。要定义事件触发时会发生什么,你必须选择事件框右下角的+符号。你可以通过多次选择+符号来添加多个在事件触发时执行的操作。
一旦将事件类型添加到事件触发器组件中,就不能再次添加,并且在添加新事件类型列表中将变为灰色。
要从事件触发器组件中移除事件类型,请选择事件类型框右上角的–符号。
一旦选择了加号,事件类型应该看起来如下:

图 8.9:带有指针进入事件的事件触发器组件
此事件的第一项设置是一个下拉菜单,包含仅运行时(默认)、编辑器和运行时和关闭选项。这是我们指定事件何时可以触发的地方。将此设置为关闭将使事件永远不会触发。将此设置为仅运行时将在游戏正在播放时触发事件。将此设置为编辑器和运行时将使事件在游戏正在播放时触发,但它也接受在游戏不在播放模式时在编辑器中的触发。大多数情况下,仅运行时对于你要做的事情就足够了,因此它是默认设置。
在下拉菜单下方有一个槽位,里面写着无(对象)。你需要从层次结构中拖动你想要运行的功能所附加的任何项目到这个槽位。一旦分配完成,所有附加到该对象的可用组件和脚本的列表将在第二个下拉菜单中显示。你可以将此槽位中附加的事件触发器对象拖动并放下,并且不限于只使用其他对象。
以下截图显示了一个当指针进入其矩形变换时带有foodSpriteSheet_1精灵的Image游戏对象。

图 8.10:带有交换精灵的指针进入事件的事件触发器组件
要查看此Chapter8场景,将鼠标悬停在图像上。它最初看起来像一瓶药水,但当你的鼠标悬停在其上时,会变成三角形。
你还可以在附加到对象的脚本中运行函数。例如,下一张截图显示了相同的图像,但现在 Main Camera 附加了一个名为 HelloWorld.cs 的脚本,其中包含一个名为 HeyThere() 的函数。

图 8.11:具有触发方法的指针点击事件的 Event Trigger 组件
HeyThere() 函数简单地在 控制台 中打印 Hello world! This is main camera speaking!,每当点击右侧的图像时。
要从 事件触发 组件中运行一个函数,它必须是公共的,返回类型为 void,并且参数不超过一个。
现在,让我们回顾一下如何通过使用事件输入来编写与事件触发组件功能相似的代码。
事件输入
如 事件触发 部分所述,你可能不想使用 事件触发 组件,因为 事件触发 组件会导致它附加的对象接收 事件触发 部分中列出的所有事件。因此,如果你担心性能问题,你将希望有另一种方式在对象上接收事件。
在 事件触发 部分中可用的所有事件类型也可以通过代码添加到对象中,而无需使用 事件触发 组件。要使用没有 事件触发 组件的事件,你必须从适当的接口派生你的脚本,并知道事件使用的事件数据类类型。
接口 是一个模板,它定义了一个类可以实现的全部所需功能。因此,通过使用接口,你可以使用该接口中定义的任何方法或函数。我将向你展示一些如何做到这一点的示例,但首先,让我们看看可用的事件及其所需的接口。
事件数据可以派生自三个类,它们是 PointerEventData、AxisEventData 和 BaseEventData:
-
PointerEventData是包含与指针相关事件的类 -
AxisEventData包含与键盘和游戏手柄相关的事件 -
BaseEventData包含所有事件类型使用的的事件
有一个第四个事件数据类,AbstractEventData。它是其他三个类继承的类。
在以下图表中提供了 StandaloneInputModule 可用的事件列表及其所需的接口和事件数据类。为了保持一致性,事件按照在事件触发组件中列出的顺序列出:
| 事件 | 接口 | 事件数据类型 |
|---|---|---|
OnPointerEnter |
IPointerEnterHandler |
PointerEventData |
OnPointerExit |
IPointerExitHandler |
PointerEventData |
OnPointerDown |
IPointerDownHandler |
PointerEventData |
OnPointerUp |
IPointerUpHandler |
PointerEventData |
OnPointerClick |
IPointerClickHandler |
PointerEventData |
OnDrag |
IdragHandler |
PointerEventData |
OnDrop |
IdropHandler |
PointerEventData |
OnScroll |
IscrollHandler |
PointerEventData |
OnUpdateSelected |
IUpdateSelectedHandler |
BaseEventData |
OnSelect |
IselectHandler |
BaseEventData |
OnDeselect |
IdeselectHandler |
BaseEventData |
OnMove |
IMoveHandler |
AxisEventData |
OnInitializePotentialDrag |
IInitializePotentialDragHandler |
PointerEventData |
OnBeginDrag |
IbeginDragHandler |
PointerEventData |
OnEndDrag |
IEndDragHandler |
PointerEventData |
OnSubmit |
ISubmitHandler |
BaseEventData |
OnCancel |
ICancelHandler |
BaseEventData |
表 8.1:各种事件的接口和事件数据类型
要编写具有这些事件之一的类,您将使用以下模板:
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class ClassName : MonoBehaviour, InterfaceName{
public void EventName(EventDataTypeName eventData){
//what happens after event triggers
}
}
上一段代码中突出显示的项目将被上一表中相应的项目替换。
例如,如果您想实现一个 OnPointerEnter 事件,在将高亮代码替换为适当的事件、接口和事件数据类型后,代码将如下所示:
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class ClassName : MonoBehaviour, IPointerEnterHandler
public void OnePointerEnter(PointerEventData eventData){
//what happens after the event triggers
}
}
您必须包含 UnityEngine.EventSystems 命名空间才能编写具有事件数据的代码。UnityEngine.UI 命名空间是可选的,并且仅在您还将为 UI 对象编写事件时才需要。
现在我们已经回顾了发送和接收事件的各种方法,让我们来看看射线发射器。
射线发射器
请记住,事件系统会跟踪射线投射以及我们讨论的所有其他事情。射线投射用于确定哪些 UI 元素正在与用户指针交互,通过从用户指针向场景投射一条射线来实现。这条射线被认为起源于摄像机的平面,然后向前穿过场景。射线击中的任何东西都会收到交互。您可以让射线继续穿过它击中的第一个 UI 元素,或者停止在它击中的第一个 UI 元素处。要让射线在击中的第一个 UI 元素处停止,该对象必须阻止射线投射。这将阻止其后面的项目被交互。接下来,我们将讨论射线发射器的类型。
图形射线发射器
当 Canvas 被添加到场景中时,它将自动获得一个 图形 射线发射器 组件。
这是允许您与该 Canvas 的所有子 UI 对象交互的射线投射系统。它有三个属性:忽略反转图形、阻挡对象和阻挡遮罩。
忽略反转图形 开关确定如果图形对象面向后(相对于射线发射器),Canvas 内的图形对象是否可以交互。阻挡对象和阻挡遮罩属性允许您分配位于 Canvas 前面的对象类型(在摄像机和 Canvas 之间),这些对象可以阻止射线投射到 Canvas 上。
其他射线发射器
如前所述,如果你想在一个非 UI 对象上使用事件系统,你必须将射线投射器组件附加到场景中的相机上。根据你是否使用 2D 或 3D,你可以向你的相机添加物理 2D 射线投射器或物理射线投射器(或两者都添加)。
在相机的检查器中,你可以通过选择添加组件 | 事件 | 物理 2D 射线投射器来添加物理 2D 射线投射器,通过选择添加组件 | 事件 | 物理射线投射器来添加物理射线投射器。
这两个组件如下所示:

图 8.12:两种物理射线投射器类型
事件掩码属性决定了哪些类型的对象可以接收射线投射。
如果你尝试将这两个组件添加到非相机 GameObject 中,系统会自动将相机组件附加到 GameObject 上。
现在我们已经回顾了我们可以用来为我们的 UI 编程交互的各种系统,让我们看看一些示例。
示例
我们将继续构建我们在过去两章中构建的 UI。为了帮助组织项目,复制你在上一章中创建的Chapter7场景;它将自动命名为Chapter8。
注意
如果你没有完成第六章和第七章的示例,但想在本章中完成示例,你可以导入标记为第八章 – 示例 1 - 开始的包。你还可以在第八章 – 示例 1 – 结束包中查看完成的示例。
使用按键显示和隐藏弹出菜单
到目前为止,我们已经创建了两个计划将其转换为弹出窗口的面板:来自第六章的暂停面板和来自第七章的库存面板。目前,它们都在场景中可见(尽管暂停面板被库存面板遮挡)。我们希望当我们在键盘上按下P和I键时,它们能够弹出。为了演示目的,我们将为每个面板使用不同的键盘键访问方式。
记住这两个面板上都有 Canvas Group 组件。这些组件将允许我们轻松访问面板的 alpha、不可交互和阻挡射线投射属性。
使用 KeyCode 与库存面板
让我们从库存面板开始。我们希望当按下键盘上的I键时,面板能够弹出和关闭。为了使库存面板通过I键显示和隐藏,请完成以下步骤:
-
在
Assets/Scripts文件夹中创建一个新的 C#脚本,通过在文件夹的项目视图中右键单击并从弹出面板中选择创建 | C#脚本来完成。 -
将脚本命名为
ShowHidePanels.cs,然后双击它以打开。 -
现在,让我们使用一个名为
inventoryPanel的public CanvasGroup变量来表示面板。我们使用CanvasGroup变量类型来引用面板,因为我们想访问ShowHidePanels脚本的属性,包括以下高亮显示的代码行:using System.Collections; using System.Collections.Generic; using UnityEngine; public class ShowHidePanels : MonoBehaviour { public CanvasGroup inventoryPanel; }尽管与 UI 元素一起使用,
CanvasGroup变量类型并不在UnityEngine.UI命名空间中,而是在UnityEngine命名空间中,因此我们目前不需要在我们的脚本中包含UnityEngine.UI命名空间。 -
让我们创建另一个变量来跟踪
Inventory Panel是否可见。将以下代码添加到脚本的下一条语句中,以初始化该变量:bool inventoryUp = false; -
我们将通过调整它们的
alpha、interactable和blocksRaycasts属性来切换面板的开启和关闭。当面板被隐藏时,它也不应接受交互或阻挡射线。因此,让我们创建一个我们可以调用的方法来执行切换。将以下命名空间添加到您的脚本中:using System;将以下方法添加到您的脚本中:
public void TogglePanel(CanvasGroup Panel, bool show) { Panel.alpha = Convert.ToInt32(show); Panel.interactable = show; Panel.blocksRaycasts = show; }如您所见,该方法有两个参数。第一个参数是一个名为
Panel的CanvasGroup,第二个参数是一个名为show的布尔值。当show为false时,它将alpha属性设置为0,当show为true时,设置为1。它还将interactable和blocksRaycasts属性在show为false时设置为false,在show为true时设置为true。 -
我们希望当场景开始播放时,
Inventory Panel被隐藏。因此,更新Start()函数以包含以下代码:void Start () { TogglePanel(inventoryPanel, inventoryUp); } -
现在,我们需要编写代码,以便在按下键盘上的 I 键时触发。我们将以
Input.GetKeyDown()函数的方式调用该函数,使其在键按下时立即被调用。我们还将使用KeyCode.I来引用键盘上的 I 键。将以下代码添加到您的Update函数中,以检查 I 键是否被按下:void Update () { //inventory Panel if(Input.GetKeyDown(KeyCode.I)){ } } -
我们希望这个键可以禁用和启用面板,因此我们将
inventoryUp的值更改为其当前值的相反。也就是说,如果它是true,我们将将其设置为false,如果它是false,我们将将其设置为true。然后,我们将调用TogglePanel()方法。将以下高亮显示的代码添加到您的
Update()函数中:void Update() { // Inventory Panel if (Input.GetKeyDown(KeyCode.I)) { inventoryUp = !inventoryUp; TogglePanel(inventoryPanel, inventoryUp); } } -
现在,为了使此代码正常工作,我们需要将其附加到场景中的某个 GameObject 上。实际上,我们附加到哪个 GameObject 上并不重要,因为我们使用了一个公共变量来访问我们的
Inventory Panel,我们可以通过检查器进行分配。然而,由于我们计划使用此脚本影响两个面板,我想将其添加到Main Camera上。将ShowHidePanels脚本拖放到Main Camera上。您现在应该看到以下内容作为Main Camera上的组件:

图 8.13:ShowHidePanel.cs 脚本组件
- 现在,我们需要将
库存面板GameObject 分配给标记为库存面板的槽位。从层次结构中拖放库存面板到这个槽位:

图 8.14:添加库存面板 ShowHidePanel.cs 脚本组件
- 播放游戏以确保代码正确无误。你应该看到库存面板一开始是不可见的,然后按键盘上的I键时开启和关闭。
现在我们已经完成了显示和隐藏库存面板所需的工作,我们可以继续到暂停面板。
使用带有暂停面板的输入管理器
现在,让我们为暂停面板做同样的事情。我们将与库存面板略有不同。为了确保你可以看到如何使用输入管理器访问一个键,我们将使用输入管理器而不是KeyCode。我们还需要实际暂停游戏。
要使用P键显示暂停面板并暂停游戏,完成以下步骤:
-
首先,我们需要设置输入管理器以包含一个
暂停轴。通过编辑 | 项目设置 | 输入管理器打开输入管理器,并选择轴旁边的箭头以展开轴。 -
默认情况下,你的项目有 30 个轴。如果你不打算使用它们,可以用新的
暂停轴替换其中的一个,但我们可以直接创建一个新的。绝对不要删除31。这将复制列表中的最后一个轴,即调试水平,如下面的截图所示:

图 8.15:带有额外轴的输入管理器
- 将第二个
调试水平轴更改为暂停轴,通过更改暂停,p,并将其余属性更改为以下内容:

图 8.16:添加到输入管理器的暂停轴
-
现在我们已经设置了
暂停轴,我们可以开始编写代码。让我们定义一些变量来与暂停面板一起使用,类似于我们为库存面板定义变量的方式。在类中你的上一个变量定义下方添加以下变量定义:public CanvasGroup pausePanel;bool pauseUp = false; -
在
Start()函数中添加以下内容,使暂停面板在开始时不可见:TogglePanel(pausePanel, pauseUp); -
由于我们向输入管理器添加了
暂停轴,我们可以使用Input.GetButtonDown()而不是Input.GetKeyDown(),就像我们使用库存面板时做的那样。我们想使用GetButtonDown()而不是GetAxis(),因为我们想要一个一次返回true而不是持续返回的函数。如果它持续返回(使用GetAxis()),当按下P键时,面板会在开启和关闭之间闪烁。在Update()函数的末尾添加以下代码。注意,它与用于库存面板的代码非常相似:// pause Panel if(Input.GetButtonDown("Pause")){ pauseUp = !pauseUp; TogglePanel(pausePanel, pauseUp); } -
现在我们已经在我们脚本中添加了新的公共变量,它应该会显示在
Main Camera中。将Pause Panel从层级拖放到 Pause Panel 插槽。

图 8.17:添加了 Pause Panel 的 ShowHidePanel.cs 脚本组件
- 现在,玩游戏并观察当你按下键盘上的 P 键时,
Pause Panel如何变得可见和不可见。
接下来,我们将学习如何暂停游戏。
暂停游戏
目前游戏实际上并没有暂停。如果我们场景中有动画或事件正在运行,即使 Pause Panel 打开,它们也会继续运行。暂停游戏的一个非常简单的方法是操作游戏的时间尺度。如果时间尺度设置为 1,时间将像平常一样运行。如果时间尺度设置为 0,游戏中的时间将暂停。
此外,我们当前的设置并不完全符合预期的暂停菜单。Inventory Panel 和 Pause Panel 可以同时显示。如果 Inventory Panel 打开,由于它渲染在后面,Pause Panel 会被它覆盖。此外,当游戏处于暂停状态时,可以激活 Inventory Panel。
为了使 Pause Panel 正确工作,我们需要暂停游戏的时间尺度、改变我们的 Panels 渲染的顺序,并在游戏暂停时禁用功能。要创建一个正常工作的 Pause Panel,请完成以下步骤:
-
将以下内容添加到
ShowHidePanels脚本的Update()函数中,以暂停游戏中的时间:// pause Panel if(Input.GetButtonDown("Pause")){ pauseUp = !pauseUp; TogglePanel(pausePanel, pauseUp); Time.timeScale = Convert.ToInt32(pauseUp); } -
现在,让我们处理这样一个事实:
Pause Panel在Inventory Panel的后面。这是一个简单的修复。只需通过拖动Pause Panel到Inventory Panel下方来改变它们在层级中的顺序。在层级中列出的较低项目将在场景中渲染在列出的较高项目之上。现在,Pause Panel将在场景中位于Inventory Panel之上:

图 8.18:Popup Canvas 的子项
-
剩下的唯一事情就是禁用
Inventory Panel在Pause Panel打开时出现和消失的能力。调整检查 I 键是否被按下的if语句,同时检查pauseUp是否为false,如下所示:if(Input.GetKeyDown(KeyCode.I) Inventory Panel cannot be activated or deactivated. If the Inventory Panel is activated when the Pause Panel is already up, it cannot be deactivated until after the game is unpaused.
重要的是要记住,当你有一个 Pause Panel 时,其他事件需要被关闭。将时间尺度设置为 0 并不会停止其他事件发生的可能性;它实际上只会停止动画和任何使用时间尺度的时钟。因此,我们需要确保我们在游戏暂停时关闭任何其他我们编程的事件。
拖放库存物品
我们有一个可以显示和隐藏的库存面板和一个 HUD 库存。我希望能够将对象从我的较大的库存面板拖拽到我们在上一章中创建的较小的 HUD 库存右下角面板。
为了让我们自己更容易操作,让我们禁用本章早期添加到主相机的ShowHidePanels脚本。你可以通过在主相机上取消选中脚本组件旁边的复选框来完成此操作:

图 8.19:禁用 ShowHidePanel.cs 脚本组件
让我们也禁用暂停面板,这样它就不会妨碍我们。通过在检查器中取消选中暂停面板名称旁边的复选框来完成此操作。
现在,我们的面板将保持可见,这使得我们更容易调试即将编写的代码。
有很多种不同的方式来实现拖拽机制。为了确保本章提供一个如何使用库存面板和右下角面板的例子,请完成以下步骤:
-
在
Assets/Scripts文件夹中创建一个新的 C#脚本,命名为DragAndDrop.cs并打开它。 -
在这个脚本中,我们将引用 UI 元素,所以将
UnityEngine.UI命名空间添加到脚本的顶部,如下所示:using UnityEngine.UI; -
我们只需要向这个脚本添加两个变量:一个将代表被拖拽的 GameObject,另一个将代表物品将被拖拽在其上的 Canvas。将以下公共变量添加到类的顶部:
public GameObject dragItem;¶public Canvas dragCanvas; -
在我们编写更多代码之前,让我们回到编辑器并做一些更多的准备工作。将
DragAndDrop.cs脚本拖拽到主相机上以将其附加为组件:![图 8.20:主相机的组件]()
图 8.20:主相机的组件
我选择创建一个附加到
主相机的脚本而不是单个库存项目,以减少需要复制此脚本的需求。 -
现在,通过选择
拖拽画布创建一个新的 UI 画布。 -
选择
HUD 画布,通过在其右上角选择设置三个点(也称为“烤肉串”)并选择复制****组件来复制其画布缩放器组件。 -
重新选择
拖拽画布,通过在其右上角选择三个点并选择粘贴****组件值,将复制的画布缩放器属性粘贴到其画布缩放器组件中。完成这些操作后,它应该具有以下值:

图 8.21:拖拽画布上的画布缩放器组件
- 将
拖拽画布的画布组件设置为1。这将导致拖拽画布上的任何内容都会渲染在其他所有画布之前,因为其他画布的值为0:

图 8.22:在拖拽画布的画布组件上更新排序顺序
- 将
Drag Canvas从层次结构拖放到主相机上的DragAndDrop脚本组件中:

图 8.23:拖放组件
-
重新打开
DragAndDrop脚本。创建一个名为StartDrag()的新函数,如下所示:public void StartDrag(GameObject selectedObject){ dragItem = Instantiate(selectedObject, Input.mousePosition, selectedObject.transform.rotation) as GameObject; dragItem.transform.SetParent(dragCanvas.transform); dragItem.GetComponent<Image>().SetNativeSize(); dragItem.transform.localScale = 1.1f * dragItem.transform.localScale; }当拖动开始时,此函数将被调用。它接受一个 GameObject 作为参数,然后在鼠标的位置创建该对象的新实例。然后,它将其移动,使其成为
dragCanvas的子对象。最后,它将图像组件上的精灵大小设置为原始大小。这会将图像的 Rect Transform 的缩放重置为其精灵的原始像素大小。(有关设置原始大小的更多信息,请参阅第十二章)。最后一行使图像比其原始大小大 10%。
注意
在我们将开始拖动和拖动事件连接起来之后,如果您注释掉设置大小为原始大小的代码行,您会发现图像实际上并没有在场景中渲染,因为它的缩放从原始 GameObject 在布局组内变得“古怪”。
-
现在,创建一个名为
Drag()的新函数,如下所示:public void Drag(){ dragItem.transform.position = Input.mousePosition; }当对象被拖动时,此函数将被调用。在对象被拖动时,它将保持与鼠标的位置一致。
-
返回到编辑器。我们现在只是将事件连接到库存面板中的第一个对象。选择库存面板中的第一个食物图像:

图 8.24:选择食物游戏对象
- 通过在检查器中选择添加组件 | 事件 | 事件触发,向食物图像添加一个新的事件触发组件:

图 8.25:带有事件触发组件的食物游戏对象
- 现在,通过选择添加新事件类型 | 开始拖动和添加新事件类型 | 拖动,将开始拖动事件类型和拖动事件类型添加到事件触发列表中:

图 8.26:具有两个事件的事件触发组件
- 现在,我们将通过选择开始拖动区域右下角的加号来向开始拖动列表添加一个动作:

图 8.27:添加开始拖动事件
- 将
Main Camera拖入对象槽中:

图 8.28:使用相机更新开始拖动事件
-
函数下拉列表现在不可操作。展开函数下拉列表以查看附加到
Main Camera的函数、组件等列表。找到DragAndDrop脚本和StartDrag (GameObject)函数:![图 8.29:添加**StartDrag**方法]()
图 8.29:添加StartDrag方法
完成上述操作后,你应该会看到以下内容:
![图 8.30:添加 StartDrag 方法]()
图 8.30:添加 StartDrag 方法
-
现在,我们需要分配 GameObject 参数。将此事件触发器组件附加到的
Food图像拖放到参数槽中。

图 8.31:更新 StartDrag 方法
- 现在,以类似的方式设置Drag事件列表,使其看起来像这样:

图 8.32:添加 Drag 方法
-
如果你玩游戏,你现在应该能够将第一个槽位中的橙色拖出其槽位。
![图 8.33:从库存中拖动橙色]()
图 8.33:从库存中拖动橙色
你会在层次结构中看到一个名为
Food(Clone)的新 GameObject,它是Drag Canvas的子对象。这是当你开始拖动时创建的橙色。![图 8.34:在 Drag Canvas 中拖动的项目]()
图 8.34:在 Drag Canvas 中拖动的项目
在这一点上,你实际上可以创建尽可能多的这些克隆。然而,稍后我们将使其在
Drag Canvas中一次只有一个克隆。 -
返回
DragAndDrop脚本,创建一个名为StopDrag()的新函数,如下所示:public void StopDrag(){ Destroy(dragItem); }此代码将在不再拖动
Food(Clone)GameObject 时将其销毁。 -
返回编辑器,重新选择
Inventory Panel中的Food图像。通过从DragAndDrop脚本中选择Drag()函数来为此事件分配EndDrag事件类型,因为这是最后一个选定的函数:

图 8.35:使用 Drag 方法结束拖动事件
- 将函数下拉菜单中的
Drag()函数替换为StopDrag()函数:

图 8.36:使用 StopDrag 方法结束拖动事件
-
开始游戏,你会看到现在可以拖动橙色出其槽位,当你释放鼠标时,它会被销毁。这阻止了你从该槽位拖动一串橙子的能力。
-
返回
DragAndDrop脚本,创建一个名为Drop()的新函数,如下所示:public void Drop(Image dropSlot){ GameObject droppedItem = dragCanvas.transform.GetChild(0).gameObject; dropSlot.sprite = droppedItem.GetComponent<Image>().sprite; }此函数接受一个
Image作为参数。此 Image 将是接收拖放操作的槽位的图像组件。函数的第一行找到dragCanvas的第一个子对象(位置0),然后将它的图像精灵分配给dropSlot的精灵。由于我们已经设置了StopDrag()函数,在停止拖动时销毁被拖动的对象,所以我们不必担心Drag CanvasGameObject 有多个子对象,这使得这是找到被拖动对象的最简单方法。 -
返回编辑器并选择
底部右面板中的第二个Food图像:![图 8.37:选择正确的食品物品]()
图 8.37:选择正确的食品物品
我们使用的是第二个
Food图像,而不是第一个,因为第一个已经有一个橙子在里头,而且很难判断我们的脚本在那个槽位是否起作用。 -
通过在其检查器中选择添加组件 | 事件 | 事件触发器来添加一个新的
Food图像。 -
通过选择添加新事件类型 | Drop,向事件触发器组件添加一个
Drop事件类型:

图 8.38:Drop 事件
-
使用+号向列表中添加一个新动作。
-
将
主相机拖到对象槽位,并从DragAndDrop脚本中选择Drop()函数:

图 8.39:带有填充方法的 Drop 事件
- 现在,将此事件触发器组件附加到的
Food图像拖入参数槽位:

图 8.40:带有正确参数的 Drop 事件
-
玩游戏,当你将橙色拖入香蕉上方的槽位时,
Drop()函数似乎没有触发。这是因为被拖动的橙色阻挡了射线投射到达香蕉,所以香蕉从未认为有任何东西被扔到它上面。这是一个简单的修复。将以下代码行添加到DragAndDrop脚本中的StartDrag()函数末尾,这样射线投射就不会再被橙色阻挡了:dragItem.GetComponent<Image>().raycastTarget = false; -
玩游戏,你现在应该能够将
库存面板的第一个槽位中的橙子拖到底部右面板的第二个槽位。 -
拖放功能现在已完成;我们只需要添加其他槽位的功能。首先,让我们向其他
库存面板物品添加拖动事件。使用组件右上角的三点复制
库存面板中的Food物品,将其与事件连接起来。 -
按住Ctrl键的同时点击其他
Food图像,以选择库存面板中的所有其他Food图像。 -
现在,所有八个
Food图像仍然被选中,点击Food图像上的三点,现在应该具有带有所有适当事件的事件触发器组件。 -
我们还没有完成这些其他库存物品。我们需要选择每个物品,并将其拖入其组件的
BeginDrag事件类型参数中。否则,这八个其他Food物品将拖出橙子而不是适当的Food物品,因为原始的橙子被分配到那个槽位。现在就做吧。 -
在继续之前,请玩游戏并确保
库存面板中的每个库存物品都能拖出相应的图像。 -
现在,我们将从
Bottom Right Panel中的第二个Food图像复制事件到面板内的所有其他Food图像。复制Bottom Right Panel中的Food图像。 -
在按住Ctrl键的同时选择
Bottom Right Panel中的其他四个Food图像。 -
在前一个屏幕截图中所显示的每个
Food图像仍然被选中时,在检查器中粘贴组件作为新组件。 -
现在,选择每个新的
Food图像,并将它们分配给它们事件****触发组件中的参数槽位。 -
播玩游戏并确保当食物物品放入其中时,正确的图像槽位会发生变化。
-
现在拖放代码已经完成,重新启用
Main Camera上的ShowHidePanels脚本和重新启用Pause Panel。
拖放代码就到这里了。目前,Pause Panel阻止了Inventory Panel内物品的射线投射,所以当游戏暂停时,你不必担心禁用这些事件。然而,如果你最终更改了布局,你将想要在执行任务之前检查ShowHidePanels中的pauseUp变量是否为false。
如果你想允许物体在两个面板之间来回移动(从两个面板拖动并放入两个面板),你只需要将适当的组件复制到对面的面板!
你可能还希望将重复的 UI 元素制作成预制体,这样你可以在开发过程中节省时间或以编程方式实例化它们。
我本想在本章中涵盖更多示例,但无法让本章占据整本书的页数!你将在接下来的章节中看到更多如何使用事件系统的示例,所以不用担心;这不是你将看到的最后一个代码示例。
使用鼠标和多点触控输入平移和缩放
本章要讨论的最后一个示例是如何使用两指触摸平移相机和捏合缩放。我们还将实现左键点击平移和滚轮缩放,以便你可以在电脑上轻松测试(当你没有多点触控输入时)。以下图片显示了我们将要实现的内容。

图 8.41:平移和缩放代码的工作演示
要在相机上实现平移和缩放,请完成以下步骤:
-
为了我能够真正看到平移和缩放的效果,我想在场景中添加一些物品。让我们先创建一个预制体,我们将在背景中多次放置它。通过在
Assets文件夹上右键单击并选择Prefabs来在你的项目中创建一个Prefabs文件夹。 -
将一个精灵拖动到场景中。我选择了
foodSpriteSheet中的第一个宝石,名为foodSpriteSheet_1。 -
确保精灵的变换组件位于原点。如果不是,请选择组件右上角的三点,然后选择重置。
-
将精灵重命名为
Tile。 -
现在,将精灵从层次结构拖动到你的
Prefabs文件夹中。这将创建一个名为Tile的预设。 -
我们不再需要场景中的预设,所以请将其删除。
-
通过在层次结构中选择+ | Create Empty创建一个空 GameObject。我们将使用它来存放实例化我们的瓷砖的代码。
-
将这个空 GameObject 重命名为
Tile Maker。 -
在本书的源文件中,你会找到三个名为
Tile.cs、TileMaker.cs和CameraHandle.cs的脚本。将它们导入到你的项目的Scripts文件夹中。 -
将
Tile.cs脚本附加到Tile预设。此脚本将用于使任何实例化的Tile预设具有随机精灵。 -
由于
Tile.cs脚本对示例不是必需的,所以我不将审查代码,但我会指出possibleSprites列表包含所有瓷砖可以转换成的精灵。将所有看起来像宝石的精灵添加到这个列表中。你应该会看到如下内容

图 8.42:瓷砖的可能精灵
-
现在,将
TileMaker.cs脚本附加到TileMakerGameObject。 -
将
Tile预设拖动到Tile Prefab槽中,并设置其他值如下:![图 8.43:TileMaker.cs 脚本组件]()
图 8.43:TileMaker.cs 脚本组件
这将总共实例化
500个瓷砖,分布在25列和20行。 -
玩游戏,你应该会看到场景中出现许多随机的宝石。

图 8.44:场景中的宝石
-
现在,让我们连接上平移和缩放脚本。将
CameraHandler.cs脚本附加到Main Camera。 -
在我们审查代码之前,让我们向组件添加正确的变量。调整值如下:
![图 8.45:CameraHandler.cs 脚本组件]()
图 8.45:CameraHandler.cs 脚本组件
不同的属性为相机平移和缩放的范围、缩放的程度以及平移和缩放的速度添加了限制。
-
现在,让我们来审查代码。代码以两种方式处理平移和缩放:一种是通过鼠标输入,另一种是通过触摸输入。你会看到它还对通过 Unity 编辑器远程播放的触摸设备有特殊条件。
这段代码的大部分是矢量数学,我将留给你自己复习。我想重点关注的代码部分是与本章相关的部分,特别是涉及
Input和Touch的部分。首先,让我们看看HandleMouse()方法。我已经突出显示了相关部分:void HandleMouse() { if (Input.GetMouseButtonDown(0)) { lastPanPosition = Input.mousePosition; } else if (Input.GetMouseButton(0)) { PanCamera(Input.mousePosition); } float scroll = Input.GetAxis("Mouse ScrollWheel"); ZoomCamera(scroll, zoomSpeedMouse); }注意它使用
Input.GetMouseButtonDown(0)来检查是否按下了左鼠标按钮,使用Input.GetMouseButton(0)来检查鼠标按钮是否被点击,以及使用Input.mousePosition来找到鼠标的位置。 -
现在,让我们看看如何在
HandleTouch()方法中处理触摸输入。首先,它使用以下switch语句查看屏幕上有多少个手指在触摸:switch(Input.touchCount returns how many *fingers* are currently touching the screen. -
当单个手指触摸屏幕时,相机可以进行平移。它首先必须获取手指的位置。它使用以下代码来完成:
Touch touch = Input.GetTouch(0); if (touch.phase == TouchPhase.Began) { lastPanPosition = touch.position; panFingerId = touch.fingerId; } else if (touch.fingerId == panFingerId && touch.phase == TouchPhase.Moved) { PanCamera(touch.position); }再次强调,我已经突出显示了相关代码。
-
当两个手指触摸屏幕时,它会使用以下方式获取手指的位置:
Vector2[] newPositions = new Vector2[]{Vector2 array. It then uses some fancy vector math to see whether the fingers are getting closer to each other or further away from each other, creating a pinch-to-zoom effect. -
我接下来要关注的是
PanCamera()方法中的以下代码行:Vector3 offset = theCamera.DragCamera() and StopCameraDrag() methods, so it will know when to get the inputs. We’ll do this with Event Triggers on the Background Canvas. Add an Background Canvas with the following events:

图 8.46:背景画布上的事件触发器
-
现在你已经审查了代码,再次玩游戏以查看平移和缩放功能。如果你能的话,我建议将移动设备连接到你的电脑,并通过 Unity Remote 运行游戏。你可以在这里查看有关如何使用 Unity Remote 的信息:
docs.unity3d.com/Manual/UnityRemote5.xhtml。 -
我们不希望游戏在暂停时以及库存面板打开时进行平移和缩放!所以,让我们更新
ShowHidePanels.cs脚本,调用TurnOffPanAndZoom()和TurnOnPanAndZoom()方法,这些方法切换canPan和canZoom布尔变量。将以下变量添加到
ShowHidePanels.cs类中:CameraHandler cameraHandler; -
现在,我们需要初始化
cameraHandler变量。添加一个Awake()方法,包含以下代码:void Awake() {¶ cameraHandler = GetComponent<CameraHandler>();¶} -
现在将以下内容添加到
TogglePanel()方法中。这将使TurnOffPanAndZoom()方法在游戏暂停或显示库存时被调用,并在两个面板都不可见时调用TurnOnPanAndZoom()方法:if (inventoryUp || pauseUp) { cameraHandler.TurnOffPanAndZoom(); } else { cameraHandler.TurnOnPanAndZoom(); }
现在你应该拥有一个完全功能的平移和缩放功能,当菜单可见时将禁用!
这几乎是我用在游戏 Barkeology 中的确切代码,你可以在 iOS 应用商店找到它:apps.apple.com/kn/app/barkeology/id1500348850。所以,如果你没有在移动设备上测试代码的能力,但想看到它的工作情况,你可以在那里查看。
虽然这标志着本章的结束,但我们将继续在本文本中使用事件系统进行工作,所以你会看到更多示例。
摘要
现在我们已经知道了如何利用事件系统和为 UI 元素编程,我们可以开始制作交互式和可视化的 UI 元素。我们还可以创建在事件发生时其各种属性会改变的 UI。
在本章中,我们涵盖了大量的内容!我们讨论了如何访问 UI 元素的属性以及如何与事件系统协同工作。我们还讨论了如何使用输入模块。现在,你可以创建对用户输入做出响应的 UI,以及对你游戏中事件做出响应的 UI。
在下一章中,我们将探讨 Unity 提供的其他输入系统:新输入系统(是的,这就是它的实际名称)。
第三部分:可交互的 Unity UI 组件
在本部分,你将探索 uGUI 系统提供的单个可交互组件。如何布局和为按钮编程将得到介绍。此外,还将介绍两种显示文本的方式,即 UI Text 和 Text-TextMeshPro。你将学习如何与 UI 图像一起工作并为其添加各种效果。讨论了如何使用遮罩、滚动条和滚动视图,以便你可以制作可展开的 UI 菜单。最后,你将学习如何使用 Unity UI 系统提供的所有其他可交互 UI 组件。
本部分包含以下章节:
-
第九章,UI 按钮组件
-
第十章,UI 文本和 Text-TextMeshPro
-
第十一章,UI 图像和效果
-
第十二章,使用遮罩、滚动条和滚动视图
-
第十三章,其他可交互 UI 组件
第九章:UI 按钮组件
Unity 的 UI 系统提供的按钮是预先利用我们在上一章中介绍的事件系统的图形对象。当按钮放置在场景中时,它会自动添加组件,允许玩家与之交互。这是有意义的,因为按钮的全部目的就是与之交互。让我们探索如何在我们的游戏中添加和利用按钮。
在本章中,我们将讨论以下主题:
-
创建 UI 按钮并设置其属性
-
如何设置按钮过渡,使按钮在突出显示、按下或禁用时改变外观
-
如何使用不可见按钮区域以允许大触摸区域
-
使用键盘或摇杆在屏幕上导航按钮选择
-
如何创建一个看起来像被物理按下屏幕上的按钮
-
通过按钮点击加载场景
-
创建按钮过渡动画
注意
在“示例”部分之前显示的所有示例都可以在提供的代码包中的 Unity 项目中找到。它们可以在标记为 Chapter9 的场景中找到。
每个示例图像都有一个标题,说明场景中的示例编号。
在场景中,每个示例都在自己的 Canvas 上,其中一些 Canvas 已被禁用。要查看已禁用的 Canvas 上的示例,只需在检查器中选择 Canvas 名称旁边的复选框。每个 Canvas 也都有自己的事件系统。如果您同时激活了多个 Canvas,这将导致错误。
技术要求
您可以在此处找到本章的相关代码和资产文件:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2009
UI 按钮
按钮是期望玩家点击的 UI 对象。您可以通过选择具有 Text 子对象的 Button 对象来创建按钮。与其他所有 UI 对象一样,如果您在创建 Button 时场景中没有 Canvas 或事件系统,将为您创建一个 Canvas 和 Event System,其中 Canvas 是您新 Button 的父对象:

图 9.1:将新的 UI 按钮添加到场景中
如果您不希望在按钮上显示文本,可以删除子 Text 对象。
按钮对象有三个主要组件:Rect Transform(类似于所有其他 UI 图形对象)、一个图像组件和一个按钮组件:

图 9.2:新 UI 按钮的组件
我们将在下一章更详细地讨论图像组件,但到目前为止,只需知道图像组件决定了按钮在标准状态下的外观。
按钮组件
当玩家尝试与按钮交互时,Button 将会执行的操作。
按钮组件的第一个属性是交互属性。此属性确定按钮是否可以通过接受玩家的输入进行交互。默认情况下,它是开启的,但如果你想要禁用按钮,可以将其关闭。
你会看到,当鼠标移出按钮的Rect Transform范围并释放鼠标时,按钮的点击事件将不会注册。你设置点击事件的方式与我们设置第八章中的事件相同。
过渡效果
按钮组件的第二个属性是过渡属性。过渡属性决定了按钮在不同状态下视觉上如何反应。这些不同的状态包括未高亮(或正常)、高亮、按下、选中或禁用。这些过渡是自动执行的,不需要编写代码。
你可以分配四种不同的过渡效果:无、颜色渐变、精灵交换和动画。
无
将过渡效果类型选择为无意味着按钮在不同状态下不会在视觉上发生变化。
颜色渐变
颜色渐变过渡类型将使按钮根据其状态改变颜色。你分配正常颜色、高亮颜色、按下颜色、选中颜色和禁用颜色。
在以下示例中,你可以看到当鼠标悬停在按钮上(因此高亮显示)时,按钮变为绿色,而当鼠标按下时,按钮变为红色:

图 9.3:第九章场景中颜色交换示例
如果你查看前面的示例,你会注意到按钮在被点击后会变成黄色。这是因为它被选中了。要将其恢复到正常颜色,请点击按钮外的任何区域。
要使按钮进入将赋予其禁用颜色的状态,按钮的交互属性必须被禁用。在以下屏幕截图中,你会看到当交互属性开启和关闭时按钮如何变化:

图 9.4:第九章场景中禁用按钮示例
你还可以选择按钮本身,这样当按钮被高亮、按下或禁用时,按钮的图像会改变颜色。然而,你可以选择将一个次要图形设置为目标图形。这意味着分配给目标图形的项目将根据按钮的交互而改变颜色。在以下示例中,一个次要图像被分配为目标图形。你会看到按钮没有进行过渡;相反,星形图像进行了过渡:

图 9.5:第九章场景中的目标图形示例
重要的是要注意,这些颜色将给 目标图形 的图像着色。因此,它实际上是在 目标图形 上叠加了一个颜色覆盖层。如果 目标图形 的图像是黑色的,这些着色看起来似乎对图像没有任何影响。注意,默认的 正常颜色 是白色。在图像上应用白色着色不会改变图像的颜色。
1,此属性将增加图形的透明度。这适用于所有状态(包括正常状态)。
淡入持续时间 属性是淡入状态颜色所需的时间(以秒为单位)。
精灵交换
精灵交换 过渡类型将使按钮在不同状态下显示不同的图像。
你会注意到没有属性可以分配一个精灵用于正常状态。这是因为正常状态将只使用分配给 图像 组件的精灵。
我们在 第六章 中导入的精灵表单有四个按钮图像,这些图像将有助于演示精灵交换过渡:标记为 uiElements_39、uiElements_40、uiElements_41 和 uiElements_42 的图像(如下截图所示):

图 9.6:我们将使用这些精灵来演示按钮精灵交换
要使按钮在适当的状态下显示这些图像,我们只需将 uiElements_39 分配给 uiElements_40,然后分配给 uiElements_41,接着分配给 uiElements_42,最后分配给 禁用精灵。我们还需要从按钮中删除子 文本 对象:

图 9.7:第九章场景中的精灵交换示例
记住,你可以通过取消选择 可交互 来查看 禁用精灵。
一个我一直觉得很有吸引力的漂亮的精灵交换动画是将一个看起来按下状态的按钮图像应用到按下图像上。例如,我取了左侧的按钮并稍作编辑,以创建右侧的按钮。变化很小,但我通过稍微向下移动按钮的顶部部分来改变它,使其看起来被按下:

图 9.8:缩进按钮动画表
当并排查看时,看起来没有太大的区别。然而,当左侧的图像用于 源图像、高亮精灵、选中精灵 和 禁用精灵,而右侧的图像用于 按下精灵 时,按钮会过渡显示一个非常好的按钮按下动画:

图 9.9:第九章场景中的按下按钮示例
要查看实际效果,请查看Chapter9Scene中的Pressed Button Example Canvas;点击确实非常令人满意。
在第十一章中,我们将探讨如何在不使用按钮过渡属性的情况下创建图像交换,例如静音/取消静音按钮。
动画
动画过渡允许按钮在其各种状态下进行动画处理。
动画过渡类型需要将动画器组件附加到按钮上。您可以通过将其拖放到按钮的检查器上来向按钮添加预存在的动画集,或者您可以通过选择自动生成动画来创建一个新的动画控制器。如果您使用预存在的动画控制器,您可以直接将动画分配给单个状态。但是,如果您生成一个新的动画控制器,您可以从动画窗口中的剪辑列表中选择状态,并从该窗口进行编辑。本章的示例部分提供了一个具有动画过渡的按钮的示例。
导航
按钮具有一个导航属性,该属性决定了它们将通过键盘或控制器输入突出显示的顺序:

图 9.10:按钮组件上的导航属性
如果您想导航到场景中的所有按钮,每个按钮都必须设置此属性。如果您还记得从第八章,我们讨论了事件系统组件的首次选中属性。如果您将按钮分配为首次选中,则在加载场景时该按钮将被突出显示。如果您随后使用键盘导航按钮,导航将从具有首次选中属性的按钮开始。然而,如果您没有将按钮分配为首次选中,则导航将不会开始,直到使用鼠标选择了一个按钮。下一个选中的按钮由您为按钮选择的导航选项确定。
有五种导航选项:无、水平、垂直、自动和显式。
选择无将禁用所有键盘导航到指定的按钮。请记住,这是针对单个按钮的,因此如果您想禁用所有键盘导航,您必须为所有按钮选择无。
水平和垂直相当直观。如果一个按钮的导航属性设置为水平,当它被选中时,下一个选中的按钮将水平选择,即使用左右箭头。垂直的作用类似;这表示从该按钮导航离开,而不是到达该按钮。因此,如果一个按钮的水平设置为水平,并且其导航属性,您仍然可以通过垂直按钮从另一个按钮访问该按钮。
自动将允许按钮在水平和垂直方向上导航,这由其相对于其他按钮的位置自动确定。
可视化按钮允许你看到导航设置的视觉表示。每个按钮都将连接,箭头显示在哪个按钮之后将被选中。每个箭头从按钮的一侧开始,表示按下的方向箭头,并指向按下该箭头时将被高亮的下一个按钮。例如,如果箭头从一个按钮的右侧开始,那么这个箭头表示如果玩家在键盘上按下右键,将选择哪个按钮。以下示例显示了五个按钮的视觉表示,所有按钮的导航属性都设置为自动:

图 9.11:第九章场景中的导航示例
在前面的示例中,每个按钮都有一个颜色渐变过渡属性,将高亮颜色和选中颜色指定为绿色。标记为1的按钮已在事件系统中指定为首次选中:

图 9.12:将按钮 1 分配给首次选择的系统事件
因此,当场景开始播放时,它将自动被突出显示。根据可视化的图,如果场景加载后键盘上选择了右箭头键,则标记为2的按钮将被选中:

图 9.13:第九章场景中的导航示例
最后一种导航类型是显式,它允许进行显著更好的控制。使用这种方式,你可以明确定义每个单独的键盘按键将访问哪个按钮。

图 9.14:显式导航类型的属性
假设你希望玩家按顺序循环操作按钮 1-2-3-4-5,然后循环回到 1。你希望这个操作可以通过上按钮或右按钮完成。之前提到的任何导航方法都无法实现这一点。然而,你可以通过使用显式导航类型来实现。本章的第一个逐步示例涵盖了如何创建显式按钮导航映射。
不可见按钮区域
在第二章中,我们讨论了屏幕上的点击区域。在移动游戏中,通常点击屏幕上的任何位置都会触发一个事件。例如,很多时候,当你选择弹出窗口之外的区域时,它将会关闭。其他例子包括你可以点击屏幕的左侧或右侧来移动角色。点击屏幕上的区域可能看起来不像按钮实现,但实际上它是!按钮只是不可见的。
让我们探索第一个场景。如果你回顾Chapter9场景中的Close Panel Example GameObject,你会看到一个当按下信息按钮时出现的面板,当按下关闭按钮时关闭,当按下面板外的区域时也会关闭。这是通过在面板后面放置一个大型、不可见的按钮来实现的。为了使其正常工作,它需要位于信息按钮的前面(阻挡对其的射线投射)。

图 9.15:第九章场景中的关闭面板示例
即使背景区域会关闭面板,包含关闭按钮也是一个好的设计选择。许多人不会直观地认为点击面板外部是一个会关闭面板的动作,如果你不提供它,他们可能会花时间寻找关闭按钮。
现在让我们探索第二种场景,其中屏幕的两边会导致不同的动作。在Tap Zone Example GameObject中,点击左侧会将拐杖糖向左移动,点击右侧则会将拐杖糖向右移动。再次强调,这里使用了大型的不可见按钮。

图 9.16:第九章场景中的点击区域示例
当使用这些大型的不可见按钮时,非常重要的一点是,你需要考虑射线投射对这些按钮的影响,即使它们是不可见的。它们会阻挡它们后面的东西!
示例
在本章的前三个示例中,我们将暂时离开我们一直在工作的场景,构建一个新的场景,以便我们可以实验按钮导航和场景加载。然后,我们将从第八章的场景继续,向我们的场景添加一些按钮。
通过按钮导航和使用首次选中
我们将构建一个如下所示的外观起始屏幕:

图 9.17:我们将构建的起始屏幕场景
这些按钮中的大多数将是虚拟按钮,但在下一个示例中,我们将设置播放按钮,以便加载我们一直在工作的场景。
为了让我们能够实验按钮导航,我们将为它分配一个显式的导航方案,这样我们就可以按照以下模式循环遍历按钮:

图 9.18:按钮导航图
首先,将选择播放按钮。在键盘上持续按下下键将导致以下选择路径:

图 9.19:带有向下箭头的按钮导航流程
持续按下上按钮将导致以下路径:

图 9.20:带有向上箭头的按钮导航流程
接下来,我们将学习如何布局按钮。
布局按钮
让我们先创建一个新的场景并布局按钮。
要制作如图图 9.17所示的模拟启动屏幕,请完成以下步骤:
-
创建一个新的空场景并将其命名为
Chapter9-Examples-StartScreen。打开新场景。 -
为了让这个场景与我们在前几章中制作的场景具有相同的背景,我们可以在层次结构中创建另一个
Chapter8-Examples场景。你现在应该看到以下内容:

图 9.21:同时加载两个场景
- 选择
Background Canvas并按Ctrl + D键进行复制。现在,拖动复制的,标记为Chapter9-Examples-StartScreen的场景:

图 9.22:复制背景 Canvas
-
我们现在可以关闭
Chapter8-Examples场景,因为我们不再需要它。为此,选择Chapter8-Examples场景右侧的三个点,并选择移除场景。以防你意外删除了某些内容,当提示时选择不保存:![图 9.23:移除场景]()
图 9.23:移除场景
现在,你应该只看到
Chapter9-Examples-StartScreen在场景中,并且Background Canvas应该可见。 -
将
Background Canvas (1)重命名为Background Canvas并保存场景。
注意
你会注意到,通过这样做,我们在场景中有一个没有事件系统的Canvas。虽然这样很好,但一旦我们添加新的 UI 元素,事件系统会自动添加。
-
在我们继续之前,我们应该注意在控制台中弹出的警告信息。由于我们从另一个场景复制了
Background Canvas,它正在尝试访问另一个场景中的相机,但找不到它。在
Background Canvas上也会出现一个警告信息:![图 9.24:Canvas 组件错误信息]()
图 9.24:Canvas 组件错误信息
为了解决这个问题,只需将当前场景中的
Main Camera拖动到渲染相机槽中,并将排序层设置为背景。![图 9.25:分配了主相机的 Canvas 组件]()
图 9.25:分配了主相机的 Canvas 组件
-
现在,让我们添加“按钮画布”。请注意,一旦你创建了“按钮画布”,就会为你创建一个“事件系统”游戏对象。
-
创建一个新的 UI 按钮(
Button Canvas)并赋予它以下Rect Transform和Image组件属性:

图 9.26:播放按钮的属性
- 现在,选择子
Play Button并设置其Rect Transform和Text组件属性如下:

图 9.27:播放按钮文本的属性
-
现在,让我们创建一个“成就按钮”、“排行榜按钮”和“信息按钮”,使它们看起来如下所示:
![图 9.28:起始屏幕按钮布局]()
图 9.28:起始屏幕按钮布局
要实现前面的布局,使用以下属性,并确保删除它们的子
Text对象:![图 9.29:三个按钮的属性]()
图 9.29:三个按钮的属性
-
现在要设置场景布局剩下的工作就是创建位于场景右下角的“Facebook 按钮”和“Twitter 按钮”。(忽略标志极其过时的实际情况。)为了实现图 9.26中的布局,创建两个具有以下属性的按钮:

图 9.30:两个按钮上的属性
你的场景现在应该布局正确,因此让我们着手设置导航。
设置显式导航和第一个选中
如果你选择了任何按钮组件中的“可视化”按钮,你应该看到以下导航路径:

图 9.31:自动按钮导航可视化
这种导航设置比我在本例开头描述的设置提供了更广泛的导航范围。这是因为每个按钮的导航都设置为“播放按钮”,它是我们事件系统中的第一个选中的按钮。
要设置本例开头描述的导航,完成以下步骤:
-
通过拖放将
Play Button选择到第一个选中槽位:![图 9.32:以播放按钮为第一个选中的事件系统]()
图 9.32:以播放按钮为第一个选中的事件系统
现在,当我们开始循环遍历我们的按钮时,我们的导航将从“播放按钮”开始。此外,如果我们在这个场景加载时按下Enter键,将自动执行“播放按钮”。
-
现在,让我们将所有按钮设置为具有“显式导航”类型。在“层次”列表中选择所有六个按钮。
-
选择所有选项后,从下拉菜单中将导航类型更改为显式。现在,每个按钮在其按钮组件中应具有以下设置。
-
为了使我们的导航更容易看到,让我们将每个按钮上的选中颜色属性更改为深红色。它并不非常吸引人,但它将使我们更容易看到按钮是否被选中。您所有按钮上的按钮组件现在应如下所示:
![图 9.33:具有颜色色调转换的按钮组件]()
图 9.33:具有颜色色调转换的按钮组件
如果您玩游戏,您应该看到
播放按钮着色为红色,表示它已被选中(因为我们已在步骤 1 中将其设置为第一个选中):![图 9.34:播放按钮被选中并着色为红色]()
图 9.34:播放按钮被选中并着色为红色
-
现在,我们可以通过拖放它们到适当的槽位来明确(因此得名)设置每个按钮要导航到的按钮。让我们首先设置
播放按钮,因为它将是第一个被选中的按钮。根据图 9.18 至图 9.20,当按下上键时,
播放按钮应导航到Twitter 按钮,当按下下键时,应导航到成就按钮。因此,将这些按钮分别拖放到上键选择和下键选择的槽位中,从层次结构中拖放。如果您选中了可视化按钮,当您将按钮拖放到它们的槽位时,您应该看到导航可视化开始构建。

图 9.35:播放按钮的导航属性
注意
记住,如果箭头从一个按钮的顶部开始,那么这个箭头表示按下上键时导航将去哪里,如果箭头从一个按钮的底部开始,它表示按下下键时导航将去哪里。
-
播玩游戏以检查它是否工作。按下上键,你应该看到
Twitter 按钮变红,表示它已被选中。要看到
成就按钮被选中,您实际上必须停止玩游戏并重新播放,因为我们尚未为Twitter 按钮设置返回到播放按钮的导航。因此,停止游戏,再次按下播放,然后按下下键,您应该看到成就按钮高亮显示为红色。
注意
您也可以用鼠标突出显示播放按钮,而不是重新启动游戏,这样您就可以导航到成就按钮。
- 一旦为一个按钮设置了导航,其余的就不太困难,尽管有些繁琐。使用以下图表来帮助您设置其余按钮:
| 按钮 | 上键选择 | 下键选择 |
|---|---|---|
播放按钮 |
Twitter 按钮 |
成就按钮 |
成就按钮 |
播放按钮 |
排行榜按钮 |
排行榜按钮 |
成就按钮 |
信息按钮 |
信息按钮 |
排行榜按钮 |
Facebook 按钮 |
Facebook 按钮 |
信息按钮 |
Twitter 按钮 |
Twitter 按钮 |
Facebook 按钮 |
播放按钮 |
表 9.1:每个按钮的向上选择和向下选择分配
当你完成时,你的导航可视化应该看起来像以下这样:

图 9.36:最终导航流程可视化
如果你玩游戏,你应该能够通过使用箭头键轻松地在按钮之间循环。
我建议将所有按钮设置为水平或垂直****导航模式,并观察它们与我们创建的模式的区别,这样你就可以看到这种模式不能通过应用于所有按钮的预定义模式来实现。
通过按钮点击加载场景
现在我们已经布置好起始屏幕,让我们将播放按钮连接到游戏。首先,使用 Ctrl + D 复制你在第八章中创建的场景,名为Chapter8-Examples。新场景应命名为Chapter9-Examples。我们的目标是让Chapter9-Examples-StartScreen中的播放按钮加载Chapter9-Examples场景。
要在Chapter9-Examples-StartScreen场景中制作播放按钮,请加载Chapter9-Examples场景并完成以下步骤:
- 要在 Unity 中切换场景,你必须首先确保它们都列在构建设置中的场景在构建中列表中。选择文件 | 构建设置(或 Ctrl + Shift + B)。以下内容应该可见:

图 9.37:构建设置中没有场景的截图
-
从
Chapter9-Examples-StartScreen和Chapter9-Examples场景到列表中:![图 9.38:将场景添加到构建中]()
图 9.38:将场景添加到构建中
在此列表中场景出现的顺序并不重要,除了第一个场景(在位置0列出的场景
Chapter9-Examples-StartScreen)。 -
现在我们已经将场景放在你的
LevelLoader文件夹中的Scripts文件夹中。打开新的LevelLoader脚本,并用以下代码替换代码:using UnityEngine; using UnityEngine.SceneManagement; public class LevelLoader : MonoBehaviour { public string sceneToLoad = ""; public void LoadTheLevel() { SceneManager.LoadScene(sceneToLoad); } }上述代码包含一个单独的函数——
LoadTheLevel()。此函数调用SceneManager类中的LoadScene方法。我们将加载sceneToLoad,这是一个在UnityEngine.SceneManagement命名空间中指定的字符串,必须在脚本顶部包含以下行:using UnityEngine.SceneManagement; -
如果你还没有打开
Chapter9-Examples-StartScreen场景,请再次打开它。选择播放按钮并将LevelLoader脚本拖放到其检查器中。 -
现在,在
Chapter9-Examples中。你不需要用引号括起来;因为sceneToLoad变量是一个字符串,所以Chapter9-Examples被认为是无需引号的一个字符串:

图 9.39:LevelLoader.cs 脚本组件
- 现在只剩下连接按钮的点击事件。选择
LevelLoader.cs,它在播放按钮上,所以将播放按钮拖放到对象槽中。现在,从函数下拉菜单中选择LevelLoader | LoadTheLevel。

图 9.40:已连接的 OnClick 事件
就这样!当你点击或当它被高亮(如开始时或使用键盘导航)时,你的播放按钮应该会导航到Chapter9-Examples场景。
按钮动画过渡
通常按钮会以动画的形式出现,以吸引你的注意。让我们给播放按钮添加一个动画过渡,这样当它处于正常状态时,它会脉冲以吸引玩家的注意。
要在播放按钮上添加按钮动画过渡,请完成以下步骤:
-
选择
播放按钮并更改其过渡类型为动画。 -
我们没有预构建的动画控制器,所以选择自动生成动画来创建一个新的。
-
将会弹出一个窗口询问你保存新创建的动画控制器。在
Assets文件夹中创建一个新的文件夹,命名为动画,并将新的动画控制器保存为Play Button到该文件夹。你现在应该能在新的
动画文件夹中看到新的动画控制器:![图 9.41:项目中的播放按钮动画]()
图 9.41:项目中的播放按钮动画
你也会看到新的
播放按钮:![图 9.42:播放按钮动画组件]()
图 9.42:播放按钮动画组件
-
通过选择窗口 | 动画打开动画窗口。如果你想要停靠这个新窗口,请将其停靠在某个位置,这样你仍然可以在打开时看到场景和游戏视图。
-
选择
播放按钮并打开动画窗口。与过渡状态相关联的各种动画剪辑将在动画剪辑下拉菜单中可见:![图 9.43:播放按钮上的各种动画]()
图 9.43:播放按钮上的各种动画
我们想编辑正常过渡状态的动画,所以请确保它被选中(正常是默认选中的动画)。
-
要使按钮看起来像是在脉动,我们想要影响其缩放。选择 添加属性 | 矩形变换,然后点击 Scale 旁边的 + 图标:
![图 9.44:将缩放属性添加到播放按钮]()
图 9.44:将缩放属性添加到播放按钮
Scale 属性现在应该显示在 动画 时间线上:
![图 9.45:播放按钮缩放时间线]()
图 9.45:播放按钮缩放时间线
-
为了通过缩放实现脉动效果,我们希望按钮从其正常大小开始,变大,然后回到正常大小。时间线上出现的钻石被称为关键帧。为了做到我刚才描述的,我们需要在时间线的正中央再添加一个关键帧。点击时间线顶部(数字出现的地方)将时间线移动到 0:30 标记处。然后,选择 添加关键帧 按钮来添加一个新的关键帧:

图 9.46:向时间线添加额外关键帧
- 通过选择左侧的箭头来展开 Scale 属性。您会看到,如果您选择任何关键帧,三个缩放方向旁边都会出现数字 1。这表明该帧的缩放为 100%(或其正常缩放):

图 9.47:展开播放按钮的缩放属性
-
选择中间的关键帧。通过点击数字 1 并输入新值来更改数字
1.2。您可以在动画窗口中按下播放按钮来预览您的动画。您会看到按钮现在在场景中脉动。当您播放游戏时,您的按钮现在应该会脉动(并且不会再变红)。请注意,它不会立即脉动,因为按钮只有在正常状态下才会脉动。由于我们将其设置为 首先选中,它在开始时就会被选中。要取消选择,只需在按钮外的场景中单击任何位置或使用键盘导航离开。一旦按钮不再被选中,它应该开始脉动。
-
让我们恢复红色选择,因为它使得判断按钮是否被选中变得容易(即使它不够吸引人)。从动画列表下拉菜单中选择 Selected 动画。

图 9.48:为播放按钮选择 Selected 动画
- 选择 添加属性 按钮,然后选择 图像 | 颜色。

图 9.49:将颜色属性添加到播放按钮时间线
-
通过选择第二个关键帧并按下 Delete 键来删除它。
-
分别将剩余关键帧上的
1、0、0和1进行更改。

图 9.50:调整播放按钮的颜色属性
现在你玩游戏时,播放按钮在没有被选中时应该会闪烁,当被选中时变为红色。
这标志着关于按钮的示例的结束,但我们将继续在未来的章节中使用它们。
摘要
一旦你学会了如何使用事件系统,使用按钮就只是个简单的扩展。按钮是最常见的交互式 UI 元素,因此掌握它们对于有效的 UI 开发至关重要。虽然将它们设置成点击时能工作只是过程的一半。如果你将为 PC、Mac 或控制台开发,你还需要确保你的按钮导航设置正确。
我们还没有完成按钮!我们将在这本书的整个过程中使用它们。一旦我们更深入地探索了图像组件,我们将介绍更多有趣的按钮实现和过渡。
在下一章中,我们将介绍 UI 文本组件!
第十章:UI 文本和 TextMeshPro
我们已经花费了一些时间与 UI 文本对象一起工作,因为它们是最基本的图形 UI 元素之一。我们在第六章中简要讨论了它们,因为在不显示任何可视内容的情况下开始布局 UI 是相当困难的。文本对象也是新按钮的子对象,我们在上一章中讨论了它们。然而,我们还没有探索文本对象的属性或如何在代码中与它们一起工作。
在本章中,我们将更深入地探讨 UI 文本对象。我们还将讨论 Text-TextMeshPro 对象以及它们如何允许我们在游戏中对文本有更多的控制。
在本章中,我们将讨论以下主题:
-
使用 UI 文本对象并设置它们的属性
-
使用 TextMeshPro - 文本对象和设置它们的属性
-
与字体和字体资产一起工作
-
使用标记格式与 UI 文本对象以及样式表与 TextMeshPro 对象
-
创建看起来像在逐字输入的动画文本
-
开发一个允许轻松文本翻译的系统
-
为 UI 文本对象创建一个自定义字体
-
创建沿曲线环绕并带有渐变的文本
注意
本节中显示的所有示例都可以在本书中提供的 Unity 项目中找到。它们可以在 Chapter10 场景中找到。
每个示例图像都有一个标题,说明场景中的示例编号。
在场景中,每个示例都在自己的画布上,其中一些画布已被禁用。要查看禁用画布上的示例,只需在 检查器 中选择画布名称旁边的复选框。每个画布也都有自己的事件系统。如果你同时激活多个画布,这将会导致错误。
技术要求
你可以在此处找到本章的相关代码和资产文件:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2010
UI 文本 GameObject
你可以使用 + | UI | Text 创建一个新的 UI 文本对象:

图 10.1:UI 文本 GameObject 检查器
UI 文本 GameObject 包含 Rect Transform 和 Canvas Renderer 组件,以及 Text 组件。
UI 文本组件为附加到其上的对象提供了一个非交互式的文本显示。这个组件不能创建你可能感兴趣的所有类型的文本,但它允许显示大多数基本的文本。
文本和字符属性
文本属性会改变将要显示的文本。在这个框内输入的任何内容都会在文本对象中显示。
在文本属性下方是一组字符属性。这些属性允许您更改文本属性字段中单个字符的属性。
字体属性决定了整个文本块使用的字体。默认情况下,字体设置为Arial。要使用任何其他字体,您必须将字体导入到您的资产文件夹中。有关如何导入额外字体的信息,请参阅处理字体部分。
字体样式提供了一个下拉列表,列出了与提供的字体一起提供的字体样式。可能的样式有正常、粗体、斜体和粗体和斜体:

图 10.2:UI Text 组件的字体样式选项
注意
需要注意的是,并非所有字体都支持列出的所有字体样式。
字体大小决定了文本的大小,而行间距表示文本每行之间的垂直间距。
如果选择富文本属性,你可以在文本属性字段中包含标记标签,并且它们将以富文本样式显示,而不是以输入的样式显示。如果此属性未选择,文本将显示为输入的样式。有关使用富文本的更多信息,请参阅标记格式部分。
段落属性
下一个属性集——段落属性(图 10**.1)——允许您确定文本如何在(或超出)Rect Transform 的边界内显示。
对齐属性决定了文本将根据 Rect Transform 边界对齐的位置。您可以同时选择水平和垂直对齐选项。按钮代表相对于 Rect Transform 边界的位置,因此左对齐将使文本推到 Rect Transform 左边界的外边缘:

图 10.3:UI Text 组件的对齐选项
按几何对齐属性将文本对齐,好像字符或字符被裁剪到它们的不透明区域而不是它们覆盖的区域。这种裁剪基于它们的字符映射。这可以提供更紧密的对齐,但也可能导致重叠。
水平溢出属性决定了文本如果太宽而超出 Rect Transform 区域时会发生什么。有两个选项:换行和溢出。换行会使文本继续在下一行,而溢出会使文本扩展到矩形区域之外:

图 10.4:第十章场景中的水平溢出示例
垂直溢出属性决定了当文本长度超出矩形变换区域时文本的处理方式。有两种选项:截断和溢出。截断会将矩形区域外的所有文本截断,而溢出则会导致文本扩展到矩形区域之外。在下面的图中,两个文本框具有相同的文本,但截断移除了最后两行文本,因为它们位于矩形区域之外,而溢出允许文本超出框外:

图 10.5:第十章场景中的垂直溢出示例
最佳适配属性尝试调整文本大小,使其全部适合矩形区域。当你选择最佳适配属性时,将出现两个新的属性:最小尺寸和最大尺寸。这些属性允许你指定字体大小可以保持的范围。
请记住,根据你写的文本,水平溢出属性可能会导致其工作方式与你的预期略有不同。

图 10.6:第十章场景中的最佳适配示例
例如,图10**.6中的两个文本框都选择了最佳适配,但第一个将水平溢出设置为换行,而第二个设置为溢出。
颜色和材质属性
颜色和材质属性允许你调整文本字体的外观。颜色属性将设置文本的基本渲染颜色,并且是更改字体颜色的最快方式。默认情况下,此属性设置为非常暗(不是全黑)的灰色。材质属性允许你为字体分配材质。这为你提供了更多控制字体外观的能力,并允许你应用特定的着色器。默认情况下,此属性设置为无。
光线投射和可遮罩属性
光线投射目标属性决定了对象的矩形变换区域是否会阻止光线投射。如果此属性被选中,则点击不会在它后面的 UI 对象上注册。如果没有选中,则可以点击对象后面的项目。如果你希望文本阻止光线投射,但不是在整个区域上,你可以使用各种光线投射****填充属性调整区域。
最后一个属性,可遮罩,决定了文本是否可以被遮罩。我们将在第十二章中讨论这一点。
文本-TextMeshPro
在 UI 文本方面,你可以做的事情有一些限制。如果你发现自己想要使用文本完成一些 UI 文本无法完成的格式化,你可能可以使用 TextMeshPro GameObject 来完成。例如,如果你想使用带下划线的文本,我建议使用 TextMeshPro GameObject。TextMeshPro 资产允许进行显著更多的文本控制。此外,它的渲染允许文本在比标准 UI 文本更多的分辨率和点大小上清晰显示。
TextMeshPro 以前是 Unity Asset Store 中的付费资产,但大约在 2017 年 3 月被 Unity 采用,现在免费提供。然而,要使用 TextMeshPro 资产,你必须下载必要的资源。要下载 TextMeshPro 资源,请尝试通过前往 + | UI | Text - TextMeshPro 将 TextMeshPro - Text 添加到场景中;你将看到以下弹出窗口:

图 10.7:TMP 导入器
选择 导入 TMP Essentials 以获取所有必要的资产。我还建议选择 导入 TMP 示例 & 附加内容。
注意
从 UI 菜单中选择任何 TextMeshPro GameObject(文本 - TextMeshPro、按钮 - TextMeshPro、下拉列表 - TextMeshPro 或输入字段 - TextMeshPro)将弹出先前的截图,并允许你下载必要的资源。
下载后,你将不再需要这样做。
由于 TextMeshPro 的强大功能,我很遗憾不能在本章中涵盖你所能用它做的所有事情。相反,我将提供一个关于其功能的广泛概述。幸运的是,TextMeshPro 资产附带许多示例和良好的文档,可以在以下位置找到:
docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/manual/index.xhtml
当你创建一个新的 TextMeshPro - Text GameObject 时,你将看到一个具有以下组件的 GameObject:

图 10.8:TextMeshPro - Text 组件
你还可以通过 GameObject | 3D Object | Text-TextMeshPro 在 Unity 的 UI 系统之外创建 Text-TextMeshPro GameObject。这将使文本独立于 UI 和 Canvas 渲染。
GameObject 本身将被命名为 Text (TMP),为了简便起见,这就是我将如何引用它的方式。
与所有其他 UI 对象一样,GameObject 上也附加了 Rect Transform 和 Canvas Renderer 组件。Text (TMP) GameObject 的图形显示由 TextMeshPro - Text (****UI) 组件控制。
让我们调查 TextMeshPro – Text (****UI) 组件的属性。
文本输入属性
你可以在 Text Input 部分输入你希望显示的文本:

图 10.9:TextMeshPro - 文本组件的文本输入设置
启用 RTL 编辑器属性允许您创建从右到左显示的文本,这对于某些语言是必要的。当您选择它时,文本将以从右到左的顺序出现在第二个区域:

图 10.10:TextMeshPro - 文本组件的 RTL 文本输入设置
文本样式设置允许您指定文本的样式。您将在下拉菜单中看到多个预定义选项:

图 10.11:TextMeshPro - 文本组件的文本样式设置
我们将在本章的样式表部分更详细地探讨如何使用它。
主设置
主设置部分允许您调整文本的所有属性:

图 10.12:TextMeshPro - 文本组件的主设置部分
LiberationSans SDF (TMP_Font Asset)。我想指出,字体和字体资产是两回事。字体用于 UI 文本游戏对象,而字体资产用于 TextMeshPro 游戏对象。我将在与字体 一起工作 *部分讨论这些差异,以及如何导入新字体和创建新字体资产。
字体资产属性需要一个材质来渲染。任何包含字体资产名称的材质都将出现在材质预设列表中。您可以创建自己的材质,但创建新字体资产时,它将附带一个默认材质。此处选择的任何材质也将出现在组件底部的额外设置下方。从此区域,您还可以选择材质的着色器:

图 10.13:TextMeshPro - 文本的材质属性
字体样式属性允许您为文本创建基本格式。您可以从粗体、斜体、下划线、删除线、小写、大写或小写字母中选择。您可以选择前四个设置中的任何组合;然而,您只能选择小写、大写或小写字母。
字体大小属性按您预期的功能工作,但您还可以选择自动大小。自动大小属性将尝试根据您指定的属性,尽可能地将文本适应 Rect Transform 的边界框内:

图 10.14:TextMeshPro - 文本组件的字体大小属性
您可以指定最小(Min)和最大(Max)字体大小,以及WD%和行属性。WD%属性允许您水平挤压文本以使字符更高,而行属性允许您指定行高。
您可以使用顶点颜色属性或颜色****渐变属性来更改文本的颜色:

图 10.15:TextMeshPro - Text 组件的颜色属性
使用<color>标记标签的示例。
您可以在间距****选项区域设置字符、单词、行和段落之间的间距。
在 TextMeshPro-Text 中,您可用的对齐选项比标准 UI Text 多得多。
就像 UI Text 一样,您可以选择启用或禁用换行。然而,您有更多的溢出选项,如下面的截图所示:

图 10.16:TextMeshPro - Text 组件的溢出选项
溢出和截断与 UI Text 对象上的功能类似。在其他选项中,我只想提到当前的省略号选项。它将文本截断到文本框区域,但会添加省略号(…):

图 10.17:第十章场景中的 Text Mesh Pro 溢出示例
水平映射和垂直映射属性允许您影响纹理在文本上的显示方式:

图 10.18:TextMeshPro - Text 组件的水平映射选项
您在文本输入部分和主要设置部分所做的大部分自定义字体工作,但让我们看看一些更细致的设置,您将使用字体进行调整。
额外设置
额外设置菜单必须展开才能可见。它允许您调整字体的一些不太常见的设置:

图 10.19:TextMeshPro - Text 额外设置部分
在此菜单中,最显著的属性是添加边距和启用射线投射目标的能力。
最后,您可以指定是否要启用字距调整或允许额外填充。选择字距调整将使用字体文件提供的字距调整数据。选择额外填充将在精灵的精灵图集上的字符周围添加一些填充。
注意
您可以在此处了解我略过或跳过的任何属性:docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/manual/TMPObjectUIText.xhtml。
TextMeshPro 项目设置
除了可以通过组件调整场景中每个 TextMeshPro 对象的个别设置外,您还可以通过 项目设置(编辑 > 项目设置)调整 TextMeshPro 的项目级设置:

图 10.20:TextMeshPro – 项目设置
这些设置允许您为新创建的 TextMeshPro 对象设置各种默认值。您可以在docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/manual/Settings.xhtml了解更多关于每个属性的信息。
与字体一起工作
极有可能您不会想使用默认的 Arial (UI 文本) 或 Liberation Sans (TMP 文本) 字体,而希望将自定义字体引入到您的项目中。让我们来探讨如何找到这些文本资源并在您的项目中使用它们。
注意
您不需要在计算机上安装字体(用于 Unity 之外的程序)即可在 Unity 中使用该字体。
导入新字体
Unity 接受的字体文件格式是 .tff (TrueType) 和 .otf (OpenType)。您可以从多个地方获取这些文件。我最喜欢的寻找字体的地方如下:
-
Google Fonts:
fonts.google.com/ -
DaFont:
www.dafont.com/ -
字体 Squirrel:
www.fontsquirrel.com/
在 Google Fonts 上的所有字体都是开源的,并且可以免费用于个人或商业用途(至少在撰写本书时是这样),但 Font Squirrel 和 DaFont 上的字体有不同的许可选项。在使用之前,请确保您获得的字体具有满足您需求的许可协议。
下载您选择的字体后,只需将字体拖放到您项目中的 Assets 文件夹。我强烈建议您在 Assets 文件夹内创建一个名为 Fonts 的文件夹,并将所有字体文件放置其中。
然后,如果您选择,可以在 检查器中调整字体的导入属性。以下截图显示了从 Google Fonts 下载的 BungeeShade-Regular 字体的导入设置:

图 10.21:Bungee Shade-Regular 字体的检查器
在这里,您可以调整许多有关字体如何被引擎处理的设置。
字体大小
字体大小设置决定了字体在其 Unity 创建的纹理图集中的显示大小。增加或减少字体大小设置将改变纹理图集中各种字符的大小。如果你的字体在游戏中看起来模糊,调整字体大小设置可能会改善其外观。
渲染模式
渲染模式设置告诉 Unity 如何平滑字符。可能的选项有平滑、提示平滑、光栅和OS 默认。
平滑渲染模式是最快的渲染模式。它使用抗锯齿渲染,这意味着它会平滑掉锯齿状、像素化的边缘。提示平滑渲染模式也会平滑边缘,但它将使用字体数据文件中包含的“提示”来确定如何填充这些锯齿边缘。这比平滑渲染模式慢,但可能看起来更清晰,更容易阅读。提示光栅渲染模式不提供抗锯齿,而是提供带锯齿或锯齿状的边缘。这是最清晰且最快的渲染模式。OS 默认将默认为 Windows 或 Mac OS 上操作系统的偏好设置。这将从平滑或提示平滑中选择。
字符
字符属性决定了哪个字体字符集将被导入到字体纹理图集中。有六个选项:动态、Unicode、ASCII 默认集、ASCII 大写、ASCII 小写和自定义集:
将字符属性设置为动态(默认值)将只包括所需的字符。这减少了字体所需的纹理大小,从而减少了游戏的下载大小。
Unicode用于那些在 ASCII 字符集中不支持的语言。例如,如果你想显示日语文本,你将需要使用Unicode。
如果你想在脚本中包含Unicode字符,你需要用 UTF-16 编码保存它们。这将允许你直接在代码中以字符串形式输入Unicode字符,以便它们可以在屏幕上的 Text 对象中显示。
由Google Fonts提供的Noto 字体支持多种语言,如果你想要创建一个被翻译成多种语言的游戏,这些字体会非常有帮助。Noto 字体可以在www.google.com/get/noto/找到。
美国信息交换标准代码(ASCII)是一组来自英语字符集的字符。ASCII字符集的三种变体允许你在完整集、仅大写或仅小写字符之间进行选择。你可以在ascii.cl/找到ASCII字符列表。
自定义集字符将允许您导入您自己的纹理图集以用于您自己的自定义字体。我发现这通常在开发者想要具有极小字符集的美化文本时最常用,例如仅数字。
上升计算模式
字体的上升是指字体基线与最高字形点之间的距离。这个所谓的最高字形点的确定没有标准,因此 Unity 提供了不同的模式供选择,每种模式确定不同的最高字形点。上升计算模式属性确定上升将如何计算。有三种选择来决定这种计算方式:旧版 2 模式(字形边界框)、面上升度量和面边界框度量。选择的方法可能会影响字体的垂直对齐。
旧版 2 模式(字形边界框)使用字体字符集中列出的任何字形的最高点作为高度来测量上升。这仅使用字符集中列出的字形,并且并非所有字形都可能包含在该集中。面上升度量使用定义来测量上升的面上升值,而面边界框度量使用面边界框来测量上升。
字体排印比许多人想象的要复杂得多,而且在这个书中完全涵盖它也太复杂了——更不用说,我并不是字体排印专家。如果您想了解更多关于字形度量信息,可以在www.freetype.org/freetype2/docs/glyphs/glyphs-3.xhtml找到一篇很好的介绍。
动态字体设置
当您使用动态字符集导入您的字体时,将提供两个新的设置:包含字体数据和字体名称。
包含字体数据会在游戏中构建字体文件。如果不选择此选项,游戏将假定玩家已经在他们的机器上安装了字体。如果您正在使用从网络上下载的字体,那么最终用户很可能没有安装该字体,您应该选择包含字体数据。
字体名称是 Unity 在找不到字体时将回退到的字体名称列表。如果字体不包含请求的字形或包含字体数据属性未选中且用户没有在他们的机器上安装字体,它将需要回退到这个字体名称。如果 Unity 找不到字体,它将在游戏的项目文件夹或用户的机器上搜索与字体名称中列出的名称之一匹配的字体。一旦字体被输入到字体名称中,适当的字体将在项目中的其他字体引用部分列出:

图 10.22:Roboto-Regular 字体的检查器
如果 Unity 找不到列出的字体之一,它将使用 Unity 中预定义的回退字体列表中提供的字体。
一些平台在其系统中没有内置字体,或者无法访问内置字体。这些平台包括 WebGL 和一些控制台系统。当构建到这些平台时,Unity 将始终包含字体,无论选择的“包含”字体数据设置如何。
导入字体样式
如果您引入了一个具有多种样式(即粗体、斜体或粗体和斜体)的字体,您必须引入所有字体样式,以便它们能够正确显示。将字体引入您的项目可能不足以让字体识别在选择了粗体、斜体和粗体和斜体字体样式时应该使用哪种字体。例如,以下截图显示了顶行文本上的Roboto-Regular Google 字体和粗体和斜体****字体样式,以及底行文本上的Roboto-BoldItalic Google 字体和正常****字体样式:

图 10.23:Roboto 字体的样式
如果字体样式属性工作正常,两行应该匹配。然而,如您所见,它们并不匹配。要使字体正确显示,请选择常规字体,重新输入字体名称属性,使所有适当的字体都出现在字体列表中(如图图 10.22所示),然后按应用。完成此操作后,两个字体应该看起来相同:

图 10.24:正确应用 Roboto 字体的样式
现在我们已经回顾了如何导入字体,让我们看看如何创建自定义字体。
自定义字体
您可以通过从项目窗口中选择创建 | 自定义字体来创建自定义字体。要使用自定义字体,您将需要一个字体材质和字体纹理。如何做到这一点在本章的示例部分中有所说明。一旦您创建了您的自定义字体,您将获得以下属性来设置:

图 10.25:自定义字体的检查器
行间距属性指定了每行文本之间的距离。
数字0,它是 ASCII 索引48。因此,您可以将48设置为表示该字体字符集中的第一个字符是数字 0。您可以在 ascii.cl/ 确定单个字符的 ASCII 索引号。
Tracking 属性表示一行文本中字符之间的间距。它允许所有字符之间的间距保持一致。
Kerning 属性已被 Tracking 属性所取代。Tracking 和 Kerning 都是关于字符间间距的属性,但它们是不同的。更多信息,请参阅 www.practicalecommerce.com/Typography-101-The-Basics。
Character Spacing 是字符间的空间量,而 Character Padding 是在间距之前围绕单个字符的填充量。
Character Rects 属性确定字体中总共有多少个字符。将数字从 0 更改为任何正数将提供一个可以展开的 Elements 列表:

图 10.26:自定义字体的字符矩形
每个元素代表字符集中的一个字符。Index 是指定字符的 ASCII 索引。
.2 和 .5。这应该适用于你所有的字符。X 和 Y 值是通过将 W 和 H 值乘以字符所在的列或行来确定的。
50 和 -50,分别。坦白说,我并不完全清楚为什么高度属性总是负数,我也似乎找不到答案。我怀疑这与它使用纹理坐标有关。Vert 的 X 和 Y 值代表位置的变化,这些数字可以是负数或正数。
Advance 设置表示特定字符与下一个字符之间的像素距离。
Flipped 设置指示字形是否翻转以及应该如何显示。
请参考 示例 部分,了解如何计算自定义字体的 UV、Vert 和 Advance 值。
在撰写本文时,Unity 的文档中并未完全定义自定义字体的所有属性,其中一些属性有些模糊不清。例如,Convert Case 以前是一个下拉菜单,现在我并不清楚它现在是如何使用的,因为它只接受数字输入。也许在未来,这些属性将得到更好的定义,手册也将更新以反映所做的新的更改,请参阅 docs.unity3d.com/Manual/class-Font.xhtml。
字体资源
请记住,TextMeshPro 对象需要使用字体资源,而不是字体。所以,如果你为你的项目下载了一些字体,你不会在 TextMeshPro 对象中看到它们作为可能的字体选项。如果你已经导入了 TextMeshPro 示例,你可能除了 Liberation Sans 之外没有其他选项。要在 TextMeshPro GameObject 中使用不同的字体,你不能简单地拖动一个新的字体到 Font Asset 槽中;你必须通过 Font Asset Creator 创建一个字体资源。
要访问字体资产创建器,请选择窗口 | TextMeshPro - 字体资产创建器。这将允许您将字体文件转换为 TextMeshPro 可以使用的字体资产:

图 10.27:字体资产创建器窗口
有很多设置可以通过字体资产创建器来控制。您可以在 https://docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/manual/FontAssetsCreator.xhtml?q=font asset creator 找到这些设置的详细说明。我将在本章的示例部分介绍创建字体资产的过程。
探索标记格式
类似于 HTML 的标记语言可以包含在 <tag>您想要格式化的文本</tag> 格式的文本字段中,其中您将 tag 替换为适当的标签。这些标签可以嵌套,就像在 HTML 中一样。
要在文本对象上使用标记格式,您必须首先在文本组件中选择富文本属性。默认情况下,它适用于TextMeshPro对象。
此格式化允许您更改字体样式、字体颜色和字体大小。以下图表列出了执行指定格式化所需的标签:
| Format | Tag |
|---|---|
Bold |
b |
Italic |
i |
Color |
color |
Size |
size |
表 10.1:格式及其标签
现在,让我们看看如何使用标记来更改字体的样式。
字体样式
您可以使用加粗和斜体标签来更改文本的字体样式。
要给文本添加加粗字体样式,请将 <b></b> 标签添加到您想要加粗的文本周围。要给文本添加斜体字体样式,请将 <i></i> 标签添加到您想要斜体的文本周围。

表 10.2:格式化标签示例
现在,让我们看看如何使用标记来更改字体的颜色。
字体颜色
您可以使用数字的十六进制值表示或使用颜色名称来更改字体颜色。要更改文本颜色,请将 <color=value></color> 添加到您想要着色的文本周围,其中您放置十六进制值(后跟一个 #)或颜色名称,其中出现单词 value。
只有有限的颜色名称可以替换十六进制值。可识别的颜色名称包括黑色、蓝色、棕色、青色、深蓝色、绿色、灰色、浅蓝色、黄绿色、洋红色、栗色、海军蓝、橄榄绿、橙色、紫色、红色、银色、青绿色、白色和黄色。您还可以用青色代替青色,用品红色代替洋红色。
当使用颜色标签时,任何未用颜色标签包围的文本将根据所选的颜色属性进行着色。
以下表格显示了如何使用颜色标签:

表 10.3:颜色格式化标签示例
现在,让我们看看如何使用标记来更改字体的大小。
字体大小
要更改字体大小,请将 <size=#></size> 标签添加到您想要调整大小的文本周围。任何不在标签内的文本都将根据 size 标签的大小进行调整:

表 10.4:多个字体大小标签的示例
到目前为止,我们已经探讨了我们可以使用标记格式化字体的方式。现在,让我们看看如何使用样式表来格式化字体。
使用样式表
除了上一节中描述的标记标签之外,TextMeshPro 对象还可以使用样式表标签。如您从图 10**.9中回忆的那样,TextMeshPro - Text (UI)组件有一个标记为Text Style的属性,在其下拉菜单中有十个选项。从那里,您可以选择以下图中显示的任何一种样式:

图 10.28:默认样式表中的各种默认样式
所有这些样式都是由分配给Project Settings中的TextMeshPro Settings的Default Style Sheet选项预定义的(正如我们在TextMeshPro Project Settings部分所讨论的)。

图 10.29:项目设置中的默认样式表设置
在Assets文件夹中的Default Style Sheet内的Default Style Sheet (TMP_Style Sheet)对象上单击:

图 10.30:资源中的默认样式表资产
您可以查看Default Style Sheet资产的Inspector,并查看每个默认样式表标签是如何定义的:

图 10.31:默认样式表的属性
您可以通过编辑此资产来更改这些标签的名称和属性。您可以添加和删除标签。
您还可以创建一个新的样式表并将其设置为默认样式表。要创建一个新的样式表,在Assets文件夹内右键单击并选择Create | TextMeshPro | Style Sheet。
您可以通过下拉菜单应用这些样式,就像我在本节开头所演示的那样,或者您可以通过使用<style>标签以标记格式将它们分配给文本的某些部分。例如,如果您想显示图 10**.32中显示的文本,您可以通过在Text字段中输入Here's <style="Title">how</style> to use styles <style="Link">in-line</style>!来实现:

图 10.32:以标记方式内联使用的样式
现在我们已经回顾了如何使用标记格式化字体,接下来让我们探索如何使用各种 Text 组件属性,并进行翻译。
翻译文本
如果您正在创建包含文本的游戏,您可能希望翻译这些文本。为了确保您的游戏易于翻译,您可以通过执行一些关键操作来简化不同语言之间的转换。
你需要确保你计划翻译的文本在翻译后如果变长或变短,仍然可以适应必要的区域。你可以通过使用文本组件的根据几何对齐和最佳匹配属性来实现这一点。这将使文本适应所需的空间。你也可以使用内容大小适配器(我们在第七章中讨论过)来确保围绕文本的任何面板都会收缩或扩展以完美地围绕文本。如果你不介意字体大小在不同语言间变化,可以使用第一个选项。如果你希望字体大小在不同语言间保持一致,可以使用第二个选项。请记住,一些语言可以在非常小的空间内渲染一个短语,而其他语言则会在非常大的空间内渲染。
使用可以支持你将要翻译的语言的字体。如果可能,一个适用于所有语言的单一字体将是首选,因为它将在所有翻译中保持一致的风格。你花了那么多时间挑选完美的字体!你不想在翻译你的游戏时所有这些都付诸东流,而字体无法渲染该语言的全部符号!这不是一个华丽或特别时尚的字体,但一个可以翻译成许多语言的字体是 Noto Sans 字体家族。如果你知道你将翻译成很多语言,你可能想要考虑它:fonts.google.com/noto/fonts.
如果你计划翻译成从右到左渲染的语言,例如阿拉伯语,你需要使用 TextMeshPro 对象,而不是 Text 对象,因为它将允许你从 RTL 渲染文本。
注意
在写作时,虽然 TextMeshPro 能够从右到左渲染文本,但它并不完全支持从右到左的语言。如果你想在你游戏的 UI 中显示阿拉伯语、波斯语和/或希伯来语,我推荐以下包:github.com/pnarimani/RTLTMPro.
你也许还会欣赏以下关于渲染从右到左文本的教程:allcorrectgames.com/insights/unity-from-right-to-left/.
以下也是一个非常有用的资源,因为它分解了如何为你的项目构建本地化解决方案,并讨论了从右到左的文本翻译:phrase.com/blog/posts/localizing-unity-games-official-localization-package/.
在示例部分,我提供了一个小型示例,重点关注翻译的 UI 布局和字体方面。
示例
在本章中,我们将进一步扩展我们一直在构建的场景,并添加一个位于我们的起始屏幕和主游戏屏幕之间的新场景。
创建动画文本
首先,我们将创建一个新的场景,它将作为我们的起始屏幕和游戏场景之间的场景过渡。它将包括我们的猫自我介绍。文本将以类似打字的方式动画化,用户可以通过按按钮来加快速度。一旦文本完全显示,按下相同的按钮将显示下一个文本块或转到游戏场景。文本窗口将如下所示:

图 10.33:我们的动画文本框的最终结果
让我们先创建一个预制件以节省我们一些开发时间。
创建背景画布预制件和新的场景
在我们开始制作动画文本之前,我们需要构建我们的场景。在我们迄今为止创建的两个场景中,我们都使用了背景画布来显示背景图像,我们将在新的场景中再次使用它。
由于我们将多次使用这个Background Canvas,我们应该创建一个Background Canvas预制件。正如我们在前一章中学到的,预制件是一个可重复使用的 GameObject。在场景中使用预制件会在场景中创建预制件的实例。如果你对保存的预制件进行了更改,更改将反映在所有未断开的预制件实例中,这些实例位于所有场景中。
要创建一个可重复使用的Background Canvas prefab GameObject,请完成以下步骤:
-
打开
Chapter9-Examples场景。 -
将
Background CanvasGameObject 从层次结构拖动到背景画布文件夹内的预制件文件夹中。现在在层次结构中Background Canvas应该显示为蓝色(表示它是一个预制件)。 -
让我们创建一个新的场景,我们将在这个场景中使用这个
Background Canvas预制件。创建一个名为Chapter10-Examples-IntroScene的新场景,并将其保存在Scenes文件夹中。 -
从项目视图中将
Background Canvas.prefab拖动到新场景中。 -
将
主摄像机分配给背景画布,并确保排序层设置为背景。
现在,我们可以开始设置将包含我们的动画文本的窗口。
布局文本框窗口
要创建将显示我们的文本的文本框窗口,请完成以下步骤:
- 创建一个新的 UI 画布,并将其命名为
文本画布。设置其画布和画布缩放器属性,如图所示:

图 10.34:文本画布检查器
-
创建一个新的 UI Image,并将其命名为
TextHolder1。将它的锚点和支点设置为uiElements_10到源图像,然后选择设置原生大小以使图像的大小设置为 223 x 158。 -
此面板将包含我们的猫的图像、一些文本和一个继续按钮。利用锚点、支点和拉伸,将 UI 对象作为
TextHolder1的子对象布局,以便它们看起来如图所示:![图 10.35:TextHolder1 及其子对象]()
图 10.35:TextHolder1 及其子对象
注意,在前面的屏幕截图中,
Text子组件的 Rect Transform 并没有延伸到TextHolder1图像的全宽。这样,文本就不会跨越窗口的白色区域。 -
我们希望能够打开和关闭这个窗口,所以添加一个
TextHolder1。保留默认设置。 -
现在,让我们更改字体。访问 https://www.dafont.com/milky-coffee.font 并下载名为Milky Coffee的字体。
-
将
Milky Coffee字体添加到Assets/Fonts文件夹中,然后将其拖放到Text对象的Text组件中。 -
将字体更改为
18。 -
复制
TextHolder1并将其命名为TextHolder2。 -
将
TextHolder1的Text子组件上的文本替换为"Hello there!",并将TextHolder2的Text子组件上的文本替换为"I'm a cat and, for some reason, I'mcollecting food!"。 -
通过禁用Canvas Group组件上的
0来隐藏和禁用TextHolder2。
现在,我们已经准备好开始对文本进行动画处理了!
动画文本框文本
现在我们已经设置了布局,我们可以对文本进行动画处理。为此,我们需要创建一个新的脚本。这个脚本将控制文本的动画,并在所有文本显示完毕后加载下一个场景。
要创建看起来像在打字的动画文本,请完成以下步骤:
-
我们想要将以下三个元素组合起来创建动画文本框:包含文本的面板、将显示消息的文本对象以及我们想要显示的字符串。因此,为了将它们全部组合起来,我们需要创建一个类。创建一个名为
DialogueBox的新类。 -
更新脚本,使其不继承自
MonoBehaviour,并如下所示:using UnityEngine; using UnityEngine.UI; [System.Serializable] public class DialogueBox { public CanvasGroup textHolder; public Text textDisplayBox; public string dialogue; }我使用了
[System.Serializable],这样我们就能在检查器中看到这些值。记住,每次我们使用Text类型时,都需要使用UnityEngine.UI命名空间。 -
创建一个名为
DialogueSystem.cs的新 C#脚本。 -
我们将编写实现场景加载并使用各种
System方法和集合的代码。因此,我们需要在脚本顶部包含以下命名空间:using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; -
现在,让我们开始变量声明。首先,我们将创建一个名为
dialogueBoxes的DialogueBox对象列表:public List<DialogueBox> dialogueBoxes; -
创建一个将保存动画后加载的场景名称的
private变量:[SerializeField] string nextScene;我用
SerializeField属性标记了它,这样我们就可以通过检查器分配它,同时它仍然是私有的。 -
在我们继续之前,让我们在 Unity 编辑器中分配这些变量。将
DialogueSystem.cs脚本附加到Text Canvas游戏对象上。你应该会看到以下内容:

图 10.36:对话系统组件
- 在
DialogueBox类序列化可编辑的底部按下加号,我们应该会看到以下内容:

图 10.37:具有两个元素的对话系统组件
-
现在,让我们将适当的元素拖放到相应的字段中。将
TextHolder1拖放到TextHolder2拖放到元素 1的文本持有者。 -
将
TextHolder1的Text子对象拖放到TextHolder2的Text子对象拖放到元素 1的文本****显示框。 -
现在,更新
Hello there!和I'm a cat and, for some reason, I'm collecting food!。我们将通过代码在文本框中生成文本。在先前的子节中,我们将文本添加到了场景中放置的文本框中。然而,由于我们将编写的代码,这一步将变得不再必要。尽管如此,将文本添加到这些文本框中是有帮助的,因为我们能够看到它将如何显示。 -
目前,在编辑器中我们需要做的最后一件事是为对话完成后游戏将导航到的场景分配。我们稍后会创建一个名为
Chapter10-Examples的场景,但,目前,让我们将Chapter9-Examples场景分配给Chapter9-Examples。您完成后的组件应如下所示:

图 10.38:填充了所有属性的对话系统组件
-
现在,我们可以回到我们的
DialogueSystem.cs脚本。我们的变量声明还没有完成。我们需要两个变量来跟踪我们想要显示的对话中的字符串,以及我们正在显示的特定字符串中的哪个字符。将以下变量声明添加到您的脚本中:int whichText = 0; int positionInString = 0;注意,这两个变量不是公共的或序列化的,因此不能在检查器中调整。它们的值将由脚本调整。
whichText变量将允许我们在显示对话列表中的第一个字符串和第二个字符串之间切换。我们将编写的代码将很容易扩展到更多的字符串。positionInString变量将跟踪动画正在输入的哪个字符。我们想要跟踪这一点,以便我们可以判断文本是否被用户加速,或者他们是否已经阅读了整个文本,只想继续到下一部分。 -
我们将使用一个协程来逐字动画化我们的文本。协程非常适合处理定时和计划事件。我们需要声明的最后一个变量将允许我们引用我们的协程,这样我们就可以轻松地停止它。声明以下变量:
Coroutine textPusher; -
将控制文本动画的协程如下:
IEnumerator WriteTheText() { for (int i = 0; i <= dialogueBoxes[whichText].dialogue.Length; i++) { dialogueBoxes[whichText].textDisplayBox.text = dialogueBoxes[whichText].dialogue.Substring(0, i); positionInString++; yield return new WaitForSeconds(0.1f); } }此代码使用
whichText变量在当前对话框中查找当前字符串,并遍历其所有字符。在循环的每一步中,UI Text 对象的文本属性被更新以显示字符串的前i个字符,其中i代表循环的当前步骤。然后增加positionInString变量,并等待十分之一秒来显示下一个字符,通过继续循环的下一步来实现。 -
在前面的代码执行任何操作之前,我们需要启动我们的协程。在启动的过程中,我还想分配
textPusher变量。将以下内容添加到Start()函数中:void Start() { textPusher = StartCoroutine(WriteTheText()); }如果你现在玩游戏,你应该会看到场景中的Hello there!文本逐字出现。
-
目前,协程只遍历第一个
DialogueBox中的字符串。我们需要通过增加whichText变量使其继续遍历列表中的DialogueBox。我们还需要添加功能,允许玩家显示所有文本,这样他们就不必等待文本完全动画化,如果他们不耐烦的话。让我们创建一个函数,该函数将在按钮按下时被调用。我们还将创建一个函数,用于封装 Canvas Group 的启用和禁用:public void ProceedText() { if (positionInString < dialogueBoxes[whichText].dialogue.Length) { StopCoroutine(textPusher); dialogueBoxes[whichText].textDisplayBox.text = dialogueBoxes[whichText].dialogue; positionInString = dialogueBoxes[whichText].dialogue.Length; } else { ToggleCanvasGroup(dialogueBoxes[whichText].textHolder, false); whichText++; if (whichText >= dialogueBoxes.Count) { SceneManager.LoadScene(nextScene); } else { positionInString = 0; ToggleCanvasGroup(dialogueBoxes[whichText].textHolder, true); textPusher = StartCoroutine(WriteTheText()); } } } public void ToggleCanvasGroup(CanvasGroup Panel, bool show) { Panel.alpha = show ? 1 : 0; Panel.interactable = show; Panel.blocksRaycasts = show; }当按钮被按下时,代码首先使用
positionInString变量确定是否已经显示了整个字符串。如果positionInString变量小于当前字符串中的总字符数,它将显示完整的字符串;否则,它将继续执行。当
positionInString变量小于当前字符串中的总字符数时,协程会通过StopCoroutine(textPusher)提前停止。textDisplayBox的text属性被更新以显示整个字符串,并将positionInString设置为字符串的长度;这样,如果按钮再次被点击,这个函数将知道它可以继续到下一步。当
positionInString变量不小于当前字符串中的总字符数时,当前 Canvas Group 被禁用,然后whichText变量增加。一旦这个变量增加,代码会检查是否还有其他文本框需要动画化。如果没有,则加载下一个场景。如果有更多文本框需要动画化,则将positionInString变量重置为0,这样字符串中的第一个字符将首先显示。新的 Canvas Group 现在被激活,textPusher变量被重新分配,以便协程循环再次播放。 -
现在我们已经完成了代码,我们只需要将我们的按钮连接到执行之前步骤中描述的功能。对于两个
TextHolder对象上的所有按钮,将DialogueSystem脚本中的ProceedText()函数设置到Text Canvas上。现在,当你玩游戏时,当你点击按钮而文本尚未完全输入时,它将完全显示,当你点击按钮而文本已经完全显示时,将显示下一个对话或下一个场景。
为了改进这一点,你也可以创建一个 TextHolder 对象的预制体,并编写代码根据 Dialogue List 在场景中实例化。如果你将要制作一个更复杂的对话系统,我建议实施这个更改。
注意
本书代码包中提供的代码示例包括一些代码注释,这里没有显示,因为它们太拥挤,无法在此文本中显示。
翻译对话
让我们扩展我们的动画文本示例,使其包括翻译。这是一个基本示例,用于演示如何访问文本组件的某些属性,并且不一定是以适合大型项目的方式架构的。
注意
请注意,我使用了 Google Translate 来获取这些翻译,因此它们可能不完全准确:

Figure 10.39: Google Translate
要将翻译添加到我们之前完成的动画文本示例中,请完成以下步骤:
-
打开
DialogueBox.cs脚本,并将以下命名空间添加到脚本顶部:using System.Collections.Generic; -
让我们向此脚本添加一个子类,该子类将根据适当的设置分组所有翻译。我们将使用此子类来存储有关翻译文本以及适当字体的信息:
[System.Serializable] public class Translation { public string languageKey; public string translatedString; public Font font; public FontStyle fontStyle; }languageKey字符串将用作键来查找适当的翻译。 -
现在,添加一个列表来存储所有翻译:
public List<Translation> translations;您的
DialogueSystem组件现在应该更新为以下样子:![]()
Figure 10.40: The Dialogue System component with translations
- 让我们在
translations列表中添加一些翻译。我们将使用 ISO 639-1 双数字代码作为每种语言的键。将以下四个键代码添加到翻译列表中,以表示西班牙语、日语、简体中文和韩语:

Figure 10.41: The translation keys filled out on the Dialogue Boxes
-
为了节省输入这些键的时间,通过右键单击“I'm a cat and, for some reason, I'm collecting food!”元素,通过右键单击并选择粘贴来复制此列表,并将其粘贴到其翻译中。
-
现在,让我们开始翻译。将以下数据输入到 Translated String 属性中:
Key Hello there! I’m a cat and, for some reason, I’m collecting food! es ¡Hola! ¡Soy un gato y, por alguna razón, estoy recolectando comida! ja こんにちは! 私は猫で、なぜか食べ物を集めています! zh 你好呀! 我是一只猫,出于某种原因,我正在收集食物! ko 안녕! 나는 고양이고, 왠지 모를 음식을 모으고 있다! 表 10.5:需要输入的翻译字符串
你会注意到 Unity 引擎能够在检查器中渲染这些语言。
-
我们现在需要为每种语言分配一些字体。当前的字体
Milky Coffee* 不支持这四种语言。如果我们尝试用这些翻译中的任何一个替换文本,引擎将使用支持它的字体渲染任何非支持符号,但将所有支持符号(如标点符号)渲染在Milky Coffee字体中。这将导致风格不一致,实际上看起来并不好。例如,如图所示,逗号和感叹号与文本的其余部分不在同一字体中:![图 10.42:使用两种字体渲染韩文文本的对话框]()
图 10.42:使用两种字体渲染韩文文本的对话框
因此,每当发生翻译时,我们希望更改字体为我们特别选择的字体。我将使用
ZCOOL KuaiLe字体进行简体中文翻译,而其他所有语言则使用RocknRoll One字体。从以下位置下载字体并将其添加到你的
Assets/Fonts文件夹: -
将正确的字体分配给每个翻译。你的 Dialogue Boxes 列表中的两个元素应如下所示:
![图 10.43:两个对话框,所有属性已完成]()
图 10.43:两个对话框,所有属性已完成
我已经包括了更改字体样式的选项,但在这个例子中,我将它们都设置为 正常。
-
让我们编写将翻译我们的文本的代码。将以下变量声明添加到
DialogueSystem.cs脚本顶部,使其成为第一个变量声明。这将用于确定游戏在运行时应该显示哪种语言。[SerializeField] string currentLanguage; -
将以下方法添加到脚本底部:
private void Translate() { foreach (DialogueBox dialogueBox in dialogueBoxes) { int index = dialogueBox.translations.FindIndex(x => x.languageKey == currentLanguage); if (index >= 0) { dialogueBox.dialogue = dialogueBox.translations[index].translatedString; dialogueBox.textDisplayBox.font = dialogueBox.translations[index].font; dialogueBox.textDisplayBox.fontStyle = dialogueBox.translations[index].fontStyle; } } }此方法将找到具有
currentLanguage变量指定的键的翻译之一。然后,它将更改dialogue变量、font和fontStyle为适当的值。如果currentLanguage变量在任何languageKey中找不到,则index将等于-1,并且不会实施任何更改。 -
为了确保翻译发生,我们需要从
Awake()方法中调用该方法。在你的Start()方法上方添加以下内容:void Awake() { Translate(); } -
返回编辑器并将
es输入到 Current Language 槽中。你会看到当你按下播放时,对话框现在会翻译并更改字体。然而,显示文本存在问题。你会注意到文本在第二个面板中被截断:

图 10.44:西班牙语翻译被截断
-
回想一下,从翻译文本部分,你应该确保你的字体不仅能渲染文本,而且文本框有足够的空间来容纳翻译后的文本,这可能会更长(尤其是在渲染为不同字体时)!最简单的方法是使用层次结构中的
Text对象,然后从它们的文本组件中选择最佳匹配设置。 -
选择
18后,这将阻止文本变得过大。
在此示例中使用最佳匹配的一个缺点是,随着文本的动画,字体大小会发生变化。这并不理想。但是,为了保持文本框大小,我们别无选择——目前是这样!在我们学习了第十二章中的滚动矩形和遮罩之后,我们可以在滚动文本时保持字体大小。
自定义字体
让我们暂时放下构建场景的工作,来探索制作自定义字体的过程。我们不会在我们一直在工作的场景中使用这个自定义字体,但创建自定义字体的过程仍然很重要。我们将使用以下精灵来创建一个显示数字 0 到 9 的自定义字体:

图 10.45:我们将创建的自定义字体
创建字体的精灵是从opengameart.org/content/shooting-gallery找到的免费艺术资产中修改的。
为了创建一个均匀分布的精灵表,我使用了 TexturePacker 程序和 Photoshop。这个过程可以用像 Photoshop 这样的照片编辑软件完全完成,但 TexturePacker 简化了过程。TexturePacker 可以在www.codeandweb.com/texturepacker找到。
创建自定义字体的过程既耗时又有点痛苦。要创建自定义字体,你必须为每个你打算用该字体渲染的字符输入坐标位置,因此我不建议用于除了数字或非常有限的字符集之外的其他任何东西。
如果你想要一个具有更丰富字符集的自定义字体,请查看 Unity 资产商店中关于简化位图字体过程的多种选项。
要创建前面图中显示的自定义字体,请完成以下步骤:
-
在你的
Assets/Fonts文件夹中创建一个名为Custom Fonts的新文件夹。 -
在代码包中找到
customFontSpriteSheet.png文件,并将其拖放到你在步骤 1中创建的文件夹中。精灵表看起来如下所示:![图 10.46:自定义精灵表]()
图 10.46:自定义精灵表
当手动创建自定义字体时,你的字符必须均匀分布。这将使你在输入单个字符设置时生活变得更加容易。你可以将精灵图集的导入设置保留在精灵(2D 和 UI)纹理类型和单精灵模式的默认值。
-
自定义字体需要一个材质来渲染。通过在
Custom Fonts文件夹中右键单击并选择CustomFontMaterial来创建一个新的材质。 -
选择
CustomFontMaterial以将其customFontSpriteSheet.png拖放到主贴图中Albedo旁边的方形中。一旦这样做,材质的预览图像应该会更新:

图 10.47:自定义字体材质
- 现在,我们需要更改材质的着色器。你可以使用几种不同的着色器选项:你可以使用UI | 默认或任何无光照或无光照 UI 选项。我的偏好是使用GUI | 文本着色器,因为我通常在正确渲染方面运气最好,并且我更喜欢它在项目视图中的显示方式:

图 10.48:自定义字体材质更新
-
现在,我们可以创建我们的自定义字体。在
Custom Fonts文件夹中,右键单击并选择CustomFont。 -
选择
CustomFont并将CustomFontMaterial分配给默认材质槽。 -
目前在
1下的属性。一旦我们有了所有字符将重复的属性,我们将将其更改为10。当你设置1时,你应该看到元素 0的属性出现:

图 10.49:自定义字体的字符矩形
-
所有字符重复的第一个属性是UV W和UV H。这些值表示单个字符占据精灵总宽度和总高度的百分比。由于我们的字符在精灵图中均匀分布,这些值对所有字符都相同。如果你的字符大小均匀,你可以按以下方式计算这些值:
![图 10.50:计算 UV W 和 UV H 值]()
图 10.50:计算 UV W 和 UV H 值
总共有五列字符。因此,每个精灵占据精灵宽度的一半。五分之一等于 20%或 0.2 作为小数。我们需要在
0.2的1/5处放入0.2。总共有两行字符。因此,每个精灵占据精灵高度的一半;一半等于 50%或 0.5 作为小数。在槽中的
0.5处键入1/2。 -
每个字符重复的下一个值是H槽中的
-57中的50。记住,H值始终是负数! -
在所有字符中保持一致性的最后一个属性是
51。在完成 步骤 9 至 11 后,你的 元素 0 角色应具有以下属性:

图 10.51:已填写值的元素 0
- 现在我们已经放置了所有重复的属性,我们可以增加
10。你会看到一旦我们这样做,元素 0 的属性就会在 元素 1 到 元素 9 中重复:

图 10.52:通过元素 10 复制的元素 0
-
现在,我们需要指定每个元素的 ASCII 索引。这将把输入的文本与正确的精灵绑定起来。没有这个,字体不知道输入数字 0 应该显示图集中的第一个精灵。以下表格表明数字 0 到 9 是 ASCII 数字 48 到 57:
www.ascii.cl/.因此,我们应该在
0中输入48到57。12345678948495051525354555657
表 10.6:每个元素的索引值
-
下一步是指定 UV X 和 Y 值。这是创建自定义字体花费时间最多的部分。这些值代表字符在精灵图中的 UV 坐标位置。这些数字基于字符所在的行和列号计算得出。行和列号从 0 开始,从左下角开始:
![图 10.53:精灵图的行和列]()
图 10.53:精灵图的行和列
要计算 UV X 和 UV Y 值,请使用以下公式:
![图 10.54:计算 UV X 和 UV Y]()
图 10.54:计算 UV X 和 UV Y
记住,由于我们的字符均匀分布,我们的 UV W 和 UV H 值对每个字符都是相同的。
因此,如果我们看
0,它的0.5。下表表示每个字符应输入的 UV X 和 UV Y 值:
元素 UV X UV Y 000.510.20.520.40.530.60.540.80.550060.2070.4080.6090.80表 10.7:字体的 UV X 和 UV Y 属性
下图提供了坐标模式的更直观表示:
![图 10.55:精灵图坐标]()
图 10.55:精灵图坐标
-
现在,我们可以测试我们的字体,看看它是否工作。在任何场景中,创建一个新的
自定义字体文本。我创建了一个名为Chapter10-Examples-CustomFont的新场景,其中包含我的测试字体。更改0123456789并将CustomFont拖到 字体 槽中。你应该会看到以下内容:

图 10.56:步骤 15 后应显示的自定义字体
- 要正确显示,请扩展文本框的大小以容纳所有字符,并将文本颜色更改为白色:

图 10.57:调整了一些属性后的自定义字体
现在我们已经完成了自定义字体的导入,让我们看看如何进一步调整它。
调整字符间距和更改字体大小
在我们的例子中,所有数字都是均匀间隔的,字体大小不能更改。你可能会希望数字更靠近或使用不同的字体大小。让我们修改我们的自定义字体,使精灵更靠近。以下图显示了调整后的字体与我们在本例最后一部分创建的原始字体的对比:

图 10.58:带和不带间距调整的自定义字体
要更改字符间距,请完成以下步骤:
-
复制
CustomFont,使用Ctrl + D并重命名副本为CustomFontTight。 -
打开
CustomFontTight的检查器。 -
如果你希望数字更靠近,你只需更改特定元素的Advance属性。Advance属性表示从精灵开始到下一个精灵开始的像素数。因此,如果我们想让数字 2 看起来更靠近数字 1,我们可以将Element 1的Advance属性更改为更小的值,如下所示:

图 10.59:1 符号间距问题
注意
你可以在场景中预览结果的同时更改自定义字体的设置。为了在更改自定义字体后使场景中的文本刷新,你必须首先保存场景。
-
调整
012345678938303535353535353835
表 10.8:自定义字体的Advance属性
- 字体现在应该如下所示:

图 10.60:显示字体的所有符号
- 0 和 1 之间的间距仍然有点大;然而,如果你尝试减少 0 的Advance属性以使 1 更靠近,如果其他数字跟在 0 后面,它们将会与 0 重叠。以下示例显示了如果将Element 0的Advance属性减少到 35 会发生什么;1 看起来在 0 后面很好,但 9 与之重叠:

图 10.61:0 符号的不同 Advance 设置
- 记住这一点,我们需要在我们的场景中将 1 号位置靠近一些。为了将 1 号位置靠近,我们需要将
-3改为向左稍微移动一点。这将给字符带来更合适的间距。
现在,如果您想调整字体大小,您不能通过改变0.5来改变自定义字体的大小。
如我之前提到的,我们可能不会使用这个字体来构建我们的主示例场景,但创建自定义字体的过程仍然是一个有用的练习。现在,让我们继续创建一些其他常见的 UI 资产——生命条和进度条。
TextMeshPro - 带渐变的扭曲文本
我们当前暂停面板的横幅看起来有点单调。现在我们已经介绍了使用 TextMeshPro - 文本,我们可以创建一个与横幅很好地对齐的漂亮弯曲文本和渐变:

图 10.62:带有渐变和扭曲的横幅文本
要创建前面截图所示的弯曲文本,请完成以下步骤:
-
复制
Chapter 9-Examples场景,并将新场景命名为Chapter 10-Examples。 -
右键点击层次结构中的
Pause BannerUI 图像,并选择Paused TMP以给它添加一个子 Text (TMP)对象。 -
如果您还没有这样做,当弹出提示时,选择导入 TMP Essentials和导入 TMP Examples & Extras。如果您之前只导入了 TMP Essentials,但没有导入示例,请转到文件 | 项目设置 | TextMeshPro并选择导入 TMP Examples & Extras。
-
在
Paused状态下。 -
将字体样式设置为粗体。
-
水平垂直居中文本。
-
设置
43。 -
将字体更改为
Roboto-Bold。(注意,这个字体资产只有在您导入了 TMP 示例后才会可用。) -
调整 Rect Transform,使文本在横幅中居中。您的文本应该如下所示:

图 10.63:横幅文本的位置
-
现在,让我们给文本添加渐变填充。在TextMeshPro - 文本(UI)组件中,选中颜色渐变旁边的复选框。
-
为了达到预期的效果,我们将保持左上角和右上角的颜色为白色。选择左下角的白色矩形以打开颜色选择器。在颜色选择器的顶部选择吸管,然后将鼠标移到
Pause Panel图像的米色区域。当您关闭颜色选择器窗口时,米色将出现在左下角的槽中。

图 10.64:使用吸管工具获取文本颜色
-
在左下角的槽中的颜色上右键点击并选择复制。
-
在右下角的空白方块上右键点击并选择粘贴。现在,上面两个颜色应该是白色,下面两个颜色应该是米色。

图 10.65:完成颜色渐变属性
-
在
0.25。 -
最后一步是弯曲文本。TextMeshPro 通过提供一个在运行时弯曲文本的示例脚本,使我们能够轻松地完成这项工作。要查看更改,您必须播放游戏,它们在场景视图中不会显示。选择添加组件按钮,然后选择脚本 | TMPro.Examples | Warp Text Example。

图 10.66:Warp Text Example 组件
- 在顶点曲线槽中选择波浪形绿色线以打开曲线编辑器。选择最左侧的选项——即平坦的线条。

图 10.67:选择顶点曲线的平坦曲线
-
在
0.5标记处的绿色线上右键单击并选择添加关键帧。 -
选择这个新键并将其向上拖动到
1.3稍下方。

图 10.68:顶点曲线的最终版本
-
从此场景开始游戏并按P键查看
Pause Panel。文本现在应该像本例开头那样显示。 -
要查看整个流程,包括起始屏幕、简介场景以及带有更新
Pause Panel的此场景,我们需要更新一些旧场景。打开Chapter10-Examples-IntroScene并将Text Canvas的Chapter9-Examples更改为Chapter10-Examples。 -
复制名为
Chapter9-Examples-StartScreen的场景并将其命名为Chapter10-Examples-StartScreen。 -
打开新场景并选择
Button Canvas下的Play Button子项。 -
更改
Chapter10-Examples-IntroScene。 -
打开Build Settings并更新构建中的场景到以下内容:
![图 10.69:包含所有适当场景的 Build Settings]()
图 10.69:包含所有适当场景的 Build Settings
您可以通过右键单击并删除它们来删除不必要的场景。
-
在
Chapter10-Examples-StartScreen场景打开的情况下,按编辑器中的播放按钮并观看游戏流程完成。
注意
我想指出,虽然我在每个新章节中复制并重命名每个场景,但您不必这样做。我这样做是为了保持一个易于查看的进度日志,记录这本书中场景的变化,但诚然,如果您也这样做,您的项目可能已经有点杂乱了。所以,如果您想知道“为什么我不能继续添加到我的场景中,而不是每次都创建一个新的场景?”您当然可以!
使用 TextMeshPro 创建包裹文本就到这里了!
摘要
哇!我敢打赌,当你开始这一章时,你可能会想,“关于文本你能说多少呢?”而且你没想到会面对迄今为止最长的章节!我几乎通过添加更多示例来让它变得更长!然而,尽管我非常愿意提供更多示例,但我必须遵守页面限制——尽管我已经超过了这个限制。如果你渴望更多关于 UI 文本和 Text-TextMeshPro 的示例,我强烈建议你回顾这一章中链接的各种示例,以及你与 TextMesh – Pro 示例一起下载的示例场景。
在下一章中,我们将深入探讨 UI 图像和效果。
第十一章:UI 图像和效果
我们在之前的章节中已经使用过 UI 图像,但现在我们将学习更多关于组件的特定属性,以及如何通过代码访问组件。我们还将查看一些可以应用于我们的 UI 对象以增强视觉吸引力的 UI 效果组件。虽然我们将彻底研究这些组件,但本章的大部分内容将专注于您在视频游戏中(尤其是移动视频游戏中)会发现的具体的 UI 功能示例。
在本章中,我们将讨论以下主题:
-
创建 UI 图像并设置其属性
-
使用各种 UI 效果组件进一步自定义我们的图形 UI
-
实现水平和圆形进度条
-
如何创建不使用内置过渡效果的按钮,例如静音/取消静音按钮
-
添加按住/长按功能
-
创建屏幕上的四方向虚拟 D-Pad
-
创建一个浮动的八方向虚拟摇杆
注意
在“示例”部分之前展示的所有示例都可以在代码包中提供的 Unity 项目中找到。它们位于标记为第十一章的场景中。
每个示例图都有一个标题,说明场景中的示例名称。
在场景中,每个示例都在自己的 Canvas 上,其中一些 Canvas 被禁用。要查看禁用 Canvas 上的示例,只需在检查器中选择 Canvas 名称旁边的复选框。每个 Canvas 也都有自己的事件系统。如果您同时激活多个 Canvas,这将导致错误。
技术要求
您可以在此处找到本章的相关代码和资产文件:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2011
UI 图像组件属性
我们之前创建了一个 UI 图像,但现在让我们看看它的属性和组件。
您可以使用 + | UI | Image 创建一个新的 UI 图像对象。
UI Image 对象包含 Rect Transform 和 Canvas Renderer 组件以及 Image 组件。我们已经详细地研究了 Rect Transform 和 Canvas Renderer 组件;现在,让我们看看 Image 组件。
图像组件的第一个设置是源图像属性,它表示将要渲染的精灵。颜色属性表示正在渲染的精灵的基本颜色。将颜色设置为白色会使图像看起来与精灵完全一样,但更改颜色将为图像添加带有色调的颜色叠加。您还可以通过降低 alpha 值来更改图像的不透明度。材质属性允许您向图像添加材质。
射线投射目标和射线投射填充属性与文本组件上的工作方式相同,通过指定图像是否会阻止其后面的 UI 对象的点击以及是否有任何填充来阻止。可遮罩属性确定图像是否可以被遮罩影响。
当精灵被分配到源图像槽位时,在图像组件下的图像类型中会出现新的选项,如图下所示:

图 11.1:UI 图像组件及其所有属性
让我们看看图像类型的各种选项以及它们如何影响精灵。
图像类型
图像类型属性确定由源图像指定的精灵将如何显示。有四种选项:简单、切片、平铺和填充。让我们来看看它们。
简单
当图像的图像类型属性设置为简单时,图像在精灵上均匀缩放。这是默认类型。当选择简单作为图像类型时,会出现一个标签为使用精灵网格的切换按钮,一个标签为保持纵横比的切换按钮,以及一个标签为设置原始大小的按钮。
选择使用精灵网格切换按钮将使图像使用由TextureImporter创建的精灵网格。默认情况下,此属性未选中,精灵的网格是一个四边形。如果你不希望图像以矩形表示,而是希望它有一个紧密围绕图像可见区域的网格,则将选择此属性。
当保持纵横比属性被勾选时,精灵将以保留其部分的方式显示,可能不会看起来填充整个 Rect Transform 区域。选择此属性确保你的精灵看起来与最初意图一致,并且不会被拉伸。
选择设置原始大小按钮将图像的尺寸设置为精灵的像素尺寸。
切片
切片图像被分成九个区域。当切片图像被缩放时,图像的所有区域都会缩放,除了角落。这允许你在不扭曲角落的情况下缩放图像。这对于你想要拉伸成圆角矩形的具有圆角精灵特别有效。
当图像设置为切片时,会出现填充中心和每单位像素乘数属性。下图显示了一个圆角矩形及其五个变体被拉伸的情况。你可以看到选择切片如何允许圆角矩形以保持圆角矩形形状的方式拉伸,而将图像类型保持在简单则会在缩放图像时导致边缘扭曲。

图 11.2:第十一章场景中的切片图像类型示例
您必须在精灵或精灵图的精灵编辑器中指定九个区域的位置。如果您没有指定区域,图像组件中将会显示一条消息。
要在精灵编辑器中指定区域,您需要将精灵边缘的绿色框拖动到所需位置。从以下屏幕截图可以看出,您想要拖动绿色线条,使它们停止围绕边缘的曲线:

图 11.3:在精灵编辑器中指定精灵的九个区域
接下来,让我们谈谈图像类型下的平铺选项。
平铺
选择平铺作为图像类型将导致图像重复以填充拉伸区域。以下图示演示了选择简单和平铺作为图像类型如何影响缩放后的图像:

图 11.4:第十一章场景中的平铺图像类型示例
接下来,让我们谈谈图像类型下的填充选项。
填充
对于图像类型选择填充的图像,将从指定方向的原点填充精灵的一部分,超过指定百分比的任何精灵部分将不会渲染。当选择填充时,将显示新的属性:

图 11.5:填充图像的属性
0.75或 75%:

图 11.6:第十一章场景中的填充图像类型示例
当您看到水平和垂直****填充方法选项在动作中的表现时,它们是相当直观的,但仅从外观上确定三种径向方法的确切工作方式可能有点困难。径向 90将径向的中心放置在角落之一,径向 180将径向的中心放置在边缘之一,而径向 360将径向的中心放置在精灵的中心。
填充 图像类型选项也有设置原始大小属性。
现在我们已经探讨了 UI 图像组件,我们可以看看一些 UI 效果组件。
UI 效果组件
三个效果组件允许您为文本和图像对象添加特殊效果:阴影、轮廓和作为 UV1 定位。它们都可以在添加组件 | UI | 效果下找到。让我们逐一查看每个组件,从阴影组件开始。
阴影
阴影组件为您的文本或图像对象添加简单的阴影。

图 11.7:阴影组件
您可以使用0更改阴影的颜色和透明度,使其不可见,但基于效果 颜色属性上指定的透明度,阴影仍然可见。
下图显示了阴影组件的一些示例:

图 11.8:第十一章场景中的阴影组件示例
所有四个香蕉都设置了相同的0透明度值,但由于阴影组件上未选择使用图形透明度,阴影没有随着香蕉变暗,并保持在指定的透明度值。
轮廓
轮廓组件通过在其周围指定距离处创建四个阴影来模拟图形的轮廓。

图 11.9:轮廓组件
轮廓组件将根据效果距离 X在原始图形的左侧和右侧创建两个阴影,根据效果距离 Y在原始图形的顶部和底部创建两个阴影。与阴影组件不同,这两个距离的正负值没有区别,因为每个轴上创建的两个阴影是镜像的。
设置-3实际上只是交换了两个水平阴影的位置,但效果看起来相同,如下图所示:

图 11.10:第十一章场景中的轮廓组件示例 1
使用图形透明度属性在这个组件上的作用与在阴影组件上相同,如下所示:

图 11.11:第十一章场景中的轮廓组件示例 2
接下来,让我们看看位置作为 UV1组件。
位置作为 UV1
位置作为 UV1组件允许您更改 Canvas 渲染的 UV 通道。如果您想创建使用烘焙光照图的自定义着色器,则使用此功能。

图 11.12:位置作为 UV1 组件
很遗憾,自定义着色器是一个相当复杂的话题,超出了本文的范围,所以我就不再深入探讨这个组件的用法了。
现在我们已经回顾了 UI 图像组件和一些 UI 效果组件,让我们看看我们可以如何使用这些组件的示例。
示例
在本章中,我们将通过添加一些新的 UI 元素来进一步扩展我们一直在构建的场景。我们还将探讨一些移动/触摸屏 UI 和交互。
这些示例中的一些可能更适合按钮章节,但既然它们包括了访问图像组件属性的权限,我就把它们放在这里。
注意
我们已经创建了两个场景,它们将加载到我们一直在构建的场景中:一个开始屏幕和一个简介场景。由于我一直复制我们的主场景以使每个章节的进度跟踪变得容易,因此我们的简介场景将不会导航到我们在本章和未来章节中进行的更新,除非我们继续更新简介场景中对话框组件的下一场景变量,并将新场景包含在我们的构建设置中。
我不会在步骤中包含这个更新,因为场景导航不再是这些示例的重点。然而,它将包含在每个章节完成场景中我包含的包中。
水平和圆形生命值/进度条
让我们回到我们的主场景。复制Chapter10-Examples场景以创建一个Chapter11-Examples场景。
在本节中,我们将介绍如何创建两种类型的进度条,一个是水平的一个是圆形的,如下面的截图所示:

图 11.13:水平和垂直进度条的示例
我们将连接圆形和水平进度条,使它们都显示相同变量的进度,并且我们可以同时观察它们的变化。
圆形进度条实际上并不适合我们一直在构建的主场景,我们将在本章之后将其隐藏,但圆形进度条是常见的游戏元素,因此我认为在本章中包含一个如何实现它们的示例很重要。
水平生命值条
创建水平生命值条有几种不同的方法,但最快、最简单的方法是根据百分比缩放单个轴。以这种方式设置水平生命值条时,确保锚点设置在表示完全耗尽的条的位置非常重要。
记得在第六章中,我们设置了生命值条的锚点为左侧,因此我们已经正确设置了锚点。我们还沿x方向缩放了生命值条,以显示条在耗尽时的样子。

图 11.14:生命值条的矩形变换组件
现在,我们只需要将百分比与生命值条的X 缩放值关联起来。
要将生命值条的填充与实际值关联,请完成以下步骤:
-
在你的
Scripts文件夹中创建一个新的 C#脚本,并将其命名为ProgressMeters.cs。 -
在
ProgressMeters脚本中,初始化以下四个变量:public uint health; [SerializeField] uint totalHealth; [SerializeField] float percentHealth; [SerializeField] RectTransform healthBar;health变量表示玩家的当前生命值,而totalHealth变量表示玩家可以获得的总生命值。由于这些值不应该是负数,它们已经被初始化为uint类型或正整数。我将health变量设置为public,以便可以通过其他脚本访问并在检查器中查看。我将totalHealth设置为私有的SerializeField,这样它就不能通过其他脚本访问,但仍然可以通过检查器查看和分配。percentHealth变量将基于health和totalHealth变量的商来计算。我将此值设置为私有并序列化,不是为了在检查器中编辑它,而是为了可以轻松地在检查器中看到其值的变化。healthBar变量存储了我们场景中Health BarUI Image 的RectTransform组件。
注意
由于 RectTransform 继承自 Transform,我们本可以将 healthBar 声明为 Transform,并且下面的代码仍然可以工作。
- 返回 Unity 编辑器,将
ProgessMeters脚本拖放到HUD Canvas>Top Left Panel。将值500分配给 Health Bar 槽中的Health BarUI Image。你尝试输入到 Percent Health 槽中的任何值都将被我们在下一步中编写的代码覆盖。你的组件应该如下所示:

图 11.15:进度条组件
-
我们希望对
health值的任何更改都能自动更新percentHealth值和healthBar的比例。为此,我们可以在Update()函数中放置以下代码:void Update() { // Cap health if (health > totalHealth) { health = totalHealth; } // Calculate health percentage percentHealth = (float)health / totalHealth; // Update horizontal health bar healthBar.localScale = new Vector2(percentHealth, 1f); }使用
uint类型声明我们的health和totalHealth变量阻止了它们变成负数,但我们仍然需要给health变量设置一个上限。它不应该超过totalHealth变量。虽然
percentHealth是一个float类型的变量,但两个uint类型的变量之间进行除法运算将得到一个uint类型的结果,因此在整数除法前添加(float)可以得到一个浮点数结果。代码的最后部分设置了
healthBar的localScale值。当你缩放 UI 对象时,你必须使用localScale。这意味着相对于其父对象进行缩放。 -
现在,我们可以在编辑器中轻松测试代码。玩游戏并将鼠标悬停在 Progress Meters 组件中的 Health 字样上,直到鼠标显示围绕它的两个箭头,如图所示:

图 11.16:进度条组件对仪表的影响
当这些箭头出现时,点击并拖动将根据您的鼠标位置操纵变量的值。您会看到,当您这样做时,场景中的“生命值条”大小会变化。您会注意到您不能设置 0 或以上 500 的值。
如您所见,设置水平生命值条并不特别困难。在游戏中通过事件减少生命值时重复此过程不需要很多步骤即可实现。只需确保正确设置生命值条的锚点。这个过程对于垂直生命值条也类似有效。
圆形进度条
水平生命值条设置起来并不需要太多工作。制作圆形进度条的工作几乎一样简单,只需再添加两行代码即可完成。由于我们场景中还没有圆形进度条,我们首先需要做一些设置。
要创建圆形进度条,请完成以下步骤:
-
从代码包中,将
circularMeter.png精灵拖入项目中的Sprites文件夹。 -
将
circularMeter.png精灵设置为 多重 并在 精灵编辑器 中自动切片。 -
在层次结构中选择“左上角面板”,并为其添加一个新的 UI Image 子项。将 Image 命名为
Progress Holder。 -
与我们设置生命值条的方式类似,将会有一个持有者和一个填充。将
circularMeter_0子精灵拖入Progress Holder。 -
我们需要为我们的持有者和填充图像获取正确的比例非常重要。因此,为了确保图像比例正确,点击 设置原始大小 按钮在 图像 组件上。
-
现在,向
Progress Holder添加一个名为Progress Meter的子 UI Image。 -
将“进度条”的锚点预设设置为居中。不要拉伸它。
-
将
circularMeter_1添加到Progress Meter。 -
同时点击“进度条”的“图像”组件。完成此步骤后,您应该看到以下内容:
![图 11.17:进度条上的进度]()
图 11.17:进度条上的进度
如果粉红色填充没有完美地嵌入蓝色持有者中,您可能忘记点击“进度条”的锚点预设没有设置为居中。
-
让我们移动这个仪表并稍微缩放一下。选择
Progress Holder并将其移动到场景中位于Character Holder下方。将Progress Holder设置为0.8以使其稍微小一些。

图 11.18:重新定位圆形进度条
- 对代码的最后一件事情是更改
Progress Meter。将 图像类型 更改为 填充 并使用 径向 360 填充方法。将 填充起源 更改为 顶部。调整 填充量 上的滚动条以预览仪表填充:

图 11.19:调整圆形进度条上的填充量
-
现在,我们准备好编写一些代码。正如您可能从调整代码中
fillAmount值到percentHealth变量中猜到的。首先,我们需要创建一个变量,我们可以通过它访问Progress Meter的图像组件。在代码顶部声明以下变量:[SerializeField] Image progressMeter; -
现在,在
Update()函数的末尾添加以下内容:// Circular progress meter progressMeter.fillAmount = percentHealth; -
我们最后需要做的是将
Progress MeterUI 图像连接到progressMeter变量。将Progress Meter拖动到 进度 条 槽中。

图 11.20:进度条组件的更新
- 播玩游戏,并在检查器中调整 健康 值,就像您之前做的那样,并观察两个仪表同时移动。

图 11.21:进度条的最终结果
如您所见,制作圆形进度条实际上并不比制作水平进度条更困难!
注意
与我们之前使用的圆形进度条的填充量相同,我们也可以使用水平生命条上的填充量。将图像类型属性设置为填充和水平,然后影响填充量值而不是缩放,会产生类似的效果。
由于这个圆形进度条不是原始 UI 计划的一部分,只是为了演示目的而放置在场景中,我将在所有未来的图表和屏幕截图中禁用它。
带有精灵交换的静音按钮
现在,让我们看看一个基于预定义状态交换按钮精灵的例子。这与我们在第九章中讨论的精灵交换过渡不同,因为它不会使用高亮、按下、选中或禁用状态。它包含在本章而不是按钮章节中,因为它涉及影响图像组件,而不是按钮组件。
在场景中,我们有一个在按下键盘上的 P 键时弹出的 Pause Panel。在这个面板上,我们将放置两个静音按钮,一个用于音乐,一个用于声音,它们将在静音和取消静音状态之间切换。面板将如以下屏幕截图所示:

图 11.22:带有新静音按钮的暂停面板
要添加前面屏幕截图所示的音效和声音按钮,请完成以下步骤:
-
首先,我们需要引入一个新的艺术资产。我们之前导入的精灵表中的按钮精灵有点太小,并且不包含静音版本。因此,我稍作编辑,并为您提供了一个新的精灵。在本书的源文件中,您应该找到一个名为
muteUnmute.png的.png文件:![图 11.23:muteUnmute.png 精灵]()
图 11.23:muteUnmute.png 精灵
将此
.png文件导入到项目文件夹Assets/Sprites中。 -
通过将 Sprite Mode 改为 Multiple,打开其 Sprite Editor 并应用自动切片类型,将精灵切割成多个子精灵。多个精灵应如下所示:

图 11.24:muteUnmute.png 精灵切片
- 创建两个新的按钮作为
Pause Panel的子项,并分别命名为Music Button和Sound Button。删除它们的文本子项,因为我们不需要它们。

图 11.25:当前层次结构视图
-
给这两个新的按钮以下 Rect Transform 和 Image 属性:
![图 11.26:两个按钮的 Rect Transform]()
图 11.26:两个按钮的 Rect Transform
您的面板现在应该看起来就像本例开头的那样:
![图 11.27:暂停面板]()
图 11.27:暂停面板
-
现在,让我们编写一些代码来使这些按钮交换代表声音和音乐开关的精灵。在您的
Assets/Scripts文件夹中创建一个新的脚本,命名为MuteUnmute.cs。将
MuteUnmute的代码替换为以下内容:public class MuteUnmute : MonoBehaviour { [SerializeField] Button musicButton; private Image musicImage; [SerializeField] private Sprite[] musicSprites = new Sprite[2]; private bool musicOn = true; [SerializeField] Button soundButton; private Image soundImage; [SerializeField] private Sprite[] soundSprites = new Sprite[2]; private bool soundOn = true; void Awake() { musicImage = musicButton.GetComponent<Image>(); soundImage = soundButton.GetComponent<Image>(); } public void ToggleMusic() { musicOn = !musicOn; musicImage.sprite = musicSprites[Convert.ToInt32(musicOn)]; } public void ToggleSound() { soundOn = !soundOn; soundImage.sprite = soundSprites[Convert.ToInt32(soundOn)]; } }
如您所见,此代码包含两个主要函数:ToggleMusic() 和 ToggleSound()。这两个函数通过根据 musicOn 和 soundOn 布尔值在指定的按钮上交换精灵而完全相同。
要交换精灵,脚本首先在 Awake() 函数中找到指定为 musicButton 和 soundButton 的两个按钮上的 Image 组件,这些按钮将在检查器中分配。然后,脚本将 Image 组件的精灵交换到精灵数组中的正确精灵。静音和取消静音状态的精灵将在未来的步骤中在检查器中分配。
注意
很遗憾,这本书没有涵盖向 Unity 项目添加声音和音乐。这里提供的代码实际上并没有静音和取消静音音频;它只是交换精灵。您只需包含两个音频源:一个用于播放音乐,一个用于播放带有 Music 和 Sound 标签的声音。
- 返回 Unity 编辑器,通过将其拖动到其 Inspector 中将
MuteUnmute.cs脚本附加到Pause Panel:

图 11.28:静音/取消静音组件
- 现在,我们想要将适当的按钮和精灵分配到槽位。从层次结构中将
Music Button和Sound Button拖动到它们的指定槽位。从项目视图中拖放音频按钮精灵到它们的适当槽位,确保将静音精灵放在数组的 0 元素中。

图 11.29:更新的静音/取消静音组件
-
现在,我们只需要将按钮连接到调用适当的函数。选择
Music Button。选择MuteUnmute.cs的OnClick()事件列表,位于Pause Panel上,因此将Pause Panel拖入对象槽中。现在,从函数下拉菜单中选择MuteUnmute | ToggleMusic。 -
对
Sound Button执行与之前步骤相同的操作,但这次从函数下拉列表中选择MuteUnmute | ToggleSound。
现在,玩游戏,按下P键调出Pause Panel,你会看到按钮在两个不同的精灵之间切换。
现在我们已经了解了如何实现进度条和精灵交换按钮,让我们看看如何实现一些不同的移动特定交互。
添加按下并保持/长按功能
在移动游戏中,按下并保持(Press-and-hold)操作被频繁使用。许多在 PC 或网页上使用右键点击的游戏,在转换为移动平台时使用按下并保持操作。
为了演示如何实现按下并保持功能,我们将创建一个具有表示保持时间的增长环的按钮。一旦经过指定的时间,就会触发一个函数:

图 11.30:按下并保持按钮示例
在处理这个示例时,重要的是要记住,尽管代码引用了指针,但这种功能并不仅限于鼠标。在触摸屏上放置手指与指针按下功能相同,抬起手指与指针抬起功能相同。
要创建一个表示保持时间的增长环的按钮,请按照以下步骤操作:
-
在
Assets/Scenes文件夹中创建一个名为Chapter11-Examples-Buttons1的新场景并打开该场景。 -
选择+ | UI | Button在场景中创建一个新的按钮。
-
将按钮组件的过渡类型设置为无。
-
将按钮上的文本更改为
Pressand Hold。 -
在层次结构中右键单击
Button,然后选择Button的Image子项。 -
更改
Image的50。 -
将
circularMeter_1精灵分配给Image组件的源图像属性。 -
将
0更改为。 -
要在
Button上创建按下并保持功能,我们将使用Button对象。 -
选择添加新事件类型并选择PointerDown。
-
选择添加新事件类型并选择PointerUp。
-
现在,我们需要实际编写在之前步骤中设置的触发器事件将要调用的函数。在
Assets/Scripts文件夹中创建一个新的脚本,命名为LongPressButton。 -
在打开脚本之前,先将它作为组件附加到
Button上。 -
在脚本顶部添加
UnityEngine.UI命名空间,如下所示:using UnityEngine.UI; -
要检查按钮被按下的持续时间,我们将使用一个布尔变量来检查按钮是否被按下,以及一些与时间相关的不同变量。将以下变量声明添加到您的脚本中:
private bool buttonPressed = false; private float startTime = 0f; private float holdTime = 0f; [SerializeField] private float longHoldTime = 1f;当
holdTime变量确定自startTime以来经过的时间时,buttonPressed变量将被设置为true,而startTime变量将被设置为当前时间。longHoldTime变量是按钮必须按下直到长按完成所需的时间量。它是序列化的,以便可以轻松自定义。 -
我们需要的最后一个变量将表示径向填充图像。将以下变量声明添加到您的代码中:
[SerializeField] private Image radialFillImage; -
现在,我们需要编写一个函数,该函数将由指针按下和指针抬起事件调用:
public void PressAndRelease(bool pressStatus) { buttonPressed = pressStatus; if (!buttonPressed) { holdTime = 0; radialFillImage.fillAmount = 0; } else { startTime = Time.time; } }此函数接受来自事件触发器的布尔变量。然后,它将
buttonPressed的值设置为传递的值。当按钮被释放时,将向函数传递一个值为
false的值。如果传递的值是false,则经过的时间holdTime将被重置为0,radialFillImage图像将被重置为fillAmount值为0。当按钮被按下时,
startTime值将被设置为当前时间。 -
创建一个函数,当指定的
longHoldTime所需的时间完成时将被调用:public void LongPressCompleted() { radialFillImage.fillAmount = 0; Debug.Log("Do something after long press"); }此函数实际上并没有做任何事情,只是重置填充图像并打印出
Debug.Log。然而,您可以在以后重用此代码,并将Debug.Log行替换为更有趣和更有意义的操作。 -
可以使用
Update()函数来使计时器向上计数。按如下方式调整Update()函数:void Update() { if (buttonPressed) { holdTime = Time.time - startTime; if (holdTime >= longHoldTime) { buttonPressed = false; LongPressCompleted(); } else { radialFillImage.fillAmount = holdTime / longHoldTime; } } }如果
buttonPressed的值设置为true,则此代码将使holdTime的值向上递增。记住——当holdTime的值达到longHoldTime指定的值时,buttonPressed将被重置为false,计时器将停止递增。此外,将调用LongPressCompleted()函数。如果尚未达到longHoldTime,图像的径向填充将更新以表示已发生所需总时间的百分比。 -
现在脚本已完成,我们可以将
PressAndRelease()函数连接到按钮上的事件触发器。将静态列表中的PressAndRelease函数添加到两个事件触发器上。PressAndRelease函数接受一个布尔变量,有一个复选框表示应该传递的布尔值。选择true的复选框(但不要选择false)。

图 11.31:事件触发器组件
- 现在,我们需要将
Image分配到长按组件上的径向填充图像槽位。

图 11.32:长按组件
现在玩游戏将演示当你按住按钮时图像径向填充,并在控制台打印Do something after long press。
按住并保持是一个相当常见的功能,虽然它不是 Unity 事件库中预安装的事件,但幸运的是,它并不太难连接。我建议保留这个脚本,以便将来可以重用它。
创建一个静态的四方向虚拟 D-Pad
D-Pad 简单来说就是方向板上四个按钮。要为移动游戏创建一个 D-Pad,你只需要创建一个包含四个按钮的图形。
本例中使用的艺术作品来自opengameart.org/content/onscreen-controls-8-styles。
要创建一个虚拟 D-Pad,请完成以下步骤:
-
在
Assets/Scenes文件夹中创建一个名为Chapter11-Examples-Buttons2的新场景并打开新场景。 -
将
dPadButtons.png精灵图集导入到Assets/Sprites文件夹。 -
将新导入的精灵的Sprite Mode更改为Multiple并自动切割。
-
创建一个新的 Canvas,并命名为
D-Pad Canvas。 -
在移动设备上,D-Pad 的大小非常重要。即使屏幕变得更小,你可能也希望 D-Pad 的大小保持不变。如果它太小,游戏可能无法玩或感觉不舒服。因此,将Canvas Scaler组件的UI Scale Mode值设置为Constant Physical Size。
-
将一个新的 Image 作为
D-Pad Canvas的子对象添加,并命名为D-Pad Background。 -
设置
dPadButtons_4。 -
将其锚点和枢轴设置为屏幕的左下角,并设置其
30。 -
设置其
200:

图 11.33:正确定位的 D-Pad
-
右键单击
D-Pad Background并添加一个新的按钮作为子对象,并命名为Up。 -
删除其子
Text对象。 -
将
Up按钮的值分别设置为0、65、60和60。这将创建一个覆盖 D-Pad Image 向上位置的方块。

图 11.34:带有向上按钮的 D-Pad
-
将
Up按钮复制三次,并将副本重命名为Right、Left和Down。 -
将
Right按钮的65和0分别设置。 -
将
Left按钮的-65和0分别设置。 -
将
Down按钮的0和-65分别设置。现在你应该有四个按钮,位置如下:![图 11.35:带有所有按钮的 D-Pad]()
图 11.35:带有所有按钮的 D-Pad
这四个按钮覆盖了方向板臂的整个区域。它们将作为方向的击中区域。
-
我们只真正需要这四个按钮的点击区域,并不希望在 UI 中实际显示它们。在层次结构中选择所有四个按钮,并将
alpha值设置为0。 -
由于方向盘图像是静态的,并且没有分成四个单独的按钮,因此应用在其上的任何过渡都会覆盖整个图像。然而,我们可以通过为方向上的箭头添加子图像,使单个方向看起来像被按下,并有一种颜色过渡。右键单击
上按钮,并添加一个带有Arrow的图像作为子项。 -
将
dPadButtons_5精灵分配给其Image组件上的源图像,并选择保持纵横比。 -
缩放并移动图像,使其与 D-Pad 背景图像上显示的箭头适当地对齐:

图 11.36:D-Pad 的上箭头
-
选择
箭头图像组件,并使用吸管工具从D-Pad 背景图像中抓取箭头的颜色。这将使其变为浅灰色而不是白色。 -
为其他三个按钮创建
箭头子项,并适当地调整大小、位置和颜色。确保也使用dPadButtons精灵图的正确子图像。完成后,你的 D-Pad 和层次结构应该如下所示:

图 11.37:D-Pad 上的所有箭头
-
现在,为了让 D-Pad 在按下四个方向时在视觉上做出反应,我们将设置四个
箭头子项,使其具有颜色渐变的按钮箭头子项到其目标图形槽在其按钮组件上。现在,当你按下单个按钮时,你会看到箭头颜色的轻微变化,指示哪个方向被按下。如果你发现难以判断是否发生了变化,你可能希望将按下颜色改为比默认灰色更明显一些的颜色。 -
将书中代码包中的名为
DPad.cs的脚本添加到Assets/Scripts文件夹。这是一个非常简单的脚本,它包含四个只写入控制台的功能。将这些功能连接到单个方向按钮不会做任何有趣的事情,但它将允许我们在控制台中看到日志,让我们知道按钮正在按预期执行。 -
将脚本附加到
D-PadBackground对象。 -
选择每个四个方向按钮,在它们全部被选中时,向
按钮组件添加一个On Click ()事件。 -
现在,将
D-Pad 背景对象拖动到On Click ()事件的对象槽中。 -
分别选择每个按钮,并将适当的函数
PressUp、PressDown、PressLeft和PressRight分配给它们的On Click ()事件。
在游戏中玩并选择四个方向按钮应该会在控制台显示适当的消息。
许多 D-Pad 实际上接受九个输入:四个方向,四个对角线(角落),以及中心。如果您想接受对角线输入以及 D-Pad 的中心点击,我建议使用网格布局组来均匀地分配您的九个按钮。
由于 D-Pad 倾向于允许按住,您可能希望将此示例中使用的过程与之前示例中描述的动作结合起来。而不是使用 OnPointerDown 和 OnPointerUp 事件。这些事件可以设置一个布尔变量为 true 和 false。例如,在 Right 按钮上,您可以将 OnPointerDown 事件设置一个名为 moveRight 的变量为 true,并将 OnPointerUp 事件将 moveRight 设置为 false。
创建一个浮动的八向虚拟模拟摇杆
在此示例中,我们将创建一个浮动的八向虚拟模拟摇杆。首先,我们将创建一个八向 D-Pad,模拟一个玩家拖动方向移动的控制杆:

图 11.38:浮动模拟摇杆的位置
然后,我们将扩展八向 D-Pad,使其浮动,这意味着它将在玩家在屏幕上按下某处之前在场景中不可见。然后,它将出现在玩家的拇指位置,并根据玩家的拇指拖动执行八个方向的运动。
设置八向虚拟模拟摇杆
要创建一个如图所示在八个方向上移动的模拟摇杆,请完成以下步骤:
-
在
Assets/Scenes文件夹中创建一个新的场景,命名为Chapter11-Examples-Buttons3并打开新场景。 -
使用
Stick Base创建一个新的图像。 -
将
dPadButtons_15精灵添加到其 Image 组件的 Source Image 槽中。 -
调整大小,使其具有
200并将其设置为0。 -
在层次结构中右键单击
Stick Base,然后选择Stick Base的Image子项。将子项命名为Stick。 -
将
Stick图像调整到与Stick Base相匹配,通过设置其 Rect Transform 拉伸和锚点以在两个方向上完全拉伸。 -
将
dPadButtons_0精灵添加到Stick图像组件中。 -
设置
10以在Stick的边缘周围提供一些填充。 -
现在,设置枢轴和位置,使
Stick在Stick Base上不会移动。

图 11.39:摇杆的 Rect Transform 组件
-
这就是设置虚拟模拟摇杆工作的全部内容。我们现在将其留在屏幕中央。现在,我们需要编写一些代码。在
Assets/Script文件夹中创建一个新的脚本,并将其命名为FloatingAnalogStick。 -
将以下代码添加到脚本顶部的
UnityEngine.UI命名空间中:using UnityEngine.UI; -
要使摇杆在基座上方摇摆,我们需要以下变量:
[SerializeField] private RectTransform theStick; private Vector2 mouseStartPosition; private Vector2 mouseCurrentPosition; [SerializeField] private int dragPadding = 30;前三个变量应该是相当直观的。
dragPadding变量将用于确定玩家需要拖动摇杆多远,它才会实际注册为移动。 -
在我们编写检查玩家拖动手指距离的代码之前,让我们添加一些虚拟函数,这将允许这个模拟摇杆在未来实际上控制某些东西。将以下函数添加到您的脚本中:
public void MovingLeft() { Debug.Log("move left"); } public void MovingRight() { Debug.Log("move right"); } public void MovingUp() { Debug.Log("move up"); } public void MovingDown() { Debug.Log("move down"); } -
当玩家将手指从起始的按下位置移动时,
Stick会向外移动。因此,让我们创建一个函数,当玩家开始拖动手指时,它会找到起始位置。将以下函数添加到您的脚本中:public void StartDrag() { mouseStartPosition = Input.mousePosition; }记住——当使用触摸屏时,
Input.mousePosition将给出触摸位置的价值。 -
现在,让我们创建一个函数,检查玩家拖动手指的距离,并根据该信息移动
Stick。将以下函数添加到您的脚本中:public void Dragging() { float xPos; float yPos; mouseCurrentPosition = Input.mousePosition; if (mouseCurrentPosition.x < mouseStartPosition.x - dragPadding) { MovingLeft(); xPos = -10; } else if (mouseCurrentPosition.x > mouseStartPosition.x + dragPadding) { MovingRight(); xPos = 10; } else { xPos = 0; } if (mouseCurrentPosition.y > mouseStartPosition.y + dragPadding) { MovingUp(); yPos = 10; } else if (mouseCurrentPosition.y < mouseStartPosition.y - dragPadding) { MovingDown(); yPos = -10; } else { yPos = 0; } theStick.anchoredPosition = new Vector2(xPos, yPos); } -
我们需要添加的最后一件事情是,当玩家停止拖动或抬起手指时,将摇杆重置到原始位置的某种东西。将以下函数添加到您的脚本中,以实现这一点:
public void StoppedDrag() { theStick.anchoredPosition = Vector2.zero; } -
现在,我们需要将此脚本和这些函数连接到场景中的项目。将
FloatingAnalogStick脚本添加到StickBase图像。 -
将
Stick图像添加到浮动模拟****摇杆组件的摇杆属性中。 -
使用添加组件 | 事件 | 事件触发添加一个
Stick Base对象。这将允许用户使用除On Click()之外的事件类型。 -
使用添加新事件****类型按钮添加开始拖动、拖动和结束拖动事件类型。
-
在附加到
Stick Base的FloatingAnalogStick脚本上添加适当的函数到事件中:

图 11.40:事件触发组件
如果现在您玩游戏,您应该会看到八方向模拟摇杆适当地响应。点击它并在任何方向上拖动将导致摇杆移动到拖动方向。
使八方向虚拟模拟摇杆浮动
如果您只需要一个八方向模拟摇杆,那么您就可以开始了!但如果您想让模拟摇杆浮动——出现在玩家点击屏幕的位置,当玩家抬起手指时消失——您需要做更多的工作。
要使模拟摇杆出现在玩家点击的位置,完成以下步骤:
-
首先,我们需要创建一个区域,玩家点击该区域将弹出模拟摇杆。在层级中右键点击
Canvas,然后选择Canvas的Button子项。将Button子项重命名为Click Area。 -
从
Click Area中移除Text子对象。 -
将
Click Area拉伸以填充整个Canvas。 -
通过更改
50为一些填充来给点击区域的两侧添加一些填充。我添加了这个填充,以便玩家不能点击屏幕的边缘,并且模拟杆大部分出现在屏幕外。 -
在层次结构中,将
点击区域移动到杆基之上。现在,点击区域将在模拟杆之后渲染:

图 11.41:显示点击区域和杆基的层次结构
-
打开你的
FloatingAnalogStick代码,以便我们可以向其中添加一些功能。 -
为了使我们的模拟杆的位置更容易与屏幕上鼠标的位置挂钩,我们应该将杆基移动到
Canvas的左下角中心。将锚点和位置设置为 底部左侧 锚点预设。 -
现在,将
0.5设置为: -
设置
0。这应该将模拟杆放置在画布(或屏幕)的左下角,其支点设置为中心:

图 11.42:显示点击区域和杆基的层次结构
-
我们现在需要两个额外的变量,以便将模拟杆放置在场景中我们想要的位置。将以下变量添加到你的脚本中,以分配杆并跟踪它是否已被添加:
[SerializeField] private RectTransform theBase; [SerializeField] private bool stickAdded = false; -
现在,创建以下函数以将杆添加到场景中:
public void AddTheStick() { theBase.anchoredPosition = Input.mousePosition; theStick.anchoredPosition = Vector2.zero; mouseStartPosition = Input.mousePosition; stickAdded = true; } -
添加以下
Update()函数以确定杆何时出现:void Update() { if (stickAdded == true) { Dragging(); if (Input.GetMouseButtonUp(0)) { // ToggleBaseCanvasGroup(false); // This line is commented out as ToggleBaseCanvasGroup is not defined in the provided code stickAdded = false; StoppedDrag(); } } } -
将
杆基添加到 基础 插槽:

图 11.43:浮动模拟杆组件
-
添加
点击区域。 -
添加以下
点击区域:![图 11.44:事件触发组件]()
图 11.44:事件触发组件
现在玩游戏时,模拟杆将出现在你点击的位置,并随着你的拖动而移动。
-
现在,让我们让它这样,模拟杆仅在玩家触摸屏幕时可见。添加一个
杆基。 -
设置
0、false和false。 -
将以下变量添加到你的
FloatingAnalogStick脚本中,以跟踪CanvasGroup:private CanvasGroup theBaseVisibility; -
添加以下
Awake()函数以初始化theBaseVisibility变量:void Awake() { theBaseVisibility = theBase.GetComponent<CanvasGroup>(); } -
创建一个名为
ToggleBaseCanvasGroup()的新函数,用于切换CanvasGroup的属性开和关:public void ToggleBaseCanvasGroup(bool visible) { theBaseVisibility.alpha = Convert.ToInt32(visible); theBaseVisibility.interactable = visible; theBaseVisibility.blocksRaycasts = visible; } -
在
AddTheStick()函数中添加以下内容以启用CanvasGroup:ToggleBaseCanvasGroup(true); -
在
Update()函数内部最内层的if语句中添加以下内容以关闭CanvasGroup:ToggleBaseCanvasGroup(false);
现在,当玩家按下时,模拟杆将出现,并朝手指的方向移动,当玩家抬起手指时消失。
摘要
UI 图像是 Unity UI 系统的核心组件之一,操作它们对于创建视觉交互式用户界面至关重要。本章通过让我们创建利用事件、按钮和图像的有趣界面,总结了我们在前几章中学到的所有技能。
在下一章中,我们将探讨如何创建遮罩和滚动视图,以便我们可以在我们的面板容器中容纳更多的子对象。
第十二章:使用遮罩、滚动条和滚动视图
我们已经学习了如何创建所有组件都同时显示在屏幕上的 UI,但通常您会有一些 UI 元素在屏幕之外或菜单之外,直到您导航到它们或揭示它们才可见。
在本章中,我们将讨论以下主题:
-
如何使用遮罩隐藏 UI 图像的部分
-
使用滚动条并通过代码访问它们的属性
-
利用 UI 滚动视图创建可滚动菜单
-
使用遮罩和滚动文本创建设置菜单
注意
本章中展示的所有示例都可以在提供的 Unity 项目中的代码包中找到。它们可以在标记为 Chapter12 的场景中找到。
每个示例图像都有一个标题,说明场景中的示例编号。
在场景中,每个示例都在自己的 Canvas 上,其中一些 Canvas 被禁用。要查看禁用 Canvas 上的示例,只需在 Inspector 中选择 Canvas 名称旁边的复选框。每个 Canvas 也都有自己的事件系统。如果您同时激活多个 Canvas,这将会导致错误。
技术要求
您可以在此处找到本章的相关代码和资产文件:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2012
使用遮罩
遮罩会影响其形状内对象的可见性。如果一个对象受到遮罩的影响,其遮罩限制区域外的任何部分都将不可见。遮罩的可见区域可以由具有 遮罩 组件的图像或具有 Rect Mask 2D 组件的 Rect Transform 确定。
使用 UI,遮罩可用于创建滚动菜单,因此存在于菜单区域之外的项目将不可见。它还用于 裁剪 图像。例如,以下图像显示了一个猫的图像被圆形遮罩裁剪出来:

图 12.1:第十二章场景中的圆形遮罩示例
您可能会注意到带有遮罩的猫的边缘看起来不太好。为了避免这种情况,请确保您使用具有适当图像分辨率的精灵,并在精灵的 导入设置 中尝试不同的过滤模式。
遮罩组件
可以将 遮罩 组件添加到具有 Image 组件的任何 UI 对象。如果将其添加到没有 Image 组件的 UI 对象,则它将无法工作,因为它需要一个 Image 来确定限制区域。
可以通过在 Inspector 中选择 添加组件 | UI | 遮罩 来将 遮罩 组件添加到 UI 对象:

图 12.2:遮罩组件
包含Mask组件的 UI 对象的任何子对象,其可见性将被限制在其Image组件上的Source Image不透明区域内的区域。
在以下图像中,名为Mask的对象是一个带有Mask组件的 UI Image。如图所示,面板左侧的紫色三角形只有部分可见,而右侧的绿色三角形完全可见。绿色三角形完全可见,因为它不是包含Mask组件的 UI Image 的子对象:

图 12.3:第十二章场景中的 Mask 组件示例
您可以选择隐藏定义Mask组件可见区域的Source Image。如果您取消选择Show Mask Graphic,则父对象的Source Image将不可见。重要的是要注意,在Source Image上更改不透明度不会影响Mask组件的功能。
Rect Mask 2D 组件
使用Mask组件可以限制可见区域为非矩形形状。然而,如果您想将可见区域限制为矩形形状,并且不想使用图像来限制可见区域,则可以使用Rect Mask 2D组件。
您可以通过在Inspector中选择Add Component | UI | Rect Mask 2D将Rect Mask 2D组件添加到 UI 对象中,如下所示:

图 12.4:Rect Mask 2D 组件
注意,该组件允许您调整Padding和Softness。
当将Rect Mask 2D组件添加到 GameObject 中时,其子对象的可见性将受到其 Rect Transform 形状的影响。对于Rect Mask 2D组件能够正常工作,父对象上不需要添加Image组件。
在以下图像中,创建了一个Empty UI GameObject,并向其添加了Rect Mask 2D组件。然后给它添加了一个子 UI Image。如图所示,三角形被 Rect Transform 区域所蒙版:

图 12.5:第十二章场景中的 Rect Mask 2D 组件示例
如果您想对一个矩形形状应用蒙版,我强烈建议您使用Rect Mask 2D组件而不是标准的Mask组件,因为它性能更优。
重要的是要注意,正如Rect Mask 2D的名称所暗示的,此蒙版仅适用于 2D 对象。您可以在docs.unity3d.com/Manual/script-RectMask2D.xhtml上了解更多关于其限制的信息。
如我之前所述,蒙版的一个常见用途是创建具有超出可见区域的对象的菜单。创建此类菜单需要滚动条和滚动视图,因此现在让我们看看这些组件。
实现 UI 滚动条
UI 滚动条对象允许用户沿着路径拖动一个手柄。手柄在路径上的位置会影响图像或对象在可用区域内的位置。
如果你从前面的描述中难以理解滚动条,这里有一个带有上下文的简单解释。它最常用于视频游戏中的菜单,这些菜单在可视区域内包含大量信息,而该信息占用的区域小于可视区域。
要创建 UI 滚动条,选择 Sliding Area。Sliding Area 也有一个名为 Handle 的子组件。
Sliding Area 子组件是一个空 GameObject。它的目的是确保其子组件,即 Handle,被正确定位和校准。Handle 是一个 UI Image。它代表滚动条的交互区域。
如果你想要更改滚动条背景和 Handle 的外观,你需要分别更改 Handle 子组件。
滚动条组件
父滚动条对象有一个 Scrollbar 组件。它具有所有交互式 UI 对象共有的属性,以及一些仅限于滚动条的属性:

图 12.6:滚动条组件的属性
滚动条的 0 和 1。滚动条用于移动占用空间大于可视空间的物体。因此,滚动条的位置应容易转换为百分比或 0 到 1 之间的值。
Handle 被分配到这个属性。你会注意到 Handle GameObject 上的 Rect Transform 组件受滚动条的影响。滚动条的 Handle 位置与 Scrollbar 组件的 Value 属性相关联。

图 12.7:Handle GameObject 上的 Rect Transform 组件
方向属性允许你选择滚动条的方向。可用的选项与滑块相同,并相应地翻译:从左到右、从右到左、从下到上和从上到下。
滚动条的 Handle 占用的 Sliding Area。这可以是 0 到 1 之间的任何 float 值。我建议你选择一个与滚动对象的大小成比例的值,这样滚动条的运动感觉会更直观。这意味着可滚动区域越大,滚动条的 Handle 就越小。
步数数量属性用于当你想要滚动条有间隔的、离散的步骤,而不希望它有连续控制运动时。这用于当你想要你的滚动条将滚动对象移动到特定位置时。
通常,你会看到一个由点(如 iOS 设备上识别你正在查看哪个主屏幕的点)表示的滚动区域。这可以通过设置 0 来实现。将此值设置为 0 将允许进行连续控制移动,而不是跳跃的离散步骤。
在本章末尾的 示例 部分提供了创建连续和离散滚动条的示例。
滚动条默认事件 - 值改变(单个)
Handle 被移动了。它可以接受一个 float 参数。
当一个公共函数有一个 float 参数时,它将在函数的 值改变(单个) 事件下拉列表中显示两次:一次在 静态参数 列表中,再次在 动态 float 列表中,如下面的屏幕截图所示:

图 12.8:静态参数和动态 float 方法
如果函数在事件中作为 float 参数被选中。那么事件将只发送那个框内的值。在下面的屏幕截图示例中,将发送给 ScrollbarWithParameter() 函数的唯一值将是 0。

图 12.9:检查器中静态参数方法的示例
如果你希望滚动条的值作为参数发送给一个具有参数的函数,你必须从 动态 float 列表中选择该函数。
以下函数和图像代表了在 Chapter12 场景中找到的滚动条示例,该示例触发调用带参数和不带参数的函数的事件:
public void ScrollbarWithoutParameter(){
Debug.Log("changed");
}
public void ScrollbarWithParameter(float value){
Debug.Log(value);
}
在下面的屏幕截图,第三个选项显示了从 动态 float 列表中选择的功能,并将 值 属性的值作为参数发送给函数:

图 12.10:第十二章场景中的滚动条事件示例
现在我们已经回顾了如何在 Unity 中实现 UI 滚动条,让我们看看 UI 滚动视图。
实现 UI 滚动视图
UI 滚动视图 对象创建了一个可滚动区域,并附带两个子 UI 滚动条。可滚动区域可以通过滚动条、拖动滚动视图内的区域或使用鼠标的滚轮进行滚动。
要创建 UI 滚动视图,请选择 Viewport、Scrollbar Horizontal 和 Scrollbar Vertical。Viewport 对象还有一个名为 Content 的子对象。Scrollbar Horizontal 和 Scrollbar Vertical 子对象与默认 UI 滚动条的关系相同,如前文所述。
尽管 UI 滚动视图默认带有两个 Scrollbar 子对象,但你不必在 Scroll View 中使用这两个滚动条。实际上,你甚至不需要使用滚动条!有关更多详细信息,请参阅 Scroll Rect 组件 部分。
视口子对象是一个 UI 图像,其视口应用了一个遮罩到滚动视图内的一个区域:

图 12.11:UI 滚动视图视口
这将使滚动视图内的项目仅在定义的区域内(滚动条之间)可见。您不能更改大多数的视口,因为这个区域是基于您在滚动视图的滚动矩形组件中设置的滚动视图设置来确定的(有关更多详细信息,请参阅实现 UI 滚动条部分)。
视口的子对象是一个名为内容的空矩形变换。这将成为您希望在滚动视图内放置的所有项目的容器。您可以将内容视为将在滚动视图内移动的东西。如您从以下图像中看到的,内容的矩形变换比由视口定义的可视区域大,因为滚动视图的目标是使项目超出可视区域。

图 12.12:UI 滚动视图内容
要向滚动视图添加项目,您只需将子对象添加到内容对象中。由于内容是视口的子对象,其任何子对象都将受到视口的影响。
以下示例显示了作为内容子对象的四个图像。内容还添加了一个垂直布局****组组件:

图 12.13:第十三章场景中的滚动视图示例
在前面的图像中,您可以看到当视口启用时,所有项目都是可见的。当您设置滚动视图时,我强烈建议您禁用视口,以便您可以看到您放置的项目的一般布局,并在完成所有项目的布局后重新启用它。
您还应调整内容的矩形变换区域,以包含其所有子项,或者使用内容不完整地包含所有项目,这样在滚动滚动视图时可能不会显示其可视区域外的项目。
滚动视图包含一个滚动视图(包围所有内容的灰色矩形),更改滚动视图。您可以通过调整Scrollbar Horizontal和Scrollbar Vertical子对象的显示外观来更改滚动条的显示外观,如实现 UI 滚动条部分所述。
现在我们已经了解了滚动矩形的意图和设置,让我们来看看滚动矩形组件的各个属性。
滚动矩形组件
滚动视图的行为由滚动视图父对象上的滚动矩形组件决定:

图 12.14:滚动矩形组件
注意,滚动矩形组件没有本章(以及前几章)中所有其他 UI 组件都有的交互性、过渡或导航属性!
我将按顺序讨论属性,以便更容易讨论。
默认情况下是内容。默认情况下是视口。
如果您正在创建一个不使用 UI 滚动视图的可滚动区域,则必须分配视口和内容属性,并且分配给视口的矩形变换必须是分配给内容的矩形变换的父级,以便滚动矩形组件正常工作。
移动属性
有三个属性与滚动视图中的内容如何移动相关。滚动视图在其边界处移动。
有三个内容。
当内容。一旦达到内容矩形变换的边缘。然而,当内容。
当内容被拖动超过其边界时,玩家停止拖动后,它将弹回原位。如果使用滚动轮,也会发生弹跳。当选择弹性时,子属性弹性变为可访问。弹性属性决定了弹跳的强度。
当选择固定作为移动类型时,内容将无法拖动超过其边界,并且不会发生弹跳。
与滚动速度相关的属性
选择内容后,在玩家停止拖动后继续移动。内容的移动由滚动条或鼠标滚轮初始化。当玩家停止拖动后,内容将停止移动。一个0将在玩家停止拖动时立即停止内容,而一个1将永远不会停止。默认值为0.135。
滚动灵敏度属性决定了每次滚动轮转动时内容将移动多远。数字越高,内容每次转动移动的距离就越远,看起来移动得越快。
如果您想禁用鼠标滚轮在滚动视图中的使用,请设置0。
滚动条属性
您可以为您的水平和垂直滚动条的反应方式分别设置属性。分别是Scrollbar Horizontal子项和Scrollbar Vertical子项。
如果您只想在滚动视图区域使用拖动,而不想使用滚动条,您只需将None设置为属性,或者简单地从场景中删除Scrollbar Horizontal和Scrollbar Vertical对象。
在每个滚动条分配下,您可以设置相应滚动条的可见性和间距属性。
可见性属性有三个选项:永久显示、自动隐藏和自动隐藏并扩展视口。当为可见性属性选择永久显示时,相应的滚动条将始终可见,即使不需要,如果允许其相应的移动。例如,如图所示,如果允许水平移动,并且将水平滚动条的可见性设置为永久显示,则相应的滚动条将可见,尽管这不是必要的(无法实现水平移动):

图 12.15:调整滚动矩形中的滚动条
然而,参考同一图像,你可以看到如果水平移动被禁用,将水平滚动条的可见性设置为永久显示,将完全从滚动视图中移除。它也在层次结构中禁用。
当为可见性属性选择自动隐藏时,如果不需要(意味着在该方向上不需要移动)或如果相应的轴移动被禁用,相应的滚动条将在游戏播放时变得不可见并在层次结构中禁用。
Viewport对象有一个遮罩组件,这将导致遮罩区域扩展以覆盖最初由滚动条占据的区域。
-3,这意味着两个矩形变换略微重叠。如果你想要改变Viewport的位置,你必须使用这个属性,因为与Viewport位置相关的属性在其矩形变换组件中被禁用。
滚动矩形默认事件 – 值更改(Vector2)
滚动矩形组件的默认事件是Scroll View的Content区域通过拖动、使用鼠标滚轮滚动或使用其中一个滚动条进行滚动。它接受一个Vector2位置作为参数,并且,就像本章中讨论的其他事件一样,你可以选择传递无参数、静态参数或动态参数。
如果你想要将Content的Vector2位置发送到函数,你需要将其发送到一个具有Vector2参数的函数,从相应的坐标中的1.0开始,到最后位置在相应坐标中的值为0.0。
让我们考虑以下函数:
public void ScrollViewWithParameter(Vector2 value)
{
Debug.Log(value);
}
如果从控制台中打印的Vector2值中选择前面的功能:

图 12.16:基于位置的滚动矩形值
现在我们已经查看了一些 UI 元素,让我们看看一些实现它们的示例。
示例
我在本章前面提到,遮罩和滚动视图的常见用法是包含多个项目的菜单。因此,在本章中,我们只创建一个示例,即通过模仿滚动视图 UI 对象的布局,从一个现有的菜单创建一个滚动视图。
从现有菜单创建滚动视图
为了帮助组织项目,复制你在上一章中创建的Chapter11-Examples场景;将其重命名为Chapter12-Examples。打开Chapter12-Examples并在其中完成以下示例。
我想能够向我的Inventory Panel添加更多项目,并允许玩家滚动查看这些项目。我可以创建一个新的 UI 滚动视图项目,并更新它以看起来像当前的Inventory Panel,但这会比值得的麻烦更多。所以,相反,我将当前的Inventory Panel转换为可滚动视图。完成后,它将看起来如下所示:

图 12.17:我们将构建的可滚动菜单
此面板可以通过拖动食物项旁边的区域来调整其视图。我本可以添加一个垂直滚动条来控制移动,但我想要展示如何在不使用滚动条的情况下创建可拖动区域。而且,没有滚动条看起来也更美观。
要使Inventory Panel可滚动,完成以下步骤:
-
目前,
Pause Panel挡在路上了。让我们通过取消选中其Inventory Panel中的复选框来更容易地禁用它。 -
为了使可滚动视图真正工作,我们需要在库存中添加更多项目。选择名为
Item Holder的Inventory Holder的所有子项,并使用Ctrl + D进行复制。现在,所有复制的项目都被选中,将它们重命名为Item Holder,这样就不会给每个项目添加编号后缀。现在,你的Inventory Holder中应该有两倍的项目,你应该看到以下内容:

图 12.18:第二步后的结果
-
要获得可滚动的视图,我们需要在场景中添加一些额外的项目,这些项目可以成为我们想要滚动的内容的父项。每个项目都需要特定的组件来创建可滚动的视图,你需要遵循非常具体的父/子关系,如下面的图所示:
![图 12.19:创建可滚动菜单的层次布局]()
图 12.19:创建可滚动菜单的层次布局
让我们从将包含
Inventory Panel的对象开始,右键单击并选择Inventory Panel。 -
将新项目重命名为
Scroll Rect,并在Inventory Banner中重新定位它。 -
给
Scroll Rect游戏对象添加Scroll Rect组件,在它的检查器中选择添加组件 | UI | Scroll Rect。 -
现在,让我们创建一个将包含
Scroll Rect的项目,并选择Scroll Rect。 -
将新的图像重命名为
Viewport。 -
Inventory Holder现在在视觉上挡住了,所以暂时禁用它。 -
将
uiElements_38图像添加到Viewport中。 -
调整
滚动矩形的 Rect Transform,使其填充其父级的 Rect Transform。

图 12.20:滚动矩形对象的 Rect Transform
- 调整
Viewport的 Rect Transform 属性,如下所示:

图 12.21:滚动矩形对象的 Rect Transform
-
给
Viewport添加遮罩组件,通过添加组件 | UI | 遮罩。 -
重新启用
Inventory Holder并将其重命名为Content。 -
将
Content的透明度设置为0,以便背景不可见。你应该看到以下内容:

图 12.22:步骤 11 的结果
- 现在,将
Content拖动到Viewport中。这样做应该会立即将遮罩应用到Content的所有子元素上:

图 12.23:步骤 12 的结果
- 将
Content定位,使列表顶部的食物完全位于遮罩区域内:

图 12.24:如何定位 Content
-
现在剩下的工作就是设置
滚动矩形上的属性,并将Content和Viewport拖到滚动``矩形组件的适当槽位中。 -
禁用水平移动,因为我们只想让菜单垂直移动。将移动类型设置为固定,这样菜单就不会拉伸和弹跳,并保持在适当的范围内。你的滚动矩形组件现在应该如下所示:

图 12.25:分配 Content 和 Viewport
- 重新启用
Pause Menu对象,以便我们的Pause Menu在游戏中能够正常工作。
如果你玩游戏,现在Inventory Panel应该有当你拖动它们时可以滚动的物品。记住,你可以通过按键盘上的I键打开Inventory Panel。由于我们在单个物品上实现了拖放功能,为了滚动,我们必须拖动Content上没有食物项的区域。
概述
本章介绍了如何隐藏 UI 元素的可见性以及使用滚动视图创建容器,这些容器可以容纳比屏幕一次能显示的更多物品。然而,仍然还有相当多的可交互 UI 元素。在下一章中,我们将探讨 Unity UI 系统提供的剩余交互 UI 元素。
第十三章:其他可交互 UI 组件
最受欢迎的可交互 UI 组件是按钮。然而,除了按钮之外,还有多种类型的可交互 UI 元素。如果您想到了您最近填写过的在线表单,您可能已经与按钮、文本字段以及可能是一个单选按钮或复选框进行了交互。虽然从技术上讲,所有这些可交互项都可以使用 UI 按钮、UI 文本和一些自定义代码来开发,但您不必自己构建它们!Unity 在 uGUI 系统中包含了多个常用可交互 UI 元素,既可以作为可编辑的 GameObject,也可以作为可以添加到现有 GameObject 中的组件。
本章将回顾 uGUI 系统附带的所有其他预构建 UI 元素。在回顾了按钮和文本章节之后,这些对象的属性中的大多数您都应该熟悉,但每个可交互项都有一些仅限于该 UI 元素类型的属性,因此我们将重点关注这些属性。
本章将讨论以下主题:
-
使用 UI 开关
-
使用 UI 滑块
-
使用 UI 下拉菜单和 Dropdown – TextMeshPros
-
使用 UI 输入字段和输入字段 – TextMeshPros
-
创建带有图像的下拉菜单
注意
本节中展示的所有示例都可以在代码包提供的 Unity 项目中找到。它们位于标记为 Chapter13 的场景中。
每个示例图像都有一个标题,说明场景中的示例编号。
在场景中,每个示例都在自己的 Canvas 上,其中一些 Canvas 已被禁用。要查看已禁用的 Canvas 上的示例,只需在 Inspector 中选择 Canvas 名称旁边的复选框。每个 Canvas 也都有自己的事件系统。如果您同时激活多个 Canvas,这将导致错误。
技术要求
您可以在此处找到本章的代码:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2013
使用 UI 开关
UI 开关 对象是一个带有标签的可交互复选框。要创建 UI 开关,请选择 + | UI | Toggle。

图 13.1:UI 开关 GameObject 及其子项
默认情况下,UI 开关有两个子项:一个 Background 和一个 Label。Background 也有一个子项,即 Checkmark。
Background 子项是一个表示 Checkmark UI 图像出现的“框”的 UI Image。Label 是一个 UI 文本对象。
如果您想更改框和勾选标记的外观,您需要更改 Background 和 Checkmark 子项上 Image 组件的源图像。
开关组件
父级切换对象有一个切换组件。切换组件看起来与按钮组件非常相似,并且具有许多相同的属性。正如您在本章中将会看到的,所有可交互 UI 对象的前几个属性都是相同的。组件底部的属性是仅属于 UI 切换对象的独有属性(图 13.2):

图 13.2:切换组件的独特属性
是否开启属性决定了当切换在场景中初始化时是否被选中。
切换过渡属性决定了当切换在开启和关闭或选中和非选中之间转换时会发生什么。有两个选项:无和淡入淡出。无过渡将立即在勾选图像可见和不可见之间切换,而淡入淡出过渡将使勾选图像淡入淡出。
图形属性分配了将显示勾选标记的图像组件。勾选标记子组件的图像组件将自动分配给此属性,但您可以更改它,如果您愿意的话。
最后一个属性,组,分配了将定义切换所属的切换组组件(如果有的话)。
切换组件的默认事件是值改变事件,正如在值改变(布尔值)部分所看到的。
切换默认事件 – 值改变(布尔值)
切换组件的默认事件是值改变事件,正如在切换组件的值改变(布尔值)部分所看到的。此事件将在切换被选中或取消选中时触发。它可以接受布尔参数。
当一个公共函数有一个布尔参数时,它将在函数下拉列表中的值改变(布尔值)事件中出现两次:一次在静态参数列表中,再次在动态布尔值列表中,如下面的截图所示:

图 13.3:ToggleWithParameter 方法的静态和动态版本
如果从ToggleWithParameter()函数中选择,则该函数将为假(因为复选框已被取消选中)。
如果您想将切换的.isOn值传递到脚本中,则必须在事件的下拉列表中选择动态布尔值(而不是静态参数)列表中的函数。
为了演示值改变(布尔值)事件的工作方式,让我们看看以下两个函数在被调用时的响应:
public void ToggleWithoutParameter(){
Debug.Log("changed");
}
public void ToggleWithParameter(bool value){
Debug.Log(value);
}
在第十三章场景中添加到切换组件的事件如下:

图 13.4:第十三章场景中切换事件示例的事件
当场景中的切换被取消选中时,以下内容将在控制台中打印:
changed
False
False
当切换被选中时,以下内容将在控制台中打印:
changed
False
True
由于从第一个事件调用的函数没有参数,它将在切换的值改变时始终执行,而不管执行时切换的值是什么。
第二个事件将始终打印False的值,因为函数有一个参数,并且由于事件是从False中选择的。因此,False的值始终被发送到函数。
第三事件的功能是从切换更改到的.isOn值中选择的。
切换组组件
切换组组件允许你拥有许多协同工作的 UI 切换,其中一次只能选择或开启一个。当切换位于同一切换组中时,选择一个切换将关闭其他所有切换。为了使切换组在开始时正常工作,你应该将切换组内的所有切换的开启属性设置为False,或者只将单个切换的开启属性设置为True。
切换组组件不会创建可渲染的 UI 对象,因此将其附加到空 GameObject 上不会创建任何可见元素。
一旦切换组组件附加到一个 GameObject 上,该 GameObject 必须被拖动到每个将被包含在组内的切换的组属性中:

图 13.5:切换组组件属性
切换组组件上只有一个属性:允许关闭。允许关闭属性允许在切换处于开启状态时将其关闭。请记住,切换组组件强制每次最多只有一个切换处于开启状态。因此,允许关闭属性被关闭将强制始终至少有一个切换被选中。
使用此组件时,我的建议是使用一个空 GameObject 作为你希望分组在一起的切换的父项。然后,这个空 GameObject 将包含切换组组件(如第十三章场景中的切换组示例所示)。包含切换组组件的对象必须随后分配到每个切换子项的切换组件上的组属性。
UI 滑块
UI 滑块对象允许用户沿着路径拖动把手。路径上的位置对应于一系列值。
要创建一个 UI 滑块,选择背景、填充区域和把手滑动区域。填充区域也有一个子项,即填充,把手滑动区域有一个子项,即把手。
背景子项是一个 UI 图像,它代表了滑块把手可以遍历的整个区域。在默认的滑块示例中,这是被填充的较暗灰色背景区域。
填充区域子对象是一个空的游戏对象。其主要目的是确保其子对象,即填充,正确对齐。填充是一个基于滑块值的 UI 图像。在默认的滑块示例中,这是跟随手柄并在背景中填充的浅灰色区域。
处理滑动区域子对象也是一个空的游戏对象。其目的是确保其子对象,即手柄,正确定位和校准。手柄也是一个 UI 图像。手柄代表滑块的交互区域。
如果您想改变滑块的外观,您需要更改背景、填充和手柄子对象上的源图像的图像组件。
滑块组件
父滑块对象有一个滑块组件。它具有所有交互式 UI 对象共有的属性,以及一些仅限于滑块的属性,如下面的图所示:

图 13.6:滑块组件的独特属性
填充矩形属性分配显示填充区域图像的对象的矩形变换。默认情况下,这是填充游戏对象的变换组件。您会注意到在填充的矩形变换组件上显示了一条消息,指出某些值由滑块驱动。这表明这些值是根据滑块组件来改变的。在播放场景时,如果您移动滑块的手柄,您将看不到填充的矩形变换属性更新。然而,如果您在游戏视图中移动手柄时使场景视图可见,您将看到填充的矩形变换区域随滑块的变化而变化。
手柄矩形属性分配显示手柄图像的对象的矩形变换。默认情况下,手柄的矩形变换被分配给此属性。您会注意到手柄游戏对象上的矩形变换组件也有某些值由滑块驱动的消息,因为手柄的位置受到滑块的影响。
滑块表示的值范围由最小值和最大值属性确定。您可以分配任何值给最小值和最大值属性,甚至负数。虽然检查器允许您将最小值定义为大于最大值的数字,但如果这样做,滑块将无法正常工作。
方向属性允许您选择滑块的朝向。可用的选项包括从左到右、从右到左、从下到上和从上到下。每个方向中位置顺序代表滑块值范围的第一个位置(或最小值)和最后一个位置(或最大值)。
如果选择整数属性,滑块可以表示的值范围将限制为整数(非小数)值。
注意
由于我是一名数学老师,我觉得有必要指出这一点。在数学中,术语“整数”代表所有非负整数(从 0 到无穷大)。在这里,在滑动条组件中,术语“整数”代表所有整数,包括负数。因此,如果你像我一样是个数学爱好者,不要让这一点让你认为如果选择了整数属性,滑动条就不能持有负值。
值属性是滑动条的值。滑动条的把手位置与该属性相关联。检查器中与值属性相邻的滑动条是对场景中滑动条一对一的表示。
滑动条默认事件 – On Value Changed (Single)
滑动条组件的默认事件是“值改变”事件,如滑动条组件的“值改变(单值)”部分所示。每当滑动条的把手被移动时,此事件都会触发。它可以接受一个浮点数参数。
如果你希望将滑动条的值作为参数传递给一个具有参数的函数,你必须从动态浮点数列表中选择该函数(类似于从切换的动态布尔列表中选择函数)。
以下函数和截图展示了在 Chapter7Text 场景中找到的滑动条示例,该示例触发调用带有和没有参数的函数的事件:
public void SliderWithoutParameter(){
Debug.Log("changed");
}
public void SliderWithParameter(float value){
Debug.Log(value);
}
在以下屏幕截图中,第三个选项显示了从动态浮点数列表中选择的函数,并将发送值属性的值作为参数传递给该函数:

图 13.7:Chapter13 场景中滑动条示例的事件
注意
重要的是要注意,如果选择了整数属性并且滑动条只能持有整数值,那么由该事件调用的函数将接收这些整数作为浮点数值。
现在我们已经回顾了如何使用滑动条,让我们回顾如何使用两种类型的下拉菜单。
UI 下拉和 Dropdown – TextMeshPro
有两种下拉 UI 对象可供使用,分别是 Unity 中打包的 UI 下拉对象和 Dropdown—TextMeshPro 对象。这两个下拉对象都允许用户从选项列表中进行选择。当点击下拉按钮时,列表会变为可见。一旦从列表中选择了一个对象,列表将折叠,使所选选项在下拉菜单中可见(如果需要的话)。
这两个下拉选项在功能上几乎相同。两者之间的唯一区别是 UI 下拉使用 UI 文本对象来显示文本,而 Dropdown—TextMeshPro 使用 Text - TextMeshPro 对象。因此,我将在这个部分同时讨论这两个对象。此外,由于这两个对象在功能上相同,如果你想包含“花哨”的文本,你需要使用 Dropdown—TextMeshPro 而不是 UI 下拉。
要创建 UI 下拉,请选择 + | UI | 下拉。要创建 TextMeshPro 下拉,请选择 + | UI | 下拉 - TextMeshPro。如以下截图(图 13**.8)所示,两个下拉对象具有相同的父/子对象关系和名称。默认情况下,下拉对象有三个子项:一个标签、一个箭头和一个模板。模板子项默认禁用(因此,在层次结构中显示为灰色),并且有多个子项。
本章的下拉模板部分讨论了模板子项及其所有子项。

图 13.8:两种下拉类型的层次结构
在以下段落中,我将讨论所有文本对象,就好像它们是 UI 文本对象一样。然而,请记住,下拉—TextMeshPro 使用 TextMeshPro - 文本对象。
标签子项是一个 UI 文本对象。默认情况下,它显示下拉对象中代表所选选项的文本。当玩家更改所选选项时,标签的文本组件的文本属性会变为相应的选项。要更改显示在下拉框区域内的文本的属性,请更改标签上的文本组件的属性。当新的文本替换标签内的文本时,它将根据标签的文本组件设置的属性自动显示。
箭头子项是一个 UI 图像。它的唯一功能是持有下拉右侧(默认情况下)出现的箭头图像。这个箭头实际上并不做任何事情,它只是一个图像。它不接受输入或随下拉组件的属性而变化。
下拉的后台图像位于主下拉父对象上,而不是名为背景的子对象上。因此,如果您想更改下拉背景和箭头的外观,您需要更改下拉父对象和箭头子对象上的图像组件的源图像。下拉的图像仅影响可以选中以显示下拉菜单的矩形。当玩家与下拉交互时向外展开的菜单的背景由模板(在以下下拉 模板部分中讨论)处理。
下拉模板
在我们讨论下拉组件的各种属性之前,让我们更仔细地看看下拉的模板。
下拉名为模板的子项允许您设置将作为下拉菜单中的选项出现的“项目”的属性。它还允许您设置菜单的背景属性以及当列表超出下拉菜单的可视区域时将出现的滚动条的属性。
请记住,模板子项默认禁用。通过在检查器中选择复选框来启用模板,将在场景中显示模板。

图 13.9:启用 Dropdown 的模板 GameObject
你可以在你的编辑器中永久启用此功能,因为一旦你进入播放模式,它就会隐藏,正如它应该的那样。
如果你仔细查看模板在层次结构中的父/子关系,你会注意到它只是一个带有单个滚动条的 UI 滚动视图对象:

图 13.10:下拉菜单内带有单个滚动条的 UI 滚动视图
查看模板 GameObject 的检查器,你会发现它实际上只是一个 UI 滚动视图对象,因为它附有一个带有水平****滚动条的滚动矩形组件,但没有分配任何水平****滚动条:

图 13.11:模板的滚动矩形组件
模板滚动视图对象的内容有一个名为Item的单个子对象。Item有三个子对象:Item 背景、Item 复选标记和Item 标签。如果你查看Item的检查器,你会发现它只是一个 UI Toggle,并且具有与本章开头讨论的 UI Toggle 相同的子对象和属性。
因此,所有的模板都是一个带有单个滚动条和单个切换作为其内容的滚动视图!一开始看起来要复杂得多,但当你分解出各个单独的部分时,你会发现它只是我们之前讨论过的几个 UI 元素的组合!

图 13.12:模板子组件的分解
当使用下拉菜单模板工作时,如果你想更改视觉属性和设置,只需记住前面图中显示的分解,编辑它的前景将显得不那么令人畏惧。
你设置在下拉菜单中出现的每个项目选项都将遵循你为项目切换设置的完全相同的视觉属性。
下拉菜单组件
现在我们已经分解了模板,我们可以查看下拉菜单组件的属性。
父级 Dropdown 对象有一个下拉菜单(或 Dropdown - TextMeshPro)组件。它具有所有其他交互式 UI 对象的共同属性,以及一些仅限于下拉菜单的属性:

图 13.13:两个下拉菜单组件之间的区别
如前图所示,UI 下拉菜单和 Dropdown - TextMeshPro 的属性几乎相同。只有两个主要区别。首先,UI 下拉菜单对象使用 UI Text 对象,而 Dropdown - TextMeshPro 对象使用 Text - TextMeshPro 对象。其次,Dropdown - TextMeshPro 有一个占位符属性。
下拉组件实际上非常强大。它处理与下拉菜单的所有交互,将切换文本显示,打开和关闭下拉菜单,并在下拉菜单内移动复选框。它甚至添加滚动条和手柄,以便下拉菜单能够显示非常长的列表。唯一必须由您编写的代码是如何解释玩家选择的属性。
让我们回顾一下两个下拉对象的各种属性。
标题属性
有两个属性与标题或当前选中的选项相关(图 13**.3)。
无(文本)。
标题图像属性包含将显示当前选中选项图像的 GameObject 的图像组件。默认情况下,此属性未分配,您会注意到下拉没有可以持有图像的子项。要显示带有文本的图像,您必须创建一个 UI 图像并将其分配给标题图像属性。最好是将您创建的 UI 图像创建为下拉的子项。
模板属性
有三个属性与将模板属性分配给所有可能的选项以在下拉列表中显示相关。
模板属性引用模板的 Rect Transform。如前所述,模板定义了下拉列表中每个选项的外观以及下拉容器的外观。默认情况下,此属性分配给子模板对象。
项目文本属性引用包含项目模板文本的 GameObject 的文本组件。默认情况下,项目标签(项目的子项)上的文本组件分配给此属性。
项目图像属性引用包含项目模板图像的 GameObject 的图像组件。默认情况下,此属性未分配,类似于标题图像。就像标题图像一样,要使用此属性,需要创建并分配一个 UI 图像到标题图像属性。如果您创建了它,请确保将其添加到模板子项中的项目作为子项,以避免混淆。
选项属性
值属性表示当前选中的选项。选项在一个列表中,值属性中的数字表示当前选中选项在列表中的索引。由于选项由它们的索引表示,第一个选项的值为 0(而不是 1)。
选项属性列出了下拉菜单中的所有选项。在列表中,每个选项都有一个文本字符串和精灵(可选)。此列表中的所有字符串和精灵将自动交换到下拉组件子项的正确组件属性中,基于下拉组件的属性。因此,您无需编写任何代码即可确保当玩家与下拉菜单交互时,这些项目能够适当地显示。
默认情况下,选项 列表包含三个选项。但是,您可以通过选择列表底部的加号和减号来添加或删除选项。您还可以通过拖放选项的手柄(两条水平线)在列表内重新排列选项。请注意,重新排列列表中的选项将更改它们在列表中的索引,然后更改发送到 下拉列表 组件的 值。
下拉列表默认事件 - 值更改(Int32)
下拉列表组件处理与下拉菜单本身的全部交互。您唯一需要编码的是如何解释玩家选择的选项。
下拉列表组件的默认事件是 值更改 事件,如 下拉列表 组件的 值更改(Int32) 部分所示。每当玩家选择新选项时,此事件都会触发。它接受一个整数作为参数,并且与其他章节中讨论的其他事件一样,您可以选择传递无参数、静态参数或动态参数。
如果您想将所选选项的索引(或 值 属性的值)发送到函数,您需要将其发送到具有 Int32 参数的 动态整数 列表中的函数。请参阅文本末尾的 创建带有图像的下拉菜单 示例,了解其实施方法。
我们接下来要审查的下一个交互式 UI 组件是 UI 输入字段。
UI 输入字段
UI 输入字段提供了一个玩家可以输入文本的空间。
要创建 UI 输入字段,请选择 + | UI | 输入字段。默认情况下,输入字段 GameObject 有两个子组件:一个占位符和一个文本对象。
占位符子组件是一个 UI Text 对象,代表玩家输入任何文本之前显示的文本。一旦玩家开始输入文本,占位符 GameObject 上的 Text 组件将停用,使文本不再可见。默认情况下,占位符显示的文本是 输入文本…,但可以通过影响占位符的 Text 组件上的属性轻松更改显示的文本及其属性。
文本子组件是一个 UI Text 对象,用于显示玩家输入的文本。在 Text 对象的 Text 组件上设置属性将改变玩家输入的文本显示。
输入字段包含一个 图像 组件。如果您想更改输入框的外观,请更改输入字段上的 图像 组件的 源图像。
输入字段组件
父输入字段对象有一个 输入字段 组件。它具有所有交互式 UI 对象的通用属性,以及一些仅限于输入字段的属性:

图 13.14:输入字段组件的独特属性
让我们来看看 UI 输入字段的各个属性。
输入文本和屏幕键盘的属性
输入字段组件中的许多属性会影响输入字段中显示的文本。由于一些属性有很多选项和相关信息,我将按顺序稍后讨论它们。
请记住,要更改输入文本的视觉样式,您需要更改 Text GameObject 上的文本组件的属性。
文本组件属性引用将显示玩家输入文本的 GameObject 的文本组件。默认情况下,这是 Text 子对象的文本组件。
文本属性是当前输入到输入字段的文本。当您尝试从输入字段检索数据时,您希望从该属性而不是 Text GameObject 上的文本组件获取信息。Text GameObject 上的文本组件只会存储当前显示的内容。因此,如果文本以星号显示,因为它是一个密码或已滚动,则完整的正确文本将不会存储在 Text GameObject 的文本组件中。
0允许无限文本输入。
占位符属性引用当玩家没有输入任何内容或已清除所有输入文本时显示文本的 GameObject 的文本组件。默认情况下,这是占位符子对象的文本组件。
隐藏移动输入属性允许您覆盖当选择文本输入字段时弹出的默认移动键盘。如果您有自己的键盘希望玩家使用,您将选择此选项。目前,这仅适用于 iOS 设备。
如果您想在 Android 设备上使用自己的键盘,您最好的选择是创建自己的自定义输入字段脚本。该脚本会在输入框被选中时显示键盘,并根据自定义键盘按键更改框内的文本。
只读属性使输入字段内的文本静态且不可由玩家编辑。当此属性激活时,玩家仍然可以选中文本进行复制和粘贴。
当选择只读属性时,输入字段显示的文本仍然可以通过访问输入字段组件上的文本属性通过代码进行编辑。但是,更改 Text GameObject 上的文本组件的文本属性,不会更改显示的文本。
内容类型
内容类型属性决定了输入字段将接受哪些字符类型。在显示屏幕键盘的设备上,它还会影响当输入字段被选中时设备显示的键盘。如果所需的键盘不可用,将显示默认键盘。例如,如果设备没有仅数字的键盘,它将显示默认键盘。有关每个键盘和与这些内容类型一起提供的字符验证的更详细说明,请参阅键盘类型和字符验证选项部分。
可能的选项有标准、自动更正、整数数字、小数数字、字母数字、名称、电子邮件地址、密码、PIN和自定义。
标准选项允许输入任何字符。请注意,然而,对于输入文本的字体不可用的字符将不会显示。
自动更正选项的工作方式类似于标准选项,但在具有屏幕键盘的设备(尤其是触摸屏键盘)上,它允许设备的自动更正功能根据其自己的自动更正算法自动覆盖单词。
整数数字选项仅允许整数值(正数和负数,没有小数)。玩家将被限制只能输入一个负号。小数数字选项的工作方式类似,但除了接受小数点外,它还接受。玩家将被限制只能输入一个小数点。在具有屏幕键盘的设备(尤其是移动设备)上,这两个选项将显示数字键盘而不是标准键盘。
字母数字选项仅允许字母(大写和小写)、数字以及输入。不允许数学符号和标点符号,包括负数和小数点(句号)。
名称选项将自动将字段中输入的每个新单词的首字母大写。玩家可以选择通过删除字母并以小写重新输入来将单词的第一个字母小写。
电子邮件地址选项允许玩家输入电子邮件地址。它还将限制玩家只能输入一个@符号或两个连续的点(句号/小数点)。
密码选项允许在字段中输入字母、数字、空格和符号。当玩家将文本输入到密码输入字段时,输入的文本将隐藏起来,并以星号(*****)显示。
PIN选项允许输入整数数字(没有小数)。负数是可接受的。玩家在具有PIN 内容类型的字段中输入的文本将以与密码选项隐藏玩家输入相同的方式被隐藏。在具有屏幕键盘的设备上(尤其是屏幕键盘设备),将显示数字键盘,并带有PIN选项。
最后一个选项自定义让您对输入类型有最大的控制权。当选择时,检查器中会出现新的属性,允许您选择行类型、输入类型、键盘类型和字符验证。
行类型
行类型选项与内容类型的标准、自动更正和自定义选项一起提供。有三个行类型选项:单行、多行提交和多行换行。所有其他内容类型都自动限制为单行类型。如果玩家可以输入比输入字段可见区域可以显示的更多文本(即字符限制属性不会将其限制在可见空间内),则文本将根据所选的行类型进行滚动。
单行选项仅允许输入的文本显示在一行上。如果文本超出可见的水平空间,文本将水平滚动。如果玩家按下Enter键,输入字段将像文本已被提交一样处理。
多行提交和多行换行选项允许文本在超出可见水平空间时垂直溢出,并在文本超出可见垂直空间时垂直滚动。两种选项之间的区别在于按下Enter键时会发生什么:多行提交将提交文本,而多行换行将开始新的一行。
输入类型
当选择自定义内容类型时,您可以从三种输入类型中进行选择:标准、自动更正和密码。
选择这些不同的输入类型不会更改键盘或提供任何验证,就像同名内容类型一样。例如,密码输入类型将接受Enter键作为新行,并在字段中显示为星号,但在文本属性中实际存储的数据中将接受它作为新行。
标准选项不对输入类型施加任何特殊条件。
自动更正选项适用于具有内置自动更正功能的屏幕键盘平台。此选项允许设备的自动更正根据需要更改文本。
密码选项将文本显示为星号。
键盘类型
当选择自定义内容类型时,您可以选择键盘类型。在具有屏幕键盘的设备上,此属性允许您选择在输入字段被选中时将显示哪个键盘。
可能的选项有默认、ASCII 兼容、数字和标点符号、URL、数字键盘、电话键盘、姓名电话键盘、电子邮件地址、社交、搜索、十进制键盘和一次性代码。
如果所选键盘在目标设备上不可用,设备将显示默认键盘。
默认选项显示设备的默认(字母)键盘。在大多数设备上,此键盘仅显示字母、空格键、退格键和回车(Enter)键。当选择此选项时,玩家将能够切换到带有数字和标点符号键的键盘。
例如,iOS 默认的英语键盘和数字标点符号键盘可以轻松切换,如图下所示:

图 13.15:iOS 默认英语键盘和数字标点符号键盘
ASCII 兼容选项显示带有标准 ASCII 键的设备键盘。此选项可用于限制键盘为英语和类似语言的键盘。此键盘也是一个字母键盘,并且可以选择切换到数字和标点符号键盘。例如,iOS 的 ASCII 键盘在先前的图表中显示,因为它与默认的英语键盘相同。
数字和标点符号选项打开设备的数字和标点符号键盘,并可以选择切换到“字母”键盘。例如,iOS 的数字和标点符号键盘在先前的图表中显示。
URL 键盘选项显示设备的 URL 键盘。此键盘具有句点(.)键、正斜杠(/)键和.com键,代替空格键。例如,以下图像显示了 iOS URL 键盘及其数字/标点符号格式。请注意,URL 键盘的数字/标点符号格式与默认/ASCII 键盘伴随的数字/标点符号格式不同:

图 13.16:iOS URL 键盘及其数字/标点符号格式
数字键盘选项显示带有数字(0-9)和(通常)退格键的设备键盘。此键盘用于 PIN,因此不允许使用其他字符。例如,以下图像显示了 iOS 数字键盘:

图 13.17:数字键盘
电话键盘选项显示与数字键盘相同键的设备键盘,但还包括星号和井号(磅号)键。例如,以下图像显示了 iOS 电话键盘及其符号显示:

图 13.18:iOS 电话键盘及其符号显示
名称电话键盘选项显示设备的“字母”键盘,并可以切换到电话键盘。例如,以下图像显示了 iOS 名称电话键盘的两个视图:

图 13.19:iOS 名称电话键盘的两个视图
电子邮件地址选项显示设备的电子邮件键盘。电子邮件键盘突出显示了@键和句点(.)键以及其他常见的电子邮件地址符号。例如,以下图像显示了 iOS 的电子邮件键盘及其数字/标点符号格式:

图 13.20:iOS 电子邮件键盘及其数字/标点符号格式
社交键盘选项显示设备的社交键盘。此键盘突出显示了常见的社交网络键,如@键和#键。例如,以下图像显示了 iOS 的“Twitter”键盘及其数字/标点符号格式。在 iOS 设备上,此键盘特别称为Twitter 键盘,但它显示在其他社交网络应用,如 Instagram 上:

图 13.21:iOS Twitter 键盘
搜索选项显示网络搜索键盘。此键盘突出显示了空格键和句点键。例如,以下图像显示了 iOS 网络搜索键盘及其数字/标点符号格式:

图 13.22:iOS 网络搜索键盘及其数字/标点符号格式
注意
您可以在iOS 上所有可用的键盘类型列表中查看。
您可以在Android 上所有输入类型列表中查看(不仅限于键盘,但它们包含在列表中)。
字符验证选项
当选择自定义内容类型时,您可以选择希望使用的哪种类型的字符验证。此选项限制了可以在输入字段中输入的字符类型。如果玩家尝试输入不符合限制的字符,则不会在输入字段中插入任何字符。
可能的选项有None、整数、小数、URL、字母数字、名称和电子邮件地址。
字符验证仅检查输入的每个单独字符,以查看它是否允许在字段中使用。它不会检查整个字符串,以查看字符串本身是否有效。例如,如果选择了电子邮件地址,它将不会检查它是否实际上符合电子邮件地址的格式。这种验证类型必须通过代码来完成。
None选项不执行任何字符验证,允许任何格式化的字符被输入到输入字段中。
0 到 9 以及破折号(负号)。输入进一步限制为只允许负号作为第一个输入字符。
十进制选项与整数选项具有相同的限制,但它还允许输入单个小数点。
字母数字选项仅允许英文字母(a 到 z)和数字 0 到 9。允许大小写字母;负号和小数点不被接受。
名称选项允许在名称中通常找到的字符,并提供格式化。它允许字母、空格和单引号(‘)。它还强制字符串中第一个字符以及每个空格后的字符大写。单引号后不能有空格,并且空格后不能有空格。字符串中只允许一个单引号。字母不受字母数字选项中仅限于 a-z 的限制。允许任何 Unicode 字母。
备注
要查看所有允许的 Unicode 字母的列表,请查看 .NET 中 Char.IsLetter 方法的说明。msdn.microsoft.com/en-us/library/system.char.isletter(v=vs.110).aspx。
电子邮件地址选项允许在电子邮件地址中允许的字符,并强制执行一些格式化规则。与其他验证选项相比,它在可以输入的字符类型方面限制较少。以下字符是允许的:
-
小写和大写英文字母(a 到 z)
-
数字 0-9
-
以下标点符号和特殊符号:
| 符号名称 | 字符 |
|---|---|
| at 符号 | @ |
| 点/句号 | . |
| 问号 | ? |
| 感叹号 | ! |
| 破折号 | - |
| 下划线 | _ |
| 单引号 | ‘ |
| 反引号 | ` |
| 波浪号 | ~ |
| 开放和闭合花括号 | |
| 竖线 | | |
| 箭头 | ^ |
| 星号 | * |
| 加号 | + |
| 等号 | + |
| 斜杠 | / |
| 哈希符号/井号 | # |
| 美元符号 | $ |
| 百分比 | % |
| 和号 | & |
表 13.1:允许的特殊字符
不允许空格,字符串中只允许一个 @ 符号,并且点不能跟随另一个点。
即使电子邮件地址的第一个字符是点不合法,电子邮件地址字符验证选项也不会限制它作为输入字段中第一个输入字符。
箭头和选择属性
InputField Input Caret。当输入字段被选中时,光标变为可见。
本节讨论的属性会影响光标的外观,以及如果使用光标(或突出显示)选择文本时的文本外观。
0.85。
1。
当选择 自定义光标颜色 属性时,一个次要属性,光标颜色,变得可用。然后你可以选择更改光标颜色。除非 自定义光标颜色 被选中并且 光标颜色 被更改,否则光标将呈现深灰色。
当光标在输入字段内的字符之间拖动时,这些字符将被选中(或突出显示)。选择颜色属性决定了选中文本的颜色。
输入字段默认事件 – 值更改(字符串)和末尾编辑(字符串)
输入字段组件有两个默认事件。第一个默认事件是值更改事件,如输入字段组件的值更改(字符串)部分所示。每当输入字段中的文本更改时,此事件都会触发。它接受一个字符串作为参数,其参数的使用方式与本章前面讨论的 UI 组件的值更改事件相同。如果你想向函数传递参数,你可以从静态参数列表或从动态字符串列表中选择函数,具体取决于你是否想向函数传递参数:

图 13.23:第十三章场景中输入字段值更改事件示例
如果你想要不断检查玩家在输入字段中输入的内容,你将使用前面图像中显示的第三种设置,该设置从动态 字符串列表中选择一个带有参数的函数。
第二个默认事件是末尾编辑事件,如输入字段组件的末尾编辑(字符串)部分所示。每当玩家完成文本编辑时,此事件都会触发。玩家的完成可以通过点击输入字段外部(这样输入就不再被选中)或提交文本来确认。
它接受一个字符串作为参数。与其他本章讨论的事件一样,你可以选择不传递参数、传递静态参数或传递动态参数。以下截图显示了所有三种选项的设置:

图 13.24:第十三章场景中输入字段末尾编辑事件示例
如果你想在按下 Enter 键时调用末尾编辑事件,请使用 单行 或 多行提交 选项的 行类型。
现在我们已经回顾了 UI 输入字段,让我们来回顾其对应物,即输入字段 – TextMeshPro。
输入字段 - TextMeshPro
Input Field - TextMeshPro 与 UI 输入字段非常相似。当添加到场景中时,您会看到它看起来几乎相同,只是占位符文本使用了不同的字体。UI 输入字段默认使用 Arial 字体,而 Input Field - TextMeshPro 使用 Liberation Sans。
要创建 UI 输入字段,请选择+ | UI | Input Field - TextMeshPro。默认情况下,Input Field - TextMeshPro GameObject 有一个名为文本区域的子对象,该子对象有两个子对象:一个占位符和一个文本对象。您将观察到它在设置上与 UI 输入字段略有不同。
文本区域 GameObject 包含一个 Rect Transform 组件和一个 Rect Mask 2D 组件。文本区域确保文本不会出现在指定的区域之外,如下图中突出显示的区域所示。如果您想更改此区域的大小,您将更改 Rect Transform 组件上的属性:

图 13.25:InputField - TextMeshPro 的文本区域
占位符和文本子对象仅仅是 Text - TextMeshPro 对象。您可以在第十章中找到有关 Text - TextMeshPro 对象的更多信息。
一个 Input Field - TextMeshPro GameObject 包含一个 Image 组件。如果您想更改输入框的外观,请更改 InputField (TMP)父级上的Image组件的源图像。
TextMeshPro - 输入字段组件
父级 InputField (TMP)对象具有一个TextMeshPro – 输入字段组件。它具有所有交互式 UI 对象的公共属性,许多与标准 UI 输入字段的相同属性,以及一些仅适用于输入字段 - TextMeshPros 的属性。本节将不会讨论与 UI 输入字段共享的属性,因为它们已在上一节中讨论过,我们只会讨论仅适用于它的属性。您可以在以下图像中查看属性:

图 13.26:TextMeshPro – 输入字段组件
文本视口属性设置为应显示输入文本的区域中的 Rect Transform。默认情况下,文本区域子对象的 Rect Transform 分配给此属性。如前所述,文本区域子对象具有一个 Rect Mask 2D 组件,该组件阻止文本在文本区域 Rect Transform 组件定义的区域之外可见。
文本组件属性设置为应显示输入文本的对象中的 Text Mesh Pro UGUI 组件。分配给此属性的 TextMeshPro - Text 对象将确定输入文本的字体和显示设置。默认情况下,文本子对象的 Text Mesh Pro UGUI 组件分配给此属性。
文本输入框组可以展开以显示一个大的文本输入区域。文本输入框属性与 UI 输入字段组件上的文本属性工作方式相同。用户输入的文本将存储在此处,可以通过代码访问。这将存储实际输入的文本,而不是格式化后的文本。例如,如果文本已被格式化为显示为星号(如Pin和密码内容类型),实际的 PIN 或密码将存储在此处,而不是一串星号。
输入字段设置
字体资产属性确定输入字段 - TextMeshPro 中显示的各种文本的字体,而点大小属性确定文本的大小。您会注意到占位符和文本子项在其 Text Mesh Pro UGUI 组件上也有字体资产和点大小属性。更改输入字段 - TextMeshPro 父对象的字体资产和点大小属性也将更改子对象上的相应属性。
此组中的其余属性是包含在 UI 输入字段中的属性。
控制设置
如果选择焦点时全选属性,当选择输入字段- TextMeshPro 时,字段内的所有文本将被突出显示。
如果选择在停用后重置属性,光标将重置到文本前面的默认位置。
如果选择按 ESC 键恢复属性,则在按下esc键时,文本将重置回默认值。默认值将是空字符串或场景开始时在文本输入框中输入的内容。
富文本属性意味着可以接受任何富文本标签,而允许富文本编辑属性允许用户在字段内输入富文本标签。
输入字段 - TextMeshPro 默认事件 - 选择时(字符串)和取消选择时(字符串)
输入字段 - TextMeshPro 有四个默认事件:值更改事件、结束编辑事件、选择事件和取消选择事件,如值更改(字符串)、结束编辑(字符串)、选择(字符串)和取消选择(字符串)部分所示。
前两个事件,值更改事件和结束编辑事件与 UI 输入字段中展示的相同。
第三个事件是选择事件。每当选择输入字段 - TextMeshPro 时,此事件都会触发。第四个事件是取消选择事件。正如您所期望的,每当取消选择输入字段 - TextMeshPro 时,此事件都会触发。它与结束编辑事件的工作方式类似,但它在文本提交时不会触发。
与本章中讨论的其他事件一样,您可以选择向选择和取消选择事件传递无参数、静态参数或动态参数。
现在我们已经回顾了 uGUI 的各种可交互组件,让我们看看如何使用它们的例子。
例子
本章包含了许多新内容,以至于我可以花掉这本书的剩余部分来展示例子!遗憾的是,我无法做到这一点,所以我将展示一些我认为最有用的例子。让我们开始吧。
创建带有图像的下拉菜单
让我们继续工作在我们的场景中,创建一个下拉菜单,允许我们在猫和狗之间切换我们的玩家角色。最终版本将如下所示:

图 13.27:暂停菜单下拉菜单的最终版本
改变我们的选择将改变屏幕顶部出现的角色图像。
包含狗图像的精灵表是我从以下免费艺术资源中修改的:
opengameart.org/content/cat-dog-free-sprites
这是我们提供猫精灵的同一资产。
当你想在播放模式下看到你的 UI 下拉菜单时,你必须按P键来弹出暂停面板。当你只想快速检查布局时,这可能会有些烦人。你可以通过禁用主相机上的ShowHidePanels.cs脚本来暂时禁用暂停面板的自动隐藏。记住,当你完成时一定要重新打开它!
以标题和项目图像布局下拉菜单
要创建一个像前一张图片中显示的 UI 下拉菜单,请完成以下步骤:
-
定位到文本源文件中提供的
dogSprites.png图像,并将其拖入项目Assets/Sprites文件夹中。 -
通过将精灵模式设置为多个并利用自动切片来切割精灵表。当你执行自动切片时,将锚点设置为底部。
-
现在,让我们向我们的
暂停面板添加一个 UI 下拉菜单。在暂停横幅中,在暂停面板上右键单击下拉菜单向上。 -
通过设置矩形变换属性来调整下拉菜单的大小和位置,如下所示:
![图 13.28:下拉菜单的矩形变换]()
图 13.28:下拉菜单的矩形变换
您的下拉菜单现在应该如下所示:
![图 13.29:下拉菜单的当前状态]()
图 13.29:下拉菜单的当前状态
-
要更改下拉菜单的背景,我们需要将
uiElements_12子精灵更改为箭头。你可以调整箭头的属性来改变其外观和一般位置。给
箭头分配uiElements_132精灵,并调整矩形变换,如图所示:

图 13.30:箭头的矩形变换
注意
在项目视图的搜索栏中键入132是快速找到uiElements_132图像的方法。
-
虽然下拉组件有一个标题变量,但 UI 下拉模板并没有预先构建一个。因此,我们必须自己手动添加一个。在
Label中的Dropdown上右键单击。重命名为Caption。 -
给
Caption赋予catSprites_0精灵并调整其矩形变换,如下所示:![图 13.31:标题的矩形变换]()
图 13.31:标题的矩形变换
我们将其设置为猫的图片,这样我们就可以看到它是否显示正确,但请记住,它将根据选择自动更改为适当的精灵。
-
现在,选择
Label并调整其 矩形变换 和 文本 组件,如下所示:![图 13.32:标签的矩形变换]()
图 13.32:标签的矩形变换
如果你尝试调整 文本,它将恢复到选项 A,因为这是由下拉组件驱动的。
-
现在我们已经按照我们想要的方式设置了标题,让我们来处理模板。启用
Template对象,以便在场景中查看它并在Scrollbar中展开它,这样我们就可以保持它不变。它将在场景视图中显示,但在播放模式下不可见,因为其 可见性 在模板的 Scroll Rect 组件中设置为 自动隐藏并展开视口。 -
要更改下拉窗口的背景,将
Template更改为uiElements_11。 -
Item子项显示了所有将在我们的下拉列表中列出的选项的通用格式。我们对它所做的任何更改都将自动应用于当下拉脚本填充它们时所有选项。选择
Item并更改其矩形变换50。

图 13.33:项目的矩形变换和层次结构
-
Content需要完全封装Item。因此,更改其矩形变换52。确保0。如果你玩过游戏,
Content从0变为其他东西。这实际上是应该发生的(在标题后面设置了选项 A),但当你试图布局你的Item时,这会让人感到烦恼,因为你将无法看到你的Item。因此,在玩游戏后,将
0更改,以便你可以继续编辑。 -
正如我们不得不向
Dropdown添加一个子图像以便有标题图像一样,我们也必须向Item添加一个子图像,以便在菜单中显示图像。在
Image中的Item上右键单击。重命名为Item Image并在 层次结构 中将其移动到Item Checkmark和Item Label之间。 -
给
Item Image赋予catSprites_0精灵并调整其矩形变换,如下所示:

图 13.34:项目图像的矩形变换和层次结构
- 选择
Item Label并调整其 矩形变换 和 文本 组件,如下所示:

图 13.35:项目标签的矩形变换
- 对
Item做的最后一件事是移除白色背景。选择Item Background并更改0。现在你的下拉菜单应该看起来如下:

图 13.36:下拉的当前状态
- 现在我们已经从视觉上设置了下拉菜单,我们需要设置下拉组件的属性。如果你玩游戏,你会看到我们的下拉菜单还没有正确的选项。虽然玩游戏时你看不出来,但Caption Image和Item Image也没有连接:

图 13.37:按下播放时的下拉当前状态
-
让我们在
Dropdown上更新下拉组件。将层次结构中的Caption子项拖到Item Image中,然后拖到Caption中,拖到Caption Image中。猫的图像将从场景中消失。别担心!它会回来的。它正在更新为选项 A的图像,目前设置为无。 -
我们最后需要设置的是将在下拉菜单中显示的选项集。
我们只需要两个选项,所以将
Option A选择为Cat,将文本Option B选择为Dog。将
catSprites_0拖到Cat下的精灵槽中,将dogSprites_0拖到Dog下的精灵槽中。你的下拉组件属性应该如下所示:![图 13.38:下拉组件的属性]()
图 13.38:下拉组件的属性
如果你玩游戏,你会看到下拉菜单现在显示了适当的选项列表,并且根据你的选择更新了标题图像和文本:
![图 13.39:下拉选项的最终视觉设置]()
图 13.39:下拉选项的最终视觉设置
使用下拉选择的信息
现在我们已经将下拉菜单设置成我们想要的样子,并且它运行正常,我们可以用代码访问玩家的选择。我们将使用玩家的选择来更新屏幕左上角的玩家角色图像。
要用下拉选择替换玩家角色图像,完成以下步骤:
-
在你的
Assets/Scripts文件夹中创建一个新的 C# 脚本,命名为PlayerCharacterSwap.cs。 -
为了访问 UI 变量类型,将
UnityEngine.UI命名空间添加到脚本顶部的以下行:using UnityEngine.UI; -
我们只需要两个变量,一个用来表示将与下拉菜单中的选择进行交换的图像,另一个用来表示下拉菜单。
我们将把这个脚本附加到下拉对象上,这样我们就不必将引用它的变量设置为公共的。在命名空间声明之后添加以下变量声明:
public Image characterImage; Dropdown dropDown; -
在
Awake()方法中初始化dropDown变量,将 Dropdown 组件附加到此脚本将要附加到的对象上:void Awake(){ dropDown = GetComponent<Dropdown>(); } -
Dropdown 组件的默认事件是On Value Changed事件,它接受一个整数参数。创建一个公共函数,接受一个整数参数,如下所示:
public void DropDownSelection(int selectionIndex){ } -
DropDownSelection方法将在我们的脚本中获取selectionIndex参数的整数值。将以下两行添加到您的
DropDownSelection函数中:Debug.Log("player selected " + dropDown.options[selectionIndex].text); characterImage.sprite = dropDown.options[selectionIndex].image;第一行将在选项列表中找到指定索引处的文本,并将其打印到控制台。
第二行将在选项列表中找到指定索引处的精灵,并将
characterImage上的精灵更改为该精灵。 -
我们现在已经完成了脚本,可以在 Unity 编辑器中连接它。将
PlayerCharacterSwap.cs脚本拖到Dropdown上以附加它。记住,
dropDown变量不是公共的,因为我们预计将此脚本作为组件附加到Dropdown。 -
公共变量
characterImage需要在检查器中分配。将左面板 | Character Holder | Character中的Character Image拖到Player Character Swap组件上的Character Image槽中。 -
现在我们需要在 Dropdown 组件的On Value Changed事件上调用DropDownSelection函数,从PlayerCharacterSwap.cs脚本中。在On Value Changed (Int32)事件列表中选择加号(+)来添加一个新的On Value Changed事件。将Dropdown从层次结构拖入对象槽中,并从PlayerCharacterSwap脚本的Dynamic int列表中选择DropDownSelection函数。On Value Changed (Int32)事件列表应如下所示:

图 13.40:On Value Changed (Int32)属性
就这样!现在玩玩游戏,看看玩家角色的图像是否与从 Dropdown 中选择的图像交换:

图 13.41:场景的最终版本
图 13.41 显示了游戏的场景最终版本。
摘要
谁能想到有这么多不同类型的可交互 UI 对象?为这些不同的 UI 对象提供模板非常有帮助。技术上,它们都可以通过按钮、图像和文本手动构建,但这将需要大量的努力,你不必担心,因为 Unity 已经为你做到了。在本章中,我们回顾了如何使用常见的 UI 元素:切换、滑块、下拉列表和输入字段。
接下来,我们将介绍如何在 UI 中使用动画!
第四部分:Unity UI 高级主题
在本部分,你将学习更多与 Unity UI 系统相关的高级主题。你将学习如何对 UI 元素进行动画处理以及如何在 UI 中显示粒子。你将学习如何创建出现在游戏世界中的 UI。最后,你将了解创建优化 UI 时需要考虑的因素。
本部分包含以下章节:
-
第十四章,UI 元素的动画
-
第十五章,UI 中的粒子
-
第十六章,利用世界空间 UI
-
第十七章,优化 Unity UI
第十四章:动画化 UI 元素
由于我们已经讨论了如何为按钮创建动画过渡,因此在本章中,我们将更深入地探讨动画过渡,并讨论如何更普遍地创建 UI 元素的动画。
本章假设你对 Unity 的动画系统有基本的了解,并且不会详细描述各种菜单的名称和动画窗口、动画控制器窗口的布局。动画片段和动画控制器将简要介绍,重点介绍它们与 UI 的关系及其实现,这些将在本章末尾的示例中进行讨论。
在本章中,我们将讨论以下主题:
-
将动画应用于各种 UI 元素
-
创建淡入淡出的弹出窗口
-
使用状态机和动画事件创建复杂的动画系统
尽管我假设你对 Unity 的动画系统有基本的了解,但我确实想强调动画片段和动画控制器之间的区别。
当你在 Unity 中为项目中的项目创建动画时,你从动画片段开始。一个动画片段应该代表一个单一的动作或运动。例如,如果你有一个执行两个独立动作的菜单,弹跳和缩放,你会为每个动作创建一个单独的动画片段。虽然你可以在单个动画片段中包含多个事件,但除非它们总是同时发生,否则非常重要不要将多个动作放入一个片段中。
每个游戏对象都可以拥有多个动画片段。动画控制器决定了所有这些动画如何链接在一起。因此,一个游戏对象的动画控制器将包含其所有的动画片段。
我想要区分这一点,因为在过去,我见过一些包含多个动作的史诗级动画片段的项目,这些动作本应该被分解成更简单的动作。
注意
与前几章一样,本节中展示的所有示例都可以在提供的代码包中的 Unity 项目中找到。它们可以在标记为第十四章的场景中找到。
技术要求
你可以在此处找到本章的相关代码和资产文件:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2014
动画片段
Unity 动画系统的优点在于你可以动画化 UI 的几乎所有属性。要创建动画片段,只需打开动画窗口(窗口 | 动画 或 Ctrl + 6),然后选择要动画化的 UI 元素,然后选择创建:

图 14.1:动画窗口
一旦这样做,系统会提示你保存动画片段。
创建动画剪辑后,您可以通过单击添加属性将任何属性添加到剪辑的时间线:

图 14.2:SliderExampleAnimation 时间线
这样做将显示对象的每个组件,以及其所有子组件的列表:

图 14.3:添加动画属性
您还可以查看每个子组件的组件和子组件:

图 14.4:展开动画组件
然后,您可以查看这些子组件的组件和子组件。您可以继续这种方式,直到耗尽所选 GameObject 下嵌套的 GameObject 列表。
如果您展开 GameObject 的组件或其子组件之一,您将注意到该组件所有可动画的属性列表。正如您将在以下屏幕截图中所注意到的,滑动组件上的几乎所有属性都可以以这种方式进行动画处理:

图 14.5:滑动组件及其可动画属性
只有具有以下数据类型的属性才能使用动画系统进行动画处理:
-
Vector2、Vector3和Vector4 -
四元数 -
布尔值 -
浮点数 -
颜色
选择加号将属性添加到动画时间线,并附带两个关键帧。然后您可以在各个关键帧更改每个属性的值。
关键帧是动画中的一个重要或关键(因此得名)帧。它表示动画中转换的开始或结束点。

图 14.6:SliderExampleAnimation 第一个关键帧
在前面的屏幕截图中,红色方框内的值表示特定帧的属性值。布尔值由复选框表示,浮点值由数字表示。每种类型都可以通过选择它们进行直接编辑。
Unity 将在关键帧之间填充值,以便它们在曲线上变化(或插值)。您可以通过选择曲线选项卡来查看插值曲线。
您可以通过播放动画或拖动播放头查看整个帧的变化。
播放头是表示当前正在显示的帧的标记。“拖动播放头”意味着将播放头拖动到时间线上以查看单个帧的变化。

图 14.7:时间线的曲线版本
布尔属性沿线性曲线插值。所有其他属性(因为它们是浮点数的组合)沿渐变-渐出曲线插值。你可以通过右键单击关键帧处的切线手柄来调整这些插值。
通常,这些渐变曲线会导致你的 UI 看起来弹跳,调整插值曲线可以消除这种弹跳。例如,由于默认插值曲线的渐变-渐出特性,在两个点之间动画一个对象可能会使对象瞬间超过目标点。
动画事件
我最喜欢的关于动画剪辑的事情之一是能够在时间轴上的帧中添加动画事件。动画事件允许你在指定的帧上调用游戏对象上存在的函数。它们由时间轴上方的白色旗帜表示,悬停在它们上方将显示由动画事件调用的函数的名称。
动画事件只能调用附加到动画剪辑上的游戏对象上存在的函数。该函数可以是公共的或私有的,也可以有参数。参数可以是以下类型:
-
float -
int -
string -
对象引用
-
AnimationEvent对象
你可以通过在想要放置动画事件的帧上方的区域右键单击并点击添加动画事件,或者点击添加事件按钮来将动画事件添加到动画剪辑的时间轴上。选择添加事件按钮将在播放头当前停留的位置添加动画事件。

图 14.8:动画事件
你可以通过选择白色旗帜并点击删除来删除动画事件,也可以通过点击并拖动它来移动它。
动画事件检查器的外观取决于动画剪辑是否附加到游戏对象,以及游戏对象是否当前被选中。以下截图显示了两种外观:

图 14.9:动画事件的检查器
如果动画剪辑附加到游戏对象上,并且游戏对象被选中,将出现一个下拉菜单,其中列出了所有可用的函数。如果选定的函数有参数,则将提供有关该参数的选项(请参阅前面的截图)。否则,必须手动输入函数的名称和要传递的参数。
如果你启用Chapter14场景中的Slider Animation Example游戏对象并播放,你会看到一个带有事件的动画正在播放。
现在我们已经了解了动画剪辑,让我们看看动画控制器。
动画控制器
每当为对象创建动画剪辑时,例如在前面章节中的Slider GameObject 上的SliderExampleAnimation动画剪辑,就会创建一个名为Slider的动画器,并将动画器组件附加到Slider GameObject:

图 14.10:滑块动画器的检查器
需要动画器来播放动画剪辑,因为它决定了何时播放动画剪辑。
动画器是一种称为状态机的决策树类型。它包含一系列状态。状态本质上是在某一时刻的状态。状态机的当前状态将表示此刻正在发生的事情。例如,如果有一个描述我的行为和动作的状态机,我的当前状态将是在键盘上打字。我的状态机将会有我可以最终过渡到的其他状态,例如睡觉或因为即将截止日期而哭泣,如果满足某些条件。
动画器中的状态由称为节点的矩形表示。状态通过表示为箭头线的转换连接。这些转换发生在预定时间或满足一组条件之后。当前状态将有一个蓝色的动画状态栏,显示该状态完成的百分比。如果当前状态正在等待转换发生,这个状态栏可能会循环或停在完全位置,直到满足转换的条件:

图 14.11:滑块动画器
动画器窗口的右下角显示了当前动画控制器名称及其文件夹位置。当你有多个不同的动画器时,这非常有用,因为它告诉你你正在使用哪个动画器。
状态可以是空的,或者代表动画剪辑。如果一个状态代表动画剪辑,那么在它的检查器中,其运动属性将设置为一个动画剪辑,如下面的截图所示:

图 14.12:动画状态的“运动”属性
大多数状态将以灰色显示,但那些颜色不同的将代表特殊状态。每个动画器在其内部都将有一个入口节点(绿色)、一个退出节点(红色)和一个任何状态节点(蓝色)。你添加到动画器的第一个状态将被分配为默认层状态节点(橙色)。你可以随时更改默认层状态的状态。请注意,动画器层将在后面的章节中讨论。
入口和退出节点本质上作为状态机之间的门。你可以在状态机内部有状态机,这些门分别决定状态机进入和退出后会发生什么。因此,入口节点代表状态机开始实例,而退出节点代表状态机停止的实例。
入口节点始终过渡到默认层状态,并且无法定义过渡的条件,因此过渡将始终自动且立即发生。因此,你可以将默认层状态视为状态机开始时将发生的第一个状态。
任何状态节点是一个包容性状态。当你想要发生过渡,而不管当前状态时,你将使用此状态。你只能从任何状态节点过渡出去。继续以描述我行为的状态机为例,我会从任何状态过渡到关于即将截止日期哭泣的状态,因为无论我现在在做什么,如果满足条件“截止日期在 24 小时内”,我可能会突然哭泣。
如前所述,动画师将保持在当前状态,直到经过指定的时间或满足一组条件。选择过渡箭头将显示过渡条件:

图 14.13:动画的扩展属性
从前一个屏幕截图的过渡需要指定的时间和条件。过渡也不是瞬时的,需要0.25秒才能完成。
发生过渡必须满足的条件由动画师的参数设置,这些参数可以在动画窗口的左上角找到和创建:

图 14.14:添加动画参数
这些参数的值可以从脚本中设置。有四种类型的参数:在过渡使用后变为False。触发参数对于创建需要停止并等待才能进入下一个状态的洪水门类型动作很有帮助。在状态和过渡形成循环的情况下,这些参数比布尔参数更受欢迎,因为布尔参数在状态循环回之前必须手动重置。
当你查看参数列表时,可以通过右侧的值来判断它们的类型。浮点参数有十进制数字,整型参数有整数,布尔参数有方形复选框,触发参数有圆形单选按钮。
由于动画器是状态机,它可以用来完成比动画更多的任务。动画器可以用来跟踪复杂的游戏逻辑。例如,我为一个三消 RPG 创建以下状态机来跟踪游戏中当前发生的情况。使用它来跟踪游戏当前状态允许我根据游戏中的情况限制玩家可以做什么。
例如,如果敌人角色正在攻击,玩家将不会与棋盘上的碎片交互:

图 14.15:用于逻辑的动画状态机示例
现在我们已经回顾了动画控制器属性,让我们看看它的用途。
过渡动画的动画器
在第九章中,我们探讨了按钮动画过渡并为按钮创建了一个简单的动画。我们让按钮组件自动为我们生成动画器,但从未查看过动画器或对其进行任何操作。现在我们已经讨论了动画器,让我们看看保存在Assets/Animations中的Play Button的动画器:

图 14.16:按钮的动画器
如前述截图所示,为我们自动生成的动画器并不特别复杂,其设置也是一目了然的。它包含以下五个动画片段的状态:Normal、Highlighted、Pressed、Selected和Disabled。所有动画片段都从Any State过渡。此外,还有五个触发参数:Normal、Highlighted、Pressed、Selected和Disabled。
为任何允许过渡动画的 UI 元素自动生成动画器将产生相同的设置。尽管这个动画器为你预设了,但你可以根据自己的需要自由调整它。
动画层
当使用动画器时,如果你有一个具有多个节点过渡的状态,则只能发生一个过渡。例如,在以下截图中,ChooseAState状态只能同时过渡到其他状态中的一个,即使所有过渡条件都满足也是如此;这适用于你使用的任何类型的参数:

图 14.17:第十四章场景中的分支动画示例
如果你想同时触发多个动画,可以使用动画层。以下图层设置将使所有三个状态同时运行:

图 14.18:第十四章场景中的动画层示例
我发现,像这样的需求最常见的情况是当您有一个由多个精灵图集组成的对象,并且您希望同时触发多个精灵图集动画,将它们全部放在同一个动画剪辑中并不合理。例如,我在一个游戏中工作,其中 2D 角色有多个可互换的部分,每个部分都有自己的精灵图集动画。必须让每个部分的无动作动画同时开始。由于部分可以互换,因此可以实现多种部分组合,制作所有可能的无动作动画组合并不合理。给每个可能的部件分配自己的动画器也不合理。因此,我为每个身体部分创建了一个层,并能够同时播放所有单个精灵动画。
在脚本中设置动画参数
您可以通过使用Animator类的SetFloat()、SetInteger()、SetBool()、SetTrigger()和ResetTrigger()函数通过脚本设置动画参数的值。您可以通过在动画器中分配给它们的字符串名称来引用动画参数变量。
要设置动画参数,您首先需要获取定义了参数的动画器;您可以使用公共动画器变量或使用GetComponent<Animator>()来完成此操作。然后,在动画器上调用必要的函数。
让我们看看一个示例,该示例将设置以下参数:

图 14.19:不同类型的动画参数
以下脚本将设置之前截图中所定义的动画参数:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Chapter14Examples : MonoBehaviour
{
Animator theAnimator;
void Awake()
{
theAnimator = GetComponent<Animator>();
}
public void SetAnimatorParameters()
{
theAnimator.SetFloat("FloatParameter", 1.0f);
theAnimator.SetInteger("IntParameter", 1);
theAnimator.SetBool("BoolParameter", true);
theAnimator.SetTrigger("TriggerParameter"); // sets to true
theAnimator.ResetTrigger("TriggerParameter"); // sets to false
}
public void ExampleFunction()
{
Debug.Log("The Animation Event is not sending an argument");
}
public void ExampleParameterFunction(int value)
{
Debug.Log("The Animation Event sends the following value: " + value);
}
}
使用触发器的优点是您通常不需要重置它,因为一旦过渡使用它,它就会立即重置。然而,如果您设置了触发器,但过渡从未到达,您将需要使用ResetTrigger()来重置它。
动画器行为
如果您想在状态内的特定点执行代码,您可以使用一种独特的脚本类,称为状态机行为。状态机行为可以添加到您在动画器中创建的任何状态节点。我指定您创建,因为您不能将它们添加到进入节点、退出节点或任何****状态节点。
您可以通过选择一个状态并点击添加行为来创建一个新的状态机行为:

图 14.20:如何将行为添加到动画状态
以这种方式创建的所有新状态机行为都保存在Assets文件夹中。
当您打开脚本时,它将自动填充以下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ChooseAStateBehaviour : StateMachineBehaviour
{
// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
//override
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// Implement state enter logic here
}
// OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
//override
public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// Implement state update logic here
}
// OnStateExit is called when a transition ends and the state machine finishes evaluating this state
//override
public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// Implement state exit logic here
}
// OnStateMove is called right after Animator.OnAnimatorMove()
//override
public override void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// Implement code that processes and affects root motion
}
// OnStateIK is called right after Animator.OnAnimatorIK()
//override
public override void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// Implement code that sets up animation IK (inverse kinematics)
}
}
注意,这个类是从StateMachineBehaviour派生出来的,而不是像我们附加到 GameObject 上的脚本那样从MonoBehaviour派生。
脚本中预先编写了一些函数,以及如何使用它们的描述。就像Awake()、Start()和Update()是 MonoBehaviour 的预定义函数一样,OnStateEnter()、OnStateUpdate()、OnStateExit()、OnStateIK()和OnStateMove()是在特定时间调用的预定义函数。你可以删除你不想使用的任何函数。你还可以在此脚本中编写其他函数,它们不受这些预定义函数的限制。
这些函数可以执行你想要它们执行的操作,甚至可以设置你的动画机的动画参数。
我发现状态机行为非常有帮助,因为我广泛使用状态机来控制游戏逻辑。在前面这个部分,我向你展示了我为三合一 RPG 创建的状态机。我使用了多个状态机行为来让其他脚本知道状态何时改变,在指定时间调用其他脚本中的函数,等等。
现在我们已经回顾了使用动画剪辑和动画控制器,让我们看看一些如何在我们的项目中实现它们的示例。
示例
本章的主要重点是提供创建常见 UI 动画和效果的示例。让我们开始吧。在这些示例中,我将向你展示一个基本的可重用动画,用于淡入淡出 UI 面板,以及一个更复杂的依赖于玩家交互的宝箱动画。
动画弹出窗口的淡入淡出效果
在我们的第一个示例中,我们将继续在我们的主场景中工作。
目前,我们有暂停面板和物品栏面板,当按下P或I键时会立即出现。这并不特别有趣,所以让我们添加一些动画,使面板通过淡入淡出和缩放动画来弹出和消失。
每次进入新场景时,必须展开所有父项以查看其子项,这可能会变得相当繁琐。一个打开所有父项的快捷方式是选择层次结构中的所有内容,然后按键盘上的右箭头键。如果你有多个嵌套,你可以多次这样做。按左箭头键将折叠父项。
我们设置这些动画及其功能的工作流程将是创建动画剪辑,设置我们的动画机,然后编写在适当时间设置动画机参数的代码。为了使步骤更容易理解,我将它们分为几个部分。第一部分涵盖了设置动画剪辑和动画机的所有步骤,第二部分涵盖了编写代码涉及的设置。
设置动画
要在暂停面板和物品栏面板上创建弹出和消失的动画,请执行以下步骤:
-
我们首先将一个动画剪辑添加到
暂停面板,这将使其缩放并淡入。打开动画窗口,从层次结构中选择暂停面板。选择Assets/Animation文件夹,并将其命名为FadeAndScale。 -
我们想控制这个面板的四个属性:其缩放、其 Alpha 值、其交互能力以及其射线投射阻塞。让我们从缩放开始。我们可以从 Rect Transform 组件调整这个属性。选择 添加属性,然后点击 Rect Transform 下 Scale 旁边的加号,如下所示:

图 14.21:添加 Scale 属性
- Scale 属性在 0:00 和 1:00 初始化了两个关键帧。这意味着动画将持续一秒钟,对于一个弹出动画来说有点长。选择 1:00 的关键帧,并将其拖动到 0:30,使动画缩短到半秒钟:

图 14.22:调整动画长度
-
我们希望面板开始时较小,然后变大。为了实现这一点,我们需要调整 x 和 y 缩放。在两个关键帧上展开
1。由于我们希望缩放从小开始,然后放大到正常大小,我们希望它从0缩放到1。我们只需要调整 x 和 y 缩放,因为 z 缩放实际上不会影响 2D 对象。因此,在关键帧0中,在它们的属性框中输入0,如下所示:![图 14.23:调整 Scale 属性]()
图 14.23:调整 Scale 属性
如果你播放动画,你会注意到
Pause Panel快速缩放。由于父子关系会以父元素的比例缩放子元素,所以我们不需要单独动画化所有Pause Panel子元素的缩放。 -
现在,让我们控制面板的 Alpha 值以渐显它。我们不想在
Pause Panel上动画化 Alpha。这样做只会影响面板背景图像的 Alpha 值,而不会影响子元素。相反,我们想在Pause Panel上动画化 Alpha 值,并让所有子元素一起工作。记住,这正是我们最初使用 Canvas Group 组件的原因。让我们选择 添加属性 | Canvas Group | Alpha:

图 14.24:添加 Alpha 属性
-
要渐显,面板应从完全透明开始,并最终变为完全不透明。两个关键帧都已经有了
1,所以将第一个关键帧的0改变。播放动画(或拖动播放头)将显示面板及其所有子元素渐显。 -
我们需要动画化面板是否可以被交互。我们不希望当面板仍在弹出时,玩家能够与下拉菜单或静音按钮交互。这同样由 Canvas Group 组件控制——选择 添加属性 | Canvas Group | Interactable。
-
只有在它完全弹出场景后,
Pause Panel和其子项才应该是可交互的。因此,在第一个关键帧旁边取消选择 CanvasGroup.Interactable 旁边的复选框,使 Canvas Group 从可交互状态动画到不可交互状态。当你拖动播放头时,你会看到属性直到最后一帧都不会再次开启。 -
我们需要动画的最后一项是其射线投射阻塞。将
false选择为true,就像你在FadeAndScale动画片段的时间线上所做的那样,现在应该出现以下内容:

图 14.25:FadeAndScale 动画片段的时间线
-
现在我们已经设置了动画片段,我们可以在动画器内部开始工作。当我们创建
FadeAndScale动画片段时,一个名为Pause Panel的动画器被自动创建。还向Pause Panel游戏对象添加了一个动画器组件,并将其分配给Pause Panel动画器。选择
Pause Panel后,打开动画器窗口。你应该会看到以下类似的内容:![图 14.26:Pause Panel 的动画器]()
图 14.26:Pause Panel 的动画器
你将看到一个名为
FadeAndScale的状态。此状态使用FadeAndScale动画片段作为其 运动,你可以在其检查器中查看。 -
目前,由于
FadeAndScale状态连接到FadeAndScale动画将立即播放。它也会循环播放。这显然不是我们想要的。让我们通过创建一个空状态作为层默认状态来阻止它在游戏开始时播放。在动画器内任意位置右键单击并选择 创建状态 | 空状态:![图 14.27:创建空状态]()
图 14.27:创建空状态
这将在动画器中添加一个名为
New State的灰色状态。 -
通过在其检查器中更改名称,将
New State重命名为Empty State。 -
通过右键单击并选择 设置为层 默认状态,将
Empty State设置为层默认状态:![图 14.28:设置状态为层默认状态]()
图 14.28:设置状态为层默认状态
现在,它将连接到
FadeAndScale,它也将不再是层默认状态,并且不会连接到任何过渡:![图 14.29:将空状态设置为层默认状态]()
图 14.29:将空状态设置为层默认状态
我们使用空状态作为层默认状态,因为我们希望面板在等待我们告诉它开始动画时什么也不做。
-
将项目重新排列为更易于查看的布局。我个人喜欢以下布局用于此动画器:

图 14.30:重新排列状态
-
FadeAndScale动画剪辑现在在游戏开始时不再立即播放。然而,它仍然设置为具有动画循环。为了修复这个问题,从您的项目文件夹视图中选择FadeAndScale动画剪辑。这将打开其检查器。取消选择循环时间属性以禁用动画的循环:

图 14.31:取消选择循环时间
- 我们希望
Pause Panel能够根据命令淡入和淡出,但我们没有淡出动画剪辑,只有淡入动画剪辑。实际上,我们不需要创建整个动画剪辑来实现这种动作。我们可以简单地播放FadeAndScale动画剪辑的反向。通过选择它并按Ctrl + D来复制FadeAndScale状态。这将为您提供一个名为FadeAndScale 0的新状态,其运动设置为FadeAndScale动画剪辑:

图 14.32:复制状态
-
将
FadeAndScale状态重命名为FadeAndScaleIn,将FadeAndScale 0状态重命名为FadeAndScaleOut。 -
要设置
FadeAndScaleOut状态反向播放FadeAndScale动画剪辑,我们只需在其检查器中更改其-1:

图 14.33:设置反向播放动画
-
现在我们已经正确设置了所有状态,我们可以创建它们之间的过渡。首先创建两个名为
FadeIn和FadeOut的触发参数:![图 14.34:动画师参数]()
图 14.34:动画师参数
我们在这里使用触发参数,因为我们希望在使用这些值后它们能够立即重置。这样,我们就可以创建一个动画循环,而无需编写重置参数值的代码。
-
您可以通过在第一个状态上右键单击,选择创建过渡,然后点击第二个状态来创建状态之间的过渡:
![图 14.35:创建过渡]()
图 14.35:创建过渡
按以下方式在状态之间创建过渡:
![图 14.36:最终状态过渡布局]()
图 14.36:最终状态过渡布局
这种过渡流程将允许面板从无动画过渡到
FadeAndScale动画剪辑,然后过渡到反转的FadeAndScale动画剪辑,并在两者之间来回切换。 -
如果你玩游戏,
暂停面板将立即从空状态变为淡入并缩放状态,然后变为淡出并缩放状态,并在这两个状态之间无限循环。这是因为过渡被自动设置为在动画完成后发生。要停止这种行为,你必须告诉过渡只在设置了一个参数之后发生。选择空状态和淡入并缩放状态之间的过渡。选择淡入触发器中的加号。由于我们不想让时间成为过渡的因素,取消选择具有 退出时间:

图 14.37:过渡属性
- 完成在
淡入并缩放和淡出并缩放状态之间的过渡。将淡入并缩放状态设置为淡出并缩放状态,将淡出并缩放状态设置为淡入并缩放状态,并将淡入并缩放状态设置为0以确保瞬间过渡:

图 14.38:如何设置暂停面板的过渡
-
如果你现在玩游戏,你会注意到
暂停面板在游戏开始时是可见的。这是因为我们的动画状态覆盖了我们之前在ShowHidePanels.cs中编写的代码,该代码使暂停面板在场景开始时不可见。我们稍后会处理我们的损坏代码,但现在,通过将Canvas Group组件设置为以下值,使暂停面板在开始时不可见:![图 14.39:Canvas Group 的属性]()
图 14.39:Canvas Group 的属性
现在你玩游戏时,
暂停面板将不会在开始时出现。 -
我们已经完成了
暂停面板动画的设置,但在你继续到库存面板之前,检查动画是否工作正常。为此,调整你的窗口,以便你的游戏视图和 Animator 窗口都可见:![图 14.40:播放空状态]()
图 14.40:播放空状态
你将看到当前
暂停面板状态上的进度条正在运行。要强制过渡,点击适当的触发参数旁边的圆圈。 -
如果你的动画工作正常,你现在可以设置
库存面板上的动画。你可能正在想,“呃,现在我必须为库存面板再次做所有这些吗?!”然而,不用担心,你不必再次这样做,因为你可以将暂停面板的 Animator 在库存面板上重用。我们在暂停面板的淡入并缩放动画剪辑中更改的所有属性也存在于库存面板上。因此,为了给库存面板提供相同的动画和控件集,我们可以简单地将我们创建的 Animator 附加到库存面板。由于我们将在两个不同的对象上使用我们创建的 Animator,所以将其重命名是个好主意。将名称从
Pause Panel更改为PopUpPanels。现在,将其拖动到Inventory Panel上。 -
正如我们不得不改变
Pause Panel上 Canvas Group 的属性以防止它在场景开始时出现一样,我们还需要改变Inventory Panel上的属性。将属性设置为 图 14**.39 中所示。
现在我们已经为我们的两个面板设置了动画,我们可以开始编写代码,当按下 P 和 I 键时触发动画以显示面板。
使用代码设置 Animator 的参数
我们有一个名为 ShowHidePanels.cs 的脚本附加到 Main Camera 上,当按下 P 和 I 键时,分别显示 Pause Panel 和 Inventory Panel。遗憾的是,它不再工作,因为现在的动画现在超过了我们在其中设置的 Canvas Groups 的属性。我们可以重用这个逻辑,但我们需要做一些工作才能让我们的面板再次弹出。
我们将对 ShowHidePanels.cs 文件所做的更改将导致前一章场景中的面板停止显示。如果您计划访问前一章的场景,请现在保存此脚本的副本来供以后使用。
要使用代码触发 Pause Panel 和 Inventory Panel 上的动画,请完成以下步骤:
-
打开
ShowHidePanels.cs脚本。注释掉Start()和Update()方法中所有使用TogglePanel()的代码。在注释掉这些行之后,您应该看到以下内容:void Start() { // Initialize Panels on Start TogglePanel(inventoryPanel, inventoryUp); TogglePanel(pausePanel, pauseUp); } void Update() { // Handle inventory Panel toggle if (Input.GetKeyDown(KeyCode.I) && !pauseUp) { inventoryUp = !inventoryUp; TogglePanel(inventoryPanel, inventoryUp); } // Handle pause Panel toggle if (Input.GetButtonDown("Pause")) { pauseUp = !pauseUp; TogglePanel(pausePanel, pauseUp); Time.timeScale = pauseUp ? 0 : 1; // Pause/unpause game by setting time scale } } -
我们不再使用它们的 Canvas Group 组件来引用
Pause Panel和Inventory Panel,而是现在使用它们的 Animator 组件。在类的顶部创建以下两个变量声明:public Animator inventoryPanelAnim; public Animator pausePanelAnim; -
现在让我们创建一个新的方法,称为
FadePanel(),它接受Animator和bool参数:public void FadePanel(Animator anim, bool show) { if (show) { anim.SetTrigger("FadeIn"); } else { anim.SetTrigger("FadeOut"); } } -
更新
Update()方法,现在在曾经调用TogglePanel()方法的地方调用新的FadePanel()方法。以下代码中加粗的文本表示添加的代码:void Update() { // inventory Panel if (Input.GetKeyDown(KeyCode.I) && !pauseUp) { inventoryUp = !inventoryUp; //TogglePanel(inventoryPanel, inventoryUp); FadePanel(inventoryPanelAnimator, inventoryUp); } // pause Panel if (Input.GetButtonDown("Pause")) { pauseUp = !pauseUp; //TogglePanel(pausePanel, pauseUp); FadePanel(pausePanelAnimator, pauseUp); Time.timeScale = Convert.ToInt32(pauseUp); } }这就是我们需要对代码所做的所有更改。
-
将
Inventory Panel和Pause Panel拖动到 显示/隐藏 面板 组件的相应插槽中:

图 14.41:显示/隐藏面板组件的属性
-
玩游戏并观察它是否大致工作。
Inventory Panel应根据需要显示和消失,但Pause Panel不会像预期的那样淡出。这是因为以下代码行阻止了所有依赖于时间缩放的动画发生:Time.timeScale = Convert.ToInt32(pauseUp);那行代码被用来有效地暂停游戏。然而,这意味着它将暂停我们的
Pause Panel弹出动画。别担心,你仍然可以使用这个简单的暂停代码,并在游戏暂停时运行动画。你只需要告诉Pause Panel上的 Animator,当时间尺度设置为0时,它仍然可以工作。你可以通过将 Animator 组件上的 Update Mode 从 Normal 更改为 Unscaled Time 来做到这一点:

图 14.42:将 Update Mode 设置为 Unscaled Time
- 也要设置
Inventory Panel的 Animator 组件。
玩游戏,你应该会有平滑动画的暂停和库存面板。我们现在可以继续到更复杂的动画。
动画复杂宝箱
对于这个示例,我们将使用一个新的场景。我们将创建的动画稍微复杂一些——一个宝箱将飞入场景,然后等待玩家打开它。一旦玩家打开它,宝箱将使用粒子系统在它前面弹出动画打开。然后,三个可收集物品将依次飞出。每个可收集物品都将有自己的 闪亮 动画开始播放。
以下图是一种故事板,显示了动画播放的一些关键帧:

图 14.43:本例动画的最终版本
在本章中,我们将涵盖所有项目,除了粒子系统,我们将在下一章中介绍。
注意
胸部精灵图集是从 bayat.itch.io/platform-game-assets 获得的,而物品精灵图集是从 opengameart.org/content/shining-coin-shining-health-shining-power-up-sprite-sheets 获得的。
这个示例有很多内容。它构建起来并不特别复杂,但提供从头开始构建它的步骤会需要太多步骤。这超出了本书的范围;但在这个书的这个阶段,你 hopefully 可以查看一个已经构建好的场景,并理解它是如何实现的。因此,我们将从这个示例的包文件开始,其中包含了场景中放置的所有项目,一些已经创建的动画,以及所有新的精灵图集。
在你开始之前,导入 Chapter 14 - Examples - LootBox - Start.unitypackage 资产包。
如果你想查看完成的示例,请查看标记为 Chapter 14 - Examples - LootBox - End.unitypackage 的包。
注意
Unity 层不会保存在 Unity 资产包中。本例将描述创建一个名为 UI Particles 的层,并让相机忽略或包含该层,但这在提供的包中并未显示。
为了使示例更容易吸收,我已经将步骤分为三个不同的部分。为了完成这个示例,我们将执行以下功能:
-
设置各个项目的各种动画片段。
-
使用状态机,即动画控制器,将所有动画连接起来,并确保它们的时间安排适当。
-
创建在箱子打开时显示的粒子系统,并确保它在 UI 中正确显示(将在下一章完成)。
设置动画
让我们从为场景中的每个对象创建动画开始这个部分。为了创建这个场景的所有动画,请完成以下步骤:
-
如果你还没有这样做,导入
Chapter 14- Examples - LootBox - Start.unitypackage。你应该看到一个场景,其中有两个画布——一个带有按钮和背景图像,另一个带有箱子、物品和按钮,如下面的截图所示:![图 14.44:包的场景布局]()
图 14.44:包的场景布局
在导入包之后,你也会注意到在
Assets文件夹中提供了一些动画片段和控制器。 -
Chest对象需要两个动画,一个是它从屏幕的侧面飞入的动画,另一个是它的精灵表打开的动画。让我们先制作飞入动画。在层级中选择Chest对象,然后选择ChestFlyingIn.anim,并将其保存到Assets/Animations/LootBox/Clips文件夹中。 -
在
Assets/Animations/LootBox/Clips文件夹中自动创建了一个名为Chest.controller的新动画控制器。将其移动到Assets/Animations/LootBox/Controllers文件夹。 -
在动画窗口中,在画布中选择
Chest:

图 14.45:ChestFlyingIn 动画
-
目前,动画时长为一秒,这比我们想要的要长一些。将第二个关键帧移动到
0:30标记,使其时长为半秒。 -
我们将在场景中移动对象到不同的关键帧,并希望它们被保存在动画中。因此,为了使动画记录你在场景中做的任何更改,请在动画窗口中选择记录按钮。
-
目前场景中
Chest的位置就是我们希望在飞入动画结束时所在的位置,所以我们不会在第二个关键帧上影响位置。然而,我们希望它从屏幕外开始,所以当动画播放头在第一帧时,使用Chest的第一帧位置。这由 Rect Transform 上的红色色调表示。![图 14.46:移动箱子并记录属性]()
图 14.46:移动箱子并记录属性
如果你拖动播放头,你会看到箱子从左向右移动。
-
现在,让我们让胸部以弧线而不是直线飞入。我们将使用动画曲线来完成这个操作。选择曲线选项卡,如下所示:
![图 14.47:选择曲线菜单]()
图 14.47:选择曲线菜单
绿色线代表锚定位置的y属性。选择Anchored Position.y属性以关注它。
-
选择第一和第二个关键帧锚点以影响它们的句柄。移动它们的句柄,直到绿色曲线看起来更像一个弧线:
![图 14.48:调整曲线的锚点]()
图 14.48:调整曲线的锚点
现在,当你播放动画时,你会看到胸部沿着弧线移动。
-
让胸部在飞过时淡入,向动画中添加一个
color属性。返回到0使胸部在第一帧不可见:

图 14.49:ChestFlyingIn 动画第一帧的属性
-
每次创建新动画时,它都会自动设置为循环。我们不希望这个动画循环播放,所以从项目视图中选择
ChestFlyingIn动画剪辑以查看其检查器属性。取消选择Loop Time复选框。 -
我们不希望
ChestFlyingIn动画剪辑在场景开始时自动播放。由于这是为Chest创建的第一个动画剪辑,它将被设置为动画器的默认状态。选择Chest,打开动画器窗口。 -
通过在动画器窗口内右键单击并选择
Empty State创建一个新的默认状态,并通过右键单击并选择设置为层默认状态将其设置为默认状态。我们将在稍后对胸部的动画器做更多操作,但现在我们只做这些。

图 14.50:胸部的动画器
-
现在,让我们设置胸部的开启动画。在仍然选择
Chest的情况下,在动画窗口中,从动画剪辑下拉列表中选择创建新剪辑…![图 14.51:创建新剪辑]()
图 14.51:创建新剪辑
将新的动画剪辑命名为
ChestOpening.anim,并将其保存在Assets/Animations/LootBox/Clips文件夹中。 -
这个动画剪辑将包含来自精灵图的全部精灵。从项目视图中,将所有子精灵拖放到动画时间轴上。将自动添加一个新的属性Image.Sprite,所有子精灵将按顺序添加到时间轴上:

图 14.52:胸部的精灵图动画
- 目前,动画播放得太快。它以每秒 60 帧的速度运行,只有 6 帧。我们需要更改帧率。首先,我们必须在动画窗口中启用采样率选项。选择时间轴上的三个点,然后选择显示 采样率。

图 14.53:启用采样率菜单项
- 现在,将
12;这将更改动画的帧率为12fps:

图 14.54:更改采样率
-
由于
胸有一个会影响其 alpha 值的动画,让我们确保这个动画在播放时始终具有全 alpha 值。在第一帧和最后一帧上选择1。实际上,我们只需要在第一帧上将 alpha 设置为1,但我喜欢在这里添加起始帧和结束帧,这样我就可以非常确定动画是如何使用这个值的。 -
我们不希望这个动画循环播放,所以从项目视图中选择
ChestOpening动画剪辑,查看其检查器属性。取消选择循环 时间复选框。 -
本例所需的所有其他动画都已设置。它们的设置与这个类似,或者与前面示例中创建的类似。然而,它们并没有连接到正确的游戏对象。
让我们给
胸打开画布中的其他对象添加动画。将硬币动画控制器拖到硬币游戏对象的检查器中,将心形动画控制器拖到心形游戏对象的检查器中,将升级动画控制器拖到升级游戏对象的检查器中。这些动画控制器都可以在Assets/Animations/Loot Box/Controllers文件夹中找到。现在,您可以通过选择游戏对象并在动画窗口中按播放来预览所有物品从胸中弹出并发光的动画(在游戏视图中按播放将不会显示正在播放的动画)。 -
让我们初始化
胸以及所有对象为不可见。从层次结构中选择胸、硬币、心形和升级游戏对象。在其图像组件中,更改它们的0的 alpha 值。 -
胸打开画布和该画布上的按钮都有动画。这些动画将影响对象上的 Canvas Group 组件。目前,它们还没有 Canvas Group 组件。因此,向胸打开画布及其子按钮添加 Canvas Group 组件。 -
初始化两个新的 Canvas Group 组件,使其 alpha 值为
0,并设置为false。现在,场景中应该只看到开始按钮。
-
现在,向
胸打开画布及其子按钮添加CanvasGroupFadeInOut动画控制器。
现在每个对象的动画都已完全设置。我们仍然需要完成胸的动画控制器的工作,并为各种动画器添加一些更多逻辑,但现在我们已经完成了动画剪辑。
构建状态机并设置动画时间
下一步是设置我们的状态机并编写代码,使各种动画在正确的时间播放。
为了连接各种动画并在正确的时间播放,完成以下步骤:
-
我们将首先创建一个状态机,它将作为我们动画序列的逻辑。在
Assets/Animations/Loot Box/Controllers中创建一个新的 Animator Controller,命名为ChestOpeningStateMachine.controller。 -
打开
ChestOpeningStateMachineAnimator Controller 并创建 12 个新的空状态。按照以下屏幕截图中的方式排列、命名和转换状态:![图 14.55:动画的状态机]()
图 14.55:动画的状态机
前一个屏幕截图中的状态机展示了打开胸部动画的事件序列和交互。标记为等待玩家的状态将在游戏等待玩家按下按钮以继续动画时播放。其他动画将根据定时事件自动播放。
-
在上一步骤中创建的状态机实际上不会包含任何动画。它仅仅是一个流程图,描述了游戏中当前正在发生的情况。我们还将使用它向场景中的各种对象发送信息,让它们知道它们应该或不应该做什么。由于我们只想用这个来控制这个序列的逻辑,而不是在场景中实际动画化任何东西,我们可以在场景中的任何对象上添加一个 Animator 组件。因此,让我们将其添加到
Main Camera上。从项目视图中拖动ChestOpeningStateMachineAnimator 到Main Camera的检查器中。 -
现在,我们需要设置状态机中各种状态的转换条件。创建四个动画触发参数,分别命名为
ShowChest、OpenChest、CloseChest和AnimationComplete。 -
现在,为每个转换设置触发条件,如图下所示;对于每个转换,确保取消选择Has Exit Time和Fixed Duration:

图 14.56:各种转换的触发器
注意
我强烈建议你在处理这个示例时将此图像添加到书签或打印出你的ChestOpeningStateMachine Animator 的屏幕截图。在处理这个示例时,拥有这样一个你可以轻松参考的流程图将使完成的步骤更容易遵循。
- 在我们编写代码之前,让我们为
Chest设置 Animator。目前,Chest的 Animator 应该看起来像以下这样:

图 14.57:胸部的 Animator
动画器仅包含动画状态,但尚未指示它们是如何相互连接的。我们需要创建过渡并设置动画参数。重新排列状态并向动画器添加过渡,使其看起来如下:

图 14.58:更新的胸部动画器
-
创建三个动画触发参数,分别命名为
ShowChest、OpenChest和Reset。 -
现在,为每个过渡设置触发条件,如以下截图所示;确保在每次过渡中取消选择Has Exit Time和Fixed Duration:

图 14.59:胸部动画器的触发器
-
现在我们所有的动画器都已适当地设置,我们可以开始编码。我们将使用
ChestOpeningStateMachine动画器,在玩家在指定状态下点击指定的按钮时,使每个适当的动画播放。然后ChestOpeningStateMachine动画器将自动设置出现在完整动画序列中的各种对象上的动画器上的适当触发器。我们需要一种方法来跟踪场景中哪些项目具有由ChestOpeningStateMachine动画器控制的动画器,它们的各项参数是什么,以及需要满足哪些条件才能设置这些参数。我们将把这些信息都记录在一个脚本中。创建一个新的脚本,命名为ChestAnimControls,并将其保存在Assets/Scripts/LootBox中。 -
为了给我们一个优雅、干净的方式来跟踪所有必要的信息,我们将使用类和枚举列表(我将在稍后解释这些是什么以及为什么我们使用它们)。从
ChestAnimControls类中删除Start()和Update()函数,并编写以下代码:// The different types of parameters public enum TypesOfParameters { floatParam, intParam, boolParam, triggerParam } // Properties of animation parameters [System.Serializable] public class ParameterProperties { public string parameterString; // What string sets it? public string whichState; // Name of the state it's called from, null=not called by the state machine public TypesOfParameters parameterType; // What type of Animator Parameter is it? public float floatValue; // Float value required, if float public int intValue; // Int value required, if int public bool boolValue; // Bool value required, if bool } // Make a list of all animatable objects and their parameters [System.Serializable] public class AnimatorProperties { public string name; // So the name will appear in the inspector rather than "Element 0, Element 1, etc" public Animator theAnimator; // The animator public List<ParameterProperties> animatorParameters; // Its parameter properties }你会注意到前面的代码有三个部分:
TypesOfParameters枚举、ParameterProperties类和AnimatorProperties类。首先,让我们看看TypesOfParameters枚举。这是一个 Animator 参数类型列表,可以在 Animator 中使用。枚举列表是一个包含一组用名称表示的常量的自定义类型。使用枚举列表的好处是列表在检查器中显示为下拉菜单。现在,让我们看看ParameterProperties类。场景中每个被动画化的对象都有一组与其 Animator 参数相关的属性,我们需要跟踪这些属性。类可以是一个有效的工具,用于将数据集分组在一起。因此,我使用类来分组参数名称、参数将被设置的状态、参数的类型以及如果是float、integer或Boolean参数,其值。请注意,参数类型是使用枚举列表TypesOfParameters定义的。这样做是因为每个动画师参数都有有限和特定的参数集。现在,让我们看看AnimatorProperties子类。对于场景中的每个对象,我们需要跟踪其名称、其动画师以及所有参数及其设置条件。请注意,参数及其属性的列表由ParameterProperties类定义。与 Unity 一起工作的一个重大好处是能够在检查器中分配和查看公共变量。然而,当你在一个类内部创建一个类时,公共变量在检查器中是不可见的,除非你在类上方放置[System.Serializable]。这给子类赋予了Serializable属性,并允许其公共变量在检查器中可见。
注意
我想指出,这段代码允许动画师拥有浮点数、整数和布尔值参数,尽管我们在任何动画师中使用的唯一参数是触发器。我这样写是为了使其更通用,以便你可以在未来的其他动画中重用此代码。
-
如果你觉得我们在前一步写的代码令人眼花缭乱,别担心——在检查器中看到所有内容列出来会稍微清晰一些。到目前为止,我们只是设置了几组不同的数据。现在,我们需要创建一个变量来使用这些信息。我们需要所有动画项目的列表,所以请将以下代码添加到你的脚本中:
public List<AnimatorProperties> animatedItems; //all the animated items controlled by this state machine -
通过将其拖动到其检查器中,将
ChestAnimControls脚本附加到主相机。 -
在
主相机的检查器中,点击动画项目旁边的箭头以展开列表:

图 14.60:胸动画控制组件
-
我们总共有六个项目需要由这个脚本和我们所创建的状态机来控制它们的动画师。因此,将列表的大小改为
6。展开Animator Items | Element 0及其Animator Parameters:![图 14.61:胸部的动画控制属性]()
图 14.61:胸部的动画控制属性
记住,
animatedItems变量是一个AnimatorProperties列表。所以,AnimatorProperties类。 -
我们首先需要列出数据的第一个项目是
Chest Open CanvasGameObject。在层次结构中将Chest Open Canvas从Chest Open Canvas输入到名称槽中,你会看到Element 0标签被替换为Chest Open Canvas。每次你在 Unity 的检查器中有一个对象的列表时,如果对象中的第一个是字符串,该字符串将替换元素 x 标签:

图 14.62:胸部的打开画布属性
-
现在,我们需要列出
Chest Open Canvas的动画器中使用的所有参数及其设置条件。它有两个我们需要列出数据的参数,所以将大小改为2。展开两个生成的元素,如下所示:![图 14.63:参数属性和参数类型]()
图 14.63:参数属性和参数类型
记住,
animatorParameters变量是一个ParameterProperties列表。所以,有两个ParameterProperties类。此外,在ParameterProperties类中,parameterType是一个TypesOfParameters变量。TypesOfParameters是一个枚举列表,所以任何该类型的变量将显示为一个下拉菜单,其中包含在定义的列表中出现的选项。 -
现在,我们需要列出
Chest Open Canvas的每个参数,ChestOpeningStateMachine将设置参数,并指定其参数类型:![图 14.64:胸部的打开画布属性]()
图 14.64:胸部的打开画布属性
由于每个都是触发动画参数,我们不需要担心浮点值、整数值或布尔值的值。
-
现在,我们可以像填写
Chest Open Canvas的信息一样,以同样的方式填写其他五个动画对象的动画师信息。按照以下方式填写Chest的动画师信息:

图 14.65:胸部的属性
- 按照以下方式填写
Coin的动画师信息:

图 14.66:硬币属性
- 按照以下方式填写
Heart的动画师信息:

图 14.67:心脏属性
- 按照以下方式填写
PowerUp的动画器信息:

图 14.68:PowerUp 属性
- 按照以下方式填写“按钮”的动画器信息:

图 14.69:按钮属性
-
现在我们已经初始化并定义了状态机所需的所有适当数据值,让我们实际上让状态机执行其适当的逻辑。首先,我们需要为动画器创建一个变量。将以下变量初始化添加到您的脚本中:
Animator theStateMachine; //the state machine animator component -
现在,在
Awake()函数中初始化状态机的动画器:void Awake() { theStateMachine = GetComponent<Animator>(); // Get the state machine } -
要让状态机自动设置各个动画器的各种参数,我们需要遍历我们所列出的所有动画项及其列出的参数。如果动画项有一个需要在状态机当前状态下设置的参数,我们将根据列出的条件设置它。创建以下函数以执行该功能:
// Functionality: Check if any of the animations need their parameters set // Called from enter state public void CheckForParameterSet() { // Loop through all of the objects foreach (AnimatorProperties animatorProp in animatedItems) { // Loop through its set of parameters foreach (ParameterProperties parameter in animatorProp.animatorParameters) { // Find the ones called on the current state if (theStateMachine.GetCurrentAnimatorStateInfo(0).IsName(parameter.whichState)) { // Determine parameter type // Float types if (parameter.parameterType == TypesOfParameters.floatParam) { animatorProp.theAnimator.SetFloat(parameter.parameterString, parameter.floatValue); } // Int types else if (parameter.parameterType == TypesOfParameters.intParam) { animatorProp.theAnimator.SetInteger(parameter.parameterString, parameter.intValue); } // Bool type else if (parameter.parameterType == TypesOfParameters.boolParam) { animatorProp.theAnimator.SetBool(parameter.parameterString, parameter.boolValue); } // Trigger type else { animatorProp.theAnimator.SetTrigger(parameter.parameterString); } } } } }CheckForParameterSet函数将确定在状态机当前状态下指定的动画器是否需要一个参数集。然而,这个函数目前还没有被任何地方调用。我们希望这个函数在状态机中的任何状态开始时被调用。我们可以通过状态机行为来实现这一点。打开ChestOpeningStateMachine动画器并选择ChestStateMachineBehaviour,然后点击ChestStateMachineBehaviour将被添加到Assets文件夹中。将其移动到Assets/Scripts/LootBox文件夹并打开它。 -
这个状态机行为中包含了很多内容。我们只需要
OnStateEnter函数。调整ChestStateMachineBehaviour类中的代码,包括以下内容以在开始时在ChestAnimControls脚本上调用CheckForParameterSet函数:ChestAnimControls theControllerScript; public void Awake() { // Get the script that holds the state machine logic theControllerScript = FindObjectOfType<ChestAnimControls>(); } // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { theControllerScript.CheckForParameterSet(); } -
我们需要这个脚本在每个不说是
ChestStateMachineBehaviour的状态上,对右侧列中的每个状态(在检查器中不说ChestStateMachineBehaviour的那些)进行操作。现在,状态机将适当地在进入适当的状态时调用每个单独项目的动画,但我们没有实际控制状态机流程的东西。 -
现在,它将停留在
Button Canvas上的Button中,显示ChestAnimControls.cs脚本:// Called by player interactions (Waiting On Player) public void PlayerInputTrigger(string triggerString) { theStateMachine.SetTrigger(triggerString); }这个函数将触发状态机由字符串作为参数指定的触发参数。
-
我们需要从“按钮画布”上的“按钮”调用该函数:

图 14.70:按钮的 On Click() 事件
-
现在,我们需要创建一些逻辑,使
Chest Open Canvas上的Button从Assets/Scripts/LootBox文件夹中的其他两个OpenCloseButton.cs转换过来。 -
使用以下代码将
UnityEngine.UI命名空间添加到OpenCloseButton脚本中:using UnityEngine.UI; -
现在,将以下代码添加到
OpenCloseButton脚本中:Text buttonText; Animator chestAnimController; void Awake() { buttonText = transform.GetComponentInChildren<Text>(); chestAnimController = Camera.main.GetComponent<Animator>(); } public void OpenOrClose() { Debug.Log("click"); if (buttonText.text == "Open") { chestAnimController.SetTrigger("OpenChest"); SetText("Close"); } else { chestAnimController.SetTrigger("CloseChest"); SetText("Open"); } } public void SetText(string setTextTo) { buttonText.text = setTextTo; }OpenOrClose()函数将由按钮在Chest Open Canvas上的Button调用,用于打开和关闭箱子。它将根据按钮上当前写入的文本设置适当的触发器,并使用SetText()函数将其文本更改为"Open"或"Close"。 -
将
OpenCloseButton脚本作为组件添加到Chest的Open Canvas上的Button。 -
现在,在
Chest的Open Canvas上添加以下Button:

图 14.71:按钮的 On Click() 属性
-
如果你现在玩游戏并点击
ChestOpeningStateMachine动画师中的AnimationComplete触发器。我们需要另一个脚本来设置此触发器。在Assets/Scripts/LootBox文件夹中创建一个新的脚本,命名为AnimationControls。 -
根据以下方式调整
AnimationControls类中的代码:Animator chestAnimController; void Awake() { chestAnimController = Camera.main.GetComponent<Animator>(); } // Call as an animation event on the last frame of animations public void ProceedStateMachine() { chestAnimController.SetTrigger("AnimationComplete"); } -
上述代码只是简单地使用
ProceedStateMachine()函数设置了AnimationComplete触发器。此触发器用于确保在开始下一个状态之前,每个单独的动画都能完全播放。为了确保动画触发器在整个动画播放完毕后才设置,我们将在必要的动画中适当的时间使用动画事件来调用
ProceedStateMachine()函数。当你使用动画事件时,你想要调用的函数必须位于与动画相同对象的脚本上。我们希望函数在
Chest Open Canvas、Chest、Coin、Heart、PowerUp和Chest Open Canvas上的Button的动画结束时被调用。因此,将AnimationControls脚本作为组件添加到每个对象上。 -
现在,我们需要将
ProceedStateMachine()函数作为动画事件添加到各种动画中。选择Chest Open Canvas并查看其CanvasGroupFadeIn动画。在其最后一个动画帧上,在时间轴的顶部深灰色区域右键单击,并从下拉菜单中选择ProceedStateMachine()。它将是列表中的最后一个函数。当你悬停在它上方时,现在你应该在最后一个关键帧上方有一个白色旗帜,上面写着 ProceedStateMachine:

图 14.72:将 ProceedStateMachine 事件添加到动画中
-
如果你现在玩游戏并点击
ChestFlyingIn动画。为了使动画序列完成,将ProceedStateMachine()函数作为动画事件添加到以下每个动画中:ChestFlyingIn、ChestOpening、CoinPopping、HeartPopping和PowerUpPop。 -
现在玩游戏几乎可以按照预期的方式运行。当重新播放动画序列时,物品从宝箱中弹出的时机有些问题。目前,
AnimationComplete触发器有些问题。我们需要确保当状态机重新启动时,该触发器被明确重置,以避免一些时序问题。为了解决这个问题,我们需要一个额外的状态机行为。在它的检查器中选择ResetTriggers。记住,每次你创建一个新的状态机行为时,它都会被添加到Asset文件夹中,所以将其移动到Asset/Scripts/LootBox文件夹。 -
我们希望
AnimationComplete触发器参数在ResetTriggers类被重置时重置,如下所示:// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { animator.ResetTrigger("AnimationComplete"); } -
现在,将
ResetTriggers状态机行为添加到所有三个等待玩家的状态中。
现在玩游戏应该能够正确触发动画序列,除了粒子系统,这部分将在下一章介绍。
摘要
在 Unity 中动画化 UI 元素与动画化任何其他 2D 对象并没有显著区别。因此,本章提供了一个动画的简要概述。本章还提供了一个使用状态机和动画事件创建复杂动画的工作流程示例。我们可以用动画做很多事情,本章中的示例展示了你在动画 UI 时可以使用的技术。希望这些示例能为你提供足够的技巧变化,以便你未来能够确定如何制作自己的动画。
在下一章中,我们将讨论如何在 UI 前面渲染粒子效果。
第十五章:UI 中的粒子
粒子效果是给游戏增添“活力”的一种有趣且吸引人的方式。Unity 引擎中的粒子系统为你提供了制作各种有趣效果(如火花、烟雾、火焰等)所需的工具。本章将讨论如何在 UI 中使用粒子效果。
在本章中,我们将讨论以下主题:
-
在 UI 中显示粒子效果的方法
-
将飞星添加到我们的宝箱动画中
本书是关于 UI,而不是粒子效果。由于粒子效果的复杂性质,我不会涵盖所有涉及的设置以及使用粒子效果的种种复杂性。我将指导你完成创建单个粒子效果的步骤,但不会进一步深入到创建粒子效果的过程。
技术要求
你可以在这里找到本章的相关代码和资产文件:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2015
UI 中的粒子
在 UI 中使用粒子是一个热门话题。似乎几乎每个带有宝箱的移动游戏都使用粒子,但没有标准化的实现方式。尝试在 UI 中使用粒子的问题在于,在将渲染模式设置为屏幕空间 - 覆盖的 Canvas 上,粒子会渲染在 UI 后面。

图 15.1:Canvas 背后的粒子渲染
前面的截图显示了我们的工作示例中的两个 UI Canvas 和一个粒子系统(白色圆点)。粉红色的背景在Background Canvas上,它使用HUD Canvas渲染,而HUD Canvas也使用HUD Canvas渲染。然而,我想让粒子也显示在HUD Canvas的前面。
对于这个问题有几个解决方案。我最喜欢的两种解决方案如下:
-
将
HUD Canvas更改为屏幕空间 - 摄像机,并调整其和粒子的排序顺序,使粒子显示在前面 -
使用第二个摄像机和一个渲染纹理,在 Canvas 中的原始图像上显示粒子
这两种方法都有利弊。第一种方法无疑是 easiest。它允许你通过仅对场景进行一到两次修改,就能在 UI 前查看粒子。然而,对于 UI 的渲染模式使用屏幕空间 - 摄像机可能对你的项目来说并不实用。如果你编辑游戏中摄像机的属性,UI 的属性也会改变。此外,在设置完成后更改 Canvas 的渲染模式可能会导致 UI 无法按照你最初期望的方式显示。
第二种方法实现起来并不复杂,但需要比第一种方法更多的工作。它的主要好处是可以在使用 Screen Space - Overlay 渲染的 Canvas 上渲染粒子。它主要的缺点,除了需要更多的工作来设置之外,可能还需要做出一些关于两个相机将要渲染什么的复杂决定,可能会稍微影响性能。本章的 示例 部分讨论了这种方法的一个示例。
对于这个问题,有其他解决方案,每个都比下一个更复杂(或成本更高),而你选择做什么取决于你的项目。有些项目完全放弃了粒子,并使用像 After Effects 这样的软件将粒子预渲染为精灵图集。有些项目使用 Asset Store 中的资源,而有些项目则完全使用脚本和着色器来处理一切。虽然我无法预见任何理由说明我提出的第二种解决方案不会适用于你的项目,但你的项目可能有一个我未考虑的边缘情况。希望在这种情况下,你将能够以最小的努力修改我的解决方案,使其适用于你的项目。
我给你的最好建议是尽早决定你是否会在你的 UI 中使用粒子。如果你知道你将要使用它们,提前规划你的 UI 布局和相机设置。此外,如果你想使用第一种方法,那就去做吧。
真的是太糟糕了,Unity 没有实现一个标准的处理方法。我猜想总有一天会有一个预先构建的 UI 粒子对象,这将使整个过程既简单又高效。
示例
对于本章的示例,我们将向第十四章第十四章中创建的动画添加一个粒子效果,该效果在宝箱打开时发生。
创建在 UI 中显示的粒子系统
让我们创建一个粒子系统,当箱子打开时会弹出。正如本章前面所述,我在 UI 前显示粒子的两种首选方式是使用 Screen Space - Camera 作为 Canvas 的 渲染模式 或使用渲染纹理。由于第二种选项更复杂,因此值得举例说明。你会注意到我们的所有 Canvas 都将它们的 渲染模式 设置为 Screen Space - Overlay,因此对于当前项目设置,使用 渲染纹理 是最佳方法。
我们将创建一个粒子系统,通过第二个相机渲染到纹理上,然后在该 UI 中的 Raw Image 上显示这个纹理。
要创建一个粒子系统,请完成以下步骤。我们将在下一节中讨论如何在 UI 中显示它:
-
我们首先需要做的是创建一个用于粒子的材质。在
Assets文件夹中创建一个名为Textures和Materials的新文件夹。 -
在新文件夹内右键单击并选择
StarsMaterial。 -
将
StarsMaterial的 着色器 设置为 Unlit/Transparent。 -
将
starIcon精灵拖动到其纹理槽中。 -
要在 UI 对象前面显示粒子,我们需要第二个相机。使用Ctrl + D复制
Main Camera并将其重命名为UIParticles Camera。 -
场景中只能有一个音频监听器,所以请删除新相机上的音频监听器组件。
-
删除动画器和胸动画 控制组件。
-
你也不希望后续的任何代码认为这可能是
Main Camera,所以将标签从MainCamera更改为Untagged。 -
这个相机将仅用于显示我们将要制作的粒子弹出效果,所以我们可以通过右键点击
UI Particles Camera并选择效果 | 粒子系统来将粒子系统设置为这个相机的子对象。

图 15.2:将粒子系统作为 UI Particles Camera 的子对象创建
注意
由于这本书不是关于粒子系统而是关于 UI,我们不会花时间详细讲解粒子系统的每个属性。幸运的是,大多数属性都有一定的自解释性,调整各种属性可以让你看到它们能做什么。因此,而不是逐个讲解粒子系统的每个属性,我将只提供必要的属性截图。
-
滚动到粒子系统组件的底部并点击以展开渲染器属性。
-
将
StarsMaterial材质分配给材质属性。这样现在你的粒子效果中就会出现星星了。

图 15.3:将 StarsMaterial 材质添加到渲染器材质属性
- 将变换
-90进行修改。现在粒子应该向上发射。

图 15.4:更改变换旋转
-
修改
10。 -
按如下方式更改粒子系统的持续时间、起始寿命和起始速度属性:

图 15.5:更新一些粒子系统设置
-
在
0和0.5上选择下拉菜单。 -
在
-45和45上选择下拉菜单。

图 15.6:更新起始大小和起始旋转
- 将
1和0.5进行修改。

图 15.7:更新粒子系统的设置
- 展开发射属性并使用加号添加一个爆发。

图 15.8:添加爆发
- 展开形状属性并从形状下拉菜单中选择半球。

图 15.9:更改形状
- 选择 Size over Lifetime 属性。

图 15.10:生命周期内大小属性
-
选择 Rotation by Speed 属性。
![图 15.11:通过速度旋转属性]()
图 15.11:通过速度旋转属性
现在,我们已经完成了粒子系统的属性设置。最终,我们将选择 Looping 和 Play on Awake,但到目前为止,我们将保留它们未选中,以便在游戏播放时可以持续看到粒子系统在播放。
-
我们要确保
UI Particles Camera只显示Particle System,而Main Camera显示除Particle System之外的一切。我们将通过 Layers 来实现这一点。选择 Layers 下拉菜单并选择 Add Layer…。
-
添加一个新的
UI Particles。 -
将
UI Particles分配给Particle System。 -
现在,我们需要指定每个相机将使用其
UI Particles Camera显示什么,仅显示Main Camera以排除 UI Particles。 -
现在,让我们让
UI Particles Camera渲染到一个纹理。在Textures and Materials文件夹中,右键单击并选择StarPopRenderTexture。 -
将其更改为
512x 512。 -
将
StarPopRenderTexture纹理分配给UI Particles Camera的 Camera 组件。

图 15.12:分配目标纹理
-
剩下的唯一任务是让渲染纹理在 UI 中显示。
使用
Particle Canvas创建一个新的 UI 画布。 -
选择
Particle Canvas后,选择Particle Renderer。 -
将
Particle Renderer的宽度和高度分别更改为512和512,以匹配StarPopRenderTexture的属性。 -
将
StarPopRenderTexture分配给Particle Renderer。

图 15.13:分配给纹理属性的渲染纹理
-
我们不希望这张图片阻挡我们的鼠标点击,因此从 Raw Image 组件中取消选择 Raycast Target。
-
现在,我们只需确保
Particle Canvas在其他两个画布之前显示。将Particle Canvas的Canvas组件设置为2。 -
现在玩游戏,你应该现在看到粒子显示场景。
-
我们将粒子系统设置为持续播放,因此取消选择 Looping 和 Play on Awake 以重置值到应有的状态。
那就是设置在 UI 中显示的粒子系统的全部内容。
调整粒子系统以在宝箱动画中播放
现在粒子系统已设置为在 UI 前显示,我们可以设置逻辑以正确顺序触发动画。为此,执行以下步骤:
-
我们希望粒子系统在箱子打开时播放,因此我们必须编写一些代码来控制其行为。在
Assets/Scripts文件夹中创建一个名为PlayParticles的新脚本。 -
编辑
PlayParticles类以包含以下代码:public ParticleSystem stars; void PlayTheParticles() { if (!stars.isPlaying) { stars.Play(); } } -
所有这些代码所做的只是检查粒子系统是否正在通过
PlayTheParticles()函数播放。如果没有播放,当函数运行时就会播放。 -
我们将通过在
Chest上的动画事件触发此函数。因此,将脚本添加到Chest作为组件。 -
将 层次结构 中的
Particle System分配到 星星 插槽。 -
在
ChestOpening动画的第一个帧上添加PlayTheParticles函数作为动画事件。

图 15.14:粒子动画事件
现在玩游戏应该会导致所有动画在适当的时间播放,粒子系统在箱子打开时显示。这样,宝箱动画教程就结束了!
摘要
初看之下,似乎你无法在设置为屏幕空间 - 覆盖的 Unity UI 中使用 Unity 粒子。然而,有一些简单的技巧可以让你在 UI 中实现粒子效果,并且让粒子渲染在它们前面。
在下一章中,我们将讨论如何使用世界空间画布渲染模式,使 UI 元素直接出现在你的 Unity 场景中,而不是在屏幕上。
第十六章:利用世界空间 UI
在第六章中,我们讨论了您可以分配给画布的三个不同的渲染模式。我们已经使用了屏幕空间叠加和屏幕空间相机,但尚未使用世界空间。正如第二章中所述,在世界空间中渲染的 UI 将直接放置在场景中。我们已经讨论了世界空间画布渲染的特性,因此本章将仅探讨何时使用它以及实现示例。
在本章中,我们将讨论以下主题:
-
何时使用世界空间 UI
-
在处理世界空间 UI 时需要考虑的一般技术
-
在 2D 游戏中使用世界空间画布创建相对于角色定位的状态指示器
-
使用世界空间画布在 3D 游戏中创建悬停在敌人头顶上的生命条
技术要求
您可以在此处找到本章的相关代码和资产文件:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2016
何时使用世界空间 UI
您可能想使用世界空间画布的许多原因。使用此渲染模式最常见的原因如下:
-
为了更好地控制单个 UI 对象相对于场景中对象的定位
-
为了旋转或弯曲 UI 元素
例如,游戏Mojikara: Japanese Trainer使用世界空间画布来旋转面板并保持 UI 对象,如文本,附加到 3D 对象上。正如以下截图所示,左侧的面板在 3D 空间中略微旋转,因为它位于世界空间画布上:

图 16.1:Mojikara: Japanese Trainer(由 Lisa Walkosz-Migliacio,Intropy Games 提供)
另一个旋转 UI 的例子可以在游戏Cloudbase Prime中找到,如下面的截图所示。它也使用了世界空间渲染来创建悬停在对象和角色上的指示器。

图 16.2:Cloudbase Prime(由 Tyrus Peace,Floating Island Games 提供)
在Cloudbase Prime中的所有 UI 都是在世界空间画布上完成的。这使得开发者能够创建酷炫的弯曲 UI,如下所示:

图 16.3:Cloudbase Prime(由 Tyrus Peace,Floating Island Games 提供)
在这里,您可以看到编辑器中的 UI 与玩家看到的 UI 之间的区别。这提供了一个很好的视角,了解 UI 是如何构建的:

图 16.4: Cloudbase Prime(由 Tyrus Peace,Floating Island Games 提供)
我建议您查看以下网站,以了解Cloudbase Prime如何实现世界空间 UI 的更多方式,因为它们确实很美:imgur.com/a/hxNgL。
世界空间 UI 的另一种常见用法是在场景中模拟计算机屏幕和显示器。例如,我为朋友名为Cloud Rise的 VR 游戏构建了以下 UI。显示器是通过将世界空间画布直接放置在游戏屏幕上方来模拟的。然后我能够轻松地锚定和动画化 UI;同样,我在屏幕空间中渲染了 UI。

图 16.5: Cloud Rise(由 Meredith Wilson,Bedhouse Games 提供)
通常,VR 游戏的交互式 UI 位于世界空间画布上,因为玩家不能与屏幕进行交互。VR UI 的常见用法是扁平的浮动面板或包裹面板。
悬停指示器到目前为止是世界空间 UI 最常见的使用;它们专门用于游戏角色头顶上的生命条,如下面的Iris Burning截图所示:

图 16.6: Iris Burning(由 William Preston,DCM Studios 提供)
当人们想到世界空间 UI 时,通常会想到 3D 游戏,因为他们认为 UI 看起来很远,但它在 2D 游戏中也很常见!管理和 RTS 游戏经常使用 UI 来创建按钮和进度条,以及其他 UI 元素与它们交互的对象保持位置。世界空间 UI 可以位于一个包含屏幕上所有项目的画布上,单个 UI 项目与它们所代表的项目的 2D 世界空间坐标相匹配,或者它们可以位于各自项目的单个画布上。我们将在本章末尾的示例中介绍如何使用世界空间 UI 创建 2D 游戏。
现在,让我们探索如何使用世界空间 UI。
在画布中适当地缩放文本
每次创建画布时,它都会以屏幕空间 - 叠加作为其渲染模式进行初始化。因此,当您将渲染模式属性更改为世界空间时,画布在场景中会变得很大。
当您将画布在场景中缩放到适当的大小时,文本可能会非常模糊或根本不可见。假设我们在屏幕空间 - 叠加中创建了以下画布,但决定将其放置在世界空间中:

图 16.7:屏幕空间中的画布 - 叠加
将其转换为4和3(因为它最初是以 4:3 的屏幕宽高比创建的),文本将消失!

图 16.8:屏幕空间中的画布 - 叠加
如果我将文本设置为允许水平溢出和垂直溢出,您会看到它与画布相比非常大。在下面的屏幕截图中的中间的小矩形是画布:

图 16.9:带有大量溢出文本的世界空间画布示例
为了解决这个问题,并使其看起来像我们想要的样子,我们需要调整1。
通常,为了确定新的905,将其除以新的4,然后在框中输入这个除法905/4将执行计算。)

图 16.10:动态每单位像素属性调整
然而,那个计算并没有得到我想要的精确外观。因此,我增加了大小,直到它看起来正确:

图 16.11:第十六章场景中动态每单位像素属性示例
每次您更改画布的宽度和高度时,您都必须调整动态每单位像素属性。减小画布的大小意味着增加动态每单位像素属性,而增加画布的大小意味着减小动态每单位的像素属性。
这里有两个画布,它们的大小都是前一个图中的四分之一。在上面的画布中,我改变了1和.75。在下面的画布中,我改变了0.25:

图 16.12:宽度和高度更改示例
在第一个例子中,因为我已经为我改变了350*4在1400(我喜欢 Unity 在框中进行计算)。
然而,在第二个画布中,我并不需要改变动态每单位像素的大小,因为以这种方式使用缩放属性进行缩放不需要我改变它。
从这个例子中我们可以看出,如果您的文本没有显示或看起来非常模糊,请调整动态每单位像素属性,直到它看起来应该是的样子,或者通过调整其缩放而不是宽度和高度来缩放您的画布。
使用世界空间 UI 时,文本缩放将是最重要的考虑因素,但让我们回顾一些其他重要主题。
在世界空间中工作时需要考虑的其他因素
在很大程度上,在世界空间中处理 UI 与在屏幕或相机空间中处理 UI 没有太大区别。尽管如此,您仍需注意一些事项。
当处理 3D 场景时,您可能希望 UI 始终面向玩家,无论玩家如何转动摄像头——这被称为广告牌效果。您可以通过在Update()函数中对对象的变换执行简单的LookAt()函数来实现这一点:
transform.LookAt(2*transform.position-theCamera.transform.position );
您可以使用前面代码的变体,具体取决于您希望旋转如何表现。
在 3D 世界空间 UI 中,另一个需要考虑的是它与相机的距离。你可能希望 UI 仅在它距离相机特定距离时渲染,因为当它太远时可能难以看到。
根据你的项目,使用世界空间 Canvas 可能会导致与 Raycasting 的困难,使得与 UI 交互成为问题。Floating Island Games 的 Tyrus Peace 建议,如果你最终需要创建自己的 Raycasting 系统,就像他在本章前面展示的Cloudbase Prime一样,创建自己的物理层。
示例
与世界空间 Canvas 一起工作与在屏幕和相机空间中与 Canvas 一起工作没有显著区别。世界空间 Canvas 提供了许多好处。如果你有一个存在于场景中的对象,并且 UI 专门与其位置相关联,使用世界空间 Canvas 非常有帮助,这样 UI 就可以跟随它无论它在哪里。这消除了尝试将对象的 World Space 坐标转换为 Screen 坐标以确保 UI 始终与对象对齐的需要。它还保证了 UI 对象将始终根据对象的位置正确显示,即使屏幕的分辨率发生变化。在本章中,我将介绍世界空间 Canvas 的两个常见用途:一个在 2D 空间中,另一个在 3D 空间中。让我们从 2D 空间开始。
2D 世界空间状态指示器
对于这个示例,我们将开始一个新的场景。为了让你不必构建场景,我们将从一个包含所有必需项目的资产包开始。
我们将创建一个 UI,允许角色在其头顶上方弹出状态指示器。场景播放 3 秒后,将在角色头顶上方出现状态指示按钮。一旦玩家点击状态指示器,将出现一个对话框。5 秒后,对话框将消失。状态指示器将在 10 秒后再次出现。

图 16.13:2D 世界空间 UI 示例
本示例中使用的艺术作品来自opengameart.org/content/medieval-rts-120。
要创建前一个示例中展示的状态指示 UI,请完成以下步骤:
-
导入“第十六章”的
- Example 1-Start.unitypackage包。此包包含一个带有背景图像和名为Mage的 2D 精灵的场景。包中包含的Assets/Scripts/MageInteractions.cs脚本控制状态指示器出现的计时器。此脚本需要两个 Canvas Group 项目——theExclamationPoint和theDialogBox——并包含一个名为ShowTheDialogBox()的函数,该函数可以通过按钮的OnClick()事件调用。 -
我们希望状态指示器和对话框与场景中“法师”的位置相关联。因此,我们将在“法师”的层级中创建一个 Canvas,并将其作为“法师”的子 Canvas 添加 UI Canvas。
-
选择新创建的
Canvas。将Canvas组件上的渲染模式更改为世界空间。 -
将
Main Camera分配到事件 相机槽位。 -
由于这个
Canvas是Mage的子对象,其坐标系相对于Mage。为了使其完美地位于Mage上方,将0更改为。 -
Canvas比Mage大得多。通过将1更改为更合理的尺寸。 -
现在我们已经将
Canvas缩放并定位在Mage周围的场景中,我们可以在其上添加 UI 元素。在层次结构中右键单击Canvas,创建一个 UI 按钮。将新按钮命名为Alert。 -
通过设置其Rect Transform组件的拉伸和锚点来调整
Alert的大小,使其与Canvas匹配。 -
更改
Alert按钮的Knob图像。它作为一个圆形比默认按钮精灵看起来更好:

图 16.14:选择 UI 旋钮图像
-
将
Alert的Text子对象更改为显示感叹号而不是Button。 -
按钮的文本目前不可见。为了修复这个问题,将
1000更改为。 -
将
Alert按钮移动到位于 Mage 头部的位置。

图 16.15:将感叹号移动到 Mage 的头上
-
添加一个
Alert按钮。 -
将
0设置为False,并将两个都设置为False。 -
给
Mage上附加的MageInteractions脚本中的Alert按钮一个ShowTheDialogBox()函数:

图 16.16:Alert 按钮的 OnClick()事件
-
在层次结构中右键单击
Canvas,创建一个 UI 文本对象。将新文本对象命名为Dialog。 -
通过设置其
Canvas来调整Dialog对象的大小,使其与Canvas匹配。 -
将
Dialog对象更改为显示Thanks!!!。同时,将文本居中对齐,并将水平溢出属性设置为溢出。 -
将
Dialog对象移动到位于 Mage 头部上方,如下所示:

图 16.17:对话框
-
添加一个
Dialog文本对象。 -
将
0设置为False,并将两个都设置为False。 -
选择
Mage并将Alert添加到对话框 框属性中的Dialog。
如果你玩游戏,你会看到 3 秒后出现感叹号按钮。点击按钮会使文本出现。尝试在场景中移动 Mage 角色。你会发现无论他在哪里,感叹号按钮和文本都会出现在他的头上。这是一个非常有用的技术,用于创建始终跟随移动角色的 UI 元素。
我知道现在的例子有点无聊,但我建议使用前两章中讨论的一些技术,为感叹号添加一个漂亮的弹跳动画,并使文本淡入淡出。
3D 悬停生命条
在 3D 场景中创建世界空间 UI 比在 2D 场景中创建世界空间 UI 需要更多的工作,如果相机可以在整个 3D 空间中旋转和移动。如果相机可以移动和旋转,UI 可能需要不断 面向 相机。否则,玩家将无法看到 UI 元素。
对于这个例子,我们将再次创建一个新的场景。为了您不必从头开始构建场景,我们将从一个包含所有必需项目的资产包开始。
我们将创建一个简单的悬停生命条,它始终面向相机。它还将接收点击,以便我们可以观察生命条减少:

图 16.18:带有悬停生命条的宇航员
本例中使用的艺术作品来自 opengameart.org/content/space-kit。
要创建一个始终面向相机并接收玩家点击输入的生命条,请完成以下步骤:
-
导入
第十六章– 示例 2 - Start.unitypackage包。此包包含一个场景,其中有一个面向相机的 3D 角色。相机附有一个简单的RotatingCamera脚本,该脚本使用鼠标围绕宇航员旋转相机。该包还包含一个附加到astronaut角色的ReduceHealth脚本。此脚本有一个ReduceHealthBar函数,当点击角色头部的生命条时,我们将调用此函数。 -
我们希望生命条与场景中宇航员的位置相关联。因此,我们将创建一个作为世界空间中宇航员子项的
Canvas。在层次结构中右键单击
astronaut并添加一个 UI Canvas 作为astronaut的子项。 -
选择新创建的
Canvas并将 Canvas 组件上的 渲染模式 改为 世界空间。 -
将
Camera分配到 事件 相机 插槽。 -
由于这个
Canvas是astronaut的子项,其坐标系相对于astronaut。为了使其完美地位于astronaut上方,请更改0。 -
Canvas的尺寸比astronaut大得多。通过更改10和1使其尺寸更合理。 -
将
Canvas定位在宇航员头部上方:

图 16.19:在宇航员上缩放 Canvas
-
现在我们已经将
Canvas缩放并定位在宇航员周围的场景中,我们可以在其上添加 UI 元素。在层次结构中右键单击Canvas并创建一个 UI 按钮。将新按钮命名为Health Bar。 -
通过设置其
Canvas使Health Bar的大小与Canvas匹配。 -
将
Health Bar改为 None 以使其显示为白色矩形图像。

图 16.20:向按钮添加空白图像
-
将
Health Bar的Text子项更改为显示点击以减少我的生命。 -
将
Text子项设置为 溢出。这将允许你在场景中看到文本当前渲染的大小:

图 16.21:健康条的放大文本
-
在 Text 组件上设置
10并取消选择 射线投射目标。 -
选择
Canvas并将鼠标悬停在 Canvas Scaler 组件中的 动态每单位像素 属性上,直到你看到鼠标光标周围出现两个箭头。一旦看到这些箭头,点击并拖动到右侧。这使得属性像滑块一样工作,允许你看到增加 动态每单位像素 属性如何连续改变场景中文本的渲染方式。这样做,直到文本适合画布:

图 16.22:健康条的放大文本
注意
当尝试在 3D 空间中使文本看起来很漂亮时,如果只更改 动态每单位像素大小 导致文本出现 锯齿状,则更改属性,直到文本在场景中看起来非常清晰。然后,通过改变文本对象的 Rect Transform 组件的 缩放 和 字体大小 来找到 最佳点。
-
右键单击
Health Bar按钮并添加一个 UI Image 作为子项。将新图像命名为Health Fill。 -
将
Health Fill调整到与画布Health Bar相匹配的大小,通过设置其Health Bar。 -
现在,将锚点和枢轴更改为 左扩展,使其将向 左侧 缩放。

图 16.23:左扩展锚点
- 在层次结构中重新定位
Health Bar,使其位于Text上方。这将使填充渲染在Text对象的后面。

图 16.24:GameObject 的层次结构
-
在
Health Fill上,将 颜色 属性更改为红色,并取消选择 射线投射 目标 属性。 -
选择宇航员并将
Health Fill对象分配给ReduceHealth组件。 -
添加一个
Health Bar按钮,该按钮在宇航员上调用ReduceHealth脚本的ReduceHealthBar函数:![图 16.25:健康条的 OnClick()事件]()
图 16.25:健康条的 OnClick()事件
现在玩游戏应该会导致点击
HealthBar按钮时Health Fill减少其填充值:![图 16.26:健康条按钮减少]()
图 16.26:健康条按钮减少
-
现在,我们只需要向
Canvas添加一个横幅效果。在Assets/Scripts文件夹中创建一个新的脚本,命名为BillboardPlane。 -
将
BillboardPlane类的脚本更改为以下内容:public Camera theCamera; void Update() { transform.LookAt(2 * transform.position - theCamera.transform.position); } -
将
BillboardPlane脚本附加到画布上。 -
将
Camera分配给BillboardPlane组件。
如果您现在玩游戏,您会看到当您移动相机时,生命条总是面向Camera。尝试更改LookAt()函数以产生更剧烈的效果。
摘要
世界空间 UI 在实现上与在相机或屏幕空间中渲染的 UI 没有显著区别。将 UI 添加到世界空间中,您将能够创建酷炫的效果,并让您对 UI 相对于场景中对象的定位有更多控制。
在下一章中,我们将讨论如何优化 Unity UI。
第十七章:优化 Unity UI
优化是我们用来确保游戏运行顺畅且帧率一致的过程。通过优化,我们首先定位到游戏中降低游戏性能的资源,然后实施解决方案以提升性能。
有很多因素可能导致游戏性能不佳或帧率低。这包括诸如未优化的光照、编写不良的脚本、大型资源和不当的 UI 构建等问题。由于这是一本关于 UI 的书,我们将仅关注如何通过改进 UI 构建来提升性能。
在本章中,我将讨论以下内容:
-
与优化相关的关键术语和基本信息
-
Unity 内提供的工具概述,这些工具可以帮助你确定你的游戏性能如何
-
UI 的各种优化策略
在你能够优化 UI 之前,你需要学习如何判断它是否具有性能。让我们回顾一下图形渲染的基本术语和原则。
注意
本章将仅涵盖基本层面的性能分析,其重点将更多地放在你可以做的事情来构建更好的 UI 上。如果你在章节结束时想了解更多关于性能分析的信息,我建议以下资源:unity.com/how-to/best-practices-for-profiling-game-performance#gpu-bound
优化基础
如我之前所述,优化是我们用来确保应用程序运行顺畅、帧率一致的过程。我们希望优化我们的游戏,确保所有玩家无论在何种条件下玩游戏都能有相同的体验。例如,如果我们正在制作 PC 游戏,我们希望确保每个玩家无论其机器的功率如何,都能有相同的体验。我们还希望确保如果一个玩家在屏幕上渲染了很多东西,游戏与屏幕上渲染较少东西时的延迟相比不会变慢。
我们不希望帧率降低,也不希望输入延迟。这对像游戏这样的应用来说非常重要。在第一人称射击游戏或平台游戏中,输入延迟可能导致玩家输掉比赛或从悬崖上掉下来。
让我们回顾一下在讨论优化时经常听到的基本术语。首先,我们将从确定应用程序性能的常用指标开始。
帧率
帧率是我们衡量应用程序优化的一项指标。这是一个很好的指标,因为它不仅仅是用户在后台无法察觉的事情。用户可以看到并注意到帧率的变化,因此测量其性能可以衡量我们的用户是如何体验我们的游戏的。
帧率可以用每秒帧数(fps)或毫秒时间来衡量。目标是无论游戏中的情况如何,都要保持一致的帧率。
当以每秒帧数来测量帧率时,每个单独的帧都会在特定的时间内渲染出来。假设你的目标是 60 fps。你需要确保应用程序的各个方面都能以 60 fps 运行。因此,你的目标是每秒保持一致的帧数。因此,当我们设置 fps 基准时,这意味着我们希望应用程序在整个应用程序的所有部分都能以该 fps 持续运行。我们不希望我们的游戏在开始时以 60 fps 运行,然后在 UI 打开时下降。
测量帧率的另一种方式是毫秒时间。每个帧都需要一定的时间来渲染。我们希望每个帧的渲染时间尽可能低。每帧 10 毫秒相当于 60 fps,这是一个很好的毫秒时间。
既然我们已经讨论了帧率是什么,那么让我们来谈谈资源在我们的计算机上是如何被使用的。
GPU 和 CPU
在尝试优化你的游戏时,你将调查游戏资源以确定资源使用最多的地方。你将想要确定问题是在 GPU 还是 CPU 上。中央处理单元(CPU)是计算机的大脑。图形处理单元(GPU)是渲染图像的部分。我们的资源是在 GPU 还是 CPU 上,这将决定优化解决方案。
CPU 上的问题示例包括指令过多,意味着运行了太多的脚本或者脚本运行时间过长。GPU 的问题通常意味着渲染了太多的东西。然而,CPU 在渲染中仍然扮演着一定的角色,因为 CPU 给出指令并告诉 GPU 要渲染什么。通常在讨论 GPU 和 CPU 时,你会听到draw calls这个术语。draw call 是指 CPU 告诉 GPU 需要在屏幕上绘制(或显示)某物。一般来说,你希望减少游戏产生的 draw call 数量。
我们将在本章的后续部分更深入地讨论 GPU 和 CPU,以及我们在设计 UI 时所做的各种选择如何受到 GPU 和 CPU 的影响。
现在我们已经介绍了一些优化基本概念,让我们回顾一下 Unity 提供的有助于优化的工具。
性能测定工具
在确定游戏性能时,你可以使用所谓的基准测试。基准测试允许你在给定条件下查看应用程序在不同时间运行得如何。在基准测试中,你收集性能指标并建立基准,或基准指标。然后,你将后续结果与该基准进行比较,以查看指标是否有所改善或恶化。这让你知道你所做的更改是否影响了游戏性能。
Unity 提供了一些工具,可以帮助您通过提供性能指标来评估游戏性能。现在让我们看看这些工具。
注意
注意环境以及您正在基准测试的内容。建议您在目标平台上进行基准测试。这样,您可以确定您计划在哪个最低标准设备上运行游戏。随着设备的升级,您游戏的性能将得到提升。
统计窗口
查看您游戏性能的一个简单方法是通过游戏视图中的统计窗口(或Stats窗口)。

图 17.1:分析统计窗口
如果您在游戏视图的右上角选择Stats按钮,您将看到有关项目的一些信息。在图形部分,您将看到每秒帧数和以毫秒为单位的时间。这将根据每一帧持续变化。您还可以看到关于 CPU 主线程和渲染线程的信息。
如果您以帧率作为基准,您可以在此处查看帧率值,并查看它们随时间的变化。记住,目标是保持一致性。
注意
您可以在此处了解更多关于统计窗口的信息:learn.unity.com/tutorial/working-with-the-stats-window-2019-3?uv=2019.4#
统计窗口提供了关于您游戏性能的一些直观的基本信息。然而,如果您想深入了解游戏运行情况,可以使用 Unity Profiler。
Unity Profiler
Profiler 显示了您游戏性能的详细信息。要查看 Profiler,请选择窗口 | 分析 | Profiler。当您玩游戏时,您可以看到哪些项目导致了性能问题。

图 17.2:查看 Unity Profiler
在Profiler中,您可以选择仅 UI 模块,以缩小每个 UI 元素对性能的影响。

图 17.3:仅启用 UI Profiler 模块
完成这些操作后,您可以看到每个 Canvas 的信息。

图 17.4:在 Profiler 中观察 UI 对象
注意
您可以在此处了解更多关于 Unity Profiler 及其使用方法:docs.unity3d.com/Manual/Profiler.xhtml
如果您对游戏中物品的批处理方式感兴趣,可以使用 Unity Frame Debugger。
Unity Frame Debugger
Frame Debugger 可以帮助您解决 UI 的批处理问题。您可以通过窗口 | 分析 | Frame Debugger访问 Frame Debugger。

图 17.5:审查帧调试器
启用帧调试器将允许您查看各种渲染事件。点击它们将在游戏窗口中前进,为您提供事件的预览。这将显示哪些项目被组合到批次中。
注意
您可以在此处了解更多关于帧调试器的信息:docs.unity3d.com/Manual/frame-debugger-window.xhtml
现在我们已经讨论了优化的基础知识,我们可以开始讨论优化 UI 的方法。
基本的 Unity UI 优化策略
记住,每个画布都有自己的画布渲染器组件。画布将所有元素组合成一起渲染的批次。如果一个画布的几何形状需要重建,则该画布被认为是脏的。优化 UI 的主要目标之一是减少画布或其元素被认为是脏的次数,以减少画布需要重新批次的次数。考虑到这一点,让我们看看一些优化 Unity UI 的技术。
使用多个画布和画布层次结构
每当画布上的一个元素被修改时,画布被认为是脏的,并且会向 GPU 发送一个绘制调用。如果画布上有多个项目,所有画布上的项目都需要重新分析,以确定它们应该如何最好地绘制。因此,修改画布上的一个元素需要 CPU 重建画布上的每个元素,这可能会导致 CPU 使用量突然增加。因此,你应该将 UI 放在多个画布上。
在确定如何分组您的画布时,考虑画布上的项目需要更改的频率。将所有静态 UI 元素分组到与动态项目分开的单独画布上是一种良好的做法。这将阻止静态元素在动态元素更改时需要重新绘制。此外,根据它们将何时更新,将动态元素拆分到画布中,尽量将同时更新的元素放在一起。
在尝试减少画布上的绘制调用时,还需要考虑元素的 z 坐标、纹理和材质。根据这些属性对 UI 元素进行分组也可以减少与 UI 相关的 CPU 使用量。
最小化使用布局组
GetComponent用于每个具有布局组的父对象。这使得嵌套布局组非常低效。
为了避免这种情况,您可以直接不使用布局组。相反,您可以使用锚点并编写自己的布局代码来动态放置项目。
完全避免它们并不一定是每个人的最佳解决方案,因此,如果你选择使用它们,尽量避免嵌套布局组或具有大量子项的布局组。你也可以选择只为动态元素使用布局组,并避免使用静态元素。你还可以在动态 UI 正确定位后立即禁用它们。
适当地隐藏对象
如果你想在画布上隐藏元素,如果可能的话,最好禁用整个画布组件。记住,画布组件是渲染 UI 的部分,因此禁用画布组件会导致它停止渲染。建议你禁用画布组件,而不是简单地改变画布的可见性,因为即使画布不可见,它也会继续进行绘制调用。禁用组件不会导致画布重建。这比启用和禁用画布 GameObject 更节省资源,因为这将触发OnEnable和OnDisable方法。
如果你想要隐藏游戏中带有 UI 的所有内容,例如你有一个完全覆盖屏幕的暂停菜单,你应该停止相机渲染游戏中的任何内容,除了 UI。例如,如果你有一个完全覆盖屏幕的菜单,即使你看不到菜单后面的项目,它们仍然被渲染。
适当地安排对象池的启用和禁用
对象池是一种优化技术,用于减少可重复使用对象创建(或实例化)的次数。在对象池中,你会在游戏开始时将一组禁用对象放置在池中,然后,而不是在游戏进行时实例化这些对象,你从池中取出它们。
对象池是提高游戏性能的绝佳方式,因为它通过创建可以重复使用的对象集合来减少对象必须创建的次数。
注意
有关对象池概念的更多信息,请访问以下网站:learn.unity.com/tutorial/introduction-to-object-pooling
由于重新父化 UI 对象会导致画布被标记为脏,因此在使用对象池时,你需要仔细安排禁用、启用和重新父化的时间。由于 Canvas 内部元素的动态更改会导致 Canvas 向 GPU 发送绘制调用,因此在禁用它们之前或之后不适当地父化项目,可能会不必要地加倍你的绘制调用。目标是确保池中的对象不会向 GPU 发送任何绘制调用。因此,池中的对象应该始终以禁用状态附加到池中。
如果你将对象放入对象池中,请先禁用它,然后将其重新父级到池中。通过确保在放入池之前将其禁用,你可以消除池需要重建的需求。相反,如果你想从池中移除对象,请先重新父级,然后启用它。这同样可以消除池发送绘制调用的需求。
减少射线路径计算
每个 UI Image 组件都有 射线路径目标 属性。默认情况下,这是选中的。如果你不希望你的 UI 元素阻止射线路径,请禁用它以减少射线路径检查。

图 17.6:射线路径目标属性
此外,如果你的 UI 完全不可交互,你可以移除 Graphic Raycaster 组件,以消除它引起的非必要计算。

图 17.7:图形射线路径组件
使用本章中介绍的基本 Unity UI 优化策略,将使你能够创建不会反向影响游戏性能的 UI。
摘要
在本章中,我们讨论了优化 Unity UI 的基础知识。我们回顾了 Unity 优化的一些基本概念,查看允许我们评估游戏性能的工具,然后讨论了创建高性能 UI 的策略。
注意
本章仅关注如何通过 UI 提高游戏性能。但请记住,你的游戏有多个方面可能导致性能问题。为了获得关于提高游戏所有方面性能的良好资源,我建议查看 Unity 提供的免费电子书:resources.unity.com/games/performance-optimization-e-book-console-pc
在下一章中,我们将讨论由 Unity 创建的新 UI 系统,称为新的 Unity UI 工具包。它不仅可以在运行时创建 UI,还可以在编辑器中创建。因此,它可以用来帮助你创建工具以改进你的工作流程。
进一步阅读
关于优化 UI 的更多信息,我推荐以下资源:
第五部分:其他 UI 和输入系统
在这部分,你将不再探索 Unity UI 系统,而是查看 Unity 提供的另外两个用于创建 UI 的系统——UI Toolkit 和 IMGUI。你还将了解如何使用新的输入系统来处理输入。
本部分包含以下章节:
-
第十八章, 开始使用 UI Toolkit
-
第十九章, 使用 IMGUI
-
第二十章, 新的输入系统
第十八章:使用 UI 工具包入门
到目前为止,本书的重点一直是 Unity UI (uGUI)。然而,Unity 一直在开发另一个系统,你可以用它为你的游戏开发 UI:UI 工具包。它基于网页设计的原理,旨在允许你以比 uGUI 更灵活和可扩展的方式创建 UI。它是通过将 UI 的设计和开发从场景和 GameObject 中分离出来,并通过代码和样式表创建 UI 来实现这一点的——就像在网页设计中一样。
UI 工具包是一个完全不同的系统,因此如果你习惯于使用 uGUI 开发 UI,那么一开始使用它可能会感到有些不适应。然而,如果你有 .xhtml 和 .css 的经验,或者使用 XML 开发 Android 或 iOS 接口,那么这些都应该非常熟悉。
由于 UI 工具包是一个完全不同的系统,它使用与本书其他章节完全不同的原则,要完全解释你可以用它做什么,可能需要一本完全致力于它的教科书。因此,本章将提供你开始使用该系统所需的基本信息,但不会全面讨论其各个方面。如果想要进一步学习 UI 工具包,我将在本章中提供额外的资源供你回顾。
在本章中,我将讨论以下内容:
-
UI 工具包概述以及如何安装它(如果它尚未包含在你的 Unity 版本中)
-
UI 工具包的各个部分如何协同工作以创建和样式化界面
-
视觉元素是什么?UI 工具包层次结构是如何工作的?
-
如何使用 UI 构建器设计和布局 UI 工具包界面
-
如何在 C# 中访问 UI 工具包构建的 UI
-
创建一个与你一起在 Unity 编辑器中玩耍的虚拟宠物,并鼓励你
-
使用 UI 构建器创建样式表和动画过渡
-
使用网络请求随机生成你的 UI 的图像和文本
注意
本章假设你具有 HTML 和 CSS 的基本知识。然而,拥有更广泛的 HTML 和 CSS 经验将使采用 UI 工具包系统变得更加容易。
在我们深入了解 UI 工具包的内部工作原理之前,让我们回顾一下它的用例以及你将在何时使用它。
技术要求
你可以在此处找到本章的相关代码和资产文件:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2018
UI 工具包概述
如你所回忆的,从 第五章 中,Unity 中可以使用总共三个 UI 系统。到目前为止,这本书的重点一直是 Unity UI (uGUI),它可以用来制作游戏中的 UI(即运行时 UI)。然而,如果你想制作可以在你的编辑器中查看的 UI,你必须使用不同的系统。可以用来创建编辑器 UI 的两个系统是 IMGUI,我们将在下一章中讨论,以及 UI Toolkit。然而,虽然 IMGUI 只能用来制作编辑器 UI,但 UI Toolkit 可以用来制作运行时和编辑器 UI。

图 18.1:比较三个 UI 系统
Unity 的目标是完全用新的 UI Toolkit 替换 uGUI 系统。然而,它仍在开发中,并不具备 uGUI 所有的功能。例如,UI Toolkit 无法创建位于 3D 世界中的 UI,就像我们在 第十六章 中讨论的那样。它只能创建覆盖在屏幕上的 UI。此外,它无法使用自定义材质和着色器,并且不容易从 MonoBehaviours 中引用。
那么,你何时会想使用 UI Toolkit 呢?如果你想要创建风格化的 UI 而不处理与预制件和预制件变体一起出现的冗余,UI Toolkit 非常有用。你可以通过代码更改 UI 的设计,而不是修改 GameObject 的检查器属性。这使得 UI 更容易在多个项目中共享,并且可以快速自定义。此外,如果你有网页开发经验,你可能对 UI Toolkit 的工作流程更舒适。
如果你在处理一个较旧的项目,你将需要这本书中讨论的信息,这些信息集中在 uGUI 上,因为它仍然是使用最广泛的 UI 系统。然而,如果你正在创建一个新项目,并希望考虑为你的项目使用 UI Toolkit,我建议你查看以下文档,以确保你的需求能够得到满足:docs.unity3d.com/Manual/UI-system-compare.xhtml
既然我们已经讨论了何时可以使用 UI Toolkit,让我们开始在我们的项目中安装它。
安装 UI Toolkit 包
根据你使用的 Unity 版本,可能没有安装必要的 UI Toolkit 包。允许你开发编辑器 UI 的 UI Toolkit 版本包含在所有相对较新的 Unity 版本中;然而,允许你创建运行时 UI 的版本只包含最新的版本。
你可以通过转到 窗口 | UI Toolkit 来确定你是否已经安装了所有必要的 UI Toolkit 包版本。如果 UI Builder 子菜单不存在或 UI Toolkit 菜单项完全缺失,你需要导入该包。

图 18.2:确定是否已安装 UI 工具包包
如果你需要安装 UI 工具包,你可以通过从 git URL 导入包来完成。为此,完成以下步骤:
-
使用 窗口 | 包管理器 打开包管理器。
-
选择 + 按钮,然后选择 从 git URL 添加包…。

图 18.3:通过 git URL 添加包
-
在出现的文本框中输入
com.unity.ui。确保地址后不要添加空格。如果你添加了空格,你会收到一个错误消息。导入完成后,你应该能在你的包窗口中看到以下工具包。

图 18.4:UI 工具包
-
如果你希望将示例导入到你的项目中,请选择 导入 按钮以下载它们。
-
你可能会注意到 UI 工具包的描述中提到 UI Builder 不包含在这个包中。因此,你必须单独导入那个包。为此,选择 + 按钮,然后再次选择 从 git URL 添加包…。
-
这次,在出现的文本框中输入
com.unity.ui.builder。导入完成后,你应该能看到以下包。

图 18.5:UI Builder 包
- 与其他包一样,你可以使用 导入 按钮将示例导入到你的项目中。
现在我们已经安装了适当的包,让我们来看看 UI 工具包系统的各个部分。
UI 工具包系统的部分
UI 工具包使用一组资产和 GameObject 组件来创建 UI。用于创建 UI 的主要资产如下:
-
UI 文档 (UXML)
-
样式表 (USS)
-
面板设置
-
主题样式表 (TSS)

图 18.6:制作 UI 工具包界面时使用的资产
.uxml 扩展名)。此文件使用 Unity 可扩展标记语言 (UXML) 来定义 UI 的布局和结构。虽然 UXML 是一个 Unity 特定的标记语言,但它与 HTML 和 XML 等其他标记语言类似。你可以通过在资产文件夹中右键单击并选择 创建 | UI 工具包 | UI 文档,然后适当地命名文件来创建 UXML 文件。
.uss 扩展名)。此文件用于指定在 UXML 文件中可以引用的样式属性。当与 HTML 一起工作时,这与 CSS 文件非常相似。你可以通过在资产文件夹中右键单击并选择 创建 | UI 工具包 | 样式表,然后适当地命名文件来创建 USS 文件。
.asset 扩展名)定义了 UI 将拥有的属性集合。

图 18.7:默认面板设置资源的属性
(.tss 扩展名)。它通过维护一个集合,确定哪些面板设置将与哪些 TSS 和 USS 文件一起使用,如下图所示。

图 18.8:TSS 文件检查器
当您创建面板设置资源时,将在同一文件夹中自动创建一个名为 Unity Themes 的文件夹。在其中,将为您创建一个名为 UnityDefaultRuntimeTheme.tss 的单个 TSS 文件。但是,您可以通过在资源文件夹中右键单击并选择 创建 | UI 工具包 | TSS 主题文件 来创建更多 TSS 文件。
UI 工具包系统的最后一部分是 UI 文档 组件,如下截图所示。

图 18.9:UI 文档组件
此组件添加到场景中的 GameObject,并允许使用 UI 工具包创建的 UI 进行渲染。它有三个属性:面板设置、源资源 和 排序顺序。
您可以将一个面板设置资源分配给 面板设置 属性,以表示由该组件渲染的 UI 将具有哪些设置。源资源 属性是您分配 UI 文档(UXML)文件的地方。排序顺序 属性确定由 UXML 文件定义的 UI 相对于场景中任何其他使用相同面板设置的 UXML 文件将渲染的顺序。您可以通过在 检查器 中使用 添加组件 并搜索 UI 文档 来将此组件添加到 GameObject。
现在我们已经知道了所有必需的资源和组件,让我们回顾一下如何使用 UI 构建器实际构建这些资源。
视觉元素和 UI 层级
与 uGUI 构建的 UI 由多个 GameObject 组成的方式相同,UI 工具包构建的 UI 由 视觉元素 组成。在 UI 工具包系统中,您有多种 UI 元素可供选择(按钮、标签、滑块等),但视觉元素是所有这些元素的基础类,因此它们都从它那里继承了许多属性。
正如 GameObject 在 Unity 编辑器层级中组织一样,UI 工具包通过称为 UI 层级 的东西来组织其视觉元素。例如,假设我想创建一个类似于 图 18.10 中显示的 UI。

图 18.10:简单示例 UI
如果使用 uGUI 构建,编辑器中的场景层级与使用 UI 构建器创建的 UI 层级非常相似,如图 18.11 所示。

图 18.11:uGUI 与 UI 工具包 UI 层级的场景层级
除了命名和嵌套的一些细微变化外,它们几乎相同。你可以将 UI 文档视为在 UI 层次结构中以Chapter18.uxml表示,它在 UI 层次结构中具有与场景层次结构中的画布类似的功能。
注意
UI 层次结构截图是从 UI 构建器工具中获取的,我们将在下一节中讨论。
当使用 uGUI 时,每个 GameObject 的对齐和位置基于它嵌套的父对象。这对于使用 UI 工具箱系统创建的视觉元素也是如此。因此,对项目嵌套有感觉对于使用 UI 工具箱系统开发 UI 很重要。
注意
你可以在提供的代码包中的 Unity 项目中找到为图 18.10和图 18.11开发的示例 UI。它们可以在标记为Chapter18.asset的场景中找到。在场景层次结构中,Canvas游戏对象包含 uGUI 版本,而UIDocument游戏对象包含 UI 工具箱版本。
现在我们已经回顾了 UI 工具箱系统的一些基本概念,让我们看看如何实际使用它来构建 UI。
使用 UI 构建器创建 UI
要使用 UI 工具箱创建 UI,你必须创建一个 UI 文档来描述你将渲染的视觉元素以及它们的布局和其他属性。在 UI 工具箱系统中创建 UI 文档有两种方式:
-
编写布局视觉元素的代码
-
在 UI 构建器中排列元素,并让它为你编写代码
你还可以结合两者,在通过代码或 UI 构建器编辑 UI 之间切换。
要访问 UI 构建器,请选择窗口 | UI 工具箱 | UI 构建器。

图 18.12:UI 构建器窗口
在这个窗口中,你可以从库(左下角)拖放各种视觉元素到视口(中央)。你还可以在这个视口中调整视觉元素的大小和位置。你可以通过层次(左中)更改父级,从而改变视觉元素的排列方式。此外,你还可以在UXML 审查面板中查看从布局生成的代码文件,以及在USS 预览面板中查看代表你样式的代码文件(两者都在底部中央)。本章末尾的示例部分提供了一个如何使用 UI 构建器构建 UI 的示例。
要使用你用 UI 构建器创建的 UI 布局,你必须将 UI 文档保存为.uxml文件。按下Ctrl + S将为你将文件保存到您选择的位置。我建议你将它们保存在Assets中的UI Toolkit文件夹内。
现在我们已经回顾了如何创建 UI 文档,让我们回顾一下如何将文档与你的游戏场景关联起来。
你可以通过在层次结构中双击任何视觉元素并输入新名称来重命名任何视觉元素。当使用 UI Builder 时,如果需要通过 C#代码引用它们,视觉元素的名称将作为变量。因此,如果你计划通过 C#代码访问它们,给视觉元素起一个独特的名称是很重要的(有关更多信息,请参阅使用事件使 UI 可交互部分)。
使用 UI 文档组件
如本章UI Toolkit 系统部分所述,UI 文档组件必须添加到场景中的 GameObject 中,以便渲染你创建的 UI。你可以通过将 UI 文档组件添加到现有的 GameObject 或选择+ | UI Toolkit | UI Document来实现这一点。这个 GameObject 将有一个Transform组件和一个UI Document组件。

图 18.13:UIDocument GameObject 的检查器
如果你在你的项目“资产”文件夹中还没有名为UI Toolkit的文件夹,系统会自动为你创建一个。在这个文件夹中,你会找到一个名为PanelSettings的资产,以及一个名为UnityThemes的文件夹。在UnityThemes文件夹中,你会找到一个名为UnityDefaultRuntimeTheme.tss的 TSS 文件。你的UIDocument GameObject 的PanelSettings资产被分配到你在源资产属性中创建的.umxl文件中,以便查看你在场景中创建的 UI。
现在我们已经了解了构建和渲染 UI 的基础知识,让我们看看如何使用 UI Toolkit 编写的 UI 的交互代码。
使用 C#使 UI 可交互
UI Builder 和你的 UXML 文档只处理 UI 的视觉属性。虽然你可以通过使用样式表来分配一些基本响应(如悬停时改变视觉),但你将需要创建 C#脚本来处理与你的 UI 相关的任何逻辑或事件。
UIElements 命名空间
要能够编写与 UI 文档交互的代码,你必须使用UnityEngine.UIElements命名空间。如果你使用 UI Toolkit 来制作编辑器 UI,你可能还需要UnityEditor.UIElements命名空间。
注意
有关UnityEngine.UIElements和UnityEditor.UIElements命名空间的信息,请参阅以下资源:docs.unity3d.com/Packages/com.unity.ui@1.0/api/UnityEditor.UIElements.xhtml。
获取 UI 文档变量的引用
在你的 C#脚本中,你可以创建一个指向UIDocument类型的引用变量。你可以像通常分配其他类的变量一样分配这个变量:在GetComponent中分配它,从一个脚本中传递它的引用,等等。
注意
虽然你可以使用FindObjectOfType,但这不是推荐用于查找UIDocument引用的方法,因为你很可能在场景中有多个 UI 文档。
在获取到您想要与之工作的 UIDocument 引用后,您可以通过获取 UI 文档上的根视觉元素引用,然后通过其名称查询该元素的视觉元素类型,来获取其中的视觉元素引用(例如,按钮、标签等)。
例如,在章节中前面显示的 UI 中,我将 Label 重命名为 DogLabel,将 Button 重命名为 CatButton,如下所示:

图 18.14:标签和按钮的新名称
要在 C# 脚本中创建这些变量的引用,我首先创建了 UIDocument、Label 和 Button 的引用变量,如下所示:
[SerializeField] private UIDocument uiDocument;
private Label dogLabel;
private Button catButton;
uiDocument 变量将在检查器中分配。与所有 MonoBehaviour 脚本一样,此脚本需要附加到场景中的 GameObject 上。我将此脚本附加到 UIDocument GameObject 上,然后将 UIDocument GameObject 拖动到 Ui 文档属性。

图 18.15:将 UIDocument 分配给脚本
重要的是要注意,当您在 C# 中引用 UIDocument 时,您引用的是 UIDocument 组件,而不是 UI 文档源文件。
为了初始化 dogLabel 和 catButton,我获取了 uiDocument 上的 rootVisualElement 引用,然后使用 Query 方法找到每个视觉元素。
void Start(){
var root = uiDocument.rootVisualElement;
dogLabel = root.Q<Label>("DogLabel");
catButton = root.Query<Button>("CatButton");
}
注意我如何使用 root.Q 来查找 Label,以及使用 root.Query 来查找 Button。您可以使用任一方法,我这样做只是为了让您知道有两种不同的方法。您可以使用您喜欢的任何一种。
管理视觉元素事件
由于 UXML 文档不管理事件,您可以通过订阅事件或注册回调方法来在事件触发时找出在 UXML 文档中定义的 UI 是否被交互。
例如,如果我想在点击 catButton 时在控制台记录一条消息,我需要创建一个订阅 catButton 的 clicked 事件的函数。
首先,我在我的脚本中添加了以下方法:
private void OnCatButtonClicked() {
Debug.Log("CatButtonClicked");
}
然后,我需要在 Start() 方法中订阅 catButton 的 clicked 事件,如下所示。
catButton.clicked += OnCatButtonClicked;
为了避免在 GameObject 禁用或销毁时事件订阅出现错误,我在 OnDisable() 方法中取消订阅事件。
private void OnDisable() {
catButton.clicked -= OnCatButtonClicked;
}
这将导致每次点击按钮时,OnCatButtonClicked 消息都会在控制台中显示。
访问视觉元素属性
与 uGUI GameObject 可以通过 C# 代码更改其属性的方式类似,视觉元素也可以通过 C# 代码调整其属性。例如,将 OnCatButtonClicked() 方法更改为以下内容将使 Text 字段变为 Meow!
public Camera theCamera;
void Update()
{
transform.LookAt(2 * transform.position - theCamera.transform.position);
}
上述代码导致 UI 如下显示:

图 18.16:通过按钮点击更改文本后的 UI
我还可以说很多关于使用 UI Toolkit 的事情,但正如我在本章开头所说的,我只能提供一个概述——否则,我不得不写一本完全关于 UI Toolkit 的书!然而,我相信我已经提供了足够的概念概述,以便你可以深入到下一节中的示例工作。
示例
现在我们已经拥有了开始使用 UI Toolkit 创建一些 UI 所需的基本构建块。我没有深入探讨样式表或各种视觉元素的性质,但我会在这里展示一些示例,以扩展这方面的内容。如果你完成这些示例后还想了解更多,请参阅 资源 部分,那里有一系列示例和文档。
当使用 UI Toolkit 开发 UI 时,工作主要有两个部分:
-
使用 UI Builder 布局 UI(或通过编辑 UXML 文档)
-
编写 C# 代码以添加 UI 功能
因此,每个示例都将分为两部分:一部分用于构建 UI,另一部分用于编写功能。
由于 UI Toolkit 可以用于编辑器和运行时 UI,我将展示两个示例。让我们从编辑器示例开始!
使用 UI Toolkit 制作编辑器虚拟宠物
尽管这本书的主要重点是运行时 UI,但我不能不向你展示如何使用 UI Toolkit 创建一些编辑器 UI,因为这是它的主要优势之一。编辑器 UI 通常用于工具,以促进开发、提高生产力、简化工作流程等。最初,我计划向你展示如何制作一些通用的编辑器工具,你可以扩展它们来改进你的工作流程。然而,我决定制作一个可以改善你心情的编辑器工具。毕竟,自我照顾对生产力至关重要!虽然这个例子在传统意义上可能没有实际用途,但它很有趣,展示了如何使用大量的编辑器 UI 功能,所以这是一个双赢的局面。
这个例子涵盖了如何制作一个虚拟宠物,它在你的编辑器窗口中与你一起闲逛,并在你点击它时向你表示赞赏。这也展示了如何在 Unity 编辑器中创建动画精灵图集。

图 18.17:本例中创建的编辑器虚拟宠物
在这个例子中使用的猫精灵是从这里提供的公共领域艺术资产中派生出来的:opengameart.org/content/a-cat。
注意
对于这个例子,由于我们正在制作一个与迄今为止我们所做的任何工作都不相关的编辑器工具,你可能需要创建一个新的 Unity 项目来完成这项工作。如果你这样做,你需要重新导入任何 2D UI 包。
让我们从使用 UI 构建器布局 UI 开始!
使用 UI 构建器制作我们的编辑器窗口的 UI
要创建如图 18.17所示的虚拟宠物,请完成以下步骤:
-
当为您的编辑器编写代码时,您应该将所有编辑器代码放在一个名为
Editor的文件夹中。这表示 Unity 中任何位于其中的代码仅用于编辑器,而不是在运行时使用。在Assets中创建一个名为Editor的文件夹,并在其中创建一个名为Resource的文件夹。 -
将在书籍源文件中找到的
idleCat.png精灵图集拖放到您新创建的Assets/Editor/Resources文件夹中。 -
设置其导入设置,使其纹理类型为精灵(2D 和 UI),精灵模式为多个。
-
打开精灵编辑器,您会注意到图像非常模糊。我们需要进一步编辑导入设置来修复这个问题。

图 18.18:模糊的精灵图集
-
返回精灵的
64和压缩为无。同时,将过滤器模式更改为点(无过滤器)。 -
选择应用,精灵现在应该是清晰的像素艺术猫。
-
现在,返回到
16x16。

图 18.19:切割的精灵图集
-
选择切片按钮以确认更改,然后选择应用以提交更改。您现在可以关闭精灵编辑器。
-
现在我们已经正确导入精灵,我们就可以在 UI 构建器中构建编辑器 UI 了。通过窗口 | UI 工具包 | UI 构建器打开 UI 构建器。
-
在层次结构中选择
<未保存文件>.uxmlUI 文档,以在检查器中显示其属性。

图 18.20:UXML 文件的检查器
-
现在将
100x100。 -
通过选择
Assets/Editor/Resources文件夹并命名文件为IdleCat.uxml(.uxml扩展名将自动为您添加)来保存 UI 文档。 -
现在,让我们将猫添加到视口中。因为我们希望猫是可点击的,所以最简单的方法是使用按钮。将图标从控件面板拖放到视口中。注意,一旦这样做,按钮将位于容器顶部。

图 18.21:添加按钮
-
在层次结构中选择
Button以打开其检查器。 -
展开背景属性,以便您可以更改按钮的外观。
-
从图像属性的旁边下拉菜单中选择精灵。这将允许您选择精灵类型的图像。

图 18.22:将背景图像类型更改为精灵
- 在
idleCat_0中选择圆形。一旦这样做,您应该在视口中看到以下内容:

图 18.23:应用了猫图像的按钮
- 让我们先将文本移开。在
Button的文本属性顶部。

图 18.24:从按钮中移除文本
- 现在,让我们调整它到适当的尺寸。将
autoxauto更改为64x64。

图 18.25:更改按钮大小
- Visual Elements 始终在 UI 文档根容器中初始化,位于左上角。如果您想要能够在容器内定位它们,您必须创建一个高级 Visual Element,它充当“外壳”,其中所有其他项目都可以定位在其中。因此,为了使这个
Button在窗口中居中,我们需要添加一个 Visual Element 来包含它。从库中将VisualElement拖动到层次结构中。

图 18.26:将 VisualElement 添加到层次结构中
- 在
Button上到VisualElement。这将使Button成为VisualElement的子元素。

图 18.27:将按钮设置为 VisualElement 的子元素
-
从层次结构中选择
VisualElement以打开其检查器。 -
您可能会注意到
VisualElement并没有完全填充根容器。展开1。这将使VisualElement填充整个根容器。

图 18.28:更新 Grow 属性
- 现在,让我们将猫在
VisualElement内居中。展开VisualElement并为中心的对齐项和对齐内容属性。

图 18.29:调整 VisualElement 的对齐属性
- 让我们更改
VisualElement的背景颜色。在BE8D8D中,通过选择颜色块并将BE8D8D输入到十六进制属性中。

图 18.30:调整 VisualElement 的背景颜色
- 现在,很容易看出我们的猫
Button周围有一些我们不想要的背景和边框。为了移除它,选择Button并更改其背景颜色,使 alpha 值为0。

图 18.31:移除按钮的背景颜色
- 按钮周围仍然有一个我们不想要的边框。展开
0。你现在应该有以下内容:

图 18.32:最终的 UI 外观
-
我们几乎完成了 UI 构建器。我们需要做的最后一件事是将
Button重命名,以便在 C#代码中易于访问。在CatButton中双击Button。 -
通过按 Ctrl + S 保存你的工作,我们就完成了!你应该会有以下内容:


注意
如果你在当前打开的场景中添加了一个 UIDocument 游戏对象,你可能会在 控制台 中看到一个关于缺少面板设置的警告。你可以直接删除这个游戏对象并忽略错误信息。
现在 UI 已经完全布局,我们可以编写代码来开始添加功能!
编写 C# 代码以创建我们的编辑器窗口并添加功能
为了让我们的猫在 Unity 编辑器中以窗口的形式出现并具有功能,我们需要编写一些 C# 编辑器代码。我们希望猫能做几件事情:
-
当你使用 工具 菜单或使用预定义的热键时,出现在浮动窗口中
-
当你悬停在它上面时说“喵”
-
当你点击它时在 控制台 中赞美你
-
坐着循环的空闲动画
-
当你点击它时播放站立动画(即抚摸它)
我们可以在一个 C# 脚本中实现所有这些。为了让你的虚拟宠物在 Unity 编辑器中的窗口内出现并具有功能,请完成以下步骤:
-
在你的
Editor文件夹中,创建一个名为IdleCat.cs的 C# 脚本并打开它。 -
我们需要做的第一件事是将脚本从
MonoBehaviour改为EditorWindow。将类定义改为以下内容:public class IdleCat : EditorWindow {这将自动添加
UnityEditor命名空间。 -
我们将通过选择
IdleCat.cs脚本来使窗口出现:[MenuItem("Tools/I'm Lonely _%#K")] public static void ShowIdleCat() { EditorWindow window = GetWindow<IdleCat>(); window.titleContent = new GUIContent("Kitty"); }在前面的代码中,
[MenuItem("Tools/I'm Lonely _%#K")]行在选中时创建ShowIdleCat()方法。_%#K表示这也可以通过 Ctrl + Shift + K 快捷键实现。以下行实例化了窗口并将其标题设置为 Kitty:
EditorWindow window = GetWindow<IdleCat>(); window.titleContent = new GUIContent("Kitty");保存你的代码,你应该能在编辑器中看到菜单项。点击它或按 Ctrl + Shift + K 应该会弹出一个名为 Kitty 的窗口。


-
你的窗口可能和我的大小不同。不用担心。让我们通过代码设置大小。将以下两行代码添加到你的
ShowIdleCat()方法中:window.maxSize = new Vector2(100, 100); window.minSize = window.maxSize;这不会自动改变你当前打开窗口的大小,因为这个函数只在你使用菜单项或快捷键时运行。你现在可以通过执行这些操作之一来调整窗口大小。你不需要关闭窗口。你可以在它仍然打开时这样做。
有一个需要注意的事项是,编辑器窗口可以在 Unity 编辑器中停靠。因此,你的窗口大小可以根据用户停靠的位置而改变。
-
现在,我们需要引用在这个脚本中用 UI Builder 创建的 UI 文档。由于这是一个编辑器脚本,它不能附加到 GameObject 上。这意味着我们无法通过拖放或使用
GetComponent来分配变量。相反,我们必须通过从我们的Editor/Resources文件夹中加载它。当使用编辑器 UI 时,
CreateGUI()方法是一个用于初始化 UI 的事件函数。编写以下代码,确保将
UnityEngine.UIElements命名空间添加到脚本顶部:private void CreateGUI() { var root = rootVisualElement; var quickToolVisualTree = Resources.Load<VisualTreeAsset>("IdleCat"); quickToolVisualTree.CloneTree(root); }此代码块执行了几件事。首先,它获取我们创建的编辑器窗口的
rootVisualElement。然后,它通过在Editor/Resources文件夹中搜索名为IdleCat的VisualTreeAsset来找到IdleCat.uxml文件。然后,它克隆IdleCat.uxmlUI 文档并将其放置在窗口的根目录中。如果您返回到编辑器,现在您应该能够在打开的窗口中看到您的 Kitty 在粉红色的背景上。

图 18.35:具有适当大小和 UI 文档的窗口
-
现在,我们需要获取按钮的访问权限。将以下变量声明添加到您的脚本中:
private Button catButton; -
为了分配
catButton,我们需要Query``root对象。将以下行添加到您的CreateGUI()方法中:catButton = root.Q<Button>("CatButton"); -
接下来,让我们在悬停在按钮上时显示提示。将以下行代码添加到
CreateGUI()方法中:catButton.tooltip = "meow";保存脚本后,您应该能够立即在编辑器中看到这些更改的反映。(注意,我的截图工具没有捕获鼠标光标,所以它没有显示在下面的图中。)

图 18.36:喵喵提示
-
现在,当您点击猫时,我们可以让它赞美您。首先,让我们创建一个名为
OnCatButtonClicked()的新方法,该方法在控制台中显示"You're doing great!"消息。将以下代码添加到您的脚本中:private void OnCatButtonClicked() { Debug.Log("You're doing great!"); } -
我们需要让
OnCatButtonClicked()方法订阅和取消订阅catButton上的点击事件。将以下行添加到您的CreateGUI()方法中:catButton.clicked += OnCatButtonClicked;此外,创建以下
OnDisable()方法:private void OnDisable() { catButton.clicked -= OnCatButtonClicked; }保存后,您可以在点击它时在控制台中看到猫在赞美您。

图 18.37:赞美的猫
-
我们对虚拟宠物的最后两个目标涉及动画。这需要一些努力。我们不能使用 Unity 动画来实现它,因为我们处于编辑器中。因此,我们必须编写代码,通过某种定时器在代码中用适当的图像替换
catButton的背景。做这件事的一种方法是用协程,但不幸的是,默认情况下协程在编辑器中不工作。然而,我们可以通过从 Unity 导入编辑器协程包在我们的编辑器中使用协程。
要做到这一点,选择窗口 | 包管理器以打开包管理器。
-
从下拉菜单中选择 Package: Unity Registry 以查看所有可用的 Unity 包。

图 18.38:更改 Package Manager 中显示的包
- 现在,滚动直到您看到 Editor Coroutines。它可能被锁定。如果是这样,请选择 解锁。

图 18.39:解锁 Editor Coroutines 包
-
现在您已经解锁了 Editor Coroutines 包,您可以将以下命名空间添加到您的
IdleCat.cs脚本顶部:using Unity.EditorCoroutines.Editor;现在,我们可以在我们的代码中使用
EditorCoroutines!它们的工作方式与常规协程非常相似。但在我们编写第一个用于控制动画的 Editor 协程之前,让我们获取我们将在动画中使用的图像,并编写一些辅助函数。 -
我想猫有两个动画:一个空闲动画和一个抚摸动画(将由鼠标点击触发)。我将将这些动画的图像存储在两个单独的列表中。为了将它们用作背景图像,我需要将它们存储在
StyleBackgrounds列表中。将以下内容添加到您的类中:private List<StyleBackground> idleBackgrounds = new List<StyleBackground>(); private List<StyleBackground> pettingBackgrounds = new List<StyleBackground>(); -
在我们可以将这些适当的精灵存储到这些列表中之前,我们需要获取精灵表中的所有精灵的引用。我们将这样做,类似于我们之前找到 UI 文档文件的方式。将以下代码行添加到您的
CreateGUI()方法中:Sprite[] allSprites = Resources.LoadAll<Sprite>("idleCat");这将在
Editor/Resources文件夹中查找,并将所有名为idleCat的精灵存储到allSprites数组中。 -
以下帧将代表特定的动画:
![图 18.40:哪些精灵属于哪个动画]()
图 18.40:哪些精灵属于哪个动画
现在,我们需要遍历
allSprites数组,并将这些精灵分配到正确的列表中。我们可以通过将以下代码添加到CreateGUI()方法中来实现这一点:for (int i = 0; i <= allSprites.Length - 1; i++) { StyleBackground backgroundImage = new StyleBackground(allSprites[i]); if (i < 11) { pettingBackgrounds.Add(backgroundImage); } if (i >= 10) { idleBackgrounds.Add(backgroundImage); } } -
在我们可以编写替换背景的正确图像的方法之前,我们需要添加一个变量来跟踪按钮当前显示的哪个动画的哪个帧。将以下变量初始化添加到您的类中:
private int animationIndex = 0; -
好的,现在我们需要编写方法来增加
animationIndex并将catButton的backgroundImage设置为适当的精灵。将以下方法添加到您的类中,以控制空闲动画的动画:private void IdleAnimation() { animationIndex++; if (animationIndex >= idleBackgrounds.Count) { animationIndex = 0; } catButton.style.backgroundImage = idleBackgrounds[animationIndex]; }注意,当索引超出范围时,它会循环回到
0。 -
添加一个类似的方法来控制抚摸动画:
private void PettingAnimation() { animationIndex++; if (animationIndex >= pettingBackgrounds.Count) { animationIndex = 0; } catButton.style.backgroundImage = pettingBackgrounds[animationIndex]; }
注意
IdleAnimation() 和 PettingAnimation() 方法有一些可重用的代码,可以为了简洁而重构;然而,为了清晰起见,我将保持原样。
-
现在,我们准备好使用
EditorCortoutine来调用这些方法并播放动画。让我们先创建以下 Editor 协程:IEnumerator NextAnimationFrame() { var waitForOneSecond = new EditorWaitForSeconds(1f); while (true) { yield return waitForOneSecond; IdleAnimation(); } }这将创建一个无限循环,每秒运行一次
IdleAnimation()方法。 -
我们需要启动我们在上一步中编写的编辑器协程。在你的
CreateGUI()方法中添加以下代码:EditorCoroutineUtility.StartCoroutine(NextAnimationFrame(), this); -
如果你回到你的编辑器,你应该能看到猫在循环空闲动画时摇尾巴。现在,我们需要编写一些代码,以便当你点击猫时播放抚摸动画。为此,我们需要另一个变量。在你的类中添加以下内容:
private bool idle = true;这个变量将跟踪猫是否应该处于空闲状态。
-
在
OnCatButtonClicked()方法中添加以下内容,以表示点击后猫不再空闲:idle = false; -
更新
NextAnimationFrame()编辑器协程,使其根据idle变量在空闲和抚摸动画之间切换。粗体的代码是新的:IEnumerator NextAnimationFrame() { var waitForOneSecond = new EditorWaitForSeconds(1f); while (true) { yield return waitForOneSecond; if (idle) { IdleAnimation(); } else { PettingAnimation(); } } } -
目前,如果你点击猫,它将进入抚摸动画,但会无限循环该动画。我们需要它在抚摸动画完成后回到空闲动画。更新
PettingAnimation()方法,在if语句中包含idle = true。这将使动画完成后将idle变量重置为true。 -
我们已经正式完成了我们设定的所有任务,但我们的代码中有一个问题。如果你在协程的
while循环中放置一个Debug.Log,然后关闭它,Debug.Log仍然会在控制台中打印!我们需要在窗口关闭时停止该协程的while循环。为此,在你的类中添加另一个变量:private bool windowOpen = true; -
将
NextAnimationFrame()协程中的while循环更改为以下内容:while (windowOpen) { -
现在,我们只需要在窗口关闭时将
windowOpen设置为false。所以,在OnDisable()方法中添加以下内容:windowOpen = false;
就这样!你现在应该有一个小猫咪朋友,它会陪伴你工作。当你需要一点提神的时候,随时点击她。
使用 UI 工具包制作具有样式表和动画过渡的菜单
我们将查看如何使用 UI Builder 创建一个使用样式表和过渡动画的基本菜单,然后在这个例子中我们将连接菜单内的按钮以访问网络数据和随机生成内容。
由于上一个例子是一个旨在带给你快乐的虚拟宠物,我决定在这个例子中也坚持“自我关怀”的主题。以下图是我们将使用随机生成的猫的图片和从网络上检索到的引言制作的 UI 截图。

图 18.41:带有随机生成内容的 UI 菜单
按下Charm Me按钮将随机生成一张猫的图片,按下Inspire Me按钮将随机生成一句励志的引言。此外,当鼠标悬停或点击按钮时,按钮会改变颜色,并在动画过程中略微变大。
与上一个例子一样,我们将首先使用 UI Builder 来布局 UI。
使用 UI Builder 布局菜单、制作样式表和制作动画过渡
要创建如图 图 18**.41 所示的 UI,请完成以下步骤:
-
创建一个名为
Chapter18-Examples的新场景。 -
在场景层次结构中右键单击并选择名为
UI Toolkit的Assets文件夹。它将包含一个PanelSettings资产和一个UnityThemes文件夹。 -
在
UI Toolkit文件夹中,右键单击并选择InspirationalMenu.uxml。 -
将新的 UXML 文件拖入
UIDocument游戏对象。 -
为了确保我们有相同的游戏视图分辨率,将你的游戏场景视图设置为 840x630。

图 18.42:设置你的游戏视图分辨率
-
双击
InspirationalMenu.uxml文件以打开 UI Builder。 -
从层次结构中选择
InspirationalMenu.uxml以查看其检查器。 -
将画布大小调整为匹配游戏视图。这将导致视口中的容器大小与你的游戏视图相同。

图 18.43:设置画布大小以匹配游戏视图
-
从控件窗口中将 Button 拖入 视口。它应该延伸到容器整个宽度。
-
Button应该有一个蓝色轮廓围绕它,并在其右下角有一个把手。选择该把手以调整按钮到令人愉悦的“按钮”大小。大小由你决定。

图 18.44:视口中的按钮元素调整大小
- 通过调整文本属性,将
Button上的文本更改为Charm Me。

图 18.45:如何更改按钮上显示的文本
- 展开
22和对齐以垂直和水平居中。根据需要拖动按钮的把手以使其稍微大一点或小一点以适应新的文本大小。

图 18.46:按钮的文本设置
- 展开
FF9BC8。

图 18.47:更改按钮的背景颜色
- 展开
5和10。

图 18.48:调整边框属性
-
如果你转到你的游戏视图,你会注意到颜色与 UI Builder 中的颜色不完全相同。
![图 18.49:UI Builder 视口和编辑器游戏视图之间的差异]()
图 18.49:UI Builder 视口和编辑器游戏视图之间的差异
要确保你在 UI Builder 中看到的内容与你的游戏视图相匹配,请从 UI Builder 视口右上角的下拉菜单中选择Unity 默认运行时主题。
![图 18.50:将视口设置为 Unity 默认运行时主题]()
图 18.50:将视口设置为 Unity 默认运行时主题
-
将
Button的 文本 颜色更改为白色。

图 18.51:更改文本颜色
-
现在,让我们创建一个将包含
Button的面板。我们将稍后创建第二个按钮。从 容器 库 中拖动一个 VisualElement 到 视口。 -
与你调整
Button大小的方式相同,将VisualElement调整到合理的面板大小。

图 18.52:调整 VisualElement 的大小
-
将
VisualElement的背景颜色设置为FFEFEF。 -
设置
5和6。你应该在你的视图中看到以下内容:

图 18.53:具有新设置的 Panel
- 现在,让我们将
Button设置为VisualElement面板的子元素。在Button上点击并拖动到VisualElement。

图 18.54:将 Button 添加到 VisualElement 中
- 我想在面板中有两个部分:左侧部分包含按钮,右侧部分包含猫的图片和励志引言。将一个
VisualElement添加到表示面板的VisualElement中作为子元素。你会注意到它自动堆叠在视口中的Button下方。

图 18.55:添加一个新的 VisualElement
-
将
Button设置为新VisualElement的子元素。 -
现在,调整
VisualElement的大小,使其占据面板左侧的一部分,通过点击并拖动其蓝色手柄。你应该得到以下类似的内容:

图 18.56:调整 VisualElement 的大小
- 现在,添加另一个
VisualElement作为最高级VisualElement的子元素。你将看到以下内容:

图 18.57:向层次结构中添加另一个 VisualElement
-
选择最顶层的
VisualElement并更改VisualElement或在检查器中。![图 18.58:设置 Flex 方向为行]()
图 18.58:设置 Flex 方向为行
这样做将导致其两个
VisualElement子元素从左到右排列,而不是从上到下。 -
从层次结构中选择最底层的
VisualElement。在其1中。这将导致VisualElement填充其父元素内的可用空间。 -
现在,我们希望将面板居中显示在屏幕上。为此,我们需要创建另一个
VisualElement来容纳它,这样我们就可以在其父元素内居中。向层次结构中添加另一个VisualElement。 -
将表示面板的
VisualElement在层次结构中拖动,使其成为新VisualElement的子元素。在下面的图中,层次结构中用红色矩形圈出的元素代表面板及其子元素。

图 18.59:我们当前布局的层次结构
-
选择层次结构中最顶层的
VisualElement。你会看到它没有填充整个根容器。将其设置为1以填充可用空间。 -
展开 Align 属性,并选择 Align Items 和 Justify Content 属性的中心。现在面板应该居中在容器中。
-
在仍然选择最顶层的
VisualElement的情况下,通过将pinkBackground展开到 Image 来更改背景图像。此时你应该会看到以下内容:

图 18.60:在背景上设置背景图像
- 让我们向面板添加第二个按钮。拖动一个
Button。你应该会看到以下内容:

图 18.61:添加新的按钮
- 我们不必手动设置新
Button的所有属性以匹配其他按钮,我们可以使用应用于两者的样式表。这将确保它们始终具有相同的属性。选择第一个Button(具有我们想要的属性),将其button-class展开到文本框中,然后按下 Extract Inlined Styles to New Class 按钮。

图 18.62:使用提取内联样式到新类创建样式表
-
从出现的弹出菜单中选择添加到新 USS。
-
将文件保存在
Asset/UI Toolkit中,命名为ButtonStyle.uss。 -
ButtonStyle.uss样式表现在应该出现在 Stylesheets 面板中。![图 18.63:新的样式表]()
图 18.63:新的样式表
现在,您可以通过从
Button拖动ButtonStyle.uss来将此样式应用到未设置样式的按钮上。现在,两个按钮将具有相同的属性。![图 18.64:具有相同样式表的两个按钮]()
图 18.64:具有相同样式表的两个按钮
-
将底部
Button上的文本更改为Inspire Me。 -
现在,让我们将这些按钮在其
VisualElement父元素中对齐。选择它们的VisualElement父元素,并在视图中设置其VisualElement或从检查器中的 Align 属性设置。您的 UI 构建器应该看起来像以下这样:

图 18.65:按钮在其 VisualElement 父元素中居中
-
现在,让我们添加用于存放猫的图片和从互联网生成的引语的容器。添加一个
VisualElement,使其成为最底层的VisualElement的子元素。 -
将其调整大小,使其小于其
VisualElement父元素。 -
在其检查器中展开位置属性,并将位置从相对更改为绝对。这将允许您通过输入数字或将其拖动到视口中的位置来手动定位它。定位如下:

图 18.66:手动定位VisualElement
-
将
Label添加为我们刚刚创建的VisualElement的兄弟元素。 -
将其位置属性更改为绝对,然后按以下方式缩放和定位。

图 18.67:向 UI 添加标签
- 将
FF9BC8进行更改。

图 18.68:标签的文本属性
-
删除文本,以便那里不显示任何内容。
-
目前,当您点击按钮时,反应不大。这使得用户难以判断他们是否点击了按钮。所以,让我们给按钮添加悬停和活动状态。
-
在样式表面板中选择
.button-class。右键单击它并选择复制。 -
右键单击副本并重命名为
.button-class:hover。这种命名约定表示此样式将应用于按钮的悬停状态。 -
再次复制它,并将新副本重命名为
.button-class:active。这种命名约定表示此样式将应用于按钮的点击状态。 -
选择
.button-class:hover样式。在检查器中,将E96FA6更改为200x90。 -
选择
.button-class:active样式。在检查器中,将C35D8B更改为200x90。 -
您现在可以通过播放您的游戏或使用视口****预览来预览您所做的更改。这将显示悬停和活动状态的变化。
![图 18.69:在视口中使用预览查看按钮样式更改]()
图 18.69:在视口中使用预览查看按钮样式更改
当您悬停和点击按钮时,按钮现在会变暗并变大。
-
这些按钮上的样式更改很好,但从一个标准状态到其他状态的变化有点剧烈。我们可以在
.button-class样式中添加过渡动画,这样当它过渡到其他样式时,它将更加平滑地动画化。从
0.5和EaseIn中选择.button-class样式。这表示当按钮更改宽度时,它将在 0.5 秒内完成,并且会平滑过渡。

图 18.70:宽度过渡动画
-
选择
0.5,EaseIn。您的过渡动画现在应该看起来像以下内容。这将确保当按钮的宽度和高度发生变化时,它将在 0.5 秒内平滑过渡。![图 18.71:按钮的过渡动画]()
图 18.71:按钮的过渡动画
播放预览以查看当您悬停在按钮上时按钮逐渐增长。
-
在 UI Builder 中,我们最后需要做的就是在这些视觉元素上为每个变量命名,这样我们就可以通过代码找到它们。将以下各种视觉元素的名称更改为以下内容:

图 18.72:重命名我们想要通过代码访问的视觉元素
呼吁。这一步真的很多!但我们正式完成了 UI Builder。现在我们可以开始编写我们的 C#脚本,以获得我们想要的功能。
使用 C#代码设置 VisualElement 和 Label 属性与网络数据
目前,我们的按钮除了在我们悬停或点击它们时进行动画外,没有任何功能。我们现在需要将它们连接到一些功能。当点击这两个按钮时,我们的目标如下:
-
CharmButton:这个按钮应该从互联网上获取可爱的小猫图片,并用可爱的小猫图片替换CatPic视觉元素的背景。 -
InspireButton:这个按钮应该从网络上获取励志名言,格式化文本,并将其放置在InspirationalQuote标签中。
实际上,到目前为止,为了这个例子,我所想要涵盖的内容就是我在最初开始规划时想要做的所有内容。然而,我对随机生成VisualElements属性变得有些兴奋,我可能使这个例子变得有些复杂。毕竟,我们工程师喜欢过度设计。这个例子使用了网络请求和 JSON 操作的概念。由于网络开发不是本书的重点,我不会详细解释执行这些功能的代码。我想这个例子关注的重点是 UI 特定的代码,而不是网络请求。我会解释每个代码部分的作用,但我不一定会逐行解释。
在我们进入实际代码之前,我想简要介绍一下我们将从其中获取数据的两个来源。我们将使用以下资源分别获取猫图片和励志名言:
Place Kitten 网站允许您通过简单地将尺寸添加到 URL 的末尾来获取具有特定尺寸的小猫图片。例如,placekitten.com/300/300显示了一个 300 x 300 像素的小猫图片。这个网站在您开发网站并只需要填充特定位置时非常有用。而且,它很可爱!
Zen Quotes 网站提供了一个 API,允许你获取励志名言。API 是一组函数的集合,允许你与其数据交互。API 具有哪些类型的函数将取决于 API 的使用以及工程师选择创建它的设计范式。但通常,API 将具有获取数据的能力。Zen Quotes 允许你获取励志名言。你可以获取单个名言或一组名言。访问他们的网站以查看它提供的数据检索选项。Zen Quotes 以 JSON 格式返回你请求的数据。JSON 是一种信息标准化格式。当你获取它时,它看起来如下:
{"string":"HelloWorld","boolean":false,"number":123,"array":[1,2,3],"object":{"prop1":"a","prob2":"b"}}
但它可以格式化为以下样子:
{
"string": "HelloWorld",
"boolean": false,
"number": 123,
"array": [
1,
2,
3
],
"object": {
"prop1": "a",
"prob2": "b"
}
}
我不会深入解释前面代码的具体含义,但我会指出我们覆盖的示例中的重要细节。如果你想了解更多关于 JSON 格式的信息,请参阅以下资源:www.w3schools.com/js/js_json_intro.asp
为了完成这个示例,并让按钮生成猫图片和励志名言,请完成以下步骤:
-
在你的
Scripts文件夹中创建一个新的 C# 脚本,并将其命名为InspriationalPanel.cs。 -
让我们从获取到我们在上一节中创建的
UIDocument的引用开始。将以下代码添加到你的类中,确保导入UnityEngine.UIElements命名空间:private UIDocument uiDocument; void Start() { uiDocument = GetComponent<UIDocument>(); }这段代码在我们之前的编辑器代码示例之后应该看起来很熟悉;主要区别是我们通过使用
GetComponent获取了UIDocument的引用。因此,我们需要将此脚本放在包含我们的 UI 文档组件的同一 GameObject 上。 -
将此脚本拖放到我们的
UIDocumentGameObject 上。 -
现在,在
Start()方法中使用以下代码创建 UI 文档根 Visual Element 的实例变量:var root = uiDocument.rootVisualElement; -
现在,让我们连接我们的魅力按钮。添加以下变量声明:
private Button charmButton; -
将以下代码添加到
Start()方法中以初始化它:charmButton = root.Q<Button>("CharmButton"); -
我们需要一个在点击
charmButton时运行的方法。更新你的代码如下。新的代码是粗体的。在我们之前的示例之后,这应该看起来都很熟悉:public class InspriationalPanel : MonoBehaviour { private UIDocument uiDocument; private Button charmButton; void Start() { uiDocument = GetComponent<UIDocument>(); var root = uiDocument.rootVisualElement; charmButton = root.Q<Button>("CharmButton"); charmButton.clicked += OnCharmClicked; } private void OnCharmClicked() { // Handle charm button click } private void OnDisable() { charmButton.clicked -= OnCharmClicked; } }我想添加前面的代码块,因为它基本上可以作为你使用 UI 工具包编写按钮的模板。
-
现在,我们需要一个名为
CatPic的 Visual Element 的引用,以便我们可以更改其背景。将以下代码添加到Start()方法中:VisualElement catPic = root.Q<VisualElement>("CatPic"); -
你可能会注意到我没有将
catPic设置为类变量,而是在Start()方法中将其作为实例变量。这是因为在这段代码中我们需要引用最多的不是CatPic本身,而是CatPic的样式。因此,请将以下变量添加到你的类中:private IStyle catPicStyle; -
现在,将以下内容添加到
Start()方法中:catPicStyle = catPic.style; -
为了获取
catPic背景的图像,我们将使用placekitten.com/。我们将随机选择 150 到 300 之间的宽度和高度,然后从placekitten.com获取适合这些尺寸的图片。为此,我们需要使用协程,因为网络请求需要协程。首先创建以下方法:IEnumerator GetCatPic() { // Implement coroutine logic here }
注意
此方法将在您输入网络请求之前在您的 IDE 中显示错误。
确保将以下命名空间添加到您的代码中,以便它能够识别IEnumerator:
using System.Collections;
-
现在,让我们随机生成我们将请求的图像的宽度和高度。将以下代码添加到您的
GetCatPic()协程中,以获取随机数字,然后更改catPic的尺寸以匹配它:int randomWidth = Random.Range(150, 300); int randomHeight = Random.Range(150, 300); catPicStyle.width = randomWidth; catPicStyle.height = randomHeight; -
记住,地点猫图片的 URL 格式是
https://placekitten.com/300/300。因此,我们需要创建一个字符串,将两个300值替换为randomWidth和randomHeight。将以下代码行添加到您的GetCatPic()协程中:string uri = "https://placekitten.com/" + randomWidth + "/" + randomHeight; -
现在,让我们向
uri发送网络请求。将以下代码添加到您的GetCatPic()协程中:UnityWebRequest request = UnityWebRequestTexture.GetTexture(uri); yield return request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success){ Debug.Log(request.error); } else { // Do stuff here with returned data }上述代码尝试从网站获取纹理。如果请求失败,
if将打印错误。确保导入以下命名空间,以便您的脚本理解UnityWebRequest:using UnityEngine.Networking; -
现在,让我们将
// Do stuff here with returned data部分替换为以下代码:Texture2D myTexture = ((DownloadHandlerTexture)request.downloadHandler).texture; Debug.Log("Texture Acquired"); catPicStyle.backgroundImage = new StyleBackground(myTexture);此代码将网络请求返回的图片存储为
Texture2D。然后,它将catPicStyle的背景图像设置为该Texture2D图像。 -
现在,我们需要让我们的按钮实际调用
GetCatPic()协程。更新OnCharmClicked()方法以调用GetCatPic()协程:private void OnCharmClicked() { StartCoroutine(GetCatPic()); }如果您玩游戏,现在您可以点击Charm Me按钮来获取随机的猫图片。

图 18.73:随机猫图片出现在我们的 UI 中
-
如果您第二次点击按钮,可能会注意到图像有一点奇怪。因为
GetCatPic()方法包含网络请求,它需要一段时间来获取图像并将其转换为纹理,所以当前占位图像将调整大小,变得扭曲,直到新图像加载完成。而不是让这种情况发生,让我们在加载新图像时移除原始图像。将以下代码作为
GetCatPic()协程的第一行添加:catPicStyle.backgroundImage = null; -
现在,让我们设置Inspire Me按钮的功能。将以下变量声明添加到您的类中:
private Button inspireButton; private Label inspirationalQuote; -
添加以下方法,该方法将在点击
inspireButton时被调用:private void OnInspireClicked() { // Implement functionality for when the inspireButton is clicked } -
将以下代码添加到
Start()方法中。现在,这一切都应该对您来说都很熟悉了:inspireButton = root.Q<Button>("InspireButton"); inspireButton.clicked += OnInspireClicked; inspirationalQuote = root.Q<Label>("InspirationalQuote"); -
现在,让我们继续填充励志引言的数据。我们将使用
zenquotes.io/api/random请求来检索一个随机引言。如果你在网页浏览器中导航到该 URL,你可以看到它返回的数据是如何格式化的。将以下代码添加到你的InspriationalPanel.cs脚本中:IEnumerator GetInspiringQuote() { UnityWebRequest request = UnityWebRequest.Get("https://zenquotes.io/api/random"); yield return request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success) { Debug.Log(request.error); } else { Debug.Log("Quote Acquired"); string response = request.downloadHandler.text; Debug.Log(response.ToString()); // more code will go here } }你会注意到这的结构与我们用来获取猫图片的 Web 请求类似。在我们继续之前,让我们探索一下数据是如何返回的。
-
使用以下代码更新你的
OnInspireClicked()方法:private void OnInspireClicked() { StartCoroutine(GetInspiringQuote()); } -
玩游戏并点击Inspire Me按钮。你将在控制台看到如下内容:
![图 18.74:API 请求响应]()
图 18.74:API 请求响应
如你所见,我们无法将完整的结果放入 Label 的文本字段中。我们需要将其格式化为可用的格式,并只获取我们想要的信息。为此,我们还需要另一个包。
使用窗口 | 包管理器打开包管理器。
-
选择加号,然后选择从 git URL 添加包…。
-
在文本框中输入
com.unity.nuget.newtonsoft-json并选择添加。经过一些加载后,你应该能在你的包列表中看到Newtonsoft Json。这将允许你轻松地操作 JSON 数据。 -
返回到你的脚本,并添加以下命名空间:
using Newtonsoft.Json.Linq;如果你的 IDE 不识别
Newtonsoft,请关闭 IDE 和 Unity,然后重新打开它。 -
现在,我们可以解析我们从 API 调用中接收到的数据,并获取我们想要的信息。API 调用返回的数据以
[开头。这意味着我们接收到的数据是以数组格式发送的。因此,我们需要将数据转换为数组。将// more code will go here替换为以下内容:JArray jArray = JArray.Parse(response); -
我们只想获取分配给
"q"属性和"a"属性的字符串,因为这些分别代表引言和作者。为了获取这些数据,我们需要我们的数据以对象格式存在。返回给我们的数组只有一个数组项,所以使用以下代码将数组项转换为对象:JObject jObject = JObject.Parse(jArray[0].ToString()); -
现在,我们可以获取我们想要的字符串。添加以下行以获取引言和作者:
string quote = (string)jObject["q"]; string author = (string)jObject["a"]; -
最后一件要做的事情是将标签的文本更改为我们想要的信息,并确保其格式正确。我想引言以引号显示。然后,在下一行,我想看到类似
~ 作者的名字的内容。以下代码行将做到这一点:inspirationalQuote.text = "\"" + quote + "\" \n~" + author;玩游戏,你现在应该能够获取到小猫图片和励志引言!

图 18.75:我们示例的最终版本
因为 zenquotes 是一个 API,开发者已经将每 30 秒的调用次数限制为 5 次。所以,这意味着如果你在 30 秒内尝试点击按钮超过 5 次,你会收到一个错误信息。
您可以通过在开始时获取一组数据并将其本地存储来扩展此示例。这可以减少图像的加载时间,并减少所需的 API 调用次数。
对于 UI Toolkit 的示例,我就介绍到这里。正如我之前所说的,这是一个很大的主题,也是关于开发 UI 的一种全新的思维方式。我甚至没有涵盖我想涵盖的一半内容。如果您喜欢使用 UI Toolkit,我建议查看资源部分以获取建议的进一步阅读和教程。
资源
如果您需要更多文档,我建议查看 Unity 提供的以下资源:
如果您对教程感兴趣,这里有一些很好的资源。需要注意的是,到目前为止,大多数教程都是关于使用 UI Toolkit 进行编辑器 UI 的,因为运行时支持仍然较新:
-
docs.unity3d.com/2021.2/Documentation/Manual/UIE-HowTo-CreateRuntimeUI.xhtml -
docs.unity3d.com/2023.3/Documentation/Manual/UIE-simple-ui-toolkit-workflow.xhtml -
learn.unity.com/tutorial/ui-toolkit-first-steps#61df0f23edbc2a2bf49579a2 -
docs.unity3d.com/Manual/UIE-HowTo-CreateCustomInspector.xhtml
最后,Unity 提供了一些使用 UI Toolkit 的出色预制作项目。您可以在资产商店中找到这些项目:
-
assetstore.unity.com/packages/essentials/tutorial-projects/quizu-a-ui-toolkit-sample-268492 -
assetstore.unity.com/packages/essentials/tutorial-projects/ui-toolkit-sample-dragon-crashers-231178
摘要
UI 工具包仍在开发中,但 Unity 正在积极开发它,作为替换当前 uGUI 系统(本书到目前为止一直关注)的系统。它使用 Web 开发的理念来开发 UI,可以提供比 uGUI 更干净、性能更好的 UI。然而,由于它仍在开发中,它还不能做到 uGUI 所做的一切……目前还不能。虽然您可能无法完全过渡到 UI 工具包系统来满足您的 UI 需求,但了解它是如何工作的还是有帮助的,因为总有一天,它将成为您唯一的选项。
为了帮助您了解 UI 工具包的概念,我们讨论了系统的通用概念以及如何使用它来在编辑器和运行时 UI 中创建 UI。
在下一章中,我们将讨论 Unity 中使用的另一个 UI 系统:IMGUI。
第十九章:使用 IMGUI
我将要介绍的最后一个 UI 系统是 IMGUI 或 即时模式图形用户界面。IMGUI 的主要用途是创建在开发过程中和调试期间帮助开发者的工具。虽然 IMGUI 在技术上可以创建运行时 UI,但 Unity 强烈不建议这样做。例如,您可以使用它来创建 Editor 扩展或调试菜单,这些菜单将在您的游戏视图中运行,并且可以在您在编辑器外玩游戏时访问。然而,请注意,这些游戏内的调试菜单是面向开发者的,而不是面向玩家的。IMGUI 最常用于开发 Editor UI 扩展。
由于本书的主要重点是运行时、面向玩家的 UI,因此我不会深入探讨这个系统;然而,我会介绍使用 IMGUI 制作面向开发者内容的基础知识。
在本章中,我将讨论以下内容:
-
IMGUI 的一般使用概述
-
最常用的 IMGUI 控件
-
如何使用 IMGUI 进行 Inspector UI
-
如何在您的游戏中显示调试帧率文本 UI
-
如何在 Inspector 组件上放置按钮并将数据导入到 ScriptableObject
重要的是要注意,Unity 并不推荐使用 IMGUI 系统。当涉及到运行时 UI 时,他们推荐使用 uGUI(本书的大部分内容都是关于这个的),而当涉及到编辑器 UI 时,他们推荐使用 UI Toolkit(在第十八章中介绍过。第十八章)。因此,学习 IMGUI 并非 Unity 开发者必须掌握的技能,尤其是对于非程序员来说。然而,它确实为程序员提供了一种非常快速构建 UI 的方法,以帮助他们进行开发,所以这些技能并非无用。
让我们回顾一下关于 IMGUI 的一些基本信息。
技术要求
您可以在此处找到本章相关代码和资产文件:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2019
IMGUI 概述
如我之前所述,IMGUI 为程序员提供了一种快速构建 UI 的方法,这可以帮助他们在开发过程中。这是因为 IMGUI 完全通过代码构建。它与 GameObjects 无关,所有对象都是通过调用 OnGUI() 或 OnInspectorGUI() 方法来渲染的。OnGUI() 方法每帧都会被调用,类似于 Update() 方法。
如果您想让 IMGUI 在您的场景中显示,您需要在继承自 MonoBehaviour 的脚本中的 OnGUI() 方法中编写所有 UI 构建代码。因为 IMGUI 项目是通过代码创建的,所以您在 MonoBehaviour 脚本上使用它创建的任何 UI 都不会渲染,直到游戏运行。
如果你希望你的 UI 出现在编辑器窗口中,你将在继承自EditorWindow的脚本中的OnGUI()方法中编写所有 UI 构建代码。如果你希望你的 UI 出现在检查器中,你将在继承自Editor的脚本中的OnInspectorGUI()方法中编写所有 UI 构建代码。
所有 IMGUI 项目都是通过在OnGUI()或OnInspectorGUI()方法中调用它们的独特方法来创建的。每个独特的方法都可以使用矩形位置来定位和调整大小。当使用矩形位置定位对象时,你首先使用Rect()方法创建一个新的矩形。Rect()方法接受四个参数:x位置,y位置,宽度,和高度。所以,例如,你可以使用以下代码行创建一个新的矩形:
Rect rect = new Rect(10, 10, 50, 50);
这将在屏幕坐标(10, 10)处创建一个宽度为50,高度为50的矩形。
记住,屏幕坐标将位置(0,0)放在屏幕的左上角。
你也可以使用Vector2s 来指定参数。例如,你可以这样做以实现相同的结果:
Vector2 position = new Vector2(10, 10);
Vector2 dimensions = new Vector2(50, 50);
Rect rect = new Rect(position, dimensions);
现在我们已经知道了如何创建 IMGUI 项目,让我们回顾一下可以使用OnGUI()绘制的项目类型。
IMGUI 控件
使用 IMGUI 绘制的项目被称为控件。虽然“控件”一词可能意味着项目是可交互的,但并非所有控件都是可交互的。IMGUI 系统中有多种控件可用,但你可以用以下几种控件实现大多数 IMGUI 目标:
-
标签 -
按钮 -
重复按钮 -
文本字段 -
文本区域 -
切换
注意
你可以在这里找到所有 IMGUI 控件的完整列表:docs.unity3d.com/2023.3/Documentation/Manual/gui-Controls.xhtml。
创建一个标签,你调用GUI.Label()方法。GUI.Label()方法有多个重载,但你将主要使用以下几种:
public static void Label(Rect position, string text);
public static void Label(Rect position, Texture image);
第一个用于定义文本标签,第二个用于定义图像标签。
例如,以下代码将在附加到 GameObject 并分配labelTexture变量到检查器的情况下,在场景中显示文本标签和图像标签:
public class Chapter19Labels : MonoBehaviour {
public Texture2D labelTexture;
private void OnGUI() {
GUI.Label(new Rect(10, 10, 100, 50), "Text Label");
GUI.Label(new Rect(10, 80, 50, 50), labelTexture);
}
}
标签将看起来如下:

图 19.1:使用 IMGUI 标签
创建一个按钮,你调用GUI.Button()方法。GUI.Button()方法有多个重载,但你将主要使用以下几种:
public static bool Button(Rect position, string text);
public static bool Button(Rect position, Texture image);
第一个将用于定义文本按钮,第二个将用于定义图像按钮。
当按钮被点击时执行代码,你可以在OnGUI()方法中的if语句内创建按钮。
例如,以下代码将在附加到 GameObject 并在检查器中分配的情况下,在场景中显示文本按钮和图像按钮:
public class Chapter19Buttons : MonoBehaviour {
public Texture2D buttonTexture;
private void OnGUI() {
if (GUI.Button(new Rect(10, 10, 100, 50), "Text Button")) {
Debug.Log("Text Button Clicked");
}
if (GUI.Button(new Rect(10, 80, 50, 50), buttonTexture)) {
Debug.Log("Image Button Clicked");
}
}
}
两个按钮每次点击都会在控制台写入一条消息。代码必须附加到一个 GameObject 上,并且必须在检查器中分配buttonTexture变量。
前面代码创建的按钮将看起来如下:

图 19.2:使用 IMGUI 按钮
如果你想要创建一个在点击并按住时调用方法的按钮,你可以使用Button。你可以将前面代码中的所有GUI.Button实例替换为GUI.RepeatButton,以实现类似的结果,但按钮将一直执行方法,直到按钮被按住。
当你只想有一行可编辑文本时,使用TextField,而当你想有多个行时,使用TextArea。要创建一个TextField或TextArea,你分别调用GUI.TextField()和GUI.TextArea()方法。GUI.TextField()方法有多个重载,但你将主要使用以下几种:
public static string TextField(Rect position, string text);
public static string TextArea(Rect position, string text);
第一个方法创建一个TextField,第二个创建一个TextArea。在两种情况下,string参数是在用户开始编辑文本之前将显示的文本。请注意,该方法返回一个string类型。你可以通过将构造的对象分配给一个string变量来获取用户输入的值。
例如,以下代码将在场景中附加到一个 GameObject 上时显示一个可以与之交互的TextField和TextArea:
public class Chapter10TextFieldAndArea : MonoBehaviour {
private string textFieldText = "Enter text";
private string textAreaText = "Enter text";
private void OnGUI() {
textFieldText = GUI.TextField(new Rect(10, 10, 100, 50), textFieldText);
textAreaText = GUI.TextArea(new Rect(10, 80, 100, 100), textAreaText);
}
}
在用户开始编辑文本之前,场景中渲染的项目如下所示:

图 19.3:使用 IMGUI 文本字段和文本区域控制
Toggle,你调用GUI.Toggle()方法。GUI.Toggle()方法有多个重载,但你将主要使用以下几种:
public static bool Toggle(Rect position, bool value, string text);
注意,该方法返回一个bool类型。你可以通过将创建的对象分配给一个bool变量来获取切换的值。
例如,以下代码将创建一个切换,其值在切换被点击时分配给一个布尔变量。这个例子,就像其他例子一样,需要附加到场景中的 GameObject 上以进行渲染:
public class Chapter19Toggle : MonoBehaviour {
private bool toggleBool = true;
private void OnGUI() {
toggleBool = GUI.Toggle(new Rect(10, 10, 100, 50), toggleBool,"Toggle Me");
}
}
切换最初将在场景中按如下方式渲染,直到用户与之交互,此时切换将随着每次点击而打开和关闭。

图 19.4:使用 IMGUI 切换控制
我所展示的所有示例都是在场景中。然而,如果你想在你编辑器窗口中显示你的 UI,你的代码将类似地工作。你只需将你的代码放入一个继承自EditorWindow的脚本中。(参见第十八章中的示例。)但是,如果你想在检查器中创建 IMGUI,情况就略有不同。现在让我们来看看。
检查器中的 IMGUI
使用 IMGUI 增强你的组件的工作方式与在游戏内 IMGUI 和 EditorWindow IMGUI 中非常相似。然而,有一些小的变化。首先,你编写从 Editor 继承的脚本。其次,你使用 OnInspectorGUI() 方法,而不是 OnGUI() 方法。第三,如果你想检查器也包含所有其通常的数据,你需要在你的 OnInspectorGUI() 方法中调用 DrawDefaultInspector() 方法。最后,如果你想使 IMGUI 按钮与各种默认检查器元素对齐,你使用 GUILayout 基类,而不是 GUI 基类。所以,例如,你不会用以下代码创建一个按钮:
GUI.Button(new Rect(10, 10, 100, 50), "Text Button");
相反,你会用以下方式创建它:
GUILayout.Button("Text Button");
使用 GUILayout 创建的按钮不需要矩形位置,它们将自动在 UI 中定位。
我将在 示例 部分介绍这个示例。
现在,你已经有了足够的基本背景信息,可以开始使用 IMGUI 开发面向开发者的 UI。让我们看看一些基本示例,以帮助你开始使用该系统。
示例
对于本章的示例,我将介绍两种 IMGUI 的使用类型:一种用于游戏内的调试菜单,另一种用于检查器 UI。到目前为止,所有涵盖的示例都已经在游戏内的调试菜单 UI 中。
使用 IMGUI 在游戏中显示帧率
让我们创建一个非常简单的脚本,它将在场景中显示我们游戏的帧率。如果帧率低于某个值,它将改变颜色。
要在游戏中显示帧率,请完成以下步骤:
-
创建一个名为
Chapter19-Examples.unity的新场景。 -
在新场景中,创建一个名为
DebugMenu的新 GameObject。 -
创建一个名为
DebugFrameRate.cs的新脚本。 -
将
DebugFrameRate.cs脚本附加到你的DebugMenuGameObject 作为组件。 -
打开新脚本并添加以下变量声明:
int fps; [SerializeField] int fpsThreshold;fps变量用于获取我们游戏帧率的估计值,而fpsThreshold变量在检查器中分配,并用于确定当我们的 fps 在场景中显示为红色时的阈值。 -
使用以下代码创建一个 IMGUI
Label,用于显示 fps:private void OnGUI() { GUI.Label(new Rect(10, 10, 50, 50), "fps = " + fps.ToString()); } -
现在,让我们计算 fps。我们可以使用以下方法来估计 fps:
private void Update() { fps = (int)(1f / Time.unscaledDeltaTime); } -
玩你的游戏,你应该在场景中看到 fps 的显示。如果你打开 Stats 窗口,你应该看到这些值相对接近。
![图 19.5:显示在游戏中的帧率]()
图 19.5:显示在游戏中的帧率
没有暂停游戏很难看到值。所以,让我们让它低于某个值时改变颜色。这将使得在帧率低于我们担忧的水平时更容易看到。我们将使用
fpsThreshold值来实现这一点。你的游戏在编辑器中运行的帧率可能不同于我的,所以请使用一个对你系统有意义的值,以便能够看到代码执行。我将使用1000。在检查器中输入你的fpsThreshould值。 -
现在,让我们让它低于那个阈值时改变颜色。将以下
if/else语句添加到你的OnGUI()方法顶部,在创建标签之前:if (fps < fpsThreshold) { GUI.contentColor = Color.red; } else { GUI.contentColor = Color.white; }
这样,你就在游戏中有了帧率估计显示。
在游戏中显示帧率是使用游戏内调试 UI 制作你可能想要的东西的一个很好的例子。这将让你在运行游戏时,即使是在编辑器之外,也能轻松地看到游戏的整体性能。你可以扩展它,使其仅在执行特定任务时出现,或者每秒只更新一次。可能性是无限的。
你可能有其他很多原因想要使用游戏内的调试菜单。例如,你可能想要一些按钮,它们可以调用帮助你跳转到游戏特定部分的函数。或者你可能想要一个清除你保存数据的按钮。制作使用 IMGUI 的游戏内 UI 时,重要的是记住,这应该都是为了帮助你,开发者,而不应该用来向玩家显示信息。
现在,让我们看看你可以在你的编辑器中做的一些事情来帮助你进行开发。
使用 IMGUI 制作一个用于导入数据的检查器按钮
在游戏开发中,你通常会在外部存储一些数据,并需要在游戏代码中将这些数据导入为可用的格式。例如,你可能有一个团队成员负责在 Excel 表中创建所有对话,然后你需要找出如何将其导入到你的游戏中。这个例子将展示一个使用检查器按钮从文件中读取文本并将其分配到游戏中的适当位置的基本示例,特别是 ScriptableObject。

图 19.6:带有导入按钮的自定义检查器
注意
在本文中,我们还没有真正讨论过 ScriptableObjects。ScriptableObject 在 Unity 中本质上是一个数据容器。
要创建一个将数据导入到你的 ScriptableObject 中的按钮,请完成以下步骤:
-
创建一个新的 C#脚本,名为
DialogueData.cs。 -
在你的
Assets文件夹中创建一个名为Data的新文件夹。 -
从代码文件中找到名为
SampleDialogue.txt的文本文件。将其导入到你的Data文件夹中。或者,你也可以创建一个至少有三行文本的文本文件,并将其放入这个文件夹中。 -
打开你的
DialogueData.cs脚本,将其修改为从Monobehavior继承到ScriptableObject,如下所示:public class DialogueData : ScriptableObject { -
向类中添加以下两行代码:
public string textPath; public List<string> importedDialogue;我们将使用
textPath变量来定义导入的文本文件在我们项目中的位置,并使用importedDialogue变量来保存所有导入的对话。 -
在类定义上方添加以下行,以创建一个菜单,使得
DialogueDataScriptableObjects:[CreateAssetMenu(fileName = "New SO", menuName = "DialogueData", order = 1)] -
现在,返回到你的编辑器,通过在
Data文件夹内右键点击并选择创建 | DialogueData来创建一个新的 ScriptableObject。![图 19.7:创建 DialogueData ScriptableObject]()
图 19.7:创建 DialogueData ScriptableObject
这将创建一个新的名为
New SO的 ScriptableObject,其检查器如下所示:![图 19.8:ScriptableObject 的检查器]()
图 19.8:ScriptableObject 的检查器
-
现在,让我们使用导入按钮自定义检查器。创建一个新的脚本
DialogueDataCustomEditor.cs并将其保存在你的Editor文件夹中。 -
打开脚本,将其继承自
Editor而不是Monobehavior,如下所示:public class DialogueDataCustomEditor : Editor { -
确保添加以下内容:
using UnityEditor; -
现在,为了让脚本知道它是一个
DialogueData类的自定义检查器,在类定义上方添加以下内容:[CustomEditor(typeof(DialogueData))] -
将以下代码添加到你的脚本中,以在检查器中显示按钮:
public override void OnInspectorGUI() { if (GUI.Button(new Rect(10, 10, 100, 50), "Import Button")) { // Handle button click logic here } }如果你返回到你的
New SO,你会看到检查器现在只包含一个按钮。

图 19.9:检查器中的按钮
-
它不再显示我们想在检查器中显示的所有数据。因此,在按钮代码上方添加以下内容:
DrawDefaultInspector(); -
它现在应该在默认检查器信息上方绘制按钮,这并不是我们想要的。
![图 19.10:检查器上方的按钮]()
图 19.10:检查器上方的按钮
将按钮代码编辑为以下内容:
if (GUILayout.Button("Import Dialogue")) { }这将导致按钮显示在组件的底部,利用 Unity GUI 的布局而不是显式定位。
![图 19.11:使用 GUILayout 基类的按钮]()
图 19.11:使用 GUILayout 基类的按钮
-
现在,我们只需要导入对话。将以下变量声明添加到你的代码中:
private string[] splitTags = { "\r\n", "\r", "\n"};我们将使用这个变量来正确解析文本文件中的数据,确保每一行都是一个新的对话行。
-
现在,我们需要添加代码,首先从文件获取数据,然后将其发送到 ScriptableObject:
private void ReadString() { DialogueData dialogueDataScript = (DialogueData)target; StreamReader reader = new StreamReader(dialogueDataScript.textPath); ParseFile(reader.ReadToEnd()); reader.Close(); } private void ParseFile(string theFileText) { Debug.Log(theFileText); DialogueData dialogueDataScript = (DialogueData)target; dialogueDataScript.importedDialogue.Clear(); string[] lines = theFileText.Split(splitTags, StringSplitOptions.None); foreach (var line in lines) { dialogueDataScript.importedDialogue.Add(line); EditorUtility.SetDirty(target); } } -
现在,更新你的
OnInspectorGUI()方法以调用ReadString()方法:public override void OnInspectorGUI() { DrawDefaultInspector(); if (GUILayout.Button("Import Dialogue")) { ReadString(); } } -
我们需要将
SampleDialogue.txt文件的位置添加到我们的 ScriptableObject 中。右键点击SampleDialogue.txt并选择复制路径。将其粘贴到文本 路径槽中。

图 19.12:在检查器中分配的 textPath 变量
- 我们应该可以开始了。点击导入对话框按钮,对话框现在将开始导入。你的检查器应该看起来像图 19。6*。
使用 IMGUI 在编辑器内创建按钮的操作就到这里了。
概述
在本章中,我们讨论了如何使用 IMGUI 系统为开发者构建 UI,包括游戏内的调试显示以及编辑器扩展。虽然 IMGUI 并非一个推荐的 UI 系统,但它对于在开发过程中快速创建辅助开发者的工具来说极为有用。
在下一章中,我们将探讨 Unity 提供的另一个输入系统:新输入系统。
第二十章:新输入系统
Unity 的 Update() 方法用于确定设备是否接收到了输入。这通常会在你的 Update() 方法中导致多个 if-else 分支,以控制每个接收到的输入所发生的事情。
输入系统通过使用基于事件的编程方法将处理单个输入设备的任务从代码中分离出来。你不需要在代码中为每个输入设备创建引用,而是创建响应特定操作的代码。然后,输入系统控制哪些来自哪些输入设备的交互触发这些操作。
这种将输入处理和代码分离的做法允许你创建更可定制的控制,更容易处理不同的输入设备,以及更容易处理来自具有极不同控制方案的不同控制台的控制。此外,输入系统的模块化允许你轻松地将你的控制方案复制到其他项目中,因为所有信息都存储在资产中。
本章旨在介绍输入系统,并将为你概述关键概念,以便你可以在项目中开始使用输入系统。
在本章中,我将讨论以下内容:
-
如何安装输入系统
-
轮询与订阅之间的差异
-
与输入系统相关的基本元素
-
如何编写使用输入系统控制游戏的代码
-
如何将使用旧输入系统(输入管理器)的项目转换为使用新输入系统
-
将你的输入系统连接到代码的两种不同方式
在我们开始查看如何使用输入系统之前,需要将其导入到你的项目中。让我们看看如何做到这一点。
技术要求
你可以在此处找到本章的资产文件和代码:github.com/PacktPublishing/Mastering-UI-Development-with-Unity-2nd-Edition/tree/main/Chapter%2020
安装输入系统
输入系统不再处于预览状态,并正式成为 Unity 的一部分。然而,它并没有预装在 unity 中,必须安装。你可以通过包管理器完成以下步骤,将输入系统安装到你的项目中。
- 从下拉菜单中选择 Unity 注册表。

图 20.1:在包管理器中更改包过滤器
- 在列表中搜索 Input System 并选择 安装。

图 20.2:在包管理器中找到输入系统
你将看到一个弹出窗口,你必须同意才能继续。

图 20.3:关于更改输入系统的警告
这个警告表明,你使用旧输入系统编写的任何代码将不再工作。这是一个破坏性行为,可能会破坏你的游戏。
注意
我建议你创建一个新的项目来探索输入系统,因为安装它可能会对你的项目造成破坏。它将导致使用输入管理器编写的所有代码停止工作。在你感到舒适之前,不要在现有的项目中安装它。
当你重新打开你的项目时,你可能想回到包管理器,并使用包安装 Unity 提供的各种示例。我建议你安装简单演示和 UI 与游戏输入。
由于这是一个新的输入系统,它确实使用了一种与旧输入系统不同的方法来访问输入。在我们开始查看输入系统的各个元素之前,让我们首先讨论这些方法之间的差异。
轮询与订阅
我们在第八章中讨论了输入管理器,直到最近,它一直是跟踪 Unity 构建的游戏中设备输入的唯一方式。当使用输入管理器时,你将特定的轴分配给不同的输入操作。然后,你编写一个 C#脚本,不断检查该操作是否被执行。

图 20.4:监视由输入管理器定义的特定输入的 C#脚本
为了实现这一点,你的代码可能看起来像以下伪代码:
void Update () {
if (some input happened){
do something
}
}
这种定期请求信息的技术被称为轮询。你可以使用轮询模式以类似的方式从输入系统中获取信息,就像你可以使用输入管理器一样。然而,为了使你的代码更加模块化,当你使用输入系统时,大部分代码将使用发布者-订阅者(pub-sub)模式。
考虑以下类比:你非常喜欢某个 YouTube 内容创作者的内容。你每天都会查看他们的频道,看看是否有新内容发布。然而,你发现这样做很麻烦,于是决定订阅他们的频道。现在,YouTube 频道会在有新内容发布时提醒你,这样就节省了你的工作。想象一下,如果你有数百个 YouTube 频道想要通过订阅来获取更新,而不是不断检查它们,你会节省多少工作量。
让我们将这个类比与编码联系起来。而不是你的 C#脚本需要在Update()方法中不断检查各种输入,你还可以订阅由输入系统定义的特定事件。用更技术性的语言来说,你将编写事件订阅方法,以监听由输入系统引发的特定事件。

图 20.5:监听由新输入系统定义的特定事件的 C#脚本
因此,当使用新的输入系统时,您的代码可能看起来像以下伪代码:
void OnEnable() {
subscribe DoSomething() to the event
}
private DoSomething() {
do something
}
void OnDisable() {
unsubscribe DoSomething() from the event
}
输入系统将根据您指定的输入和您的代码订阅的事件来确定调用哪些事件,当这些事件发生时,您的代码将执行适当的函数。
那么,您如何根据哪种输入来告诉输入系统调用哪些事件?以及它可以从这些输入调用哪些类型的事件?您可以通过使用动作、交互和输入绑定来完成此操作。现在让我们探讨这些概念。
输入系统元素
在输入管理器中,您列出各种轴并定义哪些按钮可以触发这些轴。在输入系统中,您定义一组动作并描述输入设备的各种控制如何通过输入绑定触发这些动作。
例如,您可以通过一个按下的交互将键盘上的空格键绑定到一个跳跃动作的输入绑定来创建输入绑定,其中输入设备是键盘,控制是键盘上的所有键。

图 20.6:输入绑定的示例
您可以在所谓的动作图中创建这些动作集合。动作图包含动作列表。动作包含有关输入绑定的信息。一般想法是创建包含基于其目的的动作组的动作图,例如所有角色控制动作可以放在一个动作图中,所有用户界面动作可以放在另一个动作图中。
您可以在动作编辑器中查看您的动作图、动作、绑定和交互。例如,这里显示了“玩家”和“用户界面”。

图 20.7:动作编辑器
在“玩家”动作图中,您可以看到带有“空格[键盘]”绑定和“按下”交互的“跳跃”动作。
要开始创建您的动作和动作图,在“资产”文件夹内右键单击,并在您的“资产”文件夹中选择“输入”。这样做时,您应该得到以下类似的内容:

图 20.8:动作资产的检查器和图标表示
您可以双击您刚刚创建的资产,或从其检查器中选择编辑资产来查看动作编辑器。动作编辑器将包含您的动作图和动作列表,同时显示每个动作的属性。

图 20.9:新创建的动作资产的动作编辑器
在动作图部分选择+号将创建一个新的动作图。
新的动作图将自动包含一个新动作,您可以重命名它。

图 20.10:带有新动作的 Action Map
选择 <无绑定> 将允许您创建输入绑定。
我将在本章末尾的示例部分中介绍如何选择绑定和交互的示例,但就目前而言,让我们看看您如何将这些 Actions 连接到您的代码。
将 Actions 连接到代码
在您的代码中处理 Input System 的 Actions 有多种方法。在本节中,我将概述最重要的主题,这将使您能够开始使用 Input System。
要将您的 Actions 连接到您的代码,您需要使用以下语句导入 InputSystem:
using UnityEngine.InputSystem;
我将讨论两种将 Actions 连接到您代码的方法:
-
引用 Action Asset
-
使用
PlayerInput组件
注意
有关将 Actions 连接到您代码的替代方法的更多信息,请参阅以下 Unity 文档:
docs.unity3d.com/Packages/com.unity.inputsystem@1.7/manual/Workflows.xhtml
您可以参考创建类型为 InputActionsAsset 的变量 Action Asset 的方法:
[SerializeField] private InputActionAsset actions;
然后,您可以在检查器中分配 actions 的值。
要在 InputActionAsset 中引用特定的 Action,您可以创建一个类型为 InputAction 的变量,如下所示:
private InputAction playerAction;
您可以通过找到 Action Asset 中的特定 Action Map 和 Action 来分配它,如下所示:
playerMoveAction = actions.FindActionMap("Player").FindAction("Move");
一旦找到 Action Asset 的引用,您需要启用和禁用适当的 Action Maps。例如,如果您有一个名为 Player 的 Action Map,您可以执行以下操作:
private void OnEnable()
{
actions.FindActionMap("Player").Enable();
}
private void OnDisable()
{
actions.FindActionMap("Player").Disable();
}
一旦您有了 InputActionAsset 的引用,您可以让您的方程序订阅 Action 的各种回调。每个 Action 都有以下回调,您可以订阅:
-
Performed:动作的交互已完成 -
Started:动作的交互已经开始 -
Waiting:动作已启用并等待输入以触发交互 -
Canceled:动作的交互已被取消 -
Disabled:由于禁用,动作无法接收任何输入
例如,您可以在名为 Player 的 Action Map 上订阅名为 Jump 的 Action,该 Action 由按钮按下触发,如下所示:
actions.FindActionMap("Player").FindAction("Jump").performed += OnJump;
如果您希望轮询 Actions 而不是订阅回调事件,您可以使用 ReadValue<TValue>() 方法,如下所示:
Vector2 moveVector = playerMoveAction.ReadValue<Vector2>();
如果您不是编写代码的大粉丝,您可以使用 PlayerInput 组件。PlayerInput 组件将允许您指定您的 C# 脚本中哪些方法被各种 Actions 调用。

图 20.11:默认的 Player 输入组件
使用这种方法与直接在代码中引用动作主要取决于个人喜好。它需要的代码更少,但需要更多的检查器工作。作为一名程序员,我个人更喜欢在代码中引用动作的方法。我发现它更容易调试,更可定制,当有多个对象使用动作时,编辑起来也更快。然而,当我在一个项目中工作时,设计师在检查器中做出更改,他们不想在代码中工作,使用PlayerInput组件是一个好的解决方案,因为它允许他们在我不知情的情况下完成任务。您也可以结合使用,一切基于您的需求和偏好。
注意
要了解您可以使用代码访问动作的各种方法,请参阅以下文档:
docs.unity3d.com/Packages/com.unity.inputsystem@1.7/manual/Workflow-ActionsAsset.xhtml
现在我们对 Unity 输入系统的工作原理有了大致的了解,让我们看看一些如何与之工作的示例。
示例
现在我们已经回顾了开始使用输入系统的基本知识,让我们看看一些如何实现它的示例。我们将查看一个非常基础的字符控制器示例。我们将从一个使用旧输入管理器的示例开始,然后调整它以使用输入系统。
注意
这是一个非常基础的字符控制器。它被简化了,以便更容易理解将其转换为输入系统的过程。
我们将使用的示例只是一个会跳跃和四处移动的猫。

图 20.12:使用旧输入系统的字符控制器示例
在开始这些示例之前,请完成以下步骤:
-
创建一个新的 2D Unity 项目。
-
使用
Chapter 20– Example 1 - Start包,该包由本书的源代码提供,通过Chapter 20– Example 1来感受猫的移动方式。这没有什么花哨的或特别令人印象深刻的地方,但猫可以用空格键跳跃,用箭头键和A- D键前后移动。 -
打开
InputManagerBasicCharacterController.cs类并查看代码。关键代码部分是Update()方法:void Update() { movement = Input.GetAxis("Horizontal"); catRigidbody.velocity = new Vector2(speed * movement, catRigidbody.velocity.y); if (grounded && Input.GetButtonDown("Jump")) { catRigidbody.AddForce(new Vector2(catRigidbody.velocity.x, jumpHeight)); } }注意到使用了
Input.GetAxis("Horizontal")和Input.GetButtonDown("Jump")。这些函数定义在输入管理器中,您可以在编辑 | 项目设置 | 输入管理器中找到。 -
现在您已经玩过游戏后,安装输入系统包,并在请求时重新启动。有关步骤,请参阅安装输入系统部分。
如果您现在尝试玩游戏,您将在控制台看到以下错误。
InvalidOperationException: You are trying to read Input using the UnityEngine.Input class, but you have switched active Input handling to Input System package in Player Settings. InputManagerBasicCharacterController.Update () (at Assets/Scripts/InputManagerBasicCharacterController.cs:20)这是因为当我们安装输入系统时,项目停止接受使用
UnityEngine.Input的输入。猫将不再对按键做出响应。
好的,现在我们的项目已经开始并设置好了,我们可以将代码转换为可以被输入系统使用的代码!让我们先设置我们的动作。
创建基本角色控制器动作
在我们开始调整代码之前,我们首先必须设置我们的动作映射和动作。
要设置你的基本角色控制器动作,请完成以下步骤:
-
在你的
Assets文件夹中创建一个名为Inputs的新文件夹。 -
在文件夹内右键单击并选择创建 | 输入动作。
-
将新的动作资产重命名为
CatActions。 -
双击
CatActions.inputActions以打开动作编辑器。 -
打开自动保存复选框,以便您所做的任何更改都将自动保存。

图 20.13:选择自动保存
- 现在,我们需要添加一个动作映射,它将包含我们角色的所有动作。通过选择加号并命名动作映射为
Player来创建一个新的动作映射。你现在应该看到以下内容:

图 20.14:玩家动作映射
- 让我们通过将“新建动作”重命名为“跳跃”来创建“跳跃”动作。你可以通过双击“新建动作”这个词来实现。注意动作类型被设置为按钮。这意味着跳跃动作将由类似按钮的输入触发。

图 20.15:跳跃动作的属性
- 点击
跳跃动作旁边的箭头以查看所有绑定。你应该看到无绑定。

图 20.16:跳跃动作的绑定
-
点击
空格键,然后选择空格 [键盘]。这将把键盘上的空格键绑定到这个动作。注意它会自动为你命名绑定。 -
现在我们来告诉这个绑定它可以接受哪种交互。在交互旁边选择加号,并从下拉菜单中选择按下。你的玩家动作映射现在应该如下所示:
![图 20.17:空格 [键盘] 绑定上的按下交互](https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/ms-uidev-unt-2e/img/Figure_20.17_B18327.jpg)
图 20.17:空格 [键盘] 绑定上的按下交互
-
现在我们想要添加一个绑定到之前在输入管理器中由“水平”引用的键的动作。在“移动”旁边选择加号。
-
将其动作类型设置为值,其控制类型设置为Vector2。
-
现在我们需要添加键绑定。在“移动”动作旁边选择加号,你会注意到,因为我们把“移动”设置为与“跳跃”不同的动作类型,它有不同的绑定选项。选择添加上下左右复合。你现在应该看到以下内容:

图 20.18:2D 向量绑定
-
删除 Up 和 Down 绑定以及标记为
的绑定,因为我们不需要它们。你可以通过右键点击并选择 Delete 来完成此操作。 -
在 路径 中选择
Left Arrow以找到 Left Arrow [Keyboard]。 -
在 路径 中选择
Right Arrow以找到 Right Arrow [Keyboard]。你现在应该有以下动作和绑定。![图 20.19:左右绑定]()
图 20.19:左右绑定
我们现在已经将箭头键绑定到了移动动作。
-
现在我们需要绑定 A 和 D 键。通过右键点击并选择 Duplicate 来复制 Left: Left Arrow [Keyboard] 和 Right: Right Arrow [Keyboard] 绑定。
-
在 路径 中选择复制的
a keyboard以找到 A [Keyboard]。 -
在 路径 中选择复制的
d keyboard以找到 D [Keyboard]。你现在应该看到以下动作和绑定:

图 20.20:所有必要的动作和绑定
现在我们已经完成了动作的连接,我们可以开始使用它们与我们的代码!我会展示两种方法:使用 PlayerInput 组件和通过在脚本中引用动作。
使用 PlayerInput 组件创建一个基本的角色控制器
将我们的代码切换到使用 Actions 和 PlayerInput 组件,需要一些代码调整和一些检查器工作。
要使用 PlayerInput 组件与动作,请完成以下步骤:
-
为了保留前面的例子,我将复制场景并命名为
Chapter 20– Example 2;复制InputManagerBasicCharacterController.cs脚本;并将副本重命名为PlayerInputBasicCharacterController.cs。如果你只想在同一个场景中使用相同的脚本,可以跳过这一步。然而,如果你确实想这么做,确保也要更改类定义中的脚本名称,并将脚本作为组件添加到你的新场景中的Cat对象上。 -
让我们先调整脚本。我们将移除
Update()方法中检查输入轴的代码,并改用可以在检查器中连接的公共方法。注释掉Update()方法中的所有代码。不要删除它,因为我们稍后会将其中一些代码剪切粘贴到其他方法中。 -
将以下语句添加到你的脚本顶部:
using UnityEngine.InputSystem; -
创建一个新的方法
OnJump()。这将是在Jump动作被触发时调用的方法。它应该如下所示:public void OnJump(InputAction.CallbackContext context) { } -
我们希望我们的
OnJump()方法的表现与我们在Update()方法中注释掉的以下语句相似。if (grounded && Input.GetButtonDown("Jump")) { catRigidbody.AddForce(new Vector2(catRigidbody.velocity.x, jumpHeight)); }将其复制粘贴到
OnJump()方法中。 -
从
if语句中移除&& Input.GetButtonDown("Jump")。现在你的OnJump()方法应该如下所示:public void OnJump(InputAction.CallbackContext context) { if (grounded) { catRigidbody.AddForce(new Vector2(catRigidbody.velocity.x, jumpHeight)); } } -
现在让我们在检查器中连接这个方法。从层次结构中选择您的
Cat,并添加PlayerInput组件。您的 Cat 检查器应具有以下组件:

图 20.21:Cat 上的一些组件
-
现在让我们将我们的动作添加到项目文件夹中的
CatActions到Actions槽中。您的组件现在应如下所示:![图 20.22:将 CatActions 分配给 PlayerInput 组件]()
图 20.22:将 CatActions 分配给 PlayerInput 组件
注意它已经找到了我们的Player动作映射并将其添加为默认映射。如果我们有更多的动作映射,还有其他选项可供选择。
-
从Behavior下拉菜单中选择Invoke Unity Events。这将添加一个Events设置,可以展开以显示Player事件的列表。展开Player将显示我们在玩家映射中定义的两个动作以及其他一些有用的动作。

图 20.23:各种玩家动作事件
-
我们可以将我们的
OnJump()方法连接到Cat对象的Object槽中,然后选择PlayerInputBasicCharacterController | OnJump。您的Jump事件现在应如下所示:![图 20.24:玩家跳跃事件调用我们的 OnJump 方法]()
图 20.24:玩家跳跃事件调用我们的 OnJump 方法
玩游戏,当您按下空格键时,您应该看到猫跳跃。
-
现在让我们连接移动动作。返回您的代码并创建以下方法:
public void OnMove(InputAction.CallbackContext context) {¶} -
在您的代码中添加以下变量声明:
private Vector2 moveVector = new Vector2(); -
当我们从输入轴获取
movement变量时,我们使用了以下行:movement = Input.GetAxis("Horizontal"); moveVector = context.ReadValue<Vector2>();这将在调用
OnMove()方法时获取移动动作的值。 -
返回您的
Update()方法并取消注释剩余的代码。您的Update()方法现在应如下所示:void Update() { movement = Input.GetAxis("Horizontal"); catRigidbody.velocity = new Vector2(speed * movement, catRigidbody.velocity.y); } -
删除以下使用旧输入系统的行。
Movement = Input.GetAxis("Horizontal"); -
将剩余的行编辑为使用
moveVector而不是movement浮点数。catRigidbody.velocity = new Vector2(speed * moveVector.x, catRigidbody.velocity.y); -
删除不再需要的
movement浮点变量声明。 -
返回您的检查器,并将
OnMove()方法添加到Cat的Object槽中,然后选择PlayerInputBasicCharacterController | OnMove。您的移动事件现在应如下所示:

图 20.25:玩家移动事件调用我们的 OnMove 方法
玩游戏,现在您的猫应该能够移动和跳跃了!
个人来说,我不太喜欢使用 PlayerInput 组件。由于我是一名程序员,我更喜欢把大部分时间花在代码编辑器上,而不是 Unity 编辑器上。我发现调试在检查器中连接的代码比较困难。当我使用此功能时,我会尝试通过在代码中添加注释来使未来的自己(或我的其他编程同事)的生活更轻松,注释中说明了哪些组件调用哪些方法。这可以使调试和理解代码的工作方式变得更容易。使用像 Jetbrains Rider 这样的代码编辑器可以减轻一些压力,因为它会指示代码在检查器中的连接位置,但它并不总是显示所有必要的信息,而且你不能指望你的同事也使用 Rider IDE。如果你有类似的偏好,我将在下一个示例中向你展示如何以代码为中心的方式完成此操作。
通过在代码中引用动作创建基本角色控制器
为了重新执行我们之前的示例,以便在代码中引用动作,请完成以下步骤:
-
为了保留之前的示例,我将复制场景并命名为
第二十章– 示例 3;复制PlayerInputBasicCharacterController.cs脚本;并将副本重命名为ActionReferenceBasicCharacterController.cs。如果你更愿意在同一个场景中使用相同的脚本,你可以跳过此步骤。但是,如果你确实想这样做,请确保也更改类定义中的脚本名称,并将脚本作为组件添加到你的新场景中的Cat对象上。 -
删除
Cat的检查器。注意,如果你现在尝试玩游戏,猫将不会移动或跳跃,因为我们移除了将输入与我们的代码连接的组件。 -
打开
ActionReferenceBasicCharacterController.cs脚本。 -
注释掉
OnMove()方法。我们不会在检查器中通过事件调用此方法,因此不需要该方法。然而,我们将编写类似于此方法的代码,所以我现在先注释掉它,以便稍后可以复制并粘贴到正确的位置。 -
我们需要做的第一件事是获取包含所有动作的动作资产的引用。创建以下变量声明:
[SerializeField] private InputActionAsset actions; -
我们可以通过将
CatActions资产拖动到检查器的 动作 槽中,在检查器中完成此操作。 -
返回到脚本。为你的脚本添加一个空的
OnEnable()方法和OnDisable()方法。我们稍后会向它们添加一些代码。我喜欢将
OnEnable()方法放在Awake()方法下面,因为它在它之后执行,而将OnDisable()方法放在脚本底部,因为它将在我们的其他所有方法之后执行。 -
由于我们通过代码引用我们的动作,我们需要启用和禁用我们的动作映射。将以下加粗的代码行添加到你的
OnEnable()和OnDisable()方法中。private void OnEnable() { actions.FindActionMap("Player").Enable(); } private void OnDisable() { actions.FindActionMap("Player").Disable(); } -
现在,让我们连接我们的跳跃动作。我们将通过订阅跳跃动作的
performed事件来完成此操作。将以下代码添加到你的OnEnable()方法中。actions.FindActionMap("Player").FindAction("Jump").performed += OnJump;这应该足以让之前编写的
OnJump()方法与你的动作映射一起工作。玩游戏,你现在可以在按下空格键时看到猫跳跃。 -
虽然在这个例子中并非完全必要,但我更喜欢养成在
OnDisable()中总是取消订阅任何已订阅事件的习惯。养成这个习惯将使我未来在需要时避免问题。因此,将以下代码行添加到你的OneDisable()方法顶部:actions.FindActionMap("Player").FindAction("Jump").performed -= OnJump; -
现在,让我们连接我们的移动动作。为此,我们可以创建一个表示移动动作的
InputAction的引用,以便通过代码轻松访问。将以下变量声明添加到你的代码顶部:private InputAction playerMoveAction; -
现在,让我们初始化它。将以下代码行添加到你的
OnEnable()方法中。playerMoveAction = actions.FindActionMap("Player").FindAction("Move"); -
我们不是订阅移动动作,而是在
Update()方法中轮询它。将以下代码从你的注释掉的OnMove()方法中剪切并粘贴到Update()方法顶部。你现在可以删除OnMove()方法了。moveVector = context.ReadValue<Vector2>(); -
你会在
context变量上遇到错误。它是OnMove()方法的参数,在Update()方法中不存在。我们不再读取context的值,而是读取playerMoveAction的值。将context替换为playerMoveAction,以便你的Update()方法如下所示:void Update() { moveVector = playerMoveAction.ReadValue<Vector2>(); catRigidbody.velocity = new Vector2(speed * moveVector.x, catRigidbody.velocity.y); }这样应该足以让我们的猫通过动作移动,而不需要
PlayerInput组件。玩游戏,观察猫在按下适当的键时移动和跳跃。
我知道将我们的特定项目设置为使用输入系统而不是使用输入管理器要花费更多的工作,可能看起来不值得。对于这个微小的例子,这可能确实如此。然而,如果我们想要创建一个跨平台版本的项目,该版本可以接受来自多种类型设备的输入,那么在动作映射中为动作添加新的绑定比为每个我们想要处理的可能输入配置添加新的一行代码要简单得多。
摘要
在本章中,我们介绍了如何使用新的输入系统来收集游戏输入。这使我们能够创建一个易于定制的输入系统,可以从长远来看显著简化通过各种输入设备控制游戏。虽然设置起来可能需要一些工作,但它可以节省你大量的精力。
因此,我们来到了本书的结尾!我再也没有更多用户界面知识可以传授给你们了。
第二十一章:索引
由于此电子书版本没有固定的页码,以下页码仅为参考,基于本书的印刷版。
符号
2D 游戏背景图像
放置 99-103
2D 世界空间状态指示器 478-481
3D 悬停生命值条 481-487
A
可访问性设计 40, 45
认知和情感 48
听力和言语 47
移动性 47
参考链接 48
视觉 46, 47
动作编辑器 584
动作映射 584
操作
连接,到代码 586-589
美国信息交换标准代码 (ASCII) 247
锚点处理 80-83
安卓屏幕分辨率
参考链接 19
动画文本
创建 259
动画剪辑 394-397
动画事件 398, 399
动画事件 398, 399
动画控制器 399-405
动画参数。在脚本中设置 407, 408
动画师层 406, 407
过渡动画的动画师 405, 406
行为 409, 411
动画师参数
使用代码设置 424-427
ASCII 码表
参考链接 247
增强现实 (AR) 31-33
用于设计用户界面 (UI) 37
自动布局组
网格布局组 116
水平布局组 110
类型 108-110
垂直布局组 115
B
背景画布预制体
创建 259,260
基础输入模块 162
基准测试 491
广告牌效果 477
按钮动画过渡
添加 224-229
按钮控制 568
按钮按下
使用,用于加载场景 222-224
按钮
明确导航,选择 217-221
首次选择,使用 210
布局 211-217
在 209 和 210 之间导航
尺寸 25-27
C
C#
UI,使 513 可交互
画布组件 64
屏幕空间-相机 65,66
屏幕空间-叠加 64,65
文本,缩放 474-477
世界空间 66,67
画布组组件 83,84
画布渲染器组件 74
画布标量组件 68
常量物理尺寸 71
常量像素尺寸 68,69
与屏幕尺寸缩放 69,70
世界 72
光标 375
C#代码
使用,以使用网络数据创建 VisualElement 和 Label 属性 554-563
中央处理单元(CPU)490
字符间距
调整 279-281
循环进度条
创建 299-302
复杂的宝箱
动画 428,429
动画,设置 429-436
动画,计时 436-455
状态机,构建 436, 437
控制器 567
协程 532
自定义字体 250-252, 273-279
字符间距,调整 279-281
字体大小,修改 279-281
D
DaFont
URL 245
为不便概念设计 44
设备模拟器 19-21
设备特定资源 29
对话框
翻译 267-273
逼真界面 5
绘制调用 491
下拉组件 362, 363
标题属性 364
值更改(Int32)时 365
选项属性 364
模板属性 364
下拉菜单
创建,带图像 380
信息,从下拉选择中使用 388-390
布局,带标题和项目图像 380-383
下拉模板 360-362
E
八方向虚拟模拟摇杆
创建 316-321
使其浮动 321-325
事件输入 168-170
事件系统 149-153
事件系统管理器 153
拖动阈值属性 154
首选属性 153
发送导航事件属性 154
事件触发组件 163
动作,添加到事件 166, 167
事件类型 164
事件类型 164
拖放事件 165
其他事件 165
指针事件 164
选择事件 165
扩展现实(XR)31
F
视野(FOV)34
适配器 126
宽高比适配组件 127, 128
内容大小适配组件 126, 127
Fitts’ 定律 7
字体资源创建器
参考链接 253
字体资源
参考链接 252
字体颜色
修改,带有标记 255
字体
上升计算模式 248
角色属性 247
自定义字体 250-252
动态字体设置 248, 249
字体资源 253
字体大小设置 246
字体样式,导入 250
导入 245, 246
渲染模式设置 247
与 245 合作
字体大小
修改 279-281
修改,带有标记 255
Font Squirrel
URL 245
字体样式
修改,带有标记 254
帧率 490
每秒帧数(fps) 490
全屏点击 27, 28
G
游戏
暂停 176, 177
游戏宽高比 7, 8
更改 8-13
游戏分辨率 7, 8
更改 8-13
单一分辨率,构建 14, 15
游戏视图
移动分辨率,设置 18, 19
GetAxis() 函数 159
GetButton() 函数 158
GetKey() 函数 159
GetMouseButton() 函数 160
字符度量
参考链接 248
Google 字体
URL 245
图形用户界面 (GUI)
定义 3, 4
图形射线投射器组件 72, 73, 170
图形处理单元 (GPU) 490
网格库存
布局 138-148
网格布局组 116, 117
单元大小 117
约束属性 120, 121
起始轴属性 119, 120
起始角属性 118, 120
H
头戴式显示器 (HUD) 4, 33, 64
布局,创建 85-130
水平生命条
创建 296-299
水平布局组 110, 111
子对齐属性 112, 113
儿童力量扩展属性 114, 115
控制子大小属性 113
填充属性 111, 112
反向排列属性 113
间距属性 112
使用子缩放属性 115
HUD 选择菜单
布局 130-138
I
图像类型属性,UI 图像
填充 291, 292
简单 289
切片 289, 290
平铺 291
立即模式图形用户界面 (IMGUI) 502
控件 567-570
示例 571
在检查器中 571
概览 566
使用,以创建检查器按钮 573-578
使用,以在游戏中显示帧率 572,573
输入
用于加速度计和陀螺仪 162,163
用于多点触控 162
输入字段组件 366
字符限制属性 367
字符验证选项 373-375
内容类型属性 367,368
隐藏移动输入属性 367
输入类型 369
键盘类型 369-372
行类型选项 369
值更改(字符串)和结束编辑(字符串)375,376
占位符属性 367
光标和选择属性 375
输入文本和屏幕键盘属性 367
只读属性 367
TextMeshPro 377,378
文本属性 367
输入函数
用于按钮和按键 158
获取轴 159
获取按钮 158
获取键 159
获取鼠标按钮 160
输入管理器 54,154-157
参考链接 157
使用,与暂停面板 174,176
输入模块 160
基础输入模块 162
指针输入模块 162
独立输入模块 161
输入系统 54,579
编码操作,连接 586-589
元素 583-586
示例 589,590
安装 580,581
轮询,与订阅对比 581-583
输入系统,示例
基本角色控制器动作,创建 591-594
基本角色控制器,通过在代码中引用动作创建 600-602
基本角色控制器,使用玩家输入组件创建 595-599
输入系统 53
和新的输入系统,在 54 和 55 之间选择
输入管理器 54
新的输入系统 54
可交互的用户界面
考虑因素 35, 36
位置 35, 36
接口 168
类型 4, 5
库存物品
拖放 178-188
库存面板
KeyCode,与 172-174 一起使用
不可见按钮区域 207, 208
K
KeyCode
使用,与库存面板 172-174 一起
关键帧 226
L
布局元素 121, 122
可变宽度和可变高度属性 125
忽略布局属性 122, 123
最小宽度和最小高度属性 123
推荐宽度和推荐高度属性 124
宽度和高度属性 123
M
MagicLeap
参考链接 37
标记格式
探索 254
用于修改字体颜色和大小 255
用于修改字体样式 254
遮罩
组件 328, 329
使用 328
元接口 5
银河咖啡字体
参考链接 261
混合现实 (MR) 31, 32
参考链接 37
用于设计用户界面 (UI) 37
移动设备宽高比
设备模拟器 19-21
设置 18
移动输入 29
移动方向
构建 22-25
设置 18
移动分辨率
设置 18
设置,在游戏视图中 18, 19
多点触控输入 162
静音按钮
使用精灵交换 302-308
N
命名空间 150
新输入系统 54
节点 400
非叙事类别 5
Noto 字体
参考链接 247
O
对象池 496
优化基础 490
CPU 491
帧率 490
GPU 490
P
拖动和缩放
实现,使用鼠标和多点触控输入 188-194
面板设置 507
粒子系统
定时,用于在宝箱动画中播放 467
粒子系统,显示在用户界面中
创建 459-466
暂停面板
输入管理器,使用 174-176
2D 物理射线发射器 170
轴点 80-83
指针输入模块 162
轮询 582
相对于订阅 581-583
弹入弹出动画
设置 412- 424
弹出菜单
隐藏,使用按键 171
设置 103-106
显示,使用按键 171
弹出窗口
动画,用于淡入淡出 411
预制件 259
按住/长按功能
添加 308-312
发布者-订阅者(pub-sub)模式 582
R
射线发射器 170
图形射线发射器 170
2D 物理射线投射器 170
射线投射 170
矩形遮罩 2D
组件 329, 330
矩形工具 76
定位模式 76, 77
矩形变换组件 63, 64, 76-78
编辑模式 78, 79
矩形工具 76
重复按钮 569
S
场景
创建 259, 260
屏幕部分点击 27, 28
滚动矩形组件 337, 338
移动属性 338, 339
值变更事件 341, 342
属性 339-341
序列查找器 27
射击场风格游戏
参考链接 273
滑块组件 355-357
值变更(单次)357, 358
空间界面 5
独立输入模块 161
状态机 400
静态四方向虚拟 D-Pad
创建 312-316
统计窗口 491, 492
样式表 507
使用 256, 257
订阅
与轮询 581-583
T
文本
平移 258, 259
文本区域控件 569
文本框文本
动画 262-267
文本框窗口
创建 260, 261, 262
文本字段控件 569
TextMeshPro 281, 358, 359, 377, 378
控制设置 379
输入字段设置 379
选择(字符串)和取消选择(字符串)事件 379
参考链接 237
使用,用于创建包裹文本 282-286
文本-TextMeshPro 236, 238
额外设置 242, 243
主要设置 239-242
文本输入属性 238, 239
TextMeshPro 项目设置 243, 245
TexturePacker
参考链接 273
主题样式表 508
滑块区域 28, 29
切换组件 350, 351
值变更(布尔值)事件 352, 353
切换控制 570
切换组组件 354
工具,用于确定性能 491
统计窗口 491, 492
Unity 帧调试器 494
Unity 性能分析器 492- 494
过渡属性,UI 按钮 200
动画过渡 204
颜色着色过渡 200, 202
无 200
Sprite Swap 过渡类型 202-204
U
UI 构建器
UI,创建于 511, 512
使用,以布局菜单 538-554
使用,以制作动画过渡 538-554
使用,以制作编辑器窗口的 UI 518-527
使用,以制作样式表 538-554
UI 按钮 198, 199
按钮组件 200
导航属性 205-207
过渡属性 200
UI 画布 60-63
画布组件 64
画布渲染器组件 74
画布标量组件 68
图形射线投射器组件 72, 73
矩形变换组件 63, 64
UI 文档 507
使用 513
UI 下拉菜单 358
创建 358, 359
下拉菜单组件 362, 363
下拉模板 359-362
UI 效果组件 292
轮廓组件 293, 294
位置为 UV1 组件 294
阴影组件 292, 293
UI 元素
在代码中访问 150
布局 6
UI 变量类型 150, 151
UnityEngine.UI 命名空间 150
UIElements 命名空间 514, 515
UI 层次 509-511
UI 图片组件属性 288, 289
图像类型属性 289
UI 图片 84, 85
UI 输入字段 365
创建 365
输入字段组件 366
UI 面板 75, 76
UI 滚动条
组件 331, 332
实现 330
UI 滚动条,组件
值更改事件 332-334
UI 滚动视图
示例 342
实现 334-337
从现有菜单制作 342-348
滚动矩形组件 337, 338
UI 滑块 355
创建 355
滑块组件 355, 356
UI 系统 51, 52
IMGUI 52
在 53 之间选择
UI 工具包 53
Unity UI (uGUI) 52
UI 文本 61, 84, 85
UI 文本游戏对象 232, 233
字符属性 233
颜色属性 235
材质属性 235
段落属性 233-235
光线投射填充属性 236
光线投射目标属性 236
参考链接 243
文本属性 233
UI 切换 350
切换组件 350
切换组组件 354
使用 350
UI 工具包 53
示例 517
概述 502, 503
使用,以制作编辑器虚拟宠物 517, 518
使用,以制作具有样式表和动画过渡的菜单 537
UI 工具包
安装 503-506
UI 工具包系统
面板设置 507
部分 506, 509
样式表 507
主题样式表 508, 509
UI 文档 507
Unity 编辑器
C# 代码,编写 528-536
Unity 可扩展标记语言 (UXML) 507
Unity 帧调试器 494
Unity 性能分析器 492,494
Unity UI 优化策略 495
布局组的使用,最小化 495,496
使用多个画布和画布层次结构 495
对象,缺失 496
光线投射计算,减少 497
时间对象池,启用/禁用 496,497
通用设计 39
通用设计原则 40
公平使用 40
灵活使用 41
低体力劳动 44
可感知信息 43
简单直观的使用 42
大小和空间方法 45
大小和空间使用 45
错误容忍 43
用户界面 (UI) 31,53
创建,使用 UI Builder 511,512
定义 3,4
设计,用于增强现实 (AR) 37
设计,用于混合现实 (MR) 36
设计,用于 VR 33,34
使交互式,使用 C# 513
粒子 457,458
参考,获取 UI 文档变量 514,515
UI 元素命名空间 514
可视元素事件,管理 516
可视元素属性,访问 516,517
V
垂直布局组 115,116
虚拟性连续体
参考链接 33
虚拟现实 (VR) 31,32
用于设计用户界面 (UI) 33,34
可视元素 509-511
可视用户界面
考虑 34,35
安排 34, 35
W
世界空间
工作,考虑因素 477
世界空间用户界面
使用 469-473

订阅我们的在线数字图书馆,全面访问超过 7000 本书籍和视频,以及领先的行业工具,帮助你规划个人发展并推进你的职业生涯。欲了解更多信息,请访问我们的网站。
第二十二章:为什么订阅?
-
使用来自 4000 多名行业专业人士的实用电子书和视频,节省学习时间,增加编码时间
-
通过为你量身定制的技能计划提高你的学习效果
-
每月免费获得一本电子书或视频
-
完全可搜索,便于轻松访问关键信息
-
复制粘贴、打印和收藏内容
你知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?你可以在packtpub.com升级到电子书版本,并且作为印刷书客户,你有权在电子书副本上获得折扣。如需更多信息,请联系我们 customercare@packtpub.com。
在www.packtpub.com,你还可以阅读一系列免费的技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。
你可能还会喜欢以下书籍
如果你喜欢这本书,你可能还会对 Packt 的以下书籍感兴趣:
Unity 2022 移动游戏开发
约翰·P·多兰
ISBN: 978-1-80461-372-6
-
为你的移动游戏设计响应式用户界面
-
检测碰撞、接收用户输入并创建玩家移动
-
使用移动设备输入创建有趣的游戏元素
-
添加自定义图标和演示选项
-
使用 Unity 的移动通知包保持玩家参与度
-
将社交媒体集成到你的项目中
-
为你的游戏添加增强现实功能,以吸引现实世界的吸引力
-
通过后处理和粒子效果让你的游戏更加吸引人
Unity 与 Blender 融合进行 3D 游戏开发
斯宾塞·格雷
ISBN: 978-1-80107-155-0
-
使用 Blender 将你的想象力转化为 3D 场景、道具和角色
-
掌握 Blender 中的 UV 展开和纹理模型
-
了解如何在 Blender 中绑定和动画模型
-
在 Unity 中为俯视、FPS 和其他类型的游戏动画和脚本模型
-
了解如何从 Blender 到 Unity 以及返回进行自定义资产的双向传输
-
熟悉 Unity 中的 ProBuilder、时间轴和 Cinemachine 的基础知识
Packt 正在寻找像你这样的作者
如果你感兴趣成为 Packt 的作者,请访问authors.packtpub.com并今天申请。我们已与数千名开发人员和科技专业人士合作,就像你一样,帮助他们将见解与全球科技社区分享。你可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。
分享你的想法
嗨!
我是《Unity UI 开发精通》一书的作者 Ashley Godbold。我真心希望您喜欢阅读这本书,并发现它对提高您的生产力和效率很有帮助。
如果您能在亚马逊上留下评论,分享您对这本书的看法,那将对我(以及其他潜在读者!)真的有很大帮助。
点击以下链接留下您的评论:
您的评论将帮助我们了解这本书中哪些地方做得很好,以及未来版本中哪些地方可以改进,所以这真的非常感谢。

祝好运,
Dr. Ashley Godbold
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢随时随地阅读,但无法携带您的印刷书籍到处走?
您的电子书购买是否与您选择的设备不兼容?
别担心!现在,每本 Packt 书籍都免费提供该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止这些,您还可以获得独家折扣、时事通讯和每日免费内容的每日邮箱访问权限
按照以下简单步骤获取优惠:
- 扫描以下二维码或访问以下链接:

packt.link/free-ebook/9781803235394
-
提交您的购买证明。
-
就这些!我们将直接将您的免费 PDF 和其他优惠发送到您的邮箱。






















































































































浙公网安备 33010602011771号