UE4-VR-项目-全-

UE4 VR 项目(全)

原文:zh.annas-archive.org/md5/3F4ADC3F92B633551D2F5B3D47CE968D

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

虚拟现实不仅仅是我们在二十世纪喜爱的媒体在立体眼镜中的呈现。它远不止于此。虚拟现实不仅仅是以立体 3D 的方式向我们展示周围世界的图像。从字面上说,没错,它确实是这样,但这有点像说音乐只是让我们的耳朵周围的空气动一动。从技术上讲是对的,但这样的解释太过简单,无法让我们理解它。虚拟现实通过与我们的感官互动,与我们认为我们理解世界的认知机制共舞。要理解虚拟现实并学习如何为其创建,我们必须接受它是一种全新的媒体,我们对它的语言、规则和方法所不知道的远远超过我们所知道的。这是强大的东西,毫无疑问,虚拟现实或类似的技术很可能成为二十一世纪的定义艺术形式。

你对这种说法持怀疑态度是正确的。鉴于技术和行业的现状,需要一些想象力才能看到我们现在所处的地平线之外。而且你可能已经看到,公众的期望与技术和艺术形式的实际状态相比,有时滞后,有时超前。因此,对于虚拟现实的意见各不相同。如果我们处于技术迈出重要一步的阶段,人们会对可能性感到惊讶和兴奋,而充满激情的博客则宣称世界已经改变。如果我们处于期望超前的阶段,突然间每个人都对他们的第一代 Oculus Rift 没有一夜之间变成全息甲板感到失望,我们会在博客上看到很多幻灭。在你阅读这篇文章时,很难预测钟摆在摆动时会在哪个方向。

然而,这是一个现实,也是为什么我们相信现在值得学习这种媒体的原因:虚拟现实即将到来,这是不可避免的,它将改变一切,即使从第一代技术的初级状态来看,这一点还不明显。这种媒体具有改变我们学习、玩耍、参与虚拟世界等方方面面的潜力。但这需要时间和想象力。

虚拟现实是一个处于十字路口的媒体。我们现在做出的决策将深远影响未来。在这个媒体中工作的开发人员将成为下一代塑造其语言和方法的人。在虚拟现实领域工作是在一个前沿工作,这是一个令人兴奋的地方。

在这本书中,我们打算为你提供一套坚实的工具,以便开始在这个领域上工作。本书采用实践性、动手操作的方法,教你如何使用虚幻引擎构建虚拟现实游戏和应用程序。每一章都会逐步引导你构建虚拟现实应用程序的基本构建模块,并配以深入的解释,说明你按照这些步骤进行时实际发生了什么,以及为什么要这样做。这就是为什么很重要的原因。理解底层系统和思想的工作原理对于你在完成这些教程后自己的工作至关重要,在这本书中,我们试图给你两者都提供——构建虚拟现实应用程序的方法和你在虚拟现实领域工作所需的背景知识。

你应该从这本书中获得对虚拟现实应用程序的构建有坚实的理解,以及构建它们所需的关于虚幻引擎的具体知识和理解。我们希望我们在这里一起完成的工作能够让你在这个新的领域中自由探索。

这本书适合谁阅读

如果你对创建 VR 游戏或应用程序感兴趣,对于看看 VR 如何能够增强你在当前领域的工作,或者只是对探索 VR 并看看它能做什么感兴趣,本书适合你。你不需要成为一名有经验的工程师,甚至对虚幻引擎有深入的了解,都可以从本书中获益;我们会在进行解释。对于完全不熟悉虚幻引擎的读者来说,在开始阅读本书之前,跟着 Epic 的入门教程走一遍会很有帮助,这样你就知道一切在哪里,但本书完全适合那些需要了解虚幻引擎如何与 VR 配合使用的有经验的虚幻用户,以及刚刚开始探索虚幻引擎的新用户。

无论你是完全新手,对 VR 开发和虚幻引擎都不熟悉,还是已经在其他引擎中进行 VR 开发,或者对虚幻引擎很熟悉但对 VR 还不熟悉,本书都能为你提供很多价值。(我们也希望那些已经熟悉使用虚幻引擎进行 VR 创作的人能够找到一些有趣的新视角和技巧。)

本书内容包括:

第一章,在 VR 中思考,介绍了 VR 作为一种媒介,并讨论了它在许多领域中的应用方式。我们讨论了沉浸和存在感的关键概念,并概述了设计和构建有效的 VR 体验的实践方法。

第二章,设置开发环境,带你了解设置虚幻引擎和移动 VR 开发环境的过程,并介绍了学习使用虚幻引擎和获取帮助的途径。对于那些对使用 C++感兴趣的人来说,本章还展示了如何设置开发环境来构建 C++项目和从源代码构建虚幻引擎。

第三章,你的第一个 VR 项目:Hello World,向你展示如何从头开始创建一个新的 VR 项目,创建 VR 时要使用的设置以及为什么要使用它们,以及如果你要为移动 VR 构建时需要做的不同之处。本章还教你如何将内容导入项目并与之一起工作,以及如何设置一些基本的蓝图,这些蓝图在 VR 开发中需要用到。

第四章,在虚拟世界中移动,教你如何创建和优化角色运动的导航网格,如何构建玩家控制的角色并设置输入处理,然后展示如何构建基于传送的运动方案以及如何实现无缝移动,以获得更沉浸式的虚拟现实体验。

第五章,与虚拟世界互动-第一部分,向你展示如何为玩家控制的角色添加手部,并使用手持动作控制器来驱动它们。

第六章,与虚拟世界互动-第二部分,展示如何设置动画蓝图以响应输入来动画化玩家的手部,并使玩家能够拾取和操作世界中的物体。

第七章,在 VR 中创建用户界面,向你展示如何为 VR 创建交互式的 3D 用户界面,并介绍了一个由该界面控制的 AI 伴侣角色。

第八章,构建虚拟世界并优化 VR,教你如何使用虚幻引擎编辑器的 VR 模式在 VR 中构建环境,以及如何找到环境中的性能瓶颈并修复它们。

第九章,在 VR 中显示媒体,教您如何在 VR 空间中在虚拟屏幕上显示视频媒体,包括单眼和立体。您将学习如何将 2D 和 3D 电影放在传统的虚拟屏幕上,如何将玩家包围在 360 度单眼和立体视频中,以及如何创建媒体管理器来控制其播放。

第十章,在 VR 中创建多人体验,教您关于虚幻的客户端-服务器网络模型,并向您展示如何从服务器复制角色、变量和函数调用到连接的客户端,如何设置玩家角色以在其所有者和其他玩家之间显示不同,以及如何设置远程过程调用以触发服务器上的事件。

第十一章,进一步的 VR-扩展虚幻引擎,向您展示如何安装和构建插件以扩展引擎的功能,并介绍如何使用蓝图强大的调试工具来深入了解陌生代码。

第十二章,接下来去哪里,向您展示了在深入进行 VR 开发时如何获取更多信息。

附录 A,有用的心智技巧,为您提供了一些有用的心智技巧,使您的开发更加高效。

附录 B,研究和进一步阅读,为您的搜索提供了一些有用的起点,这将逐渐帮助您加快学习速度。

为了充分利用本书

您不需要成为一名专业的虚幻引擎开发者才能从本书中受益,但了解事物的位置是有帮助的。如果您还没有安装虚幻引擎,不用担心-我们将在第二章中介绍这个,设置开发环境,但如果您以前从未使用过它,那么在重新开始阅读本书之前,可能有必要花些时间运行虚幻引擎的入门教程,以便了解一切的位置。

本书中的所有项目都经过设计,可以与 Oculus Rift 和 HTC Vive 的最低规格配合使用,因此无论您使用台式机还是笔记本电脑,只要您的系统符合这些最低规格,都应该可以正常运行。当然,您应该有一个 VR 头盔,并且如果您计划开发移动 VR,建议您也有一个台式机 VR 头盔,因为这将大大简化测试过程。本书中您将使用的所有软件都可以在网上免费获取,我们将指导您下载和安装,因此在开始之前,您不需要安装任何特殊的软件。

本书主要是为 PC 开发者编写的,但如果您在 Mac 上工作,您的开发环境设置将有所不同,但我们在引擎中所做的一切都是相同的。

就是这样。如果您有一个 VR 头盔、一个可以运行它的系统和互联网访问权限(因为我们将下载引擎和示例内容),您就拥有了所需的一切。

下载示例代码文件

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

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

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

  2. 选择“支持”选项卡。

  3. 点击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明进行操作。

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

  • Windows 用户请使用 WinRAR/7-Zip

  • Mac 用户请使用 Zipeg/iZip/UnRarX

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Unreal-Engine-4-Virtual-Reality-Projects。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

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

下载彩色图像

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

使用的约定

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

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个示例:“我们还应该快速查看一下您项目的.uproject文件。”

代码块设置如下:

html, body, #map {
 height: 100%; 
 margin: 0;
 padding: 0
}

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

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

任何命令行输入或输出都以以下方式编写:

UE4Editor.exe ProjectName ServerIP -game

粗体:表示新术语、重要单词或屏幕上显示的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。以下是一个示例:“选择窗口|开发者工具|设备配置文件以打开设备配置文件窗口。”

警告或重要提示会以这种方式出现。

技巧和诀窍会以这种方式出现。

第一章:在虚拟现实中思考

“所有的现实都是虚拟的。”

这是一个强有力的陈述,如果你以前没有考虑过,那么它并不明显,所以我会再说一遍——我们所经历的现实是我们头脑中的一种构造,基于高度不完整的数据。从进化的角度来看,它通常与现实世界很匹配,这并不奇怪,但它并不是对现实的字面反映——它只是对世界最可能状态的推断,根据我们在任何时候所知道的。

  • Michael Abrash,Oculus 首席科学家

“关于一项技术最重要的事情是它如何改变人们。”

  • Jaron Lanier,VPL 研究创始人,虚拟现实先驱,微软研究跨学科科学家

欢迎来到虚拟世界。(它在内部更大。)

在本书中,我们将探索使用虚幻引擎 4 创建虚拟现实应用程序、游戏和体验的过程。我们将花一些时间来研究虚拟现实是什么,以及我们可以如何有效地为这种媒介进行设计,然后,从那里开始,我们将使用虚幻引擎深入地演示这些概念,以制作展示和探索这些技术和思想的虚拟现实项目。

每一章都将围绕一个实践项目展开,从基础开始,如设置开发环境和创建第一个虚拟现实测试应用程序,然后深入探讨在虚拟现实中可以做什么以及如何使用虚幻引擎 4 来实现。在每个项目中,我们将引导您完成构建一个展示虚拟现实特定主题的项目的过程,并解释所使用的方法,并在某些情况下演示一些替代方法。对我们来说,重要的是,当您构建这些项目时,您不仅要知道如何做我们描述的事情,还要知道为什么要以这种方式做,这样您就可以将所学到的知识用作计划和执行自己工作的起点。

在本章中,我们将介绍虚拟现实是什么,以及它目前在各个领域中的许多用途。我们将讨论虚拟现实中最重要的两个概念:沉浸感和存在感,以及了解这些概念以及它们的工作原理将如何帮助您为用户提供更好的体验。我们将提出一系列开发沉浸式和引人入胜的虚拟现实体验的最佳实践,并讨论虚拟现实开发所面临的一些独特挑战。最后,我们将整合这些知识,并深入探讨规划和执行虚拟现实项目设计的方法。

简而言之,本章将带领我们了解以下主题:

  • 什么是虚拟现实?

  • 在虚拟现实中我们可以做什么?

  • 沉浸感和存在感

  • 虚拟现实的最佳实践

  • 规划您的虚拟现实项目

什么是虚拟现实?

让我们从头开始,谈谈虚拟现实本身。在最基本的层面上,虚拟现实是一种将用户沉浸到模拟世界中的媒介,使他们能够看到、听到并与环境以及环境中的事物进行交互,而这些事物在现实世界中并不存在。用户完全沉浸在这种体验中,这种效果被虚拟现实开发者称为“沉浸感”。沉浸在一个空间中的用户可以四处看看,通常可以移动和互动,而不会破坏他们实际存在的幻觉。沉浸感,正如我们很快将看到的,是虚拟现实工作的基础。

Rob Ruud 使用 HTC Vive 头戴式显示器测试 Ludicrous Speed 的早期版本

虚拟现实中的沉浸感是用来描述虚拟现实系统将用户置于模拟世界中的能力。他们可以四处看看,并且在许多情况下,可以像真的在那里一样移动和互动,因为头戴式显示器屏蔽了实际环境,所以他们很少有冲突的线索来提醒他们他们并不在那里。

虚拟现实硬件

沉浸用户的最常见方式,也是本书将要讨论的方式,是通过使用头戴式显示器HMD),通常简称为头戴式显示器。 (当然还有其他的 VR 方式,比如在墙上投影图像,但在本书中,我们专注于头戴式 VR。)用户的头戴式显示器显示虚拟世界,并跟踪他们的头部运动来旋转和移动视角,以营造他们实际上正在环顾四周和在物理空间中移动的错觉。一些头戴式显示器,尽管不是所有的都包括耳机,通过启用称为空间化音频的过程,使环境中的声音听起来好像来自虚拟世界中的源头。

在本书和其他关于 VR 的写作中,你会看到HMD头戴式显示器这两个术语互换使用。它们都指的是同一样东西。

有些头戴式显示器只能跟踪用户的视线方向,而其他一些还可以跟踪用户的位置变化。如果你使用的是只能跟踪旋转而不能跟踪位置的头戴式显示器,并且你向前倾斜以便更仔细地观察一个物体,什么都不会发生。当你试图靠近物体时,物体看起来好像离你越来越远。如果你在一个能够跟踪位置的头戴式显示器上这样做,你的虚拟头部会靠近物体。我们使用术语自由度DoF)来描述物体在空间中的运动方式。(是的,发音为doff是可以的,所有开发者都这样发音。)请看以下几点:

  • 3DoF:只跟踪旋转而不跟踪位置的设备通常被称为 3DoF 设备,因为它只跟踪描述旋转的三个自由度:设备向一侧倾斜的程度(横滚),向前倾斜(俯仰)或向一侧转动(偏航)。直到最近,所有移动 VR 头戴式显示器都是 3DoF 设备,因为它们使用类似于手机中的惯性测量单元IMUs)来检测旋转,但无法知道它们在空间中的位置。Oculus Go 和三星 Gear 头戴式显示器就是 3DoF 设备的例子。

  • 6DoF:既可以跟踪位置也可以跟踪旋转的设备是 6DoF 设备,因为它跟踪了全部六个自由度——横滚、俯仰和偏航,还有上下、左右和前后运动。在空间中跟踪物体的位置需要你有一个固定的参考点,从这个参考点可以描述物体的运动。大多数第一代系统需要额外的硬件来实现这一点。HTC Vive 的 Lighthouse 基站或 Oculus Rift 的 Constellation 摄像头为桌面系统提供了这种位置跟踪。Windows 混合现实头戴式显示器和独立头戴式显示器,如 Oculus Quest 和 Vive Focus,使用头戴式显示器上的摄像头阵列来跟踪头戴式显示器在房间中的位置(我们称之为内部跟踪),因此它们不需要外部摄像头或基站。HTC Vive、Oculus Rift、HTC Vive Focus、Oculus Quest 和 Windows 混合现实头戴式显示器都是 6DoF 设备。

3DoF 设备只能跟踪旋转,所以用户只能环顾四周或指向某个方向,但不能从一侧移动到另一侧。6DoF 设备不仅可以跟踪旋转,还可以跟踪位置,所以用户不仅可以环顾四周,还可以移动。

头戴式显示器可以连接到计算机上,如 Oculus Rift 和 HTC Vive,这样可以利用连接的计算机的全部计算能力来驱动视觉效果;或者它们可以是独立设备,如三星 Gear、Oculus Go、Oculus Quest 和 HTC Vive Focus。在撰写本文时,无线连接 PC 和 VR 头戴式显示器开始进入市场。

大多数头盔还配备了输入设备,允许用户与虚拟世界进行交互,可以充当指针或手。手持设备和头盔一样,可以在三个或六个自由度上进行跟踪。3DoF 设备(如 Oculus Go 的控制器)本质上是指针,用户可以瞄准它们,但无法伸手抓取物体。6DoF 设备更像是虚拟手,允许用户以更多种方式与世界进行交互。

虚拟现实不仅仅是硬件

当新开发者首次接触虚拟现实时,他们经常犯的一个重大错误是试图将他们在 2D 空间中习惯创建的传统设计应用到虚拟现实空间中,但在大多数情况下,这是行不通的。虚拟现实是一种独立的媒介,它不遵循之前的媒体所遵循的相同规则。值得花一点时间来看看这意味着什么。

当大多数人第一次考虑虚拟现实时,他们看到头盔并认为它主要是一种视觉体验——传统的平面媒体以立体方式展示。这样看起来是可以理解的,但他们的感知忽略了重点。是的,虚拟现实头盔(取决于是否包含集成音频)主要或完全是一种显示设备,但它为用户创造的体验与传统平面屏幕创造的体验非常不同。

让我们想象一下,你正在看一张照片或一个俯瞰高楼边缘的 2D 视频。你看到远处的街道,但它们并没有真正让你感觉到它们离你很远。它们只是图像中的小部分。现在拿同样的图像,通过虚拟现实头盔以立体方式呈现,你可能会感到眩晕。为什么会这样呢?看一下下面的截图:

非沉浸式媒体,无论多么庞大或详细,仍然让观众感到周围有提醒他们场景并不真实的东西。而沉浸式媒体则似乎完全包围用户。(场景:Epic Games 的 Soul:City 环境包)

首先,正如我们刚才提到的,你被沉浸在体验中。周围的世界没有任何其他东西提醒你这不是真实的。让我们回到之前的例子——你电视上的建筑边缘——转过身来看看你身后。哦。你只是在你的客厅里。即使你直接看着它,你可能购买到的最大的电视仍然会让你有很多周围视野来提醒你你在那里看到的不是真实的。平面屏幕上的一切,即使是 3D 屏幕,都发生在窗户的另一边。你在观看,但你并不真正在那里。在虚拟现实中,窗户消失了。当你向右看时,世界仍然在那里。向后看,你仍然在其中。你的感知完全被一个成为环境而不仅仅是你所看到的画面的体验所占据。

其次,立体图像创造了真实深度的感觉。你可以看到下面的陡峭有多深。街道上的汽车不仅仅是小,它们也很远。在允许运动跟踪的 6DoF 头盔中,你在现实世界中的动作在虚拟世界中得到了反映。你可以倾斜到边缘上或者后退。这种沉浸感、真实深度感知和对你动作的自然反应结合在一起,使你的身体相信你所感知的是真实的。我们称之为“存在感”,这是一种主要通过身体感受到的感觉。

虚拟现实中的“存在感”指的是用户感觉自己实际上身处于虚拟世界中,对环境作出反应,就像他们真的在那里并经历这些事情一样。创造存在感的体验是虚拟现实的全部意义——这是它能做到而其他媒体无法做到的主要事情。

沉浸的机制和由此产生的体验是虚拟现实独有的。其他媒体无法做到这一点。

在阅读有关虚拟现实的文章时,有时会看到“存在感”和“沉浸感”这两个术语被互换使用,但通常更清楚的是将“存在感”视为目标-即您试图在用户中创造的感觉,而将“沉浸感”视为实现这一目标的机制。

实现存在感是困难的

在谈到存在感时,值得指出的是,它是一种脆弱的现象,当前虚拟现实技术仍面临一些挑战,无法完全可靠地创造出存在感。其中一些问题源于硬件,随着技术的进步,这些问题几乎肯定会消失。例如,用户可以感觉到头戴式设备压在脸上,对于有线头戴式设备,他们可以感觉到从头戴式设备延伸出来的电缆。当前一代头戴式设备的视野太窄,无法提供外围视觉(桌面设备的视野为 110°,而你的眼睛可以感知到两倍宽的视野)。显示分辨率还不够高,用户仍然能够看到单个像素(虚拟现实用户称之为“屏门效应”),而且棘手的光学系统如果没有完美对准,会使用户的视野模糊。这意味着在实践中,很难在虚拟现实头盔上阅读小字,而且当用户不得不调整头盔以重新进入镜片的“甜蜜区域”时,他们有时会想起硬件的存在。

然而,从事物的现状来看,很明显这些硬件挑战不会持续太久。自包含和无线头戴式设备正在迅速进入市场,具有越来越可靠的跟踪技术,不再依赖外部设备。显示屏变得更宽,分辨率变得更高,光学波导显示技术显示出更轻、更广的焦点区域的巨大潜力。虚拟现实已经非常出色,很容易看出它将继续改进。

还有一些其他事情可能会破坏存在感,我们对此无能为力-例如意外用控制器撞到桌子,或者撞到家具,失去跟踪,或者听到来自体验之外的声音。当我们能够控制用户的空间时,我们可以处理这些问题,但在我们无法控制的情况下,我们无能为力。

尽管存在这些限制,但想象一下当前一代虚拟现实技术能够给用户带来多么强烈的存在感,并意识到它只会变得更好。用户在虚拟现实中所经历的体验,相信程度远远超过其他媒体。他们以一种其他方式无法实现的方式进行探索和学习。他们比以任何其他方式更深入地与人和地方产生共鸣和联系,除了亲身在那里,其他方式都无法达到这种程度。没有其他东西能够达到这种深度。而我们只是刚刚开始。

我们可以用虚拟现实做什么?

那么,我们可以用虚拟现实做什么呢?让我们来探索一下,但在开始之前,值得指出的是,这种媒介仍处于起步阶段。在撰写本文时,我们正处于消费者虚拟现实硬件的第一代,绝大多数人甚至还没有见过虚拟现实头盔,更不用说体验过了。试试这个:下次你在餐厅或公共场所时,问问自己周围的人中有多少人可能见过虚拟现实头盔-最多只有几个人。现在,他们中有多少人看过电影(一个有百年历史的媒介),看过电视(有三分之二世纪的历史),或者玩过电子游戏(接近半个世纪)?虚拟现实就是这么新。我们还远远没有发现我们可以用它做什么。

因此,请将这些想法作为当前事物状态的指南和一些创意的素材,但请意识到我们还有很多很多尚未想到的东西。为什么不让自己成为发现新事物的人呢?

虚拟现实游戏

正如我们刚才讨论的,VR 的核心是创造一种存在感的体验。如果你正在开发一款 VR 游戏,这意味着专注于给玩家一种“身临其境”的设计是中等候选者。《上古卷轴 VR》和《辐射 4 VR》非常成功地让玩家感觉自己真的身处于这些广阔的世界中。类似《迷失之境》的游戏将玩家置身于一个可以探索和操纵的空间中,也能很好地发挥作用。

模拟手部动作的运动控制器的加入,例如 HTC Vive、Oculus Rift 和 Oculus Quest 提供的控制器,使开发人员能够创建具有复杂交互的模拟,例如《职业模拟器》和《Vinyl Reality》,这是使用传统游戏控制器无法实现的。而 Tender Claws 的《虚拟虚拟现实》则是一个很好的例子,它使用 Oculus Go 的 3DoF 控制器实现了类似 6DoF 的控制。

VR 的沉浸感意味着将你包围在体验中的游戏,如《太空海盗训练师》,效果很好,因为玩家可以与周围的角色互动,而不仅仅是面前的东西。这种需要四面八方观察的设计可以成为你的设计重点。

VR 中引起玩家运动感的感觉将快节奏的游戏,如《Thumper》和《Ludicrous Speed》,转变为身体参与的体验,而《节奏光剑》等游戏则利用玩家的身体动作将游戏变成了健身工具。

然而,VR 中的游戏也面临一些挑战。这种存在感和身体运动的体验使得体验非常吸引人,但并不意味着每种游戏设计都适合 VR。简单地将 2D 游戏移植到 VR 中可能行不通。在 VR 中,放置在 2D 空间中的 HUD(通常缩写为 HUD)不起作用,因为没有 2D 平面可以放置它。在 2D 中可能完全正常的快速移动在 VR 中可能会让玩家晕动。选择为 VR 制作游戏需要是一个有意识的选择,并且你需要考虑到这种媒介的优势和挑战进行设计。

当考虑将游戏或游戏设计从 2D 转移到 VR 时,需要考虑几个特定的方面:移动方案在 VR 中是否有效?如何设计 UI 以适应 VR 中的世界?游戏是否符合 VR 的性能限制?将这个游戏放入 VR 中是否提高了游戏体验?我们将在后面的章节中讨论所有这些考虑因素——移动、UI 和性能。

交互式 VR

交互式 VR 体验不仅仅局限于游戏。例如《Tilt Brush》这样的 3D 绘画应用程序允许用户在房间尺度的 3D 空间中雕刻和绘画,并与其他用户分享他们的作品。《Google Earth VR》允许用户探索地球,其中大部分是以 3D 形式呈现的。交互式叙事体验,如《Colosse》、《Allumette》、《Coco VR》等,将用户沉浸在一个故事中,并允许他们与世界和角色进行互动。交互式 VR 应用程序和体验可以用于生产力或娱乐,并且可以采用几乎任何想象得到的形式。

在考虑创建交互式 VR 应用程序时,有几个考虑因素值得记住。在 VR 中,鼠标和键盘通常不可用-用户无法看到这些设备以使用它们,因此交互通常最好围绕 VR 系统提供的控制器进行设计。在 VR 中阅读文本可能会很困难-显示分辨率正在提高,但仍然很低,小字体可能无法阅读。缺乏 2D HUD 意味着传统菜单不容易使用-通常,这些菜单需要构建到世界中或附加到玩家的虚拟手上(参见《Tilt Brush》的一个很好的例子)。

输入和输出是交互式虚拟现实的主要考虑因素-用户如何将输入信息传达给系统,以及他们如何从系统中获取信息?在这两种情况下,您必须根据系统的优势和劣势进行设计。您没有 2D 的 HUD 或鼠标,但您可以在空间中移动和操作物体。虚拟现实显示器的分辨率还无法接近桌面显示器,因此阅读大量文本可能行不通。在虚拟现实中成功的设计将这些因素考虑在内,并将其转化为有意识的设计选择。

交互式虚拟现实为全新的探索和互动方式提供了令人难以置信的可能性,我们可能还没有看到全部的可能性范围。

虚拟现实电影 - 电影、纪录片和新闻报道

虚拟现实之所以非常适合某些类型的游戏,是因为它能够创造出存在感,这也使其成为纪录片和新闻报道应用的强大媒介。虚拟现实能够将用户沉浸在一种情境或环境中,并通过让观众共享深刻的体验来唤起共鸣。虚拟现实先驱电影制片人 Chris Milk 将虚拟现实称为“终极共鸣机器”,我们认为这是一个公正的描述。Alejandro Iñárritu 的《CARNE y ARENA》在 2017 年获得了奥斯卡特别奖,以表彰它对这种媒介的强大运用,以深入讲述故事。虚拟现实通过沉浸创造存在感的能力使得一些在平面屏幕上无法实现的事情成为可能。

玩家在洛杉矶县艺术博物馆体验 Alejandro Iñárritu 的《CARNE y ARENA》

虚拟现实中的电影和视频可以以几种方式呈现,这些方式通常归结为图像呈现的虚拟屏幕的形状以及这些图像是以单眼 2D 还是立体 3D 呈现的。平面或弯曲的表面通常用于呈现传统电影或电视中的媒体,而圆顶、全景或球体可以用于将观众包围在更沉浸式的 2D 或 3D 体验中。

单声道 360°视频环绕着观众,但缺乏深度-它只是映射到玩家周围的一个球体上。这样做的优点是更容易制作,需要更少的存储空间和更便宜的设备,并且对于许多场景来说,这与真正的立体声之间的区别可能很难察觉。大多数早期的虚拟现实视频都是以这种方式制作的。立体声 360°视频同样映射到玩家周围的球体上(我们将在后面的章节中学习如何做到这一点),但对每只眼睛显示不同的图像以实现真正的立体声深度(我们也将学习如何做到这一点)。使用光场、光探测与测距(LIDAR)和摄影测量法将真实环境映射到真正的 3D 虚拟环境中的体积视频的新方法开始出现,并且随着技术的成熟和处理能力的增加,这些方法可能会变得更加普遍。截至目前,它们仍然相对较新,通常昂贵,并且仍然主要局限于高端专业人员和学术界。

纪录片和新闻报道通常以单声道或立体声的 360°摄像机或设备拍摄的实景视频形式呈现,使观众能够环顾四周并沉浸在一个无缝的感官环境中。360°电影通常旨在提供直接、沉浸和引人入胜的体验,但通常不具有互动性。观众通常不能自由地在场景中移动,除非触发切换到新场景,并且通常不能影响场景中发生的事件。

在规划一个电影式虚拟现实体验时,需要做出两个主要选择:体验是以单声道还是立体声呈现,以及虚拟屏幕的形状是什么?

在电影制作中,VR 是另一个领域,仅仅将平面屏幕的语言移植过来是不够的。360°电影中没有帧的概念,也没有特写或远景的概念。VR 电影制作者必须非常小心地移动摄像机,因为移动或抖动的摄像机很容易让观众感到恶心。VR 电影制作仍处于起步阶段,我们开始了解这种语言与传统电影或电视的语法之间的差异,但在完全理解这种新媒体的语言之前,我们还有很长的路要走。

这并没有阻止像 Alejandro Innaritu、Nonny de la Pena、Chris Milk 和 Felix and Paul 这样的电影制作者在 VR 中创造令人惊叹和有力的电影体验,这凸显了参与创造和发现一种强大而全新的艺术形式的时代是多么令人兴奋。

VR 电影的变体包括以下内容:

  • 叙事故事

  • 纪录片

  • 新闻报道

  • 音乐会和活动

  • 体育

  • 虚拟旅游

建筑、工程和建筑(AEC)以及房地产

VR 非常适合建筑、工程和建筑规划,因为它允许设计师快速探索和迭代设计,并且它是设计师和客户之间的优秀沟通工具。VR 提供了一种沉浸式体验,使用户能够以真实世界的比例探索和审查空间,这是通过其他任何媒介都无法实现的。

建筑、工程和建筑行业通常被统称为 AEC。

出于与 AEC 相同的原因,VR 对于房地产应用同样有用,为潜在买家提供了远程参观房屋的机会,或者在建造之前体验空间。没有任何媒介比 VR 更好地代表空间和比例。

正如我们将看到的,虚幻引擎特别适用于建筑应用,因为它基于物理的材料和光照工作流使得可以创建看起来真实并对光线做出响应的表面,就像它们的真实世界对应物一样。

除了提供逼真的光照和阴影模型,非常适合实现空间的逼真表现外,Epic Games(虚幻引擎的制作商)还提供了一套专为非游戏用途设计的工具,如建筑可视化。其中最重要的是一个名为“Datasmith”的工具包,它允许将高细节场景从建筑计算机辅助设计(CAD)和 3D 软件包导入到虚幻引擎中,几乎不需要修改就可以重现原始来源的对象放置、光照和阴影。

建筑可视化通常缩写为 archvis 或 archviz。

在实际工作流程方面,用于 VR 的工程和建筑环境通常从 CAD 或 3D 数字内容创建(DCC)工具开始,然后通过手动或使用 Datasmith 工作流程将其引入虚幻引擎,从而可以将其制作成可以在 VR 中探索的环境。

对于房地产应用,环境可以完全建模为 3D,也可以拍摄为 360°球体或全景图,这样提供的互动性较少,但制作起来更容易、更便宜。尽管它限制了用户的移动,但 360°摄影仍然可以提供一种沉浸式的空间感,用户在其他情况下无法体验到。

工程和设计

与建筑规划一样,VR 可以成为工程和其他设计应用的非常有效的工具。设计可以在虚拟环境中进行深入测试和迭代,无需建造物理原型,并可以放置在允许在上下文中进行评估的虚拟环境中。设计师可以使用 VR 来探索设计,看看零件如何拼装在一起,并与利益相关者进行沟通,这种体验与实际处理和互动对象的体验非常相似。

教育和培训

可以说,VR 在教育领域开始其生命,早在 1929 年,埃德温·林克(Edwin Link)就使用早期的沉浸式模拟器创建了林克训练器(Link Trainer)来培训飞行员。沉浸和互动的结合使得 VR 成为教育、学习和探索的强大工具。从本质上讲,VR 能够提供比其他媒体更具体和体验性的对主题的理解。大多数其他媒体传达的是思想,而 VR 传达的是直接的体验。

传统教育通常侧重向学生传达事实,但如果学生还没有足够的背景知识来了解他们首先需要这些事实,那么孤立的事实可能会让他们感到无聊或不知所措。相比之下,VR 可以让学生通过直接使用材料和概念的表达来发现和学习概念,实践真实的技能,将抽象的想法转化为经验。沉浸式学习自然而然地带来了上下文,而 VR 唤起存在感的能力可以在学习的主题中创造出物理、社交或情感框架。这可能使学生以其他方式无法实现的方式使其有意义或可理解,并允许学生探索复杂系统的各个部分如何相互配合。

VR 还可以帮助集中注意力,因为它将学生的感官与不相关的干扰隔离开来,并且可以有效地创建虚拟社交学习环境,如虚拟教室。

教育性的 VR 可以(也应该)易于使用、沉浸式和引人入胜,并对学生有意义,可以让学生按照自己的节奏学习,并利用其互动来推动自己的探索和发现。

商业、广告和零售

在商业领域中(有时使用“虚拟商务”这个昵称来描述),VR 为顾客提供了一系列体验产品的新方式,并可以创造机会将顾客与他们可能不会遇到的产品联系起来。例如,汽车买家可以在虚拟汽车配置器中探索颜色选择和选项,以体验他们选择的选项在他们周围的外观和感觉。这种体验也可以在将理想购买从想象转化为真实感的过程中起到重要作用。

对于零售商来说,虚拟现实(VR)提供了一种能够接触到无法亲自到店的顾客的方式,增加了可访问性和销售的可能性。顾客可以更清楚地在上下文中看到产品,减少困惑和退货。即使产品可能太大、太远或太复杂以至于无法通过其他方式有效展示,VR 也可以让顾客有机会在购买前试用。例如,虚拟展厅可以让顾客将家具放在虚拟环境中,看到这些家具如何搭配以及它们在自己的空间中如何适应。

VR 还可以用于促进与品牌的情感联系,将顾客置于支持品牌情感空间的虚拟环境或体验中,例如山顶或时装秀。

医学和心理健康

VR 在心理学、医学、神经科学以及物理和职业治疗方面也提供了有希望的机会。例如,VR 可以通过“减慢时间”在物理疗法中使用,让患者缓慢而重复地执行动作,并且在疼痛管理方面取得了成功。VR 还可以为医学和紧急情况培训提供模拟的虚拟患者。

在心理和行为健康领域,VR 在评估、培训和治疗与压力相关的疾病方面具有强大的应用。患者可以接触到复杂的刺激,以帮助评估和康复中风、创伤性脑损伤和类似的神经系统疾病的认知功能。

还有很多其他的东西

所描述的虚拟现实(VR)的所有用途都有一个共同点,那就是 VR 在传达背景和通过存在感创造意义方面特别有效,并且可以让人们与物体进行复杂的物理互动,这是平面屏幕无法实现的。毫无疑问,还有更多有价值的 VR 用途尚未被发现或考虑到。唯一的限制就是我们的想象力。

沉浸感和存在感

现在我们已经对 VR 是什么以及我们可以用它做什么有了一些背景了。让我们开始动手学习以下内容:

  • VR 的工作原理

  • 可能会破坏沉浸感的因素

  • 作为开发人员,我们需要做的是确保我们构建的 VR 体验能够成功地运行

为此,让我们先列出一些 VR 的最佳实践,然后我们将深入讨论它们。

我们将首先讨论我们试图创造的体验。

沉浸感

正如我们之前讨论过的,当 VR 起作用时,它通过我们称之为“沉浸”的过程起作用,我们之前将其描述为在虚拟世界中感觉到身体存在的体验。要实现沉浸式体验,需要满足一些条件。

利用所有感官

首先,它必须涵盖用户感官的足够范围,以防止来自 VR 体验之外的竞争感官将用户拉回到虚拟空间之外。实际上,这就是为什么 VR 头戴设备被设计成阻挡所有其他光线的原因,以及为什么它们通常包括耳机或内置音频的原因。我们看到或听到的任何与 VR 体验无关的东西都可能破坏沉浸感。

虽然视觉和声音很容易通过眼睛和耳朵传达,但物理感觉更难以产生。在 VR 中,我们将物理感觉称为“触觉”。几十年的研究已经致力于找出如何重现物理感觉,但实际上,这是一个难题。在当前一代 VR 硬件中,触觉以玩家手柄中的震动装置的形式存在,它会在适当的时候震动手柄。虽然它仅限于握持手柄的手部,但即使在设计中使用这种基本的触觉反馈,仍然可以令人惊讶地有效地在虚拟空间中创造出一种物理感。当用户的虚拟手接触到物体时,稍微震动一下可以大大增强物体的存在感,并让用户感知其边界并知道何时与之接触。

记住要利用所有感官来创造沉浸式体验,不仅仅是视觉。使用声音让耳朵参与体验,并使用控制器上的触觉反馈来创造物理线索。

确保感官输入相互匹配并符合用户的期望

感官需要符合用户的期望,并且它们需要相互匹配,以使沉浸式体验感觉真实。当用户转动头部时,他们所看的物体应该在他们的视野中移动,就像它们在物理世界中的位置一样。这部分工作在 Unreal 引擎和 VR 硬件中已经很好地为您处理了,但接下来的声音部分通常被开发人员忽视。

产生声音的物体应使用空间化音频,以确保声音似乎来自物体所在的位置。正如我们刚才提到的,当用户似乎触摸到物体时,物理物体应使用触觉反馈产生触觉反应。

HMD 和虚幻引擎已经为您处理了视觉对象的行为,但请确保使用空间化音频将声音定位到其表面源,并尝试使用触觉反馈使物理动作更加真实。

尽量降低延迟

视觉和音频体验的质量对于沉浸感非常重要,而推动这种质量的最重要因素是体验的流畅性和响应性。对于开发人员来说,这意味着在 VR 中,帧率比其他任何考虑因素都更重要。VR 开发人员使用术语延迟来描述 VR 应用的响应性,即用户执行动作(例如转动头部)和看到视觉结果(在本例中,世界似乎围绕他们旋转)之间的时间。开发人员称之为运动到光子时间,这很重要。如果用户转动头部而世界滞后,它将不会感觉真实,甚至更糟,可能会让他们感到恶心。当前的 VR 头显在硬件和软件方面已经做了很多工作来最小化和掩盖延迟,但作为开发人员,您也必须尽力降低延迟。

延迟是指 VR 应用对用户的视觉反应速度,对于沉浸式体验至关重要。研究表明,您可以容忍的最高延迟是 20 毫秒,但您应该争取更低的延迟。

实际上,这意味着当您必须在场景细节和帧率之间进行选择时(作为开发人员,您将经常面临这个选择),请选择速度。用户更容易原谅较低分辨率的纹理,而不是丢失帧率。在 VR 开发中,您的大部分工作将集中在使场景以可接受的帧率运行上,我们将在虚幻引擎中详细讨论如何做到这一点。现在,请确保记住:保持低延迟对于沉浸式 VR 至关重要,您在设计和开发 VR 应用时必须考虑到这一点。

当面临图像质量和帧率之间的选择时,请始终选择帧率。如果帧率下降,精美的纹理、高多边形模型和动态阴影都无法给用户带来令人信服的体验。与此同时,用户在体验流畅时会在自己的思维中填充大量细节,而如果延迟过高,他们将完全不相信,甚至会感到恶心。

确保与世界的互动合理

与物体的互动应保持一致,并且应合乎常理。由于 VR 的沉浸性特性,用户对物体的期望会增加,他们希望物体的行为与现实生活中的行为相同。在传统媒体中,用户通过平面屏幕观看并受限于画面,他们的眼睛和大脑会不断提醒他们正在看一个不真实的平面图像,他们会原谅很多。但在 VR 中,世界已经包围着他们,似乎是真实的,他们期望它的行为也像真实一样。

在现实世界中不按照预期行为或响应的事物会打破用户的体验和沉浸感。在实践中有一个限制:当然,您不能使世界中的每个物体都可以互动,但在可能的程度上,您应该关注用户的期望,并尽量满足它们。如果您在场景中放置一个看起来可以拿起来的物体,那么请期望用户会试图拿起它,并且要明白如果它的行为与他们的期望不符,那么您将会破坏沉浸感。尽量使场景中的物体看起来互动,并且如果它们不能互动,考虑将它们移出游戏区域或改变它们的外观以管理用户的期望。这是另一个需要判断的领域-并非所有事物都可以互动,而且根据您尝试创建的体验类型,您可能并不总是希望它们互动。在决定您的世界中的物体应该如何行为时,您应该有意识地选择以沉浸感为重点,并且这些选择在世界空间内应该感觉一致而不是随意的。

用户会试图触摸看起来可以触摸的物体,并试图移动它们。在您能够满足他们期望的地方,请尽量满足他们的期望,或者设计场景以使这些互动不被期望。

探索 VR(尤其是带手柄的 6DoF VR)为互动提供的独特机会。在以前的媒体中,用户主要使用鼠标、按钮和操纵杆进行互动,但在 VR 中,用户的手与世界直接互动,这使得一系列全新的互动变得可能。在传统游戏中,浇水壶可能通过按下按钮来使用,但在 VR 中,用户可以挤压控制器握把来拿起它并翻转手掌来使用它。思考在您的世界中什么是合理的,以及当用户的手进入画面时会发生什么,并设计以利用这些机会。界面不再只是由按钮组成。

用户对互动的期望会因您所创建的体验类型而异。如果您正在制作一个模拟身临其境的游戏,沉浸感非常重要。另一方面,如果您正在制作一个电影观众,用户可能并不在乎附近桌子上的虚拟咖啡杯是否可以拿起来,因为这不是他们的目的。您需要了解对用户来说什么是重要的,什么不重要,并满足这些期望。

您如何呈现用户的手也会影响他们的行为期望。如果它们被建模为手,用户可能自然地期望他们可以拿起物体并移动它们。如果您显示的是控制器、调色板、武器或其他工具的模型,那么您就在暗示一种不同类型的互动。用户会试图做他们看起来可以做的事情。

构建一个一致的世界

正如我们之前在关于互动的讨论中提到的,整个体验应该在逻辑上合理。用户应该能够根据您在世界中给予他们的东西构建一个现实模型,即使它是一个抽象的或完全是幻想的。您正在构建的地方应该感觉像一个地方,有自己的语言和规则。

你在虚拟世界中投入的细节会对此产生影响。体验越沉浸,沉浸感就越脆弱。增加细节和沉浸元素会产生对其他世界中的一切都能达到同样标准的期望,并且如果某些事物与世界表面规则不一致,会使用户退出沉浸状态。在许多情况下,你可能希望以更加风格化的方式呈现你的世界,以管理用户的期望。沉浸并不要求虚拟现实体验与现实世界完全相同,而是要求体验在自身内部保持一致。

小心不要与用户对自身身体的感知相矛盾。

小心添加与玩家对自身身体的感知相矛盾的沉浸元素。我们都对自己的身体在哪里以及在做什么有一种自然的感知。这被称为“本体感”,它是一种即使你不看它们也能告诉你手臂和腿在哪里的感觉。以不符合这种感觉的方式呈现用户的身体会破坏沉浸感。

通常渲染用户的手部效果很好,因为动作控制器可以准确告诉我们手的位置,但渲染其他部位可能不是一个好主意,因为我们无法得知其他部位的真实动作。如果你猜测并且猜错了,用户会感觉不对劲,打破沉浸感。最好的方法往往是不要猜测,只渲染手到手腕的部分,让手臂、腿和身体保持想象。有趣的是,用户似乎更喜欢这样。他们往往不会注意到身体是看不见的,直到有人指出来,而渲染错误的身体会引起注意。

出于类似的原因,如果逼真的肉质手部与用户的真实手部不匹配,用户可能会感到不舒服。如果手部被风格化为半透明、卡通或机器人样式,用户会感觉更好,因为他们不会觉得自己在试图模拟现实并且做错了。

动画师通常提到一个称为“诡异山谷”的现象,当模拟接近人类外貌时,会触发观众对其所有不正确之处的本能意识。为了使模拟工作,它要么需要足够风格化以使观众不期望真实感,要么需要完美地实现真实感。介于两者之间的任何东西都会让人感到毛骨悚然。同样的原则也适用于虚拟现实中对用户自身身体的呈现。不要几乎做对,要么完美,要么风格化。

决定你的应用的沉浸程度,并相应地进行设计。

最后,并不是每个虚拟现实的使用都需要同样的沉浸感。你在这方面的选择取决于你的应用的目的。如果它是一个用于可视化工程模型的工具,你可能最感兴趣的是虚拟现实的能力,让用户轻松操作模型,而他们是否真的相信自己身处另一个地方可能并不重要。另一方面,如果你正在创建一个沉浸式的游戏或电影体验,这些选择将至关重要。你需要弄清楚对于你的特定应用来说,哪些规则最重要。

身临其境

沉浸在虚拟现实中的目标是创造用户身临其境的体验。身临其境,正如我们之前所定义的,是一种身处某个地方的感觉,这在很大程度上是一种身体上感知到的现象。很多时候,用户会对世界中的事物,比如高度或飞向他们的物体,做出身体上的反应。身体在很大程度上相信在虚拟现实中所感知到的,并做出相应的反应。如果你主要从生理学角度来考虑身临其境,你会更容易理解用户的体验。这种体验会让用户感受到什么?

理解沉浸感的关键是要理解,虚拟现实并不是通过试图准确模拟环境来工作,而是通过触发和欺骗我们用来感知世界的一系列系统。这就是为什么如果我们正确地移动世界并保持延迟低,我们可以在纹理上获得低细节,而我们的感知系统对运动比对细节更敏感的原因之一。虚拟现实不需要欺骗所有感官,只需要以正确的方式欺骗正确的感官。

模拟器晕动病

在虚拟现实中,你将经常遇到的一个重要因素是模拟器晕动病。这是一种常见于虚拟现实中的由视觉引起的晕动病,你将经常处理这种情况。

作为人类,我们大部分时间都是直立行走,这需要极其复杂的协调才能实现,然而我们却可以毫不费力地做到。我们通过内耳中的一个结构,称为前庭系统,来协调运动和保持平衡。这个系统非常敏感,它与我们的视觉和我们对身体的感知(本体感觉)一起工作,以理解我们的运动方式。

你会听到虚拟现实开发者经常谈论前庭系统内耳。对于我们来说,由于前庭系统位于内耳中,我们在使用这些术语时指的是同一件事。这是告诉我们是否在移动以及如何保持平衡的三个系统之一。另外两个是我们的视觉系统和我们的本体感觉(我们对身体位置的自然感知)。当这三个系统的信号不一致时,问题就会出现。

当视觉信息告诉身体它在移动,但内耳无法感受到这种运动时,就会产生问题。(研究人员称之为感觉冲突理论。)晕船和晕车也是出于同样的原因。当来自内耳前庭系统的视觉运动线索和运动线索不匹配时,身体可能会触发恶心、出汗和其他反应。(研究人员尚未就此达成一致意见,但有一种理论认为,当感觉不匹配时,身体可能会认为自己中毒了。)

虚拟现实的挑战在于它非常成功地模拟了运动。用户的大脑自然地接受他们看到的运动是真实发生的,并在内耳的信号无法证实这一点时出现问题。开发者需要意识到这个挑战并处理它。我们将在下一节讨论如何做到这一点。(请注意,相反的情况也是如此——如果用户转头,始终在头戴设备中显示运动。)

模拟器晕动病,有时缩写为晕动病,是一种可能在虚拟现实中发生的晕动病。(有时也会缩写为VIMS,代表视觉引起的晕动病。)模拟器晕动病最常见的原因是设计不良的运动方式。第二个最常见的原因是延迟高。用户在世界中移动的方式,以及世界对他们的运动如何平稳和一致地响应,是解决模拟器晕动病的关键因素。

安全

另一个重要的考虑因素是安全。由于虚拟现实完全压倒用户的感官,有可能使用户陷入不安全的情况中,作为开发者,你需要尽量避免这种情况发生。例如,如果你倾斜地平线,很有可能你的用户会失去平衡。如果你设计了一个需要大幅度身体动作的体验,比如挥动剑或棒球棒,要注意用户无法看到周围环境,很容易撞到现实世界中的物体。同时,还要注意可能导致眼部疲劳的因素,比如强迫用户专注于离摄像头太近的用户界面元素,以及可能由闪光灯引发的光敏性癫痫发作。

在考虑这些因素的基础上,让我们具体介绍一些最佳实践,以帮助保持用户的舒适和安全。

VR 最佳实践

现在我们已经谈了一些关于沉浸感和存在感的内容,让我们来看一些具体的实践方法,以保持用户的舒适并避免破坏沉浸感。不要认为这些都是铁板钉钉的(除了保持帧率和不要控制用户的头部之外)- VR 仍然是一种非常新的媒介,还有很多可以尝试和发现新的有效方法的空间。仅仅因为有人说某件事做不到并不意味着它真的做不到。话虽如此,以下建议通常代表了我们目前对 VR 中有效方法的最佳理解,遵循它们通常是个好主意。

保持帧率

你是否察觉到了一个模式?你必须维持帧率。高延迟会让用户完全脱离沉浸感,这是引发模拟器晕眩的主要原因之一。考虑一下在 VR 中渲染器需要做的工作,你会发现这是一个相当大的挑战。HTC Vive Pro 显示 2,880 x 1,600 的图像(每眼 1,400 x 1,600),而原版 Vive 和 Oculus Rift 显示 2,160 x 1,200 的图像(每眼 1,080 x 1,200),它们都要求每秒发生 90 次这样的渲染,给渲染器留下 11 毫秒的时间来准备每一帧。Oculus Go 每秒显示 2,560 x 1,440 像素(每眼 1,280 x 1,440),意味着渲染器大约有 13 毫秒的时间来呈现每一帧。虚幻引擎的渲染速度非常快,但即便如此,这仍然是一个很大的渲染量,而且时间非常有限。你需要做出一些妥协来达到你的目标。我们将在本书中讨论如何做到这一点。

以下是目前市场上的头戴设备及其渲染需求的列表。

有线头戴设备

HMD 设备 分辨率 目标帧率
Oculus Rift 2,160 x 1,200 (每眼 1,080 x 1,200) 90 FPS (11 毫秒)
HTC Vive 2,160 x 1,200 (每眼 1,080 x 1,200) 90 FPS (11 毫秒)
HTC Vive Pro 2,880 x 1,600 (每眼 1,400 x 1,600) 90 FPS (11 毫秒)
Windows Mixed Reality 不同的设备有所不同,大多数显示 2,880 x 1,440 (每眼 1,440 x 1,440) 90 FPS (11 毫秒)

独立式头戴设备

HMD 设备 分辨率 目标帧率
Gear VR 根据使用的手机而有所不同。 60 FPS (16 毫秒)
Oculus Go 2,560 x 1,440 (每眼 1,280 x 1,440) 72 FPS (13 毫秒)
Oculus Quest 3,200 x 1,440 (每眼 1,600 x 1,440) 72 FPS (13 毫秒)

还要记住,你应该以稍高于这些目标的帧率为目标,这样即使出现卡顿也不会造成严重的不适。

如果帧率下降,新的帧无法在头戴设备需要显示时进行渲染,VR 硬件会做一些工作来减少感知延迟,但这是通过一些技巧来实现的。在这些情况下,硬件会重新渲染上一帧并调整它以适应用户当前的头部运动,所以用户看到的不是完全正确的帧,而是比完全丢帧要好一些。(Oculus 称此过程为异步时间扭曲ATW),在 Vive 上称为异步重投影。)不过,不要将时间扭曲或重投影作为救命稻草,它们的作用是在应用程序出现卡顿时让用户感到舒适,但对用户来说仍然是一种降级的体验。不要让你的应用程序长时间错过目标帧率。

还要确保在你打算支持的最低规格硬件上测试你的应用程序,并为用户提供调整渲染需求的方法,以便他们能够在他们所使用的硬件上达到帧率目标。

绝对不能控制用户的头部

除了丢帧之外,模拟器晕动病的另一个最常见原因是我们之前提到的感官冲突——视觉上感知到的运动与内耳感受到的运动不匹配。在 VR 中,你需要适应两种主要类型的运动:

  • 玩家角色的移动(四处走动、传送或驾驶车辆)

  • 玩家头部相对于他们的角色的移动

玩家角色的移动由你为体验实现的运动系统来处理。在这里你真的没有选择——你将不得不创建在现实生活中不存在的移动,但有一些事情你可以做,以减少这个问题,我们很快会讨论它们。

单词“avatar”起源于梵语,指的是神在人类形式中的具体化。在当前的用法中,它将这个隐喻扩展到指代一个虚拟世界中由人类玩家控制的角色。它的伴随术语“agent”指的是由 AI 程序控制的角色。

然而,你不应该干涉玩家头部的移动。

在实践中,这意味着:永远不要以用户自己的行为之外的方式移动摄像机。如果你正在制作一个游戏,用户的角色死亡时,不要让摄像机固定在头部上,而身体却掉落下来。如果你这样做,几乎肯定会让用户感到不适。相反,考虑切换到第三人称视角或以其他方式处理动作。永远不要移动摄像机来强迫用户在电影中四处观看,也不要应用“行走晃动”或摄像机抖动。用户应该始终控制他们的头部。

永远不要将摄像机与用户的头部分开移动,也永远不要在用户的头部移动时不移动摄像机。你应该始终保持头部移动与相对于用户角色的摄像机移动之间的 1:1 关系。

这个原则适用于双向。如果玩家移动头部,摄像机必须移动,即使游戏暂停或加载。永远不要停止跟踪。

如果你需要将用户传送到一个新的位置或出于任何原因改变摄像机,考虑使用快速的黑色或白色淡入淡出来覆盖过渡。人们在快速转动头部时本能地眨眼,模仿这种行为是一个好主意。

在游戏中的剧情场景在 VR 中需要与传统平面屏幕上的处理方式不同,原因也是相同的。通常,在制作剧情场景时,你会控制摄像机,移动和切换不同的镜头,但在 VR 中你无法做到这一点。你无法控制用户会看向哪里,而且你需要小心地移动他们。这给你留下了几个选择。首先,如果你的场景是预渲染的,那么你真的没有选择,只能将它们映射到虚拟环境中的屏幕上。这会破坏沉浸感,但对用户来说并不比在现实生活中观看电影更困难。如果你在引擎中进行剧情场景制作,你需要考虑如何处理玩家的视角。

对于第一人称视角,最好的方式是将电影场景设置在用户周围,并允许他们自由地观看和移动。在这种情况下,你不能切换到另一个镜头,也不能保证用户在关键时刻看向你想要他们看的地方,但这是最沉浸式的方法。

剧情场景也可以以第三人称的方式处理,即将用户的视角从他们的身体中拉出来,让他们观看场景的展开,但你需要小心处理——超脱体验可能会让玩家感到迷失方向,削弱沉浸感和玩家对角色的认同。

在虚拟现实中制作电影时,非常小的移动可能会引起恶心。用户更容易容忍前进的运动,而不是左右或旋转的运动,如果运动是由可见的车辆或其他解释方式来解释的,用户似乎更容易容忍运动。

在虚拟现实中考虑如何使用相机不仅仅是为了管理用户的不适感。这是一个新的领域,你从电影和游戏中学到的规则在这里不同。你设计的是重现用户的眼睛,而不是相机,这对你的构图有深远的影响。用户如何移动?他们知道你希望他们看什么吗?当他们看自己的手时会看到什么?镜子呢?将用户置于一个世界中(而不是让他们通过窗户观看)会如何改变他们与之的关系?在开发工作时,所有这些因素都需要有意识地进行选择。

不要在相机上加速或减速。

根据你正在创建的应用程序的类型,你可能需要为用户提供一种改变位置的方式,无论是通过传送还是平滑移动。(我们将在后面的章节中深入探讨这个问题。)然而,如果你选择实现平滑的移动方法,请不要在玩家开始和停止移动时加速或减速。以最高速度开始移动,或者如果你选择平滑你的开始和停止,保持它们非常短暂。(当然,永远不要做一个控制用户相机的开始移动或停止移动的动画。)

不要覆盖视野,操纵景深或使用动态模糊。

我们刚才提到,虚拟现实模拟的是用户的眼睛,而不是相机。因此,在模拟中不要做眼睛在现实生活中不会做的事情。不要改变相机镜头的焦距或景深。眼睛的焦距不会像电影变焦镜头那样改变,如果你改变这个,很可能会让用户感到恶心。

在当前一代虚拟现实中,操纵景深不是一个好主意,因为我们还没有一种可靠的方法来知道用户实际上在视野中看什么。在未来,随着眼动追踪的改进,这可能会发生变化,但目前不要为用户做出这个选择。

运动模糊不应该应用于相机或场景中的物体。这是电影在固定时间内拍摄静止画面时产生的一种伪像,模糊了该画面内的运动,但这不是眼睛的工作方式,在虚拟现实中看起来不自然。

顺便说一下,避免使用其他模拟相机效果,如镜头光晕和胶片颗粒。同样,这些效果模仿的是电影的行为,而不是眼睛,我们不是在虚拟现实中模仿电影。这些胶片效果也可能导致用户产生不良的身体反应,如果效果在眼睛之间不一致,还会导致模拟器晕动症,并且会消耗宝贵的帧时间来渲染。不要使用它们。

最小化视觉错觉。

你是否曾经坐在静止的汽车窗前,看着一辆大货车或公共汽车移动,感觉自己实际上是朝相反方向移动的?这种现象被称为视觉错觉,它指的是由光流模式产生的自我运动的错觉。如果你的视野中有大部分区域在移动,这可能会在你的身体中产生运动感觉,正如我们之前讨论的,与内耳信号不匹配的运动感觉可能会引发模拟器晕动症。

视觉错觉是当你的视野中的大部分区域移动时产生的运动错觉。光流是指视野中内容的运动模式,正是这些运动模式引起了视觉错觉。

实际上,这意味着如果用户的视野中有大块移动,就有可能引发模拟器晕动症。我们已经讨论过关于移动用户头部的问题(不要这样做),并且我们已经提到了一些处理这个问题的方法,但你还需要注意其他可能引起视觉错觉的情况。

要注意填满画面大部分区域的移动模式-无论它们是否是你的运动系统的一部分,它们仍然可以产生运动错觉,这可能对用户造成问题。

一些游戏和应用程序尝试使用“隧道视觉”效果来减少在用户需要快速移动环境时的视觉错觉-当玩家的角色奔跑时,视野边缘会逐渐闭合,减少外围视觉。

用户似乎对前进运动比对侧向运动更容忍。这可能部分是因为在现实生活中,我们前进的次数远远超过侧向移动的次数,但也可能是因为用户在前进时所看到的光流仍然具有相对固定的中心点,而在侧向移动中,视野中的一切都在移动。

当你试图确定 VR 中的特定移动是否可能引起模拟器晕动时,思考该移动将产生的视觉流动类型可能会有所帮助。相对固定参考点的视觉流动,比如向前奔跑时的地平线,可能没问题,而移动视野中的一切,比如侧向移动,可能不行。

旋转玩家的视角尤其有问题。它会使视野中的几乎所有东西都移动,而前庭系统特别适应检测旋转。在这里要非常小心。平滑的旋转通常不是一个好主意,但开发者发现将用户快速转向一个新的角度可以重新定位用户而不会让他们晕动。事实证明,大脑非常擅长填补感知中的中断,因此在大幅度移动期间快速转向或“眨眼”视野可以非常有效地干扰运动感知而不会分散用户注意力。

许多开发者还发现,给用户一个可见的随着他们移动的交通工具,比如飞机座舱,可以减轻旋转时的视觉错觉。这是否适合你取决于你所创建的体验类型,但重点是用户似乎在视野中给予固定参考点时更不容易出现模拟器晕动症。在适当的情况下,考虑将其纳入设计中;在不适当的情况下,考虑其他方式来打破视觉流动,比如眨眼或快速移动。

避免使用楼梯

如果你允许用户在环境中平稳移动,要注意当用户在其中导航时,环境的某些特征可能引发模拟器晕动症。楼梯尤其糟糕。为每一步提供碰撞的楼梯会在用户导航时使视野弹跳,更糟糕。这些在穿越时会产生垂直运动感的环境特征可能很困难,因为内耳对高度变化非常敏感。

如果可以的话,尽量避免使用楼梯。如果无法避免,要注意楼梯的陡峭程度以及用户在其上移动的速度。你需要进行一些测试来找到合适的方法。

使用比通常更暗淡的灯光和颜色

在场景中使用明亮的灯光和强烈的对比要小心。明亮的灯光会导致一些用户出现模拟器晕动症,而强烈的对比会增加用户在世界移动时的运动感。此外,使用当前硬件,明亮的灯光通常会在头盔的菲涅耳透镜上产生光晕,这会提醒用户他们正在佩戴硬件,从而破坏沉浸感。一般来说,建议您使用比平常更冷的色调和更暗的灯光。

保持世界的尺度准确

VR以一种平面屏幕无法做到的方式传达了世界中物体的尺度。我们每个人通过一对距离固定的眼睛以立体视觉看世界。这个距离被称为瞳孔间距IPD),它影响了我们对世界中物体大小的感知。大多数 VR 头盔可以调整到与用户的瞳孔间距相匹配,并且应该正确调整以减少眼部疲劳。

用户眼睛瞳孔之间的距离被称为瞳孔间距,它是用户对世界中物体大小感知的主要因素。

作为开发者,这意味着您的世界中物体的尺度很重要。在平面屏幕上,用户只能通过将一个物体的大小与另一个物体进行比较来确定其大小,但在 VR 中,用户的瞳孔间距决定了绝对的尺度感。在平面屏幕上,一个物体如果独自出现,无论它是太大还是太小,看起来都是正常的。但是在 VR 中,即使没有其他物体可以进行比较,同样的物体在立体 3D 中看起来会让观众感到不对劲。

如果世界的尺度感觉不对,一些用户可能容易出现模拟器晕动症,即使那些没有出现这种情况的用户也可能会觉得世界感觉“不对”,而不一定知道原因。

确保您的世界中的物体尺度正确。在虚幻引擎中,默认情况下,一个虚幻单位UU)等于一厘米。

注意身体动作

在 VR 中,您的用户在现实世界中四处移动,戴着电动眼罩。尊重这一点,并小心在 VR 中要求他们做什么。当要求用户挥动手臂、奔跑或横移时要小心,因为他们很容易在现实世界中撞到障碍物或墙壁。对于带有电缆的头盔,不要要求用户反复向同一方向转动并缠住电缆。还要注意要求用户够到地板上或超出他们正常够到范围之外的物体-这在他们的真实物理环境中可能并不容易或可能。正如前面提到的,避免以可能导致用户失去平衡的方式改变地平线。记住,当用户在 VR 中时,他们对世界的几乎所有信息都来自于 VR 模拟-要意识到这些信息如何与周围的无形物理世界相吻合或相矛盾。

管理眼部疲劳

眼睛使用肌肉来对焦物体和定位眼睛,这些肌肉和其他肌肉一样会疲劳。我们称之为眼部疲劳。眼部疲劳的症状包括头痛、疲劳、模糊或重影。作为设计师,您可以采取一些措施来减少用户的眼部疲劳,了解一些导致眼部疲劳的原因将有助于您做到这一点。

首先,眼部疲劳可能是由闪烁引起的。我们已经谈论了保持低延迟的重要性-这是保持低延迟优先级的另一个原因。不要创造故意闪烁的内容,因为这可能导致眼部疲劳,还可能引发光敏性癫痫发作。

高延迟引起的闪烁会导致眼部疲劳。保持低延迟。

其次,眼睛需要在 3D 空间中对物体进行一些物理工作。它们必须调整其透镜的形状以对物体进行聚焦(这称为调节),并且它们需要将自己对准,使其视线在物体上汇聚(这称为汇聚)。我们自然地有一种将这两个动作相互关联的反射,因此眼睛自然地希望汇聚到与其透镜聚焦的深度平面相匹配的深度,并且透镜自然地希望以与眼睛汇聚的深度相匹配的方式进行聚焦。问题出现在虚拟现实中,眼睛实际上看到的图像是固定距离的,但这些图像的内容存在于各种虚拟深度平面上,因此眼睛仍然必须旋转,使其汇聚在它们正在观察的物体上。这会产生冲突,因为透镜正在适应的焦距与眼睛汇聚的深度不匹配,这可能导致眼部疲劳。

在虚拟现实中,眼部疲劳可能由两个因素引起:闪烁,可以通过保持低延迟来管理,以及眼睛的透镜需要聚焦以看到头戴式显示屏的固定距离,以及它们需要汇聚以看到立体深度中的物体的距离的变化之间的冲突。这通常被称为汇聚-调节冲突,您可以通过将重要物体放在虚拟世界中约 1 米远的位置来管理,以使汇聚和调节的需求基本上保持一致。

在设计世界时,您可以通过牢记这两个要求来管理这个问题。HMD 上的菲涅耳透镜使头戴式显示屏看起来离眼睛约 1 米远,使透镜能够适应约 1 米远的焦平面。然后,用户的眼睛自然会更容易集中注意力于虚拟世界中看起来大约在那个距离的物体上。实际上,物体在 0.75 米到 3.5 米的范围内最容易被观察到,其中 1 米似乎是理想的距离。避免让用户长时间盯着离眼睛不到半米的物体。

将您知道用户将长时间注视的物体至少放在离相机半米远的地方,最好在 1 米左右,以减少眼部疲劳。

不要强迫用户成为一个眼球扭曲者来查看您的用户界面。通常将 GUI 附加到用户的脸上是一个坏主意-当他们转动头部查看 UI 元素时,它会“逃离”,因为它附着在试图查看它的同一个头部上,所以用户必须单独转动眼球来专注于它。不要这样对待他们。这对用户来说是令人恼火的、令人疲劳的,并且在现实世界中没有类似的情况。将您的用户界面放在世界中,以便用户可以从舒适的视角和舒适的距离专注于它。将用户界面元素附加到用户的身体上,例如手腕,可以很好地工作,因为它允许用户在想要与之交互时将其带入视野。将 GUI 元素放入驾驶舱或车辆中也可以很好地工作。当用户看向它们时,可以将用户界面元素放置在世界各地并显示出来。

如果您确实将其附加到用户的头部,请将 GUI 元素保持在我们讨论的理想范围内,并以允许无需努力阅读的角度放置。

尽量避免创建迫使用户频繁改变焦距的情况。例如,如果您正在制作一款射击游戏,将关键信息放在附近的用户界面元素上,而敌人则在远处,那么您可能正在创建一种迫使用户频繁改变焦点以检查用户界面并专注于战场上的敌人的情况。在平面游戏中,这不是问题,但在虚拟现实中,这会使他们感到疲劳。设计您的用户界面,使用户可以在不专注于它的情况下获取关键信息,例如易于阅读的图形元素,或者考虑将用户界面元素放在敌人头顶上。

GUI 元素可能会被世界中比 UI 元素更接近相机的物体遮挡。不要试图使用 2D 游戏空间的技巧来改变这一点。在 2D 游戏设计中,通常会在 3D 元素上绘制 UI 元素,即使该元素实际上会阻挡玩家对其的视野。然而,在 VR 中这样做会创建一个令人困惑的立体图像,看起来一点也不舒服。接受这样一个事实,即你的 UI 存在于世界中,遵循与其他物体相同的规则。

有意识地选择体验的内容和强度

当 VR 实现存在感时,会产生强烈的反应。这是一种亲密的体验,一种直观的体验,有时也是一种引起恐惧的体验。在制作体验时要意识到自己在做什么——你可以很容易地引发某些用户的战斗或逃跑反应。这可能正是你的意图,我们并不建议你回避你试图创造的任何东西。但要意识到你在这里玩强大的东西,并做出有意识的选择。与其平面屏幕的前辈相比,VR 更有可能引发恐惧症,因为用户沉浸在空间中,而不是被他们的外围视觉不断提醒他们所看到的不是真实的。要注意可能引发眩晕、幽闭恐惧症、黑暗恐惧症、蛇、蜘蛛或其他恐惧症的情况。还要记住,用户对其个人空间内的威胁会有更强烈的反应。

对于那些故意在 VR 中制造恐惧、制作恐怖体验或治疗创伤后应激障碍的体验的人来说,电影和 VR 之间存在着有意义的区别——用户始终存在于 VR 中,而在电影中并非如此。他们对个人空间有一种本能的感知,你可以利用这一点产生巨大的效果。电影也没有这个。在电影中,一个看起来很近的物体只是在屏幕上很大,但它离用户的距离仍然是屏幕实际距离。在 VR 中,这个空间是真实存在的。在 VR 中,“它就在你身后”真的意味着它就在你身后。

让玩家自行管理他们的会话持续时间

VR 对用户的身体、眼睛和思维提出了其他媒体所没有的要求。他们戴着头戴设备,通常是站立或移动的。设计你的体验,让他们可以随时退出或需要时稍后继续。让他们根据需要休息。

保持加载时间短

与平面屏幕上的游戏和应用程序不同,VR 中的用户在等待应用程序加载时无法做其他事情。优化以保持加载时间短。同时要记住,即使在加载过程中,你的应用程序也必须对用户的头部跟踪做出响应。

质疑我们刚才告诉你的一切

VR 作为一种媒介和艺术形式还处于初级阶段。现在假装我们知道它的规则将会变成什么样还为时过早。在电影的早期,演员总是被全景拍摄,因为当时的常识是观众不会为了看到“半个演员”而付费。同样,你也应该愿意质疑在 VR 设计中收到的指导和建议。这些代表了目前对似乎有效的最佳理解,但这并不意味着没有其他未经尝试的方法来做事情。对它们持开放态度。这也是为什么这些指南每个都附有关于它们存在的原因的信息——这样你就可以理解它们的来龙去脉,做出自己的选择并尝试自己的实验。你是 VR 的开拓者,是全新沟通方式的创造者的一部分。不要害怕探索。

规划你的 VR 项目

我们已经在抽象层面上讨论了很多关于虚拟现实的内容-我们可以用它做什么,以及我们目前对它的工作原理和在其中有效的方法的了解。从现在开始,这本书将变得非常实际和实践,并且我们希望,当我们通过这些项目并学习如何在虚幻引擎中构建虚拟现实体验时,我们刚刚讨论的原则能够留在您的脑海中并指导您的选择。

在我们开始动手之前,还有一个主题需要探讨,那就是如何将一个想法变成您实际可以制作的东西。

明确您想要做什么

在开发设计时,首先要做的是决定它的用途。这听起来很明显,但开发人员往往会直接开始项目的构建,而没有先退后一步,弄清楚他们真正想要做什么以及为谁做。结果往往是要么体验不集中,无法真正实现预期目标,因为各个部分没有共同支持一个共同目标,要么项目需要很长时间才能完成。这浪费了很多工作,因为开发人员发现需要更改的事物,并不得不放弃现有的工作来进行更改。在开始构建软件之前,花一些时间进行计划,可以节省很多努力,并增加项目成功的可能性。

设计中要记住的第一件事是,您建造的越多,更改就越困难和昂贵,因此尽量在过程的早期做出这些决策。您可以制作的最便宜的原型是在您自己的头脑中。第二便宜的是在纸上。一旦开始构建软件,从您需要使项目运行所需的最低限度开始-一个灰色盒子环境或简单的原型,并进行测试以查看它需要如何更改。您几乎可以保证会发现一些您没有预料到的事情,而这正是发现这些事情并进行必要更改的时候。一旦您经历了这个过程,发现了真正有效和无效的内容,并根据您所学到的内容调整了设计,现在您可以开始将昂贵的艺术和修饰品加入到工作中。太多的开发人员倒过来努力,试图一开始就制作出最终产品,他们会被困在本来可以更容易改变的决策中,如果他们首先做了这些准备工作。

在这个基础上,首先要考虑的是项目是为谁以及为什么而制作。这是一个游戏还是娱乐体验?您希望用户有什么感受?他们在玩游戏或参与体验时会做什么?同样的问题也适用于电影式虚拟现实-这个体验是关于什么的?您想要讲述什么故事?花一点时间写下来。

如果您正在制作一个学习体验,用户需要学到什么?最好的教学方法是什么?

如果您正在制作建筑或设计可视化应用程序,对最终用户来说什么最重要?建筑师或工程师可能希望能够查看墙壁和结构内部,以查看电气和管道设计,而房地产买家可能更关心空间中的光线质量。

弄清楚您的用户是谁,对他们来说什么最重要,并明确您想要创造什么以及对您来说什么最重要。这应该在纸上完成。模糊的设计元素很容易隐藏在您的心智模型中,只有在您开始将它们写下来时才会暴露出漏洞或意外的问题。

这适合虚拟现实吗?为什么?

一旦您明确了 VR 项目的设计意图,几乎下一件事就是考虑它在 VR 中的适应性。

从 VR 所能提供的功能角度来思考你的项目。它是否依赖于沉浸感和强烈的存在感来运作?它是利用 VR 模拟身体或为信息提供背景的能力吗?为什么你的项目在 VR 中比在平面屏幕上更好?你的用户可以做什么或体验到什么,而在传统媒体中无法实现?

同时也要考虑 VR 带来的挑战。正如我们在最佳实践中所提到的,VR 带来的挑战与传统媒体有很大不同。模拟器晕动症是一个主要问题 - 你的项目是否要求你以对用户来说不舒服的方式移动摄像机?它是否依赖于用户以在 VR 中可能困难或不可能的方式移动?你是否要求用户阅读大量可能在当前头戴式显示器上不可读的小字体?思考我们概述的最佳实践,并评估其中是否有任何挑战与你的设计相冲突。这并不一定意味着你的设计不能在 VR 中工作,但这意味着你需要进行一些额外的设计思考来解决这些挑战。

你选择将你的项目放入 VR 中应该是经过深思熟虑的。你应该能够描述为什么你的项目在 VR 中比在传统媒体中更好,并且你计划如何应对 VR 带来的挑战。这也应该以书面形式进行。你可能会发现一些之前没有意识到的机会,以及一些你需要克服的挑战。将它们写下来将帮助你理解你的项目的重要性和成功所需的步骤。

对于项目的工作来说,什么是重要的 - 什么是必须存在的?(MVP)

现在,你已经明确了你的项目面向的用户,你打算做什么,以及为什么在 VR 中进行这个项目是有意义的,你准备开始弄清楚构建它所需的真正步骤。以最小可行产品(MVP)的方式来弄清楚这一点是有帮助的。简单来说,MVP 是产品的一个版本,它只包含满足其意图所需的内容。例如,一个建筑可视化项目需要将观众放入正确比例的建筑物中,并给用户一些移动和从不同角度观看的方式。你作为设计师可以选择 MVP 的内容,但你应该清楚你所说的东西是你需要的还是你想要的。如果项目如果不能包含某个特定功能而就不值得做,那么它就是一个必需的功能,应该包含在你的 MVP 中。如果它可以提高体验,但用户仍然可以在没有它的情况下得到他们所需的,那么它不是 MVP 的一部分。

MVP 是指项目的一个版本,它只包含满足其目标所需的内容,几乎没有其他内容。明确你的 MVP 可以帮助你理解你的项目的核心是什么,告诉你应该优先考虑什么,并为你评估你的项目是否成功地实现了它的目标提供一个基准。

不同类型的项目的 MVP 内容会有很大的不同 - 电影式 VR 体验的需求与工程可视化应用的需求大不相同,但作为设计师,你应该知道它们是什么,并将它们写下来。你不需要在这里写一本书或一篇文章 - 一个项目要点的列表应该足够了,但对于列表上的每一项,问问自己,如果没有它,项目是否仍然能够实现它的目标,并明确你的答案。想要,即使是强烈的想要,也不是需求。这里的重点是知道你的底线在哪里。

同时也要留意你可能错过的事情。想象一下你的用户在使用你的项目时,从一刻到另一刻,从他们启动应用程序的那一刻到关闭它的那一刻,他们想要做什么?利用这个练习来发现你可能错过的项目,并确定它们是想要还是需要的,并将它们加入到清单中。

拆解它

如果你已经完成了前面的练习,你应该清楚你的项目是为了什么,为什么它在 VR 中有效,以及为了使其有效而需要什么。现在你可以开始思考如何实现它了。

对于你的 MVP 中的项目,你需要什么来使它们存在?你需要一个 UI 元素来向用户显示信息吗?你需要一种让用户移动的方式吗?用户需要能够加载或保存信息,或者连接到服务器吗?

对于清单中的每一项,弄清楚那项实际上需要你构建什么,并将其写下来。通过这个练习,你应该能够清楚地拆解出你需要做的事情。

拆解是一个列出你需要做或构建的事情的清单,以便完成你的项目。将其作为一个工具,以确保你没有错过必需的元素,或低估了风险,并查看你尝试构建的项目是否在你拥有的时间和资源内实际可行。这是一个早期发现问题的工具,当你还有机会修复它们时,然后在构建过程中跟踪你的进展。

浏览一下这个清单——哪些是重要的任务,哪些是重大风险?你能够在你拥有的时间和资源内完成所有这些吗?如果范围开始看起来太大,你需要重新评估吗?请记住,做好少数事情通常比试图做所有事情并且做得不好要好得多。在这个阶段,发现项目范围超出你实际能够做好的范围是很常见的,这是一件好事。现在发现这一点的时机是最好的,因为它还只是纸上的计划,你可以重新组织工作,将项目从 MVP 中移除,或者改变你的进度或资源。如果你在开始阶段发现这些问题,你有机会解决它们,而如果你在软件开发进行了几个月后才发现这些问题,你可能会发现自己陷入了困境。在你还有灵活性的时候,通过纸上的计划为自己的成功做好准备。

按正确的顺序解决问题

拆解清单中的一些项目会比其他项目更容易完成,也会更有趣。当你确定应该按照什么顺序做事情时,要根据自己的判断。一般来说,先解决风险较高的事情是个好主意。如果某个事情足够重要,存在着可能会延期或根本无法完成的风险,通常最好是尽早解决它。这样做可以让你有时间在处理其他事情的同时迭代一个风险项目,或者在最坏的情况下,如果你发现一个你依赖的事情无法完成,你仍然处于项目的早期阶段,你可能能够退回到另一个计划。不要把高风险、高优先级的事情留到最后,如果出现问题,你会陷入困境。

寻找项目之间的依赖关系。如果某件事在另一件事之前无法完成——例如,一个角色在构建和装配之前无法进行动画,那么确保这些依赖关系纳入你的计划中。计划按照一定的顺序进行事情是没有好处的,如果发现某个事情所依赖的东西还没有准备好,你就无法按照计划进行。

在计划如何完成你的分解时,寻找涉及风险或不确定性的事项,需要很长时间的事项以及依赖于其他事项的事项。将这些因素纳入你的计划中。一般来说,如果可能的话,尽早在项目中处理重要性高、风险高的工作,这样如果出现问题,你就有时间处理。

关于项目管理的一点说明——关于规划和跟踪项目的文献很多,深入讨论超出了本书的范围。广义上来说,这些可以分为两个主要的思维流派:瀑布式和敏捷式。瀑布式项目管理方法按照一定的顺序列出任务,假设一项任务完成后,下一项任务就可以开始。这在做事情明确定义且风险不大的情况下效果很好,比如粉刷房子,但 VR 设计和开发很少能按照这种方式进行。你可能根本不知道一个功能是否完成,直到你看到它与其他系统一起运行,然后你可能需要回头改变或完全重做一些东西。敏捷方法,比如 Scrum,考虑到了这一现实,并适用于设计和开发项目,因为在项目发展过程中会需要重新审视并获得新的信息。总的来说,敏捷方法在软件开发方面比瀑布计划更有效。

根据项目的范围,你可能不需要应用正式的项目管理方法,但即使你的计划不太具体,仍然应该有一个计划,并确保计划适应你需要回头迭代功能和设计的现实,有些事项会依赖于其他事项,有些事情会比你想象的时间更长。

尽早、经常进行测试。

尽早测试你的设计。尤其是 VR 是一种非常新的媒介,人们对它的反应各不相同。尽早测试,尽可能多地测试不同类型的受试者,这样你就可以在相对容易更改的时候发现需要改变的事物。

还要记住,VR 开发者不适合作为测试对象。我们比其他用户更频繁地使用 VR,并且对 VR 界面更加熟悉,对模拟器晕动症的抵抗力也更强。与对这种媒介不熟悉的用户以及对这种媒介感到舒适的用户一起进行 VR 测试。

尽可能多地测试不同类型的人群。VR 以前的媒介无法像 VR 那样将用户融入其中,这对你的用户很重要。对你来说看起来很好的手可能对手型不同的用户来说感觉很陌生。确保你的测试人群不仅限于和你相似的人。

尽早在流程中寻找测试机会。即使在达到 MVP 之前,也要测试可能需要设计迭代的元素,比如运动系统。将用户置于一个灰盒环境中,并让他们在其中导航,观察他们的行为和困惑的地方。你做的测试越多,你的项目就会越好,而且你越早测试,就越容易根据你所学到的知识采取行动。

设计是迭代的。

很多人认为成品是由天才设计师或开发者的头脑中完整形成的。事实并非如此。任何值得制作的东西都需要经过迭代才能完成。

现在要为你的设计的第一次迭代做好准备,它不会完全符合你的期望,这就是重点。任何事物的第一稿的目的是展示你正在构建的事物中真正重要的内容以及它们如何相互配合。要为此做好计划。设计是一个过程,时间和迭代是这个过程的关键元素。

这就是为什么我们强烈建议首先在纸上进行设计,并尽早在软件中测试原型。每次您给自己一个有形的东西来回应,您都会发现一些关于它的东西,并可能发现一种改进的方法。

总结

在本章中,我们探讨了 VR 是什么以及它在现实世界中的一些应用方式。我们对沉浸感和存在感进行了相当多的讨论。让我们在这里简要回顾一下。

我们说过,存在感是一种生理上的感觉,即存在于某个地方,这实际上是 VR 的目的。我们创造 VR 是为了创造存在感。沉浸感是实现存在感的手段,它涉及完全占据用户的感官,以至于他们开始相信周围的虚拟世界。

我们讨论了一些当前被广泛认可的创建优质 VR 的最佳实践。其中最重要的是尽可能降低延迟,并且需要非常小心地处理用户视角的移动。模拟器晕动病主要是由于视觉感知的运动与内耳感觉的缺乏之间的冲突。为了让用户在体验中感到舒适,我们需要分解运动并了解最容易引发模拟器晕动的运动类型。我们还谈到了安全性,需要注意您要求用户执行的运动类型,避免眼部疲劳,并小心触发光敏性癫痫。

最后,我们概述了一个规划 VR 项目并在设计上进行迭代的过程,以使项目达到您的预期并确保其成功。

在下一章中,我们将深入研究并开始使用虚幻引擎,从此以后,本书的其余部分将是实践操作。我们希望本章中概述的思想能够伴随您的发展,并帮助您成功,不仅仅是制作运行的 VR 应用程序,而是制作出优秀的应用程序。

现在,让我们开始工作吧。

第二章:设置开发环境

本章的目标是让您准备好在虚幻引擎中进行开发。即使您已经安装并开始在引擎中工作,您可能仍然会发现浏览本章是有价值的,因为安装过程中有一些细节可能对您有用。

我们还将研究 Epic Games 启动器。习惯上,我们只将其视为更新引擎和启动项目的方式,但那里还有大量有用的学习和开发资源。忽视它将是一个错误。

对于计划在 Oculus Go 或三星 Gear 上开发移动 VR 的人,我们将为您介绍设置 Android SDK 和设置项目以部署到设备的过程,对于那些对 C++开发感兴趣的人,我们将向您展示如何设置 Visual Studio 2017 以与虚幻引擎一起使用,对于那些对最前沿技术感兴趣的人,我们将向您展示如何下载虚幻引擎源代码并自行构建。

在本章的过程中,我们将学习以下主题:

  • 使用 Epic Games 启动器安装虚幻引擎

  • 设置开发环境以构建移动 VR 项目

  • 了解更多关于虚幻引擎的信息,以及获取帮助的途径

  • 设置开发环境以构建 C++项目

  • 从源代码下载和构建虚幻引擎

先决条件-VR 硬件

如果您计划开发桌面 VR 硬件,例如 Oculus Rift 或 HTC Vive,我们假设您已经设置好了头显并确保其正常工作。如果还没有,请现在进行设置。前往www.vive.com/eu/setup/www.oculus.com/setup/,并按照指导进行安装和设置操作。

请记住,使用头显时,您的 VR 头显驱动程序软件(Oculus Home 或 Steam VR)需要运行。

如果您计划开发独立的移动 VR 应用程序,您的设置过程将涉及一些其他步骤,我们将在安装虚幻引擎后为您介绍。我们建议即使对于那些开发移动 VR 的人,您也应该准备一个桌面 VR 头显。能够直接将软件启动到头显中,而无需每次都进行烹饪和部署,可以大大加快调试速度。这不是必需的,但您会发现它很有帮助。

无论如何,测试一下您的头显,确保它正常工作,然后让我们准备好开发环境。

设置虚幻引擎

如果您要使用虚幻引擎开发 VR 应用程序,首先需要的当然是引擎。让我们一起来设置它。

费用

在考虑虚幻引擎时,一个自然的问题是费用。好消息是,虚幻引擎可以免费下载和使用,如果您在商业上使用它,条款也是合理的。

当您下载引擎时,将要求您同意两个许可协议之一,具体取决于您将用它做什么。如果您是游戏开发者,并使用虚幻引擎制作游戏或应用程序并出售,您将按照每个日历季度超过 3,000 美元的总销售额支付 5%的版税。如果您不出售游戏或应用程序,或者每个季度的收入低于该金额,可以免费使用虚幻引擎。

如果您使用 Unreal 进行的工作不打算向公众销售(培训模拟、建筑可视化或其他任何内容),根据企业许可协议的条款,Unreal 完全免费。对于大多数企业来说,标准的企业最终用户许可协议(EULA)就足够了,但如果您确实需要进行更改,您可以联系 Epic 在此处设置具有不同条款的企业许可证:www.unrealengine.com/en-US/enterprise/contact-us。 Epic 将与您合作。

目前的情况是,您可以免费下载 Unreal 并使用它,如果您开始用它赚钱,条款是合理和清晰的。

顺便提一下,值得一提的是,当您下载引擎时,您获得的 Unreal 版本与专业开发人员使用的版本相同,包括 Epic 的开发人员。没有“专业”版本和其他版本之间的分割:一切都包含在内,一切都开启。

创建 Epic Games 帐户

那么,让我们开始吧。我们将首先前往www.unrealengine.com并点击下载链接。如果您已经在 Epic 创建了一个帐户,请在此处登录。如果没有,请现在创建一个。

注册或登录后,您将被要求同意您需要同意的许可证-游戏开发者许可证或企业许可证。选择适合您情况的许可证。接下来,选择您要下载的 Windows 或 Mac 版本,下载适当的 Epic 安装程序并运行它。

这将安装 Epic Games 启动器,您将使用它作为管理引擎版本、插件、库内容和学习资源的中心。这里有一些有用的东西。

Epic Games 启动器

下载并安装启动器后,打开它。它会要求您使用刚刚用于登录 Epic 网站的相同帐户进行登录。(启动器也可以离线使用,所以即使没有互联网连接,您仍然可以运行引擎,但是如果您在线上,当然会有更多有用的东西可供您使用。)

登录后,查看启动器左侧边缘的选项卡集。有一个用于 Unreal Engine 的选项卡,然后是一系列用于 Epic 商店、游戏库和好友的选项卡。选择 UNREAL ENGINE 选项卡。我们将在这里花费所有的时间:

Epic Games 启动器的版本为 4.22;其布局经常变化,但原则保持不变。

在 Unreal Engine 选项卡的顶部,您会找到四个额外的选项卡:

  • 虚幻引擎

  • 学习

  • 市场

我们将在一会儿查看这些选项卡,但首先,请找到它们右侧的 Install Engine 按钮。默认情况下,此按钮会安装引擎的最新稳定版本。让我们这样做。

安装引擎

当您点击安装按钮时,如果尚未选择 Library 选项卡,启动器将切换到 Library 选项卡,并要求您选择安装位置。默认位置通常是一个不错的选择,但如果您想在其他位置安装引擎,可以浏览到新位置。

此页面还有一个选项按钮,我们应该花点时间讨论一下它提供的选择:

安装选项允许您确定在您的计算机上设置哪些 Unreal Engine 组件。

  • 核心组件必须安装-这是运行编辑器所需的最低要求。

  • 入门内容包括一些有用的资产,供您开始使用,包括一些材料和模型以及高级照明贴图。我们将在本书的项目中使用这些资产,所以您应该安装它。

  • 模板和功能包为你提供了一系列优秀的项目,可以作为你的游戏项目的起点,包括 VR 模板,在本书中我们将在几个项目中使用它。你也应该安装这个。

  • 引擎源码是使虚幻引擎与其他引擎不同的一点:虚幻引擎为引擎提供了完整的 C++源代码。这是学习 C++的好方法,当你真正需要了解某个功能的工作原理或者需要弄清楚为什么某个功能的行为出乎意料时,它可以帮你解救。你不需要安装引擎源码,是否安装取决于你自己,但它不会占用很多空间,所以没有理由不安装。如果你预计要进行任何 C++开发,你应该安装它。一旦安装完成,你会在安装引擎版本的目录下的\Engine\Source文件夹中找到源代码。

  • 调试时需要编辑器符号,如果你计划在 C++中进行调试。如果没有它,你将无法在引擎源代码中设置断点或跟踪执行过程。不过,这些编辑器符号占用了很多空间,所以如果你不打算使用 Visual Studio 进行 C++开发或调试,你可以跳过它,如果你意识到需要它,随时可以安装它。

这些选项在安装引擎版本后可以更改,所以如果你改变主意是否要安装某个选项,这不是个问题。你可以随时添加或删除任何选项。此外,如果你保留了旧版本的引擎,开发人员通常会这样做,如果他们在维护一个遗留项目,使用选项卸载除了核心组件以外的所有内容可以节省空间。

对于本书中的项目,默认选项是可以的——核心组件、起始内容、模板和引擎源码。如果你预计要进行 C++开发或调试,请安装编辑器符号。

在设置完选项后,点击应用并安装引擎。这需要一些时间。(如果你想在等待期间跳到了解虚幻部分,你可以在安装完成后回到这里。)

编辑 Vault 缓存位置

根据你的系统设置,你可能想要更改虚幻存储 Vault 缓存的位置。Vault 缓存存储了你从市场下载的资产,如项目和资产包。默认情况下,它位于C:\Program Files (x86)\Epic Games\Launcher\VaultCache。你应该知道它可能会变得非常大,所以如果你的系统驱动器空间不足,你可能想把它放在其他地方。

如果你想这样做,从 Epic Games 启动器中,选择设置|编辑 Vault 缓存位置,选择一个新位置,然后点击应用。然后,退出设置并退出 Epic Games 启动器(在系统托盘中找到其图标,右键单击它,选择退出——仅仅关闭启动器窗口会将其最小化而不是退出)。当你重新启动启动器时,它将在新位置创建缓存。记得删除旧位置的VaultCache目录。(虽然你可以将缓存复制到新位置,但通常最好强制系统创建一个新的缓存,因为这样可以消除你可能不再使用的很多残留文件。)

设置派生数据缓存(DDC)

还有一个额外的设置我们建议你做。当你使用编辑器时,虚幻会将资产编译成适用于本地机器硬件的形式。与其每次都强制引擎这样做,不如给它一个地方来存放这些已编译的资产,这样在第一次构建之后,所有东西加载得更快。

你不一定要这样做,但这是个好主意。特别是材质,如果你这样做,编译速度会快得多。如果你看到以下消息,你肯定要设置一个 DDC:

如果您看到此警告,请确保按照此处的指示设置您的 DDC。这将产生很大的影响。

虚幻称此功能为共享数据缓存SDC)或派生数据缓存DDC)。(这是一个与刚才提到的保险库缓存不同的缓存。)DDC 中的所有内容都是生成的,这意味着随时可以清除其内容。新数据将生成在其位置上。如果更改了您的显卡,最好清空您的 DDC,因为它将包含为旧卡编译的大量资源。

在设置 DDC 时,您有两个选项:如果您在工作室环境中工作,可以在网络可访问的位置设置一个共享 DDC。要做到这一点,请按照此处的说明操作:docs.unrealengine.com/en-us/Engine/Basics/DerivedDataCache

我们要讨论的是另一种选择:为独立开发设置本地 DDC。如果您所在的工作室已经设置了共享 DDC,您可以跳过本地设置。

设置本地 DDC

打开 Windows 控制面板 | 系统和安全 | 系统,然后点击高级系统设置链接:

您还可以通过在任何 Windows 资源管理器窗格上右键单击“此电脑”,然后选择“属性”来到这里。

在系统控制面板的左侧找到高级系统设置链接

在高级系统设置窗格中,点击环境变量按钮:

环境变量按钮位于系统属性 | 高级中。您需要管理员权限才能进行编辑。

在出现的编辑环境变量对话框中,点击新建以在用户变量或系统变量部分创建一个新的系统变量。(如果使用前者,DDC 将适用于您的登录,但不适用于其他登录到同一台机器的用户。如果将变量放在系统变量中,它将适用于所有用户。)

将 UE-SharedDataCachePath 输入为变量名,并为其值浏览到您想要存储派生数据的目录。如果您正在构建具有大量艺术内容的项目,您的 DDC 可能会占用超过 10 GB 的空间,因此请将其放在有足够空间的驱动器上:

创建一个名为 UE-SharedDataCachePath 的变量,并将其设置为您想要存储 DDC 的位置。

点击确定保存。在此生效之前,您需要重新启动计算机

如果您的 DDC 开始积累许多您不再使用的项目的杂散资源,或者如果您更改了视频硬件,可以安全地清除其内容;编辑器将重新生成缓存。

启动引擎

引擎安装完成后,让我们启动它以验证一切是否正常工作。

在 Epic Games 启动器的左侧点击启动按钮,或在库选项卡的 ENGINE VERSIONS 中点击启动按钮:

库选项卡显示已安装的引擎版本、项目、插件和资产包。

如果您以前从未在计算机上启动过虚幻引擎,它可能会要求您允许安装一些先决条件。请允许。引擎还可能要求获得通过 Windows 防火墙进行通信的权限。也请允许。

如果一切正常运行,您应该会看到一个类似于这样的窗口:

未指定要加载的项目时,每次启动引擎时都会出现虚幻项目浏览器。

让我们创建一个空的蓝图项目,只是为了确保一切正常工作。(我们将在下一章中深入讨论创建项目的内容,但现在我们只是想测试一切。)

选择新项目选项卡。在蓝图选项卡下,选择空白,并将所有选项保持默认。选择一个合理的位置,然后点击创建项目。

编辑器应该打开您的新项目,您应该准备好了。

如果您正在为桌面 VR 开发(而不是移动设备),让我们进行一个快速测试以确保一切正常。如果您正在为移动 VR 开发,我们将在下一节中介绍。

在编辑器工具栏中,找到播放按钮右侧的下拉菜单。将其拉下并选择 VR 预览:

一旦您选择了播放模式,这将成为播放按钮的默认行为,直到您更改它。

如果 VR 预览被禁用,请检查您的头显是否正确连接,并且 Oculus 或 Steam VR 软件是否正在运行且没有显示任何警告或错误。

一旦您在 VR 中启动,您应该在头显中看到您的场景。它可能不是世界上最令人兴奋的场景,而且您会意外地漂浮在地板上方(我们将在下一章中学习如何正确设置 VR 场景),但您应该在其中。恭喜!一切正常!

为移动 VR 做准备

移动 VR 头显(如三星 Gear 和 Oculus Go)是与您的 PC 分开的设备,因此您不能像使用桌面头显那样简单地进入 VR 预览。相反,您需要打包项目并将其部署到设备上,以便直接在头显上运行。您需要设置一些东西才能实现这一点。

创建或加入 Oculus 开发者组织

首先,如果您要开发基于 Oculus 的移动 VR 平台,您需要在 Oculus 上注册为开发者。我们假设您已经创建了一个 Oculus 账户,因为您必须这样做才能使用头显。如果您还没有这样做,请先注册并登录。

现在,导航到dashboard.oculus.com/organizations/create/并按照注册开发者的步骤进行操作。如果您加入的是现有组织而不是创建自己的组织,请联系管理员将您添加到注册开发者列表中。

在 Oculus Go 上将您的 VR 头显设置为开发者模式

一旦您注册为开发者,您将能够使用 Oculus 移动应用程序将您的头显设置为开发者模式。在将自己的项目部署到设备之前,您需要这样做。

在应用程序中,导航到设置 | [您的头显] | 更多设置 | 开发者模式,并打开开发者模式。

如果您无法执行此操作,请确认您的 Oculus 账户是否与开发者组织相关联。

安装 Android 调试桥(ADB)

三星 Gear 和 Oculus Go 都运行在谷歌的 Android 操作系统上。您需要安装驱动程序才能让您的 PC 与 Android 设备通信。为此,我们将安装 Android 调试桥(ADB)驱动程序。

导航到 ADB 2.0 下载页面,网址为developer.oculus.com/downloads/package/oculus-go-adb-drivers/,下载并解压.zip文件,然后右键单击android_winusb.inf并选择安装。

有关 ADB 以及如何使用它与 Oculus Go 和三星 Gear 头显进行通信的更多信息,请参阅:developer.oculus.com/documentation/mobilesdk/latest/concepts/mobile-adb/#mobile-android-debug-intro

让安装完成,然后我们将安装 Android SDK。

设置 NVIDIA CodeWorks for Android

为了开发 Android 软件,您需要安装一些软件开发工具包SDK)和其他资源,并将它们配置为相互配合。幸运的是,使用 NVIDIA 的CodeWorks for Android安装程序可以轻松完成此操作。

Epic 在您的引擎安装中包含了所需的安装程序。导航到您安装 Unreal Engine 的目录,找到Engine\Extras\AndroidWorks\Win64。运行找到的 CodeWorksforAndroid 安装程序:

C:\Program Files\Epic Games\UE_4.21\Engine\Extras\AndroidWorks\Win64\CodeWorksforAndroid-1R6u1-windows.exe

接受默认选项,并在完成后重新启动计算机。

验证 HMD 是否能与您的 PC 通信

在计算机重新启动后返回,我们要检查您的 PC 是否能与 Android 头戴设备通信。

导航到刚刚安装 Android SDK 的位置。默认情况下,这将是C:\NVPACK\android-sdk-windows。查找platform-tools目录。

在此目录中,Shift +右键单击以打开包含Open PowerShell 窗口命令的上下文菜单。如果您在不按住Shift的情况下右键单击,则上下文菜单不会包含 PowerShell。如果您使用的是较旧版本的 Windows 10,或者已禁用 PowerShell,则Shift +右键单击将打开一个命令行。

在 PowerShell 中,键入./adb devices

如果您使用的是 PowerShell,则必须在启动程序之前加上./。 (在 Unix 系统上,要求在可执行调用之前加上./是一项安全功能,以防止您意外启动可执行文件。Windows 现在也遵循此约定。)如果您使用的是传统命令提示符,则只需键入可执行文件的名称:adb devices。最好养成使用 PowerShell 而不是传统命令提示符的习惯。它更安全,而且功能更强大。

看一下下面的屏幕截图:

adb devices 命令列出当前连接的 Android 设备。

如果 Go 或 Gear 显示为未经授权,这意味着您的 PC 能够看到它,但头戴设备尚未允许 PC 与其通信。戴上头戴设备并接受应该出现的确认对话框。再次运行adb devices并确认头戴设备现在显示为设备。

为三星 Gear 生成签名文件

您无需创建签名文件即可部署到 Oculus Go 或 Quest。

对于三星 Gear 设备,您需要创建一个Oculus 签名文件osig)。

按照dashboard.oculus.com/tools/osig-generator/上的说明,将生成的文件放置在 Unreal 安装目录下的\Engine\Build\Android\Java\assets目录下。如果 assets 目录尚不存在,请创建它。

有关签名文件的更多信息,请查看此处:developer.oculus.com/documentation/mobilesdk/latest/concepts/mobile-submission-sig-file/

将测试项目部署到设备

现在,我们已经安装了所有必需的软件并验证了我们的 PC 能够看到我们的 Android 头戴设备,让我们创建一个项目并将其部署到设备上,以确保一切正常工作。

设置一个测试项目

从 Epic 启动器中启动 Unreal Engine,在项目浏览器中选择新项目。选择蓝图选项卡,空白模板,并将项目设置为 Mobile/Tablet,可扩展的 3D 或 2D,无起始内容。选择项目的位置并创建它:

您在此处设置的选项将确定项目的起始设置,但您可以稍后更改它们。

检查您的 OculusVR 插件是否已启用

项目启动后,选择“设置”|“插件”|“虚拟现实”,并验证“OculusVR”插件是否已启用。(它应该已经启用了。)

设置默认地图

由于我们将在 Gear 或 Go 上作为独立可执行文件运行此项目,我们需要告诉它在启动时打开哪个地图。保存编辑器启动时创建的“空地图”,并给它任何你想要的名字。

选择“设置”|“项目设置”|“项目”|“地图和模式”,并将刚保存的地图设置为“编辑器启动地图”和“游戏默认地图”。

清除默认的移动触摸界面

通常,移动应用程序假设您将触摸屏幕来操作它们,但当然在您的头盔中不会发生这种情况,所以我们需要从项目中清除这个默认设置。

从“项目设置”中,选择“引擎”|“输入”|“移动”,并从“默认触摸界面”下拉菜单中选择“清除”,将其设置为“无”。

设置 Android SDK 项目设置

现在,我们需要为我们的 Android 头盔配置项目。

在“平台”|“Android”|“APK 打包”下,点击“立即配置”,并接受 SDK 许可证(您只需要接受一次许可证):

点击“立即配置”按钮将在项目的 Build/Android 目录下写入一个 project.properties 文件。

我们还需要在这个类别下设置一些设置:

  • 最低 SDK 版本:21

  • 目标 SDK 版本:21

  • 在 KitKat 及以上设备上启用全屏沉浸模式:True

您会看到一些旧的文档告诉您将最低和目标 SDK 版本设置为 19。这对于三星 Gear 是正确的,但对于 Oculus Go,您必须选择版本 21。

向下滚动到“高级 APK 打包”部分,并设置以下内容:

  • 将 AndroidManifest 配置为部署到 Oculus Mobile:True

旧的步骤将把这个设置称为“配置 Android 清单以部署到 Gear VR”。它的名称已经改变了。

设置 Android SDK 位置

现在,选择“平台”|“Android SDK”,并设置以下内容(根据您安装 SDK 的位置进行调整):

  • Android SDK 的位置:C:/NVPACK/android-sdk-windows

  • Android NDK 的位置:C:/NVPACK/android-ndk-r12b

  • ANT 的位置:C:/NVPACK/apache-ant-1.8.2

  • JAVA 的位置:C:/NVPACK/jdk1.8.0_77

  • SDK API 级别:latest

  • NDK API 级别:android-21

参考以下截图:

确保你在这里指定的目录是你驱动器上实际存在的位置。

请注意,当您更新 Android SDK(每次更新引擎版本时都必须记住这样做)时,这些目录名称将会改变。在更新后确保您为每个目录指定了正确的目录,否则您将遇到一些令人难以理解的错误。

启动测试项目

关闭项目设置,并找到“启动”按钮旁边的下拉菜单。打开它,您应该能够看到您移动 VR 头盔的序列号:

这里列出的设备将根据您的项目支持的平台和找到的设备而有所不同。

选择要启动的头戴设备。编辑器在准备好之前短时间内变得无响应是完全正常的。请耐心等待。一旦编辑器再次响应,您应该会看到类似以下的内容:

Android 资源处理进度指示器

选择“窗口”|“开发者工具”|“输出日志”不是一个坏主意,这样你就可以看到它在做什么,但这不是必需的。点击“显示输出日志”链接将会做同样的事情。

养成观察输出日志的习惯。很多开发者忽视了这一点,但你不应该。通过观察日志,你可以了解引擎正在做什么。

第一次运行时需要一些时间,因为需要编译很多着色器。后续运行将更快。

资产编译完成后,虚幻将把它们复制到您的设备上:

部署时间可能会因需要传输的数据量而有所不同。

部署完成后,场景应该在您的设备上运行:

一旦此对话框指示项目在设备上运行,您就可以准备测试它了。

戴上头盔,您应该能够进入场景。恭喜!您刚刚将项目部署到了移动 VR 头盔上!

使用 Epic Games 启动器

在我们继续之前,让我们先看一下 Epic Games 启动器-这里有很多有用的材料,这应该是您学习的起点。花些时间四处看看,看看有哪些资源可用。很容易忽视这些资源,但如果您习惯了了解在需要时可以学习和找到信息的地方,您将能更快地取得进展。

启动器的虚幻引擎选项卡分为四个主要部分:

  • 虚幻引擎

  • 学习

  • 市场

让我们逐个查看它们。

虚幻引擎选项卡

虚幻引擎选项卡显示了特色内容和项目,这些内容在您使用引擎的时间越长,对您来说就越有意义。作为新用户,特别注意主横幅下面的一排图标。这些是宝贵的资源:

虚幻引擎选项卡截至虚幻 4.22 版本

  • 新闻链接是了解最新情况的好方法;它主要关注新功能、事件和引擎的有趣用途。随着您在引擎中花费的时间越来越多,这个链接对您来说会变得更有意义。

  • YouTube 链接将带您进入虚幻引擎的 YouTube 频道。这是寻找深入教程、功能亮点和项目重点的最佳地方之一。特别是在功能亮点视频中,有很多信息是您在其他地方找不到的。

  • AnswerHub 是开发者提问和回答问题的重要资源。几乎任何时候您有问题,都应该首先在这里搜索答案。您很有可能会找到您要找的内容。在提问之前,不要害羞,但请尽量搜索现有的问题和答案,以免重复提问。同时,尽量回答您知道答案的问题,这就是社区的运作方式。

  • 论坛是讨论与引擎相关的所有主题的地方,也是了解最新情况的好地方。大多数插件开发者也在论坛上与用户保持联系。这里有一个专门讨论 VR 和 AR 开发的论坛:forums.unrealengine.com/development-discussion/vr-ar-development

  • 路线图链接将带您进入一个 Trello 页面,描述即将发布的内容以及更远的未来计划。在您的虚幻开发生涯早期,这可能对您来说并不那么有意义,但随着您深入了解引擎,即将到来的变化将变得重要起来。

学习

这是启动器上最重要的资源之一。您不会后悔在这里花费的时间。

让我们首先看一下顶部栏:

学习选项卡截至虚幻 4.22 版本

  • 文档链接将带您进入虚幻引擎的文档主页docs.unrealengine.com/en-us。文档页面上的“开始使用 UE4”链接是学习艺术、关卡设计和编程流程基础知识的好地方。如果您是虚幻引擎的新手,我们建议您先学习这些基础知识,以便熟悉编辑器。如果您已经掌握了这些基础知识,那么在构建本书中的项目时,您将会更好地完成,并且能够更好地利用它们。在掌握了基础知识之后,您可以将这个文档页面视为您在引擎中使用新工具或系统时的标准参考。

  • 视频教程链接将带您进入academy.unrealengine.com/,这是一个在线学习网站,提供大量关于特定行业、角色、工作流程和概念的详细视频教程。这些课程非常有价值,是了解引擎不同部分如何相互配合以满足您需求的好方法。

  • 社区维基页面比其他页面更少用。正如我们之前提到的,该页面上的内容不能保证是最新的,甚至是正确的。了解它的存在是值得的,但通常在论坛和文档中搜索信息比在维基上搜索更好,因为论坛上的错误信息通常会被其他用户迅速纠正,而在维基上则可能滋生。

内容示例项目

在这个栏下面,我们有一些特色链接,可以快速开始指南和博客文章,以及一些引擎功能示例。其中最重要的是内容示例项目。这里的所有项目都值得查看特定主题,但内容示例应该是您的常规参考。现在让我们安装它。

点击内容示例项目以打开其详细页面,并点击页面上的“创建项目”链接:

内容示例项目详细页面

系统会询问您想要将项目放在哪里,以及您想要使用哪个引擎版本来创建它。将其放在一个合理的位置(维护一个专门用于虚幻参考项目的目录是个不错的主意),并选择最新的引擎版本。点击“创建”来创建项目。项目创建后会自动启动,以后您可以从库选项卡的“我的项目”部分访问它。让我们允许项目启动,或者在创建后从库选项卡中特定地启动它:

启动项目时,请注意虚幻编辑器的版本号和加载进度。

项目第一次启动时,初始化可能需要一些时间。虚幻引擎正在为您的计算机构建资源。如果在 45%或 95%处卡住了几分钟,不要担心,它并没有崩溃。它正在构建动画、着色器和其他资源。后续启动将会快得多。

项目打开后,点击“文件”|“打开关卡”(或按下 Ctrl + O)打开其中一个演示地图。点击播放按钮,并使用标准的 WASD 键盘控制来移动和查看示例:

其中一个演示关卡包含在内容示例项目中

花些时间浏览这个项目,熟悉编辑器,并了解虚幻引擎的功能。以后,当您想在项目中添加某些内容时,习惯性地检查一下“内容示例”中是否有示例可以帮助您弄清楚如何完成。

说真的,这是您可以使用的最有用但经常被忽视的资源之一。在“内容示例”项目中有很多好东西。

游戏概念和示例游戏

在学习页面的底部,有一系列展示特定游戏玩法概念和示例游戏的项目。这些是学习更高级主题和了解虚幻引擎中完成项目的宝贵资源。这些项目的内容往往更加高级,因为它们大多代表了可发布的完成游戏。在下载并开始探索之前,您可能需要在引擎中花些时间。现在,您应该知道它们的存在。如果您感到好奇并且想要提前了解,可以随意浏览。

市场

在本书中,我们将使用市场中的免费资源,所以您应该花些时间查看这个选项卡:

截至 Unreal 4.20 的市场内容

Epic 在市场中提供了大量高质量的免费素材。通常,如果 Epic 取消了其内部开发的游戏,它会将游戏素材免费提供给市场。在构建学习项目时,要充分利用这些资源。无尽之剑的素材对于 VR 项目尤其有用,因为它们最初是为移动游戏设计的,所以对于 VR 的严格要求进行了合理的优化。在接下来的章节中,我们将看到如何将市场内容添加到现有项目中。同时,也不要忽视市场上的付费素材。其中很多素材都非常优秀,可以极大地帮助您构建项目,无论是用于原型还是发布的作品。

库选项卡是您维护已安装的引擎版本、打开项目以及访问从学习选项卡和市场下载的插件和内容包的地方:

截至 Unreal 4.20 的库面板

使用“ENGINE VERSIONS”部分来更新已安装的引擎版本、安装新的引擎版本以及修改其选项。

关于引擎版本的一点说明:如果您看到已安装的引擎版本上出现指示器,表示可以进行更新,那么您应该进行更新。引擎版本的更新,例如从 4.20.2 到 4.20.3,通常是安全的,因为它们涉及错误修复,不会以可能破坏项目的方式改变任何工作方式。

除了更新当前安装的引擎版本外,您还可以使用“ENGINE VERSIONS”标签旁边的“+”号来添加其他已安装的版本。这样可以安装旧版本,以便打开尚未更新到当前版本的旧内容,或者安装预览版本,以便测试即将发布的内容。

在使用预览版本时要小心。它们旨在让您提前了解,但不能保证稳定性。不要在预览版本上进行关键任务。请在发布版本上工作,并使用预览版本来查看即将到来的内容,或者了解在切换到新版本时可能需要更新项目的方式。

My Projects 部分允许你启动你的项目。项目缩略图上标有当前设置的引擎版本号。你可以通过启动新版本并将该项目打开到新版本中来更新项目的引擎版本。在这样做时,会弹出一个对话框询问你是否要复制项目或在原地转换。在原地转换是危险的;建议你在复制的副本上进行更新,以确保项目中没有任何冲突。(这就是为什么原地转换选项被隐藏在“更多选项...”链接下的原因。)如果你有一个非常旧的项目,落后几个版本,通常最好一次转换一个版本,而不是尝试跳过几个版本。这样做可能会成功,但结果完全取决于你尝试跳过的版本数量和项目中的内容:

转换项目对话框提供了几个选项,以确定你希望如何处理引擎更新。

你不必使用 Epic Games Launcher 来启动你的项目;你可以直接导航到保存项目的位置,双击.uproject文件来启动与其关联的引擎版本。

Vault 部分包含了你拥有的所有内容,包括学习项目、插件和内容包。你可以在现有项目中添加插件或内容,也可以在这里创建新项目。

大多数情况下,库选项卡将是默认选项卡,因为你将使用它来启动项目,但正如我们刚才提到的,不要忘记其他选项卡。

为 C++开发进行设置

这部分是完全可选的。本书中的项目都不需要你使用 C++进行开发,但是我们偶尔会强调一些原生代码的内容,以供那些对深入了解感兴趣的人参考。如果你不打算进行编码工作,或者如果代码页让你感到非常困惑,完全可以跳过这一部分和接下来的部分。

在开发虚幻引擎时,绝对不需要使用 C++。蓝图可视化脚本语言非常强大,几乎可以实现任何功能。大多数应用程序,包括相当高级的项目,都可以完全使用蓝图构建。许多新手虚幻引擎用户看到 C++支持后会担心必须学习这门语言才能使用引擎。其实不需要。(如果你有兴趣学习 C++,这也是一个很好的方式。)

还在这里吗?太好了。如果你计划使用 C++进行开发,首先需要一个编辑器和编译器来构建你的代码。这种应用程序被称为集成开发环境IDE)。在 Windows 上开发虚幻引擎 4.20 及更高版本时,应该使用微软的Visual Studio 2017VS2017)。Visual Studio 有几个版本,但是对于虚幻引擎的开发,你不需要任何专业版或企业版的功能。免费的社区版已经包含了你所需要的一切。

安装 Microsoft Visual Studio Community

前往Microsoft Visual Studio Community页面,visualstudio.microsoft.com/vs/community/,下载安装程序。运行安装程序时,你将看到几个选项:

Visual Studio Community 2017 设置对话框确定你的安装将配置为处理哪些语言和开发任务。

在工作负载选项卡下,选择使用 C++进行游戏开发,然后在右侧的摘要侧边栏上确保你已经选中了以下内容:

  • 虚幻引擎安装程序(必需)

  • Windows 10 SDK(必需,应该已经默认选中)

  • Windows 8.1 SDK(在 VS 2017 上是必需的,应该已经默认选中)

  • C++性能分析工具(可选但有用)

如果您要为三星 Gear 或 Oculus Go 开发,请确保还包括以下内容:

  • Unreal Engine 的 Android 支持(Gear 或 Go 开发所需)

这将安装 Java 开发工具包和您需要与 Gear 和 Go 通信的 Android 工具:

Visual Studio 2017 安装详细信息面板允许您确定已安装的选项。

通过设置这些选项,您已经告诉 Visual Studio 包括 C++语言支持,并包括运行和开发 Unreal 所需的支持文件。

这些设置很重要。Visual Studio 2017 不再自动假设您将进行 C++开发,因此在安装时需要选择要支持的语言。如果您后来意识到错过了某些内容,请使用添加和删除程序控制面板修改您的 VS2017 安装选项。

推荐设置

在开始工作之前,您可能需要在 Visual Studio 中进行一些更改。这些不是必需的,但可以使其与 Unreal 更好地配合。详细文档在此处:docs.unrealengine.com/en-us/Programming/Development/VisualStudioSetup。请按照此页面进行操作并进行推荐的更改。

以下是页面要求您进行的更改的快速概述:

  • 增加标准工具栏上的解决方案配置控件的宽度,因为 Unreal 解决方案配置名称可能太长而无法阅读。

  • 确保在标准工具栏上显示解决方案平台控件。默认情况下应该已经显示。

  • 确保在工具|选项|项目和解决方案|构建完成后始终显示错误列表已关闭。

  • 设置工具|选项|文本编辑器|C/C++|视图|显示非活动块为 False。

  • 确保在工具|选项|文本编辑器|C/C++|高级下,您的 Intellisense 选项未被禁用。旧的说明会告诉您禁用 Intellisense,因为它在处理 Unreal 的源代码时效果不佳。现在情况已经不同了,告诉您关闭它的说明已经过时。如果您过去关闭了 Intellisense,请现在重新打开它。

UnrealVS 插件

现在您已经安装了 Unreal Engine 并设置了 Visual Studio,我们将要安装 UnrealVS 插件到 Visual Studio,以简化您在使用 Unreal 时在 Visual Studio 中执行的一些常见任务。

安装 UnrealVS 插件

确保关闭 Visual Studio,并导航到您安装当前 Unreal Engine 版本的位置,在Engine\Extras下找到UnrealVS目录。打开与您的 Visual Studio 版本对应的目录(在我们的情况下是 VS2017),运行UnrealVS.vsix安装程序来安装插件。

对于 Unreal 4.20,将其安装在标准位置。例如,您可以在这里找到插件:

C:\Program Files\Epic Games\UE_4.20\Engine\Extras\UnrealVS\VS2017

打开 UnrealVS 工具栏

完成插件安装后,打开 Visual Studio,并在工具栏的空白区域右键单击以设置活动工具栏。打开UnrealVS工具栏:

在 Visual Studio 2017 中右键单击空白工具栏区域,可以选择可见的工具栏。

有关配置和使用 UnrealVS 的其他文档,请参阅此处:docs.unrealengine.com/en-us/Programming/Development/VisualStudioSetup/UnrealVS。通过安装并打开工具栏,您已经完成了一切所需的操作,但是浏览一下此页面可以了解 UnrealVS 为您提供了什么以及如何使用它。

Unreal 调试支持

在我们准备好之前,还有一件事情要做,那就是在 Visual Studio 中为 Unreal 安装一个调试支持文件。

导航到您的引擎安装目录,找到Engine\Extras\VisualStudioDebugging。在那里找到UE4.natvis文件并复制它。

将其粘贴到以下两个位置之一。

您可以将其安装到 Visual Studio 的安装位置,即以下路径(您需要在计算机上具有管理员权限才能执行此操作):

  • [VisualStudioInstallPath]\Common7\Packages\Debugger\Visualizers\UE4.natvis

  • 示例:C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Packages\Debugger\Visualizers

或者,您可以将其安装到个人的“文档”目录中。如果您在用户配置文件的“文档”目录中检查,应该会找到一个在安装 IDE 时自动创建的 Visual Studio 2017 目录。如果该目录中已经存在一个 visualizers 子目录,请将UE4.natvis粘贴到其中。如果没有,请创建该目录并将 natvis 文件放在其中。

  • [UserProfile]\Documents\Visual Studio 2017/Visualizers/UE4.natvis

  • 示例:D:\OneDrive\Documents\Visual Studio 2017\Visualizers

.natvis文件包含了帮助 Visual Studio 显示特定解决方案中定义的本地数据类型内容的指令。Unreal 定义了自己的自定义字符串类型(FString)、自定义数组类型(TArray)和其他许多类型。UE4.natvis告诉 Visual Studio 在调试时如何以可读的方式显示这些类型中包含的数据。

测试一切是否正常

现在,我们准备验证是否已正确设置所有内容。从 Epic Games 启动器中启动当前的引擎版本。在新项目选项卡下,选择 C++。

如果您看到以下警告,请确保您已安装 Visual Studio 2017,并且已选择使用 C++进行游戏开发,并选择了推荐的设置:

如果您看到这个警告,要么您还没有安装 Visual Studio,要么您还没有设置所需的安装选项。

如果您已经安装了 VS2017 并且看到警告,这意味着您缺少我们刚刚提到的所需选项之一。使用“添加或删除程序”控制面板修改您的 VS2017 安装,并添加这些选项。

如果您没有看到任何警告,那么您可以准备创建一个快速测试项目。让我们在 C++选项卡下选择一个基本代码模板,使用默认选项,并选择一个位置和名称:

创建 C++项目的方式与创建蓝图项目类似。

点击创建项目,并允许工具为您创建一个新项目。稍等片刻。如果您已经正确设置了所有内容,Unreal 编辑器应该会打开您新创建的项目,而 Visual Studio 2017 应该会打开新创建的项目解决方案文件。现在让我们关闭 Unreal 编辑器,然后从 VS2017 中构建和启动新项目,看看如何操作。

在 Visual Studio 的解决方案资源管理器选项卡中,找到游戏树下的新项目解决方案。右键单击它并选择设置为启动项目:

左侧的解决方案资源管理器显示了项目中包含的文件。右侧的工作区显示了当前加载文件的内容。

再次右键单击它,选择 UnrealVS Quick Build | Win64 | DebugGame Editor。您的项目应该开始构建。

在使用 C++开发虚幻时,您通常会使用两个解决方案配置:DebugGame EditorDevelopment Editor。Visual Studio 是所谓的优化编译器,这意味着它在编译代码时会对代码进行一些修改,以使其运行更快。这有一个优点,即您可以编写易于阅读的代码,而在编译后仍然可以快速运行,但实际上这意味着如果您调试开发构建,不是每个数据位都可见,因为某些变量已经被优化掉了。

调试构建会保留您编写的所有内容,因此运行速度会稍慢一些,但您可以看到每个变量包含的确切内容。大多数情况下,您将希望使用 Development Editor 配置。

您会发现,除了 DebugGame Editor 和 Development Editor 之外,还有 DebugGame 和 Development 配置可用。在编辑器中工作时,您不会使用这些配置;它们不包括编辑器,并且需要将您的内容烹饪成发布就绪的格式。(我们稍后会讨论烹饪。)

对于这个示例,我们选择了一个 DebugGame Editor 配置,这样您就有机会看到编译器构建了一个尚未构建的配置。

一旦您的项目构建完成,请检查输出。如果看起来像这样,您就可以开始了:

1>Deploying BasicCodeTestEditor Win64 DebugGame...
1>Total build time: 45.92 seconds (Parallel executor: 27.81 seconds)
1>Done building project "BasicCodeTest.vcxproj".
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

现在,在标准工具栏上,使用解决方案配置控件选择刚刚构建的调试编辑器配置:

解决方案配置控件确定要创建的构建类型。

按下F5,或选择调试 | 启动调试,从 Visual Studio 启动编辑器。如果一切设置正确,您的项目应该启动,并且您的 Visual Studio 窗口应该看起来像这样:

在 Visual Studio Community 2017 中加载的虚幻 C++项目。底部的橙色条表示项目正在运行,并且 Visual Studio 的调试器已连接到它。

恭喜!您现在已经设置好了使用 C++进行开发。

让我们来看一个快速示例,说明为什么这样非常有用。在您的虚幻编辑器中自动创建的默认场景中,从世界大纲中选择 Floor。右键单击它:

您可以直接从虚幻编辑器中打开 C++文件。

地板是一个静态网格演员。选择打开StaticMeshActor.h的选项。您将自动切换到 Visual Studio,并打开StaticMeshActor头文件:

如果您无法打开StaticMeshActor.h,请检查是否为您的引擎版本安装了引擎源代码。转到库选项卡,在引擎版本下找到您正在运行的版本,并从启动按钮右侧的下拉菜单中选择选项。如果您还没有添加引擎源代码,请添加。

StaticMeshActor 类的 C++头文件描述了该类并声明了其函数。

这是虚幻引擎的许多令人惊奇的事情之一 - Epic 为您提供了源代码 - 所有的源代码。对于编辑器中的任何对象、任何蓝图节点,您都可以查看其下面的源代码。没有黑盒子。再次强调,这绝不是您在虚幻中必须做的事情 - 文档非常好,但如果您面临一个谜团并且真的需要弄清楚发生了什么,能够阅读源代码可能会拯救您的生命。

从源代码构建虚幻

您绝对不需要下载源代码并从头构建引擎,几乎您在实际使用中需要做的任何事情都不需要。这一部分包含在这里,以便您有自由进行引擎更改,如果您有需要的话,但您可以安全地跳过这一部分。即使对于专业开发人员来说,从最新的源代码中工作也是罕见的。

下一部分比前面的部分更加可选。只有在您打算修改引擎本身的行为,或者想要使用尚未捆绑到任何发布版本中的新功能时,才需要执行此操作。这也是这个引擎的另一个美妙之处——如果您确实需要它执行某些尚未执行的操作,您可以自行进行更改。此外,如果您进行了改进引擎或可能对其他开发人员有用的更改,您可以使用 GitHub 将您的更改贡献给 Epic。许多开发人员都这样做,其结果是这个引擎以惊人的速度增长和改进。

如果您不需要它,您可以跳过这一部分。我们将在这里深入一点。

设置 GitHub 帐户并安装 Git

Unreal 引擎的源代码是通过一个名为 GitHub 的网站进行分发的。Git 是一个版本控制系统(用于管理代码修订并将其分发给用户的系统),而 GitHub 是一个集中存放和共享 Git 存储库的地方。要下载允许您自行构建引擎的 Unreal Engine 源代码,您将需要 Git 和 GitHub。

很多人将 Git 和 GitHub 混淆。它们并不是同一回事。Git 是一个版本控制系统,允许用户跟踪代码的变化、分发这些变化并以许多其他方式进行管理。GitHub 是一个允许用户存储和共享 Git 数据的网站。还有其他网站也可以做到这一点,尽管 GitHub 是最大的,或者您可以完全自己设置一个 Git 存储库。

设置或登录您的 GitHub 帐户

如果您计划深入研究 Unreal 开发的最前沿,您首先需要一个 GitHub 帐户。前往github.com/并登录,或者如果您还没有帐户,请注册一个。

安装 Windows 版 Git

前往git-scm.com/,下载适用于 Windows 的 Git。Git 是一个软件配置管理工具,允许您将本地的 Unreal 源代码仓库与 Epic 提供的源代码同步。

使用默认选项进行安装,只有一个例外:当安装程序询问您希望将什么作为 Git 的默认编辑器时,当前选择的选项将是 Vim。对于那些已经习惯使用 Vim 的人来说,Vim 非常好用,但对于其他人来说,它可能非常反直觉,因为它遵循与您使用过的几乎任何其他应用程序完全不同的一套约定。如果您还不是那些使用和喜欢 Vim 的人之一,您几乎肯定会想选择另一个文本编辑器:

在设置 Git 时,您可以选择首选的文本编辑器作为 Git 的默认编辑器。

无论如何,拥有一个强大的文本编辑器对于您的系统来说都是一个好主意,因为它可以用于编辑配置文件和大量其他任务。常见的选择有 Visual Studio Code、Sublime Text、Notepad++或 Atom。如果您有喜欢的,请随意使用。如果没有,Visual Studio Code 是一个不错的选择,因为它是免费的,并且遵循与 Visual Studio 相同的约定。如果需要,可以在这里获取:code.visualstudio.com/

安装 Git 大文件存储

接下来,您需要安装 Git 大文件存储(Git-LFS)。这允许 Git 管理 Unreal 生成的大型二进制文件等文件。

前往git-lfs.github.com/,下载 Git-LFS 并安装它。

现在,您需要配置 Git 以使用 Git-LFS。为此,请执行以下操作:

  1. 打开 Git Bash——一个用于管理 Git 的命令行工具,它在您刚刚安装 Git for Windows 时已经安装了。在 Git Bash 中,输入git lfs install并按 Enter 键:

Git Bash 是一个专门用于与 Git 通信的终端窗口。

一旦看到 Git LFS 已初始化,你可以关闭 Git Bash。

安装 Git GUI

并不是必须使用 GUI 来操作 Git。许多开发者直接从命令行操作 Git。某些 Git 操作通过这种方式更容易执行。如果你想使用 GUI,以下说明适用。

前往desktop.github.com/下载GitHub Desktop。还有许多其他的 Git GUI 应用程序;另一个流行的选择是Atlassian's SourceTree,你可以在www.sourcetreeapp.com/找到它,但为了简单起见,我们现在将继续使用 GitHub Desktop。在安装过程中,安装程序会要求你输入刚刚创建的 GitHub 账户的凭据。在这里输入它们。

安装完成后,GitHub Desktop 应该会启动,并且你应该看到一个类似于这样的窗口:

像 GitHub Desktop 这样的 Git GUI 并不是与 Git 通信所必需的,但在刚开始时可能会有所帮助。

将你的 GitHub 账户连接到你的 Epic Games 账户

导航到www.unrealengine.com,如果你还没有登录,请现在登录。在页面的左下角找到你的用户名,将鼠标悬停在上面以显示下拉菜单。选择“管理账户”选项:

Unreal 4.22 版本的账户管理链接。这将在浏览器窗口中打开www.unrealengine.com/account/personal

请注意,Epic Games 启动器中的菜单经常变化。你也可以通过导航到www.unrealengine.com/account/personal来完成此步骤。打开“Connected Accounts”选项卡,找到 GitHub 图标,点击“Connect”将你的 Epic 账户连接到 GitHub。如果需要,同意用户许可协议,并在需要时登录 GitHub。最后,如果授权工具询问,请点击“Authorize Epic Games”按钮。你应该会收到一封确认邮件。如果需要进一步帮助或遇到问题,请查阅www.unrealengine.com/en-US/ue4-on-github上的文档。

通过导航到github.com/EpicGames/UnrealEngine来确认一切设置正确。如果你能看到页面,说明你已经正确连接。如果没有,请确保你已经正确连接了你的账户并被授权查看 UnrealEngine 存储库。

关于 Git 的一点说明:Git 是一个非常有用的工具,但它所做的工作一开始可能看起来相当复杂。详细介绍 Git 的所有知识超出了本书的范围,但我们强烈建议你花些时间了解 Git 是什么以及它的工作原理,如果你打算使用它的话。这是一个很好的起点:git-scm.com/book/en/v2/Getting-Started-Git-Basics。要了解 GitHub 如何与 Git 配合工作,请从这里开始:guides.github.com/activities/hello-world/

下载 Unreal Engine 源代码

现在,你可以开始拉取源代码了。让我们看看如何操作。导航到github.com/EpicGames/UnrealEngine,并浏览一下页面。这个页面上也有一个ReadMe文件。强烈建议你阅读一下。

选择你的源分支

请注意,Epic 维护了多个 Unreal Engine 存储库的分支:

  • 发布分支包含经过测试的源代码,与使用 Epic Games 启动器下载引擎得到的源代码相同。

  • 推广分支包含经过较少测试的代码,由 Epic 的设计师和艺术家在内部使用。它相当稳定,并且将包含比发布分支上的更新但也不太稳定的代码。

  • 主分支是绝对的最新版本,包含 Epic 工程师提交的几乎即时更改。但是,不能保证这些更改是稳定的,甚至可以编译。如果您计划为引擎做出贡献,您应该使用此分支:

Epic Games / UnrealEngine GitHub 存储库的此视图允许您选择当前分支并下载其内容。

暂时使用发布分支。在左上角附近的 Branch 下拉菜单中选择它。

分叉存储库

我们将对此存储库进行分叉。分叉 Git 存储库会创建一个副本,允许您在不影响主存储库的情况下进行自己的更改。点击右上方的 Fork 按钮。这将为您创建一个包含刚刚分叉的源代码的个人存储库。

将存储库克隆到本地计算机

现在,您需要将其放在桌面上。点击页面右侧的绿色 Clone or download 按钮:

在克隆存储库时,您可以选择身份验证方法以及希望如何交付内容。

您在这里有几个选项。

选项 1 - 使用 GitHub Desktop 进行克隆

如果您使用 GitHub Desktop 作为 GUI,请选择 Open in Desktop 并允许页面启动 GitHub Desktop。GitHub Desktop 将询问您要将新存储库存储在何处。告诉它放在哪里并点击 Clone:

确保您选择的本地路径位置有足够的空间来容纳引擎及其内容。

选项 2 - 使用命令行进行克隆

如果您使用命令行,请点击存储库 URL 右侧的 Copy to Clipboard 按钮,然后打开 Windows 命令提示符并导航到您想要存储本地存储库的目录。到达目录后,键入git clone,然后粘贴刚刚复制的 URL:

git clone https://github.com/yourusername/UnrealEngine_YourFork.git

源代码现在将下载到您指定的位置。

下载引擎二进制内容

导航到刚刚下载虚幻引擎源代码的位置,并在那里查找Setup.bat文件。运行它:

虚幻引擎内容与源代码分开提供。在引擎工作之前,您必须运行此.bat 文件。

此批处理文件现在将检查缺失或需要更新的引擎二进制内容,并进行更新。第一次运行可能需要一些时间。

生成项目文件

接下来,在相同目录中找到GenerateProjectFiles.bat并运行它。这将为 Visual Studio 创建UE4.sln解决方案文件和 Unreal Engine 的每个子项目所需的项目文件。这应该运行得相当快。

打开并构建解决方案

在 Visual Studio 中打开新生成的UE4.sln文件。确保设置为开发编辑器解决方案配置,并在解决方案资源管理器中右键单击Engine/UE4项目。选择 UnrealVS Quick Build | Win64 | Development Editor:

快速构建命令允许您选择要构建的构建配置。大多数情况下,您只对开发编辑器或调试编辑器配置感兴趣。

此构建将比之前运行的构建时间长得多,因为我们现在正在构建整个引擎,而不仅仅是一个游戏。

构建完成后,请确保将 UE4 设置为启动项目(默认情况下应该是),然后按F5以在调试器中启动它。

恭喜!您现在已经完全从源代码下载并构建了虚幻引擎。

使用新更改更新您的分叉

Epic 将很快发布尚未在您的分支中出现的新更改。具体时间取决于您所在的分支。如果您在发布分支上,新更改将每隔几周出现一次。在推广分支上,它们将每天或每两天出现一次。在主分支上,它们将每隔几分钟出现一次。在所有这些情况下,您都需要更新您的分支以获取新的更改。

您可以通过查看分支选择器下方的条来了解需要合并的新更改。它显示自上次更新分支以来发生了多少次提交:

这是一个古老分支的示例,远远落后于当前版本。我们可以看到它落后了 52000 多个更改。那很旧了。恐龙在这段代码最后更新时还在地球上漫游。我们需要修复它。

选项 - 使用命令行同步更改

保持分支与上游分支的同步是那些更容易通过命令行完成的操作之一。我们建议您以这种方式进行操作。让我们为您介绍这个过程。

设置上游存储库

我们已经从虚幻引擎源存储库中分叉了自己的存储库,并将其克隆到了本地机器上。现在,我们需要告诉我们的分支如何从原始项目(我们将其称为上游存储库)中拉取更改。您只需要执行一次此操作。在 GitHub 上,打开原始的虚幻引擎存储库页面:github.com/EpicGames/UnrealEngine,点击绿色的 Clone or download 按钮,然后点击 URL 右侧的 Copy to Clipboard 按钮。不要在桌面上打开它或下载 ZIP 文件。您只需要 URL。

打开 Windows 命令提示符(请注意,如果您熟悉 UNIX 命令,也可以使用 Git Bash 进行此操作,并且如果您将大量使用 Git,建议您这样做),并导航到您克隆存储库的目录。键入git remote -v并按Enter。您应该在此处看到您的源存储库,但没有上游存储库。这就是我们接下来要设置的内容:

添加上游存储库之前的 git remote -v 命令的结果

现在,键入git remote add upstream并粘贴您刚才复制的 URL:

git remote add upstream https://github.com/EpicGames/UnrealEngine.git

让我们通过再次键入git remote -v来验证上游存储库是否已正确设置:

添加上游存储库后的 git remote -v 命令的结果

一切看起来都很好 - 我们的上游存储库已经设置好了。

有关分叉存储库并准备从上游仓库拉取更改的更多信息,请查看 GitHub 的文档:help.github.com/articles/fork-a-repo/

同步分支

从命令提示符或 Git Bash 中的存储库目录中,键入git fetch upstream

git fetch upstream 操作的输出

现在,通过键入git checkout和分支名称来checkout您正在工作的任何分支。例如,对于发布分支,键入git checkout release,对于推广分支,键入git checkout promoted,对于主分支,键入git checkout master

接下来,通过键入git merge upstream/,然后是您的分支名称,将上游分支的更改合并到本地分支中。同样,如果您在发布分支上,这将是git merge upstream/release

最后,您需要将本地机器上的更改推送回您的分支存储库在线。键入git push origin master来执行此操作。

回顾我们刚刚使用的 Git 命令

回顾一下:每当您需要将分支与上游分支保持同步时,请使用以下命令:

  • git fetch upstream

  • git checkout [branch]

  • git merge upstream/[branch]

  • git push origin [branch]

请参考以下截图:

Git Bash 中的命令输出。

选项-使用 Web GUI 同步更改

如果您更喜欢在线同步您的分支而不是使用命令行,请按照以下步骤操作。

如果您使用命令行程序来同步您的分支,您可以跳过此部分,因为它执行相同的工作。

导航到 GitHub 上您的分支页面,并点击栏右侧的比较按钮。如果您在本地进行了更改,它们将显示在随后的比较窗口中。(为简单起见,假设我们没有进行更改,只是尝试从 Epic 获取新代码。)为此,首先点击比较页面上的切换基础链接。这将反转比较,所以我们不再寻找尚未传递到 Epic 的本地分支上的更改,而是寻找其他开发人员进行的尚未出现在我们的分支上的更改:

GitHub 上尚未合并到您的分支的更改列表

在这里,我们可以看到新的更改可以自动合并。这是预期的,因为我们没有进行任何引擎更改。(管理您自己的虚幻引擎分支和 Epic 分支之间合并的更改超出了本书的范围。)在我们的情况下,我们只想保持最新状态。

创建一个拉取请求

点击创建拉取请求按钮:

从上游分支合并更改的新拉取请求

给您的拉取请求命名,并再次点击创建拉取请求来创建它。

您的拉取请求现在已准备好进行审查。在这种情况下,由于您发起了请求,您可以直接接受它:

拉取请求确认对话框

合并拉取请求

点击合并拉取请求来执行合并,然后点击确认合并来实现它。

合并完成后,返回到您的分支,您将不再落后:

我们的分支与上游分支之间的比较。我们可以看到我们现在是同步的。

将原始版本拉到本地机器

现在,您需要更新您机器上的本地副本。

返回 GitHub Desktop,在您的虚幻引擎仓库中,寻找 Fetch origin 按钮。点击此按钮,指示 GitHub Desktop 查找您尚未在本地复制的远程仓库中的更改:

在我们从仓库中获取新的更改之前的 GitHub Desktop

在我们的情况下,我们有一些:

GitHub Desktop 准备将更改拉到我们的本地机器

现在是将这 52,000 个更改下载到我们的本地机器的时候了。点击 Pull origin 来执行此操作。GitHub Desktop 将检出这些更改,并将它们复制到本地机器。完成后,我们应该看到点击 Fetch origin 不再导致任何新文件需要拉取-我们是最新的。

重新同步您的引擎内容并重新生成项目文件

无论您是使用命令行还是 GUI 来更新您的分支,现在都需要更新您的解决方案文件和项目文件以反映您下载的新源代码。

如果您确定没有添加或删除源文件或资源,您可以跳过此部分。如果有疑问,请运行这些操作以确保您的资源是最新的,并且 Visual Studio 知道有关已更改的文件。

关闭 Visual Studio,从引擎目录中重新运行Setup.bat文件以更新二进制内容,然后重新运行GenerateProjectFiles.bat以更新 Visual Studio 文件。这些操作将比第一次运行快得多,因为它们只更新了已更改的内容。

打开解决方案,构建并运行。您应该回到当前代码的状态。

很多时候,当您重新生成项目文件时,您的启动项目会发生变化。如果发生变化,请右键单击您想要启动的项目,然后选择“设置为启动项目”进行重置。

在 GitHub 上进一步使用源代码

关于修改和构建引擎源代码,我们还可以谈论很多,但这超出了本书的范围。不过,您在这里学到的知识将使您能够下载 Epic 最新的 Unreal Engine 代码并构建引擎,如果您需要比当前版本更新的代码,或者需要修改引擎。

如果您计划从 GitHub 使用 Unreal 源代码,那么花时间了解它是值得的。它是一个强大的工具,但如果您不清楚它在做什么,可能会让人困惑。这里提供帮助:help.github.com/

再次强调,大多数用户不需要这样做,但有时候支持新的 VR 设备的代码会在推广或主分支上出现,而在发布分支和通过启动器的二进制发布渠道上出现之前很长一段时间。现在您应该已经了解足够的知识,以便在需要时使用最新和最好的工具。

其他有用的工具

在我们离开本章之前,让我们花点时间谈谈您可能想要设置的其他与 Unreal 一起工作的工具。这些工具不是本书项目所必需的,但了解它们是值得的,这样您需要时就知道去哪里找。

一个好的强大的文本编辑器

当您需要编辑大型文本文件或替换文件中的大量文本时,记事本就不够用了。我们建议您为此目的设置一个专用的文本编辑器。以下是几个选择:

  • Visual Studio Code (code.visualstudio.com/) 是一个功能强大、轻量级的文本编辑器,支持多种语言,并包含许多有用的文本编辑工具。它是免费的。

  • Sublime Text (www.sublimetext.com/) 是一个高度可定制的编辑器,具有各种语言的自定义集成。它售价 80 美元,有免费试用版。

  • Atom (atom.io/) 是由 GitHub 开发的一个相对较新的编辑器,支持大量的附加包安装程序,可以对文本页面进行几乎任何操作。由于它是由 GitHub 开发的,它的 Git 集成非常好。而且它是免费的。

  • Notepad++ (notepad-plus-plus.org/) 是一个快速轻巧的编辑器,比大多数其他编辑器更老,因此有一批忠实的用户。它也是免费的。

  • Vim (www.vim.org/download.php) 是一个独立的编辑器。它的用户界面约定与 Windows 中的任何其他东西都不相似,因此需要一些努力来学习它们。它的优点是一旦用户学会了操作它的按键,他们就可以以惊人的速度在文本文档中导航,而无需使用鼠标。它几乎可以在任何计算设备上运行。我们只建议您在已经使用并喜欢它,或者特别有兴趣学习它的情况下使用它。

这些或其他您熟悉和喜爱的文本编辑器都可以很好地工作。选择一个适合您的并坚持使用它。

3D 建模软件

Unreal 场景由 3D 模型组成,您需要在开发过程中的各个阶段修改、清理或从头开始创建它们。(您需要做多少取决于您正在创建什么、与谁合作以及您在多大程度上依赖市场或其他来源的现有艺术品。)无论如何,最好在系统上安装一个可以编辑 3D 网格的工具。

在行业中,您通常会听到将 3D 建模工具称为数字内容创建工具,通常缩写为DCC。如果您听到有人提到 DCC,他们通常是在谈论 Blender、Maya 或 3ds Max 等 3D 建模工具。

以下是几个选择:

  • Blenderwww.blender.org/)是一款免费且开源的 3D 建模程序,广泛用于独立开发社区。有很多教程可以教您如何在 Blender 中创建资产并将其导入虚幻引擎。它是免费的。

  • Autodesk Mayawww.autodesk.com/products/maya/overview)是一款专业工具,专注于创建媒体和娱乐内容。过去十年中,您在任何电影或游戏中看到的几乎所有生物都可能是在 Maya 中建模和动画制作的。Maya 的订阅费用约为每年 1500 美元,但学生可以免费使用三年。

  • Autodesk 3ds Maxwww.autodesk.com/products/3ds-max/overview)是一款专业的建模工具,专注于创建建筑、工程和建筑AEC)以及产品设计的内容。它在媒体和娱乐领域也常被使用,但其动画工具比 Maya 的工具要有限得多。3ds Max 的定价与 Maya 相同,约为每年 1500 美元,并提供免费的学生许可证。

  • Modowww.foundry.com/products/modo#)是一个较新的专业市场参与者,并且正在获得追随者。值得一看。Modo 的订阅费用为每年 600 美元。

您选择的 DCC 将取决于您的预算和计划使用它做什么。一般来说,如果您正在为娱乐而制作虚拟现实,Maya 将包含更多您所需的内容,但这绝不是绝对的。对于建筑和产品设计,3ds Max 可能是您所需要的。对于独立游戏开发,您也可以考虑使用 Blender。进行一些研究,找出最适合您特定需求的工具。

您在专业领域中还会看到其他一些工具。您通常不会看到初学者使用它们,但它们是强大的工具,您应该知道它们的存在,以便考虑它们是否可能是您尝试做某事的好解决方案:

这些通常是专家和专业人士使用的工具,但了解它们的存在和功能是值得的。

图像编辑软件

您通常还需要编辑纹理和 2D 艺术。您需要一个工具来完成这个任务,实际上您有两个可以认真考虑的选择:

  • Adobe Photoshopwww.adobe.com/products/photoshop.html)是 2D 图像编辑的标准。它值得购买。关于 Photoshop 的定价有一个秘密:如果你订阅它作为独立应用程序,每月费用为 20.99 美元,但如果你订阅摄影套餐,你只需支付每月 9.99 美元。

  • GIMPwww.gimp.org/)是一个免费的开源图像编辑应用程序。它缺少很多 Photoshop 的功能,但如果你只是偶尔修改纹理,它可能就足够了。

你在 Photoshop 和 GIMP 之间的选择将取决于你的需求和预算。如果你是专业工作者,最好还是坚持使用 Photoshop,但如果你不需要 Photoshop 的所有功能,GIMP 可能就足够了。

音频编辑软件

你偶尔需要编辑游戏和应用程序的声音和音乐。在这里,你也有几个选择:

  • Audacitywww.audacityteam.org/)是一个免费的开源音频编辑解决方案,质量出奇的好。对于大部分你需要做的音频编辑工作,Audacity 可能就足够了。

  • Adobe Auditionwww.adobe.com/products/audition.html)是一款专业的音频编辑工具。它相比 Audacity 的优势在于更高质量的效果和非破坏性的编辑工作流程,这意味着如果你对声音应用了滤镜或效果,然后想在以后进行更改,你仍然可以做到。Audition 可以按月订阅,或者可以作为 Adobe All Apps 订阅的一部分捆绑购买。

  • Avid Pro Toolswww.avid.com/pro-tools)是专业人士中最常用的音频编辑软件,有一个免费的 Pro Tools | First 版本,包含所有专业版本的功能,但限制了你可以使用的输入和音轨数量。这是否适合你取决于你预计要进行多少音频编辑以及你计划做什么。

我们提到的所有选项都涵盖了声音编辑,但对于声音创作,有很多工具和音频库可供选择。对它们进行分类超出了本书的范围,因为声音设计是一门独立的艺术,充满了深度。对于大多数在虚幻引擎中开发 VR 应用程序的用户来说,从 Audacity 开始,并在需要做特定工作时再进行其他选择是个不错的主意。

总结

在本章中,我们安装了虚幻引擎,并了解了在设置它时可用的各种选项。我们创建并启动了一个简单的测试项目来验证一切是否正常工作。此外,对于那些为移动 VR 开发的人来说,我们学会了如何设置所需的驱动程序和软件开发工具包,并设置了一个移动测试项目,将其部署到我们的设备上。

在这个过程中,我们学会了如何使用 Epic Games 启动器,不仅可以保持引擎版本的更新和启动项目,还可以作为重要的学习和支持资源。通过探索启动器,我们学会了如何从社区选项卡获取问题的答案,以及在学习选项卡中找到文档和视频教程。我们探索了非常有用的内容示例项目,并查看了其他项目,可以用来探索引擎中的特定主题。我们看到市场提供了大量免费和付费内容,可以加速我们的项目,并学会了如何使用库选项卡来维护我们的项目和引擎版本。

对于那些计划使用 C++进行开发的人,我们学习了如何设置我们的 Visual Studio 2017 开发环境,并配置它与虚幻引擎一起工作,然后我们创建了一个简单的测试项目,以确保我们能够在虚幻引擎中构建和运行我们自己的 C++代码。对于那些更加冒险的人,我们学习了如何从 GitHub 下载虚幻引擎源代码,并完全从头构建引擎。

最后,我们简要介绍了一些其他工具,这些工具对于构建虚幻引擎内容的开发人员可能会有用,包括各种免费和付费的解决方案,以满足不同的需求。

在下一章中,我们将为 VR 构建我们的第一个项目。(在本章中,我们设置的快速而简单的项目只是为了测试我们的开发环境是否正确设置,而不是专门为 VR 设计的。)现在,我们将学习如何正确设置 VR 项目。让我们开始吧!

第三章:你好世界 - 你的第一个 VR 项目

是时候开始构建了!在第一章中,我们学习了什么是 VR 以及它能做什么,我们还学习了一些关于 VR 设计的最佳实践。然后,在第二章中,我们设置了开发环境。现在我们准备好开始构建了。

在本章中,我们将从头开始在虚幻中构建一个 VR 项目。然而,我们将采取与大多数教程不同的方法。我们不仅仅给你一系列要遵循的步骤,对于我们所做的每一件事,我们都会谈一谈在幕后发生了什么,以及为什么我们要以这种方式做。这才是真正重要的。如果你对这些系统的工作原理有一些了解,你在构建自己的项目时将更加有能力知道该做什么。

在构建我们的第一个 VR 项目时,我们将了解一些关于其结构的知识,并了解适用于 VR 开发的特定项目设置。我们还将查看那些特别影响移动 VR 的设置和选择,并向您展示在这方面需要了解的内容。从这里开始,我们将在项目中引入一个详细的场景,并学习如何在项目之间安全地移动资产以及如何管理项目的内容。最后,我们将设置游戏模式和角色蓝图,以便运行 VR 项目。

本章将涵盖以下内容:

  • 从零开始创建一个 VR 项目

  • 了解在开始项目时需要做出的重要设置和选择

  • 为移动 VR 设置项目

  • 在项目之间安全地移动内容和管理项目内的内容

  • 在虚幻中设置你在 VR 开发中所需的基本蓝图

创建一个新项目

好的,让我们开始创建!

我们需要做的第一件事是创建一个新项目。在上一章中,我们创建了一些快速的临时项目,只是为了确保一切正常工作,但现在我们准备好开始真正的构建了。

如果你的Epic Games Launcher还没有打开,请打开它,转到“库”选项卡,在“引擎版本”下找到你最新的引擎版本,点击“启动”。(你也可以从启动器左侧的启动按钮进行操作。)

虚幻项目浏览器将出现。选择“新项目”选项卡,让我们选择“蓝图”选项卡和“空白”模板来创建一个空的蓝图项目。

模板是虚幻引擎项目的非常有用的起点。它们包含了许多游戏类型的简单和有用的工作基础,当你开始一个新项目时,你通常会想要使用它们。我们从一个空白项目开始,这样你就可以看到每个元素的添加过程。对于大多数项目,你可能最常用的是第一人称、第三人称和 VR 模板作为起点。

在这个对话框上我们还有一些选择要做,并且我们应该了解它们的含义:

设置硬件目标

硬件目标选择器提供了两个选项:

  • 桌面/主机

  • 移动/平板

通常情况下,你应该选择适合目标平台的正确选项,但是在开发 VR 时,即使你是为桌面开发,选择移动/平板选项也是一个好主意,因为该选项会关闭一些在 VR 中可能很昂贵的渲染选项。

具体而言,选择移动目标而不是桌面目标将关闭以下渲染选项:

  • 分离半透明度

  • 泛光

  • 环境遮蔽

设置图形目标

你需要做出的下一个选择是你的图形目标。同样,在这里你有两个选择:

  • 最高质量

  • 可扩展的 3D 或 2D

选择最高质量将打开 Unreal Engine 提供的所有默认高端渲染选项。然而,如前所述,在 VR 中,达到目标帧率比在场景中包含细节更重要。对于 VR 开发,选择可扩展选项总是一个好主意。

最好的做法是从关闭所有内容开始,根据需要逐渐打开它们。如果你从一开始就打开了所有内容,很难弄清楚是什么导致了帧率下降,并找出需要关闭的内容。最好的做法是从项目以合理的速度运行开始,并保持快速运行,而不是构建一个运行效果差的项目,并希望以后能够更快地运行。

设置摘要

对于我们的项目,我们将选择以下内容:

  • 项目模板:蓝图-空白

  • 硬件目标:移动设备/平板电脑

  • 图形目标:可扩展的 2D 或 3D

  • 没有起始内容

我们现在可以将 Starter Content 关闭,因为我们可以在需要时轻松添加这些内容。

选择你想保存项目的位置,然后点击创建项目来设置它。

快速查看项目的结构

我们现在创建了一个空项目。让我们花点时间来看看这实际上意味着什么。

如果你在 Windows 资源管理器中导航到保存项目的位置,你会看到 Unreal 在那里创建了一个带有项目名称和.uproject扩展名的文件,以及四个目录:

  • Config:配置文件,如DefaultEngine.iniDefaultGame.ini,存放在这里,保存了引擎和项目的设置。

  • Content:这是你的项目的资产所在的地方,如模型、纹理、材质和蓝图。这是你项目的主要部分。

  • Intermediate:当编译项目资产时创建的临时文件存放在这里。这里的所有内容都是临时的,如果你删除它,它们将被重新生成。

  • Saved:日志文件、截图和保存的游戏存档保存在这个目录中。

如果你生成了一个 C++项目,你会看到另外三个目录:

  • Binaries:项目的构建可执行文件和支持文件存放在这里。当你在 Visual Studio 中构建项目时,生成的可执行文件就保存在这里。

  • Build:与特定目标构建相关的文件,如 Windows 64 或 Android,存放在这里。这些包括在构建过程中生成的日志,以及某些支持资源,如应用程序图标。你很少会接触到这个目录的内容。

  • Source:C++文件和管理构建它们的 C#脚本存放在这里。

内容目录

在大多数情况下,当你使用 Unreal 项目时,你将使用Content目录和Config目录中的内容。通常情况下,你应该在 Unreal Editor 中管理 Content 目录的所有内容,因为否则很容易破坏对象之间的引用关系。我们将很快讨论如何做到这一点。

配置目录

然而,我们应该花点时间来看一下Config目录。

在这个目录中,包含了包含项目设置的配置文件。所有与引擎相关的项目设置,如渲染质量选择,都写入到DefaultEngine.ini文件中。当你在创建项目对话框中选择硬件和图形目标时,实际上只是选择将默认选项写入该文件。同样,当你从编辑器中更改项目设置时,这些设置也会写入DefaultEngine.ini(或DefaultGame.ini用于某些与游戏相关的设置)。

你的Config目录将始终包含以下两个文件:

  • DefaultEngine.ini:这个文件包含了你的渲染设置、启动地图设置、物理设置和其他控制引擎运行方式的选项。

  • DefaultGame.ini:大部分包含有关游戏和版权信息的内容,但也包含了关于应用程序在不同平台上发布时如何打包的信息

当您在编辑器中更改项目设置时,您主要是将更改写入这两个文件。

根据您在构建项目时更改的设置,可能会创建其他Config文件:

  • DefaultInput.ini:这包含与使用输入设备相关的输入映射和设置。

  • DefaultEditor.ini:这包含了控制编辑器行为的设置。

  • DefaultDeviceProfiles.ini:这包含了您可能发布应用程序的不同平台的特定设置。

您不必了解这些内容就可以使用引擎。完全可以在编辑器内部管理设置,但这也是虚幻引擎的另一个伟大之处之一-它不会将重要信息散布在奇怪的地方。如果您在某个时刻需要弄清楚您在哪里设置了什么,您知道该去哪里找。它将在这些文件中的一个中。

如果在 Windows 资源管理器中看不到.ini等文件扩展名,请打开文件资源管理器选项控制面板,并关闭隐藏已知文件类型的扩展名。在 Windows 中,默认情况下是打开的,但在开发时会隐藏有用的信息。

源目录

如果您创建了一个 C++项目,您的项目目录还将包含一个Source子目录。您的 C++源文件存放在这里。

项目文件

我们还应该快速查看一下项目的.uproject文件。实际上,它只是一个包含有关项目的一些信息的简单文本文件,但如果您在资源管理器中右键单击它,您将获得三个有用的选项:

  • 启动游戏:这只是在虚幻编辑器中打开您的项目。双击.uproject文件也可以做到这一点。

  • 生成 Visual Studio 项目文件:仅适用于创建了 C++项目的情况。通常情况下,只有在清除了保存 VS 项目文件的 Intermediates 目录,或者从编辑器外部添加了新的源代码文件时,才需要执行此操作。

  • 切换虚幻引擎版本:这会更改与您的项目关联的引擎版本。通常情况下,当您要切换到新的引擎版本时,最安全和最可取的方法是在启动器中复制并更新您的项目,但如果您已经知道可以安全执行此操作,您可以在此处切换。

一个虚幻项目结构的概述

现在我们已经快速了解了虚幻项目的结构,我们应该在工作时将其牢记在心。

同样,一个虚幻项目的最基本结构包括以下内容:

  • Project目录:

  • Project文件

  • Content目录

  • Config目录

  • (仅限 C++)Source目录

如果您需要与他人共享基于蓝图的虚幻项目,您只需要共享.uproject文件、Content目录和Config目录。其余的内容都是在项目运行时动态生成的。

根据您对项目的操作,可能会自动创建其他目录。

这就是我们在这里想要做的一切-只是快速查看一下并了解一下我们开始向项目中添加大量内容之前的情况。知道东西在哪里可以在以后让您的生活更轻松。

设置项目的 VR 设置

让我们回到编辑器中,继续设置我们的项目。在做其他任何事情之前,我们应该查看一下一些设置。

我们即将讨论的所有这些设置都会影响场景的渲染方式:

渲染是将场景中的 3D 几何图形通过虚拟摄像机观察并将该几何图形转换为可以在屏幕或头戴式显示器上显示的图像的过程。

正如我们在第一章中提到的,在 VR 中思考,与传统的平面屏幕渲染相比,VR 对渲染管线提出了更高的要求。即使是当前市场上分辨率最低的头戴设备也显示了相当多的像素,并且需要非常快速地更新。如果这还不够具有挑战性,我们还需要考虑到两只眼睛,它们看到的视图并不完全相同。这意味着我们要渲染两个独立的视图。这是一项相当繁重的任务,而时间却不多。

因此,对于 VR 开发人员来说,了解 Unreal 提供的渲染选项是很重要的。在这里做出良好的选择可以让您在实现既好看又快速的目标方面走得更远。

Instanced Stereo

还记得我们刚才提到过需要同时渲染两个独立视图吗?在不好的旧日子里(在 Unreal 4.11 之前),这是真的。引擎只是简单地运行整个渲染过程两次 - 每次为一个眼睛。这是非常浪费的,因为两个视图之间唯一的真正区别是眼睛位置的微小偏移。第二次渲染的全部成本都被用来绘制几乎与刚刚绘制的相同的东西。

Instanced Stereo渲染通过允许在单个传递中渲染场景来改进这一点。然后,渲染的视图与视频硬件一起传递,以及每个眼睛调整视图所需的信息。它比运行整个传递两次要快得多,您需要确保打开它。现在让我们来做这个。

如果您使用 VR 模板创建项目,Instanced Stereo 将已经为您打开,但如果您从头开始创建项目,或者修改现有项目以适应 VR,您需要记住自己执行此操作。

从编辑器中打开项目设置,可以通过点击编辑器工具栏上的设置按钮并选择项目设置...,或者选择编辑 | 项目设置来打开:

在项目设置中,找到引擎部分的渲染项。在渲染页面中,找到 VR 部分的 Instanced Stereo 选项并打开它:

在执行此操作后,将要求您重新启动引擎。这将需要一些时间,因为您的着色器将需要重新编译。

轮询遮挡

因为我们没有太多时间将帧传输到头戴设备上,所以我们不想浪费任何时间来绘制我们不需要绘制的东西。引擎通过一个称为剔除的过程选择要绘制的对象。它使用四种主要方法来进行剔除,按顺序从最快和最简单到最复杂:

  • 距离剔除只是忽略相机距离一定距离之外的任何对象。这是廉价的。

  • 视锥剔除会忽略不在相机当前视图中的对象。这比距离剔除更昂贵,但仍然相对便宜。

  • 预计算可见性允许设计师设置体积,明确告诉引擎从某些位置可以看到什么,不能看到什么。例如,如果您知道房间内的玩家不可能看到外面的任何东西,您可以使用预计算可见性体积告诉引擎甚至不需要检查。

  • 动态遮挡实时测试场景中的一个角色是否遮挡了另一个角色。这相对昂贵,因此只对那些没有被更便宜的方法剔除的对象进行测试。

对于 VR 项目,虚幻引擎提供了一种名为Round Robin Occlusion的优化动态遮挡剔除方法,每帧只测试一个眼睛的遮挡,而不是两个眼睛。这在有很多对象的场景中节省了大量时间,并且由于每只眼睛的视图几乎相同,所以效果很好。系统在每帧切换测试的眼睛,这就是名称的由来。

让我们打开它:

  1. 在项目设置 | 引擎 | 渲染 | VR 中,勾选 Round Robin Occlusion Queries:

前向渲染和延迟渲染

现在我们需要对我们项目中要使用的渲染方法做出重要选择。

广义上讲,绘制场景有两种方式,它们之间的区别主要取决于场景中的物体如何受光照。这两种方法被称为前向渲染延迟渲染

你有时会看到这些称为前向渲染延迟渲染,或者你会听到人们谈论前向渲染器延迟渲染器。Epic 在其文档中可互换使用这些术语,但它们都指的是同样的东西。在这里,我们将坚持使用术语前向渲染,因为这是编辑器中的选项名称,并且最准确地描述了这两种方法之间的真正区别。

着色是将光应用于几何体的过程。这包括高光、表面反射、阴影和光线击中材质时产生的各种效果:

前面的截图显示了相同的网格,没有应用着色,然后应用了着色。在左侧的图像中,你可以看到物体的形状和基本颜色(通常称为反射率),但没有阴影、反射或高光。右侧的图像已经着色,所以可以看到高光、阴影和反射。

在接下来的描述中,我们稍微简化了一些事情,但对于我们的目的来说,这是可以的。你真的不需要了解渲染管线的每个细节,以便做出关于如何使用它的正确选择。只需要了解足够的信息,以便为你需要做的事情做出正确的选择即可。

前向渲染是实时 3D 渲染历史上绘制 3D 场景的最初方式。在前向渲染中,场景中的每个几何对象在渲染时进行着色,并检查场景中的每个光源对其的影响。如果场景中有很多对象和很多光源,这将导致大量的操作。这就是为什么大多数光照倾向于嵌入到静态光照贴图中,而动态光照在 20 世纪 90 年代和 21 世纪初的游戏中很少见的原因。每个动态光源都会大大增加场景的成本。

另一方面,延迟渲染绘制视图中的每个对象,但不是立即进行光照和着色,而是写出一系列包含场景中材质信息、每个像素的深度以及其他影响场景光照的因素的图像。只有在所有这些信息被组装完毕后,才进行着色。这就是名称的由来——着色通道被推迟到基础通道完成之后。

这些缓冲区的集合被称为几何缓冲区,或者G 缓冲区,构建它们的过程被称为基础通道。如果你在虚幻引擎中使用延迟渲染(这是新项目的默认设置),你可以通过选择视图模式 | 缓冲区可视化 | 概览来查看 G 缓冲区的内容。

看一下下面的截图:

由于光照通道只发生一次,所以对于有很多动态光的场景来说,这比使用延迟渲染要快得多,而且还可以高效地处理屏幕空间效果,如环境遮蔽。然而,对于部分透明的物体来说,它的效果不如前向渲染好。

选择适合你项目的渲染方法

所以,听起来很简单,对吧?延迟渲染似乎提供了很多优势。对于非 VR 渲染来说,这基本上是正确的,在 2000 年代后期,延迟渲染成为了包括虚幻引擎在内的几乎所有游戏引擎的默认选择。

然而,在 VR 中情况就不同了。延迟渲染的问题在于,由于它处理信息的方式,很难关闭渲染过程的各个方面。在很大程度上,它是全有或全无的。这在平面屏幕上通常不是问题——开发者几乎总是希望使用延迟渲染器所提供的一切。然而,其中一些过程在 VR 中运行效率太低,或者在屏幕空间计算时看起来不匹配。在 VR 中,你通常会希望有自由关闭它们的能力。

当你听到术语“屏幕空间”时,这意味着计算不是在 3D 空间中的对象上进行,而是在包含该对象的场景的 2D 图像上进行(这个过程称为光栅化),然后在 2D 图像上执行计算。这在 VR 中可能会产生问题,因为许多屏幕空间计算在两只眼睛之间不匹配。你通常会希望避免在 VR 中使用屏幕空间效果。

在虚幻 4.14 中,Epic 添加了前向渲染作为专门为 VR 项目设计的选项。他们还引入了一种聚类系统,降低了基本通道中处理灯光的成本,所以它的成本不再像以前那样高昂。对于大多数 VR 项目来说,使用前向渲染是个好主意。

在某些情况下,如果你的场景需要支持大量可移动的灯光,或者你知道你将需要非常复杂的反射,你可能仍然希望坚持使用延迟渲染,但你应该认真考虑在大多数 VR 项目中使用前向渲染。

你几乎总是希望在 VR 项目中使用前向渲染。它可以让你更好地控制渲染过程中你想要做的部分和你想要跳过的部分;它更容易处理透明度,并支持更好的抗锯齿选项。

让我们在我们的项目中打开它。

从项目设置 | 引擎 | 渲染中,找到前向渲染器部分,打开前向渲染。你需要在这样做后重新启动编辑器:

当使用前向渲染时,许多昂贵的材质特性通常需要显式地打开。在 VR 中,这是一件好事,因为它让你可以自由地在只有在需要时使用昂贵的特性。我们稍后会讨论在为 VR 创建和修改材质时如何做到这一点。

虽然在项目开发的后期可以随时打开或关闭前向渲染,但你通常会希望做出一个选择并坚持下去,因为两种方法在项目的光照、材质和反射方面可能会有很大的差异。你不希望在开发的后期花费大量精力来开发你的外观,然后做出这样的改变。你最终会不得不重新做很多工作。

选择你的抗锯齿方法

使用前向渲染的一个主要优势是抗锯齿比使用延迟渲染要容易得多。让我们谈谈这意味着什么,以及为什么这对我们在 VR 中很重要。

当渲染器将场景绘制到平面屏幕上(无论是显示器还是 VR 头显)时,该显示实际上由一组称为像素(照片元素)的小方块组成,渲染器必须决定每个像素的颜色。当 3D 场景中的对象只部分填充 2D 空间中的像素时,这就成为一个问题。然后,渲染器必须决定像素应该填充对象的颜色还是背景的颜色。没有中间状态-它必须选择其中之一。实际上,这意味着对象可能会出现锯齿状边缘,特别是沿着跨越许多像素边界的对角线。我们称之为走样问题:

没有抗锯齿渲染的场景

请注意,没有抗锯齿渲染的场景中窗户周围的锯齿。它们在这里看起来很糟糕,在 VR 中看起来会更糟糕。

我们通过一个不出所料被称为抗锯齿的过程来解决这个问题。不同的抗锯齿方法使用各种技术来找到像素的正确颜色,以使其在前景色和背景色之间呈现混合效果,从而软化锯齿边缘。这样可以平滑锯齿边缘并消除对角线上的阶梯状线条:

使用多重采样抗锯齿(MSAA)渲染的场景

看看使用多重采样抗锯齿渲染时窗户看起来更加平滑的效果?

这在 VR 中尤为重要,因为头显的分辨率仍然相对有限,所以用户通常可以看到单个像素。在平面屏幕上可以接受的走样在 VR 中可能看起来很糟糕,因为用户在场景中观察时,锯齿状边缘会爬行和闪烁。您应该避免这种情况。

幸运的是,虚幻引擎提供了三种抗锯齿方法来解决这个问题:

  • FXAA代表快速近似抗锯齿。它寻找场景中的边缘并混合这些边缘的颜色,并且足够智能以避免处理没有对比较边缘的区域,因此看起来很棒并且运行速度相对较快。如果您在 VR 中使用延迟渲染,这应该是您的默认选择。

  • 时域抗锯齿TAA)通过查看前几帧来决定如何对当前帧进行抗锯齿处理。这通常使其在 VR 中成为一个糟糕的选择,因为用户的视图往往会移动很多,时域抗锯齿可能会在快速移动时产生“模糊”效果。即使在没有模糊的情况下,它在 VR 头显上可能看起来太模糊而无法接受。时域抗锯齿通常在平面屏幕上表现出色,但对于 VR 来说并不是一个很好的选择。

  • MSAA代表多重采样抗锯齿。此方法仅在使用正向渲染时可用,并且会比 FXAA 提供更清晰、更好的结果。如果您在项目中使用正向渲染(通常应该使用),这是您应该使用的抗锯齿方法。

让我们在项目中处理这个问题:

从项目设置 | 引擎 | 渲染中,找到默认设置部分,并将抗锯齿方法设置为 MSAA:

大多数情况下,您不需要改变抗锯齿方法的任何设置,但如果需要,继续阅读。

修改 MSAA 设置

这部分是可选的。调整抗锯齿设置是一个高级主题,对于大多数项目来说,您不需要这样做。如果您确实需要调整 MSAA 设置,以下是一个很好的方法:

选择窗口 | 开发者工具 | 设备配置文件以打开设备配置文件窗口:

从此面板中,点击 Windows 行中的 CVars 按钮。

在生成的对话框中,打开控制台变量|渲染。从这里,您可以看到您当前指定的所有与渲染相关的控制台变量。如果您点击 Rendering 旁边的+号,您可以在出现的搜索窗口中键入msaa,并为 r.MSAACount 添加一个值。默认情况下,此值设置为 4。将其减少到 3 或 2 将降低抗锯齿的质量,但会加快速度。将其设置为 1 将关闭抗锯齿。将其设置为 0 将关闭抗锯齿并回退到时间抗锯齿:

如果您在这里进行了更改,请在设备配置窗口上点击“保存为默认值”以保存这些设置。它们将被写入项目的Configs目录中的一个名为DefaultDeviceProfiles.ini的新配置文件中。

再次更改这些值是一个高级主题。我们建议您在确保理解其功能之前不要修改这些值。

在 VR 中开始

告诉我们的项目在运行时启动 VR 也很重要。如果您想构建一个既可以在 VR 中运行又可以在平面屏幕上运行的项目,您可以选择关闭此选项,并在启动时使用-vr命令行参数。但是,我们的项目只能在 VR 中运行,所以我们想打开它。

转到项目设置|项目|描述|设置,并将“在 VR 中启动”设置为 True

关闭其他不需要的杂项设置

在您的渲染|默认设置中,关闭环境遮挡静态分数。环境遮挡是一种用于创建物体接触处出现的微妙阴影的方法,但是计算它们的成本很高,并且在 VR 中可能看起来很糟糕,因为它们是在屏幕空间中计算的。我们不会在这里深入讨论这个主题。当您将项目设置为移动、可扩展的 2D/3D 时,您已经关闭了环境遮挡,所以这只是一个您应该清除的杂项设置。

关闭默认触摸界面(Oculus Go/Samsung Gear)

如果您正在开发 Oculus Go 或 Samsung Gear 的应用程序,您需要关闭默认的触摸界面。移动应用程序通常假设您将通过触摸屏幕来操作它们,但是在头戴式显示器中当然不会发生这种情况。

导航到项目设置|引擎|输入,并从移动部分中获取 Default Touch Interface 旁边的下拉菜单并清除它:

为 Android 配置您的项目(Oculus Go/Samsung Gear)

现在,我们需要配置项目使用 Android SDK。我们在上一章中已经完成了这个过程,我们只需要为这个项目设置相同的设置。这里是我们需要做的事情的快速提醒。

从项目设置|平台|Android 中,找到 APK 打包部分,然后点击“立即配置”。如果您在上一章中已经接受了 SDK 许可证,那么该按钮将被禁用-您只需要接受一次:

然后设置这些设置(正如我们在上一章中提到的,大多数指南会告诉您将 SDK 版本 19 作为最低版本。这对于 Samsung Gear 来说是可以的,但对于 Go,请使用版本 21)

  • 最低 SDK 版本:21

  • 目标 SDK 版本:21

  • 在 KitKat 及以上设备上启用全屏沉浸式:True

向下滚动到高级 APK 打包部分,并设置如下:

  • 将 AndroidManifest 配置为部署到 Oculus Mobile 的 True。

验证您的 SDK 位置

选择项目设置|平台|Android SDK,并确保正确设置 SDK 位置。如果您按照上一章的说明进行操作,它们应该已经设置好了。如果没有,请返回那里并进行设置。

确保关闭移动 HDR(Oculus Go/Samsung Gear)

检查您的项目设置|引擎|渲染|移动,并确保关闭移动 HDR。

移动多视图(Oculus Go/Samsung Gear)

还记得在关于实例化立体渲染的部分中,我们讨论了为每只眼睛渲染整个场景是多么浪费吗?移动头戴设备也有一个解决方案,称为移动多视图。移动多视图的工作原理与实例化立体渲染基本相同-先为左眼渲染场景,然后将图像移动和调整为右眼。我们想要打开它。

在项目设置 | 引擎 | 渲染 | VR 中,将移动多视图设置为 true,并同时打开移动多视图直接选项。Oculus 不建议或支持在没有直接选项的情况下使用移动多视图。将它们都打开:

单眼远场渲染(Oculus Go / Samsung Gear)

关于立体深度感知的问题是,我们只能在一定距离内看到它。超过这个距离,立体图像和平面图像之间没有可见的区别。它们对我们来说看起来是一样的。我们可以利用这一点。

如果我们将项目设置 | 引擎 | 渲染 | VR | 单眼远场设置为 true,引擎将只渲染指定距离之外的任何物体一次,这可以在适当的场景中节省大量时间:

默认情况下,单眼和立体渲染之间的分割发生在 7.5 米处,但这在每个地图上都是单独设置的。(这个分割的位置称为裁剪平面。)这个裁剪平面与摄像机的距离在每个地图上都是单独设置的。要进行调整,请打开窗口 | 世界设置,并在出现的设置面板上查找 VR 部分。调整单眼裁剪距离将改变裁剪平面的位置。

对于场景中的某些对象,特别是大型对象,如果它们的边界靠近摄像机,即使它们实际上只出现在远处,您可能需要强制它们以单眼模式渲染。在这些情况下,打开对象的详细信息,并将渲染 | 渲染为单眼设置为 true。(此选项在渲染部分的高级选项中隐藏。)

项目设置备忘单

我们刚刚介绍了一些在为 VR 设置项目时应该修改的设置,以及每个设置的一些背景知识。为了回顾一下,这是我们所更改的备忘单:

  • 项目设置 | 引擎 | 渲染 | VR | 实例化立体:True

  • 项目设置 | 引擎 | 渲染 | VR | 轮询遮挡查询:True

  • 项目设置 | 引擎 | 渲染 | 正向渲染器 | 正向着色:True

  • 项目设置 | 引擎 | 渲染 | 默认设置 | 抗锯齿方法:MSAA

  • 项目设置 | 引擎 | 渲染 | 默认设置 | 环境遮蔽静态分数:False

  • 项目设置 | 项目 | 描述 | 设置 | 启动 VR:True

这是移动 VR 版本:

  • 项目设置 | 引擎 | 输入 | 移动 | 默认触摸界面:无

  • 项目设置 | 平台 | Android | APK 打包:配置和设置所提到的设置

  • 项目设置 | 平台 | Android SDK:验证您的 SDK 位置是否设置正确。

  • 项目设置 | 引擎 | 渲染 | 移动 | 移动 HDR:False

  • 项目设置 | 引擎 | 渲染 | VR | 移动多视图:True

  • 项目设置 | 引擎 | 渲染 | VR | 移动多视图直接:True

  • 项目设置 | 引擎 | 渲染 | VR | 单眼远场:True

再次强调,不要盲目地遵循这些设置。对于大多数 VR 项目,这些是您想要的设置,但这并不意味着它们适用于您所做的每个项目。

装饰我们的项目

现在我们已经设置了项目的基本设置,让我们添加一些环境艺术,这样我们在工作时就有一些有趣的东西可以看。

将内容迁移到项目中

从您的Epic Games Launcher中打开 Learn 选项卡,并搜索Sun Temple示例环境。点击创建项目按钮,并选择一个您想保存的位置:

让它下载。项目下载完成后,打开它。它应该打开到太阳神庙地图。现在我们要将这个地图迁移到我们现有的项目中。

我们也可以下载太阳神庙项目,然后设置它以在 VR 中运行。我们这样做是为了给您一个学习“迁移...”工具的机会。当您需要将资产从一个项目迁移到另一个项目时,“迁移”工具是最好的方法。

在内容浏览器中,选择“内容”|“地图”|“太阳神庙”。右键单击它,然后选择“资产操作”|“迁移...”:

现在,您将看到一个列表,其中列出了如果迁移此地图将被复制的所有内容。这就是“迁移...”工具的强大之处,也是为什么您应该使用它的原因。当您将资产迁移到另一个项目时,虚幻会检查该资产工作所需的其他所有内容,并将其包含在要复制的资产列表中。因此,例如,如果您迁移一个网格,该网格使用的材质和纹理也将被自动找到并迁移。在我们的例子中,我们正在迁移一个地图,所以虚幻将把地图所依赖的所有内容带入新项目中:

现在你需要选择你要放置迁移内容的位置。迁移操作的目标始终必须是目标项目的Content目录。导航到该位置并选择它。(这就是为什么我们在本章开始时提到了了解虚幻项目目录结构的重要性。您偶尔需要知道其中的内容存放位置。)

迁移完成后,让我们关闭这个项目,重新打开刚刚添加了这个地图的项目。

现在你应该在内容浏览器的“Maps”目录中看到一个太阳神庙地图。让我们打开它。

如果这是您第一次打开此地图,虚幻可能需要编译大量着色器。(这是为什么我们在第二章中设置了派生数据缓存的原因之一——一旦编译了着色器,它们将存储在此缓存中,因此当您打开其他项目时,无需重新编译它们。)

在迁移此地图时,还有一些额外的内容。我们现在要摆脱它,以便我们可以专注于我们正在创建的新资产。顺便说一下,我们还将利用这个机会向您展示一些关于管理内容浏览器中的资产的重要内容,这对您的继续开发非常重要。

清理迁移内容

打开太阳神庙地图,打开“窗口”|“世界设置”,找到“游戏模式覆盖”。(我们将很快讨论游戏模式。)通过点击属性旁边的黄色“重置为默认”箭头来清除它:

每当您看到一个黄色的“重置为默认”箭头时,点击它将恢复属性的标准设置。

保存地图。

安全删除资产

现在在内容浏览器中选择“蓝图”文件夹。我们马上要创建自己的蓝图,所以我们不需要这些。删除此文件夹,但要注意出现的确认对话框。

如果您看到一个带有警告的“强制删除”按钮,这意味着您要删除的内容仍然在某个地方使用中。您几乎永远不应该删除仍然被引用的内容。(我们在这里说“几乎”是因为一旦您真正了解引擎在做什么,就有某些情况可以稍微推动它,但在您确切知道底层发生了什么之前,请不要这样做。)相反,找出资产仍在使用的位置,并将引用更改为指向其他内容,或者删除引用它的对象,或者保持不变:

如果可以安全删除对象,对话框将只显示一个删除按钮。这意味着删除它不会破坏其他内容:

在这种情况下,如果出现强制删除警告,意味着您可能没有清除地图的世界设置中的 GameMode Override,或者您在此之后没有保存地图。如果只有一个简单的删除按钮,点击它以删除文件夹及其内容。

移动资产并修复重定向器

现在让我们整理剩下的内容。在内容浏览器中,为您的项目创建一个新文件夹。我们可以将此文件夹命名为HelloVR

在内容浏览器中为您的项目创建一个文件夹是一个好主意。这样,当您从其他来源迁移更多内容到项目中,或者从市场添加资产时,您将永远不会困惑于哪些资产属于您的项目,哪些是外部来源的。同样,如果将资产迁移到其他位置,它们将全部出现在新项目的内容浏览器中。大多数开发者不这样做,但每个人都应该这样做。当您第一次迁移插件并让其将资产散落在现有文件夹中时,您就会明白为什么。通过保持自己的项目有组织,您可以避免很多混乱。

由于我们已经删除了Blueprints文件夹,我们仍然有另外两个从迁移内容中遗留在内容根目录的文件夹。让我们将它们移动到我们的项目文件夹中。

Maps文件夹拖动到HelloVR文件夹中。当询问是否移动或复制时,选择移动。现在将Assets文件夹也进行同样的操作。

但这是什么?我们已经移动了文件夹,但旧位置的文件夹并没有消失。为什么?原因是虚幻引擎留下了一组重定向器。您应该了解这些。让我们将它们显示出来。

在搜索栏旁边的筛选器下拉菜单中,选择筛选器 | 其他筛选器 | 显示重定向器:

现在让我们进入那个被遗留的Assets文件夹,并进入其中的Blueprints文件夹。里面有一个名为 BP_Godray 的重定向器,我们将其移动到了新位置。双击此重定向器,它将带您到资源的新位置。这就是重定向器的作用。当您在虚幻引擎中移动资源时,很可能项目中的其他内容正在使用该资源并指向它。虚幻引擎允许您在不更改引用的情况下移动资源,当其他对象尝试在旧位置找到它时,重定向器将把它们指向新位置,您可以稍后更改引用指向的位置。这是一个很好的系统,可以在大型项目中节省很多麻烦。

然而,如果您不需要重定向器,您不希望将其留在那里。要清理重定向器,请右键单击它,并选择“修复”:

这将找到所有引用该资源的资源,并将引用指向新位置。完成后,删除重定向器,因为它不再需要。

这也可以同时对文件夹中的每个重定向器执行。接下来我们来做这个。

首先,我们将使内容浏览器的文件夹结构更易于查看。点击筛选器下拉菜单旁边的“Sources Panel”按钮,打开“Sources panel”:

这将切换到项目内容目录的树状视图,可以更方便地浏览和移动资源:

现在我们可以看到我们正在做什么了,让我们选择包含所有重定向器的旧的Assets文件夹,右键单击它,然后选择“修复文件夹中的重定向器”:

操作完成后,您可以删除旧的Assets文件夹,因为它现在是空的。

在删除文件夹之前,验证文件夹是否为空的一个好方法是在内容浏览器中右键单击文件夹,选择在资源管理器中显示,并在资源管理器中选择文件夹,按下Alt + Enter以显示其属性。如果显示为 0 个文件,则为空。如果有任何内容,您可以深入了解其中的内容以及是否需要保留。

我们的Content目录现在应该非常有条理,我们使用的所有内容都集中在我们的HelloVR文件夹下。如果您在项目较小的时候就养成了保持Content目录整洁的习惯,那么一旦项目变得庞大,您将会更容易处理。

设置默认地图

现在我们已经导入了地图并清理了附带的额外蓝图,让我们设置项目以将 Sun Temple 作为默认地图加载。

在项目设置|项目|地图和模式|默认地图下,使用下拉菜单将 Sun Temple 设置为您的编辑器启动地图和游戏默认地图:

这样,当您启动编辑器或启动游戏作为独立可执行文件时,它将直接加载到这个地图中。

在桌面上测试我们的地图

让我们来看看我们目前所拥有的。如果我们正在开发桌面 VR,我们可以在 VR 中启动地图并四处查看。在编辑器工具栏上,选择播放按钮右侧的下拉菜单。选择 VR 预览(如果 VR 预览变暗,请确保您的 VR 头显已连接并且其软件正在运行):

这里感觉还不错,对吧?

我们还不能做太多事情,而且相对于地板来说我们的高度也不正确,但它正在运行,我们准备开始设置事物。

在移动设备上测试我们的地图(Oculus Go/Samsung Gear)

如果我们想在移动设备上测试地图,我们还需要做一些其他的事情。

假设我们已经按照说明设置好了在移动设备上运行项目,让我们首先检查我们的移动设备是否已连接并可见。

重要提示:如果您更新了 Unreal Engine 版本,请确保在<Engine Install Location>\Engine\Extras\AndroidWorks\Win64下重新运行CodeWorks for Android安装程序。使用更新的 Unreal 代码和过时的 Android SDK 代码构建可能会在尝试在移动 VR 中运行时创建难以调试的错误。记得保持您的 CodeWorks 更新。

打开 Windows PowerShell 并导航到 Android SDK 目录中的platform-tools目录。默认情况下,这将是C:\NVPACK\android-sdk-windows\platform-tools。从这里,键入./adb devices。您应该在这里看到您连接的设备的序列号,并在旁边带有device一词。如果显示为“unauthorized”,则需要在头显内部接受与 PC 的连接。如果显示为“offline”,则可能需要重新启动您的adb服务器。键入./adb kill-server,然后再次运行./adb devices

如果您正在使用移动设备进行开发,无论如何,您都将花费大量时间在 PowerShell 中与设备进行通信。花时间学习 ADB 尤为重要。当出现问题时,您将使用 ADB 来找出问题所在。在此处了解更多信息:developer.android.com/studio/command-line/adb

如果您的./adb设备看起来不错,您应该准备好将项目发布到设备上了。

在编辑器工具栏上的启动下拉菜单中,选择与您设备的序列号匹配的 Android 条目。

启动过程应该开始了。正如我们在第二章中提到的设置开发环境,第一次执行可能需要一些时间。

设置游戏模式和玩家角色

现在我们已经设置了一个基本场景并验证了它在平台上运行,让我们开始构建一些功能。

创建一个 VR 角色

我们首先需要做的是创建一个代表玩家的角色。角色是一种可以由玩家或 AI 控制的演员类型。在我们的情况下,我们将创建一个玩家可以控制的角色。

虚幻引擎是一个面向对象的系统。这意味着引擎是围绕称为对象的离散项组织的。一个对象由属性组成,你可以通过查看地图中选择的项目的详细面板来看到这些属性,以及函数,你通常可以在蓝图编辑器中看到这些函数。对象经常从彼此继承,因此一个新的对象类可能使用另一个类作为其父类来创建。这意味着新类将继承其父类的属性和行为,但可以更改这些属性和行为或添加新的属性和行为。因此,一个演员是对象类的子类,它添加了在世界中放置的能力。角色是一种可以由玩家或 AI 控制的演员类型。当我们使用角色作为父类创建自己的类时,我们正在设置该类来承担角色的所有功能,然后更改其行为或添加我们自己的功能。

让我们在内容浏览器中导航到我们的Content/HelloVR/Assets/Blueprints文件夹,在文件夹中的任何空白处右键单击,然后选择创建基本资产 | 蓝图类:

在随后的对话框中,我们将被要求选择我们新蓝图的父类。选择 Pawn:

在我们的Blueprints目录中将创建一个新的蓝图资产。让我们将其命名为BP_VRPawn

当你给你的资产命名时,遵循一个命名约定是一个好习惯。命名约定是在你思考为你正在创建的新事物命名时遵循的一组规则。通过在命名对象时遵循规则,你可以更容易地看到一个对象是什么,或者记住你给它起的名字。在这种情况下,我们使用BP_ 前缀作为提醒,我们的角色是一个蓝图类。一个特别全面和深思熟虑的命名约定在这里:github.com/Allar/ue4-style-guide

一会儿,我们将开始修改我们的角色,但是首先,我们需要告诉地图使用它。

创建一个游戏模式

每当虚幻加载一个地图时,它首先检查规定地图行为的规则。这些规则可以指定许多事情,但我们现在关心的是从Player Start对象生成什么样的角色。这些规则的集合存在于一个称为游戏模式的类中。

让我们创建一个游戏模式。在空白处右键单击,创建一个蓝图类,并选择 Game Mode Base 作为其父类。我们将其命名为BP_VRGameMode

双击我们的新游戏模式以打开它,在其详细信息部分,选择 Classes | Default Pawn Class 下拉菜单,并选择我们刚刚创建的BP_VRPawn类:

对于我们现在的目的,这就是我们需要做的关于游戏模式的一切。我们只是使用它来指定我们想要加载的角色类。编译并保存它。

蓝图是一种编译语言。在您编写的代码可以由 CPU 运行之前,需要将其转换为 CPU 可以理解的语言。这可以通过两种主要方式实现。解释语言在运行时即时翻译。然而,这样做会带来一些成本,因为解释器需要在您的代码旁边运行并尝试在运行时进行翻译。将所有内容离线翻译为准备好在 CPU 需要运行时运行的单独进程中,这样处理速度更快。这是编译语言处理事物的方式,当您编译蓝图时,这就是您所做的事情。

默认情况下,当蓝图被编译时,它们被编译为一种格式,然后由虚拟机使用该蓝图代码托管您的应用程序运行时。这个系统运行速度很快,但如果您想从中挤出更多的速度,您可以选择将它们转换为本机 C++,然后允许它们编译为机器代码。此时,它们可以像直接在 C++中编写的代码一样快速运行。

分配游戏模式

现在,我们需要告诉我们的项目使用这个游戏模式作为默认模式。

打开项目设置,并在项目|地图和模式|默认模式下,将我们的默认游戏模式设置为我们新创建的游戏模式:

现在,我们项目中加载的任何关卡都将使用此游戏模式来决定生成什么以及运行场景时遵循什么规则。

为特定地图覆盖游戏模式

如果我们想要其中一个地图使用不同的游戏模式怎么办?例如,如果我们设置了一个入口菜单场景,我们可能希望生成一个专门用于与菜单交互的棋子,以替代默认的玩家棋子。幸运的是,这很容易实现。

如果尚未可见,请选择窗口|世界设置以打开我们的世界设置选项卡。在世界设置中,在游戏模式下,将游戏模式覆盖设置为我们刚刚创建的新 BP_VRGameMode:

我们刚刚告诉引擎在加载此地图时使用我们的新游戏模式,而不管项目设置中指定了什么游戏模式。

有四个地方可以指定要使用的游戏模式:

  • 您可以在项目设置|地图和模式|默认模式|默认游戏模式中设置它。在项目中,除非有其他覆盖,否则此处指定的游戏模式将默认加载。

  • 您可以在单个地图中设置游戏模式覆盖,就像我们在这里所做的那样。如果设置了全局默认游戏模式,它将覆盖项目设置中的全局默认游戏模式。

  • 当启动可执行文件时,您可以使用命令行参数?game=MyGameMode来指定游戏模式。这将覆盖默认游戏模式以及地图中设置的任何覆盖。

  • DefaultEngine.ini中,您可以指定在加载具有特定前缀的地图时要加载的特定游戏模式。如果设置了此项,它将覆盖任何其他规定。

将一个棋子直接放在世界中

虽然通常最好使用游戏模式和玩家起始对象将玩家棋子放入世界中,但您不必这样做,有时您会遇到一些现有项目,例如默认的 VR 模板项目,不使用游戏模式来设置玩家棋子。

在这些情况下,不要在您希望玩家生成的场景中放置玩家起始对象,而是直接将您的棋子蓝图拖放到场景中。如果场景中已经有一个现有的玩家起始点,请将其删除。

记住我们说过棋子可以由玩家或 AI 控制吗?由于没有游戏模式来完成这项工作,您需要将您的棋子置于玩家控制之下。选择刚刚放置在关卡中的棋子,在其详细信息中找到 Pawn | Auto Possess Player,并将值设置为 Player 0。这将在生成到世界中时将棋子置于玩家控制之下:

一般来说,最好使用 GameMode 来指定玩家角色类,但你应该知道这种方法的存在,因为你会看到一些项目使用它。

设置 VR 角色

现在,我们已经创建了一个 VR 角色并设置了游戏模式来使用它,让我们修改这个角色以适当地在 VR 中使用。我们将从头开始做这个。通常情况下,当你创建一个简单的 VR 应用程序时,你会使用 VR 模板提供的角色类,但我们不希望你把它当作一个支撑。更好的做法是了解如何为 VR 构建角色,这样你就可以根据需要适当地构建它。

我们要做的第一件事是打开我们的角色。

添加相机

在蓝图编辑器视图的左上角,你应该看到一个组件选项卡。点击绿色的+Add Component 按钮,在下拉菜单中选择 Scene 创建一个场景组件。将其命名为Camera Root

组件是可以添加到蓝图对象中的附加元素。有各种各样的组件可供选择,它们都有不同的功能。组件按层次结构组织,允许你将组件附加到其他组件上。你可以通过这种方式做很多事情。

现在,创建一个新的相机组件。如果在创建相机组件时仍然选择了 Camera Root 场景组件,则相机组件将作为 Camera Root 的子组件创建。如果没有选择,将其拖动到 Camera Root 上,将 Camera Root 设置为其父组件。

通常情况下,像我们在这里做的这样设置一个单独的根组件是一个好主意。这样可以更灵活地更改角色的结构,或者更改组件(如相机)的旋转或位置,而无需调整对象的位置。

添加运动控制器

接下来,选择DefaultSceneRoot组件,并创建一个Motion Controller组件。对于这个组件,在添加组件菜单的顶部使用搜索组件栏,输入mot以缩小搜索范围到运动控制器组件。使用这个搜索栏可以节省很多时间。将这个新组件命名为MotionController_L,确保它是DefaultSceneRoot的子组件,而不是 CameraRoot 或 Camera 的子组件。

选择DefaultSceneRoot,再次执行上述步骤创建第二个运动控制器组件。将其命名为MotionController_R,并确保它是DefaultSceneRoot的子组件,而不是任何其他组件:

现在,你的组件层次结构应该看起来像上面的截图。

在继续之前,我们需要设置一些运动控制器组件的属性。选择MotionController_R组件,在其详细面板中找到 Motion Controller | Motion Source 条目。将其设置为 Right,以允许右手的 Oculus 或 Vive 控制器移动控制器。顺便说一下,确保MotionController_L仍然设置为使用 Left 作为其运动源。默认情况下应该是这样的:

让我们也将这两个控制器设置为可见,以便我们可以验证它们是否正常工作。从每个运动控制器组件的详细面板中,选择可视化|显示设备模型。打开此选项,并验证显示模型源是否仍设置为默认值,这将简单地显示你正在使用的运动控制器硬件的模型。我们稍后会替换我们的运动控制器显示,但现在,我们只是想看到它们,以便我们可以验证我们已经正确设置它们:

设置我们的跟踪原点。

现在,我们需要告诉我们的角色如何解释追踪空间中头戴设备的位置。在您的角色的组件选项卡下方寻找“我的蓝图”选项卡,如果您的事件图面板在主编辑窗口中尚不可见,请在“我的蓝图”选项卡中双击“图表|事件图”以显示它:

一旦进入事件图,找到 BeginPlay 事件,或者在图形编辑器中右键单击任意位置并在出现的搜索对话框中键入beginplay以查找或创建 BeginPlay 事件。从 BeginPlay 事件拖动执行线并右键单击以创建一个新节点。找到输入|头戴显示器|设置跟踪原点,或者开始在搜索框中键入以找到它。创建一个“设置跟踪原点”节点,并将其原点设置为地板高度,如果您使用的是房间规模的 VR 系统,如 HTC Vive 或带有触摸控制器的 Oculus Rift,或者将其原点设置为眼睛高度,如果您使用的是非房间规模的系统,如 Oculus Go 或较旧的单摄像头 Oculus Rift。

调整我们的玩家起始位置到地图中。

最后,我们需要调整地图中的玩家起始位置。在您的世界大纲中找到它(您可以使用搜索栏更快地找到它,然后选择它并将其拖动到场景中,直到其中心与地板相交(这是一种不太正式的对齐角色的方法,我们稍后会更好地完成这个工作,但现在它可以工作):

在头戴设备中进行测试。

我们现在拥有了在虚幻中创建 VR 体验所需的基本组件。我们有一个已经正确设置为在 VR 中高效运行的项目,以及一个可能还没有太多功能的角色,但已经准备好作为我们真正想要做的事情的基础。

让我们进行测试。使用 VR 预览启动地图,并验证您的视图是否位于正确的高度,并且在移动手时是否可以看到您的动作控制器。帧率也应该是可以接受的。

打包独立构建

当我们将虚幻应用程序分发给其他用户时,通常不会给他们编辑器的源文件。相反,我们将项目打包成一个可以在目标平台上运行的独立可执行文件。

让我们创建一个 Windows 独立可执行文件。

选择文件|打包项目|Windows|Windows(64 位)以启动打包过程。将被问到放置的位置。选择一个有意义的位置。(通常,在项目目录中创建一个“打包”目录是合理的。您可以将打包的构建放在任何您想要的地方。)当构建状态对话框出现时,点击“显示输出日志”以查看它正在做什么:

预计这个过程会花费一些时间。

一旦进程完成,关闭编辑器并检查您告诉系统构建可执行文件的位置。您应该在其中看到一个WindowsNoEditor文件夹。在其中,您应该看到一个带有项目名称的可执行文件。启动可执行文件。如果在项目设置中设置了在 VR 中启动标志,它应该直接启动到您的头戴设备。

总结

恭喜!我们涵盖了很多内容。在本章中,我们经历了创建起始 VR 项目并正确设置其在目标硬件上运行的过程。我们学会了在设置新的 VR 项目时如何决定使用哪些设置,以及如何在虚幻项目目录中找到我们的方式。我们还了解了在 VR 开发中使用的一些重要虚幻引擎功能:

  • 实例化立体

  • 循环轮询遮挡

  • 前向渲染

  • 多重采样抗锯齿(MSAA)

  • [移动]移动多视图

  • [移动]单眼远场渲染

我们学会了如何从一个项目迁移内容到另一个项目,并在其到达后如何清理我们的“内容”目录。

最后,我们设置了一个基本的 VR 棋子,并设置了一个游戏模式来指示地图加载它。在使用棋子时,我们了解了如何使用组件来将简单的部件组装成复杂的对象,添加了相机和跟踪运动控制器。最后,我们设置了我们棋子蓝图的第一个元素,以适应我们的 VR 硬件的跟踪原点,并测试了我们的地图。

在下一章中,我们将使在本章中创建的棋子能够在世界中移动。我们将使用蓝图来创建一个传送移动方案,并学习如何设置环境来支持它,然后我们将继续实现一系列沉浸式移动方案。

第四章:在虚拟世界中移动

在本章中,我们将使用前一章中构建的角色,使其在世界中移动。我们将从常用的传送移动方案开始,涵盖一系列设置任务。我们将了解环境中的导航网格,如何在项目中设置输入事件并在蓝图中使用它们,以及如何构建一个玩家角色蓝图并使其在世界中移动。最后,我们还将探讨一种沉浸式的无缝定位方案,您可以使用它让玩家在世界中移动而无需传送。

在本章的过程中,我们将讨论以下主题:

  • 导航网格-它们是什么,如何在级别中设置它们,以及如何优化它们

  • 如何为玩家角色设置蓝图,以及如何创建角色可以使用的输入事件

  • 如何使用直线和曲线进行追踪,以在环境中找到合法的目标位置

  • 如何创建简单的游戏内指示器,向玩家展示正在发生的事情

  • 如何实现无缝的定位方案,为那些不适合传送的项目提供沉浸式移动

这将涉及很多内容,但应该很有趣,您将获得一个良好的基础,帮助您弄清楚如何开发您想要的东西,以及在看到其他开发人员的蓝图时如何理解他们在做什么。在本章中,我们将以与大多数教程不同的方式进行。作为一名有效的开发人员,学习如何思考问题比仅仅记住一系列可能不适用于您面临的下一个问题的步骤更重要得多。在本章中,我们将逐步介绍构建元素的过程,然后在某些情况下发现其中的错误。之后,我们需要更改这些内容以修复这些错误。这种方法的真正价值在于,您将开始逐渐了解如何通过迭代开发软件,这才是真正的开发方式。这里的目标不是让您擅长构建这些教程,而是帮助您成为一个可以独立实现自己想法的开发人员。

说了这么多,让我们开始建设吧!

传送定位

正如我们在第一章中讨论的那样,VR 中面临的最大挑战之一是当用户尝试移动时引发的晕动病。其中最常用的解决方案之一是将用户从一个地方传送到另一个地方,而不是让他们在空间中平滑移动。这会破坏沉浸感,但完全避免了晕动病的问题,因为它根本不会产生运动感。对于沉浸式移动不是优先考虑的应用,比如建筑可视化,这可能是一种理想的方案。

创建导航网格

实现基于传送的定位方案所需的第一件事是告诉引擎玩家可以移动的位置和不允许移动的位置。我们可以使用导航网格来完成这个任务。

导航网格,通常缩写为 navmesh,是在虚幻级别中自动生成的一组表明可行走地板的表面。AI 控制的角色使用导航网格在世界中找到自己的路,但它也可以用作识别玩家角色安全着陆目的地的方式,就像我们在这里的传送系统中所做的那样。

在虚幻引擎中创建导航网格相当简单。从模式面板中选择体积选项卡,找到导航网格边界体积。将其拖入场景中,如下图所示:

从模式 | 体积中选择导航网格边界体积

移动和缩放导航网格边界体积

NavMesh 边界体积需要围绕任何您希望玩家能够传送的地板。让我们使我们的导航网格可见,以便我们可以看到可行走的地板正在设置的位置:

  1. 按下P键切换导航可见性,或者从视口菜单中选择显示|导航:

使用 P 键或者选择显示|导航来在环境中显示生成的导航网格。

如果在放置 NavMesh 边界体积后看不到任何可导航空间,请确保它与可行走的地板相交。该体积设置了导航网格生成的边界,因此如果它在地板上方,它将不会生成任何东西。

当然,我们刚刚放置的 NavMesh 边界体积太小了。让我们将其扩展以覆盖我们想要移动的空间。我们将通过缩放体积来实现这一点。

  1. 按下R键切换到缩放模式,或者只需轻按空格键直到缩放工具出现。

我们可以从透视视图缩放体积,但对于这种操作,通常最好切换到正交视图,以便我们真正看到我们在做什么。

  1. 按下Alt + J键或使用视口的视图选择器切换到俯视图:

使用菜单或相关的快捷键切换到正交俯视图。

  1. 将导航网格缩放以覆盖建筑物的可行走区域。

通过可见的导航,您可以看到它正在生成导航网格表面以及它是否在合理的范围内工作:

我们的关卡的俯视图显示了 NavMesh 边界体积的范围。

在我们的情况下,我们期望可行走的建筑物部分尚未覆盖。这是因为我们尚未对边界体积的高度进行任何处理,而这些区域的高度太高或太低,无法适应其中。让我们跳转到侧视图来修复这个问题。

  1. 按下Alt + K键跳转到左视图,或者从视口视图选择中选择左视图。

  2. 将边界体积缩放到合理覆盖地板的比例:

关卡的侧视图。您可以在这里看到我们正在缩放 NavMesh 边界体积以包围地板

  1. 按下Alt + G键跳回透视视图并查看我们的进展。或者,您可以从视图选择器中选择透视视图。

值得记住这些改变视图的按键。您会经常使用它们,而且能够快速切换非常方便。Alt + JKH切换视角。Alt + 2切换到线框视图,Alt + 4切换回实体视图。还有很多其他快捷键,但您会经常使用这些。

如果我们飞到寺庙的后面,我们会发现这里有一个问题。我们的导航网格在后面的走廊中没有按预期生成。让我们弄清楚这里发生了什么:

在这里我们可以看到我们的关卡的一部分没有被导航网格正确覆盖。

修复碰撞问题

导航网格没有生成在您期望的位置通常有两个原因。要么您的体积没有围绕您尝试生成网格的区域,要么该区域的碰撞有问题。让我们来看一下:

  1. 按下Alt + C键查看后厅的碰撞,或者按下显示|碰撞。

看起来没有任何杂散的碰撞侵入到走廊中,所以可能是地板上缺少碰撞。

  1. 选择问题区域的地板。

  2. 在其详细信息中,找到其静态网格并双击打开它:

使用详细面板找到问题地板区域的静态网格。

  1. 在静态网格编辑器中,选择碰撞工具栏项,并确保勾选了“简单碰撞”:

查看静态网格的简单碰撞

确实,我们的简单碰撞丢失了。让我们修复这个问题。

  1. 选择碰撞|添加简化碰撞盒,为我们的地板添加一个简单的碰撞平面。

好多了。现在我们应该看到我们期望的 navmesh 已经在我们的主要层级中生成:

为我们的地板网格创建简化碰撞

在继续之前,让我们花一点时间来谈谈这里发生的情况。在实时软件中,我们经常需要做的一件事是确定一个对象何时碰撞到另一个对象。Unreal 使用碰撞网格来实现这一点。碰撞网格是简化的几何体,用于检查与世界中其他碰撞网格的相交。

演员有两个:

  • 一个复杂碰撞网格。这只是模型的可见网格。

  • 一个简单碰撞网格。这是一个较少详细的凸网格,围绕着物体。这些通常在导入对象时生成,或者可以在创建模型的 DCC 中显式创建。如果缺少它,您可以在编辑器中创建一个简单的碰撞,就像我们在这里所做的一样。作为最后的手段,您可以将详细信息|碰撞|碰撞复杂性设置为使用复杂碰撞作为简单碰撞,以将对象的可见网格用于所有碰撞计算。不过,对于具有大量多边形的网格,请不要这样做。这是昂贵的。

碰撞检测和处理是一个相当深入的主题,超出了本书的范围,但对于我们在 VR 开发中的目的,我们将非常关心对象的简单碰撞网格,因为我们将使用它们作为可行走的表面来检测另一个对象何时碰撞到它们,以及是否可以抓取它们,以及其他许多用途。

从 navmesh 中排除区域

在查看我们的地图时,我们还有一些问题需要解决。我们的 Navmesh Bounds Volume 在一些我们不希望玩家传送的区域生成了 navmesh。让我们也修复这个问题:

  1. 按下Alt + 2切换到线框视图,或使用视口的视图模式选择器切换到线框视图。

我们可能有一些问题可以通过调整 NavMesh Bounds 体积的比例来解决。如果我们的 navmesh 在屋顶或窗台上生成,让我们将 Bounds 体积的垂直比例减小,以排除这些区域。这是一个可以通过按下Alt + K跳转到侧视图来帮助的地方。

如果我们的 NavMesh Bounds 体积扩展到建筑物外部的范围超出了需要的范围,我们可以使用Alt + J跳转到顶视图,并调整它以更好地适应。

我们仍然会有一些剩余的杂散区域需要排除,而这些区域不能简单地通过调整体积来修复。对于这些区域,我们将使用 Nav Modifier Volumes。请参考以下步骤:

  1. 从 Modes 面板中获取一个 Nav Modifier Volume,并将其拖入场景中。

  2. 移动和缩放它,直到它围绕着生成不需要的 navmesh 的区域。

当 nav 修改器体积围绕它时,您将看到该区域的 navmesh 消失。查看详细面板中的 nav 修改器体积属性。您是否看到默认|区域类别设置为 NavArea_Null?这告诉 navmesh 生成器在此区域中不生成 navmesh。您可以从下拉菜单中看到它还可以用于标记障碍物和爬行空间,但对于我们在这里要做的事情,我们不关心这些。我们只关心使用它来清除不需要的导航。

  1. 将这些拖到场景中,根据需要清理杂散的部分。您可以在拖动修改器体积时按住Alt键进行复制,或按下Ctrl + W进行复制:

透视线框视图对于查找导航覆盖问题非常有用。

在移动物体时,记住熟记变换热键会很有帮助。按下 W 键激活“平移”工具,可以让你滑动物体。按下 E 键激活“旋转”工具,按下 R 键激活“缩放”工具。按下空格键也可以循环使用这些工具。按住 Ctrl 键+W 键可以复制一个物体,拖动物体时按住 Alt 键也可以复制它。

完成后,你应该有一系列阻挡玩家站立的导航修改体积。

在你不希望出现奇怪的导航网格的地方飞行,确保没有问题。在发现问题时,通过缩放导航网格边界体积或添加导航修改体积来修复问题。

修改导航网格属性

在我们继续之前,还有一件事情你应该知道,那就是如何调整刚刚生成的导航网格的属性。

如果你需要改变它的行为,选择RecastNavMesh对象,它将在你的关卡中创建。在其详细面板中,你可以看到控制其生成、查询和运行时行为的属性。

我们不会在这里详细介绍它们,只是提醒你其中一个属性:如果你想调整一个玩家可以适应的区域的大小,你可以调整代理半径来实现。将其缩小将使玩家适应更狭窄的空间。同样,你可以调整代理高度和最大高度来确定导航应该生成的可接受天花板高度。通常,在你疯狂微调导航修改体积之前,你会想要对这些值进行更改,因为这里的更改会改变导航网格的生成位置。对于我们的目的,我们将保持这些值不变。

设置兵棋蓝图

现在我们已经在场景中构建和调整了导航,我们可以通过按下 P 键关闭导航可视化,并开始处理我们的运动行为。

为了实现传送运动方案,我们需要做三个工作:

  • 弄清楚玩家想要移动到哪里

  • 弄清楚玩家实际上被允许移动到哪里

  • 将玩家移动到新位置

让我们开始工作吧。

迭代开发

我们将以迭代的方式开发这种方法,就像你从头开始开发一样。大多数教程只是带你完成构建完成方法的步骤,但这种方法的问题在于它不教你为什么要做你正在做的事情。一旦你想做类似的事情,但又不完全相同,你就又回到了原点。

相反,我们将分阶段进行工作。

杰出的软件开发者肯特·贝克给开发者提出了这样的建议:“让它工作,让它正确,让它快。”

重要的是你做事情的顺序。一开始似乎几乎是显而易见的,但很少有开发者在刚开始时就做对。如果按照这个顺序工作,你将节省很多痛苦。

让它工作

构建一个大致的组装,测试早期和频繁。使其易于测试和易于更改。不断更改,直到你满意它正在做正确的工作。

让它正确

现在你知道你的代码需要做什么了,弄清楚你应该如何真正组织它。有没有更好或更清晰的方法来做你试图做的事情?有没有可以重复使用的部分?这段代码是否需要在其他地方使用?如果需要,你能调试它吗?以“让它工作”的阶段为起点,但现在你明白你真正需要做什么了,正确地编写它。在第一阶段制造混乱是可以的(事实上,如果你没有制造混乱,那么你可能做错了),但在这个阶段清理这个混乱。

让它快

一旦您有了合理干净的代码,能够正常工作,寻找可以使其运行更快的方法。是否有一个结果,您可以将其缓存到变量中并重复使用?您是否反复检查条件,即使您知道它们只会在某些事件发生时改变?您是否复制了可以直接从其原始位置读取的数据?找出您可以更高效地做什么,并在可以的地方加快速度。但要小心,在这里有些优化可能对运行应用程序没有明显的影响。选择大的优化,并使用性能分析工具了解您真正的问题所在。您要确保优化的是真正会产生差异的东西。此外,在优化代码时要小心不要使其更难以阅读或调试。将帧时间减少一点但使类难以更新或维护的更改可能不值得。在优化时要谨慎使用判断。

按顺序进行操作

许多新开发者会在优化代码之前就开始尝试优化代码,而没有确保自己正在做正确的事情。这只会浪费时间,因为很可能会丢弃其中的一些代码。其他开发者跳过了“让它正确”的阶段,并在似乎工作正常时认为他们的工作已经完成。这也是一个错误,因为代码的 80%的生命周期都用于维护和调试。如果您的代码能够工作但是一团糟,您将花费大量额外的时间来保持其运行。

在开发初期匆忙或粗心的工作所造成的问题通常被称为“技术债务”。这些是你以后需要修复的东西,因为即使它能运行,但可能不够灵活、健壮,或者只是一团难以理解的混乱。清理技术债务的时间是在完成“让它工作”阶段之后,而在继续其他工作并在需要更改的基础上构建更多代码之前。

按照这个顺序并将其视为离散阶段来进行工作将使您成为一个更有效的开发者。

从右手控制器设置一条射线追踪

让我们从获取玩家想要去的位置开始设置我们的传送功能:

  1. 打开 BP_VRPawn 蓝图,并打开我的蓝图|图表|事件图,如果尚未打开。

我们应该在事件图中仍然看到BeginPlay事件,其中我们设置了跟踪原点。现在,我们将在事件 Tick 中添加一些代码。

每次引擎更新帧时都会调用 Tick 事件。在 Tick 事件中不要放太多工作,因为它们会影响性能。

  1. 如果在事件图中还没有看到 Event Tick 节点,请在图中的任何位置右键单击,输入tick在搜索框中,然后选择添加事件|事件 Tick。如果已经定义了一个 Tick 事件,这不会添加一个新的事件,而只会将您带到事件图中的该节点。如果没有,现在将创建一个。

  2. 在 Event Tick 的右侧单击,添加一个按通道进行线性追踪。

当执行线性追踪时,您提供一个“起点”和一个“终点”,并告诉它您要查找的“碰撞通道”。如果一个具有设置为提供的碰撞通道的碰撞的 actor 与起点和终点之间的线相交,追踪将返回true,并返回有关它所击中的信息。我们将利用这种行为来找到我们的传送目的地。

让我们从右手控制器的位置开始追踪:

  1. 从组件列表中获取 MotionController_R,并将其拖动到事件图中。

  2. 我们希望从运动控制器的位置开始追踪,所以让我们从 MotionController_R 的返回值中拖出一个连接器并释放。

  3. 在弹出的对话框中,输入getworld并选择 GetWorldLocation:

蓝图节点的创建默认是上下文敏感的。这意味着如果你从另一个对象拖动连接,你只会看到适用于该对象的操作。

  1. GetWorldLocation的结果拖入 Line Trace 节点的 Start 输入引脚。

现在,让我们设置追踪的终点。我们将在距离起始位置 10,000 个单位的点结束追踪,朝向控制器的方向。让我们进行一些简单的数学计算,找出那个点在哪里。

  1. MotionController_R的输出中创建一个Get Forward Vector节点。

这将返回一个长度为 1 的向量,指向控制器所面向的方向。我们说过我们希望终点距离起点为 10,000 个单位,所以让我们将我们的 Forward 向量乘以该值。

  1. Get Forward Vector的返回值拖出并在搜索栏中输入*。选择向量*浮点数。

现在,从浮点输入拖出一个连接器到乘法操作,并选择 Promote to Variable:

这是在蓝图中快速创建变量的方法。你可以简单地从输入中拖出,选择 Promote to variable,

并且将创建一个具有正确类型的变量以供输入使用

  1. 将新变量命名为TeleportTraceLength,编译蓝图,并将变量的值设置为10000

你可以直接在乘法操作的浮点输入中键入10000,但这样做是不好的实践。如果你在蓝图中随处隐藏数值,当你需要更改它们时,你将很难找到它们。此外,键入到输入中的数字并不能解释它是什么。相反,变量可以被赋予一个描述其值改变时实际发生的事情的名称。在你的代码中没有解释的数字被开发人员称为魔法数字,它们是技术债务的一个例子。当你需要维护或调试代码时,它们只会给你带来麻烦。除非一个值在其上下文中绝对明显,否则请使用一个变量,并给它一个有意义的名称。

现在,我们有了一个长度为 10,000 个单位的向量,指向控制器的前方,但现在它将从世界的中心运行 10,000 个单位,而不是从控制器开始,这不是我们的意图。让我们将控制器的位置添加到这个向量中以修正这个问题:

  1. 从控制器的GetWorldLocation调用中拖出另一个连接器,并在搜索栏中输入+。选择向量+向量。

  2. 将我们的前向量乘法的输出拖入另一个输入。

  3. 将此加法的输出连接到LineTraceByChannel的 End 参数:

在继续之前,让我们设置一些调试绘图,以查看到目前为止是否一切都按我们的预期运行。

  1. 按住B键并点击Line Trace节点右侧的空白处,创建一个Branch节点。(你也可以右键单击并像通常那样创建一个 Branch 节点,但这是一个有用的快捷方式。)

  2. Line Trace节点的布尔返回值拖出一个连接器到这个分支的条件。

如果追踪操作命中了某个物体,它将返回True,如果没有命中,则返回False。我们只对命中物体进行调试绘图,所以我们只使用分支的True输出。

如果我们确实命中了某个物体,我们需要知道命中发生的位置。

  1. 从 Out Hit 拖出一个连接器,并选择 Break Hit Result 以查看命中结果结构的成员。

结构体是一组捆绑在一起的变量,可以被赋予一个名称并作为一个单独的单元传递。Hit Result结构体是一个常用的结构体,描述了检测到的碰撞的属性,告诉你发生碰撞的位置、被击中的演员和许多其他细节。在结构体上调用break可以查看其内容。

现在,让我们画一条表示我们的跟踪的调试线:

  1. 从我们的Branch节点的True输出拖动一个执行线,并创建一个Draw Debug Line动作。

  2. Hit Result结构体中的位置拖动到Debug Line调用的 Line End 输入中。

  3. 将击中结果的跟踪起点拖动到线的起点。

  4. 将线的粗细设置为2,并将其颜色设置为你喜欢的任何颜色。

顺便说一下,让我们在击中位置处画一个调试球体:

  1. 创建一个Draw Debug Sphere节点。

  2. 将其执行输入连接到调试线的输出。

  3. 将其中心设置为击中结果的位置:

请注意,Draw Debug调用仅在开发版本中起作用。它们对于理解正在发生的事情很有用,但它们只是调试工具,需要用实际软件的真实可视化替换。我们很快就会做到这一点。

  1. 让我们来测试一下。你的结果应该看起来像这样:

很好。到目前为止,它正在按我们的预期进行——从控制器发射一条射线,并显示它击中表面的位置。然而,问题是它同样可以击中墙壁和地板。我们需要将其限制在有效的传送目的地上。让我们来做这个。

改进我们的跟踪击中结果

我们首先要做的是设置一个简单的测试,只接受朝上的表面。我们将使用一个称为点积的向量运算来将表面法线与世界的上向量进行比较。按照以下步骤开始:

  1. 在我们的击中结果拆分的右侧某处右键单击,创建一个点积节点。

  2. 将击中结果的法线拖动到第一个输入中,并将第二个输入的Z值设置为 1.0。

法线是垂直于其延伸表面的向量。点积是一种数学运算符,返回两个向量之间夹角的余弦值。如果两个向量完全平行,它们的点积将为 1.0。如果它们完全相反,它们的点积将为-1.0。如果它们完全垂直,点积为 0。

由于向量(0,0,1)是世界的上向量,通过测试表面法线与该向量的点积,我们可以通过检查点积是否大于 0 来判断法线是否朝上。

  1. 从点积的结果中拖动一个连接器,并选择>运算符。

  2. 使用此结果作为条件创建另一个分支运算符。

  3. 按住Alt并单击 Draw Debug Line 节点的执行输入以断开连接。

  4. 从返回值的分支中拖动一个新的执行线到这个新的分支。

  5. 将点积的分支的 True 输出与我们的 Draw Debug Line 节点连接起来:

让我们来测试一下。我们会发现当射线击中地板时,我们现在看到了调试球体的绘制,但当它击中墙壁或天花板时却没有。正如我们刚才提到的,这是因为墙壁的法线与世界的上向量的点积将为 0,而天花板与世界上的点积为-1。

这样做更好了,但是我们决定不让玩家去的地方怎么办?我们花了那么多时间设置我们的导航网格边界和导航网格修改器,但我们还没有使用它们。我们应该修复这个问题。

使用导航网格数据

现在,我们要进一步测试,寻找离我们指针指向的位置最近的导航网格点:

  1. 在图表中右键单击,创建一个 Project Point to Navigation 节点。

  2. 将击中结果的位置输出连接到这个新节点的点输入

  3. 将节点的 Projected Location 输出与 debug line 的 Line End 和 Debug Sphere 的 Center 连接起来,替换之前在那里使用的位置输入:

我们在这里做的是查询我们创建的导航网格,找到离我们提供的位置最近的网格上的点。这将防止选择我们从网格中排除的位置。

然而,当我们环顾四周时,我们会发现我们将会遇到一个问题。直接从控制器发射射线将无法让我们传送到比我们当前站立位置更高的位置,因为射线无法击中更高的地板。这是我们系统的一个缺陷,我们需要重新考虑这个问题。

这就是为什么在我们投入大量工作之前坚持做一个“让它工作”的阶段非常重要的原因。通常情况下,你的第一个运行原型会揭示出你需要重新考虑的事情,最好在你付出大量努力之前尽早发现这些问题。

从线追踪切换到抛物线追踪

经过思考,我们清楚地意识到,为了到达比我们当前视点更高的点,我们需要一个曲线路径。让我们修改我们的追踪方法以实现这一点。这是我们将得到的结果:

用于计算抛物线的数学方法实际上相当简单,但我们还有一个更简单的选择。Predict Projectile Path By TraceChannel方法已经为我们处理了数学计算,并且可以节省我们一些时间。让我们现在使用它:

  1. 断开我们的 Event Tick 与旧的 Line Trace By Channel 节点的连接。

  2. 在图表中右键单击,创建一个 Predict Projectile Path by TraceChannel 节点。

  3. 将其连接到我们的 Tick。

  4. 将其 Trace Channel 设置为 Visibility。

  5. 接下来,将 MotionController_R 的 GetWorldLocation 的输出连接到 Start Pos 输入。

为了获得我们的发射速度,我们将使用 MotionController_R 的 Forward Vector,并将其乘以一个任意值:

  • 断开旧的TeleportTraceLength变量与 Multiply 节点的连接。

  • 从 Multiply 节点的 float 输入处拖出一个新的连接器,并将其提升为一个变量。让我们将其命名为TeleportLaunchVelocity

  • 编译我们的蓝图,并给它一个值为 900。

  • 将结果连接到 Launch Velocity 输入:

现在,让我们绘制结果路径,以便验证它是否按照我们的预期进行。

绘制曲线路径

Predict Projectile Path By TraceChannel方法将返回一个描述抛物线路径的点的数组。我们可以使用这些点来绘制我们的目标指示器。让我们开始吧:

  1. 就像我们之前做的那样,将一个 Branch 连接到我们的 Return Value。我们只对得到一个好结果时才感兴趣。

现在,为了绘制曲线路径,我们实际上需要绘制一系列的 debug line,而不仅仅是一个。

  1. 让我们从 Out Path Positions 拖出一个连接器并创建一个 ForEachLoop 节点:

我们应该花点时间来讨论我们在这里做什么,因为这是一个你将经常使用的概念。

到目前为止,在我们的 pawn 蓝图中处理的所有变量都只包含单个值-一个数字,一个 true 或 false 值和一个向量。然而,Out Path Positions 的连接器看起来不同。它不是一个圆圈,而是一个 3 x 3 的网格。这个图标表示这是一个数组。数组不同于单个值,它包含一个值列表。在这种情况下,这些值是构成我们要绘制的曲线路径的点的列表。

For Each Loop是一种称为迭代器的编程结构。迭代器循环遍历值的集合,并允许您对集合中的每个元素执行操作。

让我们快速查看一下 ForEach Loop 的输出:

  • 循环体将为数组中的每个项目执行一次。

  • 数组元素是它找到的项目。

  • 数组索引是它找到的位置。数组总是从零开始编号,所以第一个项目的索引为 0,第二个项目的索引为 1,依此类推。

  • 当它到达列表的末尾时,将调用 Completed 执行引脚。

我们将使用这个循环来绘制曲线的线段,但是每个线段需要两个点,这意味着在数组中达到第二个点之前我们不能绘制任何东西:

  1. 从数组索引输出拖动连接器,并将其连接到一个整数|整数节点上。将第二个值保留为 0。

  2. 将其输出连接到一个分支,并将循环体连接到分支输入。这将允许我们跳过数组中的第一个值。

  3. 创建一个 Draw Debug Line 节点,并将数组元素连接到线段结束输入。由于我们从数组的第二个值开始,该位置上的点是我们线段的结束点。我们将通过获取它之前的点来获取线段的起点:

  1. 要找到我们的线段起点,从数组索引再拖动一个连接器,并从中减去 1。

  2. 现在,从 Out Path Positions 再拖动一个连接器,并在搜索框中输入Get。选择 Get(复制):

这将获取存储在数组中与给定索引对应位置的元素。

  1. 将我们的数组索引减 1 的结果连接到 Get 节点的整数输入上。这将检索当前迭代的前一个值。

  2. 将此 Get 节点的输出连接到 Draw Debug Line 的 Line Start:

完成后,绘图例程应该看起来像前面截图中显示的样子。

我们刚刚做的是遍历 Out Path Positions 中的每个路径位置向量,并且对于第一个之后的每个位置,我们从其前一个位置绘制一条线到当前位置,直到达到列表的末尾。

在绘制完所有线段后绘制终点

最后,让我们在追踪终点处绘制一个调试球体。我们可以重复使用之前用于绘制直线追踪末端的节点:

  1. 就像之前一样,从 Out Hit 中breakHit Result结构。

  2. 将其位置输入到 ProjectPointToNavigation 节点中。

  3. 将一个分支连接到其返回值,并将 True 分支的执行连接到一个 Draw Debug Sphere 节点。

  4. 将投影位置用作调试球体的中心。

然而,不要在绘制调试线节点之后立即调用它,而是从 ForEachLoop 的 Completed 输出中调用它,因为我们只需要在绘制完所有线段后绘制一次球体。

您的图表现在应该如下所示:

让我们测试一下,看看运行时会发生什么:

太棒了!我们现在正在投射一条曲线路径,这将使我们更容易在地图上移动,并且我们使用调试绘制来验证它给我们带来了良好的结果。

我们在这里使用的 Draw Debug 方法只适用于调试和开发版本。它们不包含在发布版本中。绘制这条路径的正确方法是使用 Out Path Positions 中的点集合来改变样条网格的形状,但是这超出了本书的范围。然而,在 VR 模板中有一个很好的例子,我们在这里所做的工作是理解他们在该项目的蓝图中所做的工作的良好起点。

接下来,让我们处理下一个任务,允许玩家传送到他们选择的目的地。

传送玩家

在这种情况下,我们首先需要做的是给玩家一种告诉系统他们打算传送的方式。

创建输入映射

我们将使用引擎输入映射来设置一个新的命名输入。让我们开始吧:

  1. 打开项目设置并导航到 Engine | Input。

  2. 点击 Bindings | Action Mappings 旁边的+号创建一个新的动作映射:

  1. 我们将把它命名为TeleportRight

这将创建一个名为 TeleportRight 的输入事件,我们可以在事件图中对其进行响应。

您可能已经发现,您可以直接在事件图中设置事件来监听控制器输入和按键。然而,对于大多数项目来说,将输入映射到这里是一个更好的主意,因为它为您提供了一个集中管理它们的位置。

现在,让我们指示哪些输入应触发此传送动作。在新的动作映射下方出现了一个下拉菜单,显示了 None 指示器。(如果下拉菜单不可见,请点击动作映射旁边的展开箭头。)让我们继续:

  1. 在 TeleportRight 下方,使用下拉菜单选择 MotionController (R) Thumbstick。

这将处理我们的 Oculus Touch 控制器映射,但对于不使用拇指杆的 HTC Vive 来说并没有帮助。

  1. 点击 TeleportRight 动作旁边的+号,添加另一个映射到该组。

  2. 为此选择 MotionController (R) FaceButton1:

您的绑定现在应该看起来像前面的截图所示。

现在,我们已经告诉输入系统发送一个名为 TeleportRight 的输入事件,无论玩家是否使用带有拇指杆或带有面部按钮的动作控制器。

这些绑定存储在DefaultInput.ini中,并可以在那里进行编辑,但通常在项目设置 UI 中设置它们更方便。然而,如果您需要将一堆输入绑定从一个项目复制到另一个项目,将DefaultInput.ini的内容从一个项目复制到另一个项目可能更方便。并非每个项目都有DefaultInput.ini。如果您的项目没有,您可以简单地添加它,引擎将使用它。

让我们关闭项目设置并返回到我们的 VRPawn 的事件图。您会发现,您现在可以在这里创建一个 TeleportRight 事件,因为我们在输入设置中定义了它。让我们这样做,如下所示:

缓存我们的传送目的地

现在,在我们处理此事件之前,我们需要存储我们之前在跟踪方法中找到的位置,以便在玩家尝试传送时可以在此处使用它:

  1. 在 My Blueprint | Variables 下,点击+号创建一个新变量。

  2. 将其类型设置为布尔型,并将其命名为bHasValidTeleportDest

变量名很重要。它们告诉读者(可能是另一个开发人员维护您的代码,也可能是将来的自己)变量代表什么。您的变量名应准确反映它们所包含的内容。对于 True/False 布尔变量,确保您的名称描述了它实际回答的问题。因此,在这种情况下,Teleport将是一个不好的选择,因为它并没有说明变量的值是否意味着玩家可以传送,正在传送,最近传送,还是只是喜欢幻想传送。对这些事情要清楚明确。bHasValidTeleportDest清楚地指示了它的含义。

在 C++中,将布尔变量的名称前缀为b是 Epic 编码风格指南的规定,但在 Blueprint 开发中也是一个好主意。(如果您计划在 C++中进行开发,您应该了解并遵循 Unreal 风格指南,可以在docs.unrealengine.com/en-us/Programming/Development/CodingStandard找到。)

  1. 创建另一个变量并将其命名为TeleportDest

  2. 将其类型设置为矢量。

让我们填充这些变量。我们关心的位置是我们在命中位置调用的 Project Point to Navigation 方法找到的 Projected Location。让我们存储我们是否找到了有效的位置。由于我们即将在调用之前添加一些节点,您可能希望将 Draw Debug Sphere 节点向右移动一点以腾出一些空间:

  1. 将您的bHasValidTeleportDest变量拖放到事件图上,并在询问时选择设置。

您是否看到 ForEach 循环的 Completed 输出与我们的 Project Point to Navigation 方法输出的 Branch 语句相连?

  1. 按下Ctrl +拖动执行输入到该 Branch 节点,将其移动到CanTeleport设置器上。(注意,当变量在图表中使用时,布尔变量上的b前缀会自动隐藏。)

  2. 将 Project Point 的返回值馈送到 Navigation 方法中的此变量中。您可以按下Ctrl +拖动以将其移动。

  3. 从 Set bHasValidTeleportDest 拖动一个执行线到 Branch 输入,并使用设置器的输出来驱动该分支。

如果 Project Point to Navigation 方法返回 true,则将 TeleportDest 设置为其投影位置:

  1. 将我们的TeleportDest变量拖放到事件图上并选择设置。

  2. 将从 Branch 节点到 Draw Debug Sphere 节点的执行线拖动,并按下Ctrl +拖动它以将其移动到 Set Teleport Dest 输入中。

  3. 将 Projected Location 输出馈送到TeleportDest变量中。

  4. 现在,只是因为它更干净,让我们将TeleportDest设置器的输出馈送到我们的 DrawDebugSphere 节点的 Center 输入上。

值得学习蓝图快捷键。按下Alt +点击连接可以断开连接。按下Ctrl +拖动连接可以将其移动到其他位置。

  1. 从 Branch 的 False 执行引脚中,让我们将 TeleportDest 设置为(0.0, 0.0, 0.0)。

您的图现在应该是这样的:

您是否看到 Projected Location 和 Set Teleport Dest 之间连接上的额外引脚?那是一个Reroute Node。您可以通过拖动连接并选择从创建对话框中添加 Reroute Node 来创建一个,或者通过双击现有连接器来创建一个。这些对于组织连接非常有用,以便您可以轻松地看到图表中发生的情况。一般来说,尽量避免允许连接器在未连接到的节点下交叉,因为这可能会误导阅读您的蓝图的人。您还可以将多个输入馈送到 reroute 节点,或从 reroute 节点分支多个输出。

现在,每次 tick,我们在bHasValidTeleportDest中都有一个 true 或 false 的值,如果为 true,则有一个我们可以传送到的位置。

执行传送

让我们使用刚刚存储在bHasValidTeleportDest标志中的值来查看我们是否有有效的目标,并在有时将玩家角色传送到TeleportDest

  1. 从我们刚刚创建的TeleportRight输入操作中,我们将从其 Pressed 输出连接一个执行线到一个 Branch 节点。

请记住,您可以按住B并单击以创建一个 Branch 节点。在这里查看 Epic 的蓝图编辑器 Cheat Sheet 中找到的其他快捷键:docs.unrealengine.com/en-us/Engine/Blueprints/UserGuide/CheatSheet。它们将为您节省很多时间。

  1. 拖动您的bHasValidTeleportDest变量并将其拖放到 Branch 节点的 Condition 输入上。

  2. 从 True 执行输出中创建一个 SetActorLocation 动作,并将您的TeleportDest变量拖放到其 New Location 输入上:

将其启动到 VR 预览中并试一试。现在您应该能够在地图上进行传送。能够探索是很好的,对吧?

现在我们已经让一切正常工作,让我们做一些工作来改进事情。

当我们开始在地图上跳来跳去时,我们会注意到一个问题,那就是我们没有任何方法来改变玩家在着陆位置的朝向。我们肯定可以改进这一点。

允许玩家选择着陆方向

如果我们希望玩家能够在着陆时指定他们的面朝方向,我们首先需要做的是给他们一种告诉系统他们想要朝向何处的方法。

映射轴输入

让我们添加一个输入,为玩家提供一种改变朝向的方式:

  1. 打开“项目设置”|“引擎”|“输入”。

在“绑定”|“动作映射”中的部分中,您是否看到我们设置 TeleportRight 输入的部分?它的下方是一个轴映射列表。

  1. 点击轴映射旁边的+按钮添加一个新映射。

  2. 使用展开箭头打开它,并将其命名为MotionControllerThumbRight_Y

  3. 将其映射到 MotionController(R)的拇指杆 Y。

  4. 将其比例设置为-1.0。

  5. 创建第二个映射,命名为MotionControllerThumbRight_X

  6. 将其映射到MotionController (R) Thumbstick X,并将其比例保留为 1.0。

Unreal 的输入系统处理两种映射:动作映射轴映射。动作映射是离散事件,例如按钮或键的按下和释放。轴映射为您提供有关模拟输入(例如操纵杆或触控板)的连续信息。

您可能已经注意到,我们通过-1.0 缩放了来自运动控制器拇指杆的 Y 输入。这是因为该设备的 Y 输入是反向的,所以我们需要翻转它。将其乘以-1 只是反转输入:

您的输入映射现在应该看起来像前面的截图所示。

现在我们已经添加了新的输入映射,我们可以关闭项目设置。

清理我们的 Tick 事件

让我们回到角色的事件图。

由于我们希望在设置传送时持续检查玩家的拇指杆位置,因此我们需要将其放在事件 Tick 上。不过,我们的 Tick 事件有点拥挤。在开始添加更多内容之前,让我们先整理一下:

  1. 在当前 Tick 事件的内容上拖动一个选框:

选择与事件 Tick 连接的所有节点。

  1. 右键单击所选节点上的任意位置,并从上下文菜单中选择“折叠到函数”:

右键单击所选节点中的任意一个,并选择“折叠到函数”。

  1. 将新函数命名为SetTeleportDestination

这样干净多了,不是吗?看一下下面的截图:

一般来说,使用函数作为组织和重用代码的一种方式是一个好主意,而不是将代码散布在整个事件图中。记住,任何代码的 80%生命周期都将花在调试和维护上,因此早期组织代码可以节省很多工作量。

您给函数起的名称应该是描述性的,准确的。将它们视为对读者的承诺,函数的内容确实做了名称所暗示的事情。这个读者可能是您将来调试或更新代码的人,也可能是完全不同的另一个开发人员。如果您清晰地命名了函数,每个人都将更容易理解您的代码在做什么。如果您以改变函数的方式修改函数,也要更改其名称。不要让传统名称误导读者。

使用拇指杆输入来定位玩家

让我们创建一个新函数来处理我们的传送定位:

  1. 点击“我的蓝图”|“函数”中的+按钮创建一个新函数。

  2. 将其命名为SetTeleportOrientation

一个新的选项卡将自动打开,显示函数的内容。现在,它只包含一个带有执行引脚的入口点。

  1. 在函数的图表中的任何位置右键单击,然后在上下文菜单的搜索框中键入thumbright。您将看到您在输入设置中创建的两个轴映射现在在这里显示为函数。

  2. 在这里添加 Get MotionControllerThumbRight_Y 和 Get MotionControllerthumbRight_X 节点:

  1. 创建一个 Make Vector 节点。

  2. 将 Get MotionControllerThumbRight_Y 的返回值输入到 Make Vector 节点的 X 输入中。(这可能看起来有些奇怪,但是是正确的——我们需要转换这个输入以用于驱动我们的旋转。)

  3. 将 Get MotionControllerThumbRight_X 输入到新向量的 Y 输入中。

  4. 通过在 Make Vector 的返回值上添加一个 Normalize 节点来归一化新向量:

归一化一个向量将其缩放为长度为 1。长度为 1 的向量称为单位向量。如果对任意长度的向量进行数学运算,很多情况下会得到错误的结果。一个经验法则是,如果你正在进行向量运算以确定旋转或角度,请确保使用单位向量。

现在我们已经将输入向量归一化,我们需要将其旋转,使其指向玩家的意图方向。

关于为 VR 设计运动系统的问题是:当你向玩家展示一个旋转时,你必须决定它的基础是什么。当玩家向前推杆或触摸触控板向前时,我们如何将其转化为真实世界的旋转?如果你操作过遥控车或者玩游戏的时间足够长以记得Resident EvilFear Effect中的旧式坦克式控制,你对我们在这里描述的有一些概念。在这些系统中,“前进”意味着汽车或角色所面对的方向,如果角色此时面对摄像机,那么这些控制将会感觉反向。

在过去的二十年里,传统的第一人称设计中,我们没有必须解决这个问题。角色面对的方向和玩家所看的方向没有区别,所以使用摄像机的观察方向作为前进方向是一个明显的选择。

在 VR 中,另一方面,我们有几个选择:

  • 我们可以基于角色的旋转进行旋转,但在房间尺度的 VR 中,这不是一个好主意,因为玩家可以在跟踪范围内转身而不一定旋转角色。你不希望基于玩家可能看不到的东西来定位控制。

  • 我们可以基于玩家的观察方向进行旋转,这是一个更好的选择,因为从玩家的角度来看,它是一致的,但在玩家四处观察时会产生奇怪的行为:

在 VR 中,一个角色可以同时具有多个变换——头部、身体和手部。

在 VR 中,玩家的头部、手部和身体可以独立于彼此旋转,所以前进方向不再总是明显的。

然而,最好的选择(并且当我们处理无缝运动时,我们将在后面发现)是基于运动控制器的方向,因为玩家已经在使用它提供输入,意识到它的方向,并且可以轻松改变它的方向。

让我们按照以下方式设置我们的系统:

  1. 在我们的 Normalize 节点的返回值中添加一个 RotateVector 节点。

  2. 在图表中拖动对 MotionController_R 的引用。

  3. 从 MotionController_R 中拖动一个 GetWorldRotation 节点:

这将得到我们在世界中正确的控制器方向,但我们只对左右旋转(偏航)感兴趣。我们不需要任何俯仰或滚转信息。

  1. 右键单击 GetWorldRotation 的返回值,并选择 Split Struct Pin:

  1. 对于 RotateVector 节点的 B 输入也做同样的操作。

  2. 将 GetWorldRotation 的 Yaw 输出连接到 RotateVector 的 Yaw 输入上。将 Roll 和 Pitch 保持未连接状态:

在蓝图中,拆分结构引脚通常比使用 Break 和 Make 节点来拆分和重构它们更清晰。它们做的是同样的事情。这只是一个关于如何使你的蓝图更易读的问题。

现在,我们需要将旋转后的向量转换为可用的旋转器。

  1. 将一个 RotationFromXVector 节点添加到 RotateVector 的返回值中。

最后,我们需要存储这个向量,以便以后使用。

  1. 将 RotationFromXVector 节点的返回值拖出来,并选择 Promote to variable。

  2. 将新变量命名为TeleportOrientation

  3. 这将自动为新变量创建一个 Set 节点。从函数的入口点拖动一个执行线到这个 setter 上。

  4. 从你的 setter 拖动一个执行线,并选择添加 Return Node 来添加一个函数的退出点。

现在,我们将 RotateVector 节点的返回值转换为一个旋转器,并用它来填充 TeleportOrientation。

对于不返回值的函数添加返回节点并不是必需的,但这是一个好的实践,因为它清楚地告诉维护或调试代码的人代码的退出点在哪里。如果不这样做,不会出现任何问题,但如果这样做,你的代码将更容易阅读。我们不会在本书中的每个方法中都这样做,只是为了避免添加额外的步骤,但这是一个好习惯。

  1. 返回到事件图的 Event Tick,将 SetTeleportOrientation 函数拖动到 SetTeleportDestination 的执行输出引脚上:

在 SetTeleportDestination 完成后,SetTeleportOrientation 现在将在每一帧上被调用。

让我们使用这个新信息:

  1. 在事件图中,找到我们设置角色位置的 InputAction TeleportRight 事件。

  2. 首先,我们也将把它折叠成一个函数。在事件图中留下它是不规范的。选择输入动作右侧的节点,右键单击,将它们折叠成一个新函数。

  3. 将新函数命名为ExecuteTeleport

由于我们现在有了一个传送朝向值需要适应,SetActorLocation 对我们来说已经不够了,因为它只设置位置而不设置旋转。我们可以在它之后立即调用一个Set Actor Rotation方法,使用存储在 TeleportOrientation 变量中的值,但我们有一个更简洁的方法可用。

  1. 选择这里的 Set Actor Location 节点并删除它。

  2. 在图表中右键单击,创建一个 Teleport 节点。

  3. 将分支语句的 True 分支连接到其执行输入上。

  4. 将 TeleportDest 变量连接到其 Dest Location 输入。

  5. 从变量列表中获取 TeleportOrientation 变量,并将其拖动到 Dest Rotation 输入引脚上:

让我们试试看。好多了。现在,我们在轨迹板上的拇指位置或拇指杆的方向都会影响我们的传送方向。我们可以更容易地四处看看。

但还有一件事情我们需要修复。如果玩家的朝向与角色的旋转相同,我们的传送朝向就可以正常工作,但如果不同,它就会变得令人困惑和不准确。让我们适应一下。

我们要做的是找出玩家相对于角色朝向的朝向,然后将这个旋转差与我们选择的传送朝向结合起来,这样当玩家降落时,他们会朝向他们选择的方向。

  1. 右键单击并创建一个 GetActorRotation 节点。

  2. 我们只需要从这个旋转中获取 Yaw 值,所以右键单击节点的返回值,选择 Split Struct Pin 来分解旋转器的组件。

  3. 从组件列表中,将对相机组件的引用拖动到图表中。

  4. 拖动其输出并对其调用 GetWorldRotation。

  5. 右键单击其返回值并选择拆分结构引脚。

  6. 右键单击图表中并创建一个 Delta(Rotator)节点。拆分其 A 和 B 输入结构引脚。

  7. 将 GetActorRotation 节点的返回值 Z(偏航)输出连接到 Delta(Rotator)节点的 A Z(偏航)输入。

  8. 将相机的 GetWorldRotation 节点的返回值 Z(偏航)输出连接到 Delta(Rotator)节点的 B Z(偏航)输入。

  9. 在图表中右键单击并创建一个 CombineRotators 节点。

  10. 将传送方向变量的值输入到 CombineRotators 节点的 A 输入中。

  11. 将 Delta(Rotator)节点的返回值输入到 CombineRotator 节点的 B 输入中。

  12. 将 CombineRotators 节点的返回值输入到 Teleport 节点的 Dest Rotation 输入中。

现在,当玩家降落在选定的传送点时,他们将朝着他们期望的方向看。如果您来自传统的平面游戏开发,这是您作为 VR 开发人员需要适应的一件事情:角色的旋转与视线方向不同。在 VR 中,玩家可以四处看,而不会影响角色的方向,因此在处理 VR 中的旋转时,您始终需要记住这两个方向。

问题是我们无法看到它将指向我们降落的位置。让我们改进一下目标指示。

创建一个传送目标指示器

我们将创建一个简单的蓝图角色作为我们的传送目标指示器:

  1. 在项目的蓝图目录中,右键单击并创建一个以Actor为父类的新蓝图类。

  2. 将其命名为BP_TeleportDestIndicator

  3. 打开它。

  4. 在其组件选项卡中,点击添加组件,并添加一个圆柱体组件。

  5. 将圆柱体的比例设置为(0.9, 0.9, 0.1)。 (记得解锁比例输入右侧的统一比例锁定。)

  6. 在圆柱体的碰撞属性下,将 Can Character Step Up On 设置为 No,并将其碰撞预设设置为 NoCollision。(这很重要-如果有碰撞,此指示器将干扰角色。)

  7. 添加一个立方体组件。

  8. 将其位置设置为(60.0, 0.0, 0.0)。

  9. 将其比例设置为(0.3, 0.1, 0.1):

我们的指示器应该看起来像这样。

  1. 编译它,保存它,然后关闭它。

给它一个材质

如果白色材质对您来说不够好,我们可以创建一些更好看的东西。我们不会在这个上面花太多时间,但是我们可以通过一些快速的工作来改善它的外观:

  1. 从内容浏览器中的项目目录中,创建一个名为MaterialLibrary的新目录。

  2. 在其中右键单击并选择创建基本资产|材质。

  3. 将新材质命名为M_TeleportIndicator

  4. 打开它。

  5. 在详细信息|材质部分,将其混合模式设置为 Additive。

  6. 将其着色模型设置为未照明。

  7. 按住3键,然后在图表中的任意位置单击以创建一个 Constant 3 Vector 节点。这是材质中颜色的表示方式。

  8. 双击节点,选择主要的绿色:R=0.0,G=1.0,B=0.0。

  9. 将颜色节点的输出拖动到发射颜色输入中。

  10. 在图表中的任意位置右键单击并创建一个线性渐变节点。

  11. 将 VGradient 输出拖动到材质的不透明度输入中:

  1. 保存并关闭材质。

  2. 打开 BP_TeleportDestIndicator 蓝图并选择 Cylider 组件。在其详细信息|材料中,将其元素 0 材料设置为刚刚创建的材料。

  3. 对于立方体组件也是一样:

很好!这是一个非常简单的材质,如果我们真的想要的话,我们可以花很多时间设计出一些精彩的东西,但是对于我们现在要做的事情来说,这完全可以。

将传送指示器添加到角色

现在,让我们将这个新的指示器添加到我们的角色中:

  1. 在我们的 VRPawn 的 Components 选项卡中,添加一个 Child Actor 组件。

  2. 在其详细信息| Child Actor Component | Child Actor Class 中,选择我们刚刚创建的新 BP_TeleportDestIndicator actor。

  3. 将 ChildActor 重命名为TeleportDestIndicator。(您可以使用F2键重命名对象。)

让我们创建一个新的函数来设置其位置和方向:

  1. 在 pawn 的函数集合中创建一个新的函数,并将其命名为UpdateTeleportIndicator

  2. 将 TeleportDestIndicator 拖入函数的图表中。

  3. 从 TeleportDestIndicator 拖动输出并创建一个 SetWorldLocationAndRotation 节点,将其用作目标。

  4. 将 TeleportDest 变量拖到 New Location 输入上。

  5. 将 TeleportOrientation 变量拖到 New Rotation 输入上。

  6. 给它一个返回节点:

  1. 返回事件图表,然后在 Set Teleport Orientation 之后,将 UpdateTeleportIndicator 函数的一个实例拖到 Event Tick 上:

让我们试试看。好多了!现在,我们可以看到我们降落时将面对的方向。顺便说一句,让我们摆脱之前作为临时解决方案使用的 Debug Sphere。

  1. 在 Set Teleport Destination 函数中,找到 Draw Debug Sphere 调用并删除它。

优化和完善我们的传送

让我们用一些细化来完成这些事情,因为我们仍然看到一些粗糙的边缘。

只有在按下传送输入时显示 UI

首先,我们一直在运行传送指示器,无论用户是否真正尝试传送。让我们只在用户按下传送输入时激活这些接口:

  1. 向我们的玩家 pawn 添加一个新变量。将其类型设置为布尔型,并将其命名为bTeleportPressed

  2. 按下Alt +单击从 InputAction TeleportRight 到 ExecuteTeleport 函数调用的执行线以断开连接。

  3. bTeleportPressed变量拖到 InputAction TeleportRight 的 Pressed 执行引脚上以创建一个 setter。在这里将其设置为 True。

  4. 将另一个bTeleportPressed的实例拖到 Released 执行引脚上。将其设置为 False。

  5. 将 ExecuteTeleport 连接到清除 TeleportPressed 的 setter,以便在用户释放输入时进行传送:

现在我们有一个变量,当传送输入被按住时为 true,当没有按住时为 false,我们可以使用它来管理 Tick 事件上发生的事情。

  1. 断开 Event Tick 与 SetTeleportDestination 的连接。

  2. 在这里添加一个 Branch 节点,并使用bTeleportPressed作为其条件。

  3. 将 Event Tick 的执行线连接到 Branch 输入,并将其 True 分支连接到 SetTeleportDestination。这样,只有在用户按下传送输入时,传送 UI 才会更新或显示:

让我们试试看。这样更好,但是我们的目标指示器在输入未按下时仍然可见,并且它没有更新。我们需要在不使用它时隐藏它:

  1. 从 pawn 的 Components 选项卡中选择 TeleportDestIndicator 组件。

  2. 在其详细信息中,将 Rendering | Hidden in Game 设置为 True。

  3. 将 TeleportDestIndicator 组件拖到图表中。

  4. 从中拖出一个连接器,并在其上调用 Set Hidden in Game。

  5. bTeleportPressed的一个实例拖到图表上并获取其值。

  6. 从中拖出一个连接器,并在搜索栏中键入not。选择 NOT Boolean。

  7. 将这个值插入到“Set Hidden in Game”动作中的新隐藏输入中。

这将导致指示器在未按下传送时隐藏,在按下传送时不隐藏:

让我们再试一次。好多了。只有在需要时才显示 UI。

在我们可以发布之前,我们仍然需要用调试方法替换当前绘制的传送弧线。然而,我们不会在这里详细介绍这个过程,因为它对本章的范围来说有点太复杂了。基本上,你在这里要做的是在角色上创建一个样条线组件,并将一个网格附加到它上面。我们不再使用SetTeleportDestination中的 ForEach 循环来绘制一系列的调试线,而是将路径位置保存到一个变量中。在UpdateTeleportIndicator中,我们将使用这些位置来设置样条线上的点。如果你想尝试一下,VR 模板中有一个很好的例子。

为我们的输入创建一个死区

当我们在地图上跳跃时,也变得清楚,我们没有给玩家一个简单的方法来在不改变方向的情况下传送。当他们想要四处看看时,我们的系统运作良好,但是没有给他们一个选择退出的方式。

让我们打开SetTeleportOrientation并修复这个问题:

  1. 在 BP_VRPawn 中创建一个新的变量。将其类型设置为 Float,并将其命名为TeleportDeadzone

  2. 编译蓝图并将其值设置为 0.7。这将接受 70%的触摸板或拇指杆半径的输入。

  3. 从将两个 Get MotionControllerThumbRight 输入值组合的 Make Vector 节点中拖动第二个输出,并从中创建一个 VectorLengthSquared 节点。

  4. TeleportDeadzone变量拖动到图表上并获取其值。

  5. 对 Teleport Deadzone 的值进行平方。

  6. 拖动 VectorLengthSquared 的输出并创建一个>=节点。

  7. 将平方的 Teleport Deadzone 值拖动到其另一个输入中:

这里发生了什么?我们想知道用户的输入是否超过了其范围的 70%。我们可以通过获取向量长度并将其与 Teleport Deadzone 进行比较来找到这个答案,这将给我们一个正确的答案,但是找到向量的实际长度涉及到一个平方根,这是昂贵的。另一方面,平方一个值只涉及将其乘以自身,这是廉价的。在我们的情况下,由于我们不关心实际的向量长度,只关心它与死区的比较。我们可以跳过向量长度的平方根,只将其与平方的目标长度进行比较。这是一种常见的优化向量长度比较的方法。你会经常看到它。

使用平方向量长度来测试输入死区将为您提供一个正确的圆形测试区域,因此您将在任何输入角度下获得一致的结果。

现在,让我们使用这个比较的结果来选择我们将使用哪个旋转值:

  1. 在图表中放置一个选择节点,并将>=测试的输出连接到其 Index 输入。

  2. 将 RotationFromXVector 节点的输出从设置传送定向节点中断连接。

  3. 将 RotationFromXVector 节点的输出连接到选择节点的 True 输入。

  4. 创建一个 GetActorRotation 节点,并将其输出连接到选择节点的 False 输入。

  5. 将选择节点的返回值连接到设置传送定向节点的输入:

我们在这里做的是使用死区检查的结果来决定我们是否应该使用拇指杆输入的旋转值,还是保持角色的现有旋转。如果输入在 70%的范围或更大,我们将使用输入。如果不是,我们就使用角色的旋转。

让我们运行一下。现在,如果你触碰到触摸板的边缘或者推动拇指杆到相当远的距离,你的方向会改变,但如果它们离中心更近,你传送时将保持当前的方向。

在传送时淡出和淡入

我们的系统开始运作得相当好了,但是传送可能会感觉有点突兀。让我们淡出并重新淡入,以实现更愉快的过渡:

  1. 打开我们角色的事件图。

  2. 在 InputAction Teleport Right 事件附近,创建一个Get Player Camera Manager节点。

  3. 从该节点的返回值创建一个Start Camera Fade动作。

  4. 将其 To Alpha 值设置为 1.0。

  5. 拖动其持续时间输入并提升为变量。编译并将其值设置为0.1

这将使场景相机在十分之一秒的时间内变黑。

  1. 断开与Execute Teleport函数调用的输入的连接。

  2. 将 Teleport Pressed = False 节点的执行输出连接到新的 Start Camera Fade 动作。

  3. 您可能需要将一些节点拖到右侧以腾出空间。

现在,当用户释放传送输入时,我们将调用 Start Camera Fade,因为我们已经清除了bTeleportPressed标志:

  1. 从 Start Camera Fade 节点的执行输出拖出一个执行线,并放置一个延迟。

  2. 将延迟持续时间设置为您的 Fade Duration 变量。

  3. 从延迟的完成输出中拖出并放入您的Execute Teleport函数调用,以便在淡出和延迟发生后调用该函数。

当用户释放传送输入时,我们会在十分之一秒内淡出,等待另外十分之一秒,然后执行传送。现在,传送完成后我们需要淡入。

  1. 创建另一个 Start Camera Fade 节点,并将 Execute Teleport 的输出连接到其执行输入。

  2. 将 Get Player Camera Manager 的输出连接到该节点的目标输入。

  3. 将其持续时间设置为您的Fade Duration变量。

  4. 将其 From Alpha 值设置为 1.0,将其 To Alpha 值设置为 0.0。

  5. 将此节点的输出连接到 Teleport Dest Indicator 的 Set Hidden in Game 节点的输入:

您的图表现在应该是这样的。

让我们在游戏中进行测试。这样做更好。当传送动作发生时,我们现在有一个快速的淡出和淡入。这虽然微妙,但为应用程序增添了一些亮点,使传送不那么令人震惊。

然而,由于这个动作需要时间,我们应该确保玩家在一个传送正在进行时不能触发第二个传送:

  1. 创建一个新的布尔变量,并将其命名为bIsTeleporting

  2. 将其拖到图表上并获取其值。

  3. 在 InputAction TeleportRight 和 set Teleport Pressed to True 之间插入一个新的 Branch 节点。

  4. 使用bIsTeleporting作为分支节点的条件。

  5. 将其 False 输出连接到设置 Teleport Pressed 为 True 节点,并将其 True 输出保持未连接。

  6. 对于输入动作的 Released 执行,也做同样的操作:

这样,只有在bIsTeleporting为 False 时,才会处理传送按下或释放事件。

现在,当我们开始传送动作时,我们需要将bIsTeleporting设置为 True,然后在动作完成时再次将其设置为 False:

  1. 在从输入动作的 Released 输出出来的 Set Teleport Pressed = False 节点之后,插入一个 setter 将bIsTeleporting设置为 True。

  2. 将其输出连接到 Start Camera Fade 节点。

  1. 在第二个 Start Camera Fade 节点之后,添加另一个 setter 将bIsTeleporting设置为 False。

  2. 将该节点的输出连接到 Teleport Dest Indicator 的 Set Hidden in Game 输入。

现在,当我们释放输入执行传送时,bIsTeleporting将被设置为 true,直到传送动作完成,新的传送动作将不会被接受。

传送运动总结

我们在这里涵盖了很多内容,并创建了一个相当全面的传送运动方案。让我们回顾一下这个方案:

  • 它绑定到导航网格,因此不允许玩家传送到非法位置

  • 它使用抛物线追踪,以便玩家可以传送到比当前位置更高的目的地

  • 它允许玩家在传送时选择目标方向

  • 它在指示玩家将要去的地方和他们将面对的地方方面做得相当好

  • 它包括一些细节处理,如输入死区和相机淡入淡出

我们还可以做更多的事情,但这已经是一个相当完整的解决方案了。如果我们进一步改进它,可能希望允许它与任何一只手一起使用,并且肯定需要用适用于发布版本的其他内容替换我们绘制的调试传送路径。如果您选择从这里进一步探索,引擎附带的 VR 模板是一个很好的下一步。我们刚刚在这里编写的许多方法与该模板中使用的方法类似,因此您应该会发现,当您开始深入研究时,您站在了一个很好的基础上,可以理解您看到的内容。

传送是在虚拟现实中四处移动的有效解决方案,因为正如我们之前提到的,它不会尝试表示移动,所以通常不会引发用户晕动病。对于那些不依赖于玩家在世界中移动的高度沉浸式的应用程序来说,它效果非常好。

对于希望保持更高程度沉浸感的游戏和应用程序来说,传送可能不是您想要的,因为它的行为方式与现实世界中的移动不同:它会创建一种不连续的空间感,并引入明显不存在于世界中的界面元素。无论如何,它都会破坏沉浸感。

接下来,我们将介绍一种沉浸式移动方案,允许玩家在世界中平稳移动。非常敏感的玩家或者对虚拟现实不熟悉的玩家可能不会觉得沉浸式移动舒适,因此在某些情况下,可以在应用程序中提供传送移动作为可选项。

让我们看看它是如何工作的。

无缝移动

如果您正在制作一款沉浸式游戏或体验,那么如果玩家周围的空间感不断被传送动作打断,那么这种体验对玩家来说会更加令人信服。让我们来看一下如何处理空间中的无缝移动。

设置无缝移动的输入

通常情况下,我们可能会允许用户在选项菜单中选择他们熟悉的移动方案,但由于我们当前的角色除了移动以外什么都不做,而且我们还没有对左手控制器做任何处理,所以我们可以使用它来驱动我们的无缝移动方案。

让我们为左手控制器的拇指杆添加一对输入轴映射:

  1. 打开项目设置 | 引擎 | 输入。

  2. 点击 Bindings | Axis Mappings 旁边的+按钮两次,添加两个新的轴映射。

  3. 将它们命名为MoveForwardMoveRight

  4. 将 MoveForward 绑定到 MotionController (L) Thumbstick Y。

  5. 将其缩放设置为-1.0。

  6. 将 MoveRight 绑定到 MotionController (L) Thumbstick X,并将其缩放设置为 1.0:

我们暂时完成了输入绑定,所以可以关闭项目设置。

更改角色的父类

为了使我们的角色平稳移动,我们需要为其提供处理移动输入的方法。我们有两种方法可以做到这一点。我们可以在 Tick 事件上编写自己的输入处理程序,但这是一个相当复杂的过程,如果我们只是想实现一个简单的移动方案,这是不必要的。

更简单的方法是为我们的角色添加一个 Movement Component。然而,在蓝图中,没有办法添加一个移动组件(在 C++中是可以的),所以我们需要将我们的角色的父类更改为一个包含我们需要的组件以及其他几个我们也想要的组件的类。让我们开始吧:

  1. 打开 BP_VRPawn 的蓝图,并在工具栏上点击 Class Settings:

我们之前提到过虚幻引擎是一个面向对象的系统。一个对象是一个类的实例,类从其他类继承,继承了它们的能力和特征。这就是为什么这一点很重要。我们将通过将 BP_VRPawn 的父类更改为 Pawn 类的子类来改变它的功能,该子类包含我们需要的组件。

  1. 在详细信息 | 类选项下,将父类从 Pawn 更改为 Character:

如果你查看组件选项卡,你会发现出现了一些新的组件:

除了之前创建的组件,我们现在还有以下组件:

  • 一个胶囊组件

  • 一个箭头组件

  • 一个网格组件

  • 一个角色移动组件

这些都是从 Character 类继承的。

这很有用。我们需要移动组件来让我们移动,我们需要胶囊组件来防止我们穿过墙壁。我们不真正需要网格组件,因为我们不渲染玩家角色的身体,但在这种情况下将其放在这里并且将其 Skeletal Mesh 属性留空也不会对我们造成伤害。

当更改对象的父类时要小心。如果你要更改的类是前一个父类的子类,那通常是安全的,因为它会添加新的元素,但父类的属性和函数仍然存在。从子类更改为父类可能更加危险,因为你可能依赖于子类上存在但父类上不存在的属性或函数。更改为与当前类非常不同的类可能会导致问题。如果你知道你在做什么,引擎不会阻止你,但你可能最终需要清理很多无效的函数调用或变量引用。

修复碰撞组件

如果现在运行游戏,你会发现我们离地面比之前高一点。这是因为我们的胶囊组件与地面碰撞并将我们推向上方。为了修复这个问题,打开你的角色蓝图的视口选项卡。(如果你关闭了它,可以通过双击组件选项卡上的 BP_VRPawn(self) 条目来重新打开它。)让我们开始吧:

  • Alt + K 切换视口到侧视图。

  • 抓住你的相机根组件,将其向下拖动,直到它位于胶囊组件的底部。它的位置现在应该是 (0.0, 0.0, -90.0):

如果再次运行游戏,你会发现你已经正确地站在地板上了。

处理移动输入

现在我们给角色添加了一个移动组件,让我们使用之前映射的输入绑定来让我们移动:

  1. 在你的角色蓝图的事件图中右键单击,创建一个输入 | 轴事件 | 前进事件:

  1. 对于我们在轴绑定中创建的 MoveRight 事件也做同样的操作。

现在我们有了两个每帧运行的事件,可以向我们的移动组件提供移动输入。

  1. 创建一个 Add Movement Input 节点,并将其执行输入连接到 InputAxis MoveForward 的输出。

  2. 将 MoveForward 的轴值输入到移动输入的缩放值中。

  3. 对于 InputAxis MoveRight 也重复这个步骤:

现在,我们需要告诉它我们想要移动的方向:

  1. 从组件列表中获取你的相机组件,并将其拖动到事件图中。

  2. 从它的输出中创建一个 GetWorldRotation 节点。

  3. 右键单击 GetWorldRotation 的输出并拆分结构引脚。

  4. 在图表中右键单击,创建一个 Get Forward Vector 节点。

  5. 拆分它的输入引脚。

  6. 将 GetWorldRotation 的 Yaw 输出连接到 Get Forward Vector 的 In Rot Z (Yaw) 输入。

  7. 右键单击创建一个 Get Right Vector 节点。

  8. 拆分其输入,并将 GetWorldRotation 的 Yaw 输出连接到其 In Rot Z(Yaw)输入。

  9. 将 Get Forward Vector 的输出连接到 InputAxis MoveForward 节点的 World Direction 输入的 Add Movement Input。

  10. 将 Get Right Vector 的输出连接到 MoveRight Add Movement Input:

让我们在游戏中试试看。

我们仍然可以使用右侧的触摸板或拇指杆进行传送,但如果我们使用左侧的输入,它会平滑地将我们滑过世界,使用我们相机的观察方向作为前进方向。

习惯于第一人称射击游戏的玩家习惯于将相机方向视为前进方向。在 VR 中,这不一定是这样-角色可以向右看而向左移动是完全合理的。我们的角色有一个控制旋转的概念,它是其在空间中的实际方向,与相机面对的方向不同。实际上,如果您要从角色的控制旋转而不是相机旋转驱动移动,您需要提供视觉提示,以清楚地向玩家说明他们的前进方向是什么,否则您的移动方案将使他们困惑。为了保持清晰,在这种情况下,我们使我们的移动相对于观察方向。

这样做效果还不错,但存在一些问题。

修正移动速度

首先,我们移动得太快了。让我们修复一下:

  1. 选择您的角色的 CharacterMovement 组件,并在详细信息|角色移动中将其最大行走速度设置为 240.0

这是一个更合理的步行速度。

让玩家在不断转向的情况下观察周围

让我们面对现实吧。使用相机前向矢量作为我们转向的基础感觉有点不稳定。每次你转动头部看东西时,你都必须转向纠正自己。世界不是这样运作的。让我们改为使用左侧控制器的方向作为我们移动的基础:

  1. 抓住 MotionController_L 组件并将其拖动到事件图表中,靠近我们当前获取相机世界旋转的位置。

  2. 将 MotionController_L 组件的输出连接到 GetWorldRotation 节点,替换 Camera 的连接:

现在,我们不再使用 Camera 的偏航作为我们前进和右侧世界方向的基础,而是使用控制器,这是很直观的。前进方向是您指向控制器的方向,同时,玩家可以使用触摸板或摇杆进行精细移动。他们可以通过指向他们想要去的方向来转向,并且可以在不影响移动的情况下四处看看。

实现快速转向

我们需要给玩家提供一种改变方向的方法,而不必在现实世界中转动椅子。

虽然让玩家像我们刚才做的那样平滑地在世界中移动效果很好,但我们不希望他们平滑地转向。我们在第一章中讨论了这个原因,即在 VR 中,当玩家看到他们没有感觉到的运动时,会引起视觉诱发的晕动病。我们对看起来像旋转的运动特别敏感。这可能是由于多种原因:

  • 从中毒引起的前庭系统干扰会产生旋转的感觉。在狂欢之夜后是否曾经有过床旋转的感觉?接下来会发生什么?对,不要让你的玩家经历这种感觉。

  • 当图像中有大量视觉流动时,前庭系统的断开感最强烈。当玩家旋转时,几乎画面中的所有物体都向侧面移动。这是很多运动。

  • 在现实世界中,当我们转动头部时,我们自然会眨眼,或者我们首先将目光对准我们想要看的东西(这种运动称为扫视),然后转动头部跟随。在现实世界中,我们在转身时不会保持眼睛稳定。

通过快速转向玩家而不是让他们平滑转向不仅可以避免创建一个可能让用户感到恶心的巨大视觉流动,而且实际上比平滑转向更好地复制了我们在现实世界中感知转向的方式。

让我们设置一个快速转向。

设置快速转向的输入

让我们添加一对动作绑定来进行快速向右和向左转:

  1. 打开项目设置 | 引擎 | 输入。

  2. 在引擎 | 输入 | 绑定中添加两个新的动作映射。将它们命名为SnapTurnRightSnapTurnLeft

  3. 将 SnapTurnRight 绑定到 MotionController(L)FaceButton2。

  4. 将 SnapTurnLeft 绑定到 MotionController(L)FaceButton4 和 MotionController(L)FaceButton1。

我们将两个输入绑定到 SnapTurnLeft 以适应 Oculus 和 Vive 输入。在 Oculus Touch 控制器上,左控制器上的 FaceButton1 是 X 按钮,而 FaceButton2 是 Y 按钮。在 HTC Vive 上,FaceButton2 是触摸板的左侧,而 FaceButton4 是触摸板的右侧:

现在您的输入绑定应该如下所示。

现在我们可以关闭项目设置了。

执行快速转向

现在,让我们在按下这些按钮时执行快速转向:

  1. 在角色的事件图中,为 SnapTurnLeft 和 SnapTurnRight 动作添加输入事件:

  1. 创建一个 GetActorRotation 节点并拆分其输出。

  2. 从返回值 Z(偏航)输出处拖动并创建一个 float - float 节点。

  3. 从减法节点的第二个输入处拖出并将其提升为变量。将变量命名为SnapTurnIncrement

  4. 编译蓝图并将 SnapTurnIncrement 值设置为 30.0。

  5. 创建一个 SetActorRotation 节点,并将 GetActorRotation 节点的 Roll 和 Pitch 输出直接连接到相应的输入。

  6. 将减法的结果连接到偏航输入。

  7. 将 InputAction SnapTurnLeft 的按下执行输出连接到 SetActorRotation 节点的输入。

  8. 选择这些节点,按下 Ctrl + W 进行复制。

  9. 将复制集中的减法替换为加法。

  10. 将复制的节点连接到 InputAction SnapTurnRight 的执行输出:

在游戏中试一试。效果还不错。我们肯定可以进一步改进它 - 目前,快速转向也会触发移动,但这是一个相当可用的解决方案。如果对我们的游戏有意义,我们还可以将 Vive 触摸板上的按下或左侧 Oculus Touch 上的摇杆按下映射为 180°的转向。

进一步进行

我们可以通过几种方式来改进我们在这里所做的工作,但是完全实施它们将超出本章的范围。让我们简要地谈谈在进一步进行时如何改进这个类。

使用模拟输入进行快速转向

我们目前的快速转向实现在 Vive 手柄上效果还不错,但在 Oculus Touch 控制器上感觉不太好。对于我们的玩家来说,如果能听取其中一个摇杆的模拟输入并在超过一定阈值时触发快速转向可能会更好。这样,玩家可以将摇杆翻转到一侧来执行快速转向,或者只需触摸 Vive 触摸板的边缘而无需按下它。

您可以通过在运动控制器的拇指杆上设置输入轴绑定,并测试输入是否大于阈值(对于此测试,我们使用了 0.8)来执行此操作,以进行右转,或者小于负阈值进行左转。

您需要记住对快速转向进行冷却,以防止它在单次按下时重复触发。在我们的案例中,我们使用了 0.2 秒的冷却时间。

如果您想将其构建到您的角色中,请按照以下步骤进行:

  1. 为 MotionControllerThumbRight_X 输入轴创建一个输入事件处理程序。

  2. 创建一个分支,只有当bTeleportPressed为 False 时才继续。我们不希望在传送时处理快速转向。

  3. 创建一个名为bSnapTurnCooldownActive的新布尔变量。

  4. 创建一个分支,只有当bSnapTurnCooldownActive为 False 时才继续。

  5. 创建一个名为SnapTurnAnalogDeadzone的新浮点变量,编译并将其值设置为 0.8。

  6. 添加一个>=测试,以查看来自拇指杆输入的输入轴值是否大于或等于SnapTurnAnalogDeadzone

  7. 从此处创建一个分支,并在其 False 输出上创建另一个分支。

  8. 对于这个第二个分支,测试一下传入的轴值是否小于或等于负的 SnapTurnAnalogDeadzone(将其乘以-1.0)。

  1. 创建一个名为 ExecuteSnapTurnLeft 的新自定义事件,并将其输入到从 InputAction SnapTurnLeft 调用的 SetActorRotation 中。

  2. 创建另一个名为 ExecuteSnapTurnRight 的自定义事件,并将其输入到处理 InputAction SnapTurnRight 的位置:

  1. 现在,在 ThumbstickRight 处理程序中,如果输入轴大于等于 SnapTurnAnalogDeadzone,请调用 ExecuteSnapTurnRight。

  2. 如果输入轴小于等于-SnapTurnAnalogDeadzone,请调用 ExecuteSnapTurnLeft。

现在,我们需要设置一个冷却时间,以防止用户在移动摇杆时连续进行快速的快速转身:

  1. 添加一个 setter 来将 bSnapTurnCooldownActive 设置为 true,并在 ExecuteSnapTurnRight 和 ExecuteSnapTurnLeft 之后调用它。

  2. 添加一个延迟。默认值 0.2 在这里很好,但如果您想调整冷却时间,将此值提升为变量。

  3. 延迟后,再次将 bSnapTurnCooldownActive 设置为 False。

通过这个布尔标志和延迟,我们只是设置了一个门,使得在最后一次处理后的 0.2 秒内快速转身输入将被忽略,这给了用户释放摇杆的时间,一旦他们朝向他们想要的方向。

这个实现使得玩家在右摇杆上有一个很好的自然感觉的快速转身,同时将左摇杆用于模拟无缝移动。

总结

在本章中我们做了很多事情。

我们学习了如何在场景中设置和优化导航网格,以及如何查找和修复场景中对象的碰撞问题。我们学习了如何设置输入动作并使用它们来移动我们的玩家角色,也许最重要的是,我们学习了肯特·贝克的软件开发口号:“让它工作,让它正确,让它快”,并学习了在迭代开发中遵循它的含义。我们将经常回顾这一点。这是有效软件开发的秘诀。

这是一项很大的工作。本章的练习涵盖了很多内容,但应该让您对设置玩家角色和运动系统的各个部分如何配合有一个不错的理解。

现在我们给了我们的角色脚,下一章,我们将给它手。我们将学习如何使用动作控制器来指向、抓取和与世界中的对象交互。我们还将在设置导航网格方面进一步学习,并将一些 AI 放入世界中以使用它们。现在我们可以在世界中四处走动了,我们将开始让它生动起来。

第五章:与虚拟世界互动-第一部分

在前一章中,我们学习了如何使用传送定位和添加更沉浸式的无缝定位方案来使玩家角色移动。我们给了我们的用户脚。现在,在本章中,我们将给他们双手。

我们将通过使用市场上的资产创建一个新项目,探索另一种启动 VR 项目的方式,然后将我们在前一章中构建的 VRPawn 迁移到这个新项目中。一旦我们设置好了,我们将首先为 VRPawn 添加手部,并探索与世界中的物体互动的方式。

这很重要。作为与世界互动的人类,我们最关注的是我们环顾四周时事物的外观,但我们对我们的手和它们的动作也有很高的意识。VR 开发者称之为“手的存在感”,当它做得好时,它可以显著提高沉浸感。请稍微思考一下。你的手是你身体的一部分,你可能大部分时间都对它们最有意识。我们在 VR 中如何很好地代表它们对我们在体验中的“具身感”有着有意义的影响。

在本章中,我们将学习以下主题:

  • 如何为玩家创建基于蓝图的虚拟手

  • 如何在创建世界中的对象时使用构造脚本进行自定义

  • 如何使用动画混合空间和动画蓝图来为我们的手添加动画

  • 如何设置新的输入来驱动我们的手

让我们开始吧!

从现有工作开始一个新项目

让我们从创建一个新项目开始。我们将把我们在前一章中制作的 Pawn 和游戏模式迁移到这个项目中,并从市场上添加一些景观。当您开始开发自己开发的元素库或通过市场获得元素时,这将成为启动新项目的常见方式。

将蓝图迁移到新项目

启动当前版本的引擎,并在 Unreal 项目浏览器中使用以下参数创建一个新项目:

  • 空白蓝图模板

  • 硬件目标设置为移动/平板电脑

  • 图形目标设置为可扩展的 3D 或 2D

  • 没有初始内容

将其放在您喜欢的任何位置。

现在,让我们将在前一个项目中创建的 Pawn 添加到这个项目中。为了做到这一点,我们将不得不跳回到我们之前的项目中,以获取我们想要迁移的资产:

  1. 选择文件 | 打开项目,并浏览到您之前项目的.uproject文件。打开它。这样做时,您当前的项目将关闭。

  2. 一旦进入您之前的项目,找到我们创建的BP_VRGameMode蓝图。

  3. 右键单击它,选择 Asset Actions | Migrate...,如下图所示:

除了您选择的对象之外,Migrate...实用程序还会收集您选择的对象所依赖的任何其他对象。因为我们的游戏模式使用 VRPawn 作为默认 Pawn,所以 Migrate...实用程序将收集 Pawn 以及我们为其创建的传送指示器:

  1. 点击确定,当被问到要将新内容放在哪里时,选择你的新项目的 Content 目录。

太棒了!你的游戏模式和 Pawn 的副本现在已经添加到你的新项目中。

我们还映射了一些输入,我们也需要它们。

复制输入绑定

还记得我们提到过输入映射只是DefaultInput.ini中的文本条目吗?由于我们在新项目中没有映射任何输入,我们可以通过复制DefaultInput.ini文件来重新创建旧项目的输入绑定。您也可以使用项目设置菜单重新创建输入,但是如果可以这样做,这种方式更快:

  1. 导航到旧项目的 Config 目录。

  2. 选择DefaultInput.ini并将其复制到您的新项目的Config目录中。

如果你打开它,你会看到它包含了我们创建的输入绑定,如下面的截图所示:

设置新项目使用迁移的游戏模式

现在我们已经复制了我们的游戏模式和 Pawn,并且我们的输入绑定已经设置好,我们可以返回到我们的新项目:

  • 如果你点击文件 | 最近的项目,它应该在列表中,但如果没有,使用文件 | 打开项目导航到它

现在,让我们设置我们的项目使用刚刚带过来的游戏模式:

  • 打开项目设置 | 项目 | 地图和模式,并在默认模式下,将默认游戏模式设置为BP_VRGameMode

这将导致该游戏模式在我们项目中的任何地图上使用,除非我们覆盖它。正如你记得的那样,这个游戏模式告诉项目加载我们的 VRPawn。

VR 相关的其他项目设置

还要记得设置我们在第三章中描述的其他与 VR 相关的设置,例如:

  • 项目设置 | 引擎 | 渲染 | VR | 实例化立体声:True

  • 项目设置 | 引擎 | 渲染 | VR | 环形轮询遮蔽查询:True

  • 项目设置 | 引擎 | 渲染 | 正向渲染器 | 正向着色:True

  • 项目设置 | 引擎 | 渲染 | 默认设置 | 抗锯齿方法:MSAA

  • 项目设置 | 引擎 | 渲染 | 默认设置 | 环境遮蔽静态分数:False

  • 项目设置 | 项目 | 描述 | 设置 | 在 VR 中启动:True

还要记住,你不应该盲目地遵循这些步骤。对于许多 VR 项目,正向渲染将是最佳选择,但你应该对你正在做的特定事物是否适合延迟渲染模型进行一些思考。(如果你要进行大量的动态照明和反射表面,这可能是适用的情况。)对于抗锯齿方法也是一样。如果你使用正向渲染,通常会选择 MSAA,但在某些情况下,时域抗锯齿或 FXAA 会更好看。实例化立体声几乎总是你想要的,环形轮询遮蔽查询也是一样。

测试我们迁移的游戏模式和 Pawn

在做任何其他操作之前,让我们先测试一下:

  1. 将一个导航网格边界体拖到我们项目中默认打开的地图上,并将其缩放到覆盖整个地板。(记住你可以按下 P 键查看它。)

  2. 启动 VR 预览,验证你可以在地图上进行传送并使用无缝移动。

很好。这个快速测试可以让我们验证从其他项目中带过来的游戏模式已加载,并在玩家起始点生成了我们的 VR Pawn 的实例。

在构建时逐步测试事物。在进行了一些更改之后,找到错误的源头要比进行了很多更改之后容易得多。

添加景观

现在,让我们引入一些景观,以便我们有一个玩耍的地方:

  1. 打开你的 Epic Games Launcher,在市场中搜索 Soul: City。(它是免费的。)

  2. 点击添加到项目,并将其添加到你现在正在工作的项目中。

  3. 完成后,如果你关闭了项目,请重新打开它,并打开内容 | Soul City | 地图 | LV_Soul_Slum_Mobile。

在编译着色器时喝杯咖啡。现在,我们应该设置我们的项目自动打开这个地图。

  1. 在项目设置 | 项目 | 地图和模式中,将编辑器启动地图和游戏默认地图设置为LV_Soul_Slum_Mobile

添加一个导航网格

我们还需要在这个场景中添加一个导航网格边界体,以便我们可以通过它进行传送。

正如你在前一章中学到的,如果你想做得正确,设置边界体积可能是一个复杂的过程。对于我们在这里的目的,我们将稍微作弊,只是大致覆盖场景的大部分区域。如果你想进一步调整体积,你可以缩放它并更仔细地放置它,并使用导航修改器来排除你不想要的区域。如果你想保持简单,以下设置对我们在这里关注的内容已经足够好了:

  • 位置:X=3600,Y=-1200,Z=0

  • 比例:X=100,Y=40,Z=30

我们得到以下输出:

我们的导航网格在这个地图上有点混乱。如果你想清理一下,可以随意应用我们在前一章中讨论过的方法。

测试地图

启动 VR 预览并探索一下场景。嗯。有些问题。我们的输入没有正常工作。因为我们在前一步验证了我们的 Pawn 工作正常,输入映射也没问题,所以我们知道那不是问题。让我们确保我们加载了正确的 Pawn:

  1. 打开你的世界设置,查看游戏模式|游戏模式覆盖。

  2. 果然,还有另一个正在加载的游戏模式。使用重置箭头清除被覆盖的游戏模式。

让我们再次测试。好多了。现在,我们能够在环境中导航了。

当我们在这里并且能够四处走动时,让我们指出一些关于这个环境的事情。这不是一个完美的虚拟现实项目环境,在这种情况下,这给了我们一些有用的东西可以谈论:

  • 在 VR 中比例很重要:首先,当我们四处走动时,我们可以看到某些物体的比例不一致。有些楼梯看起来大小合适,而其他的则很大。我们在这里不打算对此做任何处理,但这是一个重要的要点:你世界中物体的比例在 VR 中非常重要。人们对物体的大小有一种本能的感觉,而 VR 给他们提供了比平面屏幕更强烈的关于物体大小的线索。如果你的比例不正确,他们会在 VR 中注意到。

  • 灯光可能会在 VR 中产生镜头光晕:另一个潜在的问题是明亮的霓虹灯。它们使环境看起来很棒,但你可能会注意到它们有时会从某些角度使你的头戴设备的菲涅耳透镜产生光晕。我们并不是说你需要避免在场景中使用明亮的灯光或对比度,但要注意它们有时会引起对硬件的注意。这里的要点是你总是希望在 VR 头戴设备和平面屏幕上检查你的艺术作品。

创建手部

现在我们有了一个场景可以使用,让我们进入本章的核心并开始设置一些交互。

在我们做其他事情之前,让我们改进一下场景中运动控制器的表示方式。目前,我们正在使用调试网格,如果我们的用户使用的是与我们在创建场景时使用的不同的头戴设备,它们将无法正确渲染。这足够让我们开始,但现在我们需要用更持久的东西来替换它。

为了获得可用的手部网格,我们将从 VR 模板中获取。对于你的许多 VR 项目来说,你可能只是从 VR 模板开始创建一个项目,或者将整个 MotionController Pawn 蓝图迁移到你创建的项目中,但对于我们在这里的目的,我们希望自己构建 Pawn,以便我们了解其中的内容。

从 VR 模板项目迁移手部网格和动画

如果你已经创建了一个 VR 模板项目的示例,请使用文件>打开项目来打开它。如果你还没有一个,关闭当前项目,然后从 Epic Launcher 中启动引擎,并使用 VR 模板创建一个新项目。对于这个项目,你使用的其他设置并不重要——我们只是为了获取网格而在这里:

  1. 在 VR 模板项目的内容浏览器中,导航到 Content | VirtualReality | Mannequin | Animations。

  2. 选择这三个动画资产,右键点击它们,选择 Asset Actions | Migrate。暂时忽略混合空间和动画蓝图,我们将学习如何自己制作它们:

您会看到迁移实用程序不仅收集了您选择的动画,还找到了网格、物理资产和骨骼,以及其材质和输入到其中的纹理:

  1. 将当前项目的 Content 目录选为目标。

现在我们已经收集了一些可以使用的资产,我们准备返回我们的项目。

  1. 点击文件 | 最近的项目,打开您之前的项目。(如果这里没有出现,请使用文件 | 打开项目。)

将手部网格添加到我们的运动控制器上

回到我们当前的项目,我们现在应该在内容浏览器中有一个VirtualReality目录,其中包含一个Mannequin子目录,其中包含AnimationsCharacter文件夹。

让我们将这些手部网格应用到我们的角色的运动控制器上。

创建一个新的蓝图 Actor 类

我们首先要做的是创建一个蓝图来表示它们,因为我们希望动画手部以响应玩家的动作:

  1. 在项目的蓝图目录中右键点击,选择创建基本资产 | 蓝图类。

  2. 将其父类设置为 Actor。

  3. 让我们将其命名为BP_VRHand

  4. 打开它。

我们在本书中早些时候提到,面向对象开发的核心原则之一是将属于一起的东西放入自包含的对象中,这些对象可以处理自己的行为。由于我们即将将动画手部网格与运动控制器连接起来,这是一个很好的机会来做到这一点。我们完全可以只向我们的角色添加一对骨骼网格组件,并将它们附加到我们的运动控制器组件上,但如果我们能更好地设计一下,事情会更加清晰,最终也更容易管理。

添加运动控制器和网格组件

让我们添加我们需要的组件:

  1. 将 MotionController 组件添加到您的组件列表中。

  2. 选择新的 MotionController 组件后,添加一个骨骼网格组件,使其成为运动控制器的子组件:

  1. 让我们将其命名为HandMesh

  2. 在骨骼网格组件的详细面板中,将其 Mesh | Skeletal Mesh 属性设置为MannequinHand_Right

添加一个 Hand 变量

由于我们将在右手和左手都重用这个 VRHand,我们需要设置一种方式让对象知道它代表的是哪只手:

  1. BP_VRHand的变量列表中添加一个变量,并将其命名为Hand

  2. 将其变量类型设置为EController Hand

  3. 将其 Instance Editable 属性设置为true

您会注意到,当您将 Instance Editable 设置为 true 时,变量名称旁边的眼睛图标是打开的。这表示该变量允许为世界中的每个单独实例设置不同的值。由于我们需要将其中一个对象设置为右手,另一个设置为左手,这正是我们想要的:

现在我们有了一个实例可编辑的 Hand 变量,指示这个对象将代表哪只手,我们还需要告诉我们的 MotionController 组件。

使用构造脚本处理对 Hand 变量的更新

如果你查看BP_VRHand类的函数列表,你会发现一个 Construction Script 已经自动为你创建了。这是一个在对象创建或更新之前在游戏开始之前运行的函数。Construction Scripts 非常有用,可以在软件运行之前同步需要对齐的值。在我们的情况下,这正是我们想要的。如果我们改变这个 Hand 变量的值,我们希望动作控制器的运动源自动改变以与之匹配。让我们实现这个目标:

  1. 打开你的 BP_VRHand 的 Construction Script。

  2. 将对 Motion Controller 组件的引用拖入 Construction Script 中。

  3. 拖出它的输出并调用Set Motion Source

  1. 将一个对Hand变量的引用拖入你的 Construction Script 中。

  2. 将其输出拖到Motion Source输入上。你会看到一个Convert EControllerHand Enum to Name节点自动出现:

某些数据类型可以很容易地转换为其他类型。在这种情况下,我们将一个枚举转换为一个名称。Enumenumerator的缩写。枚举是一种特殊的数据类型,允许我们创建一个预定义的值列表,然后将该值集合用作数据类型。如果你对数据类型有一个已知的可能值集合,最好使用枚举来列出它们,而不是使用名称或字符串。这样可以防止拼写错误导致值失败,并且与字符串比较相比,比较速度要快得多。当我们需要时,在蓝图中将枚举值转换为可读的值通常非常容易,就像我们在这里所做的一样。

  1. 最后,将你的 Construction Script 的执行输出连接到Set Motion Source输入,这样你的整个 Construction Script 看起来就像这样:

将 BP_VRHand 子 Actor 组件添加到你的 pawn 中

现在让我们返回到我们的BP_VRPawn蓝图中:

  1. 在其组件列表中,选择你的 Camera Root 组件,并添加一个 Child Actor 组件作为子组件。

  2. 将其命名为Hand_L

  3. 在其详细信息中,将 Child Actor Component 的 Child Actor Class 设置为BP_VRHand

  4. 再次选择 Camera Root,以便它成为我们接下来创建的组件的父级,并添加另一个 Child Actor 组件。

  5. 将其类设置为BP_VRHand,并将其命名为Hand_R

  6. 这次,在 Child Actor Class 属性下方,展开 Child Actor Template 属性。

  7. 将 Child Actor Template | Default | Hand 设置为Right。(我们能够这样做是因为在前面的步骤中我们使这个变量实例可编辑。)

现在我们需要确保由这些组件生成的 BP_VRHand actors 知道这个 pawn 是它们的所有者。这对于动作控制器正确注册是必需的。

  1. BP_VRPawn中,在事件图中找到事件 BeginPlay。

  2. 将刚刚创建的Hand_L组件的引用拖到图表中。

  3. 拖动它的输出并选择 Get Child Actor 以获取对其中包含的BP_VRHand对象的引用。

  4. 拖动 Child Actor 的输出并调用 Set Owner。

  5. 在图表中右键单击并选择 Get a Reference to Self 以创建一个 Self 节点。

  6. 将 Self 拖入 Set Owner 节点的 New Owner 输入。

  7. 将 Set Tracking Origin 的执行输出拖到 Set Owner 节点的执行输入中。

  8. 对于Hand_R组件也重复这个步骤。

在做其他任何事情之前,让我们进行测试。

我们应该仍然能看到我们旧的动作控制器渲染出来,因为我们还没有摆脱它们,但是我们现在应该也能看到一双手,并且它们应该能正确地随着我们的动作控制器移动。

我们的手部还有一些问题需要解决。

修复手部模型的问题

如果我们观察手部随着动作控制器移动的情况,我们会发现它们显示的角度是意外的:

  1. 让我们通过将HandMesh组件的 Transform | Rotation 设置为绕X轴旋转 90°来修复这个问题:

其次,它们都显示为右手网格,即使其中一个绑定到了左手。我们也可以在构造脚本中修复这个问题。

  1. 从我们的 Hand 变量的输出中拖出一个==运算符。测试它是否等于 Left。

  2. 使用此测试结果作为条件添加一个分支节点。

  3. 将对Hand Mesh的引用拖入构造脚本图中。

  4. 如果 Hand == Left,则在你的Hand Mesh上调用Set World Scale 3D,将其设置为 X=1.0,Y=1.0 和 Z=-1.0:

将手的网格缩放设置为-1,即在其Z轴上进行镜像,这是一种聪明的方法,可以从右手创建一个左手的网格,而无需创建第二个网格。

再试一次。现在手应该更好地倾斜,你应该有一个左手和一个右手。不过,还不完美。手的网格位置还不太对,因此它们不太像我们自己的手:

  1. 从组件列表中选择HandMesh组件,并将其详细信息|转换|位置设置为 X=-13.0,Y=0.0,Z=-1.8。

  2. 微调这些值,直到它们对你感觉合适。

在 VR 中,正确设置手的角度非常重要。正如我们在第一章中讨论的那样,我们对手的位置的感知能力非常强,如果它们看起来有一点点不对劲,它们就不会感觉真实。花时间找到在这里感觉自然的方式。这是一个微妙的细节,但它很重要。

在蓝图中替换对旧的运动控制器组件的引用

现在我们已经将手放在了正确的位置,我们需要从角色中删除旧的、多余的运动控制器组件,并将引用它们的地方替换为对我们新手的引用。让我们开始吧:

  1. 打开你的角色蓝图,并选择其MotionController_L组件。

  2. 右键单击它,选择查找引用(按下Alt + Shift + F也可以):

一个查找结果面板将打开,并显示此组件在蓝图中的使用位置。从这个列表中我们可以看到,MotionController_L在我们的图表中被使用了一次。

  1. 双击它跳转到在事件图中使用它的位置:

我们想要用对新创建的Hand_L的引用替换对MotionController_L的引用。

  1. 将对Hand_L的引用拖入你的图表中。

我们不能简单地将对MotionController_L的引用替换为对我们的Hand_L对象的引用,因为该对象本身并不随控制器移动。它包含一个运动控制器组件,可见的手网格是该运动控制器的子级。我们需要获取对该运动控制器的引用,或者更好的是,因为玩家可以看到它,获取对手的网格的引用。

创建一个函数来获取我们的手的网格

要访问我们的VRHand对象的内部组件,我们首先需要获取对包含在我们的子级角色组件中的子级角色的引用。让我们开始吧:

  1. Hand_L中拖出一个连接器,并选择“获取子级角色”:

还记得我们提到过虚幻引擎是一个面向对象的环境吗?我们一直回到这一点,因为这很重要。我们刚刚从 Child Actor 组件中提取的 Child Actor 引用是对 Actor 类的引用。正如我们在前几章中提到的,Actor 是可以放置在世界中的任何对象的父类。然而,Actor 类本身没有 Hand Mesh 组件。它只有将任何对象放置在世界中所需的基本内容。而 BP_VRHand 对象,它是 Actor 类的子类,包含了这个组件。我们需要告诉虚幻引擎,我们在这种情况下正在处理的 Actor 是一个 BP_VRHand。我们使用一个 Cast 运算符来实现这个目的。

  1. Child Actor拖动一个连接器,并选择Cast to BP_VRHand

这将创建一个 Cast 节点。Cast 节点需要一个执行输入,因为它们不能保证成功。如果你尝试将一些随机的 actor 转换为 BP_VRHand,它将失败,因为你给它的 actor 不是 VRHand。Cast 节点不会将对象转换为该类型的 actor-它只是告诉系统,如果实际上是该类型的实例,则将引用视为指定的类型。

我们将在一会儿处理这个执行线,但首先,让我们从对象中获取手部网格。

  1. 从 Cast 节点的 As BP_VRHand 输出拖动一个连接器,并选择 Get HandMesh:

现在,我们可以将其输入到当前正在从 MotionController_L 读取的 GetWorldRotation 节点中。

  1. 将 HandMesh 输出拖入 GetWorldRotation 中,替换旧的 MotionController_L 引用:

然而,这还不起作用,因为我们还没有将执行线连接到我们的 Cast 节点。如果你现在尝试编译这个,你会看到 Cast 节点上有一个警告,Get HandMesh 上有一个错误,因为这个原因。

我们有两种方法可以解决这个问题。我们可以将 Cast 节点插入到输入的主执行线中,并且只有在成功时才进行 Add Movement Input 调用,但在我们的情况下,有一种更简洁的方法。我们可以创建一个纯函数来执行转换。

纯函数是一个不改变包含它的对象状态的函数,因此它不需要放置在执行线中。在我们的情况下,我们只是获取手部网格的引用-这并不重要我们何时这样做,因为我们没有改变任何东西。我们只是读取一个值,只要在我们需要使用它之前发生这种情况,那就没问题。

  1. 选择 Hand_L 节点,它的 Child Actor,Cast 和 Get Hand Mesh 节点。

  2. 右键单击并选择折叠到函数:

  1. 将函数命名为 GetHandMeshForHand。

  2. 将其 Pure 属性设置为 true:

你会注意到,当你这样做时,执行引脚消失了。现在,我们有一个简单、干净的节点,可以用来获取我们的手部网格。

让我们改进一下。我们知道我们将需要为右手执行相同的操作,但是制作一个几乎相同的函数来完成这个工作是浪费的。让我们设置这个函数,使其可以获取任何一只手。

  1. 选择函数后,找到其详细信息|输入列表,并点击+按钮创建一个新的参数。

  2. 将参数的类型设置为 EControllerHand,并将其命名为 Hand:

你会看到你的纯函数节点现在有一个输入选择器,因为我们使用的输入是一个枚举器,它已经知道可用的值。很有用,对吧?

这是另一个枚举器优于字符串作为数据类型的原因。请不要使用字符串作为数据类型,除非有非常少的例外情况。它们速度慢,并且极易出错。

现在,我们需要更新我们的函数以使用这个新的输入。

  1. 打开Get Hand Mesh for Hand函数。

现在,无论用户选择Hand输入什么,我们都会得到对 Hand_L 的引用。是时候修复这个问题了。

  1. 从你的Hand输入拖出一个连接器并创建一个 Select 节点。

  2. 将 Select 节点的返回值拖入 Child Actor 的 Target 输入中,替换Hand_L的输入。

  3. 取 Hand_L 引用并将其输出输入到选择器的 Left 输入中。

  4. 拖出一个 Hand_R 的实例到图表中,并将其输入到选择器的 Right 输入中。

  5. 我们可以将其余的输入设为 Null,因为我们在这里不使用它们:

现在,如果用户将 Left 传递给Hand参数,将使用Hand_L引用,如果他们传递 Right,将从Hand_R读取。我们在这里没有安全处理用户传入任何其他值的情况,所以如果用户选择了 Gun 或其他输入,函数将抛出一个错误。从技术上讲,在这种情况下,这可能是可以的,因为我们知道我们计划给它什么输入,但为了良好的实践,让我们使它更安全一些。

如果我们传入一个既不是 Left 也不是 Right 的值给 Select 节点,它将返回一个 Null(空)引用。尝试从空引用中读取值是一件不好的事情。在 C++中,它会导致应用程序崩溃。在蓝图中,它只会抛出一个错误,但是让它发生仍然不是一个好的做法。

  1. 从 Select 节点拖出一个输出,并创建一个 IsValid 节点。你有两个版本可以选择。使用宏版本(带有问号的版本),因为这将为你提供方便的执行引脚:

  1. 将函数输入的执行引脚拖动到IsValid节点的 Exec 引脚上。

  2. 将 IsValid 输出拖入 Cast 节点的输入中,以便在尝试转换之前进行 IsValid 检查。

  3. 从 Is Not Valid 输出中拖出并选择 Add Return Node。在这里不要连接任何东西到 Hand Mesh 输出。如果用户将一个错误的输入传递给Hand变量,这将返回一个 Null(空)值。

  4. 在我们进行这些操作的同时,我们还应该将Cast节点的 Cast Failed 输出连接到这个空的返回节点,这样如果转换失败,它就不会尝试从一个错误的对象中获取 HandMesh。

完成的函数应该是这样的:

我们现在创建了一个纯函数,它返回所提供手的子 actor 组件中包含的 HandMesh。下面是它的使用方法:

现在,我们已经创建了一个干净、易于使用的函数来获取我们的 Hand 模型,让我们用它来替换我们的MotionController_R引用。

  1. 从你的组件列表中,右键点击MotionController_R并选择 Find References。你会看到我们在两个地方使用它。

  2. 双击第一个使用,跳转到图表的那部分。

  3. GetHandMeshForHand函数的一个实例拖到当前正在使用MotionController_R的图表上。

  4. 从 Hand 下拉菜单中选择 Right。

  5. 按住 Ctrl 键并将MotionController_R的输出连接从GetHandMeshForHand的输出连接上拖动:

按住 Ctrl 键并拖动是一种快速将所有连接从一个引脚移动到另一个引脚的方法。

你的图表现在应该是这样的:

  1. 对另一个对MotionController_R的引用也做同样的操作。

  2. 从组件列表中删除 MotionController_L 和 MotionController_R 组件。

测试一下。你的动作控制器应该像以前一样工作,但是手的模型现在替换了旧的控制器模型。

给我们的手添加动画

现在,让我们根据玩家的输入来改变手的姿势。

我们首先需要告诉手部玩家何时想要对其进行操作。让我们通过在BP_VRHand上创建一对可以从外部调用的函数来实现这一点:

  1. 打开BP_VRHand蓝图。

  2. 在函数列表中创建一个新函数。将其命名为Grab Actor

  3. 创建另一个名为Release Actor的函数。

  4. 在这些函数的内部,创建一个带有函数名称的 Print String 节点。由于我们暂时不打算让这些函数做任何事情,我们希望能够看到它们被调用的时候:

让我们更好地组织我们的函数和变量。虽然我们还没有这样做,但这是一个好的实践。

  1. 对于这两个函数,将它们的 Details | Graph | Category 设置为Grabbing。在使用过一次类别名称后,它将出现在其他函数和变量的下拉列表中。

关于访问限定符的一点说明

在这里,我们要注意这些函数的访问限定符属性。默认情况下,它设置为 Public。在这种情况下,这是我们想要的,但让我们花点时间来讨论一下这些访问限定符的含义:

  • Public函数可以从类外部调用。因此,如果我创建了一个名为Foo的类,并在其中创建了一个名为Bar的公共函数,我可以从其他蓝图中获取Foo的实例并调用其Bar函数。

  • Private函数不能从类外部调用。假设Bar函数是Foo类作为某个其他操作的一部分使用的内部操作,并且不应该从外部调用。在这种情况下,应将函数设置为私有,以便其他人不会尝试从外部调用它,并且它不会在其他上下文中混淆类的可用操作列表。

  • Protected函数不能从类外部调用,但可以从类的子对象中调用。如果FooChild类继承自Foo类,并且Foo类中的Bar函数是私有的,那么FooChild将无法调用它。如果它是受保护的,那么FooChild可以调用它,但它仍然不能从对象外部调用。

你的一般准则应该是将每个函数都设置为私有,除非你打算从类外部调用它。虚幻默认将函数设置为公共,因为这对于可能不了解访问限定符的开发人员来说很容易,但是现在你已经了解了,除非有理由不这样做,否则应该将所有函数都设置为私有。在开发的早期阶段,当应用程序还很小的时候,这不会有太大的影响,但是一旦应用程序变得庞大,它将会有所不同。能够查看一个函数并知道可以安全地更改它是一个大的时间节省和调试辅助,因为你可以确信没有其他人在使用它。

对于我们刚刚创建的这两个函数,默认的Public访问限定符是正确的,因为我们打算从 pawn 中调用它们。

从 pawn 调用我们的抓取函数

现在,我们可以关闭BP_VRHand并打开BP_VRPawn。然而,在我们对 pawn 进行任何操作之前,我们需要向项目的输入中添加一些其他的动作映射。

创建新的输入动作映射

我们将像以前一样使用项目设置中的输入 UI 来完成这个任务。同时,还要记住这些设置只是读取和写入你的DefaultInput.ini。在这里做工作几乎总是一个好主意,但了解在更改此界面时实际发生的情况也是值得的。让我们开始吧:

  1. 打开项目设置 | 引擎 | 输入,并展开动作映射列表。

  2. 添加一个名为GrabLeft的新动作映射,并将其绑定到MotionController (L) Trigger

  3. 添加另一个名为GrabRight的新动作,并将其绑定到MotionController (R) Trigger

  1. 关闭项目设置,返回到BP_VRPawn蓝图。

添加新的动作映射处理程序

现在我们已经在项目设置中创建了新的输入动作,让我们让我们的角色监听它们:

  1. 在你的角色的事件图表中,添加一个 InputAction GrabLeft。

  2. 将对 Hand_L 子级角色组件的引用拖动到图表中。

  3. 调用Get Child Actor

  4. 将子级角色的输出转换为BP_VRHand

  5. Cast节点的 As BP_VRHand 输出拖动一个连接器,并调用Grab Actor。你可以在这里调用这个函数,因为我们将它设置为公共的。

  6. 从输入动作的 Pressed 输出调用Cast节点。

  7. 如果转换成功,则调用Grab Actor。蓝图编辑器可能会自动为你连接这个:

你可以看到我们将输入堆叠在 Cast 节点的顶部。这只是一种视觉组织策略。这通常是一种方便的方式来组织你的节点,以便清楚地表明整个集群实际上只是指一个单一的对象。

  1. 拖动一个选框覆盖Hand_L节点,它的Get Child Actor调用和Cast,以选择这三个节点。

  2. 右键单击它们,选择折叠为宏。

  3. 将新宏命名为GetHand_L

新的宏将自动插入到这些节点最初所在的位置。

  1. 按下Ctrl + W复制宏。

  2. 将输入动作的 Released 输出连接到新宏的输入。

  3. 在宏的 As BP_VRHand 输出上调用Release Actor

如果我们打开GetHand_L宏,我们会看到它包含了我们之前在图表中散落的节点:

我们可以看到如果转换失败,我们什么都不做,而在这种情况下,这正是我们想要的。如果由于某种原因,Hand_L类的子级角色发生了变化或未设置,我们不希望尝试进行任何调用。

重要的是要区分宏不是函数。它们看起来像函数,通常可以用来做类似的工作,但宏实际上只是一条指令,告诉蓝图编译器将其内容粘贴到宏出现的图表中。它没有像函数那样存储局部变量的能力。宏非常简单,只是自动复制和粘贴。一些开发人员会建议你完全避免使用宏。如果你对宏与函数的区别不清楚,这绝对是一个好建议,但如果你了解它们的工作原理,它们可以非常有用。作为一个好的经验法则,保持你的宏非常小。如果你在宏中做了很多工作,你实际上是在告诉编译器将大量的节点粘贴到你的图表中,这种情况下它应该是一个函数。将宏视为一种创建可重用节点的简单任务的方式。使用它们可以提高可读性,并使你的代码更容易修改。

现在,让我们为右控制器输入重复这个过程:

  1. 从宏列表中选择你的GetHand_L宏,并按下Ctrl + W进行复制。

  2. 将新宏命名为GetHand_R

  3. 在其中,将Hand_L引用替换为对Hand_R的引用。

  4. 在图表中拖动两个GetHand_R实例。

  5. 将它们连接到 InputAction GrabRight 节点的 Pressed 和 Released 引脚。

  6. 在它们的输出上调用GrabActorReleaseActor,就像之前做的那样。

你的完成的图表应该是这样的:

如果你认为我们可以复制我们的 GetMeshForHand 函数并修改它以直接返回BP_VRHand引用,那么你是对的。我们也可以直接修改该函数,并将我们在传送函数中进行的 Get HandMesh 调用移出来。通常有很多正确的方法来完成同样的工作。在这种情况下,我们只是做了一个简单的转换,一对宏是保持我们的蓝图可读性的好方法。

让我们进行测试。如果我们做得没错,当我们挤压和释放扳机时,我们现在应该在视图中看到Grab ActorRelease Actor消息出现。

在手部蓝图中实现抓取动画

现在,我们已经设置好了输入并设置好了VRPawn以将它们传递给各自的运动控制器,让我们在接收到这些输入时使这些运动控制器进行动画化。

让我们回到我们的BP_VRHand蓝图中:

  1. BP_VRHand的变量列表中,添加一个名为bWantsToGrip的新布尔变量。

  2. 按下Alt+拖动bWantsToGrip的 setter 到Grab Actor函数图中。当调用Grab Actor时将其设置为 true。

  3. 按下Alt+拖动bWantsToGrip的 setter 到Release Actor中。在这里将其设置为 false:

按下Ctrl+拖动一个变量会自动创建该变量的 getter。按下Alt+拖动一个变量会创建一个 setter。

为手部创建一个动画蓝图

虚幻使用动画蓝图来控制骨骼网格上的动画。我们需要一个手部的动画蓝图:

  1. 在内容浏览器中,在项目的Blueprints目录中右键单击,选择创建高级资产|动画|动画蓝图:

一个对话框将出现,询问动画蓝图的父类和它要控制的目标骨骼:

  1. 将父类留空,并选择MannequinHand_Right_Skeleton作为目标骨骼。

  2. 将其命名为ABP_MannequinHand_Right

为我们的手部动画创建一个混合空间

现在,我们希望我们的手部动画对这个值做出响应。由于我们希望能够在不同的动画姿势之间平滑混合,我们最好的工具是混合空间

您有两种可用的混合空间类型。有标准的混合空间,可以混合两个不同的轴(这通常用于射击游戏中的瞄准姿势),还有一个更简单的只沿一个轴混合的混合空间。这是我们想要的那个。让我们开始吧:

  1. Blueprints目录中右键单击,选择创建高级资产|动画|1D 混合空间。

  2. 一个对话框将出现,询问这个混合空间将应用于哪个骨骼。选择MannequinHand_Right_Skeleton

  3. 将其命名为BS_HandGrip

  1. 打开我们刚刚创建的混合空间:

混合空间编辑器由左侧的资产详细信息面板、预览窗口、底部的示例点工作区组成,

和右下角的动画资产浏览器。

在右下角,您可以看到我们从 VR 模板迁移的手部动画列表。它只是显示与手部网格的骨骼映射的任何位于Content目录中的动画。

在预览下方的中心位置,我们可以看到我们将构建混合的工作区。

我们需要做的第一件事是设置我们要用于混合的轴。让我们开始吧:

  1. 在左上角找到资产详细信息|轴设置,并展开水平轴块。

  2. 将其名称设置为Grip

  3. 将其最大轴值设置为 1.0。

现在,我们有一个放置动画姿势的地方。

  1. 从资源浏览器中,将MannequinHand_Right_Open拖放到工作区,直到它与 0.0 网格线对齐。

  2. MannequinHand_Right_Grab拖放到 1.0 线上。

  3. MannequinHand_Right_CanGrab拖放到中间位置,即 0.5。

通过按住Shift键并在工作区上拖动来测试它。我们可以通过改变其值在三个动画姿势之间无缝混合,这些姿势应用于 Grip 轴:

让我们在我们的动画蓝图中使其工作。

将混合空间连接到动画蓝图

现在我们可以将刚刚创建的混合空间作为资产在其动画蓝图中使用。动画蓝图是一种强大的工具,可以控制骨骼网格上播放动画的方式。它分为两个主要部分:

  • 动画图表接收动画输入并处理它们以计算每帧上的网格姿势

  • 事件图表类似于您已经创建的蓝图,并用于处理动画蓝图将用于决定播放哪些动画的数据

让我们学习一下它的工作原理:

  1. 打开我们刚刚创建的动画蓝图。

查看其我的蓝图|图表块,您可以看到除了我们所有蓝图资产中都有的熟悉的 EventGraph 之外,还有一个名为 AnimGraph 的第二个图表。

  1. 双击我的蓝图|图表|AnimGraph 打开它:

Anim Graph负责确定每个刻度上其控制的骨骼网格的动画姿势。我们可以看到这里有一个蓝图图表,但它与我们熟悉的事件图表不同。动画图表中的所有内容都导致最终的动画姿势,并用于决定它将是什么。我们不会在这里深入研究动画蓝图,因为它们的设置是一个深入的主题,超出了本书的范围,但它们值得学习。我们的手部动画图表将非常简单。

  1. 从内容浏览器中获取我们刚刚创建的BS_HandGrip混合空间,并将其拖放到动画图中。

  2. 将其动画姿势输出拖动到最终动画姿势节点上的结果动画姿势输入。

  3. BS_HandGrip节点的 Grip 输入拖出一个连接器,并将其提升为变量。将变量命名为Grip

  1. Grip变量的滑块范围和值范围的最小值设置为 0,最大值设置为 1。

  2. 编译蓝图:

在窗口的右下角,您将看到一个 Anim Preview Editor 选项卡。您在动画蓝图中创建的变量将显示在此处,您可以实时更改它们的值以查看它们如何影响动画。(您实际上并没有更改变量的默认值-您只是使用不同的值预览系统的行为。)试试看。将鼠标移到Grip值上并拖动它,以在 0.0 和 1.0 之间滑动。您会看到它驱动了我们创建的混合空间,进而驱动了最终的动画姿势。通过改变Grip浮点数的值,您可以关闭和打开手。

让我们使其响应用户的输入。

将动画蓝图连接到我们的手部蓝图

我们需要告诉BP_VRHand角色,HandMesh组件应该使用我们的新动画蓝图来驱动其动画状态:

  1. 打开BP_VRHand并从组件列表中选择HandMesh骨骼网格组件。

  2. 在其详细信息|动画中,验证其动画模式是否设置为使用动画蓝图。(默认情况下应该是这样。)

  3. 使用 Anim Class 下拉菜单选择您的新动画蓝图:

现在,让我们驱动刚刚连接的动画蓝图上的 Grip 值。

  1. BP_VRHand的事件图中找到事件 Tick,如果需要的话创建它。

  2. 将对Hand Mesh的引用拖放到图表中。

  3. Hand Mesh拖动一个连接器,并在其上调用Get Anim Instance

对于由动画蓝图控制的骨骼网格,Anim Instance 将是对该动画蓝图的引用。现在,由于我们需要访问该蓝图的特定成员,我们需要将动画实例转换为我们正在使用的特定动画蓝图类。

  1. Get Anim Instance返回值拖动一个连接器,并将其转换为我们的新动画蓝图类(ABP_MannequinHand_Right)。

  2. 从 As ABP_Mannequin Hand Right 输出中调用Set Grip

  3. 按下Ctrl +拖动bWantsToGrip到图中以获取其值。

  4. bWantsToGrip拖出一个连接器并创建一个Select节点。

  5. 将选择节点的返回值连接到 Set Grip 的 Grip 输入。

  6. 将选择节点上的 True 值设置为 1.0。

您的图现在应该是这样的:

让我们运行并测试一下。好的,很好。我们的手对我们的输入做出了响应。它们看起来还不太好,但我们可以看到基本功能正在工作。当我们在运动控制器上按下扳机时,该输入将bWantsToGrip设置为true,并且在 VRHand 的 Tick 事件上,我们根据bWantsToGrip的当前值将 Grip 变量的值设置为 0.0 或 1.0。

现在,让我们稍微改进一下,并设置系统更加灵活。

为我们的抓握创建一个新的枚举器

现在,我们只是直接驱动手的动画蓝图上的Grip值,但更合理的做法是让动画蓝图处理这个,并告诉它发生了什么。毕竟,处理动画的系统应该负责决定如何处理它。

让我们为动画蓝图提供一种简单的方式来传达我们的抓握状态。枚举非常适合这个:

  1. 在蓝图目录中右键单击,选择“创建高级资产|蓝图|枚举”。将其命名为EGripState

  1. 打开新的枚举器。

  2. 在枚举器列表中,点击“新建”创建一个新条目。

  3. 将新条目的显示名称设置为Open。可以将其描述留空:

  1. 创建另一个枚举器条目,并将其命名为Gripping

  2. 关闭枚举器。

现在,我们已经创建了一个新的数据类型,可以用来存储信息并在对象之间传递。让我们将其添加到我们的动画蓝图中。

  1. 打开您的动画蓝图并将一个新变量添加到其变量列表中。

  2. 将其变量类型设置为EGripState,并将其命名为GripState

还记得刚才我们注意到动画蓝图包含两个图表-动画图事件图吗?现在,我们将开始使用事件图。这是一个强大的系统。它允许我们将游戏逻辑放在游戏对象中,将动画逻辑放在动画蓝图中。我们可以将一个值传递到动画蓝图中,然后在其事件图中确定我们希望它如何处理该输入。

  1. 在动画蓝图的事件图中,找到事件蓝图更新动画节点,如果不存在则创建一个。这相当于动画蓝图中的 tick 事件。

  2. 按下Ctrl +拖动对新的Grip State变量的引用到事件图中。

  3. 从其输出拖出一个连接器并创建一个选择节点。

您会注意到,当您从枚举创建选择节点时,它会自动填充该枚举的可用值:

  1. 按下Alt +拖动对Grip变量的引用到图中以创建一个设置器。

  2. 将选择节点的输出拖入 Grip 设置器中。

  3. 将其 Gripping 值设置为 1.0。

  4. 编译蓝图。

  5. 在动画预览编辑器中,验证将 Grip State 从 Open 更改为 Gripping 会关闭手:

现在,让我们更新BP_VRHand,以发送枚举值而不是抓握值:

  1. 在 BP_VRHand 的Event Tick中,删除Grip设置器和馈送它的选择节点。

  2. Cast输出中拖出一个连接器,并选择Set Grip State

  3. bWantsToGrip获取器中拖出一个新的选择节点。

  4. 将选择节点的输出拖入GripState设置器的输入中。

  5. 将选择节点的 True 值设置为Gripping

您的图现在应该是这样的:

测试一下。没有明显的变化,对吧?我们在这里做的是设置我们的图表,以便我们现在可以更容易地修改它们。既然我们已经验证了新的设置与旧的设置的工作方式相同,让我们回到动画蓝图中,改进我们处理其输入的方式。

平滑我们的握持动画

在打开和关闭动画姿势之间的切换看起来很糟糕。让我们通过随时间过渡值之间的变化来平滑处理这个问题:

  1. 跳转回动画蓝图的事件图。

  2. 右键单击并添加一个FInterp to Constant节点。

  3. 将您的Grip变量拖放到其当前输入上。

  4. 将 Grip State Select 节点的输出拖放到其目标输入上。

  5. Event Blueprint Update Animation中的 Delta Time X 值拖放到其 Delta Time 输入上。

  6. 从其Interp Speed输入中拖出一个连接器,并将其提升为名为Interp Speed的变量。

  7. 编译蓝图并将Interp Speed设置为 7.0。

  8. FInterpToConstant的输出连接到Grip设置器的输入:

测试一下。好多了。现在,我们的手部在姿势之间进行插值,而不仅仅是跳到该值。这里发生的是 Interp to Constant 节点通过 InterpSpeed 指定的持续时间平滑地过渡到由 Grip State 选择的新目标值。如果我们希望过渡发生得更快,只需减小 Interp Speed。如果我们希望过渡时间更长,只需增大 Interp Speed。

尽管这个例子很简单,但它开始展示了动画蓝图提供的强大和灵活性。我们可以轻松地从 VRHand 蓝图中传递状态信息,告诉动画蓝图我们想要做什么,然后在动画蓝图中以任何我们想要的方式来展示该状态。

总结

这是另一个复杂的章节。我们在这里做了很多工作。我们首先创建了一个新项目,并将我们的 VRPawn 蓝图以及所需的对象迁移到新项目中。我们学会了通过将DefaultInput.ini的内容复制到新项目中来重新创建输入绑定的快速方法。然后,我们将 Soul:City 资源和地图添加到我们的项目中,并设置了一个导航网格,以便我们可以探索它。

然后,我们进入了本章的重点。我们从 VR 模板项目中回收了一个手部网格,并创建了一个“蓝图”类来驱动它们的行为。我们学会了如何使用构造脚本在编辑器和游戏中创建对象时改变它们。我们学会了如何在我们的角色中创建子级角色组件以及如何在蓝图中使用它们。我们学会了如何创建动画混合空间和动画蓝图来为我们的手部网格添加动画,并学会了如何使用枚举器将状态信息传递到动画蓝图中。

在下一章中,我们将学习如何使用这些手来拾取物体。我们将学习如何使用蓝图接口来启用对各种对象进行函数调用,并学习如何检测我们可以拾取的角色。我们还将学习一些关于使用触觉反馈效果来指示玩家何时与可以拾取的物体接触的知识。

第六章:与虚拟世界交互-第二部分

在上一章中,我们设置了我们的手并学习了如何对它们进行动画。正如我们之前提到的,仅仅这一点就可以代表我们的应用程序建立存在感的重要一步。现在,让我们迈出下一步,开始使用它们。

在本章中,我们将学习以下主题:

  • 如何使用蓝图接口为各种蓝图添加功能

  • 如何使用附件来拾取和放下物理角色

  • 如何指示玩家何时可以与物体交互

  • 如何创建触觉反馈效果以提供更多触觉反馈给用户

创建一个可以拾取的物体

我们将首先制作一些可以拾取的物体。让我们从一个简单的立方体开始:

  1. 在内容浏览器中右键单击项目的Blueprints目录,然后选择“Create Basic Asset | Blueprint Class”。

  2. 这次,不要选择其中一个常见类作为其父类,而是展开“Pick Parent Class”对话框底部的“All Classes”条目。

  3. 选择“Static Mesh Actor”:

  1. 将其命名为BP_PickupCube

  2. 打开BP_PickupCube

您可以看到它继承了一个Static Mesh Component

我们也可以创建一个Actor蓝图并添加一个Static Mesh组件,但是当您构建新资产时,选择适当的父类是一个好习惯。如果不必要,不要重新发明轮子。

  1. Static Mesh Component的“Static Mesh”属性设置为Engine Content/Basic Shapes/Cube1

  2. 将其“Scale”设置为0.2, 0.2, 0.2

  3. 将其“Materials | Element 0”设置为Content/SoulCity/Environment/Materials/Props/MI_Glow。(或者您喜欢的其他任何东西,但这个在地图中很容易看到。)

现在,我们希望立方体模拟物理效果,所以让我们设置一些值来实现这一点:

  1. 将其“Physics | Simulate Physics”标志设置为True

  2. 将其“Collision | Simulation Generates Hit Events”设置为True

  3. 将其“Collision | Generate Overlap Events”设置为True

  4. 确保其“Collision | Collision Presets”设置为PhysicsActor。(当您将“Simulate Physics”设置为 true 时,这应该会自动设置。)

  5. 将其“Collision | Can Ever Affect Navigation”设置为False。(这将在“Collision”部分的高级属性中隐藏。)

我们现在创建了一个小的发光立方体,它会自然地对物理作出反应,但在移动世界时不会阻碍我们的导航网格。

现在,我们需要让它具备被拾取的能力。我们可以通过几种方式来实现这一点。我们可以直接在BP_PickupCube的蓝图中编写PickupDrop方法,但我们需要能够从外部调用这些函数。

正如我们之前所见,如果您想从蓝图外部调用一个函数,您必须确保您正在与包含该函数的类进行交流,我们通过将引用转换为该类来实现这一点。如果我们只预期拾取立方体,那么这样做就可以了,但是如果我们希望能够轻松拾取其他对象呢?我们不希望每次添加一个新类型的可拾取物体时都要重写我们的BP_VRHand蓝图,所以这不是一个很好的解决方案。

我们可以从一个实现了PickupDrop方法的共同父类派生出BP_PickupCube,然后将我们的引用转换为该父类。这样做更好,但仍然不完美。BP_PickupCube继承自StaticMeshActor,但如果我们想让从SkeletalMeshActor继承的物体也能被拾取怎么办?在这种情况下,我们没有简单的方法来创建一个共同的父类。

解决这个困境的答案是蓝图接口。接口是一个蓝图对象,允许我们定义可以在实现接口的任何对象上调用的函数,无论该对象从哪个类派生。它是一个可以附加到任何对象的类,并且它作为一个承诺,附加到它的对象将实现接口中包含的每个函数。例如,如果我创建一个声明了PickupDrop函数的接口,并将该接口应用于我的BP_PickupCube,我可以在不必先转换对象的情况下调用PickupDrop方法。这是一个强大的模式。通过巧妙地使用接口,您可以使您的代码非常灵活和易于扩展。

如果这还不完全清楚,不要担心。一旦我们构建它,它会变得更加清晰。

为拾取对象创建一个蓝图接口

要创建一个蓝图接口,请按照给定的步骤进行操作:

  1. 在项目的“蓝图”目录中右键单击,选择“创建高级资产|蓝图|蓝图接口”:

  1. 将其命名为BPI_PickupActor

当你打开它时,你会看到它包含一个函数列表,除此之外什么都没有。你会注意到图表无法编辑。这是因为接口只是一个函数列表,附加对象必须实现这些函数,但这些函数不会在接口中编写。

  1. 默认情况下,它为您创建了一个新的函数声明。将其命名为Pickup

  2. 在函数的详细信息|输入下,添加一个新的输入。将其类型设置为场景组件|对象引用,并将其命名为AttachTo

  1. 添加另一个函数,并将其命名为Drop。这个函数不需要任何输入。

  2. 编译、保存并关闭接口。

现在,让我们将这个新接口应用到BP_PickupCube上:

  1. 打开BP_PickupCube,并点击工具栏上的“类设置”项。

  2. 在详细信息|接口下,点击“已实现的接口”下的添加按钮。

  3. 选择BPI_PickupActor

实现拾取和放下函数

现在,我们已经将这个接口添加到BP_PickupCube类中,我们可以在事件图中实现我们在该接口中声明的函数。让我们开始吧:

  1. 在事件图中,右键单击并选择“事件拾取”来创建一个拾取事件。现在,这个蓝图类上存在这个事件,因为我们附加了一个声明它的接口。你会看到这个事件表明它是来自BPI_PickupActor的接口事件。

  2. 以相同的方式创建一个Drop事件。

现在,我们已经为来自接口的两个事件创建了处理程序,让我们让它们起作用。

当拾取这个物体时,我们希望关闭它的物理模拟,这样它就不会从我们的手中掉下来,并且我们希望将它附加到拾取它的手上的一个场景组件上。

  1. 将对“静态网格组件”的引用拖动到事件图中。

  2. 调用Set Simulate Physics并将 Simulate 设置为False

  3. 在图表中右键单击并选择“获取根组件”。

  4. 从根组件引用拖动一个连接器,并选择“附加到组件”。你会看到有两个选项。将鼠标悬停在上面并选择那个工具提示为“目标是场景组件”的选项,因为我们将要附加到一个场景组件上:

  1. 将“事件拾取”的“附加到”输出拖动到“附加到组件”节点上的父级输入。

  2. 在“附加到组件”节点上,将位置、旋转和缩放规则设置为“保持世界”,并将焊接模拟体设置为False

您完成的拾取实现应该如下所示:

当我们放下这个物体时,我们希望将其物理重新打开并将其从我们拾取时附加的场景组件上分离出来。

  1. 选择您的“静态网格组件”引用和Set Simulate Physics调用,并按下Ctrl + W进行复制。

  2. 将事件 Drop 引脚的执行连接到复制的Set Simulate Physics调用。

  3. 将模拟设置为 True,以便我们重新开启物理效果。

  4. 右键单击并创建一个Detach From Actor节点。

  5. 将位置、旋转和缩放规则设置为Keep World,就像我们在 Attach 节点上所做的那样。

您完成的 Drop 实现应该如下所示:

这就是我们的Pickup Cube角色的全部内容。我们可以关闭蓝图了。

设置 VRHand 以拾取物体

现在,我们准备好抓取这些物体了。

创建一个函数来查找最近的可拾取对象

我们需要做的下一件事是找出哪些物体离我们的手足够近,可以被拾取。让我们创建一个函数来完成这个任务:

  1. BP_VRHand中,创建一个名为FindNearestPickupObject的新函数。

  2. 将其类别设置为Grabbing,将其访问限定符设置为Private

  3. 在其实现图中,右键单击创建一个Get All Actors with Interface节点,并将其接口值设置为BPI_PickupActor

这将为我们提供场景中实现BPI_PickupActor接口的每个演员的数组。

  1. 从 Out Actors 输出拖出一个连接器并创建一个For Each Loop节点:

我们将遍历可能被拾取的演员,忽略任何距离太远而无法考虑的演员,然后返回最接近的剩余合格演员。

  1. For Each Loop的 Array Element 输出中拖出一个连接器并调用Get Actor Location

  2. Hand Mesh的引用拖到图表上并调用Get World Location

  3. 从数组元素的角色位置中减去手部网格的世界位置:

  1. 获取结果向量的Vector Length Squared

  2. 拖出其结果并选择提升为本地变量。将新变量命名为LocalCurrentActorDistSquared

  1. 将 Loop Body 执行线连接到本地变量的设置器。

  2. 拖动本地变量设置器的输出并创建一个<=测试,以查看它是否等于或短于我们要给它的值。

我们在这里创建一个本地变量的原因是,如果在我们的测试半径内有多个可抓取的角色,我们将需要再次使用此值,并且我们不希望浪费时间重新计算距离,因此我们将其存储在这里以便以后使用。

  1. 创建一个浮点变量并将其命名为GrabRadius。编译蓝图并将其值设置为 32.0。 (稍后,您可以根据自己的感觉调整此值。)

  2. 按住 Ctrl 键并将GrabRadius拖到图表上。

  3. 从其输出拖出一个连接器并对其进行Square操作。

  4. 将平方的结果连接到<=测试的第二个输入:

记住,当我们提到实际距离检查很昂贵时?这是一个重要的地方,因为我们将在Tick事件上调用此函数。由于我们只想看看演员是否在提供的半径内,但我们不关心它实际上有多远,所以在平方值上进行此测试更便宜。

  1. 从我们的<=测试的输出创建一个Branch节点。

如果我们的演员通过了<=测试,我们就知道它在抓取范围内。现在,我们需要看看它是否是该范围内最近的对象。

  1. 在本地变量列表中,创建一个名为ClosestRange的新的本地变量,并将其变量类型设置为Float。将其默认值设置为10000.0

局部变量是仅存在于声明它们的函数中的变量。它们不能从函数外部读取。在函数中使用局部变量来存储仅由该函数使用的值是一个好主意,这样它们不会混乱周围的对象。局部变量在每次运行函数时都会重置为其默认值,因此您不必担心来自先前函数调用的奇怪值。

  1. 按住 Ctrl 键并将LocalCurrentActorDistSquared拖动到图表上以获取其值。

  2. 从其输出处拖动一个连接器,并从中创建一个<测试。

  3. Closest Range局部变量拖动到测试的第二个输入中。

  4. 使用<测试结果创建一个 Branch:

如果此测试返回 true,则表示我们找到了一个新的最近演员。我们想保存对它的引用并将其距离记录为新的最近距离。

  1. 按住 Alt 键并将Closest Range拖动到图表上,并将LocalCurrentActorDistSquared拖动到其输入中。

  2. 从分支的 True 输出中设置此值。

  3. 创建一个名为NearestPickupActor的新的局部变量,并将其类型设置为 Actor | Object Reference。

  4. 按住 Alt 键并将其拖动到图表上以设置其值。

  5. 将其值设置为For Each Loop的 Array Element。(这将是一个很长的连接。考虑创建一些重定向节点以使其更易读。)

  6. 将其连接到Set Closest Range节点的输出:

最后,一旦我们遍历了所有可能的对象并找到了最佳的可拾取候选对象(如果存在),我们希望保存该值,以便我们的拾取方法可以使用它。

  1. 创建一个新的变量(这次不是局部变量 - 我们希望在外部读取此值),命名为AvailablePickupActor,并将其类型设置为Actor > Object Reference

  2. 按住 Alt 键并将其拖动到For Each Loop的 Completed 输出附近的事件图上。

  3. For Each Loop的 Completed 输出连接到Available Pickup Actor的 Set 输入。

  4. Nearest Pickup Actor局部变量拖动到 setter 的输入中:

这样做的目的是将一个可外部读取的Available Pickup Actor变量设置为我们在遍历可能的演员列表时找到的演员(如果有的话)。如果我们没有找到任何演员,那么Nearest Pickup Actor将为Null

在 Tick 事件上调用 Find Nearest Pickup Object

现在,是时候调用我们的新函数了,以便我们知道何时能够拾取一个对象。然而,如果我们已经拿着一个对象,我们不希望这样做,所以我们应该存储对任何我们已经拿着的对象的引用。让我们开始吧:

  1. 返回到BP_VRHand的事件图中,找到Event Tick

  2. Event Tick附近创建一个Sequence节点。

  3. 我们希望在查找可以抓取的对象之后才更新手部动画,因此按住 Ctrl 键并将来自“Event Tick”的执行引脚的输出拖动到 Sequence 节点的 Then 1 输出上。

  4. 将“Event Tick”的执行引脚连接到 Sequence 节点的输入。

  5. 选择与 Sequence 节点的 Then 1 输出连接的节点网络,并将它们拖动到下方,以便有足够的空间进行操作:

  1. 创建一个新的变量,命名为HeldActor,并将其变量类型设置为Actor > Object Reference

  2. 按住 Ctrl 键并将HeldActor拖动到事件图中以获取其值。

  3. 右键单击它并选择Convert to Validated Get

  4. 将一个调用 Find Nearest Pickup Object 的节点拖动到图表上,并从 Held Actor getter 的 Is Not Valid 输出中调用它:

这样,只有在我们还没有拿起一个对象时,我们才会检查可拾取的演员。

拾取一个演员

现在我们正在寻找可以拾取的演员,让我们在尝试抓取它们时实现这一点。让我们开始吧:

  1. 打开BP_VRHand中的Grab Actor函数。

  2. 我们不再需要这里的Print String节点,所以我们可以将其删除。

  3. 按住Ctrl并将HeldActor的 getter 拖动到图表上,右键单击它,并将其转换为已验证的获取。

  4. bWantsToGripsetter 的执行输出连接到HeldActorgetter 的输入。

  5. 按住Ctrl并将AvailablePickupActor的 getter 拖动到图表上,并将其也设置为已验证的获取。

  6. Held Actor获取的 Is Not Valid 输出连接到此 getter 的输入,因为我们只对如果我们还没有拿着物体感兴趣。

  7. Available Pickup Actor拖出一个连接器并调用Pickup (Message)

这就是为什么蓝图接口如此有用。我们不需要将拾取角色强制转换为任何特定的类来调用接口方法。我们只需进行调用,如果对象实现了接口并知道如何处理它,调用将起作用。如果对象没有实现接口,它将什么也不做。

如果您需要找出给定的角色是否实现了一个接口,请在其上调用Does Implement Interface。如果在对象上找到接口,它将返回 true。在这种特殊情况下,进行此调用将是多余的,因为我们知道Available Pickup Actor将始终实现 BPI_PickupActor 接口。当我们在Find Nearest Pickup Object函数中查找对象时,我们使用该接口作为过滤器。

  1. 将 Motion Controller 组件拖动到您的 Pickup 节点的 Attach To 输入上。

  2. Held Actor变量拖动到Available Pickup Actor的输出上,将其设置为该值。

  3. 将“返回节点”添加到您的退出点。(您不必这样做,但是如果您养成这个习惯,您的代码在长期运行中将更易读。)

您完成的Grab Actor图应如下所示:

总结一下这里发生的情况,当调用Grab Actor时,将bWantsToGrip设置为 true,然后我们检查是否已经拿着一个物体。如果是,我们不做任何其他操作。如果不是,我们检查是否在Event Tick上找到了一个我们可以拾取的对象。如果没有,就没有其他事情要做。如果找到了,我们通过其接口向其发送Pickup消息,其中包含对我们的Motion Controller组件的引用作为它应该附加到的对象,并将其存储为我们的Held Actor

释放一个角色

由于我们现在可以拾取一个角色,我们也希望能够再次放下它。现在让我们来做这个:

  1. 打开Release Actor函数。

  2. 从中删除Print String节点-我们已经完成了它。

  3. 按住Ctrl并将Held Actor拖动到图表上,右键单击它,并将其转换为已验证的获取。

  4. 在设置bWantsToGrip之后调用已验证的获取。

  5. 将返回节点连接到其 Is Not Valid 输出:

如果我们没有拿着任何东西,我们不需要做任何其他操作。如果我们拿着东西,我们应该确保演员仍然认为我们是拿着它的人(因为我们可能用另一只手抓住它),如果它仍然是我们的对象,就将其放下。

  1. Held Actor拖出一个连接器并获取其Root Component

  2. 在根组件上调用Get Attach Parent

  3. Get Attach Parent的“Return Value”拖出一个连接器并创建一个==测试。

  4. Motion Controller组件拖动到测试的另一个输入上。

  5. 使用此测试的结果创建一个Branch作为其条件:

  1. 从分支的 True 输出中,调用DropHeld Actor上。

  2. 按住Alt并将Held Actor拖动到图表上以创建一个 setter。

  3. 将其连接到Drop调用的执行输出和Branch节点的 False 输出,以便在任何情况下都清除该值:

您完成的图应如下所示:

简要回顾一下这里发生的情况,当调用Release Actor时,我们首先将bWantsToGrip设置为 false。然后,我们检查是否正在拿着任何东西。如果没有,就没有其他事情要做了。如果我们认为我们正在拿着某个东西,我们检查一下我们认为我们正在拿着的物体是否仍然将我们的动作控制器视为其父级,因为我们可能用另一只手抓住它。如果我们真的拿着这个物体,我们就放下它并清除Held Actor变量。如果事实证明我们不再拿着这个物体,我们清除Held Actor变量,这样我们就不再认为我们在拿着它了。

测试抓取和释放

让我们在地图中测试一下:

  1. 从编辑器的模式面板中,选择“放置|基本|立方体”,并将其拖入场景中。将其位置设置为 X=-2580,Y=310,Z=40,以便它位于玩家起始点附近。

  2. 从内容浏览器中选择BP_PickupCube,并将其放置在刚刚放置的立方体上。您可以使用End键将其放到下面的表面上。(X=-2600,Y=340,Z=100可能是一个不错的位置。)

  3. 按住 Alt 键并拖动更多的BP_PickupCubes并将它们堆叠在立方体上:

启动 VR 预览。走到立方体上的物体旁边,使用扳机来拾取、放下、扔掉和手到手移动它们。

还不错,但是这里有几个问题需要修复。

修复立方体碰撞

首先,最重要的是,它们与 VRPawn 的碰撞胶囊发生碰撞并将我们推开。我们最好修复一下:

  1. 打开BP_PickupCube蓝图并选择其Static Mesh Component

  2. 在其详细信息|碰撞下,将其碰撞预设从PhysicsActor更改为Custom

  3. 这个对象的个别碰撞响应通道现在可以编辑了。将 Pawn 的碰撞响应设置为Overlap而不是Block

这样,我们仍然可以检测到与 Pawn 的碰撞,如果我们对它们感兴趣的话,但它们不会阻止玩家四处移动。

让玩家知道何时可以拾取物品

其次,我们没有给玩家任何视觉提示,告诉他们他们可以拾取物品。让我们改进一下。

首先,让我们向我们的EGripState枚举器添加另一个状态:

  1. 打开项目的“蓝图”目录中的EGripState

  2. 在其枚举器列表下,点击“新建”以添加另一个条目。将其命名为CanGrab

  3. 关闭并保存它。

现在,我们需要告诉我们的动画蓝图该怎么做。

  1. 打开ABP_MannequinHand_Right动画蓝图并打开其“事件图表”。

  2. 在“事件蓝图更新动画”下,您会看到Grip State``Select节点已自动更新以反映我们添加的新的Can Grab枚举器。将其值设置为0.5

通过编译并在动画预览编辑器中更改 Grip State 来尝试一下。当 Grip State 设置为Can Grab时,手应该处于半开状态。

  1. 保存并关闭动画蓝图。

接下来,我们需要让BP_VRHand蓝图在检测到玩家可以抓取物体时将Grip State设置为Can Grab。让我们创建一个纯函数来确定我们的Grip State应该是什么。

  1. 打开BP_VRHand的“事件图表”并找到“事件 Tick”。

  2. 选择bWantsToGrip引用和与其连接的Select节点,并将它们折叠成一个函数。

  3. 将函数命名为DetermineGripState,将其类别设置为“Grabbing”,将其访问限定符设置为“Private”,将纯度设置为“True”:

  1. 打开DetermineGripState

  2. 按住 Ctrl 键并将Held Actor拖到图表中,并将其转换为已验证的获取。

  3. 将其连接到函数输入并从其 IsValid 输出添加一个新的Return Node

  4. 将此节点的返回值设置为Gripping

如果我们拿着一个物体,我们不会关心其他任何事情-我们只需要将其动画化到抓握状态。

  1. 在图表中添加一个“分支”节点。

  2. bWantsToGrip的值拖动到其条件中。

  3. 将其 True 分支连接到我们刚刚创建的Gripping“返回节点”。

  4. 按住 Ctrl 键并将AvailablePickupActor拖动到图表中,并将其转换为已验证的获取。

  5. 在其“合法”输出上添加另一个连接到“返回节点”,并将其返回值设置为Can Grab

  6. 在其“不合法”输出中添加另一个“返回节点”,其值为 Open:

让我们来测试一下。现在,当检测到可以抓取的物体时,您应该看到手的姿势发生变化。

添加触觉反馈

还有一件事情我们应该做的是,在玩家与物体接触时为手部添加一些反馈。这可能看起来像是一件小事,但实际上对于唤起存在感的过程非常重要。目前我们没有太多的方法来模拟物理感觉,但是任何与事件或动作配对的感觉都可以在很大程度上使虚拟世界感觉不那么“虚幻”而更加真实。

让我们学习如何为我们的控制器添加一点震动。

创建触觉反馈效果曲线

首先,我们需要创建要播放的触觉效果:

  1. 在项目的“蓝图”目录中右键单击,选择“创建高级资产”|“杂项”|“触觉反馈效果曲线”:

  1. 将其命名为FX_ControllerRumble

  2. 打开刚刚创建的触觉反馈效果曲线。

您会看到在触觉反馈效果|触觉详情下有两个曲线:频率和振幅。我们将在这里创建一个非常简单的效果,但是通过尝试这些曲线并找出如何创建令人信服的反馈效果是非常值得的。

  1. 右键单击频率曲线的时间轴附近的 0.0 时间,并选择“添加关键帧到无”。

  2. 将其时间和值设置修正为每个都为0.0

  1. 再次右键单击时间轴,添加另一个关键帧。将此关键帧的时间设置为0.5,值设置为1.0

  2. 在曲线上创建第三个关键帧,时间为1.0,值为0.0

  3. 为振幅曲线创建相同的三个关键帧:

您完成的曲线应该看起来像前面的截图所示。

  1. 保存并关闭新的触觉效果曲线。

按命令播放触觉效果

现在我们已经创建了一个触觉反馈效果曲线,让我们设置一个播放它的方法:

  1. 打开BP_VRHand的事件图表,右键单击。选择“添加事件”|“添加自定义事件”。将新事件命名为RumbleController

  2. 为此事件创建一个输入。将其命名为Intensity,并将其类型设置为Float

  3. 右键单击并创建一个“获取玩家控制器”节点。

  4. GetPlayerController拖动连接器并创建一个“播放触觉效果”节点。

  5. 选择刚刚创建的触觉效果。

  6. Hand变量拖动到 Hand 输入中。

  7. 将事件的强度输出拖动到比例输入中:

现在,每当我们接触到一个新的可拾取物体时,让我们调用这个触觉效果。

  1. 打开BP_VRHand的“查找最近的拾取物体”函数。

看到我们在Available Pickup Actor中设置为Nearest Pickup Actor中找到的值吗?让我们在放入新值时检测到,并在发生时触发效果。

  1. 右键单击Nearest Pickup Actor获取器,并将其转换为已验证的获取。

  2. 按住 Ctrl 键并将执行输入拖动到Set Available Pickup Actor上,然后将其放在Get Nearest Pickup Actor获取器的执行输入上。

  3. 从“最近的拾取物体”获取器的值拖动连接器,并创建一个“!=”(不等于)节点。

  4. 从变量列表中将对Available Pickup Actor的引用拖动到“不等于”节点的另一个输入中。

  5. 从其输出创建一个“分支”。

  6. Nearest Pickup Actor的 Is Valid 执行引脚拖动到Branch输入中。

  7. 从其 True 输出调用Rumble Controller并将其强度设置为0.8

  8. Rumble Controller的输出拖动到Available Pickup Actor的输入中。

  9. Nearest Pickup Actor的 Is Not Valid 输出拖动到Available Pickup Actor的 setter 中。

  10. Set Available Pickup Actor之后和Not Equal测试的False分支之后添加返回节点:

简要回顾一下这里发生的情况,一旦我们完成了对可能拾取的对象的迭代,我们需要检查是否找到了一个对象。如果没有找到,我们只需将Available Pickup Actor设置为 null 值,以便在先前包含值的情况下清除它。如果我们找到了一个可以拾取的对象,我们检查它是否与当前的Available Pickup Actor不同。如果是,我们在设置Available Pickup Actor为新值之前会使控制器震动。

进一步

我们可以进一步改进我们在这里所做的几种方法:

  • 首先,通过距离检测可抓取对象会给我们带来模糊的结果。它没有考虑到对象的大小。使用一个球体来代表我们的抓取手,并针对该球体进行重叠测试将给我们更准确的结果。如果您想重构此代码以使用该方法,VR 模板项目中包含一个很好的示例。

  • 其次,我们的触觉反馈效果感觉不够明显。它均匀地淡入淡出,并没有提供太多的物理感觉。通过编辑这些曲线以提供更锐利的攻击可以使效果更加令人信服。

总结

本章继续上一章的内容,让我们有机会开始拾取物体。我们学会了如何使用蓝图接口来使各种对象能够进行函数调用,以及如何检测我们可以拾取的演员并使用附件来拾取和放下它们。最后,我们还学会了如何创建触觉反馈效果,以指示玩家何时与可以拾取的对象接触。

正如我们在上一章的开头提到的,手的存在是 VR 中产生整体存在感的重要因素。在现实生活中,我们始终意识到自己的手,将它们带入虚拟世界也会让我们在空间中感到存在。此外,直接使用手来操纵物体的能力是我们在 VR 中可以做的关键事情之一,而在其他任何媒介中都无法做到。 (要了解这一点的一个例子,请查看EntroPi GamesVinyl Realityvinyl-reality.com/),然后想象一下尝试使用游戏手柄或键盘做同样的事情。)手在 VR 中非常重要,它们是 VR 的独特之处。在您的应用程序中花时间将它们处理正确。

在下一章中,我们将学习如何在 VR 中创建用户界面以显示信息,并使用户能够在 3D 空间中进行交互。

第七章:在 VR 中创建用户界面

在前一章中,我们学习了如何通过动作控制器创建虚拟手。这使得我们的用户不仅可以环顾四周并在其中移动,还可以开始与之互动。在本章中,我们将进一步学习如何创建传达信息并接受输入的用户界面(UI)。

您应该认真考虑您的应用程序是否真的需要图形用户界面。并不是所有应用程序都需要图形用户界面,虚拟界面元素可能会破坏沉浸感。在构建用户界面元素时,尝试找出如何将它们有意义地融入到世界中,使其看起来像是属于那里的一部分。也不要过于迷恋按钮。它们在 2D 用户界面设计中常用,因为它们与鼠标配合使用效果很好,但是 VR 手柄提供了更广泛的潜在操作方式。要超越按钮的限制。

我们为 VR 开发的大多数应用程序都需要某种形式的图形用户界面(GUI),但是 VR 中的用户界面提出了我们在平面屏幕上没有遇到的新挑战。大多数情况下,当我们构建平面屏幕用户界面时,我们可以简单地将 2D 用户界面元素叠加在我们的 3D 环境之上,使用 HUD 读取鼠标、游戏手柄或键盘输入来允许用户与之交互。但是在 VR 中这种方法行不通。

如果我们简单地在每只眼睛的视图上绘制一个 2D 界面,它的位置对于每只眼睛来说都是相同的。这样做的问题是,我们的立体视觉会将两只眼睛看到的相同物体解释为无限远。这意味着,当世界中的 3D 物体出现在屏幕上的 UI 后面时,这些物体将看起来比 UI 更近,即使 UI 是绘制在它们上面。这看起来很糟糕,几乎肯定会让用户感到不舒服。

解决方案是将用户界面元素融入到 3D 世界中,但仅仅在玩家面前创建一个 HUD 面板并投射到上面是不够的(我们将在本章后面讨论为什么)。无论如何,你都必须重新思考 VR 中的用户界面。将你所做的视为重新创建与之交互的真实世界对象,而不是重新创建平面屏幕世界的 2D 隐喻。

我们还需要重新思考在 3D 世界中如何与用户界面进行交互。在 VR 中,我们无法使用鼠标光标(对我们来说也不适用,因为它是一个 2D 输入设备),键盘命令也不是一个好主意,因为用户看不到键盘。我们需要新的方式来将输入传达到系统中。幸运的是,虚幻提供了一套强大的工具,可以很好地处理 VR 中的 3D 用户界面。

在本章中,我们将通过创建一个简单的 AI 控制的伴侣角色,并在其上显示当前 AI 状态的指示器,以及在玩家角色上创建一个控制界面,来介绍在 VR 中创建功能性 UI 所需的各种元素的过程。

具体来说,我们将涵盖以下主题:

  • 创建一个 AI 控制的角色并赋予其简单的行为

  • 使用虚幻运动图形(UMG)UI 设计师在 3D 空间中创建界面以显示信息

  • 将用户界面元素附加到世界中的对象上

  • 使用小部件交互组件与这些界面进行交互并影响世界中的对象

  • 向用户显示小部件交互组件

让我们开始吧!

入门

对于这个项目,我们将从上一章的项目开始,创建一个新的副本。在之前的章节中,我们已经探索了一些使用其他项目材料创建新项目的方法。简单地复制和重命名一个项目通常是最简单的方法,如果你正在对之前的项目所做的工作进行扩展,那么这种方法是合适的(如果你愿意,也可以继续使用本章的工作从之前的项目中继续工作)。

从现有项目创建一个新的虚幻项目

通过复制创建一个新项目时,实际上并不需要做很多事情。只需要简单地执行以下操作即可:

  • 复制旧项目目录。

  • 重命名新目录和.uproject文件。

  • 删除旧项目中生成的文件。

让我们使用我们在第五章中的项目作为本章工作的起点:

  1. 关闭虚幻编辑器,找到之前章节的虚幻项目的位置。

  2. 复制项目目录并给它一个新的名称。

  3. 在新目录中,重命名.uproject文件。你不需要将项目文件的名称与包含它的目录名称匹配,但这是一个好的做法。

  4. 从新项目目录中删除IntermediateSaved目录。当你打开新项目时,它们将被重新生成,而旧项目中残留的杂乱数据可能会引起问题。最好始终从干净的状态开始。

  5. 打开新的.uproject文件。你会看到刚刚删除的IntermediateSaved目录已经为新项目重新生成。项目应该会打开到上一章中设置的默认地图(LV_Soul_Slum_Mobile)。

  6. 点击工具栏的构建按钮以重新构建其光照。

通过启动 VR 预览来测试项目。一切应该与之前的项目一样正常工作。

正如我们之前提到的,从上一章的项目继续工作也是可以的。无论哪种方式,我们现在准备添加我们要控制的 AI 角色。

我们并不孤单-添加一个 AI 角色

从头开始创建一个 AI 控制的角色将使我们进入超出本书范围的领域,因此我们将重新使用第三人称模板中的标准玩家角色并改变其控制方式。

如果你已经有一个使用第三人称模板创建的项目,请打开它。如果没有,请创建一个:

  • 选择“文件 | 新建项目”,使用第三人称模板创建一个新的蓝图项目。可以将其他设置保留为默认值-它们不会影响我们正在做的任何事情。

迁移第三人称角色蓝图

无论是使用现有的第三人称模板项目还是创建一个新项目,我们现在要做的是迁移ThirdPersonCharacter蓝图:

  1. 在第三人称项目的内容浏览器中,导航到Content/ThirdPersonBP/Blueprints,并选择ThirdPersonCharacter蓝图。

  2. 右键单击并选择“资产操作 | 迁移”。将角色迁移到本章项目的Content目录中。

现在,我们可以关闭这个并返回到我们的工作项目。我们的内容迁移应该已经添加了一个新的ThirdPersonBP目录。

  1. 导航到Content/ThirdPersonBP/Blueprints,找到ThirdPersonCharacter蓝图。打开它。

清理第三人称角色蓝图

这里有一些我们不需要的东西,我们可以安全地清除:

  1. 首先,在事件图中选择所有内容并删除。我们不需要任何这些输入处理程序。

  2. 我们还不需要组件列表中的 FollowCamera 和 CameraBoom 项目,所以删除它们:

现在,我们有一个干净的角色,它将很好地完成我们需要它做的工作。

检查动画蓝图

尽管我们采取了捷径并迁移了我们的角色,但看一下它是如何工作的仍然不是一个坏主意。

选择角色的Mesh组件,并查看详细面板的动画部分。您会看到这个角色使用一个名为ThirdPerson_AnimBP的动画蓝图进行动画化。使用 Anim Class 属性旁边的放大镜导航到动画蓝图,然后打开它以查看内部内容:

讨论动画蓝图的深入内容超出了本书的范围,但是总的来说,您应该了解它们与受控手部一样,负责确定骨骼网格如何根据其动画的各种因素进行动画化。

您看到了一个简单的示例,其中动画蓝图驱动手部姿势。这个示例执行了类似的工作,但驱动了一个角色骨架。花点时间浏览一下这个蓝图,看看它是如何工作的,这不是一个坏主意。您可以在docs.unrealengine.com/en-us/Engine/Animation/AnimBlueprints找到更多文档。当您完成浏览后,可以随意关闭动画蓝图。我们不需要在这里做任何更改。

创建一个伙伴角色子类

由于我们将向该角色添加新的行为和组件,所以为我们创建一个新的角色蓝图并从这个蓝图派生出来是个好主意。

  1. 右键单击ThirdPersonCharacter蓝图并从上下文菜单中选择创建子蓝图类:

  1. 让我们将新类命名为BP_CompanionCharacter并将其移动到Content文件夹内的项目子目录中。

  2. 现在,我们可以将BP_CompanionCharacter的一个实例拖入关卡中:

将您的伙伴角色放置在导航网格覆盖的位置。之前,我们使用导航网格来允许我们指示地图上哪些区域是有效的传送目的地。现在,除此之外,我们还将使用它来实现其预期的目的。导航网格提供了地图可行走空间的简化模型,可以供 AI 控制的角色在其中找到路径。请记住,您可以使用P键显示和隐藏导航网格,以检查其覆盖范围。

为我们的伙伴角色添加跟随行为

让我们给角色一个简单的行为。我们让他跟随玩家:

  1. 打开BP_CompanionCharacter事件图,并找到或创建一个 Event Tick 节点。

  2. 在图表中右键单击并创建一个 Simple Move to Actor 节点。

  3. 创建一个 Get Controller 节点,并将其输出连接到 Simple Move to Actor 节点的 Controller 输入。

  4. 创建一个 Get Player Pawn 节点,并将其输出连接到 Simple Move to Actor 节点的 Goal 输入:

启动您的地图。我们的伙伴角色应该跑到您的位置(如果他没有,请验证他是否在导航网格上启动,并且他站立的导航网格部分可以访问您的 PlayerStart 位置)。

检查 AI 控制器

让我们花一点时间来讨论这里发生的事情:

  1. 关闭游戏会话,选择 Simple Move to Actor 节点,并按下F9键在那里设置一个断点

断点是一种调试工具,它指示蓝图解释器在达到您设置的点时暂停执行。在暂停状态下,您可以将鼠标悬停在变量和函数输出上,以查看它们包含的内容,并可以逐步执行代码以查看其执行方式。我们将在后面的章节中详细介绍使用断点和调试工具。

再次运行地图,但不需要戴上 VR 头盔-我们只想看看断点被触发时会发生什么:

  1. 当执行停在断点处时,将鼠标悬停在“获取控制器”节点的输出上。你会看到这个角色当前由一个自动为其创建的 AI 控制器控制。

在执行命令之前,你的关卡中的任何角色或者角色必须被一个控制器控制。作为玩家控制的角色或者角色是由一个玩家控制器控制的。预期自主行为的角色需要被一个 AI 控制器控制。

  1. 如果 Simple Move to Actor 节点已经取消选择,请再次选择它,并按下 F9 清除断点。

  2. 点击工具栏上的“恢复”按钮返回正常执行。

角色应该跑到你的位置。

在蓝图中设置断点是调试它们和查看它们如何运行的有价值的方式。如果你正在使用另一个开发者编写的蓝图,设置一个断点并逐步执行可以帮助你弄清楚它的工作原理。你可以通过按下F9来设置和清除断点,并通过使用F10来逐步执行。F11Alt + Shift + F11允许你在蓝图中进入和退出子方法。你可以通过将鼠标悬停在输入和输出连接器上来查看当前设置在蓝图中的值。

如果我们查看BP_CompanionCharacter类的Details | Pawn,我们可以看到 Auto Possess AI 被设置为 Placed in World,这意味着如果这个角色被放置在世界中,指定的 AI 控制器将自动控制这个角色。这里的其他选项允许我们指定 AI 控制器在角色生成时应该控制角色,或者根本不自动控制。AI Controller Class 指定了哪个 AI 控制器类将控制这个角色。如果需要的话,我们可以在这里选择一个新的 AI 控制器类。在我们的情况下,我们不需要这样做,因为默认的控制器可以做我们需要它做的一切:

与动画蓝图的深度讨论一样,AI 控制器和决策树的深入讨论超出了本书的范围,但如果你想进一步了解,可以在docs.unrealengine.com/en-us/Gameplay/AI上查阅文档是值得的。

花一些时间来研究这些元素是值得的。如果你正在开发涉及可见非玩家角色的应用程序,学习动画蓝图和 AI 控制器的时间绝对是值得的。

改进伙伴的跟随行为

现在我们让角色跟随我们,让我们改进它的行为。它倾向于有点拥挤,如果我们的伙伴只在我们离他一定距离时尝试跟随我们,情况会有所改善。

首先,为了组织起来,我们应该将我们的移动行为捆绑到一个函数中:

  1. 选择 Simple Move to Actor 节点和 Get Controller 和 Get Player Pawn 节点,并将它们连接到它。

  2. 右键单击并将它们折叠到名为FollowPlayer的函数中。

现在,让我们改进它的工作方式:

  1. 打开新的函数。

  2. 从 GetPlayerPawn 拖动一个输出,并选择 Promote to local variable。将新变量命名为 LocalPlayerPawn。

在函数中使用局部变量,每当你访问一个需要花费时间重新收集的信息时。由于我们知道在这个函数中我们将需要多次使用玩家角色,所以获取它一次并保存值比每次需要时重新获取它要快。

  1. 将自动为您创建的 setter 连接到函数输入。

  2. 从 Local Player Pawn 节点的输出创建一个 Get Squared Distance To 节点。

  3. 右键单击,选择 Get a reference to self,并将 Self 输入到 Get Squared Distance To 节点的 Other Actor 输入中:

  1. 创建一个名为FollowDistance的浮点变量,编译并将其值设置为320.0。(一旦行为运行起来,可以随时调整该值。)

  2. FollowDistance进行平方(记住平方节点将在图表中显示为²),并测试 Get Squared Distance To 的结果是否大于跟随距离的平方。从结果创建一个分支节点:

回想一下,我们之前提到过计算平方根是昂贵的,所以当你只是比较距离但不关心实际距离时,使用平方距离代替。

当我们距离伴侣角色超过跟随距离时,该分支节点将返回 True,而在该距离内时返回 False。

  1. 将分支节点的 True 输出连接到 Simple Move To Actor 节点。

  2. 将 False 输出连接到Return Node,因为如果我们在跟随距离内,我们不需要做任何事情。

  3. 获取一个LocalPlayerPawn的实例,并将其插入 Simple Move to Actor 节点的 Goal 输入。

  4. Get Controller仍然连接到你的 Simple Move to Actor 节点的 Controller 输入。

  5. 在 Simple Move to Actor 节点的退出处添加一个Return Node

试一下。伴侣角色现在应该在你离开他超过 320 个单位之前等待再次跟随你:

还不错。这是一个非常简单的行为,但是这是一个好的开始。

对于任何有意义的复杂 AI 行为或需要由许多角色同时执行的行为,最好使用行为树来实现,而不是使用蓝图的 tick 操作。行为树允许我们以清晰、可读的方式构建非常复杂的行为,并且比 tick 事件上的简单蓝图操作运行得更高效。我们在这里使用蓝图构建了角色的行为,以避免走得太远,但是行为树实际上是一个更好的结构来使用的。

现在我们的伴侣角色正在执行行为,是时候进入本章的真正内容了,即向世界添加 UI 元素。

向伴侣角色添加一个 UI 指示器

现在我们的角色正在世界中移动,我们将给它添加另一个行为状态,并允许玩家指示它等待。

然而,在我们创建这个新状态之前,我们首先要创建一个简单的 UI 元素来指示伴侣角色的当前状态。我们将首先构建它作为一个占位符,因为我们还没有创建它的新状态,然后一旦我们创建了它,我们将更新它以反映真实的基础数据。

使用 UMG 创建一个 UI 小部件

Unreal 提供了一个强大的工具来构建 UI 元素。UMG 允许开发人员在可视化布局工具上布置 UI 元素,并将蓝图行为直接与布局中的对象关联起来。我们称之为 UI 元素小部件。让我们学习如何创建它们:

  1. 在项目的Content目录中,右键创建一个新资产。选择 UI | Widget Blueprint:

  1. 将其命名为WBP_CompanionIndicator并打开它。

你将看到 UMG UI Designer。

Unreal 提供了两个用于创建 UI 的工具集。原始的称为Slate,只能在本机 C++中使用。编辑器本身的大部分是使用 Slate 编写的,一些较旧的游戏示例(如 ShooterGame)也使用 Slate 实现其界面。UMG提供了一种更灵活和用户友好的方法来创建虚幻引擎中的 UI 对象,这是我们将用来构建界面元素的方法。

UMG 是一个非常强大和深入的系统。您可以使用它创建几乎任何类型的界面元素。在这个例子中,我们无法涵盖 UMG 的所有功能,所以当您准备进一步时,我们鼓励您探索文档:docs.unrealengine.com/en-us/Engine/UMG

首先,请注意 UMG 设计器由两个选项卡组成:设计师和图形。设计师选项卡是您的布局工具。图形选项卡与虚幻引擎中的其他上下文一样,用于指定小部件的行为。

让我们先设置一个简单的用户界面,这样我们就可以把所有的部分放到正确的位置上:

  1. 在设计师窗口的右上角,找到 Fill Screen 下拉菜单,并将其设置为 Custom。

在平面屏幕应用程序中,设计一个可以根据屏幕自动缩放的 UI 小部件非常常见,但在 VR 中这不是可行的方法,因为我们的 UI 元素需要存在于 3D 空间中。将此值设置为 Custom 允许我们明确指定 UI 小部件的尺寸。

  1. 将自定义尺寸设置为宽度=320,高度=100(您也可以使用小部件轮廓右下角的调整工具来调整):

  1. 从 Palette 中获取一个 Common | Text 对象,并将其拖放到 Canvas Panel 的层次结构面板中作为子对象。

您可以通过将元素直接拖放到设计师工作区或将其拖放到层次结构面板中来向画布添加元素。

让我们将这个文本对象居中在我们的面板中。

  1. 如果尚未选择,请在层次结构中选择Text对象。

  2. 将其名称设置为txt_StateIndicator

您不必为小部件命名,但如果您创建了一个复杂的 UI,并且所有内容都被命名为TextBlock_128327,那么在大纲中找到您要查找的内容将会很困难。当您创建时,给您的东西起一个合理的名称是一个好习惯。

  1. 从锚点下拉菜单中选择居中的锚点并单击它:

  1. 将其 Position X 和 Position Y 属性设置为 0.0。您将看到文本对象移动,使其左上角与中心锚点对齐。

  2. 将其对齐方式设置为 X=0.5,Y=0.5。您将看到文本对象移动,使其中心与中心锚点对齐。

  3. 将其 Size 设置为 Content 为 true。

  4. 将其对齐方式设置为居中对齐文本。

  5. 将其文本设置为“Following”(我们稍后会动态设置)。

锚点是使用 UMG 构建 UI 时必须掌握的重要概念。当一个对象放置在画布面板上时,它的位置被认为是相对于其锚点的。对于不改变大小的 UI 画布,这可能并不重要 - 您可以简单地将所有内容锚定在左上角,但是一旦您开始改变 UI 的大小,锚点就很重要了。最好习惯于使用适当的锚点来确定对象的出现位置。这样您将节省很多重新工作的时间。

对象的对齐方式确定其认为原点在哪里,范围从(0,0)到(1,1),因此对齐方式为(0,0)将原点放在对象的左上角,而对齐方式为(1,1)将其放在右下角。 (0.5,0.5)将原点居中于对象。

在选择锚点时,您可以使用 Ctrl +单击和 Shift +单击来自动设置对象的位置和对齐值。

请查看以下屏幕截图:

因此,简要回顾一下,在将对象放置在 UMG 画布上时,选择一个锚点,确定对象在布局板上将位置(0,0)视为何处。这可能因对象而异,这是一个强大的功能。接下来,确定对象在其自身原点上应该考虑其自身原点的位置,使用其对齐设置。最后,设置其位置。

在 UMG 中设计界面时,如果您将自己的工作视为在面板上设置对象如何排列的规则,而不是明确设置其位置,那么您将更容易。 UMG 旨在使创建与不同小部件和屏幕尺寸正确缩放的界面,并对驱动它们的数据动态响应变得容易。它做得很好,但对于新用户来说可能会感到困惑,直到您将思维方式从静态布局转变为动态规则系统。

我们暂时完成了这个对象,所以我们可以关闭它。

将 UI 小部件添加到角色

现在我们已经创建了指示器小部件,是时候将其添加到伴侣角色中了:

  1. 打开BP_CompanionCharacter,并从其组件面板中选择+添加组件| UI | Widget。

  2. 将新组件命名为“指示器小部件”。

  3. 在其详细信息| UI 下,将其小部件类设置为我们刚刚创建的WBP_CompanionIndicator类。

  4. 将其绘制大小设置为与我们为小部件布局设置的自定义大小相匹配:(X=320,Y=100)。

  5. 如果您还没有在视口中,请跳转到视口。

现在,您应该看到您的小部件与角色一起显示,但它太大了,而且位置不正确。

在以 3D 空间显示的 UI 小部件中,如果以构建时的 100%比例显示,它们往往会显得模糊。最好的做法是将小部件构建得比实际需要的尺寸大,然后在将其附加到角色时缩小它。这将使其以比构建较小并以全尺寸显示的小部件更高的分辨率显示。

  1. 将其位置设置为(X=0.0,Y=0.0,Z=100.0)。

  2. 将其比例设置为(X=0.3,Y=0.3,Z=0.3):

指示器小部件附加到角色的胶囊组件上,并将随角色移动。

让我们在关卡中进行测试。不错,但有一个问题-指示器面向角色的方向,因此如果伴侣角色没有面向您,很难或不可能阅读。我们可以解决这个问题。

将指示器小部件定位到玩家

我们将创建一个函数,将指示器定位到相机。

  1. 在我的蓝图|函数下,创建一个名为AlignUI的新函数。

  2. 将其类别设置为 UI,将其访问说明符设置为 Private(设置类别和访问说明符不是必需的,但这是一个非常好的实践。当您的项目变得更大时,这将使您的生活更轻松)。

  3. 打开它。

实现 Align UI 函数

在此函数的主体中,我们将找到玩家相机的位置,并将指示器小部件定位到面向相机:

  1. 从组件列表中将指示器小部件拖动到函数图中。

  2. 在指示器小部件上调用 SetWorldRotation,并将函数的执行输入连接到此调用。

  3. 从指示器小部件中拖动另一个连接器,并在其上调用 GetWorldLocation。

  4. 创建一个获取玩家相机管理器节点,并在结果上调用 GetActorLocation。

  5. 创建一个查找朝向旋转节点,并将指示器小部件的位置馈入 Start 输入,将相机管理器节点的位置馈入其 Target。

  6. 将其结果馈入SetWorldRotation函数的 New Rotation 输入。

  7. 给函数一个Return Node

通过获取玩家摄像机管理器的位置,我们已经得到了玩家从场景中观察的位置。Find Look at Rotation方法返回一个旋转器,其前向矢量从起始位置(小部件所在位置)指向目标位置(相机所在位置)。使用此旋转器调用SetWorldRotation会使 UI 小部件面向相机。

从 Tick 事件中调用 Align UI

现在让我们在 Event Tick 上调用AlignUI函数:

  1. 跳回到您的事件图。

  2. 从 Event Tick 拖动一个新的执行线,并在释放时输入seq。从结果列表中选择 Sequence 并创建一个 Sequence 节点。

Sequence 节点将自动插入到 Event Tick 和之前连接到它的 Follow Player 调用之间:

  1. 从 Sequence 节点的 Then 1 输出调用Align UI

在关卡中试一试。无论伴侣棋子朝向何处,UI 指示器现在都应该面向相机:

很好。我们为伴侣棋子创建了一个简单的 UI 元素。当然,由于棋子只有一个状态,它还没有做太多事情,但我们现在准备解决这个问题。

向伴侣棋子添加一个新的 AI 状态

首先,让我们给伴侣棋子一种知道自己处于什么状态的方法。这些信息最好存储在一个枚举中:

  1. 在内容浏览器中,无论您将BP_CompanionCharacter保存在何处,右键单击以添加一个新对象,并选择蓝图|枚举。将其命名为ECompanionState

  2. 打开它并向枚举器添加两个项目,分别命名为 Following 和 Waiting,如下所示:

  1. 保存并关闭新的枚举器。

实现一个简单的 AI 状态

现在,我们已经创建了一个枚举器来命名角色的 AI 状态,让我们将我们已经创建的行为定义为角色的Following状态:

  1. 打开BP_CompanionCharacter并创建一个新的变量。将其名称设置为CompanionState,类型设置为我们刚刚创建的ECompanionState枚举。

  2. 在事件图中找到 Event Tick。

  3. 按住Ctrl并将CompanionState变量拖动到图表中。

  4. 从其输出拖动一个连接器,并在搜索框中输入sw以将搜索结果过滤为Switch on ECompanionState。添加节点。

  5. 按住Ctrl并拖动执行输入,将其从该节点的输入移动到新的 switch 语句的执行输入。

  6. 将 switch 语句的 Following 输出连接到您的Follow Player调用:

现在,当您的伴侣棋子的Companion State设置为Following时,它将执行跟随行为,但如果该状态设置为Waiting,则不会执行。

使用 UI 指示器指示 AI 状态

在继续创建角色的下一个 AI 状态之前,让我们更新我们的 UI 元素以反映角色所处的状态。当我们开始更改它时,我们很快就会需要它。

由于我们希望指示器 UI 显示与其附加的棋子相关的信息,我们需要告诉它关于该棋子的信息:

  1. 打开WBP_CompanionIndicator并从设计面板或层次结构选项卡中选择txt_StateIndicator

  2. 将其 Is Variable 属性设置为 true:

通过将txt_StateIndicator设置为变量,我们可以在此小部件的事件图中访问该对象,因此我们可以获取对它的引用并更改其值。

  1. 切换到图表选项卡。

  2. 创建一个新的函数并命名为UpdateDisplayedState

  3. 向函数添加一个名为NewState的输入,并将其类型设置为ECompanionState

  4. 打开该函数。

  5. txt_StateIndicator现在应该在您的变量列表中可见。按住Ctrl并将其拖动到函数的图表中。

  6. txt_StateIndicator拖动一个连接器,并调用SetText

  7. 从 NewState 输入拖动连接器,并在搜索框中键入se。应该会出现一个 Select 节点。将其放置在图表中如下所示:

您新创建的 Select 节点将自动填充每个ECompanionState枚举值的选项。Select 语句可用于选择各种数据类型。要设置其类型,只需将其连接到任何其他函数或变量的输入或输出,它将采用您连接到它的任何内容的类型。

  1. Select语句的返回值连接到 Set Text 节点的 In Text 输入。

您会发现Select语句现在已经采用了文本数据类型,您现在可以为 Following 和 Waiting 选项输入值。

  1. 使用适当状态的名称填充选择语句的文本输入。

  2. 将函数的执行输入与 SetText 节点连接起来:

现在,每当我们在此 UI 元素上调用Update Displayed State时,它将根据我们在新提供的状态的Select语句中输入的内容更新显示的文本。

您在此示例中以及之前看到了如何使用枚举器使用 switch 语句和 select 语句。这些是有价值的技术,值得记住,因为它们易于阅读,并且如果您向枚举器添加或删除值,它们将自动更新。枚举器、switch 语句和 select 语句是您的朋友。

值得注意的是,我们还可以通过另一种方法更新此 UI,这是一种常见的教学方法。我们可以将拥有此小部件的角色的引用存储在变量中,然后使用 Bind 方法设置文本元素的实时更新:

这是一个讨论 UI 开发中几个重要考虑因素的好机会,并解释为什么在这种情况下我们没有使用 Bind。

使用事件进行更新,而不是轮询。

首先,Bind 方法会在每次 UI 更新时更新。对于连续变化的值,这是您想要的,但对于像角色的 AI 状态这样只在偶尔变化,且仅在执行更改它的操作时才变化的值,每次都检查是否需要显示新值是很浪费的。尽可能地,您应该在只有在您知道要更新的值时才更新 UI,而不是让 UI 轮询底层数据以查看其显示的内容是否仍然准确。如果您构建了一个具有许多不同元素的界面,并且每个元素都在每一帧更新,那么这将真正开始变得重要。在 UI 中考虑效率会带来回报。

注意避免循环引用

我们要小心的另一个原因有点微妙,但很重要。如果我们将对小部件蓝图的 pawn 的引用存储在小部件蓝图上,并同时将对小部件蓝图的引用存储在 pawn 上,那么我们就引入了可能的循环引用(有时也称为循环依赖):

循环引用:类 A 在 B 构建之前无法编译,但类 B 在 A 构建之前无法编译

循环引用是指一个类在构建之前需要了解另一个类,但是那个类在构建之前需要了解第一个类。这是一种糟糕的情况,可能会导致非常难以找到的错误。

在小部件蓝图和角色之间存在循环引用的情况下,小部件蓝图可能无法正确编译,因为它需要先编译角色,但是角色可能无法正确编译,因为它需要先编译小部件蓝图(我们说“可能不会”是因为许多其他因素可能会影响对象构建的顺序,因此有时可能会工作。您可能不会立即意识到自己创建了循环引用,因为在一段时间内可能会工作,然后在更改某些看似无关的东西时停止工作)。您不需要对此过于担心。虚幻引擎的构建系统非常擅长确定构建对象的正确顺序,但是如果您尝试保持引用的单向性,您将避免遇到非常具有挑战性的错误。

使用我们设置的事件驱动结构,小部件蓝图不需要了解角色的任何信息。只有角色需要了解小部件蓝图,因此编译器可以轻松确定在构建另一个对象之前需要构建哪个对象,从而避免循环引用。

确保在状态更改时更新 UI

现在,因为我们选择使用事件驱动模型而不是轮询模型来驱动我们的指示器 UI,我们必须确保每当BP_CompanionCharacter类的Companion State发生变化时,UI 都会更新。

为了做到这一点,我们希望将变量设置为私有,并强制任何其他更改此值的对象使用事件或函数调用来更改它。通过强制外部对象使用函数调用来更改此值,我们可以确保在函数或事件的实现中包含任何其他需要在该值更改时发生的操作。因为我们将变量设置为私有,所以我们阻止任何其他人在不调用此函数的情况下更改它。

这是软件开发中的一种常见做法,也是一个很好的内化。如果有可能需要根据变量的值执行操作,请不要让外部对象直接更改它。将变量设置为私有,并只允许其他对象通过公共函数调用来更改它。如果您养成这样的习惯,当项目变得庞大时,将会节省很多麻烦。

让我们创建一个函数来处理设置伴侣状态,并将变量设置为私有,以便开发人员在想要更改 AI 状态时被迫使用它:

  1. 选择BP_CompanionCharacter类的Companion State变量,并在其详细信息中将其私有标志设置为 true。

  2. 在事件图中,创建一个新的自定义事件,并将其命名为SetNewCompanionState

  3. 向此事件添加一个输入。将其命名为NewState,并将其类型设置为ECompanionState

  4. 按住Alt并将CompanionState设置器拖动到图表上,并将其执行和新值连接到新事件:

现在我们需要告诉指示器小部件状态已经改变。

  1. 将对IndicatorWidget组件的引用拖动到图表上。

  2. IndicatorWidget引用上调用Get User Widget Object(记住IndicatorWidget不是对小部件本身的引用,而是对持有它的组件的引用)。

  3. Get User Widget Object组件的返回值转换为WBP_CompanionIndicator

  4. 在转换结果上调用Update Displayed State

现在,因为Companion State是私有的,只能通过调用SetNewCompanionState来更改它,并且我们可以确保每当发生更改时,UI 指示器将被更新。

添加一个交互式 UI

现在是时候为自己提供一种改变伴侣角色状态的方法了。为此,我们将向玩家角色添加一个小部件组件,以及一个我们可以用来与其交互的小部件交互组件:

  1. 在内容浏览器中,找到BP_VRPawn(我们的玩家角色)的位置。

  2. 在相同的目录中,创建一个 UI | Widget Blueprint,并将其命名为WBP_CompanionController

  3. 保存并打开它。

  4. 在其设计窗口中,将Fill Screen更改为Custom,就像我们之前的小部件一样。

  5. 将其大小设置为 Width=300,Height=300。

  6. 从 Palette 中,选择 Panel | Vertical Box,并将其作为 Canvas Panel 的子项拖放到层次面板中:

  1. 通过选择右下角的选项(除了管理放置规则外,锚点还可以管理拉伸规则),将其锚定填充整个面板:

  1. 将其 Offset Left,Offset Top,Offset Right 和 Offset Bottom 设置为0.0

  2. 从 Palette 中,选择 Common | Button,并将其拖放到 Vertical Box 中。将其命名为btn_Follow

  3. 将另一个按钮拖放到同一个 Vertical Box 中,并将其命名为btn_Wait

  1. 将一个 Common | Text 小部件拖放到btn_Follow上。将其文本设置为Follow

  2. 将另一个 Common | Text 小部件拖放到btn_Wait上,并将其文本设置为Wait

您可能已经注意到,我们在创建按钮时给它们起了有意义的名称,但我们没有费心为文本块重新命名。原因是这些按钮是变量,我们将在小部件蓝图的图表中引用它们,而文本标签不会在其他任何地方引用,因此它们的名称并不重要。在选择要明确命名的项目时,您可以根据自己的判断进行选择,但通常,您的规则应该是,如果您将在其他任何地方引用该对象,则应该有一个有意义的名称。您不希望在数月后返回到小部件蓝图,发现图表中引用了 Button376 的一片引用。

我们的按钮非常小,并且在小部件上放置得不好。让我们进行一些布局工作来修复这个问题。

  1. 在层次面板上右键单击btn_Follow,然后选择 Wrap With... | Size Box。

  2. 在层次面板中选择刚刚出现的 Size Box,并将其 Height Override 设置为 80.0:

Size Box用于设置 UMG 小部件的特定大小。如果不使用 Size Box,小部件将根据其规则自动缩放。使用 Size Box 包装它可以允许您覆盖这些规则并显式设置选定的尺寸,同时仍然允许其余部分自动缩放。

  1. 使用 Size Box 包装btn_Wait,并将其 Height Override 设置为 80.0。

现在,让我们在面板上垂直居中这些按钮。我们将通过添加间隔器来实现这一点。

  1. 从 Palette 中,将一个 Primitive | Spacer 拖放到层次面板中的 Vertical Box 上。将其放置在围绕btn_Follow的 Size Box 之前。

  2. 将其大小设置为Fill

  3. 在 Size Box 围绕btn_Wait之后,再次将一个 Spacer 拖放到 Vertical Box 中,并将其大小设置为 Fill:

让我们再添加一个间隔器来稍微分隔一下按钮。

  1. 在 Size Box 围绕btn_Wait之前,再次将一个 Spacer 拖放到层次面板上。将其大小保持为 Auto,并将其 Padding 设置为 4.0。

在这里,我们看到了使用间隔器告诉布局如何处理未被其他小部件占用的空间的示例,还可以强制在小部件之间添加一些间隔。通过在按钮之前和之后放置 Fill 间隔器,我们使它们在垂直框中居中,并通过在按钮之间放置 Auto 间隔器,我们将它们分隔了一个固定的距离。

调整按钮颜色

这些默认按钮颜色在我们相当暗的场景中看起来太亮,无法阅读。我们可以通过调整其背景颜色属性来解决这个问题:

  1. 选择btn_Follow,点击其 Details | Appearance | Background Color 的颜色样本。

  2. 在结果颜色选择器的 HSV 输入中,将其 Value 设置为 0.05。

  3. 对于btn_Wait也执行相同的操作:

这将使按钮的背景变暗,以便我们可以在环境的照明下清楚地阅读它。

为我们的按钮添加事件处理程序

现在,让我们在按钮被点击时执行一些操作:

  1. 选择 btn_Follow,并在其 Details | Events 中,点击 On Clicked 事件的+按钮:

您将进入小部件的事件图表,其中创建了一个名为 On Clicked (btn_Follow)的新事件。

  1. 在图表中创建一个 Get All Actors of Class 节点,并将其 Actor Class 设置为 BP_CompanionCharacter。

  2. 从其 Out Actors 数组中拖动一个连接器,并创建一个 ForEachLoop。

  3. 从 ForEachLoop 的 Array Element 输出拖动一个连接器,并调用我们在 BP_CompanionCharacter 上创建的 Set New Companion State 事件。将状态设置为 Following:

让我们对 btn_Wait 做同样的事情。

  1. 再次从 Designer 选项卡中选择 btn_Wait,并为其创建一个 On Clicked 事件。

  2. 选择与 On Clicked (btn_Follow)事件连接的节点,并按下 Ctrl + W 进行复制。

  3. 将我们设置的伴侣状态更改为 Waiting。

将 UI 元素附加到玩家角色

现在,就像我们对伴侣角色的顶部指示器所做的那样,我们需要将此 UI 放置在世界中的某个位置。

对于习惯于设计平面应用程序的人来说,自然的反应是遵循他们已经了解的设计原则,并创建一些在头戴式显示器中显示的 HUD。这不是一个好主意。

首先,您附加到头戴式显示器的任何 UI 都会附加到玩家的头部。当他们转动头部看它时,它只会继续移动。这很快就会变得无聊,并且可能会引起一些用户的晕动病。这个问题的复杂性在于 VR 头戴式显示器的菲涅耳透镜在边缘处的清晰度要比中心处的清晰度低得多,因此玩家视野边缘的 UI 元素将很难阅读。最后,我们面临的问题是没有简单的方法与我们额头上的 UI 元素进行交互。

更好的解决方案是将 UI 附加到玩家可以控制的东西上,比如他们的手腕。现在让我们这样做:

  1. 打开 BP_VRPawn,并在其组件列表中找到 Hand_L。

  2. 将一个小部件组件作为 Hand_L 的子级。将其命名为 CompanionController。

  3. 将 WBP_CompanionController 设置为小部件的 Widget Class。

  4. 将其绘制大小设置为(X=300,Y=300),以与创建时的大小匹配。

现在让我们将其附加。

  1. 找到您的 BP_VRPawn 玩家的 BeginPlay 事件。

  2. 从 BeginPlay 拖动一个新的连接器,并创建一个 Sequence 节点。我们的 Set Tracking Origin 调用应自动连接到 Sequence 节点的 Then 0 输出。

  3. 将刚刚添加到角色中的 CompanionController 小部件的引用拖动到图表中。

  4. 从它拖动一个连接器并创建一个 Attach to Component 节点。

请记住,此节点有两个变体:目标是 Actor 和目标是 Scene Component。选择与场景组件一起使用的节点。

  1. 从 Sequence 节点的 Then 1 输出中拖动一个执行线到 Attach to Component 节点的执行输入。

我们也可以简单地从 Set Tracking Origin 输出拖动一个连接器到 GetHand_L 调用,但是将不相关的操作保持在单独的执行线上是更好的做法,这样更容易看出真正属于一起的内容。通过将 Set Tracking Origin 放在一个序列输出上,将 GetHand_L 调用放在另一个序列输出上,我们向读者清楚地表明这是两个独立的任务。

  1. 拖出我们之前创建的Get Hand Mesh for Hand方法的一个实例(如果您想为左撇子玩家设置,将其 Hand 值更改为 Right;否则保持默认的 Left)。

  2. 将结果手部网格输入到 AttachToComponent 节点的 Parent 输入中:

让我们运行它。它很大,但还没有正确对齐,但它会随着我们的左手移动。

  1. CompanionController拖动另一个连接器,并在其上调用Set Relative Transform

  2. 右键单击 New Transform 输入并拆分结构引脚。

  3. 输入以下值:

  • 新的变换位置:(X=0.0,Y=-10.0,Z=0.0)

  • 新的变换旋转:(X=0.0,Y=0.0,Z=90.0)

  • 新的变换比例:(X=-0.05,Y=0.05,Z=0.05)

请注意,我们在这里否定了比例的 X 值。如果您还记得,我们通过反转其比例来翻转了左手网格。由于我们要附加到翻转的网格,我们在这里需要否定比例,否则我们的小部件将显示为镜像(如果我们将其附加到右手,则将比例的 X 值设置为正 0.05,并将旋转的 Z 值设置为正 90.0)。

再次运行它,我们会看到手腕菜单现在与我们的手腕更好地对齐了。

接下来的挑战是:我们如何按下其中一个按钮?

使用小部件交互组件

虚拟现实中的用户界面存在一个重大问题:我们如何允许用户与其进行交互?早期的解决方案通常使用凝视控制。用户通过凝视固定时间来按下按钮。是的,它就像听起来的那样笨拙。幸运的是,随着手部控制的出现,我们不再需要以这种方式进行操作。

在虚幻引擎中,我们最常使用小部件交互组件与 VR 中的 UI 元素进行交互,它在场景中充当指针,并且在与 UMG 小部件一起使用时可以模拟鼠标交互。

让我们在右手上添加一个:

  1. 打开BP_VRPawn,并将 Widget Interaction 组件添加到其组件列表中(默认名称即可)。

  2. 在其详细信息面板中,将其 Show Debug 标志设置为True

  3. 在我们的事件图中,找到Begin Play事件上的 Sequence 节点,并使用 Add pin 按钮添加一个新的输出:

  1. 将对我们的Widget Interaction组件的引用拖到图表上。

  2. Widget Interaction引用中拖动一个连接器,并创建一个“Attach To Component (Scene Component)”节点,将Widget Interaction作为其目标。

  3. Get Hand Mesh for Hand函数调用拖到图表上,并将其 Hand 属性设置为 Right(如果您将 UI 附加到右手,则设置为 Left)。

  4. 将其 Hand Mesh 输出馈入“Attach To Component”节点的 Parent 输入:

现在,我们将控制器 UI 附加到左手,将小部件交互组件附加到右手。

现在,让我们测试一下:

很好。小部件交互组件的默认放置和对齐效果不错。如果我们想要调整它,可以使用Set Relative Transform调用,但对于我们在这里要做的事情来说,这样就可以了。

设置我们附加到另一个对象的对象的放置的另一种方法是在目标对象的骨架上放置一个插座。如果您向骨架添加插座,只需将其名称放在“Attach to Component”节点的“Socket Name”属性中。为了保持主题的连贯性,我们将坚持使用简单的“Set Relative Transform”调用,但如果您想探索使用插座,可以参考docs.unrealengine.com/en-us/Engine/Content/Types/SkeletalMeshes/Sockets上的说明。

既然我们已经将小部件交互组件连接到手上,我们准备通过它传递输入。

通过小部件交互组件发送输入

首先,我们需要选择什么输入来驱动我们的小部件交互。由于我们只使用扳机来抓取对象,所以将我们的小部件交互添加到这些相同的输入中应该可以正常工作:

  1. BP_VRPawn玩家的事件图中找到InputAction_GrabLeftGrabRight事件处理程序。

  2. 将对Widget Interaction组件的引用拖动到图表中。

  3. Widget Interaction组件拖动一个连接,并从连接中调用Press Pointer Key。将其键下拉菜单设置为Left Mouse Button

  4. Widget Interaction拖动另一个连接,并调用Release Pointer Key。将此键下拉菜单设置为Left Mouse Button

  5. 如果您将Widget Interaction组件附加到右手,请在InputAction_GrabRight组件的 Pressed 事件链的末尾调用Press Pointer Key,在Grab Actor调用之后调用它(如果交互组件在左手上,请改为从GrabLeft调用)。

  6. InputAction_GrabRight组件的 Released 链中调用Release Pointer Key,在Release Actor调用之后:

我们在这里所做的是告诉小部件交互组件,让它与小部件通信,就像用户将鼠标指针移动到上面并按下左键一样。这是一个强大而灵活的系统 - 您可以重新创建几乎任何输入事件并通过交互组件传递它。

让我们来测试一下。现在,您应该能够将小部件交互组件对准手腕控制器并按下扳机以激活按钮。尝试在关卡中四处奔跑,并在跟随和等待状态之间切换您的伴侣。

为我们的交互组件创建一个更好的指针

在结束之前,我们应该改进一下小部件交互组件上那个显眼的调试光束。让我们花点时间用更好看的东西来替换它。

  1. BP_VRPawn中,选择Widget Interaction组件并关闭其 Show Debug 标志。

  2. 在组件面板中,将一个静态网格组件添加为WidgetInteraction的子组件。将其命名为InteractionBeam

  3. 将其静态网格属性设置为/Engine/BasicShapes/Cylinder

  4. 将其位置设置为(X=50.0,Y=0.0,Z=0.0)。

  5. 将其旋转设置为(Roll=0.0,Pitch=-90.0,Yaw=0.0)。请记住,Pitch在 UI 中映射到 Y。

  6. 将其比例设置为(X=0.005,Y=0.005,Z=1.0)

  7. 将其碰撞|可以踩上的角色设置为No,将其碰撞预设设置为NoCollision

如果您在手上添加了 UI 或其他附加元素,并突然发现您的移动被阻止,请检查是否已关闭其碰撞。

试一下。现在我们有一个灰色的圆柱体表示我们的交互组件。我们应该给它一个更合适的材质。

创建一个交互光束材质

我们将为交互光束提供一个简单的半透明材质。我们希望能在世界中看到它,但又不希望它过于显眼,分散我们对世界的注意力:

  1. 找到我们保存了用于传送的M_Indicator材质的Content目录中的位置。

  2. 在此目录中创建一个新的材质,并将其命名为M_WidgetInteractionBeam

  3. 打开它并将其混合模式设置为Translucent。(记住:要设置材质属性,请选择输出节点。)

  4. 按住V键并单击以创建一个矢量参数节点。将其命名为BaseColor

  5. 将 BaseColor 节点的默认值设置为纯白色 - (R=1.0,G=1.0,B=1.0,A=0.0)。

  6. 将其输出连接到 BaseColor 和 EmissiveColor 材质输入。

  7. 在材质图中右键单击并创建一个纹理坐标节点。

  8. 右键单击并创建一个线性渐变节点,将纹理坐标的输出连接到其 UV 通道输入。

  9. 按住M键并单击以创建一个乘法节点。

  10. 将线性渐变节点的 VGradient 输出连接到乘法节点的 A 输入。

  11. 按住S键并单击以创建一个标量参数。将其命名为OpacityMultiplier

  12. 将其滑块最大值设置为 1.0,将其默认值设置为 0.25。

  13. 将其输出连接到 Multiply 节点的 B 输入。

  14. 将 Multiply 节点的结果连接到材质的不透明度输入:

我们需要调整这个材质以适应我们的环境。通过创建材质实例,我们可以更轻松地完成工作。材质实例是从材质派生出来的,但只能更改在父材质中公开的那些参数。因为材质实例不包括对材质图的任何更改,只有值的更改,所以当进行这些更改时,它们不需要重新编译。在材质实例中更改值比在材质中更改值要快得多。

  1. 右键单击M_WidgetInteractionBeam,选择 Material Actions | Create Material Instance。

  2. 将新实例命名为MI_WidgetInteractionBeam

  3. MI_WidgetInteractionBeam分配给BP_VRPawn上的InteractionBeam静态网格组件。

运行地图。它仍然很亮。

  1. 打开MI_WidgetInteractionBeam并将其 OpacityMultiplier 设置为 0.01。 (在您计划更改的值旁边打勾。)

再次运行。好多了。

创建一个碰撞效果

现在我们需要一个碰撞效果来显示光束与目标的交叉点。

  1. 创建一个新的静态网格组件,作为BP_VRPawn玩家的根组件(Capsule Component)的子组件。

  2. 将其命名为InteractionBeamTarget

  3. 将其静态网格属性设置为Engine/BasicShapes/Sphere

  4. 将其缩放设置为(X=0.01, Y=0.01, Z=0.01)

  5. 将其碰撞| Can Character Step Up On 设置为No,将其碰撞预设设置为NoCollision

这个目标球体也需要一个材质。为此,我们将创建一个带有深色轮廓的自发光材质,以便在明亮和暗背景上清晰显示。

  1. 创建一个名为M_WidgetInteractionTarget的新材质。

  2. 按住V键并点击创建一个矢量参数。将其命名为BaseColor并将其默认值设置为纯白色。

  3. BaseColor拖动一个输出并点击-创建一个 Subtract 节点。

  4. 将 Subtract 节点的结果输入到材质的 Base Color 和 Emissive 输入中。

  5. 右键单击并创建一个 Fresnel 节点。

  6. 按住 1 键并点击创建一个标量材质表达式常量。将其值设置为 15。

  7. 将其输入到 Fresnel 节点的 ExponentIn 中。

  8. 按下Ctrl+W进行复制,将新常量的值设置为 0,并将其输入到 Fresnel 节点的 BaseReflectFractionIn 中。

  9. 按住M并点击创建一个 Multiply 节点。

  10. 将 Fresnel 节点的结果输入到 Multiply 节点的 A 输入中。

  11. 按住S并点击创建一个标量参数。将其命名为OutlineThickness并将其默认值设置为 10。

  12. 将 OutlineThickness 输入到 Multiply 节点的 B 输入中。

  13. 将 Multiply 节点的结果输入到 Subtract 节点的 B 输入中:

  1. 在内容浏览器中,从该材质创建一个名为MI_WidgetInteractionTarget的材质实例。

  2. MI_WidgetInteractionTarget分配给我们在BP_VRPawn上创建的InteractionBeamTarget球体。

最后,我们需要将其位置设置为交互组件的碰撞位置。

  1. BP_VRPawn玩家的事件图中,找到Event Tick并在Event TickUpdateTeleport_Implementation折叠图之间创建一个 Sequence 节点。

  2. 将对WidgetInteraction的引用拖动到图中,并在其输出上调用Get Last Hit Result

  3. 右键单击返回值并选择拆分结构引脚。

  4. 将对InteractionBeamTarget静态网格组件的引用拖动到图中。

  5. 在其上调用SetWorldLocation,并将Get Last Hit Result的返回值 Impact Point 输入到其新位置中。

  6. 将 Sequence 节点的 Then 1 输出连接到 SetWorldLocation 节点的执行输入中。

  7. 选择这些新节点,右键单击,选择折叠节点。将折叠的图命名为UpdateWidgetInteractionTarget_Implementation

  1. 打开折叠的图并进行清理。

折叠的图应该是这样的:

测试一下。光束不错,目标点也很容易找到:

我们还可以做很多其他事情,比如在光束碰到小部件时切断它,并根据它与玩家视图的接近程度调整目标球的比例,但我们在这里已经有了一个非常好的起点。这个系统功能强大,并且可以很容易地扩展和改进。

探索关卡并尝试使用伴侣控制器。虽然我们在这里所做的相当简化,但它包含了我们可能想要做的很多事情的基础。

总结

在本章中,我们为我们的开发工具库添加了一个重要的剩余部分,并为我们的项目添加了功能性的 UI 元素。

在本章中,我们学习了如何创建一个简单的 AI 控制角色并对其进行动画处理,还学习了如何使用 UMG 在 3D 空间中创建 UI,这也使我们能够改变角色的 AI 状态。

在下一章中,我们将继续从创建角色和界面转向探索创建用于 VR 的环境。

第八章:构建世界并针对 VR 进行优化

在本书迄今为止的工作过程中,我们大部分时间都专注于玩家角色。这是有道理的-虚拟现实极大地改变了玩家与世界互动的方式。我们需要学习新的方法来让玩家四处移动,使用手来与世界互动,以及构建用户界面的新方法。

这是一项不小的成就,所以恭喜你走到了这一步!

现在,我们要稍微改变一下焦点,开始关注我们周围的环境。到目前为止,我们一直在使用现有的环境,但现在是时候开始建立我们自己的环境了。在这个过程中,我们将会发现 VR 环境带来了一些需要解决的挑战。光照、物体比例和视线都比平面屏幕更重要,并且性能是一个重要考虑因素。

在本章中,我们将学习如何利用我们手头的工具和技术来解决这些挑战。我们将学习如何使用 VR 编辑器在头戴式显示器中布置环境,并在构建过程中实际查看其在 VR 中的外观,还将学习如何对这些环境进行性能分析和优化,以确保我们能够满足帧率要求。

在本章中,我们将探讨以下主题:

  • 使用 VR 编辑器构建和照明场景

  • 对场景进行性能分析以识别瓶颈

  • 使用静态网格实例化、LOD、网格组合和光照更改来优化场景

  • 优化的项目设置

  • 移动 VR 的特殊考虑和技术要求

让我们开始吧,给自己一个玩耍的地方。

设置项目并收集资产

对于本章的工作,让我们使用以下模板选项创建一个新项目:

  • 一个空白的蓝图模板

  • 针对移动/平板硬件进行优化

  • 可扩展的 2D 或 3D

  • 没有起始内容

创建项目后,打开其项目设置并设置以下菜单选项:

  • 项目 | 描述 | 设置 | 在 VR 中启动:True

  • 引擎 | 渲染 | 正向渲染器 | 正向着色:True

  • 引擎 | 渲染 | 默认设置 | 环境遮蔽静态分数:False

  • 引擎 | 渲染 | 默认设置 | 抗锯齿方法:MSAA

  • 引擎 | 渲染 | VR | 实例化立体声:True

  • 引擎 | 渲染 | VR | 循环轮询遮挡查询:True

在设置完所有这些设置后,允许项目重新启动。

项目重新启动后,打开文件菜单并使用它加载上一章的项目。就像上次一样,我们将使用迁移工具获取之前创建的元素并将它们带入新项目中。

将蓝图迁移到新项目中

从之前的项目中,选择内容资源管理器中的 BP_VRGameMode,右键点击它,选择资源操作 | 迁移。将你的新项目的Content目录作为目标内容文件夹。因为 GameMode 引用了 BP_VRPawn,而 BP_VRPawn 引用了 BP_CompanionCharacter,所有这些对象及其所需的支持资产都应该被迁移过来。

迁移完成后,还有一件事情需要做。我们在之前的项目中设置了一些自定义输入,我们在新项目中也需要它们。导航到上一章的项目目录,并将Config/DefaultInput.ini文件复制到新项目的配置目录中。

验证迁移的内容

重新打开新项目。这里我们要做的第一件事是验证我们带入的所有内容是否正常工作:

  1. 选择文件 | 新建关卡 | VR 基础,创建一个起始的 VR 地图。

  2. 将一个导航网格边界体放置在地图上,并确保它围绕着地板。将其位置设置为(X=0.0,Y=0.0,Z=0.0),将其缩放设置为(X=10.0,Y=10.0,Z=2.0)即可。记得按下“P”键来可视化你的导航网格,并确保它正常生成。

  3. 保存这个关卡(我们将其命名为 VRModePractice,并放置在Content/C07/Maps中)。

  4. 打开设置|项目设置|地图和模式|默认模式,并将默认游戏模式设置为我们从其他项目迁移的 BP_VRGameMode。将编辑器启动地图和游戏默认地图也设置为这个地图。

  5. 在关卡上放置一个 BP_CompanionCharacter 的实例。

在 VR 预览中测试地图。你应该能够移动和传送,你的伴侣角色应该跟随你:

这张地图非常适合用于学习虚幻编辑器的 VR 模式-它易于操作,并且提供了许多我们可以在界面上练习时操作的部件。让我们充分利用它。

使用 VR 编辑器

虚幻引擎配备了一个非常强大的虚拟现实编辑器,可以让你完全在虚拟环境中构建场景。几乎任何你可能需要执行的编辑操作都可以在不离开 VR 的情况下完成。

然而,当你第一次遇到 VR 模式编辑器时,可能会认为它只是一个花招。毕竟,现有的编辑器有什么问题呢?没有问题,但是这里有一点需要注意:虚拟现实不是一个平面屏幕。深度是存在的。视线是不同的。颜色的渲染也不同。通过使用平面屏幕进行虚拟现实开发会给你的设计过程增加一层抽象。当你能够直接在目标媒介中工作时,你会更加了解并获得更好的结果。

在实践中,你可能会发现两种编辑模式都很有用。就像在平面屏幕编辑器视图中很难看清楚一个场景在 VR 中的真实样子一样,在 VR 模式下放置物体时很难达到精确。当你熟悉工具时,你会发现自己的工作流程,并发现你更喜欢在哪个领域进行哪些操作。然而,这里的重点是,将 VR 模式视为 VR 场景布局工作流程的重要组成部分是值得的。花时间熟悉它,这样当需要时就可以依赖它。

VR 编辑的一个好的实践是在 VR 中进行初始的块状布局。以一种能够传达你想要表达的空间感的方式放置物体,然后转到传统的平面编辑来进一步完善你的布局并填充它。最后,返回到 VR 编辑中进行最后的调整,这样你就可以清楚地看到你将要得到的结果。

让我们激活 VR 编辑器,看看我们可以用它做些什么。由于你在戴头盔时无法阅读这本书,我们将介绍一些基本原则,让你尝试一下,然后再回到这里探索更多内容。

首先要知道的是如何进入和退出 VR 编辑器。

进入和退出 VR 模式

你可以通过使用 VR 模式工具栏按钮来激活 VR 编辑器。要退出 VR 模式,请激活径向菜单(稍后会详细介绍)并选择“系统|退出”。不过,最简单的方法是习惯使用Alt + V来进入和退出 VR 模式:

还可以将 VR 模式配置为在编辑器运行时自动进入头戴式显示器时自动进入。要做到这一点,选择“编辑|编辑器首选项|常规|VR 模式”,并将“启用 VR 模式自动进入”设置为 True。是否这样做取决于你的选择,但是在实践中,它往往很难确定何时关闭自身,因此使用Alt + V进入和退出通常是一个更好的主意。

如果你更喜欢使用左手进行交互,你可以在 VR 模式首选项中选择此选项:

VR 模式的设置可以在“编辑”|“编辑器首选项”|“常规”|“VR 模式”下找到。

如果你愿意,可以设置其中任何一个选项。我们将保留这些选项的默认设置。

我们还需要解决的另一件事是如何移动和观察周围。

在 VR 模式下导航

在 VR 编辑器中,通过挤压握持按钮来激活移动模式。当移动模式激活时,移动网格将出现,交互光束将变为绿色。

VR 编辑器中的交互光束会改变颜色以指示其所处的模式。红色表示标准交互模式,绿色表示移动模式,黄色表示你当前选择了一个角色,蓝色表示你处于 UI 交互模式。

在 VR 编辑器中,移动的隐喻是推动拉动世界。这是相当直观的。在大多数情况下,当你的移动模式处于活动状态时,世界会按照你的手的移动方式移动。

在世界中移动

如果你在握持按钮的同时移动控制器,世界会移动,就像你在拉动它,或者在其中游泳一样:

如果你在移动控制器时松开握持按钮,移动会继续一段时间,就像你从一个物体上推开并且现在漂离它一样。这需要一些练习,但一旦你掌握了它,它就会变得相当直观。再次挤压握持按钮会停止你的移动。

移动网格显示了你真实世界跟踪体积中地板的位置。将其与场景中的地板对齐,以查看从站在地板上的人的视角看物体的真实样子。

通过世界传送

要通过世界传送,挤压你主手控制器上的握持按钮并按下扳机。将控制器对准一个物体或目的地,释放时你将传送到那里:

通过传送和拖动的组合,你可以很好地在世界中移动。

旋转世界

当你需要旋转视角时,握住两个手柄的握持按钮,将手柄彼此旋转,就像你试图旋转世界一样:

你在旋转轴上看到的数字是世界当前的比例。我们也可以操纵它。

缩放世界

要缩放世界,挤压握持按钮并将控制器向彼此移动以缩小世界,或将其远离彼此以扩大世界:

将场景缩小到看起来像桌子上的微型场景真是一种奇妙的满足感。

将控制器彼此靠近会缩小世界。将它们远离彼此会扩大世界。这对于布局很有用,因为你可以将世界组装成微型,然后传送回地面并恢复其正常比例,以查看你所做的事情。

在 VR 模式下,最快的方法之一是缩小世界,然后使用传送动作(握住+扳机)在地图上传送到新的位置。当你传送时,世界会恢复到默认大小。

练习移动

现在花点时间用你的控制器练习在世界中导航。使用Alt + V进入 VR 模式,当你想退出时再次按下Alt + V。使用握持按钮在世界中移动、传送、旋转和改变其比例。玩弄它直到感觉自然。这需要一些细微的技巧,但一旦你熟悉了,它就是一个非常有用的工具。

修改 VR 模式下的世界

现在你已经练习了一下在世界中移动,让我们开始学习一些在 VR 中进行场景构图所需的技巧。

移动、旋转和缩放对象

要选择一个对象,只需将光束对准它并拉动触发器。您的交互光束将变为黄色,表示您已进入选择模式。将出现一个 Gizmo,允许您移动对象。默认情况下,这将是一个平移 Gizmo,允许您在选定的对象周围移动(我们将在一会儿看到如何切换到其他类型的 Gizmo):

如果您想移动所选对象,请释放触发器,然后再次拉动触发器,同时指向对象或变换 Gizmo。您可以使用变换 Gizmo 的箭头和平面来限制移动,或者直接与对象交互以自由移动它。当使用交互光束直接移动对象时,您可以使用触摸板将其靠近或远离您。

请注意,带有碰撞的隐藏对象有时会干扰 VR 模式下的选择。如果您的选择光束似乎穿过您想要选择的对象,请移动到不同的视角点来选择它。

通常最好使用 Gizmo 来移动对象,因为使用任何精度将对象在深度上移动是相当困难的。

可以使用径向菜单界面将默认的变换 Gizmo 切换到其他模式。要激活径向菜单,请触摸非交互手上的触摸板或拇指杆,并指向您想要选择的菜单选项。使用触发器进行选择。您的控制器菜单按钮将带您退出子菜单,或者如果您已经在顶级菜单,则关闭径向菜单:

选择 Gizmo 子菜单可以在变换 Gizmo 选项之间切换:

通用 Gizmo 提供了一个单一的 Gizmo 上的平移、旋转和缩放控制。平移、旋转和缩放 Gizmo 为这些操作提供了单独的工具。将变换模式切换为局部空间时,对象沿着自己的轴旋转、缩放和移动,而世界空间模式则沿着世界轴变换对象。

使用两个控制器旋转和缩放对象

您可能还注意到,每当您选择一个对象并将触发器放在对象本身上(而不是 Gizmo 手柄上)时,您的非主手控制器上会出现第二个交互光束。如果您将第二个交互光束对准对象并按下触发器,您可以同时使用它们来翻转和拉伸对象:

这是一个探索即兴布局的好工具。它直观并邀请您与环境中的对象进行自然互动。这是一个用于探索和即兴布局的好工具。您可能会发现将物体放在您想要的位置可能会很困难,但如果您使用此工具进行粗略布局,然后在平面编辑器中进行清理,您可以获得良好的结果。

练习移动对象

现在试试吧。按下Alt + V进入 VR 模式,并且除了练习在世界中移动之外,还要练习使用变换 Gizmo 和自由移动来移动世界中的对象。记得使用径向菜单来改变移动模式,并使用菜单按钮返回到主菜单。花些时间练习一下。一开始控制可能会感到陌生,但一旦掌握了它们,用 VR 进行世界构建将是一种有益的体验。

完成后,按下Alt + V再次退出 VR 模式,如果需要,在平面编辑中清理对象对齐。

现在我们准备开始组合一个场景,为此,我们将使用 VR 模式菜单。

在 VR 模式中组合新场景

现在我们已经学会了 VR 模式编辑器的基本操作,让我们深入了解一下如何将其用作场景组合工具。首先,我们需要一些要使用的资产。免费的无尽之剑:草地包将为我们提供一些可以玩耍的东西。

打开您的 Epic Games Launcher(在此过程中可以保留您现有的项目打开),导航到 Unreal Engine | Marketplace | Free 选项卡,并搜索 Infinity Blade: Grass Lands。点击“添加到项目”并选择您的新项目作为目标项目:

一旦资产下载和安装完成,让我们强制编译新的着色器。打开Content/InfinityBladeGrassLands/Maps/Overview,并让着色器编译。在这些着色器编译时,可以使用Alt + V进入 VR 模式,并在概览地图中导航,看看我们可以使用的资产。

在构建了您的着色器之后,我们可以使用这些资产来组合一个场景。对于这个练习,我们将从一个现有的地图开始并进行修改。

首先,我们需要学习如何在 VR 中导航编辑器菜单。

导航径向菜单

VR 编辑器中的菜单交互主要是通过附加到控制器的一系列径向菜单来处理的。实际上,这些菜单使用起来相当直观,因为它们清晰地映射到手柄上的触摸板或拇指杆输入。让我们看看它们是如何工作的:

  1. 选择Content/InfinityBladeGrassLands/Maps/ElvenRuins并打开它。

  2. 如果您愿意,您还可以更改您的项目设置|地图和模式|默认地图以自动打开此地图。

  3. 使用Alt + V进入 VR 模式,当您处于此模式时,触摸左侧的触摸板或拇指杆以激活径向菜单。

  4. 要进入菜单,请将交互光束对准它并按下扳机或使用菜单手柄的触控板选择选项。

  5. 要退出子菜单,请使用非主导手的菜单按钮:

您可以使用交互光束或菜单手柄的触控板在 VR 模式下导航菜单

让我们进入 VR 模式并探索菜单。您可以从主菜单中选择八个主要菜单类别。

Gizmo

我们已经探索了 Gizmo 菜单,所以我们不会在这里详细介绍。请记住,它用于在编辑器中切换移动工具的行为。

对齐

对齐菜单是 Gizmo 菜单的紧密伙伴。其中大多数的行为与平面编辑器中的行为相同,但智能对齐选项特别值得了解:

启用智能对齐后,您在场景中移动的对象将尝试在移动时与其他对象对齐。由于在 VR 模式下很难实现精确的定位,这是一个很大的帮助。

使用“设置目标”选项选择一个特定的对象,您希望其他对象对齐到该对象,并使用“重置目标”选项清除它。

窗口

Windows 子菜单提供了访问您在组合场景时将使用的各个调色板和菜单:

每个按钮都会打开其关联的面板。这些面板与平面编辑器中的面板相同:

在编辑器的 VR 模式中看到的内容浏览器

要移动一个窗口,将交互光束对准其下方的大条。您可以将其放置和角度调整为任何您想要的方式。移动条左侧的朝下箭头将窗口固定在原位。当它被激活时,窗口将保持在您放置的位置,无论您如何在世界中移动。当它未固定时,窗口将随您的移动而移动。条右侧的 X 形按钮关闭窗口:

您可以移动活动窗口以创建一个虚拟工作空间来进行工作

这些窗口的工作方式与平面编辑器中的窗口相同。在使用它们时,一个有效的做法是只打开您需要的窗口,并将它们排列在您周围的虚拟工作空间中以完成您正在进行的任务。

在实践中,很多时候,将内容浏览器和详细信息窗格保持打开状态会很有用。

编辑

编辑菜单允许您在场景中复制、删除和对齐对象:

大多数选项应该都很容易理解,并且符合您对编辑菜单的期望。对齐到地板是一个例外,所以值得记住它在这里。您会经常使用它。

工具

工具菜单主要用于管理编辑器中的模拟。在这里,您可以启动、暂停和恢复模拟,并将其结果保存回编辑器:

这里还包含了两个与模拟无关的选项。截图工具可以捕捉标准分辨率的截图,但请注意,截图将包括菜单,所以如果您想要一个干净的截图,请将其移出视线。手电筒工具对于在黑暗场景中找到方向非常有用,特别是如果您正在进行场景照明的中途。

模式

模式面板允许您放置诸如灯光、体积和基元等演员;管理植被;进入地形雕刻模式;以及绘制纹理和顶点颜色,就像在平面编辑器中一样:

选择其中一个选项将带出一个模式面板,然后可以将其放置在世界中,并以与 Windows 菜单中提供的其他面板相同的方式使用。

操作和系统

目前,系统菜单只提供了退出 VR 模式的方法。在撰写本文时,它没有其他功能。操作菜单的行为取决于上下文。

对场景进行更改

现在我们已经学会了如何在 VR 模式下操作,让我们将这些学习应用到实践中。我们将在 VR 模式下修改 Elven Ruins 地图。

我们要做的第一件事是改变白天的时间。让我们看看这些废墟在黎明时会是什么样子。

使用Alt + V进入 VR 模式,用非交互手的触摸板或拇指杆触摸来呼出径向菜单。使用菜单按钮导航返回主页,如果当前处于子菜单中,请选择 Windows 菜单,然后激活 World Outliner。

使用交互光束拖动菜单底部的移动框。将其放在您的侧面稍微下方。

我们要找到在这个场景中充当太阳的定向光。要找到它,点击类型列的标题,按类型对演员列表进行排序,然后使用触摸板滚动列表,找到名为 Light Source 的定向光:

不幸的是,在 VR 模式下没有简单的方法输入文本。径向菜单提供了一个数字键盘,您可以在设置值时使用,但如果您想搜索光源,您必须使用传统键盘进行输入。对于这种类型的工作,排序、滚动和选择功能非常好用。

选择定向光后,使用径向菜单激活详细信息面板。使用面板下方的条形图将其拖动到一个可以阅读和交互的位置,但仍然可以看到天空:

在这张从 VR 头盔中拍摄的照片中,你可以看到我们通过在 3D 空间中操作面板来创建了一个虚拟工作空间。

将交互光束对准光源的 Rotation Y 值,并在盒子上来回拖动以改变其值。你会看到太阳在头顶上变化。它的初始值大约为-48。将其拖动到大约 210(或者你喜欢的任何位置),可以创建一些漂亮的戏剧性阴影。

现在,选择 BP_SkySphere。在其详细信息面板中,打开 Colors Determined by Sun Position,并勾选 Refresh Material 复选框以改变天空的颜色:

这样很好,对吧?像这样的光照变化通常最好在 VR 模式编辑器中进行,因为头戴式显示器中的光照和颜色与平面屏幕上的显示非常不同。

通常最好在平面屏幕编辑器中构建地图中的新元素。VR 模式非常适合检查视线和调整物体位置,但在实践中,它仍然存在一些问题,这可能会使物体选择变得困难:

以下是在 VR 模式下工作的几种有效方法,以发挥其优势并解决其弱点:

  • 通过缩小世界规模来移动,然后使用传送来到达目的地

  • 在 VR 模式中进行粗略的光照调整,以便您可以看到它们对世界的真实影响

  • 在传统编辑器中构建几何体,但使用 VR 模式来尝试其位置

养成经常使用Alt + V来在 VR 中检查环境的习惯,以便在构建时了解哪些调整在 VR 模式下是有意义的,哪些在传统编辑器中效果最好。

最重要的是,我们在本节中想要传达的是,VR 模式绝非奢侈品或花招,而应被视为 VR 场景构建工作流程中的必备工具。

为 VR 优化场景

现在我们已经谈了很多关于使用 VR 模式编辑场景的内容,让我们谈谈 VR 开发中一个非常关键的主题-保持可接受的帧率。

我们之前已经多次讨论了在虚拟现实中保持帧率的至关重要性。这是至关重要的,也是具有挑战性的。在本章的剩余部分,我们将讨论一些可以加快场景速度并找出导致速度变慢的原因的方法。

测试当前性能

在评估场景性能时,您需要做的第一件事是找出当前运行速度有多快。我们将看一些可以用于此的命令。

从编辑器中,点击`(反引号)键。它位于键盘上 1 键的左边,Tab 键的上方。将出现一个控制台输入框:

可以在此处输入各种控制台命令。我们将讨论您在优化场景时最有可能使用的命令。

Stat FPS

在控制台命令行中输入stat fps。编辑器窗口中将出现一个帧率计数器,显示两个值:

第一个是每秒帧数(FPS)。第二个值告诉您绘制帧所花费的毫秒数,这是您应该训练自己关注的值。帧率是玩家所感知到的,但在开发和尝试解决影响帧率的问题时,如果您训练自己以毫秒为单位思考,那么您在思考所做更改如何影响性能时会更容易。帧率描述了您期望的结果,但您在渲染帧的每个部分上花费的毫秒数是原因。在修复场景时,您需要查看每个操作的单独成本,这些成本以毫秒为单位表示。

确定您的帧时间预算

如果我们要以毫秒为单位思考,首先要做的是确定我们可以花多少毫秒来绘制帧并仍然达到目标帧率。这很简单。

要找到应用程序的帧时间预算,将 1,000 除以目标帧率。

这给出了您必须绘制帧的毫秒数以实现此帧率。例如,如果您的目标是刷新率为 90 FPS 的头戴式显示器(大多数头戴式显示器都是如此),我们可以这样找到我们的帧预算:

1000 / 90 = 11.11

这给我们一个大约 11 毫秒的帧预算。如果你在 11 毫秒或更短的时间内交付帧,你的 VR 应用程序将以 90 FPS 刷新。这不是很多时间,所以我们需要在大多数场景中做一些工作来实现这一点。

关于性能分析的警告

在我们深入性能优化的兔子洞之前,让我们记住几个重要的事情。

首先,平面屏幕上报告的帧时间对于 VR 来说不准确。它是一个可以用来大致了解你的情况的基准值,但当你激活 VR 时,你的帧率会下降。

如果你在平面屏幕值和 VR 值之间看到了明显的帧率下降,请检查你的项目设置,确保已经打开了实例化立体。如果关闭了(这是默认设置),你将支付渲染整个场景两次的全部成本,这绝对是你不想做的。

确保你不仅仅在平面屏幕上检查数值。经常在 VR 中进行测试。一种快速检查 VR 性能的方法是从 VR 模式中读取 stat fps 的值。

  • 在可见的 stat fps 下激活 VR 模式。从头戴式显示器中可能无法读取文本,但你可以从平面屏幕输出中读取。

使用这种方法来检查你的环境。在地图中移动并使用 VR 模式检查问题区域。

另一个重要的事情要考虑的是,因为我们是在编辑器中进行测试,所以我们的数字受到编辑器本身的影响。我们需要支付渲染编辑器显示的所有窗口以及游戏场景的成本。为了获得准确的值,我们必须在独立会话中运行游戏。在编辑器中检查你的数字是一个好的实践,可以看到你所做的更改是好还是坏,但你应该记住它们并不能准确描述你打包的应用程序会做什么。

我们还需要记住,当我们在编辑器中测试帧时间时,我们实际上只是在看渲染性能,但我们没有得到关于应用程序的其他部分成本的任何信息。这在大多数情况下都没问题,因为你的问题很可能在渲染方面,但你仍然应该确保测试正在运行的应用程序,以确保你没有一个失控的蓝图或太多的动画角色拖累你。

最后,我们应该谈一下系统规格。不同的硬件配置会有不同的性能表现。如果你计划向公众发布一个应用程序,你应该确保你在最低规格的硬件上进行测试,以及在开发机器上进行测试。仅仅因为你的应用程序在一台配备全新高端显卡的怪物上运行良好,并不意味着它在旧硬件上也会运行得很好。如果你可以在最低规格的目标上进行测试,那就这样做。如果不能,要意识到你的开发机器与最低规格相差多远,并确保在帧时间预算中留出足够的余地来适应这一点。

现在我们已经谈了一些可能影响我们测量结果的因素,让我们深入了解如何获得比仅仅使用 stat fps 更好的信息。

Stat unit

检查我们的帧率是有用的,也是一个重要的频繁操作,但仅仅这样并不能告诉我们太多信息。它可能告诉我们有问题,但它不会给我们提供找出问题所在或如何修复问题的指导。为此,我们还有一些更有用的命令可以使用。

stat unit 命令以毫秒为单位分解了帧的成本,并显示了我们渲染场景所花费的成本和应用程序中其他活动(如动画和 AI)所花费的成本。

现在试试。点击`(反引号)键以打开控制台命令窗口,然后输入 stat unit 以在帧率信息下添加此额外信息:

stat unit 命令显示四个主要信息:

  • 帧:这是绘制帧所花费的总时间。这与我们在 stat fps 结果中看到的值相同。

  • Game:这告诉您游戏线程在 CPU 上花费了多长时间。这包括动画更新、AI 和 CPU 必须解决的其他任何事情,以更新帧。如果蓝图在 Tick 事件上执行效率低下的操作,这将增加该值。

  • Draw:这告诉您 CPU 花费了多长时间来准备渲染场景。这里的高值可能意味着您进行了过多的遮挡剔除或在光照或阴影上花费了太多时间。

  • GPU:这个值告诉您 GPU 绘制帧所花费的时间。这里的高值可能意味着您绘制了太多的多边形,使用了太多的材质,或者您的材质过于复杂。大多数情况下,您的问题将出现在这里。

这些值不是累加的。您的游戏线程将等待渲染线程完成,因此,如果游戏时间与 GPU 时间匹配,那么实际上告诉您的是您的 CPU 没有拖慢您的速度,并且您的帧时间是由渲染驱动的。

除了这四个基本值之外,我们还有两个高级信息,您现在不需要担心:

  • RHIT:这是您的渲染硬件接口线程。实际上,除非您使用高级渲染硬件或视频游戏主机,并且在专用线程上运行渲染硬件接口调用,否则您不会在这里看到与 GPU 值差异很大的值。除非您正在进行一个带有专门的工程团队的高级项目,否则这可能不适用于您。

  • DynRes:这表示您的应用程序是否支持或正在使用动态分辨率。实际上,这仅在视频游戏主机上支持,所以您不需要在这里担心它。如果您感兴趣,可以在docs.unrealengine.com/en-us/Engine/Rendering/DynamicResolution找到更多信息。

我们从 stat unit 信息中感兴趣的是我们是否在 Game CPU、Game 渲染操作或 GPU 上花费了大部分时间。我们寻找最大的数字,因为这将告诉我们需要修复的问题。

在开发过程中,您应该养成几乎一直保持 stat fps 和 stat unit 的习惯。如果您引入了新的场景,会导致帧率下降,那么发现问题的最佳时间就是在放入场景时。如果您很长时间才发现问题,那么您将需要做更多的工作来找出问题的原因。

查看统计单位值随时间变化的情况通常是值得的,无论是在应用程序中发生的事情(这对于找到卡顿很有用)还是在场景中移动时。要获取这些信息,请使用 stat unitgraph 来显示场景性能指标随时间变化的图表:

您将看到您的 stat unit 值现在已经被彩色编码以对应图表上的线条。

如前所述,大多数情况下,您的问题将与 GPU 艺术品有关,这些艺术品太重而无法适应您的场景。

当然,如果您在 Tick 上做了荒谬的事情,您的 CPU 可能会被杀死,这种情况下,您将希望寻找可以重构以响应事件或数据变化而不是使用 Tick 的蓝图。但是,大多数情况下,您可能会遇到 GPU 的问题。

对 GPU 进行分析

优化场景时,您应该学会使用的第一个工具是 GPU 分析器。您可以在控制台中输入 profilegpu 来激活它,但由于您将经常使用它,最好记住快捷键:Ctrl + Shift + ,(逗号)。现在按下它,让我们看看数字:

此配置文件报告的最重要部分是场景标题下的图表。将鼠标悬停在图表上,您将看到工具提示告诉您每个块代表什么。最大的两个块通常是您的 BasePass 和 PostProcessing pass。基本传递表示绘制场景中的所有内容的行为。后处理处理在场景绘制完成后处理的任何内容,例如屏幕空间环境遮挡、颜色校正和其他效果。

点击场景标题左侧的展开器,以获取更多关于场景渲染的详细信息:

在这里,我们可以看到更详细的细分,了解绘制帧所花费的时间。光照看起来很好,透明度也很好。我们的 BasePass 相当大,但这是可以预料的。

通过深入研究 BasePass,您不会获得太多更多的信息,但是通过深入研究 PostProcessing 操作,您可以学到一些有用的东西。使用 PostProcessing 标题旁边的三角形进行深入研究,然后单击 PostProcessing 操作中的大块以查看它们是什么:

在这种情况下,这些后续数字看起来相当不错。我们没有任何一个持续时间过长的问题。

确保在游戏运行时进行分析,否则您将看到许多来自编辑器的操作。

我们在这里没有足够的空间来深入研究渲染过程和其含义的所有内容,但总的来说,您要寻找的是可能不必要地影响帧率的大型项目。当您发现看起来可疑的东西时,在虚幻论坛上搜索它,您可能会找到关于它的讨论以及如何处理它的方法。

随着您越来越多地使用这个工具,您会逐渐对健康的外观和问题区域的外观有所了解。经常使用它来清楚地了解您的应用程序正在做什么。

现在,让我们看一些其他有用的命令,我们可以用来调试我们的场景。

Stat scenerendering

在 GPU 分析器之后,您下一个最有用的命令可能是 stat scenerendering。该命令会详细列出系统在渲染场景时所采取的步骤及其相关的时间:

在这里特别值得一看的是您的动态阴影设置和透明度绘制。

如果您在阴影设置中看到较高的值,请查看是否有一个或多个灯光正在执行过多的阴影级联或具有过长的阴影距离。您可以在此主题的docs.unrealengine.com/en-us/Platforms/Mobile/Lighting/HowTo/CascadedShadow上找到更多信息。

如果您的透明度绘制很高,请激活编辑器的 Quad Overdraw 优化视图模式,并查找互相堆叠的透明对象。如果您在这里有问题,您可以尝试使用遮罩材质而不是透明材质,或者注意它们在视图中的重叠情况:

在此列表的底部有一些非常重要的数字:网格绘制调用和静态列表绘制调用。我们应该谈谈这些。

绘制调用

影响场景性能的最大因素之一是将信息传输到 GPU 所需的绘制调用次数。我们在这里讨论什么?情况如下:你希望显卡绘制的所有内容都必须复制到该显卡的内存中。向显卡发送一组指令的行为称为绘制调用,或称为绘制原语调用(有时缩写为 DPC)。假设你的场景中出现了一个静态网格,上面有三个材质。这将需要四个绘制调用来设置它在显卡上的绘制:一个用于网格,每个材质一个。你应该尽量减少场景中的绘制调用次数。实际上,对于 VR 场景,2000 个绘制调用可能是你的限制。在移动 VR 中,如 Oculus Go 或 Quest,这个数字更低。

这对你意味着什么?首先,尽量少地在物体上使用材质;理想情况下,每个物体只使用一个材质。只需添加一个额外的材质槽,你就增加了加载该物体到视频硬件的成本的三分之一,如果该物体在场景中频繁出现,这个成本会迅速累积。

我们很快会讨论如何处理高绘制调用次数的方法,但现在你需要知道的是,如果这些数字很高,说明你向显卡发送了太多的单独指令,这会减慢速度。也许你的物体上有太多的材质槽,或者有太多单独发送的物体,但在所有情况下,这都是你需要解决的问题。

Stat RHI

另一个与之密切相关的经常使用的命令是 stat rhi。RHI 代表渲染硬件接口,它告诉你具体影响渲染性能的是什么:

在这里你最关心的两个值是绘制的三角形数量和绘制原语调用次数。养成查看这些值的习惯,并寻找三角形数量或绘制调用次数过高的视图。对于桌面 VR 头显上的 VR 场景,你希望将绘制的三角形数量保持在 200 万以下,并且将绘制调用次数保持在 2000 以下。

在这里你还应该关注的另一个值是内存消耗。在实时场景中,使用过大的纹理也会导致场景运行非常缓慢。不要将 4K 纹理放在小石子上。我们见过这种情况发生。

Stat rhi是获取场景在预算内的整体感觉最有用的命令之一。

统计内存

当你需要更多关于内存预算超支的信息时,可以使用 stat memory:

大多数情况下,如果你的内存消耗过高,罪魁祸首往往是纹理。要注意使用过大的纹理。一个巨大的物体或主角角色可能需要一个 2048x2048 的纹理。其他任何东西都应该是 1024x1024 或更小。在 VR 中,使用 4K 纹理可能在任何情况下都不合理。在考虑如何减少纹理时,看看场景中的物体。它有多大?玩家能走多近?玩家真的在意看它吗?很容易在玩家几乎看不到的物体上花费太多。开始考虑在重要的地方使用纹理和多边形预算,并在可以节省的地方节约。

优化视图模式

除了统计命令之外,我们还有一些优化视图模式,可以用来找出场景中的问题。这些模式可以从编辑器视口的视图模式菜单中访问。我们这里只讨论其中的两个。

着色器复杂度视图显示了可能导致性能下降的材质位置。当您找到一个可疑的对象时,选择它,并查看其材质中发生了什么。您的材质是否过于复杂或进行了昂贵的计算?考虑以下截图:

在上面的截图中,草和树被识别为昂贵的材质。当我们选择它们的对象并查看这些材质时,我们可以看到推高成本的原因是它们使用了世界位置偏移输入来模拟风。这是昂贵的,但是这是一个很好的效果,如果我们关闭它,玩家会注意到,所以我们可以不管它,因为我们场景的其余部分运行得相当高效。

使用此视图搜索可能会消耗大量资源但对场景没有太多价值的材质。

如果您在延迟渲染模型下使用动态光源,那么光照复杂度视图就会起作用。因为我们在这里使用的是正向渲染和静态光源,所以在这个场景中不会显示任何内容。当您使用动态光源和延迟渲染时,这个视图可以显示您的光源引起的问题所在。

CPU 分析

如果您的 CPU 时间有问题,您可以使用 CPU 分析来找出问题所在,就像我们之前使用 GPU 分析器一样。

要激活 CPU 分析,在游戏运行时,打开控制台命令并键入stat startfile开始分析。分析会生成大量数据,所以您不希望在整个会话中运行分析器-只捕获您感兴趣的内容,比如“为什么当角色警报敌人时游戏会变得如此缓慢?”

在捕获到您要查找的内容后,键入stat stopfile以关闭分析。分析器将把捕获的数据保存到项目的\Saved\Profiling\UnrealStats\目录下的.ue4stats文件中。

现在,打开您的虚幻引擎安装目录,在其中的Binaries\Win64文件夹中找到UnrealFrontend.exe应用程序。启动它并使用选项卡选择 Session | Frontend | Profiler。使用分析器的加载按钮打开刚刚生成的.ue4stats文件:

CPU 分析器显示了每个帧调用的操作所花费的时间。

就像我们在 GPU 分析器中所做的那样,您可以使用此工具来查看昂贵的函数调用并了解发生了什么。在这本书的范围之外,我们无法深入介绍如何在此处使用 CPU 分析器-它是一个非常有用和强大的工具,但需要一些时间来学习如何从中获取有用的信息。我们建议您探索有关此主题的详细信息,可以在www.unrealengine.com/en-US/blog/how-to-improve-game-thread-cpu-performance找到。

打开和关闭功能

尽管听起来很原始,但是找出导致帧率下降的原因最有效的方法之一就是打开和关闭相关统计信息显示的功能(通常,stat unit是您想要的)。使用视口的显示菜单打开和关闭单个元素,特别是如果您通过 GPU 分析或统计信息确定该元素可能会引起问题。如果从您的关卡中删除对象(只要您有备份或它在源代码控制下),并查看是否有特定对象会产生很大的变化,这也可能会有所帮助。

解决帧率问题

现在我们已经学会了如何找到场景中的问题,让我们谈谈如何处理这些问题。

清理蓝图的 Tick 事件

如果你在 CPU 上看到很高的数字,你要寻找的第一个罪魁祸首之一就是在 Tick 事件上执行操作的蓝图。这是一个非常常见的问题。请记住,Tick 事件在每一帧都会发生,所以如果你在 Tick 上做了很多工作,你就会影响到每一帧的绘制。寻找将这个工作分散到多个帧上的方法,或者避免使用 Tick,只在发生变化时使用事件来改变对象的状态。

管理骨骼动画

如果你有很多骨骼网格在进行动画,确保它们的骨架中没有荒谬的骨骼数量,并确保它们没有使用大量的混合空间动画。最好的做法是使用骨骼网格的细节层次(LOD),只在玩家能看到时包含细节,或者在电影中使用单独的骨骼网格,其中高度详细的面部动画很重要,并且在游戏中使用骨骼数量较低的骨骼网格。有关设置骨骼网格 LOD 的更多信息,请从以下链接开始查看:docs.unrealengine.com/en-US/Engine/Content/ImportingContent/ImportingSkeletalLODs

合并演员

这是一个重要的问题。还记得不久前我们提到过绘制调用数量对帧率有很大影响吗?将多个网格合并成一个单一的网格是降低绘制调用数量的最便宜和最简单的方法之一。这不仅会将你选择的多个单独网格创建为一个单一网格,还会为该网格创建一个合并的材质,其中包含每个子网格的材质。这是一个重要的事情。

假设你在房间的一个角落里有一堆碎片;大约有 25 个物体,每个物体使用一个材质槽。这样一来,你就会有 50 个绘制调用,而你整个场景可用的绘制调用总数可能是 2000 个。这是一个很大的负担。通过将它们合并成一个单一的物体,你可以将 50 个绘制调用减少到两个。这是你可以减少绘制调用数量的最快和最有效的方法之一。

不过,这里有一个需要注意的地方:还记得在本书前面我们提到过 Kent Beck 的建议“让它工作,让它正确,让它快”吗?这是其中一个适用的领域。一旦你将所有这些物体合并成一个单一的物体,你就不再有重新排列各个组件的自由,所以先让场景看起来符合你的要求,然后合并你的演员以控制事物。

以下是如何操作:

选择窗口 | 开发者工具 | 合并演员。合并演员窗口将出现。选择要合并的演员。一般来说,合并那些靠近并且可能在同一视图中的演员是一个好主意。一旦它们被合并,即使只有其中一个在镜头中,所有它们都将被绘制,所以合并那些大部分时间都会同时出现在镜头中的物体:

在视口后面看到多个选定演员的合并演员对话框

如果选择替换源演员,则在场景中选择的演员将被合并模型替换。有关合并演员的更多信息,请从以下链接开始:docs.unrealengine.com/en-us/Engine/Actors/Merging

使用网格 LOD

在场景中绘制的三角形数量(通常称为多边形数量)是决定场景渲染速度的另一个重要因素。

当然,对抗高面数的第一道防线是建模。使用像 Pixologic 的 ZBrush 这样的应用程序,从高细节模型中烘焙法线贴图,并将其应用于导入游戏引擎的低细节网格。大部分时间,你的玩家都不会注意到区别。虚拟现实对使用法线贴图模拟几何细节的宽屏显示器不太宽容,因为玩家有时会看到深度不是真实的,但你仍然应该在任何可以使用这种技术的地方使用它。

然而,一旦你在游戏中有了一个网格,你就有一个强大的 LOD 工具可用于管理你绘制的三角形数量。LOD 的工作原理如下:它们存储了同一模型的几个版本,其面数逐渐减小。随着模型在屏幕上变小,系统会将高细节网格替换为低细节网格,因为玩家无法看到远离的细节。

以下是如何设置 LOD:

  1. 选择一个静态网格,并从内容浏览器中打开静态网格编辑器。

  2. 在其详细信息下,找到 LOD 设置部分。

  3. 找到 LOD 数量条目,并将其设置为大于 1 的值。(对于此测试,只需将其设置为 2 以创建 2 个 LOD。)

  4. 点击“应用更改”。现在将创建一个或多个额外的 LOD 模型,并将其添加到静态网格资源中。

  5. 在 LOD 选择器部分,找到 LOD 条目,并使用它选择一个新的 LOD。

LOD 0 是原始模型。大部分时间你会保持不变。LOD 1 是 LOD 0 之后的第一个 LOD。

  1. 选择一个新的 LOD,比如 LOD 1,打开其 LOD 详细部分的减少设置条目并进行修改。

这里有很多选项,但大部分时间,你将管理三角形百分比值。如果在这里进行更改,请点击“应用更改”以查看结果:

你会在视口中看到修改后的网格。为了看到它在真实视距下的样子,将 LOD 选择器切换回 LOD 自动,并移动视图以查看对象在 LOD 之间切换时的变化。LOD 生成器非常出色。

有关创建和使用 LOD 的更多信息,请首先查看docs.unrealengine.com/en-us/Engine/Content/Types/StaticMeshes/HowTo/LODs

静态网格实例化

还记得我们刚才关注的绘制调用吗?还有另一种强大的方法可以减少它们的数量并大大加快渲染速度。

假设你有一个大的集合,其中大部分是相同的资产,比如一个重复使用相同树木网格数百次的森林。如果你只是单独将这些网格放置在环境中,每一个都会生成至少两个绘制调用,如果使用更多材质则会更多。这是一个幻灯片的制作方法。相反,你想要做的是实例化这个几何体。实例化是一种告诉你的 GPU 的方法,即使它即将绘制几百个网格,它们实际上都是相同的网格,只是具有不同的变换。因此,系统不是为每棵树都进行单独的绘制调用,而是进行一组绘制调用,并向视频硬件提供一个位置、方向和缩放的列表来绘制它们。这比将每个项目作为单独的项目传递要快得多。

在虚幻中,默认情况下实例化对象的最简单方法是使用植被工具。虽然它通常用于植被,但正如其名称所示,您也可以在许多其他情境中使用它来重复使用对象,比如城市街道上的路灯。您可以在docs.unrealengine.com/en-us/Engine/Foliage上找到有关植被实例化的更多信息。

在场景之外实例化静态网格是一个稍微复杂的话题,但是可以做到,并且如果您正在以程序化方式生成包含大量单独静态网格的角色,这可能是一个好主意。然而,大多数情况下,当您在场景中实例化对象时,请使用植被工具来完成。

本地化蓝图

蓝图已经以惊人的速度进行解释,但通过自动将它们转换为 C++,然后允许系统编译它们,可以使它们变得更快。

要打开此选项,请打开“项目设置 | 项目 | 打包 | 蓝图”,并使用“蓝图本地化方法”选择器选择包含独占本地化。

  • 包含本地化将在编译时将所有蓝图转换为 C++。

  • 独占本地化只会转换那些您设置了本地化标志的蓝图。

如果您使用独占本地化,请通过打开它们的“类设置”来选择要本地化的蓝图,并在其“详细信息 | 打包”面板中打开“本地化”选项。如果您使用包含本地化,则不需要这样做。在这种情况下,每个蓝图都会被本地化:

如果您计划在桌面 VR 上发布应用程序,包含本地化可能是可以的,但如果您计划部署到移动 VR,比如 Oculus Go 或 Quest,最好使用独占本地化来选择要本地化的蓝图,因为包含所有蓝图可能会增加可执行文件的大小。

这是一个比较高级的话题。一般来说,如果您的蓝图在 Tick 事件上做了很多工作,或者总体上做了很多工作,您将会看到一些好处。如果您的蓝图相当简单,无论如何都不会看到差异。由于速度对于 VR 开发非常关键,所以了解这个选项是很好的。

如果您计划这样做,请在项目开发的早期打开本地化,并经常在烹饪的构建上进行测试。本地化非常好,但有时仍可能导致意外的副作用。

总结

在本章中,我们学到了如何使用虚幻的 VR 模式编辑器在 VR 中组合环境,并学习了如何分析和优化场景以查看性能瓶颈所在。

在下一章中,我们将暂时离开在 VR 中构建实时 3D 世界的内容,转而看另一个常见的应用程序——电影和沉浸式摄影。

第九章:在 VR 中显示媒体

在之前的章节中,我们专注于为 VR 创建实时 3D 媒体,并花了很多时间研究玩家角色、界面元素和构建世界。现在,我们要稍微转变一下,探索 VR 的另一个重要应用——在平面屏幕和沉浸式环境中显示电影。

VR 在这方面非常出色。因为在头戴式显示器中可以创建一个几乎无限的空间,用户可以在巨大的虚拟屏幕上体验电影和媒体,没有任何干扰会让他们脱离体验。这些屏幕也可以采用任何形状。除了平面和弯曲屏幕外,还可以在球体中呈现整个环境的照片和电影,使玩家完全沉浸在其中。在本章中,我们将学习如何创建这些内容。

具体而言,我们将涵盖以下主题:

  • 在虚拟屏幕上显示视频

  • 从侧面到侧面和上下视频源显示具有立体深度的视频

  • 在 360 度球形环境中显示媒体

  • 在立体声中显示 360 度媒体

  • 创建交互控件,允许玩家启动、停止和倒回媒体

让我们开始学习如何播放电影吧!

设置项目

对于本章的项目,我们不需要从之前的工作中获取任何内容,所以我们将简单地创建一个具有以下设置的新项目:

  • 空白蓝图模板

  • 移动/平板硬件目标

  • 可扩展的 3D 或 2D 图形目标

  • 使用起始内容(我们将在其中使用一些起始内容)

我们仍然需要适当地设置 VR 的设置,就像我们对每个项目都这样做一样。这是一个备忘单:

  • 项目 | 描述 | 设置 | 在 VR 中启动:是

  • 引擎 | 渲染 | 正向渲染器 | 正向着色:是

  • 引擎 | 渲染 | 默认设置 | 环境遮蔽静态分数:否

  • 引擎 | 渲染 | 默认设置 | 抗锯齿方法:MSAA

  • 引擎 | 渲染 | VR | 实例化立体声:是

  • 引擎 | 渲染 | VR | 轮询遮蔽查询:是

在设置完所有这些设置后,允许项目重新启动。一旦你的项目重新打开,你就可以开始学习虚幻引擎中媒体的工作原理了。

在虚幻引擎中播放电影

我们将从学习如何在虚幻引擎中播放电影和其他媒体开始。当然,要开始,我们需要一个要播放的电影。

视频文件以令人困惑的方式呈现,你应该了解其中的一些事情。

理解容器和编解码器

当人们开始学习视频文件时,最常遇到的困惑是不理解视频文件所包含的容器并不能告诉你它是如何编码的。让我们花点时间来谈谈这个问题。

视频文件包含大量信息,全部打包到一个文件中。有代表视频轨道的图像流。通常还有音频,有时还有字幕,有时还有其他附加信息。所有这些信息都被捆绑在一个称为“容器”的封装格式中。你肯定见过扩展名为.mp4的视频文件。那是 MPEG-4 容器格式使用的扩展名。AVI 是微软的标准容器格式,还有许多其他格式。

但要记住的是,容器格式规定了文件中这些不同信息部分如何组合在一起,但它并不告诉我们视频和音频流实际是如何制作的。仅仅因为你在文件上看到了.mp4扩展名,并不意味着它一定适用于你想要使用它的用途。还有另一个因素需要考虑:编解码器。

单词编解码器压缩器解压缩器两个词的缩写组合。原始状态的视频文件可能会变得非常庞大。有多大呢?让我们来算一下。假设我们有一个 1080p 的视频文件。它的尺寸是 1920 x 1080 像素。每帧有 2073600 个像素。假设我们以 24 位色(每通道 8 位)显示这个视频文件,这允许我们显示超过 1600 万种颜色,大约每帧 50MB。如果我们以每秒 30 帧的速度运行,那么每秒将消耗约 1.49GB 的空间。这样做你会很快就用完空间。

当我们存储视频文件时,我们通过对其进行大量压缩,然后在实时流传输到屏幕时进行解压缩来处理这个问题。这项工作由编解码器来处理。它的压缩组件负责将原始源视频打包成适合存储在光盘上的格式,而解压缩组件则负责解包以便显示。关于视频编解码器的工作原理的讨论可以填满整整一本书,所以我们不会深入探讨这个问题,但你需要知道的是,虽然存在许多编解码器,但并不是所有的编解码器都适用于所有的软件解决方案,也不是所有的编解码器都适用于所有的硬件配置。最常用的编解码器,也是最广泛兼容的,被称为H.264,但还有许多其他编解码器。有些编解码器被设计为广泛使用,而有些则是专门为某些应用程序(如视频编辑)而制作的。值得花一点时间了解这些编解码器。

所以,现在你知道了关于视频文件的一个秘密。容器并不一定告诉你编解码器的信息,你需要了解两者才能知道文件是否能正常工作。(所以下次当你问别人给你什么类型的视频文件时,他们回答给了你一个.mp4时,你会知道他们并没有真正回答你的问题。)一些容器格式只能在特定的操作系统或硬件上工作,而其他一些格式,比如.mp4,几乎可以在任何地方工作。

对于你打算在虚幻引擎中使用的视频文件,通常应选择将它们封装在.mp4容器中,并使用H.264编解码器进行压缩。有关支持的编解码器的更多信息,请查看以下链接:docs.unrealengine.com/en-US/Engine/MediaFramework/TechReference

我们不会在本书中涵盖有关压缩自己的视频文件的内容 - 关于这方面有很多要说的,也有很多关于如何做的信息可以在网上找到。如果你可以访问 Adobe Creative Suite,其中包含的 Adobe Media Encoder 应用程序是一个将视频转换为几乎任何所需格式的优秀工具。如果你需要一个免费的视频编码器,AVC Free 是一个很好且常用的选择。你可以在以下链接找到它:www.any-video-converter.com/products/for_video_free/

寻找用于测试的视频文件

让我们找一个符合这些标准的文件。如果我们导航到“Video For Everybody”测试页面,我们可以找到一个适合测试的视频。转到camendesign.com/code/video_for_everybody/test.html,找到.mp4容器格式的下载视频链接。右键点击链接,选择“另存为...”将big_buck_bunny.mp4视频文件保存到硬盘上。

如果你的系统上还没有安装 VLC 媒体播放器,请从以下链接下载并安装:www.videolan.org/vlc/index.html。实际上,你可以使用任何视频播放器来检查你的文件,但 VLC 是一个很好的工具。它几乎可以播放任何格式的视频,并提供有关正在播放的文件的良好信息。请参考以下步骤:

  1. 在 VLC 中打开刚刚下载的视频文件并播放。

  2. 暂停视频并按Ctrl + J打开其编解码器信息:

您可以在此处看到,该文件使用 H.264 进行编码,并且从其文件扩展名可以看出它使用了.mp4容器。这个文件应该在虚幻的任何平台上都能正常工作。

将视频文件添加到虚幻项目

让我们将此文件添加到我们的虚幻项目中。

对于其他资产类型,您可以使用虚幻编辑器中的“导入”方法将它们添加到项目中,但视频文件不同。要将视频文件添加到虚幻项目中,您必须手动将其放置在名为MoviesContent文件夹的子目录中。

名称和位置很重要。引擎默认会在Content/Movies中查找电影,如果将它们放在其他位置,可能无法正确打包。

  1. 从内容浏览器中,确保您在根Content文件夹中,右键单击创建一个新文件夹。

  2. 如下截图所示,将其命名为Movies

  1. 从 Windows 资源管理器中找到您下载的.mp4文件,并将其移动到项目的“Content/Movies”目录中。(您可以右键单击内容浏览器中的此目录,然后选择“在资源管理器中显示”以导航到该目录。)

创建文件媒体源资产

现在,返回虚幻编辑器,在您的Content/Movies目录中,右键单击并选择创建高级资产 | 媒体 | 文件媒体源以创建一个新的文件媒体源资产。通常更容易使用与其源资产相同的名称命名文件媒体源,因此将其命名为big_buck_bunny是有意义的,因为这是我们即将附加的文件的名称:

打开它并使用省略号(...)按钮选择您放置在Content/Movies目录中的视频文件作为其文件路径:

文件媒体源资产只是一个解析器,允许媒体播放器在磁盘上找到电影。媒体播放器指向文件媒体源,而文件媒体源指向Movies目录中的实际文件。

文件媒体源还提供了一些其他选项:

  • 高级“预缓存文件”选项可用于将整个媒体文件强制加载到内存中并从那里播放。

  • “Player Overrides”列表允许您强制特定播放器在特定平台上解码媒体。除非您确定需要覆盖自动选择,否则请将其保持不变。

还有其他三种媒体源类型,虽然我们不会在这里深入研究它们,但您应该了解它们:

创建媒体播放器

现在我们已经设置好媒体源,让我们创建一个媒体播放器来播放它:

  1. Content/Movies目录中右键单击,选择“创建高级资产 | 媒体 | 媒体播放器”。我们将为所有媒体源使用相同的媒体播放器,因此通用名称如MediaPlayer就可以了。参考以下截图:

创建时,会出现一个新的对话框,询问您是否要创建一个媒体纹理资产来处理视频输出。让它这样做,如下图所示:

我们也可以通过从内容浏览器创建一个媒体/媒体纹理资产来创建它,但这样可以节省一步。

使用媒体纹理

媒体纹理资产显示其绑定的媒体播放器资产中的流媒体视频或图像。如果您打开刚刚创建的媒体纹理,您会看到它绑定到我们刚刚创建的媒体播放器:

如果您的媒体纹理看起来是空白的,不要担心。在关联的媒体播放器上播放了一些内容之前,它不会显示任何内容。

一般来说,您应该保持媒体纹理的属性不变。确保它绑定到您的媒体播放器,但您不太可能需要更改其其他属性。

测试您的媒体播放器

打开刚刚创建的新媒体播放器资产。您应该在可用媒体源列表中看到我们刚刚设置的媒体源文件。选择它并播放以验证它在虚幻引擎中可以播放:

确保为此文件源选择了“打开时播放”选项,并同时打开“循环”选项。

一旦我们验证了视频文件在媒体播放器中播放,让我们将其添加到世界中的一个对象中。

将视频添加到世界中的对象

由于我们在这个项目中包含了起始内容,所以我们的项目启动时不会启动一个空白地图,而是默认启动一个名为“Minimal Default”的简单地图,其中包含一对椅子和一张桌子。我们可以将其作为我们电影播放地图的起点。选择“文件 | 另存为当前...”保存地图,保存为Content/Chapter08/Maps/MoviePlayback2D。(记住,将您的工作放入项目的Content目录的子目录中是个好主意。否则,当您迁移其他内容时,会变得一团糟。)

如果您愿意,可以使用起始内容来布置一个更舒适的剧院或观影室。我们不会在这里涵盖这个内容,但如果您愿意,可以创建一个客厅或电影院场景,或者任何激发您想象力的场景。

我们场景中需要一个屏幕来显示我们的媒体。按照以下步骤创建一个:

  1. 从模式面板中选择“放置 | 基本 | 平面”,并将一个平面拖动到场景中。

  2. 将其位置设置为(X=-730.0, Y=0.0, Z=210.0)(或适合您构建的环境的位置)。

  3. 将其旋转设置为(Pitch=0.0, Yaw=-90, Roll=90)(在编辑器中,这读作X=90.0, Y=0.0, Z=-90.0)。

  4. 将其缩放设置为(X=8.0, Y=4.5, Z=1.0)。通过这样做,我们将屏幕的形状与我们打算播放的 16:9 宽高比的视频相匹配。

现在,我们将把我们的媒体纹理分配给这个平面:

  1. 将我们为媒体播放器创建的媒体纹理拖动到平面上。

  2. 将自动创建一个材质来显示纹理。

这就是将媒体添加到 3D 场景中的方法。分配一个使用媒体纹理作为源的材质或材质实例,并确保媒体纹理指向一个媒体播放器。

使用媒体播放材质

让我们稍微看一下这个材质。打开它。如果您查看其材质属性,您会发现它是一个使用默认光照模型的普通表面材质。这里没有什么特别的。

另一方面,纹理样本很有趣:

这里的重要细节是它的纹理源已设置为我们的媒体纹理,其采样器类型已设置为External。这将允许它实时显示我们的媒体。我们将很快对这个材质进行更多的工作,但现在你可以关闭它。

向我们的媒体播放添加声音

我们还希望能在场景中播放声音。按照以下步骤进行操作:

  1. 选择我们的屏幕演员,点击其详细面板中的“添加组件”按钮。

  2. 添加一个媒体声音组件,并将其媒体播放器属性设置为我们的媒体播放器:

这个媒体声音组件将播放与关联的媒体播放器流式传输的任何音频。默认情况下,它处理立体声音频,但也可以用于单声道或环绕声音源。

现在,我们已经设置好了一切,并在世界中放置了一个带有视频材质和声音组件的对象,让我们让我们的媒体播放器播放测试视频。

播放媒体

我们要从简单的开始,只是在关卡开始时播放电影。稍后,我们将做更多的工作来控制我们的媒体播放器。按照以下步骤开始:

  1. 点击“打开关卡蓝图”,如下图所示:

  1. 创建一个新变量,并将其类型设置为媒体播放器 | 对象引用:

  1. 编译蓝图,并将变量的默认值从None更改为我们刚刚创建的媒体播放器。

  2. Ctrl + 拖动媒体播放器变量到事件图表中。

  3. 找到或创建“事件开始播放”节点。

  4. 从媒体播放器变量中拖动连接器,并调用“打开源”。

  5. 将调用的媒体源设置为我们从电影中创建的文件媒体源:

在 VR 预览中启动它,让我们看看会发生什么:

很好。视频正在播放。让我们花点时间回顾一下我们设置这个的步骤,然后我们将看看如何改进它。请参考以下截图:

媒体播放工作如下:

  1. 您想在引擎中播放的任何媒体都始于Content/Movies中的文件。源电影不会被导入到引擎中,也不会出现在内容浏览器中。

  2. 要在引擎中访问它,您需要创建一个指向磁盘上媒体文件的文件媒体源资产。

  3. 媒体是通过可以通过蓝图调用来控制的媒体播放器对象播放的。

  4. 媒体纹理资源从其关联的媒体播放器中采样视频。这些包含在材料中。

  5. 对象上的 MediaSound 组件会播放与其关联的媒体播放器的音频。这些通常添加到场景中充当屏幕的对象上。

深入了解播放材质

让我们看看我们可以用媒体播放材料做些什么。在这里做出正确的选择完全取决于你想要创建的效果,所以我们将讨论一些你可能想要做的事情,但你需要自己决定它们是否符合你的要求。

我们需要讨论的第一件事是屏幕对光的响应方式。我们为媒体纹理创建的材质使用了默认光照模型。这意味着环境中的光线会像通常一样影响到这个材质。如果你想要的美学效果是这是一个物理屏幕在空间中,那么这可能正是你想要的,但如果你的应用程序的目的是展示媒体本身,你可能不希望有任何杂散光线落在屏幕上并改变其颜色对观众的呈现方式。

让我们看看我们在谈论什么。从模式面板中,将一个点光源拖到场景中,并将其放在屏幕前面:

你会发现光线在屏幕上产生了镜面高光,就像在场景中的其他表面上一样。如果我们关闭场景中的其他灯光,情况会变得更糟。现在,我们屏幕的某些部分变暗了,而其他部分则被剩余灯光的高光遮挡。

如果这就是我们想要的,那就没问题,但如果不是,我们可以通过将材质更改为使用无光照模型,并将视频信号输入到其自发光通道中来进行修正。让我们试试看:

  1. 打开你的媒体材质。

  2. 选择输出节点后,将材质的详细信息 | 材质 | 着色模型从默认光照改为无光照:

  1. 你会发现它的基础颜色输入变为禁用状态。Alt + 单击该输入以断开与纹理采样的连接。

  2. 将纹理采样的结果输入到材质的自发光颜色输入中。

保存材质并返回到场景。现在,因为你的材质使用了无光照模型,它不再受世界中的灯光影响。媒体的显示与其源文件完全一致:

添加额外的控制来调整视频外观

我们还可以使用材质图表来对视频信号的显示进行更多的控制。让我们来看看这个:

  1. 返回到你的材质。

  2. 按住S键并在工作区中单击以创建一个标量参数。将其命名为Brightness并将其默认值设置为1.0

  3. 按住M键并单击以创建一个乘法节点。

  4. 将你的纹理采样的输出乘以刚刚创建的Brightness参数。

  5. 按住S键并单击以创建另一个标量参数。将其命名为Contrast,并将其默认值设置为0.0

  6. 在图表中右键单击并创建一个CheapContrast_RGB节点。

  7. 将乘法节点的结果连接到其 In (V3)输入,并将你的Contrast参数输入到其对比度输入。

  8. 将结果输入到材质的自发光颜色输入中:

正如你所看到的,我们现在创建了一个简单的材质,使用两个标量参数来允许用户控制图像的亮度和对比度。

让我们从这个材质创建一个材质实例,以便我们可以实时看到这些参数的效果:

  1. 在内容浏览器中右键单击你的材质,选择材质实例操作 | 创建材质实例。

  2. 将材质实例拖动到屏幕上以将其分配给对象。

  3. 打开材质实例并尝试更改刚刚创建的BrightnessContrast值。(记住,你需要勾选参数旁边的复选框才能启用修改。)

  4. 将材质的预览网格切换为立方体原语,以便更容易看到你正在做的事情:

这里有很多我们可以做的事情,我们鼓励你去探索和学习更多关于你可以做什么的内容。

现在你已经了解了在虚幻引擎中播放视频的基础知识,让我们开始深入一些针对虚拟现实的工作,并学习如何以立体 3D 的方式显示视频。

显示立体视频

让我们首先创建另一个地图来容纳我们的立体视频屏幕。在你的MoviePlayback2D场景中,点击文件 | 另存为...,将地图保存为MoviePlayback3D

现在,我们需要找到一个立体视频文件进行测试。它们可以在网上找到,但由于我们需要下载自己的文件,所以可能会有些困难。stereomaker.net 在这里有一些示例文件:stereomaker.net/sample/。让我们从这里下载 Hibaya Park 的 Cycling 视频。我们还可以在这里找到更多的示例文件:photocreations.ca/3D/index.html。下载 Bellagio Fountains,Las Vegas,Nevada 3D 2048 x 2048 剪辑。这将为我们提供一个并排立体剪辑和一个上下立体剪辑,我们可以用来进行实验。Hibaya 剪辑包含在一个.AVI容器中,但只要我们在 Windows 上运行剪辑,那就可以工作。要在另一个平台上运行它,我们必须使用诸如 Adobe Media Encoder 或 AVC 之类的应用程序进行转换:

  1. 将这些文件放在你的Content/Movies目录中。

  2. 为每个新的视频文件创建一个文件媒体源资产。同样,通常更容易使用与磁盘上的电影剪辑匹配的文件媒体源名称。

现在,打开你的媒体播放器。你应该在其可用文件列表中看到这些新的剪辑,并且你应该能够播放它们。你应该看到两个并排的帧,代表左右立体图像(确保你首先使用一个并排立体视频进行这个测试-我们稍后会处理上下立体):

现在的关键是将并排或上下的图像解释为立体图像,并将一个帧输入到左眼,另一个帧输入到右眼。

我们将在材质中处理这个。具体来说,我们想要做的是修改我们提供给纹理的 UV 映射的纹理坐标。

UV 映射确定纹理在 3D 空间中如何在网格上对齐。通过操纵我们在材质中应用纹理的纹理坐标,我们可以选择一次只显示纹理的部分。

打开你的媒体播放器材质。

由于我们希望这个材质能够处理单声道视频源,我们将使用一个静态开关参数来在单声道和立体声模式之间切换。这将允许我们将这个材质作为主材质,但设置单独的材质实例来处理我们想要的特定设置。

静态开关参数是有价值的工具,您可以使用它们在主材质中构建很多行为,并从中派生处理特定情况的材质实例。作为额外的好处,当这些材质被编译时,通过静态开关关闭的任何内容甚至不会编译到材质实例中,所以你基本上是免费的。这意味着您可以制作相当复杂的主材质,并且只需通过使用静态开关关闭您不使用的功能来支付您使用的部分。

让我们在材质中添加一个开关,这样我们就可以创建一个立体声路径,而不会弄乱我们的单声道显示:

  1. 在材质编辑图中右键单击并创建一个静态开关参数。将其命名为SplitStereoMedia

  2. 右键单击并创建一个纹理坐标节点,并将其输出连接到开关参数的 False 输入。这将在图中显示为一个 TexCoord 节点。

现在,是时候分割图像了。当图像被渲染到 VR 头盔时,它们会分别渲染两次,并且我们可以利用这个信息来确定显示图像的哪一侧。

显示视频的一半

要分割图像,我们首先需要访问纹理坐标的两个独立轴,以便我们可以单独操作它们:

  1. 拖动纹理坐标输入的输出并从中创建一个 BreakOutFloat2Components 节点。

  2. 按住M键并单击以创建一个 Multiply 节点。

  3. 将 Break 节点的 R 输出连接到 Multiply 节点的 A 输入,并将其 Const B 参数设置为 0.5。

  4. 创建一个附加向量节点,并将乘法器的输出连接到 A 输入,将 Break 节点的 G 输出连接到其 B 输入。

  5. 将附加节点的结果馈入 Split Stereo Media 开关的 True 输入。

  6. 将 Switch 节点的结果馈入 Texture Sample 的 UVs 输入:

我们刚刚做的是将纹理坐标分成两个通道,标记为 R 和 G。然后我们将 R 通道分成一半,同时保持 G 通道不变,然后重新组装向量,并告诉我们的纹理采样器使用结果将图像映射到应用于的对象上。

让我们测试一下看看它的效果:

  1. 打开你场景的级别蓝图。它应该仍然包含对媒体播放器的开源调用。

  2. 将其媒体源切换为你的并排视频。由于我们需要一个地方来设置我们的静态开关参数,我们需要一个新的材质实例来显示我们的并排图像。

  3. 复制我们刚刚调整对比度和亮度时创建的材质实例。

  4. 将其命名为MI_MediaPlayer_SBS或类似的名称,以提醒我们它的用途是显示并排立体媒体。

  5. 打开它并将其 SplitStereoMedia 开关参数设置为 true。

  6. 将其分配给你的屏幕对象。

测试一下。现在你应该只能看到视频的左帧显示在屏幕上。由于我们仍然向每只眼睛显示相同的图像,所以你不会看到任何立体深度。

显示不同的视频半边给每只眼睛

现在,让我们在右眼中显示正确的帧:

  1. 返回到你的材质。

  2. 在材质图中右键单击并创建一个自定义节点。

  3. 在其代码属性中,输入以下内容:return ResolvedView.StereoPassIndex;

  4. 将其输出类型设置为 CMOT Float 1。

  5. 将其描述设置为 StereoPassIndex。

这将创建一个材质表达式自定义节点,当我们渲染左眼时返回 0,当我们渲染右眼时返回 1。我们可以使用这个信息来选择我们为每只眼睛显示的帧的哪一半。

  1. 按住M键并单击以创建一个乘法节点。

  2. 将 StereoPassIndex 的输出传递到其 A 输入,并将其 Const B 参数设置为 0.5:

  1. 现在,按住A键并单击以创建一个加法节点。

  2. 将纹理坐标的乘以 R 通道的结果馈入其 A 输入。

  3. 将乘法立体通道索引的结果馈入其 B 输入。

  4. 将 Add 节点的结果馈入 Append 节点的 A 输入:

再次测试一下。现在,当你在 VR 头盔中查看视频时,你应该能看到图像中的立体深度。

让我们花点时间来理解我们刚刚创建的内容。

当我们分解纹理坐标并修改 R 值时,我们正在修改纹理映射的水平轴。通过将其乘以 0.5,我们将纹理的一半涂抹在网格的整个表面上。我们制作的 Stereo Pass Index 节点返回左眼的值为 0,右眼的值为 1,因此当我们将此值乘以 0.5 时,我们得到左眼的 0 或右眼的 0.5。然后,当我们将此值添加到纹理坐标的 R 分量时,我们将其偏移了一半的宽度。因此,当渲染左眼时,它只是将纹理空间分成一半,而当渲染右眼时,它将其分成一半并偏移一半,显示正确的帧。这就是我们得到立体图像的方式。

显示上下立体视频

修改我们的材质以处理上下立体视频非常简单。我们只需要在 G 通道上进行操作,而不是 R 通道。按照以下步骤开始操作:

  1. 重新打开你的媒体播放器材质。

  2. 创建一个新的静态开关参数节点。将其命名为OverUnderStereo

  3. Ctrl + 拖动 SplitStereoMedia 开关的 True 输入,将其移动到 OverUnderStereo 开关的 False 输入。

  4. 将 OverUnderStereo 开关的输出连接到 SplitStereoMedia 开关的 True 输入:

如果 OverUnderStereo 设置为 False,我们的材质将继续使用我们刚刚设置的并排分割。现在,让我们设置它在设置为 True 时的行为。

  1. 选择包括 BreakOutFloat2Components 节点在内的节点链,一直到 Append 节点,并按下 Ctrl + W 进行复制。

  2. 将 BreakOut 节点的 R 输出直接连接到 Append 节点的 A 输入中。

  3. 将 BreakOut 节点的 G 输出连接到 Multiply 节点的 A 输入。

  4. 将 Add 节点的输出连接到 Append 节点的 B 输入。

我们刚刚交换了一些东西,所以我们现在在垂直轴上执行与之前在水平轴上执行的相同操作。

  1. 将立体通道索引的 Multiply 节点的输出输入到新的 Add 节点的 B 输入中。

  2. 将纹理坐标输入到 BreakOut 节点的输入中。

  3. 将 Append 节点的输出输入到 OverUnderStereo 开关的 True 输入中:

现在,这个材质可以处理单眼、并排立体和上下立体的源。

现在,让我们来测试一下:

  1. 关闭您的材质,并在内容浏览器中复制其中一个已经创建的材质实例。

  2. 确保其 SplitStereoMedia 参数设置为 True,并将其 OverUnderStereo 参数设置为 True。

  3. 将其分配给场景中的屏幕对象。

  4. 打开场景的 Level Blueprint,并将 Open Source 节点上的 Media Source 切换为您的上下立体视频。

进入 VR 预览模式。现在我们应该能够正确播放我们的上下立体视频。

在 VR 中显示 360 度球形媒体

到目前为止,我们在 VR 中已经相当好地复制了 2D 和 3D 传统屏幕,但让我们进一步迈出一步,做一些在现实世界中不容易做到的事情。VR 最引人注目和常见的用途之一是显示环绕观众的沉浸式 360 度视频。即使是单眼,这也可以在用户中产生相当深的存在感,并且可以使用普通相机和拼接软件或专用相机相对容易地制作出球形图像。

显示球形媒体,在大多数情况下,与在平面屏幕上的显示方式完全相同,但当然我们需要新的几何形状来显示屏幕。

寻找 360 度视频

首先,让我们找一个要播放的视频。这里有几个不错的选择:www.mettle.com/360vr-master-series-free-360-downloads-page/

Crystal Shower Falls 链接带我们到一个 Vimeo 页面,允许我们下载视频。对于我们的测试,1080p 版本应该没问题:

  1. 下载视频并将其放置在Content/Movies目录中。

  2. 为您的视频创建一个文件媒体源。

  3. 在媒体播放器中检查它以确保它可以播放。

现在,我们需要一个环境来显示它。

  1. 创建一个新的空级别并将其命名为MoviePlayback2DSpherical(或者任何您喜欢的名称 - 这是您的地图)。

创建一个球形电影屏幕

现在,我们将采取一个普通的球体并修改它,使其法线向内翻转,这样我们就可以在球体内部看到我们的材质:

  1. 从 Modes 面板中,选择 Basic | Sphere 角色并将其放置在场景中。

  2. 查看其详细信息面板,在 Static Mesh 下,点击浏览资源按钮(放大镜)以导航到内容浏览器中的球体静态网格。我们要创建一个副本。

  3. 将 Sphere 静态网格从Engine Content/BasicShapes拖动到项目的Content目录中(Content/Chapter08/Environments是一个不错的选择)。选择“复制到此处”以创建球体的副本。

  4. 将其重命名为MovieSphere

  5. 打开它。

  6. 从您的静态网格编辑器中,选择 Mesh Editing 选项卡。

  7. 通过点击工具栏按钮激活编辑模式。

  8. 拖动以选择所有网格面。

  9. 点击翻转按钮以翻转它们的法线:

  1. 保存并关闭静态网格编辑器。

  2. 在你的关卡中放置一个 MovieSphere 网格的实例,并删除旧的球体。

  3. 将其位置设置为(X=0.0,Y=0.0,Z=0.0),并将其比例设置为(X=200.0,Y=200.0,Z=200.0)。

  4. 选择 MovieSphere,将其 Materials_Element 0 设置为你的 MI_MediaPlayer_Mono 材料实例。

  5. 点击添加组件,添加一个 MediaSound 组件,并将其关联的媒体播放器设置为你的媒体播放器。

现在,就像我们之前的场景一样,我们需要告诉媒体播放器加载我们的媒体。

  1. 在地图的 Level Blueprint 中,创建一个名为MediaPlayer的变量,将其类型设置为 Media Player | Object Reference,编译它,并将其默认值设置为你的媒体播放器。

  2. 使用新的 360 度视频作为其媒体源,通过 Open Source 调用你的媒体播放器变量。

  3. 从你的 Event BeginPlay 中执行此调用。

测试你的场景。现在你应该能够看到电影在你周围播放。

播放立体 360 度视频

现在,我们要为立体 360 度视频做同样的事情。在撰写本文时,立体 360 度视频比其 2D 对应物要少得多,部分原因是它占用了更多的磁盘空间,而且制作起来更加困难,但可以合理地期望事情将继续发展。

与此同时,我们可以在这里找到一个可行的测试文件:www.dareful.com/products/free-virtual-reality-video-sequoia-national-park-vr-360-stereoscopic

像往常一样,下载文件,将其放在 Content/Movies 目录中,创建一个指向它的 File Media Source 资产,并在媒体播放器中测试以确保它在你的系统上播放。

接下来,让我们复制一份我们的 2D 球形测试地图,用于我们的 3D 测试:

  1. 将 MoviePlayback2DSpherical 地图另存为 MoviePlayback3DSpherical。

  2. 选择 MovieSphere 资产,并将其分配的材料更改为你的 OverUnder 材料实例。

  3. 打开级别蓝图,并将 Open Source 节点更改为指向我们的新文件。

让我们来测试一下。我们有球形的 3D 效果,但是我们的立体声是反转的(至少在这个文件中是这样)。所有应该靠近的东西看起来都很远。我们可以通过向主材料添加另一个选项来纠正这个问题:

  1. 打开你的媒体主材料。

  2. 添加一个新的静态开关参数,并将其命名为 FlipStereo。

  3. 将 StereoPassIndex 节点的输出拖动到 FlipStereo 开关的 False 输入中。

  4. 创建一个 OneMinus 节点,将 StereoPassIndex 的输出拖动到其输入中,并将其输出连接到 FlipStereo 开关的 True 输入。

  5. 将 FlipStereo 开关的输出连接到 Multiply 节点:

我们在这里所做的只是设置了一个选项,如果 FlipStereo 为 true,我们将接收到左眼为 1,右眼为 0,而不是相反。

现在,让我们创建另一个材料实例来保存这个选项设置,并将其应用到我们的球体上:

  1. 复制你的 OverUnder 材料实例,并将其命名为 MI_MediaPlayer_OverUnderFlipped 之类的名称。

  2. 打开新的材料实例,并将其 FlipStereo 参数设置为 True。

  3. 将其应用到你的电影球体上:

测试地图-现在你应该能够正确地看到立体图像。

花些时间四处看看。这个视频的比特率相当高,所以你可能会偶尔遇到帧率下降的情况,还有一些透视错误,但立体效果非常引人注目。很明显,随着这项技术的发展,我们将能够做出一些令人惊叹的工作。

控制你的媒体播放器

在结束本章之前,让我们给玩家一些控制媒体播放器的方法。

我们可以在关卡蓝图中完成这项工作,这是我们迄今为止所做的,但如果我们的项目中有多个地图,这不是一个理想的解决方案。我们将不得不将蓝图代码从一个关卡复制粘贴到另一个关卡,并且如果我们更新其中一个,我们必须记住更新其他关卡。这是不好的做法。

一个更好的主意是创建一个包含所有管理媒体播放器所需代码的管理器角色,并将其放入任何需要支持它的关卡中。这样,我们只需编写一次代码,随着更新,效果将在所有地方都可见。让我们这样做。

创建一个 Media Manager

让我们在项目的内容目录中创建一个新的蓝图子目录:

  1. 在其中右键单击,选择创建基本资产 | 蓝图类。

  2. 对于其父类,选择 Actor。

  3. 将其命名为BP_MediaManager

到目前为止,我们一直在使用我们的关卡蓝图来打开媒体播放器上的媒体。我们将首先将该功能移入我们的媒体管理器中:

  1. 打开 BP_MediaManager。

  2. 创建一个名为MediaPlayer的新变量,并将其类型设置为 Media Player | Object Reference。

  3. 编译它并将其默认值设置为您的媒体播放器。

  4. 创建另一个名为FileMediaSource的新变量,并将其类型设置为 File Media Source | Object Reference。

  5. 将 Instance Editable 设置为 True,因为我们需要为每个地图上的它设置不同的值。

  6. 将其类别设置为 Config,以便用户清楚地知道他们必须编辑此值。

现在,我们已经设置好了变量,让我们使用这个角色的 BeginPlay 来加载我们的媒体。首先,我们将重新创建我们在关卡蓝图中已经做过的事情:

  1. 打开 BP_MediaManager 的事件图。

  2. Ctrl + 拖动 MediaPlayer 变量到图表中。

  3. 调用 Open Source。

  4. Ctrl + 拖动您的 File Media Source 变量到图表中。

  5. 右键单击它,选择转换为验证的获取。(如果我们尚未设置文件媒体源,我们不想尝试打开它。)

  6. 将 Event BeginPlay 的执行线拖动到 File Media Source Get 中。

  7. 将 getter 的 Is Valid 执行线拖动到 Open Source 调用的执行输入中。

  8. 将 GET 的输出拖动到 Open Source 调用的 Media Source 输入中。

  9. 右键单击并创建一个 Print String 节点。

  10. 将其 In String 值设置为 Media Manager 的文件媒体源未设置!。

  11. 将 GET 的 Is Not Valid 执行线拖动到我们刚创建的 Print String 上:

现在,如果我们将此角色放置在任何关卡中并设置其文件媒体源,它将开始在项目的媒体播放器上播放该源。如果该关卡中有一个使用指向此媒体播放器的媒体纹理的材质的对象,我们正在播放的内容将显示在那里。

每当您设置一个系统,如果开发人员或用户未能执行某些操作,可能会失败,就像我们的文件媒体源变量一样,在使用验证的获取并打印警告的习惯。如果您训练自己编写能够自行告知错误的代码,您将节省大量的调试时间。

现在,让我们在当前关卡中放置一个 Media Manager,并替换我们在关卡蓝图中所做的工作:

  1. 将 BP_MediaManager 的一个实例拖动到场景中,并将其位置归零。

  2. 将其 Config | File Media Source 设置为之前在场景中播放的任何媒体源。

  3. 打开场景的关卡蓝图,并删除之前放置在 BeginPlay 中的代码。

  4. 测试场景。媒体应该仍然播放,但现在媒体管理器正在处理打开源。

对其他测试关卡重复此操作,以便它们都使用 Media Manager 蓝图。

现在,每个关卡都使用我们的 Media Manager 类的一个实例来操作 Media Player,我们可以更容易地添加适用于所有地方的功能。

现在让我们来做这个。

添加暂停和恢复功能

让我们给用户提供暂停和播放视频的方法:

  1. 打开 BP_MediaManager。

  2. 在其详细面板中,将输入|自动接收输入设置为 Player 0,并将阻止输入设置为 True。

  3. 在其事件图中右键单击,选择输入|键盘事件|空格键创建一个新的键盘事件。

  4. 再次右键单击,选择输入|游戏手柄事件|MotionController(R)触发器创建另一个输入事件。

  5. Ctrl +将媒体播放器变量拖动到图表上。

  6. 拖动其输出并创建一个正在播放节点。

  7. 将一个分支节点连接到正在播放节点的结果。

  8. 将 Space Bar 的 Pressed 执行线连接到分支节点的执行输入。对于触发器输入也是如此。

  9. 从媒体播放器变量中拖动另一个连接器,并为其创建一个暂停节点。

  10. 将分支节点的 True 执行线连接到暂停节点的执行输入。

  11. 从媒体播放器变量拖动另一个连接器(或创建一个重定向节点并从中分支出)并创建一个播放调用。

  12. 将分支节点的 False 执行线连接到播放节点:

我们在这里做了一些值得讨论的事情。

首先,我们使用了与之前不同的捕获键盘和动作控制器输入的方法。到目前为止,我们所做的一切都依赖于项目设置和DefaultInput.ini文件来捕获来自硬件设备的输入并将其重新映射到命名的输入事件。事实上,这仍然是一种更好的方法,但我们想向您展示另一种可能的方法。很多时候,使用直接在蓝图中映射的输入事件原型化系统是有意义的,一旦您的系统工作正常,将它们移入项目设置中,这样更容易为不同的控制器重新映射它们。

还要注意的是,只有因为我们设置了其自动接收输入,这个对象才能够接收输入。否则,默认情况下它不会监听其他设备的输入。

我们在这里做的是查询媒体播放器,看它是否正在播放任何内容,如果是,则暂停它,如果不是,则播放它。

虽然我们不会在这里涵盖它,因为它将成为一个独立的项目,但如果您想创建基于按钮的用户界面并使用小部件交互组件允许用户与控件进行交互,您可以通过使此媒体管理器对象拥有界面并使用按钮事件来管理媒体播放器的行为来实现。

这是一个相当简单的示例,但它演示了您可以与媒体播放器交互的几种方式。您可以查询其状态,控制播放,打开新媒体,甚至为其分配事件,以便在加载媒体完成时响应。

为媒体播放器分配事件

让我们演示一种使用媒体播放器上的事件的方法。我们将关闭媒体播放器的“打开时播放”设置,并改为在打开后让媒体管理器播放文件。这是一个重要的模式,因为大型媒体文件在调用 Open Source 后不会立即准备好播放。根据它们的大小和存储它们的硬盘的速度,它们将需要一段时间来打开,因此在打开文件后,指示媒体播放器监听文件加载完成并开始播放是一个好的做法。

实际上,“打开时播放”设置已经实现了这一点,但对于您来说,了解这种模式是很有价值的,这样您就可以在需要对媒体播放器进行更复杂操作时使用它。

让我们设置它:

  1. 打开您的媒体播放器资源并关闭其“打开时播放”设置。

如果现在测试您的地图之一,您会发现媒体不再播放,直到您点击空格键或拉动触发器才会开始播放。

  1. 打开 BP_MediaManager 并找到在事件 BeginPlay 上进行的 Open Source 调用。

  2. 将一个分支节点连接到其返回值。

如果 Open Source 调用找到要打开的文件并将其打开,则返回 True,否则返回 False。我们只希望我们的媒体播放器在我们知道它实际上正在打开文件时等待文件打开。

  1. 从媒体播放器变量中拖出一个连接器,并选择 Media | Media Player | Bind Event to OnMediaOpened。

  2. 从绑定节点的事件输入中拖出一个连接器,并选择 Add Event | Add Custom Event。

  3. 将其命名为MediaOpened

  4. 从媒体播放器变量中拖出一个连接器,并调用 Play。

  5. 将自定义事件的执行输出连接到 Play 调用的输入:

测试一下。当媒体打开完成后,它应该能够播放。实际上,它的行为与 Play on Open 为 true 时完全相同,但这里有一些重要的事情需要讨论。

大多数函数调用只有在完成它们应该完成的工作后才会继续执行。Open Source 有点不同。这就是所谓的异步任务。当您调用 Open Source 时,执行将立即继续,但任务本身将花费不确定的时间来完成。当打开大文件、访问网络上的 URL 或执行任何其他任务时,您经常会遇到这种情况,您在开始时真的不知道需要多长时间。异步Async)任务在您调用它时启动,然后在将来的某个时间点结束。您调用异步任务的对象几乎总是会在任务完成时抛出某种事件,以便在完成时执行您需要执行的操作。

在媒体播放器对象的 Open Source 任务中,当源完成打开时,将调用 OnMediaOpened 事件。通过将自定义事件绑定到此事件,我们告诉它在媒体完成打开时在蓝图中触发该事件,并在此发生时调用媒体播放器的“播放”方法。

在创建绑定的自定义事件时,最好通过拖出事件连接器并从那里创建自定义事件,就像我们在这个例子中所做的那样。这是因为许多绑定要求它们的绑定事件包含某些输入(这称为签名),如果您只创建一个不匹配所需签名的基本自定义事件,它将不允许您绑定它。如果您直接从事件连接器创建自定义事件,它将自动为您设置正确的签名。在这种情况下,OnMediaOpened 的绑定事件需要传递一个 Opened URL 参数。

这是一个重要的模式,值得学习。视频文件很大,有时对它们进行操作需要时间。了解可以绑定到媒体播放器对象的事件,并确保在任务完成并成功后执行您要执行的操作。

在您的旅行中,您可能会遇到一些开发人员,他们通过在蓝图中添加延迟来处理异步任务。他们会通过试错发现,如果他们延迟调用,那么他们尝试进行的调用将会成功,如果他们立即尝试进行调用,那么调用将会失败,所以他们只是随机设置一个延迟并称之为修复了错误。然而,您不会这样做。这是业余小时的东西,如果他们尝试打开一个更大的文件或其他事情发生变化,它将在以后失败。处理异步任务的正确方法始终是找出任务完成时调用的事件,然后将您需要执行的其他操作绑定到该事件。除非您能够以积极的方式描述为什么延迟是正确的解决方案,否则不要使用延迟来解决问题。正确的解决方案几乎总是一个绑定事件,无论任务需要多长时间都可以正常工作。

您现在已经看到了与媒体播放器对象交互的各种方式的示例。我们已经查询了它的状态,对它进行了调用,并将额外的代码绑定到它的事件上,以便在媒体播放器告诉我们发生了什么时做出响应。媒体播放器还有更多功能,我们鼓励您进行尝试。尝试将事件绑定到其 OnEndReached 上,或者其他可绑定的事件上。尝试使用媒体播放器的 Get Time 和 Duration 调用来创建进度条。您可以做很多事情。

总结

在本章中,我们学到了很多关于在虚幻引擎中播放视频文件的知识。我们了解了一些容器和编解码器的知识,以及如何理解视频文件的内容,然后我们学习了各种播放它们的方式,包括在平面屏幕和球体上播放。我们学习了如何创建材质来显示 3D 视频和 2D 视频,并学习了如何创建媒体管理器类来管理它们的播放。

在下一章中,我们将学习虚幻引擎中多人网络游戏的工作原理。

第十章:在虚拟现实中创建多人游戏体验

在本章中,我们将进入一些更高级的领域。与单人应用程序相比,多人游戏软件的编写要复杂得多。无论如何,要编写成功的多人游戏代码,您必须建立一个清晰的心智模型,了解数据是如何从一台计算机传输到另一台计算机的。好消息是,这正是我们在这里要做的。在本章中,我们将会介绍更多的理论知识,因为如果我们只是简单地引导您完成设置网络应用程序的步骤,那是不会对您有所帮助的。您必须了解网络是如何工作的,才能了解您需要如何构建应用程序。但是不要担心,我们将尝试在理论和实际示例之间进行交替,以便您可以建立对这些内容如何工作的实际理解。

我们还需要明确的是,网络是一个庞大而相当高级的主题。在本章中,我们没有足够的空间来讨论艺术的每一个黑暗角落,但如果您在本章结束时对网络应用程序的组成方式、主要部分以及信息如何最常见地传递有一个良好的理解,那就算是成功了。如果您能以一个相对清晰的状态理解这一点,那么当您进一步了解这个主题时,您将能够很好地理解您所看到的内容。

在本章中,我们将学习以下内容:

  • 与虚幻的客户端-服务器模型一起工作,确保重要的游戏事件发生在服务器上

  • 将角色从服务器复制到连接的客户端

  • 当变量的值发生变化时,自动复制变量并调用函数

  • 创建一个对拥有者玩家而言与其他玩家不同的角色

  • 使用远程过程调用在远程机器上调用事件

让我们开始吧!

测试多人游戏会话

在我们深入讨论网络工作原理之前,让我们先学习如何启动一个多人游戏会话。有多种方法可以做到这一点。最简单的方法是直接从编辑器中启动多人游戏会话,在测试网络复制时,大多数情况下这样做就可以了。对于更全面的测试,或者如果您需要其中一个会话在虚拟现实中运行,您可以启动两个独立的游戏会话并将它们连接在一起。稍后我们将展示如何做到这一点,当我们讨论会话类型时。

从编辑器中测试多人游戏

幸运的是,虚幻编辑器使得在单台机器上从编辑器中设置多人游戏会相当容易。为了进行这个测试,我们将使用“内容示例”项目:

如果您还没有下载“内容示例”项目,请在 Epic Games Launcher 中选择“虚幻引擎”标签下的“内容示例 | 创建项目”来下载。您应该养成始终在系统上安装当前版本的“内容示例”并将其用作参考的习惯。

  1. 打开“内容示例”项目并打开“网络功能”关卡。

  2. 在工具栏的“播放”按钮旁边选择下拉菜单,将“多人游戏选项 | 玩家数量”设置为 2。请参考以下截图:

  1. 选择“新建编辑器窗口(PIE)”以启动一个多人游戏会话,如下图所示(不幸的是,我们不能使用多人游戏选项在单台机器上支持多人虚拟现实会话):

以服务器和客户端身份进行场景探索。注意服务器和客户端之间的差异。我们将在不久的将来更深入地研究这些内容:

在这个例子中,左边的幽灵在服务器上可见,但在客户端上不可见,因为它没有被设置为复制到客户端。

花些时间理解到目前为止我们所描述的每个显示内容在什么情况下告诉你什么,但如果有些东西还不清楚,不要担心——我们将在接下来的练习中更多地利用这些概念。

有关编辑器中多人游戏测试选项的更多信息,请参阅此处的文档:docs.unrealengine.com/en-us/Gameplay/HowTo/Networking/TestMultiplayer

理解客户端-服务器模型

现在我们有一个正在运行的测试,我们可以在谈论下一个概念时进行一些实践。最好保持这个测试关卡打开,并在我们讨论下一个概念时进行探索。

要理解虚幻引擎中的多人游戏玩法是如何工作的,首先需要了解信息如何在连接的游戏会话之间传递,以及对游戏环境进行的更改。没有捷径可走。要成功编写多人游戏代码,必须建立一个清晰的心智模型,了解正在发生的事情,否则你将遇到很多困难。多人游戏很难调试——如果某些东西不起作用,你不能简单地在蓝图中设置断点并跟踪以查看发生了什么。很多时候,你只会知道你认为应该传递到另一台机器的一些信息从未到达那里。如果你花时间了解网络工作原理,当某些事情不像你预期的那样工作时,你会更容易找出问题所在。多人游戏绝对不是你可以靠胡乱尝试来调试的东西。

所以,让我们学习一下虚幻引擎中的网络工作原理。

为了开始思考这个问题,让我们想象一个场景。假设你创建了一个多人射击游戏,有两个玩家加入了一个会话并且都在玩。其中一个玩家瞄准并开火,现在我们需要向两个玩家展示发生了什么。

起初听起来很简单,但实际上并不是这样。

A 玩家正在瞄准,但这是在 A 玩家的游戏实例中发生的。B 玩家的游戏实例如何知道 A 玩家在哪里,更不用说他们在瞄准什么了?A 玩家开火了。B 玩家的游戏实例如何得知这一点?现在,有人需要确定 A 玩家的射击是否击中了 B 玩家的角色。谁来决定射击是否命中?如果 B 玩家的网络连接较慢,关于 A 玩家瞄准位置的信息还没有到达,怎么办?如果两个游戏实例都被允许决定射击是否命中,它们不会达成一致。谁的意见会占上风?

第一个问题的答案——B 玩家的游戏实例如何知道 A 玩家的移动和动作——是通过一种称为复制的过程来处理的。当 A 玩家移动时,他们的角色移动会被复制到 B 玩家的游戏实例中,当 B 玩家移动时,他们的移动会被复制到 A 玩家的游戏实例中。

最后一个问题——谁决定射击是否命中——由服务器处理,值得花些时间来理解这一点。

虚幻引擎使用客户端-服务器模型进行网络管理。这意味着只有一个连接到游戏会话的游戏实例被允许对实际发生的事情做出重要决策。服务器是权威的,而客户端不是。如果服务器和客户端对刚刚发生的事情得出了两个不同的结论,那么服务器的意见将被采用。

在点对点模型中,每个人都是平等的。点对点网络架构相对容易设置,但代价很高:当其中一个连接的对等方与其他对等方不同步时,没有人知道哪个状态实际上是真实的。这对于演示或课堂项目可能没问题,但在玩家真正关心结果的环境中是绝对不可接受的。我们需要毫无疑问地知道游戏及其所有玩家的实际状态,而客户端-服务器模型为我们提供了一种可靠的方法来实现这一点。

以下是实际发生的情况:

  1. 玩家 A 移动,他们的移动被复制到服务器,服务器将他们的移动复制到所有其他连接的游戏实例。

  2. 玩家 B 和其他连接的玩家在他们的游戏会话中看到一个代理,它显示了服务器说玩家 A 的角色所在的位置。

  3. 当玩家 A 瞄准并开火时,玩家 A 的客户端实际上会向服务器发送请求,告诉服务器它想要开火,服务器会进行实际决定是否可以开火。

  4. 如果服务器确定玩家 A 有弹药,处于正确状态,或者符合游戏规则的要求,它会开火并告诉所有连接的游戏实例。

  5. 服务器还收到了玩家 B 的复制移动,因此它具有确定玩家 A 的射击是否命中的所需信息。

  6. 如果服务器确定它确实发生了,它会减少玩家 B 的生命值或执行其他必要的操作来响应此事件,并告诉所有连接的客户端玩家 B 被击中。

  7. 然后,每个客户端更新其本地状态信息,播放击中动画和效果,并更新其用户界面:

顶部面板表示服务器的视图,而底部面板表示客户端的视图。添加了线条以指示状态可能发生变化并需要复制到客户端的对象。

虚幻引擎的网络架构非常高效,这就是为什么像《堡垒之夜》这样的游戏可以在大量玩家同时连接时实时运行的原因。这其中有很多原因,其中许多是作为开发人员在您的控制之下的。我们将在本章后面深入介绍其中一些重要原因。

现在,让我们仔细看一下几个重要的概念。

服务器

术语“服务器”指的是多人环境中的“网络授权”。您会听到这些术语互换使用。技术文档往往会使用术语“网络授权”,因为这更准确地描述了它的实际含义,而您阅读的其他大部分材料将称其为“服务器”。两者指的是同一件事。

当您的网络应用程序出现问题时,很大一部分时间是因为您允许客户端尝试更改游戏状态,而实际上它需要请求网络授权来进行更改。

架构的工作方式如下:服务器托管游戏,并允许多个客户端连接并相互通信数据。通信发生在客户端和服务器之间,客户端几乎不直接与其他客户端通信:

当玩家执行操作时,关于玩家正在做什么或想要做什么的信息从该玩家的客户端发送到服务器。服务器验证此信息并做出响应,告诉连接的客户端它的决定。

例如,如果您在多人游戏中移动您的玩家角色,实际上您根本没有在本地移动您的角色。相反,您的客户端将告诉服务器您想要移动,然后服务器将确定您的移动方式,并将您的新位置复制回您的客户端和其他连接的客户端。

对于看似直接的客户端之间的消息也是如此。如果你向另一个客户端发送聊天消息,实际上是将它发送到服务器,然后服务器决定哪个客户端或一组客户端应该接收它。

正如我们之前提到的,服务器是负责维护多人游戏会话的实际权威状态的网络授权机构。这个“权威”的概念是关于网络的最重要的概念之一,当我们到达实际的例子时,你会看到我们几乎在做任何事情时都会检查权限。如果你清楚地知道谁应该被允许做出改变,并检查确保任何改变确实是由被允许的实体进行的,你就会领先一步。

一个好的经验法则是:如果其他玩家关心这个变化,它就属于服务器。如果没有其他人关心,就在本地进行。所以,如果你正在播放一个对游戏无关紧要的视觉效果,就不要在服务器上运行它,但如果你正在改变玩家的生命值或移动他们,就在服务器上进行,因为其他人都需要同意这个改变。

除了确保游戏中的任何重要事物一次只有一个描述之外,还有另一个重要原因要维护一个单一的网络授权,那就是确保玩家不能轻易通过修改客户端来作弊。当重要决策留给服务器时,服务器可以相对容易地覆盖黑客客户端上的结果。如果玩家想要开火,确保他们的客户端告诉服务器,让服务器决定他们是否有足够的弹药并且被允许开枪。不要直接在客户端上处理重要的游戏事件。只有在服务器允许的情况下才让它们发生。不要相信客户端。

监听服务器、专用服务器和客户端

在虚幻网络环境中,有三种基本类型的游戏会话:两种类型的服务器和一种客户端类型。

监听服务器

当你运行一个监听服务器时,你的机器充当游戏会话的主机和该游戏会话的授权机构,但它也在运行一个客户端。如果你曾经在虚幻中设置过一个网络游戏,可能看起来好像你正在运行一个点对点会话,但实际上是这样的。监听服务器对于本地玩家来说几乎是看不见的-它看起来不像是一个单独的运行进程,但实际上它与本地客户端是分开的,就像它在另一台机器上一样。

以下命令行参数将使用未烹饪的编辑器数据启动一个监听服务器:

UE4Editor.exe ProjectName MapName?Listen -game

通常,使用这些命令的最简单方法是创建包含参数的快捷方式,或者编写一个简单的.bat 文件。

以下的.bat 文件将使用 Content Examples 项目的 Network_Features 地图启动一个监听服务器:

set editor_executable="C:\Program Files\Epic Games\UE_4.21\Engine\Binaries\Win64\UE4Editor.exe"
set project_path="D:\Reference\UE4_Examples\ContentExamples\ContentExamples.uproject"
set map_name="Network_Features"

%editor_executable% %project_path% %map_name%?listen -game -log -WINDOWED -ResX=1280 -ResY=720 -WinX=32 -WinY=32 -ConsoleX=32 -ConsoleY=752

在这个例子中,我们设置了可执行文件位置、项目路径和地图名称的变量,只是为了使文件更容易阅读和编辑。我们还打开了日志,并明确设置了窗口大小和位置,以便更容易看到正在发生的事情,并在屏幕上适应其他会话。

专用服务器

专用服务器在同一会话中没有运行客户端。它不接受输入或渲染输出,因此可以进行优化,以比监听服务器更便宜地运行。由于专用服务器比完整的游戏客户端要小得多,因为它们不需要包含任何将呈现给玩家的内容,所以可以在单台机器上容纳许多个专用服务器进行托管。现有的游戏可执行文件可以被告知将自己作为专用服务器运行,或者开发人员可以选择编译一个专用服务器的单独可执行文件,这可以进一步防止作弊,并且可以使可执行文件在磁盘上的占用空间更小。

这个命令将使用编辑器数据启动一个专用服务器:

UE4Editor.exe ProjectName MapName -server -game -log

请注意,我们选择为此会话打开日志。这是因为专用服务器不会打开渲染窗口,所以一个可见的日志对于了解它在做什么是至关重要的。

我们可以修改前面的.bat 文件来启动一个专用服务器:

set editor_executable="C:\Program Files\Epic Games\UE_4.21\Engine\Binaries\Win64\UE4Editor.exe"
set project_path="D:\Reference\UE4_Examples\ContentExamples\ContentExamples.uproject"
set map_name="Network_Features"

%editor_executable% %project_path% %map_name% -server -game -log

在这个例子中,我们用-server 参数替换了?listen 指令,当然我们也不需要任何窗口放置规格,因为专用服务器不会打开游戏窗口。

客户端

客户端是网络应用程序和玩家之间的联系点。如果我们使用监听服务器,客户端可能在与服务器相同的系统上运行,或者如果连接到远程主机或专用服务器,则完全独立于服务器。客户端负责接受玩家的输入,通过远程过程调用RPC)将输入传递给服务器,并通过复制从服务器接收有关游戏状态的新信息。

以下命令将启动一个客户端:

UE4Editor.exe ProjectName ServerIP -game

请注意,在上面的示例中,ServerIP是您要连接的服务器的 IP 地址。如果您连接到在您自己的机器上运行的服务器进行测试,则默认的主机地址127.0.0.1将连接到在本地机器上运行的服务器。

这个.bat 文件将启动一个连接到同一台机器上运行的服务器的客户端:

set editor_executable="C:\Program Files\Epic Games\UE_4.21\Engine\Binaries\Win64\UE4Editor.exe"
set project_path="D:\Reference\UE4_Examples\ContentExamples\ContentExamples.uproject"

%editor_executable% %project_path% -game 127.0.0.1 -log -WINDOWED -ResX=1280 -ResY=720 -WinX=1632 -WinY=32 -ConsoleX=1632 -ConsoleY=752

同样,-log 和窗口大小参数完全是可选的-如果您设置快捷方式以使窗口在启动时互不干扰,那么测试多人会话将更加容易。

现在我们已经进行了一些初步的实验并讨论了一些基本的想法,让我们设置我们自己的测试项目,这样我们就可以进行自己的实验了。

测试多人虚拟现实

要在虚拟现实中测试多人游戏,通常需要在网络上有两台单独的 PC。有时可以在单台机器上测试多人虚拟现实,但是某些虚拟现实头戴设备驱动程序会在第二个应用程序启动时自动发送退出信号给正在运行的 3D 应用程序。

从 Unreal 4.21 开始,HTC Vive 插件会在第二个插件启动时自动关闭现有的 Unreal 会话。(执行此操作的代码位于FSteamVRHMD::OnStartGameFrame()中,但不幸的是,已安装的二进制文件的用户无法轻松更改此行为。)Oculus HMD 插件不会自动退出现有会话,因此如果您使用 Oculus Rift,则可能能够在单台机器上测试多人游戏,但如果您使用 Vive,则需要两台 PC。

如果你想试一试,只需在任何启动字符串中添加-vr关键字。

一个服务器启动字符串看起来会像这样:

%editor_executable% %project_path% %map_name%?listen -game -vr -log -WINDOWED -ResX=1280 -ResY=720 -WinX=32 -WinY=32 -ConsoleX=32 -ConsoleY=752

而且,客户端启动字符串看起来会像这样:

%editor_executable% %project_path% -game -vr 127.0.0.1 -log -WINDOWED -ResX=1280 -ResY=720 -WinX=1632 -WinY=32 -ConsoleX=1632 -ConsoleY=752

当然,如果你想在单台机器上进行测试,只需设置一个会话一次使用 VR。

因为对许多用户来说,使用单台机器测试多人虚拟现实是不切实际的,所以我们将在大部分时间内以 2D 方式运行我们的多人示例,以便您可以在一个可以合理支持测试的环境中学习这些概念。然而,我们仍然会讨论一些特定的事情,您需要做一些特定的事情,以使玩家角色的动画对头戴式显示器和动作控制器的移动做出适当的响应,这样您就可以在多人虚拟现实中有一个良好的起点。

设置我们自己的测试项目

与上一章一样,我们将从创建一个带有以下设置的干净项目开始:

  1. 空白的蓝图模板

  2. 移动/平板硬件目标

  3. 可扩展的 3D 或 2D 图形目标

  4. 没有起始内容

像往常一样,这是我们的项目设置备忘单:

  1. 引擎|渲染|前向渲染器|前向着色:True

  2. 引擎|渲染|默认设置|环境光遮蔽静态分数:False

  3. 引擎 | 渲染 | 默认设置 | 抗锯齿方法:MSAA

  4. 引擎 | 渲染 | VR | 实例化立体声:True

  5. 引擎 | 渲染 | VR | 循环 Robin 遮挡查询:True

然而,为了简化学习这个具有挑战性的主题,我们将以不同的方式设置一个值:

  • 项目 | 描述 | 设置 | 在 VR 中启动:False

在设置完所有这些设置后,允许项目重新启动。

添加一个环境

让我们给自己一些环境资产来玩,这样我们就不会一直看着一个空的关卡了。

打开你的 Epic Games 启动器,找到 Infinity Blade: Ice Lands 包。将其添加到你的项目中。

如果你无法向项目添加内容包,因为它说它与你当前的项目版本不兼容,你通常可以通过将内容包添加到一个使用内容包允许的最高版本构建的项目中,然后将其资产迁移到你的新项目中来解决这个问题。所以,例如,如果我想将 Ice Lands 添加到一个 4.21 项目中,而启动器告诉我不能这样做,因为 Ice Lands 只与 4.20 兼容,我可以将内容添加到一个 4.20 项目中,然后将其迁移到 4.21 项目中。大多数情况下,这样做是有效的。

这可能需要一些时间。一旦这些资产被添加,打开你的项目。我们将通过创建一个新的游戏模式来为多人游戏会话做好准备。

创建一个网络游戏模式

还记得我们很久以前提到过游戏模式负责游戏规则吗?在多人游戏中,这变得更加重要,因为如我们所提到的,重要的游戏事件只应该发生在服务器上。如果你将这两个考虑因素结合起来,那么当多人游戏进行时,只会有一个游戏模式,并且它存在于服务器上。

对于开发者来说,这意味着如果你编写直接与游戏模式交互的代码,在单人游戏会话中测试时会运行良好,但在多人游戏中测试时会失败,因为客户端上没有游戏模式。这让许多新的多人游戏开发者感到困惑,所以现在是一个好时机来快速了解虚幻的网络框架,并理解不同对象的位置。

网络上的对象

在思考多人游戏框架中的对象时,你可以将它们看作占据四个不同的领域:

  • 仅服务器:对象仅存在于服务器上。

  • 服务器和客户端:对象存在于服务器和每个客户端上。

  • 服务器和拥有客户端:对象存在于服务器和拥有它们的客户端上,但在其他客户端上不存在。

  • 仅拥有客户端:对象仅存在于拥有它们的客户端上。

请参考以下截图:

虽然这一点乍一看可能像是一个学术问题,但你真的需要理解这一点。在你早期的网络职业生涯中,你会尝试与一个你认为它存在的对象进行通信,但实际上它并不在你认为的位置,因为在单人游戏中你从来不需要考虑这个问题。在多人游戏中,它们并不在同一个空间中,你需要学会它们在哪里。

让我们换个角度来看:

基于 Cedric Neukirchen 出色的多人网络手册的图表,可以在这里找到:http://cedric-neukirchen.net/2017/02/14/multiplayer-network-compendium/

在上面的图表中,你可以看到以下内容:

  • 服务器拥有游戏模式,没有客户端可以访问它。

  • 服务器和每个连接的客户端都可以看到游戏状态。只有一个这样的状态。

  • 服务器和每个连接的客户端可以看到每个客户端的玩家状态。

  • 服务器和每个连接的客户端可以看到每个客户端的角色。

  • 服务器可以看到每个连接的客户端的玩家控制器,但客户端无法看到其他客户端的玩家控制器。

  • HUD 和 UI 元素仅存在于客户端上,其他人都不知道它们。

让我们简要地讨论一下每个对象在多人游戏中的作用。

仅服务器拥有的对象

正如我们刚才提到的,游戏模式仅存在于服务器上。它运行游戏并是正在进行的游戏的唯一权威。按设计,客户端无法直接访问游戏模式。我们已经看到游戏模式负责决定为游戏创建哪些对象类。在多人游戏中,游戏模式通常承担额外的责任,例如选择玩家生成到哪个队伍,他们的角色出现在哪里,以及比赛是否准备好开始或结束。

游戏模式还适用并执行游戏规则。假设我们的游戏地图被分成了几个区域,这些区域可以变成危险区,如果玩家留在其中,就会受到伤害。游戏模式将负责确定哪个区域变得危险,以及何时发生。

然而,这引发了一个问题。如果游戏模式仅存在于服务器上,客户端无法看到它,那么客户端如何知道哪些区域是危险的,哪些不是呢?

这就是下一层对象的作用-它们在客户端和服务器上都存在。

服务器和客户端对象

当客户端需要获取游戏状态的信息时,它们从游戏状态中获取,该状态由服务器拥有但复制给客户端。我们还没有真正讨论过复制,所以现在你可以将其视为从服务器发送到连接的客户端的对象副本。游戏模式从游戏状态中读取信息并写入信息,服务器通过复制将更新后的游戏状态的副本发送给所有连接的客户端。

回到我们之前的例子,如果游戏模式仅在自身的变量中存储有关哪些区域是危险的信息,那么没有人会知道它。如果游戏模式将此信息存储在复制给客户端的游戏状态上,客户端可以从游戏状态中读取此信息并做出响应。

如果我们的游戏模式还要更新每个玩家的分数,我们应该把这些信息放在哪里?当然,我们知道它不应该放在游戏模式中,因为在那里没有人能看到它。我们可以将其放在游戏状态中,并为每个玩家维护一个分数数组,但有一个更好的地方可以存放这些信息。游戏状态为每个连接的客户端维护了一个玩家状态对象的数组。这是一个理想的位置,可以存放适用于单个玩家但其他玩家需要了解的信息,比如玩家的分数。

我们已经熟悉了角色扮演的工作-这些是玩家在虚拟世界中的化身。它们在服务器上维护并复制到客户端,因此其他玩家可以看到它们的移动和其他状态信息。

服务器和拥有客户端的对象

我们之前已经看到,玩家控制器负责管理来自玩家的输入和显示给玩家的输出。它拥有摄像机和 HUD,并处理输入事件。多人游戏中的每个连接的客户端都有一个与之关联的玩家控制器,并且可以像在单人游戏会话中一样访问它。服务器也知道每个客户端的玩家控制器的情况,但客户端无法看到其他客户端的玩家控制器的任何信息。

仅拥有客户端的对象

最后,UI 显示小部件等对象仅存在于适用于它们的客户端上。服务器不知道也不关心它们,其他客户端也一样。这些是纯粹的本地对象。

我们知道,我们给你提供了很多理论知识,但正如我们所提到的,这很重要。如果你花一点时间来理解所描述的结构,编写多人应用程序时就会少些困惑。

话虽如此,让我们回到一些实际操作。

创建我们的网络游戏模式

我们将使用此登录来在不同的生成点生成不同的玩家。在继续之前,让我们进入地图并添加第二个玩家起始对象:

  1. 从模式面板中,选择“基本 | 玩家起始点”,将其拖放到地图的某个位置,并保存地图:

记得使用P键来验证你的生成点是否在一个具有有效导航网格的区域上。(我们现在实际上不需要导航网格,但这是验证你选择的位置的地板碰撞是否良好以及是否在游戏区域内的好方法。)

在地图的另一端添加了第二个玩家起始点。

现在,让我们创建一个游戏模式来管理我们的网络游戏:

  1. 打开你的新项目后,在内容浏览器中创建一个目录。将其命名为Multiplayer(或者你喜欢的其他名称)。

  2. 在此目录中创建一个蓝图子目录。

  3. 右键单击创建基本资产 | 蓝图类 | 游戏模式基类。将其命名为BP_MultiplayerGameMode

如果你查看 Content Examples 项目的 BP_GameMode_Network,你会看到它在事件 OnPostLogin 中实现了自己的玩家起始点选择。你不需要这样做。原生的 GameModeBase 类已经为你做了这个。如果你确实想要为选择玩家起始点创建特殊规则(例如按团队选择),正确的方法是重写 ChoosePlayerStart 函数。要做到这一点,选择“函数 | 覆盖 | 选择玩家起始点”,并在生成的图表中放入任何你想要的逻辑。

  1. 打开设置 | 项目设置 | 项目 | 地图和模式,并将默认游戏模式设置为我们的新游戏模式。

让我们来测试一下:

  1. 选择工具栏“播放”按钮旁边的下拉菜单,将“Multiplayer Options | Number of Players”设置为 2。

  2. 从播放按钮中选择“在新窗口中播放此级别”,以启动一个双人测试。

你应该看到一个玩家生成在原始生成点,另一个玩家生成在你刚刚创建的新生成点。

创建一个网络客户端 HUD

让我们为客户端添加一个简单的 HUD,以便向用户显示有关游戏的信息。同样,如果我们计划此游戏仅在 VR 中运行,我们将不使用 HUD 对象,而是将其构建为附加小部件的 3D 形式。我们之所以这样做,是因为在本章中我们有很多内容要涵盖,我们希望将其集中在网络上。

虽然我们将专注于为本章创建 2D HUD,但我们可以借此机会添加一些安全性,以确保我们不会尝试在 3D 空间中显示 2D 元素。

让我们创建一个新的 HUD 来使用:

  1. 从项目的蓝图目录中,右键单击“创建基本资产 | 蓝图类”,展开“所有类”扩展器,并选择 HUD 作为您的类。请参考以下截图:

  1. 点击“选择”按钮来创建它。

  2. 将其命名为BP_MultiplayerHUD

  3. 打开我们的新游戏模式,并将此 HUD 设置为其 HUD 类。

为我们的 HUD 创建一个小部件

现在,让我们创建一个小部件来显示在我们的 HUD 上:

  1. 右键单击或选择“添加新建 | 用户界面 | 小部件蓝图”,并将生成的小部件命名为WBP_NetworkStatus

  2. 打开其设计面板,并将一个文本块拖放到面板的左下角。

请注意,因为我们在这种情况下创建了一个 2D 界面,我们没有指定显式的屏幕大小;相反,我们允许它填充整个屏幕。正如你在之前的 UI 工作中所记得的,当你构建一个用于 3D 使用的小部件时,你会想要指定其大小。

  1. 将文本块的锚点设置为左下角。

  2. 将其 Position X 设置为 64.0,将其 Position Y 设置为-64.0。

  3. 将其对齐设置为 X=0.0,Y=1.0。

  4. 将其命名为txt_ClientOrServer

  5. 点击其 Content | Text 条目旁边的 Bind 按钮以创建一个绑定,并选择 Create Binding:

在生成的函数图中,我们将检查此小部件的拥有玩家控制器是客户端还是服务器,并相应地设置此小部件的文本:

  1. 创建一个 Get Owning Player 节点。

  2. 从其返回值中拖出生成的玩家控制器引用并调用 Has Authority。

  3. 从 Has Authority 调用的结果创建一个 Select 节点。

  4. 将 Select 节点的返回值拖入函数的返回值中。

  5. 在 Select 节点的 False 输入中输入Client,在 True 输入中输入“Server”:

让我们在这里谈论一些事情。

还记得我们将服务器描述为“网络权限”吗?现在,Has Authority 检查正在测试所拥有的玩家控制器是否驻留在服务器上。在编写网络代码时,您经常需要测试权限,因为您经常需要根据代码是在客户端还是服务器上运行而采取不同的操作。将此作为一个非常重要的概念记在心中。检查权限是您指定哪些行为发生在服务器上,哪些行为发生在客户端上的方式。

还要注意 Get Owning Player 节点上的闪电和屏幕图标。在单人游戏应用程序中,我们不关心这个图标,但在多人游戏中很重要。该图标表示所调用的函数仅在客户端上发生,不能在服务器上使用。在这种情况下,这是可以的。如果您回想一下之前的图表,HUD 及其拥有的小部件仅存在于客户端上,因此这个仅限客户端的调用将起作用。它返回的玩家控制器引用可以存在于客户端或服务器上,这就是为什么我们将从 Has Authority 检查中获得有效结果的原因。

在思考时,请参考网络框架图。

将一个小部件添加到我们的 HUD 中

现在,我们将把这个小部件添加到我们的 HUD 中:

  1. 打开 HUD 的事件图,并找到或创建一个 Event BeginPlay 节点。

  2. 创建一个 Is Head Mounted Display Enabled 节点。

  3. 使用其结果创建一个分支。

  4. 从分支节点的 False 输出中拖出并创建一个 Create Widget 调用。

  5. 将其类设置为刚刚创建的小部件蓝图。

  6. 创建一个 Get Owning Player Controller 节点,并将其结果馈入 Create Widget 节点的 Owning Player 输入。

  7. 拖出 Create Widget 节点的返回值并调用 Add to Viewport:

我们刚刚做的是检查我们是否在 VR 中,如果不是,则创建一个网络状态小部件的实例并将其添加到 HUD 中。

如果您想要在 VR 中实现一个 3D 小部件,这将是一个合理的地方。您可以以与之前相同的方式创建一个 3D 小部件,并使用 Get Owning Pawn 调用来获取玩家 Pawn 并将小部件的包含的 actor 附加到它上面。同样合理的是,我们可以像之前一样在 Pawn 上创建一个 3D 小部件,并在 Is Head Mounted Display Enabled 检查返回 false 时隐藏或销毁它。

让我们来测试一下。您应该会看到一个标记为“服务器”的会话,另一个标记为“客户端”的会话。

现在,尝试在播放菜单上选中“运行专用服务器”复选框并再次运行它:

这次,您会看到两个会话都标记为客户端。这里发生的情况是,一个专用服务器以不可见的方式生成,并且两个玩家都作为客户端连接到它。在运行此测试之后,再次取消选中“运行专用服务器”。我们将需要一个可见的服务器和客户端来进行下一部分的操作。

网络复制

现在我们已经谈了一些关于服务器和客户端的内容,让我们更多地了解信息是如何在它们之间传递的。

首先,也是最重要的概念是复制。复制是一个过程,通过该过程,一个存在于一个系统上的角色或变量值被传递到另一个连接的系统,以便在那里也可以使用。

这带来了一个重要的观点:只有你选择复制的那些项目才会被传递给其他连接的系统,这是有意的。虚幻引擎的网络基础设施被设计为高效,而保持这种效率的一个主要方法,特别是如果你有很多玩家,就是只发送你绝对需要通过网络发送的信息,并且只发送给那些实际上需要接收它的人。想想像《堡垒之夜》这样的大规模游戏。如果每个连接的玩家的每个数据都被发送给其他玩家,它根本无法运行。虚幻引擎可以处理非常庞大的玩家人数,它通过让你作为开发者完全控制什么被复制以及复制给谁来实现这一点。然而,这种权力也带来了责任。如果你不告诉一个角色或变量进行复制,它就不会复制,你在连接的机器上也看不到它。

让我们从一个简单的例子开始,看看这是如何工作的。

创建一个复制的角色

假设我们想使用旗帜来标记游戏中的某个东西,并且所有玩家都能看到它的位置很重要。

我们可以从创建一个角色开始,所以让我们首先这样做:

  1. 在你的Blueprints文件夹中,右键选择创建基本资产 | 蓝图类 | 角色。我们可以将我们的角色命名为BP_ReplicatedFlag。打开它。

  2. 选择添加组件 | 静态网格。

  3. 将组件的静态网格属性设置为/Game/InfinityBladeIceLands/Environments/Ice/Env_Ice_Deco2/StaticMesh/SM_Env_Ice_Deco2_flag2

  4. 选择静态网格组件后,选择添加组件 | 骨骼网格,以创建附加到旗杆静态网格的子骨骼网格。

  5. 将组件的骨骼网格属性设置为/Game/InfinityBladeIceLands/Environments/Ice/EX_EnvAssets/Meshes/SK_Env_Ice_Deco2_BlowingFlag3

  6. 将骨骼网格组件的位置设置为(X=40.0,Y=0.0,Z=270.0),并将其缩放设置为(X=1.8,Y=1.8,Z=1.8)。

  7. 将静态网格组件拖到根组件上,并将其设置为新的根。

  8. 添加一个点光源组件,并将其位置设置为(X=40.0,Y=0.0,Z=270.0),这样我们的旗帜就会显眼起来。

仅在服务器上生成一个角色

现在,让我们将旗帜生成到关卡中,但只在服务器上生成:

  1. 从你的模式面板上,拖动一个目标点到地图上的某个位置。将其命名为FlagSpawnPoint

  2. 打开你的关卡蓝图,在 FlagSpawnPoint 仍然被选中的情况下,右键单击事件图表以创建对它的引用。

  3. 找到或创建一个事件 BeginPlay 节点。

  4. 从这个节点拖动执行线,并创建一个 Switch Has Authority 节点。

  5. 从 Switch Has Authority 节点的 Authority 输出中拖动执行线,并创建一个 Spawn Actor from Class 节点。

  6. 将其类设置为我们刚刚创建的 BP_ReplicatedFlag 角色。

  7. 从引用中拖动一个输出到你在关卡中的旗帜生成点,并调用 Get Actor Transform。

  8. 将变换输入到生成节点的生成变换中:

运行它。你会看到旗帜在服务器上生成,但你在客户端上看不到它。让我们通过讨论来看看为什么会这样。

在上面的截图中,我们在BeginPlay上做的第一件事是检查我们是否有权限。再次强调,网络权限只是服务器的另一个术语。如果我们有权限,意味着我们在服务器上运行,我们会在我们提供的位置生成旗帜。如果我们不在服务器上,我们就不会生成它,这就是为什么我们在客户端视图中没有看到它的原因。

这是一个重要的模式要记住。当我们谈论确保重要的游戏事件仅在服务器上发生时,这就是您要做的。检查是否具有权限,并仅在具有权限时执行操作。

将角色复制到客户端

当然,在这种情况下,我们也希望在客户端上看到这个角色,但目前我们不能,因为它只存在于服务器上。让我们通过将其变成一个复制角色来改变这一点:

  1. 打开我们的旗帜角色蓝图,在其详细信息|复制部分中,将 Replicates 设置为 true:

再次进行测试。现在,我们也在客户端上看到了标志。

通过指示该角色应该复制,我们现在告诉服务器将生成的对象发送给所有连接的客户端。您可能已经注意到,在测试时,您可以看到其他玩家的位置表示为一个灰色的球体漂浮在空间中。这是因为我们当前使用的默认 pawn 类也设置为复制。(如果您有兴趣在源代码中看到这一点,请打开<您的引擎安装位置|\Engine\Source\Runtime\Engine\Private\DefaultPawn.cpp,您将看到构造函数中的bReplicates设置为 true。)

复制一个变量

让我们进一步思考一下,假设我们在旗帜上放置的这个点光源对我们的游戏很重要。如果是这样的话,我们需要确保只有服务器改变其值,并且所有客户端都可以看到该值。这意味着我们需要在改变之前确保我们有权限,然后将该更改复制到连接的客户端。

  1. 打开旗帜的蓝图,在变量部分添加一个名为bFlagActive的布尔变量。

  2. 编译并保存蓝图。

  3. 在事件图中,在事件 BeginPlay 上,添加一个 Switch Has Authority 节点。

  4. 从 Authority 执行行中,Alt +拖动bFlagActive的 setter 并将其设置为 False。

  5. 创建一个 Set Timer by Event 节点,并将其连接到您的bFlagActive setter。

  6. 将其时间设置为 3.0,并将其循环属性设置为 True。

  7. 创建一个自定义事件,并将其命名为ToggleFlagState

  8. 将计时器的红色连接器(顺便说一下,这被称为事件委托)连接到自定义事件。

  9. Alt +拖动另一个bFlagActive的 setter 到图表上,并将其连接到 ToggleFlagState 事件。

  10. Ctrl +拖动bFlagActive的 getter 到图表上。

  11. 从其输出创建一个 Not Boolean 节点,并将其结果连接到 setter 的输入:

我们刚刚做的是,如果我们在服务器上,初始化bFlagActive变量,然后设置一个循环计时器,每三秒翻转其值。

您有两种可用的 Set Timer 事件类型。您可以设置定时器在触发时调用函数的名称,或者调用事件。如果您在事件图中工作,直接将事件连接到定时器的委托连接器通常更可读。如果您在函数内部工作,其中事件对您不可用,请改为按名称调用函数。

现在,我们需要找到一种方法来查看标志的状态变化:

  1. 找到或创建事件 Tick 节点。

  2. 将对点光源的引用拖动到图表上。

  3. 创建一个 Set Intensity 节点,并在点光源上调用它。

  4. Ctrl +拖动bFlagActive变量的 getter 到图表上。

  5. 拖出其结果并创建一个 Select 节点。

  6. 将 Select 节点的返回值连接到 Set Intensity 节点的 New Intensity 输入。

  7. 将选择节点的 False 值设置为 0.0,将 True 值设置为 5000.0:

正如您可能记得的那样,我们不喜欢在 tick 事件上轮询值。这是一种浪费和通常不规范的技术。别担心,我们马上就会设置一种更好的方法来做到这一点。

与此同时,让我们进行测试。

我们可以在服务器上看到我们的灯开关,但在客户端上看不到。现在你可能能猜到为什么了。由于我们的权限检查,我们只在服务器上改变了bFlagActive的值,而没有告诉任何客户端这个改变。修复这个问题相当简单:

  1. 选择bFlagActive变量,并在其详细信息部分将变量 | 复制设置为复制:

再次运行测试。现在,你应该在客户端上看到标志的状态也在改变。

这提出了一个重要的问题。只因为一个 actor 被复制并不意味着它的任何属性(除了它们的初始状态)都会被复制。再次强调,这是有意的。你不希望发送任何你不需要发送的东西到网络上。每一点流量都增加了带宽负载,并增加了添加额外玩家的成本。Unreal 默认只复制你告诉它要复制的内容。

使用 RepNotify 通知客户端值已更改

刚才我们提到,轮询 tick 上的值是浪费的,因为它会在每次更新时执行一次操作,即使没有必要执行。响应事件几乎总是一个更好的主意。

事实证明,使用复制变量很容易做到这一点:

  1. 选择你的bFlagActive变量,并在其详细信息 | 变量块中,将其复制属性设置为 RepNotify,而不是复制。

  2. 查看你的函数列表。刚刚自动添加了一个新函数,名为OnRep_bFlagActive

  3. 将你在 Event Tick 上的所有内容选中,然后按Ctrl + X剪切出来。

  4. 打开你的新的OnRep_bFlagActive函数,并将所有内容粘贴到其中,将函数的执行线连接到你的 Set Intensity 节点:

这是一种更高效的响应值变化的方式。具有复制设置为 RepNotify 的变量的OnRep函数将在该变量每次从服务器接收到新值时自动调用。这使得响应这些变化变得简单高效,如果我们想在通过复制接收到新值时触发一个效果,比如粒子系统或执行其他操作,我们现在有了一个自然的地方来做这个。

如果你需要在客户端通过复制收到新值时发生某些事情,可以使用 RepNotify 创建一个 OnRep 函数,并在那里执行操作。

到目前为止,我们构建的示例非常简单,但实际上它展示了一些非常重要的点。我们已经谈到了对象在网络框架中的位置,如何确定一个动作是在网络权限(服务器)上执行还是在远程(客户端)会话上执行,如何确定一个 Actor 是否从服务器复制到客户端,以及如何将新值复制到客户端并响应其变化。现在,让我们进一步构建一些看起来更像游戏的东西。

为多人游戏创建网络感知 pawn

现在我们已经看到了信息如何从服务器传递到客户端,让我们探索一下玩家操作如何从客户端传递回服务器。为了做好准备,我们将采取捷径,添加一个可以执行一些基本操作的 pawn,并立即开始使这些操作在多人游戏中起作用。

添加第一人称 Pawn

我们将通过添加来自第一人称模板的 pawn 来设置自己:

  1. 创建或打开一个使用蓝图 | 第一人称模板创建的项目。

  2. 选择 Content | FirstPersonBP | Blueprints | FirstPersonCharacter,并将这个角色迁移到我们的工作项目中。

现在,我们需要告诉我们的游戏模式使用它。

  1. 打开 BP_MultiplayerGameMode,并将其默认的 Pawn Class 设置为我们刚刚迁移进来的 FirstPersonCharacter。

让我们来测试一下。我们应该会看到一些问题。我们的抛射物会从看不见的墙壁上弹开。当玩家开火时,我们无法从另一台机器上看到发生的情况。另一个玩家的表示只会出现为第一人称武器。我们将修复所有这些问题。

设置碰撞响应预设

首先,让我们修复碰撞问题。虽然它与网络直接相关,但它会分散注意力,而且不难修正:

  1. 选择一个阻挡我们抛射物的阻挡体:

  1. 查看其详细信息|碰撞|碰撞预设,以查看它使用的碰撞预设。

我们可以看到它使用了 Invisible Wall 预设。很可能,这个预设正在阻挡我们不想阻挡的很多东西。对于我们的游戏,我们只想停止 Pawn。

  1. 打开设置|项目设置|碰撞,并展开预设部分。

  2. 找到 Invisible Wall 预设,并点击编辑按钮:

在这里,我们找到并选择了引擎|碰撞|预设列表中的 InvisibleWall 碰撞预设。

确实,我们可以看到它阻挡了除了可见性之外的一切。让我们进行更改。将其设置为除了 Pawn 之外的一切都忽略的 Trace Type:

我们还需要对我们的抛射物进行一些更改:

  1. 打开Content/FirstPersonBP/Blueprints/FirstPersonProjectile,并选择其CollisionComponent

  2. 在详细信息|碰撞下,将其碰撞预设属性设置为 OverlapAllDynamic。

现在这已经足够好了。墙壁不再阻挡除了 Pawn 之外的任何东西,抛射物也不再试图从世界中的物体上弹开。

完成这一步后,让我们回到设置我们的网络。

设置第三人称角色模型

我们首先要做的是使用适当的第三人称模型获取我们的远程角色。让我们添加我们需要的内容:

  1. 从内容浏览器中,点击添加新内容|添加功能或内容包...,然后选择蓝图功能|第三人称:

在这里,我们正在将第三人称内容包添加到我们的项目中。

  1. 将其添加到你的项目中。

现在,我们要修改我们的角色以使用第三人称模型:

  1. 打开你的 FirstPersonCharacter 蓝图,并点击添加组件|骨骼网格。确保选择了角色或其 CapsuleComponent,以便将此新组件创建为 CapsuleComponent 的子组件。

  2. 将新组件命名为ThirdPerson

  3. 将其详细信息|网格|骨骼网格设置为刚刚与我们的第三人称内容一起到达的 SK_Mannequin 网格。

  4. 将其详细信息|动画|动画类设置为使用 ThirdPerson_AnimBP_C 动画蓝图。

  5. 调整其位置,使其与胶囊对齐(将其位置 Z 值设置为-90.0,将其旋转 Z(偏航)值设置为-90.0 即可):

如果我们现在运行它,我们会看到第三人称模型阻挡了我们的摄像机视图。我们希望为其他玩家显示此模型,但对于自己来说隐藏它:

  1. 跳转到 FirstPersonCharacter 的事件图表,并找到其 Event BeginPlay 节点。

  2. 将 Event BeginPlay 节点拖动出一点,以便有足够的空间进行操作。

  3. 右键单击并添加一个 Is Locally Controlled 节点到图表中。

  4. 从你的 Is Locally Controlled 节点创建一个分支,并将 Begin Play 的执行输出连接到它。

  5. 将对ThirdPerson组件的引用拖动到你的图表中。

  6. 在其中调用 Set Hidden in Game,将 New Hidden 设置为 true。

  7. 从分支节点的 True 输出执行此 Set Hidden in Game 调用。

  8. 将 Set Hidden in Game 的执行输出连接到 Event BeginPlay 用于输入的分支节点。

  9. 将你的 Is Locally Controlled 分支的False输出连接到 Is Head Mounted Display Enabled 分支的输入。

在这种情况下,双击执行线以创建重定向节点是一个好主意,以避免在其他节点下交叉,并清楚地标明执行的条件部分的开始和结束。这对蓝图的行为没有影响,但可以提高其可读性。

您的图表现在应该类似于此屏幕截图:

在网络开发中,经常需要检查一个 actor 是否是本地控制的。在单人游戏环境中,当然不需要考虑这个问题,因为一切都是本地控制的,但一旦涉及到通过复制到达的对象,如果它们属于其他人,通常情况下您可能希望对它们进行不同的处理。

您还可以通过将 ThirdPerson 组件的详细信息|渲染|Owner No See 设置为 True 来实现这一点。这个标志及其伴侣 Only Owner See 也可以用于使某些东西只对所有者可见或对其不可见。您必须展开渲染选项的高级区域才能看到它。对于这个例子,我们选择使用 Is Locally Controlled 检查,因为有很多其他情况会使用它,但了解这些快捷方式是值得的。

让我们再次运行它,现在您将看到远程角色的第三人称模型和本地控制角色的第一人称模型。

调整第三人称武器

对于第三人称角色来说,武器的位置很奇怪。让我们来修复一下:

  1. 打开Content/Mannequin/Character/Mesh/UE4_Mannequin_Skeleton,在骨骼树中找到 hand_r 骨骼。

  2. 右键单击骨骼并选择添加插座:

右键单击 hand_r 骨骼并选择在此处添加插座。

  1. 将新插座命名为Weapon

  2. 右键单击插座,选择添加预览资产,并选择 SK_FPGun 作为预览。

  3. 移动插座,直到武器与手部正确对齐。(将相对位置设置为 X=-12.5,Y=5.8,Z=0.2,并将相对旋转 Z(偏航)值设置为 80.0 似乎效果不错。)

现在,我们需要将武器附加到刚刚创建的插座上,但仅适用于远程玩家:

  1. 跳回到 FirstPersonCharacter 的事件图,并找到 Event BeginPlay 节点。

  2. 从 Is Locally Controlled 分支的 False 输出中,连接一个 AttachToComponent(FP_Gun)节点。

我们之前见过这个,但再次提醒一下,AttachToComponent 有两个版本,一个适用于 actors,另一个适用于 components。选择与您的 FP_Gun 组件绑定的版本。

  1. 将您的第三人称组件拖动到 AttachToComponent 节点的父级输入中。

  2. 在插座名称中输入您在骨骼上创建的插座的名称(Weapon):

再次运行。现在武器应该放置得更合理。它没有瞄准其他玩家瞄准的位置,因为我们还没有在第三人称动画蓝图中添加任何内容来处理这个问题。添加这个功能超出了本章的范围,因为它真的让我们脱离了网络,所以对于我们这里的游戏目的,我们将保持现状。

接下来,我们需要确保当玩家开火时,服务器处理射击并将其复制到其他客户端。

复制玩家的动作

正如我们之前看到的,在当前版本中,其他玩家开火时玩家看不到它。我们将从简单的开始,确保当生成时,从服务器到客户端复制弹丸:

  • 打开 FirstPersonProjectile 蓝图,在其详细信息|复制部分中,将 Replicates 设置为 true。

现在运行它,您会发现如果在服务器上开火,客户端可以看到弹丸,但如果在客户端上开火,服务器看不到它。

花一点时间形成一个清晰的心理图像,为什么会这样。复制是单向的:从服务器到客户端。当我们在之前的示例中在服务器上生成旗帜时,我们在客户端上看到了它,因为我们告诉服务器要复制它。现在,同样的事情也发生在投射物上。那么,问题是,客户端如何告诉服务器它需要生成一个投射物呢?

使用远程过程调用与服务器通信

答案通过一种称为远程过程RPC)的过程传递。远程过程调用是从一个系统发出的,旨在在另一个系统上运行的调用。在我们的例子中,当我们想要开火时,我们将让客户端向服务器发出一个 RPC,告诉它我们想要开火,服务器将处理实际的开火操作。

让我们将我们的角色的开火方法更改为使用 RPC:

  1. 打开你的 FirstPersonCharacter 蓝图的事件图,找到 InputAction Fire。

  2. 在附近创建一个自定义事件。将其命名为ServerFire

  3. 在自定义事件的详细信息中,将其 Graph | Replicates 值设置为 Run on Server:

现在,让我们准备使用这个调用。我们首先要做的是将武器开火的那些与游戏相关且应在服务器上运行的部分与纯粹的用于装饰的部分分开。

让我们创建一个额外的自定义事件来处理非必要的客户端内容。

  1. 创建一个自定义事件并将其命名为SimulateWeaponFire

虚幻引擎开发者通常遵循一种命名约定,即将网络操作的非必要装饰性方面命名为前缀simulate。这向读者表明该函数可以安全地在客户端上运行,并且只包含非状态更改的操作(声音、动画、粒子等)。它还向读者表明该函数在专用服务器上可以安全地跳过。

  1. 找到 Play Sound at Location 调用和 GetActorLocation 调用,将它们从 SpawnActor FirstPersonProjectile 节点断开连接,并将它们连接到新的 SimulateWeaponFire 事件。

  2. 摆脱从 InputTouch 节点的 FingerIndex 分支出来的分支。它没有任何执行线进入它,这意味着它没有起作用。这只是一种杂乱无章的情况;有人没有清理图表。

部分更新的图应该看起来像这样:

从第三人称内容包中迁移到我们项目中的生成投射物的方法

  1. 现在,获取那个 Montage Play 调用,将其从当前所在的执行线断开连接,并将其放到 SimulateWeaponFire 的执行线上。

我们现在所做的是将所有纯装饰性的东西移到一个可以单独调用的事件中。

即使在开发单人应用程序时,遵循这个约定也是一个好习惯,因为它可以很容易地看出哪些代码块实际上正在改变事物,哪些是装饰性的。将它们分开是一个值得养成的好习惯。

现在我们已经创建了SimulateWeaponFire事件并填充了它,我们将确保在接收输入的任何系统上调用它:

  1. 现在,在 Montage Play 节点曾经所在的位置上调用 SimulateWeaponFire,这样它将在每次听到此输入事件时被调用。

  2. 在 Simulate Weapon Fire 调用之后添加一个 Switch Has Authority 节点。

  3. 将 Switch 节点的 Authority 输出连接到 SpawnActor First Person Projectile 调用。

  4. 从其 Remote 分支,调用我们之前创建的 ServerFire 节点。

  5. 将 ServerFire 节点的执行输出连接到 SpawnActor First Person Projectile 节点的输入。

现在,你的 SpawnProjectile 图应该看起来像这样:

SimulateWeaponFire 图应该如下所示:

试一试。对于客户端来说,瞄准会不准确,因为我们没有做任何事情来将客户端的武器瞄准发送到服务器,但是现在你应该能看到抛射物生成并且听到火焰声音。

让我们改进一下。

目前,抛射物的生成旋转来自第一人称相机。当从客户端向服务器通信时,这种方法行不通,因为服务器对相机一无所知。让我们用服务器知道的一个值来替换它:

  • 在图表中右键单击创建一个“Get Base Aim Rotation”节点,并将其输入连接到“Make Transform”节点,替换相机的“GetWorldRotation”输入:

再次测试。当在服务器上看到客户端的抛射物时,其起点仍然不正确,但是瞄准旋转现在是正确的。(修复起点实际上需要我们构建一个适当的第三人称动画蓝图,这超出了本章的范围。)

让我们来讨论一下目前的工作原理。这里有一个重要的模式值得内化。

当火焰输入事件到达时,我们检查是否有权限生成粒子。如果有,我们就直接生成它。然而,如果没有权限,我们会向服务器发起远程过程调用,告诉它生成粒子。它生成了粒子,然后我们在本地客户端看到了它,因为它已经被复制了。

大多数多人游戏中的游戏事件都会按照这个模式编写。以下是一个简化的示例,以便更清楚地理解:

在上面的截图中,执行“Do the thing”调用的只会在服务器上运行。如果触发它的事件发生在服务器上,它就会直接运行;如果事件发生在客户端上,客户端会调用“Server Do the Thing”RPC,然后处理调用“Do the Thing”。这种模式值得记住。你会经常使用它。

在虚幻开发者中有一个常见的约定,即我们将在服务器上运行的 RPC 的名称前缀为“Server”。你不一定要这样做,但这是一个好主意,如果你不这样做,虚幻开发者会不满地看着你。这样做可以更容易地看到哪些函数是 RPC,哪些函数是本地运行的。

使用多播 RPC 与客户端通信

我们所编写的代码还存在另一个问题,如果你在单台机器上进行测试,很难发现:模拟的声音和动画只会在拥有者客户端上播放。如果我们在两台独立的机器上进行游戏,并且另一个玩家在我们附近开火,我们是听不到的。

为什么不呢?

在上一个截图中,当本地客户端接收到输入事件时,它调用Simulate方法播放声音和动画,然后检查是否有权限决定自己生成抛射物还是请求服务器处理。但是,如果附近还有另一个玩家呢?

玩家 A 的客户端将发送 RPC 到服务器以生成抛射物,所以每个人都会看到,但是触发开火事件的调用只会在玩家 A 的机器上发生。在玩家 B 的机器上,玩家 A 的角色(我们称之为远程代理)没有被告知播放动画,所以它不会播放。

我们可以使用另一种类型的 RPC 来解决这个问题,称为多播事件

你经常会听到开发者将多播事件称为网络多播事件,或者称为广播事件。这些术语指的是同一件事。按照惯例,就像服务器 RPC 事件名称以“server”为前缀一样,多播事件通常以“broadcast”作为前缀命名。这个约定不如“server”前缀常见,你不一定要这样做,但是如果你养成这个习惯,以后在蓝图中会更容易跟踪。

由于我们已经将模拟方法抽象到了它们自己的事件中,所以这并不难做到:

  • 选择你的 SimulateWeaponFire 事件,在其详细信息|图表中,将其复制属性设置为 Multicast:

这将把这个事件发送到服务器,并指示服务器将其发送给所有连接的客户端。

现在,当玩家 A 开火时,生成抛射物的调用只会在服务器上发生,但播放开火声音和动画的调用会在网络上的玩家 A 的所有表示中发生。

如果你愿意,你可以将你的SimulateWeaponFire事件重命名为BroadcastSimulateWeaponFire。一些开发者遵循这个约定,而其他人则不遵循。总的来说,你给自己和其他开发者提供的关于你正在做什么的信息越多,你或他们在调试或维护代码时就会更容易。

客户端 RPC

还有一种 RPC 类型,我们在这里不打算演示,但为了完整起见,我们应该讨论一下。假设你在服务器上运行一个操作,并且你需要专门向拥有该对象的客户端发出调用。你可以通过将事件设置为在拥有客户端上运行来实现这一点。

可靠的 RPC

当我们决定如何复制函数调用时,还有一个最终的决定要做,那就是是否使调用可靠。

为了理解这个标志的含义,我们需要了解一些关于网络的关键知识。互联网是不可靠的。仅仅因为你向地球另一边的某人发送了一个远程过程调用(RPC),并不能保证它一定会到达。数据包经常会丢失。这不是虚幻的事情,而是现实世界的事情。作为开发者,你需要做出的选择是如何处理这个问题。

如果一个 RPC 对游戏很重要,比如开火,那就让它可靠。这将指示网络接口在收到来自其他系统的调用确认之前,重新发送它。然而,这会增加网络流量,所以只对你关心的那些调用进行可靠处理。如果你只是广播一个装饰性的调用,比如武器声音,那就让它不可靠,因为如果它没有到达,你的游戏不会出错。然而,开火的调用应该是可靠的,因为它对玩家和游戏的发展都很重要。

现在让我们进行这个更改:

  1. 找到你的 ServerFire 自定义事件,在其详细信息|图表中,将其可靠属性设置为 true。

  2. 将你的 BroadcastSimulateWeaponFire 事件设置为不可靠,因为它只是播放不重要的装饰性事件,不值得堵塞网络。

进一步了解

网络是一个重要的主题,老实说,我们在这里只是浅尝辄止。我们写这篇文章的目的是为了给你一个坚实的思维模型,让你能够理解虚幻的网络框架是什么样的,以及你需要理解哪些方面才能在其中工作。

这是一项复杂的工作,对于新开发者来说可能会相当困惑。网络开发的诀窍是创建一个清晰的思维模型来理解正在发生的事情。花些时间来理解这些概念,你会更容易上手。

这里有一些我们没有涵盖到的话题,比如主持会话和让其他人加入会话,以及网络工作的一些细节,比如相关性。这些都是值得了解的,而且有一些很好的资源可以帮助你进一步理解。

首先,查看你的 Content Examples 项目中的 Network Examples 地图,并花些时间理解它们展示的内容。接下来,Cedric Neukirchen 的《多人网络手册》cedric-neukirchen.net/2017/02/14/multiplayer-network-compendium/ 是一个学习虚幻网络框架工作原理的杰出资源。虚幻的文档在这里:docs.unrealengine.com/en-us/Gameplay/Networking,根据你在这里学到的知识,花些时间研究它的 Multiplayer Shootout 项目是非常值得的。

总结

这一章涉及的理论比其他章节多一些,如果其中的大部分内容仍然需要时间消化,那完全没关系。

在本章中,我们谈到了虚幻的客户端-服务器架构,以及哪些对象存在于哪些域中。了解这个结构是非常重要的。我们还学习了一些关于信息和事件如何通过复制和远程过程调用在机器之间传递的知识。

我们希望这一章为你提供了一个良好的基础,让你在网络方面深入研究并真正探索它的工作原理。对自己有耐心,花时间去实验。

我们现在已经达到了一个点,我们已经涵盖了许多你需要了解的内容,以使用虚幻引擎开发 VR。接下来,我们将看一些工具和插件,可以大大加快你在 VR 中的工作。通过你在本书中学到的知识,你应该已经准备好去研究它们,并理解它们如何帮助你开发并节省大量时间。

第十一章:进一步发展 VR - 扩展虚幻引擎

区分专业开发者和新手开发者的一个重要因素是他们如何利用现有的工具和库来加速工作。很多时候,新手开发者尝试自己做所有的事情,要么是因为他们不知道有哪些资源可以帮助他们,要么是因为他们认为依赖现有的库是一种“作弊”。其实不是这样的。如果你是一名摄影师,在你的车库里没有自己建造相机并不是作弊——你只是专注于你真正关心的艺术部分。不要害怕利用可以加速你开发的工具和库。

然而,要有效地利用其他开发者的工作,你需要付出努力去理解他们在做什么。不要只是简单地粘贴别人的代码而不真正理解它为什么有效——如果你这样做,你只会引发难以找到的错误。做好功课,找到你可以依赖的代码,但同时也要把它作为你的功课的一部分,去理解它是如何构建的,这样你才能在使用它时做出明智的选择。

在你的开发生涯中,迟早会遇到“模仿神秘”的编程术语。这个术语通常被归功于物理学家理查德·费曼,它指的是二战后南太平洋一些岛屿上观察到的土著宗教习俗,他们建造了复制的机场,试图吸引战争期间供应岛屿的神秘货机回来。他们只是复制了形式,但他们不理解这些形式是如何工作的,也不理解为什么它们现在不起作用。不要让这描述你开发软件的方式。对于你在项目中包含的任何内容,当另一个工程师指着其中的任何部分问“这是做什么的?”时,你应该能够给出一个清晰的答案。当然,并非所有情况下都可能做到这一点,但总的来说,要考虑到你的工作在你花时间去理解库或插件是如何工作的之前是不完整的。

在本章中,我们将主要关注一款对 VR 开发者非常有用的插件:Joshua (MordenTral) Statzer 的VRExpansion插件。它采用 MIT 许可证(我们马上会谈到许可证——它们很重要),这意味着它可以在非商业和商业软件中自由使用。它不需要任何费用,但它代表了非常出色的专业工作,所以如果你使用它,认真考虑支持他的 Patreon,以便项目能够继续进行。

在本章中,我们将学习如何有效地使用高级插件,如 VR 扩展插件,并使用其示例项目中的蓝图示例来学习它的预期使用方式。我们将学习探索和理解陌生代码的策略,以及使用调试工具来展示代码的运行方式。

具体来说,我们将学习以下内容:

  • 安装和构建插件以扩展引擎的功能

  • 使用文档和示例项目来了解插件的功能和预期使用方式

  • 利用插件提供的新的本地类

  • 使用策略来阅读复杂的蓝图并理解它们的结构

  • 使用调试工具来帮助我们探索陌生的蓝图并了解它们的执行流程

本章将涉及的直接蓝图构建比之前的章节要少,这是有意为之的。真正的重点在于帮助您开发学习如何使用陌生代码的策略,以便您可以利用它进行自己的开发并学习高级技术。这是作为开发人员可以培养的最重要的技能之一。对于基本主题,很容易找到教程,但一旦进入更高级的领域,您主要需要通过查看其他高级工作来学习。一开始可能看起来有些令人生畏,但我们将学习一些有效的策略来做到这一点。

有了这个,让我们为引擎添加一些功能,并学习如何使其做以前无法做到的事情。

创建一个用于存放插件的项目

让我们从创建一个新的空白项目开始:

  1. 使用空白模板创建一个新的蓝图项目,并将其硬件目标设置为移动/平板电脑,图形目标设置为可扩展的 3D 或 2D,没有起始内容。

安装 VRExpansion 插件

一旦我们创建了项目,我们将把 VRExpansion 插件添加到其中。

在我们可以安装任何插件到项目之前,我们需要做的第一件事是创建一个放置插件的位置。插件必须位于项目目录或Engine目录中名为Plugins的目录中:

  1. 打开包含您的新项目文件的目录。您应该在这里看到您的.uproject文件,以及您的ConfigContent目录。

  2. 在这里创建一个名为Plugins的新目录:

现在我们已经为项目创建了一个Plugins目录,让我们将 VRExpansion 插件添加到其中。我们有几种方法可以做到这一点。

使用预编译的二进制文件进行安装

获取插件的最简单方法是导航到其论坛讨论页面,forums.unrealengine.com/development-discussion/vr-ar-development/89050-vr-expansion-plugin,并使用适用于您的引擎版本的插件预构建下载链接:

  1. 点击完整的二进制和源代码包链接,下载压缩的插件

  2. 下载完成后,打开.zip文件,并将其中包含的VRExpansionPlugin目录拖到您的Plugins目录中

就是这样。只要安装了适用于您的引擎版本构建的插件版本,您就可以开始并打开您的项目。

编译自己的插件二进制文件

如果您需要插件的更新代码,而预构建的二进制文件中没有包含(如果您正在运行引擎的预览版本,则需要),您需要单独构建插件的二进制文件。这并不难:

  1. 在这里导航到 VRExpansionPlugin 存储库的 BitBucket:bitbucket.org/mordentral/vrexpansionplugin

  2. 点击下载链接,然后点击下载存储库链接,下载一个压缩版本的存储库

也可以直接将插件的 Git 存储库克隆到项目的插件目录中,但除非你正在进行最新的工作并且需要绝对最新的代码,否则你不需要这样做。如果你计划对插件进行自己的更改,你将需要这样做。然而,对于大多数用户来说,下载压缩的存储库更容易。

  1. 现在打开刚刚下载的.zip文件。

  2. 你会看到一个文件夹,里面的名字类似于mordentral-vrexpansionplugin-9c1737a17bef(末尾的哈希值会不同)-将其拖到你的新Plugins目录中。

  3. 将刚刚解压的目录的名称更改为VRExpansionPlugin

现在启动您的项目,或者如果它已经打开,请关闭并重新打开它。

现在应该会出现一个对话框,指示您需要构建插件的二进制文件:

如果您按照第二章中的指示设置了 Visual Studio Community 2017,那么这不是一个问题。(如果您没有,请立即返回那里并按照说明进行设置。在您的系统上设置一个工作的编译器总是值得的,因为有时您会需要它。)点击“是”并让 Visual Studio 为您构建新的二进制文件。

您的插件应该能够成功构建,但如果不能,请转到插件的主页bitbucket.org/mordentral/vrexpansionplugin并按照“基本安装步骤”下的说明进行操作,这将引导您完成手动构建过程。正如之前提到的,您还可以选择从这里下载预构建的二进制文件:forums.unrealengine.com/development-discussion/vr-ar-development/89050-vr-expansion-plugin

如果您在构建对话框上点击“显示日志”,您应该能够看到构建进度。预计需要几分钟:

构建完成后,您的项目将打开。

验证项目中的插件

无论您是如何下载和安装插件的,一旦您打开项目,它现在应该可用。

打开项目时,您应该在右下角看到两个指示,表示您有新的插件可用,并询问您是否要更新项目:

点击“管理插件...”打开插件列表。

您应该看到两个 VRExpansion 插件条目,并且它们都应该已启用:

这是应该的,所以我们可以关闭这个窗口。

现在,让我们通过点击“更新”按钮来更新我们的项目文件。

请记住,您的.uproject文件实际上只是一个文本文件,向虚幻引擎提供有关项目的一些基本信息。如果您在文本编辑器中打开它,您会看到添加了新条目,指示该项目现在依赖于 VRExpansion 插件及其伴生的 OpenVRExpansion 插件:

这是在添加 VRExpansion 插件之前和之后的.uproject 文件的文本比较

就是这样。我们已经准备好使用插件进行开发了,但在开始之前,让我们稍微谈一下我们刚刚做了什么。

理解插件

插件是虚幻生态系统的重要组成部分。它们可以包含内容、蓝图、本地代码和任何其他影响虚幻引擎能够做什么以及如何做的东西。它们可以节省大量时间,并几乎无限地扩展引擎的功能。

在大多数情况下,您实际上不需要了解虚幻引擎如何处理插件才能使用它们-它们基本上只是工作的,但如果您想要能够在出现问题时修复问题,或者如果您需要更新插件以适应新的引擎版本,了解一些关于插件存放位置和组成方式的知识是有帮助的。我们不会在这里深入探讨,但有一些快速要点可以帮助您进行未来的开发。(如果您确实需要深入了解插件的开发,请从这里开始阅读文档:docs.unrealengine.com/en-us/Programming/Plugin

插件的位置

首先,重要的是要知道将要安装到项目或引擎的新插件放在哪里,并知道从 Epic Games 启动器下载的插件将被放置在哪里。

您安装的任何插件都将位于两个位置之一:对于仅安装到特定项目的插件,它们将位于项目的Plugins目录中,对于安装到引擎并适用于所有项目的插件,它们将位于Engine\Plugins目录中。

请花点时间查看以下步骤中给出的当前安装的引擎插件:

  1. 打开您安装虚幻引擎的目录(默认情况下,这将位于C:\Program Files\Epic Games),然后打开Engine\Plugins子目录:

在这里,您会注意到一些有趣的事情:引擎的许多功能,甚至我们认为是核心引擎功能的东西,例如特效编辑器,实际上都是作为插件存在于虚幻框架中。这值得记住。在虚幻引擎中,插件并不是二等公民。通过插件将某些东西添加到引擎中与直接将其编写到引擎代码中并没有实质性的区别,只是如果以这种方式设置,更容易替换、打开或关闭它。

通过 Epic Games 启动器下载的插件将出现在Engine\Plugins目录的“市场”子目录中。通常情况下,Epic Games 启动器会在您从启动器安装的插件有可用更新时提醒您,并且您可以直接从启动器中更新它。您很少需要打开Engine\Plugins目录,但了解它的存在是值得的。

从市场安装插件

使用 Epic Games 启动器安装插件,从“市场”或“库”中选择您想要的插件,然后点击“安装到引擎”按钮,或者如果插件已配置为资源包,则点击“添加到项目”按钮。将插件安装到引擎将把它放在引擎安装的Engine\Plugins\Marketplace目录中,而将其放在项目的Plugins目录中则点击“添加到项目”按钮:

如果您使用该工具安装的插件有可用的更新,Epic Games 启动器将自动提醒您。

插件的内部是什么?

现在我们已经了解了虚幻引擎中插件的存放位置,让我们来看看它们由什么组成。

为了做到这一点,我们将快速探索一下我们安装到项目的Plugins目录中的 VRExpansion 插件:

  1. 打开项目的Plugins目录,然后打开其中的VRExpansionPlugin目录

您会看到 VRExpansion 实际上由此目录中的两个单独的插件组成:VRExpansionPluginOpenVRExpansionPlugin。后者存在是为了支持 Valve Software 的 OpenVR SDK。在这里,我们不需要担心它,我们只关注 VRExpansion。

这里有两个文件,我们应该花点时间提及一下。

第一个是README.md文件。请花点时间打开它。这是一个包含有关插件的一些基本信息的 markdown 文件。

如果您的系统上安装了 Visual Studio Code,您可以使用 VSCode 打开 markdown 文件。打开文件后,您可以右键单击查看区域中的选项卡,然后选择“打开预览”,或者只需按下Ctrl + Shift + V以带格式查看 markdown。

您会看到这个readme文件基本上重新创建了主 BitBucket 页面上的文本:bitbucket.org/mordentral/vrexpansionplugin,并链接到指令和信息页面。许多插件都附带有文档或readme文件,告诉您如何找到文档。值得一看。

关于许可证

这里我们还应该看一下LICENSE.txt文件。如果您要在项目中包含插件,了解如何使用它是很重要的。

如果你通过市场下载了一个插件,你不需要担心它。通过 Epic 的市场分发的所有插件都可以用于非商业或商业用途,并且不会对它们的使用方式施加任何额外的限制。

如果你需要更多关于市场上插件许可证的信息,详细信息在这里:www.unrealengine.com/en-US/marketplace-distribution-agreement

如果你从网络上直接下载插件,就像我们之前做的那样,你需要检查许可证并确保作者允许你按照你想要的方式使用插件。大多数插件作者不会对你使用软件的方式施加限制,但总是阅读许可证并确保。你不想在构建一个以插件为基础的项目时,发现在销售软件时你实际上是不被允许这样做的。先阅读许可证。

特别要小心使用 GNU通用公共许可证GPL)许可的软件,这个许可证对软件的使用施加了重要的限制,并且与虚幻引擎的许可条款不兼容。然而,更宽松的MITApache许可证是可以的,你会遇到很多使用它们的虚幻插件。

在我们的情况下,VRExpansion 插件的许可证允许你几乎做任何你想做的事情(除了删除许可证文件并试图假装这是你自己的作品),包括修改插件的代码。它对你使用它的项目的内容或商业与非商业使用没有任何限制。这是理想的。无论我们是将项目作为商业游戏出售,还是用于现场演出,只是作为一种爱好建设,或者其他任何情况,都没有问题。

在插件目录中

如果我们现在打开外部VRExpansionPlugin目录中的VRExpansionPlugin目录,我们会看到一个非常类似于虚幻项目结构的目录结构。这并非偶然。你可以将插件几乎看作是被插入到项目中的迷你项目。它们可以包含代码、蓝图或资产和其他内容,就像一个项目一样。

我们不会关心这个目录的内容,只是看一下其中的一个东西:

  1. 在文本编辑器中打开VRExpansionPlugin.uplugin文件

你会发现这个文件,就像你的.uproject文件一样,只是一个包含有关插件信息的文本文件。你很少需要打开这个文件,但就像你的.uproject文件一样,如果你需要手动调试或更改某些内容,你应该知道它:

{
    "FileVersion": 3,
    "Version": 4.21,
    "VersionName": "4.21",
    "FriendlyName": "VRExpansionPlugin",
    "Description": "Adds several new VR features & components to UE4",
    "Category": "VRExpansion",
    "CreatedBy": "Joshua (MordenTral) Statzer",
    "CreatedByURL": "",
    "DocsURL": "",
    "MarketplaceURL": "",
    "SupportURL": "",
    "EnabledByDefault": true,
    "CanContainContent": false,
    "IsBetaVersion": false,
    "Installed": true,
    "Modules": [
        {
            "Name": "VRExpansionPlugin",
            "Type": "RunTime",
            "LoadingPhase": "Default"
        }
    ],
    "Plugins": [
        {
            "Name": "PhysXVehicles",
            "Enabled": true
        }
    ]
}

这里的大部分信息只是描述性的,但有一个重要的细节:Plugins块用于指定插件与其他插件之间的依赖关系。在这种情况下,我们可以看到VRExpansion插件需要启用PhysXVehicles插件。这不应该是个问题,因为它默认是启用的,但如果你遇到插件无法工作的情况,看看它依赖于什么,并确保这些插件也存在。

还有一个你可能会遇到的属性。一些插件会指定它们可以使用的引擎版本,使用一个看起来像这样的EngineVersion条目:

"EngineVersion" : "4.21.0",

如果一个插件包含这个条目,虚幻将只允许它与指定的引擎版本一起加载。(你可以通过手动修改.uplugin文件中的这个值来绕过这个限制,但插件是否能编译和工作将完全取决于其中的内容以及你尝试编译的引擎版本中发生了什么变化。)

结束我们的简短之旅

这是一个快速了解虚幻插件安装和内部内容的过程。正如我们之前提到的,在大部分开发过程中,你不需要去处理这些内容,但当你需要弄清楚软件的情况时,知道从哪里开始查找是非常有价值的。

有了这个,让我们继续在 VR 中工作吧。

探索 VRExpansion 示例项目

在我们回到自己的项目之前,我们将再次进行一次绕道,看看与 VRExpansion 插件一起维护的示例项目,这样我们就可以看到这个插件能让我们做什么样的事情。我们还将通过使用这个项目中的蓝图来加速本章的一些开发,所以不要跳过这一步。

让我们从这里开始下载它:bitbucket.org/mordentral/vrexppluginexample/downloads/。按照给定的步骤进行:

  1. 点击下载存储库链接以下载项目的压缩版本

  2. 将下载的项目解压到你保存虚幻示例项目的任何位置

  3. 打开项目目录,右键点击VRExpPluginExample.uproject,从上下文菜单中选择切换虚幻引擎版本...

  4. 将其设置为你当前的虚幻引擎版本

由于这个项目是作为一个 C++项目创建的,当你设置一个新的引擎版本关联时,Visual Studio 也会为你创建一个解决方案文件。你不需要使用 C++来使用这个插件。项目本身中的所有内容都是在插件之上使用蓝图创建的,这也是我们将要构建项目的方式,但如果你对 C++类有兴趣并想看看插件是如何构建的,这个解决方案文件提供了一个很好的方法。

尝试启动项目。它可能会要求你构建其中包含的插件。让它去做吧。(再次确保你按照第二章中的指示安装和设置了 Visual Studio,设置开发环境。)

项目启动后,让它编译着色器,然后探索一下看看它提供了什么。很快就会明显,VRExpansion 为 VR 开发者提供了巨大的帮助。它是一个宝库,里面有专业编写的代码和蓝图示例,展示了你可以在 VR 中做的各种事情,许多专业制作和发布的游戏都在开发中使用了这个插件或其中的部分内容:

VR 扩展插件测试项目的视图。你会在这里找到大量有用的 VR 开发示例。

在这里玩一下。我们不会覆盖这个示例项目中的所有内容,因为我们即将开始构建我们自己的项目,但是探索一下足够让你对其中包含的内容有一个很好的了解,并且你可以为自己的应用程序重新定制一些东西。

以下是一些建议,帮助你开始:

  • 你的控制器的 D-Pad 或拇指杆触发传送移动,就像我们自己的示例中一样。

  • 当你手中没有物体时,挤压控制器抓握会改变你的移动模式

我们强烈建议你现在花些时间尝试每种移动模式。传送和 DPadPress-ControllerOrient 模式对你来说应该很熟悉,因为我们之前构建的定位项目中有这些模式。其他模式对你来说可能是新的。尝试一下并得到一些想法:

  • 许多物体可以被抓住和使用。使用扳机进行抓握。

  • 白色物体可以被抓住和攀爬。

  • 玩家角色在处理玩家将头伸进墙壁的情况时做得很好。试试看。

  • 呕吐平台名副其实。(如果你还记得我们在第一章中的讨论,在 VR 中思考,你就会明白为什么。)

将此示例项目视为一个重要的资源,因为您将了解到此插件允许您做什么。由于您在本书中所做的工作,您应该能够很好地理解蓝图中所看到的内容,并将其用作进一步开发的起点。

现在,让我们使用这个插件作为我们自己工作的基础,来构建我们自己的项目。

完成项目设置

现在我们已经设置好了我们的项目,安装了 VRExpansion 插件,并对插件有了基本的了解,让我们回到构建内容的过程中。

当然,我们首先需要适当地设置我们的项目设置以用于 VR:

  • 项目 | 描述 | 设置 | 在 VR 中启动:False

  • 引擎 | 渲染 | 正向渲染器 | 正向着色:True

  • 引擎 | 渲染 | 默认设置 | 环境光遮蔽静态分数:False

  • 引擎 | 渲染 | 默认设置 | 抗锯齿方法:MSAA

  • 引擎 | 渲染 | VR | 实例化立体声:True

  • 引擎 | 渲染 | VR | 轮询遮挡查询:True

现在让我们给自己一个可以玩耍的环境:

  1. 在市场中找到灵魂:洞穴环境包,并将其添加到您的新项目中。(在项目打开时这样做是可以的。)

  2. 一旦环境包下载完成,如果项目还没有打开,请打开您的项目。

  3. Content/SoulCave/Maps下,找到 LV_Soul_Cave_Mobile 级别并打开它。让您的着色器编译。

在此过程中,让我们将其设置为您项目的默认级别:

  1. 打开设置 | 项目设置 | 项目 | 地图和模式,并将编辑器启动地图和游戏默认地图设置为 LV_Soul_Cave_Mobile

一旦你的着色器编译完成,我们就可以开始工作了。

使用 VRExpansion 类

我们将使用这个项目作为回顾我们在为 VR 设置场景时需要做的事情,并作为 VRExpansion 类的介绍。

添加导航

当然,现在我们已经有了我们的环境,我们首先要做的事情是设置一个导航网格,这样我们就可以选择使用传送定位和让 AI 角色在其中导航。

首先检查你的碰撞环境:

  1. 按下Alt + C(或从视口中选择 Show | Collision)来可视化你的碰撞环境,并确保它看起来合理。

这里的碰撞看起来不错,所以让我们在场景中添加一个导航网格边界体。

  1. 将一个导航网格边界体拖入场景并缩放它以包含您希望玩家能够导航的区域。

  2. 以下数值效果较好:位置(X= -11420.0,Y= -3790.0,Z= -490.0),缩放(X= 100.0,Y= 160.0,Z= 20.0)。

记住,在设置体积时,您可以使用视口的顶部和侧面视图来理解您正在做的事情,从而使您的工作更加轻松。

由于生成的导航网格将覆盖许多您不希望玩家导航的地方,因此请记住使用导航修改器体积来阻止不希望的传送目的地。

添加一个游戏模式

与往常一样,我们将为我们的项目设置一个游戏模式,以指定要加载的类并处理我们想要应用于游戏的任何规则:

  1. Content目录中为您的项目创建一个目录,然后在其中创建一个蓝图目录。

  2. 在此目录中创建一个新的蓝图类,并将其父类设置为 Game Mode Base。将其命名为BP_VRExpansionGameMode

  3. 打开设置 | 项目设置 | 项目 | 地图和模式,并将默认游戏模式设置为您刚刚创建的新游戏模式。

  4. 打开您地图的世界设置,并重置游戏模式 | 游戏模式覆盖以清除它。

随着我们基于 VRExpansion 类添加新类,我们将多次回顾我们的新游戏模式。

更新 PlayerStart 类

VRExpansion 插件提供了一个新的玩家起始类,它更准确地缩放了我们要生成的VRCharacter,因此更准确地表示了玩家可以适应的位置。我们将在这里使用它:

  1. 将一个 VRPlayerStart 拖动到场景中现有的PlayerStart演员附近。

  2. 从旧的PlayerStart详细信息中,右键单击其 Transform | Location,并复制该值。

  3. 删除旧的PlayerStart

  4. 选择 VRPlayerStart,在其详细信息中,右键单击其 Transform | Location,并粘贴从旧位置复制的值。

  5. 将其向下移动一点以放置在地板上。(X= -20220.0,Y= -13080.0,Z= -2118.0)效果还不错。

添加一个 VR 角色

现在是时候向我们的项目添加一个 VR 启用的角色了。VRExpansion 插件为我们提供了两个新的类,我们可以从中派生出一个用于 VR 的角色:

  • VRSimpleCharacter是一个为 VR 启用的角色提供基础功能的基类,它自动设置了两个GripControllers,一个网络复制的 VR 摄像机,并实现了专为 VR 使用的移动组件。

  • VRCharacter包括VRSimpleCharacter中的所有内容,但还添加了一些额外的方法来通过颈部位置偏移碰撞,并支持角色碰撞胶囊的更大缩放。

一般来说,除非您确定需要使用颈部碰撞偏移或者您将大幅改变碰撞胶囊的大小,否则请使用VRSimpleCharacter

现在让我们来做这个:

  1. 在放置 GameMode 的蓝图目录中,右键单击创建一个新的蓝图类。

  2. 展开所有类扩展器,在搜索框中键入vr char

  3. 您将看到列出了VRCharacter和“VRSimpleCharacter”类。选择“VRSimpleCharacter”。将新的蓝图命名为BP_VRCharacter

  1. 现在,打开您的游戏模式并将BP_VRCharacter设置为其默认的 Pawn 类。

运行地图。您现在还无法移动,但应该已经正确注册到地板上。

设置输入

现在,我们的角色已经就位,游戏模式已被告知生成它,让我们允许玩家控制它。

首先,我们需要映射一些输入。如果我们想手动完成这个过程,我们可以通过设置 | 项目设置 | 引擎 | 输入来完成,但为了节省一些时间,让我们将DefaultInput.ini文件从 VRExpansion 示例项目复制到我们的项目中:

  1. 打开解压 VRExpansion 示例项目的目录,并从其Config目录中复制DefaultInput.ini文件。

  2. 打开当前项目目录并将DefaultInput.ini粘贴到其中

重新打开您的工作项目。当然,如果我们正在构建自己的游戏,我们会为其设计自己的输入方案,但这样可以让我们快速地有一些已经映射好并准备好进行测试的输入。

使用示例资产设置您的 VR 角色

现在,通常情况下,我们会逐步介绍如何从头开始构建这个角色,但是在这里我们有很多材料要介绍,所以我们将通过将 VRExpansion 示例项目中的示例角色迁移到我们自己的项目中,然后深入研究它的工作原理来节省一些时间。

有效使用示例资产

这提醒我们一个值得一提的关于使用插件和示例资产和代码的问题。很多时候,库和插件会附带已经设计好与它们配合使用的示例资产。熟悉这些资产总是一个好主意,因为它们向您展示了作者对代码使用的期望。通常情况下,这些资产也会非常接近您所需要的,尽管它们很少会完全符合您的需求。

在使用他人的示例资产或代码时,有两种方法可以采取:您可以整体使用示例,然后修改或删除与您想要的方式不同的任何内容,或者您可以从头开始构建自己的资产,使用示例作为指导,了解作者建议您如何使用他们的代码。每种方法都有其优点和缺点。第一种方法往往可以让您更早地开始工作,但通常会得到许多不需要的额外内容,然后需要清理掉这些内容。(请记住,我们不相信这里的模仿编程——您不会简单地将这些代码倾倒到您的项目中并离开而不理解它。)第二种方法可能需要更多的时间,但可以为您提供一个干净的类,它只做您需要的事情,并且您对它的理解相当好,因为您自己编写了它。

还有一种中间道路,这是我们推荐的路径。记住肯特·贝克的建议:“让它工作;让它正确;让它快”?考虑在“让它工作”阶段使用现有的示例资产或类作为您的一部分。在这个阶段,您正在尝试使用作者编写的类,并学习它的工作原理和使用方法。然后,一旦您掌握了这些知识,开始删除您现在知道不需要的东西,并更改需要以不同方式工作的东西,直到您拥有一个能够满足您需求的版本。现在,进入“让它正确”的阶段。它现在可以吗?是否可以轻松维护?另一个工程师或未来的您一年后能否阅读这个蓝图并理解其中的内容?考虑到这些问题,您是否想要编写一个新的、并行的类版本,现在您已经有了一个可行的模板来构建它。

迁移示例角色

考虑到这种方法,让我们将示例项目的 VR 角色蓝图迁移到我们的项目中,以便我们可以开始尝试并了解它是如何构建的:

  1. VRExpPluginExample项目中,在Content/VRExpansion/Vive中找到Vive_PawnCharacter蓝图,并将其迁移到您的新项目的Content文件夹中

不要担心 Vive 中心化的名称。这个角色也可以与 Oculus Rift 和 Windows Mixed Reality 头戴式显示器一起使用。当这个插件首次编写时,只有 Vive 支持房间规模的 VR。一旦 Oculus 添加了这个支持,插件就会更新以适应它,但示例名称从未更改过。

  1. 返回到您的新项目,并将您的游戏模式的默认角色类切换为我们刚刚迁移的Vive_PawnCharacter

我们想创建另一个 VR 角色作为示例,以证明插件中引入的新类可以像任何其他引擎类一样使用,但对于我们实际要做的工作,我们将使用迁移的角色。

测试一下。现在,您应该能够使用传送来浏览环境,并且应该能够使用抓握按钮来更改移动模式:

稍微试验一下,然后我们将来看看内部情况。

理解复杂的蓝图

现在我们已经有了基本的工作原理,让我们深入了解一下这个类是如何构建的。当我们这样做时,您会发现,在本书中迄今为止所做的工作,将使您更好地理解这个蓝图中的许多技术。

我们要探索的技术是有价值的。如果您在软件开发中从事专业工作,或者即使您是业余爱好者,迟早都会遇到现有的代码,并且您需要弄清楚它是如何工作的。我们将指导您通过一些策略,使这个任务比起初看起来要容易得多。

让我们开始吧:

  1. 打开Content/VRExpansion/Vive,找到Vive_PawnCharacter蓝图。打开它。

  2. 打开它的事件图。

天啊!这里有很多东西。

示例项目的Vive_PawnCharacter蓝图包含了很多蓝图代码。一开始挖掘它可能看起来令人生畏,但实际上并不是那么困难。

尽管这一开始可能看起来令人生畏,但你很快就会欣赏到它的价值。这门课程是一个令人难以置信的有用技术汇编,用于开发虚拟现实角色。单独来看,这已经是一件美妙的事情了,但更令人惊叹的是,这里编写的蓝图和底层 C++代码都考虑了网络复制,因此如果你计划编写一个网络虚拟现实体验,这门课程将会对你有所帮助。

然而,要使用它,你需要知道从哪里开始。让我们学习如何处理一个新的类并弄清楚它。

首先检查父类

每当你查看一个新的蓝图时,你首先要做的是检查界面右上角的父类是什么。

在我们的例子中,我们可以看到这个蓝图派生自VRCharacterVRCharacter是一个用 C++编写的本地类。如果你按照父类指示器提供的链接,它将打开 Visual Studio 到这个类,你可以探索它的本地实现以了解更多信息。对于我们在这里的目的,我们将继续使用蓝图,但值得知道你也可以这样做。

(如果我们在其本地实现中深入研究这个类,我们会发现它派生自一个VRBaseCharacter类,而这个类又派生自Character。因此,这个类本质上是一个虚幻角色,如此处所述:docs.unrealengine.com/en-US/Gameplay/Framework/Pawn/Character。但它还有额外的针对 VR 的修改,以复制相机和手柄控制器的位置,并以适合 VR 的方式处理移动。)

查看组件以了解它们是由什么组成的

探索任何你正在研究的新类时,下一步要看的是它的组件列表:

查看这个组件列表可以告诉我们很多关于这个角色类以及它能做什么的信息。最好在视口处于活动状态时进行查看,这样你就可以看到哪些组件具有可见表示。将鼠标悬停在每个组件上,查看它是什么类型的组件,并让这些信息在你的脑海中建立起整个类的整体感觉。

我们可以看到VRCharacter支持一个用于头部的静态网格,一个用于身体的静态网格,以及两个带有文本渲染器、抓取检测球和骨骼网格的运动控制器。(这个运动控制器的设置应该对我们在抓握交互方面的工作感到有些熟悉。)我们还可以看到它提供了一个角色移动组件和一些用于 VOIP 通信的支持。

当你这样做时,你不需要为每个细节而苦恼。在这个过程的这个阶段,重点是建立一个关于类的整体思维模型,以及各个部分如何组合在一起。

寻找已知事件并查看它们运行时发生了什么。

获取关于蓝图信息的另一个有用的起点是从我们知道可能已经实现的事件开始,并查看它们的功能。

大多数类在事件 BeginPlay 上会进行一些设置工作,并且大多数类在事件 Tick 上也会进行一些工作,所以这些通常是明智的起点:

  1. 按下Ctrl + F激活查找结果面板,然后在搜索栏中输入beginplay

  2. 按下Enter,因为我们只对在这个蓝图内部进行搜索感兴趣:

在查找结果列表中出现了事件 BeginPlay。我们可以双击它跳转到蓝图中的该事件。

观察 BeginPlay,我们可以看到它只在服务器上处理此事件;它使用了一个名为 SetupOnPossession 的自定义事件。我们可以看到它为本地控制的玩家设置了抓取组件;它调整了跟踪原点和观众屏幕,然后对于每个物体,它将生成并设置一对BP_Teleport_Controller角色,这些角色会附加到运动控制器上。

也许我们还不完全了解这个角色,但仅仅从观察它的 BeginPlay,我们已经学到了一些东西:

  • 这个角色已经设置为在网络游戏中使用-它根据是否具有权限执行不同的路径

  • 根据本地运行还是由其他玩家控制,角色会以不同的方式处理一些事情。

  • 传送处理由一个与角色不同的类管理。我们将要查看这个类。

现在让我们对 Event Tick 做同样的事情:

  • 搜索Tick,并双击结果中出现的 Event Tick 条目:

同样,这些信息可以立即告诉我们一些事情:

  • 远程角色在 tick 中不执行任何操作。这很好。

  • tick 主要处理移动,但攀爬移动被移到了一个单独的事件中。

  • 抓取动画和传送旋转也在 tick 中处理。

在这个阶段还不需要进行深入挖掘。你的目的是给自己一个广泛的视角,了解这个类包含的部分以及它们何时以及如何工作。这样,当你稍后寻找细节时,你就会知道在哪里寻找。

到目前为止,这个过程已经给我们提供了一些信息。仅凭知道父类、它包含的组件和两个已知事件,我们就可以对这个类的功能有一定的直觉。现在是时候更具体地开始,从一个简单的问题开始-当玩家尝试传送时会发生什么?

使用输入作为蓝图中的起点

我们可以通过查看这个庞大的事件图并尝试找到我们要找的内容来回答这个问题(在这种情况下,这对我们来说可能会相当顺利,因为图表组织得很好,作者在文档中做得很好),但有一种更简单的方法。

从你所知道的东西开始,然后从那里开始执行,看看会发生什么。

在我们的情况下,我们知道玩家通过按下 Dpads 或拇指杆之一来执行传送,具体取决于他们是使用 Vive、Oculus 还是其他设备。这将被映射为一个输入。让我们找到它:

  1. 打开设置 | 项目设置 | 引擎 | 输入,并展开动作映射扩展器。

这里有一个名为 TeleportRight 的输入听起来很有希望。如果我们展开它,我们可以看到它被映射到右拇指杆或 FaceButton 1(在 Vive 上是 Dpad 的顶部象限)。就是这个:

现在我们有一个要查找的输入名称,我们将在蓝图中搜索 TeleportRight,我们可能会找到一些东西。(有些项目在本机 C++环境中处理输入,但在蓝图中处理输入更为常见。)

  1. 跳转回你的事件图并按下 Ctrl + F 来打开查找结果面板。

  2. 在搜索框中输入TeleportRight,然后点击框右侧的放大镜符号以在所有蓝图中运行搜索:

就是这样。我们的角色正在处理这个输入:

当你在寻找输入时,另一个有用的策略是在搜索框中直接输入 inputaction。任何使用项目的输入设置(写入 DefaultInput.ini)映射的输入都会以这个前缀开头。

  1. 双击 InputAction TeleportRight 的条目,你将进入事件图中的事件处理程序:

现在我们有东西可以看了。我们可以使用断点来确认我们正在查看正确的内容。

设置断点和跟踪执行

我们将使用断点来验证当我们触发输入时,我们认为将执行的代码是否真的执行。这是一种理解他人代码的常用技术。当你对其执行路径不确定时,在你预期会被触发的位置设置断点,然后看看哪些断点真正被触发。这将为你开始探索软件提供一个起点:

  1. 选择 InputAction TeleportRight 节点,按下F9在其上设置一个断点,或者右键点击并从上下文菜单中选择切换断点:

当蓝图节点上有断点时,它会指示编辑器在达到包含断点的节点时暂停蓝图的执行。然后,你可以逐步执行每个动作并查看蓝图正在做什么。现在让我们来测试一下。

  1. 在 InputAction TeleportRight 上仍然设置断点的情况下,启动 VR 预览会话(你不需要真的戴上头盔,我们将在一秒钟内退出它),并激活右侧传送输入。

游戏应该会看起来冻结在那里,你的 VR 头戴式显示器将停止显示环境。

  1. 现在看一下 InputAction TeleportRight 节点。你会看到一个红色箭头,表示蓝图模拟已在此节点处暂停:

让我们也注意一下这里的其他一些事情。你可以看到蓝图显示被黄色指示器环绕,表示它当前正在模拟,并且从标题行可以看出,图表当前处于只读状态。在模拟蓝图时,你不允许更改蓝图:

让我们也来看一下出现在工具栏上的执行控制:

  • 恢复按钮将恢复正常执行。(在运行 VR 时这是有风险的——你的头戴式显示器可能无法从暂停状态正确唤醒。)

  • 帧跳过按钮允许执行一帧并返回到暂停状态。

  • 停止按钮将关闭你的编辑器中播放PIE)会话并返回到编辑器。

  • 查找节点按钮将带你回到当前停止执行的节点。

这三个节点是用于逐步执行代码的重要节点,你应该记住它们的快捷键,因为你会经常使用它们:

  • Step Into(F11)步进到下一个执行的节点,并且如果该节点表示蓝图函数调用或宏,则跳转到函数的实现。

在我们继续之前,让我们看一下它的运行情况。

现在按下F11。看看我们现在跳转到了 Switch on MovementMode 节点:

  1. 将鼠标悬停在 Switch on MovementMode 节点的选择输入上。悬停提示显示输入的类型和当前值:

我们可以看到 Movement Mode Right 当前设置为 Teleport,所以 switch 语句的第一个分支将执行。

  1. 再次按下F11,执行步进到Branch语句。

悬停在其输入值上,我们可以看到,因为我们没有手爬行、离开身体或处于禁止移动状态,所以这个值为 false,将执行 false 分支。

  1. 再次按下F11,我们如预期地跳转到了 SetTeleporterActive 节点。

  2. 再次按下F11,这次发生了一些有趣的事情。我们没有跳转到事件图中的下一个节点,而是跳转到了Set Teleporter Active函数内部。

这是 Step Into(F11)和 Step Over(F10)之间的区别。Step into会带你到执行的任何地方,甚至是函数调用或宏,而F10会跳过函数调用而不进入其中。

  1. 继续按下F11,直到我们进入“Is Valid”宏的内部。

我们实际上对这个宏的内容不感兴趣,所以我们想要跳出来,这样我们就可以继续查看我们的SetTeleporterActive函数。

  1. 按下Alt + Shift + F11,或者点击“步出”按钮返回到SetTeleporterActive图中。

现在你已经看到了这三个导航操作的实际效果。练习它们并熟悉使用它们的快捷键。像这样逐步查看蓝图是看到复杂蓝图运行方式最快、最有效的方法之一。

记住以下内容:

  • F11(步入)跳转到下一个执行的节点,即使它在另一个函数或宏内部。

  • F10(步过)在当前上下文中跳转到下一个执行的节点,但不会进入从该上下文调用的函数或宏中。

  • Alt + Shift + F11(步出)从函数或宏中退出到调用它的上下文。

记住这些快捷键。你会为此感到高兴的。

这些快捷键——F9切换断点,F10步过,F11步入,在 Visual Studio 中跟踪 C++代码时也基本上以相同的方式工作,相同的一般技巧——找到代码中的已知点,设置断点,然后逐步查看它的工作方式,并在那里应用它。在 Visual Studio 中使用Shift + F11从一个方法中步出。

  1. 按下F11,直到执行跳转到Activate Teleporter方法中。

看一下你的标签栏,你会发现你现在已经跳转到了一个完全不同的类中。VRExpansion 插件的示例项目使用一个名为BP_TeleportController的独立蓝图角色来处理绘制传送光束和目标指示器。这是有用的信息。

这也是设计这个系统的聪明方式。将这样的系统捆绑到自己的对象中,可以更容易地在长期运行中进行替换,将其添加到新的角色类中,或者在需要调试时找到你要找的东西。你在这里看到的是一种更高级的组织原则,但学会用这些术语思考是值得的。

查看执行跟踪

假设我们正在逐步查看蓝图,并意识到我们需要跳回几个步骤来查看是什么值驱动了一个分支或一个开关。为了做到这一点,我们可以利用调试面板的执行跟踪:

  1. 选择“窗口”|“调试”以打开调试面板。

  2. 展开面板的执行跟踪部分。

  3. 继续逐步查看你的蓝图,并观察这里发生了什么:

执行跟踪将构建一个面包屑列表,显示我们在执行过程中已经经过的部分。每当你需要重新访问以前的执行步骤时,你可以点击它,然后你将被带到图表的那个部分,你可以看到驱动它的输入和它产生的输出。

这是学习新蓝图的最有效的方法之一:设置断点并查看它的运行方式。通过这种方式,你将对类的构建方式有一个非常清晰的认识。

随着你在开发职业中的进步,并且擅长于解决和利用现有代码,你可能会惊讶地发现有多少开发人员因为未能有效地学会这样做而束缚了自己,并最终以困难的方式完成任务,如果他们真的能完成的话。你会发现,其中一些是开发人员所谓的“非自创”综合症(通常是一种将工作视为自我而掩盖的恐惧),而另一些则是简单的缺乏知识。你花在研究和学习已经解决的关于你要解决的问题的内容上的时间永远不会浪费。

使用调试窗口管理断点

我们马上要进行另一次探索,但首先,我们要清除 Vive_PawnCharacter 蓝图中的断点:

  1. 点击停止按钮结束模拟并返回编辑器。

  2. 切换回 Vive_PawnCharacter 蓝图,如果它还没有打开,请选择窗口 | 调试。

这一次,我们对此面板上显示的断点列表感兴趣:

在此屏幕截图中,我添加了一些额外的断点以使示例更清晰。

您可以单击列表中的任何断点以跳转到蓝图中的位置,并可以右键单击禁用或删除断点。

禁用断点会关闭断点而不删除它。如果您想暂时省略断点但仍然希望能够稍后重新启用它以进行进一步调试,这将非常有用。

您还可以通过选择蓝图节点并按下F9来切换任何断点的开启或关闭状态。

现在让我们将它们全部清除出我们的类:

  • 点击调试 | 删除所有断点(或使用Ctrl + Shift + F9)。

这将删除我们之前在输入动作上设置的断点,以及在此蓝图中设置的任何其他断点。此菜单还提供了禁用和启用类中所有断点的选项。

使用调用堆栈

现在让我们进行另一个实验。我们已经看到了如何在输入事件上开始逐步执行以查看调用事件时会发生什么,但是如果我们对特定函数感兴趣,并且想要查看它何时被调用以及由谁调用呢?我们有一些强大的工具可以帮助我们。

假设我们在游戏中看到了一个相机淡入的情况,并且我们想找出是谁在调用它。也许我们甚至不确定调用的名称是什么,但我们猜测它可能包含单词fade

  1. 按下Ctrl + F激活查找结果窗口,然后在搜索栏中键入fade

  2. 使用望远镜在所有蓝图中查找:

我们可以看到这里有很多条目,但大多数都是变量。显然,迷雾表中的东西不是我们要找的,但是 Vive_PawnCharacter 中的这些 Start Camera Fade 调用看起来很有希望。

  1. 双击第一个“Start Camera Fade”条目,跳转到图表中的位置,并按下F9在其上设置断点。

  2. 对其他三个重复此操作。

  3. 启动 VR 预览会话并激活传送。

执行将停在一个“Start Camera Fade”节点上。不过,这一次,我们不想逐步执行代码以查看接下来会发生什么,而是想看看我们是如何到达这里的。

  1. 点击窗口 | 开发人员工具 | 蓝图调试器以打开蓝图调试器。

您将看到显示的三个选项卡中的第一个标签为调用堆栈:

调用堆栈是一个列出了导致当前执行暂停的所有事件和函数的列表。这为您提供了大量的信息。调用堆栈的顶部表示当前执行暂停的位置,其下方的条目是调用它的函数或事件。再下方的条目是调用该函数的内容,依此类推。

查看此堆栈,我们可以看到一个 C++例程检测到按钮按下并触发了 InputAction TeleportRight。然后,从事件图表中进行了调用。让我们双击调用堆栈中的此条目以查看它:

这是由输入动作的 Released 事件触发的 Execute Teleportation 调用。

我们可以双击下一个调用——ExecuteTeleportation事件,并查看导致我们寻找的相机淡入的图表。

这是一种强大的技术,您应该养成使用它的习惯。

有关使用虚幻蓝图调试工具的更多信息,请查看这里:docs.unrealengine.com/en-us/Engine/Blueprints/UserGuide/Debugging

使用此工具在蓝图中进行一些探索,然后点击停止返回编辑器。

查找变量引用

回到我们的传送示例,如果我们想知道是什么改变了驱动switch语句的Movement Mode变量,那么怎么办呢?

这很容易做到:

  1. 选择 Movement Mode Right 变量。

  2. 右键点击它并选择 Find References,或者按下Alt + Shift + F

我们可以看到这个变量在很多地方被使用,但只在两个位置被设置。这就是我们感兴趣的:

  1. 双击 Find Results 中的 Set MovementModeRight 条目之一。

这将带我们到设置这个变量的位置,我们可以看到这是在一个名为Cycle Movement Modes的函数中进行的。然后我们可以使用我们学到的策略来查看这个函数何时以及如何被调用,以及与之相关的其他事情。

你可以使用Alt + Shift + F来查找函数和变量。练习一下。

理解别人的代码就像解开一个复杂的结。如果你试图一次理解所有内容,你会让自己感到沮丧。相反,你找到一根单独的线,开始跟随它并解开它,随着你的进行,它的结构就会变得清晰起来。这些工具可以帮助你做到这一点。

使用更多的 VRExpansion 插件

VRExpansion是一个庞大的插件,为 VR 开发者提供了很多功能。现在你已经有了一些探索它、弄清楚它的工作原理以及如何使用它的策略,你将能够释放巨大的潜力。

除了我们刚刚探索的角色之外,这个插件还提供了一个支持 VR 的玩家控制器、一个 AI 控制器、立体小部件、按钮、杠杆等等。

如果你想更好地了解这个插件包含了什么(这远远超出了本章的范围),在内容浏览器中点击 View Options 弹出菜单,打开 Show Plugin Content,并确保 Show C++ Classes 可见:

浏览类目录,看看里面有什么。如果你双击其中任何一个类,它的原始源代码将在 Visual Studio 中打开。

你最好的资源之一是 UnrealEngine.com 上的 VR Expansion Plugin 论坛,网址在这里:forums.unrealengine.com/development-discussion/vr-ar-development/89050-vr-expansion-plugin

插件的作者 Joshua Statzer(mordentral)在论坛上非常活跃,并且周围有一群乐于助人的开发者社区,他们非常愿意帮助新开发者入门。

总结

这一章与我们到目前为止所做的教程有些不同,因为它的目的实际上是帮助你达到一个能够探索 Unreal 生态系统中的众多插件、模板、示例和其他项目,并学习如何使用它们来加速你的工作和学习新技术的能力。这是你作为开发者可以自学的最有价值的技能之一。如果你能够熟练地探索在外部找到的代码,你将能够在更短的时间内开发出更强大的软件,并通过看到经验丰富的开发者如何解决你正在尝试解决的问题来学习更高级的技术。这将使你成为一个更好的开发者。

第十二章:从这里往哪里走

真诚地祝贺你。

通过达到这一点,你已经做到了许多有抱负的开发者从未做到的事情,这是一项了不起的成就。作为 VR 创作者,你可以培养的最有价值的资产就是你的坚持和愿意付出努力的态度。通过完成这本书,你已经展示了对事业的奉献和渴望,这将使你与众不同。请花一点时间认识到自己身上的这种品质。

你可能已经注意到,我们在写这本书时的重点与许多其他教程不同。有很多材料可以告诉你应该按哪些按钮,但是这里有一点-知道这些东西并不能让你成为一个好的开发者。你通过学习如何思考你试图解决的挑战以及如何使用你手头的工具来解决它们而变得擅长。为此,我们多次偏离讨论为什么某件事情的工作方式或者它还可以如何使用。在继续自我教育的过程中继续思考这个问题。花时间学习“为什么”将使你走得更远,而不仅仅是学习“什么”。后者可能会随着下一个软件更新而改变,并且通常不适用于其他任何事情。我们做事情的根本原因往往不会改变。训练自己认识到这一点,要知道一件事,你必须能够描述为什么它是这样的。在本书的过程中,我们试图让你习惯思考这些“为什么”的问题,并为你提供一些工具,让你自己更好地区分这些问题。

我们希望你在阅读本书时能够获得很多对你开发 VR 有用的东西,但是,老实说,如果你只能从中得到这两个认识,那也是值得的:

  • 成功来自于坚持和愿意付出努力

  • 关注“为什么”而不是“什么”

在本书的过程中,我们还试图展示另一个重要的真理:开发是迭代的。我们一起做的许多例子都涉及解决问题并在发现新问题或更好的解决方法时改变我们的解决方案。我们试图通过这样做来重现开发者解决问题时的真实思维方式。这是一个逐步进行的过程。通过专注于下一步,做好完成下一步所需的工作,并在不清楚下一步应该是什么时不放弃或沮丧,你可以创造几乎任何你想象的东西。

你会发现自己不断地问自己为什么会得到你所看到的结果,或者为什么你需要解决的问题可能会发生。成为一名有效的开发者不是记住很多东西的问题,而是建立有效的思考方式,以便你能够找到解决问题的方法。

牢记这一点。你正在踏上一个新的领域,为一个规则和词汇几乎完全未知的媒介进行开发,我们都在一起学习什么是有效的,以及我们如何帮助 VR 成为我们感知的艺术形式。你将面临一些没有已知答案的挑战,因为它们还没有被解决。也许你将成为第一个发现它们的人。

最后的话

当我们写这篇文章时,VR 正处于十字路口。

在 Oculus Rift 和 HTC Vive 发布后的几年里,VR 引起了一阵热潮和兴趣,然后随着时间的推移,这种兴趣逐渐平稳下来。许多人拿出他们的 VR 头盔参加派对或演示,然后再把它们收起来-为什么呢?

好吧,这是因为几个原因。

首先是技术问题,我们需要对自己诚实,这是第一代技术。使这些头戴式显示器工作的实际情况令人惊讶,但它们有局限性。这项技术的第一代对于主流消费者来说过于昂贵和难以设置。长时间佩戴头戴式显示器不舒适,镜片和屏幕的限制使用户意识到他们使用的技术。视野狭窄,镜片稍微错位就会模糊图像,低分辨率的屏幕会显示像素边界(可怕的“屏幕门”效应)。硬件开发人员知道这一点,但他们也知道,克服新兴技术的成长痛苦的唯一方法就是逐步解决问题。

展望几年后,你会看到这些问题迅速消失。屏幕分辨率只会不断提高,同时屏幕变得更轻,需要更少的功率。光学波导很可能取代我们现在使用的笨重的菲涅尔透镜,大大减轻头戴式显示器的重量,并扩大其视野。外部传感器很快将成为过去,因此设置 VR 头戴式显示器不再需要占用三个 USB 端口或在墙上安装基站。越来越好的独立头戴式显示器和可以无线与基础 PC 通信的 PC 头戴式显示器很快将成为常态。这些都是可以解决的问题,合理地期望它们很快不再是重要问题。

然后是模拟器晕动病的问题。在 VR 淘金热的早期,开发人员对此通常非常天真,或者变得偏执。通常,后者是对前者的回应。我们都拿到了我们的原始 DK1 和 DK2,并立即开始制定与平面屏幕上相同的运动方案,结果让我们感到不适。然后我们把摆锤摆向了另一个方向,到处传送。两个极端都不是必然的。我们正在了解更多关于如何移动玩家视角的方法,并鼓励您也进行实验。有解决方案可以奏效。另一个现实是,人们会适应模拟器,就像他们适应在海上船上一样。在 20 世纪 90 年代初,第一人称射击游戏也会引起晕动病。现在不再发生这种情况,部分原因是渲染技术改进了,但也部分原因是玩家习惯了。VR 也正在发生类似的效应。要意识到模拟器晕动病,并设计软件来减轻其影响,但不要过于恐惧:探索。找出其他可能奏效的方法。早期电影制片人必须弄清楚他们如何摆脱摄像机移动的限制,以及哪些剪辑会奏效。这是同样的情况。我们将通过实验来解决这个挑战。前沿不是受规则束缚的地方。我们还不了解它们。

尤其是最后一个挑战是你的机会。我们真的不知道我们可以用这项技术做什么。任何告诉你我们已经解决了这个问题的人都没有发挥他们的想象力。我们才刚刚开始。

在 Rift 和 Vive 首次发布后的淘金热潮中,大多数开发人员将这个媒介视为他们已经了解的媒介的升级版。电影人将其视为 3D 电影设备,游戏人将其视为沉浸式游戏设备。两者都没有完全正确。

这种媒介的语言大部分还未被发现,这使得这个领域成为一个令人兴奋的游戏场所。我们曾多次建议您质疑那些试图告诉您在虚拟现实中不能做什么的人,而在这里,我们希望更进一步。故意打破那些界限。尝试那些您知道不会奏效的事情,因为百分之一的机会,它们可能会奏效。在探索虚拟现实中,将积极的思考和想象力融入到您的工作中。让这种媒介向您展示它的工作原理。我们坚信,虚拟现实或其某种变体将成为 21 世纪的决定性媒介。如果您真的仔细思考,很难有其他令人信服的观点。沉浸感和存在感的好处太明显了。但我们需要弄清楚这些东西是如何真正工作的。我们了解一些基础知识,但远未掌握全部。早期观看卢米埃尔兄弟的电影中火车进站的观众很难想象电影最终会发展成什么样子。接受这种情况也同样适用于这里。我们只是刚刚开始了解我们可以用这种媒介做什么,也只是刚刚开始弄清楚我们如何做到这一点。这个探索中最令人兴奋的部分还在前方。请看下面的图片:

《拉西奥塔车站的火车进站》(L'Arrivée d'un Train en Gare de la Ciotat),奥古斯特和路易斯·卢米埃尔兄弟的一帧画面

在这本书中,我们真的尽力为您提供一个基础,让您开始这个冒险。我们希望为您提供关于虚拟现实作为一种媒介的基础知识,但我们也希望以一种能够激励您进行开发和思考虚幻引擎的方式来帮助您进行探索。我们真诚地希望我们能对此有所帮助。虚拟现实对我们来说很重要,显然对您也是如此,否则您就不会读完这本书。让我们创造一些艺术吧。

衷心感谢您与我们一同踏上这个旅程。

第十三章:有用的思维技巧

在本书中,我们一直在讨论开发思维的方式,并且曾经一两次提到,区分有效的开发者和业余开发者的因素不是他们知道什么,而是他们如何思考。

有效思考是一种技能,就像其他技能一样,它可以通过实践来提高。正如我们在上一章中所看到的,通过分解问题并找到一个起点,你可以简化一个复杂的问题。现在让我们花点时间来探索一些更多的技巧,这些技巧可以帮助你。

橡皮鸭调试

你可能以前听过这个短语:“橡皮鸭调试”。它已经存在很长时间了,是你可以执行的最有效的解决问题的技巧之一。这里的想法很简单。找一个愿意倾听的人,如果周围没有人,桌子上的一个橡皮鸭也可以(因此得名)。大声地、用简单的语言描述你要解决的问题。这迫使你组织关于问题的思考。如果你发现自己无法用清晰简单的语言描述问题,那么你还没有理解它。你还没有准备好尝试回答这个问题,因为你还不清楚你需要问什么问题。尝试并探索它,直到你真正能用简单的语言表达出来。通常,仅仅这个过程就会给你提供一个清晰的解决方案,如果不行,你现在有更好的机会找到解决方案,因为你现在知道你在问什么问题。大声说出来或写下来。当你让它在脑海中翻滚时,很容易保持模糊和忽略细节。强迫自己把话放在一起。你会惊讶于这种技术的强大和有效。

只说事实

当你意识到软件出现问题时,写下你所看到的发生的事情。不是你认为你看到的事情,只是你看到的事情。在调试中,很容易对为什么会发生某事做出结论,然后在真正确定你实际遇到的问题之前,就开始试图解决这个问题。退一步,只看你能具体观察到的事情。

像这样思考:“当火炬生成时,它出现在错误的位置”,而不是“生成例程将物体放在错误的位置”。你还不知道那个。你只知道一个火炬不在你期望的位置。做一个实验。生成一个不同的物体。它是否出现在正确的位置?好的,那么也许你的模型有一个奇怪的偏移量。另一个物体也不在正确的位置?好的,那么,是生成例程的问题。或者可能是你的关卡中的某个碰撞导致物体无法生成在你想要的位置,并将它们推到最近可用的位置。尝试移动生成点,看看是否会改变事情。

看到我们在这里做什么了吗?我们正在将基本的科学方法应用于我们正在解决的问题。我们看到了什么?我们能想到可能引起这个问题的原因吗?我们如何测试它以确定我们是否正确?我们的测试给我们带来了什么新信息?我们是否已经知道足够的信息来解决问题了?

很容易就会得出结论并浪费很多时间来调试错误的问题。花时间退一步将有助于避免这种情况,并避免盲目尝试。通过这种方式,你将解决问题。

用积极的方式描述你的解决方案

我们在第十章《在 VR 中创建多人体验》中稍微谈到了“模仿神秘仪式的编程”,并让你承诺不要这样做。为了进一步阐述这个想法,让我们看一下一个我们希望你永远不要说出口的短语:“它能用——别碰它!”

这不是我们描述一个健壮系统的方式。如果你感到诱惑说这样的话,恭喜你!你已经完成了“让它工作;让它正确;让它快速”的过程的第一步,这意味着你还没有完成。你所创建的是你最终解决方案的一个成功示例,但现在是时候开始“让它正确”的阶段了。当你能够用积极的方式回答关于你的解决方案的三个问题时,你已经满足了这个开发阶段的要求:

  • 它需要解决 X 问题

  • 我知道它解决了 X 问题,因为……

  • 我知道这是安全的,因为……

记住这三个陈述。不要跳过它们。你应该能够用清晰简单的语言描述你想要做的事情。(你的橡皮鸭也是一个好的倾听者。)你应该能够解释为什么你刚刚做的事情能够解决你想要做的事情,并且你应该能够用积极的方式来做到这一点:我们需要确保玩家在暂停菜单出现时不能开火。这通过输入处理程序在调用开火函数之前检查暂停状态来解决。最后,你应该能够解释为什么这样做是安全的:我知道这是安全的,因为我们确保在暂停状态开始时清除任何现有的输入,并且输入处理程序知道只允许取消暂停命令通过。

自律地做到这一点,并使用清晰、积极的术语。如果你含糊其辞,那就是在逃避你需要解决的问题。养成这样的习惯:描述你想要做的事情,为什么你知道你的解决方案能够做到这一点,以及为什么你知道这样做是安全的。这样做可以在软件中出现之前阻止很多错误。

在编写代码时,计划如何维护和调试你的代码

技术债务是一种隐形的东西。这个术语描述了修复代码中留下的混乱所带来的下游成本,通常是由于匆忙开发而导致的。它是一个项目杀手。

比如说,你需要为演示准备一些东西,但时间不够,所以你采用了一个临时解决方案。然后,你把它留在那里,并在其上构建了一堆额外的系统。现在,你想要将你的游戏上线,但你所做的事情在客户端上显示不正确,你恐惧地意识到你必须重写那些在演示中使用的临时解决方案上构建的所有系统,更糟糕的是,你意识到这将需要数周的工作。

或者,假设你在匆忙构建蓝图时,它能够工作,但看起来像排水管中的一团头发。你继续解决下一个问题。六个月后,你准备向出版商演示游戏,但系统中一直出现奇怪的错误。自从你写下它以来,你就没有再看过它,现在你被迫花费整天整夜来解决你制造的混乱,以便找出错误发生的位置。

在这两种情况下,如果你在事情还新鲜在脑海中的时候花时间整理你的工作,你将节省很多时间和痛苦。如果你绝对必须进行临时解决,将其标记为临时解决,并且如果你知道真正的解决方案应该是什么,就在临时解决方案旁边写入注释。然后在构建其他东西之前修复它。如果你已经运行了一个蓝图或一段代码,那么在你的“让它工作”阶段的一切都还新鲜在脑海中时,直接进入“让它正确”的阶段。

记住这个真理:“代码的绝大部分生命周期都是用来维护和调试的”。把这个铭记在心。调试代码比第一次编写代码要困难得多,所以尽可能给自己创造优势。如果你在第一次编写解决方案时匆忙行事,那么你并没有节省时间。你只是在它的生命周期的一小部分上节省了时间,却给自己带来了长时间的大麻烦。在编写代码时要为其进行调试做好计划。你会为此感到高兴的。

倾向于简单的解决方案

不幸的是,你会遇到那些写复杂、难以理解的代码或蓝图的开发者,他们错误地试图用这种方式给其他人留下智商有多高的印象。他们暗自幻想着其他人会看着他们难以阅读的代码,然后想,“哇!他们一定很聪明!我一个字都看不懂。”拜托,请不要成为这样的开发者。

那些你真正想要得到尊重的经验丰富的开发者不会对一个混乱的蓝图或晦涩难懂的代码印象深刻。他们会想知道为什么你把它留在这样的混乱中,并认为这是因为你不知道更好的方法。业余者写出难以阅读的代码,而专业人士知道他们将来一年都要维护这些代码,而且他们不想让这项工作变得更加困难。

如果你的“做对”草稿比你的“让它工作”草稿更简单、更清晰,那么你就知道你做得很好了。

在你创造之前先查一下

我们在《第十章》中提到了这一点,即在 VR 中创建多人体验,并希望在这里重申:新开发者常犯的一个核心错误是在解决问题之前没有进行研究,结果他们不得不重写已经存在的代码。

做好功课。当你试图解决一个问题时,在你开始动手之前,看看是否有其他人解决过类似的问题并留下了足迹。引擎中是否已经有一个可以完成这个任务或大部分任务的工具?模板或示例项目中是否有示例展示如何解决这个问题?是否有人在某个地方写了一个教程?有时候,答案可能是否定的,但更常见的情况是,你会找到一些直接指向解决方案或让你更接近解决方案的东西。

我们曾经看到一个小团队的工程师们在一个已经通过一个自由许可插件的单个函数解决的问题上浪费了几周的开发预算。这是没有用于改进游戏的时间,你不需要陷入这个陷阱。研究是你开发过程的一部分,应该在你开始输入或拖动节点之前进行。

这就引出了我们下一个讨论主题:当你需要找到信息时,你可以去哪里寻找?

第十四章:研究和进一步阅读

在开发软件时,浏览器和搜索引擎是你最好的朋友之一。如果你掌握了有效搜索的技巧,并记下了一些有用的搜索起点,你的学习速度将会大大加快。软件开发变化非常快,而实时虚拟现实等尖端软件开发变化更快。你花在学习如何有效搜索信息的时间将会永远为你服务。

让我们谈谈一些开始的地方。

虚幻引擎资源

当然,关于虚幻引擎的信息,最重要的地方之一就是源头。每当你需要新的信息时,www.unrealengine.com 应该是你首先查看的地方之一。你可以通过浏览器访问,或者在 Epic Games 启动器中找到信息。

以下是一些必备的虚幻引擎链接:

  • 虚幻引擎 4 文档docs.unrealengine.com/en-us/ - 无论何时你在使用新东西,都要先阅读相关页面。

  • 虚幻引擎论坛forums.unrealengine.com/ - 这里有很多有用的信息,以及一个庞大的论坛用户群体愿意帮助其他人解决问题。加入进来并做出有建设性的贡献。有了周围的社区,你作为开发者会更快成长。

  • UE4 AnswerHubanswers.unrealengine.com/index.html - 当你面临一个具体问题时,在这里搜索答案。如果找不到,就提问。关键词是“具体”。如果你问“我如何使用虚幻引擎?”这个问题明显表明你没有做好功课,所以你会被忽视。好问题会得到好答案。也要愿意回报。如果你看到一个你知道答案的问题,就加入进来帮助解答。

  • 虚幻学院academy.unrealengine.com/ - 这是一系列专注于特定主题的教程,无论是在引擎内部还是在专业世界中。它们通常采用一系列视频课程的形式,质量始终很高。这是拓宽和提升你技能的最佳地方之一。

  • 虚幻引擎 YouTube 频道www.youtube.com/channel/UCBobmJyzsJ6Ll7UbfhI4iwQ - 这是许多新开发者忽视的另一个资源,但你不应该忽视;它很重要。事实是:虚幻引擎非常庞大,有数百名工程师在开发,还有成千上万的社区成员,它不断发展壮大。因此,引擎中有大量非常有用的东西,但由于它们太新或太专业,还没有在任何地方记录下来。找到这些东西的秘诀就是虚幻引擎频道上的“实时培训”视频。这些视频几乎总是由编写相关系统的工程师或非常了解该系统的培训师提供的,它们是非常有用的信息源。如果你真的想学会如何使用这个引擎,这是你应该去的地方。

  • 用户群组www.unrealengine.com/en-US/user-groups - 参与你所在地区的真实人群社区。找到聚会和活动,然后去参加。这是我们看到新开发者经常忽视的最大秘密 - 他们不把自己放在世界上。无论你是在寻找合作者,寻找工作,还是寻找雇佣某人,通过走出去并参与社区活动,你为自己做了一项重要的服务。

对于更一般的编程问题,最好的资源之一是Stack Overflowstackoverflow.com/)。它不是以 Unreal 为中心,但如果你在寻找关于 C++开发的信息,这里是你能找到一些最有经验的开发者的地方。不过要注意,Stack 社区对低质量问题非常不耐烦。尊重每个人的时间,在自己尽力寻找答案之后再提问。描述你尝试做什么,你做了什么,以及你面临的挑战。这样做,你将得到一些在网上最可靠的专家建议。

虚拟现实资源

获取关于虚拟现实的好信息是很困难的。每个人都还在摸索这种媒介,你读到的几乎所有东西都是合法信息和无根据的观点和传说的结合。这不是谁的错 - 这种媒介对我们来说太新了,我们无法完全理解。

我们在本书开始时建议您对虚拟现实的任何被接受的智慧提出质疑,并且我们想在这里重申一下。请记住,最早的存世运动影片是在 1888 年制作的,直到 1925 年,谢尔盖·爱森斯坦才真正找到了电影剪辑的语言。花了 37 年时间才弄清楚这个流派最基本的方面。《公民凯恩》在此之后 16 年才出现。当有人告诉你在虚拟现实中你不能做什么时,请记住这一点。我们对这种媒介的限制一无所知。投入其中并尽情玩耍。你正在开拓一个新领域。不要害怕尝试你认为可能行不通的奇怪事物。这就是发现的方式。

关于偶尔会在您的信息源中出现的博客作者们宣称虚拟现实“已死”的问题,我们简单说一下:这不是新的沟通方式在社会中传播的方式。移动电话成为普通消费设备花了近 30 年的时间,个人电脑则更长。这次也不例外。在撰写本文时,我们仍然处于第一代消费者虚拟现实头盔阶段,配备了笨重的镜片和线缆,视野狭窄,需要外部传感器。(在撰写本段时,Oculus 宣布推出首款具有内置跟踪的台式机头盔。)我们在硬件开发的早期阶段,仍然面临许多技术挑战,但任何真正关注的人都能看出这些挑战将被克服。我们无法知道当您阅读本文时,我们是否会进入另一个“虚拟现实已死”的阶段,或者另一个虚拟现实热潮,或者希望处于中间状态,但请从现实的角度考虑这个问题。当 XR 头盔像眼镜一样舒适,不需要线缆,电池可以持续一整天,并且能提供沉浸式视野时,我们都会使用它们。这种媒介不会消失。从你热爱它的角度来工作。

除了这些建议,这里还有一些我们推荐您查看的资源。

  • Oculus VR 设计资源developer.oculus.com/design/ - Oculus 的《虚拟现实最佳实践》文档,虽然在撰写本文时有点过时,但仍然是关于设计虚拟现实时需要考虑的最好的综合指南之一,《开发者视角》系列应该被视为任何设计虚拟现实的人的必读资料。

  • Road to VRwww.roadtovr.com/ - 这是最长时间运营和编辑最专业的虚拟现实新闻来源之一。他们对这种媒介非常认真,也知道他们在谈论什么。请务必定期阅读。

  • Upload VRuploadvr.com/——这是另一个真正好的 VR 信息来源,也是获取 VR 硬件和软件行业状况信息的好地方。

以下是一些值得寻找和倾听的人:

  • 贾伦·兰尼尔是一位计算机科学家、作家和作曲家,他是 20 世纪 80 年代 VR 发展的关键先驱。他是我们称之为"虚拟现实"的原因。很难找到地球上有人比他更多地思考和使用这种媒介的。

  • Michael Abrash是 Oculus 的首席科学家,一直是其未来发展的坚定声音。在 YouTube 或其他地方寻找他的演讲。

  • John Carmack,《毁灭战士》和《雷神之锤》的共同创作者,现在在 Oculus 工作,是推动 VR 发展的首席工程师之一。如果你想了解当前技术的现状和即将到来的发展方向,你可以相信他是一个非常坦率和深入了解的信息来源。

  • 马歇尔·麦克卢汉是 20 世纪最有影响力的思想家之一,关于媒体和传播方式如何塑造我们的社会。无论你以前是否听说过他的名字,你都已经在他的思想海洋中游泳了。"媒介即信息"——这就是他的观点。"全球村庄"——也是麦克卢汉的观点。他的主要作品《理解媒体》有时很难阅读,但它确实改变了我们对电子媒体在世界中的角色的看法。我们在这里提到他,是因为在 VR 领域工作就是在一个沟通技术的前沿工作,通过其沉浸和存在感的力量,它有可能比以往任何事物更深刻地影响我们。麦克卢汉对 VR 可能会产生的影响会感到着迷,但也可能感到恐惧。为什么不思考一下这可能意味着什么呢?

有很多其他的资源我们可以引用,但实际上,要写一份关于 VR 这个新兴且变化快速的主题的详尽资源清单几乎是不可能的,而且这也可能是徒劳的,因为在这个领域,五个月前的信息已经被认为是过时的。把这些资源当作起点,但要明白它们远远不完整。让你自己的探索带你去任何地方。

posted @ 2024-05-15 15:27  绝不原创的飞龙  阅读(46)  评论(0编辑  收藏  举报