Unity-虚拟现实项目第二版-全-

Unity 虚拟现实项目第二版(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

今天,我们见证了虚拟现实VR)的兴起,这是一项令人兴奋的新技术和创意媒介,它承诺将从根本上改变我们与信息、朋友和整个世界互动的方式。

穿戴 VR头戴式显示器HMD),你可以观看立体 3D 场景。你可以通过移动头部来环顾四周,使用空间规模跟踪在空间中行走,并使用位置手控制器与虚拟对象交互。通过 VR,你可以参与完全沉浸式的体验。就像你真的身处另一个虚拟世界一样。

本书采用实践、基于项目的教学方法,教你如何使用 Unity 3D 游戏引擎的具体细节进行虚拟现实开发。我们通过一系列动手项目、逐步教程和深入讨论,使用 Unity 2018 和其他免费或开源软件。虽然 VR 技术正在快速发展,但我们将尝试捕捉到你可以用来制作自己的 VR 游戏和应用的基本原理和技术,使它们沉浸感强且舒适。

你将学习如何使用 Unity 开发可以在 Oculus Rift、Google Daydream、HTC VIVE 等设备上体验的 VR 应用。我们将涵盖对 VR 特别重要且可能独特的技术考虑因素。到本书结束时,你将能够开发丰富和交互式的虚拟现实体验。

关于作者和第二版

几年前,我在大学里学习了 3D 计算机图形学,在研究生阶段学习了用户界面设计,然后成立了一家小型软件公司,开发用于管理 AutoCAD 工程图纸的 3D 图形引擎。我们将业务出售给了 Autodesk。在随后的几年里,我专注于 2D 网络应用开发,撰写关于我的技术冒险的博客,并追求了几个新的创业项目。然后,在 2014 年 3 月,我了解到 Facebook 以 20 亿美元收购了 Oculus;这无疑激起了我的兴趣。我立即订购了我第一台 VR 头盔,即 Oculus DK2 开发者套件,并开始在 Unity 中开发小型 VR 项目。

2015 年 2 月,我有了写一本关于 Unity VR 开发的书的想法。Packt 立即接受了我的提案,我突然意识到,“哦不!我必须这么做!”在 6 个月内,2015 年 8 月,这本书的第一版出版了。从提案到提纲,再到章节草案、审阅、最终草案和出版,这是一个很短的时间。我着迷了。当时,我告诉我妻子,我觉得这本书有它自己的生命,“它在我体内挣扎着要出来,我必须让它出来。”她回答说,“听起来你就像怀孕了一样。”

在写作那个时期,谷歌纸盒是一个热门产品,但当时还没有消费级的 VR 设备。DK2 没有手柄控制器,只有 XBox 游戏控制器。在本书发布后的几个月,即 2015 年 11 月,HTC Vive 带着房间规模和位置跟踪手柄控制器进入市场。2016 年 3 月,Oculus Rift 的消费版发布。直到 2016 年 12 月,也就是本书出版后近一年半,Oculus 才发布了其位置跟踪的 Touch 手柄控制器。

自本书第一版以来,许多新的 VR 设备进入市场,硬件和软件功能得到了提升,Unity 游戏引擎也继续添加原生 VR SDK 集成和新功能来支持它们。Oculus、Google、Steam、Samsung、PlayStation、Microsoft 以及许多其他公司都加入了这一领域,随着行业的加速发展和繁荣。

同时,在 2016 年,我与 Packt 合作编写了另一本书,名为《Cardboard VR Projects for Android》,这是一本非 Unity VR 书籍,使用 Java 和 Android Studio 构建 Google Daydream 和纸盒应用。(在那本书中,你将构建并使用自己开发的移动设备 3D 图形引擎)。然后在 2017 年,我又与 Packt 合作编写了第三本书,名为《Augmented Reality for Developers》,这是一本关于 iOS、Android 和 HoloLens 设备上 AR 应用的激动人心且及时的基于 Unity 的项目书。

当我开始编写《Unity Virtual Reality Projects》的第二版时,我本以为这只是一个相对简单的任务,即更新到当前版本的 Unity,添加对位置跟踪手柄控制器支持,以及一些微调。但并非如此简单!虽然第一版中的许多基本原理和建议没有改变,但作为行业,我们在短短几年里学到了很多。例如,在 VR 中实现蹦床(我们在这个版本中取消的一个项目)真的不是一个好主意,因为这可能会引起运动病!

对于第二版,本书进行了重大修订和扩展。每一章和项目都进行了更新。我们将一些主题分成了独立的章节,并添加了全新的项目,例如音频火焰球游戏(第八章,玩转物理与火焰),动画(第十一章,动画与 VR 叙事),以及优化(第十三章,优化性能与舒适度)。我真诚地希望您觉得这本书有趣、有教育意义且有帮助,因为我们所有人都致力于创造伟大的新 VR 内容并探索这个令人惊叹的新媒介。

这本书面向谁

如果你对虚拟现实感兴趣,想了解它是如何工作的,或者想创建自己的 VR 体验,这本书适合你。无论你是非程序员且不熟悉 3D 计算机图形,还是在这两方面都有经验但新手,你都会从这本书中受益。任何 Unity 经验都是一种优势。如果你是 Unity 新手,你也可以阅读这本书,尽管你可能首先想完成 Unity 网站上的一些入门教程(unity3d.com/learn)。

游戏开发者可能已经熟悉这本书中关于 VR 项目的概念,同时学习了许多特定于 VR 的其他想法。已经知道如何使用 Unity 的移动和 2D 游戏设计师将发现另一个维度!工程师和 3D 设计师可能理解许多 3D 概念,但学习如何使用 Unity 引擎进行 VR 开发。应用开发者可能会欣赏 VR 在非游戏领域的潜在用途,并可能想要学习实现这一目标的工具。

本书涵盖的内容

第一章,为每个人提供虚拟现实的一切,是关于游戏和非游戏应用中消费者虚拟现实的新技术和机会的介绍,包括对立体视觉和头部跟踪的解释。

第二章,内容、对象和规模,在构建一个简单的场景时介绍了 Unity 游戏引擎,并回顾了使用 Blender、Tilt Brush、Google Poly 和 Unity EditorXR 等工具创建的 3D 内容的导入。

第三章,VR 构建与运行,帮助你设置系统并配置 Unity 项目,以便在目标设备(包括 SteamVR、Oculus Rift、Windows MR、GearVR、Oculus Go 和 Google Daydream)上构建和运行。

第四章,基于注视的控制,探讨了 VR 摄像机与场景中对象之间的关系,包括 3D 光标和基于注视的射线枪。本章还介绍了使用 C#编程语言的 Unity 脚本。

第五章,实用交互对象,探讨了使用控制器按钮和交互对象进行用户输入事件,使用包括轮询、可脚本化对象、Unity 事件和由工具包 SDK 提供的交互组件在内的各种软件模式。

第六章,世界空间 UI,展示了使用 Unity 世界空间画布实现 VR 用户界面(UI)的许多示例,包括抬头显示(HUD)、信息气泡、游戏内对象和基于手腕的菜单调色板。

第七章,移动与舒适,深入探讨了在 VR 场景中移动自己的技术,仔细研究了 Unity 第一人称角色对象和组件、移动、传送和房间规模 VR。

第八章,玩转物理和火焰,探讨了 Unity 物理引擎、物理材质、粒子系统以及更多 C#脚本,我们在构建一个拍球游戏的同时,根据您最喜欢的音乐击打火球。

第九章,探索交互式空间,教授如何构建一个交互式艺术画廊,包括关卡设计、艺术品照明、数据管理和在空间中传送。

第十章,使用所有 360 度,解释了 360 度媒体,并在各种示例中使用它们,包括地球仪、球体、全景照片和天空盒。

第十一章,动画和 VR 叙事,使用导入的 3D 资源和音轨,以及 Unity 时间轴和动画,构建一个完整的 VR 叙事体验。

第十二章,社交 VR 元宇宙,探讨了使用 Unity Networking 组件的多玩家实现,以及为 Oculus 平台头像和 VRChat 房间开发。

第十三章,优化性能和舒适度,展示了如何使用 Unity Profiler 和 Stats 窗口来减少您的 VR 应用中的延迟,包括优化您的 3D 艺术、静态照明、高效编码和 GPU 渲染。

要充分利用本书

在我们开始之前,有一些事情您需要准备。拿些小吃、一瓶水或一杯咖啡。除此之外,您需要一个安装了 Unity 2018 的 PC(Windows 或 Mac)。

您不需要一个超级强大的计算机配置。虽然 Unity 可以渲染复杂的场景,VR 制造商如 Oculus 已经发布了针对 PC 硬件的推荐规格,但实际上您可以用更少的配置来完成任务;即使是笔记本电脑也足以完成本书中的项目。

要获取 Unity,请访问store.unity.com,选择 Personal,并下载安装程序。免费的 Personal 版本就足够了。

我们还可选地使用 Blender 开源项目进行 3D 建模。本书不是关于 Blender 的,但如果您想使用,我们会使用它。要获取 Blender,请访问www.blender.org/download/并按照您平台的说明进行操作。

强烈建议您访问虚拟现实头戴式显示器HMD)以尝试您的构建并获得本书中开发项目的第一手体验。虽然并非完全必需,但在 Unity 中工作期间,您可以使用模拟模式。根据您的平台,您可能需要安装额外的开发工具。“第三章,VR 构建和运行”详细介绍了每个设备和平台所需的工具,包括 SteamVr、Oculus Rift、Windows MR、GearVR、Oculus Go、Google Daydream 以及其他。

这样就差不多了——一台 PC,Unity 软件,一个 VR 设备,第三章中描述的其他工具,我们就准备好了!哦,如果你从 Packt 网站下载了相关的资源,一些项目将会更加完整,如下所示。

下载项目资源和示例代码文件

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

你可以通过以下步骤下载代码文件:

  1. www.packtpub.com上登录或注册。

  2. 选择 SUPPORT 标签页。

  3. 点击代码下载与勘误表。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

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

  • Windows 版的 WinRAR/7-Zip

  • Mac 版的 Zipeg/iZip/UnRarX

  • Linux 版的 7-Zip/PeaZip

该书的代码包也托管在 GitHub 上,链接为github.com/PacktPublishing/Unity-Virtual-Reality-Projects-Second-Edition。如果代码有更新,它将在现有的 GitHub 仓库中更新。

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

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:https://www.packtpub.com/sites/default/files/downloads/B08826_UnityVirtualRealityProjectsSecondEdition_ColorImages.pdf。

使用的约定

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

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

代码块设置如下:

void Update () {
Transform camera = Camera.main.transform;
Ray ray;
RaycastHit hit;
GameObject hitObject; 

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

 using UnityEngine;
using UnityEngine.Networking;
public class AvatarMultiplayer : NetworkBehaviour
{
public override void OnStartLocalPlayer()
{
GameObject camera = Camera.main.gameObject;
transform.parent = camera.transform;
transform.localPosition = Vector3.zero;
GetComponent<OvrAvatar>().enabled = false;
}
} 

粗体:表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的文字会像这样显示。以下是一个示例:“从管理面板中选择系统信息。”

警告或重要提示看起来像这样。

小贴士和技巧看起来像这样。

联系我们

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

一般反馈: 发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过电子邮件发送至questions@packtpub.com

勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过发送链接至copyright@packtpub.com与我们联系。

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

评论

请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需了解更多关于 Packt 的信息,请访问 packtpub.com

第一章:为每个人提供几乎一切

这种虚拟现实事物引发了一个问题,那就是“存在”意味着什么?

“在哪里?”

在手机出现之前,你会打电话给某人,这显然是没有意义的

说,“嘿,你在哪里?”你知道他们在哪里,你打电话到他们的家,

他们就在那里。

那么手机出现了,你开始听到人们说,“你好。哦,我在星巴克,”因为对方不一定知道你在哪里,因为你已经从家庭语音通信中解脱出来。

所以当我看到一款虚拟现实演示时,我脑海中浮现出这样的画面:回到家,我妻子已经把孩子们安顿好,她有几分钟的空闲时间,她正坐在沙发上,脸上戴着护目镜。我走过去轻轻拍她的肩膀,然后说:“嘿,你在哪里?”

这太奇怪了。那个人就坐在你面前,但你不知道他们在哪里。

-移动专家和播客主持人乔纳森·斯塔克

欢迎来到虚拟现实VR)!在这本书中,我们将探讨如何自己创建虚拟现实体验。我们将通过一系列动手项目、逐步教程和深入讨论来探索,使用 Unity 3D 游戏引擎和其他免费或开源软件。尽管虚拟现实技术正在快速发展,但我们将尝试捕捉到你可以用来制作你的 VR 游戏和应用的基本原理和技术,使它们感觉沉浸且舒适。

在第一章中,我们将定义虚拟现实,并说明它不仅适用于游戏,还适用于许多其他感兴趣和富有成效的领域。本章讨论以下主题:

  • 什么是虚拟现实?

  • 虚拟现实与增强现实之间的区别

  • 虚拟现实应用可能与虚拟现实游戏有何不同

  • 虚拟现实体验的类型

  • 开发 VR 所需的技术技能

虚拟现实对你来说意味着什么?

今天,我们是消费级虚拟现实蓬勃发展的见证者,这是一项令人兴奋的技术,它承诺从根本上改变我们与信息、朋友和整个世界互动的方式。

什么是虚拟现实?一般来说,VR 是计算机生成的 3D 环境模拟,对体验者来说,它看起来非常真实,使用特殊的电子设备。目标是实现强烈的沉浸感,让人感觉仿佛置身于虚拟环境中。

当前的消费级虚拟现实技术涉及佩戴 HMD(头戴式显示器护目镜)来观看立体 3D 场景。你可以通过移动头部来四处张望,通过使用手控或动作传感器来四处走动。你正参与一个完全沉浸式的体验。仿佛你真的在另一个虚拟世界中。以下图片展示了我在 2015 年体验的Oculus Rift 开发套件 2DK2):

虚拟现实并不新鲜。它已经存在了几十年,尽管它隐藏在学术研究实验室和高端工业和军事设施中。它体积庞大、笨重且昂贵。Ivan Sutherland 在 1965 年发明了第一个 HMD(见amturing.acm.org/photo/sutherland_3467412.cfm)。它被固定在天花板上!在过去,已经尝试过几次将消费级虚拟现实产品推向市场,但都失败了。

2012 年,Oculus VR LLC 的创始人 Palmer Luckey 向著名游戏开发者 John Carmack 展示了一个临时制作的头戴式 VR 显示器。他们一起成功运行了一个Kickstarter活动,并向一个热情的社区发布了名为Oculus Rift 开发套件 1DK1)的开发者套件。这引起了投资者以及马克·扎克伯格的注意,2014 年 3 月,Facebook 以 20 亿美元的价格收购了该公司。在没有产品、没有客户和无限前景的情况下,它吸引的资金和关注帮助推动了新型消费产品的诞生。

同时,其他人也在开发自己的产品,这些产品很快就被推向市场,包括 Steam 的 HTC VIVE、Google Daydream、索尼 PlayStation VR、三星 Gear VR、微软的沉浸式混合现实等。不断有新的创新和设备被引入,以增强 VR 体验。

大部分基础研究已经完成,技术现在也因为移动技术的广泛应用而变得可负担。有一个庞大的开发者社区,他们有制作 3D 游戏和移动应用的经验。创意内容制作者也加入了进来,媒体也在大肆宣传。最后,虚拟现实真的实现了!

什么?虚拟现实真的实现了?哈哈!如果它是虚拟的,那它怎么能……哦,算了。

最终,我们将超越对新兴硬件设备的关注,认识到内容为王。当前一代 3D 开发软件(商业的、免费的和开源的),催生了大量的独立游戏开发者,也可以用来构建非游戏 VR 应用。

虽然 VR 的主要爱好者在游戏社区,但其潜在的应用范围远不止于此。任何现在使用 3D 建模和计算机图形的业务,如果使用 VR 技术将会更加有效。VR 提供的沉浸式存在感可以增强今天所有常见的在线体验,包括工程、社交网络、购物、营销、娱乐和商业发展。在不久的将来,用 VR 头戴设备浏览 3D 网站可能和今天访问普通平面网站一样普遍。

头戴式显示器的类型

目前,虚拟现实头盔(HMD)主要有两大类——桌面 VR移动 VR,尽管这些区别正变得越来越模糊。最终,我们可能就像谈论传统计算平台一样,按照操作系统来谈论它们——Windows、Android 或控制台 VR。

桌面 VR

在桌面 VR(和控制台 VR)中,您的头戴式设备是一个连接到更强大电脑的外部设备,该电脑处理重图形。电脑可能是一台 Windows PC、Mac、Linux 或游戏机,尽管 Windows 在 PC 中最为突出,而 PS4 在控制台 VR 方面是畅销产品。

很可能,头戴式设备通过有线连接到电脑。游戏在远程机器上运行,而 HMD 是一个带有运动感应输入的外部显示设备。术语桌面是一个不幸的错误名称,因为它同样可能被放置在客厅或书房。

Oculus Rift(www.oculus.com/)是一个例子,其眼镜具有集成显示屏和传感器。游戏在单独的 PC 上运行。其他桌面头戴式设备包括HTC VIVE、索尼的PlayStation VRMicrosoft immersive Mixed Reality

桌面 VR 设备依赖于桌面电脑(通常通过视频和 USB 线缆)的 CPU 和图形处理单元(GPU)功率,越多越好。请参考您特定设备的推荐规格要求。

然而,为了这本书的目的,我们的项目中不会有任何重渲染,你可以用最低的系统规格来应对。

移动 VR

移动 VR 起源于Google Cardboard(vr.google.com/cardboard/),这是一个简单的用于两个镜头和手机插槽的包装设备。手机的显示屏用于显示双立体视图。它具有旋转头部跟踪,但没有位置跟踪。Cardboard 还允许用户点击或轻触其侧面来在游戏中进行选择。图像的复杂性有限,因为它使用手机的处理器在手机显示屏上渲染视图。

Google Daydream 和 Samsung GearVR 通过要求更高的最低性能规格来改进平台,包括在手机中更大的处理能力。GearVR 的头戴式设备包括运动传感器以辅助手机设备。这些设备还引入了三自由度(DOF)手控制器,可以在 VR 体验中像激光笔一样使用。

新一代移动 VR 设备包括一体式头戴式设备,如 Oculus Go,内置显示屏和处理器,消除了对单独手机的需求。新机型可能包括深度传感器和空间映射处理器,以跟踪用户在 3D 空间中的位置。

重要的是,本书中的项目将探索从高端到低端消费级 VR 设备范围内的功能。但一般来说,我们的项目不需要太多的处理能力,也不需要高端 VR 功能,因此你可以在任何这些类型的设备上开始开发 VR,包括 Google Cardboard 和普通手机。

如果你感兴趣的是在 Android 上直接使用 Java 而不是通过 Unity 游戏引擎开发 Google Daydream 的 VR 应用程序,请参阅作者的另一本书,Packt Publishing 出版的《Cardboard VR Projects for Android》(www.packtpub.com/application-development/cardboard-vr-projects-android)。

虚拟现实与增强现实的区别

可能有必要澄清虚拟现实不是什么。

与 VR 相类似的一种技术是增强现实AR),它将计算机生成的图像(CGI)与真实世界的视图相结合。随着苹果的 ARKit 和谷歌的 ARCore 的推出,智能手机上的 AR 最近引起了广泛关注。此外,Vuforia AR 工具包现在直接集成到 Unity 游戏引擎中,有助于推动该技术的进一步采用。移动设备上的 AR 将 CGI 叠加在来自摄像头的实时视频之上。

增强现实(AR)的最新创新是可穿戴的 AR 头戴设备,例如微软的HoloLensMagic Leap,它们直接在你的视野中显示计算机图形。图形不会混合到视频图像中。如果 VR 头戴设备像封闭的眼镜,那么 AR 头戴设备就像半透明的太阳镜,将现实世界的光线与 CGI 结合在一起。AR 的一个挑战是确保 CGI 始终与真实世界空间中的对象对齐并映射,同时在移动过程中消除延迟,以便它们(CGI 和真实世界空间中的对象)保持对齐。

AR 在未来的应用方面与 VR 一样有潜力,但它有所不同。尽管 AR 旨在让用户在当前环境中参与其中,但虚拟现实是全沉浸式的。在 AR 中,你可能会打开手,看到一个小木屋就放在你的手掌上,但在 VR 中,你会直接被传送到小木屋内部,你可以在里面四处走动。

我们也开始看到结合了 VR 和 AR 功能的混合设备,并允许你在模式之间切换。

如果你感兴趣的是开发 AR 应用程序,请参阅作者由 Packt Publishing 出版的《Augmented Reality for Developers》(www.packtpub.com/web-development/augmented-reality-developers)一书。

应用程序与游戏

消费级虚拟现实始于游戏。视频游戏玩家已经习惯了在高度互动的超真实 3D 环境中参与其中。VR 只是提高了门槛。

游戏玩家是高端图形技术的早期采用者。数千万台游戏主机和基于 PC 的组件的大规模生产和供应商之间的竞争导致价格降低和性能提升。游戏开发者也效仿这一做法,经常推动技术前沿,从硬件和软件中榨取每一分性能。玩家们对游戏的要求非常高,市场也一直努力满足他们的需求。因此,许多甚至可以说是大多数当前波次的 VR 硬件和软件公司,首先将目标定位在视频游戏行业。例如,Oculus Store 上的大多数 VR 应用,如 Rift(www.oculus.com/experiences/rift/)、GearVR(www.oculus.com/experiences/gear-vr/)和 Google Play for Daydream(play.google.com/store/search?q=daydream&c=apps&hl=en),都是游戏。当然,Steam VR 平台(store.steampowered.com/steamvr)几乎全部是关于游戏的。玩家是 VR 最热情的倡导者,并且非常重视其潜力。

游戏开发者知道,游戏的核心是游戏机制,即规则,这些规则在很大程度上独立于皮肤,即游戏的主题。游戏玩法机制可以包括谜题、运气、策略、时机或肌肉记忆。VR 游戏可以拥有相同的机制元素,但可能需要根据虚拟环境进行调整。例如,在控制台视频游戏中,一个第一人称角色行走可能比现实生活中的实际步伐快 1.5 倍。如果不是这样,玩家会感觉游戏太慢、太无聊。将同样的角色放入 VR 场景中,他们会觉得太快;如果玩家真的身处战区,这可能会让他们感到恶心。在 VR 中,你希望你的角色以正常的、地球上的步伐行走。并非所有视频游戏都能很好地映射到 VR 中;当你真的身处战区时,体验其中可能并不有趣。

话虽如此,虚拟现实也正在应用于游戏以外的领域。尽管游戏仍然很重要,但非游戏应用最终将超越它们。这些应用可能在许多方面与游戏不同,其中最显著的是对游戏机制的强调较少,而对体验本身或特定应用目标的强调较多。当然,这并不排除某些游戏机制。例如,应用可能专门设计来训练用户掌握特定技能。有时,将商业或个人应用游戏化可以使它更有趣,更有效地通过竞争驱动期望的行为。

通常来说,非游戏 VR 应用更注重的是体验本身,而非胜利。

这里有一些人们正在努力开发的非游戏应用的例子:

  • 旅游与观光: 不必离开家就能游览远方的地方。在一个下午内参观巴黎、纽约和东京的艺术博物馆。在火星上散步。你甚至可以在佛蒙特州的冬日小屋中享受印度的春天色彩节——霍利节。

  • 机械工程与工业设计: 计算机辅助设计软件,如 AutoCAD 和 SOLIDWORKS,开创了三维建模、仿真和可视化的先河。有了 VR,工程师和设计师可以在产品实际建造之前直接体验最终产品,并以极低的成本玩转各种假设情景。考虑迭代一个新汽车设计。它看起来如何?它的性能如何?坐在驾驶座上时它看起来如何?

  • 建筑与土木工程: 建筑师和工程师总是构建他们设计的比例模型,无论是为了向客户和投资者推销想法,还是更重要的是,为了验证关于设计的许多假设。目前,建模和渲染软件通常用于从建筑平面图构建虚拟模型。有了 VR,与利益相关者的对话可以更加自信。其他人员,如室内设计师、暖通空调工程师和电气工程师,可以更早地参与到这个过程中。

  • 房地产: 房地产经纪人一直是互联网和可视化技术的快速采用者,以吸引买家并完成销售。房地产搜索网站是互联网上最早的成功应用之一。今天,出售房产的在线全景视频浏览已成为常态。有了 VR,我可以在纽约,并找到在洛杉矶的住处。

  • 医学: 虚拟现实在健康和医学方面的潜力可能真的是生死攸关的问题。每天,医院使用 MRI 和其他扫描设备来产生我们骨骼和器官的模型,这些模型用于医疗诊断和可能的术前规划。使用 VR 来增强可视化和测量将提供更直观的分析。虚拟现实还被用于手术模拟,以训练医学生。

  • 心理健康: 虚拟现实体验已被证明在治疗环境中对治疗创伤后应激障碍(PTSD)非常有效,这被称为暴露疗法,在这种疗法中,患者在经过培训的治疗师指导下,通过重述经历来面对他们的创伤记忆。同样,VR 也被用于治疗蜘蛛恐惧症(对蜘蛛的恐惧)和对飞行的恐惧。

  • 教育: 虚拟现实(VR)的教育机会几乎不言而喻。第一个成功的 VR 体验之一是《太空巨兽》,它让你能够亲身体验太阳系。在科学、历史、艺术和数学等领域,VR 将帮助所有年龄段的学生,因为正如他们所说,实地考察比教科书更有效。

  • 培训:丰田展示了驾驶员教育的 VR 模拟,以教育青少年关于分心驾驶的风险。在另一个项目中,职业学生有机会体验操作起重机和其他重型建筑设备。通过展示高风险情况和替代虚拟场景,VR 可以增强对紧急响应人员、警察、消防和救援人员的培训。国家橄榄球联盟NFL)和大学球队正在寻求 VR 进行运动训练。

  • 娱乐与新闻:虚拟参加摇滚音乐会和体育赛事。观看音乐视频情色内容。仿佛亲自在场重新体验新闻事件。享受 360 度电影体验。虚拟现实将改变叙事艺术。

哇,这真是一个相当长的列表!这只是低垂的果实。

这本书的目的不是深入探讨这些应用中的任何一个。相反,我希望这次调查能激发你的思考,并为你提供一个关于虚拟现实如何为每个人提供几乎任何可能的潜在想法。

虚拟现实真正的工作原理

那么,是什么让每个人都对 VR 如此兴奋呢?戴上你的头戴式设备,你将体验到合成场景。它看起来是 3D 的,感觉是 3D 的,甚至你可能有一种实际上在虚拟世界中的感觉。显而易见的事情是:VR 看起来和感觉真的很酷!但为什么呢?

沉浸感临场感是描述 VR 体验质量的两个词。终极目标就是将两者提升到极致,以至于你感觉如此真实,以至于你忘记了你在虚拟世界中。沉浸感是通过模拟你身体接收到的感官输入(视觉、听觉、运动等)而产生的。这可以从技术上解释。临场感是你被带到那里的直观感觉——一种深刻的情感或直觉感觉。你可以这样说,沉浸感是 VR 的科学,而临场感是艺术。而且,我的朋友,这很酷。

不同的技术和技巧结合在一起,使 VR 体验成为可能,这可以分为两个基本领域:

  • 3D 观看

  • 头部姿态跟踪

换句话说,显示屏和传感器,就像今天内置在移动设备中的那些,是 VR 今天可行且价格合理的主要原因。

假设 VR 系统知道在任何给定时刻你的头部位置。假设它可以立即渲染并立体显示这个精确视角的 3D 场景。那么,无论何时何地移动,你都会看到虚拟场景正如你应看到的那样。你将拥有几乎完美的视觉 VR 体验。这就是全部。哇!

嗯,慢一点。字面上的意思。

立体 3D 观看

分屏立体摄影是在摄影发明不久后发现的,就像下面图片中展示的 1876 年流行的立体照片观看器(B.W. Kilborn & Co,新罕布什尔州利特尔顿;参见en.wikipedia.org/wiki/Benjamin_W._Kilburn)。立体照片为左右眼睛提供了单独的视图,这些视图略微偏移以产生视差。这会让大脑误以为它是一个真正的三维视图。该设备包含为每只眼睛单独设计的镜头,让你可以轻松地近距离聚焦在照片上:

类似地,在 Unity 中,VR 功能相机的首要任务是渲染这些并排的立体视图。

假设你戴着 VR 头盔,并且你的头部保持非常静止,使得图像看起来像是冻结的。它仍然比简单的立体照片要好。为什么?

传统的立体照片具有相对较小的双图像,呈矩形装订。当你的眼睛聚焦在视图的中心时,3D 效果令人信服,但你仍会看到视图的边界。移动你的视线(即使头部不动),任何剩余的沉浸感都会完全消失。你只是一个在外的观察者,透过一个场景模型窥视。

现在,考虑一下没有头戴式设备时的 VR 屏幕是什么样的(参见下面的截图):

你首先会注意到,每只眼睛都有一个桶形视野。为什么会这样?头戴式眼镜的镜头是一个非常广角镜头。所以,当你通过它看时,你会有一个很宽的视野。事实上,它如此之宽(且高),以至于扭曲了图像(枕形效应)。图形软件 SDK 执行这种扭曲的逆操作(桶形扭曲),使得通过镜头看起来是正确的。这被称为眼动扭曲校正。结果是,一个明显的视野FOV)足够宽,可以包括更多的你的周边视野。例如,Oculus Rift 的 FOV 大约为 100 度。(我们在第十章中更多地讨论了 FOV,使用所有 360 度。)

当然,每只眼睛的视角也略微偏移,相当于你眼睛之间的距离或瞳距IPD)。瞳距用于计算视差,并且人与人之间可能不同。(Oculus 配置实用程序附带了一个测量和配置你的 IPD 的实用程序。或者,你可以向眼科医生询问一个准确的测量值。)

这可能不太明显,但如果你仔细观察 VR 屏幕,你会看到颜色分离,就像从打印头没有正确对齐的彩色打印机中得到的那样。这是故意的。光线通过透镜时,根据光线的波长以不同的角度折射。再次强调,渲染软件执行了颜色分离的逆操作,以便对我们看起来是正确的。这被称为色差校正。它有助于使图像看起来非常清晰。

屏幕的分辨率也很重要,以获得令人信服的视觉效果。如果分辨率太低,你会看到像素,或者有些人称之为屏幕门效应。显示器的像素宽度和高度是在比较 HMD 时经常引用的规格,但每英寸像素数PPI)可能更重要。显示技术方面的其他创新,如像素模糊视场渲染(在眼球注视的地方显示更高分辨率的细节),也将有助于减少屏幕门效应。

在 VR 中体验 3D 场景时,你还必须考虑每秒帧数FPS)。如果 FPS 太慢,动画看起来会不流畅。影响 FPS 的因素包括 GPU 性能和 Unity 场景的复杂性(多边形数量和光照计算),以及其他因素。在 VR 中,这个问题会加剧,因为你需要为每只眼睛绘制场景一次。技术创新,如针对 VR 优化的 GPU、帧插值和其他技术,将提高帧率。对我们开发者来说,Unity 中的性能调优技术,例如移动游戏开发者使用的那些技术,也可以应用于 VR。(我们将在第十三章优化性能和舒适度中更多讨论性能优化。)这些技术和光学有助于使 3D 场景看起来更真实。

声音也非常重要——比许多人意识到的更重要。VR 应该戴着立体声耳机来体验。事实上,当音频做得很好但图形相当糟糕时,你仍然可以有一个很好的体验。我们在电视和电影中经常看到这种情况。在 VR 中也是如此。双耳音频为每个耳朵提供了一个声音源的立体声视图,这样你的大脑就会想象它在 3D 空间中的位置。不需要特殊的听力设备。普通耳机就可以(扬声器不行)。例如,戴上你的耳机,访问www.youtube.com/watch?v=IUDTlvagjJA上的虚拟理发店。真正的 3D 音频提供了一种更逼真的空间音频渲染,其中声音会从附近的墙壁上反弹,并且可以被场景中的障碍物遮挡,以增强第一人称体验和现实感。

最后,VR 头盔应该舒适地贴合你的头部和面部,这样你很容易忘记你戴着它,并且应该阻挡来自你周围真实环境的灯光。

头部追踪

因此,我们有一个舒适的 3D 画面,可以在具有宽阔视野的 VR 头盔中观看。如果这就是全部,当你移动头部时,你会感觉好像有一个模型盒子粘在你的脸上。移动你的头部,盒子也会随之移动,这就像拿着古老的立体照片装置或童年的观景大师。幸运的是,VR 要好得多。

VR 头盔内部有一个运动传感器(IMU),它可以检测所有三个轴上的空间加速度和旋转速率,提供所谓的六自由度。这是在手机和一些游戏控制器中常见的技术。当你移动头部时,头盔上的传感器会计算当前视角,并在绘制下一帧图像时使用。这被称为运动检测

上一代移动运动传感器足够我们用手机玩移动游戏,但对于 VR 来说,它的精度还不够。这些不准确(舍入误差)会随着时间的推移而累积,因为传感器每秒采样数千次,最终可能会失去在现实世界中的位置。这种漂移是老式的基于手机的 Google Cardboard VR 的一个主要缺陷。它能够感知你的头部运动,但失去了你头部方向的信息。当前一代的手机,如符合 Daydream 规范的 Google Pixel 和 Samsung Galaxy,已经升级了传感器。

高端 HMD 通过一个单独的位置追踪机制来处理漂移。Oculus Rift 使用内向外位置追踪,其中 HMD 上的红外 LED 阵列(不可见)由外部光学传感器(红外摄像头)读取,以确定你的位置。你需要保持在摄像头的视野内,头部追踪才能工作。

或者,Steam VR VIVE Lighthouse 技术使用外向内位置追踪,在房间内放置两个或多个简单的激光发射器(就像在杂货店结账处的条形码扫描仪中的激光一样),头盔上的光学传感器读取这些光线以确定你的位置。

Windows MR 头盔不使用外部传感器或摄像头。相反,它集成了摄像头和传感器,以执行你周围局部环境的空间映射,以便在现实世界的 3D 空间中定位和追踪你的位置。

无论哪种方式,主要目的是准确找到你的头部和其他类似装备的设备,如手持控制器。

位置、倾斜和头部的前进方向,或者说是头部姿态,由图形软件用来从这个视角重新绘制 3D 场景。Unity 等图形引擎在这方面做得非常好。

现在,假设屏幕以 90 FPS 的速度更新,而你正在移动你的头部。软件确定头部姿态,渲染 3D 视图,并将其绘制在 HMD 屏幕上。然而,你仍然在移动头部。所以,当它显示出来时,图像相对于你的当前位置有点过时。这被称为延迟,这可能会让你感到恶心。

由于 VR 中的延迟造成的运动病发生在你移动头部而你的大脑期望你周围的世界会精确同步变化的时候。任何可感知的延迟都可能让你感到不舒服,至少可以说。

延迟可以测量为从读取运动传感器到渲染相应图像的时间,或者称为传感器到像素的延迟。根据 Oculus 的 John Carmack 的说法:

总延迟为 50 毫秒会感觉响应迅速,但仍然有明显的延迟。20 毫秒或更少将提供被认为可接受的最低延迟水平。

有许多非常聪明的策略可以用来实现延迟补偿。这些细节超出了本书的范围,并且随着设备制造商对技术的改进,这些细节不可避免地会发生变化。其中一种策略是 Oculus 所说的时间扭曲,它试图通过渲染完成时猜测你的头部位置,并使用那个未来的头部姿态而不是实际检测到的姿态。所有这些都在 SDK 中处理,所以作为一个 Unity 开发者,你不需要直接处理它。

同时,作为 VR 开发者,我们需要意识到延迟以及其他导致运动病的原因。通过加快每帧的渲染(保持推荐的 FPS)可以减少延迟。这可以通过阻止你的头部移动得太快和使用其他技术让你感到稳定和舒适来实现。

另一件事是 Rift 为了改善头部跟踪和现实感所做的是,它使用颈部骨骼表示,以便所有接收到的旋转都能更准确地映射到头部旋转。例如,低头看你的膝盖会产生一个小小的向前平移,因为它知道在原地旋转头部向下是不可能的。

除了头部跟踪、立体摄影和 3D 音频之外,虚拟现实体验可以通过身体跟踪、手部跟踪(以及手势识别)、运动跟踪(例如,VR 跑步机)和带有触觉反馈的控制器来增强。所有这些的目标都是为了增强你在虚拟世界中的沉浸感和存在感。

VR 体验类型

虚拟现实体验不仅仅是一种。实际上,有很多种。考虑以下类型的虚拟现实体验:

  • 场景:在最简单的情况下,我们构建一个 3D 场景。你从第三人称视角观察。你的眼睛是相机。实际上,每只眼睛都是一个独立的相机,为你提供立体视图。你可以四处张望。

  • 第一人称体验:这次,你将作为一个自由移动的虚拟形象沉浸在场景中。使用输入控制器(键盘、游戏控制器或某些其他技术),你可以四处走动并探索虚拟场景。

  • 交互式虚拟环境:这就像第一人称体验,但它有一个额外的功能——当你处于场景中时,你可以与之交互的对象。物理在其中发挥作用。对象可能会对你做出反应。你可能会被赋予特定的目标去实现和与游戏机制相关的挑战。你甚至可能会获得分数并保持得分。

  • 3D 内容创作:在 VR 中,创建可以在 VR 中体验的内容。"Google Tilt Brush"是第一批轰动一时的体验之一,"Oculus Medium"和"Google Blocks"以及其他也是如此。Unity 正在为 Unity 开发者开发EditorXR,以便他们可以直接在 VR 场景中工作在自己的项目上。

  • 轨道上骑行:在这种体验中,你将坐着并通过环境(或环境围绕你变化)被运输。例如,你可以通过这种虚拟现实体验乘坐过山车。然而,这不一定是一个极端刺激的过山车。它可能是一个简单的房地产浏览体验,甚至是一个缓慢、轻松和冥想的体验。

  • 360 度媒体:想象一下使用GoPro拍摄的强化全景图像,这些图像被投射在球体的内部。你位于球体的中心,可以四处环顾。一些纯粹主义者不认为这是真正的虚拟现实,因为你在看到的是一个投影而不是模型渲染。然而,它可以提供一种有效的存在感。

  • 社交 VR:当多个玩家进入同一个 VR 空间并可以看到彼此的虚拟形象并与之交谈时,它成为一种显著的社会体验。

在本书中,我们将实施一系列项目,展示如何构建这些类型的 VR 体验。为了简洁,我们需要保持其纯粹和简单,并提供进一步研究的建议区域。

对 VR 重要的技术技能

书的每一章都介绍了如果你想要构建自己的虚拟现实应用,那么重要的新技术技能和概念。你将在本书中学到以下内容:

  • 世界规模:在为 VR 体验构建时,注意 3D 空间和规模很重要。在 Unity 中,一个单位通常等于虚拟世界中的一米。

  • 第一人称控制:有各种技术可以用来控制你的虚拟形象(第一人称摄像头)的运动、基于注视的选择、跟踪手输入控制器和头部动作。

  • 用户界面控制:与传统的视频(和移动)游戏不同,在 VR 中,所有用户界面组件都在世界坐标中,而不是屏幕坐标中。我们将探讨如何向用户展示通知、按钮、选择器和其他用户界面UI)控制,以便他们可以交互和做出选择。

  • 物理和重力:在 VR 中的存在感和沉浸感至关重要的是世界的物理和重力。我们将利用 Unity 物理引擎的优势。

  • 动画:在场景中移动对象被称为动画——当然!它可以是沿着预定义的路径,或者可能使用 AI(人工智能)脚本,该脚本根据环境中的事件遵循逻辑算法。

  • 多用户服务:实时网络和多用户游戏不易实现,但在线服务使得实现变得容易,无需你是计算机工程师。

  • 构建运行和优化:不同的 HMD 使用不同的开发者工具包 SDK 和资产来构建针对特定设备的应用程序。我们将考虑让你可以使用单一界面针对多个设备的技术。理解渲染管线以及如何优化性能是 VR 开发的关键技能。

我们将使用 C#语言编写脚本,并在需要时使用 Unity 的功能来完成工作。

然而,有一些技术领域我们不会涉及,例如真实渲染、着色器、材质和照明。我们不会深入建模技术、地形或人形动画。我们也不会讨论游戏机制、动力学和策略。所有这些都是非常重要的主题,你可能需要学习(或你的团队成员需要学习),除了这本书之外,以构建完整、成功且沉浸式的 VR 应用程序。

那么,让我们看看这本书实际上涵盖了什么内容,以及它面向哪些读者。

这本书涵盖的内容

这本书采用了一种实用、基于项目的教学方法,通过使用 Unity 3D 游戏开发引擎来教授虚拟现实开发的细节。你将学习如何使用 Unity 2018 开发 VR 应用程序,这些应用程序可以通过 Oculus Rift 或 Google Cardboard 等设备以及各种介于两者之间的设备来体验。

然而,这里我们有一个小问题——技术正在非常快速地发展。当然,这是一个好问题。实际上,这是一个很棒的问题,除非你是项目中的开发者或这本书的作者!如何写一本书,使得它在出版当天就不会有过时的内容?

在整本书中,我试图提炼出一些超越任何短期虚拟现实技术进步的通用原则,以下是一些:

  • 不同类型 VR 体验的分类以及示例项目

  • 重要的技术思想和技能,尤其是与构建 VR 应用程序相关的内容

  • 对 VR 设备和软件工作原理的一般解释

  • 确保用户舒适并避免 VR 运动病的策略

  • 使用 Unity 游戏引擎构建 VR 体验的说明

一旦 VR 变得主流,许多这些课程可能将变得显而易见而不是过时,就像 20 世纪 80 年代关于如何使用鼠标的解释在今天看起来很愚蠢一样。

你是谁?

如果你对虚拟现实感兴趣,想了解它是如何工作的,或者想自己创建 VR 体验,这本书就是为你准备的。我们将通过一系列动手项目、逐步教程和深入讨论,使用 Unity 3D 游戏引擎带你了解。

无论你是非程序员,对 3D 计算机图形不熟悉,还是既有经验但又是虚拟现实的新手,你都会从这本书中受益。这可能将是你的 Unity 初探,或者你可能有一些经验,但也不需要成为专家。然而,如果你是 Unity 的新手,只要你意识到你需要适应这本书的节奏,你就可以选择这本书。

游戏开发者可能已经熟悉书中的一些概念,这些概念被应用到 VR 项目中,以及许多特定于 VR 的其他想法。工程师和 3D 设计师可能理解许多 3D 概念,但他们可能希望学习如何使用游戏引擎进行 VR 开发。应用开发者可能会欣赏 VR 在非游戏领域的潜力,并想学习实现这一目标的工具。

无论你是谁,我们将把你变成一个3D 软件 VR 忍者。好吧,好吧,这可能对这个小书来说有点过分,但我们会尽力让你走上这条路。

摘要

在本章中,我们探讨了虚拟现实,并意识到它对不同的人意味着很多不同的事情,可以有不同的应用。没有单一的定义,它是一个不断变化的目标。我们并不孤单,因为每个人都在努力弄清楚。事实上,虚拟现实是一种新的媒介,可能需要数年,甚至数十年才能达到其潜力。

VR 不仅仅是游戏;它可以是许多不同应用的变革者。我们确定了十几个。有不同类型的 VR 体验,我们将在本书的项目中探讨。

VR 头戴式设备可以分为需要独立处理单元(如运行强大 GPU 的台式 PC 或游戏机)的,以及使用你的移动技术进行处理的。

我们都是生活在激动人心的时代的先驱。因为你正在阅读这本书,你也是其中之一。接下来发生的一切实际上取决于你。预测未来的最佳方式就是创造它

那么,让我们开始吧!

在下一章中,我们将直接进入 Unity,创建我们的第一个 3D 场景,并了解世界坐标、缩放和导入 3D 资产。然后,在第三章,VR 构建和运行中,我们将在 VR 头戴式设备上构建和运行它,并讨论虚拟现实实际上是如何工作的。

第二章:内容、对象和比例

你可能还记得小时候用鞋盒制作学校项目模型。今天,我们将使用 Unity 来完成这个项目。让我们组装我们的第一个场景,它由简单的几何物体组成。在这个过程中,我们会大量讨论世界尺度。然后我们将探索开发者和艺术家用于将资产导入 Unity 的各种 3D 内容创建工具。在本章中,我们将讨论以下主题:

  • Unity 3D 游戏引擎的简要介绍

  • 在 Unity 中创建一个简单的模型

  • 制作一些测量工具,包括单位立方体和网格投影仪

  • 使用Blender创建一个带有纹理贴图的立方体并将其导入 Unity

  • 使用 Google Tilt Brush 创建 3D 草图并通过 Google Poly 将其导入 Unity

  • 使用实验性的 Unity EditorXR 工具直接在 VR 中编辑场景

开始使用 Unity

如果你还没有在 PC 上安装 Unity 3D 游戏引擎应用程序,现在就安装它!功能齐全的个人版是免费的,并且可以在 Windows 和 Mac 上运行。要获取 Unity,请访问store.unity.com/,选择你想要的版本,点击“下载安装程序”,然后继续按照说明操作。本书假设你拥有 Unity 2017.2 或更高版本。

对于那些初学者,我们将以缓慢而细致的方式处理这个第一部分,比书中后面的部分提供更多的指导。此外,即使你已经熟悉 Unity 并开发了你的游戏,回顾基本概念也可能是有价值的,因为在为虚拟现实设计时,规则有时是不同的。

创建一个新的 Unity 项目

让我们创建一个名为VR_is_Awesome的新 Unity 项目,或者你可以取你喜欢的任何名字。

要创建一个新的 Unity 项目,从你的操作系统启动 Unity,将出现打开对话框。从这个对话框中,选择“新建”,这将打开一个“新建项目”对话框,如下面的截图所示:

填写你的项目名称,并确认文件夹位置是你想要的。确保 3D 被选中(在右侧)。目前不需要添加任何额外的资产包,因为我们稍后如果需要会引入它们。点击“创建项目”。

Unity 2018 引入了 Unity Hub 工具,用于管理多个 Unity 版本和项目。如果你使用 Unity Hub,你可以为你的项目选择“3D”模板,或者选择较新的 VR 渲染管道模板。

Unity 编辑器

你的新项目将在 Unity 编辑器中打开,如下面的截图所示(我在这里自定义了窗口面板布局以方便讨论,并标记了可见的面板):

Unity 编辑器由多个非重叠窗口或面板组成,这些面板可以进一步细分为窗格。以下是前面布局图像中显示的每个面板的简要说明(您的布局可能不同):

  • 在左上角(已突出显示)的场景面板是您可以直观地组合当前场景的 3D 空间的地方,包括对象的放置。

  • 在场景面板下方是游戏视图(左下角),它显示实际的摄像机视图(目前,它是空的,有一个环境天空)。在播放模式下,您的游戏将在该面板中运行。

  • 在中间,我们安排了层次结构、项目和控制台面板(从上到下依次排列)。

  • 层次结构面板提供当前场景中所有对象的树形视图。

  • 项目面板包含项目中所有可重用的资源,包括导入的以及您在过程中创建的。

  • 控制台面板显示 Unity 的消息,包括代码脚本中的警告和错误

  • 在右侧是检查器面板(已突出显示),其中包含当前选中对象的属性。(通过在场景、层次结构或项目面板中单击对象来选择对象)。检查器面板为对象的每个组件提供单独的窗格。

  • 在顶部是主菜单栏(在 Mac 上,这将在屏幕顶部,而不是 Unity 窗口的顶部)。有一个工具栏区域,其中包含我们稍后将要使用的各种控件,包括播放(三角形图标)按钮,该按钮启动播放模式。

从主菜单栏的“窗口”菜单中,您可以根据需要打开额外的面板。编辑器的用户界面是可配置的。每个面板都可以通过抓住其中一个面板标签并拖动它来重新排列、调整大小和分页。试试看!在上右角有一个布局选择器,让您可以选择各种默认布局或保存自己的首选项。

默认世界空间

默认的空 Unity 场景包含一个主摄像机对象和一个单独的方向光对象,如层次结构面板中列出并在场景面板中所示。场景面板还显示一个无限参考地面平面网格的透视,就像一张没有任何东西的图表纸。网格跨越x(红色)和z(蓝色)轴。y轴(绿色)向上。

记住 Gizmo 轴颜色的简单方法是记住 R-G-B 对应于X-Y-Z

检查器面板显示了当前选中项的详细信息。使用鼠标选择方向光,无论是从层次列表中还是场景本身,然后查看检查器面板中与对象关联的每个属性和组件,包括其变换。对象的变换指定了其在 3D 世界空间中的位置、旋转和缩放。例如,位置 (0, 3, 0) 是在地面平面中心(X = 0, Z = 0)上方 3 个单位(在 Y 方向上)。旋转 (50, 330, 0) 意味着它在 x 轴上旋转了 50 度,在 y 轴上旋转了 330 度。正如你将看到的,你可以在这里以数值方式更改对象的变换,或者直接在场景面板中使用鼠标。

类似地,如果你点击主相机,它可能位于 (0, 1, -10) 位置,并且没有旋转。也就是说,它正对着前方,指向正 Z 方向。

当你选择主相机,如前一个编辑器截图所示,在场景面板中添加了一个相机预览嵌入,显示了相机当前看到的视图。(如果游戏标签页是打开的,你也会在那里看到相同的视图)。目前,视图是空的,参考网格没有渲染,但可以辨认出模糊的地平线,下面是灰色地面平面,上面是蓝色的默认环境 Skybox。

创建一个简单的微缩景观

现在,我们将向场景中添加一些对象来设置环境,包括一个单位立方体、一个平面、一个红色球体和一个摄影背景。以下是我们在 VR 中将要构建的微缩景观的实物模型照片:

添加立方体

让我们向场景中添加第一个对象:一个单位大小的立方体。

在层次面板中,使用创建菜单并选择 3D Object | Cube。同样的选择也可以在主菜单栏的 GameObject 下拉菜单中找到。

场景中添加了一个默认的白色立方体,位于地面平面上(0, 0, 0)的位置,没有旋转,缩放为 1,正如你在检查器面板中所见。这是重置设置,可以在检查器面板的对象变换组件中找到。

变换组件的重置值是位置 (0, 0, 0), 旋转 (0, 0, 0), 和缩放 (1, 1, 1)。

如果由于某种原因你的立方体有其他的变换值,请在检查器面板中设置这些值,或者定位检查器面板右上角的的小 齿轮 图标,点击它,并选择重置。

这个立方体的每一边都是一单位。正如我们稍后将会看到的,Unity 中的一个单位对应于世界坐标中的一米。它的局部中心是立方体的中心。

添加平面

现在,让我们向场景中添加一个地面平面对象。

在层次面板中,点击创建菜单(或主 GameObject 菜单)并选择 3D Object | Plane。

场景中添加了一个默认的白色平面,位于地面平面上,位置为(0, 0, 0)。如果需要,可以从检查器面板的变换组件的齿轮图标中选择重置。将其重命名为GroundPlane

注意,在1, 1, 1的比例下,Unity 的平面对象实际上在 X 和 Z 方向上测量 10x10 个单位。换句话说,GroundPlane的大小是 10x10 个单位,其变换的缩放比例是 1。

立方体位于位置(0, 0, 0)的中心,就像地面平面一样。然而,可能在你看来并不如此。场景面板可能显示透视投影,将 3D 场景渲染到 2D 图像上。透视畸变使得立方体看起来没有在地面平面上居中,但实际上它是。数一数立方体两侧的网格线。正如你所看到的,当你在 VR 中查看时,你实际上站在场景中,它看起来根本不会扭曲。以下是一个截图:

图片

立方体被埋在地面平面上,因为它的局部原点位于其几何中心——它的大小为 1x1x1,其中心点为(0.5, 0.5, 0.5)。这听起来可能很显然,但模型的原点可能不是其几何中心(例如其一个角)。变换组件的位置是物体的局部原点在世界空间中的位置。让我们按照以下方式移动立方体:

  1. 将立方体移动到地面平面的表面上——在检查器面板中,将其 Y 位置设置为 0.5:位置(0, 0.5, 0)。

  2. 让我们在y轴周围稍微旋转一下立方体。将其 Y 旋转输入为 20:旋转(0, 0.5, 0)。

注意它旋转的方向。那是顺时针 20 度。用你的左手做一个点赞的手势。看看你的手指指向的方向?Unity 使用的是左手坐标系。(坐标系的手性没有标准。一些软件使用左手性,而另一些则使用右手性)。

Unity 使用的是左手坐标系。而且y轴是向上的。

添加球体和一些材质

接下来,让我们添加一个球体。从菜单中选择 GameObject | 3D Object | Sphere。

与立方体一样,球体的半径为 1.0,其原点位于中心。(如果需要,可以从检查器面板变换组件的齿轮图标中选择重置)。由于它被嵌入在立方体中,很难看到球体。我们需要移动球体的位置。

这次,让我们使用场景面板的Gizmos组件来移动对象。在场景视图中,你可以选择图形控件,或 Gizmos,来操纵对象的变换,如以下 Unity 文档截图所示(docs.unity3d.com/Manual/PositioningGameObjects.html):

图片

在场景面板中,选择球体后,确保翻译工具处于激活状态(位于左上角图标工具栏的第二个图标)并使用xyz轴的箭头来定位它。我将其留在了位置(1.60.75-1.75)。

Gizmo是一种图形控件,允许你操纵对象或视图的参数。Gizmos 有可以点击并拖动的抓点或把手。

在我们进一步操作之前,让我们按照以下方式保存我们的工作:

  1. 从主菜单栏选择文件 | 保存场景为...并将其命名为Diorama

  2. 此外,导航到文件 | 保存项目以备不时之需。注意,在项目面板中,新的场景对象被保存在顶级 Assets 文件夹中。

让我们通过制作一些彩色材质并将它们应用到我们的对象上来给场景添加一些颜色。按照以下步骤操作:

  1. 在项目面板中,选择顶级 Assets 文件夹,然后选择创建 | 文件夹。将文件夹重命名为Materials

  2. 在选择Materials文件夹后,选择创建 | 材质,并将其重命名为Red Material

  3. 在检查器面板中,点击 Albedo 右侧的白色矩形,这将打开颜色面板。选择一个鲜艳的红色。

  4. 重复前面的步骤以创建一个Blue Material

  5. 从层次结构(或场景)面板中选择球体。

  6. Red Material从项目面板拖动到球体的检查器面板中。球体现在应该看起来是红色的。

  7. 从场景(或层次结构)面板中选择立方体。

  8. 这次,将Blue Material从项目面板拖动到场景中,并将其放在立方体上。现在它应该看起来是蓝色的。

保存你的场景和项目。这是我现在的场景样子(你的可能略有不同,但没关系):

注意,我们正在使用项目面板中/Assets/目录下的文件夹来组织我们的内容。

改变场景视图

你可以通过多种方式随时更改场景视图,这取决于你是否有三按钮鼠标、双按钮鼠标,或者只有一个按钮的 Mac。在 Unity 手册中查找相关信息,手册可以在docs.unity3d.com/Manual/SceneViewNavigation.html找到,以了解哪些操作适合你。

通常,左/右鼠标点击与Shift + Ctrl + Alt键的组合可以执行以下操作:

  • 在场景中拖动相机。

  • 围绕当前旋转点旋转相机。

  • 缩放。

  • 按下Alt并右击,可以向上、向下、向左和向右摆动当前眼球的旋转。

  • 工具被选中(在上左图标栏中)时,右鼠标按钮移动眼球。鼠标的中击也做类似的事情。

在场景面板右上角,你有 场景视图 Gizmo,它如以下屏幕截图所示描绘了当前场景视图方向。例如,它可能指示透视视图,其中 x 向左延伸,z 向右延伸:

图片

你可以通过点击以下屏幕截图所示的相应彩色锥体来将视图直接沿任意三个轴之一查看。点击中心的小立方体将透视视图更改为正交(非扭曲)视图:

图片

在继续之前,让我们首先将场景视图与主相机方向对齐。你可能记得我提到默认相机方向(000)是朝向正 z 方向(从后向前)。按照以下步骤操作:

  1. 点击场景视图 Gizmo 上的白色 z 锥体,从背面(背面方向)调整视图,向前看。

  2. 此外,使用手柄工具(或中间鼠标按钮)稍微向上滑动视图。

现在,当你选择主相机组件(从层次面板中),你会看到场景视图大致与相机预览相似,朝向相同方向。(参见下一节中显示的屏幕截图,了解添加照片后此视图方向下的场景和预览看起来如何。)

要查看 Unity 快捷键的完整列表,请参阅 docs.unity3d.com/Manual/UnityHotkeys.html

添加照片

现在,让我们为我们的展品大屏幕背景添加一张照片。

在计算机图形学中,映射到对象上的图像称为 纹理。虽然对象以 x、y 和 z 世界坐标表示,但纹理被认为是在 U、V 坐标(如像素)中。我们将看到纹理和 UV 映射可能有自己的缩放问题。按照以下步骤操作:

  1. 通过导航到 GameObject | 3D Object | Plane 创建一个平面,并将其重命名为 PhotoPlane

  2. 重置平面的变换。在检查器面板中,找到变换面板右上角的 齿轮 图标。点击此图标并选择重置。

  3. 接下来,围绕 z 轴旋转 90 度(将其变换组件的旋转值 z 设置为 -90)。那就是 负 90 度。所以,它现在是垂直于地面的站立状态。

  4. 围绕 y 轴旋转 90 度,使其正面朝向我们。

  5. 将其移动到地面平面的末端,位置值为 z = 5 以上,位置值为 y = 5(你可能记得地面平面是 10 x 10 单位)。

  6. 从你的电脑中选择任何照片,使用 Windows 资源管理器或 Mac Finder 粘贴到这张照片平面上。(或者,你可以使用本书附带的自带的 GrandCanyon.jpg 图像)。

  7. 在项目面板中,选择顶级资产文件夹,导航到创建 | 文件夹。将文件夹重命名为 Textures

  8. 将照片文件拖入Assets/Textures文件夹。它应该会自动导入为纹理对象。或者,你可以在资产文件夹上右键单击,选择“导入新资产...”,然后导入图片。

在项目面板中选择新的图像纹理,并在检查器面板中查看其设置。对于 Unity 的渲染目的,即使原始照片是矩形的,纹理现在是正方形(例如,2048 x 2048)并且看起来有点挤压。当你将其映射到正方形面上时,它也会在那里挤压。让我们执行以下步骤:

  1. 将照片纹理从项目面板拖动到照片平面(在场景面板中)。

哎呀!在我的情况下,图片是侧着旋转的——你的也是吗?

  1. 选择PhotoPlane(照片平面)并设置变换组件的 X 旋转值为90度。

好了,它现在是垂直的,但仍然有点挤压。让我们来修复这个问题。检查你照片的原始分辨率并确定其纵横比。我的Grand Canyon图像是 2576 x 1932。当你将其宽度除以高度时,你会得到 0.75 的比率。

  1. 在 Unity 中,将PhotoPlane平面的变换组件的 Z 缩放值设置为0.75

因为它的缩放原点是中心,我们还得把它向下移动一点。

  1. 将 y 的位置值设置为3.75

为什么是 3.75?高度从 10 开始。所以,我们将其缩放到 7.5。对象的缩放是相对于它们的原点。所以现在,高度的一半是 3.75。我们想要将背景的中心定位在地面平面上方 3.5 个单位处。

我们已经设置了大小和位置,但照片看起来有点褪色。这是因为场景中的环境光照影响了它。你可能想保持这种状态,尤其是在你构建更复杂的照明模型和材质时。但现在,我们将取消照明。

在选择PhotoPlane后,注意在检查器面板中,照片的纹理组件的默认着色器组件设置为标准。将其更改为 Unlit | Texture。

这是我看起来像的;你的应该类似:

图片

好了!看起来相当不错。保存你的场景和项目。

你可能会注意到,飞机只能从前方看到。计算机图形中的所有表面都有一个面向前方的方向(法向量)。视图相机必须朝向前方,否则对象将不会被渲染。这是一个性能优化。如果你需要一个平面两面都有面,可以使用一个薄薄的立方体,或者两个相互远离的单独平面。

注意,如果你现在检查你的材质文件夹,你会发现在 Unity 已经为你自动创建了一个名为GrandCanyon.mat的材质,它使用了GrandCanyon.jpg纹理。

着色地面平面

如果你想要更改地面平面的颜色,创建一个新的材质(在项目面板中),命名为Ground,并将其拖动到地面平面上。然后,更改其 Albedo 颜色。我建议使用滴管(图标)从你的照片平面的图像中选择一种土色调。

测量工具

我们已经创建了一个 Unity 场景,添加了一些原始的 3D 对象,并创建了一些基本的纹理,包括一张照片。在这个过程中,我们学习了如何在 Unity 的 3D 世界空间中定位和变换对象。问题是场景中实际物体的尺寸并不总是显而易见。你可能已经放大了视图,或者你可能正在使用透视或正交视图,或者其他影响可见尺寸的功能。让我们看看如何处理比例问题。

保持单位立方体在手边

我建议在层次面板中保持一个单位立方体在手边。当它不再需要时,只需禁用它(在检查器面板的左上角取消勾选复选框)。当需要时,它可以像尺子一样使用,或者更确切地说,像测量块一样使用。我使用它来估计物体的实际世界尺寸、物体之间的距离、高度和海拔等等。现在让我们来做这件事。

创建一个单位立方体,命名为Unit Cube,并将其放置在某个不易见到的位置,例如位置(-2, 0.5, -2)。

目前先保持启用状态。

使用网格投影仪

我想向大家介绍网格投影仪,这是一个方便的工具,用于在任意 Unity 场景中可视化比例。它是效果包中的标准资源之一。因此,你可能需要将其导入到你的项目中。导入时,请执行以下步骤:

  1. 在主菜单栏中选择“资产”,然后导航到“导入包”|“效果”。

  2. 导入对话框弹出,包含可以导入的所有内容的列表。然后选择“导入”。

如果你找不到导入的Effects包,那么在你安装 Unity 时可能没有安装Standard Assets。要获取它们,你需要再次运行UnityDownloadAssistant,如本章开头所述(它可能已经在你下载文件夹中)。

现在,我们将按照以下步骤将投影仪添加到场景中:

  1. 通过导航到Assets/Standard Assets/Effects/Projectors/Prefabs文件夹,在项目面板中找到网格投影仪预制体。

  2. 将网格投影仪的一个副本拖动到你的场景中。将位置y值设置为5,使其位于地面平面之上。

默认的网格投影仪朝下(x 旋转值为 90),这通常是我们的需求。在场景视图中,你可以看到正交投影的射线。Unity 文档(docs.unity3d.com/Manual/class-Projector.html)对投影仪的描述如下:

投影仪允许你将材质投影到与其视锥体相交的所有对象上。

这意味着被投影射线相交的物体将接收到投影的材质。

在这种情况下,正如你所期望的,投影器材质(也称为 GridProjector)有一个 网格 纹理,看起来就像一个十字准星。(你自己看看,在 Assets/.../Projectors/Textures/Grid 对象中)。

默认情况下,投影器将网格图案作为光投射到它照亮的表面上。在我们的场景中,GroundPlane 平面是浅色。因此,网格可能不会显示出来。现在,按照以下步骤操作:

在层次结构面板中选择 Grid Projector,在检查器面板中找到 GridProjector 材质组件,并将其着色器从投影器/光更改为投影器/乘法。

现在它将白色网格线画成黑色。为了更好地了解正在发生的事情,将场景视图更改为俯视图方向,如下所示:

  1. 点击视图面板右上角的场景视图 Gizmo 上的绿色 y 球锥。

  2. 此外,点击 Gizmo 中心的立方体,从透视视图更改为正交(平坦)视图。

你现在应该正对着地面平面直视。选择 Grid Projector(确保翻译工具是激活的,这是左上角图标工具栏中的第二个图标),你可以抓住附加到投影器的平移 Gizmo 并将其从一侧移动到另一侧。网格线会相应地移动。你可能将其留在位置(-2.55-0.5)并避免投影器 Gizmo 阻挡方向光。

到目前为止,内置视图参考网格可能有些令人困惑。因此,按照以下方式将其关闭:

  1. 在场景视图面板中,点击 Gizmos(这个名称的菜单,其中包含控制你的 Gizmos 的选项)并取消勾选显示网格。

好的,这能给我们带来什么?我们可以看到默认的网格大小是单位立方体边长的一半。在检查器中,投影器组件的正交大小值是 0.25

  1. 将投影器的正交大小值从 0.25 更改为 0.5

  2. 保存场景和项目。

现在我们有一个单位网格,可以在需要时随时打开并投射到场景中。

现在先让它保持,因为它看起来有点酷,如下面的截图所示:

图片

测量 Ethan 角色尺寸

一个化身有多大?Unity 随附一个名为 Ethan 的第三人称角色。让我们将其添加到我们的场景中。他是 Characters 包中的 Standard Assets 之一。因此,你可能需要将其导入到你的项目中。

要导入,请执行以下步骤:

  1. 在主菜单栏中选择 Assets,然后导航到导入包 | 角色。

  2. 导入对话框弹出,包含可以导入的所有内容的列表。点击全部,然后导入。ThirdPersonController 是位于项目面板中的预制件(预构建资产)。可以通过导航到 Assets/Standard Assets/Characters/ThirdPersonCharacter/Prefabs 文件夹找到它。

  3. ThirdPersonController的副本拖入你的场景中。确切的 x 和 z 位置不重要,但将 y 设置为0,以便名为Ethan的角色站在GroundPlane上。我的是在(2.2, 0, 0.75)。

让我们试试看:

  1. 点击 Unity 窗口中央顶部的播放图标,开始你的游戏。使用W, A, S, 和 D 键移动他。跑,Ethan!跑!

  2. 再次点击播放图标停止游戏并返回编辑模式。

那么,Ethan 有多高呢?根据谷歌搜索,人类男性的平均身高是 5 英尺 6 英寸,或 1.68 米(在美国,成年男性的平均身高更像是 5 英尺 10 英寸或 1.77 米)。让我们看看 Ethan 与这些数值相比有多高:

  • 使用平移工具将单位立方体移至 Ethan 旁边

好的,他大约是立方体高度的 1.6 倍

  • 将单位立方体的高度(y)缩放至 1.6,并将 y 位置居中至 0.8

再看一眼。如图所示,他并不完全是 1.6 倍。所以,Ethan 比平均男性略矮(除非你包括他的尖头发型)。我调整视角,正对着 Ethan 的脸,通过进一步调整立方体,眼睛水平大约是 1.4 米。记下这个:

  1. 恢复单位立方体的缩放(1,1,1)和位置(-2, 0.5, -2

  2. 保存场景和项目

以下截图显示了单位立方体和 Ethan 的比较:

使用第三方内容

到目前为止,我们已经向你展示了如何使用 Unity 创建场景并提高效率,但内容相当简单。本质上,Unity 不是一个 3D 建模或资产创建工具。相反(正如名称Unity所暗示的),它是一个统一平台,用于从各种来源收集内容,以组装和编程涉及动画、物理、渲染效果等的游戏或体验。如果你是 3D 艺术家,你可能知道如何在 Blender、3D Studio Max 或 Maya 等程序中创建内容。如果不是,你可以在网上找到大量的模型。

一个极好的来源是Unity Asset Store(www.assetstore.unity3d.com/en/)。许多资产包是免费的,特别是入门级套件,如果你想要更多,可能需要付费升级。如果你正在寻找一些东西来启动你的学习和实验项目,以下是我的一些免费精选:

除了 3D 模型之外,资产商店还包含了一个令人惊叹的开发工具、插件、音频等的混合体。资产商店、其活跃的开发者社区以及其庞大的内容量是 Unity 成功的原因之一。

资产商店可以直接在 Unity 编辑器中访问。要访问它,选择窗口 | 资产商店并开始探索。

要使用资产商店将资产添加到你的项目中,例如,只需找到它并选择下载,然后选择导入以将其添加到你的项目资产文件夹。资产包通常包含你可以打开以探索其外观和功能的示例场景。之后,找到其预制体文件夹,只需将任何预制体拖入你自己的场景即可。这里有一个示例:

此外,还有许多用于分享 3D 模型的网站,既有免费的也有付费的。有些针对工程师的高端 3D CAD。其他则迎合 3D 打印爱好者。无论哪种情况,请确保寻找模型的 FBX 或 OBJ 文件格式,以便它们可以被导入到 Unity 中。一些更受欢迎的资源网站包括:

我们将在本章后面使用 Google Poly

使用 Blender 创建 3D 内容

Unity 提供了一些基本的几何形状,但当我们谈到更复杂的模型时,你需要超越 Unity。正如我们讨论的那样,Unity 资产商店和许多网站上有大量的惊人模型。它们从哪里来?你在导入到 Unity 时会遇到问题吗?

我知道这本书是关于 Unity 的,但现在我们将进行一次短暂的侧翼冒险。我们将使用 Blender(版本 2.7x),这是一个免费的开源 3D 动画套件(www.blender.org/),来制作一个模型,然后将其导入到 Unity 中。来杯咖啡,系好安全带吧!

目前计划不是构建任何非常复杂的东西。我们只是制作一个立方体和一个简单的纹理贴图。这个练习的目的是了解在 Blender 中,一个单位立方体以相同的比例和方向导入 Unity 的效果如何。

随意跳过这一节,或者尝试使用你喜欢的建模软件进行类似的实验(en.wikipedia.org/wiki/List_of_3D_modeling_software)。如果你不想跟随或遇到问题,本主题中创建的完成文件的副本可在本书的下载包中找到。

Blender 简介

打开 Blender 应用程序。关闭启动屏幕。你将进入 Blender 编辑器,它类似于以下截图所示:

图片

与 Unity 类似,Blender 由多个非重叠窗口组成,它们的布局可以根据你的需求进行自定义。然而,Blender 界面可能更令人畏惧,部分原因是因为它集成了多个可以同时打开的不同编辑器,它们各自有自己的面板。

有助于意识到,默认视图,如前述截图所示,包含五个不同的编辑器!

最明显的编辑器是大的3D 视图,我用一个(红色)矩形标出了它。这里你可以查看、移动和组织 Blender 场景中的对象。

以下是要打开的其他编辑器:

  • 信息编辑器,位于应用程序顶部边缘,包含全局菜单和应用程序信息。

  • 时间轴编辑器,位于应用程序底部边缘,用于动画。

  • 在右上角的场景大纲编辑器,有一个对象的分层视图。

  • 属性编辑器,位于大纲视图右侧下方,是一个强大的面板,让你可以看到并修改场景中对象的许多属性。

每个编辑器可以有多个面板。让我们考虑 3D 视图编辑器:

  • 中间的大区域是3D 视图窗口,在这里你可以查看、移动和组织 Blender 场景中的对象。

  • 在 3D 视图窗口下方是编辑器标题栏,尽管它在这个情况下位于底部,但被称为标题栏。标题栏是一行菜单和工具,提供了对编辑器的强大控制,包括视图选择器、编辑模式、变换操纵器和图层管理。

  • 在左侧是工具架,包含可以应用于当前选中对象的多种编辑工具,这些工具可以组织成标签组。可以通过抓住并滑动其边缘或按T键来切换工具架的开启或关闭。

  • 3D 视图窗口还有一个属性面板,默认可能隐藏,可以通过按N键切换开启或关闭。它提供了当前选中对象的属性设置。

在接下来的说明中,我们将要求你更改 3D 视图编辑器的交互模式,例如在编辑模式和纹理绘制模式之间切换。这可以在标题栏中找到,如下面的截图所示:

图片

其他编辑器也有标题栏。信息编辑器(在应用顶部)只是一个标题栏!大纲和属性编辑器(在右侧)的标题栏位于面板顶部而不是底部。

一旦您熟悉了这个布局,它看起来就不会那么拥挤和混乱。

属性编辑器标题栏有一系列图标,它们的作用类似于标签,用于选择面板中显示的属性组。将鼠标悬停在图标上(就像这里的任何 UI 小部件一样)将显示一个工具提示,提供有关其用途的更好提示。以下图像(在几页中)展示了我们使用它时的样子。

Blender 布局非常灵活。您甚至可以更改一个面板从一个编辑器到另一个编辑器。每个标题栏的最左侧是编辑器类型选择器。当您点击它时,您可以看到所有选项。

除了在 Blender 界面中可以点击的众多事物外,您还可以使用几乎任何命令的快捷键。如果您忘记了查找选择的位置,请按空格键并输入您对要查找的命令名称的最佳猜测。它可能会突然出现!

下面的截图显示了 Blender 中可用的编辑器类型选择器:

一个单位立方体

现在,让我们在 Blender 中构建一个单位立方体。

默认场景可能已经包含对象,包括一个立方体、相机和光源,如前面在默认 Blender 窗口中所示。(由于可以配置,您的启动设置可能不同。)

如果你的启动场景不包含单位立方体,请按照以下步骤创建一个:

  1. 确保场景为空,通过删除其中的所有内容(右键选择,按键盘上的X键删除)。

  2. 使用Shift + S(打开捕捉选项列表)| 光标到中心将 3D 光标设置到原点(000)。

  3. 在左侧工具栏面板中,选择创建选项卡,在网格下选择立方体以添加一个立方体。

好的,现在我们都在同一页面上。

注意,在 Blender 中,参考网格沿xy轴延伸,而z轴向上(与 Unity 不同,在 Unity 中y轴向上)。

此外,请注意,Blender 中的默认立方体大小为(222)。我们想要一个单位立方体位于原点处的地面平面上。为此,请按照以下步骤操作:

  1. 使用键盘N键打开属性面板

  2. 导航到变换 | 缩放并将 X、Y、Z 设置为(0.50.50.5

  3. 导航到变换 | 位置并将 Z 设置为0.5

  4. 再次按N键隐藏面板

  5. 您可以使用鼠标滚轮进行缩放

对于我们的目的,还要确保当前渲染器是 Blender 渲染(在信息编辑器的下拉选择器中——在应用窗口顶部的中央)。

UV 纹理图像

让我们为我们的立方体上色。在 Unity 中的 3D 计算机模型由网格定义——一组通过边连接的 Vector3 点,形成三角形面的集合。当在 Blender 中构建模型时,你可以将网格展开成平面的 2D 配置,以定义纹理像素到网格表面的相应区域的映射(UV 坐标)。结果是称为 UV 纹理图像。

我们将为我们的立方体创建一个UV 纹理图像,如下所示:

  1. 使用底部标题栏中的交互模式选择器进入编辑模式。

  2. 选择所有(在键盘上按两次A键)以确保所有面都被选中。

  3. 在左侧工具架面板中,选择着色/UV 选项卡。

  4. 在 UV 映射下,点击展开,从下拉列表中选择智能 UV 投影,接受默认值,然后点击确定(以下截图所示的结果也显示了展开的立方体外观)。

  5. 现在,再次使用底部标题栏中的交互模式选择器进入纹理绘制模式。

  6. 我们需要为我们的材质定义一个绘制槽。点击添加绘制槽,选择漫反射颜色,命名为CubeFaces,然后按确定。

我们现在可以直接在立方体上绘制。首先绘制前面,如下所示:

  1. 使用更小的画笔。在左侧工具架面板中,在工具选项卡下,导航到画笔 | 半径并输入8 px

  2. 在正交视图中工作可能更容易。从底部菜单栏中,导航到视图 | 视图透视/正交。

  3. 然后,导航到视图 | 前视图。

  4. 如果需要,可以使用鼠标滚轮进行缩放或缩小。

  5. 用你最好的书法,用鼠标左键点击并写下单词Front

  6. 现在,背面。

  7. 从底部菜单栏中,导航到视图 | 后视图,并右键单击选择此面。

  8. 用你最好的书法,用鼠标左键点击并写下Back

对左、右、上、下面重复上述过程。如果在某个时刻无法绘制,请确保已选择当前面。尝试右键单击面以重新选择它。结果应该看起来像这样(在正交视角的 3D 视图编辑器和 UV/图像编辑器中并排显示):

现在,我们需要保存纹理图像并设置其属性,如下所示:

  1. 使用 3D 视图编辑器底部标题栏最左侧的选择器将当前编辑类型更改为 UV/图像编辑器。

  2. 点击浏览要链接的图像选择器图标(位于+图标左侧),从列表中选择CubeFaces

  3. 底部菜单栏上的图像菜单项现在有一个带星号的(图像*),表示有一个未保存的图像。点击它,选择另存为图像,并将其保存为CubeFaces.png。使用 Unity 项目外的文件夹。

  4. 在右侧的属性编辑器面板中,在其标题中找到长排图标并选择纹理图标(倒数第三个)。如果面板不够宽,它可能被隐藏;您可以用鼠标向下滚动以显示它,如下面的截图所示:

  5. 在纹理属性中,将类型更改为图像或电影。

  6. 然后,在属性中的图像组中,点击浏览要链接的图像选择器图标(如下面的截图所示)并选择CubeFaces

  7. 您应该在预览窗口中看到标记的面纹理图像。

好的!让我们按照以下方式保存 Blender 模型:

  1. 在信息编辑器顶部主菜单栏中选择文件,然后点击保存(或按Ctrl + S)。

  2. 使用与保存纹理图像相同的文件夹。

  3. 将其命名为UprightCube.blend并点击保存 Blender 文件。

现在我们应该有一个文件夹中的两个文件,UprightCube.blendCubeFaces.png。我在 Unity 项目的根目录下使用名为Models/的文件夹。

我们建议您然后以 FBX 格式导出模型。这是 Unity 的标准格式。(Unity 可以导入 Blend 文件,但可能需要在同一系统上安装 Blender)。使用文件 | 导出 | FBX来保存.fbx版本。

哇,这太多了。如果您没有全部理解,不要担心。Blender 可能有点令人畏惧。然而,Unity 需要模型。您可以从 Unity 资产商店和其他 3D 模型共享网站下载他人的模型,或者您可以学习制作自己的模型。哈哈!说真的,开始学习是个好主意。实际上,随着 VR 的发展,它已经变得容易多了,我们将在本章后面向您展示。

导入到 Unity 中

在 Unity 中,我们现在想逐个导入两个文件,UprightCube.fbxCubeFaces.png,如下所示:

  1. 在项目面板中,选择顶级 Assets 文件夹,导航到创建 | 文件夹,并将文件夹重命名为Models

  2. 将文件导入 Unity 的一个简单方法是将.fbx(或.blend)文件从 Windows 资源管理器(或 Mac Finder)窗口拖放到项目面板 Assets/Models 文件夹中,并将.png文件拖放到 Assets/Textures 文件夹中(或者您也可以从主菜单栏选择 Assets | Import New Asset...)。

  3. UprightCube从刚刚导入到 Assets/Models 文件夹中拖动到场景视图中。

  4. 设置其位置,使其远离其他对象。我将其设置为位置(2.62.2-3)。

  5. CubeFaces纹理从 Assets/Textures 文件夹拖动到场景视图中,悬停在刚刚添加的UprightCube上,使其接收纹理,并将纹理拖放到立方体上。

场景现在应该看起来像这样:

一些观察结果

立方体的背面朝向我们。这是错误吗?实际上,由于当前视角是向前看的,所以这是有道理的。因此,我们应该看到立方体的背面。如果你还没有注意到,对 Ethan 来说也是同样的情况。立方体似乎也有一个单位的维度。

然而,在仔细检查后,在立方体的检查器面板中,你会看到它以我们在 Blender 中给出的比例导入(0.50.50.5)。它还有一个-90 度的 X 旋转(负 90 度)。因此,如果我们重置变换,即比例到(111),它将在我们的世界空间中是 2 个单位,并且倾斜(所以不要重置它)。

在不回到 Blender 的情况下,我们无法做太多来补偿旋转调整。但在模型的导入设置(在检查器中)中可以调整比例。

Blender 的默认向上方向是 Z,而 Unity 的是 Y。导入时使用-90 度的 X 旋转来调整这一点。导入的比例可以在对象的检查器面板的导入设置中调整。

在从 Blender 导出 FBX 时,我们有更多的控制权。如图所示,在导出过程中,你可以自定义设置,例如,将 Y 设置为向上轴,Z 设置为向前轴,并设置导入的比例因子:

图片

在结束前面的过程之前,从层次结构面板中选择UprightCube并将其拖入项目面板的资产文件夹中。(你可能考虑创建一个 Assets/Prefabs 子文件夹并将其放入其中。)这使得它成为一个可重用的预制件,包括纹理图像。

在这个练习中(除了学习了一点关于 Blender 的知识之外)还有一些重要的经验教训,这些经验教训适用于任何 3D Unity 项目,包括 VR 项目。通常,你将导入比立方体复杂得多的模型。你可能会遇到与数据转换、比例、方向和可能令人困惑的 UV 纹理图像相关的问题。如果发生这种情况,尝试将问题分解成更小、更独立的场景。进行一些小测试,以了解应用程序如何交换数据,并帮助你理解哪些参数调整可能是必要的。

在 VR 中创建 3D 内容

除了像 Blender(以及 ZBrush、3D Studio Max、Maya 等等)这样的传统 3D 建模软件之外,还有新一代的 3D 设计应用程序,允许你直接在 VR 中创建内容。毕竟,用本质上 2D 的桌面屏幕和 2D 鼠标来形成、雕刻、组装和操作 3D 模型是很尴尬的。如果它能够更像现实生活中的雕塑和建筑会怎样?所以,为什么不直接在 3D 中做呢?在 VR 中!

就像其他数字平台一样,我们可以将 VR 应用分为三类:一类是提供体验的应用,一类是让你与环境互动以参与其中的应用,还有一类是实际创建内容的应用,无论是为自己还是为了分享。后者是一个成功的例子,也是最早广泛成功的一个,那就是谷歌倾斜画笔(www.tiltbrush.com/),在那里你可以进行 3D 绘画。这是我在向家人和朋友介绍 VR 时最喜欢使用的应用之一。倾斜画笔让你能够在虚拟现实中进行 3D 绘画。

其他具有雕塑和绘画工具的 VR 3D 应用,仅举几个例子,包括:

在 VR 中制作和玩耍是创造性和有趣的,但要变得有用和高效,你需要能够将你的作品分享到应用程序之外。大多数 VR 雕塑工具允许你将模型导出以在互联网上分享,例如导出为 FBX 文件格式,并将它们导入到 Unity 中。有两种不同的工作流程来完成这项任务:

  • 导出/导入:在第一个工作流程中,你创建一个模型并将其导出为兼容格式,如 FBX。这类似于我们用 Blender 使用的传统 3D 软件。

  • 发布/导入:第二种工作流程是将它上传到共享服务,然后下载并安装到你的 Unity 项目中。

在本节中,我们将以倾斜画笔为例。假设你拥有谷歌倾斜画笔和兼容的 VR 设备。进一步假设你有一个想要与 Unity VR 应用集成的作品。让我们逐一了解每个工作流程过程。

我在 VR 中打开了倾斜画笔,并使用纸带笔刷创作了一幅杰作。我称之为TiltBox,与本章中使用的立方体主题一致。我知道,它很美。

这里(在撰写本文时)展示的倾斜画笔功能和用户界面,谷歌认为它们处于测试或实验阶段,并在你阅读本文时可能发生变化。

导出和导入倾斜画笔模型

我们将导出我们的模型为 FBX,然后将其导入到 Unity 中。这是一个高级主题,如果您是 Unity 新手,您可能现在想跳过这个主题,转而查看 使用 Google Poly 发布和导入 部分。

在 Tilt Brush 中,要导出,请转到“保存”面板并选择“更多选项… | 实验室 | 导出”菜单。(注意,导出选项的位置可能在未来的版本中发生变化。)

在 Windows 中,您的文件默认保存在 Documents/Tilt Brush/Exports/[DrawingName]/ 文件夹中。如果您将右手控制器旋转一下,您会发现后面有一个信息面板,这是一个消息控制台,它会报告您系统上绘图的实际路径名,如图所示:

图片

该文件夹将包含多个文件,包括模型的 .fbx 文件和笔刷纹理的 .png 文件(未使用,因为 Tilt Brush Toolkit 也提供了它们)。

要导入到 Unity,您需要 Tilt Brush Toolkit Unity 包。Google Poly 包包含工具包(如下一主题所述,从资产商店安装)。或者您可以直接从 GitHub 安装,如下所示:

  1. 前往 github.com/googlevr/tilt-brush-toolkit 并使用 tiltbrush-UnitySDK-vNN.N.N.unitypackage 的下载链接(通过 github.com/googlevr/tilt-brush-toolkit/releases

  2. 使用“资产 | 导入包 | 自定义包…”在 Unity 中导入工具包,然后按导入。

您会发现工具包包括用于渲染笔刷的资产。

在“导出”文件夹中还有一个 README 文件,其中包含有关您的 Tilt Brush 版本和导出功能的信息,包括如何使用 CFG 文件为高级用户调整各种选项。

现在我们可以导入绘制的 FBX:

  1. 将 FBX 文件拖放到您的项目资产中(或使用“资产 | 导入新资产…”)。

  2. 忽略由导入创建的任何材质;我们将使用工具包中提供的材质。您可以在模型的导入设置中禁用此功能,在“材质 | 导入 材质”处取消选中,然后点击应用。

  3. 您现在可以将模型拖放到场景中。

  4. Assets/TiltBrush/Assets/Brushes/ 中找到您草图使用的笔刷材质。在我们的例子中,草图使用的是纸笔刷笔触,位于 Basic/Paper/ 子文件夹中。

  5. 根据需要将材质拖放到您的草图笔触上。

您的场景现在包含您的 Tilt Brush 草图。有关更多高级功能,包括音频响应功能、动画和 VR 传送,请参阅 Tilt Brush 文档和示例场景。

虽然有点繁琐,但这并不太难。其他 3D 建模应用程序也需要类似的过程来导出模型并将其导入到 Unity 中。

使用 Google Poly 发布和导入

幸运的是,Google 通过引入 Google Poly (poly.google.com/)作为一个发布、浏览和下载使用 Google Tilt Brush 和 Google Blocks(以及其他创建带有材质的 OBJ 文件的 APP)创建的免费 3D 对象的地方,使事情变得容易得多。

我并不是要听起来像是一个 Google 粉丝,但让我们继续 Tilt Brush 的主题。在 Tilt Brush 中,通过点击按钮很容易将您的草图发布到 Google Poly。并且使用资产商店中可用的 Poly Toolkit Unity 包,将 Poly 模型导入 Unity 同样简单。让我们来了解一下:

Poly 不仅仅是为 Unity 开发者准备的。Google 为多个平台提供了 SDK 以进行访问。请参阅developers.google.com/poly/

  1. 在 Tilt Brush 中,首先,确保您已登录到您的 Google 账户(我的资料)。

  2. 在“保存”菜单面板上,选择如图所示的云上传选项。这将把您的草图上传到 Poly。

  3. 然后在浏览器(非 VR)中完成发布步骤,并按“发布”。

注意,Poly Toolkit 包括 Tilt Brush Toolkit。如果您已经在上一节将 Tilt Brush toolkit 导入到您的项目中,我们建议您在导入 Poly 之前先删除它(以及第三方文件夹),以避免冲突。

在 Unity 中:

  1. 打开资产商店面板(窗口 | 资产商店)。

  2. 搜索Poly Toolkit,然后下载并将 Poly Toolkit 资产包导入到您的项目中(assetstore.unity.com/packages/templates/systems/poly-toolkit-104464)。

  3. 注意,工具包在 Unity 菜单栏中安装了一个新的 Poly 菜单。选择 Poly | 浏览资产…以打开 Poly 浏览器面板,如图所示:

图片

  1. 您可以通过拖动其标签将其面板停靠在 Unity 编辑器中。

  2. 在您浏览自己的上传内容之前,您必须使用右上角的“登录”按钮进行登录。

  3. 然后,在 Poly Toolkit 面板的“显示选择”中,选择“您的上传”。

  4. 定位您希望导入的模型。其页面包括多个导入选项,包括缩放和重新定位模型变换。

  5. 选择“导入到项目”。

  6. 默认情况下,它将模型导入到Project Assets/Poly/Assets/文件夹中作为一个预制件。

  7. 将模型的预制件从文件夹拖到您的场景中。

就这样。现在您手边就有了一个 3D 模型的世界:您在 Poly 上创建并发布的模型,以及您在 Poly、Unity 资产商店或您能探索的众多其他 3D 模型网站上发现的模型。

使用 EditorXR 在 VR 中编辑 Unity

在本章中,我们学习了 Unity 编辑器——一个在 2D 计算机屏幕上创建 3D 场景和项目的工具。我们还了解了一些关于 Blender 的知识——一个在 2D 计算机屏幕上创建 3D 资源的工具。然后我们进入了虚拟现实中的新一代 3D 资源创建工具,包括 Tilt Brush 和 Poly。现在我们还将探索如何在虚拟现实中直接创建 VR 场景!

Unity 编辑器 XREXR)是 Unity(截至撰写时)的一个新实验性功能,您可以在 VR 中直接编辑您的 3D 场景,而不是在您的 2D 监视器上。在本节中,我们可能在多个方面都走在了前面。这是一个高级话题,也是一个实验性话题。如果您刚开始接触 VR,或者刚开始使用 Unity,您可能现在想跳过这个话题,稍后再回来。

EXR 是一个高级话题,因为它假设您熟悉使用 Unity 编辑器窗口,习惯于 3D 思维,并且在处理 3D 资源方面有一些经验。它还假设您有一个配备跟踪手控器的 VR 设备,如 Oculus Rift 和 HTC Vive。如果您希望获得流畅、舒适的体验,您需要一个配备高端显卡的强大 PC。最后但同样重要的是,EXR 的一些用户交互约定需要学习和适应。

尽管如此,EXR 是一个相当不错的项目,您可以从今天开始使用以提高生产力。特别是如果您不害怕使用实验性软件。这也意味着我们在这本书中描述的 UI 一定会发生变化。(例如,在这个时候,该包正在从 EditorVR 重命名为 EditorXR 和 EXR)。当前的信息链接包括:

另一个原因是在本书的早期就讨论 EXR 是一个高级话题,是因为我们需要在我们的项目中启用 VR,这是一个我们直到下一章才涉及的话题。但我们会快速带您了解,而不进行过多解释。

设置 EditorXR

要开始在项目中使用 EXR,请下载并安装 Unity 包。到您阅读此内容时,它可能已经包含在 Unity 下载助手或资产商店中:

  1. 下载 EditorXR Unity 包(github.com/Unity-Technologies/EditorXR/releases)。

  2. 将其导入到你的项目中(资源 | 导入包 | 自定义包…)。

  3. 如果你使用的是 Unity 2018 之前的版本,请从资源商店下载并导入 Text Mesh Pro(assetstore.unity.com/packages/essentials/beta-projects/textmesh-pro-84126),这是 Unity Technologies 提供的一个免费资源。

  4. 如果你使用的是 VIVE,请从资源商店下载并导入 SteamVR 插件(www.assetstore.unity3d.com/en/#!/content/32647)。

  5. 如果你使用的是带有触摸控制器的 Oculus Rift,请下载并导入 Oculus Utilities for Unity(developer3.oculus.com/downloads/)。

  6. 在玩家设置中设置你的默认 VR 平台(编辑 | 项目设置 | 玩家)。找到 XR 设置部分(检查器面板底部),并勾选虚拟现实支持复选框。

  7. 添加 Oculus 和/或 OpenVR 的虚拟现实 SDK。

  8. 如果你使用带有触摸控制器的 Oculus Rift,请确保 Oculus 的版本排在第一位,如图所示:

当你准备好进入 EXR 时:

  1. 选择“Windows”|“EditorXR”

  2. 如有必要,按“切换设备视图”以使 VR 视图生效

  3. 然后戴上你的头戴式设备

现在,你可以在 VR 中访问在 Unity 编辑器中找到的许多相同的编辑功能。

使用 EditorXR

EXR 中的用户交互类似于 Google Tilt Brush。一只手握住你的菜单调色板,另一只手从中选择功能。就像一只方形的手套,你可以通过拇指的轻扫来切换菜单,旋转菜单框面。这是起点,但 EXR 更加复杂,因为它需要在你的虚拟工作空间中提供丰富的 Unity 编辑器功能,同时还需要导航场景、组织编辑器面板,当然还有编辑场景游戏对象。我们鼓励你在深入之前先观看一些演示视频。

手控制器选择器实现了激光指针选择远程对象和抓取(通过选择锥体)的创新、同时组合,如图所示:

要操纵对象,EXR 在 2D 编辑器中实现了熟悉的场景编辑器小部件的强大 3D 版本。它们真的非常强大且易于使用。

不深入细节,以下是 EXR 编辑器中的关键功能:

  • 选择:手控制器拇指垫/杆、按钮、扳机和抓取的稳健使用

  • 菜单:方形容器菜单面板、径向菜单、快捷键以及用于在 3D 中组织面板的工具

  • 导航:在工作过程中在场景中移动,飞行和眨眼模式,原地旋转,缩放世界,使用迷你世界视图

  • 工作空间:对应于 2D 编辑器中的窗口,如项目、层次结构、检查器、控制台、配置文件等,可以在 VR 工作区中打开和放置。

  • 其他功能包括锁定对象、吸附等。

下图显示了如何使用操纵器 Gizmo 与控制器上的径向菜单一起直接操作当前选定的对象,以切换工具:

图片

在 EXR 中学习更具挑战性的事情之一可能是了解每个手控在切换意义时在当前上下文中的具体作用。以下图显示了 VIVE 的控制器指南:

图片

下面展示了 Oculus Touch 控制器的操作指南:

图片

为了总结这个话题,你甚至可以使用 Google Poly 查找对象并将它们插入到你的 VR 场景中。这是 EditorXR 接口和 API 的第三方扩展 Poly 工作空间的示例,它可在 VR 中使用。如果你已经安装了 Poly Toolkit(如前所述)并且正在使用 EditorXR,那么 Poly 就是可用的工作空间之一。打开它来浏览并将云中的 3D 模型添加到你的场景中,如图所示:

图片

要了解更多关于 EditorXR 和 Google Poly 的信息,请参阅 Unity Labs 的 Matt Schoen 的这篇首篇博客文章:blogs.unity3d.com/2017/11/30/learn-how-googles-poly-works-with-unity-editorxr/。作为旁注,Schoen 是我的朋友,也是 Packt 另一本书的合著者,《Cardboard VR Projects for Android》(2016):www.packtpub.com/application-development/cardboard-vr-projects-android

摘要

在本章中,我们构建了一个简单的场景模型,更熟悉了 Unity 编辑器,并了解了在设计场景时世界尺度的重要性,包括一些游戏内工具帮助我们处理缩放和定位。

我们随后强调,Unity 并不仅仅是一个资产创建工具。开发者通常使用 Unity 以外的工具创建模型,然后将它们导入。我们向您介绍了免费的开源建模应用 Blender 以及 Google Tilt Brush,并展示了如何导出资产并将其导入 Unity,包括像 Google Poly 这样的云服务。

在为 VR 开发时,真正酷的一件事是事物变化之快。这是一个新兴的行业、一种新的媒体,以及随着 VR 的成熟,每年都会有新的范式演变。每个季度都会推出新的设备。Unity 每月都会更新。每周都会发布新的软件工具。每天都有新事物要做和学习。当然,这也可能非常令人沮丧。我的建议是不要让它影响到你,而是要接受它。

这个问题的关键在于不断尝试新事物。这正是我们在本章中试图引导你的内容。想出一个点子,然后看看你能否让它运作起来。尝试新的软件。学习新的 Unity 功能。一次只做一件事,这样你就不会感到不知所措。当然,这正是本书的主题。这是一段持续且充满冒险的旅程。

在下一章中,我们将设置你的开发系统和 Unity 设置,以便构建和运行项目,在你的 VR 头盔中玩游戏。

第三章:VR 构建和运行

是的,这很酷,但我的 VR 在哪里?我想要我的 VR!

等一下,孩子,我们快到了。

在本章中,我们将设置您的系统并配置您的项目,以便使用虚拟现实头戴式显示器HMD)进行构建和运行。我们将讨论以下主题:

  • VR 设备集成软件的级别

  • 启用您平台的虚拟现实功能

  • 在项目中使用特定设备的相机装置

  • 设置您的开发机器以从 Unity 构建和运行 VR 项目

本章内容非常具体。尽管 Unity 旨在提供一个统一的平台以实现一次创建,多次构建,但您始终需要做一些系统设置、项目配置,并为您的特定目标设备包含对象组件。在本章的前几个主题之后,您可以跳转到最关心您和您的目标设备的部分。本章包括以下内容的食谱说明:

  • 为 SteamVR 构建

  • 为 Oculus Rift 构建

  • 为 Windows 沉浸式 MR 构建

  • 为 Android 设备设置

  • 为 GearVR 和 Oculus Go 构建

  • 为 Google VR 构建

  • 为 iOS 设备设置

Unity VR 支持和工具包

通常,作为一名开发者,您会花费时间在您的项目场景上工作。正如我们在上一章中为展览品所做的,您将添加对象、附加材质、编写脚本等。当您构建和运行项目时,场景将在 VR 设备上渲染,并实时响应头部和手部动作。以下图表总结了这一 Unity 系统 VR 架构:

图片

在您的场景中,您可能包括相机装置和其他高级工具包预制件和组件。所有设备制造商都提供针对其特定设备的工具包。这至少包括用于渲染 VR 场景的 Unity 相机组件。可能还包括一系列预制件和组件,有些是必需的,有些是可选的,这些组件真正帮助您创建交互式、响应式和舒适的 VR 体验。我们将在本章中详细介绍如何使用这些特定设备设置您的场景。

Unity 拥有一个不断增长的内置类和组件库,以支持 VR——他们称之为XR——以及增强现实。其中一些是平台特定的。但也有一些是设备独立的。这包括立体渲染、输入跟踪和音频空间化,仅举几例。有关详细信息,请参阅 Unity 手册中关于UnityEngine.XRUnityEngine.SpatialTracking的页面(docs.unity3d.com/ScriptReference/30_search.html?q=xr)。

在较低级别,任何在 VR 上运行的 Unity 项目都必须设置XR 玩家设置,选择虚拟现实支持,并确定应用程序应使用的特定低级 SDK 来驱动 VR 设备。在本章中,我们将详细介绍如何为特定设备设置您的项目。

因此,正如您所看到的,Unity 位于应用级工具组件和设备级 SDK 之间。它提供了一个设备无关的粘合剂,用于连接特定设备的 API、工具和优化。

从战略上讲,Unity Technologies 团队致力于为 2D、3D、VR 和 AR 游戏和应用提供统一的开发平台。Unity(您阅读此书时可能已经可用)正在开发一些重要的新组件,包括 VR 基础工具包和新的输入系统。这些内容本书未涉及。

在深入之前,让我们了解将我们的 Unity 项目与虚拟现实设备集成的可能方式。将应用程序与 VR 硬件集成的软件范围很广,从内置支持和特定设备的接口到设备无关和平台无关的解决方案。因此,让我们考虑您的选择。

Unity 的内置 VR 支持

通常,您的 Unity 项目必须包含一个可以渲染立体视图的相机对象,每个 VR 头盔的眼睛需要一个。自 Unity 5.1 以来,对 VR 头盔的支持已内置到 Unity 中,适用于多个平台上的各种设备。

您可以简单地使用一个标准的相机组件,就像您在创建新场景时附加到默认的Main Camera的那个一样。正如我们将看到的,您可以在 XR 玩家设置中启用虚拟现实支持,以便 Unity 渲染立体相机视图并在 VR 头盔(HMD)上运行您的项目。在玩家设置中,然后选择在项目构建时使用哪些特定的虚拟现实 SDK。SDK 与设备运行时驱动程序和底层硬件通信。Unity 对 VR 设备的支持收集在 XR 类中,如下所述:

输入控制器按钮、扳机、触摸板和摇杆也可以映射到 Unity 的输入系统。例如,OpenVR 手控制器映射可以在以下位置找到:docs.unity3d.com/Manual/OpenVRControllers.html

设备特定工具包

虽然内置的 VR 支持可能足以开始使用,但建议您还安装制造商提供的特定设备 Unity 包。特定设备接口将提供预制对象、大量有用的自定义脚本、着色器和其他直接利用底层运行时和硬件功能的重要优化。工具包通常包括示例场景、预制件、组件和文档以引导您。工具包包括:

  • SteamVR 插件: Steam 的 SteamVR 工具包 (assetstore.unity.com/packages/tools/steamvr-plugin-32647) 最初仅针对 HTC VIVE 发布。现在它支持多个具有位置跟踪左右手控制器的 VR 设备和运行时,包括 Oculus Rift 和 Windows Immersive MR。您使用 OpenVR SDK 构建项目,最终的可执行程序将在运行时决定您连接到 PC 的哪种类型硬件,并在该设备上运行该应用程序。这样,您不需要为 VIVE、Rift 和 IMR 设备准备不同版本的应用程序。

  • Oculus 集成工具包: Unity 的 Oculus 集成插件 (assetstore.unity.com/packages/tools/integration/oculus-integration-82022) 支持包括 Rift、GearVR 和 GO 在内的 Oculus VR 设备。除了触摸手控制器外,它还支持 Oculus Avatar、空间音频和网络房间 SDK。

  • Windows 混合现实工具包: Windows MRTK 插件 (github.com/Microsoft/MixedRealityToolkit-Unity) 支持 Windows 10 UWP 混合现实系列中的 VR 和 AR 设备,包括沉浸式 HMD(如来自 Acer、HP 等厂商的产品)以及可穿戴的 HoloLens 增强现实头戴设备。

  • Unity 的 Google VR SDK: Unity 的 GVR SDK 插件 (github.com/googlevr/gvr-unity-sdk/releases) 为 Google Daydream 和更简单的 Google Cardboard 环境提供用户输入、控制器和渲染支持。

当您在 Unity 中设置 VR 项目时,您可能会安装这些工具包中的一个或多个。我们将在本章后面引导您完成这个过程。

应用程序工具包

如果你需要更多的设备独立性和更高层次的交互功能,可以考虑开源的Virtual Reality ToolKitVRTK),可在assetstore.unity.com/packages/tools/vrtk-virtual-reality-toolkit-vr-toolkit-64131找到,以及NewtonVR(github.com/TomorrowTodayLabs/NewtonVR)。这些 Unity 插件提供了一个用于开发支持多个平台、移动、交互和 UI 控制的 VR 应用的框架。NewtonVR 主要关注物理交互。VRTK 建立在 Unity 内置的 VR 支持以及特定设备的预制件之上,因此它不是替代而是这些 SDK 的包装器。

在这一点上值得提及的是,Unity 正在开发自己的工具包,即XR Foundation ToolkitXRFT),可在blogs.unity3d.com/2017/02/28/updates-from-unitys-gdc-2017-keynote/找到,它将包括:

  • 跨平台控制器输入

  • 可定制的物理系统

  • AR/VR 特定的着色器和相机淡入淡出效果

  • 对象吸附和构建系统

  • 开发者调试和性能分析工具

  • 所有主要的 AR 和 VR 硬件系统

基于 Web 和 JavaScript 的 VR

重要 JavaScript API 正直接集成到主要网络浏览器中,包括 Firefox、Chrome、Microsoft Edge 和其他浏览器,如 Oculus 和 Samsung 的 GearVR 专用浏览器。

例如,WebVR 就像WebGL(网页的 2D 和 3D 图形标记语言 API),增加了 VR 渲染和硬件支持。虽然 Unity 目前支持 WebGL,但它不支持为 WebVR 构建 VR 应用(目前还不支持)。但我们希望有一天能看到这种情况发生。

基于互联网的 WebVR 的承诺令人兴奋。互联网是历史上最伟大的内容分发系统。能够像网页一样轻松地构建和分发 VR 内容将是一场革命。

如我们所知,浏览器几乎可以在任何平台上运行。因此,如果你将游戏针对 WebVR 或类似框架,你甚至不需要知道用户的操作系统,更不用说他们使用的是哪种 VR 硬件了!这正是这个想法。一些值得关注的工具和框架包括:

3D 世界

有许多第三方 3D 世界平台,在共享虚拟空间中提供多用户社交体验。你可以与其他玩家聊天,通过传送门在房间之间移动,甚至无需成为专家就能构建复杂交互和游戏。以下是一些 3D 虚拟世界的例子:

虽然这些平台可能有自己的构建房间和交互的工具,特别是 VRChat 允许您在 Unity 中开发 3D 空间和虚拟形象。然后您使用他们的 SDK 导出它们,并将它们加载到 VRChat 中,以便您和其他人可以共享您在互联网上创建的虚拟空间,并在实时社交 VR 体验中共享。我们将在第十三章,社交 VR 元宇宙中探讨这一点。

启用您平台上的虚拟现实功能

在上一章中我们创建的展览场景是一个使用 Unity 默认Main Camera的 3D 场景。正如我们所见,当你在 Unity 编辑器中按下 Play 时,场景会在你的 2D 计算机监视器上的游戏窗口中运行。设置项目以在 VR 中运行包括以下步骤:

  • 设置项目构建的目标平台

  • 在 Unity 的 XR 播放器设置中启用虚拟现实并设置 VR SDK

  • 将您目标设备的设备工具包导入到您的项目中(可选但推荐)并使用规定的预制体而不是默认的Main Camera

  • 安装构建目标设备所需的系统工具

  • 确保您的设备操作系统已启用开发

  • 确保您的设备 VR 运行时已设置并运行

如果您不确定,请使用表格来确定您 VR 设备的目标平台、虚拟现实 SDK 和 Unity 包:

设备 目标平台 VR SDK Unity Package
HTC Vive Standalone OpenVR SteamVR Plugin
Oculus Rift Standalone OpenVR SteamVR Plugin
Oculus Rift Standalone Oculus Oculus Integration
Windows IMR Universal Windows Platform Windows Mixed Reality Mixed Reality Toolkit Unity
GearVR/GO Android Oculus Oculus Integration
Daydream Android Daydream Google VR SDK for Unity and Daydream Elements
Cardboard Android Cardboard Google VR SDK for Unity
Cardboard iOS Cardboard Google VR SDK for Unity

列出以下各种集成工具包的 Unity 包链接:

现在,让我们为你的特定 VR 头盔配置项目。

如你所知,安装和设置细节可能会有所变化。我们建议你查阅当前的 Unity 手册和你的设备 Unity 界面文档以获取最新的说明和链接。

设置你的目标平台

新的 Unity 项目通常默认为针对独立桌面平台。如果你觉得这样没问题,你不需要做任何更改。让我们看看:

  1. 打开构建设置窗口(文件 | 构建设置…)并查看平台列表

  2. 选择你的目标平台。例如:

    • 例如,如果你正在为 Oculus Rift 或 HTC VIVE 构建,请选择 PC, Mac & Linux Standalone

    • 如果你正在为 Windows MR 构建,请选择通用 Windows 平台

    • 如果你正在为 Android 上的 Google Daydream 构建,请选择Android

    • 如果你正在为 iOS 上的 Google Cardboard 构建,请选择 iOS

  3. 然后按“切换平台”

设置你的 XR SDK

当你的项目在玩家设置中启用“支持虚拟现实”时构建,它将渲染立体相机视图并在 HMD 上运行:

  1. 进入玩家设置(编辑 | 项目设置 | 玩家)。

  2. 在检查器窗口中,找到底部的 XR 设置并勾选“支持虚拟现实”复选框。

  3. 选择你目标设备所需的虚拟现实 SDK。参考前面的表格。

根据你使用的目标平台,你的 Unity 安装中可用的虚拟现实 SDK 可能会有所不同。如果你的目标 VR 显示出来,那么你就可以开始了。你可以通过按列表中的(+)按钮添加其他 SDK,通过按(-)按钮移除 SDK。

例如,以下截图显示了为独立平台选择的虚拟现实 SDK。启用“支持虚拟现实”后,如果应用可以初始化 Oculus SDK,它将使用 Oculus SDK。如果应用在运行时无法初始化 Oculus SDK,它将尝试使用 OpenVR SDK。

在此阶段,通过在 Unity 编辑器中按“播放”,你可能能够预览你的 VR 场景。不同的平台以不同的方式支持播放模式。有些根本不支持编辑器预览。

安装你的设备工具包

接下来,安装你的设备特定 Unity 包。如果工具包在 Unity Asset Store 中有提供,请按照以下步骤操作:

  1. 在 Unity 中,打开资产商店窗口(窗口 | 资产商店)

  2. 搜索你想要安装的包

  3. 在资产的页面上,按“下载”,然后点击“安装”将文件安装到你的Project Assets/文件夹

如果你从网上单独下载了包,请按照以下步骤操作:

  1. 在 Unity 中,选择资产 | 导入包 | 自定义包

  2. 导航到包含你下载的.unitypackage文件的文件夹

  3. 按“打开”然后点击“安装”将文件安装到你的Project *Assets/*文件夹

随意探索包内容文件。尝试打开并尝试任何包含的示例场景。并熟悉任何可能对您有用的预制体对象(在“预制体/”文件夹中)。

创建 MeMyselfEye 玩家预制体

大多数 VR 工具包都提供了一个预配置的玩家相机装置作为预制体,您可以将其插入到场景中。这个装置替换了默认的“主相机”。对于这本书,由于我们不知道您针对的是哪些特定的设备和平台,我们将自己制作相机装置。让我们称它为MeMyselfEye(嘿,这是 VR!)。这将有助于以后,它将简化本书中的对话,因为不同的 VR 设备可能使用不同的相机资源。就像一个装满您 VR 灵魂的空容器...

我们将在本书的各个章节中重复使用这个MeMyselfEye预制体,作为项目中方便的通用 VR 相机资源。

预制体是一个可重复使用(预制)的对象,保留在您的项目“资产”文件夹中,可以将其一次或多次添加到项目场景中。让我们按照以下步骤创建对象:

  1. 打开 Unity 和上一章的项目。然后,通过导航到文件 | 打开场景(或在“资产”面板下的场景对象上双击)来打开场景。

  2. 从主菜单栏中,导航到 GameObject | 创建空对象。

  3. 将对象重命名为MeMyselfEye

  4. 确保它有一个重置变换(在其检查器窗口的变换面板中,选择右上角的齿轮图标并选择重置)。

  5. 在层次结构面板中,将“主相机”对象拖动到MeMyselfEye中,使其成为子对象。

  6. 选择“主相机”对象后,重置其变换值(在变换面板的右上角,点击齿轮图标并选择重置)。

  7. 然后将自己定位在场景中间附近。再次选择MeMyselfEye并设置其位置(00-1.5)。

  8. 在某些 VR 设备上,玩家高度由设备校准和传感器确定,即您在现实生活中的身高,因此请将“主相机”的 Y 位置保持在0

  9. 在其他 VR 设备上,特别是没有位置跟踪的设备上,您需要指定相机高度。选择“主相机”(或者更具体地说,具有相机组件的游戏对象)并设置其位置(01.40)。

游戏视图应显示我们处于场景内部。如果您还记得我们之前做的 Ethan 实验,我选择了1.4的 Y 位置,这样我们就会在 Ethan 的眼睛水平附近。

现在,让我们将其保存为可重复使用的预制体对象,或称为“预制体”,在“资产”面板下,以便我们可以在本书的其他章节的其他场景中使用它:

  1. 在项目面板中,在“资产”下选择顶级“资产”文件夹,右键单击并导航到创建 | 文件夹。将文件夹重命名为“预制体”。

  2. MeMyselfEye预制体拖动到项目面板中的“资产/预制体”文件夹下以创建一个预制体。

你的带有预制件的层次结构如下所示:

现在我们将继续讨论如何基于每个平台构建你的项目。请跳转到适合你设置的相应主题。

如果你想要在多个平台上尝试你的项目,比如 VIVE(Windows)和 Daydream(Android),考虑为每个目标设备制作单独的预制件,例如,MeMyselfEye-SteamVRMeMyselfEye-GVR等等,然后根据需要交换它们。

为 SteamVR 构建

要将你的应用程序针对使用HTC VIVE,你将使用OpenVR SDK。此 SDK 还支持 Oculus Rift 带有触摸控制器,以及Windows 沉浸式混合现实IMR)设备:

  1. 配置你的 Unity 构建设置以针对独立平台。

  2. 在玩家设置中,在 XR 设置下,将虚拟现实设置为启用

  3. 确保 OpenVR 位于虚拟现实 SDK 列表的顶部。

  4. 按照之前的说明,从资源商店下载并安装 SteamVR 插件。

  5. 当你安装 SteamVR 时,可能会提示你接受对项目设置的推荐更改。除非你知道更好的方法,否则我们建议你接受它们。

现在我们将把 SteamVR 相机装置添加到场景中的MeMyselfEye对象:

  1. 在你的项目窗口中查看;在Assets文件夹下,你应该有一个名为SteamVR的文件夹。

  2. 在其中有一个名为Prefabs的子文件夹。将名为[CameraRig]的预制件从Assets/SteamVR/Prefabs/文件夹拖动到你的层次结构中。将其放置为MeMyselfEy*e*的子对象。

  3. 如果需要,将其变换位置重置为(000)。

  4. MeMyselfEye下禁用Main Camera对象;你可以通过在其检查器窗口的左上角取消选中启用复选框来禁用对象。或者,你也可以直接删除Main Camera对象。

  5. 通过在层次结构中选择MeMyselfEye并按下其检查器中的应用按钮来保存预制件。

注意,SteamVR 相机装置的 Y 位置应该设置为 0,因为它将使用玩家的实际身高来实时设置相机高度。

为了测试它,确保 VR 设备已经正确连接并开启。你应该在 Windows 桌面上打开 SteamVR 应用程序。点击 Unity 编辑器顶部的游戏播放按钮。戴上头戴式设备,它应该很棒!在 VR 中,你可以四处查看——左、右、上、下以及背后。你可以倾斜身体,也可以向前倾斜。使用手柄的拇指垫,你可以让 Ethan 像我们之前做的那样行走、奔跑和跳跃。

现在你可以按照以下步骤构建你的游戏作为一个独立的可执行应用程序。很可能是你已经做过这件事了,至少对于非 VR 应用程序来说是这样。基本上是一样的:

  1. 从主菜单栏,导航到文件 | 构建设置...

  2. 如果当前场景尚未在构建场景列表中,请按添加打开场景。

  3. 点击构建并将其名称设置为Diorama

  4. 我喜欢将我的构建保存在名为Build的子目录中;如果你想要的话,可以创建一个。

  5. 点击保存。

在你的构建文件夹中将会创建一个可执行文件。像运行任何可执行应用程序一样运行Diorama:双击它。

关于 Unity 对 OpenVR 的支持更多信息,请参阅docs.unity3d.com/Manual/VRDevices-OpenVR.html

为 Oculus Rift 构建

要为 Oculus Rift 构建,你可以使用 OpenVR。但如果你计划在 Oculus 商店发布,或使用 Oculus 特定的 SDK 来利用 Oculus 生态系统中的其他高价值功能,你需要构建到 Oculus SDK,如下所示:

  1. 配置你的 Unity 构建设置以针对 Standalone 平台

  2. 玩家设置中,在 XR 设置下,设置虚拟现实启用

  3. 确保Oculus位于虚拟现实 SDKs列表的顶部。

  4. 按照之前的说明,从资源商店下载并安装Oculus Integration 包。

现在我们将 OVR 相机架添加到场景中的MeMyselfEye对象:

  1. 在你的项目窗口中,在Assets文件夹下你应该有一个名为OVR的文件夹。

  2. 在其中有一个名为Prefabs的子文件夹。将名为OVRCameraRig的预制件从Assets/OVR/Prefabs/文件夹拖到你的层次结构中。将其放置为MeMyselfEye的子对象。

  3. 通过将其变换到位置(01.60)来将其 Y 位置设置为 1.6。

  4. MeMyselfEye下禁用Main Camera对象,也可以在检查器窗口的左上角取消选中启用复选框。或者,你也可以直接删除Main Camera对象。

  5. 通过在层次结构中选择MeMyselfEye并按下检查器中的应用按钮来保存预制件。

注意,OVR 相机架应该设置为你的期望高度(在这个例子中是 1.6 英寸),运行时会根据你在 Oculus 运行时设备配置中设置的高度进行调整。

要测试它,确保 VR 设备已正确连接并开启。你应该在 Windows 桌面上打开 Oculus 运行时应用程序。在 Unity 编辑器的顶部中央点击游戏播放按钮。戴上头戴式设备,它应该很棒!在 VR 中,你可以四处查看——左、右、上、下以及背后。你可以倾斜身体,靠近或远离。使用手柄的摇杆,你可以让 Ethan 像我们之前做的那样行走、奔跑和跳跃。

注意,Oculus 包会在 Unity 编辑器的菜单栏上安装有用的菜单项。这里我们不会详细介绍,它们可能会发生变化。我们鼓励你探索它们提供的选项和快捷方式。请参见截图:

图片

要包含 Oculus Dash 支持,你必须使用 Oculus OVR 版本 1.19 或更高版本(包含在 Unity 2017.3 或更高版本中)。然后:

  1. 在玩家设置中,XR 面板,展开 Oculus SDK 以获取更多设置

  2. 选中共享深度缓冲区复选框

  3. 选中Dash 支持复选框:

图片

有关 Unity 中 Oculus Dash 支持的更多信息,请参阅developer.oculus.com/documentation/unity/latest/concepts/unity-dash/

现在,你可以按照以下步骤构建你的游戏作为一个单独的可执行应用程序。很可能是你已经这样做过了,至少对于非 VR 应用程序来说是这样。基本上是一样的:

  1. 从主菜单栏,导航到文件 | 构建设置...

  2. 如果当前场景尚未在构建场景列表中,请按添加打开场景

  3. 点击构建并将其名称设置为Diorama

  4. 我喜欢将我的构建保存在名为构建的子目录中;如果你想要的话,创建一个。

  5. 点击保存

在你的构建文件夹中将会创建一个可执行文件。像运行任何可执行应用程序一样运行Diorama:双击它。

有关 Unity 对 Oculus 的支持的更多信息,请参阅developer.oculus.com/documentation/unity/latest/concepts/book-unity-gsg/

为 Windows 沉浸式 MR 构建

微软的 3D 媒体混合现实策略是支持从虚拟现实到增强现实的各种设备和应用程序。这本书和我们的项目是关于 VR 的。在另一端是微软的可穿戴 AR 设备 HoloLens。我们将使用的 MixedRealityToolkit-Unity 包包括对沉浸式 MR 头戴设备和 HoloLens 的支持。

为了使你的应用程序能够使用Windows 沉浸式混合现实IMR)头戴设备,你将使用 Window Mixed Reality SDK,如下所示:

  1. 将你的 Unity 构建设置配置为目标为通用 Windows 平台

  2. 在玩家设置中,在 XR 设置,设置虚拟现实启用

  3. 确保在虚拟现实****SDKs列表中Windows混合现实位于顶部。

  4. 按照之前的说明下载并安装 Mixed Reality Toolkit Unity。

  5. 我们还建议你安装其姐妹示例 unity 包,位置相同。

现在我们将把MixedRealityCamera装置添加到场景中的MeMyselfEye对象:

  1. 在项目窗口中查看;在Assets文件夹下,你应该有一个名为HoloToolkit(或MixedRealityToolkit)的文件夹。

  2. 在其中有一个名为Prefabs的子文件夹。将名为MixedRealityCameraParent的预制件从Assets/HoloToolkit/Prefabs/文件夹拖动到你的层次结构中。将其放置为MeMyselfEye的子对象。

  3. 如果需要,将其变换重置为位置000)。

  4. MeMyselfEye下禁用主相机对象。你可以通过在其检查器窗口的左上角取消选中启用复选框来禁用对象。或者,你也可以直接删除主相机对象。

  5. 通过在层次结构中选择MeMyselfEye并按下检查器中的应用按钮来保存预制件。

注意,MixedRealityCameraParent装置的 y 位置应设置为 0,因为它将使用玩家的实际身高来实时设置相机高度。

为了测试它,请确保 VR 设备已正确连接并开启。您应该在 Windows 桌面上打开 MR Portal 应用程序。点击 Unity 编辑器顶部中央的游戏播放按钮。戴上头戴式设备,它应该很棒!在 VR 中,您可以四处查看——左、右、上、下和背后。您可以倾斜身体,并向前或向后倾斜。使用手柄的拇指垫,您可以让 Ethan 像我们之前做的那样行走、奔跑和跳跃。

设置 Windows 10 开发者模式

对于 Windows MR,您必须在 Windows 10 上开发,并启用开发者模式。要设置开发者模式:

  1. 前往操作中心 | 所有设置 | 更新与安全 | 开发者

  2. 选择开发者模式,如图所示:

图片

在 Visual Studio 中安装 UWP 支持

当您安装 Unity 时,您可以选择安装Microsoft Visual Studio Tools for Unity作为默认脚本编辑器。这是一个出色的编辑器和调试环境。然而,与 Unity 一起安装的这个版本并不是 Visual Studio 的完整版本。要将您的构建作为单独的 UWP 应用程序,您将需要使用 Visual Studio 的完整版本。

Visual Studio 是一个强大的集成开发环境IDE),适用于各种项目。当我们从 Unity 构建 UWP 时,我们实际上会构建一个 Visual Studio 准备好的项目文件夹,然后您可以在 VS 中打开它以完成编译、构建和部署过程,在您的设备上运行应用程序。

Visual Studio 有三种版本,社区版专业版企业版;任何一种对我们来说都足够了。社区版是免费的,可以从这里下载:www.visualstudio.com/vs/

一旦下载了安装程序,打开它以选择要安装的组件。在工作负载选项卡下,我们已选择:

  • 通用 Windows 平台开发

  • 使用 Unity 进行游戏开发

图片

此外,选择使用 Unity 进行游戏开发选项,如下所示:

图片

现在,我们可以进入 Unity。首先,我们应该确保 Unity 知道我们正在使用 Visual Studio:

  1. 前往编辑 | 首选项

  2. 外部工具选项卡中,确保已选择Visual Studio作为您的外部脚本编辑器,如下所示:

图片

UWP 构建

现在,您可以使用以下步骤将您的游戏构建为单独的可执行应用程序:

  1. 从主菜单栏,导航到文件 | 构建设置...

  2. 如果当前场景尚未在要构建的场景列表中,请按添加打开的场景

  3. 对话框的右侧有选项:

    • 目标设备:PC

    • 构建类型:D3D

    • SDK:最新安装的(例如,10.0.16299.0)

  4. 点击构建并设置其名称

  5. 我喜欢将我的构建保存在名为 Build 的子目录中;如果您想的话,可以创建一个

  6. 点击保存

注意,混合现实工具包提供了对这些和其他设置及服务的快捷方式,如下所示:

图片

现在在 Visual Studio 中打开项目:

  1. 一种简单的方法是导航到文件资源管理器中的“构建”文件夹,查找项目的 .sln 文件(SLN 是 Microsoft VS 解决方案文件)。双击它以在 Visual Studio 中打开项目。

  2. 选择解决方案配置:调试、主版本或发布版本。

  3. 设置目标为 x64。

  4. 按“本地计算机上的播放”以构建解决方案。

有关 Unity 对 Windows 混合现实支持的信息,请参阅 github.com/Microsoft/MixedRealityToolkit-Unity,包括前往“入门”页面的链接。

为 Android 设备设置

要开发将在 Google Daydream、Cardboard、GearVR、Oculus GO 或其他 Android 设备上运行的 VR 应用程序,我们需要为 Android 开发设置一个开发机器。

本节将帮助您设置您的 Windows PC 或 Mac。这些要求并不特定于虚拟现实;这些是任何从 Unity 构建 Android 应用的任何人所需相同步骤。该过程在其他地方也有很好的文档记录,包括 Unity 文档 docs.unity3d.com/Manual/android-sdksetup.html

步骤包括:

  • 安装 Java 开发工具包

  • 安装 Android SDK

  • 安装 USB 设备驱动程序和调试

  • 配置 Unity 外部工具

  • 配置 Android Unity Player 设置

好的,让我们开始吧。

安装 Java 开发工具包 (JDK)

您的计算机上可能已经安装了 Java。您可以通过打开终端窗口并运行命令 java-version 来检查。如果您没有 Java 或需要升级,请按照以下步骤操作:

  1. 访问 Java SE 下载网页 www.oracle.com/technetwork/java/javase/downloads/index.html 并下载它。寻找 JDK 按钮图标,它将带您进入下载页面。

  2. 选择适合您系统的包。例如,对于 Windows,请选择 Windows x64。文件下载后,打开它并按照安装说明进行操作。

  3. 记下安装目录以备后用。

  4. 安装完成后,打开一个新的终端窗口并再次运行 java -version 以进行验证。

无论您是刚刚安装了 JDK 还是它已经存在,请记下其在磁盘上的位置。您将在稍后的步骤中需要告诉 Unity 这个信息。

在 Windows 上,路径可能类似于 Windows: C:\Program Files\Java\jdk1.8.0_111\bin

如果找不到,请打开 Windows 资源管理器,导航到 \Program Files 文件夹,查找 Java,然后向下钻取,直到看到其 bin 目录,如下截图所示:

图片

在 OS X 上,路径可能类似于:/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home

如果找不到,从终端窗口运行以下命令:/usr/libexec/java_home

安装 Android SDK

你还需要安装 Android SDK。具体来说,你需要 Android SDK 管理器。它可以作为独立的命令行工具或 Android Studio IDE 的一部分使用。如果你负担得起磁盘空间,我建议只安装 Android Studio,因为它为 SDK 管理器提供了一个不错的图形界面。

要安装 Android Studio IDE,请访问 developer.android.com/studio/install.html 并点击下载 Android Studio。下载完成后,打开它并按照安装说明进行操作。

你将需要输入 Android Studio IDE 和 SDK 的位置。你可以接受默认位置或更改它们。请记下 SDK 路径位置;你将在稍后的步骤中需要告诉 Unity 这个信息:

个人来说,我的 D: 驱动器上有更多空间,所以我将应用程序安装到了 D:\Programs\Android\Android Studio。我喜欢将 SDK 放在 Android Studio 程序文件附近,这样更容易找到,所以我将 Android SDK 安装位置更改为 D:\Programs\Android\sdk

通过命令行工具

Unity 实际上只需要命令行工具来为 Android 构建项目。如果你愿意,你可以只安装这个包来节省磁盘空间。滚动到下载页面底部的“仅获取命令行工具”部分。选择适合你平台的包:

这是一个 ZIP 文件;解压缩到一个文件夹中,并请记住其位置。如前所述,在 Windows 上我喜欢使用 D:\Programs\Android\sdk。这将包含一个 tools 子文件夹。

ZIP 文件只包含工具,而不是实际的 SDK。使用 sdkmanager 下载你需要的包。有关详细信息,请参阅 developer.android.com/studio/command-line/sdkmanager.html

要列出已安装和可用的包,运行 sdkmanager --list。你可以通过在引号中列出它们并以分号分隔来安装多个包,如下所示:

    sdkmanager "platforms;android-25"

截至撰写本文时,最低的 Android API 级别如下(请查阅当前文档以了解更改):

Cardboard: API 级别 19 (Android 4.4 KitKat)

GearVR: API 级别 21 (Android 5.0 Lollipop)

Daydream: API 级别 24 (Android 7.0 Nougat)

关于你的 Android SDK 根路径位置

如果你已经安装了 Android,或者忘记了 SDK 的安装位置,你可以通过打开 SDK 管理器 GUI 来找到根路径。当 Android Studio 打开时,导航到主菜单并选择 工具 | Android | SDK 管理器。路径通常在顶部附近:

在 Windows 上,路径可能类似于:

  • Windows: C:\Program Files\Android\sdk,或 C:/Users/Yourname/AppData/Local/Android/Sdk

在 OS X 上,路径可能类似于:

  • OS X: /Users/Yourname/Library/Android/sdk 

安装 USB 设备调试和连接

下一步是在您的 Android 设备上启用 USB 调试。这是您 Android 设置中的开发者选项的一部分。但是开发者选项可能不可见,并且需要启用:

  1. 在设备上找到 Settings | About 中的 Build number 属性。根据您的设备,您可能甚至需要深入另一个或两个层级(例如 Settings | About | Software Information | More | Build number)。

  2. 现在是时候进行魔法咒语了。点击构建编号七次。它将倒计时直到 开发者选项 被启用,并且现在将作为设置中的另一个选项出现。

  3. 前往 Settings | Developer options,找到 USB debugging 并启用它。

  4. 现在通过 USB 线缆将设备连接到您的开发机器。

安卓设备可能会自动被识别。如果您被提示更新驱动程序,您可以通过 Windows 设备管理器来完成此操作。

在 Windows 上,如果设备未被识别,您可能需要下载 Google USB Driver。您可以通过 SDK Manager 下的 SDK Tools 选项卡来完成此操作。更多信息请参阅 developer.android.com/studio/run/win-usb.html。例如,以下截图显示了 SDK Manager 的 SDK Tools 选项卡,其中已选择 Google USB Driver:

到目前为止,做得很好!

配置 Unity 外部工具

凭借我们需要的所有东西以及安装的工具的路径,我们现在可以回到 Unity 中。我们需要告诉 Unity 哪里可以找到所有的 Java 和 Android 内容。注意,如果您跳过此步骤,那么在构建应用程序时,Unity 将提示您选择文件夹:

  1. 在 Windows 上,导航到主菜单并选择 Edit | Preferences,然后在左侧选择 External Tools 选项卡。在 OS X 上,它在 Unity | Preferences 中。

  2. 在 Android SDK 文本框中,粘贴您的 Android SDK 路径。

  3. 在 Java JDK 文本框中,粘贴您的 Java JDK 路径。

这里显示了带有我的 SDK 和 JDK 的 Unity Preferences:

配置 Android 的 Unity Player 设置

现在我们将配置您的 Unity 项目以构建 Android 版本。首先,确保在 Build Settings 中 Android 是您的目标平台。Unity 为 Android 提供了大量的支持,包括对运行时功能和移动设备功能的配置和优化。这些选项可以在 Player Settings 中找到。我们现在只需要设置其中的一些。构建我们的演示项目所需的最小要求是 Bundle Identifier 和 Minimum API Level:

  1. 在 Unity 中,导航到 File | Build Settings 并检查 Platform 选项卡。

  2. 如果 Android 尚未选中,请现在选中并按切换平台。

  3. 如果您打开了构建设置窗口,请按玩家设置…按钮。或者,您可以从主菜单中通过 Edit | Project Settings | Player 获取。

  4. 看向包含玩家设置的检查器面板。

  5. 找到其他设置参数组,并点击标题栏(如果尚未打开)以找到识别变量。

  6. 将捆绑标识符设置为类似于传统 Java 包名的独特产品名称。所有 Android 应用都需要 ID。通常格式为 com.公司名.产品名。它必须在目标设备上是唯一的,最终在 Google Play 商店中也是唯一的。您可以选择任何名称。

  7. 为您的目标平台设置一个最低 API 级别(如之前列出)。

再次,玩家设置中还有许多其他选项,但现在我们可以使用它们的默认设置。

为 GearVR 和 Oculus Go 构建

要为三星 GearVR 和 Oculus Go 移动设备构建,您将使用 Oculus SDK。这两个设备都是基于 Android 的,因此您必须按照之前描述的那样设置您的开发机器以进行 Android 开发(Oculus Go 是二进制的,与 GearVR 兼容)。然后在 Unity 中完成以下步骤:

  1. 配置您的 Unity 构建设置以针对 Android 平台。

  2. 玩家设置 中,在 XR 设置下,设置虚拟现实启用

  3. 确保 Oculus 位于虚拟现实 SDK 列表的顶部。

  4. 按照之前的说明,从资源商店下载并安装 Oculus 集成包。

现在,我们将向场景中的 MeMyselfEye 对象添加 OVR 相机装置。这些步骤类似于之前描述的独立 Oculus Rift 设置。在这种情况下,您可以使用相同的 MeMyselfEye 预制件为 Rift 和 GearVR。

  1. 在您的项目窗口中查看;在 Assets 文件夹下,您应该有一个名为 OVR 的文件夹。

  2. 在其中有一个名为 Prefabs 的子文件夹。将 Assets/OVR/Prefabs/ 文件夹中的名为 OVRCameraRig 的预制件拖放到您的层次结构中。将其放置为 MeMyselfEye 的子对象。

  3. 通过将其 变换 设置为 位置 到 (0, 1.6, 0) 来将其高度设置为 1.6。

  4. MeMyselfEye 下禁用 主相机 对象。您可以通过取消勾选其检查器窗口左上角的启用复选框来禁用对象。或者,您可以直接删除 主相机 对象。

  5. 通过在层次结构中选择 MeMyselfEye 并在检查器中按下其 应用 按钮来保存预制件。

现在,您可以使用以下步骤将您的游戏构建为一个独立的可执行应用程序:

  1. 从主菜单栏导航到 File | Build Settings...

  2. 如果当前场景尚未在 要构建的场景 列表中,请按 添加打开的场景

  3. 点击 构建和运行 并将其名称设置为 Diorama

  4. 我喜欢将我的构建保存在名为 Build 的子目录中;如果您想的话,可以创建一个。

  5. 点击 保存

在您的构建文件夹中创建一个 Android APK 文件,并将其上传到您连接的 Android 设备。

有关 Unity 对 Oculus SDK 的支持的更多信息,请参阅docs.unity3d.com/Manual/VRDevices-Oculus.html.

为 Google VR 构建

Google VR SDK 支持 Daydream 和 Cardboard。Daydream是高端版本,仅限于更快速、功能更强大的 Daydream-ready Android 手机。Cardboard是低端版本,支持许多移动设备,包括 Apple iOS 的 iPhone。您可以在 Unity 中构建针对两者的项目。

Google Daydream

要在移动 Android 设备上为Google Daydream构建,您将使用 Daydream SDK。您必须按照上述说明设置您的开发机器以进行 Android 开发。然后完成以下步骤:

  1. 将 Unity 构建设置配置为针对Android平台

  2. 在玩家设置中,在 XR 设置下设置虚拟现实启用

  3. 确保 Daydream 位于虚拟现实 SDK 列表的顶部

  4. 按照之前的要求下载并安装 Google VR SDK 包

现在,我们将为我们的场景构建MeMyselfEye相机装置。目前,我们最好的例子是 Google VR SDK 提供的 GVRDemo 示例场景(可在Assets/GoogleVR/Demos/Scenes/文件夹中找到):

  1. 在您的场景层次结构中,在MeMyselfEye下创建一个空的游戏对象(选择MeMyselfEye对象,右键单击,选择创建空对象)。将其命名为MyGvrRig

  2. 通过将其TransformPosition设置为(0, 1.6, 0)来将其高度设置为 1.6。

  3. 从项目文件夹中定位提供的预制件(Assets/GoogleVR/Prefabs)。

  4. 将以下预制件的副本从项目文件夹拖到层次结构中,作为MyGvrRig的子项:

    • Headset/GvrHeadset

    • Controllers/GvrControllerMain

    • EventSystem/GvrEventSystem

    • GvrEditorEmulator

    • GvrInstantPreviewMain

  5. MeMyselfEye下保留Main Camera对象并启用它。GoogleVR 使用现有的Main Camera对象。

  6. 通过在层次结构中选择MeMyselfEye并按下其检查器中的应用按钮来保存预制件。

GvrHeadset是一个 VR 相机属性管理器。GvrControllerMain为 Daydream 3DOF 手控制器提供支持。我们将在后续章节中使用GvrEventSystem;它为 Unity 的事件系统对象提供即插即用的替代方案。GvrEditorEmulator实际上不是您应用程序的一部分,但可以在您按下 Play 时在 Unity 编辑器中预览您的场景。同样,添加GvrInstantPreviewMain可以让您在编辑器中按下 Play 时在您的手机上预览您的应用程序。

这些是我们知道将要使用的预制件。当然,您可以继续探索 SDK 中提供的其他预制件。请参阅developers.google.com/vr/unity/reference/

我们还建议你查看 Google Daydream Elements,它提供了额外的演示和脚本“用于开发高质量的 VR 体验。”我们将在下一章介绍这个。见 developers.google.com/vr/elements/overview.

当你准备好时,你可以按照以下步骤将你的游戏构建为一个独立的可执行应用程序:

  1. 从主菜单栏,导航到文件 | 构建设置...

  2. 如果当前场景尚未在构建场景列表中,请按添加打开场景。

  3. 点击构建和运行,并将其名称设置为Diorama

  4. 我喜欢将我的构建保存在名为 Build 的子目录中;如果你想要的话,可以创建一个。

  5. 点击保存。

在你的构建文件夹中创建一个 Android APK 文件,并将其上传到你的附加 Android 手机。

Google Cardboard

为 Google Cardboard 构建与 Daydream 类似,但更简单。此外,Cardboard 应用程序可以在 iPhone 上运行。你必须按照描述设置你的开发机器以进行 Android 开发。或者如果你正在为 iOS 开发,请参阅下一节以获取详细信息。然后按照以下方式设置你的项目:

  1. 配置你的 Unity 构建设置以针对 AndroidiOS 平台。

  2. 在玩家设置中,在 XR 设置下设置虚拟现实启用,并且

  3. 确保 Cardboard 在虚拟现实 SDKs 列表中。

  4. 按照之前说明的步骤下载并安装 Google VR SDK 软件包。

我们现在将为我们的场景构建MeMyselfEye相机装置。

  1. 在你的场景层次结构中,在MeMyselfEye(选择MeMyselfEye对象,右键单击,选择创建空对象)下创建一个空的游戏对象。将其命名为MyGvrRig

  2. 通过将其变换设置为位置(01.60)来将其高度设置为 1.6。

  3. 从项目文件夹中,找到提供的预制体(Assets/GoogleVR/Prefabs)。

  4. 将以下预制体的副本从项目文件夹拖到层次结构中,作为MyGvrRig的子对象:

    • 头戴式设备/GvrHeadset

    • GvrEditorEmulator

  5. MeMyselfEye下保留Main Camera对象并启用它。GoogleVR 使用现有的Main Camera对象。

  6. 通过在层次结构中选择MeMyselfEye并按下检查器中的应用按钮来保存预制体。

当你准备好时,你可以按照以下步骤将你的游戏构建为一个独立的可执行应用程序:

  1. 从主菜单栏,导航到文件 | 构建设置...

  2. 如果当前场景尚未在构建场景列表中,请按添加打开场景。

  3. 点击构建和运行,并将其名称设置为Diorama

  4. 我喜欢将我的构建保存在名为 Build 的子目录中;如果你想要的话,可以创建一个。

  5. 点击保存。

在你的构建文件夹中创建一个 Android APK 文件,并将其上传到你的附加 Android 手机。

Google VR 播放模式

当你的项目配置为 Google VR(Daydream 或 Cardboard)时,并在 Unity 中按下播放,你可以预览场景并使用键盘键来模拟设备运动:

  • 使用 Alt + 鼠标移动来平移和前后倾斜。

  • 使用 Ctrl + 鼠标移动来左右倾斜你的头部。

  • 使用 Shift + 鼠标控制 Daydream 手控制器(仅限 Daydream)。

  • 点击鼠标进行选择。

更多详细信息,请参阅 developers.google.com/vr/unity/get-started

使用 Daydream,您还有使用即时预览的选项,这允许您立即在您的设备上测试您的 VR 应用。按照 Google VR 文档中的说明 (developers.google.com/vr/tools/instant-preview) 设置您的项目和设备以利用此功能。

关于 Unity 对 Google VR SDK for Daydream 的支持更多信息,请参阅 docs.unity3d.com/Manual/VRDevices-GoogleVR.html

为 iOS 设备设置

本节将帮助您从 Unity 为 iPhone 设置 Mac 以进行 iOS 开发。这些要求并不特定于虚拟现实;这些是任何从 Unity 构建 iOS 应用的人都需要遵循的相同步骤。该过程在其他地方也有很好的文档记录,包括 Unity 文档中的 docs.unity3d.com/Manual/iphone-GettingStarted.html

苹果封闭生态系统的要求之一是您必须使用 Mac 作为您的开发机器来为 iOS 开发。就是这样。好处是设置过程非常直接。

截至写作时,唯一能在 iOS 上运行的 VR 应用是 Google Cardboard。

步骤包括:

  • 拥有一个 Apple ID

  • 安装 Xcode

  • 为 iOS 配置 Unity Player 设置

  • 构建 并 运行

好的,让我们尝一尝这个苹果。

拥有一个 Apple ID

要为 iOS 开发,您需要一个用于开发的 Mac 电脑和一个 Apple ID 以登录 App Store。这将允许您构建在您的个人设备上运行的 iOS 应用。

还建议您拥有一个 Apple 开发者账户。它每年收费 99 美元,但这是您进入包括设置配置文件在内的工具和服务的门票,这些工具和服务用于在其他设备上共享和测试您的应用。您可以在 developer.apple.com/programs/ 了解更多关于 Apple 开发者计划的信息。

安装 Xcode

Xcode 是为开发任何 Apple 设备而提供的全能工具包。您可以从 Mac App Store 免费下载它:itunes.apple.com/gb/app/xcode/id497799835?mt=12。请注意:它相当大(截至写作时超过 4.5 GB)。下载它,打开下载的 dmg 文件,并按照安装说明进行操作。

为 iOS 配置 Unity Player 设置

现在,我们将配置您的 Unity 项目以构建 iOS。首先,确保在构建设置中将 iOS 设置为目标平台。Unity 为 iOS 提供了大量的支持,包括对运行时功能和移动设备功能的配置和优化。这些选项可以在 Player 设置中找到。我们现在只需要设置其中的一些(构建我们项目所需的最小设置):

  1. 在 Unity 中,导航到 文件 | 构建设置,检查平台面板。如果 iOS 目前未选中,请现在选中它并按切换平台。

  2. 如果您打开了构建设置窗口,请按 Player Settings… 按钮。或者,您也可以从主菜单:编辑 | 项目设置 | Player 进入。查看现在包含 Player 设置的检查器面板。

  3. 找到其他设置参数组,并点击标题栏(如果尚未打开)以找到身份变量。

  4. 将捆绑标识符设置为类似于传统 Java 包名的产品唯一名称。所有 iOS 应用都需要 ID。通常,它采用 com.公司名.产品名 的格式。它必须在目标设备上唯一,最终在 App Store 上也必须是唯一的。您可以选择任何想要的名称。

  5. 将自动签名团队 ID 设置为 Xcode 中的签名团队设置,并勾选自动签名复选框。

要使用 Xcode 配置您的 Apple ID,请在 Xcode 中转到首选项 | 账户,并通过点击 + 添加一个 Apple ID。

构建和运行

Xcode 包含一个集成开发环境IDE),用于托管您的 Xcode 项目。当您从 Unity 构建 iOS 时,实际上并没有构建 iOS 可执行文件。相反,Unity 会构建一个 Xcode 准备好的项目文件夹,然后您可以在 Xcode 中打开它以完成编译、构建和部署过程,并在您的设备上运行应用程序。让我们开始吧!

  1. 确保您的设备已开启、连接,并且您已授权 Mac 访问。

  2. 在构建设置中,按构建和运行按钮开始构建。

  3. 您将被提示输入构建文件的名称和位置。我们建议您在项目根目录下创建一个名为 Build 的新文件夹,并根据需要指定该文件夹下的文件或子文件夹名称。

如果一切顺利,Unity 将创建一个 Xcode 项目并在 Xcode 中打开它。它将尝试构建应用程序,如果成功,将上传到您的设备。现在您可以在设备上运行 VR 应用程序,并向您的朋友和家人展示!

摘要

在本章中,我们帮助你设置了你的 VR 开发系统,并为你的目标平台和设备构建了你的项目。我们讨论了不同级别的设备集成软件,然后在你的开发机器上安装了适合你的目标 VR 设备的软件,并将资产包添加到了你的 Unity 项目中。虽然我们已经总结了这些步骤,但所有这些步骤都在设备制造商的网站上以及 Unity 手册中有很好的文档记录,我们鼓励你查看所有相关的文档。

在这个阶段,你应该能够在 Unity 编辑器的播放模式下预览你的 VR 场景。你应该能够构建并运行你的项目,并且可以直接在你的设备上安装并运行它作为二进制文件。

在下一章中,我们将更深入地处理场景模型,并探索在虚拟现实中控制对象的技术。从第三人称视角,我们将与场景中的对象(伊森,僵尸)进行交互,并实现基于外观的控制。

第四章:基于目光的控制

目前,我们的场景是一个第三人称虚拟现实体验。当你进入其中时,你就像一个观察者或第三人称摄像机。当然,你可以四处张望,并添加控制来移动摄像机的视角。然而,场景中的任何动作都是从第三人称视角出发的。

在本章中,我们将主要保持第三人称模式,但我们会更加个人化地参与其中。我们将探讨通过观察和凝视来控制虚拟世界中物体的一些技术。我们的角色,伊森,将受你控制,响应你的视线。此外,我们还将开始编写 Unity 脚本。在这个过程中,我们将讨论以下主题:

  • AI(人工智能的缩写)和NavMesh添加到我们的第三人称角色,伊森

  • Unity 编程使用 C#

  • 使用我们的目光移动 3D 光标

  • 以好的效果射击和杀死僵尸伊森

大多数 Unity 开发的入门教程都会让你从简单的东西开始,也许永远不会触及更有趣、更复杂的东西。在本章中,我们将混合事物,带你进入几个不同的 3D 图形话题,其中一些相对高级。如果你是新手,可以把这看作是一个调查教程。尽管如此,我们一步一步地讲解,所以你应该能够跟上,也能有很多乐趣!

伊森,行走者

游戏是虚拟现实的一个常见应用。因此,我们不妨从这里开始,也给我们的角色,伊森,一个自己的生活。好吧,有点(或不)像,因为他将成为一个僵尸!

我们在伊森闲逛的展览中停下来。如果你有一个带有摇杆或触摸板的控制器,你可以让他绕场景跑来跑去,但并非所有 VR 设备都能保证这一点。事实上,如果你用 Google Cardboard 观看场景,你不太可能有一个手持控制器(尽管有蓝牙游戏控制器)。在下一章第五章《便捷交互》中,我们将讨论手持输入控制器。现在,我们将考虑另一种让他移动的方法,即使用你佩戴 VR 头盔时的目光方向。

在我们尝试这个之前,我们首先将伊森变成一个僵尸,让他毫无目的地四处游荡,没有任何用户控制。我们将通过给他一些 AI 并编写一个脚本,将他发送到随机目标位置来实现这一点。

AI 控制器NavMesh是 Unity 中相对高级的话题,但为了好玩,我们将带你进入这个领域。此外,它并不像僵尸那样可怕。

智能化的伊森

首先,我们想要用 Unity 的 AI 角色AIThirdPersonController替换我们最初使用的ThirdPersonController预制体,以下是步骤。Unity 使用“人工智能”这个词比较宽松,指的是“脚本驱动”。执行以下步骤:

  1. 打开上一章中的 Unity 项目,使用Diorama场景,并将Characters包从Standard Assets导入。

  2. 在项目面板中,打开Standard Assets/Characters/ThirdPersonCharacter/Prefabs文件夹,将AIThirdPersonController拖拽到场景中。将其命名为Ethan

  3. 在层次结构面板(或场景中),选择之前的ThirdPersonController(旧的 Ethan)。然后,在检查器面板的变换面板中,选择变换面板右上角的齿轮图标,并选择复制组件。

  4. 选择新的Ethan对象(从层次结构面板或场景中)。然后,在检查器面板的变换面板中,选择齿轮图标并选择粘贴组件值。

  5. 现在,你可以通过从层次结构面板中选择旧Ethan对象,右键点击打开选项,然后点击删除来删除它。

如果你找不到导入的Characters包,你可能没有在安装 Unity 时安装Standard Assets。要获取它们,你现在需要再次运行UnityDownloadAssistant,如第二章开头所述的内容、对象和比例(它可能已经在你的下载文件夹中)。

注意,这个控制器有一个NavMesh Agent组件和一个AICharacterControl脚本。NavMesh Agent有参数用于控制 Ethan 在场景中的移动方式。AICharacterControl脚本需要一个目标对象,Ethan 将走向该对象。让我们按照以下方式填充它:

  1. 在层次结构面板中添加一个空的游戏对象,并将其重命名为WalkTarget

  2. 将其变换值重置为位置(0,0,0)(使用变换面板右上角的齿轮图标)。

  3. 选择 Ethan,将WalkTarget拖拽到检查器面板的 AI 角色控制面板中的目标属性,如图所示:

图片

到目前为止,我们在场景中有一个 AI 角色(Ethan),一个空的游戏对象,它最初将用作导航目标(WalkTarget),位于场景中心,并且我们告诉 AI 角色控制器使用这个目标对象。当我们运行游戏时,无论WalkTarget在哪里,Ethan 都会去那里。但还不是时候。

NavMesh 烘焙

Ethan 不能在没有被告知可以自由漫步的地方随意行走!我们需要定义一个NavMesh——一个简化的几何平面,使角色能够绕过障碍物规划路径。

在我们的场景中,Ethan 是一个代理。他可以行走的地方是navmesh。注意,他有一个NavMesh Agent组件和一个AICharacterControl脚本。NavMesh Agent有参数用于控制 Ethan 在场景中的移动方式。

通过首先识别场景中影响导航的对象,将其标记为导航静态,然后烘焙 NavMesh,如下所示:

  1. 选择导航面板。如果它不是你的编辑器中的一个标签,请通过导航到主菜单中的窗口 | 导航来打开导航窗口。

  2. 选择其对象选项卡。

  3. 在层次结构中选择地面平面,然后在导航窗口的对象面板中,勾选导航静态复选框。(或者,您可以使用对象的检查器窗口静态下拉列表。)

  4. 对应该挡住他的每个对象(立方体和球体)重复步骤 3。以下是对球体的示例。

  5. 在导航窗口中,选择烘焙选项卡,然后点击面板底部的烘焙按钮:

图片

场景视图现在应该显示一个蓝色覆盖层,其中定义了导航网格,如下面的截图所示:

图片

让我们测试一下。确保游戏面板的“播放时最大化”未选中。点击顶部的播放模式按钮(编辑器顶部的三角形)。在层次结构面板中,选择WalkTarget对象,并确保场景面板中的平移操纵杆处于活动状态(按键盘上的 W 键)。现在,将WalkTarget对象上的红色(x)和/或蓝色(z)箭头操纵杆拖动到地板平面上。当你这样做的时候,Ethan 应该会跟随!再次点击播放以停止播放模式。

城市中的随机漫步者

现在,我们将编写一个脚本,将WalkTarget对象移动到随机位置。

编写脚本是使用 Unity 开发的重要组成部分。如果你已经做了更多的事情,比如只是摆弄 Unity,你可能已经编写了一些脚本。我们将使用 C#编程语言。

如果你是编程新手,不要慌张!我们将在本章末尾提供对 Unity 脚本编写的更详细介绍。你现在可以跳到那里,然后再回来,或者只是跟着做。

对于这个第一个脚本,我们将放慢速度。我们将把脚本附加到WalkTarget对象上,如下所示:

  1. 在层次结构面板或场景视图中选择WalkTarget对象。

  2. 在其检查器面板中,点击添加组件按钮。

  3. 选择新脚本(您可能需要向下滚动以找到它)。

  4. 将其命名为RandomPosition

  5. 确保已选择 C Sharp 语言。

  6. 点击创建并添加。

  7. 这应该在WalkTarget对象上创建一个脚本组件。在检查器面板中脚本右侧的槽中双击RandomPosition脚本以在代码编辑器中打开它。

RandomPosition 脚本

我们希望将WalkTarget对象移动到随机位置,这样 Ethan 就会朝那个方向前进,等待几秒钟,然后再次移动WalkTarget对象。这样,他看起来就像是无目的地四处游荡。我们可以通过脚本来实现这一点。而不是逐步开发脚本,我首先展示完成版本,然后我们将逐行分析。RandomPosition.cs脚本看起来像这样:

using UnityEngine; 
using System.Collections; 

public class RandomPosition : MonoBehaviour { 

  void Start () { 
    StartCoroutine (RePositionWithDelay()); 
  } 

  IEnumerator RePositionWithDelay() { 
    while (true) { 
      SetRandomPosition(); 
      yield return new WaitForSeconds (5); 
    } 
  } 

  void SetRandomPosition() { 
    float x = Random.Range (-5.0f, 5.0f); 
    float z = Random.Range (-5.0f, 5.0f); 
    Debug.Log ("X,Z: " + x.ToString("F2") + ", " + 
       z.ToString("F2")); 
    transform.position = new Vector3 (x, 0.0f, z); 
  } 
} 

此脚本定义了一个名为 RandomPositionMonoBehaviour 子类。在定义类时,我们首先声明我们将要使用的任何变量。变量是值的占位符。值可以在这里初始化或在其他地方分配,只要在脚本使用它之前它有一个值。

脚本的主体部分在更下面,名为 SetRandomPosition() 的函数。让我们看看它做了什么。

如果你还记得,GroundPlane 平面是一个 10 单位平方的平面,原点位于中间。因此,平面上任何 (x, z) 位置将在每个轴上 -55 的范围内。这行代码 float x = Random.Range (-5.0f, 5.0f) 在给定的范围内选择一个随机值并将其分配给一个新的 float x 变量。我们以同样的方式获取一个随机的 z 值。(通常,我劝阻使用这种硬编码的常量值而不是使用变量,但为了说明目的,我保持事情简单。)

这行代码 Debug.Log ("X,Z: " + x.ToString("F2") + ", " + z.ToString("F2")) 在游戏运行时会在控制台面板中打印出 xz 的值。它将输出类似 X, Z: 2.33, -4.02 的内容,因为 ToString("F2") 表示四舍五入到两位小数。请注意,我们使用加号将输出字符串的各个部分组合在一起。

我们实际上使用这行代码 transform.position = new Vector3 (x, 0.0f, z); 将目标移动到给定位置。我们正在设置此脚本附加到的对象的变换位置。在 Unity 中,具有 X、Y 和 Z 值的值由 Vector3 对象表示。因此,我们创建一个新的对象,其中包含我们生成的 xz 值。我们给 y=0,这样它就会坐在 GroundPlane 上。

每个 MonoBehaviour 类都有一个内置变量叫做 this,它指向脚本附加到的对象。也就是说,当脚本成为对象的组件并出现在其检查器面板中时,脚本可以将其对象称为 this。实际上,this 是如此明显,如果你想在 this 对象上调用函数,甚至不需要说出来。我们本可以说 this.transform.position = ...,但 this 对象是隐含的,通常省略。另一方面,如果你有其他对象的变量(例如,GameObject that;),那么在设置其位置时,你需要明确指出,就像 that.transform.position = ...

最后一个神秘的部分是如何在 Unity 中处理时间延迟,使用协程。这是一种相对高级的编程技术,但非常实用。在我们的例子中,变换位置应该每五秒改变一次。它分为几个部分来解决:

  1. Start() 函数中,有这行代码 StartCoroutine (RePositionWithDelay());协程 是一段独立于调用它的函数运行的代码。因此,这行代码在协程中启动了 RePositionWithDelay() 函数。

  2. 在其中,有一个 while (true) 循环,正如你可能猜到的,它会永远运行(只要游戏在运行)。

  3. 它调用 SetRandomPosition() 函数,实际上重新定位了对象。

  4. 然后,在这个循环的底部,我们执行一个 yield return new WaitForSeconds (5); 语句,这基本上是告诉 Unity,“嘿,去做你想做的,五秒钟后再回来,这样我就可以再次通过我的循环了”。

  5. 为了使所有这些功能正常工作,RePositionWithDelay 协程必须声明为 IEnumerator 类型(因为文档中是这样说的)。

这种协程/yield 机制,虽然是一个高级编程主题,但在像 Unity 这样的时间切片程序中是一个常见的模式。

我们的脚本应该保存到一个名为 RandomPosition.cs 的文件中。

我们现在可以开始了。在 Unity 编辑器中,点击“播放”。Ethan 正像疯子一样从一个地方跑到另一个地方!

“僵尸化” Ethan!

好吧,这相当随机。让我们调整导航网格驱动参数,让他以一个不错的僵尸步伐慢下来。为此,执行以下步骤:

  1. 在层次面板中选择 Ethan

  2. 导航到检查器 | 导航网格代理 | 驱动并设置以下内容:

    • 速度:0.3

    • 角速度:60

    • 加速度:2

再玩一次。他慢了下来。这样更好。

再来一个细节处理:让我们把他变成一个僵尸。我有一个名为 EthanZombie.png 的纹理图像,这会有所帮助(包含在这本书中)。执行以下步骤:

  1. 从主菜单的“资产”选项卡中选择“导入新资产...”。导航到与这本书一起提供的资产文件文件夹。

  2. 选择 EthanZombie.png 文件。

  3. 点击导入。为了整洁,确保它位于 Assets/Textures 文件夹中。(或者,你只需从 Windows 资源管理器中将文件拖放到项目面板的 Assets/Textures 文件夹中。)

  4. 在层次面板中,展开 Ethan 对象(点击三角形)并选择 EthanBody

  5. 在“检查器”面板中,通过点击 Shader 左侧的三角形图标来展开 EthanGray 着色器。

  6. Project Assets/Textures 文件夹中选择 EthanZombie 纹理。

  7. 将其拖放到 Albedo 纹理贴图上。它是一个位于主贴图标签左侧的小方块。

  8. 在层次面板中,选择 EthanGlasses 并取消选中它,以在检查器面板中禁用眼镜。毕竟,僵尸不需要眼镜!

他的肖像如下所示。你说了什么?这还不够恐怖的僵尸吗?? 好吧,也许他刚刚变成僵尸。你可以继续自己制作一个更好的僵尸。使用 Blender、Gimp 或 Photoshop 画一个自己的(甚至可以导入一个完全不同的僵尸人形模型来替换 EthanBody):

图片

现在,构建项目并在 VR 中尝试。

我们从第三人称视角观察。你可以四处看看,看看发生了什么。这有点有趣,而且相当有趣。而且它是被动的。让我们变得更活跃一些。

看着我所指的方向走

在这个下一个脚本中,我们将不再随机发送 Ethan 到我们看的地方。在 Unity 中,这是通过使用 射线投射 来实现的。这就像从相机发射射线并查看它击中了什么(更多信息,请访问 docs.unity3d.com/Manual/CameraRays.html)。

我们将创建一个新的脚本,它将像之前一样附加到 WalkTarget 上,如下所示:

  1. 在层次结构面板或场景视图中选择 WalkTarget 对象。

  2. 在其检查器面板中,点击添加组件按钮。

  3. 选择新建脚本。

  4. 命名它为 LookMoveTo

  5. 确保已选择 C# 语言。

  6. 点击创建并添加。

这应该在 WalkTarget 对象上创建一个脚本组件。双击它以在您的代码编辑器中打开它。

这个 LookMoveTo 脚本替换了我们之前创建的 RandomPosition 脚本。在继续之前,请禁用 WalkTargetRandomPosition 组件。

LookMoveTo 脚本

在我们的脚本中,每次调用 Update() 时,我们将读取相机指向的位置(通过使用其变换位置和旋转),在该方向发射射线,并让 Unity 告诉我们它击中了地面平面。然后,我们将使用此位置来设置 WalkTarget 对象的位置。

这是完整的 LookMoveTo.cs 脚本:

using UnityEngine; 
using System.Collections; 

public class LookMoveTo : MonoBehaviour { 
  public GameObject ground; 

  void Update () { 
    Transform camera = Camera.main.transform; 
    Ray ray; 
    RaycastHit hit; 
    GameObject hitObject; 

    Debug.DrawRay (camera.position, 
      camera.rotation * Vector3.forward * 100.0f); 

    ray = new Ray (camera.position, 
      camera.rotation * Vector3.forward); 
    if (Physics.Raycast (ray, out hit)) { 
      hitObject = hit.collider.gameObject; 
      if (hitObject == ground) { 
        Debug.Log ("Hit (x,y,z): " + hit.point.ToString("F2")); 
        transform.position = hit.point; 
      } 
    } 
  } 

} 

让我们逐行分析脚本。

public GameObject ground; 

脚本的第一件事是声明一个用于 GroundPlane 对象的变量。

由于它是 public 的,我们可以使用 Unity 编辑器来分配实际的对象:

  void Update () { 
    Transform camera = Camera.main.transform; 
    Ray ray; 
    RaycastHit hit; 
    GameObject hitObject; 

Update() 内部,我们定义了一些局部变量,camerarayhithitObject,这些变量类型是我们将要使用的 Unity 函数所必需的。

Camera.main 是当前活动的相机对象(即标记为 MainCamera)。我们获取其当前变换,并将其分配给相机变量:

    ray = new Ray (camera.position, 
      camera.rotation * Vector3.forward); 

忽略一下方便的 Debug 语句,我们首先使用 new Ray() 确定从相机发出的射线。

一个 射线 可以由 x、y 和 z 空间中的起始位置和一个方向向量定义。一个 方向向量 可以定义为从 3D 起始点到空间中某个其他点的相对偏移。正方向,其中 z 为正,是 (0, 0, 1)。Unity 会为我们做数学运算。因此,如果我们取一个单位向量 (Vector3.forward),将其乘以一个三轴旋转 (camera.rotation),并按长度 (100.0f) 缩放,我们将得到一个指向与相机相同方向的射线,长度为 100 个单位:

if (Physics.Raycast (ray, out hit)) {

然后,我们发射射线并查看是否击中了任何东西。如果是这样,hit 变量将

现在包含更多关于被击中的信息,包括具体的对象在

hit.collider.gameObject。(out 关键字表示 hit 变量的值由 Physics.Raycast() 函数填充。)

      if (hitObject == ground) { 
        transform.position = hit.point; 
      } 

我们检查射线是否击中了 GroundPlane 对象,如果是,我们将将其分配为移动 WalkTarget 对象到 hit 位置的位置。

== 比较运算符不应与 = 混淆,后者是赋值运算符。

此脚本包含两个 Debug 语句,这是在 Play 模式下运行脚本时监控正在发生情况的有用方法。Debug.DrawRay() 将在场景视图中绘制给定的射线,以便您实际上可以看到它,而 Debug.Log() 将在发生碰撞时将当前碰撞位置输出到控制台。

保存脚本,切换到 Unity 编辑器,并执行以下步骤:

  1. 在选择 WalkTarget 后,在 Inspector 面板中,LookMoveTo 脚本组件现在有一个用于 GroundPlane 对象的字段。

  2. 从 Hierarchy 面板中选择并拖动 GroundPlane 游戏对象到 Ground 字段。

保存场景。脚本面板看起来像这样:

图片

然后,点击 Play 按钮。Ethan 应该跟随我们的目光(以他自己的速度)。

在具有多个具有碰撞器的对象的项目中,为了优化射线投射的性能,建议将对象放置在特定的层(例如,命名为 "Raycast")上,然后将该层掩码添加到射线投射调用中。例如,如果 "Raycast" 是层 5,则 int layerMask = 1 << 5,然后 Physics.Raycast(ray, out hit, maxDistance, layerMask);。有关详细信息和方法,请参阅 docs.unity3d.com/ScriptReference/Physics.Raycast.htmldocs.unity3d.com/Manual/Layers.html

添加反馈光标

由于您的目光击中地面平面的位置并不总是显而易见,我们现在将在场景中添加一个光标。这非常简单,因为我们一直在围绕一个不可见的空 WalkTarget 对象移动。如果我们使用以下步骤给它一个网格,它就会变得可见:

  1. 在 Hierarchy 面板中,选择 WalkTarget 对象。

  2. 右键单击鼠标,导航到 3D Object | Cylinder。这将创建一个由 WalkTarget 作为父对象的圆柱形对象。(或者,您也可以使用主菜单栏上的 GameObject 选项卡,然后将对象拖放到 WalkTarget 上。)

  3. 通过在 Transform 窗格中的 gear 图标菜单中点击 Reset,确保我们以 transform 的重置值开始。

  4. 选择新的圆柱体,并在其 Inspector 面板中,将缩放修改为 (0.4, 0.05, 0.4)。这将创建一个直径为 0.4 的扁平圆盘。

  5. 通过取消勾选复选框来禁用其 Capsule Collider。

  6. 作为性能优化,在 Mesh Renderer 中,您还可以禁用 Cast Shadows、Receive Shadows、Use Light Probes 和 Reflection Probes。

现在,再次尝试播放。光标圆盘跟随我们的目光。

如果你愿意,可以用彩色材质更好地装饰这个圆盘。更好的是,找到一个合适的纹理。例如,我们在 第二章,内容、对象和比例中使用了网格纹理 Chapter 2,用于 GridProjector 文件(Standard Assets/Effects/Projectors/Textures/Grid.psd)。CircleCrossHair.png 文件与本书的文件一起提供。将纹理拖放到圆柱形光标上。当你这样做时,将其 Shader 设置为 Standard。

通过障碍物观察

在这个项目中,我们让 Ethan 跟随我们的视线移动,通过从摄像机进行射线投射到地面平面上,并查看它与该平面的交点来确定 WalkTarget 对象的位置。

你可能已经注意到,当我们把目光滑过立方体和球体时,光标似乎会 卡住。这是因为 物理引擎 已经确定了哪个对象首先被击中,从未到达地面平面。在我们的脚本中,我们在移动 WalkTarget 之前有条件语句 if (hitObject == ground)。如果没有它,光标会漂浮在 3D 空间中任何被投射射线击中的对象上。有时,那很有趣,但在这个情况下,它不是。我们希望光标保持在地面。然而现在,如果射线击中的不是地面,它就不会重新定位,看起来 卡住。你能想到一种绕过它的方法吗?这里有一个提示:查看 Physics.RaycastAll。好吧,我会给你展示。用以下代码替换 Update() 的主体:

    Transform camera = Camera.main.transform; 
    Ray ray; 
    RaycastHit[] hits; 
    GameObject hitObject; 

    Debug.DrawRay (camera.position, camera.rotation * 
       Vector3.forward * 100.0f);
    ray = new Ray (camera.position, camera.rotation * 
       Vector3.forward); 
    hits = Physics.RaycastAll (ray); 
    for (int i = 0; i < hits.Length; i++) { 
      RaycastHit hit = hits [i]; 
      hitObject = hit.collider.gameObject; 
      if (hitObject == ground) { 
        Debug.Log ("Hit (x,y,z): " + 
           hit.point.ToString("F2")); 
        transform.position = hit.point; 
      } 
    } 

在调用 RaycastAll 时,我们得到一个列表或数组,其中包含击中项。然后,我们循环

通过每个对象,沿着射线路径寻找任何地方的地面击中

向量。现在我们的光标将沿着地面移动,无论中间是否有其他对象。

额外挑战:另一个更有效率的解决方案是使用 层系统。创建一个新的层,将其分配给平面,并将其作为参数传递给 Physics.raycast()。你能看出为什么这要高效得多吗?

如果眼神可以杀人

我们已经走到这一步了。我们不妨试试杀死 Ethan(哈哈!)以下是这个新特性的规格说明:

  • 通过观察 Ethan,我们的视线射线枪会击中他

  • 当枪击中目标时,会发出火花

  • 被击中 3 秒后,Ethan 被杀死

  • 当他被杀死时,Ethan 会爆炸(我们得到一分)然后他在新位置重生

杀死目标的脚本

这次,我们将通过以下步骤将脚本附加到一个新的空 GameController 对象上:

  1. 创建一个空的游戏对象,并将其命名为 GameController

  2. 使用“添加组件”将其附加一个新的 C# 脚本,命名为 KillTarget

  3. 在 MonoDevelop 中打开脚本。

这是完成后的 KillTarget.cs 脚本:

using UnityEngine; 
using System.Collections; 

public class KillTarget : MonoBehaviour { 
  public GameObject target; 
  public ParticleSystem hitEffect; 
  public GameObject killEffect; 
  public float timeToSelect = 3.0f; 
  public int score; 

  private float countDown; 

  void Start () { 
    score = 0; 
    countDown = timeToSelect; 
  } 
void Update () { 
    Transform camera = Camera.main.transform; 
    Ray ray = new Ray (camera.position, camera.rotation * 
       Vector3.forward); 
    RaycastHit hit; 
    if (Physics.Raycast (ray, out hit) && (hit.collider.gameObject 
       == target)) { 
      if (countDown > 0.0f) { 
        // on target 
        countDown -= Time.deltaTime; 
        // print (countDown); 
        hitEffect.transform.position = hit.point; 
        hitEffect.Play(); 
      } else { 
        // killed 
        Instantiate( killEffect, target.transform.position, 
           target.transform.rotation ); 
        score += 1; 
        countDown = timeToSelect; 
        SetRandomPosition(); 
      } 
    } else { 
      // reset 
      countDown = timeToSelect; 
      hitEffect.Stop(); 
    } 
  } 

  void SetRandomPosition() { 
    float x = Random.Range (-5.0f, 5.0f); 
    float z = Random.Range (-5.0f, 5.0f); 
    target.transform.position = new Vector3 (x, 0.0f, z); 
  } 
} 

让我们来看一下。首先,我们声明了一些公共变量,如下所示:

  public GameObject target; 
  public ParticleSystem hitEffect; 
  public GameObject killEffect; 
  public float timeToSelect = 3.0f; 
  public int score; 

就像我们在之前的LookMoveTo脚本中所做的那样,我们的目标是 Ethan。我们还添加了一个hitEffect粒子发射器、一个killEffect爆炸效果,以及倒计时计时器的起始值timeToSelect。最后,我们将在score变量中跟踪我们的击杀数。

Start()方法在游戏开始时被调用,将分数初始化为零并将countDown计时器设置为起始值。

然后,在Update()方法中,就像在LookMoveTo脚本中一样,我们从相机发射一条射线并检查它是否击中我们的目标 Ethan。当它击中时,我们检查countDown计时器。

如果计时器仍在计数,我们将使用Time.deltaTime减去自上次调用Update()以来经过的时间量,并确保hitEffect在击中点发射。

如果射线仍在目标上且计时器已计数完成,Ethan 将被杀死。我们将爆炸,将分数增加一分,重置计时器到起始值,并将 Ethan 移动(重生)到随机的新位置。

对于爆炸效果,我们将使用 Unity 的ParticleSystems包中找到的标准资产之一。要激活它,killEffect应设置为名为Explosion的预制件。然后,脚本会实例化它。换句话说,它使其成为场景中的一个对象(在指定的变换处),从而启动其惊人的脚本和效果。

最后,如果射线没有击中 Ethan,我们重置计数器并关闭粒子。

保存脚本并进入 Unity 编辑器。

额外挑战:重构脚本以使用协程来管理延迟时间,就像我们在本章开头RandomPosition脚本中所做的那样。

添加粒子效果

现在,为了填充public变量,我们将执行以下步骤:

  1. 首先,我们需要 Unity 标准资产中包含的ParticleSystems包。如果您没有它们,请导航到资产 | 导入包 | 粒子系统,选择全部,然后点击导入。

  2. 从层级面板中选择GameController并转到检查器面板中的击杀目标(脚本)面板。

  3. 从层级面板中将Ethan对象拖动到目标字段。

  4. 从主菜单栏中,导航到 GameObject | Effects | 粒子系统并将其命名为SparkEmitter

  5. 重新选择GameController并将SparkEmitter拖动到击杀效果字段。

  6. 在项目面板中,找到Assets/Standard Assets/ParticleSystems/Prefabs中的Explosion预制件,并将其拖动到击杀效果字段。

脚本面板看起来如下截图所示:

图片

我们创建了一个默认的粒子系统,它将用作火花发射器。我们需要将其设置为我们喜欢的样子。我会帮你开始,然后你可以按需进行操作,如下所示:

  1. 从层级面板中选择SparkEmitter

  2. 在其检查器面板中,在粒子系统下设置以下值:

    • 开始大小:0.15

    • 开始颜色:选择红色/橙色

    • 开始寿命:0.3

    • 最大粒子数:50

  3. 在发射下,设置随时间变化的速率:100

  4. 在形状下,设置形状:球体和半径:0.01

这是我运行播放模式并击中伊森胸部的场景视图的样子:

当伊森被射击时,hitEffect 粒子系统被激活。在 3 秒(或你在 TimeToSelect 变量中设置的任何值)后,他的 健康值 被耗尽,爆炸效果被实例化,分数增加,并在新位置重生。在 第六章,世界空间 UI 中,我们将看到如何向玩家显示当前分数。

清理

在我们完成之前,还有最后一件事:让我们清理一下 Assets 文件夹,并将所有脚本移动到 Assets/Scripts/ 子文件夹中。在项目中选择项目资产文件夹,创建一个文件夹,命名为 Scripts,并将所有脚本拖放到其中。

Unity C# 编程简介

正如我们刚才看到的,Unity 做了很多事情:它管理对象、渲染它们、动画化它们、计算这些对象的物理属性,等等。Unity 本身是一个程序。它由代码组成。可能是由一些非常聪明的人编写的大量优质代码。作为游戏开发者,你可以通过我们之前已经使用过的 Unity 编辑器点选界面来访问这些内部 Unity 代码。在 Unity 编辑器中,脚本表现为可配置的组件。然而,它也通过 Unity 脚本 API 直接为你提供了更直接的访问方式。

API(即 应用程序编程接口),指的是你可以从自己的脚本中访问的已发布软件功能。Unity 的 API 非常丰富且设计良好。这也是人们为 Unity 编写了许多惊人的应用程序和插件的原因之一。

世界上有许多编程语言。Unity 选择支持来自微软的 C# 语言。计算机语言有特定的语法必须遵守。否则,计算机将无法理解你的脚本。在 Unity 中,脚本错误(和警告)会在编辑器的控制面板以及应用程序窗口的底部页脚中显示。

Unity 的默认脚本编辑器是一个集成开发环境,或 IDE,称为 MonoDevelop。如果你想配置不同的编辑器或 IDE,比如微软的 Visual Studio,你也可以。MonoDevelop 有一些很好的功能,如自动完成和弹出帮助,这些功能理解 Unity 文档。C# 脚本是带有 .cs 扩展名的文本文件。

在 Unity C# 脚本中,一些单词和符号是 C# 语言本身的一部分,一些来自微软的 .NET 框架,还有一些是由 Unity API 提供的。然后还有你编写的代码。

一个空的默认 Unity C# 脚本看起来像这样:

using UnityEngine; 
using System.Collections; 

public class RandomPosition : MonoBehaviour { 

  // Use this for initialization 
  void Start () { 

  } 

  // Update is called once per frame 
  void Update () { 

  } 
}

让我们分解一下。

前两行表明这个脚本需要一些其他东西才能运行。using关键字属于 C#语言。using UnityEngine这一行表示我们将使用UnityEngineAPI。using System.Collections这一行表示我们可能还会使用名为Collections的函数库来访问对象列表。

在 C#中,每行代码都以分号结束。双斜杠//表示代码中的注释,从那里到行尾的内容将被忽略。

这个 Unity 脚本定义了一个名为RandomPosition的类。就像带有自己属性(变量)和行为(函数)的代码模板。从MonoBehaviour基类派生的类会被 Unity 识别并在游戏运行时使用。例如,在本章顶部编写的第一个脚本中,public class RandomPosition : MonoBehaviour这一行基本上表示“我们正在定义一个名为RandomPosition的新公共类”,它继承自MonoBehaviourUnity 基类,包括Start()Update()函数的能力。类的主体被一对花括号{}包围。

当某个东西是public时,它可以从这个特定脚本文件外的其他代码中看到。当它是private时,它只能在这个文件内被引用。我们希望 Unity 能看到RandomPosition类。

类定义变量和函数。一个变量持有特定类型的数据值,例如floatintbooleanGameObjectVector3等。函数实现逻辑(逐步指令)。函数可以接收参数——代码中括号内的变量,并在完成后返回新的值。

在 C#中,例如5.0f这样的数值float常量需要在末尾加上f以确保

数据类型是一个简单的浮点值,而不是一个双精度浮点值。

如果你定义了特殊函数,Unity 会自动调用一些。Start()Update()是两个例子。默认的 C#脚本提供了这些函数的空版本。函数前面的数据类型表示返回值的类型。Start()Update()不返回值,所以它们是void

在游戏开始之前,所有MonoBehaviour脚本中的Start()函数都会被调用。这是一个初始化数据的好地方。所有的Update()函数在游戏运行期间的每个时间片或帧中都会被调用。这是大多数动作发生的地方。

一旦你在 MonoDevelop 或 Visual Studio 编辑器中编写或修改了一个脚本,请保存它。然后,切换到 Unity 编辑器窗口。Unity 会自动识别脚本已更改并将重新导入它。如果发现错误,它将在Console面板中立即报告。

这只是对 Unity 编程的简要介绍。随着我们在本书的项目中工作,我会介绍更多的内容。

摘要

在本章中,我们探讨了 VR 相机与场景中物体之间的关系。我们首先让 Ethan(僵尸)在场景中随机行走,并使用 NavMesh 使他能够移动,但随后我们通过在 x、z 地面平面上使用 3D 光标来引导他的漫步。这个光标随着我们在虚拟现实中环顾四周而跟随我们的视线。最后,我们还使用我们的视线向 Ethan 发射一束光线,导致他失去健康并最终爆炸。

这些基于外观的技术可以用于非 VR 游戏,但在 VR 中,它们非常常见,几乎是必需的。我们也会在本书的后续章节中更多地使用它们。

在下一章中,我们将使用我们的手与虚拟场景进行交互。我们将学习 Unity 输入事件,以及 SteamVR、Oculus 和 Windows Mixed Reality 等输入系统。由于这可能会变得复杂,我们将编写我们自己的 VR 输入事件系统,以保持我们的应用程序独立于特定的 VR 设备。

第五章:方便的交互式组件

你身处一个充满这些酷炫东西的虚拟世界;我们的天性就是试图伸手触摸某物。正如我们在上一章中看到的,基于注视的选择是交互虚拟场景的一个良好开端,但大多数人直观地想要用手操作。大多数 VR 设备都提供手柄控制器来选择、抓取和与场景中的虚拟对象交互。

在本章中,我们将介绍在 Unity 中捕获用户输入的实践,说明如何在简单的 VR 场景中使用它们。每个人都喜欢气球,所以在这个项目中我们将制作气球。我们甚至可能会弄爆几个。我们将从上一章继续,使用 C#编程进行基本脚本编写,并探讨几个软件设计模式用于用户输入。我们将讨论以下主题:

  • 轮询输入设备数据

  • 使用可脚本数据对象存储和检索输入状态

  • 调用和订阅输入事件

  • 使用设备特定 Unity 包提供的交互式组件

在本章中,我们将学习的一个重要教训是,处理 VR 应用程序的用户输入并不只有一种方式。甚至没有一种最佳方式。Unity 包括处理用户输入和对象之间一般消息传递的几种机制。VR 设备制造商为其 SDK 提供自己的输入控制器对象和脚本。

此外,VR 制造商和其他人提供了方便的框架工具包,包含高级组件和预制件。我们建议你熟悉为你目标设备提供的工具包。研究演示场景,看看组件是如何工作的以及它们的推荐实践,就像我们在本章末尾将要做的那样。

话虽如此,在本章中,我们将从非常简单的按钮按下输入开始,并由此展开,展示各种设计模式。你并不总是想自己从头开始,但你应该了解事物是如何工作的。

设置场景

为了开始我们对于输入机制的探索,让我们设置我们的场景。计划是让玩家创建气球。每个人都喜欢气球!

对于这个场景,你可以从一个新场景(文件 | 新场景)开始,然后添加我们在上一章中构建的 MyMyselfEye 预制件。相反,我决定从上一章中创建的“展览馆”场景开始,并移除除了地面平面和照片平面之外的所有对象,如下所示:

  1. 打开“展览馆”场景

  2. 移除所有对象,除了 MyMyselfEye、方向光、地面平面和照片平面

  3. 将 MeMyselfEye 放置在场景原点,位置(0, 0, 0)

  4. 选择文件 | 保存场景为,并给它起一个名字,例如“气球”

创建气球

对于气球,如果你选择,可以简单地使用标准的 Unity 球体 3D 原语。或者你可以在 Unity 资产商店或其他地方找到一个对象。我们使用了一个在 Google Poly 上找到的低多边形气球对象(poly.google.com/view/a01Rp51l-L3),并且它提供了本章的下载文件。

无论哪种方式,请将对象设置为父对象,使其原点(旋转点)位于底部,如下所示:

  1. 在层次结构中创建一个空对象(创建 | 创建空对象)并将其命名为“气球”。

  2. 重置其变换(变换 | 齿轮图标 | 重置),然后将其定位在(0,1,1)。

  3. 将气球预制件拖入层次结构,作为气球(我的位于Assets/Poly/Assets/文件夹)的子对象。

  4. 如果你没有气球模型,请使用球体(创建 | 3D 对象 | 球体)。并添加一个材质,如我们在上一章中创建的“蓝色材质”。

  5. 将子对象的“位置”设置为(0,0.5,0),使其原点(旋转点)在从父对象引用时位于底部。

场景应该看起来像这样:

图片

使其成为预制件

我们的目的是在玩家按下控制器上的按钮时从预制件实例化新的气球。当按钮释放时,气球被释放并飘走。

让我们在场景中调整气球的大小和位置,使其在起始大小和工作距离内。我们还将通过添加RigidBody组件来给它一些物理属性:

我们在第八章中更详细地讨论了刚体和 Unity 物理。

  1. 在层次结构中选择你的气球对象

  2. 在检查器中,将其“变换缩放”设置为(0.1,0.1,0.1)

  3. 将其位置设置为(0,1,1)

  4. 使用“添加组件”添加一个刚体

  5. 取消选中“使用重力”复选框

我的气球对象现在有以下属性:

图片

按以下方式将其设置为预制件:

  1. 将气球对象拖入你的Prefabs/文件夹以使其成为预制件对象

  2. 从你的层次结构中删除原始气球对象

好的。现在让我们来玩控制器按钮。

如果你想要修改一个预制件,将其实例拖回场景中。进行你想要的更改。然后,使用应用按钮将更改保存回对象的预制件。如果场景中不再需要临时实例,请从层次结构中删除它。

基本按钮输入

Unity 包括一个标准的输入管理器,用于访问传统的游戏控制器、键盘、鼠标和移动触摸屏输入。这可以包括特定的按钮按下、摇杆轴和设备加速度计等。它还支持来自 VR 和 AR 系统的输入。

输入管理器在物理输入设备之上提供了一个抽象层。例如,您可以引用逻辑输入,例如映射到物理按钮的 Fire1 按钮。您可以在编辑 | 项目设置 | 输入中设置和修改项目的映射。

要了解 Unity 输入管理器的概述和详细信息,请参阅 docs.unity3d.com/Manual/ConventionalGameInput.html。有关输入类的脚本编写,请参阅 docs.unity3d.com/ScriptReference/Input.html。各种 VR 设备的输入映射可以在 docs.unity3d.com/Manual/vr-input.html 找到。

让我们看看。首先,我们将编写一个测试脚本以获取特定按钮状态并查看 Unity 输入类的工作方式。一个常见的逻辑按钮是名为 "Fire1" 的按钮。让我们看看您的输入设备为 "Fire1" 使用哪个按钮。

使用 Fire1 按钮

我们现在将编写一个名为 MyInputController 的脚本,以检测用户是否按下了 Fire1 按钮。按照以下方式将脚本添加到您的 MeMyselfEye 对象中:

  1. 在 Hierarchy 中选择 MyMyselfEye 对象

  2. 在 Inspector 中点击添加组件,然后新建脚本

  3. 将其命名为 MyInputController 并按创建和添加

  4. 双击 MyInputController 脚本以打开它进行编辑

按照以下方式编辑脚本:

public class MyInputController : MonoBehaviour 
{
  void Update () 
  {
    ButtonTest();
  }

  private void ButtonTest()
  {
    string msg = null;

    if (Input.GetButtonDown("Fire1"))
      msg = "Fire1 down";

    if (Input.GetButtonUp("Fire1"))
       msg = "Fire1 up";

    if (msg != null)
      Debug.Log("Input: " + msg);
  }
}

在此脚本中,在每一帧的 Update 中,我们调用一个私有函数 ButtonTest。此函数构建一个名为 msg 的消息字符串,报告 Fire1 按钮是否刚刚被按下或释放。例如,调用 Input.GetButtonDown("Fire1") 将返回一个布尔值(true 或 false),我们在 if 语句中检查它。当这些情况中的任何一个为真时,msg 字符串不为空(null)并将其打印到 Unity 控制台窗口:

  1. 在 Unity 编辑器中按播放键以运行场景

  2. 当您按下输入控制器上的 Fire1 按钮时,您将看到输出消息 Input: Fire1 down

  3. 当您释放 Fire1 按钮时,您将看到 Input: Fire1 up 消息,如下所示:

您甚至可以使用此脚本来识别您的输入控制器上的哪个物理按钮映射到逻辑 Fire1 按钮。例如,使用 OpenVR,Fire1 按钮是通过 Vive 控制器的菜单按钮或 Oculus Touch 控制器的 B 按钮触发的 ("Button.Two"),如 Unity 输入系统映射部分所示,该部分位于 Unity 手册的 OpenVR 控制器输入页面中 (docs.unity3d.com/Manual/OpenVRControllers.html)。您可以自由地尝试使用其他逻辑输入名称,以及/或通过输入项目设置(编辑 | 项目设置 | 输入)修改映射。

除了使用 Unity 逻辑输入外,通过 SDK 组件直接访问设备也是常见的。让我们在下一节中探讨这一点。

OpenVR 触发按钮

如果你有一个由 OpenVR 支持的 VR 设备(HTC Vive、Oculus Rift 或 Windows MR),让我们修改ButtonTest函数以检查触发按钮的拉动和释放。

要实现这一点,我们需要为我们想要查询的特定输入组件提供我们的脚本。在 OpenVR 中,这由SteamVR_TrackedObject组件表示,如下面的脚本变体所示:

public class MyInputController : MonoBehaviour 
{
  public SteamVR_TrackedObject rightHand;

  private SteamVR_Controller.Device device;

  void Update () 
  {
    ButtonTest();
  }

  private void ButtonTest()
  {
    string msg = null;

    // SteamVR
    device = SteamVR_Controller.Input((int)rightHand.index);
    if (device != null &&  
      device.GetPressDown(SteamVR_Controller.ButtonMask.Trigger))
    {
      msg = "Trigger press";
      device.TriggerHapticPulse(700);
    }
    if (device != null &&  
      device.GetPressUp(SteamVR_Controller.ButtonMask.Trigger)) 
    {
      msg = "Trigger release";
    }

    if (msg != null)
      Debug.Log("Input: " + msg);
  }
}

保存此脚本后,我们需要填充rightHand变量:

  1. 在 Unity 中,选择 MeMyselfEye 以便可以在检查器中看到 My Input Controller

  2. 在层次结构中展开[CameraRig]对象

  3. 点击控制器(右侧)子对象并将其拖动到检查器中 My Input Controller 的右手槽位

给定rightHand对象,我们直接引用其SteamVR_TrackedObject组件。在ButtonTest函数中,我们使用右手设备的设备 ID(rightHand.index)获取设备数据,并检查特定于触发器的按下状态。作为额外的好处,我已经向你展示了如何在按下触发器时在设备上提供触觉蜂鸣脉冲。

现在当你按下播放时,拉动控制器触发器将被识别。

使用这样的 SDK 组件,你可以访问 Unity 输入管理器不支持的其他特定于设备的输入。一些控制器上的触发器不仅仅是按下/未按下,还可以返回一个百分比,表示为介于 0.0 和 1.0 之间的值。另一个例子是 Oculus Touch 控制器和其他控制器上的触控敏感握把、按钮和拇指盘。

尝试修改脚本以识别控制器Grip按钮或其他输入。提示:尝试SteamVR_Controller.ButtonMask.Grip

Daydream 控制器点击

Google Daydream VR 在 Android 上默认可能不会对Fire1事件做出响应。以下代码显示了如何直接访问控制器点击:

  private void ButtonTest()
  {
    string msg = null;

    if (GvrControllerInput.ClickButtonDown)
      msg = "Button down";
    if (GvrControllerInput.ClickButtonUp)
      msg = "Button up";

    if (msg != null)
      Debug.Log("Input: " + msg);
  }
}

在这种情况下,我们调用GvrControllerInput类的静态函数ClickButtonDownClickButtonUp。没有必要识别特定的控制器对象,因为GvrControllerInput是一个单例。这就是为什么我们可以保证在场景中只有一个实例,因此我们可以直接引用其数据。这在 Daydream 中是有意义的,因为那里只有一个手柄控制器,而在 OpenVR 中则有两个。

检测点击

获取用户输入的最简单方法就是从输入组件中获取当前数据。我们已经使用 Input 类和 VR SDK 看到了这一点。目前,我们将编写自己的输入组件,将 Unity(或 SDK)输入映射到我们自己的简单 API MyInputController中。然后,我们将编写一个BalloonController来轮询输入,如图所示:

图片

我们自己的按钮接口函数

你可能还记得MeMyselfEye玩家装置可能具有特定 VR SDK 的设备特定工具箱子对象。例如,OpenVR 的版本有他们的[CameraRig]预制件。Daydream 的版本有 Daydream Player 预制件。将我们的MyInputController组件添加到MeMyselfEye中是有意义的,因为它可能需要执行设备特定的 SDK 调用。这样,如果你想要维护适用于各种平台的相机装置预制件,并在为不同的 VR 目标构建项目时进行交换,那么公开给应用程序其余部分的 API 将是统一的,并且独立于特定的设备工具箱。

我们输入控制器将公开两个自定义 API 函数,ButtonDownButtonUp。这些函数的实现将隐藏在调用它们的组件中。例如,我们可以将其编写为首先处理Fire1按钮按下,但稍后将其更改为使用触发器按下,或者为 Daydream 创建一个不使用Fire1的版本。让我们通过添加以下代码来更新MyInputController

public bool ButtonDown()
{
  return Input.GetButtonDown("Fire1");
}

public bool ButtonUp()
{
  return Input.GetButtonUp("Fire1");
}

或者,你可以修改前面的代码以使用适合你的按钮界面。例如,对于 Daydream,你可能可以使用以下代码代替:

public bool ButtonDown()
{
  return GvrControllerInput.ClickButtonDown;
}

public bool ButtonUp()
{
  return GvrControllerInput.ClickButtonUp;
}

现在,我们将使用我们的ButtonUp/ButtonDown输入 API。

创建和释放气球

让我们现在创建一个BalloonController,它将是创建和控制气球的程序组件。它将引用我们的MyInputController。按照以下步骤操作:

  1. 在层次结构中创建一个空的游戏对象,重置其变换,并将其命名为BalloonController

  2. 在名为BalloonController的对象上创建一个新的脚本,并按以下方式打开它进行编辑:

public class BalloonController : MonoBehaviour 
{
  public GameObject meMyselfEye;

  private MyInputController inputController;

  void Start () 
  {
    inputController = meMyselfEye.GetComponent<MyInputController>();
  }

  void Update () 
  {
    if (inputController.ButtonDown())
    {
      NewBalloon()
    }
    else if (inputController.ButtonUp())
    {
      ReleaseBalloon();
    }
    // else while button is still pressed, grow it
 }

这是控制器的骨架。给定MeMyselfEye对象的引用,Start()函数获取其MyInputController组件并将其分配给inputController变量。

Update()在游戏运行时每帧被调用。它将调用inputController.ButtonDownButtonUp以查看用户是否更改了输入,并根据响应创建或释放气球。我们将在下面编写这些函数。

注意,我们还在注释中包含了一个占位符,我们将在这里添加GrowBalloon函数。

给定气球预制件,BalloonController可以通过调用 Unity 的Instantiate函数在我们的场景中创建它的新实例。在你的控制器类顶部添加以下public变量声明以用于气球预制件:

 public GameObject balloonPrefab;

然后添加一个private变量来保存当前气球的实例:

 private GameObject balloon;

现在,当玩家按下按钮时调用的NewBalloon函数引用预制件,并按以下方式实例化:

 private void NewBalloon()
 {
   balloon = Instantiate(balloonPrefab);
 }

当玩家释放按钮时,会调用ReleaseBalloon函数。它将对气球施加一个轻微的向上力,使其向天空飘浮。我们将定义一个floatStrength变量并将其应用于对象的 RigidBody(Unity 物理引擎和 RigidBodies 将在后面的章节中解释):

 public float floatStrength = 20f;

然后,

private void ReleaseBalloon()
{
  balloon.GetComponent<Rigidbody>().AddForce(Vector3.up * floatStrength);
  balloon = null;
}

注意,我们还清除了气球变量(将其设置为 null),为下一次按钮按下做好准备。

保存文件并在 Unity 中:

  1. 将 Hierarchy 中的 MeMyselfEye 对象拖动到 Inspector 中 BalloonController 的 Me Myself Eye 插槽

  2. 将 Balloon 预制件从项目的 Assets 文件夹拖动到 Inspector 中 BalloonController 的 Balloon Prefab 插槽

当你准备好时,按 Play。在 VR 中,当你按下 Fire1 按钮(或你编程的任意按钮)时,会实例化一个新的气球。当你释放它时,气球会向上漂浮。在下面的游戏窗口中,我已经连续多次按下按钮,创建了一系列气球:

图片

这是相同游戏状态的 Hierarchy,显示了 Hierarchy 中的克隆气球(我的预制件名称为 Balloon-poly):

图片

按下时充气气球

下一步,我们想在按下按钮的同时充气气球。我们可以通过检查是否有来自按钮按下的当前气球实例,并在每次更新时通过指定的增长率修改其缩放来实现。让我们首先定义它,以便在按钮按下的每秒增长 150%(1.5 倍):

public float growRate = 1.5f;

现在,修改 Update 函数,添加一个第三层的 else if 条件,如下所示:

  else if (balloon != null)
  {
    GrowBalloon();
  }

并像这样添加 GrowBalloon 函数:

 private void GrowBalloon()
 {
   balloon.transform.localScale += balloon.transform.localScale * growRate * Time.deltaTime;
 }

GrowBalloon 函数将通过当前大小的百分比修改气球的本地区域缩放。growRate 是每秒的增长率。因此,我们将其乘以当前帧中的当前分数秒(Time.deltaTime)。

在 Unity 中按 Play。当你按下控制器按钮时,你会创建一个气球,气球会继续充气,直到你释放按钮。然后气球会漂浮起来。哇,这实际上很有趣!

接下来,我们将重构我们的代码,使用不同的软件模式来获取用户输入,使用可脚本化对象。

没有人编写代码而不期望对其进行更改。编程是一种动态的艺术,因为你需要重新思考如何做事,随着需求增长和问题解决,这些更改并不一定是为了添加新功能或修复错误,而是为了使代码更干净、更易于使用和维护。这被称为 重构,当你更改或重写程序的一部分,但并不一定改变玩家视角中的功能工作方式时。

使用可脚本化对象进行输入

在这个例子中,我们将使用一种称为 可脚本化 对象 的技术进一步解耦我们的应用程序与底层输入设备。这些是用于存储信息的数据对象,例如游戏状态、玩家偏好或任何其他非图形数据。可脚本化对象在运行时实例化,就像 MonoBehaviour 一样,但它们不位于 Hierarchy 中,没有 Transform,也没有其他物理和渲染行为。

将可脚本化对象视为项目中的数据容器是有用的。

在之前的实现中,BalloonController需要一个对MeMyselfEye对象的引用来使用其MyInputController组件。尽管输入控制器组件可以让你与底层的 SDK 调用分离,但如果你要修改应用程序以使用不同的MeMyselfEye(例如,从 OpenVR 到 Daydream),你需要找到并替换场景中所有对MeMyselfEye的引用,并将它们替换为新的对象。这里将输入控制器填充到一个可脚本化对象中,然后我们的BalloonController引用该对象的数据,如图所示:

实现脚本化对象的过程比具有组件的游戏对象稍微复杂一些。但并不多。让我们开始吧!

请记住,这只是一个如何使用可脚本化对象的例子,同时也是 Unity 中这种强大设计模式的入门介绍。更多信息,请参阅 Unity 教程 Introduction to scriptable objects 在unity3d.com/learn/tutorials/modules/beginner/live-training-archive/scriptable-objects。另外,参见第九章,探索交互空间,了解另一个在项目中使用可脚本化对象进行数据管理的例子。

创建脚本化对象

在这个例子中,我们的对象将只有一个变量,用于当前按钮动作。我们将说按钮可以有三个可能的值:PressedDownReleasedUpNone。我们将动作定义为在当前Update期间发生,然后清除为None。也就是说,我们不是记录当前按钮状态(例如,是否按下),而是捕获当前按钮动作(刚刚按下),以与其他章节中的示例保持一致。

将可脚本化对象保存在项目Assets中的单独文件夹中是有用的:

  1. 项目窗口中,在Assets下创建一个名为ScriptableObjects的新文件夹

  2. 在新文件夹中,右键单击并选择创建 | C# 脚本

  3. 将脚本命名为MyInputAction

  4. 然后,打开MyInputAction.cs脚本进行编辑

按照以下方式编辑MyInputAction.cs脚本:

[CreateAssetMenu(menuName = "My Objects/Input Action")]
public class MyInputAction : ScriptableObject {
  public enum ButtonAction { None, PressedDown, ReleasedUp };
  public ButtonAction buttonAction;
}

我们不会从MonoBehaviour继承,而是将类定义为ScriptableObject。我们使用枚举来表示动作,以限制其可能的值到选择列表。

enum关键字用于声明枚举,这是一个由称为枚举列表的命名常量集合组成的独特类型。” - docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/enum

注意前面脚本的第一行。我们提供了一个属性属性,它会在 Unity 编辑器中为我们的对象生成一个菜单项。由于脚本化对象不会被添加到场景层次结构中,我们需要一种在项目中创建它们的方法。使用此属性使其变得容易,如下所示:

  1. 保存脚本并返回 Unity。

  2. 在 Unity 编辑器主菜单中,导航到资产 | 创建。

  3. 你将看到一个名为 My Objects 的新项目,其中包含一个子菜单,有一个名为 Input Action 的项目,正如我们在脚本中的CreateAssetsMenu属性属性中指定的那样。菜单如下所示。

  4. 选择输入动作以创建一个实例。默认情况下,它将在当前选定的项目资产文件夹中创建。所以如果你打开了ScriptableObjects文件夹,它将在那里创建。

  5. 将对象重命名为My Input Action Data

图片

如果你选择ScriptableObjects/文件夹中的新My Input Action Data对象,你可以在检查器中看到其属性。在下面的屏幕截图中,我已点击按钮动作下拉列表以显示我们在代码中指定的可能枚举值:

图片

填充输入动作对象

下一步是将MyInputController.cs更改为填充输入数据对象,使用对对象的引用,如下所示:

public MyInputAction myInput;

void Update () 
{
  if (ButtonDown())
  {
    myInput.buttonAction = MyInputAction.ButtonAction.PressedDown;
  } else if (ButtonUp())
  {
    myInput.buttonAction = MyInputAction.ButtonAction.ReleasedUp;
  } else
  {
    myInput.buttonAction = MyInputAction.ButtonAction.None;
  }
}

脚本使用自己的ButtonDownButtonUp函数来设置buttonAction为适当的值。这些甚至可以从公共更改为私有以进一步封装。

保存脚本。然后在 Unity 中:

  1. 在层次结构中选择MeMyselfEye对象

  2. 在你的ScriptableObjects文件夹中找到 My Input Action Data 对象

  3. 将它拖放到 My Input Controller (Script)组件的 My Input 槽中,如下所示,这是我 Steam 版本的MeMyselfEye

图片

访问输入动作对象

现在BalloonController可以访问输入数据对象,而不是MeMyselfEye。否则非常相似,只是一个简单的重构。按照以下方式修改BalloonController.cs

首先,我们可以从BalloonController中移除对MeMyselfEye的所有引用,包括公共变量和整个Start()函数(我们不需要GetComponent<MyInputController>)。

为输入数据对象添加一个变量:

public MyInputAction myInput;

并在Update条件中引用它:

void Update()
{
  if (myInput.buttonAction == MyInputAction.ButtonAction.PressedDown)
  {
    NewBalloon();
  }
  else if (myInput.buttonAction == MyInputAction.ButtonAction.ReleasedUp)
  {
    ReleaseBalloon();
  }
  else if (balloon != null)
  {
   GrowBalloon();
  }
}

保存脚本。然后在 Unity 中,就像我们之前对MyInputController所做的那样:

  1. 在层次结构中选择 BalloonController 对象

  2. 在你的ScriptableObjects文件夹中找到 My Input Action Data 对象

  3. 将它拖放到 Balloon Controller 组件的 My Input 槽中

播放。应用应该和之前一样工作。按按钮创建气球,按住按钮使其膨胀,释放按钮释放气球。

使用可脚本化对象进行模拟测试

这种架构的一个有趣的优势是它如何促进测试。由于我们的应用程序对象与输入设备完全解耦,我们可以模拟输入动作而不实际使用物理输入控制器。例如,尝试这样做:

  1. 在层次结构中选择 MeMyselfEye。然后在检查器中,通过取消选中复选框暂时禁用My Input Controller组件。

  2. 在项目ScriptableObjects/文件夹中选择 My Input Action Data 对象

  3. Play

  4. 当游戏运行时,在检查器中,将按钮动作从 None 更改为 PressedDown。

  5. BalloonController 认为发生了 PressedDown 动作。它创建了一个新的气球并开始充气。

  6. 在检查器中,将输入动作更改为 PressedUp。

  7. BalloonController 检测到发生了PressedUp动作,并释放了当前气球。

测试完成后,别忘了重新启用输入控制器组件!

这种为开发和测试手动设置对象状态的方法可以非常有帮助,尤其是在你的项目增长并变得更加复杂时。

使用 Unity 事件进行输入

我们将使用 Unity 事件探索的第三个软件模式。事件允许将事件源与事件消费者解耦。基本上,事件是一个消息系统,其中一个对象触发事件。项目中的任何其他对象都可以监听该事件。它可以为事件发生时调用的特定函数进行订阅。

你可以通过 Unity 检查器使用拖放来设置此功能。或者你可以在脚本中订阅监听函数。在这个例子中,我们将最小化脚本的使用,并使用 Unity 编辑器来订阅事件。

事件是一个非常丰富的主题,我们只能在这里简要介绍。有关使用 Unity 事件的更多信息,网上有很多很好的参考资料,包括 Unity 教程unity3d.com/learn/tutorials/topics/scripting/eventsunity3d.com/learn/tutorials/topics/scripting/events-creating-simple-messaging-system

以下图表说明了我们的输入控制器(调用事件)和气球控制器(订阅事件)之间的关系:

重要的是要注意,与普通事件不同,这些事件不需要取消订阅。

如果你是一名开发者并且熟悉.NET,了解 Unity 事件是委托可能会有所帮助。正如 Unity 手册中解释的,“UnityEvents 可以添加到任何 MonoBehaviour 中,并且可以从代码中以标准.NET 委托的方式执行。当 UnityEvent 添加到 MonoBehaviour 时,它会在检查器中显示,并且可以添加持久回调。”

调用我们的输入动作事件

为了使用事件实现我们的示例,我们首先让MyInputController在按钮按下时触发一个事件,在按钮释放时触发另一个事件。

首先,在脚本的顶部,我们需要声明我们正在使用 Unity 事件 API。然后,我们声明将要调用的两个 UnityEvents。Update()函数只需要在事件发生时调用其中一个事件即可。

整个MyInputController.cs如下所示:

using UnityEngine;
using UnityEngine.Events;

public class MyInputController : MonoBehaviour 
{
 public UnityEvent ButtonDownEvent;
 public UnityEvent ButtonUpEvent;

 void Update()
 {
   if (ButtonDown())
     ButtonDownEvent.Invoke();
   else if (ButtonUp())
     ButtonUpEvent.Invoke();
 }

 private bool ButtonDown()
 {
   return Input.GetButtonDown("Fire1");
 }

 private bool ButtonUp()
 {
   return Input.GetButtonUp("Fire1");
 }
}

这就是等式这一边的全部内容。

订阅输入事件

使用事件,BalloonController不需要在每一帧更新时检查输入动作。所有这些条件逻辑都可以绕过。相反,我们将拖放组件以将它们订阅到事件。现在Update函数只需要在实例化后增长气球。

整个BalloonController.cs现在看起来像这样。除了代码更少之外,请注意,我们将NewBalloonReleaseBalloon函数从private更改为public,这样我们就可以在检查器中引用它们:

public class BalloonController : MonoBehaviour
{
  public GameObject balloonPrefab;
  public float floatStrength = 20f;
  public float growRate = 1.5f;

  private GameObject balloon;

 void Update()
 {
   if (balloon != null)
     GrowBalloon();
 }

 public void NewBalloon()
 {
   balloon = Instantiate(balloonPrefab);
 }

 public void ReleaseBalloon()
 {
   balloon.GetComponent<Rigidbody>().AddForce(Vector3.up * floatStrength);
   balloon = null;
 }

 private void GrowBalloon()
 {
   balloon.transform.localScale += balloon.transform.localScale * growRate * Time.deltaTime;
 }
}

将输入事件连接到我们的气球控制器:

  1. 选择 MeMyselfEye 并查看其检查器窗口

  2. 你会看到 My Input Controller 组件现在有两个事件列表,正如我们在其脚本中声明的。

  3. 在按钮按下事件列表中,按右下角的+号创建一个新项目。

  4. 将 BalloonController 从层次结构拖到空对象槽中

  5. 在函数选择列表中,选择BalloonController | NewBalloon

重复执行以下按钮按下事件的流程:

  1. 在按钮按下事件列表中,按右下角的+号创建一个新项目

  2. 将 BalloonController 从层次结构拖到空对象槽中

  3. 在函数选择列表中,选择BalloonController | ReleaseBalloon

现在组件应该看起来像这样:

现在当你按下播放并按下一个按钮时,输入控制器会调用一个事件。NewBalloon函数正在监听这些事件并被调用。同样对于按钮抬起事件。

此连接也可以完全通过脚本完成。我们在这里不会深入讨论。作为开发者,我们更经常是其他人设置的事件系统的“用户”。随着你的经验增长,你可能会发现自己正在实现自己的自定义事件。

对于使用 Unity 事件进行用户界面的另一个详细示例,请考虑由本书作者 Jonathan Linowes 和 Krystian Babilinski 合著的 Packt Publishing 出版的 Unity 项目书籍《Augmented Reality for Developers》。

真正地使用你的双手

本章我们将探讨的最后几件事情涉及让你更深入地体验虚拟现实。除了按钮和触摸板之外,VR 手控制器可以与你的头部一起在 3D 空间中跟踪。基于 PC 和控制台的游戏,如 Rift、Vive、MR 和 PSVR,在这方面做得非常好,为左右手提供了完全位置跟踪的手控制器。低端移动 VR,如 Daydream,有一个单手控制器,跟踪有限,但总比没有好。

首先,我们将利用位置跟踪的优势,简单地将气球绑定到你的手模型上。在 VR 中,如果没有实际的位置跟踪,例如 Daydream,你的手控制器位置将由 SDK 软件近似,但足够有用。

将气球绑定到手中

假设当你按下按钮时,而不是在空间中的固定位置创建新的气球,它从你的手位置生成并生长。实现这一目标的一种方法是将气球实例作为你的手控制器对象的子对象。

BalloonController 需要知道哪个手按下了按钮,并将气球绑定到该控制器对象。具体来说,我们将按如下方式将手游戏对象传递给 NewBalloon 函数:

public void NewBalloon(GameObject parentHand)
{
  if (balloon == null)
  {
    balloon = Instantiate(balloonPrefab);
    balloon.transform.SetParent(parentHand.transform);
    balloon.transform.localPosition = Vector3.zero;
  }
}

注意,在这个函数中,我们添加了一个额外的测试 (balloon == null),只是为了确保我们没有在未释放第一个气球的情况下连续两次调用 NewBalloon

如前所述,我们从预制体实例化一个新的气球。

然后,我们将它的父对象设置为 parentHand 对象。这相当于在层次结构中将一个对象拖动以成为另一个对象的子对象。游戏对象之间的父子关系由内置的 Transform 组件处理,因此 API 函数位于 transform 上。

最后,我们重置气球的局部位置。如果你还记得,预制体位于 (0, 1, 1) 或类似的位置。作为手的一个子对象,我们希望它直接连接到手模型的旋转中心点。(或者,根据需要,你可以将气球的原点偏移到不同的连接点。)

值得注意的是,Instantiate 函数有几种变体,允许你在一次调用中指定父对象和变换。请参阅 docs.unity3d.com/ScriptReference/Object.Instantiate.html

同样,ReleaseBalloon 在发送气球之前将其从手中分离,如下所示:

 public void ReleaseBalloon()
 {
   if (balloon != null)
   {
     balloon.transform.parent = null;
     balloon.GetComponent<Rigidbody>().AddForce(Vector3.up * floatStrength);
   }
   balloon = null;
 }

我们如何将手游戏对象传递给 NewBalloon?假设你的项目目前正在使用我们在上一主题中设置的 Unity 事件,这非常简单。在检查器中,我们需要更新“按钮按下事件”函数,因为它现在需要游戏对象参数:

  1. 在 Unity 编辑器中,选择 MeMyselfEye 对象

  2. 在“按钮按下事件”列表中,函数现在可能显示类似 Missing BalloonController.NewBalloon 的内容。

  3. 选择函数下拉菜单,选择 BalloonController | NewBalloon(GameObject)

  4. 在层次结构中展开 MeMyselfEye 对象,寻找手模型,然后将它拖放到空的游戏对象槽中

  5. 如果你使用 OpenVR,手将被称为 Controller(右手)

  6. 如果你使用 Daydream,手将被称为 GvrControllerPointer

这是我生成的一堆气球飞越大峡谷的截图,很有趣!

图片

气球弹出

说实话,很难想到创造气球而不想戳破它们!为了好玩,让我们快速实现一下。你可以想出自己的想法来改进它。

Unity 物理引擎可以检测两个对象何时发生碰撞。为此,每个对象都必须附加一个碰撞器组件。然后你可以让碰撞触发一个事件。我们可以订阅该事件,使其执行其他操作,例如播放爆炸效果。这将在气球预制体上设置。所以当两个气球相撞时,它们会爆炸。让我们这样做:

  1. 将你的气球预制体的副本从项目Assets prefabs文件夹拖动到场景层次结构中

  2. 选择添加组件 | 物理 | 球形碰撞器。

  3. 要缩放并定位碰撞器,请在其组件中点击编辑碰撞器图标。

  4. 场景窗口中,绿色的碰撞器轮廓上有你可以点击来编辑的小锚点。注意,按住Alt键可以固定中心位置,而Shift键可以锁定缩放比例。

  5. 或者,你可以直接编辑中心和半径值。我喜欢我的气球使用半径 0.25 和中心(0, 0.25, 0)。

现在,我们将添加一个脚本来处理碰撞事件。

  1. 添加组件以创建一个新的 C#脚本,

  2. 命名为Poppable

  3. 并打开它进行编辑

Poppable脚本将为OnCollisionEnter事件提供一个回调函数。当另一个具有碰撞器的对象进入这个对象的碰撞器时,我们的函数将被调用。在那个时刻,我们将调用PopBalloon来实例化爆炸并销毁气球:

public class Poppable : MonoBehaviour
{
 public GameObject popEffect;

 void OnCollisionEnter(Collision collision)
 {
   PopBalloon();
 }

 private void PopBalloon()
 {
   Instantiate(popEffect, transform.position, transform.rotation);
   Destroy(gameObject);
 }
}

你可以看到OnCollisionEnter接收一个包含碰撞信息的Collision参数,包括与之碰撞的游戏对象。我们在这里将忽略它,但你可能想进一步探索它:docs.unity3d.com/ScriptReference/Collision.html

保存脚本。现在,回到 Unity 中:

  1. 项目资源中选择一个粒子系统预制体,例如Assets/Standard Assets/ParticleSystems/Prefabs/Explosion(这是我们用来在第四章,基于注视的控制中杀死伊森的预制体)

  2. 将效果预制体拖放到 Poppable 的Pop Effect槽中

  3. 通过按 Apply 保存这些更改到预制体

  4. 你现在可以从层次结构中删除气球

好的,让我们试试。按 Play。创建一个气球。然后,伸手再次按下按钮,让一个新的气球与它相撞。它会爆炸吗?哎呀!

可交互物品

直接在 VR 中与对象交互,例如抓取物品并使用它们执行其他操作,要复杂一些。而且,正确做到这一点可能有点棘手。所以,特别是在这本书中,自己构建交互系统是没有意义的。不幸的是,也没有一个单一的标准化工具包。但是,有超过几个非常好的工具包可以使用,尽管大多数都是针对特定目标平台的。

通常,这些解决方案的架构是相似的:

  • 提供玩家相机装置的预制件

  • 相机装置包括用于您手部的对象,包括输入控制器组件

  • 手对象包括在交互发生时触发事件的组件

  • 任何可以交互的对象都会添加一个交互组件,使用输入事件进行交互

  • 额外组件和选项扩展了可交互行为

工具包将包括一些演示场景,提供丰富的示例,说明如何使用特定的工具包。通常,研究演示比实际文档更有信息量。

在本节中,我们介绍了一种使用两个工具包(SteamVR InteractionSystemDaydream VR Elements)的抓取和投掷机制。对于其他平台,技术是类似的。

对于 Oculus SDK(无 OpenVR),您需要集成 Oculus Avatar SDK(有关 Oculus Avatars 的详细信息,请参阅第十二章,Social VR Metaverse),这里还有一个快速视频,展示如何将OVR Grabber组件添加到您的OVRCameraRig控制器中:www.youtube.com/watch?v=sxvKGVDmYfY

使用 SteamVR Interaction System 进行交互

SteamVR Unity 包包括一个交互系统,最初是为 Steam 令人印象深刻的演示 VR 应用程序The Labstore.steampowered.com/app/450390/The_Lab/)中的迷你游戏和场景开发的。它可以在Assets/SteamVR/InteractionSystem/文件夹中找到。我们建议您探索示例场景、预制件和脚本。

交互系统包括它自己的玩家相机装置,它替换了我们一直在使用的默认[CameraRig]。玩家层次结构如下所示:

它包括一个VRCamera,两个手(Hand1Hand2),以及其他有用的对象。每个手都包括一个附加点(Attach_ControllerTip),一个悬停高亮(ControllerHoverHighlight),以及一个工具提示对象(ControllerButtonHints)。

  1. SteamVR/InteractionSystem/Core/Prefabs 中定位Player预制件,并将其拖动到场景层次结构中的 MyMyselfEye 作为子对象

  2. 删除或禁用[CameraRig]对象

为了与当前场景兼容,我们还需要更新Button Down Event中的NewBalloon参数:

  1. 在层次结构中展开玩家对象,以便您可以看到 Hand1 和 Hand2 对象

  2. 在层次结构中选择 MeMyselfEye

  3. 将 Hand2(或 Hand1)从 层次结构 拖动到 My Input Controller 组件的 Button Down Event 中的 GameObject 参数槽。

接下来,对于交互式对象,有一大堆组件可供选择。查看 SteamVR/InteractionSystem/Core/Scripts 文件夹的内容。我们将使用 可投掷 的一个。

首先,让我们在一个基本的立方体上尝试这个操作。然后,我们将使气球可抓取并可投掷:

  1. 在层次结构中创建一个立方体(创建 | 3D 对象 | 立方体)。

  2. 在玩家可触及的距离内调整其大小和位置。例如,大小 (0.3, 0.3, 0.3) 和 位置 (-0.25, 1.3, -0.25) 可能适用。

  3. 选择立方体,从交互系统添加 Throwable 组件。

  4. 注意这将自动添加其他所需组件,包括基本交互式对象和刚体。

  5. 在刚体组件上,取消选中使用重力复选框,这样它就会悬挂在空中而不是掉到地上。

现在当您按下播放时,伸手到立方体上,以便您的控制器穿透(碰撞)它。然后,使用控制器上的扳机,抓住立方体并将其扔出去!

要使气球可投掷,我们修改预制件:

  1. 从项目窗口中将气球预制件的副本拖动到场景层次结构中。

  2. 添加组件 Steam Throwable。

  3. 应用 保存预制件。

  4. 并从层次结构中删除气球。

按 Play。按 Fire1 按钮创建并充气一个气球。释放它。然后,用扳机抓住它。投掷气球。如果您之前实现了 可爆裂 爆炸,当它撞击到某个物体时,它甚至像投射物一样爆炸,比如地面或照片平面!

使用 Daydream VR 元素进行交互。

基础 GoogleVR 包不包含交互式对象,但您可以在 Daydream Elements 包中找到它们。这个包是谷歌工程师用于使用 Daydream VR 的示例场景、组件和预制件的集合。要获取该包:

  1. 访问 Daydream Elements 的 Github 发布页面 github.com/googlevr/daydream-elements/releases

  2. 下载 DaydreamElements.unitypackage

  3. 使用 资产 | 导入包 | 自定义包... 将其导入到您的项目中。

该包包含一个名为 ObjectManipulationPointer 的预制件,它是我们一直在使用的 GvrControllerPointer 的直接替代品:

  1. 在层次结构中,展开 MeMyselfEye 并钻到 Player 对象。

  2. 选择 GvrControllerPointer 并在检查器中禁用它。

  3. 在项目窗口中,导航到 Assets/DaydreamElements/Elements/ObjectManipulationDemo/Prefabs/UI/ 文件夹。

  4. 将 ObjectManipulationPointer 预制件拖动到层次结构中,作为 GvrControllerPointer 的同级。

为了与当前场景兼容,我们还需要更新 Button Down Event 中的 NewBalloon 参数。

  1. 在层次结构中选择 MeMyselfEye。

  2. 将 ObjectManipulationPointer 从层级拖到 My Input Controller 组件的“按钮按下事件”中的 GameObject 参数槽。

接下来,对于交互式对象,我们在Assets/DaydreamElements/Elements/ObjectManipulationDemo/Scripts/中添加一个MoveablePhysicsObject组件。

developers.google.com/vr/elements/object-manipulation可以找到关于 Daydream Elements 对象操作的相关附加信息。

首先,让我们在一个基本的立方体上尝试这个。然后,我们将使气球可抓取和可投掷:

  1. 在层级中创建一个立方体(创建 | 3D 对象 | 立方体)。

  2. 在玩家可触及的距离内调整其大小和位置。例如,缩放(0.25,0.25,0.25)和位置(-0.4,0.75,-0.3)可能适用。

  3. 选择立方体后,添加组件MoveablePhysicsObject

  4. 注意,如果不存在,这将自动添加一个RigidBody组件。

  5. RigidBody组件上,取消选中“使用重力”复选框,这样它就会悬挂在空中,而不是在播放时掉到地上。

现在,当你按“播放”时,使用你的控制器,使其激光束击中立方体。然后,按控制器上的点击按钮来抓住它。移动它,然后再次按下来释放它。

由于应用程序目前使用相同的按钮来创建新的气球和操作激光指针,因此每次我们使用按钮时都会得到一个气球。请考虑这是你应用程序中的一个错误。我们将把这个逻辑实现留给你自己去完成,例如,告诉MyInputController如果MoveablePhysicsObject正在移动某个对象,则不要调用事件。

提示:你可以在 Cube 上添加一个脚本组件,该组件检查 MoveablePhysicsObject 状态,并在对象被选中时禁用 MyInputController 动作。这没有很好地记录,但请查看 MoveablePhysicsObjects.cs 及其基类 BaseInteractiveObjects.cs 的源代码。

要使气球可投掷,我们修改预制件:

  1. 将 Balloon 预制件的副本从项目窗口拖到场景的层级中。

  2. 添加组件MoveablePhysicsObject

  3. 点击“应用”以保存预制件

  4. 并从层级中删除气球

按“播放”。按按钮创建并充气气球。释放它。然后,尝试用激光指针抓住它。如果你之前实现了Poppable爆炸,当它撞击到某物时,它甚至会像投射物一样爆炸!

摘要

在本章中,我们探讨了处理 VR 项目用户输入的各种软件模式。玩家使用控制器按钮在场景中创建、充气和释放气球。首先,我们尝试使用标准 Input 类来检测逻辑按钮点击,例如“Fire1”按钮,然后学习了如何访问特定于设备的 SDK 输入,例如具有触觉反馈的 OpenVR 扳机按钮。

在我们的场景中,我们实现了一个简单的输入组件来轮询按钮动作。然后,我们对代码进行了重构,使用可脚本化的对象来存储输入动作数据。在第三次实现中,我们使用了 Unity 事件将输入动作消息传递给监听组件。我们还增强了场景,将气球附着到你的虚拟手部位置,并添加了弹出气球作为爆炸性投射物的能力!最后,我们使用了一个交互框架(用于 SteamVR 和 Daydream)来实现抓取和投掷机制,使用给定工具包中提供的组件,而不是尝试编写自己的代码。

在下一章中,我们将进一步探讨用户交互,使用 Unity UI(用户界面)系统来实现信息画布、按钮和其他 UI 控件。

第六章:世界空间 UI

在上一章中,我们发现了如何在世界空间场景中与游戏对象交互。这些对象不仅可以是球类玩具,或工具和武器,还可以是你可以与之交互的按钮和其他用户界面小部件。此外,Unity 还包括一个用户界面画布系统,用于构建菜单和其他 UI。

图形用户界面(GUI)或简称 UI,通常指的是屏幕上的二维图形,它覆盖了主要游戏玩法,并通过状态消息、仪表和输入控件(如菜单、按钮、滑块等)向用户展示信息。

在 Unity 中,UI 元素始终位于一个画布上。Unity 手册将canvas组件描述如下:

canvas组件代表 UI 布局和渲染的抽象空间。所有 UI 元素都必须是具有附加canvas组件的GameObject的子元素。

在传统视频游戏中,UI 对象通常在屏幕空间画布上作为叠加进行渲染。屏幕空间 UI 类似于贴在你电视或显示器上的一个纸板,覆盖在它后面的游戏动作上。

然而,在 VR 中这并不适用。如果你尝试在虚拟现实中使用屏幕空间进行 UI,你会遇到问题。由于存在两个立体投影相机,你需要为每只眼睛提供不同的视图。虽然传统游戏可能会利用屏幕边缘来显示 UI,但虚拟现实没有屏幕边缘

相反,在 VR 中,我们使用各种方法将用户界面元素放置在世界空间而不是屏幕空间中。在本章中,我将介绍这些类型中的许多。我们将在本章中详细定义这些类型,并展示它们的示例:

  • 护目镜式抬头显示(HUD):在护目镜式抬头显示中,用户界面画布出现在你眼睛前方相同的位置,无论你的头部如何移动

  • 准星光标:类似于护目镜式 HUD,使用交叉线或指针光标在场景中选择事物

  • 挡风玻璃式抬头显示(HUD):这是一个像驾驶舱中的挡风玻璃一样漂浮在三维空间中的弹出面板

  • 游戏元素 UI:画布作为游戏玩法的一部分位于场景中,就像体育场中的计分板

  • 信息气泡:这是一种附着在场景中对象上的 UI 消息,就像漂浮在角色头顶上的思维气泡

  • 游戏内仪表盘:这是一个游戏玩法的一部分的控制面板,通常位于腰部或桌面高度

  • 基于手腕的菜单调色板:使用双手输入控制器,一只手可以握住菜单调色板,而另一只手进行选择并使用选定的工具

这些 UI 技术的差异基本上归结于你在哪里以及何时显示画布,以及用户如何与之交互。在本章中,我们将逐一尝试这些方法。在这个过程中,我们还将继续探索使用头部移动和手势以及按钮点击进行用户输入。

注意,本章中的一些练习使用了第四章中完成的场景,基于注视的控制,但这些练习是独立的,并不直接依赖于本书其他章节。如果你决定跳过其中任何部分或未保存你的工作,那也是可以的。

学习 VR 设计原则

在我们深入实施细节之前,我想介绍设计 3D 用户界面和 VR 体验的主题。在过去几十年里,尤其是最近几年,已经做了很多工作。

随着消费级 VR 设备的普及,以及像 Unity 这样的强大开发工具,许多人发明和尝试新事物,不断创新,并创造出真正优秀的 VR 体验,这并不令人惊讶。你可能就是其中之一。但今天 VR 的背景并非真空。它有着研究和发展的历史,这些历史为当前的工作提供了养分。例如,由 Bowman 等人撰写的《3D 用户界面:理论与实践》一书,是对消费、工业和科学应用及研究的 3D 用户交互的经典学术综述。该书最初于 2004 年出版,第二版于 2017 年(LaViola 等人)出版,是对学术理论和实践原则的最新回顾。

当前关于 VR 设计的写作更加易于理解。Adrienne Hunter 撰写的 Medium 文章《开始 VR:用户体验设计》(medium.com/vrinflux-dot-com/get-started-with-vr-user-experience-design-974486cf9d18)是一篇易于阅读但实用的 VR 用户体验设计入门文章,她是流行的 VR 物理包 NewtonVR 的联合创作者。她确定了几个重要的核心原则,包括像圆形剧场一样,通过物体、灯光和音频提示来吸引注意力,以及为高度和可访问性设计空间。

另一篇优秀的文章是《实用 VR:设计速查表》(virtualrealitypop.com/practical-vr-ce80427e8e9d)。这是一本旨在持续维护和更新的活页手册,包含了 VR 设计指南、流程、工具和其他资源。

我最喜欢的 VR 设计研究之一是 Mike Algers 作为 2015 年研究生制作的《VR 界面设计预可视化方法》。他在vimeo.com/141330081上的鼓舞人心的视频提出了一种易于消化的设计原则论点,特别是对于坐姿 VR 体验,基于工作空间和视觉感知的既定人体工程学。我们将在本章中运用这些想法。Algers 还探讨了 VR 的按钮设计、原型工作流程和 VR 操作系统设计概念。(Algers 目前供职于谷歌 VR 开发团队。)

在他的论文中,Algers 建立了一套围绕用户第一人称位置的舒适区域,如下所示:

图片

任何距离小于 0.5 米的都太近,不舒服;你可能不得不眯眼才能聚焦并跟随那个范围内的物体。超过 20 米太远,以至于无法进行有意义的互动,并且对于视差深度感知来说也太远了。你的周边区域(77–102 度)不应包含主要内容和交互,但可以包含次要内容。在你身后,他称之为好奇心区域,你需要伸展(或旋转椅子或转身)才能看到那里的情况,所以它最好很重要但不是强制性的。主要内容区域是你的正常工作空间。然后,考虑到手臂的伸展(向前、向上和向下)以及工作空间中的其他正常人类运动,Algers 定义了坐着 VR 体验的最佳虚拟工作区域,如下所示:

图片

对于站立和房间规模 VR,工作空间是不同的。站立时,可能更容易(也是预期的)能够转身来获取你周围的所有东西。在房间规模 VR 中,你可以四处走动(并且跳跃、蹲下和匍匐,等等)。Owlchemy Labs 的 Alex Schwartz 和 Devin Reimer(已被谷歌收购)在 Oculus Connect 2 会议(www.youtube.com/watch?v=hjc7AJwZ4DI)上讨论了为他们的热门游戏 Job Simulator 设计站立 VR 体验的挑战,包括适应现实世界的人体工程学和不同高度体验。

一些关于为虚拟现实设计的其他优秀资源包括:

当然,这仅仅触及了表面;每天都有更多内容被发布。谷歌搜索一下。关于虚拟现实用户界面设计和用户体验的资源列表,可以在 The UX of VR 网站上找到(www.uxofvr.com/)。

享受阅读和观看视频的乐趣。同时,让我们回到工作中。是时候我们自己实现一些 VR UI 了。

一个可重用的默认画布

Unity 的 UI 画布提供了许多选项和参数,以适应我们在游戏、网页和移动应用中期望的图形布局灵活性。这种灵活性带来了额外的复杂性。为了使本章的示例更容易理解,我们首先将构建一个可重用的预制画布,它具有我们首选的默认设置。

创建一个新的画布,并将其渲染模式更改为如下所示:

  1. 导航到 GameObject | UI | Canvas

  2. 将画布重命名为DefaultCanvas

  3. 设置渲染模式为世界空间

Rect Transform 组件定义了画布本身的网格系统,就像一张图表纸上的线条。它用于在画布上放置 UI 元素。将其设置为方便的640 x 480,具有0.75的纵横比。Rect Transform组件的宽度和高度与场景中画布的世界空间大小不同。让我们按照以下步骤配置Rect Transform组件:

  1. Rect Transform中,将宽度设置为640,高度设置为480

  2. 在缩放中,将 X、Y、Z 设置为(0.001350.001350.00135)。这是我们在世界空间单位中一个像素的大小。

  3. 现在,将画布定位在地面平面上方一个单位处,使其居中

    (0.3250.75的一半)。在 Rect Transform 中,将 Pos X、Pos Y、Pos Z 设置为(01.3250)。

接下来,我们将添加一个空的Image元素(具有白色背景),以帮助我们可视化其他情况下透明的画布,并在需要时为画布提供一个不透明的背景(我们还可以使用Panel UI 元素):

  1. 选择DefaultCanvas后,导航到 GameObject | UI | Image(确保它作为DefaultCanvas的子项创建;如果不是,将其移动到DefaultCanvas下)。

  2. 选择 Image 后,在其 Rect Transform 面板的右上角有一个锚点预设按钮(如下面的截图所示)。选择它将打开锚点预设对话框。按住Alt键以查看拉伸和位置选项,并选择右下角的选项(拉伸-拉伸)。现在,(空白)图像被拉伸以填充画布:

  1. 根据以下截图所示的DefaultCanvasImage子项的默认属性,双重检查您的图像设置:

添加一个具有有用默认设置的Text元素,如下所示:

  1. 选中DefaultCanvas后,导航到 GameObject | UI | Text(确保它作为DefaultCanvas的子组件创建,如果不是,将其移动到DefaultCanvas下)。应该在画布上出现New Text字样。

  2. 选中文本后,设置对齐为居中对齐和中间对齐,并将垂直溢出设置为溢出。将缩放设置为(444)。

  3. 使用其矩形变换面板左上角的控件,将它的锚点预设按钮设置为(拉伸 - 拉伸)。

  4. 根据以下截图所示的DefaultCanvasText子组件的默认属性,仔细检查你的文本设置:

图片

通过保持DefaultCanvas选中状态并设置 Canvas Scaler | Dynamic Pixels Per Unit 为10来提高像素分辨率,以获得更清晰的文字字体。

最后,将你的工作保存为一个预制体资产,你可以在本章中以以下方式重复使用:

  1. 如果需要,在项目资产中创建一个名为Prefabs的新文件夹。

  2. DefaultCanvas对象拖动到Project Assets/Prefabs文件夹中,以创建一个预制体。

  3. 现在请在层级面板中删除DefaultCanvas实例。

好的,很高兴我们已经解决了这个问题!现在我们可以使用DefaultCanvas预制体,配合不同的 VR 用户界面。

一个画布有一个Rect Transform组件,它定义了画布本身的网格系统,就像一张图表纸上的线条。它用于在画布上放置 UI 元素。这与画布对象在世界空间中的大小和位置不同。

视窗 HUD

头戴式显示器,或称 HUD,是你视野中一个浮动的画布,覆盖在游戏场景之上。在 VR 术语中,有两种 HUD 变体。我将把这些变体称为视窗 HUD挡风玻璃 HUD。本节将探讨第一种。

在视窗 HUD 中,UI 画布连接到相机上。它似乎不会对你的头部运动做出反应。当你移动头部时,它看起来像是粘在你的脸上。让我们看看一种更直观的展示方式。假设你戴着一顶带有视窗的头盔,UI 看起来像是投射在那个视窗的表面上。在虚拟现实中,可能有些情况下这是可以接受的,但它很可能会破坏沉浸感。因此,通常只有在视窗是游戏的一部分,或者意图是将你从场景中移出时(例如硬件或系统的实用程序菜单),才应该使用它。

让我们创建一个带有欢迎信息的视窗 HUD,如下所示,并亲自感受一下它的感觉:

  1. 在层级面板中,展开MeMyselfEye对象,然后深入到Main Camera对象(对于 OpenVR 可能是[CameraRig]/Camera (head);对于 Daydream,可能是Player/Main Camera/)。

  2. 从项目面板中,将DefaultCanvas预制体拖动到相机对象上,使其成为其子对象。

  3. 在层级面板中,选择画布后,将其重命名为VisorCanvas

  4. 在画布的检查器面板中,将矩形变换组件的 Pos X、Pos Y、Pos Z 更改为(001)。

  5. 展开视场显示器画布并选择其子Text对象。

  6. 在检查器面板中,将文本从默认文本更改为欢迎!我的现实是你的现实。(你可以在输入文本区域中输入换行符。)

  7. 将文本颜色更改为鲜艳的颜色,例如绿色。

  8. 禁用Image对象,通过在检查器中取消选中其启用复选框,使仅显示文本。

  9. 保存场景,并在 VR 中尝试。

这是带有VisorCanvas的 Rift 屏幕截图:

在 VR 中,当你四处移动头部时,文本会跟随移动,就像它被附着在你脸前的视场显示器上一样。

视场显示器(HUD)画布和瞄准十字准星光标画布被设置为相机的子对象。

现在,继续禁用VisorCanvas或直接删除它(在层次结构面板中,右键单击它并单击删除),因为我们将在稍后的部分以不同的方式显示欢迎信息。接下来,我们将查看这种技术的另一种应用。

瞄准十字准星光标

在第一人称射击游戏中至关重要的视场显示器(HUD)变体是瞄准十字准星或交叉线光标。这里的类比是你通过枪口或目镜(而不是视场显示器)观察,你的头部移动与枪或炮塔本身同步。你可以使用一个常规游戏对象(例如,四边形+纹理图像)来做这件事,但本章是关于用户界面(UI)。所以,让我们使用我们的画布,如下所示:

  1. 在层次结构面板中找到我们之前提到的主相机对象。

  2. 从项目面板中,将DefaultCanvas预制件拖放到相机对象上,使其成为相机的子对象。将其命名为ReticleCursor

  3. 将矩形变换组件的 Pos X、Pos Y、Pos Z 设置为(001)。

  4. 删除其子对象:ImageText。这将破坏预制件关联;这是可以的。

  5. 通过从主菜单栏中选择它,通过 GameObject | UI | Raw Image 导航,并确保它是ReticleCursor的子对象,添加一个原始图像子对象。

  6. 在原始图像面板的矩形变换中,将 Pos X、Pos Y、Pos Z 设置为(000)并将宽度和高度设置为(2222)。然后,在原始图像(脚本)属性中选择一个明显的颜色,例如红色。

  7. 保存场景并在 VR 中尝试。

如果你想要一个看起来更好的瞄准十字准星,在原始图像(脚本)属性中,将纹理字段填充为光标图像。例如,点击纹理字段最右侧的微小圆形图标。这会打开选择纹理对话框。找到并选择一个合适的图像,例如Crosshair图像。(本书附带Crosshair.gif的副本。)只需确保将宽度和高度更改为你的图像大小(Crosshair.gif的大小为 22 x 22)并确保锚点设置为中间中心。

我们将画布位置 Pos Z 设置为1.0,这样十字准线就会在你前方 1 米处浮动。在许多 UI 场景中,固定距离的光标就足够了,比如当你从距离你固定的平面上选择某个东西时。

然而,这是世界空间。如果你和十字准线之间有另一个对象,十字准线将被遮挡。

此外,如果你看得很远的东西,你会重新聚焦你的眼睛,同时看清楚光标会有困难。为了强调这个问题,尝试将光标移近。例如,如果你将ReticleCursor的 Pos Z 改为0.5或更小,你可能需要眯着眼睛才能看到它!为了补偿这些问题,我们可以进行射线投射并将光标移动到你所看对象的实际距离,相应地调整光标大小,使其看起来保持相同的大小。这是这个想法的一个简单版本:

  1. 选择ReticleCursor后,点击添加组件 | 新脚本,将其命名为CursorPositioner,然后点击创建并添加。

  2. 通过双击名称在 MonoDevelop 中打开脚本。

这是CursorPositioner.cs脚本:

using UnityEngine; 
using UnityEngine.EventSystems; 
using System.Collections; 

public class CursorPositioner : MonoBehaviour { 
  private float defaultPosZ; 

  void Start () { 
    defaultPosZ = transform.localPosition.z; 
  } 

  void Update () { 
    Transform camera = Camera.main.transform; 
    Ray ray = new Ray (camera.position, camera.rotation * 
       Vector3.forward); 
    RaycastHit hit; 
    if (Physics.Raycast (ray, out hit)) { 
      if (hit.distance <= defaultPosZ) { 
        transform.localPosition = new Vector3(0, 0, hit.distance); 
      } else { 
        transform.localPosition = new Vector3(0, 0, defaultPosZ); 
      } 
    } 
  } 
} 

矩形变换组件的 Pos Z 在脚本中的transform.localPosition中找到。如果它小于给定的 Pos Z,则该脚本将其更改为hit.distance。现在,你也可以将十字准线移动到一个更舒适的距离,例如 Pos Z = 2

@eVRydayVR编写的一个优秀的教程展示了如何实现距离和尺寸补偿的世界空间十字准线。您可以访问www.youtube.com/watch?v=LLKYbwNnKDg,这是一个名为 Oculus Rift DK2 - Unity 教程:十字准线的视频。

我们刚刚实现了自己的光标十字准线,但现在许多 VR SDK 也提供了光标。例如,在 Google VR 中,GvrReticlePointer.cs脚本是一个更全面的实现。另一个例子,Oculus OVR 包包括一个Cursor_Timer预制件,你可以将其用作加载...指示光标。

挡风玻璃 HUD

术语抬头显示(HUD)起源于其在飞机上的使用,飞行员能够以头部向前而不是向下看仪表板的方式查看信息。由于这种用法,我将它称为挡风玻璃 HUD。像头盔 HUD 一样,信息面板覆盖了游戏玩法,但它并没有附着在你的头上。相反,你可以将其视为在你坐在驾驶舱或牙医椅上时附着在你的座位上。

头盔 HUD 就像 UI 画布一样——它附着在你的头上。挡风玻璃 HUD 就像它附着在你周围的玻璃穹顶上。

让我们通过以下步骤创建一个简单的挡风玻璃 HUD:

  1. 从项目面板中,将DefaultCanvas预制件拖动到层次结构面板中的MeMyselfEye对象上,使其成为MeMyselfEye的直接子对象(这次不在相机下)。

  2. 将其重命名为WindshieldCanvas

  3. 在选择WindshieldCanvas后,将 Rect Transform 组件的 Pos X、Pos Y、Pos Z 设置为(01.41)。

  4. 现在,我们将设置文本组件。在WindshieldCanvas下选择Text,将文本改为“欢迎!我的现实即是你的现实。”同时,将颜色改为鲜艳的颜色,例如绿色。

  5. 这次,我们将使面板半透明。从WindshieldCanvas下的图像中选择图像,并选择其颜色样本。然后在颜色对话框中,将Alpha ("A")通道从255修改到大约115

这非常直接。当你通过 VR 查看时,画布最初就在你面前,但随着你四处张望,它的位置似乎保持静止,并相对于场景中的其他对象,如以下截图所示:

截图

正如我们在下一章(第七章)第七章中将要看到的,运动与舒适,当一个第一人称角色在场景中移动时,HUD 画布将保持在你的面前,相对于你的身体对象MeMyselfEye的相同相对位置。你现在可以在编辑器中尝试它:

  1. 在层次结构中选择MeMyselfEye

  2. 按下播放。

  3. 然后在场景窗口中,使用移动工具,移动MeMyselfEye的位置。在 VR 中,你会看到 HUD(抬头显示)就像是你身体的一部分或宇宙飞船的驾驶舱一样跟随移动。

你可能已经意识到场景中的对象可能会遮挡 HUD 面板,因为它们都占据了相同的世界空间。如果你需要防止这种情况,你必须确保画布总是最后渲染,这样它就会出现在任何其他对象的前面,无论其在 3D 空间中的位置如何。在一个传统的单视角游戏中,你可以通过添加第二个用于 UI 的相机并更改其渲染优先级来实现这一点。在立体 VR 中,你必须以不同的方式完成这项任务,可能需要为你的 UI 对象编写自定义着色器或进行每层遮挡剔除。这是一个高级话题;有关详细信息,请参阅World Space canvas on top of everything?讨论线程:answers.unity.com/questions/878667/world-space-canvas-on-top-of-everything.html

这种 HUD 的一个变体是将画布旋转,使其始终面向你,同时其 3D 空间中的位置保持固定。参见本章的信息气泡部分,了解如何编写此代码。

为了好玩,让我们编写一个脚本,在 15 秒后移除欢迎信息画布,如下所示:

  1. 在选择WindshieldCanvas后,点击添加组件 | 新脚本,将脚本命名为DestroyTimeout,然后点击创建并添加。

  2. 在 MonoDevelop 中打开脚本。

这是DestroyTimeout.cs脚本:

using UnityEngine; 

public class DestroyTimeout : MonoBehaviour 
{ 
  public float timer = 15f; 

  void Start () 
  { 
    Destroy (gameObject, timer); 
  } 
} 

当游戏启动时,计时器结束后WindshieldCanvas将消失。

风挡 HUD 画布被设置为第一人称角色的子对象,相机的同级对象。

在这个例子中,我们开始向第一人称体验迈进。想象一下坐在一辆车或飞机的驾驶舱里。你的抬头显示(HUD)投影在你前面的挡风玻璃上,但你可以自由地转动你的头四处张望。在场景的层次结构面板中,有一个第一人称对象(MeMyselfEye),其中包含相机装置,可能是你的角色身体,以及你周围的其它装饰。当车辆在游戏中移动时,整个驾驶舱会一起移动,包括相机装置和挡风玻璃。我们将在本章和第七章第七章,玩转物理和火焰中进一步探讨这一点。

游戏元素 UI

当埃森在第四章的迪奥拉玛场景中被杀时,第四章,基于注视的控制GameController 对象的 KillTarget 脚本中的分数值被更新,但我们没有向玩家(在第四章中设置)显示当前分数。我们现在将这样做,在背景图像 PhotoPlane 的右上角添加一个记分板:

  1. 从项目面板中,将 DefaultCanvas 预制体直接拖放到场景视图中

  2. 将其重命名为 ScoreBoard

  3. 选择 ScoreBoard,将矩形变换组件的 Pos X、Pos Y、Pos Z 设置为(-2.874.9),并将 Width 和 Height 设置为(3000480

  4. 选择 ScoreBoard 下的 Text,将字体大小设置为 100 并选择一个醒目的颜色,如红色

  5. 在文本中输入分数:0 示例字符串

  6. 通过取消选中启用复选框或删除它来禁用 ScoreBoard 下的图像

我们在场景中添加了另一个画布,将其大小和位置设置为我们想要的位置,并格式化文本以进行显示。它应该看起来像这样:

图片

现在,我们需要更新 KillTarget.cs 脚本,如下所示:

  • 我们可能正在使用 UnityEngine UI 类:
using UnityEngine.UI; 
  • scoreText 添加一个公共变量:
  public Text scoreText; 
  • Start() 中添加一行代码以初始化分数文本:
scoreText.text = "Score: 0"; 
  • Update() 方法中添加一行代码以更改分数文本,当分数变化时:
score += 1; 
scoreText.text = "Score: " + score; 

保存脚本文件后,返回到 Unity 编辑器,在层次结构面板中选择 GameController,然后将层次结构中的 ScoreBoard 下的 Text 对象拖放到 Kill Target (Script) 中的分数文本字段。

在 VR 中运行场景。每次你通过注视埃森(通过注视他)杀死他时,你的分数都会在 PhotoPlane 上方的 ScoreBoard 上更新。

游戏元素 UI 画布就像任何其他游戏对象一样是场景的一部分。

这是一个使用场景中物体作为信息显示的例子。我们的例子相当简单。你可能想要制作一个更漂亮的模型记分板,就像你在体育场或类似的地方看到的那样。关键是,它是场景的一部分,要看到信息,你可能实际上需要转动你的头,然后,嗯,看看它。

使用 TextMeshPro

要使广告牌像霓虹灯一样发光,你可以使用 TextMesh Pro,它目前免费包含在 Unity 中。例如:

  1. 在层次结构中选择ScoreBoard,创建一个新的TextMesh文本元素(右键点击 UI | TextMeshPro - Text)。

  2. 这将替换我们的标准 UI 文本元素,因此请禁用Text对象。

  3. 在 TMP 文本上,将其字体资产设置为 Bangers SDF。

  4. 对于其材质预设,请使用 Bangers SDF Glow。

  5. 滚动到发光设置以调整颜色和其他设置,如你所愿。

你甚至可以编写一个脚本,循环修改发光设置,制作一个闪烁的发光标志!

如果你选择尝试这个,请确保更新 GameController 的KillTarget脚本来使用 TMP 对象而不是 UI 对象。Modify KillTarget.cs如下:

我们可能会使用 UnityEngine TextMesh Pro 类:

using TMP; 

scoreText变量的数据类型替换为TMP_Text

  public TMP_Text scoreText; 

将 TMP 文本项拖放到检查器中的槽位。脚本的其他部分保持不变,因为TMP_Text具有与 UI 文本相同的text属性。

TextMesh Pro是 Unity 中用于文本格式化和布局的强大工具。它是 Unity UI Text 的替代品,增加了高级文本渲染、自定义着色器、排版控制(如段落间距和字距调整)等。它最初是一个第三方插件,现在免费包含在 Unity 中。文档可以在以下位置找到:digitalnativestudios.com/textmeshpro/docs/

这是使用 TextMesh Pro 和其检查器设置应用发光效果的分数板文本的截图:

图片

信息气泡

在漫画书中,当一个角色说话时,它会在一个话泡中显示。在许多在线社交 VR 世界中,参与者由头像表示,并且他们的名字会显示在某个人的头像上方。我将这种类型用户界面称为信息气泡

信息气泡位于世界空间中的特定 3D 位置,但画布应始终面向相机。我们可以通过脚本确保这一点。

在此示例中,我们将显示由LookMoveTo.cs脚本控制的WalkTarget对象的 X、Z 位置(在第四章,基于注视的控制中设置),添加信息气泡的步骤如下:

  1. 从项目窗口,将DefaultCanvas预制体直接拖放到层次结构窗口,使其成为WalkTarget的子项。

  2. 将其重命名为InfoBubble

  3. 选择InfoBubble后,将 Rect Transform 组件的 Pos X、Pos Y、Pos Z 设置为(0, 0.2, 0)。

  4. 选择InfoBubble下的文本,将 Rect Transform 组件的 Pos X、Pos Y、Pos Z 设置为(0, 0, 0),并将 Right 和 Bottom 设置为0, 0

  5. InfoBubble下选择 Image,将缩放设置为(0.7, 0.2, 1)。

  6. 为文本输入X:00.00, Z:00.00样本字符串。

确认画布和文本的大小和位置大致正确,并按需调整文本。(在我的场景中,原点处有一个立方体,所以我暂时禁用它以查看也位于原点的 WalkTarget。)

现在,我们将修改LookMoveTo.cs脚本以显示当前的WalkTarget X, Z 位置。在 MonoDevelop 编辑器中打开脚本,并添加以下代码:

using UnityEngine; 
using UnityEngine.UI; 

public class LookMoveTo : MonoBehaviour 
{ 
  public GameObject ground; 
  public Transform infoBubble; 

  private Text infoText; 

  void Start () 
  { 
    if (infoBubble != null) 
    { 
      infoText = infoBubble.Find ("Text").GetComponent<Text> (); 
    } 
  } 

  void Update () 
  { 
    Transform camera = Camera.main.transform; 
    Ray ray; 
    RaycastHit[] hits; 
    GameObject hitObject; 

    ray = new Ray (camera.position, camera.rotation * Vector3.forward); 
    hits = Physics.RaycastAll (ray); 
    for (int i=0; i < hits.Length; i++) 
    { 
      RaycastHit hit = hits [i]; 
      hitObject = hit.collider.gameObject; 
      if (hitObject == ground) 
      { 
        if (infoBubble != null) 
        { 
          infoText.text = "X: " + hit.point.x.ToString("F2") + 
                          "Z: " + hit.point.z.ToString("F2"); 

          infoBubble.LookAt(camera.position); 
          infoBubble.Rotate (0, 180f, 0); 
        } 
        transform.position = hit.point; 
      } 
    } 
  }
} 

using UnityEngine.UI;表示此脚本将需要访问 Unity UI API。我们定义了一个public Transform infoBubble变量,它将被设置为WalkTarget/InfoBubble对象。我们还定义了一个private Text infoText变量,它被设置为InfoBubble对象的 Text 对象。脚本假设给定的InfoBubble有一个子 Text UI 对象。

不幸的是,过度使用单词text可能会造成混淆。infoText text对象有一个text组件,该组件有一个text字符串属性!你可以在 Unity 编辑器中看到这一点。如果你在InfoBubble/Text被选中时检查检查器面板,你会看到它包含一个 Text(Script)组件,该组件有一个文本字段。这个文本字段是我们写入消息的地方。因此,在Setup()中,我们找到WalkTarget/InfoBubble/Text对象,将 Text 对象分配给infoText,然后在Update()中,我们设置infoText.text的字符串值,以便在气泡画布上显示分数。

此外,在Update()中,我们通过使用infoBubble.LookAt()并将相机位置传递给它来变换infoBubble画布,使其始终面向我们。LookAt()的结果是画布面向我们。因此,我们还需要围绕*y 轴旋转 180 度。

保存脚本,并将InfoBubble对象从层次结构拖到 Look Move To(脚本)组件中的信息气泡槽中。如果你没有分配InfoBubble画布,脚本仍然会运行,因为我们在我们引用它们之前测试了null对象。

信息气泡 UI 画布附加到其他游戏对象上,随着它们的移动而移动,并且始终面向相机(就像一个广告牌)。

在 VR 中运行场景,你会看到WalkTarget有一个小信息气泡告诉我们它的 X, Z 位置。

额外挑战:想要尝试其他东西吗?为 Ethan 实现一个生命值条。使用KillTarget脚本中的countDown变量来确定他的健康百分比,并在健康值不为 100%时在其头顶上方显示一个生命值条(水平条)。

信息气泡在需要显示属于场景中特定对象的 UI 消息时很有用,并且可能随对象一起移动。

游戏中的仪表盘包含输入事件

游戏中的仪表盘或控制面板是集成到游戏本身中的 UI 显示。一个典型的场景是汽车或宇宙飞船,你坐在驾驶舱内。在腰部水平(桌面水平)有一个面板,上面有一套控制装置、仪表、信息显示等。仪表盘在坐姿 VR 体验中感觉更自然。

在几页之前,我们讨论了挡风玻璃 HUD。仪表盘基本上是同一件事。一个区别是,仪表盘可能更明显地是关卡环境的一部分,而不仅仅是辅助信息显示或菜单。

实际上,仪表盘可以是一个非常有效的控制 VR 运动病感的机制。研究人员发现,当 VR 用户有更好的接地感和一致的地平线在视野中时,他在虚拟空间中移动时不太可能感到恶心。相比之下,成为一个没有自我或接地感的一维眼球,无疑是自找麻烦!(有关此内容和其他优秀提示,请参阅Oculus 最佳实践

在这个例子中,我们将制作一个简单的带有开始/停止按钮的仪表盘。目前,按钮将操作场景中的水管以帮助抵御僵尸。(为什么不呢?)像本章中的其他示例一样,此项目使用第四章中创建的场景,基于注视的控制

这个项目可能比你想象的要复杂一些。然而,如果你曾经在任何 Minecraft 中构建过东西,你就知道即使是简单的事情也可能需要组装多个部件。以下是我们将要做的:

  • 创建一个包含两个功能按钮的仪表盘画布——开始和停止

  • 在场景中添加一个水管并将其连接到按钮

  • 编写一个简单的脚本,用于激活按钮

  • 通过注视按钮来突出显示按钮

  • 改进脚本,以便仅在按钮被突出显示时激活按钮

那么,让我们开始吧。

创建带有按钮的仪表盘

首先,让我们创建一个带有开始和停止按钮的仪表盘,如下所示:

  1. 从项目窗口,将DefaultCanvas预制体拖动到层次结构面板中的MeMyselfEye对象,使其成为子对象。

  2. 将其重命名为Dashboard

  3. 在选择Dashboard后,将 Rect Transform 组件的 Pos X、Pos Y、Pos Z 设置为(0, 0.6, 0.6),并将旋转设置为(60, 0, 0)。请随意调整位置以适应您偏好的舒适区域和您特定的 VR 设备相机支架。

  4. 如果需要,禁用或删除Dashboard的 Text 子对象。

这样就将仪表盘放置在您的眼睛下方 1 米处,并稍微向前。

如果您想要一个工作进度的外观,我已包括一个车辆仪表盘的草图图像,您可以使用,如下所示:

  1. DashboardSketch.png文件导入到您的项目(例如Assets/Textures文件夹)中。

  2. 将一个新的 GameObject | UI | Raw Image 作为Dashboard的子对象添加。

  3. DashboardSketch纹理从项目面板拖动到 Raw Image 组件的纹理字段。

  4. 将其 Rect Transform 组件的 Pos X、Pos Y、Pos Z 设置为(0,0,0),宽度为140,高度为105

  5. 它应该在 X、Y 和 Pivot 的中间中心(0.5,0.5)锚定,旋转(0,0,0)。

  6. 将比例设置为 (4.5,4.5,4.5).

接下来,我们将添加启动和停止按钮。它们可以放在画布上的任何位置,但草图已经为它们预留了两个很好的空间:

  1. Dashboard 下添加一个新的 GameObject | UI | Button 作为新的子对象。命名为 StartButton

  2. 将其 Rect Transform 组件的 X、Y、Z 设置为 (-48, 117, 0), 宽度和高度设置为 (60, 60), 并锚定到中心中间 (0.5)。无旋转和比例 1

  3. 在按钮的 Image (Script) 组件面板中,对于源图像,点击最右侧的小圆圈以打开选择精灵选择器,并选择 ButtonAcceleratorUpSprite(你可能已将其导入到 Assets/Standard Assets/CrossPlatformInput/Sprites 文件夹)。

  4. 在按钮的 Button (Script) 组件面板中,对于正常颜色,我使用了 RGB (89,154,43) 并将高亮颜色设置为 (105, 255, 0)。

  5. 类似地,创建另一个名为 StopButton 的按钮,其 Rect Transform 组件的 X、Y、Z 为 (52, 118, 0),并将宽度和高度设置为 (60, 60)。对于源图像,选择 ButtonBrakeOverSprite,然后选择正常颜色 (236, 141, 141) 和高亮颜色 (235, 45, 0)。

结果应该看起来像这样:

图片

最后一件事情。如果你正在使用本章早期创建的 ReticleCursor,该 ReticleCursorCursorPositioner.cs 脚本一起创建,我们希望仪表板本身具有碰撞器以供脚本使用。我们可以通过以下步骤实现这一点:

  1. 选择 Dashboard,右键单击以获取选项,并导航到

    3D 对象 | 平面。

  2. 将其位置设置为 (0,0,0), 旋转设置为 (270,0,0), 比例设置为 (64,1,48)。

  3. 禁用其 Mesh Renderer(但保留其 Mesh Collider 启用)。

现在仪表板上有一个未渲染的平面子对象,但当 CursorPositioner 执行射线投射时,其碰撞器将被检测到。我们这样做是为了让你在即使不直接看按钮的情况下,也能看到这个仪表板面板上的注视点。

拥有一个具有按下和释放状态的单独切换按钮可能比单独的启动和停止按钮更好。当你完成这一章后,继续找出如何实现它!

我们刚刚创建了一个应该在 VR 中出现在腰部或桌面高度的宇宙空间画布。我们用仪表板草图进行了装饰,并添加了两个 UI 按钮。现在,我们将为按钮连接到特定的事件。

将水管链接到按钮

让我们先给按钮分配一些功能,比如打开水管的动作。如果我们有策略地瞄准,甚至可能击退流氓僵尸。巧合的是,我们之前导入的 Unity 粒子系统标准资产中有一个水管,我们可以使用它。按照以下步骤将其添加到场景中:

  1. 如果你还没有这样做,请通过导航到 Assets | Import Package | ParticleSystems 从主菜单栏导入粒子系统标准资产。

  2. 在项目窗口中,找到Assets/Standard Assets/Particle Systems/Prefabs/Hose预制件,并将其拖动到层次窗口中。

  3. 将其 Transform 组件的 X、Y、Z 设置为(-301.5),并将旋转设置为(340870)。

  4. 确保 Hose 已启用(勾选其启用复选框)。

  5. 在层次窗口中展开 Hose,以便可以看到其子 WaterShower 粒子系统。选择它。

  6. 在检查器中,在粒子系统属性面板中,查找“唤醒时播放”并取消勾选。

注意,在层次窗口中,Hose 对象有一个WaterShower子对象。这是我们将通过按钮控制的实际粒子系统。它应该以关闭的状态开始。

Hose 预制件本身带有鼠标驱动的脚本,我们不想使用,

因此,按照以下方式禁用它:

  1. 选择 Hose 后,禁用(取消勾选)其 Hose(脚本)。

  2. 还要禁用(取消勾选)简单鼠标旋转(脚本)组件。

现在我们将通过告诉按钮监听OnClick()事件,将StartButton连接到 WaterShower 粒子系统,如下所示:

  1. 在层次窗口中再次展开 Hose,以便可以看到其子 WaterShower 粒子系统。

  2. 在层次窗口中,选择StartButton(位于MeMyselfEye/Dashboard下)。

  3. 注意,在检查器中,Button组件的OnClick()面板是空的。点击该面板右下角的加号(+)图标以显示一个新字段,标签为 None(对象)。

  4. 将 WaterShower 粒子系统从层次窗口拖动到 None(对象)字段。

  5. 其功能选择器,默认值是“无功能”。将其更改为 ParticleSystem | Play()。

好的。对于StopButton,步骤类似,如下所示:

  1. 在层次窗口中,选择StopButton

  2. 点击其OnClick()面板右下角的加号(+)图标。

  3. 将 WaterShower 从层次窗口拖动到 None(对象)字段。

  4. 其功能选择器,默认值是“无功能”。将其更改为 ParticleSystem | Stop()。

开始和停止按钮监听OnClick()事件,当其中一个事件发生时,

分别调用 WaterShower 粒子系统的Play()Stop()函数。为了使其工作,我们需要按下按钮。

通过脚本激活按钮

在我们为用户提供按按钮的方法之前,让我们看看我们如何从脚本中做到这一点。在GameController上创建一个新的脚本,如下所示:

  1. 在层次窗口中选择GameController后,按“添加组件”|“新脚本”以创建一个名为ButtonExecuteTest的脚本。

  2. 在 MonoDevelop 中打开脚本。

在以下脚本中,我们每隔五秒切换水龙带的开关,如下所示:

using UnityEngine; 
using UnityEngine.UI; 

public class ButtonExecuteTest : MonoBehaviour 
{ 
  public Button startButton;
  public Button stopButton;

  private bool isOn = false; 
  private float timer = 5.0f; 

  void Update () 
  { 
    timer -= Time.deltaTime; 
    if (timer < 0.0f) 
    { 
      isOn = !isOn; 
      timer = 5.0f; 

      if (isOn) 
      { 
        stopButton.onClick.Invoke(); 
      } else 
      { 
        startButton.onClick.Invoke(); 
      } 
    } 
  } 
} 

该脚本管理一个布尔值isOn,表示水龙带是开启还是关闭。并且它有一个计时器,每次更新时从 5 秒开始倒计时。我们使用private关键字为只在此脚本中使用的变量,而public的变量可以通过 Unity 编辑器和其他脚本查看和修改。对于startButtonstopButton,你将在 Unity 编辑器中将它们拖放进去。

在此脚本中,我们使用UnityEngine.UI。正如我们在上一章中看到的,事件是不同组件之间通信的一种方式。当发生事件时,例如按钮按下,另一个脚本中的函数可能会被调用。在我们的情况下,我们将触发与开始按钮按下相对应的事件,以及与停止按钮按下相对应的事件,正如我们在检查器中设置的那样。

保存脚本并点击播放。喷嘴应该每五秒开关一次。

现在我们已经测试了按钮点击和喷嘴之间的事件系统连接,我们可以在继续下一个脚本之前禁用此脚本:

  1. 选择GameController

  2. 通过取消选中其Enable复选框禁用ButtonExecuteTest组件,或删除该组件。

将复杂功能分解成小块并单独测试是一种优秀的实现策略。

瞄准以高亮按钮

同时,让我们检测用户是否在查看按钮并高亮它。尽管按钮是 Unity UI 对象,但需要通过射线投射来检测。可能还有其他方法可以完成这项任务,如本章后面所述,但在这里我们将为每个按钮添加一个游戏对象球体并发射一条射线来检测它。首先,通过以下步骤添加球体:

  1. 在层次结构面板中,选择StartButton(位于MeMyselfEye/Dashboard下),右键单击以获取选项,并导航到 3D 对象 | 球体。

  2. 将其变换组件的缩放设置为(52, 52, 52),以便它适合

    按钮大小。

  3. 通过取消选中 Mesh Renderer 复选框禁用球体的 Mesh Renderer。

同样,为StopButton重复这些步骤。一个快捷方法是复制球体,如下所示:

  1. 右键单击 Sphere 并选择复制。

  2. 将复制的项目(Sphere (1))拖入StopButton

  3. 将其位置重置为(0,0,0)。

现在,在StartButton上创建一个新的脚本,如下所示:

  1. 选择StartButton,导航到添加组件 | 新脚本以创建一个名为RespondToGaze的脚本。

  2. 打开脚本进行编辑。

在下面的RespondToGaze.cs脚本中,我们告诉按钮在你看向它时变得高亮,使用子对象 Sphere 的碰撞器:

using UnityEngine; 
using UnityEngine.UI; 

public class RespondtoGaze : MonoBehaviour 
{ 
  public bool highlight = true;
  private Button button; 
  private bool isSelected;

  void Start ()
  {
    button = GetComponent<Button>();
  }

  void Update () 
  { 
    isSelected = false;
    Transform camera = Camera.main.transform; 
    Ray ray = new Ray(camera.position, camera.rotation * Vector3.forward); 
    RaycastHit hit; 
    if (Physics.Raycast (ray, out hit) &&
        (hit.transform.parent != null) &&
        (hit.transform.parent.gameObject == gameObject) 
    { 
      isSelected = true;
    } 

    if (isSelected)
    {
      if (highlight)
        button.Select();
    }
    else {
      UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(null);
    }
  } 
}

在此脚本中,在每次更新时,我们从相机发射一条射线。如果它击中按钮的球体碰撞器,那么击中对象的父对象应该是这个按钮。所以(在检查击中对象有父对象之后),我们将父 GameObject 与此按钮的 GameObject 进行比较。

如果注视已经选择了这个按钮,我们触发按钮的 Select 来使其高亮。高亮是在 Unity 的EventSystem中完成的。虽然 EventSystem 已经为鼠标点击和屏幕触摸实现了所有这些功能,但我们必须手动通过调用button.Select()来告诉按钮它已被选中。

取消按钮的高亮并不明显。EventSystem 在运行时场景中维护一个当前选中的对象。我们通过将 null 传递给SetSelectedGameObject()来清除它。

保存脚本并播放。当你注视一个按钮时,它应该突出显示,当你从它移开目光时,它应该移除突出显示。

这也是一个可重用组件脚本的例子。我们只为 StartButton 编写并测试了它。我们可以为 StopButton 使用相同的脚本:

  1. 从层次结构中选择 StopButton。

  2. 将 RespondToGaze 脚本从项目资产拖到按钮上,或

  3. 选择添加组件 | 脚本 | 响应注视。

再测试一下项目。当你注视按钮时,两个按钮都应该突出显示。

如果你使用 Google VR for Cardboard 或 Daydream,你可以在场景中包含GvrEventSystem预制件。然后这个RespondToGaze脚本就变得不必要且冗余了。Daydream组件已经支持基于注视的选择、突出显示和输入控制器进行点击。但我鼓励你跟随这个项目,体验一下这种功能是如何实现的。如果是这样,暂时在你的场景中禁用GvrEventSystem

观看并点击以选择

Input.GetButtonDown("Fire1").

需要更改的RespondToGaze.cs脚本更改相当简单。在类的顶部添加以下公共变量:

public bool clicker = true;
public string inputButton = "Fire1";

Update()的底部进行以下更改:

  ... 
  if (isSelected)
  {
    if (highlight)
        button.Select();
    if (clicker && InputGetButtonDown("Fire1"))
        button.onClick.Invoke();
  }

当控制器"Fire1"按钮被按下时,它将触发 UI 按钮的点击。

该组件提供了启用高亮显示和/或使用输入控制器进行点击的选项。您还可以选择将触发点击事件的逻辑输入按钮。

现在我们有一个游戏中的仪表板,带有响应用户输入的按钮,它控制场景中对象(水管)的行为。

观看并开始选择

我们可以使用基于时间的选择而不是点击器来点击按钮。为了使这起作用,我们将在注视按钮时保持倒计时计时器,就像我们在上一章中用来杀死 Ethan 的那个一样。

修改RespondToGaze.cs脚本。在类的顶部添加以下变量:

public bool timedClick = true;
public float delay = 2.0f;

private float timer = 0f;

Update()中,进行以下更改:

... 
  if (isSelected)
  {
    if (highlight)
        button.Select();
    if (clicker && Input.GetButtonDown("Fire1"))
        button.onClick.Invoke();
    if (timedClick) 
    {
      timer += Time.deltaTime;
      if (timer >= delay)
        button.onClick.Invoke();
  }
  else {
    UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(null);
    timer = 0f;
  }

现在,不仅按钮点击会在Input.GetButtonDown上涉及,而且如果你长时间注视按钮(当timedClicktrue时),也会涉及。当按钮被选中(突出显示)时,我们开始计时并计数。当计时器到期时,将调用点击事件。如果在那时之前取消选中按钮,计时器将重置为零。

它对你有效吗?太棒了!

所以这是一个相对复杂的项目。目标是创建一个带有开关水管的按钮仪表板。我们将它分解成离散的步骤,一次添加一个对象和组件,并在继续之前测试每个步骤以确保其按预期工作。如果你试图一次性实现所有这些,或者在没有测试的情况下快速完成,事情可能会(并且将会)出错,这将使找出问题所在变得更加困难。

额外挑战:此功能可以根据不同目的进一步增强。例如,它可以用来向用户显示倒计时正在运行,可能通过动画旋转光标来实现。此外,在执行点击事件时还可以提供进一步的反馈。例如,按钮 UI 对象有一个名为动画的过渡选项,这可能很有帮助。同时,考虑音频提示。

使用 VR 组件进行指向和点击

正如我们所见,虽然 Unity 提供了专门针对传统屏幕空间 UI 和移动应用的 UI 元素,如画布文本、按钮和其他控件,但在世界空间中使用它们并与 VR 用户输入相结合可能会相当复杂。世界空间交互假设一些物理、碰撞体和射线投射来检测交互事件。

幸运的是,VR 设备特定的工具包可能提供一些组件,可以处理一些这项工作。正如我们在前面的章节中看到的,设备制造商提供了基于他们 Unity SDK 的工具包,其中包含方便的脚本、预制体和演示场景,说明了如何使用它们。

在这种情况下,我们正在寻找允许您使用 Unity UI 元素在画布上设计场景的组件,利用所有它们的 EventSystem 交互功能,使用世界空间 3D 模型,以及输入控制器或激光指针的组件。例如,考虑以下内容:

最后,你可能考虑从 Unity Asset Store 购买一个包。例如,Curved UI 包($25)允许你创建 VR 准备好的弯曲画布,并支持 Vive、Oculus Touch、Daydream 控制器和注视输入,如图所示:

使用 Unity UI 和 SteamVR

我们在第五章“实用交互对象”中介绍了 SteamVR InteractionSystem。它旨在作为一个如何使用 SteamVR SDK 的示例,但包括一些非常有用的组件和演示场景。使用交互系统,将仪表板转换为可以直接使用位置跟踪手控制器操作的控制面板变得非常简单。

交互系统包括它自己的Player相机装置,它替换了我们一直在使用的默认[CameraRig]。它包括一个 VRCamera、两个手(Hand1 和 Hand2)和其他有用的对象。

  1. 在项目窗口中定位Assets/SteamVR/InteractionSystem/Core/Prefabs文件夹

  2. Player预制体作为MyMyselfEye的子对象拖入场景层次结构

  3. 删除或禁用[CameraRig]对象

要使StartButtonStopButton可交互,请添加Interactable组件。还要添加 UI Element组件来处理 OnHandClick 事件,如下所示:

  1. 在层次结构中选择StartButton对象(Dashboard的子对象)。

  2. 在检查器中,选择添加组件 | 脚本 | Valve.VR.InteractionSystem | Interactable(提示:使用搜索字段搜索“Interactable”)。

  3. 选择添加组件 | 脚本 | Valve.VR.InteractionSystem | UI Element。

  4. 在 UI 元素组件的检查器中,按“+”键添加一个“On Hand Click”处理程序。

  5. WaterShower粒子系统(Hose对象的子对象)从层次结构拖到 GameObject 字段,就像我们为标准按钮 OnClick 事件所做的那样。

  6. 选择 ParticleSystem | Play()函数。

  7. 可选地,禁用RespondToGaze组件。

类似地,为StopButton重复这些步骤,但选择 ParticleSystem | Stop()函数。

您可能还需要将“仪表板”移近一些,以便在 VR 中按钮在舒适范围内。当您按下播放时,现在您可以伸手触摸按钮;它会被高亮显示。拉动扳机按它,如图所示,水龙头就会打开:

图片

使用 Unity UI 和 Daydream

现在我们来看看如何在移动 VR 设备上使用 Google Daydream 来实现这一功能。在这种情况下,我们实际上不会伸手去按按钮,而是使用 3DOF 手柄控制器激光指针。解决方案就像将GvrReticlePointer(如果您使用过它)替换为GvrControllerPointer一样简单。

  1. 在您的 MeMyselfEye GVR Camera Rig/ Player / Main Camera /下,如果有 GvrReticlePointer,请禁用它。

  2. GoogleVR/Prefabs/Controller/文件夹中找到 GvrControllerPointer。

  3. 将 Player(作为主相机的兄弟)下的预制件拖动。

然后设置仪表板画布以接受射线投射:

  1. 在层次结构中选择仪表板对象。

  2. 添加 GvrPointerGraphicRaycaster 组件。

按下播放。现在您可以使用 Daydream 控制器来按按钮。

探索 GvrControllerPointer、其子 Laser 对象和包中提供的其他 Gvr 对象组件选项。有一些相当有趣和有用的配置可用,包括激光颜色、结束颜色和最大距离的设置。甚至还有一个复选框,可以在播放模式下的编辑器场景窗口中绘制调试射线。

构建基于手腕的菜单调色板

一些 VR 应用程序,如 Oculus Rift、HTC Vive 和 Windows MR,为双手设置设计,在一侧手腕上提供虚拟菜单调色板,而另一只手则从中选择按钮或项目。让我们看看这是如何实现的。此场景假设您有一个双手控制器 VR 系统。我们将使用 SteamVR 相机架来描述,涉及将控制绑定到您的左手,并用右手选择它们。

将我们的仪表板控制面板转换为手腕调色板并不太难。我们只需要适当地缩放并将其附加到手控制器上。

由于你已经将场景构建到上一节中描述的点,包括 SteamVR Player装置(而不是[CameraRig]),我们将复制并重新使用Dashboard,以便在你的左手腕上使用:

  1. 在层级中,右键点击仪表板并复制。

  2. 将新名称重命名为“调色板”。

  3. 禁用旧仪表板。

  4. 将调色板作为 Player/Hand1 对象的子对象拖动。

现在我们将修改调色板图形如下。请随意更改以适应你的需求:

  1. 在调色板本身上,将其 Pos X,Y,Z 设置为(0,0.1,-0.1);旋转设置为(90,-150,-115);缩放(X,Y,Z)设置为 0.0005;

  2. 展开调色板并禁用或删除原始图像对象。

  3. 启用图像子对象(如果缺失,创建一个新的图像并使用锚点预设进行拉伸拉伸)。

  4. 将图像缩放(X,Y,Z)设置为 0.5。

  5. 将图像颜色 Alpha 设置为 75,使其半透明。

  6. 启用文本子对象。将其矩形变换顶部设置为 100,字体大小设置为 18,文本设置为“软管”。

  7. 将启动按钮的 Pos Y 移动到 0。

  8. 将停止按钮的 Pos Y 移动到 0。

就这样!我们为仪表板设置的点击连接都正常工作。这里展示的是使用左手持控制器附加的调色板,并用右手控制器选择其上的启动按钮的截图:

图片

自然地,调色板可以通过其他按钮和输入控件进行扩展。如果你有多个面板排列成立方体的侧面(如 TiltBrush 菜单),你可以使用拇指盘在各个菜单之间滚动或旋转。就是这样做的。

摘要

在 Unity 中,基于画布对象和事件系统的用户界面包括按钮、文本、图像、滑块和输入字段,它们可以组装并连接到场景中的对象。

在本章中,我们仔细研究了各种世界空间 UI 技术及其在虚拟现实项目中的应用。我们考虑了 VR 用户界面与传统视频游戏和桌面应用程序用户界面不同的方式。我们还实现了其中的一些,展示了如何在你的项目中构建、编码和使用它们。我们的 C#脚本编写变得更加高级,深入探讨了 Unity 引擎 API 和模块化编码技术。

现在,你有了更广泛的词汇来处理你的 VR 项目中的用户界面。本章中的一些示例可以直接应用于你的工作。然而,并非所有都需要从头开始。VR UI 工具越来越多地提供在 VR 头戴式设备 SDK、开源 VR 中间件项目和第三方 Unity 资产商店包中。

在下一章中,我们将为我们的场景添加一个第一人称角色控制器。我们将了解化身以及控制 VR 中导航的方法,以便我们可以在虚拟世界中舒适地移动。此外,我们还将了解管理虚拟现实体验的负面方面之一——VR 运动病。

第七章:移动和舒适度

到目前为止,本书中玩家的视角摄像头一直是静止的。在本章中,我们将开始移动,考虑各种移动和传送技术。首先,我们将深入了解 Unity 标准角色组件,然后我们将自己变成一个可控制的第一人称角色,并探索在虚拟世界中移动的技术。我们还将讨论管理 VR 中的运动病感和自我意识的方法。

在本章中,我们将讨论以下主题:

  • Unity 的角色对象和组件

  • 滑行移动

  • 舒适模式移动

  • 传送

  • VR 运动病问题

注意,本章中的项目是独立的,并不直接需要本书其他章节。如果你决定跳过其中任何部分或未保存你的工作,那没有问题。

理解 Unity 角色

第一人称角色在 VR 项目中是一个关键资产,我们真的应该彻底了解其组件。因此,在我们为项目构建一个角色之前,仔细查看 Unity 提供的内置组件和标准资产是个好主意。

Unity 组件

如你所知,每个 Unity 游戏对象都包含一组相关联的组件。Unity 包含许多内置组件类型,你可以在主菜单栏中的组件菜单中浏览它们。每个组件都会为其所属对象添加属性和行为。组件的属性可以通过 Unity 编辑器的检查器面板和脚本访问。附加到游戏对象上的脚本也是一种组件,你可以在检查器面板中设置其属性。

用于实现第一人称角色的组件类型包括摄像头角色控制器和/或刚体,以及各种脚本。让我们回顾一下这些标准组件。

摄像头组件

摄像头组件指定了用于在每一帧更新时渲染场景的视图参数。任何具有摄像头组件的对象都被视为一个摄像头对象。自然地,自从我们开始使用场景以来,我们一直在脚本中访问它。

立体声 VR 摄像头对象渲染两个视图,每个眼睛一个。在 VR 中,摄像头控制器脚本从头戴式设备的运动传感器和位置跟踪中读取数据,以确定当前的头姿(位置、方向和旋转)并适当地设置摄像机的变换。

刚体组件

当你将刚体组件添加到任何 Unity 游戏对象时,它将受益于物理引擎执行的运算。刚体组件具有重力、质量和阻力等参数。在游戏过程中,物理引擎计算每个刚体对象的动量(质量、速度和方向)。

刚性物体与其他刚性物体相互作用。例如,如果它们发生碰撞,它们会相互弹开,并且可以通过具有摩擦和弹跳系数等属性的物理材质来控制交互参数。

Rigidbodies 可以被标记为运动学,这通常只在对象由动画或脚本驱动时使用。碰撞不会影响运动学对象,但它们仍然会影响其他刚性物体的运动。它主要用于将对象通过关节连接在一起,例如连接人类骨骼或摆动的摆锤。

任何刚性物体,一旦赋予子相机对象,就变成了一个刚性的第一人称角色。然后,你可以添加脚本以处理用户输入,实现移动、跳跃、环顾四周等功能。

Character Controller 组件

与 Rigidbody 一样,Character ControllerCC)用于碰撞检测角色移动。它也需要脚本来处理用户输入以实现移动、跳跃和环顾四周。然而,它并没有内置物理特性。

CC 组件专门为角色对象设计,因为在游戏中,角色通常并不期望与其他基于物理的对象表现出相同的动作。它可以替代 Rigidbody,或者与 Rigidbody 一起使用。

CC 组件内置了胶囊碰撞器行为以检测碰撞。然而,它不会自动使用物理引擎来响应碰撞。

例如,如果一个 CC 对象撞击到一个刚性物体,如墙壁,它将只是停止,而不会弹跳。如果一个刚性物体,如飞行的砖块,撞击到一个 CC 对象,砖块将根据其自身属性被弹开(弹跳),但 CC 对象将不受影响。当然,如果你想在 CC 对象上包含这种行为,你可以在自己的脚本中编程实现。

CC 组件在其脚本 API 中特别支持一种力——重力。内置参数专门与保持对象脚部在地面上有关。例如,步进偏移参数定义了角色可以跳上的步子的高度,而不是成为阻碍其前进的障碍物。同样,斜率限制参数说明了多大的斜率是过于陡峭的,以及它是否应该被视为墙壁。在你的脚本中,你可以使用Move()方法和IsGrounded变量来实现角色行为。

除非你编写脚本,否则 CC 对象没有动量,可以立即停止。这感觉非常精确,但也可能导致动作生硬。而对于 Rigidbody 对象来说,情况正好相反,它们感觉更流畅,因为它们具有动量、加速度/减速度,并遵循物理定律。在 VR 中,如果我们使用它的话,我们理想情况下希望两者结合。

使用物理在 VR 场景中移动并不总是最佳选择。正如我们将看到的,替代的移动技术可能根本不使用物理,例如传送

Unity 标准资产

Unity 标准资产中的 Characters 包含了许多第三人称和第一人称角色预制件。以下表格中比较了这些预制件:

预制件 组件
图片 图片
图片 图片
图片 图片
图片 图片

让我们更详细地讨论这个问题。

ThirdPersonController

我们已经在第二章 内容、对象和缩放 和第四章 基于注视的控制 中分别使用了这两个第三人称预制件,ThirdPersonControllerAIThirdPersonController

ThirdPersonController 预制件有子对象定义角色的身体,即我们的朋友伊森。他是一个绑定了骨骼的化身(来自 .fbx 文件),这意味着可以应用类人动画使他行走、奔跑、跳跃等。

ThirdPersonController 预制件使用 Rigidbody 进行物理运算,并使用 Capsule Collider 进行碰撞检测。

它有两个脚本。一个 ThirdPersonUserControl 脚本接收用户输入,例如摇杆按下,并告诉角色移动、跳跃等。一个 ThirdPersonCharacter 脚本实现物理运动并调用所需的动画,例如跑步、蹲下等。

AIThirdPersonController

AIThirdPersonController 预制件与 ThirdPersonController 预制件相同,但前者添加了 NavMeshAgentAICharacterControl 脚本,这限制了角色在场景中的移动位置和方式。如果您还记得,在第四章 基于注视的控制 中,我们使用了 AICharacterController 使伊森在场景中行走并避免碰撞到物体。

第一人称 FPSController

FPSController 预制件是一个第一人称控制器,它使用 CC 组件和 Rigidbody。它附有一个子相机。当角色移动时,相机也会随之移动。

第三人称控制器预制件和第一人称控制器预制件之间的核心区别是 子对象。第三人称控制器预制件有一个绑定了骨骼的类人子对象,而第一人称控制器预制件有一个子相机对象。

它的身体质量设置为低值(1),并且启用了 IsKinematic。这意味着它将具有有限的动量,不会对其他刚体对象做出反应,但它可以被动画驱动。

它的 FirstPersonController 脚本提供了大量参数,用于跑步、跳跃、音频脚步声等。脚本还包括用于 头部晃动 的参数和动画,当角色移动时,相机会以自然的方式弹跳。如果你在你的 VR 项目中使用 FPSController 脚本,**务必禁用任何头部晃动功能*,否则你可能需要清理键盘上的呕吐物!

RigidBodyFPSController

RigidBodyFPSController 预制体是一个带有 Rigidbody 但没有 CC 组件的第一人称控制器。像 FPSController 一样,它有一个子相机对象。当角色移动时,相机也会随之移动。

RigidBodyFPSController 预制体的质量更重,设置为 10,并且是非运动学(kinematic)的。这意味着当它与其它物体碰撞时,它可以被弹来弹去。它有一个独立的胶囊碰撞器组件,并使用 ZeroFriction 物理材质。RigidBodyFirstPersonController 脚本与 FPSController 脚本不同,但前者有很多相似的参数。

为什么我要在这里详细说明这些内容? 如果你曾在 Unity 中构建过任何非 VR 项目,那么你很可能使用过这些预制体。然而,你可能没有太多关注它们是如何组装的。虚拟现实是从第一人称视角体验的。我们的实现工具箱是 Unity。理解 Unity 中管理和控制这种第一人称体验的工具非常重要。

使用滑行运动

对于本章中的运动功能,让我们采取一种 敏捷 的开发方法。这意味着(部分)我们将首先定义我们的新功能或故事,并附带一系列需求。然后,我们将通过迭代和细化我们的工作,逐步构建和测试这个功能,一次处理一个需求。实验不仅被允许,而且被鼓励。

敏捷软件开发 是一个广泛的术语,指的是那些鼓励以易于应对变化和细化需求的方式,进行小幅度增量迭代开发的方法。请参阅 敏捷宣言

我们想要实现的功能是:作为一个第一人称角色,当我开始行走时,我将朝着我注视的方向在场景中移动,直到我指示停止行走。以下是实现此功能的需求:

  • 向你注视的方向移动

  • 请将双脚稳稳地放在地上

  • 不要穿过固体物体

  • 不要从世界的边缘掉下去

  • 跨过小物体并处理不平坦的地形

  • 通过点击输入按钮开始和停止移动

这听起来是合理的。

首先,如果你有从 第四章 “基于注视的控制” 保存的场景版本,你可以从那里开始。或者,构建一个类似的简单新场景,包含地面平面、一些作为障碍物的 3D 对象,以及你的 MeMyselfEye 预制体的副本。

向你注视的方向移动

我们已经有一个包含相机装置的 MeMyselfEye 对象。我们将将其转换为一个第一人称控制器。我们的第一个要求是在你注视的方向上移动场景。添加一个名为 GlideLocomotion 的脚本。保持简单,让我们先执行以下步骤:

  1. 在层次结构面板中选择 MeMyselfEye 对象

  2. 在检查器面板中,选择添加组件 | 新脚本,并将其命名为 GlideLocomotion

然后,打开脚本并按照以下方式编写代码:

using UnityEngine; 

public class GlideLocomotion : MonoBehaviour 
{ 
  public float velocity = 0.4f; 

  void Update () 
  { 
    Vector3 moveDirection = Camera.main.transform.forward; 
    moveDirection *= velocity * Time.deltaTime; 
    transform.position += moveDirection; 
  } 
} 

人类正常的行走速度大约是每秒 1.4 米。在 VR 中,这可能会让你感到恶心。让我们以比这慢得多,0.4 m/s 的速度移动。在 Update() 中,我们检查玩家当前注视的方向(camera.transform.forward)并以此方向和当前速度移动 MeMyselfEye 的变换位置。

注意变量自我修改的编码快捷键(*=+=)。前述代码的最后两行可以写成如下:

moveDirection = moveDirection * velocity * Time.deltaTime; 
transform.position = transform.position  + moveDirection; 

在这里,我使用了 *=+= 运算符。保存脚本和场景,并在 VR 中尝试。

当你向前看时,你就向前移动。向左看,就向左移动。向右看,就向右移动。它工作得很好!

向上看... 哇!!你期待这个吗?!我们简直在飞! 你可以向上、向下以及全方位移动,就像超人或者操控无人机一样。目前,MeMyselfEye 没有质量,没有物理属性,也不受重力影响。尽管如此,它满足了你在注视的方向上移动的要求。所以,让我们继续。

保持脚在地面

下一个要求是让你保持脚在地面。我们知道 GroundPlane 是平的,位置在 Y = 0。所以,让我们只给 GlideLocomotion 脚本添加这个简单的约束,如下所示:

  void Update () 
  { 
    Vector3 moveDirection = Camera.main.transform.forward; 
    moveDirection *= velocity * Time.deltaTime; 
 moveDirection.y = 0f; 
    transform.position += moveDirection; 
  } 

保存脚本并在 VR 中尝试。

还不错。现在,我们可以在 Y = 0 平面上移动。

另一方面,你就像一个鬼魂,可以轻易地穿过立方体、球体和其他物体。

不要穿过固体物体

第三项要求是不要穿过固体物体。这里有一个想法。给它添加一个刚体组件、一个碰撞体,让物理引擎来处理。按照以下步骤操作:

  1. 在层次结构面板中选择 MeMyselfEye 对象

  2. 在检查器面板中,导航到添加组件 | 物理 | 刚体

  3. 然后添加组件 | 物理 | 胶囊碰撞体

  4. 将胶囊碰撞体高度设置为 2

  5. 如果你的角色控制器胶囊碰撞体(场景窗口中的绿色网格)延伸到地面平面,请调整其 Center Y 到 1

在 VR 中尝试。

哇!!这是怎么了...? 它刚才还一切正常,但一碰到立方体,你就失去了控制,像电影 重力 中糟糕的太空行走一样在各个方向上旋转。嗯,这就是刚体的作用。力被应用到所有方向和轴上。让我们添加以下约束。

在检查器面板的 Rigidbody 面板中,勾选冻结位置:Y 和冻结旋转:X 和 Z 的复选框。

在 VR 中尝试。

现在真是太好了!你能够通过看向某个方向来移动,你不会飞(Y 位置受约束),而且你不会穿过固体物体。相反,你会在它们旁边滑行,因为只允许 Y 旋转。

如果你的KillTarget脚本仍在运行(来自第二章,内容、对象和缩放),你应该能够盯着 Ethan 直到他爆炸。做吧,让 Ethan 爆炸... 哇! 我们被爆炸冲击波吹出了这里,再次在疯狂的方向上失去控制。也许我们还没有准备好使用这个强大的物理引擎。我们可能在脚本中解决这个问题,但暂时让我们放弃 Rigidbody 的想法。我们将在下一章回到它。

你可能还记得,CC 包括胶囊碰撞体,并支持受碰撞约束的运动。我们将尝试使用它,如下所示:

  1. 在检查器面板中,点击 Rigidbody 面板的齿轮图标并选择移除组件

  2. 此外,移除其胶囊碰撞体组件

  3. 在检查器面板中,导航到添加组件 | 物理 | Character Controller

  4. 如果你的 Character Controller 的胶囊碰撞体(场景窗口中的绿色网格)延伸到地面平面,调整其中心 Y 到1

修改GlideLocomotion脚本,如下所示:

using UnityEngine; 

public class GlideLocomotion : MonoBehaviour 
{ 
  public float velocity = 0.4f; 

 private CharacterController character; void Start () 
  { character = GetComponent<CharacterController>(); } 

  void Update () 
  { 
    Vector3 moveDirection = Camera.main.transform.forward; 
    moveDirection *= velocity * Time.deltaTime; 
    moveDirection.y = 0.0f; 
 character.Move(moveDirection); 
  } 
}

代替直接更新transform.position,我们调用了内置的CharacterController.Move()函数,让它为我们完成。它知道角色应该以某些约束行为。

保存脚本并在 VR 中尝试。

这次,当我们撞到物体(一个立方体或球体)时,我们有点翻过它,然后停留在空中。Move()函数不会为我们应用场景中的重力。我们需要在脚本中添加这一点,这并不难(参见 Unity API 文档docs.unity3d.com/ScriptReference/CharacterController.Move.html)。

然而,有一个更简单的方法。CharacterController.SimpleMove()函数为我们应用重力。只需用以下单行替换整个Update()函数:

      void Update () 
      {
        character.SimpleMove(Camera.main.transform.forward * velocity);
      }

SimpleMove()函数负责处理重力和Time.deltaTime。所以,我们只需要给它一个移动方向向量。此外,由于它引入了重力,我们也不需要 Y = 0的约束。简单多了。

保存脚本并在 VR 中尝试。

太棒了!我认为我们已经满足了所有的要求。只是不要走得太远...

本节中的练习假设您正在使用坐姿或站立模式的 VR,而不是房间规模。随着玩家移动,我们正在修改整个 MyMyselfEye 装置。在房间规模中,这意味着移动游戏区域边界。由于我们将碰撞器附加到 MyMyselfEye 位置,如果您从游戏区域中心物理移动,碰撞器将不会与您的实际身体位置对齐。稍后,我们将解决房间规模 VR 中的运动问题。

不要从世界边缘掉落

现在我们有了重力,如果我们从地面平面的边缘走开,你会掉入虚无。解决这个问题并不是第一人称角色的事情。只需在场景中添加一些栏杆即可。

使用立方体,将它们缩放到所需的厚度和长度,并将它们移动到位置。去做吧。我不会给你一步一步的指导。例如,我使用了这些变换:

  • 比例:0.10.110.0

  • 栏杆 1:位置:-510

  • 栏杆 2:位置:510

  • 栏杆 3:位置:01-5;旋转:0900

  • 栏杆 4:位置:015;旋转:0900

在 VR 中试试。尝试穿过栏杆。哇!这更安全。

跨越小物体和处理不平坦地形

当我们做到这一点时,添加一些可以走和跨越的东西,比如斜坡和其他障碍物。结果将看起来像这样:

图片

在 VR 中试试。走上斜坡,然后从立方体上跳下来。嘿,这很有趣!

CC 组件正在处理跨越小物体和处理不平坦地形的要求。您可能想要调整其斜率限制和步偏移设置。

注意:滑行运动可能导致晕动症,特别是对于易感玩家。请在您的应用程序中谨慎使用。这可能会在您沿着斜坡滑行然后跳到地面平面上时变得特别明显。另一方面,有些人喜欢过山车 VR!此外,通过简单的按钮按下机制让玩家控制运动,可以在很大程度上帮助减少恶心和晕动症,我们将在下一部分添加。

开始和停止运动

下一个要求是通过点击输入按钮来开始和停止移动。我们将寻找使用逻辑"Fire1"按钮的按钮按下。如果您想使用不同的按钮,或者如果您正在针对没有"Fire1"映射的平台,请参阅第五章,实用交互项,在基本按钮输入主题下。

按照以下方式修改GlideLocomotion脚本:

using UnityEngine; 

public class GlideLocomotion : MonoBehaviour 
{ 
  public float velocity = 0.7f; 

  private CharacterController controller; 
  private bool isWalking = false; 

  void Start() 
  { 
    controller = GetComponent<CharacterController> (); 
  } 

  void Update () { 
 if (Input.GetButtonDown("Fire1")) 
        isWalking = true;
    else if (Input.GetButtonUp("Fire1"))
        isWalking = false;  if (isWalking) {      controller.SimpleMove (Camera.main.transform.forward * velocity);
    }
  } 
} 

在 Daydream 上,您可以使用GvrControllerInput.ClickButtonDownClickButtonUp

通过添加布尔isWalking标志,我们可以开关前进运动,这可以通过按键来信号。

添加舒适模式运动

我们已经在本章中多次提到运动病感的可能性,以及在这本书的早期。一般来说,你给予玩家在 VR 中移动的更多控制,她就会过得更好,并降低感到不适的风险。提供开始/停止运动的按钮是一步,正如我们刚才看到的。另一步是通常所说的 舒适模式

已经发现,在曲线周围使用滑动移动比简单地直线行走要差。因此,在 VR 场景中移动的一种技术是只允许向前移动,无论玩家朝哪个方向看,然后使用摇杆来改变方向。此外,我们限制摇杆连续改变方向角度,例如,将其限制为固定的 30 度步长。我们将此添加到我们的 GlideLocomotion 脚本中,如下所示。

在类的顶部添加以下变量:

 public float comfortAngle = 30f;
 private bool hasRotated = true;

然后在 Update() 函数中添加以下语句:

  void Update()
  {
    if (Input.GetButtonDown("Fire1"))
      isWalking = true;
    else if (Input.GetButtonUp("Fire1"))
      isWalking = false;

    if (isWalking)
      character.SimpleMove(transform.forward * velocity);

    float axis = Input.GetAxis("Horizontal"); 
    if (axis > 0.5f)
    {
      if (!hasRotated)
        transform.Rotate(0, comfortAngle, 0);
      hasRotated = true;
    }
    else if (axis < -0.5f)
    {
      if (!hasRotated)
        transform.Rotate(0, -comfortAngle, 0);
      hasRotated = true;
    } 
    else
    {
      hasRotated = false;
    }
  }

现在,当按下 "Fire1" 按钮且 isWalking 为真时,我们将 MeMyselfEye 向其变换指示的方向移动,而不是 Camera 的观察方向,将行更改为 character.SimpleMove(transform.forward * velocity)

当用户将摇杆推向右侧时,即逻辑上的 "Horizontal" 轴为正,我们将以顺时针方向旋转装置 30 度(comfortAngle)。当摇杆向左按下时,我们逆时针旋转。我们检查大于 0.5 而不是正好 1.0,这样玩家就不需要将摇杆推到边缘。

我们不希望每次摇杆被按下时都重复旋转,因此我们设置一个标志 hasRotated,然后忽略该轴,直到它静止在零位置。然后,我们将允许玩家再次按下它。

结果是一个舒适的导航机制,一个按钮让你向前移动,另一个按钮让你以大步改变方向。

供你参考,此机制中使用的某些按钮映射如下:

  • 在 HTC VIVE 上的 OpenVR 中,"Fire1" 是一个控制器的菜单按钮,"Horizontal" 是另一个控制器触摸板的触摸。

  • 在 Oculus 上的 OpenVR 中,"Fire1" 是右侧控制器的 B 按钮,"Horizontal" 是左侧控制器的摇杆。

  • 在 Daydream 上,你应该修改代码以使用 GvrControllerInput。为了检测触摸板上的水平点击,调用 GvrControllerInput.TouchPosCentered,它返回一个 Vector2,并检查 x 的值是否在 -11 之间。例如,将调用 GetAxis 替换为以下内容:

    Vector2 touchPos = GvrControllerInput.TouchPosCentered;
    float axis = touchPos.x;
    if (axis > 0.5f) ...

鼓励你扩展 第五章 开头使用的 ButtonTest() 函数,即 Handy Interactables,以确定哪些按钮映射、轴和 SDK 函数最适合你的目标 VR 设备。

我们刚刚实现了滑行移动功能,其中你可以朝着你所看的方向平滑前进,或者使用舒适模式,朝着你身体面向的方向前进,同时你的头部可以四处张望。舒适模式通过让你以 30 度角跳跃改变面向的方向来减少运动病的发生。但即使这样可能还不够舒适,一些开发者(和玩家)甚至更倾向于完全不使用滑行,而是让你能够从一处直接传送到另一处。

其他移动考虑因素

如果你想为你的玩家提供 VR 骑行体验,你可以定义一个预定义的轨道来滑行,就像建筑或艺术画廊的导览一样。轨道可以是 3D 的,不仅上下移动,还有重力,例如 VR 过山车,或者没有重力,例如太空之旅。我们不推荐这种机制,除非是最狂热的寻求刺激者,因为它有很高的可能性引起运动病。

另一种在移动过程中的舒适技术是隧道模式。在移动过程中,摄像头被剪裁,并在玩家的周边视野中显示一个简单的背景,如网格,这样用户就只能看到直接在他们面前的东西。在移动时消除周边视野可以减少运动病的发生。

对于垂直移动,应用程序已经实现了攀爬机制,使用你的手去够、抓和拉自己向上。登山模拟游戏如 The Climb([www.theclimbgame.com/](http://www.theclimbgame.com/))将这一想法提升到了新的水平(字面意思上),提供了多种不同的够取机制和握持类型来抓住。

其他应用程序也尝试使用你的手,不是为了攀爬,而是为了行走。例如,像拉绳子一样伸手和拉扯,或者像跑步者一样摆动手臂,甚至像操作轮椅一样进行圆形拉扯动作。

当然,还有硬件设备,例如使用你的脚走路和跑步来实现移动机制的设备。例如:

您可能需要为该设备编写特定的应用程序,因为这些身体跟踪设备没有标准,但它们确实非常有趣。

传送技巧

指针传送是一种机制,您指向您想要去的位置,然后跳转到那里。没有滑动。您只是传送到新位置。可能绘制一条激光束或弧线,以及一个传送位置接收器,以指示您可以前往的位置。

正如我们在前面的章节中看到的,我们可以编写自己的脚本。但鉴于这是 VR 应用程序的核心功能,传送组件通常包含在设备 SDK 工具包中。我们将编写自己的,并在之后考虑一些提供的组件。

首先,如果您有 第四章 中保存的场景版本,基于注视的控制,您可以从中开始。您可以禁用一些我们不需要的对象,包括 EthanWalkTarget。或者,构建一个类似的简单新场景,包含地面平面、一些作为障碍物的 3D 对象,以及您的 MeMyselfEye 预制件的副本。

准备传送

我们将实现的自定义传送机制将适用于任何 VR 平台,使用基于注视的指向。类似于我们在 第四章,基于注视的控制 中控制僵尸 Ethan 的方式,我们将从玩家的摄像机视图中发射一条射线到地面平面以选择移动到的位置。

在我们的脚本中,我们将使用按钮按下以开始传送,并在选择了一个有效位置后释放以跳转到那里。或者,您可以考虑其他输入,例如使用 Input.GetAxis(vertical) 的拇指杆向前推。

首先,让我们创建一个传送标记(类似于 WalkTarget 的一个),如下所示:

  1. 在层次结构面板中添加一个空的游戏对象,并将其重命名为 TeleportMarker

  2. 将其变换值重置为位置 (0,0,0)(使用变换面板右上角的齿轮图标)。

  3. 右键点击鼠标,导航到 3D 对象 | 圆柱。这将创建一个由 TeleportMarker 作为父对象的圆柱形对象。

  4. 重置其变换并更改缩放为 (0.4, 0.05, 0.4)。这将创建一个直径为 0.4 的扁平圆盘。

  5. 禁用或删除其胶囊碰撞器。

目前,我们将使用默认材质。或者,您可以用另一种材质装饰您的标记。(例如,如果您安装了 Steam InteractionSystem,请尝试 TeleportPointVisible 材质。如果您安装了 Daydream Elements,请尝试 TeleportGlow 材质。)

现在,让我们编写脚本:

  1. 在层次结构面板中选择 MeMyselfEye 对象

  2. 如果存在,禁用或删除 GlideLocomotion 组件

  3. 选择添加组件 | 新脚本,并将其命名为 LookTeleport

编写以下脚本:

using UnityEngine;

public class LookTeleport : MonoBehaviour
{
    public GameObject target;
    public GameObject ground;

    void Update()
    {
        Transform camera = Camera.main.transform;
        Ray ray;
        RaycastHit hit;

        if (Input.GetButtonDown("Fire1"))
        {
          // start searching
          target.SetActive(true);
        }
        else if (Input.GetButtonUp("Fire1")) 
        {
          // done searching, teleport player
          target.SetActive(false);
          transform.position = target.transform.position;
        }
        else if (target.activeSelf)
        {
          ray = new Ray(camera.position, camera.rotation * Vector3.forward);
          if (Physics.Raycast(ray, out hit) && 
              (hit.collider.gameObject == ground))
          {
            // move target to look-at position
            target.transform.position = hit.point;
          }
          else
          {
            // not looking a ground, reset target to player position
            target.transform.position = transform.position;
          }
       }
    }
}

脚本的工作原理如下:

  • 当玩家点击时,开始定位,并将目标标记设置为可见 (SetActive(true)).

  • 在瞄准时,我们识别玩家正在看什么(Raycast)。如果是地面,我们将目标定位在那里(hit.point)。否则,目标将重置到玩家的位置。

  • 当玩家停止按按钮时,目标将被隐藏。我们将玩家定位到目标当前位置,从而完成传送。

注意,我们正在使用 TeleportMarker 目标来存储在瞄准模式下的传送机制的状态。当目标处于活动状态时,我们正在瞄准。当我们退出瞄准时,我们使用目标的位置作为新的玩家位置。

保存脚本并在 Unity 中:

  1. GroundPlane 对象拖放到地面槽中

  2. TeleportMarker 对象拖放到目标槽中

按下播放。按下输入按钮将激活目标标记,它随着你的视线移动。在释放按钮时,你将传送到该位置。你可以通过看向地面以外的其他东西并释放按钮来取消传送。

在表面之间传送

在之前的脚本中,我们使用普通的 Raycast 来确定放置 TeleportMarker 的位置。这仅在平面对象上有效。对于任何其他 3D 对象,击中点可能是任何表面,而不仅仅是可通行的顶部表面。

另一种方法是使用 NavMesh 识别场景内可以传送到的表面。第四章中的“基于注视的控制”,我们为 Ethan 的 AIThirdPersonController 生成了一个 NavMesh,以控制他可以自由游荡的地方。这次,我们也使用 NavMesh 来确定我们(MeMyselfEye)可以去的地方。请随意回顾我们关于 NavMesh 的对话。

这种方法的优点是可用的传送位置可以是地面平面的子集。可以有多个其他对象表面,甚至复杂的地面。传送位置将限制在有效的平坦或略微倾斜的表面上。

如果您跳过了该部分,或者自那时起您已经重新排列了场景中的对象,我们现在将重新生成 NavMesh:

  1. 选择 Navigation 面板。如果它还不是您编辑器中的一个选项卡,请从主菜单打开导航窗口,导航到 Window | Navigation。

  2. 选择其 Object 选项卡。

  3. 在 Hierarchy 中选择地面平面,然后在导航窗口的对象面板中,勾选导航静态复选框。(或者,您也可以使用对象的检查器窗口中的静态下拉列表。)

  4. 对应于应该阻止你可能的传送位置的对象(如立方体、球体等)重复步骤 3。

为了演示,我们现在还将添加一个第二层平台:

  1. 在 Hierarchy 中创建一个新的 3D 立方体,并将其命名为 Overlook

  2. 将其缩放设置为 (2.5, 0.1, 5),并将其位置设置为 (4, 2.5, 0.5)

  3. 在导航窗口中,选择对象选项卡,然后为 overlook 勾选导航静态。

  4. 选择烘焙选项卡,然后单击面板底部的 Bake 按钮

注意,平台的高度(Y 轴)在导航烘焙设置中大于代理高度(2)。这将确保玩家可以进入平台下方并在其上方行走。在场景窗口中,你可以看到由 NavMesh 定义的蓝色区域,如下所示,包括二楼平台上的一个不错的观景区:

图片

我们现在可以修改脚本,以便在 NavMesh 上而不是在地面上找到我们的传送目标位置。不幸的是,Unity 并没有提供用于直接在 NavMesh 上找到击中点的 Raycast 函数。相反,我们像往常一样使用物理碰撞体(可能位于对象的侧面或底部,而不仅仅是可通行表面)来找到击中点,然后调用 NavMesh.SamplePosition 来找到 NavMesh 上的击中点位置。按照以下方式修改 LookTeleport 脚本。

在脚本顶部添加以下行以访问 NavMesh API:

using UnityEngine.AI;

现在,按照以下方式修改 Update()

  if (Physics.Raycast(ray, out hit))
  {
    NavMeshHit navHit;
    if (NavMesh.SamplePosition(hit.point, out navHit, 1.0f, NavMesh.AllAreas))
      target.transform.position = navHit.position;
  }

NavMesh.SamplePosition 的调用使用 hit.point 并在给定的半径内找到 NavMesh 上的最近点(我们给出了 1.0)。

按下播放。现在,你可以在 GroundPlane 的可通行表面上设置 TeleportMarker,甚至可以在观景台上方设置!

还有一件事。执行 Physics.Raycast 可能相当昂贵,尤其是在有很多对象的场景中。你可以通过提供层掩码来限制 Raycast 的搜索。例如,创建一个名为 Teleport 的层,并将此层设置为 GroundPlane 和 Overlook 游戏对象。然后,按照以下方式修改 Raycast 调用:

  if (Physics.Raycast(ray, out hit, LayerMask.GetMask("Teleport")))

这将限制我们的 Raycast 只在 NavMesh 上叠加的表面,即地面平面和观景台。

我们接下来要考虑的下一个场景是完全不允许自由漫游,而是设置一组有限的传送位置。

传送出生点

在 VR 应用程序中,限制传送只限于场景中的特定预定义位置是非常常见的。在这种情况下,你不需要任何自由漫游的滑行移动或任意的传送目标。相反,你可以定义特定的传送出生点。让我们看看如何做到这一点。

首先,让我们创建一个 TeleportSpawn 预制件来标记我们的位置:

  1. 在层次结构中,创建一个 3D 球体并将其命名为 TeleportSpawn

  2. 重置其变换(齿轮图标 | 重置)

  3. 将其缩放设置为 0.40.40.4

  4. 将其位置设置为类似(203

  5. 从检查器 | 层 | 添加层创建一个新的层命名为 TeleportSpawn 并在空槽中填写名称

  6. 再次在层次结构中选择 TeleportSpawn 对象,并将它的层(层 | TeleportSpawn)设置为刚刚定义的那个

让我们快速创建一个材质:

  1. 在你的材质文件夹中,右键单击以创建一个新的材质并命名为 Teleport Material

  2. 将其渲染模式设置为透明

  3. 设置其 Albedo 颜色并给予一个低 alpha 值(例如 30)以便它是半透明的,例如我们的浅绿色(702307030

  4. 将材质拖放到 TeleportSpawn 对象上

对于这个练习,我们将用新的 LookSpawnTeleport 替换 MeMyselfEye 上的 LookTeleport 组件:

  1. 在层级中选择 MeMyselfEye

  2. 如果存在,禁用 LookTeleport 组件

  3. 添加组件 | 新脚本,并将其命名为 LookSpawnTeleport

按照以下方式编写新脚本:

using UnityEngine;

public class LookSpawnTeleport : MonoBehaviour 
{
  private Color saveColor;
  private GameObject currentTarget;

    void Update()
    {
        Transform camera = Camera.main.transform;
        Ray ray;
        RaycastHit hit;
        GameObject hitTarget;

        ray = new Ray(camera.position, camera.rotation * 
        Vector3.forward);
        if (Physics.Raycast(ray, out hit, 10f, 
              LayerMask.GetMask("TeleportSpawn")))
        {
            hitTarget = hit.collider.gameObject;
            if (hitTarget != currentTarget) 
            {
                Unhighlight();
                Highlight(hitTarget);
            }

            if (Input.GetButtonDown("Fire1"))
            {
                transform.position = hitTarget.transform.position;
            }
        }
        else if (currentTarget != null)
        {
            Unhighlight();
        }
    }
}

Update() 函数执行一次 Raycast 来查看是否有任何出生点对象被选中。如果是这样,该对象将被突出显示(取消突出显示任何之前的对象)。然后,如果按下 Fire1 按钮,它将玩家传送到那个位置。

我们添加了几个私有辅助函数,Highlight()Unhighlight()。第一个通过修改材质颜色使其更不透明(alpha 0.8)来突出显示一个对象,使其更不透明。当你看开时,Unhighlight 恢复原始颜色:

    private void Highlight(GameObject target)
    {
        Material material = target.GetComponent<Renderer>().material;
        saveColor = material.color;
        Color hiColor = material.color;
        hiColor.a = 0.8f; // more opaque
        material.color = hiColor;
        currentTarget = target;
    }

    private void Unhighlight()
    {
        if (currentTarget != null)
        {
          Material material = currentTarget.GetComponent<Renderer>().material;
          material.color = saveColor;
          currentTarget = null;
        }
    }

好的,现在让我们在场景周围放置一些标记:

  1. TeleportSpawn 对象从层级拖放到项目资产中的 Prefabs 文件夹

  2. TeleportSpawn 复制三次

  3. 将其中一个放置在 (0, 0, -1.5)(默认的 MeMyselfEye 位置)

  4. 将其他对象移动到合适的位置,例如 (2, 0, 3), (-4, 0, 1),如果你有 Overlook,则 (3.5, 2.5, 0)

好的!按 Play。当你看向一个出生点时,它会突出显示。当你按下 Fire1 按钮时,你会传送到那个位置。

在 第六章 “世界空间 UI” 主题下的 The reticle cursor 部分中,我们像这样做了一个小光标(小光标),在摄像机视图中添加一个光标(小光标)可能很有用,以帮助玩家集中注意力在传送对象上:

虽然传送是有效的,但如果它还设置你的视图方向会更好。一种方法是仔细放置 TeleportSpawn 对象,使其面向我们希望玩家面对的方向,并设置玩家的变换旋转,以及位置。

为了给出生点指向的方向提供一个视觉提示,我们将添加一个图形。我们在这本书中包含了一个图像文件,flip-flops.png。否则,使用任何表示前进方向的标志。执行以下步骤:

  1. 通过将其拖放到你的 Project Textures 文件夹(或导航到 Import New Asset...)导入 flip-flops.png 纹理。

  2. 在材质文件夹中创建一个新的材质,并将其命名为 FlipFlops

  3. flip-flops 纹理拖放到 FlipFlops 材质的 Albedo 映射上,并将渲染模式选择为 Cutout。

  4. 在层级中选择 TeleportSpawn 对象。

  5. 创建一个子 Quad 对象(右键单击创建 | 3D 对象 | Quad)。

  6. FlipFlops 材质拖放到 Quad

  7. 将 Quad 的 Transform Position 设置为 (0, .01, 0),并将其 Rotation 设置为 (90, 0, 0),使其平躺在地面平面上。

  8. 选择父级 TeleportSpawn 对象,并在检查器中按 Apply 保存这些更改到预制体。现在所有出生点都将有脚

  9. 注意,对于位于 Overlook 之上的对象,你可以调整其 Quad,使其从下方可见,例如位置(0, -0.2, 0)和旋转(-90, 0, 180)

我们脚本中应用旋转的修改是微不足道的:

            if (Input.GetButtonDown("Fire1"))
            {
                transform.position = hitTarget.transform.position;
                transform.rotation = hitTarget.transform.rotation;
            }

就这样,这是一个基于目光的、具有预定义出生点的传送系统,如图所示在场景窗口中:

图片

其他传送考虑因素

关于传送,还有很多可以说的和可以做的。你可能更喜欢使用手柄控制器而不是目光选择位置。通常使用弧形激光束(使用贝塞尔曲线)来显示传送指针。传送出生点通常使用发光或火焰效果。许多这些功能已经通过高级 VR 工具包构建并提供了(见下一主题)。

闪烁传送是一种在玩家位置变化之间进行淡出淡入的技术。据说这提供了一种额外的舒适度。我们这里不会展示代码,但有一些实现 VR 中淡入淡出的技术,例如创建一个覆盖整个相机的屏幕空间画布,使用黑色面板,并在淡入淡出时 lerping 其 alpha 通道(见docs.unity3d.com/ScriptReference/Mathf.Lerp.html)。有些人甚至发现使用真实的眨眼效果进行淡入淡出非常自然,其中你从上到下快速淡出,然后从下到上淡入,就像眼睑的闭合和打开。

另一种技术是从上方提供场景的第三人称视角,有时称为迷你地图上帝视角玩具屋视角。从这个视角,玩家可以指向一个新位置进行传送。这个场景的迷你版本可以是玩家在主场景中用作工具的对象,或者你在传送选择过程中切换到这种视图模式。

你也可以传送到不同的场景。结合闪烁淡入淡出,你调用SceneManager.LoadScene("OtherSceneName")而不是简单地改变变换位置。注意,你必须将其他场景添加到构建设置场景的构建列表中(见docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.LoadScene.html)。

聪明地使用传送和玩家的方向可以有效地利用有限的游玩空间,并给人一种 VR 空间比现实中更大的感知。例如,在房间规模 VR 中,如果你让玩家走向游玩空间的边缘并进入电梯(传送),她可能会面对电梯的背面进入,并在新级别的门打开时必须转身,此时可以物理地向前行走。实际上,可以通过这种方式实现无限走廊和相连的房间,同时保持玩家的沉浸感。

传送工具包

我们已经探索了多种不同的移动和传送机制。它们都使用你的注视方向进行选择。这有时是最佳选择。有时则不是。这无疑是各种 VR 设备之间最低的共同点,从高端的 HTC VIVE 和 Oculus Rift 到低端 Google Cardboard,基于注视的简单点击选择始终可用。

很可能你更喜欢使用手柄控制器进行选择。高端系统包括两个位置跟踪控制器,每个手一个。低端设备,如 Google Daydream,包括一个单一的 3DOF“激光指针”控制器。我们之前避免使用控制器实现的原因之一是代码在不同设备之间差异很大。此外,特定设备的工具包通常附带实现此机制所需的组件和预制体,针对特定平台进行了优化,包括用于渲染弧形激光束和传送标记的高性能着色器。

在本节中,我们将展示如何使用这些高级组件实现传送,使用 SteamVR 交互系统和 Google Daydream Elements。如果你没有使用这些工具之一,请查看你的目标设备的工具包项目,或者考虑使用通用的工具包,如开源的 VRTK (github.com/thestonefox/VRTK)。

使用 SteamVR 交互系统进行传送

我们在第五章中首次介绍的 SteamVR 交互系统,Handy Interactables,包括易于使用的传送组件。如果你使用 SteamVR SDK,它可以在Assets/SteamVR/InteractionSystem/Teleport/文件夹中找到。传送工具包括许多我们没有机会自己实现的额外功能,包括材质、模型、预制体、脚本、着色器、声音、纹理、触觉等!

具体来说,传送工具包包括:

  • Teleporting预制体:传送控制器,每个场景添加一个

  • TeleportPoint预制体:你想要传送到的位置,每个位置添加一个

  • TeleportArea组件:添加到游戏对象,例如一个平面,以允许在该区域内任意位置进行传送

交互系统包括其自带的Player相机装置,它取代了我们一直使用的默认[CameraRig],如下所示:

  1. SteamVR/InteractionSystem/Core/Prefabs中定位Player预制体

  2. 将其拖动为场景HierarchyMeMyselfEye的子对象

  3. 删除或禁用[CameraRig]对象

  4. 从项目Assets/SteamVR/InteractionSystem/Teleport/Prefabs中拖动Teleporting预制体的副本作为MeMyselfEye的子对象(这个控制器实际上可以在场景中的任何地方移动)

  5. Hierarchy中选择玩家,并将它的父对象MeMyselfEye拖动到其跟踪原点变换槽中

这最后一步很重要。工具包的传送组件默认会改变 Player 对象的位置。当我们传送时,我们希望传送 Player 的父对象 MeMyselfEye。如果在你的游戏中,例如,玩家坐在车辆的驾驶舱中,而你打算传送整个车辆,而不仅仅是玩家本身,这也可能被使用。

如果你遵循了本章前面的项目,请禁用我们在这里不会使用的东西:

  1. MyMyselfEye 上禁用或移除 Look Teleport 和 Look Spawn Teleport 组件

  2. 禁用或删除每个 TeleportSpawn 对象

现在,对于每个传送位置:

  1. TeleportPoint 预制件的副本从项目 Assets/SteamVR/InteractionSystem/Teleport/Prefabs 拖到层次结构中

  2. 将其放置在场景中的任何位置。如前所述,我们使用了 (0, 0, -1.5), (2, 0, 3), (-4, 0, 1), 以及在 Overlook 位置 (3.5, 2.5, 0)

就这样!按下播放。传送点不会显示,直到你按下控制器上的按钮,然后它们会发光,一条虚线激光弧让你选择一个,然后你就可以到达那里。在此处显示的游戏窗口中,我正在传送至 Overlook 位置:

请查看 Teleport 组件上的许多选项。您可以修改或替换用于突出显示传送点的材质、声音和其他效果。Teleport Arc 组件提供了渲染激光弧的选项,而 TeleportPoints 本身也可以分别进行修改。

使用 Daydream Elements 进行传送

我们在第五章中首次介绍的 Google Daydream Elements 包,Handy Interactables 包含一些传送组件。如果你针对 Google Daydream,你可以从 GitHub 安装单独的 Daydream Elements 下载(github.com/googlevr/daydream-elements/releases)。相关文档可以在 Elements 网站上找到(developers.google.com/vr/elements/teleportation)。

一旦导入到你的项目中,它可以在 Assets/DaydreamElements/Elements/Teleport/ 文件夹中找到。这里有一个演示场景,Teleport,以及相关的材质、模型、预制件、脚本、着色器和纹理。

默认情况下,这些工具相当通用且可高度自定义。主要的预制件是 TeleportController,它负责所有工作。用于触发传送行为的用户输入可以通过在 Unity 编辑器中填充组件槽进行配置,如下所示:

你可以通过更改其 探测器可视化器过渡 类来扩展传送器。

  • 探测器:例如,ArcTeleportDetector 会进行曲线弧形射程以在场景中找到对象,并将击中限制在具有足够空间“适合”玩家的水平表面上,这样你就不会传送到墙壁中。

  • 可视化器:例如ArcTeleportVisualizer,在触发传送时渲染弧线。

  • 过渡:例如LinearTeleportTransition,将玩家动画移动到新位置。这可以修改以实现眨眼效果,例如。

要将其添加到您的场景中:

  1. TeleportController预制件拖放到您的层次结构中,作为 Player 的子对象(对我们来说就是MeMyselfEye | GVRCameraRig | Player)。

  2. 如有必要,重置其变换。

  3. MeMyselfEye对象拖放到TeleportController组件的 Player 变换槽中。

  4. GvrControllerPointer(或您正在使用的任何控制器游戏对象)拖放到控制器变换槽中。

按下播放,您可以在场景的任何地方传送。不需要放置特定的传送目标。

默认情况下,TeleportController将通过让您在任何具有碰撞器的场景对象上着陆来工作。您可以通过指定层来限制检测器 Raycast 考虑的对象。此外,如果您想在场景中添加任意形状的目标区域,这些区域不一定是游戏对象,您可以添加仅具有碰撞器而没有渲染器的对象集。这就是在 Daydream Elements 传送演示中实现岛屿上的传送区域的方式。

重置中心和位置。

有时在 VR 中,头戴式设备中呈现的视图与您的身体方向不完全同步。设备 SDK 提供函数以重置头戴式设备相对于真实世界空间的方向。这通常被称为视图的居中

Unity 提供了一个 API 调用,它映射到底层设备 SDK 以重新居中设备,UnityEngine.VR.InputTracking.Recenter()。此函数将跟踪中心定位到 HMD 的当前位置和方向。它仅适用于坐姿和站姿体验。房间规模体验不受影响。

在撰写本文时,Recenter 在 SteamVR 中不起作用,即使是坐姿配置。解决方案是调用以下代码:

Valve.VR.OpenVR.System.ResetSeatedZeroPose();
Valve.VR.OpenVR.Compositor.SetTrackingSpace(Valve.VR.ETrackingUniverseOrigin.TrackingUniverseSeated);

Daydream 控制器在底层系统中集成了重置功能(按住系统按钮)。这是因为在不想要的漂移在移动 VR 设备上非常常见。此外,对于 Cardboard(以及没有控制器的 Daydream 用户),您应该在玩家装置中包含一个标准的地面画布菜单,该菜单应包括重置和居中按钮(正如我们在第三章,VR 构建和运行)中做的那样)。

在其他系统上,您可以根据需要选择一个触发调用Recenter的按钮。

支持房间规模传送。

如前所述,Unity Recenter 功能对房间规模设置没有任何影响。我们假设房间规模玩家是站立并活跃的,因此他们可以在 VR 场景中自行转身面对“前方”。

然而,当我们传送时,我们正在将玩家移动到新的位置,可能是一个完全不同的场景。当我们重新定位 MyMyselfEye 或任何位置跟踪摄像机的父对象时,玩家不必位于该装置的原点。如果玩家传送到新位置,他的整个游戏空间应该被传送,并且玩家最终应该站在他特别选择的虚拟位置上。

以下函数将补偿传送变换,使玩家在游戏空间中的相对姿态。按照编写方式,它假定它是一个位于 MeMyselfEye 玩家根对象上的组件:

private void TeleportRoomscale( Vector3 targetPosition )
{
    Transform camera = Camera.main.transform;
    float cameraAngle = camera.eulerAngles.y;
    transform.Rotate( 0f, -cameraAngle, 0f);
    Vector3 offsetPos = camera.position - transform.position;
    transform.position = targetPosition.position - offsetPos;
}

在我们之前的传送脚本示例中使用它时,将transform.position = target.transform.position;行替换为对TeleportRoomscale(target.transform.position)的调用。

管理 VR 运动病

VR 运动病,或称模拟器病,是虚拟现实中的一个真实症状和关注点。研究人员、心理学家和技术人员,拥有广泛的专长和博士学位,正在研究这个问题,以更好地理解潜在的原因并找到解决方案。

VR 运动病的一个原因是,当你移动头部时,屏幕更新滞后,或者延迟。你的大脑期望你周围的世界会精确同步地改变。任何可感知的延迟都可能让你感到不舒服,至少可以说。

通过更快地渲染每一帧,保持推荐的每秒帧数,可以减少延迟。设备制造商将其视为他们需要解决的问题,无论是硬件还是设备驱动程序软件。GPU 和芯片制造商将其视为处理器性能和吞吐量问题。我们无疑将在未来几年看到显著的改进。

同时,作为 VR 开发者,我们需要意识到延迟和 VR 运动病的其他原因。开发者需要将其视为我们自己的问题,因为最终,这取决于性能和人体工程学。在移动 VR 与桌面 VR 持续的二分法中,玩家将使用的设备性能始终会有上限。在第十三章优化性能和舒适度中,我们深入探讨了渲染管道和性能的技术细节。

但问题不仅仅是技术。我乘坐现实世界的过山车也会感到恶心。那么,为什么 VR 过山车不会有类似的效果呢?以下是一些有助于提高玩家舒适度和安全性的考虑因素,包括以下游戏机制和用户体验设计:

  • 不要快速移动:当移动或动画化第一人称角色时,不要移动得太快。在游戏机和个人电脑上运行的高速第一人称射击游戏在 VR 中可能效果不佳。

  • 向前看:当你穿过一个场景时,如果你向侧面看而不是直视前方,你更有可能感到恶心。

  • 不要快速转动头部:使用 VR 头盔时,不要鼓励用户快速转动头部。在小的时片内视口的大幅变化加剧了更新 HMD 屏幕的延迟。

  • 提供舒适模式:当场景需要你快速多次转身时,提供一种旋转机制,也称为舒适模式,允许你以更大的增量改变观察方向。

  • 在传送和场景转换期间使用淡入或闪烁剪辑。在淡入时,过渡到深色,因为白色可能会令人震惊。

  • 在移动过程中使用隧道或其他技术。通过遮挡相机除了你正前方之外的所有可见内容,减少视野中的可见内容。

  • 使用第三人称摄像机:如果你有高速动作,但并不一定打算给用户带来刺激的体验,可以使用第三人称摄像机视角。

  • 保持脚踏实地:提供有助于用户保持平衡的视觉提示,例如地平线线、视野中的附近物体以及相对固定的位置物体,如仪表盘和身体部位。

  • 提供重新居中视图的选项:特别是移动 VR 设备容易发生漂移,有时需要重新居中。对于有线 VR 设备,这有助于你避免被 HMD 线缠住。作为一个安全问题,将你的视图相对于现实世界重新居中可能有助于你在物理空间中避免撞到家具和墙壁。

  • 不要使用剪辑场景:在传统游戏(和电影)中,用于在关卡之间切换的技术是显示一个 2D 剪辑电影。如果禁用了头部运动检测,这在 VR 中是不起作用的。它会破坏沉浸感并可能导致恶心。一种替代方法是简单地淡入黑色,然后打开新场景。

  • 优化渲染性能:所有 VR 开发者都应该了解延迟的根本原因——特别是渲染性能——以及你可以做什么来优化它,例如降低多边形数量和仔细选择光照模型。学会使用性能监控工具,以保持每秒帧数在预期和可接受的范围内。更多内容将在第十章中讨论,使用所有 360 度

  • 鼓励用户休息:或者,你可能只是提供一款游戏呕吐袋!或者不提供。

摘要

在本章中,我们探讨了在虚拟环境中移动的许多不同方式。我们首先检查了 Unity 支持的传统第三人称和第一人称角色的组件,并很快意识到这些功能在 VR 中并不太有用。例如,我们不希望应用程序在我们行走时上下晃动头部,我们也不一定想从建筑物上跳下来。移动很重要,但玩家的舒适度更为重要。你不想引起运动病。

机动性是平滑且线性地在场景中移动,类似于行走。我们利用基于注视的机制实现了朝你注视的方向移动,并使用输入按钮来开始和停止。然后,我们将机动性与头部方向分离,始终朝“前方”移动,并使用单独的输入(拇指盘)来改变身体面向的角度。在这种舒适模式下,你可以移动的同时四处张望。

跳到新位置被称为传送。我们再次从基于注视的机制开始,让你选择一个你注视的传送位置。我们实现了几种限制你允许传送位置的方法,包括使用导航网格和使用传送出生点。然后,我们研究了几个传送工具包,如 SteamVR 和 Google Daydream,它们提供了一套丰富的功能,以及一个丰富的用户体验,这些功能从零开始实现并不简单。如果你针对的是不同的平台,例如 Oculus,也有类似的工具。

在下一章中,我们将更深入地探索 Unity 物理引擎并实现一些交互式游戏。

第八章:与物理和火焰玩耍

在本章中,我们将使用物理和其他 Unity 功能来构建交互式球类游戏的变体。在这个过程中,我们将探讨管理对象、Rigidbody 物理以及增加虚拟体验的交互性。您将看到如何将基于物理的属性和材料添加到对象中,以及更多关于 C# 脚本、粒子效果和音乐的内容。

在本章中,您将学习以下主题:

  • Unity 物理引擎、Unity Rigidbody 组件和物理材料

  • 使用速度和重力

  • 管理对象生命周期和对象池

  • 使用头部和双手在 VR 中与对象交互

  • 使用粒子效果构建火球

  • 与音乐同步

注意,本章中的项目是独立的,并不直接需要本书其他章节中的其他章节。如果您决定跳过其中任何部分或未保存您的作品,那都是可以的。

Unity 物理引擎

在 Unity 中,基于物理的对象的行为与其网格(形状)、材质(UV 纹理)和渲染器属性是分开定义的。影响物理的项包括以下内容:

  • Rigidbody:使对象能够在物理引擎的控制下行动,接收力矩以实现现实的方式移动

  • Collider:定义了用于计算与其他对象碰撞的简化、近似形状的对象

  • Physic Material:定义了碰撞对象的摩擦和弹跳效果

  • Physics Manager:为您的项目应用 3D 物理的全局设置

基本上,物理(在此上下文中)是由影响对象变换的位置和旋转力定义的,例如重力、摩擦、动量和与其他对象的碰撞。它并不一定是现实世界中物理的完美模拟,因为它针对性能和关注点的分离进行了优化,以促进动画。此外,虚拟世界可能只需要它们自己的物理定律,而这些定律在我们的宇宙中是找不到的!

Unity 集成了 NVIDIA PhysX 引擎,这是一种实时物理计算中间件,为游戏和 3D 应用实现了经典牛顿力学。此多平台软件在存在时针对快速硬件处理器进行了优化。它可以通过 Unity 脚本 API 访问。

物理的关键是添加到对象中的 Rigidbody 组件。Rigidbody 具有重力、质量、阻力等参数。Rigidbody 可以自动对重力和其他对象的碰撞做出反应。无需额外的脚本编写。在游戏过程中,引擎计算每个刚体对象的动量并更新其变换位置和旋转。

有关 Rigidbody 的详细信息可以在 docs.unity3d.com/ScriptReference/Rigidbody.html 找到。

Unity 项目有一个全局重力设置,可以在项目设置中的物理管理器中找到,方法是导航到 Edit | Project Settings | Physics。正如您所期望的,默认的重力设置是一个具有值的 Vector3 (0, -9.81, 0),它对所有 Rigidbody 应用向下的力。重力以每秒平方米计算。

刚体可以自动对重力和其他物体的碰撞做出反应。不需要额外的脚本编写。

为了检测碰撞,碰撞的物体都必须具有 Collider 组件。有内置的具有基本几何形状的碰撞器,例如立方体、球体、圆柱体和胶囊。网格碰撞器可以假设任意形状。如果可能的话,最好使用一个或多个基本碰撞器形状,这些形状大致适合实际物体,而不是使用网格碰撞器,以减少游戏过程中实际碰撞计算的消耗。Unity 要求,如果您的对象将用于物理并且具有 Rigidbody,则其网格碰撞器必须标记为凸形,并且限制为 255 个三角形。

当刚性物体碰撞时,碰撞中每个物体的相关力会作用于其他物体。这些力的值是根据物体的当前速度和体重计算的。还会考虑其他因素,例如重力(即阻力)。此外,您可以选择添加约束来冻结给定物体在任何 xyz 轴上的位置或旋转。

当将物理材料分配给对象的碰撞器时,计算可以进一步受到影响,这会调整碰撞物体的摩擦和弹性效果。这些属性仅适用于拥有物理材料的对象。(注意,由于历史原因,它实际上拼写为 Physic Material 而不是 Physics Material。)

因此,假设对象 A(球)击中对象 B(砖块)。如果对象 A 有弹性而对象 B 没有,则在碰撞中对象 A 将会有一个冲量,但对象 B 不会有。然而,您可以选择确定它们的摩擦和弹性的组合方式,正如我们接下来将要看到的。这并不一定是一个真实世界物理的准确模拟。这是一个游戏引擎,而不是计算机辅助工程模型器。

从脚本的角度来看,Unity 会在物体碰撞时触发事件(OnTriggerEnter),在物体碰撞的每一帧触发(OnTriggerStay),以及当它们停止碰撞时(OnTriggerExit)。

如果这听起来令人畏惧,请继续阅读。本章的其余部分将将其分解成可理解的部分。

弹性球

我们在这里要实现的功能是,当一个球从空中落下并击中地面时,它会弹起并上下跳动,然后再次弹起,随着时间的推移逐渐减弱。

我们将从一个新的场景开始,该场景由地面平面和球体组成。然后,我们将逐步添加物理效果,如下所示:

  1. 通过导航到 File | New Scene 创建一个新的场景。

  2. 然后,导航到文件 | 保存场景为... 并将其命名为 BallsFromHeaven

  3. 通过导航到 GameObject | 3D Object | Plane 创建一个新的平面,并使用 Transform 组件的 齿轮 图标重置其变换。

  4. 通过导航到 GameObject | 3D Object | Sphere 创建一个新的球体,并将其重命名为 BouncyBall

  5. 将其缩放设置为 (0.5, 0.5, 0.5) 并将其位置设置为 (0, 5,0),使其位于平面的中心上方。

  6. 将项目资产中的红色材质拖到它上面,使其看起来像一个弹跳球。

新的 Unity 场景默认包含方向光和主摄像机。暂时使用这个主摄像机是可以的。

点击 播放 按钮。没有任何事情发生。球就停在空中,不动。

现在,让我们给它一个 Rigidbody,如下所示:

  1. BouncyBall 选择的情况下,在检查器中,导航到添加组件 | 物理 | Rigidbody。

  2. 点击 播放 按钮。它像铅球一样掉落。

让它弹跳,如下所示:

  1. 在项目面板中,选择顶级资产文件夹,导航到创建 | 文件夹,并将其重命名为 Physics

  2. Physics 文件夹中选择,通过导航到 Assets | 创建 | 物理材质 (或在该文件夹内右键单击)创建一个材质。

  3. 将其命名为 Bouncy

  4. 将其弹跳性值设置为 1

  5. 在层次结构中选择 BouncyBall 球体,将 Bouncy 资产从项目拖到检查器中球体的碰撞器材质字段。

点击 播放 按钮。球弹跳了,但并没有跳得很高。我们使用了最大值 1.0 作为弹跳性。是什么让它减速?不是摩擦设置。而是弹跳组合设置为平均,这决定了球(1)的弹跳性与平面(0)的弹跳性混合的程度。因此,它随时间迅速减弱。我们希望球保持所有的弹跳性。我们将按以下方式实现:

  1. Bouncy 对象的弹跳组合改为最大。

  2. 点击 播放 按钮。

好多了。实际上,好得太多了。球一直弹跳回到原来的高度,忽略了重力。现在,将弹跳性改为 0.8。弹跳减弱,球最终会停下来。

让我们在 VR 中检查它,如下所示:

  1. 从层次结构根删除默认的主摄像机。

  2. MeMyselfEye 预制件从项目资产拖到场景中。将其位置设置为 (0, 0, -4)。

在 VR 中运行它。非常酷!即使在 VR 中最简单的东西看起来也很令人印象深刻。

Unity 的标准资产包包括一些示例物理材质,包括 Bouncy、Ice、Meta、Rubber 和 Wood。

好的,让我们来点乐趣。让它下起弹跳球雨!为此,我们将球体制作成预制件,并编写一个脚本,实例化新的球体,从随机位置掉落,如下所示:

  1. BouncyBall 对象从层次结构拖到 Project Assets/Prefabs 文件夹中,使其成为一个预制件。

  2. 从层次结构中删除 BouncyBall 对象,因为我们将通过脚本实例化它。

  3. 通过导航到 GameObject | 创建空对象来创建一个空的游戏控制器对象,并将其命名为 GameController

  4. 在检查器中,导航到添加组件 | 新脚本,将其命名为 BallsFromHeaven,并打开脚本进行编辑。

编辑脚本,使其看起来像这样:

using UnityEngine; 

public class BallsFromHeaven : MonoBehaviour 
{ 
  public GameObject ballPrefab; 
  public float startHeight = 10f; 
  public float interval = 0.5f; 

  private float nextBallTime = 0f; 

  void Update () 
  { 
    if (Time.time > nextBallTime) 
    { 
      nextBallTime = Time.time + interval; 
      Vector3 position = new Vector3( Random.Range (-4f, 4f), 
         startHeight, Random.Range (-4f, 4f) ); 
      Instantiate( ballPrefab, position, Quaternion.identity ); 
    } 
  } 
} 

脚本以每 interval 秒的速率从 startHeight 位置掉落一个新的球体(0.5 的间隔意味着每半秒掉落一个新的球体)。新球的位置在 -44 之间的随机 X-Z 坐标。Instantiate() 函数将一个新的球体添加到场景层次结构中。

保存脚本。我们现在需要用 BouncyBall 预制件填充 Ball 字段,如下所示:

  1. 在层次结构中选择 GameController,将 BouncyBall 预制件从 Project Assets/Prefabs 文件夹拖到检查器中 Balls From Heaven(脚本)面板的 Ball 预制件槽中。

  2. 请确保使用来自项目资源的 BouncyBall 预制件,以便可以实例化。

  3. 保存场景。在 VR 中运行它。很有趣!

这是我得到的结果:

图片

总结来说,我们创建了一个带有 Rigidbody 的球体,并添加了一个具有 0.8 弹性系数和最大弹跳组合的物理材质。然后,我们将 BouncyBall 保存为预制件,并编写了一个脚本以实例化从上方掉落的新的球体。

管理游戏对象

每当你有一个实例化对象的脚本时,你必须意识到对象的生命周期,并可能安排在不再需要时销毁它。例如,你可以销毁游戏对象,在它不再在场景中可见后,或者在特定的生命周期后,或者限制场景中球体的最大数量。

销毁掉落的对象

在我们的场景中,我们有一个有限大小的地面平面,当球体相互碰撞时,一些球体会掉落到平面之外。在那个时刻,我们可以从场景中移除掉落的球体。观察层次结构面板,注意新球体的实例化。注意,一些球体最终会从平面平台上弹跳出来,但仍然留在层次结构面板中。我们需要通过添加一个脚本来清理这些球体,如下所示:

  1. Project Assets/Prefabs 中选择 BouncyBall 预制件

  2. 导航到添加组件 | 新脚本,并将其命名为 DestroyBall

这里有一个 DestroyBall.cs 脚本,如果对象的 Y 位置低于地面平面很多(Y = 0),则会销毁该对象:

using UnityEngine; 
using System.Collections; 

public class DestroyBall : MonoBehaviour 
{ 
  void Update () 
  { 
    if (transform.position.y < -5f) 
    {
      Destroy (gameObject);  
    }  
  } 
} 

设置有限的生命周期

管理对象生命周期的一种策略是限制它们的持续时间。这对于像弹射物(子弹、箭矢、弹跳球)或其他玩家在实例化时最关心的对象,在游戏进行时不再关注的情况尤其有效。

为了实现,你可以在对象预制件本身上设置一个计时器,当时间耗尽时销毁它。

修改DestroyBall.cs脚本,在delay秒后销毁对象:

  public float timer = 15f; 

  void Start () 
  { 
    Destroy (gameObject, timer); 
  } 

当你玩游戏时,注意地面平面比以前少得多。每个 BouncyBall 将在 15 秒后或当它从平面上掉落时被销毁,以先发生者为准。

实现对象池

如果你的GameController间隔是 0.5 秒,销毁计时器是 15 秒,那么(进行计算)一次最多有 30 个球在游戏中。如果有些球掉落边缘,则更少。在这种情况下,我们不需要让我们的应用程序持续分配新的内存来创建新的 BouncyBall 实例,只为了在 15 秒后删除该对象。过多的实例化和销毁对象会导致内存碎片化。Unity 将定期进行清理,这是一个计算成本高昂的过程,称为垃圾回收GC),最好尽可能避免。

对象池是在游戏中创建一个可重复使用的对象列表,而不是持续实例化新的对象。您将激活/停用对象,而不是实例化/销毁。

为了实现这一点,我们将编写一个通用的对象池器并将其添加到场景中的GameController

因此,我们还向您介绍了 C#中的列表概念。正如其名所示,列表是有序对象集合,类似于数组。列表可以进行搜索、排序和其他操作(请参阅此处文档:msdn.microsoft.com/en-us/library/6sh2ey19.aspx)。我们将简单地使用它们来存储预先实例化的对象。让我们将脚本命名为ObjectPooler

  1. 在层次结构中选择GameController

  2. 导航到“添加组件”|“新建脚本”,并将其命名为ObjectPooler

打开进行编辑。让我们首先在顶部声明几个变量:

using System.Collections.Generic;
using UnityEngine;

public class ObjectPooler : MonoBehaviour 
{
    public GameObject prefab;
    public int pooledAmount = 20;

    private List<GameObject> pooledObjects;

}

公共的prefab将获取我们想要实例化的预制体对象,即BouncyBall。而pooledAmount表示初始实例化的对象数量。实际列表存储在pooledObjects中。

现在,当场景开始时,我们初始化列表如下:

    void Start () {
        pooledObjects = new List<GameObject>();
        for (int i = 0; i < pooledAmount; i++)
        {
            GameObject obj = (GameObject)Instantiate(prefab);
            obj.SetActive(false);
            pooledObjects.Add(obj);
        }
  }

我们在for循环中分配一个新的列表,并通过实例化我们的预制体来填充它,最初使其不活动,并将其添加到列表中。

现在我们想要一个新对象时,我们将调用GetPooledObject,它将在列表中寻找当前不活动的对象。如果所有对象都处于活动状态且没有可重复使用的对象,则返回null

    public GameObject GetPooledObject()
    {
        for (int i = 0; i < pooledObjects.Count; i++)
        {
            if (!pooledObjects[i].activeInHierarchy)
            {
                return pooledObjects[i];
            }
        }

        return null;
    }

就这样。

我们还可以增强脚本,使其可选地扩展列表,使其永远不会返回 null。在顶部添加选项:

    public bool willGrow = true;

for循环之后向GetPooledObject添加以下语句:

        ...
        if (willGrow)
        {
            GameObject obj = (GameObject)Instantiate(prefab);
            pooledObjects.Add(obj);
            return obj;
        }

        return null;
    }

保存脚本,将其附加到GameController,并将BouncyBall预制体拖放到组件的预制体槽中。

现在我们需要修改我们的BallsFromHeaven脚本,使其从ObjectPooler调用GetPooledObject而不是Instantiate。更新的BallsFromHeaven脚本如下:

using UnityEngine;

[RequireComponent(typeof(ObjectPooler))]
public class BallsFromHeaven : MonoBehaviour
{
    public float startHeight = 10f;
    public float interval = 0.5f;

    private float nextBallTime = 0f;
    private ObjectPooler pool;

    void Start()
    {
        pool = GetComponent<ObjectPooler>();
        if (pool == null) 
        {
            Debug.LogError("BallsFromHeaven requires ObjectPooler component");
        }
    }

    void Update()
    {
        if (Time.time > nextBallTime)
        {
            nextBallTime = Time.time + interval;
            Vector3 position = new Vector3(Random.Range(-4f, 4f), startHeight, Random.Range(-4f, 4f));
            GameObject ball = pool.GetPooledObject();
            ball.transform.position = position;
            ball.transform.rotation = Quaternion.identity;
            ball.GetComponent<RigidBody>().velocity = Vector3.zero;
            ball.SetActive(true);
        }
    }
}

注意,我们添加了一个指令[RequireComponent(typeof(ObjectPooler))],以确保对象具有ObjectPooler组件(我们还在Start函数中进行了双重检查)。

重要的是要注意,由于我们不是实例化新对象而是重用它们,你可能需要将任何对象属性重置为其起始值。在这种情况下,我们不仅重置了变换,还将 RigidBody 的速度重置为零。

最后的部分是我们修改DestroyBall,使其只是禁用(非激活)对象而不是真正销毁它。最初,处理掉落在地面平面之外的情况如下:

using UnityEngine;

public class DestroyBall : MonoBehaviour {

    void Update () {
        if (transform.position.y < -5f)
        {
            DisableMe();
        }
    }

    private void DisableMe()
    {
        gameObject.SetActive(false);
    }
}

我们没有调用Destroy,而是将Update改为调用一个新的函数DisableMe,该函数简单地禁用对象,将其返回到可用对象池中。

对于定时销毁,有几种不同的实现方式。之前,我们从Start()中调用了Destroy(gameObject, timer)。我们可以做类似的事情,使用OnEnable而不是Start,因为这是这个实例开始的时候。它调用Invoke()而不是直接销毁:

    void OnEnable()
    {
        Invoke("DisableMe", timer);
    }

    void OnDisable()
    {
        CancelInvoke();
    }

我们还提供了一个OnDisable来取消Invoke,因为如果球在计时器完成之前掉落边缘,对象可能会被禁用并可能重新启用,我们应该确保它不会同时被调用两次。

现在当你按下播放时,你可以在检查器中看到,新的 BouncyBalls 在开始时被实例化以初始化列表,然后随着游戏的进行,对象被禁用并在返回池中重新激活,如图所示(禁用的 BouncyBall(Clone)对象比激活的对象暗淡):

图片

头部瞄准游戏

实际上玩这些弹跳球会很有趣吗?让我们制作一个游戏,你可以用头部瞄准目标。在这个游戏中,球一个接一个地从上方落下,弹在你的额头(脸部)上,瞄准目标。

我们在这里要实现的功能是,当球从你头顶上方落下时,你将其弹回你的脸部,并瞄准目标。

为了实现这一点,创建一个作为相机对象子对象的立方体(就像我们在第六章中为瞄准十字准星所做的),世界空间 UI。这提供了一个由 VR 相机父化的碰撞器,因此我们的头部姿态会移动立方体的面。我决定立方体形状的碰撞器比球体或胶囊体更适合这个游戏,因为它提供了一个平坦的面,这将使弹跳方向更加可预测。球将从天空中落下。我们将使用一个压扁的圆柱体作为目标。我们将添加音频提示,以指示何时释放新球以及何时球击中目标。

创建一个新的场景,或者更简单地说,通过执行“另存为”操作从这里开始,并按照以下方式实现头部动作:

  1. 导航到“文件”|“另存场景为”,并将其命名为BallGame

  2. 使用齿轮图标删除附加到GameControllerBallsFromHeaven脚本组件。我们不需要它

  3. 在层次结构中展开MeMyselfEye,钻到Camera对象并选择它(对于 OpenVR 可能是[CameraRig]/Camera (head);对于 Daydream,可能是Player/Main Camera/

  4. 创建一个新的 3D 对象 | 立方体

  5. 在选择GameController后,导航到添加组件 | 音频 | 音频源

  6. 点击音频源音频剪辑字段最右侧的小圆形图标以打开选择音频剪辑对话框,并选择名为Jump的剪辑

  7. 在选择GameController后,导航到添加组件 | 新脚本,将其命名为BallGame,并打开它进行编辑

你可以选择禁用立方体的网格渲染器,但我觉得在场景窗口中观看它很酷。由于摄像机在立方体内部,玩家将看不到它(因为游戏视图中只渲染面向外部的表面)。

我们将播放“跳跃”声音剪辑(Unity 标准资产包中的“角色”包提供),以指示新球被放下。你可能尝试另一个,可能更有趣的效果。

这是BallGame.cs脚本。它看起来与BallsFromHeaven非常相似,只是有一些不同:

using UnityEngine;

public class BallGame : MonoBehaviour 
{
    public Transform dropPoint;
    public float startHeight = 10f;
    public float interval = 3f;

    private float nextBallTime = 0f;
    private ObjectPooler pool;
    private GameObject activeBall;
    private AudioSource soundEffect;

    void Start()
    {
        if (dropPoint == null)
        {
            dropPoint = Camera.main.transform;
        }
        soundEffect = GetComponent<AudioSource>();
        pool = GetComponent<ObjectPooler>();
    }

    void Update()
    {
        if (Time.time > nextBallTime)
        {
            nextBallTime = Time.time + interval;
            soundEffect.Play();
            Vector3 position = new Vector3(
                dropPoint.position.x, 
                startHeight, 
                dropPoint.position.z);

            activeBall = pool.GetPooledObject();
            activeBall.transform.position = position;
            activeBall.transform.rotation = Quaternion.identity;
            activeBall.GetComponent<RigidBody>().velocity = Vector3.zero;
            activeBall.SetActive(true);
        }
    }
} 

我们每 3 秒(interval)从当前头部位置上方的startHeight位置实例化一个新的球。

放下点默认直接位于玩家头部位置的正上方,如 VR 摄像机定义。这可能会让你的脖子感到不舒服,所以让我们稍微向前延伸,0.2 个单位:

  1. 作为MeMyselfEye(或作为你的头部或主摄像机对象的孩子),创建一个空的游戏对象,并将其命名为Drop Point

  2. 将其位置设置为(0, 0, 0.2

  3. 将此Drop Point拖动到GameController的球游戏放下点槽中

在位置跟踪的 VR 设备上,如果你的放下点相对于摄像机,它将跟随玩家移动。如果它相对于MeMyselfEye,它将相对于你的游戏空间,而玩家可以四处移动。

在 VR 中尝试一下。

当你听到球的声音时,抬头并调整你脸的角度以引导球的弹跳。酷!

现在,我们需要目标。执行以下步骤:

  1. 为目标创建一个平面圆柱体,导航到游戏对象 | 3D 对象 | 圆柱体,并将其命名为Target

  2. 将其缩放设置为(3, 0.1, 3)并将位置设置为(1, 0.2, 2.5),使其位于你前面的地面上。

  3. 从“项目资产/材质”文件夹(在第二章,内容、对象和比例中创建)拖动“蓝色”材质到它上面,或者创建一个新的材质。

  4. 注意,它的默认胶囊碰撞体是圆顶形的,这实际上并不合适。在胶囊碰撞体上,选择其齿轮图标 | 删除组件。

  5. 然后,导航到添加组件 | 物理 | 网格碰撞体。

  6. 在新的网格碰撞体中,启用凸多边形复选框和触发器复选框。

  7. 通过导航到添加组件 | 音频 | 音频源来添加音频源。

  8. 在选择Target的情况下,点击音频剪辑字段右侧的小圆圈图标以打开选择音频剪辑对话框,并选择名为Land的剪辑(位于标准资产中)。

  9. 取消选中“唤醒时播放”复选框。

  10. 然后创建一个新的脚本,导航到添加组件 | 新脚本,将其命名为TriggerSound,并在 MonoDevelop 中打开它。

由于我们启用了 Is Trigger,当某物撞击碰撞器时,如果目标对象上存在,OnTriggerEnter和其他事件处理程序将得到调用。以下TriggerSound.cs脚本将在你用球击中目标时播放声音:

using UnityEngine; 
using System.Collections; 

public class TriggerSound : MonoBehaviour { 
  public AudioSource hitSound; 

  void Start() { 
    hitSound = GetComponent<AudioSource> (); 
  } 

  void OnTriggerEnter(Collider other) { 
    hitSound.Play (); 
  } 
} 

球进入目标的碰撞器,物理引擎调用触发进入事件。脚本使用OnTriggerEnter()处理程序来播放音频剪辑。

要获取完整的碰撞器属性和触发事件列表,包括OnTriggerEnterOnTriggerExit,请参阅docs.unity3d.com/ScriptReference/Collider.html中的文档。

在 VR 中尝试。这是一个 VR 游戏!以下图像显示了带有第一人称碰撞器和球从立方体碰撞器弹跳向目标的场景:

图片额外挑战:记录得分。提供一个瞄准十字线。添加篮板。添加其他功能使游戏更具挑战性。例如,你可以改变射击间隔或增加初始球的速度。

到目前为止,我们通过附加到球体对象的物理材料来分配弹跳性。当球与另一个对象碰撞时,Unity 物理引擎考虑这种弹跳性来确定球的新速度和方向。在下一节中,我们将探讨如何将弹跳力从一个对象传递到另一个对象。

拍球游戏

接下来,我们将添加由手控制器控制的拍子来击打球。为了保持一定的通用性,我们的游戏拍子将是作为你相机装置中手控制器父级的一些简单对象。我们将目标移动到墙上而不是地板上,并将球发到你前方稍远的位置,以便可以触及

为了设置场景,你可以另存为新的名称,然后从这里开始工作。我会将我的命名为PaddleBallGame

  1. 选择文件 | 保存场景为,并将其命名为PaddleBallGame

  2. 如果存在,禁用之前作为相机子元素的头部立方体

首先,让我们创建一个拍子。我们将使用圆柱体构建一个非常简单的模型。你可以在网上找到更好的,形状和纹理的模型。

  1. 在层次结构根目录下,创建 | 创建空对象,并将其命名为Paddle,然后重置其变换

  2. 添加一个子圆柱体对象(创建 | 3D 对象 | 圆柱体),并将其命名为Handle

  3. 将“Handle”的缩放设置为(0.02, 0.1, 0.02)

  4. 在“Handle”旁边添加另一个圆柱体作为其兄弟元素,并将其命名为Pad

  5. 将垫的缩放设置为(0.2, 0.005, 0.2), 旋转(90, 0, 0), 和位置(0, 0.2, 0)

  6. 在你的项目材质文件夹中,创建一个新的材质(创建 | 材质)并命名为 Paddle Material

  7. 给材料 Albedo 一个木色,例如 (107, 79, 54, 255), 然后将材料拖到把手和垫子对象上

现在修改碰撞体:

  1. 选择把手,并删除其胶囊碰撞体

  2. 选择垫子,并删除其胶囊碰撞体

  3. 选择垫子,添加一个网格碰撞体(添加组件 | 物理 | 网格碰撞体)

  4. 选择凸多边形复选框

将桨保存为预制件:

  1. 将桨拖到你的项目预制件文件夹中

  2. 从你的层级中删除桨

我们希望将桨作为你的手的父对象。这是平台特定的。例如,如果你使用 OpenVR,那可能就是 MeMyselfEye / [CameraRig] / Controller (right)。在 Daydream 上,那可能就是 MeMyselfEye / Player / GvrControllerPointer

  1. 在层级中,选择 MeMyselfEye 内的手控制器(例如 Controller (right) 或 GvrControllerPointer)

  2. 创建空的子游戏对象并命名为 Hand(如果需要,重置其变换)

  3. 在手下面创建另一个空的子对象,并命名为 Attach Point(如果需要,重置其变换)

  4. 将桨预制件从项目拖到层级中,作为 Attach Point 的子对象

现在,我们可以调整桨的相对位置和旋转,使其握感自然。以下值对我来说似乎有效:

  • 在 OpenVR 中,使用附加点旋转 (20, 90, 90)

  • 在 Daydream 中,使用位置 (0, 0, 0, 05) 和旋转 (0, 90, 90)

在 Daydream 上,GvrControllerPointer 包含一个 GvrArmModel 组件,该组件可以配置为使用简单的 3 自由度控制器模拟手臂、肘部和手腕的运动。自己设置可能会很复杂。幸运的是,Daydream Elements 包中的 ArmModelDemo 场景提供了一系列示例(在 DaydreamElements/Elements/ArmModels/D*emo/* 文件夹中),包括一些预配置的手臂模型预制件。让我们添加一个。如果你在 Daydream 上:

  1. 在项目资产中找到 Elements/ArmModels/Prefabs 文件夹

  2. SwingArm 预制件拖到 MeMyselfEye / Player 中,作为 GvrControllerPointer 的同级

  3. GvrControllerPointer 移动为 SwingArm 的子对象

这将为使用桨提供更多的手臂延伸。你可以根据需要进一步调整设置,包括尝试将 SwingArm 变换的位置向前移动更多 (0, 0, 0.3).

最后,你可能还想将球体下落的位置稍微向前延伸,使其更容易触及手。在项目的早期版本中,我们定义了一个下落点;根据需要修改其位置(例如,z = 0.6)。

这里展示了使用 HTC Vive 时使用的桨:

图片

反射器与桨

如此实现,我们的桨更像是一个防御盾牌而不是桨。球会根据垫子的表面法线方向从桨的垫子弹跳出去。但如果你击打球,则不会传递任何物理效果。我们可以通过向垫子添加一个刚体来改变这一点,如下所示:

  1. 选择你的桨的垫子

  2. 添加组件 | 物理 | 刚体

  3. 取消选中使用重力复选框

  4. 选中 Is Kinematic 复选框

  5. 点击 Inspector 顶部的 Apply 按钮以保存对预制体的更改

通过使其成为运动学,我们的垫子将对与之碰撞的对象应用物理效果,但不会对碰撞做出反应。这是好的,否则当球击中时,桨会破碎。

在这个项目中,一个重要的教训是使用附加点来定义特定行为的相对位置。我们使用了一个 Drop Point 来标记球从哪里掉落的 X、Z 位置。我们使用了一个 Attach Point 来标记你手中的桨的相对位置和旋转。我们可以在桨本身添加一个 Grip Point 来指定其相对原点。等等。

射击球游戏

对于这个项目的下一个迭代,我们将向玩家射击球,你必须击中墙上的目标。这个版本没有太多创新,但它展示了你可以如何将现有的机制翻转过来(字面和比喻意义上)。

首先,让我们制作一面墙并将目标放在上面:

  1. 在 Hierarchy 根目录下创建一个名为 TargetWall 的空游戏对象,并

  2. 将其放置在 (0, 0, 5)

  3. 创建一个子对象立方体并将其命名为 Wall

  4. 将墙的缩放设置为 (10, 5, 0.1) 和位置 (0, 2.5, 0)

  5. 创建一个新的材质名为 Wall Material

  6. 将其渲染模式设置为透明,并将其 Albedo 颜色设置为 (85, 60, 20, 75) 以使其成为半透明的玻璃色

  7. 将目标移动到 TargetWall 的子对象

  8. 修改目标变换的缩放为 (1.5, 0.1, 1.5)、旋转 (90, 0, 0) 和位置 (0, 2.5, -0.25),使其更小,正好在墙的前面

接下来,我们不再是从空中抛球并依赖重力来服务球,而是从墙上的一个源头向你射击球:

  1. 创建一个名为 Shooter 的球体游戏对象,作为 TargetWall 的子对象

  2. 设置其缩放为 (0.5, 0.5, 0.5) 和位置 (4, 2.5, -0.25)

  3. 禁用或移除其球体碰撞器组件

  4. 创建一个新的材质名为 Shooter Material,其 Albedo 颜色为 (45, 22, 12, 255)

我们将在射击器上添加一个枪管:

  1. 创建另一个名为 Barrel 的球体对象,作为 Shooter 的子对象

  2. 设置其缩放 (0.1, 0.1, 0.1)、旋转 (90, 0, 0) 和位置 (0, 0, -0.25)

复制 Shooter 并将第二个的 Position 设置为 (-4, 2.5, -0.25), 这样目标的两边就有一个。以下是 TargetWall 的场景视图截图,其中包含其性感的射击器:

图片

游戏控制器脚本与我们的 BallGame 脚本类似,但足够不同,我们应该创建一个新的:

  1. 在层次结构中,选择 GameController 并禁用或删除 BallGame 组件

  2. 创建一个名为 ShooterBallGame 的新 C# 脚本并打开它进行编辑

ShooterBallGame 脚本编写如下。我们给它两个发射器,脚本在它们之间交替以向 shootAt 位置射击球体。每次发射球体时都会播放音效。首先,让我们定义我们将需要的公共和私有变量:

using UnityEngine;

[RequireComponent(typeof(ObjectPooler))]
public class ShooterBallGame : MonoBehaviour
{
    public Transform shootAt;
    public Transform shooter0;
    public Transform shooter1;
    public float speed = 5.0f;
    public float interval = 3f;

    private float nextBallTime = 0f;
    private ObjectPooler pool;
    private GameObject activeBall;
    private int shooterId = 0;
    private Transform shooter;

    private AudioSource soundEffect;
}

Start 函数初始化我们在运行时获取的变量:

   void Start()
    {
        if (shootAt == null)
        {
            shootAt = Camera.main.transform;
        }
        soundEffect = GetComponent<AudioSource>();
        pool = GetComponent<ObjectPooler>();
        if (pool == null)
        {
            Debug.LogError("BallGame requires ObjectPooler component");
        }
    }

并且 Update 函数会在指定的时间间隔内射击球体,在两个发射器位置之间交替:

    void Update()
    {
        if (Time.time > nextBallTime)
        {
            if (shooterId == 0)
            {
                shooterId = 1;
                shooter = shooter1;
            }
            else
            {
                shooterId = 0;
                shooter = shooter0;
            }

            nextBallTime = Time.time + interval;
            ShootBall();
        } 
    }

最后,这是我们提取到其自身函数中的 ShootBall() 代码:

    private void ShootBall()
    {
        soundEffect.Play();
        activeBall = pool.GetPooledObject();
        activeBall.transform.position = shooter.position;
        activeBall.transform.rotation = Quaternion.identity;
        shooter.transform.LookAt(shootAt);
        activeBall.GetComponent<Rigidbody>().velocity = shooter.forward * speed;
        activeBall.GetComponent<Rigidbody>().angularVelocity = Vector3.zero;
        activeBall.SetActive(true);
    }

ShootBall 从对象池中获取一个新的球体,并根据发射器位置初始化其位置。然后,它将发射器旋转到指向 shootAt 位置(使用 transform.LookAt),并使用其前向向量定义球体的 RigidBody 速度向量。

在 Unity 中,我们需要填充公共变量槽位:

  1. Shooter 对象(TargetWall 的子对象)拖到 Shooter 0 槽位上

  2. 将另一个 Shooter 对象拖到 Shooter 1 槽位上

目前先保留 Shoot At 槽位为空,这样它将默认为玩家的实时头部位置。

按下播放。还不错。球太大太重了。让我们创建具有不同属性的新的球体预制件:

  1. BouncyBall 预制件从 Project 文件夹拖到层次结构中

  2. 将其重命名为 ShooterBall

  3. 将其缩放设置为 (0.25, 0.25, 0.25)

  4. 取消选中使用重力复选框(或者,你可以玩一下它的 RigidBody 质量属性)

  5. ShooterBall 从层次结构拖到你的 Prefabs 文件夹中,为它创建一个新的预制件

  6. 从层次结构中删除 ShooterBall

  7. 在层次结构中选择 GameController,并将 ShooterBall 拖到其 Object Pooler 预制件槽位上

现在,对象池将实例化一组新的预制对象。

按下播放。哦,是的! 现在游戏变得更加具有挑战性。同时,尝试修改间隔和速度设置。

球体总是朝向你的头部射击,尤其是在 Daydream 上,你只有有限的手部控制,这可能会有些尴尬。你可以调整场景,例如,将 ShootAt 空游戏对象定位为 MeMyselfEye 的子对象,位置为 (0, 0.9, 0.6), 并将其设置到 GameControllerShootAt 槽位中。

一些明显的游戏玩法改进想法应该会浮现在脑海中。你可以制作一个移动的目标,可能是可预测的振荡运动,或者完全随机。你可以在球体速度方向和速度或射击间隔之间引入一些随机变化。你可以使用 OnTriggerEnter 在目标上记分。你可以取消资格在地板上首先弹跳的银行球(使用地面平面的 OnTriggerEnter)。

增强场景效果

实现了基本机制后,我们现在可以给它添加活力!我最喜欢的 VR 游戏之一是流行的音频盾牌(audio-shield.com/)。我们几乎完成了自己的构建,我们只需要添加火焰球、引人入胜的环境场景,以及将火焰球射击与音乐同步!

游戏设计中的术语juice it由 Jonasson 和 Purho 在 2012 年的演讲中普及,演讲主题为Juice it or lose it - Martin Jonasson & Petri Purho 的演讲(www.youtube.com/watch?v=Fy0aCDmgnxg)。 一个充满活力的游戏感觉充满生机,对你的每一个动作都做出反应,有大量的连锁动作和反应,而用户输入却很少。

火球

在上一节中,我们禁用了射击球上的重力使用。我们这样做是为了预期将球从弹跳球变为火焰球。现在让我们实现这个魔法。我们将使用粒子系统来渲染它,而不是网格几何体。

将粒子效果添加到 Unity 项目中有许多方法。如果你还记得,在第四章“基于注视的控制”中,我们从Unity 标准资产包中添加了水龙带、火花发射器和爆炸效果。在这里,我们将构建自己的,但使用包中提供的材料之一,ParticleFireCloud。在 Unity 资产商店中,你还可以找到许多粒子效果和系统增强的提供。

首先,创建一个新的从 ShooterBall 派生的预制件,命名为FireBall,如下所示:

  1. 从“项目”文件夹中将ShooterBall预制件的副本拖动到层次结构中

  2. 将其重命名为 FireBall

  3. 将 FireBall 拖动到“项目”预制件文件夹中创建一个新的预制件

  4. 从层次结构中选择GameController

  5. 从“项目”预制件文件夹中将FireBall预制件拖动到对象池器预制件槽中

好的,现在我们可以添加粒子系统:

  1. 从层次结构中选择 FireBall

  2. 禁用其网格渲染器,因为我们将以粒子形式渲染它

  3. 右键单击 FireBall 并选择创建 | 效果 | 粒子系统

  4. 将其重命名为 Fireball Particle System

在处理粒子时有很多细节,很多选项和配置参数。随着我们逐步实现火焰球的快速实现,注意我们逐个更改时每个更改的效果。请注意,你可以在场景窗口中预览粒子效果。请随意进行实验。

  1. 首先,在粒子系统检查器的底部,找到渲染器面板。在其材质槽中,点击甜甜圈图标并选择 ParticleFireCloud 材质(位于Standard Assets/Particle Systems/Materials。如果不存在,你可能需要使用“资产 | 导入包 | 粒子系统”导入它)。

  2. 在粒子系统检查器的顶部附近,找到形状面板。选择形状:球体,并将其半径设置为0.1

  3. 在发射面板中,将时间速率设置为15

  4. 在检查器的顶部,设置持续时间:2.00

  5. 起始生命周期:1

  6. 起始速度:0

  7. 起始大小:0.5

  8. 对于起始旋转,点击右侧的选择器图标,并选择“在两个曲线之间随机”。然后点击槽位,滚动到检查器底部的曲线编辑器。如果你不熟悉,编辑器可能需要一些时间来适应。选择从180(图表顶部)到-180(图表底部)的全范围值,如图所示:

  1. 启用生命周期内颜色,并点击槽位以打开其渐变编辑器。我们想要调整 Alpha 曲线,使其在位置0%时 Alpha 为0,然后在10%时变为 Alpha 255,然后随着时间的推移逐渐淡出到100%的 Alpha 0。编辑器如图所示:

  1. 设置起始颜色,作为渐变(右侧选择器),然后选择一系列颜色,如黄色到红色,如图所示:

  1. 接下来,设置生命周期内的速度,使用“在两个曲线之间随机”。对于每个 X、Y、Z,使用曲线编辑器设置最大和最小值分别为0.05-0.05。(你可以通过点击轴标签并输入数字来修改图表的垂直轴;例如,你可以通过右键点击 Z 槽位,选择复制,然后右键点击 Y 槽位并选择粘贴来复制曲线。)

在这一点上,我们应该调整火球,使其大小大约与我们的原始 BouncyBall 相同。检查方法如下:

  1. 重新启用火球的网格渲染器。通过更改渲染器的最大粒子大小到0.1,或使用变换缩放来调整粒子系统。

  2. 通过在检查器顶部选择应用来保存你的工作,以更新你的预制件。

现在当你按下播放时,射手将发射火球。哇!

如果你想在火球上添加一些闪光效果,我们可以通过轨迹面板来实现:

  1. 启用轨迹面板

  2. 可能会弹出警告提示你向渲染器添加轨迹材质。

  3. 在渲染器面板中,选择轨迹材质槽位上的甜甜圈图标,并选择我们用于主火球的 ParticleFireCloud。

说到轨迹,如果你也想在火球上实现轨迹效果,也有几种方法可以做到。一个快速的解决方案是复制我们的火球粒子系统,并将其修改为使用圆锥形状而不是球体,如下所示:

  1. 在层次结构中选择火球粒子系统。

  2. 右键点击以复制,将复制品移动为火球粒子系统的子项,并命名为轨迹粒子系统。

  3. 将其形状更改为圆锥。

  4. 改变其生命周期内的速度。Z 曲线需要更高的值范围,例如0.750.25

  5. X 和 Y 速度曲线应该更小,以产生一些变化,例如0.2-0.2

  6. 设置生命周期内大小范围到1.00.5

  7. 在其变换中,将位置设置为(0, 0, 0.5)以给它一个额外的尾巴。

这里是游戏窗口中击打即将到来的火球的截图!

图片

特别感谢 Tyler Wissler 的教学视频 How To: Basic Fireballs in Unity(2014 年 6 月),这对开发这个主题非常有帮助 (www.youtube.com/watch?v=OWShSR6Tr50)。

骷髅环境

为了让我们的游戏更加精彩,我们应该找到一个令人兴奋的环境和场景。在资产商店中搜索,我发现了一个免费的资产 Skull Platform (assetstore.unity.com/packages/3d/props/skull-platform-105664)。你也可以使用它,或者寻找其他的东西。

假设你已经找到了并安装了 Skull Platform 资产,我们将将其添加到我们的场景中。首先,让我们将目标渲染成骷髅:

  1. 将 Platform_Skull_o1 作为目标(在 TargetWall 下)的子对象。

  2. 设置其变换旋转 (0, 0, 180) 和缩放 (0.3, 0.3, 0.3)。

  3. 选择目标并禁用其网格渲染器。

  4. 此外,创建一个新的聚光灯(创建 | 光 | 聚光灯),使其照在骷髅上。作为目标的子对象,我使用了以下设置:位置 (-1, -30, -0.6), 旋转 (-60, 60, 0), 范围:10,聚光灯角度:30,颜色:#FFE5D4FF,强度:3

接下来,让我们将大平台作为墙壁背后的背景添加。最快的方法是合并他们提供的 Demoscene:

  1. 在层次结构根目录下创建一个空的游戏对象,命名为 SkullPlatform,重置其变换。

  2. 将 Skull Platform 的演示场景副本 Platform(Assets/Skull Platform/Demo/ 文件夹)拖动到层次结构中。

  3. 选择演示的场景、照明和粒子对象,并将它们拖动到 SkullPlatform 的子对象。

  4. 现在我们已经拥有了想要的资产,在层次结构中右键单击平台场景,选择移除场景。当提示时,选择不保存。

  5. 将 SkullPlatform 位置设置为 (0, -1.5, 0),使其刚好在地面平面下方。

  6. 选择 GroundPlane 并禁用其网格渲染器。

现在,我们将设置场景环境照明:

  1. 从场景层次结构中删除方向光。

  2. 打开照明窗口。如果它还不是你的编辑器中的一个标签,请使用窗口 | 照明 | 设置并将其停靠在检查器旁边。

  3. 将其天空盒材质设置为 Sky(在 Skull Platform 包中提供)。

  4. 在环境照明部分,将源:颜色设置为 #141415

  5. 检查雾选项(在其他设置中),颜色为 #8194A1FF,模式:指数,密度为 0.03

这里是带有骷髅平台环境和照明的场景截图。太棒了

图片

音频同步

我们几乎完成了构建我们自己的音频盾版本,我们只需要添加同步火球射击与音乐!

Unity 提供了一个 API 用于采样音频源数据,包括AudioSource.GetSpectrumDataGetOutputData。从这些数据中提取音乐的实际节拍并不简单,需要大量的数学知识和对音乐编码工作方式的一些理解。

幸运的是,我们找到了一个开源脚本,名为 Unity-Beat-Detection (github.com/allanpichardo/Unity-Beat-Detection),它可以为我们完成这项工作。它方便地提供了onBeat的 Unity 事件,我们将使用它。(它还提供了onSpectrum事件,每帧提供音乐频率带,你也可以使用它,例如,根据频率带改变火球或其他东西的颜色。)

  1. 从 GitHub 下载AudioProcessor.cs脚本(我们已提供与本书文件一起的副本,以方便您使用)

  2. 将文件拖入你的Scripts文件夹(或使用 Assets | Import New Asset)

对于你的音乐,找到任何有良好节拍的 MP3 或 WAV 文件,并将其导入到你的项目中。我们在 SoundCloud NoCopyrightSounds 轨道 (soundcloud.com/nocopyrightsounds/tracks) 中找到了一个名为Third Prototype - Dancefloor (ncs.io/DancefloorNS)。

  1. 在项目窗口中,创建一个名为 Audio 的文件夹

  2. 将你的音乐文件拖到音频文件夹中(或使用 Assets | Import New Asset)

要实现这个功能,我们将创建一个 MusicController,然后修改ShooterBallGame脚本以使用其节拍发射火球。在 Unity 中,执行以下操作:

  1. 在层次结构中,创建一个空的游戏对象并命名为 MusicController

  2. 将 AudioProcessor 脚本作为组件添加

  3. 注意它还会自动添加一个 Audio Source 组件

  4. 将你导入的音乐文件拖到 AudioClip 槽中

  5. 将 MusicController 本身拖到 Audio Source 槽中

注意音频处理中的 G Threshold 参数。你可以使用它来调整节拍识别算法的灵敏度。

现在,按照以下方式更新GameController上的ShooterBallGame脚本:

    void Start()
    {
        if (shootAt == null)
            shootAt = Camera.main.transform;
        pool = GetComponent<ObjectPooler>();

        AudioProcessor processor = FindObjectOfType<AudioProcessor>();
        processor.onBeat.AddListener(onBeatDetected);
    }

    void onBeatDetected()
    {
        if (Random.value > 0.5f)
        {
            shooterId = 1;
            shooter = shooter1;
        } else
        {
            shooterId = 0;
            shooter = shooter0;
        }
        ShootBall();
    }

它与上一个版本非常相似,但不是从Update中调用ShootBall,而是根据时间间隔从onBeatDetected中调用它。在Start中,我们将onBeatDetected添加为onBeat事件监听器。

此外,我们决定随机选择使用哪种射击方式,而不是简单地来回交替。

按下播放并开始游戏!哇哦,我们有了自己的音频盾牌版本!以下是活跃游戏截图:

图片

摘要

在本章中,我们构建了一个使用 Unity 物理引擎和其他一些功能的游戏。首先,我们用通俗易懂的语言解释了 Rigidbody、Collider 和 Physic Materials 之间的关系,并探讨了物理引擎如何使用这些来决定场景中物体的速度和碰撞。

然后,我们考虑了游戏对象的整个生命周期,并实现了一个对象池器,它有助于避免内存碎片化和垃圾回收,这些问题可能导致性能问题和 VR 体验不适。

利用我们所学的知识,我们实现了几个球类游戏的变体,首先是用头部瞄准目标,然后使用手柄。我们修改了游戏,使其不是从上方使用重力来发球,而是从前面发射并应用一个速度向量。最后,我们增强了我们的游戏,将弹跳球变成了火球,添加了一个酷炫的关卡环境,并将火球与音乐节拍同步。最终,我们为制作我们自己的音频盾 VR 游戏版本迈出了良好的开端。

在下一章中,我们将看到另一个更实用的虚拟交互空间示例。我们将构建一个互动艺术画廊空间,你可以在其中移动并查询艺术品的详细信息。

第九章:探索交互式空间

在本章中,我们将更深入地探讨关卡设计、建模、渲染、传送和动画;实现一个可以在 VR 中体验的交互式空间。场景是一个照片画廊,你设计一个简单的平面图,并使用 Blender 将其垂直拉伸成墙壁。使用你自己的照片。你可以通过传送或动画乘坐来在空间中移动。

在本章中,我们将讨论以下主题:

  • 使用 Blender 和 Unity 构建简单的艺术画廊

  • 与对象和元数据交互

  • 数据结构、列表和可脚本化对象

  • 使用传送

  • 创建动画导览

注意,本章中的项目是独立的,并不直接依赖于本书其他章节的项目。如果你决定跳过其中任何部分或未保存你的工作,那都是可以的。

使用 Blender 进行关卡设计

对于这个项目,我们将设计一个艺术画廊布局。我们只需要一个简单的布局,一个大约 24 英尺乘以 36 英尺的小型艺术画廊展览室。这个房间非常简单,实际上,它很容易在 Unity 中使用 3D 立方体原语构建,但我们将利用这个机会更多地使用 Blender,因为我们已经在第二章,内容、对象和比例中介绍了它,保持其最小化和指导性。如果你愿意,你可以跳过这一部分,使用 Unity 立方体构建地板和墙壁。或者,使用本章提供的Gallery.blend文件。

定义墙壁

首先,在一张纸上绘制一个简单的平面图或使用绘图应用程序。我的只是一个开阔的空间,有两个入口和内部墙壁来展示艺术品(Gallery-floorplan.jpg),如下面的图像所示:

图片

现在,打开 Blender。我们将使用从简单对象(平面)开始,然后拉伸它以制作每个墙壁段落的常用技术。为了完成这个任务,请执行以下步骤:

  1. 从一个空场景开始,按 A 键选择所有对象,然后按 X 键删除。

  2. 通过按 N 键打开属性面板来添加平面图图像作为参考。在背景图像面板中,选择添加图像,点击打开并选择你的图像(Gallery-floorplan.jpg)。

    根据你的平面图参考图像的大小和比例,你需要选择一个比例因子,以确保它在 Blender 世界坐标空间中是正确的。对我来说,6.25的比例是合适的。实际上,最重要的是图上特征的相对比例,因为我们总可以在 Unity 的导入设置中调整比例,甚至在场景视图中直接调整。

  3. 在背景图像面板中,将大小设置为6.25。这个面板中大小字段被高亮显示,如下面的截图所示:截图

  4. 通过按数字键盘上的 7(或导航到视图|顶部)进入从上到下的正交视图,并通过按 5(或导航到视图|透视/正交)进入正交视图。请注意,只有当它在顶部正交视图中时,背景图像才会被绘制。

  5. 现在,我们在房间的角落处制作一个小正方形,将其拉伸成墙壁。按Shift + A添加一个面板,并选择平面。然后,按Tab进入编辑模式。按Z在实体视图和线框视图之间切换。按G将其拖入角落,点击Enter确认。按S将其缩放到适合墙角的宽度,如图所示(你可能还记得,你可以使用鼠标滚轮来缩放,Shift和点击中间鼠标按钮来平移):

图片

  1. 将角落拉伸以形成外墙。进入边缘选择模式(通过以下截图中的图标),按A取消选择所有内容,然后右键单击要拉伸的边缘。按E开始拉伸,按 X 或 Y 将其约束到该轴,然后按Enter完成拉伸到所需位置:

图片

  1. 对每面外墙重复之前的步骤。在角落处创建一个小正方形,以便可以沿垂直方向拉伸。为门留出空隙。(你可能还记得,如果你需要修改现有的边缘,右键单击选择它,使用Shift和右键单击来选择多个,然后使用G移动。你也可以复制选定的项目。)此外,你还可以使用Shift + D在对象模式下进行复制。

图片

  1. 要从中间拉伸出一个面,我们需要添加一个边缘环。将鼠标移到面上,按Ctrl + R并左键单击创建切割。滑动鼠标定位它,然后再次左键单击确认。重复这些步骤以墙宽为宽度(在外墙上做一个正方形切割)。选择边缘段并按E将其拉伸到房间内:

图片

  1. 一旦完成平面图,我们就可以沿z轴拉伸以创建墙壁。按5从正交视图切换到透视视图。使用中间鼠标点击并倾斜。按A选择所有内容。使用E拉伸。开始使用鼠标拉伸,按Z约束,然后左键单击确认。

  2. 将模型保存为名为gallery.blend的文件:

图片

添加天花板

现在,添加一个带有两个天窗的天花板。天花板将只是一个由单个立方体构建的平板。让我们看看添加天花板的步骤:

  1. 使用Tab返回对象模式

  2. 使用Shift + A添加一个立方体,并选择立方体

  3. 使用G和鼠标(Alt + G重置所有变换)将其定位在中心

  4. 沿xy轴缩放,使其大小与房间相同,使用S + XS + Y

  5. 使用1切换到前视图,使用S + Z将其缩放以使其变平,并使用G + Z将其移动到墙的顶部)

天窗将通过使用另一个立方体作为修饰符从天花板上切割出孔,如图所示截图

截图

  1. 使用Shift + A添加一个立方体,将其缩放到所需的大小,并将其移动到你想放置天窗的位置。

  2. 将立方体的z轴定位,使其穿过天花板板。

  3. 通过按Shift + D来复制立方体,并将其移动到另一个天窗的位置,如图所示截图

  4. 右键单击以选择天花板板。

  5. 在最右侧的属性编辑器面板中,选择扳手图标。

  6. 然后,导航到“添加修饰符”|“布尔”并对于操作选项,选择“差集”。对于对象选项,选择第一个立方体(Cube.001):

截图

  1. 点击应用以使操作永久。然后,删除立方体(选择它并按 X)。

  2. 重复此过程,为第二个立方体添加另一个布尔修饰符。

如果遇到困难,我已经包括了这个书的完成模型的副本。这个模型很简单。你可以仅使用 Unity 立方体来构建它。当然,还可以做更多的事情来使这个模型更加逼真的建筑模型,但我们将继续前进:

截图

在 Unity 中组装场景

现在,我们可以在 Unity 中使用画廊房间模型,并添加带有天窗的地板和天花板。我们将给墙壁应用纹理并添加照明。

我们可以按照以下方式启动一个新的 Unity 场景:

  1. 通过导航到“文件”|“新建场景”来创建一个新的场景。然后,选择“另存为场景”并将其命名为Gallery

  2. MeMyselfEye预制体的副本拖动到层次结构中。

  3. 从层次结构中删除默认的主摄像机。

画廊房间级别

首先,我们将通过以下步骤构建艺术画廊的房间结构:

  1. 通过导航到“游戏对象”|“3D 对象”|“平面”创建一个地板平面。重置其变换选项并将其重命名为Floor

  2. 为地板创建材质并将其染成棕色。创建 | 材质,重命名它,设置其 Albedo(70, 25, 5),并将材质拖到Floor上。

  3. 我们的房间大小为 24 英尺乘以 36 英尺,换算成米大约是 7.3 乘以 11。Unity 平面是 10 单位平方。因此,将其缩放为(0.73, 2, 1.1)。

  4. 导入画廊模型(例如,Gallery.blend)。从项目资产中拖动一个副本到场景中。重置其变换选项。

  5. 根据需要手动旋转和/或缩放它以适应地板,例如(我的适合,但它的旋转 Y 值需要设置为90)。如果首先将场景视图更改为顶等距视图,可能会有所帮助。

  6. 在墙上添加一个碰撞器是个好主意,这样角色就不会直接穿过了。为了实现这一点,导航到“添加组件”|“物理”|“网格碰撞器”。

注意,当我们从 Blender 导入时,正如我们定义的那样,画廊中的墙壁和天花板是分开的对象。创建了一个材质(可能命名为“未命名”),它有一个中性的灰色 Albedo(204204204)。我喜欢这种颜色用于墙壁,但我为天花板制作了一个新的材质,全部为白色(255255255)。

对于一个好的默认 Skybox,我们推荐Wispy Skybox,这是资源商店上的一个免费包(assetstore.unity.com/packages/2d/textures-materials/sky/wispy-skybox-21737)。如果您想使用它,请现在就下载并导入到您的项目中。

接下来,添加一些天空和阳光,如下所示:

  1. 如果在你的 Unity 编辑器中看不到“照明”选项卡,请导航到窗口 | 照明 | 设置

  2. 在照明面板中,选择场景选项卡

  3. 对于阳光,在照明场景面板中,在太阳输入处,从层次结构中选择(默认)方向光并将其拖放到太阳光源槽中

  4. 对于天空,如果您导入了 Wispy Skybox(见前文),则在照明场景选项卡中,选择 Skybox 材质槽上的甜甜圈图标并选择名为WispySkyboxMat的材质

由于我们选择了“方向光”作为光源,你可以通过选择方向光并修改其旋转来调整角度,在场景窗口中的 gizmo 或直接在检查器中,可能是一个与您选择的 Skybox 一致的旋转(例如旋转601750)。

你可以考虑为地板和其他表面使用纹理材质。例如,在资源商店中搜索“地板材质”。这里有免费的包和付费的包。

艺术品装置

现在,我们可以规划艺术展览。我们将创建一个带有画框、照明、定位、艺术家信息和传送观看位置的重复使用艺术品装置预制件。然后,我们将艺术挂在画廊的墙上。稍后,我们将应用实际图像。艺术品装置将包括一个画框(立方体)、一个照片平面(四边形)和一盏聚光灯,所有这些都相对于艺术品在墙上的放置。我们将在场景内创建第一个,将其保存为Prefab,然后在画廊的墙上放置副本。我建议在场景视图中这样做。让我们开始吧:

  1. 通过导航到 GameObject | 创建空对象来创建一个容器对象。将其命名为ArtworkRig

  2. 创建框架。选择ArtworkRig后,右键单击并导航到 GameObject | 3D Object | Cube。将其命名为ArtFrame。在检查器中,将其 Scale Z 设置为0.05。假设一个3:4的宽高比。因此,将其 Scale Y 值设置为0.75

  3. 将支架定位在墙上(原始楼层平面右上角面向入口的那面墙)。隐藏Gallery对象的子对象天花板(取消勾选其启用复选框选项)可能会有所帮助。然后,使用场景面板右上角的场景视图工具将场景视图更改为顶视图和等轴视图。点击绿色 Y 图标进行顶视图,点击中间的方块图标进行等轴视图。

  4. 选择ArtworkRig,确保翻译工具是激活的(位于左上角图标工具栏中的第二个图标),并使用xz轴箭头定位它。务必选择并移动ArtworkRig。将框架位置保持在(0,0,0)。将高度设置为眼睛水平(Y=1.4)。对我有效的是变换位置值(2, 1.4, -1.82),并且没有旋转(0,0,0),如图所示。

  5. 将框架设置为黑色。导航到资产 | 创建 | 材质,将其命名为FrameMaterial,并将其 Albedo 颜色设置为黑色。然后在层次结构中选择框架选项,并将FrameMaterial材质拖放到ArtFrame上。

  6. 创建图像占位符。在层次结构中选择ArtFrame,右键单击并导航到 3D 对象 | 四边形。将其命名为Image。将其定位在框架前方,使其可见;将位置设置为(0, 0, -0.8),并调整缩放使其略小于框架,将缩放设置为(0.9, 0.9, 1)。

  7. 为了更好地欣赏当前的缩放和眼睛水平,尝试在场景中插入 Ethan 的一个副本:

接下来,我们将向支架添加一个聚光灯,如下所示:

  1. 首先,通过勾选画廊子对象的启用复选框选项来将天花板放回。

  2. 在层次结构中选择ArtworkRig,右键单击,导航到光 | 聚光灯,并将其定位在离墙 1.5 米远(Z=-1.5)且靠近天花板的位置。确切的高度并不重要,因为我们实际上没有灯具。我们只有一个 Vector3 位置作为光源。我将位置设置为(0, 1.5, -1)。

  3. 现在,调整Spotlight值,使其适当地照亮艺术品。我将旋转 X 设置为2,并根据您的喜好调整光参数,例如将范围设置为5,聚光灯角度设置为45,强度设置为3。结果如图所示:

  4. 注意聚光灯正在穿过墙壁,照亮另一侧的地面。我们不想这样。选择聚光灯,通过阴影类型:软阴影启用阴影。

  5. 要将支架作为预制件保存,请在层次结构中选择ArtworkRig,并将其拖动到您的项目资产预制件文件夹中。

了解您的照明设置可能很重要。例如,如果您看到物体中的孔或阴影,尝试将方向光的法线偏差滑到0并将偏差设置为低值,如0.1。有关阴影和偏差属性的更多信息,请参阅docs.unity3d.com/Manual/ShadowOverview.html

展览计划

下一步是在我们想要显示图像的每面墙上复制ArtworkRig。根据需要定位和旋转。如果您遵循以下图中所示的计划,您的展览将显示十个图像,用星号表示:

以下是在每面墙上复制ArtworkRig的步骤:

  1. 如前所述,隐藏天花板并更改场景视图面板到顶视图和等距视图可能更容易。

  2. 在场景视图面板的左上角,更改Transform Gizmo切换,以便工具句柄放置在轴点而不是中心。

  3. 创建一个新的空游戏对象,重置其变换,并将其命名为Artworks

  4. 将现有的ArtworkRig移动,使其成为Artworks的子对象。

对于每个位置,按照以下方式在画廊中放置一件艺术品:

  1. 在层次结构中选择现有的ArtworkRig

  2. 通过右键单击复制或按Ctrl + D来复制Artworkrig

  3. 通过将旋转 Y 设置为090180-90来旋转装置,使其朝向正确的方向。

  4. 将装置放置在墙上

以下表格提供了适用于我的画廊的设置(并假设您的Artworks变换已重置到原点):

位置 X 位置 Z 旋转 Y
0 2 -1.8 0
1 -1.25 -5.28 -180
2 -3.45 -3.5 -90
3 -3.45 -0.7 -90
4 -2 1.6 0
5 2 -1.7 180
6 3.5 0 90
7 3.5 3.5 90
8 1.25 5.15 0
9 -2 1.7 180

注意,对象列表的顺序与我们将在场景动画中穿过的顺序相同。将它们作为Artworks的子对象,按照此顺序在层次结构中放置。

向画廊添加图片

请从您的照片库中找到 10 张您最喜欢的照片以供使用,并将它们添加到名为Photos的新项目资产文件夹中。我们将编写一个脚本,该脚本将根据图像列表将它们添加到场景中的每个ArtworkRigs

  1. 要创建照片文件夹,导航到资产 | 创建 | 文件夹,并将其命名为Photos

  2. 通过从文件资源管理器拖放 10 张照片到您刚刚创建的Photos文件夹(或导航到资产 | 导入新资产...)来导入 10 张照片。

现在,我们将编写一个脚本来填充Artworks Images

  1. 在层次结构中,选择Artworks。然后在检查器中,导航到添加组件 | 新脚本,并将其命名为PopulateArtFrames

  2. 打开新脚本进行编辑。

按照以下方式编写PopulateArtFrames.cs的代码:

using UnityEngine;

public class PopulateArtFrames : MonoBehaviour
{
    public Texture[] images;

    void Start()
    {
        int imageIndex = 0;
        foreach (Transform artwork in transform)
        {
            GameObject art = artwork.Find("ArtFrame/Image").gameObject;
            Renderer rend = art.GetComponent<Renderer>();
            Material material = rend.material;
            material.mainTexture = images[imageIndex];
            imageIndex++;
            if (imageIndex == images.Length)
                break;
        }
    }
}

这里发生了什么?首先,我们声明一个名为images的公共Textures数组。您将看到,这将出现在检查器中,我们可以指定要包含在场景中的哪些照片。

此脚本附加到 Artworks 容器对象上,该对象作为子对象包含 ArtworkRigs。当应用启动时,在Start()中,我们遍历所有theArtworkRigs,找到图像对象。对于每个图像,我们获取其渲染器组件的材质,并分配一个新的纹理,即列表中的下一张图像。我们使用imageIndex变量遍历列表,并在用完图像或 ArtworkRigs 时停止:

图片

聪明的读者可能会想,既然所有的 ArtworkRigs 都使用相同的材质,即默认材质,那么为什么改变任何 ArtworkRig 图像上的材质不会改变所有图像呢?实际上,当你在运行时修改其纹理(或其他属性)时,Unity 会通过克隆材质到一个新的唯一材质来处理这个问题。因此,每个 ArtworkRig 的图像都获得自己的材质和自己的纹理,因此,我们画廊中的每幅画都是不同的。

为了完成这个任务,让我们执行以下步骤:

  1. 保存脚本并返回 Unity 编辑器

  2. 在层次结构中选择Artworks,在检查器中展开 Populate Art Frames 脚本组件,并展开 Images 参数

  3. 将图像大小值设置为10

  4. 在项目资源下的Photos文件夹中找到您导入的图像,并将它们逐个拖放到图像槽中,作为元素 0 到元素 9

当您点击播放模式时,场景中的艺术作品将按照您指定的顺序填充图像:

图片

要在 VR 中查看场景,我们可以将 MeMyselfEye 定位在第一个 ArtworkRig 的前面:

  1. 在层次结构中选择MeMyselfEye相机装置

  2. 将其位置设置为(2, 0, -2.82)

这真是太好了!

管理艺术信息数据

我们可以到此为止,但假设我们想要跟踪比每个艺术作品图像更多的数据,例如艺术家、标题、描述等。首先,我们将考虑几种软件设计模式来管理数据,包括单独的列表、数据结构和可脚本化的对象。稍后,我们将更新我们的 ArtworkRig 以显示每个框架艺术作品的信息。

前两个场景仅用于说明。我们将最后实现ScriptableObjects

使用列表

一种方法是在PopulateArtFrames脚本中为每个数据字段添加更多的列表。例如,如果脚本有如下所示:

 public Texture[] images;
 public string[] titles;
 public string[] artists;
 public string[] descriptions;

检查器将显示以下内容(为了简洁,我限制了列表为四项):

图片

如您所想象,这可能会变得非常难以管理。例如,要更改元素 3,您必须访问所有列表,这很容易出错;事情可能会致命地失去同步。

使用数据结构

一个更好的方法可能是编写一个 C# struct(或 class),作为包含我们想要的每个字段的 数据结构,然后在 PopulateArtFrames 中的列表为此类型。例如,脚本可能如下所示:

[System.Serializable]
public struct ArtInfo
{
    public Texture image;
    public string title;
    public string artist;
    public string description;
}

public class PopulateArtFrames : MonoBehaviour
{
    public List<ArtInfo> artInfos = new List<ArtInfo>();
ArtInfo defining our data fields. Then, in PopulateArtFrames we declare it as a List (which must be initialized with the new List<ArtInfo>() call). In the script, we'd then reference the textures as artInfos[i].image. Likewise, you'd get its size using artInfos.Count  rather than Length. Also, we need to say it's System.Serializable so the list appears in the editor Inspector, as follows:

图片

现在我们有一个可以填充的 ArtInfo 元素列表,每个元素的数据都分组在一起。

这种结构的另一个好处是它可以从外部数据源更容易地填充,例如基于云的 JSON 数据或 CSV(逗号分隔值)数据文件。

如果你感兴趣从数据库加载数据,这里有一些方法,但超出了本章的范围。但简要来说,如果你确实找到了 CSV 数据的来源,这个方便的 CSV 解析器 (github.com/frozax/fgCSVReader) 基本上但能完成任务。如果你需要 JSON 解析器,例如从基于 Web 的 REST API,可以考虑 JSON .NET For Unity 包 (assetstore.unity.com/packages/tools/input-management/json-net-for-unity-11347) 或其他类似的包。

使用可脚本化对象

在之前的示例中,艺术信息数据是在场景层次结构中的 GameObject 上维护的。从软件设计的角度来看,这并不是数据真正所属的地方。数据对象不是游戏对象,应该单独管理。

在场景层次结构中,我们定义了关卡设计和游戏行为。ArtworkRigs 有空间坐标(Transform)和渲染器(以及可能需要的其他运行时组件,如碰撞器和刚体用于物理)。但其他数据,尽管是项目资产,可以存在于场景层次结构之外。为此,Unity 提供了 ScriptableObjects。我们首次在 第五章,实用交互对象 中介绍了 ScriptableObjects,作为在游戏对象之间共享输入数据的一种方式。我们在这里再次使用它们:

  1. 项目 窗口中,如果尚未存在,则在 Assets 下创建一个名为 ScriptableObjects 的新文件夹

  2. 在新文件夹中,右键单击并选择 创建 | C# 脚本

  3. 将脚本命名为 ArtInfo

  4. 然后,打开 ArtInfo.cs 脚本进行编辑

按如下方式创建 ArtInfo.cs 脚本:

using UnityEngine;

[CreateAssetMenu(menuName = "My Objects/Art Info")]
public class ArtInfo : ScriptableObject
{
    public Texture image;
    public string title;
    public string artist;
    [Multiline]
    public string description;
}

我们不是从 MonoBehaviour 继承,而是将类定义为 ScriptableObject。我们添加了一个 Multiline 属性用于描述,这样在检查器中的输入字段就会是一个文本区域。

如果你正在将 JSON 数据导入到你的项目中,并希望生成与 JSON 对象属性匹配的 ScriptableObject 类,请查看这个工具:app.quicktype.io/#r=json2csharp

在顶部,我们提供了一个CreateAssetMenu属性,它会在 Unity 编辑器中为我们的对象生成一个菜单项。由于可脚本化对象不会被添加到场景层次结构中,我们需要一种在项目中创建它们的方法。使用此属性会使操作变得简单,如下所示:

  1. 保存脚本并返回 Unity。

  2. 在项目窗口中,选择您导入图像纹理的相册文件夹。我们将在同一文件夹中创建艺术信息对象。

  3. 在 Unity 编辑器主菜单中,导航到“资产”|“创建”。

  4. 您将看到一个名为“我的对象”的新项,其中包含一个子菜单,其中有一个名为“艺术信息”的项,正如我们在脚本中的CreateAssetsMenu属性所指示的那样。

  5. 选择“艺术信息”以创建一个实例。默认情况下,它将在定义脚本所在的同一文件夹中创建(这可以在属性属性选项中更改)。

  6. 可能会有助于将对象重命名为与您的图像类似。例如,如果您有 PictureA,将其命名为PictureA Info

  7. 将图像纹理拖放到脚本化对象的图像槽中。

  8. 为标题、艺术家和描述添加信息。

这里是一个已填写数据的ArtInfo对象的截图:

图片

对所有图片重复这些步骤。完成时,您的艺术数据将成为项目资产。

要在项目中使用脚本化对象资产,我们可以修改PopulateArtFrames,就像我们之前对struct版本的代码所做的那样。我们将进行一些重构,在ArtworkRig上创建一个新的组件,以使用 ArtInfo 对象填充它,如下所示:

  1. 在层次结构中选择一个ArtworkRig

  2. 添加组件|新建脚本,命名为ArtworkController

打开它进行编辑,并按照以下内容编写:

using UnityEngine;

public class ArtworkController : MonoBehaviour {
    public GameObject image;

    public void SetArtInfo(ArtInfo info)
    {
        Renderer rend = image.GetComponent<Renderer>();
        Material material = rend.material;
        material.mainTexture = info.image;
    }
}

保存脚本并返回 Unity,在刚刚添加此组件的ArtworkRig上:

  1. 将图像子项拖放到图像槽中。

  2. 点击“应用”以保存ArtworkRig预制体

现在,更新PopulateArtFrames以迭代ArtInfo列表并将对象发送到ArtworkRig,如下所示:

using System.Collections.Generic;
using UnityEngine;

public class PopulateArtFrames : MonoBehaviour
{
    public List<ArtInfo> artInfos = new List<ArtInfo>();

    void Start()
    {
        int index = 0;
        foreach (Transform artwork in transform)
        {
            artwork.GetComponent<ArtworkController>().SetArtInfo(artInfos[index]);

            index++;
            if (index == artInfos.Count || artInfos[index]==null)
                break;
        }
    }
}

现在,检查器界面更加整洁且易于操作。艺术作品的PopulateArtFrames组件维护一个艺术信息对象的列表,如下所示。我们只需要填充列表并使用它。列表引用的数据作为ScriptableObjects单独维护:

图片

按下播放。艺术图像应该在启动时加载,就像之前一样,尽管我们已经大大改进了底层实现,现在可以扩展我们的应用程序以包含有关每幅艺术图片的更多信息。

在这种情况中使用 ScriptableObject 的另一个优点是,一旦您有一个可分发的应用程序,您可以将这些资产打包到 AssetBundle 中。这将允许,例如,在实时版本中更换画廊图片以及所有艺术信息。

显示艺术信息

现在我们对每件艺术品都有更多信息,我们可以将其纳入我们的项目。我们将在ArtworkRig中添加一个 UI 画布。首先,我们将包括每张图片的信息牌匾。然后我们将使其交互式。如果你想要 Unity 的画布和 UI 元素的提醒介绍,请查看第六章,世界空间 UI

创建标题牌匾

标题牌匾将是一个位于每张图片旁边的小画布,包含标题文本 UI 元素:

  1. 在层次结构中选择一个ArtworkRig对象。

  2. 添加一个子画布,创建 UI | 画布,命名为 InfoPlaque。

  3. 将其渲染模式设置为世界空间。

  4. 初始重置其位置位置为(000)。

  5. 设置画布宽度:640,高度:480

  6. 如果你记得,在世界上缩放的画布将是 640 米宽!将缩放设置为0.0006

  7. 现在你可以使用移动捕捉工具直观地调整位置,我们发现以下值有效:位置(0.8-0.1-0.01)。

  8. 接下来,创建一个子面板,创建 UI | 面板。

  9. 然后在面板的子面板中,创建一个子文本元素,创建 UI | 文本,将其重命名为 Title。

  10. 设置一些默认文本,如 Title title title title。

  11. 对于锚点预设(变换面板左上角的复杂图标),选择拉伸/拉伸,点击并也 Alt 点击它。这将使文本填充面板区域。

  12. 字体大小:80

  13. 对齐:中间,中心。

  14. 设置其水平溢出:Wrap,垂直溢出:Truncate

现在,我们可以修改ArtworkController脚本以添加一个新的public Text title变量,并将其text属性设置为info.title,如下所示:

using UnityEngine;
using UnityEngine.UI;

public class ArtworkController : MonoBehaviour {

    public GameObject image;
 public Text title;

    public void SetArtInfo(ArtInfo info)
    {
        Renderer rend = image.GetComponent<Renderer>();
        Material material = rend.material;
        material.mainTexture = info.image;

 title.text = info.title;
    }
}

那很简单。保存脚本,然后:

  1. 将 Title 元素拖放到文本槽中

  2. 要保存预制体,请确保在层次结构中选择ArtworkRig本身,然后按应用。

现在当你按播放时,图片图像和标题文本将在每个艺术品 rig 的Start时初始化。这是我的一张带有标题牌匾的照片:

图片

交互式信息详情

我们对每张适合在牌匾上放置的图片都有更多信息,因此我们将允许玩家点击输入控制器按钮以打开详细信息信息框。让我们首先创建那个画布,包括艺术家和描述的文本:

  1. 作为快捷方式,复制 InfoPlaque 并将其命名为DetailsCanvas

  2. 缩放并定位它,可能在前方并稍微倾斜。以下值对我有效:位置(0.70-0.2),宽高(1200900),旋转(0150)。

  3. 将标题文本元素重命名为Description

  4. 复制 Description,重命名为Artist,设置为顶部对齐。

  5. 点击应用以保存预制体。

ArtworkController现在也可以填充详细信息字段:

 public Text artist;
 public Text description;
 public GameObject detailsCanvas;

SetArtInfo函数中:

     artist.text = info.artist;
     description.text = info.description;

然后,我们将添加一个Update处理程序来检查用户输入并显示(或隐藏)细节画布。并确保在Start时画布处于禁用状态。

    void Start()
    {
        detailsCanvas.SetActive(false);
    }

    void Update()
    {
        if (Input.GetButtonDown("Fire1"))
        {
            detailsCanvas.SetActive(true);
        }
        if (Input.GetButtonUp("Fire1"))
        {
            detailsCanvas.SetActive(false);
        }
    }

对于 Android 上的 Daydream,你会调用GvrControllerInput.ClickButtonDownClickButtonUp

保存脚本。

  1. 将艺术家和描述元素拖动到相应的槽中

  2. 将 InfoDetails 画布拖动到细节画布槽中

  3. 在 ArtworkRig 上按下“应用”以保存预制体更改

现在,当您播放并按下输入控制器上的 Fire1 按钮时,细节画布将显示如下:

图片

如果您想实现不同的按钮,例如手指触发器,或者使用没有 Fire1 映射(Daydream)的设备,请参考第五章,实用交互项,以获取实现选项和处理输入事件。

如此实现,当您按下按钮时,所有细节画布都会出现。如果您想一次控制一个画布,可以在 InfoPlaque 上添加一个 UI 按钮,例如,然后使用该按钮的点击事件来触发画布的可见性,使用基于注视的查看和点击,或激光指针和点击交互。参考第六章,世界空间 UI,以获取实现想法。

调整图像的宽高比

您可能已经注意到,由于我们的框架图像以固定大小和宽高比显示,一些图片看起来被压扁了。我们真正希望的是框架和图像根据图像的尺寸进行调整。

当 Unity 导入纹理时,它将其(默认情况下)准备为 GPU 渲染的对象材质纹理,这包括将其调整到平方的 2 的幂(例如,1024 x 1024,2048 x 2048 等)。如果您将项目调整为在运行时读取图像,例如从 Resources 目录、设备的照片流或通过网络,那么您将能够访问图像文件的元数据标题,其中包含其像素宽度和高度。由于我们使用的是导入的纹理,我们可以更改我们使用的图像的高级导入设置:

  1. 从您的“资产照片”文件夹中选择一个图像纹理

  2. 在检查器中,在“高级”选项下,将“非 2 的幂”更改为“无”

  3. 按下“应用”

对项目中的每个图像重复此操作。请注意,这也将解压缩图像,因此原本可能是一个 400k 的.jpg文件,在项目中变成了 3MB,24 位的图像,所以请谨慎选择要使用的源图像的宽度和高度。

不将纹理缩放到 2 的幂次方对性能非常不利。如果您有超过几个图像,您应该避免这样做。一种方法是将图像宽高比作为 ArtInfo 的另一个字段,并在相应的脚本对象中手动设置该值。然后,将 ArtworkController 更改为使用此值而不是计算它。

ArtworkController.cs 中添加以下辅助函数,该函数返回纹理的归一化缩放。较大的一边将是 1.0,较小的一边将是分数。例如,一个 1024w x 768h 的图像将获得缩放 (1.0, 0.75)。它还使用 Z 缩放值保持图片的当前相对缩放,因为 Z 缩放值不会因我们的宽高比计算而改变,但会被缩放工具改变!

首先修改 ArtworkController,添加一个私有函数 TextureToScale,该函数将图像缩放归一化到宽度或高度较大的 1.0,并将另一维设置为宽高比,如下所示:

    private Vector3 TextureToScale(Texture texture, float depth)
    {
        Vector3 scale = Vector3.one;
        scale.z = depth;
        if (texture.width > texture.height)
        {
            scale.y = (float)texture.height / (float)texture.width;
        } else
        {
            scale.x = (float)texture.width / (float)texture.height;
        }
        return scale;
    }

该函数还保留了返回的缩放向量中的帧深度。现在,我们可以在 SetArtInfo 函数中使用它。为 frame 添加一个新的公共变量:

public Transform frame;

然后,添加以下行以设置帧的缩放:

frame.localScale = TextureToScale(info.image, frame.localScale.z);

保存更新脚本。然后,在 Unity 中:

  1. 将 ArtFrame 拖动到组件中的 Frame 槽

  2. 按下 Apply 以保存预制体

现在当你玩游戏时,框定的图像会以正确的宽高比缩放,就像这里显示的:

图片

在画廊中移动

我们已经做了很多,但还没有讨论在画廊级别移动。在第七章 Chapter 7,Locomotion and Comfort(移动和舒适度)中,我们探讨了实现移动和传送的多种方法。现在,让我们考虑设置特定的传送出生点,为画廊中每幅艺术作品的图片提供最佳的观看姿势。

在图片之间传送

我认为一个良好的观看位置大约在图片后一米处。我们可以在 ArtworkRig 中添加一个 ViewPose 对象,并将其原点放在地板上。现在让我们添加它:

  1. 在 Hierarchy 中选择 ArtworkRig

  2. 创建一个空的子游戏对象,命名为 ViewPose

  3. 重置 ViewPose 变换

  4. 将其位置设置为 (0, -1.4, -1.5)

在第七章 Chapter 7,Locomotion and Comfort(移动和舒适度)中,我们探讨了实现移动和传送的多种方法,包括我们自己的脚本以及更高级的工具包。在这里,我们将使用 SteamVR 和 Daydream 的传送工具包。有关这些工具包的更一般介绍或替代解决方案,请参阅该章节。

使用 SteamVR 交互系统传送

要使用 SteamVR 交互系统,我们首先从他们的 Player 预制体开始,并添加我们想要使用的组件:

  1. SteamVR/InteractionSystem/Core/Prefabs 中定位 Player 预制体

  2. 在你的场景 Hierarchy 中将其作为 MeMyselfEye 的子对象拖动

  3. 删除或禁用 [CameraRig] 对象

  4. 从项目 Assets/SteamVR/InteractionSystem/Teleport/Prefabs 中拖动 Teleporting 预制体的一个副本作为 MeMyselfEye(此控制器实际上可以在场景中的任何地方移动)的子对象

  5. 在 Hierarchy 中选择 Player,并将其父对象 MeMyselfEye 拖动到其 Tracking Origin Transform 槽

  6. 在“ArtworkRig”中选择“ViewPose”对象

  7. 从项目Assets/SteamVR/InteractionSystem/Teleport/Prefabs中将“TeleportPoint”预制件的副本拖放到层次结构中作为“ViewPose”的子对象

  8. 选择“ArtworkRig”并应用以保存预制件更改

就这样!按播放。传送点不会显示,直到你按下控制器上的按钮,然后它们会发光,一个虚线激光弧让你选择一个,然后你就能到达那里。以下是激活传送点时的场景视图截图:

图片

使用“Daydream Elements”传送

“Daydream Elements”工具包更细致,因此需要更多时间才能使其工作。默认情况下,“TeleportController”允许你传送到场景中任何水平表面(前提是有碰撞器)。为了限制它只传送到我们的传送站,我们将搜索限制在特定层,命名为Teleport

  1. 在层次结构中的“ArtworkRig”中,选择其“ViewPose”对象,并创建一个子圆柱体(创建 | 3D 对象 | 圆柱体),并将其命名为TeleportPod

  2. 将其缩放设置为(0.50.50.01)。你可以选择装饰其材质,例如,使用透明度。

  3. 将其放在层“Teleport”上(如果没有名为"Teleport"的层,首先从层选择列表中选择添加层...)。

  4. 选择父级“ArtworkRig”并应用以保存预制件更改。

现在,我们添加“Daydream Elements”的传送控制器:

  1. 将“TeleportController”预制件拖放到你的层次结构中作为“Player”的子对象(对我们来说,MeMyselfEye / GVRCameraRig / Player

  2. 如果需要,重置其变换

  3. MeMyselfEye对象拖放到TeleportController组件的“Player”变换槽中

  4. 将“GvrControllerPointer”(或你正在使用的任何控制器游戏对象)拖放到“Controller”变换槽中

  5. 在“TeleportController”的“Valid Teleport Layers”中,选择“Teleport”(这样“Default”和“Teleport”都被选中)

  6. 在“Raycast Mask”中,我们只想选择“Teleport”,因此选择“Nothing”(取消选择所有选项),然后选择“Teleport”。层设置在此处的屏幕截图中显示:

图片

按播放。当控制器指针的弧线接触到传送舱时,它会发光高亮。如果你点击,你将被传送到那个位置。

房间规模考虑事项

我们设计的画廊级别布局在坐着、站着或非位置跟踪 VR 中效果最好。例如,我们使用的之字形隔断在房间规模 VR 中不是一个好主意,除非你小心不要让玩家的游戏空间(守护者边界)穿过这些墙壁。这是可以做到的,但你可能需要使整体空间更大,可能适应实际的播放空间大小,并添加条件到我们在本章后面实现的传送功能中,这会使我们的示例复杂化。有关房间规模考虑事项的更多信息,请参阅第七章,移动和舒适

以下图像展示了 MeMyselfEye 在房间规模 OpenVR 相机装置中的初始位置,显示守护边界完美地适合第一个 ArtworkRig 的画廊观看空间。在其他观看情况下可能不太容易适应,因此你可能需要调整以阻止玩家穿过墙壁(或穿过 Ethan!)。此外,虽然这是默认的长度和宽度,但玩家的实际空间将根据其配置要求而变化。为了完全适应这些可能性,可能需要前往一个程序生成的关卡布局,其中墙壁的位置和缩放在运行时根据玩家设置确定。

图片

动画化浏览体验

如果你能够确定玩家将坐着或至少站在一个地方,他们可能会喜欢画廊的浏览体验导览。

在传统游戏中,通常使用第一人称动画来制作过场动画,即从一个关卡到另一个关卡的预录飞行动画。在 VR 中,情况有所不同。浏览体验可以真正成为 VR 体验本身。头部跟踪仍然处于活动状态。因此,这不仅仅是一个预录制的视频。你可以四处张望并体验它,这更像是游乐园的游乐设施。这通常被称为轨道式 VR 体验。

在你的 VR 应用中使用浏览动画时要小心。它可能会引起晕动症。如果你这样做,尽可能给玩家提供更多控制权。例如,我们正在动画化MeMyselfEye装置,允许用户继续四处张望。将用户置于一个有固定表面的驾驶舱或车辆中,也可以减少晕动症的倾向。另一方面,如果你是寻求刺激的人,可以使用这里类似的技术在三维移动轨道上制作过山车之旅!

在这个主题中,我们正在自己编写动画脚本。在后面的章节中,我们将更深入地探讨其他 Unity 动画和电影工具。我们创建了一个RidethroughController控制器,用于动画化第一人称角色(MeMyselfEye)的变换位置和随时间旋转。它通过定义关键帧变换,使用 Unity 的AnimationCurve类(docs.unity3d.com/ScriptReference/AnimationCurve.html)来实现。正如其名所示,对于关键帧动画,我们在骑行中定义玩家在特定关键时间点的位置。中间帧会自动计算。

  1. 在层次结构的根目录下创建一个新的空游戏对象,并将其命名为RidethroughController

  2. 添加一个新的 C#脚本组件,并将其命名为RidethroughController

  3. 打开脚本进行编辑

首先,我们将定义一些我们将需要的变量:

 public Transform playerRoot;
 public GameObject artWorks;
 public float startDelay = 3f;
 public float transitionTime = 5f;

 private AnimationCurve xCurve, zCurve, rCurve;

playerRoot是我们将要动画化的玩家变换(MeMyselfEye)。artWorks是艺术品装置的容器。我们包括了一个指定初始延迟和图片之间过渡时间的选项。设置函数将生成三个曲线,用于位置(xz)和旋转(关于y轴)。

接下来,我们编写一个SetupCurves函数,生成动画曲线,使用每个 ArtworkRig 的 ViewPose 作为曲线中的节点。我们同时为x、z 和旋转曲线这样做如下:

    private void SetupCurves()
    {
        int count = artWorks.transform.childCount + 1;
        Keyframe[] xKeys = new Keyframe[count];
        Keyframe[] zKeys = new Keyframe[count];
        Keyframe[] rKeys = new Keyframe[count];

        int i = 0;
        float time = startDelay;
        xKeys[0] = new Keyframe(time, playerRoot.position.x);
        zKeys[0] = new Keyframe(time, playerRoot.position.z);
        rKeys[0] = new Keyframe(time, playerRoot.rotation.y);

        foreach (Transform artwork in artWorks.transform)
        {
            i++;
            time += transitionTime;
            Transform pose = artwork.Find("ViewPose");
            xKeys[i] = new Keyframe(time, pose.position.x);
            zKeys[i] = new Keyframe(time, pose.position.z);
            rKeys[i] = new Keyframe(time, pose.rotation.y);
        }
        xCurve = new AnimationCurve(xKeys);
        zCurve = new AnimationCurve(zKeys);
        rCurve = new AnimationCurve(rKeys);
    }

我们将定义RidethroughController,当游戏对象被启用时开始动画:

    void OnEnable()
    {
        SetupCurves();
    }

在每次更新中,我们评估 X 和 Z 曲线来设置玩家的当前位置。同时,我们评估旋转曲线来设置玩家的当前旋转。我们使用原生的Quaternion表示旋转,因为我们正在在两个我们不想使用欧拉坐标的角度之间进行插值:

    void Update()
    {
        playerRoot.position = new Vector3(
                xCurve.Evaluate(Time.time), 
                playerRoot.position.y, 
                zCurve.Evaluate(Time.time));

        Quaternion rot = playerRoot.rotation;
        rot.y = rCurve.Evaluate(Time.time);
        playerRoot.rotation = rot;

        // done?
        if (Time.time >= xCurve[xCurve.length - 1].time)
            gameObject.SetActive(false);
    }

最后,我们通过比较当前时间与曲线中最后一个节点的时时间来检查我们是否完成了动画。如果是这样,我们将禁用游戏对象。

在这个脚本中,我直接使用了transform.rotation的 Quaternion y 值。通常不建议直接操作 Quaternion 的值,但由于我们一致地只改变一个轴,这是安全的。有关 Quaternion 与欧拉角度的更多信息,请参阅docs.unity3d.com/Manual/QuaternionAndEulerRotationsInUnity.html

如所写,如果/当 RidethroughController 游戏对象被启用时,动画将播放。你可以保存启用它的场景,当应用开始时它将播放。我们将把它留给你来修改,使其可以通过玩家选项触发,例如应用内的开始骑行按钮!

保存脚本,然后执行以下步骤:

  1. 从层次结构中,将MeMyselfEye拖放到玩家根槽中

  2. Artworks(包含所有ArtworkRigs)拖放到 Artworks 槽中

当你播放场景时,你将获得一次轻松愉快的艺术画廊之旅,每张照片会有短暂的暂停来观看。这真的很棒!希望你能挑选出可以展示给你的所有朋友和家人的图片!

摘要

在本章中,我们从零开始构建了一个艺术画廊场景,首先从一张 2D 平面图开始,然后在 Blender 中构建一个 3D 建筑结构。我们将模型导入 Unity,并添加了一些环境照明。接着,我们构建了一个由图像、画框和聚光灯组成的艺术品装置,并在画廊的各个墙上放置了装置的实例。然后,我们导入了一大批个人照片,并编写了一个在运行时填充艺术画框的脚本。添加了关于每件艺术品的更详细数据后,我们探索了管理非图形数据列表的几种方法。最后,我们添加了在艺术画廊级别内移动的能力,通过传送和自动的第一人称场景浏览来实现。

在下一章中,我们将探讨一种使用预先录制的 360 度媒体的不同类型的 VR 体验。你将构建并了解球形照片、等经纬投影和图表信息。

第十章:使用所有 360 度

360 度照片和视频是消费者今天可以体验、制作和发布的一种虚拟现实的不同方式。观看预先录制的图像所需的计算能力远低于渲染完整的 3D 场景,尤其是在基于手机的 VR 中效果非常好。在本章中,我们将探讨以下主题:

  • 理解 360 度媒体和格式

  • 使用纹理查看地球仪、全景照片和天空盒

  • 将 360 度视频添加到您的 Unity 项目中

  • 编写和使用自定义着色器

  • 在您的 Unity 应用程序中捕获 360 度图像和视频

注意,本章中的项目是独立的,并不直接需要本书中其他章节的项目。如果你决定跳过其中任何部分或没有保存你的工作,那没问题。

360 度媒体

最近,360 度和虚拟现实这两个术语被频繁使用,经常出现在同一句话中。消费者可能会被误导,认为这一切都是相同的,一切都已解决,制作起来非常简单,而实际上并非如此简单。

通常,360 度一词指的是以允许你旋转视图方向以揭示视野之外内容的方式查看预先录制的照片或视频。

非 VR 360 度媒体已经变得相对常见。例如,许多房地产列表网站提供基于网页的播放器,允许你交互式地平移查看空间的全景预览。同样,Facebook 和 YouTube 支持上传和播放 360 度视频,并提供具有交互式控制功能的播放器,在播放过程中可以四处查看。Google Maps 允许你上传 360 度静态球面图像,就像你可以使用 Android 或 iOS 应用程序或消费级相机创建的街景一样(更多信息,请访问www.google.com/maps/about/contribute/photosphere/)。互联网上充满了 360 度媒体!

使用 VR 头盔观看 360 度媒体非常令人沉浸,即使是静态照片也是如此。你站在一个球体的中心,图像投射到内部表面,但你感觉你真的在捕捉的场景中。只需转动你的头四处查看。这是那些第一次看到就会让人对 VR 产生兴趣的事情之一,并且它是 Google Cardboard 和 Gear VR 的流行应用,为许多人启动了消费级 VR 革命。

等角投影

自从发现地球是圆的以来,制图员和航海家一直在努力将球形地球投影到二维图表上。变化多种多样,历史非常迷人(如果你对这类事情感兴趣的话!)结果是地球某些区域的不可避免扭曲。

想了解更多关于地图投影和球形畸变的信息,请访问 en.wikipedia.org/wiki/Map_projection

作为一名计算机图形设计师,这可能比古代航海家所面临的神秘性要小一些,因为我们了解 UV 纹理映射

在 Unity 中的 3D 计算机模型由 网格 定义 - 一组通过边连接的 Vector3 点,形成三角形面。你可以将网格(例如在 Blender 中)展开成扁平的 2D 配置,以定义纹理像素到网格表面的相应区域的映射(UV 坐标)。地球仪展开后将会扭曲,正如展开的网格所定义的那样。生成的图像称为 UV 纹理图像

在计算机图形建模中,这种 UV 映射可以是任意的,并且取决于手头的艺术要求。然而,对于 360 度媒体,这通常使用 等角(或子午线)投影(更多信息,请访问 en.wikipedia.org/wiki/Equirectangular_projection)来完成,其中球体被展开成圆柱投影,当你向南北极前进时拉伸纹理,同时保持子午线作为等距的垂直直线。以下 Tissot's indicatrix(更多信息请访问 en.wikipedia.org/wiki/Tissot%27s_indicatrix)显示了一个地球仪,其中战略性地排列了相同的圆圈(Stefan Kühn 绘制):

以下图像显示了使用等角投影展开的地球仪:

由 Eric Gaba 绘制 - 维基媒体 Commons 用户:Sting

我们将为我们的照片球体使用等角网格,并为它的纹理图使用适当的投影(扭曲)图像。

虚拟现实正在黑客攻击你的视野

好吧,但为什么虚拟现实中的 360 度媒体如此吸引人?我们在平屏幕上观看 360 度视频和在 VR 头盔内观看时的体验有巨大的差异。例如,IMAX 电影院比传统电影院拥有更大的屏幕,它包含了更多的你的周边视野,并且有更宽的 视野FOV)。在自然观看距离下,手机或电脑显示器大约是 26 度的 FOV;电影院是 54 度(IMAX 是 70 度)。Oculus Rift 和 HTC VIVE 大约是 120 度。在人类视觉中,一只眼睛大约是 160 度,两只眼睛结合提供大约 200 度的水平视野。

想了解更多关于传统视频游戏中视野调整的信息,请阅读优秀的文章 关于视野的一切(2014 年 7 月 18 日)并访问 steamcommunity.com/sharedfiles/filedetails/?id=287241027

在 VR 中,你不会那么明显地受到 FOV(视场)和屏幕物理尺寸的限制,因为你可以轻松地移动头部来改变你的观看方向。这提供了完全沉浸式的视角,水平 360 度,当你左右和上下看时,可以由 180 度实现。在 VR 中,当你头部不动时,视场只与你的外围视野和眼球运动有关。但当你移动头部(颈部和/或全身)时,软件会检测头部姿势(观看方向)的变化并更新显示。结果是,你相信自己有一个不间断的 360 度图像视角。

180 度媒体

我有时开玩笑说,用我的消费级 360 度相机拍照和录视频就像同时拍摄风景照和自拍!尤其是当你拍照时手持相机,你总是出现在照片中。但事实上,当你拍照时,你很可能已经面对着动作,所以当查看照片时,用户也在面对着动作。所以可能你只需要 180 度。此外,向后看可能会很麻烦。正如其名所示,180 度图像是 360 度图像的一半,投影到一个半球上。

2017 年,谷歌为 VR 引入了 180 度媒体的标准(vr.google.com/vr180/)。除了提供等角投影外,这些相机有两个镜头用于捕捉立体图像,每个眼睛一个。对于 180 度视角来说,效果相当不错,因为虽然你可以左右移动来环顾四周,但实际上需要的移动相对较小(人类的水平外围视野大约是 200 度)。立体 360 度更具挑战性。

立体 180 度媒体

要捕捉单视角 360 度媒体,你可以使用消费级 360 度相机。这些相机通常有几个背对背的超广角镜头和相应的图像传感器。通过巧妙算法将捕获的图像拼接在一起,避免接缝,然后将结果处理成等角投影。在 VR 中观看,每只眼睛看到相同的 360 度照片。对于风景,如山景或其他大区域,如果主题距离你的观看位置超过 20 米,那就没问题,因为没有视差。每只眼睛几乎从相同的观看角度看到。但如果照片包括离你更近的物体,它看起来可能不正确,或者至少是人为地扁平,因为你会期望有视差,而每只眼睛有略微不同的视角。

那么,真正的 360 度立体是什么样的呢?难道每只眼睛不应该有自己的从另一只眼睛位置偏移的球面照片吗?

要捕获立体360 度媒体,不能简单地由两个从两个视点拍摄的 360 度摄像机拍摄,而可以通过拼接旋转立体对中的图像来构建。摄像机图像捕获之间的距离模拟了人类眼睛之间的分离(IPD,瞳距)。有一代新的消费级摄像机(如Fuze Cameravuze.camera/,拥有八个摄像头),以及高端专业摄像机装置,如Google Jump (vr.google.com/jump/),它在一个圆柱阵列中排列了十六个独立的摄像头。然后,先进的图像处理软件构建立体视图。

谷歌推出了一种用于立体 360 度视频的高级文件格式:全向立体,或 ODS。它是一种传统的等经线投影的变体,具有避免不良接缝或死区的优点,它预先渲染以实现更快的播放,视频使用传统的编码,因此您可以使用传统的工具进行编辑。Unity 支持其在全景天空盒着色器中的 ODS(见本章后面的主题)。

对于立体 360 度媒体捕获的挑战和几何形状的更详细解释,请参阅谷歌白皮书《渲染全向立体内容》(developers.google.com/vr/jump/rendering-ods-content.pdf)。此外,通过访问paulbourke.net/stereographics/stereopanoramic/,您可以查看保罗·博克(2002 年 5 月)撰写的文章《立体 3D 全景图像》。

玩转照片球

要开始探索这些概念,让我们在将普通(矩形)图像作为纹理应用到球体上时玩点小乐趣,看看它会做什么以及看起来有多糟糕。然后,我们将使用正确扭曲的等经线球面纹理。

水晶球

在《绿野仙踪》中,多萝西对着水晶球寻求邪恶女巫的帮助时,她喊道:“阿姨!阿姨!”让我们考虑使用 Unity 制作一个水晶球,我的小宝贝!

首先,通过以下步骤为这一章设置一个新的场景:

  1. 通过导航到文件 | 新场景创建一个新的场景。然后,通过导航到文件 | 保存场景为...并将它命名为360Degrees

  2. 通过导航到 GameObject | 3D Object | Plane 创建一个新的平面,并使用变换组件的齿轮图标重置其变换 | 重置。

  3. 将主摄像机位置设置为(00-1

您可以选择使用我们在整本书中使用的 MeMyselfEye 摄像机装置,但在这个项目中不是必需的。主摄像机将实现基于您在玩家设置中选择的 SDK 的 VR 摄像机。我们将不会使用特定于设备的输入或其他功能。

现在,在创建第一个球体时,同时编写一个旋转脚本。我正在使用这本书提供的 EthanSkull.png 图片(将其拖放到您的 Project Assets Textures 文件夹中)。

然后,执行以下步骤:

  1. 通过导航到 GameObject | 3D Object | Sphere 创建一个新的球体,使用变换组件的 齿轮 图标重置其变换 | 重置,并将其命名为 CrystalBall

  2. 将其位置设置为(01.50)。

  3. 将名为 EthanSkull 的纹理(你可以使用任何照片)拖放到球体上。

  4. 通过导航到添加组件 | 新脚本并命名为 Rotator 来创建一个新的脚本。

注意,将纹理拖放到游戏对象上会自动在您的 Materials/ 文件夹中创建一个名为 EthanSkull.mat 的相应材质,并在 Albedo 纹理贴图槽中使用此纹理。

打开 rotator.cs 脚本并编写如下:

using UnityEngine;

public class Rotator : MonoBehaviour
{
    [Tooltip("Rotation rate in degrees per second")]
    public Vector3 rate;

    void Update()
    {
        transform.Rotate(rate * Time.deltaTime);
    }
}

注意,我们为 Unity 编辑器添加了一个 Tooltip 属性,为开发者提供了更多关于如何使用 rate 值的详细信息。

然后,设置旋转速率,使其围绕 y 轴每秒旋转 20 度,如下所示:

  1. 在旋转脚本组件上,将 X、Y、Z 的速率设置为(0200)。

  2. 保存场景。在 VR 中尝试它。

图片

这是不是有点吓人? 别担心。投影的图像可能会扭曲,但它看起来很酷。对于某些应用,一点扭曲是艺术意图,你不必担心。

仔细编辑,如模糊照片的边缘,可以帮助避免纹理贴图中的接缝。

当我们在这里时,让我们尝试通过调整着色器属性来使球体看起来更像水晶玻璃:

  1. 在检查器中选择 CrystalBall

  2. 将其金属值设置为 0.75

  3. 将其光滑度值设置为 0.75

  4. 打开 Albedo 颜色(点击颜色块),并将 Alpha(A)值调整为 100

这样看起来更好。将更多具有不同纹理的对象添加到场景中,以可视化透明度和镜面高光。

如果您对水晶球的更逼真的玻璃模拟感兴趣,这里有一些建议:

  • 考虑向场景中添加一个反射探针(docs.unity3d.com/Manual/class-ReflectionProbe.html),以便表面看起来像在反射场景中的其他对象。

  • 对于透明度和折射,Standard Assets Effects 包中提供了一个 GlassRefractive 材质。

  • 在您的材质中尝试一个自定义着色器。Unity ShaderLab 文档中给出了一个简单玻璃着色器的示例(docs.unity3d.com/Manual/SL-CullAndDepth.html)。

  • 还可以考虑第三方材质和着色器,它们可以模拟具有折射、扭曲、玻璃表面图案和颜色的玻璃(在 Asset Store 中搜索,assetstore.unity.com/search?q=category%3A121&q=glass)。

  • 注意,在 VR 应用程序中应谨慎使用透明度,因为它需要每个像素额外的渲染过程,可能会减慢帧生成并造成不希望的延迟。

球体

接下来,我们将制作另一个球体并添加一个纹理,就像我们刚才做的那样,但这次使用具有等经线(球面)畸变的纹理。

将包含在此书中的Tissot_euirectangular.png图像(可在维基百科上找到,en.wikipedia.org/wiki/Tissot%27s_indicatrix#/media/File:Tissot_behrmann.png)导入到你的纹理文件夹中,并执行以下步骤:

  1. 创建一个新的球体并命名为Globe。如果你想的话,可以添加Rotator脚本。

  2. 将名为Tissot_equirectangular的纹理拖放到球体上。

  3. 在 VR 中尝试一下。仔细观察球体,如图所示:

图片

注意,不幸的是,Tissot 圆圈是椭圆形的,而不是圆形,除了赤道沿线。结果发现,Unity 中提供的默认球体不适合等经线纹理图。相反,我提供了一个专门为此目的设计的,PhotoSphere.fbx(这恰好是 3D Studio Max 中的默认球体模型)。让我们试试:

  1. 通过将PhotoSphere.fbx文件拖动到你的项目资产模型文件夹中(或通过菜单:资产 | 导入新资产...)来导入它。

  2. 通过将PhotoSphere模型从“项目资产”拖动到场景中,创建一个新的等经线球体。

  3. 设置其位置并命名为Globe2。如果你想的话,可以添加Rotator脚本。

  4. 将名为Tissot_equirectangular的纹理拖放到球体上。

在 VR 中尝试一下。效果更佳。现在你可以看到纹理已经正确映射;圆圈是圆的(并且底层的网格更规则):

图片

现在你可以将任何 360 度照片应用到球体上,创建你自己的照片球体或虚拟圣诞树饰品!

在这个话题上进一步扩展,你可以构建一个漂亮的太阳系模型。每个行星和卫星的等经线纹理图都可以从太阳系视界免费下载(www.solarsystemscope.com/)。有关自转(昼夜)和轨道(绕太阳)的数据可以在 NASA 网站上找到(nssdc.gsfc.nasa.gov/planetary/factsheet/index.html)。完整的 Unity 教程项目可以在《开发者增强现实》一书中找到(www.amazon.com/Augmented-Reality-Developers-Jonathan-Linowes/dp/1787286436)。

另一个想法是,照片球体在 VR 游戏中已被用作传送机制——作为玩家,你抓住描绘另一个场景的球体,把它放在你的脸上,你就会被传送到那个世界。有关如何捕获 Unity 场景的 360 度照片,请参阅捕获 360 度媒体这一主题。

渲染全景球

地球的倒数是全景球。地球将等经线纹理映射到球体的外表面,而全景球会将纹理映射到内部表面,并且你从内部观看,这样它就包围了你。

对于我们的示例,我正在使用本书提供的Farmhouse.png图像,如下所示。您可以使用自己的 360 度照片,无论您是否有理光 Theta 这样的 360 度相机,还是使用 Android 或 iOS 的图片拼接应用,或者从网络上的任何数量照片源下载。

图片

正如我们所见,Unity 通常只渲染物体的外表面。这在数学上由其表面网格每个面的法线方向向量决定。平面是最简单的例子。在第二章“内容、对象和比例”中,我们创建了一个带有大峡谷的大屏幕图像平面。当你面对平面时,你会看到图像。但是如果你在平面后面移动,它不会被渲染,就像它根本不在场景中一样。同样,假设你面前有一个立方体或球体;你会看到它被渲染、照明和着色。但是如果你把头伸进物体里面,它似乎消失了,因为你现在正在看物体的内部面。这一切都是由着色器处理的。而且由于我们想要改变它,我们需要使用不同的着色器。

编写自定义内着色器

我们将编写一个自定义着色器来在球体网格的内部渲染我们的纹理。

着色器是 Unity 渲染管道的关键部分,也是计算机图形学和虚拟现实中的许多魔法实际发生的地方。Unity 提供了一套令人印象深刻的内置着色器,正如您可能通过在检查器中打开任何对象的 Shader 选择列表所注意到的。您导入的许多资产包也可能包括实现自定义效果的着色器,包括我们在前几章中已经使用的一些,如 TextMeshPro 和 TiltBrush。Oculus、Google Daydream 和 SteamVR 的 VR 工具包也包括提供额外性能和渲染管道优化的着色器。

编写着色器是计算机图形和 Unity 开发的高级主题。尽管如此,Unity 提供了工具来简化着色器的编程(请参阅docs.unity3d.com/Manual/SL-Reference.html),包括名为ShaderLab的声明性语言、大量文档和教程以及可从中工作的示例着色器。我们不会深入探讨,但许多人发现这是一项非常有趣且宝贵的技能。

要创建一个新的着色器,开始如下:

  1. 导航到创建 | 着色器 | Unlit Shader,并将其命名为MyInwardShader

  2. 双击新的着色器文件以打开它进行编辑

要将着色器转换为内部着色器,你只需要添加一行Cull Front,例如,紧接在Tags行之后,如下所示:

    ...
    Tags { "RenderType"="Opaque" }
    Cull Front
    ...

Cull命令告诉着色器是否忽略正面或背面朝向的表面。默认是背面;我们将将其更改为裁剪正面并渲染背面。(有关详细信息,请参阅docs.unity3d.com/Manual/SL-CullAndDepth.html。)

保存文件。现在我们可以在我们的项目中使用它。

注意着色器文件的第一行将其命名为Shader "Unlit/MyInwardShader",这意味着你可以在选择 Shader | Unlit 子菜单中找到它,或者你可以不使用子菜单将其修改为Shader "MyInwardShader"

由于我们正在反转纹理,它可能看起来是反向镜像的。我们将通过将其 X 平铺设置为-1来修复它,正如我们将看到的。

另一种方法是着色器内反转顶点法线。我们在本书的第一版中使用了该技术,这里展示了它:

Shader "MyInwardNormalsShader" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType" = "Opaque" }
        Cull Off

        CGPROGRAM
        #pragma surface surf Lambert vertex:vert
        sampler2D _MainTex;

        struct Input {
            float2 uv_MainTex;
            float4 color : COLOR;
        };

        void vert(inout appdata_full v) {
            v.normal.xyz = v.normal * -1;
        }

        void surf (Input IN, inout SurfaceOutput o) {
             fixed3 result = tex2D(_MainTex, IN.uv_MainTex);
             o.Albedo = result.rgb;
             o.Alpha = 1;
        }
        ENDCG
    }
      Fallback "Diffuse"
}

简而言之,这个着色器脚本声明了以下内容:

  • 允许你提供纹理和颜色属性

  • 不进行表面裁剪(纹理将在内部和外部都可见)

  • 使用简单的 Lambert 漫反射光照算法(与未照明或标准 Unity 物理光照相比)

  • vert函数通过乘以法向量-1来反转网格顶点

  • surf渲染器复制纹理像素,并允许你使用 Albedo 颜色对其进行着色(但强制 Alpha 为不透明)

你可以使用这个着色器代替我们之前写的快速着色器。

考虑一下,如果你在着色器设置中使用了 Alpha 通道并设置了一个裁剪遮罩会发生什么。这将允许照片球体的一些区域完全透明。这为在场景内嵌套多个照片球体以创建 360 度活动视觉层提供了可能性!

魔法球体

在我们进行完整的 360 度照片查看之前,为了好玩,让我们首先考虑一个特殊情况,魔法球体。对于这个例子,我们将从内部查看球体,将 360 度图像映射到其内部表面。然后,我们将在外部放置一个实色壳体。所以,你真的需要把头伸进球体里才能看到里面的东西,或者抓起球体,“戴”在眼睛上!

要构建它,请按照以下步骤操作:

  1. 通过导航到资产 | 创建 | 材质创建一个新的材质,并将其命名为FarmhouseInward

  2. 在检查器中,使用着色器选择器并选择 Unlit | MyInwardShader,这是我们刚刚创建的。

  3. 定位到Farmhouse纹理图像,并将其拖拽到着色器组件的 Albedo 纹理上。如果需要,将 Tiling X 设置为-1以补偿镜像。

  4. 将一个新的球体添加到场景中,将之前介绍的PhotoSphere.fbx从你的模型文件夹中拖拽出来,并将其命名为"MagicOrb"。

  5. FarmhouseInward材质拖拽到球体上。

通过以下步骤,我们将将其封装在一个单色球体中:

  1. 在层次结构中选择MagicOrb对象,右键单击,导航到 3D 对象 | 球体,以便新的球体成为子对象。

  2. 将其缩放设置为略大于内球体的大小,例如(1.02, 1.02, 1.02

  3. 通过取消勾选禁用其球体碰撞器组件。

  4. 找到一个实色材质,例如我们在前一章中制作的名为RedMaterial的材质,并将其拖拽到新的球体上。

在 VR 中尝试。从外面看,它像一个实心球,但靠近它,里面有一个全新的小世界!以下图片是我看到的截图。就像透过鸡蛋壳看进去一样!

图片

对于非位置跟踪的移动 VR 设备,你可能无法在 VR 中完成此操作,但可以在编辑器中播放场景的同时,在场景视图中手动拖拽相机装置。或者,添加一些如第七章所述的移动,移动和舒适度。或者,使球体可抓取,这样玩家就可以拿起它并将其移动到非常接近他们脸部的位置,使用第五章中描述的技术,便捷交互对象

如果你想要深入了解着色器,作为一个练习,尝试看看你如何修改 InwardShader 以接受一个额外的颜色参数,该参数用于渲染外表面,而纹理用于渲染内表面。

光晕层

是的,先生,这现在是热门话题。它比全景图好。它比自拍好。它甚至可能比 Snapchat 更好!我们终于到了你一直等待的时刻!这是 360 度光晕层!

在本章中,我们涵盖了众多主题,这将使得讨论 360 度光晕层变得相对容易。要构建一个,我们只需使用MyInwardShader着色器制作一个非常大的球体。

开始一个新的空场景:

  1. 通过导航到文件 | 新场景创建一个新的场景。然后,文件 | 保存场景并将其命名为PhotoSphere。删除默认的主相机。

  2. 添加MyMyselfEye预制件,并将其变换位置重置为(0, 0, 0)。

  3. 通过从Project Models文件夹中将PhotoSphere模型拖拽到场景中(如前一个示例中从PhotoSphere.fbx导入),创建一个等经线球体。

  4. 重置其变换(齿轮图标 | 重置)并将其缩放设置为(10, 10, 10)。

  5. 创建一个材质(创建 | 材质)并将其命名为PhotoSphere Material

  6. 导航到着色器 | 无光照 | MyInwardShader(如本章前面创建)。

  7. Photosphere材质拖放到Photosphere游戏对象上。

  8. 如果场景中将有其他对象,您可能需要禁用阴影。在球面游戏对象上,在其 Mesh Renderer 组件中取消选中接收阴影复选框。

现在,要添加照片:

  1. 导入您想要使用的照片;我们的照片命名为FarmHouse.jpg

  2. 选择PhotoSphere(或PhotoSphere Material本身),将FarmHouse纹理拖放到 Albedo 纹理图块上。

  3. 如果需要,将 Tiling X 值设置为-1以补偿镜像反转。

按下播放。现在您应该能在场景中看到环绕您的球面照片。

如果您使用的是带有位置跟踪的设备,例如 Oculus Rift,我们需要禁用它。在MemMyselfEye上创建一个新的脚本如下:

public class DisablePositionalTracking : MonoBehaviour
{
    void Start()
    {
        UnityEngine.XR.InputTracking.disablePositionalTracking = true;
    }
} 

您可能会发现默认的纹理分辨率和/或压缩质量不足以满足您的需求。要修改分辨率,请按照以下步骤操作:

  1. 选择纹理(Farmhouse.png)

  2. 在检查器中,将最大尺寸更改为40968192

  3. 按下应用以重新导入纹理

注意文件大小(检查器底部)可能会呈指数级增长,影响您应用程序的最终大小、加载时间和运行时性能。还可以尝试其他压缩设置,包括新的 Crunch 压缩(blogs.unity3d.com/2017/11/15/updated-crunch-texture-compression-library/)。您可以根据每个平台配置这些设置。

要切换图像,重复最后两个步骤:导入资产并将其分配给Photosphere Mataterial的 Albedo 纹理。如果您想在游戏中这样做,可以在脚本中完成(例如,使用Material.mainTexture())。

播放 360 度视频

添加 360 度视频的步骤与将常规矩形视频添加到项目中的步骤大致相同(见docs.unity3d.com/Manual/class-MovieTexture.html)。要播放 360 度视频,您使用Video PlayerRender Texture上渲染视频。如果您没有现成的 360 度视频,可以在网上搜索免费下载,并选择一个不太长且文件大小有限的一个。

根据您视频的格式,您可能需要首先在系统上安装 QuickTime,然后才能将视频导入 Unity 进行转换编解码器。

如果您愿意,开始一个新的场景并将 MyMyselfEye 变换重置到原点。然后,将 360 度视频导入到您的项目资产中。注意其尺寸(例如,4K 视频是 4096 x 2048)。如果您不确定,可以在检查器中查看。

按照以下方式将视频播放器添加到您的项目中:

  1. 创建一个名为"VideoPlayer"的空对象

  2. 添加组件 | 视频播放器

  3. 将您的视频文件拖放到其视频剪辑槽中

  4. 选择“唤醒时播放”和“循环”复选框

  5. 确保渲染模式设置为渲染纹理

现在,我们将创建一个 Render Texture,这是 Unity 的一个特殊纹理,将在运行时由视频播放器渲染:

  1. 在你的项目资源中,创建 | 渲染纹理,命名为 "Video Render Texture"

  2. 将大小设置为视频的确切大小,(例如 4096 x 2048)。

  3. 建议将抗锯齿设置为 2 个样本。

  4. 你可以将深度缓冲区设置为无深度缓冲区

  5. 在层次结构中选择视频播放器,并将视频渲染纹理拖放到其目标纹理槽中

现在,创建你的全景照片:

  1. 创建一个新的 3D 球体并命名为 "VideoSphere"

  2. 重置其变换,使其位置为 (0, 0, 0), 然后将缩放设置为 (10, 10, 10)

  3. 将视频渲染纹理拖放到球体上并创建一个新的材质(或者你可以在创建材质之前先创建这个材质)

  4. 将材质着色器更改为 MyInwardShader

这里显示了 Inspector 中的结果视频播放器:

按下播放。你现在已经使用 Unity 构建了一个基本的 360 度视频播放器。

为了复习,球体使用的是向内着色器的材质。着色器在球体的内部渲染一个等角纹理。视频播放器在每个更新中都会用下一个视频帧修改那个纹理。

当为 Android 和 iOS 构建时,你必须将你的视频文件(例如 MP4)放入项目资源中的名为 StreamingAssets 的文件夹中。有关此信息和视频播放器和编解码器的其他考虑因素,请参阅 Unity 文档docs.unity3d.com/ScriptReference/Video.VideoPlayer.html

如果视频有音频,我们可以将视频作为音频源如下设置:

  • 选择视频播放器并添加组件 | 音频源

  • VideoPlayer 本身拖放到其视频播放器组件的音频源槽中

与所有 Unity 组件一样,视频播放器有一个 API,可以通过脚本进行控制。例如,要简单地通过按钮点击暂停视频,你可以在 VideoPlayer 上添加此脚本:

using UnityEngine;
using UnityEngine.Video;

public class PlayPause : MonoBehaviour {
    private VideoPlayer player;

    void Start() {
        player = GetComponent<VideoPlayer>();
    }

    void Update() {
        if (Input.GetButtonDown("Fire1"))
        {
            if (player.isPlaying)
            {
                player.Pause();
            }
            else
            {
                player.Play();
            }
        }
    }
}

对于额外的提示,还可以查看 Unity 的教程 交互式 360 视频入门:下载我们的示例项目 blogs.unity3d.com/2018/01/19/getting-started-in-interactive-360-video-download-our-sample-project/

使用 Unity skyboxes

在古代,或者至少在 360 度照片之前,我们简单地称 skyboxes 为在计算机图形中创建背景图像的方式。Skyboxes 描绘了地平线上的远处,可能有助于场景的环境光照,用于在物体表面上渲染反射,并且不可交互。Unity 支持每个场景的照明环境中的 skyboxes。我们已经在之前几章的项目中使用了 skyboxes(包括 Wispy Sky 和 Skull Platform 项目)。

天空盒的常见来源包括圆柱形全景、球形全景(360 度图像)和六面体。我们不会考虑圆柱形,因为它在 VR 中不太有用。

六面体或立方体贴图天空盒

天空盒可以表示为一个立方体的六个面,每个面都类似于一个摄像头捕捉其六个方向之一的视图,如图所示:

图片

给定这六张图片,作为纹理,你会创建一个如 WispySky 立方体贴图所示的下六面体天空盒材质。然后,在灯光窗口中将其设置为场景的天空盒材质:

图片

或者,你也可以将六张图片合并成一张单独的立方体贴图图像,布局类似。

立方体贴图的优势在于,等经纬度纹理在球形投影的顶部和底部极点拉伸图像时浪费了像素。另一方面,必须仔细设计图像,以确保它们可以平滑拼接,不会产生接缝或其他视觉伪影。

传统的立方体贴图的一个变体是等角立方体贴图EAC)。EAC 力求拥有更加均匀的像素大小和“在 3D 空间中均匀的像素角度分布”。(参见blog.google/products/google-vr/bringing-pixels-front-and-center-vr-video/。)

但如今大多数 360 度媒体,尤其是来自消费级相机的媒体,都使用等经纬度投影,也就是球形全景。

球形全景天空盒

使用 360 度照片作为天空盒被称为球形全景。在本章的早期,我们使用球形游戏对象渲染等经纬度纹理,并将玩家摄像头放置在其中的正中央。现在,我们将使用相同的图像在天空盒中。 (注意,这同样适用于 180 度内容。)

从一个新的空场景开始:

  1. 通过导航到文件 | 新场景来创建一个新的场景。然后,文件 | 保存场景并将它命名为Skybox。将Main Camera替换为MyMyselfEye预制体。

  2. 假设你正在使用Farmhouse.jpg图像,如之前所述,创建一个新的Material并将其命名为Farmhouse Skybox

  3. 对于材质的着色器,选择天空盒 | 全景。

  4. 将你的 360 度图像(Farmhouse.jpg)拖放到球形纹理区域。

  5. 将映射设置为经纬度布局。

  6. 将图像类型设置为 360 度。

材料设置如下所示:

图片

现在要在场景中使用它:

  1. 打开灯光窗口标签(如果不在你的编辑器中,请导航到窗口 | 灯光)

  2. 将你的Farmhouse Skybox材质拖放到天空盒材质槽中。

灯光环境设置如下所示:

图片

按下播放。哇!你现在应该能在场景中看到围绕你的照片球体。这几乎太简单了。谢天谢地!

有趣的一点是,由于天空盒始终以非常远的距离渲染,因此相机将始终位于球面的中心。因此,我们不需要在原点设置相机装置,也不需要禁用位置跟踪,就像我们在本项目的球形游戏对象版本中做的那样。无论您走到哪里,天空盒都会围绕您。如果您的 360 度图像包含相对较近的内容(人或物体),这可能会感觉非常不自然,就像物体被投影或压扁在球形投影上(它们确实是!)这就是为什么天空盒通常用于风景和开阔空间。(稍后,我们将看到如何使用 立体 天空盒来解决这个问题。)

到目前为止,您可以为场景添加更多内容。毕竟,我们在 Unity 中,而不仅仅是制作一个通用的 360 度照片查看器。通过添加下雪或落叶(例如,参见 Falling Leaves 粒子包,assetstore.unity.com/packages/3d/falling-leaves-54725)来增强您可爱的户外场景。

常见的应用是在大厅场景中使用 360 度图像,并添加一个交互式菜单面板来启动其他应用或场景。例如,谷歌 Daydream 大厅。

另一个应用是通过添加 UI 画布来标注照片中的内容,使 360 度图像更具交互性。这可能需要一些深思熟虑的工作来将标签与球面相匹配。然后,使用相机射线投射,您可以动态突出显示玩家正在查看的内容(参见第四章,基于注视的控制,获取编码技巧)。

360 度视频天空盒

将您的天空盒转换为 360 度视频播放器几乎与之前为球形游戏对象版本概述的步骤相同。我们不会重复所有内容,但简要来说如下:

  1. 设置一个 Video Player 来播放视频源到 Render Texture

  2. 设置一个 Skybox Material 以接收 Render Texture

  3. 将场景设置为使用 Skybox Material

注意,根据 Unity,等角圆柱体视频的天空盒着色器应具有精确的 2:1 宽高比(或对于 180 度内容,1:1)。此外,许多桌面硬件视频解码器限制在 4K 分辨率,而移动硬件视频解码器通常限制在 2K 或更低,这限制了在这些平台上可以实时播放的分辨率。

3D 立体天空盒

如果您有一个带有立体视图的 360 度图像或视频,Unity 现在可以为左右眼使用这些图像。截至 Unity 2017.3,全景天空盒材质支持具有 3D 布局的 3D 纹理。您可以选择并排或上下,如图所示:

图片

在下一主题中给出了一个示例 3D 立体等角圆柱体上下图像,我们将讨论在您的 Unity 项目中捕捉 360 度媒体。

在 Unity 中捕捉 360 度

我们已经讨论了使用 360 度相机捕获的 360 度媒体。但如果你想在 Unity 应用内捕获 360 度图像或视频并在互联网上分享呢?这可以用于市场营销和推广你的 VR 应用,或者仅仅是将 Unity 作为一个内容生成工具,但使用 360 度视频作为最终的分发媒介。

捕获立方体贴图和反射探针

Unity 包括支持将其照明引擎的一部分捕获场景视图。调用camera.RenderToCubemap()将烘焙场景的静态立方体贴图,使用相机的当前位置和其他设置。

Unity 文档中给出的示例脚本,docs.unity3d.com/Documentation/ScriptReference/Camera.RenderToCubemap.html,实现了一个编辑器向导,可以直接在编辑器中捕获场景的立方体贴图,并包含在此处:

using UnityEngine;
using UnityEditor;
using System.Collections;

public class RenderCubemapWizard : ScriptableWizard
{
    public Transform renderFromPosition;
    public Cubemap cubemap;

    void OnWizardUpdate()
    {
        string helpString = "Select transform to render from and cubemap to render into";
        bool isValid = (renderFromPosition != null) && (cubemap != null);
    }

    void OnWizardCreate()
    {
        // create temporary camera for rendering
        GameObject go = new GameObject("CubemapCamera");
        go.AddComponent<Camera>();
        // place it on the object
        go.transform.position = renderFromPosition.position;
        go.transform.rotation = Quaternion.identity;
        // render into cubemap
        go.GetComponent<Camera>().RenderToCubemap(cubemap);

        // destroy temporary camera
        DestroyImmediate(go);
    }

    [MenuItem("GameObject/Render into Cubemap")]
    static void RenderCubemap()
    {
        ScriptableWizard.DisplayWizard<RenderCubemapWizard>(
            "Render cubemap", "Render!");
    }
}

要运行向导:

  1. 创建一个空的游戏对象作为捕获位置

  2. 创建一个用于渲染的立方体贴图(Assets | Create | Legacy | Cubemap)

  3. 将面大小设置为高分辨率,例如2048

  4. 选择可读复选框

  5. 运行向导(GameObject | Render into Cubemap)

  6. 将位置对象拖入 Render From Position 槽

  7. 将立方体贴图拖入 Cubemap 槽

  8. 按下渲染!

现在,这个.cubemap文件可以用于 Skybox Cubemap 材质。

另一种类似但不同的方法是使用反射探针。它们通常用于具有反射材质的对象,以渲染逼真的表面反射(见docs.unity3d.com/Manual/class-ReflectionProbe.html)。反射探针捕获其周围环境的球形视图,然后存储为立方体贴图。场景设计师会策略性地在场景中放置多个反射探针,以提供更逼真的渲染。你可以将反射探针作为场景的 360 度图像捕获!由于它们旨在用于反射照明,通常分辨率较低。

Unity 根据你的照明设置选择存储反射探针光照贴图文件(.exr)的位置。要将它保存在你的Assets文件夹下(而不是 GI 缓存),转到照明选项卡,禁用实时全局照明,并禁用自动生成。这将生成与场景同名的文件夹中的反射探针.exr文件。

通过导航到 GameObject | Light | Reflection Probe 尝试在你的场景中添加一个。将分辨率设置为高值,如2048。然后,按烘焙。然后你可以将这个.exr文件分配给 Skybox Cubemap 材质,快速轻松地制作 360 度场景快照。

使用第三方包进行 360 度图像捕获

有许多包提供了在 Unity 中捕获 360 度图像和视频的能力,包括:

这些软件包都支持单眼和立体捕获,序列化捕获用于视频编码,以及可能的其他功能,如颜色转换、抗锯齿、相机图像效果和 3D 空间化音频。

使用 eVRydayVR 的 360 Panorama Capture 脚本,例如,要捕获单个 360 图像,打开您想要捕获的场景,然后:

  1. 创建一个空的游戏对象,命名为 CapturePanorama,放置在您想要进行捕获的位置

  2. 添加 Capture Panorama 脚本作为组件

  3. 按下播放,然后按键盘上的 P

屏幕将变为黑色,并将捕获的图像保存到您的项目根目录。组件选项如下所示:

要捕获视频,您需要启用 Capture Every Frame 复选框。它推荐使用开源的 ffmpeg 工具 (www.ffmpeg.org/) 来组装帧并编码视频。有关详细信息,请参阅 README 文件。

当然,此组件也可以通过脚本进行控制,并可以构建到您的运行时游戏中,而不仅仅是用于编辑器。

Unity 内置的立体 360 图像和视频捕获

截至 Unity 2018.1,Unity 包含了集成的立体 360 图像和视频捕获功能。该功能基于 Google 的全向立体 (ODS),如本章开头所述。本节中的详细信息总结了 2018 年 1 月的 Unity 博客文章 (blogs.unity3d.com/2018/01/26/stereo-360-image-and-video-capture/),该文章解释了如何捕获 ODS 立体立方体贴图并将它们转换为立体等角纹理。

要在编辑器或独立播放器中捕获场景,每次对每个眼睛调用一次 camera.RenderToCubemap()。我们之前使用过此函数;有一个接受 stereoEye 参数的变体,例如:

camera.stereoSeparation = 0.064; // Eye separation (IPD) of 64mm.
camera.RenderToCubemap(cubemapLeftEye, 63, 
        Camera.MonoOrStereoscopicEye.Left);
camera.RenderToCubemap(cubemapRightEye, 63,
        Camera.MonoOrStereoscopicEye.Right);

要将立方体贴图转换为立体等角贴图,按照以下方式调用 RenderTexture.ConvertToEquirect()

cubemapLeftEye.ConvertToEquirect(equirect, 
        Camera.MonoOrStereoscopicEye.Left);
cubemapRightEye.ConvertToEquirect(equirect, 
        Camera.MonoOrStereoscopicEye.Right);

使用 Unity 帧记录器 (github.com/Unity-Technologies/GenericFrameRecorder),可以将这些图像作为立体 360 视频的帧序列捕获。

要在 PC 独立构建中捕获,您需要在构建设置中启用 360 Stereo Capture,如图所示,这样 Unity 就会生成此功能所需的着色器变体:

图片

这里是一个立体等经纬度视频捕获结果的示例(来自 Unity 博客,blogs.unity3d.com/wp-content/uploads/2018/01/image5-2.gif):

图片

摘要

360 度媒体令人着迷,因为 VR 技术改变了你的视野(FOV)。当你移动头部时,你所看到的视图会实时更新,使其看起来没有边缘。我们本章一开始就描述了 360 度图像是什么,以及球面的表面是如何被压扁(投影)成二维图像的,特别是等经纬度投影。立体 3D 媒体包括为左右眼分别提供的等经纬度视图。

我们在 Unity 中开始探索这一概念,通过简单地将常规图像映射到球体的外部,并可能因此对产生的扭曲感到惊讶。然后,我们看到了等经纬度纹理是如何均匀地覆盖球体的。接下来,我们使用自定义着色器反转球体,将图像映射到球体内部,使其成为一个 360 度全景照片查看器。然后,我们添加了视频。

然后,我们探讨了使用天空盒而不是游戏对象来渲染 360 度媒体的方法。我们看到了 Unity 如何支持立方体贴图和球形全景,视频天空盒以及 3D 立体天空盒。最后,我们探讨了使用第三方包和 Unity 内置 API 从 Unity 场景中捕获 360 度媒体的方法。

在下一章中,我们将探讨虚拟现实的一个重要应用,即叙事。利用 Unity 的动画和电影剪辑功能,我们构建了一个简短的 VR 电影体验。

第十一章:动画和 VR 叙事

我们讲述的故事以及我们讲述故事的方式,在很大程度上反映了我们是谁以及我们将成为什么样的人。人类之间的叙事与任何人类活动一样原始,是人际交流、神话、历史记录、娱乐以及所有艺术的基础。VR 正在成为最新且可能最深刻的叙事媒体格式之一。

在上一章中,我们探讨了 360 度媒体,它本身正在成为 VR 叙事的一种新形式,尤其是对于非虚构纪录片,能够传递人类经验并为人道主义危机创造沉浸式同理心。我们在这章中涵盖的许多工具和课程也可以用于 360 度媒体,但我们将重点关注 3D 计算机图形和动画。

对于这个项目,我们将创建一个简单的 VR 体验,一个关于一只鸟获得翅膀并学会飞翔的简单故事。

在本章中,我们将学习以下主题:

  • 导入和使用外部模型和动画

  • 使用 Unity 时间线激活和动画化对象

  • 使用动画编辑器窗口编辑属性关键帧

  • 使用动画控制器控制动画片段

  • 使故事互动

编写我们的故事

你开始在一个黑暗的场景中,注意到你面前地上有一棵小树苗。它开始长成一棵树。随着黎明的到来,一个鸟巢出现在我们面前,我们注意到里面有一个蛋。蛋开始晃动,然后孵化。一只小鸟破壳而出,四处跳跃,成长,并测试它的翅膀。最后,在白天,它飞向自由。

我们的故事是关于出生、成长、展开翅膀(字面和比喻意义上的),以及前进。我们将从一个音乐配乐开始,并根据其部分来动画化我们的图形。

我们正在使用免费现成的资产。当然,你可以使用你自己的音乐和图形,但我们将假设你正在跟随我们选择的资产进行操作,这些资产都可以在网络上免费获得(链接将在下面提供)。作为一个教学项目,它简约而不加装饰,没有添加一个精良产品可能期望的效果。但如果你 9 岁的表亲或侄子制作了这个项目,你一定会感到非常自豪!

我们将使用的配乐是披头士乐队和保罗·麦卡特尼的歌曲《黑鸟》的演绎版本。(下载链接将在下一节中提供,并且为了方便,本章文件中包含了一份副本。)基于我们对这首歌的 mp3 录音,我们在图表上草拟了我们的 VR 体验的大致时间线计划,如下所示:

图片

如所示,整首歌时长为 165 秒(2 分 45 秒)。它以 35 秒的器乐前奏开始,然后是第一段和第二段(也是 35 秒),接着是一个 25 秒的器乐部分,然后第三段是 35 秒。我们将利用这一点将我们的故事分为五个部分。

还应该计划出许多其他功能。例如,场景照明将从夜晚的黑暗逐渐变亮,直到黎明和白天。

收集资源

如前所述,我们将从各种免费和简单的资源中构建我们的故事。我建议你现在下载并安装每个资源(或你自己的替代品),以便我们在工作时可以访问它们:

注意,我们使用的鸟巢和鸡蛋对象是从网上找到的修改版本。它原本是 .c4d 格式,我们将其转换为 .fbx,打包成预制件,并做了一些其他修改。

创建初始场景

我们将使用一个飞机作为地面和一些来自《Nature Starter Kit》的自然岩石,一个带有鸡蛋的鸟巢和一只鸟来制作一个简单、极简的场景:

  1. 创建一个新的场景(文件 | 新场景)并将其命名为 "Blackbird"(文件 | 保存场景为)

  2. 创建一个名为 GroundPlane 的 3D 平面,重置其变换,然后将其缩放到 (10, 10, 10)

  3. 创建一个新的材质 GroundMaterial,将其 Albedo 颜色设置为土色棕色(例如 #251906ff),并将材质拖放到平面上

  4. 设置主摄像机位置为 (0, 2, -3)

你可以用我们全书一直在使用的 MeMyselfEye 摄像机装置替换 Main Camera,但在这个项目中不是必需的,因为我们不会使用特定设备的输入或其他功能。Main Camera 将根据你在玩家设置中选择的 SDK 提供足够的 VR 摄像机。

我们正在使用一个简单的地面平面,因为它给出了我们想要的美学效果。但这可能是一个探索 Unity 地形系统的良好机会。这是一个另一个丰富且非常强大的主题,你可以用树木和草地“绘制”复杂的景观。请参阅手册docs.unity3d.com/Manual/script-Terrain.html

现在,添加一棵树和一些石头:

  1. Assets/NatureStarterKit/Models/ 文件夹中,将树拖入场景。重置其变换,使其位于原点。

  2. 在树附近添加几块石头,将它们移动到部分埋在地下。你可能将这些石头放在名为 Environment 的空游戏对象下。

  3. 添加一个风区(创建 | 3D 对象 | WindZone),使树对象对风做出反应并使树叶沙沙作响。

我场景中的石头放置如下(所有都在缩放 100):

预制体 位置
rock03 (2.9, -0.6, -0.26)
rock03 (2.6, -0.7, -3.6)
rock04 (2.1, -0.65, -3.1)
rock01 (-6, -3.4, -0.6)
rock04 (-5, -0.7, 3.8)

接下来,我们将添加巢:

  1. 将 NestAndEgg 模型的副本拖入场景中。

  2. 在地面上将其缩放和定位,使其易于观看,靠近树,不要太小。我们选择了位置 (0.5, 0.36, -1.2) 和缩放 (0.2, 0.2, 0.2)。

然后添加一只鸟。Living Birds 包含包中没有乌鸦,但它确实有蓝松鸦,这已经足够接近了:

  1. 从项目 Assets/living birds/resources/ 文件夹中,将 lb_blueJayHQ 预制体拖入层次结构。为了方便,将其重命名为 Bluejay

  2. 缩放和定位它,使其看起来成熟并栖息在巢的边缘。我们选择了缩放 (8, 8, 8)、位置 (0.75, 0.4, -1.25) 和旋转 (0, 0, 0)。

鸟以 T 形插入场景。它附加了动画,我们将在本项目的稍后部分控制它们。像大多数角色动画一样,它最初运行一个 Idle 动画。(注意,不要旋转鸟对象,这会搞乱飞行动画。)

记得按播放键并检查它在 VR 中的外观。在 VR 中的外观总是与你在平面屏幕上看到的外观有很大不同。我们的场景和层次结构如下屏幕截图所示。你现在可能也想调整主相机的位置:

时间线和音频轨道

之前,我们使用坐标纸时间线规划了我们的电影。Unity 提供了几乎可以直接实现这些工具。这个时间线功能是在 Unity 2017 中引入的。

时间线由一个或多个随时间播放的轨道组成。它就像一个动画(它控制单个游戏对象的属性),但时间线与许多不同的对象和不同类型的轨道一起工作。正如我们稍后将要看到和解释的,时间线可以有音频轨道、激活轨道、动画轨道和控制轨道。

时间轴是一种 Unity 可播放 对象。可播放对象是运行时对象,它们随时间“播放”,根据其预定的行为更新每一帧。动画也是可播放对象。有关更多详细信息,请参阅 docs.unity3d.com/ScriptReference/Playables.Playable.html

现在我们将在项目中添加一个时间轴并添加一个音频轨道。要创建时间轴对象并在时间轴编辑器窗口中打开它,请按照以下步骤操作:

  1. 在层次结构中,创建一个空的游戏对象并命名为 BlackbirdDirector

  2. 打开时间轴编辑器(窗口 | 时间轴)。

  3. 在窗口中,您将看到一个消息 "要开始使用 BlackbirdTimeline 的新时间轴,请创建一个 Director 组件和一个时间轴资产",并附带一个创建按钮。

  4. 按下创建按钮。

  5. 然后,您将被提示在项目 资源 文件夹中保存一个新的可播放资产。命名为 BlackbirdTimeline。按保存。

到目前为止,您可能已经注意到了一些重要的事情刚刚发生:

  • BlackbirdTimeline 资产已创建在您指定的 Asset 文件夹中

  • 已将 Playable Director 组件添加到 BlackbirdDirector 游戏对象中,并将其与 BlackbirdTimeline 关联

  • 时间轴编辑器窗口已打开,用于 BlackbirdTimeline

下一个截图显示了 BlackbirdDirector 检查器及其 Playable Director 组件。Playable Director 组件控制时间轴实例何时以及如何播放,包括是否在唤醒时播放,以及包装模式(当时间轴播放完毕时做什么:保持、循环或无):

图片

这是 BlackbirdTimeline 的时间轴编辑器窗口:

图片

现在让我们向时间轴添加一个音频轨道,使用我们的披头士乐队歌曲:

  1. 在你的项目资源中定位 mp3 文件,并将其直接拖动到时间轴编辑器中

  2. 按下播放以正常播放场景,此时音乐也应该开始播放。

现在的时间轴编辑器中包含音频轨道:

图片

白色的垂直光标,或 播放头,指示当前的时间框架。默认的比例是帧,但在上一个截图我们已经将其更改为秒(使用右上角的齿轮图标)。您可以看到此剪辑从 0 开始,并持续到大约 165 秒。

您可以使用鼠标滚轮缩放视图。按键盘上的 "A" 键可查看所有内容。当时间轴包含多个轨道时,您可以通过按键盘上的 "F" 键来聚焦于特定的剪辑。

您可能会注意到时间轴编辑器左上角有一些预览控件。这些控件允许您播放时间轴的预览,而不是使用常规的编辑器播放按钮来播放整个场景。

不幸的是,在撰写本文时,时间轴预览播放模式无法播放音频剪辑。您需要使用编辑器播放模式来播放音频。

在这个场景中,我们决定制作环境音乐。如果没有选择音频源,音频将在 2D 模式下播放。如果你想将其作为空间音频播放,从场景中的特定位置发出,你应该创建一个音频源并将其放入时间轴轨道中。

使用时间轴激活对象

我们刚刚向时间轴添加了一个音频轨道。另一种类型的时间轴轨道是激活轨道。与特定的游戏对象相关联,激活轨道可以在指定的时间启用或禁用该游戏对象。

根据我们的计划,当时间轴开始时,鸟的巢将被隐藏(NestAndEgg对象)。在 35 秒时,它变为启用状态。此外,当巢首次启用时,它应该有WholeEgg。然后在 80 秒时,它被隐藏,HatchedEgg被启用代替。

如此,NestAndEgg游戏对象层级包含巢本身、一个WholeEgg对象和一个HatchedEgg(它有两个蛋壳半部分):

图片

现在我们将激活序列添加到时间轴上:

  1. 在层级中选择BlackbirdDirector,将NestAndEgg对象从层级拖动到时间轴编辑器窗口中。

  2. 弹出一个菜单,询问要添加哪种类型的轨道;选择激活轨道。

  3. 在轨道上添加了一个小矩形轨道标记。点击并拖动到合适的位置。

  4. 将轨道定位和大小调整到从35:00开始到165:00结束。

现在来处理鸡蛋。尽管鸡蛋模型是NestAndEgg的子对象,但它们可以独立于父对象(当然,前提是父对象本身已经启用)被激活:

  1. WholeEgg对象从层级拖动到时间轴上作为激活轨道。

  2. 将其定位为从35:00开始到60:00结束。

  3. HatchedEgg对象从层级拖动到时间轴上作为激活轨道。

  4. 将其定位为从60:00开始到165:00结束。

类似地,当鸡蛋在 60 秒时孵化,激活鸟:

  1. Bluejay对象从层级拖动到时间轴上作为激活轨道。

  2. 将其定位为从35:00开始到60:00结束。

  3. HatchedEgg对象从层级拖动到时间轴上作为激活轨道。

  4. 将其定位为从60:00开始到165:00结束。

现在带有激活轨道的时间轴看起来如下。你可以看到,在左侧,每个轨道都有一个对象槽,包含由轨道控制的游戏对象。

图片

使用预览播放(时间轴编辑器左上角的控制图标)可以播放和预览这些轨道。你可以通过拖动白色的播放头光标来浏览时间框架。你会看到巢、鸡蛋和鸟按照指定的时间激活和停用。

记录动画轨道

如你所预期,除了音频和激活轨道外,时间线还可以包括动画轨道。Unity 的动画功能在过去几年中不断发展,时间线大大简化了 Unity 中的基本动画功能。你可以在时间线内直接创建和编辑动画,而无需创建单独的动画剪辑和动画控制器。这些内容我们将在本章的后面部分介绍。现在,我们将从简单开始,仅对树和巢的几个变换参数进行动画处理。

一棵生长的树

我们想在时间线中添加一个动画,使树从小(缩放 0.1)生长到全尺寸,从 0 秒到 30 秒。我们通过为树添加一个动画轨道,并在每个关键帧时间记录参数值来实现这一点:

  1. 确保在层次结构中选择 BlackbirdDirector 并打开时间线编辑器窗口

  2. Tree 从层次结构拖到时间线窗口中

  3. 选择动画轨道作为我们添加的轨道类型

现在,我们可以开始记录关键帧:

  1. 确保播放头光标设置为 0:00

  2. 在时间线中,按 Tree 轨道上的红色录制按钮开始录制

  3. 在层次结构中选择 Tree

  4. 将其缩放设置为 (0.1, 0.1, 0.1)

  5. 将播放头滑到 30 秒标记

  6. 在层次结构中仍然选择 Tree,将其缩放设置为 (1, 1, 1)

  7. 再次按下闪烁的红色录制按钮以停止录制

  8. 点击小图表图标以显示动画曲线,如图所示:

你可以看到,我们的时间线现在有一个引用 Tree 游戏对象的动画轨道。它有两个关键帧,从 0 秒开始,到 30 秒结束。Unity 为这两个关键值之间的过渡添加了一个温和的曲线,以平滑过渡。

当你抓住并拖动播放头光标在时间线曲线上时,你可以在场景窗口中看到树的大小变化。如果你按下预览播放图标,你可以播放动画。

一只生长的鸟

重复之前的练习,这次是培养蓝松鸦。将其从幼鸟(缩放 = 1)生长到全尺寸(缩放 = 8),在 60 秒和 70 秒标记之间持续 10 秒。

使用动画编辑器

接下来,我们将创建另一个动画轨道,以动画化巢,使其从生长的树中开始定位,然后缓慢飘落到地面,像落叶一样飘动。我们希望它表现出轻微的摇摆动作。这比我们刚才做的简单两个关键帧动画要复杂一些,所以我们将在一个单独的动画窗口中工作,而不是在时间线编辑器的狭窄轨道带上。它将从 0:35 动画到 0:45。

动画基于关键帧。要动画化一个属性,你需要创建一个关键帧,并定义该帧在时间上的属性值。在上一个例子中,我们只有两个关键帧,对应于起始和结束的缩放值。Unity 在两者之间填充了漂亮的曲线。你可以插入额外的关键帧,并编辑曲线形状。

一堆飘动的巢

假设您的场景已经将巢穴定位在地面,这是我们想要它最终到达的位置,以下步骤:

  1. NestAndEgg对象从层级结构拖动到时间轴窗口中。

  2. 选择动画轨道作为轨道类型。

  3. 将播放头光标设置到35:00

  4. 注意,当对象处于非活动状态时,录制图标将不可用。播放头必须在对象的激活轨道的激活范围内。

  5. 按下NestAndEgg动画轨道的录制图标开始录制。

  6. 在层级结构中选择NestAndEgg对象。

  7. 将当前变换值复制到剪贴板(在检查器中,选择变换组件上的齿轮图标,并复制组件)。

  8. 在场景窗口中,确保当前选中的是移动操纵杆。

  9. 在树中重新定位巢穴。Y 位置 = 5对我来说适用。

  10. 将播放头滑动到45:00

  11. NestAndEgg检查器中,点击变换的齿轮图标并粘贴组件值。

  12. 再次按下闪烁的红色录制按钮以停止录制。

在定义了初始动画录制后,我们可以在动画编辑器窗口中开始工作:

  1. 在轨道上,点击其右上角的小菜单图标

  2. 选择在动画窗口中编辑,如图所示:

图片

动画窗口有两种视图模式:Dopesheet 和 Curves。Dopesheet 视图允许您专注于每个属性的键帧。Curves 视图允许您专注于键帧之间的过渡。

目标是制作一个微妙的浮动动作,巢穴在 X 和 Z 轴上从一侧摇到另一侧,并在每个轴上轻微旋转。为此,我们首先将在落下的开始、中间和结束时“锚定”巢穴。(我们已经有开始和结束位置。)然后,我们将添加几个具有任意值的关键帧来实现柔和的运动。

使用 Dopesheet 视图,我们首先确保在开始和结束时间以及中间都有一个关键帧。按照以下方式在 35 秒、40 秒和 45 秒处添加关键帧:

  1. 如果没有,添加旋转属性(添加属性 | 变换 | 旋转 | “+”)

  2. 将播放头放置在动画的开始(35:00

  3. 在属性列表上方的控制栏中点击添加关键帧图标(如图下屏幕截图所示)

  4. 将播放头移动到大约一半的位置,到40秒标记

  5. 点击添加关键帧图标

  6. 再次确认,确保在结尾处有关键帧标记(45:00

您可以使用快捷键在关键帧之间切换。按“Alt+.”(Alt+句号)跳转到下一个关键帧。按“Alt+,”(Alt+逗号)跳转到上一个关键帧,按“Shift+,”(Shift+逗号)跳转到第一个关键帧。

现在,我们在37.5处添加一个关键帧:

  1. 将播放头移动到 37.5

  2. 点击添加关键帧图标

  3. 点击左上角的红色录制图标以捕获新值

  4. 在层级结构中选择NestAndEgg对象

  5. 在场景视图中,使用移动工具操纵杆,将巢穴在 X 和 Z 轴上稍微移动一点(大约 0.4 个单位)

  6. 使用旋转工具,在任意轴组合上轻轻旋转巢(最多 10 度)

  7. 将播放头移动到42.5并重复步骤 2-6

在 Dopesheet 视图中,显示的动画窗口,包括其位置和旋转属性值,如下所示,在关键帧 37.5 处。为读者标识了添加关键帧图标:

图片

曲线视图让您专注于关键帧之间的过渡,并提供调整值和塑造曲线样条的能力。我的当前曲线视图如下所示:

图片

动画窗口中滚动条的长度表示当前缩放视图。每个滚动条的椭圆形末端是可以抓取的控制,让您可以直接调整缩放以及视图的位置。

返回到时间轴编辑器窗口。您可以通过滑动播放头光标在场景窗口中查看动画,或按预览播放图标播放它们。

动画其他属性

在我们的故事中,我们希望灯光从夜晚开始,逐渐过渡到黎明再到白天。我们将通过操作方向光、天空盒材质和聚光灯来实现这一点。

动画灯光

为了达到戏剧效果,让我们让场景从夜晚慢慢淡入白天。我们将从开始关闭方向光,并逐渐增加其强度:

  1. 在层级中选择BlackbirdController并打开时间轴编辑器窗口

  2. 方向光对象从层级拖到时间轴上

  3. 按下其记录按钮

  4. 确保播放头在0:00

  5. 在层级中选择方向光并更改其强度参数为0

  6. 将播放头移动到40:00秒标记

  7. 将强度设置为1

这里显示了方向光的动画轨迹,其强度参数曲线如下:

图片

光照的其他参数也可以进行动画处理,包括其颜色和变换旋转角度。只需想象一下可能性!

让我们再添加一个点光源。为了达到戏剧效果,将其放置在巢的休息位置。这将首先照亮小树,一旦巢落在地面上,就会将用户的注意力集中在巢中的蛋上:

  1. 创建 | 光 | 点光源

  2. 在场景视图中,使用移动工具操纵杆将其定位在巢内,位于巢的地面位置

  3. 选择BlackbirdDirector并打开时间轴编辑器

  4. 点光源拖到时间轴编辑器

  5. 选择激活轨道

  6. 0到大约95启用灯光,在鸡蛋孵化后的一段时间内

看起来相当不错!

我们的 时间轴 开始变得有些拥挤。让我们将灯光移动到 轨道组:

  1. 在时间轴中,选择添加 | 轨道组

  2. 点击其标签并将其命名为“灯光”

  3. 将每个灯光轨迹拖入组中

使用组轨道以嵌套树结构组织时间轴

动画脚本组件属性

正如我们所看到的,你可以动画化任何你可以在检查器中修改的 GameObject 属性。这包括你自己的 C#脚本组件的序列化属性。

我们想要将环境光照从夜晚渐变到白天。有几种方法可以实现这一点(参见前一章中关于球面照明的讨论)。我们决定通过修改 Skybox 材质的曝光值来实现(0 是关闭,1 是完全开启)。但是时间轴只能动画化 GameObject 属性,而这不是其中一个。所以我们将创建一个空的 LightingController GameObject 并编写一个控制 Skybox 材质的脚本。

让我们为场景添加自己的 Skybox 材质。你可以使用你喜欢的任何天空盒纹理。我们将从之前导入的 WispySkybox 包中获取一个,名为WispyCubemap2

  1. 创建一个新的材质(资产 | 创建 | 材质),并将其命名为BlackbirdSkyMaterial

  2. 在检查器中,为其着色器选择 Skybox/Cubemap

  3. 点击选择其 Cubemap 纹理芯片,并选择WispyCubemap2

  4. 打开光照窗口(如果不在编辑器中,请选择窗口 | 光照 | 设置)

  5. BlackbirdSkyMaterial从项目资产拖动到天空盒材质槽位

  6. 取消选中“混合光照烘焙全局光照”复选框

我们不想烘焙任何环境光照,因为我们将在运行时修改其设置。

再次选择BlackbirdSkyMaterial,看看当你滑动曝光值在10之间时会发生什么。它将渐变天空盒的亮度。我们将动画化这个值来修改场景中的环境光。但是动画只能修改 GameObject 参数,所以我们将编写一个脚本:

  1. 创建一个新的 C#脚本,并将其命名为SkyboxMaterialExposureControl

  2. 打开脚本并按照以下方式编写:

public class SkyboxMaterialExposureControl : MonoBehaviour
{
    public Material skyboxMaterial;
    public float exp = 1.0f;

    private void Update()
    {
        SetExposure(exp);
    }

    public void SetExposure(float value)
    {
        skyboxMaterial.SetFloat("_Exposure", value);
    }
}

保存文件。在 Unity 中,让我们创建一个使用该脚本的 LightingController 对象,如下所示:

  1. 在层级面板中创建一个空对象,命名为"LightingController"

  2. SkyboxMaterialExposureControl添加到该对象

  3. BlackbirdSkyMaterial拖动到其 Skybox 材质槽位

现在,让我们为这个参数添加动画:

  1. 在层级面板中选择BlackbirdController并打开时间轴编辑器窗口

  2. 从层级面板中将LightingController对象拖动到时间轴上

  3. 按下它的录音按钮

  4. 确保播放头在0:00

  5. 在层级面板中选择LightingController,并将其 Exp 参数更改为0

  6. 将播放头移动到100:00秒标记

  7. 将 Exp 设置为1

这里显示了带有 SkyboxMaterialExposureControl 轨道的时间轴编辑器窗口:

图片

按下播放,随着天空盒材质的曝光从 0 到 1 变化,场景光照将从夜晚渐变到白天。(注意,在时间轴预览播放中不可用,仅在编辑器播放中可用)。以下是场景在约 45 秒时的截图:

图片

控制粒子系统

你可以使用其他效果继续改进场景。我们希望包括落叶效果,这可以通过粒子系统实现,并使用控制轨道播放。

很抱歉,我们无法推荐一个特定的免费“落叶”资产,因为我们找到的所有 Asset Store 中的都是付费的。有一个过时的免费 Sky FX 包(assetstore.unity.com/packages/vfx/particles/environment/sky-fx-pack-19242),我们从其中借用了纹理并制作了自己的粒子系统预制件,包含在这本书中。

假设你有一个 FallingLeaves 粒子系统,现在我们可以将其添加到项目中:

  1. FallingLeaves预制件的副本拖入场景中。

  2. 在时间轴编辑器窗口(选择BlackbirdDirector),点击添加并选择控制轨道。

  3. 在控制轨道的菜单图标中,选择添加控制可播放资产剪辑。

  4. 这在轨道上为剪辑创建了一个小矩形。选择它。

  5. 在检查器中,将FallingLeaves游戏对象从层次结构拖到源游戏对象槽中。

  6. 返回到时间轴窗口,抓住并滑动矩形到 120 秒的位置,然后将其右边缘拉伸到时间轴的末端(165 秒)。

这里显示了可播放资产的检查器:

图片

以及这个控制轨道的时间轴如下:

图片

同样,如果你在场景中有多个时间轴,你可以使用控制轨道(通过具有PlayableDirector组件的游戏对象)从另一个时间轴控制它们。在我们的应用程序中,我们使用一个单独的时间轴,带有在唤醒时播放,因此它从应用程序的开始播放到结束。然而,在场景中有多个时间轴时,你可以按需播放它们。

你也可以编写自己的自定义时间轴轨道类。例如,使用控制轨道播放粒子系统是有限的。这里(github.com/keijiro/TimelineParticleControl)是一个自定义轨道类,ParticleSystemControlTrack,它提供了控制发射率、速度和其他功能。如果你查看它们的.cs代码,它提供了一个编写自定义轨道类的良好示例。

分离的动画剪辑是另一种可添加并按顺序排列在时间轴轨道中的可播放资产。我们接下来看看这一点。

使用动画剪辑

对于下一个动画示例,我们将让鸡蛋在孵化前摇动和震动。我们将创建一个简单的动画,并使其在整个过程中循环。为了说明,我们将创建一个 WholeEgg 摇动的动画剪辑,并将其添加到动画剪辑轨道上的时间轴中。

摇晃鸡蛋

要在 WholeEgg 对象上创建一个新的动画剪辑,请按照以下步骤操作:

  1. 在层次结构中,选择 WholeEgg 对象(NestAndEgg 的子对象)

  2. 打开动画窗口(窗口 | 动画)

  3. 您应该看到一个消息,要开始动画 WhileEgg,请创建一个动画剪辑并一个创建按钮。

  4. 按下创建。

  5. 当提示文件名时,将其保存为EggShaker.anim

我们在本章的早期部分看到了动画窗口。我们将制作一个非常短的 2 秒动画,通过操作动画曲线来旋转鸡蛋在 X 轴和 Z 轴上:

  1. 使用窗口底部的曲线按钮显示曲线视图。

  2. 按下添加属性和 WholeEgg | 变换 | 旋转 | +以添加旋转属性。

  3. 在左侧选择 WholeEgg:旋转属性组。

  4. 按键盘上的A键以全屏缩放;您应该看到三条平行的线,每条线代表一个 X、Y、Z 旋转轴。

  5. 点击控制栏右上角的添加关键帧图标。

  6. 默认情况下,可能已经有一个关键帧在 1 秒(1:00)处。如果没有,移动播放头并单击添加关键帧。

  7. 滚出(鼠标中间滚轮或使用水平滚动条的椭圆形端点手柄)以便您可以看到 2 秒标记。

  8. 将播放头移动到 2 秒并添加关键帧。

  9. 将播放头移动到 1 秒标记处。

现在,我们将编辑动画样条曲线。如果您熟悉样条编辑,每个节点上有一条线表示该点的曲线切线,以及线的两端用于编辑曲线的手柄。(您也可以通过右键单击节点来修改此工具的操作。)

  1. 点击 Rotation.X 属性 1:00s 节点,然后抓住一个手柄来制作一个平滑的 S 曲线。不要太陡峭,介于 30 度和 45 度之间。

  2. 重复此操作,对 Y 轴和 Z 轴进行一些变化,如下所示:

图片

对于一个或两个轴,添加一个额外的关键帧,使曲线看起来更随机。我的最终曲线如下所示。

图片

完成这些操作后(曲线可以在稍后编辑和细化),选择 BlackbirdDirector,打开时间轴窗口,并执行以下步骤:

  1. 选择添加并选择动画轨道。

  2. WholeEgg对象从层次结构拖到时间轴上。

  3. 选择动画轨道。

这次,我们不会录制,而是使用我们刚刚创建的,并使其来回动画如下:

  1. 使用轨道上的菜单图标选择从动画剪辑添加。

  2. 在轨道上添加一个小矩形。将其滑动到大约 50 秒处,此时巢穴在地面,但小鸡尚未孵化。

  3. 在检查器中,我们现在有更多的剪辑选项。在动画外推下,选择后外推:ping pong。

带有时间轴的动画剪辑相当灵活。您可以将多个动画剪辑添加到动画轨道中,并通过将它们滑动到彼此之间来混合它们。如果您需要更多的控制,则可以使用 Animator Controller。

使用 Animator Controllers。

虽然将动画作为时间轴轨道记录非常方便,但它确实有限制。这些动画“存在于”时间轴中。但有时你希望将动画视为独立的资产。例如,如果你想使动画重复循环,或在不同动画之间切换,或混合动作,或将相同的动画曲线应用到其他对象上,你会使用动画片段。

我们将查看一些现有的动画师示例,然后使用现有的鸟类示例来让我们的蓝松鸦飞翔。

动画和动画师的定义

动画师是管理 Unity 中动画片段的标准方式,在时间轴出现之前。它使用动画组件、动画控制器和动画片段。幸运的是,如果你在对象上创建一个新的动画片段,Unity 会为你创建这些项目中的每一个。但了解它们如何协同工作是很重要的。

简单来说,根据 Unity 手册(docs.unity3d.com/Manual/animeditor-CreatingANewAnimationClip.html):

“在 Unity 中动画化 GameObject,对象或对象需要附加一个动画组件。这个动画组件必须引用一个动画控制器,而动画控制器反过来又包含对一个或多个动画片段的引用。”

这些对象起源于 Unity 中折叠的 Mecanim 动画系统,这个系统在几个版本之前就被整合进来了(你可能在 Unity 手册和网络搜索中仍然看到对 Mecanim 的引用)。这个动画系统特别适合用于人形角色动画(参见docs.unity3d.com/Manual/AnimationOverview.html)。术语可能看起来冗余且令人困惑。以下定义可能有所帮助(也可能不会!)。请特别注意“动画师”与“动画”的使用:

  • 动画片段:描述了对象属性随时间的变化。

  • 动画控制器:在状态机流程图中组织片段,跟踪当前应该播放哪个片段,何时动画应该改变或混合。引用它所使用的片段。

  • 动画组件:将动画片段、动画控制器和(如果使用)Avatar 结合在一起。

  • 不要使用旧版动画组件:动画组件是旧版的,但动画窗口不是!

  • 动画窗口:用于创建/编辑单个动画片段,可以动画化你在检查器中可以编辑的任何属性。显示时间轴,但与时间轴窗口不同。提供 Dopesheet 与曲线视图。

  • 动画师窗口:将现有的动画片段资产组织成类似流程图的有限状态机图。

实际上,时间轴动画记录也使用动画片段,只是你不需要明确创建它们。时间轴中的每个记录的动画轨道在你的资产文件夹中都有一个对应的可播放的动画文件(命名为“Recorded (n)”)。

第三人称控制器动画师

我们在前面章节中为伊森使用的ThirdPersonController角色预制件使用动画控制器来管理绑定模型上的人形动画剪辑。出于好奇,我们现在来检查它(尽管我们不会在这个场景中使用它):

  1. 暂时将ThirdPersonController预制件的副本从你的项目Assets/Standard Assets/Characters/ThirdPersonCharacter/Prefabs/文件夹拖到场景中。

  2. 在检查器中注意,它有一个动画组件,并且控制器槽引用了ThirPersonAnimatorController。点击它。

  3. 这将突出显示控制器资产(在Assets/.../ThirdPersonCharacter/Animator)。

  4. 双击ThirdPersonAnimatorController以在动画器窗口中打开它。

以下是伊森的动画图。你可以看到当角色被激活(Entry)时,它初始化到Grounded状态。椭圆形框是状态;它们之间的线条是转换。左侧是动画器可以使用的状态属性列表。例如,当Crouch为真时,动画转换到Crouching,播放该动画,然后转换回(并清除Crouch状态标志):

如果你打开Grounded状态(双击),你可以看到一个包含令人印象深刻的站立空闲、行走、转向等动画剪辑的 Blend Tree。这些将根据用户输入被激活和组合(混合)。

接下来,让我们看看另一个例子,我们Bluejay使用的BirdAnimatorController

你现在可以从场景中删除ThirdPersonController对象。

Living Birds 动画器

Living Birds 包附带了很多动画剪辑。你实际上可以在 Blender 或其他动画应用程序中打开 FBX 模型,检查模型和动画是如何定义的。这些已经被组合成一个BirdAnimationController。使用以下步骤检查动画器:

  1. 在层次结构中选择Bluejay

  2. 在检查器中注意,它有一个动画组件,并且控制器槽引用了BirdAnimatorController。点击它。

  3. 在项目资产中,双击ThirdPersonAnimatorController以在动画器窗口中打开它。

这里显示了动画图:

你可以看到几乎所有的动画都可以轻松地转换到和从空闲状态,无论是 Preen、Peck、Sing 还是 HopLeft、HopRight、HopForward 等。此外,注意 Idle -> Fly -> Landing -> Idle 循环,因为我们将要使用它。

蓝松鸦还有一个 C#脚本,lb_Bird,它调用动画器行为。这不是最干净的代码,但很有用。最相关的函数是OnGroundBehaviorsFlyToTarget

  • OnGroundBehaviors每 3 秒随机选择并播放一个空闲动画

  • FlyToTarget将使鸟飞到指定位置,包括起飞和降落以及随机振翅;看起来相当自然

因此,在我们的项目中,我们不会像记录下落巢穴的关键帧位置细节那样记录鸟儿动画路径的关键帧位置,而是定义特定的目标,并让 lb_Bird 脚本实际控制鸟儿的变换。这就像我们在第四章 基于注视的控制 中使用 Navmesh 指导 Ethan 的移动一样。我们将使用时间轴在一段时间内选择一个目标位置到下一个位置。

学习飞翔

首先,让我们创建一个 BirdController 并指定鸟儿应该飞越的位置列表。然后,我们将将其添加到时间轴中:

  1. 在层次结构中创建一个名为 BirdController 的空游戏对象并重置其变换。

  2. 创建一个子空对象,命名为 Location1。将其移动到 Nest 最近的岩石顶部。

  3. 创建另一个空对象,命名为 Location2,这次将其定位在巢穴附近但不在巢穴内。

  4. 继续创建位置标记。我使用的值基于我的场景和岩石位置,如下表所示。

  5. 最后一个位置应该很远。鸟儿将在视频结束时飞向那里。

名称 位置 描述
Location0 (0.75, 0.4, -1.25) Bluejay 的起始位置
Location1 (3, 0.8, 0) 在最近的岩石顶部
Location2 (1.2, 0.2, -1.7) 地面靠近巢穴但不在巢穴内
Location3 (2.5, 0.8, -3.4) 在下一个最近的岩石顶部
Location4 (-5.85, 0.8, -0.3) 下一个岩石
Location5 (-5, 0.33, 3.5) 最后一个岩石
Location6 (45, 11, 45) 在远处

BirdController 上创建一个新的 C# 脚本,命名为 BirdController,并编写如下:

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

public class BirdController : MonoBehaviour
{
    public GameObject bird;
    public List<GameObject> targets = new List<GameObject>();
    public int animIndex;

    public bool collideWithObjects = false;
    public float birdScale = 1.0f;

    private int prevIndex;

    void Start()
    {
        prevIndex = 0;
    }

    void Update()
    {
        if (animIndex != prevIndex && 
            index > 0 && 
            index < targets.Count)
        {
            prevIndex = animIndex;
            bird.gameObject.SendMessage("FlyToTarget", targets[index].transform.position);
        }
    }
}
}

这里发生了很多事情。我们将进行解释。

BirdController 有对 bird 的引用,以及一个位置 targets 列表。我们将在 Unity 编辑器中填充这个列表。每个位置由一个介于 0 和列表大小之间的索引值标识。一个整数 animIndex 将是时间轴控制的参数,告诉控制器鸟儿应该飞向哪个位置。

在每个更新中,我们检查 animIndex 是否已更改。如果是,并且它在我们的列表范围内,它将在鸟儿上调用 FlyToTarget。(我们使用 SendMessage,这不是触发另一个对象中函数的最佳实践方式,但鉴于提供的现有脚本,这是最不具破坏性的方法。)

额外的两个变量 collideWithObjectsbirdScalelb_Bird.cs 脚本中未使用,但需要在 Bluejay 上使用。

保存脚本。现在,在 Unity 中:

  1. BirdController 脚本拖动到 BirdController 对象上作为组件

  2. Bluejay 拖动到鸟类槽位

  3. 展开目标列表并将大小设置为 7

  4. Location0 拖动到元素 0,Location1 拖动到元素 1,依此类推

这里展示了带有 BirdController 组件的层次结构:

鸟类黑客技术

不幸的是,就像您在网上找到的很多代码一样,Living Birds 代码仅适用于其自身目的,但不一定适用于我们的目的。在这种情况下,该包是为生成一群随机飞翔和着陆的鸟而设计的,可以避免碰撞,甚至可以被杀死。我们只有一只鸟,并希望对着陆地点有更多的控制权,因此我们将使用我们的BirdController而不是包中的lb_BirdController

打开lb_Bird.cs文件(附加到Bluejay)并按以下方式修改:

controller的定义替换为我们的BirdController

// lb_BirdController controller; // removed
public BirdController controller; // added

注释掉或删除SetController函数:

// remove this
// void SetController(lb_BirdController cont){
//     controller = cont;
// }

保存它。在 Unity 中,将BirdController对象拖动到 Bluejay 的 LB_Bird Controller 槽中。

飞走吧!

现在,我们将 BirdController 添加到我们的时间轴中作为动画轨道。AnimIndex 参数是一个整数值,其值将在时间轴上逐步增加。我们希望 Bluejay 在大约 80 秒开始学习飞翔,每隔大约 10 秒从一个地点跳到另一个地点(80, 90, 100, 110, 120,并在 130 秒时飞走)。

  1. 打开BlackbirdDirector的时间轴编辑器窗口。

  2. BirdController对象从层级拖动到时间轴上,添加一个新的动画轨道。

  3. 按下它的红色录音按钮。

  4. 在层级中选择BirdController

  5. 将播放头移动到80,在检查器中设置动画索引为1

  6. 将播放头移动到90,并将动画索引设置为2

  7. 对其他索引36重复此操作。

  8. 再次按下红色录音按钮以停止录音。

  9. 预览曲线。如果它不在 0(80 秒之前)开始,请使用动画窗口中的编辑,并添加另一个值为0的关键帧。

这里显示了动画索引参数的动画轨道曲线,每个关键帧简单地递增一次:

图片

播放它。哇!鸟从一块石头飞到另一块石头,最终飞走了!

您可以通过移动位置对象和动画曲线关键帧来调整鸟的路径和着陆之间的时间。您还可以尝试动画化 BirdController 的 Bird Scale 参数,使鸟在学习飞翔的过程中变得越来越勇敢和强壮。这里提供了一个屏幕截图,展示了鸟在飞翔和树叶飘落:

图片

我们已经完成了一个故事。为了结束这个话题,让我们添加一点交互性,以便玩家可以控制故事何时开始播放。

制作交互式故事

到目前为止,我们使用了时间线从开始到结束驱动我们的整个 VR 故事体验。但事实上,时间线就像 Unity 中的其他可播放资产一样。例如,如果你选择 BlackbirdDirector 对象并在检查器中查看其 Playable Director,你会看到它有一个 Play On Awake 复选框,并且目前是勾选的。我们现在要做的不是在唤醒时播放,而是在用户事件上开始播放,即直接看几秒钟的小树。当故事结束时,它会自动重置。

看看如何播放

首先,我们将添加一个包围小树的 LookAtTarget,然后使用它来触发播放时间线:

  1. 选择 BlackbirdDirector 并取消勾选 Play On Awake 复选框

  2. 为了参考,将树游戏对象的缩放设置为它的起始关键帧缩放 (0.1, 0.1, 0.1)

  3. Hierarchy 中创建一个立方体(创建 | 3D 对象 | 立方体)并将其命名为 LookAtTarget

  4. 缩放并放置它以包围小树,缩放 (0.4, 0.5, 0.4),位置 (0, 0.3, 0)

  5. 禁用其 Mesh Renderer,但保留其 Box Collider

  6. 在立方体上创建一个新的 C# 脚本,命名为 LookAtToStart,并按照以下内容编写:

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

public class LookAtToStart : MonoBehaviour
{
    public PlayableDirector timeline;
    public float timeToSelect = 2f;
    private float countDown;

    void Start()
    {
        countDown = timeToSelect;
    }

    void Update()
    {
        // Do nothing if already playing
        if (timeline.state == PlayState.Playing)
            return;

        // Is user looking here?
        Transform camera = Camera.main.transform;
        Ray ray = new Ray(camera.position, camera.rotation * Vector3.forward);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit) && 
            (hit.collider.gameObject == gameObject))
        {
            if (countDown > 0f)
            {
                countDown -= Time.deltaTime;
            }
            else
            {
                // go!
                timeline.Play();
            }
        }
        else
        {
            // reset timer
            countDown = timeToSelect;
        }
    }
}

脚本与我们在第四章中编写的类似。我们使用主摄像机并确定它所看的方向。使用物理引擎,我们调用 Physics.Raycast 来在视图方向上发射一条射线并确定是否击中这个对象。如果是这样,我们开始或继续倒计时计时器并播放时间线。同时,如果你看向别处,我们重置计时器。

现在试试。时间线不会开始播放,直到你看了几秒钟的立方体。

重置初始场景设置

很可能你已经注意到了,不幸的是,默认的起始场景并不一定是时间线开始时的相同状态。你可以通过手动确保场景层次结构中的每个对象都具有时间线开始时的相同初始状态来修复这个问题。相反,我们将添加一个小技巧,让时间线播放短短的 0.1 秒来重置对象。

我们将使用协程来实现这一点。修改 LookAtToStart 脚本,添加一个新变量 resetSetup 并将其初始化为 true:

private bool resetSetup;

void Start()
{
    countDown = timeToSelect;
    resetSetup = true;
}

添加一个 PlayToSetup 函数,它将作为一个协程运行。协程是一种运行函数的方式,让 Unity 短暂地做其他事情,然后从上次停止的地方继续(通过 yield 语句)。在这里,我们开始播放时间线,离开 0.1 秒,然后告诉它停止播放:

IEnumerator PlayToSetup()
{
    timeline.Play();
    yield return new WaitForSeconds(0.1f);
    timeline.Stop();
}

当我们想要重置设置时,从 Update 中调用协程:

    void Update()
    {
        if (timeline.state == PlayState.Playing)
        {
            return;
        }
        if (resetSetup)
        {
            StartCoroutine("PlayToSetup");
            resetSetup = false;
        }

我们还希望时间线播放完毕后场景能够重置,因此我们一启动时间线就设置 resetSetup。它将在 timeline.state 不再播放时被识别:

        ...
            // go!
            timeline.Play();
            resetSetup = true;
        }

按下播放。看向树木。享受体验。当它结束时,你会重置到开始位置,可以再次看向树木以重新播放。

更多交互性想法

我们现在将停止开发。以下是一些改进交互性和用户体验的建议:

  • 在树周围添加粒子效果,以表明它是触发器

  • 当你看着树时,将其突出显示作为反馈

  • 显示倒计时光标,以指示计时器已开始,以及故事何时开始播放

这里有一些其他建议,你可以将这些可交互对象添加到故事中:

  • 看巢中的蛋会导致它比默认时间提前孵化

  • 当你看着闲置的鸟时,它会转身回看你

  • 如果你用你的手控制器戳鸟,它会跳开

  • 你可以捡起一块石头扔向鸟(nooo,只是开玩笑!)

摘要

在本章中,我们构建了一个动画 VR 故事。我们首先决定我们要做什么,规划时间表、音乐曲目、图形资产、动画序列和照明。我们导入了我们的资产并将它们放置在场景中,然后创建了一个时间轴,并使用激活轨道草拟了特定对象何时启用和禁用。接下来,我们为几个对象进行了动画处理,包括让树生长、让巢漂浮和让蛋震动。我们还对照明进行了动画处理,学习了如何动画化除了变换之外的游戏对象参数。

我们还使用了动画剪辑和动画控制器,使用从第三方包导入的动画。我们审查了一个调用动画器的脚本,并在其上编写了一个控制器,以使鸟从地点飞到地点。最后,我们通过基于注视的控制来启动和重播体验,添加了故事中的交互。

在下一章中,我们将探讨如何将多人网络添加到 Unity VR 项目中,以及如何将场景添加到新兴的元宇宙中。多人游戏对我们大多数人来说都很熟悉,但与虚拟现实结合时,它提供了一种其他任何技术都无法比拟的社会体验。我们将通过使用 Unity 网络功能来学习网络技术。

第十二章:社交 VR 元宇宙

那是我,Linojon,前面戴棒球帽的家伙! 重要的是,以下照片是在 2014 年 12 月 21 日元宇宙前夕,在一场现场 VRChat 会议期间捕捉到的。我建立了一个季节性的世界名为 GingerLand,并邀请我的聊天室朋友在每周聚会期间参观。然后,有人建议,“嘿,让我们拍一张集体照!”于是,我们都聚集在我的冬日小屋前门,说“Cheese!”其余的就是历史:

图片

对于许多人来说,在 VR 中与其他人实时社交互动的直观体验至少与使用 Facebook 浏览静态网站或分享 Snapchats 查看在线相册之间的差异一样戏剧性。这非常个人化和生动。如果你亲自尝试过,你就知道我是什么意思。我们现在将探讨如何使用 Unity 实现社交 VR 体验。有许多方法,从从头开始构建到连接到现有的社交 VR 平台。在本章中,我们将讨论以下主题:

  • 多玩家网络工作原理简介

  • 使用 Unity 网络引擎在 VR 中实现多玩家场景

  • 使用 Oculus 个性化化身

  • 构建和分享自定义 VRChat 房间

注意,本章中的项目是独立的,并不直接需要本书其他章节中的其他章节。如果你决定跳过其中任何部分或没有保存你的工作,那没问题。

多玩家网络

在我们开始任何实现之前,让我们先了解一下多玩家网络是什么,并定义一些术语。

网络服务

考虑一种情况,你正在运行一个 VR 应用程序,该应用程序通过互联网连接到其他玩家,这些玩家同时在自己的 VR 设备上运行相同的应用程序。当你移动你的第一人称视角在游戏中,射击东西,或以其他方式与虚拟环境互动时,你希望其他玩家也能看到。他们的游戏版本与你的保持同步,反之亦然。这是如何工作的?

运行中的游戏的一个实例充当主机或服务器。其他玩家同时连接到同一个实例。当你移动时,你的角色的新位置会与每个其他连接共享,然后它们在自己的视图中更新你的化身位置。同样,当你游戏接收到另一个角色的位置变化时,它会在你的视图中更新。越快越好。也就是说,发送和接收消息以及相应屏幕更新的延迟(延迟)越短,交互感觉就越真实,或越实时。

多玩家服务应该帮助你管理所有活跃客户端之间游戏状态的共享,新玩家和对象的生成,安全考虑,以及低级网络连接、协议和服务质量(如数据速率和性能)的管理。

网络被构建为一系列 API 层,其中低级函数处理数据传输的细节,并且对数据内容一无所知。中间和高级层提供越来越聚合的功能,也可能更直接地有助于网络应用程序。在我们的案例中,这是多玩家游戏和社交 VR。理想情况下,高级层将提供你将多玩家功能集成到游戏中的所有所需,同时通过干净的 API 提供对其他层的访问,以防你有特殊要求。

可用的多玩家服务有很多,包括 Exit Games 的 Photon 和来自 Google、Facebook/Oculus、Apple、Microsoft、Amazon 等平台的平台。

网络架构

网络的关键是客户端-服务器系统架构。我们在当今世界的各个方面都能看到这一点;你的网页浏览器是一个客户端,而网站托管在服务器上。你最喜欢的音乐收听应用是一个客户端,它的流媒体服务是服务器。同样,当你的游戏实例连接到网络时,它也是一个客户端。它与服务器通信,传递所有其他游戏客户端之间的状态和控制信息。

我说的是 服务器,但并不一定需要是一个位于某处的独立物理计算机。它可以是,但可能不是。最好将客户端和服务器视为 进程:程序或应用程序的实例在某处运行。一个 云服务器 是一个可以通过互联网作为服务访问的虚拟进程。

一个应用程序有时可以同时充当客户端和服务器。这种服务器和客户端合为一体的运行方式被称为作为主机运行。使用 Unity 网络,游戏可以作为客户端、服务器和/或作为主机运行。

即使如此,游戏实例之间进行通信需要公共IP互联网协议)地址。一个轻量级的中继服务器可以用最少的资源提供这项服务。

本地与服务器

在 Unity 中,你可以在游戏过程中使用脚本创建或实例化新对象。在多人游戏中,这些对象需要在本地以及网络上激活或生成,以便所有客户端都能了解它。一个生成系统管理所有客户端的对象。

区分本地玩家对象和网络对象非常重要。本地玩家对象由你在游戏版本中的操作控制,在你的客户端上,而不是远程控制。

例如,在第一人称体验中,你是摄像头,而其他玩家将你视为你的化身,你希望有安全预防措施;例如,防止他人黑客攻击游戏并更改你的化身。

本地玩家对象具有本地权限,也就是说玩家对象负责控制自身,例如其自身的移动。否则,当对象的创建、移动和销毁不由任何玩家控制时,权限应位于服务器上。当个别玩家在驱动游戏玩法时,需要本地权限。

另一方面,当游戏逻辑和随机事件驱动游戏玩法时,需要服务器权限。例如,当游戏在随机位置创建敌人时,你希望所有客户端都能获得相同的随机位置。当新玩家加入正在进行的游戏时,服务器帮助创建和设置当前游戏玩法中活跃的对象。你不想对象出现在默认位置,然后在与其他客户端同步时跳转到不同的当前位置。

以下来自 Unity 文档的图片显示了在网络中执行操作的方式。服务器对客户端进行远程过程调用(RPC)以生成或更新对象。客户端向服务器发送命令并影响动作,然后这些动作被传达给所有远程客户端:

图片

图片来源:docs.unity3d.com/Manual/class-NetworkBehaviour.html

实时网络是一个工程学科。分层网络架构旨在简化并保护你免受残酷的神秘细节。

所有这些都归结于性能、安全和可靠性。如果你需要在你的多人游戏中调试或优化这些方面中的任何一个,你可能需要站稳脚跟,更好地理解底层发生了什么。请参考下一章,第十三章,优化性能和舒适度,以获取建议。

Unity 网络系统

Unity 网络引擎(UNet)包括一套强大的高级组件脚本,使得将多人游戏功能添加到你的游戏中变得容易。其中一些更重要的组件包括网络身份网络行为网络变换网络管理器

Unity 网络概念和概念的概述可以在 Unity 网络概念文档中找到(docs.unity3d.com/Manual/UNetConcepts.html)。

每个可能在客户端被实例化(创建)的游戏对象预制件都需要网络身份组件。内部,它提供了一个通用的唯一资产 ID 和其他参数,以便对象可以在网络中被明确识别和实例化。

NetworkBehaviour类是从MonoBehaviour派生出来的,为脚本提供网络功能。我们将在本章的示例中使用它。详细信息请参阅docs.unity3d.com/Manual/class-NetworkBehaviour.html

当你想同步对象的运动和物理时,添加一个网络变换组件。它类似于更通用的SyncVar变量同步的快捷方式,并具有额外的智能插值,以在更新之间实现更平滑的运动。

网络管理器组件是将其全部粘合在一起的关键。它处理连接管理、跨网络的对象实例化以及配置。

当新的玩家对象被实例化时,你可以在网络管理器组件中指定一个实例化位置。或者,你可以在场景中添加游戏对象并给它们一个网络起始位置组件,该组件可以被实例化系统使用。

可以被实例化的非玩家对象也可以在网络管理器的实例列表中设置。此外,网络管理器组件处理场景变化并提供调试信息。

与网络管理器组件相关的是匹配功能,使用 Unity 云服务,可以配置以匹配玩家,使他们能够同时聚集并开始游戏——一个多人游戏大厅管理器,玩家可以在其中设置自己已准备好开始游戏,以及其他有用功能。

设置简单场景

让我们直接开始制作自己的多人演示项目。出于教学目的,我们将从一个非常简单的场景开始,其中包含标准的单人第一人称摄像头,并实现网络功能。然后,我们将通过网络同步多个玩家的头像。然后,我们将共享一个游戏对象,一个弹跳球,让玩家玩游戏。

创建场景环境

为了设置环境,我们将创建一个带有地面平面和立方体的新场景,并创建一个基本的单人第一人称角色。执行以下步骤:

  1. 通过导航到文件 | 新场景创建一个新的场景。然后,文件 | 另存为...并将场景命名为MultiPlayer

  2. 删除Main Camera并插入你的MeMyselfEye预制件的副本。重置其变换,使其位于原点。

  3. 通过导航到游戏对象 | 3D 对象 | 平面创建一个新的平面,将其重命名为GroundPlane,并使用变换组件的齿轮图标 | 重置来重置其变换。通过设置缩放为(10, 1, 10)来使平面更大。

  4. 使GroundPlane看起来更舒服。将你的Ground Material拖到平面上。如果你需要创建一个,导航到资产 | 创建 | 材质,命名为Ground Material,点击其 Albedo 颜色芯片,并选择一个中性颜色。

  5. 为了提供一些上下文和方向,我们只需添加一个立方体。导航到游戏对象 | 3D 对象 | 立方体,重置其变换,并将其位置设置为侧面,例如(-2, 0.75, 1)。

  6. 给立方体添加一些颜色。将你的Red Material拖到立方体上。如果你需要创建一个,导航到资产 | 创建 | 材质,命名为Red Material,点击其 Albedo 颜色芯片,并选择一个漂亮的红色,例如 RGB (240, 115, 115)。

创建头像头部

接下来,你需要一个头像来代表你自己和你的朋友。同样,我会保持这个非常简单,这样我们就可以专注于基础知识。现在先不考虑身体。只需制作一个带有脸的漂浮头部。这是我做的。你的效果可能会有所不同。只需确保它面向前方(正 Z 方向):

  1. 创建一个头像容器。导航到游戏对象 | 创建空对象,重命名为Avatar,重置其变换,并将其位置设置为眼睛水平,例如(0, 1.4, 0)。

  2. Avatar下创建一个球体作为头部(3D 对象 | 球体),重命名为Head,重置其变换,并将缩放设置为(0.5, 0.5, 0.5)。

  3. 给头部添加一些颜色。导航到资产 | 创建 | 材质,命名为Avatar Head Material,点击其 Albedo 颜色芯片,并选择一个漂亮的红色,例如 RGB (115, 115, 240)。将Avatar Head Material拖到Head上。

  4. 这家伙必须很酷(并且秃头)。我们将借用一副伊森的眼镜并戴在头上。导航到游戏对象 | 创建空对象,作为Avatar的子对象,重命名为Glasses,重置其变换,并将其位置设置为(0, -5.6, 0.1)和缩放(4, 4, 4)。

  5. 然后,当 Glasses 被选中时,转到 项目 选项卡,深入到 Assets/Standard Assets/Characters/ThirdPersonCharacter/Models 文件夹,展开 Ethan 预制体,找到 EthanGlasses.fbx 文件(网格文件),并将其拖入检查器面板. 请确保选择 EthanGlasses 的 fbx 版本,而不是预制体。

  6. 它有一个网格,但需要一个材质。当 Glasses 被选中时,转到项目面板,找到 Assets/Standard Assets/Characters/ThirdPersonCharacter/Materials/ 文件夹,找到   EthanWhite,并将其拖入检查器。

以下截图显示了 mine 的一个版本(其中也包括了嘴巴):

图片

当以多人游戏运行时,将为每个连接的玩家实例化一个头像实例。因此,我们必须首先将对象保存为预制体并将其从场景中删除,如下所示:

  1. 在层次结构中选择 Avatar 并将其拖入您的  项目 Assets/Prefabs 文件夹

  2. 再次从层次结构中选择 Avatar 并删除它

  3. 保存场景

好的,现在我们应该准备好添加多人网络。

添加多人网络

要使场景以多人游戏运行,我们至少需要一个网络管理器组件,并且需要使用网络身份组件识别任何将被实例化的对象。

网络管理器和 HUD

首先,我们将添加网络管理器组件,如下所示:

  1. 创建一个空的游戏对象并将其命名为 NetworkController

  2. 选择 添加组件 | 网络 | 网络管理器

  3. 选择 添加组件 | 网络 | 网络管理器 HUD

我们添加了一个网络控制器 HUD,它显示了一个 Unity 提供的简单默认菜单,在屏幕空间中,可以选择运行时网络选项(您可以在随后的图像中看到它)。它是用于开发的。在实际项目中,您可能会用更有趣的东西替换默认的 HUD。对于 VR,您希望在自己的世界中创建它。

网络身份和同步变换

接下来,向 Avatar 预制体添加一个网络身份。我们还将添加一个网络变换,指示网络系统将玩家的变换值同步到每个客户端上的头像实例,如下所示:

  1. 在项目资产中,选择 Avatar 预制体

  2. 导航到添加组件 | 网络 | 网络身份

  3. 确保已勾选“本地玩家权限”复选框

我们现在将告诉 Avatar 通过添加 Network Transform 组件与所有其他玩家通过网络同步其变换属性:

  1. 导航到添加组件 | 网络 | 网络变换

  2. 确保变换同步模式设置为同步变换

  3. 并且旋转轴设置为 XYZ(完整 3D)

网络变换组件被配置为与其他玩家实例的此对象的实际变换值共享,包括完整的 XYZ 旋转。

现在,告诉 网络管理器 我们的 Avatar 预制体代表玩家:

  1. 在层次结构中,选择 NetworkController

  2. 在检查器中,展开网络管理器出生信息参数,以便可以看到玩家预制件槽位

  3. Avatar 预制件从项目资源拖放到玩家预制件槽位

  4. 保存场景

作为主机运行

点击播放模式。如图所示,屏幕出现 HUD 启动菜单,允许你选择是否运行并连接此游戏:

图片

选择 LAN 主机(按键盘上的 H 键)。这将启动一个服务器(默认端口 7777localhost 上)并生成一个 Avatar。该头像位于默认位置(0, 0, 0)。此外,它没有连接到相机。因此,它更像是第三人称视角。如上所述,对于 VR,你最终可能希望修改此默认 HUD 以在 World Space 中运行。

下一步要做的是运行第二个游戏实例,并在场景中看到两个生成的头像。然而,我们不想让它们重叠,因为它们都位于原点,所以首先我们定义几个出生位置。

添加出生位置

要添加出生位置,你只需要一个具有网络起始位置组件的游戏对象:

  1. 导航到游戏对象 | 创建空对象,将其重命名为 Spawn1,并设置其位置为 (0, 1.4, 1)

  2. 导航到添加组件 | 网络 | 网络起始位置

  3. 复制对象(Ctrl-D),重命名为 Spawn2,并设置其位置为 (0, 1.4, -1)

  4. 在层次结构中,选择 NetworkController。在检查器中,网络管理器 | 出生信息 | 玩家出生方法,选择循环

现在我们有两个不同的出生位置。网络管理器将在新玩家加入游戏时选择其中一个或另一个。

运行两个游戏实例

在同一台机器(localhost)上运行两个游戏副本的合理方法是构建并运行一个实例作为独立的可执行文件,另一个实例从 Unity 编辑器(播放模式)运行。不幸的是,我们无法在 VR 中同时运行它们。(通常,你一次只能在 PC 上运行一个 VR 设备,并在该设备上运行一个 VR 应用程序)。因此,我们将构建一个不带 VR 的版本,使用非 VR 第一人称控制器,并启用 VR 运行编辑器版本。

将标准第一人称角色添加到场景中,如下所示:

  1. 如果你没有加载标准的 Characters 资产包,导航到 Assets | 导入包 | Characters 并选择导入

  2. 在项目 Assets /Standard Assets/Characters/FirstPersonCharacter/Prefabs/ 文件夹中找到 FPSController 并将其拖入场景

  3. 重置其 Transform,并使其朝向物体的前方。设置 Position 为眼睛水平,(0, 1.4, 0)

  4. 选择 FPSController,在检查器中,在第一人称控制器组件上,将行走速度设置为 1

  5. 禁用MeMyselfEye

还可以通过在玩家设置中修改 XR 设置来有所帮助,通过将名为 None 的 SDK 添加到列表的顶部。这将导致项目在没有 VR 硬件的情况下构建和运行,即使你忘记取消勾选虚拟现实支持复选框。

按照常规构建可执行文件。对于独立 Windows:

  1. 导航到“文件”|“构建设置…”

  2. 确保当前场景在“构建中的场景”中是唯一被勾选的。如果它不存在,点击“添加打开场景”。

  3. 打开玩家设置...

  4. 在“XR 设置”下,取消勾选“虚拟现实支持”复选框。

  5. 在“分辨率和显示”下,将“在后台运行”复选框勾选为真。

  6. 选择“构建和运行”,给它起个名字。随后,你可以通过双击构建后的文件来启动游戏。

启用“在后台运行”将允许在运行可执行文件时在每个窗口中输入用户控制(键盘和鼠标)。

要在 Unity 编辑器中运行游戏,我们需要反转一些设置:

  1. 在层次结构中,禁用FPSController并启用MeMyselfEye

  2. 在玩家设置中,勾选“虚拟现实支持”复选框,并将你的 SDK 移动到列表的顶部

在你的游戏窗口之一中,点击播放模式并选择 LAN 主机(H),就像我们之前做的那样。然后在另一个窗口中,选择 LAN 客户端(C)。在每个游戏中,你现在应该看到两个头像实例,每个玩家一个,如下面的截图所示:

图片

如果你想在另一台机器上运行游戏实例,将主机机的 IP 地址输入到客户端输入字段中(例如,我的 LAN 上的10.0.1.14),而不是localhost。如果每台机器都有自己的 VR 设备,它们可以分别运行相应的 MeMyselfEye prefab。

如果你在一台机器上运行多个项目实例,只需将 LAN 客户端地址设置为localhost。如果你想在你的网络上的其他机器(包括移动设备)上运行,注意 LAN 主机机的 IP 地址,并在客户端连接中输入该值(例如,我的 LAN 上的10.0.1.14)。甚至可以将此默认值添加到你的项目网络管理器组件的“网络信息 | 网络地址”参数中。

将 Avatar 与第一人称角色关联

如果头像不移动,那就没有多大意思。这是这个谜题的下一部分。

你可能会认为我们应该将头像对象放在玩家相机下(MeMyselfEyeFPSController),并将其保存为 Prefab,然后告诉网络管理器使用它进行生成。但那样的话,场景中会有多个相机和控制脚本监听用户输入,这并不好。

场景中必须只有一个活动的玩家。其他玩家的头像会被生成,但在这里不会被控制。换句话说,当本地玩家(只有本地玩家)被生成时,其头像应该成为相机的子对象。为了实现这一点,我们将编写一个脚本:

  1. 在项目资产中,选择Avatar,导航到添加组件 | 新脚本,并将其命名为AvatarMultiplayer

  2. 打开并编辑AvatarMultiplayer.cs脚本,如下所示:

using UnityEngine; 
using UnityEngine.Networking; 

public class AvatarMultiplayer : NetworkBehaviour 
{ 
  public override void OnStartLocalPlayer () 
  { 
    GameObject camera = Camera.main.gameObject; 
    transform.parent = camera.transform; 
    transform.localPosition = Vector3.zero; 
  } 
} 

您首先会注意到,我们需要包含using UnityEngine.Networking命名空间以访问网络 API。然后,AvatarMultiplayer类从NetworkBehaviour派生,而NetworkBehaviour内部是从MonoBehaviour派生的。

NetworkBehaviour提供了额外的回调函数。我们将使用OnStartLocalPlayer,该函数在本地玩家对象被实例化时被调用。然而,当远程玩家对象被实例化时,它不会被调用。其声明需要override关键字。

OnStartLocalPlayer正是我们想要的,因为只有当本地玩家被实例化时,我们才希望将其与摄像头关联。我们访问当前主摄像头对象并将其设置为角色的父对象(transform.parent = camera.transform)。我们还重置了角色的变换,使其位于摄像头的位置。

考虑改进脚本以指定您想要将角色作为父对象的实际游戏对象。

运行两个游戏实例:一个用于构建和运行,另一个使用游戏模式。在一个窗口中控制玩家,它将在另一个窗口中移动角色。哇!您甚至可以启动更多可执行文件并举办派对!

根据您的角色的大小和本地位置,其模型对象(如眼镜)可能从第一人称摄像头中可见并遮挡视线。您可以通过禁用子图形来隐藏它们。但这样,例如,您将看不到自己的影子(我喜欢)。另一个选项是将角色图形向后移动,以确保它们不会遮挡摄像头的视线。无论哪种方式,这都可以在AvatarMultiplayer脚本中完成。同样,如果您的游戏为每个角色提供了身体、椅子或其他东西,当前玩家的实例可能不需要或想要所有这些图形细节跟随他们。

添加匹配大厅

到目前为止,通过网络连接两个或更多玩家需要您知道正在运行的游戏主实例的 IP 地址,或者如果它们都在同一台机器上运行,则简单地使用localhost

Unity 网络和云服务包括一个内置的网络大厅管理器,用于在线玩家之间的匹配。它允许您创建和加入在线“房间”,玩家数量有限。使用大厅功能与在应用程序中的网络 HUD 中选择“启用匹配器”一样简单。但首先,您必须订阅 Unity 多人云服务(免费,基于您的 Unity 许可证,对并发用户数量有限制)。

要使用它,首先为您的应用程序启用 Unity Cloud Services:

  1. 在检查器上方,选择云图标(如下截图所示)以打开服务窗口

  2. 为此项目创建或选择一个 Unity 项目 ID。要创建 ID,请点击“选择组织”并选择您的组织,然后点击“创建”。

  3. 选择多人游戏以打开多人服务面板

  4. 从那里,打开基于 Web 的仪表板,你将被要求指定每间房的最大玩家数。输入4并按保存

这里显示了一个配置好的多人服务面板,云服务图标突出显示以供参考:

在你的项目中启用服务后,你可能需要重新构建可执行文件(文件 | 构建和运行),然后在游戏的第一个实例中:

  1. 从 HUD 菜单中选择启用匹配器(M)

  2. 输入你的房间名称

  3. 选择创建互联网匹配

在游戏的第二个实例中:

  1. 从 HUD 菜单中,还可以选择查找互联网匹配

  2. 你的房间应作为一个新按钮出现

  3. 选择你的房间的“加入”按钮

你现在可以在互联网上运行多人游戏,让 Unity 服务协商 IP 地址和每间房的最大连接数。

这将帮助你开始。当然,你对网络大厅匹配制作有完全的控制权,就像其他 Unity 网络服务一样。你很可能想制作自己的 GUI。有关文档,请参阅 NetworkManager API(docs.unity3d.com/ScriptReference/Networking.NetworkManager.html)。

可以从 Unity 的示例网络大厅免费资产开始(assetstore.unity.com/packages/essentials/network-lobby-41836)。不幸的是,这个资产已经过时并且有 bug,但你可以使它工作(阅读评论)。或者至少,在编写自己的 UI 时将其作为示例参考。此外,它是一个屏幕空间 UI;对于 VR,你需要修改它以成为世界空间画布。

在这个论坛评论中可以找到一个看起来最新的 HUD 代码示例:forum.unity.com/threads/networkmanagerhud-source.333482/#post-3308400

同步对象和属性

让我们开始玩球!在第八章,“与物理和火焰玩耍”中,我们实现了各种 VR 球类游戏。现在,我们有制作多人游戏的方法。我们将制作一个类似于Headshot的游戏,该游戏使用你的头部作为桨。但在这次练习之后,你可以自由地构建Paddle Ball和/或Shooter Ball游戏的多人版本,这些游戏使用手柄控制器来握住和移动桨来击打或反弹球。

此外,由于这里的目的是专注于多人网络考虑因素,我们将省略早期章节中的一些细节,例如音效、粒子效果和对象池。

设置头像球游戏

首先,我们将把立方体桨添加到 Avatar 头部,作为 Avatar 上的唯一 Collider:

  1. Avatar预制体的副本拖动到你的层次结构中进行编辑

  2. 对于它的每个子项(Head, Glasses),如果存在则禁用 Collider

  3. 创建一个名为CubePaddle的新立方体子对象(创建 | 3D 对象 | 立方体),并将其命名为CubePaddle

  4. 重置其变换并设置其缩放为(0.5, 0.5, 0.5

  5. 禁用立方体的 Mesh Renderer

  6. 将 Avatar 更改应用到其预制件上(在检查器中点击应用)

  7. 从层次结构中删除它

现在,我们将添加一个GameController对象和一个以固定间隔向角色提供球体的脚本:

  1. 在层次结构根处创建一个名为GameController的空游戏对象,并重置其变换

  2. 添加组件 | 新脚本并将其命名为BallServer

打开脚本并按照以下内容编写:

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

public class BallServer : MonoBehaviour
{
    public GameObject ballPrefab;
    public float startHeight = 10f;
    public float interval = 5f;
    public List<Color> colors = new List<Color>();

    [SerializeField] private int colorId;
    private Transform player;

    void Start()
    {
        colorId = Random.Range(0, colors.Count);
        player = Camera.main.transform;
        StartCoroutine("DropBall");
    }

    IEnumerator DropBall()
    {
        while (true)
        {
            Vector3 position = new Vector3(player.position.x, 
                                   startHeight, player.position.z);
            GameObject ball = Instantiate(ballPrefab, position, 
                                   Quaternion.identity);
            ball.GetComponent<Renderer>().material.color = 
                                                 colors[colorId];
            // (network spawn will go here)

            Destroy(ball, interval * 5);

            yield return new WaitForSeconds(interval);
        }
    }
}

在此脚本中,我们每 5 秒播放一个新的球。每个球在场景中保持 25 秒(interval * 5)。我们使用协程,通过yield return new WaitForSeconds(interval)在每个间隔中实例化一个新的球。

我们还创建了一个colors列表,并在游戏开始时为该玩家随机选择一个颜色。该玩家实例化的所有球都将为此颜色。创建一个可供选择的颜色列表:

  1. 在球服务器组件中,展开颜色参数

  2. 设置大小为4或更高

  3. 为每个元素的颜色槽定义独特的颜色

GameController 组件在检查器中的外观将类似于以下内容:

图片

创建一个弹跳球。我们将它命名为NetworkBall,因为在下一节中我们将在网络上共享它:

  1. 创建 3D 对象 | 球体,将其命名为NetworkBall,并缩放(0.5, 0.5, 0.5

  2. 在其球体碰撞器上,将Bouncy物理材质分配到材质槽

  3. 添加一个Rigidbody组件(添加组件 | 物理 | Rigidbody)

  4. NetworkBall拖动到你的项目资源预制件文件夹中创建一个预制件,并从层次结构中删除该对象

  5. NetworkBall预制件拖动到 GameController 的 BallServer 的球预制件槽中

按下播放。本地,你将从上方获得球,你可以用你的头将其反弹,就像我们在第八章,玩物理和火中所做的那样。

在网络上生成球

我们网络游戏中其他玩家需要看到与你相同的球。要实现这一点,需要几个步骤:

  • 首先,当我们本地实例化一个球时,我们需要告诉网络为所有玩家也生成它

  • 当球移动、弹跳或被击中时,其变换必须对所有玩家进行更新

  • 当球的寿命结束时,它必须对所有玩家进行销毁

在我们目前单玩家版本的游戏中,我们在BallServer脚本中实例化新的球。让我们使其网络化:

  1. 打开BallServer脚本进行编辑

  2. 在顶部添加using UnityEngine.Networking;命名空间

  3. 在创建实例后,添加对NetworkServer.Spawn(ball);的调用

然后,我们必须将NetworkBall预制件注册到NetworkManager中,以通知它预制件是可生成的:

  1. 在层次结构中选择NetworkController

  2. 在检查器中,展开 Spawn Info 参数

  3. 在注册的可生成预制件列表中点击+

  4. NetworkBall的副本拖动到可生成预制件游戏对象槽中

网络管理组件现在看起来如下:

还有另一件事我们还没有处理:销毁球实例。在独立版本中,我们调用Destroy(ball, interval*5)在给定时间后销毁球。对于网络生成的对象,您将调用Network.Destroy(ball)代替。然而,没有带有计时器参数的等效版本。您可以在 BallServer 中为其编写倒计时计时器,或者使用其他策略来确定其生命周期何时完成并且可以被销毁(例如,在球预制件本身上的 DestroySelf 脚本)。

同步球变换

Unity Networking 有一个组件可以在玩家之间共享此数据,即Network Transform,我们之前用它来同步 Avatar 头部。现在,我们将用它来同步球:

  1. 在项目资源中选择NetworkBall预制件。

  2. 添加组件 | 网络变换。

  3. 确保变换同步模式设置为同步 Rigidbody/3D。

  4. 添加网络变换将为您添加一个网络身份。勾选其本地玩家权限复选框。

NetworkBall的网络变换参数如下所示:

现在运行您项目的两个副本。哇! 当您在网络中将游戏连接起来时,玩家的球将对所有其他玩家可见(nsfw?保持干净,伙计们)。本地游戏中每个球的移动将控制其他所有游戏实例中的变换。

注意,Unity 正在提供优化,以限制数据量和更新的频率,同时确保每个玩家继续看到相同的内容。例如,在网络变换中,虽然您可以在每次更新时同步对象的变换位置和旋转,但您还可以指定移动和速度阈值,以指示何时需要同步。也许更重要的是,您可以选择要同步的内容。您不必同步变换值本身,可以同步 Rigidbody 物理(速度、角速度等)的变化,这些变化发生得较少,并让每个玩家的本地游戏计算相应的新变换。这就是我们为 NetworkBall 选择的选项。

状态变量同步

当我们在网络上生成对象时,它使用与网络管理器注册的预制件对象。因此,生成的球都具有默认颜色,而不是我们在 BallServer 中实例化对象时在本地设置的颜色。我们将利用这个机会来展示如何同步其他属性。

这个例子有点牵强,但让我们假设我们想要对象颜色成为一个状态变量。(您也可以添加其他变量,例如功率、健康、魔法等。)我们将编写一个脚本,告诉网络在值更改时同步网络上的属性。

编译器[SyncVar]属性标识了我们想要同步的属性,并设置了一个观察者。如果我们包含一个hook,那么当值发生变化时,观察者将调用该函数。

NetworkBall预制体上,创建一个新的脚本名为StateVariables,并编写如下:

using UnityEngine;
using UnityEngine.Networking;

public class StateVariables : NetworkBehaviour
{
    [SyncVar(hook = "OnColorChanged")]
    public Color color;

    public void SetColor(Color changedColor)
    {
        color = changedColor;
        GetComponent<Renderer>().material.color = color;
    }

    void OnColorChanged(Color networkColor)
    {
        GetComponent<Renderer>().material.color = networkColor;
    }
}

该类从NetworkBehaviour派生。我们使用SyncVar属性声明color。我们提供了一个公共设置函数SetColor,可以从其他游戏对象中正常调用。同样,当color变量发生变化时,它将通过网络同步。运行你的游戏的远程副本将调用OnColorChanged来更改该实例的对象。

现在,我们只需要修改BallServer,使用这个接口来设置颜色,而不是直接修改材质颜色。修改DropBall函数中的循环,使其如下所示:

IEnumerator DropBall()
{
    while (true)
    {
        Vector3 position = new Vector3(player.position.x, startHeight, player.position.z);
        GameObject ball = Instantiate(ballPrefab, position, Quaternion.identity);
        NetworkServer.Spawn(ball);
 ball.GetComponent<StateVariables>().SetColor( colors[colorId] );
        Destroy(ball, interval * 5);

        yield return new WaitForSeconds(interval);
    }
}

现在服务器不仅会在客户端生成球,还会发送其颜色属性设置。

下面是我们临时游戏场的实时双人 HeadShot 游戏截图:

使用这个基本模式,你可以扩展这个脚本以设置和同步表示单个对象状态(健康、力量等)或游戏本身(得分、轮到谁发球等)的其他变量。

高级网络主题

我们只是触及了你可以用网络做到的事情的表面。如果你对此感兴趣,我建议你仔细阅读 Unity 手册,并完成他们的教程。正如我们在本章开头提到的,一个好的开始是 Unity 网络概念文档(docs.unity3d.com/Manual/UNetConcepts.html)。

理解对等网络与客户端-服务器与专用服务器网络架构之间的区别非常重要。正如我们所见,默认情况下 Unity 网络是客户端-服务器,玩家作为主机服务器(玩家也是自己的客户端)。你还可以选择设置一个专用服务器,运行 Unity 作为独立玩家在无头模式下。

一些其他的网络主题和问题包括:

网络不是专门针对 VR 的主题,但如果你决定构建一个多人网络 VR 应用程序,你应该花时间了解客户端和服务器之间数据、消息和命令是如何交换的。VR 包括其独特的网络挑战。VR 的即时、沉浸式体验可能会放大延迟、同步和现实感的问题。我们将在下一章中讨论一些这些问题。

语音聊天的选项

当有两个人或更多人同时在同一个 VR 空间时,自然想要相互交谈。几乎所有的 VR 设备都配备了耳机和麦克风,因此硬件支持无处不在。

目前,Unity Networking 不支持语音聊天 (VoIP)。但还有其他解决方案:

使用 Oculus 平台和头像

在这一点上提及 Oculus 为其 VR 设备提供的丰富平台网络工具是值得的,也是有趣的。作为一个 Facebook 组织,Oculus 显然对使 VR 成为一个引人入胜的社会体验有着浓厚的兴趣。通过 Oculus 平台 SDK (developer.oculus.com/documentation/platform/latest/concepts/book-plat-sdk-intro/),每个用户都可以在 Oculus 游戏 和应用中创建和使用个性化的身份和头像,并找到并连接朋友,这一切都拥有可尊敬的安全性和认证度。

除了基本的 Unity 集成 SDK 之外,Oculus 开发生态系统还包括带有匹配功能的 Oculus Rooms、3D 球面声、语音聊天、唇同步以及他们集成的 Oculus Avatar 系统。

在 第三章,“VR 构建和运行”中,我们包括了一个关于 为 Oculus Rift 构建 的部分,您可能已经设置了场景以包含以下内容:

Oculus 平台权限检查

要使用 Oculus 平台和云服务,您的应用需要在 Oculus 上注册。

在开发者中心注册您的应用以获取 App ID,如下所示:

  1. 在您的浏览器中,访问 dashboard.oculus.com/

  2. 选择“创建新应用”并选择设备,GearVR 或 Oculus Rift

  3. 记下 App ID(复制到您的剪贴板),这是初始化平台 SDK 所必需的(如果您需要再次访问此页面,它位于“管理”|“您的组织”|“您的应用”|“入门 API”)

  4. 通过导航到“管理”|“您的组织”|“设置”|“测试用户”,创建一个测试用户并添加测试用户

现在,在 Unity 中,我们需要配置您的设置,以便它可以通过权限检查:

  1. 从主菜单选择“Oculus 平台”|“编辑设置”

  2. 将您的 App ID 粘贴到检查器中的相应槽位

  3. 在 Unity 编辑器设置下,勾选“使用独立平台”复选框,并输入之前通过“添加测试用户”生成的测试用户电子邮件和密码

设置“使用独立平台”将在 Unity 编辑器运行时绕过您的凭证权限检查在 Oculus 服务器上的检查。但否则,您需要将此代码添加到您的项目中,如下所示:

  1. 在您的层次结构中的对象上创建一个名为 OculusEntitlementCheck 的脚本,例如 GameController

  2. 按照以下方式编写(源自 Oculus 文档):

using UnityEngine;
using Oculus.Platform;

public class OculusEntitlementCheck : MonoBehaviour
{
    void Awake()
    {
        try
        {
            Core.AsyncInitialize();
            Entitlements.IsUserEntitledToApplication().OnComplete(EntitlementCallback);
        }
        catch (UnityException e)
        {
            Debug.LogError("Oculus Platform failed to initialize due to 
                                                     exception.");
            Debug.LogException(e);
            // Immediately quit the application
            UnityEngine.Application.Quit();
        }
    }

    void EntitlementCallback(Message msg)
    {
        if (msg.IsError)
        {
            Debug.LogError("Oculus entitlement check FAILED.");
            UnityEngine.Application.Quit();
        }
        else
        {
            Debug.Log("Oculus entitlement passed.");
        }
    }
}

添加本地头像

现在,我们将为本地玩家将 Oculus Avatar 添加到场景中。在项目Assets/OvrAvatar文件夹中有两个头像预制件:一个用于本地用户,在第一人称视图中可能只显示玩家的手,另一个用于远程玩家。请注意,Oculus 头像将在您按下播放之前不会出现在您的 Unity 场景窗口中,因为它们是程序生成的,并且(通常)需要连接到 Oculus 云服务器:

  1. 在“层次”中,找到并展开您的OVRCameraRig。注意它包含一个子项TrackingSpace

  2. 从“项目资源”OvrAvatar/Content/Prefabs/文件夹中,将LocalAvatar拖到层次中的TrackingSpace作为子项

  3. 在检查器中,勾选“使用控制器开始”复选框

  4. 勾选“显示第一人称”复选框

按下播放。现在您可以看到您的手和控制器。

添加远程头像

Avatar SDK 还使用 Oculus 云服务来获取特定玩家的头像设置和偏好。按照以下方式设置 Avatar SDK 的 App ID:

  1. 从主菜单中选择“Oculus 头像”|“编辑设置”

  2. 将您的 App ID 粘贴到检查器中相应的槽位

如果您对默认的“蓝色”头像感到满意,现在可能并不真正需要它,但我们在多人联网时将需要它。根据 Oculus 文档:

注意:在开发过程中,您可能可以忽略任何“无 Oculus Rift App ID”警告。虽然需要 App ID 才能为特定用户检索 Oculus 头像,但您可以使用默认的蓝色头像来原型设计和测试使用 Touch 和头像的体验。

要添加其他玩家的头像,我们将使用 Oculus RemoteAvatar 预制件。我们需要像之前为我们手工制作的预制件那样设置它以用于 Unity Networking,包括网络身份和网络变换。

  1. 在“项目资源”OvrAvatar/Content/Prefabs/文件夹中,选择RemoteAvatar预制件

  2. 选择“添加组件”|“网络”|“网络身份”

  3. 确保已勾选“本地玩家权限”复选框

  4. 选择“添加组件”|“网络”|“网络变换”

  5. 将“Transform 同步模式”设置为“同步变换”

  6. 将“旋转轴”设置为 XYZ(完整 3D)

  7. 在层次中选择Network Manager

  8. RemoteAvatar拖到网络管理器的玩家预制件槽中

我们还可以修改我们之前编写的AvatarMultiplayer脚本,该脚本将本地玩家的头像移动到玩家相机下。在这种情况下,我们并不真的想渲染远程头像,但我们确实想使其他玩家的变换值同步,因此我们将禁用渲染如下:

using UnityEngine;
using UnityEngine.Networking;

public class AvatarMultiplayer : NetworkBehaviour 
{
    public override void OnStartLocalPlayer()
    {
        GameObject camera = Camera.main.gameObject;
        transform.parent = camera.transform;
        transform.localPosition = Vector3.zero;

 GetComponent<OvrAvatar>().enabled = false;
    }
}

现在,当两个或更多玩家加入同一个房间时,玩家应该通过网络进行跟踪和同步。以下是 Oculus Avatar 在我们的场景中玩球的屏幕截图:

图片

构建和共享自定义 VRChat 房间

如果你的目标更简单,即构建一个虚拟现实世界并与他人共享作为共享社交体验,你可以使用许多现有的社交 VR 平台之一,这些平台提供基础设施并允许定制。在最好的平台中,VRChat 是唯一一个让你可以使用 Unity 创建自定义世界和个性化角色装备的平台。

VRChat 是使用 Unity 构建的,你可以使用 Unity 来创建自定义的世界和角色。如果你还没有尝试过,请从 Steam([store.steampowered.com/app/438100/VRChat/](http://store.steampowered.com/app/438100/VRChat/))下载客户端并尝试一下。

就本文所述,VRChat 需要旧的 Unity 5.6.3p1 版本。(在 https://unity3d.com/unity/qa/patch-releases/5.6.3p1 下载。)在尝试在旧版本的 Unity 中打开之前,将你的项目复制到一个新文件夹中。你可能会收到警告,但请继续。它们大多与脚本有关,而我们不会将脚本导出到 VRChat。

要为 VRChat 开发,你需要在他们网站上有一个账户(不同于你的 Steam 账户)。前往 www.vrchat.net/register 进行注册。

准备和构建世界

在我们开始之前,决定在 VRChat 中使用哪个场景。选择你想要的任何 Unity 场景。这可能是在本书中较早使用的 Diorama 游戏场,第九章 制作交互式空间 中的 PhotoGallery,或者其他任何东西。

  1. 打开你想要导出的 Unity 场景。

  2. 将副本保存为新名称,例如 VRChatRoom

www.vrchat.net/download/sdk 下载 VRChat SDK,并查看 docs.vrchat.com/ 上的最新说明:

  1. 导入 VRChat SDK 包。导航到 Assets | 导入包 | 自定义包...,找到你下载的 VRCSDK-*.package 复制件,点击打开,并选择导入

  2. 删除相机对象(Main CameraMeMyselfEye 或任何其他名称)

  3. Project Assets/VRCSDK/Prefabs/World/文件夹中,将VRCWorld` 预制件添加到场景中

生成点定义了玩家进入场景的位置。默认情况下,VRCWorld 充当生成点,因此你只需将此对象放置在场景中即可。或者,创建其他空游戏对象,将它们放置在你喜欢的地方,并将它们添加到 VRCWorld VRC_SceneDescriptor 组件中的生成列表中。

查看其他 VRC_SceneDescriptor 参数。解释可以在 docs.vrchat.com/docs/vrc_scenedescriptor 的文档中找到。这里显示了 VRC_SceneDescriptor 检查器:

图片

按照以下步骤继续准备你的 VRChat 场景:

  1. 通过 VRChat SDK | 设置登录你的 VRChat 账户

  2. 导航到 VRChat SDK | 显示构建控制面板并查看那里的选项。

  3. 如果存在,点击“设置层”按钮以添加 VRChat 所需的层

  4. 如果存在,点击“设置碰撞层矩阵”按钮

  5. 点击“启用 3D 空间化”按钮

当你准备好时,你可以测试你的世界:

  1. 点击“测试 | 新构建”按钮以开始构建新的测试世界

  2. VRChat 的本地版本将在一个窗口中打开

当你准备好在虚拟空间中发布世界时:

  1. 点击“发布 | 新构建”按钮

  2. 当提示时,在 Unity 的游戏窗口中输入名称、玩家容量、描述和其他请求的信息

  3. 世界将被上传到 VRChat

  4. 你可以通过 VRChat SDK | 管理上传内容来管理你的上传

你上传的世界将是私有的。你可以在 VRChat 中使用它并邀请他人加入你,但除此之外它不是公开的。要使你的上传内容公开,你必须向 support@vrchat.net 发送请求邮件。

VRC SDK 提供了一系列组件,你可以将其添加到你的场景中,包括基座、镜面反射、YouTube 视频,甚至战斗系统。为了使你的场景交互式,你可以向具有基本动作的对象添加自己的脚本,这些动作由世界中的事件触发,包括 OnSpawnOnPickupOnDropOnAvatarHit 等,仅举几例。

VRChat 是最初的社会虚拟现实平台之一,凭借强大的社区和持久性证明了自己的实力。它在边缘处略显粗糙,但作为一个独立项目,我们非常尊重并给予了很多赞誉!它是一个良好的稳定实现,由社区驱动,并欢迎用户贡献内容。

摘要

在本章中,我们学习了网络概念和架构,并使用了一些 Unity 自身多玩家网络系统的许多功能。我们构建了一个简单的场景和一个化身,考虑到意图是允许化身的头部运动与玩家的头戴式显示器同步。

然后,我们将场景转换为多玩家模式,添加了 Unity 网络组件,这简化了多玩家实现,只需几步点击即可完成。在证明我们可以使用化身构建共享的多玩家体验后,我们添加了一个玩家之间共享的弹跳球游戏对象,为构建多玩家网络游戏提供了基础。

接下来,我们快速浏览了 Oculus Avatar SDKs,用 Oculus 平台生态系统中完整的个性化化身替换了我们的球形化身。最后,我们展示了在 VRChat 中创建虚拟房间是多么容易,只需导出可以几乎立即分享的场景。

在下一章中,我们将深入了解优化你的 VR 项目以在 VR 中平稳舒适运行的细节。我们将考虑影响性能和延迟的不同区域,从模型多边形数量到 Unity 脚本,再到 CPU 和 GPU 处理器的瓶颈。

第十三章:优化性能和舒适度

正如我们在这些章节中提到的,你的 VR 应用程序的成功将受到用户感受到的任何不适的负面影响。一个事实是,VR 可以引起运动病。

运动病症状包括恶心、出汗、头痛,甚至呕吐。可能需要数小时,甚至一整夜的睡眠才能恢复。在现实生活中,人类容易患运动病:乘坐过山车、颠簸的飞机、摇晃的船只。当平衡感知系统的一部分认为你的身体在运动,而其他部分则没有这种感觉时,就会发生这种情况。

在 VR 中,这可能会发生在眼睛看到运动,但你的身体没有感觉到运动的时候。我们已经考虑了你可以设计你的 VR 应用程序来避免这种情况的方法。在移动时,始终让用户控制他们的第一人称移动。尽量避免乘坐轨道体验,尤其是避免自由落体。包括使用前景中的地平线或仪表盘,至少让玩家感觉到他们在一个驾驶舱中,如果不是在坚实的地面上。

反过来也是一样:当你身体感觉到运动,但你的眼睛没有看到运动时。即使是非常微妙的失调也可能产生不良影响。在 VR 中,一个主要的原因是延迟。如果你移动你的头,但你看到的视图没有跟上运动,这可能会导致恶心。

尽管这一章位于本书的末尾,但我们并不想建议将性能问题留到项目实施的最后。老话“先让它工作,再让它更快”并不一定适用于 VR 开发。你需要在整个开发过程中关注性能和舒适度,这是我们将在本章中讨论的主要主题:

  • 优化你的艺术作品和 3D 模型

  • 优化你的场景和照明

  • 优化你的代码

  • 使用着色器和设置优化渲染

分析和诊断性能问题的关键工具是内置的 Unity Profiler 和 Stats 窗口。我们将从对这些窗口的快速介绍开始。

使用 Unity Profiler 和 Stats

优化可能是一项大量工作,并且需要学习曲线来掌握它。好消息是,它可以逐步完成。先解决更明显、收益更大的问题。经过一些实验后,你可以用很少或没有视觉降级来完成很多事情。

Unity 编辑器包括两个内置工具来评估性能:Stats 窗口和 Profiler 窗口。

Stats 窗口

当你在 Unity 编辑器中按 Play 时,Stats 窗口会显示实时渲染统计信息。审查和理解这些统计信息是评估和改进你的应用程序性能的第一步,并可以帮助你决定首先解决哪些优化策略,包括本章中涵盖的策略。

在 Game 窗口中,通过按 Stats 按钮启用 Stats。这里显示了一个截图:

图片

显示的实际统计数据将根据您当前的构建目标而变化(见docs.unity3d.com/Manual/RenderingStatistics.html),包括:

  • 图形帧率(FPS)和每帧时间

  • 每帧的 CPU 时间

  • 三角形(Tris)/顶点(Verts)

  • 批次

在 VR 中,您需要密切关注每秒帧数。可接受的最小速率因目标设备而异,但通常对于桌面设备,您应该将其保持在 90 FPS 或以上,而 60 FPS(或 75 FPS)被认为是绝对最低。索尼 PlayStation VR 接受 60 FPS,但使用硬件自动将速率加倍至 120 FPS 以补偿。Windows Mixed Reality HMD 将根据您计算机上的图形处理器硬件调整帧率在 90 到 60 之间,允许配备较慢移动 GPU 的笔记本电脑运行 VR。基于手机的移动 VR 设备可以针对 60 FPS。

在编辑器播放模式下,FPS 不一定与您在设备上运行构建的可执行文件时的体验相同,因此它应该用作指示器,而不是实际值。但幸运的是,它不包括任何仅编辑器处理的操作,例如绘制场景视图。

通过检查每帧的 CPU 时间并与整体图形时间每帧进行比较,可以告诉您您的游戏是 CPU 受限还是 GPU 受限。也就是说,哪个过程是瓶颈,最慢地减慢了您的速度。CPU 用于物理计算、几何剔除和其他准备数据以供 GPU 渲染的操作。GPU 运行着色器并实际生成用于显示的像素值。了解您是 CPU 受限还是 GPU 受限可以帮助您决定在哪里集中优化努力以提高游戏性能。

三角形(Tris)和顶点(Verts)值显示了绘制的几何模型网格的大小。只有网格的可见面被计算,因此您的场景可能包含更多内容。也就是说,Stats 中的值是摄像机正在查看的几何形状,不包括任何视图外的顶点,以及任何遮挡表面被移除后的情况。当您移动摄像机或场景中的对象移动时,数字会变化。正如我们将在下一个主题中看到的,减少模型的顶点数可以显著提高性能。

批次(Batches)值是衡量您的 GPU 工作强度的指标。批次越多,GPU 每帧必须执行的渲染就越多。批次的数量,而不是批次的大小,是瓶颈。您可以通过减少场景中的几何形状来减少批次。由于少量(尽管较大)的批次比大量小批次更快,因此您可以告诉 Unity 通过将更多几何形状组合成更大的批次来优化图形,并通过 GPU 管道推送这些批次。

在分析和优化时,写下(或截图)统计数据并标记它们,可能是在电子表格中,以记录你的进度并衡量你尝试的每种技术的有效性。

性能分析器概述

Unity 性能分析器是一个性能监控工具,它报告了你在游戏中的各个区域(包括渲染和脚本)所花费的处理时间。它记录游戏过程中的统计数据,并在时间轴图中显示。点击可以让你深入查看详细信息。请参阅docs.unity3d.com/Manual/Profiler.html和以下截图:

图片

性能分析器将大量信息压缩到一个小空间中,因此你应该识别其各个部分,以便更好地理解你所看到的内容。窗口顶部的性能分析器控制工具栏允许你开启和关闭性能分析(记录)并浏览分析过的帧。性能分析跟踪中的白色垂直线是播放头,指示当前正在检查的帧。

深度分析按钮允许你深入查看更多细节,记录脚本中的所有函数调用。这有助于你确切地了解游戏代码中时间花费的位置。请注意,深度分析会带来大量的开销,并导致游戏运行非常缓慢。

在工具栏下方是性能分析跟踪。通过滚动“跟踪”面板可以显示更多内容。你可以使用“添加性能分析器选择列表”来添加和删除跟踪。

每个跟踪都包含与该处理类别相关的许多参数的统计数据。例如,CPU 使用率包括脚本和物理;渲染跟踪包括批量和三角形。可视图表允许你轻松检测异常。在故障排除时,寻找数据超过预期阈值的长段和尖峰。

你可以在 Unity 编辑器中分析运行的游戏,或者远程分析在单独的玩家中运行的游戏,例如移动设备。

优化你的艺术作品

影响性能最大的决策中,有些是你有意为之的创意决策。也许你想要超逼真的图形和高质量的音效,因为“它必须非常棒!”意识到你可能需要降低这些设置可能构成一个困难的设计妥协。然而,通过一点创新的“跳出思维定式”思考和实验,你可能会以(几乎)相同的外观效果实现更好的性能。在你的项目中,你最有控制权的是场景内容。

质量不仅仅是外观,还包括感觉。优化用户体验与任何基本的设计决策一样重要。

通常情况下,尽量减少模型网格中的顶点和面的数量。避免使用复杂的网格。移除那些永远不会被看到的面,例如固体物体内部的那些面。清理重复的顶点并移除双面。这很可能会在最初创建模型时使用的相同 3D 建模应用程序中完成。例如,Blender 就有这样的工具。此外,您还可以购买第三方工具来简化模型网格。

一定要检查 Unity 的 FBX 模型导入设置。例如,有选项可以压缩和优化您的网格。请参阅docs.unity3d.com/Manual/FBXImporter-Model.html

让我们来演示一下我们的意思。我们将设置一个具有高多边形计数的模型场景,复制该模型 1000 次,在分析器中检查它,并尝试一些优化技术。

设置场景

首先,我们需要一个高多边形模型。我们在 Turbosquid 上找到了一副太阳镜的模型,超过 5,800 个三角形,并且包括用于镜片的透明材料(www.turbosquid.com/3d-models/3ds-sunglasses-blender/764082)。请现在下载 FBX 文件。本书的文件中也包含了一个副本,以方便使用。我们将称这个文件为Sunglasses-original.fbx,以区别于我们将在途中修改的其他版本。

然后,进入 Unity,如下所示:

  1. 创建一个新的场景(文件 | 新场景),然后保存它(文件 | 保存场景为)并命名为“优化”

  2. 将模型导入到您的项目Assets Models文件夹中(资产 | 导入新资产)

  3. 创建一个参考地面(创建 | 3D 对象 | 平面),命名为“地面平面”,重置其变换,并创建或分配一个中性颜色的材料(例如我们的“地面材料”具有 Albedo #908070FF

  4. 创建一个立方体(创建 | 3D 对象 | 立方体),位置在(-1, 1, 1),并给它一个着色材料(例如我们的“红色材料”具有 Albedo #E52A2AFF

  5. 将主摄像机移动到位置(0, 0.5, -2

现在添加一副太阳镜的副本:

  1. Sunglasses-original模型的副本拖入场景

  2. 设置其位置(0, 1, 0),旋转(90, 180, 15),和缩放(10, 10, 10

作为基线,让我们看看它的统计数据和配置文件,并记录下数值:

  1. 在游戏窗口中,按下 Stats

  2. 此外,打开分析器窗口(窗口 | 分析器)

  3. 按下播放

游戏窗口具有以下场景和统计数据窗口,显示图形大约在 420 FPS,CPU 主频 2.4ms,22.6k 三角形:

图片

下一个窗口显示了相应的分析器窗口。您可以看到在渲染时间线中我移动了 HMD 的位置:

图片

这个场景太简单了,无法收集到多少有意义的统计数据。让我们在场景中创建 1000 个太阳镜的副本;按照以下步骤操作:

  1. 创建一个空的游戏对象,并将其命名为SunglassesReplicator

  2. 在其上创建一个新的 C#脚本,命名为SunglassesReplicator,并编写如下:

using UnityEngine;

public class SunglassesReplicator : MonoBehaviour
{
    public GameObject prefab;
    public Vector3Int dup = new Vector3Int(10, 10, 10);
    public Vector3 delta = new Vector3(2, 2, 2);

    void Start()
    {
        Vector3 position = transform.position;
        for (int ix = 0; ix < dup.x; ix++)
        {
            for (int iy = 0; iy < dup.y; iy++)
            {
                for (int iz = 0; iz < dup.z; iz++)
                {
                    position.x = transform.position.x + ix * delta.x;
                    position.y = transform.position.y + iy * delta.y;
                    position.z = transform.position.z + iz * delta.z;
                    GameObject glasses = Instantiate(prefab);
                    glasses.transform.position = position;
                }
            }
        }
    }
}

脚本接受一个prefab对象,并在 X、Y 和 Z 轴的每个轴上实例化dup次数(10),每个轴偏移delta单位(2),总共生成 1000 个预制件实例。

保存脚本,然后回到 Unity 中,设置并分配复制器参数如下:

  1. 创建你太阳镜的预制件。将Sunglasses-original从层次结构拖到你的项目Assets prefabs文件夹

  2. 再次在层次结构中选择SunglassesReplicator,并将预制件从项目资产拖到其预制件槽中

  3. SunglassesReplicator的位置设置为(-10, 1, 0)作为我们一叠太阳镜的起点

按下播放,生成的太阳镜博格在场景窗口中显示:

图片

现在的统计数据显示超过 3600 万个三角形,帧率低于 60 FPS。哎呀!相应的 Profiler 时间线如下所示:

图片

好的,现在我们有一个性能不佳的场景,让我们看看我们能做些什么。

减少模型数量

我们可以尝试简化导入到 Unity 中的模型。如果你在项目资产中选择SunGlasses-original对象,你可以看到它由两个网格组成:Frame网格有 4176 个三角形,Lens网格有 1664 个三角形。我们应该减少网格上的面数,或者减少模型数量。目前,我们将使用独立的免费和开源 Blender 应用程序(www.blender.org/)。

注意,从 Turbosquid 下载的此模型的原始 FBX 文件是 FBX 6 ASCII 格式,与 Blender 2.7+不兼容。本书提供的文件版本是使用 Autodesk FBX Converter 2013 转换的(usa.autodesk.com/adsk/servlet/pc/item?siteID=123112&id=22694909)。

按以下步骤在 Blender 中减少模型数量:

  1. 打开 Blender,删除所有内容以清除默认场景(键盘 A | 再次 A | X | 删除)

  2. 导入原始太阳镜 fbx 文件(文件 | 导入 | FBX)

  3. 选择太阳镜的框架模型网格(右键点击)

  4. 在右侧,选择修改工具(扳手图标)

  5. 选择添加修改器 | 减少模型数量

  6. 将比例设置为0.1,如图所示:

图片

  1. 然后,按下应用

  2. 选择太阳镜的镜片模型网格(右键点击)

  3. 也将其减少到比例0.1并应用

  4. 删除相机、灯光和背景对象(用鼠标选择,键盘 X 删除)

  5. 以 FBX 格式导出(文件 | 导出 | FBX)并给它一个新的名称,例如SunGlasses-decimated.fbx

现在回到 Unity 中,导入模型并在我们的复制器中使用它如下:

  1. 将新的SunGlasses-decimated.fbx文件导入到你的Models文件夹(资产 | 导入新资产)

  2. 将此模型 SunGlasses-decimated 的副本拖入场景

  3. 复制/粘贴原始对象的变换(使用原始对象的变换复制组件,并在减面版本上粘贴组件值)

  4. 将其保存为预制件(将 SunGlasses-decimated 从层次结构拖到项目 Assets Prefabs 文件夹中)

  5. SunglassesReplicator 中设置此材质(将项目资源中的预制件拖到复制器的预制件槽中)

按下播放键,正如预期的那样,我们现在运行大约 3.4M 个三角形,大约是之前的 10%,并且我们提高了 FPS,始终超过 60 FPS。更好,但还不够好。

透明材质

图形处理和帧率的另一个杀手是使用透明度和其他需要渲染每个像素多次的渲染技术。为了使太阳镜镜头看起来透明,Unity 将首先渲染其后面的实体对象,然后渲染半透明镜头像素,从而有效地合并像素值。可能有一打镜头堆叠在一起,这会导致相当多的处理工作。

让我们看看当我们用不透明材质替换透明镜头材质会发生什么:

  1. 在你的项目 Assets Materials 文件夹中,创建一个新的材质并将其命名为 Lens_Opaque

  2. 对于其 Albedo 颜色,选择一个不透明的灰色,例如 #333333FF

  3. Sunglasses-source 的副本拖到层次结构中并重命名为 Sunglasses-opaque

  4. 展开它并选择 Lens 子对象

  5. Lens_Opaque 材质拖到镜头上

  6. 选择 Sunglasses-opaque 并将其拖入 Prefabs 文件夹,创建一个新的预制件

  7. 在选择 SunglassesReplicator 后,将 Sunglasses-opaque 预制件拖到其预制件槽中

当你按下播放键时,我们现在有 1000 副不透明太阳镜,并且我们得到了更好的帧率,大约 80 FPS。

如果我们将这两种技术结合起来会怎样?让我们在减面后的镜头上使用不透明材质。就像我们刚才做的那样,创建另一个预制件版本,命名为 Sunglasses-decimated-opaque,如下所示:

  1. Sunglasses-decimated 的副本拖到层次结构中并重命名为 Sunglasses-decimated-opaque

  2. 展开它并选择 Lens 子对象

  3. Lens_Opaque 材质拖到镜头上

  4. 选择 Sunglasses-decimated-opaque 并将其拖入 Prefabs 文件夹,创建一个新的预制件,具有不透明镜头

  5. 在选择 SunglassesReplicator 后,将 Sunglasses-decimated-opaque 预制件拖到其预制件槽中

按下播放键,我们在 Profiler 时间轴上始终得到大约 100 FPS

图片

太棒了!我们得到了想要的帧率。但是……这也不是我们想要的外观。我们有不透明的镜头,但我们期望的是半透明的。而且,令人失望的是,眼镜的低多边形版本看起来……就是低多边形。这根本不能接受。也许有一个折衷方案。

详细程度

检查我们的场景,我们发现高多边形太阳镜实际上只需要在你最近的地方。随着它们退到更远的地方,低多边形版本就足够了。同样,镜片上的透明度实际上主要只需要在你附近的地方。远处的太阳镜和被其他眼镜遮挡的太阳镜实际上不需要透明度。Unity 理解这一点,并提供了一个组件来自动管理细节级别,称为 LOD 组(见 docs.unity3d.com/Manual/LevelOfDetail.html)。

现在让我们使用它。我们将创建一组太阳镜,每个版本都有不同的细节级别:

  1. 在层次结构中,创建一个空的游戏对象,命名为 SunglassesLOD,并重置其变换

  2. Sunglasses-original 预制体的副本作为 SunglassesLOD 的子对象拖动

  3. Sunglasses-decimated 预制体的副本也作为子对象拖动

  4. 还拖动一个 Sunglasses-decimated-opaque 的副本

  5. 选择父对象 Sunglasses 并添加组件 | LOD 组

查看检查器中的 LOD 组组件。注意它有几个基于相机距离使用每个模型的范围,标记为 LOD0、LOD1 和 LOD2。范围是对象边界框高度相对于屏幕高度的百分比。当最近时,LOD0 对象是活动的。更远的地方,这些将停用,而 LOD1 对象将活动,依此类推。

现在让我们分配 LOD 组:

  1. 选择 LOD0

  2. Sunglasses-original 游戏对象从层次结构拖动到添加按钮

  3. 选择 LOD1

  4. Sunglasses-decimated 游戏对象拖动到添加按钮

  5. 选择 LOD2

  6. Sunglasses-decimated-opaque 对象也拖动到添加

这里显示了检查器的屏幕截图:

图片

注意 LODn 组的顶部边缘有一个小相机图标。你可以选择并滑动它来预览基于相机距离的 LOD 激活。你还可以通过滑动每个区域框的边缘来配置每个 LOD 的活动范围(百分比)。

现在,让我们在我们的场景中尝试它:

  1. SunglassesLOD 对象拖动到你的项目预制体文件夹

  2. 在层次结构中选择 SunglassesReplicator 并将 SunglassesLOD 预制体拖动到其预制体槽

按下播放。接下来显示的是 Profiler 时间线。基本上与我们的最优化版本没有区别,但当我们需要时,我们得到了高多边形模型和透明镜片:

图片

接下来是使用 SunglassesLOD 的游戏视图的屏幕截图。离我们最近的是高多边形眼镜。中间的是低多边形,但带有透明镜片。更远的地方是低多边形和不透明的模型版本:

图片

Unity Asset Store 中提供了许多 LOD 工具,可以帮助管理细节级别,甚至可以从你的模型中生成降级的网格。Unity 本身也在尝试这样的工具,AutoLOD,它可以在 GitHub 上免费获得(blogs.unity3d.com/2018/01/12/unity-labs-autolod-experimenting-with-automatic-performance-improvements/)。

使用静态对象优化场景

除了你的艺术对象之外,优化的下一步可能就是你的场景本身是如何组织的。如果我们告诉 Unity 某些对象在场景中不会移动,它可以在运行前预先计算大量的渲染,而不是在运行时。我们通过将这些游戏对象定义为静态,然后将其烘焙到特定的上下文中来实现这一点。

我们在第四章“基于注视的控制”中使用了静态对象,当我们为 Ethan 设置Navmesh时。他的可通行nav区域由平坦的地面平面减去可能挡路的任何大型静态对象定义,并烘焙到navmesh中。

静态对象也可以用来帮助预先计算场景渲染。烘焙的光照图和阴影图预先计算光照和阴影。烘焙的遮挡将场景划分为静态体积,当它们在视图中不可见时可以轻松剔除,从而通过可能一次消除许多对象来节省处理时间。让我们尝试一些示例。

设置场景

为了演示静态游戏对象的使用,我们不能使用由SunglassesReplicator动态实例化的太阳镜。但既然我们有这个脚本,现在我们就利用它:

  1. 在层次结构中选择SunglassesReplicator,并将Sunglasses-original预制体从项目资产拖动到其预制体槽中

  2. 按下播放

  3. 在播放时,在层次结构中选择所有Sunglasses-original(Clone)对象(有 1000 个!)。右键单击并选择复制

  4. 停止播放模式

  5. 在层次结构中创建一个空的游戏对象,并将其命名为SunglassesBorg

  6. 将复制的太阳镜作为SunglassesBorg的子对象

  7. 禁用SunglassesReplicator对象,因为我们不再想使用它

如果你需要多次这样做,你可以编写一个编辑器脚本。例如,你可能需要在编辑器的主菜单栏中添加一个 BorgMaker 菜单选项。它可以通过一个对话框提示你输入预制体对象、复制次数和偏移参数,就像我们的SunglassesReplicator一样。编写自定义和扩展 Unity 编辑器的脚本是一种常见做法。如果你感兴趣,请参阅手册:扩展编辑器(docs.unity3d.com/Manual/ExtendingTheEditor.html)和编辑器脚本入门教程(unity3d.com/learn/tutorials/topics/scripting/editor-scripting-intro)。

灯光和烘焙

场景中灯光的使用会影响帧率。你可以在灯光数量、灯光类型、位置和设置方面拥有很大的控制权。请参阅 Unity 手册中的相关信息,手册可以在docs.unity3d.com/Manual/LightPerformance.html找到。

尽可能使用烘焙光照贴图,这会将光照效果预先计算到单独的图像中,而不是在运行时。谨慎使用实时阴影。当一个物体投射阴影时,会生成一个阴影贴图,该贴图将用于渲染可能接收到阴影的其他物体。阴影具有很高的渲染开销,通常需要高端 GPU 硬件。

让我们看看使用烘焙光照贴图对我们场景的影响:

  1. 选择SunglassesBorg并在检查器右上角点击其静态复选框

  2. 当提示时,回答是,更改子项

如果你收到错误,“网格没有适合光照贴图的 UVs”,请在你项目的窗口中选择导入的 fbx 模型,选择生成光照贴图 UVs,并应用。

根据你的光照设置,光照贴图可能会立即开始生成。以下是如何审查和修改光照贴图设置:

  1. 打开光照窗口(窗口 | 光照)

  2. 如果勾选了自动生成,那么每次场景变化时都会开始生成光照贴图

  3. 或者,取消选中它并点击生成光照来手动构建

这是游戏窗口运行时的屏幕截图,使用了 1000 个高多边形透明太阳镜。我们现在得到了 90 FPS,当我移动非静态的红色立方体时,它仍然以透明度和阴影的形式渲染:

在光照窗口中,还有实时光照(默认启用)、烘焙全局光照(默认启用)、光照映射子系统(默认为 Enlighten)和雾效果(默认禁用)的设置,所有这些都会影响场景的质量和性能。

在处理光照时,这里有一些额外的提示:

  • 避免使用动态阴影,只需使用投影仪(见docs.unity3d.com/Manual/class-Projector.html)在移动物体下方创建一个“模糊的块”

  • 检查你项目的质量设置(编辑 | 项目设置 | 质量)。使用较少的像素灯光(在移动设备上,限制为 1 或 2)。在硬阴影和软阴影上使用高分辨率。

  • 你可以拥有任意数量的烘焙灯光。烘焙光照产生高质量的结果,而实时阴影可能会显得块状。

  • 在烘焙时,可以通过增加烘焙分辨率(40-100 像素的分辨率是合理的)来提高光照贴图的质量。

  • 使用烘焙光照与光照探针照亮动态物体。

  • 使用反射探针照亮反射表面。这些可以是静态的(烘焙)或动态的(实时)。

光探针(实时或烘焙)和着色器(以及着色器选项)的选择可以使你的场景看起来非常惊艳。然而,它们也可能对性能产生重大影响。平衡美学和图形性能是一门艺术和科学。

遮挡剔除

正如我们所见,你尝试减少需要渲染的对象数量越多,效果越好。无论你使用的是高多边形还是低多边形模型,Unity 都需要确定哪些面在视图中。当有很多对象时,我们可能可以通过提供一些线索来帮助 Unity。

遮挡剔除在对象被其他对象遮挡而看不到时禁用渲染。请参阅docs.unity3d.com/Manual/OcclusionCulling.html。它会检查你的场景,并使用每个对象的边界框(范围),将世界空间划分为一系列的立方体。当 Unity 需要确定一个对象是否在视图中时,它会丢弃任何显然在视图之外的剔除框中的对象,并继续通过层次结构。

为了演示,我们将复制几个 SunglassesBorg 的副本并设置遮挡剔除:

  1. 在层级中选择 SunglassesBorg,右键点击,然后重复三次复制

  2. 对于第一个副本,将 Y 旋转设置为 90,然后将其移动到位置 X = 20

  3. 对于第二个副本,将 Y 旋转设置为 -90 并将其移动到位置 X = -20

  4. 对于第三个副本,将 Y 旋转设置为 180 并将其位置 Z = -20

当我按下播放时,由于有这么多对象,我们的帧率下降到大约 50 FPS。

现在,通过以下更改,我们可以解决我们的性能问题:

  1. 所有四个 Borgs 已经设置为静态,但请确认静态:遮挡者和被遮挡者都被勾选了(检查静态下拉列表)

  2. 打开遮挡剔除窗口(窗口 | 遮挡剔除)

  3. 点击 烘焙

注意,我们本来可以,但并没有在我们的场景中区分遮挡者和被遮挡者。被遮挡者是那些被遮挡的对象。遮挡者是那些可能位于前面,遮挡其他对象的对象。不遮挡的半透明对象应标记为被遮挡者,而不是遮挡者。

这可能需要一些时间。这里是一个从上到下的场景视图,显示了生成的剔除体积:

现在我按下播放,性能又回到了 90 FPS(多多少少,取决于你从哪里看,作为场景中的用户)。

另一种减少场景中细节的方法是使用基于距离的全局雾,超出雾限制的对象将不会绘制。

优化你的代码

另一个容易产生性能问题且适合优化的领域是你的脚本代码。在这本书中,我们使用了各种编码最佳实践,但并不一定解释了原因。(另一方面,本书中的一些示例可能并不一定高效,为了简单和解释。)例如,在第八章“玩转物理与火焰”中,我们实现了一个对象池内存管理器,以避免反复实例化和销毁游戏对象,这会导致内存垃圾回收GC)问题,进而减慢你的应用程序速度。

通常,尽量减少重复大量计算代码。尽量预先计算尽可能多的工作,并将部分结果存储在变量中。

在某个时候,你可能需要使用分析工具来查看你的代码在底层是如何运行的。如果分析器显示你编写的脚本中花费了大量时间,你应该考虑另一种重构代码的方法,使其更高效。这通常与内存管理有关,但也可能是数学或物理问题。(参见docs.unity3d.com/Manual/MobileOptimizationPracticalScriptingOptimizations.html。)

请遵循编码最佳实践,但避免过度优化。人们常犯的一个错误是,在不需要优化的代码区域投入过多精力,在这个过程中牺牲了可读性和可维护性。使用分析器来分析性能瓶颈所在,并首先集中精力优化这些区域。

理解 Unity 的生命周期

就像所有视频游戏引擎一样,当你的游戏运行时,Unity 正在执行一个巨大的循环,每帧更新时都会重复。Unity 提供了许多钩子,可以在游戏循环的每个步骤中接入事件。下面是生命周期流程图,摘自 Unity 手册页面“事件函数执行顺序”(docs.unity3d.com/Manual/ExecutionOrder.html)。你最熟悉的两个事件函数StartUpdate用红色箭头突出显示。绿色圆点突出显示了我们将在这次对话中引用的许多其他事件:

图片

从图表的顶部开始,当游戏开始时,每个从 MonoBehaviour 类派生的 GameObject 的组件将通过调用 Awake 来唤醒。除非你需要使用 AwakeOnEnable,我们通常在 Start 中初始化对象。跳到游戏逻辑部分,Update 在每一帧迭代时被调用。注意循环线/箭头。(物理引擎有自己的循环计时,用于处理刚体,可能比帧更新更频繁。你可以通过 FixedUpdate 来挂钩它。)OnDestroy 事件被调用以退役对象。

对于当前的讨论,重要的是要注意哪些事件在游戏循环内,哪些在游戏循环外。

编写高效的代码

我们希望将所有代码(如 FixedUpdateUpdateLateUpdate)保持在游戏循环中尽可能精简。将任何初始化移动到 AwakeOnEnableStart。我们还希望在初始化函数中预先计算并缓存任何计算密集型工作。

例如,调用 GetComponent 是昂贵的。正如我们在本书的许多脚本中看到的,在 Start 中获取 Update 需要的任何组件的引用,而不是在游戏逻辑循环中,是一种最佳实践。以下代码,用于 第七章,《运动与舒适》,在 Start 中获取 CharacterController 组件,将其缓存到变量中,然后在 Update 中引用它,而不是每帧调用 GetComponent

public class GlideLocomotion : MonoBehaviour 
{
    private Camera camera;
    private CharacterController controller;

    void Start ()
    {
        camera = Camera.main;
        character = GetComponent<CharacterController>();
    }

    void Update()
    {
        character.SimpleMove(camera.transform.forward * 0.4f);
    }
}

任何你在脚本中声明 Update() 函数(或任何其他事件函数)的时候,Unity 都会调用它,即使它是空的。因此,你应该删除任何未使用的 Updates,即使它们在创建新的 C# MonoBehaviour 脚本时的默认模板中。

同样,如果你在 Update 中的代码不需要每帧调用,当它们不需要时,使用状态变量(以及一个 if 语句)关闭计算,例如:

    public bool isWalking;

    void Update()
    {
        if (isWalking)
        {
            character.SimpleMove(camera.transform.forward * 0.4f);
        }
    }

避免昂贵的 API 调用

除了将昂贵的 API 调用从 Update 移动到初始化函数之外,还有一些 API 如果可能的话应该完全避免。以下是一些例子。

避免使用 Object.Find()。为了获取场景中游戏对象的引用,不要调用 Find。不仅 Find 按名称搜索是昂贵的,因为它必须搜索 Hierarchy 树,而且如果重命名了它正在寻找的对象,它可能是不稳定的(可能会出错)。如果你可以,定义一个 public 变量来引用对象,并在编辑器检查器中将其关联。如果你必须在运行时找到对象,请使用标签或可能使用层来限制搜索到一个已知的固定候选集。

避免使用 SendMessage()SendMessage 的传统用法是计算密集型的(因为它使用了运行时 反射)。要触发另一个对象中的函数,请使用 Unity 事件代替。

避免内存碎片化和垃圾回收。数据和对象的临时分配可能会导致内存碎片化。Unity 会定期遍历内存堆以合并空闲块,但这很昂贵,可能会导致应用中的帧跳过。

想要更多建议和深入讨论,请参阅 Unity 最佳实践指南,Unity 中的优化理解 (docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity.html)。

另一个优化领域是 Unity 物理。在前面的章节中,我们简要提到了使用层进行射线投射,以限制 Unity 需要搜索的对象,例如,在 VR 中进行基于注视的选择。同样,可以通过定义层碰撞矩阵将物理碰撞检测限制在特定层上的对象。请参阅优化物理性能的手册页面(docs.unity3d.com/Manual/iphone-Optimizing-Physics.html)和物理最佳实践教程(unity3d.com/learn/tutorials/topics/physics/physics-best-practices)。

优化渲染

有一些重要的性能考虑因素是针对 Unity 如何进行渲染的特定问题。其中一些可能对任何图形引擎都适用。一些建议可能会随着 Unity 新版本的推出、技术的进步和算法的替换而改变。

有许多文章提供了关于如何设置以优化 VR 应用的推荐,而且一个人的建议与另一个人的建议相矛盾并不罕见。这里有一些好的建议:

  • 使用前向渲染路径。这是图形设置中的默认设置。

  • 使用4X MSAA(多采样抗锯齿)。这是一种低成本抗锯齿技术,有助于在质量设置中去除锯齿边缘和闪烁效果。

  • 使用单次遍历立体渲染。在玩家设置中,它可以在单次遍历中高效地为每只眼睛渲染视差透视。

  • 在玩家设置中启用静态批处理和动态批处理。这些将在后面讨论。

注意,一些渲染设置是设备或平台特定的,可以在玩家设置中找到(编辑 | 项目设置 | 玩家)。其他一些已经被 Unity 抽象为项目质量设置(编辑 | 项目设置 | 质量)。还有一些在图形设置中(编辑 | 项目设置 | 图形)。

Unity 中的短语玩家设置并不指用户(玩家)或第一人称角色(玩家装置)。相反,它指的是播放你应用的平台可执行文件。更像是媒体播放器,例如播放 mp4 的视频播放器,Unity 的玩家在编译后运行你的游戏。玩家设置配置生成的可执行文件。

生活就是批量处理

也许,Unity 中最大的性价比特性是能够将不同的网格组合成一个批处理的功能,然后一次性将它们推送到图形硬件。这比分别发送网格要快得多。实际上,网格首先被编译成 OpenGL 顶点缓冲对象,或 VBO,但这只是渲染管道的低级细节。

每个批处理占用一个绘制调用。在场景中减少绘制调用的数量比实际的顶点数或三角形数更重要。例如,对于移动 VR,保持大约 50(最多 100)个绘制调用。

有两种批处理类型,静态批处理动态批处理,这些功能在玩家设置中启用。

对于静态批处理,只需在 Unity 检查器中为场景中的每个对象勾选“静态”复选框即可标记对象为静态。将对象标记为静态告诉 Unity 该对象永远不会移动、动画化或缩放。Unity 将自动将共享相同材质的网格批处理成一个单一的、大的网格。

这里的限制是网格必须共享相同的材质设置:相同的纹理、着色器、着色器参数和材质指针对象。这怎么可能呢?它们是不同的对象!这可以通过将多个纹理组合成一个单独的宏纹理文件或 TextureAtlas 来实现,然后对尽可能多的模型进行 UV 贴图。这就像用于 2D 和网页图形的精灵图像。有一些第三方工具可以帮助您构建这些。

检查场景中资源的实用分析工具是 Unity 资源检查器,您可以在以下链接找到:github.com/handcircus/Unity-Resource-Checker

动态批处理与静态批处理类似。对于未标记为静态的对象,Unity 仍然会尝试批处理它们,但这是一个较慢的过程,因为它需要逐帧考虑(CPU 成本)。共享材质的要求仍然存在,以及其他限制,如顶点计数(少于 300 个顶点)和均匀的变换缩放规则。(参见docs.unity3d.com/Manual/DrawCallBatching.html。)

在脚本中管理纹理时,使用Renderer.sharedMaterial而不是Renderer.material来避免创建重复的材料。接收重复材料的对象将退出批处理。

目前,只有网格渲染器和粒子系统被批处理。这意味着蒙皮网格、布料、尾迹渲染器和其他类型的渲染组件不被批处理。

多通道像素填充

在渲染管线中,有时将像素填充率作为一个关注点。如果你这么想,渲染的最终目标是在显示设备上的每个像素中填充正确的颜色值。如果必须多次绘制任何像素,那将更加昂贵。例如,要注意透明粒子效果,如烟雾,它们与许多像素接触,大部分是透明的四边形。

对于 VR,Unity 将图像绘制到一个比物理显示尺寸更大的帧缓冲区中,然后进行后处理以校正视觉畸变(桶形效应)和色差校正(颜色分离),最后才被投射到 HMD 显示器上。实际上,在后期处理之前可能还有多个叠加缓冲区。

这种多次遍历像素填充是某些高级渲染器的工作方式,包括光照和材料效果,如多个光源、动态阴影和透明度(透明和淡入渲染模式)——Unity 标准着色器也是如此。基本上,所有的好东西!

需要多次遍历像素填充的材料 VBO 批次会被多次提交,从而增加了总的绘制调用次数。根据你的项目,你可能选择对其进行优化,避免完全使用多次遍历像素填充,或者仔细策划场景,理解哪些应该有高性能,哪些应该有高保真度。

你可以使用 Light Probes 以低成本模拟动态对象的动态光照。Light Probes 是烘焙的立方体贴图,存储了关于场景中各个点的直接、间接甚至发射光的信息。当动态对象移动时,它会插值附近的 Light Probes 的样本,以近似特定位置的光照。这是一种在不使用昂贵的实时灯光的情况下,以低成本模拟动态对象真实光照的简单方法。(参见 docs.unity3d.com/Manual/LightProbes.html。)

Unity 2018 引入了一种新的可脚本渲染管线,提供了一种从 C# 脚本中配置和控制渲染的方法。Unity 2018 包含了用于轻量级渲染(如移动和 VR 应用)的替代内置管线,以及用于高清晰度渲染(如高保真物理渲染)的管线,社区有机会构建和分享更多。使用这些管线可能会超越此处提供的信息和建议。

VR 优化的着色器

着色器是编译后在 GPU 上运行的程序。它们处理由游戏引擎在 CPU 上准备好的 3D 向量和多边形(三角形),包括光照信息、纹理贴图和其他参数,以在显示设备上生成像素。

Unity 提供了一套丰富的着色器。默认表面着色器是一个强大且优化的着色器,支持纹理、法线贴图、高度贴图、遮挡贴图、发射贴图、镜面高光、反射等。

Unity 还包含一套针对移动设备优化的着色器,这些着色器在移动(和桌面)虚拟现实开发中很受欢迎。虽然它们可能不会提供与高保真度 AAA 渲染能力相匹配的照明和渲染支持,但它们旨在在移动设备上表现良好,并且应该被任何开发者的工具箱所考虑,即使在桌面虚拟现实应用中也是如此。

虚拟现实设备制造商和开发者已经发布了他们自己的定制着色器,以他们认为合适的方式优化图形处理。

Daydream Renderer (developers.google.com/vr/develop/unity/renderer) 是一个为 Daydream 平台优化的高质量渲染 Unity 包。它支持法线贴图、最多八个动态光源的镜面高光,“英雄阴影”在性能上比 Unity 的标准着色器有显著提升。

Valve (Steam) 将他们在令人印象深刻的演示项目 The Lab 中使用的 VR 着色器作为 Unity 包(assetstore.unity.com/packages/tools/the-lab-renderer-63141)发布。它支持在单次遍历中使用 MSAA 支持多达 18 个动态阴影光源。

Unity 内含的 Oculus OVRPlugin 包含了多个针对 Oculus 特定的着色器,这些着色器用于他们的预制件和脚本组件。

第三方开发者也通过他们的工具和实用程序提供着色器。如第二章,内容、对象和规模中提到的,Unity 的 Google Poly Toolkit 包含从 Poly 下载的模型的着色器,包括使用 TiltBrush 创建的艺术作品。

你还可以进行实验并编写自己的着色器。在第十章,使用所有 360 度,我们编写自己的内向着色器时,研究了 Unity ShaderLab 语言。Unity 2018 引入了一个新的 Shader Graph 工具,用于可视化构建着色器而不是使用代码。它旨在“足够简单,让新用户也能参与着色器的创建”。

运行时性能和调试

图形硬件架构继续朝着有利于虚拟现实(和增强现实)渲染管道的性能方向发展。VR 引入了对于传统视频游戏不那么重要的要求。延迟和丢帧(渲染帧所需时间超过刷新率)在高质量 AAA 渲染能力方面退居次要位置。VR 需要在时间上渲染每一帧,并且要渲染两次:一次为每只眼睛。受这个新兴行业需求驱动,半导体和硬件制造商正在构建新的和改进的设备,这不可避免地会影响内容开发者对优化的思考。

话虽如此,您很可能应该针对您想要的目标的低规格进行开发和优化。如果此类优化需要不希望做出的妥协,请考虑为高端和低端平台提供游戏的不同版本。VR 设备制造商已经开始发布最低/推荐硬件规格,这减少了猜测。从目标设备的推荐 Unity 设置开始,并根据需要进行调整。

例如,对于移动 VR,建议您针对 CPU 密集型而不是 GPU 密集型使用进行调整。有些游戏会使 CPU 工作更努力,而有些游戏会影响 GPU。通常,您应该优先考虑 CPU 而不是 GPU。Oculus 移动 SDK(GearVR)有一个 API,用于限制 CPU 和 GPU 以控制热量和电池消耗。

在编辑器中运行与在移动设备上运行不同。但是,您仍然可以在设备上运行时使用 Profiler。

在您的应用中拥有一个开发者模式,显示运行时的当前每秒帧数FPS)和其他重要统计数据,这可能很有用。要制作一个如何显示 FPS HUD 的教程,请将一个 UI Canvas 添加到场景中,并添加一个子 Text 对象。以下脚本会更新文本字符串,包含 FPS 值:

public class FramesPerSecondText : MonoBehaviour
{
    private float updateInterval = 0.5f;
    private int framesCount;
    private float famesTime;
    private Text text;

    void Start()
    {
        text = GetComponent<Text>();
    }

    void Update()
    {
        framesCount++;
        framesTime += Time.unscaledDeltaTime;
        if (framesTime > updateInterval)
        {
            float fps = framesCount / framesTime;
            text.text = string.Format("{0:F2} FPS", fps);
            framesCount = 0;
            framesTime = 0;
        }
    }
}

一些 VR 设备也提供自己的工具,我们将在下一部分讨论。

Daydream

Daydream 开发者选项包括GvrInstalPreviewMain预制件,它允许您使用 Daydream 设备与 Unity 编辑器播放模式一起使用。

Daydream 性能抬头显示developers.google.com/vr/develop/unity/perfhud)已内置到 Android 中。要启用它:

  1. 在您的手机上启动 Daydream 应用程序

  2. 点击屏幕右上角的齿轮图标

  3. 按 Tab 键构建版本六次以使开发者选项项出现

  4. 选择开发者选项 | 启用性能抬头显示

然后,运行一个 VR 应用程序,您将看到性能叠加层。

Oculus

Oculus 提供了一套性能分析和优化工具(developer.oculus.com/documentation/pcsdk/latest/concepts/dg-performance/),其中包括广泛的文档和开发者的工作流程指南。非常好!它还包括 Oculus 调试工具、丢失帧捕获工具、性能分析器和性能抬头显示(developer.oculus.com/documentation/pcsdk/latest/concepts/dg-hud/)。

要激活性能抬头显示,您可以从 Oculus 调试工具中运行它,如下所示:

  1. 前往Program Files\Oculus\Support\oculus-diagnostics\

  2. 双击OculusDebugTool.exe

  3. 他们建议您首先关闭异步空间扭曲(ASW),以在没有 ASW 的情况下获得您应用程序性能的良好感觉。找到异步空间扭曲并从选择列表中选择禁用。

  4. 查找可见的 HUD 并选择你想要看到的类型:性能、立体调试、层或无。

摘要

延迟和低帧率是不可接受的,并且可能导致 VR 中的运动病。我们受限于我们运行的硬件设备的性能和限制以及它们的 SDK。在本章中,我们深入探讨了制作优秀 VR 的一些更技术性的方面,考虑了影响性能的四个独立领域:艺术作品、场景、代码和渲染管线。

我们从介绍内置的 Unity 分析器和 Stats 窗口开始本章,这是我们这场战斗的主要武器。为了说明设计模型和材料的影响,我们构建了一个包含 1000 副高多边形透明镜片的场景,检查了性能统计信息,然后尝试了多种提高帧率的方法:降低模型的多边形数量(使其成为低多边形)、移除材料中的透明度,以及管理场景中的细节级别(LOD)。然后,我们考虑了在场景级别可以做的事情,使用静态对象、烘焙光照贴图和遮挡剔除。

接下来,我们探讨了优化你的 C#脚本的基本实践。关键是理解 Unity 的生命周期、游戏循环和昂贵的 API 函数,鼓励你尽可能使帧Update处理变得瘦。然后,我们研究了渲染管线,获得了一些关于它是如何工作的以及如何使用推荐的品质、图形和玩家设置、VR 优化着色器和运行时工具来分析和改进性能的见解。

到现在为止,应该已经很清楚,为 VR 开发有很多方面(这不是字面意义上的)。你努力创造一个令人惊叹的场景,拥有美丽的模型、纹理和照明。你试图为你的访客提供令人兴奋的交互体验。同时,你也应该考虑你的目标平台的需求、渲染性能、每秒帧数、延迟和运动病。专注于性能永远不会太早。开始得太晚是一个错误。遵循易于实施的推荐最佳实践,同时保持你的代码和对象层次结构干净、可维护。然而,在故障排除和性能调整方面采取深思熟虑、科学的方法,使用分析器和其他工具分析你的项目,以便你能专注于根本原因,而不是花费时间在可能产生微乎其微净效果的领域。

我们开发者很快就会对所有但最明显的渲染错误产生免疫力,因此我们是最糟糕的代码测试者。它引入了程序员防御的新颖和令人兴奋的变体,“在我的机器上它工作” - 在这种情况下,“在我的大脑中它工作。” - 托姆·福赛斯,Oculus

开发 VR 应用是一个不断变化的目标。平台硬件、软件 SDK 以及 Unity 3D 引擎本身都在快速变化和改进。随着产品的改进和新开发者见解的出现,书籍、博客文章和 YouTube 视频可能会迅速过时。另一方面,已经取得了巨大进步,确立了最佳实践、首选的 Unity 设置和针对 VR 开发者需求的优化设备 SDK。

随着 VR 的普及,它正作为一个新的表达、沟通、教育、解决问题和讲故事媒介崭露头角。你的祖父母需要学习打字和阅读。你的父母需要学习 PowerPoint 和浏览网页。你的孩子们将在虚拟空间中建造城堡和传送。VR 不会取代现实世界和我们的本性;它将增强它们。

posted @ 2025-10-25 10:34  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报