UE4 VR 项目(四)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:在 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 生态系统中的众多插件、模板、示例和其他项目,并学习如何使用它们来加速你的工作和学习新技术的能力。这是你作为开发者可以自学的最有价值的技能之一。如果你能够熟练地探索在外部找到的代码,你将能够在更短的时间内开发出更强大的软件,并通过看到经验丰富的开发者如何解决你正在尝试解决的问题来学习更高级的技术。这将使你成为一个更好的开发者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值